diff --git a/ROADMAP.md b/ROADMAP.md index f55002d99..534533793 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -171,6 +171,7 @@ Newest first. rotation part of the spread transform (#484). - **2026-10-11** · The document grid is drawn (View ▸ Grids & Guides ▸ Show/Hide Document Grid, ⌘'), on the lines Snap to Document Grid uses, behind or in front of page items (#530). +- **2026-10-12** · Double-clicking the Fill or Stroke box in the Tools panel opens the Color Picker with the selection's colour, as in InDesign (it opened the Swatches panel). No readiness percentage change. - **2026-10-10 (UI.4–UI.5):** Panel tabs can move into other groups, split the dock, float, resize and return to their saved locations through shared docking transactions. Layouts persist in custom workspaces; UI and control commands share the same validated action path. Shared pane geometry and widget-state theme application retain all five palettes and local fonts. Split panes send keyboard input only to the focused canvas and scope contextual toolbar IDs per pane. No readiness percentage change. - **2026-10-10** · Version 0.6.0; releases add a Linux riscv64 tar.gz (cross-compiled, CLI smoke-tested under QEMU). diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index f404ae648..d5e0183e5 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -274,16 +274,31 @@ fn fill_stroke_proxy(app: &mut DesignApp, ui: &mut egui::Ui, width: f32) { crate::rtl::label(ui, fill_tip); }); fresp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, fill_tip)); + let stroke_tip = crate::i18n::tr(&app.ui.language, "Stroke (X)"); let sresp = ui.interact(stroke_r.translate(vec2(0.0, 0.0)), ui.id().with("proxy_stroke"), Sense::click()).on_hover_ui(|ui| { - crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Stroke (X)")); + crate::rtl::label(ui, stroke_tip); }); + sresp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, stroke_tip)); if fresp.clicked() || sresp.clicked() { let front = if stroke_front { !fresp.clicked() } else { sresp.clicked() && !fill_r.contains(sresp.interact_pointer_pos().unwrap_or_default()) }; ui.data_mut(|d| d.insert_temp(stroke_front_id, front)); } + // A double-click opens the Color Picker for that box (as InDesign's), with the selection's + // colour: the selected text's when text is selected. if fresp.double_clicked() || sresp.double_clicked() { - let _ = app.run("window.panel", json!({"panel": "swatches"})); + let on_fill = fresp.double_clicked() && (!stroke_front || !sresp.double_clicked()); + let stroke = !on_fill; + let text = crate::panels::text_attrs(app).filter(|_| app.session.active().is_some_and(|d| d.selection.text.is_some())); + let current = match text { + Some(a) => a["chars"][if stroke { "stroke" } else { "fill" }].as_str().unwrap_or("[None]").to_string(), + None if stroke => stroke_sw.clone(), + None => fill_sw.clone(), + }; + let rgb = doc.as_ref().and_then(|d| d.resolve_color(¤t, 1.0)).map_or([0.0; 3], |c| c.to_rgb()); + let byte = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8; + let hex = format!("#{:02x}{:02x}{:02x}", byte(rgb[0]), byte(rgb[1]), byte(rgb[2])); + app.ui.dialog = Some(crate::dialogs::Dialog::new("colorPicker", json!({"target": if stroke { "stroke" } else { "fill" }, "hex": hex}))); } ui.add_space(3.0); // Formatting affects container / text. @@ -473,6 +488,36 @@ mod tests { assert!(!h.state().app.ui.tools_double_column); } + #[test] + fn double_clicking_the_fill_box_opens_the_color_picker_with_the_selections_colour() { + let mut app = app(false); + app.run("file.new", serde_json::json!({})).unwrap(); + let id = app.run("frame.create", serde_json::json!({"rect": [72, 72, 200, 200]})).unwrap()["id"].clone(); + app.run("object.color", serde_json::json!({"color": "#456daf", "ids": [id]})).unwrap(); + app.run("selection.set", serde_json::json!({"ids": [id]})).unwrap(); + // Frames 1 ms apart, so two clicks make a double-click. + let mut h = egui_kittest::Harness::builder().with_size(vec2(1400.0, 900.0)).with_step_dt(0.001).build_ui_state( + |ui, app: &mut crate::DesignApp| { + let ctx = ui.ctx().clone(); + app.logic(&ctx); + app.ui(ui); + }, + app, + ); + h.run_steps(6); + let at = h.get_by_label(FILL).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: Default::default() }); + h.step(); + } + h.run_steps(3); + let d = h.state().ui.dialog.clone().expect("the Color Picker opened"); + assert_eq!(d.id, "colorPicker"); + assert_eq!((d.fields["target"].as_str(), d.fields["hex"].as_str()), (Some("fill"), Some("#456daf")), "{:?}", d.fields); + } + #[test] fn a_tall_window_keeps_one_column_and_does_not_scroll() { let mut h = test_window::open(app(false), vec2(1400.0, 900.0));