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.
This commit is contained in:
dotCooCoo
2026-11-04 22:29:14 +07:00
committed by bflatastic
parent 75ba71618a
commit e0ebc13a2e
3 changed files with 47 additions and 6 deletions
+15 -5
View File
@@ -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;
}
}
}
}
+2 -1
View File
@@ -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,
+30
View File
@@ -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