refactor: group ui/ style+popup dirs, rename cmd_result driver
- Group the flat src/ui/ dir: move the 9 style editors into ui/style/ (dimstyle, mleaderstyle, mlstyle, plotstyle, textstyle, tablestyle, point_style, style_list, style_manager) and the 6 popups into ui/popup/ (cycle, isolate, scale, selection_filter, snap, units). Call sites repointed to crate::ui::style::/crate::ui::popup::; no compat re-exports. ui/ root drops from 36 to 21 files. - Rename app/cmd_result.rs -> app/command_driver.rs: the file is the StepInput->feed_command driver, not the CmdResult type (which lives in command.rs). Name now matches contents. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
7cb666801f
commit
9cd0479047
24 changed files with 40 additions and 38 deletions
91
src/ui/popup/cycle_popup.rs
Normal file
91
src/ui/popup/cycle_popup.rs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
//! Selection-cycling list box — shown at the cursor when a click lands on
|
||||
//! two or more overlapping objects. Each row names a candidate; clicking it
|
||||
//! adds that object to the current selection. Clicking outside dismisses it.
|
||||
|
||||
use iced::widget::{button, column, container, mouse_area, opaque, row, text, Space};
|
||||
use iced::{Background, Border, Color, Element, Fill, Length, Theme};
|
||||
|
||||
use crate::app::Message;
|
||||
|
||||
/// Full-canvas overlay: the list box anchored at `anchor` (canvas
|
||||
/// coordinates) plus a transparent click-catcher that cancels.
|
||||
pub fn cycle_popup_overlay(
|
||||
anchor: iced::Point,
|
||||
items: Vec<(acadrust::Handle, String)>,
|
||||
) -> Element<'static, Message> {
|
||||
let rows: Vec<Element<'static, Message>> = items
|
||||
.into_iter()
|
||||
.map(|(handle, label)| item_row(handle, label))
|
||||
.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(150.0));
|
||||
|
||||
let positioned = column![
|
||||
Space::new().height(Length::Fixed(anchor.y.max(0.0))),
|
||||
row![
|
||||
Space::new().width(Length::Fixed(anchor.x.max(0.0))),
|
||||
opaque(panel),
|
||||
],
|
||||
]
|
||||
.width(Fill)
|
||||
.height(Fill);
|
||||
|
||||
mouse_area(positioned).on_press(Message::CycleCancel).into()
|
||||
}
|
||||
|
||||
fn item_row(handle: acadrust::Handle, label: String) -> Element<'static, Message> {
|
||||
let content = text(label).size(11).color(LABEL).align_y(iced::Center);
|
||||
let btn = button(content)
|
||||
.on_press(Message::CycleSelect(handle))
|
||||
.style(|_: &Theme, status| button::Style {
|
||||
background: Some(Background::Color(match status {
|
||||
button::Status::Hovered => ROW_HOVER,
|
||||
_ => Color::TRANSPARENT,
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
.width(Fill)
|
||||
.padding([4, 10]);
|
||||
// Highlight the underlying object while the cursor is over this row.
|
||||
mouse_area(btn)
|
||||
.on_enter(Message::CycleHover(Some(handle)))
|
||||
.on_exit(Message::CycleHoverExit(handle))
|
||||
.into()
|
||||
}
|
||||
|
||||
// ── 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.45,
|
||||
b: 0.62,
|
||||
a: 1.0,
|
||||
};
|
||||
const LABEL: Color = Color {
|
||||
r: 0.92,
|
||||
g: 0.92,
|
||||
b: 0.92,
|
||||
a: 1.0,
|
||||
};
|
||||
117
src/ui/popup/isolate_popup.rs
Normal file
117
src/ui/popup/isolate_popup.rs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
//! 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.
|
||||
|
||||
use iced::widget::{button, column, container, mouse_area, row, text};
|
||||
use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme};
|
||||
|
||||
use crate::app::Message;
|
||||
|
||||
/// 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(has_selection: bool, isolation_active: bool) -> Element<'static, Message> {
|
||||
let rows = column![
|
||||
action_row(
|
||||
"Isolate Objects",
|
||||
has_selection,
|
||||
Message::Command("ISOLATEOBJECTS".to_string()),
|
||||
),
|
||||
action_row(
|
||||
"Hide Objects",
|
||||
has_selection,
|
||||
Message::Command("HIDEOBJECTS".to_string()),
|
||||
),
|
||||
action_row(
|
||||
"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 = container(panel)
|
||||
.align_right(Fill)
|
||||
.align_bottom(Fill)
|
||||
.padding(Padding {
|
||||
bottom: 27.0,
|
||||
right: 4.0,
|
||||
top: 0.0,
|
||||
left: 0.0,
|
||||
})
|
||||
.width(Fill)
|
||||
.height(Fill);
|
||||
|
||||
mouse_area(positioned)
|
||||
.on_press(Message::CloseIsolatePopup)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn action_row(label: &'static str, enabled: bool, msg: Message) -> Element<'static, Message> {
|
||||
let lbl = text(label)
|
||||
.size(11)
|
||||
.color(if enabled { LABEL_ON } else { LABEL_OFF });
|
||||
let content = row![lbl].align_y(iced::Center);
|
||||
|
||||
let mut btn = button(content)
|
||||
.style(move |_: &Theme, status| button::Style {
|
||||
background: Some(Background::Color(match (enabled, status) {
|
||||
(true, button::Status::Hovered) => ROW_HOVER,
|
||||
_ => Color::TRANSPARENT,
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
.width(Fill)
|
||||
.padding([4, 12]);
|
||||
if enabled {
|
||||
btn = btn.on_press(msg);
|
||||
}
|
||||
btn.into()
|
||||
}
|
||||
|
||||
// ── 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,
|
||||
b: 0.22,
|
||||
a: 1.0,
|
||||
};
|
||||
const LABEL_ON: Color = Color {
|
||||
r: 0.92,
|
||||
g: 0.92,
|
||||
b: 0.92,
|
||||
a: 1.0,
|
||||
};
|
||||
const LABEL_OFF: Color = Color {
|
||||
r: 0.5,
|
||||
g: 0.5,
|
||||
b: 0.5,
|
||||
a: 1.0,
|
||||
};
|
||||
6
src/ui/popup/mod.rs
Normal file
6
src/ui/popup/mod.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
pub mod cycle_popup;
|
||||
pub mod isolate_popup;
|
||||
pub mod scale_popup;
|
||||
pub mod selection_filter_popup;
|
||||
pub mod snap_popup;
|
||||
pub mod units_popup;
|
||||
132
src/ui/popup/scale_popup.rs
Normal file
132
src/ui/popup/scale_popup.rs
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
//! 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.
|
||||
|
||||
use iced::widget::{button, column, container, mouse_area, row, text};
|
||||
use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme};
|
||||
|
||||
use crate::app::Message;
|
||||
|
||||
/// 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(
|
||||
is_model: bool,
|
||||
current_anno_scale: f32,
|
||||
viewport_scale: Option<f64>,
|
||||
file_scales: Vec<(String, f32, f64)>,
|
||||
) -> Element<'static, Message> {
|
||||
let rows: Vec<Element<'static, Message>> = file_scales
|
||||
.into_iter()
|
||||
.map(|(label, anno_scale, vp_scale)| {
|
||||
let active = if is_model {
|
||||
(current_anno_scale - anno_scale).abs() < 0.001 * current_anno_scale.max(0.001)
|
||||
} else {
|
||||
viewport_scale
|
||||
.map(|vs| (vs - vp_scale).abs() < 0.001 * vp_scale.max(0.001))
|
||||
.unwrap_or(false)
|
||||
};
|
||||
let msg = if is_model {
|
||||
Message::SetAnnotationScale(anno_scale)
|
||||
} else {
|
||||
Message::SetViewportScale(vp_scale)
|
||||
};
|
||||
scale_row(label, active, msg)
|
||||
})
|
||||
.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(120.0));
|
||||
|
||||
let positioned = container(panel)
|
||||
.align_right(Fill)
|
||||
.align_bottom(Fill)
|
||||
.padding(Padding {
|
||||
bottom: 27.0,
|
||||
right: 4.0,
|
||||
top: 0.0,
|
||||
left: 0.0,
|
||||
})
|
||||
.width(Fill)
|
||||
.height(Fill);
|
||||
|
||||
mouse_area(positioned)
|
||||
.on_press(Message::CloseScalePopup)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn scale_row(label: String, active: bool, msg: Message) -> Element<'static, Message> {
|
||||
let check = crate::ui::icons::check_cell(active, CHECK_COLOR);
|
||||
|
||||
let lbl = text(label)
|
||||
.size(11)
|
||||
.color(if active { LABEL_ON } else { LABEL_OFF });
|
||||
|
||||
let content = row![check, lbl].spacing(6).align_y(iced::Center);
|
||||
|
||||
button(content)
|
||||
.on_press(msg)
|
||||
.style(|_: &Theme, status| button::Style {
|
||||
background: Some(Background::Color(match status {
|
||||
button::Status::Hovered => ROW_HOVER,
|
||||
_ => Color::TRANSPARENT,
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
.width(Fill)
|
||||
.padding([4, 10])
|
||||
.into()
|
||||
}
|
||||
|
||||
// ── 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,
|
||||
b: 0.22,
|
||||
a: 1.0,
|
||||
};
|
||||
const CHECK_COLOR: Color = Color {
|
||||
r: 0.35,
|
||||
g: 0.75,
|
||||
b: 1.00,
|
||||
a: 1.0,
|
||||
};
|
||||
const LABEL_ON: Color = Color {
|
||||
r: 0.92,
|
||||
g: 0.92,
|
||||
b: 0.92,
|
||||
a: 1.0,
|
||||
};
|
||||
const LABEL_OFF: Color = Color {
|
||||
r: 0.65,
|
||||
g: 0.65,
|
||||
b: 0.65,
|
||||
a: 1.0,
|
||||
};
|
||||
128
src/ui/popup/selection_filter_popup.rs
Normal file
128
src/ui/popup/selection_filter_popup.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
//! Selection-filter type picker — 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, Padding, Theme};
|
||||
|
||||
use crate::app::Message;
|
||||
|
||||
/// 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(
|
||||
types: Vec<String>,
|
||||
excluded: &HashSet<String>,
|
||||
) -> Element<'static, Message> {
|
||||
let rows: Vec<Element<'static, Message>> = if types.is_empty() {
|
||||
vec![empty_row()]
|
||||
} else {
|
||||
types
|
||||
.into_iter()
|
||||
.map(|name| {
|
||||
let included = !excluded.contains(&name);
|
||||
type_row(name, included)
|
||||
})
|
||||
.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(160.0));
|
||||
|
||||
let positioned = container(panel)
|
||||
.align_right(Fill)
|
||||
.align_bottom(Fill)
|
||||
.padding(Padding {
|
||||
bottom: 27.0,
|
||||
right: 4.0,
|
||||
top: 0.0,
|
||||
left: 0.0,
|
||||
})
|
||||
.width(Fill)
|
||||
.height(Fill);
|
||||
|
||||
mouse_area(positioned)
|
||||
.on_press(Message::CloseSelectionFilterPopup)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn type_row(name: String, included: bool) -> Element<'static, Message> {
|
||||
let check = crate::ui::icons::check_cell(included, CHECK_COLOR);
|
||||
|
||||
let lbl = text(name.clone())
|
||||
.size(11)
|
||||
.color(if included { LABEL_ON } else { LABEL_OFF });
|
||||
|
||||
let content = row![check, lbl].spacing(6).align_y(iced::Center);
|
||||
|
||||
button(content)
|
||||
.on_press(Message::ToggleSelectionFilterType(name))
|
||||
.style(|_: &Theme, status| button::Style {
|
||||
background: Some(Background::Color(match status {
|
||||
button::Status::Hovered => ROW_HOVER,
|
||||
_ => Color::TRANSPARENT,
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
.width(Fill)
|
||||
.padding([4, 10])
|
||||
.into()
|
||||
}
|
||||
|
||||
fn empty_row() -> Element<'static, Message> {
|
||||
container(text("No objects").size(11).color(LABEL_OFF))
|
||||
.padding([4, 10])
|
||||
.into()
|
||||
}
|
||||
|
||||
// ── 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,
|
||||
b: 0.22,
|
||||
a: 1.0,
|
||||
};
|
||||
const CHECK_COLOR: Color = Color {
|
||||
r: 0.35,
|
||||
g: 0.75,
|
||||
b: 1.00,
|
||||
a: 1.0,
|
||||
};
|
||||
const LABEL_ON: Color = Color {
|
||||
r: 0.92,
|
||||
g: 0.92,
|
||||
b: 0.92,
|
||||
a: 1.0,
|
||||
};
|
||||
const LABEL_OFF: Color = Color {
|
||||
r: 0.6,
|
||||
g: 0.6,
|
||||
b: 0.6,
|
||||
a: 1.0,
|
||||
};
|
||||
228
src/ui/popup/snap_popup.rs
Normal file
228
src/ui/popup/snap_popup.rs
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
//! 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.
|
||||
|
||||
use iced::widget::{button, column, container, mouse_area, row, text};
|
||||
use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme};
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::snap::{SnapType, Snapper, ALL_SNAP_MODES};
|
||||
|
||||
/// 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, right_offset: f32) -> Element<'a, Message> {
|
||||
// ── Panel content ─────────────────────────────────────────────────────
|
||||
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),
|
||||
]
|
||||
.spacing(1)
|
||||
.padding([4u16, 8]);
|
||||
|
||||
// Divider
|
||||
let divider = container(iced::widget::Space::new().height(1))
|
||||
.style(|_: &Theme| container::Style {
|
||||
background: Some(Background::Color(DIVIDER)),
|
||||
..Default::default()
|
||||
})
|
||||
.width(Fill)
|
||||
.padding([0, 4]);
|
||||
|
||||
// Snap mode rows
|
||||
let mut rows: Vec<Element<'_, Message>> = Vec::new();
|
||||
for &(snap_type, _glyph, label) in ALL_SNAP_MODES {
|
||||
rows.push(snap_row(snap_type, label, snapper.is_on(snap_type)));
|
||||
}
|
||||
|
||||
// "Object Snap Settings…" footer
|
||||
let footer_divider = container(iced::widget::Space::new().height(1))
|
||||
.style(|_: &Theme| container::Style {
|
||||
background: Some(Background::Color(DIVIDER)),
|
||||
..Default::default()
|
||||
})
|
||||
.width(Fill)
|
||||
.padding([0, 4]);
|
||||
|
||||
let footer = button(text("Object Snap Settings…").size(11).color(Color {
|
||||
r: 0.75,
|
||||
g: 0.75,
|
||||
b: 0.75,
|
||||
a: 1.0,
|
||||
}))
|
||||
.on_press(Message::Command("DSETTINGS".into()))
|
||||
.style(|_: &Theme, status| button::Style {
|
||||
background: Some(Background::Color(match status {
|
||||
button::Status::Hovered => ROW_HOVER,
|
||||
_ => Color::TRANSPARENT,
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
.width(Fill)
|
||||
.padding([5, 12]);
|
||||
|
||||
let panel_content = column![header, divider, column(rows), footer_divider, footer,];
|
||||
|
||||
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(210.0));
|
||||
|
||||
// ── Position: bottom-right, above the status bar (26 px) ─────────────
|
||||
let positioned = container(panel)
|
||||
.align_right(Fill)
|
||||
.align_bottom(Fill)
|
||||
.padding(Padding {
|
||||
bottom: 27.0,
|
||||
right: right_offset,
|
||||
top: 0.0,
|
||||
left: 0.0,
|
||||
})
|
||||
.width(Fill)
|
||||
.height(Fill);
|
||||
|
||||
// ── Click-catcher: closes popup on any outside click ──────────────────
|
||||
mouse_area(positioned)
|
||||
.on_press(Message::CloseSnapPopup)
|
||||
.into()
|
||||
}
|
||||
|
||||
// ── Individual snap row ───────────────────────────────────────────────────
|
||||
|
||||
fn snap_row<'a>(snap_type: SnapType, label: &'a str, active: bool) -> Element<'a, Message> {
|
||||
let checkmark = crate::ui::icons::check_cell(active, CHECK_COLOR);
|
||||
|
||||
// SVG marker (not a Unicode glyph) so the symbols render on the web build,
|
||||
// whose bundled font lacks them and showed tofu boxes. (#138)
|
||||
let icon_el = container(crate::ui::icons::tinted::<Message>(
|
||||
crate::ui::icons::osnap(snap_type),
|
||||
13.0,
|
||||
ICON_COLOR,
|
||||
))
|
||||
.width(Length::Fixed(16.0))
|
||||
.align_x(iced::Center);
|
||||
|
||||
let label_el = text(label)
|
||||
.size(11)
|
||||
.color(if active { LABEL_ON } else { LABEL_OFF });
|
||||
|
||||
let content = row![checkmark, icon_el, label_el]
|
||||
.spacing(4)
|
||||
.align_y(iced::Center);
|
||||
|
||||
button(content)
|
||||
.on_press(Message::ToggleSnap(snap_type))
|
||||
.style(|_: &Theme, status| button::Style {
|
||||
background: Some(Background::Color(match status {
|
||||
button::Status::Hovered => ROW_HOVER,
|
||||
_ => Color::TRANSPARENT,
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
.width(Fill)
|
||||
.padding([3, 8])
|
||||
.into()
|
||||
}
|
||||
|
||||
fn header_btn(label: &str, msg: Message, enabled: bool) -> Element<'_, Message> {
|
||||
let b = button(text(label).size(10).color(if enabled {
|
||||
Color {
|
||||
r: 0.70,
|
||||
g: 0.70,
|
||||
b: 0.70,
|
||||
a: 1.0,
|
||||
}
|
||||
} else {
|
||||
Color {
|
||||
r: 0.38,
|
||||
g: 0.38,
|
||||
b: 0.38,
|
||||
a: 1.0,
|
||||
}
|
||||
}));
|
||||
let b = if enabled { b.on_press(msg) } else { b };
|
||||
b.style(|_: &Theme, status| button::Style {
|
||||
background: Some(Background::Color(match status {
|
||||
button::Status::Hovered => ROW_HOVER,
|
||||
_ => BTN_BG,
|
||||
})),
|
||||
border: Border {
|
||||
color: PANEL_BORDER,
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.padding([3, 8])
|
||||
.into()
|
||||
}
|
||||
|
||||
// ── 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,
|
||||
b: 0.32,
|
||||
a: 1.0,
|
||||
};
|
||||
const DIVIDER: Color = Color {
|
||||
r: 0.28,
|
||||
g: 0.28,
|
||||
b: 0.28,
|
||||
a: 1.0,
|
||||
};
|
||||
const ROW_HOVER: Color = Color {
|
||||
r: 0.24,
|
||||
g: 0.24,
|
||||
b: 0.24,
|
||||
a: 1.0,
|
||||
};
|
||||
const BTN_BG: Color = Color {
|
||||
r: 0.20,
|
||||
g: 0.20,
|
||||
b: 0.20,
|
||||
a: 1.0,
|
||||
};
|
||||
const CHECK_COLOR: Color = Color {
|
||||
r: 0.20,
|
||||
g: 0.75,
|
||||
b: 0.35,
|
||||
a: 1.0,
|
||||
}; // green ✓
|
||||
const ICON_COLOR: Color = Color {
|
||||
r: 0.25,
|
||||
g: 0.75,
|
||||
b: 0.45,
|
||||
a: 1.0,
|
||||
}; // green icon
|
||||
const LABEL_ON: Color = Color {
|
||||
r: 0.92,
|
||||
g: 0.92,
|
||||
b: 0.92,
|
||||
a: 1.0,
|
||||
};
|
||||
const LABEL_OFF: Color = Color {
|
||||
r: 0.52,
|
||||
g: 0.52,
|
||||
b: 0.52,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
135
src/ui/popup/units_popup.rs
Normal file
135
src/ui/popup/units_popup.rs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
//! Drawing-units picker — sets the INSUNITS header value. Rendered as a
|
||||
//! floating overlay above the status bar, same pattern as the scale picker.
|
||||
|
||||
use iced::widget::{button, column, container, mouse_area, row, text};
|
||||
use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme};
|
||||
|
||||
use crate::app::Message;
|
||||
|
||||
/// Units offered in the picker: (INSUNITS code, menu label).
|
||||
const UNITS: &[(i16, &str)] = &[
|
||||
(0, "Unitless"),
|
||||
(4, "Millimeters"),
|
||||
(5, "Centimeters"),
|
||||
(6, "Meters"),
|
||||
(7, "Kilometers"),
|
||||
(1, "Inches"),
|
||||
(2, "Feet"),
|
||||
(3, "Miles"),
|
||||
(10, "Yards"),
|
||||
];
|
||||
|
||||
/// Short label shown on the status-bar pill for an INSUNITS code.
|
||||
pub fn unit_short(code: i16) -> &'static str {
|
||||
match code {
|
||||
1 => "in",
|
||||
2 => "ft",
|
||||
3 => "mi",
|
||||
4 => "mm",
|
||||
5 => "cm",
|
||||
6 => "m",
|
||||
7 => "km",
|
||||
10 => "yd",
|
||||
0 => "Unitless",
|
||||
_ => "Unit",
|
||||
}
|
||||
}
|
||||
|
||||
/// Full-screen overlay: transparent click-catcher + units list pinned
|
||||
/// bottom-right, above the status bar.
|
||||
pub fn units_popup_overlay(current: i16) -> Element<'static, Message> {
|
||||
let rows: Vec<Element<'static, Message>> = 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()
|
||||
})
|
||||
.width(Length::Fixed(140.0));
|
||||
|
||||
let positioned = container(panel)
|
||||
.align_right(Fill)
|
||||
.align_bottom(Fill)
|
||||
.padding(Padding {
|
||||
bottom: 27.0,
|
||||
right: 4.0,
|
||||
top: 0.0,
|
||||
left: 0.0,
|
||||
})
|
||||
.width(Fill)
|
||||
.height(Fill);
|
||||
|
||||
mouse_area(positioned)
|
||||
.on_press(Message::CloseUnitsPopup)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn unit_row(label: &'static str, active: bool, msg: Message) -> Element<'static, Message> {
|
||||
let check = crate::ui::icons::check_cell(active, CHECK_COLOR);
|
||||
|
||||
let lbl = text(label)
|
||||
.size(11)
|
||||
.color(if active { LABEL_ON } else { LABEL_OFF });
|
||||
|
||||
let content = row![check, lbl].spacing(6).align_y(iced::Center);
|
||||
|
||||
button(content)
|
||||
.on_press(msg)
|
||||
.style(|_: &Theme, status| button::Style {
|
||||
background: Some(Background::Color(match status {
|
||||
button::Status::Hovered => ROW_HOVER,
|
||||
_ => Color::TRANSPARENT,
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
.width(Fill)
|
||||
.padding([4, 10])
|
||||
.into()
|
||||
}
|
||||
|
||||
// ── 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,
|
||||
b: 0.22,
|
||||
a: 1.0,
|
||||
};
|
||||
const CHECK_COLOR: Color = Color {
|
||||
r: 0.35,
|
||||
g: 0.75,
|
||||
b: 1.00,
|
||||
a: 1.0,
|
||||
};
|
||||
const LABEL_ON: Color = Color {
|
||||
r: 0.92,
|
||||
g: 0.92,
|
||||
b: 0.92,
|
||||
a: 1.0,
|
||||
};
|
||||
const LABEL_OFF: Color = Color {
|
||||
r: 0.65,
|
||||
g: 0.65,
|
||||
b: 0.65,
|
||||
a: 1.0,
|
||||
};
|
||||
Loading…
Reference in a new issue