ailloli_ui_runtime/layout/
layout_commit.rs1use ailloli_ui_core::geometry::{Offset, Rect};
4use ailloli_ui_core::ids::ElementId;
5
6use crate::app::{Invalidation, RuntimeHandle};
7use crate::component::reactive::{with_untracked_reads, ReactiveReadScope, ReactiveStage};
8use crate::element::{ElementKind, ElementTree};
9use crate::layout::LayoutCtx;
10
11pub fn commit_layout_element<A: 'static>(
46 tree: &mut ElementTree<A>,
47 ctx: &mut LayoutCtx<'_>,
48 element_id: ElementId,
49 origin: Offset,
50) {
51 commit_layout_element_impl(tree, None, ctx, element_id, origin);
52}
53
54pub(crate) fn commit_layout_element_observed<A: 'static>(
56 tree: &mut ElementTree<A>,
57 runtime: &RuntimeHandle<A>,
58 ctx: &mut LayoutCtx<'_>,
59 element_id: ElementId,
60 origin: Offset,
61) {
62 commit_layout_element_impl(tree, Some(runtime), ctx, element_id, origin);
63}
64
65fn commit_layout_element_impl<A: 'static>(
67 tree: &mut ElementTree<A>,
68 runtime: Option<&RuntimeHandle<A>>,
69 ctx: &mut LayoutCtx<'_>,
70 element_id: ElementId,
71 origin: Offset,
72) {
73 let Some(el) = tree.get(element_id) else {
74 return;
75 };
76 if el
77 .layout_cache_key
78 .is_some_and(|key| key.layout_pass.is_measure())
79 {
80 return;
81 }
82 let Some(layout) = el.layout.as_ref() else {
83 return;
84 };
85
86 let bounds = layout.paint_bounds.translate(origin);
87 let mut notifications = Vec::new();
88 precommit_layout_subtree(tree, element_id, bounds, &mut notifications);
89 notify_layout_committed(tree, runtime, ctx, notifications);
90}
91
92struct LayoutCommitNotification<A> {
94 element_id: ElementId,
96 bounds: Rect,
98 kind: ElementKind<A>,
100 layout: crate::layout::LayoutResult,
102}
103
104fn precommit_layout_subtree<A: 'static>(
106 tree: &mut ElementTree<A>,
107 element_id: ElementId,
108 bounds: Rect,
109 notifications: &mut Vec<LayoutCommitNotification<A>>,
110) {
111 let Some(el) = tree.get(element_id) else {
112 return;
113 };
114 let Some(layout) = el.layout.as_ref() else {
115 return;
116 };
117
118 let bounds_changed = el.committed_bounds != Some(bounds);
119 if !el.commit_dirty && !bounds_changed {
120 return;
121 }
122 let hook_dependencies_stale = el.commit_dirty
123 && !el.layout_commit_reactive_dependencies.is_empty()
124 && !el.layout_commit_reactive_dependencies.is_current();
125 let should_commit_widget = el.layout_changed
126 || el.layout_callback_executed
127 || bounds_changed
128 || hook_dependencies_stale;
129 let kind = el.kind.clone();
130 let layout = layout.clone();
131 let children = el.children.clone();
132
133 if let Some(el) = tree.get_mut(element_id) {
134 el.committed_bounds = Some(bounds);
135 el.layout_changed = false;
136 el.layout_callback_executed = false;
137 el.commit_dirty = false;
138 }
139
140 if should_commit_widget {
141 notifications.push(LayoutCommitNotification {
142 element_id,
143 bounds,
144 kind,
145 layout: layout.clone(),
146 });
147 }
148
149 for (idx, child_id) in children.into_iter().enumerate() {
150 let Some(child_layout) = layout.children.get(idx) else {
151 continue;
152 };
153 let child_bounds = Rect::new(
154 bounds.x + child_layout.offset.x,
155 bounds.y + child_layout.offset.y,
156 child_layout.size.w,
157 child_layout.size.h,
158 );
159 precommit_layout_subtree(tree, child_id, child_bounds, notifications);
160 }
161}
162
163fn notify_layout_committed<A: 'static>(
165 tree: &mut ElementTree<A>,
166 runtime: Option<&RuntimeHandle<A>>,
167 ctx: &mut LayoutCtx<'_>,
168 notifications: Vec<LayoutCommitNotification<A>>,
169) {
170 for notification in notifications {
171 tree.record_layout_commit(notification.element_id);
172 if let ElementKind::Widget(widget) = notification.kind {
173 let attempt_token = tree
174 .get(notification.element_id)
175 .and_then(|element| element.committed_layout_attempt);
176 if let Some(runtime) = runtime {
177 let observation = ReactiveReadScope::new();
178 ctx.with_committed_layout_attempt(attempt_token, |ctx| {
179 widget.layout_committed(ctx, notification.bounds, ¬ification.layout);
180 });
181 let dependencies = observation.finish();
182 if dependencies.is_current() {
183 let Some(element) = tree.get_mut(notification.element_id) else {
184 continue;
185 };
186 element.layout_commit_reactive_dependencies = dependencies;
187 let mut combined = element.layout_reactive_dependencies.clone();
188 combined.merge(&element.layout_commit_reactive_dependencies);
189 let mount_generation = element.mount_generation();
190 let _ = runtime.replace_reactive_dependencies(
191 notification.element_id,
192 mount_generation,
193 ReactiveStage::Layout,
194 &combined,
195 );
196 } else {
197 runtime.invalidate(notification.element_id, Invalidation::Layout);
198 }
199 } else {
200 with_untracked_reads(|| {
201 ctx.with_committed_layout_attempt(attempt_token, |ctx| {
202 widget.layout_committed(ctx, notification.bounds, ¬ification.layout)
203 })
204 });
205 }
206 }
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use std::panic::{catch_unwind, AssertUnwindSafe};
213 use std::rc::Rc;
214
215 use ailloli_ui_core::{Constraints, Scale, Size};
216
217 use super::*;
218 use crate::component::Widget;
219 use crate::layout::{ChildLayout, LayoutChild, LayoutEngine, LayoutResult};
220 use crate::scene::PaintCtx;
221
222 struct PanickingCommitHook;
224
225 impl Widget<()> for PanickingCommitHook {
226 fn debug_name(&self) -> &'static str {
227 "PanickingCommitHook"
228 }
229
230 fn layout(
231 &self,
232 _engine: &mut LayoutEngine<'_, ()>,
233 _ctx: &mut LayoutCtx<'_>,
234 _children: &mut [LayoutChild],
235 _constraints: Constraints,
236 ) -> LayoutResult {
237 LayoutResult::empty()
238 }
239
240 fn layout_committed(
241 &self,
242 _ctx: &mut LayoutCtx<'_>,
243 _bounds: Rect,
244 _layout: &LayoutResult,
245 ) {
246 panic!("intentional parent layout_committed panic");
247 }
248
249 fn paint(&self, _ctx: &mut PaintCtx<'_>, _bounds: Rect, _layout: &LayoutResult) {}
250 }
251
252 #[test]
253 fn parent_hook_panic_keeps_precommitted_child_bounds() {
254 let mut tree = ElementTree::<()>::new();
255 let root = tree.create_element(
256 ElementKind::Widget(Rc::new(PanickingCommitHook)),
257 None,
258 None,
259 );
260 let child = tree.create_element(ElementKind::Empty, None, Some(root));
261 tree.set_children(root, vec![child]);
262
263 let child_size = Size::new(20.0, 10.0);
264 tree.set_layout(
265 child,
266 LayoutResult {
267 size: child_size,
268 paint_bounds: Rect::new(50.0, 60.0, child_size.w, child_size.h),
269 visual_bounds: Rect::new(50.0, 60.0, child_size.w, child_size.h),
270 ..LayoutResult::empty()
271 },
272 );
273 tree.set_layout(
274 root,
275 LayoutResult {
276 size: Size::new(40.0, 30.0),
277 children: vec![ChildLayout {
278 offset: Offset::new(7.0, 9.0),
279 size: child_size,
280 paint_bounds: Rect::new(7.0, 9.0, child_size.w, child_size.h),
281 visual_bounds: Rect::new(7.0, 9.0, child_size.w, child_size.h),
282 }],
283 paint_bounds: Rect::new(1.0, 2.0, 40.0, 30.0),
284 visual_bounds: Rect::new(1.0, 2.0, 40.0, 30.0),
285 ..LayoutResult::empty()
286 },
287 );
288
289 let mut ctx = LayoutCtx::new(Scale::new(1.0));
290 let panic = catch_unwind(AssertUnwindSafe(|| {
291 commit_layout_element(&mut tree, &mut ctx, root, Offset::new(3.0, 4.0));
292 }));
293
294 assert!(panic.is_err());
295 assert_eq!(
296 tree.get(root).and_then(|element| element.committed_bounds),
297 Some(Rect::new(4.0, 6.0, 40.0, 30.0))
298 );
299 assert_eq!(
300 tree.get(child).and_then(|element| element.committed_bounds),
301 Some(Rect::new(11.0, 15.0, 20.0, 10.0)),
302 "a parent hook panic must not leave child bounds behind published layout caches"
303 );
304 }
305}