Skip to main content

alpha2_layout/layout/
resolve.rs

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