Skip to main content

alpha2_layout/layout/
mod.rs

1//! Layout generators: box trees laid out by CSS flexbox and grid with the
2//! fonts the engine draws with.
3//!
4//! A layout is resolved where text is prepared, which holds the fonts, so
5//! its text is measured and drawn by the same code. It is resolved at the
6//! render size, in render pixels, and rounded to whole pixels; a text node
7//! breaks its lines at its unrounded content width plus
8//! [`LAYOUT_WRAP_SLACK`], when measured and when drawn alike.
9
10mod resolve;
11
12#[cfg(test)]
13mod tests;
14
15use crate::text::{MAX_FONT_PIXELS, MAX_OUTLINE_PIXELS, layout_text_key, rgba8};
16use crate::{Baselines, CanvasRect, PictureSize};
17use alpha2_eval::{EvaluatedLayout, EvaluatedNode, EvaluatedText, LayoutPlan, NodeContent};
18use alpha2_media::EmojiPolicy;
19use alpha2_model::{LayoutNode, TextAlign};
20use alpha2_text::{ShapedText, TextRasterError, TextRasterRequest, TextRasterizer};
21use alpha2_time::MediaTime;
22use std::sync::Arc;
23
24pub use resolve::resolve;
25
26/// What a text node's wrapping width adds to its content width, in pixels:
27/// float summation order cannot then break a text sized to its own
28/// max-content width again. A line it lets through overflows its box by
29/// under this much, and a text taller than its box by under this much
30/// still fits it.
31pub const LAYOUT_WRAP_SLACK: f32 = 1.0 / 64.0;
32
33/// The width a text node with content `width` pixels wide breaks its lines
34/// at, when measured and when drawn.
35pub(crate) fn wrap(width: f32) -> f32 {
36    (width + LAYOUT_WRAP_SLACK).max(1.0)
37}
38
39/// One node's boxes in render pixels, `[left, top, right, bottom]`, whole
40/// pixels from the picture's top-left corner.
41#[derive(Copy, Clone, Debug, Default, PartialEq)]
42pub struct NodeRects {
43    /// The border box.
44    pub border: [f32; 4],
45    /// The padding box.
46    pub padding: [f32; 4],
47    /// The content box.
48    pub content: [f32; 4],
49    /// The unrounded content width a text breaks at, before the slack.
50    pub text_width: f32,
51    /// Whether it overflows, as [`NodeBounds::overflow`] says.
52    pub overflow: bool,
53    /// A text node's line count, zero for empty text.
54    pub line_count: Option<u32>,
55    /// A text node's first and last baselines in pixels from the picture's
56    /// top, `[first, last]`; `None` for other nodes and empty text.
57    pub baselines: Option<[f32; 2]>,
58}
59
60/// Every node's boxes, in preorder ([`resolve`]).
61#[derive(Clone, Debug, Default, PartialEq)]
62pub struct ResolvedLayout {
63    /// Each node, the root first.
64    pub nodes: Vec<NodeRects>,
65}
66
67/// Where a layout's nodes lie ([`measure_layout`]), in preorder.
68#[derive(Clone, Debug, PartialEq)]
69pub struct LayoutBounds {
70    /// Each node, the root first.
71    pub nodes: Vec<NodeBounds>,
72}
73
74/// Where one layout node lies, before its clip's transform and before node
75/// visuals: its layout boxes, not where it is drawn.
76#[derive(Clone, Debug, PartialEq)]
77pub struct NodeBounds {
78    /// Its path from the root, like `root.children[1]`.
79    pub at: String,
80    /// The border box, as fractions of the picture.
81    pub border: CanvasRect,
82    /// The content box, as fractions of the picture.
83    pub content: CanvasRect,
84    /// Whether it overflows: its text is taller than its content box or
85    /// splits a word, or an in-flow child reaches past its padding box.
86    pub overflow: bool,
87    /// A text node's line count; `None` for other nodes.
88    pub line_count: Option<u32>,
89    /// A text node's first and last baselines, as fractions of the picture's
90    /// height: its content box's top plus its text's baselines. `None` for
91    /// other nodes and empty text.
92    pub baselines: Option<Baselines>,
93}
94
95/// The font size in pixels of `text` on a picture whose shorter side is
96/// `unit` pixels, within what the text host draws.
97pub(crate) fn font_pixels(text: &EvaluatedText, unit: f32) -> f32 {
98    (text.font_size * unit).clamp(1.0, MAX_FONT_PIXELS)
99}
100
101/// The text host request for a layout's text, wrapped at `max_width`.
102pub(crate) fn text_request(
103    text: &EvaluatedText,
104    unit: f32,
105    max_width: f32,
106    content_version: u64,
107) -> TextRasterRequest {
108    let font_size = font_pixels(text, unit);
109    TextRasterRequest {
110        text: text.content.clone(),
111        font_size,
112        max_width,
113        line_height: text.line_height,
114        align: match text.text_align {
115            TextAlign::Start => alpha2_media::TextAlign::Start,
116            TextAlign::Center => alpha2_media::TextAlign::Center,
117            TextAlign::End => alpha2_media::TextAlign::End,
118        },
119        color: rgba8(text.color),
120        emoji_policy: EmojiPolicy::Color,
121        content_version,
122        outline: text.outline.map(|outline| alpha2_text::TextOutline {
123            color: rgba8(outline.color),
124            width: (outline.width * font_size).clamp(f32::MIN_POSITIVE, MAX_OUTLINE_PIXELS),
125        }),
126        background: None,
127        font_family: text.font_family.clone(),
128        letter_spacing: text.letter_spacing * font_size,
129        font_weight: text.font_weight,
130    }
131}
132
133/// Each text node's text in preorder: `None` for empty text, which measures
134/// nothing and draws nothing.
135pub(crate) fn texts<'a>(node: &'a EvaluatedNode, out: &mut Vec<Option<&'a EvaluatedText>>) {
136    match &node.content {
137        NodeContent::Text(text) => out.push(Some(text).filter(|text| !text.content.is_empty())),
138        NodeContent::Children(children) => {
139            for child in children {
140                texts(child, out);
141            }
142        }
143        NodeContent::Empty | NodeContent::Shape(_) => {}
144    }
145}
146
147/// Each text node's shaped text, in preorder, shaped by `shape` on a
148/// picture of `size`: what [`resolve`] lays the texts out with.
149///
150/// # Errors
151/// Returns the first refusal of `shape`.
152pub fn shape_texts(
153    layout: &EvaluatedLayout,
154    size: PictureSize,
155    mut shape: impl FnMut(&TextRasterRequest) -> Result<Arc<ShapedText>, TextRasterError>,
156) -> Result<Vec<Option<Arc<ShapedText>>>, TextRasterError> {
157    let unit = size.unit();
158    let mut list = Vec::new();
159    texts(&layout.root, &mut list);
160    list.into_iter()
161        .map(|text| {
162            text.map(|text| shape(&text_request(text, unit, 0.0, 1)))
163                .transpose()
164        })
165        .collect()
166}
167
168/// Each text node's store key in preorder (`None` for empty text), and each
169/// distinct key's raster request: wrapped at its unrounded content width
170/// plus the slack, as it was measured.
171#[must_use]
172pub fn layout_texts(
173    layout: &EvaluatedLayout,
174    resolved: &ResolvedLayout,
175    size: PictureSize,
176) -> (Vec<Option<u64>>, Vec<(u64, TextRasterRequest)>) {
177    fn walk<'a>(
178        node: &'a EvaluatedNode,
179        index: &mut usize,
180        out: &mut Vec<(usize, Option<&'a EvaluatedText>)>,
181    ) {
182        let at = *index;
183        *index += 1;
184        match &node.content {
185            NodeContent::Text(text) => {
186                out.push((at, Some(text).filter(|text| !text.content.is_empty())));
187            }
188            NodeContent::Children(children) => {
189                for child in children {
190                    walk(child, index, out);
191                }
192            }
193            NodeContent::Empty | NodeContent::Shape(_) => {}
194        }
195    }
196    let unit = size.unit();
197    let mut found = Vec::new();
198    walk(&layout.root, &mut 0, &mut found);
199    let mut keys = Vec::with_capacity(found.len());
200    let mut requests: Vec<(u64, TextRasterRequest)> = Vec::new();
201    for (node, text) in found {
202        let Some(text) = text else {
203            keys.push(None);
204            continue;
205        };
206        let width = resolved
207            .nodes
208            .get(node)
209            .map_or(0.0, |rects| rects.text_width);
210        let mut request = text_request(text, unit, wrap(width), 1);
211        let key = layout_text_key(&request);
212        request.content_version = key;
213        if !requests.iter().any(|(held, _)| *held == key) {
214            requests.push((key, request));
215        }
216        keys.push(Some(key));
217    }
218    (keys, requests)
219}
220
221/// Where each node of a valid layout tree lies on a picture of `size`, at
222/// picture time `time`, laid out with `rasterizer`'s fonts as a frame lays it
223/// out. `canvas` is the project canvas, which the tree's pixel lengths are
224/// pixels of (the picture's own size when they are measured at it). Rects are
225/// before the clip's transform and before node visuals: layout boxes, not
226/// where they are drawn. An invalid tree gives unspecified rects, but never
227/// panics.
228///
229/// # Errors
230/// Returns the text host's refusal of a text: no fonts, or a size out of
231/// range.
232#[allow(clippy::cast_precision_loss)]
233pub fn measure_layout(
234    rasterizer: &TextRasterizer,
235    root: &LayoutNode,
236    time: MediaTime,
237    size: PictureSize,
238    canvas: (u32, u32),
239) -> Result<LayoutBounds, TextRasterError> {
240    let evaluated = LayoutPlan::new(root, canvas).evaluate(time);
241    let shaped = shape_texts(&evaluated, size, |request| {
242        rasterizer.shape(request).map(Arc::new)
243    })?;
244    let resolved = resolve(&evaluated, size, &shaped);
245    let (width, height) = (size.width() as f32, size.height() as f32);
246    let rect = |[left, top, right, bottom]: [f32; 4]| CanvasRect {
247        left: left / width,
248        top: top / height,
249        right: right / width,
250        bottom: bottom / height,
251    };
252    let mut paths = Vec::new();
253    root.walk(|at, _| paths.push(at.to_owned()));
254    Ok(LayoutBounds {
255        nodes: paths
256            .into_iter()
257            .zip(&resolved.nodes)
258            .map(|(at, node)| NodeBounds {
259                at,
260                border: rect(node.border),
261                content: rect(node.content),
262                overflow: node.overflow,
263                line_count: node.line_count,
264                baselines: node.baselines.map(|[first, last]| Baselines {
265                    first: first / height,
266                    last: last / height,
267                }),
268            })
269            .collect(),
270    })
271}