feat(ui): add configurable Iced themes

This commit is contained in:
Hakan Seven 2026-07-28 09:33:12 +03:00
commit 4bf14baea4
52 changed files with 3058 additions and 5665 deletions

View file

@ -18,6 +18,8 @@ use crate::ui::window::plot::PlotDialogState;
pub struct AppConfig { pub struct AppConfig {
/// Input modes, backup, plugin lists, viewport background colours, … /// Input modes, backup, plugin lists, viewport background colours, …
pub settings: UserSettings, pub settings: UserSettings,
/// Iced theme selection and the six base colours used by a custom theme.
pub theme: UiThemeConfig,
/// Recent-files list + retained count. /// Recent-files list + retained count.
pub recent: RecentConfig, pub recent: RecentConfig,
/// Last selected section on the tabbed Start page. /// Last selected section on the tabbed Start page.
@ -35,6 +37,7 @@ impl Default for AppConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
settings: UserSettings::default(), settings: UserSettings::default(),
theme: UiThemeConfig::default(),
recent: RecentConfig::default(), recent: RecentConfig::default(),
start: StartConfig::default(), start: StartConfig::default(),
statusbar: StatusBarConfig::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> {
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)] #[derive(Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)] #[serde(default)]
pub struct RecentConfig { pub struct RecentConfig {

View file

@ -1,7 +1,7 @@
mod alias; mod alias;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
mod automation; mod automation;
mod config; pub(crate) mod config;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub use automation::{export_headless, serve}; pub use automation::{export_headless, serve};
mod command_driver; mod command_driver;
@ -688,6 +688,8 @@ pub(super) struct OpenCADStudio {
// ── Color Scheme ────────────────────────────────────────────────────── // ── Color Scheme ──────────────────────────────────────────────────────
active_theme: Theme, active_theme: Theme,
ui_theme: config::UiThemeConfig,
theme_color_inputs: [String; 6],
// ── Keyboard Shortcut Editor ────────────────────────────────────────── // ── Keyboard Shortcut Editor ──────────────────────────────────────────
/// User-defined function-key overrides: "F3" → command string. /// User-defined function-key overrides: "F3" → command string.
@ -1474,6 +1476,10 @@ pub enum Message {
OptionsOpen, OptionsOpen,
/// Set the default type/version used when first saving a new drawing. /// Set the default type/version used when first saving a new drawing.
DefaultSaveFormatChanged(String), 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, ClearScene,
SetWireframe(bool), SetWireframe(bool),
/// Set the active tab's render mode (one of acadrust's seven visual /// Set the active tab's render mode (one of acadrust's seven visual
@ -2626,6 +2632,8 @@ impl OpenCADStudio {
active_plot_style: None, active_plot_style: None,
// Color scheme (default: dark CAD-style) // Color scheme (default: dark CAD-style)
active_theme: Theme::Dark, active_theme: Theme::Dark,
ui_theme: config::UiThemeConfig::default(),
theme_color_inputs: config::UiThemePalette::default().hex_values(),
// Keyboard shortcuts // Keyboard shortcuts
shortcut_overrides: rustc_hash::FxHashMap::default(), shortcut_overrides: rustc_hash::FxHashMap::default(),
// Command aliases (populated from ocad.pgp just after construction) // Command aliases (populated from ocad.pgp just after construction)

View file

@ -320,6 +320,7 @@ impl OpenCADStudio {
pub(in crate::app) fn current_config(&self) -> crate::app::config::AppConfig { pub(in crate::app) fn current_config(&self) -> crate::app::config::AppConfig {
crate::app::config::AppConfig { crate::app::config::AppConfig {
settings: self.current_settings(), settings: self.current_settings(),
theme: self.ui_theme.clone(),
recent: crate::app::config::RecentConfig { recent: crate::app::config::RecentConfig {
files: self files: self
.recent_files .recent_files
@ -342,6 +343,9 @@ impl OpenCADStudio {
/// Distribute a loaded config into live app state (called once at startup). /// 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) { pub(in crate::app) fn apply_config(&mut self, cfg: crate::app::config::AppConfig) {
self.apply_settings(&cfg.settings); 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 self.recent_files = cfg
.recent .recent
.files .files

View file

@ -3551,7 +3551,12 @@ impl OpenCADStudio {
} }
Message::SetTheme(theme) => { 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.active_theme = theme;
self.persist_settings_if_changed();
Task::none() Task::none()
} }
@ -3620,6 +3625,37 @@ impl OpenCADStudio {
Message::DefaultSaveFormatChanged(format) => { Message::DefaultSaveFormatChanged(format) => {
self.default_save_format = self.default_save_format =
crate::io::canonical_save_format(&format).to_string(); 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() Task::none()
} }

View file

@ -4,7 +4,7 @@ use super::super::Message;
use iced::widget::{ use iced::widget::{
button, container, mouse_area, row, button, container, mouse_area, row,
}; };
use iced::{Background, Border, Color, Element, Theme}; use iced::{Background, Border, Element, Theme};
pub(super) fn viewport_controls<'a>( pub(super) fn viewport_controls<'a>(
render_mode: acadrust::entities::ViewportRenderMode, render_mode: acadrust::entities::ViewportRenderMode,
@ -23,64 +23,58 @@ pub(super) fn viewport_controls<'a>(
RenderModeChoice(M::FlatShadedWithEdges), RenderModeChoice(M::FlatShadedWithEdges),
RenderModeChoice(M::GouraudShadedWithEdges), RenderModeChoice(M::GouraudShadedWithEdges),
]; ];
let light = Color { r: 0.85, g: 0.85, b: 0.85, a: 1.0 }; let danger_btn = move |bytes: &'static [u8], msg: Message| {
let accent = Color { r: 0.45, g: 0.70, b: 1.0, a: 1.0 }; button(crate::ui::icons::themed_danger(bytes, 15.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))
.on_press(msg) .on_press(msg)
.padding([4, 6]) .padding([4, 6])
.style(move |_: &Theme, status| iced::widget::button::Style { .style(move |theme: &Theme, status| iced::widget::button::Style {
background: Some(Background::Color(match status { background: matches!(
status,
iced::widget::button::Status::Hovered iced::widget::button::Status::Hovered
| iced::widget::button::Status::Pressed => Color { | iced::widget::button::Status::Pressed
r: 0.25, )
g: 0.25, .then_some(Background::Color(
b: 0.25, theme.extended_palette().danger.weak.color
a: 0.9, )),
},
_ => Color::TRANSPARENT,
})),
border: Border { border: Border {
radius: 3.0.into(), radius: 3.0.into(),
..Default::default() ..Default::default()
}, },
text_color: color, text_color: theme.extended_palette().danger.base.color,
..Default::default() ..Default::default()
}) })
}; };
// Borderless icon button; an `active` toggle gets an accent tint + fill. // Borderless icon button; an `active` toggle gets an accent tint + fill.
let icon_btn = move |bytes: &'static [u8], active: bool, msg: Message| { let icon_btn = move |bytes: &'static [u8], active: bool, msg: Message| {
let tint = if active { accent } else { light }; let icon = if active {
button(crate::ui::icons::tinted(bytes, 15.0, tint)) crate::ui::icons::themed_primary(bytes, 15.0)
} else {
crate::ui::icons::themed(bytes, 15.0)
};
button(icon)
.on_press(msg) .on_press(msg)
.padding([4, 6]) .padding([4, 6])
.style(move |_: &Theme, status| iced::widget::button::Style { .style(move |theme: &Theme, status| {
background: Some(Background::Color(match (active, status) { let palette = theme.extended_palette();
(_, iced::widget::button::Status::Hovered) => Color { let pair = match (active, status) {
r: 0.25, (_, iced::widget::button::Status::Hovered) => {
g: 0.25, Some(palette.background.strong)
b: 0.25, }
a: 0.9, (true, _) => Some(palette.primary.weak),
}, (false, _) => None,
(true, _) => Color { };
r: 0.16, iced::widget::button::Style {
g: 0.22, background: pair.map(|p| Background::Color(p.color)),
b: 0.32,
a: 0.9,
},
(false, _) => Color::TRANSPARENT,
})),
border: Border { border: Border {
radius: 3.0.into(), radius: 3.0.into(),
..Default::default() ..Default::default()
}, },
text_color: tint, text_color: pair
.map(|p| p.text)
.unwrap_or(palette.background.base.text),
..Default::default() ..Default::default()
}
}) })
}; };
@ -92,27 +86,27 @@ pub(super) fn viewport_controls<'a>(
) )
.text_size(11) .text_size(11)
.padding([4, 6]) .padding([4, 6])
.style(move |_: &Theme, _| iced::widget::pick_list::Style { .style(move |theme: &Theme, _| {
background: Background::Color(Color::TRANSPARENT), let text = theme.extended_palette().background.base.text;
iced::widget::pick_list::Style {
background: Background::Color(iced::Color::TRANSPARENT),
border: Border { border: Border {
radius: 3.0.into(), radius: 3.0.into(),
..Default::default() ..Default::default()
}, },
text_color: light, text_color: text,
placeholder_color: light, placeholder_color: text.scale_alpha(0.68),
handle_color: light, handle_color: text,
}
}); });
// Thin vertical divider between control groups. // Thin vertical divider between control groups.
let sep = || { 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 { iced::widget::container::Style {
background: Some(Background::Color(Color { background: Some(Background::Color(
r: 0.45, theme.extended_palette().background.neutral.color.scale_alpha(0.7)
g: 0.45, )),
b: 0.45,
a: 0.7,
})),
..Default::default() ..Default::default()
} }
}) })
@ -139,7 +133,7 @@ pub(super) fn viewport_controls<'a>(
// would only fire on release). Placed just left of Close. // would only fire on release). Placed just left of Close.
if tile_count > 1 { if tile_count > 1 {
let drag = mouse_area( 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]) .padding([4, 6])
.style(|_: &Theme| iced::widget::container::Style { .style(|_: &Theme| iced::widget::container::Style {
border: Border { border: Border {
@ -155,30 +149,25 @@ pub(super) fn viewport_controls<'a>(
.push(sep()) .push(sep())
.push(drag) .push(drag)
.push(sep()) .push(sep())
.push(tinted_btn(crate::ui::icons::CLOSE, red, Message::CloseModelViewport)); .push(danger_btn(crate::ui::icons::CLOSE, Message::CloseModelViewport));
} }
} }
container(bar) container(bar)
.padding(2) .padding(2)
.style(|_: &Theme| iced::widget::container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(Color { let palette = theme.extended_palette();
r: 0.10, iced::widget::container::Style {
g: 0.10, background: Some(Background::Color(
b: 0.10, palette.background.weak.color.scale_alpha(0.92)
a: 0.75, )),
})),
border: Border { border: Border {
color: Color { color: palette.background.neutral.color,
r: 0.35,
g: 0.35,
b: 0.35,
a: 1.0,
},
width: 1.0, width: 1.0,
radius: 4.0.into(), radius: 4.0.into(),
}, },
..Default::default() ..Default::default()
}
}) })
.into() .into()
} }

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
use super::super::{Message, OpenCADStudio}; use super::super::{Message, OpenCADStudio};
use iced::widget::{button, column, container, pick_list, row, text, Space}; 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 { impl OpenCADStudio {
/// Title shown in the active modal's title bar, left of the move/close /// 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) sized(crate::ui::window::alias_editor::view_window(&self.alias_editor_rows), 480, 520)
} }
super::super::ModalKind::Options => sized( super::super::ModalKind::Options => sized(
crate::ui::window::options::view_window(&self.default_save_format), crate::ui::window::options::view_window(
480, &self.default_save_format,
190, &self.ui_theme,
&self.theme_color_inputs,
),
520,
500,
), ),
super::super::ModalKind::PluginManager => sized( super::super::ModalKind::PluginManager => sized(
crate::ui::window::plugin_manager::view_window( 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 /// Compact Save-As options dialog: pick the format/version and a default file
/// name. The destination folder and overwrite confirmation come from the /// name. The destination folder and overwrite confirmation come from the
/// native OS save dialog (native) or the browser download (web) that follows. /// 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> { 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 let sel_fmt = crate::io::SAVE_FORMAT_OPTIONS
.iter() .iter()
.copied() .copied()
.find(|&s| s == format); .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<Element<'a, Message>> = Vec::new(); let mut items: Vec<Element<'a, Message>> = 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()); items.push(Space::new().height(12).into());
// Web has no native file dialog, so the file name is typed here. On native // 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. // the OS save dialog collects the name, so this field is omitted.
#[cfg(target_arch = "wasm32")] #[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( items.push(
row![ row![
label("File name:").width(70), label("File name:").width(70),
iced::widget::text_input("drawing.dwg", filename) iced::widget::text_input("drawing.dwg", filename)
.on_input(Message::SaveDialogFilenameChanged) .on_input(Message::SaveDialogFilenameChanged)
.style(input_sty)
.size(13) .size(13)
.padding([5, 8]) .padding([5, 8])
.width(Fill), .width(Fill),
@ -860,9 +795,9 @@ fn save_as_dialog_window<'a>(filename: &'a str, format: &'a str) -> Element<'a,
items.push( items.push(
row![ row![
Space::new().width(Fill), 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), Space::new().width(8),
btn("Cancel", Message::SaveDialogCancel, BTN_GREY, BTN_GHOV), dialog_button("Cancel", Message::SaveDialogCancel, button::secondary),
] ]
.into(), .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); let body = column(items).spacing(0);
container(body) container(body)
.style(|_: &Theme| container::Style { .style(dialog_body_style)
background: Some(Background::Color(BG)),
..Default::default()
})
.padding([14, 16]) .padding([14, 16])
.width(Fill) .width(Fill)
.height(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> { 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 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( container(
column![ column![
text(body_text).size(13).color(TEXT_COL), text(body_text).size(13),
iced::widget::Space::new().height(20), iced::widget::Space::new().height(20),
row![ row![
btn("Save", Message::UnsavedDialogSave, BTN_SAVE, BTN_HOVER), dialog_button("Save", Message::UnsavedDialogSave, button::primary),
iced::widget::Space::new().width(8), 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), iced::widget::Space::new().width(8),
btn("Cancel", Message::UnsavedDialogCancel, BTN_DISC, BTN_DHOV), dialog_button("Cancel", Message::UnsavedDialogCancel, button::secondary),
], ],
] ]
.spacing(0), .spacing(0),
) )
.style(move |_: &Theme| container::Style { .style(dialog_body_style)
background: Some(Background::Color(BG)),
..Default::default()
})
.center(Fill) .center(Fill)
.padding([24, 28]) .padding([24, 28])
.into() .into()
@ -971,55 +837,6 @@ fn unsaved_changes_dialog_window(name: &str) -> Element<'static, Message> {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
fn file_in_use_dialog_window(path: &str, error: &str) -> Element<'static, Message> { 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) let file_name = std::path::Path::new(path)
.file_name() .file_name()
.map(|name| name.to_string_lossy().into_owned()) .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 heading = format!("\"{file_name}\" could not be saved.");
let path_line = format!("Path: {path}"); let path_line = format!("Path: {path}");
let details = format!("Details: {error}"); 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( container(
column![ column![
text(heading).size(14).color(TEXT_COL), text(heading).size(14),
Space::new().height(8), Space::new().height(8),
text( text(
"The file is open or being used by another application. \ "The file is open or being used by another application. \
Close it there and retry, or save this drawing under a different name." Close it there and retry, or save this drawing under a different name."
) )
.size(13) .size(13)
.color(TEXT_COL)
.width(Fill), .width(Fill),
Space::new().height(12), 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), 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), Space::new().height(18),
row![ row![
btn( dialog_button(
"Retry", "Retry",
Message::SaveFileInUseRetry, Message::SaveFileInUseRetry,
BTN_PRIMARY, button::primary
BTN_PRIMARY_HOVER
), ),
Space::new().width(8), Space::new().width(8),
btn( dialog_button(
"Save As", "Save As",
Message::SaveFileInUseSaveAs, Message::SaveFileInUseSaveAs,
BTN_SECONDARY, button::secondary
BTN_SECONDARY_HOVER
), ),
Space::new().width(8), Space::new().width(8),
btn( dialog_button(
"Cancel", "Cancel",
Message::SaveFileInUseCancel, Message::SaveFileInUseCancel,
BTN_SECONDARY, button::secondary
BTN_SECONDARY_HOVER
), ),
], ],
] ]
.spacing(0), .spacing(0),
) )
.style(|_: &Theme| container::Style { .style(dialog_body_style)
background: Some(Background::Color(BG)),
..Default::default()
})
.padding([18, 20]) .padding([18, 20])
.width(Fill) .width(Fill)
.height(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"))] #[cfg(not(target_arch = "wasm32"))]
fn external_change_dialog_window(path: &str) -> Element<'static, Message> { 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) let file_name = std::path::Path::new(path)
.file_name() .file_name()
.map(|name| name.to_string_lossy().into_owned()) .map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| "Drawing".to_string()); .unwrap_or_else(|| "Drawing".to_string());
let heading = format!("\"{file_name}\" was changed by another application."); let heading = format!("\"{file_name}\" was changed by another application.");
let path_line = format!("Path: {path}"); 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( container(
column![ column![
text(heading).size(14).color(TEXT_COL), text(heading).size(14),
Space::new().height(8), Space::new().height(8),
text( text(
"Saving now could destroy those external changes. Reload the disk copy, \ "Saving now could destroy those external changes. Reload the disk copy, \
save your local work elsewhere, or explicitly overwrite it." save your local work elsewhere, or explicitly overwrite it."
) )
.size(13) .size(13)
.color(TEXT_COL)
.width(Fill), .width(Fill),
Space::new().height(12), 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), Space::new().height(18),
row![ row![
btn( dialog_button(
"Reload from Disk", "Reload from Disk",
Message::ExternalChangeReload, Message::ExternalChangeReload,
BTN_PRIMARY, button::primary
BTN_PRIMARY_HOVER
), ),
Space::new().width(8), Space::new().width(8),
btn( dialog_button(
"Save As", "Save As",
Message::ExternalChangeSaveAs, Message::ExternalChangeSaveAs,
BTN_SECONDARY, button::secondary
BTN_SECONDARY_HOVER
), ),
Space::new().width(8), Space::new().width(8),
btn( dialog_button(
"Overwrite", "Overwrite",
Message::ExternalChangeOverwrite, Message::ExternalChangeOverwrite,
BTN_DANGER, button::danger
BTN_DANGER_HOVER
), ),
Space::new().width(8), Space::new().width(8),
btn( dialog_button(
"Cancel", "Cancel",
Message::ExternalChangeCancel, Message::ExternalChangeCancel,
BTN_SECONDARY, button::secondary
BTN_SECONDARY_HOVER
), ),
], ],
] ]
.spacing(0), .spacing(0),
) )
.style(|_: &Theme| container::Style { .style(dialog_body_style)
background: Some(Background::Color(BG)),
..Default::default()
})
.padding([18, 20]) .padding([18, 20])
.width(Fill) .width(Fill)
.height(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 /// 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). /// 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> { 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!( let body_text = format!(
"This drawing contains {count} AEC/Civil objects that \"{target}\" \ "This drawing contains {count} AEC/Civil objects that \"{target}\" \
cannot store, so they will not be saved.\n\n\ cannot store, so they will not be saved.\n\n\
To keep them, save in the source version ({src_version})." 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( container(
column![ column![
text(body_text).size(13).color(TEXT_COL), text(body_text).size(13),
iced::widget::Space::new().height(20), iced::widget::Space::new().height(20),
row![ 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), 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), iced::widget::Space::new().width(8),
btn("Back", Message::AecDropBack, BTN_DISC, BTN_DHOV), dialog_button("Back", Message::AecDropBack, button::secondary),
], ],
] ]
.spacing(0), .spacing(0),
) )
.style(move |_: &Theme| container::Style { .style(dialog_body_style)
background: Some(Background::Color(BG)),
..Default::default()
})
.center(Fill) .center(Fill)
.padding([24, 28]) .padding([24, 28])
.into() .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" /// Confirm deleting layer(s) that still have objects on them. "Delete Objects"
/// erases them and removes the layers; "Cancel" leaves everything. /// erases them and removes the layers; "Cancel" leaves everything.
fn layer_delete_warning_window(names: &[String], count: usize) -> Element<'static, Message> { 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 obj = if count == 1 { "object" } else { "objects" };
let subject = if names.len() == 1 { let subject = if names.len() == 1 {
format!("Layer \"{}\"", names[0]) 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" } 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( container(
column![ column![
text(body_text).size(13).color(TEXT_COL), text(body_text).size(13),
iced::widget::Space::new().height(20), iced::widget::Space::new().height(20),
row![ 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), iced::widget::Space::new().width(8),
btn("Cancel", Message::CloseModal, BTN_CANCEL, BTN_CANCEL_HOV), dialog_button("Cancel", Message::CloseModal, button::secondary),
], ],
] ]
.spacing(0), .spacing(0),
) )
.style(move |_: &Theme| container::Style { .style(dialog_body_style)
background: Some(Background::Color(BG)),
..Default::default()
})
.center(Fill) .center(Fill)
.padding([24, 28]) .padding([24, 28])
.into() .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` /// just dismisses. Either answer flips the persisted `default_assoc_prompted`
/// flag so the dialog never reappears. /// flag so the dialog never reappears.
fn default_assoc_dialog_window() -> Element<'static, Message> { 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( container(
column![ column![
text("Make Open CAD Studio your default CAD app?") text("Make Open CAD Studio your default CAD app?")
.size(15) .size(15),
.color(TEXT_COL),
iced::widget::Space::new().height(10), 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.") text("Open .dwg and .dxf drawings in Open CAD Studio by default. You can change this later in your system settings.")
.size(12) .size(12)
.color(DIM_COL), .style(dialog_muted_text_style),
iced::widget::Space::new().height(22), iced::widget::Space::new().height(22),
row![ row![
iced::widget::Space::new().width(Fill), 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), 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), .align_y(iced::Center),
] ]
.spacing(0), .spacing(0),
) )
.style(move |_: &Theme| container::Style { .style(dialog_body_style)
background: Some(Background::Color(BG)),
..Default::default()
})
.center(Fill) .center(Fill)
.padding([24, 28]) .padding([24, 28])
.into() .into()

View file

@ -26,19 +26,6 @@ pub(super) fn text_inline_overlay(
ed: &super::super::text_inline::TextInlineState, ed: &super::super::text_inline::TextInlineState,
canvas: (f32, f32), canvas: (f32, f32),
) -> Element<'_, Message> { ) -> 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) let field = text_input("Text", &ed.value)
.id(iced::widget::Id::new(TEXT_INLINE_ID)) .id(iced::widget::Id::new(TEXT_INLINE_ID))
.on_input(Message::TextInlineInput) .on_input(Message::TextInlineInput)
@ -48,14 +35,17 @@ pub(super) fn text_inline_overlay(
.width(iced::Length::Fixed(240.0)); .width(iced::Length::Fixed(240.0));
let panel = container(field) let panel = container(field)
.style(move |_: &Theme| container::Style { .style(move |theme: &Theme| {
background: Some(Background::Color(PANEL_BG)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.weak.color)),
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 5.0.into(), radius: 5.0.into(),
}, },
..Default::default() ..Default::default()
}
}) })
.padding(4); .padding(4);
@ -195,7 +185,7 @@ impl iced::widget::canvas::Program<Message> for MTextPreview {
&self, &self,
_state: &MTextPreviewState, _state: &MTextPreviewState,
renderer: &iced::Renderer, renderer: &iced::Renderer,
_theme: &Theme, theme: &Theme,
bounds: iced::Rectangle, bounds: iced::Rectangle,
_cursor: iced::mouse::Cursor, _cursor: iced::mouse::Cursor,
) -> Vec<iced::widget::canvas::Geometry> { ) -> Vec<iced::widget::canvas::Geometry> {
@ -221,12 +211,7 @@ impl iced::widget::canvas::Program<Message> for MTextPreview {
); );
frame.fill( frame.fill(
&rect, &rect,
Color { theme.extended_palette().primary.base.color.scale_alpha(0.45),
r: 0.20,
g: 0.42,
b: 0.72,
a: 0.45,
},
); );
} }
} }
@ -262,12 +247,7 @@ impl iced::widget::canvas::Program<Message> for MTextPreview {
frame.stroke( frame.stroke(
&path, &path,
Stroke::default() Stroke::default()
.with_color(Color { .with_color(theme.extended_palette().warning.base.color)
r: 0.95,
g: 0.95,
b: 0.55,
a: 1.0,
})
.with_width(1.5), .with_width(1.5),
); );
} else if collapsed { } else if collapsed {
@ -291,12 +271,7 @@ impl iced::widget::canvas::Program<Message> for MTextPreview {
frame.stroke( frame.stroke(
&path, &path,
Stroke::default() Stroke::default()
.with_color(Color { .with_color(theme.extended_palette().warning.base.color)
r: 0.95,
g: 0.95,
b: 0.55,
a: 1.0,
})
.with_width(1.5), .with_width(1.5),
); );
} }
@ -344,59 +319,34 @@ pub(super) fn mtext_editor_overlay<'a>(
modal_resize: iced::Vector, modal_resize: iced::Vector,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
use super::super::mtext_editor::{JustifyChoice, MTextFmt, ParaAlign}; use super::super::mtext_editor::{JustifyChoice, MTextFmt, ParaAlign};
use iced::widget::{canvas, svg}; use iced::widget::canvas;
const BORDER: Color = Color { let btn_style = |theme: &Theme, status: button::Status| {
r: 0.40, let palette = theme.extended_palette();
g: 0.40, let pair = match status {
b: 0.40, button::Status::Hovered | button::Status::Pressed => palette.background.strong,
a: 1.0, _ => palette.background.weak,
}; };
const TEXT_COL: Color = Color { button::Style {
r: 0.88, background: Some(Background::Color(pair.color)),
g: 0.88, text_color: pair.text,
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,
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 3.0.into(), radius: 3.0.into(),
}, },
shadow: iced::Shadow::default(), shadow: iced::Shadow::default(),
snap: false, snap: false,
}
}; };
let icon_btn = move |bytes: &'static [u8], msg: Message| -> Element<'static, Message> { 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) .on_press(msg)
.padding(3) .padding(3)
.style(btn_style) .style(btn_style)
.into() .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, let small_input = |placeholder: &'static str,
val: &str, val: &str,
on: fn(String) -> Message, on: fn(String) -> Message,
@ -640,14 +590,17 @@ pub(super) fn mtext_editor_overlay<'a>(
.width(Fill) .width(Fill)
.height(Fill), .height(Fill),
) )
.style(move |_: &Theme| container::Style { .style(move |theme: &Theme| {
background: Some(Background::Color(FIELD_BG)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.base.color)),
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 3.0.into(), radius: 3.0.into(),
}, },
..Default::default() ..Default::default()
}
}) })
.padding(2) .padding(2)
.width(Fill) .width(Fill)
@ -665,8 +618,10 @@ pub(super) fn mtext_editor_overlay<'a>(
] ]
.align_y(iced::Alignment::Center), .align_y(iced::Alignment::Center),
) )
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(crate::ui::style::style_manager::TB)), background: Some(Background::Color(
theme.extended_palette().background.weak.color
)),
..Default::default() ..Default::default()
}) })
.width(Fill) .width(Fill)
@ -710,50 +665,10 @@ pub(super) fn viewport_context_menu_overlay(
last_cmds: Vec<String>, last_cmds: Vec<String>,
draworder_open: bool, draworder_open: bool,
) -> Element<'static, Message> { ) -> 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> { 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) .on_press(msg)
.style(|_: &Theme, status| button::Style { .style(button::subtle)
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,
})
.padding([4, 12]) .padding([4, 12])
.width(Fill) .width(Fill)
.into() .into()
@ -761,8 +676,10 @@ pub(super) fn viewport_context_menu_overlay(
let sep = || -> Element<'static, Message> { let sep = || -> Element<'static, Message> {
container(iced::widget::Space::new().width(Fill).height(1)) container(iced::widget::Space::new().width(Fill).height(1))
.style(move |_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(SEP_COL)), background: Some(Background::Color(
theme.extended_palette().background.weak.color,
)),
..Default::default() ..Default::default()
}) })
.width(Fill) .width(Fill)
@ -773,18 +690,9 @@ pub(super) fn viewport_context_menu_overlay(
// Indented variant for sub-menu rows (e.g. Draw Order children). // Indented variant for sub-menu rows (e.g. Draw Order children).
let subitem = |label: String, msg: Message| -> Element<'static, Message> { 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) .on_press(msg)
.style(|_: &Theme, status| button::Style { .style(button::subtle)
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,
})
.padding(iced::Padding { .padding(iced::Padding {
top: 4.0, top: 4.0,
right: 12.0, right: 12.0,
@ -827,30 +735,21 @@ pub(super) fn viewport_context_menu_overlay(
)); ));
items.push(sep()); items.push(sep());
let do_caret = if draworder_open { let do_caret = if draworder_open {
crate::ui::icons::arrow_down(9.0, TEXT_COL) crate::ui::icons::themed_arrow_down(9.0)
} else { } else {
crate::ui::icons::arrow_right(9.0, TEXT_COL) crate::ui::icons::themed_arrow_right(9.0)
}; };
items.push( items.push(
button( button(
row![ row![
text("Draw Order").size(12).color(TEXT_COL), text("Draw Order").size(12),
iced::widget::Space::new().width(Fill), iced::widget::Space::new().width(Fill),
do_caret, do_caret,
] ]
.align_y(iced::Center), .align_y(iced::Center),
) )
.on_press(Message::DrawOrderSubmenuToggle) .on_press(Message::DrawOrderSubmenuToggle)
.style(|_: &Theme, status| button::Style { .style(button::subtle)
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,
})
.padding([4, 12]) .padding([4, 12])
.width(Fill) .width(Fill)
.into(), .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_col = column(items).spacing(0).width(iced::Length::Fixed(180.0));
let menu = container(menu_col) let menu = container(menu_col)
.style(move |_: &Theme| container::Style { .style(container::bordered_box)
background: Some(Background::Color(MENU_BG)),
border: Border {
color: MENU_BORDER,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
})
.padding([4, 0]) .padding([4, 0])
.width(iced::Length::Fixed(180.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 /// snap ICONS only — the names show as hover tooltips. Picking one applies
/// that snap to just the next point pick. /// that snap to just the next point pick.
pub(super) fn snap_override_overlay(pos: iced::Point) -> Element<'static, Message> { 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; const COLS: usize = 4;
let cell = |snap_type: crate::snap::SnapType, label: &'static str| -> Element<'static, Message> { let cell = |snap_type: crate::snap::SnapType, label: &'static str| -> Element<'static, Message> {
let icon = container(crate::ui::icons::tinted::<Message>( let icon = container(crate::ui::icons::themed::<Message>(
crate::ui::icons::osnap(snap_type), crate::ui::icons::osnap(snap_type),
16.0, 16.0,
ICON_COLOR,
)) ))
.width(26) .width(26)
.height(26) .height(26)
@ -947,26 +832,33 @@ pub(super) fn snap_override_overlay(pos: iced::Point) -> Element<'static, Messag
.align_y(iced::Center); .align_y(iced::Center);
let btn = button(icon) let btn = button(icon)
.on_press(Message::SnapOverridePick(snap_type)) .on_press(Message::SnapOverridePick(snap_type))
.style(|_: &Theme, status| button::Style { .style(|theme: &Theme, status| button::Style {
background: Some(Background::Color(match status { background: matches!(
button::Status::Hovered | button::Status::Pressed => HOVER, status,
_ => Color::TRANSPARENT, button::Status::Hovered | button::Status::Pressed
})), )
.then_some(Background::Color(
theme.extended_palette().primary.weak.color
)),
border: Border::default(), border: Border::default(),
..Default::default() ..Default::default()
}) })
.padding(2); .padding(2);
iced::widget::tooltip( iced::widget::tooltip(
btn, btn,
container(text(label).size(11).color(Color::WHITE)) container(text(label).size(11))
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(TIP_BG)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.strong.color)),
border: Border { border: Border {
color: PANEL_BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 2.0.into(), radius: 2.0.into(),
}, },
text_color: Some(palette.background.strong.text),
..Default::default() ..Default::default()
}
}) })
.padding([2, 6]), .padding([2, 6]),
iced::widget::tooltip::Position::Bottom, 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) let panel = container(grid)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(PANEL_BG)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.weak.color)),
border: Border { border: Border {
color: PANEL_BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 4.0.into(), radius: 4.0.into(),
}, },
..Default::default() ..Default::default()
}
}) })
.padding(4); .padding(4);
@ -1024,49 +919,6 @@ pub(super) fn qselect_overlay<'a>(
properties: &[(String, String)], properties: &[(String, String)],
) -> Element<'a, Message> { ) -> Element<'a, Message> {
use iced::widget::{checkbox, pick_list}; 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<String> = vec![QSELECT_ANY_TYPE.to_string()]; let mut type_options: Vec<String> = vec![QSELECT_ANY_TYPE.to_string()];
type_options.extend(types.iter().cloned()); type_options.extend(types.iter().cloned());
@ -1111,28 +963,33 @@ pub(super) fn qselect_overlay<'a>(
let label = |s: &'static str| { let label = |s: &'static str| {
text(s) text(s)
.size(12) .size(12)
.color(TEXT)
.width(iced::Length::Fixed(90.0)) .width(iced::Length::Fixed(90.0))
}; };
let btn = |lbl: &'static str, msg: Message, base: Color, hov: Color| { let btn = |lbl: &'static str, msg: Message, primary: bool| {
button(text(lbl).size(12).color(TEXT)) button(text(lbl).size(12))
.on_press(msg) .on_press(msg)
.style(move |_: &Theme, st| button::Style { .style(move |theme: &Theme, st| {
background: Some(Background::Color( let palette = theme.extended_palette();
if matches!(st, button::Status::Hovered | button::Status::Pressed) { let pair = match (
hov primary,
} else { matches!(st, button::Status::Hovered | button::Status::Pressed),
base ) {
}, (true, true) => palette.primary.strong,
)), (true, false) => palette.primary.base,
text_color: TEXT, (false, true) => palette.background.strong,
(false, false) => palette.background.weak,
};
button::Style {
background: Some(Background::Color(pair.color)),
text_color: pair.text,
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 4.0.into(), radius: 4.0.into(),
}, },
..Default::default() ..Default::default()
}
}) })
.padding([4, 14]) .padding([4, 14])
}; };
@ -1143,7 +1000,7 @@ pub(super) fn qselect_overlay<'a>(
} }
let panel_body = column![ let panel_body = column![
text("Quick Select").size(14).color(TEXT), text("Quick Select").size(14),
Space::new().height(10), Space::new().height(10),
row![ row![
label("Object type:"), label("Object type:"),
@ -1198,15 +1055,15 @@ pub(super) fn qselect_overlay<'a>(
.on_toggle(Message::QSelectSetAppend) .on_toggle(Message::QSelectSetAppend)
.size(14), .size(14),
Space::new().width(6), 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), .align_y(iced::Alignment::Center),
Space::new().height(14), Space::new().height(14),
row![ row![
Space::new().width(Fill), Space::new().width(Fill),
btn("Cancel", Message::QSelectClose, BTN_BG, BTN_HOV), btn("Cancel", Message::QSelectClose, false),
Space::new().width(8), Space::new().width(8),
btn("Apply", Message::QSelectApply, BTN_OK, BTN_OK_HOV), btn("Apply", Message::QSelectApply, true),
] ]
.align_y(iced::Alignment::Center), .align_y(iced::Alignment::Center),
] ]
@ -1215,14 +1072,17 @@ pub(super) fn qselect_overlay<'a>(
let panel = container(panel_body) let panel = container(panel_body)
.padding(16) .padding(16)
.width(iced::Length::Fixed(400.0)) .width(iced::Length::Fixed(400.0))
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(BG)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.weak.color)),
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 6.0.into(), radius: 6.0.into(),
}, },
..Default::default() ..Default::default()
}
}); });
// Outside-click catcher — fills the whole screen, sits below the // Outside-click catcher — fills the whole screen, sits below the

View file

@ -3,7 +3,7 @@ use crate::scene::{VIEWCUBE_PX, VIEWCUBE_REGION_PX};
use iced::widget::{ use iced::widget::{
button, container, mouse_area, pick_list, stack, Space, button, container, mouse_area, pick_list, stack, Space,
}; };
use iced::{Background, Border, Color, Element, Theme}; use iced::{Background, Border, Element, Theme};
// ── Render-mode picker ────────────────────────────────────────────────────── // ── Render-mode picker ──────────────────────────────────────────────────────
@ -30,16 +30,14 @@ fn vc_btn<'a>(content: Element<'a, Message>, size: f32, msg: Message) -> Element
) )
.padding(0) .padding(0)
.on_press(msg) .on_press(msg)
.style(|_: &Theme, status| iced::widget::button::Style { .style(|theme: &Theme, status| iced::widget::button::Style {
background: Some(Background::Color(match status { background: matches!(
iced::widget::button::Status::Hovered | iced::widget::button::Status::Pressed => Color { status,
r: 0.45, iced::widget::button::Status::Hovered | iced::widget::button::Status::Pressed
g: 0.62, )
b: 0.95, .then_some(Background::Color(
a: 0.30, theme.extended_palette().primary.weak.color
}, )),
_ => Color::TRANSPARENT,
})),
border: Border { border: Border {
radius: 3.0.into(), radius: 3.0.into(),
..Default::default() ..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> { pub(super) fn viewcube_nav_controls<'a>() -> Element<'a, Message> {
use crate::scene::NudgeDir; use crate::scene::NudgeDir;
use crate::ui::icons; 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 r = VIEWCUBE_REGION_PX;
let c = r * 0.5; let c = r * 0.5;
let cube_half = VIEWCUBE_PX as f32 * 0.36; // VIEWCUBE_PX * VIEWCUBE_SCALE 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( vc_place(
3.0, 3.0,
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( vc_place(
rax, rax,
ray, 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( vc_place(
rbx, rbx,
rby, 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( vc_place(
tux, tux,
tuy, tuy,
vc_btn( vc_btn(
icons::arrow_down(8.0, tint), icons::themed_arrow_down(8.0),
TRI, TRI,
Message::ViewCubeNudge(NudgeDir::Up) Message::ViewCubeNudge(NudgeDir::Up)
) )
@ -119,7 +111,7 @@ pub(super) fn viewcube_nav_controls<'a>() -> Element<'a, Message> {
tdx, tdx,
tdy, tdy,
vc_btn( vc_btn(
icons::arrow_up(8.0, tint), icons::themed_arrow_up(8.0),
TRI, TRI,
Message::ViewCubeNudge(NudgeDir::Down) Message::ViewCubeNudge(NudgeDir::Down)
) )
@ -128,7 +120,7 @@ pub(super) fn viewcube_nav_controls<'a>() -> Element<'a, Message> {
tlx, tlx,
tly, tly,
vc_btn( vc_btn(
icons::arrow_right(8.0, tint), icons::themed_arrow_right(8.0),
TRI, TRI,
Message::ViewCubeNudge(NudgeDir::Left) Message::ViewCubeNudge(NudgeDir::Left)
) )
@ -137,7 +129,7 @@ pub(super) fn viewcube_nav_controls<'a>() -> Element<'a, Message> {
trx, trx,
try_, try_,
vc_btn( vc_btn(
icons::arrow_left(8.0, tint), icons::themed_arrow_left(8.0),
TRI, TRI,
Message::ViewCubeNudge(NudgeDir::Right) 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. /// The WCS / named-UCS selector shown under the cube.
pub(super) fn viewcube_ucs_picker<'a>(current: String, names: Vec<String>) -> Element<'a, Message> { pub(super) fn viewcube_ucs_picker<'a>(current: String, names: Vec<String>) -> 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()]; let mut options = vec!["WCS".to_string()];
options.extend(names); options.extend(names);
let selected = if current.is_empty() { let selected = if current.is_empty() {
@ -174,20 +160,20 @@ pub(super) fn viewcube_ucs_picker<'a>(current: String, names: Vec<String>) -> El
// Fixed width so the caller can centre it under the cube centre with a // 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). // simple half-width offset (content-sized width would drift off-centre).
.width(iced::Length::Fixed(UCS_PICKER_W)) .width(iced::Length::Fixed(UCS_PICKER_W))
.style(move |_: &Theme, _| iced::widget::pick_list::Style { .style(move |theme: &Theme, _| {
background: Background::Color(Color { let palette = theme.extended_palette();
r: 0.16, iced::widget::pick_list::Style {
g: 0.17, background: Background::Color(palette.background.weak.color),
b: 0.20,
a: 0.92,
}),
border: Border { border: Border {
radius: 3.0.into(), radius: 3.0.into(),
color: palette.background.neutral.color,
width: 1.0,
..Default::default() ..Default::default()
}, },
text_color: light, text_color: palette.background.base.text,
placeholder_color: light, placeholder_color: palette.background.base.text.scale_alpha(0.68),
handle_color: light, handle_color: palette.background.base.text,
}
}) })
.into() .into()
} }

View file

@ -20,25 +20,6 @@ pub struct ColorExtras {
pub by_block: bool, 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 /// 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). /// (ByBlock=0, ByLayer=256, indexed 1-255; RGB has no ACI slot → ByLayer).
pub fn color_to_aci_string(c: AcadColor) -> String { 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. /// A small colour square.
fn swatch<'a>(bg: Color) -> Element<'a, Message> { fn swatch<'a>(bg: Color) -> Element<'a, Message> {
container(text("").width(13).height(13)) container(text("").width(13).height(13))
.style(move |_: &Theme| container::Style { .style(move |theme: &Theme| container::Style {
background: Some(Background::Color(bg)), background: Some(Background::Color(bg)),
border: Border { border: Border {
color: Color { color: theme.extended_palette().background.neutral.color,
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.5,
},
width: 1.0, width: 1.0,
radius: 2.0.into(), radius: 2.0.into(),
}, },
@ -126,8 +102,8 @@ pub fn color_selector<'a>(
let head = button( let head = button(
row![ row![
swatch(cur_bg), swatch(cur_bg),
text(cur_name).size(11).color(TEXT), text(cur_name).size(11),
crate::ui::icons::arrow_toggle(open, 9.0, TEXT), crate::ui::icons::themed_arrow_toggle(open, 9.0),
] ]
.spacing(5) .spacing(5)
.align_y(iced::Center), .align_y(iced::Center),
@ -141,14 +117,17 @@ pub fn color_selector<'a>(
} }
let popup = container(color_list(extras, on_select, on_more)) let popup = container(color_list(extras, on_select, on_more))
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(PICKER_BG)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.weak.color)),
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 2.0.into(), radius: 2.0.into(),
}, },
..Default::default() ..Default::default()
}
}) })
.padding(5) .padding(5)
.width(220); .width(220);
@ -172,19 +151,15 @@ pub fn color_list<'a>(
let named_row = |color: AcadColor| -> Element<'a, Message> { let named_row = |color: AcadColor| -> Element<'a, Message> {
let (bg, name) = acad_color_display(color); let (bg, name) = acad_color_display(color);
button( button(
row![swatch(bg), text(name).size(11).color(TEXT)] row![swatch(bg), text(name).size(11)]
.spacing(5) .spacing(5)
.align_y(iced::Center), .align_y(iced::Center),
) )
.on_press(on_select(color)) .on_press(on_select(color))
.style(|_: &Theme, status| button::Style { .style(|theme: &Theme, status| button::Style {
background: matches!(status, button::Status::Hovered) background: matches!(status, button::Status::Hovered).then_some(
.then_some(Background::Color(Color { Background::Color(theme.extended_palette().background.strong.color)
r: 0.25, ),
g: 0.25,
b: 0.30,
a: 1.0,
})),
..Default::default() ..Default::default()
}) })
.padding([2, 4]) .padding([2, 4])
@ -203,16 +178,12 @@ pub fn color_list<'a>(
list = list.push(named_row(AcadColor::Index(i))); list = list.push(named_row(AcadColor::Index(i)));
} }
list = list.push( list = list.push(
button(text("More…").size(11).color(TEXT)) button(text("More…").size(11))
.on_press(on_more) .on_press(on_more)
.style(|_: &Theme, status| button::Style { .style(|theme: &Theme, status| button::Style {
background: matches!(status, button::Status::Hovered) background: matches!(status, button::Status::Hovered).then_some(
.then_some(Background::Color(Color { Background::Color(theme.extended_palette().background.strong.color)
r: 0.25, ),
g: 0.25,
b: 0.30,
a: 1.0,
})),
..Default::default() ..Default::default()
}) })
.padding([2, 4]) .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 chip = |color: AcadColor, label: &'static str| -> Element<'static, Message> {
let (bg, _) = acad_color_display(color); let (bg, _) = acad_color_display(color);
button( button(
row![swatch(bg), text(label).size(11).color(TEXT)] row![swatch(bg), text(label).size(11)]
.spacing(5) .spacing(5)
.align_y(iced::Center), .align_y(iced::Center),
) )
@ -250,18 +221,13 @@ pub fn color_grid_window(on_pick: impl Fn(AcadColor) -> Message) -> Element<'sta
r = r.push( r = r.push(
button(text("").width(18).height(18)) button(text("").width(18).height(18))
.on_press(on_pick(AcadColor::Index(ci))) .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)), background: Some(Background::Color(bg)),
border: Border { border: Border {
color: if matches!(status, button::Status::Hovered) { color: if matches!(status, button::Status::Hovered) {
Color::WHITE theme.extended_palette().primary.base.color
} else { } else {
Color { theme.extended_palette().background.neutral.color
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.4,
}
}, },
width: if matches!(status, button::Status::Hovered) { width: if matches!(status, button::Status::Hovered) {
1.5 1.5
@ -281,14 +247,16 @@ pub fn color_grid_window(on_pick: impl Fn(AcadColor) -> Message) -> Element<'sta
container( container(
column![ column![
text("Select Color").size(13).color(TEXT), text("Select Color").size(13),
row![chip(AcadColor::ByLayer, "ByLayer"), chip(AcadColor::ByBlock, "ByBlock")].spacing(6), row![chip(AcadColor::ByLayer, "ByLayer"), chip(AcadColor::ByBlock, "ByBlock")].spacing(6),
scrollable(grid).height(Length::Fill), scrollable(grid).height(Length::Fill),
] ]
.spacing(8), .spacing(8),
) )
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(PICKER_BG)), background: Some(Background::Color(
theme.extended_palette().background.weak.color
)),
..Default::default() ..Default::default()
}) })
.padding(10) .padding(10)

View file

@ -370,11 +370,14 @@ impl CommandLine {
let history_rows = visible[start..] let history_rows = visible[start..]
.iter() .iter()
.fold(column![].spacing(0), |col, entry| { .fold(column![].spacing(0), |col, entry| {
let color = match entry.kind { let kind = entry.kind.clone();
EntryKind::Command => CMD_COLOR, let entry_text = |value: String| {
EntryKind::Output => OUT_COLOR, text(value).size(11).style({
EntryKind::Error => ERR_COLOR, let kind = kind.clone();
EntryKind::Info => INFO_COLOR, 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 // The current step's prompt is the single pinned line. When the
// step offers options, render them as clickable buttons inline // 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). // listing is dropped here (the history log keeps the full text).
if entry.pinned && !self.step_options.is_empty() { if entry.pinned && !self.step_options.is_empty() {
let shown = strip_option_listing(&entry.text); 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) .spacing(6)
.align_y(iced::Center); .align_y(iced::Center);
for opt in &self.step_options { for opt in &self.step_options {
let btn = button( let btn = button(text(opt.label.to_uppercase()).size(11))
text(opt.label.to_uppercase()).size(11).color(CMD_COLOR),
)
.on_press(Message::CommandOptionPick(opt.keyword.clone())) .on_press(Message::CommandOptionPick(opt.keyword.clone()))
.padding([1, 6]) .padding([1, 6])
.style(|_: &Theme, status| { .style(|theme: &Theme, status| {
let bg = if matches!(status, button::Status::Hovered) { let palette = theme.extended_palette();
Color { let pair = if matches!(
r: 0.28, status,
g: 0.40, button::Status::Hovered | button::Status::Pressed
b: 0.56, ) {
a: 1.0, palette.primary.weak
}
} else { } else {
INPUT_ROW_BG palette.background.weakest
}; };
button::Style { button::Style {
background: Some(Background::Color(bg)), background: Some(Background::Color(pair.color)),
text_color: Color::WHITE, text_color: pair.text,
border: Border { border: Border {
color: BORDER_COLOR, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 3.0.into(), radius: 3.0.into(),
}, },
@ -419,46 +419,41 @@ impl CommandLine {
} }
col.push(container(r).padding([1, 8])) col.push(container(r).padding([1, 8]))
} else { } 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 // Literal-space toggle: while active, every line behaves as if it
// started with `>` — Space stays in the line instead of submitting, so // started with `>` — Space stays in the line instead of submitting, so
// arguments with spaces (text strings, paths, `UCS Z 90` as one line) // arguments with spaces (text strings, paths, `UCS Z 90` as one line)
// can be typed. Persists until toggled off; a hand-typed leading `>` // can be typed. Persists until toggled off; a hand-typed leading `>`
// lights the button too (same mode, one line only). // lights the button too (same mode, one line only).
let literal_active = self.literal_spaces || self.input.starts_with('>'); let literal_active = self.literal_spaces || self.input.starts_with('>');
let literal_btn = button(text(">").size(11).color(if literal_active { let literal_btn = button(text(">").size(11))
Color::WHITE
} else {
PROMPT_COLOR
}))
.on_press(Message::CommandLiteralToggle) .on_press(Message::CommandLiteralToggle)
.padding([2, 6]) .padding([2, 6])
.style(move |_: &Theme, status| { .style(move |theme: &Theme, status| {
let bg = if literal_active { let palette = theme.extended_palette();
Color { let pair = if literal_active {
r: 0.28, palette.primary.weak
g: 0.40, } else if matches!(
b: 0.56, status,
a: 1.0, button::Status::Hovered | button::Status::Pressed
} ) {
} else if matches!(status, button::Status::Hovered) { palette.background.weak
Color {
r: 0.22,
g: 0.30,
b: 0.42,
a: 1.0,
}
} else { } else {
INPUT_ROW_BG palette.background.weakest
}; };
button::Style { button::Style {
background: Some(Background::Color(bg)), background: Some(Background::Color(pair.color)),
text_color: Color::WHITE, text_color: pair.text,
border: Border { border: Border {
color: BORDER_COLOR, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 3.0.into(), radius: 3.0.into(),
}, },
@ -467,19 +462,10 @@ impl CommandLine {
}); });
let literal_tip = container( 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.") 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) .size(11),
.color(Color::WHITE),
) )
.padding([3, 6]) .padding([3, 6])
.style(|_: &Theme| container::Style { .style(container::bordered_box);
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: BORDER_COLOR,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
});
let literal_btn = tooltip(literal_btn, literal_tip, tooltip::Position::Top).gap(4); let literal_btn = tooltip(literal_btn, literal_tip, tooltip::Position::Top).gap(4);
// While dynamic input is capturing keystrokes, the command-line // While dynamic input is capturing keystrokes, the command-line
// text field is left without an `on_input` handler so it can't // text field is left without an `on_input` handler so it can't
@ -492,33 +478,6 @@ impl CommandLine {
.on_submit(Message::CommandSubmit); .on_submit(Message::CommandSubmit);
} }
let input = input 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) .size(11)
.padding([4, 6]); .padding([4, 6]);
// Autocomplete suggestions panel, shown above the input row // Autocomplete suggestions panel, shown above the input row
@ -537,31 +496,25 @@ impl CommandLine {
let mut col = column![].spacing(0).width(Length::Fill); let mut col = column![].spacing(0).width(Length::Fill);
for (idx, cmd) in matches.iter().enumerate() { for (idx, cmd) in matches.iter().enumerate() {
let is_selected = cursor == idx; 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())) .on_press(Message::CommandSuggestionPick(cmd.clone()))
.width(Length::Fill) .width(Length::Fill)
.padding([2, 8]) .padding([2, 8])
.style(move |_: &Theme, status| { .style(move |theme: &Theme, status| {
let bg = if is_selected { let palette = theme.extended_palette();
Color { let pair = if is_selected {
r: 0.28, palette.primary.weak
g: 0.40, } else if matches!(
b: 0.56, status,
a: 1.0, button::Status::Hovered | button::Status::Pressed
} ) {
} else if matches!(status, button::Status::Hovered) { palette.background.weak
Color {
r: 0.22,
g: 0.30,
b: 0.42,
a: 1.0,
}
} else { } else {
PANEL_BG palette.background.base
}; };
button::Style { button::Style {
background: Some(Background::Color(bg)), background: Some(Background::Color(pair.color)),
text_color: Color::WHITE, text_color: pair.text,
border: Border::default(), border: Border::default(),
..Default::default() ..Default::default()
} }
@ -569,15 +522,7 @@ impl CommandLine {
col = col.push(row); col = col.push(row);
} }
container(col) container(col)
.style(|_: &Theme| container::Style { .style(container::bordered_box)
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: BORDER_COLOR,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
})
.width(Length::Fill) .width(Length::Fill)
.into() .into()
} }
@ -590,22 +535,13 @@ impl CommandLine {
// user can recover anything that has already faded off the // user can recover anything that has already faded off the
// overlay. // overlay.
let dropdown_icon = if self.history_open { 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 { } else {
crate::ui::icons::arrow_right(11.0, PROMPT_COLOR) crate::ui::icons::themed_arrow_right(11.0)
}; };
let dropdown_btn = button(dropdown_icon) let dropdown_btn = button(dropdown_icon)
.on_press(Message::CommandHistoryToggle) .on_press(Message::CommandHistoryToggle)
.style(|_: &Theme, _status| button::Style { .style(button::subtle)
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()
})
.padding([2, 6]); .padding([2, 6]);
let input_row = row![prompt, literal_btn, input, dropdown_btn] let input_row = row![prompt, literal_btn, input, dropdown_btn]
.spacing(4) .spacing(4)
@ -625,23 +561,21 @@ impl CommandLine {
.size(11) .size(11)
.padding([2, 8]) .padding([2, 8])
.max_height(180.0) .max_height(180.0)
.style(|_: &Theme, _status| text_editor::Style { .style(|theme: &Theme, _status| {
background: Background::Color(PANEL_BG), let palette = theme.extended_palette();
border: Border::default(), text_editor::Style {
placeholder: OUT_COLOR, background: Background::Color(palette.background.base.color),
value: CMD_COLOR, border: Border::default(),
selection: Color { placeholder: palette.background.base.text.scale_alpha(0.72),
r: 0.20, value: palette.background.base.text,
g: 0.44, selection: palette.primary.base.color.scale_alpha(0.5),
b: 0.72, }
a: 0.5,
},
}); });
// Header strip: a Copy-all and a Clear button pinned above the log. // Header strip: a Copy-all and a Clear button pinned above the log.
let copy_btn = button( let copy_btn = button(
row![ row![
crate::ui::icons::tinted(crate::ui::icons::COPY, 11.0, PROMPT_COLOR), crate::ui::icons::themed_success(crate::ui::icons::COPY, 11.0),
text("Copy").size(11).color(CMD_COLOR), text("Copy").size(11),
] ]
.spacing(4) .spacing(4)
.align_y(iced::Center), .align_y(iced::Center),
@ -651,8 +585,8 @@ impl CommandLine {
.padding([2, 6]); .padding([2, 6]);
let clear_btn = button( let clear_btn = button(
row![ row![
crate::ui::icons::tinted(crate::ui::icons::TRASH, 11.0, ERR_COLOR), crate::ui::icons::themed_warning(crate::ui::icons::TRASH, 11.0),
text("Clear").size(11).color(CMD_COLOR), text("Clear").size(11),
] ]
.spacing(4) .spacing(4)
.align_y(iced::Center), .align_y(iced::Center),
@ -668,15 +602,7 @@ impl CommandLine {
.width(Length::Fill) .width(Length::Fill)
.padding([2, 6]); .padding([2, 6]);
let panel = container(column![header, log]) let panel = container(column![header, log])
.style(|_: &Theme| container::Style { .style(container::bordered_box)
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: BORDER_COLOR,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
})
.width(Length::Fill) .width(Length::Fill)
.padding([4, 0]); .padding([4, 0]);
opaque(panel).into() opaque(panel).into()
@ -688,35 +614,43 @@ impl CommandLine {
autocomplete, autocomplete,
dropdown, dropdown,
container(history_rows) container(history_rows)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(HISTORY_BG)), background: Some(Background::Color(
theme.extended_palette().background.base.color,
)),
..Default::default() ..Default::default()
}) })
.width(Length::Fill) .width(Length::Fill)
.padding([2, 0]), .padding([2, 0]),
container(input_row) container(input_row)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(INPUT_ROW_BG)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.weakest.color)),
border: Border { border: Border {
color: BORDER_COLOR, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 3.0.into() radius: 3.0.into()
}, },
..Default::default() ..Default::default()
}
}) })
.width(Length::Fill) .width(Length::Fill)
// Match the drawing tab bar / status bar height, and vertically // Match the drawing tab bar / status bar height, and vertically
// centre the prompt/input/dropdown within it (issue #216). // centre the prompt/input/dropdown within it (issue #216).
.center_y(Length::Fixed(30.0)), .center_y(Length::Fixed(30.0)),
]) ])
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(PANEL_BG)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.base.color)),
border: Border { border: Border {
color: BORDER_COLOR, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 4.0.into(), radius: 4.0.into(),
}, },
..Default::default() ..Default::default()
}
}) })
.width(Length::Fixed(720.0)) .width(Length::Fixed(720.0))
.into() .into()
@ -780,22 +714,18 @@ pub fn ranked_matches(
/// Flat button style for the history dropdown's Copy / Clear strip: a subtle /// Flat button style for the history dropdown's Copy / Clear strip: a subtle
/// filled pill that brightens on hover. /// filled pill that brightens on hover.
fn header_btn_style(_: &Theme, status: button::Status) -> button::Style { fn header_btn_style(theme: &Theme, status: button::Status) -> button::Style {
let bg = if matches!(status, button::Status::Hovered) { let palette = theme.extended_palette();
Color { let pair = if matches!(status, button::Status::Hovered | button::Status::Pressed) {
r: 0.24, palette.background.weak
g: 0.24,
b: 0.24,
a: 1.0,
}
} else { } else {
INPUT_ROW_BG palette.background.weakest
}; };
button::Style { button::Style {
background: Some(Background::Color(bg)), background: Some(Background::Color(pair.color)),
text_color: Color::WHITE, text_color: pair.text,
border: Border { border: Border {
color: BORDER_COLOR, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 3.0.into(), radius: 3.0.into(),
}, },
@ -803,66 +733,15 @@ fn header_btn_style(_: &Theme, status: button::Status) -> button::Style {
} }
} }
const PANEL_BG: Color = Color { fn history_color(theme: &Theme, kind: &EntryKind) -> Color {
r: 0.15, let palette = theme.extended_palette();
g: 0.15, match kind {
b: 0.15, EntryKind::Command => palette.background.base.text,
a: 1.0, EntryKind::Output => palette.background.base.text.scale_alpha(0.72),
}; EntryKind::Error => palette.danger.base.color,
const HISTORY_BG: Color = Color { EntryKind::Info => palette.primary.base.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,
};
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {

View file

@ -7,7 +7,7 @@
//! boxes. Drawing them from SVG instead makes the chrome font-independent. //! boxes. Drawing them from SVG instead makes the chrome font-independent.
use iced::widget::{container, svg, Space}; 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_DOWN: &[u8] = include_bytes!("../../assets/icons/ui/tri_down.svg");
const TRI_UP: &[u8] = include_bytes!("../../assets/icons/ui/tri_up.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 PRINT: &[u8] = include_bytes!("../../assets/icons/ui/print.svg");
pub const HEART: &[u8] = include_bytes!("../../assets/icons/ui/heart.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 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"); pub const ARROW_LONG_RIGHT: &[u8] = include_bytes!("../../assets/icons/ui/arrow_long_right.svg");
// ── Status-bar toggle icons (issue #216) ────────────────────────────────── // ── 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_SELCYCLE: &[u8] = include_bytes!("../../assets/icons/status/selcycle.svg");
pub const ST_CLEANSCREEN: &[u8] = include_bytes!("../../assets/icons/status/cleanscreen.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`. /// Render a chrome icon with the active Iced theme's normal text color.
pub fn tinted<'a, M: 'a>(bytes: &'static [u8], size: f32, color: Color) -> Element<'a, M> { pub fn themed<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
svg(svg::Handle::from_memory(bytes)) svg(svg::Handle::from_memory(bytes))
.width(size) .width(size)
.height(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() .into()
} }
/// Backwards-compatible alias used by the caret/undo/redo helpers below. /// Render secondary chrome with the active Iced theme's text color.
fn icon<'a, M: 'a>(bytes: &'static [u8], size: f32, color: Color) -> Element<'a, M> { pub fn themed_secondary<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
tinted(bytes, size, color) 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 /// Render disabled chrome with the active Iced theme's text color.
/// when `active`, otherwise an empty spacer that preserves alignment. Used by pub fn themed_disabled<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
/// the many dropdown / popup list rows that mark the selected entry. svg(svg::Handle::from_memory(bytes))
pub fn check_cell<'a, M: 'a>(active: bool, color: Color) -> Element<'a, M> { .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 { let inner: Element<'a, M> = if active {
tinted(CHECK, 11.0, color) themed_primary(CHECK, 11.0)
} else { } else {
Space::new().width(0).into() Space::new().width(0).into()
}; };
@ -166,46 +251,59 @@ pub fn layer_lock(locked: bool) -> &'static [u8] {
} }
} }
/// Downward dropdown caret (replaces `▾`). pub fn themed_arrow_down<'a, M: 'a>(size: f32) -> Element<'a, M> {
pub fn arrow_down<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> { themed(TRI_DOWN, size)
icon(TRI_DOWN, size, color)
} }
/// Upward dropdown caret, shown when a dropdown is open (replaces `▲`). pub fn themed_arrow_up<'a, M: 'a>(size: f32) -> Element<'a, M> {
pub fn arrow_up<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> { themed(TRI_UP, size)
icon(TRI_UP, size, color)
} }
/// Rightward caret for a collapsed item (replaces `▸`). pub fn themed_arrow_right<'a, M: 'a>(size: f32) -> Element<'a, M> {
pub fn arrow_right<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> { themed(TRI_RIGHT, size)
icon(TRI_RIGHT, size, color)
} }
/// Leftward caret. pub fn themed_arrow_left<'a, M: 'a>(size: f32) -> Element<'a, M> {
pub fn arrow_left<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> { themed(TRI_LEFT, size)
icon(TRI_LEFT, size, color)
} }
/// House glyph — the ViewCube "home view" button. pub fn themed_primary_arrow_down<'a, M: 'a>(size: f32) -> Element<'a, M> {
pub fn home<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> { themed_primary(TRI_DOWN, size)
icon(HOME, size, color) }
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`. /// 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 { if open {
arrow_up(size, color) themed_arrow_up(size)
} else { } else {
arrow_down(size, color) themed_arrow_down(size)
} }
} }
/// Undo curved arrow (replaces `↶`). pub fn themed_undo<'a, M: 'a>(size: f32, enabled: bool) -> Element<'a, M> {
pub fn undo<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> { if enabled {
icon(UNDO, size, color) themed(UNDO, size)
} else {
themed_disabled(UNDO, size)
}
} }
/// Redo curved arrow (replaces `↷`). pub fn themed_redo<'a, M: 'a>(size: f32, enabled: bool) -> Element<'a, M> {
pub fn redo<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> { if enabled {
icon(REDO, size, color) themed(REDO, size)
} else {
themed_disabled(REDO, size)
}
} }

View file

@ -7,34 +7,7 @@
use crate::app::Message; use crate::app::Message;
use iced::widget::{button, column, container, mouse_area, opaque, row, stack}; use iced::widget::{button, column, container, mouse_area, opaque, row, stack};
use iced::{Background, Border, Color, Element, Length, Padding, Theme, Vector}; use iced::{Background, Border, 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,
};
/// Stack `content` over `base` behind a dimmed backdrop, framed with a /// Stack `content` over `base` behind a dimmed backdrop, framed with a
/// draggable title bar (the ✕ close button at its right end). The backdrop only /// draggable title bar (the ✕ close button at its right end). The backdrop only
@ -52,15 +25,9 @@ pub fn modal<'a>(
offset: Vector, offset: Vector,
resizable: bool, resizable: bool,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
let close = button(crate::ui::icons::tinted( let close = button(crate::ui::icons::themed_danger(
crate::ui::icons::CLOSE, crate::ui::icons::CLOSE,
13.0, 13.0,
Color {
r: 0.85,
g: 0.85,
b: 0.85,
a: 1.0,
},
)) ))
.on_press(on_close) .on_press(on_close)
.padding([1, 7]) .padding([1, 7])
@ -71,10 +38,12 @@ pub fn modal<'a>(
// screen width — the dialog stays sized to its content. Pressing the grip // screen width — the dialog stays sized to its content. Pressing the grip
// starts a drag (handled in `update`). // starts a drag (handled in `update`).
let grip = mouse_area( 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]) .padding([1, 7])
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(TITLE_C)), background: Some(Background::Color(
theme.extended_palette().background.weakest.color,
)),
border: Border { border: Border {
radius: 4.0.into(), radius: 4.0.into(),
..Default::default() ..Default::default()
@ -89,12 +58,7 @@ pub fn modal<'a>(
// overlaid at the right edge. The bar takes an explicit `title_width` // 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 caller's content width) instead of `Fill` — a Fill child inside
// the Shrink frame would blow the dialog out to the full screen. // the Shrink frame would blow the dialog out to the full screen.
let title_text = iced::widget::text(title).size(15).color(Color { let title_text = iced::widget::text(title).size(15);
r: 0.88,
g: 0.88,
b: 0.88,
a: 1.0,
});
let title_bar = stack![ let title_bar = stack![
container(title_text) container(title_text)
.width(Length::Fixed(title_width)) .width(Length::Fixed(title_width))
@ -108,10 +72,12 @@ pub fn modal<'a>(
.align_y(iced::alignment::Vertical::Center), .align_y(iced::alignment::Vertical::Center),
]; ];
let panel_style = |_: &Theme| container::Style { let panel_style = |theme: &Theme| container::Style {
background: Some(Background::Color(PANEL)), background: Some(Background::Color(
theme.extended_palette().background.base.color,
)),
border: Border { border: Border {
color: BORDER_C, color: theme.extended_palette().background.neutral.color,
width: 1.0, width: 1.0,
radius: 6.0.into(), radius: 6.0.into(),
}, },
@ -125,7 +91,7 @@ pub fn modal<'a>(
// top-right above the content either way (`align_x(Right)`). // top-right above the content either way (`align_x(Right)`).
let body = if resizable { let body = if resizable {
let resize = mouse_area( 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]), .padding([0, 2]),
) )
.on_press(Message::ModalResizeGrab) .on_press(Message::ModalResizeGrab)
@ -157,11 +123,15 @@ pub fn modal<'a>(
.center_x(Length::Fill) .center_x(Length::Fill)
.center_y(Length::Fill) .center_y(Length::Fill)
.padding(pad) .padding(pad)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(Color { background: Some(Background::Color(
a: 0.55, theme
..Color::BLACK .extended_palette()
})), .background
.strongest
.color
.scale_alpha(0.55),
)),
..Default::default() ..Default::default()
}), }),
) )
@ -177,24 +147,15 @@ pub fn modal<'a>(
.into() .into()
} }
fn close_style(_: &Theme, status: button::Status) -> button::Style { fn close_style(theme: &Theme, status: button::Status) -> button::Style {
let bg = match status { let palette = theme.extended_palette();
button::Status::Hovered | button::Status::Pressed => Color { let pair = match status {
r: 0.7, button::Status::Hovered | button::Status::Pressed => palette.danger.strong,
g: 0.2, _ => palette.background.weakest,
b: 0.2,
a: 1.0,
},
_ => Color {
r: 0.25,
g: 0.25,
b: 0.25,
a: 1.0,
},
}; };
button::Style { button::Style {
background: Some(Background::Color(bg)), background: Some(Background::Color(pair.color)),
text_color: Color::WHITE, text_color: pair.text,
border: Border { border: Border {
radius: 4.0.into(), radius: 4.0.into(),
..Default::default() ..Default::default()

View file

@ -292,7 +292,7 @@ struct SelectionCanvas {
hover_locked: bool, 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 sp = grip.pos;
let h = crate::scene::pick::grip::GRIP_HALF_PX; let h = crate::scene::pick::grip::GRIP_HALF_PX;
let path = match grip.shape { let path = match grip.shape {
@ -332,30 +332,13 @@ fn draw_grip_marker(frame: &mut canvas::Frame, grip: &GripMarker) {
}; };
if grip.is_hot { if grip.is_hot {
frame.fill( frame.fill(&path, theme.extended_palette().danger.base.color);
&path,
Color {
r: 1.0,
g: 0.15,
b: 0.10,
a: 1.0,
},
);
} else { } else {
let color = Color { let palette = theme.extended_palette();
r: 0.10, let color = palette.primary.base.color;
g: 0.45,
b: 0.90,
a: 1.0,
};
frame.fill( frame.fill(
&path, &path,
Color { palette.background.base.color.scale_alpha(0.7),
r: 0.10,
g: 0.10,
b: 0.20,
a: 0.7,
},
); );
frame.stroke( frame.stroke(
&path, &path,
@ -442,7 +425,7 @@ impl canvas::Program<Message> for SelectionCanvas {
&self, &self,
_state: &(), _state: &(),
renderer: &iced::Renderer, renderer: &iced::Renderer,
_theme: &Theme, theme: &Theme,
bounds: iced::Rectangle, bounds: iced::Rectangle,
cursor: mouse::Cursor, cursor: mouse::Cursor,
) -> Vec<canvas::Geometry> { ) -> Vec<canvas::Geometry> {
@ -452,18 +435,13 @@ impl canvas::Program<Message> for SelectionCanvas {
// Filled bars in the pane_grid spacing gaps, so adjacent panes read as // Filled bars in the pane_grid spacing gaps, so adjacent panes read as
// distinct viewports. Drawn first so all other overlays sit on top. // distinct viewports. Drawn first so all other overlays sit on top.
if !self.dividers.is_empty() { if !self.dividers.is_empty() {
const DIVIDER: Color = Color { let divider = theme.extended_palette().background.neutral.color;
r: 0.46,
g: 0.52,
b: 0.62,
a: 1.0,
};
for d in &self.dividers { for d in &self.dividers {
let bar = canvas::Path::rectangle( let bar = canvas::Path::rectangle(
Point::new(d.x, d.y), Point::new(d.x, d.y),
iced::Size::new(d.width.max(1.0), d.height.max(1.0)), 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<Message> for SelectionCanvas {
// under the cursor, and drag a translucent ghost card along the cursor // under the cursor, and drag a translucent ghost card along the cursor
// so the pane is visibly "moving". // so the pane is visibly "moving".
if let Some(src) = self.pane_move_rect { if let Some(src) = self.pane_move_rect {
let accent = Color { let accent = theme.extended_palette().primary.base.color;
r: 0.30,
g: 0.62,
b: 1.0,
a: 1.0,
};
// Source pane: dimmed + dashed-feel outline (it has been lifted). // Source pane: dimmed + dashed-feel outline (it has been lifted).
let src_path = let src_path =
canvas::Path::rectangle(Point::new(src.x, src.y), iced::Size::new(src.width, src.height)); canvas::Path::rectangle(Point::new(src.x, src.y), iced::Size::new(src.width, src.height));
frame.fill( frame.fill(
&src_path, &src_path,
Color { theme.extended_palette().background.strong.color.scale_alpha(0.28),
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.28,
},
); );
frame.stroke( frame.stroke(
&src_path, &src_path,
@ -539,18 +507,20 @@ impl canvas::Program<Message> for SelectionCanvas {
// Draw a selection marquee (green crossing / blue window) as a filled, // Draw a selection marquee (green crossing / blue window) as a filled,
// stroked rectangle between two canvas points. Shared by the live // stroked rectangle between two canvas points. Shared by the live
// box-selection and the preview-only window marquee (#291). // box-selection and the preview-only window marquee (#291).
fn draw_marquee(frame: &mut canvas::Frame, a: Point, b: Point, crossing: bool) { fn draw_marquee(
let (fill, stroke) = if crossing { frame: &mut canvas::Frame,
( a: Point,
Color { r: 0.20, g: 0.72, b: 0.44, a: 0.12 }, b: Point,
Color { r: 0.20, g: 0.72, b: 0.44, a: 0.9 }, crossing: bool,
) theme: &Theme,
) {
let base = if crossing {
theme.extended_palette().success.base.color
} else { } else {
( theme.extended_palette().primary.base.color
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 },
)
}; };
let fill = base.scale_alpha(0.12);
let stroke = base.scale_alpha(0.9);
let x0 = a.x.min(b.x); let x0 = a.x.min(b.x);
let y0 = a.y.min(b.y); let y0 = a.y.min(b.y);
let w = (a.x - b.x).abs(); let w = (a.x - b.x).abs();
@ -568,45 +538,21 @@ impl canvas::Program<Message> for SelectionCanvas {
} }
if let (Some(a), Some(b)) = (self.selection.box_anchor, self.selection.box_current) { 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. // Preview marquee for point-picked windows (STRETCH) — same look, no pick.
if let Some((a, b, crossing)) = self.selection.preview_box { 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 { if self.selection.poly_active && self.selection.poly_points.len() > 1 {
let (fill, stroke) = if self.selection.poly_crossing { let base = if self.selection.poly_crossing {
( theme.extended_palette().success.base.color
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,
},
)
} else { } else {
( theme.extended_palette().primary.base.color
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,
},
)
}; };
let fill = base.scale_alpha(0.12);
let stroke = base.scale_alpha(0.9);
if let Some(cur) = self.selection.last_move_pos { if let Some(cur) = self.selection.last_move_pos {
let start = self.selection.poly_points[0]; let start = self.selection.poly_points[0];
let fill_path = canvas::Path::new(|p| { let fill_path = canvas::Path::new(|p| {
@ -667,7 +613,7 @@ impl canvas::Program<Message> for SelectionCanvas {
}; };
frame.with_clip(grip_clip, |frame| { frame.with_clip(grip_clip, |frame| {
for grip in &self.grips { for grip in &self.grips {
draw_grip_marker(frame, grip); draw_grip_marker(frame, grip, theme);
} }
}); });
} }
@ -962,12 +908,12 @@ impl canvas::Program<Message> for SelectionCanvas {
// PAN mode replaces the crosshair with a hand cursor. // PAN mode replaces the crosshair with a hand cursor.
if !over_viewcube && !over_divider && !self.pan_mode && !self.suppressed { if !over_viewcube && !over_divider && !self.pan_mode && !self.suppressed {
if let Some(cp) = self.selection.last_move_pos { if let Some(cp) = self.selection.last_move_pos {
let color = Color { let color = theme
r: 0.85, .extended_palette()
g: 0.85, .background
b: 0.85, .base
a: 0.90, .text
}; .scale_alpha(0.90);
let stroke = canvas::Stroke { let stroke = canvas::Stroke {
width: 1.0, width: 1.0,
style: canvas::Style::Solid(color), style: canvas::Style::Solid(color),
@ -1010,8 +956,9 @@ impl canvas::Program<Message> for SelectionCanvas {
// the hovered object sits on a locked layer (issue: locked // the hovered object sits on a locked layer (issue: locked
// objects are visible + snappable but not selectable/editable). // objects are visible + snappable but not selectable/editable).
if self.hover_locked { if self.hover_locked {
let amber = Color { r: 0.96, g: 0.76, b: 0.26, a: 0.98 }; let warning = theme.extended_palette().warning.base;
let dark = Color { r: 0.12, g: 0.10, b: 0.04, a: 1.0 }; let amber = warning.color.scale_alpha(0.98);
let dark = warning.text;
let bx = cp.x + sq + 7.0; let bx = cp.x + sq + 7.0;
let by = cp.y - sq - 13.0; let by = cp.y - sq - 13.0;
// Lock body (filled). // Lock body (filled).
@ -1058,12 +1005,7 @@ impl canvas::Program<Message> for SelectionCanvas {
} }
// ── Object Snap Tracking ───────────────────────────────────────────── // ── Object Snap Tracking ─────────────────────────────────────────────
let track_color = Color { let track_color = theme.extended_palette().primary.base.color.scale_alpha(0.7);
r: 0.15,
g: 0.85,
b: 0.95,
a: 0.7,
};
// The alignment line the cursor is currently locked to — drawn at its // The alignment line the cursor is currently locked to — drawn at its
// real angle from the acquired point through the lock and a little // real angle from the acquired point through the lock and a little
// beyond, dashed so it reads as a construction guide. This covers the // beyond, dashed so it reads as a construction guide. This covers the
@ -2142,10 +2084,12 @@ struct DynInputCanvas {
} }
impl DynInputCanvas { impl DynInputCanvas {
fn dotted() -> canvas::Stroke<'static> { fn dotted(theme: &Theme) -> canvas::Stroke<'static> {
canvas::Stroke { canvas::Stroke {
width: 1.0, 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 }, line_dash: canvas::LineDash { segments: &[2.0, 3.0], offset: 0 },
..Default::default() ..Default::default()
} }
@ -2160,13 +2104,19 @@ impl DynInputCanvas {
} }
/// Draw a value box centred at `center`, clamped inside `bounds`. /// 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 content = Self::box_content(b);
let w = (content.len() as f32 * DYN_CHAR_W) + DYN_PAD * 2.0; 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 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 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 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.fill(&rect, fill);
frame.stroke( frame.stroke(
&rect, &rect,
@ -2177,7 +2127,7 @@ impl DynInputCanvas {
frame.fill_text(canvas::Text { frame.fill_text(canvas::Text {
content, content,
position: Point { x: x + DYN_PAD, y: y + DYN_PAD }, 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), size: iced::Pixels(DYN_FONT),
// Force Advanced shaping: the default `Auto` uses Basic shaping for // Force Advanced shaping: the default `Auto` uses Basic shaping for
// ASCII-only strings, which the web (wgpu/webgl) backend fails to // 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 { if b.active {
( (
Color { r: 0.12, g: 0.18, b: 0.30, a: 0.95 }, palette.primary.weak.color,
Color { r: 0.45, g: 0.70, b: 1.0, a: 1.0 }, palette.primary.base.color,
palette.primary.weak.text,
) )
} else if b.locked { } else if b.locked {
( (
Color { r: 0.05, g: 0.05, b: 0.12, a: 0.9 }, palette.warning.weak.color,
Color { r: 0.95, g: 0.75, b: 0.30, a: 0.9 }, palette.warning.base.color,
palette.warning.weak.text,
) )
} else { } else {
( (
Color { r: 0.05, g: 0.05, b: 0.12, a: 0.9 }, palette.background.weak.color,
Color { r: 0.35, g: 0.55, b: 0.90, a: 0.9 }, palette.background.neutral.color,
palette.background.weak.text,
) )
} }
} }
/// Prompt pill at `pos`. /// 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() { if self.prompt.is_empty() {
return; return;
} }
let palette = theme.extended_palette();
let pw = (self.prompt.len() as f32 * DYN_CHAR_W) + DYN_PAD * 2.0; 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 }); 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( frame.stroke(
&rect, &rect,
canvas::Stroke::default() 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), .with_width(1.0),
); );
frame.fill_text(canvas::Text { frame.fill_text(canvas::Text {
content: self.prompt.clone(), content: self.prompt.clone(),
position: Point { x: pos.x + DYN_PAD, y: pos.y + DYN_PAD }, 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), size: iced::Pixels(DYN_FONT),
shaping: iced::advanced::text::Shaping::Advanced, shaping: iced::advanced::text::Shaping::Advanced,
..Default::default() ..Default::default()
@ -2233,7 +2188,13 @@ impl DynInputCanvas {
/// Guided layout: draw the guide geometry anchored at `base`, then place /// Guided layout: draw the guide geometry anchored at `base`, then place
/// each box according to its role. /// 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 cursor_raw = self.cursor_screen;
let (vx, vy) = (cursor_raw.x - base.x, cursor_raw.y - base.y); let (vx, vy) = (cursor_raw.x - base.x, cursor_raw.y - base.y);
let raw_len = (vx * vx + vy * vy).sqrt().max(1.0); let raw_len = (vx * vx + vy * vy).sqrt().max(1.0);
@ -2308,7 +2269,7 @@ impl DynInputCanvas {
y: base.y + a_ref.sin() * len, 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 arc = canvas::Path::new(|p| {
let steps = 48; let steps = 48;
for k in 0..=steps { for k in 0..=steps {
@ -2324,14 +2285,14 @@ impl DynInputCanvas {
} }
} }
}); });
frame.stroke(&arc, Self::dotted()); frame.stroke(&arc, Self::dotted(theme));
} }
DynGuide::Radius => { DynGuide::Radius => {
let line = canvas::Path::new(|p| { let line = canvas::Path::new(|p| {
p.move_to(base); p.move_to(base);
p.line_to(cursor); p.line_to(cursor);
}); });
frame.stroke(&line, Self::dotted()); frame.stroke(&line, Self::dotted(theme));
} }
DynGuide::Perp => { DynGuide::Perp => {
if let Some((end, _, _)) = perp_info { if let Some((end, _, _)) = perp_info {
@ -2340,7 +2301,7 @@ impl DynInputCanvas {
p.move_to(base); p.move_to(base);
p.line_to(end); p.line_to(end);
}); });
frame.stroke(&line, Self::dotted()); frame.stroke(&line, Self::dotted(theme));
} }
} }
DynGuide::PerpDim => { DynGuide::PerpDim => {
@ -2351,14 +2312,14 @@ impl DynInputCanvas {
p.move_to(ob); p.move_to(ob);
p.line_to(oe); p.line_to(oe);
}); });
frame.stroke(&dim, Self::dotted()); frame.stroke(&dim, Self::dotted(theme));
let ext = canvas::Path::new(|p| { let ext = canvas::Path::new(|p| {
p.move_to(base); p.move_to(base);
p.line_to(ob); p.line_to(ob);
p.move_to(end); p.move_to(end);
p.line_to(oe); p.line_to(oe);
}); });
frame.stroke(&ext, Self::dotted()); frame.stroke(&ext, Self::dotted(theme));
} }
} }
DynGuide::AxisDelta | DynGuide::RectSides => { DynGuide::AxisDelta | DynGuide::RectSides => {
@ -2368,7 +2329,7 @@ impl DynInputCanvas {
p.line_to(corner); p.line_to(corner);
p.line_to(cursor); p.line_to(cursor);
}); });
frame.stroke(&legs, Self::dotted()); frame.stroke(&legs, Self::dotted(theme));
if self.guide == DynGuide::RectSides { if self.guide == DynGuide::RectSides {
// Close the rectangle so both side pairs read as a box. // Close the rectangle so both side pairs read as a box.
let rest = canvas::Path::new(|p| { 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(Point { x: base.x, y: cursor.y });
p.line_to(cursor); p.line_to(cursor);
}); });
frame.stroke(&rest, Self::dotted()); frame.stroke(&rest, Self::dotted(theme));
} }
} }
DynGuide::None => {} DynGuide::None => {}
@ -2423,12 +2384,12 @@ impl DynInputCanvas {
y: base.y + dy * len * 0.5 + ny * 16.0, 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). /// 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<String> = self let texts: Vec<String> = self
.boxes .boxes
.iter() .iter()
@ -2466,7 +2427,7 @@ impl DynInputCanvas {
py = (by - pad - DYN_BOX_H).max(0.0); py = (by - pad - DYN_BOX_H).max(0.0);
} }
if has_prompt { 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; let mut x = bx;
@ -2474,7 +2435,7 @@ impl DynInputCanvas {
let w = widths[i]; let w = widths[i];
let rect = let rect =
canvas::Path::rectangle(Point { x, y: by }, Size { width: w, height: DYN_BOX_H }); 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.fill(&rect, fill);
frame.stroke( frame.stroke(
&rect, &rect,
@ -2485,7 +2446,7 @@ impl DynInputCanvas {
frame.fill_text(canvas::Text { frame.fill_text(canvas::Text {
content: texts[i].clone(), content: texts[i].clone(),
position: Point { x: x + DYN_PAD, y: by + DYN_PAD }, 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), size: iced::Pixels(DYN_FONT),
shaping: iced::advanced::text::Shaping::Advanced, shaping: iced::advanced::text::Shaping::Advanced,
..Default::default() ..Default::default()
@ -2511,7 +2472,7 @@ impl canvas::Program<Message> for DynInputCanvas {
&self, &self,
_state: &(), _state: &(),
renderer: &iced::Renderer, renderer: &iced::Renderer,
_theme: &Theme, theme: &Theme,
bounds: iced::Rectangle, bounds: iced::Rectangle,
_cursor: mouse::Cursor, _cursor: mouse::Cursor,
) -> Vec<canvas::Geometry> { ) -> Vec<canvas::Geometry> {
@ -2529,15 +2490,15 @@ impl canvas::Program<Message> for DynInputCanvas {
if py + DYN_BOX_H > bounds.height { if py + DYN_BOX_H > bounds.height {
py = (self.cursor_screen.y - DYN_BOX_H - 4.0).max(0.0); 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()]; return vec![frame.into_geometry()];
} }
// Guided layouts need the anchor; without it fall back to a cursor row. // Guided layouts need the anchor; without it fall back to a cursor row.
match (self.guide, self.base_screen) { match (self.guide, self.base_screen) {
(DynGuide::None, _) | (_, None) => self.draw_row(&mut frame, bounds), (DynGuide::None, _) | (_, None) => self.draw_row(&mut frame, bounds, theme),
(_, Some(base)) => self.draw_guided(&mut frame, bounds, base), (_, Some(base)) => self.draw_guided(&mut frame, bounds, base, theme),
} }
vec![frame.into_geometry()] vec![frame.into_geometry()]
} }

View file

@ -3,7 +3,7 @@
//! adds that object to the current selection. Clicking outside dismisses it. //! adds that object to the current selection. Clicking outside dismisses it.
use iced::widget::{button, column, container, mouse_area, opaque, text}; 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; use crate::app::Message;
@ -19,15 +19,7 @@ pub fn cycle_popup_overlay(
.collect(); .collect();
let panel = container(column(rows)) let panel = container(column(rows))
.style(|_: &Theme| container::Style { .style(container::bordered_box)
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.width(Length::Fixed(150.0)); .width(Length::Fixed(150.0));
let positioned = crate::ui::pin_at(anchor, opaque(panel)); 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> { 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) let btn = button(content)
.on_press(Message::CycleSelect(handle)) .on_press(Message::CycleSelect(handle))
.style(|_: &Theme, status| button::Style { .style(button::subtle)
background: Some(Background::Color(match status {
button::Status::Hovered => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.width(Fill) .width(Fill)
.padding([4, 10]); .padding([4, 10]);
// Highlight the underlying object while the cursor is over this row. // 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)) .on_exit(Message::CycleHoverExit(handle))
.into() .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,
};

View file

@ -1,7 +1,7 @@
//! Isolate / Hide / End Isolation status menu. //! Isolate / Hide / End Isolation status menu.
use iced::widget::{button, row, text}; use iced::widget::{button, row, text};
use iced::{Background, Color, Element, Fill, Theme}; use iced::{Element, Fill};
use crate::app::Message; use crate::app::Message;
use crate::ui::statusbar::status_menu::Entry; 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> { fn action_row(label: &'static str, enabled: bool, msg: Message) -> Element<'static, Message> {
let lbl = text(label) let lbl = text(label).size(11);
.size(11)
.color(if enabled { LABEL_ON } else { LABEL_OFF });
let content = row![lbl].align_y(iced::Center); let content = row![lbl].align_y(iced::Center);
let mut btn = button(content) let mut btn = button(content)
.style(move |_: &Theme, status| button::Style { .style(button::subtle)
background: Some(Background::Color(match (enabled, status) {
(true, button::Status::Hovered) => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.width(Fill) .width(Fill)
.padding([4, 12]); .padding([4, 12]);
if enabled { if enabled {
@ -61,24 +53,3 @@ fn action_row(label: &'static str, enabled: bool, msg: Message) -> Element<'stat
} }
btn.into() 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,
};

View file

@ -1,7 +1,7 @@
//! Polar-tracking angle status menu. //! Polar-tracking angle status menu.
use iced::widget::{button, container, row, text, text_input}; 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::app::Message;
use crate::ui::statusbar::status_menu::Entry; use crate::ui::statusbar::status_menu::Entry;
@ -41,7 +41,7 @@ pub fn menu_entries<'a>(
let custom_row = container( let custom_row = container(
row![ row![
custom_field, custom_field,
text("°").size(11).color(LABEL_OFF), text("°").size(11),
] ]
.spacing(4) .spacing(4)
.align_y(iced::Center), .align_y(iced::Center),
@ -52,51 +52,16 @@ pub fn menu_entries<'a>(
} }
fn angle_row<'a>(deg: f32, active: bool) -> Element<'a, Message> { 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)) let lbl = text(angle_label(deg)).size(11);
.size(11)
.color(if active { LABEL_ON } else { LABEL_OFF });
let content = row![check, lbl].spacing(6).align_y(iced::Center); let content = row![check, lbl].spacing(6).align_y(iced::Center);
button(content) button(content)
.on_press(Message::SetPolarAngle(deg)) .on_press(Message::SetPolarAngle(deg))
.style(|_: &Theme, status| button::Style { .style(button::subtle)
background: Some(Background::Color(match status {
button::Status::Hovered => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.width(Fill) .width(Fill)
.padding([4, 10]) .padding([4, 10])
.into() .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,
};

View file

@ -1,7 +1,7 @@
//! Annotation / viewport scale status menu. //! Annotation / viewport scale status menu.
use iced::widget::{button, row, text}; use iced::widget::{button, row, text};
use iced::{Background, Color, Element, Fill, Theme}; use iced::{Element, Fill};
use crate::app::Message; use crate::app::Message;
use crate::ui::statusbar::status_menu::Entry; 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> { 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) let lbl = text(label).size(11);
.size(11)
.color(if active { LABEL_ON } else { LABEL_OFF });
let content = row![check, lbl].spacing(6).align_y(iced::Center); let content = row![check, lbl].spacing(6).align_y(iced::Center);
button(content) button(content)
.on_press(msg) .on_press(msg)
.style(|_: &Theme, status| button::Style { .style(button::subtle)
background: Some(Background::Color(match status {
button::Status::Hovered => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.width(Fill) .width(Fill)
.padding([4, 10]) .padding([4, 10])
.into() .into()
} }
fn manage_row() -> Element<'static, Message> { fn manage_row() -> Element<'static, Message> {
button(text("Manage...").size(11).color(CHECK_COLOR)) button(text("Manage...").size(11))
.on_press(Message::ScaleManagerOpen) .on_press(Message::ScaleManagerOpen)
.style(|_: &Theme, status| button::Style { .style(button::primary)
background: Some(Background::Color(match status {
button::Status::Hovered => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.width(Fill) .width(Fill)
.padding([5, 10]) .padding([5, 10])
.into() .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,
};

View file

@ -5,7 +5,7 @@
use rustc_hash::FxHashSet as HashSet; use rustc_hash::FxHashSet as HashSet;
use iced::widget::{button, container, row, text}; 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::app::Message;
use crate::ui::statusbar::status_menu::Entry; use crate::ui::statusbar::status_menu::Entry;
@ -37,8 +37,10 @@ pub fn menu_entries(
.padding([4u16, 8]); .padding([4u16, 8]);
let divider = container(iced::widget::Space::new().height(1)) let divider = container(iced::widget::Space::new().height(1))
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(DIVIDER)), background: Some(Background::Color(
theme.extended_palette().background.weak.color,
)),
..Default::default() ..Default::default()
}) })
.width(Fill) .width(Fill)
@ -62,99 +64,41 @@ pub fn menu_entries(
} }
fn type_row(name: String, included: bool) -> Element<'static, Message> { 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()) let lbl = text(name.clone()).size(11);
.size(11)
.color(if included { LABEL_ON } else { LABEL_OFF });
let content = row![check, lbl].spacing(6).align_y(iced::Center); let content = row![check, lbl].spacing(6).align_y(iced::Center);
button(content) button(content)
.on_press(Message::ToggleSelectionFilterType(name)) .on_press(Message::ToggleSelectionFilterType(name))
.style(|_: &Theme, status| button::Style { .style(button::subtle)
background: Some(Background::Color(match status {
button::Status::Hovered => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.width(Fill) .width(Fill)
.padding([4, 10]) .padding([4, 10])
.into() .into()
} }
fn empty_row() -> Element<'static, Message> { 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]) .padding([4, 10])
.into() .into()
} }
fn header_btn(label: &str, msg: Message, enabled: bool) -> Element<'_, Message> { fn header_btn(label: &str, msg: Message, enabled: bool) -> Element<'_, Message> {
let color = if enabled { let b = button(text(label).size(10));
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 = if enabled { b.on_press(msg) } else { b }; let b = if enabled { b.on_press(msg) } else { b };
b.style(|_: &Theme, status| button::Style { b.style(button::secondary)
background: Some(Background::Color(match status {
button::Status::Hovered => ROW_HOVER,
_ => BTN_BG,
})),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 2.0.into(),
},
..Default::default()
})
.padding([3, 8]) .padding([3, 8])
.into() .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,
};

View file

@ -1,7 +1,7 @@
//! OpenCADStudio-style OSNAP status menu. //! OpenCADStudio-style OSNAP status menu.
use iced::widget::{button, container, row, text}; 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::app::Message;
use crate::snap::{SnapType, Snapper, ALL_SNAP_MODES}; use crate::snap::{SnapType, Snapper, ALL_SNAP_MODES};
@ -20,8 +20,10 @@ pub fn menu_entries<'a>(snapper: &'a Snapper) -> Vec<Entry<'a>> {
// Divider // Divider
let divider = container(iced::widget::Space::new().height(1)) let divider = container(iced::widget::Space::new().height(1))
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(DIVIDER)), background: Some(Background::Color(
theme.extended_palette().background.weak.color,
)),
..Default::default() ..Default::default()
}) })
.width(Fill) .width(Fill)
@ -41,21 +43,18 @@ pub fn menu_entries<'a>(snapper: &'a Snapper) -> Vec<Entry<'a>> {
// ── Individual snap row ─────────────────────────────────────────────────── // ── Individual snap row ───────────────────────────────────────────────────
fn snap_row<'a>(snap_type: SnapType, label: &'a str, active: bool) -> Element<'a, Message> { 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, // SVG marker (not a Unicode glyph) so the symbols render on the web build,
// whose bundled font lacks them and showed tofu boxes. (#138) // whose bundled font lacks them and showed tofu boxes. (#138)
let icon_el = container(crate::ui::icons::tinted::<Message>( let icon_el = container(crate::ui::icons::themed_success::<Message>(
crate::ui::icons::osnap(snap_type), crate::ui::icons::osnap(snap_type),
13.0, 13.0,
ICON_COLOR,
)) ))
.width(Length::Fixed(16.0)) .width(Length::Fixed(16.0))
.align_x(iced::Center); .align_x(iced::Center);
let label_el = text(label) let label_el = text(label).size(11);
.size(11)
.color(if active { LABEL_ON } else { LABEL_OFF });
let content = row![checkmark, icon_el, label_el] let content = row![checkmark, icon_el, label_el]
.spacing(4) .spacing(4)
@ -63,98 +62,16 @@ fn snap_row<'a>(snap_type: SnapType, label: &'a str, active: bool) -> Element<'a
button(content) button(content)
.on_press(Message::ToggleSnap(snap_type)) .on_press(Message::ToggleSnap(snap_type))
.style(|_: &Theme, status| button::Style { .style(button::subtle)
background: Some(Background::Color(match status {
button::Status::Hovered => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.width(Fill) .width(Fill)
.padding([3, 8]) .padding([3, 8])
.into() .into()
} }
fn header_btn(label: &str, msg: Message, enabled: bool) -> Element<'_, Message> { fn header_btn(label: &str, msg: Message, enabled: bool) -> Element<'_, Message> {
let b = button(text(label).size(10).color(if enabled { let b = button(text(label).size(10));
Color {
r: 0.70,
g: 0.70,
b: 0.70,
a: 1.0,
}
} else {
Color {
r: 0.38,
g: 0.38,
b: 0.38,
a: 1.0,
}
}));
let b = if enabled { b.on_press(msg) } else { b }; let b = if enabled { b.on_press(msg) } else { b };
b.style(|_: &Theme, status| button::Style { b.style(button::secondary)
background: Some(Background::Color(match status {
button::Status::Hovered => ROW_HOVER,
_ => BTN_BG,
})),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 2.0.into(),
},
..Default::default()
})
.padding([3, 8]) .padding([3, 8])
.into() .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,
};

View file

@ -1,7 +1,7 @@
//! Drawing-units status menu. //! Drawing-units status menu.
use iced::widget::{button, row, text}; use iced::widget::{button, row, text};
use iced::{Background, Color, Element, Fill, Theme}; use iced::{Element, Fill};
use crate::app::Message; use crate::app::Message;
use crate::ui::statusbar::status_menu::Entry; use crate::ui::statusbar::status_menu::Entry;
@ -49,51 +49,16 @@ pub fn menu_entries(current: i16) -> Vec<Entry<'static>> {
} }
fn unit_row(label: &'static str, active: bool, msg: Message) -> Element<'static, Message> { fn unit_row(label: &'static str, active: bool, msg: Message) -> Element<'static, Message> {
let check = crate::ui::icons::check_cell(active, CHECK_COLOR); let check = crate::ui::icons::themed_check_cell(active);
let lbl = text(label) let lbl = text(label).size(11);
.size(11)
.color(if active { LABEL_ON } else { LABEL_OFF });
let content = row![check, lbl].spacing(6).align_y(iced::Center); let content = row![check, lbl].spacing(6).align_y(iced::Center);
button(content) button(content)
.on_press(msg) .on_press(msg)
.style(|_: &Theme, status| button::Style { .style(button::subtle)
background: Some(Background::Color(match status {
button::Status::Hovered => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.width(Fill) .width(Fill)
.padding([4, 10]) .padding([4, 10])
.into() .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,
};

View file

@ -208,9 +208,11 @@ impl PropertiesPanel {
pub fn view(&self) -> Element<'_, Message> { pub fn view(&self) -> Element<'_, Message> {
// ── Header ────────────────────────────────────────────────────────── // ── Header ──────────────────────────────────────────────────────────
let header = container(text("Properties").size(12).color(Color::WHITE)) let header = container(text("Properties").size(12))
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(HEADER_BG)), background: Some(Background::Color(
theme.extended_palette().background.weak.color,
)),
..Default::default() ..Default::default()
}) })
.width(Length::Fill) .width(Length::Fill)
@ -218,7 +220,10 @@ impl PropertiesPanel {
// ── Title bar (entity type / "No Selection") ───────────────────── // ── Title bar (entity type / "No Selection") ─────────────────────
let title_content: Element<'_, Message> = if self.selection_groups.is_empty() { 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 { } else {
combo_box( combo_box(
&self.selection_group_combo, &self.selection_group_combo,
@ -235,14 +240,17 @@ impl PropertiesPanel {
}; };
let title_bar = container(title_content) let title_bar = container(title_content)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(SECTION_BG)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.weakest.color)),
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 0.0.into(), radius: 0.0.into(),
}, },
..Default::default() ..Default::default()
}
}) })
.width(Length::Fill) .width(Length::Fill)
.padding([4, 10]); .padding([4, 10]);
@ -252,7 +260,7 @@ impl PropertiesPanel {
container( container(
text("Select an object to view properties") text("Select an object to view properties")
.size(10) .size(10)
.color(HINT_COLOR), .style(hint_text_style),
) )
.padding([10, 10]) .padding([10, 10])
.into() .into()
@ -265,14 +273,17 @@ impl PropertiesPanel {
}; };
container(column![header, title_bar, content]) container(column![header, title_bar, content])
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(PANEL_BG)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.base.color)),
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 0.0.into(), radius: 0.0.into(),
}, },
..Default::default() ..Default::default()
}
}) })
.width(250) .width(250)
.height(Length::Fill) .height(Length::Fill)
@ -286,15 +297,22 @@ impl PropertiesPanel {
if self.sections.is_empty() { if self.sections.is_empty() {
return None; return None;
} }
let title = container(text(crate::ui::text_util::elide(&self.title, 34)).size(FONT_SZ).color(SECTION_LABEL)) let title = container(
.style(|_: &Theme| container::Style { text(crate::ui::text_util::elide(&self.title, 34))
background: Some(Background::Color(SECTION_BG)), .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 { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 0.0.into(), radius: 0.0.into(),
}, },
..Default::default() ..Default::default()
}
}) })
.width(Length::Fill) .width(Length::Fill)
.padding([4, 10]); .padding([4, 10]);
@ -306,14 +324,17 @@ impl PropertiesPanel {
Some( Some(
container(col) container(col)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(PANEL_BG)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.base.color)),
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 3.0.into(), radius: 3.0.into(),
}, },
..Default::default() ..Default::default()
}
}) })
.width(230) .width(230)
.into(), .into(),
@ -324,15 +345,18 @@ impl PropertiesPanel {
fn render_section<'a>(&'a self, section: &'a PropSection) -> Element<'a, Message> { fn render_section<'a>(&'a self, section: &'a PropSection) -> Element<'a, Message> {
// Section header // Section header
let hdr = container(text(&section.title).size(10).color(Color::WHITE)) let hdr = container(text(&section.title).size(10))
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(SECTION_HDR_BG)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.weak.color)),
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 0.0.into(), radius: 0.0.into(),
}, },
..Default::default() ..Default::default()
}
}) })
.width(Length::Fill) .width(Length::Fill)
.padding([3, 8]); .padding([3, 8]);
@ -508,31 +532,25 @@ impl PropertiesPanel {
fn render_color_varies_row<'a>(&'a self, label: &'a str) -> Element<'a, Message> { fn render_color_varies_row<'a>(&'a self, label: &'a str) -> Element<'a, Message> {
let color_btn = button( let color_btn = button(
row![ row![
container(text("?").size(10).color(VALUE_COLOR)) container(text("?").size(10))
.style(move |_: &Theme| container::Style { .style(move |theme: &Theme| {
background: Some(Background::Color(Color { let palette = theme.extended_palette();
r: 0.32, container::Style {
g: 0.32, background: Some(Background::Color(palette.background.strong.color)),
b: 0.32,
a: 1.0,
})),
border: Border { border: Border {
color: Color { color: palette.background.neutral.color,
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.5
},
width: 1.0, width: 1.0,
radius: 2.0.into() radius: 2.0.into()
}, },
text_color: Some(palette.background.strong.text),
..Default::default() ..Default::default()
}
}) })
.width(SWATCH_SZ) .width(SWATCH_SZ)
.height(SWATCH_SZ) .height(SWATCH_SZ)
.align_x(iced::Center) .align_x(iced::Center)
.align_y(iced::Center), .align_y(iced::Center),
text(VARIES_LABEL).size(FONT_SZ).color(VALUE_COLOR), text(VARIES_LABEL).size(FONT_SZ),
] ]
.spacing(4) .spacing(4)
.align_y(iced::Center), .align_y(iced::Center),
@ -724,7 +742,7 @@ impl PropertiesPanel {
.on_input(move |v| Message::PropGeomInput { field, value: v }) .on_input(move |v| Message::PropGeomInput { field, value: v })
.on_submit(Message::PropGeomCommit(field)) .on_submit(Message::PropGeomCommit(field))
.size(FONT_SZ) .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 // The wrapping container draws the border; keep the input flat
// so field + caret read as one control. // so field + caret read as one control.
border: Border { border: Border {
@ -732,33 +750,32 @@ impl PropertiesPanel {
width: 0.0, width: 0.0,
radius: 0.0.into(), radius: 0.0.into(),
}, },
..text_input_style(&Theme::Dark, status) ..text_input_style(theme, status)
}) })
.padding([3, 6]) .padding([3, 6])
.width(Length::Fill); .width(Length::Fill);
let caret = button( let caret = button(
container(crate::ui::icons::arrow_toggle( container(if self.edit_choice_open {
self.edit_choice_open, crate::ui::icons::themed_arrow_up(FONT_SZ)
FONT_SZ, } else {
VALUE_COLOR, crate::ui::icons::themed_arrow_down(FONT_SZ)
)) })
.height(Length::Fill) .height(Length::Fill)
.align_y(iced::Center), .align_y(iced::Center),
) )
.on_press(Message::PropEditChoiceToggle) .on_press(Message::PropEditChoiceToggle)
.style(|_: &Theme, status| button::Style { .style(|theme: &Theme, status| {
background: Some(Background::Color(match status { let palette = theme.extended_palette();
button::Status::Hovered | button::Status::Pressed => Color { let pair = match status {
r: 0.28, button::Status::Hovered | button::Status::Pressed => palette.background.weak,
g: 0.28, _ => palette.background.base,
b: 0.28, };
a: 1.0, button::Style {
}, background: Some(Background::Color(pair.color)),
_ => VALUE_BG, text_color: pair.text,
})),
text_color: VALUE_COLOR,
border: Border::default(), border: Border::default(),
..Default::default() ..Default::default()
}
}) })
.padding(Padding { .padding(Padding {
top: 0.0, top: 0.0,
@ -768,14 +785,17 @@ impl PropertiesPanel {
}) })
.height(Length::Fixed(ROW_H - 6.0)); .height(Length::Fixed(ROW_H - 6.0));
let head = container(row![input, caret].align_y(iced::Center)) let head = container(row![input, caret].align_y(iced::Center))
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(VALUE_BG)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.base.color)),
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 2.0.into(), radius: 2.0.into(),
}, },
..Default::default() ..Default::default()
}
}) })
.width(Length::Fill); .width(Length::Fill);
@ -794,39 +814,15 @@ impl PropertiesPanel {
} }
let value = opt.clone(); let value = opt.clone();
list = list.push( 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 }) .on_press(Message::PropGeomChoiceChanged { field, value })
.style(|_: &Theme, status| button::Style { .style(button::subtle)
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()
})
.padding([2, 6]) .padding([2, 6])
.width(Length::Fill), .width(Length::Fill),
); );
} }
let popup = container(scrollable(list).height(Length::Shrink)) let popup = container(scrollable(list).height(Length::Shrink))
.style(|_: &Theme| container::Style { .style(container::bordered_box)
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()
})
.padding(2) .padding(2)
.width(200) .width(200)
.max_height(220.0); .max_height(220.0);
@ -940,13 +936,13 @@ pub fn color_picker_dropdown<'a>(
r.push( r.push(
button(text("").width(18).height(18)) button(text("").width(18).height(18))
.on_press(msg) .on_press(msg)
.style(move |_: &Theme, status| button::Style { .style(move |theme: &Theme, status| button::Style {
background: Some(Background::Color(bg)), background: Some(Background::Color(bg)),
border: Border { border: Border {
color: if matches!(status, button::Status::Hovered) { color: if matches!(status, button::Status::Hovered) {
Color::WHITE theme.extended_palette().primary.base.color
} else { } else {
Color::BLACK theme.extended_palette().background.neutral.color
}, },
width: if matches!(status, button::Status::Hovered) { width: if matches!(status, button::Status::Hovered) {
1.5 1.5
@ -965,20 +961,20 @@ pub fn color_picker_dropdown<'a>(
// "More Colors…" toggle button // "More Colors…" toggle button
let more_btn = button( let more_btn = button(
row![ 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…" }) text(if palette_open { "Less" } else { "More Colors…" })
.size(10) .size(10)
.color(HINT_COLOR), .style(hint_text_style),
] ]
.spacing(4) .spacing(4)
.align_y(iced::Center), .align_y(iced::Center),
) )
.on_press(palette_toggle_msg) .on_press(palette_toggle_msg)
.style(|_: &Theme, _| button::Style { .style(button::subtle)
background: Some(Background::Color(PICKER_BG)),
text_color: HINT_COLOR,
..Default::default()
})
.padding([2, 6]) .padding([2, 6])
.width(Length::Fill); .width(Length::Fill);
@ -989,14 +985,17 @@ pub fn color_picker_dropdown<'a>(
}; };
let mut col = column![container(inner) let mut col = column![container(inner)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(PICKER_BG)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.base.color)),
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 0.0.into() radius: 0.0.into()
}, },
..Default::default() ..Default::default()
}
}) })
.padding([6, 8]) .padding([6, 8])
.width(Length::Fill)] .width(Length::Fill)]
@ -1019,18 +1018,13 @@ pub fn color_picker_dropdown<'a>(
r = r.push( r = r.push(
button(text("").width(12).height(12)) button(text("").width(12).height(12))
.on_press(msg) .on_press(msg)
.style(move |_: &Theme, status| button::Style { .style(move |theme: &Theme, status| button::Style {
background: Some(Background::Color(bg)), background: Some(Background::Color(bg)),
border: Border { border: Border {
color: if matches!(status, button::Status::Hovered) { color: if matches!(status, button::Status::Hovered) {
Color::WHITE theme.extended_palette().primary.base.color
} else { } else {
Color { theme.extended_palette().background.neutral.color
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.4,
}
}, },
width: if matches!(status, button::Status::Hovered) { width: if matches!(status, button::Status::Hovered) {
1.5 1.5
@ -1049,14 +1043,17 @@ pub fn color_picker_dropdown<'a>(
} }
col = col.push( col = col.push(
container(scrollable(rows).height(160)) container(scrollable(rows).height(160))
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(PICKER_BG)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.base.color)),
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 0.0.into(), radius: 0.0.into(),
}, },
..Default::default() ..Default::default()
}
}) })
.padding([4, 6]) .padding([4, 6])
.width(Length::Fill), .width(Length::Fill),
@ -1071,22 +1068,23 @@ pub fn color_picker_dropdown<'a>(
/// A boolean toggle button row (for "Invisible" etc.). /// A boolean toggle button row (for "Invisible" etc.).
fn render_stepper_row<'a>(label: &'a str, display: &'a str) -> Element<'a, Message> { fn render_stepper_row<'a>(label: &'a str, display: &'a str) -> Element<'a, Message> {
let arrow = |glyph: &'static str, delta: i8| { 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)) .on_press(Message::PropVertexStep(delta))
.padding([0, 6]) .padding([0, 6])
.style(|_: &Theme, status| { .style(|theme: &Theme, status| {
let bg = match status { let palette = theme.extended_palette();
button::Status::Hovered | button::Status::Pressed => HOVER_BG, let pair = match status {
_ => VALUE_BG, button::Status::Hovered | button::Status::Pressed => palette.background.weak,
_ => palette.background.base,
}; };
button::Style { button::Style {
background: Some(Background::Color(bg)), background: Some(Background::Color(pair.color)),
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 2.0.into(), radius: 2.0.into(),
}, },
text_color: VALUE_COLOR, text_color: pair.text,
..Default::default() ..Default::default()
} }
}) })
@ -1095,7 +1093,6 @@ fn render_stepper_row<'a>(label: &'a str, display: &'a str) -> Element<'a, Messa
arrow("◀", -1), arrow("◀", -1),
text(display) text(display)
.size(FONT_SZ) .size(FONT_SZ)
.color(VALUE_COLOR)
.width(Length::Fill) .width(Length::Fill)
.align_x(iced::Center), .align_x(iced::Center),
arrow("▶", 1), arrow("▶", 1),
@ -1111,22 +1108,29 @@ fn render_bool_row<'a>(label: &'a str, field: &'static str, value: bool) -> Elem
button( button(
text(btn_label) text(btn_label)
.size(FONT_SZ) .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)) .on_press(Message::PropBoolToggle(field))
.style(move |_: &Theme, status| { .style(move |theme: &Theme, status| {
let bg = match status { let palette = theme.extended_palette();
button::Status::Hovered | button::Status::Pressed => HOVER_BG, let pair = match status {
_ => VALUE_BG, button::Status::Hovered | button::Status::Pressed => palette.background.weak,
_ => palette.background.base,
}; };
button::Style { button::Style {
background: Some(Background::Color(bg)), background: Some(Background::Color(pair.color)),
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 2.0.into(), 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() ..Default::default()
} }
}) })
@ -1211,10 +1215,14 @@ fn render_group_row(
let label_btn = button( let label_btn = button(
container( container(
row![ 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)) text(crate::ui::text_util::elide(base, 16))
.size(FONT_SZ) .size(FONT_SZ)
.color(LABEL_COLOR), .style(muted_text_style),
] ]
.spacing(4) .spacing(4)
.align_y(iced::Center), .align_y(iced::Center),
@ -1223,20 +1231,7 @@ fn render_group_row(
.align_y(iced::Center), .align_y(iced::Center),
) )
.on_press(Message::PropGroupToggle(key)) .on_press(Message::PropGroupToggle(key))
.style(|_: &Theme, status| button::Style { .style(button::subtle)
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()
})
.padding(Padding { .padding(Padding {
top: 0.0, top: 0.0,
bottom: 0.0, bottom: 0.0,
@ -1246,8 +1241,10 @@ fn render_group_row(
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fixed(ROW_H)); .height(Length::Fixed(ROW_H));
let label_col = container(label_btn) let label_col = container(label_btn)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(LABEL_BG)), background: Some(Background::Color(
theme.extended_palette().background.weakest.color,
)),
..Default::default() ..Default::default()
}) })
.width(Length::FillPortion(5)) .width(Length::FillPortion(5))
@ -1262,8 +1259,10 @@ fn render_group_row(
.padding([3, 6]) .padding([3, 6])
.width(Length::Fill); .width(Length::Fill);
let value_col = container(value_field) let value_col = container(value_field)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(VALUE_BG)), background: Some(Background::Color(
theme.extended_palette().background.base.color,
)),
..Default::default() ..Default::default()
}) })
.width(Length::FillPortion(6)) .width(Length::FillPortion(6))
@ -1278,9 +1277,9 @@ fn render_group_row(
container(row![label_col, value_col]) container(row![label_col, value_col])
.height(Length::Fixed(ROW_H)) .height(Length::Fixed(ROW_H))
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
border: Border { border: Border {
color: BORDER, color: theme.extended_palette().background.neutral.color,
width: 1.0, width: 1.0,
radius: 0.0.into(), 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. /// Build a label | widget property row.
fn prop_row_widget<'a>(label: &'a str, widget: Element<'a, Message>) -> Element<'a, Message> { 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)) let label_col = container(
.style(|_: &Theme| container::Style { text(crate::ui::text_util::elide(label, 18))
background: Some(Background::Color(LABEL_BG)), .size(FONT_SZ)
.style(muted_text_style),
)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.extended_palette().background.weakest.color,
)),
..Default::default() ..Default::default()
}) })
.width(Length::FillPortion(5)) .width(Length::FillPortion(5))
@ -1320,8 +1325,10 @@ fn prop_row_widget<'a>(label: &'a str, widget: Element<'a, Message>) -> Element<
right: 6.0, right: 6.0,
}); });
let value_col = container(widget) let value_col = container(widget)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(VALUE_BG)), background: Some(Background::Color(
theme.extended_palette().background.base.color,
)),
..Default::default() ..Default::default()
}) })
.width(Length::FillPortion(6)) .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]) container(row![label_col, value_col])
.height(Length::Fixed(ROW_H)) .height(Length::Fixed(ROW_H))
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
border: Border { border: Border {
color: BORDER, color: theme.extended_palette().background.neutral.color,
width: 1.0, width: 1.0,
radius: 0.0.into(), 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. /// A plain text button used inside the color picker for ByLayer / ByBlock.
fn picker_text_btn(label: &str, msg: Message) -> Element<'_, Message> { 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) .on_press(msg)
.style(|_: &Theme, _| button::Style { .style(button::secondary)
background: Some(Background::Color(LABEL_BG)),
border: Border {
color: BORDER,
width: 1.0,
radius: 2.0.into(),
},
text_color: VALUE_COLOR,
..Default::default()
})
.padding([2, 8]) .padding([2, 8])
.into() .into()
} }
@ -1419,49 +1417,41 @@ fn aci_label(idx: u8) -> &'static str {
// ── Widget style helpers ────────────────────────────────────────────────── // ── Widget style helpers ──────────────────────────────────────────────────
fn combo_btn_style(_theme: &Theme, status: button::Status) -> button::Style { fn combo_btn_style(theme: &Theme, status: button::Status) -> button::Style {
let bg = match status { let palette = theme.extended_palette();
button::Status::Hovered | button::Status::Pressed => HOVER_BG, let pair = match status {
_ => VALUE_BG, button::Status::Hovered | button::Status::Pressed => palette.background.weak,
_ => palette.background.base,
}; };
button::Style { button::Style {
background: Some(Background::Color(bg)), background: Some(Background::Color(pair.color)),
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 2.0.into(), radius: 2.0.into(),
}, },
text_color: VALUE_COLOR, text_color: pair.text,
..Default::default() ..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 { let border_color = match status {
text_input::Status::Focused { .. } => Color { text_input::Status::Focused { .. } => palette.primary.base.color,
r: 0.3, _ => palette.background.neutral.color,
g: 0.6,
b: 1.0,
a: 1.0,
},
_ => BORDER,
}; };
text_input::Style { text_input::Style {
background: Background::Color(VALUE_BG), background: Background::Color(palette.background.base.color),
border: Border { border: Border {
color: border_color, color: border_color,
width: 1.0, width: 1.0,
radius: 2.0.into(), radius: 2.0.into(),
}, },
icon: Color::TRANSPARENT, icon: Color::TRANSPARENT,
placeholder: HINT_COLOR, placeholder: palette.background.base.text.scale_alpha(0.48),
value: VALUE_COLOR, value: palette.background.base.text,
selection: Color { selection: palette.primary.base.color.scale_alpha(0.5),
r: 0.2,
g: 0.4,
b: 0.8,
a: 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 /// 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 /// 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. /// 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 { text_input::Style {
background: Background::Color(VALUE_BG), background: Background::Color(palette.background.base.color),
border: Border { border: Border {
color: Color::TRANSPARENT, color: Color::TRANSPARENT,
width: 0.0, width: 0.0,
radius: 0.0.into(), radius: 0.0.into(),
}, },
icon: Color::TRANSPARENT, icon: Color::TRANSPARENT,
placeholder: HINT_COLOR, placeholder: palette.background.base.text.scale_alpha(0.48),
value: VALUE_COLOR, value: palette.background.base.text,
selection: Color { selection: palette.primary.base.color.scale_alpha(0.5),
r: 0.2,
g: 0.4,
b: 0.8,
a: 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 { fn hint_text_style(theme: &Theme) -> iced::widget::text::Style {
r: 0.19, iced::widget::text::Style {
g: 0.19, color: Some(theme.extended_palette().background.base.text.scale_alpha(0.48)),
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,
};

View file

@ -15,7 +15,7 @@ use iced::advanced::layout::{self, Layout};
use iced::advanced::widget::{self, Widget}; use iced::advanced::widget::{self, Widget};
use iced::advanced::{mouse, overlay, renderer, Clipboard, Renderer as _, Shell}; use iced::advanced::{mouse, overlay, renderer, Clipboard, Renderer as _, Shell};
use iced::{ 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, Theme, Vector,
}; };
@ -113,8 +113,6 @@ pub struct CollapsePanels<'a> {
open: Option<String>, open: Option<String>,
/// Fallback row height, used only when there are no panels to measure. /// Fallback row height, used only when there are no panels to measure.
row_h: f32, row_h: f32,
/// Colour of the 1px divider drawn between panels.
divider: Color,
/// Chosen degradation level per panel; set during layout. /// Chosen degradation level per panel; set during layout.
levels: RefCell<Vec<u8>>, levels: RefCell<Vec<u8>>,
/// If set, the measured row height is written here each layout (read when /// 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> { impl<'a> CollapsePanels<'a> {
pub fn new(panels: Vec<Panel<'a>>, open: Option<String>, row_h: f32, divider: Color) -> Self { pub fn new(panels: Vec<Panel<'a>>, open: Option<String>, row_h: f32) -> Self {
let n = panels.len(); let n = panels.len();
Self { Self {
panels, panels,
open, open,
row_h, row_h,
divider,
levels: RefCell::new(vec![FULL; n]), levels: RefCell::new(vec![FULL; n]),
height_out: None, height_out: None,
tight_out: None, tight_out: None,
@ -480,7 +477,7 @@ impl<'a> Widget<Message, Theme, Renderer> for CollapsePanels<'a> {
shadow: Shadow::default(), shadow: Shadow::default(),
snap: true, snap: true,
}, },
Background::Color(self.divider), Background::Color(theme.extended_palette().background.neutral.color),
); );
} }
} }

View file

@ -12,7 +12,7 @@ use std::sync::Arc;
use rustc_hash::FxHashMap as HashMap; use rustc_hash::FxHashMap as HashMap;
use acadrust::types::{Color as AcadColor, LineWeight}; 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 iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme};
use crate::app::Message; use crate::app::Message;
@ -419,58 +419,41 @@ impl Ribbon {
let is_active = i == self.active; let is_active = i == self.active;
let is_contextual = module.id() == "layout"; 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( let btn = container(
button(text(module.title()).size(12)) button(text(module.title()).size(12))
.on_press(Message::RibbonSelectTab(i)) .on_press(Message::RibbonSelectTab(i))
.style(move |_: &Theme, status| button::Style { .style(move |theme: &Theme, status| {
background: Some(Background::Color(match (is_active, status) { let palette = theme.extended_palette();
(true, _) => RIBBON_BG, let accent = if is_contextual {
(false, button::Status::Hovered) => hover_bg, palette.warning.base
_ => Color::TRANSPARENT,
})),
text_color: if is_active {
Color::WHITE
} else { } 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 { border: Border {
color: if is_active { color: if is_active {
accent accent.color
} else { } else {
Color::TRANSPARENT Color::TRANSPARENT
}, },
@ -479,13 +462,18 @@ impl Ribbon {
}, },
shadow: iced::Shadow::default(), shadow: iced::Shadow::default(),
snap: false, snap: false,
}
}) })
.padding([5, 14]), .padding([5, 14]),
) )
.style(move |_: &Theme| container::Style { .style(move |theme: &Theme| container::Style {
border: Border { border: Border {
color: if is_active { color: if is_active {
accent if is_contextual {
theme.extended_palette().warning.base.color
} else {
theme.extended_palette().primary.base.color
}
} else { } else {
Color::TRANSPARENT Color::TRANSPARENT
}, },
@ -514,9 +502,11 @@ impl Ribbon {
// persisted (see `Ribbon::set_collapse_mode`). It hides itself once the // persisted (see `Ribbon::set_collapse_mode`). It hides itself once the
// tool row is tight, giving the cramped tab row its space back. // 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 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())) .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) .height(24)
.padding([2, 8]); .padding([2, 8]);
let mode_dd = PosReport::new(COLLAPSE_MODE_ID, mode_btn); let mode_dd = PosReport::new(COLLAPSE_MODE_ID, mode_btn);
@ -534,8 +524,10 @@ impl Ribbon {
} }
let tab_bar = container(tab_row) let tab_bar = container(tab_row)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(TOPBAR_BG)), background: Some(Background::Color(
theme.extended_palette().background.base.color,
)),
..Default::default() ..Default::default()
}) })
.padding(Padding { .padding(Padding {
@ -653,10 +645,12 @@ impl Ribbon {
self.active_lineweight, self.active_lineweight,
&style_ctx, &style_ctx,
)) ))
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(RIBBON_BG)), background: Some(Background::Color(
theme.extended_palette().background.weakest.color,
)),
border: Border { border: Border {
color: BORDER_DARK, color: theme.extended_palette().background.neutral.color,
width: 1.0, width: 1.0,
radius: 0.0.into(), radius: 0.0.into(),
}, },
@ -666,7 +660,7 @@ impl Ribbon {
} }
}) })
.collect(); .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_height(self.tool_bar_h.clone())
.report_tight(self.collapse_tight.clone()) .report_tight(self.collapse_tight.clone())
.mode(self.collapse_mode) .mode(self.collapse_mode)
@ -676,10 +670,12 @@ impl Ribbon {
}; };
let tool_bar = container(tool_area) let tool_bar = container(tool_area)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(RIBBON_BG)), background: Some(Background::Color(
theme.extended_palette().background.weakest.color,
)),
border: Border { border: Border {
color: BORDER_DARK, color: theme.extended_palette().background.neutral.color,
width: 1.0, width: 1.0,
radius: 0.0.into(), radius: 0.0.into(),
}, },
@ -712,19 +708,13 @@ impl Ribbon {
.enumerate() .enumerate()
.map(|(idx, label)| { .map(|(idx, label)| {
let step = idx + 1; let step = idx + 1;
button(text(label.clone()).size(11).color(LABEL_ON)) button(text(label.clone()).size(11))
.on_press(if is_undo { .on_press(if is_undo {
Message::UndoMany(step) Message::UndoMany(step)
} else { } else {
Message::RedoMany(step) Message::RedoMany(step)
}) })
.style(|_: &Theme, status| button::Style { .style(popup_row_style)
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.width(Fill) .width(Fill)
.padding([5, 10]) .padding([5, 10])
.into() .into()
@ -732,15 +722,7 @@ impl Ribbon {
.collect(); .collect();
let panel = container(column(rows)) let panel = container(column(rows))
.style(|_: &Theme| container::Style { .style(popup_panel_style)
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.width(Length::Fixed(170.0)); .width(Length::Fixed(170.0));
let (align_right, h_pad, top) = self.dd_anchor(open_id, 170.0, win.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<Element<Message>> = CollapseMode::ALL let rows: Vec<Element<Message>> = CollapseMode::ALL
.iter() .iter()
.map(|&m| { .map(|&m| {
let mark: Element<Message> = if m == current {
crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR)
} else {
iced::widget::Space::new().width(0).into()
};
button( button(
row![ row![
container(mark).width(Length::Fixed(16.0)), crate::ui::icons::themed_check_cell(m == current),
text(m.label()).size(11).color(LABEL_ON), text(m.label()).size(11),
] ]
.spacing(4) .spacing(4)
.align_y(iced::Center), .align_y(iced::Center),
) )
.on_press(Message::SetRibbonCollapseMode(m)) .on_press(Message::SetRibbonCollapseMode(m))
.style(|_: &Theme, status| button::Style { .style(popup_row_style)
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.width(Fill) .width(Fill)
.padding([5, 10]) .padding([5, 10])
.into() .into()
@ -783,15 +754,7 @@ impl Ribbon {
.collect(); .collect();
let panel = container(column(rows)) let panel = container(column(rows))
.style(|_: &Theme| container::Style { .style(popup_panel_style)
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.width(Length::Fixed(W)); .width(Length::Fixed(W));
let (align_right, h_pad, top) = self.dd_anchor(open_id, W, win.0); let (align_right, h_pad, top) = self.dd_anchor(open_id, W, win.0);
@ -852,28 +815,25 @@ impl Ribbon {
.iter() .iter()
.map(|(cmd, label, item_icon)| { .map(|(cmd, label, item_icon)| {
let is_current = *cmd == last_cmd; let is_current = *cmd == last_cmd;
let checkmark: Element<'_, Message> = container(if is_current { let checkmark: Element<'_, Message> =
crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR) crate::ui::icons::themed_check_cell(is_current);
} else { let icon_el: Element<Message> =
iced::widget::Space::new().width(0).into() container(make_icon(*item_icon, 20.0))
})
.width(Length::Fixed(14.0))
.into();
let icon_el: Element<Message> = match *item_icon {
IconKind::Glyph(s) => text(s)
.size(13)
.color(ICON_COLOR)
.width(Length::Fixed(20.0)) .width(Length::Fixed(20.0))
.into(), .into();
IconKind::Svg(bytes) => {
let handle = svg::Handle::from_memory(bytes);
svg(handle).width(20).height(20).into()
}
};
let label_el = let label_el =
text(*label) text(*label)
.size(11) .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( button(
row![checkmark, icon_el, label_el] row![checkmark, icon_el, label_el]
@ -884,13 +844,7 @@ impl Ribbon {
dropdown_id: dd_id, dropdown_id: dd_id,
cmd: *cmd, cmd: *cmd,
}) })
.style(|_: &Theme, status| button::Style { .style(popup_row_style)
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.width(Fill) .width(Fill)
.padding([4, 10]) .padding([4, 10])
.into() .into()
@ -898,15 +852,7 @@ impl Ribbon {
.collect(); .collect();
let panel = container(column(rows)) let panel = container(column(rows))
.style(|_: &Theme| container::Style { .style(popup_panel_style)
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.width(Length::Fixed(190.0)); .width(Length::Fixed(190.0));
let (align_right, h_pad, top) = self.dd_anchor(open_id, 190.0, win.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> { let icon_btn = |bytes: &'static [u8], msg: Message| -> Element<'_, Message> {
button(crate::ui::icons::raw(bytes, 14.0)) button(crate::ui::icons::raw(bytes, 14.0))
.on_press(msg) .on_press(msg)
.style(|_: &Theme, status| button::Style { .style(popup_row_style)
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.padding([2, 4]) .padding([2, 4])
.into() .into()
}; };
@ -952,10 +892,10 @@ impl Ribbon {
let name = info.name.clone(); let name = info.name.clone();
let swatch = container(text("")) let swatch = container(text(""))
.style(move |_: &Theme| container::Style { .style(move |theme: &Theme| container::Style {
background: Some(Background::Color(lc)), background: Some(Background::Color(lc)),
border: Border { border: Border {
color: SWATCH_BORDER, color: theme.extended_palette().background.strong.color,
width: 1.0, width: 1.0,
radius: 1.0.into(), radius: 1.0.into(),
}, },
@ -976,29 +916,27 @@ impl Ribbon {
crate::ui::icons::layer_lock(ll), crate::ui::icons::layer_lock(ll),
Message::LayerToggleLock(index), Message::LayerToggleLock(index),
); );
let checkmark: Element<'_, Message> = container(if is_active { let checkmark: Element<'_, Message> =
crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR) crate::ui::icons::themed_check_cell(is_active);
} else {
iced::widget::Space::new().width(0).into()
})
.width(Length::Fixed(14.0))
.into();
let label = let label =
text(&info.name) text(&info.name)
.size(11) .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 // The swatch + label area selects the layer as active; the
// icon buttons above handle their own toggles. // icon buttons above handle their own toggles.
let select = button(row![swatch, label].spacing(5).align_y(iced::Center)) let select = button(row![swatch, label].spacing(5).align_y(iced::Center))
.on_press(Message::RibbonLayerChanged(name)) .on_press(Message::RibbonLayerChanged(name))
.style(|_: &Theme, status| button::Style { .style(popup_row_style)
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.width(Fill) .width(Fill)
.padding([4, 4]); .padding([4, 4]);
@ -1021,34 +959,7 @@ impl Ribbon {
let search = iced::widget::text_input("Search layers…", &self.layer_filter) let search = iced::widget::text_input("Search layers…", &self.layer_filter)
.on_input(Message::RibbonLayerFilterChanged) .on_input(Message::RibbonLayerFilterChanged)
.size(11) .size(11)
.padding([4, 6]) .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,
},
});
let panel = container( let panel = container(
column![ column![
container(search).padding([4, 4]), container(search).padding([4, 4]),
@ -1056,15 +967,7 @@ impl Ribbon {
] ]
.spacing(2), .spacing(2),
) )
.style(|_: &Theme| container::Style { .style(popup_panel_style)
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.width(Length::Fixed(220.0)); .width(Length::Fixed(220.0));
let (align_right, h_pad, top) = self.dd_anchor(LAYER_COMBO_ID, 220.0, win.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 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<Element<Message>> = ctx let mut rows: Vec<Element<Message>> = ctx
.names_for(style_key) .names_for(style_key)
.iter() .iter()
.map(|name| { .map(|name| {
let is_sel = name.as_str() == active.as_str(); let is_sel = name.as_str() == active.as_str();
let n = name.clone(); let n = name.clone();
let checkmark: Element<Message> = container(if is_sel { let checkmark: Element<Message> =
crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR) crate::ui::icons::themed_check_cell(is_sel);
} else {
iced::widget::Space::new().width(0).into()
})
.width(Length::Fixed(14.0))
.into();
button( button(
row![ row![
checkmark, checkmark,
text(name.clone()).size(11).color(if is_sel { text(name.clone())
LABEL_ON .size(11)
} else { .style(move |theme: &Theme| iced::widget::text::Style {
LABEL_OFF color: (!is_sel).then_some(
}), theme
.extended_palette()
.background
.base
.text
.scale_alpha(0.72),
),
}),
] ]
.spacing(4) .spacing(4)
.align_y(iced::Center), .align_y(iced::Center),
@ -1150,7 +1047,7 @@ impl Ribbon {
key: style_key, key: style_key,
name: n, name: n,
}) })
.style(row_style) .style(popup_row_style)
.width(Fill) .width(Fill)
.padding([4, 10]) .padding([4, 10])
.into() .into()
@ -1159,9 +1056,9 @@ impl Ribbon {
if let Some(mgr) = manager_cmd { if let Some(mgr) = manager_cmd {
rows.push( rows.push(
button(text("Manage…").size(11).color(LABEL_ON)) button(text("Manage…").size(11))
.on_press(Message::Command(mgr.to_string())) .on_press(Message::Command(mgr.to_string()))
.style(row_style) .style(popup_row_style)
.width(Fill) .width(Fill)
.padding([4, 10]) .padding([4, 10])
.into(), .into(),
@ -1169,15 +1066,7 @@ impl Ribbon {
} }
let panel = container(column(rows)) let panel = container(column(rows))
.style(|_: &Theme| container::Style { .style(popup_panel_style)
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.width(Length::Fixed(LARGE_W * 2.3)); .width(Length::Fixed(LARGE_W * 2.3));
let (align_right, h_pad, top) = self.dd_anchor(open_id, LARGE_W * 2.3, win.0); 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) let panel = container(picker)
.style(|_: &Theme| container::Style { .style(popup_panel_style)
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.width(Length::Fixed(200.0)); .width(Length::Fixed(200.0));
let (align_right, h_pad, top) = self.dd_anchor(PROP_COLOR_ID, 200.0, win.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() .into_iter()
.map(|lt| { .map(|lt| {
let is_cur = lt.name == *active_lt; let is_cur = lt.name == *active_lt;
let check: Element<'_, Message> = container(if is_cur { let check: Element<'_, Message> =
crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR) crate::ui::icons::themed_check_cell(is_cur);
} else {
iced::widget::Space::new().width(0).into()
})
.width(Length::Fixed(14.0))
.into();
let name_col = text(lt.name.clone()) let name_col = text(lt.name.clone())
.size(11) .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)); .width(Length::Fixed(90.0));
let art_col = text(lt.art.clone()).size(9).color(Color { let art_col = text(lt.art.clone()).size(9).style(muted_text_style);
r: 0.55,
g: 0.55,
b: 0.55,
a: 1.0,
});
let name = lt.name.clone(); let name = lt.name.clone();
button( button(
row![check, name_col, art_col] row![check, name_col, art_col]
@ -1267,13 +1147,7 @@ impl Ribbon {
.align_y(iced::Center), .align_y(iced::Center),
) )
.on_press(Message::RibbonLinetypeChanged(name)) .on_press(Message::RibbonLinetypeChanged(name))
.style(|_: &Theme, status| button::Style { .style(popup_row_style)
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.width(Fill) .width(Fill)
.padding([4, 6]) .padding([4, 6])
.into() .into()
@ -1281,15 +1155,7 @@ impl Ribbon {
.collect(); .collect();
let list = container(scrollable(column(rows)).height(Length::Fixed(200.0))) let list = container(scrollable(column(rows)).height(Length::Fixed(200.0)))
.style(|_: &Theme| container::Style { .style(popup_panel_style)
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.width(Length::Fixed(220.0)); .width(Length::Fixed(220.0));
let (align_right, h_pad, top) = self.dd_anchor(PROP_LINETYPE_ID, 220.0, win.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| { .map(|item| {
let is_cur = item.0 == active_lw; let is_cur = item.0 == active_lw;
let label = item.to_string(); let label = item.to_string();
let check: Element<'_, Message> = container(if is_cur { let check: Element<'_, Message> =
crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR) crate::ui::icons::themed_check_cell(is_cur);
} else {
iced::widget::Space::new().width(0).into()
})
.width(Length::Fixed(14.0))
.into();
button( button(
row![ row![
check, check,
text(label) text(label)
.size(11) .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) .spacing(5)
.align_y(iced::Center), .align_y(iced::Center),
) )
.on_press(Message::RibbonLineweightChanged(item.0)) .on_press(Message::RibbonLineweightChanged(item.0))
.style(|_: &Theme, status| button::Style { .style(popup_row_style)
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.width(Fill) .width(Fill)
.padding([4, 8]) .padding([4, 8])
.into() .into()
@ -1350,15 +1214,7 @@ impl Ribbon {
win: (f32, f32), win: (f32, f32),
) -> Option<Element<'a, Message>> { ) -> Option<Element<'a, Message>> {
let panel = container(column(rows)) let panel = container(column(rows))
.style(|_: &Theme| container::Style { .style(popup_panel_style)
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.width(Length::Fixed(width)); .width(Length::Fixed(width));
let (align_right, h_pad, top) = self.dd_anchor(dd_id, width, win.0); let (align_right, h_pad, top) = self.dd_anchor(dd_id, width, win.0);
@ -1439,7 +1295,7 @@ fn render_group<'a>(
column![ column![
tools_el, 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) .align_x(iced::Center)
.spacing(0) .spacing(0)
@ -1526,8 +1382,8 @@ fn collapse_button<'a>(
column![ column![
icon, icon,
row![ row![
text(title.to_string()).size(9).color(GROUP_LABEL), text(title.to_string()).size(9).style(muted_text_style),
crate::ui::icons::arrow_down(8.0, GROUP_LABEL), crate::ui::icons::themed_secondary_arrow_down(8.0),
] ]
.spacing(3) .spacing(3)
.align_y(iced::Center), .align_y(iced::Center),
@ -1536,22 +1392,7 @@ fn collapse_button<'a>(
.spacing(2), .spacing(2),
) )
.on_press(Message::ToggleRibbonPanel(title.to_string())) .on_press(Message::ToggleRibbonPanel(title.to_string()))
.style(|_: &Theme, status| button::Style { .style(button::subtle)
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()
})
.padding([3, 5]) .padding([3, 5])
.into(); .into();
} }
@ -1594,29 +1435,14 @@ fn collapse_button<'a>(
let opener = button( let opener = button(
row![ row![
text(title.to_string()).size(9).color(GROUP_LABEL), text(title.to_string()).size(9).style(muted_text_style),
crate::ui::icons::arrow_down(8.0, GROUP_LABEL), crate::ui::icons::themed_secondary_arrow_down(8.0),
] ]
.spacing(3) .spacing(3)
.align_y(iced::Center), .align_y(iced::Center),
) )
.on_press(Message::ToggleRibbonPanel(title.to_string())) .on_press(Message::ToggleRibbonPanel(title.to_string()))
.style(|_: &Theme, status| button::Style { .style(button::subtle)
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()
})
.padding([1, 4]); .padding([1, 4]);
// The large face fills a fixed slot so a collapsed panel is shorter than a full // The large face fills a fixed slot so a collapsed panel is shorter than a full

View file

@ -8,7 +8,7 @@ use acadrust::types::{Color as AcadColor, LineWeight};
// Ribbon tooltips anchor to the right of their button so the cursor — which // Ribbon tooltips anchor to the right of their button so the cursor — which
// rests on the button itself — never covers the tip text. (#143) // rests on the button itself — never covers the tip text. (#143)
use iced::widget::tooltip::Position as TipPos; 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 iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme};
use crate::app::Message; 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_LINETYPE_ID: &str = "PROP_LINETYPE";
pub(super) const PROP_LW_ID: &str = "PROP_LW"; 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) ──────────────────── // ── Style context (passed from Ribbon to render_large) ────────────────────
pub(super) struct StyleContext { 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> { pub(super) fn make_icon(icon: IconKind, size: f32) -> Element<'static, Message> {
match icon { match icon {
IconKind::Glyph(s) => text(s).size(size * 0.7).color(Color::WHITE).into(), IconKind::Glyph(s) => text(s).size(size * 0.7).into(),
IconKind::Svg(bytes) => { IconKind::Svg(bytes) => icons::themed(bytes, size),
let handle = svg::Handle::from_memory(bytes);
svg(handle).width(size).height(size).into()
}
} }
} }
@ -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)) && !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). /// `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> { pub(super) fn make_icon_dim(icon: IconKind, size: f32, dim: bool) -> Element<'static, Message> {
if !dim { if !dim {
return make_icon(icon, size); return make_icon(icon, size);
} }
match icon { match icon {
IconKind::Glyph(s) => text(s).size(size * 0.7).color(DIM_TOOL).into(), IconKind::Glyph(s) => text(s)
IconKind::Svg(bytes) => icons::tinted(bytes, size, DIM_TOOL), .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 ─────────────────────────────────────────────────────────── // ── 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 { button::Style {
background: Some(Background::Color(match (is_active, status) { background: is_active
(true, _) => TOOL_ACTIVE, .then_some(Background::Color(pair.color))
(_, button::Status::Hovered) => TOOL_HOVER, .or_else(|| matches!(status, button::Status::Hovered | button::Status::Pressed)
(_, button::Status::Pressed) => TOOL_ACTIVE, .then_some(Background::Color(pair.color))),
_ => Color::TRANSPARENT, text_color: pair.text,
})),
text_color: Color::WHITE,
border: Border { border: Border {
radius: 3.0.into(), radius: 3.0.into(),
color: Color::TRANSPARENT, 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 combo_btn_style(
theme: &Theme,
pub(super) fn make_tip(tip: String) -> Element<'static, Message> { is_open: bool,
text(tip).size(11).color(Color::WHITE).into() 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 { container::Style {
background: Some(Background::Color(TIP_BG)), background: Some(Background::Color(palette.background.base.color)),
border: Border { border: Border {
color: COMBO_BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 3.0.into(), 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() ..Default::default()
} }
} }
@ -408,7 +314,7 @@ pub(super) fn render_small<'a>(
let tip_text = format!("{}\nCommand: {}", t.label, t.id); let tip_text = format!("{}\nCommand: {}", t.label, t.id);
let btn = button(make_icon_dim(t.icon, SMALL_ICON, dim)) let btn = button(make_icon_dim(t.icon, SMALL_ICON, dim))
.on_press(Message::RibbonToolClick { tool_id, event }) .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)) .width(Length::Fixed(SMALL_W))
.height(ROW_H) .height(ROW_H)
.padding([4, 4]); .padding([4, 4]);
@ -474,31 +380,22 @@ pub(super) fn render_small<'a>(
tool_id: last.to_string(), tool_id: last.to_string(),
event: ModuleEvent::Command(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)) .width(Length::Fixed(SMALL_W))
.height(ROW_H) .height(ROW_H)
.padding([4, 4]); .padding([4, 4]);
let arr_tip = format!("{} options", cur_label); let arr_tip = format!("{} options", cur_label);
let arr_btn = button( let arr_btn = button(
container(icons::arrow_down(8.0, ARROW_COLOR)) container(icons::themed_arrow_down(8.0))
.width(Fill) .width(Fill)
.height(Fill) .height(Fill)
.align_x(iced::Center) .align_x(iced::Center)
.align_y(iced::Center), .align_y(iced::Center),
) )
.on_press(Message::ToggleRibbonDropdown(id.to_string())) .on_press(Message::ToggleRibbonDropdown(id.to_string()))
.style(move |_: &Theme, status| button::Style { .style(move |theme: &Theme, status| {
background: Some(Background::Color(match status { tool_btn_style(theme, dd_open, 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()
}) })
.width(Length::Fixed(ARROW_W)) .width(Length::Fixed(ARROW_W))
.height(ROW_H) .height(ROW_H)
@ -569,7 +466,9 @@ pub(super) fn render_large_dropdown<'a>(
let top_btn = button( let top_btn = button(
column![ column![
make_icon_dim(cur_icon, LARGE_ICON, dim), 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) .align_x(iced::Center)
.spacing(3), .spacing(3),
@ -578,7 +477,7 @@ pub(super) fn render_large_dropdown<'a>(
tool_id: last.to_string(), tool_id: last.to_string(),
event: ModuleEvent::Command(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)) .width(Length::Fixed(LARGE_W))
.height(Fill) .height(Fill)
.padding(Padding { .padding(Padding {
@ -589,24 +488,15 @@ pub(super) fn render_large_dropdown<'a>(
}); });
let arr_btn = button( let arr_btn = button(
container(icons::arrow_down(9.0, ARROW_COLOR)) container(icons::themed_arrow_down(9.0))
.width(Fill) .width(Fill)
.height(Fill) .height(Fill)
.align_x(iced::Center) .align_x(iced::Center)
.align_y(iced::Center), .align_y(iced::Center),
) )
.on_press(Message::ToggleRibbonDropdown(id.to_string())) .on_press(Message::ToggleRibbonDropdown(id.to_string()))
.style(move |_: &Theme, status| button::Style { .style(move |theme: &Theme, status| {
background: Some(Background::Color(match status { tool_btn_style(theme, dd_open, 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()
}) })
.width(Length::Fixed(LARGE_W)) .width(Length::Fixed(LARGE_W))
.height(LARGE_ARR) .height(LARGE_ARR)
@ -659,13 +549,15 @@ pub(super) fn render_large<'a>(
let btn = button( let btn = button(
column![ column![
make_icon_dim(t.icon, LARGE_ICON, dim), 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) .align_x(iced::Center)
.spacing(3), .spacing(3),
) )
.on_press(Message::RibbonToolClick { tool_id, event }) .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)) .width(Length::Fixed(LARGE_W))
.height(Fill) .height(Fill)
.padding(Padding { .padding(Padding {
@ -737,10 +629,10 @@ pub(super) fn render_large<'a>(
let freeze_icon = icons::raw(icons::layer_freeze(lf), 14.0); let freeze_icon = icons::raw(icons::layer_freeze(lf), 14.0);
let lock_icon = icons::raw(icons::layer_lock(ll), 14.0); let lock_icon = icons::raw(icons::layer_lock(ll), 14.0);
let swatch = container(text("")) let swatch = container(text(""))
.style(move |_: &Theme| container::Style { .style(move |theme: &Theme| container::Style {
background: Some(Background::Color(lc)), background: Some(Background::Color(lc)),
border: Border { border: Border {
color: SWATCH_BORDER, color: theme.extended_palette().background.strong.color,
width: 1.0, width: 1.0,
radius: 1.0.into(), radius: 1.0.into(),
}, },
@ -758,27 +650,17 @@ pub(super) fn render_large<'a>(
freeze_icon, freeze_icon,
lock_icon, lock_icon,
swatch, swatch,
container(text(active_layer).size(11).color(Color::WHITE)) container(text(active_layer).size(11))
.width(name_w) .width(name_w)
.clip(true), .clip(true),
icons::arrow_down(9.0, COMBO_ARROW), icons::themed_arrow_down(9.0),
] ]
.spacing(4) .spacing(4)
.align_y(iced::Center), .align_y(iced::Center),
) )
.on_press(Message::ToggleRibbonDropdown(LAYER_COMBO_ID.to_string())) .on_press(Message::ToggleRibbonDropdown(LAYER_COMBO_ID.to_string()))
.style(move |_: &Theme, status| button::Style { .style(move |theme: &Theme, status| {
background: Some(Background::Color(match (is_open, status) { combo_btn_style(theme, is_open, status, 3.0)
(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()
}) })
.padding([3, 8]) .padding([3, 8])
.width(Fill); .width(Fill);
@ -793,20 +675,16 @@ pub(super) fn render_large<'a>(
let event = t.event.clone(); let event = t.event.clone();
let icon_el: Element<Message> = if dim { let icon_el: Element<Message> = if dim {
make_icon_dim(t.icon, 16.0, true) make_icon_dim(t.icon, 16.0, true)
} else { match t.icon { } else {
IconKind::Glyph(g) => text(g).size(13).color(Color::WHITE).into(), make_icon(t.icon, 16.0)
IconKind::Svg(bytes) => { };
iced::widget::svg(iced::widget::svg::Handle::from_memory(bytes))
.width(16)
.height(16)
.into()
}
} };
let msg = module_event_to_message(event); let msg = module_event_to_message(event);
tooltip( tooltip(
button(icon_el) button(icon_el)
.on_press(msg) .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]), .padding([2, 5]),
make_tip(tip.to_string()), make_tip(tip.to_string()),
TipPos::Right, TipPos::Right,
@ -864,7 +742,9 @@ pub(super) fn render_large<'a>(
let mp_btn = button( let mp_btn = button(
column![ column![
make_icon_dim(match_prop.icon, LARGE_ICON, mp_dim), 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) .align_x(iced::Center)
.spacing(3), .spacing(3),
@ -873,7 +753,7 @@ pub(super) fn render_large<'a>(
tool_id: mp_id, tool_id: mp_id,
event: mp_event, 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)) .width(Length::Fixed(LARGE_W))
.height(Fill) .height(Fill)
.padding(Padding { .padding(Padding {
@ -895,10 +775,10 @@ pub(super) fn render_large<'a>(
let is_open = open_dd.as_deref() == Some(dd_id); let is_open = open_dd.as_deref() == Some(dd_id);
let swatch_el: Element<'a, Message> = if let Some(c) = swatch { let swatch_el: Element<'a, Message> = if let Some(c) = swatch {
container(text("")) container(text(""))
.style(move |_: &Theme| container::Style { .style(move |theme: &Theme| container::Style {
background: Some(Background::Color(c)), background: Some(Background::Color(c)),
border: Border { border: Border {
color: SWATCH_BORDER, color: theme.extended_palette().background.strong.color,
width: 1.0, width: 1.0,
radius: 1.0.into(), radius: 1.0.into(),
}, },
@ -913,40 +793,21 @@ pub(super) fn render_large<'a>(
button( button(
row![ row![
swatch_el, swatch_el,
container(text(label).size(10).color(Color::WHITE)) container(text(label).size(10))
.width(Fill) .width(Fill)
.clip(true), .clip(true),
icons::arrow_toggle( if is_open {
is_open, icons::themed_arrow_up(8.0)
8.0, } else {
Color { icons::themed_arrow_down(8.0)
r: 0.6, },
g: 0.6,
b: 0.6,
a: 1.0,
},
),
] ]
.spacing(4) .spacing(4)
.align_y(iced::Center), .align_y(iced::Center),
) )
.on_press(Message::ToggleRibbonDropdown(dd_id.to_string())) .on_press(Message::ToggleRibbonDropdown(dd_id.to_string()))
.style(move |_: &Theme, status| button::Style { .style(move |theme: &Theme, status| {
background: Some(Background::Color(match (is_open, status) { combo_btn_style(theme, is_open, status, 2.0)
(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()
}) })
.padding([3, 8]) .padding([3, 8])
.width(Length::Fixed(PROP_W)) .width(Length::Fixed(PROP_W))
@ -999,31 +860,21 @@ pub(super) fn render_large<'a>(
// ── combo button ── // ── combo button ──
let combo_btn = button( let combo_btn = button(
row![ row![
container(text(active.clone()).size(11).color(Color::WHITE)) container(text(active.clone()).size(11))
.width(Fill) .width(Fill)
.clip(true), .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) .spacing(4)
.align_y(iced::Center), .align_y(iced::Center),
) )
.on_press(Message::ToggleRibbonDropdown(combo_id.to_string())) .on_press(Message::ToggleRibbonDropdown(combo_id.to_string()))
.style(move |_: &Theme, status| button::Style { .style(move |theme: &Theme, status| {
background: Some(Background::Color(match (is_open, status) { combo_btn_style(theme, is_open, status, 3.0)
(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()
}) })
.padding([3, 8]) .padding([3, 8])
.width(Fill); .width(Fill);
@ -1045,20 +896,16 @@ pub(super) fn render_large<'a>(
let event = t.event.clone(); let event = t.event.clone();
let icon_el: Element<Message> = if dim { let icon_el: Element<Message> = if dim {
make_icon_dim(t.icon, 16.0, true) make_icon_dim(t.icon, 16.0, true)
} else { match t.icon { } else {
IconKind::Glyph(g) => text(g).size(13).color(Color::WHITE).into(), make_icon(t.icon, 16.0)
IconKind::Svg(bytes) => { };
iced::widget::svg(iced::widget::svg::Handle::from_memory(bytes))
.width(16)
.height(16)
.into()
}
} };
let msg = module_event_to_message(event); let msg = module_event_to_message(event);
tooltip( tooltip(
button(icon_el) button(icon_el)
.on_press(msg) .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]), .padding([2, 5]),
make_tip(tip.to_string()), make_tip(tip.to_string()),
TipPos::Right, 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). // they read on the dark top strip (raw black is invisible there).
// On the Start tab, commands the start gate refuses render dimmed. // On the Start tab, commands the start gate refuses render dimmed.
let dim = is_start && !crate::app::commands::start_allowed(cmd); 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( let btn = button(
container(icon) container(icon)
.width(Fill) .width(Fill)
@ -1137,22 +988,7 @@ pub(super) fn quick_access_btn<'a>(
.align_y(iced::Center), .align_y(iced::Center),
) )
.on_press(Message::Command(cmd.to_string())) .on_press(Message::Command(cmd.to_string()))
.style(|_: &Theme, status| button::Style { .style(button::subtle)
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()
})
.width(Length::Fixed(TOP_HIST_W)) .width(Length::Fixed(TOP_HIST_W))
.height(24) .height(24)
.padding([2, 0]); .padding([2, 0]);
@ -1171,13 +1007,12 @@ pub(super) fn render_history_control<'a>(
) -> Element<'a, Message> { ) -> Element<'a, Message> {
let dd_open = open_dropdown.as_deref() == Some(dropdown_id); let dd_open = open_dropdown.as_deref() == Some(dropdown_id);
let active = count > 0; let active = count > 0;
let icon_color = if active { Color::WHITE } else { LABEL_OFF };
let main_btn = { let main_btn = {
let glyph = if dropdown_id == UNDO_HISTORY_ID { let glyph = if dropdown_id == UNDO_HISTORY_ID {
icons::undo(15.0, icon_color) icons::themed_undo(15.0, active)
} else { } else {
icons::redo(15.0, icon_color) icons::themed_redo(15.0, active)
}; };
let btn = button( let btn = button(
container(glyph) container(glyph)
@ -1186,7 +1021,9 @@ pub(super) fn render_history_control<'a>(
.align_x(iced::Center) .align_x(iced::Center)
.align_y(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)) .width(Length::Fixed(TOP_HIST_W))
.height(24) .height(24)
.padding([2, 0]); .padding([2, 0]);
@ -1211,16 +1048,19 @@ pub(super) fn render_history_control<'a>(
let arrow_btn = { let arrow_btn = {
let btn = button( let btn = button(
container(icons::arrow_down( container(if active {
8.0, icons::themed_arrow_down(8.0)
if active { ARROW_COLOR } else { LABEL_OFF }, } else {
)) icons::themed_disabled_arrow_down(8.0)
})
.width(Fill) .width(Fill)
.height(Fill) .height(Fill)
.align_x(iced::Center) .align_x(iced::Center)
.align_y(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)) .width(Length::Fixed(TOP_ARR_W))
.height(24) .height(24)
.padding(0); .padding(0);
@ -1243,19 +1083,26 @@ pub(super) fn render_history_control<'a>(
} }
pub(super) fn top_hist_btn_style( pub(super) fn top_hist_btn_style(
theme: &Theme,
active: bool, active: bool,
open: bool, open: bool,
status: button::Status, status: button::Status,
) -> button::Style { ) -> 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 { button::Style {
background: Some(Background::Color(match (active, open, status) { background: (!active || open || matches!(
(false, _, _) => HIST_INACTIVE_BG, status,
(_, true, _) => TOOL_ACTIVE, button::Status::Hovered | button::Status::Pressed
(_, _, button::Status::Hovered) => TOOL_HOVER, ))
(_, _, button::Status::Pressed) => TOOL_ACTIVE, .then_some(Background::Color(pair.color)),
_ => Color::TRANSPARENT, text_color: pair.text,
})),
text_color: Color::WHITE,
border: Border { border: Border {
radius: 3.0.into(), radius: 3.0.into(),
color: Color::TRANSPARENT, color: Color::TRANSPARENT,

View file

@ -7,15 +7,12 @@
//! module's tools can drive it. First used for paper-space viewport / plot //! module's tools can drive it. First used for paper-space viewport / plot
//! actions; reusable for any future context action set. //! actions; reusable for any future context action set.
use iced::widget::{button, column, container, svg, text, tooltip}; use iced::widget::{button, column, container, text, tooltip};
use iced::{Background, Border, Color, Element, Length, Theme}; use iced::{Background, Border, Element, Length, Theme};
use crate::app::Message; use crate::app::Message;
use crate::modules::{IconKind, ToolDef}; 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 ICON_SIZE: f32 = 22.0;
const BTN_SIZE: f32 = 38.0; const BTN_SIZE: f32 = 38.0;
/// Gap between the toolbar and the right edge of the canvas. /// 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> { fn icon_el(icon: IconKind) -> Element<'static, Message> {
match icon { match icon {
IconKind::Glyph(s) => text(s).size(ICON_SIZE * 0.85).color(Color::WHITE).into(), IconKind::Glyph(s) => text(s).size(ICON_SIZE * 0.85).into(),
IconKind::Svg(bytes) => svg(svg::Handle::from_memory(bytes)) IconKind::Svg(bytes) => crate::ui::icons::themed(bytes, ICON_SIZE),
.width(Length::Fixed(ICON_SIZE))
.height(Length::Fixed(ICON_SIZE))
.into(),
} }
} }
fn tip_panel(label: &'static str) -> Element<'static, Message> { 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]) .padding([2, 6])
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(PANEL_BG)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.strong.color)),
border: Border { border: Border {
color: PANEL_BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 3.0.into(), radius: 3.0.into(),
}, },
text_color: Some(palette.background.strong.text),
..Default::default() ..Default::default()
}
}) })
.into() .into()
} }
@ -63,17 +61,22 @@ pub fn view(tools: &[ToolDef]) -> Option<Element<'static, Message>> {
}) })
.width(Length::Fixed(BTN_SIZE)) .width(Length::Fixed(BTN_SIZE))
.height(Length::Fixed(BTN_SIZE)) .height(Length::Fixed(BTN_SIZE))
.style(|_: &Theme, status| button::Style { .style(|theme: &Theme, status| {
background: Some(Background::Color(match status { let palette = theme.extended_palette();
button::Status::Hovered | button::Status::Pressed => BTN_HOVER, let hovered = matches!(
_ => Color::TRANSPARENT, status,
})), button::Status::Hovered | button::Status::Pressed
);
button::Style {
background: hovered
.then_some(Background::Color(palette.background.strong.color)),
border: Border { border: Border {
radius: 3.0.into(), radius: 3.0.into(),
..Default::default() ..Default::default()
}, },
text_color: Color::WHITE, text_color: palette.background.base.text,
..Default::default() ..Default::default()
}
}); });
// Label tooltip on the left so it never runs off the right edge. // Label tooltip on the left so it never runs off the right edge.
col = col.push( col = col.push(
@ -81,14 +84,17 @@ pub fn view(tools: &[ToolDef]) -> Option<Element<'static, Message>> {
); );
} }
let panel = container(col).padding(4).style(|_: &Theme| container::Style { let panel = container(col).padding(4).style(|theme: &Theme| {
background: Some(Background::Color(PANEL_BG)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.weak.color)),
border: Border { border: Border {
color: PANEL_BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 5.0.into(), radius: 5.0.into(),
}, },
..Default::default() ..Default::default()
}
}); });
// Fill the canvas, pin the panel to the right edge and centre it // Fill the canvas, pin the panel to the right edge and centre it

View file

@ -120,22 +120,13 @@ impl StatusBar {
// Leftmost hamburger: opens a dropdown listing Model + every layout, so // Leftmost hamburger: opens a dropdown listing Model + every layout, so
// a layout can be picked directly even when the tab strip is scrolled. // a layout can be picked directly even when the tab strip is scrolled.
let menu_button = button(crate::ui::icons::tinted( let menu_icon = if is_start {
crate::ui::icons::MENU, crate::ui::icons::themed_disabled(crate::ui::icons::MENU, 16.0)
16.0, } else {
if is_start { DISABLED_COLOR } else { ICON_COLOR }, crate::ui::icons::themed_secondary(crate::ui::icons::MENU, 16.0)
)) };
.style(|_: &Theme, status| button::Style { let menu_button = button(menu_icon)
background: Some(Background::Color(match status { .style(button::subtle)
button::Status::Hovered => PILL_BG,
_ => Color::TRANSPARENT,
})),
border: Border {
radius: 3.0.into(),
..Default::default()
},
..Default::default()
})
.padding([4, 8]); .padding([4, 8]);
let menu_btn = if is_start { let menu_btn = if is_start {
tip( tip(
@ -156,15 +147,8 @@ impl StatusBar {
) )
}; };
let add_button = button(text("+").size(12).color(if is_start { let add_button = button(text("+").size(12))
DISABLED_COLOR .style(button::subtle)
} else {
ICON_COLOR
}))
.style(|_: &Theme, _| button::Style {
background: Some(Background::Color(Color::TRANSPARENT)),
..Default::default()
})
.padding([4, 8]); .padding([4, 8]);
let add_btn = if is_start { let add_btn = if is_start {
tip( tip(
@ -453,14 +437,17 @@ impl StatusBar {
.justify_end(true); .justify_end(true);
container(wrap) container(wrap)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(BAR_BG)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.base.color)),
border: Border { border: Border {
color: BORDER_COLOR, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 0.0.into(), radius: 0.0.into(),
}, },
..Default::default() ..Default::default()
}
}) })
.width(Length::Fill) .width(Length::Fill)
// One row matches the drawing (document) tab bar height so the three // One row matches the drawing (document) tab bar height so the three
@ -501,19 +488,9 @@ fn format_coords(cursor: glam::DVec3, last: Option<glam::DVec3>, mode: i16, pick
// ── Customization handle ────────────────────────────────────────────────── // ── Customization handle ──────────────────────────────────────────────────
fn customize_btn() -> Element<'static, Message> { fn customize_btn() -> Element<'static, Message> {
button(crate::ui::icons::tinted(crate::ui::icons::MENU, 16.0, ICON_COLOR)) button(crate::ui::icons::themed_secondary(crate::ui::icons::MENU, 16.0))
.on_press(Message::StatusMenuTooltipHidden(true)) .on_press(Message::StatusMenuTooltipHidden(true))
.style(|_: &Theme, status| button::Style { .style(button::subtle)
background: Some(Background::Color(match status {
button::Status::Hovered => PILL_BG,
_ => Color::TRANSPARENT,
})),
border: Border {
radius: 3.0.into(),
..Default::default()
},
..Default::default()
})
.padding([4, 8]) .padding([4, 8])
.into() .into()
} }
@ -521,7 +498,7 @@ fn customize_btn() -> Element<'static, Message> {
// ── Tooltip helper ──────────────────────────────────────────────────────── // ── Tooltip helper ────────────────────────────────────────────────────────
fn tip<'a>(content: Element<'a, Message>, label: &'static str) -> Element<'a, Message> { 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 /// 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( tooltip(
content, content,
container(body) container(body)
.style(|_: &Theme| container::Style { .style(container::bordered_box)
background: Some(Background::Color(Color {
r: 0.13,
g: 0.13,
b: 0.13,
a: 0.97,
})),
border: Border {
color: Color {
r: 0.35,
g: 0.35,
b: 0.35,
a: 1.0,
},
width: 1.0,
radius: 3.0.into(),
},
text_color: Some(Color::WHITE),
..Default::default()
})
.padding([4, 8]), .padding([4, 8]),
TipPos::Top, 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 /// text labels were too small to read). The name lives in the tooltip each
/// call site already wraps it with. /// call site already wraps it with.
fn toggle_pill(icon: &'static [u8], active: bool, msg: Message) -> Element<'static, Message> { fn toggle_pill(icon: &'static [u8], active: bool, msg: Message) -> Element<'static, Message> {
let color = if active { OSNAP_ON_TEXT } else { OSNAP_OFF_TEXT }; let icon = if active {
button(crate::ui::icons::tinted(icon, 17.0, color)) crate::ui::icons::themed_primary(icon, 17.0)
} else {
crate::ui::icons::themed_secondary(icon, 17.0)
};
button(icon)
.on_press(msg) .on_press(msg)
.style(move |_: &Theme, status| button::Style { .style(move |theme: &Theme, status| {
background: Some(Background::Color(match (active, status) { let mut style = button::subtle(theme, status);
(true, button::Status::Hovered) => SNAP_ON_HOVER, if active {
(true, _) => SNAP_ON_BG, let palette = theme.extended_palette();
(false, button::Status::Hovered) => SNAP_OFF_HOVER, style.background = Some(Background::Color(match status {
(false, _) => SNAP_OFF_BG, button::Status::Hovered => palette.primary.base.color,
})), _ => palette.primary.weak.color,
border: Border { }));
color: if active { SNAP_BORDER_ON } else { BORDER_COLOR }, style.text_color = palette.primary.weak.text;
width: 1.0, style.border.color = palette.primary.base.color;
radius: 2.0.into(), style.border.width = 1.0;
}, }
text_color: color, style
shadow: iced::Shadow::default(),
snap: false,
}) })
.padding([4, 7]) .padding([4, 7])
.into() .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 // and a dropdown caret, so both halves read as a single integrated control and
// the border / background / caret / sizing live in exactly ONE place. // 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 /// Wrap a caller-built, already-click-wired `main` element together with its
/// menu-bearing dropdown caret. /// menu-bearing dropdown caret.
fn split_pill<'a>( fn split_pill<'a>(
@ -628,22 +578,26 @@ fn split_pill<'a>(
caret: Element<'a, Message>, caret: Element<'a, Message>,
active: bool, active: bool,
) -> Element<'a, Message> { ) -> 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)) container(row![main, caret].spacing(3).align_y(iced::Center))
.style(move |_: &Theme| container::Style { .style(move |theme: &Theme| {
background: Some(Background::Color(bg)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(if active {
palette.primary.weak.color
} else {
palette.background.weakest.color
})),
border: Border { border: Border {
color: border_color, color: if active {
palette.primary.base.color
} else {
palette.background.neutral.color
},
width: 1.0, width: 1.0,
radius: 2.0.into(), radius: 2.0.into(),
}, },
..Default::default() ..Default::default()
}
}) })
.padding([4, 6]) .padding([4, 6])
.into() .into()
@ -664,7 +618,6 @@ fn polar_pill<'a>(
let tooltip_text = format!( let tooltip_text = format!(
"Polar Tracking ({angle})\nF10 — left-click on/off\nRight-click cycles · ▾ picks angle", "Polar Tracking ({angle})\nF10 — left-click on/off\nRight-click cycles · ▾ picks angle",
); );
let color = pill_text_color(active);
// Right-click quick-cycles through the same increments the picker lists. // 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]; 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, 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( let main = mouse_area(
row![ row![
crate::ui::icons::tinted(crate::ui::icons::ST_POLAR, 17.0, color), polar_icon,
text(angle).size(11).color(color), 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) .spacing(2)
.align_y(iced::Center), .align_y(iced::Center),
@ -685,26 +652,8 @@ fn polar_pill<'a>(
.on_right_press(Message::SetPolarAngle(next_angle)); .on_right_press(Message::SetPolarAngle(next_angle));
let main = tooltip( let main = tooltip(
main, main,
container(text(tooltip_text).size(11).color(Color::WHITE)) container(text(tooltip_text).size(11))
.style(|_: &Theme| container::Style { .style(container::bordered_box)
background: Some(Background::Color(Color {
r: 0.13,
g: 0.13,
b: 0.13,
a: 0.95,
})),
border: Border {
color: Color {
r: 0.35,
g: 0.35,
b: 0.35,
a: 1.0,
},
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.padding([4, 8]), .padding([4, 8]),
TipPos::Top, TipPos::Top,
); );
@ -712,7 +661,12 @@ fn polar_pill<'a>(
let caret = status_menu::menu_bar( let caret = status_menu::menu_bar(
menu_tip( menu_tip(
mouse_area( 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)) .on_press(Message::StatusMenuTooltipHidden(true))
.into(), .into(),
@ -737,12 +691,13 @@ fn osnap_btn<'a>(
entries: Vec<StatusMenuEntry<'a>>, entries: Vec<StatusMenuEntry<'a>>,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
let on = active || snap_enabled; 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( let main = tip(
mouse_area(crate::ui::icons::tinted( mouse_area(snap_icon)
crate::ui::icons::ST_OSNAP,
17.0,
pill_text_color(on),
))
.on_press(Message::ToggleSnapEnabled) .on_press(Message::ToggleSnapEnabled)
.into(), .into(),
"Object Snap: toggle on/off\nF3", "Object Snap: toggle on/off\nF3",
@ -751,7 +706,12 @@ fn osnap_btn<'a>(
let caret = status_menu::menu_bar( let caret = status_menu::menu_bar(
menu_tip( menu_tip(
mouse_area( 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)) .on_press(Message::StatusMenuTooltipHidden(true))
.into(), .into(),
@ -768,44 +728,10 @@ fn osnap_btn<'a>(
// ── Helpers ─────────────────────────────────────────────────────────────── // ── Helpers ───────────────────────────────────────────────────────────────
fn layout_tab_context_menu(name: String) -> Element<'static, Message> { 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| { let item = |label: &'static str, msg: Message| {
button(text(label).size(12).color(TEXT_COLOR)) button(text(label).size(12))
.on_press(msg) .on_press(msg)
.style(|_: &Theme, status| button::Style { .style(button::subtle)
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,
})
.padding([4, 12]) .padding([4, 12])
.width(Length::Fill) .width(Length::Fill)
}; };
@ -818,15 +744,7 @@ fn layout_tab_context_menu(name: String) -> Element<'static, Message> {
.spacing(0) .spacing(0)
.width(160), .width(160),
) )
.style(move |_: &Theme| container::Style { .style(container::bordered_box)
background: Some(Background::Color(MENU_BG)),
border: Border {
color: MENU_BORDER,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
})
.padding([4, 0]) .padding([4, 0])
.into() .into()
} }
@ -843,46 +761,34 @@ fn space_tab<'a>(
enabled: bool, enabled: bool,
reorderable_layouts: Arc<[String]>, reorderable_layouts: Arc<[String]>,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
let bg = move |is_active: bool, hovered: bool| { let tab_style = move |theme: &Theme| {
if is_active { let palette = theme.extended_palette();
TAB_ACTIVE let text_color = if !enabled {
} else if hovered { palette.background.base.text.scale_alpha(0.42)
TAB_HOVER } else if is_active {
palette.primary.weak.text
} else { } else {
Color::TRANSPARENT palette.background.base.text.scale_alpha(0.72)
} };
}; container::Style {
background: is_active.then_some(Background::Color(palette.primary.weak.color)),
let border = Border { border: Border {
color: if is_active { color: if is_active {
ACCENT palette.primary.base.color
} else { } else {
Color::TRANSPARENT Color::TRANSPARENT
}, },
width: if is_active { 1.0 } else { 0.0 }, width: if is_active { 1.0 } else { 0.0 },
radius: 2.0.into(), radius: 2.0.into(),
}; },
text_color: Some(text_color),
let text_color = if !enabled { ..Default::default()
DISABLED_COLOR
} else if is_active {
Color::WHITE
} else {
Color {
r: 0.65,
g: 0.65,
b: 0.65,
a: 1.0,
} }
}; };
if !enabled { if !enabled {
let display = container(text(label.clone()).size(12).color(text_color)) let display = container(text(label.clone()).size(12))
.style(move |_: &Theme| container::Style { .style(tab_style)
background: Some(Background::Color(bg(is_active, false))),
border,
..Default::default()
})
.padding([4, 10]); .padding([4, 10]);
crate::ui::wrap_bar::PosReport::owned( crate::ui::wrap_bar::PosReport::owned(
format!("SB_LAYOUT_TAB:{label}"), format!("SB_LAYOUT_TAB:{label}"),
@ -899,49 +805,15 @@ fn space_tab<'a>(
.on_input(Message::LayoutRenameEdit) .on_input(Message::LayoutRenameEdit)
.on_submit(Message::LayoutRenameCommit) .on_submit(Message::LayoutRenameCommit)
.size(12) .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]) .padding([3, 6])
.width(Length::Fixed(90.0)); .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, crate::ui::icons::CLOSE,
10.0, 10.0,
Color {
r: 0.65,
g: 0.65,
b: 0.65,
a: 1.0,
},
)) ))
.on_press(Message::LayoutRenameCancel) .on_press(Message::LayoutRenameCancel)
.style(|_: &Theme, _| button::Style { .style(button::subtle)
background: Some(Background::Color(Color::TRANSPARENT)),
border: Border::default(),
shadow: iced::Shadow::default(),
snap: false,
..Default::default()
})
.padding([4, 4]); .padding([4, 4]);
row![input, cancel_btn] row![input, cancel_btn]
@ -951,12 +823,8 @@ fn space_tab<'a>(
} else { } else {
// Normal clickable tab — left click switches. Paper-layout tabs are // Normal clickable tab — left click switches. Paper-layout tabs are
// wrapped in `ContextMenu`, which owns right-click handling. // wrapped in `ContextMenu`, which owns right-click handling.
let display = container(text(label.clone()).size(12).color(text_color)) let display = container(text(label.clone()).size(12))
.style(move |_: &Theme| container::Style { .style(tab_style)
background: Some(Background::Color(bg(is_active, false))),
border,
..Default::default()
})
.padding([4, 10]); .padding([4, 10]);
let switch_msg = Message::LayoutSwitch(label.clone()); 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)) ("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 clickable = on_press.is_some();
let mut btn = button(text(label).size(12).color(text_color)) let mut btn = button(text(label).size(12))
.style(move |_: &Theme, status| button::Style { .style(move |theme: &Theme, status| {
background: Some(Background::Color(match status { let mut style = button::subtle(theme, status);
button::Status::Hovered if clickable => bg_hover, if active {
_ => bg_normal, let palette = theme.extended_palette();
})), style.background = Some(Background::Color(match status {
border: Border { button::Status::Hovered if clickable => palette.primary.base.color,
color: border_color, _ => palette.primary.weak.color,
width: 1.0, }));
radius: 2.0.into(), style.text_color = palette.primary.weak.text;
}, style.border.color = palette.primary.base.color;
text_color, style.border.width = 1.0;
shadow: iced::Shadow::default(), }
snap: false, style
}) })
.padding([4, 7]); .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<String>) -> Element<'static, Message> { fn status_pill(label: impl Into<String>) -> Element<'static, Message> {
container(text(label.into()).size(12).color(Color { container(text(label.into()).size(12))
r: 0.65, .style(container::bordered_box)
g: 0.65,
b: 0.65,
a: 1.0,
}))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(PILL_BG)),
border: Border {
color: BORDER_COLOR,
width: 1.0,
radius: 2.0.into(),
},
..Default::default()
})
.padding([4, 8]) .padding([4, 8])
.into() .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 ──────────────────────────────────────────────────── // ── Scale popup button ────────────────────────────────────────────────────
/// Shared visual root for labelled status-bar menus (units, scale, …). /// 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> { fn action_pill(label: &str, msg: Message) -> Element<'static, Message> {
let label = label.to_string(); let label = label.to_string();
button( button(text(label).size(12))
text(label)
.size(12)
.color(OSNAP_OFF_TEXT),
)
.on_press(msg) .on_press(msg)
.style(|_: &Theme, status| button::Style { .style(button::subtle)
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,
})
.padding([4, 7]) .padding([4, 7])
.into() .into()
} }

View file

@ -51,57 +51,30 @@ pub fn menu_bar<'a>(
.safe_bounds_margin(0.0) .safe_bounds_margin(0.0)
.close_on_background_click_global(true) .close_on_background_click_global(true)
.draw_path(DrawPath::Backdrop) .draw_path(DrawPath::Backdrop)
.style(|_: &Theme, _| iced_aw::style::menu_bar::Style { .style(|theme: &Theme, _| {
bar_background: Background::Color(Color::TRANSPARENT), let palette = theme.extended_palette();
bar_border: Border::default(), iced_aw::style::menu_bar::Style {
bar_shadow: Shadow::default(), bar_background: Background::Color(Color::TRANSPARENT),
menu_background: Background::Color(MENU_BG), bar_border: Border::default(),
menu_border: Border { bar_shadow: Shadow::default(),
color: MENU_BORDER, menu_background: Background::Color(palette.background.weakest.color),
width: 1.0, menu_border: Border {
radius: 3.0.into(), color: palette.background.neutral.color,
}, width: 1.0,
menu_shadow: Shadow { radius: 3.0.into(),
color: Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.35,
}, },
offset: iced::Vector::new(0.0, -2.0), menu_shadow: Shadow {
blur_radius: 6.0, color: palette.background.strongest.color.scale_alpha(0.35),
}, offset: iced::Vector::new(0.0, -2.0),
path: Background::Color(ACTIVE_BG), blur_radius: 6.0,
path_border: Border { },
color: ACTIVE_BORDER, path: Background::Color(palette.primary.weak.color),
width: 1.0, path_border: Border {
radius: 2.0.into(), color: palette.primary.base.color,
}, width: 1.0,
radius: 2.0.into(),
},
}
}) })
.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,
};

View file

@ -1,7 +1,7 @@
//! Status-bar customization and layout-list menu entries. //! Status-bar customization and layout-list menu entries.
use iced::widget::{button, row, text}; 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::app::Message;
use crate::ui::statusbar::statusbar_config::{StatusBarConfig, StatusPill}; 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> { fn layout_row<'a>(name: String, is_current: bool) -> Element<'a, Message> {
let lbl = text(name.clone()) let lbl = text(name.clone()).size(11);
.size(11)
.color(if is_current { LABEL_ON } else { LABEL_OFF });
button(row![lbl].align_y(iced::Center)) button(row![lbl].align_y(iced::Center))
.on_press(Message::LayoutSwitch(name)) .on_press(Message::LayoutSwitch(name))
.style(move |_: &Theme, status| button::Style { .style(move |theme: &Theme, status| {
background: Some(Background::Color(match (is_current, status) { let mut style = button::subtle(theme, status);
(_, button::Status::Hovered) => ROW_HOVER, if is_current && status == button::Status::Active {
(true, _) => Color { let palette = theme.extended_palette();
r: 0.18, style.background = Some(Background::Color(palette.primary.weak.color));
g: 0.26, style.text_color = palette.primary.weak.text;
b: 0.36, }
a: 1.0, style
},
_ => Color::TRANSPARENT,
})),
..Default::default()
}) })
.width(Fill) .width(Fill)
.padding([4, 12]) .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> { 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) let lbl = text(label).size(11);
.size(11)
.color(if checked { LABEL_ON } else { LABEL_OFF });
let content = row![check, lbl].spacing(6).align_y(iced::Center); let content = row![check, lbl].spacing(6).align_y(iced::Center);
button(content) button(content)
.on_press(msg) .on_press(msg)
.style(|_: &Theme, status| button::Style { .style(button::subtle)
background: Some(Background::Color(match status {
button::Status::Hovered => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.width(Fill) .width(Fill)
.padding([4, 10]) .padding([4, 10])
.into() .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,
};

View file

@ -7,16 +7,9 @@
//! the style / scale managers' frame so it looks consistent. //! the style / scale managers' frame so it looks consistent.
use crate::app::Message; 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::widget::{column, container, mouse_area, row, scrollable, text, Space};
use iced::{Background, Border, Color, Element, Fill, Theme}; use iced::{Background, Border, Element, Fill, Theme};
const MEMBER_CHECK: Color = Color {
r: 0.30,
g: 0.82,
b: 0.36,
a: 1.0,
};
/// `scales` is `(name, "paper:drawing" ratio, is_member)`. Every label is cloned /// `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. /// into the widget tree, so the returned element borrows nothing from the args.
@ -26,17 +19,17 @@ pub fn view_window(
) -> Element<'static, Message> { ) -> Element<'static, Message> {
let toolbar = container( let toolbar = container(
row![ row![
text(format!("Object: {object_label}")) text(format!("Object: {object_label}")).size(11),
.size(11)
.color(TEXT),
Space::new().width(Fill), Space::new().width(Fill),
tb_button("Close", Message::CloseModal, true), tb_button("Close", Message::CloseModal, true),
] ]
.spacing(4) .spacing(4)
.align_y(iced::Center), .align_y(iced::Center),
) )
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(TB)), background: Some(Background::Color(
theme.extended_palette().background.weak.color
)),
..Default::default() ..Default::default()
}) })
.width(Fill) .width(Fill)
@ -45,21 +38,17 @@ pub fn view_window(
let rows: Vec<Element<'_, Message>> = scales let rows: Vec<Element<'_, Message>> = scales
.iter() .iter()
.map(|(name, ratio, member)| { .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![ let label = row![
check, check,
text(name.clone()).size(11).color(TEXT).width(Fill), text(name.clone()).size(11).width(Fill),
text(ratio.clone()).size(10).color(DIM), text(ratio.clone()).size(10).style(muted_text_style),
] ]
.spacing(4) .spacing(4)
.align_y(iced::Center); .align_y(iced::Center);
let cell = container(label) let cell = container(label)
.padding([4, 8]) .padding([4, 8])
.width(Fill) .width(Fill);
.style(move |_: &Theme| container::Style {
text_color: Some(TEXT),
..Default::default()
});
mouse_area(cell) mouse_area(cell)
.on_press(Message::AnnoObjectScaleToggle(name.clone())) .on_press(Message::AnnoObjectScaleToggle(name.clone()))
.into() .into()
@ -67,14 +56,17 @@ pub fn view_window(
.collect(); .collect();
let list = container(scrollable(column(rows).spacing(1)).height(Fill)) let list = container(scrollable(column(rows).spacing(1)).height(Fill))
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(LIST)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.weak.color)),
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 3.0.into(), radius: 3.0.into(),
}, },
..Default::default() ..Default::default()
}
}) })
.width(Fill) .width(Fill)
.height(Fill) .height(Fill)
@ -84,7 +76,7 @@ pub fn view_window(
column![ column![
text("Click a scale to add or remove the object's representation for it.") text("Click a scale to add or remove the object's representation for it.")
.size(10) .size(10)
.color(DIM), .style(muted_text_style),
list, list,
] ]
.spacing(6) .spacing(6)
@ -95,8 +87,10 @@ pub fn view_window(
.padding(12); .padding(12);
container(column![toolbar, hdivider(), body]) container(column![toolbar, hdivider(), body])
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(BG)), background: Some(Background::Color(
theme.extended_palette().background.base.color
)),
..Default::default() ..Default::default()
}) })
.width(Fill) .width(Fill)

View file

@ -4,44 +4,7 @@ use crate::app::{ColorPickTarget, DsField, Message};
use iced::widget::{ use iced::widget::{
button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Space, button, checkbox, 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};
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,
};
/// All DimStyle field values needed by the view. /// All DimStyle field values needed by the view.
pub struct DimStyleValues<'a> { 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 { fn tab_btn_style(active: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
move |_: &Theme, st| button::Style { move |theme: &Theme, st| {
background: Some(Background::Color(match (active, st) { let palette = theme.extended_palette();
(true, _) => ACTIVE, let pair = match (active, st) {
(false, button::Status::Hovered | button::Status::Pressed) => Color { (true, _) => palette.primary.strong,
r: 0.28, (false, button::Status::Hovered | button::Status::Pressed) => {
g: 0.28, palette.background.strong
b: 0.28, }
a: 1.0, _ => palette.background.weak,
}, };
_ => Color { button::Style {
r: 0.20, background: Some(Background::Color(pair.color)),
g: 0.20, text_color: pair.text,
b: 0.20,
a: 1.0,
},
})),
text_color: TEXT,
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 3.0.into(), radius: 3.0.into(),
}, },
..Default::default() ..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 { text_input::Style {
background: Background::Color(FIELD), background: Background::Color(palette.background.base.color),
border: Border { border: Border {
color: BORDER, color: border,
width: 1.0, width: 1.0,
radius: 3.0.into(), radius: 3.0.into(),
}, },
icon: TEXT, icon: palette.background.base.text,
placeholder: DIM, placeholder: palette.background.base.text.scale_alpha(0.48),
value: TEXT, value: palette.background.base.text,
selection: ACCENT, 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)) container(Space::new().width(Fill).height(1))
.width(Fill) .width(Fill)
.height(1) .height(1)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(BORDER)), background: Some(Background::Color(
theme.extended_palette().background.neutral.color
)),
..Default::default() ..Default::default()
}) })
.into() .into()
@ -221,7 +199,7 @@ pub fn view_window<'a>(
] ]
.spacing(2); .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> { let mk_field = |fld: DsField, val: &'a str| -> Element<'a, Message> {
text_input("", val) text_input("", val)
@ -338,7 +316,7 @@ pub fn view_window<'a>(
let tab_content: Element<'_, Message> = match tab { let tab_content: Element<'_, Message> = match tab {
0 => column![ 0 => column![
text("Dimension Line").size(11).color(ACCENT), text("Dimension Line").size(11).style(primary_style),
row![ row![
lbl("Extension (DIMDLE)"), lbl("Extension (DIMDLE)"),
mk_field(DsField::Dimdle, vals.dimdle) mk_field(DsField::Dimdle, vals.dimdle)
@ -359,7 +337,7 @@ pub fn view_window<'a>(
.align_y(iced::Center), .align_y(iced::Center),
chk("Suppress 1st line (DIMSD1)", vals.dimsd1, DsField::Dimsd1), chk("Suppress 1st line (DIMSD1)", vals.dimsd1, DsField::Dimsd1),
chk("Suppress 2nd line (DIMSD2)", vals.dimsd2, DsField::Dimsd2), 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![ row![
lbl("Extension (DIMEXE)"), lbl("Extension (DIMEXE)"),
mk_field(DsField::Dimexe, vals.dimexe) mk_field(DsField::Dimexe, vals.dimexe)
@ -421,7 +399,7 @@ pub fn view_window<'a>(
.spacing(7) .spacing(7)
.into(), .into(),
1 => column![ 1 => column![
text("Arrows").size(11).color(ACCENT), text("Arrows").size(11).style(primary_style),
hrow( hrow(
"Arrowhead (DIMBLK)", "Arrowhead (DIMBLK)",
vals.block_opts.clone(), vals.block_opts.clone(),
@ -485,7 +463,7 @@ pub fn view_window<'a>(
.spacing(7) .spacing(7)
.into(), .into(),
2 => column![ 2 => column![
text("Text").size(11).color(ACCENT), text("Text").size(11).style(primary_style),
row![ row![
lbl("Height (DIMTXT)"), lbl("Height (DIMTXT)"),
mk_field(DsField::Dimtxt, vals.dimtxt) mk_field(DsField::Dimtxt, vals.dimtxt)
@ -551,7 +529,7 @@ pub fn view_window<'a>(
.spacing(7) .spacing(7)
.into(), .into(),
3 => column![ 3 => column![
text("Scale").size(11).color(ACCENT), text("Scale").size(11).style(primary_style),
chk("Annotative", vals.annotative, DsField::Annotative), chk("Annotative", vals.annotative, DsField::Annotative),
row![ row![
lbl("Overall scale (DIMSCALE)"), lbl("Overall scale (DIMSCALE)"),
@ -565,7 +543,7 @@ pub fn view_window<'a>(
] ]
.spacing(8) .spacing(8)
.align_y(iced::Center), .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), enum_field("Format (DIMLUNIT)", DsField::Dimlunit, vals.dimlunit, OPT_LUNIT),
row![ row![
lbl("Decimals (DIMDEC)"), lbl("Decimals (DIMDEC)"),
@ -636,7 +614,7 @@ pub fn view_window<'a>(
("3", "Leading & trailing"), ("3", "Leading & trailing"),
], ],
), ),
text("Fit").size(11).color(ACCENT), text("Fit").size(11).style(primary_style),
enum_field( enum_field(
"Fit (DIMATFIT)", "Fit (DIMATFIT)",
DsField::Dimatfit, DsField::Dimatfit,
@ -680,7 +658,7 @@ pub fn view_window<'a>(
.spacing(7) .spacing(7)
.into(), .into(),
5 => column![ 5 => column![
text("Alternate Units").size(11).color(ACCENT), text("Alternate Units").size(11).style(primary_style),
chk( chk(
"Enable alternate units (DIMALT)", "Enable alternate units (DIMALT)",
vals.dimalt, vals.dimalt,
@ -728,7 +706,7 @@ pub fn view_window<'a>(
.spacing(7) .spacing(7)
.into(), .into(),
_ => column![ _ => column![
text("Tolerances").size(11).color(ACCENT), text("Tolerances").size(11).style(primary_style),
chk("Generate tolerances (DIMTOL)", vals.dimtol, DsField::Dimtol), chk("Generate tolerances (DIMTOL)", vals.dimtol, DsField::Dimtol),
chk("Limits generation (DIMLIM)", vals.dimlim, DsField::Dimlim), chk("Limits generation (DIMLIM)", vals.dimlim, DsField::Dimlim),
row![ row![
@ -773,7 +751,7 @@ pub fn view_window<'a>(
// content container instead of the panel padding. // content container instead of the panel padding.
let right_panel = container( let right_panel = container(
column![ column![
text(format!("Editing: {selected}")).size(11).color(DIM), text(format!("Editing: {selected}")).size(11).style(muted_style),
tabs, tabs,
hdivider(), hdivider(),
scrollable(container(tab_content).padding([12, 12]).width(Fill)) scrollable(container(tab_content).padding([12, 12]).width(Fill))

View file

@ -4,63 +4,41 @@ use crate::app::Message;
use iced::widget::{ use iced::widget::{
button, checkbox, column, container, pick_list, row, scrollable, text, text_input, button, checkbox, column, container, pick_list, row, scrollable, text, text_input,
}; };
use iced::{Background, Border, Color, Element, Fill, Theme}; use iced::{Background, Border, 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,
};
fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style { fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
move |_: &Theme, st| button::Style { move |theme: &Theme, st| {
background: Some(Background::Color(match (accent, st) { let palette = theme.extended_palette();
(true, button::Status::Hovered | button::Status::Pressed) => Color { let pair = match (accent, st) {
r: 0.20, (true, button::Status::Hovered | button::Status::Pressed) => palette.primary.strong,
g: 0.42, (false, button::Status::Hovered | button::Status::Pressed) => {
b: 0.72, palette.background.strong
a: 1.0, }
}, (true, _) => palette.primary.base,
(false, button::Status::Hovered | button::Status::Pressed) => Color { _ => palette.background.weak,
r: 0.28, };
g: 0.28, button::Style {
b: 0.28, background: Some(Background::Color(pair.color)),
a: 1.0, text_color: pair.text,
},
(true, _) => ACCENT,
_ => Color {
r: 0.22,
g: 0.22,
b: 0.22,
a: 1.0,
},
})),
text_color: TEXT,
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 4.0.into(), radius: 4.0.into(),
}, },
..Default::default() ..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> { 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>( fn num_row<'a>(
@ -116,7 +94,7 @@ fn num_row<'a>(
field: &'static str, field: &'static str,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
row![ row![
text(label).size(11).color(DIM).width(150), text(label).size(11).style(muted_style).width(150),
text_input(placeholder, value) text_input(placeholder, value)
.on_input(move |v| Message::MLeaderStyleEdit { field, value: v }) .on_input(move |v| Message::MLeaderStyleEdit { field, value: v })
.size(11) .size(11)
@ -150,7 +128,7 @@ fn color_row<'a>(
Message::MLeaderColorMore(field), Message::MLeaderColorMore(field),
Message::OpenColorWindow(crate::app::ColorPickTarget::MLeader(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) .spacing(8)
.align_y(iced::Center) .align_y(iced::Center)
.into() .into()
@ -163,7 +141,7 @@ fn enum_row<'a>(
field: &'static str, field: &'static str,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
row![ 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| { pick_list(options, Some(selected), move |value| {
Message::MLeaderStyleSetEnum { field, value } Message::MLeaderStyleSetEnum { field, value }
}) })
@ -178,7 +156,7 @@ fn enum_row<'a>(
fn lineweight_row<'a>(selected: acadrust::types::LineWeight) -> Element<'a, Message> { fn lineweight_row<'a>(selected: acadrust::types::LineWeight) -> Element<'a, Message> {
let selected = crate::ui::properties::LwItem(selected); let selected = crate::ui::properties::LwItem(selected);
row![ row![
text("Line weight:").size(11).color(DIM).width(150), text("Line weight:").size(11).style(muted_style).width(150),
pick_list( pick_list(
crate::ui::properties::lw_options(), crate::ui::properties::lw_options(),
Some(selected), Some(selected),
@ -220,7 +198,7 @@ fn handle_row<'a>(
field: &'static str, field: &'static str,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
row![ 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| { pick_list(options, Some(selected), move |value| {
Message::MLeaderStyleSetHandle { field, value } Message::MLeaderStyleSetHandle { field, value }
}) })
@ -247,7 +225,7 @@ pub fn view_window<'a>(v: MLeaderStyleView<'a>) -> Element<'a, Message> {
scrollable( scrollable(
column![ column![
row![ row![
text("Name:").size(11).color(DIM).width(150), text("Name:").size(11).style(muted_style).width(150),
text(s.name.clone()).size(11), text(s.name.clone()).size(11),
] ]
.spacing(8), .spacing(8),
@ -422,7 +400,7 @@ pub fn view_window<'a>(v: MLeaderStyleView<'a>) -> Element<'a, Message> {
.height(Fill) .height(Fill)
.into() .into()
} else { } 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]) .padding([12, 12])
.into() .into()
}; };

View file

@ -2,14 +2,13 @@
use crate::app::Message; use crate::app::Message;
use iced::widget::{column, container, row, scrollable, text}; use iced::widget::{column, container, row, scrollable, text};
use iced::{Color, Element, Fill}; use iced::{Element, Fill, Theme};
const DIM: Color = Color { fn muted_style(theme: &Theme) -> iced::widget::text::Style {
r: 0.55, iced::widget::text::Style {
g: 0.55, color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
b: 0.55, }
a: 1.0, }
};
pub fn view_window<'a>( pub fn view_window<'a>(
styles: Vec<String>, styles: Vec<String>,
@ -22,7 +21,7 @@ pub fn view_window<'a>(
// ── Right: Details panel ────────────────────────────────────────────── // ── Right: Details panel ──────────────────────────────────────────────
let info_row = |label: &'static str, val: String| -> Element<'_, Message> { let info_row = |label: &'static str, val: String| -> Element<'_, Message> {
row![ row![
text(label).size(11).color(DIM).width(120), text(label).size(11).style(muted_style).width(120),
text(val).size(11), text(val).size(11),
] ]
.spacing(8) .spacing(8)
@ -49,7 +48,7 @@ pub fn view_window<'a>(
&e.linetype &e.linetype
}; };
row![ 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(format!("{:+.3}", e.offset)).size(10).width(70),
text(color_str).size(10).width(90), text(color_str).size(10).width(90),
text(lt).size(10), text(lt).size(10),
@ -65,7 +64,7 @@ pub fn view_window<'a>(
info_row("Elements:", s.elements.len().to_string()), info_row("Elements:", s.elements.len().to_string()),
text(" Off Color Ltype") text(" Off Color Ltype")
.size(10) .size(10)
.color(DIM) .style(muted_style)
.into(), .into(),
]; ];
col_items.extend(elem_rows); col_items.extend(elem_rows);
@ -73,7 +72,7 @@ pub fn view_window<'a>(
.height(Fill) .height(Fill)
.into() .into()
} else { } 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]) .padding([12, 12])
.into() .into()
}; };

View file

@ -2,108 +2,55 @@
use crate::app::Message; use crate::app::Message;
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space}; use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
use iced::{Background, Border, Color, Element, Fill, Theme}; use iced::{Background, Border, 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,
};
fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style { fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
move |_: &Theme, st| button::Style { move |theme: &Theme, st| {
background: Some(Background::Color(match (accent, st) { let palette = theme.extended_palette();
(true, button::Status::Hovered | button::Status::Pressed) => Color { let pair = match (accent, st) {
r: 0.20, (true, button::Status::Hovered | button::Status::Pressed) => palette.primary.strong,
g: 0.42, (false, button::Status::Hovered | button::Status::Pressed) => {
b: 0.72, palette.background.strong
a: 1.0, }
}, (true, _) => palette.primary.base,
(false, button::Status::Hovered | button::Status::Pressed) => Color { _ => palette.background.weak,
r: 0.28, };
g: 0.28, button::Style {
b: 0.28, background: Some(Background::Color(pair.color)),
a: 1.0, text_color: pair.text,
},
(true, _) => ACCENT,
_ => Color {
r: 0.22,
g: 0.22,
b: 0.22,
a: 1.0,
},
})),
text_color: TEXT,
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 4.0.into(), radius: 4.0.into(),
}, },
..Default::default() ..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 { text_input::Style {
background: Background::Color(FIELD), background: Background::Color(palette.background.base.color),
border: Border { border: Border {
color: BORDER, color: border,
width: 1.0, width: 1.0,
radius: 3.0.into(), radius: 3.0.into(),
}, },
icon: TEXT, icon: palette.background.base.text,
placeholder: DIM, placeholder: palette.background.base.text.scale_alpha(0.48),
value: TEXT, value: palette.background.base.text,
selection: ACCENT, 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)) container(Space::new().width(Fill).height(1))
.width(Fill) .width(Fill)
.height(1) .height(1)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(BORDER)), background: Some(Background::Color(
theme.extended_palette().background.neutral.color
)),
..Default::default() ..Default::default()
}) })
.into() .into()
@ -122,8 +71,10 @@ fn vsep<'a>() -> Element<'a, Message> {
container(Space::new().width(1).height(Fill)) container(Space::new().width(1).height(Fill))
.width(1) .width(1)
.height(Fill) .height(Fill)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(BORDER)), background: Some(Background::Color(
theme.extended_palette().background.neutral.color
)),
..Default::default() ..Default::default()
}) })
.into() .into()
@ -156,13 +107,15 @@ pub fn view_window<'a>(
.style(btn_s(false)) .style(btn_s(false))
.padding([4, 10]), .padding([4, 10]),
Space::new().width(Fill), Space::new().width(Fill),
text(table_name).size(10).color(DIM), text(table_name).size(10).style(muted_style),
] ]
.spacing(4) .spacing(4)
.align_y(iced::Center), .align_y(iced::Center),
) )
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(TB)), background: Some(Background::Color(
theme.extended_palette().background.weak.color
)),
..Default::default() ..Default::default()
}) })
.width(Fill) .width(Fill)
@ -199,19 +152,22 @@ pub fn view_window<'a>(
}; };
button(text(label).size(10).font(iced::Font::MONOSPACE)) button(text(label).size(10).font(iced::Font::MONOSPACE))
.on_press(Message::PlotStylePanelSelectAci(aci)) .on_press(Message::PlotStylePanelSelectAci(aci))
.style(move |_: &Theme, st| button::Style { .style(move |theme: &Theme, st| {
background: Some(Background::Color(match (is_sel, st) { let palette = theme.extended_palette();
(true, _) => ACTIVE, let pair = match (is_sel, st) {
(false, button::Status::Hovered | button::Status::Pressed) => Color { (true, _) => Some(palette.primary.strong),
r: 0.26, (false, button::Status::Hovered | button::Status::Pressed) => {
g: 0.26, Some(palette.background.strong)
b: 0.26, }
a: 1.0, _ => None,
}, };
_ => Color::TRANSPARENT, button::Style {
})), background: pair.map(|p| Background::Color(p.color)),
text_color: TEXT, text_color: pair
.map(|p| p.text)
.unwrap_or(palette.background.base.text),
..Default::default() ..Default::default()
}
}) })
.padding([2, 8]) .padding([2, 8])
.width(Fill) .width(Fill)
@ -221,16 +177,19 @@ pub fn view_window<'a>(
let aci_list = container( let aci_list = container(
column![ 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)) container(scrollable(column(aci_items).spacing(1)).height(Fill))
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(LIST)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.weak.color)),
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 3.0.into() radius: 3.0.into()
}, },
..Default::default() ..Default::default()
}
}) })
.width(Fill) .width(Fill)
.height(Fill) .height(Fill)
@ -269,12 +228,12 @@ pub fn view_window<'a>(
.map(|e| format!("{}%", e.screening)) .map(|e| format!("{}%", e.screening))
.unwrap_or_else(|| "—".into()); .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( let edit_panel = container(
column![ column![
row![ row![
text("ACI:").size(11).color(DIM).width(100), text("ACI:").size(11).style(muted_style).width(100),
text(format!("{selected_aci}")).size(11), text(format!("{selected_aci}")).size(11),
] ]
.spacing(8) .spacing(8)
@ -298,7 +257,7 @@ pub fn view_window<'a>(
.size(11) .size(11)
.padding([4, 8]), .padding([4, 8]),
Space::new().height(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!(" Color: {cur_color}")).size(10),
text(format!(" Lineweight: {cur_lw}")).size(10), text(format!(" Lineweight: {cur_lw}")).size(10),
text(format!(" Screening: {cur_scr}")).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); let body = row![aci_list, vsep(), edit_panel].height(Fill);
container(column![toolbar, hdivider(), body].spacing(0)) container(column![toolbar, hdivider(), body].spacing(0))
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(BG)), background: Some(Background::Color(
theme.extended_palette().background.base.color
)),
..Default::default() ..Default::default()
}) })
.width(Fill) .width(Fill)

View file

@ -7,15 +7,7 @@
use crate::app::Message; use crate::app::Message;
use iced::widget::{button, canvas, column, container, radio, row, text, text_input, Space}; 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}; use iced::{mouse, Background, Border, 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 };
const CELL_PX: f32 = 44.0; const CELL_PX: f32 = 44.0;
@ -37,22 +29,23 @@ impl canvas::Program<Message> for GlyphCanvas {
&self, &self,
_state: &(), _state: &(),
renderer: &iced::Renderer, renderer: &iced::Renderer,
_theme: &Theme, theme: &Theme,
bounds: Rectangle, bounds: Rectangle,
_cursor: mouse::Cursor, _cursor: mouse::Cursor,
) -> Vec<canvas::Geometry> { ) -> Vec<canvas::Geometry> {
let mut frame = canvas::Frame::new(renderer, bounds.size()); 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 (cx, cy) = (bounds.width * 0.5, bounds.height * 0.5);
let r = bounds.width.min(bounds.height) * 0.30; let r = bounds.width.min(bounds.height) * 0.30;
let stroke = canvas::Stroke { let stroke = canvas::Stroke {
width: 1.4, width: 1.4,
style: canvas::Style::Solid(GLYPH), style: canvas::Style::Solid(glyph),
..Default::default() ..Default::default()
}; };
let line = |a: Point, b: Point| canvas::Path::line(a, b); let line = |a: Point, b: Point| canvas::Path::line(a, b);
match self.mode & 0x0F { 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 => {} 1 => {}
2 => { 2 => {
frame.stroke(&line(Point::new(cx - r, cy), Point::new(cx + r, cy)), stroke.clone()); 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) button(glyph)
.padding(0) .padding(0)
.on_press(Message::PointStyleSetMode(value)) .on_press(Message::PointStyleSetMode(value))
.style(move |_: &Theme, status| { .style(move |theme: &Theme, status| {
let bg = if selected { let palette = theme.extended_palette();
CELL_SEL let pair = if selected {
palette.primary.strong
} else if matches!(status, button::Status::Hovered | button::Status::Pressed) { } 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 { } else {
CELL palette.background.weak
}; };
button::Style { button::Style {
background: Some(Background::Color(bg)), background: Some(Background::Color(pair.color)),
border: Border { border: Border {
color: Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 }, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 3.0.into(), radius: 3.0.into(),
}, },
text_color: pair.text,
..Default::default() ..Default::default()
} }
}) })
.into() .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 { 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 { border: Border {
color: Color { r: 0.3, g: 0.3, b: 0.3, a: 1.0 }, color: border,
width: 1.0, width: 1.0,
radius: 4.0.into(), radius: 4.0.into(),
}, },
icon: DIM, icon: palette.background.base.text,
placeholder: DIM, placeholder: palette.background.base.text.scale_alpha(0.48),
value: WHITE, value: palette.background.base.text,
selection: ACCENT, 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![ let size_row = row![
text("Point Size:").size(13).color(WHITE), text("Point Size:").size(13),
Space::new().width(10), Space::new().width(10),
text_input("0", size_buf) text_input("0", size_buf)
.on_input(Message::PointStyleSizeInput) .on_input(Message::PointStyleSizeInput)
@ -147,7 +153,7 @@ pub fn view_window<'a>(pdmode: i16, relative: bool, size_buf: &str) -> Element<'
.size(13) .size(13)
.width(110), .width(110),
Space::new().width(6), 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); .align_y(iced::Center);
@ -171,26 +177,14 @@ pub fn view_window<'a>(pdmode: i16, relative: bool, size_buf: &str) -> Element<'
] ]
.spacing(6); .spacing(6);
let ok = button(text("OK").size(13).color(WHITE)) let ok = button(text("OK").size(13))
.padding([5, 22]) .padding([5, 22])
.on_press(Message::PointStyleOk) .on_press(Message::PointStyleOk)
.style(|_: &Theme, status| { .style(button::primary);
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()
}
});
container( container(
column![ column![
text("Point Style").size(18).color(WHITE), text("Point Style").size(18),
Space::new().height(6), Space::new().height(6),
grid, grid,
Space::new().height(12), Space::new().height(12),
@ -203,8 +197,10 @@ pub fn view_window<'a>(pdmode: i16, relative: bool, size_buf: &str) -> Element<'
.spacing(4) .spacing(4)
.padding(20), .padding(20),
) )
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(BG)), background: Some(Background::Color(
theme.extended_palette().background.base.color
)),
..Default::default() ..Default::default()
}) })
.into() .into()

View file

@ -6,53 +6,36 @@
//! messages instead of the StyleKind machinery. //! messages instead of the StyleKind machinery.
use crate::app::Message; 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::{ use iced::widget::{
column, container, mouse_area, row, scrollable, text, text_input, Space, 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. /// Inline-rename text-input id, so the rename-start handler can focus it.
pub fn rename_input_id() -> iced::widget::Id { pub fn rename_input_id() -> iced::widget::Id {
iced::widget::Id::new("scale-rename-input") iced::widget::Id::new("scale-rename-input")
} }
const INPUT_BG: Color = Color { fn field_style(theme: &Theme, status: text_input::Status) -> text_input::Style {
r: 0.10, let palette = theme.extended_palette();
g: 0.10, let border = match status {
b: 0.10, text_input::Status::Focused { .. } => palette.primary.base.color,
a: 1.0, _ => palette.background.neutral.color,
}; };
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 {
text_input::Style { text_input::Style {
background: Background::Color(INPUT_BG), background: Background::Color(palette.background.base.color),
border: Border { border: Border {
color: BORDER, color: border,
width: 1.0, width: 1.0,
radius: 4.0.into(), radius: 4.0.into(),
}, },
icon: TEXT, icon: palette.background.base.text,
placeholder: DIM, placeholder: palette.background.base.text.scale_alpha(0.48),
value: TEXT, value: palette.background.base.text,
selection: Color { selection: palette.primary.base.color.scale_alpha(0.5),
r: 0.20,
g: 0.46,
b: 0.80,
a: 0.45,
},
} }
} }
@ -80,8 +63,10 @@ pub fn view_window<'a, 'b>(
.spacing(4) .spacing(4)
.align_y(iced::Center), .align_y(iced::Center),
) )
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(TB)), background: Some(Background::Color(
theme.extended_palette().background.weak.color
)),
..Default::default() ..Default::default()
}) })
.width(Fill) .width(Fill)
@ -105,21 +90,24 @@ pub fn view_window<'a, 'b>(
} }
let is_sel = name.as_str() == selected; let is_sel = name.as_str() == selected;
let is_cur = name.eq_ignore_ascii_case(current); 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![ let label = row![
check, check,
text(name.clone()).size(11).color(TEXT).width(Fill), text(name.clone()).size(11).width(Fill),
text(ratio.clone()).size(10).color(DIM), text(ratio.clone()).size(10).style(muted_text_style),
] ]
.spacing(4) .spacing(4)
.align_y(iced::Center); .align_y(iced::Center);
let cell = container(label) let cell = container(label)
.padding([4, 8]) .padding([4, 8])
.width(Fill) .width(Fill)
.style(move |_: &Theme| container::Style { .style(move |theme: &Theme| {
background: is_sel.then_some(Background::Color(ACTIVE)), let pair = theme.extended_palette().primary.strong;
text_color: Some(TEXT), container::Style {
background: is_sel.then_some(Background::Color(pair.color)),
text_color: is_sel.then_some(pair.text),
..Default::default() ..Default::default()
}
}); });
mouse_area(cell) mouse_area(cell)
.on_press(Message::ScaleManagerSelect(name.clone())) .on_press(Message::ScaleManagerSelect(name.clone()))
@ -130,16 +118,19 @@ pub fn view_window<'a, 'b>(
let list_panel = container( let list_panel = container(
column![ column![
text("Scales").size(10).color(DIM), text("Scales").size(10).style(muted_text_style),
container(scrollable(column(rows).spacing(1)).height(Fill)) container(scrollable(column(rows).spacing(1)).height(Fill))
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(LIST)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.weak.color)),
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 3.0.into() radius: 3.0.into()
}, },
..Default::default() ..Default::default()
}
}) })
.width(Fill) .width(Fill)
.height(Fill) .height(Fill)
@ -161,7 +152,7 @@ pub fn view_window<'a, 'b>(
let field = let field =
|label: &'static str, ph: &'static str, value: &str, on: fn(String) -> Message| { |label: &'static str, ph: &'static str, value: &str, on: fn(String) -> Message| {
row![ row![
text(label).size(11).color(DIM).width(96), text(label).size(11).style(muted_text_style).width(96),
text_input(ph, value) text_input(ph, value)
.on_input(on) .on_input(on)
.style(field_style) .style(field_style)
@ -175,13 +166,13 @@ pub fn view_window<'a, 'b>(
let editor = container( let editor = container(
column![ column![
text("Scale").size(10).color(DIM), text("Scale").size(10).style(muted_text_style),
field("Paper units", "1", paper_buf, Message::ScaleManagerPaperBuf), field("Paper units", "1", paper_buf, Message::ScaleManagerPaperBuf),
field("Drawing units", "50", drawing_buf, Message::ScaleManagerDrawingBuf), field("Drawing units", "50", drawing_buf, Message::ScaleManagerDrawingBuf),
Space::new().height(6), 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.") 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) .size(10)
.color(DIM), .style(muted_text_style),
] ]
.spacing(8), .spacing(8),
) )
@ -192,8 +183,10 @@ pub fn view_window<'a, 'b>(
let body = row![list_panel, vsep(), editor].height(Fill); let body = row![list_panel, vsep(), editor].height(Fill);
container(column![toolbar, hdivider(), body]) container(column![toolbar, hdivider(), body])
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(BG)), background: Some(Background::Color(
theme.extended_palette().background.base.color
)),
..Default::default() ..Default::default()
}) })
.width(Fill) .width(Fill)

View file

@ -10,7 +10,7 @@
use crate::app::{Message, StyleKind}; use crate::app::{Message, StyleKind};
use iced::widget::{container, mouse_area, row, text, text_input}; 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 /// Shared id for the inline rename field, so the rename-start handler can focus
/// it the moment the row turns editable. /// 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") 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 /// 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 /// the style being renamed (`rename_active`), otherwise a selectable row whose
/// double click starts the rename. The current style gets a green ✓. /// double click starts the rename. The current style gets a green ✓.
@ -61,16 +42,19 @@ pub fn item<'a>(
} else { } else {
// Fixed-width ✓ column keeps every name left-aligned whether or not the // Fixed-width ✓ column keeps every name left-aligned whether or not the
// row is current. // row is current.
let check = crate::ui::icons::check_cell(is_current, CURRENT_CHECK); let check = crate::ui::icons::themed_check_cell(is_current);
let label = row![check, text(name.to_string()).size(11).color(TEXT)] let label = row![check, text(name.to_string()).size(11)]
.align_y(iced::Center); .align_y(iced::Center);
let cell = container(label) let cell = container(label)
.padding([4, 8]) .padding([4, 8])
.width(Fill) .width(Fill)
.style(move |_: &Theme| container::Style { .style(move |theme: &Theme| {
background: is_selected.then_some(Background::Color(ACTIVE)), let pair = theme.extended_palette().primary.strong;
text_color: Some(TEXT), container::Style {
background: is_selected.then_some(Background::Color(pair.color)),
text_color: is_selected.then_some(pair.text),
..Default::default() ..Default::default()
}
}); });
mouse_area(cell) mouse_area(cell)
.on_press(on_select) .on_press(on_select)

View file

@ -10,52 +10,7 @@
use crate::app::{Message, StyleKind}; use crate::app::{Message, StyleKind};
use iced::widget::button::{Status, Style}; use iced::widget::button::{Status, Style};
use iced::widget::{button, column, container, row, scrollable, text, Space}; 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};
// 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,
};
/// Everything the shared frame needs. The per-manager `editor` element is the /// Everything the shared frame needs. The per-manager `editor` element is the
/// only bespoke part. /// only bespoke part.
@ -103,8 +58,10 @@ pub fn view<'a, 'b>(s: Scaffold<'a, 'b>) -> Element<'a, Message> {
.spacing(4) .spacing(4)
.align_y(iced::Center); .align_y(iced::Center);
let toolbar = container(bar) let toolbar = container(bar)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(TB)), background: Some(Background::Color(
theme.extended_palette().background.weak.color
)),
..Default::default() ..Default::default()
}) })
.width(Fill) .width(Fill)
@ -131,16 +88,19 @@ pub fn view<'a, 'b>(s: Scaffold<'a, 'b>) -> Element<'a, Message> {
let list_panel = container( let list_panel = container(
column![ column![
text("Styles").size(10).color(DIM), text("Styles").size(10).style(muted_text_style),
container(scrollable(column(rows).spacing(1)).height(Fill)) container(scrollable(column(rows).spacing(1)).height(Fill))
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(LIST)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.weak.color)),
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 3.0.into() radius: 3.0.into()
}, },
..Default::default() ..Default::default()
}
}) })
.width(Fill) .width(Fill)
.height(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); let body = row![list_panel, vsep(), s.editor].height(Fill);
container(column![toolbar, hdivider(), body]) container(column![toolbar, hdivider(), body])
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(BG)), background: Some(Background::Color(
theme.extended_palette().background.base.color
)),
..Default::default() ..Default::default()
}) })
.width(Fill) .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 { fn btn_s(accent: bool) -> impl Fn(&Theme, Status) -> Style {
move |_: &Theme, st| Style { move |theme: &Theme, st| {
background: Some(Background::Color(match (accent, st) { let palette = theme.extended_palette();
(true, Status::Hovered | Status::Pressed) => Color { let pair = match (accent, st) {
r: 0.20, (true, Status::Hovered | Status::Pressed) => palette.primary.strong,
g: 0.42, (false, Status::Hovered | Status::Pressed) => palette.background.strong,
b: 0.72, (true, _) => palette.primary.base,
a: 1.0, _ => palette.background.weak,
}, };
(false, Status::Hovered | Status::Pressed) => Color { Style {
r: 0.28, background: Some(Background::Color(pair.color)),
g: 0.28, text_color: pair.text,
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 { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 4.0.into(), radius: 4.0.into(),
}, },
..Default::default() ..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)) container(Space::new().width(Fill).height(1))
.width(Fill) .width(Fill)
.height(1) .height(1)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(BORDER)), background: Some(Background::Color(
theme.extended_palette().background.neutral.color
)),
..Default::default() ..Default::default()
}) })
.into() .into()
@ -229,8 +188,10 @@ pub(crate) fn vsep<'a>() -> Element<'a, Message> {
container(Space::new().width(1).height(Fill)) container(Space::new().width(1).height(Fill))
.width(1) .width(1)
.height(Fill) .height(Fill)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(BORDER)), background: Some(Background::Color(
theme.extended_palette().background.neutral.color
)),
..Default::default() ..Default::default()
}) })
.into() .into()

