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