ferroui_base/styling/
theme_variant.rs1use 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#[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 static ROOT_TYPES: RefCell<Vec<&'static TypeInfo>> = const { RefCell::new(Vec::new()) };
33}
34
35impl ThemeVariant {
36 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 #[allow(clippy::should_implement_trait)]
57 pub fn default() -> ThemeVariant {
58 DEFAULT.with(Clone::clone)
59 }
60
61 pub fn light() -> ThemeVariant {
63 LIGHT.with(Clone::clone)
64 }
65
66 pub fn dark() -> ThemeVariant {
68 DARK.with(Clone::clone)
69 }
70
71 ferro_property!(for StyledElement;
72 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 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 pub fn key(&self) -> &ResourceKey {
98 &self.0.key
99 }
100
101 pub fn inherit_variant(&self) -> Option<ThemeVariant> {
103 self.0.inherit_variant.clone()
104 }
105
106 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 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 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 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
168impl 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#[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 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 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}