use plotive_base::geom;
use ttf_parser as ttf;
use super::{
Align, Boundaries, Direction, Error, Glyph, HorAlign, Layout, LineSpan, PropsSpan, RichText,
RichTextBuilder, ShapeSpan, TextOptProps, TextProps, VerAlign, VerDirection, VerProgression,
};
use crate::bidi::BidiAlgo;
use crate::font::{self, DatabaseExt};
use crate::{fontdb, line};
#[derive(Debug)]
struct BuilderCtx<C>
where
C: Clone,
{
resolver: PropsResolver<C>,
bidi_algo: BidiAlgo,
buffer: Option<rustybuzz::UnicodeBuffer>,
}
#[derive(Debug)]
struct PropsResolver<C>
where
C: Clone,
{
init_props: TextProps<C>,
stack: Vec<TextOptProps<C>>,
}
impl<C> PropsResolver<C>
where
C: Clone + PartialEq,
{
fn new(init_props: TextProps<C>) -> PropsResolver<C> {
PropsResolver {
init_props,
stack: Vec::new(),
}
}
fn resolved(&self) -> TextProps<C> {
let mut props = self.init_props.clone();
for opts in self.stack.iter() {
props.apply_opts(opts);
}
TextProps {
font: props.font,
font_size: props.font_size,
fill: props.fill.clone(),
outline: props.outline.clone(),
underline: props.underline,
strikeout: props.strikeout,
}
}
fn push_opts(&mut self, opts: TextOptProps<C>) {
self.stack.push(opts);
}
fn pop_opts(&mut self, opts: &TextOptProps<C>) {
for i in (0..self.stack.len()).rev() {
if &self.stack[i] == opts {
self.stack.remove(i);
break;
}
}
}
}
#[derive(Debug)]
enum Justify {
Nope,
Ws { added_gap: f32 },
Glyph { fact: f32 },
}
impl<C> ShapeSpan<C>
where
C: Clone,
{
fn x_advance(&self) -> f32 {
self.glyphs.iter().map(|g| g.x_advance as f32).sum()
}
}
impl<C> ShapeSpan<C>
where
C: Clone,
{
fn col_y_advance(&self) -> f32 {
self.glyphs.iter().map(|g| g.y_advance as f32).sum()
}
}
impl<C> LineSpan<C>
where
C: Clone,
{
fn metrics(&self) -> font::ScaledMetrics {
let mut metrics = font::ScaledMetrics::null();
for s in &self.shapes {
metrics.scale = metrics.scale.max(s.metrics.scale);
metrics.ascent = metrics.ascent.max(s.metrics.ascent);
metrics.descent = metrics.descent.max(s.metrics.descent);
metrics.x_height = metrics.x_height.max(s.metrics.x_height);
metrics.cap_height = metrics.cap_height.max(s.metrics.cap_height);
metrics.line_gap = metrics.line_gap.max(s.metrics.line_gap);
}
metrics
}
fn em_size(&self) -> f32 {
self.shapes
.iter()
.map(|s| s.metrics.em_size)
.max_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap_or(0.0)
}
fn x_advance(&self) -> f32 {
self.shapes.iter().map(|s| s.x_advance()).sum()
}
}
impl<C> LineSpan<C>
where
C: Clone,
{
fn col_width(&self) -> f32 {
self.shapes
.iter()
.map(|s| s.x_advance())
.max_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap_or(0.0)
}
fn col_height(&self) -> f32 {
self.shapes.iter().map(|s| s.col_y_advance()).sum()
}
}
trait Lines {
fn baseline(&self, idx: usize) -> f32;
}
impl<C> Lines for [LineSpan<C>]
where
C: Clone,
{
fn baseline(&self, idx: usize) -> f32 {
let mut h = 0.0;
let mut l = 0;
while l < idx {
h += self[l].total_height();
l += 1;
}
h
}
}
impl VerProgression {
fn from_script(text: &str) -> VerProgression {
if crate::script_is_rtl(text).unwrap_or(false) {
VerProgression::RTL
} else {
VerProgression::LTR
}
}
}
impl<C> RichTextBuilder<C>
where
C: Clone + PartialEq,
{
pub(super) fn done_impl(self, fontdb: &fontdb::Database) -> Result<RichText<C>, Error> {
if self.text.is_empty() {
return Ok(RichText::empty());
}
let bidi_algo = match &self.layout {
Layout::Horizontal(_, _, Direction::Mixed) => BidiAlgo::Yep { default_lev: None },
Layout::Horizontal(_, _, Direction::MixedLTR) => BidiAlgo::Yep {
default_lev: Some(unicode_bidi::LTR_LEVEL),
},
Layout::Horizontal(_, _, Direction::MixedRTL) => BidiAlgo::Yep {
default_lev: Some(unicode_bidi::RTL_LEVEL),
},
Layout::Horizontal(_, _, Direction::LTR) => {
BidiAlgo::Nope(rustybuzz::Direction::LeftToRight)
}
Layout::Horizontal(_, _, Direction::RTL) => {
BidiAlgo::Nope(rustybuzz::Direction::RightToLeft)
}
Layout::Vertical(_, _, VerDirection::TTB, _, _) => {
BidiAlgo::Nope(rustybuzz::Direction::TopToBottom)
}
Layout::Vertical(_, _, VerDirection::BTT, _, _) => {
BidiAlgo::Nope(rustybuzz::Direction::BottomToTop)
}
};
let resolver = PropsResolver::new(self.root_props.clone());
let mut ctx = BuilderCtx {
resolver,
bidi_algo,
buffer: None,
};
let mut lines = Vec::new();
let mut line_start = 0;
let mut was_cr = false;
for (i, c) in self.text.char_indices() {
match c {
'\r' => {
was_cr = true;
}
'\n' => {
lines.push(self.shape_line(
line_start,
if was_cr { i - 1 } else { i },
if was_cr { 2 } else { 1 },
fontdb,
&mut ctx,
)?);
line_start = i + 1;
was_cr = false;
}
'\u{85}' => {
lines.push(self.shape_line(line_start, i, 2, fontdb, &mut ctx)?);
line_start = i + 2;
was_cr = false;
}
'\u{2028}' | '\u{2029}' => {
lines.push(self.shape_line(line_start, i, 3, fontdb, &mut ctx)?);
line_start = i + 3;
was_cr = false;
}
_ => {
was_cr = false;
}
}
}
if line_start < self.text.len() {
lines.push(self.shape_line(line_start, self.text.len(), 0, fontdb, &mut ctx)?);
}
self.build_layout(lines)
}
fn shape_line(
&self,
start: usize,
end: usize,
_eol: usize,
fontdb: &fontdb::Database,
ctx: &mut BuilderCtx<C>,
) -> Result<LineSpan<C>, Error> {
debug_assert!(self.text.is_char_boundary(start) && self.text.is_char_boundary(end));
let line_txt = &self.text[start..end];
let main_dir = ctx.bidi_algo.start_dir();
let mut cur_dir = main_dir;
let bidi_runs = ctx.bidi_algo.visual_runs(line_txt, start);
let mut boundaries = Boundaries::new(start, end);
for run in bidi_runs.iter() {
boundaries.check_in(run.start);
boundaries.check_in(run.end);
}
for span in self.spans.iter().filter(|s| s.props.affect_shape()) {
boundaries.check_in(span.start);
boundaries.check_in(span.end);
}
let boundaries = boundaries.into_iter();
let mut shapes = Vec::with_capacity(boundaries.len());
for (span_start, span_end) in boundaries {
for run in bidi_runs.iter() {
if span_start == run.start {
cur_dir = run.dir;
}
}
shapes.push(self.shape_span(span_start, span_end, cur_dir, fontdb, ctx)?);
}
Ok(LineSpan {
start,
end,
shapes,
main_dir,
bbox: None,
})
}
fn shape_span(
&self,
start: usize,
end: usize,
dir: rustybuzz::Direction,
fontdb: &fontdb::Database,
ctx: &mut BuilderCtx<C>,
) -> Result<ShapeSpan<C>, Error> {
debug_assert!(self.text.is_char_boundary(start) && self.text.is_char_boundary(end));
let txt = &self.text[start..end];
let mut boundaries = Boundaries::new(start, end);
for span in self.spans.iter() {
boundaries.check_in(span.start);
boundaries.check_in(span.end);
}
let boundaries = boundaries.into_iter();
let mut props_spans = Vec::with_capacity(boundaries.len());
for (span_start, span_end) in boundaries {
for span in self.spans.iter() {
if span.start == span_start {
ctx.resolver.push_opts(span.props.clone());
}
}
props_spans.push(PropsSpan {
start: span_start,
end: span_end,
props: ctx.resolver.resolved(),
bbox: None,
});
for span in self.spans.iter() {
if span.end == span_end {
ctx.resolver.pop_opts(&span.props);
}
}
}
let shape_props = &props_spans.first().unwrap().props;
let face_id = fontdb
.select_face_for_str(&shape_props.font, txt)
.or_else(|| fontdb.select_face(&shape_props.font))
.ok_or_else(|| Error::NoSuchFont(shape_props.font.clone()))?;
let mut buffer = ctx
.buffer
.take()
.unwrap_or_else(|| rustybuzz::UnicodeBuffer::new());
buffer.push_str(txt);
if start != 0 {
buffer.set_pre_context(&self.text[..start]);
}
if end != self.text.len() {
buffer.set_post_context(&self.text[end..]);
}
buffer.set_direction(dir);
buffer.guess_segment_properties();
let (glyphs, metrics, buffer) = fontdb
.with_face_data(face_id, |data, index| -> Result<_, Error> {
let face = ttf::Face::parse(data, index)?;
let metrics = font::face_metrics(&face).scaled(shape_props.font_size);
let mut hbface = rustybuzz::Face::from_face(face);
font::apply_hb_variations(&mut hbface, &shape_props.font);
let buffer = rustybuzz::shape(&hbface, &[], buffer);
let mut glyphs = Vec::with_capacity(buffer.len());
for (i, p) in buffer.glyph_infos().iter().zip(buffer.glyph_positions()) {
let id = ttf::GlyphId(i.glyph_id as u16);
let rect = hbface.glyph_bounding_box(id).unwrap_or(ttf::Rect {
x_min: 0,
y_min: 0,
x_max: 0,
y_max: 0,
});
glyphs.push(Glyph {
id,
cluster: i.cluster as usize + start,
x_advance: p.x_advance as f32 * metrics.scale,
y_advance: p.y_advance as f32 * metrics.scale,
x_offset: p.x_offset as f32 * metrics.scale,
y_offset: p.y_offset as f32 * metrics.scale,
ts: tiny_skia::Transform::identity(),
rect,
})
}
Ok((glyphs, metrics, buffer))
})
.expect("should be a valid face id")?;
ctx.buffer = Some(buffer.clear());
let shape = ShapeSpan {
start,
end,
spans: props_spans,
face_id,
glyphs,
metrics,
y_baseline: f32::NAN,
bbox: None,
};
Ok(shape)
}
fn build_layout(self, mut lines: Vec<LineSpan<C>>) -> Result<RichText<C>, Error> {
if lines.is_empty() {
return Ok(RichText::empty());
}
let layout = match self.layout {
Layout::Horizontal(align, ver_align, direction) => {
self.build_horizontal_layout(&mut lines)?;
Layout::Horizontal(align, ver_align, direction)
}
Layout::Vertical(align, hor_align, direction, _, inter_col) => {
let progression = self.build_vertical_layout(&mut lines)?;
Layout::Vertical(align, hor_align, direction, progression, inter_col)
}
};
let bbox = lines
.iter()
.map(|l| l.bbox)
.reduce(|a, b| geom::Rect::unite_opt(a.as_ref(), b.as_ref()));
let bbox = bbox.unwrap_or_default();
Ok(RichText {
text: self.text,
layout,
lines,
bbox,
})
}
fn build_horizontal_layout(&self, lines: &mut Vec<LineSpan<C>>) -> Result<(), Error> {
let Layout::Horizontal(align, ver_align, _) = self.layout else {
unreachable!()
};
let lines_len = lines.len();
let mut y_cursor = match ver_align {
VerAlign::Top => lines[0].ascent(),
VerAlign::Bottom => lines[lines_len - 1].descent() - lines.baseline(lines_len - 1),
VerAlign::Center => {
let top = lines[0].ascent();
let bottom = lines[lines_len - 1].descent() - lines.baseline(lines_len - 1);
(top + bottom) / 2.0
}
VerAlign::Line(line, align) => {
let baseline = lines.baseline(line);
let lst_metrics = lines[lines_len - 1].metrics();
match align {
line::VerAlign::Bottom => lst_metrics.descent - baseline,
line::VerAlign::Baseline => -baseline,
line::VerAlign::Middle => lst_metrics.x_height / 2.0 - baseline,
line::VerAlign::Hanging => lst_metrics.cap_height - baseline,
line::VerAlign::Top => lst_metrics.ascent - baseline,
}
}
};
for lidx in 0..lines_len {
if lidx != 0 {
y_cursor += lines[lidx].height();
}
self.layout_horizontal_line(&mut lines[lidx], y_cursor, align);
y_cursor += lines[lidx].gap();
}
Ok(())
}
fn layout_horizontal_line(&self, line: &mut LineSpan<C>, y_baseline: f32, align: Align) {
let ws = self.text[line.start..line.end]
.chars()
.filter(|c| c.is_whitespace())
.count();
let width = line.x_advance();
let (width, justify) = match align {
Align::Justify(sz) => {
let sz = sz.max(width);
let justify = if ws > 0 {
Justify::Ws {
added_gap: (sz - width) / ws as f32,
}
} else {
Justify::Glyph { fact: sz / width }
};
(sz, justify)
}
_ => (width, Justify::Nope),
};
let x_start = match (align, line.main_dir) {
(Align::Start, rustybuzz::Direction::LeftToRight)
| (Align::End, rustybuzz::Direction::RightToLeft)
| (Align::Left, _) => 0.0,
(Align::Start, rustybuzz::Direction::RightToLeft)
| (Align::End, rustybuzz::Direction::LeftToRight)
| (Align::Right, _) => -width,
(Align::Center, _) => -width / 2.0,
_ => unreachable!(),
};
let top = y_baseline - line.ascent();
let bottom = y_baseline - line.descent();
let mut x_cursor = x_start;
let mut y_cursor = y_baseline;
let y_flip = geom::Transform::from_scale(1.0, -1.0);
for shape in line.shapes.iter_mut() {
let shape_start = x_cursor;
let scale_ts = geom::Transform::from_scale(shape.metrics.scale, shape.metrics.scale);
for glyph in shape.glyphs.iter_mut() {
let x = x_cursor + glyph.x_offset;
let y = y_cursor - glyph.y_offset;
let pos_ts = geom::Transform::from_translate(x, y);
glyph.ts = y_flip.post_concat(scale_ts).post_concat(pos_ts);
let glyph_start = x_cursor;
x_cursor += match justify {
Justify::Nope => glyph.x_advance,
Justify::Glyph { fact } => glyph.x_advance * fact,
Justify::Ws { added_gap } => {
let is_ws = self.text[glyph.cluster..]
.chars()
.next()
.unwrap()
.is_whitespace();
if is_ws {
glyph.x_advance + added_gap
} else {
glyph.x_advance
}
}
};
y_cursor -= glyph.y_advance;
for s in shape.spans.iter_mut() {
if s.start <= glyph.cluster && glyph.cluster < s.end {
s.bbox = geom::Rect::unite_opt(
s.bbox.as_ref(),
Some(&geom::Rect::from_trbl(top, x_cursor, bottom, glyph_start)),
);
}
}
}
shape.y_baseline = y_baseline;
shape.bbox = Some(geom::Rect::from_trbl(top, x_cursor, bottom, shape_start));
}
line.bbox = Some(geom::Rect::from_trbl(
y_baseline - line.ascent(),
x_cursor,
y_baseline - line.descent(),
x_start,
));
}
fn build_vertical_layout(&self, cols: &mut Vec<LineSpan<C>>) -> Result<VerProgression, Error> {
let Layout::Vertical(align, hor_align, _, progression, inter_col) = self.layout else {
unreachable!()
};
let progression = match progression {
VerProgression::PerScript => VerProgression::from_script(&self.text),
progression => progression,
};
let move_x = |x_cursor: &mut f32, value: f32| match progression {
VerProgression::LTR => *x_cursor += value,
VerProgression::RTL => *x_cursor -= value,
VerProgression::PerScript => unreachable!(),
};
let width = cols[0].em_size();
let mut x_cursor = match hor_align {
HorAlign::Left => width / 2.0,
HorAlign::Center => 0.0,
HorAlign::Right => -width / 2.0,
};
for (idx, col) in cols.iter_mut().enumerate() {
if idx != 0 {
move_x(&mut x_cursor, col.col_width());
}
self.layout_vertical_column(col, x_cursor, align);
move_x(&mut x_cursor, col.em_size() * inter_col.0);
}
Ok(progression)
}
fn layout_vertical_column(&self, col: &mut LineSpan<C>, x_leftline: f32, type_align: Align) {
let ws = self.text[col.start..col.end]
.chars()
.filter(|c| c.is_whitespace())
.count();
let height = col.col_height();
let (height, justify) = match type_align {
Align::Justify(sz) => {
let sz = sz.max(height);
let justify = if ws > 0 {
Justify::Ws {
added_gap: (sz - height) / ws as f32,
}
} else {
Justify::Glyph { fact: sz / height }
};
(sz, justify)
}
_ => (height, Justify::Nope),
};
let y_start = match (type_align, col.main_dir) {
(Align::Start, rustybuzz::Direction::TopToBottom)
| (Align::End, rustybuzz::Direction::BottomToTop)
| (Align::Left, _) => 0.0,
(Align::Start, rustybuzz::Direction::BottomToTop)
| (Align::End, rustybuzz::Direction::TopToBottom)
| (Align::Right, _) => height,
(Align::Center, _) => height / 2.0,
_ => unreachable!(),
};
let left = x_leftline;
let right = x_leftline + col.col_width();
let mut x_cursor = x_leftline;
let mut y_cursor = y_start;
let y_flip = geom::Transform::from_scale(1.0, -1.0);
for shape in col.shapes.iter_mut() {
let shape_start = x_cursor;
let scale_ts = geom::Transform::from_scale(shape.metrics.scale, shape.metrics.scale);
for glyph in shape.glyphs.iter_mut() {
let x = x_cursor + glyph.x_offset;
let y = y_cursor - glyph.y_offset;
let pos_ts = geom::Transform::from_translate(x, y);
glyph.ts = y_flip.post_concat(scale_ts).post_concat(pos_ts);
let glyph_start = y_cursor;
y_cursor -= match justify {
Justify::Nope => glyph.y_advance,
Justify::Glyph { fact } => glyph.y_advance * fact,
Justify::Ws { added_gap } => {
let is_ws = self.text[glyph.cluster..]
.chars()
.next()
.unwrap()
.is_whitespace();
if is_ws {
glyph.y_advance + added_gap
} else {
glyph.y_advance
}
}
};
x_cursor += glyph.x_advance;
for s in shape.spans.iter_mut() {
if s.start <= glyph.cluster && glyph.cluster < s.end {
s.bbox = geom::Rect::unite_opt(
s.bbox.as_ref(),
Some(&geom::Rect::from_trbl(glyph_start, right, y_cursor, left)),
);
}
}
}
shape.y_baseline = f32::NAN;
shape.bbox = Some(geom::Rect::from_trbl(shape_start, right, x_cursor, left));
col.bbox = geom::Rect::unite_opt(col.bbox.as_ref(), shape.bbox.as_ref());
}
}
}
#[cfg(test)]
mod tests {
use plotive_base::ColorU8;
use super::*;
use crate::bundled_font_db;
#[test]
fn underline_span() {
let db = bundled_font_db();
let mut builder: RichTextBuilder<ColorU8> =
RichTextBuilder::new("Some RICH\ntext string".to_string(), TextProps::new(12.0));
builder.add_span(
5,
9,
TextOptProps {
underline: Some(true),
..Default::default()
},
);
let text = builder.done(&db).unwrap();
assert_eq!(text.lines.len(), 2);
assert_eq!(text.lines[0].shapes.len(), 1);
assert_eq!(text.lines[1].shapes.len(), 1);
assert_eq!(text.lines[0].shapes[0].spans.len(), 2);
assert_eq!(text.lines[1].shapes[0].spans.len(), 1);
assert_eq!(text.lines[0].shapes[0].spans[0].props.underline, false);
assert_eq!(text.lines[0].shapes[0].spans[1].props.underline, true);
assert_eq!(text.lines[1].shapes[0].spans[0].props.underline, false);
}
}