torin/
torin.rs

1use std::{
2    collections::HashMap,
3    mem,
4};
5
6pub use euclid::Rect;
7use itertools::Itertools;
8use rustc_hash::FxHashMap;
9
10use crate::{
11    custom_measurer::LayoutMeasurer,
12    dom_adapter::{
13        LayoutNode,
14        NodeKey,
15        TreeAdapter,
16    },
17    geometry::Area,
18    measure::{
19        MeasureContext,
20        Phase,
21    },
22    prelude::{
23        AreaModel,
24        Gaps,
25        Length,
26    },
27};
28
29pub struct LayoutMetadata {
30    pub root_area: Area,
31}
32
33/// Contains the best Root node candidate from where to start measuring
34#[derive(PartialEq, Debug, Clone)]
35pub enum RootNodeCandidate<Key: NodeKey> {
36    /// A valid Node ID
37    Valid(Key),
38
39    /// None
40    None,
41}
42
43impl<Key: NodeKey> RootNodeCandidate<Key> {
44    #[must_use]
45    pub fn take(&mut self) -> Self {
46        mem::replace(self, Self::None)
47    }
48
49    /// Propose a new root candidate
50    pub fn propose_new_candidate(
51        &mut self,
52        proposed_candidate: &Key,
53        dom_adapter: &mut impl TreeAdapter<Key>,
54        dirty: &mut FxHashMap<Key, DirtyReason>,
55    ) {
56        if let RootNodeCandidate::Valid(current_candidate) = self {
57            if current_candidate != proposed_candidate {
58                let mut continue_waking = true;
59                let closest_parent = dom_adapter.closest_common_parent(
60                    proposed_candidate,
61                    current_candidate,
62                    |id| {
63                        if !continue_waking {
64                            return;
65                        }
66                        let reason = dirty.get(&id);
67                        match reason {
68                            Some(DirtyReason::InnerLayout) => {
69                                // Replace [DirtyReason::InnerLayout] with [DirtyReason::None]
70                                // for all the nodes between the proposed candidate and the current candidate
71                                dirty.insert(id, DirtyReason::None);
72                            }
73                            Some(DirtyReason::None | DirtyReason::Reorder)
74                                if id != *proposed_candidate =>
75                            {
76                                // No need to continue checking if we encountered an ascendant
77                                // that is dirty but not with [DirtyReason::InnerLayout]
78                                continue_waking = false;
79                            }
80                            _ => {}
81                        }
82                    },
83                );
84
85                if let Some(closest_parent) = closest_parent {
86                    *self = RootNodeCandidate::Valid(closest_parent);
87                }
88            }
89        } else {
90            *self = RootNodeCandidate::Valid(*proposed_candidate);
91        }
92    }
93}
94
95#[derive(Clone, Copy, Debug, PartialEq)]
96pub enum DirtyReason {
97    None,
98    /// Node was moved from one position to another in its parent' children list.
99    Reorder,
100    /// The inner layout of the Node changed, e.g the offsets.
101    InnerLayout,
102}
103
104pub struct Torin<Key: NodeKey> {
105    /// Layout results of the registered Nodes
106    pub results: FxHashMap<Key, LayoutNode>,
107
108    /// Invalid registered nodes since previous layout measurement
109    pub dirty: FxHashMap<Key, DirtyReason>,
110
111    /// Best Root node candidate from where to start measuring
112    pub root_node_candidate: RootNodeCandidate<Key>,
113}
114
115impl<Key: NodeKey> Default for Torin<Key> {
116    fn default() -> Self {
117        Self::new()
118    }
119}
120
121impl<Key: NodeKey> Torin<Key> {
122    /// Create a new Layout
123    pub fn new() -> Self {
124        Self {
125            results: HashMap::default(),
126            dirty: FxHashMap::default(),
127            root_node_candidate: RootNodeCandidate::None,
128        }
129    }
130
131    pub fn size(&self) -> usize {
132        self.results.len()
133    }
134
135    /// Clear the dirty nodes buffer
136    pub fn clear_dirty(&mut self) {
137        self.dirty.clear();
138    }
139
140    /// Reset the layout
141    pub fn reset(&mut self) {
142        self.root_node_candidate = RootNodeCandidate::None;
143        self.results.clear();
144        self.dirty.clear();
145    }
146
147    /// Read the HashSet of dirty nodes
148    pub fn get_dirty_nodes(&self) -> &FxHashMap<Key, DirtyReason> {
149        &self.dirty
150    }
151
152    /// Remove a Node's result and data
153    pub fn raw_remove(&mut self, node_id: Key) {
154        self.results.remove(&node_id);
155        self.dirty.remove(&node_id);
156        if let RootNodeCandidate::Valid(id) = self.root_node_candidate
157            && id == node_id
158        {
159            self.root_node_candidate = RootNodeCandidate::None;
160        }
161    }
162
163    /// Remove a Node from the layout
164    /// # Panics
165    /// Might panic if the parent is not found.
166    pub fn remove(
167        &mut self,
168        node_id: Key,
169        dom_adapter: &mut impl TreeAdapter<Key>,
170        invalidate_parent: bool,
171    ) {
172        // Remove itself
173        self.raw_remove(node_id);
174
175        // Mark as dirty the Node's parent
176        if invalidate_parent && let Some(parent) = dom_adapter.parent_of(&node_id) {
177            self.invalidate(parent);
178        }
179
180        // Remove all it's children
181        for child_id in dom_adapter.children_of(&node_id) {
182            self.remove(child_id, dom_adapter, false);
183        }
184    }
185
186    /// Safely mark as dirty a Node, with no reason.
187    pub fn safe_invalidate(&mut self, node_id: Key) {
188        self.dirty.insert(node_id, DirtyReason::None);
189    }
190
191    /// Mark as dirty a Node, with no reason.
192    pub fn invalidate(&mut self, node_id: Key) {
193        self.dirty.insert(node_id, DirtyReason::None);
194    }
195
196    /// Mark as dirty a Node, with a reason.
197    pub fn invalidate_with_reason(&mut self, node_id: Key, reason: DirtyReason) {
198        self.dirty.entry(node_id).or_insert(reason);
199    }
200
201    // Mark as dirty the given Node and all the nodes that depend on it
202    pub fn check_dirty_dependants(
203        &mut self,
204        node_id: Key,
205        reason: DirtyReason,
206        dom_adapter: &mut impl TreeAdapter<Key>,
207        ignore: bool,
208    ) {
209        if self.dirty.contains_key(&node_id) && ignore {
210            return;
211        }
212
213        // Mark this node as dirty
214        self.invalidate_with_reason(node_id, reason);
215
216        self.root_node_candidate
217            .propose_new_candidate(&node_id, dom_adapter, &mut self.dirty);
218
219        // Mark this Node's parent if it is affected
220        let parent_id = dom_adapter.parent_of(&node_id);
221
222        if let Some(parent_id) = parent_id {
223            if reason == DirtyReason::InnerLayout {
224                self.root_node_candidate.propose_new_candidate(
225                    &parent_id,
226                    dom_adapter,
227                    &mut self.dirty,
228                );
229                return;
230            }
231
232            let parent = dom_adapter.get_node(&parent_id);
233
234            if let Some(parent) = parent {
235                if parent.does_depend_on_inner() {
236                    // Mark parent if it depends on it's inner children
237                    self.check_dirty_dependants(parent_id, DirtyReason::None, dom_adapter, true);
238                } else {
239                    let parent_children = dom_adapter.children_of(&parent_id);
240                    let multiple_children = parent_children.len() > 1;
241
242                    let mut found_node = match reason {
243                        DirtyReason::None | DirtyReason::InnerLayout => false,
244                        // Invalidate all siblings if the node was reordered
245                        DirtyReason::Reorder => true,
246                    };
247                    for child_id in parent_children {
248                        if found_node {
249                            self.safe_invalidate(child_id);
250                        }
251                        if child_id == node_id {
252                            found_node = true;
253                        }
254                    }
255
256                    // Try using the node's parent as root candidate if it has multiple children
257                    if multiple_children || parent.do_inner_depend_on_parent() {
258                        self.root_node_candidate.propose_new_candidate(
259                            &parent_id,
260                            dom_adapter,
261                            &mut self.dirty,
262                        );
263                    }
264                }
265            }
266        }
267    }
268
269    /// Get the Root Node candidate
270    pub fn get_root_candidate(&self) -> RootNodeCandidate<Key> {
271        self.root_node_candidate.clone()
272    }
273
274    /// Find the best root Node from where to start measuring
275    pub fn find_best_root(&mut self, dom_adapter: &mut impl TreeAdapter<Key>) {
276        if self.results.is_empty() {
277            return;
278        }
279        for (id, reason) in self
280            .dirty
281            .clone()
282            .into_iter()
283            .sorted_by_key(|e| dom_adapter.height(&e.0))
284        {
285            self.check_dirty_dependants(id, reason, dom_adapter, false);
286        }
287    }
288
289    /// Measure dirty Nodes
290    /// # Panics
291    /// Might panic if the final root node is not found.
292    pub fn measure(
293        &mut self,
294        suggested_root_id: Key,
295        root_area: Area,
296        measurer: &mut Option<impl LayoutMeasurer<Key>>,
297        dom_adapter: &mut impl TreeAdapter<Key>,
298    ) {
299        // If there are previosuly cached results
300        // But no dirty nodes, we can simply skip the measurement
301        // as this means no changes has been made to the layout
302        if self.dirty.is_empty() && !self.results.is_empty() {
303            return;
304        }
305
306        // Try the Root candidate otherwise use the provided Root
307        let root_id = if let RootNodeCandidate::Valid(id) = self.root_node_candidate.take() {
308            id
309        } else {
310            suggested_root_id
311        };
312        let root_parent_id = dom_adapter.parent_of(&root_id);
313        let layout_node = root_parent_id
314            .and_then(|root_parent_id| self.get(&root_parent_id).cloned())
315            .unwrap_or(LayoutNode {
316                area: root_area,
317                inner_area: root_area,
318                margin: Gaps::default(),
319                offset_x: Length::default(),
320                offset_y: Length::default(),
321                data: None,
322            });
323        let root = dom_adapter.get_node(&root_id).unwrap();
324
325        #[cfg(debug_assertions)]
326        {
327            let root_height = dom_adapter.height(&root_id).unwrap();
328            tracing::info!(
329                "Processing {} dirty nodes and {} cached nodes from a height of {}",
330                self.dirty.len(),
331                self.results.len(),
332                root_height
333            );
334        }
335
336        let layout_metadata = LayoutMetadata { root_area };
337
338        let mut available_area = layout_node.inner_area;
339        if let Some(root_parent_id) = root_parent_id {
340            let root_parent = dom_adapter.get_node(&root_parent_id).unwrap();
341            available_area.move_with_offsets(&root_parent.offset_x, &root_parent.offset_y);
342        }
343
344        let mut measure_context = MeasureContext {
345            layout: self,
346            layout_metadata,
347            dom_adapter,
348            measurer,
349        };
350
351        let (root_revalidated, mut root_layout_node) = measure_context.measure_node(
352            root_id,
353            &root,
354            &layout_node.inner_area,
355            &available_area,
356            true,
357            false,
358            Phase::Final,
359        );
360
361        // Cache the root Node results if it was modified
362        if root_revalidated {
363            // Adjust the size of the area if needed
364            root_layout_node.area.adjust_size(&root);
365
366            self.cache_node(root_id, root_layout_node);
367        }
368
369        self.dirty.clear();
370        self.root_node_candidate = RootNodeCandidate::None;
371    }
372
373    /// Get a reference to [LayoutNode] of a Node
374    pub fn get(&self, node_id: &Key) -> Option<&LayoutNode> {
375        self.results.get(node_id)
376    }
377
378    /// Get a mutable reference to [LayoutNode] of a Node
379    pub fn get_mut(&mut self, node_id: &Key) -> Option<&mut LayoutNode> {
380        self.results.get_mut(node_id)
381    }
382
383    /// Cache a Node's [LayoutNode]
384    pub fn cache_node(&mut self, node_id: Key, layout_node: LayoutNode) {
385        self.results.insert(node_id, layout_node);
386    }
387}