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 {
/// Input modes, backup, plugin lists, viewport background colours, …
pub settings: UserSettings,
/// Iced theme selection and the six base colours used by a custom theme.
pub theme: UiThemeConfig,
/// Recent-files list + retained count.
pub recent: RecentConfig,
/// Last selected section on the tabbed Start page.
@ -35,6 +37,7 @@ impl Default for AppConfig {
fn default() -> Self {
Self {
settings: UserSettings::default(),
theme: UiThemeConfig::default(),
recent: RecentConfig::default(),
start: StartConfig::default(),
statusbar: StatusBarConfig::default(),
@ -44,6 +47,136 @@ impl Default for AppConfig {
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct UiThemeConfig {
pub name: String,
pub palette: UiThemePalette,
}
impl Default for UiThemeConfig {
fn default() -> Self {
let theme = iced::Theme::Dark;
Self {
name: theme.to_string(),
palette: UiThemePalette::from_iced(theme.palette()),
}
}
}
impl UiThemeConfig {
pub fn to_iced(&self) -> iced::Theme {
if self.name == "Custom" {
iced::Theme::custom("Custom", self.palette.to_iced())
} else {
builtin_theme(&self.name).unwrap_or(iced::Theme::Dark)
}
}
}
#[derive(Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct UiThemePalette {
pub background: [u8; 3],
pub text: [u8; 3],
pub primary: [u8; 3],
pub success: [u8; 3],
pub warning: [u8; 3],
pub danger: [u8; 3],
}
impl Default for UiThemePalette {
fn default() -> Self {
Self::from_iced(iced::Theme::Dark.palette())
}
}
impl UiThemePalette {
pub fn from_iced(palette: iced::theme::Palette) -> Self {
Self {
background: color_to_rgb(palette.background),
text: color_to_rgb(palette.text),
primary: color_to_rgb(palette.primary),
success: color_to_rgb(palette.success),
warning: color_to_rgb(palette.warning),
danger: color_to_rgb(palette.danger),
}
}
pub fn to_iced(self) -> iced::theme::Palette {
iced::theme::Palette {
background: rgb_to_color(self.background),
text: rgb_to_color(self.text),
primary: rgb_to_color(self.primary),
success: rgb_to_color(self.success),
warning: rgb_to_color(self.warning),
danger: rgb_to_color(self.danger),
}
}
pub fn hex_values(self) -> [String; 6] {
[
rgb_to_hex(self.background),
rgb_to_hex(self.text),
rgb_to_hex(self.primary),
rgb_to_hex(self.success),
rgb_to_hex(self.warning),
rgb_to_hex(self.danger),
]
}
pub fn set_hex(&mut self, index: usize, value: &str) -> bool {
let Some(rgb) = parse_hex(value) else {
return false;
};
match index {
0 => self.background = rgb,
1 => self.text = rgb,
2 => self.primary = rgb,
3 => self.success = rgb,
4 => self.warning = rgb,
5 => self.danger = rgb,
_ => return false,
}
true
}
}
pub fn builtin_theme(name: &str) -> Option<iced::Theme> {
iced::Theme::ALL
.iter()
.find(|theme| theme.to_string() == name)
.cloned()
}
fn color_to_rgb(color: iced::Color) -> [u8; 3] {
[
(color.r * 255.0).round() as u8,
(color.g * 255.0).round() as u8,
(color.b * 255.0).round() as u8,
]
}
fn rgb_to_color(rgb: [u8; 3]) -> iced::Color {
iced::Color::from_rgb8(rgb[0], rgb[1], rgb[2])
}
fn rgb_to_hex(rgb: [u8; 3]) -> String {
format!("#{:02X}{:02X}{:02X}", rgb[0], rgb[1], rgb[2])
}
fn parse_hex(value: &str) -> Option<[u8; 3]> {
let value = value.trim().strip_prefix('#').unwrap_or(value.trim());
if value.len() != 6 {
return None;
}
Some([
u8::from_str_radix(&value[0..2], 16).ok()?,
u8::from_str_radix(&value[2..4], 16).ok()?,
u8::from_str_radix(&value[4..6], 16).ok()?,
])
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct RecentConfig {

View file

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

View file

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

View file

@ -3551,7 +3551,12 @@ impl OpenCADStudio {
}
Message::SetTheme(theme) => {
self.ui_theme.name = theme.to_string();
self.ui_theme.palette =
crate::app::config::UiThemePalette::from_iced(theme.palette());
self.theme_color_inputs = self.ui_theme.palette.hex_values();
self.active_theme = theme;
self.persist_settings_if_changed();
Task::none()
}
@ -3620,6 +3625,37 @@ impl OpenCADStudio {
Message::DefaultSaveFormatChanged(format) => {
self.default_save_format =
crate::io::canonical_save_format(&format).to_string();
self.persist_settings_if_changed();
Task::none()
}
Message::OptionsThemeChanged(name) => {
self.ui_theme.name = name;
if let Some(theme) =
crate::app::config::builtin_theme(&self.ui_theme.name)
{
self.ui_theme.palette =
crate::app::config::UiThemePalette::from_iced(theme.palette());
self.theme_color_inputs = self.ui_theme.palette.hex_values();
self.active_theme = theme;
} else {
self.ui_theme.name = "Custom".to_string();
self.active_theme = self.ui_theme.to_iced();
}
self.persist_settings_if_changed();
Task::none()
}
Message::OptionsThemeColorChanged(index, value) => {
if index >= self.theme_color_inputs.len() {
return Task::none();
}
self.theme_color_inputs[index] = value.clone();
if self.ui_theme.palette.set_hex(index, &value) {
self.ui_theme.name = "Custom".to_string();
self.active_theme = self.ui_theme.to_iced();
self.persist_settings_if_changed();
}
Task::none()
}

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
use super::super::{Message, OpenCADStudio};
use iced::widget::{button, column, container, pick_list, row, text, Space};
use iced::{Background, Border, Color, Element, Fill, Theme};
use iced::{Background, Element, Fill, Theme};
impl OpenCADStudio {
/// Title shown in the active modal's title bar, left of the move/close
@ -63,9 +63,13 @@ impl OpenCADStudio {
sized(crate::ui::window::alias_editor::view_window(&self.alias_editor_rows), 480, 520)
}
super::super::ModalKind::Options => sized(
crate::ui::window::options::view_window(&self.default_save_format),
480,
190,
crate::ui::window::options::view_window(
&self.default_save_format,
&self.ui_theme,
&self.theme_color_inputs,
),
520,
500,
),
super::super::ModalKind::PluginManager => sized(
crate::ui::window::plugin_manager::view_window(
@ -712,125 +716,56 @@ impl OpenCADStudio {
}
}
fn dialog_button(
label: &'static str,
message: Message,
style: fn(&Theme, button::Status) -> button::Style,
) -> Element<'static, Message> {
button(text(label).size(13))
.on_press(message)
.style(style)
.padding([6, 18])
.into()
}
fn dialog_body_style(theme: &Theme) -> container::Style {
let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.base.color)),
text_color: Some(palette.background.base.text),
..Default::default()
}
}
fn dialog_muted_text_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
}
}
/// Compact Save-As options dialog: pick the format/version and a default file
/// name. The destination folder and overwrite confirmation come from the
/// native OS save dialog (native) or the browser download (web) that follows.
fn save_as_dialog_window<'a>(filename: &'a str, format: &'a str) -> Element<'a, Message> {
const BG: Color = Color {
r: 0.15,
g: 0.15,
b: 0.17,
a: 1.0,
};
const BORDER: Color = Color {
r: 0.32,
g: 0.32,
b: 0.36,
a: 1.0,
};
const TEXT: Color = Color {
r: 0.90,
g: 0.90,
b: 0.90,
a: 1.0,
};
const DIM: Color = Color {
r: 0.58,
g: 0.58,
b: 0.62,
a: 1.0,
};
const BTN_OK: Color = Color {
r: 0.20,
g: 0.46,
b: 0.80,
a: 1.0,
};
const BTN_HOV: Color = Color {
r: 0.26,
g: 0.55,
b: 0.92,
a: 1.0,
};
const BTN_GREY: Color = Color {
r: 0.26,
g: 0.26,
b: 0.29,
a: 1.0,
};
const BTN_GHOV: Color = Color {
r: 0.34,
g: 0.34,
b: 0.38,
a: 1.0,
};
let btn = |lbl: &'static str, msg: Message, base: Color, hov: Color| {
button(text(lbl).size(12).color(TEXT))
.on_press(msg)
.style(move |_: &Theme, st| button::Style {
background: Some(Background::Color(
if matches!(st, button::Status::Hovered | button::Status::Pressed) {
hov
} else {
base
},
)),
text_color: TEXT,
border: Border {
color: BORDER,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
})
.padding([4, 12])
};
let sel_fmt = crate::io::SAVE_FORMAT_OPTIONS
.iter()
.copied()
.find(|&s| s == format);
let label = |s: &'static str| text(s).size(11).color(DIM);
let label = |s: &'static str| text(s).size(11).style(dialog_muted_text_style);
let mut items: Vec<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());
// Web has no native file dialog, so the file name is typed here. On native
// the OS save dialog collects the name, so this field is omitted.
#[cfg(target_arch = "wasm32")]
{
const INPUT_BG: Color = Color {
r: 0.10,
g: 0.10,
b: 0.12,
a: 1.0,
};
let input_sty =
|_: &Theme, _: iced::widget::text_input::Status| iced::widget::text_input::Style {
background: Background::Color(INPUT_BG),
border: Border {
color: BORDER,
width: 1.0,
radius: 4.0.into(),
},
icon: TEXT,
placeholder: DIM,
value: TEXT,
selection: Color {
r: 0.20,
g: 0.46,
b: 0.80,
a: 0.45,
},
};
items.push(
row![
label("File name:").width(70),
iced::widget::text_input("drawing.dwg", filename)
.on_input(Message::SaveDialogFilenameChanged)
.style(input_sty)
.size(13)
.padding([5, 8])
.width(Fill),
@ -860,9 +795,9 @@ fn save_as_dialog_window<'a>(filename: &'a str, format: &'a str) -> Element<'a,
items.push(
row![
Space::new().width(Fill),
btn("Save as...", Message::SaveDialogConfirm, BTN_OK, BTN_HOV),
dialog_button("Save as...", Message::SaveDialogConfirm, button::primary),
Space::new().width(8),
btn("Cancel", Message::SaveDialogCancel, BTN_GREY, BTN_GHOV),
dialog_button("Cancel", Message::SaveDialogCancel, button::secondary),
]
.into(),
);
@ -870,10 +805,7 @@ fn save_as_dialog_window<'a>(filename: &'a str, format: &'a str) -> Element<'a,
let body = column(items).spacing(0);
container(body)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(BG)),
..Default::default()
})
.style(dialog_body_style)
.padding([14, 16])
.width(Fill)
.height(Fill)
@ -881,89 +813,23 @@ fn save_as_dialog_window<'a>(filename: &'a str, format: &'a str) -> Element<'a,
}
fn unsaved_changes_dialog_window(name: &str) -> Element<'static, Message> {
const BG: Color = Color {
r: 0.18,
g: 0.18,
b: 0.20,
a: 1.0,
};
const BORDER_COL: Color = Color {
r: 0.38,
g: 0.38,
b: 0.42,
a: 1.0,
};
const TEXT_COL: Color = Color {
r: 0.90,
g: 0.90,
b: 0.90,
a: 1.0,
};
const BTN_SAVE: Color = Color {
r: 0.20,
g: 0.46,
b: 0.80,
a: 1.0,
};
const BTN_HOVER: Color = Color {
r: 0.26,
g: 0.55,
b: 0.92,
a: 1.0,
};
const BTN_DISC: Color = Color {
r: 0.28,
g: 0.28,
b: 0.30,
a: 1.0,
};
const BTN_DHOV: Color = Color {
r: 0.36,
g: 0.36,
b: 0.40,
a: 1.0,
};
let body_text = format!("Do you want to save changes to \"{}\"?", name);
let btn = |label: &'static str, msg: Message, base: Color, hov: Color| {
button(text(label).size(13).color(TEXT_COL))
.on_press(msg)
.style(move |_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => hov,
_ => base,
})),
text_color: TEXT_COL,
border: Border {
color: BORDER_COL,
width: 1.0,
radius: 4.0.into(),
},
shadow: iced::Shadow::default(),
snap: false,
})
.padding([6, 18])
};
container(
column![
text(body_text).size(13).color(TEXT_COL),
text(body_text).size(13),
iced::widget::Space::new().height(20),
row![
btn("Save", Message::UnsavedDialogSave, BTN_SAVE, BTN_HOVER),
dialog_button("Save", Message::UnsavedDialogSave, button::primary),
iced::widget::Space::new().width(8),
btn("Discard", Message::UnsavedDialogDiscard, BTN_DISC, BTN_DHOV),
dialog_button("Discard", Message::UnsavedDialogDiscard, button::danger),
iced::widget::Space::new().width(8),
btn("Cancel", Message::UnsavedDialogCancel, BTN_DISC, BTN_DHOV),
dialog_button("Cancel", Message::UnsavedDialogCancel, button::secondary),
],
]
.spacing(0),
)
.style(move |_: &Theme| container::Style {
background: Some(Background::Color(BG)),
..Default::default()
})
.style(dialog_body_style)
.center(Fill)
.padding([24, 28])
.into()
@ -971,55 +837,6 @@ fn unsaved_changes_dialog_window(name: &str) -> Element<'static, Message> {
#[cfg(not(target_arch = "wasm32"))]
fn file_in_use_dialog_window(path: &str, error: &str) -> Element<'static, Message> {
const BG: Color = Color {
r: 0.18,
g: 0.18,
b: 0.20,
a: 1.0,
};
const BORDER_COL: Color = Color {
r: 0.38,
g: 0.38,
b: 0.42,
a: 1.0,
};
const TEXT_COL: Color = Color {
r: 0.90,
g: 0.90,
b: 0.90,
a: 1.0,
};
const DIM_COL: Color = Color {
r: 0.62,
g: 0.62,
b: 0.66,
a: 1.0,
};
const BTN_PRIMARY: Color = Color {
r: 0.20,
g: 0.46,
b: 0.80,
a: 1.0,
};
const BTN_PRIMARY_HOVER: Color = Color {
r: 0.26,
g: 0.55,
b: 0.92,
a: 1.0,
};
const BTN_SECONDARY: Color = Color {
r: 0.28,
g: 0.28,
b: 0.30,
a: 1.0,
};
const BTN_SECONDARY_HOVER: Color = Color {
r: 0.36,
g: 0.36,
b: 0.40,
a: 1.0,
};
let file_name = std::path::Path::new(path)
.file_name()
.map(|name| name.to_string_lossy().into_owned())
@ -1027,70 +844,45 @@ fn file_in_use_dialog_window(path: &str, error: &str) -> Element<'static, Messag
let heading = format!("\"{file_name}\" could not be saved.");
let path_line = format!("Path: {path}");
let details = format!("Details: {error}");
let btn = |label: &'static str, msg: Message, base: Color, hover: Color| {
button(text(label).size(13).color(TEXT_COL))
.on_press(msg)
.style(move |_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => hover,
_ => base,
})),
text_color: TEXT_COL,
border: Border {
color: BORDER_COL,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
})
.padding([6, 18])
};
container(
column![
text(heading).size(14).color(TEXT_COL),
text(heading).size(14),
Space::new().height(8),
text(
"The file is open or being used by another application. \
Close it there and retry, or save this drawing under a different name."
)
.size(13)
.color(TEXT_COL)
.width(Fill),
Space::new().height(12),
text(path_line).size(11).color(DIM_COL).width(Fill),
text(path_line).size(11).style(dialog_muted_text_style).width(Fill),
Space::new().height(4),
text(details).size(11).color(DIM_COL).width(Fill),
text(details).size(11).style(dialog_muted_text_style).width(Fill),
Space::new().height(18),
row![
btn(
dialog_button(
"Retry",
Message::SaveFileInUseRetry,
BTN_PRIMARY,
BTN_PRIMARY_HOVER
button::primary
),
Space::new().width(8),
btn(
dialog_button(
"Save As",
Message::SaveFileInUseSaveAs,
BTN_SECONDARY,
BTN_SECONDARY_HOVER
button::secondary
),
Space::new().width(8),
btn(
dialog_button(
"Cancel",
Message::SaveFileInUseCancel,
BTN_SECONDARY,
BTN_SECONDARY_HOVER
button::secondary
),
],
]
.spacing(0),
)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(BG)),
..Default::default()
})
.style(dialog_body_style)
.padding([18, 20])
.width(Fill)
.height(Fill)
@ -1099,142 +891,55 @@ fn file_in_use_dialog_window(path: &str, error: &str) -> Element<'static, Messag
#[cfg(not(target_arch = "wasm32"))]
fn external_change_dialog_window(path: &str) -> Element<'static, Message> {
const BG: Color = Color {
r: 0.18,
g: 0.18,
b: 0.20,
a: 1.0,
};
const BORDER_COL: Color = Color {
r: 0.38,
g: 0.38,
b: 0.42,
a: 1.0,
};
const TEXT_COL: Color = Color {
r: 0.90,
g: 0.90,
b: 0.90,
a: 1.0,
};
const DIM_COL: Color = Color {
r: 0.62,
g: 0.62,
b: 0.66,
a: 1.0,
};
const BTN_PRIMARY: Color = Color {
r: 0.20,
g: 0.46,
b: 0.80,
a: 1.0,
};
const BTN_PRIMARY_HOVER: Color = Color {
r: 0.26,
g: 0.55,
b: 0.92,
a: 1.0,
};
const BTN_SECONDARY: Color = Color {
r: 0.28,
g: 0.28,
b: 0.30,
a: 1.0,
};
const BTN_SECONDARY_HOVER: Color = Color {
r: 0.36,
g: 0.36,
b: 0.40,
a: 1.0,
};
const BTN_DANGER: Color = Color {
r: 0.62,
g: 0.31,
b: 0.14,
a: 1.0,
};
const BTN_DANGER_HOVER: Color = Color {
r: 0.78,
g: 0.39,
b: 0.17,
a: 1.0,
};
let file_name = std::path::Path::new(path)
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| "Drawing".to_string());
let heading = format!("\"{file_name}\" was changed by another application.");
let path_line = format!("Path: {path}");
let btn = |label: &'static str, msg: Message, base: Color, hover: Color| {
button(text(label).size(13).color(TEXT_COL))
.on_press(msg)
.style(move |_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => hover,
_ => base,
})),
text_color: TEXT_COL,
border: Border {
color: BORDER_COL,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
})
.padding([6, 14])
};
container(
column![
text(heading).size(14).color(TEXT_COL),
text(heading).size(14),
Space::new().height(8),
text(
"Saving now could destroy those external changes. Reload the disk copy, \
save your local work elsewhere, or explicitly overwrite it."
)
.size(13)
.color(TEXT_COL)
.width(Fill),
Space::new().height(12),
text(path_line).size(11).color(DIM_COL).width(Fill),
text(path_line).size(11).style(dialog_muted_text_style).width(Fill),
Space::new().height(18),
row![
btn(
dialog_button(
"Reload from Disk",
Message::ExternalChangeReload,
BTN_PRIMARY,
BTN_PRIMARY_HOVER
button::primary
),
Space::new().width(8),
btn(
dialog_button(
"Save As",
Message::ExternalChangeSaveAs,
BTN_SECONDARY,
BTN_SECONDARY_HOVER
button::secondary
),
Space::new().width(8),
btn(
dialog_button(
"Overwrite",
Message::ExternalChangeOverwrite,
BTN_DANGER,
BTN_DANGER_HOVER
button::danger
),
Space::new().width(8),
btn(
dialog_button(
"Cancel",
Message::ExternalChangeCancel,
BTN_SECONDARY,
BTN_SECONDARY_HOVER
button::secondary
),
],
]
.spacing(0),
)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(BG)),
..Default::default()
})
.style(dialog_body_style)
.padding([18, 20])
.width(Fill)
.height(Fill)
@ -1246,58 +951,31 @@ fn external_change_dialog_window(path: &str) -> Element<'static, Message> {
/// bytes, so saving to a different version or to DXF would drop them. Offers to
/// save in the source version (keep them) or proceed (drop them).
fn aec_drop_dialog_window(count: usize, target: &str, src_version: &str) -> Element<'static, Message> {
const BG: Color = Color { r: 0.18, g: 0.18, b: 0.20, a: 1.0 };
const BORDER_COL: Color = Color { r: 0.38, g: 0.38, b: 0.42, a: 1.0 };
const TEXT_COL: Color = Color { r: 0.90, g: 0.90, b: 0.90, a: 1.0 };
const BTN_SAVE: Color = Color { r: 0.20, g: 0.46, b: 0.80, a: 1.0 };
const BTN_HOVER: Color = Color { r: 0.26, g: 0.55, b: 0.92, a: 1.0 };
const BTN_DISC: Color = Color { r: 0.28, g: 0.28, b: 0.30, a: 1.0 };
const BTN_DHOV: Color = Color { r: 0.36, g: 0.36, b: 0.40, a: 1.0 };
let body_text = format!(
"This drawing contains {count} AEC/Civil objects that \"{target}\" \
cannot store, so they will not be saved.\n\n\
To keep them, save in the source version ({src_version})."
);
let btn = |label: &'static str, msg: Message, base: Color, hov: Color| {
button(text(label).size(13).color(TEXT_COL))
.on_press(msg)
.style(move |_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => hov,
_ => base,
})),
text_color: TEXT_COL,
border: Border {
color: BORDER_COL,
width: 1.0,
radius: 4.0.into(),
},
shadow: iced::Shadow::default(),
snap: false,
})
.padding([6, 14])
};
container(
column![
text(body_text).size(13).color(TEXT_COL),
text(body_text).size(13),
iced::widget::Space::new().height(20),
row![
btn("Save in source version", Message::AecDropSameVersion, BTN_SAVE, BTN_HOVER),
dialog_button(
"Save in source version",
Message::AecDropSameVersion,
button::primary
),
iced::widget::Space::new().width(8),
btn("Save anyway", Message::AecDropProceed, BTN_DISC, BTN_DHOV),
dialog_button("Save anyway", Message::AecDropProceed, button::warning),
iced::widget::Space::new().width(8),
btn("Back", Message::AecDropBack, BTN_DISC, BTN_DHOV),
dialog_button("Back", Message::AecDropBack, button::secondary),
],
]
.spacing(0),
)
.style(move |_: &Theme| container::Style {
background: Some(Background::Color(BG)),
..Default::default()
})
.style(dialog_body_style)
.center(Fill)
.padding([24, 28])
.into()
@ -1308,14 +986,6 @@ fn aec_drop_dialog_window(count: usize, target: &str, src_version: &str) -> Elem
/// Confirm deleting layer(s) that still have objects on them. "Delete Objects"
/// erases them and removes the layers; "Cancel" leaves everything.
fn layer_delete_warning_window(names: &[String], count: usize) -> Element<'static, Message> {
const BG: Color = Color { r: 0.18, g: 0.18, b: 0.20, a: 1.0 };
const BORDER_COL: Color = Color { r: 0.38, g: 0.38, b: 0.42, a: 1.0 };
const TEXT_COL: Color = Color { r: 0.90, g: 0.90, b: 0.90, a: 1.0 };
const BTN_DEL: Color = Color { r: 0.72, g: 0.26, b: 0.24, a: 1.0 };
const BTN_DEL_HOV: Color = Color { r: 0.84, g: 0.32, b: 0.30, a: 1.0 };
const BTN_CANCEL: Color = Color { r: 0.28, g: 0.28, b: 0.30, a: 1.0 };
const BTN_CANCEL_HOV: Color = Color { r: 0.36, g: 0.36, b: 0.40, a: 1.0 };
let obj = if count == 1 { "object" } else { "objects" };
let subject = if names.len() == 1 {
format!("Layer \"{}\"", names[0])
@ -1328,38 +998,23 @@ fn layer_delete_warning_window(names: &[String], count: usize) -> Element<'stati
if count == 1 { "that object" } else { "those objects" }
);
let btn = |label: &'static str, msg: Message, base: Color, hov: Color| {
button(text(label).size(13).color(TEXT_COL))
.on_press(msg)
.style(move |_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => hov,
_ => base,
})),
text_color: TEXT_COL,
border: Border { color: BORDER_COL, width: 1.0, radius: 4.0.into() },
shadow: iced::Shadow::default(),
snap: false,
})
.padding([6, 18])
};
container(
column![
text(body_text).size(13).color(TEXT_COL),
text(body_text).size(13),
iced::widget::Space::new().height(20),
row![
btn("Delete Objects", Message::LayerDeleteConfirm, BTN_DEL, BTN_DEL_HOV),
dialog_button(
"Delete Objects",
Message::LayerDeleteConfirm,
button::danger
),
iced::widget::Space::new().width(8),
btn("Cancel", Message::CloseModal, BTN_CANCEL, BTN_CANCEL_HOV),
dialog_button("Cancel", Message::CloseModal, button::secondary),
],
]
.spacing(0),
)
.style(move |_: &Theme| container::Style {
background: Some(Background::Color(BG)),
..Default::default()
})
.style(dialog_body_style)
.center(Fill)
.padding([24, 28])
.into()
@ -1370,99 +1025,30 @@ fn layer_delete_warning_window(names: &[String], count: usize) -> Element<'stati
/// just dismisses. Either answer flips the persisted `default_assoc_prompted`
/// flag so the dialog never reappears.
fn default_assoc_dialog_window() -> Element<'static, Message> {
const BG: Color = Color {
r: 0.18,
g: 0.18,
b: 0.20,
a: 1.0,
};
const BORDER_COL: Color = Color {
r: 0.38,
g: 0.38,
b: 0.42,
a: 1.0,
};
const TEXT_COL: Color = Color {
r: 0.90,
g: 0.90,
b: 0.90,
a: 1.0,
};
const DIM_COL: Color = Color {
r: 0.62,
g: 0.62,
b: 0.66,
a: 1.0,
};
const BTN_YES: Color = Color {
r: 0.20,
g: 0.46,
b: 0.80,
a: 1.0,
};
const BTN_YHOV: Color = Color {
r: 0.26,
g: 0.55,
b: 0.92,
a: 1.0,
};
const BTN_NO: Color = Color {
r: 0.28,
g: 0.28,
b: 0.30,
a: 1.0,
};
const BTN_NHOV: Color = Color {
r: 0.36,
g: 0.36,
b: 0.40,
a: 1.0,
};
let btn = |label: &'static str, msg: Message, base: Color, hov: Color| {
button(text(label).size(13).color(TEXT_COL))
.on_press(msg)
.style(move |_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => hov,
_ => base,
})),
text_color: TEXT_COL,
border: Border {
color: BORDER_COL,
width: 1.0,
radius: 4.0.into(),
},
shadow: iced::Shadow::default(),
snap: false,
})
.padding([6, 18])
};
container(
column![
text("Make Open CAD Studio your default CAD app?")
.size(15)
.color(TEXT_COL),
.size(15),
iced::widget::Space::new().height(10),
text("Open .dwg and .dxf drawings in Open CAD Studio by default. You can change this later in your system settings.")
.size(12)
.color(DIM_COL),
.style(dialog_muted_text_style),
iced::widget::Space::new().height(22),
row![
iced::widget::Space::new().width(Fill),
btn("Not now", Message::AssocPromptNo, BTN_NO, BTN_NHOV),
dialog_button("Not now", Message::AssocPromptNo, button::secondary),
iced::widget::Space::new().width(8),
btn("Yes, set as default", Message::AssocPromptYes, BTN_YES, BTN_YHOV),
dialog_button(
"Yes, set as default",
Message::AssocPromptYes,
button::primary
),
]
.align_y(iced::Center),
]
.spacing(0),
)
.style(move |_: &Theme| container::Style {
background: Some(Background::Color(BG)),
..Default::default()
})
.style(dialog_body_style)
.center(Fill)
.padding([24, 28])
.into()

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@
//! boxes. Drawing them from SVG instead makes the chrome font-independent.
use iced::widget::{container, svg, Space};
use iced::{Color, Element, Length, Theme};
use iced::{Element, Length, Theme};
const TRI_DOWN: &[u8] = include_bytes!("../../assets/icons/ui/tri_down.svg");
const TRI_UP: &[u8] = include_bytes!("../../assets/icons/ui/tri_up.svg");
@ -65,7 +65,6 @@ pub const FILE_EXPORT: &[u8] = include_bytes!("../../assets/icons/ui/file_export
pub const PRINT: &[u8] = include_bytes!("../../assets/icons/ui/print.svg");
pub const HEART: &[u8] = include_bytes!("../../assets/icons/ui/heart.svg");
pub const DOT: &[u8] = include_bytes!("../../assets/icons/ui/dot.svg");
pub const TRI_LEFT_B: &[u8] = include_bytes!("../../assets/icons/ui/tri_left.svg");
pub const ARROW_LONG_RIGHT: &[u8] = include_bytes!("../../assets/icons/ui/arrow_long_right.svg");
// ── Status-bar toggle icons (issue #216) ──────────────────────────────────
@ -82,26 +81,112 @@ pub const ST_FILTER: &[u8] = include_bytes!("../../assets/icons/status/filter.sv
pub const ST_SELCYCLE: &[u8] = include_bytes!("../../assets/icons/status/selcycle.svg");
pub const ST_CLEANSCREEN: &[u8] = include_bytes!("../../assets/icons/status/cleanscreen.svg");
/// Render one of the bundled SVGs tinted to `color` at a square `size`.
pub fn tinted<'a, M: 'a>(bytes: &'static [u8], size: f32, color: Color) -> Element<'a, M> {
/// Render a chrome icon with the active Iced theme's normal text color.
pub fn themed<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
svg(svg::Handle::from_memory(bytes))
.width(size)
.height(size)
.style(move |_: &Theme, _| svg::Style { color: Some(color) })
.style(|theme: &Theme, _| svg::Style {
color: Some(theme.extended_palette().background.base.text),
})
.into()
}
/// Backwards-compatible alias used by the caret/undo/redo helpers below.
fn icon<'a, M: 'a>(bytes: &'static [u8], size: f32, color: Color) -> Element<'a, M> {
tinted(bytes, size, color)
/// Render secondary chrome with the active Iced theme's text color.
pub fn themed_secondary<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
svg(svg::Handle::from_memory(bytes))
.width(size)
.height(size)
.style(|theme: &Theme, _| svg::Style {
color: Some(
theme
.extended_palette()
.background
.base
.text
.scale_alpha(0.72),
),
})
.into()
}
/// A fixed-width (14 px) "current row" check column: a green-tintable check
/// when `active`, otherwise an empty spacer that preserves alignment. Used by
/// the many dropdown / popup list rows that mark the selected entry.
pub fn check_cell<'a, M: 'a>(active: bool, color: Color) -> Element<'a, M> {
/// Render disabled chrome with the active Iced theme's text color.
pub fn themed_disabled<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
svg(svg::Handle::from_memory(bytes))
.width(size)
.height(size)
.style(|theme: &Theme, _| svg::Style {
color: Some(
theme
.extended_palette()
.background
.base
.text
.scale_alpha(0.42),
),
})
.into()
}
/// Render an emphasized chrome icon with the active Iced theme's primary color.
pub fn themed_primary<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
svg(svg::Handle::from_memory(bytes))
.width(size)
.height(size)
.style(|theme: &Theme, _| svg::Style {
color: Some(theme.extended_palette().primary.base.color),
})
.into()
}
/// Render a positive-state chrome icon with the active Iced theme's success color.
pub fn themed_success<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
svg(svg::Handle::from_memory(bytes))
.width(size)
.height(size)
.style(|theme: &Theme, _| svg::Style {
color: Some(theme.extended_palette().success.base.color),
})
.into()
}
/// Render a warning-state chrome icon from the active Iced theme.
pub fn themed_warning<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
svg(svg::Handle::from_memory(bytes))
.width(size)
.height(size)
.style(|theme: &Theme, _| svg::Style {
color: Some(theme.extended_palette().warning.base.color),
})
.into()
}
/// Render a destructive-state chrome icon from the active Iced theme.
pub fn themed_danger<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
svg(svg::Handle::from_memory(bytes))
.width(size)
.height(size)
.style(|theme: &Theme, _| svg::Style {
color: Some(theme.extended_palette().danger.base.color),
})
.into()
}
/// Render an icon with the foreground chosen for a danger-coloured surface.
pub fn themed_danger_text<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
svg(svg::Handle::from_memory(bytes))
.width(size)
.height(size)
.style(|theme: &Theme, _| svg::Style {
color: Some(theme.extended_palette().danger.base.text),
})
.into()
}
/// Fixed-width check column colored from the active Iced theme.
pub fn themed_check_cell<'a, M: 'a>(active: bool) -> Element<'a, M> {
let inner: Element<'a, M> = if active {
tinted(CHECK, 11.0, color)
themed_primary(CHECK, 11.0)
} else {
Space::new().width(0).into()
};
@ -166,46 +251,59 @@ pub fn layer_lock(locked: bool) -> &'static [u8] {
}
}
/// Downward dropdown caret (replaces `▾`).
pub fn arrow_down<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> {
icon(TRI_DOWN, size, color)
pub fn themed_arrow_down<'a, M: 'a>(size: f32) -> Element<'a, M> {
themed(TRI_DOWN, size)
}
/// Upward dropdown caret, shown when a dropdown is open (replaces `▲`).
pub fn arrow_up<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> {
icon(TRI_UP, size, color)
pub fn themed_arrow_up<'a, M: 'a>(size: f32) -> Element<'a, M> {
themed(TRI_UP, size)
}
/// Rightward caret for a collapsed item (replaces `▸`).
pub fn arrow_right<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> {
icon(TRI_RIGHT, size, color)
pub fn themed_arrow_right<'a, M: 'a>(size: f32) -> Element<'a, M> {
themed(TRI_RIGHT, size)
}
/// Leftward caret.
pub fn arrow_left<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> {
icon(TRI_LEFT, size, color)
pub fn themed_arrow_left<'a, M: 'a>(size: f32) -> Element<'a, M> {
themed(TRI_LEFT, size)
}
/// House glyph — the ViewCube "home view" button.
pub fn home<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> {
icon(HOME, size, color)
pub fn themed_primary_arrow_down<'a, M: 'a>(size: f32) -> Element<'a, M> {
themed_primary(TRI_DOWN, size)
}
pub fn themed_secondary_arrow_down<'a, M: 'a>(size: f32) -> Element<'a, M> {
themed_secondary(TRI_DOWN, size)
}
pub fn themed_disabled_arrow_down<'a, M: 'a>(size: f32) -> Element<'a, M> {
themed_disabled(TRI_DOWN, size)
}
pub fn themed_home<'a, M: 'a>(size: f32) -> Element<'a, M> {
themed(HOME, size)
}
/// Caret that flips up/down with `open`.
pub fn arrow_toggle<'a, M: 'a>(open: bool, size: f32, color: Color) -> Element<'a, M> {
pub fn themed_arrow_toggle<'a, M: 'a>(open: bool, size: f32) -> Element<'a, M> {
if open {
arrow_up(size, color)
themed_arrow_up(size)
} else {
arrow_down(size, color)
themed_arrow_down(size)
}
}
/// Undo curved arrow (replaces `↶`).
pub fn undo<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> {
icon(UNDO, size, color)
pub fn themed_undo<'a, M: 'a>(size: f32, enabled: bool) -> Element<'a, M> {
if enabled {
themed(UNDO, size)
} else {
themed_disabled(UNDO, size)
}
}
/// Redo curved arrow (replaces `↷`).
pub fn redo<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> {
icon(REDO, size, color)
pub fn themed_redo<'a, M: 'a>(size: f32, enabled: bool) -> Element<'a, M> {
if enabled {
themed(REDO, size)
} else {
themed_disabled(REDO, size)
}
}

