From 8b4ef958ac09aa4cd389262a0d02f2a11f09b511 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 6 Nov 2026 23:48:49 -0800 Subject: [PATCH] Type: a ligature's font caret on a vertical path divides the proportional cell Proportional Metrics changes an upright glyph's cell on a vertical path, and its width runs along that changed cell. Its font ligature carets were still divided by the unchanged cell, so the caret, a click and the selection missed the drawn stop. They now divide the same length the width uses. --- crates/text/src/layout.rs | 21 +++++++++---- crates/text/src/test_fonts.rs | 38 ++++++++++++++++++++++++ crates/text/src/tests/ligature_carets.rs | 33 +++++++++++++++++++- 3 files changed, 85 insertions(+), 7 deletions(-) diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index 942bc4dbe..6e9334041 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -57,8 +57,9 @@ fn continues_cluster(prev: Option<&SGlyph>, g: Option<&SGlyph>) -> bool { /// The caret stops inside glyph `g` when it alone stands for several characters (grapheme clusters) /// of `text`, a ligature ([`PositionedGlyph::carets`]); `rtl` when its logical leading edge is its /// right edge; `upright` when it stands upright in vertical type, its carets then down its cell -/// ([`upright_extent`]). Empty for a glyph of one character. -fn ligature_carets(text: &str, g: &SGlyph, rtl: bool, upright: bool) -> Vec<(usize, f64)> { +/// ([`upright_extent`]) as a share of `upright`, the length in font units its own width runs down. +/// Empty for a glyph of one character. +fn ligature_carets(text: &str, g: &SGlyph, rtl: bool, upright: Option) -> Vec<(usize, f64)> { use unicode_segmentation::UnicodeSegmentation; let Some(s) = text.get(g.byte..g.byte + g.len).filter(|_| g.len > 1) else { return Vec::new() }; let starts: Vec = s.grapheme_indices(true).skip(1).map(|(i, _)| g.byte + i).collect(); @@ -66,12 +67,13 @@ fn ligature_carets(text: &str, g: &SGlyph, rtl: bool, upright: bool) -> Vec<(usi return Vec::new(); } let parts = starts.len() + 1; - let from_font = if upright { + let from_font = if let Some(length) = upright { let (cell, top) = upright_extent(g); g.face .vertical_ligature_carets(g.gid, starts.len(), top, cell) - .filter(|c| c.len() == starts.len() && cell > 0.0) - .map(|c| c.into_iter().map(|d| d / cell).collect::>()) + .filter(|c| c.len() == starts.len() && length > 0.0) + .map(|c| c.into_iter().map(|d| d / length).collect::>()) + .filter(|c| c.iter().all(|f| *f > 0.0 && *f < 1.0)) } else { let advance = g.face.advance(g.gid); g.face @@ -465,7 +467,14 @@ impl Ctx<'_> { bend: None, unturned, pivot, - carets: if g.inline.is_some() { Vec::new() } else { ligature_carets(self.text, g, g.level.is_rtl() && !self.vertical, upright) }, + // On a path its carets divide the cell its width runs along, with the Proportional + // Metrics change to it. + carets: if g.inline.is_some() { + Vec::new() + } else { + let length = upright.then(|| path_cell.map_or_else(|| upright_extent(g).0, |(cell, _)| cell)); + ligature_carets(self.text, g, g.level.is_rtl() && !self.vertical, length) + }, width, width_start, }); diff --git a/crates/text/src/test_fonts.rs b/crates/text/src/test_fonts.rs index 2f591b6e0..00c994467 100644 --- a/crates/text/src/test_fonts.rs +++ b/crates/text/src/test_fonts.rs @@ -855,6 +855,44 @@ pub fn vertical_ligature_caret_font(caret: i16) -> Option> { Some(builder.build()) } +/// [`vertical_ligature_caret_font`] with its one caret at y = `caret` font units, f + i joined by +/// `ccmp` (which shaping applies top to bottom too, unlike `liga`), and its `GPOS` replaced by a +/// `vpal` single adjustment that changes the ligature glyph's advance down the column by +/// `y_advance` font units. `None` when it can't be built. +pub fn proportional_vertical_ligature_caret_font(caret: i16, y_advance: i16) -> Option> { + use write_fonts::tables::gpos::{Gpos, PositionLookup, PositionLookupList, SinglePos, ValueRecord}; + use write_fonts::tables::gsub::{Gsub, Ligature, LigatureSet, LigatureSubstFormat1, SubstitutionLookup, SubstitutionLookupList}; + use write_fonts::tables::layout::{CoverageFormat1, Feature, FeatureList, FeatureRecord, LangSys, Lookup, Script, ScriptList, ScriptRecord}; + use write_fonts::types::GlyphId16; + + let base = vertical_ligature_caret_font(caret)?; + let font = skrifa::FontRef::new(&base).ok()?; + let gid = |c| Some(GlyphId16::new(source_sans_gid(c)?)); + let ligature = gid(LIGATURE_CARET_GLYPH)?; + let langsys = || Script::new(Some(LangSys::new(vec![0])), vec![]); + let scripts = || ScriptList::new(vec![ScriptRecord::new(Tag::new(b"DFLT"), langsys()), ScriptRecord::new(Tag::new(b"latn"), langsys())]); + let subst = LigatureSubstFormat1::new( + CoverageFormat1::new(vec![gid('f')?]).into(), + vec![LigatureSet::new(vec![Ligature::new(ligature, vec![gid('i')?])])], + ); + let features = FeatureList::new(vec![FeatureRecord::new(Tag::new(b"ccmp"), Feature::new(None, vec![0]))]); + let lookups = SubstitutionLookupList::new(vec![SubstitutionLookup::Ligature(Lookup::new(Default::default(), vec![subst]))]); + let gsub = Gsub::new(scripts(), features, lookups); + let pos = SinglePos::format_1(CoverageFormat1::new(vec![ligature]).into(), ValueRecord::new().with_y_advance(y_advance)); + let features = FeatureList::new(vec![FeatureRecord::new(Tag::new(b"vpal"), Feature::new(None, vec![0]))]); + let lookups = PositionLookupList::new(vec![PositionLookup::Single(Lookup::new(Default::default(), vec![pos]))]); + let mut builder = FontBuilder::new(); + builder.add_table(&gsub).ok()?; + builder.add_table(&Gpos::new(scripts(), features, lookups)).ok()?; + for r in font.table_directory.table_records() { + let tag = Tag::new(&r.tag().to_be_bytes()); + if !builder.contains(tag) { + builder.add_raw(tag, font.table_data(r.tag())?.as_bytes()); + } + } + Some(builder.build()) +} + /// The family of [`variable_ligature_caret_font`]. pub const VARIABLE_LIGATURE_CARET_FAMILY: &str = "Varicaret Sans"; diff --git a/crates/text/src/tests/ligature_carets.rs b/crates/text/src/tests/ligature_carets.rs index af2ebae63..f669b07f4 100644 --- a/crates/text/src/tests/ligature_carets.rs +++ b/crates/text/src/tests/ligature_carets.rs @@ -8,7 +8,8 @@ use vectorcraft_geom::PathData; use crate::test_fonts::{ LIGATURE_CARET_FAMILY, LIGATURE_CARET_GLYPH, VARIABLE_LIGATURE_CARET_FAMILY, VERTICAL_LIGATURE_CARET_FAMILY, lam_alef_family, lam_alef_font, - ligature_caret_font, placed_ligature_caret_font, source_sans_gid, variable_ligature_caret_font, vertical_ligature_caret_font, + ligature_caret_font, placed_ligature_caret_font, proportional_vertical_ligature_caret_font, source_sans_gid, variable_ligature_caret_font, + vertical_ligature_caret_font, }; use crate::*; @@ -753,6 +754,36 @@ fn an_upright_ligature_on_a_path_reads_along_it_in_order() { assert!((up - pen).dot(tangent) > 1.0 && (ahead - pen).dot(right) > 1.0, "{pen:?} {up:?} {ahead:?}"); } +/// On a path, an upright ligature whose cell Proportional Metrics shortens (`vpal`, from 1000 units +/// to 800) keeps its font caret (600 units down the cell) where it is drawn: the inner stop lies at +/// 600 / 800 of its width, and the caret stands at the drawn stop. +#[test] +fn an_upright_ligature_on_a_path_keeps_its_font_caret_on_a_proportional_cell() { + use vectorcraft_doc::PathEffect; + let db = db_with(proportional_vertical_ligature_caret_font(280, -200)); + let mut t = point("fi", VERTICAL_LIGATURE_CARET_FAMILY); + t.vertical = true; + t.runs[0].style.standard_vertical_roman = true; + t.runs[0].style.proportional_metrics = true; + let mut line = BezPath::new(); + line.move_to((0.0, 0.0)); + line.line_to((1000.0, 0.0)); + t.kind = TextKind::OnPath { path: PathData::from_bezpath(&line), start: 0.0, end: None }; + t.path_effect = PathEffect::Rainbow; + let l = layout(&db, &t); + let Some(g) = l.glyphs.iter().find(|g| g.len == 2) else { panic!("f + i is one glyph: {:?}", l.glyphs) }; + assert!((g.width - 80.0).abs() < 1e-6, "the cell is 800 units at 100 pt: {}", g.width); + let &[(1, f)] = g.carets.as_slice() else { panic!("one inner stop: {:?}", g.carets) }; + assert!((f - 0.75).abs() < 1e-9, "{f}"); + let tangent = Vec2::new(g.angle.cos(), g.angle.sin()); + let along = |p: Point| (p - g.origin).dot(tangent); + let (top, drawn) = (along(g.xf * Point::new(0.0, -880.0)), along(g.xf * Point::new(0.0, -280.0))); + assert!((top - g.width_start).abs() < 1e-6 && (drawn - top - 60.0).abs() < 1e-6, "{top} {drawn} {}", g.width_start); + let (a, b) = caret_position(&l, 1); + let caret = along(a.midpoint(b)); + assert!((caret - drawn).abs() < 1e-6, "the caret stands at the drawn stop: {caret} against {drawn}"); +} + /// 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.