Skip to main content

floem/style/
props.rs

1//! Style property traits, classes, and keys.
2//!
3//! This module provides the core traits and types for defining and working with
4//! style properties:
5//! - [`StyleClass`] - trait for defining style classes
6//! - [`StyleProp`] - trait for defining style properties
7//! - [`StylePropReader`] - trait for reading properties from styles
8//! - [`StyleKey`] - unique identifier for style entries
9//! - Macros: `style_class!`, `prop!`, `prop_extractor!`
10
11use std::any::{Any, type_name};
12use std::fmt::{self, Debug};
13use std::hash::{BuildHasherDefault, Hash, Hasher};
14use std::ptr;
15use std::rc::Rc;
16
17use imbl::shared_ptr::DefaultSharedPtr;
18use rustc_hash::FxHasher;
19
20#[cfg(not(target_arch = "wasm32"))]
21use std::time::Instant;
22#[cfg(target_arch = "wasm32")]
23use web_time::Instant;
24
25use crate::view::{IntoView, View};
26use crate::views::Label;
27
28use super::Style;
29use super::selectors::StyleSelectors;
30use super::transition::TransitionState;
31use super::values::{CombineResult, StyleMapValue, StylePropValue, StyleValue};
32
33// ============================================================================
34// StyleClass
35// ============================================================================
36
37pub trait StyleClass: Default + Copy + 'static {
38    fn key() -> StyleKey;
39    fn class_ref() -> StyleClassRef {
40        StyleClassRef { key: Self::key() }
41    }
42}
43
44#[derive(Debug, Clone)]
45pub struct StyleClassInfo {
46    pub(crate) name: fn() -> &'static str,
47}
48
49impl StyleClassInfo {
50    pub const fn new<Name>() -> Self {
51        StyleClassInfo {
52            name: || std::any::type_name::<Name>(),
53        }
54    }
55}
56
57#[derive(Copy, Clone, Debug, PartialEq)]
58pub struct StyleClassRef {
59    pub key: StyleKey,
60}
61
62macro_rules! style_key_selector {
63    ($v:vis $name:ident, $sel:expr) => {
64        fn $name() -> $crate::style::StyleKey {
65            static INFO: $crate::style::StyleKeyInfo = $crate::style::StyleKeyInfo::Selector($sel);
66            $crate::style::StyleKey { info: &INFO }
67        }
68    };
69}
70
71pub(crate) use style_key_selector;
72
73#[macro_export]
74macro_rules! style_class {
75    ($(#[$meta:meta])* $v:vis $name:ident) => {
76        $(#[$meta])*
77        #[derive(Default, Copy, Clone)]
78        $v struct $name;
79
80        impl $crate::style::StyleClass for $name {
81            fn key() -> $crate::style::StyleKey {
82                static INFO: $crate::style::StyleKeyInfo = $crate::style::StyleKeyInfo::Class(
83                    $crate::style::StyleClassInfo::new::<$name>()
84                );
85                $crate::style::StyleKey { info: &INFO }
86            }
87        }
88    };
89}
90
91// ============================================================================
92// StyleProp
93// ============================================================================
94
95pub trait StyleProp: Default + Copy + 'static {
96    type Type: StylePropValue;
97    fn key() -> StyleKey;
98    fn prop_ref() -> StylePropRef {
99        StylePropRef { key: Self::key() }
100    }
101    fn default_value() -> Self::Type;
102}
103
104pub(crate) type InterpolateFn =
105    fn(val1: &dyn Any, val2: &dyn Any, time: f64) -> Option<Rc<dyn Any>>;
106
107pub(crate) type CombineFn = fn(val1: Rc<dyn Any>, val2: Rc<dyn Any>) -> Rc<dyn Any>;
108
109/// Function pointer type for computing content hash of a style value.
110pub(crate) type HashAnyFn = fn(val: &dyn Any) -> u64;
111
112/// Function pointer type for comparing two style values for equality.
113pub(crate) type EqAnyFn = fn(val1: &dyn Any, val2: &dyn Any) -> bool;
114
115#[derive(Debug)]
116pub struct StylePropInfo {
117    pub(crate) name: fn() -> &'static str,
118    pub(crate) inherited: bool,
119    #[allow(unused)]
120    pub(crate) default_as_any: fn() -> Rc<dyn Any>,
121    pub(crate) interpolate: InterpolateFn,
122    pub(crate) debug_any: fn(val: &dyn Any) -> String,
123    pub(crate) debug_view: fn(val: &dyn Any) -> Option<Box<dyn View>>,
124    pub(crate) combine: CombineFn,
125    pub(crate) transition_key: StyleKey,
126    /// Computes a content-based hash for a style value.
127    pub(crate) hash_any: HashAnyFn,
128    /// Compares two style values for equality.
129    pub(crate) eq_any: EqAnyFn,
130}
131
132impl StylePropInfo {
133    pub const fn new<Name, T: StylePropValue + 'static>(
134        inherited: bool,
135        default_as_any: fn() -> Rc<dyn Any>,
136        transition_key: StyleKey,
137    ) -> Self {
138        StylePropInfo {
139            name: || std::any::type_name::<Name>(),
140            inherited,
141            default_as_any,
142            debug_any: |val| {
143                if let Some(v) = val.downcast_ref::<StyleMapValue<T>>() {
144                    match v {
145                        StyleMapValue::Val(v) | StyleMapValue::Animated(v) => format!("{v:?}"),
146
147                        StyleMapValue::Unset => "Unset".to_owned(),
148                    }
149                } else {
150                    panic!(
151                        "expected type {} for property {}",
152                        type_name::<T>(),
153                        std::any::type_name::<Name>(),
154                    )
155                }
156            },
157            interpolate: |val1, val2, time| {
158                if let (Some(v1), Some(v2)) = (
159                    val1.downcast_ref::<StyleMapValue<T>>(),
160                    val2.downcast_ref::<StyleMapValue<T>>(),
161                ) {
162                    if let (
163                        StyleMapValue::Val(v1) | StyleMapValue::Animated(v1),
164                        StyleMapValue::Val(v2) | StyleMapValue::Animated(v2),
165                    ) = (v1, v2)
166                    {
167                        v1.interpolate(v2, time)
168                            .map(|val| Rc::new(StyleMapValue::Animated(val)) as Rc<dyn Any>)
169                    } else {
170                        None
171                    }
172                } else {
173                    panic!(
174                        "expected type {} for property {}. Got typeids {:?} and {:?}",
175                        type_name::<T>(),
176                        std::any::type_name::<Name>(),
177                        val1.type_id(),
178                        val2.type_id()
179                    )
180                }
181            },
182            debug_view: |val| {
183                if let Some(v) = val.downcast_ref::<StyleMapValue<T>>() {
184                    match v {
185                        StyleMapValue::Val(v) | StyleMapValue::Animated(v) => v.debug_view(),
186
187                        StyleMapValue::Unset => Some(Label::new("Unset").into_any()),
188                    }
189                } else {
190                    panic!(
191                        "expected type {} for property {}",
192                        type_name::<T>(),
193                        std::any::type_name::<Name>(),
194                    )
195                }
196            },
197            combine: |val1, val2| {
198                if let (Some(v1), Some(v2)) = (
199                    val1.downcast_ref::<StyleMapValue<T>>(),
200                    val2.downcast_ref::<StyleMapValue<T>>(),
201                ) {
202                    match (v1, v2) {
203                        (StyleMapValue::Val(a), StyleMapValue::Val(b)) => match a.combine(b) {
204                            CombineResult::Other => val2,
205                            CombineResult::New(result) => {
206                                Rc::new(StyleMapValue::Val(result)) as Rc<dyn Any>
207                            }
208                        },
209                        (StyleMapValue::Unset, _) => val2,
210                        (_, StyleMapValue::Unset) => val2,
211                        (
212                            StyleMapValue::Val(a) | StyleMapValue::Animated(a),
213                            StyleMapValue::Animated(b) | StyleMapValue::Val(b),
214                        ) => match a.combine(b) {
215                            CombineResult::Other => val2,
216                            CombineResult::New(result) => {
217                                Rc::new(StyleMapValue::Animated(result)) as Rc<dyn Any>
218                            }
219                        },
220                    }
221                } else {
222                    panic!(
223                        "expected type {} for property {}. Got typeids {:?} and {:?}",
224                        type_name::<StyleMapValue<T>>(),
225                        std::any::type_name::<Name>(),
226                        val1.type_id(),
227                        val2.type_id()
228                    )
229                }
230            },
231            transition_key,
232            hash_any: |val| {
233                if let Some(v) = val.downcast_ref::<StyleMapValue<T>>() {
234                    match v {
235                        StyleMapValue::Val(v) | StyleMapValue::Animated(v) => v.content_hash(),
236                        StyleMapValue::Unset => 0, // Stable hash for unset
237                    }
238                } else {
239                    panic!(
240                        "expected type {} for property {}",
241                        type_name::<T>(),
242                        std::any::type_name::<Name>(),
243                    )
244                }
245            },
246            eq_any: |val1, val2| {
247                if let (Some(v1), Some(v2)) = (
248                    val1.downcast_ref::<StyleMapValue<T>>(),
249                    val2.downcast_ref::<StyleMapValue<T>>(),
250                ) {
251                    match (v1, v2) {
252                        (
253                            StyleMapValue::Val(a) | StyleMapValue::Animated(a),
254                            StyleMapValue::Val(b) | StyleMapValue::Animated(b),
255                        ) => {
256                            // Compare by content hash since we don't have PartialEq
257                            a.content_hash() == b.content_hash()
258                        }
259                        (StyleMapValue::Unset, StyleMapValue::Unset) => true,
260                        _ => false,
261                    }
262                } else {
263                    panic!(
264                        "expected type {} for property {}. Got typeids {:?} and {:?}",
265                        type_name::<T>(),
266                        std::any::type_name::<Name>(),
267                        val1.type_id(),
268                        val2.type_id()
269                    )
270                }
271            },
272        }
273    }
274}
275
276#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
277pub struct StylePropRef {
278    pub key: StyleKey,
279}
280
281impl StylePropRef {
282    pub(crate) fn info(&self) -> &StylePropInfo {
283        if let StyleKeyInfo::Prop(prop) = self.key.info {
284            prop
285        } else {
286            panic!()
287        }
288    }
289}
290
291// ============================================================================
292// StylePropReader
293// ============================================================================
294
295pub trait StylePropReader {
296    type State: Debug;
297    type Type: Clone;
298
299    /// Reads the property from the style.
300    /// Returns true if the property changed.
301    fn read(
302        state: &mut Self::State,
303        style: &Style,
304        fallback: &Style,
305        now: &Instant,
306        request_transition: &mut bool,
307    ) -> bool;
308
309    fn get(state: &Self::State) -> Self::Type;
310    fn new() -> Self::State;
311}
312
313impl<P: StyleProp> StylePropReader for P {
314    type State = (P::Type, TransitionState<P::Type>);
315    type Type = P::Type;
316
317    // returns true if the value has changed
318    fn read(
319        state: &mut Self::State,
320        style: &Style,
321        fallback: &Style,
322        now: &Instant,
323        request_transition: &mut bool,
324    ) -> bool {
325        // get the style property
326        let style_value = style.get_prop_style_value::<P>();
327        let mut prop_animated = false;
328        let new = match style_value {
329            StyleValue::Animated(val) => {
330                *request_transition = true;
331                prop_animated = true;
332                val
333            }
334            StyleValue::Val(val) => val,
335            StyleValue::Unset | StyleValue::Base => fallback
336                .get_prop::<P>()
337                .unwrap_or_else(|| P::default_value()),
338        };
339        // set the transition state to the transition if one is found
340        state.1.read(
341            style
342                .get_transition::<P>()
343                .or_else(|| fallback.get_transition::<P>()),
344        );
345
346        // there is a previously stored value in state.0. if the values are different, a transition should be started if there is one
347        let changed = new != state.0;
348        if changed && !prop_animated {
349            state.1.transition(&Self::get(state), &new);
350            state.0 = new;
351        } else if prop_animated {
352            state.0 = new;
353        }
354        changed | state.1.step(now, request_transition)
355    }
356
357    // get the current value from the transition state if one is active, else just return the value that was read from the style map
358    fn get(state: &Self::State) -> Self::Type {
359        state.1.get(&state.0)
360    }
361
362    fn new() -> Self::State {
363        (P::default_value(), TransitionState::default())
364    }
365}
366
367impl<P: StyleProp> StylePropReader for Option<P> {
368    type State = Option<P::Type>;
369    type Type = Option<P::Type>;
370    fn read(
371        state: &mut Self::State,
372        style: &Style,
373        fallback: &Style,
374        _now: &Instant,
375        _transition: &mut bool,
376    ) -> bool {
377        let new = style.get_prop::<P>().or_else(|| fallback.get_prop::<P>());
378        let changed = new != *state;
379        *state = new;
380        changed
381    }
382    fn get(state: &Self::State) -> Self::Type {
383        state.clone()
384    }
385    fn new() -> Self::State {
386        None
387    }
388}
389
390// ============================================================================
391// ExtractorField
392// ============================================================================
393
394#[derive(Clone)]
395pub struct ExtractorField<R: StylePropReader> {
396    state: R::State,
397}
398
399impl<R: StylePropReader> Debug for ExtractorField<R> {
400    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
401        self.state.fmt(f)
402    }
403}
404
405impl<R: StylePropReader> ExtractorField<R> {
406    pub fn read(
407        &mut self,
408        style: &Style,
409        fallback: &Style,
410        now: &Instant,
411        request_transition: &mut bool,
412    ) -> bool {
413        R::read(&mut self.state, style, fallback, now, request_transition)
414    }
415    pub fn get(&self) -> R::Type {
416        R::get(&self.state)
417    }
418    #[allow(clippy::new_without_default)]
419    pub fn new() -> Self {
420        Self { state: R::new() }
421    }
422}
423
424impl<R: StylePropReader> PartialEq for ExtractorField<R>
425where
426    R::Type: PartialEq,
427{
428    fn eq(&self, other: &Self) -> bool {
429        self.get() == other.get()
430    }
431}
432
433impl<R: StylePropReader> Eq for ExtractorField<R> where R::Type: Eq {}
434
435impl<R: StylePropReader> std::hash::Hash for ExtractorField<R>
436where
437    R::Type: std::hash::Hash,
438{
439    fn hash<H: Hasher>(&self, state: &mut H) {
440        self.get().hash(state)
441    }
442}
443
444// ============================================================================
445// Macros
446// ============================================================================
447
448#[macro_export]
449macro_rules! prop {
450    ($(#[$meta:meta])* $v:vis $name:ident: $ty:ty { $($options:tt)* } = $default:expr
451    ) => {
452        $(#[$meta])*
453        #[derive(Default, Copy, Clone)]
454        #[allow(missing_docs)]
455        $v struct $name;
456        impl $crate::style::StyleProp for $name {
457            type Type = $ty;
458            fn key() -> $crate::style::StyleKey {
459                static TRANSITION_INFO: $crate::style::StyleKeyInfo = $crate::style::StyleKeyInfo::Transition;
460                static INFO: $crate::style::StyleKeyInfo = $crate::style::StyleKeyInfo::Prop($crate::style::StylePropInfo::new::<$name, $ty>(
461                    prop!([impl inherited][$($options)*]),
462                    || std::rc::Rc::new($crate::style::StyleMapValue::Val($name::default_value())),
463                    $crate::style::StyleKey { info: &TRANSITION_INFO },
464                ));
465                $crate::style::StyleKey { info: &INFO }
466            }
467            fn default_value() -> Self::Type {
468                $default
469            }
470        }
471    };
472    ([impl inherited][inherited]) => {
473        true
474    };
475    ([impl inherited][]) => {
476        false
477    };
478}
479
480#[macro_export]
481macro_rules! prop_extractor {
482    (
483        $(#[$attrs:meta])* $vis:vis $name:ident {
484            $($prop_vis:vis $prop:ident: $reader:ty),*
485            $(,)?
486        }
487    ) => {
488        #[derive(Debug, Clone)]
489        $(#[$attrs])?
490        $vis struct $name {
491            $(
492                $prop_vis $prop: $crate::style::ExtractorField<$reader>,
493            )*
494        }
495
496        impl $name {
497            #[allow(dead_code)]
498            $vis fn read_style(&mut self, cx: &mut $crate::context::StyleCx, style: &$crate::style::Style) -> bool {
499                let mut transition = false;
500                let changed = false $(| self.$prop.read(style, style, &cx.now(), &mut transition))*;
501                if transition {
502                    cx.request_transition();
503                }
504                changed
505            }
506
507           #[allow(dead_code)]
508            $vis fn read(&mut self, cx: &mut $crate::context::StyleCx) -> bool {
509                let mut transition = false;
510                let changed = self.read_explicit(&cx.direct_style(), &cx.indirect_style(), &cx.now(), &mut transition);
511                if transition {
512                    cx.request_transition();
513                }
514                changed
515            }
516
517            #[allow(dead_code)]
518            $vis fn read_explicit(
519                &mut self,
520                style: &$crate::style::Style,
521                fallback: &$crate::style::Style,
522                #[cfg(not(target_arch = "wasm32"))]
523                now: &std::time::Instant,
524                #[cfg(target_arch = "wasm32")]
525                now: &web_time::Instant,
526                request_transition: &mut bool
527            ) -> bool {
528                false $(| self.$prop.read(style, fallback, now, request_transition))*
529            }
530
531            $($prop_vis fn $prop(&self) -> <$reader as $crate::style::StylePropReader>::Type
532            {
533                self.$prop.get()
534            })*
535        }
536
537        impl Default for $name {
538            fn default() -> Self {
539                Self {
540                    $(
541                        $prop: $crate::style::ExtractorField::new(),
542                    )*
543                }
544            }
545        }
546    };
547}
548
549// ============================================================================
550// StyleKey
551// ============================================================================
552
553#[derive(Debug)]
554pub enum StyleKeyInfo {
555    Transition,
556    Prop(StylePropInfo),
557    Selector(StyleSelectors),
558    Class(StyleClassInfo),
559    /// Storage for context mapping closures.
560    ContextMappings,
561}
562
563pub(crate) static CONTEXT_MAPPINGS_INFO: StyleKeyInfo = StyleKeyInfo::ContextMappings;
564
565#[derive(Copy, Clone)]
566pub struct StyleKey {
567    pub info: &'static StyleKeyInfo,
568}
569
570impl StyleKey {
571    pub(crate) fn debug_any(&self, value: &dyn Any) -> String {
572        match self.info {
573            StyleKeyInfo::Selector(selectors) => selectors.debug_string(),
574            StyleKeyInfo::Transition | StyleKeyInfo::ContextMappings => String::new(),
575            StyleKeyInfo::Class(info) => (info.name)().to_string(),
576            StyleKeyInfo::Prop(v) => (v.debug_any)(value),
577        }
578    }
579    pub(crate) fn inherited(&self) -> bool {
580        match self.info {
581            StyleKeyInfo::Selector(..)
582            | StyleKeyInfo::Transition
583            | StyleKeyInfo::ContextMappings => false,
584            StyleKeyInfo::Class(..) => true,
585            StyleKeyInfo::Prop(v) => v.inherited,
586        }
587    }
588}
589
590impl PartialEq for StyleKey {
591    fn eq(&self, other: &Self) -> bool {
592        ptr::eq(self.info, other.info)
593    }
594}
595
596impl Hash for StyleKey {
597    fn hash<H: Hasher>(&self, state: &mut H) {
598        state.write_usize(self.info as *const _ as usize)
599    }
600}
601
602impl Eq for StyleKey {}
603
604impl Debug for StyleKey {
605    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
606        match self.info {
607            StyleKeyInfo::Selector(selectors) => {
608                write!(f, "selectors: {}", selectors.debug_string())
609            }
610            StyleKeyInfo::Transition => write!(f, "transition"),
611            StyleKeyInfo::ContextMappings => write!(f, "ContextMappings"),
612            StyleKeyInfo::Class(v) => write!(f, "{}", (v.name)()),
613            StyleKeyInfo::Prop(v) => write!(f, "{}", (v.name)()),
614        }
615    }
616}
617
618// ============================================================================
619// ImHashMap
620// ============================================================================
621
622pub(crate) type ImHashMap<K, V> =
623    imbl::GenericHashMap<K, V, BuildHasherDefault<FxHasher>, DefaultSharedPtr>;