Skip to main content

alpha2_layout/layout/
resolve.rs

1//! Flexbox and grid resolution with taffy. This is the only file that names taffy.
2
3use super::{LAYOUT_WRAP_SLACK, NodeRects, ResolvedLayout, wrap};
4use crate::PictureSize;
5use alpha2_eval::{EvaluatedLayout, EvaluatedNode, NodeContent, NodeLayout, Sides};
6use alpha2_model::{
7    AlignContent, AlignItems, Display, FlexDirection, FlexWrap, GridAutoFlow, GridEdge,
8    GridPlacement, JustifyContent, LayoutLength, Overflow, Position, RepeatCount, TrackBreadth,
9    TrackComponent, TrackList, TrackSize,
10};
11use alpha2_text::ShapedText;
12use std::sync::Arc;
13use taffy::{
14    AvailableSpace, Dimension, GridTemplateComponent, GridTemplateRepetition, LengthPercentage,
15    LengthPercentageAuto, MaxTrackSizingFunction, MinTrackSizingFunction, NodeId, Point, Rect,
16    RepetitionCount, Size, Style, TaffyTree, TrackSizingFunction,
17};
18
19/// What a leaf measures by.
20#[derive(Copy, Clone)]
21enum NodeContext {
22    /// The text node with this index among the shaped texts.
23    Text(usize),
24    /// A shape node, an empty node, or an empty text: no size of its own.
25    Other,
26}
27
28/// Lays `layout` out on a picture of `size` pixels. `shaped` holds each text node's shaped
29/// text in preorder, `None` for empty text.
30#[allow(clippy::cast_precision_loss)]
31#[must_use]
32pub fn resolve(
33    layout: &EvaluatedLayout,
34    size: PictureSize,
35    shaped: &[Option<Arc<ShapedText>>],
36) -> ResolvedLayout {
37    let (width, height) = (size.width() as f32, size.height() as f32);
38    let unit = width.min(height);
39    let mut tree = TaffyTree::<NodeContext>::new();
40    let mut ids = Vec::new();
41    let mut texts = 0;
42    let root = build(
43        &mut tree,
44        &layout.root,
45        unit,
46        Some(Size { width, height }),
47        &mut ids,
48        &mut texts,
49    );
50    // A tree built from a valid layout always lays out; a failure leaves every rect empty.
51    let laid_out = tree
52        .compute_layout_with_measure(
53            root,
54            Size {
55                width: AvailableSpace::Definite(width),
56                height: AvailableSpace::Definite(height),
57            },
58            |inputs, _, context, style| match context {
59                Some(NodeContext::Text(index)) => {
60                    let text = shaped.get(*index).and_then(Option::as_deref);
61                    taffy::compute_leaf_layout(
62                        inputs,
63                        style,
64                        |_, _| 0.0,
65                        |known, available| {
66                            let Some(text) = text else {
67                                return Size::ZERO;
68                            };
69                            let (min, max) = (text.min_content_width(), text.max_content_width());
70                            let width = known.width.unwrap_or(match available.width {
71                                AvailableSpace::MinContent => min,
72                                AvailableSpace::MaxContent => max,
73                                AvailableSpace::Definite(space) => space.min(max).max(min),
74                            });
75                            let height = known
76                                .height
77                                .unwrap_or_else(|| text.lines_at(wrap(width)).height);
78                            Size { width, height }
79                        },
80                    )
81                }
82                _ => taffy::compute_leaf_layout(
83                    inputs,
84                    style,
85                    |_, _| 0.0,
86                    |known, _| Size {
87                        width: known.width.unwrap_or(0.0),
88                        height: known.height.unwrap_or(0.0),
89                    },
90                ),
91            },
92        )
93        .is_ok();
94    let mut nodes = Vec::with_capacity(ids.len());
95    if laid_out {
96        collect(
97            &tree,
98            &layout.root,
99            &ids,
100            [0.0, 0.0],
101            &mut 0,
102            shaped,
103            &mut 0,
104            &mut nodes,
105        );
106    } else {
107        nodes.resize(ids.len(), NodeRects::default());
108    }
109    ResolvedLayout { nodes }
110}
111
112/// Adds `node` and its subtree to `tree`, children first; `ids` gets each node's id in
113/// preorder.
114fn build(
115    tree: &mut TaffyTree<NodeContext>,
116    node: &EvaluatedNode,
117    unit: f32,
118    root_size: Option<Size<f32>>,
119    ids: &mut Vec<NodeId>,
120    texts: &mut usize,
121) -> NodeId {
122    let style = taffy_style(&node.layout, unit, root_size);
123    let slot = ids.len();
124    ids.push(NodeId::from(0_u64));
125    let id = match &node.content {
126        NodeContent::Children(children) => {
127            let children = children
128                .iter()
129                .map(|child| build(tree, child, unit, None, ids, texts))
130                .collect::<Vec<_>>();
131            tree.new_with_children(style, &children)
132        }
133        NodeContent::Text(_) => {
134            let index = *texts;
135            *texts += 1;
136            tree.new_leaf_with_context(style, NodeContext::Text(index))
137        }
138        NodeContent::Empty | NodeContent::Shape(_) => {
139            tree.new_leaf_with_context(style, NodeContext::Other)
140        }
141    }
142    .expect("taffy adds nodes without failing");
143    ids[slot] = id;
144    id
145}
146
147fn length(value: f32, unit: f32) -> LengthPercentage {
148    LengthPercentage::length(value * unit)
149}
150
151// A plan holds no pixels (they became fractions); one left over would read as a fraction.
152
153fn length_auto(value: Option<LayoutLength>, unit: f32) -> LengthPercentageAuto {
154    match value {
155        None => LengthPercentageAuto::auto(),
156        Some(LayoutLength::Fraction(value) | LayoutLength::Px(value)) => {
157            LengthPercentageAuto::length(value * unit)
158        }
159        Some(LayoutLength::Percent(value)) => LengthPercentageAuto::percent(value / 100.0),
160    }
161}
162
163fn dimension(value: Option<LayoutLength>, unit: f32) -> Dimension {
164    match value {
165        None => Dimension::auto(),
166        Some(LayoutLength::Fraction(value) | LayoutLength::Px(value)) => {
167            Dimension::length(value * unit)
168        }
169        Some(LayoutLength::Percent(value)) => Dimension::percent(value / 100.0),
170    }
171}
172
173fn min_breadth(breadth: TrackBreadth, unit: f32) -> MinTrackSizingFunction {
174    match breadth {
175        TrackBreadth::Length(LayoutLength::Fraction(value) | LayoutLength::Px(value)) => {
176            MinTrackSizingFunction::length(value * unit)
177        }
178        TrackBreadth::Length(LayoutLength::Percent(value)) => {
179            MinTrackSizingFunction::percent(value / 100.0)
180        }
181        // A minimum is never fr: the parser refuses it, as CSS does.
182        TrackBreadth::Auto | TrackBreadth::Fr(_) => MinTrackSizingFunction::auto(),
183        TrackBreadth::MinContent => MinTrackSizingFunction::min_content(),
184        TrackBreadth::MaxContent => MinTrackSizingFunction::max_content(),
185    }
186}
187
188fn max_breadth(breadth: TrackBreadth, unit: f32) -> MaxTrackSizingFunction {
189    match breadth {
190        TrackBreadth::Length(LayoutLength::Fraction(value) | LayoutLength::Px(value)) => {
191            MaxTrackSizingFunction::length(value * unit)
192        }
193        TrackBreadth::Length(LayoutLength::Percent(value)) => {
194            MaxTrackSizingFunction::percent(value / 100.0)
195        }
196        TrackBreadth::Fr(share) => MaxTrackSizingFunction::fr(share),
197        TrackBreadth::Auto => MaxTrackSizingFunction::auto(),
198        TrackBreadth::MinContent => MaxTrackSizingFunction::min_content(),
199        TrackBreadth::MaxContent => MaxTrackSizingFunction::max_content(),
200    }
201}
202
203/// A track's sizing functions as CSS reads a single breadth: a length is fixed, `<n>fr` is
204/// `minmax(auto, <n>fr)`, and a keyword sizes by content.
205fn track(size: TrackSize, unit: f32) -> TrackSizingFunction {
206    let (min, max) = match size {
207        TrackSize::Breadth(TrackBreadth::Fr(share)) => (
208            MinTrackSizingFunction::auto(),
209            MaxTrackSizingFunction::fr(share),
210        ),
211        TrackSize::Breadth(breadth) => (min_breadth(breadth, unit), max_breadth(breadth, unit)),
212        TrackSize::MinMax(min, max) => (min_breadth(min, unit), max_breadth(max, unit)),
213        TrackSize::FitContent(limit) => (
214            MinTrackSizingFunction::auto(),
215            match limit {
216                LayoutLength::Fraction(value) | LayoutLength::Px(value) => {
217                    MaxTrackSizingFunction::fit_content_px(value * unit)
218                }
219                LayoutLength::Percent(value) => {
220                    MaxTrackSizingFunction::fit_content_percent(value / 100.0)
221                }
222            },
223        ),
224    };
225    TrackSizingFunction { min, max }
226}
227
228/// An explicit track list, `repeat()` kept for taffy to expand, without line names (the
229/// model refuses them).
230fn template<S: taffy::CheapCloneStr>(
231    tracks: Option<&TrackList>,
232    unit: f32,
233) -> Vec<GridTemplateComponent<S>> {
234    tracks.map_or_else(Vec::new, |tracks| {
235        tracks
236            .0
237            .iter()
238            .map(|component| match component {
239                TrackComponent::Single(size) => GridTemplateComponent::Single(track(*size, unit)),
240                TrackComponent::Repeat(count, sizes) => {
241                    GridTemplateComponent::Repeat(GridTemplateRepetition {
242                        count: match count {
243                            RepeatCount::Count(count) => RepetitionCount::Count(*count),
244                            RepeatCount::AutoFill => RepetitionCount::AutoFill,
245                            RepeatCount::AutoFit => RepetitionCount::AutoFit,
246                        },
247                        tracks: sizes.iter().map(|size| track(*size, unit)).collect(),
248                        line_names: Vec::new(),
249                    })
250                }
251            })
252            .collect()
253    })
254}
255
256/// Implicit track sizes: the model refuses `repeat()` in them.
257fn implicit(tracks: Option<&TrackList>, unit: f32) -> Vec<TrackSizingFunction> {
258    tracks.map_or_else(Vec::new, |tracks| {
259        tracks.sizes().map(|size| track(size, unit)).collect()
260    })
261}
262
263fn placement<S: taffy::CheapCloneStr>(
264    placement: GridPlacement,
265) -> taffy::Line<taffy::GridPlacement<S>> {
266    let edge = |edge: GridEdge| match edge {
267        GridEdge::Auto => taffy::GridPlacement::Auto,
268        GridEdge::Line(line) => taffy::style_helpers::line(line),
269        GridEdge::Span(count) => taffy::GridPlacement::Span(count),
270    };
271    taffy::Line {
272        start: edge(placement.start),
273        end: edge(placement.end),
274    }
275}
276
277const fn align(value: AlignItems) -> taffy::AlignItems {
278    match value {
279        AlignItems::Start => taffy::AlignItems::START,
280        AlignItems::Center => taffy::AlignItems::CENTER,
281        AlignItems::End => taffy::AlignItems::END,
282        AlignItems::Stretch => taffy::AlignItems::STRETCH,
283    }
284}
285
286const fn align_content(value: AlignContent) -> taffy::AlignContent {
287    match value {
288        AlignContent::Start => taffy::AlignContent::START,
289        AlignContent::Center => taffy::AlignContent::CENTER,
290        AlignContent::End => taffy::AlignContent::END,
291        AlignContent::SpaceBetween => taffy::AlignContent::SPACE_BETWEEN,
292        AlignContent::SpaceAround => taffy::AlignContent::SPACE_AROUND,
293        AlignContent::SpaceEvenly => taffy::AlignContent::SPACE_EVENLY,
294        AlignContent::Stretch => taffy::AlignContent::STRETCH,
295    }
296}
297
298/// The taffy style of one node, lengths scaled by `unit` pixels. The root takes the
299/// picture's size.
300fn taffy_style(node: &NodeLayout, unit: f32, root_size: Option<Size<f32>>) -> Style {
301    let insets = |insets: Sides| Rect {
302        left: length(insets.left, unit),
303        right: length(insets.right, unit),
304        top: length(insets.top, unit),
305        bottom: length(insets.bottom, unit),
306    };
307    let margin = Rect {
308        left: LengthPercentageAuto::length(node.margin.left * unit),
309        right: LengthPercentageAuto::length(node.margin.right * unit),
310        top: LengthPercentageAuto::length(node.margin.top * unit),
311        bottom: LengthPercentageAuto::length(node.margin.bottom * unit),
312    };
313    let border = length(node.border_width, unit);
314    let [left, top, right, bottom] = node.inset;
315    let overflow = match node.overflow {
316        Overflow::Visible => taffy::Overflow::Visible,
317        Overflow::Hidden => taffy::Overflow::Hidden,
318    };
319    Style {
320        display: match node.display {
321            Display::Flex => taffy::Display::Flex,
322            Display::Grid => taffy::Display::Grid,
323        },
324        box_sizing: taffy::BoxSizing::BorderBox,
325        direction: taffy::Direction::Ltr,
326        flex_direction: match node.flex_direction {
327            FlexDirection::Row => taffy::FlexDirection::Row,
328            FlexDirection::Column => taffy::FlexDirection::Column,
329            FlexDirection::RowReverse => taffy::FlexDirection::RowReverse,
330            FlexDirection::ColumnReverse => taffy::FlexDirection::ColumnReverse,
331        },
332        flex_wrap: match node.flex_wrap {
333            FlexWrap::NoWrap => taffy::FlexWrap::NoWrap,
334            FlexWrap::Wrap => taffy::FlexWrap::Wrap,
335        },
336        gap: Size {
337            width: length(node.column_gap, unit),
338            height: length(node.row_gap, unit),
339        },
340        justify_content: match node.justify_content {
341            JustifyContent::Start => taffy::JustifyContent::START,
342            JustifyContent::Center => taffy::JustifyContent::CENTER,
343            JustifyContent::End => taffy::JustifyContent::END,
344            JustifyContent::SpaceBetween => taffy::JustifyContent::SPACE_BETWEEN,
345            JustifyContent::SpaceAround => taffy::JustifyContent::SPACE_AROUND,
346            JustifyContent::SpaceEvenly => taffy::JustifyContent::SPACE_EVENLY,
347        },
348        align_items: align(node.align_items),
349        align_self: node.align_self.map(align),
350        align_content: align_content(node.align_content),
351        padding: insets(node.padding),
352        margin,
353        border: Rect {
354            left: border,
355            right: border,
356            top: border,
357            bottom: border,
358        },
359        size: root_size.map_or_else(
360            || Size {
361                width: dimension(node.width, unit),
362                height: dimension(node.height, unit),
363            },
364            |size| Size {
365                width: Dimension::length(size.width),
366                height: Dimension::length(size.height),
367            },
368        ),
369        min_size: Size {
370            width: length_auto(node.min_width, unit),
371            height: length_auto(node.min_height, unit),
372        },
373        max_size: Size {
374            width: length_auto(node.max_width, unit),
375            height: length_auto(node.max_height, unit),
376        },
377        aspect_ratio: node.aspect_ratio,
378        flex_grow: node.flex_grow,
379        flex_shrink: node.flex_shrink,
380        flex_basis: dimension(node.flex_basis, unit),
381        // Every box is positioned, so an absolute box's containing block is its parent.
382        position: match node.position {
383            Position::Relative => taffy::Position::Relative,
384            Position::Absolute => taffy::Position::Absolute,
385        },
386        inset: Rect {
387            left: length_auto(left, unit),
388            top: length_auto(top, unit),
389            right: length_auto(right, unit),
390            bottom: length_auto(bottom, unit),
391        },
392        overflow: Point {
393            x: overflow,
394            y: overflow,
395        },
396        scrollbar_width: 0.0,
397        ..grid_style(node, unit)
398    }
399}
400
401/// The grid container and grid item fields of one node's taffy style, lengths scaled by
402/// `unit` pixels.
403fn grid_style(node: &NodeLayout, unit: f32) -> Style {
404    Style {
405        justify_items: align(node.justify_items),
406        justify_self: node.justify_self.map(align),
407        grid_template_columns: template(node.grid_template_columns.as_ref(), unit),
408        grid_template_rows: template(node.grid_template_rows.as_ref(), unit),
409        grid_auto_columns: implicit(node.grid_auto_columns.as_ref(), unit),
410        grid_auto_rows: implicit(node.grid_auto_rows.as_ref(), unit),
411        grid_auto_flow: match node.grid_auto_flow {
412            GridAutoFlow::Row => taffy::GridAutoFlow::Row,
413            GridAutoFlow::Column => taffy::GridAutoFlow::Column,
414            GridAutoFlow::RowDense => taffy::GridAutoFlow::RowDense,
415            GridAutoFlow::ColumnDense => taffy::GridAutoFlow::ColumnDense,
416        },
417        grid_column: placement(node.grid_column),
418        grid_row: placement(node.grid_row),
419        ..Style::default()
420    }
421}
422
423/// Reads each node's rects in preorder as absolute whole-pixel boxes, summed from the
424/// rounded locations (taffy rounds by absolute position).
425#[allow(clippy::too_many_arguments)]
426fn collect(
427    tree: &TaffyTree<NodeContext>,
428    node: &EvaluatedNode,
429    ids: &[NodeId],
430    origin: [f32; 2],
431    index: &mut usize,
432    shaped: &[Option<Arc<ShapedText>>],
433    texts: &mut usize,
434    out: &mut Vec<NodeRects>,
435) {
436    let id = ids[*index];
437    *index += 1;
438    let rounded = tree.layout(id).expect("laid out");
439    let unrounded = tree.unrounded_layout(id);
440    let left = origin[0] + rounded.location.x;
441    let top = origin[1] + rounded.location.y;
442    let border = [
443        left,
444        top,
445        left + rounded.size.width,
446        top + rounded.size.height,
447    ];
448    let padding = [
449        border[0] + rounded.border.left,
450        border[1] + rounded.border.top,
451        border[2] - rounded.border.right,
452        border[3] - rounded.border.bottom,
453    ];
454    let content = [
455        padding[0] + rounded.padding.left,
456        padding[1] + rounded.padding.top,
457        padding[2] - rounded.padding.right,
458        padding[3] - rounded.padding.bottom,
459    ];
460    let text_width = (unrounded.size.width
461        - unrounded.border.left
462        - unrounded.border.right
463        - unrounded.padding.left
464        - unrounded.padding.right)
465        .max(0.0);
466    // Compare with the text's own unrounded height: taffy rounds edges by absolute position,
467    // so a whole-pixel box can be up to a pixel shorter than the height it was given.
468    let text_height = (unrounded.size.height
469        - unrounded.border.top
470        - unrounded.border.bottom
471        - unrounded.padding.top
472        - unrounded.padding.bottom)
473        .max(0.0);
474    let slot = out.len();
475    out.push(NodeRects {
476        border,
477        padding,
478        content,
479        text_width,
480        overflow: false,
481        line_count: None,
482        baselines: None,
483    });
484    let mut overflow = false;
485    match &node.content {
486        NodeContent::Text(_) => {
487            let text = shaped.get(*texts).and_then(Option::as_deref);
488            *texts += 1;
489            let lines = text.map(|text| {
490                let lines = text.lines_at(wrap(text_width));
491                overflow = lines.height > text_height + LAYOUT_WRAP_SLACK
492                    || text.min_content_width() > wrap(text_width);
493                lines
494            });
495            out[slot].line_count = Some(lines.map_or(0, |lines| lines.line_count));
496            out[slot].baselines = lines.map(|lines| {
497                [
498                    content[1] + lines.first_baseline,
499                    content[1] + lines.last_baseline,
500                ]
501            });
502        }
503        NodeContent::Children(children) => {
504            for child in children {
505                let at = out.len();
506                collect(tree, child, ids, [left, top], index, shaped, texts, out);
507                if child.layout.position == Position::Relative {
508                    let [l, t, r, b] = out[at].border;
509                    overflow |= l < padding[0] - 0.5
510                        || t < padding[1] - 0.5
511                        || r > padding[2] + 0.5
512                        || b > padding[3] + 0.5;
513                }
514            }
515        }
516        NodeContent::Empty | NodeContent::Shape(_) => {}
517    }
518    out[slot].overflow = overflow;
519}