View file

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

View file

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

View file

@ -3,7 +3,7 @@
//! adds that object to the current selection. Clicking outside dismisses it.
use iced::widget::{button, column, container, mouse_area, opaque, text};
use iced::{Background, Border, Color, Element, Fill, Length, Theme};
use iced::{Element, Fill, Length};
use crate::app::Message;
@ -19,15 +19,7 @@ pub fn cycle_popup_overlay(
.collect();
let panel = container(column(rows))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.style(container::bordered_box)
.width(Length::Fixed(150.0));
let positioned = crate::ui::pin_at(anchor, opaque(panel));
@ -36,16 +28,10 @@ pub fn cycle_popup_overlay(
}
fn item_row(handle: acadrust::Handle, label: String) -> Element<'static, Message> {
let content = text(label).size(11).color(LABEL).align_y(iced::Center);
let content = text(label).size(11).align_y(iced::Center);
let btn = button(content)
.on_press(Message::CycleSelect(handle))
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.style(button::subtle)
.width(Fill)
.padding([4, 10]);
// Highlight the underlying object while the cursor is over this row.
@ -54,30 +40,3 @@ fn item_row(handle: acadrust::Handle, label: String) -> Element<'static, Message
.on_exit(Message::CycleHoverExit(handle))
.into()
}
// ── Colours ───────────────────────────────────────────────────────────────
const PANEL_BG: Color = Color {
r: 0.15,
g: 0.15,
b: 0.15,
a: 1.0,
};
const PANEL_BORDER: Color = Color {
r: 0.32,
g: 0.32,
b: 0.32,
a: 1.0,
};
const ROW_HOVER: Color = Color {
r: 0.22,
g: 0.45,
b: 0.62,
a: 1.0,
};
const LABEL: Color = Color {
r: 0.92,
g: 0.92,
b: 0.92,
a: 1.0,
};

