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 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-21 10:38:17 +03:00
commit d6b1307a0e
4 changed files with 30 additions and 20 deletions

View file

@ -1505,7 +1505,7 @@ impl OpenCADStudio {
.unwrap_or_else(|| iced::widget::Space::new().width(0).height(0).into()); .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 { 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 { } else {
iced::widget::Space::new().width(0).height(0).into() iced::widget::Space::new().width(0).height(0).into()
}; };

View file

@ -935,7 +935,7 @@ pub(super) fn viewport_context_menu_overlay(
/// A small right-click context menu rendered above the status bar. /// A small right-click context menu rendered above the status bar.
/// The `name` is the layout tab that was right-clicked. /// 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 { const MENU_BG: Color = Color {
r: 0.17, r: 0.17,
g: 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_press(Message::LayoutContextMenuClose)
.on_right_press(Message::LayoutContextMenuClose); .on_right_press(Message::LayoutContextMenuClose);
// Position the menu above the status bar at the left. // Anchor the menu above the status bar next to the right-clicked tab —
let positioned = container(menu) // its bounds were recorded by the tab's PosReport wrapper (#428). A
.align_bottom(Fill) // missing report (shouldn't happen) falls back to the left edge.
.align_left(Fill) let pill = crate::ui::wrap_bar::dropdown_bounds(&format!("SB_LAYOUT_TAB:{name}"));
.padding(iced::Padding { let positioned =
top: 0.0, crate::ui::popup::position_statusbar_popup(menu.into(), pill, win, 160.0, false);
right: 0.0,
bottom: 30.0,
left: 8.0,
});
stack![catcher, positioned].into() stack![catcher, positioned].into()
} }

View file

@ -742,10 +742,16 @@ fn space_tab<'a>(
let ctx_msg = Message::LayoutContextMenu(label.clone()); let ctx_msg = Message::LayoutContextMenu(label.clone());
// Use mouse_area so we can capture right-click for the context menu. // Use mouse_area so we can capture right-click for the context menu.
mouse_area(display) // PosReport records the tab's screen bounds so the context menu can
.on_press(switch_msg) // anchor next to the clicked tab instead of the screen's left edge
.on_right_press(ctx_msg) // (#428).
.into() crate::ui::wrap_bar::PosReport::owned(
format!("SB_LAYOUT_TAB:{label}"),
mouse_area(display)
.on_press(switch_msg)
.on_right_press(ctx_msg),
)
.into()
} }
} }

View file

@ -31,7 +31,7 @@ thread_local! {
/// Screen bounds of every ribbon dropdown button, keyed by dropdown id, so /// 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 /// an open dropdown's overlay can anchor directly below its widget at any
/// size. Written by `PosReport` on draw, read by `dropdown_bounds`. /// size. Written by `PosReport` on draw, read by `dropdown_bounds`.
static DD_BOUNDS: RefCell<FxHashMap<&'static str, Rectangle>> = static DD_BOUNDS: RefCell<FxHashMap<String, Rectangle>> =
RefCell::new(FxHashMap::default()); RefCell::new(FxHashMap::default());
} }
@ -871,14 +871,22 @@ impl<'a> From<DensitySwap<'a>> for Element<'a, Message> {
/// A transparent wrapper that records its child's screen bounds under `id` on /// 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. /// every draw, so an open dropdown can anchor its overlay just below the widget.
pub struct PosReport<'a> { pub struct PosReport<'a> {
id: &'static str, id: std::borrow::Cow<'static, str>,
child: Element<'a, Message>, child: Element<'a, Message>,
} }
impl<'a> PosReport<'a> { impl<'a> PosReport<'a> {
pub fn new(id: &'static str, child: impl Into<Element<'a, Message>>) -> Self { pub fn new(id: &'static str, child: impl Into<Element<'a, Message>>) -> Self {
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<Element<'a, Message>>) -> Self {
Self {
id: std::borrow::Cow::Owned(id),
child: child.into(), child: child.into(),
} }
} }
@ -971,7 +979,7 @@ impl<'a> Widget<Message, Theme, Renderer> for PosReport<'a> {
viewport: &Rectangle, viewport: &Rectangle,
) { ) {
DD_BOUNDS.with(|m| { 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( self.child.as_widget().draw(
&tree.children[0], &tree.children[0],