Skip to main content

cranpose_app_shell/
shell_frame.rs

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;
6const DEV_OVERLAY_REFRESH_INTERVAL: std::time::Duration = std::time::Duration::from_millis(250);
7const DEFAULT_FRAME_STAGE_TELEMETRY_THRESHOLD_MS: f64 = 4.0;
8
9#[derive(Copy, Clone)]
10enum DispatchInvalidationKind {
11    Pointer,
12    Focus,
13}
14
15fn frame_stage_telemetry_threshold_ms() -> Option<f64> {
16    static THRESHOLD_MS: std::sync::OnceLock<Option<f64>> = std::sync::OnceLock::new();
17    *THRESHOLD_MS.get_or_init(|| {
18        let explicit = std::env::var("CRANPOSE_FRAME_STAGE_TELEMETRY_MS")
19            .ok()
20            .and_then(|value| value.parse::<f64>().ok())
21            .filter(|value| value.is_finite() && *value >= 0.0);
22        explicit.or_else(|| {
23            std::env::var_os("CRANPOSE_FRAME_STAGE_TELEMETRY")
24                .is_some()
25                .then_some(DEFAULT_FRAME_STAGE_TELEMETRY_THRESHOLD_MS)
26        })
27    })
28}
29
30fn log_frame_stage_telemetry(
31    frame_start: Instant,
32    after_initial_layout: Instant,
33    after_layout: Instant,
34    after_dispatch: Instant,
35    after_render: Instant,
36) {
37    let Some(threshold_ms) = frame_stage_telemetry_threshold_ms() else {
38        return;
39    };
40
41    let total_ms = after_render.duration_since(frame_start).as_secs_f64() * 1000.0;
42    if total_ms < threshold_ms {
43        return;
44    }
45
46    let layout_ms = after_layout.duration_since(frame_start).as_secs_f64() * 1000.0;
47    let initial_layout_ms = after_initial_layout
48        .duration_since(frame_start)
49        .as_secs_f64()
50        * 1000.0;
51    let post_layout_ms = after_layout
52        .duration_since(after_initial_layout)
53        .as_secs_f64()
54        * 1000.0;
55    let dispatch_ms = after_dispatch.duration_since(after_layout).as_secs_f64() * 1000.0;
56    let scene_ms = after_render.duration_since(after_dispatch).as_secs_f64() * 1000.0;
57    log::warn!(
58        "[frame-stage-telemetry] total_ms={total_ms:.2} layout_ms={layout_ms:.2} initial_layout_ms={initial_layout_ms:.2} post_layout_ms={post_layout_ms:.2} dispatch_ms={dispatch_ms:.2} scene_ms={scene_ms:.2}",
59    );
60}
61
62fn render_phase_dirty_diagnostics_enabled() -> bool {
63    static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
64    *ENABLED.get_or_init(|| std::env::var_os("CRANPOSE_RENDER_PHASE_DIRTY_DIAG").is_some())
65}
66
67struct RenderPhaseDirtyDiagnostics<'a> {
68    render_dirty: bool,
69    pointer_dirty: bool,
70    scene_dirty: bool,
71    draw_repass_pending: bool,
72    draw_dirty_nodes: usize,
73    layout_dirty_nodes: usize,
74    partial_dirty_nodes: usize,
75    dirty_node_ids: Option<String>,
76    render_only_dirty: bool,
77    recomposed_this_frame: bool,
78    path: &'a str,
79}
80
81fn log_render_phase_dirty_diagnostics(diagnostics: RenderPhaseDirtyDiagnostics<'_>) {
82    if !render_phase_dirty_diagnostics_enabled() {
83        return;
84    }
85
86    let RenderPhaseDirtyDiagnostics {
87        render_dirty,
88        pointer_dirty,
89        scene_dirty,
90        draw_repass_pending,
91        draw_dirty_nodes,
92        layout_dirty_nodes,
93        partial_dirty_nodes,
94        dirty_node_ids,
95        render_only_dirty,
96        recomposed_this_frame,
97        path,
98    } = diagnostics;
99    if let Some(dirty_node_ids) = dirty_node_ids {
100        log::warn!(
101            "[render-phase-dirty] path={path} render_dirty={render_dirty} pointer_dirty={pointer_dirty} scene_dirty={scene_dirty} draw_repass_pending={draw_repass_pending} draw_dirty_nodes={draw_dirty_nodes} layout_dirty_nodes={layout_dirty_nodes} partial_dirty_nodes={partial_dirty_nodes} render_only_dirty={render_only_dirty} recomposed_this_frame={recomposed_this_frame} ids={dirty_node_ids}",
102        );
103    } else {
104        log::warn!(
105            "[render-phase-dirty] path={path} render_dirty={render_dirty} pointer_dirty={pointer_dirty} scene_dirty={scene_dirty} draw_repass_pending={draw_repass_pending} draw_dirty_nodes={draw_dirty_nodes} layout_dirty_nodes={layout_dirty_nodes} partial_dirty_nodes={partial_dirty_nodes} render_only_dirty={render_only_dirty} recomposed_this_frame={recomposed_this_frame}",
106        );
107    }
108}
109
110impl<R> AppShell<R>
111where
112    R: Renderer,
113    R::Error: Debug,
114{
115    pub fn set_semantics_enabled(&mut self, enabled: bool) {
116        if self.semantics_enabled == enabled {
117            return;
118        }
119        self.semantics_enabled = enabled;
120        if enabled {
121            self.request_forced_layout_pass();
122            self.mark_dirty();
123        } else {
124            self.semantics_tree = None;
125        }
126    }
127
128    pub(crate) fn process_frame(&mut self) -> FrameUpdateResult {
129        let app_context = Rc::clone(&self.app_context);
130        app_context.enter(|| self.process_frame_in_context(false))
131    }
132
133    pub(crate) fn process_frame_in_context(
134        &mut self,
135        recomposed_before_frame: bool,
136    ) -> FrameUpdateResult {
137        let frame_start = Instant::now();
138
139        self.run_layout_phase();
140        let after_initial_layout = Instant::now();
141
142        let recomposed_this_frame = recomposed_before_frame || self.run_post_layout_recomposition();
143
144        let after_layout = Instant::now();
145
146        self.run_dispatch_queues();
147
148        let after_dispatch = Instant::now();
149
150        clear_transient_scroll_motion_contexts();
151
152        let result = self.run_render_phase_with_recomposition_state(recomposed_this_frame);
153        let after_render = Instant::now();
154        log_frame_stage_telemetry(
155            frame_start,
156            after_initial_layout,
157            after_layout,
158            after_dispatch,
159            after_render,
160        );
161        result
162    }
163
164    pub(crate) fn run_layout_phase(&mut self) {
165        let app_context = Rc::clone(&self.app_context);
166        app_context.enter(|| self.run_layout_phase_in_context());
167    }
168
169    fn run_layout_phase_in_context(&mut self) {
170        // Both scoped layout repasses (re-placement) and scoped measure repasses
171        // (re-sizing, e.g. a collapsing swipe-dismissed row) drive a layout pass.
172        let has_scoped_repasses = cranpose_ui::has_pending_layout_repasses()
173            || cranpose_ui::has_pending_measure_repasses();
174        let scoped_layout_nodes = if cranpose_ui::has_pending_layout_repasses() {
175            cranpose_ui::pending_layout_repass_nodes_snapshot()
176        } else {
177            Vec::new()
178        };
179
180        // Global layout invalidation is reserved for app-wide inputs such as
181        // viewport, density, font-scale, or debug layout changes. Normal node
182        // updates should arrive through scoped repasses.
183        let invalidation_requested = take_layout_invalidation();
184        let global_layout_invalidation = invalidation_requested && !has_scoped_repasses;
185        let force_layout_pass = self.force_layout_pass;
186
187        if invalidation_requested && !has_scoped_repasses {
188            cranpose_ui::layout::invalidate_all_layout_caches();
189
190            // Mark root as needing layout AND measure so tree_needs_layout() returns true
191            // and intrinsic sizes are recalculated (e.g., text field resizing on content change)
192            if let Some(root) = self.composition.root() {
193                let mut applier = self.composition.applier_mut();
194                match applier.with_node::<LayoutNode, _>(root, |node| {
195                    node.mark_needs_measure();
196                    node.mark_needs_layout();
197                }) {
198                    Ok(()) | Err(NodeError::Missing { .. }) => {}
199                    Err(NodeError::TypeMismatch { .. }) => {
200                        let _ = applier.with_node::<SubcomposeLayoutNode, _>(root, |node| {
201                            node.mark_needs_measure();
202                            node.mark_needs_layout_flag();
203                        });
204                    }
205                    Err(_) => {}
206                }
207            }
208            self.request_forced_layout_pass();
209        } else if invalidation_requested || has_scoped_repasses {
210            self.request_layout_pass();
211        }
212
213        if !self.layout_requested {
214            return;
215        }
216
217        let viewport_size = Size {
218            width: self.viewport.0,
219            height: self.viewport.1,
220        };
221        if let Some(root) = self.composition.root() {
222            let handle = self.composition.runtime_handle();
223            let mut applier = self.composition.applier_mut();
224            applier.set_runtime_handle(handle);
225
226            let tree_needs_layout_check = cranpose_ui::tree_needs_layout(&mut *applier, root)
227                .unwrap_or_else(|err| {
228                    log::warn!(
229                        "Cannot check layout dirty status for root #{}: {}",
230                        root,
231                        err
232                    );
233                    true // Assume dirty on error
234                });
235
236            let needs_layout =
237                self.force_layout_pass || has_scoped_repasses || tree_needs_layout_check;
238
239            if !needs_layout {
240                log::trace!("Skipping layout: tree is clean");
241                self.layout_requested = false;
242                self.force_layout_pass = false;
243                applier.clear_runtime_handle();
244                return;
245            }
246
247            self.layout_requested = false;
248            self.force_layout_pass = false;
249
250            // Ensure slots exist and borrow mutably (handled inside measure_layout via MemoryApplier)
251            match cranpose_ui::measure_layout_with_options(
252                &mut applier,
253                root,
254                viewport_size,
255                MeasureLayoutOptions {
256                    collect_semantics: false,
257                    build_layout_tree: false,
258                },
259            ) {
260                Ok(_measurements) => {
261                    self.layout_tree = None;
262                    if self.semantics_enabled {
263                        self.semantics_tree = None;
264                    }
265                    if has_scoped_repasses
266                        && !global_layout_invalidation
267                        && !force_layout_pass
268                        && !scoped_layout_nodes.is_empty()
269                    {
270                        self.scoped_layout_scene_nodes = scoped_layout_nodes;
271                    } else {
272                        self.scoped_layout_scene_nodes.clear();
273                    }
274                    self.scene_dirty = true;
275                }
276                Err(err) => {
277                    log::error!("failed to compute layout: {err}");
278                    self.layout_tree = None;
279                    self.semantics_tree = None;
280                    self.scoped_layout_scene_nodes.clear();
281                    self.scene_dirty = true;
282                }
283            }
284            applier.clear_runtime_handle();
285        } else {
286            self.layout_tree = None;
287            self.semantics_tree = None;
288            self.scoped_layout_scene_nodes.clear();
289            self.scene_dirty = true;
290            self.layout_requested = false;
291            self.force_layout_pass = false;
292        }
293    }
294
295    fn run_post_layout_recomposition(&mut self) -> bool {
296        if !self.composition.should_render() {
297            return false;
298        }
299
300        let Some(root_key) = self.composition.root_key() else {
301            return false;
302        };
303
304        match self.composition.reconcile(root_key, &mut *self.content) {
305            Ok(changed) => {
306                if !changed {
307                    return false;
308                }
309                self.fps_monitor.record_recomposition();
310                if self.composition_tree_needs_layout() {
311                    self.request_layout_pass();
312                    self.run_layout_phase_in_context();
313                }
314                request_render_invalidation();
315                true
316            }
317            Err(NodeError::Missing { id }) => {
318                log::debug!(
319                    "Post-layout recomposition skipped: node {} no longer exists",
320                    id
321                );
322                self.request_layout_pass();
323                request_render_invalidation();
324                true
325            }
326            Err(err) => {
327                log::error!("post-layout recomposition failed: {err}");
328                self.request_layout_pass();
329                request_render_invalidation();
330                true
331            }
332        }
333    }
334
335    fn run_dispatch_queues(&mut self) {
336        // Process pointer input repasses
337        // Similar to Jetpack Compose's pointer input invalidation processing,
338        // we service nodes that need pointer input state updates without forcing layout/draw
339        if has_pending_pointer_repasses() {
340            let mut applier = self.composition.applier_mut();
341            process_pointer_repasses(|node_id| {
342                match clear_dispatch_invalidation(
343                    &mut applier,
344                    node_id,
345                    DispatchInvalidationKind::Pointer,
346                ) {
347                    Ok(true) => {
348                        log::trace!("Cleared pointer repass flag for node #{}", node_id);
349                    }
350                    Ok(false) => {}
351                    Err(err) => {
352                        log::debug!(
353                            "Could not process pointer repass for node #{}: {}",
354                            node_id,
355                            err
356                        );
357                    }
358                }
359            });
360        }
361
362        // Process focus invalidations
363        // Mirrors Jetpack Compose's FocusInvalidationManager.invalidateNodes(),
364        // processing nodes that need focus state synchronization
365        if has_pending_focus_invalidations() {
366            let mut applier = self.composition.applier_mut();
367            process_focus_invalidations(|node_id| {
368                match clear_dispatch_invalidation(
369                    &mut applier,
370                    node_id,
371                    DispatchInvalidationKind::Focus,
372                ) {
373                    Ok(true) => {
374                        log::trace!("Cleared focus sync flag for node #{}", node_id);
375                    }
376                    Ok(false) => {}
377                    Err(err) => {
378                        log::debug!(
379                            "Could not process focus invalidation for node #{}: {}",
380                            node_id,
381                            err
382                        );
383                    }
384                }
385            });
386        }
387    }
388
389    fn refresh_draw_repasses(&mut self) -> Vec<NodeId> {
390        let dirty_nodes = take_draw_repass_nodes();
391        if dirty_nodes.is_empty() {
392            return dirty_nodes;
393        }
394        self.refresh_draw_nodes(dirty_nodes)
395    }
396
397    fn refresh_retained_redraw_nodes(&mut self) -> Vec<NodeId> {
398        let Some(root) = self.composition.root() else {
399            return Vec::new();
400        };
401        let mut dirty_nodes = Vec::new();
402        {
403            let mut applier = self.composition.applier_mut();
404            collect_retained_redraw_nodes(&mut applier, root, &mut dirty_nodes);
405        }
406        if dirty_nodes.is_empty() {
407            return dirty_nodes;
408        }
409        self.refresh_draw_nodes(dirty_nodes)
410    }
411
412    fn refresh_draw_nodes(&mut self, dirty_nodes: Vec<NodeId>) -> Vec<NodeId> {
413        let Some(layout_tree) = self.layout_tree.as_mut() else {
414            return dirty_nodes;
415        };
416
417        let dirty_set: HashSet<NodeId> = dirty_nodes.into_iter().collect();
418        let mut applier = self.composition.applier_mut();
419        let refresh_scope = build_draw_refresh_scope(&mut applier, &dirty_set);
420        refresh_layout_box_data(
421            &mut applier,
422            layout_tree.root_mut(),
423            &refresh_scope,
424            &dirty_set,
425        );
426        dirty_set.into_iter().collect()
427    }
428
429    #[cfg(test)]
430    pub(crate) fn run_render_phase(&mut self) -> FrameUpdateResult {
431        let app_context = Rc::clone(&self.app_context);
432        app_context.enter(|| self.run_render_phase_in_context(false))
433    }
434
435    fn run_render_phase_with_recomposition_state(
436        &mut self,
437        recomposed_this_frame: bool,
438    ) -> FrameUpdateResult {
439        let app_context = Rc::clone(&self.app_context);
440        app_context.enter(|| self.run_render_phase_in_context(recomposed_this_frame))
441    }
442
443    fn run_render_phase_in_context(&mut self, recomposed_this_frame: bool) -> FrameUpdateResult {
444        let render_dirty = take_render_invalidation();
445        let pointer_dirty = take_pointer_invalidation();
446        take_focus_invalidation();
447        let draw_repass_pending = cranpose_ui::has_pending_draw_repasses();
448        let mut draw_dirty_nodes = self.refresh_draw_repasses();
449        if render_dirty && !draw_repass_pending && draw_dirty_nodes.is_empty() {
450            draw_dirty_nodes = self.refresh_retained_redraw_nodes();
451        }
452        let layout_dirty_nodes = std::mem::take(&mut self.scoped_layout_scene_nodes);
453        let mut partial_dirty_nodes = draw_dirty_nodes.clone();
454        partial_dirty_nodes.extend(layout_dirty_nodes.iter().copied());
455        partial_dirty_nodes.sort_unstable();
456        partial_dirty_nodes.dedup();
457        let draw_dirty_node_count = draw_dirty_nodes.len();
458        let layout_dirty_node_count = layout_dirty_nodes.len();
459        let partial_dirty_node_count = partial_dirty_nodes.len();
460        // Detect a focused text field that left the composition this frame:
461        // the check clears the stale focus entry and asks the platform to
462        // hide its soft keyboard (no-op while focus is live or when no
463        // keyboard was requested).
464        let _ = cranpose_ui::has_focused_field();
465        // Tick cursor blink timer - only marks dirty when visibility state changes
466        let cursor_blink_dirty = cranpose_ui::tick_cursor_blink();
467
468        let render_only_dirty =
469            (render_dirty && partial_dirty_nodes.is_empty() && !draw_repass_pending)
470                || cursor_blink_dirty;
471        let scene_dirty = self.scene_dirty;
472        let draw_only_partial_dirty = !draw_dirty_nodes.is_empty()
473            && layout_dirty_nodes.is_empty()
474            && !pointer_dirty
475            && !recomposed_this_frame;
476        let scoped_scene_dirty = scene_dirty && !layout_dirty_nodes.is_empty();
477        let full_scene_dirty = scene_dirty && !scoped_scene_dirty && !draw_only_partial_dirty;
478        let partial_scene_dirty = !partial_dirty_nodes.is_empty();
479        let needs_scene_rebuild = full_scene_dirty
480            || scoped_scene_dirty
481            || partial_scene_dirty
482            || draw_repass_pending
483            || render_only_dirty;
484
485        if !needs_scene_rebuild {
486            log_render_phase_dirty_diagnostics(RenderPhaseDirtyDiagnostics {
487                render_dirty,
488                pointer_dirty,
489                scene_dirty,
490                draw_repass_pending,
491                draw_dirty_nodes: draw_dirty_node_count,
492                layout_dirty_nodes: layout_dirty_node_count,
493                partial_dirty_nodes: partial_dirty_node_count,
494                dirty_node_ids: render_phase_dirty_diagnostics_enabled().then(|| {
495                    format!(
496                        "draw={:?} layout={:?} partial={:?}",
497                        draw_dirty_nodes, layout_dirty_nodes, partial_dirty_nodes
498                    )
499                }),
500                render_only_dirty,
501                recomposed_this_frame,
502                path: "skip",
503            });
504            self.scoped_layout_scene_nodes = layout_dirty_nodes;
505            return FrameUpdateResult::default();
506        }
507        self.scene_dirty = false;
508        let viewport_size = Size {
509            width: self.viewport.0,
510            height: self.viewport.1,
511        };
512        let visual_update_only =
513            draw_only_partial_dirty && !partial_dirty_nodes.is_empty() && !full_scene_dirty;
514        let structure_changed = !render_only_dirty && !visual_update_only;
515
516        // Use new direct traversal rendering
517        if let Some(root) = self.composition.root() {
518            let mut applier = self.composition.applier_mut();
519            let use_partial_update =
520                !partial_dirty_nodes.is_empty() && !render_only_dirty && !full_scene_dirty;
521            let use_visual_update = use_partial_update && draw_only_partial_dirty;
522            log_render_phase_dirty_diagnostics(RenderPhaseDirtyDiagnostics {
523                render_dirty,
524                pointer_dirty,
525                scene_dirty,
526                draw_repass_pending,
527                draw_dirty_nodes: draw_dirty_node_count,
528                layout_dirty_nodes: layout_dirty_node_count,
529                partial_dirty_nodes: partial_dirty_node_count,
530                dirty_node_ids: render_phase_dirty_diagnostics_enabled().then(|| {
531                    format!(
532                        "draw={:?} layout={:?} partial={:?}",
533                        draw_dirty_nodes, layout_dirty_nodes, partial_dirty_nodes
534                    )
535                }),
536                render_only_dirty,
537                recomposed_this_frame,
538                path: if use_visual_update {
539                    "visual-update"
540                } else if use_partial_update {
541                    "update"
542                } else {
543                    "rebuild"
544                },
545            });
546            let rebuild_result = if use_visual_update {
547                self.renderer.update_visual_scene_from_applier(
548                    &mut applier,
549                    root,
550                    viewport_size,
551                    &partial_dirty_nodes,
552                )
553            } else if use_partial_update {
554                self.renderer.update_scene_from_applier(
555                    &mut applier,
556                    root,
557                    viewport_size,
558                    &partial_dirty_nodes,
559                )
560            } else {
561                self.renderer
562                    .rebuild_scene_from_applier(&mut applier, root, viewport_size)
563            };
564            if let Err(err) = rebuild_result {
565                // Fallback to clearing scene on error
566                log::error!("renderer rebuild failed: {err:?}");
567                self.renderer.scene_mut().clear();
568            }
569        } else {
570            self.renderer.scene_mut().clear();
571        }
572
573        // Draw FPS overlay if enabled (directly by renderer, no composition)
574        if self.dev_options.fps_counter {
575            self.refresh_dev_overlay_text_for_frame_at(viewport_size, Instant::now());
576            let renderer = &mut self.renderer;
577            let text = self.dev_overlay_text.as_str();
578            renderer.draw_dev_overlay(text, viewport_size);
579        }
580
581        FrameUpdateResult {
582            visual_changed: true,
583            structure_changed,
584        }
585    }
586
587    fn refresh_dev_overlay_text_for_frame_at(&mut self, viewport_size: Size, now: Instant) {
588        if !self.dev_overlay_text_needs_refresh(viewport_size, now) {
589            return;
590        }
591        self.dev_overlay_text = self.build_dev_overlay_text(viewport_size);
592        self.dev_overlay_last_refresh = Some(now);
593        self.dev_overlay_viewport = Some(viewport_size);
594    }
595
596    fn dev_overlay_text_needs_refresh(&self, viewport_size: Size, now: Instant) -> bool {
597        if self.dev_overlay_text.is_empty() || self.dev_overlay_viewport != Some(viewport_size) {
598            return true;
599        }
600
601        self.dev_overlay_last_refresh
602            .map(|last| {
603                now.checked_duration_since(last).unwrap_or_default() >= DEV_OVERLAY_REFRESH_INTERVAL
604            })
605            .unwrap_or(true)
606    }
607
608    fn build_dev_overlay_text(&mut self, viewport_size: Size) -> String {
609        self.dev_overlay_controls.clear();
610
611        let stats = self.fps_monitor.stats();
612        let mut text = format!(
613            "{:.0} FPS | avg {:.1}ms | p95 {:.1}ms | max {:.1}ms | work {:.1}ms | {} recomp/s",
614            stats.fps,
615            stats.avg_ms,
616            stats.p95_ms,
617            stats.max_ms,
618            stats.work_avg_ms,
619            stats.recomps_per_second
620        );
621
622        if !self.dev_options.frame_pacing_controls {
623            return text;
624        }
625
626        text.push_str(" | ");
627        let mut controls = Vec::with_capacity(FramePacingMode::ALL.len());
628        for (index, mode) in FramePacingMode::ALL.into_iter().enumerate() {
629            if index > 0 {
630                text.push(' ');
631            }
632            let start = text.len();
633            if mode == self.dev_options.frame_pacing_mode {
634                text.push('[');
635                text.push_str(mode.label());
636                text.push(']');
637            } else {
638                text.push_str(mode.label());
639            }
640            controls.push((start, text.len(), mode));
641        }
642
643        let overlay_width = text.len() as f32 * DEV_OVERLAY_CHAR_WIDTH;
644        let overlay_x = (viewport_size.width - overlay_width - DEV_OVERLAY_PADDING * 2.0)
645            .max(DEV_OVERLAY_PADDING);
646        let overlay_y = DEV_OVERLAY_PADDING;
647        let text_x = overlay_x + DEV_OVERLAY_PADDING / 2.0;
648        let text_y = overlay_y + DEV_OVERLAY_PADDING / 4.0;
649        let text_height = DEV_OVERLAY_FONT_SIZE * 1.4;
650
651        self.dev_overlay_controls = controls
652            .into_iter()
653            .map(|(start, end, mode)| DevOverlayControl {
654                bounds: Rect {
655                    x: text_x + start as f32 * DEV_OVERLAY_CHAR_WIDTH - 3.0,
656                    y: text_y - 3.0,
657                    width: (end - start) as f32 * DEV_OVERLAY_CHAR_WIDTH + 6.0,
658                    height: text_height + 6.0,
659                },
660                mode,
661            })
662            .collect();
663
664        text
665    }
666}
667
668fn clear_dispatch_invalidation(
669    applier: &mut MemoryApplier,
670    node_id: NodeId,
671    invalidation: DispatchInvalidationKind,
672) -> Result<bool, NodeError> {
673    match invalidation {
674        DispatchInvalidationKind::Pointer => {
675            match applier.with_node::<LayoutNode, _>(node_id, |node| {
676                let needs_pointer_pass = node.needs_pointer_pass();
677                if needs_pointer_pass {
678                    node.clear_needs_pointer_pass();
679                }
680                needs_pointer_pass
681            }) {
682                Ok(cleared) => Ok(cleared),
683                Err(NodeError::TypeMismatch { .. }) => applier
684                    .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
685                        let needs_pointer_pass = node.needs_pointer_pass();
686                        if needs_pointer_pass {
687                            node.clear_needs_pointer_pass();
688                        }
689                        needs_pointer_pass
690                    }),
691                Err(err) => Err(err),
692            }
693        }
694        DispatchInvalidationKind::Focus => {
695            match applier.with_node::<LayoutNode, _>(node_id, |node| {
696                let needs_focus_sync = node.needs_focus_sync();
697                if needs_focus_sync {
698                    node.clear_needs_focus_sync();
699                }
700                needs_focus_sync
701            }) {
702                Ok(cleared) => Ok(cleared),
703                Err(NodeError::TypeMismatch { .. }) => applier
704                    .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
705                        let needs_focus_sync = node.needs_focus_sync();
706                        if needs_focus_sync {
707                            node.clear_needs_focus_sync();
708                        }
709                        needs_focus_sync
710                    }),
711                Err(err) => Err(err),
712            }
713        }
714    }
715}
716
717pub(crate) fn build_draw_refresh_scope(
718    applier: &mut MemoryApplier,
719    dirty_nodes: &HashSet<NodeId>,
720) -> HashSet<NodeId> {
721    let mut refresh_scope = HashSet::with_capacity(dirty_nodes.len());
722    for &dirty_node in dirty_nodes {
723        let mut current = Some(dirty_node);
724        while let Some(node_id) = current {
725            if !refresh_scope.insert(node_id) {
726                break;
727            }
728            current = applier.get_mut(node_id).ok().and_then(|node| node.parent());
729        }
730    }
731    refresh_scope
732}
733
734fn collect_retained_redraw_nodes(
735    applier: &mut MemoryApplier,
736    node_id: NodeId,
737    dirty_nodes: &mut Vec<NodeId>,
738) {
739    let children = match applier.get_mut(node_id) {
740        Ok(node) => node.children(),
741        Err(_) => return,
742    };
743
744    let redraw = match applier.with_node::<LayoutNode, _>(node_id, |node| {
745        let needs_redraw = node.needs_redraw();
746        if needs_redraw {
747            node.clear_needs_redraw();
748        }
749        needs_redraw
750    }) {
751        Ok(needs_redraw) => needs_redraw,
752        Err(NodeError::TypeMismatch { .. }) => applier
753            .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
754                let needs_redraw = node.needs_redraw();
755                if needs_redraw {
756                    node.clear_needs_redraw();
757                }
758                needs_redraw
759            })
760            .unwrap_or(false),
761        Err(_) => false,
762    };
763    if redraw {
764        dirty_nodes.push(node_id);
765    }
766
767    for child in children {
768        collect_retained_redraw_nodes(applier, child, dirty_nodes);
769    }
770}
771
772fn refresh_layout_box_data(
773    applier: &mut MemoryApplier,
774    layout: &mut cranpose_ui::layout::LayoutBox,
775    refresh_scope: &HashSet<NodeId>,
776    dirty_nodes: &HashSet<NodeId>,
777) {
778    if !refresh_scope.contains(&layout.node_id) {
779        return;
780    }
781
782    if dirty_nodes.contains(&layout.node_id) {
783        if let Ok((modifier, resolved_modifiers, slices)) =
784            applier.with_node::<LayoutNode, _>(layout.node_id, |node| {
785                node.clear_needs_redraw();
786                (
787                    node.modifier.clone(),
788                    node.resolved_modifiers(),
789                    node.modifier_slices_snapshot(),
790                )
791            })
792        {
793            layout.node_data.modifier = modifier;
794            layout.node_data.resolved_modifiers = resolved_modifiers;
795            layout.node_data.modifier_slices = slices;
796        } else if let Ok((modifier, resolved_modifiers)) = applier
797            .with_node::<SubcomposeLayoutNode, _>(layout.node_id, |node| {
798                node.clear_needs_redraw();
799                (node.modifier(), node.resolved_modifiers())
800            })
801        {
802            layout.node_data.modifier = modifier.clone();
803            layout.node_data.resolved_modifiers = resolved_modifiers;
804            layout.node_data.modifier_slices =
805                std::rc::Rc::new(cranpose_ui::collect_slices_from_modifier(&modifier));
806        }
807    }
808
809    for child in &mut layout.children {
810        refresh_layout_box_data(applier, child, refresh_scope, dirty_nodes);
811    }
812}