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