View file

@ -4,63 +4,41 @@ use crate::app::Message;
use iced::widget::{ use iced::widget::{
button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Column, button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Column,
}; };
use iced::{Background, Border, Color, Element, Fill, Theme}; use iced::{Background, Border, 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,
};
fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style { fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
move |_: &Theme, st| button::Style { move |theme: &Theme, st| {
background: Some(Background::Color(match (accent, st) { let palette = theme.extended_palette();
(true, button::Status::Hovered | button::Status::Pressed) => Color { let pair = match (accent, st) {
r: 0.20, (true, button::Status::Hovered | button::Status::Pressed) => palette.primary.strong,
g: 0.42, (false, button::Status::Hovered | button::Status::Pressed) => {
b: 0.72, palette.background.strong
a: 1.0, }
}, (true, _) => palette.primary.base,
(false, button::Status::Hovered | button::Status::Pressed) => Color { _ => palette.background.weak,
r: 0.28, };
g: 0.28, button::Style {
b: 0.28, background: Some(Background::Color(pair.color)),
a: 1.0, text_color: pair.text,
},
(true, _) => ACCENT,
_ => Color {
r: 0.22,
g: 0.22,
b: 0.22,
a: 1.0,
},
})),
text_color: TEXT,
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 4.0.into(), radius: 4.0.into(),
}, },
..Default::default() ..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 ────────────────────────────────────────────── // ── Right: Details panel ──────────────────────────────────────────────
let info_row = |label: &'static str, val: String| -> Element<'_, Message> { let info_row = |label: &'static str, val: String| -> Element<'_, Message> {
row![ row![
text(label).size(11).color(DIM).width(160), text(label).size(11).style(muted_style).width(160),
text(val).size(11), text(val).size(11),
] ]
.spacing(8) .spacing(8)
@ -108,7 +86,7 @@ pub fn view_window<'a>(
field: &'static str| field: &'static str|
-> Element<'a, Message> { -> Element<'a, Message> {
row![ row![
text(label).size(11).color(DIM).width(150), text(label).size(11).style(muted_style).width(150),
text_input(placeholder, value) text_input(placeholder, value)
.on_input(move |v| Message::TableStyleCellEdit { .on_input(move |v| Message::TableStyleCellEdit {
row, row,
@ -142,14 +120,14 @@ pub fn view_window<'a>(
Message::TableColorMore(row, field), Message::TableColorMore(row, field),
Message::OpenColorWindow(crate::app::ColorPickTarget::Table(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) .spacing(8)
.align_y(iced::Center) .align_y(iced::Center)
.into() .into()
}; };
let mut col = Column::new() let mut col = Column::new()
.spacing(3) .spacing(3)
.push(text(row_label).size(11).color(ACCENT)) .push(text(row_label).size(11).style(primary_style))
.push(cell_in( .push(cell_in(
" Text style:", " Text style:",
"Standard", "Standard",
@ -161,7 +139,7 @@ pub fn view_window<'a>(
.push(cell_color(" Fill color:", &cell_fillcolor[r], "fillcolor")) .push(cell_color(" Fill color:", &cell_fillcolor[r], "fillcolor"))
.push( .push(
row![ row![
text(" Alignment:").size(11).color(DIM).width(150), text(" Alignment:").size(11).style(muted_style).width(150),
pick_list( pick_list(
[ [
"TopLeft", "TopLeft",
@ -199,7 +177,7 @@ pub fn view_window<'a>(
.push( .push(
text(" Borders (type / weight / color / spacing / hidden)") text(" Borders (type / weight / color / spacing / hidden)")
.size(10) .size(10)
.color(DIM), .style(muted_style),
); );
let borders: [(&'static str, &acadrust::objects::TableCellBorder); 6] = [ let borders: [(&'static str, &acadrust::objects::TableCellBorder); 6] = [
@ -214,7 +192,7 @@ pub fn view_window<'a>(
let bu = b as u8; let bu = b as u8;
col = col.push( col = col.push(
row![ row![
text(format!(" {bname}")).size(11).color(DIM).width(28), text(format!(" {bname}")).size(11).style(muted_style).width(28),
pick_list( pick_list(
["Single", "Double"] ["Single", "Double"]
.iter() .iter()
@ -282,7 +260,7 @@ pub fn view_window<'a>(
column![ column![
info_row("Name:", s.name.clone()), info_row("Name:", s.name.clone()),
row![ row![
text("Description:").size(11).color(DIM).width(160), text("Description:").size(11).style(muted_style).width(160),
text_input("", description_buf) text_input("", description_buf)
.on_input(|v| Message::TableStyleEdit { .on_input(|v| Message::TableStyleEdit {
field: "description", field: "description",
@ -294,7 +272,7 @@ pub fn view_window<'a>(
.spacing(8) .spacing(8)
.align_y(iced::Center), .align_y(iced::Center),
row![ row![
text("Flow direction:").size(11).color(DIM).width(160), text("Flow direction:").size(11).style(muted_style).width(160),
pick_list( pick_list(
["Down", "Up"] ["Down", "Up"]
.iter() .iter()
@ -314,7 +292,7 @@ pub fn view_window<'a>(
.size(14) .size(14)
.text_size(11), .text_size(11),
row![ 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) text_input("1.5", hmargin_buf)
.on_input(|v| Message::TableStyleEdit { .on_input(|v| Message::TableStyleEdit {
field: "hmargin", field: "hmargin",
@ -326,7 +304,7 @@ pub fn view_window<'a>(
.spacing(8) .spacing(8)
.align_y(iced::Center), .align_y(iced::Center),
row![ 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) text_input("1.5", vmargin_buf)
.on_input(|v| Message::TableStyleEdit { .on_input(|v| Message::TableStyleEdit {
field: "vmargin", field: "vmargin",
@ -362,7 +340,7 @@ pub fn view_window<'a>(
.height(Fill) .height(Fill)
.into() .into()
} else { } 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]) .padding([12, 12])
.into() .into()
}; };

View file

@ -5,7 +5,7 @@ use crate::app::StyleKind;
use iced::widget::{ use iced::widget::{
button, canvas, checkbox, column, container, row, scrollable, text, text_input, Space, 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. /// View-model for the Text Style editor window.
pub struct TextStyleView<'a> { pub struct TextStyleView<'a> {
@ -28,49 +28,6 @@ pub struct TextStyleView<'a> {
pub rename_buf: &'a str, 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] = &[ const BUILTIN_FONTS: &[&str] = &[
"Standard", "ISO", "Simplex", "RomanS", "RomanD", "RomanC", "RomanT", "ItalicC", "ItalicT", "Standard", "ISO", "Simplex", "RomanS", "RomanD", "RomanC", "RomanT", "ItalicC", "ItalicT",
"ScriptS", "ScriptC", "GothGBT", "GothGRT", "GothITT", "GreekC", "Symbol", "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 { fn list_item(active: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
move |_: &Theme, st| button::Style { move |theme: &Theme, st| {
background: Some(Background::Color(match (active, st) { let palette = theme.extended_palette();
(true, _) => ACTIVE, let pair = match (active, st) {
(false, button::Status::Hovered | button::Status::Pressed) => Color { (true, _) => Some(palette.primary.strong),
r: 0.26, (false, button::Status::Hovered | button::Status::Pressed) => {
g: 0.26, Some(palette.background.strong)
b: 0.26, }
a: 1.0, _ => None,
}, };
_ => Color::TRANSPARENT, button::Style {
})), background: pair.map(|p| Background::Color(p.color)),
text_color: TEXT, text_color: pair.map(|p| p.text).unwrap_or(palette.background.base.text),
..Default::default() ..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 { text_input::Style {
background: Background::Color(FIELD), background: Background::Color(palette.background.base.color),
border: Border { border: Border {
color: BORDER, color: border,
width: 1.0, width: 1.0,
radius: 3.0.into(), radius: 3.0.into(),
}, },
icon: TEXT, icon: palette.background.base.text,
placeholder: DIM, placeholder: palette.background.base.text.scale_alpha(0.48),
value: TEXT, value: palette.background.base.text,
selection: ACCENT, 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)) container(Space::new().width(1).height(Fill))
.width(1) .width(1)
.height(Fill) .height(Fill)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(BORDER)), background: Some(Background::Color(
theme.extended_palette().background.neutral.color
)),
..Default::default() ..Default::default()
}) })
.into() .into()
@ -143,7 +120,7 @@ impl canvas::Program<Message> for TextPreviewCanvas {
&self, &self,
_state: &(), _state: &(),
renderer: &iced::Renderer, renderer: &iced::Renderer,
_theme: &Theme, theme: &Theme,
bounds: Rectangle, bounds: Rectangle,
_cursor: mouse::Cursor, _cursor: mouse::Cursor,
) -> Vec<canvas::Geometry> { ) -> Vec<canvas::Geometry> {
@ -185,7 +162,7 @@ impl canvas::Program<Message> for TextPreviewCanvas {
}; };
let stroke = canvas::Stroke { let stroke = canvas::Stroke {
width: 1.4, width: 1.4,
style: canvas::Style::Solid(TEXT), style: canvas::Style::Solid(theme.extended_palette().background.base.text),
..Default::default() ..Default::default()
}; };
for s in &strokes { for s in &strokes {
@ -237,16 +214,19 @@ pub fn view_window<'a>(v: TextStyleView<'a>) -> Element<'a, Message> {
let font_panel = container( let font_panel = container(
column![ 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)) container(scrollable(column(font_items).spacing(1)).height(Fill))
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(LIST)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.weak.color)),
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 3.0.into() radius: 3.0.into()
}, },
..Default::default() ..Default::default()
}
}) })
.width(Fill) .width(Fill)
.height(Fill) .height(Fill)
@ -275,7 +255,7 @@ pub fn view_window<'a>(v: TextStyleView<'a>) -> Element<'a, Message> {
field: &'static str, field: &'static str,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
row![ row![
text(label).size(11).color(DIM).width(120), text(label).size(11).style(muted_style).width(120),
text_input(ph, buf) text_input(ph, buf)
.on_input(move |v| Message::TextStyleEdit { field, value: v }) .on_input(move |v| Message::TextStyleEdit { field, value: v })
.style(field_style) .style(field_style)
@ -312,7 +292,7 @@ pub fn view_window<'a>(v: TextStyleView<'a>) -> Element<'a, Message> {
// ── Right: Properties ───────────────────────────────────────────────── // ── Right: Properties ─────────────────────────────────────────────────
let props_panel = container( let props_panel = container(
column![ column![
text("Properties").size(11).color(ACCENT), text("Properties").size(11).style(primary_style),
frow("Big Font:", "big-font file…", bigfont_buf, "bigfont"), frow("Big Font:", "big-font file…", bigfont_buf, "bigfont"),
frow("TrueType Font:", "e.g. Arial", ttf_buf, "ttf"), frow("TrueType Font:", "e.g. Arial", ttf_buf, "ttf"),
frow("Fixed Height:", "0 = variable", height_buf, "height"), 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) .size(15)
.text_size(11), .text_size(11),
Space::new().height(8), Space::new().height(8),
text("Preview").size(10).color(DIM), text("Preview").size(10).style(muted_style),
container(preview) container(preview)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(FIELD)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.base.color)),
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 4.0.into() radius: 4.0.into()
}, },
..Default::default() ..Default::default()
}
}) })
.padding(8) .padding(8)
.width(Fill), .width(Fill),
@ -382,16 +365,19 @@ pub fn view_window<'a>(v: TextStyleView<'a>) -> Element<'a, Message> {
let ttf_panel = container( let ttf_panel = container(
column![ 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)) container(scrollable(column(ttf_items).spacing(1)).height(Fill))
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(LIST)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.weak.color)),
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 3.0.into() radius: 3.0.into()
}, },
..Default::default() ..Default::default()
}
}) })
.width(Fill) .width(Fill)
.height(Fill) .height(Fill)

View file

@ -1,48 +1,16 @@
use crate::app::Message; use crate::app::Message;
use iced::widget::{button, column, container, row, text}; use iced::widget::{button, column, container, row, text};
use iced::{Background, Border, Color, Element, Theme}; use iced::{Background, 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,
};
fn info_row<'a>(label: &'static str, value: String) -> Element<'a, Message> { fn info_row<'a>(label: &'static str, value: String) -> Element<'a, Message> {
row![ row![
text(label).size(11).color(DIM).width(100), text(label)
text(value).size(11).color(WHITE), .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) .spacing(8)
.align_y(iced::Center) .align_y(iced::Center)
@ -57,10 +25,16 @@ pub fn view_window<'a>() -> Element<'a, Message> {
let logo = container( let logo = container(
column![ 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") text("CAD application for Architecture & Engineering")
.size(11) .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) .spacing(4)
.align_x(iced::Center), .align_x(iced::Center),
@ -82,35 +56,11 @@ pub fn view_window<'a>() -> Element<'a, Message> {
.spacing(2) .spacing(2)
.padding([12, 16]), .padding([12, 16]),
) )
.style(|_: &Theme| container::Style { .style(container::bordered_box);
background: Some(Background::Color(PANEL)),
border: Border {
color: BORDER,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
});
let copy_btn = button(text("Copy Info").size(11)) let copy_btn = button(text("Copy Info").size(11))
.on_press(Message::AboutCopyInfo) .on_press(Message::AboutCopyInfo)
.style(|_: &Theme, st| button::Style { .style(button::primary)
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()
})
.padding([6, 16]); .padding([6, 16]);
let footer = row![copy_btn] let footer = row![copy_btn]
@ -132,8 +82,10 @@ pub fn view_window<'a>() -> Element<'a, Message> {
left: 20.0, left: 20.0,
}), }),
) )
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(BG)), background: Some(Background::Color(
theme.extended_palette().background.base.color,
)),
..Default::default() ..Default::default()
}) })
.into() .into()

View file

@ -6,7 +6,7 @@
use crate::app::Message; use crate::app::Message;
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space}; 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. /// Which column of an alias row a text edit targets.
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
@ -18,33 +18,21 @@ pub enum AliasField {
/// Right-hand lane reserved for the scrollbar so it never overlaps the ✕ column. /// Right-hand lane reserved for the scrollbar so it never overlaps the ✕ column.
const GUTTER: f32 = 16.0; const GUTTER: f32 = 16.0;
const BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 }; fn muted_style(theme: &Theme) -> iced::widget::text::Style {
const FIELD_BG: Color = Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 }; iced::widget::text::Style {
const BORDER: Color = Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 }; color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
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 },
} }
} }
/// Build the alias editor content. `rows` is the live working buffer. /// Build the alias editor content. `rows` is the live working buffer.
pub fn view_window(rows: &[(String, String)]) -> Element<'_, Message> { 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( let hint = text(
"Type an alias and the command it runs (e.g. L → LINE). \ "Type an alias and the command it runs (e.g. L → LINE). \
Apply to save to ocad.pgp; closing discards unapplied edits.", Apply to save to ocad.pgp; closing discards unapplied edits.",
) )
.size(11) .size(11)
.color(DIM); .style(muted_style);
// Right gutter reserved so the scrollbar has its own lane and never sits on // 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 // 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( let head = container(
row![ row![
container(text("Alias").size(11).color(DIM)).width(Length::Fixed(120.0)), container(text("Alias").size(11).style(muted_style)).width(Length::Fixed(120.0)),
container(text("Command").size(11).color(DIM)).width(Fill), container(text("Command").size(11).style(muted_style)).width(Fill),
Space::new().width(Length::Fixed(30.0)), Space::new().width(Length::Fixed(30.0)),
] ]
.spacing(8), .spacing(8),
@ -65,31 +53,18 @@ pub fn view_window(rows: &[(String, String)]) -> Element<'_, Message> {
for (idx, (alias, cmd)) in rows.iter().enumerate() { for (idx, (alias, cmd)) in rows.iter().enumerate() {
let alias_box = text_input("alias", alias) let alias_box = text_input("alias", alias)
.on_input(move |v| Message::AliasEditorInput { idx, field: AliasField::Alias, value: v }) .on_input(move |v| Message::AliasEditorInput { idx, field: AliasField::Alias, value: v })
.style(field_style)
.size(13) .size(13)
.padding([3, 6]) .padding([3, 6])
.width(Length::Fixed(120.0)); .width(Length::Fixed(120.0));
let cmd_box = text_input("command", cmd) let cmd_box = text_input("command", cmd)
.on_input(move |v| Message::AliasEditorInput { idx, field: AliasField::Command, value: v }) .on_input(move |v| Message::AliasEditorInput { idx, field: AliasField::Command, value: v })
.style(field_style)
.size(13) .size(13)
.padding([3, 6]) .padding([3, 6])
.width(Fill); .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)) .on_press(Message::AliasEditorRemove(idx))
.padding([2, 6]) .padding([2, 6])
.style(|_: &Theme, status| { .style(button::danger);
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()
}
});
list = list.push( list = list.push(
row![alias_box, cmd_box, del] row![alias_box, cmd_box, del]
.spacing(8) .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) .on_press(Message::AliasEditorAdd)
.padding([4, 10]) .padding([4, 10])
.style(|_: &Theme, status| { .style(button::secondary);
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()
}
});
// Apply — primary action; commits the rows to ocad.pgp and stays open. // 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) .on_press(Message::AliasEditorApply)
.padding([4, 16]) .padding([4, 16])
.style(|_: &Theme, status| { .style(button::primary);
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()
}
});
container( container(
column![ column![
@ -149,8 +100,10 @@ pub fn view_window(rows: &[(String, String)]) -> Element<'_, Message> {
.padding(12) .padding(12)
.width(Fill) .width(Fill)
.height(Fill) .height(Fill)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(BG)), background: Some(Background::Color(
theme.extended_palette().background.base.color,
)),
..Default::default() ..Default::default()
}) })
.into() .into()

View file

@ -19,23 +19,10 @@ use acadrust::types::{Color as AcadColor, LineWeight};
use iced::widget::{ use iced::widget::{
button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Space, 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}; 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; const LABEL_W: f32 = 120.0;
/// Which tab of the editor is showing. /// Which tab of the editor is showing.
@ -146,28 +133,25 @@ pub fn color_from_label(label: &str) -> Option<AcadColor> {
}) })
} }
fn field_style(_t: &Theme, _s: text_input::Status) -> text_input::Style { fn muted_style(theme: &Theme) -> iced::widget::text::Style {
text_input::Style { iced::widget::text::Style {
background: Background::Color(FIELD_BG), color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
border: Border { color: BORDER, width: 1.0, radius: 3.0.into() },
icon: WHITE,
placeholder: DIM,
value: WHITE,
selection: ACCENT,
} }
} }
fn accent_btn(_t: &Theme, status: button::Status) -> button::Style { fn field_style(theme: &Theme, status: text_input::Status) -> text_input::Style {
let bg = if matches!(status, button::Status::Hovered | button::Status::Pressed) { let palette = theme.extended_palette();
Color { r: 0.20, g: 0.42, b: 0.72, a: 1.0 } let border = match status {
} else { text_input::Status::Focused { .. } => palette.primary.base.color,
ACCENT _ => palette.background.neutral.color,
}; };
button::Style { text_input::Style {
background: Some(Background::Color(bg)), background: Background::Color(palette.background.base.color),
text_color: WHITE, border: Border { color: border, width: 1.0, radius: 3.0.into() },
border: Border { color: BORDER, width: 1.0, radius: 4.0.into() }, icon: palette.background.base.text,
..Default::default() 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)) container(Space::new().width(Length::Fill).height(1))
.width(Length::Fill) .width(Length::Fill)
.height(1) .height(1)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(BORDER)), background: Some(Background::Color(
theme.extended_palette().background.neutral.color
)),
..Default::default() ..Default::default()
}) })
.into() .into()
@ -186,7 +172,7 @@ fn hdivider<'a>() -> Element<'a, Message> {
/// One `label : widget` row with a fixed-width label column. /// One `label : widget` row with a fixed-width label column.
fn field_row<'a>(label: &'a str, widget: Element<'a, Message>) -> Element<'a, Message> { fn field_row<'a>(label: &'a str, widget: Element<'a, Message>) -> Element<'a, Message> {
row![ row![
container(text(label).size(12).color(DIM)).width(LABEL_W), container(text(label).size(12).style(muted_style)).width(LABEL_W),
widget, widget,
] ]
.spacing(8) .spacing(8)
@ -226,20 +212,28 @@ fn pick_field<'a>(
fn tab_button<'a>(label: &'a str, this: AttrTab, active: AttrTab) -> Element<'a, Message> { fn tab_button<'a>(label: &'a str, this: AttrTab, active: AttrTab) -> Element<'a, Message> {
let is_active = this == active; let is_active = this == active;
button(text(label).size(11).color(WHITE)) button(text(label).size(11))
.padding([4, 12]) .padding([4, 12])
.on_press(Message::AttrEditorTab(this)) .on_press(Message::AttrEditorTab(this))
.style(move |_t: &Theme, status| button::Style { .style(move |theme: &Theme, status| {
background: Some(Background::Color(match (is_active, status) { let palette = theme.extended_palette();
(true, _) => ACTIVE, let pair = match (is_active, status) {
(true, _) => palette.primary.strong,
(false, button::Status::Hovered | button::Status::Pressed) => { (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 }, _ => palette.background.weak,
})), };
text_color: WHITE, button::Style {
border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, 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() ..Default::default()
}
}) })
.into() .into()
} }
@ -259,20 +253,22 @@ pub fn view_window<'a>(
) -> Element<'a, Message> { ) -> Element<'a, Message> {
// ── Top toolbar: block name on the left, Apply on the right ─────────── // ── Top toolbar: block name on the left, Apply on the right ───────────
// Mirrors the style-manager windows (actions left, primary action 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]) .padding([4, 14])
.on_press(Message::AttrEditorApply) .on_press(Message::AttrEditorApply)
.style(accent_btn); .style(button::primary);
let toolbar = container( let toolbar = container(
row![ row![
text(format!("Block: {block}")).size(12).color(DIM), text(format!("Block: {block}")).size(12).style(muted_style),
Space::new().width(Length::Fill), Space::new().width(Length::Fill),
apply, apply,
] ]
.align_y(iced::Center), .align_y(iced::Center),
) )
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(TB)), background: Some(Background::Color(
theme.extended_palette().background.weak.color
)),
..Default::default() ..Default::default()
}) })
.width(Length::Fill) .width(Length::Fill)
@ -286,7 +282,7 @@ pub fn view_window<'a>(
.spacing(2); .spacing(2);
let body: Element<'_, Message> = if rows.is_empty() { 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 { } else {
match tab { match tab {
AttrTab::Attribute => attribute_tab(rows, selected), AttrTab::Attribute => attribute_tab(rows, selected),
@ -303,8 +299,10 @@ pub fn view_window<'a>(
.padding(12); .padding(12);
container(column![toolbar, hdivider(), content]) container(column![toolbar, hdivider(), content])
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(BG)), background: Some(Background::Color(
theme.extended_palette().background.base.color
)),
..Default::default() ..Default::default()
}) })
.width(Length::Fill) .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. /// 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> { fn attribute_tab<'a>(rows: &'a [AttrRow], selected: usize) -> Element<'a, Message> {
let head = row![ let head = row![
container(text("Tag").size(11).color(DIM)).width(130), container(text("Tag").size(11).style(muted_style)).width(130),
container(text("Prompt").size(11).color(DIM)).width(Length::Fill), container(text("Prompt").size(11).style(muted_style)).width(Length::Fill),
container(text("Value").size(11).color(DIM)).width(140), container(text("Value").size(11).style(muted_style)).width(140),
] ]
.spacing(6); .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() { for (idx, r) in rows.iter().enumerate() {
let is_sel = idx == selected; let is_sel = idx == selected;
let line = row![ let line = row![
container(text(r.tag.as_str()).size(12).color(WHITE)).width(130), container(text(r.tag.as_str()).size(12)).width(130),
container(text(r.prompt.as_str()).size(12).color(DIM)).width(Length::Fill), container(text(r.prompt.as_str()).size(12).style(muted_style)).width(Length::Fill),
container(text(r.value.as_str()).size(12).color(WHITE)).width(140), container(text(r.value.as_str()).size(12)).width(140),
] ]
.spacing(6); .spacing(6);
let btn = button(line) let btn = button(line)
.on_press(Message::AttrEditorSelect(idx)) .on_press(Message::AttrEditorSelect(idx))
.padding([3, 4]) .padding([3, 4])
.width(Length::Fill) .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 hovered = matches!(status, button::Status::Hovered);
let bg = if is_sel { let pair = if is_sel {
ROW_SEL palette.primary.strong
} else if hovered { } else if hovered {
Color { r: 0.18, g: 0.18, b: 0.18, a: 1.0 } palette.background.strong
} else { } else {
ROW_BG palette.background.weak
}; };
button::Style { button::Style {
background: Some(Background::Color(bg)), background: Some(Background::Color(pair.color)),
text_color: WHITE, text_color: pair.text,
border: Border::default(), border: Border::default(),
..Default::default() ..Default::default()
} }

View file

@ -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). /// scrolled into view after it is added (#271).
pub const LAYER_TABLE_SCROLL_ID: &str = "layer-manager-table-scroll"; 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 ──────────────────────────────────────────────────────────── // ── Layer data ────────────────────────────────────────────────────────────
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -313,39 +342,15 @@ impl LayerPanel {
.size(FONT_SZ) .size(FONT_SZ)
.padding([3, 6]) .padding([3, 6])
.width(Length::Fixed(180.0)) .width(Length::Fixed(180.0))
.style(|_: &Theme, _| iced::widget::text_input::Style { .style(table_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,
},
}),
] ]
.spacing(2) .spacing(2)
.align_y(iced::Center), .align_y(iced::Center),
) )
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(TOOLBAR_BG)), background: Some(Background::Color(
theme.extended_palette().background.weak.color
)),
..Default::default() ..Default::default()
}) })
.width(Fill) .width(Fill)
@ -355,13 +360,15 @@ impl LayerPanel {
let sc = self.sort_col; let sc = self.sort_col;
let sa = self.sort_asc; let sa = self.sort_asc;
let mut header_row = row![ 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), sortable_header("Name", LayerSortCol::Name, Length::Fixed(name_col_w), sc, sa),
// Draggable divider: adjusts the Name column width (#359). // Draggable divider: adjusts the Name column width (#359).
iced::widget::mouse_area( iced::widget::mouse_area(
container(iced::widget::Space::new().width(2).height(14)).style( container(iced::widget::Space::new().width(2).height(14)).style(
|_: &Theme| container::Style { |theme: &Theme| container::Style {
background: Some(Background::Color(BORDER_COLOR)), background: Some(Background::Color(
theme.extended_palette().background.neutral.color
)),
..Default::default() ..Default::default()
}, },
), ),
@ -390,20 +397,23 @@ impl LayerPanel {
header_row = header_row.push( header_row = header_row.push(
text(vp.label.as_str()) text(vp.label.as_str())
.size(10) .size(10)
.color(DIM) .style(muted_style)
.width(Length::Fixed(COL_ICON)), .width(Length::Fixed(COL_ICON)),
); );
} }
let col_header = container(header_row) let col_header = container(header_row)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(COL_HEADER_BG)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.weak.color)),
border: Border { border: Border {
color: BORDER_COLOR, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 0.0.into(), radius: 0.0.into(),
}, },
..Default::default() ..Default::default()
}
}) })
.padding([4, 8]) .padding([4, 8])
.width(Fill); .width(Fill);
@ -451,8 +461,10 @@ impl LayerPanel {
// ── Full-window frame ───────────────────────────────────────────── // ── Full-window frame ─────────────────────────────────────────────
container(column![toolbar, col_header, table].spacing(0)) container(column![toolbar, col_header, table].spacing(0))
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(PANEL_BG)), background: Some(Background::Color(
theme.extended_palette().background.base.color
)),
..Default::default() ..Default::default()
}) })
.width(Fill) .width(Fill)
@ -478,28 +490,26 @@ fn sortable_header<'a>(
active: Option<LayerSortCol>, active: Option<LayerSortCol>,
asc: bool, asc: bool,
) -> Element<'a, Message> { ) -> 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) .spacing(3)
.align_y(iced::Center); .align_y(iced::Center);
if active == Some(col) { if active == Some(col) {
content = content.push(if asc { content = content.push(if asc {
crate::ui::icons::arrow_up(8.0, DIM) crate::ui::icons::themed_arrow_up(8.0)
} else { } else {
crate::ui::icons::arrow_down(8.0, DIM) crate::ui::icons::themed_arrow_down(8.0)
}); });
} }
button(content) button(content)
.on_press(Message::LayerSort(col)) .on_press(Message::LayerSort(col))
.style(|_: &Theme, status| button::Style { .style(|theme: &Theme, status| button::Style {
background: Some(Background::Color(match status { background: matches!(
button::Status::Hovered | button::Status::Pressed => Color { status,
r: 0.30, button::Status::Hovered | button::Status::Pressed
g: 0.30, )
b: 0.30, .then_some(Background::Color(
a: 1.0, theme.extended_palette().background.strong.color
}, )),
_ => Color::TRANSPARENT,
})),
..Default::default() ..Default::default()
}) })
.padding(Padding { .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> { fn toolbar_btn<'a>(icon: &'static [u8], label: &'a str, msg: Message) -> Element<'a, Message> {
button( button(
row![ row![
crate::ui::icons::tinted(icon, 12.0, Color::WHITE), crate::ui::icons::themed(icon, 12.0),
text(label).size(11).color(Color::WHITE), text(label).size(11),
] ]
.spacing(5) .spacing(5)
.align_y(iced::Center), .align_y(iced::Center),
) )
.on_press(msg) .on_press(msg)
.style(|_: &Theme, status| button::Style { .style(|theme: &Theme, status| {
background: Some(Background::Color(match status { let palette = theme.extended_palette();
button::Status::Hovered => Color { let pair = match status {
r: 0.32, button::Status::Hovered | button::Status::Pressed => {
g: 0.32, palette.background.strong
b: 0.32, }
a: 1.0, _ => palette.background.weak,
}, };
button::Status::Pressed => Color { button::Style {
r: 0.25, background: Some(Background::Color(pair.color)),
g: 0.25,
b: 0.25,
a: 1.0,
},
_ => Color {
r: 0.26,
g: 0.26,
b: 0.26,
a: 1.0,
},
})),
border: Border { border: Border {
radius: 3.0.into(), radius: 3.0.into(),
color: BORDER_COLOR, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
}, },
text_color: Color::WHITE, text_color: pair.text,
..Default::default() ..Default::default()
}
}) })
.padding([4, 10]) .padding([4, 10])
.into() .into()
@ -563,46 +563,46 @@ fn toolbar_btn_cond<'a>(
msg: Message, msg: Message,
enabled: bool, enabled: bool,
) -> Element<'a, Message> { ) -> 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( let mut b = button(
row![ row![
crate::ui::icons::tinted(icon, 12.0, fg), if enabled {
text(label).size(11).color(fg), 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) .spacing(5)
.align_y(iced::Center), .align_y(iced::Center),
) )
.style(|_: &Theme, status| button::Style { .style(move |theme: &Theme, status| {
background: Some(Background::Color(match status { let palette = theme.extended_palette();
button::Status::Hovered => Color { let pair = match status {
r: 0.32, button::Status::Hovered if enabled => palette.background.strong,
g: 0.32, _ => palette.background.weak,
b: 0.32, };
a: 1.0, button::Style {
}, background: Some(Background::Color(pair.color)),
_ => Color {
r: 0.26,
g: 0.26,
b: 0.26,
a: 1.0,
},
})),
border: Border { border: Border {
radius: 3.0.into(), radius: 3.0.into(),
color: BORDER_COLOR, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
}, },
text_color: Color::WHITE, text_color: if enabled {
pair.text
} else {
pair.text.scale_alpha(0.42)
},
..Default::default() ..Default::default()
}
}) })
.padding([4, 10]); .padding([4, 10]);
if enabled { if enabled {
@ -616,21 +616,25 @@ fn toolbar_btn_cond<'a>(
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
/// Hover popup showing a layer's full name when the cell truncates it. /// Hover popup showing a layer's full name when the cell truncates it.
fn name_tip<'a>(name: &'a str) -> Element<'a, Message> { 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 { .padding(Padding {
top: 3.0, top: 3.0,
bottom: 3.0, bottom: 3.0,
left: 7.0, left: 7.0,
right: 7.0, right: 7.0,
}) })
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(PANEL_BG)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.strong.color)),
border: Border { border: Border {
color: BORDER_COLOR, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 3.0.into(), radius: 3.0.into(),
}, },
text_color: Some(palette.background.strong.text),
..Default::default() ..Default::default()
}
}) })
.into() .into()
} }
@ -655,16 +659,10 @@ fn layer_row<'a>(
.height(ICON_SZ), .height(ICON_SZ),
) )
.on_press(on_press) .on_press(on_press)
.style(|_: &Theme, status| button::Style { .style(|theme: &Theme, status| button::Style {
background: Some(Background::Color(match status { background: matches!(status, button::Status::Hovered).then_some(
button::Status::Hovered => Color { Background::Color(theme.extended_palette().background.strong.color)
r: 0.35, ),
g: 0.35,
b: 0.35,
a: 1.0,
},
_ => Color::TRANSPARENT,
})),
..Default::default() ..Default::default()
}) })
.padding(Padding { .padding(Padding {
@ -682,27 +680,9 @@ fn layer_row<'a>(
let lck_svg = crate::ui::icons::layer_lock(layer.locked); let lck_svg = crate::ui::icons::layer_lock(layer.locked);
let status_dot: Element<'_, Message> = if is_current { let status_dot: Element<'_, Message> = if is_current {
crate::ui::icons::tinted( crate::ui::icons::themed_success(crate::ui::icons::CHECK, 13.0)
crate::ui::icons::CHECK,
13.0,
Color {
r: 0.25,
g: 0.85,
b: 0.45,
a: 1.0,
},
)
} else { } else {
crate::ui::icons::tinted( crate::ui::icons::themed_secondary(crate::ui::icons::DOT, 9.0)
crate::ui::icons::DOT,
9.0,
Color {
r: 0.55,
g: 0.55,
b: 0.55,
a: 1.0,
},
)
}; };
// Name cell // Name cell
@ -717,38 +697,7 @@ fn layer_row<'a>(
left: 4.0, left: 4.0,
right: 4.0, right: 4.0,
}) })
.style(|_: &Theme, _| iced::widget::text_input::Style { .style(table_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,
},
})
.width(Length::Fixed(name_col_w)) .width(Length::Fixed(name_col_w))
.into() .into()
} else { } else {
@ -756,20 +705,13 @@ fn layer_row<'a>(
let name_budget = ((name_col_w / 6.0) as usize).max(8); let name_budget = ((name_col_w / 6.0) as usize).max(8);
let name_btn = button( let name_btn = button(
text(crate::ui::text_util::elide(&layer.name, name_budget)) text(crate::ui::text_util::elide(&layer.name, name_budget))
.size(FONT_SZ) .size(FONT_SZ),
.color(ROW_TEXT),
) )
.on_press(Message::LayerRenameStart(index)) .on_press(Message::LayerRenameStart(index))
.style(|_: &Theme, status| button::Style { .style(|theme: &Theme, status| button::Style {
background: Some(Background::Color(match status { background: matches!(status, button::Status::Hovered).then_some(
button::Status::Hovered => Color { Background::Color(theme.extended_palette().background.strong.color)
r: 0.30, ),
g: 0.30,
b: 0.30,
a: 1.0,
},
_ => Color::TRANSPARENT,
})),
..Default::default() ..Default::default()
}) })
.padding(Padding { .padding(Padding {
@ -837,7 +779,7 @@ fn layer_row<'a>(
} else { } else {
text(layer.linetype.as_str()) text(layer.linetype.as_str())
.size(FONT_SZ) .size(FONT_SZ)
.color(DIM) .style(muted_style)
.width(Length::Fixed(COL_LT)) .width(Length::Fixed(COL_LT))
.into() .into()
}; };
@ -861,7 +803,7 @@ fn layer_row<'a>(
} else { } else {
text(cur_lw_item.to_string()) text(cur_lw_item.to_string())
.size(FONT_SZ) .size(FONT_SZ)
.color(DIM) .style(muted_style)
.width(Length::Fixed(COL_LW)) .width(Length::Fixed(COL_LW))
.into() .into()
}; };
@ -877,33 +819,13 @@ fn layer_row<'a>(
left: 4.0, left: 4.0,
right: 4.0, right: 4.0,
}) })
.style(|_: &Theme, _| iced::widget::text_input::Style { .style(|theme: &Theme, status| {
background: iced::Background::Color(Color::TRANSPARENT), let mut style = table_input_style(theme, status);
border: Border { style.background = iced::Background::Color(Color::TRANSPARENT);
radius: 2.0.into(), style
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,
},
}) })
.width(Length::Fixed(COL_TRANS)); .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![ let mut row_content = row![
container(status_dot) container(status_dot)
.width(50) .width(50)
@ -945,9 +867,20 @@ fn layer_row<'a>(
mouse_area( mouse_area(
container(row_content) container(row_content)
.style(move |_: &Theme| container::Style { .style(move |theme: &Theme| {
background: Some(Background::Color(bg)), 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() ..Default::default()
}
}) })
.padding(Padding { .padding(Padding {
top: 0.0, top: 0.0,
@ -965,31 +898,10 @@ fn layer_row<'a>(
// ── Combo style ──────────────────────────────────────────────────────────── // ── Combo style ────────────────────────────────────────────────────────────
fn combo_input_style( fn combo_input_style(
_theme: &Theme, theme: &Theme,
_status: iced::widget::text_input::Status, status: iced::widget::text_input::Status,
) -> iced::widget::text_input::Style { ) -> iced::widget::text_input::Style {
iced::widget::text_input::Style { table_input_style(theme, status)
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,
},
}
} }
// ── Display helpers ─────────────────────────────────────────────────────── // ── Display helpers ───────────────────────────────────────────────────────
@ -1054,67 +966,3 @@ const COL_COLOR: f32 = 90.0;
const COL_LT: f32 = 110.0; const COL_LT: f32 = 110.0;
const COL_LW: f32 = 90.0; const COL_LW: f32 = 90.0;
const COL_TRANS: f32 = 80.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,
};

View file

@ -2,131 +2,37 @@
use crate::app::Message; use crate::app::Message;
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space}; 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 { fn muted_style(theme: &Theme) -> iced::widget::text::Style {
r: 0.13, iced::widget::text::Style {
g: 0.13, color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
b: 0.13, }
a: 1.0, }
};
const BG: Color = Color { fn primary_style(theme: &Theme) -> iced::widget::text::Style {
r: 0.15, iced::widget::text::Style {
g: 0.15, color: Some(theme.extended_palette().primary.base.color),
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 btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style { fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
move |_: &Theme, st| button::Style { move |theme: &Theme, status| {
background: Some(Background::Color(match (accent, st) { if accent {
(true, button::Status::Hovered | button::Status::Pressed) => Color { button::primary(theme, status)
r: 0.20, } else {
g: 0.42, button::secondary(theme, status)
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()
} }
} }
fn list_item(active: bool) -> impl Fn(&Theme, button::Status) -> button::Style { fn list_item(active: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
move |_: &Theme, st| button::Style { move |theme: &Theme, status| {
background: Some(Background::Color(match (active, st) { if active {
(true, _) => ACTIVE, button::primary(theme, status)
(false, button::Status::Hovered | button::Status::Pressed) => Color { } else {
r: 0.26, button::subtle(theme, status)
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,
} }
} }
@ -134,8 +40,10 @@ fn hdivider<'a>() -> Element<'a, Message> {
container(Space::new().width(Fill).height(1)) container(Space::new().width(Fill).height(1))
.width(Fill) .width(Fill)
.height(1) .height(1)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(BORDER)), background: Some(Background::Color(
theme.extended_palette().background.neutral.color,
)),
..Default::default() ..Default::default()
}) })
.into() .into()
@ -145,8 +53,10 @@ fn vsep<'a>() -> Element<'a, Message> {
container(Space::new().width(1).height(Fill)) container(Space::new().width(1).height(Fill))
.width(1) .width(1)
.height(Fill) .height(Fill)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(BORDER)), background: Some(Background::Color(
theme.extended_palette().background.neutral.color,
)),
..Default::default() ..Default::default()
}) })
.into() .into()
@ -169,34 +79,18 @@ pub fn view_window<'a>(
.padding([4, 10]), .padding([4, 10]),
button(text("Delete").size(11)) button(text("Delete").size(11))
.on_press(Message::LayoutManagerDelete) .on_press(Message::LayoutManagerDelete)
.style(move |_: &Theme, st| button::Style { .style(move |theme: &Theme, status| {
background: Some(Background::Color(match st { if is_model {
button::Status::Hovered | button::Status::Pressed => Color { button::secondary(theme, status)
r: 0.60, } else {
g: 0.20, button::danger(theme, status)
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()
}) })
.padding([4, 10]), .padding([4, 10]),
Space::new().width(Fill), Space::new().width(Fill),
button( button(
row![ 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), text("Move Left").size(11),
] ]
.spacing(4) .spacing(4)
@ -208,7 +102,7 @@ pub fn view_window<'a>(
button( button(
row![ row![
text("Move Right").size(11), text("Move Right").size(11),
crate::ui::icons::arrow_right(9.0, Color::WHITE), crate::ui::icons::themed_arrow_right(9.0),
] ]
.spacing(4) .spacing(4)
.align_y(iced::Center), .align_y(iced::Center),
@ -224,8 +118,10 @@ pub fn view_window<'a>(
.spacing(4) .spacing(4)
.align_y(iced::Center), .align_y(iced::Center),
) )
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(TB)), background: Some(Background::Color(
theme.extended_palette().background.weakest.color,
)),
..Default::default() ..Default::default()
}) })
.width(Fill) .width(Fill)
@ -241,11 +137,7 @@ pub fn view_window<'a>(
.spacing(5) .spacing(5)
.align_y(iced::Center); .align_y(iced::Center);
if is_cur { if is_cur {
item_row = item_row.push(crate::ui::icons::tinted( item_row = item_row.push(crate::ui::icons::themed_arrow_left(8.0));
crate::ui::icons::TRI_LEFT_B,
8.0,
Color::WHITE,
));
} }
button(item_row) button(item_row)
.on_press(Message::LayoutManagerSelect(name.clone())) .on_press(Message::LayoutManagerSelect(name.clone()))
@ -258,17 +150,9 @@ pub fn view_window<'a>(
let layout_list = container( let layout_list = container(
column![ column![
text("Layouts").size(10).color(DIM), text("Layouts").size(10).style(muted_style),
container(scrollable(column(list_items).spacing(2)).height(Fill)) container(scrollable(column(list_items).spacing(2)).height(Fill))
.style(|_: &Theme| container::Style { .style(container::bordered_box)
background: Some(Background::Color(LIST)),
border: Border {
color: BORDER,
width: 1.0,
radius: 3.0.into()
},
..Default::default()
})
.width(Fill) .width(Fill)
.height(Fill) .height(Fill)
.padding(2), .padding(2),
@ -293,38 +177,38 @@ pub fn view_window<'a>(
} else { } else {
"Paper Space Layout" "Paper Space Layout"
}) })
.size(13) .size(13),
.color(TEXT),
Space::new().height(8), Space::new().height(8),
row![ row![
text("Name:").size(11).color(DIM).width(80), text("Name:").size(11).style(muted_style).width(80),
text(selected).size(11), text(selected).size(11),
] ]
.spacing(8) .spacing(8)
.align_y(iced::Center), .align_y(iced::Center),
row![ row![
text("Status:").size(11).color(DIM).width(80), text("Status:").size(11).style(muted_style).width(80),
text(if selected == current.as_str() { text(if selected == current.as_str() {
"Active" "Active"
} else { } else {
"Inactive" "Inactive"
}) })
.size(11) .size(11)
.color(if selected == current.as_str() { .style(move |theme: &Theme| {
ACCENT if selected == current.as_str() {
} else { primary_style(theme)
DIM } else {
muted_style(theme)
}
}), }),
] ]
.spacing(8) .spacing(8)
.align_y(iced::Center), .align_y(iced::Center),
Space::new().height(16), Space::new().height(16),
text("Rename").size(10).color(DIM), text("Rename").size(10).style(muted_style),
row![ row![
text_input("New name…", rename_buf) text_input("New name…", rename_buf)
.on_input(Message::LayoutManagerRenameBuf) .on_input(Message::LayoutManagerRenameBuf)
.on_submit(Message::LayoutManagerRenameCommit) .on_submit(Message::LayoutManagerRenameCommit)
.style(field_style)
.size(11) .size(11)
.padding([4, 8]), .padding([4, 8]),
button(text("OK").size(11)) button(text("OK").size(11))
@ -343,8 +227,10 @@ pub fn view_window<'a>(
let body = row![layout_list, vsep(), details].height(Fill); let body = row![layout_list, vsep(), details].height(Fill);
container(column![toolbar, hdivider(), body].spacing(0)) container(column![toolbar, hdivider(), body].spacing(0))
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(BG)), background: Some(Background::Color(
theme.extended_palette().background.base.color,
)),
..Default::default() ..Default::default()
}) })
.width(Fill) .width(Fill)

View file

@ -5,7 +5,7 @@
use iced::time::Instant; use iced::time::Instant;
use iced::widget::{button, column, container, row, stack, text, Space}; 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 std::sync::atomic::Ordering;
use crate::app::{ use crate::app::{
@ -60,13 +60,10 @@ pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Messag
.width(Length::Fixed(fill_width)) .width(Length::Fixed(fill_width))
.height(Length::Fixed(BAR_TRACK_HEIGHT)), .height(Length::Fixed(BAR_TRACK_HEIGHT)),
) )
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(Color { background: Some(Background::Color(
r: 0.30, theme.extended_palette().primary.base.color
g: 0.62, )),
b: 0.95,
a: 1.0,
})),
border: Border { border: Border {
radius: 3.0.into(), radius: 3.0.into(),
..Default::default() ..Default::default()
@ -86,13 +83,10 @@ pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Messag
let bar_track: Element<'_, Message> = container( let bar_track: Element<'_, Message> = container(
stack![ stack![
container(Space::new().width(Length::Fixed(BAR_TRACK_WIDTH)).height(Length::Fixed(BAR_TRACK_HEIGHT))) container(Space::new().width(Length::Fixed(BAR_TRACK_WIDTH)).height(Length::Fixed(BAR_TRACK_HEIGHT)))
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(Color { background: Some(Background::Color(
r: 0.18, theme.extended_palette().background.strong.color
g: 0.18, )),
b: 0.18,
a: 1.0,
})),
border: Border { border: Border {
radius: 3.0.into(), radius: 3.0.into(),
..Default::default() ..Default::default()
@ -107,9 +101,7 @@ pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Messag
.into(); .into();
// ── Card body ──────────────────────────────────────────────────────── // ── Card body ────────────────────────────────────────────────────────
let title = text("Opening file") let title = text("Opening file").size(15);
.size(15)
.color(Color::WHITE);
let name_line = text(format!( 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) format_size(progress.size_bytes)
)) ))
.size(13) .size(13)
.color(Color { .style(|theme: &Theme| iced::widget::text::Style {
r: 0.82, color: Some(theme.extended_palette().background.base.text.scale_alpha(0.82)),
g: 0.82,
b: 0.82,
a: 1.0,
}); });
let phase_line = text(format!( 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 basis_points as f32 / 100.0
)) ))
.size(12) .size(12)
.color(Color { .style(|theme: &Theme| iced::widget::text::Style {
r: 0.70, color: Some(theme.extended_palette().primary.base.color),
g: 0.80,
b: 0.95,
a: 1.0,
}); });
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) .on_press(Message::OpenCancel)
.style(|_: &Theme, status| { .style(button::danger)
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()
}
})
.padding([4, 14]) .padding([4, 14])
.into(); .into();
@ -187,35 +137,25 @@ pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Messag
.width(Length::Fixed(CARD_WIDTH)), .width(Length::Fixed(CARD_WIDTH)),
) )
.padding([18, 22]) .padding([18, 22])
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(Color { let palette = theme.extended_palette();
r: 0.13, container::Style {
g: 0.13, background: Some(Background::Color(palette.background.weak.color)),
b: 0.13,
a: 0.98,
})),
border: Border { border: Border {
color: Color { color: palette.background.neutral.color,
r: 0.45,
g: 0.45,
b: 0.45,
a: 1.0,
},
width: 1.0, width: 1.0,
radius: 6.0.into(), radius: 6.0.into(),
}, },
..Default::default() ..Default::default()
}
}); });
// ── Backdrop (click-blocker + dim) ──────────────────────────────────── // ── Backdrop (click-blocker + dim) ────────────────────────────────────
let backdrop: Element<'_, Message> = container(Space::new().width(Fill).height(Fill)) let backdrop: Element<'_, Message> = container(Space::new().width(Fill).height(Fill))
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(Color { background: Some(Background::Color(
r: 0.0, theme.extended_palette().background.strong.color.scale_alpha(0.72)
g: 0.0, )),
b: 0.0,
a: 0.55,
})),
..Default::default() ..Default::default()
}) })
.width(Fill) .width(Fill)

