1mod 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
26pub const LAYOUT_WRAP_SLACK: f32 = 1.0 / 64.0;
32
33pub(crate) fn wrap(width: f32) -> f32 {
36 (width + LAYOUT_WRAP_SLACK).max(1.0)
37}
38
39#[derive(Copy, Clone, Debug, Default, PartialEq)]
42pub struct NodeRects {
43 pub border: [f32; 4],
45 pub padding: [f32; 4],
47 pub content: [f32; 4],
49 pub text_width: f32,
51 pub overflow: bool,
53 pub line_count: Option<u32>,
55 pub baselines: Option<[f32; 2]>,
58}
59
60#[derive(Clone, Debug, Default, PartialEq)]
62pub struct ResolvedLayout {
63 pub nodes: Vec<NodeRects>,
65}
66
67#[derive(Clone, Debug, PartialEq)]
69pub struct LayoutBounds {
70 pub nodes: Vec<NodeBounds>,
72}
73
74#[derive(Clone, Debug, PartialEq)]
77pub struct NodeBounds {
78 pub at: String,
80 pub border: CanvasRect,
82 pub content: CanvasRect,
84 pub overflow: bool,
87 pub line_count: Option<u32>,
89 pub baselines: Option<Baselines>,
93}
94
95pub(crate) fn font_pixels(text: &EvaluatedText, unit: f32) -> f32 {
98 (text.font_size * unit).clamp(1.0, MAX_FONT_PIXELS)
99}
100
101pub(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
133pub(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
147pub 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#[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#[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}