mod resolve;
#[cfg(test)]
mod tests;
use crate::text::{MAX_FONT_PIXELS, TextDraw, layout_text_key, stroke_pixels, text_fill};
use crate::{Baselines, CanvasRect, PictureSize};
use alpha2_eval::{EvaluatedLayout, EvaluatedNode, EvaluatedText, LayoutPlan, NodeContent};
use alpha2_media::EmojiPolicy;
use alpha2_model::{LayoutNode, TextAlign};
use alpha2_text::{ShapedText, TextRasterError, TextRasterRequest, TextRasterizer};
use alpha2_time::MediaTime;
use std::sync::Arc;
pub use resolve::resolve;
pub const LAYOUT_WRAP_SLACK: f32 = 1.0 / 64.0;
pub(crate) fn wrap(width: f32) -> f32 {
(width + LAYOUT_WRAP_SLACK).max(1.0)
}
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub struct NodeRects {
pub border: [f32; 4],
pub padding: [f32; 4],
pub content: [f32; 4],
pub text_width: f32,
pub overflow: bool,
pub line_count: Option<u32>,
pub baselines: Option<[f32; 2]>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ResolvedLayout {
pub nodes: Vec<NodeRects>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct LayoutBounds {
pub nodes: Vec<NodeBounds>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct NodeBounds {
pub at: String,
pub border: CanvasRect,
pub content: CanvasRect,
pub overflow: bool,
pub line_count: Option<u32>,
pub baselines: Option<Baselines>,
}
pub(crate) fn font_pixels(text: &EvaluatedText, unit: f32) -> f32 {
(text.font_size * unit).clamp(1.0, MAX_FONT_PIXELS)
}
pub(crate) fn text_request(
text: &EvaluatedText,
unit: f32,
max_width: f32,
content_version: u64,
) -> TextRasterRequest {
let font_size = font_pixels(text, unit);
TextRasterRequest {
text: text.content.clone(),
font_size,
max_width,
line_height: text.line_height,
align: match text.text_align {
TextAlign::Start => alpha2_media::TextAlign::Start,
TextAlign::Center => alpha2_media::TextAlign::Center,
TextAlign::End => alpha2_media::TextAlign::End,
},
fill: text_fill(&text.fill),
emoji_policy: EmojiPolicy::Color,
content_version,
strokes: stroke_pixels(&text.strokes, font_size),
background: None,
split_background: false,
font_family: text.font_family.clone(),
letter_spacing: text.letter_spacing * font_size,
font_weight: text.font_weight,
}
}
pub(crate) fn texts<'a>(node: &'a EvaluatedNode, out: &mut Vec<Option<&'a EvaluatedText>>) {
match &node.content {
NodeContent::Text(text) => out.push(Some(text).filter(|text| !text.content.is_empty())),
NodeContent::Children(children) => {
for child in children {
texts(child, out);
}
}
NodeContent::Empty | NodeContent::Shape(_) => {}
}
}
pub fn shape_texts(
layout: &EvaluatedLayout,
size: PictureSize,
mut shape: impl FnMut(&TextRasterRequest) -> Result<Arc<ShapedText>, TextRasterError>,
) -> Result<Vec<Option<Arc<ShapedText>>>, TextRasterError> {
let unit = size.unit();
let mut list = Vec::new();
texts(&layout.root, &mut list);
list.into_iter()
.map(|text| {
text.map(|text| shape(&text_request(text, unit, 0.0, 1)))
.transpose()
})
.collect()
}
#[must_use]
pub fn layout_texts(
layout: &EvaluatedLayout,
resolved: &ResolvedLayout,
size: PictureSize,
) -> (Vec<Option<u64>>, Vec<(u64, TextDraw)>) {
fn walk<'a>(
node: &'a EvaluatedNode,
index: &mut usize,
out: &mut Vec<(usize, Option<&'a EvaluatedText>)>,
) {
let at = *index;
*index += 1;
match &node.content {
NodeContent::Text(text) => {
out.push((at, Some(text).filter(|text| !text.content.is_empty())));
}
NodeContent::Children(children) => {
for child in children {
walk(child, index, out);
}
}
NodeContent::Empty | NodeContent::Shape(_) => {}
}
}
let unit = size.unit();
let mut found = Vec::new();
walk(&layout.root, &mut 0, &mut found);
let mut keys = Vec::with_capacity(found.len());
let mut requests: Vec<(u64, TextDraw)> = Vec::new();
for (node, text) in found {
let Some(text) = text else {
keys.push(None);
continue;
};
let width = resolved
.nodes
.get(node)
.map_or(0.0, |rects| rects.text_width);
let mut request = text_request(text, unit, wrap(width), 1);
let shadows = crate::text::shadow_pixels(&text.shadows, request.font_size);
let key = layout_text_key(&request, &shadows, &text.animators);
request.content_version = key;
if !requests.iter().any(|(held, _)| *held == key) {
let animation = (!text.animators.is_empty()).then(|| crate::TextAnimation {
animators: text.animators.clone(),
runs: Vec::new(),
time: layout.time,
});
requests.push((
key,
TextDraw {
request,
shadows,
animation,
},
));
}
keys.push(Some(key));
}
(keys, requests)
}
#[allow(clippy::cast_precision_loss)]
pub fn measure_layout(
rasterizer: &TextRasterizer,
root: &LayoutNode,
time: MediaTime,
size: PictureSize,
canvas: (u32, u32),
) -> Result<LayoutBounds, TextRasterError> {
let evaluated = LayoutPlan::new(root, canvas).evaluate(time);
let shaped = shape_texts(&evaluated, size, |request| {
rasterizer.shape(request).map(Arc::new)
})?;
let resolved = resolve(&evaluated, size, &shaped);
let (width, height) = (size.width() as f32, size.height() as f32);
let rect = |[left, top, right, bottom]: [f32; 4]| CanvasRect {
left: left / width,
top: top / height,
right: right / width,
bottom: bottom / height,
};
let mut paths = Vec::new();
root.walk(|at, _| paths.push(at.to_owned()));
Ok(LayoutBounds {
nodes: paths
.into_iter()
.zip(&resolved.nodes)
.map(|(at, node)| NodeBounds {
at,
border: rect(node.border),
content: rect(node.content),
overflow: node.overflow,
line_count: node.line_count,
baselines: node.baselines.map(|[first, last]| Baselines {
first: first / height,
last: last / height,
}),
})
.collect(),
})
}