View file

@ -1,96 +1,123 @@
use crate::app::config::UiThemeConfig;
use crate::app::Message; use crate::app::Message;
use iced::widget::{button, column, container, pick_list, row, text, Space}; use iced::widget::{
use iced::{Background, Border, Color, Element, Fill, Theme}; 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> { pub fn view_window<'a>(
const BG: Color = Color { default_save_format: &'a str,
r: 0.15, ui_theme: &'a UiThemeConfig,
g: 0.15, theme_color_inputs: &'a [String; 6],
b: 0.17, ) -> Element<'a, Message> {
a: 1.0, let selected_format = crate::io::SAVE_FORMAT_OPTIONS
};
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
.iter() .iter()
.copied() .copied()
.find(|candidate| *candidate == default_save_format); .find(|candidate| *candidate == default_save_format);
let close = button(text("Close").size(12).color(TEXT)) let theme_options = Theme::ALL
.on_press(Message::CloseModal) .iter()
.padding([5, 16]) .map(ToString::to_string)
.style(|_: &Theme, status| button::Style { .chain(std::iter::once("Custom".to_string()))
background: Some(Background::Color(match status { .collect::<Vec<_>>();
button::Status::Hovered | button::Status::Pressed => Color { let selected_theme = Some(ui_theme.name.clone());
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 body = column![ let palette = ui_theme.palette.to_iced();
text("Open and Save").size(15).color(TEXT), let colors = [
Space::new().height(16), ("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![ row![
text("Default save format:").size(12).color(TEXT).width(150), text("Default save format:").size(12).width(150),
pick_list(crate::io::SAVE_FORMAT_OPTIONS, selected, |format: &str| { pick_list(
Message::DefaultSaveFormatChanged(format.to_string()) crate::io::SAVE_FORMAT_OPTIONS,
}) selected_format,
|format: &str| Message::DefaultSaveFormatChanged(format.to_string())
)
.width(Fill), .width(Fill),
] ]
.spacing(12) .spacing(12)
.align_y(iced::Center), .align_y(iced::Center),
Space::new().height(10), Space::new().height(8),
text( 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) .size(11),
.color(DIM) Space::new().height(22),
.width(Fill), text("Theme").size(15),
Space::new().height(Fill), Space::new().height(10),
row![Space::new().width(Fill), close], 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) .spacing(0)
.width(Fill);
let body = column![
scrollable(content).height(Fill),
Space::new().height(12),
row![Space::new().width(Fill), close],
]
.width(Fill) .width(Fill)
.height(Fill); .height(Fill);
container(body) container(body)
.style(|_: &Theme| container::Style { .style(container::rounded_box)
background: Some(Background::Color(BG)),
..Default::default()
})
.padding([16, 18]) .padding([16, 18])
.width(Fill) .width(Fill)
.height(Fill) .height(Fill)

View file

@ -10,17 +10,7 @@ use iced::widget::{
button, checkbox, column, container, mouse_area, pick_list, row, scrollable, text, text_input, button, checkbox, column, container, mouse_area, pick_list, row, scrollable, text, text_input,
Space, Space,
}; };
use iced::{Background, Border, Color, Element, Fill, Length, Theme}; use iced::{Background, Border, 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 };
/// Sentinel entries in the printer dropdown (not real printer names). /// Sentinel entries in the printer dropdown (not real printer names).
pub const OUT_DEFAULT: &str = "System default printer"; 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 { fn btn(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
move |_: &Theme, st| button::Style { move |theme: &Theme, st| {
background: Some(Background::Color(match (accent, st) { let palette = theme.extended_palette();
(true, button::Status::Hovered | button::Status::Pressed) => { let pair = match (accent, st) {
Color { r: 0.20, g: 0.42, b: 0.72, a: 1.0 } (true, button::Status::Hovered | button::Status::Pressed) => palette.primary.strong,
}
(false, button::Status::Hovered | button::Status::Pressed) => { (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, (true, _) => palette.primary.base,
_ => Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 }, _ => palette.background.weak,
})), };
text_color: TEXT, button::Style {
border: Border { color: BORDER, width: 1.0, radius: 4.0.into() }, 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(), shadow: iced::Shadow::default(),
snap: false, 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 { text_input::Style {
background: Background::Color(FIELD), background: Background::Color(palette.background.base.color),
border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, border: Border { color: border, width: 1.0, radius: 3.0.into() },
icon: TEXT, icon: palette.background.base.text,
placeholder: DIM, placeholder: palette.background.base.text.scale_alpha(0.48),
value: TEXT, value: palette.background.base.text,
selection: ACCENT, 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)) container(Space::new().width(Fill).height(1))
.width(Fill) .width(Fill)
.height(1) .height(1)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(BORDER)), background: Some(Background::Color(
theme.extended_palette().background.neutral.color
)),
..Default::default() ..Default::default()
}) })
.into() .into()
} }
fn section_label<'a>(s: &'static str) -> Element<'a, Message> { 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> { fn vsep<'a>() -> Element<'a, Message> {
container(Space::new().width(1).height(Fill)) container(Space::new().width(1).height(Fill))
.width(1) .width(1)
.height(Fill) .height(Fill)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(BORDER)), background: Some(Background::Color(
theme.extended_palette().background.neutral.color
)),
..Default::default() ..Default::default()
}) })
.into() .into()
@ -295,13 +306,16 @@ fn setup_row<'a>(
.into(); .into();
} }
let is_sel = name == selected; 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]) .padding([4, 8])
.width(Fill) .width(Fill)
.style(move |_: &Theme| container::Style { .style(move |theme: &Theme| {
background: is_sel.then_some(Background::Color(ACTIVE)), let palette = theme.extended_palette();
text_color: Some(TEXT), 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() ..Default::default()
}
}); });
mouse_area(cell) mouse_area(cell)
.on_press(Message::PlotDlg(PlotDlgMsg::SelectSetup(name.to_string()))) .on_press(Message::PlotDlg(PlotDlgMsg::SelectSetup(name.to_string())))
@ -321,7 +335,7 @@ fn drop_row<'a>(
.text_size(12) .text_size(12)
.padding([3, 6]) .padding([3, 6])
.width(Length::Fill); .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) .spacing(8)
.align_y(iced::Center) .align_y(iced::Center)
.into() .into()
@ -335,7 +349,7 @@ fn field_row<'a>(
width: u16, width: u16,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
row![ row![
text(label).size(11).color(DIM).width(92), text(label).size(11).style(muted_style).width(92),
text_input("", value) text_input("", value)
.on_input(move |s| Message::PlotDlg(ctor(s))) .on_input(move |s| Message::PlotDlg(ctor(s)))
.style(field_style) .style(field_style)
@ -413,8 +427,10 @@ pub fn view_window(s: &PlotDialogState) -> Element<'_, Message> {
] ]
.align_y(iced::Center), .align_y(iced::Center),
) )
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(TB)), background: Some(Background::Color(
theme.extended_palette().background.weak.color
)),
..Default::default() ..Default::default()
}) })
.width(Fill) .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)) .map(|name| setup_row(name, &s.selected_setup, renaming, rename_buf))
.collect(); .collect();
let list_body: Element<'_, Message> = if rows.is_empty() { 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]) .padding([6, 8])
.into() .into()
} else { } else {
@ -453,16 +469,19 @@ pub fn view_window(s: &PlotDialogState) -> Element<'_, Message> {
}; };
let list_panel = container( let list_panel = container(
column![ column![
text("Page setups").size(10).color(DIM), text("Page setups").size(10).style(muted_style),
container(list_body) container(list_body)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(LIST)), let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.weak.color)),
border: Border { border: Border {
color: BORDER, color: palette.background.neutral.color,
width: 1.0, width: 1.0,
radius: 3.0.into(), radius: 3.0.into(),
}, },
..Default::default() ..Default::default()
}
}) })
.width(Fill) .width(Fill)
.height(Fill) .height(Fill)
@ -550,11 +569,18 @@ pub fn view_window(s: &PlotDialogState) -> Element<'_, Message> {
hdivider(), hdivider(),
section_label("Plot style table (pen assignments)"), section_label("Plot style table (pen assignments)"),
row![ row![
container(text(style_label).size(12).color(TEXT)) container(text(style_label).size(12))
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(FIELD)), let palette = theme.extended_palette();
border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, 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() ..Default::default()
}
}) })
.padding([4, 8]) .padding([4, 8])
.width(Fill), .width(Fill),
@ -583,7 +609,7 @@ pub fn view_window(s: &PlotDialogState) -> Element<'_, Message> {
.width(Length::Fill) .width(Length::Fill)
) )
.width(Fill), .width(Fill),
text("DPI").size(11).color(DIM), text("DPI").size(11).style(muted_style),
text_input("", &s.dpi) text_input("", &s.dpi)
.on_input(move |v| Message::PlotDlg(PlotDlgMsg::Dpi(v))) .on_input(move |v| Message::PlotDlg(PlotDlgMsg::Dpi(v)))
.style(field_style) .style(field_style)
@ -631,8 +657,10 @@ pub fn view_window(s: &PlotDialogState) -> Element<'_, Message> {
let body = row![list_panel, vsep(), detail].height(Fill); let body = row![list_panel, vsep(), detail].height(Fill);
container(column![toolbar, hdivider(), body].spacing(0)) container(column![toolbar, hdivider(), body].spacing(0))
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(BG)), background: Some(Background::Color(
theme.extended_palette().background.base.color
)),
..Default::default() ..Default::default()
}) })
.width(Fill) .width(Fill)

