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        // 1. Update tree
50        let density_scale = locals::effective_density_scale();
51        let max_w_dp = size_px.0 as f32 / density_scale;
52        let max_h_dp = size_px.1 as f32 / density_scale;
53        self.tree
54            .set_subcompose_scope(repose_core::SubcomposeScope::new(
55                0.0, max_w_dp, 0.0, max_h_dp,
56            ));
57        let root_node_id = self.tree.update(root);
58        self.stats.tree = self.tree.stats.clone();
59
60        // 1a. Build scope maps from TreeNode.scope_key (set by scope! macro)
61        self.build_scope_maps();
62
63        // 2. Determine layout need
64        let size_changed = self.last_size_px != Some(size_px);
65        // 2a. Publish the current window size class as a default local so that
66        //     `window_size_class()` returns an up-to-date value even outside
67        //     a `with_window_size_class { ... }` scope. We only touch the
68        //     default when it actually changes to keep the lock uncontended.
69        // Reuse the same effective density_scale computed above (do not shadow)
70        let class = locals::calculate_window_size_class(size_px.0, size_px.1, density_scale);
71        if class != locals::window_size_class() {
72            locals::set_window_size_class_default(class);
73        }
74        let inv_density = if density_scale > 0.0 {
75            1.0 / density_scale
76        } else {
77            1.0
78        };
79        locals::set_window_container_size(
80            size_px.0 as f32 * inv_density,
81            size_px.1 as f32 * inv_density,
82        );
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        // Helpers
126        let px = |dp_val: f32| dp_to_px(dp_val);
127        let font_px = |dp_font: f32| dp_to_px(dp_font) * locals::text_scale().0;
128
129        // 3. Sync Taffy
130        // 3a. Sync scope-internal TaffyTrees first
131        self.sync_scope_trees(&font_px);
132        // 3b. Sync root TaffyTree (non-scope nodes + scope root markers)
133        self.sync_taffy_tree(root_node_id, &font_px);
134
135        // 4. Compute Layout
136        let taffy_root = self.taffy_map.get(&root_node_id).copied();
137        if let Some(taffy_root) = taffy_root {
138            if need_layout {
139                if let Ok(mut style) = self.taffy.style(taffy_root).cloned() {
140                    style.size.width = length(size_px.0 as f32);
141                    style.size.height = length(size_px.1 as f32);
142                    let _ = self.taffy.set_style(taffy_root, style);
143                }
144
145                let available = taffy::geometry::Size {
146                    width: AvailableSpace::Definite(size_px.0 as f32),
147                    height: AvailableSpace::Definite(size_px.1 as f32),
148                };
149
150                Self::run_measure_pass(
151                    &mut self.taffy,
152                    taffy_root,
153                    available,
154                    &self.tree,
155                    &mut self.text_cache,
156                    &self.reverse_map,
157                    &self.scope_root_map,
158                    &self.node_to_scope,
159                    &mut self.scope_trees,
160                    &font_px,
161                    &px,
162                );
163
164                // 4a. Store Taffy-computed sizes for non-scope + scope-root nodes
165                for (&node_id, &taffy_id) in &self.taffy_map {
166                    if let Ok(layout) = self.taffy.layout(taffy_id) {
167                        let dp_w = layout.size.width / density_scale;
168                        let dp_h = layout.size.height / density_scale;
169                        let rect = repose_core::Rect {
170                            x: 0.0,
171                            y: 0.0,
172                            w: dp_w,
173                            h: dp_h,
174                        };
175                        self.tree
176                            .set_layout(node_id, rect, rect, LayoutConstraints::default());
177                    }
178                }
179
180                self.last_locals_stamp = Some(locals_stamp);
181
182                self.layout_valid = true;
183                self.last_size_px = Some(size_px);
184                self.stats.layout_misses += 1;
185            } else {
186                self.stats.layout_hits += 1;
187            }
188        }
189        self.stats.layout_time_ms = (web_time::Instant::now() - start).as_secs_f32() * 1000.0;
190
191        // 4.5. Advance scroll physics (pre-paint, so paint only reads offset)
192        self.walk_tick(root_node_id);
193
194        // 5. Paint
195        let t_paint = web_time::Instant::now();
196        self.focus_interaction_sources.clear();
197        let (scene, hits, sems) = self.paint(
198            root_node_id,
199            textfield_states,
200            interactions,
201            focused,
202            &font_px,
203        );
204        self.stats.paint_time_ms = (web_time::Instant::now() - t_paint).as_secs_f32() * 1000.0;
205
206        // Fire focus change callbacks.
207        if self.prev_focused != focused {
208            if let Some(old_id) = self.prev_focused {
209                if let Some(cb) = self.focus_callbacks.get(&old_id) {
210                    (cb)(false);
211                }
212                if let Some(src) = self.focus_interaction_sources.get(&old_id) {
213                    src.to_mutable().emit(Interaction::Unfocus);
214                }
215            }
216            if let Some(new_id) = focused {
217                if let Some(cb) = self.focus_callbacks.get(&new_id) {
218                    (cb)(true);
219                }
220                if let Some(src) = self.focus_interaction_sources.get(&new_id) {
221                    src.to_mutable().emit(Interaction::Focus);
222                }
223            }
224            self.prev_focused = focused;
225        }
226
227        // Clean up callbacks for removed nodes
228        for &node_id in &self.tree.removed_ids {
229            if let Some(&vid) = self.view_ids.get(&node_id) {
230                self.focus_callbacks.remove(&vid);
231                self.focus_interaction_sources.remove(&vid);
232            }
233        }
234
235        self.tree.clear_dirty();
236        (scene, hits, sems)
237    }
238
239    pub fn intrinsic_size(&mut self, view: &View, mode: IntrinsicSizeMode) -> (f32, f32) {
240        let px_closure = |dp_val: f32| dp_to_px(dp_val);
241        let font_px_closure = |dp_font: f32| dp_to_px(dp_font) * locals::text_scale().0;
242
243        let mut temp_taffy = taffy::TaffyTree::new();
244        let root_tid = self.build_taffy_subtree(view, &mut temp_taffy, &font_px_closure);
245
246        let avail = match mode {
247            IntrinsicSizeMode::MinContent => taffy::geometry::Size {
248                width: taffy::style::AvailableSpace::MinContent,
249                height: taffy::style::AvailableSpace::MinContent,
250            },
251            IntrinsicSizeMode::MaxContent => taffy::geometry::Size {
252                width: taffy::style::AvailableSpace::MaxContent,
253                height: taffy::style::AvailableSpace::MaxContent,
254            },
255        };
256
257        let mut text_cache: FxHashMap<NodeId, TextLayout> = FxHashMap::default();
258        let reverse_map: FxHashMap<taffy::NodeId, NodeId> = FxHashMap::default();
259
260        Self::run_measure_pass(
261            &mut temp_taffy,
262            root_tid,
263            avail,
264            &self.tree,
265            &mut text_cache,
266            &reverse_map,
267            &self.scope_root_map,
268            &self.node_to_scope,
269            &mut self.scope_trees,
270            &font_px_closure,
271            &px_closure,
272        );
273
274        let layout = temp_taffy.layout(root_tid).ok();
275        match layout {
276            Some(l) => (l.size.width, l.size.height),
277            None => (0.0, 0.0),
278        }
279    }
280
281    pub fn new() -> Self {
282        Self {
283            tree: ViewTree::new(),
284            taffy: TaffyTree::new(),
285            taffy_map: FxHashMap::default(),
286            reverse_map: FxHashMap::default(),
287            scope_trees: HashMap::new(),
288            scope_root_map: FxHashMap::default(),
289            node_to_scope: FxHashMap::default(),
290            text_cache: FxHashMap::default(),
291            last_size_px: None,
292            layout_valid: false,
293            paint_cache: FxHashMap::default(),
294            stats: LayoutStats::default(),
295            last_locals_stamp: None,
296            view_ids: FxHashMap::default(),
297            next_view_id: 1,
298            layer_id_counter: 0,
299            prev_focused: None,
300            focus_callbacks: FxHashMap::default(),
301            focus_interaction_sources: FxHashMap::default(),
302            prev_observed_rects: FxHashMap::default(),
303            focus_group_stack: Vec::new(),
304        }
305    }
306
307    pub(crate) fn layout_for_node(&self, node_id: NodeId) -> taffy::prelude::Layout {
308        // Scope root nodes: use the root tree layout (has correct position + size after flexbox resolve).
309        // Their children use the scope tree layout (positions relative to scope root).
310        if self.scope_root_map.contains_key(&node_id) {
311            if let Some(&tid) = self.taffy_map.get(&node_id) {
312                return *self.taffy.layout(tid).unwrap();
313            }
314            // Nested scope root: the enclosing scope positions it via a leaf
315            // marker; inherit that layout so the subtree paints at the right
316            // spot instead of the nested scope's origin.
317            if let Some(parent_id) = self.tree.get(node_id).and_then(|n| n.parent)
318                && let Some(outer_key) = self.node_to_scope.get(&parent_id)
319                && let Some(st) = self.scope_trees.get(outer_key)
320                && let Some(&tid) = st.taffy_map.get(&node_id)
321            {
322                return *st.taffy.layout(tid).unwrap();
323            }
324            if let Some(key) = self.node_to_scope.get(&node_id)
325                && let Some(st) = self.scope_trees.get(key)
326                && let Some(&tid) = st.taffy_map.get(&node_id)
327            {
328                return *st.taffy.layout(tid).unwrap();
329            }
330        }
331        if let Some(key) = self.node_to_scope.get(&node_id)
332            && let Some(st) = self.scope_trees.get(key)
333        {
334            let tid = st.taffy_map[&node_id];
335            return *st.taffy.layout(tid).unwrap();
336        }
337        let tid = self.taffy_map[&node_id];
338        *self.taffy.layout(tid).unwrap()
339    }
340
341    pub(crate) fn ensure_view_id(&mut self, node_id: NodeId) -> u64 {
342        if let Some(&id) = self.view_ids.get(&node_id) {
343            return id;
344        }
345        let id = self.next_view_id;
346        self.next_view_id += 1;
347        self.view_ids.insert(node_id, id);
348        id
349    }
350
351    pub(crate) fn locals_stamp() -> u64 {
352        let mut h = FxHasher::default();
353
354        // These affect layout measurement and/or flex direction decisions.
355        locals::density().scale.to_bits().hash(&mut h);
356        locals::ui_scale().0.to_bits().hash(&mut h);
357        locals::text_scale().0.to_bits().hash(&mut h);
358
359        let dir_u8 = match locals::text_direction() {
360            locals::TextDirection::Ltr => 0u8,
361            locals::TextDirection::Rtl => 1u8,
362        };
363        dir_u8.hash(&mut h);
364
365        h.finish()
366    }
367}