Skip to main content

floem/style/
recalc.rs

1//! Style recalculation change tracking.
2//!
3//! This module provides [`StyleRecalcChange`], a graduated system for tracking
4//! what kind of style recalculation is needed. Inspired by Chromium's approach,
5//! this enables optimizations like:
6//! - Skipping style recalc entirely for unchanged views
7//! - Using fast "inherited only" paths when only inherited props changed
8//! - Limiting recalc to immediate children vs entire subtrees
9//!
10//! # Comparison with ChangeFlags
11//!
12//! The existing [`ChangeFlags`] are binary: a view either needs style recalc or doesn't.
13//! [`StyleRecalcChange`] adds *why* the recalc is needed, enabling optimization decisions.
14
15use crate::{
16    ElementId,
17    style::{StyleClassRef, StyleSelector, StyleSelectors},
18};
19
20use bitflags::bitflags;
21use smallvec::SmallVec;
22
23bitflags! {
24    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
25    pub struct StyleReasonFlags: u32 {
26        /// The view has style selectors (`:hover`, `:focus`, `:disabled`, etc.) whose
27        /// matching result may have changed. Requires full `resolve_nested_maps` to re-run
28        /// since selector matching depends on interaction state.
29        const SELECTOR = 1 << 0;
30
31        /// One or more animations are actively running on this view. The animation stack
32        /// needs to be advanced and `animate_into` applied to the combined style. Implies
33        /// the view needs to be scheduled for the next frame via `schedule_style`.
34        const ANIMATION = 1 << 1;
35
36        /// A CSS-style property transition is in progress (e.g. background color, opacity).
37        /// The transition interpolator needs to be stepped and its output applied to the
38        /// view style props (`view_style_props.read_explicit`). Implies `schedule_style`.
39        const TRANSITION = 1 << 2;
40
41        /// One or more specific sub-element rectangles owned by this view need restyling,
42        /// identified by their `ElementId`. Used when a view owns multiple `ElementId`s
43        /// (e.g. a scroll view's content area, vertical scrollbar, horizontal scrollbar)
44        /// and only a subset need to be updated. The `targets` vec carries each affected
45        /// `ElementId` and the `StyleReasonSet` describing why that specific element is dirty,
46        /// allowing `style_pass` to dispatch targeted updates rather than fully restyling
47        /// the entire view.
48        const TARGET = 1 << 3;
49
50        /// Request that the `style_pass` be run again.
51        const STYLE_PASS = 1 << 4;
52
53        /// Request that the `view_style` method from the `View` trait be run again.
54        const VIEW_STYLE = 1 << 5;
55
56        /// Request that the `style_pass` be run again
57        const VISIBILITY = 1 << 6;
58
59        /// Parent's inherited scalar properties changed (font size, color, etc.).
60        /// Child must recompute computed_style but may skip resolve_nested_maps
61        /// if it has no selectors and its own style stack is clean.
62        const INHERITED_CHANGE = 1 << 7;
63
64        /// Parent's class context map changed (new class definitions visible to children).
65        /// Child must re-run resolve_nested_maps to pick up new class rules,
66        /// but inherited scalar props are unchanged so with_context values are stable.
67        const CLASS_CONTEXT_CHANGE = 1 << 8;
68
69    }
70}
71
72/// Describes why a view has been marked style-dirty. Combines a cheap bitmap of
73/// reason categories with optional payloads for the reasons that carry associated data.
74///
75/// The flags are the primary signal used to decide *how much work* `style_view` needs
76/// to do. The payload fields provide the details needed to actually do that work.
77///
78/// Invariants:
79/// - `selectors.is_some()` iff `flags.contains(SELECTOR)`
80/// - `targets.is_empty()` iff `!flags.contains(TARGET)`
81#[derive(Clone, Debug)]
82pub struct StyleReason {
83    pub flags: StyleReasonFlags,
84
85    /// The selector set that caused this view to be dirty. Present when `SELECTOR` is set.
86    /// Stored so that the style pass can skip `resolve_nested_maps` if the selectors that
87    /// changed don't actually affect any properties (e.g. a `:hover` selector exists but
88    /// none of the now-hovered selectors match any style rules on this view).
89    pub selectors: Option<StyleSelectors>,
90
91    pub classes_changed: Option<SmallVec<[StyleClassRef; 4]>>,
92
93    /// Sub-element rectangles owned by this view that need targeted restyling.
94    /// Each entry is `(element_id, reason)` where `element_id` identifies a specific
95    /// box-tree rectangle (e.g. the vertical scrollbar of a scroll view) and `reason`
96    /// describes why that rectangle is dirty (typically `SELECTOR` due to hover/press
97    /// state change on that specific hit region).
98    ///
99    /// Present when `TARGET` is set. `SmallVec<2>` because most views with sub-elements
100    /// have only a small fixed number of them (e.g. scroll view has 3 total, rarely more
101    /// than 2 dirty simultaneously).
102    pub targets: Vec<(crate::ElementId, StyleReason)>,
103}
104
105impl StyleReason {
106    pub fn empty() -> Self {
107        Self {
108            flags: StyleReasonFlags::empty(),
109            selectors: None,
110            classes_changed: None,
111            targets: Vec::new(),
112        }
113    }
114
115    pub fn with_selectors(selectors: StyleSelectors) -> Self {
116        let mut s = Self::empty();
117        s.set_selectors(selectors);
118        s
119    }
120
121    pub fn with_selector(selector: StyleSelector) -> Self {
122        let mut s = Self::empty();
123        s.set_selectors(StyleSelectors::empty().set_selector(selector, true));
124        s
125    }
126
127    pub fn is_empty(&self) -> bool {
128        self.flags.is_empty()
129    }
130
131    // --- Setters ---
132
133    pub fn set_inherited(&mut self) {
134        self.flags |= StyleReasonFlags::INHERITED_CHANGE;
135    }
136
137    pub fn set_class_cx(&mut self) {
138        self.flags |= StyleReasonFlags::CLASS_CONTEXT_CHANGE;
139    }
140
141    pub fn set_animation(&mut self) {
142        self.flags |= StyleReasonFlags::ANIMATION;
143    }
144
145    pub fn set_transition(&mut self) {
146        self.flags |= StyleReasonFlags::TRANSITION;
147    }
148
149    pub fn set_style_pass(&mut self) {
150        self.flags |= StyleReasonFlags::STYLE_PASS;
151    }
152
153    pub fn set_view_style(&mut self) {
154        self.flags |= StyleReasonFlags::VIEW_STYLE;
155    }
156
157    pub fn set_visibility(&mut self) {
158        self.flags |= StyleReasonFlags::VISIBILITY;
159    }
160
161    pub fn set_selectors(&mut self, selectors: StyleSelectors) {
162        self.flags |= StyleReasonFlags::SELECTOR;
163        self.selectors = Some(selectors);
164    }
165
166    pub fn add_target(&mut self, id: crate::ElementId, reason: StyleReason) {
167        self.flags |= StyleReasonFlags::TARGET;
168        self.targets.push((id, reason));
169    }
170
171    pub fn animation() -> Self {
172        let mut s = Self::empty();
173        s.set_animation();
174        s
175    }
176
177    pub fn style_pass() -> Self {
178        let mut s = Self::empty();
179        s.set_style_pass();
180        s
181    }
182
183    pub fn view_style() -> Self {
184        let mut s = Self::empty();
185        s.set_view_style();
186        s
187    }
188
189    pub fn visibility() -> Self {
190        let mut s = Self::empty();
191        s.set_visibility();
192        s
193    }
194
195    pub fn transition() -> Self {
196        let mut s = Self::empty();
197        s.set_transition();
198        s
199    }
200
201    pub fn inherited() -> Self {
202        let mut s = Self::empty();
203        s.set_inherited();
204        s
205    }
206
207    pub fn class_cx(classes: SmallVec<[StyleClassRef; 4]>) -> Self {
208        let mut s = Self::empty();
209        s.set_class_cx();
210        s.classes_changed = Some(classes);
211        s
212    }
213
214    // --- Queries ---
215
216    /// Returns true if this reason set requires a full cascade recomputation
217    /// (i.e. `resolve_nested_maps` must run). False means only animation/transition
218    /// stepping is needed and the cached `combined_style` can be reused.
219    pub fn needs_resolve_nested_maps(&self) -> bool {
220        self.flags.intersects(
221            StyleReasonFlags::SELECTOR
222                | StyleReasonFlags::VIEW_STYLE
223                | StyleReasonFlags::CLASS_CONTEXT_CHANGE
224                | StyleReasonFlags::INHERITED_CHANGE,
225        )
226    }
227
228    pub fn needs_animation(&self) -> bool {
229        self.flags.intersects(StyleReasonFlags::ANIMATION)
230    }
231
232    pub fn needs_property_extraction(&self) -> bool {
233        self.needs_resolve_nested_maps()
234            || self.needs_animation()
235            || self.flags.intersects(StyleReasonFlags::TRANSITION)
236            || self.flags.intersects(StyleReasonFlags::INHERITED_CHANGE)
237            || self
238                .flags
239                .intersects(StyleReasonFlags::CLASS_CONTEXT_CHANGE)
240    }
241
242    pub fn has_animation(&self) -> bool {
243        self.flags.contains(StyleReasonFlags::ANIMATION)
244    }
245
246    pub fn has_visiblity(&self) -> bool {
247        self.flags.contains(StyleReasonFlags::VISIBILITY)
248    }
249
250    pub fn has_transition(&self) -> bool {
251        self.flags.contains(StyleReasonFlags::TRANSITION)
252    }
253
254    pub fn has_selector(&self) -> bool {
255        self.flags.contains(StyleReasonFlags::SELECTOR)
256    }
257
258    pub fn has_target(&self) -> bool {
259        self.flags.contains(StyleReasonFlags::TARGET)
260    }
261
262    pub fn has_target_id(&self, id: crate::ElementId) -> bool {
263        self.targets.iter().any(|(tid, _)| *tid == id)
264    }
265
266    pub fn target_reason(&self, id: crate::ElementId) -> Option<&StyleReason> {
267        self.targets
268            .iter()
269            .find(|(tid, _)| *tid == id)
270            .map(|(_, r)| r)
271    }
272
273    // --- Merging ---
274
275    /// Merge another set into self, unioning all reasons.
276    pub fn merge(&mut self, other: StyleReason) {
277        self.flags |= other.flags;
278
279        // Merge selectors (union, not replace)
280        match (self.selectors, other.selectors) {
281            (Some(a), Some(b)) => self.selectors = Some(a | b),
282            (None, Some(b)) => self.selectors = Some(b),
283            _ => {}
284        }
285
286        // Merge classes if present
287        match (&mut self.classes_changed, other.classes_changed) {
288            (Some(a), Some(b)) => {
289                for class in b {
290                    if !a.contains(&class) {
291                        a.push(class);
292                    }
293                }
294            }
295            (None, Some(b)) => {
296                self.classes_changed = Some(b);
297            }
298            _ => {}
299        }
300
301        // Merge per-target reasons
302        for (id, reason) in other.targets {
303            if let Some((_, existing)) = self.targets.iter_mut().find(|(tid, _)| *tid == id) {
304                existing.merge(reason);
305            } else {
306                self.targets.push((id, reason));
307            }
308        }
309    }
310
311    /// Returns a new set containing only the reasons that match the given flags.
312    pub fn filter(&self, flags: StyleReasonFlags) -> StyleReason {
313        let mut out = StyleReason::empty();
314        let masked = self.flags & flags;
315        out.flags = masked;
316
317        if masked.contains(StyleReasonFlags::SELECTOR) {
318            out.selectors = self.selectors;
319        }
320        if masked.contains(StyleReasonFlags::TARGET) {
321            out.targets = self.targets.clone();
322        }
323        out
324    }
325
326    /// Remove a specific flag and its associated data.
327    pub fn clear_flag(&mut self, flag: StyleReasonFlags) {
328        self.flags.remove(flag);
329        if flag.contains(StyleReasonFlags::SELECTOR) {
330            self.selectors = None;
331        }
332        if flag.contains(StyleReasonFlags::CLASS_CONTEXT_CHANGE) {
333            self.classes_changed = None;
334        }
335        if flag.contains(StyleReasonFlags::TARGET) {
336            self.targets.clear();
337        }
338    }
339
340    pub fn clear_target(&mut self, id: crate::ElementId) {
341        self.targets.retain(|(tid, _)| *tid != id);
342        if self.targets.is_empty() {
343            self.flags.remove(StyleReasonFlags::TARGET);
344        }
345    }
346
347    pub fn for_children(&self) -> StyleReason {
348        let mut out = self.clone();
349        out.targets.clear();
350        out.flags.remove(StyleReasonFlags::TARGET);
351        out.flags.remove(StyleReasonFlags::TRANSITION);
352        out.flags.remove(StyleReasonFlags::ANIMATION);
353        out.flags.remove(StyleReasonFlags::STYLE_PASS);
354        out.flags.remove(StyleReasonFlags::VIEW_STYLE);
355        // visibility is handled later
356        out.flags.remove(StyleReasonFlags::VISIBILITY);
357
358        if let Some(selectors) = out.selectors {
359            let propagating = selectors.propagating();
360            if propagating.is_empty() {
361                out.selectors = None;
362                out.flags.remove(StyleReasonFlags::SELECTOR);
363            } else {
364                out.selectors = Some(propagating);
365            }
366        }
367
368        out
369    }
370
371    pub fn with_target(element_id: ElementId, reason: StyleReason) -> Self {
372        let mut s = Self::empty();
373        s.add_target(element_id, reason);
374        s
375    }
376
377    pub fn with_selectors_and_target(element_id: ElementId, selectors: StyleSelectors) -> Self {
378        let inner = StyleReason::with_selectors(selectors);
379        Self::with_target(element_id, inner)
380    }
381
382    /// All flags set — forces a full recalc with no fast paths possible.
383    /// Use as a fallback when the specific reason is unknown.
384    pub fn full_recalc() -> Self {
385        Self {
386            flags: StyleReasonFlags::all(),
387            selectors: None,
388            targets: Vec::new(),
389            classes_changed: None,
390        }
391    }
392}