pub(crate) mod draw;
pub use draw::PreparedLayout;
pub(crate) mod paint;
mod resolve;
#[cfg(test)]
mod tests;
use crate::text::{MAX_FONT_PIXELS, MAX_OUTLINE_PIXELS, rgba8};
use crate::{CanvasRect, TextError};
use alpha2_eval::{EvaluatedLayout, EvaluatedNode, EvaluatedText, LayoutPlan, NodeContent};
use alpha2_media::EmojiPolicy;
use alpha2_model::{LayoutNode, TextAlign};
use alpha2_render::RenderSize;
use alpha2_text::{ShapedText, TextRasterError, TextRasterRequest, TextRasterizer};
use alpha2_time::MediaTime;
use std::sync::Arc;
pub(crate) 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(crate) struct NodeRects {
pub(crate) border: [f32; 4],
pub(crate) padding: [f32; 4],
pub(crate) content: [f32; 4],
pub(crate) text_width: f32,
pub(crate) overflow: bool,
pub(crate) line_count: Option<u32>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct ResolvedLayout {
pub(crate) 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(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,
},
color: rgba8(text.color),
emoji_policy: EmojiPolicy::Color,
content_version,
outline: text.outline.map(|outline| alpha2_text::TextOutline {
color: rgba8(outline.color),
width: (outline.width * font_size).clamp(f32::MIN_POSITIVE, MAX_OUTLINE_PIXELS),
}),
background: None,
font_family: text.font_family.clone(),
}
}
#[allow(clippy::cast_precision_loss)]
pub(crate) fn unit_of(size: RenderSize) -> f32 {
size.width.min(size.height) as f32
}
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(crate) fn shape_texts(
layout: &EvaluatedLayout,
size: RenderSize,
mut shape: impl FnMut(&TextRasterRequest) -> Result<Arc<ShapedText>, TextRasterError>,
) -> Result<Vec<Option<Arc<ShapedText>>>, TextError> {
let unit = unit_of(size);
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()
.map_err(TextError::Raster)
})
.collect()
}
pub(crate) fn layout_texts(
layout: &EvaluatedLayout,
resolved: &ResolvedLayout,
size: RenderSize,
) -> (Vec<Option<u64>>, Vec<(u64, TextRasterRequest)>) {
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 = unit_of(size);
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, TextRasterRequest)> = 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 key = crate::text::layout_text_key(&request);
request.content_version = key;
if !requests.iter().any(|(held, _)| *held == key) {
requests.push((key, request));
}
keys.push(Some(key));
}
(keys, requests)
}
#[allow(clippy::cast_precision_loss)]
pub fn measure_layout(
rasterizer: &TextRasterizer,
root: &LayoutNode,
time: MediaTime,
size: RenderSize,
canvas: (u32, u32),
) -> Result<LayoutBounds, TextError> {
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,
})
.collect(),
})
}