ailloli_ui_runtime/app/
invalidation.rs

1//! Retained-work strengths and host-visible aggregate frame plans.
2
3use std::cmp::Ordering;
4
5/// Smallest unit of retained UI work requested for an element.
6///
7/// The ordering is intentional: several requests for the same element are
8/// coalesced to the strongest level (`Paint < Layout < Build`). Hosts only see
9/// the aggregate [`FrameWorkPlan`]; exact element roots remain runtime-local.
10/// The enum is non-exhaustive so downstream matches require a wildcard arm.
11///
12/// # Examples
13///
14/// ```
15/// use ailloli_ui_runtime::app::Invalidation;
16/// assert!(Invalidation::Paint < Invalidation::Layout);
17/// assert!(Invalidation::Layout < Invalidation::Build);
18/// ```
19#[non_exhaustive]
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub enum Invalidation {
22    /// Repaint from already reconciled and laid-out retained state.
23    Paint,
24    /// Recompute layout, then paint. Ancestors may be affected; siblings are
25    /// not invalidated unless their own inputs or constraints change.
26    Layout,
27    /// Rebuild the owning component, then layout and paint its affected path.
28    Build,
29}
30
31/// Provides the operations defined for Invalidation.
32impl Invalidation {
33    /// Returns the stable coalescing rank: paint 0, layout 1, build 2.
34    ///
35    /// # Examples
36    ///
37    /// ```
38    /// use ailloli_ui_runtime::app::Invalidation;
39    /// assert_eq!(Invalidation::Paint.rank(), 0);
40    /// assert_eq!(Invalidation::Layout.rank(), 1);
41    /// assert_eq!(Invalidation::Build.rank(), 2);
42    /// ```
43    pub const fn rank(self) -> u8 {
44        match self {
45            Self::Paint => 0,
46            Self::Layout => 1,
47            Self::Build => 2,
48        }
49    }
50
51    /// Coalesces two requests without losing the stronger unit of work.
52    ///
53    /// The operation is commutative, associative, and idempotent for the current
54    /// variants; it returns whichever value has the greater [`Self::rank`].
55    ///
56    /// # Examples
57    ///
58    /// ```
59    /// use ailloli_ui_runtime::app::Invalidation;
60    /// assert_eq!(Invalidation::Paint.merge(Invalidation::Build), Invalidation::Build);
61    /// assert_eq!(Invalidation::Layout.merge(Invalidation::Paint), Invalidation::Layout);
62    /// ```
63    pub const fn merge(self, other: Self) -> Self {
64        if self.rank() >= other.rank() {
65            self
66        } else {
67            other
68        }
69    }
70
71    /// Returns `true` only for component-build invalidation.
72    ///
73    /// # Examples
74    ///
75    /// ```
76    /// use ailloli_ui_runtime::app::Invalidation;
77    /// assert!(Invalidation::Build.needs_build());
78    /// assert!(!Invalidation::Layout.needs_build());
79    /// ```
80    pub const fn needs_build(self) -> bool {
81        matches!(self, Self::Build)
82    }
83
84    /// Returns `true` for layout and build invalidations.
85    ///
86    /// Build implies the layout pass that follows reconciliation.
87    ///
88    /// # Examples
89    ///
90    /// ```
91    /// use ailloli_ui_runtime::app::Invalidation;
92    /// assert!(!Invalidation::Paint.needs_layout());
93    /// assert!(Invalidation::Layout.needs_layout());
94    /// assert!(Invalidation::Build.needs_layout());
95    /// ```
96    pub const fn needs_layout(self) -> bool {
97        matches!(self, Self::Layout | Self::Build)
98    }
99
100    /// Returns `true` for every current invalidation strength.
101    ///
102    /// Any retained-work request ultimately requires painting a presented frame.
103    ///
104    /// # Examples
105    ///
106    /// ```
107    /// use ailloli_ui_runtime::app::Invalidation;
108    /// assert!(Invalidation::Paint.needs_paint());
109    /// assert!(Invalidation::Build.needs_paint());
110    /// ```
111    pub const fn needs_paint(self) -> bool {
112        true
113    }
114}
115
116/// Implements the Ord contract for Invalidation.
117impl Ord for Invalidation {
118    /// Compares two values using the documented total order.
119    fn cmp(&self, other: &Self) -> Ordering {
120        self.rank().cmp(&other.rank())
121    }
122}
123
124/// Implements the PartialOrd contract for Invalidation.
125impl PartialOrd for Invalidation {
126    /// Delegates partial comparison to the total order.
127    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
128        Some(self.cmp(other))
129    }
130}
131
132/// Provider-neutral aggregate consumed by a presentation host.
133///
134/// Element identifiers and propagation roots deliberately do not cross this
135/// boundary. The runtime retains those details for incremental reconciliation
136/// and layout.
137///
138/// The fields are intentionally private: hosts can ask which frame stages are
139/// necessary but cannot infer or manipulate retained element roots.
140///
141/// # Examples
142///
143/// ```
144/// use ailloli_ui_runtime::app::{FrameWorkPlan, Invalidation};
145/// let plan = FrameWorkPlan::from_invalidation(Invalidation::Layout);
146/// assert!(!plan.needs_build());
147/// assert!(plan.needs_layout() && plan.needs_paint());
148/// ```
149#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
150pub struct FrameWorkPlan {
151    /// Whether retained components must be rebuilt before layout.
152    needs_build: bool,
153    /// Whether geometry must be recomputed before painting.
154    needs_layout: bool,
155    /// Whether a new scene must be painted for presentation.
156    needs_paint: bool,
157}
158
159/// Provides the operations defined for FrameWorkPlan.
160impl FrameWorkPlan {
161    /// Returns a plan with no build, layout, or paint work.
162    ///
163    /// This equals [`Default::default`].
164    ///
165    /// # Examples
166    ///
167    /// ```
168    /// use ailloli_ui_runtime::app::FrameWorkPlan;
169    /// assert_eq!(FrameWorkPlan::none(), FrameWorkPlan::default());
170    /// assert!(FrameWorkPlan::none().is_empty());
171    /// ```
172    pub const fn none() -> Self {
173        Self {
174            needs_build: false,
175            needs_layout: false,
176            needs_paint: false,
177        }
178    }
179
180    /// Converts one invalidation strength into its implied frame stages.
181    ///
182    /// Paint maps to paint only; layout maps to layout plus paint; build maps to
183    /// all three stages.
184    ///
185    /// # Examples
186    ///
187    /// ```
188    /// use ailloli_ui_runtime::app::{FrameWorkPlan, Invalidation};
189    /// let plan = FrameWorkPlan::from_invalidation(Invalidation::Build);
190    /// assert!(plan.needs_build() && plan.needs_layout() && plan.needs_paint());
191    /// ```
192    pub const fn from_invalidation(invalidation: Invalidation) -> Self {
193        Self {
194            needs_build: invalidation.needs_build(),
195            needs_layout: invalidation.needs_layout(),
196            needs_paint: invalidation.needs_paint(),
197        }
198    }
199
200    /// Returns whether component reconciliation/build work is required.
201    ///
202    /// # Examples
203    ///
204    /// ```
205    /// use ailloli_ui_runtime::app::{FrameWorkPlan, Invalidation};
206    /// assert!(FrameWorkPlan::from_invalidation(Invalidation::Build).needs_build());
207    /// assert!(!FrameWorkPlan::from_invalidation(Invalidation::Layout).needs_build());
208    /// ```
209    pub const fn needs_build(self) -> bool {
210        self.needs_build
211    }
212
213    /// Returns whether retained layout work is required.
214    ///
215    /// # Examples
216    ///
217    /// ```
218    /// use ailloli_ui_runtime::app::{FrameWorkPlan, Invalidation};
219    /// assert!(FrameWorkPlan::from_invalidation(Invalidation::Layout).needs_layout());
220    /// assert!(!FrameWorkPlan::from_invalidation(Invalidation::Paint).needs_layout());
221    /// ```
222    pub const fn needs_layout(self) -> bool {
223        self.needs_layout
224    }
225
226    /// Returns whether scene painting is required.
227    ///
228    /// # Examples
229    ///
230    /// ```
231    /// use ailloli_ui_runtime::app::{FrameWorkPlan, Invalidation};
232    /// assert!(FrameWorkPlan::from_invalidation(Invalidation::Paint).needs_paint());
233    /// assert!(!FrameWorkPlan::none().needs_paint());
234    /// ```
235    pub const fn needs_paint(self) -> bool {
236        self.needs_paint
237    }
238
239    /// Returns `true` exactly when painting is not required.
240    ///
241    /// Public constructors preserve the invariant that build/layout imply paint,
242    /// so this is equivalent to all stages being false.
243    ///
244    /// # Examples
245    ///
246    /// ```
247    /// use ailloli_ui_runtime::app::{FrameWorkPlan, Invalidation};
248    /// assert!(FrameWorkPlan::none().is_empty());
249    /// assert!(!FrameWorkPlan::from_invalidation(Invalidation::Paint).is_empty());
250    /// ```
251    pub const fn is_empty(self) -> bool {
252        !self.needs_paint
253    }
254
255    /// Combines two plans by OR-ing each stage flag.
256    ///
257    /// # Examples
258    ///
259    /// ```
260    /// use ailloli_ui_runtime::app::{FrameWorkPlan, Invalidation};
261    /// let plan = FrameWorkPlan::none().merge(FrameWorkPlan::from_invalidation(Invalidation::Layout));
262    /// assert!(!plan.needs_build());
263    /// assert!(plan.needs_layout() && plan.needs_paint());
264    /// ```
265    pub const fn merge(self, other: Self) -> Self {
266        Self {
267            needs_build: self.needs_build || other.needs_build,
268            needs_layout: self.needs_layout || other.needs_layout,
269            needs_paint: self.needs_paint || other.needs_paint,
270        }
271    }
272}
273
274#[cfg(test)]
275/// Tests implementation details.
276mod tests {
277    use super::*;
278
279    #[test]
280    /// Verifies that invalidations form the locked work order.
281    fn invalidations_form_the_locked_work_order() {
282        assert!(Invalidation::Paint < Invalidation::Layout);
283        assert!(Invalidation::Layout < Invalidation::Build);
284        assert_eq!(
285            Invalidation::Paint.merge(Invalidation::Build),
286            Invalidation::Build
287        );
288        assert_eq!(
289            Invalidation::Layout.merge(Invalidation::Paint),
290            Invalidation::Layout
291        );
292    }
293
294    #[test]
295    /// Verifies that frame plan exposes only aggregate work.
296    fn frame_plan_exposes_only_aggregate_work() {
297        assert_eq!(
298            FrameWorkPlan::from_invalidation(Invalidation::Paint),
299            FrameWorkPlan {
300                needs_build: false,
301                needs_layout: false,
302                needs_paint: true,
303            }
304        );
305        assert_eq!(
306            FrameWorkPlan::from_invalidation(Invalidation::Build),
307            FrameWorkPlan {
308                needs_build: true,
309                needs_layout: true,
310                needs_paint: true,
311            }
312        );
313    }
314}