1use super::*;
2
3const DEV_OVERLAY_PADDING: f32 = 8.0;
4const DEV_OVERLAY_FONT_SIZE: f32 = 14.0;
5const DEV_OVERLAY_CHAR_WIDTH: f32 = 7.0;
6
7#[derive(Copy, Clone)]
8enum DispatchInvalidationKind {
9 Pointer,
10 Focus,
11}
12
13impl<R> AppShell<R>
14where
15 R: Renderer,
16 R::Error: Debug,
17{
18 pub fn set_semantics_enabled(&mut self, enabled: bool) {
19 if self.semantics_enabled == enabled {
20 return;
21 }
22 self.semantics_enabled = enabled;
23 if enabled {
24 self.request_forced_layout_pass();
25 self.mark_dirty();
26 } else {
27 self.semantics_tree = None;
28 }
29 }
30
31 pub(crate) fn process_frame(&mut self) {
32 let app_context = Rc::clone(&self.app_context);
33 app_context.enter(|| self.process_frame_in_context());
34 }
35
36 pub(crate) fn process_frame_in_context(&mut self) {
37 let frame_start = Instant::now();
38
39 self.run_layout_phase();
40
41 #[cfg(debug_assertions)]
42 let _after_layout = Instant::now();
43
44 self.run_dispatch_queues();
45
46 #[cfg(debug_assertions)]
47 let _after_dispatch = Instant::now();
48
49 self.run_render_phase();
50 self.fps_monitor
51 .record_frame_work(frame_start, Instant::now());
52 }
53
54 pub(crate) fn run_layout_phase(&mut self) {
55 let app_context = Rc::clone(&self.app_context);
56 app_context.enter(|| self.run_layout_phase_in_context());
57 }
58
59 fn run_layout_phase_in_context(&mut self) {
60 let has_scoped_repasses = cranpose_ui::has_pending_layout_repasses();
61
62 let invalidation_requested = take_layout_invalidation();
66
67 if invalidation_requested && !has_scoped_repasses {
68 cranpose_ui::layout::invalidate_all_layout_caches();
69
70 if let Some(root) = self.composition.root() {
73 let mut applier = self.composition.applier_mut();
74 match applier.with_node::<LayoutNode, _>(root, |node| {
75 node.mark_needs_measure();
76 node.mark_needs_layout();
77 }) {
78 Ok(()) | Err(NodeError::Missing { .. }) => {}
79 Err(NodeError::TypeMismatch { .. }) => {
80 let _ = applier.with_node::<SubcomposeLayoutNode, _>(root, |node| {
81 node.mark_needs_measure();
82 node.mark_needs_layout_flag();
83 });
84 }
85 Err(_) => {}
86 }
87 }
88 self.request_forced_layout_pass();
89 } else if invalidation_requested || has_scoped_repasses {
90 self.request_layout_pass();
91 }
92
93 if !self.layout_requested {
94 return;
95 }
96
97 let viewport_size = Size {
98 width: self.viewport.0,
99 height: self.viewport.1,
100 };
101 if let Some(root) = self.composition.root() {
102 let handle = self.composition.runtime_handle();
103 let mut applier = self.composition.applier_mut();
104 applier.set_runtime_handle(handle);
105
106 let tree_needs_layout_check = cranpose_ui::tree_needs_layout(&mut *applier, root)
107 .unwrap_or_else(|err| {
108 log::warn!(
109 "Cannot check layout dirty status for root #{}: {}",
110 root,
111 err
112 );
113 true });
115
116 let needs_layout =
117 self.force_layout_pass || has_scoped_repasses || tree_needs_layout_check;
118
119 if !needs_layout {
120 log::trace!("Skipping layout: tree is clean");
121 self.layout_requested = false;
122 self.force_layout_pass = false;
123 applier.clear_runtime_handle();
124 return;
125 }
126
127 self.layout_requested = false;
128 self.force_layout_pass = false;
129
130 match cranpose_ui::measure_layout_with_options(
132 &mut applier,
133 root,
134 viewport_size,
135 MeasureLayoutOptions {
136 collect_semantics: false,
137 build_layout_tree: false,
138 },
139 ) {
140 Ok(_measurements) => {
141 self.layout_tree = None;
142 if self.semantics_enabled {
143 self.semantics_tree = None;
144 }
145 self.scene_dirty = true;
146 }
147 Err(err) => {
148 log::error!("failed to compute layout: {err}");
149 self.layout_tree = None;
150 self.semantics_tree = None;
151 self.scene_dirty = true;
152 }
153 }
154 applier.clear_runtime_handle();
155 } else {
156 self.layout_tree = None;
157 self.semantics_tree = None;
158 self.scene_dirty = true;
159 self.layout_requested = false;
160 self.force_layout_pass = false;
161 }
162 }
163
164 fn run_dispatch_queues(&mut self) {
165 if has_pending_pointer_repasses() {
169 let mut applier = self.composition.applier_mut();
170 process_pointer_repasses(|node_id| {
171 match clear_dispatch_invalidation(
172 &mut applier,
173 node_id,
174 DispatchInvalidationKind::Pointer,
175 ) {
176 Ok(true) => {
177 log::trace!("Cleared pointer repass flag for node #{}", node_id);
178 }
179 Ok(false) => {}
180 Err(err) => {
181 log::debug!(
182 "Could not process pointer repass for node #{}: {}",
183 node_id,
184 err
185 );
186 }
187 }
188 });
189 }
190
191 if has_pending_focus_invalidations() {
195 let mut applier = self.composition.applier_mut();
196 process_focus_invalidations(|node_id| {
197 match clear_dispatch_invalidation(
198 &mut applier,
199 node_id,
200 DispatchInvalidationKind::Focus,
201 ) {
202 Ok(true) => {
203 log::trace!("Cleared focus sync flag for node #{}", node_id);
204 }
205 Ok(false) => {}
206 Err(err) => {
207 log::debug!(
208 "Could not process focus invalidation for node #{}: {}",
209 node_id,
210 err
211 );
212 }
213 }
214 });
215 }
216 }
217
218 fn refresh_draw_repasses(&mut self) -> Vec<NodeId> {
219 let dirty_nodes = take_draw_repass_nodes();
220 if dirty_nodes.is_empty() {
221 return dirty_nodes;
222 }
223
224 let Some(layout_tree) = self.layout_tree.as_mut() else {
225 return dirty_nodes;
226 };
227
228 let dirty_set: HashSet<NodeId> = dirty_nodes.into_iter().collect();
229 let mut applier = self.composition.applier_mut();
230 let refresh_scope = build_draw_refresh_scope(&mut applier, &dirty_set);
231 refresh_layout_box_data(
232 &mut applier,
233 layout_tree.root_mut(),
234 &refresh_scope,
235 &dirty_set,
236 );
237 dirty_set.into_iter().collect()
238 }
239
240 pub(crate) fn run_render_phase(&mut self) {
241 let app_context = Rc::clone(&self.app_context);
242 app_context.enter(|| self.run_render_phase_in_context());
243 }
244
245 fn run_render_phase_in_context(&mut self) {
246 let render_dirty = take_render_invalidation();
247 let pointer_dirty = take_pointer_invalidation();
248 take_focus_invalidation();
249 let draw_repass_pending = cranpose_ui::has_pending_draw_repasses();
250 let cursor_blink_dirty = cranpose_ui::tick_cursor_blink();
252
253 let render_only_dirty = (render_dirty && !draw_repass_pending) || cursor_blink_dirty;
254 let scene_dirty = self.scene_dirty;
257 let needs_scene_rebuild =
258 scene_dirty || draw_repass_pending || render_only_dirty || pointer_dirty;
259
260 if !needs_scene_rebuild {
261 return;
262 }
263 self.scene_dirty = false;
264 let draw_dirty_nodes = self.refresh_draw_repasses();
265 let viewport_size = Size {
266 width: self.viewport.0,
267 height: self.viewport.1,
268 };
269
270 if let Some(root) = self.composition.root() {
272 let mut applier = self.composition.applier_mut();
273 let rebuild_result = if !draw_dirty_nodes.is_empty()
274 && !render_only_dirty
275 && !pointer_dirty
276 && !scene_dirty
277 {
278 self.renderer.update_scene_from_applier(
279 &mut applier,
280 root,
281 viewport_size,
282 &draw_dirty_nodes,
283 )
284 } else {
285 self.renderer
286 .rebuild_scene_from_applier(&mut applier, root, viewport_size)
287 };
288 if let Err(err) = rebuild_result {
289 log::error!("renderer rebuild failed: {err:?}");
291 self.renderer.scene_mut().clear();
292 }
293 } else {
294 self.renderer.scene_mut().clear();
295 }
296
297 if self.dev_options.fps_counter {
299 let text = self.build_dev_overlay_text(viewport_size);
300 self.renderer.draw_dev_overlay(&text, viewport_size);
301 }
302 }
303
304 fn build_dev_overlay_text(&mut self, viewport_size: Size) -> String {
305 self.dev_overlay_controls.clear();
306
307 let stats = self.fps_monitor.stats();
308 let mut text = format!(
309 "{:.0} FPS | avg {:.1}ms | p95 {:.1}ms | work {:.1}ms | max {:.1}ms | {} recomp/s",
310 stats.fps,
311 stats.avg_ms,
312 stats.p95_ms,
313 stats.work_p95_ms,
314 stats.max_ms,
315 stats.recomps_per_second
316 );
317
318 if !self.dev_options.frame_pacing_controls {
319 return text;
320 }
321
322 text.push_str(" | ");
323 let mut controls = Vec::with_capacity(FramePacingMode::ALL.len());
324 for (index, mode) in FramePacingMode::ALL.into_iter().enumerate() {
325 if index > 0 {
326 text.push(' ');
327 }
328 let start = text.len();
329 if mode == self.dev_options.frame_pacing_mode {
330 text.push('[');
331 text.push_str(mode.label());
332 text.push(']');
333 } else {
334 text.push_str(mode.label());
335 }
336 controls.push((start, text.len(), mode));
337 }
338
339 let overlay_width = text.len() as f32 * DEV_OVERLAY_CHAR_WIDTH;
340 let overlay_x = (viewport_size.width - overlay_width - DEV_OVERLAY_PADDING * 2.0)
341 .max(DEV_OVERLAY_PADDING);
342 let overlay_y = DEV_OVERLAY_PADDING;
343 let text_x = overlay_x + DEV_OVERLAY_PADDING / 2.0;
344 let text_y = overlay_y + DEV_OVERLAY_PADDING / 4.0;
345 let text_height = DEV_OVERLAY_FONT_SIZE * 1.4;
346
347 self.dev_overlay_controls = controls
348 .into_iter()
349 .map(|(start, end, mode)| DevOverlayControl {
350 bounds: Rect {
351 x: text_x + start as f32 * DEV_OVERLAY_CHAR_WIDTH - 3.0,
352 y: text_y - 3.0,
353 width: (end - start) as f32 * DEV_OVERLAY_CHAR_WIDTH + 6.0,
354 height: text_height + 6.0,
355 },
356 mode,
357 })
358 .collect();
359
360 text
361 }
362}
363
364fn clear_dispatch_invalidation(
365 applier: &mut MemoryApplier,
366 node_id: NodeId,
367 invalidation: DispatchInvalidationKind,
368) -> Result<bool, NodeError> {
369 match invalidation {
370 DispatchInvalidationKind::Pointer => {
371 match applier.with_node::<LayoutNode, _>(node_id, |node| {
372 let needs_pointer_pass = node.needs_pointer_pass();
373 if needs_pointer_pass {
374 node.clear_needs_pointer_pass();
375 }
376 needs_pointer_pass
377 }) {
378 Ok(cleared) => Ok(cleared),
379 Err(NodeError::TypeMismatch { .. }) => applier
380 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
381 let needs_pointer_pass = node.needs_pointer_pass();
382 if needs_pointer_pass {
383 node.clear_needs_pointer_pass();
384 }
385 needs_pointer_pass
386 }),
387 Err(err) => Err(err),
388 }
389 }
390 DispatchInvalidationKind::Focus => {
391 match applier.with_node::<LayoutNode, _>(node_id, |node| {
392 let needs_focus_sync = node.needs_focus_sync();
393 if needs_focus_sync {
394 node.clear_needs_focus_sync();
395 }
396 needs_focus_sync
397 }) {
398 Ok(cleared) => Ok(cleared),
399 Err(NodeError::TypeMismatch { .. }) => applier
400 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
401 let needs_focus_sync = node.needs_focus_sync();
402 if needs_focus_sync {
403 node.clear_needs_focus_sync();
404 }
405 needs_focus_sync
406 }),
407 Err(err) => Err(err),
408 }
409 }
410 }
411}
412
413pub(crate) fn build_draw_refresh_scope(
414 applier: &mut MemoryApplier,
415 dirty_nodes: &HashSet<NodeId>,
416) -> HashSet<NodeId> {
417 let mut refresh_scope = HashSet::with_capacity(dirty_nodes.len());
418 for &dirty_node in dirty_nodes {
419 let mut current = Some(dirty_node);
420 while let Some(node_id) = current {
421 if !refresh_scope.insert(node_id) {
422 break;
423 }
424 current = applier.get_mut(node_id).ok().and_then(|node| node.parent());
425 }
426 }
427 refresh_scope
428}
429
430fn refresh_layout_box_data(
431 applier: &mut MemoryApplier,
432 layout: &mut cranpose_ui::layout::LayoutBox,
433 refresh_scope: &HashSet<NodeId>,
434 dirty_nodes: &HashSet<NodeId>,
435) {
436 if !refresh_scope.contains(&layout.node_id) {
437 return;
438 }
439
440 if dirty_nodes.contains(&layout.node_id) {
441 if let Ok((modifier, resolved_modifiers, slices)) =
442 applier.with_node::<LayoutNode, _>(layout.node_id, |node| {
443 node.clear_needs_redraw();
444 (
445 node.modifier.clone(),
446 node.resolved_modifiers(),
447 node.modifier_slices_snapshot(),
448 )
449 })
450 {
451 layout.node_data.modifier = modifier;
452 layout.node_data.resolved_modifiers = resolved_modifiers;
453 layout.node_data.modifier_slices = slices;
454 } else if let Ok((modifier, resolved_modifiers)) = applier
455 .with_node::<SubcomposeLayoutNode, _>(layout.node_id, |node| {
456 node.clear_needs_redraw();
457 (node.modifier(), node.resolved_modifiers())
458 })
459 {
460 layout.node_data.modifier = modifier.clone();
461 layout.node_data.resolved_modifiers = resolved_modifiers;
462 layout.node_data.modifier_slices =
463 std::rc::Rc::new(cranpose_ui::collect_slices_from_modifier(&modifier));
464 }
465 }
466
467 for child in &mut layout.children {
468 refresh_layout_box_data(applier, child, refresh_scope, dirty_nodes);
469 }
470}