aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpml68 <contact@pml68.dev>2025-08-01 09:03:42 +0200
committerpml68 <contact@pml68.dev>2025-08-01 09:05:54 +0200
commitcea1b9b4acb4a016436cf51f43683f362674af9e (patch)
treeb90f4b121f7bb8df597a1d0451c671e5ba21c75e
parentfeat(docs): add **Features** section to README (diff)
downloadiced_material-cea1b9b4acb4a016436cf51f43683f362674af9e.tar.gz
feat: switch to `mundy` for the `system-theme` feature
-rw-r--r--Cargo.toml10
-rw-r--r--examples/styling.rs31
-rw-r--r--src/lib.rs176
3 files changed, 178 insertions, 39 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 88d9390..8e1002c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,7 +13,7 @@ rust-version = "1.85"
[features]
default = ["system-theme"]
# Adds a `System` theme variant that follows the system theme mode.
-system-theme = ["dep:dark-light"]
+system-theme = ["dep:mundy", "dep:arc-swap"]
# Provides `serde` support.
serde = ["dep:serde"]
# Provides support for animating with `iced_anim`.
@@ -31,7 +31,7 @@ qr_code = ["iced_widget/qr_code"]
[dependencies]
iced_widget = "0.14.0-dev"
-dark-light = { version = "2.0", optional = true }
+arc-swap = { version = "1.7.1", optional = true }
serde = { version = "1.0", optional = true }
[dependencies.iced_dialog]
@@ -45,6 +45,12 @@ branch = "iced/master"
features = ["derive"]
optional = true
+[dependencies.mundy]
+version = "0.1.10"
+default-features = false
+features = ["async-io", "color-scheme"]
+optional = true
+
[dev-dependencies]
iced = "0.14.0-dev"
diff --git a/examples/styling.rs b/examples/styling.rs
index a636585..127f615 100644
--- a/examples/styling.rs
+++ b/examples/styling.rs
@@ -35,6 +35,7 @@ enum Message {
TogglerToggled(bool),
PreviousTheme,
NextTheme,
+ SystemThemeChanged(Theme),
}
impl Styling {
@@ -65,6 +66,9 @@ impl Styling {
};
}
}
+ Message::SystemThemeChanged(theme) => {
+ Theme::update_system_theme(theme)
+ }
}
}
@@ -165,16 +169,23 @@ impl Styling {
}
fn subscription(&self) -> Subscription<Message> {
- keyboard::on_key_press(|key, _modifiers| match key {
- keyboard::Key::Named(
- keyboard::key::Named::ArrowUp | keyboard::key::Named::ArrowLeft,
- ) => Some(Message::PreviousTheme),
- keyboard::Key::Named(
- keyboard::key::Named::ArrowDown
- | keyboard::key::Named::ArrowRight,
- ) => Some(Message::NextTheme),
- _ => None,
- })
+ let theme_toggle =
+ keyboard::on_key_press(|key, _modifiers| match key {
+ keyboard::Key::Named(
+ keyboard::key::Named::ArrowUp
+ | keyboard::key::Named::ArrowLeft,
+ ) => Some(Message::PreviousTheme),
+ keyboard::Key::Named(
+ keyboard::key::Named::ArrowDown
+ | keyboard::key::Named::ArrowRight,
+ ) => Some(Message::NextTheme),
+ _ => None,
+ });
+
+ let system_theme =
+ Theme::subscription().map(Message::SystemThemeChanged);
+
+ Subscription::batch([theme_toggle, system_theme])
}
fn theme(&self) -> Theme {
diff --git a/src/lib.rs b/src/lib.rs
index 9e68ba0..dac1ec8 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,8 +1,21 @@
use std::borrow::Cow;
-use std::sync::LazyLock;
+#[cfg(feature = "system-theme")]
+use std::sync::{Arc, LazyLock};
-use iced_widget::core::theme::{Base, Style};
-use iced_widget::core::{Color, color};
+#[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;
@@ -47,8 +60,16 @@ macro_rules! from_argb {
}
#[cfg(feature = "system-theme")]
-pub static SYSTEM_IS_DARK: LazyLock<bool> = LazyLock::new(|| {
- dark_light::detect().is_ok_and(|mode| mode == dark_light::Mode::Dark)
+static SYSTEM_THEME: LazyLock<ArcSwap<Theme>> = 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)]
@@ -65,10 +86,10 @@ pub enum Theme {
impl Theme {
pub const ALL: &'static [Self] = &[
- Self::Dark,
- Self::Light,
#[cfg(feature = "system-theme")]
Self::System,
+ Self::Dark,
+ Self::Light,
];
pub fn new(
@@ -108,7 +129,7 @@ impl Theme {
Self::Dark => true,
Self::Light => false,
#[cfg(feature = "system-theme")]
- Self::System => *SYSTEM_IS_DARK,
+ Self::System => SYSTEM_THEME.load().is_dark(),
Self::Custom(custom) => custom.is_dark,
}
}
@@ -118,28 +139,96 @@ impl Theme {
Self::Dark => ColorScheme::DARK,
Self::Light => ColorScheme::LIGHT,
#[cfg(feature = "system-theme")]
- Self::System => {
- if *SYSTEM_IS_DARK {
- ColorScheme::DARK
- } else {
- ColorScheme::LIGHT
- }
- }
+ 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<Self> {
+ struct ThemeSubscription;
+
+ impl Recipe for ThemeSubscription {
+ type Output = Theme;
+
+ fn hash(&self, state: &mut Hasher) {
+ use std::hash::Hash;
+ std::any::TypeId::of::<Self::Output>().hash(state);
+ }
+
+ fn stream(
+ self: Box<Self>,
+ _input: EventStream,
+ ) -> BoxStream<Self::Output> {
+ 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<Message> {
+ /// // ...
+ /// }
+ ///
+ /// 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<Message> {
+ /// 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 {
- #[cfg(feature = "system-theme")]
- {
- Self::System
- }
-
- #[cfg(not(feature = "system-theme"))]
- {
- Self::Dark
+ if cfg!(feature = "system-theme") {
+ Theme::System
+ } else {
+ Theme::Dark
}
}
}
@@ -172,6 +261,17 @@ impl Base for Theme {
}
}
+#[cfg(feature = "system-theme")]
+impl From<Preferences> 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 {
@@ -203,11 +303,15 @@ impl iced_anim::Animate for Theme {
}
}
+/// 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,
}
@@ -220,10 +324,13 @@ impl From<Custom> for Theme {
impl From<Theme> for Custom {
fn from(theme: Theme) -> Self {
- Self {
- name: theme.name(),
- is_dark: theme.is_dark(),
- colorscheme: theme.colors(),
+ match theme {
+ Theme::Custom(custom) => custom,
+ theme => Self {
+ name: theme.name(),
+ is_dark: theme.is_dark(),
+ colorscheme: theme.colors(),
+ },
}
}
}
@@ -244,19 +351,34 @@ impl Clone for Custom {
}
}
+/// 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,
}