use crate::FontLibrary;
use motion_core::{
Canvas, Color, DeferredContent, 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 paragraph;
mod parts;
#[cfg(test)]
mod tests;
pub use crate::{TextAlignment, TextSplit};
pub use cosmic_text::Weight as FontWeight;
pub use paragraph::ParagraphStyle;
pub(crate) use paragraph::{ParagraphSpec, SharedLayouts, SpanSpec};
pub use parts::{PartStyle, TextPart};
#[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
}
}
#[derive(Clone, Debug)]
pub struct Text {
paragraph: ParagraphStyle,
fill: Paint,
stroke: Option<(f64, Paint)>,
reveal: Reveal,
split: TextSplit,
visible: Option<usize>,
}
#[derive(Clone, Debug, Default)]
enum Reveal {
#[default]
None,
Write(f64),
Parts(PartAnimation),
}
#[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::from_paragraph(ParagraphStyle::new(text))
}
pub fn rich(spans: impl IntoIterator<Item = TextSpan>) -> Self {
Self::from_paragraph(ParagraphStyle::rich(spans))
}
pub fn from_paragraph(paragraph: ParagraphStyle) -> Self {
Self {
paragraph,
fill: Color::WHITE.into(),
stroke: None,
reveal: Reveal::None,
split: TextSplit::Characters,
visible: None,
}
}
pub fn paragraph(&self) -> &ParagraphStyle {
&self.paragraph
}
pub fn family(mut self, family: impl Into<Arc<str>>) -> Self {
self.paragraph = self.paragraph.family(family);
self
}
pub fn size(mut self, size: f64) -> Self {
self.paragraph = self.paragraph.size(size);
self
}
pub fn weight(mut self, weight: FontWeight) -> Self {
self.paragraph = self.paragraph.weight(weight);
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 {
self.paragraph = self.paragraph.line_height(height);
self
}
pub fn letter_spacing(mut self, spacing: f64) -> Self {
self.paragraph = self.paragraph.letter_spacing(spacing);
self
}
pub fn max_width(mut self, width: f64) -> Self {
self.paragraph = self.paragraph.max_width(width);
self
}
pub fn alignment(mut self, alignment: TextAlignment) -> Self {
self.paragraph = self.paragraph.alignment(alignment);
self
}
pub fn fonts(mut self, fonts: &FontLibrary) -> Self {
self.paragraph = self.paragraph.fonts(fonts);
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.reveal = Reveal::Write(progress);
self
}
pub fn animate_parts(
mut self,
delay: motion_core::Duration,
style: impl Fn(&TextPart) -> PartStyle + Send + Sync + 'static,
) -> Self {
self.reveal = Reveal::Parts(PartAnimation {
delay,
style: Arc::new(style),
});
self
}
pub fn fit_lines(
&self,
max_lines: usize,
fonts: &FontLibrary,
) -> Result<(Self, bool), ValidationError> {
self.validate_ink()?;
let (paragraph, truncated) = self.paragraph.fit_lines(max_lines, fonts)?;
Ok((
Self {
paragraph,
..self.clone()
},
truncated,
))
}
pub fn layout(&self, fonts: &FontLibrary) -> Result<Arc<ParagraphLayout>, ValidationError> {
self.validate_ink()?;
self.paragraph.layout(fonts)
}
pub(super) fn resolve(
&self,
context: &DrawContext,
) -> Result<Arc<ParagraphLayout>, ValidationError> {
self.validate_ink()?;
self.paragraph.resolve(context)
}
fn validate_ink(&self) -> Result<(), ValidationError> {
self.fill.validate()?;
if let Some((width, paint)) = &self.stroke {
if !width.is_finite() || *width < 0.0 {
return Err(invalid(
"stroke",
"Text stroke requires finite nonnegative width",
));
}
paint.validate()?;
}
Ok(())
}
pub fn write_with_context(
&self,
progress: f64,
context: &DrawContext,
) -> Result<Node, ValidationError> {
self.vector_tree(context)?.reveal(&[], Some(progress))
}
pub fn vector_tree(
&self,
context: &DrawContext,
) -> Result<Arc<crate::vector::VectorTree>, ValidationError> {
let layout = self.resolve(context)?;
self.vector_tree_of(&layout)
}
pub fn vector_tree_of(
&self,
layout: &ParagraphLayout,
) -> Result<Arc<crate::vector::VectorTree>, ValidationError> {
use crate::vector::{VectorGroup, VectorImage, VectorItem, VectorPath, VectorTree};
let mut children = Vec::new();
for (index, glyph) in self.visible_glyphs(layout) {
let fill = self.glyph_fill(glyph);
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.ink_bounds(layout),
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)
}