fix(ui): Double-clicking the Fill or Stroke box opens the Color Picker

In InDesign a double-click on the Tools panel's Fill or Stroke box opens
the Color Picker; DesignCraft opened the Swatches panel. It now opens
the Color Picker for that box, with the selection's colour (the selected
text's when text is selected). The Stroke box also reports its name to
screen readers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
moppeeer 2026-10-12 05:08:30 +02:00 committed by paaspaas00
parent d020b4e85d
commit 03d8b1e61b
2 changed files with 48 additions and 2 deletions

View File

@ -132,6 +132,7 @@ Newest first.
- **2026-10-12** · Window close and application/menu Quit ask about unsaved documents; later Cancel keeps every tab open, including earlier Don't Save choices. Approvals are rechecked after edits. Cancelable macOS Dock Quit remains open (#544). No readiness percentage change.
- **2026-10-12** · Each running app keeps recovery data in its own locked folder; current folder and lock ownership are checked before writes and cleanup. Failed isolation disables recovery with a warning instead of sharing document IDs. No readiness percentage change.
- **2026-10-12** · Native engine saves and exports replace files atomically; failed writes retain the original, private staging preserves Unix permissions, and valid long filenames work. Desktop Services and CLI raster exports remain separate follow-ups. No readiness percentage change.
- **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-12** · Words in capitals hyphenate like capitalized words (FERRUM COM-MODI), as in InDesign; Hyphenate Capitalized Words off keeps them whole. No readiness percentage change.
- **2026-10-11** · `transform.move {copy}`, Edit › Duplicate and Step and Repeat return the copies' ids, so a script can go on with them. No readiness percentage change.
- **2026-10-11** · `object.fill` and `object.stroke` refuse a swatch name the document doesn't have (it painted nothing) and suggest the name meant (`Paper` → `[Paper]`). No readiness percentage change.

View File

@ -272,16 +272,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(&current, 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.
@ -472,6 +487,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));