View file

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

View file

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

View file

@ -1,7 +1,7 @@
//! Annotation / viewport scale status menu.
use iced::widget::{button, row, text};
use iced::{Background, Color, Element, Fill, Theme};
use iced::{Element, Fill};
use crate::app::Message;
use crate::ui::statusbar::status_menu::Entry;
@ -44,66 +44,25 @@ pub fn menu_entries(
}
fn scale_row(label: String, active: bool, msg: Message) -> Element<'static, Message> {
let check = crate::ui::icons::check_cell(active, CHECK_COLOR);
let check = crate::ui::icons::themed_check_cell(active);
let lbl = text(label)
.size(11)
.color(if active { LABEL_ON } else { LABEL_OFF });
let lbl = text(label).size(11);
let content = row![check, lbl].spacing(6).align_y(iced::Center);
button(content)
.on_press(msg)
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.style(button::subtle)
.width(Fill)
.padding([4, 10])
.into()
}
fn manage_row() -> Element<'static, Message> {
button(text("Manage...").size(11).color(CHECK_COLOR))
button(text("Manage...").size(11))
.on_press(Message::ScaleManagerOpen)
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.style(button::primary)
.width(Fill)
.padding([5, 10])
.into()
}
// ── Colours ───────────────────────────────────────────────────────────────
const ROW_HOVER: Color = Color {
r: 0.22,
g: 0.22,
b: 0.22,
a: 1.0,
};
const CHECK_COLOR: Color = Color {
r: 0.35,
g: 0.75,
b: 1.00,
a: 1.0,
};
const LABEL_ON: Color = Color {
r: 0.92,
g: 0.92,
b: 0.92,
a: 1.0,
};
const LABEL_OFF: Color = Color {
r: 0.65,
g: 0.65,
b: 0.65,
a: 1.0,
};

