Swatch legend: linked chips, labels that fit, exact Gray

Each chip's fill links to its swatch as paint.setFill {swatch} does, so
editing a global or spot swatch recolours the legend. Labels are area
type the size of the chip that shrinks text to fit. Gray values use the
app's own Gray conversion, and fractional column counts are refused.
This commit is contained in:
Don Omondi
2026-11-02 14:36:07 +07:00
committed by bflatastic
parent c823e7f5b2
commit aa71acc29f
2 changed files with 42 additions and 11 deletions
+20 -10
View File
@@ -4,7 +4,7 @@
use serde_json::{Value, json};
use vectorcraft_color::{Color, Paint, cms::Model};
use vectorcraft_doc::{Appearance, Node, NodeKind, TextObject};
use vectorcraft_doc::{Appearance, AreaFit, Node, NodeKind, TextKind, TextObject};
use vectorcraft_geom::{Point, Rect, shapes};
use super::{CommandSpec, Result, Session, bad, cmd, has_doc};
@@ -62,7 +62,8 @@ fn value_lines(c: Color, values: &[String], spot: bool) -> Vec<String> {
format!("L={} a={} b={}", l.l.round() as i32, l.a.round() as i32, l.b.round() as i32)
}
_ => {
let k = match c {
// As Edit Colors › Convert to Grayscale and the Color panel convert it.
let k = match c.in_model(Model::Gray) {
Color::Gray { k } => k,
_ => 1.0 - c.to_lab().l / 100.0,
};
@@ -78,7 +79,11 @@ fn value_lines(c: Color, values: &[String], spot: bool) -> Vec<String> {
fn legend(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "swatch.legend";
let columns = number(p, "columns", 4.0, 1.0..=50.0, C)? as usize;
let columns = number(p, "columns", 4.0, 1.0..=50.0, C)?;
if columns.fract() != 0.0 {
return Err(bad(C, "`columns` must be a whole number 1..50"));
}
let columns = columns as usize;
let size = number(p, "size", 72.0, 8.0..=1000.0, C)?;
let gap = number(p, "gap", 12.0, 0.0..=1000.0, C)?;
let values: Vec<String> = match p.get("values") {
@@ -125,17 +130,17 @@ fn legend(s: &mut Session, p: &Value) -> Result<Value> {
let pad = (size / 12.0).clamp(1.0, 8.0);
let style = super::create::new_type_style(s, &json!({ "size": text_size }));
let count = swatches.len();
// Each chip's fill links to its swatch, as paint.setFill {swatch} links art: editing a global or
// spot swatch then recolours the legend too.
let fills: Vec<Paint> = swatches.iter().map(|w| super::paint::swatch_solid(doc, &w.name, None).unwrap_or_else(|_| w.paint.clone())).collect();
let gid = s.edit("Create Swatch Legend", |d, sel| {
let mut children = Vec::with_capacity(count * 2);
for (i, w) in swatches.iter().enumerate() {
for (i, (w, fill)) in swatches.iter().zip(fills).enumerate() {
let Some(c) = w.paint.color() else { continue };
let (col, row) = (i % columns, i / columns);
let (x, y) = (x0 + col as f64 * (size + gap), y0 + row as f64 * (size + gap));
let mut chip = Node::path(
d.alloc_id(),
shapes::rectangle(Rect::new(x, y, x + size, y + size)),
Appearance::basic(w.paint.clone(), Paint::None, 0.0),
);
let mut chip =
Node::path(d.alloc_id(), shapes::rectangle(Rect::new(x, y, x + size, y + size)), Appearance::basic(fill, Paint::None, 0.0));
chip.name = Some(w.name.clone());
children.push(std::sync::Arc::new(chip));
let mut lines = vec![w.name.clone()];
@@ -143,7 +148,12 @@ fn legend(s: &mut Session, p: &Value) -> Result<Value> {
let mut label_style = style.clone();
label_style.fill = Paint::solid(if c.to_lab().l < 50.0 { Color::WHITE } else { Color::BLACK });
label_style.stroke = Paint::None;
let mut t = TextObject::point(Point::new(x + pad, y + pad + text_size), &lines.join("\n"), label_style);
// Area type the size of the chip (less its inset) that shrinks text to fit, so a long
// name or value line stays on its chip.
let mut t = TextObject::point(Point::new(x + pad, y + pad), &lines.join("\n"), label_style);
let inner = (size - 2.0 * pad).max(1.0);
t.kind = TextKind::Area { frame: shapes::rectangle(Rect::new(0.0, 0.0, inner, inner)) };
t.area.fit = AreaFit::ShrinkText { min_percent: AreaFit::MIN_PERCENT };
t.para = super::create::new_type_para();
t.system_layout = system_layout;
super::typecmd::refresh_bounds(&mut t);
+22 -1
View File
@@ -45,7 +45,7 @@ fn a_legend_draws_a_labelled_chip_per_colour_swatch_in_one_undo_step() {
let chips: Vec<&Node> = kids.iter().filter(|n| matches!(n.kind, NodeKind::Path { .. })).map(|n| &**n).collect();
let doc = &s.doc().unwrap().doc;
for (chip, w) in chips.iter().zip(doc.swatches_iter().filter(|w| w.paint.color().is_some())) {
assert_eq!(chip.appearance.fill_paint(), w.paint, "{}", w.name);
assert_eq!(chip.appearance.fill_paint().color(), w.paint.color(), "{}", w.name);
assert_eq!(chip.name.as_deref(), Some(w.name.as_str()));
}
let b = |i: usize| chips[i].geometric_bounds().unwrap();
@@ -82,6 +82,7 @@ fn a_legend_takes_names_values_and_a_position() {
json!({"names": "Signal"}),
json!({"values": ["xyz"]}),
json!({"columns": 0}),
json!({"columns": 2.5}),
json!({"size": 2}),
json!({"gap": -1}),
json!({"x": f64::MAX}),
@@ -91,3 +92,23 @@ fn a_legend_takes_names_values_and_a_position() {
}
assert_eq!(s.doc().unwrap().history.undo.len(), before);
}
/// A chip of a global swatch is linked to it: editing the swatch recolours the chip. Each label is
/// area type inside its chip that shrinks a long name to fit (#4919 review).
#[test]
fn chips_stay_linked_and_labels_stay_on_their_chips() {
let mut s = session();
s.execute("swatch.new", &json!({"name": "A Rather Long Brand Colour Name", "color": "#2050a0", "global": true})).unwrap();
let r = s.execute("swatch.legend", &json!({"names": ["A Rather Long Brand Colour Name"], "size": 40, "values": ["hex", "rgb", "cmyk"]})).unwrap();
let id = r["id"].as_u64().unwrap();
s.execute("swatch.edit", &json!({"name": "A Rather Long Brand Colour Name", "color": "#a02050"})).unwrap();
let g = group(&s, id);
let kids = g.children().unwrap();
let chip = kids.iter().find(|n| matches!(n.kind, NodeKind::Path { .. })).unwrap();
assert_eq!(chip.appearance.fill_paint().color().map(|c| c.to_hex()), Some("#a02050".into()), "the edit reached the chip");
let label = kids.iter().find(|n| matches!(n.kind, NodeKind::Text(_))).unwrap();
let NodeKind::Text(t) = &label.kind else { unreachable!() };
assert!(matches!(t.kind, vectorcraft_doc::TextKind::Area { .. }) && matches!(t.area.fit, vectorcraft_doc::AreaFit::ShrinkText { .. }));
let (c, l) = (chip.geometric_bounds().unwrap(), label.geometric_bounds().unwrap());
assert!(l.x0 >= c.x0 - 1e-6 && l.y0 >= c.y0 - 1e-6 && l.x1 <= c.x1 + 1e-6 && l.y1 <= c.y1 + 1e-6, "label {l:?} inside chip {c:?}");
}