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.