View file

@ -5,7 +5,7 @@
use rustc_hash::FxHashSet as HashSet;
use iced::widget::{button, container, row, text};
use iced::{Background, Border, Color, Element, Fill, Theme};
use iced::{Background, Element, Fill, Theme};
use crate::app::Message;
use crate::ui::statusbar::status_menu::Entry;
@ -37,8 +37,10 @@ pub fn menu_entries(
.padding([4u16, 8]);
let divider = container(iced::widget::Space::new().height(1))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(DIVIDER)),
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.extended_palette().background.weak.color,
)),
..Default::default()
})
.width(Fill)
@ -62,99 +64,41 @@ pub fn menu_entries(
}
fn type_row(name: String, included: bool) -> Element<'static, Message> {
let check = crate::ui::icons::check_cell(included, CHECK_COLOR);
let check = crate::ui::icons::themed_check_cell(included);
let lbl = text(name.clone())
.size(11)
.color(if included { LABEL_ON } else { LABEL_OFF });
let lbl = text(name.clone()).size(11);
let content = row![check, lbl].spacing(6).align_y(iced::Center);
button(content)
.on_press(Message::ToggleSelectionFilterType(name))
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.style(button::subtle)
.width(Fill)
.padding([4, 10])
.into()
}
fn empty_row() -> Element<'static, Message> {
container(text("No objects").size(11).color(LABEL_OFF))
container(
text("No objects").size(11).style(|theme: &Theme| text::Style {
color: Some(
theme
.extended_palette()
.background
.base
.text
.scale_alpha(0.42),
),
}),
)
.padding([4, 10])
.into()
}
fn header_btn(label: &str, msg: Message, enabled: bool) -> Element<'_, Message> {
let color = if enabled {
Color { r: 0.70, g: 0.70, b: 0.70, a: 1.0 }
} else {
Color { r: 0.38, g: 0.38, b: 0.38, a: 1.0 }
};
let b = button(text(label).size(10).color(color));
let b = button(text(label).size(10));
let b = if enabled { b.on_press(msg) } else { b };
b.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered => ROW_HOVER,
_ => BTN_BG,
})),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 2.0.into(),
},
..Default::default()
})
b.style(button::secondary)
.padding([3, 8])
.into()
}
// ── Colours ───────────────────────────────────────────────────────────────
const PANEL_BORDER: Color = Color {
r: 0.32,
g: 0.32,
b: 0.32,
a: 1.0,
};
const ROW_HOVER: Color = Color {
r: 0.22,
g: 0.22,
b: 0.22,
a: 1.0,
};
const DIVIDER: Color = Color {
r: 0.28,
g: 0.28,
b: 0.28,
a: 1.0,
};
const BTN_BG: Color = Color {
r: 0.20,
g: 0.20,
b: 0.20,
a: 1.0,
};
const CHECK_COLOR: Color = Color {
r: 0.35,
g: 0.75,
b: 1.00,
a: 1.0,
};
const LABEL_ON: Color = Color {
r: 0.92,
g: 0.92,
b: 0.92,
a: 1.0,
};
const LABEL_OFF: Color = Color {
r: 0.6,
g: 0.6,
b: 0.6,
a: 1.0,
};

View file

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

View file

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

View file

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

View file

