From 3f811ebef76e0b9ad937be34f70515fe626c21a3 Mon Sep 17 00:00:00 2001 From: pml68 Date: Mon, 24 Mar 2025 11:12:00 +0100 Subject: feat: add custom theme struct with dark and light variants --- assets/themes/dark.toml | 46 ++++++++++++++++++++++++++++++++++++++++++++++ assets/themes/light.toml | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 assets/themes/dark.toml create mode 100644 assets/themes/light.toml (limited to 'assets') diff --git a/assets/themes/dark.toml b/assets/themes/dark.toml new file mode 100644 index 0000000..a3bde68 --- /dev/null +++ b/assets/themes/dark.toml @@ -0,0 +1,46 @@ +name = "Dark" + +[primary] +primary = "#6200ee" +on_primary = "#ffffff" +primary_container = "#3700b3" +on_primary_container = "#ffffff" + +[secondary] +secondary = "#03dac6" +on_secondary = "#ffffff" +secondary_container = "#018786" +on_secondary_container = "#ffffff" + +[tertiary] +tertiary = "#bb86fc" +on_tertiary = "#000000" +tertiary_container = "#6200ee" +on_tertiary_container = "#000000" + +[error] +error = "#b00020" +on_error = "#ffffff" +error_container = "#cf6679" +on_error_container = "#000000" + +[surface] +surface = "#121212" +on_surface = "#ffffff" +on_surface_variant = "#b0b0b0" + +[surface.surface_container] +lowest = "#1e1e2e" +low = "#333333" +base = "#444444" +high = "#555555" +highest = "#666666" + +[inverse] +inverse_surface = "#212121" +inverse_on_surface = "#ffffff" +inverse_primary = "#bb86fc" + +[outline] +outline = "#737373" +outline_variant = "#aaaaaa" diff --git a/assets/themes/light.toml b/assets/themes/light.toml new file mode 100644 index 0000000..4852b8b --- /dev/null +++ b/assets/themes/light.toml @@ -0,0 +1,46 @@ +name = "Dark" + +[primary] +primary = "#6200ee" +on_primary = "#ffffff" +primary_container = "#e1bee7" +on_primary_container = "#000000" + +[secondary] +secondary = "#03dac6" +on_secondary = "#ffffff" +secondary_container = "#018786" +on_secondary_container = "#ffffff" + +[tertiary] +tertiary = "#bb86fc" +on_tertiary = "#000000" +tertiary_container = "#6200ee" +on_tertiary_container = "#000000" + +[error] +error = "#b00020" +on_error = "#ffffff" +error_container = "#cf6679" +on_error_container = "#000000" + +[surface] +surface = "#ffffff" +on_surface = "#000000" +on_surface_variant = "#757575" + +[surface.surface_container] +lowest = "#fafafa" +low = "#eeeeee" +base = "#dddddd" +high = "#cccccc" +highest = "#bbbbbb" + +[inverse] +inverse_surface = "#121212" +inverse_on_surface = "#ffffff" +inverse_primary = "#bb86fc" + +[outline] +outline = "#757575" +outline_variant = "#b0b0b0" -- cgit v1.2.3 From bbc151e82b86bf2d7114f0ea05cde7e8858ba610 Mon Sep 17 00:00:00 2001 From: pml68 Date: Sun, 30 Mar 2025 20:16:48 +0200 Subject: feat: add shadow color, impl `button::Catalog` and `text::Catalog` --- assets/themes/dark.toml | 16 ++-- assets/themes/light.toml | 16 ++-- src/main.rs | 20 +++++ src/theme.rs | 21 ++++-- src/theme/button.rs | 189 +++++++++++++++++++++++++++++++++++++++++++++++ src/theme/text.rs | 50 +++++++++++++ 6 files changed, 290 insertions(+), 22 deletions(-) create mode 100644 src/theme/button.rs create mode 100644 src/theme/text.rs (limited to 'assets') diff --git a/assets/themes/dark.toml b/assets/themes/dark.toml index a3bde68..3c02ce8 100644 --- a/assets/themes/dark.toml +++ b/assets/themes/dark.toml @@ -1,31 +1,33 @@ name = "Dark" +shadow = "#000000" + [primary] -primary = "#6200ee" +color = "#6200ee" on_primary = "#ffffff" primary_container = "#3700b3" on_primary_container = "#ffffff" [secondary] -secondary = "#03dac6" +color = "#03dac6" on_secondary = "#ffffff" secondary_container = "#018786" on_secondary_container = "#ffffff" [tertiary] -tertiary = "#bb86fc" +color = "#bb86fc" on_tertiary = "#000000" tertiary_container = "#6200ee" on_tertiary_container = "#000000" [error] -error = "#b00020" +color = "#b00020" on_error = "#ffffff" error_container = "#cf6679" on_error_container = "#000000" [surface] -surface = "#121212" +color = "#121212" on_surface = "#ffffff" on_surface_variant = "#b0b0b0" @@ -42,5 +44,5 @@ inverse_on_surface = "#ffffff" inverse_primary = "#bb86fc" [outline] -outline = "#737373" -outline_variant = "#aaaaaa" +color = "#737373" +variant = "#aaaaaa" diff --git a/assets/themes/light.toml b/assets/themes/light.toml index 4852b8b..2842c82 100644 --- a/assets/themes/light.toml +++ b/assets/themes/light.toml @@ -1,31 +1,33 @@ name = "Dark" +shadow = "#000000" + [primary] -primary = "#6200ee" +color = "#6200ee" on_primary = "#ffffff" primary_container = "#e1bee7" on_primary_container = "#000000" [secondary] -secondary = "#03dac6" +color = "#03dac6" on_secondary = "#ffffff" secondary_container = "#018786" on_secondary_container = "#ffffff" [tertiary] -tertiary = "#bb86fc" +color = "#bb86fc" on_tertiary = "#000000" tertiary_container = "#6200ee" on_tertiary_container = "#000000" [error] -error = "#b00020" +color = "#b00020" on_error = "#ffffff" error_container = "#cf6679" on_error_container = "#000000" [surface] -surface = "#ffffff" +color = "#ffffff" on_surface = "#000000" on_surface_variant = "#757575" @@ -42,5 +44,5 @@ inverse_on_surface = "#ffffff" inverse_primary = "#bb86fc" [outline] -outline = "#757575" -outline_variant = "#b0b0b0" +color = "#757575" +variant = "#b0b0b0" diff --git a/src/main.rs b/src/main.rs index c8a60c6..7728e63 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,6 +35,8 @@ use types::{ Project, }; +//pub type Element<'a, Message> = iced::Element<'a, Message, OtherTheme>; + fn main() -> Result<(), Box> { let version = std::env::args() .nth(1) @@ -58,7 +60,9 @@ fn main() -> Result<(), Box> { iced::application(App::title, App::update, App::view) .font(icon::FONT) .theme(|state| state.theme.value().clone()) + //.theme(|_| theme::LIGHT.clone()) .subscription(App::subscription) + .antialiasing(true) .run_with(move || App::new(config_load))?; Ok(()) } @@ -445,5 +449,21 @@ impl App { Animation::new(&self.theme, content) .on_update(Message::SwitchTheme) .into() + //row![ + // button("filled") + // .style(theme::button::filled) + // .on_press(Message::RefreshEditorContent), + // button("elevated") + // .style(theme::button::elevated) + // .on_press(Message::RefreshEditorContent), + // button("filled tonal") + // .style(theme::button::filled_tonal) + // .on_press(Message::RefreshEditorContent), + // button("outlined") + // .style(theme::button::outlined) + // .on_press(Message::RefreshEditorContent), + //] + //.spacing(10) + //.into() } } diff --git a/src/theme.rs b/src/theme.rs index 3e6092b..6fe844c 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1,3 +1,6 @@ +pub mod button; +pub mod text; + use std::sync::{Arc, LazyLock}; use iced::Color; @@ -115,7 +118,7 @@ impl Default for OtherTheme { impl Base for OtherTheme { fn base(&self) -> iced::theme::Style { iced::theme::Style { - background_color: self.colorscheme.surface.surface, + background_color: self.colorscheme.surface.color, text_color: self.colorscheme.surface.on_surface, } } @@ -130,6 +133,8 @@ pub struct ColorScheme { pub surface: Surface, pub inverse: Inverse, pub outline: Outline, + #[serde(with = "color_serde")] + pub shadow: Color, } pub static DARK: LazyLock = LazyLock::new(|| { @@ -143,7 +148,7 @@ pub static LIGHT: LazyLock = LazyLock::new(|| { #[derive(Debug, Clone, Copy, PartialEq, Deserialize)] pub struct Primary { #[serde(with = "color_serde")] - pub primary: Color, + pub color: Color, #[serde(with = "color_serde")] pub on_primary: Color, #[serde(with = "color_serde")] @@ -155,7 +160,7 @@ pub struct Primary { #[derive(Debug, Clone, Copy, PartialEq, Deserialize)] pub struct Secondary { #[serde(with = "color_serde")] - pub secondary: Color, + pub color: Color, #[serde(with = "color_serde")] pub on_secondary: Color, #[serde(with = "color_serde")] @@ -167,7 +172,7 @@ pub struct Secondary { #[derive(Debug, Clone, Copy, PartialEq, Deserialize)] pub struct Tertiary { #[serde(with = "color_serde")] - pub tertiary: Color, + pub color: Color, #[serde(with = "color_serde")] pub on_tertiary: Color, #[serde(with = "color_serde")] @@ -179,7 +184,7 @@ pub struct Tertiary { #[derive(Debug, Clone, Copy, PartialEq, Deserialize)] pub struct Error { #[serde(with = "color_serde")] - pub error: Color, + pub color: Color, #[serde(with = "color_serde")] pub on_error: Color, #[serde(with = "color_serde")] @@ -191,7 +196,7 @@ pub struct Error { #[derive(Debug, Clone, Copy, PartialEq, Deserialize)] pub struct Surface { #[serde(with = "color_serde")] - pub surface: Color, + pub color: Color, #[serde(with = "color_serde")] pub on_surface: Color, #[serde(with = "color_serde")] @@ -226,9 +231,9 @@ pub struct Inverse { #[derive(Debug, Clone, Copy, PartialEq, Deserialize)] pub struct Outline { #[serde(with = "color_serde")] - pub outline: Color, + pub color: Color, #[serde(with = "color_serde")] - pub outline_variant: Color, + pub variant: Color, } #[derive(Debug, Deserialize)] diff --git a/src/theme/button.rs b/src/theme/button.rs new file mode 100644 index 0000000..ddd2c71 --- /dev/null +++ b/src/theme/button.rs @@ -0,0 +1,189 @@ +#![allow(dead_code)] +use iced::widget::button::{Catalog, Status, Style, StyleFn}; +use iced::{Background, Border, Color, Shadow, Vector}; + +use super::OtherTheme; + +impl Catalog for OtherTheme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(default) + } + + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + class(self, status) + } +} + +fn default(theme: &OtherTheme, status: Status) -> Style { + filled(theme, status) +} + +fn button( + foreground: Color, + background: Color, + background_hover: Color, + disabled: Color, + shadow_color: Color, + shadow_elevation: u8, + status: Status, +) -> Style { + let border = Border { + radius: 400.0.into(), + ..Default::default() + }; + + let elevation_to_offset = |elevation: u8| { + (match elevation { + 0 => 0.0, + 1 => 1.0, + 2 => 3.0, + 3 => 6.0, + 4 => 8.0, + _ => 12.0, + } as f32) + }; + + match status { + Status::Active | Status::Pressed => Style { + background: Some(Background::Color(background)), + text_color: foreground, + border, + shadow: Shadow { + color: shadow_color, + offset: Vector { + x: 0.0, + y: elevation_to_offset(shadow_elevation), + }, + blur_radius: elevation_to_offset(shadow_elevation) + * (1.0 + + 0.4_f32.powf(elevation_to_offset(shadow_elevation))), + }, + }, + Status::Hovered => Style { + background: Some(Background::Color(background_hover)), + text_color: foreground, + border, + shadow: Shadow { + color: shadow_color, + offset: Vector { + x: 0.0, + y: elevation_to_offset(shadow_elevation + 1), + }, + blur_radius: (elevation_to_offset(shadow_elevation + 1)) + * (1.0 + + 0.4_f32 + .powf(elevation_to_offset(shadow_elevation + 1))), + }, + }, + Status::Disabled => Style { + background: Some(Background::Color(Color { + a: 0.12, + ..disabled + })), + text_color: Color { + a: 0.38, + ..disabled + }, + border, + ..Default::default() + }, + } +} + +pub fn elevated(theme: &OtherTheme, status: Status) -> Style { + let surface_colors = theme.colorscheme.surface; + + let foreground = theme.colorscheme.primary.color; + let background = surface_colors.surface_container.low; + let disabled = surface_colors.on_surface; + + let shadow_color = theme.colorscheme.shadow; + + button( + foreground, + background, + background, + disabled, + shadow_color, + 1, + status, + ) +} + +pub fn filled(theme: &OtherTheme, status: Status) -> Style { + let primary_colors = theme.colorscheme.primary; + + let foreground = primary_colors.on_primary; + let background = primary_colors.color; + let disabled = theme.colorscheme.surface.on_surface; + + let shadow_color = theme.colorscheme.shadow; + + button( + foreground, + background, + background, + disabled, + shadow_color, + 0, + status, + ) +} + +pub fn filled_tonal(theme: &OtherTheme, status: Status) -> Style { + let secondary_colors = theme.colorscheme.secondary; + + let foreground = secondary_colors.on_secondary_container; + let background = secondary_colors.secondary_container; + let disabled = theme.colorscheme.surface.on_surface; + + let shadow_color = theme.colorscheme.shadow; + + button( + foreground, + background, + background, + disabled, + shadow_color, + 0, + status, + ) +} + +pub fn outlined(theme: &OtherTheme, status: Status) -> Style { + let foreground = theme.colorscheme.primary.color; + let background = Color::TRANSPARENT; + let disabled = theme.colorscheme.surface.on_surface; + + let outline = theme.colorscheme.outline.color; + + let border = match status { + Status::Active | Status::Pressed | Status::Hovered => Border { + color: outline, + width: 1.0, + radius: 400.0.into(), + }, + Status::Disabled => Border { + color: Color { + a: 0.12, + ..disabled + }, + width: 1.0, + radius: 400.0.into(), + }, + }; + + let style = button( + foreground, + background, + background, + disabled, + Color::TRANSPARENT, + 0, + status, + ); + + Style { border, ..style } +} diff --git a/src/theme/text.rs b/src/theme/text.rs new file mode 100644 index 0000000..9cbd056 --- /dev/null +++ b/src/theme/text.rs @@ -0,0 +1,50 @@ +#![allow(dead_code)] +use iced::widget::text::{Catalog, Style, StyleFn}; + +use super::OtherTheme; + +impl Catalog for OtherTheme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(none) + } + + fn style(&self, class: &Self::Class<'_>) -> Style { + class(self) + } +} + +pub fn none(_: &OtherTheme) -> Style { + Style { color: None } +} + +pub fn primary(theme: &OtherTheme) -> Style { + Style { + color: Some(theme.colorscheme.primary.on_primary), + } +} + +pub fn secondary(theme: &OtherTheme) -> Style { + Style { + color: Some(theme.colorscheme.secondary.on_secondary), + } +} + +pub fn tertiary(theme: &OtherTheme) -> Style { + Style { + color: Some(theme.colorscheme.tertiary.on_tertiary), + } +} + +pub fn error(theme: &OtherTheme) -> Style { + Style { + color: Some(theme.colorscheme.error.on_error), + } +} + +pub fn surface(theme: &OtherTheme) -> Style { + Style { + color: Some(theme.colorscheme.surface.on_surface), + } +} -- cgit v1.2.3 From e9af14434454e8512e99612271b557789f28deeb Mon Sep 17 00:00:00 2001 From: pml68 Date: Mon, 7 Apr 2025 02:05:39 +0200 Subject: refactor: move custom theme into its separate crate --- Cargo.lock | 20 ++- Cargo.toml | 27 ++-- assets/themes/dark.toml | 48 ------- assets/themes/light.toml | 48 ------- material_theme/Cargo.toml | 40 ++++++ material_theme/assets/themes/dark.toml | 48 +++++++ material_theme/assets/themes/light.toml | 48 +++++++ material_theme/src/button.rs | 209 +++++++++++++++++++++++++++++ material_theme/src/container.rs | 173 ++++++++++++++++++++++++ material_theme/src/lib.rs | 229 ++++++++++++++++++++++++++++++++ material_theme/src/text.rs | 86 ++++++++++++ material_theme/src/utils.rs | 61 +++++++++ src/main.rs | 19 --- src/theme.rs | 162 +--------------------- src/theme/button.rs | 189 -------------------------- src/theme/text.rs | 50 ------- theme_test/Cargo.toml | 8 ++ theme_test/src/main.rs | 89 +++++++++++++ 18 files changed, 1029 insertions(+), 525 deletions(-) delete mode 100644 assets/themes/dark.toml delete mode 100644 assets/themes/light.toml create mode 100644 material_theme/Cargo.toml create mode 100644 material_theme/assets/themes/dark.toml create mode 100644 material_theme/assets/themes/light.toml create mode 100644 material_theme/src/button.rs create mode 100644 material_theme/src/container.rs create mode 100644 material_theme/src/lib.rs create mode 100644 material_theme/src/text.rs create mode 100644 material_theme/src/utils.rs delete mode 100644 src/theme/button.rs delete mode 100644 src/theme/text.rs create mode 100644 theme_test/Cargo.toml create mode 100644 theme_test/src/main.rs (limited to 'assets') diff --git a/Cargo.lock b/Cargo.lock index e92c3fc..2dc8556 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1971,7 +1971,6 @@ dependencies = [ name = "iced_builder" version = "0.1.0" dependencies = [ - "dark-light", "dirs-next", "embed-resource 3.0.2", "fxhash", @@ -1981,6 +1980,7 @@ dependencies = [ "iced_dialog", "iced_drop", "iced_fontello", + "material_theme", "rfd", "rust-format", "serde", @@ -2673,6 +2673,16 @@ dependencies = [ "libc", ] +[[package]] +name = "material_theme" +version = "0.14.0-dev" +dependencies = [ + "dark-light", + "iced_widget", + "serde", + "toml", +] + [[package]] name = "maybe-rayon" version = "0.1.1" @@ -4634,6 +4644,14 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "theme_test" +version = "0.0.1" +dependencies = [ + "iced", + "material_theme", +] + [[package]] name = "thiserror" version = "1.0.69" diff --git a/Cargo.toml b/Cargo.toml index 39e69af..33d3b8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,23 +16,28 @@ debug = ["iced/debug"] [dependencies] iced.workspace = true -iced_anim = { git = "https://github.com/pml68/iced_anim", features = ["derive"] } +iced_anim.workspace = true iced_custom_highlighter = { git = "https://github.com/pml68/iced_custom_highlighter", branch = "master" } iced_drop = { path = "iced_drop" } iced_dialog = { git = "https://github.com/pml68/iced_dialog", branch = "iced/personal" } -serde = { version = "1.0.217", features = ["derive"] } -serde_json = "1.0.138" -toml = "0.8.20" -tokio = { version = "1.43", features = ["fs"] } +material_theme = { path = "material_theme" } +serde.workspace = true +serde_json = "1.0.140" +toml.workspace = true +tokio = { version = "1.42.1", features = ["fs"] } tokio-stream = { version = "0.1", features = ["fs"] } # TODO: enable tokio when it actually compiles # rfd = { version = "0.15.2", default-features = false, features = ["tokio", "xdg-portal"] } rfd = "0.15.3" rust-format = "0.3.4" fxhash = "0.2.1" -thiserror = "2.0.11" +thiserror = "2.0.12" dirs-next = "2.0.0" -dark-light = "2.0.0" + +[workspace.dependencies] +iced_anim = { version = "0.2.1", features = ["derive"] } +serde = { version = "1.0.219", features = ["derive"] } +toml = "0.8.20" [workspace.dependencies.iced] git = "https://github.com/pml68/iced" @@ -46,7 +51,7 @@ iced_fontello = "0.13.2" xdg = "2.5.2" [target.'cfg(windows)'.build-dependencies] -embed-resource = "3.0.1" +embed-resource = "3.0.2" windows_exe_info = "0.5" [profile.dev] @@ -68,7 +73,7 @@ name = "iced-builder" path = "src/main.rs" [workspace] -members = ["iced_drop"] +members = ["iced_drop", "material_theme", "theme_test"] [lints.rust] missing_debug_implementations = "deny" @@ -89,3 +94,7 @@ from_over_into = "deny" needless_borrow = "deny" new_without_default = "deny" useless_conversion = "deny" + +[patch.crates-io] +iced_anim = { git = "https://github.com/pml68/iced_anim" } +iced_widget = { git = "https://github.com/pml68/iced", branch = "feat/rehighlight-on-redraw" } diff --git a/assets/themes/dark.toml b/assets/themes/dark.toml deleted file mode 100644 index 3c02ce8..0000000 --- a/assets/themes/dark.toml +++ /dev/null @@ -1,48 +0,0 @@ -name = "Dark" - -shadow = "#000000" - -[primary] -color = "#6200ee" -on_primary = "#ffffff" -primary_container = "#3700b3" -on_primary_container = "#ffffff" - -[secondary] -color = "#03dac6" -on_secondary = "#ffffff" -secondary_container = "#018786" -on_secondary_container = "#ffffff" - -[tertiary] -color = "#bb86fc" -on_tertiary = "#000000" -tertiary_container = "#6200ee" -on_tertiary_container = "#000000" - -[error] -color = "#b00020" -on_error = "#ffffff" -error_container = "#cf6679" -on_error_container = "#000000" - -[surface] -color = "#121212" -on_surface = "#ffffff" -on_surface_variant = "#b0b0b0" - -[surface.surface_container] -lowest = "#1e1e2e" -low = "#333333" -base = "#444444" -high = "#555555" -highest = "#666666" - -[inverse] -inverse_surface = "#212121" -inverse_on_surface = "#ffffff" -inverse_primary = "#bb86fc" - -[outline] -color = "#737373" -variant = "#aaaaaa" diff --git a/assets/themes/light.toml b/assets/themes/light.toml deleted file mode 100644 index 2842c82..0000000 --- a/assets/themes/light.toml +++ /dev/null @@ -1,48 +0,0 @@ -name = "Dark" - -shadow = "#000000" - -[primary] -color = "#6200ee" -on_primary = "#ffffff" -primary_container = "#e1bee7" -on_primary_container = "#000000" - -[secondary] -color = "#03dac6" -on_secondary = "#ffffff" -secondary_container = "#018786" -on_secondary_container = "#ffffff" - -[tertiary] -color = "#bb86fc" -on_tertiary = "#000000" -tertiary_container = "#6200ee" -on_tertiary_container = "#000000" - -[error] -color = "#b00020" -on_error = "#ffffff" -error_container = "#cf6679" -on_error_container = "#000000" - -[surface] -color = "#ffffff" -on_surface = "#000000" -on_surface_variant = "#757575" - -[surface.surface_container] -lowest = "#fafafa" -low = "#eeeeee" -base = "#dddddd" -high = "#cccccc" -highest = "#bbbbbb" - -[inverse] -inverse_surface = "#121212" -inverse_on_surface = "#ffffff" -inverse_primary = "#bb86fc" - -[outline] -color = "#757575" -variant = "#b0b0b0" diff --git a/material_theme/Cargo.toml b/material_theme/Cargo.toml new file mode 100644 index 0000000..0597d78 --- /dev/null +++ b/material_theme/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "material_theme" +description = "An M3 inspired theme for `iced`" +authors = ["pml68 "] +version = "0.14.0-dev" +edition = "2024" +license = "MIT" +# readme = "README.md" +repository = "https://github.com/pml68/iced_builder" +categories = ["gui"] +keywords = ["gui", "ui", "graphics", "interface", "widgets"] +rust-version = "1.85" + +[dependencies] +iced_widget = "0.14.0-dev" +serde.workspace = true +toml.workspace = true +dark-light = "2.0.0" + +[lints.rust] +unsafe_code = "deny" +unused_results = "deny" + +[lints.clippy] +type-complexity = "allow" +semicolon_if_nothing_returned = "deny" +trivially-copy-pass-by-ref = "deny" +default_trait_access = "deny" +match-wildcard-for-single-variants = "deny" +redundant-closure-for-method-calls = "deny" +filter_map_next = "deny" +manual_let_else = "deny" +unused_async = "deny" +from_over_into = "deny" +needless_borrow = "deny" +new_without_default = "deny" +useless_conversion = "deny" + +[lints.rustdoc] +broken_intra_doc_links = "forbid" diff --git a/material_theme/assets/themes/dark.toml b/material_theme/assets/themes/dark.toml new file mode 100644 index 0000000..4d23fc8 --- /dev/null +++ b/material_theme/assets/themes/dark.toml @@ -0,0 +1,48 @@ +name = "Dark" + +shadow = "#000000" + +[primary] +color = "#9bd4a1" +on_primary = "#003916" +primary_container = "#1b5129" +on_primary_container = "#b6f1bb" + +[secondary] +color = "#b8ccb6" +on_secondary = "#233425" +secondary_container = "#394b3a" +on_secondary_container = "#d3e8d1" + +[tertiary] +color = "#a1ced7" +on_tertiary = "#00363e" +tertiary_container = "#1f4d55" +on_tertiary_container = "#bdeaf4" + +[error] +color = "#ffb4ab" +on_error = "#690005" +error_container = "#93000a" +on_error_container = "#ffdad6" + +[surface] +color = "#101510" +on_surface = "#e0e4dc" +on_surface_variant = "#c1c9be" + +[surface.surface_container] +lowest = "#0b0f0b" +low = "#181d18" +base = "#1c211c" +high = "#262b26" +highest = "#313631" + +[inverse] +inverse_surface = "#e0e4dc" +inverse_on_surface = "#2d322c" +inverse_primary = "#34693f" + +[outline] +color = "#8b9389" +variant = "#414941" diff --git a/material_theme/assets/themes/light.toml b/material_theme/assets/themes/light.toml new file mode 100644 index 0000000..5288c84 --- /dev/null +++ b/material_theme/assets/themes/light.toml @@ -0,0 +1,48 @@ +name = "Light" + +shadow = "#000000" + +[primary] +color = "#34693f" +on_primary = "#ffffff" +primary_container = "#b6f1bb" +on_primary_container = "#1b5129" + +[secondary] +color = "#516351" +on_secondary = "#ffffff" +secondary_container = "#d3e8d1" +on_secondary_container = "#394b3a" + +[tertiary] +color = "#39656d" +on_tertiary = "#ffffff" +tertiary_container = "#bdeaf4" +on_tertiary_container = "#1f4d55" + +[error] +color = "#ba1a1a" +on_error = "#ffffff" +error_container = "#ffdad6" +on_error_container = "#93000a" + +[surface] +color = "#f7fbf2" +on_surface = "#181d18" +on_surface_variant = "#414941" + +[surface.surface_container] +lowest = "#ffffff" +low = "#f1f5ed" +base = "#ebefe7" +high = "#e5e9e1" +highest = "#e0e4dc" + +[inverse] +inverse_surface = "#2d322c" +inverse_on_surface = "#eef2ea" +inverse_primary = "#9bd4a1" + +[outline] +color = "#727970" +variant = "#c1c9be" diff --git a/material_theme/src/button.rs b/material_theme/src/button.rs new file mode 100644 index 0000000..051d6c9 --- /dev/null +++ b/material_theme/src/button.rs @@ -0,0 +1,209 @@ +#![allow(dead_code)] +use iced_widget::button::{Catalog, Status, Style, StyleFn}; +use iced_widget::core::{Background, Border, Color, Shadow, Vector}; + +use crate::Theme; +use crate::utils::{elevation, mix}; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(filled) + } + + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + class(self, status) + } +} + +fn button( + foreground: Color, + background: Color, + tone_overlay: Color, + disabled: Color, + shadow_color: Color, + shadow_elevation: u8, + status: Status, +) -> Style { + let border = Border { + radius: 400.into(), + ..Default::default() + }; + + let active = Style { + background: Some(Background::Color(background)), + text_color: foreground, + border, + shadow: Shadow { + color: shadow_color, + offset: Vector { + x: 0.0, + y: elevation(shadow_elevation), + }, + blur_radius: elevation(shadow_elevation) + * (1.0 + 0.4_f32.powf(elevation(shadow_elevation))), + }, + }; + + match status { + Status::Active => active, + Status::Pressed => Style { + background: Some(Background::Color(mix( + background, + tone_overlay, + 0.08, + ))), + ..active + }, + Status::Hovered => Style { + background: Some(Background::Color(mix( + background, + tone_overlay, + 0.1, + ))), + text_color: foreground, + border, + shadow: Shadow { + color: shadow_color, + offset: Vector { + x: 0.0, + y: elevation(shadow_elevation + 1), + }, + blur_radius: (elevation(shadow_elevation + 1)) + * (1.0 + 0.4_f32.powf(elevation(shadow_elevation + 1))), + }, + }, + Status::Disabled => Style { + background: Some(Background::Color(Color { + a: 0.12, + ..disabled + })), + text_color: Color { + a: 0.38, + ..disabled + }, + border, + ..Default::default() + }, + } +} + +pub fn elevated(theme: &Theme, status: Status) -> Style { + let surface_colors = theme.colorscheme.surface; + + let foreground = theme.colorscheme.primary.color; + let background = surface_colors.surface_container.low; + let disabled = surface_colors.on_surface; + + let shadow_color = theme.colorscheme.shadow; + + button( + foreground, + background, + foreground, + disabled, + shadow_color, + 1, + status, + ) +} + +pub fn filled(theme: &Theme, status: Status) -> Style { + let primary_colors = theme.colorscheme.primary; + + let foreground = primary_colors.on_primary; + let background = primary_colors.color; + let disabled = theme.colorscheme.surface.on_surface; + + let shadow_color = theme.colorscheme.shadow; + + button( + foreground, + background, + foreground, + disabled, + shadow_color, + 0, + status, + ) +} + +pub fn filled_tonal(theme: &Theme, status: Status) -> Style { + let secondary_colors = theme.colorscheme.secondary; + + let foreground = secondary_colors.on_secondary_container; + let background = secondary_colors.secondary_container; + let disabled = theme.colorscheme.surface.on_surface; + let shadow_color = theme.colorscheme.shadow; + + button( + foreground, + background, + foreground, + disabled, + shadow_color, + 0, + status, + ) +} + +pub fn outlined(theme: &Theme, status: Status) -> Style { + let foreground = theme.colorscheme.primary.color; + let background = Color::TRANSPARENT; + let disabled = theme.colorscheme.surface.on_surface; + + let outline = theme.colorscheme.outline.color; + + let border = match status { + Status::Active | Status::Pressed | Status::Hovered => Border { + color: outline, + width: 1.0, + radius: 400.0.into(), + }, + Status::Disabled => Border { + color: Color { + a: 0.12, + ..disabled + }, + width: 1.0, + radius: 400.0.into(), + }, + }; + + let style = button( + foreground, + background, + foreground, + disabled, + Color::TRANSPARENT, + 0, + status, + ); + + Style { border, ..style } +} + +pub fn text(theme: &Theme, status: Status) -> Style { + let foreground = theme.colorscheme.primary.color; + let background = Color::TRANSPARENT; + let disabled = theme.colorscheme.surface.on_surface; + + let style = button( + foreground, + background, + foreground, + disabled, + Color::TRANSPARENT, + 0, + status, + ); + + match status { + Status::Hovered | Status::Pressed => style, + _ => Style { + background: None, + ..style + }, + } +} diff --git a/material_theme/src/container.rs b/material_theme/src/container.rs new file mode 100644 index 0000000..a14cfd5 --- /dev/null +++ b/material_theme/src/container.rs @@ -0,0 +1,173 @@ +use iced_widget::container::{Catalog, Style, StyleFn}; +use iced_widget::core::{Background, border}; + +use super::Theme; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(transparent) + } + + fn style(&self, class: &Self::Class<'_>) -> Style { + class(self) + } +} + +pub fn transparent(_theme: &Theme) -> Style { + Style { + border: border::rounded(4), + ..Style::default() + } +} + +pub fn primary(theme: &Theme) -> Style { + let colors = theme.colorscheme.primary; + Style { + background: Some(Background::Color(colors.color)), + text_color: Some(colors.on_primary), + border: border::rounded(4), + ..Style::default() + } +} + +pub fn primary_container(theme: &Theme) -> Style { + let colors = theme.colorscheme.primary; + Style { + background: Some(Background::Color(colors.primary_container)), + text_color: Some(colors.on_primary_container), + border: border::rounded(8), + ..Style::default() + } +} + +pub fn secondary(theme: &Theme) -> Style { + let colors = theme.colorscheme.secondary; + Style { + background: Some(Background::Color(colors.color)), + text_color: Some(colors.on_secondary), + border: border::rounded(4), + ..Style::default() + } +} + +pub fn secondary_container(theme: &Theme) -> Style { + let colors = theme.colorscheme.secondary; + Style { + background: Some(Background::Color(colors.secondary_container)), + text_color: Some(colors.on_secondary_container), + border: border::rounded(8), + ..Style::default() + } +} + +pub fn tertiary(theme: &Theme) -> Style { + let colors = theme.colorscheme.tertiary; + Style { + background: Some(Background::Color(colors.color)), + text_color: Some(colors.on_tertiary), + border: border::rounded(4), + ..Style::default() + } +} + +pub fn tertiary_container(theme: &Theme) -> Style { + let colors = theme.colorscheme.tertiary; + Style { + background: Some(Background::Color(colors.tertiary_container)), + text_color: Some(colors.on_tertiary_container), + border: border::rounded(8), + ..Style::default() + } +} + +pub fn error(theme: &Theme) -> Style { + let colors = theme.colorscheme.error; + Style { + background: Some(Background::Color(colors.color)), + text_color: Some(colors.on_error), + border: border::rounded(4), + ..Style::default() + } +} + +pub fn error_container(theme: &Theme) -> Style { + let colors = theme.colorscheme.error; + Style { + background: Some(Background::Color(colors.error_container)), + text_color: Some(colors.on_error_container), + border: border::rounded(8), + ..Style::default() + } +} + +pub fn surface(theme: &Theme) -> Style { + let colors = theme.colorscheme.surface; + Style { + background: Some(Background::Color(colors.color)), + text_color: Some(colors.on_surface), + border: border::rounded(4), + ..Style::default() + } +} + +pub fn surface_container_lowest(theme: &Theme) -> Style { + let colors = theme.colorscheme.surface; + Style { + background: Some(Background::Color(colors.surface_container.lowest)), + text_color: Some(colors.on_surface), + border: border::rounded(8), + ..Style::default() + } +} + +pub fn surface_container_low(theme: &Theme) -> Style { + let colors = theme.colorscheme.surface; + Style { + background: Some(Background::Color(colors.surface_container.low)), + text_color: Some(colors.on_surface), + border: border::rounded(8), + ..Style::default() + } +} + +pub fn surface_container(theme: &Theme) -> Style { + let colors = theme.colorscheme.surface; + Style { + background: Some(Background::Color(colors.surface_container.base)), + text_color: Some(colors.on_surface), + border: border::rounded(8), + ..Style::default() + } +} + +pub fn surface_container_high(theme: &Theme) -> Style { + let colors = theme.colorscheme.surface; + Style { + background: Some(Background::Color(colors.surface_container.high)), + text_color: Some(colors.on_surface), + border: border::rounded(8), + ..Style::default() + } +} + +pub fn surface_container_highest(theme: &Theme) -> Style { + let colors = theme.colorscheme.surface; + Style { + background: Some(Background::Color(colors.surface_container.highest)), + text_color: Some(colors.on_surface), + border: border::rounded(8), + ..Style::default() + } +} + +pub fn inverse_surface(theme: &Theme) -> Style { + let colors = theme.colorscheme.inverse; + Style { + background: Some(Background::Color(colors.inverse_surface)), + text_color: Some(colors.inverse_on_surface), + border: border::rounded(4), + ..Style::default() + } +} diff --git a/material_theme/src/lib.rs b/material_theme/src/lib.rs new file mode 100644 index 0000000..6641b74 --- /dev/null +++ b/material_theme/src/lib.rs @@ -0,0 +1,229 @@ +use std::sync::LazyLock; + +use iced_widget::core::Color; +use iced_widget::core::theme::{Base, Style}; +use serde::Deserialize; + +pub mod button; +pub mod container; +pub mod text; +pub mod utils; + +const DARK_THEME_CONTENT: &str = include_str!("../assets/themes/dark.toml"); +const LIGHT_THEME_CONTENT: &str = include_str!("../assets/themes/light.toml"); + +#[derive(Debug, PartialEq, Deserialize)] +pub struct Theme { + pub name: String, + #[serde(flatten)] + pub colorscheme: ColorScheme, +} + +impl Theme { + pub fn new(name: impl Into, colorscheme: ColorScheme) -> Self { + Self { + name: name.into(), + colorscheme, + } + } +} + +impl Clone for Theme { + fn clone(&self) -> Self { + Self { + name: self.name.clone(), + colorscheme: self.colorscheme, + } + } + + fn clone_from(&mut self, source: &Self) { + self.name = source.name.clone(); + self.colorscheme = source.colorscheme; + } +} + +impl Default for Theme { + fn default() -> Self { + match dark_light::detect().unwrap_or(dark_light::Mode::Unspecified) { + dark_light::Mode::Dark | dark_light::Mode::Unspecified => { + DARK.clone() + } + dark_light::Mode::Light => LIGHT.clone(), + } + } +} + +impl Base for Theme { + fn base(&self) -> Style { + Style { + background_color: self.colorscheme.surface.color, + text_color: self.colorscheme.surface.on_surface, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +pub struct ColorScheme { + pub primary: Primary, + pub secondary: Secondary, + pub tertiary: Tertiary, + pub error: Error, + pub surface: Surface, + pub inverse: Inverse, + pub outline: Outline, + #[serde(with = "color_serde")] + pub shadow: Color, +} + +pub static DARK: LazyLock = LazyLock::new(|| { + toml::from_str(DARK_THEME_CONTENT).expect("parse dark theme") +}); + +pub static LIGHT: LazyLock = LazyLock::new(|| { + toml::from_str(LIGHT_THEME_CONTENT).expect("parse light theme") +}); + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +pub struct Primary { + #[serde(with = "color_serde")] + pub color: Color, + #[serde(with = "color_serde")] + pub on_primary: Color, + #[serde(with = "color_serde")] + pub primary_container: Color, + #[serde(with = "color_serde")] + pub on_primary_container: Color, +} + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +pub struct Secondary { + #[serde(with = "color_serde")] + pub color: Color, + #[serde(with = "color_serde")] + pub on_secondary: Color, + #[serde(with = "color_serde")] + pub secondary_container: Color, + #[serde(with = "color_serde")] + pub on_secondary_container: Color, +} + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +pub struct Tertiary { + #[serde(with = "color_serde")] + pub color: Color, + #[serde(with = "color_serde")] + pub on_tertiary: Color, + #[serde(with = "color_serde")] + pub tertiary_container: Color, + #[serde(with = "color_serde")] + pub on_tertiary_container: Color, +} + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +pub struct Error { + #[serde(with = "color_serde")] + pub color: Color, + #[serde(with = "color_serde")] + pub on_error: Color, + #[serde(with = "color_serde")] + pub error_container: Color, + #[serde(with = "color_serde")] + pub on_error_container: Color, +} + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +pub struct Surface { + #[serde(with = "color_serde")] + pub color: Color, + #[serde(with = "color_serde")] + pub on_surface: Color, + #[serde(with = "color_serde")] + pub on_surface_variant: Color, + pub surface_container: SurfaceContainer, +} + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +pub struct SurfaceContainer { + #[serde(with = "color_serde")] + pub lowest: Color, + #[serde(with = "color_serde")] + pub low: Color, + #[serde(with = "color_serde")] + pub base: Color, + #[serde(with = "color_serde")] + pub high: Color, + #[serde(with = "color_serde")] + pub highest: Color, +} + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +pub struct Inverse { + #[serde(with = "color_serde")] + pub inverse_surface: Color, + #[serde(with = "color_serde")] + pub inverse_on_surface: Color, + #[serde(with = "color_serde")] + pub inverse_primary: Color, +} + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +pub struct Outline { + #[serde(with = "color_serde")] + pub color: Color, + #[serde(with = "color_serde")] + pub variant: Color, +} + +pub fn parse_argb(s: &str) -> Option { + let hex = s.strip_prefix('#').unwrap_or(s); + + let parse_channel = |from: usize, to: usize| { + let num = + usize::from_str_radix(&hex[from..=to], 16).ok()? as f32 / 255.0; + + // If we only got half a byte (one letter), expand it into a full byte (two letters) + Some(if from == to { num + num * 16.0 } else { num }) + }; + + Some(match hex.len() { + 3 => Color::from_rgb( + parse_channel(0, 0)?, + parse_channel(1, 1)?, + parse_channel(2, 2)?, + ), + 4 => Color::from_rgba( + parse_channel(1, 1)?, + parse_channel(2, 2)?, + parse_channel(3, 3)?, + parse_channel(0, 0)?, + ), + 6 => Color::from_rgb( + parse_channel(0, 1)?, + parse_channel(2, 3)?, + parse_channel(4, 5)?, + ), + 8 => Color::from_rgba( + parse_channel(2, 3)?, + parse_channel(4, 5)?, + parse_channel(6, 7)?, + parse_channel(0, 1)?, + ), + _ => None?, + }) +} + +mod color_serde { + use iced_widget::core::Color; + use serde::{Deserialize, Deserializer}; + + use super::parse_argb; + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok(String::deserialize(deserializer) + .map(|hex| parse_argb(&hex))? + .unwrap_or(Color::TRANSPARENT)) + } +} diff --git a/material_theme/src/text.rs b/material_theme/src/text.rs new file mode 100644 index 0000000..10b2e65 --- /dev/null +++ b/material_theme/src/text.rs @@ -0,0 +1,86 @@ +#![allow(dead_code)] +use iced_widget::text::{Catalog, Style, StyleFn}; + +use crate::Theme; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(none) + } + + fn style(&self, class: &Self::Class<'_>) -> Style { + class(self) + } +} + +pub fn none(_: &Theme) -> Style { + Style { color: None } +} + +pub fn primary(theme: &Theme) -> Style { + Style { + color: Some(theme.colorscheme.primary.on_primary), + } +} + +pub fn primary_container(theme: &Theme) -> Style { + Style { + color: Some(theme.colorscheme.primary.on_primary_container), + } +} + +pub fn secondary(theme: &Theme) -> Style { + Style { + color: Some(theme.colorscheme.secondary.on_secondary), + } +} + +pub fn secondary_container(theme: &Theme) -> Style { + Style { + color: Some(theme.colorscheme.secondary.on_secondary_container), + } +} + +pub fn tertiary(theme: &Theme) -> Style { + Style { + color: Some(theme.colorscheme.tertiary.on_tertiary), + } +} + +pub fn tertiary_container(theme: &Theme) -> Style { + Style { + color: Some(theme.colorscheme.tertiary.on_tertiary_container), + } +} + +pub fn error(theme: &Theme) -> Style { + Style { + color: Some(theme.colorscheme.error.on_error), + } +} + +pub fn error_container(theme: &Theme) -> Style { + Style { + color: Some(theme.colorscheme.error.on_error_container), + } +} + +pub fn surface(theme: &Theme) -> Style { + Style { + color: Some(theme.colorscheme.surface.on_surface), + } +} + +pub fn surface_variant(theme: &Theme) -> Style { + Style { + color: Some(theme.colorscheme.surface.on_surface_variant), + } +} + +pub fn inverse_surface(theme: &Theme) -> Style { + Style { + color: Some(theme.colorscheme.inverse.inverse_on_surface), + } +} diff --git a/material_theme/src/utils.rs b/material_theme/src/utils.rs new file mode 100644 index 0000000..c9eb78e --- /dev/null +++ b/material_theme/src/utils.rs @@ -0,0 +1,61 @@ +use iced_widget::core::Color; + +pub fn elevation(elevation_level: u8) -> f32 { + (match elevation_level { + 0 => 0.0, + 1 => 1.0, + 2 => 3.0, + 3 => 6.0, + 4 => 8.0, + _ => 12.0, + } as f32) +} + +pub fn mix(color1: Color, color2: Color, p2: f32) -> Color { + if p2 <= 0.0 { + return color1; + } else if p2 >= 1.0 { + return color2; + } + + let p1 = 1.0 - p2; + + if color1.a != 1.0 || color2.a != 1.0 { + let a = color1.a * p1 + color2.a * p2; + if a > 0.0 { + let c1 = color1.into_linear().map(|c| c * color1.a * p1); + let c2 = color2.into_linear().map(|c| c * color2.a * p2); + + let [r, g, b] = + [c1[0] + c2[0], c1[1] + c2[1], c1[2] + c2[2]].map(|u| u / a); + + return Color::from_linear_rgba(r, g, b, a); + } + } + + let c1 = color1.into_linear().map(|c| c * p1); + let c2 = color2.into_linear().map(|c| c * p2); + + Color::from_linear_rgba( + c1[0] + c2[0], + c1[1] + c2[1], + c1[2] + c2[2], + c1[3] + c2[3], + ) +} + +#[cfg(test)] +mod tests { + use super::{Color, mix}; + + #[test] + fn mixing_works() { + let base = Color::from_rgba(1.0, 0.0, 0.0, 0.7); + let overlay = Color::from_rgba(0.0, 1.0, 0.0, 0.2); + + assert_eq!( + mix(base, overlay, 0.75).into_rgba8(), + Color::from_linear_rgba(0.53846, 0.46154, 0.0, 0.325).into_rgba8() + ); + } +} diff --git a/src/main.rs b/src/main.rs index 63e9deb..6ab4da9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,8 +35,6 @@ use types::{ Project, }; -//pub type Element<'a, Message> = iced::Element<'a, Message, OtherTheme>; - fn main() -> Result<(), Box> { let version = std::env::args() .nth(1) @@ -60,7 +58,6 @@ fn main() -> Result<(), Box> { iced::application(App::title, App::update, App::view) .font(icon::FONT) .theme(|state| state.theme.value().clone()) - //.theme(|_| theme::LIGHT.clone()) .subscription(App::subscription) .antialiasing(true) .run_with(move || App::new(config_load))?; @@ -446,21 +443,5 @@ impl App { Animation::new(&self.theme, content) .on_update(Message::SwitchTheme) .into() - //row![ - // button("filled") - // .style(theme::button::filled) - // .on_press(Message::RefreshEditorContent), - // button("elevated") - // .style(theme::button::elevated) - // .on_press(Message::RefreshEditorContent), - // button("filled tonal") - // .style(theme::button::filled_tonal) - // .on_press(Message::RefreshEditorContent), - // button("outlined") - // .style(theme::button::outlined) - // .on_press(Message::RefreshEditorContent), - //] - //.spacing(10) - //.into() } } diff --git a/src/theme.rs b/src/theme.rs index 6fe844c..232f309 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1,18 +1,11 @@ -pub mod button; -pub mod text; - -use std::sync::{Arc, LazyLock}; +use std::sync::Arc; use iced::Color; -use iced::theme::Base; use iced::theme::palette::Extended; use serde::Deserialize; use crate::config::Config; -const DARK_THEME_CONTENT: &str = include_str!("../assets/themes/dark.toml"); -const LIGHT_THEME_CONTENT: &str = include_str!("../assets/themes/light.toml"); - pub fn theme_index(theme_name: &str, slice: &[iced::Theme]) -> Option { slice .iter() @@ -83,159 +76,6 @@ impl Default for Appearance { } } -#[derive(Debug, PartialEq, Deserialize)] -pub struct OtherTheme { - name: String, - #[serde(flatten)] - colorscheme: ColorScheme, -} - -impl Clone for OtherTheme { - fn clone(&self) -> Self { - Self { - name: self.name.clone(), - colorscheme: self.colorscheme, - } - } - - fn clone_from(&mut self, source: &Self) { - self.name = source.name.clone(); - self.colorscheme = source.colorscheme; - } -} - -impl Default for OtherTheme { - fn default() -> Self { - match dark_light::detect().unwrap_or(dark_light::Mode::Unspecified) { - dark_light::Mode::Dark | dark_light::Mode::Unspecified => { - DARK.clone() - } - dark_light::Mode::Light => LIGHT.clone(), - } - } -} - -impl Base for OtherTheme { - fn base(&self) -> iced::theme::Style { - iced::theme::Style { - background_color: self.colorscheme.surface.color, - text_color: self.colorscheme.surface.on_surface, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] -pub struct ColorScheme { - pub primary: Primary, - pub secondary: Secondary, - pub tertiary: Tertiary, - pub error: Error, - pub surface: Surface, - pub inverse: Inverse, - pub outline: Outline, - #[serde(with = "color_serde")] - pub shadow: Color, -} - -pub static DARK: LazyLock = LazyLock::new(|| { - toml::from_str(DARK_THEME_CONTENT).expect("parse dark theme") -}); - -pub static LIGHT: LazyLock = LazyLock::new(|| { - toml::from_str(LIGHT_THEME_CONTENT).expect("parse light theme") -}); - -#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] -pub struct Primary { - #[serde(with = "color_serde")] - pub color: Color, - #[serde(with = "color_serde")] - pub on_primary: Color, - #[serde(with = "color_serde")] - pub primary_container: Color, - #[serde(with = "color_serde")] - pub on_primary_container: Color, -} - -#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] -pub struct Secondary { - #[serde(with = "color_serde")] - pub color: Color, - #[serde(with = "color_serde")] - pub on_secondary: Color, - #[serde(with = "color_serde")] - pub secondary_container: Color, - #[serde(with = "color_serde")] - pub on_secondary_container: Color, -} - -#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] -pub struct Tertiary { - #[serde(with = "color_serde")] - pub color: Color, - #[serde(with = "color_serde")] - pub on_tertiary: Color, - #[serde(with = "color_serde")] - pub tertiary_container: Color, - #[serde(with = "color_serde")] - pub on_tertiary_container: Color, -} - -#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] -pub struct Error { - #[serde(with = "color_serde")] - pub color: Color, - #[serde(with = "color_serde")] - pub on_error: Color, - #[serde(with = "color_serde")] - pub error_container: Color, - #[serde(with = "color_serde")] - pub on_error_container: Color, -} - -#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] -pub struct Surface { - #[serde(with = "color_serde")] - pub color: Color, - #[serde(with = "color_serde")] - pub on_surface: Color, - #[serde(with = "color_serde")] - pub on_surface_variant: Color, - pub surface_container: SurfaceContainer, -} - -#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] -pub struct SurfaceContainer { - #[serde(with = "color_serde")] - pub lowest: Color, - #[serde(with = "color_serde")] - pub low: Color, - #[serde(with = "color_serde")] - pub base: Color, - #[serde(with = "color_serde")] - pub high: Color, - #[serde(with = "color_serde")] - pub highest: Color, -} - -#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] -pub struct Inverse { - #[serde(with = "color_serde")] - pub inverse_surface: Color, - #[serde(with = "color_serde")] - pub inverse_on_surface: Color, - #[serde(with = "color_serde")] - pub inverse_primary: Color, -} - -#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] -pub struct Outline { - #[serde(with = "color_serde")] - pub color: Color, - #[serde(with = "color_serde")] - pub variant: Color, -} - #[derive(Debug, Deserialize)] pub struct Theme { name: String, diff --git a/src/theme/button.rs b/src/theme/button.rs deleted file mode 100644 index ddd2c71..0000000 --- a/src/theme/button.rs +++ /dev/null @@ -1,189 +0,0 @@ -#![allow(dead_code)] -use iced::widget::button::{Catalog, Status, Style, StyleFn}; -use iced::{Background, Border, Color, Shadow, Vector}; - -use super::OtherTheme; - -impl Catalog for OtherTheme { - type Class<'a> = StyleFn<'a, Self>; - - fn default<'a>() -> Self::Class<'a> { - Box::new(default) - } - - fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { - class(self, status) - } -} - -fn default(theme: &OtherTheme, status: Status) -> Style { - filled(theme, status) -} - -fn button( - foreground: Color, - background: Color, - background_hover: Color, - disabled: Color, - shadow_color: Color, - shadow_elevation: u8, - status: Status, -) -> Style { - let border = Border { - radius: 400.0.into(), - ..Default::default() - }; - - let elevation_to_offset = |elevation: u8| { - (match elevation { - 0 => 0.0, - 1 => 1.0, - 2 => 3.0, - 3 => 6.0, - 4 => 8.0, - _ => 12.0, - } as f32) - }; - - match status { - Status::Active | Status::Pressed => Style { - background: Some(Background::Color(background)), - text_color: foreground, - border, - shadow: Shadow { - color: shadow_color, - offset: Vector { - x: 0.0, - y: elevation_to_offset(shadow_elevation), - }, - blur_radius: elevation_to_offset(shadow_elevation) - * (1.0 - + 0.4_f32.powf(elevation_to_offset(shadow_elevation))), - }, - }, - Status::Hovered => Style { - background: Some(Background::Color(background_hover)), - text_color: foreground, - border, - shadow: Shadow { - color: shadow_color, - offset: Vector { - x: 0.0, - y: elevation_to_offset(shadow_elevation + 1), - }, - blur_radius: (elevation_to_offset(shadow_elevation + 1)) - * (1.0 - + 0.4_f32 - .powf(elevation_to_offset(shadow_elevation + 1))), - }, - }, - Status::Disabled => Style { - background: Some(Background::Color(Color { - a: 0.12, - ..disabled - })), - text_color: Color { - a: 0.38, - ..disabled - }, - border, - ..Default::default() - }, - } -} - -pub fn elevated(theme: &OtherTheme, status: Status) -> Style { - let surface_colors = theme.colorscheme.surface; - - let foreground = theme.colorscheme.primary.color; - let background = surface_colors.surface_container.low; - let disabled = surface_colors.on_surface; - - let shadow_color = theme.colorscheme.shadow; - - button( - foreground, - background, - background, - disabled, - shadow_color, - 1, - status, - ) -} - -pub fn filled(theme: &OtherTheme, status: Status) -> Style { - let primary_colors = theme.colorscheme.primary; - - let foreground = primary_colors.on_primary; - let background = primary_colors.color; - let disabled = theme.colorscheme.surface.on_surface; - - let shadow_color = theme.colorscheme.shadow; - - button( - foreground, - background, - background, - disabled, - shadow_color, - 0, - status, - ) -} - -pub fn filled_tonal(theme: &OtherTheme, status: Status) -> Style { - let secondary_colors = theme.colorscheme.secondary; - - let foreground = secondary_colors.on_secondary_container; - let background = secondary_colors.secondary_container; - let disabled = theme.colorscheme.surface.on_surface; - - let shadow_color = theme.colorscheme.shadow; - - button( - foreground, - background, - background, - disabled, - shadow_color, - 0, - status, - ) -} - -pub fn outlined(theme: &OtherTheme, status: Status) -> Style { - let foreground = theme.colorscheme.primary.color; - let background = Color::TRANSPARENT; - let disabled = theme.colorscheme.surface.on_surface; - - let outline = theme.colorscheme.outline.color; - - let border = match status { - Status::Active | Status::Pressed | Status::Hovered => Border { - color: outline, - width: 1.0, - radius: 400.0.into(), - }, - Status::Disabled => Border { - color: Color { - a: 0.12, - ..disabled - }, - width: 1.0, - radius: 400.0.into(), - }, - }; - - let style = button( - foreground, - background, - background, - disabled, - Color::TRANSPARENT, - 0, - status, - ); - - Style { border, ..style } -} diff --git a/src/theme/text.rs b/src/theme/text.rs deleted file mode 100644 index 9cbd056..0000000 --- a/src/theme/text.rs +++ /dev/null @@ -1,50 +0,0 @@ -#![allow(dead_code)] -use iced::widget::text::{Catalog, Style, StyleFn}; - -use super::OtherTheme; - -impl Catalog for OtherTheme { - type Class<'a> = StyleFn<'a, Self>; - - fn default<'a>() -> Self::Class<'a> { - Box::new(none) - } - - fn style(&self, class: &Self::Class<'_>) -> Style { - class(self) - } -} - -pub fn none(_: &OtherTheme) -> Style { - Style { color: None } -} - -pub fn primary(theme: &OtherTheme) -> Style { - Style { - color: Some(theme.colorscheme.primary.on_primary), - } -} - -pub fn secondary(theme: &OtherTheme) -> Style { - Style { - color: Some(theme.colorscheme.secondary.on_secondary), - } -} - -pub fn tertiary(theme: &OtherTheme) -> Style { - Style { - color: Some(theme.colorscheme.tertiary.on_tertiary), - } -} - -pub fn error(theme: &OtherTheme) -> Style { - Style { - color: Some(theme.colorscheme.error.on_error), - } -} - -pub fn surface(theme: &OtherTheme) -> Style { - Style { - color: Some(theme.colorscheme.surface.on_surface), - } -} diff --git a/theme_test/Cargo.toml b/theme_test/Cargo.toml new file mode 100644 index 0000000..34008eb --- /dev/null +++ b/theme_test/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "theme_test" +version = "0.0.1" +edition = "2024" + +[dependencies] +iced.workspace = true +material_theme = { path = "../material_theme" } diff --git a/theme_test/src/main.rs b/theme_test/src/main.rs new file mode 100644 index 0000000..c4735be --- /dev/null +++ b/theme_test/src/main.rs @@ -0,0 +1,89 @@ +use iced::Element; +use iced::widget::{button, column, container, row, text}; +use material_theme::Theme; +use material_theme::button::{ + elevated, filled_tonal, outlined, text as text_style, +}; +use material_theme::container::{ + error, error_container, inverse_surface, primary, primary_container, + secondary, secondary_container, surface, surface_container, + surface_container_high, surface_container_highest, surface_container_low, + surface_container_lowest, tertiary, tertiary_container, +}; +use material_theme::text::surface_variant; + +fn main() { + iced::application("Theme Test", (), view) + .theme(|_| material_theme::DARK.clone()) + .run() + .unwrap(); +} + +#[derive(Debug, Clone)] +enum Message { + Noop, +} + +fn view(_: &()) -> Element<'_, Message, Theme> { + container( + row![ + column![ + button("Disabled"), + button("Filled").on_press(Message::Noop), + button("Filled Tonal") + .on_press(Message::Noop) + .style(filled_tonal), + button("Elevated").on_press(Message::Noop).style(elevated), + button("Outlined").on_press(Message::Noop).style(outlined), + button("Text").on_press(Message::Noop).style(text_style), + button("Text Disabled").style(text_style), + ] + .spacing(10), + column![ + text("None"), + container("Primary").padding(8).style(primary), + container("Primary Container") + .padding(8) + .style(primary_container), + container("Secondary").padding(8).style(secondary), + container("Secondary Container") + .padding(8) + .style(secondary_container), + container("Tertiary").padding(8).style(tertiary), + container("Tertiary Container") + .padding(8) + .style(tertiary_container), + container("Error").padding(8).style(error), + container("Error Container") + .padding(8) + .style(error_container), + container("Surface").padding(8).style(surface), + container(text("Surface Variant").style(surface_variant)) + .padding(8) + .style(surface), + container("Inverse Surface") + .padding(8) + .style(inverse_surface), + container("Surface Container Lowest") + .padding(8) + .style(surface_container_lowest), + container("Surface Container Low") + .padding(8) + .style(surface_container_low), + container("Surface Container") + .padding(8) + .style(surface_container), + container("Surface Container High") + .padding(8) + .style(surface_container_high), + container("Surface Container Highest") + .padding(8) + .style(surface_container_highest), + ] + .spacing(10) + ] + .spacing(20), + ) + .padding(12) + .into() +} -- cgit v1.2.3 From e17ce59fa4c907511f38795c342b2232a7bba26d Mon Sep 17 00:00:00 2001 From: pml68 Date: Mon, 28 Apr 2025 10:59:52 +0200 Subject: feat: switch to fully custom, Material3-based theme --- assets/themes/rose_pine.toml | 41 ----- src/appearance.rs | 46 ++++++ src/config.rs | 41 +++-- src/dialogs.rs | 4 +- src/main.rs | 23 ++- src/options.rs | 2 +- src/panes/code_view.rs | 46 +++--- src/panes/designer_view.rs | 7 +- src/panes/element_list.rs | 7 +- src/panes/style.rs | 23 +-- src/theme.rs | 357 ------------------------------------------ src/types.rs | 6 +- src/types/project.rs | 18 +-- src/types/rendered_element.rs | 30 +--- src/widget.rs | 18 ++- theme_test/src/main.rs | 2 +- 16 files changed, 155 insertions(+), 516 deletions(-) delete mode 100644 assets/themes/rose_pine.toml create mode 100644 src/appearance.rs delete mode 100644 src/theme.rs (limited to 'assets') diff --git a/assets/themes/rose_pine.toml b/assets/themes/rose_pine.toml deleted file mode 100644 index e4540fb..0000000 --- a/assets/themes/rose_pine.toml +++ /dev/null @@ -1,41 +0,0 @@ -name = "Rosé Pine" - -dark = true - -[palette] -background = "#26233a" -text = "#e0def4" -primary = "#9ccfd8" -success = "#f6c177" -danger = "#eb6f92" -warning = "#e4b363" - -[background] -base = { color = "#191724", text = "#e0def4" } -weak = { color = "#1f1d2e", text = "#e0def4" } -strong = { color = "#26233a", text = "#f4ebd3" } - -[primary] -base = { color = "#eb6f92", text = "#000000" } -weak = { color = "#f6c177", text = "#000000" } -strong = { color = "#ebbcba", text = "#000000" } - -[secondary] -base = { color = "#31748f", text = "#ffffff" } -weak = { color = "#9ccfd8", text = "#000000" } -strong = { color = "#c4a7e7", text = "#000000" } - -[success] -base = { color = "#9ccfd8", text = "#000000" } -weak = { color = "#6e6a86", text = "#ffffff" } -strong = { color = "#908caa", text = "#000000" } - -[danger] -base = { color = "#eb6f92", text = "#ffffff" } -weak = { color = "#f6c177", text = "#ffffff" } -strong = { color = "#524f67", text = "#ffffff" } - -[warning] -base = { color = "#e4b363", text = "#ffffff" } -weak = { color = "#f4e1a1", text = "#000000" } -strong = { color = "#d8a343", text = "#ffffff" } diff --git a/src/appearance.rs b/src/appearance.rs new file mode 100644 index 0000000..78e782d --- /dev/null +++ b/src/appearance.rs @@ -0,0 +1,46 @@ +use std::sync::Arc; + +use material_theme::Theme; + +pub fn iced_theme_from_str(theme_name: &str) -> iced::Theme { + match theme_name { + "Light" => iced::Theme::Light, + "Dark" => iced::Theme::Dark, + "Dracula" => iced::Theme::Dracula, + "Nord" => iced::Theme::Nord, + "Solarized Light" => iced::Theme::SolarizedLight, + "Solarized Dark" => iced::Theme::SolarizedDark, + "Gruvbox Light" => iced::Theme::GruvboxLight, + "Gruvbox Dark" => iced::Theme::GruvboxDark, + "Catppuccin Latte" => iced::Theme::CatppuccinLatte, + "Catppuccin Frappé" => iced::Theme::CatppuccinFrappe, + "Catppuccin Macchiato" => iced::Theme::CatppuccinMacchiato, + "Catppuccin Mocha" => iced::Theme::CatppuccinMocha, + "Tokyo Night" => iced::Theme::TokyoNight, + "Tokyo Night Storm" => iced::Theme::TokyoNightStorm, + "Tokyo Night Light" => iced::Theme::TokyoNightLight, + "Kanagawa Wave" => iced::Theme::KanagawaWave, + "Kanagawa Dragon" => iced::Theme::KanagawaDragon, + "Kanagawa Lotus" => iced::Theme::KanagawaLotus, + "Moonfly" => iced::Theme::Moonfly, + "Nightfly" => iced::Theme::Nightfly, + "Oxocarbon" => iced::Theme::Oxocarbon, + "Ferra" => iced::Theme::Ferra, + _ => iced::Theme::default(), + } +} + +#[derive(Debug, Clone)] +pub struct Appearance { + pub selected: Theme, + pub all: Arc<[Theme]>, +} + +impl Default for Appearance { + fn default() -> Self { + Self { + selected: Theme::default(), + all: Theme::ALL.into(), + } + } +} diff --git a/src/config.rs b/src/config.rs index 1da1239..7f6f8ce 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,26 +1,36 @@ // (c) 2022-2024 Cory Forsstrom, Casper Rogild Storm, Calvin Lee, Andrew Baldwin, Reza Alizadeh Majd // (c) 2024-2025 Polesznyák Márk László -use std::path::PathBuf; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use material_theme::Theme; use serde::Deserialize; use tokio_stream::StreamExt; use tokio_stream::wrappers::ReadDirStream; -use crate::theme::{Appearance, Theme, theme_from_str, theme_index}; +use crate::appearance::Appearance; use crate::{Error, environment}; #[derive(Debug, Clone, Default)] pub struct Config { - pub theme: Appearance, - pub last_project: Option, + theme: Appearance, + last_project: Option, } impl Config { - pub fn selected_theme(&self) -> iced::Theme { + pub fn selected_theme(&self) -> Theme { self.theme.selected.clone() } + pub fn themes(&self) -> Arc<[Theme]> { + self.theme.all.clone() + } + + pub fn last_project(&self) -> Option<&Path> { + self.last_project.as_deref() + } + pub fn config_dir() -> PathBuf { let dir = environment::config_dir(); @@ -67,7 +77,7 @@ impl Config { last_project, } = toml::from_str(content.as_ref())?; - let theme = Self::load_theme(theme).await.unwrap_or_default(); + let theme = Self::load_appearance(&theme).await.unwrap_or_default(); Ok(Self { theme, @@ -75,7 +85,9 @@ impl Config { }) } - pub async fn load_theme(theme_name: String) -> Result { + pub async fn load_appearance( + theme_name: &str, + ) -> Result { use tokio::fs; let read_entry = async move |entry: fs::DirEntry| { @@ -83,15 +95,16 @@ impl Config { let theme: Theme = toml::from_str(content.as_ref()).ok()?; - Some(iced::Theme::from(theme)) + Some(theme) }; - let mut selected = Theme::default().into(); - let mut all = iced::Theme::ALL.to_owned(); - all.push(Theme::default().into()); + let mut selected = Theme::default(); + let mut all = Theme::ALL.to_owned(); - if theme_index(&theme_name, iced::Theme::ALL).is_some() { - selected = theme_from_str(None, &theme_name); + if let Some(index) = + Theme::ALL.iter().position(|t| t.name() == theme_name) + { + selected = Theme::ALL[index].clone(); } let mut stream = @@ -102,7 +115,7 @@ impl Config { }; if let Some(theme) = read_entry(entry).await { - if theme.to_string() == theme_name { + if theme.name() == theme_name { selected = theme.clone(); } all.push(theme); diff --git a/src/dialogs.rs b/src/dialogs.rs index a623f35..c1933ec 100644 --- a/src/dialogs.rs +++ b/src/dialogs.rs @@ -1,8 +1,8 @@ -use iced::{Element, Task}; +use iced::Task; use iced_dialog::button; use crate::Message; -use crate::types::{DialogAction, DialogButtons}; +use crate::types::{DialogAction, DialogButtons, Element}; pub const UNSAVED_CHANGES_TITLE: &str = "Unsaved changes"; pub const WARNING_TITLE: &str = "Heads up!"; diff --git a/src/main.rs b/src/main.rs index 5014077..1ac1d67 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +mod appearance; mod config; mod dialogs; mod environment; @@ -6,7 +7,6 @@ mod error; mod icon; mod options; mod panes; -mod theme; mod types; mod values; mod widget; @@ -24,13 +24,15 @@ use iced::advanced::widget::Id; use iced::widget::{ Column, container, pane_grid, pick_list, row, text, text_editor, }; -use iced::{Alignment, Element, Length, Task, Theme, clipboard, keyboard}; +use iced::{Alignment, Length, Task, clipboard, keyboard}; use iced_anim::transition::Easing; use iced_anim::{Animated, Animation}; use iced_dialog::dialog::Dialog; +use material_theme::Theme; use panes::{code_view, designer_view, element_list}; use types::{ - Action, DesignerPane, DialogAction, DialogButtons, Message, Project, + Action, DesignerPane, DialogAction, DialogButtons, Element, Message, + Project, }; fn main() -> Result<(), Box> { @@ -148,11 +150,10 @@ impl IcedBuilder { self.config = Arc::new(config); self.theme.update(self.config.selected_theme().into()); - return if let Some(path) = self.config.last_project.clone() - { + return if let Some(path) = self.config.last_project() { if path.exists() && path.is_file() { Task::perform( - Project::from_path(path), + Project::from_path(path.to_owned()), Message::FileOpened, ) } else { @@ -394,7 +395,7 @@ impl IcedBuilder { fn view(&self) -> Element<'_, Message> { let header = row![pick_list( - self.config.theme.all.clone(), + self.config.themes(), Some(self.theme.target()), |theme| Message::SwitchTheme(theme.into()) )] @@ -442,13 +443,7 @@ impl IcedBuilder { DialogButtons::OkCancel => vec![ok_button(), cancel_button()], }, ) - .title(self.dialog_title) - .container_style(|theme| container::Style { - background: Some( - theme.extended_palette().background.strong.color.into(), - ), - ..Default::default() - }); + .title(self.dialog_title); Animation::new(&self.theme, content) .on_update(Message::SwitchTheme) diff --git a/src/options.rs b/src/options.rs index 2dc25d7..931182a 100644 --- a/src/options.rs +++ b/src/options.rs @@ -258,7 +258,7 @@ impl ApplyOptions for Row<'_, Message> { } } -impl ApplyOptions for Image { +impl ApplyOptions for Image<'_, Handle> { fn apply_options(self, options: BTreeMap>) -> Self { let mut image = self; diff --git a/src/panes/code_view.rs b/src/panes/code_view.rs index 890af8a..5999b8f 100644 --- a/src/panes/code_view.rs +++ b/src/panes/code_view.rs @@ -1,27 +1,36 @@ use iced::advanced::text::highlighter::Format; -use iced::widget::{Space, button, pane_grid, row, text, text_editor}; -use iced::{Alignment, Background, Border, Font, Length, Theme}; +use iced::border::Radius; +use iced::widget::{button, pane_grid, row, text, text_editor}; +use iced::{Alignment, Border, Font, Length}; use iced_custom_highlighter::{Highlight, Highlighter, Scope, Settings}; +use material_theme::Theme; use super::style; use crate::icon; use crate::types::{DesignerPane, Message}; use crate::widget::tip; +// TODO: implement a highlight style for the material theme fn highlight_style(theme: &Theme, scope: &Scope) -> Format { + let theme = if theme.is_dark() { + iced::Theme::SolarizedDark + } else { + iced::Theme::SolarizedLight + }; + match scope { Scope::Custom { .. } | Scope::Other => Format { color: Some(theme.extended_palette().primary.strong.color), font: None, }, - _ => Highlight::default_style(theme, scope), + _ => Highlight::default_style(&theme, scope), } } pub fn view( editor_content: &text_editor::Content, is_focused: bool, -) -> pane_grid::Content<'_, Message> { +) -> pane_grid::Content<'_, Message, Theme> { let title_bar = pane_grid::TitleBar::new(text("Generated Code").center()) .controls(pane_grid::Controls::dynamic( row![ @@ -29,28 +38,28 @@ pub fn view( button(icon::copy()) .on_press(Message::CopyCode) .padding([2, 7]) - .style(button::text), + .style(material_theme::button::text), "Copy", tip::Position::FollowCursor ), - Space::with_width(20), button("Switch to Designer view") .on_press(Message::SwitchPage(DesignerPane::DesignerView)) ] + .spacing(20) .align_y(Alignment::Center), row![ tip( button(icon::copy()) .on_press(Message::CopyCode) .padding([2, 7]) - .style(button::text), + .style(material_theme::button::text), "Copy", tip::Position::FollowCursor ), - Space::with_width(20), button(icon::switch()) .on_press(Message::SwitchPage(DesignerPane::DesignerView)) ] + .spacing(20) .align_y(Alignment::Center), )) .padding(10) @@ -60,25 +69,22 @@ pub fn view( text_editor(editor_content) .on_action(Message::EditorAction) .font(Font::MONOSPACE) - .highlight_with::( + .highlight_with::>( Settings::new(vec![], highlight_style, "rs"), Highlight::to_format, ) .style(|theme, _| { - let palette = theme.extended_palette(); + let style = material_theme::text_editor::default( + theme, + text_editor::Status::Active, + ); + text_editor::Style { - background: Background::Color( - palette.background.base.color, - ), border: Border { - radius: 2.into(), - width: 1.0, - color: palette.background.strong.color, + radius: Radius::default(), + ..style.border }, - icon: palette.background.weak.text, - placeholder: palette.background.strong.color, - value: palette.background.base.text, - selection: palette.primary.weak.color, + ..style } }) .height(Length::Fill) diff --git a/src/panes/designer_view.rs b/src/panes/designer_view.rs index af72022..0255b40 100644 --- a/src/panes/designer_view.rs +++ b/src/panes/designer_view.rs @@ -1,7 +1,8 @@ use iced::widget::{ button, center, container, pane_grid, responsive, row, text, themer, }; -use iced::{Alignment, Element, Length}; +use iced::{Alignment, Length}; +use material_theme::Theme; use super::style; use crate::icon; @@ -11,8 +12,8 @@ pub fn view<'a>( element_tree: Option<&'a RenderedElement>, designer_theme: iced::Theme, is_focused: bool, -) -> pane_grid::Content<'a, Message> { - let el_tree: Element<'a, Message> = match element_tree { +) -> pane_grid::Content<'a, Message, Theme> { + let el_tree: iced::Element<'a, Message> = match element_tree { Some(tree) => responsive(|size| { center( container(tree.clone()) diff --git a/src/panes/element_list.rs b/src/panes/element_list.rs index 594c203..0e5dbfe 100644 --- a/src/panes/element_list.rs +++ b/src/panes/element_list.rs @@ -1,9 +1,10 @@ use iced::widget::{Column, column, container, pane_grid, text}; -use iced::{Alignment, Element, Length}; +use iced::{Alignment, Length}; use iced_drop::droppable; +use material_theme::Theme; use super::style; -use crate::types::{ElementName, Message}; +use crate::types::{Element, ElementName, Message}; fn items_list_view<'a>() -> Element<'a, Message> { let mut column = Column::new() @@ -25,7 +26,7 @@ fn items_list_view<'a>() -> Element<'a, Message> { .into() } -pub fn view<'a>(is_focused: bool) -> pane_grid::Content<'a, Message> { +pub fn view<'a>(is_focused: bool) -> pane_grid::Content<'a, Message, Theme> { let items_list = items_list_view(); let content = column![items_list] .align_x(Alignment::Center) diff --git a/src/panes/style.rs b/src/panes/style.rs index 1eefb2d..acca6f9 100644 --- a/src/panes/style.rs +++ b/src/panes/style.rs @@ -1,24 +1,25 @@ use iced::widget::container::Style; -use iced::{Border, Theme}; +use iced::{Background, Border}; +use material_theme::Theme; pub fn title_bar(theme: &Theme) -> Style { - let palette = theme.extended_palette(); + let surface = theme.colors().surface; Style { - text_color: Some(palette.background.strong.text), - background: Some(palette.background.strong.color.into()), + text_color: Some(surface.on_surface), + background: Some(Background::Color(surface.surface_container.high)), ..Default::default() } } pub fn pane_active(theme: &Theme) -> Style { - let palette = theme.extended_palette(); + let surface = theme.colors().surface; Style { - background: Some(palette.background.weak.color.into()), + background: Some(Background::Color(surface.surface_container.low)), border: Border { width: 1.0, - color: palette.background.strong.color, + color: surface.surface_container.high, ..Border::default() }, ..Default::default() @@ -26,13 +27,13 @@ pub fn pane_active(theme: &Theme) -> Style { } pub fn pane_focused(theme: &Theme) -> Style { - let palette = theme.extended_palette(); + let surface = theme.colors().surface; Style { - background: Some(palette.background.weak.color.into()), + background: Some(Background::Color(surface.surface_container.low)), border: Border { - width: 4.0, - color: palette.background.strong.color, + width: 2.0, + color: surface.surface_container.high, ..Border::default() }, ..Default::default() diff --git a/src/theme.rs b/src/theme.rs deleted file mode 100644 index b721ddc..0000000 --- a/src/theme.rs +++ /dev/null @@ -1,357 +0,0 @@ -use std::sync::Arc; - -use iced::Color; -use iced::theme::palette::Extended; -use serde::Deserialize; - -use crate::config::Config; - -const DEFAULT_THEME_CONTENT: &str = - include_str!("../assets/themes/rose_pine.toml"); - -pub fn theme_index(theme_name: &str, slice: &[iced::Theme]) -> Option { - slice - .iter() - .position(|theme| theme.to_string() == theme_name) -} - -pub fn theme_from_str( - config: Option<&Config>, - theme_name: &str, -) -> iced::Theme { - match theme_name { - "Light" => iced::Theme::Light, - "Dark" => iced::Theme::Dark, - "Dracula" => iced::Theme::Dracula, - "Nord" => iced::Theme::Nord, - "Solarized Light" => iced::Theme::SolarizedLight, - "Solarized Dark" => iced::Theme::SolarizedDark, - "Gruvbox Light" => iced::Theme::GruvboxLight, - "Gruvbox Dark" => iced::Theme::GruvboxDark, - "Catppuccin Latte" => iced::Theme::CatppuccinLatte, - "Catppuccin Frappé" => iced::Theme::CatppuccinFrappe, - "Catppuccin Macchiato" => iced::Theme::CatppuccinMacchiato, - "Catppuccin Mocha" => iced::Theme::CatppuccinMocha, - "Tokyo Night" => iced::Theme::TokyoNight, - "Tokyo Night Storm" => iced::Theme::TokyoNightStorm, - "Tokyo Night Light" => iced::Theme::TokyoNightLight, - "Kanagawa Wave" => iced::Theme::KanagawaWave, - "Kanagawa Dragon" => iced::Theme::KanagawaDragon, - "Kanagawa Lotus" => iced::Theme::KanagawaLotus, - "Moonfly" => iced::Theme::Moonfly, - "Nightfly" => iced::Theme::Nightfly, - "Oxocarbon" => iced::Theme::Oxocarbon, - "Ferra" => iced::Theme::Ferra, - _ => { - if let Some(config) = config { - if theme_name == config.theme.selected.to_string() { - config.theme.selected.clone() - } else if let Some(index) = - theme_index(theme_name, &config.theme.all) - { - config.theme.all[index].clone() - } else { - iced::Theme::default() - } - } else { - iced::Theme::default() - } - } - } -} - -#[derive(Debug, Clone)] -pub struct Appearance { - pub selected: iced::Theme, - pub all: Arc<[iced::Theme]>, -} - -impl Default for Appearance { - fn default() -> Self { - Self { - selected: Theme::default().into(), - all: { - let mut themes = iced::Theme::ALL.to_owned(); - themes.push(Theme::default().into()); - themes.into() - }, - } - } -} - -#[derive(Debug, Deserialize)] -pub struct Theme { - name: String, - palette: ThemePalette, - dark: Option, - #[serde(flatten)] - extended: Option, -} - -impl From for iced::Theme { - fn from(value: Theme) -> Self { - iced::Theme::custom_with_fn( - value.name.clone(), - value.palette.into(), - |_| value.into(), - ) - } -} - -impl Default for Theme { - fn default() -> Self { - toml::from_str(DEFAULT_THEME_CONTENT).expect("parse default theme") - } -} - -#[derive(Debug, Clone, Copy, Deserialize)] -pub struct ThemePalette { - #[serde(with = "color_serde")] - background: Color, - #[serde(with = "color_serde")] - text: Color, - #[serde(with = "color_serde")] - primary: Color, - #[serde(with = "color_serde")] - success: Color, - #[serde(with = "color_serde")] - danger: Color, - #[serde(with = "color_serde")] - warning: Color, -} - -impl Default for ThemePalette { - fn default() -> Self { - let palette = iced::Theme::default().palette(); - Self { - background: palette.background, - text: palette.text, - primary: palette.primary, - success: palette.success, - danger: palette.danger, - warning: palette.warning, - } - } -} - -impl From for iced::theme::Palette { - fn from(palette: ThemePalette) -> Self { - iced::theme::Palette { - background: palette.background, - text: palette.text, - primary: palette.primary, - success: palette.success, - danger: palette.danger, - warning: palette.warning, - } - } -} - -impl From for Extended { - fn from(theme: Theme) -> Self { - let mut extended = Extended::generate(theme.palette.into()); - - if let Some(is_dark) = theme.dark { - extended.is_dark = is_dark; - } - - if let Some(extended_palette) = theme.extended { - if let Some(background) = extended_palette.background { - if let Some(base) = background.base { - extended.background.base = base.into(); - } - if let Some(weak) = background.weak { - extended.background.weak = weak.into(); - } - if let Some(strong) = background.strong { - extended.background.strong = strong.into(); - } - } - - if let Some(primary) = extended_palette.primary { - if let Some(base) = primary.base { - extended.primary.base = base.into(); - } - if let Some(weak) = primary.weak { - extended.primary.weak = weak.into(); - } - if let Some(strong) = primary.strong { - extended.primary.strong = strong.into(); - } - } - - if let Some(secondary) = extended_palette.secondary { - if let Some(base) = secondary.base { - extended.secondary.base = base.into(); - } - if let Some(weak) = secondary.weak { - extended.secondary.weak = weak.into(); - } - if let Some(strong) = secondary.strong { - extended.secondary.strong = strong.into(); - } - } - - if let Some(success) = extended_palette.success { - if let Some(base) = success.base { - extended.success.base = base.into(); - } - if let Some(weak) = success.weak { - extended.success.weak = weak.into(); - } - if let Some(strong) = success.strong { - extended.success.strong = strong.into(); - } - } - - if let Some(danger) = extended_palette.danger { - if let Some(base) = danger.base { - extended.danger.base = base.into(); - } - if let Some(weak) = danger.weak { - extended.danger.weak = weak.into(); - } - if let Some(strong) = danger.strong { - extended.danger.strong = strong.into(); - } - } - - if let Some(warning) = extended_palette.warning { - if let Some(base) = warning.base { - extended.warning.base = base.into(); - } - if let Some(weak) = warning.weak { - extended.warning.weak = weak.into(); - } - if let Some(strong) = warning.strong { - extended.warning.strong = strong.into(); - } - } - } - - extended - } -} - -#[derive(Debug, Clone, Copy, Default, Deserialize)] -struct ExtendedThemePalette { - background: Option, - primary: Option, - secondary: Option, - success: Option, - danger: Option, - warning: Option, -} - -#[derive(Debug, Clone, Copy, Default, Deserialize)] -struct ThemeBackground { - base: Option, - weak: Option, - strong: Option, -} - -#[derive(Debug, Clone, Copy, Default, Deserialize)] -struct ThemePrimary { - base: Option, - weak: Option, - strong: Option, -} - -#[derive(Debug, Clone, Copy, Default, Deserialize)] -struct ThemeSecondary { - base: Option, - weak: Option, - strong: Option, -} - -#[derive(Debug, Clone, Copy, Default, Deserialize)] -struct ThemeSuccess { - base: Option, - weak: Option, - strong: Option, -} - -#[derive(Debug, Clone, Copy, Default, Deserialize)] -struct ThemeDanger { - base: Option, - weak: Option, - strong: Option, -} - -#[derive(Debug, Clone, Copy, Default, Deserialize)] -struct ThemeWarning { - base: Option, - weak: Option, - strong: Option, -} - -#[derive(Debug, Clone, Copy, Default, Deserialize)] -struct ThemePair { - #[serde(with = "color_serde")] - color: Color, - #[serde(with = "color_serde")] - text: Color, -} - -impl From for iced::theme::palette::Pair { - fn from(pair: ThemePair) -> Self { - Self { - color: pair.color, - text: pair.text, - } - } -} - -pub fn parse_argb(s: &str) -> Option { - let hex = s.strip_prefix('#').unwrap_or(s); - - let parse_channel = |from: usize, to: usize| { - let num = - usize::from_str_radix(&hex[from..=to], 16).ok()? as f32 / 255.0; - - // If we only got half a byte (one letter), expand it into a full byte (two letters) - Some(if from == to { num + num * 16.0 } else { num }) - }; - - Some(match hex.len() { - 3 => Color::from_rgb( - parse_channel(0, 0)?, - parse_channel(1, 1)?, - parse_channel(2, 2)?, - ), - 4 => Color::from_rgba( - parse_channel(1, 1)?, - parse_channel(2, 2)?, - parse_channel(3, 3)?, - parse_channel(0, 0)?, - ), - 6 => Color::from_rgb( - parse_channel(0, 1)?, - parse_channel(2, 3)?, - parse_channel(4, 5)?, - ), - 8 => Color::from_rgba( - parse_channel(2, 3)?, - parse_channel(4, 5)?, - parse_channel(6, 7)?, - parse_channel(0, 1)?, - ), - _ => None?, - }) -} - -mod color_serde { - use iced::Color; - use serde::{Deserialize, Deserializer}; - - use super::parse_argb; - - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - Ok(String::deserialize(deserializer) - .map(|hex| parse_argb(&hex))? - .unwrap_or(Color::TRANSPARENT)) - } -} diff --git a/src/types.rs b/src/types.rs index adb788e..608f285 100644 --- a/src/types.rs +++ b/src/types.rs @@ -8,16 +8,20 @@ pub use element_name::ElementName; use iced::advanced::widget::Id; use iced::widget::{pane_grid, text_editor}; use iced_anim::Event; +use material_theme::Theme; pub use project::Project; pub use rendered_element::*; use crate::Error; use crate::config::Config; +pub type Element<'a, Message> = iced::Element<'a, Message, Theme>; + +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone)] pub enum Message { ConfigLoad(Result), - SwitchTheme(Event), + SwitchTheme(Event), CopyCode, SwitchPage(DesignerPane), EditorAction(text_editor::Action), diff --git a/src/types/project.rs b/src/types/project.rs index 91b2bb1..11789ac 100644 --- a/src/types/project.rs +++ b/src/types/project.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use super::rendered_element::RenderedElement; use crate::Error; -use crate::theme::{theme_from_str, theme_index}; +use crate::appearance::iced_theme_from_str; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Project { @@ -33,8 +33,8 @@ impl Project { pub fn get_theme(&self) -> Theme { match &self.theme { - Some(theme) => theme_from_str(None, theme), - None => Theme::default(), + Some(theme) => iced_theme_from_str(theme), + None => iced::Theme::default(), } } @@ -90,20 +90,10 @@ impl Project { Some(ref element_tree) => { let (imports, view) = element_tree.codegen(); let theme = self.get_theme(); - let theme_code = theme.to_string().replace(" ", ""); - let mut theme_imports = ""; - if theme_index(&theme.to_string(), Theme::ALL).is_none() { - if theme_code.contains("Extended") { - theme_imports = "use iced::{{color,theme::{{Palette,palette::{{Extended,Background,Primary,Secondary,Success,Danger,Warning,Pair}}}}}};\n"; - } else { - theme_imports = "use iced::{{color,theme::Palette}};\n"; - } - } let app_code = format!( r#"// Automatically generated by iced Builder use iced::{{widget::{{{imports}}},Element}}; -{theme_imports} fn main() -> iced::Result {{ iced::application(State::default, State::update, State::view).title("{}").theme(State::theme).run() @@ -130,7 +120,7 @@ impl State {{ Some(ref t) => t, None => "New app", }, - theme_code + theme.to_string().replace(" ", "") ); let config = rust_format::Config::new_str() .edition(Edition::Rust2021) diff --git a/src/types/rendered_element.rs b/src/types/rendered_element.rs index 9639299..020aa46 100755 --- a/src/types/rendered_element.rs +++ b/src/types/rendered_element.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use iced::advanced::widget::Id; -use iced::{Element, Length, widget}; +use iced::{Element, widget}; use serde::{Deserialize, Serialize}; use crate::Error; @@ -171,34 +171,6 @@ impl RenderedElement { self } - pub fn text_view<'a>(self) -> Element<'a, Message> { - let mut children = widget::column![]; - - if let Some(els) = self.child_elements.clone() { - for el in els { - children = children.push(el.clone().text_view()); - } - } - iced_drop::droppable( - widget::container( - widget::column![ - widget::text(self.name.clone().to_string()), - children - ] - .width(Length::Fill) - .spacing(10), - ) - .padding(10) - .style(widget::container::bordered_box), - ) - .id(self.id().clone()) - .drag_hide(true) - .on_drop(move |point, rect| { - Message::MoveElement(self.clone(), point, rect) - }) - .into() - } - pub fn codegen(&self) -> (String, String) { let mut imports = String::new(); let mut view = String::new(); diff --git a/src/widget.rs b/src/widget.rs index f1eb0f3..859d25e 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -1,5 +1,7 @@ -use iced::Element; -use iced::widget::{container, text, tooltip}; +use iced::widget::{self, container, text, tooltip}; +use material_theme::Theme; + +use crate::types::Element; pub mod tip { pub use super::tooltip::Position; @@ -12,10 +14,16 @@ pub fn tip<'a, Message: 'a>( ) -> Element<'a, Message> { tooltip( target, - container(text(tip).size(14)) - .padding(5) - .style(container::rounded_box), + container(text(tip).size(14)).padding(5).style(|theme| { + let base = material_theme::container::surface_container_low(theme); + container::Style { + border: iced::border::rounded(4), + ..base + } + }), position, ) .into() } + +pub type Text<'a> = widget::Text<'a, Theme>; diff --git a/theme_test/src/main.rs b/theme_test/src/main.rs index b4a7731..cba4377 100644 --- a/theme_test/src/main.rs +++ b/theme_test/src/main.rs @@ -18,7 +18,7 @@ use material_theme::text::surface_variant; fn main() -> iced::Result { iced::application(State::default, State::update, State::view) - .theme(|state| *state.theme.value()) + .theme(|state| state.theme.value().clone()) .run() } -- cgit v1.2.3