use crate::FontLibrary;
use motion_core::{
Canvas, Color, DrawContext, Drawable, FillRule, Node, NodeModifiers, Paint, Rect, Time,
Transform, ValidationError,
};
use std::{
collections::BTreeMap,
ops::Range,
sync::{Arc, Mutex},
};
use unicode_segmentation::UnicodeSegmentation;
mod drawable;
pub(crate) mod glyph;
mod layout;
mod parts;
#[cfg(test)]
mod tests;
pub use cosmic_text::Weight as FontWeight;
pub use parts::{PartStyle, TextPart};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum TextAlignment {
#[default]
Start,
End,
Center,
Justified,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TextSplit {
#[default]
Characters,
Words,
Lines,
}
#[derive(Clone, Debug)]
pub struct TextSpan {
text: Arc<str>,
family: Option<Arc<str>>,
size: Option<f64>,
weight: Option<FontWeight>,
italic: bool,
fill: Option<Paint>,
}
impl TextSpan {
pub fn new(text: impl Into<Arc<str>>) -> Self {
Self {
text: text.into(),
family: None,
size: None,
weight: None,
italic: false,
fill: None,
}
}
pub fn family(mut self, family: impl Into<Arc<str>>) -> Self {
self.family = Some(family.into());
self
}
pub fn size(mut self, size: f64) -> Self {
self.size = Some(size);
self
}
pub fn weight(mut self, weight: FontWeight) -> Self {
self.weight = Some(weight);
self
}
pub fn italic(mut self, italic: bool) -> Self {
self.italic = italic;
self
}
pub fn fill(mut self, paint: impl Into<Paint>) -> Self {
self.fill = Some(paint.into());
self
}
}
#[derive(Clone, Debug)]
pub struct TextGlyph {
pub source: Range<usize>,
pub line: usize,
pub family: Arc<str>,
pub bounds: Rect,
artwork: Arc<glyph::Artwork>,
transform: Transform,
span: usize,
}
#[derive(Clone, Debug)]
pub struct ParagraphLayout {
glyphs: Arc<[TextGlyph]>,
bounds: Rect,
baseline: f64,
lines: Arc<[Rect]>,
line_sources: Arc<[Range<usize>]>,
text: Arc<str>,
}
impl ParagraphLayout {
pub fn bounds(&self) -> Rect {
self.bounds
}
pub fn baseline(&self) -> f64 {
self.baseline
}
pub fn glyphs(&self) -> &[TextGlyph] {
&self.glyphs
}
pub fn lines(&self) -> &[Rect] {
&self.lines
}
}
type LayoutCache = Arc<Mutex<BTreeMap<u64, Result<Arc<ParagraphLayout>, ValidationError>>>>;
pub(crate) type SharedLayouts = Arc<Mutex<BTreeMap<LayoutKey, Arc<ParagraphLayout>>>>;
type SpanLayoutKey = (Arc<str>, Option<Arc<str>>, Option<u64>, Option<u16>, bool);
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct LayoutKey {
spans: Vec<SpanLayoutKey>,
family: Arc<str>,
size: u64,
weight: u16,
line_height: u64,
spacing: u64,
width: Option<u64>,
alignment: TextAlignment,
}
#[derive(Clone, Debug)]
pub struct Text {
spans: Arc<[TextSpan]>,
family: Arc<str>,
size: f64,
weight: FontWeight,
fill: Paint,
stroke: Option<(f64, Paint)>,
line_height: f64,
spacing: f64,
width: Option<f64>,
alignment: TextAlignment,
write_progress: Option<f64>,
part_animation: Option<PartAnimation>,
split: TextSplit,
visible: Option<usize>,
fonts: Option<FontLibrary>,
layout_cache: LayoutCache,
}
#[derive(Clone)]
struct PartAnimation {
delay: motion_core::Duration,
style: Arc<dyn Fn(&TextPart) -> PartStyle + Send + Sync>,
}
impl std::fmt::Debug for PartAnimation {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("PartAnimation")
.field("delay", &self.delay)
.finish_non_exhaustive()
}
}
impl Text {
pub fn from_font(
text: &str,
font: &crate::FontFace,
size: f64,
paint: impl Into<Paint>,
) -> Result<Self, ValidationError> {
if !size.is_finite() || size <= 0.0 || size > 4096.0 {
return Err(invalid(
"font_size",
"Require a finite font size in (0, 4096]",
));
}
let fonts = font.library()?;
let face = fonts
.database()
.faces()
.find(|face| face.index == font.index())
.ok_or_else(|| invalid("font", "Explicit font face is unavailable"))?;
let family = face
.families
.first()
.ok_or_else(|| invalid("font", "Explicit font face has no family"))?
.0
.clone();
let span = TextSpan::new(text)
.family(family)
.weight(FontWeight(face.weight.0))
.italic(face.style == fontdb::Style::Italic);
let value = Self::rich([span]).fonts(&fonts).size(size).fill(paint);
value.layout(&fonts)?;
Ok(value)
}
pub fn new(text: impl Into<Arc<str>>) -> Self {
Self::rich([TextSpan::new(text)])
}
pub fn rich(spans: impl IntoIterator<Item = TextSpan>) -> Self {
Self {
write_progress: None,
part_animation: None,
spans: spans.into_iter().take(257).collect::<Vec<_>>().into(),
family: "".into(),
size: 24.0,
weight: FontWeight::NORMAL,
fill: Color::WHITE.into(),
stroke: None,
line_height: 1.3,
spacing: 0.0,
width: None,
alignment: TextAlignment::Start,
fonts: None,
split: TextSplit::Characters,
visible: None,
layout_cache: Arc::new(Mutex::new(BTreeMap::new())),
}
}
fn invalidate_layout_cache(&mut self) {
self.layout_cache = Arc::new(Mutex::new(BTreeMap::new()));
}
pub fn family(mut self, family: impl Into<Arc<str>>) -> Self {
let family = family.into();
if self.family != family {
self.family = family;
self.invalidate_layout_cache();
}
self
}
pub fn size(mut self, size: f64) -> Self {
if self.size.to_bits() != size.to_bits() {
self.size = size;
self.invalidate_layout_cache();
}
self
}
pub fn weight(mut self, weight: FontWeight) -> Self {
if self.weight != weight {
self.weight = weight;
self.invalidate_layout_cache();
}
self
}
pub fn fill(mut self, paint: impl Into<Paint>) -> Self {
self.fill = paint.into();
self
}
pub fn stroke(mut self, width: f64, paint: impl Into<Paint>) -> Self {
self.stroke = Some((width, paint.into()));
self
}
pub fn line_height(mut self, height: f64) -> Self {
if self.line_height.to_bits() != height.to_bits() {
self.line_height = height;
self.invalidate_layout_cache();
}
self
}
pub fn letter_spacing(mut self, spacing: f64) -> Self {
if self.spacing.to_bits() != spacing.to_bits() {
self.spacing = spacing;
self.invalidate_layout_cache();
}
self
}
pub fn max_width(mut self, width: f64) -> Self {
if self.width.map(f64::to_bits) != Some(width.to_bits()) {
self.width = Some(width);
self.invalidate_layout_cache();
}
self
}
pub fn alignment(mut self, alignment: TextAlignment) -> Self {
if self.alignment != alignment {
self.alignment = alignment;
self.invalidate_layout_cache();
}
self
}
pub fn fonts(mut self, fonts: &FontLibrary) -> Self {
self.fonts = Some(fonts.clone());
self
}
pub fn split(mut self, split: TextSplit) -> Self {
self.split = split;
self
}
pub fn visible_parts(mut self, count: usize) -> Self {
self.visible = Some(count);
self
}
pub fn write(mut self, progress: f64) -> Self {
self.write_progress = Some(progress);
self
}
pub fn animate_parts(
mut self,
delay: motion_core::Duration,
style: impl Fn(&TextPart) -> PartStyle + Send + Sync + 'static,
) -> Self {
self.part_animation = Some(PartAnimation {
delay,
style: Arc::new(style),
});
self
}
pub fn write_with_context(
&self,
progress: f64,
context: &DrawContext,
) -> Result<Node, ValidationError> {
self.vector_tree(context)?
.write(progress, crate::vector::WriteOptions::default())
}
pub fn vector_tree(
&self,
context: &DrawContext,
) -> Result<Arc<crate::vector::VectorTree>, ValidationError> {
use crate::vector::{VectorGroup, VectorImage, VectorItem, VectorPath, VectorTree};
let layout = self.resolve(context)?;
let parts = layout.parts(self.split);
let mut children = Vec::new();
for (index, glyph) in layout.glyphs.iter().enumerate() {
let owner = parts.iter().position(|part| part.owns(glyph)).unwrap_or(0);
if self.visible.is_some_and(|visible| owner >= visible) {
continue;
}
let fill = self.spans[glyph.span]
.fill
.as_ref()
.unwrap_or(&self.fill)
.clone();
let mut ink = Vec::new();
if !glyph.artwork.path.verbs().is_empty() {
ink.push(VectorItem::Path(VectorPath {
id: None,
path: glyph.artwork.path.clone(),
fill: Some((fill.clone(), FillRule::NonZero)),
stroke: None,
source: None,
}));
}
for (path, color) in &glyph.artwork.layers {
ink.push(VectorItem::Path(VectorPath {
id: None,
path: path.clone(),
fill: Some((
color.map_or_else(|| fill.clone(), Paint::from),
FillRule::NonZero,
)),
stroke: None,
source: None,
}));
}
let mut glyph_children = vec![VectorItem::Group(VectorGroup {
transform: glyph.transform,
children: ink,
..Default::default()
})];
if let Some((image, local)) = &glyph.artwork.bitmap {
let pattern =
motion_core::ImagePattern::from_asset(image.clone()).transform(*local);
let rect = motion_core::display_list::mapped_bounds(
Rect::new(
0.0,
0.0,
f64::from(image.width()),
f64::from(image.height()),
),
*local,
"text.bitmap",
)?;
let mut image = VectorImage::new(image.clone(), rect);
image.pixel_transform = *local;
image.sampling = pattern.sampling;
image.extend = pattern.extend;
image.decal = pattern.decal;
glyph_children.push(VectorItem::Group(VectorGroup {
transform: glyph.transform,
children: vec![VectorItem::Image(image)],
..Default::default()
}));
}
if let Some((width, paint)) = &self.stroke {
for path in std::iter::once(&glyph.artwork.path)
.chain(glyph.artwork.layers.iter().map(|(path, _)| path))
.filter(|path| !path.verbs().is_empty())
{
let outline = motion_core::StrokeStyle {
width: *width,
..Default::default()
}
.outline(&path.transformed(&glyph.transform)?, "text.vector.stroke")?;
glyph_children
.push(VectorItem::Path(VectorPath::filled(outline, paint.clone())));
}
}
children.push(VectorItem::Group(VectorGroup {
id: Some(format!("glyph-{index}").into()),
source: Some(glyph.source.clone()),
children: glyph_children,
..Default::default()
}));
}
Ok(Arc::new(
VectorTree::new(
self.try_bounds(context)?,
VectorGroup {
children,
..Default::default()
},
)?
.with_baseline(layout.baseline),
))
}
}
pub(crate) fn invalid(field: &str, message: &str) -> ValidationError {
ValidationError::invalid("text", field, message)
}
pub(crate) fn limited(message: &str) -> ValidationError {
ValidationError::limit("text", "work", message)
}