@ -15,7 +15,7 @@ use iced::advanced::layout::{self, Layout};
use iced::advanced::widget::{self, Widget};
use iced::advanced::{mouse, overlay, renderer, Clipboard, Renderer as _, Shell};
use iced::{
Background, Border, Color, Element, Event, Length, Point, Rectangle, Renderer, Shadow, Size,
Background, Border, Element, Event, Length, Point, Rectangle, Renderer, Shadow, Size,
Theme, Vector,
};
@ -113,8 +113,6 @@ pub struct CollapsePanels<'a> {
open: Option<String>,
/// Fallback row height, used only when there are no panels to measure.
row_h: f32,
/// Colour of the 1px divider drawn between panels.
divider: Color,
/// Chosen degradation level per panel; set during layout.
levels: RefCell<Vec<u8>>,
/// If set, the measured row height is written here each layout (read when
@ -129,13 +127,12 @@ pub struct CollapsePanels<'a> {
}
impl<'a> CollapsePanels<'a> {
pub fn new(panels: Vec<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();
Self {
panels,
open,
row_h,
divider,
levels: RefCell::new(vec![FULL; n]),
height_out: None,
tight_out: None,
@ -480,7 +477,7 @@ impl<'a> Widget<Message, Theme, Renderer> for CollapsePanels<'a> {
shadow: Shadow::default(),
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 acadrust::types::{Color as AcadColor, LineWeight};
use iced::widget::{button, column, container, mouse_area, row, scrollable, svg, text};
use iced::widget::{button, column, container, mouse_area, row, scrollable, text};
use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme};
use crate::app::Message;
@ -419,58 +419,41 @@ impl Ribbon {
let is_active = i == self.active;
let is_contextual = module.id() == "layout";
let accent = if is_contextual {
ACCENT_GOLD
} else {
ACCENT_BLUE
};
let text_inactive = if is_contextual {
Color {
r: 0.90,
g: 0.72,
b: 0.30,
a: 1.0,
}
} else {
Color {
r: 0.75,
g: 0.75,
b: 0.75,
a: 1.0,
}
};
let hover_bg = if is_contextual {
Color {
r: 0.28,
g: 0.24,
b: 0.12,
a: 1.0,
}
} else {
Color {
r: 0.25,
g: 0.25,
b: 0.25,
a: 1.0,
}
};
let btn = container(
button(text(module.title()).size(12))
.on_press(Message::RibbonSelectTab(i))
.style(move |_: &Theme, status| button::Style {
background: Some(Background::Color(match (is_active, status) {
(true, _) => RIBBON_BG,
(false, button::Status::Hovered) => hover_bg,
_ => Color::TRANSPARENT,
})),
text_color: if is_active {
Color::WHITE
.style(move |theme: &Theme, status| {
let palette = theme.extended_palette();
let accent = if is_contextual {
palette.warning.base
} else {
text_inactive
palette.primary.base
};
let pair = match (is_active, status) {
(true, _) => palette.background.weakest,
(false, button::Status::Hovered) => {
if is_contextual {
palette.warning.weak
} else {
palette.background.weak
}
}
_ => palette.background.base,
};
button::Style {
background: (is_active
|| matches!(status, button::Status::Hovered))
.then_some(Background::Color(pair.color)),
text_color: if is_active {
pair.text
} else if is_contextual {
accent.color
} else {
palette.background.base.text.scale_alpha(0.72)
},
border: Border {
color: if is_active {
accent
accent.color
} else {
Color::TRANSPARENT
},
@ -479,13 +462,18 @@ impl Ribbon {
},
shadow: iced::Shadow::default(),
snap: false,
}
})
.padding([5, 14]),
)
.style(move |_: &Theme| container::Style {
.style(move |theme: &Theme| container::Style {
border: Border {
color: if is_active {
accent
if is_contextual {
theme.extended_palette().warning.base.color
} else {
theme.extended_palette().primary.base.color
}
} else {
Color::TRANSPARENT
},
@ -514,9 +502,11 @@ impl Ribbon {
// persisted (see `Ribbon::set_collapse_mode`). It hides itself once the
// tool row is tight, giving the cramped tab row its space back.
let dd_open = self.open_dropdown.as_deref() == Some(COLLAPSE_MODE_ID);
let mode_btn = button(crate::ui::icons::arrow_down(10.0, ARROW_COLOR))
let mode_btn = button(crate::ui::icons::themed_arrow_down(10.0))
.on_press(Message::ToggleRibbonDropdown(COLLAPSE_MODE_ID.to_string()))
.style(move |_: &Theme, status| top_hist_btn_style(true, dd_open, status))
.style(move |theme: &Theme, status| {
top_hist_btn_style(theme, true, dd_open, status)
})
.height(24)
.padding([2, 8]);
let mode_dd = PosReport::new(COLLAPSE_MODE_ID, mode_btn);
@ -534,8 +524,10 @@ impl Ribbon {
}
let tab_bar = container(tab_row)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(TOPBAR_BG)),
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.extended_palette().background.base.color,
)),
..Default::default()
})
.padding(Padding {
@ -653,10 +645,12 @@ impl Ribbon {
self.active_lineweight,
&style_ctx,
))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(RIBBON_BG)),
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.extended_palette().background.weakest.color,
)),
border: Border {
color: BORDER_DARK,
color: theme.extended_palette().background.neutral.color,
width: 1.0,
radius: 0.0.into(),
},
@ -666,7 +660,7 @@ impl Ribbon {
}
})
.collect();
CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H, BORDER_DARK)
CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H)
.report_height(self.tool_bar_h.clone())
.report_tight(self.collapse_tight.clone())
.mode(self.collapse_mode)
@ -676,10 +670,12 @@ impl Ribbon {
};
let tool_bar = container(tool_area)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(RIBBON_BG)),
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.extended_palette().background.weakest.color,
)),
border: Border {
color: BORDER_DARK,
color: theme.extended_palette().background.neutral.color,
width: 1.0,
radius: 0.0.into(),
},
@ -712,19 +708,13 @@ impl Ribbon {
.enumerate()
.map(|(idx, label)| {
let step = idx + 1;
button(text(label.clone()).size(11).color(LABEL_ON))
button(text(label.clone()).size(11))
.on_press(if is_undo {
Message::UndoMany(step)
} else {
Message::RedoMany(step)
})
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.style(popup_row_style)
.width(Fill)
.padding([5, 10])
.into()
@ -732,15 +722,7 @@ impl Ribbon {
.collect();
let panel = container(column(rows))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.style(popup_panel_style)
.width(Length::Fixed(170.0));
let (align_right, h_pad, top) = self.dd_anchor(open_id, 170.0, win.0);
@ -755,27 +737,16 @@ impl Ribbon {
let rows: Vec<Element<Message>> = CollapseMode::ALL
.iter()
.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(
row![
container(mark).width(Length::Fixed(16.0)),
text(m.label()).size(11).color(LABEL_ON),
crate::ui::icons::themed_check_cell(m == current),
text(m.label()).size(11),
]
.spacing(4)
.align_y(iced::Center),
)
.on_press(Message::SetRibbonCollapseMode(m))
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.style(popup_row_style)
.width(Fill)
.padding([5, 10])
.into()
@ -783,15 +754,7 @@ impl Ribbon {
.collect();
let panel = container(column(rows))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.style(popup_panel_style)
.width(Length::Fixed(W));
let (align_right, h_pad, top) = self.dd_anchor(open_id, W, win.0);
@ -852,28 +815,25 @@ impl Ribbon {
.iter()
.map(|(cmd, label, item_icon)| {
let is_current = *cmd == last_cmd;
let checkmark: Element<'_, Message> = container(if is_current {
crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR)
} else {
iced::widget::Space::new().width(0).into()
})
.width(Length::Fixed(14.0))
.into();
let icon_el: Element<Message> = match *item_icon {
IconKind::Glyph(s) => text(s)
.size(13)
.color(ICON_COLOR)
let checkmark: Element<'_, Message> =
crate::ui::icons::themed_check_cell(is_current);
let icon_el: Element<Message> =
container(make_icon(*item_icon, 20.0))
.width(Length::Fixed(20.0))
.into(),
IconKind::Svg(bytes) => {
let handle = svg::Handle::from_memory(bytes);
svg(handle).width(20).height(20).into()
}
};
.into();
let label_el =
text(*label)
.size(11)
.color(if is_current { LABEL_ON } else { LABEL_OFF });
.style(move |theme: &Theme| iced::widget::text::Style {
color: (!is_current).then_some(
theme
.extended_palette()
.background
.base
.text
.scale_alpha(0.72),
),
});
button(
row![checkmark, icon_el, label_el]
@ -884,13 +844,7 @@ impl Ribbon {
dropdown_id: dd_id,
cmd: *cmd,
})
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.style(popup_row_style)
.width(Fill)
.padding([4, 10])
.into()
@ -898,15 +852,7 @@ impl Ribbon {
.collect();
let panel = container(column(rows))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.style(popup_panel_style)
.width(Length::Fixed(190.0));
let (align_right, h_pad, top) = self.dd_anchor(open_id, 190.0, win.0);
@ -925,13 +871,7 @@ impl Ribbon {
let icon_btn = |bytes: &'static [u8], msg: Message| -> Element<'_, Message> {
button(crate::ui::icons::raw(bytes, 14.0))
.on_press(msg)
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.style(popup_row_style)
.padding([2, 4])
.into()
};
@ -952,10 +892,10 @@ impl Ribbon {
let name = info.name.clone();
let swatch = container(text(""))
.style(move |_: &Theme| container::Style {
.style(move |theme: &Theme| container::Style {
background: Some(Background::Color(lc)),
border: Border {
color: SWATCH_BORDER,
color: theme.extended_palette().background.strong.color,
width: 1.0,
radius: 1.0.into(),
},
@ -976,29 +916,27 @@ impl Ribbon {
crate::ui::icons::layer_lock(ll),
Message::LayerToggleLock(index),
);
let checkmark: Element<'_, Message> = container(if is_active {
crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR)
} else {
iced::widget::Space::new().width(0).into()
})
.width(Length::Fixed(14.0))
.into();
let checkmark: Element<'_, Message> =
crate::ui::icons::themed_check_cell(is_active);
let label =
text(&info.name)
.size(11)
.color(if is_active { LABEL_ON } else { LABEL_OFF });
.style(move |theme: &Theme| iced::widget::text::Style {
color: (!is_active).then_some(
theme
.extended_palette()
.background
.base
.text
.scale_alpha(0.72),
),
});
// The swatch + label area selects the layer as active; the
// icon buttons above handle their own toggles.
let select = button(row![swatch, label].spacing(5).align_y(iced::Center))
.on_press(Message::RibbonLayerChanged(name))
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.style(popup_row_style)
.width(Fill)
.padding([4, 4]);
@ -1021,34 +959,7 @@ impl Ribbon {
let search = iced::widget::text_input("Search layers…", &self.layer_filter)
.on_input(Message::RibbonLayerFilterChanged)
.size(11)
.padding([4, 6])
.style(|_: &Theme, _| iced::widget::text_input::Style {
background: Background::Color(Color {
r: 0.10,
g: 0.10,
b: 0.10,
a: 1.0,
}),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 2.0.into(),
},
icon: Color::WHITE,
placeholder: Color {
r: 0.45,
g: 0.45,
b: 0.45,
a: 1.0,
},
value: Color::WHITE,
selection: Color {
r: 0.20,
g: 0.44,
b: 0.72,
a: 0.5,
},
});
.padding([4, 6]);
let panel = container(
column![
container(search).padding([4, 4]),
@ -1056,15 +967,7 @@ impl Ribbon {
]
.spacing(2),
)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.style(popup_panel_style)
.width(Length::Fixed(220.0));
let (align_right, h_pad, top) = self.dd_anchor(LAYER_COMBO_ID, 220.0, win.0);
@ -1113,35 +1016,29 @@ impl Ribbon {
};
let active = ctx.active_for(style_key).to_string();
let row_style = |_: &Theme, status: button::Status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
};
let mut rows: Vec<Element<Message>> = ctx
.names_for(style_key)
.iter()
.map(|name| {
let is_sel = name.as_str() == active.as_str();
let n = name.clone();
let checkmark: Element<Message> = container(if is_sel {
crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR)
} else {
iced::widget::Space::new().width(0).into()
})
.width(Length::Fixed(14.0))
.into();
let checkmark: Element<Message> =
crate::ui::icons::themed_check_cell(is_sel);
button(
row![
checkmark,
text(name.clone()).size(11).color(if is_sel {
LABEL_ON
} else {
LABEL_OFF
}),
text(name.clone())
.size(11)
.style(move |theme: &Theme| iced::widget::text::Style {
color: (!is_sel).then_some(
theme
.extended_palette()
.background
.base
.text
.scale_alpha(0.72),
),
}),
]
.spacing(4)
.align_y(iced::Center),
@ -1150,7 +1047,7 @@ impl Ribbon {
key: style_key,
name: n,
})
.style(row_style)
.style(popup_row_style)
.width(Fill)
.padding([4, 10])
.into()
@ -1159,9 +1056,9 @@ impl Ribbon {
if let Some(mgr) = manager_cmd {
rows.push(
button(text("Manage…").size(11).color(LABEL_ON))
button(text("Manage…").size(11))
.on_press(Message::Command(mgr.to_string()))
.style(row_style)
.style(popup_row_style)
.width(Fill)
.padding([4, 10])
.into(),
@ -1169,15 +1066,7 @@ impl Ribbon {
}
let panel = container(column(rows))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.style(popup_panel_style)
.width(Length::Fixed(LARGE_W * 2.3));
let (align_right, h_pad, top) = self.dd_anchor(open_id, LARGE_W * 2.3, win.0);
@ -1200,15 +1089,7 @@ impl Ribbon {
);
let panel = container(picker)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.style(popup_panel_style)
.width(Length::Fixed(200.0));
let (align_right, h_pad, top) = self.dd_anchor(PROP_COLOR_ID, 200.0, win.0);
@ -1243,23 +1124,22 @@ impl Ribbon {
.into_iter()
.map(|lt| {
let is_cur = lt.name == *active_lt;
let check: Element<'_, Message> = container(if is_cur {
crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR)
} else {
iced::widget::Space::new().width(0).into()
})
.width(Length::Fixed(14.0))
.into();
let check: Element<'_, Message> =
crate::ui::icons::themed_check_cell(is_cur);
let name_col = text(lt.name.clone())
.size(11)
.color(if is_cur { LABEL_ON } else { LABEL_OFF })
.style(move |theme: &Theme| iced::widget::text::Style {
color: (!is_cur).then_some(
theme
.extended_palette()
.background
.base
.text
.scale_alpha(0.72),
),
})
.width(Length::Fixed(90.0));
let art_col = text(lt.art.clone()).size(9).color(Color {
r: 0.55,
g: 0.55,
b: 0.55,
a: 1.0,
});
let art_col = text(lt.art.clone()).size(9).style(muted_text_style);
let name = lt.name.clone();
button(
row![check, name_col, art_col]
@ -1267,13 +1147,7 @@ impl Ribbon {
.align_y(iced::Center),
)
.on_press(Message::RibbonLinetypeChanged(name))
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.style(popup_row_style)
.width(Fill)
.padding([4, 6])
.into()
@ -1281,15 +1155,7 @@ impl Ribbon {
.collect();
let list = container(scrollable(column(rows)).height(Length::Fixed(200.0)))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.style(popup_panel_style)
.width(Length::Fixed(220.0));
let (align_right, h_pad, top) = self.dd_anchor(PROP_LINETYPE_ID, 220.0, win.0);
@ -1308,31 +1174,29 @@ impl Ribbon {
.map(|item| {
let is_cur = item.0 == active_lw;
let label = item.to_string();
let check: Element<'_, Message> = container(if is_cur {
crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR)
} else {
iced::widget::Space::new().width(0).into()
})
.width(Length::Fixed(14.0))
.into();
let check: Element<'_, Message> =
crate::ui::icons::themed_check_cell(is_cur);
button(
row![
check,
text(label)
.size(11)
.color(if is_cur { LABEL_ON } else { LABEL_OFF })
.style(move |theme: &Theme| iced::widget::text::Style {
color: (!is_cur).then_some(
theme
.extended_palette()
.background
.base
.text
.scale_alpha(0.72),
),
})
]
.spacing(5)
.align_y(iced::Center),
)
.on_press(Message::RibbonLineweightChanged(item.0))
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.style(popup_row_style)
.width(Fill)
.padding([4, 8])
.into()
@ -1350,15 +1214,7 @@ impl Ribbon {
win: (f32, f32),
) -> Option<Element<'a, Message>> {
let panel = container(column(rows))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.style(popup_panel_style)
.width(Length::Fixed(width));
let (align_right, h_pad, top) = self.dd_anchor(dd_id, width, win.0);
@ -1439,7 +1295,7 @@ fn render_group<'a>(
column![
tools_el,
container(text(group.title).size(9).color(GROUP_LABEL)).padding([1, 4]),
container(text(group.title).size(9).style(muted_text_style)).padding([1, 4]),
]
.align_x(iced::Center)
.spacing(0)
@ -1526,8 +1382,8 @@ fn collapse_button<'a>(
column![
icon,
row![
text(title.to_string()).size(9).color(GROUP_LABEL),
crate::ui::icons::arrow_down(8.0, GROUP_LABEL),
text(title.to_string()).size(9).style(muted_text_style),
crate::ui::icons::themed_secondary_arrow_down(8.0),
]
.spacing(3)
.align_y(iced::Center),
@ -1536,22 +1392,7 @@ fn collapse_button<'a>(
.spacing(2),
)
.on_press(Message::ToggleRibbonPanel(title.to_string()))
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered => Color {
r: 0.25,
g: 0.25,
b: 0.25,
a: 1.0,
},
_ => Color::TRANSPARENT,
})),
border: Border {
radius: 2.0.into(),
..Default::default()
},
..Default::default()
})
.style(button::subtle)
.padding([3, 5])
.into();
}
@ -1594,29 +1435,14 @@ fn collapse_button<'a>(
let opener = button(
row![
text(title.to_string()).size(9).color(GROUP_LABEL),
crate::ui::icons::arrow_down(8.0, GROUP_LABEL),
text(title.to_string()).size(9).style(muted_text_style),
crate::ui::icons::themed_secondary_arrow_down(8.0),
]
.spacing(3)
.align_y(iced::Center),
)
.on_press(Message::ToggleRibbonPanel(title.to_string()))
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered => Color {
r: 0.25,
g: 0.25,
b: 0.25,
a: 1.0,
},
_ => Color::TRANSPARENT,
})),
border: Border {
radius: 2.0.into(),
..Default::default()
},
..Default::default()
})
.style(button::subtle)
.padding([1, 4]);
// The large face fills a fixed slot so a collapsed panel is shorter than a full

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
// rests on the button itself — never covers the tip text. (#143)
use iced::widget::tooltip::Position as TipPos;
use iced::widget::{button, column, container, row, svg, text, tooltip};
use iced::widget::{button, column, container, row, text, tooltip};
use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme};
use crate::app::Message;
@ -77,175 +77,6 @@ pub(super) const PROP_COLOR_ID: &str = "PROP_COLOR";
pub(super) const PROP_LINETYPE_ID: &str = "PROP_LINETYPE";
pub(super) const PROP_LW_ID: &str = "PROP_LW";
// ── Colours ────────────────────────────────────────────────────────────────
/// Light chrome grey for the quick-access file-command icons on the top strip.
pub(super) const QA_ICON_COLOR: Color = Color {
r: 0.82,
g: 0.83,
b: 0.85,
a: 1.0,
};
pub(super) const TOPBAR_BG: Color = Color {
r: 0.17,
g: 0.17,
b: 0.17,
a: 1.0,
};
pub(super) const RIBBON_BG: Color = Color {
r: 0.22,
g: 0.22,
b: 0.22,
a: 1.0,
};
pub(super) const BORDER_DARK: Color = Color {
r: 0.12,
g: 0.12,
b: 0.12,
a: 1.0,
};
pub(super) const ACCENT_BLUE: Color = Color {
r: 0.20,
g: 0.55,
b: 0.90,
a: 1.0,
};
pub(super) const ACCENT_GOLD: Color = Color {
r: 0.90,
g: 0.65,
b: 0.10,
a: 1.0,
};
pub(super) const LABEL_COLOR: Color = Color {
r: 0.82,
g: 0.82,
b: 0.82,
a: 1.0,
};
pub(super) const GROUP_LABEL: Color = Color {
r: 0.50,
g: 0.50,
b: 0.50,
a: 1.0,
};
pub(super) const TOOL_HOVER: Color = Color {
r: 0.32,
g: 0.32,
b: 0.32,
a: 1.0,
};
pub(super) const TOOL_ACTIVE: Color = Color {
r: 0.18,
g: 0.42,
b: 0.70,
a: 1.0,
};
pub(super) const ARROW_COLOR: Color = Color {
r: 0.65,
g: 0.65,
b: 0.65,
a: 1.0,
};
pub(super) const PANEL_BG: Color = Color {
r: 0.16,
g: 0.16,
b: 0.16,
a: 0.98,
};
pub(super) const PANEL_BORDER: Color = Color {
r: 0.32,
g: 0.32,
b: 0.32,
a: 1.0,
};
pub(super) const ROW_HOVER: Color = Color {
r: 0.24,
g: 0.24,
b: 0.24,
a: 1.0,
};
pub(super) const CHECK_COLOR: Color = Color {
r: 0.20,
g: 0.75,
b: 0.35,
a: 1.0,
};
pub(super) const ICON_COLOR: Color = Color {
r: 0.25,
g: 0.75,
b: 0.45,
a: 1.0,
};
pub(super) const LABEL_ON: Color = Color {
r: 0.92,
g: 0.92,
b: 0.92,
a: 1.0,
};
pub(super) const LABEL_OFF: Color = Color {
r: 0.72,
g: 0.72,
b: 0.72,
a: 1.0,
};
// ── Combo / dropdown colors ───────────────────────────────────────────────
pub(super) const COMBO_BG: Color = Color {
r: 0.18,
g: 0.18,
b: 0.18,
a: 1.0,
};
pub(super) const COMBO_HOVER_BG: Color = Color {
r: 0.26,
g: 0.26,
b: 0.26,
a: 1.0,
};
pub(super) const COMBO_OPEN_BG: Color = Color {
r: 0.14,
g: 0.14,
b: 0.14,
a: 1.0,
};
pub(super) const COMBO_BORDER: Color = Color {
r: 0.35,
g: 0.35,
b: 0.35,
a: 1.0,
};
pub(super) const COMBO_ACTIVE_BORDER: Color = Color {
r: 0.45,
g: 0.65,
b: 0.90,
a: 1.0,
};
pub(super) const COMBO_ARROW: Color = Color {
r: 0.70,
g: 0.70,
b: 0.70,
a: 1.0,
};
pub(super) const SWATCH_BORDER: Color = Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.5,
};
pub(super) const TIP_BG: Color = Color {
r: 0.13,
g: 0.13,
b: 0.13,
a: 0.97,
};
pub(super) const HIST_INACTIVE_BG: Color = Color {
r: 0.20,
g: 0.20,
b: 0.20,
a: 1.0,
};
// ── Style context (passed from Ribbon to render_large) ────────────────────
pub(super) struct StyleContext {
@ -294,11 +125,8 @@ pub(super) fn flush_small_col<'a>(
pub(super) fn make_icon(icon: IconKind, size: f32) -> Element<'static, Message> {
match icon {
IconKind::Glyph(s) => text(s).size(size * 0.7).color(Color::WHITE).into(),
IconKind::Svg(bytes) => {
let handle = svg::Handle::from_memory(bytes);
svg(handle).width(size).height(size).into()
}
IconKind::Glyph(s) => text(s).size(size * 0.7).into(),
IconKind::Svg(bytes) => icons::themed(bytes, size),
}
}
@ -309,22 +137,19 @@ pub(super) fn start_dimmed(state: &ToggleState, event: &ModuleEvent) -> bool {
&& !matches!(event, ModuleEvent::Command(c) if crate::app::commands::start_allowed(c))
}
/// Label / glyph color for a possibly-dimmed tool.
pub(super) const DIM_TOOL: Color = Color {
r: 0.42,
g: 0.42,
b: 0.45,
a: 1.0,
};
/// `make_icon`, greyed out when `dim` (SVGs render monochrome via tint).
pub(super) fn make_icon_dim(icon: IconKind, size: f32, dim: bool) -> Element<'static, Message> {
if !dim {
return make_icon(icon, size);
}
match icon {
IconKind::Glyph(s) => text(s).size(size * 0.7).color(DIM_TOOL).into(),
IconKind::Svg(bytes) => icons::tinted(bytes, size, DIM_TOOL),
IconKind::Glyph(s) => text(s)
.size(size * 0.7)
.style(|theme: &Theme| iced::widget::text::Style {
color: Some(theme.extended_palette().background.base.text.scale_alpha(0.42)),
})
.into(),
IconKind::Svg(bytes) => icons::themed_disabled(bytes, size),
}
}
@ -349,15 +174,24 @@ pub(super) fn is_active_tool(
// ── Button style ───────────────────────────────────────────────────────────
pub(super) fn tool_btn_style(is_active: bool, status: button::Status) -> button::Style {
pub(super) fn tool_btn_style(
theme: &Theme,
is_active: bool,
status: button::Status,
) -> button::Style {
let palette = theme.extended_palette();
let pair = match (is_active, status) {
(true, _) => palette.primary.weak,
(_, button::Status::Hovered) => palette.background.weak,
(_, button::Status::Pressed) => palette.primary.weak,
_ => palette.background.base,
};
button::Style {
background: Some(Background::Color(match (is_active, status) {
(true, _) => TOOL_ACTIVE,
(_, button::Status::Hovered) => TOOL_HOVER,
(_, button::Status::Pressed) => TOOL_ACTIVE,
_ => Color::TRANSPARENT,
})),
text_color: Color::WHITE,
background: is_active
.then_some(Background::Color(pair.color))
.or_else(|| matches!(status, button::Status::Hovered | button::Status::Pressed)
.then_some(Background::Color(pair.color))),
text_color: pair.text,
border: Border {
radius: 3.0.into(),
color: Color::TRANSPARENT,
@ -368,21 +202,93 @@ pub(super) fn tool_btn_style(is_active: bool, status: button::Status) -> button:
}
}
// ── Tooltip helpers ────────────────────────────────────────────────────────
pub(super) fn make_tip(tip: String) -> Element<'static, Message> {
text(tip).size(11).color(Color::WHITE).into()
pub(super) fn combo_btn_style(
theme: &Theme,
is_open: bool,
status: button::Status,
radius: f32,
) -> button::Style {
let palette = theme.extended_palette();
let pair = if is_open {
palette.primary.weak
} else if matches!(status, button::Status::Hovered | button::Status::Pressed) {
palette.background.weak
} else {
palette.background.weakest
};
button::Style {
background: Some(Background::Color(pair.color)),
text_color: pair.text,
border: Border {
radius: radius.into(),
width: 1.0,
color: if is_open {
palette.primary.base.color
} else {
palette.background.neutral.color
},
},
..Default::default()
}
}
pub(super) fn tip_style(_theme: &Theme) -> container::Style {
pub(super) fn popup_row_style(theme: &Theme, status: button::Status) -> button::Style {
let palette = theme.extended_palette();
let pair = if matches!(status, button::Status::Hovered | button::Status::Pressed) {
palette.background.weak
} else {
palette.background.base
};
button::Style {
background: Some(Background::Color(pair.color)),
text_color: pair.text,
..Default::default()
}
}
pub(super) fn popup_panel_style(theme: &Theme) -> container::Style {
let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(TIP_BG)),
background: Some(Background::Color(palette.background.base.color)),
border: Border {
color: COMBO_BORDER,
color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
text_color: Some(Color::WHITE),
..Default::default()
}
}
pub(super) fn muted_text_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
color: Some(theme.extended_palette().background.base.text.scale_alpha(0.72)),
}
}
pub(super) fn tool_label_style(theme: &Theme, dim: bool) -> iced::widget::text::Style {
iced::widget::text::Style {
color: dim.then_some(
theme.extended_palette().background.base.text.scale_alpha(0.42),
),
}
}
// ── Tooltip helpers ────────────────────────────────────────────────────────
pub(super) fn make_tip(tip: String) -> Element<'static, Message> {
text(tip).size(11).into()
}
pub(super) fn tip_style(theme: &Theme) -> container::Style {
let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.strong.color)),
border: Border {
color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
text_color: Some(palette.background.strong.text),
..Default::default()
}
}
@ -408,7 +314,7 @@ pub(super) fn render_small<'a>(
let tip_text = format!("{}\nCommand: {}", t.label, t.id);
let btn = button(make_icon_dim(t.icon, SMALL_ICON, dim))
.on_press(Message::RibbonToolClick { tool_id, event })
.style(move |_: &Theme, status| tool_btn_style(active, status))
.style(move |theme: &Theme, status| tool_btn_style(theme, active, status))
.width(Length::Fixed(SMALL_W))
.height(ROW_H)
.padding([4, 4]);
@ -474,31 +380,22 @@ pub(super) fn render_small<'a>(
tool_id: last.to_string(),
event: ModuleEvent::Command(last.to_string()),
})
.style(move |_: &Theme, status| tool_btn_style(active, status))
.style(move |theme: &Theme, status| tool_btn_style(theme, active, status))
.width(Length::Fixed(SMALL_W))
.height(ROW_H)
.padding([4, 4]);
let arr_tip = format!("{} options", cur_label);
let arr_btn = button(
container(icons::arrow_down(8.0, ARROW_COLOR))
container(icons::themed_arrow_down(8.0))
.width(Fill)
.height(Fill)
.align_x(iced::Center)
.align_y(iced::Center),
)
.on_press(Message::ToggleRibbonDropdown(id.to_string()))
.style(move |_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => TOOL_HOVER,
_ if dd_open => TOOL_ACTIVE,
_ => Color::TRANSPARENT,
})),
border: Border {
radius: 2.0.into(),
..Default::default()
},
..Default::default()
.style(move |theme: &Theme, status| {
tool_btn_style(theme, dd_open, status)
})
.width(Length::Fixed(ARROW_W))
.height(ROW_H)
@ -569,7 +466,9 @@ pub(super) fn render_large_dropdown<'a>(
let top_btn = button(
column![
make_icon_dim(cur_icon, LARGE_ICON, dim),
text(label.to_string()).size(10).color(if dim { DIM_TOOL } else { LABEL_COLOR }),
text(label.to_string())
.size(10)
.style(move |theme: &Theme| tool_label_style(theme, dim)),
]
.align_x(iced::Center)
.spacing(3),
@ -578,7 +477,7 @@ pub(super) fn render_large_dropdown<'a>(
tool_id: last.to_string(),
event: ModuleEvent::Command(last.to_string()),
})
.style(move |_: &Theme, status| tool_btn_style(active, status))
.style(move |theme: &Theme, status| tool_btn_style(theme, active, status))
.width(Length::Fixed(LARGE_W))
.height(Fill)
.padding(Padding {
@ -589,24 +488,15 @@ pub(super) fn render_large_dropdown<'a>(
});
let arr_btn = button(
container(icons::arrow_down(9.0, ARROW_COLOR))
container(icons::themed_arrow_down(9.0))
.width(Fill)
.height(Fill)
.align_x(iced::Center)
.align_y(iced::Center),
)
.on_press(Message::ToggleRibbonDropdown(id.to_string()))
.style(move |_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => TOOL_HOVER,
_ if dd_open => TOOL_ACTIVE,
_ => Color::TRANSPARENT,
})),
border: Border {
radius: 3.0.into(),
..Default::default()
},
..Default::default()
.style(move |theme: &Theme, status| {
tool_btn_style(theme, dd_open, status)
})
.width(Length::Fixed(LARGE_W))
.height(LARGE_ARR)
@ -659,13 +549,15 @@ pub(super) fn render_large<'a>(
let btn = button(
column![
make_icon_dim(t.icon, LARGE_ICON, dim),
text(t.label).size(10).color(if dim { DIM_TOOL } else { LABEL_COLOR }),
text(t.label)
.size(10)
.style(move |theme: &Theme| tool_label_style(theme, dim)),
]
.align_x(iced::Center)
.spacing(3),
)
.on_press(Message::RibbonToolClick { tool_id, event })
.style(move |_: &Theme, status| tool_btn_style(active, status))
.style(move |theme: &Theme, status| tool_btn_style(theme, active, status))
.width(Length::Fixed(LARGE_W))
.height(Fill)
.padding(Padding {
@ -737,10 +629,10 @@ pub(super) fn render_large<'a>(
let freeze_icon = icons::raw(icons::layer_freeze(lf), 14.0);
let lock_icon = icons::raw(icons::layer_lock(ll), 14.0);
let swatch = container(text(""))
.style(move |_: &Theme| container::Style {
.style(move |theme: &Theme| container::Style {
background: Some(Background::Color(lc)),
border: Border {
color: SWATCH_BORDER,
color: theme.extended_palette().background.strong.color,
width: 1.0,
radius: 1.0.into(),
},
@ -758,27 +650,17 @@ pub(super) fn render_large<'a>(
freeze_icon,
lock_icon,
swatch,
container(text(active_layer).size(11).color(Color::WHITE))
container(text(active_layer).size(11))
.width(name_w)
.clip(true),
icons::arrow_down(9.0, COMBO_ARROW),
icons::themed_arrow_down(9.0),
]
.spacing(4)
.align_y(iced::Center),
)
.on_press(Message::ToggleRibbonDropdown(LAYER_COMBO_ID.to_string()))
.style(move |_: &Theme, status| button::Style {
background: Some(Background::Color(match (is_open, status) {
(true, _) => COMBO_OPEN_BG,
(_, button::Status::Hovered) => COMBO_HOVER_BG,
_ => COMBO_BG,
})),
border: Border {
radius: 3.0.into(),
width: 1.0,
color: COMBO_BORDER,
},
..Default::default()
.style(move |theme: &Theme, status| {
combo_btn_style(theme, is_open, status, 3.0)
})
.padding([3, 8])
.width(Fill);
@ -793,20 +675,16 @@ pub(super) fn render_large<'a>(
let event = t.event.clone();
let icon_el: Element<Message> = if dim {
make_icon_dim(t.icon, 16.0, true)
} else { match t.icon {
IconKind::Glyph(g) => text(g).size(13).color(Color::WHITE).into(),
IconKind::Svg(bytes) => {
iced::widget::svg(iced::widget::svg::Handle::from_memory(bytes))
.width(16)
.height(16)
.into()
}
} };
} else {
make_icon(t.icon, 16.0)
};
let msg = module_event_to_message(event);
tooltip(
button(icon_el)
.on_press(msg)
.style(move |_: &Theme, status| tool_btn_style(is_active, status))
.style(move |theme: &Theme, status| {
tool_btn_style(theme, is_active, status)
})
.padding([2, 5]),
make_tip(tip.to_string()),
TipPos::Right,
@ -864,7 +742,9 @@ pub(super) fn render_large<'a>(
let mp_btn = button(
column![
make_icon_dim(match_prop.icon, LARGE_ICON, mp_dim),
text(match_prop.label).size(10).color(if mp_dim { DIM_TOOL } else { LABEL_COLOR }),
text(match_prop.label)
.size(10)
.style(move |theme: &Theme| tool_label_style(theme, mp_dim)),
]
.align_x(iced::Center)
.spacing(3),
@ -873,7 +753,7 @@ pub(super) fn render_large<'a>(
tool_id: mp_id,
event: mp_event,
})
.style(move |_: &Theme, status| tool_btn_style(mp_active, status))
.style(move |theme: &Theme, status| tool_btn_style(theme, mp_active, status))
.width(Length::Fixed(LARGE_W))
.height(Fill)
.padding(Padding {
@ -895,10 +775,10 @@ pub(super) fn render_large<'a>(
let is_open = open_dd.as_deref() == Some(dd_id);
let swatch_el: Element<'a, Message> = if let Some(c) = swatch {
container(text(""))
.style(move |_: &Theme| container::Style {
.style(move |theme: &Theme| container::Style {
background: Some(Background::Color(c)),
border: Border {
color: SWATCH_BORDER,
color: theme.extended_palette().background.strong.color,
width: 1.0,
radius: 1.0.into(),
},
@ -913,40 +793,21 @@ pub(super) fn render_large<'a>(
button(
row![
swatch_el,
container(text(label).size(10).color(Color::WHITE))
container(text(label).size(10))
.width(Fill)
.clip(true),
icons::arrow_toggle(
is_open,
8.0,
Color {
r: 0.6,
g: 0.6,
b: 0.6,
a: 1.0,
},
),
if is_open {
icons::themed_arrow_up(8.0)
} else {
icons::themed_arrow_down(8.0)
},
]
.spacing(4)
.align_y(iced::Center),
)
.on_press(Message::ToggleRibbonDropdown(dd_id.to_string()))
.style(move |_: &Theme, status| button::Style {
background: Some(Background::Color(match (is_open, status) {
(true, _) => COMBO_OPEN_BG,
(_, button::Status::Hovered) => COMBO_HOVER_BG,
_ => COMBO_BG,
})),
border: Border {
radius: 2.0.into(),
width: 1.0,
color: if is_open {
COMBO_ACTIVE_BORDER
} else {
COMBO_BORDER
},
},
..Default::default()
.style(move |theme: &Theme, status| {
combo_btn_style(theme, is_open, status, 2.0)
})
.padding([3, 8])
.width(Length::Fixed(PROP_W))
@ -999,31 +860,21 @@ pub(super) fn render_large<'a>(
// ── combo button ──
let combo_btn = button(
row![
container(text(active.clone()).size(11).color(Color::WHITE))
container(text(active.clone()).size(11))
.width(Fill)
.clip(true),
icons::arrow_toggle(is_open, 9.0, COMBO_ARROW),
if is_open {
icons::themed_arrow_up(9.0)
} else {
icons::themed_arrow_down(9.0)
},
]
.spacing(4)
.align_y(iced::Center),
)
.on_press(Message::ToggleRibbonDropdown(combo_id.to_string()))
.style(move |_: &Theme, status| button::Style {
background: Some(Background::Color(match (is_open, status) {
(true, _) => COMBO_OPEN_BG,
(_, button::Status::Hovered) => COMBO_HOVER_BG,
_ => COMBO_BG,
})),
border: Border {
radius: 3.0.into(),
width: 1.0,
color: if is_open {
COMBO_ACTIVE_BORDER
} else {
COMBO_BORDER
},
},
..Default::default()
.style(move |theme: &Theme, status| {
combo_btn_style(theme, is_open, status, 3.0)
})
.padding([3, 8])
.width(Fill);
@ -1045,20 +896,16 @@ pub(super) fn render_large<'a>(
let event = t.event.clone();
let icon_el: Element<Message> = if dim {
make_icon_dim(t.icon, 16.0, true)
} else { match t.icon {
IconKind::Glyph(g) => text(g).size(13).color(Color::WHITE).into(),
IconKind::Svg(bytes) => {
iced::widget::svg(iced::widget::svg::Handle::from_memory(bytes))
.width(16)
.height(16)
.into()
}
} };
} else {
make_icon(t.icon, 16.0)
};
let msg = module_event_to_message(event);
tooltip(
button(icon_el)
.on_press(msg)
.style(move |_: &Theme, status| tool_btn_style(is_active, status))
.style(move |theme: &Theme, status| {
tool_btn_style(theme, is_active, status)
})
.padding([2, 5]),
make_tip(tip.to_string()),
TipPos::Right,
@ -1128,7 +975,11 @@ pub(super) fn quick_access_btn<'a>(
// they read on the dark top strip (raw black is invisible there).
// On the Start tab, commands the start gate refuses render dimmed.
let dim = is_start && !crate::app::commands::start_allowed(cmd);
let icon = icons::tinted(icon_bytes, 16.0, if dim { DIM_TOOL } else { QA_ICON_COLOR });
let icon = if dim {
icons::themed_disabled(icon_bytes, 16.0)
} else {
icons::themed(icon_bytes, 16.0)
};
let btn = button(
container(icon)
.width(Fill)
@ -1137,22 +988,7 @@ pub(super) fn quick_access_btn<'a>(
.align_y(iced::Center),
)
.on_press(Message::Command(cmd.to_string()))
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => Color {
r: 0.30,
g: 0.30,
b: 0.30,
a: 1.0,
},
_ => Color::TRANSPARENT,
})),
border: Border {
radius: 2.0.into(),
..Default::default()
},
..Default::default()
})
.style(button::subtle)
.width(Length::Fixed(TOP_HIST_W))
.height(24)
.padding([2, 0]);
@ -1171,13 +1007,12 @@ pub(super) fn render_history_control<'a>(
) -> Element<'a, Message> {
let dd_open = open_dropdown.as_deref() == Some(dropdown_id);
let active = count > 0;
let icon_color = if active { Color::WHITE } else { LABEL_OFF };
let main_btn = {
let glyph = if dropdown_id == UNDO_HISTORY_ID {
icons::undo(15.0, icon_color)
icons::themed_undo(15.0, active)
} else {
icons::redo(15.0, icon_color)
icons::themed_redo(15.0, active)
};
let btn = button(
container(glyph)
@ -1186,7 +1021,9 @@ pub(super) fn render_history_control<'a>(
.align_x(iced::Center)
.align_y(iced::Center),
)
.style(move |_: &Theme, status| top_hist_btn_style(active, dd_open, status))
.style(move |theme: &Theme, status| {
top_hist_btn_style(theme, active, dd_open, status)
})
.width(Length::Fixed(TOP_HIST_W))
.height(24)
.padding([2, 0]);
@ -1211,16 +1048,19 @@ pub(super) fn render_history_control<'a>(
let arrow_btn = {
let btn = button(
container(icons::arrow_down(
8.0,
if active { ARROW_COLOR } else { LABEL_OFF },
))
container(if active {
icons::themed_arrow_down(8.0)
} else {
icons::themed_disabled_arrow_down(8.0)
})
.width(Fill)
.height(Fill)
.align_x(iced::Center)
.align_y(iced::Center),
)
.style(move |_: &Theme, status| top_hist_btn_style(active, dd_open, status))
.style(move |theme: &Theme, status| {
top_hist_btn_style(theme, active, dd_open, status)
})
.width(Length::Fixed(TOP_ARR_W))
.height(24)
.padding(0);
@ -1243,19 +1083,26 @@ pub(super) fn render_history_control<'a>(
}
pub(super) fn top_hist_btn_style(
theme: &Theme,
active: bool,
open: bool,
status: button::Status,
) -> button::Style {
let palette = theme.extended_palette();
let pair = match (active, open, status) {
(false, _, _) => palette.background.weakest,
(_, true, _) => palette.primary.weak,
(_, _, button::Status::Hovered) => palette.background.weak,
(_, _, button::Status::Pressed) => palette.primary.weak,
_ => palette.background.base,
};
button::Style {
background: Some(Background::Color(match (active, open, status) {
(false, _, _) => HIST_INACTIVE_BG,
(_, true, _) => TOOL_ACTIVE,
(_, _, button::Status::Hovered) => TOOL_HOVER,
(_, _, button::Status::Pressed) => TOOL_ACTIVE,
_ => Color::TRANSPARENT,
})),
text_color: Color::WHITE,
background: (!active || open || matches!(
status,
button::Status::Hovered | button::Status::Pressed
))
.then_some(Background::Color(pair.color)),
text_color: pair.text,
border: Border {
radius: 3.0.into(),
color: Color::TRANSPARENT,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,52 +10,7 @@
use crate::app::{Message, StyleKind};
use iced::widget::button::{Status, Style};
use iced::widget::{button, column, container, row, scrollable, text, Space};
use iced::{Background, Border, Color, Element, Fill, Theme};
// Shared palette — also reused by the scale manager so it matches the style
// managers exactly ([[feedback_shared_infra]]).
pub(crate) const TB: Color = Color {
r: 0.13,
g: 0.13,
b: 0.13,
a: 1.0,
};
pub(crate) const BG: Color = Color {
r: 0.15,
g: 0.15,
b: 0.15,
a: 1.0,
};
pub(crate) const BORDER: Color = Color {
r: 0.35,
g: 0.35,
b: 0.35,
a: 1.0,
};
pub(crate) const TEXT: Color = Color {
r: 0.88,
g: 0.88,
b: 0.88,
a: 1.0,
};
pub(crate) const DIM: Color = Color {
r: 0.55,
g: 0.55,
b: 0.55,
a: 1.0,
};
const ACCENT: Color = Color {
r: 0.25,
g: 0.50,
b: 0.85,
a: 1.0,
};
pub(crate) const LIST: Color = Color {
r: 0.12,
g: 0.12,
b: 0.12,
a: 1.0,
};
use iced::{Background, Border, Element, Fill, Theme};
/// Everything the shared frame needs. The per-manager `editor` element is the
/// only bespoke part.
@ -103,8 +58,10 @@ pub fn view<'a, 'b>(s: Scaffold<'a, 'b>) -> Element<'a, Message> {
.spacing(4)
.align_y(iced::Center);
let toolbar = container(bar)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(TB)),
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.extended_palette().background.weak.color
)),
..Default::default()
})
.width(Fill)
@ -131,16 +88,19 @@ pub fn view<'a, 'b>(s: Scaffold<'a, 'b>) -> Element<'a, Message> {
let list_panel = container(
column![
text("Styles").size(10).color(DIM),
text("Styles").size(10).style(muted_text_style),
container(scrollable(column(rows).spacing(1)).height(Fill))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(LIST)),
.style(|theme: &Theme| {
let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.weak.color)),
border: Border {
color: BORDER,
color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into()
},
..Default::default()
}
})
.width(Fill)
.height(Fill)
@ -161,8 +121,10 @@ pub fn view<'a, 'b>(s: Scaffold<'a, 'b>) -> Element<'a, Message> {
let body = row![list_panel, vsep(), s.editor].height(Fill);
container(column![toolbar, hdivider(), body])
.style(|_: &Theme| container::Style {
background: Some(Background::Color(BG)),
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.extended_palette().background.base.color
)),
..Default::default()
})
.width(Fill)
@ -182,35 +144,30 @@ pub(crate) fn tb_button<'a>(label: &'a str, msg: Message, accent: bool) -> Eleme
}
fn btn_s(accent: bool) -> impl Fn(&Theme, Status) -> Style {
move |_: &Theme, st| Style {
background: Some(Background::Color(match (accent, st) {
(true, Status::Hovered | Status::Pressed) => Color {
r: 0.20,
g: 0.42,
b: 0.72,
a: 1.0,
},
(false, Status::Hovered | Status::Pressed) => Color {
r: 0.28,
g: 0.28,
b: 0.28,
a: 1.0,
},
(true, _) => ACCENT,
_ => Color {
r: 0.22,
g: 0.22,
b: 0.22,
a: 1.0,
},
})),
text_color: TEXT,
move |theme: &Theme, st| {
let palette = theme.extended_palette();
let pair = match (accent, st) {
(true, Status::Hovered | Status::Pressed) => palette.primary.strong,
(false, Status::Hovered | Status::Pressed) => palette.background.strong,
(true, _) => palette.primary.base,
_ => palette.background.weak,
};
Style {
background: Some(Background::Color(pair.color)),
text_color: pair.text,
border: Border {
color: BORDER,
color: palette.background.neutral.color,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
}
}
}
pub(crate) fn muted_text_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
}
}
@ -218,8 +175,10 @@ pub(crate) fn hdivider<'a>() -> Element<'a, Message> {
container(Space::new().width(Fill).height(1))
.width(Fill)
.height(1)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(BORDER)),
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.extended_palette().background.neutral.color
)),
..Default::default()
})
.into()
@ -229,8 +188,10 @@ pub(crate) fn vsep<'a>() -> Element<'a, Message> {
container(Space::new().width(1).height(Fill))
.width(1)
.height(Fill)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(BORDER)),
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.extended_palette().background.neutral.color
)),
..Default::default()
})
.into()

