Skip to main content

telar_layout_core/
engine.rs

1use rustc_hash::{FxHashMap, FxHashSet};
2use taffy::{TaffyTree, TraversePartialTree};
3
4use crate::direction::Direction;
5use crate::error::LayoutError;
6use crate::style::{AvailableSpace, LayoutStyle};
7
8pub type NodeId = taffy::NodeId;
9
10/// Per-node measure callback: given the available main-axis width, returns the
11/// node's intrinsic (width, height). Used for text nodes whose height depends on
12/// how many lines the content wraps into at the resolved width.
13pub type MeasureFn = Box<dyn FnMut(f32) -> (f32, f32)>;
14
15pub struct LayoutEngine {
16    tree: TaffyTree<MeasureFn>,
17    direction: Direction,
18    /// Every node's single current intent — a logical edge or out-of-band mutator state — that every path pushing a style to taffy resolves from; a node needing neither pays nothing (see [`LogicalStyle::needs_tracking`]).
19    styles: FxHashMap<NodeId, LayoutStyle>,
20    /// Plain direction-following rows, cheaper than a `styles` entry until one acquires other tracked state.
21    directional_rows: FxHashSet<NodeId>,
22    /// Every node this engine currently owns. Kept because taffy has no total way to ask: `style()` indexes
23    /// its slot map and panics on a freed key rather than answering, so there is nothing to guard with. See
24    /// [`alive`](Self::alive) for why anything asks at all.
25    live: FxHashSet<NodeId>,
26}
27
28impl LayoutEngine {
29    pub fn new() -> Self {
30        Self {
31            tree: TaffyTree::new(),
32            direction: Direction::default(),
33            styles: FxHashMap::default(),
34            directional_rows: FxHashSet::default(),
35            live: FxHashSet::default(),
36        }
37    }
38
39    /// The direction logical edges currently resolve against.
40    pub fn direction(&self) -> Direction {
41        self.direction
42    }
43
44    /// Re-resolves every direction-dependent node against `direction`, returning whether anything changed.
45    /// The caller still has to mark the tree dirty and recompute — this only rewrites styles.
46    ///
47    /// This is what lets one build serve both directions: rather than rebuilding the widget tree, each node
48    /// that was authored logically is resolved again from the intent recorded when it was created.
49    pub fn set_direction(&mut self, direction: Direction) -> bool {
50        if self.direction == direction {
51            return false;
52        }
53        self.direction = direction;
54        let rows = std::mem::take(&mut self.directional_rows);
55        for &node in &rows {
56            if let Some(current) = self.style_of(node) {
57                let mut style = current.clone();
58                style.flex_direction = if direction.is_rtl() {
59                    taffy::FlexDirection::RowReverse
60                } else {
61                    taffy::FlexDirection::Row
62                };
63                let _ = self.tree.set_style(node, style);
64            }
65        }
66        self.directional_rows = rows;
67        let styles = std::mem::take(&mut self.styles);
68        for (&node, style) in &styles {
69            self.push_style(node, style);
70        }
71        self.styles = styles;
72        true
73    }
74
75    /// Files `style` under `styles` or `directional_rows` depending on what it needs tracked for later.
76    fn track(&mut self, node: NodeId, style: LayoutStyle) {
77        self.live.insert(node);
78        if style.logical.needs_tracking() {
79            self.directional_rows.remove(&node);
80            self.styles.insert(node, style);
81            return;
82        }
83        self.styles.remove(&node);
84        if style.logical.row_follows_direction {
85            self.directional_rows.insert(node);
86        } else {
87            self.directional_rows.remove(&node);
88        }
89    }
90
91    /// The node's tracked style, or one reconstructed from its live taffy style if it never needed tracking.
92    fn current_style(&self, node: NodeId) -> LayoutStyle {
93        if let Some(style) = self.styles.get(&node) {
94            return style.clone();
95        }
96        LayoutStyle {
97            inner: self.tree.style(node).cloned().unwrap_or_default(),
98            logical: crate::style::LogicalStyle {
99                row_follows_direction: self.directional_rows.contains(&node),
100                ..Default::default()
101            },
102        }
103    }
104
105    /// Resolves `style` and pushes it to taffy, placing the leading margin — `resolve` cannot, since that needs the parent's axis to know which physical edge is "leading".
106    fn push_style(&mut self, node: NodeId, style: &LayoutStyle) {
107        let mut resolved = style.resolve(self.direction);
108        if let Some((is_row, px)) = style.logical.leading_margin {
109            let leading_right = is_row && self.leads_from_right(node);
110            let m = taffy::LengthPercentageAuto::length(px);
111            if is_row {
112                if leading_right {
113                    resolved.margin.right = m;
114                } else {
115                    resolved.margin.left = m;
116                }
117            } else {
118                resolved.margin.top = m;
119            }
120        }
121        let _ = self.tree.set_style(node, resolved);
122    }
123
124    /// Shared plumbing for every out-of-band mutator: mutate one field of the tracked style, push, re-track.
125    fn mutate_style(&mut self, node: NodeId, f: impl FnOnce(&mut LayoutStyle)) {
126        if !self.live.contains(&node) {
127            return;
128        }
129        let mut style = self.current_style(node);
130        f(&mut style);
131        self.push_style(node, &style);
132        self.track(node, style);
133    }
134
135    fn forget(&mut self, node: NodeId) {
136        self.live.remove(&node);
137        self.styles.remove(&node);
138        self.directional_rows.remove(&node);
139    }
140
141    pub fn new_leaf(&mut self, style: LayoutStyle) -> Result<NodeId, LayoutError> {
142        let node = self.tree.new_leaf(style.resolve(self.direction))?;
143        self.track(node, style);
144        Ok(node)
145    }
146
147    pub fn new_measured_leaf(
148        &mut self,
149        style: LayoutStyle,
150        measure: MeasureFn,
151    ) -> Result<NodeId, LayoutError> {
152        let node = self
153            .tree
154            .new_leaf_with_context(style.resolve(self.direction), measure)?;
155        self.track(node, style);
156        Ok(node)
157    }
158
159    pub fn new_container(
160        &mut self,
161        style: LayoutStyle,
162        children: &[NodeId],
163    ) -> Result<NodeId, LayoutError> {
164        let node = self
165            .tree
166            .new_with_children(style.resolve(self.direction), children)?;
167        self.track(node, style);
168        Ok(node)
169    }
170
171    /// Replaces `node`'s declared style, carrying forward whatever the out-of-band mutators below set — a freshly-built `style` (e.g. from a `styled_by` closure reacting to an unrelated signal) has no way to know about them.
172    pub fn set_style(&mut self, node: NodeId, mut style: LayoutStyle) -> Result<(), LayoutError> {
173        self.alive(node)?;
174        if let Some(previous) = self.styles.get(&node) {
175            style.logical.hidden |= previous.logical.hidden;
176            style.logical.row_forced |= previous.logical.row_forced;
177            style.logical.min_height_override = style
178                .logical
179                .min_height_override
180                .or(previous.logical.min_height_override);
181            style.logical.leading_margin = style
182                .logical
183                .leading_margin
184                .or(previous.logical.leading_margin);
185        }
186        self.push_style(node, &style);
187        self.track(node, style);
188        Ok(())
189    }
190
191    /// Replaces `parent`'s children with `children`, in order. Used by reactive lists to insert, move,
192    /// and drop item nodes as their source collection changes.
193    pub fn set_children(&mut self, parent: NodeId, children: &[NodeId]) -> Result<(), LayoutError> {
194        self.alive(parent)?;
195        for &child in children {
196            self.alive(child)?;
197        }
198        self.tree
199            .set_children(parent, children)
200            .map_err(LayoutError::from)
201    }
202
203    /// Appends `child` to `parent`'s existing children (unlike [`set_children`], which replaces them). Used
204    /// to attach an overlay's out-of-flow content to the layout root without touching the root's other children.
205    pub fn add_child(&mut self, parent: NodeId, child: NodeId) -> Result<(), LayoutError> {
206        self.alive(parent)?;
207        self.alive(child)?;
208        self.tree
209            .add_child(parent, child)
210            .map_err(LayoutError::from)
211    }
212
213    /// Detaches `child` from `parent` (does not free it — call [`remove`] afterwards to release the node).
214    pub fn remove_child(&mut self, parent: NodeId, child: NodeId) -> Result<(), LayoutError> {
215        self.alive(parent)?;
216        self.alive(child)?;
217        self.tree
218            .remove_child(parent, child)
219            .map(|_| ())
220            .map_err(LayoutError::from)
221    }
222
223    /// The node's style, or `None` once it has been freed — the read-side twin of [`alive`](Self::alive).
224    fn style_of(&self, node: NodeId) -> Option<&taffy::Style> {
225        if !self.live.contains(&node) {
226            return None;
227        }
228        self.tree.style(node).ok()
229    }
230
231    /// An error, rather than a panic, for a node that has been freed.
232    ///
233    /// Every mutator goes through this because a widget can legitimately outlive its node: a `Segment` holds
234    /// the widget so a re-render mid-dispatch can still flatten it, so an effect the widget owns may fire
235    /// once after the list that held it dropped the node. Taffy indexes its slot map directly and would
236    /// panic on that id — and these all return `Result` already, so the honest answer to "attach a node that
237    /// is gone" is the error the signature promises. The read-only queries take the same view (`is_size_auto`
238    /// and friends already fall back rather than fail).
239    fn alive(&self, node: NodeId) -> Result<(), LayoutError> {
240        if self.live.contains(&node) {
241            Ok(())
242        } else {
243            Err(LayoutError::Engine(format!(
244                "node {node:?} no longer exists"
245            )))
246        }
247    }
248
249    /// Frees a node (and its measure context) from the tree. The caller must have already detached it from
250    /// its parent (via [`set_children`]); a removed node id must not be used again.
251    pub fn remove(&mut self, node: NodeId) {
252        self.forget(node);
253        let _ = self.tree.remove(node);
254    }
255
256    pub fn mark_dirty(&mut self, node: NodeId) -> Result<(), LayoutError> {
257        self.alive(node)?;
258        self.tree.mark_dirty(node).map_err(LayoutError::from)
259    }
260
261    /// Whether the node's `width`/`height` are `auto` (i.e. content-sized).
262    pub fn is_size_auto(&self, node: NodeId) -> (bool, bool) {
263        match self.style_of(node) {
264            Some(s) => (s.size.width.is_auto(), s.size.height.is_auto()),
265            None => (false, false),
266        }
267    }
268
269    /// Sets the node's width to a definite length, or back to `auto` when `None`.
270    pub fn set_width(&mut self, node: NodeId, width: Option<f32>) {
271        self.mutate_style(node, |style| {
272            style.inner.size.width =
273                width.map_or(taffy::Dimension::auto(), taffy::Dimension::length);
274        });
275    }
276
277    /// Sets the node's height to a definite length, or back to `auto` when `None`.
278    pub fn set_height(&mut self, node: NodeId, height: Option<f32>) {
279        self.mutate_style(node, |style| {
280            style.inner.size.height =
281                height.map_or(taffy::Dimension::auto(), taffy::Dimension::length);
282        });
283    }
284
285    /// Sets the node's minimum height to a definite length, or clears it (`auto`) when `None`. Lets a
286    /// content-measured leaf (e.g. a code editor's text area) fill a viewport it would otherwise underflow.
287    pub fn set_min_height(&mut self, node: NodeId, height: Option<f32>) {
288        self.mutate_style(node, |style| {
289            style.logical.min_height_override = height;
290        });
291    }
292
293    /// Turns `node` into a flex row after construction, registering it as direction-following so a later
294    /// RTL flip reverses it like an authored `flex_row`. What a reconciling list calls when it learns —
295    /// from the container it is being attached to — that its items run horizontally.
296    pub fn make_flex_row(&mut self, node: NodeId) {
297        self.mutate_style(node, |style| {
298            style.logical.row_forced = true;
299        });
300    }
301
302    /// Whether the node lays its children along the main (horizontal) axis — a flex row. A column, or any
303    /// non-row node (missing / errored), is `false`. A transparent fragment reads its host's axis to know
304    /// which margin edge a per-item gap sits on.
305    pub fn is_row(&self, node: NodeId) -> bool {
306        // Same guard every other read takes, and for the same reason: taffy panics on a freed key rather
307        // than reporting it, so a node removed since the caller last saw it has to be answered here.
308        if self.alive(node).is_err() {
309            return false;
310        }
311        self.tree
312            .style(node)
313            .map(|s| {
314                matches!(
315                    s.flex_direction,
316                    taffy::FlexDirection::Row | taffy::FlexDirection::RowReverse
317                )
318            })
319            .unwrap_or(false)
320    }
321
322    /// Sets the node's leading margin on the host's main axis (`top` for a column; for a row, whichever
323    /// horizontal edge the host lays out from) to `px`, leaving the other edges untouched. A transparent
324    /// `for … gap:N` uses this to space its items by a gap without a container of its own: the item cell
325    /// carries the gap as a margin instead.
326    pub fn set_leading_margin(&mut self, node: NodeId, is_row: bool, px: f32) {
327        self.mutate_style(node, |style| {
328            style.logical.leading_margin = Some((is_row, px));
329        });
330    }
331
332    /// Whether the node's host lays its children out from the right — an RTL row, or one explicitly reversed.
333    fn leads_from_right(&self, node: NodeId) -> bool {
334        if !self.live.contains(&node) {
335            return false;
336        }
337        self.tree
338            .parent(node)
339            .and_then(|parent| self.style_of(parent))
340            .map(|s| s.flex_direction == taffy::FlexDirection::RowReverse)
341            .unwrap_or(false)
342    }
343
344    /// Whether this node is itself out of layout flow. Says nothing about its ancestors — [`walk`](Self::walk)
345    /// carries that down as it descends, and a caller asking about one node has to climb for itself.
346    pub fn is_display_none(&self, node: NodeId) -> bool {
347        self.style_of(node)
348            .map(|s| s.display == taffy::Display::None)
349            .unwrap_or(false)
350    }
351
352    /// Toggles a node in or out of layout flow. A hidden node (`Display::None`) takes no space and lays out none of its subtree; a visible node returns to whichever `display` it declared. Used for responsive show/hide (e.g. collapsing a sidebar on narrow windows).
353    pub fn set_display(&mut self, node: NodeId, visible: bool) {
354        self.mutate_style(node, |style| {
355            style.logical.hidden = !visible;
356        });
357    }
358
359    pub fn compute_layout(
360        &mut self,
361        root: NodeId,
362        available_width: AvailableSpace,
363        available_height: AvailableSpace,
364    ) -> Result<(), LayoutError> {
365        self.alive(root)?;
366        self.tree
367            .compute_layout_with_measure(
368                root,
369                taffy::geometry::Size {
370                    width: available_width.into(),
371                    height: available_height.into(),
372                },
373                |known, available, _node, context, _style| {
374                    let Some(measure) = context else {
375                        return taffy::geometry::Size::ZERO;
376                    };
377                    // Width to wrap against: a resolved width wins, else the definite available width, else a large bound so MaxContent stays single-line.
378                    let width = known.width.unwrap_or(match available.width {
379                        taffy::AvailableSpace::Definite(w) => w,
380                        taffy::AvailableSpace::MaxContent => 1.0e6,
381                        taffy::AvailableSpace::MinContent => 0.0,
382                    });
383                    let (mw, mh) = measure(width);
384                    taffy::geometry::Size {
385                        width: known.width.unwrap_or(mw),
386                        height: known.height.unwrap_or(mh),
387                    }
388                },
389            )
390            .map_err(LayoutError::from)
391    }
392
393    pub fn is_dirty(&self, node: NodeId) -> bool {
394        self.live.contains(&node) && self.tree.dirty(node).unwrap_or(true)
395    }
396
397    pub fn layout(&self, node: NodeId) -> Result<geometry_core::Rect, LayoutError> {
398        self.alive(node)?;
399        let layout = self.tree.layout(node).map_err(LayoutError::from)?;
400        Ok(geometry_core::Rect::new(
401            layout.location.x,
402            layout.location.y,
403            layout.size.width,
404            layout.size.height,
405        ))
406    }
407
408    pub fn is_fixed_size(&self, node: NodeId) -> Option<(f32, f32)> {
409        let style = self.style_of(node)?;
410        let w = style.size.width.into_option()?;
411        let h = style.size.height.into_option()?;
412        if style.flex_grow > 0.0 {
413            return None;
414        }
415        Some((w, h))
416    }
417
418    pub fn collect_dirty_nodes(&self, root: NodeId, out: &mut Vec<NodeId>) {
419        let mut stack = vec![root];
420        while let Some(node) = stack.pop() {
421            if self.is_dirty(node) {
422                out.push(node);
423            }
424            for child in self.tree.child_ids(node) {
425                stack.push(child);
426            }
427        }
428    }
429
430    pub fn walk<F>(&self, root: NodeId, f: &mut F) -> Result<(), LayoutError>
431    where
432        F: FnMut(NodeId, geometry_core::Rect) -> bool,
433    {
434        self.alive(root)?;
435        struct StackEntry {
436            node: NodeId,
437            offset_x: f32,
438            offset_y: f32,
439            // `display:none` on an ancestor: taffy stops laying out the subtree, leaving stale layouts,
440            // so descendants keep their last visible size and widgets that draw at fixed coordinates
441            // (e.g. a Canvas) would still paint. Force the whole subtree to a zero size instead.
442            hidden: bool,
443        }
444
445        let mut stack = Vec::with_capacity(64);
446        stack.push(StackEntry {
447            node: root,
448            offset_x: 0.0,
449            offset_y: 0.0,
450            hidden: false,
451        });
452
453        while let Some(entry) = stack.pop() {
454            let layout = self.tree.layout(entry.node).map_err(LayoutError::from)?;
455            let abs_x = entry.offset_x + layout.location.x;
456            let abs_y = entry.offset_y + layout.location.y;
457            let hidden = entry.hidden
458                || self
459                    .tree
460                    .style(entry.node)
461                    .map(|s| s.display == taffy::Display::None)
462                    .unwrap_or(false);
463            let (w, h) = if hidden {
464                (0.0, 0.0)
465            } else {
466                (layout.size.width, layout.size.height)
467            };
468
469            let descend = f(entry.node, geometry_core::Rect::new(abs_x, abs_y, w, h));
470
471            if descend {
472                let base = stack.len();
473                for child in self.tree.child_ids(entry.node) {
474                    stack.push(StackEntry {
475                        node: child,
476                        offset_x: abs_x,
477                        offset_y: abs_y,
478                        hidden,
479                    });
480                }
481                stack[base..].reverse();
482            }
483        }
484        Ok(())
485    }
486}
487
488impl Default for LayoutEngine {
489    fn default() -> Self {
490        Self::new()
491    }
492}
493
494#[cfg(test)]
495mod tests {
496    use super::*;
497
498    fn lay_out(engine: &mut LayoutEngine, root: NodeId) {
499        engine
500            .compute_layout(
501                root,
502                AvailableSpace::Definite(300.0),
503                AvailableSpace::Definite(100.0),
504            )
505            .unwrap();
506    }
507
508    /// Every mutator answers for a freed node instead of taking the process down with it.
509    ///
510    /// Taffy indexes its slot map directly — `style()` included, so there is nothing to guard with but our
511    /// own record — and a widget can outlive its node by design: a `Segment` holds it so a re-render
512    /// mid-dispatch can still flatten it, so an effect it owns may fire once after the node is gone. These
513    /// all return `Result`; an error is what the signature already promised.
514    #[test]
515    fn a_freed_node_is_an_error_and_never_a_panic() {
516        let mut engine = LayoutEngine::new();
517        let parent = engine.new_container(LayoutStyle::new(), &[]).unwrap();
518        let child = engine.new_leaf(LayoutStyle::new()).unwrap();
519        let ghost = engine.new_leaf(LayoutStyle::new()).unwrap();
520        engine.remove(ghost);
521
522        assert!(engine.set_children(ghost, &[]).is_err());
523        assert!(engine.set_children(parent, &[ghost]).is_err());
524        assert!(engine.set_style(ghost, LayoutStyle::new()).is_err());
525        assert!(engine.mark_dirty(ghost).is_err());
526        assert!(engine.add_child(parent, ghost).is_err());
527        assert!(engine.remove_child(parent, ghost).is_err());
528        assert!(engine.layout(ghost).is_err());
529        assert!(
530            engine
531                .compute_layout(
532                    ghost,
533                    AvailableSpace::MaxContent,
534                    AvailableSpace::MaxContent
535                )
536                .is_err()
537        );
538        assert_eq!(engine.is_size_auto(ghost), (false, false));
539        assert!(engine.is_fixed_size(ghost).is_none());
540        // The reads that cannot fail still have to answer without touching taffy.
541        engine.set_width(ghost, Some(10.0));
542        engine.set_height(ghost, None);
543        engine.set_leading_margin(ghost, true, 4.0);
544
545        // And the live node beside it is untouched by any of that.
546        assert!(engine.set_children(parent, &[child]).is_ok());
547    }
548
549    #[test]
550    fn flipping_direction_relays_an_existing_row_without_rebuilding_it() {
551        // The whole point of resolving late: the same nodes, laid out the other way round.
552        let mut engine = LayoutEngine::new();
553        let first = engine
554            .new_leaf(LayoutStyle::new().width(50.0).height(10.0))
555            .unwrap();
556        let second = engine
557            .new_leaf(LayoutStyle::new().width(50.0).height(10.0))
558            .unwrap();
559        let row = engine
560            .new_container(
561                LayoutStyle::new().flex_row().width(300.0).height(100.0),
562                &[first, second],
563            )
564            .unwrap();
565        lay_out(&mut engine, row);
566        assert_eq!(engine.layout(first).unwrap().x, 0.0);
567        assert_eq!(engine.layout(second).unwrap().x, 50.0);
568
569        assert!(engine.set_direction(Direction::Rtl));
570        engine.mark_dirty(row).unwrap();
571        lay_out(&mut engine, row);
572        assert_eq!(
573            engine.layout(first).unwrap().x,
574            250.0,
575            "the first item now starts at the right edge"
576        );
577        assert_eq!(engine.layout(second).unwrap().x, 200.0);
578    }
579
580    #[test]
581    fn flipping_direction_moves_logical_padding_to_the_other_edge() {
582        let mut engine = LayoutEngine::new();
583        let child = engine
584            .new_leaf(LayoutStyle::new().width(50.0).height(10.0))
585            .unwrap();
586        let box_ = engine
587            .new_container(
588                LayoutStyle::new()
589                    .flex_column()
590                    .width(300.0)
591                    .height(100.0)
592                    .padding_start(20.0),
593                &[child],
594            )
595            .unwrap();
596        lay_out(&mut engine, box_);
597        assert_eq!(engine.layout(child).unwrap().x, 20.0);
598
599        engine.set_direction(Direction::Rtl);
600        engine.mark_dirty(box_).unwrap();
601        lay_out(&mut engine, box_);
602        assert_eq!(
603            engine.layout(child).unwrap().x,
604            0.0,
605            "padding moved to the right edge, so the child starts flush left"
606        );
607    }
608
609    #[test]
610    fn setting_the_same_direction_reports_no_change() {
611        let mut engine = LayoutEngine::new();
612        assert!(!engine.set_direction(Direction::Ltr));
613        assert!(engine.set_direction(Direction::Rtl));
614        assert!(!engine.set_direction(Direction::Rtl));
615    }
616
617    #[test]
618    fn restyling_a_node_drops_the_logical_edges_it_no_longer_has() {
619        // A stale entry would keep re-applying the old padding on every flip.
620        let mut engine = LayoutEngine::new();
621        let node = engine
622            .new_leaf(LayoutStyle::new().padding_start(20.0).width(50.0))
623            .unwrap();
624        engine
625            .set_style(node, LayoutStyle::new().width(50.0))
626            .unwrap();
627        engine.set_direction(Direction::Rtl);
628        let style = engine.tree.style(node).unwrap();
629        assert_eq!(style.padding.left, taffy::LengthPercentage::length(0.0));
630        assert_eq!(style.padding.right, taffy::LengthPercentage::length(0.0));
631    }
632
633    #[test]
634    fn a_gap_margin_follows_the_edge_its_row_leads_from() {
635        let mut engine = LayoutEngine::new();
636        let first = engine.new_leaf(LayoutStyle::new().width(50.0)).unwrap();
637        let second = engine.new_leaf(LayoutStyle::new().width(50.0)).unwrap();
638        let row = engine
639            .new_container(LayoutStyle::new().flex_row().width(300.0), &[first, second])
640            .unwrap();
641        engine.set_leading_margin(second, true, 8.0);
642        assert_eq!(
643            engine.tree.style(second).unwrap().margin.left,
644            taffy::LengthPercentageAuto::length(8.0)
645        );
646
647        engine.set_direction(Direction::Rtl);
648        engine.mark_dirty(row).unwrap();
649        let margin = engine.tree.style(second).unwrap().margin;
650        assert_eq!(
651            margin.right,
652            taffy::LengthPercentageAuto::length(8.0),
653            "the gap moved to the edge the reversed row leads from"
654        );
655        assert_eq!(
656            margin.left,
657            taffy::LengthPercentageAuto::length(0.0),
658            "and does not linger on the old one"
659        );
660    }
661
662    #[test]
663    fn engine_leaf_layout() {
664        let mut engine = LayoutEngine::new();
665        let leaf = engine
666            .new_leaf(LayoutStyle::new().width(50.0).height(40.0))
667            .unwrap();
668        engine
669            .compute_layout(
670                leaf,
671                AvailableSpace::Definite(200.0),
672                AvailableSpace::Definite(200.0),
673            )
674            .unwrap();
675        let rect = engine.layout(leaf).unwrap();
676        assert_eq!(rect.width, 50.0_f32);
677        assert_eq!(rect.height, 40.0_f32);
678    }
679
680    #[test]
681    fn engine_flex_row_positions() {
682        let mut engine = LayoutEngine::new();
683        let child1 = engine
684            .new_leaf(LayoutStyle::new().width(100.0).height(100.0))
685            .unwrap();
686        let child2 = engine
687            .new_leaf(LayoutStyle::new().width(100.0).height(100.0))
688            .unwrap();
689        let root = engine
690            .new_container(
691                LayoutStyle::new().flex_row().width(200.0).height(100.0),
692                &[child1, child2],
693            )
694            .unwrap();
695        engine
696            .compute_layout(
697                root,
698                AvailableSpace::Definite(200.0),
699                AvailableSpace::Definite(100.0),
700            )
701            .unwrap();
702
703        let r1 = engine.layout(child1).unwrap();
704        let r2 = engine.layout(child2).unwrap();
705        assert_eq!(r1.x, 0.0_f32);
706        assert_eq!(r1.y, 0.0_f32);
707        assert_eq!(r2.x, 100.0_f32);
708        assert_eq!(r2.y, 0.0_f32);
709    }
710
711    #[test]
712    fn engine_flex_column_positions() {
713        let mut engine = LayoutEngine::new();
714        let child1 = engine
715            .new_leaf(LayoutStyle::new().width(100.0).height(100.0))
716            .unwrap();
717        let child2 = engine
718            .new_leaf(LayoutStyle::new().width(100.0).height(100.0))
719            .unwrap();
720        let root = engine
721            .new_container(
722                LayoutStyle::new().flex_column().width(100.0).height(200.0),
723                &[child1, child2],
724            )
725            .unwrap();
726        engine
727            .compute_layout(
728                root,
729                AvailableSpace::Definite(100.0),
730                AvailableSpace::Definite(200.0),
731            )
732            .unwrap();
733
734        let r1 = engine.layout(child1).unwrap();
735        let r2 = engine.layout(child2).unwrap();
736        assert_eq!(r1.x, 0.0_f32);
737        assert_eq!(r1.y, 0.0_f32);
738        assert_eq!(r2.x, 0.0_f32);
739        assert_eq!(r2.y, 100.0_f32);
740    }
741
742    #[test]
743    fn engine_walk_absolute() {
744        let mut engine = LayoutEngine::new();
745        let inner_child = engine
746            .new_leaf(LayoutStyle::new().width(50.0).height(50.0))
747            .unwrap();
748        let inner = engine
749            .new_container(
750                LayoutStyle::new().flex_row().width(50.0).height(50.0),
751                &[inner_child],
752            )
753            .unwrap();
754        let outer_first = engine
755            .new_leaf(LayoutStyle::new().width(100.0).height(50.0))
756            .unwrap();
757        let root = engine
758            .new_container(
759                LayoutStyle::new().flex_row().width(150.0).height(50.0),
760                &[outer_first, inner],
761            )
762            .unwrap();
763        engine
764            .compute_layout(
765                root,
766                AvailableSpace::Definite(150.0),
767                AvailableSpace::Definite(50.0),
768            )
769            .unwrap();
770
771        let mut hits: Vec<(NodeId, geometry_core::Rect)> = Vec::new();
772        engine
773            .walk(root, &mut |node, rect| {
774                hits.push((node, rect));
775                true
776            })
777            .unwrap();
778
779        let inner_child_rect = hits
780            .iter()
781            .find(|(n, _)| *n == inner_child)
782            .map(|(_, r)| *r)
783            .unwrap();
784        assert_eq!(inner_child_rect.x, 100.0_f32);
785        assert_eq!(inner_child_rect.y, 0.0_f32);
786        assert_eq!(inner_child_rect.width, 50.0_f32);
787        assert_eq!(inner_child_rect.height, 50.0_f32);
788    }
789
790    #[test]
791    fn engine_set_style() {
792        let mut engine = LayoutEngine::new();
793        let leaf = engine
794            .new_leaf(LayoutStyle::new().width(10.0).height(10.0))
795            .unwrap();
796        engine
797            .set_style(leaf, LayoutStyle::new().width(80.0).height(60.0))
798            .unwrap();
799        engine
800            .compute_layout(
801                leaf,
802                AvailableSpace::Definite(200.0),
803                AvailableSpace::Definite(200.0),
804            )
805            .unwrap();
806        let rect = engine.layout(leaf).unwrap();
807        assert_eq!(rect.width, 80.0_f32);
808        assert_eq!(rect.height, 60.0_f32);
809    }
810}