1use super::*;
2
3#[derive(Copy, Clone)]
4enum DispatchInvalidationKind {
5 Pointer,
6 Focus,
7}
8
9impl<R> AppShell<R>
10where
11 R: Renderer,
12 R::Error: Debug,
13{
14 pub fn set_semantics_enabled(&mut self, enabled: bool) {
15 if self.semantics_enabled == enabled {
16 return;
17 }
18 self.semantics_enabled = enabled;
19 if enabled {
20 self.request_forced_layout_pass();
21 self.mark_dirty();
22 } else {
23 self.semantics_tree = None;
24 }
25 }
26
27 pub(crate) fn process_frame(&mut self) {
28 fps_monitor::record_frame();
30
31 #[cfg(debug_assertions)]
32 let _frame_start = Instant::now();
33
34 self.run_layout_phase();
35
36 #[cfg(debug_assertions)]
37 let _after_layout = Instant::now();
38
39 self.run_dispatch_queues();
40
41 #[cfg(debug_assertions)]
42 let _after_dispatch = Instant::now();
43
44 self.run_render_phase();
45 }
46
47 pub(crate) fn run_layout_phase(&mut self) {
48 let has_scoped_repasses = cranpose_ui::has_pending_layout_repasses();
49
50 let invalidation_requested = take_layout_invalidation();
68
69 if invalidation_requested && !has_scoped_repasses {
70 cranpose_ui::layout::invalidate_all_layout_caches();
73
74 if let Some(root) = self.composition.root() {
77 let mut applier = self.composition.applier_mut();
78 match applier.with_node::<LayoutNode, _>(root, |node| {
79 node.mark_needs_measure();
80 node.mark_needs_layout();
81 }) {
82 Ok(()) | Err(NodeError::Missing { .. }) => {}
83 Err(NodeError::TypeMismatch { .. }) => {
84 let _ = applier.with_node::<SubcomposeLayoutNode, _>(root, |node| {
85 node.mark_needs_measure();
86 node.mark_needs_layout_flag();
87 });
88 }
89 Err(_) => {}
90 }
91 }
92 self.request_forced_layout_pass();
93 } else if invalidation_requested || has_scoped_repasses {
94 self.request_layout_pass();
95 }
96
97 if !self.layout_requested {
98 return;
99 }
100
101 let viewport_size = Size {
102 width: self.viewport.0,
103 height: self.viewport.1,
104 };
105 if let Some(root) = self.composition.root() {
106 let handle = self.composition.runtime_handle();
107 let mut applier = self.composition.applier_mut();
108 applier.set_runtime_handle(handle);
109
110 let tree_needs_layout_check = cranpose_ui::tree_needs_layout(&mut *applier, root)
111 .unwrap_or_else(|err| {
112 log::warn!(
113 "Cannot check layout dirty status for root #{}: {}",
114 root,
115 err
116 );
117 true });
119
120 let tree_needs_semantics_check = self.semantics_enabled
121 && cranpose_ui::tree_needs_semantics(&mut *applier, root).unwrap_or_else(|err| {
122 log::warn!(
123 "Cannot check semantics dirty status for root #{}: {}",
124 root,
125 err
126 );
127 true
128 });
129 let needs_layout = self.force_layout_pass
130 || has_scoped_repasses
131 || tree_needs_layout_check
132 || tree_needs_semantics_check;
133
134 if !needs_layout {
135 log::trace!("Skipping layout: tree is clean");
136 self.layout_requested = false;
137 self.force_layout_pass = false;
138 applier.clear_runtime_handle();
139 return;
140 }
141
142 self.layout_requested = false;
143 self.force_layout_pass = false;
144
145 match cranpose_ui::measure_layout_with_options(
147 &mut applier,
148 root,
149 viewport_size,
150 MeasureLayoutOptions {
151 collect_semantics: self.semantics_enabled,
152 build_layout_tree: true,
153 },
154 ) {
155 Ok(measurements) => {
156 let semantics_tree = measurements.semantics_tree().cloned();
157 self.layout_tree = Some(measurements.into_layout_tree());
158 self.semantics_tree = semantics_tree;
159 self.scene_dirty = true;
160 }
161 Err(err) => {
162 log::error!("failed to compute layout: {err}");
163 self.layout_tree = None;
164 self.semantics_tree = None;
165 self.scene_dirty = true;
166 }
167 }
168 applier.clear_runtime_handle();
169 } else {
170 self.layout_tree = None;
171 self.semantics_tree = None;
172 self.scene_dirty = true;
173 self.layout_requested = false;
174 self.force_layout_pass = false;
175 }
176 }
177
178 fn run_dispatch_queues(&mut self) {
179 if has_pending_pointer_repasses() {
183 let mut applier = self.composition.applier_mut();
184 process_pointer_repasses(|node_id| {
185 match clear_dispatch_invalidation(
186 &mut applier,
187 node_id,
188 DispatchInvalidationKind::Pointer,
189 ) {
190 Ok(true) => {
191 log::trace!("Cleared pointer repass flag for node #{}", node_id);
192 }
193 Ok(false) => {}
194 Err(err) => {
195 log::debug!(
196 "Could not process pointer repass for node #{}: {}",
197 node_id,
198 err
199 );
200 }
201 }
202 });
203 }
204
205 if has_pending_focus_invalidations() {
209 let mut applier = self.composition.applier_mut();
210 process_focus_invalidations(|node_id| {
211 match clear_dispatch_invalidation(
212 &mut applier,
213 node_id,
214 DispatchInvalidationKind::Focus,
215 ) {
216 Ok(true) => {
217 log::trace!("Cleared focus sync flag for node #{}", node_id);
218 }
219 Ok(false) => {}
220 Err(err) => {
221 log::debug!(
222 "Could not process focus invalidation for node #{}: {}",
223 node_id,
224 err
225 );
226 }
227 }
228 });
229 }
230 }
231
232 fn refresh_draw_repasses(&mut self) {
233 let dirty_nodes = take_draw_repass_nodes();
234 if dirty_nodes.is_empty() {
235 return;
236 }
237
238 let Some(layout_tree) = self.layout_tree.as_mut() else {
239 return;
240 };
241
242 let dirty_set: HashSet<NodeId> = dirty_nodes.into_iter().collect();
243 let mut applier = self.composition.applier_mut();
244 let refresh_scope = build_draw_refresh_scope(&mut applier, &dirty_set);
245 refresh_layout_box_data(
246 &mut applier,
247 layout_tree.root_mut(),
248 &refresh_scope,
249 &dirty_set,
250 );
251 }
252
253 pub(crate) fn run_render_phase(&mut self) {
254 let render_dirty = take_render_invalidation();
255 let pointer_dirty = take_pointer_invalidation();
256 take_focus_invalidation();
257 let draw_repass_pending = cranpose_ui::has_pending_draw_repasses();
258 let cursor_blink_dirty = cranpose_ui::tick_cursor_blink();
260
261 let render_only_dirty = render_dirty || cursor_blink_dirty;
262 let needs_scene_rebuild =
265 self.scene_dirty || draw_repass_pending || render_only_dirty || pointer_dirty;
266
267 if !needs_scene_rebuild {
268 return;
269 }
270 self.scene_dirty = false;
271 self.refresh_draw_repasses();
272 let viewport_size = Size {
273 width: self.viewport.0,
274 height: self.viewport.1,
275 };
276
277 if let Some(root) = self.composition.root() {
279 let mut applier = self.composition.applier_mut();
280 if let Err(err) =
281 self.renderer
282 .rebuild_scene_from_applier(&mut applier, root, viewport_size)
283 {
284 log::error!("renderer rebuild failed: {err:?}");
286 self.renderer.scene_mut().clear();
287 }
288 } else {
289 self.renderer.scene_mut().clear();
290 }
291
292 if self.dev_options.fps_counter {
294 let stats = fps_monitor::fps_stats();
295 let text = format!(
296 "{:.0} FPS | {:.1}ms | {} recomp/s",
297 stats.fps, stats.avg_ms, stats.recomps_per_second
298 );
299 self.renderer.draw_dev_overlay(&text, viewport_size);
300 }
301 }
302}
303
304fn clear_dispatch_invalidation(
305 applier: &mut MemoryApplier,
306 node_id: NodeId,
307 invalidation: DispatchInvalidationKind,
308) -> Result<bool, NodeError> {
309 match invalidation {
310 DispatchInvalidationKind::Pointer => {
311 match applier.with_node::<LayoutNode, _>(node_id, |node| {
312 let needs_pointer_pass = node.needs_pointer_pass();
313 if needs_pointer_pass {
314 node.clear_needs_pointer_pass();
315 }
316 needs_pointer_pass
317 }) {
318 Ok(cleared) => Ok(cleared),
319 Err(NodeError::TypeMismatch { .. }) => applier
320 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
321 let needs_pointer_pass = node.needs_pointer_pass();
322 if needs_pointer_pass {
323 node.clear_needs_pointer_pass();
324 }
325 needs_pointer_pass
326 }),
327 Err(err) => Err(err),
328 }
329 }
330 DispatchInvalidationKind::Focus => {
331 match applier.with_node::<LayoutNode, _>(node_id, |node| {
332 let needs_focus_sync = node.needs_focus_sync();
333 if needs_focus_sync {
334 node.clear_needs_focus_sync();
335 }
336 needs_focus_sync
337 }) {
338 Ok(cleared) => Ok(cleared),
339 Err(NodeError::TypeMismatch { .. }) => applier
340 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
341 let needs_focus_sync = node.needs_focus_sync();
342 if needs_focus_sync {
343 node.clear_needs_focus_sync();
344 }
345 needs_focus_sync
346 }),
347 Err(err) => Err(err),
348 }
349 }
350 }
351}
352
353pub(crate) fn build_draw_refresh_scope(
354 applier: &mut MemoryApplier,
355 dirty_nodes: &HashSet<NodeId>,
356) -> HashSet<NodeId> {
357 let mut refresh_scope = HashSet::with_capacity(dirty_nodes.len());
358 for &dirty_node in dirty_nodes {
359 let mut current = Some(dirty_node);
360 while let Some(node_id) = current {
361 if !refresh_scope.insert(node_id) {
362 break;
363 }
364 current = applier.get_mut(node_id).ok().and_then(|node| node.parent());
365 }
366 }
367 refresh_scope
368}
369
370fn refresh_layout_box_data(
371 applier: &mut MemoryApplier,
372 layout: &mut cranpose_ui::layout::LayoutBox,
373 refresh_scope: &HashSet<NodeId>,
374 dirty_nodes: &HashSet<NodeId>,
375) {
376 if !refresh_scope.contains(&layout.node_id) {
377 return;
378 }
379
380 if dirty_nodes.contains(&layout.node_id) {
381 if let Ok((modifier, resolved_modifiers, slices)) =
382 applier.with_node::<LayoutNode, _>(layout.node_id, |node| {
383 node.clear_needs_redraw();
384 (
385 node.modifier.clone(),
386 node.resolved_modifiers(),
387 node.modifier_slices_snapshot(),
388 )
389 })
390 {
391 layout.node_data.modifier = modifier;
392 layout.node_data.resolved_modifiers = resolved_modifiers;
393 layout.node_data.modifier_slices = slices;
394 } else if let Ok((modifier, resolved_modifiers)) = applier
395 .with_node::<SubcomposeLayoutNode, _>(layout.node_id, |node| {
396 node.clear_needs_redraw();
397 (node.modifier(), node.resolved_modifiers())
398 })
399 {
400 layout.node_data.modifier = modifier.clone();
401 layout.node_data.resolved_modifiers = resolved_modifiers;
402 layout.node_data.modifier_slices =
403 std::rc::Rc::new(cranpose_ui::collect_slices_from_modifier(&modifier));
404 }
405 }
406
407 for child in &mut layout.children {
408 refresh_layout_box_data(applier, child, refresh_scope, dirty_nodes);
409 }
410}