forked from 1b-agent/opencadstudio-upstream
refactor(statusbar): unify popup menus
This commit is contained in:
1 parent
d5be78a187
commit
b9de8940bc
14 files changed
+478
-557
No files matched your search
Generated
+50
@@ -29,6 +29,7 @@ dependencies = [
|
|||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
"glam 0.33.2",
|
"glam 0.33.2",
|
||||||
"iced",
|
"iced",
|
||||||
|
"iced_aw",
|
||||||
"image",
|
"image",
|
||||||
"inventory",
|
"inventory",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
@@ -2184,6 +2185,22 @@ dependencies = [
|
|||||||
"thiserror 2.0.18",
|
"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]]
|
[[package]]
|
||||||
name = "iced_core"
|
name = "iced_core"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
@@ -2213,6 +2230,29 @@ dependencies = [
|
|||||||
"log",
|
"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]]
|
[[package]]
|
||||||
name = "iced_futures"
|
name = "iced_futures"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
@@ -3243,6 +3283,16 @@ dependencies = [
|
|||||||
"syn 2.0.119",
|
"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]]
|
[[package]]
|
||||||
name = "num-integer"
|
name = "num-integer"
|
||||||
version = "0.1.46"
|
version = "0.1.46"
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
# the binary self-contained on every platform.
|
# the binary self-contained on every platform.
|
||||||
lzma-sys = { version = "0.1", features = ["static"], optional = true }
|
lzma-sys = { version = "0.1", features = ["static"], optional = true }
|
||||||
iced = { version = "0.14", features = ["image", "svg", "advanced", "canvas", "smol"] }
|
iced = { version = "0.14", features = ["image", "svg", "advanced", "canvas", "smol"] }
|
||||||
|
iced_aw = { version = "0.14.1", features = ["menu"] }
|
||||||
bytemuck = { version = "1.25", features = ["derive"] }
|
bytemuck = { version = "1.25", features = ["derive"] }
|
||||||
glam = { version = "0.33", features = ["bytemuck"] }
|
glam = { version = "0.33", features = ["bytemuck"] }
|
||||||
truck-modeling = "0.6"
|
truck-modeling = "0.6"
|
||||||
|
|||||||
@@ -295,6 +295,9 @@ pub(super) struct OpenCADStudio {
|
|||||||
isolate_popup_open: bool,
|
isolate_popup_open: bool,
|
||||||
/// True while the selection-filter type picker is open.
|
/// True while the selection-filter type picker is open.
|
||||||
selection_filter_popup_open: bool,
|
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 mode: hide ribbon and side panels for a full canvas.
|
||||||
clean_screen: bool,
|
clean_screen: bool,
|
||||||
/// Quick Properties: show a compact floating property panel on selection.
|
/// Quick Properties: show a compact floating property panel on selection.
|
||||||
@@ -2177,6 +2180,8 @@ pub enum Message {
|
|||||||
OsWindowClosed(window::Id),
|
OsWindowClosed(window::Id),
|
||||||
/// No-op — used as a fallback when a TabEvent has no host mapping.
|
/// No-op — used as a fallback when a TabEvent has no host mapping.
|
||||||
Noop,
|
Noop,
|
||||||
|
/// Suppress menu-root tooltips between clicking the root and leaving it.
|
||||||
|
StatusMenuTooltipHidden(bool),
|
||||||
/// GitHub releases API returned a result. `Some(version)` means a
|
/// GitHub releases API returned a result. `Some(version)` means a
|
||||||
/// newer release exists; we open the update-notice window.
|
/// newer release exists; we open the update-notice window.
|
||||||
UpdateCheckResult(Option<crate::io::update_check::UpdateInfo>),
|
UpdateCheckResult(Option<crate::io::update_check::UpdateInfo>),
|
||||||
@@ -2501,6 +2506,7 @@ impl OpenCADStudio {
|
|||||||
units_popup_open: false,
|
units_popup_open: false,
|
||||||
isolate_popup_open: false,
|
isolate_popup_open: false,
|
||||||
selection_filter_popup_open: false,
|
selection_filter_popup_open: false,
|
||||||
|
status_menu_tooltip_hidden: false,
|
||||||
statusbar_config: crate::ui::statusbar::statusbar_config::StatusBarConfig::default(),
|
statusbar_config: crate::ui::statusbar::statusbar_config::StatusBarConfig::default(),
|
||||||
last_saved_config: None,
|
last_saved_config: None,
|
||||||
otrack_active: None,
|
otrack_active: None,
|
||||||
|
|||||||
+10
-6
@@ -2098,12 +2098,9 @@ impl OpenCADStudio {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::TogglePolarPopup => {
|
Message::TogglePolarPopup => {
|
||||||
self.polar_popup_open ^= true;
|
// MenuBar owns its open state. Reset only the transient field
|
||||||
if self.polar_popup_open {
|
// whenever the caret starts a fresh interaction.
|
||||||
// Start the custom field empty each time; picking a preset or
|
self.polar_custom_input.clear();
|
||||||
// typing a value is what actually enables polar tracking.
|
|
||||||
self.polar_custom_input.clear();
|
|
||||||
}
|
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::ClosePolarPopup => {
|
Message::ClosePolarPopup => {
|
||||||
@@ -4169,6 +4166,13 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Message::Noop => Task::none(),
|
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 ────────────────────────────────────
|
// ── Unsaved-changes dialog ────────────────────────────────────
|
||||||
Message::UnsavedDialogCancel => {
|
Message::UnsavedDialogCancel => {
|
||||||
|
|||||||
+16
-107
@@ -1429,13 +1429,26 @@ impl OpenCADStudio {
|
|||||||
if let Some(be) = &tab.block_edit {
|
if let Some(be) = &tab.block_edit {
|
||||||
displayed_layouts.push(be.block_name.clone());
|
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.status_bar.view(
|
||||||
&self.snapper,
|
&self.snapper,
|
||||||
self.snap_popup_open,
|
|
||||||
self.ortho_mode,
|
self.ortho_mode,
|
||||||
self.polar_mode,
|
self.polar_mode,
|
||||||
self.polar_increment_deg,
|
self.polar_increment_deg,
|
||||||
self.polar_popup_open,
|
|
||||||
self.dyn_input,
|
self.dyn_input,
|
||||||
self.snapper.otrack_enabled,
|
self.snapper.otrack_enabled,
|
||||||
displayed_layouts,
|
displayed_layouts,
|
||||||
@@ -1451,7 +1464,6 @@ impl OpenCADStudio {
|
|||||||
tab.scene.active_viewport.is_some(),
|
tab.scene.active_viewport.is_some(),
|
||||||
self.show_layout_tabs,
|
self.show_layout_tabs,
|
||||||
tab.scene.annotation_scale,
|
tab.scene.annotation_scale,
|
||||||
self.scale_popup_open,
|
|
||||||
scale_pill_enabled,
|
scale_pill_enabled,
|
||||||
tab.scene.document.header.lineweight_display,
|
tab.scene.document.header.lineweight_display,
|
||||||
cursor_coord,
|
cursor_coord,
|
||||||
@@ -1460,13 +1472,13 @@ impl OpenCADStudio {
|
|||||||
picking,
|
picking,
|
||||||
self.clean_screen,
|
self.clean_screen,
|
||||||
tab.scene.document.header.insertion_units,
|
tab.scene.document.header.insertion_units,
|
||||||
self.units_popup_open,
|
|
||||||
tab.scene.is_isolation_active(),
|
tab.scene.is_isolation_active(),
|
||||||
tab.scene.transparency_display,
|
tab.scene.transparency_display,
|
||||||
self.quick_properties,
|
self.quick_properties,
|
||||||
tab.scene.selection_filter_active(),
|
tab.scene.selection_filter_active(),
|
||||||
self.selection_cycling,
|
self.selection_cycling,
|
||||||
&self.statusbar_config,
|
&self.statusbar_config,
|
||||||
|
status_menu_data,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
@@ -1485,101 +1497,6 @@ impl OpenCADStudio {
|
|||||||
.height(Fill);
|
.height(Fill);
|
||||||
|
|
||||||
let win = self.win_size;
|
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<String> = 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
|
let dropdown_layer: Element<'_, Message> = self
|
||||||
.ribbon
|
.ribbon
|
||||||
@@ -1640,14 +1557,6 @@ impl OpenCADStudio {
|
|||||||
|
|
||||||
let composed = stack![
|
let composed = stack![
|
||||||
main_ui,
|
main_ui,
|
||||||
snap_layer,
|
|
||||||
scale_layer,
|
|
||||||
polar_layer,
|
|
||||||
statusbar_menu_layer,
|
|
||||||
layout_list_layer,
|
|
||||||
units_layer,
|
|
||||||
isolate_layer,
|
|
||||||
sel_filter_layer,
|
|
||||||
dropdown_layer,
|
dropdown_layer,
|
||||||
layout_ctx_layer,
|
layout_ctx_layer,
|
||||||
doc_tab_ctx_layer,
|
doc_tab_ctx_layer,
|
||||||
|
|||||||
@@ -1,59 +1,43 @@
|
|||||||
//! Isolate action menu — Isolate / Hide / End Isolation. Opened from the
|
//! Isolate / Hide / End Isolation status menu.
|
||||||
//! 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.
|
|
||||||
|
|
||||||
use iced::widget::{button, column, container, mouse_area, row, text};
|
use iced::widget::{button, row, text};
|
||||||
use iced::{Background, Border, Color, Element, Fill, Length, Rectangle, Theme};
|
use iced::{Background, Color, Element, Fill, Theme};
|
||||||
|
|
||||||
use crate::app::Message;
|
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).
|
/// - `has_selection`: enables Isolate / Hide (they act on the selection).
|
||||||
/// - `isolation_active`: enables End Isolation (something is hidden).
|
/// - `isolation_active`: enables End Isolation (something is hidden).
|
||||||
pub fn isolate_popup_overlay(
|
pub fn menu_entries(
|
||||||
has_selection: bool,
|
has_selection: bool,
|
||||||
isolation_active: bool,
|
isolation_active: bool,
|
||||||
pill: Option<Rectangle>,
|
) -> Vec<Entry<'static>> {
|
||||||
win: (f32, f32),
|
vec![
|
||||||
) -> Element<'static, Message> {
|
action_entry(
|
||||||
let rows = column![
|
|
||||||
action_row(
|
|
||||||
"Isolate Objects",
|
"Isolate Objects",
|
||||||
has_selection,
|
has_selection,
|
||||||
Message::Command("ISOLATEOBJECTS".to_string()),
|
Message::Command("ISOLATEOBJECTS".to_string()),
|
||||||
),
|
),
|
||||||
action_row(
|
action_entry(
|
||||||
"Hide Objects",
|
"Hide Objects",
|
||||||
has_selection,
|
has_selection,
|
||||||
Message::Command("HIDEOBJECTS".to_string()),
|
Message::Command("HIDEOBJECTS".to_string()),
|
||||||
),
|
),
|
||||||
action_row(
|
action_entry(
|
||||||
"End Isolation",
|
"End Isolation",
|
||||||
isolation_active,
|
isolation_active,
|
||||||
Message::Command("UNISOLATEOBJECTS".to_string()),
|
Message::Command("UNISOLATEOBJECTS".to_string()),
|
||||||
),
|
),
|
||||||
];
|
]
|
||||||
|
}
|
||||||
|
|
||||||
let panel = container(rows)
|
fn action_entry(label: &'static str, enabled: bool, msg: Message) -> Entry<'static> {
|
||||||
.style(|_: &Theme| container::Style {
|
let row = action_row(label, enabled, msg);
|
||||||
background: Some(Background::Color(PANEL_BG)),
|
if enabled {
|
||||||
border: Border {
|
Entry::close(row)
|
||||||
color: PANEL_BORDER,
|
} else {
|
||||||
width: 1.0,
|
Entry::stay(row)
|
||||||
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_row(label: &'static str, enabled: bool, msg: Message) -> Element<'static, Message> {
|
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 ───────────────────────────────────────────────────────────────
|
// ── 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 {
|
const ROW_HOVER: Color = Color {
|
||||||
r: 0.22,
|
r: 0.22,
|
||||||
g: 0.22,
|
g: 0.22,
|
||||||
|
|||||||
+10
-46
@@ -1,12 +1,10 @@
|
|||||||
//! Polar-tracking angle picker — sets the polar increment used by POLAR
|
//! Polar-tracking angle status menu.
|
||||||
//! 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).
|
|
||||||
|
|
||||||
use iced::widget::{button, column, container, mouse_area, row, text, text_input};
|
use iced::widget::{button, container, row, text, text_input};
|
||||||
use iced::{Background, Border, Color, Element, Fill, Length, Rectangle, Theme};
|
use iced::{Background, Color, Element, Fill, Length, Theme};
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
|
use crate::ui::statusbar::status_menu::Entry;
|
||||||
|
|
||||||
/// Angle increments offered in the picker, in degrees. Matches the common
|
/// Angle increments offered in the picker, in degrees. Matches the common
|
||||||
/// drafting set and adds the fine 1° step requested in #264.
|
/// 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
|
pub fn menu_entries<'a>(
|
||||||
/// bottom-right, above the status bar. `custom` is the live text of the
|
|
||||||
/// free-entry field.
|
|
||||||
pub fn polar_popup_overlay<'a>(
|
|
||||||
current: f32,
|
current: f32,
|
||||||
custom: &'a str,
|
custom: &'a str,
|
||||||
pill: Option<Rectangle>,
|
) -> Vec<Entry<'a>> {
|
||||||
win: (f32, f32),
|
let mut entries: Vec<Entry<'a>> = PRESETS
|
||||||
) -> Element<'a, Message> {
|
|
||||||
let mut rows: Vec<Element<'a, Message>> = PRESETS
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|°| {
|
.map(|°| {
|
||||||
let active = (current - deg).abs() < 1e-3;
|
let active = (current - deg).abs() < 1e-3;
|
||||||
angle_row(deg, active)
|
Entry::close(angle_row(deg, active))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -54,25 +47,8 @@ pub fn polar_popup_overlay<'a>(
|
|||||||
.align_y(iced::Center),
|
.align_y(iced::Center),
|
||||||
)
|
)
|
||||||
.padding([5, 10]);
|
.padding([5, 10]);
|
||||||
rows.push(custom_row.into());
|
entries.push(Entry::stay(custom_row));
|
||||||
|
entries
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn angle_row<'a>(deg: f32, active: bool) -> Element<'a, Message> {
|
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 ───────────────────────────────────────────────────────────────
|
// ── 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 {
|
const ROW_HOVER: Color = Color {
|
||||||
r: 0.22,
|
r: 0.22,
|
||||||
g: 0.22,
|
g: 0.22,
|
||||||
|
|||||||
+10
-46
@@ -1,29 +1,24 @@
|
|||||||
//! Scale picker dropdown — annotation scale (model space) or viewport scale (paper space).
|
//! Annotation / viewport scale status menu.
|
||||||
//!
|
|
||||||
//! Rendered as a floating overlay above the status bar, same pattern as snap_popup.
|
|
||||||
|
|
||||||
use iced::widget::{button, column, container, mouse_area, row, text};
|
use iced::widget::{button, row, text};
|
||||||
use iced::{Background, Border, Color, Element, Fill, Length, Rectangle, Theme};
|
use iced::{Background, Color, Element, Fill, Theme};
|
||||||
|
|
||||||
use crate::app::Message;
|
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).
|
/// - `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).
|
/// - `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).
|
/// - `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
|
/// - `file_scales`: scale list read from the drawing (`ACAD_SCALELIST`). Only
|
||||||
/// scales actually stored in the file are shown; the picker never injects
|
/// scales actually stored in the file are shown; the picker never injects
|
||||||
/// scales of its own.
|
/// scales of its own.
|
||||||
pub fn scale_popup_overlay(
|
pub fn menu_entries(
|
||||||
is_model: bool,
|
is_model: bool,
|
||||||
current_anno_scale: f32,
|
current_anno_scale: f32,
|
||||||
viewport_scale: Option<f64>,
|
viewport_scale: Option<f64>,
|
||||||
file_scales: Vec<(String, f32, f64)>,
|
file_scales: Vec<(String, f32, f64)>,
|
||||||
pill: Option<Rectangle>,
|
) -> Vec<Entry<'static>> {
|
||||||
win: (f32, f32),
|
let mut entries: Vec<Entry<'static>> = file_scales
|
||||||
) -> Element<'static, Message> {
|
|
||||||
let mut rows: Vec<Element<'static, Message>> = file_scales
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(label, anno_scale, vp_scale)| {
|
.map(|(label, anno_scale, vp_scale)| {
|
||||||
let active = if is_model {
|
let active = if is_model {
|
||||||
@@ -38,33 +33,14 @@ pub fn scale_popup_overlay(
|
|||||||
} else {
|
} else {
|
||||||
Message::SetViewportScale(vp_scale)
|
Message::SetViewportScale(vp_scale)
|
||||||
};
|
};
|
||||||
scale_row(label, active, msg)
|
Entry::close(scale_row(label, active, msg))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Model space gets a "Manage scales..." row that opens the scale manager.
|
|
||||||
if is_model {
|
if is_model {
|
||||||
rows.push(manage_row());
|
entries.push(Entry::close(manage_row()));
|
||||||
}
|
}
|
||||||
|
entries
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scale_row(label: String, active: bool, msg: Message) -> Element<'static, Message> {
|
fn scale_row(label: String, active: bool, msg: Message) -> Element<'static, Message> {
|
||||||
@@ -107,18 +83,6 @@ fn manage_row() -> Element<'static, Message> {
|
|||||||
|
|
||||||
// ── Colours ───────────────────────────────────────────────────────────────
|
// ── 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 {
|
const ROW_HOVER: Color = Color {
|
||||||
r: 0.22,
|
r: 0.22,
|
||||||
g: 0.22,
|
g: 0.22,
|
||||||
|
|||||||
@@ -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
|
//! A checked row means that type can be picked; unchecking it excludes the
|
||||||
//! type from interactive selection. Opened from the FILTER status pill.
|
//! type from interactive selection. Opened from the FILTER status pill.
|
||||||
|
|
||||||
use rustc_hash::FxHashSet as HashSet;
|
use rustc_hash::FxHashSet as HashSet;
|
||||||
|
|
||||||
use iced::widget::{button, column, container, mouse_area, row, text};
|
use iced::widget::{button, container, row, text};
|
||||||
use iced::{Background, Border, Color, Element, Fill, Length, Rectangle, Theme};
|
use iced::{Background, Border, Color, Element, Fill, Theme};
|
||||||
|
|
||||||
use crate::app::Message;
|
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.
|
/// - `types`: entity-type names present in the current layout.
|
||||||
/// - `excluded`: types currently filtered out (unchecked).
|
/// - `excluded`: types currently filtered out (unchecked).
|
||||||
pub fn selection_filter_popup_overlay(
|
pub fn menu_entries(
|
||||||
types: Vec<String>,
|
types: Vec<String>,
|
||||||
excluded: &HashSet<String>,
|
excluded: &HashSet<String>,
|
||||||
pill: Option<Rectangle>,
|
) -> Vec<Entry<'static>> {
|
||||||
win: (f32, f32),
|
|
||||||
) -> Element<'static, Message> {
|
|
||||||
// "Select All / Clear All" header, mirroring the OSNAP popup: Select All
|
// "Select All / Clear All" header, mirroring the OSNAP popup: Select All
|
||||||
// clears every exclusion, Clear All excludes every present type.
|
// clears every exclusion, Clear All excludes every present type.
|
||||||
let has_types = !types.is_empty();
|
let has_types = !types.is_empty();
|
||||||
@@ -48,35 +44,21 @@ pub fn selection_filter_popup_overlay(
|
|||||||
.width(Fill)
|
.width(Fill)
|
||||||
.padding([0, 4]);
|
.padding([0, 4]);
|
||||||
|
|
||||||
let rows: Vec<Element<'static, Message>> = if types.is_empty() {
|
let rows: Vec<Entry<'static>> = if types.is_empty() {
|
||||||
vec![empty_row()]
|
vec![Entry::stay(empty_row())]
|
||||||
} else {
|
} else {
|
||||||
types
|
types
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|name| {
|
.map(|name| {
|
||||||
let included = !excluded.contains(&name);
|
let included = !excluded.contains(&name);
|
||||||
type_row(name, included)
|
Entry::stay(type_row(name, included))
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
let panel = container(column![header, divider, column(rows)])
|
let mut entries = vec![Entry::stay(header), Entry::stay(divider)];
|
||||||
.style(|_: &Theme| container::Style {
|
entries.extend(rows);
|
||||||
background: Some(Background::Color(PANEL_BG)),
|
entries
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn type_row(name: String, included: bool) -> Element<'static, Message> {
|
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 ───────────────────────────────────────────────────────────────
|
// ── Colours ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const PANEL_BG: Color = Color {
|
|
||||||
r: 0.15,
|
|
||||||
g: 0.15,
|
|
||||||
b: 0.15,
|
|
||||||
a: 1.0,
|
|
||||||
};
|
|
||||||
const PANEL_BORDER: Color = Color {
|
const PANEL_BORDER: Color = Color {
|
||||||
r: 0.32,
|
r: 0.32,
|
||||||
g: 0.32,
|
g: 0.32,
|
||||||
|
|||||||
+15
-52
@@ -1,27 +1,16 @@
|
|||||||
//! OpenCADStudio-style OSNAP dropdown popup panel.
|
//! OpenCADStudio-style OSNAP status menu.
|
||||||
//!
|
|
||||||
//! Rendered as a floating overlay above the status bar. The popup is only
|
|
||||||
//! inserted into the view stack when `snap_popup_open` is true.
|
|
||||||
|
|
||||||
use iced::widget::{button, column, container, mouse_area, row, text};
|
use iced::widget::{button, container, row, text};
|
||||||
use iced::{Background, Border, Color, Element, Fill, Length, Rectangle, Theme};
|
use iced::{Background, Border, Color, Element, Fill, Length, Theme};
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::snap::{SnapType, Snapper, ALL_SNAP_MODES};
|
use crate::snap::{SnapType, Snapper, ALL_SNAP_MODES};
|
||||||
|
use crate::ui::statusbar::status_menu::Entry;
|
||||||
|
|
||||||
/// Returns a full-screen overlay element:
|
pub fn menu_entries<'a>(snapper: &'a Snapper) -> Vec<Entry<'a>> {
|
||||||
/// - 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<Rectangle>,
|
|
||||||
win: (f32, f32),
|
|
||||||
) -> Element<'a, Message> {
|
|
||||||
// ── Panel content ─────────────────────────────────────────────────────
|
|
||||||
let all_on = snapper.all_on();
|
let all_on = snapper.all_on();
|
||||||
let none_on = snapper.none_on();
|
let none_on = snapper.none_on();
|
||||||
|
|
||||||
// "Select All / Clear All" header row
|
|
||||||
let header = row![
|
let header = row![
|
||||||
header_btn("Select All", Message::SnapSelectAll, !all_on),
|
header_btn("Select All", Message::SnapSelectAll, !all_on),
|
||||||
header_btn("Clear All", Message::SnapClearAll, !none_on),
|
header_btn("Clear All", Message::SnapClearAll, !none_on),
|
||||||
@@ -38,34 +27,15 @@ pub fn snap_popup_overlay<'a>(
|
|||||||
.width(Fill)
|
.width(Fill)
|
||||||
.padding([0, 4]);
|
.padding([0, 4]);
|
||||||
|
|
||||||
// Snap mode rows
|
let mut entries = vec![Entry::stay(header), Entry::stay(divider)];
|
||||||
let mut rows: Vec<Element<'_, Message>> = Vec::new();
|
|
||||||
for &(snap_type, _glyph, label) in ALL_SNAP_MODES {
|
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),
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
entries
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Individual snap row ───────────────────────────────────────────────────
|
// ── Individual snap row ───────────────────────────────────────────────────
|
||||||
@@ -140,12 +110,6 @@ fn header_btn(label: &str, msg: Message, enabled: bool) -> Element<'_, Message>
|
|||||||
|
|
||||||
// ── Colours ───────────────────────────────────────────────────────────────
|
// ── Colours ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const PANEL_BG: Color = Color {
|
|
||||||
r: 0.16,
|
|
||||||
g: 0.16,
|
|
||||||
b: 0.16,
|
|
||||||
a: 0.98,
|
|
||||||
};
|
|
||||||
const PANEL_BORDER: Color = Color {
|
const PANEL_BORDER: Color = Color {
|
||||||
r: 0.32,
|
r: 0.32,
|
||||||
g: 0.32,
|
g: 0.32,
|
||||||
@@ -171,11 +135,11 @@ const BTN_BG: Color = Color {
|
|||||||
a: 1.0,
|
a: 1.0,
|
||||||
};
|
};
|
||||||
const CHECK_COLOR: Color = Color {
|
const CHECK_COLOR: Color = Color {
|
||||||
r: 0.20,
|
r: 0.35,
|
||||||
g: 0.75,
|
g: 0.75,
|
||||||
b: 0.35,
|
b: 1.00,
|
||||||
a: 1.0,
|
a: 1.0,
|
||||||
}; // green ✓
|
};
|
||||||
const ICON_COLOR: Color = Color {
|
const ICON_COLOR: Color = Color {
|
||||||
r: 0.25,
|
r: 0.25,
|
||||||
g: 0.75,
|
g: 0.75,
|
||||||
@@ -194,4 +158,3 @@ const LABEL_OFF: Color = Color {
|
|||||||
b: 0.52,
|
b: 0.52,
|
||||||
a: 1.0,
|
a: 1.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
+13
-43
@@ -1,10 +1,10 @@
|
|||||||
//! Drawing-units picker — sets the INSUNITS header value. Rendered as a
|
//! Drawing-units status menu.
|
||||||
//! floating overlay above the status bar, same pattern as the scale picker.
|
|
||||||
|
|
||||||
use iced::widget::{button, column, container, mouse_area, row, text};
|
use iced::widget::{button, row, text};
|
||||||
use iced::{Background, Border, Color, Element, Fill, Length, Rectangle, Theme};
|
use iced::{Background, Color, Element, Fill, Theme};
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
|
use crate::ui::statusbar::status_menu::Entry;
|
||||||
|
|
||||||
/// Units offered in the picker: (INSUNITS code, menu label).
|
/// Units offered in the picker: (INSUNITS code, menu label).
|
||||||
const UNITS: &[(i16, &str)] = &[
|
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
|
pub fn menu_entries(current: i16) -> Vec<Entry<'static>> {
|
||||||
/// bottom-right, above the status bar.
|
UNITS
|
||||||
pub fn units_popup_overlay(
|
|
||||||
current: i16,
|
|
||||||
pill: Option<Rectangle>,
|
|
||||||
win: (f32, f32),
|
|
||||||
) -> Element<'static, Message> {
|
|
||||||
let rows: Vec<Element<'static, Message>> = UNITS
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&(code, label)| unit_row(label, code == current, Message::SetDrawingUnits(code)))
|
.map(|&(code, label)| {
|
||||||
.collect();
|
Entry::close(unit_row(
|
||||||
|
label,
|
||||||
let panel = container(column(rows))
|
code == current,
|
||||||
.style(|_: &Theme| container::Style {
|
Message::SetDrawingUnits(code),
|
||||||
background: Some(Background::Color(PANEL_BG)),
|
))
|
||||||
border: Border {
|
|
||||||
color: PANEL_BORDER,
|
|
||||||
width: 1.0,
|
|
||||||
radius: 3.0.into(),
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
})
|
})
|
||||||
.width(Length::Fixed(140.0));
|
.collect()
|
||||||
|
|
||||||
let positioned = super::position_statusbar_popup(panel.into(), pill, win, 140.0, true);
|
|
||||||
|
|
||||||
mouse_area(positioned)
|
|
||||||
.on_press(Message::CloseUnitsPopup)
|
|
||||||
.into()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unit_row(label: &'static str, active: bool, msg: Message) -> Element<'static, Message> {
|
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 ───────────────────────────────────────────────────────────────
|
// ── 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 {
|
const ROW_HOVER: Color = Color {
|
||||||
r: 0.22,
|
r: 0.22,
|
||||||
g: 0.22,
|
g: 0.22,
|
||||||
|
|||||||
+195
-96
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
pub mod statusbar_config;
|
pub mod statusbar_config;
|
||||||
pub mod statusbar_menu;
|
pub mod statusbar_menu;
|
||||||
|
pub mod status_menu;
|
||||||
|
|
||||||
use iced::widget::tooltip::Position as TipPos;
|
use iced::widget::tooltip::Position as TipPos;
|
||||||
use iced::widget::{
|
use iced::widget::{
|
||||||
@@ -22,17 +23,19 @@ pub const LAYOUT_RENAME_INPUT_ID: &str = "layout_rename_input";
|
|||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::snap::Snapper;
|
use crate::snap::Snapper;
|
||||||
use crate::ui::statusbar::statusbar_config::{StatusBarConfig, StatusPill};
|
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 struct StatusMenuData<'a> {
|
||||||
pub const SB_OSNAP_ID: &str = "SB_OSNAP";
|
pub layout_names: Vec<String>,
|
||||||
pub const SB_SCALE_ID: &str = "SB_SCALE";
|
pub polar_custom_input: &'a str,
|
||||||
pub const SB_UNITS_ID: &str = "SB_UNITS";
|
pub scale_is_model: bool,
|
||||||
pub const SB_ISOLATE_ID: &str = "SB_ISOLATE";
|
pub scale_list: Vec<(String, f32, f64)>,
|
||||||
pub const SB_FILTER_ID: &str = "SB_FILTER";
|
pub has_selection: bool,
|
||||||
pub const SB_MENU_ID: &str = "SB_MENU";
|
pub selection_types: Vec<String>,
|
||||||
pub const SB_LAYOUTLIST_ID: &str = "SB_LAYOUTLIST";
|
pub selection_filter: &'a rustc_hash::FxHashSet<String>,
|
||||||
pub const SB_POLAR_ID: &str = "SB_POLAR";
|
pub tooltip_hidden: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct StatusBar {
|
pub struct StatusBar {
|
||||||
@@ -50,12 +53,9 @@ impl StatusBar {
|
|||||||
pub fn view<'a>(
|
pub fn view<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
snapper: &'a Snapper,
|
snapper: &'a Snapper,
|
||||||
popup_open: bool,
|
|
||||||
ortho_mode: bool,
|
ortho_mode: bool,
|
||||||
polar_mode: bool,
|
polar_mode: bool,
|
||||||
polar_increment_deg: f32,
|
polar_increment_deg: f32,
|
||||||
// True while the polar-angle picker popup is open.
|
|
||||||
polar_popup_open: bool,
|
|
||||||
dyn_input: bool,
|
dyn_input: bool,
|
||||||
otrack: bool,
|
otrack: bool,
|
||||||
layouts: Vec<String>,
|
layouts: Vec<String>,
|
||||||
@@ -75,8 +75,6 @@ impl StatusBar {
|
|||||||
show_layout_tabs: bool,
|
show_layout_tabs: bool,
|
||||||
// Current annotation scale for model space (1.0 = 1:1, 50.0 = 1:50, etc.).
|
// Current annotation scale for model space (1.0 = 1:1, 50.0 = 1:50, etc.).
|
||||||
annotation_scale: f32,
|
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).
|
// True when the scale pill is interactive (always model space; paper space only when a viewport is active/selected).
|
||||||
scale_pill_enabled: bool,
|
scale_pill_enabled: bool,
|
||||||
// LWDISPLAY header flag — controls lineweight visibility in the viewport.
|
// LWDISPLAY header flag — controls lineweight visibility in the viewport.
|
||||||
@@ -94,8 +92,6 @@ impl StatusBar {
|
|||||||
clean_screen: bool,
|
clean_screen: bool,
|
||||||
// Drawing units (INSUNITS) for the units pill.
|
// Drawing units (INSUNITS) for the units pill.
|
||||||
insertion_units: i16,
|
insertion_units: i16,
|
||||||
// True while the drawing-units picker is open.
|
|
||||||
units_popup_open: bool,
|
|
||||||
// True when objects are hidden by Isolate / Hide.
|
// True when objects are hidden by Isolate / Hide.
|
||||||
isolation_active: bool,
|
isolation_active: bool,
|
||||||
// Whether entity transparency is shown (Transparency pill state).
|
// Whether entity transparency is shown (Transparency pill state).
|
||||||
@@ -108,7 +104,19 @@ impl StatusBar {
|
|||||||
selection_cycling: bool,
|
selection_cycling: bool,
|
||||||
// Which pills the user has chosen to show on the bar.
|
// Which pills the user has chosen to show on the bar.
|
||||||
config: &'a StatusBarConfig,
|
config: &'a StatusBarConfig,
|
||||||
|
menu_data: StatusMenuData<'a>,
|
||||||
) -> Element<'a, Message> {
|
) -> 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
|
// Leftmost hamburger: opens a dropdown listing Model + every layout, so
|
||||||
// a layout can be picked directly even when the tab strip is scrolled.
|
// a layout can be picked directly even when the tab strip is scrolled.
|
||||||
let menu_button = button(crate::ui::icons::tinted(
|
let menu_button = button(crate::ui::icons::tinted(
|
||||||
@@ -134,7 +142,17 @@ impl StatusBar {
|
|||||||
"Open or create a drawing to manage layouts.",
|
"Open or create a drawing to manage layouts.",
|
||||||
)
|
)
|
||||||
} else {
|
} 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 {
|
let add_button = button(text("+").size(12).color(if is_start {
|
||||||
@@ -173,9 +191,19 @@ impl StatusBar {
|
|||||||
format_scale(viewport_scale)
|
format_scale(viewport_scale)
|
||||||
};
|
};
|
||||||
let scale_element: Element<'_, Message> = if scale_pill_enabled {
|
let scale_element: Element<'_, Message> = if scale_pill_enabled {
|
||||||
tip(
|
status_menu::menu_bar(
|
||||||
popup_pill(&scale_label, scale_popup_open, Message::ToggleScalePopup),
|
menu_tip(
|
||||||
"Annotation / Viewport Scale\nClick to change",
|
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 {
|
} else {
|
||||||
status_pill(scale_label).into()
|
status_pill(scale_label).into()
|
||||||
@@ -189,7 +217,7 @@ impl StatusBar {
|
|||||||
let coords_label = format_coords(cursor_world, last_point, coords_mode, picking);
|
let coords_label = format_coords(cursor_world, last_point, coords_mode, picking);
|
||||||
pills.push(
|
pills.push(
|
||||||
tip(
|
tip(
|
||||||
popup_pill(&coords_label, false, Message::CycleCoordsMode),
|
action_pill(&coords_label, Message::CycleCoordsMode),
|
||||||
"Cursor coordinates ($COORDS)\nClick to cycle: static / live / polar",
|
"Cursor coordinates ($COORDS)\nClick to cycle: static / live / polar",
|
||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
@@ -215,9 +243,14 @@ impl StatusBar {
|
|||||||
}
|
}
|
||||||
if vis(StatusPill::Polar) {
|
if vis(StatusPill::Polar) {
|
||||||
pills.push(
|
pills.push(
|
||||||
PosReport::new(
|
polar_pill(
|
||||||
SB_POLAR_ID,
|
polar_mode,
|
||||||
polar_pill(polar_mode, polar_increment_deg, polar_popup_open),
|
polar_increment_deg,
|
||||||
|
tooltip_hidden,
|
||||||
|
crate::ui::popup::polar_popup::menu_entries(
|
||||||
|
polar_increment_deg,
|
||||||
|
polar_custom_input,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
@@ -242,9 +275,11 @@ impl StatusBar {
|
|||||||
}
|
}
|
||||||
if vis(StatusPill::Osnap) {
|
if vis(StatusPill::Osnap) {
|
||||||
pills.push(
|
pills.push(
|
||||||
PosReport::new(
|
osnap_btn(
|
||||||
SB_OSNAP_ID,
|
osnap_active,
|
||||||
osnap_btn(osnap_active, snapper.snap_enabled, popup_open),
|
snapper.snap_enabled,
|
||||||
|
tooltip_hidden,
|
||||||
|
crate::ui::popup::snap_popup::menu_entries(snapper),
|
||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
@@ -259,20 +294,18 @@ impl StatusBar {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if vis(StatusPill::Scale) {
|
if vis(StatusPill::Scale) {
|
||||||
pills.push(PosReport::new(SB_SCALE_ID, scale_element).into());
|
pills.push(scale_element);
|
||||||
}
|
}
|
||||||
if vis(StatusPill::Units) {
|
if vis(StatusPill::Units) {
|
||||||
pills.push(
|
pills.push(
|
||||||
PosReport::new(
|
status_menu::menu_bar(
|
||||||
SB_UNITS_ID,
|
menu_tip(
|
||||||
tip(
|
popup_pill(crate::ui::popup::units_popup::unit_short(insertion_units)),
|
||||||
popup_pill(
|
|
||||||
crate::ui::popup::units_popup::unit_short(insertion_units),
|
|
||||||
units_popup_open,
|
|
||||||
Message::ToggleUnitsPopup,
|
|
||||||
),
|
|
||||||
"Drawing Units (INSUNITS)\nClick to change",
|
"Drawing Units (INSUNITS)\nClick to change",
|
||||||
|
tooltip_hidden,
|
||||||
),
|
),
|
||||||
|
crate::ui::popup::units_popup::menu_entries(insertion_units),
|
||||||
|
140.0,
|
||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
@@ -292,12 +325,21 @@ impl StatusBar {
|
|||||||
}
|
}
|
||||||
if vis(StatusPill::Isolate) {
|
if vis(StatusPill::Isolate) {
|
||||||
pills.push(
|
pills.push(
|
||||||
PosReport::new(
|
status_menu::menu_bar(
|
||||||
SB_ISOLATE_ID,
|
menu_tip(
|
||||||
tip(
|
toggle_pill(
|
||||||
toggle_pill(crate::ui::icons::ST_ISOLATE, isolation_active, Message::ToggleIsolatePopup),
|
crate::ui::icons::ST_ISOLATE,
|
||||||
|
isolation_active,
|
||||||
|
Message::StatusMenuTooltipHidden(true),
|
||||||
|
),
|
||||||
"Isolate Objects\nClick for Isolate / Hide / End",
|
"Isolate Objects\nClick for Isolate / Hide / End",
|
||||||
|
tooltip_hidden,
|
||||||
),
|
),
|
||||||
|
crate::ui::popup::isolate_popup::menu_entries(
|
||||||
|
has_selection,
|
||||||
|
isolation_active,
|
||||||
|
),
|
||||||
|
160.0,
|
||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
@@ -313,16 +355,21 @@ impl StatusBar {
|
|||||||
}
|
}
|
||||||
if vis(StatusPill::SelFilter) {
|
if vis(StatusPill::SelFilter) {
|
||||||
pills.push(
|
pills.push(
|
||||||
PosReport::new(
|
status_menu::menu_bar(
|
||||||
SB_FILTER_ID,
|
menu_tip(
|
||||||
tip(
|
|
||||||
toggle_pill(
|
toggle_pill(
|
||||||
crate::ui::icons::ST_FILTER,
|
crate::ui::icons::ST_FILTER,
|
||||||
selection_filter_active,
|
selection_filter_active,
|
||||||
Message::ToggleSelectionFilterPopup,
|
Message::StatusMenuTooltipHidden(true),
|
||||||
),
|
),
|
||||||
"Selection Filtering\nLimit which object types can be picked",
|
"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(),
|
.into(),
|
||||||
);
|
);
|
||||||
@@ -356,12 +403,14 @@ impl StatusBar {
|
|||||||
}
|
}
|
||||||
// Customization handle: opens the pill show/hide menu.
|
// Customization handle: opens the pill show/hide menu.
|
||||||
pills.push(
|
pills.push(
|
||||||
PosReport::new(
|
status_menu::menu_bar(
|
||||||
SB_MENU_ID,
|
menu_tip(
|
||||||
tip(
|
|
||||||
customize_btn(),
|
customize_btn(),
|
||||||
"Customization\nShow or hide status-bar items",
|
"Customization\nShow or hide status-bar items",
|
||||||
|
tooltip_hidden,
|
||||||
),
|
),
|
||||||
|
statusbar_menu::customization_entries(config),
|
||||||
|
200.0,
|
||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
@@ -375,7 +424,7 @@ impl StatusBar {
|
|||||||
// use the remaining space on the final tab row when they fit; otherwise
|
// use the remaining space on the final tab row when they fit; otherwise
|
||||||
// WrapBar adds another right-aligned row.
|
// WrapBar adds another right-aligned row.
|
||||||
let mut left: Vec<Element<'_, Message>> = Vec::new();
|
let mut left: Vec<Element<'_, Message>> = Vec::new();
|
||||||
left.push(PosReport::new(SB_LAYOUTLIST_ID, menu_btn).into());
|
left.push(menu_btn);
|
||||||
if show_layout_tabs {
|
if show_layout_tabs {
|
||||||
let reorderable_layouts: Arc<[String]> = reorderable_layouts.into();
|
let reorderable_layouts: Arc<[String]> = reorderable_layouts.into();
|
||||||
for name in layouts {
|
for name in layouts {
|
||||||
@@ -452,7 +501,7 @@ fn format_coords(cursor: glam::DVec3, last: Option<glam::DVec3>, mode: i16, pick
|
|||||||
|
|
||||||
fn customize_btn() -> Element<'static, Message> {
|
fn customize_btn() -> Element<'static, Message> {
|
||||||
button(crate::ui::icons::tinted(crate::ui::icons::MENU, 16.0, ICON_COLOR))
|
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 {
|
.style(|_: &Theme, status| button::Style {
|
||||||
background: Some(Background::Color(match status {
|
background: Some(Background::Color(match status {
|
||||||
button::Status::Hovered => PILL_BG,
|
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())
|
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
|
/// 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
|
/// glyph (e.g. the dropdown caret) instead of a Unicode character that renders
|
||||||
/// as tofu on the web. (#138)
|
/// 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
|
/// Wrap a caller-built, already-click-wired `main` element together with its
|
||||||
/// dropdown caret that emits `caret_msg`. `active` lights the pill; `open`
|
/// menu-bearing dropdown caret.
|
||||||
/// highlights the border while its popup is showing.
|
fn split_pill<'a>(
|
||||||
fn split_pill(
|
|
||||||
main: Element<'static, Message>,
|
main: Element<'static, Message>,
|
||||||
caret_msg: Message,
|
caret: Element<'a, Message>,
|
||||||
active: bool,
|
active: bool,
|
||||||
open: bool,
|
) -> Element<'a, Message> {
|
||||||
) -> Element<'static, Message> {
|
|
||||||
let color = pill_text_color(active);
|
|
||||||
let bg = if active { SNAP_ON_BG } else { SNAP_OFF_BG };
|
let bg = if active { SNAP_ON_BG } else { SNAP_OFF_BG };
|
||||||
let border_color = if open {
|
let border_color = if active {
|
||||||
ACCENT
|
|
||||||
} else if active {
|
|
||||||
SNAP_BORDER_ON
|
SNAP_BORDER_ON
|
||||||
} else {
|
} else {
|
||||||
BORDER_COLOR
|
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))
|
container(row![main, caret].spacing(3).align_y(iced::Center))
|
||||||
.style(move |_: &Theme| container::Style {
|
.style(move |_: &Theme| container::Style {
|
||||||
background: Some(Background::Color(bg)),
|
background: Some(Background::Color(bg)),
|
||||||
@@ -596,7 +653,12 @@ fn split_pill(
|
|||||||
// Main: left-click toggles polar on/off; right-click cycles the increment.
|
// Main: left-click toggles polar on/off; right-click cycles the increment.
|
||||||
// Caret: opens the angle picker.
|
// 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<StatusMenuEntry<'a>>,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
let angle = crate::ui::popup::polar_popup::angle_label(increment_deg);
|
let angle = crate::ui::popup::polar_popup::angle_label(increment_deg);
|
||||||
let tooltip_text = format!(
|
let tooltip_text = format!(
|
||||||
"Polar Tracking ({angle})\nF10 — left-click on/off\nRight-click cycles · ▾ picks angle",
|
"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_press(Message::TogglePolar)
|
||||||
.on_right_press(Message::SetPolarAngle(next_angle));
|
.on_right_press(Message::SetPolarAngle(next_angle));
|
||||||
|
let main = tooltip(
|
||||||
tooltip(
|
main,
|
||||||
split_pill(main.into(), Message::TogglePolarPopup, active, open),
|
|
||||||
container(text(tooltip_text).size(11).color(Color::WHITE))
|
container(text(tooltip_text).size(11).color(Color::WHITE))
|
||||||
.style(|_: &Theme| container::Style {
|
.style(|_: &Theme| container::Style {
|
||||||
background: Some(Background::Color(Color {
|
background: Some(Background::Color(Color {
|
||||||
@@ -645,27 +706,62 @@ fn polar_pill(active: bool, increment_deg: f32, open: bool) -> Element<'static,
|
|||||||
})
|
})
|
||||||
.padding([4, 8]),
|
.padding([4, 8]),
|
||||||
TipPos::Top,
|
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 ─────────────────────────────────────────────────────────────
|
// ── OSNAP pill ─────────────────────────────────────────────────────────────
|
||||||
//
|
//
|
||||||
// Main: toggles the global snap on/off. Caret: opens the snap-type dropdown.
|
// 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<StatusMenuEntry<'a>>,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
let on = active || snap_enabled;
|
let on = active || snap_enabled;
|
||||||
let main = mouse_area(crate::ui::icons::tinted(
|
let main = tip(
|
||||||
crate::ui::icons::ST_OSNAP,
|
mouse_area(crate::ui::icons::tinted(
|
||||||
17.0,
|
crate::ui::icons::ST_OSNAP,
|
||||||
pill_text_color(on),
|
17.0,
|
||||||
))
|
pill_text_color(on),
|
||||||
.on_press(Message::ToggleSnapEnabled);
|
))
|
||||||
|
.on_press(Message::ToggleSnapEnabled)
|
||||||
|
.into(),
|
||||||
|
"Object Snap: toggle on/off\nF3",
|
||||||
|
);
|
||||||
|
|
||||||
tip(
|
let caret = status_menu::menu_bar(
|
||||||
split_pill(main.into(), Message::ToggleSnapPopup, on, open),
|
menu_tip(
|
||||||
"Object Snap: toggle on/off · ▾ opens the snap list\nF3",
|
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 ───────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
@@ -999,30 +1095,33 @@ const SNAP_OFF_HOVER: Color = Color {
|
|||||||
|
|
||||||
// ── Scale popup button ────────────────────────────────────────────────────
|
// ── Scale popup button ────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// A labelled status-bar pill that opens a picker popup on click (units,
|
/// Shared visual root for labelled status-bar menus (units, scale, …).
|
||||||
/// scale, …). Lit + accent-bordered while its popup is `open`. Shared so the
|
fn popup_pill(label: &str) -> Element<'static, Message> {
|
||||||
/// popup-opening pills don't each re-declare the same button chrome.
|
action_pill(label, Message::StatusMenuTooltipHidden(true))
|
||||||
fn popup_pill(label: &str, open: bool, msg: Message) -> Element<'static, Message> {
|
}
|
||||||
|
|
||||||
|
fn action_pill(label: &str, msg: Message) -> Element<'static, Message> {
|
||||||
let label = label.to_string();
|
let label = label.to_string();
|
||||||
button(
|
button(
|
||||||
text(label)
|
text(label)
|
||||||
.size(12)
|
.size(12)
|
||||||
.color(if open { SNAP_BORDER_ON } else { OSNAP_OFF_TEXT }),
|
.color(OSNAP_OFF_TEXT),
|
||||||
)
|
)
|
||||||
.on_press(msg)
|
.on_press(msg)
|
||||||
.style(move |_: &Theme, status| button::Style {
|
.style(|_: &Theme, status| button::Style {
|
||||||
background: Some(Background::Color(match (open, status) {
|
background: Some(Background::Color(match status {
|
||||||
(true, button::Status::Hovered) => SNAP_ON_HOVER,
|
button::Status::Hovered => SNAP_ON_HOVER,
|
||||||
(true, _) => SNAP_ON_BG,
|
_ => SNAP_OFF_BG,
|
||||||
(false, button::Status::Hovered) => SNAP_OFF_HOVER,
|
|
||||||
(false, _) => SNAP_OFF_BG,
|
|
||||||
})),
|
})),
|
||||||
border: Border {
|
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,
|
width: 1.0,
|
||||||
radius: 2.0.into(),
|
radius: 2.0.into(),
|
||||||
},
|
},
|
||||||
text_color: if open { SNAP_BORDER_ON } else { OSNAP_OFF_TEXT },
|
text_color: OSNAP_OFF_TEXT,
|
||||||
shadow: iced::Shadow::default(),
|
shadow: iced::Shadow::default(),
|
||||||
snap: false,
|
snap: false,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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<Element<'a, Message>>) -> Self {
|
||||||
|
Self {
|
||||||
|
content: content.into(),
|
||||||
|
close_on_click: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close the menu after this row is clicked.
|
||||||
|
pub fn close(content: impl Into<Element<'a, Message>>) -> 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<Element<'a, Message>>,
|
||||||
|
entries: Vec<Entry<'a>>,
|
||||||
|
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,
|
||||||
|
};
|
||||||
@@ -1,85 +1,33 @@
|
|||||||
//! Status-bar customization menu — a checkmark list of every toggle pill,
|
//! Status-bar customization and layout-list menu entries.
|
||||||
//! opened from the bar's far-right handle. Rendered as a floating overlay
|
|
||||||
//! above the status bar, same pattern as the scale picker.
|
|
||||||
|
|
||||||
use iced::widget::{button, column, container, mouse_area, row, scrollable, text};
|
use iced::widget::{button, row, text};
|
||||||
use iced::{Background, Border, Color, Element, Fill, Length, Rectangle, Theme};
|
use iced::{Background, Color, Element, Fill, Theme};
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::ui::statusbar::statusbar_config::{StatusBarConfig, StatusPill};
|
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
|
pub fn customization_entries(config: &StatusBarConfig) -> Vec<Entry<'static>> {
|
||||||
/// the bottom-right, just above the status bar.
|
StatusPill::ALL
|
||||||
pub fn statusbar_menu_overlay(
|
|
||||||
config: &StatusBarConfig,
|
|
||||||
pill: Option<Rectangle>,
|
|
||||||
win: (f32, f32),
|
|
||||||
) -> Element<'static, Message> {
|
|
||||||
let rows: Vec<Element<'static, Message>> = StatusPill::ALL
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&pill| {
|
.map(|&pill| {
|
||||||
menu_row(
|
Entry::stay(menu_row(
|
||||||
pill.label(),
|
pill.label(),
|
||||||
config.is_visible(pill),
|
config.is_visible(pill),
|
||||||
Message::ToggleStatusPill(pill),
|
Message::ToggleStatusPill(pill),
|
||||||
)
|
))
|
||||||
})
|
})
|
||||||
.collect();
|
.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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dropdown listing Model + every paper layout, opened from the leftmost
|
pub fn layout_entries<'a>(
|
||||||
/// 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>(
|
|
||||||
layouts: &[String],
|
layouts: &[String],
|
||||||
current: &str,
|
current: &str,
|
||||||
pill: Option<Rectangle>,
|
) -> Vec<Entry<'a>> {
|
||||||
win: (f32, f32),
|
layouts
|
||||||
) -> Element<'a, Message> {
|
|
||||||
let rows: Vec<Element<'a, Message>> = layouts
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|name| layout_row(name.clone(), name == current))
|
.map(|name| Entry::close(layout_row(name.clone(), name == current)))
|
||||||
.collect();
|
.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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout_row<'a>(name: String, is_current: bool) -> Element<'a, Message> {
|
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 ───────────────────────────────────────────────────────────────
|
// ── 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 {
|
const ROW_HOVER: Color = Color {
|
||||||
r: 0.22,
|
r: 0.22,
|
||||||
g: 0.22,
|
g: 0.22,
|
||||||
|
|||||||
Reference in new issue
Block a user