Swatches: InDesign's type columns on each row

Rows show a crossed pencil on the swatches that can't be edited ([None],
[Registration], [Black]), the colour type (process halftone or spot disc)
and the colour mode (CMYK, RGB, Gray, or the [None] and [Registration]
glyphs), drawn in code, in place of a lock and "CMYK" text. The Tint field
reads 100 with nothing selected, as InDesign's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
moppeeer
2026-10-21 02:08:17 +02:00
co-authored by Claude Opus 5.5
parent 8dff7ecdb0
commit d24ce2cfd3
2 changed files with 169 additions and 15 deletions
+1
View File
@@ -348,6 +348,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
Newest first.
- **2026-10-21** · Swatches panel rows show InDesign's columns, drawn in code: a crossed pencil on the swatches that can't be edited ([None], [Registration], [Black]), the colour type (process or spot) and the colour mode (CMYK, RGB, Gray, or [None]'s and [Registration]'s own glyphs), in place of a lock and "CMYK" text. Tint reads 100 with nothing selected. No readiness percentage change.
- **2026-10-16** · Type › Reload Fonts (`font.reload`): fonts installed, edited or removed while DesignCraft runs are read again without a restart, and the open documents recompose with them (#328, #547). An edited font file is noticed by its length and modification time; bundled, added and document fonts stay as they are. InDesign does this by itself, so noticing changes automatically is still open (gap 6). No readiness percentage change.
- **2026-10-16** · Parents can be deleted from their Pages-panel context menu or `layout.parents.delete {id}`. Applied-page and based-on references are cleared, local override copies survive, and Undo restores the parent. Addresses the deletion portion of #956; no readiness percentage change.
- **2026-10-17** · File › Package writes the document's and the IDML's links relative to the package (`Links/logo.png`, `file:Links/logo.png`), so a package holds no path from the machine that made it; opening a document resolves them to the files beside it. Links › Copy Link(s) To still links by full path. (#340). No readiness percentage change.
+168 -15
View File
@@ -196,7 +196,8 @@ fn header(app: &mut DesignApp, ui: &mut egui::Ui, doc: &designcraft_doc::Documen
}
ui.add_space(6.0);
crate::widgets::caption(ui, crate::i18n::tr(&app.ui.language, "Tint:"));
let tint = target.map(|(_, ft, _, st)| f64::from(if stroke_front { *st } else { *ft }) * 100.0);
// With nothing selected InDesign's field reads 100.
let tint = target.map_or(Some(100.0), |(_, ft, _, st)| Some(f64::from(if stroke_front { *st } else { *ft }) * 100.0));
if let Some(v) = crate::widgets::number(ui, "swatches_tint", tint, "", 44.0, 0) {
let _ = set_tint(app, v);
}
@@ -232,6 +233,127 @@ fn folder_button(ui: &mut egui::Ui, tip: &str) -> egui::Response {
})
}
/// What a Swatches panel row marks on its right, as InDesign's columns.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct RowMarks {
/// [None], [Registration] and [Black] can't be edited ([Paper] can).
not_editable: bool,
kind: Option<ColorType>,
mode: Option<RowMode>,
}
/// The colour-mode column: the ink model, or the special swatches' own glyphs.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum RowMode {
Cmyk,
Rgb,
Gray,
None,
Registration,
}
fn row_marks(sw: &designcraft_color::swatch::Swatch) -> RowMarks {
use designcraft_color::Color;
let not_editable = sw.locked && sw.name != designcraft_color::swatch::PAPER;
match &sw.value {
SwatchValue::Color { color, color_type } => {
let mode = match color {
Color::Cmyk { .. } => RowMode::Cmyk,
Color::Rgb { .. } => RowMode::Rgb,
Color::Gray { .. } => RowMode::Gray,
};
RowMarks { not_editable, kind: Some(*color_type), mode: Some(mode) }
}
SwatchValue::None => RowMarks { not_editable, kind: None, mode: Some(RowMode::None) },
SwatchValue::Registration => RowMarks { not_editable, kind: None, mode: Some(RowMode::Registration) },
_ => RowMarks { not_editable, kind: None, mode: None },
}
}
/// A pencil with a stroke through it: the swatch can't be edited. Drawn in code.
fn paint_not_editable(p: &egui::Painter, r: egui::Rect, ink: egui::Color32) {
// The pencil: its tip at the lower left, its body up to the right.
let tip = r.left_bottom() + vec2(1.5, -1.5);
let d = vec2(1.0, -1.0).normalized();
let n = vec2(d.y, -d.x) * 1.9;
let (b0, b1) = (tip + d * 3.4, tip + d * 12.5);
p.add(egui::Shape::convex_polygon(vec![tip, b0 + n, b0 - n], ink, egui::Stroke::NONE));
p.add(egui::Shape::convex_polygon(vec![b0 + n, b1 + n, b1 - n, b0 - n], ink, egui::Stroke::NONE));
// The stroke through it, with a gap of the panel's colour around it so it reads on the body.
let (s0, s1) = (r.left_top() + vec2(1.5, 1.5), r.right_bottom() - vec2(1.5, 1.5));
p.line_segment([s0, s1], egui::Stroke::new(3.2, egui::Color32::from_black_alpha(140)));
p.line_segment([s0, s1], egui::Stroke::new(1.2, ink));
}
/// The colour type: a process colour is a halftone square, a spot colour a disc in a square.
fn paint_color_type(p: &egui::Painter, r: egui::Rect, kind: ColorType, ink: egui::Color32) {
let sq = egui::Rect::from_center_size(r.center(), vec2(10.0, 10.0));
p.rect_filled(sq, 0.0, ink);
if kind == ColorType::Spot {
p.circle_filled(sq.center(), 3.2, egui::Color32::from_black_alpha(110));
} else {
// A fine halftone: a grey square with a light dot screen.
let dot = egui::Color32::from_black_alpha(70);
for i in 0..4u8 {
for j in 0..4u8 {
let o = if j % 2 == 0 { 1.5 } else { 2.6 };
p.circle_filled(sq.min + vec2(o + 2.2 * f32::from(i), 1.6 + 2.3 * f32::from(j)), 0.6, dot);
}
}
}
}
/// The colour mode: CMYK's four inks meeting in the middle, RGB's three primaries, a grey ramp,
/// [None]'s red slash and [Registration]'s target.
fn paint_mode(p: &egui::Painter, r: egui::Rect, mode: RowMode, ink: egui::Color32) {
let sq = egui::Rect::from_center_size(r.center(), vec2(11.0, 11.0));
let c = sq.center();
let rgb = egui::Color32::from_rgb;
match mode {
RowMode::Cmyk => {
let corners = [sq.left_top(), sq.right_top(), sq.right_bottom(), sq.left_bottom()];
// Top, right, bottom, left: magenta, yellow, black, cyan.
let inks = [rgb(236, 0, 140), rgb(255, 230, 0), rgb(30, 30, 30), rgb(0, 174, 239)];
for (k, colour) in inks.into_iter().enumerate() {
let (a, b) = (corners[k], corners[(k + 1) % 4]);
p.add(egui::Shape::convex_polygon(vec![a, b, c], colour, egui::Stroke::NONE));
}
p.rect_stroke(sq, 0.0, egui::Stroke::new(1.0, ink), egui::StrokeKind::Outside);
}
RowMode::Rgb => {
let w = sq.width() / 3.0;
for (k, colour) in [rgb(230, 40, 40), rgb(40, 190, 60), rgb(40, 90, 230)].into_iter().enumerate() {
let x = sq.min.x + w * k as f32;
p.rect_filled(egui::Rect::from_min_max(egui::pos2(x, sq.min.y), egui::pos2(x + w, sq.max.y)), 0.0, colour);
}
p.rect_stroke(sq, 0.0, egui::Stroke::new(1.0, ink), egui::StrokeKind::Outside);
}
RowMode::Gray => {
let w = sq.width() / 4.0;
for k in 0..4u8 {
let x = sq.min.x + w * f32::from(k);
p.rect_filled(
egui::Rect::from_min_max(egui::pos2(x, sq.min.y), egui::pos2(x + w, sq.max.y)),
0.0,
egui::Color32::from_gray(255 - 70 * k),
);
}
p.rect_stroke(sq, 0.0, egui::Stroke::new(1.0, ink), egui::StrokeKind::Outside);
}
RowMode::None => {
p.rect_filled(sq, 0.0, egui::Color32::WHITE);
p.line_segment([sq.left_bottom(), sq.right_top()], egui::Stroke::new(1.5, rgb(220, 30, 30)));
p.rect_stroke(sq, 0.0, egui::Stroke::new(1.0, ink), egui::StrokeKind::Outside);
}
RowMode::Registration => {
p.rect_stroke(sq, 0.0, egui::Stroke::new(1.0, ink), egui::StrokeKind::Inside);
p.circle_stroke(c, 3.0, egui::Stroke::new(1.0, ink));
p.line_segment([c - vec2(4.5, 0.0), c + vec2(4.5, 0.0)], egui::Stroke::new(1.0, ink));
p.line_segment([c - vec2(0.0, 4.5), c + vec2(0.0, 4.5)], egui::Stroke::new(1.0, ink));
}
}
}
/// One Swatches panel row: chip, name, type; click applies it to the fill or stroke in front of
/// the proxy (of the frames or their text, see [`super::colors_affect_text`]), the context menu
/// to the stroke, colour groups and delete.
@@ -267,24 +389,26 @@ fn swatch_row(
let chip = egui::Rect::from_min_size(row.min + vec2(4.0 + indent, 3.0), vec2(16.0, 16.0));
crate::widgets::paint_chip(ui.painter(), chip, c, g);
ui.painter().text(row.min + vec2(28.0 + indent, 11.0), egui::Align2::LEFT_CENTER, &sw.name, egui::FontId::proportional(12.5), t.text);
// Type indicators: spot dot / process square, colour mode.
let kind = match &sw.value {
SwatchValue::Color { color, color_type } => {
let m = match color {
designcraft_color::Color::Cmyk { .. } => "CMYK",
designcraft_color::Color::Rgb { .. } => "RGB",
designcraft_color::Color::Gray { .. } => crate::i18n::tr(&app.ui.language, "Gray"),
};
format!("{}{}", if *color_type == ColorType::Spot { "● " } else { "" }, m)
}
SwatchValue::Gradient { .. } => "Gradient".into(),
// InDesign's columns on the right: not editable, colour type, colour mode.
let marks = row_marks(sw);
let col = |k: f32| egui::Rect::from_min_size(egui::pos2(row.max.x - 19.0 - 18.0 * k, row.min.y + 4.5), vec2(13.0, 13.0));
if marks.not_editable {
paint_not_editable(ui.painter(), col(2.0), t.text_dim);
}
if let Some(kind) = marks.kind {
paint_color_type(ui.painter(), col(1.0), kind, t.text_dim);
}
if let Some(mode) = marks.mode {
paint_mode(ui.painter(), col(0.0), mode, t.text_dim);
}
let label = match &sw.value {
SwatchValue::Gradient { .. } => crate::i18n::tr(&app.ui.language, "Gradient").to_string(),
SwatchValue::Tint { tint, .. } => format!("{:.0}%", tint * 100.0),
_ => String::new(),
};
if sw.locked {
icons::paint(ui.painter(), egui::Rect::from_min_size(egui::pos2(row.max.x - 64.0, row.min.y + 4.0), vec2(13.0, 13.0)), "lock", t.text_dim);
if !label.is_empty() {
ui.painter().text(row.right_center() - vec2(6.0, 0.0), egui::Align2::RIGHT_CENTER, label, egui::FontId::proportional(10.5), t.text_dim);
}
ui.painter().text(row.right_center() - vec2(6.0, 0.0), egui::Align2::RIGHT_CENTER, kind, egui::FontId::proportional(10.5), t.text_dim);
if resp.clicked() && !resp.double_clicked() {
ui.data_mut(|d| d.insert_temp(egui::Id::new(PICKED), sw.name.clone()));
let _ = super::apply_swatch(app, app.ui.stroke_front, &sw.name, None);
@@ -1161,3 +1285,32 @@ mod swatches_header_tests {
assert_eq!((it[1].fill.swatch.as_str(), it[1].fill.tint), ("C=0 M=100 Y=0 K=0", 0.4));
}
}
#[cfg(test)]
mod row_mark_tests {
use designcraft_color::swatch::{Swatch, default_swatches};
use designcraft_color::{Color, ColorType, SwatchValue};
use super::{RowMarks, RowMode, row_marks};
/// The Swatches panel's right-hand columns, as InDesign's: [None], [Registration] and [Black]
/// can't be edited, [Paper] can; colours show their type and mode.
#[test]
fn swatch_rows_mark_type_mode_and_whether_they_can_be_edited() {
let all = default_swatches();
let marks = |name: &str| row_marks(all.iter().find(|s| s.name == name).unwrap());
assert_eq!(marks("[None]"), RowMarks { not_editable: true, kind: None, mode: Some(RowMode::None) });
assert_eq!(marks("[Registration]"), RowMarks { not_editable: true, kind: None, mode: Some(RowMode::Registration) });
assert_eq!(marks("[Paper]"), RowMarks { not_editable: false, kind: None, mode: None });
assert_eq!(marks("[Black]"), RowMarks { not_editable: true, kind: Some(ColorType::Process), mode: Some(RowMode::Cmyk) });
let cyan = all.iter().find(|s| matches!(&s.value, SwatchValue::Color { .. }) && !s.locked).unwrap();
assert_eq!(row_marks(cyan), RowMarks { not_editable: false, kind: Some(ColorType::Process), mode: Some(RowMode::Cmyk) });
let spot = Swatch {
name: "PANTONE".into(),
value: SwatchValue::Color { color: Color::Rgb { r: 1.0, g: 0.0, b: 0.0 }, color_type: ColorType::Spot },
locked: false,
..cyan.clone()
};
assert_eq!(row_marks(&spot), RowMarks { not_editable: false, kind: Some(ColorType::Spot), mode: Some(RowMode::Rgb) });
}
}