Files
vectorcraft/crates/text/src/layout/vertical.rs
T
dotCooCoo 7b6b6af7b1 Type: the Mojikumi Set menu offers the predefined spacing sets
The Paragraph panel's Mojikumi Set lists None, YakumonoHankaku,
GyomatsuYakumonoHankaku, GyomatsuYakumonoZenkaku and YakumonoZenkaku,
then Line-end Punctuation Half Width, and text.setFormat {mojikumi}
takes yakumonoHankaku, gyomatsuYakumonoHankaku, gyomatsuYakumonoZenkaku
and yakumonoZenkaku besides none and lineEndHalf.

The spacing of each set is VectorCraft's own rule, built on the help
page's one-line description of each set:
- YakumonoHankaku sets every bracket, comma and full stop half width,
  takes a quarter em off each side of a middle dot, and puts a quarter
  em between Japanese and Latin letters or digits.
- GyomatsuYakumonoHankaku keeps punctuation full width inside a line,
  sets an opening bracket starting a line flush and a closing mark
  ending it half width, and takes the quarter em after a middle dot
  ending a line off.
- GyomatsuYakumonoZenkaku keeps punctuation full width and adds the
  quarter em between Japanese and Latin text.
- YakumonoZenkaku sets characters as None does.

Both composers and vertical type measure each line as it is set. A
right-to-left paragraph, whose line ends are measured untrimmed, also
draws them untrimmed, and a right-to-left line that ends between a
Japanese and a Latin character is measured without the quarter em the
set adds there. With a Desired glyph scaling other than 100%, the
space a set takes off before an opening bracket or middle dot, and the
Japanese-Latin space recorded at a line's end, scale with the glyph.

The sets save with the document and paragraph styles; new type and
Reset Panel keep Line-end Punctuation Half Width. SVG export lays type
with middle dots out again for its positioned spans. A .ai save
reports a paragraph with one of the new sets as left out.

Docs: docs/mcp.md, docs/target-app-parity.md, docs/type-parity.md,
docs/gaps.md.
2026-11-12 21:07:44 -08:00

