M3.31: Swatches panel menu adds used colours, selects unused, merges, ungroups and sorts by kind
New commands: swatch.addUsedColors {selection?, global?} adds a swatch for
each colour the art uses that no solid swatch has (optionally global, the
art linking to them); swatch.unused lists the swatches nothing uses (links,
colours, gradients and patterns in art, symbols, pattern tiles and graphic
styles count); swatch.merge keeps the first colour and relinks the others'
art; swatch.ungroup and swatch.sortByKind (process, spot, gradients,
patterns). Each edit is one undo step; the panel menu items are enabled.
Document::visit_paints walks every paint of a document.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
fe258915e3
commit
bdf8f704af
|
|
@ -105,6 +105,23 @@ impl Document {
|
|||
}
|
||||
}
|
||||
|
||||
impl Document {
|
||||
/// Visit every paint held by the art, symbol definitions, pattern tiles and graphic styles
|
||||
/// (fills, strokes and text runs), and the colour of every gradient-mesh point as a solid paint.
|
||||
pub fn visit_paints(&self, f: &mut dyn FnMut(&Paint)) {
|
||||
let mut tree = |n: &Node| {
|
||||
n.walk(&mut |m| {
|
||||
node_paints(m).for_each(&mut *f);
|
||||
if let NodeKind::Mesh(mesh) = &m.kind {
|
||||
mesh.points.iter().for_each(|p| f(&Paint::solid(p.color)));
|
||||
}
|
||||
})
|
||||
};
|
||||
self.layers.iter().chain(self.symbols.iter().map(|s| &s.art)).chain(self.patterns.iter().flat_map(|p| &p.art)).for_each(|n| tree(n));
|
||||
self.graphic_styles.iter().flat_map(|gs| &gs.appearance.items).for_each(|it| f(item_paint(it)));
|
||||
}
|
||||
}
|
||||
|
||||
/// The default name of a solid swatch: its values in its own colour model ("C=10 M=20 Y=30 K=0",
|
||||
/// "R=255 G=128 B=0", "Gray K=40").
|
||||
pub fn color_name(c: Color) -> String {
|
||||
|
|
|
|||
|
|
@ -68,6 +68,51 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
has_doc,
|
||||
swatch_move
|
||||
),
|
||||
cmd!(
|
||||
"swatch.addUsedColors",
|
||||
"Add Used Colors",
|
||||
["Window", "Swatches"],
|
||||
None,
|
||||
"{selection?: false (only the selected art's colours; default: all the art's), global?: false (the new swatches are global and the matching unlinked colours in that art link to them)} add a swatch for each colour used (fills, strokes, text, gradient stops, mesh points) that no solid swatch has yet, as one undo step → {added: [names], linked}",
|
||||
has_doc,
|
||||
swatch_add_used
|
||||
),
|
||||
cmd!(
|
||||
query "swatch.unused",
|
||||
"Select All Unused",
|
||||
["Window", "Swatches"],
|
||||
None,
|
||||
"{} → {names: [the swatches nothing uses, in panel order]}: a colour is used when a paint links to it or an unlinked paint, gradient stop or mesh point has its colour; a gradient when a gradient links to it or has its stops; a pattern when it fills or strokes anything (art, symbols, pattern tiles and graphic styles count). None is never listed",
|
||||
has_doc,
|
||||
swatch_unused
|
||||
),
|
||||
cmd!(
|
||||
"swatch.merge",
|
||||
"Merge Swatches",
|
||||
["Window", "Swatches"],
|
||||
None,
|
||||
"{names: [two or more solid-colour swatches]} keep the first (its name and colour) and delete the others; paints linked to them take its colour and link to it (or unlink when it isn't global), as one undo step → {name, merged: [deleted names], relinked}",
|
||||
has_doc,
|
||||
swatch_merge
|
||||
),
|
||||
cmd!(
|
||||
"swatch.ungroup",
|
||||
"Ungroup Color Group",
|
||||
["Window", "Swatches"],
|
||||
None,
|
||||
"{name: colour group} move its swatches to the end of the ungrouped swatches and remove the group, as one undo step → {swatches: [names]}",
|
||||
has_doc,
|
||||
swatch_ungroup
|
||||
),
|
||||
cmd!(
|
||||
"swatch.sortByKind",
|
||||
"Sort by Kind",
|
||||
["Window", "Swatches"],
|
||||
None,
|
||||
"{} order the swatches of each list by kind (None first, then process colours, spot colours, gradients, patterns; colour groups stay after them), keeping their order within a kind, as one undo step",
|
||||
has_doc,
|
||||
swatch_sort_by_kind
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -176,6 +221,27 @@ fn artwork_colors(d: &Document, ids: &[NodeId], tints: bool, swatches: &mut Vec<
|
|||
}
|
||||
}
|
||||
|
||||
/// Link the solid paints in the subtrees of `ids` whose colour is one of `links`' to that swatch,
|
||||
/// unless they already link to an existing swatch. Returns the number of paints linked.
|
||||
fn link_colors(d: &mut Document, ids: &[NodeId], links: &[(Color, String)]) -> usize {
|
||||
if links.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
let live: Vec<String> = d.swatches_iter().map(|w| w.name.clone()).collect();
|
||||
d.map_solid_paints_in(ids, &mut |c, link| {
|
||||
if link.as_ref().is_some_and(|l| live.contains(l)) {
|
||||
return false;
|
||||
}
|
||||
match links.iter().find(|(lc, _)| lc == c) {
|
||||
Some((_, n)) => {
|
||||
*link = Some(n.clone());
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn swatch_new_group(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "swatch.newGroup";
|
||||
let mut names = str_list(p, "swatches");
|
||||
|
|
@ -216,23 +282,7 @@ fn swatch_new_group(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
}
|
||||
let members: Vec<String> = group.swatches.iter().map(|w| w.name.clone()).collect();
|
||||
d.swatch_groups.push(group);
|
||||
let linked = if links.is_empty() {
|
||||
0
|
||||
} else {
|
||||
let live: Vec<String> = d.swatches_iter().map(|w| w.name.clone()).collect();
|
||||
d.map_solid_paints_in(&sel.objects, &mut |c, link| {
|
||||
if link.as_ref().is_some_and(|l| live.contains(l)) {
|
||||
return false;
|
||||
}
|
||||
match links.iter().find(|(lc, _)| lc == c) {
|
||||
Some((_, n)) => {
|
||||
*link = Some(n.clone());
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
})
|
||||
};
|
||||
let linked = link_colors(d, &sel.objects, &links);
|
||||
Ok((name, members, linked))
|
||||
})?;
|
||||
Ok(json!({"name": name, "swatches": members, "linked": linked}))
|
||||
|
|
@ -481,3 +531,120 @@ fn swatch_sort(s: &mut Session, _: &Value) -> Result<Value> {
|
|||
})?;
|
||||
ok()
|
||||
}
|
||||
|
||||
fn swatch_add_used(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "swatch.addUsedColors";
|
||||
let global = bool_or(p, "global", false);
|
||||
let only_selection = bool_or(p, "selection", false);
|
||||
let (added, linked) = s.edit("Add Used Colors", |d, sel| {
|
||||
let ids: Vec<NodeId> = if only_selection { sel.objects.clone() } else { d.layers.iter().map(|l| l.id).collect() };
|
||||
if only_selection && ids.is_empty() {
|
||||
return Err(bad(C, "select artwork to add its colours"));
|
||||
}
|
||||
let (mut linked_to, mut colors) = (vec![], vec![]);
|
||||
artwork_colors(d, &ids, false, &mut linked_to, &mut colors);
|
||||
// Colours a solid swatch already has are skipped.
|
||||
colors.retain(|c| !d.swatches_iter().any(|w| w.paint.color() == Some(*c)));
|
||||
let mut links = vec![];
|
||||
for c in colors {
|
||||
let name = d.free_swatch_name(&color_name(c));
|
||||
d.swatches.push(Swatch { name: name.clone(), paint: Paint::solid(c), global, spot: false });
|
||||
links.push((c, name));
|
||||
}
|
||||
let linked = if global { link_colors(d, &ids, &links) } else { 0 };
|
||||
Ok((links.into_iter().map(|(_, n)| n).collect::<Vec<_>>(), linked))
|
||||
})?;
|
||||
Ok(json!({"added": added, "linked": linked}))
|
||||
}
|
||||
|
||||
fn swatch_unused(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let d = &s.doc()?.doc;
|
||||
let (mut links, mut colors, mut gradients, mut patterns) = (vec![], vec![], vec![], vec![]);
|
||||
d.visit_paints(&mut |p| match p {
|
||||
Paint::Solid { swatch: Some(l), .. } if d.swatch(l).is_some() => links.push(l.clone()),
|
||||
Paint::Solid { color, .. } => colors.push(*color),
|
||||
Paint::Gradient(g) => {
|
||||
links.extend(g.swatch.clone());
|
||||
colors.extend(g.gradient.stops.iter().map(|s| s.color));
|
||||
gradients.push(g.gradient.clone());
|
||||
}
|
||||
Paint::Pattern { pattern, .. } => patterns.push(pattern.clone()),
|
||||
Paint::None => {}
|
||||
});
|
||||
let used = |w: &Swatch| {
|
||||
links.contains(&w.name)
|
||||
|| match &w.paint {
|
||||
Paint::None => true,
|
||||
Paint::Solid { color, .. } => colors.contains(color),
|
||||
Paint::Gradient(g) => gradients.contains(&g.gradient),
|
||||
Paint::Pattern { pattern, .. } => patterns.contains(pattern),
|
||||
}
|
||||
};
|
||||
let names: Vec<&str> = d.swatches_iter().filter(|w| !used(w)).map(|w| w.name.as_str()).collect();
|
||||
Ok(json!({ "names": names }))
|
||||
}
|
||||
|
||||
fn swatch_merge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "swatch.merge";
|
||||
let names = names_param(p, C)?;
|
||||
let d = &s.doc()?.doc;
|
||||
for n in &names {
|
||||
if d.swatch(n).ok_or_else(|| bad(C, format!("no swatch `{n}`")))?.paint.color().is_none() {
|
||||
return Err(bad(C, format!("`{n}` isn't a solid colour")));
|
||||
}
|
||||
}
|
||||
let (keep, merged) = names.split_first().ok_or_else(|| bad(C, "give two or more swatches"))?;
|
||||
if merged.is_empty() {
|
||||
return Err(bad(C, "give two or more swatches"));
|
||||
}
|
||||
let kept = d.swatch(keep).ok_or_else(|| bad(C, format!("no swatch `{keep}`")))?;
|
||||
let (color, link) = (kept.paint.color().unwrap_or_default(), kept.global.then(|| keep.clone()));
|
||||
let mut relink = |c: &mut Color, l: &mut Option<String>| {
|
||||
if !l.as_ref().is_some_and(|l| merged.contains(l)) {
|
||||
return false;
|
||||
}
|
||||
*c = color;
|
||||
*l = link.clone();
|
||||
true
|
||||
};
|
||||
let relinked = s.edit("Merge Swatches", |d, _| {
|
||||
for n in merged {
|
||||
d.remove_swatch(n);
|
||||
}
|
||||
Ok(d.map_solid_paints(&mut relink))
|
||||
})?;
|
||||
map_default_paints(s, &mut relink);
|
||||
Ok(json!({"name": keep, "merged": merged, "relinked": relinked}))
|
||||
}
|
||||
|
||||
fn swatch_ungroup(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "swatch.ungroup";
|
||||
let name = str_param(p, "name").ok_or_else(|| bad(C, "missing `name`"))?.to_string();
|
||||
let moved = s.edit("Ungroup Color Group", |d, _| {
|
||||
let i = d.swatch_groups.iter().position(|g| g.name == name).ok_or_else(|| bad(C, format!("no colour group `{name}`")))?;
|
||||
let g = d.swatch_groups.remove(i);
|
||||
let names: Vec<String> = g.swatches.iter().map(|w| w.name.clone()).collect();
|
||||
d.swatches.extend(g.swatches);
|
||||
Ok(names)
|
||||
})?;
|
||||
Ok(json!({ "swatches": moved }))
|
||||
}
|
||||
|
||||
fn swatch_sort_by_kind(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let rank = |w: &Swatch| match &w.paint {
|
||||
Paint::None => 0,
|
||||
Paint::Solid { .. } if !w.spot => 1,
|
||||
Paint::Solid { .. } => 2,
|
||||
Paint::Gradient(_) => 3,
|
||||
Paint::Pattern { .. } => 4,
|
||||
};
|
||||
s.edit("Sort Swatches", |d, _| {
|
||||
// A stable sort: swatches keep their order within a kind.
|
||||
d.swatches.sort_by_key(rank);
|
||||
for g in &mut d.swatch_groups {
|
||||
g.swatches.sort_by_key(rank);
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
ok()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,3 +76,118 @@ fn move_reorders_groups_and_refuses_bad_moves() {
|
|||
assert_eq!(undo_len(&s), undo, "failed moves leave no undo step");
|
||||
assert!(doc(&s).swatch("Sunset").is_some_and(|w| matches!(w.paint, Paint::Gradient(_))));
|
||||
}
|
||||
|
||||
fn rect_filled(s: &mut Session, fill: Value) -> NodeId {
|
||||
let id = NodeId(run(s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10}))["id"].as_u64().unwrap());
|
||||
let mut p = fill;
|
||||
p["ids"] = json!([id.0]);
|
||||
run(s, "paint.setFill", p);
|
||||
run(s, "paint.setStroke", json!({"ids": [id.0], "none": true}));
|
||||
id
|
||||
}
|
||||
|
||||
fn fill(s: &Session, id: NodeId) -> Paint {
|
||||
doc(s).node(id).unwrap().appearance.fill_paint()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_used_colors_skips_colours_that_have_swatches() {
|
||||
let mut s = session();
|
||||
rect_filled(&mut s, json!({"color": "#123456"}));
|
||||
rect_filled(&mut s, json!({"color": "#ed1c24"}));
|
||||
rect_filled(&mut s, json!({"swatch": "Orange"}));
|
||||
let undo = undo_len(&s);
|
||||
let r = run(&mut s, "swatch.addUsedColors", json!({}));
|
||||
assert_eq!(r, json!({"added": ["R=18 G=52 B=86"], "linked": 0}), "Red and Orange are swatches already");
|
||||
assert_eq!(undo_len(&s), undo + 1);
|
||||
assert_eq!(run(&mut s, "swatch.addUsedColors", json!({}))["added"], json!([]));
|
||||
// Only the selection's colours, as global swatches its paints link to.
|
||||
let d = rect_filled(&mut s, json!({"color": "#abcdef"}));
|
||||
rect_filled(&mut s, json!({"color": "#fedcba"}));
|
||||
run(&mut s, "select.set", json!({"ids": [d.0]}));
|
||||
let r = run(&mut s, "swatch.addUsedColors", json!({"selection": true, "global": true}));
|
||||
assert_eq!(r, json!({"added": ["R=171 G=205 B=239"], "linked": 1}));
|
||||
assert_eq!(fill(&s, d), Paint::Solid { color: vectorcraft_color::Color::from_hex("#abcdef").unwrap(), swatch: Some("R=171 G=205 B=239".into()) });
|
||||
assert!(doc(&s).swatch("R=171 G=205 B=239").unwrap().global);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unused_leaves_out_linked_colours_gradients_and_patterns_in_use() {
|
||||
let mut s = session();
|
||||
let all = run(&mut s, "swatch.unused", json!({}));
|
||||
assert!(!all["names"].as_array().unwrap().contains(&json!("[None]")));
|
||||
assert!(all["names"].as_array().unwrap().contains(&json!("Sunset")));
|
||||
run(&mut s, "swatch.edit", json!({"name": "Teal", "global": true}));
|
||||
rect_filled(&mut s, json!({"swatch": "Teal"}));
|
||||
run(&mut s, "swatch.edit", json!({"name": "Teal", "color": "#010203"}));
|
||||
rect_filled(&mut s, json!({"swatch": "Sunset"}));
|
||||
rect_filled(&mut s, json!({"color": "#ed1c24"}));
|
||||
let r = rect_filled(&mut s, json!({"color": "#ff0000"}));
|
||||
run(&mut s, "select.set", json!({"ids": [r.0]}));
|
||||
run(&mut s, "object.pattern.make", json!({"name": "Dots", "width": 20, "height": 20}));
|
||||
run(&mut s, "object.pattern.done", json!({}));
|
||||
rect_filled(&mut s, json!({"swatch": "Dots"}));
|
||||
let names = run(&mut s, "swatch.unused", json!({}))["names"].clone();
|
||||
let names: Vec<&str> = names.as_array().unwrap().iter().filter_map(Value::as_str).collect();
|
||||
for used in ["Teal", "Sunset", "Red", "Dots", "White"] {
|
||||
assert!(!names.contains(&used), "{used} is used: {names:?}");
|
||||
}
|
||||
for unused in ["Orange", "Ocean", "Bright Red", "K=50"] {
|
||||
assert!(names.contains(&unused), "{unused} is unused: {names:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_keeps_the_first_and_relinks_the_others_art() {
|
||||
let mut s = session();
|
||||
for n in ["Red", "Orange"] {
|
||||
run(&mut s, "swatch.edit", json!({"name": n, "global": true}));
|
||||
}
|
||||
let a = rect_filled(&mut s, json!({"swatch": "Red"}));
|
||||
let b = rect_filled(&mut s, json!({"swatch": "Orange"}));
|
||||
let red = doc(&s).swatch("Red").unwrap().paint.color().unwrap();
|
||||
let before = doc(&s).clone();
|
||||
let undo = undo_len(&s);
|
||||
let r = run(&mut s, "swatch.merge", json!({"names": ["Red", "Orange"]}));
|
||||
assert_eq!(r, json!({"name": "Red", "merged": ["Orange"], "relinked": 1}));
|
||||
assert!(doc(&s).swatch("Orange").is_none());
|
||||
assert_eq!(fill(&s, b), Paint::Solid { color: red, swatch: Some("Red".into()) });
|
||||
assert_eq!(undo_len(&s), undo + 1);
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(*doc(&s), before);
|
||||
// A kept swatch that isn't global leaves the merged swatches' art unlinked, in its colour.
|
||||
run(&mut s, "swatch.merge", json!({"names": ["Lime", "Red", "Bright Red"]}));
|
||||
let lime = doc(&s).swatch("Lime").unwrap().paint.color().unwrap();
|
||||
assert_eq!(fill(&s, a), Paint::solid(lime));
|
||||
assert!(doc(&s).swatch("Red").is_none() && doc(&s).swatch("Bright Red").is_none());
|
||||
for bad in [json!({"names": ["Lime"]}), json!({"names": ["Lime", "Sunset"]}), json!({"names": ["Lime", "Nope"]})] {
|
||||
assert!(s.execute("swatch.merge", &bad).is_err(), "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ungroup_and_sort_by_kind_are_one_undo_step_each() {
|
||||
let mut s = session();
|
||||
let undo = undo_len(&s);
|
||||
assert_eq!(run(&mut s, "swatch.ungroup", json!({"name": "Brights"}))["swatches"].as_array().unwrap().len(), 5);
|
||||
assert!(doc(&s).swatch_groups.iter().all(|g| g.name != "Brights"));
|
||||
assert_eq!(names(&s, None).last().map(String::as_str), Some("Bright Violet"));
|
||||
assert!(s.execute("swatch.ungroup", &json!({"name": "Red"})).is_err());
|
||||
// A gradient first, a spot colour among the process ones.
|
||||
run(&mut s, "swatch.move", json!({"name": "Ocean", "to": 0}));
|
||||
run(&mut s, "swatch.edit", json!({"name": "Orange", "spot": true}));
|
||||
run(&mut s, "swatch.sortByKind", json!({}));
|
||||
assert_eq!(undo_len(&s), undo + 4);
|
||||
let rank = |w: &vectorcraft_color::Swatch| match &w.paint {
|
||||
Paint::None => 0,
|
||||
Paint::Solid { .. } if !w.spot => 1,
|
||||
Paint::Solid { .. } => 2,
|
||||
Paint::Gradient(_) => 3,
|
||||
Paint::Pattern { .. } => 4,
|
||||
};
|
||||
let ranks: Vec<u8> = doc(&s).swatches.iter().map(rank).collect();
|
||||
assert!(ranks.is_sorted(), "{ranks:?}");
|
||||
let n = names(&s, None);
|
||||
assert_eq!((n[0].as_str(), n[1].as_str()), ("[None]", "White"), "the order within a kind is kept");
|
||||
assert_eq!(n[n.iter().position(|x| x == "Orange").unwrap() + 1], "Ocean", "the spot colour comes after the process ones, then the gradients");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -713,6 +713,18 @@ fn editable(app: &VectorcraftApp, sel: &[String]) -> Option<String> {
|
|||
}
|
||||
}
|
||||
|
||||
/// The selected solid-colour swatches in selection order (Merge Swatches keeps the first).
|
||||
fn mergeable(app: &VectorcraftApp, sel: &[String]) -> Vec<String> {
|
||||
let Some(st) = app.session.active() else { return vec![] };
|
||||
sel.iter().filter(|n| st.doc.swatch(n).is_some_and(|w| w.paint.color().is_some())).cloned().collect()
|
||||
}
|
||||
|
||||
/// The first selected colour group (Ungroup Color Group).
|
||||
fn selected_group(app: &VectorcraftApp, sel: &[String]) -> Option<String> {
|
||||
let d = &app.session.active()?.doc;
|
||||
sel.iter().find(|n| d.swatch_groups.iter().any(|g| g.name == **n)).cloned()
|
||||
}
|
||||
|
||||
/// Delete `names` (swatches and colour groups) after asking, or at once with `now` (Alt-click).
|
||||
fn delete(app: &mut VectorcraftApp, names: Vec<String>, now: bool) {
|
||||
let params = json!({"names": names});
|
||||
|
|
@ -801,18 +813,36 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
{
|
||||
app.run("swatch.duplicate", json!({"name": n})).ok();
|
||||
}
|
||||
menu_item(ui, "Merge Swatches", false, false);
|
||||
// The first selected colour is kept.
|
||||
let merge = mergeable(app, &selected);
|
||||
if menu_item(ui, "Merge Swatches", merge.len() > 1, false) {
|
||||
app.run("swatch.merge", json!({ "names": merge })).ok();
|
||||
}
|
||||
if menu_item(ui, "Delete Swatch", !del.is_empty(), false) {
|
||||
delete(app, del, false);
|
||||
}
|
||||
menu_item(ui, "Ungroup Color Group", false, false);
|
||||
menu_item(ui, "Select All Unused", false, false);
|
||||
menu_item(ui, "Add Used Colors", false, false);
|
||||
let group = selected_group(app, &selected);
|
||||
if menu_item(ui, "Ungroup Color Group", group.is_some(), false)
|
||||
&& let Some(g) = group
|
||||
{
|
||||
app.run("swatch.ungroup", json!({ "name": g })).ok();
|
||||
}
|
||||
if menu_item(ui, "Select All Unused", true, false)
|
||||
&& let Ok(r) = app.run("swatch.unused", json!({}))
|
||||
{
|
||||
let names: Vec<String> = r["names"].as_array().into_iter().flatten().filter_map(Value::as_str).map(str::to_string).collect();
|
||||
set_pstate(ui.ctx(), "swatch-selected", names);
|
||||
}
|
||||
if menu_item(ui, "Add Used Colors", true, false) {
|
||||
app.run("swatch.addUsedColors", json!({})).ok();
|
||||
}
|
||||
ui.separator();
|
||||
if menu_item(ui, "Sort by Name", true, false) {
|
||||
app.run("swatch.sortByName", json!({})).ok();
|
||||
}
|
||||
menu_item(ui, "Sort by Kind", false, false);
|
||||
if menu_item(ui, "Sort by Kind", true, false) {
|
||||
app.run("swatch.sortByKind", json!({})).ok();
|
||||
}
|
||||
ui.separator();
|
||||
for (v, label) in View::ALL {
|
||||
if menu_item(ui, label, true, v == view) {
|
||||
|
|
@ -1154,6 +1184,15 @@ mod tests {
|
|||
assert!(egui::DragAndDrop::payload::<Paint>(&ctx).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn menu_targets_follow_the_panel_selection() {
|
||||
let app = app();
|
||||
let sel: Vec<String> = ["Red", "Sunset", "Grays", "Orange", "Brights"].map(String::from).to_vec();
|
||||
assert_eq!(mergeable(&app, &sel), ["Red", "Orange"], "Merge Swatches takes the selected colours, first kept");
|
||||
assert_eq!(selected_group(&app, &sel).as_deref(), Some("Grays"));
|
||||
assert_eq!(selected_group(&app, &sel[..2]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kind_filter() {
|
||||
let solid = Paint::solid(Color::BLACK);
|
||||
|
|
|
|||
|
|
@ -97,6 +97,9 @@ art's colours (by default as global swatches the art links to).
|
|||
`swatch.move {names, to?, group?}` reorders swatches or moves them into or out of a colour group (as dragging
|
||||
them in the Swatches panel does); give colour group names instead to reorder the groups. Dropping a swatch on art
|
||||
in the app runs `paint.setFill` (or `paint.setStroke`, the active proxy) with `ids: [the object under the pointer]`.
|
||||
The Swatches panel menu's commands: `swatch.addUsedColors {selection?, global?}`, `swatch.unused` (a query: the
|
||||
names Select All Unused selects), `swatch.merge {names}` (the first is kept), `swatch.ungroup {name}` and
|
||||
`swatch.sortByKind`.
|
||||
|
||||
## Resources
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue