Skip to main content

alpha2_layout/layout/
mod.rs

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