mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-02 22:50:27 -06:00
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:
co-authored by
Claude Opus 5.5
parent
8dff7ecdb0
commit
d24ce2cfd3
@@ -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.
|
||||
|
||||
@@ -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) });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user