Skip to main content

ravenrs_layout/
lib.rs

1//! Layout for RavenRS, built on [Taffy](https://github.com/DioxusLabs/taffy).
2//!
3//! Widgets describe boxes with [`LayoutStyle`] (a deliberately small subset of
4//! flexbox: stacks, gaps, padding, alignment, sizes and growth). Leaves that size
5//! themselves, such as text, carry a context `C` and are measured through a
6//! callback, so this crate does not depend on the text engine.
7
8use std::fmt;
9
10use ravenrs_core::{Insets, Point, Rect, Size};
11use taffy::prelude::{
12    AvailableSpace, Dimension, FlexDirection, LengthPercentage, LengthPercentageAuto, TaffyTree,
13};
14
15pub use taffy::NodeId as LayoutId;
16
17/// Direction in which a stack places its children.
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
19#[non_exhaustive]
20#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
21pub enum Axis {
22    /// Children top to bottom.
23    #[default]
24    Vertical,
25    /// Children left to right.
26    Horizontal,
27}
28
29/// Cross-axis alignment of a stack's children.
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
31#[non_exhaustive]
32#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
33pub enum Align {
34    /// Align to the leading edge of the cross axis.
35    #[default]
36    Start,
37    /// Center on the cross axis.
38    Center,
39    /// Align to the trailing edge of the cross axis.
40    End,
41    /// Fill the whole cross axis.
42    Stretch,
43    /// As `align_self`: keep its own size. It follows a parent that centers or
44    /// ends its children, and goes to the start of one that stretches them.
45    /// This is what a button wants, so it never grows to a column's width.
46    Fit,
47}
48
49/// Main-axis distribution of a stack's children.
50#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
51#[non_exhaustive]
52#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
53pub enum Justify {
54    /// Pack children at the start of the main axis.
55    #[default]
56    Start,
57    /// Center children on the main axis.
58    Center,
59    /// Pack children at the end of the main axis.
60    End,
61    /// Put free space evenly between children, none at the ends.
62    SpaceBetween,
63}
64
65/// The box model a widget asks for. `axis: None` makes a leaf.
66#[derive(Clone, Copy, Debug, Default, PartialEq)]
67#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
68pub struct LayoutStyle {
69    /// Stack direction, or `None` for a leaf.
70    pub axis: Option<Axis>,
71    /// Gap between consecutive children, in logical pixels.
72    pub spacing: f32,
73    /// Inner padding, in logical pixels.
74    pub padding: Insets,
75    /// Cross-axis alignment of children.
76    pub align: Align,
77    /// Main-axis distribution of children.
78    pub justify: Justify,
79    /// Fixed width in logical pixels; `None` sizes to content.
80    pub width: Option<f32>,
81    /// Fixed height in logical pixels; `None` sizes to content.
82    pub height: Option<f32>,
83    /// Minimum width in logical pixels.
84    pub min_width: Option<f32>,
85    /// Minimum height in logical pixels.
86    pub min_height: Option<f32>,
87    /// Maximum width in logical pixels.
88    pub max_width: Option<f32>,
89    /// Share of free main-axis space this box takes inside its parent stack.
90    pub grow: f32,
91    /// Overrides the parent's cross-axis alignment for this box.
92    pub align_self: Option<Align>,
93    /// Content may overflow this box (it scrolls or clips): the box does not
94    /// grow to fit its content.
95    pub overflow_hidden: bool,
96    /// Lays children out in this many equal-width grid columns instead of a stack.
97    pub grid_columns: Option<u16>,
98    /// Children move to a new line when they do not fit (flex-wrap).
99    pub wrap: bool,
100    /// Width divided by height; with one dimension known, the other follows.
101    pub aspect_ratio: Option<f32>,
102    /// Takes the box out of the flow and places it at this `(x, y)` offset
103    /// from its parent's top-left corner (design surfaces, diagrams, canvases).
104    pub absolute: Option<(f32, f32)>,
105}
106
107#[cfg_attr(feature = "live", ravenrs_live::expose)]
108impl LayoutStyle {
109    /// A stack along `axis` with default spacing, padding and alignment.
110    pub fn stack(axis: Axis) -> Self {
111        Self { axis: Some(axis), ..Self::default() }
112    }
113
114    /// A leaf box with no children and default sizing.
115    pub fn leaf() -> Self {
116        Self::default()
117    }
118
119    fn to_taffy(self) -> taffy::Style {
120        let len = LengthPercentage::length;
121        let dim = |v: Option<f32>| v.map_or(Dimension::auto(), Dimension::length);
122        let lpa = |v: Option<f32>| v.map_or(LengthPercentageAuto::auto(), LengthPercentageAuto::length);
123        let align = |a: Align| match a {
124            Align::Start | Align::Fit => taffy::AlignItems::FLEX_START,
125            Align::Center => taffy::AlignItems::CENTER,
126            Align::End => taffy::AlignItems::FLEX_END,
127            Align::Stretch => taffy::AlignItems::STRETCH,
128        };
129        let mut style = taffy::Style {
130            display: taffy::Display::Flex,
131            flex_direction: match self.axis {
132                Some(Axis::Horizontal) => FlexDirection::Row,
133                _ => FlexDirection::Column,
134            },
135            gap: taffy::Size { width: len(self.spacing), height: len(self.spacing) },
136            padding: taffy::Rect {
137                left: len(self.padding.left),
138                right: len(self.padding.right),
139                top: len(self.padding.top),
140                bottom: len(self.padding.bottom),
141            },
142            align_items: align(self.align),
143            align_self: self.align_self.map(align),
144            justify_content: match self.justify {
145                Justify::Start => taffy::JustifyContent::FLEX_START,
146                Justify::Center => taffy::JustifyContent::CENTER,
147                Justify::End => taffy::JustifyContent::FLEX_END,
148                Justify::SpaceBetween => taffy::JustifyContent::SPACE_BETWEEN,
149            },
150            size: taffy::Size { width: dim(self.width), height: dim(self.height) },
151            min_size: taffy::Size { width: lpa(self.min_width), height: lpa(self.min_height) },
152            max_size: taffy::Size { width: lpa(self.max_width), height: LengthPercentageAuto::auto() },
153            flex_grow: self.grow,
154            flex_shrink: 1.0,
155            ..taffy::Style::default()
156        };
157        style.aspect_ratio = self.aspect_ratio;
158        // A box with an explicit size keeps it: it never shrinks to make room.
159        if self.width.is_some() || self.height.is_some() {
160            style.flex_shrink = 0.0;
161        }
162        if self.wrap {
163            style.flex_wrap = taffy::FlexWrap::Wrap;
164        }
165        if let Some((x, y)) = self.absolute {
166            style.position = taffy::Position::Absolute;
167            style.inset = taffy::Rect {
168                left: LengthPercentageAuto::length(x),
169                top: LengthPercentageAuto::length(y),
170                right: LengthPercentageAuto::auto(),
171                bottom: LengthPercentageAuto::auto(),
172            };
173        }
174        if let Some(columns) = self.grid_columns {
175            style.display = taffy::Display::Grid;
176            style.grid_template_columns = taffy::style_helpers::evenly_sized_tracks(columns);
177            style.align_items = taffy::AlignItems::STRETCH;
178        }
179        if self.overflow_hidden {
180            style.overflow = taffy::Point { x: taffy::Overflow::Scroll, y: taffy::Overflow::Scroll };
181            style.scrollbar_width = 0.0;
182        }
183        // Text and other measured leaves must be allowed to shrink below their
184        // max-content width so they can wrap.
185        if self.axis.is_none() {
186            style.min_size.width = LengthPercentageAuto::length(self.min_width.unwrap_or(0.0));
187            style.margin = taffy::Rect {
188                left: LengthPercentageAuto::length(0.0),
189                right: LengthPercentageAuto::length(0.0),
190                top: LengthPercentageAuto::length(0.0),
191                bottom: LengthPercentageAuto::length(0.0),
192            };
193        }
194        style
195    }
196}
197
198/// Space offered to a measured leaf along one axis.
199#[derive(Clone, Copy, Debug, PartialEq)]
200#[non_exhaustive]
201#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
202pub enum Available {
203    /// A definite number of logical pixels.
204    Definite(f32),
205    /// Lay out as narrow as possible (e.g. wrap at every opportunity).
206    MinContent,
207    /// Lay out without constraints (e.g. never wrap).
208    MaxContent,
209}
210
211#[cfg_attr(feature = "live", ravenrs_live::expose)]
212impl Available {
213    /// The width to wrap at, if any.
214    pub fn limit(self) -> Option<f32> {
215        match self {
216            Self::Definite(v) => Some(v),
217            Self::MinContent => Some(0.0),
218            Self::MaxContent => None,
219        }
220    }
221}
222
223/// What a measured leaf is asked.
224#[derive(Clone, Copy, Debug, PartialEq)]
225#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
226pub struct MeasureInput {
227    /// Dimensions already fixed by the parent.
228    pub known_width: Option<f32>,
229    /// Height already fixed by the parent, if any.
230    pub known_height: Option<f32>,
231    /// Horizontal space offered by the parent.
232    pub available_width: Available,
233    /// Vertical space offered by the parent.
234    pub available_height: Available,
235}
236
237/// A failure reported by the underlying Taffy tree, e.g. an unknown node id.
238#[derive(Debug)]
239#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
240pub struct LayoutError(taffy::TaffyError);
241
242impl fmt::Display for LayoutError {
243    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244        write!(f, "layout error: {}", self.0)
245    }
246}
247
248impl std::error::Error for LayoutError {}
249
250impl From<taffy::TaffyError> for LayoutError {
251    fn from(e: taffy::TaffyError) -> Self {
252        Self(e)
253    }
254}
255
256/// A layout tree whose measured leaves carry a context `C`.
257pub struct LayoutTree<C> {
258    taffy: TaffyTree<C>,
259    /// Nodes whose `align_self` is [`Align::Fit`], until their parent is known.
260    fit: std::collections::HashSet<LayoutId>,
261}
262
263impl<C> Default for LayoutTree<C> {
264    fn default() -> Self {
265        Self::new()
266    }
267}
268
269impl<C> fmt::Debug for LayoutTree<C> {
270    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271        f.debug_struct("LayoutTree").field("nodes", &self.taffy.total_node_count()).finish()
272    }
273}
274
275fn to_available(a: AvailableSpace) -> Available {
276    match a {
277        AvailableSpace::Definite(v) => Available::Definite(v),
278        AvailableSpace::MinContent => Available::MinContent,
279        AvailableSpace::MaxContent => Available::MaxContent,
280    }
281}
282
283#[cfg_attr(feature = "live", ravenrs_live::expose)]
284impl<C> LayoutTree<C> {
285    /// Creates an empty tree.
286    ///
287    /// ```
288    /// use ravenrs_core::Size;
289    /// use ravenrs_layout::{Axis, LayoutStyle, LayoutTree};
290    ///
291    /// let mut tree = LayoutTree::<()>::new();
292    /// let child = tree.node(LayoutStyle { height: Some(20.0), ..LayoutStyle::leaf() }, &[]).unwrap();
293    /// let root = tree.node(LayoutStyle { spacing: 8.0, ..LayoutStyle::stack(Axis::Vertical) }, &[child]).unwrap();
294    /// tree.compute(root, Size::new(200.0, 100.0), |_, _| Size::ZERO).unwrap();
295    /// assert_eq!(tree.frame(child).unwrap().height(), 20.0);
296    /// ```
297    pub fn new() -> Self {
298        Self { taffy: TaffyTree::new(), fit: std::collections::HashSet::new() }
299    }
300
301    /// Removes every node, keeping allocations for the next build.
302    pub fn clear(&mut self) {
303        self.taffy.clear();
304        self.fit.clear();
305    }
306
307    /// Adds a container (or a fixed-size leaf) with the given children.
308    pub fn node(&mut self, style: LayoutStyle, children: &[LayoutId]) -> Result<LayoutId, LayoutError> {
309        if matches!(style.align, Align::Center | Align::End) {
310            for &child in children {
311                if self.fit.contains(&child) {
312                    let mut own = self.taffy.style(child)?.clone();
313                    own.align_self = None;
314                    self.taffy.set_style(child, own)?;
315                }
316            }
317        }
318        let id = self.taffy.new_with_children(style.to_taffy(), children)?;
319        if style.align_self == Some(Align::Fit) {
320            self.fit.insert(id);
321        }
322        Ok(id)
323    }
324
325    /// Adds a leaf whose size comes from the measure callback given to [`Self::compute`].
326    pub fn measured_leaf(&mut self, style: LayoutStyle, context: C) -> Result<LayoutId, LayoutError> {
327        Ok(self.taffy.new_leaf_with_context(style.to_taffy(), context)?)
328    }
329
330    /// Computes the layout of `root` inside `available` logical pixels.
331    pub fn compute(
332        &mut self,
333        root: LayoutId,
334        available: Size,
335        mut measure: impl FnMut(&mut C, MeasureInput) -> Size,
336    ) -> Result<(), LayoutError> {
337        let space = taffy::Size {
338            width: AvailableSpace::Definite(available.width),
339            height: AvailableSpace::Definite(available.height),
340        };
341        self.taffy.compute_layout_with_measure(root, space, |inputs, _id, context, style| {
342            taffy::compute_leaf_layout(
343                inputs,
344                style,
345                |_, _| 0.0,
346                |known, avail| {
347                    let Some(ctx) = context else {
348                        return taffy::Size::ZERO;
349                    };
350                    let size = measure(
351                        ctx,
352                        MeasureInput {
353                            known_width: known.width,
354                            known_height: known.height,
355                            available_width: to_available(avail.width),
356                            available_height: to_available(avail.height),
357                        },
358                    );
359                    taffy::Size { width: size.width, height: size.height }
360                },
361            )
362        })?;
363        Ok(())
364    }
365
366    /// The node's frame relative to its parent's origin.
367    pub fn frame(&self, node: LayoutId) -> Result<Rect, LayoutError> {
368        let l = self.taffy.layout(node)?;
369        Ok(Rect::from_origin_size(
370            Point::new(l.location.x, l.location.y),
371            Size::new(l.size.width, l.size.height),
372        ))
373    }
374
375    /// The content box (frame minus padding) relative to the node's own origin.
376    pub fn content_box(&self, node: LayoutId) -> Result<Rect, LayoutError> {
377        let l = self.taffy.layout(node)?;
378        Ok(Rect::new(
379            l.padding.left,
380            l.padding.top,
381            (l.size.width - l.padding.left - l.padding.right).max(0.0),
382            (l.size.height - l.padding.top - l.padding.bottom).max(0.0),
383        ))
384    }
385
386    /// The size of the node's content, which can exceed its frame when it overflows.
387    pub fn content_size(&self, node: LayoutId) -> Result<Size, LayoutError> {
388        let l = self.taffy.layout(node)?;
389        let r = l.scrollable_overflow_rect;
390        Ok(Size::new(r.right.max(0.0), r.bottom.max(0.0)))
391    }
392
393    /// Mutable access to a measured leaf's context, or `None` for nodes without one.
394    pub fn context_mut(&mut self, node: LayoutId) -> Option<&mut C> {
395        self.taffy.get_node_context_mut(node)
396    }
397
398    /// Total number of nodes in the tree.
399    pub fn node_count(&self) -> usize {
400        self.taffy.total_node_count()
401    }
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407
408    fn fixed(tree: &mut LayoutTree<()>, w: f32, h: f32) -> LayoutId {
409        let style = LayoutStyle { width: Some(w), height: Some(h), ..LayoutStyle::leaf() };
410        tree.node(style, &[]).unwrap()
411    }
412
413    #[test]
414    fn vstack_applies_padding_and_spacing() {
415        let mut tree = LayoutTree::<()>::new();
416        let a = fixed(&mut tree, 100.0, 20.0);
417        let b = fixed(&mut tree, 50.0, 30.0);
418        let style = LayoutStyle {
419            spacing: 24.0,
420            padding: Insets::uniform(32.0),
421            width: Some(800.0),
422            height: Some(600.0),
423            ..LayoutStyle::stack(Axis::Vertical)
424        };
425        let root = tree.node(style, &[a, b]).unwrap();
426        tree.compute(root, Size::new(800.0, 600.0), |_, _| Size::ZERO).unwrap();
427        assert_eq!(tree.frame(a).unwrap(), Rect::new(32.0, 32.0, 100.0, 20.0));
428        assert_eq!(tree.frame(b).unwrap(), Rect::new(32.0, 76.0, 50.0, 30.0));
429        assert_eq!(tree.frame(root).unwrap().size, Size::new(800.0, 600.0));
430    }
431
432    #[test]
433    fn absolute_children_sit_at_their_offset_outside_the_flow() {
434        let mut tree = LayoutTree::<()>::new();
435        let flow = fixed(&mut tree, 40.0, 10.0);
436        let pinned = tree
437            .node(
438                LayoutStyle {
439                    width: Some(30.0),
440                    height: Some(12.0),
441                    absolute: Some((50.0, 70.0)),
442                    ..LayoutStyle::leaf()
443                },
444                &[],
445            )
446            .unwrap();
447        let style = LayoutStyle {
448            padding: Insets::uniform(8.0),
449            width: Some(200.0),
450            height: Some(100.0),
451            ..LayoutStyle::stack(Axis::Vertical)
452        };
453        let root = tree.node(style, &[pinned, flow]).unwrap();
454        tree.compute(root, Size::new(200.0, 100.0), |_, _| Size::ZERO).unwrap();
455        assert_eq!(tree.frame(pinned).unwrap(), Rect::new(50.0, 70.0, 30.0, 12.0));
456        assert_eq!(tree.frame(flow).unwrap().origin, Point::new(8.0, 8.0), "the flow ignores absolute boxes");
457    }
458
459    #[test]
460    fn hstack_centers_on_cross_axis() {
461        let mut tree = LayoutTree::<()>::new();
462        let a = fixed(&mut tree, 10.0, 10.0);
463        let b = fixed(&mut tree, 10.0, 30.0);
464        let style = LayoutStyle {
465            spacing: 5.0,
466            align: Align::Center,
467            height: Some(100.0),
468            ..LayoutStyle::stack(Axis::Horizontal)
469        };
470        let root = tree.node(style, &[a, b]).unwrap();
471        tree.compute(root, Size::new(100.0, 100.0), |_, _| Size::ZERO).unwrap();
472        assert_eq!(tree.frame(b).unwrap().origin, Point::new(15.0, 35.0));
473        assert_eq!(tree.frame(a).unwrap().origin, Point::new(0.0, 45.0));
474    }
475
476    #[test]
477    fn explicit_sizes_do_not_shrink_next_to_overflowing_content() {
478        let mut tree = LayoutTree::<()>::new();
479        let header = tree
480            .node(LayoutStyle { height: Some(40.0), overflow_hidden: true, ..LayoutStyle::leaf() }, &[])
481            .unwrap();
482        let tall = fixed(&mut tree, 10.0, 5000.0);
483        let body = tree
484            .node(
485                LayoutStyle {
486                    grow: 1.0,
487                    min_height: Some(0.0),
488                    overflow_hidden: true,
489                    ..LayoutStyle::stack(Axis::Vertical)
490                },
491                &[tall],
492            )
493            .unwrap();
494        let root = tree
495            .node(LayoutStyle { height: Some(300.0), ..LayoutStyle::stack(Axis::Vertical) }, &[header, body])
496            .unwrap();
497        tree.compute(root, Size::new(100.0, 300.0), |_, _| Size::ZERO).unwrap();
498        assert_eq!(tree.frame(header).unwrap().size.height, 40.0);
499        let body = tree.frame(body).unwrap();
500        assert_eq!((body.y(), body.height()), (40.0, 260.0), "the overflowing body takes the rest");
501    }
502
503    #[test]
504    fn grid_splits_width_into_equal_columns() {
505        let mut tree = LayoutTree::<()>::new();
506        let cells: Vec<_> = (0..4).map(|_| fixed(&mut tree, 10.0, 10.0)).collect();
507        let style = LayoutStyle {
508            grid_columns: Some(2),
509            spacing: 10.0,
510            width: Some(210.0),
511            ..LayoutStyle::stack(Axis::Vertical)
512        };
513        let root = tree.node(style, &cells).unwrap();
514        tree.compute(root, Size::new(500.0, 500.0), |_, _| Size::ZERO).unwrap();
515        assert_eq!(tree.frame(cells[1]).unwrap().origin, Point::new(110.0, 0.0));
516        assert_eq!(tree.frame(cells[3]).unwrap().origin, Point::new(110.0, 20.0));
517    }
518
519    #[test]
520    fn measured_leaf_wraps_to_available_width() {
521        // A fake "text" of 300px that wraps into 100px-tall lines of at most the available width.
522        let mut tree = LayoutTree::<f32>::new();
523        let text = tree.measured_leaf(LayoutStyle::leaf(), 300.0).unwrap();
524        let style = LayoutStyle { align: Align::Stretch, ..LayoutStyle::stack(Axis::Vertical) };
525        let root = tree.node(LayoutStyle { width: Some(120.0), ..style }, &[text]).unwrap();
526        tree.compute(root, Size::new(500.0, 500.0), |natural, input| {
527            let limit = input.known_width.or(input.available_width.limit()).unwrap_or(*natural);
528            let width = natural.min(limit.max(1.0));
529            Size::new(width, (*natural / width).ceil() * 10.0)
530        })
531        .unwrap();
532        assert_eq!(tree.frame(text).unwrap().size, Size::new(120.0, 30.0));
533        assert_eq!(*tree.context_mut(text).unwrap(), 300.0);
534    }
535}