diff --git a/src/app/config.rs b/src/app/config.rs index a1e3544b..7bc63e96 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -18,6 +18,8 @@ use crate::ui::window::plot::PlotDialogState; pub struct AppConfig { /// Input modes, backup, plugin lists, viewport background colours, … pub settings: UserSettings, + /// Iced theme selection and the six base colours used by a custom theme. + pub theme: UiThemeConfig, /// Recent-files list + retained count. pub recent: RecentConfig, /// Last selected section on the tabbed Start page. @@ -35,6 +37,7 @@ impl Default for AppConfig { fn default() -> Self { Self { settings: UserSettings::default(), + theme: UiThemeConfig::default(), recent: RecentConfig::default(), start: StartConfig::default(), statusbar: StatusBarConfig::default(), @@ -44,6 +47,136 @@ impl Default for AppConfig { } } +#[derive(Clone, PartialEq, Serialize, Deserialize)] +#[serde(default)] +pub struct UiThemeConfig { + pub name: String, + pub palette: UiThemePalette, +} + +impl Default for UiThemeConfig { + fn default() -> Self { + let theme = iced::Theme::Dark; + Self { + name: theme.to_string(), + palette: UiThemePalette::from_iced(theme.palette()), + } + } +} + +impl UiThemeConfig { + pub fn to_iced(&self) -> iced::Theme { + if self.name == "Custom" { + iced::Theme::custom("Custom", self.palette.to_iced()) + } else { + builtin_theme(&self.name).unwrap_or(iced::Theme::Dark) + } + } +} + +#[derive(Clone, Copy, PartialEq, Serialize, Deserialize)] +#[serde(default)] +pub struct UiThemePalette { + pub background: [u8; 3], + pub text: [u8; 3], + pub primary: [u8; 3], + pub success: [u8; 3], + pub warning: [u8; 3], + pub danger: [u8; 3], +} + +impl Default for UiThemePalette { + fn default() -> Self { + Self::from_iced(iced::Theme::Dark.palette()) + } +} + +impl UiThemePalette { + pub fn from_iced(palette: iced::theme::Palette) -> Self { + Self { + background: color_to_rgb(palette.background), + text: color_to_rgb(palette.text), + primary: color_to_rgb(palette.primary), + success: color_to_rgb(palette.success), + warning: color_to_rgb(palette.warning), + danger: color_to_rgb(palette.danger), + } + } + + pub fn to_iced(self) -> iced::theme::Palette { + iced::theme::Palette { + background: rgb_to_color(self.background), + text: rgb_to_color(self.text), + primary: rgb_to_color(self.primary), + success: rgb_to_color(self.success), + warning: rgb_to_color(self.warning), + danger: rgb_to_color(self.danger), + } + } + + pub fn hex_values(self) -> [String; 6] { + [ + rgb_to_hex(self.background), + rgb_to_hex(self.text), + rgb_to_hex(self.primary), + rgb_to_hex(self.success), + rgb_to_hex(self.warning), + rgb_to_hex(self.danger), + ] + } + + pub fn set_hex(&mut self, index: usize, value: &str) -> bool { + let Some(rgb) = parse_hex(value) else { + return false; + }; + match index { + 0 => self.background = rgb, + 1 => self.text = rgb, + 2 => self.primary = rgb, + 3 => self.success = rgb, + 4 => self.warning = rgb, + 5 => self.danger = rgb, + _ => return false, + } + true + } +} + +pub fn builtin_theme(name: &str) -> Option { + iced::Theme::ALL + .iter() + .find(|theme| theme.to_string() == name) + .cloned() +} + +fn color_to_rgb(color: iced::Color) -> [u8; 3] { + [ + (color.r * 255.0).round() as u8, + (color.g * 255.0).round() as u8, + (color.b * 255.0).round() as u8, + ] +} + +fn rgb_to_color(rgb: [u8; 3]) -> iced::Color { + iced::Color::from_rgb8(rgb[0], rgb[1], rgb[2]) +} + +fn rgb_to_hex(rgb: [u8; 3]) -> String { + format!("#{:02X}{:02X}{:02X}", rgb[0], rgb[1], rgb[2]) +} + +fn parse_hex(value: &str) -> Option<[u8; 3]> { + let value = value.trim().strip_prefix('#').unwrap_or(value.trim()); + if value.len() != 6 { + return None; + } + Some([ + u8::from_str_radix(&value[0..2], 16).ok()?, + u8::from_str_radix(&value[2..4], 16).ok()?, + u8::from_str_radix(&value[4..6], 16).ok()?, + ]) +} + #[derive(Clone, PartialEq, Serialize, Deserialize)] #[serde(default)] pub struct RecentConfig { diff --git a/src/app/mod.rs b/src/app/mod.rs index 565e0663..b2e3833b 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,7 +1,7 @@ mod alias; #[cfg(not(target_arch = "wasm32"))] mod automation; -mod config; +pub(crate) mod config; #[cfg(not(target_arch = "wasm32"))] pub use automation::{export_headless, serve}; mod command_driver; @@ -688,6 +688,8 @@ pub(super) struct OpenCADStudio { // ── Color Scheme ────────────────────────────────────────────────────── active_theme: Theme, + ui_theme: config::UiThemeConfig, + theme_color_inputs: [String; 6], // ── Keyboard Shortcut Editor ────────────────────────────────────────── /// User-defined function-key overrides: "F3" → command string. @@ -1474,6 +1476,10 @@ pub enum Message { OptionsOpen, /// Set the default type/version used when first saving a new drawing. DefaultSaveFormatChanged(String), + /// Select one of Iced's built-in themes or the editable Custom theme. + OptionsThemeChanged(String), + /// Edit one of Custom theme's six base colours as #RRGGBB. + OptionsThemeColorChanged(usize, String), ClearScene, SetWireframe(bool), /// Set the active tab's render mode (one of acadrust's seven visual @@ -2626,6 +2632,8 @@ impl OpenCADStudio { active_plot_style: None, // Color scheme (default: dark CAD-style) active_theme: Theme::Dark, + ui_theme: config::UiThemeConfig::default(), + theme_color_inputs: config::UiThemePalette::default().hex_values(), // Keyboard shortcuts shortcut_overrides: rustc_hash::FxHashMap::default(), // Command aliases (populated from ocad.pgp just after construction) diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 29102e90..ebe9e0b8 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -320,6 +320,7 @@ impl OpenCADStudio { pub(in crate::app) fn current_config(&self) -> crate::app::config::AppConfig { crate::app::config::AppConfig { settings: self.current_settings(), + theme: self.ui_theme.clone(), recent: crate::app::config::RecentConfig { files: self .recent_files @@ -342,6 +343,9 @@ impl OpenCADStudio { /// Distribute a loaded config into live app state (called once at startup). pub(in crate::app) fn apply_config(&mut self, cfg: crate::app::config::AppConfig) { self.apply_settings(&cfg.settings); + self.ui_theme = cfg.theme.clone(); + self.active_theme = self.ui_theme.to_iced(); + self.theme_color_inputs = self.ui_theme.palette.hex_values(); self.recent_files = cfg .recent .files diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index c19138f4..e4b31e84 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -3551,7 +3551,12 @@ impl OpenCADStudio { } Message::SetTheme(theme) => { + self.ui_theme.name = theme.to_string(); + self.ui_theme.palette = + crate::app::config::UiThemePalette::from_iced(theme.palette()); + self.theme_color_inputs = self.ui_theme.palette.hex_values(); self.active_theme = theme; + self.persist_settings_if_changed(); Task::none() } @@ -3620,6 +3625,37 @@ impl OpenCADStudio { Message::DefaultSaveFormatChanged(format) => { self.default_save_format = crate::io::canonical_save_format(&format).to_string(); + self.persist_settings_if_changed(); + Task::none() + } + + Message::OptionsThemeChanged(name) => { + self.ui_theme.name = name; + if let Some(theme) = + crate::app::config::builtin_theme(&self.ui_theme.name) + { + self.ui_theme.palette = + crate::app::config::UiThemePalette::from_iced(theme.palette()); + self.theme_color_inputs = self.ui_theme.palette.hex_values(); + self.active_theme = theme; + } else { + self.ui_theme.name = "Custom".to_string(); + self.active_theme = self.ui_theme.to_iced(); + } + self.persist_settings_if_changed(); + Task::none() + } + + Message::OptionsThemeColorChanged(index, value) => { + if index >= self.theme_color_inputs.len() { + return Task::none(); + } + self.theme_color_inputs[index] = value.clone(); + if self.ui_theme.palette.set_hex(index, &value) { + self.ui_theme.name = "Custom".to_string(); + self.active_theme = self.ui_theme.to_iced(); + self.persist_settings_if_changed(); + } Task::none() } diff --git a/src/app/view/controls.rs b/src/app/view/controls.rs index 3aec2fbf..43af88c6 100644 --- a/src/app/view/controls.rs +++ b/src/app/view/controls.rs @@ -4,7 +4,7 @@ use super::super::Message; use iced::widget::{ button, container, mouse_area, row, }; -use iced::{Background, Border, Color, Element, Theme}; +use iced::{Background, Border, Element, Theme}; pub(super) fn viewport_controls<'a>( render_mode: acadrust::entities::ViewportRenderMode, @@ -23,64 +23,58 @@ pub(super) fn viewport_controls<'a>( RenderModeChoice(M::FlatShadedWithEdges), RenderModeChoice(M::GouraudShadedWithEdges), ]; - let light = Color { r: 0.85, g: 0.85, b: 0.85, a: 1.0 }; - let accent = Color { r: 0.45, g: 0.70, b: 1.0, a: 1.0 }; - let green = Color { r: 0.36, g: 0.80, b: 0.45, a: 1.0 }; - let red = Color { r: 0.92, g: 0.38, b: 0.38, a: 1.0 }; - - // Fixed-colour icon button (close = red); colour stays on hover. - let tinted_btn = move |bytes: &'static [u8], color: Color, msg: Message| { - button(crate::ui::icons::tinted(bytes, 15.0, color)) + let danger_btn = move |bytes: &'static [u8], msg: Message| { + button(crate::ui::icons::themed_danger(bytes, 15.0)) .on_press(msg) .padding([4, 6]) - .style(move |_: &Theme, status| iced::widget::button::Style { - background: Some(Background::Color(match status { + .style(move |theme: &Theme, status| iced::widget::button::Style { + background: matches!( + status, iced::widget::button::Status::Hovered - | iced::widget::button::Status::Pressed => Color { - r: 0.25, - g: 0.25, - b: 0.25, - a: 0.9, - }, - _ => Color::TRANSPARENT, - })), + | iced::widget::button::Status::Pressed + ) + .then_some(Background::Color( + theme.extended_palette().danger.weak.color + )), border: Border { radius: 3.0.into(), ..Default::default() }, - text_color: color, + text_color: theme.extended_palette().danger.base.color, ..Default::default() }) }; // Borderless icon button; an `active` toggle gets an accent tint + fill. let icon_btn = move |bytes: &'static [u8], active: bool, msg: Message| { - let tint = if active { accent } else { light }; - button(crate::ui::icons::tinted(bytes, 15.0, tint)) + let icon = if active { + crate::ui::icons::themed_primary(bytes, 15.0) + } else { + crate::ui::icons::themed(bytes, 15.0) + }; + button(icon) .on_press(msg) .padding([4, 6]) - .style(move |_: &Theme, status| iced::widget::button::Style { - background: Some(Background::Color(match (active, status) { - (_, iced::widget::button::Status::Hovered) => Color { - r: 0.25, - g: 0.25, - b: 0.25, - a: 0.9, - }, - (true, _) => Color { - r: 0.16, - g: 0.22, - b: 0.32, - a: 0.9, - }, - (false, _) => Color::TRANSPARENT, - })), + .style(move |theme: &Theme, status| { + let palette = theme.extended_palette(); + let pair = match (active, status) { + (_, iced::widget::button::Status::Hovered) => { + Some(palette.background.strong) + } + (true, _) => Some(palette.primary.weak), + (false, _) => None, + }; + iced::widget::button::Style { + background: pair.map(|p| Background::Color(p.color)), border: Border { radius: 3.0.into(), ..Default::default() }, - text_color: tint, + text_color: pair + .map(|p| p.text) + .unwrap_or(palette.background.base.text), ..Default::default() + } }) }; @@ -92,27 +86,27 @@ pub(super) fn viewport_controls<'a>( ) .text_size(11) .padding([4, 6]) - .style(move |_: &Theme, _| iced::widget::pick_list::Style { - background: Background::Color(Color::TRANSPARENT), + .style(move |theme: &Theme, _| { + let text = theme.extended_palette().background.base.text; + iced::widget::pick_list::Style { + background: Background::Color(iced::Color::TRANSPARENT), border: Border { radius: 3.0.into(), ..Default::default() }, - text_color: light, - placeholder_color: light, - handle_color: light, + text_color: text, + placeholder_color: text.scale_alpha(0.68), + handle_color: text, + } }); // Thin vertical divider between control groups. let sep = || { - container(iced::widget::Space::new().width(1.0).height(16.0)).style(|_: &Theme| { + container(iced::widget::Space::new().width(1.0).height(16.0)).style(|theme: &Theme| { iced::widget::container::Style { - background: Some(Background::Color(Color { - r: 0.45, - g: 0.45, - b: 0.45, - a: 0.7, - })), + background: Some(Background::Color( + theme.extended_palette().background.neutral.color.scale_alpha(0.7) + )), ..Default::default() } }) @@ -139,7 +133,7 @@ pub(super) fn viewport_controls<'a>( // would only fire on release). Placed just left of Close. if tile_count > 1 { let drag = mouse_area( - container(crate::ui::icons::tinted(crate::ui::icons::MOVE, 15.0, green)) + container(crate::ui::icons::themed_success(crate::ui::icons::MOVE, 15.0)) .padding([4, 6]) .style(|_: &Theme| iced::widget::container::Style { border: Border { @@ -155,30 +149,25 @@ pub(super) fn viewport_controls<'a>( .push(sep()) .push(drag) .push(sep()) - .push(tinted_btn(crate::ui::icons::CLOSE, red, Message::CloseModelViewport)); + .push(danger_btn(crate::ui::icons::CLOSE, Message::CloseModelViewport)); } } container(bar) .padding(2) - .style(|_: &Theme| iced::widget::container::Style { - background: Some(Background::Color(Color { - r: 0.10, - g: 0.10, - b: 0.10, - a: 0.75, - })), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + iced::widget::container::Style { + background: Some(Background::Color( + palette.background.weak.color.scale_alpha(0.92) + )), border: Border { - color: Color { - r: 0.35, - g: 0.35, - b: 0.35, - a: 1.0, - }, + color: palette.background.neutral.color, width: 1.0, radius: 4.0.into(), }, ..Default::default() + } }) .into() } diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index bb25ea63..ea827b8a 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -930,56 +930,47 @@ impl OpenCADStudio { for (idx, item) in popup.items.iter().enumerate() { let is_sel = idx == popup.selected; let label = item.label; - let btn = button(text(label).size(12).color(Color::WHITE)) + let btn = button(text(label).size(12)) .on_press(Message::GripMenuPick(idx)) .padding([3, 10]) .width(Fill) - .style(move |_: &Theme, status| iced::widget::button::Style { - background: Some(Background::Color(match (is_sel, status) { - (true, _) => Color { - r: 0.20, - g: 0.45, - b: 0.95, - a: 1.0, - }, - (_, iced::widget::button::Status::Hovered) => Color { - r: 0.22, - g: 0.22, - b: 0.22, - a: 1.0, - }, - _ => Color::TRANSPARENT, - })), + .style(move |theme: &Theme, status| { + let palette = theme.extended_palette(); + let pair = match (is_sel, status) { + (true, _) => Some(palette.primary.strong), + (_, iced::widget::button::Status::Hovered) => { + Some(palette.background.strong) + } + _ => None, + }; + iced::widget::button::Style { + background: pair.map(|p| Background::Color(p.color)), border: Border { color: Color::TRANSPARENT, width: 0.0, radius: 0.0.into(), }, - text_color: Color::WHITE, + text_color: pair + .map(|p| p.text) + .unwrap_or(palette.background.base.text), ..Default::default() + } }); col = col.push(btn); } let menu_panel = container(col) .padding(2) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { - r: 0.10, - g: 0.10, - b: 0.10, - a: 0.95, - })), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.weak.color)), border: Border { - color: Color { - r: 0.40, - g: 0.40, - b: 0.40, - a: 1.0, - }, + color: palette.background.neutral.color, width: 1.0, radius: 3.0.into(), }, ..Default::default() + } }); // Offset the menu by 12 px so the cursor doesn't land on // the first item immediately, matching the right-click @@ -1005,14 +996,14 @@ impl OpenCADStudio { for (idx, name) in popup.items.iter().enumerate() { let is_cur = popup.current == Some(idx); let mark: Element<'_, Message> = if is_cur { - crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, Color::WHITE) + crate::ui::icons::themed_check_cell(true) } else { Space::new().width(11).into() }; let btn = button( row![ container(mark).width(16), - text(name).size(12).color(Color::WHITE), + text(name).size(12), ] .spacing(2) .align_y(iced::Center), @@ -1020,47 +1011,39 @@ impl OpenCADStudio { .on_press(Message::VisibilityPick(idx)) .padding([3, 10]) .width(Fill) - .style(move |_: &Theme, status| iced::widget::button::Style { - background: Some(Background::Color(match status { - iced::widget::button::Status::Hovered => Color { - r: 0.20, - g: 0.45, - b: 0.95, - a: 1.0, - }, - _ => Color::TRANSPARENT, - })), + .style(move |theme: &Theme, status| { + let palette = theme.extended_palette(); + iced::widget::button::Style { + background: matches!( + status, + iced::widget::button::Status::Hovered + ) + .then_some(Background::Color(palette.primary.weak.color)), border: Border { color: Color::TRANSPARENT, width: 0.0, radius: 0.0.into(), }, - text_color: Color::WHITE, + text_color: palette.background.base.text, ..Default::default() + } }); col = col.push(btn); } let panel = container(iced::widget::scrollable(col).height(iced::Length::Shrink)) .max_height(360.0) .padding(2) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { - r: 0.10, - g: 0.10, - b: 0.10, - a: 0.95, - })), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.weak.color)), border: Border { - color: Color { - r: 0.40, - g: 0.40, - b: 0.40, - a: 1.0, - }, + color: palette.background.neutral.color, width: 1.0, radius: 3.0.into(), }, ..Default::default() + } }); let anchor = iced::Point::new(popup.anchor.x + 12.0, popup.anchor.y + 12.0); viewport_stack = @@ -1127,42 +1110,27 @@ impl OpenCADStudio { } else { trace }; - let perf_button_style = |_: &Theme, status: button::Status| button::Style { - background: Some(Background::Color(if matches!(status, button::Status::Hovered) { - Color { - r: 0.24, - g: 0.24, - b: 0.24, - a: 1.0, - } + let perf_button_style = |theme: &Theme, status: button::Status| { + let palette = theme.extended_palette(); + let pair = if matches!(status, button::Status::Hovered) { + palette.background.strong } else { - Color { - r: 0.14, - g: 0.14, - b: 0.14, - a: 1.0, - } - })), - text_color: Color::WHITE, + palette.background.weak + }; + button::Style { + background: Some(Background::Color(pair.color)), + text_color: pair.text, border: Border { - color: Color { - r: 0.35, - g: 0.35, - b: 0.35, - a: 1.0, - }, + color: palette.background.neutral.color, width: 1.0, radius: 3.0.into(), }, ..Default::default() + } }; let copy_btn = button( row![ - crate::ui::icons::tinted( - crate::ui::icons::COPY, - 11.0, - Color::from_rgb(0.7, 0.85, 1.0), - ), + crate::ui::icons::themed_primary(crate::ui::icons::COPY, 11.0), text("Copy").size(11), ] .spacing(4) @@ -1173,11 +1141,7 @@ impl OpenCADStudio { .padding([2, 6]); let clear_btn = button( row![ - crate::ui::icons::tinted( - crate::ui::icons::TRASH, - 11.0, - Color::from_rgb(1.0, 0.55, 0.55), - ), + crate::ui::icons::themed_danger(crate::ui::icons::TRASH, 11.0), text("Clear").size(11), ] .spacing(4) @@ -1187,20 +1151,24 @@ impl OpenCADStudio { .style(perf_button_style) .padding([2, 6]); let header = row![ - text("PERF").size(12).color(Color::from_rgb(0.6, 1.0, 0.6)), + text("PERF").size(12).style(|theme: &Theme| iced::widget::text::Style { + color: Some(theme.extended_palette().success.base.color), + }), Space::new().width(iced::Length::Fill), copy_btn, clear_btn, ] .spacing(6) .align_y(iced::Center); - let log = scrollable(text(trace).size(11).color(Color::from_rgb(0.8, 0.9, 0.8))) + let log = scrollable(text(trace).size(11)) .height(iced::Length::Fixed(220.0)) .width(iced::Length::Fill); let panel = container( column![ header, - text(summary).size(11).color(Color::from_rgb(0.6, 1.0, 0.6)), + text(summary).size(11).style(|theme: &Theme| iced::widget::text::Style { + color: Some(theme.extended_palette().success.base.color), + }), log, ] .spacing(5), @@ -1454,13 +1422,10 @@ impl OpenCADStudio { .width(Fill) .height(Fill) }) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { - r: 0.11, - g: 0.11, - b: 0.11, - a: 1.0, - })), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.base.color + )), ..Default::default() }) .width(Fill) @@ -1897,58 +1862,10 @@ fn doc_tab_context_menu( has_other_drawings: bool, ) -> Element<'static, Message> { const MENU_W: f32 = 210.0; - const MENU_BG: Color = Color { - r: 0.17, - g: 0.17, - b: 0.17, - a: 1.0, - }; - const MENU_BORDER: Color = Color { - r: 0.35, - g: 0.35, - b: 0.35, - a: 1.0, - }; - const ITEM_HOVER: Color = Color { - r: 0.25, - g: 0.45, - b: 0.70, - a: 1.0, - }; - const TEXT_COLOR: Color = Color { - r: 0.88, - g: 0.88, - b: 0.88, - a: 1.0, - }; - const DISABLED_TEXT: Color = Color { - r: 0.43, - g: 0.43, - b: 0.43, - a: 1.0, - }; let item = |label: &'static str, msg: Option| { - let enabled = msg.is_some(); - let mut item = button(text(label).size(12).color(if enabled { - TEXT_COLOR - } else { - DISABLED_TEXT - })) - .style(move |_: &Theme, status| button::Style { - background: Some(Background::Color(if enabled { - match status { - button::Status::Hovered | button::Status::Pressed => ITEM_HOVER, - _ => Color::TRANSPARENT, - } - } else { - Color::TRANSPARENT - })), - text_color: if enabled { TEXT_COLOR } else { DISABLED_TEXT }, - border: Border::default(), - shadow: iced::Shadow::default(), - snap: false, - }) + let mut item = button(text(label).size(12)) + .style(button::subtle) .padding([4, 12]) .width(Fill); if let Some(msg) = msg { @@ -1978,71 +1895,13 @@ fn doc_tab_context_menu( .spacing(0) .width(MENU_W), ) - .style(move |_: &Theme| container::Style { - background: Some(Background::Color(MENU_BG)), - border: Border { - color: MENU_BORDER, - width: 1.0, - radius: 4.0.into(), - }, - ..Default::default() - }) + .style(container::bordered_box) .padding([4, 0]) .width(iced::Length::Fixed(MENU_W)) .into() } pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Element<'a, Message> { - const BAR_BG: Color = Color { - r: 0.13, - g: 0.13, - b: 0.13, - a: 1.0, - }; - const TAB_ACTIVE: Color = Color { - r: 0.22, - g: 0.22, - b: 0.22, - a: 1.0, - }; - const TAB_HOVER: Color = Color { - r: 0.18, - g: 0.18, - b: 0.18, - a: 1.0, - }; - const TAB_INACTIVE: Color = Color { - r: 0.13, - g: 0.13, - b: 0.13, - a: 1.0, - }; - const ACCENT: Color = Color { - r: 0.20, - g: 0.55, - b: 0.90, - a: 1.0, - }; - const TEXT_ACTIVE: Color = Color::WHITE; - const TEXT_INACTIVE: Color = Color { - r: 0.60, - g: 0.60, - b: 0.60, - a: 1.0, - }; - const CLOSE_HOVER: Color = Color { - r: 0.70, - g: 0.22, - b: 0.22, - a: 1.0, - }; - const BORDER_COLOR: Color = Color { - r: 0.25, - g: 0.25, - b: 0.25, - a: 1.0, - }; - // Document tabs live in a flex-wrap flow so they spill onto lower rows when // there are more tabs than the width can hold on one line. let mut items: Vec> = Vec::new(); @@ -2058,16 +1917,7 @@ pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Ele let name = crate::ui::text_util::elide(&tab.tab_display_name(), 24); let title_inner: Element<'_, Message> = if tab.dirty { row![ - crate::ui::icons::tinted( - crate::ui::icons::DOT, - 7.0, - Color { - r: 0.90, - g: 0.75, - b: 0.30, - a: 1.0, - }, - ), + crate::ui::icons::themed_warning(crate::ui::icons::DOT, 7.0), text(name).size(12), ] .spacing(5) @@ -2080,28 +1930,35 @@ pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Ele let title_btn = button(title_inner) .on_press(Message::TabSwitch(idx)) .padding([5, 12]) - .style(move |_: &Theme, status| button::Style { - background: Some(Background::Color(match (is_active, status) { - (true, _) => TAB_ACTIVE, - (false, button::Status::Hovered) => TAB_HOVER, - _ => TAB_INACTIVE, - })), - text_color: if is_active { - TEXT_ACTIVE + .style(move |theme: &Theme, status| { + let palette = theme.extended_palette(); + let pair = if is_active { + palette.primary.weak } else { - TEXT_INACTIVE - }, - border: Border { - color: if is_active { - ACCENT + match status { + button::Status::Hovered => palette.background.weak, + _ => palette.background.base, + } + }; + button::Style { + background: Some(Background::Color(pair.color)), + text_color: if is_active { + pair.text } else { - Color::TRANSPARENT + palette.background.base.text.scale_alpha(0.72) }, - width: if is_active { 1.0 } else { 0.0 }, - radius: 0.0.into(), - }, - shadow: iced::Shadow::default(), - snap: false, + border: Border { + color: if is_active { + palette.primary.base.color + } else { + Color::TRANSPARENT + }, + width: if is_active { 1.0 } else { 0.0 }, + radius: 0.0.into(), + }, + shadow: iced::Shadow::default(), + snap: false, + } }); let title_btn: Element<'_, Message> = if tab.is_start { title_btn.into() @@ -2118,42 +1975,36 @@ pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Ele let row_inner: Row<'_, Message> = if tab.is_start { row![title_btn].spacing(0).align_y(iced::Center) } else { - let close_btn = button(crate::ui::icons::tinted( + let close_btn = button(crate::ui::icons::themed_secondary( crate::ui::icons::CLOSE, 10.0, - Color { - r: 0.55, - g: 0.55, - b: 0.55, - a: 1.0, - }, )) .on_press(Message::TabClose(idx)) .padding([3, 5]) - .style(move |_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered => CLOSE_HOVER, - _ => { - if is_active { - TAB_ACTIVE - } else { - TAB_INACTIVE - } - } - })), - border: Border { - radius: 3.0.into(), + .style(move |theme: &Theme, status| { + let palette = theme.extended_palette(); + let pair = match status { + button::Status::Hovered | button::Status::Pressed => palette.danger.weak, + _ if is_active => palette.primary.weak, + _ => palette.background.base, + }; + button::Style { + background: Some(Background::Color(pair.color)), + text_color: pair.text, + border: Border { + radius: 3.0.into(), + ..Default::default() + }, ..Default::default() - }, - ..Default::default() + } }); row![title_btn, close_btn].spacing(0).align_y(iced::Center) }; - let tab_container = container(row_inner).style(move |_: &Theme| container::Style { + let tab_container = container(row_inner).style(move |theme: &Theme| container::Style { border: Border { color: if is_active { - BORDER_COLOR + theme.extended_palette().background.neutral.color } else { Color::TRANSPARENT }, @@ -2179,33 +2030,20 @@ pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Ele items.push(tab_element); } - let new_btn = button(text("+").size(14).color(Color { - r: 0.65, - g: 0.65, - b: 0.65, - a: 1.0, - })) - .on_press(Message::TabNew) - .padding([4, 10]) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered => TAB_HOVER, - _ => Color::TRANSPARENT, - })), - border: Border { - radius: 0.0.into(), - ..Default::default() - }, - ..Default::default() - }); + let new_btn = button(text("+").size(14)) + .on_press(Message::TabNew) + .padding([4, 10]) + .style(button::subtle); items.push(new_btn.into()); container(WrapFlow::new(items).spacing_x(0.0).row_h(30.0)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BAR_BG)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.base.color, + )), border: Border { - color: BORDER_COLOR, + color: theme.extended_palette().background.neutral.color, width: 1.0, radius: 0.0.into(), }, @@ -2234,21 +2072,17 @@ pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Ele // fixed Start tab (`DocumentTab::is_start`). English-only by design — this // is the public welcome screen and stays consistent across locales. // -// The page picks up the application icon's red-brown (#B03020) as a tint so -// it visually belongs to OpenCADStudio without overpowering the dark workspace. +fn start_muted_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)), + } +} -const BRAND: Color = Color { - r: 0.690, - g: 0.188, - b: 0.125, - a: 1.0, -}; // #B03020 -const BRAND_DARK: Color = Color { - r: 0.45, - g: 0.12, - b: 0.08, - a: 1.0, -}; +fn start_primary_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().primary.base.color), + } +} /// Transparent input layer for one Model pane: a `mouse_area` filling the pane /// that emits pane-tagged viewport events (`idx` = the pane's tile index). The @@ -2270,7 +2104,6 @@ fn pane_mouse_area<'a>(idx: usize) -> Element<'a, Message> { /// Canvas that draws a label rotated 90° (for a collapsed panel's bar). struct VBarLabel { text: String, - color: Color, } impl canvas::Program for VBarLabel { @@ -2280,7 +2113,7 @@ impl canvas::Program for VBarLabel { &self, _state: &(), renderer: &iced::Renderer, - _theme: &Theme, + theme: &Theme, bounds: iced::Rectangle, _cursor: iced::advanced::mouse::Cursor, ) -> Vec { @@ -2291,7 +2124,7 @@ impl canvas::Program for VBarLabel { frame.fill_text(canvas::Text { content: self.text.clone(), position: iced::Point::ORIGIN, - color: self.color, + color: theme.extended_palette().background.base.text.scale_alpha(0.72), size: iced::Pixels(13.0), align_x: iced::advanced::text::Alignment::Center, align_y: iced::alignment::Vertical::Center, @@ -2306,28 +2139,8 @@ impl canvas::Program for VBarLabel { /// A collapsed panel rendered as a tall narrow bar with its name written along /// it, rotated 90°. Pressing it emits `on_press`. pub(super) fn collapse_bar<'a>(name: &str, on_press: Message) -> Element<'a, Message> { - const LABEL: Color = Color { - r: 0.72, - g: 0.72, - b: 0.74, - a: 1.0, - }; - const BAR_BG: Color = Color { - r: 0.13, - g: 0.13, - b: 0.14, - a: 1.0, - }; - const BAR_BORDER: Color = Color { - r: 0.22, - g: 0.22, - b: 0.24, - a: 1.0, - }; - let label = canvas(VBarLabel { text: name.to_string(), - color: LABEL, }) .width(Fill) .height(Fill); @@ -2336,10 +2149,12 @@ pub(super) fn collapse_bar<'a>(name: &str, on_press: Message) -> Element<'a, Mes container(label) .width(iced::Length::Fixed(26.0)) .height(Fill) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BAR_BG)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.base.color, + )), border: Border { - color: BAR_BORDER, + color: theme.extended_palette().background.neutral.color, width: 1.0, radius: 0.0.into(), }, @@ -2366,68 +2181,38 @@ pub(super) fn start_page_view<'a>( avail_w: f32, active: super::StartSection, ) -> Element<'a, Message> { - const TEXT: Color = Color { - r: 0.94, - g: 0.93, - b: 0.92, - a: 1.0, - }; - const CARD_BG: Color = Color { - r: 0.12, - g: 0.12, - b: 0.13, - a: 1.0, - }; - const CARD_BORDER: Color = Color { - r: 0.20, - g: 0.20, - b: 0.22, - a: 1.0, - }; - - let headline = text("Open CAD Studio").size(40).color(BRAND); + let headline = text("Open CAD Studio").size(40).style(start_primary_style); // Plain outlined button (Open / New / Help / Contribute). let outline_btn = |label: &'static str, msg: Message| { - button(text(label).size(14).color(TEXT)) + button(text(label).size(14)) .on_press(msg) .padding([10, 22]) - .style(move |_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered => Color { - r: 0.18, - g: 0.18, - b: 0.20, - a: 1.0, - }, - _ => Color { - r: 0.13, - g: 0.13, - b: 0.15, - a: 1.0, - }, - })), - text_color: TEXT, + .style(move |theme: &Theme, status| { + let palette = theme.extended_palette(); + let pair = match status { + button::Status::Hovered => palette.background.strong, + _ => palette.background.weak, + }; + button::Style { + background: Some(Background::Color(pair.color)), + text_color: pair.text, border: Border { - color: Color { - r: 0.30, - g: 0.30, - b: 0.33, - a: 1.0, - }, + color: palette.background.neutral.color, width: 1.0, radius: 6.0.into(), }, ..Default::default() + } }) }; - // Donate — the prominent call-to-action. Solid brand fill, white text. + // Donate — the prominent call-to-action, using the theme's danger role. let donate_btn = { button( row![ - crate::ui::icons::tinted(crate::ui::icons::HEART, 14.0, Color::WHITE), - text("Donate").size(14).color(Color::WHITE), + crate::ui::icons::themed_danger_text(crate::ui::icons::HEART, 14.0), + text("Donate").size(14), ] .spacing(5) .align_y(iced::Center), @@ -2437,29 +2222,7 @@ pub(super) fn start_page_view<'a>( event: crate::modules::ModuleEvent::Command("DONATE".to_string()), }) .padding([12, 28]) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered => BRAND_DARK, - _ => BRAND, - })), - text_color: Color::WHITE, - border: Border { - color: BRAND_DARK, - width: 1.0, - radius: 6.0.into(), - }, - shadow: iced::Shadow { - color: Color { - r: 0.0, - g: 0.0, - b: 0.0, - a: 0.4, - }, - offset: iced::Vector::new(0.0, 2.0), - blur_radius: 6.0, - }, - ..Default::default() - }) + .style(button::danger) }; let primary_row = WrapFlow::new(vec![ @@ -2487,42 +2250,15 @@ pub(super) fn start_page_view<'a>( // link to the web version. #[cfg(not(target_arch = "wasm32"))] { - // Bright ribbon blue (matches the active-tool accent), filled. + // Filled with the active theme's primary colour. secondary_items.push( - button(text("OCS Web").size(14).color(Color::WHITE)) + button(text("OCS Web").size(14)) .on_press(Message::RibbonToolClick { tool_id: "WEBVERSION".to_string(), event: crate::modules::ModuleEvent::Command("WEBVERSION".to_string()), }) .padding([10, 22]) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered => Color { - r: 0.15, - g: 0.45, - b: 0.78, - a: 1.0, - }, - _ => Color { - r: 0.20, - g: 0.55, - b: 0.90, - a: 1.0, - }, - })), - text_color: Color::WHITE, - border: Border { - color: Color { - r: 0.20, - g: 0.55, - b: 0.90, - a: 1.0, - }, - width: 1.0, - radius: 6.0.into(), - }, - ..Default::default() - }) + .style(button::primary) .into(), ); } @@ -2530,7 +2266,7 @@ pub(super) fn start_page_view<'a>( .spacing_x(12.0) .row_h(44.0); - // Buttons sit on a transparent container with a large, brand-tinted + // Buttons sit on a transparent container with a large, primary-tinted // ambient shadow (offset = 0, big blur) — produces a soft halo behind // the action row, matching the Thunderbird coloured-glow look against // the dark page. @@ -2541,15 +2277,10 @@ pub(super) fn start_page_view<'a>( bottom: 4.0, left: 8.0, }) - .style(|_: &Theme| container::Style { + .style(|theme: &Theme| container::Style { background: Some(Background::Color(Color::TRANSPARENT)), shadow: iced::Shadow { - color: Color { - r: BRAND.r, - g: BRAND.g, - b: BRAND.b, - a: 0.45, - }, + color: theme.extended_palette().primary.base.color.scale_alpha(0.45), offset: iced::Vector::ZERO, blur_radius: 80.0, }, @@ -2569,15 +2300,6 @@ pub(super) fn start_page_view<'a>( .width(Fill) .height(Fill); - // Page background reverts to plain dark — the glow alone provides the - // brand colour cue, the rest of the page stays neutral so it reads as - // "workspace area" not "advertising banner". - const PAGE_BG: Color = Color { - r: 0.08, - g: 0.08, - b: 0.085, - a: 1.0, - }; // Collapse side panels one at a time as width shrinks: Tutorials first, // then Supporters, and Recent Documents last. The previous all-or-nothing // threshold reserved a videos-sized empty margin on both sides of the @@ -2638,15 +2360,9 @@ pub(super) fn start_page_view<'a>( // Tutorial-videos rail: the official playlist, fetched at boot (cached on // disk) — thumbnail card + title per video, click opens the browser. let videos_panel: Element<'a, Message> = { - const NAME_COLOR: Color = Color { - r: 0.80, - g: 0.80, - b: 0.82, - a: 1.0, - }; // Inner width = panel 300 − padding 2×16 − scrollbar gutter 14. const THUMB_H: f32 = (300.0 - 32.0 - 14.0) * 9.0 / 16.0; - let mut list = column![text("Tutorials").size(15).color(TEXT)] + let mut list = column![text("Tutorials").size(15)] .spacing(10) .width(Fill) // Keep the scrollbar off the thumbnails. @@ -2666,9 +2382,9 @@ pub(super) fn start_page_view<'a>( ) .width(Fill) .height(iced::Length::Fixed(THUMB_H)) - .style(|_: &Theme| container::Style { + .style(|theme: &Theme| container::Style { border: Border { - color: CARD_BORDER, + color: theme.extended_palette().background.neutral.color, width: 1.0, radius: 6.0.into(), }, @@ -2677,7 +2393,7 @@ pub(super) fn start_page_view<'a>( .clip(true), ); } - card = card.push(text(v.title.clone()).size(12).color(NAME_COLOR)); + card = card.push(text(v.title.clone()).size(12).style(start_muted_style)); list = list.push( mouse_area(card) .interaction(iced::mouse::Interaction::Pointer) @@ -2690,28 +2406,25 @@ pub(super) fn start_page_view<'a>( } else { "Videos load from the internet." }; - list = list.push(text(note).size(12).color(NAME_COLOR)); + list = list.push(text(note).size(12).style(start_muted_style)); } let playlist_btn = mouse_area( - container( - text("Open playlist on YouTube").size(12).color(Color::WHITE), - ) + container(text("Open playlist on YouTube").size(12)) .padding([6, 10]) .width(Fill) .center_x(Fill) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { - r: 0.85, - g: 0.15, - b: 0.15, - a: 1.0, - })), + .style(|theme: &Theme| { + let pair = theme.extended_palette().danger.base; + container::Style { + background: Some(Background::Color(pair.color)), border: Border { color: Color::TRANSPARENT, width: 0.0, radius: 6.0.into(), }, + text_color: Some(pair.text), ..Default::default() + } }), ) .interaction(iced::mouse::Interaction::Pointer) @@ -2729,14 +2442,17 @@ pub(super) fn start_page_view<'a>( }) .height(Fill) .padding(16) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(CARD_BG)), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.weak.color)), border: Border { - color: CARD_BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 8.0.into(), }, ..Default::default() + } }) .into() }; @@ -2745,14 +2461,8 @@ pub(super) fn start_page_view<'a>( // (no token configured / offline) only the "Support on Patreon" button // shows, so the rail always invites support. let supporters: Element<'a, Message> = { - const NAME_COLOR: Color = Color { - r: 0.78, - g: 0.78, - b: 0.80, - a: 1.0, - }; let mut list = column![ - text("Supporters").size(15).color(TEXT), + text("Supporters").size(15), Space::new().height(iced::Length::Fixed(12.0)), ] .spacing(6) @@ -2763,8 +2473,8 @@ pub(super) fn start_page_view<'a>( let amount = format!("${:.2}", *cents as f64 / 100.0); list = list.push( iced::widget::row![ - text(name).size(12).color(NAME_COLOR).width(Fill), - text(amount).size(12).color(NAME_COLOR), + text(name).size(12).style(start_muted_style).width(Fill), + text(amount).size(12).style(start_muted_style), ] .spacing(6), ); @@ -2772,8 +2482,8 @@ pub(super) fn start_page_view<'a>( let support_btn = mouse_area( container( iced::widget::row![ - crate::ui::icons::tinted(crate::ui::icons::HEART, 13.0, Color::WHITE), - text("Support on Patreon").size(12).color(Color::WHITE), + crate::ui::icons::themed_danger_text(crate::ui::icons::HEART, 13.0), + text("Support on Patreon").size(12), ] .spacing(6) .align_y(iced::Center), @@ -2781,19 +2491,18 @@ pub(super) fn start_page_view<'a>( .padding([6, 10]) .width(Fill) .center_x(Fill) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { - r: 0.90, - g: 0.28, - b: 0.30, - a: 1.0, - })), + .style(|theme: &Theme| { + let pair = theme.extended_palette().danger.base; + container::Style { + background: Some(Background::Color(pair.color)), border: Border { color: Color::TRANSPARENT, width: 0.0, radius: 6.0.into(), }, + text_color: Some(pair.text), ..Default::default() + } }), ) .interaction(iced::mouse::Interaction::Pointer) @@ -2814,14 +2523,17 @@ pub(super) fn start_page_view<'a>( }) .height(Fill) .padding(20) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(CARD_BG)), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.weak.color)), border: Border { - color: CARD_BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 8.0.into(), }, ..Default::default() + } }) .into() }; @@ -2852,38 +2564,31 @@ pub(super) fn start_page_view<'a>( button(text(label).size(14)) .on_press(Message::StartSectionSelect(section)) .padding([8, 18]) - .style(move |_: &Theme, status| button::Style { - background: Some(Background::Color(match (is_active, status) { - (true, _) => Color { - r: 0.18, - g: 0.18, - b: 0.20, - a: 1.0, - }, - (false, button::Status::Hovered) => Color { - r: 0.14, - g: 0.14, - b: 0.16, - a: 1.0, - }, - _ => Color::TRANSPARENT, - })), - text_color: if is_active { - TEXT - } else { - Color { - r: 0.62, - g: 0.62, - b: 0.62, - a: 1.0, + .style(move |theme: &Theme, status| { + let palette = theme.extended_palette(); + let pair = match (is_active, status) { + (true, _) => Some(palette.primary.weak), + (false, button::Status::Hovered) => { + Some(palette.background.strong) } - }, + _ => None, + }; + button::Style { + background: pair.map(|p| Background::Color(p.color)), + text_color: pair + .map(|p| p.text) + .unwrap_or(palette.background.base.text.scale_alpha(0.68)), border: Border { - color: if is_active { BRAND } else { Color::TRANSPARENT }, + color: if is_active { + palette.primary.base.color + } else { + Color::TRANSPARENT + }, width: if is_active { 1.0 } else { 0.0 }, radius: 6.0.into(), }, ..Default::default() + } }) }; let tab_bar = WrapFlow::new(vec![ @@ -2924,8 +2629,10 @@ pub(super) fn start_page_view<'a>( }; container(body) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PAGE_BG)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.base.color + )), ..Default::default() }) .padding(iced::Padding { @@ -2955,44 +2662,16 @@ pub(super) fn recent_files_panel<'a>( limit_input: &'a str, width: iced::Length, ) -> Element<'a, Message> { - // Card chrome matches the Supporters rail (the canonical start-page card). - const PANEL_BG: Color = Color { - r: 0.12, - g: 0.12, - b: 0.13, - a: 1.0, - }; - const PANEL_BORDER: Color = Color { - r: 0.20, - g: 0.20, - b: 0.22, - a: 1.0, - }; - const ITEM_HOVER: Color = Color { - r: 0.16, - g: 0.16, - b: 0.18, - a: 1.0, - }; - const TEXT: Color = Color { - r: 0.94, - g: 0.93, - b: 0.92, - a: 1.0, - }; - const MUTED: Color = Color { - r: 0.60, - g: 0.60, - b: 0.62, - a: 1.0, - }; - // Title mirrors the Supporters rail: size 15 in the bright text colour, // followed by a 12px gap before the content. - let title = text("Recent Documents").size(15).color(TEXT); + let title = text("Recent Documents").size(15); let body: Element<'a, Message> = if recents.is_empty() { - container(text("Files you open will show up here.").size(12).color(MUTED)) + container( + text("Files you open will show up here.") + .size(12) + .style(start_muted_style) + ) .height(Fill) .into() } else { @@ -3033,11 +2712,10 @@ pub(super) fn recent_files_panel<'a>( thumb, column![ text(crate::ui::text_util::elide(&name, 28)) - .size(12) - .color(TEXT), + .size(12), text(crate::ui::text_util::elide(&dir, 38)) .size(10) - .color(MUTED), + .style(start_muted_style), ] .spacing(2), ] @@ -3047,41 +2725,42 @@ pub(super) fn recent_files_panel<'a>( .on_press(Message::OpenRecent(path_for_open)) .padding([6, 12]) .width(Fill) - .style(move |_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered => ITEM_HOVER, - _ => Color::TRANSPARENT, - })), - text_color: TEXT, + .style(move |theme: &Theme, status| { + let palette = theme.extended_palette(); + button::Style { + background: matches!(status, button::Status::Hovered).then_some( + Background::Color(palette.background.strong.color) + ), + text_color: palette.background.base.text, border: Border { color: Color::TRANSPARENT, width: 0.0, radius: 0.0.into(), }, ..Default::default() + } }); let path_for_remove = path.clone(); - let remove_btn = button(crate::ui::icons::tinted(crate::ui::icons::CLOSE, 11.0, MUTED)) + let remove_btn = button(crate::ui::icons::themed_secondary( + crate::ui::icons::CLOSE, + 11.0, + )) .on_press(Message::RecentRemove(path_for_remove)) .padding([4, 8]) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered => Color { - r: 0.45, - g: 0.15, - b: 0.15, - a: 1.0, - }, - _ => Color::TRANSPARENT, - })), - text_color: MUTED, + .style(|theme: &Theme, status| { + let palette = theme.extended_palette(); + button::Style { + background: matches!(status, button::Status::Hovered) + .then_some(Background::Color(palette.danger.weak.color)), + text_color: palette.background.base.text.scale_alpha(0.68), border: Border { color: Color::TRANSPARENT, width: 0.0, radius: 3.0.into(), }, ..Default::default() + } }); col = col.push(row![open_btn, remove_btn].spacing(0).align_y(iced::Center)); @@ -3094,18 +2773,20 @@ pub(super) fn recent_files_panel<'a>( // update handler clamps to [MIN, MAX] and persists (see `set_recent_limit`), // so an over-max entry snaps to the max. const STEP: usize = 5; - let step_style = |_: &Theme, status: button::Status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered => ITEM_HOVER, - _ => Color::TRANSPARENT, - })), - text_color: TEXT, + let step_style = |theme: &Theme, status: button::Status| { + let palette = theme.extended_palette(); + button::Style { + background: matches!(status, button::Status::Hovered).then_some( + Background::Color(palette.background.strong.color) + ), + text_color: palette.background.base.text, border: Border { - color: PANEL_BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 4.0.into(), }, ..Default::default() + } }; // +/- step from whatever is currently shown in the box (mid-edit included). let shown = limit_input.parse::().unwrap_or(limit); @@ -3116,19 +2797,19 @@ pub(super) fn recent_files_panel<'a>( .padding([2, 6]) .width(iced::Length::Fixed(46.0)); let limit_row = row![ - text("Keep recent files").size(11).color(MUTED).width(Fill), - button(crate::ui::icons::tinted(crate::ui::icons::MINUS, 11.0, TEXT)) + text("Keep recent files").size(11).style(start_muted_style).width(Fill), + button(crate::ui::icons::themed(crate::ui::icons::MINUS, 11.0)) .on_press(Message::SetRecentLimit(shown.saturating_sub(STEP))) .padding([3, 6]) .style(step_style), count_box, - button(crate::ui::icons::tinted(crate::ui::icons::PLUS, 11.0, TEXT)) + button(crate::ui::icons::themed(crate::ui::icons::PLUS, 11.0)) .on_press(Message::SetRecentLimit(shown + STEP)) .padding([3, 6]) .style(step_style), text(format!("/ {}", super::recent::RECENT_MAX)) .size(11) - .color(MUTED), + .style(start_muted_style), ] .spacing(6) .align_y(iced::Center); @@ -3146,14 +2827,17 @@ pub(super) fn recent_files_panel<'a>( .width(width) .height(Fill) .padding(20) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.weak.color)), border: Border { - color: PANEL_BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 8.0.into(), }, ..Default::default() + } }) .into() } diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 45a2eea0..9e4f6b04 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -1,6 +1,6 @@ use super::super::{Message, OpenCADStudio}; use iced::widget::{button, column, container, pick_list, row, text, Space}; -use iced::{Background, Border, Color, Element, Fill, Theme}; +use iced::{Background, Element, Fill, Theme}; impl OpenCADStudio { /// Title shown in the active modal's title bar, left of the move/close @@ -63,9 +63,13 @@ impl OpenCADStudio { sized(crate::ui::window::alias_editor::view_window(&self.alias_editor_rows), 480, 520) } super::super::ModalKind::Options => sized( - crate::ui::window::options::view_window(&self.default_save_format), - 480, - 190, + crate::ui::window::options::view_window( + &self.default_save_format, + &self.ui_theme, + &self.theme_color_inputs, + ), + 520, + 500, ), super::super::ModalKind::PluginManager => sized( crate::ui::window::plugin_manager::view_window( @@ -712,125 +716,56 @@ impl OpenCADStudio { } } +fn dialog_button( + label: &'static str, + message: Message, + style: fn(&Theme, button::Status) -> button::Style, +) -> Element<'static, Message> { + button(text(label).size(13)) + .on_press(message) + .style(style) + .padding([6, 18]) + .into() +} + +fn dialog_body_style(theme: &Theme) -> container::Style { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.base.color)), + text_color: Some(palette.background.base.text), + ..Default::default() + } +} + +fn dialog_muted_text_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)), + } +} + /// Compact Save-As options dialog: pick the format/version and a default file /// name. The destination folder and overwrite confirmation come from the /// native OS save dialog (native) or the browser download (web) that follows. fn save_as_dialog_window<'a>(filename: &'a str, format: &'a str) -> Element<'a, Message> { - const BG: Color = Color { - r: 0.15, - g: 0.15, - b: 0.17, - a: 1.0, - }; - const BORDER: Color = Color { - r: 0.32, - g: 0.32, - b: 0.36, - a: 1.0, - }; - const TEXT: Color = Color { - r: 0.90, - g: 0.90, - b: 0.90, - a: 1.0, - }; - const DIM: Color = Color { - r: 0.58, - g: 0.58, - b: 0.62, - a: 1.0, - }; - const BTN_OK: Color = Color { - r: 0.20, - g: 0.46, - b: 0.80, - a: 1.0, - }; - const BTN_HOV: Color = Color { - r: 0.26, - g: 0.55, - b: 0.92, - a: 1.0, - }; - const BTN_GREY: Color = Color { - r: 0.26, - g: 0.26, - b: 0.29, - a: 1.0, - }; - const BTN_GHOV: Color = Color { - r: 0.34, - g: 0.34, - b: 0.38, - a: 1.0, - }; - - let btn = |lbl: &'static str, msg: Message, base: Color, hov: Color| { - button(text(lbl).size(12).color(TEXT)) - .on_press(msg) - .style(move |_: &Theme, st| button::Style { - background: Some(Background::Color( - if matches!(st, button::Status::Hovered | button::Status::Pressed) { - hov - } else { - base - }, - )), - text_color: TEXT, - border: Border { - color: BORDER, - width: 1.0, - radius: 4.0.into(), - }, - ..Default::default() - }) - .padding([4, 12]) - }; - let sel_fmt = crate::io::SAVE_FORMAT_OPTIONS .iter() .copied() .find(|&s| s == format); - let label = |s: &'static str| text(s).size(11).color(DIM); + let label = |s: &'static str| text(s).size(11).style(dialog_muted_text_style); let mut items: Vec> = Vec::new(); - items.push(text("Save Drawing As").size(14).color(TEXT).into()); + items.push(text("Save Drawing As").size(14).into()); items.push(Space::new().height(12).into()); // Web has no native file dialog, so the file name is typed here. On native // the OS save dialog collects the name, so this field is omitted. #[cfg(target_arch = "wasm32")] { - const INPUT_BG: Color = Color { - r: 0.10, - g: 0.10, - b: 0.12, - a: 1.0, - }; - let input_sty = - |_: &Theme, _: iced::widget::text_input::Status| iced::widget::text_input::Style { - background: Background::Color(INPUT_BG), - border: Border { - color: BORDER, - width: 1.0, - radius: 4.0.into(), - }, - icon: TEXT, - placeholder: DIM, - value: TEXT, - selection: Color { - r: 0.20, - g: 0.46, - b: 0.80, - a: 0.45, - }, - }; items.push( row![ label("File name:").width(70), iced::widget::text_input("drawing.dwg", filename) .on_input(Message::SaveDialogFilenameChanged) - .style(input_sty) .size(13) .padding([5, 8]) .width(Fill), @@ -860,9 +795,9 @@ fn save_as_dialog_window<'a>(filename: &'a str, format: &'a str) -> Element<'a, items.push( row![ Space::new().width(Fill), - btn("Save as...", Message::SaveDialogConfirm, BTN_OK, BTN_HOV), + dialog_button("Save as...", Message::SaveDialogConfirm, button::primary), Space::new().width(8), - btn("Cancel", Message::SaveDialogCancel, BTN_GREY, BTN_GHOV), + dialog_button("Cancel", Message::SaveDialogCancel, button::secondary), ] .into(), ); @@ -870,10 +805,7 @@ fn save_as_dialog_window<'a>(filename: &'a str, format: &'a str) -> Element<'a, let body = column(items).spacing(0); container(body) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BG)), - ..Default::default() - }) + .style(dialog_body_style) .padding([14, 16]) .width(Fill) .height(Fill) @@ -881,89 +813,23 @@ fn save_as_dialog_window<'a>(filename: &'a str, format: &'a str) -> Element<'a, } fn unsaved_changes_dialog_window(name: &str) -> Element<'static, Message> { - const BG: Color = Color { - r: 0.18, - g: 0.18, - b: 0.20, - a: 1.0, - }; - const BORDER_COL: Color = Color { - r: 0.38, - g: 0.38, - b: 0.42, - a: 1.0, - }; - const TEXT_COL: Color = Color { - r: 0.90, - g: 0.90, - b: 0.90, - a: 1.0, - }; - const BTN_SAVE: Color = Color { - r: 0.20, - g: 0.46, - b: 0.80, - a: 1.0, - }; - const BTN_HOVER: Color = Color { - r: 0.26, - g: 0.55, - b: 0.92, - a: 1.0, - }; - const BTN_DISC: Color = Color { - r: 0.28, - g: 0.28, - b: 0.30, - a: 1.0, - }; - const BTN_DHOV: Color = Color { - r: 0.36, - g: 0.36, - b: 0.40, - a: 1.0, - }; - let body_text = format!("Do you want to save changes to \"{}\"?", name); - let btn = |label: &'static str, msg: Message, base: Color, hov: Color| { - button(text(label).size(13).color(TEXT_COL)) - .on_press(msg) - .style(move |_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => hov, - _ => base, - })), - text_color: TEXT_COL, - border: Border { - color: BORDER_COL, - width: 1.0, - radius: 4.0.into(), - }, - shadow: iced::Shadow::default(), - snap: false, - }) - .padding([6, 18]) - }; - container( column![ - text(body_text).size(13).color(TEXT_COL), + text(body_text).size(13), iced::widget::Space::new().height(20), row![ - btn("Save", Message::UnsavedDialogSave, BTN_SAVE, BTN_HOVER), + dialog_button("Save", Message::UnsavedDialogSave, button::primary), iced::widget::Space::new().width(8), - btn("Discard", Message::UnsavedDialogDiscard, BTN_DISC, BTN_DHOV), + dialog_button("Discard", Message::UnsavedDialogDiscard, button::danger), iced::widget::Space::new().width(8), - btn("Cancel", Message::UnsavedDialogCancel, BTN_DISC, BTN_DHOV), + dialog_button("Cancel", Message::UnsavedDialogCancel, button::secondary), ], ] .spacing(0), ) - .style(move |_: &Theme| container::Style { - background: Some(Background::Color(BG)), - ..Default::default() - }) + .style(dialog_body_style) .center(Fill) .padding([24, 28]) .into() @@ -971,55 +837,6 @@ fn unsaved_changes_dialog_window(name: &str) -> Element<'static, Message> { #[cfg(not(target_arch = "wasm32"))] fn file_in_use_dialog_window(path: &str, error: &str) -> Element<'static, Message> { - const BG: Color = Color { - r: 0.18, - g: 0.18, - b: 0.20, - a: 1.0, - }; - const BORDER_COL: Color = Color { - r: 0.38, - g: 0.38, - b: 0.42, - a: 1.0, - }; - const TEXT_COL: Color = Color { - r: 0.90, - g: 0.90, - b: 0.90, - a: 1.0, - }; - const DIM_COL: Color = Color { - r: 0.62, - g: 0.62, - b: 0.66, - a: 1.0, - }; - const BTN_PRIMARY: Color = Color { - r: 0.20, - g: 0.46, - b: 0.80, - a: 1.0, - }; - const BTN_PRIMARY_HOVER: Color = Color { - r: 0.26, - g: 0.55, - b: 0.92, - a: 1.0, - }; - const BTN_SECONDARY: Color = Color { - r: 0.28, - g: 0.28, - b: 0.30, - a: 1.0, - }; - const BTN_SECONDARY_HOVER: Color = Color { - r: 0.36, - g: 0.36, - b: 0.40, - a: 1.0, - }; - let file_name = std::path::Path::new(path) .file_name() .map(|name| name.to_string_lossy().into_owned()) @@ -1027,70 +844,45 @@ fn file_in_use_dialog_window(path: &str, error: &str) -> Element<'static, Messag let heading = format!("\"{file_name}\" could not be saved."); let path_line = format!("Path: {path}"); let details = format!("Details: {error}"); - let btn = |label: &'static str, msg: Message, base: Color, hover: Color| { - button(text(label).size(13).color(TEXT_COL)) - .on_press(msg) - .style(move |_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => hover, - _ => base, - })), - text_color: TEXT_COL, - border: Border { - color: BORDER_COL, - width: 1.0, - radius: 4.0.into(), - }, - ..Default::default() - }) - .padding([6, 18]) - }; container( column![ - text(heading).size(14).color(TEXT_COL), + text(heading).size(14), Space::new().height(8), text( "The file is open or being used by another application. \ Close it there and retry, or save this drawing under a different name." ) .size(13) - .color(TEXT_COL) .width(Fill), Space::new().height(12), - text(path_line).size(11).color(DIM_COL).width(Fill), + text(path_line).size(11).style(dialog_muted_text_style).width(Fill), Space::new().height(4), - text(details).size(11).color(DIM_COL).width(Fill), + text(details).size(11).style(dialog_muted_text_style).width(Fill), Space::new().height(18), row![ - btn( + dialog_button( "Retry", Message::SaveFileInUseRetry, - BTN_PRIMARY, - BTN_PRIMARY_HOVER + button::primary ), Space::new().width(8), - btn( + dialog_button( "Save As", Message::SaveFileInUseSaveAs, - BTN_SECONDARY, - BTN_SECONDARY_HOVER + button::secondary ), Space::new().width(8), - btn( + dialog_button( "Cancel", Message::SaveFileInUseCancel, - BTN_SECONDARY, - BTN_SECONDARY_HOVER + button::secondary ), ], ] .spacing(0), ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BG)), - ..Default::default() - }) + .style(dialog_body_style) .padding([18, 20]) .width(Fill) .height(Fill) @@ -1099,142 +891,55 @@ fn file_in_use_dialog_window(path: &str, error: &str) -> Element<'static, Messag #[cfg(not(target_arch = "wasm32"))] fn external_change_dialog_window(path: &str) -> Element<'static, Message> { - const BG: Color = Color { - r: 0.18, - g: 0.18, - b: 0.20, - a: 1.0, - }; - const BORDER_COL: Color = Color { - r: 0.38, - g: 0.38, - b: 0.42, - a: 1.0, - }; - const TEXT_COL: Color = Color { - r: 0.90, - g: 0.90, - b: 0.90, - a: 1.0, - }; - const DIM_COL: Color = Color { - r: 0.62, - g: 0.62, - b: 0.66, - a: 1.0, - }; - const BTN_PRIMARY: Color = Color { - r: 0.20, - g: 0.46, - b: 0.80, - a: 1.0, - }; - const BTN_PRIMARY_HOVER: Color = Color { - r: 0.26, - g: 0.55, - b: 0.92, - a: 1.0, - }; - const BTN_SECONDARY: Color = Color { - r: 0.28, - g: 0.28, - b: 0.30, - a: 1.0, - }; - const BTN_SECONDARY_HOVER: Color = Color { - r: 0.36, - g: 0.36, - b: 0.40, - a: 1.0, - }; - const BTN_DANGER: Color = Color { - r: 0.62, - g: 0.31, - b: 0.14, - a: 1.0, - }; - const BTN_DANGER_HOVER: Color = Color { - r: 0.78, - g: 0.39, - b: 0.17, - a: 1.0, - }; - let file_name = std::path::Path::new(path) .file_name() .map(|name| name.to_string_lossy().into_owned()) .unwrap_or_else(|| "Drawing".to_string()); let heading = format!("\"{file_name}\" was changed by another application."); let path_line = format!("Path: {path}"); - let btn = |label: &'static str, msg: Message, base: Color, hover: Color| { - button(text(label).size(13).color(TEXT_COL)) - .on_press(msg) - .style(move |_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => hover, - _ => base, - })), - text_color: TEXT_COL, - border: Border { - color: BORDER_COL, - width: 1.0, - radius: 4.0.into(), - }, - ..Default::default() - }) - .padding([6, 14]) - }; container( column![ - text(heading).size(14).color(TEXT_COL), + text(heading).size(14), Space::new().height(8), text( "Saving now could destroy those external changes. Reload the disk copy, \ save your local work elsewhere, or explicitly overwrite it." ) .size(13) - .color(TEXT_COL) .width(Fill), Space::new().height(12), - text(path_line).size(11).color(DIM_COL).width(Fill), + text(path_line).size(11).style(dialog_muted_text_style).width(Fill), Space::new().height(18), row![ - btn( + dialog_button( "Reload from Disk", Message::ExternalChangeReload, - BTN_PRIMARY, - BTN_PRIMARY_HOVER + button::primary ), Space::new().width(8), - btn( + dialog_button( "Save As", Message::ExternalChangeSaveAs, - BTN_SECONDARY, - BTN_SECONDARY_HOVER + button::secondary ), Space::new().width(8), - btn( + dialog_button( "Overwrite", Message::ExternalChangeOverwrite, - BTN_DANGER, - BTN_DANGER_HOVER + button::danger ), Space::new().width(8), - btn( + dialog_button( "Cancel", Message::ExternalChangeCancel, - BTN_SECONDARY, - BTN_SECONDARY_HOVER + button::secondary ), ], ] .spacing(0), ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BG)), - ..Default::default() - }) + .style(dialog_body_style) .padding([18, 20]) .width(Fill) .height(Fill) @@ -1246,58 +951,31 @@ fn external_change_dialog_window(path: &str) -> Element<'static, Message> { /// bytes, so saving to a different version or to DXF would drop them. Offers to /// save in the source version (keep them) or proceed (drop them). fn aec_drop_dialog_window(count: usize, target: &str, src_version: &str) -> Element<'static, Message> { - const BG: Color = Color { r: 0.18, g: 0.18, b: 0.20, a: 1.0 }; - const BORDER_COL: Color = Color { r: 0.38, g: 0.38, b: 0.42, a: 1.0 }; - const TEXT_COL: Color = Color { r: 0.90, g: 0.90, b: 0.90, a: 1.0 }; - const BTN_SAVE: Color = Color { r: 0.20, g: 0.46, b: 0.80, a: 1.0 }; - const BTN_HOVER: Color = Color { r: 0.26, g: 0.55, b: 0.92, a: 1.0 }; - const BTN_DISC: Color = Color { r: 0.28, g: 0.28, b: 0.30, a: 1.0 }; - const BTN_DHOV: Color = Color { r: 0.36, g: 0.36, b: 0.40, a: 1.0 }; - let body_text = format!( "This drawing contains {count} AEC/Civil objects that \"{target}\" \ cannot store, so they will not be saved.\n\n\ To keep them, save in the source version ({src_version})." ); - let btn = |label: &'static str, msg: Message, base: Color, hov: Color| { - button(text(label).size(13).color(TEXT_COL)) - .on_press(msg) - .style(move |_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => hov, - _ => base, - })), - text_color: TEXT_COL, - border: Border { - color: BORDER_COL, - width: 1.0, - radius: 4.0.into(), - }, - shadow: iced::Shadow::default(), - snap: false, - }) - .padding([6, 14]) - }; - container( column![ - text(body_text).size(13).color(TEXT_COL), + text(body_text).size(13), iced::widget::Space::new().height(20), row![ - btn("Save in source version", Message::AecDropSameVersion, BTN_SAVE, BTN_HOVER), + dialog_button( + "Save in source version", + Message::AecDropSameVersion, + button::primary + ), iced::widget::Space::new().width(8), - btn("Save anyway", Message::AecDropProceed, BTN_DISC, BTN_DHOV), + dialog_button("Save anyway", Message::AecDropProceed, button::warning), iced::widget::Space::new().width(8), - btn("Back", Message::AecDropBack, BTN_DISC, BTN_DHOV), + dialog_button("Back", Message::AecDropBack, button::secondary), ], ] .spacing(0), ) - .style(move |_: &Theme| container::Style { - background: Some(Background::Color(BG)), - ..Default::default() - }) + .style(dialog_body_style) .center(Fill) .padding([24, 28]) .into() @@ -1308,14 +986,6 @@ fn aec_drop_dialog_window(count: usize, target: &str, src_version: &str) -> Elem /// Confirm deleting layer(s) that still have objects on them. "Delete Objects" /// erases them and removes the layers; "Cancel" leaves everything. fn layer_delete_warning_window(names: &[String], count: usize) -> Element<'static, Message> { - const BG: Color = Color { r: 0.18, g: 0.18, b: 0.20, a: 1.0 }; - const BORDER_COL: Color = Color { r: 0.38, g: 0.38, b: 0.42, a: 1.0 }; - const TEXT_COL: Color = Color { r: 0.90, g: 0.90, b: 0.90, a: 1.0 }; - const BTN_DEL: Color = Color { r: 0.72, g: 0.26, b: 0.24, a: 1.0 }; - const BTN_DEL_HOV: Color = Color { r: 0.84, g: 0.32, b: 0.30, a: 1.0 }; - const BTN_CANCEL: Color = Color { r: 0.28, g: 0.28, b: 0.30, a: 1.0 }; - const BTN_CANCEL_HOV: Color = Color { r: 0.36, g: 0.36, b: 0.40, a: 1.0 }; - let obj = if count == 1 { "object" } else { "objects" }; let subject = if names.len() == 1 { format!("Layer \"{}\"", names[0]) @@ -1328,38 +998,23 @@ fn layer_delete_warning_window(names: &[String], count: usize) -> Element<'stati if count == 1 { "that object" } else { "those objects" } ); - let btn = |label: &'static str, msg: Message, base: Color, hov: Color| { - button(text(label).size(13).color(TEXT_COL)) - .on_press(msg) - .style(move |_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => hov, - _ => base, - })), - text_color: TEXT_COL, - border: Border { color: BORDER_COL, width: 1.0, radius: 4.0.into() }, - shadow: iced::Shadow::default(), - snap: false, - }) - .padding([6, 18]) - }; - container( column![ - text(body_text).size(13).color(TEXT_COL), + text(body_text).size(13), iced::widget::Space::new().height(20), row![ - btn("Delete Objects", Message::LayerDeleteConfirm, BTN_DEL, BTN_DEL_HOV), + dialog_button( + "Delete Objects", + Message::LayerDeleteConfirm, + button::danger + ), iced::widget::Space::new().width(8), - btn("Cancel", Message::CloseModal, BTN_CANCEL, BTN_CANCEL_HOV), + dialog_button("Cancel", Message::CloseModal, button::secondary), ], ] .spacing(0), ) - .style(move |_: &Theme| container::Style { - background: Some(Background::Color(BG)), - ..Default::default() - }) + .style(dialog_body_style) .center(Fill) .padding([24, 28]) .into() @@ -1370,99 +1025,30 @@ fn layer_delete_warning_window(names: &[String], count: usize) -> Element<'stati /// just dismisses. Either answer flips the persisted `default_assoc_prompted` /// flag so the dialog never reappears. fn default_assoc_dialog_window() -> Element<'static, Message> { - const BG: Color = Color { - r: 0.18, - g: 0.18, - b: 0.20, - a: 1.0, - }; - const BORDER_COL: Color = Color { - r: 0.38, - g: 0.38, - b: 0.42, - a: 1.0, - }; - const TEXT_COL: Color = Color { - r: 0.90, - g: 0.90, - b: 0.90, - a: 1.0, - }; - const DIM_COL: Color = Color { - r: 0.62, - g: 0.62, - b: 0.66, - a: 1.0, - }; - const BTN_YES: Color = Color { - r: 0.20, - g: 0.46, - b: 0.80, - a: 1.0, - }; - const BTN_YHOV: Color = Color { - r: 0.26, - g: 0.55, - b: 0.92, - a: 1.0, - }; - const BTN_NO: Color = Color { - r: 0.28, - g: 0.28, - b: 0.30, - a: 1.0, - }; - const BTN_NHOV: Color = Color { - r: 0.36, - g: 0.36, - b: 0.40, - a: 1.0, - }; - - let btn = |label: &'static str, msg: Message, base: Color, hov: Color| { - button(text(label).size(13).color(TEXT_COL)) - .on_press(msg) - .style(move |_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => hov, - _ => base, - })), - text_color: TEXT_COL, - border: Border { - color: BORDER_COL, - width: 1.0, - radius: 4.0.into(), - }, - shadow: iced::Shadow::default(), - snap: false, - }) - .padding([6, 18]) - }; - container( column![ text("Make Open CAD Studio your default CAD app?") - .size(15) - .color(TEXT_COL), + .size(15), iced::widget::Space::new().height(10), text("Open .dwg and .dxf drawings in Open CAD Studio by default. You can change this later in your system settings.") .size(12) - .color(DIM_COL), + .style(dialog_muted_text_style), iced::widget::Space::new().height(22), row![ iced::widget::Space::new().width(Fill), - btn("Not now", Message::AssocPromptNo, BTN_NO, BTN_NHOV), + dialog_button("Not now", Message::AssocPromptNo, button::secondary), iced::widget::Space::new().width(8), - btn("Yes, set as default", Message::AssocPromptYes, BTN_YES, BTN_YHOV), + dialog_button( + "Yes, set as default", + Message::AssocPromptYes, + button::primary + ), ] .align_y(iced::Center), ] .spacing(0), ) - .style(move |_: &Theme| container::Style { - background: Some(Background::Color(BG)), - ..Default::default() - }) + .style(dialog_body_style) .center(Fill) .padding([24, 28]) .into() diff --git a/src/app/view/overlay.rs b/src/app/view/overlay.rs index d9970875..13404472 100644 --- a/src/app/view/overlay.rs +++ b/src/app/view/overlay.rs @@ -26,19 +26,6 @@ pub(super) fn text_inline_overlay( ed: &super::super::text_inline::TextInlineState, canvas: (f32, f32), ) -> Element<'_, Message> { - const PANEL_BG: Color = Color { - r: 0.16, - g: 0.16, - b: 0.16, - a: 0.98, - }; - const BORDER: Color = Color { - r: 0.40, - g: 0.40, - b: 0.40, - a: 1.0, - }; - let field = text_input("Text", &ed.value) .id(iced::widget::Id::new(TEXT_INLINE_ID)) .on_input(Message::TextInlineInput) @@ -48,14 +35,17 @@ pub(super) fn text_inline_overlay( .width(iced::Length::Fixed(240.0)); let panel = container(field) - .style(move |_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), + .style(move |theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.weak.color)), border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 5.0.into(), }, ..Default::default() + } }) .padding(4); @@ -195,7 +185,7 @@ impl iced::widget::canvas::Program for MTextPreview { &self, _state: &MTextPreviewState, renderer: &iced::Renderer, - _theme: &Theme, + theme: &Theme, bounds: iced::Rectangle, _cursor: iced::mouse::Cursor, ) -> Vec { @@ -221,12 +211,7 @@ impl iced::widget::canvas::Program for MTextPreview { ); frame.fill( &rect, - Color { - r: 0.20, - g: 0.42, - b: 0.72, - a: 0.45, - }, + theme.extended_palette().primary.base.color.scale_alpha(0.45), ); } } @@ -262,12 +247,7 @@ impl iced::widget::canvas::Program for MTextPreview { frame.stroke( &path, Stroke::default() - .with_color(Color { - r: 0.95, - g: 0.95, - b: 0.55, - a: 1.0, - }) + .with_color(theme.extended_palette().warning.base.color) .with_width(1.5), ); } else if collapsed { @@ -291,12 +271,7 @@ impl iced::widget::canvas::Program for MTextPreview { frame.stroke( &path, Stroke::default() - .with_color(Color { - r: 0.95, - g: 0.95, - b: 0.55, - a: 1.0, - }) + .with_color(theme.extended_palette().warning.base.color) .with_width(1.5), ); } @@ -344,59 +319,34 @@ pub(super) fn mtext_editor_overlay<'a>( modal_resize: iced::Vector, ) -> Element<'a, Message> { use super::super::mtext_editor::{JustifyChoice, MTextFmt, ParaAlign}; - use iced::widget::{canvas, svg}; + use iced::widget::canvas; - const BORDER: Color = Color { - r: 0.40, - g: 0.40, - b: 0.40, - a: 1.0, - }; - const TEXT_COL: Color = Color { - r: 0.88, - g: 0.88, - b: 0.88, - a: 1.0, - }; - const FIELD_BG: Color = Color { - r: 0.12, - g: 0.12, - b: 0.12, - a: 1.0, - }; - - let btn_style = |_: &Theme, status: button::Status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => Color { - r: 0.28, - g: 0.40, - b: 0.55, - a: 1.0, - }, - _ => Color { - r: 0.22, - g: 0.22, - b: 0.22, - a: 1.0, - }, - })), - text_color: TEXT_COL, + let btn_style = |theme: &Theme, status: button::Status| { + let palette = theme.extended_palette(); + let pair = match status { + button::Status::Hovered | button::Status::Pressed => palette.background.strong, + _ => palette.background.weak, + }; + button::Style { + background: Some(Background::Color(pair.color)), + text_color: pair.text, border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 3.0.into(), }, shadow: iced::Shadow::default(), snap: false, + } }; let icon_btn = move |bytes: &'static [u8], msg: Message| -> Element<'static, Message> { - button(svg(svg::Handle::from_memory(bytes)).width(18).height(18)) + button(crate::ui::icons::themed(bytes, 18.0)) .on_press(msg) .padding(3) .style(btn_style) .into() }; - let lbl = |s: &'static str| text(s).size(11).color(TEXT_COL); + let lbl = |s: &'static str| text(s).size(11); let small_input = |placeholder: &'static str, val: &str, on: fn(String) -> Message, @@ -640,14 +590,17 @@ pub(super) fn mtext_editor_overlay<'a>( .width(Fill) .height(Fill), ) - .style(move |_: &Theme| container::Style { - background: Some(Background::Color(FIELD_BG)), + .style(move |theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.base.color)), border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 3.0.into(), }, ..Default::default() + } }) .padding(2) .width(Fill) @@ -665,8 +618,10 @@ pub(super) fn mtext_editor_overlay<'a>( ] .align_y(iced::Alignment::Center), ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(crate::ui::style::style_manager::TB)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.weak.color + )), ..Default::default() }) .width(Fill) @@ -710,50 +665,10 @@ pub(super) fn viewport_context_menu_overlay( last_cmds: Vec, draworder_open: bool, ) -> Element<'static, Message> { - const MENU_BG: Color = Color { - r: 0.17, - g: 0.17, - b: 0.17, - a: 1.0, - }; - const MENU_BORDER: Color = Color { - r: 0.35, - g: 0.35, - b: 0.35, - a: 1.0, - }; - const ITEM_HOVER: Color = Color { - r: 0.25, - g: 0.45, - b: 0.70, - a: 1.0, - }; - const TEXT_COL: Color = Color { - r: 0.88, - g: 0.88, - b: 0.88, - a: 1.0, - }; - const SEP_COL: Color = Color { - r: 0.30, - g: 0.30, - b: 0.30, - a: 1.0, - }; - let item = |label: String, msg: Message| -> Element<'static, Message> { - button(text(label).size(12).color(TEXT_COL)) + button(text(label).size(12)) .on_press(msg) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => ITEM_HOVER, - _ => Color::TRANSPARENT, - })), - text_color: TEXT_COL, - border: Border::default(), - shadow: iced::Shadow::default(), - snap: false, - }) + .style(button::subtle) .padding([4, 12]) .width(Fill) .into() @@ -761,8 +676,10 @@ pub(super) fn viewport_context_menu_overlay( let sep = || -> Element<'static, Message> { container(iced::widget::Space::new().width(Fill).height(1)) - .style(move |_: &Theme| container::Style { - background: Some(Background::Color(SEP_COL)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.weak.color, + )), ..Default::default() }) .width(Fill) @@ -773,18 +690,9 @@ pub(super) fn viewport_context_menu_overlay( // Indented variant for sub-menu rows (e.g. Draw Order children). let subitem = |label: String, msg: Message| -> Element<'static, Message> { - button(text(label).size(12).color(TEXT_COL)) + button(text(label).size(12)) .on_press(msg) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => ITEM_HOVER, - _ => Color::TRANSPARENT, - })), - text_color: TEXT_COL, - border: Border::default(), - shadow: iced::Shadow::default(), - snap: false, - }) + .style(button::subtle) .padding(iced::Padding { top: 4.0, right: 12.0, @@ -827,30 +735,21 @@ pub(super) fn viewport_context_menu_overlay( )); items.push(sep()); let do_caret = if draworder_open { - crate::ui::icons::arrow_down(9.0, TEXT_COL) + crate::ui::icons::themed_arrow_down(9.0) } else { - crate::ui::icons::arrow_right(9.0, TEXT_COL) + crate::ui::icons::themed_arrow_right(9.0) }; items.push( button( row![ - text("Draw Order").size(12).color(TEXT_COL), + text("Draw Order").size(12), iced::widget::Space::new().width(Fill), do_caret, ] .align_y(iced::Center), ) .on_press(Message::DrawOrderSubmenuToggle) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => ITEM_HOVER, - _ => Color::TRANSPARENT, - })), - text_color: TEXT_COL, - border: Border::default(), - shadow: iced::Shadow::default(), - snap: false, - }) + .style(button::subtle) .padding([4, 12]) .width(Fill) .into(), @@ -909,15 +808,7 @@ pub(super) fn viewport_context_menu_overlay( let menu_col = column(items).spacing(0).width(iced::Length::Fixed(180.0)); let menu = container(menu_col) - .style(move |_: &Theme| container::Style { - background: Some(Background::Color(MENU_BG)), - border: Border { - color: MENU_BORDER, - width: 1.0, - radius: 4.0.into(), - }, - ..Default::default() - }) + .style(container::bordered_box) .padding([4, 0]) .width(iced::Length::Fixed(180.0)); @@ -928,18 +819,12 @@ pub(super) fn viewport_context_menu_overlay( /// snap ICONS only — the names show as hover tooltips. Picking one applies /// that snap to just the next point pick. pub(super) fn snap_override_overlay(pos: iced::Point) -> Element<'static, Message> { - const PANEL_BG: Color = Color { r: 0.16, g: 0.16, b: 0.16, a: 0.98 }; - const PANEL_BORDER: Color = Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 }; - const ICON_COLOR: Color = Color { r: 0.85, g: 0.85, b: 0.85, a: 1.0 }; - const HOVER: Color = Color { r: 0.25, g: 0.45, b: 0.70, a: 1.0 }; - const TIP_BG: Color = Color { r: 0.10, g: 0.10, b: 0.10, a: 0.98 }; const COLS: usize = 4; let cell = |snap_type: crate::snap::SnapType, label: &'static str| -> Element<'static, Message> { - let icon = container(crate::ui::icons::tinted::( + let icon = container(crate::ui::icons::themed::( crate::ui::icons::osnap(snap_type), 16.0, - ICON_COLOR, )) .width(26) .height(26) @@ -947,26 +832,33 @@ pub(super) fn snap_override_overlay(pos: iced::Point) -> Element<'static, Messag .align_y(iced::Center); let btn = button(icon) .on_press(Message::SnapOverridePick(snap_type)) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => HOVER, - _ => Color::TRANSPARENT, - })), + .style(|theme: &Theme, status| button::Style { + background: matches!( + status, + button::Status::Hovered | button::Status::Pressed + ) + .then_some(Background::Color( + theme.extended_palette().primary.weak.color + )), border: Border::default(), ..Default::default() }) .padding(2); iced::widget::tooltip( btn, - container(text(label).size(11).color(Color::WHITE)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(TIP_BG)), + container(text(label).size(11)) + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.strong.color)), border: Border { - color: PANEL_BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 2.0.into(), }, + text_color: Some(palette.background.strong.text), ..Default::default() + } }) .padding([2, 6]), iced::widget::tooltip::Position::Bottom, @@ -984,14 +876,17 @@ pub(super) fn snap_override_overlay(pos: iced::Point) -> Element<'static, Messag } let panel = container(grid) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.weak.color)), border: Border { - color: PANEL_BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 4.0.into(), }, ..Default::default() + } }) .padding(4); @@ -1024,49 +919,6 @@ pub(super) fn qselect_overlay<'a>( properties: &[(String, String)], ) -> Element<'a, Message> { use iced::widget::{checkbox, pick_list}; - const BG: Color = Color { - r: 0.12, - g: 0.12, - b: 0.12, - a: 0.98, - }; - const BORDER: Color = Color { - r: 0.35, - g: 0.35, - b: 0.35, - a: 1.0, - }; - const TEXT: Color = Color { - r: 0.88, - g: 0.88, - b: 0.88, - a: 1.0, - }; - const BTN_OK: Color = Color { - r: 0.22, - g: 0.42, - b: 0.68, - a: 1.0, - }; - const BTN_OK_HOV: Color = Color { - r: 0.30, - g: 0.52, - b: 0.80, - a: 1.0, - }; - const BTN_BG: Color = Color { - r: 0.22, - g: 0.22, - b: 0.22, - a: 1.0, - }; - const BTN_HOV: Color = Color { - r: 0.30, - g: 0.30, - b: 0.30, - a: 1.0, - }; - let mut type_options: Vec = vec![QSELECT_ANY_TYPE.to_string()]; type_options.extend(types.iter().cloned()); @@ -1111,28 +963,33 @@ pub(super) fn qselect_overlay<'a>( let label = |s: &'static str| { text(s) .size(12) - .color(TEXT) .width(iced::Length::Fixed(90.0)) }; - let btn = |lbl: &'static str, msg: Message, base: Color, hov: Color| { - button(text(lbl).size(12).color(TEXT)) + let btn = |lbl: &'static str, msg: Message, primary: bool| { + button(text(lbl).size(12)) .on_press(msg) - .style(move |_: &Theme, st| button::Style { - background: Some(Background::Color( - if matches!(st, button::Status::Hovered | button::Status::Pressed) { - hov - } else { - base - }, - )), - text_color: TEXT, + .style(move |theme: &Theme, st| { + let palette = theme.extended_palette(); + let pair = match ( + primary, + matches!(st, button::Status::Hovered | button::Status::Pressed), + ) { + (true, true) => palette.primary.strong, + (true, false) => palette.primary.base, + (false, true) => palette.background.strong, + (false, false) => palette.background.weak, + }; + button::Style { + background: Some(Background::Color(pair.color)), + text_color: pair.text, border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 4.0.into(), }, ..Default::default() + } }) .padding([4, 14]) }; @@ -1143,7 +1000,7 @@ pub(super) fn qselect_overlay<'a>( } let panel_body = column![ - text("Quick Select").size(14).color(TEXT), + text("Quick Select").size(14), Space::new().height(10), row![ label("Object type:"), @@ -1198,15 +1055,15 @@ pub(super) fn qselect_overlay<'a>( .on_toggle(Message::QSelectSetAppend) .size(14), Space::new().width(6), - text("Append to current selection").size(12).color(TEXT), + text("Append to current selection").size(12), ] .align_y(iced::Alignment::Center), Space::new().height(14), row![ Space::new().width(Fill), - btn("Cancel", Message::QSelectClose, BTN_BG, BTN_HOV), + btn("Cancel", Message::QSelectClose, false), Space::new().width(8), - btn("Apply", Message::QSelectApply, BTN_OK, BTN_OK_HOV), + btn("Apply", Message::QSelectApply, true), ] .align_y(iced::Alignment::Center), ] @@ -1215,14 +1072,17 @@ pub(super) fn qselect_overlay<'a>( let panel = container(panel_body) .padding(16) .width(iced::Length::Fixed(400.0)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BG)), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.weak.color)), border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 6.0.into(), }, ..Default::default() + } }); // Outside-click catcher — fills the whole screen, sits below the diff --git a/src/app/view/viewcube.rs b/src/app/view/viewcube.rs index 6f1c686b..e566bbe6 100644 --- a/src/app/view/viewcube.rs +++ b/src/app/view/viewcube.rs @@ -3,7 +3,7 @@ use crate::scene::{VIEWCUBE_PX, VIEWCUBE_REGION_PX}; use iced::widget::{ button, container, mouse_area, pick_list, stack, Space, }; -use iced::{Background, Border, Color, Element, Theme}; +use iced::{Background, Border, Element, Theme}; // ── Render-mode picker ────────────────────────────────────────────────────── @@ -30,16 +30,14 @@ fn vc_btn<'a>(content: Element<'a, Message>, size: f32, msg: Message) -> Element ) .padding(0) .on_press(msg) - .style(|_: &Theme, status| iced::widget::button::Style { - background: Some(Background::Color(match status { - iced::widget::button::Status::Hovered | iced::widget::button::Status::Pressed => Color { - r: 0.45, - g: 0.62, - b: 0.95, - a: 0.30, - }, - _ => Color::TRANSPARENT, - })), + .style(|theme: &Theme, status| iced::widget::button::Style { + background: matches!( + status, + iced::widget::button::Status::Hovered | iced::widget::button::Status::Pressed + ) + .then_some(Background::Color( + theme.extended_palette().primary.weak.color + )), border: Border { radius: 3.0.into(), ..Default::default() @@ -54,12 +52,6 @@ fn vc_btn<'a>(content: Element<'a, Message>, size: f32, msg: Message) -> Element pub(super) fn viewcube_nav_controls<'a>() -> Element<'a, Message> { use crate::scene::NudgeDir; use crate::ui::icons; - let tint = Color { - r: 0.86, - g: 0.89, - b: 0.96, - a: 1.0, - }; let r = VIEWCUBE_REGION_PX; let c = r * 0.5; let cube_half = VIEWCUBE_PX as f32 * 0.36; // VIEWCUBE_PX * VIEWCUBE_SCALE @@ -94,23 +86,23 @@ pub(super) fn viewcube_nav_controls<'a>() -> Element<'a, Message> { vc_place( 3.0, 3.0, - vc_btn(icons::home(13.0, tint), BTN, Message::ViewCubeHome) + vc_btn(icons::themed_home(13.0), BTN, Message::ViewCubeHome) ), vc_place( rax, ray, - vc_btn(icons::undo(12.0, tint), BTN, Message::ViewCubeRoll(false)) + vc_btn(icons::themed_undo(12.0, true), BTN, Message::ViewCubeRoll(false)) ), vc_place( rbx, rby, - vc_btn(icons::redo(12.0, tint), BTN, Message::ViewCubeRoll(true)) + vc_btn(icons::themed_redo(12.0, true), BTN, Message::ViewCubeRoll(true)) ), vc_place( tux, tuy, vc_btn( - icons::arrow_down(8.0, tint), + icons::themed_arrow_down(8.0), TRI, Message::ViewCubeNudge(NudgeDir::Up) ) @@ -119,7 +111,7 @@ pub(super) fn viewcube_nav_controls<'a>() -> Element<'a, Message> { tdx, tdy, vc_btn( - icons::arrow_up(8.0, tint), + icons::themed_arrow_up(8.0), TRI, Message::ViewCubeNudge(NudgeDir::Down) ) @@ -128,7 +120,7 @@ pub(super) fn viewcube_nav_controls<'a>() -> Element<'a, Message> { tlx, tly, vc_btn( - icons::arrow_right(8.0, tint), + icons::themed_arrow_right(8.0), TRI, Message::ViewCubeNudge(NudgeDir::Left) ) @@ -137,7 +129,7 @@ pub(super) fn viewcube_nav_controls<'a>() -> Element<'a, Message> { trx, try_, vc_btn( - icons::arrow_left(8.0, tint), + icons::themed_arrow_left(8.0), TRI, Message::ViewCubeNudge(NudgeDir::Right) ) @@ -155,12 +147,6 @@ pub(super) const UCS_PICKER_W: f32 = 84.0; /// The WCS / named-UCS selector shown under the cube. pub(super) fn viewcube_ucs_picker<'a>(current: String, names: Vec) -> Element<'a, Message> { - let light = Color { - r: 0.85, - g: 0.87, - b: 0.93, - a: 1.0, - }; let mut options = vec!["WCS".to_string()]; options.extend(names); let selected = if current.is_empty() { @@ -174,20 +160,20 @@ pub(super) fn viewcube_ucs_picker<'a>(current: String, names: Vec) -> El // Fixed width so the caller can centre it under the cube centre with a // simple half-width offset (content-sized width would drift off-centre). .width(iced::Length::Fixed(UCS_PICKER_W)) - .style(move |_: &Theme, _| iced::widget::pick_list::Style { - background: Background::Color(Color { - r: 0.16, - g: 0.17, - b: 0.20, - a: 0.92, - }), + .style(move |theme: &Theme, _| { + let palette = theme.extended_palette(); + iced::widget::pick_list::Style { + background: Background::Color(palette.background.weak.color), border: Border { radius: 3.0.into(), + color: palette.background.neutral.color, + width: 1.0, ..Default::default() }, - text_color: light, - placeholder_color: light, - handle_color: light, + text_color: palette.background.base.text, + placeholder_color: palette.background.base.text.scale_alpha(0.68), + handle_color: palette.background.base.text, + } }) .into() } diff --git a/src/ui/color_select.rs b/src/ui/color_select.rs index 08edb5c6..41c659f4 100644 --- a/src/ui/color_select.rs +++ b/src/ui/color_select.rs @@ -20,25 +20,6 @@ pub struct ColorExtras { pub by_block: bool, } -const PICKER_BG: Color = Color { - r: 0.12, - g: 0.12, - b: 0.12, - a: 1.0, -}; -const BORDER: Color = Color { - r: 0.35, - g: 0.35, - b: 0.35, - a: 1.0, -}; -const TEXT: Color = Color { - r: 0.88, - g: 0.88, - b: 0.88, - a: 1.0, -}; - /// Encode a colour as the ACI integer string the style editors store /// (ByBlock=0, ByLayer=256, indexed 1-255; RGB has no ACI slot → ByLayer). pub fn color_to_aci_string(c: AcadColor) -> String { @@ -85,15 +66,10 @@ pub fn color_display_name(c: AcadColor) -> String { /// A small colour square. fn swatch<'a>(bg: Color) -> Element<'a, Message> { container(text("").width(13).height(13)) - .style(move |_: &Theme| container::Style { + .style(move |theme: &Theme| container::Style { background: Some(Background::Color(bg)), border: Border { - color: Color { - r: 0.0, - g: 0.0, - b: 0.0, - a: 0.5, - }, + color: theme.extended_palette().background.neutral.color, width: 1.0, radius: 2.0.into(), }, @@ -126,8 +102,8 @@ pub fn color_selector<'a>( let head = button( row![ swatch(cur_bg), - text(cur_name).size(11).color(TEXT), - crate::ui::icons::arrow_toggle(open, 9.0, TEXT), + text(cur_name).size(11), + crate::ui::icons::themed_arrow_toggle(open, 9.0), ] .spacing(5) .align_y(iced::Center), @@ -141,14 +117,17 @@ pub fn color_selector<'a>( } let popup = container(color_list(extras, on_select, on_more)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PICKER_BG)), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.weak.color)), border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 2.0.into(), }, ..Default::default() + } }) .padding(5) .width(220); @@ -172,19 +151,15 @@ pub fn color_list<'a>( let named_row = |color: AcadColor| -> Element<'a, Message> { let (bg, name) = acad_color_display(color); button( - row![swatch(bg), text(name).size(11).color(TEXT)] + row![swatch(bg), text(name).size(11)] .spacing(5) .align_y(iced::Center), ) .on_press(on_select(color)) - .style(|_: &Theme, status| button::Style { - background: matches!(status, button::Status::Hovered) - .then_some(Background::Color(Color { - r: 0.25, - g: 0.25, - b: 0.30, - a: 1.0, - })), + .style(|theme: &Theme, status| button::Style { + background: matches!(status, button::Status::Hovered).then_some( + Background::Color(theme.extended_palette().background.strong.color) + ), ..Default::default() }) .padding([2, 4]) @@ -203,16 +178,12 @@ pub fn color_list<'a>( list = list.push(named_row(AcadColor::Index(i))); } list = list.push( - button(text("More…").size(11).color(TEXT)) + button(text("More…").size(11)) .on_press(on_more) - .style(|_: &Theme, status| button::Style { - background: matches!(status, button::Status::Hovered) - .then_some(Background::Color(Color { - r: 0.25, - g: 0.25, - b: 0.30, - a: 1.0, - })), + .style(|theme: &Theme, status| button::Style { + background: matches!(status, button::Status::Hovered).then_some( + Background::Color(theme.extended_palette().background.strong.color) + ), ..Default::default() }) .padding([2, 4]) @@ -227,7 +198,7 @@ pub fn color_grid_window(on_pick: impl Fn(AcadColor) -> Message) -> Element<'sta let chip = |color: AcadColor, label: &'static str| -> Element<'static, Message> { let (bg, _) = acad_color_display(color); button( - row![swatch(bg), text(label).size(11).color(TEXT)] + row![swatch(bg), text(label).size(11)] .spacing(5) .align_y(iced::Center), ) @@ -250,18 +221,13 @@ pub fn color_grid_window(on_pick: impl Fn(AcadColor) -> Message) -> Element<'sta r = r.push( button(text("").width(18).height(18)) .on_press(on_pick(AcadColor::Index(ci))) - .style(move |_: &Theme, status| button::Style { + .style(move |theme: &Theme, status| button::Style { background: Some(Background::Color(bg)), border: Border { color: if matches!(status, button::Status::Hovered) { - Color::WHITE + theme.extended_palette().primary.base.color } else { - Color { - r: 0.0, - g: 0.0, - b: 0.0, - a: 0.4, - } + theme.extended_palette().background.neutral.color }, width: if matches!(status, button::Status::Hovered) { 1.5 @@ -281,14 +247,16 @@ pub fn color_grid_window(on_pick: impl Fn(AcadColor) -> Message) -> Element<'sta container( column![ - text("Select Color").size(13).color(TEXT), + text("Select Color").size(13), row![chip(AcadColor::ByLayer, "ByLayer"), chip(AcadColor::ByBlock, "ByBlock")].spacing(6), scrollable(grid).height(Length::Fill), ] .spacing(8), ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PICKER_BG)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.weak.color + )), ..Default::default() }) .padding(10) diff --git a/src/ui/command_line.rs b/src/ui/command_line.rs index 24a0b05e..085d33de 100644 --- a/src/ui/command_line.rs +++ b/src/ui/command_line.rs @@ -370,11 +370,14 @@ impl CommandLine { let history_rows = visible[start..] .iter() .fold(column![].spacing(0), |col, entry| { - let color = match entry.kind { - EntryKind::Command => CMD_COLOR, - EntryKind::Output => OUT_COLOR, - EntryKind::Error => ERR_COLOR, - EntryKind::Info => INFO_COLOR, + let kind = entry.kind.clone(); + let entry_text = |value: String| { + text(value).size(11).style({ + let kind = kind.clone(); + move |theme: &Theme| iced::widget::text::Style { + color: Some(history_color(theme, &kind)), + } + }) }; // The current step's prompt is the single pinned line. When the // step offers options, render them as clickable buttons inline @@ -384,31 +387,28 @@ impl CommandLine { // listing is dropped here (the history log keeps the full text). if entry.pinned && !self.step_options.is_empty() { let shown = strip_option_listing(&entry.text); - let mut r = row![text(shown).size(11).color(color)] + let mut r = row![entry_text(shown)] .spacing(6) .align_y(iced::Center); for opt in &self.step_options { - let btn = button( - text(opt.label.to_uppercase()).size(11).color(CMD_COLOR), - ) + let btn = button(text(opt.label.to_uppercase()).size(11)) .on_press(Message::CommandOptionPick(opt.keyword.clone())) .padding([1, 6]) - .style(|_: &Theme, status| { - let bg = if matches!(status, button::Status::Hovered) { - Color { - r: 0.28, - g: 0.40, - b: 0.56, - a: 1.0, - } + .style(|theme: &Theme, status| { + let palette = theme.extended_palette(); + let pair = if matches!( + status, + button::Status::Hovered | button::Status::Pressed + ) { + palette.primary.weak } else { - INPUT_ROW_BG + palette.background.weakest }; button::Style { - background: Some(Background::Color(bg)), - text_color: Color::WHITE, + background: Some(Background::Color(pair.color)), + text_color: pair.text, border: Border { - color: BORDER_COLOR, + color: palette.background.neutral.color, width: 1.0, radius: 3.0.into(), }, @@ -419,46 +419,41 @@ impl CommandLine { } col.push(container(r).padding([1, 8])) } else { - col.push(container(text(&entry.text).size(11).color(color)).padding([1, 8])) + col.push(container(entry_text(entry.text.clone())).padding([1, 8])) } }); - let prompt = container(text("Command:").size(11).color(PROMPT_COLOR)).padding([5, 8]); + let prompt = container( + text("Command:").size(11).style(|theme: &Theme| iced::widget::text::Style { + color: Some(theme.extended_palette().success.base.color), + }), + ) + .padding([5, 8]); // Literal-space toggle: while active, every line behaves as if it // started with `>` — Space stays in the line instead of submitting, so // arguments with spaces (text strings, paths, `UCS Z 90` as one line) // can be typed. Persists until toggled off; a hand-typed leading `>` // lights the button too (same mode, one line only). let literal_active = self.literal_spaces || self.input.starts_with('>'); - let literal_btn = button(text(">").size(11).color(if literal_active { - Color::WHITE - } else { - PROMPT_COLOR - })) + let literal_btn = button(text(">").size(11)) .on_press(Message::CommandLiteralToggle) .padding([2, 6]) - .style(move |_: &Theme, status| { - let bg = if literal_active { - Color { - r: 0.28, - g: 0.40, - b: 0.56, - a: 1.0, - } - } else if matches!(status, button::Status::Hovered) { - Color { - r: 0.22, - g: 0.30, - b: 0.42, - a: 1.0, - } + .style(move |theme: &Theme, status| { + let palette = theme.extended_palette(); + let pair = if literal_active { + palette.primary.weak + } else if matches!( + status, + button::Status::Hovered | button::Status::Pressed + ) { + palette.background.weak } else { - INPUT_ROW_BG + palette.background.weakest }; button::Style { - background: Some(Background::Color(bg)), - text_color: Color::WHITE, + background: Some(Background::Color(pair.color)), + text_color: pair.text, border: Border { - color: BORDER_COLOR, + color: palette.background.neutral.color, width: 1.0, radius: 3.0.into(), }, @@ -467,19 +462,10 @@ impl CommandLine { }); let literal_tip = container( text("Literal spaces: Space stays in the line instead of running the command (same as typing a leading '>'). Stays on until toggled off.") - .size(11) - .color(Color::WHITE), + .size(11), ) .padding([3, 6]) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), - border: Border { - color: BORDER_COLOR, - width: 1.0, - radius: 3.0.into(), - }, - ..Default::default() - }); + .style(container::bordered_box); let literal_btn = tooltip(literal_btn, literal_tip, tooltip::Position::Top).gap(4); // While dynamic input is capturing keystrokes, the command-line // text field is left without an `on_input` handler so it can't @@ -492,33 +478,6 @@ impl CommandLine { .on_submit(Message::CommandSubmit); } let input = input - .style(|_: &Theme, _| text_input::Style { - background: Background::Color(INPUT_BG), - border: Border { - color: Color { - r: 0.40, - g: 0.60, - b: 0.90, - a: 1.0, - }, - width: 1.0, - radius: 2.0.into(), - }, - icon: Color::WHITE, - placeholder: Color { - r: 0.4, - g: 0.4, - b: 0.4, - a: 1.0, - }, - value: Color::WHITE, - selection: Color { - r: 0.20, - g: 0.44, - b: 0.72, - a: 0.5, - }, - }) .size(11) .padding([4, 6]); // Autocomplete suggestions panel, shown above the input row @@ -537,31 +496,25 @@ impl CommandLine { let mut col = column![].spacing(0).width(Length::Fill); for (idx, cmd) in matches.iter().enumerate() { let is_selected = cursor == idx; - let row = button(text(cmd.clone()).size(11).color(CMD_COLOR)) + let row = button(text(cmd.clone()).size(11)) .on_press(Message::CommandSuggestionPick(cmd.clone())) .width(Length::Fill) .padding([2, 8]) - .style(move |_: &Theme, status| { - let bg = if is_selected { - Color { - r: 0.28, - g: 0.40, - b: 0.56, - a: 1.0, - } - } else if matches!(status, button::Status::Hovered) { - Color { - r: 0.22, - g: 0.30, - b: 0.42, - a: 1.0, - } + .style(move |theme: &Theme, status| { + let palette = theme.extended_palette(); + let pair = if is_selected { + palette.primary.weak + } else if matches!( + status, + button::Status::Hovered | button::Status::Pressed + ) { + palette.background.weak } else { - PANEL_BG + palette.background.base }; button::Style { - background: Some(Background::Color(bg)), - text_color: Color::WHITE, + background: Some(Background::Color(pair.color)), + text_color: pair.text, border: Border::default(), ..Default::default() } @@ -569,15 +522,7 @@ impl CommandLine { col = col.push(row); } container(col) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), - border: Border { - color: BORDER_COLOR, - width: 1.0, - radius: 4.0.into(), - }, - ..Default::default() - }) + .style(container::bordered_box) .width(Length::Fill) .into() } @@ -590,22 +535,13 @@ impl CommandLine { // user can recover anything that has already faded off the // overlay. let dropdown_icon = if self.history_open { - crate::ui::icons::arrow_down(11.0, PROMPT_COLOR) + crate::ui::icons::themed_arrow_down(11.0) } else { - crate::ui::icons::arrow_right(11.0, PROMPT_COLOR) + crate::ui::icons::themed_arrow_right(11.0) }; let dropdown_btn = button(dropdown_icon) .on_press(Message::CommandHistoryToggle) - .style(|_: &Theme, _status| button::Style { - background: Some(Background::Color(INPUT_ROW_BG)), - text_color: Color::WHITE, - border: Border { - color: BORDER_COLOR, - width: 1.0, - radius: 3.0.into(), - }, - ..Default::default() - }) + .style(button::subtle) .padding([2, 6]); let input_row = row![prompt, literal_btn, input, dropdown_btn] .spacing(4) @@ -625,23 +561,21 @@ impl CommandLine { .size(11) .padding([2, 8]) .max_height(180.0) - .style(|_: &Theme, _status| text_editor::Style { - background: Background::Color(PANEL_BG), - border: Border::default(), - placeholder: OUT_COLOR, - value: CMD_COLOR, - selection: Color { - r: 0.20, - g: 0.44, - b: 0.72, - a: 0.5, - }, + .style(|theme: &Theme, _status| { + let palette = theme.extended_palette(); + text_editor::Style { + background: Background::Color(palette.background.base.color), + border: Border::default(), + placeholder: palette.background.base.text.scale_alpha(0.72), + value: palette.background.base.text, + selection: palette.primary.base.color.scale_alpha(0.5), + } }); // Header strip: a Copy-all and a Clear button pinned above the log. let copy_btn = button( row![ - crate::ui::icons::tinted(crate::ui::icons::COPY, 11.0, PROMPT_COLOR), - text("Copy").size(11).color(CMD_COLOR), + crate::ui::icons::themed_success(crate::ui::icons::COPY, 11.0), + text("Copy").size(11), ] .spacing(4) .align_y(iced::Center), @@ -651,8 +585,8 @@ impl CommandLine { .padding([2, 6]); let clear_btn = button( row![ - crate::ui::icons::tinted(crate::ui::icons::TRASH, 11.0, ERR_COLOR), - text("Clear").size(11).color(CMD_COLOR), + crate::ui::icons::themed_warning(crate::ui::icons::TRASH, 11.0), + text("Clear").size(11), ] .spacing(4) .align_y(iced::Center), @@ -668,15 +602,7 @@ impl CommandLine { .width(Length::Fill) .padding([2, 6]); let panel = container(column![header, log]) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), - border: Border { - color: BORDER_COLOR, - width: 1.0, - radius: 4.0.into(), - }, - ..Default::default() - }) + .style(container::bordered_box) .width(Length::Fill) .padding([4, 0]); opaque(panel).into() @@ -688,35 +614,43 @@ impl CommandLine { autocomplete, dropdown, container(history_rows) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(HISTORY_BG)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.base.color, + )), ..Default::default() }) .width(Length::Fill) .padding([2, 0]), container(input_row) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(INPUT_ROW_BG)), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.weakest.color)), border: Border { - color: BORDER_COLOR, + color: palette.background.neutral.color, width: 1.0, radius: 3.0.into() }, ..Default::default() + } }) .width(Length::Fill) // Match the drawing tab bar / status bar height, and vertically // centre the prompt/input/dropdown within it (issue #216). .center_y(Length::Fixed(30.0)), ]) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.base.color)), border: Border { - color: BORDER_COLOR, + color: palette.background.neutral.color, width: 1.0, radius: 4.0.into(), }, ..Default::default() + } }) .width(Length::Fixed(720.0)) .into() @@ -780,22 +714,18 @@ pub fn ranked_matches( /// Flat button style for the history dropdown's Copy / Clear strip: a subtle /// filled pill that brightens on hover. -fn header_btn_style(_: &Theme, status: button::Status) -> button::Style { - let bg = if matches!(status, button::Status::Hovered) { - Color { - r: 0.24, - g: 0.24, - b: 0.24, - a: 1.0, - } +fn header_btn_style(theme: &Theme, status: button::Status) -> button::Style { + let palette = theme.extended_palette(); + let pair = if matches!(status, button::Status::Hovered | button::Status::Pressed) { + palette.background.weak } else { - INPUT_ROW_BG + palette.background.weakest }; button::Style { - background: Some(Background::Color(bg)), - text_color: Color::WHITE, + background: Some(Background::Color(pair.color)), + text_color: pair.text, border: Border { - color: BORDER_COLOR, + color: palette.background.neutral.color, width: 1.0, radius: 3.0.into(), }, @@ -803,66 +733,15 @@ fn header_btn_style(_: &Theme, status: button::Status) -> button::Style { } } -const PANEL_BG: Color = Color { - r: 0.15, - g: 0.15, - b: 0.15, - a: 1.0, -}; -const HISTORY_BG: Color = Color { - r: 0.15, - g: 0.15, - b: 0.15, - a: 1.0, -}; -const INPUT_ROW_BG: Color = Color { - r: 0.18, - g: 0.18, - b: 0.18, - a: 1.0, -}; -const INPUT_BG: Color = Color { - r: 0.12, - g: 0.12, - b: 0.12, - a: 1.0, -}; -const BORDER_COLOR: Color = Color { - r: 0.30, - g: 0.30, - b: 0.30, - a: 1.0, -}; -const PROMPT_COLOR: Color = Color { - r: 0.55, - g: 0.78, - b: 0.55, - a: 1.0, -}; -const CMD_COLOR: Color = Color { - r: 0.80, - g: 0.80, - b: 0.80, - a: 1.0, -}; -const OUT_COLOR: Color = Color { - r: 0.65, - g: 0.65, - b: 0.65, - a: 1.0, -}; -const ERR_COLOR: Color = Color { - r: 0.90, - g: 0.35, - b: 0.35, - a: 1.0, -}; -const INFO_COLOR: Color = Color { - r: 0.50, - g: 0.70, - b: 0.90, - a: 1.0, -}; +fn history_color(theme: &Theme, kind: &EntryKind) -> Color { + let palette = theme.extended_palette(); + match kind { + EntryKind::Command => palette.background.base.text, + EntryKind::Output => palette.background.base.text.scale_alpha(0.72), + EntryKind::Error => palette.danger.base.color, + EntryKind::Info => palette.primary.base.color, + } +} #[cfg(test)] mod tests { diff --git a/src/ui/icons.rs b/src/ui/icons.rs index 61c1b7ed..b57a56c5 100644 --- a/src/ui/icons.rs +++ b/src/ui/icons.rs @@ -7,7 +7,7 @@ //! boxes. Drawing them from SVG instead makes the chrome font-independent. use iced::widget::{container, svg, Space}; -use iced::{Color, Element, Length, Theme}; +use iced::{Element, Length, Theme}; const TRI_DOWN: &[u8] = include_bytes!("../../assets/icons/ui/tri_down.svg"); const TRI_UP: &[u8] = include_bytes!("../../assets/icons/ui/tri_up.svg"); @@ -65,7 +65,6 @@ pub const FILE_EXPORT: &[u8] = include_bytes!("../../assets/icons/ui/file_export pub const PRINT: &[u8] = include_bytes!("../../assets/icons/ui/print.svg"); pub const HEART: &[u8] = include_bytes!("../../assets/icons/ui/heart.svg"); pub const DOT: &[u8] = include_bytes!("../../assets/icons/ui/dot.svg"); -pub const TRI_LEFT_B: &[u8] = include_bytes!("../../assets/icons/ui/tri_left.svg"); pub const ARROW_LONG_RIGHT: &[u8] = include_bytes!("../../assets/icons/ui/arrow_long_right.svg"); // ── Status-bar toggle icons (issue #216) ────────────────────────────────── @@ -82,26 +81,112 @@ pub const ST_FILTER: &[u8] = include_bytes!("../../assets/icons/status/filter.sv pub const ST_SELCYCLE: &[u8] = include_bytes!("../../assets/icons/status/selcycle.svg"); pub const ST_CLEANSCREEN: &[u8] = include_bytes!("../../assets/icons/status/cleanscreen.svg"); -/// Render one of the bundled SVGs tinted to `color` at a square `size`. -pub fn tinted<'a, M: 'a>(bytes: &'static [u8], size: f32, color: Color) -> Element<'a, M> { +/// Render a chrome icon with the active Iced theme's normal text color. +pub fn themed<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> { svg(svg::Handle::from_memory(bytes)) .width(size) .height(size) - .style(move |_: &Theme, _| svg::Style { color: Some(color) }) + .style(|theme: &Theme, _| svg::Style { + color: Some(theme.extended_palette().background.base.text), + }) .into() } -/// Backwards-compatible alias used by the caret/undo/redo helpers below. -fn icon<'a, M: 'a>(bytes: &'static [u8], size: f32, color: Color) -> Element<'a, M> { - tinted(bytes, size, color) +/// Render secondary chrome with the active Iced theme's text color. +pub fn themed_secondary<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> { + svg(svg::Handle::from_memory(bytes)) + .width(size) + .height(size) + .style(|theme: &Theme, _| svg::Style { + color: Some( + theme + .extended_palette() + .background + .base + .text + .scale_alpha(0.72), + ), + }) + .into() } -/// A fixed-width (14 px) "current row" check column: a green-tintable check -/// when `active`, otherwise an empty spacer that preserves alignment. Used by -/// the many dropdown / popup list rows that mark the selected entry. -pub fn check_cell<'a, M: 'a>(active: bool, color: Color) -> Element<'a, M> { +/// Render disabled chrome with the active Iced theme's text color. +pub fn themed_disabled<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> { + svg(svg::Handle::from_memory(bytes)) + .width(size) + .height(size) + .style(|theme: &Theme, _| svg::Style { + color: Some( + theme + .extended_palette() + .background + .base + .text + .scale_alpha(0.42), + ), + }) + .into() +} + +/// Render an emphasized chrome icon with the active Iced theme's primary color. +pub fn themed_primary<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> { + svg(svg::Handle::from_memory(bytes)) + .width(size) + .height(size) + .style(|theme: &Theme, _| svg::Style { + color: Some(theme.extended_palette().primary.base.color), + }) + .into() +} + +/// Render a positive-state chrome icon with the active Iced theme's success color. +pub fn themed_success<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> { + svg(svg::Handle::from_memory(bytes)) + .width(size) + .height(size) + .style(|theme: &Theme, _| svg::Style { + color: Some(theme.extended_palette().success.base.color), + }) + .into() +} + +/// Render a warning-state chrome icon from the active Iced theme. +pub fn themed_warning<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> { + svg(svg::Handle::from_memory(bytes)) + .width(size) + .height(size) + .style(|theme: &Theme, _| svg::Style { + color: Some(theme.extended_palette().warning.base.color), + }) + .into() +} + +/// Render a destructive-state chrome icon from the active Iced theme. +pub fn themed_danger<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> { + svg(svg::Handle::from_memory(bytes)) + .width(size) + .height(size) + .style(|theme: &Theme, _| svg::Style { + color: Some(theme.extended_palette().danger.base.color), + }) + .into() +} + +/// Render an icon with the foreground chosen for a danger-coloured surface. +pub fn themed_danger_text<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> { + svg(svg::Handle::from_memory(bytes)) + .width(size) + .height(size) + .style(|theme: &Theme, _| svg::Style { + color: Some(theme.extended_palette().danger.base.text), + }) + .into() +} + +/// Fixed-width check column colored from the active Iced theme. +pub fn themed_check_cell<'a, M: 'a>(active: bool) -> Element<'a, M> { let inner: Element<'a, M> = if active { - tinted(CHECK, 11.0, color) + themed_primary(CHECK, 11.0) } else { Space::new().width(0).into() }; @@ -166,46 +251,59 @@ pub fn layer_lock(locked: bool) -> &'static [u8] { } } -/// Downward dropdown caret (replaces `▾`). -pub fn arrow_down<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> { - icon(TRI_DOWN, size, color) +pub fn themed_arrow_down<'a, M: 'a>(size: f32) -> Element<'a, M> { + themed(TRI_DOWN, size) } -/// Upward dropdown caret, shown when a dropdown is open (replaces `▲`). -pub fn arrow_up<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> { - icon(TRI_UP, size, color) +pub fn themed_arrow_up<'a, M: 'a>(size: f32) -> Element<'a, M> { + themed(TRI_UP, size) } -/// Rightward caret for a collapsed item (replaces `▸`). -pub fn arrow_right<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> { - icon(TRI_RIGHT, size, color) +pub fn themed_arrow_right<'a, M: 'a>(size: f32) -> Element<'a, M> { + themed(TRI_RIGHT, size) } -/// Leftward caret. -pub fn arrow_left<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> { - icon(TRI_LEFT, size, color) +pub fn themed_arrow_left<'a, M: 'a>(size: f32) -> Element<'a, M> { + themed(TRI_LEFT, size) } -/// House glyph — the ViewCube "home view" button. -pub fn home<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> { - icon(HOME, size, color) +pub fn themed_primary_arrow_down<'a, M: 'a>(size: f32) -> Element<'a, M> { + themed_primary(TRI_DOWN, size) +} + +pub fn themed_secondary_arrow_down<'a, M: 'a>(size: f32) -> Element<'a, M> { + themed_secondary(TRI_DOWN, size) +} + +pub fn themed_disabled_arrow_down<'a, M: 'a>(size: f32) -> Element<'a, M> { + themed_disabled(TRI_DOWN, size) +} + +pub fn themed_home<'a, M: 'a>(size: f32) -> Element<'a, M> { + themed(HOME, size) } /// Caret that flips up/down with `open`. -pub fn arrow_toggle<'a, M: 'a>(open: bool, size: f32, color: Color) -> Element<'a, M> { +pub fn themed_arrow_toggle<'a, M: 'a>(open: bool, size: f32) -> Element<'a, M> { if open { - arrow_up(size, color) + themed_arrow_up(size) } else { - arrow_down(size, color) + themed_arrow_down(size) } } -/// Undo curved arrow (replaces `↶`). -pub fn undo<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> { - icon(UNDO, size, color) +pub fn themed_undo<'a, M: 'a>(size: f32, enabled: bool) -> Element<'a, M> { + if enabled { + themed(UNDO, size) + } else { + themed_disabled(UNDO, size) + } } -/// Redo curved arrow (replaces `↷`). -pub fn redo<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> { - icon(REDO, size, color) +pub fn themed_redo<'a, M: 'a>(size: f32, enabled: bool) -> Element<'a, M> { + if enabled { + themed(REDO, size) + } else { + themed_disabled(REDO, size) + } } diff --git a/src/ui/modal.rs b/src/ui/modal.rs index 5b30ab83..64a0a887 100644 --- a/src/ui/modal.rs +++ b/src/ui/modal.rs @@ -7,34 +7,7 @@ use crate::app::Message; use iced::widget::{button, column, container, mouse_area, opaque, row, stack}; -use iced::{Background, Border, Color, Element, Length, Padding, Theme, Vector}; - -const PANEL: Color = Color { - r: 0.13, - g: 0.13, - b: 0.13, - a: 1.0, -}; -const BORDER_C: Color = Color { - r: 0.35, - g: 0.35, - b: 0.35, - a: 1.0, -}; -/// Title-bar background. -const TITLE_C: Color = Color { - r: 0.18, - g: 0.18, - b: 0.18, - a: 1.0, -}; -/// Drag-handle arrow — bright green so the move affordance stands out. -const GRIP_C: Color = Color { - r: 0.2, - g: 1.0, - b: 0.3, - a: 1.0, -}; +use iced::{Background, Border, Element, Length, Padding, Theme, Vector}; /// Stack `content` over `base` behind a dimmed backdrop, framed with a /// draggable title bar (the ✕ close button at its right end). The backdrop only @@ -52,15 +25,9 @@ pub fn modal<'a>( offset: Vector, resizable: bool, ) -> Element<'a, Message> { - let close = button(crate::ui::icons::tinted( + let close = button(crate::ui::icons::themed_danger( crate::ui::icons::CLOSE, 13.0, - Color { - r: 0.85, - g: 0.85, - b: 0.85, - a: 1.0, - }, )) .on_press(on_close) .padding([1, 7]) @@ -71,10 +38,12 @@ pub fn modal<'a>( // screen width — the dialog stays sized to its content. Pressing the grip // starts a drag (handled in `update`). let grip = mouse_area( - container(crate::ui::icons::tinted(crate::ui::icons::MOVE, 14.0, GRIP_C)) + container(crate::ui::icons::themed_primary(crate::ui::icons::MOVE, 14.0)) .padding([1, 7]) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(TITLE_C)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.weakest.color, + )), border: Border { radius: 4.0.into(), ..Default::default() @@ -89,12 +58,7 @@ pub fn modal<'a>( // overlaid at the right edge. The bar takes an explicit `title_width` // (the caller's content width) instead of `Fill` — a Fill child inside // the Shrink frame would blow the dialog out to the full screen. - let title_text = iced::widget::text(title).size(15).color(Color { - r: 0.88, - g: 0.88, - b: 0.88, - a: 1.0, - }); + let title_text = iced::widget::text(title).size(15); let title_bar = stack![ container(title_text) .width(Length::Fixed(title_width)) @@ -108,10 +72,12 @@ pub fn modal<'a>( .align_y(iced::alignment::Vertical::Center), ]; - let panel_style = |_: &Theme| container::Style { - background: Some(Background::Color(PANEL)), + let panel_style = |theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.base.color, + )), border: Border { - color: BORDER_C, + color: theme.extended_palette().background.neutral.color, width: 1.0, radius: 6.0.into(), }, @@ -125,7 +91,7 @@ pub fn modal<'a>( // top-right above the content either way (`align_x(Right)`). let body = if resizable { let resize = mouse_area( - container(crate::ui::icons::tinted(crate::ui::icons::RESIZE, 15.0, GRIP_C)) + container(crate::ui::icons::themed_primary(crate::ui::icons::RESIZE, 15.0)) .padding([0, 2]), ) .on_press(Message::ModalResizeGrab) @@ -157,11 +123,15 @@ pub fn modal<'a>( .center_x(Length::Fill) .center_y(Length::Fill) .padding(pad) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { - a: 0.55, - ..Color::BLACK - })), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme + .extended_palette() + .background + .strongest + .color + .scale_alpha(0.55), + )), ..Default::default() }), ) @@ -177,24 +147,15 @@ pub fn modal<'a>( .into() } -fn close_style(_: &Theme, status: button::Status) -> button::Style { - let bg = match status { - button::Status::Hovered | button::Status::Pressed => Color { - r: 0.7, - g: 0.2, - b: 0.2, - a: 1.0, - }, - _ => Color { - r: 0.25, - g: 0.25, - b: 0.25, - a: 1.0, - }, +fn close_style(theme: &Theme, status: button::Status) -> button::Style { + let palette = theme.extended_palette(); + let pair = match status { + button::Status::Hovered | button::Status::Pressed => palette.danger.strong, + _ => palette.background.weakest, }; button::Style { - background: Some(Background::Color(bg)), - text_color: Color::WHITE, + background: Some(Background::Color(pair.color)), + text_color: pair.text, border: Border { radius: 4.0.into(), ..Default::default() diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index f333049e..31e31b27 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -292,7 +292,7 @@ struct SelectionCanvas { hover_locked: bool, } -fn draw_grip_marker(frame: &mut canvas::Frame, grip: &GripMarker) { +fn draw_grip_marker(frame: &mut canvas::Frame, grip: &GripMarker, theme: &Theme) { let sp = grip.pos; let h = crate::scene::pick::grip::GRIP_HALF_PX; let path = match grip.shape { @@ -332,30 +332,13 @@ fn draw_grip_marker(frame: &mut canvas::Frame, grip: &GripMarker) { }; if grip.is_hot { - frame.fill( - &path, - Color { - r: 1.0, - g: 0.15, - b: 0.10, - a: 1.0, - }, - ); + frame.fill(&path, theme.extended_palette().danger.base.color); } else { - let color = Color { - r: 0.10, - g: 0.45, - b: 0.90, - a: 1.0, - }; + let palette = theme.extended_palette(); + let color = palette.primary.base.color; frame.fill( &path, - Color { - r: 0.10, - g: 0.10, - b: 0.20, - a: 0.7, - }, + palette.background.base.color.scale_alpha(0.7), ); frame.stroke( &path, @@ -442,7 +425,7 @@ impl canvas::Program for SelectionCanvas { &self, _state: &(), renderer: &iced::Renderer, - _theme: &Theme, + theme: &Theme, bounds: iced::Rectangle, cursor: mouse::Cursor, ) -> Vec { @@ -452,18 +435,13 @@ impl canvas::Program for SelectionCanvas { // Filled bars in the pane_grid spacing gaps, so adjacent panes read as // distinct viewports. Drawn first so all other overlays sit on top. if !self.dividers.is_empty() { - const DIVIDER: Color = Color { - r: 0.46, - g: 0.52, - b: 0.62, - a: 1.0, - }; + let divider = theme.extended_palette().background.neutral.color; for d in &self.dividers { let bar = canvas::Path::rectangle( Point::new(d.x, d.y), iced::Size::new(d.width.max(1.0), d.height.max(1.0)), ); - frame.fill(&bar, DIVIDER); + frame.fill(&bar, divider); } } @@ -472,23 +450,13 @@ impl canvas::Program for SelectionCanvas { // under the cursor, and drag a translucent ghost card along the cursor // so the pane is visibly "moving". if let Some(src) = self.pane_move_rect { - let accent = Color { - r: 0.30, - g: 0.62, - b: 1.0, - a: 1.0, - }; + let accent = theme.extended_palette().primary.base.color; // Source pane: dimmed + dashed-feel outline (it has been lifted). let src_path = canvas::Path::rectangle(Point::new(src.x, src.y), iced::Size::new(src.width, src.height)); frame.fill( &src_path, - Color { - r: 0.0, - g: 0.0, - b: 0.0, - a: 0.28, - }, + theme.extended_palette().background.strong.color.scale_alpha(0.28), ); frame.stroke( &src_path, @@ -539,18 +507,20 @@ impl canvas::Program for SelectionCanvas { // Draw a selection marquee (green crossing / blue window) as a filled, // stroked rectangle between two canvas points. Shared by the live // box-selection and the preview-only window marquee (#291). - fn draw_marquee(frame: &mut canvas::Frame, a: Point, b: Point, crossing: bool) { - let (fill, stroke) = if crossing { - ( - Color { r: 0.20, g: 0.72, b: 0.44, a: 0.12 }, - Color { r: 0.20, g: 0.72, b: 0.44, a: 0.9 }, - ) + fn draw_marquee( + frame: &mut canvas::Frame, + a: Point, + b: Point, + crossing: bool, + theme: &Theme, + ) { + let base = if crossing { + theme.extended_palette().success.base.color } else { - ( - Color { r: 0.20, g: 0.44, b: 0.72, a: 0.12 }, - Color { r: 0.20, g: 0.44, b: 0.72, a: 0.9 }, - ) + theme.extended_palette().primary.base.color }; + let fill = base.scale_alpha(0.12); + let stroke = base.scale_alpha(0.9); let x0 = a.x.min(b.x); let y0 = a.y.min(b.y); let w = (a.x - b.x).abs(); @@ -568,45 +538,21 @@ impl canvas::Program for SelectionCanvas { } if let (Some(a), Some(b)) = (self.selection.box_anchor, self.selection.box_current) { - draw_marquee(&mut frame, a, b, self.selection.box_crossing); + draw_marquee(&mut frame, a, b, self.selection.box_crossing, theme); } // Preview marquee for point-picked windows (STRETCH) — same look, no pick. if let Some((a, b, crossing)) = self.selection.preview_box { - draw_marquee(&mut frame, a, b, crossing); + draw_marquee(&mut frame, a, b, crossing, theme); } if self.selection.poly_active && self.selection.poly_points.len() > 1 { - let (fill, stroke) = if self.selection.poly_crossing { - ( - Color { - r: 0.20, - g: 0.72, - b: 0.44, - a: 0.12, - }, - Color { - r: 0.20, - g: 0.72, - b: 0.44, - a: 0.9, - }, - ) + let base = if self.selection.poly_crossing { + theme.extended_palette().success.base.color } else { - ( - Color { - r: 0.20, - g: 0.44, - b: 0.72, - a: 0.12, - }, - Color { - r: 0.20, - g: 0.44, - b: 0.72, - a: 0.9, - }, - ) + theme.extended_palette().primary.base.color }; + let fill = base.scale_alpha(0.12); + let stroke = base.scale_alpha(0.9); if let Some(cur) = self.selection.last_move_pos { let start = self.selection.poly_points[0]; let fill_path = canvas::Path::new(|p| { @@ -667,7 +613,7 @@ impl canvas::Program for SelectionCanvas { }; frame.with_clip(grip_clip, |frame| { for grip in &self.grips { - draw_grip_marker(frame, grip); + draw_grip_marker(frame, grip, theme); } }); } @@ -962,12 +908,12 @@ impl canvas::Program for SelectionCanvas { // PAN mode replaces the crosshair with a hand cursor. if !over_viewcube && !over_divider && !self.pan_mode && !self.suppressed { if let Some(cp) = self.selection.last_move_pos { - let color = Color { - r: 0.85, - g: 0.85, - b: 0.85, - a: 0.90, - }; + let color = theme + .extended_palette() + .background + .base + .text + .scale_alpha(0.90); let stroke = canvas::Stroke { width: 1.0, style: canvas::Style::Solid(color), @@ -1010,8 +956,9 @@ impl canvas::Program for SelectionCanvas { // the hovered object sits on a locked layer (issue: locked // objects are visible + snappable but not selectable/editable). if self.hover_locked { - let amber = Color { r: 0.96, g: 0.76, b: 0.26, a: 0.98 }; - let dark = Color { r: 0.12, g: 0.10, b: 0.04, a: 1.0 }; + let warning = theme.extended_palette().warning.base; + let amber = warning.color.scale_alpha(0.98); + let dark = warning.text; let bx = cp.x + sq + 7.0; let by = cp.y - sq - 13.0; // Lock body (filled). @@ -1058,12 +1005,7 @@ impl canvas::Program for SelectionCanvas { } // ── Object Snap Tracking ───────────────────────────────────────────── - let track_color = Color { - r: 0.15, - g: 0.85, - b: 0.95, - a: 0.7, - }; + let track_color = theme.extended_palette().primary.base.color.scale_alpha(0.7); // The alignment line the cursor is currently locked to — drawn at its // real angle from the acquired point through the lock and a little // beyond, dashed so it reads as a construction guide. This covers the @@ -2142,10 +2084,12 @@ struct DynInputCanvas { } impl DynInputCanvas { - fn dotted() -> canvas::Stroke<'static> { + fn dotted(theme: &Theme) -> canvas::Stroke<'static> { canvas::Stroke { width: 1.0, - style: canvas::Style::Solid(Color { r: 0.55, g: 0.55, b: 0.58, a: 0.9 }), + style: canvas::Style::Solid( + theme.extended_palette().background.neutral.color.scale_alpha(0.9) + ), line_dash: canvas::LineDash { segments: &[2.0, 3.0], offset: 0 }, ..Default::default() } @@ -2160,13 +2104,19 @@ impl DynInputCanvas { } /// Draw a value box centred at `center`, clamped inside `bounds`. - fn draw_box(frame: &mut canvas::Frame, b: &DynBox, center: Point, bounds: iced::Rectangle) { + fn draw_box( + frame: &mut canvas::Frame, + b: &DynBox, + center: Point, + bounds: iced::Rectangle, + theme: &Theme, + ) { let content = Self::box_content(b); let w = (content.len() as f32 * DYN_CHAR_W) + DYN_PAD * 2.0; let x = (center.x - w * 0.5).clamp(0.0, (bounds.width - w).max(0.0)); let y = (center.y - DYN_BOX_H * 0.5).clamp(0.0, (bounds.height - DYN_BOX_H).max(0.0)); let rect = canvas::Path::rectangle(Point { x, y }, Size { width: w, height: DYN_BOX_H }); - let (fill, border) = Self::box_colors(b); + let (fill, border, text) = Self::box_colors(b, theme); frame.fill(&rect, fill); frame.stroke( &rect, @@ -2177,7 +2127,7 @@ impl DynInputCanvas { frame.fill_text(canvas::Text { content, position: Point { x: x + DYN_PAD, y: y + DYN_PAD }, - color: Color { r: 0.92, g: 0.92, b: 0.92, a: 1.0 }, + color: text, size: iced::Pixels(DYN_FONT), // Force Advanced shaping: the default `Auto` uses Basic shaping for // ASCII-only strings, which the web (wgpu/webgl) backend fails to @@ -2188,43 +2138,48 @@ impl DynInputCanvas { }); } - fn box_colors(b: &DynBox) -> (Color, Color) { + fn box_colors(b: &DynBox, theme: &Theme) -> (Color, Color, Color) { + let palette = theme.extended_palette(); if b.active { ( - Color { r: 0.12, g: 0.18, b: 0.30, a: 0.95 }, - Color { r: 0.45, g: 0.70, b: 1.0, a: 1.0 }, + palette.primary.weak.color, + palette.primary.base.color, + palette.primary.weak.text, ) } else if b.locked { ( - Color { r: 0.05, g: 0.05, b: 0.12, a: 0.9 }, - Color { r: 0.95, g: 0.75, b: 0.30, a: 0.9 }, + palette.warning.weak.color, + palette.warning.base.color, + palette.warning.weak.text, ) } else { ( - Color { r: 0.05, g: 0.05, b: 0.12, a: 0.9 }, - Color { r: 0.35, g: 0.55, b: 0.90, a: 0.9 }, + palette.background.weak.color, + palette.background.neutral.color, + palette.background.weak.text, ) } } /// Prompt pill at `pos`. - fn draw_prompt(&self, frame: &mut canvas::Frame, pos: Point) { + fn draw_prompt(&self, frame: &mut canvas::Frame, pos: Point, theme: &Theme) { if self.prompt.is_empty() { return; } + let palette = theme.extended_palette(); let pw = (self.prompt.len() as f32 * DYN_CHAR_W) + DYN_PAD * 2.0; let rect = canvas::Path::rectangle(pos, Size { width: pw, height: DYN_BOX_H }); - frame.fill(&rect, Color { r: 0.10, g: 0.10, b: 0.12, a: 1.0 }); + frame.fill(&rect, palette.background.strong.color); frame.stroke( &rect, canvas::Stroke::default() - .with_color(Color { r: 0.35, g: 0.55, b: 0.90, a: 0.9 }) + .with_color(palette.primary.base.color.scale_alpha(0.9)) .with_width(1.0), ); frame.fill_text(canvas::Text { content: self.prompt.clone(), position: Point { x: pos.x + DYN_PAD, y: pos.y + DYN_PAD }, - color: Color { r: 0.70, g: 0.85, b: 0.70, a: 1.0 }, + color: palette.background.strong.text, size: iced::Pixels(DYN_FONT), shaping: iced::advanced::text::Shaping::Advanced, ..Default::default() @@ -2233,7 +2188,13 @@ impl DynInputCanvas { /// Guided layout: draw the guide geometry anchored at `base`, then place /// each box according to its role. - fn draw_guided(&self, frame: &mut canvas::Frame, bounds: iced::Rectangle, base: Point) { + fn draw_guided( + &self, + frame: &mut canvas::Frame, + bounds: iced::Rectangle, + base: Point, + theme: &Theme, + ) { let cursor_raw = self.cursor_screen; let (vx, vy) = (cursor_raw.x - base.x, cursor_raw.y - base.y); let raw_len = (vx * vx + vy * vy).sqrt().max(1.0); @@ -2308,7 +2269,7 @@ impl DynInputCanvas { y: base.y + a_ref.sin() * len, }); }); - frame.stroke(&href, Self::dotted()); + frame.stroke(&href, Self::dotted(theme)); let arc = canvas::Path::new(|p| { let steps = 48; for k in 0..=steps { @@ -2324,14 +2285,14 @@ impl DynInputCanvas { } } }); - frame.stroke(&arc, Self::dotted()); + frame.stroke(&arc, Self::dotted(theme)); } DynGuide::Radius => { let line = canvas::Path::new(|p| { p.move_to(base); p.line_to(cursor); }); - frame.stroke(&line, Self::dotted()); + frame.stroke(&line, Self::dotted(theme)); } DynGuide::Perp => { if let Some((end, _, _)) = perp_info { @@ -2340,7 +2301,7 @@ impl DynInputCanvas { p.move_to(base); p.line_to(end); }); - frame.stroke(&line, Self::dotted()); + frame.stroke(&line, Self::dotted(theme)); } } DynGuide::PerpDim => { @@ -2351,14 +2312,14 @@ impl DynInputCanvas { p.move_to(ob); p.line_to(oe); }); - frame.stroke(&dim, Self::dotted()); + frame.stroke(&dim, Self::dotted(theme)); let ext = canvas::Path::new(|p| { p.move_to(base); p.line_to(ob); p.move_to(end); p.line_to(oe); }); - frame.stroke(&ext, Self::dotted()); + frame.stroke(&ext, Self::dotted(theme)); } } DynGuide::AxisDelta | DynGuide::RectSides => { @@ -2368,7 +2329,7 @@ impl DynInputCanvas { p.line_to(corner); p.line_to(cursor); }); - frame.stroke(&legs, Self::dotted()); + frame.stroke(&legs, Self::dotted(theme)); if self.guide == DynGuide::RectSides { // Close the rectangle so both side pairs read as a box. let rest = canvas::Path::new(|p| { @@ -2376,7 +2337,7 @@ impl DynInputCanvas { p.line_to(Point { x: base.x, y: cursor.y }); p.line_to(cursor); }); - frame.stroke(&rest, Self::dotted()); + frame.stroke(&rest, Self::dotted(theme)); } } DynGuide::None => {} @@ -2423,12 +2384,12 @@ impl DynInputCanvas { y: base.y + dy * len * 0.5 + ny * 16.0, }, }; - Self::draw_box(frame, b, center, bounds); + Self::draw_box(frame, b, center, bounds, theme); } } /// Fallback row layout near the cursor (no anchor / `None` guide). - fn draw_row(&self, frame: &mut canvas::Frame, bounds: iced::Rectangle) { + fn draw_row(&self, frame: &mut canvas::Frame, bounds: iced::Rectangle, theme: &Theme) { let texts: Vec = self .boxes .iter() @@ -2466,7 +2427,7 @@ impl DynInputCanvas { py = (by - pad - DYN_BOX_H).max(0.0); } if has_prompt { - self.draw_prompt(frame, Point { x: bx, y: py }); + self.draw_prompt(frame, Point { x: bx, y: py }, theme); } let mut x = bx; @@ -2474,7 +2435,7 @@ impl DynInputCanvas { let w = widths[i]; let rect = canvas::Path::rectangle(Point { x, y: by }, Size { width: w, height: DYN_BOX_H }); - let (fill, border) = Self::box_colors(b); + let (fill, border, text) = Self::box_colors(b, theme); frame.fill(&rect, fill); frame.stroke( &rect, @@ -2485,7 +2446,7 @@ impl DynInputCanvas { frame.fill_text(canvas::Text { content: texts[i].clone(), position: Point { x: x + DYN_PAD, y: by + DYN_PAD }, - color: Color { r: 0.92, g: 0.92, b: 0.92, a: 1.0 }, + color: text, size: iced::Pixels(DYN_FONT), shaping: iced::advanced::text::Shaping::Advanced, ..Default::default() @@ -2511,7 +2472,7 @@ impl canvas::Program for DynInputCanvas { &self, _state: &(), renderer: &iced::Renderer, - _theme: &Theme, + theme: &Theme, bounds: iced::Rectangle, _cursor: mouse::Cursor, ) -> Vec { @@ -2529,15 +2490,15 @@ impl canvas::Program for DynInputCanvas { if py + DYN_BOX_H > bounds.height { py = (self.cursor_screen.y - DYN_BOX_H - 4.0).max(0.0); } - self.draw_prompt(&mut frame, Point { x: px, y: py }); + self.draw_prompt(&mut frame, Point { x: px, y: py }, theme); } return vec![frame.into_geometry()]; } // Guided layouts need the anchor; without it fall back to a cursor row. match (self.guide, self.base_screen) { - (DynGuide::None, _) | (_, None) => self.draw_row(&mut frame, bounds), - (_, Some(base)) => self.draw_guided(&mut frame, bounds, base), + (DynGuide::None, _) | (_, None) => self.draw_row(&mut frame, bounds, theme), + (_, Some(base)) => self.draw_guided(&mut frame, bounds, base, theme), } vec![frame.into_geometry()] } diff --git a/src/ui/popup/cycle_popup.rs b/src/ui/popup/cycle_popup.rs index e04440ba..979a913c 100644 --- a/src/ui/popup/cycle_popup.rs +++ b/src/ui/popup/cycle_popup.rs @@ -3,7 +3,7 @@ //! adds that object to the current selection. Clicking outside dismisses it. use iced::widget::{button, column, container, mouse_area, opaque, text}; -use iced::{Background, Border, Color, Element, Fill, Length, Theme}; +use iced::{Element, Fill, Length}; use crate::app::Message; @@ -19,15 +19,7 @@ pub fn cycle_popup_overlay( .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() - }) + .style(container::bordered_box) .width(Length::Fixed(150.0)); let positioned = crate::ui::pin_at(anchor, opaque(panel)); @@ -36,16 +28,10 @@ pub fn cycle_popup_overlay( } fn item_row(handle: acadrust::Handle, label: String) -> Element<'static, Message> { - let content = text(label).size(11).color(LABEL).align_y(iced::Center); + let content = text(label).size(11).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() - }) + .style(button::subtle) .width(Fill) .padding([4, 10]); // Highlight the underlying object while the cursor is over this row. @@ -54,30 +40,3 @@ fn item_row(handle: acadrust::Handle, label: String) -> Element<'static, Message .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, -}; diff --git a/src/ui/popup/isolate_popup.rs b/src/ui/popup/isolate_popup.rs index 7121addc..de2200bf 100644 --- a/src/ui/popup/isolate_popup.rs +++ b/src/ui/popup/isolate_popup.rs @@ -1,7 +1,7 @@ //! Isolate / Hide / End Isolation status menu. use iced::widget::{button, row, text}; -use iced::{Background, Color, Element, Fill, Theme}; +use iced::{Element, Fill}; use crate::app::Message; use crate::ui::statusbar::status_menu::Entry; @@ -41,19 +41,11 @@ fn action_entry(label: &'static str, enabled: bool, msg: Message) -> Entry<'stat } 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 lbl = text(label).size(11); 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() - }) + .style(button::subtle) .width(Fill) .padding([4, 12]); if enabled { @@ -61,24 +53,3 @@ fn action_row(label: &'static str, enabled: bool, msg: Message) -> Element<'stat } btn.into() } - -// ── Colours ─────────────────────────────────────────────────────────────── - -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, -}; diff --git a/src/ui/popup/polar_popup.rs b/src/ui/popup/polar_popup.rs index 4393a3f9..4a1fa26d 100644 --- a/src/ui/popup/polar_popup.rs +++ b/src/ui/popup/polar_popup.rs @@ -1,7 +1,7 @@ //! Polar-tracking angle status menu. use iced::widget::{button, container, row, text, text_input}; -use iced::{Background, Color, Element, Fill, Length, Theme}; +use iced::{Element, Fill, Length}; use crate::app::Message; use crate::ui::statusbar::status_menu::Entry; @@ -41,7 +41,7 @@ pub fn menu_entries<'a>( let custom_row = container( row![ custom_field, - text("°").size(11).color(LABEL_OFF), + text("°").size(11), ] .spacing(4) .align_y(iced::Center), @@ -52,51 +52,16 @@ pub fn menu_entries<'a>( } fn angle_row<'a>(deg: f32, active: bool) -> Element<'a, Message> { - let check = crate::ui::icons::check_cell(active, CHECK_COLOR); + let check = crate::ui::icons::themed_check_cell(active); - let lbl = text(angle_label(deg)) - .size(11) - .color(if active { LABEL_ON } else { LABEL_OFF }); + let lbl = text(angle_label(deg)).size(11); let content = row![check, lbl].spacing(6).align_y(iced::Center); button(content) .on_press(Message::SetPolarAngle(deg)) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered => ROW_HOVER, - _ => Color::TRANSPARENT, - })), - ..Default::default() - }) + .style(button::subtle) .width(Fill) .padding([4, 10]) .into() } - -// ── Colours ─────────────────────────────────────────────────────────────── - -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, -}; diff --git a/src/ui/popup/scale_popup.rs b/src/ui/popup/scale_popup.rs index d5d31c77..33375ae4 100644 --- a/src/ui/popup/scale_popup.rs +++ b/src/ui/popup/scale_popup.rs @@ -1,7 +1,7 @@ //! Annotation / viewport scale status menu. use iced::widget::{button, row, text}; -use iced::{Background, Color, Element, Fill, Theme}; +use iced::{Element, Fill}; use crate::app::Message; use crate::ui::statusbar::status_menu::Entry; @@ -44,66 +44,25 @@ pub fn menu_entries( } fn scale_row(label: String, active: bool, msg: Message) -> Element<'static, Message> { - let check = crate::ui::icons::check_cell(active, CHECK_COLOR); + let check = crate::ui::icons::themed_check_cell(active); - let lbl = text(label) - .size(11) - .color(if active { LABEL_ON } else { LABEL_OFF }); + let lbl = text(label).size(11); 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() - }) + .style(button::subtle) .width(Fill) .padding([4, 10]) .into() } fn manage_row() -> Element<'static, Message> { - button(text("Manage...").size(11).color(CHECK_COLOR)) + button(text("Manage...").size(11)) .on_press(Message::ScaleManagerOpen) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered => ROW_HOVER, - _ => Color::TRANSPARENT, - })), - ..Default::default() - }) + .style(button::primary) .width(Fill) .padding([5, 10]) .into() } - -// ── Colours ─────────────────────────────────────────────────────────────── - -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, -}; diff --git a/src/ui/popup/selection_filter_popup.rs b/src/ui/popup/selection_filter_popup.rs index fdff751c..085e6677 100644 --- a/src/ui/popup/selection_filter_popup.rs +++ b/src/ui/popup/selection_filter_popup.rs @@ -5,7 +5,7 @@ use rustc_hash::FxHashSet as HashSet; use iced::widget::{button, container, row, text}; -use iced::{Background, Border, Color, Element, Fill, Theme}; +use iced::{Background, Element, Fill, Theme}; use crate::app::Message; use crate::ui::statusbar::status_menu::Entry; @@ -37,8 +37,10 @@ pub fn menu_entries( .padding([4u16, 8]); let divider = container(iced::widget::Space::new().height(1)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(DIVIDER)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.weak.color, + )), ..Default::default() }) .width(Fill) @@ -62,99 +64,41 @@ pub fn menu_entries( } fn type_row(name: String, included: bool) -> Element<'static, Message> { - let check = crate::ui::icons::check_cell(included, CHECK_COLOR); + let check = crate::ui::icons::themed_check_cell(included); - let lbl = text(name.clone()) - .size(11) - .color(if included { LABEL_ON } else { LABEL_OFF }); + let lbl = text(name.clone()).size(11); 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() - }) + .style(button::subtle) .width(Fill) .padding([4, 10]) .into() } fn empty_row() -> Element<'static, Message> { - container(text("No objects").size(11).color(LABEL_OFF)) + container( + text("No objects").size(11).style(|theme: &Theme| text::Style { + color: Some( + theme + .extended_palette() + .background + .base + .text + .scale_alpha(0.42), + ), + }), + ) .padding([4, 10]) .into() } fn header_btn(label: &str, msg: Message, enabled: bool) -> Element<'_, Message> { - let 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 = button(text(label).size(10).color(color)); + let b = button(text(label).size(10)); 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() - }) + b.style(button::secondary) .padding([3, 8]) .into() } - -// ── Colours ─────────────────────────────────────────────────────────────── - -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 DIVIDER: Color = Color { - r: 0.28, - g: 0.28, - b: 0.28, - 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.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, -}; diff --git a/src/ui/popup/snap_popup.rs b/src/ui/popup/snap_popup.rs index 8c88ca07..6259a9f4 100644 --- a/src/ui/popup/snap_popup.rs +++ b/src/ui/popup/snap_popup.rs @@ -1,7 +1,7 @@ //! OpenCADStudio-style OSNAP status menu. use iced::widget::{button, container, row, text}; -use iced::{Background, Border, Color, Element, Fill, Length, Theme}; +use iced::{Background, Element, Fill, Length, Theme}; use crate::app::Message; use crate::snap::{SnapType, Snapper, ALL_SNAP_MODES}; @@ -20,8 +20,10 @@ pub fn menu_entries<'a>(snapper: &'a Snapper) -> Vec> { // Divider let divider = container(iced::widget::Space::new().height(1)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(DIVIDER)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.weak.color, + )), ..Default::default() }) .width(Fill) @@ -41,21 +43,18 @@ pub fn menu_entries<'a>(snapper: &'a Snapper) -> Vec> { // ── 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); + let checkmark = crate::ui::icons::themed_check_cell(active); // 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::( + let icon_el = container(crate::ui::icons::themed_success::( 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 label_el = text(label).size(11); let content = row![checkmark, icon_el, label_el] .spacing(4) @@ -63,98 +62,16 @@ fn snap_row<'a>(snap_type: SnapType, label: &'a str, active: bool) -> Element<'a 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() - }) + .style(button::subtle) .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 = button(text(label).size(10)); 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() - }) + b.style(button::secondary) .padding([3, 8]) .into() } - -// ── Colours ─────────────────────────────────────────────────────────────── - -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.35, - g: 0.75, - b: 1.00, - a: 1.0, -}; -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, -}; diff --git a/src/ui/popup/units_popup.rs b/src/ui/popup/units_popup.rs index 86765673..d8301824 100644 --- a/src/ui/popup/units_popup.rs +++ b/src/ui/popup/units_popup.rs @@ -1,7 +1,7 @@ //! Drawing-units status menu. use iced::widget::{button, row, text}; -use iced::{Background, Color, Element, Fill, Theme}; +use iced::{Element, Fill}; use crate::app::Message; use crate::ui::statusbar::status_menu::Entry; @@ -49,51 +49,16 @@ pub fn menu_entries(current: i16) -> Vec> { } fn unit_row(label: &'static str, active: bool, msg: Message) -> Element<'static, Message> { - let check = crate::ui::icons::check_cell(active, CHECK_COLOR); + let check = crate::ui::icons::themed_check_cell(active); - let lbl = text(label) - .size(11) - .color(if active { LABEL_ON } else { LABEL_OFF }); + let lbl = text(label).size(11); 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() - }) + .style(button::subtle) .width(Fill) .padding([4, 10]) .into() } - -// ── Colours ─────────────────────────────────────────────────────────────── - -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, -}; diff --git a/src/ui/properties.rs b/src/ui/properties.rs index d5d49303..d7708e5f 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -208,9 +208,11 @@ impl PropertiesPanel { pub fn view(&self) -> Element<'_, Message> { // ── Header ────────────────────────────────────────────────────────── - let header = container(text("Properties").size(12).color(Color::WHITE)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(HEADER_BG)), + let header = container(text("Properties").size(12)) + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.weak.color, + )), ..Default::default() }) .width(Length::Fill) @@ -218,7 +220,10 @@ impl PropertiesPanel { // ── Title bar (entity type / "No Selection") ───────────────────── let title_content: Element<'_, Message> = if self.selection_groups.is_empty() { - text(crate::ui::text_util::elide(&self.title, 34)).size(FONT_SZ).color(SECTION_LABEL).into() + text(crate::ui::text_util::elide(&self.title, 34)) + .size(FONT_SZ) + .style(muted_text_style) + .into() } else { combo_box( &self.selection_group_combo, @@ -235,14 +240,17 @@ impl PropertiesPanel { }; let title_bar = container(title_content) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(SECTION_BG)), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.weakest.color)), border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 0.0.into(), }, ..Default::default() + } }) .width(Length::Fill) .padding([4, 10]); @@ -252,7 +260,7 @@ impl PropertiesPanel { container( text("Select an object to view properties") .size(10) - .color(HINT_COLOR), + .style(hint_text_style), ) .padding([10, 10]) .into() @@ -265,14 +273,17 @@ impl PropertiesPanel { }; container(column![header, title_bar, content]) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.base.color)), border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 0.0.into(), }, ..Default::default() + } }) .width(250) .height(Length::Fill) @@ -286,15 +297,22 @@ impl PropertiesPanel { if self.sections.is_empty() { return None; } - let title = container(text(crate::ui::text_util::elide(&self.title, 34)).size(FONT_SZ).color(SECTION_LABEL)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(SECTION_BG)), + let title = container( + text(crate::ui::text_util::elide(&self.title, 34)) + .size(FONT_SZ) + .style(muted_text_style), + ) + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.weakest.color)), border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 0.0.into(), }, ..Default::default() + } }) .width(Length::Fill) .padding([4, 10]); @@ -306,14 +324,17 @@ impl PropertiesPanel { Some( container(col) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.base.color)), border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 3.0.into(), }, ..Default::default() + } }) .width(230) .into(), @@ -324,15 +345,18 @@ impl PropertiesPanel { fn render_section<'a>(&'a self, section: &'a PropSection) -> Element<'a, Message> { // Section header - let hdr = container(text(§ion.title).size(10).color(Color::WHITE)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(SECTION_HDR_BG)), + let hdr = container(text(§ion.title).size(10)) + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.weak.color)), border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 0.0.into(), }, ..Default::default() + } }) .width(Length::Fill) .padding([3, 8]); @@ -508,31 +532,25 @@ impl PropertiesPanel { fn render_color_varies_row<'a>(&'a self, label: &'a str) -> Element<'a, Message> { let color_btn = button( row![ - container(text("?").size(10).color(VALUE_COLOR)) - .style(move |_: &Theme| container::Style { - background: Some(Background::Color(Color { - r: 0.32, - g: 0.32, - b: 0.32, - a: 1.0, - })), + container(text("?").size(10)) + .style(move |theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.strong.color)), border: Border { - color: Color { - r: 0.0, - g: 0.0, - b: 0.0, - a: 0.5 - }, + color: palette.background.neutral.color, width: 1.0, radius: 2.0.into() }, + text_color: Some(palette.background.strong.text), ..Default::default() + } }) .width(SWATCH_SZ) .height(SWATCH_SZ) .align_x(iced::Center) .align_y(iced::Center), - text(VARIES_LABEL).size(FONT_SZ).color(VALUE_COLOR), + text(VARIES_LABEL).size(FONT_SZ), ] .spacing(4) .align_y(iced::Center), @@ -724,7 +742,7 @@ impl PropertiesPanel { .on_input(move |v| Message::PropGeomInput { field, value: v }) .on_submit(Message::PropGeomCommit(field)) .size(FONT_SZ) - .style(|_: &Theme, status| text_input::Style { + .style(|theme: &Theme, status| text_input::Style { // The wrapping container draws the border; keep the input flat // so field + caret read as one control. border: Border { @@ -732,33 +750,32 @@ impl PropertiesPanel { width: 0.0, radius: 0.0.into(), }, - ..text_input_style(&Theme::Dark, status) + ..text_input_style(theme, status) }) .padding([3, 6]) .width(Length::Fill); let caret = button( - container(crate::ui::icons::arrow_toggle( - self.edit_choice_open, - FONT_SZ, - VALUE_COLOR, - )) + container(if self.edit_choice_open { + crate::ui::icons::themed_arrow_up(FONT_SZ) + } else { + crate::ui::icons::themed_arrow_down(FONT_SZ) + }) .height(Length::Fill) .align_y(iced::Center), ) .on_press(Message::PropEditChoiceToggle) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => Color { - r: 0.28, - g: 0.28, - b: 0.28, - a: 1.0, - }, - _ => VALUE_BG, - })), - text_color: VALUE_COLOR, + .style(|theme: &Theme, status| { + let palette = theme.extended_palette(); + let pair = match status { + button::Status::Hovered | button::Status::Pressed => palette.background.weak, + _ => palette.background.base, + }; + button::Style { + background: Some(Background::Color(pair.color)), + text_color: pair.text, border: Border::default(), ..Default::default() + } }) .padding(Padding { top: 0.0, @@ -768,14 +785,17 @@ impl PropertiesPanel { }) .height(Length::Fixed(ROW_H - 6.0)); let head = container(row![input, caret].align_y(iced::Center)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(VALUE_BG)), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.base.color)), border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 2.0.into(), }, ..Default::default() + } }) .width(Length::Fill); @@ -794,39 +814,15 @@ impl PropertiesPanel { } let value = opt.clone(); list = list.push( - button(text(opt.as_str()).size(FONT_SZ).color(VALUE_COLOR)) + button(text(opt.as_str()).size(FONT_SZ)) .on_press(Message::PropGeomChoiceChanged { field, value }) - .style(|_: &Theme, status| button::Style { - background: matches!(status, button::Status::Hovered).then_some( - Background::Color(Color { - r: 0.25, - g: 0.45, - b: 0.70, - a: 1.0, - }), - ), - text_color: VALUE_COLOR, - ..Default::default() - }) + .style(button::subtle) .padding([2, 6]) .width(Length::Fill), ); } let popup = container(scrollable(list).height(Length::Shrink)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { - r: 0.17, - g: 0.17, - b: 0.17, - a: 1.0, - })), - border: Border { - color: BORDER, - width: 1.0, - radius: 2.0.into(), - }, - ..Default::default() - }) + .style(container::bordered_box) .padding(2) .width(200) .max_height(220.0); @@ -940,13 +936,13 @@ pub fn color_picker_dropdown<'a>( r.push( button(text("").width(18).height(18)) .on_press(msg) - .style(move |_: &Theme, status| button::Style { + .style(move |theme: &Theme, status| button::Style { background: Some(Background::Color(bg)), border: Border { color: if matches!(status, button::Status::Hovered) { - Color::WHITE + theme.extended_palette().primary.base.color } else { - Color::BLACK + theme.extended_palette().background.neutral.color }, width: if matches!(status, button::Status::Hovered) { 1.5 @@ -965,20 +961,20 @@ pub fn color_picker_dropdown<'a>( // "More Colors…" toggle button let more_btn = button( row![ - crate::ui::icons::arrow_toggle(palette_open, 9.0, HINT_COLOR), + if palette_open { + crate::ui::icons::themed_arrow_up(9.0) + } else { + crate::ui::icons::themed_arrow_down(9.0) + }, text(if palette_open { "Less" } else { "More Colors…" }) .size(10) - .color(HINT_COLOR), + .style(hint_text_style), ] .spacing(4) .align_y(iced::Center), ) .on_press(palette_toggle_msg) - .style(|_: &Theme, _| button::Style { - background: Some(Background::Color(PICKER_BG)), - text_color: HINT_COLOR, - ..Default::default() - }) + .style(button::subtle) .padding([2, 6]) .width(Length::Fill); @@ -989,14 +985,17 @@ pub fn color_picker_dropdown<'a>( }; let mut col = column![container(inner) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PICKER_BG)), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.base.color)), border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 0.0.into() }, ..Default::default() + } }) .padding([6, 8]) .width(Length::Fill)] @@ -1019,18 +1018,13 @@ pub fn color_picker_dropdown<'a>( r = r.push( button(text("").width(12).height(12)) .on_press(msg) - .style(move |_: &Theme, status| button::Style { + .style(move |theme: &Theme, status| button::Style { background: Some(Background::Color(bg)), border: Border { color: if matches!(status, button::Status::Hovered) { - Color::WHITE + theme.extended_palette().primary.base.color } else { - Color { - r: 0.0, - g: 0.0, - b: 0.0, - a: 0.4, - } + theme.extended_palette().background.neutral.color }, width: if matches!(status, button::Status::Hovered) { 1.5 @@ -1049,14 +1043,17 @@ pub fn color_picker_dropdown<'a>( } col = col.push( container(scrollable(rows).height(160)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PICKER_BG)), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.base.color)), border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 0.0.into(), }, ..Default::default() + } }) .padding([4, 6]) .width(Length::Fill), @@ -1071,22 +1068,23 @@ pub fn color_picker_dropdown<'a>( /// A boolean toggle button row (for "Invisible" etc.). fn render_stepper_row<'a>(label: &'a str, display: &'a str) -> Element<'a, Message> { let arrow = |glyph: &'static str, delta: i8| { - button(text(glyph).size(FONT_SZ).color(VALUE_COLOR)) + button(text(glyph).size(FONT_SZ)) .on_press(Message::PropVertexStep(delta)) .padding([0, 6]) - .style(|_: &Theme, status| { - let bg = match status { - button::Status::Hovered | button::Status::Pressed => HOVER_BG, - _ => VALUE_BG, + .style(|theme: &Theme, status| { + let palette = theme.extended_palette(); + let pair = match status { + button::Status::Hovered | button::Status::Pressed => palette.background.weak, + _ => palette.background.base, }; button::Style { - background: Some(Background::Color(bg)), + background: Some(Background::Color(pair.color)), border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 2.0.into(), }, - text_color: VALUE_COLOR, + text_color: pair.text, ..Default::default() } }) @@ -1095,7 +1093,6 @@ fn render_stepper_row<'a>(label: &'a str, display: &'a str) -> Element<'a, Messa arrow("◀", -1), text(display) .size(FONT_SZ) - .color(VALUE_COLOR) .width(Length::Fill) .align_x(iced::Center), arrow("▶", 1), @@ -1111,22 +1108,29 @@ fn render_bool_row<'a>(label: &'a str, field: &'static str, value: bool) -> Elem button( text(btn_label) .size(FONT_SZ) - .color(if value { WARN_COLOR } else { VALUE_COLOR }), + .style(move |theme: &Theme| iced::widget::text::Style { + color: value.then_some(theme.extended_palette().warning.base.color), + }), ) .on_press(Message::PropBoolToggle(field)) - .style(move |_: &Theme, status| { - let bg = match status { - button::Status::Hovered | button::Status::Pressed => HOVER_BG, - _ => VALUE_BG, + .style(move |theme: &Theme, status| { + let palette = theme.extended_palette(); + let pair = match status { + button::Status::Hovered | button::Status::Pressed => palette.background.weak, + _ => palette.background.base, }; button::Style { - background: Some(Background::Color(bg)), + background: Some(Background::Color(pair.color)), border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 2.0.into(), }, - text_color: if value { WARN_COLOR } else { VALUE_COLOR }, + text_color: if value { + palette.warning.base.color + } else { + pair.text + }, ..Default::default() } }) @@ -1211,10 +1215,14 @@ fn render_group_row( let label_btn = button( container( row![ - crate::ui::icons::arrow_toggle(expanded, FONT_SZ, LABEL_COLOR), + if expanded { + crate::ui::icons::themed_arrow_down(FONT_SZ) + } else { + crate::ui::icons::themed_arrow_right(FONT_SZ) + }, text(crate::ui::text_util::elide(base, 16)) .size(FONT_SZ) - .color(LABEL_COLOR), + .style(muted_text_style), ] .spacing(4) .align_y(iced::Center), @@ -1223,20 +1231,7 @@ fn render_group_row( .align_y(iced::Center), ) .on_press(Message::PropGroupToggle(key)) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => Color { - r: 0.24, - g: 0.24, - b: 0.24, - a: 1.0, - }, - _ => LABEL_BG, - })), - text_color: LABEL_COLOR, - border: Border::default(), - ..Default::default() - }) + .style(button::subtle) .padding(Padding { top: 0.0, bottom: 0.0, @@ -1246,8 +1241,10 @@ fn render_group_row( .width(Length::Fill) .height(Length::Fixed(ROW_H)); let label_col = container(label_btn) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(LABEL_BG)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.weakest.color, + )), ..Default::default() }) .width(Length::FillPortion(5)) @@ -1262,8 +1259,10 @@ fn render_group_row( .padding([3, 6]) .width(Length::Fill); let value_col = container(value_field) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(VALUE_BG)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.base.color, + )), ..Default::default() }) .width(Length::FillPortion(6)) @@ -1278,9 +1277,9 @@ fn render_group_row( container(row![label_col, value_col]) .height(Length::Fixed(ROW_H)) - .style(|_: &Theme| container::Style { + .style(|theme: &Theme| container::Style { border: Border { - color: BORDER, + color: theme.extended_palette().background.neutral.color, width: 1.0, radius: 0.0.into(), }, @@ -1305,9 +1304,15 @@ fn render_ro_row<'a>(label: &'a str, value: &'a str) -> Element<'a, Message> { /// Build a label | widget property row. fn prop_row_widget<'a>(label: &'a str, widget: Element<'a, Message>) -> Element<'a, Message> { - let label_col = container(text(crate::ui::text_util::elide(label, 18)).size(FONT_SZ).color(LABEL_COLOR)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(LABEL_BG)), + let label_col = container( + text(crate::ui::text_util::elide(label, 18)) + .size(FONT_SZ) + .style(muted_text_style), + ) + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.weakest.color, + )), ..Default::default() }) .width(Length::FillPortion(5)) @@ -1320,8 +1325,10 @@ fn prop_row_widget<'a>(label: &'a str, widget: Element<'a, Message>) -> Element< right: 6.0, }); let value_col = container(widget) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(VALUE_BG)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.base.color, + )), ..Default::default() }) .width(Length::FillPortion(6)) @@ -1335,9 +1342,9 @@ fn prop_row_widget<'a>(label: &'a str, widget: Element<'a, Message>) -> Element< }); container(row![label_col, value_col]) .height(Length::Fixed(ROW_H)) - .style(|_: &Theme| container::Style { + .style(|theme: &Theme| container::Style { border: Border { - color: BORDER, + color: theme.extended_palette().background.neutral.color, width: 1.0, radius: 0.0.into(), }, @@ -1348,18 +1355,9 @@ fn prop_row_widget<'a>(label: &'a str, widget: Element<'a, Message>) -> Element< /// A plain text button used inside the color picker for ByLayer / ByBlock. fn picker_text_btn(label: &str, msg: Message) -> Element<'_, Message> { - button(text(label).size(FONT_SZ).color(VALUE_COLOR)) + button(text(label).size(FONT_SZ)) .on_press(msg) - .style(|_: &Theme, _| button::Style { - background: Some(Background::Color(LABEL_BG)), - border: Border { - color: BORDER, - width: 1.0, - radius: 2.0.into(), - }, - text_color: VALUE_COLOR, - ..Default::default() - }) + .style(button::secondary) .padding([2, 8]) .into() } @@ -1419,49 +1417,41 @@ fn aci_label(idx: u8) -> &'static str { // ── Widget style helpers ────────────────────────────────────────────────── -fn combo_btn_style(_theme: &Theme, status: button::Status) -> button::Style { - let bg = match status { - button::Status::Hovered | button::Status::Pressed => HOVER_BG, - _ => VALUE_BG, +fn combo_btn_style(theme: &Theme, status: button::Status) -> button::Style { + let palette = theme.extended_palette(); + let pair = match status { + button::Status::Hovered | button::Status::Pressed => palette.background.weak, + _ => palette.background.base, }; button::Style { - background: Some(Background::Color(bg)), + background: Some(Background::Color(pair.color)), border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 2.0.into(), }, - text_color: VALUE_COLOR, + text_color: pair.text, ..Default::default() } } -fn text_input_style(_theme: &Theme, status: text_input::Status) -> text_input::Style { +fn text_input_style(theme: &Theme, status: text_input::Status) -> text_input::Style { + let palette = theme.extended_palette(); let border_color = match status { - text_input::Status::Focused { .. } => Color { - r: 0.3, - g: 0.6, - b: 1.0, - a: 1.0, - }, - _ => BORDER, + text_input::Status::Focused { .. } => palette.primary.base.color, + _ => palette.background.neutral.color, }; text_input::Style { - background: Background::Color(VALUE_BG), + background: Background::Color(palette.background.base.color), border: Border { color: border_color, width: 1.0, radius: 2.0.into(), }, icon: Color::TRANSPARENT, - placeholder: HINT_COLOR, - value: VALUE_COLOR, - selection: Color { - r: 0.2, - g: 0.4, - b: 0.8, - a: 0.5, - }, + placeholder: palette.background.base.text.scale_alpha(0.48), + value: palette.background.base.text, + selection: palette.primary.base.color.scale_alpha(0.5), } } @@ -1472,109 +1462,30 @@ fn combo_input_style(theme: &Theme, status: text_input::Status) -> text_input::S /// Style for a read-only-but-selectable value field: flat (no input box or /// focus highlight, so it reads as plain text, unlike the bordered editable /// fields) yet with a visible selection colour so Ctrl+C copy is discoverable. -fn ro_input_style(_theme: &Theme, _status: text_input::Status) -> text_input::Style { +fn ro_input_style(theme: &Theme, _status: text_input::Status) -> text_input::Style { + let palette = theme.extended_palette(); text_input::Style { - background: Background::Color(VALUE_BG), + background: Background::Color(palette.background.base.color), border: Border { color: Color::TRANSPARENT, width: 0.0, radius: 0.0.into(), }, icon: Color::TRANSPARENT, - placeholder: HINT_COLOR, - value: VALUE_COLOR, - selection: Color { - r: 0.2, - g: 0.4, - b: 0.8, - a: 0.5, - }, + placeholder: palette.background.base.text.scale_alpha(0.48), + value: palette.background.base.text, + selection: palette.primary.base.color.scale_alpha(0.5), } } -// ── Colour constants ────────────────────────────────────────────────────── +fn muted_text_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().background.base.text.scale_alpha(0.72)), + } +} -const PANEL_BG: Color = Color { - r: 0.19, - g: 0.19, - b: 0.19, - a: 1.0, -}; -const HEADER_BG: Color = Color { - r: 0.24, - g: 0.24, - b: 0.24, - a: 1.0, -}; -const SECTION_BG: Color = Color { - r: 0.21, - g: 0.21, - b: 0.21, - a: 1.0, -}; -const SECTION_HDR_BG: Color = Color { - r: 0.26, - g: 0.26, - b: 0.28, - a: 1.0, -}; -const LABEL_BG: Color = Color { - r: 0.22, - g: 0.22, - b: 0.22, - a: 1.0, -}; -const VALUE_BG: Color = Color { - r: 0.18, - g: 0.18, - b: 0.18, - a: 1.0, -}; -const HOVER_BG: Color = Color { - r: 0.25, - g: 0.25, - b: 0.28, - a: 1.0, -}; -const PICKER_BG: Color = Color { - r: 0.16, - g: 0.16, - b: 0.18, - a: 1.0, -}; -const LABEL_COLOR: Color = Color { - r: 0.70, - g: 0.70, - b: 0.70, - a: 1.0, -}; -const VALUE_COLOR: Color = Color { - r: 0.90, - g: 0.90, - b: 0.90, - a: 1.0, -}; -const HINT_COLOR: Color = Color { - r: 0.45, - g: 0.45, - b: 0.50, - a: 1.0, -}; -const SECTION_LABEL: Color = Color { - r: 0.75, - g: 0.75, - b: 0.75, - a: 1.0, -}; -const BORDER: Color = Color { - r: 0.32, - g: 0.32, - b: 0.32, - a: 1.0, -}; -const WARN_COLOR: Color = Color { - r: 1.00, - g: 0.60, - b: 0.10, - a: 1.0, -}; +fn hint_text_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().background.base.text.scale_alpha(0.48)), + } +} diff --git a/src/ui/ribbon/collapse.rs b/src/ui/ribbon/collapse.rs index 6f753c34..c7c193e2 100644 --- a/src/ui/ribbon/collapse.rs +++ b/src/ui/ribbon/collapse.rs @@ -15,7 +15,7 @@ use iced::advanced::layout::{self, Layout}; use iced::advanced::widget::{self, Widget}; use iced::advanced::{mouse, overlay, renderer, Clipboard, Renderer as _, Shell}; use iced::{ - Background, Border, Color, Element, Event, Length, Point, Rectangle, Renderer, Shadow, Size, + Background, Border, Element, Event, Length, Point, Rectangle, Renderer, Shadow, Size, Theme, Vector, }; @@ -113,8 +113,6 @@ pub struct CollapsePanels<'a> { open: Option, /// Fallback row height, used only when there are no panels to measure. row_h: f32, - /// Colour of the 1px divider drawn between panels. - divider: Color, /// Chosen degradation level per panel; set during layout. levels: RefCell>, /// If set, the measured row height is written here each layout (read when @@ -129,13 +127,12 @@ pub struct CollapsePanels<'a> { } impl<'a> CollapsePanels<'a> { - pub fn new(panels: Vec>, open: Option, row_h: f32, divider: Color) -> Self { + pub fn new(panels: Vec>, open: Option, row_h: f32) -> Self { let n = panels.len(); Self { panels, open, row_h, - divider, levels: RefCell::new(vec![FULL; n]), height_out: None, tight_out: None, @@ -480,7 +477,7 @@ impl<'a> Widget for CollapsePanels<'a> { shadow: Shadow::default(), snap: true, }, - Background::Color(self.divider), + Background::Color(theme.extended_palette().background.neutral.color), ); } } diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs index 22a44c3e..233b9bd2 100644 --- a/src/ui/ribbon/mod.rs +++ b/src/ui/ribbon/mod.rs @@ -12,7 +12,7 @@ use std::sync::Arc; use rustc_hash::FxHashMap as HashMap; use acadrust::types::{Color as AcadColor, LineWeight}; -use iced::widget::{button, column, container, mouse_area, row, scrollable, svg, text}; +use iced::widget::{button, column, container, mouse_area, row, scrollable, text}; use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme}; use crate::app::Message; @@ -419,58 +419,41 @@ impl Ribbon { let is_active = i == self.active; let is_contextual = module.id() == "layout"; - let accent = if is_contextual { - ACCENT_GOLD - } else { - ACCENT_BLUE - }; - let text_inactive = if is_contextual { - Color { - r: 0.90, - g: 0.72, - b: 0.30, - a: 1.0, - } - } else { - Color { - r: 0.75, - g: 0.75, - b: 0.75, - a: 1.0, - } - }; - let hover_bg = if is_contextual { - Color { - r: 0.28, - g: 0.24, - b: 0.12, - a: 1.0, - } - } else { - Color { - r: 0.25, - g: 0.25, - b: 0.25, - a: 1.0, - } - }; let btn = container( button(text(module.title()).size(12)) .on_press(Message::RibbonSelectTab(i)) - .style(move |_: &Theme, status| button::Style { - background: Some(Background::Color(match (is_active, status) { - (true, _) => RIBBON_BG, - (false, button::Status::Hovered) => hover_bg, - _ => Color::TRANSPARENT, - })), - text_color: if is_active { - Color::WHITE + .style(move |theme: &Theme, status| { + let palette = theme.extended_palette(); + let accent = if is_contextual { + palette.warning.base } else { - text_inactive + palette.primary.base + }; + let pair = match (is_active, status) { + (true, _) => palette.background.weakest, + (false, button::Status::Hovered) => { + if is_contextual { + palette.warning.weak + } else { + palette.background.weak + } + } + _ => palette.background.base, + }; + button::Style { + background: (is_active + || matches!(status, button::Status::Hovered)) + .then_some(Background::Color(pair.color)), + text_color: if is_active { + pair.text + } else if is_contextual { + accent.color + } else { + palette.background.base.text.scale_alpha(0.72) }, border: Border { color: if is_active { - accent + accent.color } else { Color::TRANSPARENT }, @@ -479,13 +462,18 @@ impl Ribbon { }, shadow: iced::Shadow::default(), snap: false, + } }) .padding([5, 14]), ) - .style(move |_: &Theme| container::Style { + .style(move |theme: &Theme| container::Style { border: Border { color: if is_active { - accent + if is_contextual { + theme.extended_palette().warning.base.color + } else { + theme.extended_palette().primary.base.color + } } else { Color::TRANSPARENT }, @@ -514,9 +502,11 @@ impl Ribbon { // persisted (see `Ribbon::set_collapse_mode`). It hides itself once the // tool row is tight, giving the cramped tab row its space back. let dd_open = self.open_dropdown.as_deref() == Some(COLLAPSE_MODE_ID); - let mode_btn = button(crate::ui::icons::arrow_down(10.0, ARROW_COLOR)) + let mode_btn = button(crate::ui::icons::themed_arrow_down(10.0)) .on_press(Message::ToggleRibbonDropdown(COLLAPSE_MODE_ID.to_string())) - .style(move |_: &Theme, status| top_hist_btn_style(true, dd_open, status)) + .style(move |theme: &Theme, status| { + top_hist_btn_style(theme, true, dd_open, status) + }) .height(24) .padding([2, 8]); let mode_dd = PosReport::new(COLLAPSE_MODE_ID, mode_btn); @@ -534,8 +524,10 @@ impl Ribbon { } let tab_bar = container(tab_row) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(TOPBAR_BG)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.base.color, + )), ..Default::default() }) .padding(Padding { @@ -653,10 +645,12 @@ impl Ribbon { self.active_lineweight, &style_ctx, )) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(RIBBON_BG)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.weakest.color, + )), border: Border { - color: BORDER_DARK, + color: theme.extended_palette().background.neutral.color, width: 1.0, radius: 0.0.into(), }, @@ -666,7 +660,7 @@ impl Ribbon { } }) .collect(); - CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H, BORDER_DARK) + CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H) .report_height(self.tool_bar_h.clone()) .report_tight(self.collapse_tight.clone()) .mode(self.collapse_mode) @@ -676,10 +670,12 @@ impl Ribbon { }; let tool_bar = container(tool_area) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(RIBBON_BG)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.weakest.color, + )), border: Border { - color: BORDER_DARK, + color: theme.extended_palette().background.neutral.color, width: 1.0, radius: 0.0.into(), }, @@ -712,19 +708,13 @@ impl Ribbon { .enumerate() .map(|(idx, label)| { let step = idx + 1; - button(text(label.clone()).size(11).color(LABEL_ON)) + button(text(label.clone()).size(11)) .on_press(if is_undo { Message::UndoMany(step) } else { Message::RedoMany(step) }) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => ROW_HOVER, - _ => Color::TRANSPARENT, - })), - ..Default::default() - }) + .style(popup_row_style) .width(Fill) .padding([5, 10]) .into() @@ -732,15 +722,7 @@ impl Ribbon { .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() - }) + .style(popup_panel_style) .width(Length::Fixed(170.0)); let (align_right, h_pad, top) = self.dd_anchor(open_id, 170.0, win.0); @@ -755,27 +737,16 @@ impl Ribbon { let rows: Vec> = CollapseMode::ALL .iter() .map(|&m| { - let mark: Element = if m == current { - crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR) - } else { - iced::widget::Space::new().width(0).into() - }; button( row![ - container(mark).width(Length::Fixed(16.0)), - text(m.label()).size(11).color(LABEL_ON), + crate::ui::icons::themed_check_cell(m == current), + text(m.label()).size(11), ] .spacing(4) .align_y(iced::Center), ) .on_press(Message::SetRibbonCollapseMode(m)) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => ROW_HOVER, - _ => Color::TRANSPARENT, - })), - ..Default::default() - }) + .style(popup_row_style) .width(Fill) .padding([5, 10]) .into() @@ -783,15 +754,7 @@ impl Ribbon { .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() - }) + .style(popup_panel_style) .width(Length::Fixed(W)); let (align_right, h_pad, top) = self.dd_anchor(open_id, W, win.0); @@ -852,28 +815,25 @@ impl Ribbon { .iter() .map(|(cmd, label, item_icon)| { let is_current = *cmd == last_cmd; - let checkmark: Element<'_, Message> = container(if is_current { - crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR) - } else { - iced::widget::Space::new().width(0).into() - }) - .width(Length::Fixed(14.0)) - .into(); - let icon_el: Element = match *item_icon { - IconKind::Glyph(s) => text(s) - .size(13) - .color(ICON_COLOR) + let checkmark: Element<'_, Message> = + crate::ui::icons::themed_check_cell(is_current); + let icon_el: Element = + container(make_icon(*item_icon, 20.0)) .width(Length::Fixed(20.0)) - .into(), - IconKind::Svg(bytes) => { - let handle = svg::Handle::from_memory(bytes); - svg(handle).width(20).height(20).into() - } - }; + .into(); let label_el = text(*label) .size(11) - .color(if is_current { LABEL_ON } else { LABEL_OFF }); + .style(move |theme: &Theme| iced::widget::text::Style { + color: (!is_current).then_some( + theme + .extended_palette() + .background + .base + .text + .scale_alpha(0.72), + ), + }); button( row![checkmark, icon_el, label_el] @@ -884,13 +844,7 @@ impl Ribbon { dropdown_id: dd_id, cmd: *cmd, }) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => ROW_HOVER, - _ => Color::TRANSPARENT, - })), - ..Default::default() - }) + .style(popup_row_style) .width(Fill) .padding([4, 10]) .into() @@ -898,15 +852,7 @@ impl Ribbon { .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() - }) + .style(popup_panel_style) .width(Length::Fixed(190.0)); let (align_right, h_pad, top) = self.dd_anchor(open_id, 190.0, win.0); @@ -925,13 +871,7 @@ impl Ribbon { let icon_btn = |bytes: &'static [u8], msg: Message| -> Element<'_, Message> { button(crate::ui::icons::raw(bytes, 14.0)) .on_press(msg) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => ROW_HOVER, - _ => Color::TRANSPARENT, - })), - ..Default::default() - }) + .style(popup_row_style) .padding([2, 4]) .into() }; @@ -952,10 +892,10 @@ impl Ribbon { let name = info.name.clone(); let swatch = container(text("")) - .style(move |_: &Theme| container::Style { + .style(move |theme: &Theme| container::Style { background: Some(Background::Color(lc)), border: Border { - color: SWATCH_BORDER, + color: theme.extended_palette().background.strong.color, width: 1.0, radius: 1.0.into(), }, @@ -976,29 +916,27 @@ impl Ribbon { crate::ui::icons::layer_lock(ll), Message::LayerToggleLock(index), ); - let checkmark: Element<'_, Message> = container(if is_active { - crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR) - } else { - iced::widget::Space::new().width(0).into() - }) - .width(Length::Fixed(14.0)) - .into(); + let checkmark: Element<'_, Message> = + crate::ui::icons::themed_check_cell(is_active); let label = text(&info.name) .size(11) - .color(if is_active { LABEL_ON } else { LABEL_OFF }); + .style(move |theme: &Theme| iced::widget::text::Style { + color: (!is_active).then_some( + theme + .extended_palette() + .background + .base + .text + .scale_alpha(0.72), + ), + }); // The swatch + label area selects the layer as active; the // icon buttons above handle their own toggles. let select = button(row![swatch, label].spacing(5).align_y(iced::Center)) .on_press(Message::RibbonLayerChanged(name)) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => ROW_HOVER, - _ => Color::TRANSPARENT, - })), - ..Default::default() - }) + .style(popup_row_style) .width(Fill) .padding([4, 4]); @@ -1021,34 +959,7 @@ impl Ribbon { let search = iced::widget::text_input("Search layers…", &self.layer_filter) .on_input(Message::RibbonLayerFilterChanged) .size(11) - .padding([4, 6]) - .style(|_: &Theme, _| iced::widget::text_input::Style { - background: Background::Color(Color { - r: 0.10, - g: 0.10, - b: 0.10, - a: 1.0, - }), - border: Border { - color: PANEL_BORDER, - width: 1.0, - radius: 2.0.into(), - }, - icon: Color::WHITE, - placeholder: Color { - r: 0.45, - g: 0.45, - b: 0.45, - a: 1.0, - }, - value: Color::WHITE, - selection: Color { - r: 0.20, - g: 0.44, - b: 0.72, - a: 0.5, - }, - }); + .padding([4, 6]); let panel = container( column![ container(search).padding([4, 4]), @@ -1056,15 +967,7 @@ impl Ribbon { ] .spacing(2), ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), - border: Border { - color: PANEL_BORDER, - width: 1.0, - radius: 3.0.into(), - }, - ..Default::default() - }) + .style(popup_panel_style) .width(Length::Fixed(220.0)); let (align_right, h_pad, top) = self.dd_anchor(LAYER_COMBO_ID, 220.0, win.0); @@ -1113,35 +1016,29 @@ impl Ribbon { }; let active = ctx.active_for(style_key).to_string(); - let row_style = |_: &Theme, status: button::Status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => ROW_HOVER, - _ => Color::TRANSPARENT, - })), - ..Default::default() - }; - let mut rows: Vec> = ctx .names_for(style_key) .iter() .map(|name| { let is_sel = name.as_str() == active.as_str(); let n = name.clone(); - let checkmark: Element = container(if is_sel { - crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR) - } else { - iced::widget::Space::new().width(0).into() - }) - .width(Length::Fixed(14.0)) - .into(); + let checkmark: Element = + crate::ui::icons::themed_check_cell(is_sel); button( row![ checkmark, - text(name.clone()).size(11).color(if is_sel { - LABEL_ON - } else { - LABEL_OFF - }), + text(name.clone()) + .size(11) + .style(move |theme: &Theme| iced::widget::text::Style { + color: (!is_sel).then_some( + theme + .extended_palette() + .background + .base + .text + .scale_alpha(0.72), + ), + }), ] .spacing(4) .align_y(iced::Center), @@ -1150,7 +1047,7 @@ impl Ribbon { key: style_key, name: n, }) - .style(row_style) + .style(popup_row_style) .width(Fill) .padding([4, 10]) .into() @@ -1159,9 +1056,9 @@ impl Ribbon { if let Some(mgr) = manager_cmd { rows.push( - button(text("Manage…").size(11).color(LABEL_ON)) + button(text("Manage…").size(11)) .on_press(Message::Command(mgr.to_string())) - .style(row_style) + .style(popup_row_style) .width(Fill) .padding([4, 10]) .into(), @@ -1169,15 +1066,7 @@ impl Ribbon { } 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() - }) + .style(popup_panel_style) .width(Length::Fixed(LARGE_W * 2.3)); let (align_right, h_pad, top) = self.dd_anchor(open_id, LARGE_W * 2.3, win.0); @@ -1200,15 +1089,7 @@ impl Ribbon { ); let panel = container(picker) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), - border: Border { - color: PANEL_BORDER, - width: 1.0, - radius: 3.0.into(), - }, - ..Default::default() - }) + .style(popup_panel_style) .width(Length::Fixed(200.0)); let (align_right, h_pad, top) = self.dd_anchor(PROP_COLOR_ID, 200.0, win.0); @@ -1243,23 +1124,22 @@ impl Ribbon { .into_iter() .map(|lt| { let is_cur = lt.name == *active_lt; - let check: Element<'_, Message> = container(if is_cur { - crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR) - } else { - iced::widget::Space::new().width(0).into() - }) - .width(Length::Fixed(14.0)) - .into(); + let check: Element<'_, Message> = + crate::ui::icons::themed_check_cell(is_cur); let name_col = text(lt.name.clone()) .size(11) - .color(if is_cur { LABEL_ON } else { LABEL_OFF }) + .style(move |theme: &Theme| iced::widget::text::Style { + color: (!is_cur).then_some( + theme + .extended_palette() + .background + .base + .text + .scale_alpha(0.72), + ), + }) .width(Length::Fixed(90.0)); - let art_col = text(lt.art.clone()).size(9).color(Color { - r: 0.55, - g: 0.55, - b: 0.55, - a: 1.0, - }); + let art_col = text(lt.art.clone()).size(9).style(muted_text_style); let name = lt.name.clone(); button( row![check, name_col, art_col] @@ -1267,13 +1147,7 @@ impl Ribbon { .align_y(iced::Center), ) .on_press(Message::RibbonLinetypeChanged(name)) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => ROW_HOVER, - _ => Color::TRANSPARENT, - })), - ..Default::default() - }) + .style(popup_row_style) .width(Fill) .padding([4, 6]) .into() @@ -1281,15 +1155,7 @@ impl Ribbon { .collect(); let list = container(scrollable(column(rows)).height(Length::Fixed(200.0))) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), - border: Border { - color: PANEL_BORDER, - width: 1.0, - radius: 3.0.into(), - }, - ..Default::default() - }) + .style(popup_panel_style) .width(Length::Fixed(220.0)); let (align_right, h_pad, top) = self.dd_anchor(PROP_LINETYPE_ID, 220.0, win.0); @@ -1308,31 +1174,29 @@ impl Ribbon { .map(|item| { let is_cur = item.0 == active_lw; let label = item.to_string(); - let check: Element<'_, Message> = container(if is_cur { - crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR) - } else { - iced::widget::Space::new().width(0).into() - }) - .width(Length::Fixed(14.0)) - .into(); + let check: Element<'_, Message> = + crate::ui::icons::themed_check_cell(is_cur); button( row![ check, text(label) .size(11) - .color(if is_cur { LABEL_ON } else { LABEL_OFF }) + .style(move |theme: &Theme| iced::widget::text::Style { + color: (!is_cur).then_some( + theme + .extended_palette() + .background + .base + .text + .scale_alpha(0.72), + ), + }) ] .spacing(5) .align_y(iced::Center), ) .on_press(Message::RibbonLineweightChanged(item.0)) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => ROW_HOVER, - _ => Color::TRANSPARENT, - })), - ..Default::default() - }) + .style(popup_row_style) .width(Fill) .padding([4, 8]) .into() @@ -1350,15 +1214,7 @@ impl Ribbon { win: (f32, f32), ) -> Option> { 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() - }) + .style(popup_panel_style) .width(Length::Fixed(width)); let (align_right, h_pad, top) = self.dd_anchor(dd_id, width, win.0); @@ -1439,7 +1295,7 @@ fn render_group<'a>( column![ tools_el, - container(text(group.title).size(9).color(GROUP_LABEL)).padding([1, 4]), + container(text(group.title).size(9).style(muted_text_style)).padding([1, 4]), ] .align_x(iced::Center) .spacing(0) @@ -1526,8 +1382,8 @@ fn collapse_button<'a>( column![ icon, row![ - text(title.to_string()).size(9).color(GROUP_LABEL), - crate::ui::icons::arrow_down(8.0, GROUP_LABEL), + text(title.to_string()).size(9).style(muted_text_style), + crate::ui::icons::themed_secondary_arrow_down(8.0), ] .spacing(3) .align_y(iced::Center), @@ -1536,22 +1392,7 @@ fn collapse_button<'a>( .spacing(2), ) .on_press(Message::ToggleRibbonPanel(title.to_string())) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered => Color { - r: 0.25, - g: 0.25, - b: 0.25, - a: 1.0, - }, - _ => Color::TRANSPARENT, - })), - border: Border { - radius: 2.0.into(), - ..Default::default() - }, - ..Default::default() - }) + .style(button::subtle) .padding([3, 5]) .into(); } @@ -1594,29 +1435,14 @@ fn collapse_button<'a>( let opener = button( row![ - text(title.to_string()).size(9).color(GROUP_LABEL), - crate::ui::icons::arrow_down(8.0, GROUP_LABEL), + text(title.to_string()).size(9).style(muted_text_style), + crate::ui::icons::themed_secondary_arrow_down(8.0), ] .spacing(3) .align_y(iced::Center), ) .on_press(Message::ToggleRibbonPanel(title.to_string())) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered => Color { - r: 0.25, - g: 0.25, - b: 0.25, - a: 1.0, - }, - _ => Color::TRANSPARENT, - })), - border: Border { - radius: 2.0.into(), - ..Default::default() - }, - ..Default::default() - }) + .style(button::subtle) .padding([1, 4]); // The large face fills a fixed slot so a collapsed panel is shorter than a full diff --git a/src/ui/ribbon/widgets.rs b/src/ui/ribbon/widgets.rs index f555e679..4ad059b9 100644 --- a/src/ui/ribbon/widgets.rs +++ b/src/ui/ribbon/widgets.rs @@ -8,7 +8,7 @@ use acadrust::types::{Color as AcadColor, LineWeight}; // Ribbon tooltips anchor to the right of their button so the cursor — which // rests on the button itself — never covers the tip text. (#143) use iced::widget::tooltip::Position as TipPos; -use iced::widget::{button, column, container, row, svg, text, tooltip}; +use iced::widget::{button, column, container, row, text, tooltip}; use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme}; use crate::app::Message; @@ -77,175 +77,6 @@ pub(super) const PROP_COLOR_ID: &str = "PROP_COLOR"; pub(super) const PROP_LINETYPE_ID: &str = "PROP_LINETYPE"; pub(super) const PROP_LW_ID: &str = "PROP_LW"; -// ── Colours ──────────────────────────────────────────────────────────────── - -/// Light chrome grey for the quick-access file-command icons on the top strip. -pub(super) const QA_ICON_COLOR: Color = Color { - r: 0.82, - g: 0.83, - b: 0.85, - a: 1.0, -}; -pub(super) const TOPBAR_BG: Color = Color { - r: 0.17, - g: 0.17, - b: 0.17, - a: 1.0, -}; -pub(super) const RIBBON_BG: Color = Color { - r: 0.22, - g: 0.22, - b: 0.22, - a: 1.0, -}; -pub(super) const BORDER_DARK: Color = Color { - r: 0.12, - g: 0.12, - b: 0.12, - a: 1.0, -}; -pub(super) const ACCENT_BLUE: Color = Color { - r: 0.20, - g: 0.55, - b: 0.90, - a: 1.0, -}; -pub(super) const ACCENT_GOLD: Color = Color { - r: 0.90, - g: 0.65, - b: 0.10, - a: 1.0, -}; -pub(super) const LABEL_COLOR: Color = Color { - r: 0.82, - g: 0.82, - b: 0.82, - a: 1.0, -}; -pub(super) const GROUP_LABEL: Color = Color { - r: 0.50, - g: 0.50, - b: 0.50, - a: 1.0, -}; -pub(super) const TOOL_HOVER: Color = Color { - r: 0.32, - g: 0.32, - b: 0.32, - a: 1.0, -}; -pub(super) const TOOL_ACTIVE: Color = Color { - r: 0.18, - g: 0.42, - b: 0.70, - a: 1.0, -}; -pub(super) const ARROW_COLOR: Color = Color { - r: 0.65, - g: 0.65, - b: 0.65, - a: 1.0, -}; -pub(super) const PANEL_BG: Color = Color { - r: 0.16, - g: 0.16, - b: 0.16, - a: 0.98, -}; -pub(super) const PANEL_BORDER: Color = Color { - r: 0.32, - g: 0.32, - b: 0.32, - a: 1.0, -}; -pub(super) const ROW_HOVER: Color = Color { - r: 0.24, - g: 0.24, - b: 0.24, - a: 1.0, -}; -pub(super) const CHECK_COLOR: Color = Color { - r: 0.20, - g: 0.75, - b: 0.35, - a: 1.0, -}; -pub(super) const ICON_COLOR: Color = Color { - r: 0.25, - g: 0.75, - b: 0.45, - a: 1.0, -}; -pub(super) const LABEL_ON: Color = Color { - r: 0.92, - g: 0.92, - b: 0.92, - a: 1.0, -}; -pub(super) const LABEL_OFF: Color = Color { - r: 0.72, - g: 0.72, - b: 0.72, - a: 1.0, -}; - -// ── Combo / dropdown colors ─────────────────────────────────────────────── - -pub(super) const COMBO_BG: Color = Color { - r: 0.18, - g: 0.18, - b: 0.18, - a: 1.0, -}; -pub(super) const COMBO_HOVER_BG: Color = Color { - r: 0.26, - g: 0.26, - b: 0.26, - a: 1.0, -}; -pub(super) const COMBO_OPEN_BG: Color = Color { - r: 0.14, - g: 0.14, - b: 0.14, - a: 1.0, -}; -pub(super) const COMBO_BORDER: Color = Color { - r: 0.35, - g: 0.35, - b: 0.35, - a: 1.0, -}; -pub(super) const COMBO_ACTIVE_BORDER: Color = Color { - r: 0.45, - g: 0.65, - b: 0.90, - a: 1.0, -}; -pub(super) const COMBO_ARROW: Color = Color { - r: 0.70, - g: 0.70, - b: 0.70, - a: 1.0, -}; -pub(super) const SWATCH_BORDER: Color = Color { - r: 0.0, - g: 0.0, - b: 0.0, - a: 0.5, -}; -pub(super) const TIP_BG: Color = Color { - r: 0.13, - g: 0.13, - b: 0.13, - a: 0.97, -}; -pub(super) const HIST_INACTIVE_BG: Color = Color { - r: 0.20, - g: 0.20, - b: 0.20, - a: 1.0, -}; - // ── Style context (passed from Ribbon to render_large) ──────────────────── pub(super) struct StyleContext { @@ -294,11 +125,8 @@ pub(super) fn flush_small_col<'a>( pub(super) fn make_icon(icon: IconKind, size: f32) -> Element<'static, Message> { match icon { - IconKind::Glyph(s) => text(s).size(size * 0.7).color(Color::WHITE).into(), - IconKind::Svg(bytes) => { - let handle = svg::Handle::from_memory(bytes); - svg(handle).width(size).height(size).into() - } + IconKind::Glyph(s) => text(s).size(size * 0.7).into(), + IconKind::Svg(bytes) => icons::themed(bytes, size), } } @@ -309,22 +137,19 @@ pub(super) fn start_dimmed(state: &ToggleState, event: &ModuleEvent) -> bool { && !matches!(event, ModuleEvent::Command(c) if crate::app::commands::start_allowed(c)) } -/// Label / glyph color for a possibly-dimmed tool. -pub(super) const DIM_TOOL: Color = Color { - r: 0.42, - g: 0.42, - b: 0.45, - a: 1.0, -}; - /// `make_icon`, greyed out when `dim` (SVGs render monochrome via tint). pub(super) fn make_icon_dim(icon: IconKind, size: f32, dim: bool) -> Element<'static, Message> { if !dim { return make_icon(icon, size); } match icon { - IconKind::Glyph(s) => text(s).size(size * 0.7).color(DIM_TOOL).into(), - IconKind::Svg(bytes) => icons::tinted(bytes, size, DIM_TOOL), + IconKind::Glyph(s) => text(s) + .size(size * 0.7) + .style(|theme: &Theme| iced::widget::text::Style { + color: Some(theme.extended_palette().background.base.text.scale_alpha(0.42)), + }) + .into(), + IconKind::Svg(bytes) => icons::themed_disabled(bytes, size), } } @@ -349,15 +174,24 @@ pub(super) fn is_active_tool( // ── Button style ─────────────────────────────────────────────────────────── -pub(super) fn tool_btn_style(is_active: bool, status: button::Status) -> button::Style { +pub(super) fn tool_btn_style( + theme: &Theme, + is_active: bool, + status: button::Status, +) -> button::Style { + let palette = theme.extended_palette(); + let pair = match (is_active, status) { + (true, _) => palette.primary.weak, + (_, button::Status::Hovered) => palette.background.weak, + (_, button::Status::Pressed) => palette.primary.weak, + _ => palette.background.base, + }; button::Style { - background: Some(Background::Color(match (is_active, status) { - (true, _) => TOOL_ACTIVE, - (_, button::Status::Hovered) => TOOL_HOVER, - (_, button::Status::Pressed) => TOOL_ACTIVE, - _ => Color::TRANSPARENT, - })), - text_color: Color::WHITE, + background: is_active + .then_some(Background::Color(pair.color)) + .or_else(|| matches!(status, button::Status::Hovered | button::Status::Pressed) + .then_some(Background::Color(pair.color))), + text_color: pair.text, border: Border { radius: 3.0.into(), color: Color::TRANSPARENT, @@ -368,21 +202,93 @@ pub(super) fn tool_btn_style(is_active: bool, status: button::Status) -> button: } } -// ── Tooltip helpers ──────────────────────────────────────────────────────── - -pub(super) fn make_tip(tip: String) -> Element<'static, Message> { - text(tip).size(11).color(Color::WHITE).into() +pub(super) fn combo_btn_style( + theme: &Theme, + is_open: bool, + status: button::Status, + radius: f32, +) -> button::Style { + let palette = theme.extended_palette(); + let pair = if is_open { + palette.primary.weak + } else if matches!(status, button::Status::Hovered | button::Status::Pressed) { + palette.background.weak + } else { + palette.background.weakest + }; + button::Style { + background: Some(Background::Color(pair.color)), + text_color: pair.text, + border: Border { + radius: radius.into(), + width: 1.0, + color: if is_open { + palette.primary.base.color + } else { + palette.background.neutral.color + }, + }, + ..Default::default() + } } -pub(super) fn tip_style(_theme: &Theme) -> container::Style { +pub(super) fn popup_row_style(theme: &Theme, status: button::Status) -> button::Style { + let palette = theme.extended_palette(); + let pair = if matches!(status, button::Status::Hovered | button::Status::Pressed) { + palette.background.weak + } else { + palette.background.base + }; + button::Style { + background: Some(Background::Color(pair.color)), + text_color: pair.text, + ..Default::default() + } +} + +pub(super) fn popup_panel_style(theme: &Theme) -> container::Style { + let palette = theme.extended_palette(); container::Style { - background: Some(Background::Color(TIP_BG)), + background: Some(Background::Color(palette.background.base.color)), border: Border { - color: COMBO_BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 3.0.into(), }, - text_color: Some(Color::WHITE), + ..Default::default() + } +} + +pub(super) fn muted_text_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().background.base.text.scale_alpha(0.72)), + } +} + +pub(super) fn tool_label_style(theme: &Theme, dim: bool) -> iced::widget::text::Style { + iced::widget::text::Style { + color: dim.then_some( + theme.extended_palette().background.base.text.scale_alpha(0.42), + ), + } +} + +// ── Tooltip helpers ──────────────────────────────────────────────────────── + +pub(super) fn make_tip(tip: String) -> Element<'static, Message> { + text(tip).size(11).into() +} + +pub(super) fn tip_style(theme: &Theme) -> container::Style { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.strong.color)), + border: Border { + color: palette.background.neutral.color, + width: 1.0, + radius: 3.0.into(), + }, + text_color: Some(palette.background.strong.text), ..Default::default() } } @@ -408,7 +314,7 @@ pub(super) fn render_small<'a>( let tip_text = format!("{}\nCommand: {}", t.label, t.id); let btn = button(make_icon_dim(t.icon, SMALL_ICON, dim)) .on_press(Message::RibbonToolClick { tool_id, event }) - .style(move |_: &Theme, status| tool_btn_style(active, status)) + .style(move |theme: &Theme, status| tool_btn_style(theme, active, status)) .width(Length::Fixed(SMALL_W)) .height(ROW_H) .padding([4, 4]); @@ -474,31 +380,22 @@ pub(super) fn render_small<'a>( tool_id: last.to_string(), event: ModuleEvent::Command(last.to_string()), }) - .style(move |_: &Theme, status| tool_btn_style(active, status)) + .style(move |theme: &Theme, status| tool_btn_style(theme, active, status)) .width(Length::Fixed(SMALL_W)) .height(ROW_H) .padding([4, 4]); let arr_tip = format!("{} options", cur_label); let arr_btn = button( - container(icons::arrow_down(8.0, ARROW_COLOR)) + container(icons::themed_arrow_down(8.0)) .width(Fill) .height(Fill) .align_x(iced::Center) .align_y(iced::Center), ) .on_press(Message::ToggleRibbonDropdown(id.to_string())) - .style(move |_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => TOOL_HOVER, - _ if dd_open => TOOL_ACTIVE, - _ => Color::TRANSPARENT, - })), - border: Border { - radius: 2.0.into(), - ..Default::default() - }, - ..Default::default() + .style(move |theme: &Theme, status| { + tool_btn_style(theme, dd_open, status) }) .width(Length::Fixed(ARROW_W)) .height(ROW_H) @@ -569,7 +466,9 @@ pub(super) fn render_large_dropdown<'a>( let top_btn = button( column![ make_icon_dim(cur_icon, LARGE_ICON, dim), - text(label.to_string()).size(10).color(if dim { DIM_TOOL } else { LABEL_COLOR }), + text(label.to_string()) + .size(10) + .style(move |theme: &Theme| tool_label_style(theme, dim)), ] .align_x(iced::Center) .spacing(3), @@ -578,7 +477,7 @@ pub(super) fn render_large_dropdown<'a>( tool_id: last.to_string(), event: ModuleEvent::Command(last.to_string()), }) - .style(move |_: &Theme, status| tool_btn_style(active, status)) + .style(move |theme: &Theme, status| tool_btn_style(theme, active, status)) .width(Length::Fixed(LARGE_W)) .height(Fill) .padding(Padding { @@ -589,24 +488,15 @@ pub(super) fn render_large_dropdown<'a>( }); let arr_btn = button( - container(icons::arrow_down(9.0, ARROW_COLOR)) + container(icons::themed_arrow_down(9.0)) .width(Fill) .height(Fill) .align_x(iced::Center) .align_y(iced::Center), ) .on_press(Message::ToggleRibbonDropdown(id.to_string())) - .style(move |_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => TOOL_HOVER, - _ if dd_open => TOOL_ACTIVE, - _ => Color::TRANSPARENT, - })), - border: Border { - radius: 3.0.into(), - ..Default::default() - }, - ..Default::default() + .style(move |theme: &Theme, status| { + tool_btn_style(theme, dd_open, status) }) .width(Length::Fixed(LARGE_W)) .height(LARGE_ARR) @@ -659,13 +549,15 @@ pub(super) fn render_large<'a>( let btn = button( column![ make_icon_dim(t.icon, LARGE_ICON, dim), - text(t.label).size(10).color(if dim { DIM_TOOL } else { LABEL_COLOR }), + text(t.label) + .size(10) + .style(move |theme: &Theme| tool_label_style(theme, dim)), ] .align_x(iced::Center) .spacing(3), ) .on_press(Message::RibbonToolClick { tool_id, event }) - .style(move |_: &Theme, status| tool_btn_style(active, status)) + .style(move |theme: &Theme, status| tool_btn_style(theme, active, status)) .width(Length::Fixed(LARGE_W)) .height(Fill) .padding(Padding { @@ -737,10 +629,10 @@ pub(super) fn render_large<'a>( let freeze_icon = icons::raw(icons::layer_freeze(lf), 14.0); let lock_icon = icons::raw(icons::layer_lock(ll), 14.0); let swatch = container(text("")) - .style(move |_: &Theme| container::Style { + .style(move |theme: &Theme| container::Style { background: Some(Background::Color(lc)), border: Border { - color: SWATCH_BORDER, + color: theme.extended_palette().background.strong.color, width: 1.0, radius: 1.0.into(), }, @@ -758,27 +650,17 @@ pub(super) fn render_large<'a>( freeze_icon, lock_icon, swatch, - container(text(active_layer).size(11).color(Color::WHITE)) + container(text(active_layer).size(11)) .width(name_w) .clip(true), - icons::arrow_down(9.0, COMBO_ARROW), + icons::themed_arrow_down(9.0), ] .spacing(4) .align_y(iced::Center), ) .on_press(Message::ToggleRibbonDropdown(LAYER_COMBO_ID.to_string())) - .style(move |_: &Theme, status| button::Style { - background: Some(Background::Color(match (is_open, status) { - (true, _) => COMBO_OPEN_BG, - (_, button::Status::Hovered) => COMBO_HOVER_BG, - _ => COMBO_BG, - })), - border: Border { - radius: 3.0.into(), - width: 1.0, - color: COMBO_BORDER, - }, - ..Default::default() + .style(move |theme: &Theme, status| { + combo_btn_style(theme, is_open, status, 3.0) }) .padding([3, 8]) .width(Fill); @@ -793,20 +675,16 @@ pub(super) fn render_large<'a>( let event = t.event.clone(); let icon_el: Element = if dim { make_icon_dim(t.icon, 16.0, true) - } else { match t.icon { - IconKind::Glyph(g) => text(g).size(13).color(Color::WHITE).into(), - IconKind::Svg(bytes) => { - iced::widget::svg(iced::widget::svg::Handle::from_memory(bytes)) - .width(16) - .height(16) - .into() - } - } }; + } else { + make_icon(t.icon, 16.0) + }; let msg = module_event_to_message(event); tooltip( button(icon_el) .on_press(msg) - .style(move |_: &Theme, status| tool_btn_style(is_active, status)) + .style(move |theme: &Theme, status| { + tool_btn_style(theme, is_active, status) + }) .padding([2, 5]), make_tip(tip.to_string()), TipPos::Right, @@ -864,7 +742,9 @@ pub(super) fn render_large<'a>( let mp_btn = button( column![ make_icon_dim(match_prop.icon, LARGE_ICON, mp_dim), - text(match_prop.label).size(10).color(if mp_dim { DIM_TOOL } else { LABEL_COLOR }), + text(match_prop.label) + .size(10) + .style(move |theme: &Theme| tool_label_style(theme, mp_dim)), ] .align_x(iced::Center) .spacing(3), @@ -873,7 +753,7 @@ pub(super) fn render_large<'a>( tool_id: mp_id, event: mp_event, }) - .style(move |_: &Theme, status| tool_btn_style(mp_active, status)) + .style(move |theme: &Theme, status| tool_btn_style(theme, mp_active, status)) .width(Length::Fixed(LARGE_W)) .height(Fill) .padding(Padding { @@ -895,10 +775,10 @@ pub(super) fn render_large<'a>( let is_open = open_dd.as_deref() == Some(dd_id); let swatch_el: Element<'a, Message> = if let Some(c) = swatch { container(text("")) - .style(move |_: &Theme| container::Style { + .style(move |theme: &Theme| container::Style { background: Some(Background::Color(c)), border: Border { - color: SWATCH_BORDER, + color: theme.extended_palette().background.strong.color, width: 1.0, radius: 1.0.into(), }, @@ -913,40 +793,21 @@ pub(super) fn render_large<'a>( button( row![ swatch_el, - container(text(label).size(10).color(Color::WHITE)) + container(text(label).size(10)) .width(Fill) .clip(true), - icons::arrow_toggle( - is_open, - 8.0, - Color { - r: 0.6, - g: 0.6, - b: 0.6, - a: 1.0, - }, - ), + if is_open { + icons::themed_arrow_up(8.0) + } else { + icons::themed_arrow_down(8.0) + }, ] .spacing(4) .align_y(iced::Center), ) .on_press(Message::ToggleRibbonDropdown(dd_id.to_string())) - .style(move |_: &Theme, status| button::Style { - background: Some(Background::Color(match (is_open, status) { - (true, _) => COMBO_OPEN_BG, - (_, button::Status::Hovered) => COMBO_HOVER_BG, - _ => COMBO_BG, - })), - border: Border { - radius: 2.0.into(), - width: 1.0, - color: if is_open { - COMBO_ACTIVE_BORDER - } else { - COMBO_BORDER - }, - }, - ..Default::default() + .style(move |theme: &Theme, status| { + combo_btn_style(theme, is_open, status, 2.0) }) .padding([3, 8]) .width(Length::Fixed(PROP_W)) @@ -999,31 +860,21 @@ pub(super) fn render_large<'a>( // ── combo button ── let combo_btn = button( row![ - container(text(active.clone()).size(11).color(Color::WHITE)) + container(text(active.clone()).size(11)) .width(Fill) .clip(true), - icons::arrow_toggle(is_open, 9.0, COMBO_ARROW), + if is_open { + icons::themed_arrow_up(9.0) + } else { + icons::themed_arrow_down(9.0) + }, ] .spacing(4) .align_y(iced::Center), ) .on_press(Message::ToggleRibbonDropdown(combo_id.to_string())) - .style(move |_: &Theme, status| button::Style { - background: Some(Background::Color(match (is_open, status) { - (true, _) => COMBO_OPEN_BG, - (_, button::Status::Hovered) => COMBO_HOVER_BG, - _ => COMBO_BG, - })), - border: Border { - radius: 3.0.into(), - width: 1.0, - color: if is_open { - COMBO_ACTIVE_BORDER - } else { - COMBO_BORDER - }, - }, - ..Default::default() + .style(move |theme: &Theme, status| { + combo_btn_style(theme, is_open, status, 3.0) }) .padding([3, 8]) .width(Fill); @@ -1045,20 +896,16 @@ pub(super) fn render_large<'a>( let event = t.event.clone(); let icon_el: Element = if dim { make_icon_dim(t.icon, 16.0, true) - } else { match t.icon { - IconKind::Glyph(g) => text(g).size(13).color(Color::WHITE).into(), - IconKind::Svg(bytes) => { - iced::widget::svg(iced::widget::svg::Handle::from_memory(bytes)) - .width(16) - .height(16) - .into() - } - } }; + } else { + make_icon(t.icon, 16.0) + }; let msg = module_event_to_message(event); tooltip( button(icon_el) .on_press(msg) - .style(move |_: &Theme, status| tool_btn_style(is_active, status)) + .style(move |theme: &Theme, status| { + tool_btn_style(theme, is_active, status) + }) .padding([2, 5]), make_tip(tip.to_string()), TipPos::Right, @@ -1128,7 +975,11 @@ pub(super) fn quick_access_btn<'a>( // they read on the dark top strip (raw black is invisible there). // On the Start tab, commands the start gate refuses render dimmed. let dim = is_start && !crate::app::commands::start_allowed(cmd); - let icon = icons::tinted(icon_bytes, 16.0, if dim { DIM_TOOL } else { QA_ICON_COLOR }); + let icon = if dim { + icons::themed_disabled(icon_bytes, 16.0) + } else { + icons::themed(icon_bytes, 16.0) + }; let btn = button( container(icon) .width(Fill) @@ -1137,22 +988,7 @@ pub(super) fn quick_access_btn<'a>( .align_y(iced::Center), ) .on_press(Message::Command(cmd.to_string())) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => Color { - r: 0.30, - g: 0.30, - b: 0.30, - a: 1.0, - }, - _ => Color::TRANSPARENT, - })), - border: Border { - radius: 2.0.into(), - ..Default::default() - }, - ..Default::default() - }) + .style(button::subtle) .width(Length::Fixed(TOP_HIST_W)) .height(24) .padding([2, 0]); @@ -1171,13 +1007,12 @@ pub(super) fn render_history_control<'a>( ) -> Element<'a, Message> { let dd_open = open_dropdown.as_deref() == Some(dropdown_id); let active = count > 0; - let icon_color = if active { Color::WHITE } else { LABEL_OFF }; let main_btn = { let glyph = if dropdown_id == UNDO_HISTORY_ID { - icons::undo(15.0, icon_color) + icons::themed_undo(15.0, active) } else { - icons::redo(15.0, icon_color) + icons::themed_redo(15.0, active) }; let btn = button( container(glyph) @@ -1186,7 +1021,9 @@ pub(super) fn render_history_control<'a>( .align_x(iced::Center) .align_y(iced::Center), ) - .style(move |_: &Theme, status| top_hist_btn_style(active, dd_open, status)) + .style(move |theme: &Theme, status| { + top_hist_btn_style(theme, active, dd_open, status) + }) .width(Length::Fixed(TOP_HIST_W)) .height(24) .padding([2, 0]); @@ -1211,16 +1048,19 @@ pub(super) fn render_history_control<'a>( let arrow_btn = { let btn = button( - container(icons::arrow_down( - 8.0, - if active { ARROW_COLOR } else { LABEL_OFF }, - )) + container(if active { + icons::themed_arrow_down(8.0) + } else { + icons::themed_disabled_arrow_down(8.0) + }) .width(Fill) .height(Fill) .align_x(iced::Center) .align_y(iced::Center), ) - .style(move |_: &Theme, status| top_hist_btn_style(active, dd_open, status)) + .style(move |theme: &Theme, status| { + top_hist_btn_style(theme, active, dd_open, status) + }) .width(Length::Fixed(TOP_ARR_W)) .height(24) .padding(0); @@ -1243,19 +1083,26 @@ pub(super) fn render_history_control<'a>( } pub(super) fn top_hist_btn_style( + theme: &Theme, active: bool, open: bool, status: button::Status, ) -> button::Style { + let palette = theme.extended_palette(); + let pair = match (active, open, status) { + (false, _, _) => palette.background.weakest, + (_, true, _) => palette.primary.weak, + (_, _, button::Status::Hovered) => palette.background.weak, + (_, _, button::Status::Pressed) => palette.primary.weak, + _ => palette.background.base, + }; button::Style { - background: Some(Background::Color(match (active, open, status) { - (false, _, _) => HIST_INACTIVE_BG, - (_, true, _) => TOOL_ACTIVE, - (_, _, button::Status::Hovered) => TOOL_HOVER, - (_, _, button::Status::Pressed) => TOOL_ACTIVE, - _ => Color::TRANSPARENT, - })), - text_color: Color::WHITE, + background: (!active || open || matches!( + status, + button::Status::Hovered | button::Status::Pressed + )) + .then_some(Background::Color(pair.color)), + text_color: pair.text, border: Border { radius: 3.0.into(), color: Color::TRANSPARENT, diff --git a/src/ui/side_toolbar.rs b/src/ui/side_toolbar.rs index 9a3349c7..db01ebb1 100644 --- a/src/ui/side_toolbar.rs +++ b/src/ui/side_toolbar.rs @@ -7,15 +7,12 @@ //! module's tools can drive it. First used for paper-space viewport / plot //! actions; reusable for any future context action set. -use iced::widget::{button, column, container, svg, text, tooltip}; -use iced::{Background, Border, Color, Element, Length, Theme}; +use iced::widget::{button, column, container, text, tooltip}; +use iced::{Background, Border, Element, Length, Theme}; use crate::app::Message; use crate::modules::{IconKind, ToolDef}; -const PANEL_BG: Color = Color { r: 0.13, g: 0.13, b: 0.13, a: 0.96 }; -const PANEL_BORDER: Color = Color { r: 0.32, g: 0.32, b: 0.32, a: 1.0 }; -const BTN_HOVER: Color = Color { r: 0.24, g: 0.24, b: 0.24, a: 1.0 }; const ICON_SIZE: f32 = 22.0; const BTN_SIZE: f32 = 38.0; /// Gap between the toolbar and the right edge of the canvas. @@ -23,25 +20,26 @@ const EDGE_MARGIN: f32 = 8.0; fn icon_el(icon: IconKind) -> Element<'static, Message> { match icon { - IconKind::Glyph(s) => text(s).size(ICON_SIZE * 0.85).color(Color::WHITE).into(), - IconKind::Svg(bytes) => svg(svg::Handle::from_memory(bytes)) - .width(Length::Fixed(ICON_SIZE)) - .height(Length::Fixed(ICON_SIZE)) - .into(), + IconKind::Glyph(s) => text(s).size(ICON_SIZE * 0.85).into(), + IconKind::Svg(bytes) => crate::ui::icons::themed(bytes, ICON_SIZE), } } fn tip_panel(label: &'static str) -> Element<'static, Message> { - container(text(label).size(11).color(Color::WHITE)) + container(text(label).size(11)) .padding([2, 6]) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.strong.color)), border: Border { - color: PANEL_BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 3.0.into(), }, + text_color: Some(palette.background.strong.text), ..Default::default() + } }) .into() } @@ -63,17 +61,22 @@ pub fn view(tools: &[ToolDef]) -> Option> { }) .width(Length::Fixed(BTN_SIZE)) .height(Length::Fixed(BTN_SIZE)) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => BTN_HOVER, - _ => Color::TRANSPARENT, - })), + .style(|theme: &Theme, status| { + let palette = theme.extended_palette(); + let hovered = matches!( + status, + button::Status::Hovered | button::Status::Pressed + ); + button::Style { + background: hovered + .then_some(Background::Color(palette.background.strong.color)), border: Border { radius: 3.0.into(), ..Default::default() }, - text_color: Color::WHITE, + text_color: palette.background.base.text, ..Default::default() + } }); // Label tooltip on the left so it never runs off the right edge. col = col.push( @@ -81,14 +84,17 @@ pub fn view(tools: &[ToolDef]) -> Option> { ); } - let panel = container(col).padding(4).style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), + let panel = container(col).padding(4).style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.weak.color)), border: Border { - color: PANEL_BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 5.0.into(), }, ..Default::default() + } }); // Fill the canvas, pin the panel to the right edge and centre it diff --git a/src/ui/statusbar/mod.rs b/src/ui/statusbar/mod.rs index cee526a4..6ab82a43 100644 --- a/src/ui/statusbar/mod.rs +++ b/src/ui/statusbar/mod.rs @@ -120,22 +120,13 @@ impl StatusBar { // Leftmost hamburger: opens a dropdown listing Model + every layout, so // a layout can be picked directly even when the tab strip is scrolled. - let menu_button = button(crate::ui::icons::tinted( - crate::ui::icons::MENU, - 16.0, - if is_start { DISABLED_COLOR } else { ICON_COLOR }, - )) - .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() - }) + let menu_icon = if is_start { + crate::ui::icons::themed_disabled(crate::ui::icons::MENU, 16.0) + } else { + crate::ui::icons::themed_secondary(crate::ui::icons::MENU, 16.0) + }; + let menu_button = button(menu_icon) + .style(button::subtle) .padding([4, 8]); let menu_btn = if is_start { tip( @@ -156,15 +147,8 @@ impl StatusBar { ) }; - let add_button = button(text("+").size(12).color(if is_start { - DISABLED_COLOR - } else { - ICON_COLOR - })) - .style(|_: &Theme, _| button::Style { - background: Some(Background::Color(Color::TRANSPARENT)), - ..Default::default() - }) + let add_button = button(text("+").size(12)) + .style(button::subtle) .padding([4, 8]); let add_btn = if is_start { tip( @@ -453,14 +437,17 @@ impl StatusBar { .justify_end(true); container(wrap) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BAR_BG)), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.base.color)), border: Border { - color: BORDER_COLOR, + color: palette.background.neutral.color, width: 1.0, radius: 0.0.into(), }, ..Default::default() + } }) .width(Length::Fill) // One row matches the drawing (document) tab bar height so the three @@ -501,19 +488,9 @@ fn format_coords(cursor: glam::DVec3, last: Option, mode: i16, pick // ── Customization handle ────────────────────────────────────────────────── fn customize_btn() -> Element<'static, Message> { - button(crate::ui::icons::tinted(crate::ui::icons::MENU, 16.0, ICON_COLOR)) + button(crate::ui::icons::themed_secondary(crate::ui::icons::MENU, 16.0)) .on_press(Message::StatusMenuTooltipHidden(true)) - .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() - }) + .style(button::subtle) .padding([4, 8]) .into() } @@ -521,7 +498,7 @@ fn customize_btn() -> Element<'static, Message> { // ── 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()) + tip_node(content, text(label).size(11).into()) } /// A menu root shows its tooltip until clicked. Moving from the root into the @@ -549,26 +526,7 @@ fn tip_node<'a>(content: Element<'a, Message>, body: Element<'a, Message>) -> El 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() - }) + .style(container::bordered_box) .padding([4, 8]), TipPos::Top, ) @@ -581,24 +539,26 @@ fn tip_node<'a>(content: Element<'a, Message>, body: Element<'a, Message>) -> El /// text labels were too small to read). The name lives in the tooltip each /// call site already wraps it with. fn toggle_pill(icon: &'static [u8], active: bool, msg: Message) -> Element<'static, Message> { - let color = if active { OSNAP_ON_TEXT } else { OSNAP_OFF_TEXT }; - button(crate::ui::icons::tinted(icon, 17.0, color)) + let icon = if active { + crate::ui::icons::themed_primary(icon, 17.0) + } else { + crate::ui::icons::themed_secondary(icon, 17.0) + }; + button(icon) .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: color, - shadow: iced::Shadow::default(), - snap: false, + .style(move |theme: &Theme, status| { + let mut style = button::subtle(theme, status); + if active { + let palette = theme.extended_palette(); + style.background = Some(Background::Color(match status { + button::Status::Hovered => palette.primary.base.color, + _ => palette.primary.weak.color, + })); + style.text_color = palette.primary.weak.text; + style.border.color = palette.primary.base.color; + style.border.width = 1.0; + } + style }) .padding([4, 7]) .into() @@ -611,16 +571,6 @@ fn toggle_pill(icon: &'static [u8], active: bool, msg: Message) -> Element<'stat // and a dropdown caret, so both halves read as a single integrated control and // the border / background / caret / sizing live in exactly ONE place. -/// Pill text/icon tint for the lit vs. dim state — shared so `main` (built by -/// the caller) and the caret always match. -fn pill_text_color(active: bool) -> Color { - if active { - OSNAP_ON_TEXT - } else { - OSNAP_OFF_TEXT - } -} - /// Wrap a caller-built, already-click-wired `main` element together with its /// menu-bearing dropdown caret. fn split_pill<'a>( @@ -628,22 +578,26 @@ fn split_pill<'a>( caret: Element<'a, Message>, active: bool, ) -> Element<'a, Message> { - let bg = if active { SNAP_ON_BG } else { SNAP_OFF_BG }; - let border_color = if active { - SNAP_BORDER_ON - } else { - BORDER_COLOR - }; - container(row![main, caret].spacing(3).align_y(iced::Center)) - .style(move |_: &Theme| container::Style { - background: Some(Background::Color(bg)), + .style(move |theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(if active { + palette.primary.weak.color + } else { + palette.background.weakest.color + })), border: Border { - color: border_color, + color: if active { + palette.primary.base.color + } else { + palette.background.neutral.color + }, width: 1.0, radius: 2.0.into(), }, ..Default::default() + } }) .padding([4, 6]) .into() @@ -664,7 +618,6 @@ fn polar_pill<'a>( let tooltip_text = format!( "Polar Tracking ({angle})\nF10 — left-click on/off\nRight-click cycles · ▾ picks angle", ); - let color = pill_text_color(active); // Right-click quick-cycles through the same increments the picker lists. const CYCLE: &[f32] = &[90.0, 45.0, 30.0, 22.5, 18.0, 15.0, 10.0, 5.0, 1.0]; @@ -673,10 +626,24 @@ fn polar_pill<'a>( None => 45.0, }; + let polar_icon = if active { + crate::ui::icons::themed_primary(crate::ui::icons::ST_POLAR, 17.0) + } else { + crate::ui::icons::themed_secondary(crate::ui::icons::ST_POLAR, 17.0) + }; let main = mouse_area( row![ - crate::ui::icons::tinted(crate::ui::icons::ST_POLAR, 17.0, color), - text(angle).size(11).color(color), + polar_icon, + text(angle).size(11).style(move |theme: &Theme| { + let palette = theme.extended_palette(); + text::Style { + color: Some(if active { + palette.primary.base.color + } else { + palette.background.base.text.scale_alpha(0.72) + }), + } + }), ] .spacing(2) .align_y(iced::Center), @@ -685,26 +652,8 @@ fn polar_pill<'a>( .on_right_press(Message::SetPolarAngle(next_angle)); let main = tooltip( main, - 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() - }) + container(text(tooltip_text).size(11)) + .style(container::bordered_box) .padding([4, 8]), TipPos::Top, ); @@ -712,7 +661,12 @@ fn polar_pill<'a>( let caret = status_menu::menu_bar( menu_tip( mouse_area( - container(crate::ui::icons::arrow_down(9.0, color)).padding([4, 7]), + container(if active { + crate::ui::icons::themed_primary_arrow_down(9.0) + } else { + crate::ui::icons::themed_secondary_arrow_down(9.0) + }) + .padding([4, 7]), ) .on_press(Message::StatusMenuTooltipHidden(true)) .into(), @@ -737,12 +691,13 @@ fn osnap_btn<'a>( entries: Vec>, ) -> Element<'a, Message> { let on = active || snap_enabled; + let snap_icon = if on { + crate::ui::icons::themed_primary(crate::ui::icons::ST_OSNAP, 17.0) + } else { + crate::ui::icons::themed_secondary(crate::ui::icons::ST_OSNAP, 17.0) + }; let main = tip( - mouse_area(crate::ui::icons::tinted( - crate::ui::icons::ST_OSNAP, - 17.0, - pill_text_color(on), - )) + mouse_area(snap_icon) .on_press(Message::ToggleSnapEnabled) .into(), "Object Snap: toggle on/off\nF3", @@ -751,7 +706,12 @@ fn osnap_btn<'a>( let caret = status_menu::menu_bar( menu_tip( mouse_area( - container(crate::ui::icons::arrow_down(9.0, pill_text_color(on))).padding([4, 7]), + container(if on { + crate::ui::icons::themed_primary_arrow_down(9.0) + } else { + crate::ui::icons::themed_secondary_arrow_down(9.0) + }) + .padding([4, 7]), ) .on_press(Message::StatusMenuTooltipHidden(true)) .into(), @@ -768,44 +728,10 @@ fn osnap_btn<'a>( // ── Helpers ─────────────────────────────────────────────────────────────── fn layout_tab_context_menu(name: String) -> Element<'static, Message> { - const MENU_BG: Color = Color { - r: 0.17, - g: 0.17, - b: 0.17, - a: 1.0, - }; - const MENU_BORDER: Color = Color { - r: 0.35, - g: 0.35, - b: 0.35, - a: 1.0, - }; - const ITEM_HOVER: Color = Color { - r: 0.25, - g: 0.45, - b: 0.70, - a: 1.0, - }; - const TEXT_COLOR: Color = Color { - r: 0.88, - g: 0.88, - b: 0.88, - a: 1.0, - }; - let item = |label: &'static str, msg: Message| { - button(text(label).size(12).color(TEXT_COLOR)) + button(text(label).size(12)) .on_press(msg) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => ITEM_HOVER, - _ => Color::TRANSPARENT, - })), - text_color: TEXT_COLOR, - border: Border::default(), - shadow: iced::Shadow::default(), - snap: false, - }) + .style(button::subtle) .padding([4, 12]) .width(Length::Fill) }; @@ -818,15 +744,7 @@ fn layout_tab_context_menu(name: String) -> Element<'static, Message> { .spacing(0) .width(160), ) - .style(move |_: &Theme| container::Style { - background: Some(Background::Color(MENU_BG)), - border: Border { - color: MENU_BORDER, - width: 1.0, - radius: 4.0.into(), - }, - ..Default::default() - }) + .style(container::bordered_box) .padding([4, 0]) .into() } @@ -843,46 +761,34 @@ fn space_tab<'a>( enabled: bool, reorderable_layouts: Arc<[String]>, ) -> Element<'a, Message> { - let bg = move |is_active: bool, hovered: bool| { - if is_active { - TAB_ACTIVE - } else if hovered { - TAB_HOVER + let tab_style = move |theme: &Theme| { + let palette = theme.extended_palette(); + let text_color = if !enabled { + palette.background.base.text.scale_alpha(0.42) + } else if is_active { + palette.primary.weak.text } 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 !enabled { - DISABLED_COLOR - } else if is_active { - Color::WHITE - } else { - Color { - r: 0.65, - g: 0.65, - b: 0.65, - a: 1.0, + palette.background.base.text.scale_alpha(0.72) + }; + container::Style { + background: is_active.then_some(Background::Color(palette.primary.weak.color)), + border: Border { + color: if is_active { + palette.primary.base.color + } else { + Color::TRANSPARENT + }, + width: if is_active { 1.0 } else { 0.0 }, + radius: 2.0.into(), + }, + text_color: Some(text_color), + ..Default::default() } }; if !enabled { - let display = container(text(label.clone()).size(12).color(text_color)) - .style(move |_: &Theme| container::Style { - background: Some(Background::Color(bg(is_active, false))), - border, - ..Default::default() - }) + let display = container(text(label.clone()).size(12)) + .style(tab_style) .padding([4, 10]); crate::ui::wrap_bar::PosReport::owned( format!("SB_LAYOUT_TAB:{label}"), @@ -899,49 +805,15 @@ fn space_tab<'a>( .on_input(Message::LayoutRenameEdit) .on_submit(Message::LayoutRenameCommit) .size(12) - .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([3, 6]) .width(Length::Fixed(90.0)); - let cancel_btn = button(crate::ui::icons::tinted( + let cancel_btn = button(crate::ui::icons::themed_secondary( 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() - }) + .style(button::subtle) .padding([4, 4]); row![input, cancel_btn] @@ -951,12 +823,8 @@ fn space_tab<'a>( } else { // Normal clickable tab — left click switches. Paper-layout tabs are // wrapped in `ContextMenu`, which owns right-click handling. - let display = container(text(label.clone()).size(12).color(text_color)) - .style(move |_: &Theme| container::Style { - background: Some(Background::Color(bg(is_active, false))), - border, - ..Default::default() - }) + let display = container(text(label.clone()).size(12)) + .style(tab_style) .padding([4, 10]); let switch_msg = Message::LayoutSwitch(label.clone()); @@ -1006,34 +874,21 @@ fn space_mode_btn(current_layout: &str, in_mspace: bool) -> Element<'static, Mes ("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(12).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, + let mut btn = button(text(label).size(12)) + .style(move |theme: &Theme, status| { + let mut style = button::subtle(theme, status); + if active { + let palette = theme.extended_palette(); + style.background = Some(Background::Color(match status { + button::Status::Hovered if clickable => palette.primary.base.color, + _ => palette.primary.weak.color, + })); + style.text_color = palette.primary.weak.text; + style.border.color = palette.primary.base.color; + style.border.width = 1.0; + } + style }) .padding([4, 7]); @@ -1045,119 +900,12 @@ fn space_mode_btn(current_layout: &str, in_mspace: bool) -> Element<'static, Mes } fn status_pill(label: impl Into) -> Element<'static, Message> { - container(text(label.into()).size(12).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() - }) + container(text(label.into()).size(12)) + .style(container::bordered_box) .padding([4, 8]) .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 DISABLED_COLOR: Color = Color { - r: 0.35, - g: 0.35, - b: 0.35, - 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 ──────────────────────────────────────────────────── /// Shared visual root for labelled status-bar menus (units, scale, …). @@ -1167,29 +915,9 @@ fn popup_pill(label: &str) -> Element<'static, Message> { fn action_pill(label: &str, msg: Message) -> Element<'static, Message> { let label = label.to_string(); - button( - text(label) - .size(12) - .color(OSNAP_OFF_TEXT), - ) + button(text(label).size(12)) .on_press(msg) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered => SNAP_ON_HOVER, - _ => SNAP_OFF_BG, - })), - border: Border { - color: match status { - button::Status::Hovered => SNAP_BORDER_ON, - _ => BORDER_COLOR, - }, - width: 1.0, - radius: 2.0.into(), - }, - text_color: OSNAP_OFF_TEXT, - shadow: iced::Shadow::default(), - snap: false, - }) + .style(button::subtle) .padding([4, 7]) .into() } diff --git a/src/ui/statusbar/status_menu.rs b/src/ui/statusbar/status_menu.rs index 64dafe2f..48053ba6 100644 --- a/src/ui/statusbar/status_menu.rs +++ b/src/ui/statusbar/status_menu.rs @@ -51,57 +51,30 @@ pub fn menu_bar<'a>( .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, + .style(|theme: &Theme, _| { + let palette = theme.extended_palette(); + iced_aw::style::menu_bar::Style { + bar_background: Background::Color(Color::TRANSPARENT), + bar_border: Border::default(), + bar_shadow: Shadow::default(), + menu_background: Background::Color(palette.background.weakest.color), + menu_border: Border { + color: palette.background.neutral.color, + width: 1.0, + radius: 3.0.into(), }, - 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(), - }, + menu_shadow: Shadow { + color: palette.background.strongest.color.scale_alpha(0.35), + offset: iced::Vector::new(0.0, -2.0), + blur_radius: 6.0, + }, + path: Background::Color(palette.primary.weak.color), + path_border: Border { + color: palette.primary.base.color, + width: 1.0, + radius: 2.0.into(), + }, + } }) .into() } - -const MENU_BG: Color = Color { - r: 0.15, - g: 0.15, - b: 0.15, - a: 1.0, -}; -const MENU_BORDER: Color = Color { - r: 0.32, - g: 0.32, - b: 0.32, - a: 1.0, -}; -const ACTIVE_BG: Color = Color { - r: 0.10, - g: 0.20, - b: 0.32, - a: 1.0, -}; -const ACTIVE_BORDER: Color = Color { - r: 0.20, - g: 0.50, - b: 0.85, - a: 1.0, -}; diff --git a/src/ui/statusbar/statusbar_menu.rs b/src/ui/statusbar/statusbar_menu.rs index c20e8b17..20777b08 100644 --- a/src/ui/statusbar/statusbar_menu.rs +++ b/src/ui/statusbar/statusbar_menu.rs @@ -1,7 +1,7 @@ //! Status-bar customization and layout-list menu entries. use iced::widget::{button, row, text}; -use iced::{Background, Color, Element, Fill, Theme}; +use iced::{Background, Element, Fill, Theme}; use crate::app::Message; use crate::ui::statusbar::statusbar_config::{StatusBarConfig, StatusPill}; @@ -31,23 +31,17 @@ pub fn layout_entries<'a>( } fn layout_row<'a>(name: String, is_current: bool) -> Element<'a, Message> { - let lbl = text(name.clone()) - .size(11) - .color(if is_current { LABEL_ON } else { LABEL_OFF }); + let lbl = text(name.clone()).size(11); button(row![lbl].align_y(iced::Center)) .on_press(Message::LayoutSwitch(name)) - .style(move |_: &Theme, status| button::Style { - background: Some(Background::Color(match (is_current, status) { - (_, button::Status::Hovered) => ROW_HOVER, - (true, _) => Color { - r: 0.18, - g: 0.26, - b: 0.36, - a: 1.0, - }, - _ => Color::TRANSPARENT, - })), - ..Default::default() + .style(move |theme: &Theme, status| { + let mut style = button::subtle(theme, status); + if is_current && status == button::Status::Active { + let palette = theme.extended_palette(); + style.background = Some(Background::Color(palette.primary.weak.color)); + style.text_color = palette.primary.weak.text; + } + style }) .width(Fill) .padding([4, 12]) @@ -55,51 +49,16 @@ fn layout_row<'a>(name: String, is_current: bool) -> Element<'a, Message> { } fn menu_row(label: &'static str, checked: bool, msg: Message) -> Element<'static, Message> { - let check = crate::ui::icons::check_cell(checked, CHECK_COLOR); + let check = crate::ui::icons::themed_check_cell(checked); - let lbl = text(label) - .size(11) - .color(if checked { LABEL_ON } else { LABEL_OFF }); + let lbl = text(label).size(11); 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() - }) + .style(button::subtle) .width(Fill) .padding([4, 10]) .into() } - -// ── Colours ─────────────────────────────────────────────────────────────── - -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, -}; diff --git a/src/ui/style/anno_object_scale.rs b/src/ui/style/anno_object_scale.rs index 693919cc..477e86fb 100644 --- a/src/ui/style/anno_object_scale.rs +++ b/src/ui/style/anno_object_scale.rs @@ -7,16 +7,9 @@ //! the style / scale managers' frame so it looks consistent. use crate::app::Message; -use crate::ui::style::style_manager::{hdivider, tb_button, BG, BORDER, DIM, LIST, TB, TEXT}; +use crate::ui::style::style_manager::{hdivider, muted_text_style, tb_button}; use iced::widget::{column, container, mouse_area, row, scrollable, text, Space}; -use iced::{Background, Border, Color, Element, Fill, Theme}; - -const MEMBER_CHECK: Color = Color { - r: 0.30, - g: 0.82, - b: 0.36, - a: 1.0, -}; +use iced::{Background, Border, Element, Fill, Theme}; /// `scales` is `(name, "paper:drawing" ratio, is_member)`. Every label is cloned /// into the widget tree, so the returned element borrows nothing from the args. @@ -26,17 +19,17 @@ pub fn view_window( ) -> Element<'static, Message> { let toolbar = container( row![ - text(format!("Object: {object_label}")) - .size(11) - .color(TEXT), + text(format!("Object: {object_label}")).size(11), Space::new().width(Fill), tb_button("Close", Message::CloseModal, true), ] .spacing(4) .align_y(iced::Center), ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(TB)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.weak.color + )), ..Default::default() }) .width(Fill) @@ -45,21 +38,17 @@ pub fn view_window( let rows: Vec> = scales .iter() .map(|(name, ratio, member)| { - let check = crate::ui::icons::check_cell(*member, MEMBER_CHECK); + let check = crate::ui::icons::themed_check_cell(*member); let label = row![ check, - text(name.clone()).size(11).color(TEXT).width(Fill), - text(ratio.clone()).size(10).color(DIM), + text(name.clone()).size(11).width(Fill), + text(ratio.clone()).size(10).style(muted_text_style), ] .spacing(4) .align_y(iced::Center); let cell = container(label) .padding([4, 8]) - .width(Fill) - .style(move |_: &Theme| container::Style { - text_color: Some(TEXT), - ..Default::default() - }); + .width(Fill); mouse_area(cell) .on_press(Message::AnnoObjectScaleToggle(name.clone())) .into() @@ -67,14 +56,17 @@ pub fn view_window( .collect(); let list = container(scrollable(column(rows).spacing(1)).height(Fill)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(LIST)), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.weak.color)), border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 3.0.into(), }, ..Default::default() + } }) .width(Fill) .height(Fill) @@ -84,7 +76,7 @@ pub fn view_window( column![ text("Click a scale to add or remove the object's representation for it.") .size(10) - .color(DIM), + .style(muted_text_style), list, ] .spacing(6) @@ -95,8 +87,10 @@ pub fn view_window( .padding(12); container(column![toolbar, hdivider(), body]) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BG)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.base.color + )), ..Default::default() }) .width(Fill) diff --git a/src/ui/style/dimstyle.rs b/src/ui/style/dimstyle.rs index 375474fd..07c9ad11 100644 --- a/src/ui/style/dimstyle.rs +++ b/src/ui/style/dimstyle.rs @@ -4,44 +4,7 @@ use crate::app::{ColorPickTarget, DsField, Message}; use iced::widget::{ button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Space, }; -use iced::{Background, Border, Color, Element, Fill, Theme}; - -const BORDER: Color = Color { - r: 0.35, - g: 0.35, - b: 0.35, - a: 1.0, -}; -const TEXT: Color = Color { - r: 0.88, - g: 0.88, - b: 0.88, - a: 1.0, -}; -const DIM: Color = Color { - r: 0.55, - g: 0.55, - b: 0.55, - a: 1.0, -}; -const ACCENT: Color = Color { - r: 0.25, - g: 0.50, - b: 0.85, - a: 1.0, -}; -const ACTIVE: Color = Color { - r: 0.20, - g: 0.40, - b: 0.70, - a: 1.0, -}; -const FIELD: Color = Color { - r: 0.10, - g: 0.10, - b: 0.10, - a: 1.0, -}; +use iced::{Background, Border, Element, Fill, Theme}; /// All DimStyle field values needed by the view. pub struct DimStyleValues<'a> { @@ -131,44 +94,57 @@ pub struct DimStyleValues<'a> { } fn tab_btn_style(active: bool) -> impl Fn(&Theme, button::Status) -> button::Style { - move |_: &Theme, st| button::Style { - background: Some(Background::Color(match (active, st) { - (true, _) => ACTIVE, - (false, button::Status::Hovered | button::Status::Pressed) => Color { - r: 0.28, - g: 0.28, - b: 0.28, - a: 1.0, - }, - _ => Color { - r: 0.20, - g: 0.20, - b: 0.20, - a: 1.0, - }, - })), - text_color: TEXT, + move |theme: &Theme, st| { + let palette = theme.extended_palette(); + let pair = match (active, st) { + (true, _) => palette.primary.strong, + (false, button::Status::Hovered | button::Status::Pressed) => { + palette.background.strong + } + _ => palette.background.weak, + }; + button::Style { + background: Some(Background::Color(pair.color)), + text_color: pair.text, border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 3.0.into(), }, ..Default::default() + } } } -fn field_style(_: &Theme, _: text_input::Status) -> text_input::Style { +fn field_style(theme: &Theme, status: text_input::Status) -> text_input::Style { + let palette = theme.extended_palette(); + let border = match status { + text_input::Status::Focused { .. } => palette.primary.base.color, + _ => palette.background.neutral.color, + }; text_input::Style { - background: Background::Color(FIELD), + background: Background::Color(palette.background.base.color), border: Border { - color: BORDER, + color: border, width: 1.0, radius: 3.0.into(), }, - icon: TEXT, - placeholder: DIM, - value: TEXT, - selection: ACCENT, + icon: palette.background.base.text, + placeholder: palette.background.base.text.scale_alpha(0.48), + value: palette.background.base.text, + selection: palette.primary.base.color.scale_alpha(0.5), + } +} + +fn muted_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)), + } +} + +fn primary_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().primary.base.color), } } @@ -176,8 +152,10 @@ fn hdivider<'a>() -> Element<'a, Message> { container(Space::new().width(Fill).height(1)) .width(Fill) .height(1) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BORDER)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.neutral.color + )), ..Default::default() }) .into() @@ -221,7 +199,7 @@ pub fn view_window<'a>( ] .spacing(2); - let lbl = |s: &'static str| text(s).size(11).color(DIM).width(180); + let lbl = |s: &'static str| text(s).size(11).style(muted_style).width(180); let mk_field = |fld: DsField, val: &'a str| -> Element<'a, Message> { text_input("", val) @@ -338,7 +316,7 @@ pub fn view_window<'a>( let tab_content: Element<'_, Message> = match tab { 0 => column![ - text("Dimension Line").size(11).color(ACCENT), + text("Dimension Line").size(11).style(primary_style), row![ lbl("Extension (DIMDLE)"), mk_field(DsField::Dimdle, vals.dimdle) @@ -359,7 +337,7 @@ pub fn view_window<'a>( .align_y(iced::Center), chk("Suppress 1st line (DIMSD1)", vals.dimsd1, DsField::Dimsd1), chk("Suppress 2nd line (DIMSD2)", vals.dimsd2, DsField::Dimsd2), - text("Extension Line").size(11).color(ACCENT), + text("Extension Line").size(11).style(primary_style), row![ lbl("Extension (DIMEXE)"), mk_field(DsField::Dimexe, vals.dimexe) @@ -421,7 +399,7 @@ pub fn view_window<'a>( .spacing(7) .into(), 1 => column![ - text("Arrows").size(11).color(ACCENT), + text("Arrows").size(11).style(primary_style), hrow( "Arrowhead (DIMBLK)", vals.block_opts.clone(), @@ -485,7 +463,7 @@ pub fn view_window<'a>( .spacing(7) .into(), 2 => column![ - text("Text").size(11).color(ACCENT), + text("Text").size(11).style(primary_style), row![ lbl("Height (DIMTXT)"), mk_field(DsField::Dimtxt, vals.dimtxt) @@ -551,7 +529,7 @@ pub fn view_window<'a>( .spacing(7) .into(), 3 => column![ - text("Scale").size(11).color(ACCENT), + text("Scale").size(11).style(primary_style), chk("Annotative", vals.annotative, DsField::Annotative), row![ lbl("Overall scale (DIMSCALE)"), @@ -565,7 +543,7 @@ pub fn view_window<'a>( ] .spacing(8) .align_y(iced::Center), - text("Units").size(11).color(ACCENT), + text("Units").size(11).style(primary_style), enum_field("Format (DIMLUNIT)", DsField::Dimlunit, vals.dimlunit, OPT_LUNIT), row![ lbl("Decimals (DIMDEC)"), @@ -636,7 +614,7 @@ pub fn view_window<'a>( ("3", "Leading & trailing"), ], ), - text("Fit").size(11).color(ACCENT), + text("Fit").size(11).style(primary_style), enum_field( "Fit (DIMATFIT)", DsField::Dimatfit, @@ -680,7 +658,7 @@ pub fn view_window<'a>( .spacing(7) .into(), 5 => column![ - text("Alternate Units").size(11).color(ACCENT), + text("Alternate Units").size(11).style(primary_style), chk( "Enable alternate units (DIMALT)", vals.dimalt, @@ -728,7 +706,7 @@ pub fn view_window<'a>( .spacing(7) .into(), _ => column![ - text("Tolerances").size(11).color(ACCENT), + text("Tolerances").size(11).style(primary_style), chk("Generate tolerances (DIMTOL)", vals.dimtol, DsField::Dimtol), chk("Limits generation (DIMLIM)", vals.dimlim, DsField::Dimlim), row![ @@ -773,7 +751,7 @@ pub fn view_window<'a>( // content container instead of the panel padding. let right_panel = container( column![ - text(format!("Editing: {selected}")).size(11).color(DIM), + text(format!("Editing: {selected}")).size(11).style(muted_style), tabs, hdivider(), scrollable(container(tab_content).padding([12, 12]).width(Fill)) diff --git a/src/ui/style/mleaderstyle.rs b/src/ui/style/mleaderstyle.rs index e9e7474f..22c908c3 100644 --- a/src/ui/style/mleaderstyle.rs +++ b/src/ui/style/mleaderstyle.rs @@ -4,63 +4,41 @@ use crate::app::Message; use iced::widget::{ button, checkbox, column, container, pick_list, row, scrollable, text, text_input, }; -use iced::{Background, Border, Color, Element, Fill, Theme}; - -const BORDER: Color = Color { - r: 0.35, - g: 0.35, - b: 0.35, - a: 1.0, -}; -const TEXT: Color = Color { - r: 0.88, - g: 0.88, - b: 0.88, - a: 1.0, -}; -const DIM: Color = Color { - r: 0.55, - g: 0.55, - b: 0.55, - a: 1.0, -}; -const ACCENT: Color = Color { - r: 0.25, - g: 0.50, - b: 0.85, - a: 1.0, -}; +use iced::{Background, Border, Element, Fill, Theme}; fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style { - move |_: &Theme, st| button::Style { - background: Some(Background::Color(match (accent, st) { - (true, button::Status::Hovered | button::Status::Pressed) => Color { - r: 0.20, - g: 0.42, - b: 0.72, - a: 1.0, - }, - (false, button::Status::Hovered | button::Status::Pressed) => Color { - r: 0.28, - g: 0.28, - b: 0.28, - a: 1.0, - }, - (true, _) => ACCENT, - _ => Color { - r: 0.22, - g: 0.22, - b: 0.22, - a: 1.0, - }, - })), - text_color: TEXT, + move |theme: &Theme, st| { + let palette = theme.extended_palette(); + let pair = match (accent, st) { + (true, button::Status::Hovered | button::Status::Pressed) => palette.primary.strong, + (false, button::Status::Hovered | button::Status::Pressed) => { + palette.background.strong + } + (true, _) => palette.primary.base, + _ => palette.background.weak, + }; + button::Style { + background: Some(Background::Color(pair.color)), + text_color: pair.text, border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 4.0.into(), }, ..Default::default() + } + } +} + +fn muted_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)), + } +} + +fn primary_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().primary.base.color), } } @@ -106,7 +84,7 @@ pub struct MLeaderStyleView<'a> { } fn section<'a>(label: &'static str) -> Element<'a, Message> { - text(label).size(11).color(ACCENT).into() + text(label).size(11).style(primary_style).into() } fn num_row<'a>( @@ -116,7 +94,7 @@ fn num_row<'a>( field: &'static str, ) -> Element<'a, Message> { row![ - text(label).size(11).color(DIM).width(150), + text(label).size(11).style(muted_style).width(150), text_input(placeholder, value) .on_input(move |v| Message::MLeaderStyleEdit { field, value: v }) .size(11) @@ -150,7 +128,7 @@ fn color_row<'a>( Message::MLeaderColorMore(field), Message::OpenColorWindow(crate::app::ColorPickTarget::MLeader(field)), ); - row![text(label).size(11).color(DIM).width(150), selector] + row![text(label).size(11).style(muted_style).width(150), selector] .spacing(8) .align_y(iced::Center) .into() @@ -163,7 +141,7 @@ fn enum_row<'a>( field: &'static str, ) -> Element<'a, Message> { row![ - text(label).size(11).color(DIM).width(150), + text(label).size(11).style(muted_style).width(150), pick_list(options, Some(selected), move |value| { Message::MLeaderStyleSetEnum { field, value } }) @@ -178,7 +156,7 @@ fn enum_row<'a>( fn lineweight_row<'a>(selected: acadrust::types::LineWeight) -> Element<'a, Message> { let selected = crate::ui::properties::LwItem(selected); row![ - text("Line weight:").size(11).color(DIM).width(150), + text("Line weight:").size(11).style(muted_style).width(150), pick_list( crate::ui::properties::lw_options(), Some(selected), @@ -220,7 +198,7 @@ fn handle_row<'a>( field: &'static str, ) -> Element<'a, Message> { row![ - text(label).size(11).color(DIM).width(150), + text(label).size(11).style(muted_style).width(150), pick_list(options, Some(selected), move |value| { Message::MLeaderStyleSetHandle { field, value } }) @@ -247,7 +225,7 @@ pub fn view_window<'a>(v: MLeaderStyleView<'a>) -> Element<'a, Message> { scrollable( column![ row![ - text("Name:").size(11).color(DIM).width(150), + text("Name:").size(11).style(muted_style).width(150), text(s.name.clone()).size(11), ] .spacing(8), @@ -422,7 +400,7 @@ pub fn view_window<'a>(v: MLeaderStyleView<'a>) -> Element<'a, Message> { .height(Fill) .into() } else { - container(text("Select a style to view details.").size(11).color(DIM)) + container(text("Select a style to view details.").size(11).style(muted_style)) .padding([12, 12]) .into() }; diff --git a/src/ui/style/mlstyle.rs b/src/ui/style/mlstyle.rs index bfe5ddb1..9a1c22d3 100644 --- a/src/ui/style/mlstyle.rs +++ b/src/ui/style/mlstyle.rs @@ -2,14 +2,13 @@ use crate::app::Message; use iced::widget::{column, container, row, scrollable, text}; -use iced::{Color, Element, Fill}; +use iced::{Element, Fill, Theme}; -const DIM: Color = Color { - r: 0.55, - g: 0.55, - b: 0.55, - a: 1.0, -}; +fn muted_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)), + } +} pub fn view_window<'a>( styles: Vec, @@ -22,7 +21,7 @@ pub fn view_window<'a>( // ── Right: Details panel ────────────────────────────────────────────── let info_row = |label: &'static str, val: String| -> Element<'_, Message> { row![ - text(label).size(11).color(DIM).width(120), + text(label).size(11).style(muted_style).width(120), text(val).size(11), ] .spacing(8) @@ -49,7 +48,7 @@ pub fn view_window<'a>( &e.linetype }; row![ - text(format!(" {idx}:")).size(10).color(DIM).width(24), + text(format!(" {idx}:")).size(10).style(muted_style).width(24), text(format!("{:+.3}", e.offset)).size(10).width(70), text(color_str).size(10).width(90), text(lt).size(10), @@ -65,7 +64,7 @@ pub fn view_window<'a>( info_row("Elements:", s.elements.len().to_string()), text(" Off Color Ltype") .size(10) - .color(DIM) + .style(muted_style) .into(), ]; col_items.extend(elem_rows); @@ -73,7 +72,7 @@ pub fn view_window<'a>( .height(Fill) .into() } else { - container(text("Select a style to view details.").size(11).color(DIM)) + container(text("Select a style to view details.").size(11).style(muted_style)) .padding([12, 12]) .into() }; diff --git a/src/ui/style/plotstyle.rs b/src/ui/style/plotstyle.rs index 7db90850..0a4ef8c7 100644 --- a/src/ui/style/plotstyle.rs +++ b/src/ui/style/plotstyle.rs @@ -2,108 +2,55 @@ use crate::app::Message; use iced::widget::{button, column, container, row, scrollable, text, text_input, Space}; -use iced::{Background, Border, Color, Element, Fill, Theme}; - -const TB: Color = Color { - r: 0.13, - g: 0.13, - b: 0.13, - a: 1.0, -}; -const BG: Color = Color { - r: 0.15, - g: 0.15, - b: 0.15, - a: 1.0, -}; -const BORDER: Color = Color { - r: 0.35, - g: 0.35, - b: 0.35, - a: 1.0, -}; -const TEXT: Color = Color { - r: 0.88, - g: 0.88, - b: 0.88, - a: 1.0, -}; -const DIM: Color = Color { - r: 0.55, - g: 0.55, - b: 0.55, - a: 1.0, -}; -const ACCENT: Color = Color { - r: 0.25, - g: 0.50, - b: 0.85, - a: 1.0, -}; -const ACTIVE: Color = Color { - r: 0.20, - g: 0.40, - b: 0.70, - a: 1.0, -}; -const FIELD: Color = Color { - r: 0.10, - g: 0.10, - b: 0.10, - a: 1.0, -}; -const LIST: Color = Color { - r: 0.12, - g: 0.12, - b: 0.12, - a: 1.0, -}; +use iced::{Background, Border, Element, Fill, Theme}; fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style { - move |_: &Theme, st| button::Style { - background: Some(Background::Color(match (accent, st) { - (true, button::Status::Hovered | button::Status::Pressed) => Color { - r: 0.20, - g: 0.42, - b: 0.72, - a: 1.0, - }, - (false, button::Status::Hovered | button::Status::Pressed) => Color { - r: 0.28, - g: 0.28, - b: 0.28, - a: 1.0, - }, - (true, _) => ACCENT, - _ => Color { - r: 0.22, - g: 0.22, - b: 0.22, - a: 1.0, - }, - })), - text_color: TEXT, + move |theme: &Theme, st| { + let palette = theme.extended_palette(); + let pair = match (accent, st) { + (true, button::Status::Hovered | button::Status::Pressed) => palette.primary.strong, + (false, button::Status::Hovered | button::Status::Pressed) => { + palette.background.strong + } + (true, _) => palette.primary.base, + _ => palette.background.weak, + }; + button::Style { + background: Some(Background::Color(pair.color)), + text_color: pair.text, border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 4.0.into(), }, ..Default::default() + } } } -fn field_style(_: &Theme, _: text_input::Status) -> text_input::Style { +fn field_style(theme: &Theme, status: text_input::Status) -> text_input::Style { + let palette = theme.extended_palette(); + let border = match status { + text_input::Status::Focused { .. } => palette.primary.base.color, + _ => palette.background.neutral.color, + }; text_input::Style { - background: Background::Color(FIELD), + background: Background::Color(palette.background.base.color), border: Border { - color: BORDER, + color: border, width: 1.0, radius: 3.0.into(), }, - icon: TEXT, - placeholder: DIM, - value: TEXT, - selection: ACCENT, + icon: palette.background.base.text, + placeholder: palette.background.base.text.scale_alpha(0.48), + value: palette.background.base.text, + selection: palette.primary.base.color.scale_alpha(0.5), + } +} + +fn muted_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)), } } @@ -111,8 +58,10 @@ fn hdivider<'a>() -> Element<'a, Message> { container(Space::new().width(Fill).height(1)) .width(Fill) .height(1) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BORDER)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.neutral.color + )), ..Default::default() }) .into() @@ -122,8 +71,10 @@ fn vsep<'a>() -> Element<'a, Message> { container(Space::new().width(1).height(Fill)) .width(1) .height(Fill) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BORDER)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.neutral.color + )), ..Default::default() }) .into() @@ -156,13 +107,15 @@ pub fn view_window<'a>( .style(btn_s(false)) .padding([4, 10]), Space::new().width(Fill), - text(table_name).size(10).color(DIM), + text(table_name).size(10).style(muted_style), ] .spacing(4) .align_y(iced::Center), ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(TB)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.weak.color + )), ..Default::default() }) .width(Fill) @@ -199,19 +152,22 @@ pub fn view_window<'a>( }; button(text(label).size(10).font(iced::Font::MONOSPACE)) .on_press(Message::PlotStylePanelSelectAci(aci)) - .style(move |_: &Theme, st| button::Style { - background: Some(Background::Color(match (is_sel, st) { - (true, _) => ACTIVE, - (false, button::Status::Hovered | button::Status::Pressed) => Color { - r: 0.26, - g: 0.26, - b: 0.26, - a: 1.0, - }, - _ => Color::TRANSPARENT, - })), - text_color: TEXT, + .style(move |theme: &Theme, st| { + let palette = theme.extended_palette(); + let pair = match (is_sel, st) { + (true, _) => Some(palette.primary.strong), + (false, button::Status::Hovered | button::Status::Pressed) => { + Some(palette.background.strong) + } + _ => None, + }; + button::Style { + background: pair.map(|p| Background::Color(p.color)), + text_color: pair + .map(|p| p.text) + .unwrap_or(palette.background.base.text), ..Default::default() + } }) .padding([2, 8]) .width(Fill) @@ -221,16 +177,19 @@ pub fn view_window<'a>( let aci_list = container( column![ - text("ACI Color Index").size(10).color(DIM), + text("ACI Color Index").size(10).style(muted_style), container(scrollable(column(aci_items).spacing(1)).height(Fill)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(LIST)), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.weak.color)), border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 3.0.into() }, ..Default::default() + } }) .width(Fill) .height(Fill) @@ -269,12 +228,12 @@ pub fn view_window<'a>( .map(|e| format!("{}%", e.screening)) .unwrap_or_else(|| "—".into()); - let lbl = |s: &'static str| text(s).size(11).color(DIM); + let lbl = |s: &'static str| text(s).size(11).style(muted_style); let edit_panel = container( column![ row![ - text("ACI:").size(11).color(DIM).width(100), + text("ACI:").size(11).style(muted_style).width(100), text(format!("{selected_aci}")).size(11), ] .spacing(8) @@ -298,7 +257,7 @@ pub fn view_window<'a>( .size(11) .padding([4, 8]), Space::new().height(8), - text("Current values:").size(10).color(DIM), + text("Current values:").size(10).style(muted_style), text(format!(" Color: {cur_color}")).size(10), text(format!(" Lineweight: {cur_lw}")).size(10), text(format!(" Screening: {cur_scr}")).size(10), @@ -318,8 +277,10 @@ pub fn view_window<'a>( let body = row![aci_list, vsep(), edit_panel].height(Fill); container(column![toolbar, hdivider(), body].spacing(0)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BG)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.base.color + )), ..Default::default() }) .width(Fill) diff --git a/src/ui/style/point_style.rs b/src/ui/style/point_style.rs index d2a5ee6a..539b2317 100644 --- a/src/ui/style/point_style.rs +++ b/src/ui/style/point_style.rs @@ -7,15 +7,7 @@ use crate::app::Message; use iced::widget::{button, canvas, column, container, radio, row, text, text_input, Space}; -use iced::{mouse, Background, Border, Color, Element, Length, Point, Rectangle, Size, Theme}; - -const BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 }; -const WHITE: Color = Color { r: 0.92, g: 0.92, b: 0.92, a: 1.0 }; -const DIM: Color = Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 }; -const ACCENT: Color = Color { r: 0.30, g: 0.62, b: 0.95, a: 1.0 }; -const CELL: Color = Color { r: 0.20, g: 0.20, b: 0.20, a: 1.0 }; -const CELL_SEL: Color = Color { r: 0.30, g: 0.62, b: 0.95, a: 1.0 }; -const GLYPH: Color = Color { r: 0.90, g: 0.90, b: 0.90, a: 1.0 }; +use iced::{mouse, Background, Border, Element, Length, Point, Rectangle, Size, Theme}; const CELL_PX: f32 = 44.0; @@ -37,22 +29,23 @@ impl canvas::Program for GlyphCanvas { &self, _state: &(), renderer: &iced::Renderer, - _theme: &Theme, + theme: &Theme, bounds: Rectangle, _cursor: mouse::Cursor, ) -> Vec { let mut frame = canvas::Frame::new(renderer, bounds.size()); + let glyph = theme.extended_palette().background.base.text; let (cx, cy) = (bounds.width * 0.5, bounds.height * 0.5); let r = bounds.width.min(bounds.height) * 0.30; let stroke = canvas::Stroke { width: 1.4, - style: canvas::Style::Solid(GLYPH), + style: canvas::Style::Solid(glyph), ..Default::default() }; let line = |a: Point, b: Point| canvas::Path::line(a, b); match self.mode & 0x0F { - 0 => frame.fill(&canvas::Path::circle(Point::new(cx, cy), 2.4), GLYPH), + 0 => frame.fill(&canvas::Path::circle(Point::new(cx, cy), 2.4), glyph), 1 => {} 2 => { frame.stroke(&line(Point::new(cx - r, cy), Point::new(cx + r, cy)), stroke.clone()); @@ -89,39 +82,52 @@ fn cell<'a>(value: i16, selected: bool) -> Element<'a, Message> { button(glyph) .padding(0) .on_press(Message::PointStyleSetMode(value)) - .style(move |_: &Theme, status| { - let bg = if selected { - CELL_SEL + .style(move |theme: &Theme, status| { + let palette = theme.extended_palette(); + let pair = if selected { + palette.primary.strong } else if matches!(status, button::Status::Hovered | button::Status::Pressed) { - Color { r: 0.28, g: 0.28, b: 0.28, a: 1.0 } + palette.background.strong } else { - CELL + palette.background.weak }; button::Style { - background: Some(Background::Color(bg)), + background: Some(Background::Color(pair.color)), border: Border { - color: Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 }, + color: palette.background.neutral.color, width: 1.0, radius: 3.0.into(), }, + text_color: pair.text, ..Default::default() } }) .into() } -fn field_style(_: &Theme, _: text_input::Status) -> text_input::Style { +fn field_style(theme: &Theme, status: text_input::Status) -> text_input::Style { + let palette = theme.extended_palette(); + let border = match status { + text_input::Status::Focused { .. } => palette.primary.base.color, + _ => palette.background.neutral.color, + }; text_input::Style { - background: Background::Color(Color { r: 0.1, g: 0.1, b: 0.1, a: 1.0 }), + background: Background::Color(palette.background.base.color), border: Border { - color: Color { r: 0.3, g: 0.3, b: 0.3, a: 1.0 }, + color: border, width: 1.0, radius: 4.0.into(), }, - icon: DIM, - placeholder: DIM, - value: WHITE, - selection: ACCENT, + icon: palette.background.base.text, + placeholder: palette.background.base.text.scale_alpha(0.48), + value: palette.background.base.text, + selection: palette.primary.base.color.scale_alpha(0.5), + } +} + +fn muted_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)), } } @@ -138,7 +144,7 @@ pub fn view_window<'a>(pdmode: i16, relative: bool, size_buf: &str) -> Element<' } let size_row = row![ - text("Point Size:").size(13).color(WHITE), + text("Point Size:").size(13), Space::new().width(10), text_input("0", size_buf) .on_input(Message::PointStyleSizeInput) @@ -147,7 +153,7 @@ pub fn view_window<'a>(pdmode: i16, relative: bool, size_buf: &str) -> Element<' .size(13) .width(110), Space::new().width(6), - text(if relative { "%" } else { "units" }).size(12).color(DIM), + text(if relative { "%" } else { "units" }).size(12).style(muted_style), ] .align_y(iced::Center); @@ -171,26 +177,14 @@ pub fn view_window<'a>(pdmode: i16, relative: bool, size_buf: &str) -> Element<' ] .spacing(6); - let ok = button(text("OK").size(13).color(WHITE)) + let ok = button(text("OK").size(13)) .padding([5, 22]) .on_press(Message::PointStyleOk) - .style(|_: &Theme, status| { - let bg = if matches!(status, button::Status::Hovered | button::Status::Pressed) { - Color { r: 0.32, g: 0.55, b: 0.85, a: 1.0 } - } else { - ACCENT - }; - button::Style { - background: Some(Background::Color(bg)), - text_color: WHITE, - border: Border { radius: 4.0.into(), ..Default::default() }, - ..Default::default() - } - }); + .style(button::primary); container( column![ - text("Point Style").size(18).color(WHITE), + text("Point Style").size(18), Space::new().height(6), grid, Space::new().height(12), @@ -203,8 +197,10 @@ pub fn view_window<'a>(pdmode: i16, relative: bool, size_buf: &str) -> Element<' .spacing(4) .padding(20), ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BG)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.base.color + )), ..Default::default() }) .into() diff --git a/src/ui/style/scale_manager.rs b/src/ui/style/scale_manager.rs index 0f51dfd8..fba9e389 100644 --- a/src/ui/style/scale_manager.rs +++ b/src/ui/style/scale_manager.rs @@ -6,53 +6,36 @@ //! messages instead of the StyleKind machinery. use crate::app::Message; -use crate::ui::style::style_manager::{hdivider, tb_button, vsep, BG, BORDER, DIM, LIST, TB, TEXT}; +use crate::ui::style::style_manager::{ + hdivider, muted_text_style, tb_button, vsep, +}; use iced::widget::{ column, container, mouse_area, row, scrollable, text, text_input, Space, }; -use iced::{Background, Border, Color, Element, Fill, Theme}; +use iced::{Background, Border, Element, Fill, Theme}; /// Inline-rename text-input id, so the rename-start handler can focus it. pub fn rename_input_id() -> iced::widget::Id { iced::widget::Id::new("scale-rename-input") } -const INPUT_BG: Color = Color { - r: 0.10, - g: 0.10, - b: 0.10, - a: 1.0, -}; -const ACTIVE: Color = Color { - r: 0.20, - g: 0.40, - b: 0.70, - a: 1.0, -}; -const CURRENT_CHECK: Color = Color { - r: 0.30, - g: 0.82, - b: 0.36, - a: 1.0, -}; - -fn field_style(_: &Theme, _: text_input::Status) -> text_input::Style { +fn field_style(theme: &Theme, status: text_input::Status) -> text_input::Style { + let palette = theme.extended_palette(); + let border = match status { + text_input::Status::Focused { .. } => palette.primary.base.color, + _ => palette.background.neutral.color, + }; text_input::Style { - background: Background::Color(INPUT_BG), + background: Background::Color(palette.background.base.color), border: Border { - color: BORDER, + color: border, width: 1.0, radius: 4.0.into(), }, - icon: TEXT, - placeholder: DIM, - value: TEXT, - selection: Color { - r: 0.20, - g: 0.46, - b: 0.80, - a: 0.45, - }, + icon: palette.background.base.text, + placeholder: palette.background.base.text.scale_alpha(0.48), + value: palette.background.base.text, + selection: palette.primary.base.color.scale_alpha(0.5), } } @@ -80,8 +63,10 @@ pub fn view_window<'a, 'b>( .spacing(4) .align_y(iced::Center), ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(TB)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.weak.color + )), ..Default::default() }) .width(Fill) @@ -105,21 +90,24 @@ pub fn view_window<'a, 'b>( } let is_sel = name.as_str() == selected; let is_cur = name.eq_ignore_ascii_case(current); - let check = crate::ui::icons::check_cell(is_cur, CURRENT_CHECK); + let check = crate::ui::icons::themed_check_cell(is_cur); let label = row![ check, - text(name.clone()).size(11).color(TEXT).width(Fill), - text(ratio.clone()).size(10).color(DIM), + text(name.clone()).size(11).width(Fill), + text(ratio.clone()).size(10).style(muted_text_style), ] .spacing(4) .align_y(iced::Center); let cell = container(label) .padding([4, 8]) .width(Fill) - .style(move |_: &Theme| container::Style { - background: is_sel.then_some(Background::Color(ACTIVE)), - text_color: Some(TEXT), + .style(move |theme: &Theme| { + let pair = theme.extended_palette().primary.strong; + container::Style { + background: is_sel.then_some(Background::Color(pair.color)), + text_color: is_sel.then_some(pair.text), ..Default::default() + } }); mouse_area(cell) .on_press(Message::ScaleManagerSelect(name.clone())) @@ -130,16 +118,19 @@ pub fn view_window<'a, 'b>( let list_panel = container( column![ - text("Scales").size(10).color(DIM), + text("Scales").size(10).style(muted_text_style), container(scrollable(column(rows).spacing(1)).height(Fill)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(LIST)), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.weak.color)), border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 3.0.into() }, ..Default::default() + } }) .width(Fill) .height(Fill) @@ -161,7 +152,7 @@ pub fn view_window<'a, 'b>( let field = |label: &'static str, ph: &'static str, value: &str, on: fn(String) -> Message| { row![ - text(label).size(11).color(DIM).width(96), + text(label).size(11).style(muted_text_style).width(96), text_input(ph, value) .on_input(on) .style(field_style) @@ -175,13 +166,13 @@ pub fn view_window<'a, 'b>( let editor = container( column![ - text("Scale").size(10).color(DIM), + text("Scale").size(10).style(muted_text_style), field("Paper units", "1", paper_buf, Message::ScaleManagerPaperBuf), field("Drawing units", "50", drawing_buf, Message::ScaleManagerDrawingBuf), Space::new().height(6), text("Double-click a scale to rename it; edit its paper : drawing ratio here. New / Copy add a scale. Changes are kept only if you click Apply before closing.") .size(10) - .color(DIM), + .style(muted_text_style), ] .spacing(8), ) @@ -192,8 +183,10 @@ pub fn view_window<'a, 'b>( let body = row![list_panel, vsep(), editor].height(Fill); container(column![toolbar, hdivider(), body]) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BG)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.base.color + )), ..Default::default() }) .width(Fill) diff --git a/src/ui/style/style_list.rs b/src/ui/style/style_list.rs index 3e0ee819..14ab3ae9 100644 --- a/src/ui/style/style_list.rs +++ b/src/ui/style/style_list.rs @@ -10,7 +10,7 @@ use crate::app::{Message, StyleKind}; use iced::widget::{container, mouse_area, row, text, text_input}; -use iced::{Background, Color, Element, Fill, Theme}; +use iced::{Background, Element, Fill, Theme}; /// Shared id for the inline rename field, so the rename-start handler can focus /// it the moment the row turns editable. @@ -18,25 +18,6 @@ pub fn rename_input_id() -> iced::widget::Id { iced::widget::Id::new("style-rename-input") } -const TEXT: Color = Color { - r: 0.88, - g: 0.88, - b: 0.88, - a: 1.0, -}; -const ACTIVE: Color = Color { - r: 0.20, - g: 0.40, - b: 0.70, - a: 1.0, -}; -const CURRENT_CHECK: Color = Color { - r: 0.30, - g: 0.82, - b: 0.36, - a: 1.0, -}; - /// One row of the style list. Renders an editable `text_input` when `name` is /// the style being renamed (`rename_active`), otherwise a selectable row whose /// double click starts the rename. The current style gets a green ✓. @@ -61,16 +42,19 @@ pub fn item<'a>( } else { // Fixed-width ✓ column keeps every name left-aligned whether or not the // row is current. - let check = crate::ui::icons::check_cell(is_current, CURRENT_CHECK); - let label = row![check, text(name.to_string()).size(11).color(TEXT)] + let check = crate::ui::icons::themed_check_cell(is_current); + let label = row![check, text(name.to_string()).size(11)] .align_y(iced::Center); let cell = container(label) .padding([4, 8]) .width(Fill) - .style(move |_: &Theme| container::Style { - background: is_selected.then_some(Background::Color(ACTIVE)), - text_color: Some(TEXT), + .style(move |theme: &Theme| { + let pair = theme.extended_palette().primary.strong; + container::Style { + background: is_selected.then_some(Background::Color(pair.color)), + text_color: is_selected.then_some(pair.text), ..Default::default() + } }); mouse_area(cell) .on_press(on_select) diff --git a/src/ui/style/style_manager.rs b/src/ui/style/style_manager.rs index 8307028f..499fe80d 100644 --- a/src/ui/style/style_manager.rs +++ b/src/ui/style/style_manager.rs @@ -10,52 +10,7 @@ use crate::app::{Message, StyleKind}; use iced::widget::button::{Status, Style}; use iced::widget::{button, column, container, row, scrollable, text, Space}; -use iced::{Background, Border, Color, Element, Fill, Theme}; - -// Shared palette — also reused by the scale manager so it matches the style -// managers exactly ([[feedback_shared_infra]]). -pub(crate) const TB: Color = Color { - r: 0.13, - g: 0.13, - b: 0.13, - a: 1.0, -}; -pub(crate) const BG: Color = Color { - r: 0.15, - g: 0.15, - b: 0.15, - a: 1.0, -}; -pub(crate) const BORDER: Color = Color { - r: 0.35, - g: 0.35, - b: 0.35, - a: 1.0, -}; -pub(crate) const TEXT: Color = Color { - r: 0.88, - g: 0.88, - b: 0.88, - a: 1.0, -}; -pub(crate) const DIM: Color = Color { - r: 0.55, - g: 0.55, - b: 0.55, - a: 1.0, -}; -const ACCENT: Color = Color { - r: 0.25, - g: 0.50, - b: 0.85, - a: 1.0, -}; -pub(crate) const LIST: Color = Color { - r: 0.12, - g: 0.12, - b: 0.12, - a: 1.0, -}; +use iced::{Background, Border, Element, Fill, Theme}; /// Everything the shared frame needs. The per-manager `editor` element is the /// only bespoke part. @@ -103,8 +58,10 @@ pub fn view<'a, 'b>(s: Scaffold<'a, 'b>) -> Element<'a, Message> { .spacing(4) .align_y(iced::Center); let toolbar = container(bar) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(TB)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.weak.color + )), ..Default::default() }) .width(Fill) @@ -131,16 +88,19 @@ pub fn view<'a, 'b>(s: Scaffold<'a, 'b>) -> Element<'a, Message> { let list_panel = container( column![ - text("Styles").size(10).color(DIM), + text("Styles").size(10).style(muted_text_style), container(scrollable(column(rows).spacing(1)).height(Fill)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(LIST)), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.weak.color)), border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 3.0.into() }, ..Default::default() + } }) .width(Fill) .height(Fill) @@ -161,8 +121,10 @@ pub fn view<'a, 'b>(s: Scaffold<'a, 'b>) -> Element<'a, Message> { let body = row![list_panel, vsep(), s.editor].height(Fill); container(column![toolbar, hdivider(), body]) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BG)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.base.color + )), ..Default::default() }) .width(Fill) @@ -182,35 +144,30 @@ pub(crate) fn tb_button<'a>(label: &'a str, msg: Message, accent: bool) -> Eleme } fn btn_s(accent: bool) -> impl Fn(&Theme, Status) -> Style { - move |_: &Theme, st| Style { - background: Some(Background::Color(match (accent, st) { - (true, Status::Hovered | Status::Pressed) => Color { - r: 0.20, - g: 0.42, - b: 0.72, - a: 1.0, - }, - (false, Status::Hovered | Status::Pressed) => Color { - r: 0.28, - g: 0.28, - b: 0.28, - a: 1.0, - }, - (true, _) => ACCENT, - _ => Color { - r: 0.22, - g: 0.22, - b: 0.22, - a: 1.0, - }, - })), - text_color: TEXT, + move |theme: &Theme, st| { + let palette = theme.extended_palette(); + let pair = match (accent, st) { + (true, Status::Hovered | Status::Pressed) => palette.primary.strong, + (false, Status::Hovered | Status::Pressed) => palette.background.strong, + (true, _) => palette.primary.base, + _ => palette.background.weak, + }; + Style { + background: Some(Background::Color(pair.color)), + text_color: pair.text, border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 4.0.into(), }, ..Default::default() + } + } +} + +pub(crate) fn muted_text_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)), } } @@ -218,8 +175,10 @@ pub(crate) fn hdivider<'a>() -> Element<'a, Message> { container(Space::new().width(Fill).height(1)) .width(Fill) .height(1) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BORDER)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.neutral.color + )), ..Default::default() }) .into() @@ -229,8 +188,10 @@ pub(crate) fn vsep<'a>() -> Element<'a, Message> { container(Space::new().width(1).height(Fill)) .width(1) .height(Fill) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BORDER)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.neutral.color + )), ..Default::default() }) .into() diff --git a/src/ui/style/tablestyle.rs b/src/ui/style/tablestyle.rs index c5bbfc0b..b6750524 100644 --- a/src/ui/style/tablestyle.rs +++ b/src/ui/style/tablestyle.rs @@ -4,63 +4,41 @@ use crate::app::Message; use iced::widget::{ button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Column, }; -use iced::{Background, Border, Color, Element, Fill, Theme}; - -const BORDER: Color = Color { - r: 0.35, - g: 0.35, - b: 0.35, - a: 1.0, -}; -const TEXT: Color = Color { - r: 0.88, - g: 0.88, - b: 0.88, - a: 1.0, -}; -const DIM: Color = Color { - r: 0.55, - g: 0.55, - b: 0.55, - a: 1.0, -}; -const ACCENT: Color = Color { - r: 0.25, - g: 0.50, - b: 0.85, - a: 1.0, -}; +use iced::{Background, Border, Element, Fill, Theme}; fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style { - move |_: &Theme, st| button::Style { - background: Some(Background::Color(match (accent, st) { - (true, button::Status::Hovered | button::Status::Pressed) => Color { - r: 0.20, - g: 0.42, - b: 0.72, - a: 1.0, - }, - (false, button::Status::Hovered | button::Status::Pressed) => Color { - r: 0.28, - g: 0.28, - b: 0.28, - a: 1.0, - }, - (true, _) => ACCENT, - _ => Color { - r: 0.22, - g: 0.22, - b: 0.22, - a: 1.0, - }, - })), - text_color: TEXT, + move |theme: &Theme, st| { + let palette = theme.extended_palette(); + let pair = match (accent, st) { + (true, button::Status::Hovered | button::Status::Pressed) => palette.primary.strong, + (false, button::Status::Hovered | button::Status::Pressed) => { + palette.background.strong + } + (true, _) => palette.primary.base, + _ => palette.background.weak, + }; + button::Style { + background: Some(Background::Color(pair.color)), + text_color: pair.text, border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 4.0.into(), }, ..Default::default() + } + } +} + +fn muted_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)), + } +} + +fn primary_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().primary.base.color), } } @@ -89,7 +67,7 @@ pub fn view_window<'a>( // ── Right: Details panel ────────────────────────────────────────────── let info_row = |label: &'static str, val: String| -> Element<'_, Message> { row![ - text(label).size(11).color(DIM).width(160), + text(label).size(11).style(muted_style).width(160), text(val).size(11), ] .spacing(8) @@ -108,7 +86,7 @@ pub fn view_window<'a>( field: &'static str| -> Element<'a, Message> { row![ - text(label).size(11).color(DIM).width(150), + text(label).size(11).style(muted_style).width(150), text_input(placeholder, value) .on_input(move |v| Message::TableStyleCellEdit { row, @@ -142,14 +120,14 @@ pub fn view_window<'a>( Message::TableColorMore(row, field), Message::OpenColorWindow(crate::app::ColorPickTarget::Table(row, field)), ); - row![text(label).size(11).color(DIM).width(150), selector] + row![text(label).size(11).style(muted_style).width(150), selector] .spacing(8) .align_y(iced::Center) .into() }; let mut col = Column::new() .spacing(3) - .push(text(row_label).size(11).color(ACCENT)) + .push(text(row_label).size(11).style(primary_style)) .push(cell_in( " Text style:", "Standard", @@ -161,7 +139,7 @@ pub fn view_window<'a>( .push(cell_color(" Fill color:", &cell_fillcolor[r], "fillcolor")) .push( row![ - text(" Alignment:").size(11).color(DIM).width(150), + text(" Alignment:").size(11).style(muted_style).width(150), pick_list( [ "TopLeft", @@ -199,7 +177,7 @@ pub fn view_window<'a>( .push( text(" Borders (type / weight / color / spacing / hidden)") .size(10) - .color(DIM), + .style(muted_style), ); let borders: [(&'static str, &acadrust::objects::TableCellBorder); 6] = [ @@ -214,7 +192,7 @@ pub fn view_window<'a>( let bu = b as u8; col = col.push( row![ - text(format!(" {bname}")).size(11).color(DIM).width(28), + text(format!(" {bname}")).size(11).style(muted_style).width(28), pick_list( ["Single", "Double"] .iter() @@ -282,7 +260,7 @@ pub fn view_window<'a>( column![ info_row("Name:", s.name.clone()), row![ - text("Description:").size(11).color(DIM).width(160), + text("Description:").size(11).style(muted_style).width(160), text_input("", description_buf) .on_input(|v| Message::TableStyleEdit { field: "description", @@ -294,7 +272,7 @@ pub fn view_window<'a>( .spacing(8) .align_y(iced::Center), row![ - text("Flow direction:").size(11).color(DIM).width(160), + text("Flow direction:").size(11).style(muted_style).width(160), pick_list( ["Down", "Up"] .iter() @@ -314,7 +292,7 @@ pub fn view_window<'a>( .size(14) .text_size(11), row![ - text("H Margin:").size(11).color(DIM).width(160), + text("H Margin:").size(11).style(muted_style).width(160), text_input("1.5", hmargin_buf) .on_input(|v| Message::TableStyleEdit { field: "hmargin", @@ -326,7 +304,7 @@ pub fn view_window<'a>( .spacing(8) .align_y(iced::Center), row![ - text("V Margin:").size(11).color(DIM).width(160), + text("V Margin:").size(11).style(muted_style).width(160), text_input("1.5", vmargin_buf) .on_input(|v| Message::TableStyleEdit { field: "vmargin", @@ -362,7 +340,7 @@ pub fn view_window<'a>( .height(Fill) .into() } else { - container(text("Select a style to view details.").size(11).color(DIM)) + container(text("Select a style to view details.").size(11).style(muted_style)) .padding([12, 12]) .into() }; diff --git a/src/ui/style/textstyle.rs b/src/ui/style/textstyle.rs index 16a3cbab..9983f6fd 100644 --- a/src/ui/style/textstyle.rs +++ b/src/ui/style/textstyle.rs @@ -5,7 +5,7 @@ use crate::app::StyleKind; use iced::widget::{ button, canvas, checkbox, column, container, row, scrollable, text, text_input, Space, }; -use iced::{mouse, Background, Border, Color, Element, Fill, Length, Point, Rectangle, Theme}; +use iced::{mouse, Background, Border, Element, Fill, Length, Point, Rectangle, Theme}; /// View-model for the Text Style editor window. pub struct TextStyleView<'a> { @@ -28,49 +28,6 @@ pub struct TextStyleView<'a> { pub rename_buf: &'a str, } -const BORDER: Color = Color { - r: 0.35, - g: 0.35, - b: 0.35, - a: 1.0, -}; -const TEXT: Color = Color { - r: 0.88, - g: 0.88, - b: 0.88, - a: 1.0, -}; -const DIM: Color = Color { - r: 0.55, - g: 0.55, - b: 0.55, - a: 1.0, -}; -const ACCENT: Color = Color { - r: 0.25, - g: 0.50, - b: 0.85, - a: 1.0, -}; -const ACTIVE: Color = Color { - r: 0.20, - g: 0.40, - b: 0.70, - a: 1.0, -}; -const FIELD: Color = Color { - r: 0.10, - g: 0.10, - b: 0.10, - a: 1.0, -}; -const LIST: Color = Color { - r: 0.12, - g: 0.12, - b: 0.12, - a: 1.0, -}; - const BUILTIN_FONTS: &[&str] = &[ "Standard", "ISO", "Simplex", "RomanS", "RomanD", "RomanC", "RomanT", "ItalicC", "ItalicT", "ScriptS", "ScriptC", "GothGBT", "GothGRT", "GothITT", "GreekC", "Symbol", @@ -78,34 +35,52 @@ const BUILTIN_FONTS: &[&str] = &[ ]; fn list_item(active: bool) -> impl Fn(&Theme, button::Status) -> button::Style { - move |_: &Theme, st| button::Style { - background: Some(Background::Color(match (active, st) { - (true, _) => ACTIVE, - (false, button::Status::Hovered | button::Status::Pressed) => Color { - r: 0.26, - g: 0.26, - b: 0.26, - a: 1.0, - }, - _ => Color::TRANSPARENT, - })), - text_color: TEXT, + move |theme: &Theme, st| { + let palette = theme.extended_palette(); + let pair = match (active, st) { + (true, _) => Some(palette.primary.strong), + (false, button::Status::Hovered | button::Status::Pressed) => { + Some(palette.background.strong) + } + _ => None, + }; + button::Style { + background: pair.map(|p| Background::Color(p.color)), + text_color: pair.map(|p| p.text).unwrap_or(palette.background.base.text), ..Default::default() + } } } -fn field_style(_: &Theme, _: text_input::Status) -> text_input::Style { +fn field_style(theme: &Theme, status: text_input::Status) -> text_input::Style { + let palette = theme.extended_palette(); + let border = match status { + text_input::Status::Focused { .. } => palette.primary.base.color, + _ => palette.background.neutral.color, + }; text_input::Style { - background: Background::Color(FIELD), + background: Background::Color(palette.background.base.color), border: Border { - color: BORDER, + color: border, width: 1.0, radius: 3.0.into(), }, - icon: TEXT, - placeholder: DIM, - value: TEXT, - selection: ACCENT, + icon: palette.background.base.text, + placeholder: palette.background.base.text.scale_alpha(0.48), + value: palette.background.base.text, + selection: palette.primary.base.color.scale_alpha(0.5), + } +} + +fn muted_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)), + } +} + +fn primary_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().primary.base.color), } } @@ -113,8 +88,10 @@ fn vsep<'a>() -> Element<'a, Message> { container(Space::new().width(1).height(Fill)) .width(1) .height(Fill) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BORDER)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.neutral.color + )), ..Default::default() }) .into() @@ -143,7 +120,7 @@ impl canvas::Program for TextPreviewCanvas { &self, _state: &(), renderer: &iced::Renderer, - _theme: &Theme, + theme: &Theme, bounds: Rectangle, _cursor: mouse::Cursor, ) -> Vec { @@ -185,7 +162,7 @@ impl canvas::Program for TextPreviewCanvas { }; let stroke = canvas::Stroke { width: 1.4, - style: canvas::Style::Solid(TEXT), + style: canvas::Style::Solid(theme.extended_palette().background.base.text), ..Default::default() }; for s in &strokes { @@ -237,16 +214,19 @@ pub fn view_window<'a>(v: TextStyleView<'a>) -> Element<'a, Message> { let font_panel = container( column![ - text("Font File").size(10).color(DIM), + text("Font File").size(10).style(muted_style), container(scrollable(column(font_items).spacing(1)).height(Fill)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(LIST)), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.weak.color)), border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 3.0.into() }, ..Default::default() + } }) .width(Fill) .height(Fill) @@ -275,7 +255,7 @@ pub fn view_window<'a>(v: TextStyleView<'a>) -> Element<'a, Message> { field: &'static str, ) -> Element<'a, Message> { row![ - text(label).size(11).color(DIM).width(120), + text(label).size(11).style(muted_style).width(120), text_input(ph, buf) .on_input(move |v| Message::TextStyleEdit { field, value: v }) .style(field_style) @@ -312,7 +292,7 @@ pub fn view_window<'a>(v: TextStyleView<'a>) -> Element<'a, Message> { // ── Right: Properties ───────────────────────────────────────────────── let props_panel = container( column![ - text("Properties").size(11).color(ACCENT), + text("Properties").size(11).style(primary_style), frow("Big Font:", "big-font file…", bigfont_buf, "bigfont"), frow("TrueType Font:", "e.g. Arial", ttf_buf, "ttf"), frow("Fixed Height:", "0 = variable", height_buf, "height"), @@ -337,16 +317,19 @@ pub fn view_window<'a>(v: TextStyleView<'a>) -> Element<'a, Message> { .size(15) .text_size(11), Space::new().height(8), - text("Preview").size(10).color(DIM), + text("Preview").size(10).style(muted_style), container(preview) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(FIELD)), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.base.color)), border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 4.0.into() }, ..Default::default() + } }) .padding(8) .width(Fill), @@ -382,16 +365,19 @@ pub fn view_window<'a>(v: TextStyleView<'a>) -> Element<'a, Message> { let ttf_panel = container( column![ - text("TrueType (system)").size(10).color(DIM), + text("TrueType (system)").size(10).style(muted_style), container(scrollable(column(ttf_items).spacing(1)).height(Fill)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(LIST)), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.weak.color)), border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 3.0.into() }, ..Default::default() + } }) .width(Fill) .height(Fill) diff --git a/src/ui/window/about.rs b/src/ui/window/about.rs index ce170cb3..53c3d431 100644 --- a/src/ui/window/about.rs +++ b/src/ui/window/about.rs @@ -1,48 +1,16 @@ use crate::app::Message; use iced::widget::{button, column, container, row, text}; -use iced::{Background, Border, Color, Element, Theme}; - -const BG: Color = Color { - r: 0.15, - g: 0.15, - b: 0.15, - a: 1.0, -}; -const PANEL: Color = Color { - r: 0.12, - g: 0.12, - b: 0.12, - a: 1.0, -}; -const BORDER: Color = Color { - r: 0.30, - g: 0.30, - b: 0.30, - a: 1.0, -}; -const DIM: Color = Color { - r: 0.55, - g: 0.55, - b: 0.55, - a: 1.0, -}; -const ACCENT: Color = Color { - r: 0.25, - g: 0.50, - b: 0.85, - a: 1.0, -}; -const WHITE: Color = Color { - r: 0.92, - g: 0.92, - b: 0.92, - a: 1.0, -}; +use iced::{Background, Element, Theme}; fn info_row<'a>(label: &'static str, value: String) -> Element<'a, Message> { row![ - text(label).size(11).color(DIM).width(100), - text(value).size(11).color(WHITE), + text(label) + .size(11) + .style(|theme: &Theme| iced::widget::text::Style { + color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)), + }) + .width(100), + text(value).size(11), ] .spacing(8) .align_y(iced::Center) @@ -57,10 +25,16 @@ pub fn view_window<'a>() -> Element<'a, Message> { let logo = container( column![ - text("Open CAD Studio").size(32).color(ACCENT), + text("Open CAD Studio") + .size(32) + .style(|theme: &Theme| iced::widget::text::Style { + color: Some(theme.extended_palette().primary.base.color), + }), text("CAD application for Architecture & Engineering") .size(11) - .color(DIM), + .style(|theme: &Theme| iced::widget::text::Style { + color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)), + }), ] .spacing(4) .align_x(iced::Center), @@ -82,35 +56,11 @@ pub fn view_window<'a>() -> Element<'a, Message> { .spacing(2) .padding([12, 16]), ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL)), - border: Border { - color: BORDER, - width: 1.0, - radius: 4.0.into(), - }, - ..Default::default() - }); + .style(container::bordered_box); let copy_btn = button(text("Copy Info").size(11)) .on_press(Message::AboutCopyInfo) - .style(|_: &Theme, st| button::Style { - background: Some(Background::Color(match st { - button::Status::Hovered | button::Status::Pressed => Color { - r: 0.20, - g: 0.42, - b: 0.72, - a: 1.0, - }, - _ => ACCENT, - })), - text_color: WHITE, - border: Border { - radius: 4.0.into(), - ..Default::default() - }, - ..Default::default() - }) + .style(button::primary) .padding([6, 16]); let footer = row![copy_btn] @@ -132,8 +82,10 @@ pub fn view_window<'a>() -> Element<'a, Message> { left: 20.0, }), ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BG)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.base.color, + )), ..Default::default() }) .into() diff --git a/src/ui/window/alias_editor.rs b/src/ui/window/alias_editor.rs index 6309bece..6f453802 100644 --- a/src/ui/window/alias_editor.rs +++ b/src/ui/window/alias_editor.rs @@ -6,7 +6,7 @@ use crate::app::Message; use iced::widget::{button, column, container, row, scrollable, text, text_input, Space}; -use iced::{Background, Border, Color, Element, Fill, Length, Theme}; +use iced::{Background, Element, Fill, Length, Theme}; /// Which column of an alias row a text edit targets. #[derive(Clone, Copy, Debug)] @@ -18,33 +18,21 @@ pub enum AliasField { /// Right-hand lane reserved for the scrollbar so it never overlaps the ✕ column. const GUTTER: f32 = 16.0; -const BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 }; -const FIELD_BG: Color = Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 }; -const BORDER: Color = Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 }; -const DIM: Color = Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 }; -const WHITE: Color = Color { r: 0.85, g: 0.85, b: 0.85, a: 1.0 }; -const ADD_C: Color = Color { r: 0.20, g: 0.40, b: 0.62, a: 1.0 }; - -fn field_style(_: &Theme, _s: text_input::Status) -> text_input::Style { - text_input::Style { - background: Background::Color(FIELD_BG), - border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, - icon: WHITE, - placeholder: DIM, - value: WHITE, - selection: Color { r: 0.25, g: 0.40, b: 0.60, a: 1.0 }, +fn muted_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)), } } /// Build the alias editor content. `rows` is the live working buffer. pub fn view_window(rows: &[(String, String)]) -> Element<'_, Message> { - let title = text("Command Aliases").size(15).color(WHITE); + let title = text("Command Aliases").size(15); let hint = text( "Type an alias and the command it runs (e.g. L → LINE). \ Apply to save to ocad.pgp; closing discards unapplied edits.", ) .size(11) - .color(DIM); + .style(muted_style); // Right gutter reserved so the scrollbar has its own lane and never sits on // top of the row delete (✕) buttons. Applied to both the header and the @@ -53,8 +41,8 @@ pub fn view_window(rows: &[(String, String)]) -> Element<'_, Message> { let head = container( row![ - container(text("Alias").size(11).color(DIM)).width(Length::Fixed(120.0)), - container(text("Command").size(11).color(DIM)).width(Fill), + container(text("Alias").size(11).style(muted_style)).width(Length::Fixed(120.0)), + container(text("Command").size(11).style(muted_style)).width(Fill), Space::new().width(Length::Fixed(30.0)), ] .spacing(8), @@ -65,31 +53,18 @@ pub fn view_window(rows: &[(String, String)]) -> Element<'_, Message> { for (idx, (alias, cmd)) in rows.iter().enumerate() { let alias_box = text_input("alias", alias) .on_input(move |v| Message::AliasEditorInput { idx, field: AliasField::Alias, value: v }) - .style(field_style) .size(13) .padding([3, 6]) .width(Length::Fixed(120.0)); let cmd_box = text_input("command", cmd) .on_input(move |v| Message::AliasEditorInput { idx, field: AliasField::Command, value: v }) - .style(field_style) .size(13) .padding([3, 6]) .width(Fill); - let del = button(crate::ui::icons::tinted(crate::ui::icons::CLOSE, 12.0, WHITE)) + let del = button(crate::ui::icons::themed_danger(crate::ui::icons::CLOSE, 12.0)) .on_press(Message::AliasEditorRemove(idx)) .padding([2, 6]) - .style(|_: &Theme, status| { - let bg = if matches!(status, button::Status::Hovered) { - Color { r: 0.45, g: 0.22, b: 0.22, a: 1.0 } - } else { - Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 } - }; - button::Style { - background: Some(Background::Color(bg)), - border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, - ..Default::default() - } - }); + .style(button::danger); list = list.push( row![alias_box, cmd_box, del] .spacing(8) @@ -97,40 +72,16 @@ pub fn view_window(rows: &[(String, String)]) -> Element<'_, Message> { ); } - let add = button(text("+ Add alias").size(12).color(WHITE)) + let add = button(text("+ Add alias").size(12)) .on_press(Message::AliasEditorAdd) .padding([4, 10]) - .style(|_: &Theme, status| { - let bg = if matches!(status, button::Status::Hovered) { - Color { r: 0.30, g: 0.30, b: 0.30, a: 1.0 } - } else { - Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 } - }; - button::Style { - background: Some(Background::Color(bg)), - text_color: WHITE, - border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, - ..Default::default() - } - }); + .style(button::secondary); // Apply — primary action; commits the rows to ocad.pgp and stays open. - let apply = button(text("Apply").size(12).color(WHITE)) + let apply = button(text("Apply").size(12)) .on_press(Message::AliasEditorApply) .padding([4, 16]) - .style(|_: &Theme, status| { - let bg = if matches!(status, button::Status::Hovered | button::Status::Pressed) { - Color { r: 0.24, g: 0.46, b: 0.74, a: 1.0 } - } else { - ADD_C - }; - button::Style { - background: Some(Background::Color(bg)), - text_color: WHITE, - border: Border { radius: 4.0.into(), ..Default::default() }, - ..Default::default() - } - }); + .style(button::primary); container( column![ @@ -149,8 +100,10 @@ pub fn view_window(rows: &[(String, String)]) -> Element<'_, Message> { .padding(12) .width(Fill) .height(Fill) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BG)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.base.color, + )), ..Default::default() }) .into() diff --git a/src/ui/window/attribute_editor.rs b/src/ui/window/attribute_editor.rs index 5c2efae0..ba3c1915 100644 --- a/src/ui/window/attribute_editor.rs +++ b/src/ui/window/attribute_editor.rs @@ -19,23 +19,10 @@ use acadrust::types::{Color as AcadColor, LineWeight}; use iced::widget::{ button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Space, }; -use iced::{Background, Border, Color, Element, Length, Theme}; +use iced::{Background, Border, Element, Length, Theme}; use crate::ui::properties::{lw_options, LwItem}; -// Palette shared with the style-manager windows so the editor reads as one of -// them: toolbar (TB) on top, BG panels, ACTIVE for the selected tab / row. -const TB: Color = Color { r: 0.13, g: 0.13, b: 0.13, a: 1.0 }; -const BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 }; -const WHITE: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 }; -const DIM: Color = Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 }; -const ACCENT: Color = Color { r: 0.25, g: 0.50, b: 0.85, a: 1.0 }; -const FIELD_BG: Color = Color { r: 0.10, g: 0.10, b: 0.10, a: 1.0 }; -const BORDER: Color = Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 }; -const ACTIVE: Color = Color { r: 0.20, g: 0.40, b: 0.70, a: 1.0 }; -const ROW_SEL: Color = ACTIVE; -const ROW_BG: Color = Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 }; - const LABEL_W: f32 = 120.0; /// Which tab of the editor is showing. @@ -146,28 +133,25 @@ pub fn color_from_label(label: &str) -> Option { }) } -fn field_style(_t: &Theme, _s: text_input::Status) -> text_input::Style { - text_input::Style { - background: Background::Color(FIELD_BG), - border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, - icon: WHITE, - placeholder: DIM, - value: WHITE, - selection: ACCENT, +fn muted_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)), } } -fn accent_btn(_t: &Theme, status: button::Status) -> button::Style { - let bg = if matches!(status, button::Status::Hovered | button::Status::Pressed) { - Color { r: 0.20, g: 0.42, b: 0.72, a: 1.0 } - } else { - ACCENT +fn field_style(theme: &Theme, status: text_input::Status) -> text_input::Style { + let palette = theme.extended_palette(); + let border = match status { + text_input::Status::Focused { .. } => palette.primary.base.color, + _ => palette.background.neutral.color, }; - button::Style { - background: Some(Background::Color(bg)), - text_color: WHITE, - border: Border { color: BORDER, width: 1.0, radius: 4.0.into() }, - ..Default::default() + text_input::Style { + background: Background::Color(palette.background.base.color), + border: Border { color: border, width: 1.0, radius: 3.0.into() }, + icon: palette.background.base.text, + placeholder: palette.background.base.text.scale_alpha(0.48), + value: palette.background.base.text, + selection: palette.primary.base.color.scale_alpha(0.5), } } @@ -176,8 +160,10 @@ fn hdivider<'a>() -> Element<'a, Message> { container(Space::new().width(Length::Fill).height(1)) .width(Length::Fill) .height(1) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BORDER)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.neutral.color + )), ..Default::default() }) .into() @@ -186,7 +172,7 @@ fn hdivider<'a>() -> Element<'a, Message> { /// One `label : widget` row with a fixed-width label column. fn field_row<'a>(label: &'a str, widget: Element<'a, Message>) -> Element<'a, Message> { row![ - container(text(label).size(12).color(DIM)).width(LABEL_W), + container(text(label).size(12).style(muted_style)).width(LABEL_W), widget, ] .spacing(8) @@ -226,20 +212,28 @@ fn pick_field<'a>( fn tab_button<'a>(label: &'a str, this: AttrTab, active: AttrTab) -> Element<'a, Message> { let is_active = this == active; - button(text(label).size(11).color(WHITE)) + button(text(label).size(11)) .padding([4, 12]) .on_press(Message::AttrEditorTab(this)) - .style(move |_t: &Theme, status| button::Style { - background: Some(Background::Color(match (is_active, status) { - (true, _) => ACTIVE, + .style(move |theme: &Theme, status| { + let palette = theme.extended_palette(); + let pair = match (is_active, status) { + (true, _) => palette.primary.strong, (false, button::Status::Hovered | button::Status::Pressed) => { - Color { r: 0.28, g: 0.28, b: 0.28, a: 1.0 } + palette.background.strong } - _ => Color { r: 0.20, g: 0.20, b: 0.20, a: 1.0 }, - })), - text_color: WHITE, - border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, + _ => palette.background.weak, + }; + button::Style { + background: Some(Background::Color(pair.color)), + text_color: pair.text, + border: Border { + color: palette.background.neutral.color, + width: 1.0, + radius: 3.0.into(), + }, ..Default::default() + } }) .into() } @@ -259,20 +253,22 @@ pub fn view_window<'a>( ) -> Element<'a, Message> { // ── Top toolbar: block name on the left, Apply on the right ─────────── // Mirrors the style-manager windows (actions left, primary action right). - let apply = button(text("Apply").size(11).color(WHITE)) + let apply = button(text("Apply").size(11)) .padding([4, 14]) .on_press(Message::AttrEditorApply) - .style(accent_btn); + .style(button::primary); let toolbar = container( row![ - text(format!("Block: {block}")).size(12).color(DIM), + text(format!("Block: {block}")).size(12).style(muted_style), Space::new().width(Length::Fill), apply, ] .align_y(iced::Center), ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(TB)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.weak.color + )), ..Default::default() }) .width(Length::Fill) @@ -286,7 +282,7 @@ pub fn view_window<'a>( .spacing(2); let body: Element<'_, Message> = if rows.is_empty() { - text("This block has no attributes.").size(13).color(DIM).into() + text("This block has no attributes.").size(13).style(muted_style).into() } else { match tab { AttrTab::Attribute => attribute_tab(rows, selected), @@ -303,8 +299,10 @@ pub fn view_window<'a>( .padding(12); container(column![toolbar, hdivider(), content]) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BG)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.base.color + )), ..Default::default() }) .width(Length::Fill) @@ -315,9 +313,9 @@ pub fn view_window<'a>( /// Attribute tab: tag / prompt / value list with row-select, plus a value box. fn attribute_tab<'a>(rows: &'a [AttrRow], selected: usize) -> Element<'a, Message> { let head = row![ - container(text("Tag").size(11).color(DIM)).width(130), - container(text("Prompt").size(11).color(DIM)).width(Length::Fill), - container(text("Value").size(11).color(DIM)).width(140), + container(text("Tag").size(11).style(muted_style)).width(130), + container(text("Prompt").size(11).style(muted_style)).width(Length::Fill), + container(text("Value").size(11).style(muted_style)).width(140), ] .spacing(6); @@ -325,27 +323,28 @@ fn attribute_tab<'a>(rows: &'a [AttrRow], selected: usize) -> Element<'a, Messag for (idx, r) in rows.iter().enumerate() { let is_sel = idx == selected; let line = row![ - container(text(r.tag.as_str()).size(12).color(WHITE)).width(130), - container(text(r.prompt.as_str()).size(12).color(DIM)).width(Length::Fill), - container(text(r.value.as_str()).size(12).color(WHITE)).width(140), + container(text(r.tag.as_str()).size(12)).width(130), + container(text(r.prompt.as_str()).size(12).style(muted_style)).width(Length::Fill), + container(text(r.value.as_str()).size(12)).width(140), ] .spacing(6); let btn = button(line) .on_press(Message::AttrEditorSelect(idx)) .padding([3, 4]) .width(Length::Fill) - .style(move |_t: &Theme, status| { + .style(move |theme: &Theme, status| { + let palette = theme.extended_palette(); let hovered = matches!(status, button::Status::Hovered); - let bg = if is_sel { - ROW_SEL + let pair = if is_sel { + palette.primary.strong } else if hovered { - Color { r: 0.18, g: 0.18, b: 0.18, a: 1.0 } + palette.background.strong } else { - ROW_BG + palette.background.weak }; button::Style { - background: Some(Background::Color(bg)), - text_color: WHITE, + background: Some(Background::Color(pair.color)), + text_color: pair.text, border: Border::default(), ..Default::default() } diff --git a/src/ui/window/layers.rs b/src/ui/window/layers.rs index 1fcdc27b..7f45db66 100644 --- a/src/ui/window/layers.rs +++ b/src/ui/window/layers.rs @@ -47,6 +47,35 @@ const COMBO_PAD_V: f32 = (ROW_H - FONT_SZ * 1.3 - 2.0) / 2.0; /// scrolled into view after it is added (#271). pub const LAYER_TABLE_SCROLL_ID: &str = "layer-manager-table-scroll"; +fn muted_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)), + } +} + +fn table_input_style( + theme: &Theme, + status: iced::widget::text_input::Status, +) -> iced::widget::text_input::Style { + let palette = theme.extended_palette(); + let border = match status { + iced::widget::text_input::Status::Focused { .. } => palette.primary.base.color, + _ => palette.background.neutral.color, + }; + iced::widget::text_input::Style { + background: Background::Color(palette.background.base.color), + border: Border { + color: border, + width: 1.0, + radius: 2.0.into(), + }, + icon: palette.background.base.text, + placeholder: palette.background.base.text.scale_alpha(0.48), + value: palette.background.base.text, + selection: palette.primary.base.color.scale_alpha(0.5), + } +} + // ── Layer data ──────────────────────────────────────────────────────────── #[derive(Clone, Debug)] @@ -313,39 +342,15 @@ impl LayerPanel { .size(FONT_SZ) .padding([3, 6]) .width(Length::Fixed(180.0)) - .style(|_: &Theme, _| iced::widget::text_input::Style { - background: Background::Color(Color { - r: 0.10, - g: 0.10, - b: 0.10, - a: 1.0, - }), - border: Border { - color: BORDER_COLOR, - width: 1.0, - radius: 2.0.into(), - }, - icon: Color::WHITE, - placeholder: Color { - r: 0.45, - g: 0.45, - b: 0.45, - a: 1.0, - }, - value: Color::WHITE, - selection: Color { - r: 0.20, - g: 0.44, - b: 0.72, - a: 0.5, - }, - }), + .style(table_input_style), ] .spacing(2) .align_y(iced::Center), ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(TOOLBAR_BG)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.weak.color + )), ..Default::default() }) .width(Fill) @@ -355,13 +360,15 @@ impl LayerPanel { let sc = self.sort_col; let sa = self.sort_asc; let mut header_row = row![ - text("Status").size(10).color(DIM).width(50), + text("Status").size(10).style(muted_style).width(50), sortable_header("Name", LayerSortCol::Name, Length::Fixed(name_col_w), sc, sa), // Draggable divider: adjusts the Name column width (#359). iced::widget::mouse_area( container(iced::widget::Space::new().width(2).height(14)).style( - |_: &Theme| container::Style { - background: Some(Background::Color(BORDER_COLOR)), + |theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.neutral.color + )), ..Default::default() }, ), @@ -390,20 +397,23 @@ impl LayerPanel { header_row = header_row.push( text(vp.label.as_str()) .size(10) - .color(DIM) + .style(muted_style) .width(Length::Fixed(COL_ICON)), ); } let col_header = container(header_row) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(COL_HEADER_BG)), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.weak.color)), border: Border { - color: BORDER_COLOR, + color: palette.background.neutral.color, width: 1.0, radius: 0.0.into(), }, ..Default::default() + } }) .padding([4, 8]) .width(Fill); @@ -451,8 +461,10 @@ impl LayerPanel { // ── Full-window frame ───────────────────────────────────────────── container(column![toolbar, col_header, table].spacing(0)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.base.color + )), ..Default::default() }) .width(Fill) @@ -478,28 +490,26 @@ fn sortable_header<'a>( active: Option, asc: bool, ) -> Element<'a, Message> { - let mut content = row![text(label).size(10).color(DIM)] + let mut content = row![text(label).size(10).style(muted_style)] .spacing(3) .align_y(iced::Center); if active == Some(col) { content = content.push(if asc { - crate::ui::icons::arrow_up(8.0, DIM) + crate::ui::icons::themed_arrow_up(8.0) } else { - crate::ui::icons::arrow_down(8.0, DIM) + crate::ui::icons::themed_arrow_down(8.0) }); } button(content) .on_press(Message::LayerSort(col)) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => Color { - r: 0.30, - g: 0.30, - b: 0.30, - a: 1.0, - }, - _ => Color::TRANSPARENT, - })), + .style(|theme: &Theme, status| button::Style { + background: matches!( + status, + button::Status::Hovered | button::Status::Pressed + ) + .then_some(Background::Color( + theme.extended_palette().background.strong.color + )), ..Default::default() }) .padding(Padding { @@ -517,41 +527,31 @@ fn sortable_header<'a>( fn toolbar_btn<'a>(icon: &'static [u8], label: &'a str, msg: Message) -> Element<'a, Message> { button( row![ - crate::ui::icons::tinted(icon, 12.0, Color::WHITE), - text(label).size(11).color(Color::WHITE), + crate::ui::icons::themed(icon, 12.0), + text(label).size(11), ] .spacing(5) .align_y(iced::Center), ) .on_press(msg) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered => Color { - r: 0.32, - g: 0.32, - b: 0.32, - a: 1.0, - }, - button::Status::Pressed => Color { - r: 0.25, - g: 0.25, - b: 0.25, - a: 1.0, - }, - _ => Color { - r: 0.26, - g: 0.26, - b: 0.26, - a: 1.0, - }, - })), + .style(|theme: &Theme, status| { + let palette = theme.extended_palette(); + let pair = match status { + button::Status::Hovered | button::Status::Pressed => { + palette.background.strong + } + _ => palette.background.weak, + }; + button::Style { + background: Some(Background::Color(pair.color)), border: Border { radius: 3.0.into(), - color: BORDER_COLOR, + color: palette.background.neutral.color, width: 1.0, }, - text_color: Color::WHITE, + text_color: pair.text, ..Default::default() + } }) .padding([4, 10]) .into() @@ -563,46 +563,46 @@ fn toolbar_btn_cond<'a>( msg: Message, enabled: bool, ) -> Element<'a, Message> { - let fg = if enabled { - Color::WHITE - } else { - Color { - r: 0.45, - g: 0.45, - b: 0.45, - a: 1.0, - } - }; let mut b = button( row![ - crate::ui::icons::tinted(icon, 12.0, fg), - text(label).size(11).color(fg), + if enabled { + crate::ui::icons::themed(icon, 12.0) + } else { + crate::ui::icons::themed_disabled(icon, 12.0) + }, + if enabled { + text(label).size(11) + } else { + text(label).size(11).style(|theme: &Theme| iced::widget::text::Style { + color: Some( + theme.extended_palette().background.base.text.scale_alpha(0.42) + ), + }) + }, ] .spacing(5) .align_y(iced::Center), ) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered => Color { - r: 0.32, - g: 0.32, - b: 0.32, - a: 1.0, - }, - _ => Color { - r: 0.26, - g: 0.26, - b: 0.26, - a: 1.0, - }, - })), + .style(move |theme: &Theme, status| { + let palette = theme.extended_palette(); + let pair = match status { + button::Status::Hovered if enabled => palette.background.strong, + _ => palette.background.weak, + }; + button::Style { + background: Some(Background::Color(pair.color)), border: Border { radius: 3.0.into(), - color: BORDER_COLOR, + color: palette.background.neutral.color, width: 1.0, }, - text_color: Color::WHITE, + text_color: if enabled { + pair.text + } else { + pair.text.scale_alpha(0.42) + }, ..Default::default() + } }) .padding([4, 10]); if enabled { @@ -616,21 +616,25 @@ fn toolbar_btn_cond<'a>( #[allow(clippy::too_many_arguments)] /// Hover popup showing a layer's full name when the cell truncates it. fn name_tip<'a>(name: &'a str) -> Element<'a, Message> { - container(text(name).size(FONT_SZ).color(ROW_TEXT)) + container(text(name).size(FONT_SZ)) .padding(Padding { top: 3.0, bottom: 3.0, left: 7.0, right: 7.0, }) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.strong.color)), border: Border { - color: BORDER_COLOR, + color: palette.background.neutral.color, width: 1.0, radius: 3.0.into(), }, + text_color: Some(palette.background.strong.text), ..Default::default() + } }) .into() } @@ -655,16 +659,10 @@ fn layer_row<'a>( .height(ICON_SZ), ) .on_press(on_press) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered => Color { - r: 0.35, - g: 0.35, - b: 0.35, - a: 1.0, - }, - _ => Color::TRANSPARENT, - })), + .style(|theme: &Theme, status| button::Style { + background: matches!(status, button::Status::Hovered).then_some( + Background::Color(theme.extended_palette().background.strong.color) + ), ..Default::default() }) .padding(Padding { @@ -682,27 +680,9 @@ fn layer_row<'a>( let lck_svg = crate::ui::icons::layer_lock(layer.locked); let status_dot: Element<'_, Message> = if is_current { - crate::ui::icons::tinted( - crate::ui::icons::CHECK, - 13.0, - Color { - r: 0.25, - g: 0.85, - b: 0.45, - a: 1.0, - }, - ) + crate::ui::icons::themed_success(crate::ui::icons::CHECK, 13.0) } else { - crate::ui::icons::tinted( - crate::ui::icons::DOT, - 9.0, - Color { - r: 0.55, - g: 0.55, - b: 0.55, - a: 1.0, - }, - ) + crate::ui::icons::themed_secondary(crate::ui::icons::DOT, 9.0) }; // Name cell @@ -717,38 +697,7 @@ fn layer_row<'a>( left: 4.0, right: 4.0, }) - .style(|_: &Theme, _| iced::widget::text_input::Style { - background: iced::Background::Color(Color { - r: 0.12, - g: 0.12, - b: 0.12, - a: 1.0, - }), - border: Border { - radius: 2.0.into(), - width: 1.0, - color: Color { - r: 0.45, - g: 0.65, - b: 0.90, - a: 1.0, - }, - }, - icon: Color::WHITE, - placeholder: Color { - r: 0.4, - g: 0.4, - b: 0.4, - a: 1.0, - }, - value: Color::WHITE, - selection: Color { - r: 0.25, - g: 0.45, - b: 0.75, - a: 0.5, - }, - }) + .style(table_input_style) .width(Length::Fixed(name_col_w)) .into() } else { @@ -756,20 +705,13 @@ fn layer_row<'a>( let name_budget = ((name_col_w / 6.0) as usize).max(8); let name_btn = button( text(crate::ui::text_util::elide(&layer.name, name_budget)) - .size(FONT_SZ) - .color(ROW_TEXT), + .size(FONT_SZ), ) .on_press(Message::LayerRenameStart(index)) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered => Color { - r: 0.30, - g: 0.30, - b: 0.30, - a: 1.0, - }, - _ => Color::TRANSPARENT, - })), + .style(|theme: &Theme, status| button::Style { + background: matches!(status, button::Status::Hovered).then_some( + Background::Color(theme.extended_palette().background.strong.color) + ), ..Default::default() }) .padding(Padding { @@ -837,7 +779,7 @@ fn layer_row<'a>( } else { text(layer.linetype.as_str()) .size(FONT_SZ) - .color(DIM) + .style(muted_style) .width(Length::Fixed(COL_LT)) .into() }; @@ -861,7 +803,7 @@ fn layer_row<'a>( } else { text(cur_lw_item.to_string()) .size(FONT_SZ) - .color(DIM) + .style(muted_style) .width(Length::Fixed(COL_LW)) .into() }; @@ -877,33 +819,13 @@ fn layer_row<'a>( left: 4.0, right: 4.0, }) - .style(|_: &Theme, _| iced::widget::text_input::Style { - background: iced::Background::Color(Color::TRANSPARENT), - border: Border { - radius: 2.0.into(), - width: 1.0, - color: BORDER_COLOR, - }, - icon: Color::WHITE, - placeholder: DIM, - value: ROW_TEXT, - selection: Color { - r: 0.25, - g: 0.45, - b: 0.75, - a: 0.5, - }, + .style(|theme: &Theme, status| { + let mut style = table_input_style(theme, status); + style.background = iced::Background::Color(Color::TRANSPARENT); + style }) .width(Length::Fixed(COL_TRANS)); - let bg = if is_selected { - ROW_SEL - } else if index % 2 == 0 { - ROW_EVEN - } else { - ROW_ODD - }; - let mut row_content = row![ container(status_dot) .width(50) @@ -945,9 +867,20 @@ fn layer_row<'a>( mouse_area( container(row_content) - .style(move |_: &Theme| container::Style { - background: Some(Background::Color(bg)), + .style(move |theme: &Theme| { + let palette = theme.extended_palette(); + let pair = if is_selected { + palette.primary.weak + } else if index % 2 == 0 { + palette.background.base + } else { + palette.background.weak + }; + container::Style { + background: Some(Background::Color(pair.color)), + text_color: Some(pair.text), ..Default::default() + } }) .padding(Padding { top: 0.0, @@ -965,31 +898,10 @@ fn layer_row<'a>( // ── Combo style ──────────────────────────────────────────────────────────── fn combo_input_style( - _theme: &Theme, - _status: iced::widget::text_input::Status, + theme: &Theme, + status: iced::widget::text_input::Status, ) -> iced::widget::text_input::Style { - iced::widget::text_input::Style { - background: iced::Background::Color(Color { - r: 0.13, - g: 0.13, - b: 0.13, - a: 1.0, - }), - border: Border { - radius: 2.0.into(), - width: 1.0, - color: BORDER_COLOR, - }, - icon: Color::WHITE, - placeholder: DIM, - value: Color::WHITE, - selection: Color { - r: 0.25, - g: 0.45, - b: 0.75, - a: 0.5, - }, - } + table_input_style(theme, status) } // ── Display helpers ─────────────────────────────────────────────────────── @@ -1054,67 +966,3 @@ const COL_COLOR: f32 = 90.0; const COL_LT: f32 = 110.0; const COL_LW: f32 = 90.0; const COL_TRANS: f32 = 80.0; - -// ── Colors ──────────────────────────────────────────────────────────────── - -const PANEL_BG: Color = Color { - r: 0.18, - g: 0.18, - b: 0.18, - a: 1.0, -}; -const TOOLBAR_BG: Color = Color { - r: 0.20, - g: 0.20, - b: 0.20, - a: 1.0, -}; -const COL_HEADER_BG: Color = Color { - r: 0.21, - g: 0.21, - b: 0.21, - a: 1.0, -}; -const ROW_EVEN: Color = Color { - r: 0.18, - g: 0.18, - b: 0.18, - a: 1.0, -}; -const ROW_ODD: Color = Color { - r: 0.21, - g: 0.21, - b: 0.21, - a: 1.0, -}; -const ROW_SEL: Color = Color { - r: 0.18, - g: 0.32, - b: 0.52, - a: 1.0, -}; -const ROW_TEXT: Color = Color { - r: 0.85, - g: 0.85, - b: 0.85, - a: 1.0, -}; -const DIM: Color = Color { - r: 0.50, - g: 0.50, - b: 0.50, - a: 1.0, -}; -const BORDER_COLOR: Color = Color { - r: 0.30, - g: 0.30, - b: 0.30, - a: 1.0, -}; -#[allow(dead_code)] -const ICON_COLOR: Color = Color { - r: 0.80, - g: 0.80, - b: 0.80, - a: 1.0, -}; diff --git a/src/ui/window/layout_manager.rs b/src/ui/window/layout_manager.rs index 1c2316aa..28a616dc 100644 --- a/src/ui/window/layout_manager.rs +++ b/src/ui/window/layout_manager.rs @@ -2,131 +2,37 @@ use crate::app::Message; use iced::widget::{button, column, container, row, scrollable, text, text_input, Space}; -use iced::{Background, Border, Color, Element, Fill, Theme}; +use iced::{Background, Element, Fill, Theme}; -const TB: Color = Color { - r: 0.13, - g: 0.13, - b: 0.13, - a: 1.0, -}; -const BG: Color = Color { - r: 0.15, - g: 0.15, - b: 0.15, - a: 1.0, -}; -const BORDER: Color = Color { - r: 0.35, - g: 0.35, - b: 0.35, - a: 1.0, -}; -const TEXT: Color = Color { - r: 0.88, - g: 0.88, - b: 0.88, - a: 1.0, -}; -const DIM: Color = Color { - r: 0.55, - g: 0.55, - b: 0.55, - a: 1.0, -}; -const ACCENT: Color = Color { - r: 0.25, - g: 0.50, - b: 0.85, - a: 1.0, -}; -const ACTIVE: Color = Color { - r: 0.20, - g: 0.40, - b: 0.70, - a: 1.0, -}; -const FIELD: Color = Color { - r: 0.10, - g: 0.10, - b: 0.10, - a: 1.0, -}; -const LIST: Color = Color { - r: 0.12, - g: 0.12, - b: 0.12, - a: 1.0, -}; -const WARN: Color = Color { - r: 0.80, - g: 0.35, - b: 0.25, - a: 1.0, -}; +fn muted_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)), + } +} + +fn primary_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().primary.base.color), + } +} fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style { - move |_: &Theme, st| button::Style { - background: Some(Background::Color(match (accent, st) { - (true, button::Status::Hovered | button::Status::Pressed) => Color { - r: 0.20, - g: 0.42, - b: 0.72, - a: 1.0, - }, - (false, button::Status::Hovered | button::Status::Pressed) => Color { - r: 0.28, - g: 0.28, - b: 0.28, - a: 1.0, - }, - (true, _) => ACCENT, - _ => Color { - r: 0.22, - g: 0.22, - b: 0.22, - a: 1.0, - }, - })), - text_color: TEXT, - border: Border { - color: BORDER, - width: 1.0, - radius: 4.0.into(), - }, - ..Default::default() + move |theme: &Theme, status| { + if accent { + button::primary(theme, status) + } else { + button::secondary(theme, status) + } } } fn list_item(active: bool) -> impl Fn(&Theme, button::Status) -> button::Style { - move |_: &Theme, st| button::Style { - background: Some(Background::Color(match (active, st) { - (true, _) => ACTIVE, - (false, button::Status::Hovered | button::Status::Pressed) => Color { - r: 0.26, - g: 0.26, - b: 0.26, - a: 1.0, - }, - _ => Color::TRANSPARENT, - })), - text_color: TEXT, - ..Default::default() - } -} - -fn field_style(_: &Theme, _: text_input::Status) -> text_input::Style { - text_input::Style { - background: Background::Color(FIELD), - border: Border { - color: BORDER, - width: 1.0, - radius: 3.0.into(), - }, - icon: TEXT, - placeholder: DIM, - value: TEXT, - selection: ACCENT, + move |theme: &Theme, status| { + if active { + button::primary(theme, status) + } else { + button::subtle(theme, status) + } } } @@ -134,8 +40,10 @@ fn hdivider<'a>() -> Element<'a, Message> { container(Space::new().width(Fill).height(1)) .width(Fill) .height(1) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BORDER)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.neutral.color, + )), ..Default::default() }) .into() @@ -145,8 +53,10 @@ fn vsep<'a>() -> Element<'a, Message> { container(Space::new().width(1).height(Fill)) .width(1) .height(Fill) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BORDER)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.neutral.color, + )), ..Default::default() }) .into() @@ -169,34 +79,18 @@ pub fn view_window<'a>( .padding([4, 10]), button(text("Delete").size(11)) .on_press(Message::LayoutManagerDelete) - .style(move |_: &Theme, st| button::Style { - background: Some(Background::Color(match st { - button::Status::Hovered | button::Status::Pressed => Color { - r: 0.60, - g: 0.20, - b: 0.18, - a: 1.0 - }, - _ => Color { - r: 0.22, - g: 0.22, - b: 0.22, - a: 1.0 - }, - })), - text_color: if is_model { DIM } else { WARN }, - border: Border { - color: BORDER, - width: 1.0, - radius: 4.0.into() - }, - ..Default::default() + .style(move |theme: &Theme, status| { + if is_model { + button::secondary(theme, status) + } else { + button::danger(theme, status) + } }) .padding([4, 10]), Space::new().width(Fill), button( row![ - crate::ui::icons::tinted(crate::ui::icons::TRI_LEFT_B, 9.0, Color::WHITE), + crate::ui::icons::themed_arrow_left(9.0), text("Move Left").size(11), ] .spacing(4) @@ -208,7 +102,7 @@ pub fn view_window<'a>( button( row![ text("Move Right").size(11), - crate::ui::icons::arrow_right(9.0, Color::WHITE), + crate::ui::icons::themed_arrow_right(9.0), ] .spacing(4) .align_y(iced::Center), @@ -224,8 +118,10 @@ pub fn view_window<'a>( .spacing(4) .align_y(iced::Center), ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(TB)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.weakest.color, + )), ..Default::default() }) .width(Fill) @@ -241,11 +137,7 @@ pub fn view_window<'a>( .spacing(5) .align_y(iced::Center); if is_cur { - item_row = item_row.push(crate::ui::icons::tinted( - crate::ui::icons::TRI_LEFT_B, - 8.0, - Color::WHITE, - )); + item_row = item_row.push(crate::ui::icons::themed_arrow_left(8.0)); } button(item_row) .on_press(Message::LayoutManagerSelect(name.clone())) @@ -258,17 +150,9 @@ pub fn view_window<'a>( let layout_list = container( column![ - text("Layouts").size(10).color(DIM), + text("Layouts").size(10).style(muted_style), container(scrollable(column(list_items).spacing(2)).height(Fill)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(LIST)), - border: Border { - color: BORDER, - width: 1.0, - radius: 3.0.into() - }, - ..Default::default() - }) + .style(container::bordered_box) .width(Fill) .height(Fill) .padding(2), @@ -293,38 +177,38 @@ pub fn view_window<'a>( } else { "Paper Space Layout" }) - .size(13) - .color(TEXT), + .size(13), Space::new().height(8), row![ - text("Name:").size(11).color(DIM).width(80), + text("Name:").size(11).style(muted_style).width(80), text(selected).size(11), ] .spacing(8) .align_y(iced::Center), row![ - text("Status:").size(11).color(DIM).width(80), + text("Status:").size(11).style(muted_style).width(80), text(if selected == current.as_str() { "Active" } else { "Inactive" }) .size(11) - .color(if selected == current.as_str() { - ACCENT - } else { - DIM + .style(move |theme: &Theme| { + if selected == current.as_str() { + primary_style(theme) + } else { + muted_style(theme) + } }), ] .spacing(8) .align_y(iced::Center), Space::new().height(16), - text("Rename").size(10).color(DIM), + text("Rename").size(10).style(muted_style), row![ text_input("New name…", rename_buf) .on_input(Message::LayoutManagerRenameBuf) .on_submit(Message::LayoutManagerRenameCommit) - .style(field_style) .size(11) .padding([4, 8]), button(text("OK").size(11)) @@ -343,8 +227,10 @@ pub fn view_window<'a>( let body = row![layout_list, vsep(), details].height(Fill); container(column![toolbar, hdivider(), body].spacing(0)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BG)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.base.color, + )), ..Default::default() }) .width(Fill) diff --git a/src/ui/window/open_progress.rs b/src/ui/window/open_progress.rs index 9bcf36aa..661687ea 100644 --- a/src/ui/window/open_progress.rs +++ b/src/ui/window/open_progress.rs @@ -5,7 +5,7 @@ use iced::time::Instant; use iced::widget::{button, column, container, row, stack, text, Space}; -use iced::{Background, Border, Color, Element, Fill, Length, Theme}; +use iced::{Background, Border, Element, Fill, Length, Theme}; use std::sync::atomic::Ordering; use crate::app::{ @@ -60,13 +60,10 @@ pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Messag .width(Length::Fixed(fill_width)) .height(Length::Fixed(BAR_TRACK_HEIGHT)), ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { - r: 0.30, - g: 0.62, - b: 0.95, - a: 1.0, - })), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().primary.base.color + )), border: Border { radius: 3.0.into(), ..Default::default() @@ -86,13 +83,10 @@ pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Messag let bar_track: Element<'_, Message> = container( stack![ container(Space::new().width(Length::Fixed(BAR_TRACK_WIDTH)).height(Length::Fixed(BAR_TRACK_HEIGHT))) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { - r: 0.18, - g: 0.18, - b: 0.18, - a: 1.0, - })), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.strong.color + )), border: Border { radius: 3.0.into(), ..Default::default() @@ -107,9 +101,7 @@ pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Messag .into(); // ── Card body ──────────────────────────────────────────────────────── - let title = text("Opening file") - .size(15) - .color(Color::WHITE); + let title = text("Opening file").size(15); let name_line = text(format!( "{} ({})", @@ -117,11 +109,8 @@ pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Messag format_size(progress.size_bytes) )) .size(13) - .color(Color { - r: 0.82, - g: 0.82, - b: 0.82, - a: 1.0, + .style(|theme: &Theme| iced::widget::text::Style { + color: Some(theme.extended_palette().background.base.text.scale_alpha(0.82)), }); let phase_line = text(format!( @@ -130,52 +119,13 @@ pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Messag basis_points as f32 / 100.0 )) .size(12) - .color(Color { - r: 0.70, - g: 0.80, - b: 0.95, - a: 1.0, + .style(|theme: &Theme| iced::widget::text::Style { + color: Some(theme.extended_palette().primary.base.color), }); - let cancel_btn: Element<'_, Message> = button(text("Cancel").size(12).color(Color::WHITE)) + let cancel_btn: Element<'_, Message> = button(text("Cancel").size(12)) .on_press(Message::OpenCancel) - .style(|_: &Theme, status| { - let bg = match status { - button::Status::Hovered => Color { - r: 0.32, - g: 0.32, - b: 0.32, - a: 1.0, - }, - button::Status::Pressed => Color { - r: 0.42, - g: 0.18, - b: 0.18, - a: 1.0, - }, - _ => Color { - r: 0.22, - g: 0.22, - b: 0.22, - a: 1.0, - }, - }; - button::Style { - background: Some(Background::Color(bg)), - border: Border { - color: Color { - r: 0.40, - g: 0.40, - b: 0.40, - a: 1.0, - }, - width: 1.0, - radius: 3.0.into(), - }, - text_color: Color::WHITE, - ..Default::default() - } - }) + .style(button::danger) .padding([4, 14]) .into(); @@ -187,35 +137,25 @@ pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Messag .width(Length::Fixed(CARD_WIDTH)), ) .padding([18, 22]) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { - r: 0.13, - g: 0.13, - b: 0.13, - a: 0.98, - })), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.weak.color)), border: Border { - color: Color { - r: 0.45, - g: 0.45, - b: 0.45, - a: 1.0, - }, + color: palette.background.neutral.color, width: 1.0, radius: 6.0.into(), }, ..Default::default() + } }); // ── Backdrop (click-blocker + dim) ──────────────────────────────────── let backdrop: Element<'_, Message> = container(Space::new().width(Fill).height(Fill)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { - r: 0.0, - g: 0.0, - b: 0.0, - a: 0.55, - })), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.strong.color.scale_alpha(0.72) + )), ..Default::default() }) .width(Fill) diff --git a/src/ui/window/options.rs b/src/ui/window/options.rs index 1e68316d..15913622 100644 --- a/src/ui/window/options.rs +++ b/src/ui/window/options.rs @@ -1,96 +1,123 @@ +use crate::app::config::UiThemeConfig; use crate::app::Message; -use iced::widget::{button, column, container, pick_list, row, text, Space}; -use iced::{Background, Border, Color, Element, Fill, Theme}; +use iced::widget::{ + button, column, container, pick_list, row, scrollable, text, text_input, Space, +}; +use iced::{Background, Border, Element, Fill, Theme}; -pub fn view_window<'a>(default_save_format: &'a str) -> Element<'a, Message> { - const BG: Color = Color { - r: 0.15, - g: 0.15, - b: 0.17, - a: 1.0, - }; - const BORDER: Color = Color { - r: 0.32, - g: 0.32, - b: 0.36, - a: 1.0, - }; - const TEXT: Color = Color { - r: 0.90, - g: 0.90, - b: 0.90, - a: 1.0, - }; - const DIM: Color = Color { - r: 0.60, - g: 0.60, - b: 0.64, - a: 1.0, - }; - - let selected = crate::io::SAVE_FORMAT_OPTIONS +pub fn view_window<'a>( + default_save_format: &'a str, + ui_theme: &'a UiThemeConfig, + theme_color_inputs: &'a [String; 6], +) -> Element<'a, Message> { + let selected_format = crate::io::SAVE_FORMAT_OPTIONS .iter() .copied() .find(|candidate| *candidate == default_save_format); - let close = button(text("Close").size(12).color(TEXT)) - .on_press(Message::CloseModal) - .padding([5, 16]) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => Color { - r: 0.34, - g: 0.34, - b: 0.38, - a: 1.0, - }, - _ => Color { - r: 0.26, - g: 0.26, - b: 0.29, - a: 1.0, - }, - })), - text_color: TEXT, - border: Border { - color: BORDER, - width: 1.0, - radius: 4.0.into(), - }, - ..Default::default() - }); + let theme_options = Theme::ALL + .iter() + .map(ToString::to_string) + .chain(std::iter::once("Custom".to_string())) + .collect::>(); + let selected_theme = Some(ui_theme.name.clone()); - let body = column![ - text("Open and Save").size(15).color(TEXT), - Space::new().height(16), + let palette = ui_theme.palette.to_iced(); + let colors = [ + ("Background", palette.background), + ("Text", palette.text), + ("Primary", palette.primary), + ("Success", palette.success), + ("Warning", palette.warning), + ("Danger", palette.danger), + ]; + + let mut color_controls = column![].spacing(8); + for (index, (label, color)) in colors.into_iter().enumerate() { + let swatch = container(Space::new()) + .width(28) + .height(22) + .style(move |theme: &Theme| container::Style { + background: Some(Background::Color(color)), + border: Border { + color: theme.extended_palette().background.strong.color, + width: 1.0, + radius: 3.0.into(), + }, + ..Default::default() + }); + color_controls = color_controls.push( + row![ + text(label).size(12).width(110), + swatch, + text_input("#RRGGBB", theme_color_inputs[index].as_str()) + .on_input(move |value| Message::OptionsThemeColorChanged(index, value)) + .width(130), + ] + .spacing(10) + .align_y(iced::Center), + ); + } + + let close = button(text("Close").size(12)) + .on_press(Message::CloseModal) + .padding([6, 18]) + .style(button::secondary); + + let content = column![ + text("Open and Save").size(15), + Space::new().height(10), row![ - text("Default save format:").size(12).color(TEXT).width(150), - pick_list(crate::io::SAVE_FORMAT_OPTIONS, selected, |format: &str| { - Message::DefaultSaveFormatChanged(format.to_string()) - }) + text("Default save format:").size(12).width(150), + pick_list( + crate::io::SAVE_FORMAT_OPTIONS, + selected_format, + |format: &str| Message::DefaultSaveFormatChanged(format.to_string()) + ) .width(Fill), ] .spacing(12) .align_y(iced::Center), - Space::new().height(10), + Space::new().height(8), text( - "Used when a new drawing is saved for the first time. Existing drawings keep their current file type and version." + "Used for the first save of a new drawing. Existing drawings keep their file type and version." ) - .size(11) - .color(DIM) - .width(Fill), - Space::new().height(Fill), - row![Space::new().width(Fill), close], + .size(11), + Space::new().height(22), + text("Theme").size(15), + Space::new().height(10), + row![ + text("Iced theme:").size(12).width(150), + pick_list( + theme_options, + selected_theme, + Message::OptionsThemeChanged, + ) + .width(Fill), + ] + .spacing(12) + .align_y(iced::Center), + Space::new().height(8), + text( + "Changing a base colour switches to Custom. Iced generates every component shade from these six colours." + ) + .size(11), + Space::new().height(12), + color_controls, ] .spacing(0) + .width(Fill); + + let body = column![ + scrollable(content).height(Fill), + Space::new().height(12), + row![Space::new().width(Fill), close], + ] .width(Fill) .height(Fill); container(body) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BG)), - ..Default::default() - }) + .style(container::rounded_box) .padding([16, 18]) .width(Fill) .height(Fill) diff --git a/src/ui/window/plot.rs b/src/ui/window/plot.rs index cf0d858c..133fa54a 100644 --- a/src/ui/window/plot.rs +++ b/src/ui/window/plot.rs @@ -10,17 +10,7 @@ use iced::widget::{ button, checkbox, column, container, mouse_area, pick_list, row, scrollable, text, text_input, Space, }; -use iced::{Background, Border, Color, Element, Fill, Length, Theme}; - -const TB: Color = Color { r: 0.13, g: 0.13, b: 0.13, a: 1.0 }; -const BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 }; -const BORDER: Color = Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 }; -const TEXT: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 }; -const DIM: Color = Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 }; -const ACCENT: Color = Color { r: 0.25, g: 0.50, b: 0.85, a: 1.0 }; -const FIELD: Color = Color { r: 0.10, g: 0.10, b: 0.10, a: 1.0 }; -const ACTIVE: Color = Color { r: 0.20, g: 0.40, b: 0.70, a: 1.0 }; -const LIST: Color = Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 }; +use iced::{Background, Border, Element, Fill, Length, Theme}; /// Sentinel entries in the printer dropdown (not real printer names). pub const OUT_DEFAULT: &str = "System default printer"; @@ -221,32 +211,49 @@ impl PlotDialogState { } fn btn(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style { - move |_: &Theme, st| button::Style { - background: Some(Background::Color(match (accent, st) { - (true, button::Status::Hovered | button::Status::Pressed) => { - Color { r: 0.20, g: 0.42, b: 0.72, a: 1.0 } - } + move |theme: &Theme, st| { + let palette = theme.extended_palette(); + let pair = match (accent, st) { + (true, button::Status::Hovered | button::Status::Pressed) => palette.primary.strong, (false, button::Status::Hovered | button::Status::Pressed) => { - Color { r: 0.28, g: 0.28, b: 0.28, a: 1.0 } + palette.background.strong } - (true, _) => ACCENT, - _ => Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 }, - })), - text_color: TEXT, - border: Border { color: BORDER, width: 1.0, radius: 4.0.into() }, + (true, _) => palette.primary.base, + _ => palette.background.weak, + }; + button::Style { + background: Some(Background::Color(pair.color)), + text_color: pair.text, + border: Border { + color: palette.background.neutral.color, + width: 1.0, + radius: 4.0.into(), + }, shadow: iced::Shadow::default(), snap: false, + } } } -fn field_style(_: &Theme, _: text_input::Status) -> text_input::Style { +fn field_style(theme: &Theme, status: text_input::Status) -> text_input::Style { + let palette = theme.extended_palette(); + let border = match status { + text_input::Status::Focused { .. } => palette.primary.base.color, + _ => palette.background.neutral.color, + }; text_input::Style { - background: Background::Color(FIELD), - border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, - icon: TEXT, - placeholder: DIM, - value: TEXT, - selection: ACCENT, + background: Background::Color(palette.background.base.color), + border: Border { color: border, width: 1.0, radius: 3.0.into() }, + icon: palette.background.base.text, + placeholder: palette.background.base.text.scale_alpha(0.48), + value: palette.background.base.text, + selection: palette.primary.base.color.scale_alpha(0.5), + } +} + +fn muted_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)), } } @@ -254,23 +261,27 @@ fn hdivider<'a>() -> Element<'a, Message> { container(Space::new().width(Fill).height(1)) .width(Fill) .height(1) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BORDER)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.neutral.color + )), ..Default::default() }) .into() } fn section_label<'a>(s: &'static str) -> Element<'a, Message> { - text(s).size(11).color(DIM).into() + text(s).size(11).style(muted_style).into() } fn vsep<'a>() -> Element<'a, Message> { container(Space::new().width(1).height(Fill)) .width(1) .height(Fill) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BORDER)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.neutral.color + )), ..Default::default() }) .into() @@ -295,13 +306,16 @@ fn setup_row<'a>( .into(); } let is_sel = name == selected; - let cell = container(text(name.to_string()).size(11).color(TEXT)) + let cell = container(text(name.to_string()).size(11)) .padding([4, 8]) .width(Fill) - .style(move |_: &Theme| container::Style { - background: is_sel.then_some(Background::Color(ACTIVE)), - text_color: Some(TEXT), + .style(move |theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: is_sel.then_some(Background::Color(palette.primary.strong.color)), + text_color: is_sel.then_some(palette.primary.strong.text), ..Default::default() + } }); mouse_area(cell) .on_press(Message::PlotDlg(PlotDlgMsg::SelectSetup(name.to_string()))) @@ -321,7 +335,7 @@ fn drop_row<'a>( .text_size(12) .padding([3, 6]) .width(Length::Fill); - row![text(label).size(11).color(DIM).width(92), pl] + row![text(label).size(11).style(muted_style).width(92), pl] .spacing(8) .align_y(iced::Center) .into() @@ -335,7 +349,7 @@ fn field_row<'a>( width: u16, ) -> Element<'a, Message> { row![ - text(label).size(11).color(DIM).width(92), + text(label).size(11).style(muted_style).width(92), text_input("", value) .on_input(move |s| Message::PlotDlg(ctor(s))) .style(field_style) @@ -413,8 +427,10 @@ pub fn view_window(s: &PlotDialogState) -> Element<'_, Message> { ] .align_y(iced::Center), ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(TB)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.weak.color + )), ..Default::default() }) .width(Fill) @@ -445,7 +461,7 @@ pub fn view_window(s: &PlotDialogState) -> Element<'_, Message> { .map(|name| setup_row(name, &s.selected_setup, renaming, rename_buf)) .collect(); let list_body: Element<'_, Message> = if rows.is_empty() { - container(text("(no page setups)").size(11).color(DIM)) + container(text("(no page setups)").size(11).style(muted_style)) .padding([6, 8]) .into() } else { @@ -453,16 +469,19 @@ pub fn view_window(s: &PlotDialogState) -> Element<'_, Message> { }; let list_panel = container( column![ - text("Page setups").size(10).color(DIM), + text("Page setups").size(10).style(muted_style), container(list_body) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(LIST)), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.weak.color)), border: Border { - color: BORDER, + color: palette.background.neutral.color, width: 1.0, radius: 3.0.into(), }, ..Default::default() + } }) .width(Fill) .height(Fill) @@ -550,11 +569,18 @@ pub fn view_window(s: &PlotDialogState) -> Element<'_, Message> { hdivider(), section_label("Plot style table (pen assignments)"), row![ - container(text(style_label).size(12).color(TEXT)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(FIELD)), - border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, + container(text(style_label).size(12)) + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(Background::Color(palette.background.base.color)), + border: Border { + color: palette.background.neutral.color, + width: 1.0, + radius: 3.0.into(), + }, ..Default::default() + } }) .padding([4, 8]) .width(Fill), @@ -583,7 +609,7 @@ pub fn view_window(s: &PlotDialogState) -> Element<'_, Message> { .width(Length::Fill) ) .width(Fill), - text("DPI").size(11).color(DIM), + text("DPI").size(11).style(muted_style), text_input("", &s.dpi) .on_input(move |v| Message::PlotDlg(PlotDlgMsg::Dpi(v))) .style(field_style) @@ -631,8 +657,10 @@ pub fn view_window(s: &PlotDialogState) -> Element<'_, Message> { let body = row![list_panel, vsep(), detail].height(Fill); container(column![toolbar, hdivider(), body].spacing(0)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BG)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.base.color + )), ..Default::default() }) .width(Fill) diff --git a/src/ui/window/plugin_manager.rs b/src/ui/window/plugin_manager.rs index bd8e43d9..3f1b903e 100644 --- a/src/ui/window/plugin_manager.rs +++ b/src/ui/window/plugin_manager.rs @@ -7,7 +7,7 @@ use crate::app::Message; use crate::plugin::external::{ExternalPlugin, RegistryEntry}; use iced::widget::{button, column, container, pick_list, row, scrollable, text, text_input, Space}; -use iced::{Background, Border, Color, Element, Fill, Theme}; +use iced::{Background, Border, Element, Fill, Theme}; use rustc_hash::{FxHashMap, FxHashSet}; /// Marketplace state passed to the Plugin Manager view. @@ -25,124 +25,105 @@ inventory::submit!(crate::command::CommandRegistration { names: &["PLUGINS", "PLUGINMANAGER"] }); -const BG: Color = Color { - r: 0.15, - g: 0.15, - b: 0.15, - a: 1.0, -}; -const CARD: Color = Color { - r: 0.12, - g: 0.12, - b: 0.12, - a: 1.0, -}; -const BORDER: Color = Color { - r: 0.30, - g: 0.30, - b: 0.30, - a: 1.0, -}; -const DIM: Color = Color { - r: 0.55, - g: 0.55, - b: 0.55, - a: 1.0, -}; -const ACCENT: Color = Color { - r: 0.30, - g: 0.62, - b: 0.95, - a: 1.0, -}; -const WHITE: Color = Color { - r: 0.92, - g: 0.92, - b: 0.92, - a: 1.0, -}; +fn muted_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)), + } +} + +fn primary_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().primary.base.color), + } +} fn badge<'a>(label: String) -> Element<'a, Message> { - container(text(label).size(11).color(WHITE)) + container(text(label).size(11)) .padding([2, 8]) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { - r: 0.20, - g: 0.34, - b: 0.52, - a: 1.0, - })), + .style(|theme: &Theme| { + let pair = theme.extended_palette().primary.weak; + container::Style { + background: Some(Background::Color(pair.color)), + text_color: Some(pair.text), border: Border { radius: 4.0.into(), ..Default::default() }, ..Default::default() + } }) .into() } fn toggle_button<'a>(id: &str, disabled: bool) -> Element<'a, Message> { // Label shows the action the click performs. - let (label, on, off) = if disabled { - ("Enable", Color { r: 0.18, g: 0.5, b: 0.25, a: 1.0 }, Color { r: 0.22, g: 0.6, b: 0.3, a: 1.0 }) + let label = if disabled { + "Enable" } else { - ("Disable", Color { r: 0.4, g: 0.22, b: 0.22, a: 1.0 }, Color { r: 0.55, g: 0.28, b: 0.28, a: 1.0 }) + "Disable" }; let want_enabled = disabled; // clicking flips the state let id_owned = id.to_string(); - button(text(label).size(12).color(WHITE)) + button(text(label).size(12)) .padding([3, 12]) .on_press(Message::SetPluginEnabled(id_owned, want_enabled)) - .style(move |_: &Theme, status| { - let bg = match status { - button::Status::Hovered | button::Status::Pressed => off, - _ => on, - }; - button::Style { - background: Some(Background::Color(bg)), - text_color: WHITE, - border: Border { radius: 4.0.into(), ..Default::default() }, - ..Default::default() - } - }) + .style(if disabled { button::success } else { button::danger }) .into() } +#[derive(Clone, Copy)] +enum StatusKind { + Muted, + Success, + Danger, + Warning, +} + /// Coloured status pill for a discovered external package. -fn status_badge<'a>(label: &str, color: Color) -> Element<'a, Message> { - container(text(label.to_string()).size(11).color(WHITE)) +fn status_badge<'a>(label: &str, kind: StatusKind) -> Element<'a, Message> { + container(text(label.to_string()).size(11)) .padding([2, 8]) - .style(move |_: &Theme| container::Style { - background: Some(Background::Color(color)), + .style(move |theme: &Theme| { + let palette = theme.extended_palette(); + let pair = match kind { + StatusKind::Muted => palette.background.weak, + StatusKind::Success => palette.success.weak, + StatusKind::Danger => palette.danger.weak, + StatusKind::Warning => palette.warning.weak, + }; + container::Style { + background: Some(Background::Color(pair.color)), + text_color: Some(pair.text), border: Border { radius: 4.0.into(), ..Default::default() }, ..Default::default() + } }) .into() } fn external_card<'a>(p: &ExternalPlugin, loaded: bool, disabled: bool) -> Element<'a, Message> { - let (status, color) = if loaded && disabled { - ("Disabled", Color { r: 0.45, g: 0.45, b: 0.45, a: 1.0 }) + let (status, kind) = if loaded && disabled { + ("Disabled", StatusKind::Muted) } else if loaded { - ("Loaded", Color { r: 0.2, g: 0.5, b: 0.3, a: 1.0 }) + ("Loaded", StatusKind::Success) } else if !p.api_compatible() { - ("API incompatible", Color { r: 0.55, g: 0.28, b: 0.28, a: 1.0 }) + ("API incompatible", StatusKind::Danger) } else if !p.lib_present { - ("No library", Color { r: 0.5, g: 0.42, b: 0.2, a: 1.0 }) + ("No library", StatusKind::Warning) } else { - ("Restart to load", Color { r: 0.5, g: 0.42, b: 0.2, a: 1.0 }) + ("Restart to load", StatusKind::Warning) }; let mut header = row![ - text(p.name.clone()).size(15).color(WHITE), + text(p.name.clone()).size(15), Space::new().width(8), badge(format!("v{}", p.version)), Space::new().width(8), badge(format!("API {}", p.api_version)), Space::new().width(Fill), - status_badge(status, color), + status_badge(status, kind), ] .align_y(iced::Center); // A loaded plugin can be turned off (drops its ribbon tab + dispatch). @@ -154,75 +135,52 @@ fn external_card<'a>(p: &ExternalPlugin, loaded: bool, disabled: bool) -> Elemen header = header.push(pill_button( "Uninstall", Message::PluginUninstall(p.id.clone()), - Color { r: 0.4, g: 0.25, b: 0.25, a: 1.0 }, + button::danger, )); - let id_line = text(p.id.clone()).size(11).color(ACCENT); + let id_line = text(p.id.clone()).size(11).style(primary_style); let mut body = column![header, id_line].spacing(5); if !p.description.is_empty() { - body = body.push(text(p.description.clone()).size(12).color(DIM)); + body = body.push(text(p.description.clone()).size(12).style(muted_style)); } if !p.command_prefixes.is_empty() { body = body.push( text(format!("Commands: {}", p.command_prefixes.join(", "))) .size(11) - .color(DIM), + .style(muted_style), ); } container(body.padding([12, 14])) .width(Fill) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(CARD)), - border: Border { color: BORDER, width: 1.0, radius: 6.0.into() }, - ..Default::default() - }) + .style(container::bordered_box) .into() } -fn pill_button<'a>(label: &str, msg: Message, bg: Color) -> Element<'a, Message> { - button(text(label.to_string()).size(12).color(WHITE)) +fn pill_button<'a>( + label: &str, + msg: Message, + style: fn(&Theme, button::Status) -> button::Style, +) -> Element<'a, Message> { + button(text(label.to_string()).size(12)) .padding([4, 12]) .on_press(msg) - .style(move |_: &Theme, status| { - let c = if matches!(status, button::Status::Hovered | button::Status::Pressed) { - Color { r: bg.r + 0.08, g: bg.g + 0.08, b: bg.b + 0.08, a: 1.0 } - } else { - bg - }; - button::Style { - background: Some(Background::Color(c)), - text_color: WHITE, - border: Border { radius: 4.0.into(), ..Default::default() }, - ..Default::default() - } - }) + .style(style) .into() } /// Square icon variant of [`pill_button`] for glyph-free actions (e.g. remove). -fn pill_icon_button<'a>(icon: &'static [u8], msg: Message, bg: Color) -> Element<'a, Message> { - button(crate::ui::icons::tinted(icon, 11.0, WHITE)) +fn pill_icon_button<'a>( + icon: &'static [u8], + msg: Message, + style: fn(&Theme, button::Status) -> button::Style, +) -> Element<'a, Message> { + button(crate::ui::icons::themed(icon, 11.0)) .padding([5, 9]) .on_press(msg) - .style(move |_: &Theme, status| { - let c = if matches!(status, button::Status::Hovered | button::Status::Pressed) { - Color { r: bg.r + 0.08, g: bg.g + 0.08, b: bg.b + 0.08, a: 1.0 } - } else { - bg - }; - button::Style { - background: Some(Background::Color(c)), - text_color: WHITE, - border: Border { radius: 4.0.into(), ..Default::default() }, - ..Default::default() - } - }) + .style(style) .into() } -const GREEN: Color = Color { r: 0.2, g: 0.45, b: 0.28, a: 1.0 }; -const RED: Color = Color { r: 0.4, g: 0.25, b: 0.25, a: 1.0 }; - /// Release dropdown + Install (+ optional unlink) for one repo. fn install_controls<'a>( repo: &str, @@ -232,7 +190,7 @@ fn install_controls<'a>( ) -> Element<'a, Message> { let repo_s = repo.to_string(); let picker: Element<'_, Message> = if tags.is_empty() { - text("no releases").size(11).color(DIM).into() + text("no releases").size(11).style(muted_style).into() } else { let r = repo_s.clone(); pick_list(tags, selected, move |tag| { @@ -244,7 +202,11 @@ fn install_controls<'a>( let mut controls = row![ picker, Space::new().width(8), - pill_button("Install", Message::PluginInstall(repo_s.clone()), GREEN), + pill_button( + "Install", + Message::PluginInstall(repo_s.clone()), + button::success, + ), ] .align_y(iced::Center) .spacing(4); @@ -253,7 +215,7 @@ fn install_controls<'a>( controls = controls.push(pill_icon_button( crate::ui::icons::CLOSE, Message::PluginRepoRemove(repo_s), - RED, + button::danger, )); } controls.into() @@ -262,37 +224,33 @@ fn install_controls<'a>( fn market_card<'a>(body: iced::widget::Column<'a, Message>) -> Element<'a, Message> { container(body.spacing(4).padding([10, 12])) .width(Fill) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(CARD)), - border: Border { color: BORDER, width: 1.0, radius: 6.0.into() }, - ..Default::default() - }) + .style(container::bordered_box) .into() } fn marketplace_section<'a>(m: &MarketView) -> Element<'a, Message> { - let mut col = column![text("Available plugins").size(13).color(ACCENT)].spacing(6); + let mut col = column![text("Available plugins").size(13).style(primary_style)].spacing(6); // Curated registry entries (from the OpenCADStudio repo). for e in m.registry { let tags = m.release_tags.get(&e.repo).cloned().unwrap_or_default(); let selected = m.selected_tag.get(&e.repo).cloned(); let header = row![ - text(e.name.clone()).size(14).color(WHITE), + text(e.name.clone()).size(14), Space::new().width(Fill), install_controls(&e.repo, tags, selected, false), ] .align_y(iced::Center); - let mut body = column![header, text(e.repo.clone()).size(11).color(ACCENT)]; + let mut body = column![header, text(e.repo.clone()).size(11).style(primary_style)]; if !e.description.is_empty() { - body = body.push(text(e.description.clone()).size(12).color(DIM)); + body = body.push(text(e.description.clone()).size(12).style(muted_style)); } col = col.push(market_card(body)); } // Manual: link any repo by owner/repo. col = col.push(Space::new().height(6)); - col = col.push(text("Add a repository").size(12).color(DIM)); + col = col.push(text("Add a repository").size(12).style(muted_style)); col = col.push( row![ text_input("owner/repo", m.input) @@ -301,7 +259,7 @@ fn marketplace_section<'a>(m: &MarketView) -> Element<'a, Message> { .size(13) .width(Fill), Space::new().width(8), - pill_button("Add", Message::PluginRepoAdd, Color { r: 0.2, g: 0.4, b: 0.62, a: 1.0 }), + pill_button("Add", Message::PluginRepoAdd, button::primary), ] .align_y(iced::Center), ); @@ -309,7 +267,7 @@ fn marketplace_section<'a>(m: &MarketView) -> Element<'a, Message> { let tags = m.release_tags.get(repo).cloned().unwrap_or_default(); let selected = m.selected_tag.get(repo).cloned(); let header = row![ - text(repo.clone()).size(13).color(WHITE), + text(repo.clone()).size(13), Space::new().width(Fill), install_controls(repo, tags, selected, true), ] @@ -318,7 +276,7 @@ fn marketplace_section<'a>(m: &MarketView) -> Element<'a, Message> { } if !m.status.is_empty() { - col = col.push(text(m.status.to_string()).size(11).color(DIM)); + col = col.push(text(m.status.to_string()).size(11).style(muted_style)); } col.into() } @@ -329,17 +287,17 @@ pub fn view_window<'a>( loaded: &FxHashSet, market: MarketView, ) -> Element<'a, Message> { - let title = text("Plugins").size(20).color(WHITE); + let title = text("Plugins").size(20); let subtitle = text("Add-ons load from the plugins folder. Install from a repository below.") .size(12) - .color(DIM); + .style(muted_style); let mut list = column![].spacing(10); // Installed external packages (from the plugins folder). if externals.is_empty() { - list = list.push(text("No plugins installed yet.").size(13).color(DIM)); + list = list.push(text("No plugins installed yet.").size(13).style(muted_style)); } else { - list = list.push(text("Installed").size(13).color(ACCENT)); + list = list.push(text("Installed").size(13).style(primary_style)); for p in externals { list = list.push(external_card( p, @@ -360,8 +318,10 @@ pub fn view_window<'a>( .width(Fill) .height(Fill), ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BG)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.base.color, + )), ..Default::default() }) .width(Fill) diff --git a/src/ui/window/shortcuts.rs b/src/ui/window/shortcuts.rs index 48f983ce..2f5cdc69 100644 --- a/src/ui/window/shortcuts.rs +++ b/src/ui/window/shortcuts.rs @@ -2,7 +2,7 @@ use crate::app::Message; use iced::widget::{column, container, row, scrollable, text, Space}; -use iced::{Background, Color, Element, Fill, Theme}; +use iced::{Background, Element, Fill, Theme}; use std::borrow::Cow; /// Display name of the primary accelerator modifier on this platform. @@ -14,49 +14,26 @@ const MOD: &str = "Cmd"; #[cfg(not(target_os = "macos"))] const MOD: &str = "Ctrl"; -const TB: Color = Color { - r: 0.13, - g: 0.13, - b: 0.13, - a: 1.0, -}; -const BG: Color = Color { - r: 0.15, - g: 0.15, - b: 0.15, - a: 1.0, -}; -const BORDER: Color = Color { - r: 0.35, - g: 0.35, - b: 0.35, - a: 1.0, -}; -const DIM: Color = Color { - r: 0.55, - g: 0.55, - b: 0.55, - a: 1.0, -}; -const ACCENT: Color = Color { - r: 0.25, - g: 0.50, - b: 0.85, - a: 1.0, -}; -const KEY: Color = Color { - r: 0.40, - g: 0.70, - b: 1.00, - a: 1.0, -}; +fn muted_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)), + } +} + +fn primary_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().primary.base.color), + } +} fn hdivider<'a>() -> Element<'a, Message> { container(Space::new().width(Fill).height(1)) .width(Fill) .height(1) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BORDER)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.neutral.color, + )), ..Default::default() }) .into() @@ -69,7 +46,7 @@ fn shortcut_row<'a>( row![ text(key.into()) .size(11) - .color(KEY) + .style(primary_style) .font(iced::Font::MONOSPACE) .width(160), text(action).size(11), @@ -81,7 +58,7 @@ fn shortcut_row<'a>( } fn section<'a>(title: impl Into>) -> Element<'a, Message> { - container(text(title.into()).size(11).color(DIM)) + container(text(title.into()).size(11).style(muted_style)) .padding(iced::Padding { top: 6.0, right: 0.0, @@ -99,12 +76,14 @@ pub fn view_window<'a>( row![ text("Type SHORTCUTS SET to add custom shortcuts.") .size(10) - .color(DIM), + .style(muted_style), ] .align_y(iced::Center), ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(TB)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.weakest.color, + )), ..Default::default() }) .width(Fill) @@ -147,7 +126,7 @@ pub fn view_window<'a>( rows.push( text(" (none — use: SHORTCUTS SET )") .size(11) - .color(DIM) + .style(muted_style) .into(), ); } else { @@ -158,7 +137,7 @@ pub fn view_window<'a>( row![ text(key.as_str()) .size(11) - .color(KEY) + .style(primary_style) .font(iced::Font::MONOSPACE) .width(160), text(cmd.as_str()).size(11), @@ -179,26 +158,25 @@ pub fn view_window<'a>( // ── Header row with accent ──────────────────────────────────────────── let header = container( row![ - text("Key").size(10).color(ACCENT).width(160), - text("Action").size(10).color(ACCENT), + text("Key").size(10).style(primary_style).width(160), + text("Action").size(10).style(primary_style), ] .spacing(8) .padding([4, 16]), ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { - r: 0.13, - g: 0.13, - b: 0.18, - a: 1.0, - })), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().primary.weak.color, + )), ..Default::default() }) .width(Fill); container(column![toolbar, hdivider(), header, hdivider(), content].spacing(0)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BG)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.base.color, + )), ..Default::default() }) .width(Fill) diff --git a/src/ui/window/update_notice.rs b/src/ui/window/update_notice.rs index 95e84563..39d7c227 100644 --- a/src/ui/window/update_notice.rs +++ b/src/ui/window/update_notice.rs @@ -1,60 +1,33 @@ use crate::app::Message; use iced::widget::{button, column, container, row, scrollable, text, Space}; -use iced::{Background, Border, Color, Element, Fill, Theme}; +use iced::{Background, Border, Element, Fill, Theme}; -const BG: Color = Color { - r: 0.15, - g: 0.15, - b: 0.15, - a: 1.0, -}; -const PANEL: Color = Color { - r: 0.12, - g: 0.12, - b: 0.12, - a: 1.0, -}; -const BORDER: Color = Color { - r: 0.30, - g: 0.30, - b: 0.30, - a: 1.0, -}; -const DIM: Color = Color { - r: 0.55, - g: 0.55, - b: 0.55, - a: 1.0, -}; -const ACCENT: Color = Color { - r: 0.25, - g: 0.50, - b: 0.85, - a: 1.0, -}; -const WHITE: Color = Color { - r: 0.92, - g: 0.92, - b: 0.92, - a: 1.0, -}; +fn muted_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)), + } +} + +fn primary_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.extended_palette().primary.base.color), + } +} /// Renders one of the two "Installed" / "Latest" cards. The `highlight` /// flag tints the border + label with the accent colour, making the new /// version the visual anchor of the row. fn version_card<'a>(label: &'static str, value: String, highlight: bool) -> Element<'a, Message> { - let label_color = if highlight { ACCENT } else { DIM }; - let value_color = WHITE; - let border_color = if highlight { ACCENT } else { BORDER }; - let bg = if highlight { - Color { r: 0.10, g: 0.16, b: 0.22, a: 1.0 } // accent-tinted dark - } else { - PANEL - }; container( column![ - text(label).size(10).color(label_color), - text(value).size(20).color(value_color), + text(label) + .size(10) + .style(move |theme: &Theme| if highlight { + primary_style(theme) + } else { + muted_style(theme) + }), + text(value).size(20), ] .spacing(4) .align_x(iced::Center), @@ -67,14 +40,26 @@ fn version_card<'a>(label: &'static str, value: String, highlight: bool) -> Elem left: 12.0, }) .align_x(iced::Center) - .style(move |_: &Theme| container::Style { - background: Some(Background::Color(bg)), + .style(move |theme: &Theme| { + let palette = theme.extended_palette(); + let pair = if highlight { + palette.primary.weak + } else { + palette.background.base + }; + container::Style { + background: Some(Background::Color(pair.color)), border: Border { - color: border_color, + color: if highlight { + palette.primary.base.color + } else { + palette.background.neutral.color + }, width: 1.0, radius: 6.0.into(), }, ..Default::default() + } }) .into() } @@ -98,24 +83,23 @@ fn render_notes_line<'a>(raw: &str) -> Element<'a, Message> { if let Some(rest) = trimmed.strip_prefix("## ") { return text(strip_inline_md(rest)) .size(13) - .color(ACCENT) + .style(primary_style) .into(); } if let Some(rest) = trimmed.strip_prefix("### ") { return text(strip_inline_md(rest)) .size(12) - .color(WHITE) .into(); } if let Some(rest) = trimmed.strip_prefix("- ").or_else(|| trimmed.strip_prefix("* ")) { return row![ - container(crate::ui::icons::tinted(crate::ui::icons::DOT, 5.0, DIM)).width(14), - text(strip_inline_md(rest)).size(11).color(WHITE), + container(crate::ui::icons::themed_secondary(crate::ui::icons::DOT, 5.0)).width(14), + text(strip_inline_md(rest)).size(11), ] .spacing(4) .into(); } - text(strip_inline_md(trimmed)).size(11).color(WHITE).into() + text(strip_inline_md(trimmed)).size(11).into() } /// Drop `**…**` and `` `…` `` markers without preserving emphasis (iced 0.14 @@ -139,10 +123,10 @@ fn strip_inline_md(s: &str) -> String { pub fn view_window<'a>(latest: &'a str, body: &'a str) -> Element<'a, Message> { let header = container( column![ - text("New Release Available").size(20).color(ACCENT), + text("New Release Available").size(20).style(primary_style), text("A newer Open CAD Studio version is published on GitHub.") .size(11) - .color(DIM), + .style(muted_style), ] .spacing(4) .align_x(iced::Center), @@ -165,10 +149,9 @@ pub fn view_window<'a>(latest: &'a str, body: &'a str) -> Element<'a, Message> { false, ); let latest_card = version_card("Latest", format!("v{}", latest), true); - let arrow = container(crate::ui::icons::tinted( + let arrow = container(crate::ui::icons::themed_secondary( crate::ui::icons::ARROW_LONG_RIGHT, 20.0, - DIM, )) .width(iced::Length::Fixed(32.0)) .height(Fill) @@ -181,50 +164,12 @@ pub fn view_window<'a>(latest: &'a str, body: &'a str) -> Element<'a, Message> { let later_btn = button(text("Later").size(11)) .on_press(Message::UpdateNoticeClose) - .style(|_: &Theme, st| button::Style { - background: Some(Background::Color(match st { - button::Status::Hovered | button::Status::Pressed => Color { - r: 0.22, - g: 0.22, - b: 0.22, - a: 1.0, - }, - _ => Color { - r: 0.18, - g: 0.18, - b: 0.18, - a: 1.0, - }, - })), - text_color: WHITE, - border: Border { - color: BORDER, - width: 1.0, - radius: 4.0.into(), - }, - ..Default::default() - }) + .style(button::secondary) .padding([6, 16]); let open_btn = button(text("Open Release Page").size(11)) .on_press(Message::UpdateNoticeOpenRelease) - .style(|_: &Theme, st| button::Style { - background: Some(Background::Color(match st { - button::Status::Hovered | button::Status::Pressed => Color { - r: 0.20, - g: 0.42, - b: 0.72, - a: 1.0, - }, - _ => ACCENT, - })), - text_color: WHITE, - border: Border { - radius: 4.0.into(), - ..Default::default() - }, - ..Default::default() - }) + .style(button::primary) .padding([6, 16]); let footer = row![Space::new().width(Fill), later_btn, open_btn] @@ -240,7 +185,7 @@ pub fn view_window<'a>(latest: &'a str, body: &'a str) -> Element<'a, Message> { // Release notes panel. Rendered as a light-markdown column inside a // bordered scrollable so long bodies stay contained and don't // explode the window. Empty body → "No release notes provided." - let notes_heading = container(text("What's new").size(11).color(DIM)) + let notes_heading = container(text("What's new").size(11).style(muted_style)) .padding(iced::Padding { top: 10.0, right: 0.0, @@ -251,7 +196,7 @@ pub fn view_window<'a>(latest: &'a str, body: &'a str) -> Element<'a, Message> { let notes_body: Element<'a, Message> = if body.trim().is_empty() { text("No release notes provided.") .size(11) - .color(DIM) + .style(muted_style) .into() } else { let mut col = column![].spacing(4); @@ -264,15 +209,7 @@ pub fn view_window<'a>(latest: &'a str, body: &'a str) -> Element<'a, Message> { let notes_block = container(notes_body) .width(Fill) .height(Fill) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL)), - border: Border { - color: BORDER, - width: 1.0, - radius: 4.0.into(), - }, - ..Default::default() - }); + .style(container::bordered_box); // Wrap notes_block in a Fill-height container outside the column so it // greedily claims every pixel left over after the fixed-height rows @@ -294,8 +231,10 @@ pub fn view_window<'a>(latest: &'a str, body: &'a str) -> Element<'a, Message> { left: 20.0, }), ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BG)), + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.extended_palette().background.base.color, + )), ..Default::default() }) .width(Fill) diff --git a/src/ui/wrap_bar.rs b/src/ui/wrap_bar.rs index c088c472..1ae75309 100644 --- a/src/ui/wrap_bar.rs +++ b/src/ui/wrap_bar.rs @@ -25,7 +25,7 @@ use iced::advanced::layout::{self, Layout}; use iced::advanced::widget::{self, tree, Widget}; use iced::advanced::{mouse, overlay, renderer, Clipboard, Renderer as _, Shell}; use iced::{ - Background, Border, Color, Element, Event, Length, Point, Rectangle, Renderer, Shadow, Size, + Background, Border, Element, Event, Length, Point, Rectangle, Renderer, Shadow, Size, Theme, Vector, }; @@ -1360,12 +1360,7 @@ impl<'a> Widget for ReorderTab<'a> { shadow: Shadow::default(), snap: true, }, - Background::Color(Color { - r: 0.20, - g: 0.55, - b: 0.90, - a: 1.0, - }), + Background::Color(theme.extended_palette().primary.base.color), ); } }