Skip to main content

kozan_core/paint/
property_state.rs

1//! Paint property state — the visual context for display items.
2//!
3//! Chrome equivalent: `PropertyTreeState` — a 4-tuple pointing to nodes
4//! in the transform, clip, effect, and scroll property trees.
5//!
6//! For Kozan's initial implementation, we use a flat state rather than
7//! full property trees. Property trees will be added when compositing
8//! and incremental paint invalidation are needed.
9//!
10//! # Chrome mapping
11//!
12//! | Chrome | Kozan |
13//! |--------|-------|
14//! | `PropertyTreeState` | `PropertyState` |
15//! | `TransformPaintPropertyNode` | `transform` field |
16//! | `ClipPaintPropertyNode` | `clip` field |
17//! | `EffectPaintPropertyNode` | `opacity` field |
18
19use kozan_primitives::geometry::Rect;
20
21/// The visual property state affecting a group of display items.
22///
23/// Chrome equivalent: `PropertyTreeState`.
24/// Adjacent display items sharing the same `PropertyState` are grouped
25/// into a `PaintChunk`.
26///
27/// # Future
28///
29/// When compositing is added, this will reference nodes in separate
30/// transform/clip/effect property trees for incremental updates.
31#[derive(Debug, Clone, PartialEq)]
32pub struct PropertyState {
33    /// Cumulative transform (translation only for now).
34    /// Chrome: reference to `TransformPaintPropertyNode`.
35    pub transform_x: f32,
36    pub transform_y: f32,
37
38    /// Current clip rectangle. `None` = no clip.
39    /// Chrome: reference to `ClipPaintPropertyNode`.
40    pub clip: Option<Rect>,
41
42    /// Current opacity (1.0 = fully opaque).
43    /// Chrome: reference to `EffectPaintPropertyNode`.
44    pub opacity: f32,
45}
46
47impl Default for PropertyState {
48    fn default() -> Self {
49        Self {
50            transform_x: 0.0,
51            transform_y: 0.0,
52            clip: None,
53            opacity: 1.0,
54        }
55    }
56}
57
58impl PropertyState {
59    /// Create a root property state (no transform, no clip, full opacity).
60    #[must_use]
61    pub fn root() -> Self {
62        Self::default()
63    }
64
65    /// Whether this state is the identity (no visual effects applied).
66    #[must_use]
67    pub fn is_identity(&self) -> bool {
68        self.transform_x == 0.0
69            && self.transform_y == 0.0
70            && self.clip.is_none()
71            && self.opacity == 1.0
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn root_is_identity() {
81        let state = PropertyState::root();
82        assert!(state.is_identity());
83    }
84
85    #[test]
86    fn non_identity_transform() {
87        let state = PropertyState {
88            transform_x: 10.0,
89            ..PropertyState::default()
90        };
91        assert!(!state.is_identity());
92    }
93
94    #[test]
95    fn non_identity_opacity() {
96        let state = PropertyState {
97            opacity: 0.5,
98            ..PropertyState::default()
99        };
100        assert!(!state.is_identity());
101    }
102
103    #[test]
104    fn non_identity_clip() {
105        let state = PropertyState {
106            clip: Some(Rect::new(0.0, 0.0, 100.0, 100.0)),
107            ..PropertyState::default()
108        };
109        assert!(!state.is_identity());
110    }
111
112    #[test]
113    fn equality() {
114        let a = PropertyState::root();
115        let b = PropertyState::root();
116        assert_eq!(a, b);
117    }
118}