refactor(tabs): use context menu widget
This commit is contained in:
parent
ce1091cea5
commit
73545520c8
8 changed files with 194 additions and 403 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue