diff options
Diffstat (limited to 'crates/material_theme/src')
| -rw-r--r-- | crates/material_theme/src/button.rs | 193 | ||||
| -rw-r--r-- | crates/material_theme/src/container.rs | 173 | ||||
| -rw-r--r-- | crates/material_theme/src/dialog.rs | 25 | ||||
| -rw-r--r-- | crates/material_theme/src/lib.rs | 248 | ||||
| -rw-r--r-- | crates/material_theme/src/menu.rs | 33 | ||||
| -rw-r--r-- | crates/material_theme/src/pick_list.rs | 40 | ||||
| -rw-r--r-- | crates/material_theme/src/scrollable.rs | 153 | ||||
| -rw-r--r-- | crates/material_theme/src/text.rs | 86 | ||||
| -rw-r--r-- | crates/material_theme/src/utils.rs | 116 |
9 files changed, 1067 insertions, 0 deletions
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>() + -> <Self as iced_widget::container::Catalog>::Class<'a> { + Box::new(default_container) + } + + fn default_title<'a>() -> <Self as iced_widget::text::Catalog>::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<String>, colorscheme: ColorScheme) -> Self { + Self { + name: name.into(), + colorscheme, + } + } +} + +impl Clone for Theme { + fn clone(&self) -> Self { + Self { + name: self.name.clone(), + colorscheme: self.colorscheme, + } + } + + fn clone_from(&mut self, source: &Self) { + self.name = source.name.clone(); + self.colorscheme = source.colorscheme; + } +} + +impl Default for Theme { + fn default() -> Self { + static DEFAULT: LazyLock<Theme> = 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<iced_widget::theme::Palette> { + // 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<Item = f32>) { + let mut colors = self.colorscheme; + colors.update(components); + + *self = Theme::new("Animating Theme", colors); + } + + fn distance_to(&self, end: &Self) -> Vec<f32> { + 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<Theme> = LazyLock::new(|| { + toml::from_str(DARK_THEME_CONTENT).expect("parse dark theme") +}); + +pub static LIGHT: LazyLock<Theme> = LazyLock::new(|| { + toml::from_str(LIGHT_THEME_CONTENT).expect("parse light theme") +}); + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +#[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<Color, D::Error> + 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>() -> <Self as Catalog>::Class<'a> { + Box::new(default) + } + + fn style(&self, class: &<Self as Catalog>::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>() -> <Self as Catalog>::Class<'a> { + Box::new(default) + } + + fn style( + &self, + class: &<Self as Catalog>::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<Color> { + let hex = s.strip_prefix('#').unwrap_or(s); + + let parse_channel = |from: usize, to: usize| { + let num = + usize::from_str_radix(&hex[from..=to], 16).ok()? as f32 / 255.0; + + // If we only got half a byte (one letter), expand it into a full byte (two letters) + Some(if from == to { num + num * 16.0 } else { num }) + }; + + Some(match hex.len() { + 3 => Color::from_rgb( + parse_channel(0, 0)?, + parse_channel(1, 1)?, + parse_channel(2, 2)?, + ), + 4 => Color::from_rgba( + parse_channel(1, 1)?, + parse_channel(2, 2)?, + parse_channel(3, 3)?, + parse_channel(0, 0)?, + ), + 6 => Color::from_rgb( + parse_channel(0, 1)?, + parse_channel(2, 3)?, + parse_channel(4, 5)?, + ), + 8 => Color::from_rgba( + parse_channel(2, 3)?, + parse_channel(4, 5)?, + parse_channel(6, 7)?, + parse_channel(0, 1)?, + ), + _ => None?, + }) +} + +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() + ); + } +} |
