1mod 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
24pub const LAYOUT_WRAP_SLACK: f32 = 1.0 / 64.0;
28
29pub(crate) fn wrap(width: f32) -> f32 {
31 (width + LAYOUT_WRAP_SLACK).max(1.0)
32}
33
34#[derive(Copy, Clone, Debug, Default, PartialEq)]
37pub struct NodeRects {
38 pub border: [f32; 4],
40 pub padding: [f32; 4],
42 pub content: [f32; 4],
44 pub text_width: f32,
46 pub overflow: bool,
48 pub line_count: Option<u32>,
50 pub baselines: Option<[f32; 2]>,
53}
54
55#[derive(Clone, Debug, Default, PartialEq)]
57pub struct ResolvedLayout {
58 pub nodes: Vec<NodeRects>,
60}
61
62#[derive(Clone, Debug, PartialEq)]
64pub struct LayoutBounds {
65 pub nodes: Vec<NodeBounds>,
67}
68
69#[derive(Clone, Debug, PartialEq)]
71pub struct NodeBounds {
72 pub at: String,
74 pub border: CanvasRect,
76 pub content: CanvasRect,
78 pub overflow: bool,
81 pub line_count: Option<u32>,
83 pub baselines: Option<Baselines>,
86}
87
88pub(crate) fn font_pixels(text: &EvaluatedText, unit: f32) -> f32 {
91 (text.font_size * unit).clamp(1.0, MAX_FONT_PIXELS)
92}
93
94pub(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
124pub(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
138pub 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#[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#[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}