diff --git a/ROADMAP.md b/ROADMAP.md index 36ad477c0..61a4dd942 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -465,6 +465,7 @@ Newest first. - **2026-10-15** · A panel tab's context menu (and the end of its panel menu) has InDesign's Close, Close Tab Group, Minimize and Collapse to Icons, before Float Panel and Group with; `window.panel.closeGroup` and `window.collapseToIcons` are commands. No readiness percentage change. - **2026-10-15** · Window › Styles › Paragraph Styles (or Character Styles) opens InDesign's floating Character Styles | Paragraph Styles group, which stays open while other panels open; it was a flyout that the next panel replaced. No readiness percentage change. - 2026-10-15: Double-clicking the status bar's preflight count or its light shows the Preflight panel, as in InDesign. +- 2026-10-15: A tool group's flyout closes on Esc and on a click anywhere outside it (a panel, the Fill swatch), as InDesign's; it only closed on a click on the canvas. - **2026-10-14** · The Type tool draws a new text frame over a locked empty frame, as InDesign does, instead of turning the locked frame into a text frame. No readiness percentage change. - **2026-10-14** · 100% black shows as pure black on screen by default, as InDesign's Appearance of Black does out of the box (Display All Blacks as Rich Black); Preferences › Appearance of Black › Display All Blacks Accurately still shows it as the dark grey it prints, and a saved choice is kept. No readiness percentage change. - **2026-10-14** · Pathfinder keeps the frontmost object, as InDesign does: a photo placed over shapes and intersected with them shows through the shapes (it was dropped, and the shapes kept their black fill); Add, Exclude Overlap and Minus Back take the front object's fill too, Subtract still keeps the back one. No readiness percentage change. diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 2f941fa15..5af6ea6a3 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -45,6 +45,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { ui.add_space(2.0); let current = app.session.tool_id(); let mut chosen: Option<&'static str> = None; + let mut opened_now = false; // A slot is a group (its current tool shown, the others in its flyout) or, in two // columns, one tool of a group InDesign lays out tool by tool. let mut row_items: Vec)>> = Vec::new(); @@ -118,6 +119,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { || (resp.is_pointer_button_down_on() && ui.input(|i| i.pointer.press_start_time().is_some_and(|s| i.time - s > 0.35)))) { + opened_now |= app.ui.flyout != Some(gi); app.ui.flyout = Some(gi); } if app.ui.flyout == Some(gi) { @@ -157,6 +159,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { t.text_dim, ); } + row.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, tl.label)); if row.clicked() { chosen = Some(tl.id); } @@ -178,8 +181,18 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { if let Some(id) = chosen { app.select_tool(id); } - if app.ui.flyout.is_some() && ui.input(|i| i.pointer.any_click()) && chosen.is_none() && !ui.ctx().is_pointer_over_egui() { - app.ui.flyout = None; + // As InDesign's, an open flyout closes on Esc and on a click anywhere outside it (a + // panel, a swatch, the canvas), not only on the canvas. + if let Some(gi) = app.ui.flyout + && chosen.is_none() + && !opened_now + { + let area = ui.ctx().memory(|m| m.area_rect(egui::Id::new(("flyout", gi)))); + let outside = ui.input(|i| i.pointer.any_click() && i.pointer.interact_pos().is_some_and(|p| area.is_none_or(|a| !a.contains(p)))); + let esc = ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape)); + if outside || esc { + app.ui.flyout = None; + } } ui.add_space(8.0); fill_stroke_proxy(app, ui, width, cols); @@ -618,6 +631,39 @@ mod tests { h.get_by_label(label).rect() } + /// As InDesign's: a tool group's flyout closes on Esc and on a click outside it (the fill + /// swatch, a panel), not only on the canvas. + #[test] + fn a_tool_flyout_closes_on_escape_and_on_a_click_elsewhere() { + use egui::{Event, Modifiers, PointerButton}; + use egui_kittest::kittest::Queryable; + let app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + let mut h = crate::test_window::open(app, vec2(1400.0, 900.0)); + let open = |h: &mut egui_kittest::Harness<'static, crate::test_window::Window>| { + let at = h.get_by_label("Rectangle Frame Tool (F)").rect().center(); + h.input_mut().events.push(Event::PointerMoved(at)); + for pressed in [true, false] { + h.input_mut().events.push(Event::PointerButton { pos: at, button: PointerButton::Secondary, pressed, modifiers: Modifiers::NONE }); + } + h.run_steps(3); + assert!(h.state().app.ui.flyout.is_some(), "a right-click opens the flyout"); + let _ = h.get_by_label("Ellipse Frame Tool"); + }; + open(&mut h); + h.key_press(egui::Key::Escape); + h.run_steps(2); + assert!(h.state().app.ui.flyout.is_none(), "Esc closes it"); + // A click on a panel closes it. + open(&mut h); + crate::test_window::click_at(&mut h, egui::pos2(1300.0, 700.0)); + assert!(h.state().app.ui.flyout.is_none(), "a click elsewhere closes it"); + // Choosing a tool in it closes it and selects the tool. + open(&mut h); + h.get_by_label("Ellipse Frame Tool").click(); + h.run_steps(3); + assert_eq!((h.state().app.session.tool_id(), h.state().app.ui.flyout), ("ellipseFrame", None)); + } + #[test] fn a_short_window_reaches_every_tool() { for double in [false, true] {