416 lines
15 KiB
Rust
416 lines
15 KiB
Rust
//! Line breaking and glyph placement for point, area and on-path type.
|
|
|
|
use std::ops::Range;
|
|
|
|
use drawcraft_doc::{CharStyle, Justify, ParaStyle, TextKind, TextObject};
|
|
use kurbo::{Affine, BezPath, ParamCurve, ParamCurveArclen, PathEl, PathSeg, Point, Rect, Shape, Vec2};
|
|
|
|
use crate::fontdb::FontDb;
|
|
use crate::shape::{SGlyph, shape_range, style_metrics};
|
|
use crate::{LineInfo, PositionedGlyph, TextLayout};
|
|
|
|
const EPS: f64 = 1e-6;
|
|
|
|
struct Ctx<'a> {
|
|
db: &'a FontDb,
|
|
text: &'a str,
|
|
runs: Vec<(Range<usize>, &'a CharStyle)>,
|
|
default: CharStyle,
|
|
out: TextLayout,
|
|
}
|
|
|
|
impl Ctx<'_> {
|
|
fn style_at(&self, b: usize) -> &CharStyle {
|
|
let find = |b: usize| self.runs.iter().find(|(r, _)| r.start <= b && b < r.end).map(|(_, s)| *s);
|
|
find(b).or_else(|| b.checked_sub(1).and_then(find)).or_else(|| self.runs.first().map(|(_, s)| *s)).unwrap_or(&self.default)
|
|
}
|
|
|
|
fn shape_para(&self, r: Range<usize>) -> Vec<SGlyph> {
|
|
let mut v = Vec::with_capacity(r.len());
|
|
shape_range(self.db, self.text, r, &self.runs, &mut v);
|
|
v
|
|
}
|
|
|
|
fn emit(&mut self, g: &SGlyph, pre: Affine, origin: Point, angle: f64, advance: f64, line: usize) {
|
|
let local = Affine::rotate(-g.rotation.to_radians()) * Affine::translate((g.dx, g.dy - g.bshift)) * Affine::scale_non_uniform(g.sx, g.sy);
|
|
let src = self.db.outline(&g.face, g.gid);
|
|
let outline = if src.elements().is_empty() {
|
|
BezPath::new()
|
|
} else {
|
|
let mut p = (*src).clone();
|
|
p.apply_affine(pre * local);
|
|
p
|
|
};
|
|
self.out.glyphs.push(PositionedGlyph { outline, run: g.run, byte: g.byte, origin, advance, len: g.len, angle, line, font_id: g.face.id() });
|
|
}
|
|
}
|
|
|
|
/// Lay out a text object into text-space glyph outlines.
|
|
pub fn layout(db: &FontDb, t: &TextObject) -> TextLayout {
|
|
let text = t.plain_text();
|
|
let mut runs = Vec::with_capacity(t.runs.len());
|
|
let mut off = 0;
|
|
for r in &t.runs {
|
|
runs.push((off..off + r.text.len(), &r.style));
|
|
off += r.text.len();
|
|
}
|
|
let mut paras = Vec::new();
|
|
let mut s = 0;
|
|
for (i, c) in text.char_indices() {
|
|
if c == '\n' {
|
|
paras.push(s..i);
|
|
s = i + 1;
|
|
}
|
|
}
|
|
paras.push(s..text.len());
|
|
let mut cx = Ctx { db, text: &text, runs, default: CharStyle::default(), out: TextLayout::default() };
|
|
match &t.kind {
|
|
TextKind::Point => flow(&mut cx, ¶s, &t.para, None),
|
|
TextKind::Area { frame } => {
|
|
let region = Region::new(&frame.to_bezpath());
|
|
flow(&mut cx, ¶s, &t.para, Some(®ion));
|
|
}
|
|
TextKind::OnPath { path, start } => on_path(&mut cx, ¶s, &t.para, &path.to_bezpath(), *start, path.is_closed()),
|
|
}
|
|
finish_bounds(&mut cx.out);
|
|
cx.out
|
|
}
|
|
|
|
fn finish_bounds(out: &mut TextLayout) {
|
|
let mut b: Option<Rect> = None;
|
|
let mut add = |r: Rect| b = Some(b.map_or(r, |b| b.union(r)));
|
|
for g in &out.glyphs {
|
|
if !g.outline.elements().is_empty() {
|
|
add(g.outline.bounding_box());
|
|
}
|
|
}
|
|
if !out.on_path {
|
|
for l in &out.lines {
|
|
add(Rect::new(l.x0.min(l.x1), l.baseline - l.ascent, l.x0.max(l.x1), l.baseline + l.descent));
|
|
}
|
|
}
|
|
out.bounds = b.unwrap_or_default();
|
|
}
|
|
|
|
/// A flattened area-type frame.
|
|
struct Region {
|
|
bbox: Rect,
|
|
polys: Vec<Vec<Point>>,
|
|
}
|
|
|
|
impl Region {
|
|
fn new(path: &BezPath) -> Self {
|
|
let mut polys: Vec<Vec<Point>> = Vec::new();
|
|
kurbo::flatten(path, 0.1, |el| match el {
|
|
PathEl::MoveTo(p) => polys.push(vec![p]),
|
|
PathEl::LineTo(p) => {
|
|
if let Some(v) = polys.last_mut() {
|
|
v.push(p);
|
|
}
|
|
}
|
|
_ => {}
|
|
});
|
|
polys.retain(|p| p.len() >= 3);
|
|
Self { bbox: path.bounding_box(), polys }
|
|
}
|
|
|
|
/// Inside intervals (even-odd) of the horizontal line at `y`.
|
|
fn intervals(&self, y: f64) -> Vec<(f64, f64)> {
|
|
let mut xs = Vec::new();
|
|
for poly in &self.polys {
|
|
for i in 0..poly.len() {
|
|
let a = poly[i];
|
|
let b = poly[(i + 1) % poly.len()];
|
|
if (a.y <= y) != (b.y <= y) {
|
|
xs.push(a.x + (y - a.y) / (b.y - a.y) * (b.x - a.x));
|
|
}
|
|
}
|
|
}
|
|
xs.sort_by(f64::total_cmp);
|
|
xs.chunks_exact(2).map(|c| (c[0], c[1])).collect()
|
|
}
|
|
|
|
/// Widest horizontal span inside the frame over the band `top..bottom`.
|
|
fn span(&self, top: f64, bottom: f64) -> Option<(f64, f64)> {
|
|
if self.polys.is_empty() {
|
|
return (self.bbox.width() > 0.0).then_some((self.bbox.x0, self.bbox.x1));
|
|
}
|
|
let clamp = |y: f64| y.clamp(self.bbox.y0 + 1e-4, self.bbox.y1 - 1e-4);
|
|
let mid = self.intervals(clamp((top + bottom) * 0.5));
|
|
let others = [self.intervals(clamp(top)), self.intervals(clamp(bottom))];
|
|
mid.into_iter()
|
|
.filter_map(|(mut a, mut b)| {
|
|
for o in &others {
|
|
let best =
|
|
o.iter().map(|&(c, d)| (a.max(c), b.min(d))).filter(|(c, d)| d > c).max_by(|x, y| (x.1 - x.0).total_cmp(&(y.1 - y.0)))?;
|
|
a = best.0;
|
|
b = best.1;
|
|
}
|
|
(b > a).then_some((a, b))
|
|
})
|
|
.max_by(|x, y| (x.1 - x.0).total_cmp(&(y.1 - y.0)))
|
|
}
|
|
}
|
|
|
|
/// Greedy break: returns the end glyph index for a line starting at `i` of the given width.
|
|
fn break_line(g: &[SGlyph], i: usize, width: f64) -> usize {
|
|
if !width.is_finite() {
|
|
return g.len();
|
|
}
|
|
let mut x = 0.0;
|
|
let mut last_break = None;
|
|
let mut j = i;
|
|
while j < g.len() {
|
|
let gl = &g[j];
|
|
if j > i && !gl.is_space() && x + gl.adv > width + EPS {
|
|
break;
|
|
}
|
|
x += gl.adv;
|
|
if gl.break_after() {
|
|
last_break = Some(j + 1);
|
|
}
|
|
j += 1;
|
|
}
|
|
if j >= g.len() {
|
|
return g.len();
|
|
}
|
|
// Don't separate a cluster's glyphs.
|
|
let mut end = match last_break {
|
|
Some(b) if b > i => b,
|
|
_ => j,
|
|
};
|
|
while end > i + 1 && end < g.len() && g[end].byte == g[end - 1].byte {
|
|
end -= 1;
|
|
}
|
|
end
|
|
}
|
|
|
|
fn max_metrics(g: &[SGlyph]) -> Option<(f64, f64, f64)> {
|
|
(!g.is_empty()).then(|| g.iter().fold((0.0f64, 0.0f64, 0.0f64), |m, g| (m.0.max(g.ascent), m.1.max(g.descent), m.2.max(g.leading))))
|
|
}
|
|
|
|
fn flow(cx: &mut Ctx<'_>, paras: &[Range<usize>], para: &ParaStyle, region: Option<&Region>) {
|
|
let mut prev_baseline: Option<f64> = None;
|
|
let mut pending_space = 0.0;
|
|
'paras: for (pi, pr) in paras.iter().enumerate() {
|
|
let sg = cx.shape_para(pr.clone());
|
|
let pm = style_metrics(cx.db, cx.style_at(pr.start));
|
|
if pi > 0 {
|
|
pending_space += para.space_before;
|
|
}
|
|
let n = sg.len();
|
|
let mut i = 0;
|
|
let mut first_line = true;
|
|
loop {
|
|
let est = if i < n { (sg[i].ascent, sg[i].descent, sg[i].leading) } else { pm };
|
|
let first_ind = if first_line { para.first_line_indent } else { 0.0 };
|
|
let ind_l = para.left_indent + first_ind;
|
|
let mut baseline = match (prev_baseline, region) {
|
|
(None, Some(r)) => r.bbox.y0 + est.0,
|
|
(None, None) => 0.0,
|
|
(Some(b), _) => b + est.2 + pending_space,
|
|
};
|
|
let (x0, x1) = match region {
|
|
None => (f64::NEG_INFINITY, f64::INFINITY),
|
|
Some(r) => loop {
|
|
if baseline + est.1 > r.bbox.y1 + 0.01 {
|
|
cx.out.overflow = cx.text.len() > if i < n { sg[i].byte } else { pr.start };
|
|
break 'paras;
|
|
}
|
|
match r.span(baseline - est.0, baseline + est.1) {
|
|
Some((a, b)) if b - a - ind_l - para.right_indent > est.0.max(1.0) => break (a, b),
|
|
_ => baseline += est.2.max(1.0),
|
|
}
|
|
},
|
|
};
|
|
let (ax0, ax1) = (x0 + ind_l, x1 - para.right_indent);
|
|
let width = ax1 - ax0;
|
|
let end = if i < n { break_line(&sg, i, width) } else { n };
|
|
let m = max_metrics(&sg[i..end]).unwrap_or(pm);
|
|
baseline += if prev_baseline.is_none() { m.0 - est.0 } else { m.2 - est.2 };
|
|
if let Some(r) = region
|
|
&& baseline + m.1 > r.bbox.y1 + 0.01
|
|
{
|
|
cx.out.overflow = cx.text.len() > if i < n { sg[i].byte } else { pr.start };
|
|
break 'paras;
|
|
}
|
|
pending_space = 0.0;
|
|
let last_of_para = end >= n;
|
|
let mut trimmed = end;
|
|
while trimmed > i && sg[trimmed - 1].is_space() {
|
|
trimmed -= 1;
|
|
}
|
|
let w: f64 = sg[i..trimmed].iter().map(|g| g.adv).sum();
|
|
let (align, justify) = match para.justify {
|
|
Justify::Left => (0, false),
|
|
Justify::Center => (1, false),
|
|
Justify::Right => (2, false),
|
|
Justify::JustifyLeft => (0, !last_of_para),
|
|
Justify::JustifyCenter => (1, !last_of_para),
|
|
Justify::JustifyRight => (2, !last_of_para),
|
|
Justify::JustifyAll => (0, true),
|
|
};
|
|
let justify = justify && region.is_some();
|
|
let (mut per_space, mut per_gap) = (0.0, 0.0);
|
|
if justify && width - w > EPS {
|
|
let spaces = sg[i..trimmed].iter().filter(|g| g.is_space()).count();
|
|
if spaces > 0 {
|
|
per_space = (width - w) / spaces as f64;
|
|
} else if para.justify == Justify::JustifyAll && trimmed - i > 1 {
|
|
per_gap = (width - w) / (trimmed - i - 1) as f64;
|
|
}
|
|
}
|
|
let start_x = if justify {
|
|
ax0
|
|
} else if region.is_none() {
|
|
match align {
|
|
0 => ind_l,
|
|
1 => (ind_l - para.right_indent - w) * 0.5,
|
|
_ => -para.right_indent - w,
|
|
}
|
|
} else {
|
|
match align {
|
|
0 => ax0,
|
|
1 => ax0 + (width - w) * 0.5,
|
|
_ => ax1 - w,
|
|
}
|
|
};
|
|
let li = cx.out.lines.len();
|
|
let glyph_start = cx.out.glyphs.len();
|
|
let mut x = start_x;
|
|
let mut x_end = start_x;
|
|
for (j, g) in sg.iter().enumerate().take(end).skip(i) {
|
|
let mut adv = g.adv;
|
|
if j < trimmed {
|
|
if g.is_space() {
|
|
adv += per_space;
|
|
} else if j + 1 < trimmed {
|
|
adv += per_gap;
|
|
}
|
|
}
|
|
cx.emit(g, Affine::translate((x, baseline)), Point::new(x, baseline), 0.0, adv, li);
|
|
x += adv;
|
|
if j + 1 == trimmed {
|
|
x_end = x;
|
|
}
|
|
}
|
|
cx.out.lines.push(LineInfo {
|
|
baseline,
|
|
x0: start_x,
|
|
x1: x_end,
|
|
ascent: m.0,
|
|
descent: m.1,
|
|
start: if i < n { sg[i].byte } else { pr.start },
|
|
end: if last_of_para { pr.end } else { sg[end].byte },
|
|
glyph_start,
|
|
glyph_end: cx.out.glyphs.len(),
|
|
});
|
|
prev_baseline = Some(baseline);
|
|
first_line = false;
|
|
i = end;
|
|
if i >= n {
|
|
break;
|
|
}
|
|
}
|
|
pending_space += para.space_after;
|
|
}
|
|
}
|
|
|
|
/// Arc-length parameterised path.
|
|
struct ArcPath {
|
|
segs: Vec<(PathSeg, f64, f64)>,
|
|
len: f64,
|
|
}
|
|
|
|
impl ArcPath {
|
|
fn new(p: &BezPath) -> Self {
|
|
let mut segs = Vec::new();
|
|
let mut cum = 0.0;
|
|
for s in p.segments() {
|
|
let l = s.arclen(1e-4);
|
|
if l > 1e-9 {
|
|
segs.push((s, cum, l));
|
|
cum += l;
|
|
}
|
|
}
|
|
Self { segs, len: cum }
|
|
}
|
|
|
|
/// Point and unit tangent at arc length `s`.
|
|
fn at(&self, s: f64) -> (Point, Vec2) {
|
|
let s = s.clamp(0.0, self.len);
|
|
let i = self.segs.partition_point(|(_, c, _)| *c <= s).saturating_sub(1);
|
|
let (seg, c, l) = self.segs[i];
|
|
let t = seg.inv_arclen((s - c).min(l), 1e-4).clamp(0.0, 1.0);
|
|
let p = seg.eval(t);
|
|
let (t0, t1) = ((t - 1e-4).max(0.0), (t + 1e-4).min(1.0));
|
|
let d = seg.eval(t1) - seg.eval(t0);
|
|
let len = d.hypot();
|
|
(p, if len > 1e-12 { d / len } else { Vec2::new(1.0, 0.0) })
|
|
}
|
|
}
|
|
|
|
fn on_path(cx: &mut Ctx<'_>, paras: &[Range<usize>], para: &ParaStyle, path: &BezPath, start: f64, closed: bool) {
|
|
cx.out.on_path = true;
|
|
let mut sg = Vec::new();
|
|
for pr in paras {
|
|
sg.extend(cx.shape_para(pr.clone()));
|
|
}
|
|
let ap = ArcPath::new(path);
|
|
let m = max_metrics(&sg).unwrap_or_else(|| style_metrics(cx.db, cx.style_at(0)));
|
|
let text_len = cx.text.len();
|
|
if ap.segs.is_empty() {
|
|
cx.out.overflow = !sg.is_empty();
|
|
cx.out.lines.push(LineInfo {
|
|
baseline: 0.0,
|
|
x0: 0.0,
|
|
x1: 0.0,
|
|
ascent: m.0,
|
|
descent: m.1,
|
|
start: 0,
|
|
end: text_len,
|
|
glyph_start: 0,
|
|
glyph_end: 0,
|
|
});
|
|
return;
|
|
}
|
|
let s_start = start.clamp(0.0, 1.0) * ap.len;
|
|
let avail = if closed { ap.len } else { ap.len - s_start };
|
|
let w: f64 = sg.iter().map(|g| g.adv).sum();
|
|
let s0 = match para.justify {
|
|
Justify::Center | Justify::JustifyCenter => s_start + ((avail - w) * 0.5).max(0.0),
|
|
Justify::Right | Justify::JustifyRight => s_start + (avail - w).max(0.0),
|
|
_ => s_start,
|
|
};
|
|
let mut x = 0.0;
|
|
for g in &sg {
|
|
let s = s0 + x;
|
|
if s + g.adv - s_start > avail + 1e-6 {
|
|
cx.out.overflow = true;
|
|
break;
|
|
}
|
|
let mut mid = s + g.adv * 0.5;
|
|
if closed {
|
|
mid = mid.rem_euclid(ap.len);
|
|
}
|
|
let (p, dir) = ap.at(mid);
|
|
let angle = dir.y.atan2(dir.x);
|
|
let pre = Affine::translate(p.to_vec2()) * Affine::rotate(angle) * Affine::translate((-g.adv * 0.5, 0.0));
|
|
cx.emit(g, pre, p - dir * (g.adv * 0.5), angle, g.adv, 0);
|
|
x += g.adv;
|
|
}
|
|
let (ps, _) = ap.at(if closed { s0.rem_euclid(ap.len) } else { s0 });
|
|
let (pe, _) = ap.at(if closed { (s0 + x).rem_euclid(ap.len) } else { s0 + x });
|
|
cx.out.lines.push(LineInfo {
|
|
baseline: ps.y,
|
|
x0: ps.x,
|
|
x1: pe.x,
|
|
ascent: m.0,
|
|
descent: m.1,
|
|
start: 0,
|
|
end: text_len,
|
|
glyph_start: 0,
|
|
glyph_end: cx.out.glyphs.len(),
|
|
});
|
|
}
|