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 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;