Skip to main content

ferroui_base/styling/
theme_variant.rs

1use crate::controls::ResourceKey;
2use crate::platform::{IPlatformSettings, PlatformThemeVariant};
3use crate::{FerroLocator, LocatorExtensions, TypeInfo};
4use crate::{ferro_property, FerroObject, FerroProperty, StyledElement, StyledProperty, StyledPropertyOptions};
5use std::cell::RefCell;
6use std::fmt;
7use std::hash::{Hash, Hasher};
8use std::rc::Rc;
9use std::str::FromStr;
10
11struct Inner {
12    key: ResourceKey,
13    inherit_variant: Option<ThemeVariant>,
14}
15
16/// Specifies a UI theme variant that should be used for the control and
17/// application types.
18///
19/// Theme variants are compared by their key.
20#[derive(Clone)]
21pub struct ThemeVariant(Rc<Inner>);
22
23crate::ferro_thread_local! {
24    static DEFAULT: ThemeVariant = ThemeVariant::from_key("Default".into());
25    static LIGHT: ThemeVariant = ThemeVariant::from_key("Light".into());
26    static DARK: ThemeVariant = ThemeVariant::from_key("Dark".into());
27}
28
29crate::ferro_thread_local! {
30    /// The classes, other than styled elements, whose objects are theme
31    /// variant roots.
32    static ROOT_TYPES: RefCell<Vec<&'static TypeInfo>> = const { RefCell::new(Vec::new()) };
33}
34
35impl ThemeVariant {
36    /// Creates a theme variant.
37    ///
38    /// `key` is the key of the custom theme variant. `inherit_variant` is the
39    /// variant that this variant inherits from: if set, a resource that is
40    /// missing from the theme dictionary of the current variant is looked up
41    /// in the dictionary of the inherited one. Inheriting the default variant
42    /// is not supported.
43    pub fn new(key: impl Into<ResourceKey>, inherit_variant: Option<ThemeVariant>) -> Self {
44        if inherit_variant.as_ref().is_some_and(|v| *v == Self::default()) {
45            panic!("Inheriting default theme variant is not supported.");
46        }
47        ThemeVariant(Rc::new(Inner { key: key.into(), inherit_variant }))
48    }
49
50    fn from_key(key: ResourceKey) -> Self {
51        ThemeVariant(Rc::new(Inner { key, inherit_variant: None }))
52    }
53
54    /// Use the default theme variant: inherit the variant from the parent,
55    /// and at the root use the variant of the platform.
56    #[allow(clippy::should_implement_trait)]
57    pub fn default() -> ThemeVariant {
58        DEFAULT.with(Clone::clone)
59    }
60
61    /// Use the Light theme variant.
62    pub fn light() -> ThemeVariant {
63        LIGHT.with(Clone::clone)
64    }
65
66    /// Use the Dark theme variant.
67    pub fn dark() -> ThemeVariant {
68        DARK.with(Clone::clone)
69    }
70
71    ferro_property!(for StyledElement;
72        /// Defines the ActualThemeVariant property: the theme variant in
73        /// effect for an element, inherited down the tree. It is `None`
74        /// until a root or an element with a requested variant sets it.
75        pub fn actual_theme_variant_property() -> StyledProperty<Option<ThemeVariant>> {
76            FerroProperty::register_with::<StyledElement, _>(
77                "ActualThemeVariant",
78                StyledPropertyOptions::new(None).inherits(true),
79            )
80        }
81    );
82
83    ferro_property!(for StyledElement;
84        /// Defines the RequestedThemeVariant property: the theme variant
85        /// requested for an element and its descendants.
86        pub fn requested_theme_variant_property() -> StyledProperty<Option<ThemeVariant>> {
87            let property = FerroProperty::register::<StyledElement, _>(
88                "RequestedThemeVariant",
89                Some(ThemeVariant::default()),
90            );
91            property.changed().add_class_handler::<FerroObject>(|x, _| ThemeVariant::update_actual_theme_variant(x));
92            property
93        }
94    );
95
96    /// Key of the theme variant by which variants are compared.
97    pub fn key(&self) -> &ResourceKey {
98        &self.0.key
99    }
100
101    /// The theme variant that this variant inherits from.
102    pub fn inherit_variant(&self) -> Option<ThemeVariant> {
103        self.0.inherit_variant.clone()
104    }
105
106    /// Updates the actual theme variant of `target` from its requested
107    /// theme variant: a requested variant other than the default becomes the
108    /// actual one, otherwise the actual variant is inherited.
109    pub fn update_actual_theme_variant(target: &FerroObject) {
110        let requested =
111            target.get_value(Self::requested_theme_variant_property()).unwrap_or_else(ThemeVariant::default);
112        if requested != ThemeVariant::default() {
113            target.set_value(Self::actual_theme_variant_property(), Some(requested));
114            return;
115        }
116
117        let is_root = target.downcast_ref::<StyledElement>().is_some_and(|element| element.is_theme_variant_root())
118            || ROOT_TYPES.with_borrow(|types| types.iter().any(|type_| type_.is_assignable_from(target.get_type())));
119        if is_root {
120            target.set_value(Self::actual_theme_variant_property(), Some(Self::get_platform_theme_variant()));
121        } else {
122            target.clear_value(Self::actual_theme_variant_property());
123        }
124    }
125
126    /// Declares that the objects of a class that is not a styled element
127    /// (the application class) are roots of theme variant resolution.
128    /// Styled elements say so through the `is_theme_variant_root` virtual.
129    /// (Upstream: the `IThemeVariantRoot` interface.)
130    pub fn register_theme_variant_root_type(type_: &'static TypeInfo) {
131        ROOT_TYPES.with_borrow_mut(|types| {
132            if !types.iter().any(|t| std::ptr::eq(*t, type_)) {
133                types.push(type_);
134            }
135        });
136    }
137
138    /// The theme variant of the platform: the one of the colour values of
139    /// the platform settings, light when there are no platform settings.
140    fn get_platform_theme_variant() -> ThemeVariant {
141        let variant = FerroLocator::current()
142            .get_service::<dyn IPlatformSettings>()
143            .map(|settings| settings.get_color_values().theme_variant())
144            .unwrap_or(PlatformThemeVariant::Light);
145
146        ThemeVariant::from(variant)
147    }
148
149    /// The platform theme variant of a theme variant (the original's
150    /// explicit conversion to a nullable platform theme variant): the light
151    /// and the dark variant convert to their counterparts, another variant
152    /// converts as the variant it inherits from, and a variant that inherits
153    /// from neither, or no variant, converts to nothing.
154    pub fn to_platform_theme_variant(theme_variant: Option<&ThemeVariant>) -> Option<PlatformThemeVariant> {
155        let theme_variant = theme_variant?;
156        if *theme_variant == ThemeVariant::light() {
157            Some(PlatformThemeVariant::Light)
158        } else if *theme_variant == ThemeVariant::dark() {
159            Some(PlatformThemeVariant::Dark)
160        } else if let Some(inherit_variant) = theme_variant.inherit_variant() {
161            Self::to_platform_theme_variant(Some(&inherit_variant))
162        } else {
163            None
164        }
165    }
166}
167
168/// The original's explicit conversion from a platform theme variant.
169impl From<PlatformThemeVariant> for ThemeVariant {
170    fn from(theme_variant: PlatformThemeVariant) -> Self {
171        match theme_variant {
172            PlatformThemeVariant::Light => ThemeVariant::light(),
173            PlatformThemeVariant::Dark => ThemeVariant::dark(),
174        }
175    }
176}
177
178impl Default for ThemeVariant {
179    fn default() -> Self {
180        ThemeVariant::default()
181    }
182}
183
184impl PartialEq for ThemeVariant {
185    fn eq(&self, other: &Self) -> bool {
186        Rc::ptr_eq(&self.0, &other.0) || self.0.key == other.0.key
187    }
188}
189
190impl Eq for ThemeVariant {}
191
192impl Hash for ThemeVariant {
193    fn hash<H: Hasher>(&self, state: &mut H) {
194        self.0.key.hash(state)
195    }
196}
197
198impl fmt::Display for ThemeVariant {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        fmt::Display::fmt(&self.0.key, f)
201    }
202}
203
204impl fmt::Debug for ThemeVariant {
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206        fmt::Display::fmt(&self.0.key, f)
207    }
208}
209
210/// The error returned when parsing a theme variant from a string fails.
211#[derive(Clone, Debug, PartialEq, Eq)]
212pub struct ThemeVariantParseError;
213
214impl fmt::Display for ThemeVariantParseError {
215    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216        f.write_str("Only the built in theme variants (Default, Light, Dark) can be converted from a string.")
217    }
218}
219
220impl std::error::Error for ThemeVariantParseError {}
221
222impl FromStr for ThemeVariant {
223    type Err = ThemeVariantParseError;
224
225    /// Converts the name of a built in theme variant.
226    fn from_str(s: &str) -> Result<Self, Self::Err> {
227        match s {
228            "Default" => Ok(ThemeVariant::default()),
229            "Light" => Ok(ThemeVariant::light()),
230            "Dark" => Ok(ThemeVariant::dark()),
231            _ => Err(ThemeVariantParseError),
232        }
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    // Not from upstream: the original has no tests of the conversions.
239    use super::*;
240
241    #[test]
242    fn platform_theme_variant_converts_to_the_built_in_variants() {
243        assert_eq!(ThemeVariant::light(), ThemeVariant::from(PlatformThemeVariant::Light));
244        assert_eq!(ThemeVariant::dark(), ThemeVariant::from(PlatformThemeVariant::Dark));
245    }
246
247    #[test]
248    fn theme_variant_converts_to_the_platform_variant_it_is_or_inherits() {
249        assert_eq!(None, ThemeVariant::to_platform_theme_variant(None));
250        assert_eq!(None, ThemeVariant::to_platform_theme_variant(Some(&ThemeVariant::default())));
251        assert_eq!(
252            Some(PlatformThemeVariant::Light),
253            ThemeVariant::to_platform_theme_variant(Some(&ThemeVariant::light()))
254        );
255        assert_eq!(
256            Some(PlatformThemeVariant::Dark),
257            ThemeVariant::to_platform_theme_variant(Some(&ThemeVariant::dark()))
258        );
259
260        let custom = ThemeVariant::new("Custom", None);
261        assert_eq!(None, ThemeVariant::to_platform_theme_variant(Some(&custom)));
262
263        let inherited = ThemeVariant::new("Inherited", Some(ThemeVariant::dark()));
264        let nested = ThemeVariant::new("Nested", Some(inherited));
265        assert_eq!(Some(PlatformThemeVariant::Dark), ThemeVariant::to_platform_theme_variant(Some(&nested)));
266    }
267}