diff --git a/src/app/mod.rs b/src/app/mod.rs index 2eefcad8e..565e06638 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -573,10 +573,6 @@ pub(super) struct OpenCADStudio { mtext_editor: Option, /// Open in-place single-line TEXT editor (plain text-entry box), if any. text_inline: Option, - /// Which layout tab has its context menu open (None = closed). - layout_context_menu: Option, - /// Which drawing tab has its context menu open (None = closed). - doc_tab_context_menu: Option, /// Cursor-anchored one-shot snap override menu (Shift+RMB): the canvas /// point it opened at, or `None` when closed (#337). snap_override_popup: Option, @@ -1515,9 +1511,6 @@ pub enum Message { }, /// Close the given tab index. TabClose(usize), - /// Open/close the right-click menu for a drawing tab. - DocTabContextMenu(usize), - DocTabContextMenuClose, /// Save every drawing that already has a file path. DocTabSaveAll, /// Close every non-Start drawing tab. @@ -1968,10 +1961,6 @@ pub enum Message { LayoutRenameCommit, /// Cancel an in-progress rename (Escape). LayoutRenameCancel, - /// Open the right-click context menu for the given layout tab. - LayoutContextMenu(String), - /// Close the layout context menu. - LayoutContextMenuClose, // ── Layout Manager Panel ──────────────────────────────────────────── LayoutManagerOpen, #[allow(dead_code)] @@ -2599,8 +2588,6 @@ impl OpenCADStudio { cont_anchor: None, mtext_editor: None, text_inline: None, - layout_context_menu: None, - doc_tab_context_menu: None, snap_override_popup: None, axis_lock_dir: None, layout_rename_state: None, diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 176214e1b..4376ad2ae 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -561,7 +561,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { self.refresh_properties(); return Task::none(); } - // Cancel layout rename / context menus first, then fall through. + // Cancel layout rename first, then fall through. let i_e = self.active_tab; if self.qselect.take().is_some() { return Task::none(); @@ -573,10 +573,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { return Task::none(); } } - if self.layout_rename_state.take().is_some() - || self.layout_context_menu.take().is_some() - || self.doc_tab_context_menu.take().is_some() - { + if self.layout_rename_state.take().is_some() { return Task::none(); } // Typed text on the command line cancels first — one diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index d8c824823..c19138f45 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -930,9 +930,7 @@ impl OpenCADStudio { } Message::TabSwitch(idx) => { - self.doc_tab_context_menu = None; self.layout_list_open = false; - self.layout_context_menu = None; self.layout_rename_state = None; if idx < self.tabs.len() { if idx != self.active_tab { @@ -991,35 +989,14 @@ impl OpenCADStudio { if let Some(index) = self.tabs.iter().position(|tab| tab.id == active_id) { self.active_tab = index; } - self.doc_tab_context_menu = None; Task::none() } - Message::TabClose(idx) => { - self.doc_tab_context_menu = None; - self.on_tab_close(idx) - } + Message::TabClose(idx) => self.on_tab_close(idx), - Message::DocTabContextMenu(idx) => { - if self.tabs.get(idx).is_some_and(|tab| !tab.is_start) { - self.layout_context_menu = None; - self.doc_tab_context_menu = Some(idx); - } - Task::none() - } - - Message::DocTabContextMenuClose => { - self.doc_tab_context_menu = None; - Task::none() - } - - Message::DocTabSaveAll => { - self.doc_tab_context_menu = None; - self.dispatch_command("SAVEALL") - } + Message::DocTabSaveAll => self.dispatch_command("SAVEALL"), Message::DocTabCloseAll => { - self.doc_tab_context_menu = None; let ids = self .tabs .iter() @@ -1030,7 +1007,6 @@ impl OpenCADStudio { } Message::DocTabCloseOthers(idx) => { - self.doc_tab_context_menu = None; let Some(keep_id) = self.tabs.get(idx).filter(|tab| !tab.is_start).map(|t| t.id) else { return Task::none(); @@ -1046,7 +1022,6 @@ impl OpenCADStudio { } Message::DocTabCopyFullPath(idx) => { - self.doc_tab_context_menu = None; #[cfg(not(target_arch = "wasm32"))] { let Some(path) = self.tabs.get(idx).and_then(|tab| tab.current_path.clone()) @@ -1078,7 +1053,6 @@ impl OpenCADStudio { } Message::DocTabOpenFileLocation(idx) => { - self.doc_tab_context_menu = None; #[cfg(not(target_arch = "wasm32"))] { let Some(path) = self.tabs.get(idx).and_then(|tab| tab.current_path.clone()) @@ -3383,7 +3357,6 @@ impl OpenCADStudio { self.push_undo_snapshot(i, "LAYOUT REORDER"); self.tabs[i].scene.set_layout_tab_order(&paper); self.tabs[i].dirty = true; - self.layout_context_menu = None; Task::none() } @@ -3393,7 +3366,6 @@ impl OpenCADStudio { let i = self.active_tab; self.push_undo_snapshot(i, "LAYOUT DEL"); if self.tabs[i].scene.delete_layout(&name) { - self.layout_context_menu = None; self.layout_rename_state = None; self.command_line .push_output(&format!("Layout \"{name}\" silindi")); @@ -3405,7 +3377,6 @@ impl OpenCADStudio { Message::LayoutRenameStart(name) => { if name != "Model" { self.layout_rename_state = Some((name.clone(), name)); - self.layout_context_menu = None; // Focus the inline field so the user types into it // directly instead of the command line (issue #86). return iced::widget::operation::focus(iced::widget::Id::new( @@ -3430,26 +3401,6 @@ impl OpenCADStudio { Task::none() } - Message::LayoutContextMenu(name) => { - // No menu on the transient BEDIT block tab: Rename/Delete are - // layout operations and don't apply to it. - let is_block_tab = self.tabs[self.active_tab] - .block_edit - .as_ref() - .map(|be| be.block_name == name) - .unwrap_or(false); - if name != "Model" && !is_block_tab { - self.doc_tab_context_menu = None; - self.layout_context_menu = Some(name); - } - Task::none() - } - - Message::LayoutContextMenuClose => { - self.layout_context_menu = None; - Task::none() - } - // ── Layout Manager Panel ────────────────────────────────────────── Message::LayoutManagerOpen => { let i = self.active_tab; diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 102d0faaf..ff2ae6c88 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -3702,9 +3702,8 @@ impl OpenCADStudio { self.tabs[i].scene.sync_camera_to_document(); self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation; let sync_ms = perf_phase.elapsed().as_secs_f64() * 1000.0; - // Cancel any pending rename/context-menu and active viewport when switching. + // Cancel any pending rename and active viewport when switching. self.layout_rename_state = None; - self.layout_context_menu = None; self.tabs[i].scene.active_viewport = None; let perf_phase = Instant::now(); self.tabs[i].scene.set_current_layout(name.clone()); diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 18f585c64..bb25ea630 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -13,6 +13,7 @@ use iced::widget::{ }; use iced::window; use iced::{keyboard, Background, Border, Color, Element, Fill, Subscription, Task, Theme}; +use iced_aw::ContextMenu; mod controls; mod modal; @@ -21,8 +22,8 @@ mod viewcube; use controls::{dyn_component_value, viewport_controls}; use overlay::{ - doc_tab_context_menu_overlay, layout_context_menu_overlay, mtext_editor_overlay, - position_canvas_overlay, qselect_overlay, text_inline_overlay, viewport_context_menu_overlay, + mtext_editor_overlay, position_canvas_overlay, qselect_overlay, text_inline_overlay, + viewport_context_menu_overlay, }; use viewcube::{viewcube_nav_controls, viewcube_ucs_picker, UCS_PICKER_W}; @@ -1465,8 +1466,6 @@ impl OpenCADStudio { .width(Fill) .height(Fill); - let win = self.win_size; - let dropdown_layer: Element<'_, Message> = self .ribbon .dropdown_overlay( @@ -1476,33 +1475,6 @@ 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, win) - } else { - iced::widget::Space::new().width(0).height(0).into() - }; - - let doc_tab_ctx_layer: Element<'_, Message> = - if let Some(idx) = self.doc_tab_context_menu { - if let Some(context_tab) = self.tabs.get(idx) { - let has_other_drawings = self - .tabs - .iter() - .enumerate() - .any(|(other_idx, tab)| other_idx != idx && !tab.is_start); - doc_tab_context_menu_overlay( - idx, - context_tab.current_path.as_deref(), - has_other_drawings, - win, - ) - } else { - iced::widget::Space::new().width(0).height(0).into() - } - } else { - iced::widget::Space::new().width(0).height(0).into() - }; - let snap_override_layer: Element<'_, Message> = if let Some(pos) = self.snap_override_popup { overlay::snap_override_overlay(pos) @@ -1527,8 +1499,6 @@ impl OpenCADStudio { let composed = stack![ main_ui, dropdown_layer, - layout_ctx_layer, - doc_tab_ctx_layer, qselect_layer, snap_override_layer, open_progress_layer, @@ -1919,6 +1889,109 @@ impl OpenCADStudio { // ── Document tab bar ─────────────────────────────────────────────────────── +/// Right-click actions for a drawing tab. `ContextMenu` owns opening, +/// cursor-relative placement, boundary clamping, and dismissal. +fn doc_tab_context_menu( + tab_idx: usize, + has_current_path: bool, + has_other_drawings: bool, +) -> Element<'static, Message> { + const MENU_W: f32 = 210.0; + const MENU_BG: Color = Color { + r: 0.17, + g: 0.17, + b: 0.17, + a: 1.0, + }; + const MENU_BORDER: Color = Color { + r: 0.35, + g: 0.35, + b: 0.35, + a: 1.0, + }; + const ITEM_HOVER: Color = Color { + r: 0.25, + g: 0.45, + b: 0.70, + a: 1.0, + }; + const TEXT_COLOR: Color = Color { + r: 0.88, + g: 0.88, + b: 0.88, + a: 1.0, + }; + const DISABLED_TEXT: Color = Color { + r: 0.43, + g: 0.43, + b: 0.43, + a: 1.0, + }; + + let item = |label: &'static str, msg: Option| { + let enabled = msg.is_some(); + let mut item = button(text(label).size(12).color(if enabled { + TEXT_COLOR + } else { + DISABLED_TEXT + })) + .style(move |_: &Theme, status| button::Style { + background: Some(Background::Color(if enabled { + match status { + button::Status::Hovered | button::Status::Pressed => ITEM_HOVER, + _ => Color::TRANSPARENT, + } + } else { + Color::TRANSPARENT + })), + text_color: if enabled { TEXT_COLOR } else { DISABLED_TEXT }, + border: Border::default(), + shadow: iced::Shadow::default(), + snap: false, + }) + .padding([4, 12]) + .width(Fill); + if let Some(msg) = msg { + item = item.on_press(msg); + } + item + }; + + let native_path_actions = cfg!(not(target_arch = "wasm32")) && has_current_path; + container( + column![ + item("Save All", Some(Message::DocTabSaveAll)), + item("Close All", Some(Message::DocTabCloseAll)), + item( + "Close All Other Drawings", + has_other_drawings.then_some(Message::DocTabCloseOthers(tab_idx)), + ), + item( + "Copy Full File Path", + native_path_actions.then_some(Message::DocTabCopyFullPath(tab_idx)), + ), + item( + "Open File Location", + native_path_actions.then_some(Message::DocTabOpenFileLocation(tab_idx)), + ), + ] + .spacing(0) + .width(MENU_W), + ) + .style(move |_: &Theme| container::Style { + background: Some(Background::Color(MENU_BG)), + border: Border { + color: MENU_BORDER, + width: 1.0, + radius: 4.0.into(), + }, + ..Default::default() + }) + .padding([4, 0]) + .width(iced::Length::Fixed(MENU_W)) + .into() +} + pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Element<'a, Message> { const BAR_BG: Color = Color { r: 0.13, @@ -2093,9 +2166,13 @@ pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Ele let tab_element: Element<'_, Message> = if tab.is_start { tab_container.into() } else { + let has_current_path = tab.current_path.is_some(); + let has_other_drawings = drag_targets.len() > 1; crate::ui::wrap_bar::PosReport::owned( format!("DOC_TAB:{idx}"), - mouse_area(tab_container).on_right_press(Message::DocTabContextMenu(idx)), + ContextMenu::new(tab_container, move || { + doc_tab_context_menu(idx, has_current_path, has_other_drawings) + }), ) .into() }; diff --git a/src/app/view/overlay.rs b/src/app/view/overlay.rs index 660b5a8b3..d99708753 100644 --- a/src/app/view/overlay.rs +++ b/src/app/view/overlay.rs @@ -700,132 +700,6 @@ pub(super) fn mtext_editor_overlay<'a>( ) } -// ── Drawing-tab right-click context menu ─────────────────────────────────── - -/// Right-click menu for a drawing tab. The scope deliberately contains only -/// the actions approved in issue #493. -pub(super) fn doc_tab_context_menu_overlay( - tab_idx: usize, - current_path: Option<&std::path::Path>, - has_other_drawings: bool, - win: (f32, f32), -) -> Element<'_, Message> { - const MENU_W: f32 = 210.0; - const MENU_H: f32 = 136.0; - const MENU_BG: Color = Color { - r: 0.17, - g: 0.17, - b: 0.17, - a: 1.0, - }; - const MENU_BORDER: Color = Color { - r: 0.35, - g: 0.35, - b: 0.35, - a: 1.0, - }; - const ITEM_HOVER: Color = Color { - r: 0.25, - g: 0.45, - b: 0.70, - a: 1.0, - }; - const TEXT_COLOR: Color = Color { - r: 0.88, - g: 0.88, - b: 0.88, - a: 1.0, - }; - const DISABLED_TEXT: Color = Color { - r: 0.43, - g: 0.43, - b: 0.43, - a: 1.0, - }; - - let item = |label: &'static str, msg: Option| { - let enabled = msg.is_some(); - let mut item = button(text(label).size(12).color(if enabled { - TEXT_COLOR - } else { - DISABLED_TEXT - })) - .style(move |_: &Theme, status| button::Style { - background: Some(Background::Color(if enabled { - match status { - button::Status::Hovered | button::Status::Pressed => ITEM_HOVER, - _ => Color::TRANSPARENT, - } - } else { - Color::TRANSPARENT - })), - text_color: if enabled { TEXT_COLOR } else { DISABLED_TEXT }, - border: Border::default(), - shadow: iced::Shadow::default(), - snap: false, - }) - .padding([4, 12]) - .width(Fill); - if let Some(msg) = msg { - item = item.on_press(msg); - } - item - }; - - let native_path_actions = cfg!(not(target_arch = "wasm32")) && current_path.is_some(); - let menu = container( - column![ - item("Save All", Some(Message::DocTabSaveAll)), - item("Close All", Some(Message::DocTabCloseAll)), - item( - "Close All Other Drawings", - has_other_drawings.then_some(Message::DocTabCloseOthers(tab_idx)), - ), - item( - "Copy Full File Path", - native_path_actions.then_some(Message::DocTabCopyFullPath(tab_idx)), - ), - item( - "Open File Location", - native_path_actions.then_some(Message::DocTabOpenFileLocation(tab_idx)), - ), - ] - .spacing(0) - .width(MENU_W), - ) - .style(move |_: &Theme| container::Style { - background: Some(Background::Color(MENU_BG)), - border: Border { - color: MENU_BORDER, - width: 1.0, - radius: 4.0.into(), - }, - ..Default::default() - }) - .padding([4, 0]) - .width(iced::Length::Fixed(MENU_W)); - - let catcher = mouse_area( - container(Space::new().width(Fill).height(Fill)) - .width(Fill) - .height(Fill), - ) - .on_press(Message::DocTabContextMenuClose) - .on_right_press(Message::DocTabContextMenuClose); - - let bounds = crate::ui::wrap_bar::dropdown_bounds(&format!("DOC_TAB:{tab_idx}")); - let pos = bounds - .map(|b| { - iced::Point::new( - b.x.clamp(0.0, (win.0 - MENU_W).max(0.0)), - (b.y + b.height).clamp(0.0, (win.1 - MENU_H).max(0.0)), - ) - }) - .unwrap_or(iced::Point::new(4.0, 30.0)); - - stack![catcher, position_canvas_overlay(pos, menu.into())].into() -} - // ── Viewport right-click context menu ────────────────────────────────────── pub(super) fn viewport_context_menu_overlay( @@ -1050,92 +924,6 @@ pub(super) fn viewport_context_menu_overlay( position_canvas_overlay(pos, menu.into()) } -/// 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, win: (f32, f32)) -> Element<'_, Message> { - const MENU_BG: Color = Color { - r: 0.17, - g: 0.17, - b: 0.17, - a: 1.0, - }; - const MENU_BORDER: Color = Color { - r: 0.35, - g: 0.35, - b: 0.35, - a: 1.0, - }; - const ITEM_HOVER: Color = Color { - r: 0.25, - g: 0.45, - b: 0.70, - a: 1.0, - }; - const TEXT_COLOR: Color = Color { - r: 0.88, - g: 0.88, - b: 0.88, - a: 1.0, - }; - - let item = |label: &'static str, msg: Message| { - button(text(label).size(12).color(TEXT_COLOR)) - .on_press(msg) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => ITEM_HOVER, - _ => Color::TRANSPARENT, - })), - text_color: TEXT_COLOR, - border: Border::default(), - shadow: iced::Shadow::default(), - snap: false, - }) - .padding([4, 12]) - .width(Fill) - }; - - let rename_name = name.to_string(); - let delete_name = name.to_string(); - - let menu = container( - column![ - item("Rename", Message::LayoutRenameStart(rename_name)), - item("Delete", Message::LayoutDelete(delete_name)), - ] - .spacing(0) - .width(160), - ) - .style(move |_: &Theme| container::Style { - background: Some(Background::Color(MENU_BG)), - border: Border { - color: MENU_BORDER, - width: 1.0, - radius: 4.0.into(), - }, - ..Default::default() - }) - .padding([4, 0]); - - // Click-catcher fills the whole screen to close the menu when clicking outside. - let catcher = mouse_area( - container(iced::widget::Space::new().width(Fill).height(Fill)) - .width(Fill) - .height(Fill), - ) - .on_press(Message::LayoutContextMenuClose) - .on_right_press(Message::LayoutContextMenuClose); - - // 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() -} - /// One-shot snap override menu (Shift+RMB, #337): a cursor-anchored grid of /// snap ICONS only — the names show as hover tooltips. Picking one applies /// that snap to just the next point pick. diff --git a/src/ui/popup/mod.rs b/src/ui/popup/mod.rs index 8d7c7ea91..e5ff95d10 100644 --- a/src/ui/popup/mod.rs +++ b/src/ui/popup/mod.rs @@ -5,76 +5,3 @@ pub mod scale_popup; pub mod selection_filter_popup; pub mod snap_popup; pub mod units_popup; - -use iced::widget::container; -use iced::{Element, Fill, Padding, Rectangle}; - -use crate::app::Message; - -/// Decide a status-bar popup's horizontal anchor and paddings from its pill's -/// screen bounds. Returns `(align_right, horizontal_pad, bottom_pad)`. -/// -/// The popup opens just above the pill. Horizontally it prefers `prefer_right` -/// (right edge aligned to the pill's right, growing left) but flips to the other -/// side when that direction would cross the window edge. -pub fn popup_anchor( - pill: Option, - win: (f32, f32), - popup_w: f32, - prefer_right: bool, -) -> (bool, f32, f32) { - let Some(b) = pill else { - // No recorded position yet — fall back to the bottom-right corner. - return (prefer_right, 4.0, 27.0); - }; - let bottom = (win.1 - b.y).max(0.0); - let right_pad = (win.0 - (b.x + b.width)).max(4.0); - let left_pad = b.x.max(4.0); - - if prefer_right { - // Right-aligned popup grows left; flip left-aligned if it would pass the - // left window edge. - if (b.x + b.width) - popup_w < 2.0 { - (false, left_pad, bottom) - } else { - (true, right_pad, bottom) - } - } else { - // Left-aligned popup grows right; flip right-aligned if it would pass the - // right window edge. - if b.x + popup_w > win.0 - 2.0 { - (true, right_pad, bottom) - } else { - (false, left_pad, bottom) - } - } -} - -/// Wrap a status-bar popup `panel` in a full-window container positioned just -/// above its pill, flipping horizontally to stay on screen. -pub fn position_statusbar_popup<'a>( - panel: Element<'a, Message>, - pill: Option, - win: (f32, f32), - popup_w: f32, - prefer_right: bool, -) -> Element<'a, Message> { - let (align_right, h_pad, bottom) = popup_anchor(pill, win, popup_w, prefer_right); - let pad = Padding { - top: 0.0, - bottom, - left: if align_right { 0.0 } else { h_pad }, - right: if align_right { h_pad } else { 0.0 }, - }; - let c = container(panel) - .align_bottom(Fill) - .width(Fill) - .height(Fill) - .padding(pad); - let c = if align_right { - c.align_right(Fill) - } else { - c.align_left(Fill) - }; - c.into() -} diff --git a/src/ui/statusbar/mod.rs b/src/ui/statusbar/mod.rs index f5891e633..cee526a40 100644 --- a/src/ui/statusbar/mod.rs +++ b/src/ui/statusbar/mod.rs @@ -6,9 +6,10 @@ pub mod status_menu; use iced::widget::tooltip::Position as TipPos; use iced::widget::{ - button, container, mouse_area, row, text, text_input, tooltip, + button, column, container, mouse_area, row, text, text_input, tooltip, }; use iced::{Background, Border, Color, Element, Length, Theme}; +use iced_aw::ContextMenu; use std::sync::Arc; /// Scrollable id of the status-bar layout-tab strip (retained so the existing @@ -766,6 +767,70 @@ fn osnap_btn<'a>( // ── Helpers ─────────────────────────────────────────────────────────────── +fn layout_tab_context_menu(name: String) -> Element<'static, Message> { + const MENU_BG: Color = Color { + r: 0.17, + g: 0.17, + b: 0.17, + a: 1.0, + }; + const MENU_BORDER: Color = Color { + r: 0.35, + g: 0.35, + b: 0.35, + a: 1.0, + }; + const ITEM_HOVER: Color = Color { + r: 0.25, + g: 0.45, + b: 0.70, + a: 1.0, + }; + const TEXT_COLOR: Color = Color { + r: 0.88, + g: 0.88, + b: 0.88, + a: 1.0, + }; + + let item = |label: &'static str, msg: Message| { + button(text(label).size(12).color(TEXT_COLOR)) + .on_press(msg) + .style(|_: &Theme, status| button::Style { + background: Some(Background::Color(match status { + button::Status::Hovered | button::Status::Pressed => ITEM_HOVER, + _ => Color::TRANSPARENT, + })), + text_color: TEXT_COLOR, + border: Border::default(), + shadow: iced::Shadow::default(), + snap: false, + }) + .padding([4, 12]) + .width(Length::Fill) + }; + + container( + column![ + item("Rename", Message::LayoutRenameStart(name.clone())), + item("Delete", Message::LayoutDelete(name)), + ] + .spacing(0) + .width(160), + ) + .style(move |_: &Theme| container::Style { + background: Some(Background::Color(MENU_BG)), + border: Border { + color: MENU_BORDER, + width: 1.0, + radius: 4.0.into(), + }, + ..Default::default() + }) + .padding([4, 0]) + .into() +} + /// A layout tab button. /// /// When `rename_edit` is `Some(value)` the tab shows an inline text input @@ -884,7 +949,8 @@ fn space_tab<'a>( .align_y(iced::Center) .into() } else { - // Normal clickable tab — left click switches, right click opens context menu. + // Normal clickable tab — left click switches. Paper-layout tabs are + // wrapped in `ContextMenu`, which owns right-click handling. let display = container(text(label.clone()).size(12).color(text_color)) .style(move |_: &Theme| container::Style { background: Some(Background::Color(bg(is_active, false))), @@ -894,16 +960,10 @@ fn space_tab<'a>( .padding([4, 10]); let switch_msg = Message::LayoutSwitch(label.clone()); - let ctx_msg = Message::LayoutContextMenu(label.clone()); - - // Use mouse_area so we can capture right-click for the context menu. - // 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). - let tab = mouse_area(display) - .on_press(switch_msg) - .on_right_press(ctx_msg); - let tab: Element<'a, Message> = if reorderable_layouts.contains(&label) { + let has_context_menu = reorderable_layouts.contains(&label); + let report_key = format!("SB_LAYOUT_TAB:{label}"); + let tab = mouse_area(display).on_press(switch_msg); + let tab: Element<'a, Message> = if has_context_menu { crate::ui::wrap_bar::ReorderTab::layout( label.clone(), reorderable_layouts, @@ -914,8 +974,13 @@ fn space_tab<'a>( tab.into() }; + let tab = if has_context_menu { + ContextMenu::new(tab, move || layout_tab_context_menu(label.clone())).into() + } else { + tab + }; crate::ui::wrap_bar::PosReport::owned( - format!("SB_LAYOUT_TAB:{label}"), + report_key, tab, ) .into()