mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 21:10:40 +00:00
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:
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user