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 --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) (limited to 'Cargo.toml') diff --git a/Cargo.toml b/Cargo.toml index d646fd7..39e69af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ rust-format = "0.3.4" fxhash = "0.2.1" thiserror = "2.0.11" dirs-next = "2.0.0" +dark-light = "2.0.0" [workspace.dependencies.iced] git = "https://github.com/pml68/iced" -- 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 'Cargo.toml') 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 c6a76e63604b16b9a14d5de6bd0ea91eea7f9bf2 Mon Sep 17 00:00:00 2001 From: pml68 Date: Tue, 8 Apr 2025 01:13:38 +0200 Subject: feat(material_theme): implement Catalog for iced_dialog (`dialog` feature) --- Cargo.lock | 2 ++ Cargo.toml | 3 +- material_theme/Cargo.toml | 3 ++ material_theme/src/lib.rs | 74 ++++++++++++++++++++++++++--------------------- src/main.rs | 17 ++++++----- src/panes/element_list.rs | 11 +++---- 6 files changed, 61 insertions(+), 49 deletions(-) (limited to 'Cargo.toml') diff --git a/Cargo.lock b/Cargo.lock index 2dc8556..b2ab721 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2678,6 +2678,8 @@ name = "material_theme" version = "0.14.0-dev" dependencies = [ "dark-light", + "iced_anim", + "iced_dialog", "iced_widget", "serde", "toml", diff --git a/Cargo.toml b/Cargo.toml index 33d3b8e..63e5f89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ iced.workspace = true 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" } +iced_dialog.workspace = true material_theme = { path = "material_theme" } serde.workspace = true serde_json = "1.0.140" @@ -36,6 +36,7 @@ dirs-next = "2.0.0" [workspace.dependencies] iced_anim = { version = "0.2.1", features = ["derive"] } +iced_dialog = { git = "https://github.com/pml68/iced_dialog", branch = "iced/personal" } serde = { version = "1.0.219", features = ["derive"] } toml = "0.8.20" diff --git a/material_theme/Cargo.toml b/material_theme/Cargo.toml index 5a48dc0..e88ce92 100644 --- a/material_theme/Cargo.toml +++ b/material_theme/Cargo.toml @@ -14,12 +14,15 @@ rust-version = "1.85" [features] default = [] animate = ["dep:iced_anim"] +dialog = ["dep:iced_dialog"] [dependencies] iced_widget = "0.14.0-dev" serde.workspace = true toml.workspace = true dark-light = "2.0.0" +iced_dialog.workspace = true +iced_dialog.optional = true [dependencies.iced_anim] workspace = true diff --git a/material_theme/src/lib.rs b/material_theme/src/lib.rs index 930e511..87cf353 100644 --- a/material_theme/src/lib.rs +++ b/material_theme/src/lib.rs @@ -28,31 +28,6 @@ impl Theme { } } -#[cfg(feature = "animate")] -impl iced_anim::Animate for Theme { - fn components() -> usize { - ColorScheme::components() - } - - fn update(&mut self, components: &mut impl Iterator) { - let mut colors = self.colorscheme; - colors.update(components); - - *self = Theme::new("Animating Theme", colors); - } - - fn distance_to(&self, end: &Self) -> Vec { - self.colorscheme.distance_to(&end.colorscheme) - } - - fn lerp(&mut self, start: &Self, end: &Self, progress: f32) { - let mut colors = self.colorscheme; - colors.lerp(&start.colorscheme, &end.colorscheme, progress); - - *self = Theme::new("Animating Theme", colors); - } -} - impl Clone for Theme { fn clone(&self) -> Self { Self { @@ -92,6 +67,47 @@ impl Base for Theme { } } +#[cfg(feature = "animate")] +impl iced_anim::Animate for Theme { + fn components() -> usize { + ColorScheme::components() + } + + fn update(&mut self, components: &mut impl Iterator) { + let mut colors = self.colorscheme; + colors.update(components); + + *self = Theme::new("Animating Theme", colors); + } + + fn distance_to(&self, end: &Self) -> Vec { + self.colorscheme.distance_to(&end.colorscheme) + } + + fn lerp(&mut self, start: &Self, end: &Self, progress: f32) { + let mut colors = self.colorscheme; + colors.lerp(&start.colorscheme, &end.colorscheme, progress); + + *self = Theme::new("Animating Theme", colors); + } +} + +#[cfg(feature = "dialog")] +impl iced_dialog::dialog::Catalog for Theme { + fn default_container<'a>() + -> ::Class<'a> { + Box::new(container::surface) + } +} + +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)] #[cfg_attr(feature = "animate", derive(iced_anim::Animate))] pub struct ColorScheme { @@ -106,14 +122,6 @@ pub struct ColorScheme { 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)] #[cfg_attr(feature = "animate", derive(iced_anim::Animate))] pub struct Primary { diff --git a/src/main.rs b/src/main.rs index 6ab4da9..3895dbc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,8 +31,7 @@ use iced_dialog::dialog::Dialog; use panes::{code_view, designer_view, element_list}; use tokio::runtime; use types::{ - Action, DesignerPane, DialogAction, DialogButtons, ElementName, Message, - Project, + Action, DesignerPane, DialogAction, DialogButtons, Message, Project, }; fn main() -> Result<(), Box> { @@ -79,7 +78,6 @@ struct App { dialog_content: String, dialog_buttons: DialogButtons, dialog_action: DialogAction, - element_list: &'static [ElementName], editor_content: text_editor::Content, } @@ -132,7 +130,6 @@ impl App { dialog_content: String::new(), dialog_buttons: DialogButtons::None, dialog_action: DialogAction::None, - element_list: ElementName::ALL, editor_content: text_editor::Content::new(), }, task, @@ -408,9 +405,7 @@ impl App { code_view::view(&self.editor_content, is_focused) } }, - Panes::ElementList => { - element_list::view(self.element_list, is_focused) - } + Panes::ElementList => element_list::view(is_focused), } }, ) @@ -438,7 +433,13 @@ impl App { DialogButtons::OkCancel => vec![ok_button(), cancel_button()], }, ) - .title(self.dialog_title); + .title(self.dialog_title) + .container_style(|theme| container::Style { + background: Some( + theme.extended_palette().background.strong.color.into(), + ), + ..Default::default() + }); Animation::new(&self.theme, content) .on_update(Message::SwitchTheme) diff --git a/src/panes/element_list.rs b/src/panes/element_list.rs index 10eea66..594c203 100644 --- a/src/panes/element_list.rs +++ b/src/panes/element_list.rs @@ -5,13 +5,13 @@ use iced_drop::droppable; use super::style; use crate::types::{ElementName, Message}; -fn items_list_view(items: &[ElementName]) -> Element<'_, Message> { +fn items_list_view<'a>() -> Element<'a, Message> { let mut column = Column::new() .spacing(20) .align_x(Alignment::Center) .width(Length::Fill); - for item in items { + for item in ElementName::ALL { column = column.push( droppable(text(item.clone().to_string())).on_drop(|point, rect| { Message::DropNewElement(item.clone(), point, rect) @@ -25,11 +25,8 @@ fn items_list_view(items: &[ElementName]) -> Element<'_, Message> { .into() } -pub fn view( - element_list: &[ElementName], - is_focused: bool, -) -> pane_grid::Content<'_, Message> { - let items_list = items_list_view(element_list); +pub fn view<'a>(is_focused: bool) -> pane_grid::Content<'a, Message> { + let items_list = items_list_view(); let content = column![items_list] .align_x(Alignment::Center) .height(Length::Fill) -- cgit v1.2.3 From 941eb51e043b6b847089130625c2df10b0674154 Mon Sep 17 00:00:00 2001 From: pml68 Date: Wed, 9 Apr 2025 00:29:34 +0200 Subject: feat: update `iced`, make designer view more usable --- Cargo.lock | 252 +++++++++++++++++++++++------------------- Cargo.toml | 4 +- src/main.rs | 18 ++- src/panes/designer_view.rs | 19 +++- src/types/rendered_element.rs | 42 ++++--- 5 files changed, 193 insertions(+), 142 deletions(-) (limited to 'Cargo.toml') diff --git a/Cargo.lock b/Cargo.lock index b2ab721..a324ccd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,6 +57,12 @@ dependencies = [ "zerocopy 0.7.35", ] +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + [[package]] name = "aligned-vec" version = "0.5.0" @@ -105,15 +111,6 @@ version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" -[[package]] -name = "approx" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" -dependencies = [ - "num-traits", -] - [[package]] name = "arbitrary" version = "1.4.1" @@ -569,12 +566,6 @@ version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" -[[package]] -name = "by_address" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" - [[package]] name = "bytemuck" version = "1.22.0" @@ -1316,12 +1307,6 @@ dependencies = [ "zune-inflate", ] -[[package]] -name = "fast-srgb8" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" - [[package]] name = "fastrand" version = "2.3.0" @@ -1767,9 +1752,9 @@ dependencies = [ [[package]] name = "half" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ "cfg-if", "crunchy", @@ -1784,6 +1769,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -1938,11 +1929,14 @@ dependencies = [ [[package]] name = "iced" version = "0.14.0-dev" -source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#1a84a8019cbd95ae1b1c88f75aeb0a15f0f3caf2" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#add48e518b3423bed980983ddc373cfb8f5e8980" dependencies = [ "iced_core", + "iced_debug", + "iced_devtools", "iced_futures", "iced_renderer", + "iced_runtime", "iced_widget", "iced_winit", "image", @@ -1967,6 +1961,21 @@ dependencies = [ "syn", ] +[[package]] +name = "iced_beacon" +version = "0.14.0-dev" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#add48e518b3423bed980983ddc373cfb8f5e8980" +dependencies = [ + "bincode", + "futures", + "iced_core", + "log", + "semver", + "serde", + "thiserror 1.0.69", + "tokio", +] + [[package]] name = "iced_builder" version = "0.1.0" @@ -1996,7 +2005,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.14.0-dev" -source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#1a84a8019cbd95ae1b1c88f75aeb0a15f0f3caf2" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#add48e518b3423bed980983ddc373cfb8f5e8980" dependencies = [ "bitflags 2.9.0", "bytes", @@ -2005,8 +2014,8 @@ dependencies = [ "lilt", "log", "num-traits", - "palette", "rustc-hash 2.1.1", + "serde", "smol_str", "thiserror 1.0.69", "web-time", @@ -2022,6 +2031,26 @@ dependencies = [ "two-face", ] +[[package]] +name = "iced_debug" +version = "0.14.0-dev" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#add48e518b3423bed980983ddc373cfb8f5e8980" +dependencies = [ + "iced_beacon", + "iced_core", + "log", +] + +[[package]] +name = "iced_devtools" +version = "0.14.0-dev" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#add48e518b3423bed980983ddc373cfb8f5e8980" +dependencies = [ + "iced_debug", + "iced_program", + "iced_widget", +] + [[package]] name = "iced_dialog" version = "0.14.0-dev" @@ -2055,7 +2084,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.14.0-dev" -source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#1a84a8019cbd95ae1b1c88f75aeb0a15f0f3caf2" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#add48e518b3423bed980983ddc373cfb8f5e8980" dependencies = [ "futures", "iced_core", @@ -2069,7 +2098,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.14.0-dev" -source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#1a84a8019cbd95ae1b1c88f75aeb0a15f0f3caf2" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#add48e518b3423bed980983ddc373cfb8f5e8980" dependencies = [ "bitflags 2.9.0", "bytemuck", @@ -2086,10 +2115,19 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "iced_program" +version = "0.14.0-dev" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#add48e518b3423bed980983ddc373cfb8f5e8980" +dependencies = [ + "iced_graphics", + "iced_runtime", +] + [[package]] name = "iced_renderer" version = "0.14.0-dev" -source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#1a84a8019cbd95ae1b1c88f75aeb0a15f0f3caf2" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#add48e518b3423bed980983ddc373cfb8f5e8980" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -2101,20 +2139,20 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.14.0-dev" -source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#1a84a8019cbd95ae1b1c88f75aeb0a15f0f3caf2" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#add48e518b3423bed980983ddc373cfb8f5e8980" dependencies = [ "bytes", "iced_core", + "iced_debug", "iced_futures", "raw-window-handle", - "sipper", "thiserror 1.0.69", ] [[package]] name = "iced_tiny_skia" version = "0.14.0-dev" -source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#1a84a8019cbd95ae1b1c88f75aeb0a15f0f3caf2" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#add48e518b3423bed980983ddc373cfb8f5e8980" dependencies = [ "bytemuck", "cosmic-text", @@ -2130,7 +2168,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.14.0-dev" -source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#1a84a8019cbd95ae1b1c88f75aeb0a15f0f3caf2" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#add48e518b3423bed980983ddc373cfb8f5e8980" dependencies = [ "bitflags 2.9.0", "bytemuck", @@ -2149,12 +2187,13 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.14.0-dev" -source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#1a84a8019cbd95ae1b1c88f75aeb0a15f0f3caf2" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#add48e518b3423bed980983ddc373cfb8f5e8980" dependencies = [ "iced_renderer", "iced_runtime", "log", "num-traits", + "ouroboros", "rustc-hash 2.1.1", "thiserror 1.0.69", "unicode-segmentation", @@ -2163,11 +2202,10 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.14.0-dev" -source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#1a84a8019cbd95ae1b1c88f75aeb0a15f0f3caf2" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#add48e518b3423bed980983ddc373cfb8f5e8980" dependencies = [ - "iced_futures", - "iced_graphics", - "iced_runtime", + "iced_debug", + "iced_program", "log", "rustc-hash 2.1.1", "thiserror 1.0.69", @@ -2758,9 +2796,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", "simd-adler32", @@ -3347,36 +3385,36 @@ dependencies = [ ] [[package]] -name = "owned_ttf_parser" -version = "0.25.0" +name = "ouroboros" +version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4" +checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" dependencies = [ - "ttf-parser 0.25.1", + "aliasable", + "ouroboros_macro", + "static_assertions", ] [[package]] -name = "palette" -version = "0.7.6" +name = "ouroboros_macro" +version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" dependencies = [ - "approx", - "fast-srgb8", - "palette_derive", - "phf", + "heck 0.4.1", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", ] [[package]] -name = "palette_derive" -version = "0.7.6" +name = "owned_ttf_parser" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4" dependencies = [ - "by_address", - "proc-macro2", - "quote", - "syn", + "ttf-parser 0.25.1", ] [[package]] @@ -3430,48 +3468,6 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_macros", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared", - "rand 0.8.5", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", -] - [[package]] name = "pico-args" version = "0.5.0" @@ -3600,6 +3596,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", + "yansi", +] + [[package]] name = "profiling" version = "1.0.16" @@ -4176,6 +4185,9 @@ name = "semver" version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +dependencies = [ + "serde", +] [[package]] name = "serde" @@ -4308,16 +4320,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" -[[package]] -name = "sipper" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bccb4192828b3d9a08e0b5a73f17795080dfb278b50190216e3ae2132cf4f95" -dependencies = [ - "futures", - "pin-project-lite", -] - [[package]] name = "skrifa" version = "0.26.6" @@ -4484,7 +4486,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "rustversion", @@ -4612,7 +4614,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" dependencies = [ "cfg-expr", - "heck", + "heck 0.5.0", "pkg-config", "toml", "version-compare", @@ -4790,9 +4792,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.44.1" +version = "1.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" dependencies = [ "backtrace", "bytes", @@ -4800,9 +4802,21 @@ dependencies = [ "mio", "pin-project-lite", "socket2", + "tokio-macros", "windows-sys 0.52.0", ] +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio-native-tls" version = "0.3.1" @@ -6029,9 +6043,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" +checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" dependencies = [ "memchr", ] @@ -6161,6 +6175,12 @@ dependencies = [ "lzma-sys", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yazi" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 63e5f89..e48a39c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ material_theme = { path = "material_theme" } serde.workspace = true serde_json = "1.0.140" toml.workspace = true -tokio = { version = "1.42.1", features = ["fs"] } +tokio = { version = "1.44.2", 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"] } @@ -43,7 +43,7 @@ toml = "0.8.20" [workspace.dependencies.iced] git = "https://github.com/pml68/iced" branch = "feat/rehighlight-on-redraw" -features = ["image", "svg", "advanced", "tokio"] +features = ["image", "svg", "advanced", "tokio", "lazy"] [build-dependencies] iced_fontello = "0.13.2" diff --git a/src/main.rs b/src/main.rs index 3895dbc..d58329a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -54,12 +54,18 @@ fn main() -> Result<(), Box> { rt.block_on(Config::load()) }; - iced::application(App::title, App::update, App::view) - .font(icon::FONT) - .theme(|state| state.theme.value().clone()) - .subscription(App::subscription) - .antialiasing(true) - .run_with(move || App::new(config_load))?; + iced::application( + move || App::new(config_load.clone()), + App::update, + App::view, + ) + .title(App::title) + .font(icon::FONT) + .theme(|state| state.theme.value().clone()) + .subscription(App::subscription) + .antialiasing(true) + .run()?; + Ok(()) } diff --git a/src/panes/designer_view.rs b/src/panes/designer_view.rs index 6340f73..69ff750 100644 --- a/src/panes/designer_view.rs +++ b/src/panes/designer_view.rs @@ -1,16 +1,29 @@ -use iced::widget::{Space, button, container, pane_grid, row, text, themer}; +use iced::widget::{ + Space, button, center, container, pane_grid, responsive, row, text, themer, +}; use iced::{Alignment, Element, Length}; use super::style; use crate::types::{DesignerPane, Message, RenderedElement}; pub fn view<'a>( - element_tree: Option<&RenderedElement>, + 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 { - Some(tree) => tree.clone().into(), + Some(tree) => responsive(|size| { + center( + container(tree.clone()) + .style(|theme| { + container::background(theme.palette().background) + }) + .height(size.height * 0.5) + .width(size.height * 0.8), + ) + .into() + }) + .into(), None => text("Open a project or begin creating one").into(), }; let content = container(themer(designer_theme, el_tree)) diff --git a/src/types/rendered_element.rs b/src/types/rendered_element.rs index 77b76e4..bd8187e 100755 --- a/src/types/rendered_element.rs +++ b/src/types/rendered_element.rs @@ -340,15 +340,19 @@ impl<'a> From for Element<'a, Message> { ElementName::Container => if child_elements.len() == 1 { widget::container(child_elements[0].clone()) } else { - widget::container("New Container") - .padding(20) - .style(|theme| widget::container::Style { - border: iced::border::rounded(4).color( - theme.extended_palette().background.strongest.text, - ), + widget::container("New Container").style( + |theme: &iced::Theme| widget::container::Style { + border: iced::Border { + color: theme.palette().text, + + width: 2.0, + radius: 4.into(), + }, ..Default::default() - }) + }, + ) } + .padding(20) .apply_options(copy.options) .into(), ElementName::Row => { @@ -356,6 +360,7 @@ impl<'a> From for Element<'a, Message> { widget::Row::with_children( child_elements.into_iter().map(Into::into), ) + .padding(20) .apply_options(copy.options) .into() } else { @@ -364,10 +369,13 @@ impl<'a> From for Element<'a, Message> { .padding(20) .apply_options(copy.options), ) - .style(|theme| widget::container::Style { - border: iced::border::rounded(4).color( - theme.extended_palette().background.strongest.text, - ), + .style(|theme: &iced::Theme| widget::container::Style { + border: iced::Border { + color: theme.palette().text, + + width: 2.0, + radius: 4.into(), + }, ..Default::default() }) .into() @@ -378,6 +386,7 @@ impl<'a> From for Element<'a, Message> { widget::Column::with_children( child_elements.into_iter().map(Into::into), ) + .padding(20) .apply_options(copy.options) .into() } else { @@ -386,10 +395,13 @@ impl<'a> From for Element<'a, Message> { .padding(20) .apply_options(copy.options), ) - .style(|theme| widget::container::Style { - border: iced::border::rounded(4).color( - theme.extended_palette().background.strongest.text, - ), + .style(|theme: &iced::Theme| widget::container::Style { + border: iced::Border { + color: theme.palette().text, + + width: 2.0, + radius: 4.into(), + }, ..Default::default() }) .into() -- cgit v1.2.3 From 495985f449e46b24e6b734d3aa9e135a779a8b77 Mon Sep 17 00:00:00 2001 From: pml68 Date: Sun, 13 Apr 2025 03:40:38 +0200 Subject: refactor: move `material_theme` and `iced_drop` into separate crates dir --- Cargo.toml | 7 +- crates/iced_drop/Cargo.toml | 7 + crates/iced_drop/LICENSE | 21 + crates/iced_drop/README.md | 73 ++++ crates/iced_drop/src/lib.rs | 55 +++ crates/iced_drop/src/widget.rs | 2 + crates/iced_drop/src/widget/droppable.rs | 574 +++++++++++++++++++++++++ crates/iced_drop/src/widget/operation.rs | 1 + crates/iced_drop/src/widget/operation/drop.rs | 88 ++++ crates/material_theme/Cargo.toml | 53 +++ crates/material_theme/README.md | 3 + crates/material_theme/assets/themes/dark.toml | 49 +++ crates/material_theme/assets/themes/light.toml | 49 +++ crates/material_theme/src/button.rs | 193 +++++++++ crates/material_theme/src/container.rs | 173 ++++++++ crates/material_theme/src/dialog.rs | 25 ++ crates/material_theme/src/lib.rs | 248 +++++++++++ crates/material_theme/src/menu.rs | 33 ++ crates/material_theme/src/pick_list.rs | 40 ++ crates/material_theme/src/scrollable.rs | 153 +++++++ crates/material_theme/src/text.rs | 86 ++++ crates/material_theme/src/utils.rs | 116 +++++ iced_drop/Cargo.toml | 7 - iced_drop/LICENSE | 21 - iced_drop/README.md | 73 ---- iced_drop/src/lib.rs | 55 --- iced_drop/src/widget.rs | 2 - iced_drop/src/widget/droppable.rs | 574 ------------------------- iced_drop/src/widget/operation.rs | 1 - iced_drop/src/widget/operation/drop.rs | 88 ---- material_theme/Cargo.toml | 53 --- material_theme/README.md | 3 - material_theme/assets/themes/dark.toml | 49 --- material_theme/assets/themes/light.toml | 49 --- material_theme/src/button.rs | 193 --------- material_theme/src/container.rs | 173 -------- material_theme/src/dialog.rs | 25 -- material_theme/src/lib.rs | 248 ----------- material_theme/src/menu.rs | 33 -- material_theme/src/pick_list.rs | 40 -- material_theme/src/scrollable.rs | 161 ------- material_theme/src/text.rs | 86 ---- material_theme/src/utils.rs | 116 ----- theme_test/Cargo.toml | 2 +- 44 files changed, 2047 insertions(+), 2054 deletions(-) create mode 100644 crates/iced_drop/Cargo.toml create mode 100644 crates/iced_drop/LICENSE create mode 100644 crates/iced_drop/README.md create mode 100644 crates/iced_drop/src/lib.rs create mode 100644 crates/iced_drop/src/widget.rs create mode 100644 crates/iced_drop/src/widget/droppable.rs create mode 100644 crates/iced_drop/src/widget/operation.rs create mode 100644 crates/iced_drop/src/widget/operation/drop.rs create mode 100644 crates/material_theme/Cargo.toml create mode 100644 crates/material_theme/README.md create mode 100644 crates/material_theme/assets/themes/dark.toml create mode 100644 crates/material_theme/assets/themes/light.toml create mode 100644 crates/material_theme/src/button.rs create mode 100644 crates/material_theme/src/container.rs create mode 100644 crates/material_theme/src/dialog.rs create mode 100644 crates/material_theme/src/lib.rs create mode 100644 crates/material_theme/src/menu.rs create mode 100644 crates/material_theme/src/pick_list.rs create mode 100644 crates/material_theme/src/scrollable.rs create mode 100644 crates/material_theme/src/text.rs create mode 100644 crates/material_theme/src/utils.rs delete mode 100644 iced_drop/Cargo.toml delete mode 100644 iced_drop/LICENSE delete mode 100644 iced_drop/README.md delete mode 100644 iced_drop/src/lib.rs delete mode 100644 iced_drop/src/widget.rs delete mode 100644 iced_drop/src/widget/droppable.rs delete mode 100644 iced_drop/src/widget/operation.rs delete mode 100644 iced_drop/src/widget/operation/drop.rs delete mode 100644 material_theme/Cargo.toml delete mode 100644 material_theme/README.md delete mode 100644 material_theme/assets/themes/dark.toml delete mode 100644 material_theme/assets/themes/light.toml delete mode 100644 material_theme/src/button.rs delete mode 100644 material_theme/src/container.rs delete mode 100644 material_theme/src/dialog.rs delete mode 100644 material_theme/src/lib.rs delete mode 100644 material_theme/src/menu.rs delete mode 100644 material_theme/src/pick_list.rs delete mode 100644 material_theme/src/scrollable.rs delete mode 100644 material_theme/src/text.rs delete mode 100644 material_theme/src/utils.rs (limited to 'Cargo.toml') diff --git a/Cargo.toml b/Cargo.toml index e48a39c..1c0984b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,9 +18,9 @@ debug = ["iced/debug"] iced.workspace = true iced_anim.workspace = true iced_custom_highlighter = { git = "https://github.com/pml68/iced_custom_highlighter", branch = "master" } -iced_drop = { path = "iced_drop" } +iced_drop = { path = "crates/iced_drop" } iced_dialog.workspace = true -material_theme = { path = "material_theme" } +material_theme = { path = "crates/material_theme" } serde.workspace = true serde_json = "1.0.140" toml.workspace = true @@ -74,7 +74,8 @@ name = "iced-builder" path = "src/main.rs" [workspace] -members = ["iced_drop", "material_theme", "theme_test"] +members = ["crates/*", "theme_test"] +default-members = ["crates/material_theme", "."] [lints.rust] missing_debug_implementations = "deny" diff --git a/crates/iced_drop/Cargo.toml b/crates/iced_drop/Cargo.toml new file mode 100644 index 0000000..0692084 --- /dev/null +++ b/crates/iced_drop/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "iced_drop" +version = "0.1.0" +edition = "2021" + +[dependencies] +iced.workspace = true diff --git a/crates/iced_drop/LICENSE b/crates/iced_drop/LICENSE new file mode 100644 index 0000000..89d9fee --- /dev/null +++ b/crates/iced_drop/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 jhannyj + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/iced_drop/README.md b/crates/iced_drop/README.md new file mode 100644 index 0000000..be854f2 --- /dev/null +++ b/crates/iced_drop/README.md @@ -0,0 +1,73 @@ +# iced_drop - updated to `iced` 0.14-dev + +A small library which provides a custom widget and operation to make drag and drop easier to implement in [iced](https://github.com/iced-rs/iced/tree/master) + +## Usage + +To add drag and drog functionality, first define two messages with the following format + +```rust +enum Message { + Drop(iced::Point, iced::Rectangle) + HandleZones(Vec<(iced::advanced::widget::Id, iced::Rectangle)>) +} +``` + +The `Drop` message will be sent when the droppable is being dragged, and the left mouse button is released. This message provides the mouse position and layout boundaries of the droppable at the release point. + +The `HandleZones` message will be sent after an operation that finds the drop zones under the mouse position. It provides the Id and bounds for each drop zone. + +Next, create create a droppable in the view method and assign the on_drop message. The dropopable function takes an `impl Into` object, so it's easy to make a droppable from any iced widget. + +```rust +iced_drop::droppable("Drop me!").on_drop(Message::Drop); +``` + +Next, create a "drop zone." A drop zone is any widget that operates like a container andhas some assigned Id. It's important that the widget is assigned some Id or it won't be recognized as a drop zone. + +```rust +iced::widget::container("Drop zone") + .id(iced::widget::container::Id::new("drop_zone")); +``` + +Finally, handle the updates of the drop messages + +```rust +match message { + Message::Drop(cursor_pos, _) => { + return iced_drop::zones_on_point( + Message::HandleZonesFound, + point, + None, + None, + ); + } + Message::HandleZones(zones) => { + println!("{:?}", zones) + } +} +``` + +On Drop, we return a widget operation that looks for drop zones under the cursor_pos. When this operation finishes, it returns the zones found and sends the `HandleZones` message. In this example, we only defined one zone, so the zones vector will either be empty if the droppable was not dropped on the zone, or it will contain the `drop_zone` + +## Examples + +There are two examples: color, todo. + +The color example is a very basic drag/drop showcase where the user can drag colors into zones and change the zone's color. I would start here. + +[Link to video](https://drive.google.com/file/d/1K1CCi2Lc90IUyDufsvoUBZmUCbeg6_Fi/view?usp=sharing) + +To run this examples: `cargo run -p color` + +The todo example is a basic todo board application similar to Trello. This is a much much more complex example as it handles custom highlighting and nested droppables, but it just shows you can make some pretty cool things with iced. + +[Link to video](https://drive.google.com/file/d/1MLOCk4Imd_oUnrTj_psbpYbwua976HmR/view?usp=sharing) + +To run this example try: `cargo run -p todo` + +Note: the todo example might also be a good example on how one can use operations. Check examples/todo/src/operation.rs. I didn't find any other examples of this in the iced repo except for the built in focus operations. + +## Future Development + +Right now it's a little annoying having to work with iced's Id type. At some point, I will work on a drop_zone widget that can take some generic clonable type as an id, and I will create a seperate find_zones operation that will return a list of this custom Id. This should make it easier to determine which drop zones were found. diff --git a/crates/iced_drop/src/lib.rs b/crates/iced_drop/src/lib.rs new file mode 100644 index 0000000..c1e1b03 --- /dev/null +++ b/crates/iced_drop/src/lib.rs @@ -0,0 +1,55 @@ +pub mod widget; + +use iced::advanced::graphics::futures::MaybeSend; +use iced::advanced::renderer; +use iced::advanced::widget::{operate, Id}; +use iced::task::Task; +use iced::{Element, Point, Rectangle}; +use widget::droppable::*; +use widget::operation::drop; + +pub fn droppable<'a, Message, Theme, Renderer>( + content: impl Into>, +) -> Droppable<'a, Message, Theme, Renderer> +where + Message: Clone, + Renderer: renderer::Renderer, +{ + Droppable::new(content) +} + +pub fn zones_on_point( + msg: MF, + point: Point, + options: Option>, + depth: Option, +) -> Task +where + T: Send + 'static, + MF: Fn(Vec<(Id, Rectangle)>) -> T + MaybeSend + Sync + Clone + 'static, +{ + operate(drop::find_zones( + move |bounds| bounds.contains(point), + options, + depth, + )) + .map(move |id| msg(id)) +} + +pub fn find_zones( + msg: MF, + filter: F, + options: Option>, + depth: Option, +) -> Task +where + Message: Send + 'static, + MF: Fn(Vec<(Id, Rectangle)>) -> Message + + MaybeSend + + Sync + + Clone + + 'static, + F: Fn(&Rectangle) -> bool + Send + 'static, +{ + operate(drop::find_zones(filter, options, depth)).map(move |id| msg(id)) +} diff --git a/crates/iced_drop/src/widget.rs b/crates/iced_drop/src/widget.rs new file mode 100644 index 0000000..6b3fed2 --- /dev/null +++ b/crates/iced_drop/src/widget.rs @@ -0,0 +1,2 @@ +pub mod droppable; +pub mod operation; diff --git a/crates/iced_drop/src/widget/droppable.rs b/crates/iced_drop/src/widget/droppable.rs new file mode 100644 index 0000000..947cf5b --- /dev/null +++ b/crates/iced_drop/src/widget/droppable.rs @@ -0,0 +1,574 @@ +//! Encapsulates a widget that can be dragged and dropped. +use std::fmt::Debug; +use std::vec; + +use iced::advanced::widget::{Operation, Tree, Widget}; +use iced::advanced::{self, Layout, layout, mouse, overlay, renderer}; +use iced::{Element, Point, Rectangle, Size, Vector}; + +/// An element that can be dragged and dropped on a [`DropZone`] +pub struct Droppable< + 'a, + Message, + Theme = iced::Theme, + Renderer = iced::Renderer, +> where + Message: Clone, + Renderer: renderer::Renderer, +{ + content: Element<'a, Message, Theme, Renderer>, + id: Option, + on_click: Option, + on_drop: Option Message + 'a>>, + on_drag: Option Message + 'a>>, + on_cancel: Option, + drag_mode: Option<(bool, bool)>, + drag_overlay: bool, + drag_hide: bool, + drag_center: bool, + drag_size: Option, + reset_delay: usize, + status: Option, +} + +impl<'a, Message, Theme, Renderer> Droppable<'a, Message, Theme, Renderer> +where + Message: Clone, + Renderer: renderer::Renderer, +{ + /// Creates a new [`Droppable`]. + pub fn new( + content: impl Into>, + ) -> Self { + Self { + content: content.into(), + id: None, + on_click: None, + on_drop: None, + on_drag: None, + on_cancel: None, + drag_mode: Some((true, true)), + drag_overlay: true, + drag_hide: false, + drag_center: false, + drag_size: None, + reset_delay: 0, + status: None, + } + } + + /// Sets the unique identifier of the [`Droppable`]. + pub fn id(mut self, id: iced::advanced::widget::Id) -> Self { + self.id = Some(id); + self + } + + /// Sets the message that will be produced when the [`Droppable`] is clicked. + pub fn on_click(mut self, message: Message) -> Self { + self.on_click = Some(message); + self + } + + /// Sets the message that will be produced when the [`Droppable`] is dropped on a [`DropZone`]. + /// + /// Unless this is set, the [`Droppable`] will be disabled. + pub fn on_drop(mut self, message: F) -> Self + where + F: Fn(Point, Rectangle) -> Message + 'a, + { + self.on_drop = Some(Box::new(message)); + self + } + + /// Sets the message that will be produced when the [`Droppable`] is dragged. + pub fn on_drag(mut self, message: F) -> Self + where + F: Fn(Point, Rectangle) -> Message + 'a, + { + self.on_drag = Some(Box::new(message)); + self + } + + /// Sets the message that will be produced when the user right clicks while dragging the [`Droppable`]. + pub fn on_cancel(mut self, message: Message) -> Self { + self.on_cancel = Some(message); + self + } + + /// Sets whether the [`Droppable`] should be drawn under the cursor while dragging. + pub fn drag_overlay(mut self, drag_overlay: bool) -> Self { + self.drag_overlay = drag_overlay; + self + } + + /// Sets whether the [`Droppable`] should be hidden while dragging. + pub fn drag_hide(mut self, drag_hide: bool) -> Self { + self.drag_hide = drag_hide; + self + } + + /// Sets whether the [`Droppable`] should be centered on the cursor while dragging. + pub fn drag_center(mut self, drag_center: bool) -> Self { + self.drag_center = drag_center; + self + } + + // Sets whether the [`Droppable`] can be dragged along individual axes. + pub fn drag_mode(mut self, drag_x: bool, drag_y: bool) -> Self { + self.drag_mode = Some((drag_x, drag_y)); + self + } + + /// Sets whether the [`Droppable`] should be be resized to a given size while dragging. + pub fn drag_size(mut self, hide_size: Size) -> Self { + self.drag_size = Some(hide_size); + self + } + + /// Sets the number of frames/layout calls to wait before resetting the size of the [`Droppable`] after dropping. + /// + /// This is useful for cases where the [`Droppable`] is being moved to a new location after some widget operation. + /// In this case, the [`Droppable`] will mainting the 'drag_size' for the given number of frames before resetting to its original size. + /// This prevents the [`Droppable`] from 'jumping' back to its original size before the new location is rendered which + /// prevents flickering. + /// + /// Warning: this should only be set if there's is some noticeble flickering when the [`Droppable`] is dropped. That is, if the + /// [`Droppable`] returns to its original size before it's moved to it's new location. + pub fn reset_delay(mut self, reset_delay: usize) -> Self { + self.reset_delay = reset_delay; + self + } +} + +impl<'a, Message, Theme, Renderer> Widget + for Droppable<'a, Message, Theme, Renderer> +where + Message: Clone, + Renderer: renderer::Renderer, +{ + fn state(&self) -> iced::advanced::widget::tree::State { + advanced::widget::tree::State::new(State::default()) + } + + fn tag(&self) -> iced::advanced::widget::tree::Tag { + advanced::widget::tree::Tag::of::() + } + + fn children(&self) -> Vec { + vec![advanced::widget::Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut iced::advanced::widget::Tree) { + tree.diff_children(std::slice::from_ref(&self.content)) + } + + fn size(&self) -> iced::Size { + self.content.as_widget().size() + } + + fn update( + &mut self, + tree: &mut iced::advanced::widget::Tree, + event: &iced::Event, + layout: iced::advanced::Layout<'_>, + cursor: iced::advanced::mouse::Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn iced::advanced::Clipboard, + shell: &mut iced::advanced::Shell<'_, Message>, + _viewport: &iced::Rectangle, + ) { + // handle the on event of the content first, in case that the droppable is nested + self.content.as_widget_mut().update( + &mut tree.children[0], + event, + layout, + cursor, + _renderer, + _clipboard, + shell, + _viewport, + ); + // this should really only be captured if the droppable is nested or it contains some other + // widget that captures the event + if shell.is_event_captured() { + return; + } + + if let Some(on_drop) = self.on_drop.as_deref() { + let state = tree.state.downcast_mut::(); + if let iced::Event::Mouse(mouse) = event { + match mouse { + mouse::Event::ButtonPressed(btn) => { + if *btn == mouse::Button::Left + && cursor.is_over(layout.bounds()) + { + // select the droppable and store the position of the widget before dragging + state.action = + Action::Select(cursor.position().unwrap()); + let bounds = layout.bounds(); + state.widget_pos = bounds.position(); + state.overlay_bounds.width = bounds.width; + state.overlay_bounds.height = bounds.height; + + if let Some(on_click) = self.on_click.clone() { + shell.publish(on_click); + } + shell.capture_event(); + } else if *btn == mouse::Button::Right { + if let Action::Drag(_, _) = state.action { + shell.invalidate_layout(); + state.action = Action::None; + if let Some(on_cancel) = self.on_cancel.clone() + { + shell.publish(on_cancel); + } + } + } + } + mouse::Event::CursorMoved { mut position } => match state + .action + { + Action::Select(start) | Action::Drag(start, _) => { + // calculate the new position of the widget after dragging + + if let Some((drag_x, drag_y)) = self.drag_mode { + position = Point { + x: if drag_x { + position.x + } else { + start.x + }, + y: if drag_y { + position.y + } else { + start.y + }, + }; + } + + state.action = Action::Drag(start, position); + // update the position of the overlay since the cursor was moved + if self.drag_center { + state.overlay_bounds.x = position.x + - state.overlay_bounds.width / 2.0; + state.overlay_bounds.y = position.y + - state.overlay_bounds.height / 2.0; + } else { + state.overlay_bounds.x = + state.widget_pos.x + position.x - start.x; + state.overlay_bounds.y = + state.widget_pos.y + position.y - start.y; + } + // send on drag msg + if let Some(on_drag) = self.on_drag.as_deref() { + let message = + (on_drag)(position, state.overlay_bounds); + shell.publish(message); + } + + shell.request_redraw(); + } + _ => (), + }, + mouse::Event::ButtonReleased(mouse::Button::Left) => { + match state.action { + Action::Select(_) => { + state.action = Action::None; + } + Action::Drag(_, current) => { + // send on drop msg + let message = + (on_drop)(current, state.overlay_bounds); + shell.publish(message); + + if self.reset_delay == 0 { + state.action = Action::None; + } else { + state.action = + Action::Wait(self.reset_delay); + } + } + _ => (), + } + } + _ => {} + } + } + } + + let current_status = if cursor.is_over(layout.bounds()) { + if self.on_drop.is_none() { + Status::Disabled + } else { + let state = tree.state.downcast_ref::(); + + if let Action::Drag(_, _) = state.action { + Status::Dragged + } else { + Status::Hovered + } + } + } else { + Status::Active + }; + + if let iced::Event::Window(iced::window::Event::RedrawRequested(_now)) = + event + { + self.status = Some(current_status); + } else if self.status.is_some_and(|status| status != current_status) { + shell.request_redraw(); + } + } + + fn layout( + &self, + tree: &mut iced::advanced::widget::Tree, + renderer: &Renderer, + limits: &iced::advanced::layout::Limits, + ) -> iced::advanced::layout::Node { + let state: &mut State = tree.state.downcast_mut::(); + let content_node = self.content.as_widget().layout( + &mut tree.children[0], + renderer, + limits, + ); + + // Adjust the size of the original widget if it's being dragged or we're wating to reset the size + if let Some(new_size) = self.drag_size { + match state.action { + Action::Drag(_, _) => { + return iced::advanced::layout::Node::with_children( + new_size, + content_node.children().to_vec(), + ); + } + Action::Wait(reveal_index) => { + if reveal_index <= 1 { + state.action = Action::None; + } else { + state.action = Action::Wait(reveal_index - 1); + } + + return iced::advanced::layout::Node::with_children( + new_size, + content_node.children().to_vec(), + ); + } + _ => (), + } + } + + content_node + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation, + ) { + let state = tree.state.downcast_mut::(); + operation.custom(self.id.as_ref(), layout.bounds(), state); + operation.container( + self.id.as_ref(), + layout.bounds(), + &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout, + renderer, + operation, + ); + }, + ); + } + + fn draw( + &self, + tree: &iced::advanced::widget::Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: iced::advanced::Layout<'_>, + cursor: iced::advanced::mouse::Cursor, + viewport: &iced::Rectangle, + ) { + let state: &State = tree.state.downcast_ref::(); + if let Action::Drag(_, _) = state.action { + if self.drag_hide { + return; + } + } + + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor, + &viewport, + ); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + _translation: Vector, + ) -> Option> { + let state: &mut State = tree.state.downcast_mut::(); + if self.drag_overlay { + if let Action::Drag(_, _) = state.action { + return Some(overlay::Element::new(Box::new(Overlay { + content: &self.content, + tree: &mut tree.children[0], + overlay_bounds: state.overlay_bounds, + }))); + } + } + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout, + renderer, + _translation, + ) + } + + fn mouse_interaction( + &self, + tree: &iced::advanced::widget::Tree, + layout: iced::advanced::Layout<'_>, + cursor: iced::advanced::mouse::Cursor, + _viewport: &iced::Rectangle, + _renderer: &Renderer, + ) -> iced::advanced::mouse::Interaction { + let child_interact = self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor, + _viewport, + _renderer, + ); + + if child_interact != mouse::Interaction::default() { + return child_interact; + } + + let state = tree.state.downcast_ref::(); + + if self.on_drop.is_none() && cursor.is_over(layout.bounds()) { + return mouse::Interaction::NotAllowed; + } + + if let Action::Drag(_, _) = state.action { + return mouse::Interaction::Grabbing; + } + + if cursor.is_over(layout.bounds()) { + return mouse::Interaction::Pointer; + } + + mouse::Interaction::default() + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a + Clone, + Theme: 'a, + Renderer: 'a + renderer::Renderer, +{ + fn from( + droppable: Droppable<'a, Message, Theme, Renderer>, + ) -> Element<'a, Message, Theme, Renderer> { + Element::new(droppable) + } +} + +#[derive(Default, Clone, Copy, PartialEq, Debug)] +pub struct State { + widget_pos: Point, + overlay_bounds: Rectangle, + action: Action, +} + +#[derive(Default, Clone, Copy, PartialEq, Debug)] +pub enum Status { + #[default] + Active, + Hovered, + Dragged, + Disabled, +} + +#[derive(Default, Clone, Copy, PartialEq, Debug)] +pub enum Action { + #[default] + None, + /// (point clicked) + Select(Point), + /// (start pos, current pos) + Drag(Point, Point), + /// (frames to wait) + Wait(usize), +} + +struct Overlay<'a, 'b, Message, Theme, Renderer> +where + Renderer: renderer::Renderer, +{ + content: &'b Element<'a, Message, Theme, Renderer>, + tree: &'b mut advanced::widget::Tree, + overlay_bounds: Rectangle, +} + +impl<'a, 'b, Message, Theme, Renderer> + overlay::Overlay + for Overlay<'a, 'b, Message, Theme, Renderer> +where + Renderer: renderer::Renderer, +{ + fn layout(&mut self, renderer: &Renderer, _bounds: Size) -> layout::Node { + Widget::::layout( + self.content.as_widget(), + self.tree, + renderer, + &layout::Limits::new(Size::ZERO, self.overlay_bounds.size()), + ) + .move_to(self.overlay_bounds.position()) + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &Theme, + inherited_style: &renderer::Style, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + ) { + Widget::::draw( + self.content.as_widget(), + self.tree, + renderer, + theme, + inherited_style, + layout, + cursor_position, + &Rectangle::with_size(Size::INFINITY), + ); + } + + fn is_over( + &self, + _layout: Layout<'_>, + _renderer: &Renderer, + _cursor_position: Point, + ) -> bool { + false + } +} diff --git a/crates/iced_drop/src/widget/operation.rs b/crates/iced_drop/src/widget/operation.rs new file mode 100644 index 0000000..3d7dcff --- /dev/null +++ b/crates/iced_drop/src/widget/operation.rs @@ -0,0 +1 @@ +pub mod drop; diff --git a/crates/iced_drop/src/widget/operation/drop.rs b/crates/iced_drop/src/widget/operation/drop.rs new file mode 100644 index 0000000..ead412c --- /dev/null +++ b/crates/iced_drop/src/widget/operation/drop.rs @@ -0,0 +1,88 @@ +use iced::advanced::widget::operation::{Outcome, Scrollable}; +use iced::advanced::widget::{Id, Operation}; +use iced::{Rectangle, Vector}; + +/// Produces an [`Operation`] that will find the drop zones that pass a filter on the zone's bounds. +/// For any drop zone to be considered, the Element must have some Id. +/// If `options` is `None`, all drop zones will be considered. +/// Depth determines how how deep into nested drop zones to go. +/// If 'depth' is `None`, nested dropzones will be fully explored +pub fn find_zones( + filter: F, + options: Option>, + depth: Option, +) -> impl Operation> +where + F: Fn(&Rectangle) -> bool + Send + 'static, +{ + struct FindDropZone { + filter: F, + options: Option>, + zones: Vec<(Id, Rectangle)>, + max_depth: Option, + c_depth: usize, + offset: Vector, + } + + impl Operation> for FindDropZone + where + F: Fn(&Rectangle) -> bool + Send + 'static, + { + fn container( + &mut self, + id: Option<&Id>, + bounds: iced::Rectangle, + operate_on_children: &mut dyn FnMut( + &mut dyn Operation>, + ), + ) { + match id { + Some(id) => { + let is_option = match &self.options { + Some(options) => options.contains(id), + None => true, + }; + let bounds = bounds - self.offset; + if is_option && (self.filter)(&bounds) { + self.c_depth += 1; + self.zones.push((id.clone(), bounds)); + } + } + None => (), + } + let goto_next = match &self.max_depth { + Some(m_depth) => self.c_depth < *m_depth, + None => true, + }; + if goto_next { + operate_on_children(self); + } + } + + fn finish(&self) -> Outcome> { + Outcome::Some(self.zones.clone()) + } + + fn scrollable( + &mut self, + _id: Option<&Id>, + bounds: Rectangle, + _content_bounds: Rectangle, + translation: Vector, + _state: &mut dyn Scrollable, + ) { + if (self.filter)(&bounds) { + self.offset = self.offset + translation; + } + } + } + + FindDropZone { + filter, + options, + zones: vec![], + max_depth: depth, + c_depth: 0, + offset: Vector { x: 0.0, y: 0.0 }, + } +} diff --git a/crates/material_theme/Cargo.toml b/crates/material_theme/Cargo.toml new file mode 100644 index 0000000..eef9605 --- /dev/null +++ b/crates/material_theme/Cargo.toml @@ -0,0 +1,53 @@ +[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" + +[features] +default = [] +animate = ["dep:iced_anim"] +dialog = ["dep:iced_dialog"] + +[dependencies] +iced_widget = "0.14.0-dev" +serde.workspace = true +toml.workspace = true +dark-light = "2.0.0" +iced_dialog.workspace = true +iced_dialog.optional = true + +[dependencies.iced_anim] +workspace = true +features = ["derive"] +optional = true + +[lints.rust] +missing_debug_implementations = "deny" +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/crates/material_theme/README.md b/crates/material_theme/README.md new file mode 100644 index 0000000..da5a1ec --- /dev/null +++ b/crates/material_theme/README.md @@ -0,0 +1,3 @@ +# material_theme + +## A [Material3](https://m3.material.io) inspired custom theme for [`iced`](https://iced.rs) diff --git a/crates/material_theme/assets/themes/dark.toml b/crates/material_theme/assets/themes/dark.toml new file mode 100644 index 0000000..18a369f --- /dev/null +++ b/crates/material_theme/assets/themes/dark.toml @@ -0,0 +1,49 @@ +name = "Dark" + +shadow = "#000000" +scrim = "#4d000000" + +[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/crates/material_theme/assets/themes/light.toml b/crates/material_theme/assets/themes/light.toml new file mode 100644 index 0000000..a7115c4 --- /dev/null +++ b/crates/material_theme/assets/themes/light.toml @@ -0,0 +1,49 @@ +name = "Light" + +shadow = "#000000" +scrim = "#4d000000" + +[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/crates/material_theme/src/button.rs b/crates/material_theme/src/button.rs new file mode 100644 index 0000000..21d77b7 --- /dev/null +++ b/crates/material_theme/src/button.rs @@ -0,0 +1,193 @@ +use iced_widget::button::{Catalog, Status, Style, StyleFn}; +use iced_widget::core::{Background, Border, Color, border}; + +use crate::Theme; +use crate::utils::{ + DISABLED_CONTAINER_OPACITY, DISABLED_TEXT_OPACITY, HOVERED_LAYER_OPACITY, + PRESSED_LAYER_OPACITY, elevation, mix, shadow_from_elevation, +}; + +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, + elevation_level: u8, + status: Status, +) -> Style { + let active = Style { + background: Some(Background::Color(background)), + text_color: foreground, + border: border::rounded(400), + shadow: shadow_from_elevation(elevation(elevation_level), shadow_color), + }; + + match status { + Status::Active => active, + Status::Pressed => Style { + background: Some(Background::Color(mix( + background, + tone_overlay, + HOVERED_LAYER_OPACITY, + ))), + ..active + }, + Status::Hovered => Style { + background: Some(Background::Color(mix( + background, + tone_overlay, + PRESSED_LAYER_OPACITY, + ))), + text_color: foreground, + border: border::rounded(400), + shadow: shadow_from_elevation( + elevation(elevation_level + 1), + shadow_color, + ), + }, + Status::Disabled => Style { + background: Some(Background::Color(Color { + a: DISABLED_CONTAINER_OPACITY, + ..disabled + })), + text_color: Color { + a: DISABLED_TEXT_OPACITY, + ..disabled + }, + border: border::rounded(400), + ..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: DISABLED_CONTAINER_OPACITY, + ..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/crates/material_theme/src/container.rs b/crates/material_theme/src/container.rs new file mode 100644 index 0000000..a14cfd5 --- /dev/null +++ b/crates/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/crates/material_theme/src/dialog.rs b/crates/material_theme/src/dialog.rs new file mode 100644 index 0000000..68c61b5 --- /dev/null +++ b/crates/material_theme/src/dialog.rs @@ -0,0 +1,25 @@ +use iced_widget::container::Style; +use iced_widget::core::{Background, border}; + +use super::{Theme, text}; + +impl iced_dialog::dialog::Catalog for Theme { + fn default_container<'a>() + -> ::Class<'a> { + Box::new(default_container) + } + + fn default_title<'a>() -> ::Class<'a> { + Box::new(text::surface) + } +} + +pub fn default_container(theme: &Theme) -> Style { + let colors = theme.colorscheme.surface; + Style { + background: Some(Background::Color(colors.surface_container.high)), + text_color: Some(colors.on_surface_variant), + border: border::rounded(28), + ..Style::default() + } +} diff --git a/crates/material_theme/src/lib.rs b/crates/material_theme/src/lib.rs new file mode 100644 index 0000000..521af2c --- /dev/null +++ b/crates/material_theme/src/lib.rs @@ -0,0 +1,248 @@ +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; +#[cfg(feature = "dialog")] +pub mod dialog; +pub mod menu; +pub mod pick_list; +pub mod scrollable; +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 { + static DEFAULT: LazyLock = LazyLock::new(|| { + 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(), + } + }); + + DEFAULT.clone() + } +} + +impl std::fmt::Display for Theme { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name) + } +} + +impl Base for Theme { + fn base(&self) -> Style { + Style { + background_color: self.colorscheme.surface.color, + text_color: self.colorscheme.surface.on_surface, + } + } + + fn palette(&self) -> Option { + // TODO: create a Palette + None + } +} + +#[cfg(feature = "animate")] +impl iced_anim::Animate for Theme { + fn components() -> usize { + ColorScheme::components() + } + + fn update(&mut self, components: &mut impl Iterator) { + let mut colors = self.colorscheme; + colors.update(components); + + *self = Theme::new("Animating Theme", colors); + } + + fn distance_to(&self, end: &Self) -> Vec { + self.colorscheme.distance_to(&end.colorscheme) + } + + fn lerp(&mut self, start: &Self, end: &Self, progress: f32) { + let mut colors = self.colorscheme; + colors.lerp(&start.colorscheme, &end.colorscheme, progress); + + *self = Theme::new("Animating Theme", colors); + } +} + +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)] +#[cfg_attr(feature = "animate", derive(iced_anim::Animate))] +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, + #[serde(with = "color_serde")] + pub scrim: Color, +} + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +#[cfg_attr(feature = "animate", derive(iced_anim::Animate))] +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)] +#[cfg_attr(feature = "animate", derive(iced_anim::Animate))] +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)] +#[cfg_attr(feature = "animate", derive(iced_anim::Animate))] +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)] +#[cfg_attr(feature = "animate", derive(iced_anim::Animate))] +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)] +#[cfg_attr(feature = "animate", derive(iced_anim::Animate))] +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)] +#[cfg_attr(feature = "animate", derive(iced_anim::Animate))] +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)] +#[cfg_attr(feature = "animate", derive(iced_anim::Animate))] +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)] +#[cfg_attr(feature = "animate", derive(iced_anim::Animate))] +pub struct Outline { + #[serde(with = "color_serde")] + pub color: Color, + #[serde(with = "color_serde")] + pub variant: Color, +} + +mod color_serde { + use iced_widget::core::Color; + use serde::{Deserialize, Deserializer}; + + use super::utils::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/crates/material_theme/src/menu.rs b/crates/material_theme/src/menu.rs new file mode 100644 index 0000000..9f43c72 --- /dev/null +++ b/crates/material_theme/src/menu.rs @@ -0,0 +1,33 @@ +use iced_widget::core::{Background, border}; +use iced_widget::overlay::menu::{Catalog, Style, StyleFn}; + +use super::Theme; +use crate::utils::{HOVERED_LAYER_OPACITY, mix}; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> ::Class<'a> { + Box::new(default) + } + + fn style(&self, class: &::Class<'_>) -> Style { + class(self) + } +} + +pub fn default(theme: &Theme) -> Style { + let colors = theme.colorscheme.surface; + + Style { + border: border::rounded(4), + background: Background::Color(colors.surface_container.base), + text_color: colors.on_surface, + selected_background: Background::Color(mix( + colors.surface_container.base, + colors.on_surface, + HOVERED_LAYER_OPACITY, + )), + selected_text_color: colors.on_surface, + } +} diff --git a/crates/material_theme/src/pick_list.rs b/crates/material_theme/src/pick_list.rs new file mode 100644 index 0000000..c589100 --- /dev/null +++ b/crates/material_theme/src/pick_list.rs @@ -0,0 +1,40 @@ +use iced_widget::core::{Background, border}; +use iced_widget::pick_list::{Catalog, Status, Style, StyleFn}; + +use super::Theme; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> ::Class<'a> { + Box::new(default) + } + + fn style( + &self, + class: &::Class<'_>, + status: Status, + ) -> Style { + class(self, status) + } +} + +pub fn default(theme: &Theme, status: Status) -> Style { + let surface = theme.colorscheme.surface; + + let active = Style { + text_color: surface.on_surface, + placeholder_color: surface.on_surface_variant, + handle_color: surface.on_surface_variant, + background: Background::Color(surface.surface_container.highest), + border: border::rounded(4), + }; + + match status { + Status::Active => active, + Status::Hovered | Status::Opened { .. } => Style { + background: Background::Color(surface.surface_container.highest), + ..active + }, + } +} diff --git a/crates/material_theme/src/scrollable.rs b/crates/material_theme/src/scrollable.rs new file mode 100644 index 0000000..ee739ba --- /dev/null +++ b/crates/material_theme/src/scrollable.rs @@ -0,0 +1,153 @@ +use iced_widget::core::{Border, Color, border}; +use iced_widget::scrollable::{ + Catalog, Rail, Scroller, Status, Style, StyleFn, +}; + +use super::Theme; +use super::container::surface_container; +use super::utils::mix; +use crate::utils::{ + DISABLED_CONTAINER_OPACITY, DISABLED_TEXT_OPACITY, HOVERED_LAYER_OPACITY, + PRESSED_LAYER_OPACITY, +}; + +impl Catalog for Theme { + 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) + } +} + +pub fn default(theme: &Theme, status: Status) -> Style { + let colors = theme.colorscheme.surface; + + let active = Rail { + background: None, + scroller: Scroller { + color: colors.on_surface, + border: border::rounded(400), + }, + border: Border::default(), + }; + + let disabled = Rail { + background: Some( + Color { + a: DISABLED_CONTAINER_OPACITY, + ..colors.on_surface + } + .into(), + ), + scroller: Scroller { + color: Color { + a: DISABLED_TEXT_OPACITY, + ..colors.on_surface + }, + border: border::rounded(400), + }, + ..active + }; + + let style = Style { + container: surface_container(theme), + vertical_rail: active, + horizontal_rail: active, + gap: None, + }; + + match status { + Status::Active { + is_horizontal_scrollbar_disabled, + is_vertical_scrollbar_disabled, + } => Style { + horizontal_rail: if is_horizontal_scrollbar_disabled { + disabled + } else { + active + }, + vertical_rail: if is_vertical_scrollbar_disabled { + disabled + } else { + active + }, + ..style + }, + Status::Hovered { + is_horizontal_scrollbar_hovered, + is_vertical_scrollbar_hovered, + is_horizontal_scrollbar_disabled, + is_vertical_scrollbar_disabled, + } => { + let hovered_rail = Rail { + scroller: Scroller { + color: mix( + colors.on_surface, + colors.color, + HOVERED_LAYER_OPACITY, + ), + border: border::rounded(400), + }, + ..active + }; + + Style { + horizontal_rail: if is_horizontal_scrollbar_disabled { + disabled + } else if is_horizontal_scrollbar_hovered { + hovered_rail + } else { + active + }, + vertical_rail: if is_vertical_scrollbar_disabled { + disabled + } else if is_vertical_scrollbar_hovered { + hovered_rail + } else { + active + }, + ..style + } + } + Status::Dragged { + is_horizontal_scrollbar_dragged, + is_vertical_scrollbar_dragged, + is_horizontal_scrollbar_disabled, + is_vertical_scrollbar_disabled, + } => { + let dragged_rail = Rail { + scroller: Scroller { + color: mix( + colors.on_surface, + colors.color, + PRESSED_LAYER_OPACITY, + ), + border: border::rounded(400), + }, + ..active + }; + + Style { + horizontal_rail: if is_horizontal_scrollbar_disabled { + disabled + } else if is_horizontal_scrollbar_dragged { + dragged_rail + } else { + active + }, + vertical_rail: if is_vertical_scrollbar_disabled { + disabled + } else if is_vertical_scrollbar_dragged { + dragged_rail + } else { + active + }, + ..style + } + } + } +} diff --git a/crates/material_theme/src/text.rs b/crates/material_theme/src/text.rs new file mode 100644 index 0000000..10b2e65 --- /dev/null +++ b/crates/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/crates/material_theme/src/utils.rs b/crates/material_theme/src/utils.rs new file mode 100644 index 0000000..a05bc62 --- /dev/null +++ b/crates/material_theme/src/utils.rs @@ -0,0 +1,116 @@ +use iced_widget::core::{Color, Shadow, Vector}; + +pub const HOVERED_LAYER_OPACITY: f32 = 0.08; +pub const PRESSED_LAYER_OPACITY: f32 = 0.1; + +pub const DISABLED_TEXT_OPACITY: f32 = 0.38; +pub const DISABLED_CONTAINER_OPACITY: f32 = 0.12; + +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 shadow_from_elevation(elevation: f32, color: Color) -> Shadow { + Shadow { + color, + offset: Vector { + x: 0.0, + y: elevation, + }, + blur_radius: (elevation) * (1.0 + 0.4_f32.powf(elevation)), + } +} + +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?, + }) +} + +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/iced_drop/Cargo.toml b/iced_drop/Cargo.toml deleted file mode 100644 index 0692084..0000000 --- a/iced_drop/Cargo.toml +++ /dev/null @@ -1,7 +0,0 @@ -[package] -name = "iced_drop" -version = "0.1.0" -edition = "2021" - -[dependencies] -iced.workspace = true diff --git a/iced_drop/LICENSE b/iced_drop/LICENSE deleted file mode 100644 index 89d9fee..0000000 --- a/iced_drop/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024 jhannyj - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/iced_drop/README.md b/iced_drop/README.md deleted file mode 100644 index be854f2..0000000 --- a/iced_drop/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# iced_drop - updated to `iced` 0.14-dev - -A small library which provides a custom widget and operation to make drag and drop easier to implement in [iced](https://github.com/iced-rs/iced/tree/master) - -## Usage - -To add drag and drog functionality, first define two messages with the following format - -```rust -enum Message { - Drop(iced::Point, iced::Rectangle) - HandleZones(Vec<(iced::advanced::widget::Id, iced::Rectangle)>) -} -``` - -The `Drop` message will be sent when the droppable is being dragged, and the left mouse button is released. This message provides the mouse position and layout boundaries of the droppable at the release point. - -The `HandleZones` message will be sent after an operation that finds the drop zones under the mouse position. It provides the Id and bounds for each drop zone. - -Next, create create a droppable in the view method and assign the on_drop message. The dropopable function takes an `impl Into` object, so it's easy to make a droppable from any iced widget. - -```rust -iced_drop::droppable("Drop me!").on_drop(Message::Drop); -``` - -Next, create a "drop zone." A drop zone is any widget that operates like a container andhas some assigned Id. It's important that the widget is assigned some Id or it won't be recognized as a drop zone. - -```rust -iced::widget::container("Drop zone") - .id(iced::widget::container::Id::new("drop_zone")); -``` - -Finally, handle the updates of the drop messages - -```rust -match message { - Message::Drop(cursor_pos, _) => { - return iced_drop::zones_on_point( - Message::HandleZonesFound, - point, - None, - None, - ); - } - Message::HandleZones(zones) => { - println!("{:?}", zones) - } -} -``` - -On Drop, we return a widget operation that looks for drop zones under the cursor_pos. When this operation finishes, it returns the zones found and sends the `HandleZones` message. In this example, we only defined one zone, so the zones vector will either be empty if the droppable was not dropped on the zone, or it will contain the `drop_zone` - -## Examples - -There are two examples: color, todo. - -The color example is a very basic drag/drop showcase where the user can drag colors into zones and change the zone's color. I would start here. - -[Link to video](https://drive.google.com/file/d/1K1CCi2Lc90IUyDufsvoUBZmUCbeg6_Fi/view?usp=sharing) - -To run this examples: `cargo run -p color` - -The todo example is a basic todo board application similar to Trello. This is a much much more complex example as it handles custom highlighting and nested droppables, but it just shows you can make some pretty cool things with iced. - -[Link to video](https://drive.google.com/file/d/1MLOCk4Imd_oUnrTj_psbpYbwua976HmR/view?usp=sharing) - -To run this example try: `cargo run -p todo` - -Note: the todo example might also be a good example on how one can use operations. Check examples/todo/src/operation.rs. I didn't find any other examples of this in the iced repo except for the built in focus operations. - -## Future Development - -Right now it's a little annoying having to work with iced's Id type. At some point, I will work on a drop_zone widget that can take some generic clonable type as an id, and I will create a seperate find_zones operation that will return a list of this custom Id. This should make it easier to determine which drop zones were found. diff --git a/iced_drop/src/lib.rs b/iced_drop/src/lib.rs deleted file mode 100644 index c1e1b03..0000000 --- a/iced_drop/src/lib.rs +++ /dev/null @@ -1,55 +0,0 @@ -pub mod widget; - -use iced::advanced::graphics::futures::MaybeSend; -use iced::advanced::renderer; -use iced::advanced::widget::{operate, Id}; -use iced::task::Task; -use iced::{Element, Point, Rectangle}; -use widget::droppable::*; -use widget::operation::drop; - -pub fn droppable<'a, Message, Theme, Renderer>( - content: impl Into>, -) -> Droppable<'a, Message, Theme, Renderer> -where - Message: Clone, - Renderer: renderer::Renderer, -{ - Droppable::new(content) -} - -pub fn zones_on_point( - msg: MF, - point: Point, - options: Option>, - depth: Option, -) -> Task -where - T: Send + 'static, - MF: Fn(Vec<(Id, Rectangle)>) -> T + MaybeSend + Sync + Clone + 'static, -{ - operate(drop::find_zones( - move |bounds| bounds.contains(point), - options, - depth, - )) - .map(move |id| msg(id)) -} - -pub fn find_zones( - msg: MF, - filter: F, - options: Option>, - depth: Option, -) -> Task -where - Message: Send + 'static, - MF: Fn(Vec<(Id, Rectangle)>) -> Message - + MaybeSend - + Sync - + Clone - + 'static, - F: Fn(&Rectangle) -> bool + Send + 'static, -{ - operate(drop::find_zones(filter, options, depth)).map(move |id| msg(id)) -} diff --git a/iced_drop/src/widget.rs b/iced_drop/src/widget.rs deleted file mode 100644 index 6b3fed2..0000000 --- a/iced_drop/src/widget.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod droppable; -pub mod operation; diff --git a/iced_drop/src/widget/droppable.rs b/iced_drop/src/widget/droppable.rs deleted file mode 100644 index 947cf5b..0000000 --- a/iced_drop/src/widget/droppable.rs +++ /dev/null @@ -1,574 +0,0 @@ -//! Encapsulates a widget that can be dragged and dropped. -use std::fmt::Debug; -use std::vec; - -use iced::advanced::widget::{Operation, Tree, Widget}; -use iced::advanced::{self, Layout, layout, mouse, overlay, renderer}; -use iced::{Element, Point, Rectangle, Size, Vector}; - -/// An element that can be dragged and dropped on a [`DropZone`] -pub struct Droppable< - 'a, - Message, - Theme = iced::Theme, - Renderer = iced::Renderer, -> where - Message: Clone, - Renderer: renderer::Renderer, -{ - content: Element<'a, Message, Theme, Renderer>, - id: Option, - on_click: Option, - on_drop: Option Message + 'a>>, - on_drag: Option Message + 'a>>, - on_cancel: Option, - drag_mode: Option<(bool, bool)>, - drag_overlay: bool, - drag_hide: bool, - drag_center: bool, - drag_size: Option, - reset_delay: usize, - status: Option, -} - -impl<'a, Message, Theme, Renderer> Droppable<'a, Message, Theme, Renderer> -where - Message: Clone, - Renderer: renderer::Renderer, -{ - /// Creates a new [`Droppable`]. - pub fn new( - content: impl Into>, - ) -> Self { - Self { - content: content.into(), - id: None, - on_click: None, - on_drop: None, - on_drag: None, - on_cancel: None, - drag_mode: Some((true, true)), - drag_overlay: true, - drag_hide: false, - drag_center: false, - drag_size: None, - reset_delay: 0, - status: None, - } - } - - /// Sets the unique identifier of the [`Droppable`]. - pub fn id(mut self, id: iced::advanced::widget::Id) -> Self { - self.id = Some(id); - self - } - - /// Sets the message that will be produced when the [`Droppable`] is clicked. - pub fn on_click(mut self, message: Message) -> Self { - self.on_click = Some(message); - self - } - - /// Sets the message that will be produced when the [`Droppable`] is dropped on a [`DropZone`]. - /// - /// Unless this is set, the [`Droppable`] will be disabled. - pub fn on_drop(mut self, message: F) -> Self - where - F: Fn(Point, Rectangle) -> Message + 'a, - { - self.on_drop = Some(Box::new(message)); - self - } - - /// Sets the message that will be produced when the [`Droppable`] is dragged. - pub fn on_drag(mut self, message: F) -> Self - where - F: Fn(Point, Rectangle) -> Message + 'a, - { - self.on_drag = Some(Box::new(message)); - self - } - - /// Sets the message that will be produced when the user right clicks while dragging the [`Droppable`]. - pub fn on_cancel(mut self, message: Message) -> Self { - self.on_cancel = Some(message); - self - } - - /// Sets whether the [`Droppable`] should be drawn under the cursor while dragging. - pub fn drag_overlay(mut self, drag_overlay: bool) -> Self { - self.drag_overlay = drag_overlay; - self - } - - /// Sets whether the [`Droppable`] should be hidden while dragging. - pub fn drag_hide(mut self, drag_hide: bool) -> Self { - self.drag_hide = drag_hide; - self - } - - /// Sets whether the [`Droppable`] should be centered on the cursor while dragging. - pub fn drag_center(mut self, drag_center: bool) -> Self { - self.drag_center = drag_center; - self - } - - // Sets whether the [`Droppable`] can be dragged along individual axes. - pub fn drag_mode(mut self, drag_x: bool, drag_y: bool) -> Self { - self.drag_mode = Some((drag_x, drag_y)); - self - } - - /// Sets whether the [`Droppable`] should be be resized to a given size while dragging. - pub fn drag_size(mut self, hide_size: Size) -> Self { - self.drag_size = Some(hide_size); - self - } - - /// Sets the number of frames/layout calls to wait before resetting the size of the [`Droppable`] after dropping. - /// - /// This is useful for cases where the [`Droppable`] is being moved to a new location after some widget operation. - /// In this case, the [`Droppable`] will mainting the 'drag_size' for the given number of frames before resetting to its original size. - /// This prevents the [`Droppable`] from 'jumping' back to its original size before the new location is rendered which - /// prevents flickering. - /// - /// Warning: this should only be set if there's is some noticeble flickering when the [`Droppable`] is dropped. That is, if the - /// [`Droppable`] returns to its original size before it's moved to it's new location. - pub fn reset_delay(mut self, reset_delay: usize) -> Self { - self.reset_delay = reset_delay; - self - } -} - -impl<'a, Message, Theme, Renderer> Widget - for Droppable<'a, Message, Theme, Renderer> -where - Message: Clone, - Renderer: renderer::Renderer, -{ - fn state(&self) -> iced::advanced::widget::tree::State { - advanced::widget::tree::State::new(State::default()) - } - - fn tag(&self) -> iced::advanced::widget::tree::Tag { - advanced::widget::tree::Tag::of::() - } - - fn children(&self) -> Vec { - vec![advanced::widget::Tree::new(&self.content)] - } - - fn diff(&self, tree: &mut iced::advanced::widget::Tree) { - tree.diff_children(std::slice::from_ref(&self.content)) - } - - fn size(&self) -> iced::Size { - self.content.as_widget().size() - } - - fn update( - &mut self, - tree: &mut iced::advanced::widget::Tree, - event: &iced::Event, - layout: iced::advanced::Layout<'_>, - cursor: iced::advanced::mouse::Cursor, - _renderer: &Renderer, - _clipboard: &mut dyn iced::advanced::Clipboard, - shell: &mut iced::advanced::Shell<'_, Message>, - _viewport: &iced::Rectangle, - ) { - // handle the on event of the content first, in case that the droppable is nested - self.content.as_widget_mut().update( - &mut tree.children[0], - event, - layout, - cursor, - _renderer, - _clipboard, - shell, - _viewport, - ); - // this should really only be captured if the droppable is nested or it contains some other - // widget that captures the event - if shell.is_event_captured() { - return; - } - - if let Some(on_drop) = self.on_drop.as_deref() { - let state = tree.state.downcast_mut::(); - if let iced::Event::Mouse(mouse) = event { - match mouse { - mouse::Event::ButtonPressed(btn) => { - if *btn == mouse::Button::Left - && cursor.is_over(layout.bounds()) - { - // select the droppable and store the position of the widget before dragging - state.action = - Action::Select(cursor.position().unwrap()); - let bounds = layout.bounds(); - state.widget_pos = bounds.position(); - state.overlay_bounds.width = bounds.width; - state.overlay_bounds.height = bounds.height; - - if let Some(on_click) = self.on_click.clone() { - shell.publish(on_click); - } - shell.capture_event(); - } else if *btn == mouse::Button::Right { - if let Action::Drag(_, _) = state.action { - shell.invalidate_layout(); - state.action = Action::None; - if let Some(on_cancel) = self.on_cancel.clone() - { - shell.publish(on_cancel); - } - } - } - } - mouse::Event::CursorMoved { mut position } => match state - .action - { - Action::Select(start) | Action::Drag(start, _) => { - // calculate the new position of the widget after dragging - - if let Some((drag_x, drag_y)) = self.drag_mode { - position = Point { - x: if drag_x { - position.x - } else { - start.x - }, - y: if drag_y { - position.y - } else { - start.y - }, - }; - } - - state.action = Action::Drag(start, position); - // update the position of the overlay since the cursor was moved - if self.drag_center { - state.overlay_bounds.x = position.x - - state.overlay_bounds.width / 2.0; - state.overlay_bounds.y = position.y - - state.overlay_bounds.height / 2.0; - } else { - state.overlay_bounds.x = - state.widget_pos.x + position.x - start.x; - state.overlay_bounds.y = - state.widget_pos.y + position.y - start.y; - } - // send on drag msg - if let Some(on_drag) = self.on_drag.as_deref() { - let message = - (on_drag)(position, state.overlay_bounds); - shell.publish(message); - } - - shell.request_redraw(); - } - _ => (), - }, - mouse::Event::ButtonReleased(mouse::Button::Left) => { - match state.action { - Action::Select(_) => { - state.action = Action::None; - } - Action::Drag(_, current) => { - // send on drop msg - let message = - (on_drop)(current, state.overlay_bounds); - shell.publish(message); - - if self.reset_delay == 0 { - state.action = Action::None; - } else { - state.action = - Action::Wait(self.reset_delay); - } - } - _ => (), - } - } - _ => {} - } - } - } - - let current_status = if cursor.is_over(layout.bounds()) { - if self.on_drop.is_none() { - Status::Disabled - } else { - let state = tree.state.downcast_ref::(); - - if let Action::Drag(_, _) = state.action { - Status::Dragged - } else { - Status::Hovered - } - } - } else { - Status::Active - }; - - if let iced::Event::Window(iced::window::Event::RedrawRequested(_now)) = - event - { - self.status = Some(current_status); - } else if self.status.is_some_and(|status| status != current_status) { - shell.request_redraw(); - } - } - - fn layout( - &self, - tree: &mut iced::advanced::widget::Tree, - renderer: &Renderer, - limits: &iced::advanced::layout::Limits, - ) -> iced::advanced::layout::Node { - let state: &mut State = tree.state.downcast_mut::(); - let content_node = self.content.as_widget().layout( - &mut tree.children[0], - renderer, - limits, - ); - - // Adjust the size of the original widget if it's being dragged or we're wating to reset the size - if let Some(new_size) = self.drag_size { - match state.action { - Action::Drag(_, _) => { - return iced::advanced::layout::Node::with_children( - new_size, - content_node.children().to_vec(), - ); - } - Action::Wait(reveal_index) => { - if reveal_index <= 1 { - state.action = Action::None; - } else { - state.action = Action::Wait(reveal_index - 1); - } - - return iced::advanced::layout::Node::with_children( - new_size, - content_node.children().to_vec(), - ); - } - _ => (), - } - } - - content_node - } - - fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn Operation, - ) { - let state = tree.state.downcast_mut::(); - operation.custom(self.id.as_ref(), layout.bounds(), state); - operation.container( - self.id.as_ref(), - layout.bounds(), - &mut |operation| { - self.content.as_widget().operate( - &mut tree.children[0], - layout, - renderer, - operation, - ); - }, - ); - } - - fn draw( - &self, - tree: &iced::advanced::widget::Tree, - renderer: &mut Renderer, - theme: &Theme, - style: &renderer::Style, - layout: iced::advanced::Layout<'_>, - cursor: iced::advanced::mouse::Cursor, - viewport: &iced::Rectangle, - ) { - let state: &State = tree.state.downcast_ref::(); - if let Action::Drag(_, _) = state.action { - if self.drag_hide { - return; - } - } - - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - style, - layout, - cursor, - &viewport, - ); - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - _translation: Vector, - ) -> Option> { - let state: &mut State = tree.state.downcast_mut::(); - if self.drag_overlay { - if let Action::Drag(_, _) = state.action { - return Some(overlay::Element::new(Box::new(Overlay { - content: &self.content, - tree: &mut tree.children[0], - overlay_bounds: state.overlay_bounds, - }))); - } - } - self.content.as_widget_mut().overlay( - &mut tree.children[0], - layout, - renderer, - _translation, - ) - } - - fn mouse_interaction( - &self, - tree: &iced::advanced::widget::Tree, - layout: iced::advanced::Layout<'_>, - cursor: iced::advanced::mouse::Cursor, - _viewport: &iced::Rectangle, - _renderer: &Renderer, - ) -> iced::advanced::mouse::Interaction { - let child_interact = self.content.as_widget().mouse_interaction( - &tree.children[0], - layout, - cursor, - _viewport, - _renderer, - ); - - if child_interact != mouse::Interaction::default() { - return child_interact; - } - - let state = tree.state.downcast_ref::(); - - if self.on_drop.is_none() && cursor.is_over(layout.bounds()) { - return mouse::Interaction::NotAllowed; - } - - if let Action::Drag(_, _) = state.action { - return mouse::Interaction::Grabbing; - } - - if cursor.is_over(layout.bounds()) { - return mouse::Interaction::Pointer; - } - - mouse::Interaction::default() - } -} - -impl<'a, Message, Theme, Renderer> From> - for Element<'a, Message, Theme, Renderer> -where - Message: 'a + Clone, - Theme: 'a, - Renderer: 'a + renderer::Renderer, -{ - fn from( - droppable: Droppable<'a, Message, Theme, Renderer>, - ) -> Element<'a, Message, Theme, Renderer> { - Element::new(droppable) - } -} - -#[derive(Default, Clone, Copy, PartialEq, Debug)] -pub struct State { - widget_pos: Point, - overlay_bounds: Rectangle, - action: Action, -} - -#[derive(Default, Clone, Copy, PartialEq, Debug)] -pub enum Status { - #[default] - Active, - Hovered, - Dragged, - Disabled, -} - -#[derive(Default, Clone, Copy, PartialEq, Debug)] -pub enum Action { - #[default] - None, - /// (point clicked) - Select(Point), - /// (start pos, current pos) - Drag(Point, Point), - /// (frames to wait) - Wait(usize), -} - -struct Overlay<'a, 'b, Message, Theme, Renderer> -where - Renderer: renderer::Renderer, -{ - content: &'b Element<'a, Message, Theme, Renderer>, - tree: &'b mut advanced::widget::Tree, - overlay_bounds: Rectangle, -} - -impl<'a, 'b, Message, Theme, Renderer> - overlay::Overlay - for Overlay<'a, 'b, Message, Theme, Renderer> -where - Renderer: renderer::Renderer, -{ - fn layout(&mut self, renderer: &Renderer, _bounds: Size) -> layout::Node { - Widget::::layout( - self.content.as_widget(), - self.tree, - renderer, - &layout::Limits::new(Size::ZERO, self.overlay_bounds.size()), - ) - .move_to(self.overlay_bounds.position()) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Theme, - inherited_style: &renderer::Style, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - ) { - Widget::::draw( - self.content.as_widget(), - self.tree, - renderer, - theme, - inherited_style, - layout, - cursor_position, - &Rectangle::with_size(Size::INFINITY), - ); - } - - fn is_over( - &self, - _layout: Layout<'_>, - _renderer: &Renderer, - _cursor_position: Point, - ) -> bool { - false - } -} diff --git a/iced_drop/src/widget/operation.rs b/iced_drop/src/widget/operation.rs deleted file mode 100644 index 3d7dcff..0000000 --- a/iced_drop/src/widget/operation.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod drop; diff --git a/iced_drop/src/widget/operation/drop.rs b/iced_drop/src/widget/operation/drop.rs deleted file mode 100644 index ead412c..0000000 --- a/iced_drop/src/widget/operation/drop.rs +++ /dev/null @@ -1,88 +0,0 @@ -use iced::advanced::widget::operation::{Outcome, Scrollable}; -use iced::advanced::widget::{Id, Operation}; -use iced::{Rectangle, Vector}; - -/// Produces an [`Operation`] that will find the drop zones that pass a filter on the zone's bounds. -/// For any drop zone to be considered, the Element must have some Id. -/// If `options` is `None`, all drop zones will be considered. -/// Depth determines how how deep into nested drop zones to go. -/// If 'depth' is `None`, nested dropzones will be fully explored -pub fn find_zones( - filter: F, - options: Option>, - depth: Option, -) -> impl Operation> -where - F: Fn(&Rectangle) -> bool + Send + 'static, -{ - struct FindDropZone { - filter: F, - options: Option>, - zones: Vec<(Id, Rectangle)>, - max_depth: Option, - c_depth: usize, - offset: Vector, - } - - impl Operation> for FindDropZone - where - F: Fn(&Rectangle) -> bool + Send + 'static, - { - fn container( - &mut self, - id: Option<&Id>, - bounds: iced::Rectangle, - operate_on_children: &mut dyn FnMut( - &mut dyn Operation>, - ), - ) { - match id { - Some(id) => { - let is_option = match &self.options { - Some(options) => options.contains(id), - None => true, - }; - let bounds = bounds - self.offset; - if is_option && (self.filter)(&bounds) { - self.c_depth += 1; - self.zones.push((id.clone(), bounds)); - } - } - None => (), - } - let goto_next = match &self.max_depth { - Some(m_depth) => self.c_depth < *m_depth, - None => true, - }; - if goto_next { - operate_on_children(self); - } - } - - fn finish(&self) -> Outcome> { - Outcome::Some(self.zones.clone()) - } - - fn scrollable( - &mut self, - _id: Option<&Id>, - bounds: Rectangle, - _content_bounds: Rectangle, - translation: Vector, - _state: &mut dyn Scrollable, - ) { - if (self.filter)(&bounds) { - self.offset = self.offset + translation; - } - } - } - - FindDropZone { - filter, - options, - zones: vec![], - max_depth: depth, - c_depth: 0, - offset: Vector { x: 0.0, y: 0.0 }, - } -} diff --git a/material_theme/Cargo.toml b/material_theme/Cargo.toml deleted file mode 100644 index eef9605..0000000 --- a/material_theme/Cargo.toml +++ /dev/null @@ -1,53 +0,0 @@ -[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" - -[features] -default = [] -animate = ["dep:iced_anim"] -dialog = ["dep:iced_dialog"] - -[dependencies] -iced_widget = "0.14.0-dev" -serde.workspace = true -toml.workspace = true -dark-light = "2.0.0" -iced_dialog.workspace = true -iced_dialog.optional = true - -[dependencies.iced_anim] -workspace = true -features = ["derive"] -optional = true - -[lints.rust] -missing_debug_implementations = "deny" -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/README.md b/material_theme/README.md deleted file mode 100644 index da5a1ec..0000000 --- a/material_theme/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# material_theme - -## A [Material3](https://m3.material.io) inspired custom theme for [`iced`](https://iced.rs) diff --git a/material_theme/assets/themes/dark.toml b/material_theme/assets/themes/dark.toml deleted file mode 100644 index 18a369f..0000000 --- a/material_theme/assets/themes/dark.toml +++ /dev/null @@ -1,49 +0,0 @@ -name = "Dark" - -shadow = "#000000" -scrim = "#4d000000" - -[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 deleted file mode 100644 index a7115c4..0000000 --- a/material_theme/assets/themes/light.toml +++ /dev/null @@ -1,49 +0,0 @@ -name = "Light" - -shadow = "#000000" -scrim = "#4d000000" - -[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 deleted file mode 100644 index 21d77b7..0000000 --- a/material_theme/src/button.rs +++ /dev/null @@ -1,193 +0,0 @@ -use iced_widget::button::{Catalog, Status, Style, StyleFn}; -use iced_widget::core::{Background, Border, Color, border}; - -use crate::Theme; -use crate::utils::{ - DISABLED_CONTAINER_OPACITY, DISABLED_TEXT_OPACITY, HOVERED_LAYER_OPACITY, - PRESSED_LAYER_OPACITY, elevation, mix, shadow_from_elevation, -}; - -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, - elevation_level: u8, - status: Status, -) -> Style { - let active = Style { - background: Some(Background::Color(background)), - text_color: foreground, - border: border::rounded(400), - shadow: shadow_from_elevation(elevation(elevation_level), shadow_color), - }; - - match status { - Status::Active => active, - Status::Pressed => Style { - background: Some(Background::Color(mix( - background, - tone_overlay, - HOVERED_LAYER_OPACITY, - ))), - ..active - }, - Status::Hovered => Style { - background: Some(Background::Color(mix( - background, - tone_overlay, - PRESSED_LAYER_OPACITY, - ))), - text_color: foreground, - border: border::rounded(400), - shadow: shadow_from_elevation( - elevation(elevation_level + 1), - shadow_color, - ), - }, - Status::Disabled => Style { - background: Some(Background::Color(Color { - a: DISABLED_CONTAINER_OPACITY, - ..disabled - })), - text_color: Color { - a: DISABLED_TEXT_OPACITY, - ..disabled - }, - border: border::rounded(400), - ..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: DISABLED_CONTAINER_OPACITY, - ..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 deleted file mode 100644 index a14cfd5..0000000 --- a/material_theme/src/container.rs +++ /dev/null @@ -1,173 +0,0 @@ -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/dialog.rs b/material_theme/src/dialog.rs deleted file mode 100644 index 68c61b5..0000000 --- a/material_theme/src/dialog.rs +++ /dev/null @@ -1,25 +0,0 @@ -use iced_widget::container::Style; -use iced_widget::core::{Background, border}; - -use super::{Theme, text}; - -impl iced_dialog::dialog::Catalog for Theme { - fn default_container<'a>() - -> ::Class<'a> { - Box::new(default_container) - } - - fn default_title<'a>() -> ::Class<'a> { - Box::new(text::surface) - } -} - -pub fn default_container(theme: &Theme) -> Style { - let colors = theme.colorscheme.surface; - Style { - background: Some(Background::Color(colors.surface_container.high)), - text_color: Some(colors.on_surface_variant), - border: border::rounded(28), - ..Style::default() - } -} diff --git a/material_theme/src/lib.rs b/material_theme/src/lib.rs deleted file mode 100644 index 521af2c..0000000 --- a/material_theme/src/lib.rs +++ /dev/null @@ -1,248 +0,0 @@ -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; -#[cfg(feature = "dialog")] -pub mod dialog; -pub mod menu; -pub mod pick_list; -pub mod scrollable; -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 { - static DEFAULT: LazyLock = LazyLock::new(|| { - 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(), - } - }); - - DEFAULT.clone() - } -} - -impl std::fmt::Display for Theme { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.name) - } -} - -impl Base for Theme { - fn base(&self) -> Style { - Style { - background_color: self.colorscheme.surface.color, - text_color: self.colorscheme.surface.on_surface, - } - } - - fn palette(&self) -> Option { - // TODO: create a Palette - None - } -} - -#[cfg(feature = "animate")] -impl iced_anim::Animate for Theme { - fn components() -> usize { - ColorScheme::components() - } - - fn update(&mut self, components: &mut impl Iterator) { - let mut colors = self.colorscheme; - colors.update(components); - - *self = Theme::new("Animating Theme", colors); - } - - fn distance_to(&self, end: &Self) -> Vec { - self.colorscheme.distance_to(&end.colorscheme) - } - - fn lerp(&mut self, start: &Self, end: &Self, progress: f32) { - let mut colors = self.colorscheme; - colors.lerp(&start.colorscheme, &end.colorscheme, progress); - - *self = Theme::new("Animating Theme", colors); - } -} - -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)] -#[cfg_attr(feature = "animate", derive(iced_anim::Animate))] -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, - #[serde(with = "color_serde")] - pub scrim: Color, -} - -#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] -#[cfg_attr(feature = "animate", derive(iced_anim::Animate))] -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)] -#[cfg_attr(feature = "animate", derive(iced_anim::Animate))] -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)] -#[cfg_attr(feature = "animate", derive(iced_anim::Animate))] -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)] -#[cfg_attr(feature = "animate", derive(iced_anim::Animate))] -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)] -#[cfg_attr(feature = "animate", derive(iced_anim::Animate))] -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)] -#[cfg_attr(feature = "animate", derive(iced_anim::Animate))] -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)] -#[cfg_attr(feature = "animate", derive(iced_anim::Animate))] -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)] -#[cfg_attr(feature = "animate", derive(iced_anim::Animate))] -pub struct Outline { - #[serde(with = "color_serde")] - pub color: Color, - #[serde(with = "color_serde")] - pub variant: Color, -} - -mod color_serde { - use iced_widget::core::Color; - use serde::{Deserialize, Deserializer}; - - use super::utils::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/menu.rs b/material_theme/src/menu.rs deleted file mode 100644 index 9f43c72..0000000 --- a/material_theme/src/menu.rs +++ /dev/null @@ -1,33 +0,0 @@ -use iced_widget::core::{Background, border}; -use iced_widget::overlay::menu::{Catalog, Style, StyleFn}; - -use super::Theme; -use crate::utils::{HOVERED_LAYER_OPACITY, mix}; - -impl Catalog for Theme { - type Class<'a> = StyleFn<'a, Self>; - - fn default<'a>() -> ::Class<'a> { - Box::new(default) - } - - fn style(&self, class: &::Class<'_>) -> Style { - class(self) - } -} - -pub fn default(theme: &Theme) -> Style { - let colors = theme.colorscheme.surface; - - Style { - border: border::rounded(4), - background: Background::Color(colors.surface_container.base), - text_color: colors.on_surface, - selected_background: Background::Color(mix( - colors.surface_container.base, - colors.on_surface, - HOVERED_LAYER_OPACITY, - )), - selected_text_color: colors.on_surface, - } -} diff --git a/material_theme/src/pick_list.rs b/material_theme/src/pick_list.rs deleted file mode 100644 index c589100..0000000 --- a/material_theme/src/pick_list.rs +++ /dev/null @@ -1,40 +0,0 @@ -use iced_widget::core::{Background, border}; -use iced_widget::pick_list::{Catalog, Status, Style, StyleFn}; - -use super::Theme; - -impl Catalog for Theme { - type Class<'a> = StyleFn<'a, Self>; - - fn default<'a>() -> ::Class<'a> { - Box::new(default) - } - - fn style( - &self, - class: &::Class<'_>, - status: Status, - ) -> Style { - class(self, status) - } -} - -pub fn default(theme: &Theme, status: Status) -> Style { - let surface = theme.colorscheme.surface; - - let active = Style { - text_color: surface.on_surface, - placeholder_color: surface.on_surface_variant, - handle_color: surface.on_surface_variant, - background: Background::Color(surface.surface_container.highest), - border: border::rounded(4), - }; - - match status { - Status::Active => active, - Status::Hovered | Status::Opened { .. } => Style { - background: Background::Color(surface.surface_container.highest), - ..active - }, - } -} diff --git a/material_theme/src/scrollable.rs b/material_theme/src/scrollable.rs deleted file mode 100644 index 8a22e56..0000000 --- a/material_theme/src/scrollable.rs +++ /dev/null @@ -1,161 +0,0 @@ -use iced_widget::core::{Border, Color, border}; -use iced_widget::scrollable::{ - Catalog, Rail, Scroller, Status, Style, StyleFn, -}; - -use super::Theme; -use super::container::surface_container; -use super::utils::mix; -use crate::utils::{ - DISABLED_CONTAINER_OPACITY, DISABLED_TEXT_OPACITY, HOVERED_LAYER_OPACITY, - PRESSED_LAYER_OPACITY, -}; - -impl Catalog for Theme { - 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) - } -} - -pub fn default(theme: &Theme, status: Status) -> Style { - let colors = theme.colorscheme.surface; - - let active = Rail { - background: None, - scroller: Scroller { - color: colors.on_surface, - border: border::rounded(400), - }, - border: Border::default(), - }; - - let disabled = Rail { - background: Some( - Color { - a: DISABLED_CONTAINER_OPACITY, - ..colors.on_surface - } - .into(), - ), - scroller: Scroller { - color: Color { - a: DISABLED_TEXT_OPACITY, - ..colors.on_surface - }, - border: border::rounded(400), - }, - ..active - }; - - let style = Style { - container: surface_container(theme), - vertical_rail: active, - horizontal_rail: active, - gap: None, - }; - - match status { - Status::Active { - is_horizontal_scrollbar_disabled, - is_vertical_scrollbar_disabled, - } => Style { - horizontal_rail: if is_horizontal_scrollbar_disabled { - disabled - } else { - active - }, - vertical_rail: if is_vertical_scrollbar_disabled { - disabled - } else { - active - }, - ..style - }, - Status::Hovered { - is_horizontal_scrollbar_hovered, - is_vertical_scrollbar_hovered, - is_horizontal_scrollbar_disabled, - is_vertical_scrollbar_disabled, - } => { - let hovered_rail = Rail { - scroller: Scroller { - color: mix( - colors.on_surface, - colors.color, - HOVERED_LAYER_OPACITY, - ), - border: border::rounded(400), - }, - ..active - }; - - Style { - horizontal_rail: if is_horizontal_scrollbar_disabled { - disabled - } else { - if is_horizontal_scrollbar_hovered { - hovered_rail - } else { - active - } - }, - vertical_rail: if is_vertical_scrollbar_disabled { - disabled - } else { - if is_vertical_scrollbar_hovered { - hovered_rail - } else { - active - } - }, - ..style - } - } - Status::Dragged { - is_horizontal_scrollbar_dragged, - is_vertical_scrollbar_dragged, - is_horizontal_scrollbar_disabled, - is_vertical_scrollbar_disabled, - } => { - let dragged_rail = Rail { - scroller: Scroller { - color: mix( - colors.on_surface, - colors.color, - PRESSED_LAYER_OPACITY, - ), - border: border::rounded(400), - }, - ..active - }; - - Style { - horizontal_rail: if is_horizontal_scrollbar_disabled { - disabled - } else { - if is_horizontal_scrollbar_dragged { - dragged_rail - } else { - active - } - }, - vertical_rail: if is_vertical_scrollbar_disabled { - disabled - } else { - if is_vertical_scrollbar_dragged { - dragged_rail - } else { - active - } - }, - ..style - } - } - } -} diff --git a/material_theme/src/text.rs b/material_theme/src/text.rs deleted file mode 100644 index 10b2e65..0000000 --- a/material_theme/src/text.rs +++ /dev/null @@ -1,86 +0,0 @@ -#![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 deleted file mode 100644 index a05bc62..0000000 --- a/material_theme/src/utils.rs +++ /dev/null @@ -1,116 +0,0 @@ -use iced_widget::core::{Color, Shadow, Vector}; - -pub const HOVERED_LAYER_OPACITY: f32 = 0.08; -pub const PRESSED_LAYER_OPACITY: f32 = 0.1; - -pub const DISABLED_TEXT_OPACITY: f32 = 0.38; -pub const DISABLED_CONTAINER_OPACITY: f32 = 0.12; - -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 shadow_from_elevation(elevation: f32, color: Color) -> Shadow { - Shadow { - color, - offset: Vector { - x: 0.0, - y: elevation, - }, - blur_radius: (elevation) * (1.0 + 0.4_f32.powf(elevation)), - } -} - -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?, - }) -} - -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/theme_test/Cargo.toml b/theme_test/Cargo.toml index 5ccbefa..29fcdc8 100644 --- a/theme_test/Cargo.toml +++ b/theme_test/Cargo.toml @@ -7,4 +7,4 @@ edition = "2024" iced.workspace = true iced_anim.workspace = true iced_dialog.workspace = true -material_theme = { path = "../material_theme", features = ["dialog", "animate"] } +material_theme = { path = "../crates/material_theme", features = ["dialog", "animate"] } -- cgit v1.2.3 From 4dc531f63712e8072a785bd1ec22f35aab3b9140 Mon Sep 17 00:00:00 2001 From: pml68 Date: Tue, 22 Apr 2025 20:27:20 +0200 Subject: refactor(material_theme): make `Theme` `Copy`, add `Theme::ALL` constant --- Cargo.toml | 1 + crates/material_theme/src/lib.rs | 60 +++++++++++++++++----------------------- theme_test/src/main.rs | 6 ++-- 3 files changed, 29 insertions(+), 38 deletions(-) (limited to 'Cargo.toml') diff --git a/Cargo.toml b/Cargo.toml index 1c0984b..a4abf56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,6 +96,7 @@ from_over_into = "deny" needless_borrow = "deny" new_without_default = "deny" useless_conversion = "deny" +clone_on_copy = "deny" [patch.crates-io] iced_anim = { git = "https://github.com/pml68/iced_anim" } diff --git a/crates/material_theme/src/lib.rs b/crates/material_theme/src/lib.rs index 13f13b1..25b6a9c 100644 --- a/crates/material_theme/src/lib.rs +++ b/crates/material_theme/src/lib.rs @@ -1,7 +1,7 @@ use std::sync::LazyLock; +use iced_widget::core::Color; use iced_widget::core::theme::{Base, Style}; -use iced_widget::core::{Color, color}; pub mod button; pub mod checkbox; @@ -29,55 +29,48 @@ pub mod text_input; pub mod toggler; pub mod utils; -pub static DARK: LazyLock = - LazyLock::new(|| Theme::new("Dark", ColorScheme::DARK)); -pub static LIGHT: LazyLock = - LazyLock::new(|| Theme::new("Light", ColorScheme::LIGHT)); - -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Theme { - pub name: String, + pub name: &'static str, #[cfg_attr(feature = "serde", serde(flatten))] pub colorscheme: ColorScheme, } impl Theme { + pub const ALL: &'static [Self] = &[Self::DARK, Self::LIGHT]; + + pub const DARK: Self = Self { + name: "Dark", + colorscheme: ColorScheme::DARK, + }; + + pub const LIGHT: Self = Self { + name: "Light", + colorscheme: ColorScheme::LIGHT, + }; + pub fn new(name: impl Into, colorscheme: ColorScheme) -> Self { Self { - name: name.into(), + name: Box::leak(name.into().into_boxed_str()), 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.clone_from(&source.name); - self.colorscheme = source.colorscheme; - } -} - impl Default for Theme { fn default() -> Self { static DEFAULT: LazyLock = LazyLock::new(|| { match dark_light::detect().unwrap_or(dark_light::Mode::Unspecified) { dark_light::Mode::Dark | dark_light::Mode::Unspecified => { - DARK.clone() + Theme::DARK } - dark_light::Mode::Light => LIGHT.clone(), + dark_light::Mode::Light => Theme::LIGHT, } }); - DEFAULT.clone() + *DEFAULT } } @@ -108,10 +101,8 @@ impl iced_anim::Animate for Theme { } fn update(&mut self, components: &mut impl Iterator) { - let mut colors = self.colorscheme; - colors.update(components); - - *self = Theme::new("Animating Theme", colors); + self.colorscheme.update(components); + self.name = "Animating Theme"; } fn distance_to(&self, end: &Self) -> Vec { @@ -119,10 +110,9 @@ impl iced_anim::Animate for Theme { } fn lerp(&mut self, start: &Self, end: &Self, progress: f32) { - let mut colors = self.colorscheme; - colors.lerp(&start.colorscheme, &end.colorscheme, progress); - - *self = Theme::new("Animating Theme", colors); + self.colorscheme + .lerp(&start.colorscheme, &end.colorscheme, progress); + self.name = "Animating Theme"; } } @@ -153,7 +143,7 @@ macro_rules! from_argb { let g = (hex & 0x0000ff00) >> 8; let b = (hex & 0x000000ff); - color!(r as u8, g as u8, b as u8, a) + ::iced_widget::core::color!(r as u8, g as u8, b as u8, a) }}; } diff --git a/theme_test/src/main.rs b/theme_test/src/main.rs index 3331196..7ec7f15 100644 --- a/theme_test/src/main.rs +++ b/theme_test/src/main.rs @@ -5,6 +5,7 @@ use iced::widget::{ use iced::{Element, Length}; use iced_anim::{Animated, Animation, Event}; use iced_dialog::dialog; +use material_theme::Theme; use material_theme::button::{elevated, filled_tonal, outlined, text}; use material_theme::container::{ error, error_container, inverse_surface, primary, primary_container, @@ -13,11 +14,10 @@ use material_theme::container::{ surface_container_lowest, tertiary, tertiary_container, }; use material_theme::text::surface_variant; -use material_theme::{DARK, LIGHT, Theme}; fn main() -> iced::Result { iced::application(State::default, State::update, State::view) - .theme(|state| state.theme.value().clone()) + .theme(|state| *state.theme.value()) .run() } @@ -190,7 +190,7 @@ impl State { column![ // Pick List pick_list( - [LIGHT.clone(), DARK.clone()], + Theme::ALL, Some(self.theme.target()), |theme| Message::SwitchTheme(theme.into()) ) -- cgit v1.2.3 From f9f854f124d4676e8c79adeda6bfaceba586805f Mon Sep 17 00:00:00 2001 From: pml68 Date: Fri, 25 Apr 2025 11:30:46 +0200 Subject: feat(material_theme): create an iced `Palette` for `iced::theme::Base` impl --- Cargo.toml | 3 ++- crates/material_theme/src/lib.rs | 44 +++++++++++++++++++++++++--------------- src/main.rs | 2 +- theme_test/Cargo.toml | 1 + 4 files changed, 32 insertions(+), 18 deletions(-) (limited to 'Cargo.toml') diff --git a/Cargo.toml b/Cargo.toml index a4abf56..0a7d769 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,8 @@ toml = "0.8.20" [workspace.dependencies.iced] git = "https://github.com/pml68/iced" branch = "feat/rehighlight-on-redraw" -features = ["image", "svg", "advanced", "tokio", "lazy"] +default-features = false +features = ["wgpu", "tiny-skia", "web-colors", "auto-detect-theme", "image", "svg", "advanced", "tokio", "lazy"] [build-dependencies] iced_fontello = "0.13.2" diff --git a/crates/material_theme/src/lib.rs b/crates/material_theme/src/lib.rs index 25b6a9c..569b06c 100644 --- a/crates/material_theme/src/lib.rs +++ b/crates/material_theme/src/lib.rs @@ -29,6 +29,20 @@ pub mod text_input; pub mod toggler; pub mod utils; +#[allow(clippy::cast_precision_loss)] +macro_rules! from_argb { + ($hex:expr) => {{ + let hex = $hex as u32; + + let a = ((hex & 0xff000000) >> 24) as f32 / 255.0; + let r = (hex & 0x00ff0000) >> 16; + let g = (hex & 0x0000ff00) >> 8; + let b = (hex & 0x000000ff); + + ::iced_widget::core::color!(r as u8, g as u8, b as u8, a) + }}; +} + #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Theme { @@ -89,8 +103,20 @@ impl Base for Theme { } fn palette(&self) -> Option { - // TODO: create a Palette - None + let colors = self.colorscheme; + + Some(iced_widget::theme::Palette { + background: colors.surface.color, + text: colors.surface.on_surface, + primary: colors.primary.color, + success: colors.primary.primary_container, + warning: utils::mix( + from_argb!(0xffffff00), + colors.primary.color, + 0.25, + ), + danger: colors.error.color, + }) } } @@ -133,20 +159,6 @@ pub struct ColorScheme { pub scrim: Color, } -#[allow(clippy::cast_precision_loss)] -macro_rules! from_argb { - ($hex:expr) => {{ - let hex = $hex as u32; - - let a = ((hex & 0xff000000) >> 24) as f32 / 255.0; - let r = (hex & 0x00ff0000) >> 16; - let g = (hex & 0x0000ff00) >> 8; - let b = (hex & 0x000000ff); - - ::iced_widget::core::color!(r as u8, g as u8, b as u8, a) - }}; -} - #[allow(clippy::cast_precision_loss)] impl ColorScheme { const DARK: Self = Self { diff --git a/src/main.rs b/src/main.rs index 0f1a1bf..5014077 100644 --- a/src/main.rs +++ b/src/main.rs @@ -146,8 +146,8 @@ impl IcedBuilder { Message::ConfigLoad(result) => match result { Ok(config) => { self.config = Arc::new(config); - self.theme.update(self.config.selected_theme().into()); + return if let Some(path) = self.config.last_project.clone() { if path.exists() && path.is_file() { diff --git a/theme_test/Cargo.toml b/theme_test/Cargo.toml index 29fcdc8..300a7af 100644 --- a/theme_test/Cargo.toml +++ b/theme_test/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] iced.workspace = true +iced.features = ["debug"] iced_anim.workspace = true iced_dialog.workspace = true material_theme = { path = "../crates/material_theme", features = ["dialog", "animate"] } -- cgit v1.2.3 From 9dfb469d1ba975e59f39f3bb799b019204315784 Mon Sep 17 00:00:00 2001 From: pml68 Date: Mon, 28 Apr 2025 10:57:42 +0200 Subject: feat: switch to modified `iced_fontello` for custom Theme support --- Cargo.lock | 10 +- Cargo.toml | 8 +- crates/iced_fontello/.gitignore | 2 + crates/iced_fontello/Cargo.toml | 18 ++ crates/iced_fontello/README.md | 109 +++++++++++ crates/iced_fontello/src/lib.rs | 394 ++++++++++++++++++++++++++++++++++++++++ src/icon.rs | 4 +- 7 files changed, 534 insertions(+), 11 deletions(-) create mode 100644 crates/iced_fontello/.gitignore create mode 100644 crates/iced_fontello/Cargo.toml create mode 100644 crates/iced_fontello/README.md create mode 100644 crates/iced_fontello/src/lib.rs (limited to 'Cargo.toml') diff --git a/Cargo.lock b/Cargo.lock index 9154eb4..02b475d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2082,8 +2082,6 @@ dependencies = [ [[package]] name = "iced_fontello" version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1088f296a44a5c7e51ae5e533954429bfa479d44b304cb7c370480994abab7ef" dependencies = [ "reqwest", "serde", @@ -3737,9 +3735,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.37.4" +version = "0.37.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ce8c88de324ff838700f36fb6ab86c96df0e3c4ab6ef3a9b2044465cce1369" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" dependencies = [ "memchr", ] @@ -4619,9 +4617,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.100" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 0a7d769..9ffaa5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ iced_anim.workspace = true iced_custom_highlighter = { git = "https://github.com/pml68/iced_custom_highlighter", branch = "master" } iced_drop = { path = "crates/iced_drop" } iced_dialog.workspace = true -material_theme = { path = "crates/material_theme" } +material_theme = { path = "crates/material_theme", features = ["animate", "serde", "dialog", "image", "svg"] } serde.workspace = true serde_json = "1.0.140" toml.workspace = true @@ -38,16 +38,16 @@ dirs-next = "2.0.0" iced_anim = { version = "0.2.1", features = ["derive"] } iced_dialog = { git = "https://github.com/pml68/iced_dialog", branch = "iced/personal" } serde = { version = "1.0.219", features = ["derive"] } -toml = "0.8.20" +toml = "0.8.21" [workspace.dependencies.iced] git = "https://github.com/pml68/iced" branch = "feat/rehighlight-on-redraw" default-features = false -features = ["wgpu", "tiny-skia", "web-colors", "auto-detect-theme", "image", "svg", "advanced", "tokio", "lazy"] +features = ["wgpu", "tiny-skia", "web-colors", "auto-detect-theme", "advanced", "tokio", "image", "svg", "lazy"] [build-dependencies] -iced_fontello = "0.13.2" +iced_fontello = { path = "crates/iced_fontello" } [target.'cfg(target_os = "macos")'.dependencies] xdg = "2.5.2" diff --git a/crates/iced_fontello/.gitignore b/crates/iced_fontello/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/crates/iced_fontello/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/crates/iced_fontello/Cargo.toml b/crates/iced_fontello/Cargo.toml new file mode 100644 index 0000000..8c170a1 --- /dev/null +++ b/crates/iced_fontello/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "iced_fontello" +version = "0.13.2" +edition = "2021" +description = "Generate type-safe icon fonts for `iced` at compile time" +repository = "https://github.com/hecrj/iced_fontello" +license = "MIT" +categories = ["gui"] +keywords = ["gui", "ui", "graphics", "interface", "widgets"] +rust-version = "1.85" + +[dependencies] +reqwest = { version = "0.12", features = ["blocking", "json", "multipart"] } +sha2 = "0.10" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +toml = "0.8" +zip = "2" diff --git a/crates/iced_fontello/README.md b/crates/iced_fontello/README.md new file mode 100644 index 0000000..52a59c1 --- /dev/null +++ b/crates/iced_fontello/README.md @@ -0,0 +1,109 @@ +
+ +# iced_fontello + +[![Documentation](https://docs.rs/iced_fontello/badge.svg)](https://docs.rs/iced_fontello) +[![Crates.io](https://img.shields.io/crates/v/iced_fontello.svg)](https://crates.io/crates/iced_fontello) +[![License](https://img.shields.io/crates/l/iced_fontello.svg)](https://github.com/hecrj/iced_fontello/blob/master/LICENSE) +[![Downloads](https://img.shields.io/crates/d/iced_fontello.svg)](https://crates.io/crates/iced_fontello) +[![Test Status](https://img.shields.io/github/actions/workflow/status/hecrj/iced_fontello/test.yml?branch=master&event=push&label=test)](https://github.com/hecrj/iced_fontello/actions) +[![Discourse](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscourse.iced.rs%2Fsite%2Fstatistics.json&query=%24.users_count&suffix=%20users&label=discourse&color=5e7ce2)](https://discourse.iced.rs/) +[![Discord Server](https://img.shields.io/discord/628993209984614400?label=&labelColor=6A7EC2&logo=discord&logoColor=ffffff&color=7389D8)](https://discord.gg/3xZJ65GAhd) + +A compile-time, type-safe icon font generator for [`iced`]. +Powered by [Fontello]. + +[`iced`]: https://github.com/iced-rs/iced +[Fontello]: https://github.com/fontello/fontello + +
+ +## Usage +Create a `.toml` file somewhere in your crate with the font definition: + +```toml +# fonts/example-icons.toml +module = "icon" + +[glyphs] +edit = "fontawesome-pencil" +save = "entypo-floppy" +trash = "typicons-trash" +``` + +The `module` value defines the Rust module that will be generated in your `src` +directory containing a type-safe API to use the font. + +Each entry in the `[glyphs]` section corresponds to an icon. The keys will be +used as names for the functions of the module of the font; while the values +specify the glyph for that key using the format: `-`. You can browse +the available glyphs in [Fontello] or [the `fonts.json` file](fonts.json). + +Next, add `iced_fontello` to your `build-dependencies`: + +```rust +[build-dependencies] +iced_fontello = "0.13" +``` + +Then, call `iced_fontello::build` in your [build script](https://doc.rust-lang.org/cargo/reference/build-scripts.html), +passing the path of your font definition: + +```rust +pub fn main() { + println!("cargo::rerun-if-changed=fonts/example-icons.toml"); + iced_fontello::build("fonts/example-icons.toml").expect("Build example-icons font"); +} +``` + +The library will generate the font and save its `.ttf` file right next to its definition. +In this example, the library would generate `fonts/example-icons.ttf`. + +Finally, it will generate a type-safe `iced` API that lets you use the font. In our example: + +```rust +// Generated automatically by iced_fontello at build time. +// Do not edit manually. +// d24460a00249b2acd0ccc64c3176452c546ad12d1038974e974d7bdb4cdb4a8f +use iced::widget::{text, Text}; +use iced::Font; + +pub const FONT: &[u8] = include_bytes!("../fonts/example-icons.ttf"); + +pub fn edit<'a>() -> Text<'a> { + icon("\u{270E}") +} + +pub fn save<'a>() -> Text<'a> { + icon("\u{1F4BE}") +} + +pub fn trash<'a>() -> Text<'a> { + icon("\u{E10A}") +} + +fn icon<'a>(codepoint: &'a str) -> Text<'a> { + text(codepoint).font(Font::with_name("example-icons")) +} +``` + +Now you can simply add `mod icon;` to your `lib.rs` or `main.rs` file and enjoy your new font: + +```rust +mod icon; + +use iced::widget::row; + +// ... + +row![icon::edit(), icon::save(), icon::trash()].spacing(10) + +// ... +``` + +Check out [the full example](example) to see it all in action. + +## Packaging +If you plan to package your crate, you must make sure you include the generated module +and font file in the final package. `build` is effectively a no-op when the module and +the font already exist and are up-to-date. diff --git a/crates/iced_fontello/src/lib.rs b/crates/iced_fontello/src/lib.rs new file mode 100644 index 0000000..2b39647 --- /dev/null +++ b/crates/iced_fontello/src/lib.rs @@ -0,0 +1,394 @@ +#![allow(clippy::needless_doctest_main)] +//! A compile-time, type-safe icon font generator for [`iced`]. +//! Powered by [Fontello]. +//! +//! [`iced`]: https://github.com/iced-rs/iced +//! [Fontello]: https://github.com/fontello/fontello +//! +//! # Usage +//! Create a `.toml` file somewhere in your crate with the font definition: +//! +//! ```toml +//! # fonts/example-icons.toml +//! module = "icon" +//! +//! [glyphs] +//! edit = "fontawesome-pencil" +//! save = "entypo-floppy" +//! trash = "typicons-trash" +//! ``` +//! +//! The `module` value defines the Rust module that will be generated in your `src` +//! directory containing a type-safe API to use the font. +//! +//! Each entry in the `[glyphs]` section corresponds to an icon. The keys will be +//! used as names for the functions of the module of the font; while the values +//! specify the glyph for that key using the format: `-`. You can browse +//! the available glyphs in [Fontello] or [the `fonts.json` file](fonts.json). +//! +//! Next, add `iced_fontello` to your `build-dependencies`: +//! +//! ```toml +//! [build-dependencies] +//! iced_fontello = "0.13" +//! ``` +//! +//! Then, call `iced_fontello::build` in your [build script](https://doc.rust-lang.org/cargo/reference/build-scripts.html), +//! passing the path of your font definition: +//! +//! ```rust,no_run +//! pub fn main() { +//! println!("cargo::rerun-if-changed=fonts/example-icons.toml"); +//! iced_fontello::build("fonts/example-icons.toml").expect("Build example-icons font"); +//! } +//! ``` +//! +//! The library will generate the font and save its `.ttf` file right next to its definition. +//! In this example, the library would generate `fonts/example-icons.ttf`. +//! +//! Finally, it will generate a type-safe `iced` API that lets you use the font. In our example: +//! +//! ```rust,ignore +//! // Generated automatically by iced_fontello at build time. +//! // Do not edit manually. +//! // d24460a00249b2acd0ccc64c3176452c546ad12d1038974e974d7bdb4cdb4a8f +//! use iced::widget::{text, Text}; +//! use iced::Font; +//! +//! pub const FONT: &[u8] = include_bytes!("../fonts/example-icons.ttf"); +//! +//! pub fn edit<'a>() -> Text<'a> { +//! icon("\u{270E}") +//! } +//! +//! pub fn save<'a>() -> Text<'a> { +//! icon("\u{1F4BE}") +//! } +//! +//! pub fn trash<'a>() -> Text<'a> { +//! icon("\u{E10A}") +//! } +//! +//! fn icon<'a>(codepoint: &'a str) -> Text<'a> { +//! text(codepoint).font(Font::with_name("example-icons")) +//! } +//! ``` +//! +//! Now you can simply add `mod icon;` to your `lib.rs` or `main.rs` file and enjoy your new font: +//! +//! ```rust,ignore +//! mod icon; +//! +//! use iced::widget::row; +//! +//! // ... +//! +//! row![icon::edit(), icon::save(), icon::trash()].spacing(10) +//! +//! // ... +//! ``` +//! +//! Check out [the full example](example) to see it all in action. +//! +//! # Packaging +//! If you plan to package your crate, you must make sure you include the generated module +//! and font file in the final package. `build` is effectively a no-op when the module and +//! the font already exist and are up-to-date. +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use std::{fs, io}; + +use reqwest::blocking as reqwest; +use serde::{Deserialize, Serialize}; + +pub fn build(path: impl AsRef) -> Result<(), Error> { + let path = path.as_ref(); + + let definition: Definition = { + let contents = fs::read_to_string(path).unwrap_or_else(|error| { + panic!( + "Font definition {path} could not be read: {error}", + path = path.display() + ) + }); + + toml::from_str(&contents).unwrap_or_else(|error| { + panic!( + "Font definition {path} is invalid: {error}", + path = path.display() + ) + }) + }; + + let fonts = parse_fonts(); + + let glyphs: BTreeMap = definition + .glyphs + .into_iter() + .map(|(name, id)| { + let Some((font_name, glyph)) = id.split_once('-') else { + panic!( + "Invalid glyph identifier: \"{id}\"\n\ + Glyph identifier must have \"-\" format" + ) + }; + + let Some(font) = fonts.get(font_name) else { + panic!( + "Font \"{font_name}\" was not found. Available fonts are:\n{}", + fonts + .keys() + .map(|name| format!("- {name}")) + .collect::>() + .join("\n") + ); + }; + + let Some(glyph) = font.glyphs.get(glyph) else { + // TODO: Display similarly named candidates + panic!( + "Glyph \"{glyph}\" was not found. Available glyphs are:\n{}", + font.glyphs + .keys() + .map(|name| format!("- {name}")) + .collect::>() + .join("\n") + ); + }; + + ( + name, + ChosenGlyph { + uid: glyph.uid.clone(), + css: glyph.name.clone(), + code: glyph.code, + src: font.name.clone(), + }, + ) + }) + .collect(); + + #[derive(Serialize)] + struct Config { + name: String, + css_prefix_text: &'static str, + css_use_suffix: bool, + hinting: bool, + units_per_em: u32, + ascent: u32, + glyphs: Vec, + } + + #[derive(Clone, Serialize)] + struct ChosenGlyph { + uid: Id, + css: String, + code: u64, + src: String, + } + + let file_name = path + .file_stem() + .expect("Get file stem from definition path") + .to_string_lossy() + .into_owned(); + + let config = Config { + name: file_name.clone(), + css_prefix_text: "icon-", + css_use_suffix: false, + hinting: true, + units_per_em: 1000, + ascent: 850, + glyphs: glyphs.values().cloned().collect(), + }; + + let hash = { + use sha2::Digest as _; + + let mut hasher = sha2::Sha256::new(); + hasher.update( + serde_json::to_string(&config).expect("Serialize config as JSON"), + ); + + format!("{:x}", hasher.finalize()) + }; + + let module_target = PathBuf::new() + .join("src") + .join(definition.module.replace("::", "/")) + .with_extension("rs"); + + let module_contents = + fs::read_to_string(&module_target).unwrap_or_default(); + let module_hash = module_contents + .lines() + .nth(2) + .unwrap_or_default() + .trim_start_matches("// "); + + if hash != module_hash || !path.with_extension("ttf").exists() { + let client = reqwest::Client::new(); + let session = client + .post("https://fontello.com/") + .multipart( + reqwest::multipart::Form::new().part( + "config", + reqwest::multipart::Part::text( + serde_json::to_string(&config) + .expect("Serialize Fontello config"), + ) + .file_name("config.json"), + ), + ) + .send() + .and_then(reqwest::Response::error_for_status) + .and_then(reqwest::Response::text) + .expect("Create Fontello session"); + + let font = client + .get(format!("https://fontello.com/{session}/get")) + .send() + .and_then(reqwest::Response::error_for_status) + .and_then(reqwest::Response::bytes) + .expect("Download Fontello font"); + + let mut archive = zip::ZipArchive::new(io::Cursor::new(font)) + .expect("Parse compressed font"); + + let mut font_file = (0..archive.len()) + .find(|i| { + let file = + archive.by_index(*i).expect("Access zip archive by index"); + + file.name().ends_with(&format!("{file_name}.ttf")) + }) + .and_then(|i| archive.by_index(i).ok()) + .expect("Find font file in zipped archive"); + + io::copy( + &mut font_file, + &mut fs::File::create(path.with_extension("ttf")) + .expect("Create font file"), + ) + .expect("Extract font file"); + } + + let relative_path = PathBuf::from( + std::iter::repeat("../") + .take(definition.module.split("::").count()) + .collect::(), + ); + + let mut module = String::new(); + + module.push_str(&format!( + "// Generated automatically by iced_fontello at build time.\n\ + // Do not edit manually. Source: {source}\n\ + // {hash}\n\ + use iced::Font;\n\ + use iced::widget::text;\n\n\ + use crate::widget::Text;\n\n\ + pub const FONT: &[u8] = include_bytes!(\"{path}\");\n\n", + source = relative_path.join(path.with_extension("toml")).display(), + path = relative_path.join(path.with_extension("ttf")).display() + )); + + for (name, glyph) in glyphs { + module.push_str(&format!( + "\ +pub fn {name}<'a>() -> Text<'a> {{ + icon(\"\\u{{{code:X}}}\") +}}\n\n", + code = glyph.code + )); + } + + module.push_str(&format!( + "\ +fn icon(codepoint: &str) -> Text<'_> {{ + text(codepoint).font(Font::with_name(\"{file_name}\")) +}}\n" + )); + + if module != module_contents { + if let Some(directory) = module_target.parent() { + fs::create_dir_all(directory) + .expect("Create parent directory of font module"); + } + + fs::write(module_target, module).expect("Write font module"); + } + + Ok(()) +} + +#[derive(Debug, Clone)] +pub enum Error {} + +#[derive(Debug, Clone, Deserialize)] +struct Definition { + module: String, + glyphs: BTreeMap, +} + +#[derive(Debug, Clone)] +struct Font { + name: String, + glyphs: BTreeMap, +} + +#[derive(Debug, Clone, Deserialize)] +struct Glyph { + uid: Id, + code: u64, + #[serde(rename = "css")] + name: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +struct Id(String); + +fn parse_fonts() -> BTreeMap { + #[derive(Deserialize)] + struct ItemSchema { + font: FontSchema, + glyphs: Vec, + } + + #[derive(Deserialize)] + struct FontSchema { + fontname: String, + } + + let items: Vec = + serde_json::from_str(include_str!("../fonts.json")) + .expect("Deserialize fonts"); + + items + .into_iter() + .map(|item| { + ( + item.font.fontname.clone(), + Font { + name: item.font.fontname, + glyphs: item + .glyphs + .into_iter() + .map(|glyph| (glyph.name.clone(), glyph)) + .collect(), + }, + ) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_parses_fonts() { + assert!(!parse_fonts().is_empty()); + } +} diff --git a/src/icon.rs b/src/icon.rs index 3fbeb83..d218943 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -1,8 +1,10 @@ // Generated automatically by iced_fontello at build time. // Do not edit manually. Source: ../fonts/icons.toml // 915ea6b0646871c0f04350f201f27f28881b61f3bd6ef292a415d67a211739c1 -use iced::widget::{text, Text}; use iced::Font; +use iced::widget::text; + +use crate::widget::Text; pub const FONT: &[u8] = include_bytes!("../fonts/icons.ttf"); -- cgit v1.2.3 From 74ce597cf1ba4f2f28436d84e019baf171d15b48 Mon Sep 17 00:00:00 2001 From: pml68 Date: Tue, 29 Apr 2025 00:19:13 +0200 Subject: chore: remove `theme_test`, dependency cleanup --- Cargo.lock | 87 ++++++------ Cargo.toml | 7 +- crates/material_theme/Cargo.toml | 6 +- theme_test/Cargo.toml | 11 -- theme_test/src/main.rs | 276 --------------------------------------- 5 files changed, 48 insertions(+), 339 deletions(-) delete mode 100644 theme_test/Cargo.toml delete mode 100644 theme_test/src/main.rs (limited to 'Cargo.toml') diff --git a/Cargo.lock b/Cargo.lock index 02b475d..96ac115 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -232,14 +232,15 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" +checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" dependencies = [ "async-task", "concurrent-queue", "fastrand", "futures-lite", + "pin-project-lite", "slab", ] @@ -1941,7 +1942,7 @@ dependencies = [ [[package]] name = "iced" version = "0.14.0-dev" -source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#64d4a365f3eb25f716c0632316d46d82bfe818c6" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#11c915418f2ad45922d3ca9c113e8c2352497067" dependencies = [ "iced_core", "iced_debug", @@ -1976,7 +1977,7 @@ dependencies = [ [[package]] name = "iced_beacon" version = "0.14.0-dev" -source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#64d4a365f3eb25f716c0632316d46d82bfe818c6" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#11c915418f2ad45922d3ca9c113e8c2352497067" dependencies = [ "bincode", "futures", @@ -2017,7 +2018,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.14.0-dev" -source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#64d4a365f3eb25f716c0632316d46d82bfe818c6" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#11c915418f2ad45922d3ca9c113e8c2352497067" dependencies = [ "bitflags 2.9.0", "bytes", @@ -2046,7 +2047,7 @@ dependencies = [ [[package]] name = "iced_debug" version = "0.14.0-dev" -source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#64d4a365f3eb25f716c0632316d46d82bfe818c6" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#11c915418f2ad45922d3ca9c113e8c2352497067" dependencies = [ "iced_beacon", "iced_core", @@ -2056,7 +2057,7 @@ dependencies = [ [[package]] name = "iced_devtools" version = "0.14.0-dev" -source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#64d4a365f3eb25f716c0632316d46d82bfe818c6" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#11c915418f2ad45922d3ca9c113e8c2352497067" dependencies = [ "iced_debug", "iced_program", @@ -2094,7 +2095,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.14.0-dev" -source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#64d4a365f3eb25f716c0632316d46d82bfe818c6" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#11c915418f2ad45922d3ca9c113e8c2352497067" dependencies = [ "futures", "iced_core", @@ -2108,7 +2109,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.14.0-dev" -source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#64d4a365f3eb25f716c0632316d46d82bfe818c6" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#11c915418f2ad45922d3ca9c113e8c2352497067" dependencies = [ "bitflags 2.9.0", "bytemuck", @@ -2129,7 +2130,7 @@ dependencies = [ [[package]] name = "iced_program" version = "0.14.0-dev" -source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#64d4a365f3eb25f716c0632316d46d82bfe818c6" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#11c915418f2ad45922d3ca9c113e8c2352497067" dependencies = [ "iced_graphics", "iced_runtime", @@ -2138,7 +2139,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.14.0-dev" -source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#64d4a365f3eb25f716c0632316d46d82bfe818c6" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#11c915418f2ad45922d3ca9c113e8c2352497067" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -2150,7 +2151,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.14.0-dev" -source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#64d4a365f3eb25f716c0632316d46d82bfe818c6" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#11c915418f2ad45922d3ca9c113e8c2352497067" dependencies = [ "bytes", "iced_core", @@ -2163,7 +2164,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.14.0-dev" -source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#64d4a365f3eb25f716c0632316d46d82bfe818c6" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#11c915418f2ad45922d3ca9c113e8c2352497067" dependencies = [ "bytemuck", "cosmic-text", @@ -2180,7 +2181,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.14.0-dev" -source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#64d4a365f3eb25f716c0632316d46d82bfe818c6" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#11c915418f2ad45922d3ca9c113e8c2352497067" dependencies = [ "bitflags 2.9.0", "bytemuck", @@ -2201,7 +2202,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.14.0-dev" -source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#64d4a365f3eb25f716c0632316d46d82bfe818c6" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#11c915418f2ad45922d3ca9c113e8c2352497067" dependencies = [ "iced_renderer", "iced_runtime", @@ -2219,7 +2220,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.14.0-dev" -source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#64d4a365f3eb25f716c0632316d46d82bfe818c6" +source = "git+https://github.com/pml68/iced?branch=feat%2Frehighlight-on-redraw#11c915418f2ad45922d3ca9c113e8c2352497067" dependencies = [ "iced_debug", "iced_program", @@ -2554,9 +2555,9 @@ dependencies = [ [[package]] name = "kurbo" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89234b2cc610a7dd927ebde6b41dd1a5d4214cffaef4cf1fb2195d592f92518f" +checksum = "1077d333efea6170d9ccb96d3c3026f300ca0773da4938cc4c811daa6df68b0c" dependencies = [ "arrayvec", "smallvec", @@ -4600,7 +4601,7 @@ version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" dependencies = [ - "kurbo 0.11.1", + "kurbo 0.11.2", "siphasher", ] @@ -4737,16 +4738,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "theme_test" -version = "0.0.1" -dependencies = [ - "iced", - "iced_anim", - "iced_dialog", - "material_theme", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -4954,9 +4945,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900f6c86a685850b1bc9f6223b20125115ee3f31e01207d81655bbcc0aea9231" +checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" dependencies = [ "serde", "serde_spanned", @@ -4975,9 +4966,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.25" +version = "0.22.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10558ed0bd2a1562e630926a2d1f0b98c827da99fabd3fe20920a59642504485" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ "indexmap", "serde", @@ -4989,9 +4980,9 @@ dependencies = [ [[package]] name = "toml_write" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28391a4201ba7eb1984cfeb6862c0b3ea2cfe23332298967c749dddc0d6cd976" +checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" [[package]] name = "tower" @@ -5210,7 +5201,7 @@ dependencies = [ "flate2", "fontdb 0.18.0", "imagesize", - "kurbo 0.11.1", + "kurbo 0.11.2", "log", "pico-args", "roxmltree", @@ -5414,9 +5405,9 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf" +checksum = "c2bea670be0e24795f39416e5461ccef0185b47df2749ed2b226b8a7557ac871" dependencies = [ "cc", "downcast-rs", @@ -5428,9 +5419,9 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.8" +version = "0.31.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f" +checksum = "5029b0ff4f7be961c326169ee8e247d534d8507dbe390cac6aa3d2cc7c268825" dependencies = [ "bitflags 2.9.0", "rustix 0.38.44", @@ -5451,9 +5442,9 @@ dependencies = [ [[package]] name = "wayland-cursor" -version = "0.31.8" +version = "0.31.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a93029cbb6650748881a00e4922b076092a6a08c11e7fbdb923f064b23968c5d" +checksum = "28d6ec438d7c38bde05a10e80c3e3a1212d85f941be9fc9f80c86e6f5f498252" dependencies = [ "rustix 0.38.44", "wayland-client", @@ -5462,9 +5453,9 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.32.6" +version = "0.32.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0781cf46869b37e36928f7b432273c0995aa8aed9552c556fb18754420541efc" +checksum = "ba8de1f9dda5e589d08848af3ad4cd694bbfd059c3eb3c6d89c7120e8c0efa71" dependencies = [ "bitflags 2.9.0", "wayland-backend", @@ -5474,9 +5465,9 @@ dependencies = [ [[package]] name = "wayland-protocols-plasma" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ccaacc76703fefd6763022ac565b590fcade92202492381c95b2edfdf7d46b3" +checksum = "87e10c27e3290310d7e0d3221bc4e945d9b296b249577af2eb595726b546a3f8" dependencies = [ "bitflags 2.9.0", "wayland-backend", @@ -5487,9 +5478,9 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248a02e6f595aad796561fa82d25601bd2c8c3b145b1c7453fc8f94c1a58f8b2" +checksum = "9f3334ee752fbe3c228adfda339a9e7a03e0ba65a78806d8d464b69928cf4ef2" dependencies = [ "bitflags 2.9.0", "wayland-backend", diff --git a/Cargo.toml b/Cargo.toml index 9ffaa5d..a653682 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,14 +35,14 @@ thiserror = "2.0.12" dirs-next = "2.0.0" [workspace.dependencies] +iced_widget = "0.14.0-dev" iced_anim = { version = "0.2.1", features = ["derive"] } iced_dialog = { git = "https://github.com/pml68/iced_dialog", branch = "iced/personal" } serde = { version = "1.0.219", features = ["derive"] } toml = "0.8.21" [workspace.dependencies.iced] -git = "https://github.com/pml68/iced" -branch = "feat/rehighlight-on-redraw" +version = "0.14.0-dev" default-features = false features = ["wgpu", "tiny-skia", "web-colors", "auto-detect-theme", "advanced", "tokio", "image", "svg", "lazy"] @@ -75,7 +75,7 @@ name = "iced-builder" path = "src/main.rs" [workspace] -members = ["crates/*", "theme_test"] +members = ["crates/*"] default-members = ["crates/material_theme", "."] [lints.rust] @@ -100,5 +100,6 @@ useless_conversion = "deny" clone_on_copy = "deny" [patch.crates-io] +iced = { git = "https://github.com/pml68/iced", branch = "feat/rehighlight-on-redraw" } 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/crates/material_theme/Cargo.toml b/crates/material_theme/Cargo.toml index 30de38b..0116c1e 100644 --- a/crates/material_theme/Cargo.toml +++ b/crates/material_theme/Cargo.toml @@ -29,7 +29,6 @@ svg = ["iced_widget/svg"] qr_code = ["iced_widget/qr_code"] [dependencies] -iced_widget = "0.14.0-dev" dark-light = "2.0.0" serde.workspace = true @@ -38,6 +37,11 @@ serde.optional = true iced_dialog.workspace = true iced_dialog.optional = true +[dependencies.iced_widget] +version = "0.14.0-dev" +git = "https://github.com/pml68/iced" +branch = "feat/rehighlight-on-redraw" + [dependencies.iced_anim] workspace = true features = ["derive"] diff --git a/theme_test/Cargo.toml b/theme_test/Cargo.toml deleted file mode 100644 index 300a7af..0000000 --- a/theme_test/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "theme_test" -version = "0.0.1" -edition = "2024" - -[dependencies] -iced.workspace = true -iced.features = ["debug"] -iced_anim.workspace = true -iced_dialog.workspace = true -material_theme = { path = "../crates/material_theme", features = ["dialog", "animate"] } diff --git a/theme_test/src/main.rs b/theme_test/src/main.rs deleted file mode 100644 index cba4377..0000000 --- a/theme_test/src/main.rs +++ /dev/null @@ -1,276 +0,0 @@ -use iced::widget::{ - button, center, checkbox, column, container, horizontal_rule, pane_grid, - pick_list, progress_bar, radio, row, slider, text_editor, text_input, - toggler, -}; -use iced::{Element, Length}; -use iced_anim::{Animated, Animation, Event}; -use iced_dialog::dialog; -use material_theme::Theme; -use material_theme::button::{elevated, filled_tonal, outlined, text}; -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::Result { - iced::application(State::default, State::update, State::view) - .theme(|state| state.theme.value().clone()) - .run() -} - -#[allow(dead_code)] -#[derive(Debug, Clone)] -enum Message { - Noop, - OpenDialog, - CloseDialog, - Input(String), - Bool(bool), - Radio(Choice), - Slider(f32), - Edit(text_editor::Action), - Resize(pane_grid::ResizeEvent), - SwitchTheme(Event), -} - -#[derive(Debug)] -pub struct State { - theme: Animated, - show_dialog: bool, - content: String, - is_checked: bool, - selection: Option, - value: f32, - editor_content: text_editor::Content, - panes: pane_grid::State, -} - -impl Default for State { - fn default() -> Self { - Self { - theme: Default::default(), - show_dialog: Default::default(), - content: Default::default(), - is_checked: Default::default(), - selection: Default::default(), - value: Default::default(), - editor_content: text_editor::Content::new(), - panes: pane_grid::State::with_configuration( - pane_grid::Configuration::Split { - axis: pane_grid::Axis::Vertical, - ratio: 0.5, - a: Box::new(pane_grid::Configuration::Pane(Pane::Left)), - b: Box::new(pane_grid::Configuration::Pane(Pane::Right)), - }, - ), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Choice { - A, - B, - C, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Pane { - Left, - Right, -} - -impl State { - fn update(&mut self, message: Message) { - match message { - Message::Noop => {} - Message::OpenDialog => { - self.show_dialog = true; - } - Message::CloseDialog => { - self.show_dialog = false; - } - Message::Input(content) => self.content = content, - Message::Bool(is_checked) => self.is_checked = is_checked, - Message::Radio(choice) => self.selection = Some(choice), - Message::Slider(value) => self.value = value, - Message::Edit(action) => self.editor_content.perform(action), - Message::Resize(pane_grid::ResizeEvent { split, ratio }) => { - self.panes.resize(split, ratio); - } - Message::SwitchTheme(event) => { - self.theme.update(event); - } - } - } - fn view(&self) -> Element<'_, Message, Theme> { - let base: pane_grid::PaneGrid<'_, Message, Theme> = - pane_grid(&self.panes, |_pane, state, _is_maximized| { - pane_grid::Content::new(match state { - Pane::Left => 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), - button("Text Disabled").style(text), - ] - .spacing(10), - column![ - container("None").padding(8), - 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( - iced::widget::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(10), - ) - .width(Length::Fill) - .height(Length::Fill) - .padding(12), - - Pane::Right => container( - column![ - // Pick List - pick_list( - Theme::ALL, - Some(self.theme.target()), - |theme| Message::SwitchTheme(theme.into()) - ) - .placeholder("Select a theme..."), - horizontal_rule(1), - // Dialog - button("Open Dialog").on_press(Message::OpenDialog), - horizontal_rule(1), - // Text Input - text_input("Type something here...", &self.content) - .on_input(Message::Input), - text_input("Disabled", "Disabled"), - horizontal_rule(1), - // Checkbox - checkbox("Normal", self.is_checked) - .on_toggle(Message::Bool), - checkbox("Error", self.is_checked) - .on_toggle(Message::Bool) - .style(material_theme::checkbox::error), - checkbox("Disabled", self.is_checked), - horizontal_rule(1), - // Radio - radio( - "A", - Choice::A, - self.selection, - Message::Radio, - ), - radio( - "B", - Choice::B, - self.selection, - Message::Radio, - ), - radio( - "C", - Choice::C, - self.selection, - Message::Radio, - ), - horizontal_rule(1), - // Slider - center(iced::widget::text!("{:.1}", self.value)) - .width(Length::Fill) - .height(Length::Shrink), - slider(0.0..=100.0, self.value, Message::Slider) - .step(0.1), - progress_bar(0.0..=100.0, self.value), - horizontal_rule(1), - // Toggler - toggler(self.is_checked) - .on_toggle(Message::Bool) - .size(24.0), - toggler(self.is_checked).size(24.0), - horizontal_rule(1), - // Text Editor - text_editor(&self.editor_content) - .on_action(Message::Edit), - text_editor(&self.editor_content) - .placeholder("Disabled") - ] - .spacing(10), - ) - .width(Length::Fill) - .height(Length::Fill) - .padding(12), - }) - }) - .on_resize(10, Message::Resize); - - let dialog = - dialog(self.show_dialog, base, iced::widget::text("Say Hi!")) - .title("This is a Dialog.") - .push_button(iced_dialog::button("Hi!", Message::CloseDialog)) - .width(280) - .height(186); - - Animation::new(&self.theme, dialog) - .on_update(Message::SwitchTheme) - .into() - } -} -- cgit v1.2.3