View file

@ -7,7 +7,7 @@
use crate::app::Message; use crate::app::Message;
use crate::plugin::external::{ExternalPlugin, RegistryEntry}; use crate::plugin::external::{ExternalPlugin, RegistryEntry};
use iced::widget::{button, column, container, pick_list, row, scrollable, text, text_input, Space}; 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}; use rustc_hash::{FxHashMap, FxHashSet};
/// Marketplace state passed to the Plugin Manager view. /// Marketplace state passed to the Plugin Manager view.
@ -25,124 +25,105 @@ inventory::submit!(crate::command::CommandRegistration {
names: &["PLUGINS", "PLUGINMANAGER"] names: &["PLUGINS", "PLUGINMANAGER"]
}); });
const BG: Color = Color { fn muted_style(theme: &Theme) -> iced::widget::text::Style {
r: 0.15, iced::widget::text::Style {
g: 0.15, color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
b: 0.15, }
a: 1.0, }
};
const CARD: Color = Color { fn primary_style(theme: &Theme) -> iced::widget::text::Style {
r: 0.12, iced::widget::text::Style {
g: 0.12, color: Some(theme.extended_palette().primary.base.color),
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 badge<'a>(label: String) -> Element<'a, Message> { fn badge<'a>(label: String) -> Element<'a, Message> {
container(text(label).size(11).color(WHITE)) container(text(label).size(11))
.padding([2, 8]) .padding([2, 8])
.style(|_: &Theme| container::Style { .style(|theme: &Theme| {
background: Some(Background::Color(Color { let pair = theme.extended_palette().primary.weak;
r: 0.20, container::Style {
g: 0.34, background: Some(Background::Color(pair.color)),
b: 0.52, text_color: Some(pair.text),
a: 1.0,
})),
border: Border { border: Border {
radius: 4.0.into(), radius: 4.0.into(),
..Default::default() ..Default::default()
}, },
..Default::default() ..Default::default()
}
}) })
.into() .into()
} }
fn toggle_button<'a>(id: &str, disabled: bool) -> Element<'a, Message> { fn toggle_button<'a>(id: &str, disabled: bool) -> Element<'a, Message> {
// Label shows the action the click performs. // Label shows the action the click performs.
let (label, on, off) = if disabled { let label = 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 }) "Enable"
} else { } 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 want_enabled = disabled; // clicking flips the state
let id_owned = id.to_string(); let id_owned = id.to_string();
button(text(label).size(12).color(WHITE)) button(text(label).size(12))
.padding([3, 12]) .padding([3, 12])
.on_press(Message::SetPluginEnabled(id_owned, want_enabled)) .on_press(Message::SetPluginEnabled(id_owned, want_enabled))
.style(move |_: &Theme, status| { .style(if disabled { button::success } else { button::danger })
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()
}
})
.into() .into()
} }
#[derive(Clone, Copy)]
enum StatusKind {
Muted,
Success,
Danger,
Warning,
}
/// Coloured status pill for a discovered external package. /// Coloured status pill for a discovered external package.
fn status_badge<'a>(label: &str, color: Color) -> Element<'a, Message> { fn status_badge<'a>(label: &str, kind: StatusKind) -> Element<'a, Message> {
container(text(label.to_string()).size(11).color(WHITE)) container(text(label.to_string()).size(11))
.padding([2, 8]) .padding([2, 8])
.style(move |_: &Theme| container::Style { .style(move |theme: &Theme| {
background: Some(Background::Color(color)), 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 { border: Border {
radius: 4.0.into(), radius: 4.0.into(),
..Default::default() ..Default::default()
}, },
..Default::default() ..Default::default()
}
}) })
.into() .into()
} }
fn external_card<'a>(p: &ExternalPlugin, loaded: bool, disabled: bool) -> Element<'a, Message> { fn external_card<'a>(p: &ExternalPlugin, loaded: bool, disabled: bool) -> Element<'a, Message> {
let (status, color) = if loaded && disabled { let (status, kind) = if loaded && disabled {
("Disabled", Color { r: 0.45, g: 0.45, b: 0.45, a: 1.0 }) ("Disabled", StatusKind::Muted)
} else if loaded { } 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() { } 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 { } 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 { } 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![ let mut header = row![
text(p.name.clone()).size(15).color(WHITE), text(p.name.clone()).size(15),
Space::new().width(8), Space::new().width(8),
badge(format!("v{}", p.version)), badge(format!("v{}", p.version)),
Space::new().width(8), Space::new().width(8),
badge(format!("API {}", p.api_version)), badge(format!("API {}", p.api_version)),
Space::new().width(Fill), Space::new().width(Fill),
status_badge(status, color), status_badge(status, kind),
] ]
.align_y(iced::Center); .align_y(iced::Center);
// A loaded plugin can be turned off (drops its ribbon tab + dispatch). // 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( header = header.push(pill_button(
"Uninstall", "Uninstall",
Message::PluginUninstall(p.id.clone()), 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); let mut body = column![header, id_line].spacing(5);
if !p.description.is_empty() { 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() { if !p.command_prefixes.is_empty() {
body = body.push( body = body.push(
text(format!("Commands: {}", p.command_prefixes.join(", "))) text(format!("Commands: {}", p.command_prefixes.join(", ")))
.size(11) .size(11)
.color(DIM), .style(muted_style),
); );
} }
container(body.padding([12, 14])) container(body.padding([12, 14]))
.width(Fill) .width(Fill)
.style(|_: &Theme| container::Style { .style(container::bordered_box)
background: Some(Background::Color(CARD)),
border: Border { color: BORDER, width: 1.0, radius: 6.0.into() },
..Default::default()
})
.into() .into()
} }
fn pill_button<'a>(label: &str, msg: Message, bg: Color) -> Element<'a, Message> { fn pill_button<'a>(
button(text(label.to_string()).size(12).color(WHITE)) label: &str,
msg: Message,
style: fn(&Theme, button::Status) -> button::Style,
) -> Element<'a, Message> {
button(text(label.to_string()).size(12))
.padding([4, 12]) .padding([4, 12])
.on_press(msg) .on_press(msg)
.style(move |_: &Theme, status| { .style(style)
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()
}
})
.into() .into()
} }
/// Square icon variant of [`pill_button`] for glyph-free actions (e.g. remove). /// 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> { fn pill_icon_button<'a>(
button(crate::ui::icons::tinted(icon, 11.0, WHITE)) 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]) .padding([5, 9])
.on_press(msg) .on_press(msg)
.style(move |_: &Theme, status| { .style(style)
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()
}
})
.into() .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. /// Release dropdown + Install (+ optional unlink) for one repo.
fn install_controls<'a>( fn install_controls<'a>(
repo: &str, repo: &str,
@ -232,7 +190,7 @@ fn install_controls<'a>(
) -> Element<'a, Message> { ) -> Element<'a, Message> {
let repo_s = repo.to_string(); let repo_s = repo.to_string();
let picker: Element<'_, Message> = if tags.is_empty() { 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 { } else {
let r = repo_s.clone(); let r = repo_s.clone();
pick_list(tags, selected, move |tag| { pick_list(tags, selected, move |tag| {
@ -244,7 +202,11 @@ fn install_controls<'a>(
let mut controls = row![ let mut controls = row![
picker, picker,
Space::new().width(8), 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) .align_y(iced::Center)
.spacing(4); .spacing(4);
@ -253,7 +215,7 @@ fn install_controls<'a>(
controls = controls.push(pill_icon_button( controls = controls.push(pill_icon_button(
crate::ui::icons::CLOSE, crate::ui::icons::CLOSE,
Message::PluginRepoRemove(repo_s), Message::PluginRepoRemove(repo_s),
RED, button::danger,
)); ));
} }
controls.into() controls.into()
@ -262,37 +224,33 @@ fn install_controls<'a>(
fn market_card<'a>(body: iced::widget::Column<'a, Message>) -> Element<'a, Message> { fn market_card<'a>(body: iced::widget::Column<'a, Message>) -> Element<'a, Message> {
container(body.spacing(4).padding([10, 12])) container(body.spacing(4).padding([10, 12]))
.width(Fill) .width(Fill)
.style(|_: &Theme| container::Style { .style(container::bordered_box)
background: Some(Background::Color(CARD)),
border: Border { color: BORDER, width: 1.0, radius: 6.0.into() },
..Default::default()
})
.into() .into()
} }
fn marketplace_section<'a>(m: &MarketView) -> Element<'a, Message> { 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). // Curated registry entries (from the OpenCADStudio repo).
for e in m.registry { for e in m.registry {
let tags = m.release_tags.get(&e.repo).cloned().unwrap_or_default(); let tags = m.release_tags.get(&e.repo).cloned().unwrap_or_default();
let selected = m.selected_tag.get(&e.repo).cloned(); let selected = m.selected_tag.get(&e.repo).cloned();
let header = row![ let header = row![
text(e.name.clone()).size(14).color(WHITE), text(e.name.clone()).size(14),
Space::new().width(Fill), Space::new().width(Fill),
install_controls(&e.repo, tags, selected, false), install_controls(&e.repo, tags, selected, false),
] ]
.align_y(iced::Center); .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() { 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)); col = col.push(market_card(body));
} }
// Manual: link any repo by owner/repo. // Manual: link any repo by owner/repo.
col = col.push(Space::new().height(6)); 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( col = col.push(
row![ row![
text_input("owner/repo", m.input) text_input("owner/repo", m.input)
@ -301,7 +259,7 @@ fn marketplace_section<'a>(m: &MarketView) -> Element<'a, Message> {
.size(13) .size(13)
.width(Fill), .width(Fill),
Space::new().width(8), 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), .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 tags = m.release_tags.get(repo).cloned().unwrap_or_default();
let selected = m.selected_tag.get(repo).cloned(); let selected = m.selected_tag.get(repo).cloned();
let header = row![ let header = row![
text(repo.clone()).size(13).color(WHITE), text(repo.clone()).size(13),
Space::new().width(Fill), Space::new().width(Fill),
install_controls(repo, tags, selected, true), install_controls(repo, tags, selected, true),
] ]
@ -318,7 +276,7 @@ fn marketplace_section<'a>(m: &MarketView) -> Element<'a, Message> {
} }
if !m.status.is_empty() { 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() col.into()
} }
@ -329,17 +287,17 @@ pub fn view_window<'a>(
loaded: &FxHashSet<String>, loaded: &FxHashSet<String>,
market: MarketView, market: MarketView,
) -> Element<'a, Message> { ) -> 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.") let subtitle = text("Add-ons load from the plugins folder. Install from a repository below.")
.size(12) .size(12)
.color(DIM); .style(muted_style);
let mut list = column![].spacing(10); let mut list = column![].spacing(10);
// Installed external packages (from the plugins folder). // Installed external packages (from the plugins folder).
if externals.is_empty() { 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 { } else {
list = list.push(text("Installed").size(13).color(ACCENT)); list = list.push(text("Installed").size(13).style(primary_style));
for p in externals { for p in externals {
list = list.push(external_card( list = list.push(external_card(
p, p,
@ -360,8 +318,10 @@ pub fn view_window<'a>(
.width(Fill) .width(Fill)
.height(Fill), .height(Fill),
) )
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(BG)), background: Some(Background::Color(
theme.extended_palette().background.base.color,
)),
..Default::default() ..Default::default()
}) })
.width(Fill) .width(Fill)

View file

@ -2,7 +2,7 @@
use crate::app::Message; use crate::app::Message;
use iced::widget::{column, container, row, scrollable, text, Space}; 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; use std::borrow::Cow;
/// Display name of the primary accelerator modifier on this platform. /// Display name of the primary accelerator modifier on this platform.
@ -14,49 +14,26 @@ const MOD: &str = "Cmd";
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
const MOD: &str = "Ctrl"; const MOD: &str = "Ctrl";
const TB: Color = Color { fn muted_style(theme: &Theme) -> iced::widget::text::Style {
r: 0.13, iced::widget::text::Style {
g: 0.13, color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
b: 0.13, }
a: 1.0, }
};
const BG: Color = Color { fn primary_style(theme: &Theme) -> iced::widget::text::Style {
r: 0.15, iced::widget::text::Style {
g: 0.15, color: Some(theme.extended_palette().primary.base.color),
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 hdivider<'a>() -> Element<'a, Message> { fn hdivider<'a>() -> Element<'a, Message> {
container(Space::new().width(Fill).height(1)) container(Space::new().width(Fill).height(1))
.width(Fill) .width(Fill)
.height(1) .height(1)
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(BORDER)), background: Some(Background::Color(
theme.extended_palette().background.neutral.color,
)),
..Default::default() ..Default::default()
}) })
.into() .into()
@ -69,7 +46,7 @@ fn shortcut_row<'a>(
row![ row![
text(key.into()) text(key.into())
.size(11) .size(11)
.color(KEY) .style(primary_style)
.font(iced::Font::MONOSPACE) .font(iced::Font::MONOSPACE)
.width(160), .width(160),
text(action).size(11), text(action).size(11),
@ -81,7 +58,7 @@ fn shortcut_row<'a>(
} }
fn section<'a>(title: impl Into<Cow<'static, str>>) -> Element<'a, Message> { fn section<'a>(title: impl Into<Cow<'static, str>>) -> Element<'a, Message> {
container(text(title.into()).size(11).color(DIM)) container(text(title.into()).size(11).style(muted_style))
.padding(iced::Padding { .padding(iced::Padding {
top: 6.0, top: 6.0,
right: 0.0, right: 0.0,
@ -99,12 +76,14 @@ pub fn view_window<'a>(
row![ row![
text("Type SHORTCUTS SET <key> <cmd> to add custom shortcuts.") text("Type SHORTCUTS SET <key> <cmd> to add custom shortcuts.")
.size(10) .size(10)
.color(DIM), .style(muted_style),
] ]
.align_y(iced::Center), .align_y(iced::Center),
) )
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(TB)), background: Some(Background::Color(
theme.extended_palette().background.weakest.color,
)),
..Default::default() ..Default::default()
}) })
.width(Fill) .width(Fill)
@ -147,7 +126,7 @@ pub fn view_window<'a>(
rows.push( rows.push(
text(" (none — use: SHORTCUTS SET <key> <command>)") text(" (none — use: SHORTCUTS SET <key> <command>)")
.size(11) .size(11)
.color(DIM) .style(muted_style)
.into(), .into(),
); );
} else { } else {
@ -158,7 +137,7 @@ pub fn view_window<'a>(
row![ row![
text(key.as_str()) text(key.as_str())
.size(11) .size(11)
.color(KEY) .style(primary_style)
.font(iced::Font::MONOSPACE) .font(iced::Font::MONOSPACE)
.width(160), .width(160),
text(cmd.as_str()).size(11), text(cmd.as_str()).size(11),
@ -179,26 +158,25 @@ pub fn view_window<'a>(
// ── Header row with accent ──────────────────────────────────────────── // ── Header row with accent ────────────────────────────────────────────
let header = container( let header = container(
row![ row![
text("Key").size(10).color(ACCENT).width(160), text("Key").size(10).style(primary_style).width(160),
text("Action").size(10).color(ACCENT), text("Action").size(10).style(primary_style),
] ]
.spacing(8) .spacing(8)
.padding([4, 16]), .padding([4, 16]),
) )
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(Color { background: Some(Background::Color(
r: 0.13, theme.extended_palette().primary.weak.color,
g: 0.13, )),
b: 0.18,
a: 1.0,
})),
..Default::default() ..Default::default()
}) })
.width(Fill); .width(Fill);
container(column![toolbar, hdivider(), header, hdivider(), content].spacing(0)) container(column![toolbar, hdivider(), header, hdivider(), content].spacing(0))
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(BG)), background: Some(Background::Color(
theme.extended_palette().background.base.color,
)),
..Default::default() ..Default::default()
}) })
.width(Fill) .width(Fill)

