From e0ebc13a2e14a36eb45e24bf1833407fbc48e36b Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 4 Nov 2026 06:59:03 -0800 Subject: [PATCH] Type: right-to-left manual kerning keeps whole-point spacing on both sides With System Layout on or Fractional Widths off, each advance is rounded to whole points, but a right-to-left character with manual kerning still moved by the unrounded kerning value. The difference between the two changed the space after the character before it in reading order. The character now moves by the change the kerning makes to its rounded advance, so only the space before the next character changes. --- crates/text/src/layout/vertical.rs | 20 +++++++++++++++----- crates/text/src/shape.rs | 3 ++- crates/text/src/tests/typo.rs | 30 ++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 6 deletions(-) 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