diff --git a/crates/text/src/layout/vertical.rs b/crates/text/src/layout/vertical.rs index 8f01a5d32..372c70b2f 100644 --- a/crates/text/src/layout/vertical.rs +++ b/crates/text/src/layout/vertical.rs @@ -1,14 +1,24 @@ use super::flow::flow; use super::region::Region; -use super::{Affine, Ctx, Punct, Range, Rect, SGlyph, Tcy, TextLayout, TextObject, VerticalAlign, is_cjk, punct}; +use super::{Affine, Ctx, 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. +/// 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. pub(super) fn whole_character_widths(glyphs: &mut [SGlyph]) { - for g in glyphs { - if g.adv.is_finite() && g.adv > 0.0 { - g.adv = g.adv.round().max(1.0); + let whole = |adv: f64| if adv.is_finite() && adv > 0.0 { adv.round().max(1.0) } else { adv }; + for cluster in glyphs.chunk_by_mut(|a, b| continues_cluster(Some(a), Some(b))) { + // The cluster's last glyph carries the manual kerning in its advance. + let (adv, shift) = cluster.last().map_or((0.0, 0.0), |g| (g.adv, g.kern_shift)); + let rounded_shift = Some(whole(adv) - whole(adv - shift)).filter(|s| s.is_finite()).unwrap_or(shift); + for g in cluster { + g.adv = whole(g.adv); + if shift != 0.0 { + g.kern_shift = rounded_shift; + } } } } diff --git a/crates/text/src/shape.rs b/crates/text/src/shape.rs index 3311a8aa4..454fe0dba 100644 --- a/crates/text/src/shape.rs +++ b/crates/text/src/shape.rs @@ -34,7 +34,8 @@ pub(crate) struct SGlyph { pub dx: f64, pub dy: f64, /// Right to left: the run's manual kerning, which moves the glyph right along the baseline, - /// outside its Character Rotation. 0 otherwise. + /// outside its Character Rotation; with whole-point advances, the difference the kerning makes + /// to the cluster's rounded advance. 0 otherwise. pub kern_shift: f64, /// Outline scale (font units -> points). pub sx: f64, diff --git a/crates/text/src/tests/typo.rs b/crates/text/src/tests/typo.rs index 689e956b3..ebc54d61c 100644 --- a/crates/text/src/tests/typo.rs +++ b/crates/text/src/tests/typo.rs @@ -98,6 +98,36 @@ fn rtl_manual_kerning_spaces_the_character_from_the_next_in_reading_order() { } } +#[test] +fn rtl_manual_kerning_with_whole_point_widths_keeps_the_previous_character_in_place() { + // Reading order: alef (bytes 0..2), bet (2..4), gimel (4..6); bet carries the kerning, which + // is a fraction of a point. + for (system_layout, fractional_widths) in [(true, true), (false, false)] { + for value in [3.0, 4.0, 6.0, 7.0] { + let kerned = |value: f64| { + let mut t = TextObject::point(Point::ZERO, "אבג", CharStyle { size: 100.0, ..CharStyle::default() }); + t.system_layout = system_layout; + t.fractional_widths = fractional_widths; + edit::style_range(&mut t.runs, 2, 4, |s| s.kerning = Some(value)); + layout(db(), &t) + }; + let ink = |lay: &TextLayout, byte: usize| { + let g = lay.glyphs.iter().find(|g| g.byte == byte).unwrap(); + assert!(g.rtl && g.gid != 0, "byte {byte} is set right to left with a glyph of its own"); + (g.xf * Point::ZERO).x + }; + let (plain, spaced) = (kerned(0.0), kerned(value)); + let gap = |lay: &TextLayout, right: usize, left: usize| ink(lay, right) - ink(lay, left); + assert!( + (gap(&spaced, 0, 2) - gap(&plain, 0, 2)).abs() < 1e-9, + "alef, before bet in reading order, keeps its place (kerning {value}, System Layout {system_layout})" + ); + let wider = gap(&spaced, 2, 4) - gap(&plain, 2, 4); + assert!((wider - wider.round()).abs() < 1e-9, "bet moves from gimel by whole points: {wider} (kerning {value})"); + } + } +} + #[test] fn rtl_manual_kerning_on_a_rotated_character_moves_it_along_the_baseline() { // Reading order: alef (bytes 0..2), bet (2..4), gimel (4..6); bet is turned 90 degrees and