1pub(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
30pub const LAYOUT_WRAP_SLACK: f32 = 1.0 / 64.0;
36
37pub(crate) fn wrap(width: f32) -> f32 {
40 (width + LAYOUT_WRAP_SLACK).max(1.0)
41}
42
43#[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 pub(crate) text_width: f32,
52 pub(crate) overflow: bool,
53 pub(crate) line_count: Option<u32>,
55}
56
57#[derive(Clone, Debug, Default, PartialEq)]
59pub(crate) struct ResolvedLayout {
60 pub(crate) nodes: Vec<NodeRects>,
61}
62
63#[derive(Clone, Debug, PartialEq)]
65pub struct LayoutBounds {
66 pub nodes: Vec<NodeBounds>,
68}
69
70#[derive(Clone, Debug, PartialEq)]
73pub struct NodeBounds {
74 pub at: String,
76 pub border: CanvasRect,
78 pub content: CanvasRect,
80 pub overflow: bool,
83 pub line_count: Option<u32>,
85}
86
87pub(crate) fn font_pixels(text: &EvaluatedText, unit: f32) -> f32 {
90 (text.font_size * unit).clamp(1.0, MAX_FONT_PIXELS)
91}
92
93pub(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#[allow(clippy::cast_precision_loss)]
126pub(crate) fn unit_of(size: RenderSize) -> f32 {
127 size.width.min(size.height) as f32
128}
129
130pub(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
144pub(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
162pub(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#[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}