diff options
Diffstat (limited to '')
| -rw-r--r-- | Cargo.lock | 20 | ||||
| -rw-r--r-- | Cargo.toml | 27 | ||||
| -rw-r--r-- | assets/themes/dark.toml | 48 | ||||
| -rw-r--r-- | assets/themes/light.toml | 48 | ||||
| -rw-r--r-- | material_theme/Cargo.toml | 40 | ||||
| -rw-r--r-- | material_theme/assets/themes/dark.toml | 48 | ||||
| -rw-r--r-- | material_theme/assets/themes/light.toml | 48 | ||||
| -rw-r--r-- | material_theme/src/button.rs (renamed from src/theme/button.rs) | 118 | ||||
| -rw-r--r-- | material_theme/src/container.rs | 173 | ||||
| -rw-r--r-- | material_theme/src/lib.rs | 229 | ||||
| -rw-r--r-- | material_theme/src/text.rs | 86 | ||||
| -rw-r--r-- | material_theme/src/utils.rs | 61 | ||||
| -rw-r--r-- | src/main.rs | 19 | ||||
| -rw-r--r-- | src/theme.rs | 162 | ||||
| -rw-r--r-- | src/theme/text.rs | 50 | ||||
| -rw-r--r-- | theme_test/Cargo.toml | 8 | ||||
| -rw-r--r-- | theme_test/src/main.rs | 89 |
17 files changed, 889 insertions, 385 deletions
@@ -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", @@ -2674,6 +2674,16 @@ dependencies = [ ] [[package]] +name = "material_theme" +version = "0.14.0-dev" +dependencies = [ + "dark-light", + "iced_widget", + "serde", + "toml", +] + +[[package]] name = "maybe-rayon" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4635,6 +4645,14 @@ dependencies = [ ] [[package]] +name = "theme_test" +version = "0.0.1" +dependencies = [ + "iced", + "material_theme", +] + +[[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -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 <contact@pml68.dev>"] +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/src/theme/button.rs b/material_theme/src/button.rs index ddd2c71..051d6c9 100644 --- a/src/theme/button.rs +++ b/material_theme/src/button.rs @@ -1,14 +1,15 @@ #![allow(dead_code)] -use iced::widget::button::{Catalog, Status, Style, StyleFn}; -use iced::{Background, Border, Color, Shadow, Vector}; +use iced_widget::button::{Catalog, Status, Style, StyleFn}; +use iced_widget::core::{Background, Border, Color, Shadow, Vector}; -use super::OtherTheme; +use crate::Theme; +use crate::utils::{elevation, mix}; -impl Catalog for OtherTheme { +impl Catalog for Theme { type Class<'a> = StyleFn<'a, Self>; fn default<'a>() -> Self::Class<'a> { - Box::new(default) + Box::new(filled) } fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { @@ -16,65 +17,61 @@ impl Catalog for OtherTheme { } } -fn default(theme: &OtherTheme, status: Status) -> Style { - filled(theme, status) -} - fn button( foreground: Color, background: Color, - background_hover: Color, + tone_overlay: Color, disabled: Color, shadow_color: Color, shadow_elevation: u8, status: Status, ) -> Style { let border = Border { - radius: 400.0.into(), + radius: 400.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) + 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 | 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::Active => active, + Status::Pressed => Style { + background: Some(Background::Color(mix( + background, + tone_overlay, + 0.08, + ))), + ..active }, Status::Hovered => Style { - background: Some(Background::Color(background_hover)), + 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_to_offset(shadow_elevation + 1), + y: elevation(shadow_elevation + 1), }, - blur_radius: (elevation_to_offset(shadow_elevation + 1)) - * (1.0 - + 0.4_f32 - .powf(elevation_to_offset(shadow_elevation + 1))), + blur_radius: (elevation(shadow_elevation + 1)) + * (1.0 + 0.4_f32.powf(elevation(shadow_elevation + 1))), }, }, Status::Disabled => Style { @@ -92,7 +89,7 @@ fn button( } } -pub fn elevated(theme: &OtherTheme, status: Status) -> Style { +pub fn elevated(theme: &Theme, status: Status) -> Style { let surface_colors = theme.colorscheme.surface; let foreground = theme.colorscheme.primary.color; @@ -104,7 +101,7 @@ pub fn elevated(theme: &OtherTheme, status: Status) -> Style { button( foreground, background, - background, + foreground, disabled, shadow_color, 1, @@ -112,7 +109,7 @@ pub fn elevated(theme: &OtherTheme, status: Status) -> Style { ) } -pub fn filled(theme: &OtherTheme, status: Status) -> Style { +pub fn filled(theme: &Theme, status: Status) -> Style { let primary_colors = theme.colorscheme.primary; let foreground = primary_colors.on_primary; @@ -124,7 +121,7 @@ pub fn filled(theme: &OtherTheme, status: Status) -> Style { button( foreground, background, - background, + foreground, disabled, shadow_color, 0, @@ -132,19 +129,18 @@ pub fn filled(theme: &OtherTheme, status: Status) -> Style { ) } -pub fn filled_tonal(theme: &OtherTheme, status: Status) -> Style { +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, - background, + foreground, disabled, shadow_color, 0, @@ -152,7 +148,7 @@ pub fn filled_tonal(theme: &OtherTheme, status: Status) -> Style { ) } -pub fn outlined(theme: &OtherTheme, status: Status) -> Style { +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; @@ -178,7 +174,7 @@ pub fn outlined(theme: &OtherTheme, status: Status) -> Style { let style = button( foreground, background, - background, + foreground, disabled, Color::TRANSPARENT, 0, @@ -187,3 +183,27 @@ pub fn outlined(theme: &OtherTheme, status: Status) -> Style { 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<String>, 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<Theme> = LazyLock::new(|| { + toml::from_str(DARK_THEME_CONTENT).expect("parse dark theme") +}); + +pub static LIGHT: LazyLock<Theme> = 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<Color> { + 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<Color, D::Error> + 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<dyn std::error::Error>> { let version = std::env::args() .nth(1) @@ -60,7 +58,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { 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<usize> { 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<OtherTheme> = LazyLock::new(|| { - toml::from_str(DARK_THEME_CONTENT).expect("parse dark theme") -}); - -pub static LIGHT: LazyLock<OtherTheme> = 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/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() +} |
