From b9de8940bc7d43d6cd2d0ad6f6ab2cc0373286e2 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 28 Jul 2026 01:02:00 +0300 Subject: [PATCH] refactor(statusbar): unify popup menus --- Cargo.lock | 50 +++++ Cargo.toml | 1 + src/app/mod.rs | 6 + src/app/update/mod.rs | 16 +- src/app/view/mod.rs | 123 ++--------- src/ui/popup/isolate_popup.rs | 66 ++---- src/ui/popup/polar_popup.rs | 56 +---- src/ui/popup/scale_popup.rs | 56 +---- src/ui/popup/selection_filter_popup.rs | 48 +--- src/ui/popup/snap_popup.rs | 67 ++---- src/ui/popup/units_popup.rs | 56 ++--- src/ui/statusbar/mod.rs | 291 +++++++++++++++++-------- src/ui/statusbar/status_menu.rs | 107 +++++++++ src/ui/statusbar/statusbar_menu.rs | 92 ++------ 14 files changed, 478 insertions(+), 557 deletions(-) create mode 100644 src/ui/statusbar/status_menu.rs diff --git a/Cargo.lock b/Cargo.lock index d7a22fa2..fe83fea2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,7 @@ dependencies = [ "getrandom 0.3.4", "glam 0.33.2", "iced", + "iced_aw", "image", "inventory", "js-sys", @@ -2184,6 +2185,22 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "iced_aw" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b7f923642b024c415150b70ef84ee5c11626e3b0af73b954d817d573bcd0a4" +dependencies = [ + "cfg-if", + "chrono", + "iced_core", + "iced_fonts", + "iced_widget", + "num-format", + "num-traits", + "web-time", +] + [[package]] name = "iced_core" version = "0.14.0" @@ -2213,6 +2230,29 @@ dependencies = [ "log", ] +[[package]] +name = "iced_fonts" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214cff7c8499e328774216690e58e315a1a5f8f6fdd1035aed6298e62ffc4c1d" +dependencies = [ + "iced_core", + "iced_fonts_macros", + "iced_widget", +] + +[[package]] +name = "iced_fonts_macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef5125e110cb19cd1910a28298661c98c5d9ab02eef43594968352940e8752e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "ttf-parser", +] + [[package]] name = "iced_futures" version = "0.14.0" @@ -3243,6 +3283,16 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "num-format" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" +dependencies = [ + "arrayvec", + "itoa", +] + [[package]] name = "num-integer" version = "0.1.46" diff --git a/Cargo.toml b/Cargo.toml index 1ed48982..3514b315 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ serde = { version = "1", features = ["derive"] } # the binary self-contained on every platform. lzma-sys = { version = "0.1", features = ["static"], optional = true } iced = { version = "0.14", features = ["image", "svg", "advanced", "canvas", "smol"] } +iced_aw = { version = "0.14.1", features = ["menu"] } bytemuck = { version = "1.25", features = ["derive"] } glam = { version = "0.33", features = ["bytemuck"] } truck-modeling = "0.6" diff --git a/src/app/mod.rs b/src/app/mod.rs index 247cd32d..2eefcad8 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -295,6 +295,9 @@ pub(super) struct OpenCADStudio { isolate_popup_open: bool, /// True while the selection-filter type picker is open. selection_filter_popup_open: bool, + /// Hide a status-menu tooltip after its root is clicked; reset when the + /// pointer leaves the root for the opened menu. + status_menu_tooltip_hidden: bool, /// Clean-screen mode: hide ribbon and side panels for a full canvas. clean_screen: bool, /// Quick Properties: show a compact floating property panel on selection. @@ -2177,6 +2180,8 @@ pub enum Message { OsWindowClosed(window::Id), /// No-op — used as a fallback when a TabEvent has no host mapping. Noop, + /// Suppress menu-root tooltips between clicking the root and leaving it. + StatusMenuTooltipHidden(bool), /// GitHub releases API returned a result. `Some(version)` means a /// newer release exists; we open the update-notice window. UpdateCheckResult(Option), @@ -2501,6 +2506,7 @@ impl OpenCADStudio { units_popup_open: false, isolate_popup_open: false, selection_filter_popup_open: false, + status_menu_tooltip_hidden: false, statusbar_config: crate::ui::statusbar::statusbar_config::StatusBarConfig::default(), last_saved_config: None, otrack_active: None, diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 9a8e832c..d8c82482 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -2098,12 +2098,9 @@ impl OpenCADStudio { Task::none() } Message::TogglePolarPopup => { - self.polar_popup_open ^= true; - if self.polar_popup_open { - // Start the custom field empty each time; picking a preset or - // typing a value is what actually enables polar tracking. - self.polar_custom_input.clear(); - } + // MenuBar owns its open state. Reset only the transient field + // whenever the caret starts a fresh interaction. + self.polar_custom_input.clear(); Task::none() } Message::ClosePolarPopup => { @@ -4169,6 +4166,13 @@ impl OpenCADStudio { } Message::Noop => Task::none(), + Message::StatusMenuTooltipHidden(hidden) => { + self.status_menu_tooltip_hidden = hidden; + if hidden { + self.polar_custom_input.clear(); + } + Task::none() + } // ── Unsaved-changes dialog ──────────────────────────────────── Message::UnsavedDialogCancel => { diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 66673a06..0704495f 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1429,13 +1429,26 @@ impl OpenCADStudio { if let Some(be) = &tab.block_edit { displayed_layouts.push(be.block_name.clone()); } + let status_menu_data = crate::ui::statusbar::StatusMenuData { + layout_names: layout_names.clone(), + polar_custom_input: &self.polar_custom_input, + scale_is_model: is_model, + scale_list: tab.scene.scale_list(), + has_selection: !tab.scene.selected.is_empty(), + selection_types: tab + .scene + .entity_type_names_in_layout() + .into_iter() + .map(|name| name.to_string()) + .collect(), + selection_filter: &tab.scene.selection_filter, + tooltip_hidden: self.status_menu_tooltip_hidden, + }; self.status_bar.view( &self.snapper, - self.snap_popup_open, self.ortho_mode, self.polar_mode, self.polar_increment_deg, - self.polar_popup_open, self.dyn_input, self.snapper.otrack_enabled, displayed_layouts, @@ -1451,7 +1464,6 @@ impl OpenCADStudio { tab.scene.active_viewport.is_some(), self.show_layout_tabs, tab.scene.annotation_scale, - self.scale_popup_open, scale_pill_enabled, tab.scene.document.header.lineweight_display, cursor_coord, @@ -1460,13 +1472,13 @@ impl OpenCADStudio { picking, self.clean_screen, tab.scene.document.header.insertion_units, - self.units_popup_open, tab.scene.is_isolation_active(), tab.scene.transparency_display, self.quick_properties, tab.scene.selection_filter_active(), self.selection_cycling, &self.statusbar_config, + status_menu_data, ) }) .width(Fill) @@ -1485,101 +1497,6 @@ impl OpenCADStudio { .height(Fill); let win = self.win_size; - let sb_pill = crate::ui::wrap_bar::dropdown_bounds; - - let snap_layer: Element<'_, Message> = if self.snap_popup_open { - crate::ui::popup::snap_popup::snap_popup_overlay( - &self.snapper, - sb_pill(crate::ui::statusbar::SB_OSNAP_ID), - win, - ) - } else { - iced::widget::Space::new().width(0).height(0).into() - }; - - let scale_layer: Element<'_, Message> = if self.scale_popup_open { - let is_model = tab.scene.current_layout == "Model"; - crate::ui::popup::scale_popup::scale_popup_overlay( - is_model, - tab.scene.annotation_scale, - tab.scene.first_viewport_scale(), - tab.scene.scale_list(), - sb_pill(crate::ui::statusbar::SB_SCALE_ID), - win, - ) - } else { - iced::widget::Space::new().width(0).height(0).into() - }; - - let polar_layer: Element<'_, Message> = if self.polar_popup_open { - crate::ui::popup::polar_popup::polar_popup_overlay( - self.polar_increment_deg, - &self.polar_custom_input, - sb_pill(crate::ui::statusbar::SB_POLAR_ID), - win, - ) - } else { - iced::widget::Space::new().width(0).height(0).into() - }; - - let statusbar_menu_layer: Element<'_, Message> = if self.statusbar_menu_open { - crate::ui::statusbar::statusbar_menu::statusbar_menu_overlay( - &self.statusbar_config, - sb_pill(crate::ui::statusbar::SB_MENU_ID), - win, - ) - } else { - iced::widget::Space::new().width(0).height(0).into() - }; - - let layout_list_layer: Element<'_, Message> = if self.layout_list_open && !tab.is_start { - crate::ui::statusbar::statusbar_menu::layout_list_overlay( - &tab.scene.layout_names(), - &tab.scene.current_layout, - sb_pill(crate::ui::statusbar::SB_LAYOUTLIST_ID), - win, - ) - } else { - iced::widget::Space::new().width(0).height(0).into() - }; - - let units_layer: Element<'_, Message> = if self.units_popup_open { - crate::ui::popup::units_popup::units_popup_overlay( - tab.scene.document.header.insertion_units, - sb_pill(crate::ui::statusbar::SB_UNITS_ID), - win, - ) - } else { - iced::widget::Space::new().width(0).height(0).into() - }; - - let isolate_layer: Element<'_, Message> = if self.isolate_popup_open { - crate::ui::popup::isolate_popup::isolate_popup_overlay( - !tab.scene.selected.is_empty(), - tab.scene.is_isolation_active(), - sb_pill(crate::ui::statusbar::SB_ISOLATE_ID), - win, - ) - } else { - iced::widget::Space::new().width(0).height(0).into() - }; - - let sel_filter_layer: Element<'_, Message> = if self.selection_filter_popup_open { - let types: Vec = tab - .scene - .entity_type_names_in_layout() - .into_iter() - .map(|s| s.to_string()) - .collect(); - crate::ui::popup::selection_filter_popup::selection_filter_popup_overlay( - types, - &tab.scene.selection_filter, - sb_pill(crate::ui::statusbar::SB_FILTER_ID), - win, - ) - } else { - iced::widget::Space::new().width(0).height(0).into() - }; let dropdown_layer: Element<'_, Message> = self .ribbon @@ -1640,14 +1557,6 @@ impl OpenCADStudio { let composed = stack![ main_ui, - snap_layer, - scale_layer, - polar_layer, - statusbar_menu_layer, - layout_list_layer, - units_layer, - isolate_layer, - sel_filter_layer, dropdown_layer, layout_ctx_layer, doc_tab_ctx_layer, diff --git a/src/ui/popup/isolate_popup.rs b/src/ui/popup/isolate_popup.rs index a112dad9..7121addc 100644 --- a/src/ui/popup/isolate_popup.rs +++ b/src/ui/popup/isolate_popup.rs @@ -1,59 +1,43 @@ -//! Isolate action menu — Isolate / Hide / End Isolation. Opened from the -//! ISO status pill, rendered as a floating overlay above the status bar -//! (same pattern as the scale picker). The same actions are also offered -//! in the viewport right-click menu. +//! Isolate / Hide / End Isolation status menu. -use iced::widget::{button, column, container, mouse_area, row, text}; -use iced::{Background, Border, Color, Element, Fill, Length, Rectangle, Theme}; +use iced::widget::{button, row, text}; +use iced::{Background, Color, Element, Fill, Theme}; use crate::app::Message; +use crate::ui::statusbar::status_menu::Entry; -/// Full-screen overlay: transparent click-catcher + action list pinned -/// bottom-right, above the status bar. -/// /// - `has_selection`: enables Isolate / Hide (they act on the selection). /// - `isolation_active`: enables End Isolation (something is hidden). -pub fn isolate_popup_overlay( +pub fn menu_entries( has_selection: bool, isolation_active: bool, - pill: Option, - win: (f32, f32), -) -> Element<'static, Message> { - let rows = column![ - action_row( +) -> Vec> { + vec![ + action_entry( "Isolate Objects", has_selection, Message::Command("ISOLATEOBJECTS".to_string()), ), - action_row( + action_entry( "Hide Objects", has_selection, Message::Command("HIDEOBJECTS".to_string()), ), - action_row( + action_entry( "End Isolation", isolation_active, Message::Command("UNISOLATEOBJECTS".to_string()), ), - ]; + ] +} - let panel = container(rows) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), - border: Border { - color: PANEL_BORDER, - width: 1.0, - radius: 3.0.into(), - }, - ..Default::default() - }) - .width(Length::Fixed(160.0)); - - let positioned = super::position_statusbar_popup(panel.into(), pill, win, 160.0, true); - - mouse_area(positioned) - .on_press(Message::CloseIsolatePopup) - .into() +fn action_entry(label: &'static str, enabled: bool, msg: Message) -> Entry<'static> { + let row = action_row(label, enabled, msg); + if enabled { + Entry::close(row) + } else { + Entry::stay(row) + } } fn action_row(label: &'static str, enabled: bool, msg: Message) -> Element<'static, Message> { @@ -80,18 +64,6 @@ fn action_row(label: &'static str, enabled: bool, msg: Message) -> Element<'stat // ── Colours ─────────────────────────────────────────────────────────────── -const PANEL_BG: Color = Color { - r: 0.15, - g: 0.15, - b: 0.15, - a: 1.0, -}; -const PANEL_BORDER: Color = Color { - r: 0.32, - g: 0.32, - b: 0.32, - a: 1.0, -}; const ROW_HOVER: Color = Color { r: 0.22, g: 0.22, diff --git a/src/ui/popup/polar_popup.rs b/src/ui/popup/polar_popup.rs index 7ec7d193..4393a3f9 100644 --- a/src/ui/popup/polar_popup.rs +++ b/src/ui/popup/polar_popup.rs @@ -1,12 +1,10 @@ -//! Polar-tracking angle picker — sets the polar increment used by POLAR -//! tracking. Rendered as a floating overlay above the status bar, same pattern -//! as the units / scale pickers. Offers the common AutoCAD-style increments -//! plus a free-text field for any custom angle (#264). +//! Polar-tracking angle status menu. -use iced::widget::{button, column, container, mouse_area, row, text, text_input}; -use iced::{Background, Border, Color, Element, Fill, Length, Rectangle, Theme}; +use iced::widget::{button, container, row, text, text_input}; +use iced::{Background, Color, Element, Fill, Length, Theme}; use crate::app::Message; +use crate::ui::statusbar::status_menu::Entry; /// Angle increments offered in the picker, in degrees. Matches the common /// drafting set and adds the fine 1° step requested in #264. @@ -21,20 +19,15 @@ pub fn angle_label(deg: f32) -> String { } } -/// Full-screen overlay: transparent click-catcher + angle list pinned -/// bottom-right, above the status bar. `custom` is the live text of the -/// free-entry field. -pub fn polar_popup_overlay<'a>( +pub fn menu_entries<'a>( current: f32, custom: &'a str, - pill: Option, - win: (f32, f32), -) -> Element<'a, Message> { - let mut rows: Vec> = PRESETS +) -> Vec> { + let mut entries: Vec> = PRESETS .iter() .map(|°| { let active = (current - deg).abs() < 1e-3; - angle_row(deg, active) + Entry::close(angle_row(deg, active)) }) .collect(); @@ -54,25 +47,8 @@ pub fn polar_popup_overlay<'a>( .align_y(iced::Center), ) .padding([5, 10]); - rows.push(custom_row.into()); - - let panel = container(column(rows)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), - border: Border { - color: PANEL_BORDER, - width: 1.0, - radius: 3.0.into(), - }, - ..Default::default() - }) - .width(Length::Fixed(120.0)); - - let positioned = super::position_statusbar_popup(panel.into(), pill, win, 120.0, true); - - mouse_area(positioned) - .on_press(Message::ClosePolarPopup) - .into() + entries.push(Entry::stay(custom_row)); + entries } fn angle_row<'a>(deg: f32, active: bool) -> Element<'a, Message> { @@ -100,18 +76,6 @@ fn angle_row<'a>(deg: f32, active: bool) -> Element<'a, Message> { // ── Colours ─────────────────────────────────────────────────────────────── -const PANEL_BG: Color = Color { - r: 0.15, - g: 0.15, - b: 0.15, - a: 1.0, -}; -const PANEL_BORDER: Color = Color { - r: 0.32, - g: 0.32, - b: 0.32, - a: 1.0, -}; const ROW_HOVER: Color = Color { r: 0.22, g: 0.22, diff --git a/src/ui/popup/scale_popup.rs b/src/ui/popup/scale_popup.rs index b0c6aeb4..d5d31c77 100644 --- a/src/ui/popup/scale_popup.rs +++ b/src/ui/popup/scale_popup.rs @@ -1,29 +1,24 @@ -//! Scale picker dropdown — annotation scale (model space) or viewport scale (paper space). -//! -//! Rendered as a floating overlay above the status bar, same pattern as snap_popup. +//! Annotation / viewport scale status menu. -use iced::widget::{button, column, container, mouse_area, row, text}; -use iced::{Background, Border, Color, Element, Fill, Length, Rectangle, Theme}; +use iced::widget::{button, row, text}; +use iced::{Background, Color, Element, Fill, Theme}; use crate::app::Message; +use crate::ui::statusbar::status_menu::Entry; -/// Full-screen overlay: transparent click-catcher + scale list panel pinned bottom-right. -/// /// - `is_model`: true = model space (dispatches SetAnnotationScale), false = paper space (SetViewportScale). /// - `current_anno_scale`: current annotation_scale from Scene (used to highlight active row in model space). /// - `viewport_scale`: current effective vp scale, view_height-first (used to highlight in paper space). /// - `file_scales`: scale list read from the drawing (`ACAD_SCALELIST`). Only /// scales actually stored in the file are shown; the picker never injects /// scales of its own. -pub fn scale_popup_overlay( +pub fn menu_entries( is_model: bool, current_anno_scale: f32, viewport_scale: Option, file_scales: Vec<(String, f32, f64)>, - pill: Option, - win: (f32, f32), -) -> Element<'static, Message> { - let mut rows: Vec> = file_scales +) -> Vec> { + let mut entries: Vec> = file_scales .into_iter() .map(|(label, anno_scale, vp_scale)| { let active = if is_model { @@ -38,33 +33,14 @@ pub fn scale_popup_overlay( } else { Message::SetViewportScale(vp_scale) }; - scale_row(label, active, msg) + Entry::close(scale_row(label, active, msg)) }) .collect(); - // Model space gets a "Manage scales..." row that opens the scale manager. if is_model { - rows.push(manage_row()); + entries.push(Entry::close(manage_row())); } - - let width = if is_model { 150.0 } else { 120.0 }; - let panel = container(column(rows)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), - border: Border { - color: PANEL_BORDER, - width: 1.0, - radius: 3.0.into(), - }, - ..Default::default() - }) - .width(Length::Fixed(width)); - - let positioned = super::position_statusbar_popup(panel.into(), pill, win, width, true); - - mouse_area(positioned) - .on_press(Message::CloseScalePopup) - .into() + entries } fn scale_row(label: String, active: bool, msg: Message) -> Element<'static, Message> { @@ -107,18 +83,6 @@ fn manage_row() -> Element<'static, Message> { // ── Colours ─────────────────────────────────────────────────────────────── -const PANEL_BG: Color = Color { - r: 0.15, - g: 0.15, - b: 0.15, - a: 1.0, -}; -const PANEL_BORDER: Color = Color { - r: 0.32, - g: 0.32, - b: 0.32, - a: 1.0, -}; const ROW_HOVER: Color = Color { r: 0.22, g: 0.22, diff --git a/src/ui/popup/selection_filter_popup.rs b/src/ui/popup/selection_filter_popup.rs index b86b616c..fdff751c 100644 --- a/src/ui/popup/selection_filter_popup.rs +++ b/src/ui/popup/selection_filter_popup.rs @@ -1,25 +1,21 @@ -//! Selection-filter type picker — choose which entity types are selectable. +//! Selection-filter status menu — choose which entity types are selectable. //! A checked row means that type can be picked; unchecking it excludes the //! type from interactive selection. Opened from the FILTER status pill. use rustc_hash::FxHashSet as HashSet; -use iced::widget::{button, column, container, mouse_area, row, text}; -use iced::{Background, Border, Color, Element, Fill, Length, Rectangle, Theme}; +use iced::widget::{button, container, row, text}; +use iced::{Background, Border, Color, Element, Fill, Theme}; use crate::app::Message; +use crate::ui::statusbar::status_menu::Entry; -/// Full-screen overlay: transparent click-catcher + type list pinned -/// bottom-right, above the status bar. -/// /// - `types`: entity-type names present in the current layout. /// - `excluded`: types currently filtered out (unchecked). -pub fn selection_filter_popup_overlay( +pub fn menu_entries( types: Vec, excluded: &HashSet, - pill: Option, - win: (f32, f32), -) -> Element<'static, Message> { +) -> Vec> { // "Select All / Clear All" header, mirroring the OSNAP popup: Select All // clears every exclusion, Clear All excludes every present type. let has_types = !types.is_empty(); @@ -48,35 +44,21 @@ pub fn selection_filter_popup_overlay( .width(Fill) .padding([0, 4]); - let rows: Vec> = if types.is_empty() { - vec![empty_row()] + let rows: Vec> = if types.is_empty() { + vec![Entry::stay(empty_row())] } else { types .into_iter() .map(|name| { let included = !excluded.contains(&name); - type_row(name, included) + Entry::stay(type_row(name, included)) }) .collect() }; - let panel = container(column![header, divider, column(rows)]) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), - border: Border { - color: PANEL_BORDER, - width: 1.0, - radius: 3.0.into(), - }, - ..Default::default() - }) - .width(Length::Fixed(180.0)); - - let positioned = super::position_statusbar_popup(panel.into(), pill, win, 180.0, true); - - mouse_area(positioned) - .on_press(Message::CloseSelectionFilterPopup) - .into() + let mut entries = vec![Entry::stay(header), Entry::stay(divider)]; + entries.extend(rows); + entries } fn type_row(name: String, included: bool) -> Element<'static, Message> { @@ -134,12 +116,6 @@ fn header_btn(label: &str, msg: Message, enabled: bool) -> Element<'_, Message> // ── Colours ─────────────────────────────────────────────────────────────── -const PANEL_BG: Color = Color { - r: 0.15, - g: 0.15, - b: 0.15, - a: 1.0, -}; const PANEL_BORDER: Color = Color { r: 0.32, g: 0.32, diff --git a/src/ui/popup/snap_popup.rs b/src/ui/popup/snap_popup.rs index 37d7ff3b..8c88ca07 100644 --- a/src/ui/popup/snap_popup.rs +++ b/src/ui/popup/snap_popup.rs @@ -1,27 +1,16 @@ -//! OpenCADStudio-style OSNAP dropdown popup panel. -//! -//! Rendered as a floating overlay above the status bar. The popup is only -//! inserted into the view stack when `snap_popup_open` is true. +//! OpenCADStudio-style OSNAP status menu. -use iced::widget::{button, column, container, mouse_area, row, text}; -use iced::{Background, Border, Color, Element, Fill, Length, Rectangle, Theme}; +use iced::widget::{button, container, row, text}; +use iced::{Background, Border, Color, Element, Fill, Length, Theme}; use crate::app::Message; use crate::snap::{SnapType, Snapper, ALL_SNAP_MODES}; +use crate::ui::statusbar::status_menu::Entry; -/// Returns a full-screen overlay element: -/// - a transparent click-catcher that closes the popup on outside click -/// - the popup panel pinned to the bottom-right (above the status bar) -pub fn snap_popup_overlay<'a>( - snapper: &'a Snapper, - pill: Option, - win: (f32, f32), -) -> Element<'a, Message> { - // ── Panel content ───────────────────────────────────────────────────── +pub fn menu_entries<'a>(snapper: &'a Snapper) -> Vec> { let all_on = snapper.all_on(); let none_on = snapper.none_on(); - // "Select All / Clear All" header row let header = row![ header_btn("Select All", Message::SnapSelectAll, !all_on), header_btn("Clear All", Message::SnapClearAll, !none_on), @@ -38,34 +27,15 @@ pub fn snap_popup_overlay<'a>( .width(Fill) .padding([0, 4]); - // Snap mode rows - let mut rows: Vec> = Vec::new(); + let mut entries = vec![Entry::stay(header), Entry::stay(divider)]; for &(snap_type, _glyph, label) in ALL_SNAP_MODES { - rows.push(snap_row(snap_type, label, snapper.is_on(snap_type))); + entries.push(Entry::stay(snap_row( + snap_type, + label, + snapper.is_on(snap_type), + ))); } - - let panel_content = column![header, divider, column(rows)]; - - const PANEL_W: f32 = 210.0; - let panel = container(panel_content) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), - border: Border { - color: PANEL_BORDER, - width: 1.0, - radius: 3.0.into(), - }, - ..Default::default() - }) - .width(Length::Fixed(PANEL_W)); - - // Anchored just above its pill, flipping horizontally to stay on screen. - let positioned = super::position_statusbar_popup(panel.into(), pill, win, PANEL_W, true); - - // ── Click-catcher: closes popup on any outside click ────────────────── - mouse_area(positioned) - .on_press(Message::CloseSnapPopup) - .into() + entries } // ── Individual snap row ─────────────────────────────────────────────────── @@ -140,12 +110,6 @@ fn header_btn(label: &str, msg: Message, enabled: bool) -> Element<'_, Message> // ── Colours ─────────────────────────────────────────────────────────────── -const PANEL_BG: Color = Color { - r: 0.16, - g: 0.16, - b: 0.16, - a: 0.98, -}; const PANEL_BORDER: Color = Color { r: 0.32, g: 0.32, @@ -171,11 +135,11 @@ const BTN_BG: Color = Color { a: 1.0, }; const CHECK_COLOR: Color = Color { - r: 0.20, + r: 0.35, g: 0.75, - b: 0.35, + b: 1.00, a: 1.0, -}; // green ✓ +}; const ICON_COLOR: Color = Color { r: 0.25, g: 0.75, @@ -194,4 +158,3 @@ const LABEL_OFF: Color = Color { b: 0.52, a: 1.0, }; - diff --git a/src/ui/popup/units_popup.rs b/src/ui/popup/units_popup.rs index 17421569..86765673 100644 --- a/src/ui/popup/units_popup.rs +++ b/src/ui/popup/units_popup.rs @@ -1,10 +1,10 @@ -//! Drawing-units picker — sets the INSUNITS header value. Rendered as a -//! floating overlay above the status bar, same pattern as the scale picker. +//! Drawing-units status menu. -use iced::widget::{button, column, container, mouse_area, row, text}; -use iced::{Background, Border, Color, Element, Fill, Length, Rectangle, Theme}; +use iced::widget::{button, row, text}; +use iced::{Background, Color, Element, Fill, Theme}; use crate::app::Message; +use crate::ui::statusbar::status_menu::Entry; /// Units offered in the picker: (INSUNITS code, menu label). const UNITS: &[(i16, &str)] = &[ @@ -35,35 +35,17 @@ pub fn unit_short(code: i16) -> &'static str { } } -/// Full-screen overlay: transparent click-catcher + units list pinned -/// bottom-right, above the status bar. -pub fn units_popup_overlay( - current: i16, - pill: Option, - win: (f32, f32), -) -> Element<'static, Message> { - let rows: Vec> = UNITS +pub fn menu_entries(current: i16) -> Vec> { + UNITS .iter() - .map(|&(code, label)| unit_row(label, code == current, Message::SetDrawingUnits(code))) - .collect(); - - let panel = container(column(rows)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), - border: Border { - color: PANEL_BORDER, - width: 1.0, - radius: 3.0.into(), - }, - ..Default::default() + .map(|&(code, label)| { + Entry::close(unit_row( + label, + code == current, + Message::SetDrawingUnits(code), + )) }) - .width(Length::Fixed(140.0)); - - let positioned = super::position_statusbar_popup(panel.into(), pill, win, 140.0, true); - - mouse_area(positioned) - .on_press(Message::CloseUnitsPopup) - .into() + .collect() } fn unit_row(label: &'static str, active: bool, msg: Message) -> Element<'static, Message> { @@ -91,18 +73,6 @@ fn unit_row(label: &'static str, active: bool, msg: Message) -> Element<'static, // ── Colours ─────────────────────────────────────────────────────────────── -const PANEL_BG: Color = Color { - r: 0.15, - g: 0.15, - b: 0.15, - a: 1.0, -}; -const PANEL_BORDER: Color = Color { - r: 0.32, - g: 0.32, - b: 0.32, - a: 1.0, -}; const ROW_HOVER: Color = Color { r: 0.22, g: 0.22, diff --git a/src/ui/statusbar/mod.rs b/src/ui/statusbar/mod.rs index e87bcde4..f5891e63 100644 --- a/src/ui/statusbar/mod.rs +++ b/src/ui/statusbar/mod.rs @@ -2,6 +2,7 @@ pub mod statusbar_config; pub mod statusbar_menu; +pub mod status_menu; use iced::widget::tooltip::Position as TipPos; use iced::widget::{ @@ -22,17 +23,19 @@ pub const LAYOUT_RENAME_INPUT_ID: &str = "layout_rename_input"; use crate::app::Message; use crate::snap::Snapper; use crate::ui::statusbar::statusbar_config::{StatusBarConfig, StatusPill}; -use crate::ui::wrap_bar::{PosReport, WrapBar, WrapFlow}; +use crate::ui::statusbar::status_menu::Entry as StatusMenuEntry; +use crate::ui::wrap_bar::{WrapBar, WrapFlow}; -// PosReport ids for anchoring each status-bar popup directly to its pill. -pub const SB_OSNAP_ID: &str = "SB_OSNAP"; -pub const SB_SCALE_ID: &str = "SB_SCALE"; -pub const SB_UNITS_ID: &str = "SB_UNITS"; -pub const SB_ISOLATE_ID: &str = "SB_ISOLATE"; -pub const SB_FILTER_ID: &str = "SB_FILTER"; -pub const SB_MENU_ID: &str = "SB_MENU"; -pub const SB_LAYOUTLIST_ID: &str = "SB_LAYOUTLIST"; -pub const SB_POLAR_ID: &str = "SB_POLAR"; +pub struct StatusMenuData<'a> { + pub layout_names: Vec, + pub polar_custom_input: &'a str, + pub scale_is_model: bool, + pub scale_list: Vec<(String, f32, f64)>, + pub has_selection: bool, + pub selection_types: Vec, + pub selection_filter: &'a rustc_hash::FxHashSet, + pub tooltip_hidden: bool, +} #[derive(Clone, Default)] pub struct StatusBar { @@ -50,12 +53,9 @@ impl StatusBar { pub fn view<'a>( &'a self, snapper: &'a Snapper, - popup_open: bool, ortho_mode: bool, polar_mode: bool, polar_increment_deg: f32, - // True while the polar-angle picker popup is open. - polar_popup_open: bool, dyn_input: bool, otrack: bool, layouts: Vec, @@ -75,8 +75,6 @@ impl StatusBar { show_layout_tabs: bool, // Current annotation scale for model space (1.0 = 1:1, 50.0 = 1:50, etc.). annotation_scale: f32, - // True when the scale picker popup is open. - scale_popup_open: bool, // True when the scale pill is interactive (always model space; paper space only when a viewport is active/selected). scale_pill_enabled: bool, // LWDISPLAY header flag — controls lineweight visibility in the viewport. @@ -94,8 +92,6 @@ impl StatusBar { clean_screen: bool, // Drawing units (INSUNITS) for the units pill. insertion_units: i16, - // True while the drawing-units picker is open. - units_popup_open: bool, // True when objects are hidden by Isolate / Hide. isolation_active: bool, // Whether entity transparency is shown (Transparency pill state). @@ -108,7 +104,19 @@ impl StatusBar { selection_cycling: bool, // Which pills the user has chosen to show on the bar. config: &'a StatusBarConfig, + menu_data: StatusMenuData<'a>, ) -> Element<'a, Message> { + let StatusMenuData { + layout_names, + polar_custom_input, + scale_is_model, + scale_list, + has_selection, + selection_types, + selection_filter, + tooltip_hidden, + } = menu_data; + // Leftmost hamburger: opens a dropdown listing Model + every layout, so // a layout can be picked directly even when the tab strip is scrolled. let menu_button = button(crate::ui::icons::tinted( @@ -134,7 +142,17 @@ impl StatusBar { "Open or create a drawing to manage layouts.", ) } else { - menu_button.on_press(Message::ToggleLayoutList).into() + status_menu::menu_bar( + menu_tip( + menu_button + .on_press(Message::StatusMenuTooltipHidden(true)) + .into(), + "Model and layout list", + tooltip_hidden, + ), + statusbar_menu::layout_entries(&layout_names, ¤t_layout), + 200.0, + ) }; let add_button = button(text("+").size(12).color(if is_start { @@ -173,9 +191,19 @@ impl StatusBar { format_scale(viewport_scale) }; let scale_element: Element<'_, Message> = if scale_pill_enabled { - tip( - popup_pill(&scale_label, scale_popup_open, Message::ToggleScalePopup), - "Annotation / Viewport Scale\nClick to change", + status_menu::menu_bar( + menu_tip( + popup_pill(&scale_label), + "Annotation / Viewport Scale\nClick to change", + tooltip_hidden, + ), + crate::ui::popup::scale_popup::menu_entries( + scale_is_model, + annotation_scale, + viewport_scale, + scale_list, + ), + if scale_is_model { 150.0 } else { 120.0 }, ) } else { status_pill(scale_label).into() @@ -189,7 +217,7 @@ impl StatusBar { let coords_label = format_coords(cursor_world, last_point, coords_mode, picking); pills.push( tip( - popup_pill(&coords_label, false, Message::CycleCoordsMode), + action_pill(&coords_label, Message::CycleCoordsMode), "Cursor coordinates ($COORDS)\nClick to cycle: static / live / polar", ) .into(), @@ -215,9 +243,14 @@ impl StatusBar { } if vis(StatusPill::Polar) { pills.push( - PosReport::new( - SB_POLAR_ID, - polar_pill(polar_mode, polar_increment_deg, polar_popup_open), + polar_pill( + polar_mode, + polar_increment_deg, + tooltip_hidden, + crate::ui::popup::polar_popup::menu_entries( + polar_increment_deg, + polar_custom_input, + ), ) .into(), ); @@ -242,9 +275,11 @@ impl StatusBar { } if vis(StatusPill::Osnap) { pills.push( - PosReport::new( - SB_OSNAP_ID, - osnap_btn(osnap_active, snapper.snap_enabled, popup_open), + osnap_btn( + osnap_active, + snapper.snap_enabled, + tooltip_hidden, + crate::ui::popup::snap_popup::menu_entries(snapper), ) .into(), ); @@ -259,20 +294,18 @@ impl StatusBar { ); } if vis(StatusPill::Scale) { - pills.push(PosReport::new(SB_SCALE_ID, scale_element).into()); + pills.push(scale_element); } if vis(StatusPill::Units) { pills.push( - PosReport::new( - SB_UNITS_ID, - tip( - popup_pill( - crate::ui::popup::units_popup::unit_short(insertion_units), - units_popup_open, - Message::ToggleUnitsPopup, - ), + status_menu::menu_bar( + menu_tip( + popup_pill(crate::ui::popup::units_popup::unit_short(insertion_units)), "Drawing Units (INSUNITS)\nClick to change", + tooltip_hidden, ), + crate::ui::popup::units_popup::menu_entries(insertion_units), + 140.0, ) .into(), ); @@ -292,12 +325,21 @@ impl StatusBar { } if vis(StatusPill::Isolate) { pills.push( - PosReport::new( - SB_ISOLATE_ID, - tip( - toggle_pill(crate::ui::icons::ST_ISOLATE, isolation_active, Message::ToggleIsolatePopup), + status_menu::menu_bar( + menu_tip( + toggle_pill( + crate::ui::icons::ST_ISOLATE, + isolation_active, + Message::StatusMenuTooltipHidden(true), + ), "Isolate Objects\nClick for Isolate / Hide / End", + tooltip_hidden, ), + crate::ui::popup::isolate_popup::menu_entries( + has_selection, + isolation_active, + ), + 160.0, ) .into(), ); @@ -313,16 +355,21 @@ impl StatusBar { } if vis(StatusPill::SelFilter) { pills.push( - PosReport::new( - SB_FILTER_ID, - tip( + status_menu::menu_bar( + menu_tip( toggle_pill( crate::ui::icons::ST_FILTER, selection_filter_active, - Message::ToggleSelectionFilterPopup, + Message::StatusMenuTooltipHidden(true), ), "Selection Filtering\nLimit which object types can be picked", + tooltip_hidden, ), + crate::ui::popup::selection_filter_popup::menu_entries( + selection_types, + selection_filter, + ), + 180.0, ) .into(), ); @@ -356,12 +403,14 @@ impl StatusBar { } // Customization handle: opens the pill show/hide menu. pills.push( - PosReport::new( - SB_MENU_ID, - tip( + status_menu::menu_bar( + menu_tip( customize_btn(), "Customization\nShow or hide status-bar items", + tooltip_hidden, ), + statusbar_menu::customization_entries(config), + 200.0, ) .into(), ); @@ -375,7 +424,7 @@ impl StatusBar { // use the remaining space on the final tab row when they fit; otherwise // WrapBar adds another right-aligned row. let mut left: Vec> = Vec::new(); - left.push(PosReport::new(SB_LAYOUTLIST_ID, menu_btn).into()); + left.push(menu_btn); if show_layout_tabs { let reorderable_layouts: Arc<[String]> = reorderable_layouts.into(); for name in layouts { @@ -452,7 +501,7 @@ fn format_coords(cursor: glam::DVec3, last: Option, mode: i16, pick fn customize_btn() -> Element<'static, Message> { button(crate::ui::icons::tinted(crate::ui::icons::MENU, 16.0, ICON_COLOR)) - .on_press(Message::ToggleStatusBarMenu) + .on_press(Message::StatusMenuTooltipHidden(true)) .style(|_: &Theme, status| button::Style { background: Some(Background::Color(match status { button::Status::Hovered => PILL_BG, @@ -474,6 +523,24 @@ fn tip<'a>(content: Element<'a, Message>, label: &'static str) -> Element<'a, Me tip_node(content, text(label).size(11).color(Color::WHITE).into()) } +/// A menu root shows its tooltip until clicked. Moving from the root into the +/// opened menu resets suppression for the next hover without covering the menu. +fn menu_tip<'a>( + content: Element<'a, Message>, + label: &'static str, + hidden: bool, +) -> Element<'a, Message> { + let content = if hidden { + content + } else { + tip(content, label) + }; + + mouse_area(content) + .on_exit(Message::StatusMenuTooltipHidden(false)) + .into() +} + /// Like [`tip`] but the tooltip body is any element — used to embed an SVG /// glyph (e.g. the dropdown caret) instead of a Unicode character that renders /// as tofu on the web. (#138) @@ -553,30 +620,20 @@ fn pill_text_color(active: bool) -> Color { } } -/// Wrap a caller-built, already-click-wired `main` element together with a -/// dropdown caret that emits `caret_msg`. `active` lights the pill; `open` -/// highlights the border while its popup is showing. -fn split_pill( +/// Wrap a caller-built, already-click-wired `main` element together with its +/// menu-bearing dropdown caret. +fn split_pill<'a>( main: Element<'static, Message>, - caret_msg: Message, + caret: Element<'a, Message>, active: bool, - open: bool, -) -> Element<'static, Message> { - let color = pill_text_color(active); +) -> Element<'a, Message> { let bg = if active { SNAP_ON_BG } else { SNAP_OFF_BG }; - let border_color = if open { - ACCENT - } else if active { + let border_color = if active { SNAP_BORDER_ON } else { BORDER_COLOR }; - let caret = mouse_area( - container(crate::ui::icons::arrow_down(9.0, color)).padding([0, 1]), - ) - .on_press(caret_msg); - container(row![main, caret].spacing(3).align_y(iced::Center)) .style(move |_: &Theme| container::Style { background: Some(Background::Color(bg)), @@ -596,7 +653,12 @@ fn split_pill( // Main: left-click toggles polar on/off; right-click cycles the increment. // Caret: opens the angle picker. -fn polar_pill(active: bool, increment_deg: f32, open: bool) -> Element<'static, Message> { +fn polar_pill<'a>( + active: bool, + increment_deg: f32, + tooltip_hidden: bool, + entries: Vec>, +) -> Element<'a, Message> { let angle = crate::ui::popup::polar_popup::angle_label(increment_deg); let tooltip_text = format!( "Polar Tracking ({angle})\nF10 — left-click on/off\nRight-click cycles · ▾ picks angle", @@ -620,9 +682,8 @@ fn polar_pill(active: bool, increment_deg: f32, open: bool) -> Element<'static, ) .on_press(Message::TogglePolar) .on_right_press(Message::SetPolarAngle(next_angle)); - - tooltip( - split_pill(main.into(), Message::TogglePolarPopup, active, open), + let main = tooltip( + main, container(text(tooltip_text).size(11).color(Color::WHITE)) .style(|_: &Theme| container::Style { background: Some(Background::Color(Color { @@ -645,27 +706,62 @@ fn polar_pill(active: bool, increment_deg: f32, open: bool) -> Element<'static, }) .padding([4, 8]), TipPos::Top, - ) - .into() + ); + + let caret = status_menu::menu_bar( + menu_tip( + mouse_area( + container(crate::ui::icons::arrow_down(9.0, color)).padding([4, 7]), + ) + .on_press(Message::StatusMenuTooltipHidden(true)) + .into(), + "Polar angle\nClick to choose", + tooltip_hidden, + ), + entries, + 120.0, + ); + + split_pill(main.into(), caret, active) } // ── OSNAP pill ───────────────────────────────────────────────────────────── // // Main: toggles the global snap on/off. Caret: opens the snap-type dropdown. -fn osnap_btn(active: bool, snap_enabled: bool, open: bool) -> Element<'static, Message> { +fn osnap_btn<'a>( + active: bool, + snap_enabled: bool, + tooltip_hidden: bool, + entries: Vec>, +) -> Element<'a, Message> { let on = active || snap_enabled; - let main = mouse_area(crate::ui::icons::tinted( - crate::ui::icons::ST_OSNAP, - 17.0, - pill_text_color(on), - )) - .on_press(Message::ToggleSnapEnabled); + let main = tip( + mouse_area(crate::ui::icons::tinted( + crate::ui::icons::ST_OSNAP, + 17.0, + pill_text_color(on), + )) + .on_press(Message::ToggleSnapEnabled) + .into(), + "Object Snap: toggle on/off\nF3", + ); - tip( - split_pill(main.into(), Message::ToggleSnapPopup, on, open), - "Object Snap: toggle on/off · ▾ opens the snap list\nF3", - ) + let caret = status_menu::menu_bar( + menu_tip( + mouse_area( + container(crate::ui::icons::arrow_down(9.0, pill_text_color(on))).padding([4, 7]), + ) + .on_press(Message::StatusMenuTooltipHidden(true)) + .into(), + "Object Snap list\nClick to choose snap types", + tooltip_hidden, + ), + entries, + 210.0, + ); + + split_pill(main, caret, on) } // ── Helpers ─────────────────────────────────────────────────────────────── @@ -999,30 +1095,33 @@ const SNAP_OFF_HOVER: Color = Color { // ── Scale popup button ──────────────────────────────────────────────────── -/// A labelled status-bar pill that opens a picker popup on click (units, -/// scale, …). Lit + accent-bordered while its popup is `open`. Shared so the -/// popup-opening pills don't each re-declare the same button chrome. -fn popup_pill(label: &str, open: bool, msg: Message) -> Element<'static, Message> { +/// Shared visual root for labelled status-bar menus (units, scale, …). +fn popup_pill(label: &str) -> Element<'static, Message> { + action_pill(label, Message::StatusMenuTooltipHidden(true)) +} + +fn action_pill(label: &str, msg: Message) -> Element<'static, Message> { let label = label.to_string(); button( text(label) .size(12) - .color(if open { SNAP_BORDER_ON } else { OSNAP_OFF_TEXT }), + .color(OSNAP_OFF_TEXT), ) .on_press(msg) - .style(move |_: &Theme, status| button::Style { - background: Some(Background::Color(match (open, status) { - (true, button::Status::Hovered) => SNAP_ON_HOVER, - (true, _) => SNAP_ON_BG, - (false, button::Status::Hovered) => SNAP_OFF_HOVER, - (false, _) => SNAP_OFF_BG, + .style(|_: &Theme, status| button::Style { + background: Some(Background::Color(match status { + button::Status::Hovered => SNAP_ON_HOVER, + _ => SNAP_OFF_BG, })), border: Border { - color: if open { SNAP_BORDER_ON } else { BORDER_COLOR }, + color: match status { + button::Status::Hovered => SNAP_BORDER_ON, + _ => BORDER_COLOR, + }, width: 1.0, radius: 2.0.into(), }, - text_color: if open { SNAP_BORDER_ON } else { OSNAP_OFF_TEXT }, + text_color: OSNAP_OFF_TEXT, shadow: iced::Shadow::default(), snap: false, }) diff --git a/src/ui/statusbar/status_menu.rs b/src/ui/statusbar/status_menu.rs new file mode 100644 index 00000000..64dafe2f --- /dev/null +++ b/src/ui/statusbar/status_menu.rs @@ -0,0 +1,107 @@ +//! Shared `iced_aw::MenuBar` plumbing for status-bar menus. + +use iced::{Background, Border, Color, Element, Length, Shadow, Theme}; +use iced_aw::menu::{DrawPath, Item, Menu, MenuBar}; + +use crate::app::Message; + +/// One row in a status-bar menu. +pub struct Entry<'a> { + content: Element<'a, Message>, + close_on_click: bool, +} + +impl<'a> Entry<'a> { + /// Keep the menu open after this row is clicked. + pub fn stay(content: impl Into>) -> Self { + Self { + content: content.into(), + close_on_click: false, + } + } + + /// Close the menu after this row is clicked. + pub fn close(content: impl Into>) -> Self { + Self { + content: content.into(), + close_on_click: true, + } + } +} + +/// Attach a menu to `root`. Each status-bar menu uses one root so the existing +/// wrapping layout can still move pills independently between rows. +pub fn menu_bar<'a>( + root: impl Into>, + entries: Vec>, + width: f32, +) -> Element<'a, Message> { + let items = entries + .into_iter() + .map(|entry| Item::new(entry.content).close_on_click(entry.close_on_click)) + .collect(); + let menu = Menu::new(items) + .width(Length::Fixed(width)) + .padding(0) + .spacing(0) + .offset(1.0) + .close_on_background_click(true); + + MenuBar::new(vec![Item::with_menu(root, menu)]) + .safe_bounds_margin(0.0) + .close_on_background_click_global(true) + .draw_path(DrawPath::Backdrop) + .style(|_: &Theme, _| iced_aw::style::menu_bar::Style { + bar_background: Background::Color(Color::TRANSPARENT), + bar_border: Border::default(), + bar_shadow: Shadow::default(), + menu_background: Background::Color(MENU_BG), + menu_border: Border { + color: MENU_BORDER, + width: 1.0, + radius: 3.0.into(), + }, + menu_shadow: Shadow { + color: Color { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.35, + }, + offset: iced::Vector::new(0.0, -2.0), + blur_radius: 6.0, + }, + path: Background::Color(ACTIVE_BG), + path_border: Border { + color: ACTIVE_BORDER, + width: 1.0, + radius: 2.0.into(), + }, + }) + .into() +} + +const MENU_BG: Color = Color { + r: 0.15, + g: 0.15, + b: 0.15, + a: 1.0, +}; +const MENU_BORDER: Color = Color { + r: 0.32, + g: 0.32, + b: 0.32, + a: 1.0, +}; +const ACTIVE_BG: Color = Color { + r: 0.10, + g: 0.20, + b: 0.32, + a: 1.0, +}; +const ACTIVE_BORDER: Color = Color { + r: 0.20, + g: 0.50, + b: 0.85, + a: 1.0, +}; diff --git a/src/ui/statusbar/statusbar_menu.rs b/src/ui/statusbar/statusbar_menu.rs index d078bae0..c20e8b17 100644 --- a/src/ui/statusbar/statusbar_menu.rs +++ b/src/ui/statusbar/statusbar_menu.rs @@ -1,85 +1,33 @@ -//! Status-bar customization menu — a checkmark list of every toggle pill, -//! opened from the bar's far-right handle. Rendered as a floating overlay -//! above the status bar, same pattern as the scale picker. +//! Status-bar customization and layout-list menu entries. -use iced::widget::{button, column, container, mouse_area, row, scrollable, text}; -use iced::{Background, Border, Color, Element, Fill, Length, Rectangle, Theme}; +use iced::widget::{button, row, text}; +use iced::{Background, Color, Element, Fill, Theme}; use crate::app::Message; use crate::ui::statusbar::statusbar_config::{StatusBarConfig, StatusPill}; +use crate::ui::statusbar::status_menu::Entry; -/// Full-screen overlay: transparent click-catcher + the menu panel pinned to -/// the bottom-right, just above the status bar. -pub fn statusbar_menu_overlay( - config: &StatusBarConfig, - pill: Option, - win: (f32, f32), -) -> Element<'static, Message> { - let rows: Vec> = StatusPill::ALL +pub fn customization_entries(config: &StatusBarConfig) -> Vec> { + StatusPill::ALL .iter() .map(|&pill| { - menu_row( + Entry::stay(menu_row( pill.label(), config.is_visible(pill), Message::ToggleStatusPill(pill), - ) + )) }) - .collect(); - - let panel = container(column(rows)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), - border: Border { - color: PANEL_BORDER, - width: 1.0, - radius: 3.0.into(), - }, - ..Default::default() - }) - .width(Length::Fixed(200.0)); - - let positioned = - crate::ui::popup::position_statusbar_popup(panel.into(), pill, win, 200.0, true); - - mouse_area(positioned) - .on_press(Message::CloseStatusBarMenu) - .into() + .collect() } -/// Dropdown listing Model + every paper layout, opened from the leftmost -/// hamburger. Pinned bottom-left just above the status bar; a click selects a -/// layout (and closes), an outside click just closes. -pub fn layout_list_overlay<'a>( +pub fn layout_entries<'a>( layouts: &[String], current: &str, - pill: Option, - win: (f32, f32), -) -> Element<'a, Message> { - let rows: Vec> = layouts +) -> Vec> { + layouts .iter() - .map(|name| layout_row(name.clone(), name == current)) - .collect(); - - let panel = container(scrollable(column(rows))) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), - border: Border { - color: PANEL_BORDER, - width: 1.0, - radius: 3.0.into(), - }, - ..Default::default() - }) - .width(Length::Fixed(200.0)) - .max_height(360.0); - - // Hamburger is on the left → prefer left-aligned (grows right). - let positioned = - crate::ui::popup::position_statusbar_popup(panel.into(), pill, win, 200.0, false); - - mouse_area(positioned) - .on_press(Message::CloseLayoutList) - .into() + .map(|name| Entry::close(layout_row(name.clone(), name == current))) + .collect() } fn layout_row<'a>(name: String, is_current: bool) -> Element<'a, Message> { @@ -131,18 +79,6 @@ fn menu_row(label: &'static str, checked: bool, msg: Message) -> Element<'static // ── Colours ─────────────────────────────────────────────────────────────── -const PANEL_BG: Color = Color { - r: 0.15, - g: 0.15, - b: 0.15, - a: 1.0, -}; -const PANEL_BORDER: Color = Color { - r: 0.32, - g: 0.32, - b: 0.32, - a: 1.0, -}; const ROW_HOVER: Color = Color { r: 0.22, g: 0.22,