View file

@ -1,60 +1,33 @@
use crate::app::Message; use crate::app::Message;
use iced::widget::{button, column, container, row, scrollable, text, Space}; 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 { fn muted_style(theme: &Theme) -> iced::widget::text::Style {
r: 0.15, iced::widget::text::Style {
g: 0.15, color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
b: 0.15, }
a: 1.0, }
};
const PANEL: Color = Color { fn primary_style(theme: &Theme) -> iced::widget::text::Style {
r: 0.12, iced::widget::text::Style {
g: 0.12, color: Some(theme.extended_palette().primary.base.color),
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,
};
/// Renders one of the two "Installed" / "Latest" cards. The `highlight` /// Renders one of the two "Installed" / "Latest" cards. The `highlight`
/// flag tints the border + label with the accent colour, making the new /// flag tints the border + label with the accent colour, making the new
/// version the visual anchor of the row. /// version the visual anchor of the row.
fn version_card<'a>(label: &'static str, value: String, highlight: bool) -> Element<'a, Message> { 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( container(
column![ column![
text(label).size(10).color(label_color), text(label)
text(value).size(20).color(value_color), .size(10)
.style(move |theme: &Theme| if highlight {
primary_style(theme)
} else {
muted_style(theme)
}),
text(value).size(20),
] ]
.spacing(4) .spacing(4)
.align_x(iced::Center), .align_x(iced::Center),
@ -67,14 +40,26 @@ fn version_card<'a>(label: &'static str, value: String, highlight: bool) -> Elem
left: 12.0, left: 12.0,
}) })
.align_x(iced::Center) .align_x(iced::Center)
.style(move |_: &Theme| container::Style { .style(move |theme: &Theme| {
background: Some(Background::Color(bg)), 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 { border: Border {
color: border_color, color: if highlight {
palette.primary.base.color
} else {
palette.background.neutral.color
},
width: 1.0, width: 1.0,
radius: 6.0.into(), radius: 6.0.into(),
}, },
..Default::default() ..Default::default()
}
}) })
.into() .into()
} }
@ -98,24 +83,23 @@ fn render_notes_line<'a>(raw: &str) -> Element<'a, Message> {
if let Some(rest) = trimmed.strip_prefix("## ") { if let Some(rest) = trimmed.strip_prefix("## ") {
return text(strip_inline_md(rest)) return text(strip_inline_md(rest))
.size(13) .size(13)
.color(ACCENT) .style(primary_style)
.into(); .into();
} }
if let Some(rest) = trimmed.strip_prefix("### ") { if let Some(rest) = trimmed.strip_prefix("### ") {
return text(strip_inline_md(rest)) return text(strip_inline_md(rest))
.size(12) .size(12)
.color(WHITE)
.into(); .into();
} }
if let Some(rest) = trimmed.strip_prefix("- ").or_else(|| trimmed.strip_prefix("* ")) { if let Some(rest) = trimmed.strip_prefix("- ").or_else(|| trimmed.strip_prefix("* ")) {
return row![ return row![
container(crate::ui::icons::tinted(crate::ui::icons::DOT, 5.0, DIM)).width(14), container(crate::ui::icons::themed_secondary(crate::ui::icons::DOT, 5.0)).width(14),
text(strip_inline_md(rest)).size(11).color(WHITE), text(strip_inline_md(rest)).size(11),
] ]
.spacing(4) .spacing(4)
.into(); .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 /// 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> { pub fn view_window<'a>(latest: &'a str, body: &'a str) -> Element<'a, Message> {
let header = container( let header = container(
column![ 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.") text("A newer Open CAD Studio version is published on GitHub.")
.size(11) .size(11)
.color(DIM), .style(muted_style),
] ]
.spacing(4) .spacing(4)
.align_x(iced::Center), .align_x(iced::Center),
@ -165,10 +149,9 @@ pub fn view_window<'a>(latest: &'a str, body: &'a str) -> Element<'a, Message> {
false, false,
); );
let latest_card = version_card("Latest", format!("v{}", latest), true); 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, crate::ui::icons::ARROW_LONG_RIGHT,
20.0, 20.0,
DIM,
)) ))
.width(iced::Length::Fixed(32.0)) .width(iced::Length::Fixed(32.0))
.height(Fill) .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)) let later_btn = button(text("Later").size(11))
.on_press(Message::UpdateNoticeClose) .on_press(Message::UpdateNoticeClose)
.style(|_: &Theme, st| button::Style { .style(button::secondary)
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()
})
.padding([6, 16]); .padding([6, 16]);
let open_btn = button(text("Open Release Page").size(11)) let open_btn = button(text("Open Release Page").size(11))
.on_press(Message::UpdateNoticeOpenRelease) .on_press(Message::UpdateNoticeOpenRelease)
.style(|_: &Theme, st| button::Style { .style(button::primary)
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()
})
.padding([6, 16]); .padding([6, 16]);
let footer = row![Space::new().width(Fill), later_btn, open_btn] 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 // Release notes panel. Rendered as a light-markdown column inside a
// bordered scrollable so long bodies stay contained and don't // bordered scrollable so long bodies stay contained and don't
// explode the window. Empty body → "No release notes provided." // 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 { .padding(iced::Padding {
top: 10.0, top: 10.0,
right: 0.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() { let notes_body: Element<'a, Message> = if body.trim().is_empty() {
text("No release notes provided.") text("No release notes provided.")
.size(11) .size(11)
.color(DIM) .style(muted_style)
.into() .into()
} else { } else {
let mut col = column![].spacing(4); 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) let notes_block = container(notes_body)
.width(Fill) .width(Fill)
.height(Fill) .height(Fill)
.style(|_: &Theme| container::Style { .style(container::bordered_box);
background: Some(Background::Color(PANEL)),
border: Border {
color: BORDER,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
});
// Wrap notes_block in a Fill-height container outside the column so it // Wrap notes_block in a Fill-height container outside the column so it
// greedily claims every pixel left over after the fixed-height rows // 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, left: 20.0,
}), }),
) )
.style(|_: &Theme| container::Style { .style(|theme: &Theme| container::Style {
background: Some(Background::Color(BG)), background: Some(Background::Color(
theme.extended_palette().background.base.color,
)),
..Default::default() ..Default::default()
}) })
.width(Fill) .width(Fill)

View file

@ -25,7 +25,7 @@ use iced::advanced::layout::{self, Layout};
use iced::advanced::widget::{self, tree, Widget}; use iced::advanced::widget::{self, tree, Widget};
use iced::advanced::{mouse, overlay, renderer, Clipboard, Renderer as _, Shell}; use iced::advanced::{mouse, overlay, renderer, Clipboard, Renderer as _, Shell};
use iced::{ 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, Theme, Vector,
}; };
@ -1360,12 +1360,7 @@ impl<'a> Widget<Message, Theme, Renderer> for ReorderTab<'a> {
shadow: Shadow::default(), shadow: Shadow::default(),
snap: true, snap: true,
}, },
Background::Color(Color { Background::Color(theme.extended_palette().primary.base.color),
r: 0.20,
g: 0.55,
b: 0.90,
a: 1.0,
}),
); );
} }
} }