refactor(tabs): use context menu widget

This commit is contained in:
Hakan Seven committed 2026-07-28 01:34:45 +03:00
1 parent ce1091cea5
commit 73545520c8
8 files changed
+194 -403

No files matched your search

+111 -34
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()
};
-212
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.