diff --git a/Cargo.lock b/Cargo.lock index 4ae17bd..9e921ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -686,6 +686,7 @@ dependencies = [ "display-info", "gtk", "iced", + "iced_anim", "interprocess", "rdev", "rfd", @@ -2247,6 +2248,27 @@ dependencies = [ "thiserror", ] +[[package]] +name = "iced_anim" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764a9667ffd3c98162cf594182d8c866e1d7231899089154476d09ec4347edf" +dependencies = [ + "iced", + "iced_anim_derive", + "serde", +] + +[[package]] +name = "iced_anim_derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b89898a5300d2800451406a3d6cfaed89ef08797dc392143f7c16c5c747e95" +dependencies = [ + "quote", + "syn 2.0.77", +] + [[package]] name = "iced_core" version = "0.13.2" diff --git a/Cargo.toml b/Cargo.toml index 5aab9b4..84d23e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ arboard = { version = "3.4", features = ["wayland-data-control", "wl-clipboard-r chrono = "0.4" display-info = { version = "0.5"} iced = { version = "0.13", features = ["advanced", "canvas", "multi-window", "image", "tokio", "svg"] } +iced_anim = { version = "0.1.1", features = ["derive", "serde"] } interprocess = { version = "2.2", features = ["tokio"] } rdev = { git = "https://github.com/rustdesk-org/rdev", branch = "master"} rfd = { version = "0.14", features = ["gtk3", "tokio"], default-features = false } diff --git a/src/entities/config.rs b/src/entities/config.rs index 46050fc..1900bab 100644 --- a/src/entities/config.rs +++ b/src/entities/config.rs @@ -1,9 +1,18 @@ +use iced_anim::{Spring, SpringEvent}; use serde::{Deserialize, Serialize}; use crate::entities::theme::Theme; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct Config { + pub theme: Spring, + pub directory: String, +} + +/// The configuration that gets serialized to disk. +/// This is distinct to avoid serializing animated values. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StoredConfig { #[serde(default)] pub theme: Theme, #[serde(default = "Config::default_path")] @@ -20,6 +29,6 @@ pub struct ConfigureWindow { pub enum ConfigEvent { UpdateFolderPath, OpenFolder, - ToggleTheme, + UpdateTheme(SpringEvent), RequestExit, } diff --git a/src/entities/theme.rs b/src/entities/theme.rs index 3c76956..4ebfc3c 100644 --- a/src/entities/theme.rs +++ b/src/entities/theme.rs @@ -1,4 +1,5 @@ use iced::Color; +use iced_anim::Animate; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] @@ -6,8 +7,11 @@ pub enum Theme { #[default] Light, Dark, + #[serde(skip)] + Custom(Palette), } +#[derive(Debug, Clone, Copy, PartialEq, Animate)] pub struct Palette { pub background: Color, pub surface: Color, diff --git a/src/main.rs b/src/main.rs index badf7e0..9531267 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ use std::collections::BTreeMap; use assets::{APPNAME, FONT_BOLD, FONT_MEDIUM, ICON, MEDIUM}; use entities::{ app::{App, AppEvent}, - config::{Config, ConfigureWindow}, + config::{Config, ConfigEvent, ConfigureWindow}, crop::CropWindow, theme::Theme, window::WindowType, @@ -28,6 +28,7 @@ use iced::{ }, Size, Subscription, Task, }; +use iced_anim::Animation; use interprocess::local_socket::{traits::Stream, GenericNamespaced, ToNsName}; use style::Element; use utils::{ @@ -189,11 +190,13 @@ impl App { None => horizontal_space().into(), }; - content + Animation::new(&self.config.theme, content) + .on_update(move |event| AppEvent::Config(id, ConfigEvent::UpdateTheme(event))) + .into() } pub fn theme(&self, _id: Id) -> Theme { - self.config.theme.clone() + self.config.theme.value().clone() } pub fn style(&self, theme: &Theme) -> Appearance { diff --git a/src/style/mod.rs b/src/style/mod.rs index 09683b7..8eca981 100644 --- a/src/style/mod.rs +++ b/src/style/mod.rs @@ -40,6 +40,15 @@ impl Theme { match self { Theme::Light => LIGHT, Theme::Dark => DARK, + Theme::Custom(palette) => *palette, + } + } + + /// Toggles the theme between light and dark, defaulting to `Light` if using a custom palette. + pub fn toggle(&self) -> Self { + match self { + Theme::Light => Theme::Dark, + _ => Theme::Light, } } } @@ -49,6 +58,7 @@ impl Display for Theme { match self { Self::Light => write!(f, "Light"), Self::Dark => write!(f, "Dark"), + Self::Custom(_) => write!(f, "Custom"), } } } @@ -61,3 +71,19 @@ impl DefaultStyle for Theme { } } } + +impl iced_anim::Animate for Theme { + fn components() -> usize { + Palette::components() + } + + fn distance_to(&self, end: &Self) -> Vec { + self.palette().distance_to(&end.palette()) + } + + fn update(&mut self, components: &mut impl Iterator) { + let mut palette = self.palette(); + palette.update(components); + *self = Theme::Custom(palette); + } +} diff --git a/src/utils/config.rs b/src/utils/config.rs index 1d83b30..47fbdc5 100644 --- a/src/utils/config.rs +++ b/src/utils/config.rs @@ -6,19 +6,17 @@ use std::{ process::Command, }; +use iced_anim::Spring; use rfd::FileDialog; -use crate::entities::{ - config::{Config, ConfigureWindow}, - theme::Theme, -}; +use crate::entities::config::{Config, ConfigureWindow, StoredConfig}; use super::shorten_path; impl Default for Config { fn default() -> Self { Self { - theme: Theme::default(), + theme: Spring::default(), directory: Config::default_path(), } } @@ -31,12 +29,12 @@ impl Config { let mut file_content = String::new(); let _ = file.read_to_string(&mut file_content).unwrap(); let bool = file_content.is_empty(); - let config = match toml::from_str::(&file_content) { - Ok(config) => config, + let config: Config = match toml::from_str::(&file_content) { + Ok(config) => config.into(), Err(_) => { let config = Self::default(); Self::update_config(&config); - config + config.into() } }; (config, bool) @@ -76,7 +74,8 @@ impl Config { match Self::get_config_file() { Ok(mut file) => { file.set_len(0).unwrap(); - let contents = toml::to_string(self).unwrap(); + let config = StoredConfig::from(self); + let contents = toml::to_string(&config).unwrap(); file.write_all(contents.as_bytes()).unwrap(); } Err(_) => println!("Config can't be updated"), @@ -106,6 +105,25 @@ impl Config { } } +// From impls to convert betwen different config types +impl From for Config { + fn from(config: StoredConfig) -> Self { + Self { + theme: Spring::new(config.theme), + directory: config.directory, + } + } +} + +impl From<&Config> for StoredConfig { + fn from(config: &Config) -> Self { + Self { + theme: config.theme.target().clone(), + directory: config.directory.clone(), + } + } +} + impl ConfigureWindow { pub fn new(config: &Config) -> Self { Self { @@ -113,12 +131,6 @@ impl ConfigureWindow { path: shorten_path(config.directory.clone()), } } - pub fn toggle_theme(&mut self) { - self.config.theme = match self.config.theme { - Theme::Light => Theme::Dark, - Theme::Dark => Theme::Light, - } - } pub fn update_directory(&mut self) { if let Some(path) = FileDialog::new() diff --git a/src/windows/config.rs b/src/windows/config.rs index d61720f..782ea27 100644 --- a/src/windows/config.rs +++ b/src/windows/config.rs @@ -31,8 +31,8 @@ impl ConfigureWindow { self.open_directory(); Task::none() } - ConfigEvent::ToggleTheme => { - self.toggle_theme(); + ConfigEvent::UpdateTheme(event) => { + self.config.theme.update(event); Task::done(AppEvent::UpdateConfig(id)) } ConfigEvent::RequestExit => Task::done(AppEvent::ExitApp), @@ -83,10 +83,16 @@ impl ConfigureWindow { row![ text("App Theme").align_x(Left).size(22).font(BOLD), horizontal_space().width(Fill), - button(text(self.config.theme.to_string()).size(20).center()) - .height(40) - .width(160) - .on_press(ConfigEvent::ToggleTheme) + button( + text(self.config.theme.target().to_string()) + .size(20) + .center() + ) + .height(40) + .width(160) + .on_press(ConfigEvent::UpdateTheme( + self.config.theme.target().toggle().into() + )) ] .align_y(Alignment::Center) .width(Fill)