Skip to main content

fret_ui/tree/
mod.rs

1use crate::{
2    Theme, UiHost, declarative,
3    elements::GlobalElementId,
4    widget::{
5        CommandCx, EventCx, Invalidation, LayoutCx, PaintCx, PlatformTextInputCx, SemanticsCx,
6        Widget,
7    },
8};
9use fret_core::time::{Duration, Instant};
10use fret_core::{
11    AppWindowId, Corners, Event, KeyCode, NodeId, Point, PointerEvent, PointerId, Px, Rect, Scene,
12    SceneOp, SemanticsNode, SemanticsRole, SemanticsRoot, SemanticsSnapshot, Size, TextConstraints,
13    Transform2D, UiServices, ViewId,
14};
15use fret_runtime::{
16    CommandId, Effect, FrameId, InputContext, InputDispatchPhase, KeyChord, KeymapService, ModelId,
17    Platform, PlatformCapabilities, TickId,
18};
19use slotmap::{Key, SecondaryMap, SlotMap};
20use std::any::TypeId;
21use std::collections::{HashMap, HashSet};
22use std::panic::{AssertUnwindSafe, Location, catch_unwind, resume_unwind};
23use std::sync::Arc;
24use std::sync::{Mutex, OnceLock};
25
26mod bounds_tree;
27mod commands;
28mod debug;
29mod dispatch;
30mod dispatch_snapshot;
31mod frame_arena;
32mod hit_test;
33mod invalidation_dedup;
34mod layers;
35mod layout;
36mod measure;
37mod node_storage;
38mod observation;
39mod paint;
40mod paint_cache;
41pub(crate) mod paint_style;
42mod prepaint;
43mod profiling;
44mod propagation_depth;
45mod semantics;
46mod shortcuts;
47mod small_list;
48mod ui_tree_accessors;
49mod ui_tree_debug;
50mod ui_tree_default;
51mod ui_tree_focus;
52mod ui_tree_input_snapshot;
53mod ui_tree_invalidation;
54mod ui_tree_invalidation_walk;
55mod ui_tree_mutation;
56mod ui_tree_outside_press;
57mod ui_tree_scratch;
58mod ui_tree_semantics;
59mod ui_tree_subtree_layout_dirty;
60mod ui_tree_text_input;
61mod ui_tree_view_cache;
62mod ui_tree_widget;
63mod util;
64use debug::{
65    DebugLayoutStackFrame, DebugPaintStackFrame, DebugViewCacheRootRecord,
66    DebugWidgetMeasureStackFrame, UiDebugHoverDeclarativeInvalidationCounts,
67};
68pub use debug::{
69    PointerOcclusion, UiDebugCacheRootReuseReason, UiDebugCacheRootStats, UiDebugDirtyView,
70    UiDebugFrameStats, UiDebugGlobalChangeHotspot, UiDebugGlobalChangeUnobserved, UiDebugHitTest,
71    UiDebugHoverDeclarativeInvalidationHotspot, UiDebugInvalidationDetail,
72    UiDebugInvalidationSource, UiDebugInvalidationWalk, UiDebugLayerInfo,
73    UiDebugLayoutEngineMeasureChildHotspot, UiDebugLayoutEngineMeasureHotspot,
74    UiDebugLayoutEngineSolve, UiDebugLayoutHotspot, UiDebugModelChangeHotspot,
75    UiDebugModelChangeUnobserved, UiDebugNotifyRequest, UiDebugPaintTextPrepareHotspot,
76    UiDebugPaintWidgetHotspot, UiDebugPrepaintAction, UiDebugPrepaintActionKind,
77    UiDebugRetainedVirtualListReconcile, UiDebugRetainedVirtualListReconcileKind,
78    UiDebugScrollAxis, UiDebugScrollHandleChange, UiDebugScrollHandleChangeKind,
79    UiDebugScrollNodeTelemetry, UiDebugScrollOverflowObservationTelemetry,
80    UiDebugScrollbarTelemetry, UiDebugTextConstraintsSnapshot, UiDebugVirtualListWindow,
81    UiDebugVirtualListWindowShiftApplyMode, UiDebugVirtualListWindowShiftKind,
82    UiDebugVirtualListWindowShiftReason, UiDebugVirtualListWindowShiftSample,
83    UiDebugVirtualListWindowSource, UiDebugWidgetMeasureHotspot, UiInputArbitrationSnapshot,
84};
85use frame_arena::FrameArenaScratch;
86use invalidation_dedup::{InvalidationDedupTable, InvalidationVisited};
87use measure::{DebugMeasureChildRecord, MeasureReentrancyDiagnostics, MeasureStackKey};
88use observation::{GlobalObservationIndex, ObservationIndex, ObservationMask};
89use profiling::{
90    LayoutNodeProfileConfig, LayoutNodeProfileState, MeasureNodeProfileConfig,
91    MeasureNodeProfileState,
92};
93use propagation_depth::PropagationDepthCacheEntry;
94#[cfg(test)]
95use util::event_allows_hit_test_path_cache_reuse;
96use util::{
97    TouchPointerDownOutsideCandidate, event_position, interactive_resize_stable_frames_required,
98    pointer_type_supports_hover, rect_aabb_transformed, text_wrap_width_bucket_px,
99    text_wrap_width_small_step_bucket_px, text_wrap_width_small_step_max_dw_px,
100};
101
102#[cfg(feature = "diagnostics")]
103pub use debug::{
104    UiDebugDispatchSnapshot, UiDebugDispatchSnapshotNode, UiDebugDispatchSnapshotParityReport,
105    UiDebugOverlayPolicyDecisionWrite, UiDebugParentSeverWrite, UiDebugRemoveSubtreeFrameContext,
106    UiDebugRemoveSubtreeOutcome, UiDebugRemoveSubtreeRecord, UiDebugSetChildrenWrite,
107    UiDebugSetLayerVisibleWrite,
108};
109
110use layers::UiLayer;
111pub use layers::{OverlayRootOptions, UiLayerId};
112use node_storage::{
113    ChildrenWritePolicy, HitTestPathCache, Node, NodeMeasureCache, NodeMeasureCacheKey,
114    PrepaintHitTestCache, ViewCacheFlags,
115};
116pub use paint_cache::PaintCachePolicy;
117use paint_cache::{PaintCacheEntry, PaintCacheKey, PaintCacheState};
118use shortcuts::{
119    KeydownShortcutParams, PendingShortcut, PointerDownOutsideOutcome, PointerDownOutsideParams,
120};
121use small_list::{SmallCopyList, SmallNodeList};
122
123pub(crate) use dispatch_snapshot::UiDispatchSnapshot;
124
125fn type_id_sort_key(id: TypeId) -> u64 {
126    use std::hash::{Hash, Hasher};
127
128    let mut hasher = std::collections::hash_map::DefaultHasher::new();
129    id.hash(&mut hasher);
130    hasher.finish()
131}
132
133fn record_layout_invalidation_transition(count: &mut u32, before: bool, after: bool) {
134    if before == after {
135        return;
136    }
137    if after {
138        *count = count.saturating_add(1);
139    } else {
140        debug_assert!(*count > 0);
141        *count = count.saturating_sub(1);
142    }
143}
144
145#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
146pub struct InvalidationFlags {
147    pub layout: bool,
148    pub paint: bool,
149    pub hit_test: bool,
150}
151
152impl InvalidationFlags {
153    pub fn mark(&mut self, inv: Invalidation) {
154        match inv {
155            Invalidation::Paint => self.paint = true,
156            Invalidation::Layout => {
157                self.layout = true;
158                self.paint = true;
159            }
160            Invalidation::HitTest => {
161                self.hit_test = true;
162                self.layout = true;
163                self.paint = true;
164            }
165            Invalidation::HitTestOnly => {
166                self.hit_test = true;
167                self.paint = true;
168            }
169        }
170    }
171
172    pub fn clear(&mut self) {
173        self.layout = false;
174        self.paint = false;
175        self.hit_test = false;
176    }
177}
178
179/// Retained UI tree and per-window interaction state machine.
180///
181/// `UiTree` owns the widget/node graph for a single window and is responsible for:
182/// - mounting declarative element roots,
183/// - routing input events and commands,
184/// - running layout and producing paint scenes,
185/// - producing semantics snapshots for accessibility backends,
186/// - tracking focus/capture/hover and other interaction state across frames.
187///
188/// Higher-level driver layers (e.g. `fret-bootstrap`) orchestrate when and how a `UiTree` is
189/// ticked and provide host services via the [`UiHost`] trait.
190pub struct UiTree<H: UiHost> {
191    nodes: SlotMap<NodeId, Node<H>>,
192    layers: SlotMap<UiLayerId, UiLayer>,
193    layer_order: Vec<UiLayerId>,
194    root_to_layer: HashMap<NodeId, UiLayerId>,
195    base_layer: Option<UiLayerId>,
196    focus: Option<NodeId>,
197    pending_focus_target: Option<GlobalElementId>,
198    captured: HashMap<PointerId, NodeId>,
199    active_touch_drag_target: HashMap<PointerId, GlobalElementId>,
200    last_pointer_move_hit: HashMap<PointerId, Option<NodeId>>,
201    touch_pointer_down_outside_candidates: HashMap<PointerId, TouchPointerDownOutsideCandidate>,
202    hit_test_path_cache: Option<HitTestPathCache>,
203    hit_test_bounds_trees: bounds_tree::HitTestBoundsTrees,
204    last_internal_drag_target: Option<NodeId>,
205    window: Option<AppWindowId>,
206    ime_allowed: bool,
207    ime_composing: bool,
208    suppress_text_input_until_key_up: Option<KeyCode>,
209    pending_shortcut: PendingShortcut,
210    replaying_pending_shortcut: bool,
211    alt_menu_bar_arm_key: Option<KeyCode>,
212    alt_menu_bar_canceled: bool,
213    observed_in_layout: ObservationIndex,
214    observed_in_paint: ObservationIndex,
215    observed_globals_in_layout: GlobalObservationIndex,
216    observed_globals_in_paint: GlobalObservationIndex,
217    measure_stack: Vec<MeasureStackKey>,
218    measure_cache_this_frame: HashMap<MeasureStackKey, Size>,
219    frame_arena: FrameArenaScratch,
220    paint_pass: u64,
221    scratch_pending_invalidations: HashMap<NodeId, u8>,
222    scratch_node_stack: Vec<NodeId>,
223    scratch_element_nodes: Vec<(GlobalElementId, NodeId)>,
224    scratch_bounds_records: Vec<(GlobalElementId, Rect)>,
225    scratch_visual_bounds_records: Vec<(GlobalElementId, Rect)>,
226    measure_reentrancy_diagnostics: MeasureReentrancyDiagnostics,
227    layout_engine: crate::layout_engine::TaffyLayoutEngine,
228    layout_call_depth: u32,
229    layout_invalidations_count: u32,
230    last_layout_frame_id: Option<FrameId>,
231    last_layout_bounds: Option<Rect>,
232    last_layout_scale_factor: Option<f32>,
233    interactive_resize_active: bool,
234    interactive_resize_needs_full_rebuild: bool,
235    interactive_resize_stable_frames: u8,
236    interactive_resize_last_updated_frame: Option<FrameId>,
237    interactive_resize_last_bounds_delta: Option<(fret_core::Px, fret_core::Px)>,
238    viewport_roots: Vec<(NodeId, Rect)>,
239    pending_barrier_relayouts: Vec<NodeId>,
240    pending_declarative_window_snapshot_roots: HashSet<NodeId>,
241    pending_post_layout_window_runtime_snapshot_refine: bool,
242
243    #[cfg(debug_assertions)]
244    debug_last_declarative_render_root_frame_id: Option<FrameId>,
245
246    debug_enabled: bool,
247    debug_stats: UiDebugFrameStats,
248    debug_view_cache_roots: Vec<DebugViewCacheRootRecord>,
249    debug_view_cache_contained_relayout_roots: Vec<NodeId>,
250    debug_paint_cache_replays: HashMap<NodeId, u32>,
251    debug_paint_widget_exclusive_started: Option<Instant>,
252    debug_layout_engine_solves: Vec<UiDebugLayoutEngineSolve>,
253    debug_layout_hotspots: Vec<UiDebugLayoutHotspot>,
254    debug_layout_inclusive_hotspots: Vec<UiDebugLayoutHotspot>,
255    debug_layout_stack: Vec<DebugLayoutStackFrame>,
256    debug_widget_measure_hotspots: Vec<UiDebugWidgetMeasureHotspot>,
257    debug_widget_measure_stack: Vec<DebugWidgetMeasureStackFrame>,
258    debug_paint_widget_hotspots: Vec<UiDebugPaintWidgetHotspot>,
259    debug_paint_text_prepare_hotspots: Vec<UiDebugPaintTextPrepareHotspot>,
260    debug_paint_stack: Vec<DebugPaintStackFrame>,
261    debug_measure_children: HashMap<NodeId, HashMap<NodeId, DebugMeasureChildRecord>>,
262    debug_invalidation_walks: Vec<UiDebugInvalidationWalk>,
263    debug_model_change_hotspots: Vec<UiDebugModelChangeHotspot>,
264    debug_model_change_unobserved: Vec<UiDebugModelChangeUnobserved>,
265    debug_global_change_hotspots: Vec<UiDebugGlobalChangeHotspot>,
266    debug_global_change_unobserved: Vec<UiDebugGlobalChangeUnobserved>,
267    debug_hover_edge_this_frame: bool,
268    debug_hover_declarative_invalidations:
269        HashMap<NodeId, UiDebugHoverDeclarativeInvalidationCounts>,
270    debug_dirty_views: Vec<UiDebugDirtyView>,
271    #[cfg(feature = "diagnostics")]
272    debug_notify_requests: Vec<UiDebugNotifyRequest>,
273    debug_virtual_list_windows: Vec<UiDebugVirtualListWindow>,
274    debug_virtual_list_window_shift_samples: Vec<UiDebugVirtualListWindowShiftSample>,
275    debug_retained_virtual_list_reconciles: Vec<UiDebugRetainedVirtualListReconcile>,
276    debug_scroll_handle_changes: Vec<UiDebugScrollHandleChange>,
277    debug_scroll_nodes: Vec<UiDebugScrollNodeTelemetry>,
278    debug_scrollbars: Vec<UiDebugScrollbarTelemetry>,
279    debug_prepaint_actions: Vec<UiDebugPrepaintAction>,
280    #[cfg(feature = "diagnostics")]
281    debug_set_children_writes: HashMap<NodeId, UiDebugSetChildrenWrite>,
282    #[cfg(feature = "diagnostics")]
283    debug_parent_sever_writes: HashMap<NodeId, UiDebugParentSeverWrite>,
284    #[cfg(feature = "diagnostics")]
285    debug_layer_visible_writes: Vec<UiDebugSetLayerVisibleWrite>,
286    #[cfg(feature = "diagnostics")]
287    debug_overlay_policy_decisions: Vec<UiDebugOverlayPolicyDecisionWrite>,
288    #[cfg(feature = "diagnostics")]
289    debug_remove_subtree_frame_context: HashMap<NodeId, UiDebugRemoveSubtreeFrameContext>,
290    #[cfg(feature = "diagnostics")]
291    debug_removed_subtrees: Vec<UiDebugRemoveSubtreeRecord>,
292    #[cfg(feature = "diagnostics")]
293    debug_reachable_from_layer_roots: Option<(FrameId, HashSet<NodeId>)>,
294    #[cfg(feature = "diagnostics")]
295    debug_dispatch_snapshot: Option<UiDispatchSnapshot>,
296    #[cfg(feature = "diagnostics")]
297    debug_text_constraints_measured: HashMap<NodeId, TextConstraints>,
298    #[cfg(feature = "diagnostics")]
299    debug_text_constraints_prepared: HashMap<NodeId, TextConstraints>,
300
301    view_cache_enabled: bool,
302    paint_cache_policy: PaintCachePolicy,
303    inspection_active: bool,
304    paint_cache: PaintCacheState,
305    interaction_cache: prepaint::InteractionCacheState,
306
307    dirty_cache_roots: HashSet<NodeId>,
308    dirty_cache_root_reasons:
309        HashMap<NodeId, (UiDebugInvalidationSource, UiDebugInvalidationDetail)>,
310    last_redraw_request_tick: Option<TickId>,
311
312    propagation_depth_generation: u32,
313    propagation_depth_cache: SecondaryMap<NodeId, PropagationDepthCacheEntry>,
314    propagation_chain: Vec<NodeId>,
315    propagation_entries: Vec<(u8, u32, u64, NodeId, Invalidation)>,
316    invalidation_dedup: InvalidationDedupTable,
317    invalidated_layout_nodes: u32,
318    invalidated_paint_nodes: u32,
319    invalidated_hit_test_nodes: u32,
320
321    semantics: Option<Arc<SemanticsSnapshot>>,
322    semantics_requested: bool,
323    layout_node_profile: Option<LayoutNodeProfileState>,
324    measure_node_profile: Option<MeasureNodeProfileState>,
325    deferred_cleanup: Vec<Box<dyn Widget<H>>>,
326}
327
328#[cfg(test)]
329mod tests;