Skip to main content

repose_ui/layout/
engine.rs

1#![allow(non_snake_case)]
2
3use std::cell::RefCell;
4use std::collections::HashMap;
5use std::hash::{Hash, Hasher};
6use std::rc::Rc;
7
8use repose_core::*;
9use repose_tree::{LayoutConstraints, NodeId, ViewTree};
10use rustc_hash::{FxHashMap, FxHasher};
11use taffy::TaffyTree;
12use taffy::prelude::*;
13
14use crate::Interactions;
15use crate::textfield::TextFieldState;
16
17use super::*;
18impl Default for LayoutEngine {
19    fn default() -> Self {
20        Self::new()
21    }
22}
23
24impl LayoutEngine {
25    pub fn layout_frame(
26        &mut self,
27        root: &View,
28        size_px: (u32, u32),
29        textfield_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
30        interactions: &Interactions,
31        focused: Option<u64>,
32    ) -> (Scene, Vec<HitRegion>, Vec<SemNode>) {
33        let start = web_time::Instant::now();
34        repose_text::begin_frame();
35        self.stats = LayoutStats::default();
36
37        // 0a. Reset per-frame state
38        self.focus_group_stack.clear();
39
40        // 0b. Check global invalidation
41        let locals_stamp = Self::locals_stamp();
42        let locals_changed = self.last_locals_stamp != Some(locals_stamp);
43        if locals_changed {
44            self.layout_valid = false;
45            self.paint_cache.clear();
46            self.text_cache.clear();
47        }
48
49        let density_scale = locals::effective_density_scale();
50        let mut max_w_dp = size_px.0 as f32 / density_scale;
51        let mut max_h_dp = size_px.1 as f32 / density_scale;
52        if !max_w_dp.is_finite() || max_w_dp < 10.0 {
53            max_w_dp = 1280.0;
54        }
55        if !max_h_dp.is_finite() || max_h_dp < 10.0 {
56            max_h_dp = 800.0;
57        }
58        self.tree
59            .set_subcompose_scope(repose_core::SubcomposeScope::new(
60                Dp::ZERO,
61                Dp(max_w_dp),
62                Dp::ZERO,
63                Dp(max_h_dp),
64            ));
65        let root_node_id = self.tree.update(root);
66        self.stats.tree = self.tree.stats.clone();
67        let has_paint_mutation = !self.tree.paint_dirty_nodes().is_empty();
68        let size_changed = self.last_size_px != Some(size_px);
69        if has_paint_mutation || size_changed {
70            self.paint_generation = self.paint_generation.wrapping_add(1);
71        }
72        let has_tree_mutation =
73            !self.tree.dirty_nodes().is_empty() || !self.tree.removed_ids.is_empty();
74        if !self.scope_maps_valid || has_tree_mutation {
75            self.build_scope_maps();
76        }
77        if has_tree_mutation {
78            let mut scroll_node_ids = Vec::new();
79            let mut stack = vec![root_node_id];
80            while let Some(id) = stack.pop() {
81                let Some(node) = self.tree.get(id) else {
82                    continue;
83                };
84                if node.modifier.scroll.is_some() {
85                    scroll_node_ids.push(id);
86                }
87                stack.extend(node.children.iter().copied());
88            }
89            self.scroll_node_ids = scroll_node_ids;
90        }
91        if has_tree_mutation || has_paint_mutation {
92            self.deferred_z_cache.clear();
93            self.transform_layer_cache.clear();
94        }
95
96        // 2. Determine layout need
97        // 2a. Publish the current window size class as a default local so that
98        let class = locals::calculate_window_size_class(
99            (max_w_dp * density_scale) as u32,
100            (max_h_dp * density_scale) as u32,
101            density_scale,
102        );
103        if class != locals::window_size_class() {
104            locals::set_window_size_class_default(class);
105        }
106        locals::set_window_container_size(max_w_dp, max_h_dp);
107        let mut need_layout =
108            size_changed || !self.layout_valid || has_tree_mutation || locals_changed;
109
110        // Must force a style refresh for all nodes to keep physical sizes in sync with the new scale.
111        if locals_changed {
112            let all_ids: Vec<NodeId> = self.tree.iter_with_ids().map(|(id, _)| id).collect();
113            for id in all_ids {
114                self.tree.mark_dirty(id);
115            }
116            for st in self.scope_trees.values_mut() {
117                st.valid = false;
118            }
119            need_layout = true;
120        }
121
122        // NOTE: Needed to ensure that text is always re-measured with the new available width
123        if size_changed {
124            // Root tree text cache
125            for &node_id in self.text_cache.keys() {
126                if let Some(&taffy_id) = self.taffy_map.get(&node_id) {
127                    let _ = self.taffy.mark_dirty(taffy_id);
128                }
129            }
130            self.text_cache.clear();
131            // Scope tree text caches
132            for st in self.scope_trees.values_mut() {
133                for &node_id in st.text_cache.keys() {
134                    if let Some(&tid) = st.taffy_map.get(&node_id) {
135                        let _ = st.taffy.mark_dirty(tid);
136                    }
137                }
138                st.text_cache.clear();
139            }
140        }
141        if locals_changed {
142            for st in self.scope_trees.values_mut() {
143                st.text_cache.clear();
144            }
145        }
146
147        if has_tree_mutation {
148            let dirty: Vec<NodeId> = self.tree.dirty_nodes().iter().copied().collect();
149            for nid in dirty {
150                if self.text_cache.contains_key(&nid) {
151                    if let Some(&tid) = self.taffy_map.get(&nid) {
152                        let _ = self.taffy.mark_dirty(tid);
153                    }
154                    self.text_cache.remove(&nid);
155                }
156                for st in self.scope_trees.values_mut() {
157                    if st.text_cache.contains_key(&nid) {
158                        if let Some(&tid) = st.taffy_map.get(&nid) {
159                            let _ = st.taffy.mark_dirty(tid);
160                        }
161                        st.text_cache.remove(&nid);
162                    }
163                }
164                if let Some(&tid) = self.taffy_map.get(&nid) {
165                    let _ = self.taffy.mark_dirty(tid);
166                }
167                for st in self.scope_trees.values_mut() {
168                    if let Some(&tid) = st.taffy_map.get(&nid) {
169                        let _ = st.taffy.mark_dirty(tid);
170                    }
171                }
172                self.paint_cache.remove(&nid);
173            }
174        }
175
176        // Helpers: Dp/Sp authoring units -> px floats for Taffy.
177        // (Sp includes TextScale, like Compose `TextUnit.toPx`.)
178        let px = |v: Dp| v.to_px().0;
179        let font_px = |s: Sp| s.to_px().0;
180
181        // 3. Sync Taffy
182        // 3a. Sync scope-internal TaffyTrees first
183        self.sync_scope_trees(&font_px);
184        // 3b. Sync root TaffyTree (non-scope nodes + scope root markers)
185        self.sync_taffy_tree(root_node_id, &font_px);
186
187        // 4. Compute Layout
188        let taffy_root = self.taffy_map.get(&root_node_id).copied();
189        if let Some(taffy_root) = taffy_root {
190            if need_layout {
191                if let Ok(mut style) = self.taffy.style(taffy_root).cloned() {
192                    style.size.width = length(size_px.0 as f32);
193                    style.size.height = length(size_px.1 as f32);
194                    if let Err(e) = self.taffy.set_style(taffy_root, style) {
195                        log::error!("taffy set_style failed for root: {e:?}");
196                    }
197                } else {
198                    log::error!("taffy root style missing for {:?}", taffy_root);
199                }
200
201                let available = taffy::geometry::Size {
202                    width: AvailableSpace::Definite(size_px.0 as f32),
203                    height: AvailableSpace::Definite(size_px.1 as f32),
204                };
205
206                Self::run_measure_pass(
207                    &mut self.taffy,
208                    taffy_root,
209                    available,
210                    &self.tree,
211                    &mut self.text_cache,
212                    &self.reverse_map,
213                    &self.scope_root_map,
214                    &self.node_to_scope,
215                    &mut self.scope_trees,
216                    &font_px,
217                    &px,
218                    &mut self.baseline_map,
219                    &self.taffy_map,
220                    &mut self.baseline_shifts,
221                );
222
223                // 4a. Store Taffy-computed sizes for non-scope + scope-root nodes
224                for (&node_id, &taffy_id) in &self.taffy_map {
225                    if let Ok(layout) = self.taffy.layout(taffy_id) {
226                        let dp_w = layout.size.width / density_scale;
227                        let dp_h = layout.size.height / density_scale;
228                        let rect = repose_core::Rect {
229                            x: 0.0,
230                            y: 0.0,
231                            w: dp_w,
232                            h: dp_h,
233                        };
234                        self.tree
235                            .set_layout(node_id, rect, rect, LayoutConstraints::default());
236                    }
237                }
238
239                self.last_locals_stamp = Some(locals_stamp);
240
241                self.layout_valid = true;
242                self.last_size_px = Some(size_px);
243                self.stats.layout_misses += 1;
244            } else {
245                self.stats.layout_hits += 1;
246            }
247        }
248        self.stats.layout_time_ms = (web_time::Instant::now() - start).as_secs_f32() * 1000.0;
249
250        // 4.5. Advance scroll physics (pre-paint, so paint only reads offset)
251        self.walk_tick(root_node_id);
252
253        // 5. Paint
254        let t_paint = web_time::Instant::now();
255        self.focus_interaction_sources.clear();
256        let (scene, hits, sems) = self.paint(
257            root_node_id,
258            textfield_states,
259            interactions,
260            focused,
261            &font_px,
262        );
263        self.stats.paint_time_ms = (web_time::Instant::now() - t_paint).as_secs_f32() * 1000.0;
264
265        // Fire focus change callbacks.
266        if self.prev_focused != focused {
267            if let Some(old_id) = self.prev_focused {
268                if let Some(cb) = self.focus_callbacks.get(&old_id) {
269                    (cb)(false);
270                }
271                if let Some(src) = self.focus_interaction_sources.get(&old_id) {
272                    src.to_mutable().emit(Interaction::Unfocus);
273                }
274            }
275            if let Some(new_id) = focused {
276                if let Some(cb) = self.focus_callbacks.get(&new_id) {
277                    (cb)(true);
278                }
279                if let Some(src) = self.focus_interaction_sources.get(&new_id) {
280                    src.to_mutable().emit(Interaction::Focus);
281                }
282            }
283            self.prev_focused = focused;
284        }
285
286        // Clean up callbacks for removed nodes
287        for &node_id in &self.tree.removed_ids {
288            if let Some(&vid) = self.view_ids.get(&node_id) {
289                self.focus_callbacks.remove(&vid);
290                self.focus_interaction_sources.remove(&vid);
291            }
292        }
293
294        self.tree.clear_dirty();
295        (scene, hits, sems)
296    }
297
298    pub fn intrinsic_size(&mut self, view: &View, mode: IntrinsicSizeMode) -> (f32, f32) {
299        let px_closure = |v: Dp| v.to_px().0;
300        let font_px_closure = |s: Sp| s.to_px().0;
301
302        let mut temp_taffy = taffy::TaffyTree::new();
303        let root_tid = self.build_taffy_subtree(view, &mut temp_taffy, &font_px_closure);
304
305        let avail = match mode {
306            IntrinsicSizeMode::MinContent => taffy::geometry::Size {
307                width: taffy::style::AvailableSpace::MinContent,
308                height: taffy::style::AvailableSpace::MinContent,
309            },
310            IntrinsicSizeMode::MaxContent => taffy::geometry::Size {
311                width: taffy::style::AvailableSpace::MaxContent,
312                height: taffy::style::AvailableSpace::MaxContent,
313            },
314        };
315
316        let mut text_cache: FxHashMap<NodeId, TextLayout> = FxHashMap::default();
317        let reverse_map: FxHashMap<taffy::NodeId, NodeId> = FxHashMap::default();
318        let mut baseline_map: FxHashMap<NodeId, TextBaselines> = FxHashMap::default();
319        // Intrinsic queries don't consume shifts; the growth loop needs maps
320        // matching `temp_taffy`, so pass throwaways (no-ops, as today).
321        let empty_map: FxHashMap<NodeId, taffy::NodeId> = FxHashMap::default();
322        let mut dropped_shifts: FxHashMap<NodeId, f32> = FxHashMap::default();
323
324        Self::run_measure_pass(
325            &mut temp_taffy,
326            root_tid,
327            avail,
328            &self.tree,
329            &mut text_cache,
330            &reverse_map,
331            &self.scope_root_map,
332            &self.node_to_scope,
333            &mut self.scope_trees,
334            &font_px_closure,
335            &px_closure,
336            &mut baseline_map,
337            &empty_map,
338            &mut dropped_shifts,
339        );
340
341        let layout = temp_taffy.layout(root_tid).ok();
342        match layout {
343            Some(l) => (l.size.width, l.size.height),
344            None => (0.0, 0.0),
345        }
346    }
347
348    pub fn new() -> Self {
349        Self {
350            tree: ViewTree::new(),
351            taffy: TaffyTree::new(),
352            taffy_map: FxHashMap::default(),
353            reverse_map: FxHashMap::default(),
354            scope_trees: HashMap::new(),
355            scope_root_map: FxHashMap::default(),
356            node_to_scope: FxHashMap::default(),
357            scope_root_ids: FxHashMap::default(),
358            scope_maps_valid: false,
359            scroll_node_ids: Vec::new(),
360            deferred_z_cache: FxHashMap::default(),
361            transform_layer_cache: FxHashMap::default(),
362            text_cache: FxHashMap::default(),
363            baseline_map: FxHashMap::default(),
364            baseline_shifts: FxHashMap::default(),
365            last_size_px: None,
366            layout_valid: false,
367            paint_cache: FxHashMap::default(),
368            stats: LayoutStats::default(),
369            last_locals_stamp: None,
370            view_ids: FxHashMap::default(),
371            next_view_id: 1,
372            layer_id_counter: 0,
373            paint_generation: 0,
374            prev_focused: None,
375            focus_callbacks: FxHashMap::default(),
376            focus_interaction_sources: FxHashMap::default(),
377            prev_observed_rects: FxHashMap::default(),
378            focus_group_stack: Vec::new(),
379        }
380    }
381
382    pub(crate) fn layout_for_node(&self, node_id: NodeId) -> taffy::prelude::Layout {
383        let mut l = self.raw_layout_for_node(node_id);
384        // RowScope baseline-alignment shifts (no-op when absent).
385        if let Some(s) = self.baseline_shift_for(node_id) {
386            l.location.y += s;
387        }
388        l
389    }
390
391    /// Baseline shift for `node_id`, mirroring `raw_layout_for_node`'s
392    /// tree resolution so shifts come from the tree that laid the node out.
393    fn baseline_shift_for(&self, node_id: NodeId) -> Option<f32> {
394        if self.scope_root_map.contains_key(&node_id)
395            && let Some(&s) = self.baseline_shifts.get(&node_id)
396        {
397            return Some(s);
398        }
399        if let Some(parent_id) = self.tree.get(node_id).and_then(|n| n.parent)
400            && let Some(outer_key) = self.node_to_scope.get(&parent_id)
401            && let Some(st) = self.scope_trees.get(outer_key)
402            && let Some(&s) = st.baseline_shifts.get(&node_id)
403        {
404            return Some(s);
405        }
406        if let Some(key) = self.node_to_scope.get(&node_id)
407            && let Some(st) = self.scope_trees.get(key)
408            && let Some(&s) = st.baseline_shifts.get(&node_id)
409        {
410            return Some(s);
411        }
412        self.baseline_shifts.get(&node_id).copied()
413    }
414
415    fn raw_layout_for_node(&self, node_id: NodeId) -> taffy::prelude::Layout {
416        fn fallback() -> taffy::prelude::Layout {
417            log::error!("layout_for_node: missing taffy layout, returning zero rect");
418            taffy::prelude::Layout {
419                size: taffy::geometry::Size {
420                    width: 0.0,
421                    height: 0.0,
422                },
423                location: taffy::geometry::Point { x: 0.0, y: 0.0 },
424                ..Default::default()
425            }
426        }
427        if self.scope_root_map.contains_key(&node_id) {
428            if let Some(&tid) = self.taffy_map.get(&node_id) {
429                if let Ok(l) = self.taffy.layout(tid) {
430                    return *l;
431                }
432                log::error!(
433                    "layout_for_node: taffy layout missing for scope root {:?}",
434                    node_id
435                );
436                return fallback();
437            }
438            if let Some(parent_id) = self.tree.get(node_id).and_then(|n| n.parent)
439                && let Some(outer_key) = self.node_to_scope.get(&parent_id)
440                && let Some(st) = self.scope_trees.get(outer_key)
441                && let Some(&tid) = st.taffy_map.get(&node_id)
442                && let Ok(l) = st.taffy.layout(tid)
443            {
444                return *l;
445            }
446            if let Some(key) = self.node_to_scope.get(&node_id)
447                && let Some(st) = self.scope_trees.get(key)
448                && let Some(&tid) = st.taffy_map.get(&node_id)
449                && let Ok(l) = st.taffy.layout(tid)
450            {
451                return *l;
452            }
453            return fallback();
454        }
455        if let Some(key) = self.node_to_scope.get(&node_id)
456            && let Some(st) = self.scope_trees.get(key)
457        {
458            if let Some(&tid) = st.taffy_map.get(&node_id) {
459                if let Ok(l) = st.taffy.layout(tid) {
460                    return *l;
461                }
462                log::error!(
463                    "layout_for_node: scope taffy layout missing for {:?}",
464                    node_id
465                );
466                return fallback();
467            }
468            log::error!("layout_for_node: scope taffy_map missing for {:?}", node_id);
469            return fallback();
470        }
471        if let Some(&tid) = self.taffy_map.get(&node_id) {
472            if let Ok(l) = self.taffy.layout(tid) {
473                return *l;
474            }
475            log::error!("layout_for_node: taffy layout missing for {:?}", node_id);
476        } else {
477            log::error!("layout_for_node: taffy_map missing for {:?}", node_id);
478        }
479        fallback()
480    }
481
482    pub(crate) fn ensure_view_id(&mut self, node_id: NodeId) -> u64 {
483        if let Some(&id) = self.view_ids.get(&node_id) {
484            return id;
485        }
486        let id = self.next_view_id;
487        self.next_view_id += 1;
488        self.view_ids.insert(node_id, id);
489        id
490    }
491
492    /// Debug dump of the taffy subtree rooted at `node_id` (styles +
493    /// computed layout), for the devtools inspector. Empty string when the
494    /// node has no taffy node. Backed by taffy's `write_tree`.
495    pub fn debug_taffy_subtree(&self, node_id: NodeId) -> String {
496        let trees: Vec<(&taffy::TaffyTree<NodeContext>, taffy::NodeId)> = if let Some(key) =
497            self.node_to_scope.get(&node_id)
498            && let Some(st) = self.scope_trees.get(key)
499            && let Some(&tid) = st.taffy_map.get(&node_id)
500        {
501            vec![(&st.taffy, tid)]
502        } else if let Some(&tid) = self.taffy_map.get(&node_id) {
503            vec![(&self.taffy, tid)]
504        } else {
505            Vec::new()
506        };
507        let mut out = String::new();
508        for (tree, tid) in trees {
509            let mut buf = Vec::new();
510            if taffy::util::write_tree(&mut buf, tree, tid).is_ok() {
511                out.push_str(&String::from_utf8_lossy(&buf));
512            }
513        }
514        out
515    }
516
517    /// One-line grid summary (track counts, line positions, resolved
518    /// templates) for a grid container, for the devtools inspector.
519    /// `None` when the node is not a grid container with detailed info.
520    /// Requires taffy's `detailed_layout_info` feature (default-on).
521    pub fn debug_grid_summary(&self, node_id: NodeId) -> Option<String> {
522        let tid = if let Some(key) = self.node_to_scope.get(&node_id)
523            && let Some(st) = self.scope_trees.get(key)
524            && let Some(&tid) = st.taffy_map.get(&node_id)
525        {
526            return Self::grid_summary_for(&st.taffy, tid);
527        } else {
528            self.taffy_map.get(&node_id).copied()?
529        };
530        Self::grid_summary_for(&self.taffy, tid)
531    }
532
533    fn grid_summary_for(
534        tree: &taffy::TaffyTree<NodeContext>,
535        tid: taffy::NodeId,
536    ) -> Option<String> {
537        let taffy::tree::DetailedLayoutInfo::Grid(grid) = tree.detailed_layout_info(tid) else {
538            return None;
539        };
540        Some(format!(
541            "grid rows=({} explicit of {} tracks) cols=({} explicit of {} tracks) rows=[{}] cols=[{}] items={}",
542            grid.rows.explicit_tracks,
543            grid.rows.explicit_tracks
544                + grid.rows.negative_implicit_tracks
545                + grid.rows.positive_implicit_tracks,
546            grid.columns.explicit_tracks,
547            grid.columns.explicit_tracks
548                + grid.columns.negative_implicit_tracks
549                + grid.columns.positive_implicit_tracks,
550            grid.grid_template_rows(),
551            grid.grid_template_columns(),
552            grid.items.len(),
553        ))
554    }
555
556    pub(crate) fn locals_stamp() -> u64 {
557        let mut h = FxHasher::default();
558
559        // These affect layout measurement and/or flex direction decisions.
560        locals::density().scale.to_bits().hash(&mut h);
561        locals::ui_scale().0.to_bits().hash(&mut h);
562        locals::text_scale().0.to_bits().hash(&mut h);
563
564        let dir_u8 = match locals::text_direction() {
565            locals::TextDirection::Ltr => 0u8,
566            locals::TextDirection::Rtl => 1u8,
567        };
568        dir_u8.hash(&mut h);
569        // For font fallback generation (ex: emoji loaded dynamically)
570        repose_text::font_generation().hash(&mut h);
571
572        h.finish()
573    }
574}