M3.58: gradient libraries; Swatch Options renames gradient swatches; Alt-drop replaces a swatch
Window > Swatch Libraries > Gradients lists five generated gradient
libraries (Spectrum, Sky, Neutral incl. fades to transparent, Duotone,
Radial Glows); the library panel's menu lists them too.
Swatch Options on a gradient swatch shows its name and the gradient; OK
renames it. Alt-dropping an edited gradient (or a colour) on a swatch of its
kind in the Swatches panel replaces it, via swatch.edit {paint}.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
786f2c05d3
commit
3a79e6d922
|
|
@ -67,19 +67,32 @@ pub const SWATCH_LIBRARIES: &[BuiltinLibrary] = &[
|
|||
lib!("harmony-sets", "Harmony Sets", harmony_sets),
|
||||
];
|
||||
|
||||
/// Built-in library `id`, computed once.
|
||||
/// The built-in gradient libraries (Window → Swatch Libraries → Gradients), in menu order.
|
||||
pub const GRADIENT_LIBRARIES: &[BuiltinLibrary] = &[
|
||||
lib!("spectrum-gradients", "Spectrum Gradients", spectrum_gradients),
|
||||
lib!("sky-gradients", "Sky Gradients", sky_gradients),
|
||||
lib!("neutral-gradients", "Neutral Gradients", neutral_gradients),
|
||||
lib!("duotone-gradients", "Duotone Gradients", duotone_gradients),
|
||||
lib!("radial-glows", "Radial Glows", radial_glows),
|
||||
];
|
||||
|
||||
/// Every built-in library: the swatch libraries, then the gradient libraries.
|
||||
fn builtins() -> impl Iterator<Item = &'static BuiltinLibrary> {
|
||||
SWATCH_LIBRARIES.iter().chain(GRADIENT_LIBRARIES)
|
||||
}
|
||||
|
||||
/// Built-in library `id` (swatch or gradient library), computed once.
|
||||
pub fn builtin_library(id: &str) -> Option<Arc<SwatchLibrary>> {
|
||||
static LIBS: OnceLock<Vec<Arc<SwatchLibrary>>> = OnceLock::new();
|
||||
let libs = LIBS.get_or_init(|| {
|
||||
SWATCH_LIBRARIES
|
||||
.iter()
|
||||
builtins()
|
||||
.map(|b| {
|
||||
let (swatches, groups) = (b.make)();
|
||||
Arc::new(SwatchLibrary { name: b.name.into(), swatches, groups })
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
SWATCH_LIBRARIES.iter().position(|b| b.id == id).map(|i| libs[i].clone())
|
||||
builtins().position(|b| b.id == id).map(|i| libs[i].clone())
|
||||
}
|
||||
|
||||
// ---------- colour construction ----------
|
||||
|
|
@ -287,6 +300,88 @@ fn metallic() -> (Vec<Swatch>, Vec<SwatchGroup>) {
|
|||
/// OKLCH `(l, c, h°)` anchors a scale passes through.
|
||||
type Anchors = &'static [(f64, f64, f64)];
|
||||
|
||||
/// A linear gradient through evenly spaced, opaque `colors`.
|
||||
fn even(name: &str, kind: GradientKind, colors: &[Color]) -> Swatch {
|
||||
let last = (colors.len() - 1).max(1) as f32;
|
||||
let stops: Vec<(f32, Color, f32)> = colors.iter().enumerate().map(|(i, c)| (i as f32 / last, *c, 1.0)).collect();
|
||||
gradient(name, kind, &stops)
|
||||
}
|
||||
|
||||
/// OKLCH `(l, c, h°)` colours.
|
||||
fn lch(points: &[(f64, f64, f64)]) -> Vec<Color> {
|
||||
points.iter().map(|&(l, c, h)| oklch(l, c, h)).collect()
|
||||
}
|
||||
|
||||
/// Full-hue sweeps and hue-neighbour blends.
|
||||
fn spectrum_gradients() -> (Vec<Swatch>, Vec<SwatchGroup>) {
|
||||
let wheel: Vec<Color> = (0..=6).map(|i| oklch(0.72, 0.4, 25.0 + i as f64 * 60.0)).collect();
|
||||
let mut s = vec![
|
||||
even("Spectrum", GradientKind::Linear, &wheel),
|
||||
even("Soft Spectrum", GradientKind::Linear, &lch(&[(0.88, 0.08, 25.0), (0.9, 0.08, 145.0), (0.86, 0.08, 265.0), (0.88, 0.08, 25.0)])),
|
||||
];
|
||||
// Each hue to the one two steps round the wheel.
|
||||
s.extend((0..HUES.len()).map(|i| {
|
||||
let ((a, ha), (b, hb)) = (HUES[i], HUES[(i + 2) % HUES.len()]);
|
||||
even(&format!("{a} to {b}"), GradientKind::Linear, &[oklch(0.7, 0.4, ha), oklch(0.7, 0.4, hb)])
|
||||
}));
|
||||
(s, vec![])
|
||||
}
|
||||
|
||||
fn sky_gradients() -> (Vec<Swatch>, Vec<SwatchGroup>) {
|
||||
let skies: [(&str, Anchors); 6] = [
|
||||
("Dawn", &[(0.45, 0.08, 280.0), (0.72, 0.12, 350.0), (0.9, 0.09, 75.0)]),
|
||||
("Noon", &[(0.55, 0.13, 250.0), (0.78, 0.09, 235.0), (0.95, 0.03, 220.0)]),
|
||||
("Sunset", &[(0.35, 0.09, 300.0), (0.62, 0.18, 20.0), (0.85, 0.15, 70.0)]),
|
||||
("Dusk", &[(0.25, 0.07, 275.0), (0.48, 0.1, 310.0), (0.7, 0.1, 30.0)]),
|
||||
("Night", &[(0.15, 0.04, 270.0), (0.3, 0.07, 265.0)]),
|
||||
("Overcast", &[(0.62, 0.015, 240.0), (0.86, 0.01, 230.0)]),
|
||||
];
|
||||
(skies.iter().map(|(n, p)| even(n, GradientKind::Linear, &lch(p))).collect(), vec![])
|
||||
}
|
||||
|
||||
fn neutral_gradients() -> (Vec<Swatch>, Vec<SwatchGroup>) {
|
||||
let (white, black) = (Color::gray(0.0), Color::gray(1.0));
|
||||
let s = vec![
|
||||
even("White to Black", GradientKind::Linear, &[white, black]),
|
||||
even("Light Grays", GradientKind::Linear, &[white, Color::gray(0.4)]),
|
||||
even("Dark Grays", GradientKind::Linear, &[Color::gray(0.6), black]),
|
||||
even("Warm Grays", GradientKind::Linear, &lch(&[(0.95, 0.012, 70.0), (0.3, 0.012, 70.0)])),
|
||||
even("Cool Grays", GradientKind::Linear, &lch(&[(0.95, 0.015, 250.0), (0.3, 0.015, 250.0)])),
|
||||
gradient("Fade to Transparent", GradientKind::Linear, &[(0.0, black, 1.0), (1.0, black, 0.0)]),
|
||||
gradient("White Fade", GradientKind::Linear, &[(0.0, white, 1.0), (1.0, white, 0.0)]),
|
||||
gradient("Vignette", GradientKind::Radial, &[(0.0, black, 0.0), (0.6, black, 0.0), (1.0, black, 0.7)]),
|
||||
];
|
||||
(s, vec![])
|
||||
}
|
||||
|
||||
/// Pairs of hues from opposite sides of the wheel, at different lightness.
|
||||
fn duotone_gradients() -> (Vec<Swatch>, Vec<SwatchGroup>) {
|
||||
let pairs = [
|
||||
("Coral", 0.7, 30.0, "Navy", 0.32, 265.0),
|
||||
("Plum", 0.38, 330.0, "Lime", 0.88, 125.0),
|
||||
("Teal", 0.5, 190.0, "Gold", 0.85, 90.0),
|
||||
("Indigo", 0.3, 280.0, "Peach", 0.86, 55.0),
|
||||
("Forest", 0.35, 150.0, "Rose", 0.8, 0.0),
|
||||
];
|
||||
let s = pairs
|
||||
.iter()
|
||||
.map(|&(a, la, ha, b, lb, hb)| even(&format!("{a} and {b}"), GradientKind::Linear, &[oklch(la, 0.15, ha), oklch(lb, 0.15, hb)]))
|
||||
.collect();
|
||||
(s, vec![])
|
||||
}
|
||||
|
||||
/// A bright centre fading to a transparent edge, per hue.
|
||||
fn radial_glows() -> (Vec<Swatch>, Vec<SwatchGroup>) {
|
||||
let s = HUES
|
||||
.iter()
|
||||
.map(|(n, h)| {
|
||||
let c = oklch(0.78, 0.2, *h);
|
||||
gradient(&format!("{n} Glow"), GradientKind::Radial, &[(0.0, Color::WHITE, 1.0), (0.25, c, 1.0), (1.0, c, 0.0)])
|
||||
})
|
||||
.collect();
|
||||
(s, vec![])
|
||||
}
|
||||
|
||||
/// The sequential scales of [`perceptual`]: name and anchors of rising lightness.
|
||||
const SEQUENTIAL: [(&str, Anchors); 5] = [
|
||||
("Ember", &[(0.22, 0.10, 300.0), (0.55, 0.18, 30.0), (0.93, 0.15, 100.0)]),
|
||||
|
|
@ -348,7 +443,7 @@ mod tests {
|
|||
use super::*;
|
||||
|
||||
fn all() -> Vec<Arc<SwatchLibrary>> {
|
||||
SWATCH_LIBRARIES.iter().map(|b| builtin_library(b.id).unwrap()).collect()
|
||||
builtins().map(|b| builtin_library(b.id).unwrap()).collect()
|
||||
}
|
||||
|
||||
/// Every component of every colour (solid or gradient stop).
|
||||
|
|
@ -370,10 +465,10 @@ mod tests {
|
|||
#[test]
|
||||
fn libraries_are_deterministic_unique_and_in_range() {
|
||||
let mut ids = std::collections::HashSet::new();
|
||||
for (b, lib) in SWATCH_LIBRARIES.iter().zip(all()) {
|
||||
for (b, lib) in builtins().zip(all()) {
|
||||
assert!(ids.insert(b.id), "duplicate id {}", b.id);
|
||||
assert_eq!(lib.name, b.name);
|
||||
assert!(lib.len() >= 8, "{} has {}", b.id, lib.len());
|
||||
assert!(lib.len() >= 5, "{} has {}", b.id, lib.len());
|
||||
// Recomputing gives the same library.
|
||||
let (s, g) = (b.make)();
|
||||
assert_eq!((s, g), (lib.swatches.clone(), lib.groups.clone()), "{} is deterministic", b.id);
|
||||
|
|
@ -420,6 +515,10 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn gradient_libraries_have_sorted_stops() {
|
||||
for b in GRADIENT_LIBRARIES {
|
||||
let lib = builtin_library(b.id).unwrap();
|
||||
assert!(lib.iter().all(|s| matches!(s.paint, Paint::Gradient(_))), "{} holds gradients only", b.id);
|
||||
}
|
||||
for lib in all() {
|
||||
for s in lib.iter() {
|
||||
if let Paint::Gradient(g) = &s.paint {
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
"Swatch Options",
|
||||
["Window", "Swatches"],
|
||||
None,
|
||||
"{name, newName?, color?, mode?: \"gray\"|\"rgb\"|\"hsb\"|\"cmyk\"|\"web\" (convert the colour), global?, spot? (spot colours are always global)} edit a swatch in any colour group, as one undo step. Fills, strokes and text linked to a global swatch take its new colour and name; turning Global off unlinks them (they keep their colour). Colour, mode and spot apply to solid colours only → {name, relinked: paints changed}",
|
||||
"{name, newName?, color?, mode?: \"gray\"|\"rgb\"|\"hsb\"|\"cmyk\"|\"web\" (convert the colour), global?, spot? (spot colours are always global), paint?: paint.setFill params ({color}, {gradient}, {swatch} or {pattern}) replacing the swatch's colour, gradient or pattern with one of the same kind} edit a swatch in any colour group, as one undo step. Fills, strokes and text linked to a global swatch take its new colour and name; turning Global off unlinks them (they keep their colour). Colour, mode and spot apply to solid colours only → {name, relinked: paints changed}",
|
||||
has_doc,
|
||||
swatch_edit
|
||||
),
|
||||
|
|
@ -474,9 +474,21 @@ fn swatch_edit(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
if solid.is_none() && (given("color") || given("mode") || bool_or(p, "spot", false)) {
|
||||
return Err(bad(C, "colour, mode and spot apply to solid-colour swatches only"));
|
||||
}
|
||||
// A replacement paint of the swatch's kind (a dropped colour or edited gradient).
|
||||
let paint = match p.get("paint").filter(|v| !v.is_null()) {
|
||||
Some(v) => match (paint_from(s, v)?, &sw.paint) {
|
||||
(Some(Paint::Solid { color, .. }), Paint::Solid { .. }) => Some(Paint::solid(color)),
|
||||
(Some(Paint::Gradient(g)), Paint::Gradient(_)) => {
|
||||
Some(Paint::Gradient(Box::new(vectorcraft_color::GradientPaint { geom: None, swatch: None, ..*g })))
|
||||
}
|
||||
(Some(new @ Paint::Pattern { .. }), Paint::Pattern { .. }) => Some(new),
|
||||
_ => return Err(bad(C, format!("`{name}` takes a paint of its own kind (colour, gradient or pattern)"))),
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let mut color = match p.get("color").filter(|v| !v.is_null()) {
|
||||
Some(v) => Some(color_value(v).ok_or_else(|| bad(C, format!("bad color {v}")))?),
|
||||
None => solid,
|
||||
None => paint.as_ref().and_then(Paint::color).or(solid),
|
||||
};
|
||||
if let (Some(c), Some(m)) = (color, str_param(p, "mode")) {
|
||||
color = Some(convert_to_mode(c, m, C)?);
|
||||
|
|
@ -490,8 +502,13 @@ fn swatch_edit(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
w.name = to.clone();
|
||||
w.spot = spot;
|
||||
w.global = global;
|
||||
if let (Some(c), Paint::Solid { color: wc, .. }) = (color, &mut w.paint) {
|
||||
*wc = c;
|
||||
match (color, &mut w.paint) {
|
||||
(Some(c), Paint::Solid { color: wc, .. }) => *wc = c,
|
||||
(_, wp) => {
|
||||
if let Some(new) = paint {
|
||||
*wp = new;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(d.map_solid_paints(&mut |c, l| relink.apply(c, l)))
|
||||
})?;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::libraries::{SWATCH_LIBRARIES, builtin_library};
|
||||
use vectorcraft_color::libraries::{BuiltinLibrary, GRADIENT_LIBRARIES, SWATCH_LIBRARIES, builtin_library};
|
||||
use vectorcraft_color::palette_io::{self, PaletteFormat};
|
||||
use vectorcraft_color::{Paint, Swatch, SwatchGroup, SwatchLibrary, default_swatches};
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
"Swatch Libraries",
|
||||
[],
|
||||
None,
|
||||
"{} → {libraries: [{id, name, category: \"builtIn\"|\"user\" (User Defined: the library files in the user library folder, rescanned now)|\"loaded\" (swatch.library.load), count, path?}], userFolder} every library the library panel opens, in menu order",
|
||||
"{} → {libraries: [{id, name, category: \"builtIn\"|\"gradients\"|\"user\" (User Defined: the library files in the user library folder, rescanned now)|\"loaded\" (swatch.library.load), count, path?}], userFolder} every library the library panel opens, in menu order",
|
||||
always,
|
||||
list
|
||||
),
|
||||
|
|
@ -156,25 +156,34 @@ fn stem(name: &str) -> &str {
|
|||
pub struct LibraryInfo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
/// "builtIn", "user" or "loaded".
|
||||
/// "builtIn", "gradients", "user" or "loaded".
|
||||
pub category: &'static str,
|
||||
}
|
||||
|
||||
/// Every library, in menu order: built-in, User Defined, loaded.
|
||||
/// The built-in libraries with their categories.
|
||||
fn builtins() -> impl Iterator<Item = (&'static str, &'static BuiltinLibrary)> {
|
||||
SWATCH_LIBRARIES.iter().map(|b| ("builtIn", b)).chain(GRADIENT_LIBRARIES.iter().map(|b| ("gradients", b)))
|
||||
}
|
||||
|
||||
fn builtin_info((category, b): (&'static str, &BuiltinLibrary)) -> LibraryInfo {
|
||||
LibraryInfo { id: b.id.into(), name: b.name.into(), category }
|
||||
}
|
||||
|
||||
/// Every library, in menu order: built-in, gradients, User Defined, loaded.
|
||||
pub fn libraries(s: &Session) -> Vec<LibraryInfo> {
|
||||
let builtin = SWATCH_LIBRARIES.iter().map(|b| LibraryInfo { id: b.id.into(), name: b.name.into(), category: "builtIn" });
|
||||
builtin.chain(s.swatch_libraries.extra.iter().map(|e| e.info.clone())).collect()
|
||||
builtins().map(builtin_info).chain(s.swatch_libraries.extra.iter().map(|e| e.info.clone())).collect()
|
||||
}
|
||||
|
||||
/// Library `key` (an id, or a name in any case) with its info.
|
||||
pub fn library(s: &Session, key: &str) -> Option<(LibraryInfo, Arc<SwatchLibrary>)> {
|
||||
let all = libraries(s);
|
||||
let info = all.iter().find(|l| l.id == key).or_else(|| all.iter().find(|l| l.name.eq_ignore_ascii_case(key)))?.clone();
|
||||
let lib = match s.swatch_libraries.extra.iter().find(|e| e.info.id == info.id) {
|
||||
Some(e) => e.lib.clone(),
|
||||
None => builtin_library(&info.id)?,
|
||||
};
|
||||
Some((info, lib))
|
||||
if let Some(e) = s.swatch_libraries.extra.iter().find(|e| e.info.id == key) {
|
||||
return Some((e.info.clone(), e.lib.clone()));
|
||||
}
|
||||
if let Some(b) = builtins().find(|(_, b)| b.id == key) {
|
||||
return Some((builtin_info(b), builtin_library(key)?));
|
||||
}
|
||||
let id = libraries(s).into_iter().find(|l| l.name.eq_ignore_ascii_case(key))?.id;
|
||||
library(s, &id)
|
||||
}
|
||||
|
||||
fn library_param(s: &Session, p: &Value, cmd: &str) -> Result<(LibraryInfo, Arc<SwatchLibrary>)> {
|
||||
|
|
|
|||
|
|
@ -196,3 +196,28 @@ fn another_documents_swatches_load_as_a_library() {
|
|||
assert!(lib.swatch("Signal").is_some() && lib.swatch("[None]").is_none());
|
||||
assert!(s.execute("swatch.library.load", &json!({"data": "hello", "name": "x.txt"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gradient_libraries_are_listed_and_gradient_swatches_rename_and_take_new_gradients() {
|
||||
let mut s = session();
|
||||
let list = run(&mut s, "swatch.library.list", json!({}));
|
||||
let grads: Vec<&Value> = list["libraries"].as_array().unwrap().iter().filter(|l| l["category"] == "gradients").collect();
|
||||
assert!(grads.len() >= 5 && grads.iter().any(|l| l["id"] == "sky-gradients"));
|
||||
run(&mut s, "swatch.library.add", json!({"library": "sky-gradients", "names": ["Dawn"]}));
|
||||
// Rename keeps the gradient.
|
||||
let before = doc(&s).swatch("Dawn").unwrap().paint.clone();
|
||||
run(&mut s, "swatch.edit", json!({"name": "Dawn", "newName": "Early"}));
|
||||
assert_eq!(doc(&s).swatch("Early").unwrap().paint, before);
|
||||
// An edited gradient replaces it, unplaced and unlinked; one undo step.
|
||||
let undo = undo_len(&s);
|
||||
let g =
|
||||
json!({"gradient": {"kind": "radial", "stops": [{"offset": 0, "color": "#ff0000"}, {"offset": 1, "color": "#0000ff"}], "swatch": "Early"}});
|
||||
run(&mut s, "swatch.edit", json!({"name": "Early", "paint": g}));
|
||||
let Paint::Gradient(gp) = &doc(&s).swatch("Early").unwrap().paint else { panic!("a gradient") };
|
||||
assert_eq!((gp.gradient.kind, gp.gradient.stops.len(), gp.swatch.clone(), gp.geom), (vectorcraft_color::GradientKind::Radial, 2, None, None));
|
||||
assert_eq!(undo_len(&s), undo + 1);
|
||||
// Kinds don't mix; a colour replaces a colour.
|
||||
assert!(s.execute("swatch.edit", &json!({"name": "Early", "paint": {"color": "#00ff00"}})).is_err());
|
||||
run(&mut s, "swatch.edit", json!({"name": "Red", "paint": {"color": "#00ff00"}}));
|
||||
assert_eq!(doc(&s).swatch("Red").unwrap().paint.color().map(|c| c.to_hex()), Some("#00ff00".into()));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
//! Swatch Options: name, colour type (process or spot), Global, colour mode with sliders and hex,
|
||||
//! previewed live on the canvas (Cancel rolls the preview back). Double-clicking a swatch opens it
|
||||
//! for a solid colour; gradients open the Gradient panel and patterns pattern editing ([`open`]).
|
||||
//! for a solid colour; for a gradient it shows the name and the gradient; patterns open pattern
|
||||
//! editing ([`open`]).
|
||||
//!
|
||||
//! Fields: `__swatch` (the swatch being edited), `name`, `spot`, `global`, `mode` (`gray`, `rgb`,
|
||||
//! `hsb`, `cmyk` or `web`), `color` (`"#rrggbb"` or a colour object such as
|
||||
//! `{"model": "cmyk", "c": 0.1, "m": 0.2, "y": 0.3, "k": 0}`) and `preview`.
|
||||
//! `{"model": "cmyk", "c": 0.1, "m": 0.2, "y": 0.3, "k": 0}`) and `preview`; a gradient swatch has
|
||||
//! `name`, `preview` and `__gradient` (the gradient shown) only.
|
||||
|
||||
use egui::vec2;
|
||||
use serde_json::{Value, json};
|
||||
|
|
@ -51,8 +53,8 @@ fn set_color(d: &mut Dialog, c: Color) {
|
|||
d.fields.insert("color".into(), json!(c));
|
||||
}
|
||||
|
||||
/// Open the editor of swatch `name`: Swatch Options for a colour, the Gradient panel (with the
|
||||
/// gradient applied to the active proxy) for a gradient, pattern editing for a pattern.
|
||||
/// Open the editor of swatch `name`: Swatch Options for a colour or a gradient (its name only),
|
||||
/// pattern editing for a pattern.
|
||||
pub fn open(app: &mut VectorcraftApp, name: &str) -> Result<Value, String> {
|
||||
let sw = app.session.active().and_then(|st| st.doc.swatch(name).cloned()).ok_or_else(|| format!("no swatch `{name}`"))?;
|
||||
match &sw.paint {
|
||||
|
|
@ -64,9 +66,9 @@ pub fn open(app: &mut VectorcraftApp, name: &str) -> Result<Value, String> {
|
|||
app.ui.dialog = Some(Dialog::new(KIND, fields));
|
||||
Ok(Value::Null)
|
||||
}
|
||||
Paint::Gradient(_) => {
|
||||
app.run(crate::panels::proxy_cmd(app, false), json!({"swatch": name}))?;
|
||||
app.ui.open_panel = Some("gradient".into());
|
||||
Paint::Gradient(g) => {
|
||||
let fields = json!({"__swatch": name, "name": name, "__gradient": g.gradient, "preview": true});
|
||||
app.ui.dialog = Some(Dialog::new(KIND, fields));
|
||||
Ok(Value::Null)
|
||||
}
|
||||
Paint::Pattern { pattern, .. } => {
|
||||
|
|
@ -78,8 +80,11 @@ pub fn open(app: &mut VectorcraftApp, name: &str) -> Result<Value, String> {
|
|||
}
|
||||
}
|
||||
|
||||
/// `swatch.edit` parameters from the fields.
|
||||
/// `swatch.edit` parameters from the fields (a gradient swatch only renames).
|
||||
fn edit_params(d: &Dialog) -> Value {
|
||||
if d.fields.contains_key("__gradient") {
|
||||
return json!({"name": d.str("__swatch"), "newName": d.str("name")});
|
||||
}
|
||||
json!({
|
||||
"name": d.str("__swatch"),
|
||||
"newName": d.str("name"),
|
||||
|
|
@ -91,7 +96,16 @@ fn edit_params(d: &Dialog) -> Value {
|
|||
}
|
||||
|
||||
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
let changed = editor(ui, d);
|
||||
let changed = match d.fields.get("__gradient").and_then(|g| serde_json::from_value::<vectorcraft_color::Gradient>(g.clone()).ok()) {
|
||||
Some(g) => {
|
||||
let changed = grid(ui, |ui| name_row(ui, d));
|
||||
ui.add_space(6.0);
|
||||
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 28.0), egui::Sense::hover());
|
||||
widgets::gradient_chip(ui, r, &g);
|
||||
changed
|
||||
}
|
||||
None => editor(ui, d),
|
||||
};
|
||||
ui.add_space(8.0);
|
||||
let mut pv = d.bool("preview");
|
||||
let pv_changed = widgets::check(ui, "Preview", pv, true);
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
|||
"ui.swatchOptions",
|
||||
"Swatch Options…",
|
||||
"",
|
||||
"{name} edit a swatch: Swatch Options for a colour (dialog `swatchOptions`, engine: swatch.edit), the Gradient panel for a gradient, pattern editing for a pattern",
|
||||
"{name} edit a swatch: Swatch Options for a colour (dialog `swatchOptions`, engine: swatch.edit) or a gradient (the same dialog with `name` only; it shows the gradient), pattern editing for a pattern",
|
||||
),
|
||||
(
|
||||
"ui.newSwatch",
|
||||
|
|
|
|||
|
|
@ -415,8 +415,9 @@ mod tests {
|
|||
assert!(ctx.read_response(tile_id::<SwatchLibraries>("#FF0033")).is_some());
|
||||
assert!(ctx.read_response(tile_id::<SwatchLibraries>("#00FF00")).is_none());
|
||||
let first = SWATCH_IDS[0];
|
||||
let last = SWATCH_IDS[SWATCH_IDS.len() - 1];
|
||||
assert_eq!(neighbour::<SwatchLibraries>(&app, first, -1).as_deref(), Some(last), "wraps around");
|
||||
let last = SwatchLibraries::list(&app).pop().unwrap().id;
|
||||
assert_eq!(last, "radial-glows", "gradient libraries come after the swatch libraries");
|
||||
assert_eq!(neighbour::<SwatchLibraries>(&app, first, -1), Some(last), "wraps around");
|
||||
assert_eq!(neighbour::<SwatchLibraries>(&app, "web-safe-216", 1).as_deref(), Some(SWATCH_IDS[1]));
|
||||
}
|
||||
|
||||
|
|
@ -440,9 +441,17 @@ mod tests {
|
|||
.filter(|e| e.path == ["Window", "Swatch Libraries"] && e.command.as_deref() == Some("window.swatchLibrary"))
|
||||
.map(|e| e.params["library"].clone())
|
||||
.collect();
|
||||
let listed: Vec<String> = vectorcraft_engine::cmd::swatchlib::libraries(&app.session).into_iter().map(|l| l.id).collect();
|
||||
assert_eq!(ids, listed.iter().map(|i| json!(i)).collect::<Vec<_>>());
|
||||
assert_eq!(listed, SWATCH_IDS);
|
||||
let all = vectorcraft_engine::cmd::swatchlib::libraries(&app.session);
|
||||
let listed = |category: &str| all.iter().filter(|l| l.category == category).map(|l| json!(l.id)).collect::<Vec<_>>();
|
||||
assert_eq!(ids, listed("builtIn"));
|
||||
assert_eq!(ids, SWATCH_IDS.map(|i| json!(i)));
|
||||
let gradients: Vec<Value> = crate::menus::menu_entries(&app)
|
||||
.into_iter()
|
||||
.filter(|e| e.path == ["Window", "Swatch Libraries", "Gradients"])
|
||||
.map(|e| e.params["library"].clone())
|
||||
.collect();
|
||||
assert!(gradients.len() >= 5);
|
||||
assert_eq!(gradients, listed("gradients"));
|
||||
let defaults = crate::menus::menu_entries(&app).into_iter().any(|e| e.command.as_deref() == Some("swatch.resetDefaults"));
|
||||
assert!(defaults, "Default Swatches is listed");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use std::sync::Arc;
|
|||
|
||||
use egui::{Color32, Rect, Response, Sense, Shape, Stroke, StrokeKind, Ui, pos2, vec2};
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::libraries::SWATCH_LIBRARIES;
|
||||
use vectorcraft_color::libraries::{BuiltinLibrary, GRADIENT_LIBRARIES, SWATCH_LIBRARIES};
|
||||
use vectorcraft_color::{Color, GradientKind, Paint, Swatch, SwatchLibrary};
|
||||
use vectorcraft_doc::Document;
|
||||
use vectorcraft_engine::cmd::swatchlib;
|
||||
|
|
@ -343,17 +343,18 @@ enum Drop {
|
|||
/// at the end (`None`).
|
||||
Move { names: Vec<String>, target: Option<String>, after: bool },
|
||||
/// A paint from elsewhere (a Fill/Stroke proxy, the Gradient panel's thumbnail) dropped on row
|
||||
/// `target` (or between rows) becomes a swatch.
|
||||
New { paint: Paint, target: Option<String> },
|
||||
/// `target` (or between rows) becomes a swatch; with Alt held (`replace`) it replaces swatch
|
||||
/// `target` when that is of its kind.
|
||||
New { paint: Paint, target: Option<String>, replace: bool },
|
||||
}
|
||||
|
||||
impl Drop {
|
||||
/// What releasing `d` before or `after` row `target` (`None`: at the end) does: move the rows
|
||||
/// it carries, or make a swatch of a paint dragged from elsewhere (appearances: nothing).
|
||||
fn of(d: &PanelDrag, target: Option<String>, after: bool) -> Option<Self> {
|
||||
fn of(d: &PanelDrag, target: Option<String>, after: bool, replace: bool) -> Option<Self> {
|
||||
match d {
|
||||
PanelDrag::Paint { rows: Some(r), .. } => Some(Drop::Move { names: r.names.clone(), target, after }),
|
||||
PanelDrag::Paint { paint, .. } => Some(Drop::New { paint: paint.clone(), target }),
|
||||
PanelDrag::Paint { paint, .. } => Some(Drop::New { paint: paint.clone(), target, replace }),
|
||||
PanelDrag::Appearance(_) => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -440,7 +441,7 @@ fn tile_drop(ui: &Ui, resp: &Response, e: &Entry, list: bool, ev: &mut TileEvent
|
|||
ui.painter().line_segment([pos2(x, r.top() - 1.0), pos2(x, r.bottom() + 1.0)], Stroke::new(2.0, t.accent));
|
||||
}
|
||||
if resp.dnd_release_payload::<PanelDrag>().is_some() {
|
||||
ev.drop = Drop::of(&d, Some(name.to_string()), after);
|
||||
ev.drop = Drop::of(&d, Some(name.to_string()), after, super::alt_held(ui));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -452,7 +453,7 @@ fn zone_input(ui: &Ui, zone: &Response, ev: &mut TileEvents) {
|
|||
ui.painter().rect_stroke(zone.rect, 0.0, Stroke::new(1.5, Tokens::get(ui.ctx()).accent), StrokeKind::Inside);
|
||||
}
|
||||
if zone.dnd_release_payload::<PanelDrag>().is_some() {
|
||||
ev.drop = Drop::of(&d, None, false);
|
||||
ev.drop = Drop::of(&d, None, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -497,6 +498,11 @@ fn move_params(d: &Document, names: &[String], target: Option<&str>, after: bool
|
|||
(!movable.is_empty()).then(|| json!({"names": movable, "to": to, "group": group}))
|
||||
}
|
||||
|
||||
/// Are both paints colours, gradients or patterns?
|
||||
fn same_kind(a: &Paint, b: &Paint) -> bool {
|
||||
std::mem::discriminant(a) == std::mem::discriminant(b) && !a.is_none()
|
||||
}
|
||||
|
||||
/// Run a drop on the panel: move the swatches or make a swatch of the proxy's paint (a colour
|
||||
/// dropped on a folder or a grouped swatch joins that group).
|
||||
fn apply_drop(app: &mut VectorcraftApp, drop: Drop) {
|
||||
|
|
@ -506,7 +512,11 @@ fn apply_drop(app: &mut VectorcraftApp, drop: Drop) {
|
|||
Some(p) => ("swatch.move", p),
|
||||
None => return,
|
||||
},
|
||||
Drop::New { paint, target } => {
|
||||
// Alt: an edited gradient (or colour) replaces the swatch it is dropped on.
|
||||
Drop::New { paint, target: Some(t), replace: true } if d.swatch(&t).is_some_and(|w| same_kind(&w.paint, &paint)) => {
|
||||
("swatch.edit", json!({"name": t, "paint": super::paint_params(&paint)}))
|
||||
}
|
||||
Drop::New { paint, target, .. } => {
|
||||
let mut p = super::paint_params(&paint);
|
||||
if paint.color().is_some()
|
||||
&& let Some(g) = target.and_then(|t| group_of(d, &t))
|
||||
|
|
@ -908,6 +918,7 @@ impl LibraryKind for SwatchLibraries {
|
|||
|
||||
fn list(app: &VectorcraftApp) -> Vec<LibraryRef> {
|
||||
let submenu = |category: &str| match category {
|
||||
"gradients" => Some("Gradients"),
|
||||
"user" => Some("User Defined"),
|
||||
"loaded" => Some("Other Libraries"),
|
||||
_ => None,
|
||||
|
|
@ -1035,9 +1046,11 @@ pub(crate) fn window_menu() -> Vec<Item> {
|
|||
"window.userSwatchLibrary9",
|
||||
"window.userSwatchLibrary10",
|
||||
];
|
||||
let open = |b: &'static BuiltinLibrary| Item::Cmd(b.name, "window.swatchLibrary", json!({ "library": b.id }));
|
||||
let mut items = vec![Item::Cmd("Default Swatches", "swatch.resetDefaults", Value::Null), Item::Sep];
|
||||
items.extend(SWATCH_LIBRARIES.iter().map(|b| Item::Cmd(b.name, "window.swatchLibrary", json!({ "library": b.id }))));
|
||||
items.extend(SWATCH_LIBRARIES.iter().map(open));
|
||||
items.extend([
|
||||
Item::Sub("Gradients", GRADIENT_LIBRARIES.iter().map(open).collect()),
|
||||
Item::Sep,
|
||||
Item::Sub("User Defined", SLOTS.iter().map(|id| Item::Cmd("User Library", id, Value::Null)).collect()),
|
||||
Item::Sep,
|
||||
|
|
@ -1154,10 +1167,15 @@ mod tests {
|
|||
crate::dialogs::cancel(&mut app);
|
||||
assert!(app.ui.dialog.is_none() && !app.session.in_interaction());
|
||||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||||
// Gradients open the Gradient panel, patterns pattern editing, None nothing.
|
||||
// A gradient opens Swatch Options with its name (and the gradient shown): OK renames it.
|
||||
double_click_tile(&mut app, &ctx, "Sunset");
|
||||
assert!(app.ui.dialog.is_none());
|
||||
assert_eq!(app.ui.open_panel.as_deref(), Some("gradient"));
|
||||
let d = app.ui.dialog.as_mut().expect("Swatch Options for a gradient");
|
||||
assert!(d.kind == KIND && d.fields.contains_key("__gradient") && !d.fields.contains_key("color"));
|
||||
d.fields.insert("name".into(), json!("Evening"));
|
||||
dialog_frame(&mut app, &ctx);
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
assert!(matches!(app.session.doc().unwrap().doc.swatch("Evening").map(|w| &w.paint), Some(Paint::Gradient(_))));
|
||||
// Patterns open pattern editing, None nothing.
|
||||
assert!(app.run("ui.swatchOptions", json!({"name": "[None]"})).is_err());
|
||||
assert!(app.run("ui.swatchOptions", json!({})).is_err());
|
||||
}
|
||||
|
|
@ -1372,6 +1390,36 @@ mod tests {
|
|||
assert!(egui::DragAndDrop::payload::<PanelDrag>(&ctx).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alt_dropping_a_gradient_on_a_gradient_swatch_replaces_it() {
|
||||
let mut app = app();
|
||||
let ctx = context();
|
||||
frame(&mut app, &ctx, vec![], 0.0, show);
|
||||
let grad = |c: &str| {
|
||||
let stops = vec![
|
||||
vectorcraft_color::GradientStop { offset: 0.0, color: Color::from_hex(c).unwrap(), opacity: 1.0, midpoint: 0.5 },
|
||||
vectorcraft_color::GradientStop { offset: 1.0, color: Color::WHITE, opacity: 1.0, midpoint: 0.5 },
|
||||
];
|
||||
Paint::Gradient(Box::new(vectorcraft_color::GradientPaint::new(vectorcraft_color::Gradient { kind: GradientKind::Radial, stops })))
|
||||
};
|
||||
let drop_on = |app: &mut VectorcraftApp, name: &str, paint: Paint, m: Modifiers, time: f64| {
|
||||
let at = tile_center(&ctx, name);
|
||||
egui::DragAndDrop::set_payload(&ctx, PanelDrag::paint(paint));
|
||||
let release = Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: false, modifiers: m };
|
||||
frame_with(app, &ctx, vec![Event::PointerMoved(at), release], m, time, show);
|
||||
};
|
||||
let count = app.session.doc().unwrap().doc.swatches_iter().count();
|
||||
drop_on(&mut app, "Ocean", grad("#ff0000"), Modifiers::ALT, 1.0);
|
||||
let d = &app.session.doc().unwrap().doc;
|
||||
let Some(Paint::Gradient(g)) = d.swatch("Ocean").map(|w| &w.paint) else { panic!("Ocean is a gradient") };
|
||||
assert_eq!((g.gradient.kind, d.swatches_iter().count()), (GradientKind::Radial, count), "replaced, not added");
|
||||
// Without Alt, or on a swatch of another kind, it becomes a new swatch.
|
||||
frame(&mut app, &ctx, vec![], 2.0, show);
|
||||
drop_on(&mut app, "Red", grad("#00ff00"), Modifiers::ALT, 3.0);
|
||||
assert_eq!(app.session.doc().unwrap().doc.swatches_iter().count(), count + 1);
|
||||
assert!(app.session.doc().unwrap().doc.swatch("Red").is_some_and(|w| w.paint.color().is_some()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn menu_targets_follow_the_panel_selection() {
|
||||
let app = app();
|
||||
|
|
|
|||
|
|
@ -24,8 +24,9 @@
|
|||
Swatch editors: `engine.execute` with `ui.swatchOptions {name}` opens the `swatchOptions` dialog for a colour swatch
|
||||
(fields `name`, `spot`, `global`, `mode`: `gray`/`rgb`/`hsb`/`cmyk`/`web`, `color`: `"#rrggbb"` or a colour object,
|
||||
`preview`), which previews on the canvas while open; `ui.dialog.confirm` applies it with `swatch.edit` as one undo
|
||||
step and `ui.dialog.cancel` rolls the preview back. Gradient swatches open the Gradient panel and pattern swatches
|
||||
pattern editing instead.
|
||||
step and `ui.dialog.cancel` rolls the preview back. For a gradient swatch the dialog has `name` only (it shows the
|
||||
gradient) and OK renames it; pattern swatches open pattern editing instead. Dropping a gradient (or colour) on a
|
||||
swatch of its kind in the Swatches panel with Alt held replaces it (`swatch.edit {name, paint}`).
|
||||
|
||||
Confirmations: deleting swatches from the Swatches panel opens a `confirm` dialog (fields `message`, `detail`);
|
||||
`ui.dialog.confirm` runs the command it asks about (here `swatch.delete`) and `ui.dialog.cancel` drops it.
|
||||
|
|
|
|||
Loading…
Reference in New Issue