Skip to main content

alpha2_engine/layout/
mod.rs

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