View file

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

View file

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

View file

@ -1,48 +1,16 @@
use crate::app::Message;
use iced::widget::{button, column, container, row, text};
use iced::{Background, Border, Color, Element, Theme};
const BG: Color = Color {
r: 0.15,
g: 0.15,
b: 0.15,
a: 1.0,
};
const PANEL: Color = Color {
r: 0.12,
g: 0.12,
b: 0.12,
a: 1.0,
};
const BORDER: Color = Color {
r: 0.30,
g: 0.30,
b: 0.30,
a: 1.0,
};
const DIM: Color = Color {
r: 0.55,
g: 0.55,
b: 0.55,
a: 1.0,
};
const ACCENT: Color = Color {
r: 0.25,
g: 0.50,
b: 0.85,
a: 1.0,
};
const WHITE: Color = Color {
r: 0.92,
g: 0.92,
b: 0.92,
a: 1.0,
};
use iced::{Background, Element, Theme};
fn info_row<'a>(label: &'static str, value: String) -> Element<'a, Message> {
row![
text(label).size(11).color(DIM).width(100),
text(value).size(11).color(WHITE),
text(label)
.size(11)
.style(|theme: &Theme| iced::widget::text::Style {
color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
})
.width(100),
text(value).size(11),
]
.spacing(8)
.align_y(iced::Center)
@ -57,10 +25,16 @@ pub fn view_window<'a>() -> Element<'a, Message> {
let logo = container(
column![
text("Open CAD Studio").size(32).color(ACCENT),
text("Open CAD Studio")
.size(32)
.style(|theme: &Theme| iced::widget::text::Style {
color: Some(theme.extended_palette().primary.base.color),
}),
text("CAD application for Architecture & Engineering")
.size(11)
.color(DIM),
.style(|theme: &Theme| iced::widget::text::Style {
color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
}),
]
.spacing(4)
.align_x(iced::Center),
@ -82,35 +56,11 @@ pub fn view_window<'a>() -> Element<'a, Message> {
.spacing(2)
.padding([12, 16]),
)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(PANEL)),
border: Border {
color: BORDER,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
});
.style(container::bordered_box);
let copy_btn = button(text("Copy Info").size(11))
.on_press(Message::AboutCopyInfo)
.style(|_: &Theme, st| button::Style {
background: Some(Background::Color(match st {
button::Status::Hovered | button::Status::Pressed => Color {
r: 0.20,
g: 0.42,
b: 0.72,
a: 1.0,
},
_ => ACCENT,
})),
text_color: WHITE,
border: Border {
radius: 4.0.into(),
..Default::default()
},
..Default::default()
})
.style(button::primary)
.padding([6, 16]);
let footer = row![copy_btn]
@ -132,8 +82,10 @@ pub fn view_window<'a>() -> Element<'a, Message> {
left: 20.0,
}),
)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(BG)),
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.extended_palette().background.base.color,
)),
..Default::default()
})
.into()

