From 9792554e00c2fee5d74f2188b046a9515ea1dabd Mon Sep 17 00:00:00 2001 From: paaspaas00 <151774837+paaspaas00@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:19:32 +0200 Subject: [PATCH 1/6] Properties: a double-click on a Fill or Stroke chip opens the Color Picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As in InDesign, a double-click on Properties › Appearance's Fill or Stroke chip opens the Color Picker with that colour (a click still opens the swatches). For selected text, its own Fill and Stroke chips open it for the text. Found in the Learn Skills Daily course, chapter 23. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/ui-egui/src/panels/properties.rs | 79 +++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index a7b5105d3..3230114b1 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -348,6 +348,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-21** · A double-click on a Fill or Stroke chip of Properties › Appearance opens the Color Picker with its colour, as in InDesign (a click still opens the swatches); for selected text, its own chips open it for the text. Found in the Learn Skills Daily course, chapter 23. 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. diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs index 44acf6e2c..ea103f57c 100644 --- a/crates/ui-egui/src/panels/properties.rs +++ b/crates/ui-egui/src/panels/properties.rs @@ -790,6 +790,9 @@ fn appearance_section(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo) { )); resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, "Fill swatch")); swatch_menu(app, &resp, &doc, &i.fill, false, i.is_text && i.count == 1, false); + if resp.double_clicked() { + open_color_picker(app, &doc, &i.fill, false, false); + } crate::rtl::paint( ui.painter(), b.min + vec2(34.0, FIELD_H / 2.0), @@ -812,6 +815,9 @@ fn appearance_section(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo) { )); resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, "Stroke swatch")); swatch_menu(app, &resp, &doc, &i.stroke, true, i.is_text && i.count == 1, false); + if resp.double_clicked() { + open_color_picker(app, &doc, &i.stroke, true, false); + } if place(ui, sub(b, 34.0, 0.0, 56.0, FIELD_H), |ui| link_label(ui, crate::i18n::tr(&app.ui.language, "Stroke"), 56.0)).clicked() { show_panel(app, "stroke"); } @@ -871,6 +877,14 @@ fn appearance_section(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo) { }); } +/// The Color Picker for a Fill or Stroke chip (a double-click on it, as in InDesign), with the +/// chip's colour: the objects', or with `text` the selected text's. +fn open_color_picker(app: &mut DesignApp, doc: &designcraft_doc::Document, swatch: &str, stroke: bool, text: bool) { + let mut fields = super::swatches::picker_fields(doc.resolve_color(swatch, 1.0), stroke); + fields["text"] = json!(text); + app.ui.dialog = Some(crate::dialogs::Dialog::new("colorPicker", fields)); +} + /// The Fill (or Stroke) popup of Properties › Appearance, as InDesign's: Apply To (a text frame's /// frame or its text), the tint, then the swatches. `cur` is the frame's swatch. With `text_only` /// (text selected) it colours the selected text. @@ -1219,7 +1233,13 @@ fn text_appearance_section(app: &mut DesignApp, ui: &mut Ui) { ui.painter().text(chip.center(), egui::Align2::CENTER_CENTER, "T", crate::theme::semibold(12.0), tc); } let resp = ui.interact(chip, ui.id().with(("tchip", k)), Sense::click()).on_hover_text(format!("{label}: {sw}")); + resp.widget_info(|| { + egui::WidgetInfo::labeled(egui::WidgetType::Button, true, if k == 0 { "Text fill swatch" } else { "Text stroke swatch" }) + }); swatch_menu(app, &resp, &doc, &sw, k != 0, false, true); + if resp.double_clicked() { + open_color_picker(app, &doc, &sw, k != 0, true); + } if k == 0 { crate::rtl::paint( ui.painter(), @@ -3300,6 +3320,65 @@ mod tests { assert_eq!(after, name, "clicking the swatch chip applies it"); } + /// The course's chapter 23: a double-click on Properties › Appearance › Fill opens the Color + /// Picker with the fill's colour, as InDesign's (a click opens the swatches). For selected + /// text, the text's Fill chip opens it for the text. + #[test] + fn double_clicking_a_fill_chip_opens_the_color_picker() { + let double_click = |h: &mut Harness<'_, DesignApp>, label: &str| { + let at = h.get_by_label(label).rect().center(); + h.hover_at(at); + h.step(); + for pressed in [true, false, true, false] { + h.event(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE }); + h.step(); + } + h.run_steps(3); + }; + // Frames 1 ms apart, so two clicks make a double-click. + let panel = |app: DesignApp| { + let mut h = Harness::builder().with_size(vec2(320.0, 1400.0)).with_step_dt(0.001).build_ui_state( + |ui, app: &mut DesignApp| { + let id = egui::Id::new("test_fonts"); + if ui.data(|d| d.get_temp::(id)).is_none() { + crate::theme::install_fonts(ui.ctx(), ""); + ui.data_mut(|d| d.insert_temp(id, true)); + return; + } + show(app, ui); + }, + app, + ); + h.run_steps(3); + h + }; + let frame = || { + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + let id = app.run("frame.create", json!({"rect": [72, 72, 300, 200], "content": "unassigned"})).unwrap()["id"].as_u64().unwrap(); + app.run("selection.set", json!({"ids": [id]})).unwrap(); + app.run("object.fill", json!({"swatch": "C=0 M=0 Y=100 K=0"})).unwrap(); + app + }; + let mut h = panel(frame()); + double_click(&mut h, "Fill swatch"); + let d = h.state().ui.dialog.clone().expect("the Color Picker"); + assert_eq!((d.id.as_str(), d.s("target"), d.s("model"), d.b("text")), ("colorPicker", "fill".into(), "cmyk".into(), false)); + assert_eq!(d.fields["cmyk"], json!([0.0, 0.0, 100.0, 0.0])); + let mut h = panel(frame()); + double_click(&mut h, "Stroke swatch"); + assert_eq!(h.state().ui.dialog.as_ref().map(|d| d.s("target")), Some("stroke".into())); + // Selected text: its own Fill chip. + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + let f = app.run("frame.create", json!({"rect": [72, 72, 300, 200], "content": "text", "text": "Lemon"})).unwrap(); + app.run("text.select", json!({"story": f["story"], "anchor": 0, "focus": 5})).unwrap(); + let mut h = panel(app); + double_click(&mut h, "Text fill swatch"); + let d = h.state().ui.dialog.clone().expect("the Color Picker"); + assert_eq!((d.id.as_str(), d.s("target"), d.b("text")), ("colorPicker", "fill".into(), true)); + } + fn panel(app: DesignApp) -> Harness<'static, DesignApp> { let mut h = Harness::builder().with_size(vec2(320.0, 1400.0)).build_ui_state( |ui, app: &mut DesignApp| { From 114e3682fd57901abcc36dcffb17d1dd8d0c6ab7 Mon Sep 17 00:00:00 2001 From: paaspaas00 <151774837+paaspaas00@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:48:00 +0200 Subject: [PATCH 2/6] Properties: the test checks a double-click leaves no swatches popup open Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/properties.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs index ea103f57c..9989ec56a 100644 --- a/crates/ui-egui/src/panels/properties.rs +++ b/crates/ui-egui/src/panels/properties.rs @@ -3362,6 +3362,7 @@ mod tests { }; let mut h = panel(frame()); double_click(&mut h, "Fill swatch"); + assert!(h.query_by_label("Tint:").is_none(), "the swatches popup isn't left open too"); let d = h.state().ui.dialog.clone().expect("the Color Picker"); assert_eq!((d.id.as_str(), d.s("target"), d.s("model"), d.b("text")), ("colorPicker", "fill".into(), "cmyk".into(), false)); assert_eq!(d.fields["cmyk"], json!([0.0, 0.0, 100.0, 0.0])); From 3de3efccafdf84725588e4bec78a08f20e6eb9ed Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 29 Oct 2026 12:50:00 +0100 Subject: [PATCH 3/6] rustfmt Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/properties/tests.rs | 112 +++++++++--------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/crates/ui-egui/src/panels/properties/tests.rs b/crates/ui-egui/src/panels/properties/tests.rs index 91004afa9..96a1b14b6 100644 --- a/crates/ui-egui/src/panels/properties/tests.rs +++ b/crates/ui-egui/src/panels/properties/tests.rs @@ -638,65 +638,65 @@ fn clicking_a_swatch_chip_in_the_appearance_fill_menu_applies_it() { assert_eq!(after, name, "clicking the swatch chip applies it"); } - /// The course's chapter 23: a double-click on Properties › Appearance › Fill opens the Color - /// Picker with the fill's colour, as InDesign's (a click opens the swatches). For selected - /// text, the text's Fill chip opens it for the text. - #[test] - fn double_clicking_a_fill_chip_opens_the_color_picker() { - let double_click = |h: &mut Harness<'_, DesignApp>, label: &str| { - let at = h.get_by_label(label).rect().center(); - h.hover_at(at); +/// The course's chapter 23: a double-click on Properties › Appearance › Fill opens the Color +/// Picker with the fill's colour, as InDesign's (a click opens the swatches). For selected +/// text, the text's Fill chip opens it for the text. +#[test] +fn double_clicking_a_fill_chip_opens_the_color_picker() { + let double_click = |h: &mut Harness<'_, DesignApp>, label: &str| { + let at = h.get_by_label(label).rect().center(); + h.hover_at(at); + h.step(); + for pressed in [true, false, true, false] { + h.event(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE }); h.step(); - for pressed in [true, false, true, false] { - h.event(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE }); - h.step(); - } - h.run_steps(3); - }; - // Frames 1 ms apart, so two clicks make a double-click. - let panel = |app: DesignApp| { - let mut h = Harness::builder().with_size(vec2(320.0, 1400.0)).with_step_dt(0.001).build_ui_state( - |ui, app: &mut DesignApp| { - let id = egui::Id::new("test_fonts"); - if ui.data(|d| d.get_temp::(id)).is_none() { - crate::theme::install_fonts(ui.ctx(), ""); - ui.data_mut(|d| d.insert_temp(id, true)); - return; - } - show(app, ui); - }, - app, - ); - h.run_steps(3); - h - }; - let frame = || { - let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); - app.run("file.new", json!({})).unwrap(); - let id = app.run("frame.create", json!({"rect": [72, 72, 300, 200], "content": "unassigned"})).unwrap()["id"].as_u64().unwrap(); - app.run("selection.set", json!({"ids": [id]})).unwrap(); - app.run("object.fill", json!({"swatch": "C=0 M=0 Y=100 K=0"})).unwrap(); - app - }; - let mut h = panel(frame()); - double_click(&mut h, "Fill swatch"); - assert!(h.query_by_label("Tint:").is_none(), "the swatches popup isn't left open too"); - let d = h.state().ui.dialog.clone().expect("the Color Picker"); - assert_eq!((d.id.as_str(), d.s("target"), d.s("model"), d.b("text")), ("colorPicker", "fill".into(), "cmyk".into(), false)); - assert_eq!(d.fields["cmyk"], json!([0.0, 0.0, 100.0, 0.0])); - let mut h = panel(frame()); - double_click(&mut h, "Stroke swatch"); - assert_eq!(h.state().ui.dialog.as_ref().map(|d| d.s("target")), Some("stroke".into())); - // Selected text: its own Fill chip. + } + h.run_steps(3); + }; + // Frames 1 ms apart, so two clicks make a double-click. + let panel = |app: DesignApp| { + let mut h = Harness::builder().with_size(vec2(320.0, 1400.0)).with_step_dt(0.001).build_ui_state( + |ui, app: &mut DesignApp| { + let id = egui::Id::new("test_fonts"); + if ui.data(|d| d.get_temp::(id)).is_none() { + crate::theme::install_fonts(ui.ctx(), ""); + ui.data_mut(|d| d.insert_temp(id, true)); + return; + } + show(app, ui); + }, + app, + ); + h.run_steps(3); + h + }; + let frame = || { let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); app.run("file.new", json!({})).unwrap(); - let f = app.run("frame.create", json!({"rect": [72, 72, 300, 200], "content": "text", "text": "Lemon"})).unwrap(); - app.run("text.select", json!({"story": f["story"], "anchor": 0, "focus": 5})).unwrap(); - let mut h = panel(app); - double_click(&mut h, "Text fill swatch"); - let d = h.state().ui.dialog.clone().expect("the Color Picker"); - assert_eq!((d.id.as_str(), d.s("target"), d.b("text")), ("colorPicker", "fill".into(), true)); - } + let id = app.run("frame.create", json!({"rect": [72, 72, 300, 200], "content": "unassigned"})).unwrap()["id"].as_u64().unwrap(); + app.run("selection.set", json!({"ids": [id]})).unwrap(); + app.run("object.fill", json!({"swatch": "C=0 M=0 Y=100 K=0"})).unwrap(); + app + }; + let mut h = panel(frame()); + double_click(&mut h, "Fill swatch"); + assert!(h.query_by_label("Tint:").is_none(), "the swatches popup isn't left open too"); + let d = h.state().ui.dialog.clone().expect("the Color Picker"); + assert_eq!((d.id.as_str(), d.s("target"), d.s("model"), d.b("text")), ("colorPicker", "fill".into(), "cmyk".into(), false)); + assert_eq!(d.fields["cmyk"], json!([0.0, 0.0, 100.0, 0.0])); + let mut h = panel(frame()); + double_click(&mut h, "Stroke swatch"); + assert_eq!(h.state().ui.dialog.as_ref().map(|d| d.s("target")), Some("stroke".into())); + // Selected text: its own Fill chip. + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + let f = app.run("frame.create", json!({"rect": [72, 72, 300, 200], "content": "text", "text": "Lemon"})).unwrap(); + app.run("text.select", json!({"story": f["story"], "anchor": 0, "focus": 5})).unwrap(); + let mut h = panel(app); + double_click(&mut h, "Text fill swatch"); + let d = h.state().ui.dialog.clone().expect("the Color Picker"); + assert_eq!((d.id.as_str(), d.s("target"), d.b("text")), ("colorPicker", "fill".into(), true)); +} fn panel(app: DesignApp) -> Harness<'static, DesignApp> { let mut h = Harness::builder().with_size(vec2(320.0, 1400.0)).build_ui_state( From a9814cae3995ac610f0ddfb32994ffa436740db9 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 29 Oct 2026 15:27:13 +0100 Subject: [PATCH 4/6] Fill colour picker: the picker opener shared with the text panel, the swatches module's path, one Fill popup per chip Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/properties/object.rs | 4 ++-- crates/ui-egui/src/panels/properties/text.rs | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/panels/properties/object.rs b/crates/ui-egui/src/panels/properties/object.rs index 50f8e498e..e9daa0f32 100644 --- a/crates/ui-egui/src/panels/properties/object.rs +++ b/crates/ui-egui/src/panels/properties/object.rs @@ -378,8 +378,8 @@ pub(super) fn appearance_section(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo) /// The Color Picker for a Fill or Stroke chip (a double-click on it, as in InDesign), with the /// chip's colour: the objects', or with `text` the selected text's. -fn open_color_picker(app: &mut DesignApp, doc: &designcraft_doc::Document, swatch: &str, stroke: bool, text: bool) { - let mut fields = super::swatches::picker_fields(doc.resolve_color(swatch, 1.0), stroke); +pub(super) fn open_color_picker(app: &mut DesignApp, doc: &designcraft_doc::Document, swatch: &str, stroke: bool, text: bool) { + let mut fields = crate::panels::swatches::picker_fields(doc.resolve_color(swatch, 1.0), stroke); fields["text"] = json!(text); app.ui.dialog = Some(crate::dialogs::Dialog::new("colorPicker", fields)); } diff --git a/crates/ui-egui/src/panels/properties/text.rs b/crates/ui-egui/src/panels/properties/text.rs index 05a2175a4..64c6d6890 100644 --- a/crates/ui-egui/src/panels/properties/text.rs +++ b/crates/ui-egui/src/panels/properties/text.rs @@ -1,4 +1,4 @@ -use super::object::swatch_menu; +use super::object::{open_color_picker, swatch_menu}; use super::typography::{para_field_min, show_typography_panel}; use super::{ Align, DesignApp, FIELD_H, NumField, Rect, Sense, Stroke, StrokeKind, Tokens, Ui, Value, block, full_width, icon_toggle, icons, json, link_label, @@ -128,7 +128,6 @@ pub(super) fn text_appearance_section(app: &mut DesignApp, ui: &mut Ui) { ui.painter().text(chip.center(), egui::Align2::CENTER_CENTER, "T", crate::theme::semibold(12.0), tc); } let resp = ui.interact(chip, ui.scope_id().with(("tchip", k)), Sense::click()).on_hover_text(format!("{label}: {sw}")); - swatch_menu(app, &resp, &doc, &sw, k != 0, false, true); resp.widget_info(|| { egui::WidgetInfo::labeled(egui::WidgetType::Button, true, if k == 0 { "Text fill swatch" } else { "Text stroke swatch" }) }); From 15b2f49342cf648d5216751d027815ab654f67a4 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 29 Oct 2026 18:49:03 +0100 Subject: [PATCH 5/6] egui 0.37: the renamed APIs in this branch's code Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/properties/tests.rs | 2 +- crates/ui-egui/src/panels/properties/text.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/panels/properties/tests.rs b/crates/ui-egui/src/panels/properties/tests.rs index 96a1b14b6..2f6a6f4fb 100644 --- a/crates/ui-egui/src/panels/properties/tests.rs +++ b/crates/ui-egui/src/panels/properties/tests.rs @@ -657,7 +657,7 @@ fn double_clicking_a_fill_chip_opens_the_color_picker() { let panel = |app: DesignApp| { let mut h = Harness::builder().with_size(vec2(320.0, 1400.0)).with_step_dt(0.001).build_ui_state( |ui, app: &mut DesignApp| { - let id = egui::Id::new("test_fonts"); + let id = egui::Id::unique("test_fonts"); if ui.data(|d| d.get_temp::(id)).is_none() { crate::theme::install_fonts(ui.ctx(), ""); ui.data_mut(|d| d.insert_temp(id, true)); diff --git a/crates/ui-egui/src/panels/properties/text.rs b/crates/ui-egui/src/panels/properties/text.rs index 64c6d6890..ad25a5a5a 100644 --- a/crates/ui-egui/src/panels/properties/text.rs +++ b/crates/ui-egui/src/panels/properties/text.rs @@ -129,7 +129,7 @@ pub(super) fn text_appearance_section(app: &mut DesignApp, ui: &mut Ui) { } let resp = ui.interact(chip, ui.scope_id().with(("tchip", k)), Sense::click()).on_hover_text(format!("{label}: {sw}")); resp.widget_info(|| { - egui::WidgetInfo::labeled(egui::WidgetType::Button, true, if k == 0 { "Text fill swatch" } else { "Text stroke swatch" }) + egui::WidgetInfo::labeled(egui::Role::Button, true, if k == 0 { "Text fill swatch" } else { "Text stroke swatch" }) }); swatch_menu(app, &resp, &doc, &sw, k != 0, false, true); if resp.double_clicked() { From 9577b7f09cb0d67846905a05fecd966115b4f087 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 29 Oct 2026 18:51:14 +0100 Subject: [PATCH 6/6] rustfmt Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/properties/text.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/ui-egui/src/panels/properties/text.rs b/crates/ui-egui/src/panels/properties/text.rs index ad25a5a5a..a9d6e9baa 100644 --- a/crates/ui-egui/src/panels/properties/text.rs +++ b/crates/ui-egui/src/panels/properties/text.rs @@ -128,9 +128,7 @@ pub(super) fn text_appearance_section(app: &mut DesignApp, ui: &mut Ui) { ui.painter().text(chip.center(), egui::Align2::CENTER_CENTER, "T", crate::theme::semibold(12.0), tc); } let resp = ui.interact(chip, ui.scope_id().with(("tchip", k)), Sense::click()).on_hover_text(format!("{label}: {sw}")); - resp.widget_info(|| { - egui::WidgetInfo::labeled(egui::Role::Button, true, if k == 0 { "Text fill swatch" } else { "Text stroke swatch" }) - }); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, if k == 0 { "Text fill swatch" } else { "Text stroke swatch" })); swatch_menu(app, &resp, &doc, &sw, k != 0, false, true); if resp.double_clicked() { open_color_picker(app, &doc, &sw, k != 0, true);