Skip to main content

lgui_core/core/layout/
layout.rs

1use std::{
2    collections::{HashMap, HashSet},
3    hash::{Hash, Hasher},
4};
5
6use super::{EdgeInsets, HostTree, Size, UiId, UiRect};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9pub enum Axis {
10    Horizontal,
11    Vertical,
12}
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
15pub enum Align {
16    Start,
17    Center,
18    End,
19    Stretch,
20}
21
22#[derive(Clone, Copy, Debug, PartialEq)]
23pub enum LayoutSpec {
24    Absolute,
25    Stack {
26        axis: Axis,
27        gap: f32,
28        padding: EdgeInsets,
29        align: Align,
30    },
31    Fixed(Size),
32}
33
34impl Eq for LayoutSpec {}
35
36impl Hash for LayoutSpec {
37    fn hash<H: Hasher>(&self, state: &mut H) {
38        std::mem::discriminant(self).hash(state);
39        match self {
40            Self::Absolute => {}
41            Self::Stack {
42                axis,
43                gap,
44                padding,
45                align,
46            } => {
47                axis.hash(state);
48                gap.to_bits().hash(state);
49                padding.hash(state);
50                align.hash(state);
51            }
52            Self::Fixed(size) => size.hash(state),
53        }
54    }
55}
56
57impl Default for LayoutSpec {
58    fn default() -> Self {
59        Self::Absolute
60    }
61}
62
63pub fn apply_layout(tree: &mut HostTree, root: UiId) {
64    let Some(root_node) = tree.node(&root).cloned() else {
65        return;
66    };
67    let LayoutSpec::Stack {
68        axis,
69        gap,
70        padding,
71        align,
72    } = root_node.layout
73    else {
74        return;
75    };
76    let children = root_node.children.clone();
77    let mut cursor = match axis {
78        Axis::Horizontal => root_node.layout_rect.left + padding.left,
79        Axis::Vertical => root_node.layout_rect.top + padding.top,
80    };
81    let content = root_node.layout_rect.inset(padding);
82    for child_id in children {
83        let Some(child) = tree.node_mut(&child_id) else {
84            continue;
85        };
86        let size = Size::new(child.layout_rect.width(), child.layout_rect.height());
87        let rect = stack_child_rect(content, axis, align, cursor, size);
88        child.layout_rect = rect;
89        child.hit_rect = rect;
90        child.paint_bounds = rect;
91        cursor += match axis {
92            Axis::Horizontal => size.width + gap,
93            Axis::Vertical => size.height + gap,
94        };
95    }
96}
97
98#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
99pub struct LayoutCommitMetrics {
100    pub visited_nodes: usize,
101    pub laid_out_nodes: usize,
102    pub reused_nodes: usize,
103}
104
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106pub enum LayoutInvalidation {
107    None,
108    SelfOnly,
109    Subtree,
110    BubbleToLayoutBoundary,
111}
112
113#[derive(Clone, Debug, PartialEq, Eq)]
114struct LayoutInput {
115    parent: Option<UiId>,
116    children: Vec<UiId>,
117    spec: LayoutSpec,
118    rect: UiRect,
119    hit_rect: UiRect,
120    paint_bounds: UiRect,
121}
122
123#[derive(Clone)]
124struct LayoutOutput {
125    layout_rect: UiRect,
126    hit_rect: UiRect,
127    paint_bounds: UiRect,
128}
129
130#[derive(Default)]
131pub struct LayoutRuntime {
132    inputs: HashMap<UiId, LayoutInput>,
133    outputs: HashMap<UiId, LayoutOutput>,
134    metrics: LayoutCommitMetrics,
135}
136
137impl LayoutRuntime {
138    pub fn new() -> Self {
139        Self::default()
140    }
141
142    /// Resolves logical layout while retaining committed results per node. Paint-only changes do
143    /// not execute layout; geometric changes execute only their nearest layout boundary subtree.
144    pub fn update(&mut self, tree: &mut HostTree) -> LayoutCommitMetrics {
145        let changes = tree.take_projection_changes();
146        self.update_projection(tree, changes).0
147    }
148
149    pub(crate) fn update_projection(
150        &mut self,
151        tree: &mut HostTree,
152        mut changes: super::ProjectionChanges,
153    ) -> (LayoutCommitMetrics, super::ProjectionChanges) {
154        let first_layout = self.inputs.is_empty();
155        let candidate_ids = if first_layout {
156            tree.nodes()
157                .iter()
158                .map(|node| node.id.clone())
159                .collect::<HashSet<_>>()
160        } else {
161            changes.changed.clone()
162        };
163        let next_inputs = candidate_ids
164            .iter()
165            .filter_map(|id| {
166                let node = tree.node(id)?;
167                Some((
168                    id.clone(),
169                    LayoutInput {
170                        parent: node.parent.clone(),
171                        children: node.children.clone(),
172                        spec: node.layout,
173                        rect: node.layout_rect,
174                        hit_rect: node.hit_rect,
175                        paint_bounds: node.paint_bounds,
176                    },
177                ))
178            })
179            .collect::<HashMap<_, _>>();
180        let visited_nodes = next_inputs.len();
181        let mut dirty = HashSet::new();
182
183        if first_layout {
184            dirty.extend(
185                tree.nodes()
186                    .iter()
187                    .filter(|node| node.parent.is_none())
188                    .map(|node| node.id.clone()),
189            );
190        } else {
191            for (id, input) in &next_inputs {
192                match self.inputs.get(id) {
193                    None => {
194                        dirty.insert(layout_boundary(tree, id));
195                    }
196                    Some(previous) if previous != input => {
197                        dirty.insert(layout_boundary(tree, id));
198                        if previous.parent != input.parent || previous.children != input.children {
199                            if let Some(parent) = previous
200                                .parent
201                                .as_ref()
202                                .filter(|parent| next_inputs.contains_key(*parent))
203                            {
204                                dirty.insert(layout_boundary(tree, parent));
205                            }
206                        }
207                    }
208                    Some(_) => {}
209                }
210            }
211            for removed in &changes.removed {
212                if let Some(parent) = self.inputs.get(removed).and_then(|input| {
213                    input
214                        .parent
215                        .as_ref()
216                        .filter(|parent| tree.node(parent).is_some())
217                }) {
218                    dirty.insert(layout_boundary(tree, parent));
219                }
220            }
221        }
222
223        for id in &candidate_ids {
224            if let (Some(node), Some(output)) = (tree.node_mut(id), self.outputs.get(id)) {
225                node.layout_rect = output.layout_rect;
226                node.hit_rect = output.hit_rect;
227                node.paint_bounds = output.paint_bounds;
228            }
229        }
230
231        let dirty_roots = minimal_dirty_roots(tree, dirty);
232        let laid_out_ids = dirty_roots
233            .iter()
234            .flat_map(|root| subtree_ids(tree, root))
235            .collect::<HashSet<_>>();
236        for id in &laid_out_ids {
237            let Some(input) = next_inputs.get(id) else {
238                continue;
239            };
240            if let Some(node) = tree.node_mut(id) {
241                node.layout_rect = input.rect;
242                node.hit_rect = input.hit_rect;
243                node.paint_bounds = input.paint_bounds;
244            }
245        }
246        for root in dirty_roots {
247            layout_subtree(tree, root);
248        }
249        for removed in &changes.removed {
250            self.inputs.remove(&removed);
251            self.outputs.remove(&removed);
252        }
253        self.inputs.extend(next_inputs);
254        for id in &laid_out_ids {
255            let Some(node) = tree.node(id) else {
256                continue;
257            };
258            self.outputs.insert(
259                id.clone(),
260                LayoutOutput {
261                    layout_rect: node.layout_rect,
262                    hit_rect: node.hit_rect,
263                    paint_bounds: node.paint_bounds,
264                },
265            );
266        }
267        let laid_out_nodes = laid_out_ids.len();
268        changes.changed.extend(laid_out_ids);
269        self.metrics = LayoutCommitMetrics {
270            visited_nodes,
271            laid_out_nodes,
272            reused_nodes: visited_nodes.saturating_sub(laid_out_nodes),
273        };
274        (self.metrics, changes)
275    }
276
277    pub fn metrics(&self) -> LayoutCommitMetrics {
278        self.metrics
279    }
280
281    pub fn clear(&mut self) {
282        *self = Self::default();
283    }
284}
285
286fn layout_boundary(tree: &HostTree, id: &UiId) -> UiId {
287    let Some(node) = tree.node(id) else {
288        return id.clone();
289    };
290    let Some(parent_id) = node.parent.as_ref() else {
291        return id.clone();
292    };
293    if tree
294        .node(parent_id)
295        .is_some_and(|parent| matches!(parent.layout, LayoutSpec::Stack { .. }))
296    {
297        parent_id.clone()
298    } else {
299        id.clone()
300    }
301}
302
303fn minimal_dirty_roots(tree: &HostTree, dirty: HashSet<UiId>) -> Vec<UiId> {
304    dirty
305        .iter()
306        .filter(|candidate| {
307            let mut parent = tree.node(candidate).and_then(|node| node.parent.as_ref());
308            while let Some(parent_id) = parent {
309                if dirty.contains(parent_id) {
310                    return false;
311                }
312                parent = tree.node(parent_id).and_then(|node| node.parent.as_ref());
313            }
314            true
315        })
316        .cloned()
317        .collect()
318}
319
320fn subtree_ids(tree: &HostTree, root: &UiId) -> Vec<UiId> {
321    let Some(node) = tree.node(root) else {
322        return Vec::new();
323    };
324    let mut ids = vec![root.clone()];
325    for child in &node.children {
326        ids.extend(subtree_ids(tree, child));
327    }
328    ids
329}
330
331pub fn apply_layout_tree(tree: &mut HostTree) {
332    let roots = tree
333        .nodes()
334        .iter()
335        .filter(|node| node.parent.is_none())
336        .map(|node| node.id.clone())
337        .collect::<Vec<_>>();
338    for root in roots {
339        layout_subtree(tree, root);
340    }
341}
342
343fn layout_subtree(tree: &mut HostTree, root: UiId) {
344    let Some(root_node) = tree.node(&root).cloned() else {
345        return;
346    };
347    if let LayoutSpec::Stack {
348        axis,
349        gap,
350        padding,
351        align,
352    } = root_node.layout
353    {
354        let mut cursor = match axis {
355            Axis::Horizontal => root_node.layout_rect.left + padding.left,
356            Axis::Vertical => root_node.layout_rect.top + padding.top,
357        };
358        let content = root_node.layout_rect.inset(padding);
359        for child_id in &root_node.children {
360            let Some(child) = tree.node(child_id).cloned() else {
361                continue;
362            };
363            let size = match child.layout {
364                LayoutSpec::Fixed(size) => size,
365                _ => Size::new(child.layout_rect.width(), child.layout_rect.height()),
366            };
367            let rect = stack_child_rect(content, axis, align, cursor, size);
368            translate_subtree(
369                tree,
370                child_id,
371                rect.left - child.layout_rect.left,
372                rect.top - child.layout_rect.top,
373            );
374            cursor += match axis {
375                Axis::Horizontal => size.width + gap,
376                Axis::Vertical => size.height + gap,
377            };
378        }
379    }
380    for child in root_node.children {
381        layout_subtree(tree, child);
382    }
383}
384
385fn translate_subtree(tree: &mut HostTree, root: &UiId, x: f32, y: f32) {
386    if x == 0.0 && y == 0.0 {
387        return;
388    }
389    let Some(node) = tree.node(root).cloned() else {
390        return;
391    };
392    let children = node.children.clone();
393    if let Some(current) = tree.node_mut(root) {
394        *current = node.translate(x, y);
395    }
396    for child in children {
397        translate_subtree(tree, &child, x, y);
398    }
399}
400
401fn stack_child_rect(content: UiRect, axis: Axis, align: Align, cursor: f32, size: Size) -> UiRect {
402    match axis {
403        Axis::Horizontal => {
404            let top = match align {
405                Align::Start => content.top,
406                Align::Center => content.top + (content.height() - size.height) / 2.0,
407                Align::End => content.bottom - size.height,
408                Align::Stretch => content.top,
409            };
410            let bottom = if align == Align::Stretch {
411                content.bottom
412            } else {
413                top + size.height
414            };
415            UiRect::new(cursor, top, cursor + size.width, bottom)
416        }
417        Axis::Vertical => {
418            let left = match align {
419                Align::Start => content.left,
420                Align::Center => content.left + (content.width() - size.width) / 2.0,
421                Align::End => content.right - size.width,
422                Align::Stretch => content.left,
423            };
424            let right = if align == Align::Stretch {
425                content.right
426            } else {
427                left + size.width
428            };
429            UiRect::new(left, cursor, right, cursor + size.height)
430        }
431    }
432}
433
434#[cfg(test)]
435#[path = "layout_test.rs"]
436mod tests;