refactor(tabs): use context menu widget

This commit is contained in:
Hakan Seven 2026-07-28 01:34:45 +03:00
commit 73545520c8
8 changed files with 194 additions and 403 deletions

View file

@ -573,10 +573,6 @@ pub(super) struct OpenCADStudio {
mtext_editor: Option<mtext_editor::MTextEditorState>,
/// Open in-place single-line TEXT editor (plain text-entry box), if any.
text_inline: Option<text_inline::TextInlineState>,
/// Which layout tab has its context menu open (None = closed).
layout_context_menu: Option<String>,
/// Which drawing tab has its context menu open (None = closed).
doc_tab_context_menu: Option<usize>,
/// Cursor-anchored one-shot snap override menu (Shift+RMB): the canvas
/// point it opened at, or `None` when closed (#337).
snap_override_popup: Option<iced::Point>,
@ -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,

View file

@ -561,7 +561,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
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<Message> {
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

View file

@ -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;

View file

@ -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());

View file

@ -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<Message>| {
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()
};

View file

@ -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<Message>| {
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.

View file

@ -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<Rectangle>,
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<Rectangle>,
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()
}

View file

@ -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()