use crate::controls::ResourceKey;
use crate::platform::{IPlatformSettings, PlatformThemeVariant};
use crate::{FerroLocator, LocatorExtensions, TypeInfo};
use crate::{ferro_property, FerroObject, FerroProperty, StyledElement, StyledProperty, StyledPropertyOptions};
use std::cell::RefCell;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::rc::Rc;
use std::str::FromStr;
struct Inner {
key: ResourceKey,
inherit_variant: Option<ThemeVariant>,
}
#[derive(Clone)]
pub struct ThemeVariant(Rc<Inner>);
crate::ferro_thread_local! {
static DEFAULT: ThemeVariant = ThemeVariant::from_key("Default".into());
static LIGHT: ThemeVariant = ThemeVariant::from_key("Light".into());
static DARK: ThemeVariant = ThemeVariant::from_key("Dark".into());
}
crate::ferro_thread_local! {
static ROOT_TYPES: RefCell<Vec<&'static TypeInfo>> = const { RefCell::new(Vec::new()) };
}
impl ThemeVariant {
pub fn new(key: impl Into<ResourceKey>, inherit_variant: Option<ThemeVariant>) -> Self {
if inherit_variant.as_ref().is_some_and(|v| *v == Self::default()) {
panic!("Inheriting default theme variant is not supported.");
}
ThemeVariant(Rc::new(Inner { key: key.into(), inherit_variant }))
}
fn from_key(key: ResourceKey) -> Self {
ThemeVariant(Rc::new(Inner { key, inherit_variant: None }))
}
#[allow(clippy::should_implement_trait)]
pub fn default() -> ThemeVariant {
DEFAULT.with(Clone::clone)
}
pub fn light() -> ThemeVariant {
LIGHT.with(Clone::clone)
}
pub fn dark() -> ThemeVariant {
DARK.with(Clone::clone)
}
ferro_property!(for StyledElement;
pub fn actual_theme_variant_property() -> StyledProperty<Option<ThemeVariant>> {
FerroProperty::register_with::<StyledElement, _>(
"ActualThemeVariant",
StyledPropertyOptions::new(None).inherits(true),
)
}
);
ferro_property!(for StyledElement;
pub fn requested_theme_variant_property() -> StyledProperty<Option<ThemeVariant>> {
let property = FerroProperty::register::<StyledElement, _>(
"RequestedThemeVariant",
Some(ThemeVariant::default()),
);
property.changed().add_class_handler::<FerroObject>(|x, _| ThemeVariant::update_actual_theme_variant(x));
property
}
);
pub fn key(&self) -> &ResourceKey {
&self.0.key
}
pub fn inherit_variant(&self) -> Option<ThemeVariant> {
self.0.inherit_variant.clone()
}
pub fn update_actual_theme_variant(target: &FerroObject) {
let requested =
target.get_value(Self::requested_theme_variant_property()).unwrap_or_else(ThemeVariant::default);
if requested != ThemeVariant::default() {
target.set_value(Self::actual_theme_variant_property(), Some(requested));
return;
}
let is_root = target.downcast_ref::<StyledElement>().is_some_and(|element| element.is_theme_variant_root())
|| ROOT_TYPES.with_borrow(|types| types.iter().any(|type_| type_.is_assignable_from(target.get_type())));
if is_root {
target.set_value(Self::actual_theme_variant_property(), Some(Self::get_platform_theme_variant()));
} else {
target.clear_value(Self::actual_theme_variant_property());
}
}
pub fn register_theme_variant_root_type(type_: &'static TypeInfo) {
ROOT_TYPES.with_borrow_mut(|types| {
if !types.iter().any(|t| std::ptr::eq(*t, type_)) {
types.push(type_);
}
});
}
fn get_platform_theme_variant() -> ThemeVariant {
let variant = FerroLocator::current()
.get_service::<dyn IPlatformSettings>()
.map(|settings| settings.get_color_values().theme_variant())
.unwrap_or(PlatformThemeVariant::Light);
ThemeVariant::from(variant)
}
pub fn to_platform_theme_variant(theme_variant: Option<&ThemeVariant>) -> Option<PlatformThemeVariant> {
let theme_variant = theme_variant?;
if *theme_variant == ThemeVariant::light() {
Some(PlatformThemeVariant::Light)
} else if *theme_variant == ThemeVariant::dark() {
Some(PlatformThemeVariant::Dark)
} else if let Some(inherit_variant) = theme_variant.inherit_variant() {
Self::to_platform_theme_variant(Some(&inherit_variant))
} else {
None
}
}
}
impl From<PlatformThemeVariant> for ThemeVariant {
fn from(theme_variant: PlatformThemeVariant) -> Self {
match theme_variant {
PlatformThemeVariant::Light => ThemeVariant::light(),
PlatformThemeVariant::Dark => ThemeVariant::dark(),
}
}
}
impl Default for ThemeVariant {
fn default() -> Self {
ThemeVariant::default()
}
}
impl PartialEq for ThemeVariant {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.0, &other.0) || self.0.key == other.0.key
}
}
impl Eq for ThemeVariant {}
impl Hash for ThemeVariant {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.key.hash(state)
}
}
impl fmt::Display for ThemeVariant {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0.key, f)
}
}
impl fmt::Debug for ThemeVariant {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0.key, f)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ThemeVariantParseError;
impl fmt::Display for ThemeVariantParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Only the built in theme variants (Default, Light, Dark) can be converted from a string.")
}
}
impl std::error::Error for ThemeVariantParseError {}
impl FromStr for ThemeVariant {
type Err = ThemeVariantParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"Default" => Ok(ThemeVariant::default()),
"Light" => Ok(ThemeVariant::light()),
"Dark" => Ok(ThemeVariant::dark()),
_ => Err(ThemeVariantParseError),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn platform_theme_variant_converts_to_the_built_in_variants() {
assert_eq!(ThemeVariant::light(), ThemeVariant::from(PlatformThemeVariant::Light));
assert_eq!(ThemeVariant::dark(), ThemeVariant::from(PlatformThemeVariant::Dark));
}
#[test]
fn theme_variant_converts_to_the_platform_variant_it_is_or_inherits() {
assert_eq!(None, ThemeVariant::to_platform_theme_variant(None));
assert_eq!(None, ThemeVariant::to_platform_theme_variant(Some(&ThemeVariant::default())));
assert_eq!(
Some(PlatformThemeVariant::Light),
ThemeVariant::to_platform_theme_variant(Some(&ThemeVariant::light()))
);
assert_eq!(
Some(PlatformThemeVariant::Dark),
ThemeVariant::to_platform_theme_variant(Some(&ThemeVariant::dark()))
);
let custom = ThemeVariant::new("Custom", None);
assert_eq!(None, ThemeVariant::to_platform_theme_variant(Some(&custom)));
let inherited = ThemeVariant::new("Inherited", Some(ThemeVariant::dark()));
let nested = ThemeVariant::new("Nested", Some(inherited));
assert_eq!(Some(PlatformThemeVariant::Dark), ThemeVariant::to_platform_theme_variant(Some(&nested)));
}
}