From cdd014c4e7b211adec4ada82b82fc6cb683eef3e Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 3 Nov 2026 16:05:44 -0800 Subject: [PATCH] Type: the stop inside an upright ligature on a vertical path follows its drawn cell On a path, vertical type turns an upright glyph about its pen, so its cell runs from 0.12 em before the pen to 0.88 em after it without the font's vertical metrics. The ligature caret, a click and the selection measured the cell from the pen instead. They now take the cell where the glyph's transform draws it, with its placement, baseline shift, Horizontal Scale and Skew. --- crates/text/src/layout.rs | 15 ++++++ crates/text/src/lib.rs | 4 +- crates/text/src/tests/ligature_carets.rs | 60 ++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 1 deletion(-) diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index 9d7ed666e..38688b8ce 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -382,6 +382,21 @@ impl Ctx<'_> { } else { 0.0 }; + // On a path, an upright glyph turns about its pen: its cell runs along the path between + // where the glyph's transform sets the cell's foot and its top, moved by the shaper's + // placement and a baseline shift, and slanted by the path's effect. + let (width, width_start) = match (upright && self.on_path, g.sy) { + (true, sy) if sy != 0.0 => { + // In font units, with the Proportional Metrics change to its length. + let (cell, top) = upright_extent(g); + let proportional = g.vpal.filter(|_| g.face.vertical_glyph(g.gid).is_some()).map_or(0.0, |a| a.advance) / sy; + let tangent = Vec2::new(angle.cos(), angle.sin()); + let along = |y: f64| (m * Point::new(0.0, y) - origin).dot(tangent); + let (start, end) = (along(cell + proportional - top), along(-top)); + if start.is_finite() && end > start { (end - start, start) } else { (width, width_start) } + } + _ => (width, width_start), + }; if let Some(art) = g.inline.as_ref().and_then(|ib| ib.art) { self.out.inlines.push(InlineGlyph { run: g.run, byte: g.byte, glyph: self.out.glyphs.len(), xf: m, bounds: m.transform_rect_bbox(art) }); } diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index 1d3203607..a49e187ae 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -166,7 +166,9 @@ pub struct PositionedGlyph { /// in text space: the font's placement of the glyph (`GPOS` x placement), less a leading space /// taken off, plus the manual kerning of right-to-left text, which moves the glyph right so that /// the space it adds lies on the glyph's left. 0 for a glyph standing upright or set across the - /// column in vertical type, and for an inline graphic. + /// column in vertical type, and for an inline graphic. An upright glyph in vertical type on a + /// path turns about its pen, and its cell starts one cell length before the cell's top: 0.12 em + /// before the pen without the font's vertical metrics. pub width_start: f64, } diff --git a/crates/text/src/tests/ligature_carets.rs b/crates/text/src/tests/ligature_carets.rs index ee42864c5..a0a194d9b 100644 --- a/crates/text/src/tests/ligature_carets.rs +++ b/crates/text/src/tests/ligature_carets.rs @@ -459,6 +459,66 @@ fn an_upright_ligature_takes_its_caret_down_its_cell() { assert!((at(900) - 0.5).abs() < 1e-9, "{}", at(900)); } +/// On a path, an upright ligature in vertical type turns about its pen, so its cell runs from +/// 0.12 em before the pen to 0.88 em after it, with or without the font's vertical metrics. The +/// inner stop lies at the middle of that cell, and the first character's selection ends there. +#[test] +fn an_upright_ligature_on_a_path_takes_its_stop_in_its_turned_cell() { + for (font, family) in [(ligature_caret_font(None), LIGATURE_CARET_FAMILY), (vertical_ligature_caret_font(900), VERTICAL_LIGATURE_CARET_FAMILY)] { + let db = db_with(font); + let mut line = BezPath::new(); + line.move_to((0.0, 0.0)); + line.line_to((1000.0, 0.0)); + let mut t = point("fi", family); + t.vertical = true; + t.runs[0].style.standard_vertical_roman = true; + t.kind = TextKind::OnPath { path: PathData::from_bezpath(&line), start: 0.0, end: None }; + let l = layout(&db, &t); + assert!(l.on_path); + let Some(g) = l.glyphs.iter().find(|g| g.len == 2) else { panic!("f + i is one glyph: {:?}", l.glyphs) }; + assert_eq!(g.carets, vec![(1, 0.5)], "{family}"); + assert!((g.width - 100.0).abs() < 1e-9 && (g.width_start + 12.0).abs() < 1e-9, "{family}: {} {}", g.width, g.width_start); + let x = |b| caret_position(&l, b).0.x - g.origin.x; + assert!((x(1) - 38.0).abs() < 1e-6, "{family}: {}", x(1)); + let q = selection_quads(&l, 0, 1); + assert_eq!(q.len(), 1, "{q:?}"); + assert!(((q[0][1] - q[0][0]).hypot() - 38.0).abs() < 1e-6, "{family}: {q:?}"); + // A baseline shift moves the turned glyph along the path, and its stop with it. + let ink = g.outline.bounding_box().x0; + t.runs[0].style.baseline_shift = 10.0; + let l = layout(&db, &t); + let Some(s) = l.glyphs.iter().find(|g| g.len == 2) else { panic!("f + i is one glyph: {:?}", l.glyphs) }; + let moved = s.outline.bounding_box().x0 - ink; + assert!(moved.abs() > 1.0, "{family}: the shift moves the glyph along the path: {moved}"); + let stop = caret_position(&l, 1).0.x - s.origin.x; + assert!((stop - 38.0 - moved).abs() < 1e-6, "{family}: {stop} against {}", 38.0 + moved); + // Horizontal Scale widens the glyph across the path; its stop stays inside the glyph. + t.runs[0].style.baseline_shift = 0.0; + t.runs[0].style.h_scale = 200.0; + let l = layout(&db, &t); + let Some(s) = l.glyphs.iter().find(|g| g.len == 2) else { panic!("f + i is one glyph: {:?}", l.glyphs) }; + let bounds = s.outline.bounding_box(); + let stop = caret_position(&l, 1).0.x - s.origin.x; + assert!(bounds.x0 < stop && stop < bounds.x1 && stop > 0.0, "{family}: {stop} outside {bounds:?}"); + t.runs[0].style.h_scale = 100.0; + // Skew on a slanted path: the stop stays inside the drawn glyph along the path. + let mut slant = BezPath::new(); + slant.move_to((0.0, 0.0)); + slant.line_to((1000.0, 1000.0)); + t.runs[0].style.baseline_shift = 0.0; + t.kind = TextKind::OnPath { path: PathData::from_bezpath(&slant), start: 0.0, end: None }; + t.path_effect = vectorcraft_doc::PathEffect::Skew; + let l = layout(&db, &t); + let Some(s) = l.glyphs.iter().find(|g| g.len == 2) else { panic!("f + i is one glyph: {:?}", l.glyphs) }; + let tangent = Vec2::new(s.angle.cos(), s.angle.sin()); + let ink = s.outline.elements().iter().filter_map(|el| el.end_point()).map(|p| (p - s.origin).dot(tangent)); + let (lo, hi) = ink.fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), d| (lo.min(d), hi.max(d))); + let (a, b) = caret_position(&l, 1); + let stop = (a.midpoint(b) - s.origin).dot(tangent); + assert!(lo < stop && stop < hi, "{family}: {stop} outside {lo}..{hi}"); + } +} + /// Tracking that makes an upright ligature's advance negative leaves it upright: it keeps its /// caret down its cell, and its cell starts where it does without tracking, with or without the /// font's vertical metrics.