From f0ae1c318add8067879d667495cec2b1d188dd9e Mon Sep 17 00:00:00 2001 From: paaspaas00 <151774837+paaspaas00@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:36:22 +0200 Subject: [PATCH 1/3] Color panel edits the gradient stop selected in the Gradient panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With a gradient fill or stroke, the Color panel showed "[None] — choose a colour on the ramp", and a spectrum click replaced the whole gradient with a flat colour. As in InDesign, it now shows the stop selected in the Gradient panel (the stop before a selected midpoint) with its values, and its C/M/Y/K or R/G/B sliders and spectrum recolour that stop, keeping the others. Found in the Learn Skills Daily course, chapter 24. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + .../ui-egui/src/panels/color_panel_tests.rs | 39 ++++++++ crates/ui-egui/src/panels/swatches.rs | 96 +++++++++++++++++-- 3 files changed, 126 insertions(+), 10 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index a7b5105d3..e59952263 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** · With a gradient fill or stroke, the Color panel shows the stop selected in the Gradient panel (the stop before a selected midpoint) with its values, and its C/M/Y/K or R/G/B sliders and spectrum colour that stop, keeping the others, as in InDesign. It showed "[None] — choose a colour on the ramp", and a spectrum click replaced the whole gradient with a flat colour. Found in the Learn Skills Daily course, chapter 24. 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/color_panel_tests.rs b/crates/ui-egui/src/panels/color_panel_tests.rs index cd102b2e3..05d5899b6 100644 --- a/crates/ui-egui/src/panels/color_panel_tests.rs +++ b/crates/ui-egui/src/panels/color_panel_tests.rs @@ -81,3 +81,42 @@ fn a_typed_tint_is_kept_while_typing_and_applied_on_enter_or_leaving_the_field() h.run_steps(3); assert_eq!(fill_tint(h.state(), id), 0.0, "dragging the slider to the left end applies 0 %"); } + +/// The course's chapter 24: with a gradient fill, the Color panel shows the stop selected in the +/// Gradient panel (black, then) and its C/M/Y/K fields colour that stop, keeping the others. +#[test] +fn the_color_panel_edits_the_gradient_stop_selected_in_the_gradient_panel() { + use super::swatches::{GRAD_SEL, RampSel}; + 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": [36, 36, 200, 200], "content": "unassigned"})).unwrap()["id"].as_u64().unwrap(); + app.run("selection.set", json!({"ids": [id]})).unwrap(); + // InDesign's default White → Black. + app.run("object.gradient", json!({})).unwrap(); + let mut h = harness(app); + h.run_steps(2); + h.ctx.data_mut(|d| d.insert_temp(egui::Id::new(GRAD_SEL), RampSel::Stop(1))); + h.run_steps(3); + assert!(h.query_by_label("R=0 G=0 B=0").is_some(), "the black stop's values"); + // C: the topmost field. + let at = h + .query_all_by(|n| matches!(n.role(), Role::SpinButton | Role::TextInput)) + .map(|n| n.rect()) + .min_by(|a, b| a.min.y.total_cmp(&b.min.y)) + .unwrap() + .center(); + h.hover_at(at); + h.drag_at(at); + h.drop_at(at); + h.run_steps(2); + h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A); + h.event(egui::Event::Text("100".into())); + h.run_steps(2); + h.key_press(egui::Key::Enter); + h.run_steps(3); + let doc = h.state().session.doc().unwrap().doc.clone(); + let fill = &doc.item(designcraft_doc::ItemId(id)).unwrap().fill.swatch; + let g = designcraft_color::swatch::resolve_gradient(&doc.swatches, fill).cloned().expect("still a gradient"); + assert_eq!(g.stops[1].color, designcraft_color::Color::cmyk(1.0, 0.0, 0.0, 1.0), "C 100 on the black stop"); + assert_eq!(g.stops[0].color, designcraft_color::Gradient::default().stops[0].color, "the white stop stays"); +} diff --git a/crates/ui-egui/src/panels/swatches.rs b/crates/ui-egui/src/panels/swatches.rs index 3e7030e87..8ae422e06 100644 --- a/crates/ui-egui/src/panels/swatches.rs +++ b/crates/ui-egui/src/panels/swatches.rs @@ -397,6 +397,11 @@ pub fn color_panel(app: &mut DesignApp, ui: &mut egui::Ui) { }); app.ui.stroke_front = stroke; ui.data_mut(|d| d.insert_temp(mode_id, mode)); + // A gradient: the stop selected in the Gradient panel, as InDesign's Color panel shows it. + if !text && let Some(g) = designcraft_color::swatch::resolve_gradient(&doc.swatches, &name).cloned() { + gradient_stop(app, ui, &g, target, mode); + return; + } let sw = doc.swatch(&name).cloned(); let Some(c) = doc.resolve_color(&name, 1.0) else { ui.label(crate::rtl::widget( @@ -544,7 +549,79 @@ fn cmyk_ramp(k: usize, x: f32) -> egui::Color32 { } /// The spectrum ramp: hue across, light to dark down; click to apply. +/// The Color panel on a gradient fill (or stroke): the stop selected in the Gradient panel (the +/// stop before a selected midpoint), in the panel's colour mode, as InDesign's. Its fields and the +/// spectrum colour that stop. +fn gradient_stop(app: &mut DesignApp, ui: &mut egui::Ui, g: &designcraft_color::Gradient, target: &str, mode: ColorMode) { + use designcraft_color::Color; + let t = Tokens::get(ui.ctx()); + let sel: RampSel = ui.data(|d| d.get_temp(egui::Id::new(GRAD_SEL))).unwrap_or(RampSel::Stop(0)); + let i = match sel { + RampSel::Stop(i) | RampSel::Mid(i) => i, + } + .min(g.stops.len().saturating_sub(1)); + let Some(stop) = g.stops.get(i) else { return }; + let c = stop.color; + let pct = |v: f32| (v * 100.0).round(); + let values = match c { + Color::Cmyk { c, m, y, k } => format!("C={} M={} Y={} K={}", pct(c), pct(m), pct(y), pct(k)), + Color::Gray { k } => format!("K={}", pct(k)), + Color::Rgb { .. } => { + let [r, g, b, _] = c.to_rgba8(1.0); + format!("R={r} G={g} B={b}") + } + }; + ui.label(egui::RichText::new(values).size(11.0).color(t.text_dim)); + let mut new = None; + match mode { + ColorMode::Cmyk => { + let mut v = c.to_cmyk(); + let mut done = false; + for (k, label) in ["C", "M", "Y", "K"].iter().enumerate() { + done |= channel(ui, label, &mut v[k], 100.0, "%", |x| cmyk_ramp(k, x)); + } + if done { + new = Some(Color::cmyk(v[0], v[1], v[2], v[3])); + } + } + ColorMode::Rgb => { + let mut v = c.to_rgb(); + let mut done = false; + for (k, label) in ["R", "G", "B"].iter().enumerate() { + done |= channel(ui, label, &mut v[k], 255.0, "", |x| { + let mut p = [0.0; 3]; + p[k] = x; + egui::Color32::from_rgb((p[0] * 255.0) as u8, (p[1] * 255.0) as u8, (p[2] * 255.0) as u8) + }); + } + if done { + new = Some(Color::rgb(v[0], v[1], v[2])); + } + } + ColorMode::Lab => { + let lab = c.to_lab(); + ui.label(egui::RichText::new(format!("L {:.0} a {:.0} b {:.0}", lab.l, lab.a, lab.b)).color(t.text_dim)); + } + } + if let Some([r, gr, b]) = spectrum_pick(ui) { + new = Some(Color::rgb(r, gr, b)); + } + if let Some(c) = new { + let mut stops = stops_json(g); + set_stop(&mut stops, i, "color", color_json(&c)); + let _ = app.run("object.gradient", json!({"stops": stops, "target": target})); + } +} + fn spectrum(app: &mut DesignApp, ui: &mut egui::Ui, target: &str) { + if let Some([r, g, b]) = spectrum_pick(ui) { + let text = super::colors_affect_text(app); + let _ = app.run("object.color", json!({"color": {"model": "rgb", "r": r, "g": g, "b": b}, "target": target, "text": text})); + } +} + +/// The spectrum ramp; a click on it gives that colour (RGB, 0–1, in 1/255 steps). +fn spectrum_pick(ui: &mut egui::Ui) -> Option<[f32; 3]> { let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 36.0), Sense::click()); let at = |u: f32, v: f32| -> [f32; 3] { let c = egui::ecolor::Hsva::new(u, (v * 2.0).min(1.0), (2.0 - v * 2.0).min(1.0), 1.0).to_rgb(); @@ -561,14 +638,9 @@ fn spectrum(app: &mut DesignApp, ui: &mut egui::Ui, target: &str) { ui.painter().rect_filled(cell, 0.0, egui::Color32::from_rgb((c[0] * 255.0) as u8, (c[1] * 255.0) as u8, (c[2] * 255.0) as u8)); } } - if resp.clicked() - && let Some(p) = resp.interact_pointer_pos() - { - let c = at(((p.x - r.min.x) / r.width()).clamp(0.0, 1.0), ((p.y - r.min.y) / r.height()).clamp(0.0, 1.0)); - let text = super::colors_affect_text(app); - let q = |v: f32| (v * 255.0).round() / 255.0; - let _ = app.run("object.color", json!({"color": {"model": "rgb", "r": q(c[0]), "g": q(c[1]), "b": q(c[2])}, "target": target, "text": text})); - } + let p = resp.interact_pointer_pos().filter(|_| resp.clicked())?; + let c = at(((p.x - r.min.x) / r.width()).clamp(0.0, 1.0), ((p.y - r.min.y) / r.height()).clamp(0.0, 1.0)); + Some(c.map(|v| (v * 255.0).round() / 255.0)) } /// A swatch dragged out of the Swatches panel (the Gradient panel's ramp takes it as a stop). @@ -621,9 +693,13 @@ fn thumbnail(p: &egui::Painter, r: egui::Rect, g: &designcraft_color::Gradient, p.rect_stroke(r, 0.0, egui::Stroke::new(1.0, border), egui::StrokeKind::Outside); } +/// Temp-data key of what's selected on the Gradient panel's ramp ([`RampSel`]): the Color panel +/// edits that stop. +pub(super) const GRAD_SEL: &str = "grad_sel"; + /// What's selected on the ramp: a stop, or the midpoint after a stop. #[derive(Clone, Copy, PartialEq, Debug)] -enum RampSel { +pub(super) enum RampSel { Stop(usize), Mid(usize), } @@ -666,7 +742,7 @@ pub fn gradient_panel(app: &mut DesignApp, ui: &mut egui::Ui) { let is_gradient = cur.is_some(); let g = cur.unwrap_or_default(); let radial = g.kind == designcraft_color::GradientKind::Radial; - let sel_id = egui::Id::new("grad_sel"); + let sel_id = egui::Id::new(GRAD_SEL); let drag_id = egui::Id::new("grad_drag"); let mut sel: RampSel = ui.data(|d| d.get_temp(sel_id)).unwrap_or(RampSel::Stop(0)); let last = g.stops.len().saturating_sub(1); From 8f9c9dd6cc49331991ff6caa8a2aa1168799fa93 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 22 Oct 2026 01:38:43 +0200 Subject: [PATCH 2/3] Color panel: the spectrum's doc comment back above `spectrum` Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/swatches.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui-egui/src/panels/swatches.rs b/crates/ui-egui/src/panels/swatches.rs index 8ae422e06..02d5c51c1 100644 --- a/crates/ui-egui/src/panels/swatches.rs +++ b/crates/ui-egui/src/panels/swatches.rs @@ -548,7 +548,6 @@ fn cmyk_ramp(k: usize, x: f32) -> egui::Color32 { egui::Color32::from_rgb((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8) } -/// The spectrum ramp: hue across, light to dark down; click to apply. /// The Color panel on a gradient fill (or stroke): the stop selected in the Gradient panel (the /// stop before a selected midpoint), in the panel's colour mode, as InDesign's. Its fields and the /// spectrum colour that stop. @@ -613,6 +612,7 @@ fn gradient_stop(app: &mut DesignApp, ui: &mut egui::Ui, g: &designcraft_color:: } } +/// The spectrum ramp: hue across, light to dark down; click to apply. fn spectrum(app: &mut DesignApp, ui: &mut egui::Ui, target: &str) { if let Some([r, g, b]) = spectrum_pick(ui) { let text = super::colors_affect_text(app); From 38d84c00814c782ad1c8bdd943c1b5d49daf537a Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 29 Oct 2026 17:31:39 +0100 Subject: [PATCH 3/3] egui 0.37: the renamed APIs in this branch's code Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/color_panel_tests.rs | 2 +- crates/ui-egui/src/panels/swatches.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/panels/color_panel_tests.rs b/crates/ui-egui/src/panels/color_panel_tests.rs index 05d5899b6..153216100 100644 --- a/crates/ui-egui/src/panels/color_panel_tests.rs +++ b/crates/ui-egui/src/panels/color_panel_tests.rs @@ -95,7 +95,7 @@ fn the_color_panel_edits_the_gradient_stop_selected_in_the_gradient_panel() { app.run("object.gradient", json!({})).unwrap(); let mut h = harness(app); h.run_steps(2); - h.ctx.data_mut(|d| d.insert_temp(egui::Id::new(GRAD_SEL), RampSel::Stop(1))); + h.ctx.data_mut(|d| d.insert_temp(egui::Id::unique(GRAD_SEL), RampSel::Stop(1))); h.run_steps(3); assert!(h.query_by_label("R=0 G=0 B=0").is_some(), "the black stop's values"); // C: the topmost field. diff --git a/crates/ui-egui/src/panels/swatches.rs b/crates/ui-egui/src/panels/swatches.rs index 92d7ff2ef..854e16a89 100644 --- a/crates/ui-egui/src/panels/swatches.rs +++ b/crates/ui-egui/src/panels/swatches.rs @@ -581,7 +581,7 @@ fn cmyk_ramp(k: usize, x: f32) -> egui::Color32 { fn gradient_stop(app: &mut DesignApp, ui: &mut egui::Ui, g: &designcraft_color::Gradient, target: &str, mode: ColorMode) { use designcraft_color::Color; let t = Tokens::get(ui.ctx()); - let sel: RampSel = ui.data(|d| d.get_temp(egui::Id::new(GRAD_SEL))).unwrap_or(RampSel::Stop(0)); + let sel: RampSel = ui.data(|d| d.get_temp(egui::Id::unique(GRAD_SEL))).unwrap_or(RampSel::Stop(0)); let i = match sel { RampSel::Stop(i) | RampSel::Mid(i) => i, }