Files
vectorcraft/crates/text/src/layout.rs
2026-11-02 14:32:59 +07:00

360 lines
19 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Line breaking and glyph placement for point, area and on-path type.
use std::ops::Range;
use kurbo::{Affine, BezPath, ParamCurve, PathEl, Point, Rect, Shape, Vec2};
use unicode_bidi::{BidiInfo, Level};
use vectorcraft_doc::text::Kashida;
use vectorcraft_doc::{
Burasagari, CharStyle, HyphenationOptions, InlineArt, Justify, Kinsoku, LeadingModel, Mojikumi, ParaDirection, ParaStyle, PathAlign, PathEffect,
RubyAlign, RubyPosition, SingleWordJustify, TextKind, TextObject,
};
use vectorcraft_geom::{ArcPath, PathData};
use crate::GlyphBend;
use crate::composer::{Breakpoint, Params, compose};
use crate::fontdb::FontDb;
use crate::hyphen::hyphen_points_with;
use crate::justify::{Flex, LineFit, apply_desired, fit_line, is_word_space};
use crate::kashida;
use crate::shape::{
Punct, SGlyph, Tcy, cap_x_heights, hyphen_glyph, is_cjk, mark_word_ends, no_line_end, no_line_start, punct, shape_range, soft_line_start,
style_metrics,
};
use crate::{Composer, FirstBaseline, InlineGlyph, LayoutOptions, LineInfo, ListMarker, OtFeatures, PositionedGlyph, TextLayout, VerticalAlign};
mod bidi;
mod breaking;
mod driver;
mod flow;
mod on_path;
mod region;
mod ruby;
mod vertical;
pub(crate) use bidi::{is_rtl, para_bidi};
use breaking::tab_advance;
#[cfg(test)]
use breaking::{break_line, candidates};
pub use driver::{decorations, fit_first_line, layout, layout_with, scaled};
use vertical::{TcyStyle, stands_upright, tate_chu_yoko, upright_cell, upright_centre, whole_character_widths};
const EPS: f64 = 1e-6;
/// Is glyph `g` a later glyph of the cluster of glyph `prev` before it (a combining mark the shaper
/// set as a glyph of its own)? A list marker's glyphs stand for no character and are not.
fn continues_cluster(prev: Option<&SGlyph>, g: Option<&SGlyph>) -> bool {
prev.zip(g).is_some_and(|(a, b)| a.len > 0 && b.len > 0 && a.byte == b.byte)
}
struct Ctx<'a> {
on_path: bool,
db: &'a FontDb,
text: &'a str,
runs: Vec<(Range<usize>, &'a CharStyle)>,
/// The inline graphic of each run that is one (parallel to `runs`).
inlines: Vec<Option<&'a InlineArt>>,
default: CharStyle,
opts: &'a LayoutOptions,
/// Vertical type: lines are laid out as horizontal lines in line space, upright glyphs turned
/// a quarter turn back (see [`stands_upright`]); [`TextLayout::line_xf`] then stands the lines
/// up as columns.
vertical: bool,
/// Character panel menu › System Layout. On: positive advances are whole points.
system_layout: bool,
/// Character panel menu › Fractional Widths. Off, positive advances are whole points.
fractional_widths: bool,
/// Vertical type: the upright glyph [`Self::emit`] placed last, as (its byte, the index the
/// next glyph of its cluster would take, the turn it got, the line-space x that next glyph is
/// set after), so that a combining mark shaped into the same cluster turns with it.
upright_cluster: Option<(usize, usize, Affine, f64)>,
out: TextLayout,
}
impl Ctx<'_> {
fn style_at(&self, b: usize) -> &CharStyle {
// The runs follow one another: the first that ends after `b` is the only one that can hold it.
let find = |b: usize| {
let i = self.runs.partition_point(|(r, _)| r.end <= b);
self.runs.get(i).filter(|(r, _)| r.start <= b).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)
}
/// The stretches of paragraph `r` whose characters have a Character Direction, as byte ranges
/// from `r.start` with their direction, in order; neighboring runs of one direction make one
/// stretch.
fn char_directions(&self, r: &Range<usize>) -> Vec<(Range<usize>, ParaDirection)> {
let mut out: Vec<(Range<usize>, ParaDirection)> = Vec::new();
for (rr, st) in &self.runs {
let (a, b) = (rr.start.max(r.start), rr.end.min(r.end));
let Some(d) = st.char_direction.filter(|_| a < b) else { continue };
let (a, b) = (a - r.start, b - r.start);
match out.last_mut() {
Some((prev, pd)) if *pd == d && prev.end == a => prev.end = b,
_ => out.push((a..b, d)),
}
}
out
}
/// Shape paragraph `r` (its bidi resolution `bidi`).
fn shape_para(&self, r: Range<usize>, bidi: Option<&BidiInfo<'_>>) -> Vec<SGlyph> {
let mut v = Vec::with_capacity(r.len());
let levels = bidi.map_or(&[][..], |b| &b.levels);
shape_range(self.db, self.text, r, &self.runs, &self.inlines, &self.opts.features, levels, &mut v);
self.finish_shaping(&mut v, |g| self.style_at(g.byte));
v
}
/// Set shaped glyphs `v` as the type sets them: in vertical type, tate-chu-yoko (by the size
/// and Tate-chu-yoko of each glyph's `style`) and the cells of upright glyphs; with System
/// Layout on or Fractional Widths off, whole-point advances.
fn finish_shaping<'s>(&self, v: &mut [SGlyph], style: impl Fn(&SGlyph) -> &'s CharStyle) {
if self.vertical {
tate_chu_yoko(v, |g| {
let st = style(g);
TcyStyle { size: st.size, marked: st.tate_chu_yoko, up: st.tcy_up_down, right: st.tcy_left_right }
});
// An upright glyph advances down the column by its vertical advance (the font's vertical
// metrics; without them at least one em, as CJK fonts' is), centred across it: a narrow
// mark like § must not overlap its neighbours. A combining mark shaped into the cluster
// of the glyph before it takes no cell of its own: it turns with that glyph (`emit`).
for i in 0..v.len() {
let continues = continues_cluster(i.checked_sub(1).and_then(|p| v.get(p)), v.get(i));
let Some(g) = v.get_mut(i).filter(|g| g.adv > 0.0 && !continues && stands_upright(g)) else { continue };
let extra = upright_cell(g) - g.face.advance(g.gid) * g.sx;
g.dx += extra * 0.5;
g.adv += extra;
// Proportional Metrics: `vpal` (and the vertical kerning with it) moves it and
// changes its length down the column, in fonts with vertical metrics (else the cell
// stays as it is, #966).
if let Some(a) = g.vpal.filter(|_| g.tcy.is_none() && g.face.vertical_glyph(g.gid).is_some()) {
g.dx += a.dx;
g.dy += a.dy;
g.adv += a.advance;
g.proportional = a.proportional;
}
}
}
// System Layout or Fractional Widths off: whole-point advances, after tate-chu-yoko and the
// upright cell have set the widths the line breaker will use. A zero advance stays zero.
if self.system_layout || !self.fractional_widths {
whole_character_widths(v);
}
}
/// Bullets and numbered lists: put marker `text` (the bullet, or the number and its period;
/// [`list_markers`]) of list paragraph `pr` (attributes `para`, right to left when `rtl`)
/// before its glyphs `sg` and return how many glyphs went in (none for a paragraph without a
/// marker). The marker's characters are set in
/// [`vectorcraft_doc::text::ParaList::marker_style`], and a tab after them takes the text to
/// the next tab stop past them on the first line, the left indent counting as a stop when no
/// explicit stop comes before it. In a horizontal right-to-left paragraph, whose indents are
/// mirrored ([`line_indents`]), these are measured from the right, and a number reads left to
/// right with its period at its left. The tab keeps that width however the line is aligned or
/// justified. Each of these glyphs stands for no character of the text: length 0, at the
/// paragraph's first byte, in its first character's run, at the paragraph's embedding level.
fn prepend_marker(&self, pr: &Range<usize>, para: &ParaStyle, text: Option<&str>, rtl: bool, sg: &mut Vec<SGlyph>) -> usize {
let Some(text) = text.filter(|_| para.list.is_some() && !pr.is_empty()) else { return 0 };
let st = vectorcraft_doc::text::ParaList::marker_style(self.style_at(pr.start));
let Some(run) = self.runs.iter().position(|(r, _)| r.start <= pr.start && pr.start < r.end) else { return 0 };
// The paragraph's direction: the marker starts a right-to-left paragraph at its right.
let level = if rtl { Level::rtl() } else { Level::ltr() };
let mut marker = Vec::with_capacity(text.len() + 1);
shape_range(self.db, text, 0..text.len(), &[(0..text.len(), &st)], &[None], &self.opts.features, &vec![level; text.len()], &mut marker);
if rtl {
// [`visual_order`] draws a right-to-left marker's glyphs in reverse: the number's
// glyphs go in reversed, so that it reads left to right, and the period after them.
let number = marker.iter().take_while(|g| text.get(g.byte..).is_some_and(|s| !s.starts_with('.'))).count();
marker[..number].reverse();
}
for g in &mut marker {
g.byte = pr.start;
g.len = 0;
g.run = run;
}
self.finish_shaping(&mut marker, |_| &st);
let Some(last) = marker.last() else { return 0 };
let mut tab = last.clone();
tab.ch = '\t';
tab.gid = 0;
tab.dx = 0.0;
tab.dy = 0.0;
tab.kern_shift = 0.0;
tab.tcy = None;
tab.vpal = None;
let pen = para.left_indent + para.first_line_indent + marker.iter().map(|g| g.adv).sum::<f64>();
let to_indent = para.left_indent - pen;
let stop_before_indent = para.tabs.iter().any(|t| t.position > pen + EPS && t.position < para.left_indent - EPS);
tab.adv = if to_indent > EPS && !stop_before_indent { to_indent } else { tab_advance(&para.tabs, 0.0, pen, sg).0 };
marker.push(tab);
let n = marker.len();
sg.splice(0..0, marker);
n
}
fn emit(&mut self, g: &SGlyph, pre: Affine, origin: Point, angle: f64, advance: f64, line: usize) {
// Only the glyph right after an upright glyph can continue its cluster.
let cluster = self.upright_cluster.take();
// Control characters (tabs) and soft hyphens draw nothing (fonts map them to .notdef).
let blank = g.is_soft_hyphen() || g.ch.is_control() || g.ch == '\u{2028}';
// A colour glyph draws its art (COLR layers, an SVG document) or its image (a bitmap, or
// COLR paints documents have no form for), not its outline; COLR, then SVG, then bitmaps.
// Art in the text's colours takes the run's fill (black when it isn't a colour) and
// stroke.
let plain = g.inline.is_some() || blank;
let colour_of = |p: &vectorcraft_doc::color::Paint| match p {
vectorcraft_doc::color::Paint::Solid { color, .. } => Some(*color),
_ => None,
};
let st = self.style_at(g.byte);
let foreground = colour_of(&st.fill).unwrap_or(vectorcraft_doc::color::Color::BLACK);
let stroke = colour_of(&st.stroke).filter(|_| st.has_stroke());
let (colour, image) = match (!plain).then(|| self.db.glyph_colour(&g.face, g.gid, foreground, stroke)).flatten() {
Some(crate::colr::ColourContent::Art(art)) => (Some(art), None),
Some(crate::colr::ColourContent::Image(image)) => (None, Some(image)),
None => (None, (!plain).then(|| self.db.glyph_image(&g.face, g.gid)).flatten()),
};
let src = if g.inline.is_some() || image.is_some() || colour.is_some() {
std::sync::Arc::new(BezPath::new())
} else {
self.db.outline(&g.face, g.gid)
};
// A glyph whose leading space was taken off (mojikumi) is drawn that much earlier: an
// upright one in vertical type by moving it up the column once it stands upright.
let upright = self.vertical && !self.on_path && g.tcy.is_none() && stands_upright(g);
let lead = if upright { 0.0 } else { g.lead };
let local = match &g.inline {
// Inline graphics: art space to glyph space (no font outline).
Some(ib) => Affine::translate((-lead, -g.bshift)) * ib.xf,
// Baseline shift moves the glyph across its line, and Character Rotation then turns it
// about its shifted pen position, so the shift is across the line at any rotation.
// Right-to-left manual kerning moves that pen position along the line.
None => {
Affine::translate((g.kern_shift, -g.bshift))
* Affine::rotate(-g.rotation.to_radians())
* Affine::translate((g.dx - lead, g.dy))
* Affine::scale_non_uniform(g.sx, g.sy)
}
};
// The turning point in font units, which later moves of the glyph's transform keep.
let pivot = match local.determinant() {
det if det != 0.0 && det.is_finite() => local.inverse() * Point::new(g.kern_shift, -g.bshift),
_ => Point::ZERO,
};
let unturned = if g.inline.is_none() && g.rotation != 0.0 && g.sx != 0.0 && g.sy != 0.0 {
let placed = Affine::translate((g.dx - lead, g.dy)) * Affine::scale_non_uniform(g.sx, g.sy);
placed.inverse() * Affine::rotate(g.rotation.to_radians()) * placed
} else {
Affine::IDENTITY
};
let mut m = pre * local;
if self.vertical && self.on_path {
// Vertical path type keeps the baseline path and turns each glyph across it.
m = Affine::rotate_about(std::f64::consts::FRAC_PI_2, origin) * m;
} else if let (true, Some(t)) = (self.vertical, g.tcy) {
// Tate-chu-yoko: the block set across the column, centred on its em of it, then moved up
// by its Up/Down and right by its Left/Right (line x runs down the column and line y
// from right to left across it).
let em = self.style_at(g.byte).size;
let start = origin.x - t.pen;
let centre = Point::new(start + em * 0.5, origin.y - g.face.ideographic_centre() * em);
let across = Affine::translate((centre.x - t.width * 0.5 + t.ink - origin.x, 0.0));
let squeeze = Affine::translate((centre.x, 0.0)) * Affine::scale_non_uniform(t.squeeze, 1.0) * Affine::translate((-centre.x, 0.0));
let offset = Affine::translate((-t.up, -t.right));
m = offset * Affine::rotate_about(-std::f64::consts::FRAC_PI_2, centre) * squeeze * across * m;
} else if self.vertical && stands_upright(g) {
let next = self.out.glyphs.len();
let after_base = self.out.glyphs.last().is_some_and(|p| p.byte == g.byte && p.line == line && p.len > 0);
if let Some((_, _, turn, after)) = cluster.filter(|c| c.0 == g.byte && c.1 == next && after_base && g.len > 0) {
// A later glyph of an upright glyph's cluster: set after it as the shaper placed
// it, and turned with it.
m = turn * Affine::translate((after - origin.x, 0.0)) * m;
self.upright_cluster = Some((g.byte, next + 1, turn, after));
} else {
// Turned about the centre of its cell, which the column's centre line runs through:
// the middle of its own cell, not of its advance (tracking and justification add
// space after the cell, and must not push the glyph off the centre line).
let em = self.style_at(g.byte).size;
let cell = if g.adv > 0.0 { upright_cell(g) } else { advance };
// A leading space taken off moves it up the column after the turn (moving the
// turning point instead would move it across the column too).
let turn = Affine::translate((-g.lead, 0.0))
* Affine::rotate_about(-std::f64::consts::FRAC_PI_2, Point::new(origin.x + cell * 0.5, origin.y - upright_centre(g, cell, em)));
m = turn * m;
// Its own advance across, from where the cell centres it: where the shaper set the
// next glyph of its cluster.
let across = g.face.advance(g.gid) * g.sx;
self.upright_cluster = (g.len > 0).then_some((g.byte, next + 1, turn, origin.x + (cell - across) * 0.5 + across));
}
}
let outline = if src.elements().is_empty() || blank {
BezPath::new()
} else {
let mut p = BezPath::with_capacity(src.elements().len());
for el in src.elements() {
p.push(m * *el);
}
p
};
let font_id = g.face.id();
if let Some(art) = g.inline.as_ref().and_then(|ib| ib.art) {
self.out.inlines.push(InlineGlyph { run: g.run, byte: g.byte, glyph: self.out.glyphs.len(), xf: m, bounds: m.transform_rect_bbox(art) });
}
self.out.glyphs.push(PositionedGlyph {
outline,
run: g.run,
byte: g.byte,
origin,
advance,
len: g.len,
angle,
line,
font_id,
gid: g.gid,
xf: m,
// Vertical type keeps its logical order down the column.
rtl: g.level.is_rtl() && !self.vertical,
scale: 1.0,
image,
colour,
bend: None,
unturned,
pivot,
});
}
/// Draw a kashida `len` points long from `at` (the pen on the baseline) on line `line`, as
/// tatweels in the face, size and style of glyph `g`. Short, Medium and Long kashidas are one
/// tatweel stretched along the baseline; a Stylistic one is tatweels side by side. Each tatweel
/// is a glyph at `g`'s byte offset that stands for no character (length 0) and does not
/// advance: `g`'s advance takes the kashida in.
fn emit_kashida(&mut self, g: &SGlyph, kind: Kashida, len: f64, at: Point, line: usize) {
let Some((gid, t)) = kashida::tatweel(g) else { return };
let mut k = g.clone();
k.gid = gid;
k.adv = t;
k.dx = 0.0;
k.dy = 0.0;
k.kern_shift = 0.0;
k.len = 0;
k.lead = 0.0;
k.ch = kashida::TATWEEL;
k.inline = None;
k.tcy = None;
if kind == Kashida::Stylistic {
let n = (len / t).round().clamp(1.0, kashida::MAX_TATWEELS) as usize;
for m in 0..n {
let x = at.x + m as f64 * t;
self.emit(&k, Affine::translate((x, at.y)), Point::new(x, at.y), 0.0, 0.0, line);
}
return;
}
self.emit(&k, Affine::translate((at.x, at.y)) * Affine::scale_non_uniform(len / t, 1.0), at, 0.0, 0.0, line);
}
}
#[cfg(test)]
mod wrapping_tests;