From 885793f763527ef47d696b998b3638e7e8dbb683 Mon Sep 17 00:00:00 2001 From: Paul Gemignani <4waymedia@gmail.com> Date: Mon, 2 Nov 2026 08:31:14 -0800 Subject: [PATCH] Color Picker: reopens on the component it was left on The picker opened with R selected every time, so anyone who works in H (the hue slider with the saturation/brightness field) chose it again on every open. The component now lives in the UI preferences (colorPickerComponent, default r, so existing preference files and openers that name a component behave as before) and the picker reads it on open and writes it when a radio button is clicked, as Photoshop's picker keeps its last mode. Verified locally on a288a87b: fmt, clippy, the picker's tests; tried on macOS. --- crates/ui-egui/src/dialogs.rs | 3 +++ crates/ui-egui/src/dialogs/tests.rs | 36 +++++++++++++++++++++++++++++ crates/ui-egui/src/state.rs | 4 ++++ 3 files changed, 43 insertions(+) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index d546cd108..8f50c1990 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -9021,6 +9021,8 @@ fn color_picker(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { let tr = |s: &str| crate::i18n::tr(&lang, s).to_string(); let old = d.s("hex"); d.fields.entry("old").or_insert_with(|| json!(old)); + // The component the picker was last left on, unless the opener chose one. + d.fields.entry("component").or_insert_with(|| json!(app.ui.color_picker_component)); let comp = PickComp::from_key(&d.s("component")); let mut p = picked_of(d); let before = p; @@ -9215,6 +9217,7 @@ fn color_picker(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { } if comp_now != comp { d.fields.insert("component".into(), json!(comp_now.key())); + app.ui.color_picker_component = comp_now.key().to_string(); } // The hex code, and the eyedropper beside it. let cy = area.min.y + at::ROWS[7]; diff --git a/crates/ui-egui/src/dialogs/tests.rs b/crates/ui-egui/src/dialogs/tests.rs index 1cdce534f..3641a0e7d 100644 --- a/crates/ui-egui/src/dialogs/tests.rs +++ b/crates/ui-egui/src/dialogs/tests.rs @@ -3698,6 +3698,42 @@ fn the_color_picker_picks_on_its_field_and_slider_and_adds_a_cmyk_swatch() { assert_eq!(st.doc.resolve_color(&fill, 1.0).unwrap().to_hex(), "#7c1315"); } +/// The Color Picker reopens on the component it was left on, and the choice survives the +/// preferences file; an opener that names a component still gets it. +#[test] +fn the_color_picker_remembers_its_component() { + use egui_kittest::kittest::Queryable; + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + app.run("frame.create", json!({"rect": [72, 72, 200, 200], "shape": "rectangle"})).unwrap(); + assert_eq!(app.ui.color_picker_component, "r", "R at first, as before"); + app.ui.dialog = Some(Dialog::new("colorPicker", json!({"target": "fill", "hex": "#7c1315", "text": false}))); + let mut h = crate::test_window::open(app, egui::vec2(1400.0, 900.0)); + h.run_steps(3); + assert_eq!(h.state().app.ui.dialog.as_ref().unwrap().s("component"), "r"); + h.get_by_role_and_label(egui::accesskit::Role::RadioButton, "H:").click(); + h.run_steps(2); + assert_eq!(h.state().app.ui.dialog.as_ref().unwrap().s("component"), "h"); + assert_eq!(h.state().app.ui.color_picker_component, "h", "the choice is kept outside the dialog"); + h.get_by_label("Cancel").click(); + h.run_steps(2); + assert!(h.state().app.ui.dialog.is_none()); + // Reopened: H, not R. + h.state_mut().app.ui.dialog = Some(Dialog::new("colorPicker", json!({"target": "fill", "hex": "#7c1315", "text": false}))); + h.run_steps(3); + assert_eq!(h.state().app.ui.dialog.as_ref().unwrap().s("component"), "h"); + h.get_by_label("Cancel").click(); + h.run_steps(2); + // An opener's own choice wins. + h.state_mut().app.ui.dialog = Some(Dialog::new("colorPicker", json!({"target": "fill", "hex": "#7c1315", "text": false, "component": "l"}))); + h.run_steps(3); + assert_eq!(h.state().app.ui.dialog.as_ref().unwrap().s("component"), "l"); + // Saved with the preferences, and read back; a file without it is R. + let saved = serde_json::to_vec(&h.state().app.ui).unwrap(); + assert_eq!(crate::UiState::from_preferences_json(&saved).unwrap().color_picker_component, "h"); + assert_eq!(crate::UiState::from_preferences_json(b"{}").unwrap().color_picker_component, "r"); +} + #[test] fn create_guides_dialog_covers_the_spread_in_view() { let mut app = facing_app(6); diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index bd038090a..63100f614 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -226,6 +226,9 @@ pub struct UiState { pub story_editor_size: f32, /// View › Story Editor › Show/Hide Depth Ruler. pub story_editor_depth_ruler: bool, + /// The Color Picker's component (its radio button, as `PickComp::key`: `h`, `s`, `b`, `l`, + /// `la`, `lb`, `r`, `g`, `bl`), kept between openings and sessions, as Photoshop's picker is. + pub color_picker_component: String, /// Preferences › General › Interface Language: supported codes are listed in `i18n::LANGUAGES`. pub language: String, /// Edit › Transparency Flattener Presets: "" (none), "high", "medium" or "low" for PDF export. @@ -399,6 +402,7 @@ impl Default for UiState { show_full_menus: false, story_editor_size: 14.0, story_editor_depth_ruler: true, + color_picker_component: "r".into(), scripts: vec![ ("Number the pages".into(), "# A page-number frame at the bottom of the current page.\nframe.create {\"rect\": [288, 740, 324, 760], \"content\": \"text\", \"text\": \"\"}\ntext.select {\"story\": \"$0.story\", \"anchor\": 0, \"focus\": 0}\ntext.insert {\"text\": \"\\ue000\", \"raw\": true}".into()), ("Two-column grid".into(), "# Four text frames in a 2 × 2 grid.\nframe.grid {\"rect\": [36, 36, 576, 756], \"cols\": 2, \"rows\": 2, \"gutter\": 12}".into()),