refactor(text): split flow and ruby layout phases

This commit is contained in:
Dylan M. Taylor
2026-10-27 17:47:12 +00:00
parent daad363da4
commit 0e2815629f
9 changed files with 868 additions and 672 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ use super::names::norm;
#[cfg(not(target_arch = "wasm32"))]
use super::scan::{file_face_names_within, is_font_file};
use super::traits::FaceStyle;
use super::*;
use super::{HashMap, Path};
/// A font a document names that isn't available, to look for in font files ([`WantedFonts`]).
#[derive(Clone, Debug, PartialEq, Eq)]
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{BidiInfo, Level, ParaDirection, Range, SGlyph};
/// The bidirectional resolution (UAX #9) of paragraph `text`, its base direction `direction` or,
/// without one, from its first strong character. `None` for the usual paragraph with nothing right
+4 -1
View File
@@ -1,5 +1,8 @@
use super::region::Region;
use super::*;
use super::{
Affine, BezPath, Breakpoint, Burasagari, EPS, FirstBaseline, FontDb, HyphenationOptions, Kinsoku, LeadingModel, SGlyph, hyphen_glyph,
hyphen_points_with, no_line_end, no_line_start, soft_line_start,
};
/// Vertical metrics of a line: (ascent, descent, leading, cap height, x height).
#[derive(Clone, Copy, Debug, PartialEq)]
+3 -1
View File
@@ -3,7 +3,9 @@ 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::*;
use super::{
Affine, BezPath, CharStyle, Ctx, FontDb, LayoutOptions, OtFeatures, Point, Rect, Shape, TextKind, TextLayout, TextObject, VerticalAlign,
};
/// Lay out a text object into text-space glyph outlines (default [`LayoutOptions`]).
pub fn layout(db: &FontDb, t: &TextObject) -> TextLayout {
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -2,7 +2,10 @@ use super::bidi::{is_rtl, para_bidi_with, visual_order};
use super::breaking::{Metrics, optical_hangs, optical_hangs_rtl, optical_share, roman_share};
use super::flow::list_markers;
use super::region::in_range;
use super::*;
use super::{
Affine, ArcPath, BezPath, Ctx, GlyphBend, Justify, LineInfo, ListMarker, ParamCurve, PathAlign, PathData, PathEffect, PathEl, Point, Range,
SGlyph, Shape, TextObject, Vec2, style_metrics,
};
/// The distance along type on a path each glyph takes: its advance measured `spacing` points above
/// the path (Type on a Path Options › Spacing), so glyphs close up round the outside of a curve and
+3 -1
View File
@@ -1,4 +1,6 @@
use super::*;
#[cfg(test)]
use super::PathData;
use super::{BezPath, LayoutOptions, PathEl, Point, Rect, Shape};
/// A flattened text wrap shape: (polygons, offset, invert).
pub(super) type FlatWrap = (Vec<Vec<Point>>, f64, bool);
+278 -232
View File
@@ -1,5 +1,5 @@
use super::driver::layout;
use super::*;
use super::{Affine, CharStyle, Ctx, EPS, Point, PositionedGlyph, RubyAlign, RubyPosition, Shape, TextKind, TextObject};
/// Ruby (furigana): lays each reading out over its base text in line space, grows the line's
/// ascent/descent so the ruby fits, and moves the lines after it on in the block direction.
@@ -11,270 +11,316 @@ use super::*;
/// A reading wider than its base hangs over the neighbours (`overhang`), or is squeezed to the
/// base's width. Type on a path is left out (it has no above/below).
pub(super) fn ruby_layout(cx: &mut Ctx<'_>, t: &TextObject) {
use std::collections::BTreeMap;
let db = cx.db;
let text_len = cx.text.len();
let n_lines = cx.out.lines.len();
if n_lines == 0 || cx.out.glyphs.is_empty() {
let line_count = cx.out.lines.len();
if line_count == 0 || cx.out.glyphs.is_empty() {
return;
}
let base_baselines: Vec<f64> = cx.out.lines.iter().map(|l| l.baseline).collect();
let old_ascent: Vec<f64> = cx.out.lines.iter().map(|l| l.ascent).collect();
let old_descent: Vec<f64> = cx.out.lines.iter().map(|l| l.descent).collect();
// One piece of base text on one line, with the part of the reading that belongs to it.
struct Piece {
line: usize,
x0: f64,
x1: f64,
run: usize,
byte: usize,
reading: String,
style: CharStyle,
position: RubyPosition,
align: RubyAlign,
base_size: f64,
overhang: bool,
let base_baselines = cx.out.lines.iter().map(|line| line.baseline).collect::<Vec<_>>();
let old_ascent = cx.out.lines.iter().map(|line| line.ascent).collect::<Vec<_>>();
let old_descent = cx.out.lines.iter().map(|line| line.descent).collect::<Vec<_>>();
let pieces = ruby_pieces(cx, t);
let placed = place_ruby_pieces(cx, &pieces, &base_baselines, &old_descent);
if !placed.is_empty() {
grow_ruby_lines(cx, t, placed, &old_ascent, &old_descent);
}
let mut pieces: Vec<Piece> = Vec::new();
// The glyphs by their byte, to find a span's without a scan of them all for each span.
}
struct RubyPiece {
line: usize,
x0: f64,
x1: f64,
run: usize,
byte: usize,
reading: String,
style: CharStyle,
position: RubyPosition,
align: RubyAlign,
base_size: f64,
overhang: bool,
}
struct PlacedRuby {
line: usize,
glyphs: Vec<PositionedGlyph>,
ascent: f64,
descent: f64,
}
fn ruby_pieces(cx: &Ctx<'_>, t: &TextObject) -> Vec<RubyPiece> {
use std::collections::BTreeMap;
let text_len = cx.text.len();
let byte_of = |i: usize| cx.out.glyphs.get(i).map_or(0, |g| g.byte);
let mut by_byte: Vec<usize> = (0..cx.out.glyphs.len()).collect();
by_byte.sort_by_key(|&i| byte_of(i));
let longest = cx.out.glyphs.iter().map(|g| g.len.max(1)).max().unwrap_or(1);
let mut pieces = Vec::new();
for span in &t.ruby {
let (start, end) = (span.start.min(text_len), span.end.min(text_len));
if start >= end || span.text.trim().is_empty() {
continue;
}
// The base glyphs of the span, by line; a glyph's cluster is its byte range (`len`, or
// one byte for generated glyphs). A list marker is not part of the base.
let mut by_line: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
let hi = by_byte.partition_point(|&i| byte_of(i) < end);
let lo = by_byte.partition_point(|&i| byte_of(i).saturating_add(longest) <= start).min(hi);
let mut hits: Vec<usize> = by_byte.get(lo..hi).unwrap_or_default().to_vec();
// In glyph order, as the scan found them.
hits.sort_unstable();
for i in hits {
let Some(g) = cx.out.glyphs.get(i) else { continue };
let b = g.byte.saturating_add(g.len.max(1));
if g.byte < end && b > start && cx.out.marker_of(i).is_none() {
by_line.entry(g.line).or_default().push(i);
let Some(glyph) = cx.out.glyphs.get(i) else { continue };
let glyph_end = glyph.byte.saturating_add(glyph.len.max(1));
if glyph.byte < end && glyph_end > start && cx.out.marker_of(i).is_none() {
by_line.entry(glyph.line).or_default().push(i);
}
}
let total: usize = by_line.values().map(Vec::len).sum();
if total == 0 {
continue;
}
let reading: Vec<char> = span.text.trim().chars().collect();
let ratio = span.safe_size_percent() / 100.0;
// Split the reading by each line's share of the base glyphs: cumulative fractions keep
// every character once and in order.
let mut taken = 0;
let mut done = 0;
let lines: Vec<usize> = by_line.keys().copied().collect();
for line in &lines {
let count = by_line.get(line).map_or(0, Vec::len);
done += count;
let upto = if reading.is_empty() { 0 } else { (reading.len() * done / total).min(reading.len()) };
let part: String = reading.get(taken.min(reading.len())..upto.min(reading.len())).unwrap_or_default().iter().collect();
taken = upto;
if part.is_empty() {
continue;
}
let glyphs = by_line.get(line).map(Vec::as_slice).unwrap_or_default();
let mut x0 = f64::INFINITY;
let mut x1 = f64::NEG_INFINITY;
let mut base_size: f64 = 0.0;
let mut run = 0;
let mut byte = start;
for (j, &gi) in glyphs.iter().enumerate() {
let Some(g) = cx.out.glyphs.get(gi) else { continue };
x0 = x0.min(g.origin.x);
x1 = x1.max(g.origin.x + g.advance);
base_size = base_size.max(cx.style_at(g.byte).size);
if j == 0 {
run = g.run;
byte = g.byte;
}
}
if !(x0.is_finite() && x1.is_finite()) || base_size <= 0.0 {
continue;
}
let mut style = cx.style_at(byte).clone();
style.size = base_size * ratio;
style.leading = None;
style.baseline_shift = 0.0;
style.tracking = 0.0;
style.kerning = None;
pieces.push(Piece {
line: *line,
x0,
x1,
run,
byte,
reading: part,
style,
position: span.position,
align: span.align,
base_size,
overhang: span.overhang,
});
collect_ruby_pieces_for_span(cx, span, start, total, &by_line, &mut pieces);
}
pieces
}
fn collect_ruby_pieces_for_span(
cx: &Ctx<'_>,
span: &vectorcraft_doc::text::RubySpan,
start: usize,
total: usize,
by_line: &std::collections::BTreeMap<usize, Vec<usize>>,
pieces: &mut Vec<RubyPiece>,
) {
let reading: Vec<char> = span.text.trim().chars().collect();
let ratio = span.safe_size_percent() / 100.0;
let mut taken = 0;
let mut done = 0;
for (&line, glyph_indices) in by_line {
done += glyph_indices.len();
let upto = if reading.is_empty() { 0 } else { (reading.len() * done / total).min(reading.len()) };
let part: String = reading.get(taken.min(reading.len())..upto.min(reading.len())).unwrap_or_default().iter().collect();
taken = upto;
if part.is_empty() {
continue;
}
if let Some(piece) = ruby_piece_for_line(cx, line, start, &part, ratio, glyph_indices, span) {
pieces.push(piece);
}
}
// Phase A: shape each reading, place it relative to the (unshifted) baseline, and note how
// far above/below the baseline its ink reaches.
let mut placed: Vec<(usize, Vec<PositionedGlyph>, f64, f64)> = Vec::new(); // (line, glyphs, ascent, descent)
for piece in &pieces {
let Some(baseline) = base_baselines.get(piece.line).copied() else { continue };
let mini = TextObject::point(Point::ZERO, &piece.reading, piece.style.clone());
let lay = layout(db, &mini);
let Some(first_line) = lay.lines.first() else { continue };
let mini_base = first_line.baseline;
let mini_asc = first_line.ascent;
let mini_desc = first_line.descent;
let base_w = (piece.x1 - piece.x0).max(0.0);
// Above: the reading's bottom sits on the base's em box top. Below: its top sits on the
// base's em box bottom (the line's descent below the baseline).
let ruby_y = if piece.position == RubyPosition::Above {
baseline - piece.base_size - mini_desc
} else {
baseline + old_descent.get(piece.line).copied().unwrap_or(0.0) + mini_asc
};
let dest = |width: f64| match piece.align {
RubyAlign::Center => piece.x0 + (base_w - width) * 0.5,
RubyAlign::Start => piece.x0,
RubyAlign::End => piece.x1 - width,
};
let mut glyphs: Vec<PositionedGlyph> = Vec::new();
if cx.vertical {
// Each reading character in its own cell along the column, upright once the lines turn.
let cell = (mini_asc + mini_desc).max(piece.style.size * 0.5);
let mut clusters: Vec<Vec<&PositionedGlyph>> = Vec::new();
for g in &lay.glyphs {
match clusters.last_mut() {
Some(c) if c.last().is_some_and(|p| p.byte == g.byte && p.len == g.len) => c.push(g),
_ => clusters.push(vec![g]),
}
}
let mut ruby_w = clusters.len() as f64 * cell;
let mut squeeze = 1.0;
if !piece.overhang && ruby_w > base_w + EPS && ruby_w > EPS {
squeeze = base_w / ruby_w;
ruby_w = base_w;
}
let x = dest(ruby_w);
for (ci, cluster) in clusters.iter().enumerate() {
let anchor = Point::new(x + (ci as f64 + 0.5) * cell, ruby_y);
let mut box_: Option<Rect> = None;
for g in cluster {
if g.outline.elements().is_empty() {
continue;
}
let b = g.outline.bounding_box();
box_ = Some(box_.map_or(b, |a| a.union(b)));
}
let centre = box_.map_or(anchor, |b| Point::new((b.x0 + b.x1) * 0.5, (b.y0 + b.y1) * 0.5));
let mut m =
Affine::rotate_about(-std::f64::consts::FRAC_PI_2, anchor) * Affine::translate((anchor.x - centre.x, anchor.y - centre.y));
if squeeze != 1.0 {
m = Affine::translate((x, 0.0)) * Affine::scale_non_uniform(squeeze, 1.0) * Affine::translate((-x, 0.0)) * m;
}
for g in cluster {
glyphs.push(ruby_glyph(g, m, piece.run, piece.byte, piece.line));
}
}
} else {
// The reading set along the line: the line's extent, centred or aligned to the base.
let mut ruby_w = (first_line.x1 - first_line.x0).max(0.0);
let mut squeeze = 1.0;
if !piece.overhang && ruby_w > base_w + EPS && ruby_w > EPS {
squeeze = base_w / ruby_w;
ruby_w = base_w;
}
let x = dest(ruby_w);
let mut m = Affine::translate((x - first_line.x0, ruby_y - mini_base));
if squeeze != 1.0 {
m = Affine::translate((x, 0.0)) * Affine::scale_non_uniform(squeeze, 1.0) * Affine::translate((-x, 0.0)) * m;
}
for g in &lay.glyphs {
glyphs.push(ruby_glyph(g, m, piece.run, piece.byte, piece.line));
}
}
// How far the reading reaches above and below the base line's baseline (its ink).
let mut ascent: f64 = 0.0;
let mut descent: f64 = 0.0;
for g in &glyphs {
if g.outline.elements().is_empty() {
continue;
}
let b = g.outline.bounding_box();
ascent = ascent.max(baseline - b.y0);
descent = descent.max(b.y1 - baseline);
}
if !glyphs.is_empty() {
placed.push((piece.line, glyphs, ascent, descent));
}
fn ruby_piece_for_line(
cx: &Ctx<'_>,
line: usize,
start: usize,
reading: &str,
ratio: f64,
glyph_indices: &[usize],
span: &vectorcraft_doc::text::RubySpan,
) -> Option<RubyPiece> {
let mut x0 = f64::INFINITY;
let mut x1 = f64::NEG_INFINITY;
let mut base_size: f64 = 0.0;
let mut run = 0;
let mut byte = start;
for (index, &glyph_index) in glyph_indices.iter().enumerate() {
let Some(glyph) = cx.out.glyphs.get(glyph_index) else { continue };
x0 = x0.min(glyph.origin.x);
x1 = x1.max(glyph.origin.x + glyph.advance);
base_size = base_size.max(cx.style_at(glyph.byte).size);
if index == 0 {
run = glyph.run;
byte = glyph.byte;
}
}
if placed.is_empty() {
return;
if !(x0.is_finite() && x1.is_finite()) || base_size <= 0.0 {
return None;
}
// Phase B: grow each line's box to fit its ruby, then move the lines on after it by what they
// need in the block direction (the first line too for area type, so the ruby stays inside the
// frame; point type's first baseline is its anchor and stays).
let mut growth = vec![0.0_f64; n_lines];
for (line, _, ascent, descent) in &placed {
if let (Some(a), Some(d)) = (old_ascent.get(*line), old_descent.get(*line)) {
let grown = (a.max(*ascent) + d.max(*descent)) - (a + d);
if grown > 0.0 {
let g = growth.get_mut(*line);
if let Some(slot) = g {
*slot = slot.max(grown);
}
}
let mut style = cx.style_at(byte).clone();
style.size = base_size * ratio;
style.leading = None;
style.baseline_shift = 0.0;
style.tracking = 0.0;
style.kerning = None;
Some(RubyPiece {
line,
x0,
x1,
run,
byte,
reading: reading.to_string(),
style,
position: span.position,
align: span.align,
base_size,
overhang: span.overhang,
})
}
fn place_ruby_pieces(cx: &Ctx<'_>, pieces: &[RubyPiece], baselines: &[f64], old_descent: &[f64]) -> Vec<PlacedRuby> {
pieces.iter().filter_map(|piece| place_ruby_piece(cx, piece, baselines, old_descent)).collect()
}
fn place_ruby_piece(cx: &Ctx<'_>, piece: &RubyPiece, baselines: &[f64], old_descent: &[f64]) -> Option<PlacedRuby> {
let baseline = baselines.get(piece.line).copied()?;
let mini = TextObject::point(Point::ZERO, &piece.reading, piece.style.clone());
let layout = layout(cx.db, &mini);
let first_line = layout.lines.first()?;
let base_width = (piece.x1 - piece.x0).max(0.0);
let ruby_y = ruby_baseline(piece, baseline, first_line.ascent, first_line.descent, old_descent);
let glyphs = if cx.vertical {
place_vertical_ruby(piece, &layout.glyphs, first_line.ascent, first_line.descent, base_width, ruby_y)
} else {
place_horizontal_ruby(piece, &layout.glyphs, first_line, base_width, ruby_y)
};
let (ascent, descent) = ruby_ink_extents(&glyphs, baseline);
(!glyphs.is_empty()).then_some(PlacedRuby { line: piece.line, glyphs, ascent, descent })
}
fn ruby_baseline(piece: &RubyPiece, baseline: f64, reading_ascent: f64, reading_descent: f64, old_descent: &[f64]) -> f64 {
if piece.position == RubyPosition::Above {
baseline - piece.base_size - reading_descent
} else {
baseline + old_descent.get(piece.line).copied().unwrap_or(0.0) + reading_ascent
}
}
fn ruby_destination(piece: &RubyPiece, base_width: f64, reading_width: f64) -> f64 {
match piece.align {
RubyAlign::Center => piece.x0 + (base_width - reading_width) * 0.5,
RubyAlign::Start => piece.x0,
RubyAlign::End => piece.x1 - reading_width,
}
}
fn place_vertical_ruby(piece: &RubyPiece, reading: &[PositionedGlyph], asc: f64, desc: f64, base_width: f64, ruby_y: f64) -> Vec<PositionedGlyph> {
let cell = (asc + desc).max(piece.style.size * 0.5);
let clusters = ruby_clusters(reading);
let (ruby_width, squeeze) = ruby_width(clusters.len() as f64 * cell, base_width, piece.overhang);
let x = ruby_destination(piece, base_width, ruby_width);
let mut glyphs = Vec::new();
for (index, cluster) in clusters.iter().enumerate() {
let anchor = Point::new(x + (index as f64 + 0.5) * cell, ruby_y);
let centre = ruby_cluster_center(cluster, anchor);
let transform = squeeze_transform(
Affine::rotate_about(-std::f64::consts::FRAC_PI_2, anchor) * Affine::translate((anchor.x - centre.x, anchor.y - centre.y)),
x,
squeeze,
);
for glyph in cluster {
glyphs.push(ruby_glyph(glyph, transform, piece.run, piece.byte, piece.line));
}
}
glyphs
}
fn ruby_clusters(glyphs: &[PositionedGlyph]) -> Vec<Vec<&PositionedGlyph>> {
let mut clusters: Vec<Vec<&PositionedGlyph>> = Vec::new();
for glyph in glyphs {
match clusters.last_mut() {
Some(cluster) if cluster.last().is_some_and(|previous| previous.byte == glyph.byte && previous.len == glyph.len) => cluster.push(glyph),
_ => clusters.push(vec![glyph]),
}
}
clusters
}
fn ruby_cluster_center(cluster: &[&PositionedGlyph], anchor: Point) -> Point {
let bounds = cluster
.iter()
.filter(|glyph| !glyph.outline.elements().is_empty())
.map(|glyph| glyph.outline.bounding_box())
.reduce(|left, right| left.union(right));
bounds.map_or(anchor, |bounds| Point::new((bounds.x0 + bounds.x1) * 0.5, (bounds.y0 + bounds.y1) * 0.5))
}
fn place_horizontal_ruby(
piece: &RubyPiece,
reading: &[PositionedGlyph],
line: &super::LineInfo,
base_width: f64,
ruby_y: f64,
) -> Vec<PositionedGlyph> {
let reading_width = (line.x1 - line.x0).max(0.0);
let (ruby_width, squeeze) = ruby_width(reading_width, base_width, piece.overhang);
let x = ruby_destination(piece, base_width, ruby_width);
let transform = squeeze_transform(Affine::translate((x - line.x0, ruby_y - line.baseline)), x, squeeze);
reading.iter().map(|glyph| ruby_glyph(glyph, transform, piece.run, piece.byte, piece.line)).collect()
}
fn ruby_width(reading_width: f64, base_width: f64, overhang: bool) -> (f64, f64) {
if !overhang && reading_width > base_width + EPS && reading_width > EPS { (base_width, base_width / reading_width) } else { (reading_width, 1.0) }
}
fn squeeze_transform(transform: Affine, x: f64, squeeze: f64) -> Affine {
if squeeze == 1.0 {
transform
} else {
Affine::translate((x, 0.0)) * Affine::scale_non_uniform(squeeze, 1.0) * Affine::translate((-x, 0.0)) * transform
}
}
fn ruby_ink_extents(glyphs: &[PositionedGlyph], baseline: f64) -> (f64, f64) {
let mut ascent: f64 = 0.0;
let mut descent: f64 = 0.0;
for glyph in glyphs.iter().filter(|glyph| !glyph.outline.elements().is_empty()) {
let bounds = glyph.outline.bounding_box();
ascent = ascent.max(baseline - bounds.y0);
descent = descent.max(bounds.y1 - baseline);
}
(ascent, descent)
}
fn grow_ruby_lines(cx: &mut Ctx<'_>, t: &TextObject, placed: Vec<PlacedRuby>, old_ascent: &[f64], old_descent: &[f64]) {
let line_count = cx.out.lines.len();
let mut growth = vec![0.0_f64; line_count];
for ruby in &placed {
if let (Some(asc), Some(desc), Some(slot)) = (old_ascent.get(ruby.line), old_descent.get(ruby.line), growth.get_mut(ruby.line)) {
let grown = (asc.max(ruby.ascent) + desc.max(ruby.descent)) - (asc + desc);
*slot = slot.max(grown.max(0.0));
}
}
// A line's ruby grows above it: the line moves on in the block direction by its own growth,
// so the line before it keeps its room (for area type the first line moves too, keeping the
// ruby inside the frame; point type's first baseline is its anchor and stays).
let area = matches!(t.kind, TextKind::Area { .. });
let mut shift = vec![0.0_f64; n_lines];
let mut cum = 0.0;
for i in 0..n_lines {
if area || i > 0 {
cum += growth.get(i).copied().unwrap_or(0.0);
if let Some(s) = shift.get_mut(i) {
*s = cum;
let mut shift = vec![0.0_f64; line_count];
let mut cumulative = 0.0;
for (index, amount) in growth.iter().copied().enumerate() {
if area || index > 0 {
cumulative += amount;
shift[index] = cumulative;
}
}
for ruby in &placed {
if let Some(line) = cx.out.lines.get_mut(ruby.line) {
line.ascent = line.ascent.max(ruby.ascent);
line.descent = line.descent.max(ruby.descent);
}
}
for (index, line) in cx.out.lines.iter_mut().enumerate() {
line.baseline += shift.get(index).copied().unwrap_or(0.0);
}
shift_base_glyphs(cx, &shift);
append_ruby_glyphs(cx, placed, &shift);
}
fn shift_base_glyphs(cx: &mut Ctx<'_>, shift: &[f64]) {
for glyph in &mut cx.out.glyphs {
let amount = shift.get(glyph.line).copied().unwrap_or(0.0);
if amount != 0.0 {
glyph.origin.y += amount;
glyph.outline.apply_affine(Affine::translate((0.0, amount)));
glyph.xf = Affine::translate((0.0, amount)) * glyph.xf;
}
}
}
fn append_ruby_glyphs(cx: &mut Ctx<'_>, placed: Vec<PlacedRuby>, shift: &[f64]) {
for ruby in placed {
let amount = shift.get(ruby.line).copied().unwrap_or(0.0);
for mut glyph in ruby.glyphs {
if amount != 0.0 {
glyph.origin.y += amount;
glyph.outline.apply_affine(Affine::translate((0.0, amount)));
}
}
}
// Grow the lines' boxes to fit their ruby, then move them on in the block direction.
for (line, _, ascent, descent) in &placed {
if let Some(l) = cx.out.lines.get_mut(*line) {
l.ascent = l.ascent.max(*ascent);
l.descent = l.descent.max(*descent);
}
}
for (i, line) in cx.out.lines.iter_mut().enumerate() {
line.baseline += shift.get(i).copied().unwrap_or(0.0);
}
for g in &mut cx.out.glyphs {
let s = shift.get(g.line).copied().unwrap_or(0.0);
if s != 0.0 {
g.origin.y += s;
g.outline.apply_affine(Affine::translate((0.0, s)));
g.xf = Affine::translate((0.0, s)) * g.xf;
}
}
for (line, glyphs, _, _) in placed {
let s = shift.get(line).copied().unwrap_or(0.0);
for mut g in glyphs {
if s != 0.0 {
g.origin.y += s;
g.outline.apply_affine(Affine::translate((0.0, s)));
}
g.line = line;
cx.out.ruby.push(g);
glyph.line = ruby.line;
cx.out.ruby.push(glyph);
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
use super::flow::flow;
use super::region::Region;
use super::*;
use super::{Affine, Ctx, Punct, Range, Rect, SGlyph, Shape, Tcy, TextLayout, TextObject, VerticalAlign, is_cjk, punct};
/// System Layout on or Fractional Widths off: each character that takes space advances by a whole
/// number of points, at least one, so small type does not collapse onto itself. A zero advance (a