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.
This commit is contained in:
dotCooCoo
2026-11-08 23:51:42 +07:00
committed by bflatastic
parent f4e51d33d3
commit 8b4ef958ac
3 changed files with 85 additions and 7 deletions
+15 -6
View File
@@ -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<f64>) -> 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<usize> = 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::<Vec<f64>>())
.filter(|c| c.len() == starts.len() && length > 0.0)
.map(|c| c.into_iter().map(|d| d / length).collect::<Vec<f64>>())
.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,
});
+38
View File
@@ -855,6 +855,44 @@ pub fn vertical_ligature_caret_font(caret: i16) -> Option<Vec<u8>> {
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<Vec<u8>> {
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";
+32 -1
View File
@@ -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.