View file

@ -6,7 +6,7 @@
use crate::app::Message;
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
use iced::{Background, Border, Color, Element, Fill, Length, Theme};
use iced::{Background, Element, Fill, Length, Theme};
/// Which column of an alias row a text edit targets.
#[derive(Clone, Copy, Debug)]
@ -18,33 +18,21 @@ pub enum AliasField {
/// Right-hand lane reserved for the scrollbar so it never overlaps the ✕ column.
const GUTTER: f32 = 16.0;
const BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 };
const FIELD_BG: Color = Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 };
const BORDER: Color = Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 };
const DIM: Color = Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 };
const WHITE: Color = Color { r: 0.85, g: 0.85, b: 0.85, a: 1.0 };
const ADD_C: Color = Color { r: 0.20, g: 0.40, b: 0.62, a: 1.0 };
fn field_style(_: &Theme, _s: text_input::Status) -> text_input::Style {
text_input::Style {
background: Background::Color(FIELD_BG),
border: Border { color: BORDER, width: 1.0, radius: 3.0.into() },
icon: WHITE,
placeholder: DIM,
value: WHITE,
selection: Color { r: 0.25, g: 0.40, b: 0.60, a: 1.0 },
fn muted_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
}
}
/// Build the alias editor content. `rows` is the live working buffer.
pub fn view_window(rows: &[(String, String)]) -> Element<'_, Message> {
let title = text("Command Aliases").size(15).color(WHITE);
let title = text("Command Aliases").size(15);
let hint = text(
"Type an alias and the command it runs (e.g. L → LINE). \
Apply to save to ocad.pgp; closing discards unapplied edits.",
)
.size(11)
.color(DIM);
.style(muted_style);
// Right gutter reserved so the scrollbar has its own lane and never sits on
// top of the row delete (✕) buttons. Applied to both the header and the
@ -53,8 +41,8 @@ pub fn view_window(rows: &[(String, String)]) -> Element<'_, Message> {
let head = container(
row![
container(text("Alias").size(11).color(DIM)).width(Length::Fixed(120.0)),
container(text("Command").size(11).color(DIM)).width(Fill),
container(text("Alias").size(11).style(muted_style)).width(Length::Fixed(120.0)),
container(text("Command").size(11).style(muted_style)).width(Fill),
Space::new().width(Length::Fixed(30.0)),
]
.spacing(8),
@ -65,31 +53,18 @@ pub fn view_window(rows: &[(String, String)]) -> Element<'_, Message> {
for (idx, (alias, cmd)) in rows.iter().enumerate() {
let alias_box = text_input("alias", alias)
.on_input(move |v| Message::AliasEditorInput { idx, field: AliasField::Alias, value: v })
.style(field_style)
.size(13)
.padding([3, 6])
.width(Length::Fixed(120.0));
let cmd_box = text_input("command", cmd)
.on_input(move |v| Message::AliasEditorInput { idx, field: AliasField::Command, value: v })
.style(field_style)
.size(13)
.padding([3, 6])
.width(Fill);
let del = button(crate::ui::icons::tinted(crate::ui::icons::CLOSE, 12.0, WHITE))
let del = button(crate::ui::icons::themed_danger(crate::ui::icons::CLOSE, 12.0))
.on_press(Message::AliasEditorRemove(idx))
.padding([2, 6])
.style(|_: &Theme, status| {
let bg = if matches!(status, button::Status::Hovered) {
Color { r: 0.45, g: 0.22, b: 0.22, a: 1.0 }
} else {
Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 }
};
button::Style {
background: Some(Background::Color(bg)),
border: Border { color: BORDER, width: 1.0, radius: 3.0.into() },
..Default::default()
}
});
.style(button::danger);
list = list.push(
row![alias_box, cmd_box, del]
.spacing(8)
@ -97,40 +72,16 @@ pub fn view_window(rows: &[(String, String)]) -> Element<'_, Message> {
);
}
let add = button(text("+ Add alias").size(12).color(WHITE))
let add = button(text("+ Add alias").size(12))
.on_press(Message::AliasEditorAdd)
.padding([4, 10])
.style(|_: &Theme, status| {
let bg = if matches!(status, button::Status::Hovered) {
Color { r: 0.30, g: 0.30, b: 0.30, a: 1.0 }
} else {
Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 }
};
button::Style {
background: Some(Background::Color(bg)),
text_color: WHITE,
border: Border { color: BORDER, width: 1.0, radius: 3.0.into() },
..Default::default()
}
});
.style(button::secondary);
// Apply — primary action; commits the rows to ocad.pgp and stays open.
let apply = button(text("Apply").size(12).color(WHITE))
let apply = button(text("Apply").size(12))
.on_press(Message::AliasEditorApply)
.padding([4, 16])
.style(|_: &Theme, status| {
let bg = if matches!(status, button::Status::Hovered | button::Status::Pressed) {
Color { r: 0.24, g: 0.46, b: 0.74, a: 1.0 }
} else {
ADD_C
};
button::Style {
background: Some(Background::Color(bg)),
text_color: WHITE,
border: Border { radius: 4.0.into(), ..Default::default() },
..Default::default()
}
});
.style(button::primary);
container(
column![
@ -149,8 +100,10 @@ pub fn view_window(rows: &[(String, String)]) -> Element<'_, Message> {
.padding(12)
.width(Fill)
.height(Fill)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(BG)),
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.extended_palette().background.base.color,
)),
..Default::default()
})
.into()

