From 0b43158fbb67601c00790dddd65998573a207fa0 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 6 Nov 2026 21:33:24 -0800 Subject: [PATCH] Fix #5813: floating panels hide while a tool group's flyout is open A tool group's flyout and the All Tools drawer draw in the main window, so a floating panel's OS window covered them. On the desktop, except on Wayland, floating panels now hide while either one is open, as they do for a menu or a dialog, and show again when it closes. A flyout or drawer left set under a hidden toolbar does not draw and does not hide them. --- crates/ui-egui/src/edit_toolbar.rs | 3 + crates/ui-egui/src/panel_docking.rs | 4 +- .../ui-egui/src/panel_docking/native_tests.rs | 133 ++++++++++++++++++ crates/ui-egui/src/toolbar.rs | 23 +++ docs/target-app-parity.md | 1 + 5 files changed, 162 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/edit_toolbar.rs b/crates/ui-egui/src/edit_toolbar.rs index eb79cf357..1a4c54989 100644 --- a/crates/ui-egui/src/edit_toolbar.rs +++ b/crates/ui-egui/src/edit_toolbar.rs @@ -598,6 +598,9 @@ pub(crate) fn drawer(app: &mut VectorcraftApp, ctx: &egui::Context) { // it open. app.ui.tools_drawer = false; } + if app.ui.tools_drawer { + crate::toolbar::mark_flyout_open(ctx); + } ghost(ctx, &t); } diff --git a/crates/ui-egui/src/panel_docking.rs b/crates/ui-egui/src/panel_docking.rs index e2cb86d34..73d981000 100644 --- a/crates/ui-egui/src/panel_docking.rs +++ b/crates/ui-egui/src/panel_docking.rs @@ -1136,9 +1136,9 @@ pub(crate) fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) { } let modal = modal_open(app, &ctx); // Palette windows are OS windows of their own, held above the app's window while it is active, - // so a dialog or a menu drawn in the main window would be covered by them. + // so a dialog, a menu or a tool flyout drawn in the main window would be covered by them. #[cfg(not(target_arch = "wasm32"))] - palette_window_levels(&ctx, &layout, modal, egui::Popup::is_any_open(&ctx) || crate::menus::menu_open(&ctx)); + palette_window_levels(&ctx, &layout, modal, egui::Popup::is_any_open(&ctx) || crate::menus::menu_open(&ctx) || crate::toolbar::flyout_open(&ctx)); let mut blocked = Vec::new(); if modal || egui::Popup::is_any_open(&ctx) { blocked.push(ctx.content_rect()); diff --git a/crates/ui-egui/src/panel_docking/native_tests.rs b/crates/ui-egui/src/panel_docking/native_tests.rs index 5b538b765..784b86fcf 100644 --- a/crates/ui-egui/src/panel_docking/native_tests.rs +++ b/crates/ui-egui/src/panel_docking/native_tests.rs @@ -651,6 +651,139 @@ fn a_menu_of_the_menu_bar_lowers_the_palette_windows() { "and is hidden until the menu closes" ); } + +/// The visibility and level commands the palette window `viewport` was sent, cleared after reading. +fn take_palette_commands(h: &Harness, viewport: ViewportId) -> (Vec, Vec) { + let commands = std::mem::take(&mut h.host.borrow_mut().commands); + let shown = commands + .iter() + .filter_map(|(id, command)| match command { + ViewportCommand::Visible(visible) if *id == viewport => Some(*visible), + _ => None, + }) + .collect(); + let levels = commands + .iter() + .filter_map(|(id, command)| match command { + ViewportCommand::WindowLevel(level) if *id == viewport => Some(*level), + _ => None, + }) + .collect(); + (shown, levels) +} + +/// Opens the flyout of the group `tools` on the Tools panel. +fn open_tool_flyout(h: &mut Harness, tools: &[&str]) { + h.app.ui.flyout = Some(0); + let tools: Vec = tools.iter().map(|tool| tool.to_string()).collect(); + h.ctx.data_mut(|d| { + d.insert_temp(egui::Id::unique("flyout-tools"), tools); + d.insert_temp(crate::toolbar::flyout_toolbar_id(), None::); + }); +} + +/// A tool group's flyout is drawn in the main window, so a palette window over it hides until it +/// closes (#5813), and takes its palette level again with the keyboard left where it was. +#[test] +fn a_tool_flyout_hides_the_palette_windows_until_it_closes() { + let mut h = Harness::new(); + let viewport = h.float("navigator"); + h.host.borrow_mut().focus = ViewportId::ROOT; + h.frames(2); + take_palette_commands(&h, viewport); + open_tool_flyout(&mut h, &["pen", "curvature"]); + h.frames(2); + let (shown, levels) = take_palette_commands(&h, viewport); + assert_eq!(shown, [false], "hidden while the flyout is open"); + assert_eq!(levels, [egui::WindowLevel::Normal], "and out of the top level"); + assert_eq!(h.app.ui.flyout, Some(0), "the flyout stays open"); + h.frames(2); + assert_eq!(take_palette_commands(&h, viewport), (vec![], vec![]), "the commands go once"); + h.app.ui.flyout = None; + h.frames(2); + let focus_commands = h.host.borrow().commands.iter().filter(|(_, command)| matches!(command, ViewportCommand::Focus)).count(); + assert_eq!(focus_commands, 0, "nothing takes the keyboard"); + let (shown, levels) = take_palette_commands(&h, viewport); + assert_eq!(shown, [true], "shown again when the flyout closes"); + assert_eq!(levels, [egui::WindowLevel::AlwaysOnTop], "at its palette level"); +} + +/// The All Tools drawer is drawn in the main window too, so it hides a palette window the same way. +#[test] +fn the_all_tools_drawer_hides_the_palette_windows_until_it_closes() { + let mut h = Harness::new(); + let viewport = h.float("navigator"); + h.host.borrow_mut().focus = ViewportId::ROOT; + h.frames(2); + take_palette_commands(&h, viewport); + h.app.run("window.editToolbar", json!({"open": true})).unwrap(); + h.frames(2); + assert_eq!(take_palette_commands(&h, viewport).0, [false], "hidden while the drawer is open"); + h.app.run("window.editToolbar", json!({"open": false})).unwrap(); + h.frames(2); + assert_eq!(take_palette_commands(&h, viewport).0, [true], "shown again when the drawer closes"); +} + +/// A flyout or a drawer left set while its toolbar is hidden does not draw, so the palette windows +/// stay on screen. +#[test] +fn a_flyout_or_drawer_of_a_hidden_toolbar_leaves_the_palette_windows_shown() { + let mut h = Harness::new(); + let viewport = h.float("navigator"); + h.host.borrow_mut().focus = ViewportId::ROOT; + h.frames(2); + take_palette_commands(&h, viewport); + h.app.run("window.editToolbar", json!({"open": true})).unwrap(); + h.frames(2); + assert_eq!(take_palette_commands(&h, viewport).0, [false], "hidden under the drawer"); + h.app.run("window.toolbar", json!({})).unwrap(); + h.frames(1); + assert!(!h.app.ui.toolbar && h.app.ui.tools_drawer, "the drawer is left set with the Tools panel hidden"); + assert_eq!(take_palette_commands(&h, viewport).0, [true], "shown again: the drawer does not draw"); + h.app.ui.tools_drawer = false; + h.app.run("window.toolbar", json!({})).unwrap(); + open_tool_flyout(&mut h, &["pen", "curvature"]); + h.frames(2); + assert_eq!(take_palette_commands(&h, viewport).0, [false], "hidden under the flyout"); + // Tab hides the toolbar and the panels, and Shift+Tab shows the panels alone. + h.app.run("window.dock", json!({})).unwrap(); + h.app.run("window.panels", json!({})).unwrap(); + h.frames(1); + assert!(!h.app.ui.toolbar && h.app.ui.flyout.is_some(), "the flyout is left set with the Tools panel hidden"); + assert_eq!(take_palette_commands(&h, viewport).0.last(), Some(&true), "shown again: the flyout does not draw"); +} + +/// The frame in which a click elsewhere closes a flyout or the drawer shows the palette windows, as +/// no later frame need come while the app is idle. +#[test] +fn a_click_elsewhere_shows_the_palette_windows_in_the_frame_it_closes_a_flyout() { + let mut h = Harness::new(); + let viewport = h.float("navigator"); + h.host.borrow_mut().focus = ViewportId::ROOT; + h.frames(2); + take_palette_commands(&h, viewport); + // On the dock: a press on the canvas closes a flyout later in its frame, and the release's frame + // shows the palette windows. + let at = pos2(1500.0, 700.0); + let button = |pressed| Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE }; + let click = |h: &mut Harness| { + h.frame(vec![Event::PointerMoved(at), button(true)]); + h.frame(vec![button(false)]); + }; + open_tool_flyout(&mut h, &["pen", "curvature"]); + h.frames(2); + assert_eq!(take_palette_commands(&h, viewport).0, [false], "hidden under the flyout"); + click(&mut h); + assert_eq!(h.app.ui.flyout, None, "the click closes the flyout"); + assert_eq!(take_palette_commands(&h, viewport).0, [true], "shown in that frame"); + h.app.run("window.editToolbar", json!({"open": true})).unwrap(); + h.frames(2); + assert_eq!(take_palette_commands(&h, viewport).0, [false], "hidden under the drawer"); + click(&mut h); + assert!(!h.app.ui.tools_drawer, "the click closes the drawer"); + assert_eq!(take_palette_commands(&h, viewport).0, [true], "shown in that frame"); +} + /// A modal dialog is drawn in the main window, so the palettes' OS windows leave their topmost /// level while it is open and take it back after, or the dialog would be covered by them. #[test] diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 2280853e1..be04485d0 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -873,9 +873,32 @@ fn flyout(app: &mut VectorcraftApp, ctx: &egui::Context) { // long press) leaves it to that button. app.ui.flyout = None; } + if app.ui.flyout.is_some() { + mark_flyout_open(ctx); + } let _ = theme::semibold; } +/// The pass in which a tool group's flyout or the All Tools drawer last stayed open. +fn flyout_open_pass_id() -> egui::Id { + egui::Id::unique("flyout-open-pass") +} + +/// Records that a tool group's flyout or the All Tools drawer drew in this pass and stays open. +pub(crate) fn mark_flyout_open(ctx: &egui::Context) { + let pass = ctx.cumulative_pass_nr(); + ctx.data_mut(|d| d.insert_temp(flyout_open_pass_id(), pass)); +} + +/// Whether a tool group's flyout or the All Tools drawer is on screen in the main window this pass. +/// One left set while its toolbar is hidden does not draw, so it does not count. The toolbars draw +/// before the panels, which read this. +#[cfg(not(target_arch = "wasm32"))] +pub(crate) fn flyout_open(ctx: &egui::Context) -> bool { + let pass = ctx.cumulative_pass_nr(); + ctx.data(|d| d.get_temp::(flyout_open_pass_id())) == Some(pass) +} + /// The toolbar made with New Toolbar whose group the open flyout shows (none: the Tools panel's). pub(crate) fn flyout_toolbar_id() -> egui::Id { egui::Id::unique("flyout-toolbar") diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 7bf38edc6..b91ffb976 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -678,6 +678,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Screen readers get the value of the color and channel sliders that panels and dialogs draw (the Color panel, the Color Picker, Swatch Options, the Navigator's Zoom, the Character panel's variable font axes, the Perspective Grid and flattener options, and the sliders of the effect, tool option and transform dialogs): the number and text of the field beside each one, such as 128 for G or 50% for an opacity, with the slider's range, which extends to include a number past the slider's end. The harmony wheel's Brightness slider, which has no field, reports its brightness in percent. - A layer whose Layer Options › Print is off shows its name italic in the Layers panel, and while its row is dragged, as the reference application shows non-printing layers (#5739); a printing layer stays upright and ticking Print again restores the upright name (`panels::layers` test `a_layer_that_does_not_print_has_its_name_italic`). - Panels undock (#495): a dock tab, a panel icon or a popped-out panel's title dragged out of the dock floats that panel (in an OS window of its own on the desktop, #4135), and the strip right of the dock's tabs floats the whole tabbed group. A floating group moves by its title bar, a tab dragged out of it floats on its own, and dropped on another group's title bar or tabs it stacks with that group; dropped on a dock group's tabs, which light up under the pointer, its panels join that group, and on the group's upper or lower half they make a new group above or below it (`window.panel.dock {anchor, zone}`); dropped on the icon column they become icons there, the tabbed group's panels too (`window.panel.dock {icon: true}`); dropped anywhere else on the dock, which lights up, they go back where they live (the tabbed group or the icon column), as its × and the panel menus' Float Panel / Dock Panel do (`window.panel.float`, `window.panel.dock`). The Tools panel floats by its title bar and docks again on the window's left edge. Floating groups stack into sets: dropped on the upper or lower half of another group (a line shows where), a group joins it right above or below it, and the set's groups stack top to bottom, each showing one panel, moving together by the top group's title bar, whose × docks them all; a group dragged by its tab strip leaves the set (`window.panel.float {below, above}`). A double-click on a tab collapses a floating group to its tabs (`collapsed`), and a dock group too, a group below it moving up into the room; a second double-click, or a click on another of its tabs, expands it (`window.panel.collapse`, kept with the preferences and in saved workspaces). Sets dropped on the left edge of the icon column or of another dock column dock as dock columns (a multi-column dock, `window.panel.dock {column}`), whose groups stack, tab and tear out as a floating set's do, and whose top strip floats the column again. Floating panels, their sets and the Tools panel's position are kept with the preferences and in user workspaces, and clamped into a smaller window. Not done: panels in separate OS windows, reordering tabs by dragging, collapsing a set or dock column to icons, dock columns right of the default dock, resizing floating panels and the groups of a set. + - A floating panel's OS window hides while a tool group's flyout or the All Tools drawer is open in the main window, as it does under a modal dialog or a menu, and shows again when the flyout or the drawer closes (#5813; `a_tool_flyout_hides_the_palette_windows_until_it_closes`). - On the desktop a floating panel in an OS window stores the monitor it is on and reopens there at the next launch. When that monitor is no longer connected, the panel opens on the main window's monitor at the same offset (#5582). Outside macOS, a desktop whose monitors have different scale factors keeps opening floating panels on the main window's monitor. Test: `panel_docking::native_tests::a_floating_panel_reopens_on_the_monitor_where_it_was_left`. - A menu in a floating panel's OS window stops at that window's bottom edge and scrolls past it, and is never taller than the window (#5347; `a_long_panel_menu_scrolls_inside_a_short_native_window`). A panel's long submenus (Digits, Stylistic Sets, Save Selection, Move to Group), the libraries menu, the Color Guide's Limit to Library menu and the Layers panel's Filter popup scroll the same way. - The dock's double arrow collapses Properties, Layers and Libraries to icons at the top of the icon column, popping out like the other icon panels, and expands them again (`window.collapseDock`, kept with the preferences and in saved workspaces). The icon column's own arrow (expanding the icon panels into a full column) is not done.