use std::borrow::Cow; #[cfg(feature = "system-theme")] use std::sync::{Arc, LazyLock}; #[cfg(feature = "system-theme")] use arc_swap::ArcSwap; use iced_widget::core::{ Color, color, theme::{Base, Style}, }; #[cfg(feature = "system-theme")] use iced_widget::runtime::futures::{ BoxStream, futures::StreamExt, subscription::{EventStream, Hasher, Recipe, Subscription, from_recipe}, }; #[cfg(feature = "system-theme")] use mundy::{Interest, Preferences}; use utils::{lightness, mix}; pub mod button; pub mod checkbox; pub mod combo_box; pub mod container; #[cfg(feature = "dialog")] pub mod dialog; #[cfg(feature = "markdown")] pub mod markdown; pub mod menu; pub mod pane_grid; pub mod pick_list; pub mod progress_bar; #[cfg(feature = "qr_code")] pub mod qr_code; pub mod radio; pub mod rule; pub mod scrollable; pub mod slider; #[cfg(feature = "svg")] pub mod svg; pub mod table; pub mod text; pub mod text_editor; 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) }}; } #[cfg(feature = "system-theme")] static SYSTEM_THEME: LazyLock> = LazyLock::new(|| { use std::time::Duration; ArcSwap::new(Arc::new( Preferences::once_blocking( Interest::ColorScheme, Duration::from_millis(1200), ) .map_or(Theme::Dark, Theme::from), )) }); #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(from = "Custom", into = "Custom"))] pub enum Theme { Dark, Light, #[cfg(feature = "system-theme")] System, Custom(Custom), } impl Theme { pub const ALL: &'static [Self] = &[ #[cfg(feature = "system-theme")] Self::System, Self::Dark, Self::Light, ]; pub fn new( name: impl Into>, colorscheme: ColorScheme, ) -> Self { Self::Custom(Custom { name: name.into(), is_dark: lightness(colorscheme.surface.color) <= 0.5, colorscheme, }) } pub const fn new_const( name: &'static str, colorscheme: ColorScheme, ) -> Self { Self::Custom(Custom { name: Cow::Borrowed(name), is_dark: lightness(colorscheme.surface.color) <= 0.5, colorscheme, }) } pub fn name(&self) -> Cow<'static, str> { match self { Self::Dark => "Dark".into(), Self::Light => "Light".into(), #[cfg(feature = "system-theme")] Self::System => "System".into(), Self::Custom(custom) => custom.name.clone(), } } pub fn is_dark(&self) -> bool { match self { Self::Dark => true, Self::Light => false, #[cfg(feature = "system-theme")] Self::System => SYSTEM_THEME.load().is_dark(), Self::Custom(custom) => custom.is_dark, } } pub fn colors(&self) -> ColorScheme { match self { Self::Dark => ColorScheme::DARK, Self::Light => ColorScheme::LIGHT, #[cfg(feature = "system-theme")] Self::System => SYSTEM_THEME.load().colors(), Self::Custom(custom) => custom.colorscheme, } } /// A subscription that responds to the user's theme preference changing and returns the /// corresponding [`Theme`] variant. #[cfg(feature = "system-theme")] pub fn subscription() -> Subscription { struct ThemeSubscription; impl Recipe for ThemeSubscription { type Output = Theme; fn hash(&self, state: &mut Hasher) { use std::hash::Hash; std::any::TypeId::of::().hash(state); } fn stream( self: Box, _input: EventStream, ) -> BoxStream { Preferences::stream(Interest::ColorScheme) .map(Theme::from) .boxed() } } from_recipe(ThemeSubscription) } /// Updates the [`System`] variant with the given theme. /// /// Meant to be used in conjunction with [`Theme::subscription`]. /// /// # Example /// /// ```no_run /// use iced_material::Theme; /// /// fn main() -> iced::Result { /// iced::application(State::default, State::update, State::view) /// .theme(State::theme) /// .subscription(State::subscription) /// .run() /// } /// /// #[derive(Debug, Default)] /// struct State; /// /// #[derive(Debug, Clone)] /// enum Message { /// SystemThemeChanged(Theme) /// } /// /// impl State { /// fn view(&self) -> iced::Element { /// // ... /// } /// /// fn update(&mut self, message: Message) { /// match Message { /// Message::SystemThemeChanged(theme) => Theme::update_system_theme(theme) /// } /// } /// /// fn theme(&self) -> Theme { /// Theme::System /// } /// /// fn subscription(&self) -> iced::Subscription { /// Theme::subscription().map(Message::SystemThemeChanged) /// } /// } /// ``` /// /// [`System`]: Theme::System #[cfg(feature = "system-theme")] pub fn update_system_theme(this: Self) { SYSTEM_THEME.store(Arc::new(this)); } } impl Default for Theme { fn default() -> Self { if cfg!(feature = "system-theme") { Theme::System } else { Theme::Dark } } } 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.colors().surface.color, text_color: self.colors().surface.on_surface, } } fn palette(&self) -> Option { let colors = self.colors(); Some(iced_widget::theme::Palette { background: colors.surface.color, text: colors.surface.on_surface, primary: colors.primary.color, success: colors.primary.primary_container, warning: mix(from_argb!(0xffffff00), colors.primary.color, 0.25), danger: colors.error.color, }) } } #[cfg(feature = "system-theme")] impl From for Theme { fn from(preference: Preferences) -> Self { if preference.color_scheme == mundy::ColorScheme::Light { Theme::Light } else { Theme::Dark } } } #[cfg(feature = "animate")] impl iced_anim::Animate for Theme { fn components() -> usize { ColorScheme::components() } fn update(&mut self, components: &mut impl Iterator) { let mut colorscheme = self.colors(); colorscheme.update(components); *self = Self::Custom(Custom { name: "Animating Theme".into(), is_dark: self.is_dark(), colorscheme, }); } fn distance_to(&self, end: &Self) -> Vec { self.colors().distance_to(&end.colors()) } fn lerp(&mut self, start: &Self, end: &Self, progress: f32) { let mut colorscheme = self.colors(); colorscheme.lerp(&start.colors(), &end.colors(), progress); *self = Self::Custom(Custom { name: "Animating Theme".into(), is_dark: self.is_dark(), colorscheme, }); } } /// A custom [`Theme`]. #[derive(Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Custom { /// The [`Theme`]'s name. pub name: Cow<'static, str>, /// Whether the [`Theme`] is dark. pub is_dark: bool, /// The [`Theme`]'s [`ColorScheme`]. #[cfg_attr(feature = "serde", serde(flatten))] pub colorscheme: ColorScheme, } impl From for Theme { fn from(custom: Custom) -> Self { Self::Custom(custom) } } impl From for Custom { fn from(theme: Theme) -> Self { match theme { Theme::Custom(custom) => custom, theme => Self { name: theme.name(), is_dark: theme.is_dark(), colorscheme: theme.colors(), }, } } } impl Clone for Custom { fn clone(&self) -> Self { Self { name: self.name.clone(), is_dark: self.is_dark, colorscheme: self.colorscheme, } } fn clone_from(&mut self, source: &Self) { self.name.clone_from(&source.name); self.is_dark = source.is_dark; self.colorscheme = source.colorscheme; } } /// A [`Theme`]'s color scheme. /// /// These color roles are base on Material Design 3. For more information about them, visit the /// official [M3 documentation](https://m3.material.io/styles/color/roles). /// /// [M3 page]: https://m3.material.io/styles/color/roles #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "animate", derive(iced_anim::Animate))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ColorScheme { /// The primary colors. pub primary: Primary, /// The secondary colors. pub secondary: Secondary, /// The tertiary colors. pub tertiary: Tertiary, /// The error colors. pub error: Error, /// The surface colors. pub surface: Surface, /// The inverse colors. pub inverse: Inverse, /// The outline colors. pub outline: Outline, /// The shadow color. #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub shadow: Color, /// The scrim color. #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub scrim: Color, } #[allow(clippy::cast_precision_loss)] impl ColorScheme { const DARK: Self = Self { primary: Primary { color: color!(0x9bd4a1), on_primary: color!(0x003916), primary_container: color!(0x1b5129), on_primary_container: color!(0xb6f1bb), }, secondary: Secondary { color: color!(0xb8ccb6), on_secondary: color!(0x233425), secondary_container: color!(0x394b3a), on_secondary_container: color!(0xd3e8d1), }, tertiary: Tertiary { color: color!(0xa1ced7), on_tertiary: color!(0x00363e), tertiary_container: color!(0x1f4d55), on_tertiary_container: color!(0xbdeaf4), }, error: Error { color: color!(0xffb4ab), on_error: color!(0x690005), error_container: color!(0x93000a), on_error_container: color!(0xffdad6), }, surface: Surface { color: color!(0x101510), on_surface: color!(0xe0e4dc), on_surface_variant: color!(0xc1c9be), surface_container: SurfaceContainer { lowest: color!(0x0b0f0b), low: color!(0x181d18), base: color!(0x1c211c), high: color!(0x262b26), highest: color!(0x313631), }, }, inverse: Inverse { inverse_surface: color!(0xe0e4dc), inverse_on_surface: color!(0x2d322c), inverse_primary: color!(0x34693f), }, outline: Outline { color: color!(0x8b9389), variant: color!(0x414941), }, shadow: color!(0x000000), scrim: from_argb!(0x4d000000), }; const LIGHT: Self = Self { primary: Primary { color: color!(0x34693f), on_primary: color!(0xffffff), primary_container: color!(0xb6f1bb), on_primary_container: color!(0x1b5129), }, secondary: Secondary { color: color!(0x516351), on_secondary: color!(0xffffff), secondary_container: color!(0xd3e8d1), on_secondary_container: color!(0x394b3a), }, tertiary: Tertiary { color: color!(0x39656d), on_tertiary: color!(0xffffff), tertiary_container: color!(0xbdeaf4), on_tertiary_container: color!(0x1f4d55), }, error: Error { color: color!(0xba1a1a), on_error: color!(0xffffff), error_container: color!(0xffdad6), on_error_container: color!(0x93000a), }, surface: Surface { color: color!(0xf7fbf2), on_surface: color!(0x181d18), on_surface_variant: color!(0x414941), surface_container: SurfaceContainer { lowest: color!(0xffffff), low: color!(0xf1f5ed), base: color!(0xebefe7), high: color!(0xe5e9e1), highest: color!(0xe0e4dc), }, }, inverse: Inverse { inverse_surface: color!(0x2d322c), inverse_on_surface: color!(0xeef2ea), inverse_primary: color!(0x9bd4a1), }, outline: Outline { color: color!(0x727970), variant: color!(0xc1c9be), }, shadow: color!(0x000000), scrim: from_argb!(0x4d000000), }; } #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "animate", derive(iced_anim::Animate))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Primary { #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub color: Color, #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub on_primary: Color, #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub primary_container: Color, #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub on_primary_container: Color, } #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "animate", derive(iced_anim::Animate))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Secondary { #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub color: Color, #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub on_secondary: Color, #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub secondary_container: Color, #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub on_secondary_container: Color, } #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "animate", derive(iced_anim::Animate))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Tertiary { #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub color: Color, #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub on_tertiary: Color, #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub tertiary_container: Color, #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub on_tertiary_container: Color, } #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "animate", derive(iced_anim::Animate))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Error { #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub color: Color, #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub on_error: Color, #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub error_container: Color, #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub on_error_container: Color, } #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "animate", derive(iced_anim::Animate))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Surface { #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub color: Color, #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub on_surface: Color, #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub on_surface_variant: Color, pub surface_container: SurfaceContainer, } #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "animate", derive(iced_anim::Animate))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct SurfaceContainer { #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub lowest: Color, #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub low: Color, #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub base: Color, #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub high: Color, #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub highest: Color, } #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "animate", derive(iced_anim::Animate))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Inverse { #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub inverse_surface: Color, #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub inverse_on_surface: Color, #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub inverse_primary: Color, } #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "animate", derive(iced_anim::Animate))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Outline { #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub color: Color, #[cfg_attr(feature = "serde", serde(with = "color_serde"))] pub variant: Color, } #[cfg(feature = "serde")] mod color_serde { use iced_widget::core::Color; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use super::utils::{color_to_argb, 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)) } pub fn serialize(color: &Color, serializer: S) -> Result where S: Serializer, { color_to_argb(*color).serialize(serializer) } }