View file

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

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

View file

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

View file

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

View file

@ -1,96 +1,123 @@
use crate::app::config::UiThemeConfig;
use crate::app::Message;
use iced::widget::{button, column, container, pick_list, row, text, Space};
use iced::{Background, Border, Color, Element, Fill, Theme};
use iced::widget::{
button, column, container, pick_list, row, scrollable, text, text_input, Space,
};
use iced::{Background, Border, Element, Fill, Theme};
pub fn view_window<'a>(default_save_format: &'a str) -> Element<'a, Message> {
const BG: Color = Color {
r: 0.15,
g: 0.15,
b: 0.17,
a: 1.0,
};
const BORDER: Color = Color {
r: 0.32,
g: 0.32,
b: 0.36,
a: 1.0,
};
const TEXT: Color = Color {
r: 0.90,
g: 0.90,
b: 0.90,
a: 1.0,
};
const DIM: Color = Color {
r: 0.60,
g: 0.60,
b: 0.64,
a: 1.0,
};
let selected = crate::io::SAVE_FORMAT_OPTIONS
pub fn view_window<'a>(
default_save_format: &'a str,
ui_theme: &'a UiThemeConfig,
theme_color_inputs: &'a [String; 6],
) -> Element<'a, Message> {
let selected_format = crate::io::SAVE_FORMAT_OPTIONS
.iter()
.copied()
.find(|candidate| *candidate == default_save_format);
let close = button(text("Close").size(12).color(TEXT))
.on_press(Message::CloseModal)
.padding([5, 16])
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => Color {
r: 0.34,
g: 0.34,
b: 0.38,
a: 1.0,
},
_ => Color {
r: 0.26,
g: 0.26,
b: 0.29,
a: 1.0,
},
})),
text_color: TEXT,
border: Border {
color: BORDER,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
});
let theme_options = Theme::ALL
.iter()
.map(ToString::to_string)
.chain(std::iter::once("Custom".to_string()))
.collect::<Vec<_>>();
let selected_theme = Some(ui_theme.name.clone());
let body = column![
text("Open and Save").size(15).color(TEXT),
Space::new().height(16),
let palette = ui_theme.palette.to_iced();
let colors = [
("Background", palette.background),
("Text", palette.text),
("Primary", palette.primary),
("Success", palette.success),
("Warning", palette.warning),
("Danger", palette.danger),
];
let mut color_controls = column![].spacing(8);
for (index, (label, color)) in colors.into_iter().enumerate() {
let swatch = container(Space::new())
.width(28)
.height(22)
.style(move |theme: &Theme| container::Style {
background: Some(Background::Color(color)),
border: Border {
color: theme.extended_palette().background.strong.color,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
});
color_controls = color_controls.push(
row![
text(label).size(12).width(110),
swatch,
text_input("#RRGGBB", theme_color_inputs[index].as_str())
.on_input(move |value| Message::OptionsThemeColorChanged(index, value))
.width(130),
]
.spacing(10)
.align_y(iced::Center),
);
}
let close = button(text("Close").size(12))
.on_press(Message::CloseModal)
.padding([6, 18])
.style(button::secondary);
let content = column![
text("Open and Save").size(15),
Space::new().height(10),
row![
text("Default save format:").size(12).color(TEXT).width(150),
pick_list(crate::io::SAVE_FORMAT_OPTIONS, selected, |format: &str| {
Message::DefaultSaveFormatChanged(format.to_string())
})
text("Default save format:").size(12).width(150),
pick_list(
crate::io::SAVE_FORMAT_OPTIONS,
selected_format,
|format: &str| Message::DefaultSaveFormatChanged(format.to_string())
)
.width(Fill),
]
.spacing(12)
.align_y(iced::Center),
Space::new().height(10),
Space::new().height(8),
text(
"Used when a new drawing is saved for the first time. Existing drawings keep their current file type and version."
"Used for the first save of a new drawing. Existing drawings keep their file type and version."
)
.size(11)
.color(DIM)
.width(Fill),
Space::new().height(Fill),
row![Space::new().width(Fill), close],
.size(11),
Space::new().height(22),
text("Theme").size(15),
Space::new().height(10),
row![
text("Iced theme:").size(12).width(150),
pick_list(
theme_options,
selected_theme,
Message::OptionsThemeChanged,
)
.width(Fill),
]
.spacing(12)
.align_y(iced::Center),
Space::new().height(8),
text(
"Changing a base colour switches to Custom. Iced generates every component shade from these six colours."
)
.size(11),
Space::new().height(12),
color_controls,
]
.spacing(0)
.width(Fill);
let body = column![
scrollable(content).height(Fill),
Space::new().height(12),
row![Space::new().width(Fill), close],
]
.width(Fill)
.height(Fill);
container(body)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(BG)),
..Default::default()
})
.style(container::rounded_box)
.padding([16, 18])
.width(Fill)
.height(Fill)

View file

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

View file

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

View file

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

View file

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

View file

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