558 lines
26 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::region::Region;
use super::{Affine, Ctx, Mojikumi, Punct, Range, Rect, SGlyph, Tcy, TextLayout, TextObject, VerticalAlign, continues_cluster, 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
/// tate-chu-yoko continuation, a zero-width mark) stays zero. Right to left, a cluster with manual
/// kerning moves by the difference the kerning makes to its rounded advance, so the rounding
/// changes the space before the next character in reading order and not the space after the one
/// before it. Each side of the Insert Aki is rounded to whole points on its own and added to the
/// rounded advance, so justification keeps it at that width.
pub(super) fn whole_character_widths(glyphs: &mut [SGlyph]) {
let whole = |adv: f64| if adv.is_finite() && adv > 0.0 { adv.round().max(1.0) } else { adv };
let points = |aki: f64| if aki.is_finite() { aki.round() } else { 0.0 };
for cluster in glyphs.chunk_by_mut(|a, b| continues_cluster(Some(a), Some(b))) {
// The cluster's last glyph carries the manual kerning and the Insert Aki in its advance.
let (adv, shift) = cluster.last().map_or((0.0, 0.0), |g| (g.adv - g.aki, g.kern_shift - g.aki_shift));
let rounded_shift = Some(whole(adv) - whole(adv - shift)).filter(|s| s.is_finite()).unwrap_or(shift);
for g in cluster {
// The space before the character and the space after it are each rounded.
let aki_shift = points(g.aki_shift);
let aki = if g.aki == 0.0 { 0.0 } else { aki_shift + points(g.aki - g.aki_shift) };
g.adv = whole(g.adv - g.aki) + aki;
g.kern_shift = if shift != 0.0 { rounded_shift } else { g.kern_shift - g.aki_shift } + aki_shift;
g.aki = aki;
g.aki_shift = aki_shift;
}
}
}
/// Tate-chu-yoko: a horizontal block one em of the column long, squeezed to the em when wider.
///
/// Automatic: a run of two or three half-width digits (`10`月, `100`年). A single digit and a
/// longer number (`2026`) stay on their side.
///
/// Manual (Character panel menu › Tate-chu-yoko): any characters marked `tateChuYoko`, including
/// one character, letters and numbers longer than three digits. Marked characters are taken
/// before the automatic digit rule. A marked block moves by the Tate-chu-yoko Settings Up/Down
/// and Left/Right of its first character; an automatic block does not move.
/// <https://helpx.adobe.com/illustrator/using/formatting-asian-characters.html>
///
/// With `whole` (System Layout on or Fractional Widths off), each side of a block's Insert Aki is
/// set in whole points ([`whole_character_widths`]).
pub(super) fn tate_chu_yoko(g: &mut [SGlyph], whole: bool, style: impl Fn(&SGlyph) -> TcyStyle) {
let manual: Vec<bool> = g.iter().map(|gl| gl.ch != '\u{2028}' && style(gl).marked).collect();
let digits: Vec<bool> = g.iter().map(|gl| gl.inline.is_none() && gl.ch.is_ascii_digit()).collect();
let mut i = 0;
while i < g.len() {
if manual.get(i).copied().unwrap_or(false) {
let mut end = i + 1;
while manual.get(end).copied().unwrap_or(false) {
end += 1;
}
let first = style(&g[i]);
place_tcy_block(&mut g[i..end], first.size, (first.up, first.right), whole);
i = end;
continue;
}
if !digits.get(i).copied().unwrap_or(false) {
i += 1;
continue;
}
let mut end = i + 1;
while digits.get(end).copied().unwrap_or(false) && !manual.get(end).copied().unwrap_or(false) {
end += 1;
}
if (2..=3).contains(&(end - i)) {
let em = style(&g[i]).size;
place_tcy_block(&mut g[i..end], em, (0.0, 0.0), whole);
}
i = end;
}
}
/// What [`tate_chu_yoko`] reads from a glyph's character style: its size, whether it is marked
/// Tate-chu-yoko, and its Tate-chu-yoko Settings Up/Down and Left/Right in points.
pub(super) struct TcyStyle {
pub size: f64,
pub marked: bool,
pub up: f64,
pub right: f64,
}
/// Lay `g` across one em of the column, moved by `(up, right)` points. The first glyph takes the
/// em; the rest share its cell. The Insert Aki of the block's first character is space above and
/// below the block, and the Insert Aki of its other characters is left out: the block is set
/// down the column that much further, and its last glyph carries the space in its advance. With
/// `whole`, the space above and the space below are each rounded to whole points.
fn place_tcy_block(g: &mut [SGlyph], em: f64, (up, right): (f64, f64), whole: bool) {
let Some(first) = g.first() else { return };
let (byte, before) = (first.byte, first.aki_shift);
let aki: f64 = g.iter().filter(|gl| gl.byte == byte).map(|gl| gl.aki).sum();
let points = |v: f64| {
if !whole {
v
} else if v.is_finite() {
v.round()
} else {
0.0
}
};
let (before, aki) = (points(before), points(before) + points(aki - before));
let width: f64 = g.iter().map(|gl| gl.adv - gl.aki).sum();
let squeeze = if width > em && width > 0.0 { em / width } else { 1.0 };
let last = g.len() - 1;
let mut ink = 0.0;
for (k, gl) in g.iter_mut().enumerate() {
let adv = gl.adv - gl.aki;
gl.tcy = Some(Tcy { pen: if k == 0 { 0.0 } else { em }, ink, width, squeeze, up: up - before, right });
gl.kern_shift -= gl.aki_shift;
gl.aki_shift = 0.0;
gl.aki = if k == last { aki } else { 0.0 };
gl.adv = if k == 0 { em } else { 0.0 } + gl.aki;
ink += adv;
}
}
/// Half an em of `g`'s size when it is full-width Japanese punctuation of kind `kind` (its
/// advance an em, give or take a tenth), the space that mojikumi can take off.
pub(super) fn punct_half(g: &SGlyph, kind: Punct) -> Option<f64> {
// A glyph Proportional Metrics re-spaced has no empty half left to take off (#966).
if punct(g.ch)? != kind || g.tcy.is_some() || g.proportional {
return None;
}
let em = g.face.units_per_em();
((g.face.advance(g.gid) - em).abs() <= em * 0.1).then_some(em * 0.5 * g.sx)
}
/// A quarter em of `g`'s size when it is a full-width Japanese middle dot (・, its advance an em,
/// give or take a tenth): the space on each side of the dot that a Mojikumi set can take off.
pub(super) fn dot_quarter(g: &SGlyph) -> Option<f64> {
if g.ch != '・' || g.tcy.is_some() || g.proportional {
return None;
}
let em = g.face.units_per_em();
((g.face.advance(g.gid) - em).abs() <= em * 0.1).then_some(em * 0.25 * g.sx)
}
/// What a paragraph's Mojikumi set does to its characters. The five predefined sets other than
/// Line-end Punctuation Half Width follow VectorCraft's own spacing rules, built on the help
/// page's one-line description of each set
/// (<https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/use-mojikumi-for-japanese-text-composition.html>).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(super) struct MojikumiRules {
/// Consecutive punctuation shares one half-em space ([`compress_punctuation`]).
pub share: bool,
/// Every opening and closing bracket, comma and full stop is set half width, and a middle dot
/// loses the quarter em on each side of it ([`halve_punctuation`]).
pub half: bool,
/// An opening bracket starting a line is set flush with the line's start.
pub flush_start: bool,
/// A closing bracket, comma or full stop ending a line is set half width.
pub half_end: bool,
/// A middle dot ending a line loses the quarter em after it.
pub dot_end: bool,
/// A quarter em between Japanese and Latin letters or digits ([`space_japanese_and_latin`]),
/// taken off again at a line's end.
pub wakan: bool,
}
impl MojikumiRules {
pub(super) fn of(set: Mojikumi) -> Self {
let none = Self::default();
match set {
Mojikumi::None | Mojikumi::YakumonoZenkaku => none,
Mojikumi::LineEndHalf => Self { share: true, flush_start: true, half_end: true, wakan: true, ..none },
Mojikumi::YakumonoHankaku => Self { half: true, wakan: true, ..none },
Mojikumi::GyomatsuYakumonoHankaku => Self { flush_start: true, half_end: true, dot_end: true, wakan: true, ..none },
Mojikumi::GyomatsuYakumonoZenkaku => Self { wakan: true, ..none },
}
}
/// The rules a line's end follows: a right-to-left line keeps its punctuation's width there
/// and only loses the space between Japanese and Latin text.
pub(super) fn at_line_end(self, rtl: bool) -> Self {
if rtl { Self { half_end: false, dot_end: false, ..self } } else { self }
}
/// Whether a line ending at some character can be set narrower than its characters' advances.
pub(super) fn trims_line_end(&self) -> bool {
self.half_end || self.dot_end || self.wakan
}
}
/// YakumonoHankaku: every full-width opening bracket loses the half-em space before it, every
/// closing bracket, comma and full stop the half-em space after it, and every middle dot a quarter
/// em on each side. Returns which glyphs lost the space after them (a line ending in one takes
/// nothing more off).
pub(super) fn halve_punctuation(sg: &mut [SGlyph]) -> Vec<bool> {
let mut lost_after = vec![false; sg.len()];
for (g, lost) in sg.iter_mut().zip(lost_after.iter_mut()) {
if let Some(h) = punct_half(g, Punct::Closing) {
g.adv -= h;
*lost = true;
} else if let Some(h) = punct_half(g, Punct::Opening) {
g.adv -= h;
g.lead += h;
} else if let Some(q) = dot_quarter(g) {
g.adv -= 2.0 * q;
g.lead += q;
*lost = true;
}
}
lost_after
}
/// Mojikumi (JLREQ 3.1.4): consecutive punctuation shares one half-em space. A closing bracket,
/// comma or full stop followed by punctuation loses the space after it; an opening bracket after
/// another loses the space before it. Returns which glyphs lost the space after them (a line
/// ending in one takes nothing more off).
pub(super) fn compress_punctuation(sg: &mut [SGlyph]) -> Vec<bool> {
let mut lost_after = vec![false; sg.len()];
for j in 1..sg.len() {
let (before, after) = sg.split_at_mut(j);
let (Some(a), Some(b)) = (before.last_mut(), after.first_mut()) else { continue };
if punct(b.ch).is_none() {
continue;
}
if let Some(h) = punct_half(a, Punct::Closing) {
a.adv -= h;
if let Some(l) = lost_after.get_mut(j - 1) {
*l = true;
}
} else if punct(a.ch) == Some(Punct::Opening)
&& let Some(h) = punct_half(b, Punct::Opening)
{
b.adv -= h;
b.lead += h;
}
}
lost_after
}
/// The space between Japanese and Latin letters or digits (JLREQ 3.2.2: a quarter em of the
/// Japanese characters' size), with the Mojikumi sets that add it ([`MojikumiRules::wakan`]).
const WAKAN_AKI: f64 = 0.25;
/// A Japanese character for the space next to Latin text: kana and kanji (and full-width
/// letters, and the iteration and abbreviation marks 々 〆 〇), not punctuation or symbols, nor
/// Hangul (Korean sets a word space instead).
fn is_japanese_letter(c: char) -> bool {
if matches!(c, '々' | '〆' | '〇') {
return true;
}
is_cjk(c)
&& !matches!(
c as u32,
0x3000..=0x303F | 0x3130..=0x318F | 0xAC00..=0xD7AF | 0xFF01..=0xFF0F | 0xFF1A..=0xFF20 | 0xFF3B..=0xFF40 | 0xFF5B..=0xFF65 | 0xFFA0..=0xFFDC
)
&& c != '・'
&& punct(c).is_none()
}
impl TextLayout {
/// Whether a Mojikumi set can set type laid out as this layout differently from Mojikumi
/// None: `text`, the type's plain text, or one of the layout's list markers holds Japanese
/// punctuation, a middle dot or a Japanese letter, the only characters the sets space. When it
/// holds none of them, the type lays out the same with any set.
pub fn mojikumi_applies(&self, text: &str) -> bool {
let spaced = |c: char| punct(c).is_some() || c == '・' || is_japanese_letter(c);
text.chars().any(spaced) || self.markers.iter().any(|m| m.text.chars().any(spaced))
}
}
/// A Latin letter or digit (or another script's letter), set proportionally.
fn is_latin_letter(c: char) -> bool {
c.is_alphanumeric() && !is_cjk(c)
}
/// Mojikumi (JLREQ 3.2.2): a quarter em between a Japanese character and a Latin letter or digit,
/// either way round, added after the first of the two. Returns what each glyph got after it (taken
/// off again when the line ends there).
pub(super) fn space_japanese_and_latin(sg: &mut [SGlyph]) -> Vec<f64> {
let mut added = vec![0.0; sg.len()];
for j in 1..sg.len() {
let (before, after) = sg.split_at_mut(j);
let (Some(a), Some(b)) = (before.last_mut(), after.first()) else { continue };
if a.tcy.is_some() || b.tcy.is_some() {
continue;
}
let japanese = if is_japanese_letter(a.ch) && is_latin_letter(b.ch) {
Some(&*a)
} else if is_latin_letter(a.ch) && is_japanese_letter(b.ch) {
Some(b)
} else {
None
};
if let Some(g) = japanese {
let aki = WAKAN_AKI * g.face.units_per_em() * g.sx;
a.adv += aki;
if let Some(x) = added.get_mut(j - 1) {
*x = aki;
}
}
}
added
}
/// The size of `g`'s em, in points.
pub(super) fn glyph_em(g: &SGlyph) -> f64 {
g.face.units_per_em() * g.sy
}
/// How far up (line space) Character Alignment `a` moves glyph `g` on a line whose largest em is
/// `line_em`: the glyph's em box top, centre or bottom onto the line's (the em box running from
/// its centre less half an em to its centre plus half an em above the baseline), or its ICF's top
/// or bottom (the em box's less the face's [`crate::IcfMargins`]; across a vertical line, its
/// right and left). Nothing on the Roman baseline, or for the line's largest characters.
///
/// An inline graphic has its run's em (whatever its scale): it counts as one of the line's
/// largest characters only when its run's size is, and moves with the text of its run (its art
/// stays centred on that text's cap height).
pub(super) fn align_shift(g: &SGlyph, a: vectorcraft_doc::CharAlign, line_em: f64, vertical: bool) -> f64 {
use vectorcraft_doc::CharAlign;
let icf = || g.face.icf_margins();
let k = match a {
CharAlign::RomanBaseline => return 0.0,
CharAlign::EmBoxTop => 0.5,
CharAlign::EmBoxCenter => 0.0,
CharAlign::EmBoxBottom => -0.5,
CharAlign::IcfTop => 0.5 - if vertical { icf().right } else { icf().top },
CharAlign::IcfBottom => -0.5 + if vertical { icf().left } else { icf().bottom },
};
(g.face.ideographic_centre() + k) * (line_em - glyph_em(g)).max(0.0)
}
/// The length an upright glyph takes down the column before tracking and justification: its
/// vertical advance (the font's vertical metrics), else its advance, at least one em.
pub(super) fn upright_cell(g: &SGlyph) -> f64 {
match g.face.vertical_glyph(g.gid) {
Some((advance, _)) => advance * g.sy,
None => (g.face.advance(g.gid) * g.sx).max(g.face.units_per_em() * g.sx),
}
}
/// An upright glyph's cell in font units, as [`upright_cell`] and [`upright_centre`] place it before
/// its size: (its length down the column, the height of its top above the baseline). From the
/// font's vertical metrics, else at least one em long, centred on the ideographic em box.
pub(super) fn upright_extent(g: &SGlyph) -> (f64, f64) {
match g.face.vertical_glyph(g.gid) {
Some((advance, origin)) => (advance, origin),
None => {
let em = g.face.units_per_em();
let cell = g.face.advance(g.gid).max(em);
(cell, EM_CENTER * em + cell * 0.5)
}
}
}
/// Height above the baseline (line space) of the centre of an upright glyph's `cell`, the point it
/// turns about: from the font's vertical metrics, the cell hanging from the glyph's vertical origin;
/// else the centre of the ideographic em box ([`EM_CENTER`] of the size `em`).
pub(super) fn upright_centre(g: &SGlyph, cell: f64, em: f64) -> f64 {
match g.face.vertical_glyph(g.gid) {
Some((_, origin)) => origin * g.sy - cell * 0.5,
None => EM_CENTER * em,
}
}
/// Height of the centre of the ideographic em box above the baseline, in ems (the em box runs
/// from 0.12 em below the baseline to 0.88 em above it), for fonts without vertical metrics.
pub(super) const EM_CENTER: f64 = 0.38;
/// A quarter turn clockwise (y down), exact: line space → text space for vertical type.
pub(super) const QUARTER_TURN: Affine = Affine::new([0.0, 1.0, -1.0, 0.0, 0.0, 0.0]);
/// Does the glyph stand upright in vertical type? CJK characters and symbols do (their vertical
/// forms come from the font's `vert` feature); Latin letters, digits and other characters lie on
/// their side. Brackets, the long vowel mark and similar marks that need a vertical form lie on
/// their side when the font has none (Unicode's Vertical_Orientation Tr).
/// Fullwidth !/? instead keep the nominal glyph upright (Tu fallback, UAX #50 §3.1):
/// <https://www.unicode.org/reports/tr50/#vo>.
///
/// Standard Vertical Roman Alignment (Character panel menu): a marked Latin letter, digit or other
/// character outside the classes above stands upright instead of lying on its side, except spaces
/// and control characters (they keep their width down the column) and the glyphs of a
/// tate-chu-yoko block, which stays across the column.
/// <https://helpx.adobe.com/illustrator/using/formatting-asian-characters.html>
pub(super) fn stands_upright(g: &SGlyph) -> bool {
let c = g.ch as u32;
let upright = matches!(c,
0x00A7 | 0x00A9 | 0x00AE | 0x00B1 | 0x00BC..=0x00BE | 0x00D7 | 0x00F7
| 0x2016 | 0x2020 | 0x2021 | 0x2030 | 0x2031 | 0x203B | 0x203C | 0x2042 | 0x2047..=0x2049 | 0x2051
| 0x20DD..=0x20E0 | 0x20E2..=0x20E4 | 0x2100..=0x2101 | 0x2103..=0x2109 | 0x210F | 0x2113 | 0x2116 | 0x2117
| 0x211E..=0x2123 | 0x2125 | 0x2127 | 0x2129 | 0x212E | 0x2135..=0x213F | 0x2145..=0x214A | 0x214C | 0x214D | 0x214F..=0x2189
| 0x2460..=0x24FF | 0x25A0..=0x27BF | 0x2B12..=0x2B2F | 0x2B50..=0x2B59
| 0x1100..=0x11FF | 0x2E80..=0x303F | 0x3040..=0xA4CF | 0xA960..=0xA97F | 0xAC00..=0xD7FF
| 0xE000..=0xFAFF | 0xFE10..=0xFE1F | 0xFE30..=0xFE4F | 0xFE50..=0xFE6F | 0xFF00..=0xFFE7
| 0x1F000..=0x1FAFF | 0x20000..=0x3FFFF);
if !upright {
return g.vertical_roman && g.tcy.is_none() && g.inline.is_none() && !g.ch.is_whitespace() && !g.ch.is_control();
}
let needs_vertical_form = matches!(c,
0x3001 | 0x3002 | 0x3008..=0x3011 | 0x3013..=0x301F | 0x3030 | 0x30A0 | 0x30FC
| 0xFE50..=0xFE52 | 0xFE59..=0xFE5E | 0xFF08 | 0xFF09 | 0xFF0C | 0xFF0E | 0xFF1A | 0xFF1B
| 0xFF3B | 0xFF3D | 0xFF3F | 0xFF5B..=0xFF60 | 0xFFE3);
// Without a vertical alternate the shaped glyph is the nominal one.
!needs_vertical_form || g.gid != g.face.glyph_for(g.ch)
}
pub(super) fn finish_bounds(out: &mut TextLayout) {
let xf = out.line_xf;
let mut b: Option<Rect> = out.ink_bounds();
let mut add = |r: Rect| b = Some(b.map_or(r, |b| b.union(r)));
if !out.on_path {
for l in &out.lines {
add(xf.transform_rect_bbox(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();
}
/// Space left in each cell after flowing top-aligned: (space above the first line's ascent, space
/// below the last line's descent, number of distinct baselines). Cells without lines are `None`.
fn cell_space(out: &TextLayout, regions: &[Region]) -> Vec<Option<(f64, f64, usize)>> {
regions
.iter()
.enumerate()
.map(|(ri, r)| {
let mut top = f64::INFINITY;
let mut bottom = f64::NEG_INFINITY;
let mut baselines: Vec<f64> = vec![];
for l in out.lines.iter().filter(|l| l.region == ri) {
top = top.min(l.baseline - l.ascent);
bottom = bottom.max(l.baseline + l.descent);
if !baselines.iter().any(|b| (b - l.baseline).abs() < 1e-6) {
baselines.push(l.baseline);
}
}
if baselines.is_empty() || !top.is_finite() || !bottom.is_finite() {
return None;
}
// Never negative: a full (or overflowing) cell stays where top alignment put it.
Some(((top - r.top()).max(0.0), (r.bottom() - bottom).max(0.0), baselines.len()))
})
.collect()
}
/// Area Type Options "Align" other than Top: move each cell's lines down. Rectangular cells
/// without text wrap shift their lines (centre: half the space left below the last line;
/// bottom: all of it; justify: the first line stays, each further line gets an equal share of it).
/// Other frames give lines different widths at different heights, so the text flows again with
/// each cell's lines started lower (or spaced wider) until it settles, at most eight passes; a
/// pass that would push text out of the frame is undone and retried with half the step.
pub(super) fn align_vertically(cx: &mut Ctx<'_>, paras: &[Range<usize>], t: &TextObject, regions: &mut [Region]) {
let align = cx.opts.vertical_align;
let plain = regions.iter().all(|r| (r.rect || r.polys.is_empty()) && r.wraps.is_empty());
if plain {
let space = cell_space(&cx.out, regions);
// Each distinct baseline's index within its cell, for justify.
let mut seen: Vec<Vec<f64>> = vec![vec![]; regions.len()];
let offs: Vec<f64> = cx
.out
.lines
.iter()
.map(|l| {
let Some(Some((_, below, n))) = space.get(l.region).copied() else { return 0.0 };
match align {
VerticalAlign::Top => 0.0,
VerticalAlign::Center => below * 0.5,
VerticalAlign::Bottom => below,
VerticalAlign::Justify => {
let Some(s) = seen.get_mut(l.region) else { return 0.0 };
let k = s.iter().position(|b| (b - l.baseline).abs() < 1e-6).unwrap_or_else(|| {
s.push(l.baseline);
s.len() - 1
});
if n > 1 { below * k as f64 / (n - 1) as f64 } else { 0.0 }
}
}
})
.collect();
for (l, d) in cx.out.lines.iter_mut().zip(&offs) {
l.baseline += d;
}
for g in &mut cx.out.glyphs {
let d = offs.get(g.line).copied().unwrap_or(0.0);
if d != 0.0 {
let m = Affine::translate((0.0, d));
g.outline.apply_affine(m);
g.xf = m * g.xf;
g.origin.y += d;
}
}
// Inline art moves with its glyph's line.
for i in &mut cx.out.inlines {
let d = cx.out.glyphs.get(i.glyph).and_then(|g| offs.get(g.line)).copied().unwrap_or(0.0);
if d != 0.0 {
let m = Affine::translate((0.0, d));
i.xf = m * i.xf;
i.bounds = m.transform_rect_bbox(i.bounds);
}
}
return;
}
let laid_out = |out: &TextLayout| out.lines.last().map_or(0, |l| l.end);
// How much of the measured space a pass takes up: halved after a pass that lost text.
let mut step = 1.0;
for _ in 0..8 {
let space = cell_space(&cx.out, regions);
let mut moved = false;
let saved: Vec<(f64, f64)> = regions.iter().map(|r| (r.shift, r.gap)).collect();
for (r, s) in regions.iter_mut().zip(&space) {
let Some((above, below, n)) = *s else { continue };
let (shift, gap) = match align {
VerticalAlign::Top => (r.shift, r.gap),
VerticalAlign::Center => (r.shift + (below - above) * 0.5 * step, r.gap),
VerticalAlign::Bottom => (r.shift + below * step, r.gap),
VerticalAlign::Justify if n > 1 => (r.shift, r.gap + below * step / (n - 1) as f64),
VerticalAlign::Justify => (r.shift, r.gap),
};
let (shift, gap) = (shift.clamp(0.0, r.cell.height().max(0.0)), gap.clamp(0.0, r.cell.height().max(0.0)));
if (shift - r.shift).abs() > 0.25 || (gap - r.gap).abs() > 0.01 {
moved = true;
}
r.shift = shift;
r.gap = gap;
}
if !moved {
return;
}
let before = laid_out(&cx.out);
let prev = (
std::mem::take(&mut cx.out.glyphs),
std::mem::take(&mut cx.out.lines),
std::mem::take(&mut cx.out.inlines),
std::mem::take(&mut cx.out.markers),
std::mem::replace(&mut cx.out.overflow, false),
);
flow(cx, paras, t, Some(regions));
if laid_out(&cx.out) < before {
// Text no longer fits (lines got narrower, or flow around a wrap object): undo the
// pass and try a smaller step.
(cx.out.glyphs, cx.out.lines, cx.out.inlines, cx.out.markers, cx.out.overflow) = prev;
for (r, (shift, gap)) in regions.iter_mut().zip(saved) {
r.shift = shift;
r.gap = gap;
}
step *= 0.5;
if step < 0.1 {
return;
}
}
}
}
#[cfg(test)]
#[test]
fn japanese_letters_for_the_latin_space_are_kana_kanji_and_marks_not_hangul_or_punctuation() {
for c in ['あ', 'カ', '漢', '々', '〆', '〇', 'A'] {
assert!(is_japanese_letter(c), "{c}");
}
// Hangul in every form: syllables, compatibility jamo (ㄱ ㅏ), half-width jamo (ᄀ).
for c in ['한', 'ㄱ', 'ㅏ', '\u{FFA1}', '。', '「', '・', '、', 'a'] {
assert!(!is_japanese_letter(c), "{c}");
}
}