diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index 38688b8ce..b814506e6 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -327,10 +327,14 @@ impl Ctx<'_> { } else if self.vertical && stands_upright(g) { let next = self.out.glyphs.len(); let after_base = self.out.glyphs.last().is_some_and(|p| p.byte == g.byte && p.line == line && p.len > 0); + // Baseline shift moves it across the column at any Character Rotation: the shift is + // taken off before the turn and put back after it. + let shift = pre * Point::new(0.0, -g.bshift) - pre * Point::ZERO; + let (shift, unshift) = (Affine::translate(shift), Affine::translate(-shift)); if let Some((_, _, turn, after)) = cluster.filter(|c| c.0 == g.byte && c.1 == next && after_base && g.len > 0) { // A later glyph of an upright glyph's cluster: set after it as the shaper placed // it, and turned with it. - m = turn * Affine::translate((after - origin.x, 0.0)) * m; + m = shift * turn * Affine::translate((after - origin.x, 0.0)) * unshift * m; self.upright_cluster = Some((g.byte, next + 1, turn, after)); } else { // Turned about the centre of its cell, which the column's centre line runs through: @@ -342,7 +346,7 @@ impl Ctx<'_> { // turning point instead would move it across the column too). let turn = Affine::translate((-g.lead, 0.0)) * Affine::rotate_about(-std::f64::consts::FRAC_PI_2, Point::new(origin.x + cell * 0.5, origin.y - upright_centre(g, cell, em))); - m = turn * m; + m = shift * turn * unshift * m; // Its own advance across, from where the cell centres it: where the shaper set the // next glyph of its cluster. let across = g.face.advance(g.gid) * g.sx; diff --git a/crates/text/src/tests/vertical.rs b/crates/text/src/tests/vertical.rs index 9c9d61681..c0a2b5e13 100644 --- a/crates/text/src/tests/vertical.rs +++ b/crates/text/src/tests/vertical.rs @@ -343,6 +343,41 @@ fn tracking_spaces_upright_marks_without_moving_them_off_the_centre_line() { assert!((step - 24.0).abs() < 1e-6, "one em plus 200/1000 em of tracking: {step}"); } +/// Baseline shift moves an upright glyph across the column, as it moves a letter lying on its +/// side, at any Character Rotation, and leaves its neighbors where they are. The glyph turns about +/// its shifted pen position (`xf * pivot`), and `xf * unturned` places the shifted glyph without +/// its rotation. +#[test] +fn baseline_shift_moves_an_upright_glyph_across_the_column() { + let at = |byte: usize, rotation: f64, shift: f64| { + let mut t = vertical("§l§"); + edit::style_range(&mut t.runs, byte, byte + "§l§"[byte..].chars().next().map_or(1, char::len_utf8), |s| { + s.rotation = rotation; + s.baseline_shift = shift; + }); + layout(FontDb::global(), &t) + }; + let centre = |l: &TextLayout, byte: usize| l.glyphs.iter().find(|g| g.byte == byte).map(|g| g.outline.bounding_box().center()).unwrap(); + // The letter lying on its side moves across the column by the shift. + let across = centre(&at(2, 0.0, 10.0), 2) - centre(&at(2, 0.0, 0.0), 2); + assert!((across.x.abs() - 10.0).abs() < 1e-6 && across.y.abs() < 1e-6, "{across:?}"); + for rotation in [0.0, 30.0, 90.0, -90.0] { + let (plain, shifted) = (at(3, rotation, 0.0), at(3, rotation, 10.0)); + let moved = centre(&shifted, 3) - centre(&plain, 3); + assert!((moved - across).hypot() < 1e-6, "{rotation}°: {moved:?}, across {across:?}"); + for byte in [0, 2] { + assert!((centre(&shifted, byte) - centre(&plain, byte)).hypot() < 1e-6, "{rotation}°: byte {byte} moved"); + } + let (p, s) = (plain.glyphs.iter().find(|g| g.byte == 3).unwrap(), shifted.glyphs.iter().find(|g| g.byte == 3).unwrap()); + let turn_at = s.xf * s.pivot - p.xf * p.pivot; + assert!((turn_at - across).hypot() < 1e-6, "{rotation}°: turns about a point moved by {turn_at:?}"); + let unrotated = at(3, 0.0, 10.0); + let u = unrotated.glyphs.iter().find(|g| g.byte == 3).unwrap(); + let (a, b) = ((s.xf * s.unturned).as_coeffs(), u.xf.as_coeffs()); + assert!(a.iter().zip(b).all(|(a, b)| (a - b).abs() < 1e-6), "{rotation}°: {a:?} vs {b:?}"); + } +} + /// Character panel menu › Tate-chu-yoko: a marked run stands across one em of the column, including /// a number longer than three digits and Latin letters. Unmarked text keeps the automatic rule /// (two or three digits only). Horizontal type stores the mark and does not move the letters.