From 37e632881a71b474f201adb9c44fd43368b6d1b2 Mon Sep 17 00:00:00 2001 From: moppeeer <339764117+moppeeer@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:52:01 +0200 Subject: [PATCH 1/3] UI.ColorDefaults: show document defaults in the Color panel When nothing is selected, display the new-object fill and stroke supplied by #1081. Existing selection targets retain their path. Exercise a real spectrum click, existing/new shape colors and undo in the whole-window regression. Co-Authored-By: GPT-6 Astra --- crates/ui-egui/src/panels/swatches.rs | 47 ++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/panels/swatches.rs b/crates/ui-egui/src/panels/swatches.rs index 3e7030e87..9cd71ac82 100644 --- a/crates/ui-egui/src/panels/swatches.rs +++ b/crates/ui-egui/src/panels/swatches.rs @@ -340,7 +340,15 @@ pub enum ColorMode { /// swatch), the spectrum ramp, Add to Swatches. Edits apply unnamed colours (`object.color`). pub fn color_panel(app: &mut DesignApp, ui: &mut egui::Ui) { let t = Tokens::get(ui.ctx()); - let Some((fill, fill_tint, stroke_sw, stroke_tint)) = super::color_target(app) else { + let target = super::color_target(app).or_else(|| { + let state = app.session.active()?; + if !state.selection.items.is_empty() || state.selection.text.is_some() || state.selection.cells.is_some() { + return None; + } + let colors = &state.doc.settings.new_object; + Some((colors.fill.clone(), colors.fill_tint, colors.stroke.clone(), colors.stroke_tint)) + }); + let Some((fill, fill_tint, stroke_sw, stroke_tint)) = target else { ui.label(crate::rtl::widget( ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "Select an object or text to color its fill or stroke.")).color(t.text_dim), @@ -546,6 +554,7 @@ fn cmyk_ramp(k: usize, x: f32) -> egui::Color32 { /// The spectrum ramp: hue across, light to dark down; click to apply. fn spectrum(app: &mut DesignApp, ui: &mut egui::Ui, target: &str) { let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 36.0), Sense::click()); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, crate::i18n::tr(&app.ui.language, "Color spectrum"))); 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(); [c[0], c[1], c[2]] @@ -586,6 +595,42 @@ fn color_json(c: &designcraft_color::Color) -> serde_json::Value { } } +#[cfg(test)] +mod color_panel_defaults_tests { + use super::*; + use egui_kittest::kittest::Queryable; + + #[test] + fn color_panel_with_no_selection_edits_future_shapes_without_recoloring_existing_ones() { + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + let old = + designcraft_doc::ItemId(app.run("frame.create", json!({"rect":[72,72,200,200],"content":"unassigned"})).unwrap()["id"].as_u64().unwrap()); + let original = app.session.active().unwrap().doc.item(old).unwrap().stroke.clone(); + app.run("selection.set", json!({"ids":[]})).unwrap(); + app.ui.stroke_front = true; + app.run("window.panel", json!({"panel":"color"})).unwrap(); + let mut h = crate::test_window::open(app, vec2(1440.0, 900.0)); + assert!(h.query_by_label("Select an object or text to color its fill or stroke.").is_none()); + let ramp = h.get_by_label("Color spectrum").rect(); + crate::test_window::click_at(&mut h, egui::pos2(ramp.left() + ramp.width() / 3.0, ramp.center().y)); + let defaults = h.state().app.session.active().unwrap().doc.settings.new_object.clone(); + assert_ne!(defaults.stroke, original.swatch); + assert_eq!(h.state().app.session.active().unwrap().doc.item(old).unwrap().stroke, original); + h.state_mut().app.run("selection.set", json!({"ids":[old.0]})).unwrap(); + assert_eq!(crate::panels::color_target(&mut h.state_mut().app).unwrap().2, original.swatch); + h.state_mut().app.run("selection.set", json!({"ids":[]})).unwrap(); + let new = designcraft_doc::ItemId( + h.state_mut().app.run("frame.create", json!({"rect":[240,72,368,200],"content":"unassigned"})).unwrap()["id"].as_u64().unwrap(), + ); + assert_eq!(h.state().app.session.active().unwrap().doc.item(new).unwrap().stroke.swatch, defaults.stroke); + h.state_mut().app.run("edit.undo", json!({})).unwrap(); + h.state_mut().app.run("edit.undo", json!({})).unwrap(); + assert_eq!(h.state().app.session.active().unwrap().doc.settings.new_object.stroke, original.swatch); + assert_eq!(h.state().app.session.active().unwrap().doc.item(old).unwrap().stroke, original); + } +} + /// Stops in `object.gradient` form. fn stops_json(g: &designcraft_color::Gradient) -> Vec { g.stops From 83e789340c8244c3ab0595ea62df109f9e244e0d Mon Sep 17 00:00:00 2001 From: moppeeer Date: Tue, 20 Oct 2026 18:17:13 +0200 Subject: [PATCH 2/3] PR-1141: Swatches panel shows new-shape defaults; translate the spectrum label; test the fill Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/i18n.rs | 9 ++++ crates/ui-egui/src/i18n/ar.rs | 1 + crates/ui-egui/src/i18n/cs.rs | 1 + crates/ui-egui/src/i18n/de.rs | 1 + crates/ui-egui/src/i18n/it.rs | 1 + crates/ui-egui/src/i18n/ja.rs | 1 + crates/ui-egui/src/i18n/ka.rs | 1 + crates/ui-egui/src/i18n/pl.rs | 1 + crates/ui-egui/src/i18n/pt_br.rs | 1 + crates/ui-egui/src/i18n/ru.rs | 1 + crates/ui-egui/src/i18n/uk.rs | 1 + crates/ui-egui/src/panels/mod.rs | 13 ++++++ crates/ui-egui/src/panels/swatches.rs | 62 +++++++++++++++++++++++---- 13 files changed, 85 insertions(+), 9 deletions(-) diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index efe089ef9..1bd7074bd 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -826,6 +826,7 @@ const TABLE: &[(&str, [&str; 5])] = &[ ("Choose Image…", ["Choose Image…", "Choisir une image…", "Choose Image…", "Choose Image…", "选择图像…"]), ("Class:", ["Class:", "Classe :", "Class:", "Class:", "类别:"]), ("Color Picker", ["Color Picker", "Sélecteur de couleurs", "Color Picker", "Color Picker", "拾色器"]), + ("Color spectrum", ["Farbspektrum", "Spectre de couleurs", "Espectro de color", "カラースペクトル", "色谱"]), ("Color Settings", ["Color Settings", "Réglages des couleurs", "Color Settings", "Color Settings", "颜色设置"]), ("Color:", ["Color:", "Couleur :", "Color:", "Color:", "颜色:"]), ("Columns", ["Columns", "Colonnes", "Columns", "Columns", "栏"]), @@ -5132,4 +5133,12 @@ mod tests { } } } + + #[test] + fn color_panel_spectrum_label_is_translated_in_every_language() { + for (code, _) in LANGUAGES.iter().filter(|(c, _)| !c.is_empty()) { + assert_ne!(tr(code, "Color spectrum"), "Color spectrum", "{code}"); + } + assert_eq!(tr("", "Color spectrum"), "Color spectrum"); + } } diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index b050de38b..d3466915a 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -349,6 +349,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Object Layer Options", "خيارات طبقات الكائن"), ("Colour Picker", "منتقي الألوان"), ("Color Picker", "أداة اختيار الألوان"), + ("Color spectrum", "طيف الألوان"), ("Color Settings", "إعدادات الألوان"), ("User Dictionary", "قاموس المستخدم"), ("Menu Customization", "تخصيص القوائم"), diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs index e77e72a1c..8ee256913 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -154,6 +154,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Choose Image…", "Vybrat obrázek…"), ("Class:", "Třída:"), ("Color Picker", "Výběr barvy"), + ("Color spectrum", "Barevné spektrum"), ("Color Settings", "Nastavení barev"), ("Color:", "Barva:"), ("Columns", "Sloupce"), diff --git a/crates/ui-egui/src/i18n/de.rs b/crates/ui-egui/src/i18n/de.rs index efc128858..9747be28e 100644 --- a/crates/ui-egui/src/i18n/de.rs +++ b/crates/ui-egui/src/i18n/de.rs @@ -198,6 +198,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Choose Image…", "Bild auswählen …"), ("Class:", "Klasse:"), ("Color Picker", "Farbwähler"), + ("Color spectrum", "Farbspektrum"), ("Color Settings", "Farbeinstellungen"), ("Color:", "Farbe:"), ("Columns", "Spalten"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index a918a9829..9d4fcdd2f 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -265,6 +265,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Choose Image…", "Scegli immagine…"), ("Class:", "Classe:"), ("Color Picker", "Selettore colore"), + ("Color spectrum", "Spettro dei colori"), ("Color Settings", "Impostazioni colore"), ("Color:", "Colore:"), ("Columns", "Colonne"), diff --git a/crates/ui-egui/src/i18n/ja.rs b/crates/ui-egui/src/i18n/ja.rs index 5647ca940..c8f33adf3 100644 --- a/crates/ui-egui/src/i18n/ja.rs +++ b/crates/ui-egui/src/i18n/ja.rs @@ -383,6 +383,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Choose Image…", "画像を選択…"), ("Class:", "クラス:"), ("Color Picker", "カラーピッカー"), + ("Color spectrum", "カラースペクトル"), ("Color Settings", "カラー設定"), ("Color:", "カラー:"), ("Columns", "列数"), diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index 975da9ca1..7a80d8455 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -187,6 +187,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Choose Image…", "გამოსახულების არჩევა…"), ("Class:", "კლასი:"), ("Color Picker", "ფერის ამრჩევი"), + ("Color spectrum", "ფერთა სპექტრი"), ("Color Settings", "ფერის პარამეტრები"), ("Color:", "ფერი:"), ("Columns", "სვეტები"), diff --git a/crates/ui-egui/src/i18n/pl.rs b/crates/ui-egui/src/i18n/pl.rs index 68d685d99..c70ba75ad 100644 --- a/crates/ui-egui/src/i18n/pl.rs +++ b/crates/ui-egui/src/i18n/pl.rs @@ -154,6 +154,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Choose Image…", "Wybierz obraz…"), ("Class:", "Klasa:"), ("Color Picker", "Próbnik kolorów"), + ("Color spectrum", "Spektrum kolorów"), ("Color Settings", "Ustawienia kolorów"), ("Color:", "Kolor:"), ("Columns", "Łamy"), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index 8521de594..8b1a1af7e 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -357,6 +357,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Object Layer Options", "Opções de camada do objeto"), ("Colour Picker", "Seletor de cores"), ("Color Picker", "Seletor de cores"), + ("Color spectrum", "Espectro de cores"), ("Color Settings", "Configurações de cores"), ("User Dictionary", "Dicionário do usuário"), ("Menu Customization", "Personalização de menus"), diff --git a/crates/ui-egui/src/i18n/ru.rs b/crates/ui-egui/src/i18n/ru.rs index 251555ca5..a4bc3e7f5 100644 --- a/crates/ui-egui/src/i18n/ru.rs +++ b/crates/ui-egui/src/i18n/ru.rs @@ -154,6 +154,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Choose Image…", "Выбрать изображение…"), ("Class:", "Класс:"), ("Color Picker", "Палитра цветов"), + ("Color spectrum", "Цветовой спектр"), ("Color Settings", "Настройка цветов"), ("Color:", "Цвет:"), ("Columns", "Колонки"), diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index dd4a1434d..1fb033cce 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -268,6 +268,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Choose Image…", "Вибрати зображення…"), ("Class:", "Клас:"), ("Color Picker", "Палітра кольорів"), + ("Color spectrum", "Колірний спектр"), ("Color Settings", "Налаштування кольору"), ("Color:", "Колір:"), ("Columns", "Колонки"), diff --git a/crates/ui-egui/src/panels/mod.rs b/crates/ui-egui/src/panels/mod.rs index 7c40f3b1b..bc3f71795 100644 --- a/crates/ui-egui/src/panels/mod.rs +++ b/crates/ui-egui/src/panels/mod.rs @@ -80,6 +80,19 @@ pub fn color_target(app: &mut DesignApp) -> Option<(String, f32, String, f32)> { Some((i.fill, sel_tint(app, false), i.stroke, sel_tint(app, true))) } +/// [`color_target`], or with nothing selected the colours new shapes get (the Color and Swatches +/// panels then edit those, as InDesign's do). +pub fn color_target_or_defaults(app: &mut DesignApp) -> Option<(String, f32, String, f32)> { + color_target(app).or_else(|| { + let state = app.session.active()?; + if !state.selection.items.is_empty() || state.selection.text.is_some() || state.selection.cells.is_some() { + return None; + } + let colors = &state.doc.settings.new_object; + Some((colors.fill.clone(), colors.fill_tint, colors.stroke.clone(), colors.stroke_tint)) + }) +} + /// Apply a swatch (and tint) to the fill or stroke of what colour edits apply to. pub fn apply_swatch(app: &mut DesignApp, stroke: bool, swatch: &str, tint: Option) -> Result { remember_swatch(app, swatch); diff --git a/crates/ui-egui/src/panels/swatches.rs b/crates/ui-egui/src/panels/swatches.rs index 9cd71ac82..87984b7a0 100644 --- a/crates/ui-egui/src/panels/swatches.rs +++ b/crates/ui-egui/src/panels/swatches.rs @@ -11,7 +11,8 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { let t = Tokens::get(ui.ctx()); let Some(st) = app.session.active() else { return }; let doc = st.doc.clone(); - let target = super::color_target(app); + // With nothing selected, the proxy, Tint field and highlighted row are those of new shapes. + let target = super::color_target_or_defaults(app); header(app, ui, &doc, target.as_ref()); // The row of the colour in front of the proxy is highlighted. let fill_cur = target.map(|(fill, _, stroke, _)| if app.ui.stroke_front { stroke } else { fill }); @@ -340,14 +341,7 @@ pub enum ColorMode { /// swatch), the spectrum ramp, Add to Swatches. Edits apply unnamed colours (`object.color`). pub fn color_panel(app: &mut DesignApp, ui: &mut egui::Ui) { let t = Tokens::get(ui.ctx()); - let target = super::color_target(app).or_else(|| { - let state = app.session.active()?; - if !state.selection.items.is_empty() || state.selection.text.is_some() || state.selection.cells.is_some() { - return None; - } - let colors = &state.doc.settings.new_object; - Some((colors.fill.clone(), colors.fill_tint, colors.stroke.clone(), colors.stroke_tint)) - }); + let target = super::color_target_or_defaults(app); let Some((fill, fill_tint, stroke_sw, stroke_tint)) = target else { ui.label(crate::rtl::widget( ui, @@ -624,10 +618,60 @@ mod color_panel_defaults_tests { h.state_mut().app.run("frame.create", json!({"rect":[240,72,368,200],"content":"unassigned"})).unwrap()["id"].as_u64().unwrap(), ); assert_eq!(h.state().app.session.active().unwrap().doc.item(new).unwrap().stroke.swatch, defaults.stroke); + // Two undo steps: the new frame, then the spectrum click's default stroke. h.state_mut().app.run("edit.undo", json!({})).unwrap(); h.state_mut().app.run("edit.undo", json!({})).unwrap(); assert_eq!(h.state().app.session.active().unwrap().doc.settings.new_object.stroke, original.swatch); assert_eq!(h.state().app.session.active().unwrap().doc.item(old).unwrap().stroke, original); + // With the Fill box in front, the ramp sets the default fill instead. + let original_fill = h.state().app.session.active().unwrap().doc.item(old).unwrap().fill.clone(); + h.state_mut().app.ui.stroke_front = false; + h.run_steps(2); + let ramp = h.get_by_label("Color spectrum").rect(); + crate::test_window::click_at(&mut h, egui::pos2(ramp.left() + ramp.width() * 2.0 / 3.0, ramp.center().y)); + let defaults = h.state().app.session.active().unwrap().doc.settings.new_object.clone(); + assert_ne!(defaults.fill, original_fill.swatch); + assert_eq!(defaults.stroke, original.swatch, "the stroke default is left alone"); + assert_eq!(h.state().app.session.active().unwrap().doc.item(old).unwrap().fill, original_fill); + } + + /// The Swatches panel shows the same defaults: its Tint field reads the default fill's tint, + /// and a click on a swatch row sets the default fill, not an existing frame's. + #[test] + fn swatches_panel_with_no_selection_shows_and_edits_future_shape_colors() { + use egui::accesskit::Role; + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + let old = + designcraft_doc::ItemId(app.run("frame.create", json!({"rect":[72,72,200,200],"content":"unassigned"})).unwrap()["id"].as_u64().unwrap()); + let original = app.session.active().unwrap().doc.item(old).unwrap().fill.clone(); + app.run("selection.set", json!({"ids":[]})).unwrap(); + app.run("object.fill", json!({"swatch": "[Black]", "tint": 0.4})).unwrap(); + app.ui.stroke_front = false; + let mut h = egui_kittest::Harness::builder().with_size(vec2(320.0, 480.0)).build_ui_state( + |ui, app: &mut DesignApp| { + // The panel's fonts are installed on the first frame and used from the next. + 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); + let fields = h.query_all_by(|n| matches!(n.role(), Role::SpinButton | Role::TextInput)).filter_map(|n| n.value()).collect::>(); + assert!(fields.iter().any(|v| v.starts_with("40")), "the Tint field shows the default fill's 40 %: {fields:?}"); + let row = h.get_by_label("[Paper]").rect().center(); + h.hover_at(row); + h.drag_at(row); + h.drop_at(row); + h.run_steps(3); + let defaults = h.state().session.active().unwrap().doc.settings.new_object.clone(); + assert_eq!((defaults.fill.as_str(), defaults.fill_tint), ("[Paper]", 1.0)); + assert_eq!(h.state().session.active().unwrap().doc.item(old).unwrap().fill, original); } } From 7a5101d0a85344763c39c7837285867df489e47b Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 29 Oct 2026 18:01:19 +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/swatches.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/panels/swatches.rs b/crates/ui-egui/src/panels/swatches.rs index 51643b2fc..8864e75ce 100644 --- a/crates/ui-egui/src/panels/swatches.rs +++ b/crates/ui-egui/src/panels/swatches.rs @@ -575,7 +575,7 @@ fn cmyk_ramp(k: usize, x: f32) -> egui::Color32 { /// The spectrum ramp: hue across, light to dark down; click to apply. fn spectrum(app: &mut DesignApp, ui: &mut egui::Ui, target: &str) { let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 36.0), Sense::click()); - resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, crate::i18n::tr(&app.ui.language, "Color spectrum"))); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, crate::i18n::tr(&app.ui.language, "Color spectrum"))); 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(); [c[0], c[1], c[2]] @@ -683,7 +683,7 @@ mod color_panel_defaults_tests { let mut h = egui_kittest::Harness::builder().with_size(vec2(320.0, 480.0)).build_ui_state( |ui, app: &mut DesignApp| { // The panel's fonts are installed on the first frame and used from the next. - 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));