From 11810ecbfc0ee6d8945cfe1b5a6219587e340ebb Mon Sep 17 00:00:00 2001 From: aiongg Date: Fri, 16 Oct 2026 06:27:17 +0000 Subject: [PATCH] Integration: panel menus stay reachable in the docking layout --- crates/ui-egui/src/dock.rs | 10 ++--- crates/ui-egui/src/panel_docking.rs | 48 +++++++++++++++++++---- crates/ui-egui/src/panel_docking_tests.rs | 41 +++++++++++++++++++ 3 files changed, 84 insertions(+), 15 deletions(-) diff --git a/crates/ui-egui/src/dock.rs b/crates/ui-egui/src/dock.rs index 7e7ce2a64..fb9a7cf17 100644 --- a/crates/ui-egui/src/dock.rs +++ b/crates/ui-egui/src/dock.rs @@ -106,13 +106,9 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { } icons::paint(ui.painter(), menu_r, "menu", t.icon); let tab = app.ui.dock_tab.clone(); - egui::Popup::menu(&menu).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| { - if tab == "pages" { - panels::pages::panel_menu(app, ui); - ui.separator(); - } - crate::panel_docking::panel_menu(app, ui, &tab); - }); + egui::Popup::menu(&menu) + .close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside) + .show(|ui| crate::panel_docking::three_line_menu(app, ui, &tab)); egui::Frame::NONE.inner_margin(egui::Margin { left: 10, right: 10, top: 4, bottom: 6 }).show(ui, |ui| { // The Pages panel scrolls its parts itself and keeps its footer at the bottom. if app.ui.dock_tab == "pages" { diff --git a/crates/ui-egui/src/panel_docking.rs b/crates/ui-egui/src/panel_docking.rs index 9b85c0b9e..bec168ea1 100644 --- a/crates/ui-egui/src/panel_docking.rs +++ b/crates/ui-egui/src/panel_docking.rs @@ -499,7 +499,8 @@ pub(crate) fn show(app: &mut DesignApp, ui: &mut egui::Ui) -> bool { ) .to_owned() }, - |_| craft_ui::docking::Permissions::default(), + // Close comes from the panel's own tab commands, so the surface doesn't add another. + |_| craft_ui::docking::Permissions { closable: false, ..Default::default() }, |_| craft_ui::docking::PanelLimits { min: egui::vec2(220.0, 80.0), ..Default::default() }, &mut PanelContent(app), ) @@ -598,10 +599,33 @@ pub(crate) fn legacy_tab(app: &mut DesignApp, ui: &mut egui::Ui, panel: &str, re } } +/// The menu behind a panel's three-line button, in either dock: the panel's own commands, then +/// its docking commands. +pub(crate) fn three_line_menu(app: &mut DesignApp, ui: &mut egui::Ui, panel: &str) { + if panel == "pages" { + crate::panels::pages::panel_menu(app, ui); + ui.separator(); + } + panel_menu(app, ui, panel); +} + +/// The three-line button at the right of a tab strip, for the panel in front. +fn three_line_button(app: &mut DesignApp, ui: &mut egui::Ui, panel: &str) { + let t = crate::theme::Tokens::get(ui.ctx()); + let area = ui.max_rect(); + let r = egui::Rect::from_center_size(area.center(), egui::vec2(18.0, 18.0)); + let menu = ui.interact(r, ui.id().with("panel-menu"), egui::Sense::click()); + if menu.hovered() { + ui.painter().rect_filled(r, 3.0, t.hover); + } + crate::icons::paint(ui.painter(), r, "menu", t.icon); + egui::Popup::menu(&menu).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| three_line_menu(app, ui, panel)); +} + /// The docking commands of a panel: its tab's context menu and the end of its panel menu. pub(crate) fn panel_menu(app: &mut DesignApp, ui: &mut egui::Ui, panel: &str) { let Some(panel) = normalize(panel) else { return }; - tab_commands(app, ui, panel, true); + tab_commands(app, ui, panel); ui.separator(); let language = app.ui.language.clone(); if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&language, "Float Panel"))).clicked() { @@ -643,12 +667,21 @@ impl craft_ui::docking::DockContent for PanelContent<'_> { }); } fn panel_menu(&mut self, ui: &mut egui::Ui, id: &String) { - // The docking surface adds Float, Group with and Close after these. + // The docking surface adds Float Panel and Group with after these, so a tab's menu lists + // what the classic dock's does. if let Some(panel) = normalize(id) { - tab_commands(self.0, ui, panel, false); + tab_commands(self.0, ui, panel); ui.separator(); } } + fn group_header_width(&self, _: &[String], _: &String, _: bool) -> f32 { + 24.0 + } + fn group_header(&mut self, ui: &mut egui::Ui, _: &[String], active: &String, _: bool) { + if let Some(panel) = normalize(active) { + three_line_button(self.0, ui, panel); + } + } fn label(&mut self, ui: &egui::Ui, _: &String, logical: &str, font: &egui::FontId, color: egui::Color32) -> egui::WidgetText { egui::WidgetText::Galley(crate::rtl::plain(ui.ctx(), logical, font.clone(), color)) } @@ -670,9 +703,8 @@ fn close_group(app: &mut DesignApp, params: &Value) -> Result { } /// InDesign's commands on a panel tab (and at the end of its panel menu): Close, Close Tab Group, -/// Minimize (an open accordion entry) and Collapse to Icons. `close` is false where the docking -/// surface adds its own Close. -fn tab_commands(app: &mut DesignApp, ui: &mut egui::Ui, panel: &'static str, close: bool) { +/// Minimize (an open accordion entry) and Collapse to Icons. +fn tab_commands(app: &mut DesignApp, ui: &mut egui::Ui, panel: &'static str) { let language = app.ui.language.clone(); let tr = |s: &str| crate::i18n::tr(&language, s).to_owned(); let run = |app: &mut DesignApp, ui: &mut egui::Ui, id: &str, p: Value| { @@ -681,7 +713,7 @@ fn tab_commands(app: &mut DesignApp, ui: &mut egui::Ui, panel: &'static str, clo } ui.close(); }; - if close && ui.button(crate::rtl::widget(ui, tr("Close"))).clicked() { + if ui.button(crate::rtl::widget(ui, tr("Close"))).clicked() { run(app, ui, "window.panel.close", json!({"panel": panel})); } if ui.button(crate::rtl::widget(ui, tr("Close Tab Group"))).clicked() { diff --git a/crates/ui-egui/src/panel_docking_tests.rs b/crates/ui-egui/src/panel_docking_tests.rs index 1ffb17ebb..f436cfb5e 100644 --- a/crates/ui-egui/src/panel_docking_tests.rs +++ b/crates/ui-egui/src/panel_docking_tests.rs @@ -746,3 +746,44 @@ fn paragraph_styles_closed_from_the_dock_reopen_there() { assert!(layout.contains(&"paragraphStyles".to_string())); assert!(layout.floating.iter().all(|g| !g.panels.iter().any(|p| p == "paragraphStyles")), "{:?}", layout.floating); } + +fn click(h: &mut Harness<'static, DesignApp>, at: Pos2, button: PointerButton) { + h.event(Event::PointerMoved(at)); + h.run_steps(1); + for pressed in [true, false] { + h.event(Event::PointerButton { pos: at, button, pressed, modifiers: Modifiers::NONE }); + h.run_steps(1); + } + h.run_steps(2); +} + +/// Window › Paragraph Styles switches to the docking layout. After Window › Pages the Pages panel +/// keeps its three-line panel menu, and its tab menu lists what the classic dock's does. +#[test] +fn pages_keeps_its_panel_menu_in_the_docking_layout() { + let mut app = app(); + app.run("file.new", json!({})).unwrap(); + app.run("window.panel", json!({"panel":"paragraphStyles"})).unwrap(); + app.run("window.panel", json!({"panel":"pages"})).unwrap(); + assert!(app.ui.docking.is_some()); + let mut h = harness(app, vec2(1280.0, 800.0)); + let tab = h.query_all_by_label("Pages").find(|node| node.rect().height() <= 45.0).unwrap().rect(); + let dock = egui::containers::panel::PanelState::load(&h.ctx, egui::Id::new("shared-panel-dock")).unwrap().outer_rect; + click(&mut h, egui::pos2(dock.right() - 12.0, tab.center().y), PointerButton::Primary); + assert!(egui::Popup::is_any_open(&h.ctx), "the three-line button must open a menu"); + for item in ["Numbering & Section Options…", "Close Tab Group", "Float Panel"] { + assert!(h.query_by_label(item).is_some(), "{item}"); + } + h.get_by_label("Panel Options…").click(); + h.run_steps(3); + assert!(h.state().ui.dialog.as_ref().is_some_and(|d| d.id == "pagesPanelOptions")); + h.state_mut().ui.dialog = None; + h.run_steps(2); + + click(&mut h, tab.center(), PointerButton::Secondary); + for item in ["Close", "Close Tab Group", "Minimize", "Collapse to Icons", "Float Panel", "Group with"] { + assert!(h.query_all_by_label(item).next().is_some(), "{item}"); + } + assert!(h.query_by_label("Close Panel").is_none()); + assert!(h.query_by_label("Panel Options…").is_none(), "the panel's own commands stay in its panel menu"); +}