refactor: group ui/ statusbar and window subdirs
Continue tidying the flat ui/ root: - ui/statusbar/: statusbar.rs becomes statusbar/mod.rs (its crate::ui::statusbar::* paths are preserved) and absorbs statusbar_config.rs + statusbar_menu.rs. - ui/window/: the standalone manager/dialog windows move here — about, layout_manager, page_setup, plugin_manager, shortcuts, layers, update_notice, open_progress. The crate::ui::LayerPanel re-export is repointed so callers are unchanged. Call sites repointed to the new paths; no compat re-exports beyond the existing LayerPanel one. ui/ root drops from 21 to 10 flat files with five subdirs (ribbon, style, popup, statusbar, window). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9b8ee94c0c
commit
a502379048
17 changed files with 29 additions and 27 deletions
931
src/ui/statusbar/mod.rs
Normal file
931
src/ui/statusbar/mod.rs
Normal file
|
|
@ -0,0 +1,931 @@
|
|||
//! Bottom status bar — Model/Layout tabs + OSNAP toggle + status info
|
||||
|
||||
pub mod statusbar_config;
|
||||
pub mod statusbar_menu;
|
||||
|
||||
use iced::widget::tooltip::Position as TipPos;
|
||||
use iced::widget::{button, column, container, mouse_area, row, text, text_input, tooltip, Row};
|
||||
use iced::{Background, Border, Color, Element, Length, Theme};
|
||||
|
||||
/// Widget id of the inline layout-rename text input, so the rename can grab
|
||||
/// keyboard focus the moment it opens (issue #86).
|
||||
pub const LAYOUT_RENAME_INPUT_ID: &str = "layout_rename_input";
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::snap::Snapper;
|
||||
use crate::ui::statusbar::statusbar_config::{StatusBarConfig, StatusPill};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct StatusBar {
|
||||
#[allow(dead_code)]
|
||||
pub coord_display: String,
|
||||
}
|
||||
|
||||
impl StatusBar {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
coord_display: "MODEL".into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view<'a>(
|
||||
&'a self,
|
||||
snapper: &'a Snapper,
|
||||
popup_open: bool,
|
||||
ortho_mode: bool,
|
||||
polar_mode: bool,
|
||||
polar_increment_deg: f32,
|
||||
dyn_input: bool,
|
||||
otrack: bool,
|
||||
layouts: Vec<String>,
|
||||
current_layout: String,
|
||||
// If `Some((original, edit_value))`, the named tab shows a text input.
|
||||
rename_state: Option<&'a (String, String)>,
|
||||
// Scale of the first user viewport in the active paper layout.
|
||||
viewport_scale: Option<f64>,
|
||||
// Number of user viewports in the current paper layout (0 = model space).
|
||||
viewport_count: usize,
|
||||
// True when the user is editing inside a paper-space viewport (MSPACE).
|
||||
in_mspace: bool,
|
||||
// Whether the layout tabs (Model/Paper) are visible (LAYOUTTAB).
|
||||
show_layout_tabs: bool,
|
||||
// Current annotation scale for model space (1.0 = 1:1, 50.0 = 1:50, etc.).
|
||||
annotation_scale: f32,
|
||||
// True when the scale picker popup is open.
|
||||
scale_popup_open: bool,
|
||||
// True when the scale pill is interactive (always model space; paper space only when a viewport is active/selected).
|
||||
scale_pill_enabled: bool,
|
||||
// LWDISPLAY header flag — controls lineweight visibility in the viewport.
|
||||
lineweight_display: bool,
|
||||
// Live cursor position in model coordinates, for the coordinate readout.
|
||||
cursor_world: glam::Vec3,
|
||||
// True while clean-screen mode hides the ribbon and side panels.
|
||||
clean_screen: bool,
|
||||
// Drawing units (INSUNITS) for the units pill.
|
||||
insertion_units: i16,
|
||||
// True while the drawing-units picker is open.
|
||||
units_popup_open: bool,
|
||||
// True when objects are hidden by Isolate / Hide.
|
||||
isolation_active: bool,
|
||||
// Whether entity transparency is shown (Transparency pill state).
|
||||
transparency_display: bool,
|
||||
// Whether the Quick Properties floating panel is enabled.
|
||||
quick_properties: bool,
|
||||
// True when the selection filter is excluding at least one type.
|
||||
selection_filter_active: bool,
|
||||
// Whether selection cycling is enabled.
|
||||
selection_cycling: bool,
|
||||
// Which pills the user has chosen to show on the bar.
|
||||
config: &'a StatusBarConfig,
|
||||
) -> Element<'a, Message> {
|
||||
let menu_btn = button(crate::ui::icons::tinted(crate::ui::icons::MENU, 14.0, ICON_COLOR))
|
||||
.on_press(Message::Command("MENU".into()))
|
||||
.style(|_: &Theme, _| button::Style {
|
||||
background: Some(Background::Color(Color::TRANSPARENT)),
|
||||
..Default::default()
|
||||
})
|
||||
.padding([2, 8]);
|
||||
|
||||
let add_btn = button(text("+").size(12).color(ICON_COLOR))
|
||||
.on_press(Message::LayoutCreate)
|
||||
.style(|_: &Theme, _| button::Style {
|
||||
background: Some(Background::Color(Color::TRANSPARENT)),
|
||||
..Default::default()
|
||||
})
|
||||
.padding([3, 7]);
|
||||
|
||||
// ── Right side ────────────────────────────────────────────────────
|
||||
let osnap_active = snapper.is_active();
|
||||
|
||||
let vp_label = if viewport_count > 0 {
|
||||
format!("{} VP", viewport_count)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
// Scale pill: opens the scale picker popup.
|
||||
// Model space: always interactive, shows annotation scale.
|
||||
// Paper space: interactive only when a viewport is active/selected.
|
||||
let scale_label = if current_layout == "Model" {
|
||||
format_scale(Some(1.0 / annotation_scale as f64))
|
||||
} else {
|
||||
format_scale(viewport_scale)
|
||||
};
|
||||
let scale_element: Element<'_, Message> = if scale_pill_enabled {
|
||||
tip(
|
||||
scale_popup_btn(&scale_label, scale_popup_open),
|
||||
"Annotation / Viewport Scale\nClick to change",
|
||||
)
|
||||
} else {
|
||||
status_pill(scale_label).into()
|
||||
};
|
||||
// Build the right-side pills, honouring the user's per-pill visibility.
|
||||
let vis = |p: StatusPill| config.is_visible(p);
|
||||
let mut right_status = Row::new().spacing(2).align_y(iced::Center);
|
||||
if vis(StatusPill::Coords) {
|
||||
right_status = right_status.push(tip(
|
||||
status_pill(format_coords(cursor_world)).into(),
|
||||
"Cursor coordinates (X, Y, Z)",
|
||||
));
|
||||
}
|
||||
if vis(StatusPill::Ortho) {
|
||||
right_status = right_status.push(tip(
|
||||
toggle_pill("ORTHO", ortho_mode, Message::ToggleOrtho),
|
||||
"Orthogonal Mode\nF8",
|
||||
));
|
||||
}
|
||||
if vis(StatusPill::Lwt) {
|
||||
right_status = right_status.push(tip(
|
||||
toggle_pill("LWT", lineweight_display, Message::ToggleLineweightDisplay),
|
||||
"Show Lineweight\nLWDISPLAY",
|
||||
));
|
||||
}
|
||||
if vis(StatusPill::Polar) {
|
||||
right_status = right_status.push(polar_pill(polar_mode, polar_increment_deg));
|
||||
}
|
||||
if vis(StatusPill::Dyn) {
|
||||
right_status = right_status.push(tip(
|
||||
toggle_pill("DYN", dyn_input, Message::ToggleDynInput),
|
||||
"Dynamic Input\nF12",
|
||||
));
|
||||
}
|
||||
if vis(StatusPill::Otrack) {
|
||||
right_status = right_status.push(tip(
|
||||
toggle_pill("OTRACK", otrack, Message::ToggleOTrack),
|
||||
"Object Snap Tracking\nF11",
|
||||
));
|
||||
}
|
||||
if vis(StatusPill::Osnap) {
|
||||
right_status =
|
||||
right_status.push(osnap_btn(osnap_active, snapper.snap_enabled, popup_open));
|
||||
}
|
||||
if vis(StatusPill::Space) {
|
||||
right_status = right_status.push(tip(
|
||||
space_mode_btn(¤t_layout, in_mspace),
|
||||
"PAPER: double-click viewport to enter MSPACE\nMODEL: click to switch to Model Space",
|
||||
));
|
||||
}
|
||||
if vis(StatusPill::Scale) {
|
||||
right_status = right_status.push(scale_element);
|
||||
}
|
||||
if vis(StatusPill::Units) {
|
||||
right_status = right_status.push(tip(
|
||||
units_btn(
|
||||
crate::ui::popup::units_popup::unit_short(insertion_units),
|
||||
units_popup_open,
|
||||
),
|
||||
"Drawing Units (INSUNITS)\nClick to change",
|
||||
));
|
||||
}
|
||||
if vis(StatusPill::Transparency) {
|
||||
right_status = right_status.push(tip(
|
||||
toggle_pill(
|
||||
"TPY",
|
||||
transparency_display,
|
||||
Message::ToggleTransparencyDisplay,
|
||||
),
|
||||
"Show Transparency\nForce opaque when off",
|
||||
));
|
||||
}
|
||||
if vis(StatusPill::Isolate) {
|
||||
right_status = right_status.push(tip(
|
||||
toggle_pill("ISO", isolation_active, Message::ToggleIsolatePopup),
|
||||
"Isolate Objects\nClick for Isolate / Hide / End",
|
||||
));
|
||||
}
|
||||
if vis(StatusPill::QuickProps) {
|
||||
right_status = right_status.push(tip(
|
||||
toggle_pill("QP", quick_properties, Message::ToggleQuickProperties),
|
||||
"Quick Properties\nFloating panel on selection",
|
||||
));
|
||||
}
|
||||
if vis(StatusPill::SelFilter) {
|
||||
right_status = right_status.push(tip(
|
||||
toggle_pill(
|
||||
"FILTER",
|
||||
selection_filter_active,
|
||||
Message::ToggleSelectionFilterPopup,
|
||||
),
|
||||
"Selection Filtering\nLimit which object types can be picked",
|
||||
));
|
||||
}
|
||||
if vis(StatusPill::SelCycle) {
|
||||
right_status = right_status.push(tip(
|
||||
toggle_pill("SC", selection_cycling, Message::ToggleSelectionCycling),
|
||||
"Selection Cycling\nRepeat-click to step through overlapping objects",
|
||||
));
|
||||
}
|
||||
if vis(StatusPill::Vp) && !vp_label.is_empty() {
|
||||
right_status = right_status.push(tip(
|
||||
status_pill(vp_label).into(),
|
||||
"Viewport count in active layout",
|
||||
));
|
||||
}
|
||||
if vis(StatusPill::CleanScreen) {
|
||||
right_status = right_status.push(tip(
|
||||
toggle_pill("CLEAN", clean_screen, Message::ToggleCleanScreen),
|
||||
"Clean Screen\nHide ribbon and panels",
|
||||
));
|
||||
}
|
||||
// Customization handle: opens the pill show/hide menu.
|
||||
right_status = right_status.push(tip(
|
||||
customize_btn(),
|
||||
"Customization\nShow or hide status-bar items",
|
||||
));
|
||||
let right_status = right_status;
|
||||
|
||||
let mut bar = Row::new().align_y(iced::Center).spacing(0);
|
||||
bar = bar.push(menu_btn);
|
||||
if show_layout_tabs {
|
||||
for name in layouts {
|
||||
let is_active = name == current_layout;
|
||||
let renaming = rename_state
|
||||
.filter(|(orig, _)| *orig == name)
|
||||
.map(|(_, edit)| edit.as_str());
|
||||
bar = bar.push(space_tab(name, is_active, renaming));
|
||||
}
|
||||
bar = bar.push(add_btn);
|
||||
}
|
||||
bar = bar.push(iced::widget::Space::new().width(Length::Fill));
|
||||
bar = bar.push(right_status);
|
||||
|
||||
container(bar)
|
||||
.style(|_: &Theme| container::Style {
|
||||
background: Some(Background::Color(BAR_BG)),
|
||||
border: Border {
|
||||
color: BORDER_COLOR,
|
||||
width: 1.0,
|
||||
radius: 0.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.width(Length::Fill)
|
||||
.height(26)
|
||||
.padding([0, 4])
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Coordinate readout ────────────────────────────────────────────────────
|
||||
|
||||
fn format_coords(p: glam::Vec3) -> String {
|
||||
format!("{:.4}, {:.4}, {:.4}", p.x, p.y, p.z)
|
||||
}
|
||||
|
||||
// ── Customization handle ──────────────────────────────────────────────────
|
||||
|
||||
fn customize_btn() -> Element<'static, Message> {
|
||||
button(crate::ui::icons::tinted(crate::ui::icons::MENU, 14.0, ICON_COLOR))
|
||||
.on_press(Message::ToggleStatusBarMenu)
|
||||
.style(|_: &Theme, status| button::Style {
|
||||
background: Some(Background::Color(match status {
|
||||
button::Status::Hovered => PILL_BG,
|
||||
_ => Color::TRANSPARENT,
|
||||
})),
|
||||
border: Border {
|
||||
radius: 3.0.into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.padding([2, 7])
|
||||
.into()
|
||||
}
|
||||
|
||||
// ── Tooltip helper ────────────────────────────────────────────────────────
|
||||
|
||||
fn tip<'a>(content: Element<'a, Message>, label: &'static str) -> Element<'a, Message> {
|
||||
tip_node(content, text(label).size(11).color(Color::WHITE).into())
|
||||
}
|
||||
|
||||
/// Like [`tip`] but the tooltip body is any element — used to embed an SVG
|
||||
/// glyph (e.g. the dropdown caret) instead of a Unicode character that renders
|
||||
/// as tofu on the web. (#138)
|
||||
fn tip_node<'a>(content: Element<'a, Message>, body: Element<'a, Message>) -> Element<'a, Message> {
|
||||
tooltip(
|
||||
content,
|
||||
container(body)
|
||||
.style(|_: &Theme| container::Style {
|
||||
background: Some(Background::Color(Color {
|
||||
r: 0.13,
|
||||
g: 0.13,
|
||||
b: 0.13,
|
||||
a: 0.97,
|
||||
})),
|
||||
border: Border {
|
||||
color: Color {
|
||||
r: 0.35,
|
||||
g: 0.35,
|
||||
b: 0.35,
|
||||
a: 1.0,
|
||||
},
|
||||
width: 1.0,
|
||||
radius: 3.0.into(),
|
||||
},
|
||||
text_color: Some(Color::WHITE),
|
||||
..Default::default()
|
||||
})
|
||||
.padding([4, 8]),
|
||||
TipPos::Top,
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
// ── Simple toggle pill ────────────────────────────────────────────────────
|
||||
|
||||
fn toggle_pill(label: &'static str, active: bool, msg: Message) -> Element<'static, Message> {
|
||||
button(text(label).size(10).color(if active {
|
||||
OSNAP_ON_TEXT
|
||||
} else {
|
||||
OSNAP_OFF_TEXT
|
||||
}))
|
||||
.on_press(msg)
|
||||
.style(move |_: &Theme, status| button::Style {
|
||||
background: Some(Background::Color(match (active, status) {
|
||||
(true, button::Status::Hovered) => SNAP_ON_HOVER,
|
||||
(true, _) => SNAP_ON_BG,
|
||||
(false, button::Status::Hovered) => SNAP_OFF_HOVER,
|
||||
(false, _) => SNAP_OFF_BG,
|
||||
})),
|
||||
border: Border {
|
||||
color: if active { SNAP_BORDER_ON } else { BORDER_COLOR },
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
},
|
||||
text_color: if active {
|
||||
OSNAP_ON_TEXT
|
||||
} else {
|
||||
OSNAP_OFF_TEXT
|
||||
},
|
||||
shadow: iced::Shadow::default(),
|
||||
snap: false,
|
||||
})
|
||||
.padding([2, 6])
|
||||
.into()
|
||||
}
|
||||
|
||||
// ── Polar tracking pill ───────────────────────────────────────────────────
|
||||
//
|
||||
// Left-click toggles polar on/off.
|
||||
// Right-click cycles through common angle increments: 15 → 30 → 45 → 90 → 15 …
|
||||
|
||||
fn polar_pill(active: bool, increment_deg: f32) -> Element<'static, Message> {
|
||||
let label = format!("POLAR {:.0}°", increment_deg);
|
||||
let tooltip_text = format!(
|
||||
"Polar Tracking ({}°)\nF10 — left-click on/off\nRight-click to change angle",
|
||||
increment_deg as u32
|
||||
);
|
||||
|
||||
let bg_color = move |hovered: bool| match (active, hovered) {
|
||||
(true, true) => SNAP_ON_HOVER,
|
||||
(true, false) => SNAP_ON_BG,
|
||||
(false, true) => SNAP_OFF_HOVER,
|
||||
(false, false) => SNAP_OFF_BG,
|
||||
};
|
||||
|
||||
// Cycle to the next common angle on right-click.
|
||||
let next_angle = match increment_deg as u32 {
|
||||
15 => 30.0_f32,
|
||||
30 => 45.0,
|
||||
45 => 90.0,
|
||||
_ => 15.0,
|
||||
};
|
||||
|
||||
let inner = container(text(label).size(10).color(if active {
|
||||
OSNAP_ON_TEXT
|
||||
} else {
|
||||
OSNAP_OFF_TEXT
|
||||
}))
|
||||
.style(move |_: &Theme| container::Style {
|
||||
background: Some(Background::Color(bg_color(false))),
|
||||
border: Border {
|
||||
color: if active { SNAP_BORDER_ON } else { BORDER_COLOR },
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.padding([2, 6]);
|
||||
|
||||
let pill = mouse_area(inner)
|
||||
.on_press(Message::TogglePolar)
|
||||
.on_right_press(Message::SetPolarAngle(next_angle));
|
||||
|
||||
tooltip(
|
||||
pill,
|
||||
container(text(tooltip_text).size(11).color(Color::WHITE))
|
||||
.style(|_: &Theme| container::Style {
|
||||
background: Some(Background::Color(Color {
|
||||
r: 0.13,
|
||||
g: 0.13,
|
||||
b: 0.13,
|
||||
a: 0.95,
|
||||
})),
|
||||
border: Border {
|
||||
color: Color {
|
||||
r: 0.35,
|
||||
g: 0.35,
|
||||
b: 0.35,
|
||||
a: 1.0,
|
||||
},
|
||||
width: 1.0,
|
||||
radius: 3.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.padding([4, 8]),
|
||||
TipPos::Top,
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
// ── OSNAP split button ────────────────────────────────────────────────────
|
||||
//
|
||||
// Left part ("⚡ OSNAP"): toggles the global snap on/off.
|
||||
// Right part ("▾"): opens the snap-type dropdown.
|
||||
|
||||
fn osnap_btn(active: bool, snap_enabled: bool, open: bool) -> Element<'static, Message> {
|
||||
let bg = match (active || snap_enabled, open) {
|
||||
(true, true) => SNAP_ON_HOVER,
|
||||
(true, false) => SNAP_ON_BG,
|
||||
(false, _) => SNAP_OFF_BG,
|
||||
};
|
||||
let border_color = if open {
|
||||
ACCENT
|
||||
} else if active {
|
||||
SNAP_BORDER_ON
|
||||
} else {
|
||||
BORDER_COLOR
|
||||
};
|
||||
let text_color = if active {
|
||||
OSNAP_ON_TEXT
|
||||
} else {
|
||||
OSNAP_OFF_TEXT
|
||||
};
|
||||
|
||||
let left = button(
|
||||
row![
|
||||
crate::ui::icons::tinted(crate::ui::icons::BOLT, 11.0, text_color),
|
||||
text("OSNAP").size(10).color(text_color),
|
||||
]
|
||||
.spacing(3)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.on_press(Message::ToggleSnapEnabled)
|
||||
.style(move |_: &Theme, status| button::Style {
|
||||
background: Some(Background::Color(match status {
|
||||
button::Status::Hovered => {
|
||||
if active || snap_enabled {
|
||||
SNAP_ON_HOVER
|
||||
} else {
|
||||
SNAP_OFF_HOVER
|
||||
}
|
||||
}
|
||||
_ => bg,
|
||||
})),
|
||||
border: Border {
|
||||
color: border_color,
|
||||
width: 1.0,
|
||||
radius: iced::border::Radius {
|
||||
top_left: 2.0,
|
||||
top_right: 0.0,
|
||||
bottom_right: 0.0,
|
||||
bottom_left: 2.0,
|
||||
},
|
||||
},
|
||||
text_color,
|
||||
shadow: iced::Shadow::default(),
|
||||
snap: false,
|
||||
})
|
||||
.padding([2, 6]);
|
||||
|
||||
let right = button(crate::ui::icons::arrow_down(9.0, text_color))
|
||||
.on_press(Message::ToggleSnapPopup)
|
||||
.style(move |_: &Theme, status| button::Style {
|
||||
background: Some(Background::Color(match status {
|
||||
button::Status::Hovered => {
|
||||
if active || snap_enabled {
|
||||
SNAP_ON_HOVER
|
||||
} else {
|
||||
SNAP_OFF_HOVER
|
||||
}
|
||||
}
|
||||
_ => bg,
|
||||
})),
|
||||
border: Border {
|
||||
color: border_color,
|
||||
width: 1.0,
|
||||
radius: iced::border::Radius {
|
||||
top_left: 0.0,
|
||||
top_right: 2.0,
|
||||
bottom_right: 2.0,
|
||||
bottom_left: 0.0,
|
||||
},
|
||||
},
|
||||
text_color,
|
||||
shadow: iced::Shadow::default(),
|
||||
snap: false,
|
||||
})
|
||||
.padding([2, 4]);
|
||||
|
||||
row![
|
||||
tip(left.into(), "Object Snap: toggle on/off\nF3"),
|
||||
tip_node(
|
||||
right.into(),
|
||||
column![
|
||||
text("Object Snap settings").size(11).color(Color::WHITE),
|
||||
row![
|
||||
text("Click").size(11).color(Color::WHITE),
|
||||
crate::ui::icons::arrow_down(9.0, Color::WHITE),
|
||||
text("to open the list").size(11).color(Color::WHITE),
|
||||
]
|
||||
.spacing(3)
|
||||
.align_y(iced::Center),
|
||||
]
|
||||
.into(),
|
||||
),
|
||||
]
|
||||
.spacing(0)
|
||||
.into()
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// A layout tab button.
|
||||
///
|
||||
/// When `rename_edit` is `Some(value)` the tab shows an inline text input
|
||||
/// instead of the normal button. The tab is not renameable when it is the
|
||||
/// "Model" tab (callers simply never pass `Some` for that name).
|
||||
fn space_tab<'a>(
|
||||
label: String,
|
||||
is_active: bool,
|
||||
rename_edit: Option<&'a str>,
|
||||
) -> Element<'a, Message> {
|
||||
let bg = move |is_active: bool, hovered: bool| {
|
||||
if is_active {
|
||||
TAB_ACTIVE
|
||||
} else if hovered {
|
||||
TAB_HOVER
|
||||
} else {
|
||||
Color::TRANSPARENT
|
||||
}
|
||||
};
|
||||
|
||||
let border = Border {
|
||||
color: if is_active {
|
||||
ACCENT
|
||||
} else {
|
||||
Color::TRANSPARENT
|
||||
},
|
||||
width: if is_active { 1.0 } else { 0.0 },
|
||||
radius: 2.0.into(),
|
||||
};
|
||||
|
||||
let text_color = if is_active {
|
||||
Color::WHITE
|
||||
} else {
|
||||
Color {
|
||||
r: 0.65,
|
||||
g: 0.65,
|
||||
b: 0.65,
|
||||
a: 1.0,
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(edit_val) = rename_edit {
|
||||
// Inline rename text input with a cancel (✕) button.
|
||||
let input = text_input("", edit_val)
|
||||
.id(iced::widget::Id::new(LAYOUT_RENAME_INPUT_ID))
|
||||
.on_input(Message::LayoutRenameEdit)
|
||||
.on_submit(Message::LayoutRenameCommit)
|
||||
.size(11)
|
||||
.style(|_: &Theme, _| text_input::Style {
|
||||
background: Background::Color(TAB_ACTIVE),
|
||||
border: Border {
|
||||
color: ACCENT,
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
},
|
||||
icon: Color::WHITE,
|
||||
placeholder: Color {
|
||||
r: 0.5,
|
||||
g: 0.5,
|
||||
b: 0.5,
|
||||
a: 1.0,
|
||||
},
|
||||
value: Color::WHITE,
|
||||
selection: Color {
|
||||
r: 0.20,
|
||||
g: 0.55,
|
||||
b: 0.90,
|
||||
a: 0.4,
|
||||
},
|
||||
})
|
||||
.padding([2, 6])
|
||||
.width(Length::Fixed(90.0));
|
||||
|
||||
let cancel_btn = button(crate::ui::icons::tinted(
|
||||
crate::ui::icons::CLOSE,
|
||||
10.0,
|
||||
Color {
|
||||
r: 0.65,
|
||||
g: 0.65,
|
||||
b: 0.65,
|
||||
a: 1.0,
|
||||
},
|
||||
))
|
||||
.on_press(Message::LayoutRenameCancel)
|
||||
.style(|_: &Theme, _| button::Style {
|
||||
background: Some(Background::Color(Color::TRANSPARENT)),
|
||||
border: Border::default(),
|
||||
shadow: iced::Shadow::default(),
|
||||
snap: false,
|
||||
..Default::default()
|
||||
})
|
||||
.padding([2, 4]);
|
||||
|
||||
row![input, cancel_btn]
|
||||
.spacing(0)
|
||||
.align_y(iced::Center)
|
||||
.into()
|
||||
} else {
|
||||
// Normal clickable tab — left click switches, right click opens context menu.
|
||||
let display = container(text(label.clone()).size(11).color(text_color))
|
||||
.style(move |_: &Theme| container::Style {
|
||||
background: Some(Background::Color(bg(is_active, false))),
|
||||
border,
|
||||
..Default::default()
|
||||
})
|
||||
.padding([3, 10]);
|
||||
|
||||
let switch_msg = Message::LayoutSwitch(label.clone());
|
||||
let ctx_msg = Message::LayoutContextMenu(label.clone());
|
||||
|
||||
// Use mouse_area so we can capture right-click for the context menu.
|
||||
mouse_area(display)
|
||||
.on_press(switch_msg)
|
||||
.on_right_press(ctx_msg)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Space-mode toggle button in the status bar.
|
||||
///
|
||||
/// - Model tab → "MODEL" (non-clickable, informational)
|
||||
/// - Layout, PSPACE → "PAPER" (click → MspaceCommand: enter MSPACE)
|
||||
/// - Layout, MSPACE → "MODEL" (click → ExitViewport: return to PSPACE)
|
||||
fn space_mode_btn(current_layout: &str, in_mspace: bool) -> Element<'static, Message> {
|
||||
let is_model_tab = current_layout == "Model";
|
||||
|
||||
// Labels and styling follow AutoCAD convention:
|
||||
// PAPER = currently in paper-space editing
|
||||
// MODEL = currently in model-space editing (either the Model tab or MSPACE)
|
||||
let (label, active, on_press) = if is_model_tab {
|
||||
("MODEL", false, None::<Message>)
|
||||
} else if in_mspace {
|
||||
("MODEL", true, Some(Message::ExitViewport))
|
||||
} else {
|
||||
("PAPER", false, Some(Message::MspaceCommand))
|
||||
};
|
||||
|
||||
let text_color = if active {
|
||||
SNAP_BORDER_ON
|
||||
} else {
|
||||
OSNAP_OFF_TEXT
|
||||
};
|
||||
let bg_normal = if active { SNAP_ON_BG } else { SNAP_OFF_BG };
|
||||
let bg_hover = if active {
|
||||
SNAP_ON_HOVER
|
||||
} else {
|
||||
SNAP_OFF_HOVER
|
||||
};
|
||||
let border_color = if active { SNAP_BORDER_ON } else { BORDER_COLOR };
|
||||
|
||||
let clickable = on_press.is_some();
|
||||
let mut btn = button(text(label).size(10).color(text_color))
|
||||
.style(move |_: &Theme, status| button::Style {
|
||||
background: Some(Background::Color(match status {
|
||||
button::Status::Hovered if clickable => bg_hover,
|
||||
_ => bg_normal,
|
||||
})),
|
||||
border: Border {
|
||||
color: border_color,
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
},
|
||||
text_color,
|
||||
shadow: iced::Shadow::default(),
|
||||
snap: false,
|
||||
})
|
||||
.padding([2, 6]);
|
||||
|
||||
if let Some(msg) = on_press {
|
||||
btn = btn.on_press(msg);
|
||||
}
|
||||
|
||||
btn.into()
|
||||
}
|
||||
|
||||
fn status_pill(label: impl Into<String>) -> Element<'static, Message> {
|
||||
container(text(label.into()).size(10).color(Color {
|
||||
r: 0.65,
|
||||
g: 0.65,
|
||||
b: 0.65,
|
||||
a: 1.0,
|
||||
}))
|
||||
.style(|_: &Theme| container::Style {
|
||||
background: Some(Background::Color(PILL_BG)),
|
||||
border: Border {
|
||||
color: BORDER_COLOR,
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.padding([2, 6])
|
||||
.into()
|
||||
}
|
||||
|
||||
// ── Colours ───────────────────────────────────────────────────────────────
|
||||
|
||||
const BAR_BG: Color = Color {
|
||||
r: 0.14,
|
||||
g: 0.14,
|
||||
b: 0.14,
|
||||
a: 1.0,
|
||||
};
|
||||
const TAB_ACTIVE: Color = Color {
|
||||
r: 0.25,
|
||||
g: 0.25,
|
||||
b: 0.25,
|
||||
a: 1.0,
|
||||
};
|
||||
const TAB_HOVER: Color = Color {
|
||||
r: 0.20,
|
||||
g: 0.20,
|
||||
b: 0.20,
|
||||
a: 1.0,
|
||||
};
|
||||
const PILL_BG: Color = Color {
|
||||
r: 0.19,
|
||||
g: 0.19,
|
||||
b: 0.19,
|
||||
a: 1.0,
|
||||
};
|
||||
const BORDER_COLOR: Color = Color {
|
||||
r: 0.28,
|
||||
g: 0.28,
|
||||
b: 0.28,
|
||||
a: 1.0,
|
||||
};
|
||||
const ICON_COLOR: Color = Color {
|
||||
r: 0.70,
|
||||
g: 0.70,
|
||||
b: 0.70,
|
||||
a: 1.0,
|
||||
};
|
||||
const ACCENT: Color = Color {
|
||||
r: 0.20,
|
||||
g: 0.55,
|
||||
b: 0.90,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
const OSNAP_ON_TEXT: Color = Color {
|
||||
r: 0.35,
|
||||
g: 0.75,
|
||||
b: 1.00,
|
||||
a: 1.0,
|
||||
};
|
||||
const OSNAP_OFF_TEXT: Color = Color {
|
||||
r: 0.42,
|
||||
g: 0.42,
|
||||
b: 0.42,
|
||||
a: 1.0,
|
||||
};
|
||||
const SNAP_ON_BG: Color = Color {
|
||||
r: 0.10,
|
||||
g: 0.20,
|
||||
b: 0.32,
|
||||
a: 1.0,
|
||||
};
|
||||
const SNAP_ON_HOVER: Color = Color {
|
||||
r: 0.14,
|
||||
g: 0.27,
|
||||
b: 0.42,
|
||||
a: 1.0,
|
||||
};
|
||||
const SNAP_BORDER_ON: Color = Color {
|
||||
r: 0.20,
|
||||
g: 0.50,
|
||||
b: 0.85,
|
||||
a: 1.0,
|
||||
};
|
||||
const SNAP_OFF_BG: Color = Color {
|
||||
r: 0.17,
|
||||
g: 0.17,
|
||||
b: 0.17,
|
||||
a: 1.0,
|
||||
};
|
||||
const SNAP_OFF_HOVER: Color = Color {
|
||||
r: 0.22,
|
||||
g: 0.22,
|
||||
b: 0.22,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
// ── Scale popup button ────────────────────────────────────────────────────
|
||||
|
||||
fn units_btn(label: &str, open: bool) -> Element<'static, Message> {
|
||||
let label = label.to_string();
|
||||
button(
|
||||
text(label)
|
||||
.size(10)
|
||||
.color(if open { SNAP_BORDER_ON } else { OSNAP_OFF_TEXT }),
|
||||
)
|
||||
.on_press(Message::ToggleUnitsPopup)
|
||||
.style(move |_: &Theme, status| button::Style {
|
||||
background: Some(Background::Color(match (open, status) {
|
||||
(true, button::Status::Hovered) => SNAP_ON_HOVER,
|
||||
(true, _) => SNAP_ON_BG,
|
||||
(false, button::Status::Hovered) => SNAP_OFF_HOVER,
|
||||
(false, _) => SNAP_OFF_BG,
|
||||
})),
|
||||
border: Border {
|
||||
color: if open { SNAP_BORDER_ON } else { BORDER_COLOR },
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
},
|
||||
text_color: if open { SNAP_BORDER_ON } else { OSNAP_OFF_TEXT },
|
||||
shadow: iced::Shadow::default(),
|
||||
snap: false,
|
||||
})
|
||||
.padding([2, 6])
|
||||
.into()
|
||||
}
|
||||
|
||||
fn scale_popup_btn(label: &str, open: bool) -> Element<'static, Message> {
|
||||
let label = label.to_string();
|
||||
button(
|
||||
text(label)
|
||||
.size(10)
|
||||
.color(if open { SNAP_BORDER_ON } else { OSNAP_OFF_TEXT }),
|
||||
)
|
||||
.on_press(Message::ToggleScalePopup)
|
||||
.style(move |_: &Theme, status| button::Style {
|
||||
background: Some(Background::Color(match (open, status) {
|
||||
(true, button::Status::Hovered) => SNAP_ON_HOVER,
|
||||
(true, _) => SNAP_ON_BG,
|
||||
(false, button::Status::Hovered) => SNAP_OFF_HOVER,
|
||||
(false, _) => SNAP_OFF_BG,
|
||||
})),
|
||||
border: Border {
|
||||
color: if open { SNAP_BORDER_ON } else { BORDER_COLOR },
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
},
|
||||
text_color: if open { SNAP_BORDER_ON } else { OSNAP_OFF_TEXT },
|
||||
shadow: iced::Shadow::default(),
|
||||
snap: false,
|
||||
})
|
||||
.padding([2, 6])
|
||||
.into()
|
||||
}
|
||||
|
||||
// ── Scale display ─────────────────────────────────────────────────────────
|
||||
|
||||
/// Formats a viewport scale factor as a human-readable ratio string.
|
||||
///
|
||||
/// - `None` → "1:1" (model space or no viewport yet)
|
||||
/// - `1.0` → "1:1"
|
||||
/// - `0.02` → "1:50"
|
||||
/// - `2.0` → "2:1"
|
||||
fn format_scale(scale: Option<f64>) -> String {
|
||||
let s = match scale {
|
||||
None => return "1:1".to_string(),
|
||||
Some(v) if v <= 0.0 => return "1:1".to_string(),
|
||||
Some(v) => v,
|
||||
};
|
||||
|
||||
// Try to express as a clean integer ratio.
|
||||
if s >= 1.0 {
|
||||
let n = s.round() as u32;
|
||||
if (s - n as f64).abs() < 0.01 * s {
|
||||
return if n == 1 {
|
||||
"1:1".to_string()
|
||||
} else {
|
||||
format!("{}:1", n)
|
||||
};
|
||||
}
|
||||
} else {
|
||||
let inv = (1.0 / s).round() as u32;
|
||||
if (s - 1.0 / inv as f64).abs() < 0.01 * s {
|
||||
return format!("1:{}", inv);
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to a decimal string.
|
||||
format!("{:.4}", s)
|
||||
.trim_end_matches('0')
|
||||
.trim_end_matches('.')
|
||||
.to_string()
|
||||
}
|
||||
177
src/ui/statusbar/statusbar_config.rs
Normal file
177
src/ui/statusbar/statusbar_config.rs
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
//! Status-bar customization — which toggle pills are shown on the bar.
|
||||
//!
|
||||
//! The customization menu (opened from the bar's far-right handle) lists every
|
||||
//! pill with a check mark next to the ones currently shown. Toggling a row
|
||||
//! adds or removes that pill from the bar. The choice is persisted so it
|
||||
//! survives across sessions.
|
||||
|
||||
use rustc_hash::FxHashSet as HashSet;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Identifies a toggleable status-bar pill.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
pub enum StatusPill {
|
||||
Coords,
|
||||
Ortho,
|
||||
Lwt,
|
||||
Polar,
|
||||
Dyn,
|
||||
Otrack,
|
||||
Osnap,
|
||||
Space,
|
||||
Scale,
|
||||
Units,
|
||||
Transparency,
|
||||
Isolate,
|
||||
QuickProps,
|
||||
SelFilter,
|
||||
SelCycle,
|
||||
Vp,
|
||||
CleanScreen,
|
||||
}
|
||||
|
||||
impl StatusPill {
|
||||
/// Every pill, in status-bar display order. Drives both the bar layout and
|
||||
/// the customization menu.
|
||||
pub const ALL: &'static [StatusPill] = &[
|
||||
StatusPill::Coords,
|
||||
StatusPill::Ortho,
|
||||
StatusPill::Lwt,
|
||||
StatusPill::Polar,
|
||||
StatusPill::Dyn,
|
||||
StatusPill::Otrack,
|
||||
StatusPill::Osnap,
|
||||
StatusPill::Space,
|
||||
StatusPill::Scale,
|
||||
StatusPill::Units,
|
||||
StatusPill::Transparency,
|
||||
StatusPill::Isolate,
|
||||
StatusPill::QuickProps,
|
||||
StatusPill::SelFilter,
|
||||
StatusPill::SelCycle,
|
||||
StatusPill::Vp,
|
||||
StatusPill::CleanScreen,
|
||||
];
|
||||
|
||||
/// Stable identifier used for persistence.
|
||||
pub fn id(self) -> &'static str {
|
||||
match self {
|
||||
StatusPill::Coords => "coords",
|
||||
StatusPill::Ortho => "ortho",
|
||||
StatusPill::Lwt => "lwt",
|
||||
StatusPill::Polar => "polar",
|
||||
StatusPill::Dyn => "dyn",
|
||||
StatusPill::Otrack => "otrack",
|
||||
StatusPill::Osnap => "osnap",
|
||||
StatusPill::Space => "space",
|
||||
StatusPill::Scale => "scale",
|
||||
StatusPill::Units => "units",
|
||||
StatusPill::Transparency => "transparency",
|
||||
StatusPill::Isolate => "isolate",
|
||||
StatusPill::QuickProps => "quickprops",
|
||||
StatusPill::SelFilter => "selfilter",
|
||||
StatusPill::SelCycle => "selcycle",
|
||||
StatusPill::Vp => "vp",
|
||||
StatusPill::CleanScreen => "cleanscreen",
|
||||
}
|
||||
}
|
||||
|
||||
/// Label shown in the customization menu.
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
StatusPill::Coords => "Coordinates",
|
||||
StatusPill::Ortho => "Ortho Mode",
|
||||
StatusPill::Lwt => "Show Lineweight",
|
||||
StatusPill::Polar => "Polar Tracking",
|
||||
StatusPill::Dyn => "Dynamic Input",
|
||||
StatusPill::Otrack => "Object Snap Tracking",
|
||||
StatusPill::Osnap => "Object Snap",
|
||||
StatusPill::Space => "Model/Paper Space",
|
||||
StatusPill::Scale => "Annotation Scale",
|
||||
StatusPill::Units => "Drawing Units",
|
||||
StatusPill::Transparency => "Show Transparency",
|
||||
StatusPill::Isolate => "Isolate Objects",
|
||||
StatusPill::QuickProps => "Quick Properties",
|
||||
StatusPill::SelFilter => "Selection Filtering",
|
||||
StatusPill::SelCycle => "Selection Cycling",
|
||||
StatusPill::Vp => "Viewport Count",
|
||||
StatusPill::CleanScreen => "Clean Screen",
|
||||
}
|
||||
}
|
||||
|
||||
fn from_id(s: &str) -> Option<StatusPill> {
|
||||
StatusPill::ALL.iter().copied().find(|p| p.id() == s)
|
||||
}
|
||||
}
|
||||
|
||||
/// Tracks which pills the user has hidden. Empty = every pill visible.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct StatusBarConfig {
|
||||
hidden: HashSet<StatusPill>,
|
||||
}
|
||||
|
||||
impl StatusBarConfig {
|
||||
/// Load the saved customization, or all-visible when none exists.
|
||||
pub fn load() -> Self {
|
||||
let mut hidden = HashSet::default();
|
||||
if let Some(path) = config_path() {
|
||||
if let Ok(body) = std::fs::read_to_string(path) {
|
||||
for line in body.lines() {
|
||||
if let Some(p) = StatusPill::from_id(line.trim()) {
|
||||
hidden.insert(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Self { hidden }
|
||||
}
|
||||
|
||||
pub fn is_visible(&self, pill: StatusPill) -> bool {
|
||||
!self.hidden.contains(&pill)
|
||||
}
|
||||
|
||||
/// Flip a pill's visibility and persist the change.
|
||||
pub fn toggle(&mut self, pill: StatusPill) {
|
||||
if !self.hidden.remove(&pill) {
|
||||
self.hidden.insert(pill);
|
||||
}
|
||||
self.save();
|
||||
}
|
||||
|
||||
fn save(&self) {
|
||||
let Some(path) = config_path() else { return };
|
||||
if let Some(dir) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(dir);
|
||||
}
|
||||
let body: String = StatusPill::ALL
|
||||
.iter()
|
||||
.filter(|p| self.hidden.contains(p))
|
||||
.map(|p| p.id())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let _ = std::fs::write(path, body);
|
||||
}
|
||||
}
|
||||
|
||||
/// `<config-dir>/OpenCADStudio/statusbar.txt`, matching the recent-files store.
|
||||
fn config_path() -> Option<PathBuf> {
|
||||
let base: PathBuf = if cfg!(target_os = "windows") {
|
||||
std::env::var_os("APPDATA").map(PathBuf::from)?
|
||||
} else if cfg!(target_os = "macos") {
|
||||
let home = std::env::var_os("HOME")?;
|
||||
let mut p = PathBuf::from(home);
|
||||
p.push("Library");
|
||||
p.push("Application Support");
|
||||
p
|
||||
} else if let Some(d) = std::env::var_os("XDG_CONFIG_HOME") {
|
||||
PathBuf::from(d)
|
||||
} else {
|
||||
let home = std::env::var_os("HOME")?;
|
||||
let mut p = PathBuf::from(home);
|
||||
p.push(".config");
|
||||
p
|
||||
};
|
||||
let mut p = base;
|
||||
p.push("OpenCADStudio");
|
||||
Some(p.join("statusbar.txt"))
|
||||
}
|
||||
114
src/ui/statusbar/statusbar_menu.rs
Normal file
114
src/ui/statusbar/statusbar_menu.rs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
//! Status-bar customization menu — a checkmark list of every toggle pill,
|
||||
//! opened from the bar's far-right handle. Rendered as a floating overlay
|
||||
//! above the status bar, same pattern as the scale picker.
|
||||
|
||||
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::ui::statusbar::statusbar_config::{StatusBarConfig, StatusPill};
|
||||
|
||||
/// Full-screen overlay: transparent click-catcher + the menu panel pinned to
|
||||
/// the bottom-right, just above the status bar.
|
||||
pub fn statusbar_menu_overlay(config: &StatusBarConfig) -> Element<'static, Message> {
|
||||
let rows: Vec<Element<'static, Message>> = StatusPill::ALL
|
||||
.iter()
|
||||
.map(|&pill| {
|
||||
menu_row(
|
||||
pill.label(),
|
||||
config.is_visible(pill),
|
||||
Message::ToggleStatusPill(pill),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let panel = container(column(rows))
|
||||
.style(|_: &Theme| container::Style {
|
||||
background: Some(Background::Color(PANEL_BG)),
|
||||
border: Border {
|
||||
color: PANEL_BORDER,
|
||||
width: 1.0,
|
||||
radius: 3.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.width(Length::Fixed(200.0));
|
||||
|
||||
let positioned = 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::CloseStatusBarMenu)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn menu_row(label: &'static str, checked: bool, msg: Message) -> Element<'static, Message> {
|
||||
let check = crate::ui::icons::check_cell(checked, CHECK_COLOR);
|
||||
|
||||
let lbl = text(label)
|
||||
.size(11)
|
||||
.color(if checked { 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