Files
vectorcraft/crates/text/src/layout/driver.rs
T
dotCooCoo 2606b70604 Type: Dynamic Text sizes type on a path to fill its brackets
Type › Dynamic Text (`type.dynamicText`, also on the Contextual Task Bar
for type on a path) scales the text by one factor so it fills the span
between its start and end brackets, and the layout fits it again after
each edit to the text, the path or a bracket. The first character's
size moves in 0.1 pt steps within 0.1 to 1,296 pt. Turning it off, or
threading the type, keeps the sizes it was drawn at.

- The model saves `dynamicText` only when on.
- SVG, the legacy .ai writer and the .ai editing data write the drawn
  sizes through `vectorcraft_text::drawn`; the editing data warns that
  the setting is not written.
- Shrink Text to Fit and Dynamic Text step the size the Character panel
  shows when a size measures the cap height, x-height or ICF box.

Docs: docs/mcp.md, docs/type-parity.md, docs/target-app-parity.md.
Part of #4134.
2026-11-11 10:42:49 -08:00

550 lines
25 KiB
Rust
Raw 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.
use super::flow::flow;
use super::on_path::on_path;
use super::region::Region;
use super::ruby::ruby_layout;
use super::vertical::{EM_CENTER, QUARTER_TURN, align_vertically, finish_bounds};
use super::{
Affine, BezPath, CharStyle, Ctx, FontDb, LayoutOptions, OtFeatures, Point, Rect, Shape, TextKind, TextLayout, TextObject, VerticalAlign,
keep_lone_ligature_carets,
};
/// Lay out a text object into text-space glyph outlines (default [`LayoutOptions`]).
pub fn layout(db: &FontDb, t: &TextObject) -> TextLayout {
let a = &t.area;
let opts = LayoutOptions {
rows: a.rows,
columns: a.columns,
gutter: a.gutter,
row_gutter: a.row_gutter,
inset: a.inset,
first_baseline: a.first_baseline,
first_baseline_min: a.first_baseline_min,
vertical_align: a.vertical_align,
flow: a.flow,
fit: a.fit,
..LayoutOptions::default()
};
layout_with(db, t, &opts)
}
/// Area type in a frame too short for its first line draws nothing (type that doesn't fit its frame
/// isn't drawn). For importers whose source shows that line: the frame is made taller, from its
/// top, by what the first line needs (vertical type's frame wider, from its right edge, where its
/// first column is). Whether it was.
pub fn fit_first_line(db: &FontDb, t: &mut TextObject) -> bool {
let TextKind::Area { frame } = &t.kind else { return false };
let across = |b: &Rect| if t.vertical { b.width() } else { b.height() };
let Some(b) = frame.bounds().filter(|b| across(b) > 1e-6) else { return false };
if !layout(db, t).lines.is_empty() {
return false;
}
// The height of its first line (a column's width): the line's as point type, its baseline at
// least Area Type Options › First Baseline's Min from the frame's top.
let point = TextObject { kind: TextKind::Point, ..t.clone() };
let min = if t.area.first_baseline_min.is_finite() { t.area.first_baseline_min.max(0.0) } else { 0.0 };
let Some(need) = layout(db, &point).lines.first().map(|l| l.ascent.max(min) + l.descent) else { return false };
// (A little more than it, for the rounding of the layout.)
let by = (need + 0.05) / across(&b);
if !(by.is_finite() && by > 1.0) {
return false;
}
let taller = if t.vertical {
frame.transformed(Affine::translate((b.x1, 0.0)) * Affine::scale_non_uniform(by, 1.0) * Affine::translate((-b.x1, 0.0)))
} else {
frame.transformed(Affine::translate((0.0, b.y0)) * Affine::scale_non_uniform(1.0, by) * Affine::translate((0.0, -b.y0)))
};
let short = std::mem::replace(&mut t.kind, TextKind::Area { frame: taller });
// Something else keeps the line out (a frame narrower than a letter): as the source has it.
if layout(db, t).lines.is_empty() {
t.kind = short;
return false;
}
true
}
/// Area type whose source lays out `lines` lines in its frame, more than fit here: a font that isn't
/// available shows in the fallback font, whose lines may reach lower, and the source may set a last
/// line whose descent passes the frame's bottom, which isn't drawn here. For importers: the frame is
/// made taller, from its bottom, by its lines' deepest descent plus 0.05 pt, when that lays out
/// more of the source's lines and no more than it has. Horizontal type only. Whether it was.
pub fn fit_source_lines(db: &FontDb, t: &mut TextObject, lines: usize) -> bool {
let TextKind::Area { frame } = &t.kind else { return false };
if t.vertical {
return false;
}
let Some(b) = frame.bounds().filter(|b| b.height() > 1e-6) else { return false };
let laid = layout(db, t);
let have = laid.lines.len();
if !laid.overflow || have == 0 || have >= lines {
return false;
}
// The deepest descent of its lines (a little more, for the rounding of the layout).
let Some(descent) = laid.lines.iter().map(|l| l.descent).reduce(f64::max).filter(|d| d.is_finite() && *d > 0.0) else { return false };
let by = (b.height() + descent + 0.05) / b.height();
if !(by.is_finite() && by > 1.0) {
return false;
}
let taller = frame.transformed(Affine::translate((0.0, b.y0)) * Affine::scale_non_uniform(1.0, by) * Affine::translate((0.0, -b.y0)));
let short = std::mem::replace(&mut t.kind, TextKind::Area { frame: taller });
let now = layout(db, t).lines.len();
if now > have && now <= lines {
return true;
}
t.kind = short;
false
}
/// The underline and strikethrough bars of `t` laid out as `layout` (#847): per run and line, a
/// bar from the stretch's first glyph to its last one's advance, placed and sized by the font's
/// own underline and strikeout metrics through the glyph's transform (so the size, baseline shift
/// and scaling follow) → (run, bar in text space). Type on a path and vertical type have none yet.
pub fn decorations(layout: &TextLayout, db: &FontDb, t: &TextObject) -> Vec<(usize, BezPath)> {
let mut out = vec![];
if layout.on_path || layout.vertical {
return out;
}
let glyphs = &layout.glyphs;
let mut i = 0;
while let Some(g) = glyphs.get(i) {
// The stretch: the glyphs of this run on this line, in a row. A list marker has no
// underline or strikethrough.
let marker = layout.marker_of(i).is_some();
let end = glyphs
.iter()
.enumerate()
.skip(i)
.position(|(k, h)| h.run != g.run || h.line != g.line || layout.marker_of(k).is_some() != marker)
.map_or(glyphs.len(), |n| i + n);
let stretch = glyphs.get(i..end).unwrap_or_default();
i = end.max(i + 1);
if marker {
continue;
}
let Some(style) = t.runs.get(g.run).map(|r| &r.style) else { continue };
if !(style.underline || style.strikethrough) {
continue;
}
let Some(face) = db.face_by_id(g.font_id) else { continue };
let (x0, x1) = stretch.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), h| {
let (s, e) = (h.origin.x, h.origin.x + h.advance);
(a.min(s).min(e), b.max(s).max(e))
});
for (on, (offset, thickness)) in [(style.underline, face.underline), (style.strikethrough, face.strikeout)] {
if !on {
continue;
}
// Font units run y down in the glyph's transform: the bar's top is at -offset.
let (top, bottom) = ((g.xf * Point::new(0.0, -offset)).y, (g.xf * Point::new(0.0, -offset + thickness)).y);
let bar = Rect::new(x0, top.min(bottom), x1, top.max(bottom));
if bar.width() > 0.0 && bar.height() > 0.0 && bar.width().is_finite() && bar.height().is_finite() {
out.push((g.run, bar.to_path(0.1)));
}
}
}
out
}
/// The box of what `t` draws with text space mapped by `m`: its glyph outlines
/// ([`TextLayout::ink_bounds_in`]) and its underline and strikethrough bars ([`decorations`]). With
/// `projection` `(h, after)`, each point `m` maps is then drawn through `h` (a point `h` can't map
/// stays where it is, as the renderer draws it) and mapped by `after`; the outlines and bars are
/// flattened to within 1/1000 of their size, and images, color glyphs and inline graphics count by
/// their boxes' corners. What an envelope maps type from ([`register_envelope_ink`]). `None` when
/// nothing draws.
pub fn envelope_ink(t: &TextObject, m: Affine, projection: Option<(vectorcraft_geom::Homography, Affine)>) -> Option<Rect> {
let db = FontDb::global();
let laid = layout(db, t);
let bars = decorations(&laid, db, t).into_iter().map(|(_, bar)| bar);
let union = |acc: Option<Rect>, b: Rect| Some(acc.map_or(b, |a| a.union(b)));
let Some((h, after)) = projection else {
return bars.map(|bar| (m * bar).bounding_box()).fold(laid.ink_bounds_in(m), union);
};
let draw = |p: Point| {
let q = m * p;
after * h.apply(q).unwrap_or(q)
};
let add = |acc: Option<Rect>, p: Point| union(acc, Rect::from_points(p, p));
let projected = |path: &BezPath| {
let size = path.bounding_box();
let tolerance = (size.width().max(size.height()) / 1000.0).max(1e-6);
let mut points = vec![];
kurbo::flatten(path, tolerance, |el| {
if let kurbo::PathEl::MoveTo(p) | kurbo::PathEl::LineTo(p) = el {
points.push(draw(p));
}
});
points.into_iter().fold(None, add)
};
let corners = |r: Rect| {
[Point::new(r.x0, r.y0), Point::new(r.x1, r.y0), Point::new(r.x1, r.y1), Point::new(r.x0, r.y1)].map(draw).into_iter().fold(None, add)
};
let glyphs = laid.glyphs.iter().chain(&laid.ruby).filter_map(|g| projected(&g.outline));
let boxes = laid.glyphs.iter().flat_map(|g| {
let image = g.placed_image().map(|(image, xf)| xf.transform_rect_bbox(image.rect()));
let colour = g.colour.as_ref().and_then(|c| c.geometric_bounds()).map(|b| g.xf.transform_rect_bbox(b));
[image, colour]
});
let boxes = boxes.chain(laid.inlines.iter().map(|i| Some(i.bounds))).flatten().filter_map(corners);
glyphs.chain(bars.filter_map(|bar| projected(&bar))).chain(boxes).fold(None, union)
}
/// Make envelopes map type from [`envelope_ink`], measured again when the fonts or the hyphenation
/// exceptions change (the first registration stays, see `vectorcraft_doc::live::set_type_ink`).
pub fn register_envelope_ink() {
vectorcraft_doc::live::set_type_ink(envelope_ink, envelope_ink_inputs);
}
/// A number that changes whenever what [`envelope_ink`] reads besides the type changes: the
/// installed fonts and the hyphenation exceptions.
pub(crate) fn envelope_ink_inputs() -> u64 {
FontDb::global().generation().rotate_left(32) ^ crate::hyphen::exceptions_generation()
}
/// Most layout passes Shrink Text to Fit runs past the first (bisection plus the final pass).
const SHRINK_PASSES: usize = 11;
/// Lay out a text object with explicit options (area type rows/columns, inset, first baseline,
/// fit, composer, OpenType features). Each run is set at the em size its font height reference
/// gives it ([`FontDb::font_heights_resolved`]).
pub fn layout_with(db: &FontDb, t: &TextObject, opts: &LayoutOptions) -> TextLayout {
#[cfg(any(test, feature = "test-fonts"))]
crate::test_fonts::count_layout();
// The sizes the fits step and limit are the ones the Character panel shows (`shown`), whatever
// em size a font height reference sets them at.
let shown = t;
let resolved = db.font_heights_resolved(t);
let t = &*resolved;
let first = layout_once(db, t, opts);
match (opts.fit.min_scale(), &t.kind) {
(Some(min), TextKind::Area { .. }) if first.overflow => shrink_to_fit(db, t, shown, opts, min).unwrap_or(first),
(_, TextKind::OnPath { .. }) if t.dynamic_text => fill_path(db, t, shown, opts, &first).unwrap_or(first),
_ => first,
}
}
/// The document size of the first character of `t` (its run's size; an empty run before it draws
/// nothing), in tenths of a point. None when it is not a usable size.
fn first_size_tenths(t: &TextObject) -> Option<f64> {
let points = t.style_scale()?.points;
let run = t.runs.iter().find(|r| !r.text.is_empty()).or(t.runs.first());
run.map(|r| r.style.size * points * 10.0).filter(|s| s.is_finite() && *s > 0.0)
}
/// Most layout passes Dynamic Text runs past the first (a guess, a search out from it, bisection).
const DYNAMIC_PASSES: usize = 11;
/// The sizes, in tenths of a document point, Dynamic Text keeps the first run's size within: 0.1
/// to 1,296 pt, the Character panel's range.
const DYNAMIC_TENTHS: (f64, f64) = (1.0, 12_960.0);
/// Dynamic Text for type on a path: the largest scale at which the text does not run past its end
/// bracket, so that it fills the span between its brackets. The first run's document size moves in
/// steps of 0.1 pt within 0.1 to 1,296 pt; at either limit the text keeps that size, and at the
/// smallest it may still overflow. Each guess is the size tried last times the span's length over
/// the length the text took there, which is exact for a straight span with no Spacing; a guess
/// outside the sizes still open gives way to a step out from the last one (doubling) or to
/// bisection. At most [`DYNAMIC_PASSES`] passes. `t` is laid out; the sizes are those of `shown`,
/// `t` before its font height references were resolved. None (keep `first`) when the text takes no
/// length, the span has none, or the text has no usable size.
fn fill_path(db: &FontDb, t: &TextObject, shown: &TextObject, opts: &LayoutOptions, first: &TextLayout) -> Option<TextLayout> {
let tenths = first_size_tenths(shown)?;
let (min, max) = DYNAMIC_TENTHS;
// The size in tenths that would fill the span, from a layout at `k` tenths.
let guess = |k: f64, l: &TextLayout| -> Option<f64> {
let (used, room) = l.path_fill?;
let g = (k * room / used).floor();
(used.is_finite() && used > 1e-9 && room.is_finite() && room > 1e-9 && g.is_finite()).then(|| g.clamp(min, max))
};
let mut next = guess(tenths, first)?;
let at = |k: f64| -> TextLayout {
#[cfg(test)]
FIT_PASSES.set(FIT_PASSES.get() + 1);
let f = k / tenths;
let mut l = layout_once(db, &scaled(t, f), opts);
l.fit_scale = f;
l
};
// The largest size tried that fits, and the smallest that overflows, each with its layout.
let mut fits: Option<(f64, TextLayout)> = None;
let mut over: Option<(f64, TextLayout)> = None;
let mut step = 1.0;
for _ in 0..DYNAMIC_PASSES {
let l = at(next);
let guessed = guess(next, &l);
if l.overflow {
if over.as_ref().is_none_or(|(o, _)| next < *o) {
over = Some((next, l));
}
} else if fits.as_ref().is_none_or(|(f, _)| next > *f) {
fits = Some((next, l));
}
// The sizes still open lie strictly between the largest that fits and the smallest that
// overflows, within the limits.
let lo = fits.as_ref().map_or(min - 1.0, |f| f.0);
let hi = over.as_ref().map_or(max + 1.0, |o| o.0);
if hi - lo <= 1.0 {
break;
}
next = match guessed.filter(|g| *g > lo && *g < hi) {
Some(g) => g,
None if fits.is_some() && over.is_some() => (lo + (hi - lo) * 0.5).floor(),
// Everything tried fits: try larger. Everything tried overflows: try smaller.
None if fits.is_some() => (lo + step).min(hi - 1.0),
None => (hi - step).max(lo + 1.0),
};
step *= 2.0;
}
// The largest that fits, or at the smallest size (or at the pass limit) the smallest tried.
fits.or(over).map(|(_, l)| l)
}
/// Shrink Text to Fit for text that overflows at full size: the largest scale in `min..1` at which
/// it fits, searched by bisection over the first run's document size in steps of 0.1 pt (so its scaled size
/// is a whole number of tenths of a point, the same every time). Down at `min` it lays out there,
/// still overflowing. `t` is laid out; the sizes are those of `shown`, `t` before its font height
/// references were resolved. None (keep the full-size layout) when the text has no usable size.
fn shrink_to_fit(db: &FontDb, t: &TextObject, shown: &TextObject, opts: &LayoutOptions, min: f64) -> Option<TextLayout> {
// Exact size edits after object scaling may store a local size below 0.1. Fit in the
// same document points as the Character controls, independent of that representation.
let points = shown.style_scale()?.points;
let size = shown.runs.first().map(|r| r.style.size * points).filter(|s| s.is_finite() && *s >= 0.1)?;
if !(min.is_finite() && min < 1.0) {
return None;
}
let tenths = size * 10.0;
if !tenths.is_finite() {
return None;
}
// Candidate sizes k/10 pt for k in lo..hi: `hi` (full size, rounded down) is known to overflow
// unless it is below the full size; `lo` is the smallest allowed.
let mut lo = (tenths * min).ceil().max(1.0);
let mut hi = tenths.floor();
if hi < lo {
hi = lo;
}
let at = |k: f64| -> TextLayout {
let f = (k / tenths).clamp(min, 1.0);
let mut l = layout_once(db, &scaled(t, f), opts);
l.fit_scale = f;
l
};
let mut passes = 0;
let mut best: Option<TextLayout> = None;
// `hi` itself may fit when the size isn't a whole number of tenths.
if hi < tenths && hi > lo {
let l = at(hi);
passes += 1;
if !l.overflow {
return Some(l);
}
}
// Invariant: everything at `hi` or above overflows; `lo` fits or is the floor.
while hi - lo > 1.0 && passes + 1 < SHRINK_PASSES {
let mid = (lo + (hi - lo) * 0.5).floor();
let l = at(mid);
passes += 1;
if l.overflow {
hi = mid;
} else {
lo = mid;
best = Some(l);
}
}
// `best` is the layout at `lo` once `lo` has moved; at the floor it still needs laying out.
Some(best.unwrap_or_else(|| at(lo)))
}
/// `t` with every run's size, explicit leading, baseline shift and tate-chu-yoko offsets scaled by `f`, inline art's own
/// shift included (auto leading and the art's size follow the size; paragraph spacing stays).
/// Shrink Text to Fit lays `t` out as `scaled(t, f)`, where `f` is the layout's
/// [`TextLayout::fit_scale`], so the result holds the styles the text is drawn in.
pub fn scaled(t: &TextObject, f: f64) -> TextObject {
let mut s = t.clone();
for r in &mut s.runs {
r.style.size *= f;
r.style.leading = r.style.leading.map(|l| l * f);
r.style.baseline_shift *= f;
r.style.tcy_up_down *= f;
r.style.tcy_left_right *= f;
if let Some(a) = &mut r.inline {
a.baseline_shift *= f;
}
}
s
}
/// `t` in the styles its layout draws it in: [`scaled`] by the layout's
/// [`TextLayout::fit_scale`] when Shrink Text to Fit (area type) or Dynamic Text (type on a path)
/// scales it, `t` itself otherwise. Text with neither setting is not laid out.
pub fn drawn(t: &TextObject) -> std::borrow::Cow<'_, TextObject> {
let fits = match &t.kind {
TextKind::Area { .. } => t.area.fit.min_scale().is_some(),
TextKind::OnPath { .. } => t.dynamic_text,
TextKind::Point => false,
};
let f = if fits { layout(FontDb::global(), t).fit_scale } else { 1.0 };
if f != 1.0 { std::borrow::Cow::Owned(scaled(t, f)) } else { std::borrow::Cow::Borrowed(t) }
}
#[cfg(test)]
thread_local! {
/// How many layouts Dynamic Text ran past the first on this thread ([`fit_passes`]).
static FIT_PASSES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
/// How many layouts Dynamic Text ran past the first on this thread.
#[cfg(test)]
pub(crate) fn fit_passes() -> usize {
FIT_PASSES.get()
}
#[cfg(test)]
thread_local! {
/// Set while [`layout_faces_per_call`] lays text out: layout keeps no run faces.
static FACES_PER_CALL: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
/// [`layout`] keeping no run faces, so that every lookup of a face asks [`FontDb::style_face`]
/// again: the reference the faces kept per run are compared with.
#[cfg(test)]
pub(crate) fn layout_faces_per_call(db: &FontDb, t: &TextObject) -> TextLayout {
FACES_PER_CALL.set(true);
let l = layout(db, t);
FACES_PER_CALL.set(false);
l
}
/// One layout pass, without fitting. Runs set in a composite font are laid out as the runs of one
/// class each that [`FontDb::composite_split`] makes of them; the glyphs, ruby and inline
/// graphics still name the runs of `t`.
fn layout_once(db: &FontDb, t: &TextObject, opts: &LayoutOptions) -> TextLayout {
let Some((split, from)) = db.composite_split(t) else { return layout_runs(db, t, opts, None) };
let mut l = layout_runs(db, &split, opts, Some(t));
let original = |run: &mut usize| *run = from.get(*run).copied().unwrap_or(*run);
l.glyphs.iter_mut().chain(l.ruby.iter_mut()).for_each(|g| original(&mut g.run));
l.inlines.iter_mut().for_each(|i| original(&mut i.run));
l
}
/// [`layout_once`] for text set in no composite font: `t` itself, or with `set`, the pieces a
/// composite font splits `set` into (ruby readings take their style from `set`'s runs).
fn layout_runs(db: &FontDb, t: &TextObject, opts: &LayoutOptions, set: Option<&TextObject>) -> TextLayout {
let mut set_runs = Vec::new();
let mut off = 0;
for r in set.map(|s| s.runs.as_slice()).unwrap_or_default() {
set_runs.push((off..off + r.text.len(), &r.style));
off += r.text.len();
}
let text = t.plain_text();
let mut runs = Vec::with_capacity(t.runs.len());
let mut inlines = Vec::with_capacity(t.runs.len());
let mut off = 0;
for r in &t.runs {
runs.push((off..off + r.text.len(), &r.style));
// Only a well-formed inline run (one object replacement character) is a graphic.
inlines.push(r.inline.as_ref().filter(|_| crate::edit::is_inline_text(&r.text)));
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 vertical = t.vertical;
let is_on_path = matches!(&t.kind, TextKind::OnPath { .. });
let vopts;
let opts = if vertical {
vopts = LayoutOptions { features: OtFeatures { vertical: true, ..opts.features }, ..opts.clone() };
&vopts
} else {
opts
};
let faces = runs.iter().map(|_| std::cell::OnceCell::new()).collect();
#[cfg(test)]
let faces = if FACES_PER_CALL.get() { Vec::new() } else { faces };
let mut cx = Ctx {
on_path: is_on_path,
db,
text: &text,
faces,
runs,
set_runs,
inlines,
default: CharStyle::default(),
opts,
vertical,
system_layout: t.system_layout,
fractional_widths: t.fractional_widths,
upright_cluster: None,
tcy_block: None,
out: TextLayout::default(),
};
// Vertical type: line space turned a quarter turn clockwise (lines become columns, each next
// one to the left). Point type's anchor is on the first column's centre line. Vertical path
// type stays on its path (each glyph turned across it in `emit`).
let line_xf = match &t.kind {
_ if !vertical => Affine::IDENTITY,
TextKind::OnPath { .. } => Affine::IDENTITY,
TextKind::Point => {
let first = cx.style_at(0);
let centre = cx.face_at(0).map_or(EM_CENTER, |f| f.ideographic_centre());
Affine::translate((-centre * first.size, 0.0)) * QUARTER_TURN
}
_ => QUARTER_TURN,
};
match &t.kind {
TextKind::Point => flow(&mut cx, &paras, t, None),
TextKind::Area { frame } => {
let to_lines = line_xf.inverse();
let wrap: Vec<vectorcraft_doc::WrapShape> = if vertical {
t.wrap.iter().map(|w| vectorcraft_doc::WrapShape { path: w.path.transformed(to_lines), ..w.clone() }).collect()
} else {
t.wrap.clone()
};
let mut regions = Region::cells(&(to_lines * frame.to_bezpath()), opts, &wrap);
cx.out.frames = regions.iter().map(|r| line_xf.transform_rect_bbox(r.cell)).collect();
flow(&mut cx, &paras, t, Some(&regions));
if opts.vertical_align != VerticalAlign::Top {
align_vertically(&mut cx, &paras, t, &mut regions);
}
}
TextKind::OnPath { path, .. } => on_path(&mut cx, &paras, t, path),
}
// Ruby (furigana): placed over its base text in line space, before vertical type turns the
// lines into columns, so the same pass puts it above (right in vertical type) and widens the
// lines it sits on. Type on a path has no above/below; its ruby is left out for now.
if !is_on_path && !t.ruby.is_empty() {
ruby_layout(&mut cx, t);
}
if vertical && !is_on_path {
// Glyph origins, outlines and transforms to text space (lines stay in line space).
for g in &mut cx.out.glyphs {
g.outline.apply_affine(line_xf);
g.xf = line_xf * g.xf;
g.origin = line_xf * g.origin;
g.angle += std::f64::consts::FRAC_PI_2;
}
// Ruby glyphs are placed in line space too (upright, along the column, in the vertical
// pass); the same turn takes them to text space.
for g in &mut cx.out.ruby {
g.outline.apply_affine(line_xf);
g.xf = line_xf * g.xf;
g.origin = line_xf * g.origin;
g.angle += std::f64::consts::FRAC_PI_2;
}
for i in &mut cx.out.inlines {
i.xf = line_xf * i.xf;
i.bounds = line_xf.transform_rect_bbox(i.bounds);
}
cx.out.vertical = true;
cx.out.line_xf = line_xf;
}
keep_lone_ligature_carets(&mut cx.out.glyphs);
finish_bounds(&mut cx.out);
cx.out
}