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 {