From d6b1307a0ec9bbe9dbb579e5106428065b41f3a1 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 21 Jul 2026 10:38:17 +0300 Subject: [PATCH] fix(statusbar): open the layout context menu at the clicked tab The right-click menu was pinned to the screen's bottom-left corner. Each layout tab now records its screen bounds through a PosReport wrapper (which learned runtime-built ids for the dynamic tab names), and the menu anchors next to the clicked tab via the shared status-bar popup positioning, falling back to the left edge only when no bounds were recorded (#428). Co-Authored-By: Claude Fable 5 --- src/app/view/mod.rs | 2 +- src/app/view/overlay.rs | 18 +++++++----------- src/ui/statusbar/mod.rs | 14 ++++++++++---- src/ui/wrap_bar.rs | 16 ++++++++++++---- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index e0339f820..dbd865e5d 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1505,7 +1505,7 @@ impl OpenCADStudio { .unwrap_or_else(|| iced::widget::Space::new().width(0).height(0).into()); let layout_ctx_layer: Element<'_, Message> = if let Some(name) = &self.layout_context_menu { - layout_context_menu_overlay(name) + layout_context_menu_overlay(name, win) } else { iced::widget::Space::new().width(0).height(0).into() }; diff --git a/src/app/view/overlay.rs b/src/app/view/overlay.rs index c0fcfadb4..c50419fd4 100644 --- a/src/app/view/overlay.rs +++ b/src/app/view/overlay.rs @@ -935,7 +935,7 @@ pub(super) fn viewport_context_menu_overlay( /// A small right-click context menu rendered above the status bar. /// The `name` is the layout tab that was right-clicked. -pub(super) fn layout_context_menu_overlay(name: &str) -> Element<'_, Message> { +pub(super) fn layout_context_menu_overlay(name: &str, win: (f32, f32)) -> Element<'_, Message> { const MENU_BG: Color = Color { r: 0.17, g: 0.17, @@ -1009,16 +1009,12 @@ pub(super) fn layout_context_menu_overlay(name: &str) -> Element<'_, Message> { .on_press(Message::LayoutContextMenuClose) .on_right_press(Message::LayoutContextMenuClose); - // Position the menu above the status bar at the left. - let positioned = container(menu) - .align_bottom(Fill) - .align_left(Fill) - .padding(iced::Padding { - top: 0.0, - right: 0.0, - bottom: 30.0, - left: 8.0, - }); + // Anchor the menu above the status bar next to the right-clicked tab — + // its bounds were recorded by the tab's PosReport wrapper (#428). A + // missing report (shouldn't happen) falls back to the left edge. + let pill = crate::ui::wrap_bar::dropdown_bounds(&format!("SB_LAYOUT_TAB:{name}")); + let positioned = + crate::ui::popup::position_statusbar_popup(menu.into(), pill, win, 160.0, false); stack![catcher, positioned].into() } diff --git a/src/ui/statusbar/mod.rs b/src/ui/statusbar/mod.rs index ddeadcf6e..00455c6a6 100644 --- a/src/ui/statusbar/mod.rs +++ b/src/ui/statusbar/mod.rs @@ -742,10 +742,16 @@ fn space_tab<'a>( let ctx_msg = Message::LayoutContextMenu(label.clone()); // Use mouse_area so we can capture right-click for the context menu. - mouse_area(display) - .on_press(switch_msg) - .on_right_press(ctx_msg) - .into() + // PosReport records the tab's screen bounds so the context menu can + // anchor next to the clicked tab instead of the screen's left edge + // (#428). + crate::ui::wrap_bar::PosReport::owned( + format!("SB_LAYOUT_TAB:{label}"), + mouse_area(display) + .on_press(switch_msg) + .on_right_press(ctx_msg), + ) + .into() } } diff --git a/src/ui/wrap_bar.rs b/src/ui/wrap_bar.rs index a2aedb8c9..ea16572e3 100644 --- a/src/ui/wrap_bar.rs +++ b/src/ui/wrap_bar.rs @@ -31,7 +31,7 @@ thread_local! { /// Screen bounds of every ribbon dropdown button, keyed by dropdown id, so /// an open dropdown's overlay can anchor directly below its widget at any /// size. Written by `PosReport` on draw, read by `dropdown_bounds`. - static DD_BOUNDS: RefCell> = + static DD_BOUNDS: RefCell> = RefCell::new(FxHashMap::default()); } @@ -871,14 +871,22 @@ impl<'a> From> for Element<'a, Message> { /// A transparent wrapper that records its child's screen bounds under `id` on /// every draw, so an open dropdown can anchor its overlay just below the widget. pub struct PosReport<'a> { - id: &'static str, + id: std::borrow::Cow<'static, str>, child: Element<'a, Message>, } impl<'a> PosReport<'a> { pub fn new(id: &'static str, child: impl Into>) -> Self { Self { - id, + id: std::borrow::Cow::Borrowed(id), + child: child.into(), + } + } + + /// Report under a runtime-built id (e.g. one per layout tab). + pub fn owned(id: String, child: impl Into>) -> Self { + Self { + id: std::borrow::Cow::Owned(id), child: child.into(), } } @@ -971,7 +979,7 @@ impl<'a> Widget for PosReport<'a> { viewport: &Rectangle, ) { DD_BOUNDS.with(|m| { - m.borrow_mut().insert(self.id, layout.bounds()); + m.borrow_mut().insert(self.id.to_string(), layout.bounds()); }); self.child.as_widget().draw( &tree.children[0],