feat(ui): selectable UI themes (10 palettes + swatch picker)
The UI was hardcoded to one Catppuccin Mocha palette (color literals in view() + theme() pinned to Dark). It now sources colours from a chosen theme's palette, selectable in Settings. - theme.rs (NEW): Palette (13 semantic colour roles) + AppTheme enum (Catppuccin Mocha/Macchiato/Frappe/Latte, Dracula, Nord, Tokyo Night, Gruvbox Dark, Solarized Light, Gruvbox Light). Pure palette()/label()/ ALL/base_theme()/is_dark() + WCAG relative_luminance()/contrast_ratio(). - config: theme: AppTheme field (serde-default Mocha) + backward-compat. - app: theme() returns config.theme.base_theme() (iced widget chrome); view() + with_layout_picker() source colours from the palette; new ThemeSwatch canvas widget; a Theme section of clickable swatches in Settings; SelectTheme applies live + persists. Canvas widgets already take colours as data, so they re-theme for free. Tests written alongside (+7 theme, +2 config; 135 -> 144 lib): every palette clears WCAG AA text-on-base contrast (4.5:1) with subtext/accent >= 3:1, is_dark matches luminance direction, variants distinct/labeled, serde round-trips, default = Mocha. Verified live: the Settings swatch grid renders all 10 palettes and the whole UI re-themes (screenshot-checked Latte light + Dracula dark). clippy --all-targets clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
use crate::theme::AppTheme;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
@@ -137,6 +138,9 @@ pub struct AppConfig {
|
||||
/// Chosen arrangement of the in-call room screen.
|
||||
#[serde(default)]
|
||||
pub room_layout: RoomLayout,
|
||||
/// Chosen UI colour theme.
|
||||
#[serde(default)]
|
||||
pub theme: AppTheme,
|
||||
#[serde(default)]
|
||||
pub custom_sound_self_join: Option<String>,
|
||||
#[serde(default)]
|
||||
@@ -190,6 +194,7 @@ impl Default for AppConfig {
|
||||
controls_width: default_controls_width(),
|
||||
chat_drawer_width: default_chat_drawer_width(),
|
||||
room_layout: RoomLayout::default(),
|
||||
theme: AppTheme::default(),
|
||||
custom_sound_self_join: None,
|
||||
custom_sound_peer_join: None,
|
||||
custom_sound_peer_leave: None,
|
||||
@@ -318,6 +323,28 @@ mod tests {
|
||||
assert_eq!(back.window_y, Some(-50));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_theme_field() {
|
||||
// Default theme is Mocha (the original look).
|
||||
assert_eq!(AppConfig::default().theme, AppTheme::Mocha);
|
||||
// A chosen theme round-trips.
|
||||
let cfg = AppConfig {
|
||||
theme: AppTheme::Nord,
|
||||
..AppConfig::default()
|
||||
};
|
||||
let json = serde_json::to_string(&cfg).unwrap();
|
||||
let back: AppConfig = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back.theme, AppTheme::Nord);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_config_without_theme_loads() {
|
||||
// Configs predating the theme picker load with the default (Mocha).
|
||||
let json = r#"{"input_device":"","output_device":"","noise_gate_threshold":0.01}"#;
|
||||
let cfg: AppConfig = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(cfg.theme, AppTheme::Mocha);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_config_without_position_loads() {
|
||||
// A config written before window_x/window_y existed still deserializes
|
||||
|
||||
Reference in New Issue
Block a user