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    /// Nodes whose style holds logical edges, kept so a direction flip can re-resolve them from the original
19    /// intent. Only these nodes pay for the copy; a tree with no logical edges keeps the map empty.
20    logical: FxHashMap<NodeId, LayoutStyle>,
21    /// Rows whose main axis follows the writing direction. Held apart from `logical` because flipping one is
22    /// a flag toggle that needs no original style — and rows are common enough that a `NodeId` is worth the
23    /// saving over a whole `LayoutStyle`.
24    directional_rows: FxHashSet<NodeId>,
25    /// Main-axis gap margins applied by [`set_leading_margin`](Self::set_leading_margin), which sit on the
26    /// *leading* edge and so must move to the other side when a row reverses.
27    leading_margins: FxHashMap<NodeId, (bool, f32)>,
28}
29
30impl LayoutEngine {
31    pub fn new() -> Self {
32        Self {
33            tree: TaffyTree::new(),
34            direction: Direction::default(),
35            logical: FxHashMap::default(),
36            directional_rows: FxHashSet::default(),
37            leading_margins: FxHashMap::default(),
38        }
39    }
40
41    /// The direction logical edges currently resolve against.
42    pub fn direction(&self) -> Direction {
43        self.direction
44    }
45
46    /// Re-resolves every direction-dependent node against `direction`, returning whether anything changed.
47    /// The caller still has to mark the tree dirty and recompute — this only rewrites styles.
48    ///
49    /// This is what lets one build serve both directions: rather than rebuilding the widget tree, each node
50    /// that was authored logically is resolved again from the intent recorded when it was created.
51    pub fn set_direction(&mut self, direction: Direction) -> bool {
52        if self.direction == direction {
53            return false;
54        }
55        self.direction = direction;
56        let rows = std::mem::take(&mut self.directional_rows);
57        for &node in &rows {
58            if let Ok(current) = self.tree.style(node) {
59                let mut style = current.clone();
60                style.flex_direction = if direction.is_rtl() {
61                    taffy::FlexDirection::RowReverse
62                } else {
63                    taffy::FlexDirection::Row
64                };
65                let _ = self.tree.set_style(node, style);
66            }
67        }
68        self.directional_rows = rows;
69        let logical = std::mem::take(&mut self.logical);
70        for (&node, style) in &logical {
71            let _ = self.tree.set_style(node, style.resolve(direction));
72        }
73        self.logical = logical;
74        let margins = std::mem::take(&mut self.leading_margins);
75        for (&node, &(is_row, px)) in &margins {
76            self.apply_leading_margin(node, is_row, px, true);
77        }
78        self.leading_margins = margins;
79        true
80    }
81
82    /// Records whichever direction-dependent parts of `style` the node will need re-resolved on a flip, and
83    /// drops any it no longer has — a restyled node must not keep the previous style's logical edges.
84    fn track(&mut self, node: NodeId, style: LayoutStyle) {
85        if style.logical.has_edges() {
86            self.directional_rows.remove(&node);
87            self.logical.insert(node, style);
88            return;
89        }
90        self.logical.remove(&node);
91        if style.logical.row_follows_direction {
92            self.directional_rows.insert(node);
93        } else {
94            self.directional_rows.remove(&node);
95        }
96    }
97
98    fn forget(&mut self, node: NodeId) {
99        self.logical.remove(&node);
100        self.directional_rows.remove(&node);
101        self.leading_margins.remove(&node);
102    }
103
104    pub fn new_leaf(&mut self, style: LayoutStyle) -> Result<NodeId, LayoutError> {
105        let node = self.tree.new_leaf(style.resolve(self.direction))?;
106        self.track(node, style);
107        Ok(node)
108    }
109
110    pub fn new_measured_leaf(
111        &mut self,
112        style: LayoutStyle,
113        measure: MeasureFn,
114    ) -> Result<NodeId, LayoutError> {
115        let node = self
116            .tree
117            .new_leaf_with_context(style.resolve(self.direction), measure)?;
118        self.track(node, style);
119        Ok(node)
120    }
121
122    pub fn new_container(
123        &mut self,
124        style: LayoutStyle,
125        children: &[NodeId],
126    ) -> Result<NodeId, LayoutError> {
127        let node = self
128            .tree
129            .new_with_children(style.resolve(self.direction), children)?;
130        self.track(node, style);
131        Ok(node)
132    }
133
134    pub fn set_style(&mut self, node: NodeId, style: LayoutStyle) -> Result<(), LayoutError> {
135        self.tree.set_style(node, style.resolve(self.direction))?;
136        self.track(node, style);
137        Ok(())
138    }
139
140    /// Replaces `parent`'s children with `children`, in order. Used by reactive lists to insert, move,
141    /// and drop item nodes as their source collection changes.
142    pub fn set_children(&mut self, parent: NodeId, children: &[NodeId]) -> Result<(), LayoutError> {
143        self.tree
144            .set_children(parent, children)
145            .map_err(LayoutError::from)
146    }
147
148    /// Appends `child` to `parent`'s existing children (unlike [`set_children`], which replaces them). Used
149    /// to attach an overlay's out-of-flow content to the layout root without touching the root's other children.
150    pub fn add_child(&mut self, parent: NodeId, child: NodeId) -> Result<(), LayoutError> {
151        self.tree
152            .add_child(parent, child)
153            .map_err(LayoutError::from)
154    }
155
156    /// Detaches `child` from `parent` (does not free it — call [`remove`] afterwards to release the node).
157    pub fn remove_child(&mut self, parent: NodeId, child: NodeId) -> Result<(), LayoutError> {
158        self.tree
159            .remove_child(parent, child)
160            .map(|_| ())
161            .map_err(LayoutError::from)
162    }
163
164    /// Frees a node (and its measure context) from the tree. The caller must have already detached it from
165    /// its parent (via [`set_children`]); a removed node id must not be used again.
166    pub fn remove(&mut self, node: NodeId) {
167        self.forget(node);
168        let _ = self.tree.remove(node);
169    }
170
171    pub fn mark_dirty(&mut self, node: NodeId) -> Result<(), LayoutError> {
172        self.tree.mark_dirty(node).map_err(LayoutError::from)
173    }
174
175    /// Whether the node's `width`/`height` are `auto` (i.e. content-sized).
176    pub fn is_size_auto(&self, node: NodeId) -> (bool, bool) {
177        match self.tree.style(node) {
178            Ok(s) => (s.size.width.is_auto(), s.size.height.is_auto()),
179            Err(_) => (false, false),
180        }
181    }
182
183    /// Sets the node's width to a definite length, or back to `auto` when `None`.
184    pub fn set_width(&mut self, node: NodeId, width: Option<f32>) {
185        if let Ok(s) = self.tree.style(node) {
186            let mut style = s.clone();
187            style.size.width = width.map_or(taffy::Dimension::auto(), taffy::Dimension::length);
188            let _ = self.tree.set_style(node, style);
189        }
190    }
191
192    /// Sets the node's height to a definite length, or back to `auto` when `None`.
193    pub fn set_height(&mut self, node: NodeId, height: Option<f32>) {
194        if let Ok(s) = self.tree.style(node) {
195            let mut style = s.clone();
196            style.size.height = height.map_or(taffy::Dimension::auto(), taffy::Dimension::length);
197            let _ = self.tree.set_style(node, style);
198        }
199    }
200
201    /// Sets the node's minimum height to a definite length, or clears it (`auto`) when `None`. Lets a
202    /// content-measured leaf (e.g. a code editor's text area) fill a viewport it would otherwise underflow.
203    pub fn set_min_height(&mut self, node: NodeId, height: Option<f32>) {
204        if let Ok(s) = self.tree.style(node) {
205            let mut style = s.clone();
206            style.min_size.height =
207                height.map_or(taffy::Dimension::auto(), taffy::Dimension::length);
208            let _ = self.tree.set_style(node, style);
209        }
210    }
211
212    /// Whether the node lays its children along the main (horizontal) axis — a flex row. A column, or any
213    /// non-row node (missing / errored), is `false`. A transparent fragment reads its host's axis to know
214    /// which margin edge a per-item gap sits on.
215    pub fn is_row(&self, node: NodeId) -> bool {
216        self.tree
217            .style(node)
218            .map(|s| {
219                matches!(
220                    s.flex_direction,
221                    taffy::FlexDirection::Row | taffy::FlexDirection::RowReverse
222                )
223            })
224            .unwrap_or(false)
225    }
226
227    /// Sets the node's leading margin on the host's main axis (`top` for a column; for a row, whichever
228    /// horizontal edge the host lays out from) to `px`, leaving the other edges untouched. A transparent
229    /// `for … gap:N` uses this to space its items by a gap without a container of its own: the item cell
230    /// carries the gap as a margin instead.
231    pub fn set_leading_margin(&mut self, node: NodeId, is_row: bool, px: f32) {
232        self.leading_margins.insert(node, (is_row, px));
233        self.apply_leading_margin(node, is_row, px, false);
234    }
235
236    /// Whether the node's host lays its children out from the right — an RTL row, or one explicitly reversed.
237    fn leads_from_right(&self, node: NodeId) -> bool {
238        self.tree
239            .parent(node)
240            .and_then(|parent| self.tree.style(parent).ok())
241            .map(|s| s.flex_direction == taffy::FlexDirection::RowReverse)
242            .unwrap_or(false)
243    }
244
245    /// `clear_opposite` un-sets the horizontal edge this node's gap used to sit on, which a re-application
246    /// after a direction flip needs and a first application must not do (it would clobber an author's margin).
247    fn apply_leading_margin(&mut self, node: NodeId, is_row: bool, px: f32, clear_opposite: bool) {
248        let leading_right = is_row && self.leads_from_right(node);
249        let Ok(current) = self.tree.style(node) else {
250            return;
251        };
252        let mut style = current.clone();
253        let m = taffy::LengthPercentageAuto::length(px);
254        if is_row {
255            if leading_right {
256                style.margin.right = m;
257            } else {
258                style.margin.left = m;
259            }
260            if clear_opposite {
261                let zero = taffy::LengthPercentageAuto::length(0.0);
262                if leading_right {
263                    style.margin.left = zero;
264                } else {
265                    style.margin.right = zero;
266                }
267            }
268        } else {
269            style.margin.top = m;
270        }
271        let _ = self.tree.set_style(node, style);
272    }
273
274    /// 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 is `Display::Flex`. Used for responsive show/hide (e.g. collapsing a sidebar on narrow windows).
275    pub fn set_display(&mut self, node: NodeId, visible: bool) {
276        if let Ok(s) = self.tree.style(node) {
277            let mut style = s.clone();
278            style.display = if visible {
279                taffy::Display::Flex
280            } else {
281                taffy::Display::None
282            };
283            let _ = self.tree.set_style(node, style);
284        }
285    }
286
287    pub fn compute_layout(
288        &mut self,
289        root: NodeId,
290        available_width: AvailableSpace,
291        available_height: AvailableSpace,
292    ) -> Result<(), LayoutError> {
293        self.tree
294            .compute_layout_with_measure(
295                root,
296                taffy::geometry::Size {
297                    width: available_width.into(),
298                    height: available_height.into(),
299                },
300                |known, available, _node, context, _style| {
301                    let Some(measure) = context else {
302                        return taffy::geometry::Size::ZERO;
303                    };
304                    // Width to wrap against: a resolved width wins, else the definite available width, else a large bound so MaxContent stays single-line.
305                    let width = known.width.unwrap_or(match available.width {
306                        taffy::AvailableSpace::Definite(w) => w,
307                        taffy::AvailableSpace::MaxContent => 1.0e6,
308                        taffy::AvailableSpace::MinContent => 0.0,
309                    });
310                    let (mw, mh) = measure(width);
311                    taffy::geometry::Size {
312                        width: known.width.unwrap_or(mw),
313                        height: known.height.unwrap_or(mh),
314                    }
315                },
316            )
317            .map_err(LayoutError::from)
318    }
319
320    pub fn is_dirty(&self, node: NodeId) -> bool {
321        self.tree.dirty(node).unwrap_or(true)
322    }
323
324    pub fn layout(&self, node: NodeId) -> Result<geometry_core::Rect, LayoutError> {
325        let layout = self.tree.layout(node).map_err(LayoutError::from)?;
326        Ok(geometry_core::Rect::new(
327            layout.location.x,
328            layout.location.y,
329            layout.size.width,
330            layout.size.height,
331        ))
332    }
333
334    pub fn is_fixed_size(&self, node: NodeId) -> Option<(f32, f32)> {
335        let style = self.tree.style(node).ok()?;
336        let w = style.size.width.into_option()?;
337        let h = style.size.height.into_option()?;
338        if style.flex_grow > 0.0 {
339            return None;
340        }
341        Some((w, h))
342    }
343
344    pub fn collect_dirty_nodes(&self, root: NodeId, out: &mut Vec<NodeId>) {
345        let mut stack = vec![root];
346        while let Some(node) = stack.pop() {
347            if self.is_dirty(node) {
348                out.push(node);
349            }
350            for child in self.tree.child_ids(node) {
351                stack.push(child);
352            }
353        }
354    }
355
356    pub fn walk<F>(&self, root: NodeId, f: &mut F) -> Result<(), LayoutError>
357    where
358        F: FnMut(NodeId, geometry_core::Rect) -> bool,
359    {
360        struct StackEntry {
361            node: NodeId,
362            offset_x: f32,
363            offset_y: f32,
364            // `display:none` on an ancestor: taffy stops laying out the subtree, leaving stale layouts,
365            // so descendants keep their last visible size and widgets that draw at fixed coordinates
366            // (e.g. a Canvas) would still paint. Force the whole subtree to a zero size instead.
367            hidden: bool,
368        }
369
370        let mut stack = Vec::with_capacity(64);
371        stack.push(StackEntry {
372            node: root,
373            offset_x: 0.0,
374            offset_y: 0.0,
375            hidden: false,
376        });
377
378        while let Some(entry) = stack.pop() {
379            let layout = self.tree.layout(entry.node).map_err(LayoutError::from)?;
380            let abs_x = entry.offset_x + layout.location.x;
381            let abs_y = entry.offset_y + layout.location.y;
382            let hidden = entry.hidden
383                || self
384                    .tree
385                    .style(entry.node)
386                    .map(|s| s.display == taffy::Display::None)
387                    .unwrap_or(false);
388            let (w, h) = if hidden {
389                (0.0, 0.0)
390            } else {
391                (layout.size.width, layout.size.height)
392            };
393
394            let descend = f(entry.node, geometry_core::Rect::new(abs_x, abs_y, w, h));
395
396            if descend {
397                let base = stack.len();
398                for child in self.tree.child_ids(entry.node) {
399                    stack.push(StackEntry {
400                        node: child,
401                        offset_x: abs_x,
402                        offset_y: abs_y,
403                        hidden,
404                    });
405                }
406                stack[base..].reverse();
407            }
408        }
409        Ok(())
410    }
411}
412
413impl Default for LayoutEngine {
414    fn default() -> Self {
415        Self::new()
416    }
417}
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422
423    fn lay_out(engine: &mut LayoutEngine, root: NodeId) {
424        engine
425            .compute_layout(
426                root,
427                AvailableSpace::Definite(300.0),
428                AvailableSpace::Definite(100.0),
429            )
430            .unwrap();
431    }
432
433    #[test]
434    fn flipping_direction_relays_an_existing_row_without_rebuilding_it() {
435        // The whole point of resolving late: the same nodes, laid out the other way round.
436        let mut engine = LayoutEngine::new();
437        let first = engine
438            .new_leaf(LayoutStyle::new().width(50.0).height(10.0))
439            .unwrap();
440        let second = engine
441            .new_leaf(LayoutStyle::new().width(50.0).height(10.0))
442            .unwrap();
443        let row = engine
444            .new_container(
445                LayoutStyle::new().flex_row().width(300.0).height(100.0),
446                &[first, second],
447            )
448            .unwrap();
449        lay_out(&mut engine, row);
450        assert_eq!(engine.layout(first).unwrap().x, 0.0);
451        assert_eq!(engine.layout(second).unwrap().x, 50.0);
452
453        assert!(engine.set_direction(Direction::Rtl));
454        engine.mark_dirty(row).unwrap();
455        lay_out(&mut engine, row);
456        assert_eq!(
457            engine.layout(first).unwrap().x,
458            250.0,
459            "the first item now starts at the right edge"
460        );
461        assert_eq!(engine.layout(second).unwrap().x, 200.0);
462    }
463
464    #[test]
465    fn flipping_direction_moves_logical_padding_to_the_other_edge() {
466        let mut engine = LayoutEngine::new();
467        let child = engine
468            .new_leaf(LayoutStyle::new().width(50.0).height(10.0))
469            .unwrap();
470        let box_ = engine
471            .new_container(
472                LayoutStyle::new()
473                    .flex_column()
474                    .width(300.0)
475                    .height(100.0)
476                    .padding_start(20.0),
477                &[child],
478            )
479            .unwrap();
480        lay_out(&mut engine, box_);
481        assert_eq!(engine.layout(child).unwrap().x, 20.0);
482
483        engine.set_direction(Direction::Rtl);
484        engine.mark_dirty(box_).unwrap();
485        lay_out(&mut engine, box_);
486        assert_eq!(
487            engine.layout(child).unwrap().x,
488            0.0,
489            "padding moved to the right edge, so the child starts flush left"
490        );
491    }
492
493    #[test]
494    fn setting_the_same_direction_reports_no_change() {
495        let mut engine = LayoutEngine::new();
496        assert!(!engine.set_direction(Direction::Ltr));
497        assert!(engine.set_direction(Direction::Rtl));
498        assert!(!engine.set_direction(Direction::Rtl));
499    }
500
501    #[test]
502    fn restyling_a_node_drops_the_logical_edges_it_no_longer_has() {
503        // A stale entry would keep re-applying the old padding on every flip.
504        let mut engine = LayoutEngine::new();
505        let node = engine
506            .new_leaf(LayoutStyle::new().padding_start(20.0).width(50.0))
507            .unwrap();
508        engine
509            .set_style(node, LayoutStyle::new().width(50.0))
510            .unwrap();
511        engine.set_direction(Direction::Rtl);
512        let style = engine.tree.style(node).unwrap();
513        assert_eq!(style.padding.left, taffy::LengthPercentage::length(0.0));
514        assert_eq!(style.padding.right, taffy::LengthPercentage::length(0.0));
515    }
516
517    #[test]
518    fn a_gap_margin_follows_the_edge_its_row_leads_from() {
519        let mut engine = LayoutEngine::new();
520        let first = engine.new_leaf(LayoutStyle::new().width(50.0)).unwrap();
521        let second = engine.new_leaf(LayoutStyle::new().width(50.0)).unwrap();
522        let row = engine
523            .new_container(LayoutStyle::new().flex_row().width(300.0), &[first, second])
524            .unwrap();
525        engine.set_leading_margin(second, true, 8.0);
526        assert_eq!(
527            engine.tree.style(second).unwrap().margin.left,
528            taffy::LengthPercentageAuto::length(8.0)
529        );
530
531        engine.set_direction(Direction::Rtl);
532        engine.mark_dirty(row).unwrap();
533        let margin = engine.tree.style(second).unwrap().margin;
534        assert_eq!(
535            margin.right,
536            taffy::LengthPercentageAuto::length(8.0),
537            "the gap moved to the edge the reversed row leads from"
538        );
539        assert_eq!(
540            margin.left,
541            taffy::LengthPercentageAuto::length(0.0),
542            "and does not linger on the old one"
543        );
544    }
545
546    #[test]
547    fn engine_leaf_layout() {
548        let mut engine = LayoutEngine::new();
549        let leaf = engine
550            .new_leaf(LayoutStyle::new().width(50.0).height(40.0))
551            .unwrap();
552        engine
553            .compute_layout(
554                leaf,
555                AvailableSpace::Definite(200.0),
556                AvailableSpace::Definite(200.0),
557            )
558            .unwrap();
559        let rect = engine.layout(leaf).unwrap();
560        assert_eq!(rect.width, 50.0_f32);
561        assert_eq!(rect.height, 40.0_f32);
562    }
563
564    #[test]
565    fn engine_flex_row_positions() {
566        let mut engine = LayoutEngine::new();
567        let child1 = engine
568            .new_leaf(LayoutStyle::new().width(100.0).height(100.0))
569            .unwrap();
570        let child2 = engine
571            .new_leaf(LayoutStyle::new().width(100.0).height(100.0))
572            .unwrap();
573        let root = engine
574            .new_container(
575                LayoutStyle::new().flex_row().width(200.0).height(100.0),
576                &[child1, child2],
577            )
578            .unwrap();
579        engine
580            .compute_layout(
581                root,
582                AvailableSpace::Definite(200.0),
583                AvailableSpace::Definite(100.0),
584            )
585            .unwrap();
586
587        let r1 = engine.layout(child1).unwrap();
588        let r2 = engine.layout(child2).unwrap();
589        assert_eq!(r1.x, 0.0_f32);
590        assert_eq!(r1.y, 0.0_f32);
591        assert_eq!(r2.x, 100.0_f32);
592        assert_eq!(r2.y, 0.0_f32);
593    }
594
595    #[test]
596    fn engine_flex_column_positions() {
597        let mut engine = LayoutEngine::new();
598        let child1 = engine
599            .new_leaf(LayoutStyle::new().width(100.0).height(100.0))
600            .unwrap();
601        let child2 = engine
602            .new_leaf(LayoutStyle::new().width(100.0).height(100.0))
603            .unwrap();
604        let root = engine
605            .new_container(
606                LayoutStyle::new().flex_column().width(100.0).height(200.0),
607                &[child1, child2],
608            )
609            .unwrap();
610        engine
611            .compute_layout(
612                root,
613                AvailableSpace::Definite(100.0),
614                AvailableSpace::Definite(200.0),
615            )
616            .unwrap();
617
618        let r1 = engine.layout(child1).unwrap();
619        let r2 = engine.layout(child2).unwrap();
620        assert_eq!(r1.x, 0.0_f32);
621        assert_eq!(r1.y, 0.0_f32);
622        assert_eq!(r2.x, 0.0_f32);
623        assert_eq!(r2.y, 100.0_f32);
624    }
625
626    #[test]
627    fn engine_walk_absolute() {
628        let mut engine = LayoutEngine::new();
629        let inner_child = engine
630            .new_leaf(LayoutStyle::new().width(50.0).height(50.0))
631            .unwrap();
632        let inner = engine
633            .new_container(
634                LayoutStyle::new().flex_row().width(50.0).height(50.0),
635                &[inner_child],
636            )
637            .unwrap();
638        let outer_first = engine
639            .new_leaf(LayoutStyle::new().width(100.0).height(50.0))
640            .unwrap();
641        let root = engine
642            .new_container(
643                LayoutStyle::new().flex_row().width(150.0).height(50.0),
644                &[outer_first, inner],
645            )
646            .unwrap();
647        engine
648            .compute_layout(
649                root,
650                AvailableSpace::Definite(150.0),
651                AvailableSpace::Definite(50.0),
652            )
653            .unwrap();
654
655        let mut hits: Vec<(NodeId, geometry_core::Rect)> = Vec::new();
656        engine
657            .walk(root, &mut |node, rect| {
658                hits.push((node, rect));
659                true
660            })
661            .unwrap();
662
663        let inner_child_rect = hits
664            .iter()
665            .find(|(n, _)| *n == inner_child)
666            .map(|(_, r)| *r)
667            .unwrap();
668        assert_eq!(inner_child_rect.x, 100.0_f32);
669        assert_eq!(inner_child_rect.y, 0.0_f32);
670        assert_eq!(inner_child_rect.width, 50.0_f32);
671        assert_eq!(inner_child_rect.height, 50.0_f32);
672    }
673
674    #[test]
675    fn engine_set_style() {
676        let mut engine = LayoutEngine::new();
677        let leaf = engine
678            .new_leaf(LayoutStyle::new().width(10.0).height(10.0))
679            .unwrap();
680        engine
681            .set_style(leaf, LayoutStyle::new().width(80.0).height(60.0))
682            .unwrap();
683        engine
684            .compute_layout(
685                leaf,
686                AvailableSpace::Definite(200.0),
687                AvailableSpace::Definite(200.0),
688            )
689            .unwrap();
690        let rect = engine.layout(leaf).unwrap();
691        assert_eq!(rect.width, 80.0_f32);
692        assert_eq!(rect.height, 60.0_f32);
693    }
694}