feat(ui): add configurable Iced themes

This commit is contained in:
Hakan Seven committed 2026-07-28 09:33:12 +03:00
1 parent 73545520c8
commit 4bf14baea4
52 files changed
+3063 -5670

No files matched your search

+22 -70
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()
+18 -65
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()
+64 -65
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()
}
+147 -299
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,
};
+61 -175
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)
+26 -86
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)
+100 -73
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)
+82 -54
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)
+97 -137
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)
+35 -57
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)
+51 -112
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)