From 966b637796bc9a7cf175c066ded41521b7c72e0f Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Tue, 13 Oct 2026 04:27:41 +0200 Subject: [PATCH] fix(ui): open the dock's panel menu from its three-line button (part of #676) --- crates/ui-egui/src/dock.rs | 12 +++++ crates/ui-egui/src/panel_docking.rs | 54 ++++++++++++----------- crates/ui-egui/src/panel_docking_tests.rs | 25 +++++++++++ crates/ui-egui/src/panels/pages.rs | 9 ++++ 4 files changed, 75 insertions(+), 25 deletions(-) diff --git a/crates/ui-egui/src/dock.rs b/crates/ui-egui/src/dock.rs index 72810e1..0d581e9 100644 --- a/crates/ui-egui/src/dock.rs +++ b/crates/ui-egui/src/dock.rs @@ -90,7 +90,19 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { x += w; } let menu_r = egui::Rect::from_min_size(egui::pos2(strip.max.x - 24.0, strip.min.y + 4.0), vec2(18.0, 18.0)); + let menu = ui.interact(menu_r, ui.id().with("dock-menu"), Sense::click()); + if menu.hovered() { + ui.painter().rect_filled(menu_r, 3.0, t.hover); + } 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::Frame::NONE.inner_margin(egui::Margin { left: 10, right: 10, top: 4, bottom: 6 }).show(ui, |ui| { egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| match app.ui.dock_tab.as_str() { "pages" => panels::pages::show(app, ui), diff --git a/crates/ui-egui/src/panel_docking.rs b/crates/ui-egui/src/panel_docking.rs index e53986c..05fc115 100644 --- a/crates/ui-egui/src/panel_docking.rs +++ b/crates/ui-egui/src/panel_docking.rs @@ -418,31 +418,7 @@ fn icon_rail(app: &mut DesignApp, ui: &mut egui::Ui) { pub(crate) fn legacy_tab(app: &mut DesignApp, ui: &mut egui::Ui, panel: &str, response: &egui::Response) { let Some(panel) = normalize(panel) else { return }; - let language = app.ui.language.clone(); - response.context_menu(|ui| { - if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&language, "Float Panel"))).clicked() { - if let Err(error) = app.run("window.panel.float", json!({"panel": panel})) { - app.status(error); - } - ui.close(); - } - ui.menu_button(crate::rtl::widget(ui, crate::i18n::tr(&language, "Group with")), |ui| { - egui::ScrollArea::vertical().max_height(360.0).show(ui, |ui| { - for (anchor, label, _) in crate::dock::DOCK_TABS.iter().chain(crate::dock::ICON_PANELS) { - if *anchor != panel && ui.button(crate::rtl::widget(ui, crate::i18n::tr(&language, label))).clicked() { - // A closed icon panel is made visible before becoming the destination. - let result = app - .run("window.panel.activate", json!({"panel": anchor})) - .and_then(|_| app.run("window.panel.move", json!({"panel":panel,"anchor":anchor}))); - if let Err(error) = result { - app.status(error); - } - ui.close(); - } - } - }); - }); - }); + response.context_menu(|ui| panel_menu(app, ui, panel)); if response.drag_started() && let Some(id) = normalize(panel) { @@ -472,6 +448,34 @@ pub(crate) fn legacy_tab(app: &mut DesignApp, ui: &mut egui::Ui, panel: &str, re } } +/// 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 }; + let language = app.ui.language.clone(); + if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&language, "Float Panel"))).clicked() { + if let Err(error) = app.run("window.panel.float", json!({"panel": panel})) { + app.status(error); + } + ui.close(); + } + ui.menu_button(crate::rtl::widget(ui, crate::i18n::tr(&language, "Group with")), |ui| { + egui::ScrollArea::vertical().max_height(360.0).show(ui, |ui| { + for (anchor, label, _) in crate::dock::DOCK_TABS.iter().chain(crate::dock::ICON_PANELS) { + if *anchor != panel && ui.button(crate::rtl::widget(ui, crate::i18n::tr(&language, label))).clicked() { + // A closed icon panel is made visible before becoming the destination. + let result = app + .run("window.panel.activate", json!({"panel": anchor})) + .and_then(|_| app.run("window.panel.move", json!({"panel":panel,"anchor":anchor}))); + if let Err(error) = result { + app.status(error); + } + ui.close(); + } + } + }); + }); +} + struct PanelContent<'a>(&'a mut DesignApp); impl craft_ui::docking::DockContent for PanelContent<'_> { fn body(&mut self, ui: &mut egui::Ui, id: &String) { diff --git a/crates/ui-egui/src/panel_docking_tests.rs b/crates/ui-egui/src/panel_docking_tests.rs index 210ae0c..de08e3c 100644 --- a/crates/ui-egui/src/panel_docking_tests.rs +++ b/crates/ui-egui/src/panel_docking_tests.rs @@ -597,3 +597,28 @@ fn capture_legacy_panel_menu_visual_fixture() { assert_eq!(serde_json::to_value(&h.state().session.active().unwrap().doc).unwrap(), doc); h.render().unwrap().save(directory.join("designcraft-legacy-last-target-selected-dark-ar.png")).unwrap(); } + +#[test] +fn legacy_dock_menu_button_opens_the_active_panel_menu() { + let mut app = app(); + app.run("file.new", json!({})).unwrap(); + app.ui.dock_tab = "pages".into(); + let pages = app.session.active().unwrap().doc.page_count(); + let mut h = harness(app, vec2(1280.0, 800.0)); + assert!(h.state().ui.docking.is_none()); + 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("dock")).unwrap().outer_rect; + let at = egui::pos2(dock.right() - 15.0, tab.center().y); + h.event(Event::PointerMoved(at)); + h.run_steps(1); + for pressed in [true, false] { + h.event(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }); + h.run_steps(1); + } + h.run_steps(2); + assert!(egui::Popup::is_any_open(&h.ctx), "the three-line button must open a menu"); + h.get_by_label("Insert Page After").click(); + h.run_steps(3); + assert_eq!(h.state().session.active().unwrap().doc.page_count(), pages + 1); + assert!(!egui::Popup::is_any_open(&h.ctx)); +} diff --git a/crates/ui-egui/src/panels/pages.rs b/crates/ui-egui/src/panels/pages.rs index 6bbad90..ae32322 100644 --- a/crates/ui-egui/src/panels/pages.rs +++ b/crates/ui-egui/src/panels/pages.rs @@ -216,6 +216,15 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { } } +/// The panel menu's page commands, acting on the current page and its spread. +pub fn panel_menu(app: &mut DesignApp, ui: &mut egui::Ui) { + let Some(st) = app.session.active() else { return }; + let doc = st.doc.clone(); + let cur = crate::canvas::current_page(app).unwrap_or(0); + let Some((si, _)) = doc.page_loc(cur) else { return }; + page_menu(app, ui, &doc, cur, si); +} + fn page_menu(app: &mut DesignApp, ui: &mut egui::Ui, doc: &designcraft_doc::Document, abs: usize, si: usize) { if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Insert Page After"))).clicked() { let _ = app.run("layout.pages.insert", json!({"after": abs, "count": 1}));