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}