mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 01:50:22 -06:00
Merge pull request #5225 from dotCooCoo/fix/ligature-caret-positions
Type: the caret, a click and the arrow keys reach the characters inside a ligature
This commit is contained in:
@@ -1257,6 +1257,28 @@ fn type_tool_selection_replace_and_delete() {
|
||||
assert_eq!(obj(&s, id).plain_text(), " ");
|
||||
}
|
||||
|
||||
/// A Type tool click inside a ligature (Source Serif 4's fi) puts the caret between its
|
||||
/// characters, and Shift+Right from there selects the i alone.
|
||||
#[test]
|
||||
fn type_tool_click_reaches_the_inside_of_a_ligature() {
|
||||
let mut s = session();
|
||||
let v = ViewInfo::default();
|
||||
let r = s.execute("text.create", &json!({"x": 100, "y": 100, "text": "fin", "size": 40, "font": "Source Serif 4"})).unwrap();
|
||||
let id = NodeId(r["id"].as_u64().unwrap());
|
||||
s.select_tool("type", v).unwrap();
|
||||
let t = obj(&s, id);
|
||||
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t);
|
||||
let g = lay.glyphs.iter().find(|g| g.len == 2).expect("f + i is one glyph");
|
||||
let (_, f) = g.carets[0];
|
||||
let p = t.xf * (g.origin + vectorcraft_geom::Vec2::new(g.advance * f + 0.2, -10.0));
|
||||
click(&mut s, p.x, p.y);
|
||||
assert_eq!(s.tool_options()["caret"], json!(1), "between the f and the i");
|
||||
key(&mut s, ToolKey::Right, Mods { shift: true, ..Default::default() });
|
||||
assert_eq!((s.tool_options()["start"].as_u64(), s.tool_options()["end"].as_u64()), (Some(1), Some(2)));
|
||||
s.tool_text("I", v).unwrap();
|
||||
assert_eq!(obj(&s, id).plain_text(), "fIn");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_panel_flow_styles_the_selected_range_while_editing() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -239,6 +239,72 @@ impl FontFace {
|
||||
.map(|a| a as f64)
|
||||
.unwrap_or(self.upem * 0.5)
|
||||
}
|
||||
/// The coordinates of the carets inside ligature glyph `gid`, in font units, in the order the
|
||||
/// font lists them: the font's ligature caret list (`GDEF` `LigCaretList`,
|
||||
/// <https://learn.microsoft.com/en-us/typography/opentype/spec/gdef>), one between each two
|
||||
/// characters the glyph stands for, each an x in horizontal text and a y (up from the baseline)
|
||||
/// in vertical text. A coordinate with a variation index (format 3 in a variable font) takes its
|
||||
/// delta from the `GDEF` item variation store at the face's location; a device table's
|
||||
/// adjustments for pixel sizes are not applied. `None` when the font lists no carets for the
|
||||
/// glyph, lists a number other than `count`, gives one by a contour point, or gives one a
|
||||
/// variation index whose delta can't be read.
|
||||
fn ligature_caret_coordinates(&self, gid: u32, count: usize) -> Option<Vec<f64>> {
|
||||
use skrifa::raw::TableProvider;
|
||||
use skrifa::raw::tables::gdef::CaretValue;
|
||||
use skrifa::raw::tables::layout::DeviceOrVariationIndex;
|
||||
use skrifa::raw::tables::variations::DeltaSetIndex;
|
||||
let font = self.skrifa()?;
|
||||
let gdef = font.gdef().ok()?;
|
||||
let list = gdef.lig_caret_list()?.ok()?;
|
||||
let index = list.coverage().ok()?.get(GlyphId::new(gid))?;
|
||||
let glyph = list.lig_glyphs().get(usize::from(index)).ok()?;
|
||||
if usize::from(glyph.caret_count()) != count {
|
||||
return None;
|
||||
}
|
||||
let carets = glyph
|
||||
.caret_values()
|
||||
.iter()
|
||||
.map(|c| match c.ok()? {
|
||||
CaretValue::Format1(v) => Some(f64::from(v.coordinate())),
|
||||
CaretValue::Format3(v) => {
|
||||
let coordinate = f64::from(v.coordinate());
|
||||
if v.device_offset().is_null() {
|
||||
return Some(coordinate);
|
||||
}
|
||||
match v.device().ok()? {
|
||||
DeviceOrVariationIndex::Device(_) => Some(coordinate),
|
||||
DeviceOrVariationIndex::VariationIndex(ix) => {
|
||||
let store = gdef.item_var_store()?.ok()?;
|
||||
let index = DeltaSetIndex { outer: ix.delta_set_outer_index(), inner: ix.delta_set_inner_index() };
|
||||
Some(coordinate + store.compute_delta(index, self.location.coords()).ok()?.to_f64())
|
||||
}
|
||||
}
|
||||
}
|
||||
CaretValue::Format2(_) => None,
|
||||
})
|
||||
.collect::<Option<Vec<f64>>>()?;
|
||||
(!carets.is_empty()).then_some(carets)
|
||||
}
|
||||
/// The caret positions inside ligature glyph `gid` set across, in font units from its left
|
||||
/// edge, left to right: the x coordinates of the font's ligature caret list. `None` when the
|
||||
/// font lists no carets for the glyph, lists a number other than `count`, gives one by a contour
|
||||
/// point, or gives positions that are not increasing inside the glyph's advance.
|
||||
pub fn ligature_carets(&self, gid: u32, count: usize) -> Option<Vec<f64>> {
|
||||
let carets = self.ligature_caret_coordinates(gid, count)?;
|
||||
let advance = self.advance(gid);
|
||||
let inside = carets.iter().all(|x| *x > 0.0 && *x < advance) && carets.windows(2).all(|w| w[0] < w[1]);
|
||||
inside.then_some(carets)
|
||||
}
|
||||
/// The caret positions inside ligature glyph `gid` standing upright in vertical type, in font
|
||||
/// units down from the top of its cell, top to bottom: the y coordinates of the font's ligature
|
||||
/// caret list, for a cell `cell` long whose top is `top` above the baseline. `None` when the
|
||||
/// font lists no carets for the glyph, lists a number other than `count`, gives one by a contour
|
||||
/// point, or gives positions that are not increasing inside the cell.
|
||||
pub fn vertical_ligature_carets(&self, gid: u32, count: usize, top: f64, cell: f64) -> Option<Vec<f64>> {
|
||||
let carets = self.ligature_caret_coordinates(gid, count)?;
|
||||
let inside = carets.iter().all(|y| *y < top && *y > top - cell) && carets.windows(2).all(|w| w[0] < w[1]);
|
||||
inside.then(|| carets.iter().rev().map(|y| top - y).collect())
|
||||
}
|
||||
/// Glyph `gid` set upright in vertical type, from the font's vertical metrics: (its advance down
|
||||
/// the column, the height above the baseline of its vertical origin, the top of its cell), in
|
||||
/// font units. The origin is the `VORG` table's, else the glyph's top plus its top side bearing.
|
||||
|
||||
@@ -37,7 +37,7 @@ use breaking::tab_advance;
|
||||
#[cfg(test)]
|
||||
use breaking::{break_line, candidates};
|
||||
pub use driver::{decorations, fit_first_line, layout, layout_with, scaled};
|
||||
use vertical::{TcyStyle, stands_upright, tate_chu_yoko, upright_cell, upright_centre, whole_character_widths};
|
||||
use vertical::{TcyStyle, stands_upright, tate_chu_yoko, upright_cell, upright_centre, upright_extent, whole_character_widths};
|
||||
|
||||
const EPS: f64 = 1e-6;
|
||||
|
||||
@@ -47,6 +47,60 @@ fn continues_cluster(prev: Option<&SGlyph>, g: Option<&SGlyph>) -> bool {
|
||||
prev.zip(g).is_some_and(|(a, b)| a.len > 0 && b.len > 0 && a.byte == b.byte)
|
||||
}
|
||||
|
||||
/// 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)> {
|
||||
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();
|
||||
if starts.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let parts = starts.len() + 1;
|
||||
let from_font = if 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>>())
|
||||
} else {
|
||||
let advance = g.face.advance(g.gid);
|
||||
g.face
|
||||
.ligature_carets(g.gid, starts.len())
|
||||
.filter(|c| c.len() == starts.len() && advance > 0.0)
|
||||
.map(|c| c.into_iter().map(|x| if rtl { 1.0 - x / advance } else { x / advance }).collect::<Vec<f64>>())
|
||||
};
|
||||
let mut at = from_font.unwrap_or_else(|| (1..parts).map(|j| j as f64 / parts as f64).collect());
|
||||
at.sort_by(f64::total_cmp);
|
||||
starts.into_iter().zip(at).collect()
|
||||
}
|
||||
|
||||
/// Does glyph `g`, standing upright in vertical type, take a cell of its own down the column? Every
|
||||
/// glyph with an advance does, including one whose tracking or kerning makes its advance zero or
|
||||
/// negative; a glyph without a font advance (a mark) does not.
|
||||
fn takes_cell(g: &SGlyph) -> bool {
|
||||
g.adv > 0.0 || g.face.advance(g.gid) > 0.0
|
||||
}
|
||||
|
||||
/// Clear the caret stops of each glyph that shares its cluster with another glyph: only a glyph
|
||||
/// that alone stands for its characters is a ligature.
|
||||
fn keep_lone_ligature_carets(glyphs: &mut [PositionedGlyph]) {
|
||||
if glyphs.iter().all(|g| g.carets.is_empty()) {
|
||||
return;
|
||||
}
|
||||
let mut count: std::collections::HashMap<(usize, usize), usize> = std::collections::HashMap::new();
|
||||
for g in glyphs.iter().filter(|g| g.len > 0) {
|
||||
*count.entry((g.byte, g.len)).or_default() += 1;
|
||||
}
|
||||
for g in glyphs.iter_mut().filter(|g| !g.carets.is_empty()) {
|
||||
if count.get(&(g.byte, g.len)).is_some_and(|n| *n > 1) {
|
||||
g.carets.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Ctx<'a> {
|
||||
on_path: bool,
|
||||
db: &'a FontDb,
|
||||
@@ -278,7 +332,7 @@ impl Ctx<'_> {
|
||||
// the middle of its own cell, not of its advance (tracking and justification add
|
||||
// space after the cell, and must not push the glyph off the centre line).
|
||||
let em = self.style_at(g.byte).size;
|
||||
let cell = if g.adv > 0.0 { upright_cell(g) } else { advance };
|
||||
let cell = if takes_cell(g) { upright_cell(g) } else { advance };
|
||||
// A leading space taken off moves it up the column after the turn (moving the
|
||||
// turning point instead would move it across the column too).
|
||||
let turn = Affine::translate((-g.lead, 0.0))
|
||||
@@ -300,6 +354,29 @@ impl Ctx<'_> {
|
||||
p
|
||||
};
|
||||
let font_id = g.face.id();
|
||||
// Its own width along the baseline: its font advance through its size, scale and the glyph
|
||||
// scaling `pre` adds. In vertical type, an upright glyph's own length down the column is
|
||||
// its cell with the Proportional Metrics change to it, and a tate-chu-yoko glyph takes its
|
||||
// advance, as an inline graphic does.
|
||||
let upright = self.vertical && g.inline.is_none() && g.tcy.is_none() && takes_cell(g) && stands_upright(g);
|
||||
let width = if g.inline.is_some() || (self.vertical && g.tcy.is_some()) {
|
||||
advance
|
||||
} else if upright {
|
||||
let proportional = g.vpal.filter(|_| g.face.vertical_glyph(g.gid).is_some()).map_or(0.0, |a| a.advance);
|
||||
Some(upright_cell(g) + proportional).filter(|w| w.is_finite() && *w > 0.0).unwrap_or(advance)
|
||||
} else {
|
||||
let [a, b, ..] = pre.as_coeffs();
|
||||
Some(g.face.advance(g.gid) * g.sx * a.hypot(b)).filter(|w| w.is_finite()).unwrap_or(advance)
|
||||
};
|
||||
// Where its own width starts along its advance: the shaper's placement, a leading space
|
||||
// taken off and right-to-left manual kerning move the glyph along the line. An upright or
|
||||
// tate-chu-yoko glyph in vertical type starts its cell at the start of its advance.
|
||||
let width_start = if g.inline.is_none() && !upright && !(self.vertical && g.tcy.is_some()) {
|
||||
let [a, b, ..] = pre.as_coeffs();
|
||||
Some((g.kern_shift + g.dx - g.lead) * a.hypot(b)).filter(|s| s.is_finite()).unwrap_or(0.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
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) });
|
||||
}
|
||||
@@ -323,6 +400,9 @@ 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) },
|
||||
width,
|
||||
width_start,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ use super::ruby::ruby_layout;
|
||||
use super::vertical::{EM_CENTER, QUARTER_TURN, align_vertically, finish_bounds};
|
||||
use super::{
|
||||
Affine, BezPath, CharStyle, Ctx, FontDb, LayoutOptions, OtFeatures, Point, Rect, Shape, TextKind, TextLayout, TextObject, VerticalAlign,
|
||||
keep_lone_ligature_carets,
|
||||
};
|
||||
|
||||
/// Lay out a text object into text-space glyph outlines (default [`LayoutOptions`]).
|
||||
@@ -305,6 +306,7 @@ fn layout_once(db: &FontDb, t: &TextObject, opts: &LayoutOptions) -> TextLayout
|
||||
cx.out.vertical = true;
|
||||
cx.out.line_xf = line_xf;
|
||||
}
|
||||
keep_lone_ligature_carets(&mut cx.out.glyphs);
|
||||
finish_bounds(&mut cx.out);
|
||||
cx.out
|
||||
}
|
||||
|
||||
@@ -369,5 +369,8 @@ fn ruby_glyph(g: &PositionedGlyph, m: Affine, run: usize, byte: usize, line: usi
|
||||
bend: None,
|
||||
unturned: Affine::IDENTITY,
|
||||
pivot: m * g.origin,
|
||||
carets: Vec::new(),
|
||||
width: 0.0,
|
||||
width_start: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,6 +206,20 @@ pub(super) fn upright_cell(g: &SGlyph) -> f64 {
|
||||
}
|
||||
}
|
||||
|
||||
/// An upright glyph's cell in font units, as [`upright_cell`] and [`upright_centre`] place it before
|
||||
/// its size: (its length down the column, the height of its top above the baseline). From the
|
||||
/// font's vertical metrics, else at least one em long, centred on the ideographic em box.
|
||||
pub(super) fn upright_extent(g: &SGlyph) -> (f64, f64) {
|
||||
match g.face.vertical_glyph(g.gid) {
|
||||
Some((advance, origin)) => (advance, origin),
|
||||
None => {
|
||||
let em = g.face.units_per_em();
|
||||
let cell = g.face.advance(g.gid).max(em);
|
||||
(cell, EM_CENTER * em + cell * 0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Height above the baseline (line space) of the centre of an upright glyph's `cell`, the point it
|
||||
/// turns about: from the font's vertical metrics, the cell hanging from the glyph's vertical origin;
|
||||
/// else the centre of the ideographic em box ([`EM_CENTER`] of the size `em`).
|
||||
|
||||
+116
-16
@@ -149,6 +149,25 @@ pub struct PositionedGlyph {
|
||||
/// Font units: `xf * pivot` is the point its Character Rotation turns it about, its pen
|
||||
/// position on the baseline moved by its baseline shift.
|
||||
pub pivot: Point,
|
||||
/// A ligature (one glyph that alone stands for several characters, grapheme clusters, of the
|
||||
/// text): the caret stop between each two of them, in logical order, as the byte offset of the
|
||||
/// later character and how far the caret stands along the glyph's own width ([`Self::width`])
|
||||
/// from its logical leading edge, 0 to 1. The font's ligature carets
|
||||
/// ([`FontFace::ligature_carets`], or [`FontFace::vertical_ligature_carets`] for a glyph
|
||||
/// standing upright in vertical type) when it lists one for each stop, else the width divided
|
||||
/// equally among the characters. Empty for any other glyph.
|
||||
pub carets: Vec<(usize, f64)>,
|
||||
/// The glyph's own width along its advance in text space: its font advance at its size,
|
||||
/// Horizontal Scale and glyph scaling, without the tracking, kerning, justification space and
|
||||
/// kashida that [`Self::advance`] includes after it (to its right in horizontal type). The
|
||||
/// advance itself in vertical type.
|
||||
pub width: f64,
|
||||
/// How far right of the left end of its advance the glyph's own width ([`Self::width`]) starts,
|
||||
/// 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.
|
||||
pub width_start: f64,
|
||||
}
|
||||
|
||||
/// How a glyph on a path is bent along it: a point of its pen space (x along its advance from its
|
||||
@@ -167,6 +186,57 @@ pub struct GlyphBend {
|
||||
}
|
||||
|
||||
impl PositionedGlyph {
|
||||
/// The glyph's caret stops in logical order: its first byte, its ligature carets
|
||||
/// ([`Self::carets`]) and its end, each with how far along its own width ([`Self::width`]) from
|
||||
/// its logical leading edge the caret stands, 0 to 1.
|
||||
fn width_stops(&self) -> impl Iterator<Item = (usize, f64)> + '_ {
|
||||
std::iter::once((self.byte, 0.0)).chain(self.carets.iter().copied()).chain(std::iter::once((self.byte + self.len, 1.0)))
|
||||
}
|
||||
/// The glyph's caret stops in logical order, each with its distance in text space from the
|
||||
/// logical leading edge of a span `extent` long that holds the glyph (its advance, or its
|
||||
/// cluster's): 0 at its first byte and `extent` at its end. A ligature caret stands at its place
|
||||
/// in the glyph's own width ([`Self::width`]), which starts [`Self::width_start`] right of the
|
||||
/// glyph's left edge; the rest of the space the advance adds lies to the right of that width,
|
||||
/// after the last ligature caret left to right, and before the first one right to left.
|
||||
/// Negative kerning or tracking makes the advance shorter than the width, or zero or negative,
|
||||
/// and a ligature caret then stands past the end of the advance.
|
||||
fn stops_along(&self, extent: f64) -> impl Iterator<Item = (usize, f64)> + '_ {
|
||||
let (width, start) = if self.width > 0.0 && self.width.is_finite() {
|
||||
(self.width, if self.width_start.is_finite() { self.width_start } else { 0.0 })
|
||||
} else {
|
||||
(extent, 0.0)
|
||||
};
|
||||
let ends = (self.byte, self.byte + self.len);
|
||||
self.width_stops().map(move |(b, f)| {
|
||||
if b == ends.0 {
|
||||
(b, 0.0)
|
||||
} else if b == ends.1 {
|
||||
(b, extent)
|
||||
} else if self.rtl {
|
||||
(b, extent - start - (1.0 - f) * width)
|
||||
} else {
|
||||
(b, start + f * width)
|
||||
}
|
||||
})
|
||||
}
|
||||
/// The distance in text space from the logical leading edge of a span `extent` long that holds
|
||||
/// the glyph (its advance, or its cluster's) to the caret for byte `byte`: 0 at its first byte,
|
||||
/// `extent` at its end, a ligature caret's place at that caret's byte ([`Self::stops_along`],
|
||||
/// which can lie outside 0 to `extent`), and in proportion to the bytes between two stops.
|
||||
pub fn caret_offset(&self, byte: usize, extent: f64) -> f64 {
|
||||
if byte <= self.byte || self.len == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
let mut from = (self.byte, 0.0);
|
||||
for (b, d) in self.stops_along(extent).skip(1) {
|
||||
if byte <= b {
|
||||
let span = b.saturating_sub(from.0).max(1) as f64;
|
||||
return from.1 + (d - from.1) * (byte - from.0) as f64 / span;
|
||||
}
|
||||
from = (b, d);
|
||||
}
|
||||
extent
|
||||
}
|
||||
/// The glyph's colour bitmap, and the transform from its pixels to text space.
|
||||
pub fn placed_image(&self) -> Option<(&Arc<GlyphImage>, Affine)> {
|
||||
self.image.as_ref().map(|i| (i, self.xf * i.xf))
|
||||
@@ -436,7 +506,8 @@ fn cluster_extent(layout: &TextLayout, glyphs: &[PositionedGlyph], i: usize) ->
|
||||
}
|
||||
|
||||
/// Caret for byte offset `byte`, as a (top, bottom) segment in text space. In horizontal type, a
|
||||
/// cluster drawn with several glyphs is measured whole ([`cluster_extent`]).
|
||||
/// cluster drawn with several glyphs is measured whole ([`cluster_extent`]). Inside a ligature the
|
||||
/// caret stands at its caret stops ([`PositionedGlyph::carets`]).
|
||||
pub fn caret_position(layout: &TextLayout, byte: usize) -> (Point, Point) {
|
||||
let Some(line) = layout.lines.get(layout.line_of(byte)) else {
|
||||
return (Point::ZERO, Point::ZERO);
|
||||
@@ -444,12 +515,12 @@ pub fn caret_position(layout: &TextLayout, byte: usize) -> (Point, Point) {
|
||||
let glyphs = &layout.glyphs[line.glyph_start..line.glyph_end];
|
||||
let (pos, angle) = if let Some(i) = glyphs.iter().position(|g| g.byte <= byte && g.byte + g.len > byte) {
|
||||
let g = &glyphs[i];
|
||||
let frac = if byte <= g.byte { 0.0 } else { (byte - g.byte) as f64 / g.len.max(1) as f64 };
|
||||
let (origin, advance) = match cluster_extent(layout, glyphs, i) {
|
||||
Some((left, right)) => (Point::new(left, g.origin.y), right - left),
|
||||
None => (g.origin, g.advance),
|
||||
};
|
||||
(origin + dir(g.angle) * (advance * if g.rtl { 1.0 - frac } else { frac }), g.angle)
|
||||
let off = g.caret_offset(byte, advance);
|
||||
(origin + dir(g.angle) * if g.rtl { advance - off } else { off }, g.angle)
|
||||
} else if let Some(g) = glyphs.iter().max_by_key(|g| g.byte + g.len) {
|
||||
(g.origin + dir(g.angle) * if g.rtl { 0.0 } else { g.advance }, g.angle)
|
||||
} else {
|
||||
@@ -464,11 +535,16 @@ pub fn caret_position(layout: &TextLayout, byte: usize) -> (Point, Point) {
|
||||
/// Byte offset nearest to the text-space point `p` (for clicking/dragging with the Type tool).
|
||||
pub fn hit_byte(layout: &TextLayout, p: Point) -> usize {
|
||||
if layout.on_path {
|
||||
let best = layout.glyphs.iter().enumerate().min_by(|(_, a), (_, b)| {
|
||||
let ca = a.origin + dir(a.angle) * (a.advance * 0.5);
|
||||
let cb = b.origin + dir(b.angle) * (b.advance * 0.5);
|
||||
(p - ca).hypot2().total_cmp(&(p - cb).hypot2())
|
||||
});
|
||||
// The distance from `p` to a glyph's middle, or to one of its inner ligature caret stops
|
||||
// when that is nearer (a stop can lie past the glyph's advance).
|
||||
let near = |g: &PositionedGlyph| {
|
||||
let along = dir(g.angle);
|
||||
let mid = (p - (g.origin + along * (g.advance * 0.5))).hypot2();
|
||||
(g.stops_along(g.advance).filter(|(b, _)| *b != g.byte && *b != g.byte + g.len))
|
||||
.map(|(_, d)| (p - (g.origin + along * if g.rtl { g.advance - d } else { d })).hypot2())
|
||||
.fold(mid, f64::min)
|
||||
};
|
||||
let best = layout.glyphs.iter().enumerate().min_by(|(_, a), (_, b)| near(a).total_cmp(&near(b)));
|
||||
let Some((i, g)) = best else { return layout.lines.first().map_or(0, |l| l.start) };
|
||||
// `first` and `last` are the first and last glyphs of the cluster. A click before the middle of
|
||||
// the cluster, measured along the first glyph's direction, goes to the cluster's start.
|
||||
@@ -476,6 +552,14 @@ pub fn hit_byte(layout: &TextLayout, p: Point) -> usize {
|
||||
let first = layout.glyphs[..=i].iter().rev().take_while(same).last().unwrap_or(g);
|
||||
let last = layout.glyphs[i..].iter().take_while(same).last().unwrap_or(g);
|
||||
let along = dir(first.angle);
|
||||
if !g.carets.is_empty() {
|
||||
// A ligature: the nearest of its caret stops, measured along it from its logical
|
||||
// leading edge in text space, within the span its stops cover.
|
||||
let (lo, hi) = g.stops_along(g.advance).fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), (_, d)| (lo.min(d), hi.max(d)));
|
||||
let t = (p - g.origin).dot(along);
|
||||
let at = (if g.rtl { g.advance - t } else { t }).max(lo).min(hi);
|
||||
return g.stops_along(g.advance).min_by(|a, b| (a.1 - at).abs().total_cmp(&(b.1 - at).abs())).map_or(g.byte, |s| s.0);
|
||||
}
|
||||
let span = (last.origin + dir(last.angle) * last.advance - first.origin).dot(along);
|
||||
return if ((p - first.origin).dot(along) < span * 0.5) != g.rtl { g.byte } else { g.byte + g.len };
|
||||
}
|
||||
@@ -517,7 +601,8 @@ pub fn byte_in_line(layout: &TextLayout, li: usize, x: f64) -> usize {
|
||||
// a consonant and its reph) has one caret stop at each end of its glyphs taken together. The
|
||||
// tatweels of a kashida have no source length or advance and come after the glyphs of the
|
||||
// letter after the kashida in the text; they belong to that letter's cluster. A list marker's
|
||||
// glyphs (no source length, an advance) are a cluster of their own.
|
||||
// glyphs (no source length, an advance) are a cluster of their own. A ligature also has a stop
|
||||
// at each of its caret stops.
|
||||
let mut clusters: Vec<(f64, f64, &PositionedGlyph)> = Vec::with_capacity(glyphs.len());
|
||||
for g in glyphs {
|
||||
let left = layout.logical_point(g.origin).x;
|
||||
@@ -529,7 +614,7 @@ pub fn byte_in_line(layout: &TextLayout, li: usize, x: f64) -> usize {
|
||||
}
|
||||
let nearest = clusters
|
||||
.iter()
|
||||
.flat_map(|&(left, right, g)| if g.rtl { [(right, g.byte), (left, g.byte + g.len)] } else { [(left, g.byte), (right, g.byte + g.len)] })
|
||||
.flat_map(|&(left, right, g)| g.stops_along(right - left).map(move |(b, d)| (if g.rtl { right - d } else { left + d }, b)))
|
||||
.min_by(|a, b| (a.0 - x).abs().total_cmp(&(b.0 - x).abs()));
|
||||
nearest.map_or(line.start, |(_, byte)| byte)
|
||||
}
|
||||
@@ -562,9 +647,9 @@ fn x_in_line(layout: &TextLayout, li: usize, byte: usize) -> f64 {
|
||||
let glyphs = &layout.glyphs[line.glyph_start..line.glyph_end];
|
||||
if let Some(i) = glyphs.iter().position(|g| g.byte <= byte && g.byte + g.len > byte) {
|
||||
let g = &glyphs[i];
|
||||
let frac = if byte <= g.byte { 0.0 } else { (byte - g.byte) as f64 / g.len.max(1) as f64 };
|
||||
let (left, advance) = cluster_extent(layout, glyphs, i).map_or((layout.logical_point(g.origin).x, g.advance), |(l, r)| (l, r - l));
|
||||
return left + advance * if g.rtl { 1.0 - frac } else { frac };
|
||||
let off = g.caret_offset(byte, advance);
|
||||
return left + if g.rtl { advance - off } else { off };
|
||||
}
|
||||
glyphs.iter().max_by_key(|g| g.byte + g.len).map_or(line.x0, |g| layout.logical_point(g.origin).x + if g.rtl { 0.0 } else { g.advance })
|
||||
}
|
||||
@@ -592,13 +677,25 @@ pub fn selection_quads(layout: &TextLayout, a: usize, b: usize) -> Vec<[Point; 4
|
||||
if a == b {
|
||||
return out;
|
||||
}
|
||||
// The part of glyph `g` that the selection covers, as distances in text space from its left
|
||||
// (its start along a path), the smaller first: all of its advance, or the characters of a
|
||||
// ligature that are selected. A ligature caret past a shortened advance lies outside it.
|
||||
let covered = |g: &PositionedGlyph| {
|
||||
if g.len == 0 {
|
||||
return (g.advance.min(0.0), g.advance.max(0.0));
|
||||
}
|
||||
let (fa, fb) = (g.caret_offset(a.max(g.byte), g.advance), g.caret_offset(b.min(g.byte + g.len), g.advance));
|
||||
let (f0, f1) = if g.rtl { (g.advance - fb, g.advance - fa) } else { (fa, fb) };
|
||||
(f0.min(f1), f0.max(f1))
|
||||
};
|
||||
if layout.on_path {
|
||||
// A list marker's glyphs stand for no characters.
|
||||
for g in layout.glyphs.iter().filter(|g| g.byte >= a && g.byte < b && g.len > 0) {
|
||||
for g in layout.glyphs.iter().filter(|g| g.byte < b && g.byte + g.len > a && g.len > 0) {
|
||||
let li = &layout.lines[0];
|
||||
let d = dir(g.angle);
|
||||
let up = Vec2::new(d.y, -d.x);
|
||||
let (p0, p1) = (g.origin, g.origin + d * g.advance);
|
||||
let (f0, f1) = covered(g);
|
||||
let (p0, p1) = (g.origin + d * f0, g.origin + d * f1);
|
||||
out.push([p0 + up * li.ascent, p1 + up * li.ascent, p1 - up * li.descent, p0 - up * li.descent]);
|
||||
}
|
||||
return out;
|
||||
@@ -609,7 +706,8 @@ pub fn selection_quads(layout: &TextLayout, a: usize, b: usize) -> Vec<[Point; 4
|
||||
for (_, g) in glyphs {
|
||||
let Some(l) = layout.lines.get(g.line) else { continue };
|
||||
let p = layout.logical_point(g.origin);
|
||||
let (x0, x1) = (p.x, p.x + g.advance);
|
||||
let (f0, f1) = covered(g);
|
||||
let (x0, x1) = (p.x + f0, p.x + f1);
|
||||
out.push(
|
||||
[
|
||||
Point::new(x0, l.baseline - l.ascent),
|
||||
@@ -636,6 +734,8 @@ pub fn selection_quads(layout: &TextLayout, a: usize, b: usize) -> Vec<[Point; 4
|
||||
let last = layout.glyphs[l.glyph_start..l.glyph_end].last().map_or(l.x1, |g| layout.logical_point(g.origin).x + g.advance);
|
||||
xb = last.max(l.x1) + (l.ascent + l.descent) * 0.25;
|
||||
}
|
||||
// A ligature caret past the ligature's shortened advance stands right of the glyph's end.
|
||||
let (xa, xb) = (xa.min(xb), xa.max(xb));
|
||||
if xb - xa <= 1e-9 {
|
||||
continue;
|
||||
}
|
||||
@@ -678,7 +778,7 @@ fn caret_horizontal_filtered(layout: &TextLayout, byte: usize, right: bool, vali
|
||||
(layout.logical_point(caret_position(layout, b).0).x, if rtl { -(b as f64) } else { b as f64 })
|
||||
};
|
||||
let (x, order) = stop(byte);
|
||||
let mut stops: Vec<_> = glyphs.iter().flat_map(|g| [g.byte, g.byte + g.len]).collect();
|
||||
let mut stops: Vec<_> = glyphs.iter().flat_map(|g| g.width_stops().map(|(b, _)| b)).collect();
|
||||
stops.sort_unstable();
|
||||
stops.dedup();
|
||||
stops
|
||||
|
||||
@@ -489,6 +489,195 @@ pub fn lam_alef_font(feature: &[u8; 4]) -> Option<Vec<u8>> {
|
||||
Some(builder.build())
|
||||
}
|
||||
|
||||
/// The family of [`ligature_caret_font`].
|
||||
pub const LIGATURE_CARET_FAMILY: &str = "Carettest Sans";
|
||||
|
||||
/// The glyph [`ligature_caret_font`]'s `liga` gives for f + i: W, wider than either letter.
|
||||
pub const LIGATURE_CARET_GLYPH: char = 'W';
|
||||
|
||||
/// A test-only cmap over bundled Source Sans 3 for f, i and the space, whose `GSUB` `liga` joins f +
|
||||
/// i into [`LIGATURE_CARET_GLYPH`]. With `caret`, a `GDEF` ligature caret list puts that glyph's one
|
||||
/// caret at x = `caret` font units; without it the font has no `GDEF` table. `None` when it can't be
|
||||
/// built.
|
||||
pub fn ligature_caret_font(caret: Option<i16>) -> Option<Vec<u8>> {
|
||||
use write_fonts::tables::cmap::Cmap;
|
||||
use write_fonts::tables::gdef::{CaretValue, Gdef, LigCaretList, LigGlyph};
|
||||
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;
|
||||
|
||||
const BASE: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf");
|
||||
let font = skrifa::FontRef::new(BASE).ok()?;
|
||||
let charmap = font.charmap();
|
||||
let mapping = [('f', 'f'), ('i', 'i'), (' ', ' ')];
|
||||
let mappings = mapping.into_iter().map(|(c, source)| Some((c, write_fonts::types::GlyphId::new(charmap.map(source)?.to_u32()))));
|
||||
let cmap = Cmap::from_mappings(mappings.collect::<Option<Vec<_>>>()?).ok()?;
|
||||
let family = LIGATURE_CARET_FAMILY;
|
||||
let name = name_table(&[(1, family), (2, "Regular"), (4, "Carettest Sans Regular"), (6, "CarettestSans-Regular")])?;
|
||||
let mut builder = FontBuilder::new();
|
||||
builder.add_table(&cmap).ok()?;
|
||||
builder.add_raw(Tag::new(b"name"), name);
|
||||
let gid = |c| Some(GlyphId16::new(u16::try_from(charmap.map(c)?.to_u32()).ok()?));
|
||||
let ligature = gid(LIGATURE_CARET_GLYPH)?;
|
||||
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"liga"), Feature::new(None, vec![0]))]);
|
||||
let lookups = SubstitutionLookupList::new(vec![SubstitutionLookup::Ligature(Lookup::new(Default::default(), vec![subst]))]);
|
||||
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())]);
|
||||
builder.add_table(&Gsub::new(scripts, features, lookups)).ok()?;
|
||||
if let Some(x) = caret {
|
||||
let carets = LigCaretList::new(CoverageFormat1::new(vec![ligature]).into(), vec![LigGlyph::new(vec![CaretValue::format_1(x)])]);
|
||||
builder.add_table(&Gdef::new(None, None, Some(carets), None)).ok()?;
|
||||
}
|
||||
for r in font.table_directory.table_records() {
|
||||
let tag = Tag::new(&r.tag().to_be_bytes());
|
||||
if tag != Tag::new(b"GDEF") && !builder.contains(tag) {
|
||||
builder.add_raw(tag, font.table_data(r.tag())?.as_bytes());
|
||||
}
|
||||
}
|
||||
Some(builder.build())
|
||||
}
|
||||
|
||||
/// The family of [`vertical_ligature_caret_font`].
|
||||
pub const VERTICAL_LIGATURE_CARET_FAMILY: &str = "Vertical Carettest Sans";
|
||||
|
||||
/// [`ligature_caret_font`] with its one caret at y = `caret` font units, as family
|
||||
/// [`VERTICAL_LIGATURE_CARET_FAMILY`], with vertical metrics (`vhea`, `vmtx` and `VORG`): every glyph
|
||||
/// one em down the column with its vertical origin at 0.88 em. `None` when it can't be built.
|
||||
pub fn vertical_ligature_caret_font(caret: i16) -> Option<Vec<u8>> {
|
||||
let base = ligature_caret_font(Some(caret))?;
|
||||
let font = skrifa::FontRef::new(&base).ok()?;
|
||||
let n = font.maxp().ok()?.num_glyphs();
|
||||
let be16 = |v: &mut Vec<u8>, x: i16| v.extend_from_slice(&x.to_be_bytes());
|
||||
// vmtx: (advance height, top side bearing) for every glyph; VORG gives the origin.
|
||||
let mut vmtx = Vec::with_capacity(4 * usize::from(n));
|
||||
for _ in 0..n {
|
||||
vmtx.extend_from_slice(&1000_u16.to_be_bytes());
|
||||
be16(&mut vmtx, 0);
|
||||
}
|
||||
// vhea 1.1: ascent/descent of the em box across the column, then the metrics' count.
|
||||
let mut vhea = Vec::with_capacity(36);
|
||||
vhea.extend_from_slice(&0x0001_1000_u32.to_be_bytes());
|
||||
for x in [500, -500, 0] {
|
||||
be16(&mut vhea, x);
|
||||
}
|
||||
vhea.extend_from_slice(&1000_u16.to_be_bytes());
|
||||
for x in [0, 0, 1000, 1, 0, 0, 0, 0, 0, 0, 0] {
|
||||
be16(&mut vhea, x);
|
||||
}
|
||||
vhea.extend_from_slice(&n.to_be_bytes());
|
||||
// VORG 1.0: the default origin and no glyph that differs.
|
||||
let mut vorg = Vec::new();
|
||||
for x in [1, 0, 880, 0] {
|
||||
be16(&mut vorg, x);
|
||||
}
|
||||
let family = VERTICAL_LIGATURE_CARET_FAMILY;
|
||||
let name = name_table(&[(1, family), (2, "Regular"), (4, "Vertical Carettest Sans Regular"), (6, "VerticalCarettestSans-Regular")])?;
|
||||
let mut builder = FontBuilder::new();
|
||||
builder.add_raw(Tag::new(b"name"), name);
|
||||
builder.add_raw(Tag::new(b"vhea"), vhea);
|
||||
builder.add_raw(Tag::new(b"vmtx"), vmtx);
|
||||
builder.add_raw(Tag::new(b"VORG"), vorg);
|
||||
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";
|
||||
|
||||
/// [`ligature_caret_font`] as a variable font of family [`VARIABLE_LIGATURE_CARET_FAMILY`]: one
|
||||
/// weight axis (400–700, default 400) with the named instances Regular (400) and Bold (700), and a
|
||||
/// `GDEF` ligature caret list whose one caret is a format 3 value at x = `caret` font units with a
|
||||
/// variation index into the `GDEF` item variation store that moves it by `delta` at 700. `None`
|
||||
/// when it can't be built.
|
||||
pub fn variable_ligature_caret_font(caret: i16, delta: i16) -> Option<Vec<u8>> {
|
||||
use write_fonts::tables::gdef::{CaretValue, Gdef, LigCaretList, LigGlyph};
|
||||
use write_fonts::tables::layout::{CoverageFormat1, DeviceOrVariationIndex};
|
||||
use write_fonts::tables::variations::{ItemVariationData, ItemVariationStore, RegionAxisCoordinates, VariationRegion, VariationRegionList};
|
||||
use write_fonts::types::GlyphId16;
|
||||
|
||||
let base = ligature_caret_font(None)?;
|
||||
let font = skrifa::FontRef::new(&base).ok()?;
|
||||
let ligature = GlyphId16::new(source_sans_gid(LIGATURE_CARET_GLYPH)?);
|
||||
let value = CaretValue::format_3(caret, DeviceOrVariationIndex::variation_index(0, 0));
|
||||
let carets = LigCaretList::new(CoverageFormat1::new(vec![ligature]).into(), vec![LigGlyph::new(vec![value])]);
|
||||
let mut gdef = Gdef::new(None, None, Some(carets), None);
|
||||
let region = VariationRegion::new(vec![RegionAxisCoordinates::new(F2Dot14::from_f32(0.0), F2Dot14::from_f32(1.0), F2Dot14::from_f32(1.0))]);
|
||||
// One delta set of one 16-bit delta for the one region.
|
||||
let data = ItemVariationData::new(1, 1, vec![0], delta.to_be_bytes().to_vec());
|
||||
gdef.item_var_store = Some(ItemVariationStore::new(VariationRegionList::new(1, vec![region]), vec![Some(data)])).into();
|
||||
let axis = VariationAxisRecord {
|
||||
axis_tag: Tag::new(b"wght"),
|
||||
min_value: Fixed::from_i32(400),
|
||||
default_value: Fixed::from_i32(400),
|
||||
max_value: Fixed::from_i32(700),
|
||||
flags: 0,
|
||||
axis_name_id: NameId::new(256),
|
||||
};
|
||||
let instance = |name: u16, wght: i32| InstanceRecord {
|
||||
subfamily_name_id: NameId::new(name),
|
||||
flags: 0,
|
||||
coordinates: vec![Fixed::from_i32(wght)],
|
||||
post_script_name_id: None,
|
||||
};
|
||||
let fvar = Fvar::new(AxisInstanceArrays::new(vec![axis], vec![instance(257, 400), instance(258, 700)]));
|
||||
let family = VARIABLE_LIGATURE_CARET_FAMILY;
|
||||
let name = name_table(&[
|
||||
(1, family),
|
||||
(2, "Regular"),
|
||||
(4, "Varicaret Sans Regular"),
|
||||
(6, "VaricaretSans-Regular"),
|
||||
(256, "Weight"),
|
||||
(257, "Regular"),
|
||||
(258, "Bold"),
|
||||
])?;
|
||||
let mut builder = FontBuilder::new();
|
||||
builder.add_raw(Tag::new(b"name"), name);
|
||||
builder.add_table(&gdef).ok()?.add_table(&fvar).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())
|
||||
}
|
||||
|
||||
/// [`ligature_caret_font`] with its one caret at x = `caret` font units and a `GPOS` `kern` single
|
||||
/// adjustment that places the ligature glyph `x` font units right of its pen position. `None` when
|
||||
/// it can't be built.
|
||||
pub fn placed_ligature_caret_font(caret: i16, x: i16) -> Option<Vec<u8>> {
|
||||
use write_fonts::tables::gpos::{Gpos, PositionLookup, PositionLookupList, SinglePos, ValueRecord};
|
||||
use write_fonts::tables::layout::{CoverageFormat1, Feature, FeatureList, FeatureRecord, LangSys, Lookup, Script, ScriptList, ScriptRecord};
|
||||
use write_fonts::types::GlyphId16;
|
||||
|
||||
let base = ligature_caret_font(Some(caret))?;
|
||||
let font = skrifa::FontRef::new(&base).ok()?;
|
||||
let ligature = GlyphId16::new(source_sans_gid(LIGATURE_CARET_GLYPH)?);
|
||||
let pos = SinglePos::format_1(CoverageFormat1::new(vec![ligature]).into(), ValueRecord::new().with_x_placement(x));
|
||||
let features = FeatureList::new(vec![FeatureRecord::new(Tag::new(b"kern"), Feature::new(None, vec![0]))]);
|
||||
let lookups = PositionLookupList::new(vec![PositionLookup::Single(Lookup::new(Default::default(), vec![pos]))]);
|
||||
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 mut builder = FontBuilder::new();
|
||||
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 [`pair_kerning_font`].
|
||||
pub const PAIR_KERNING_FAMILY: &str = "Pairtest Sans";
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ mod hang;
|
||||
mod inline;
|
||||
mod joiners;
|
||||
mod justify;
|
||||
mod ligature_carets;
|
||||
mod path_joins;
|
||||
mod ruby;
|
||||
mod scripts;
|
||||
|
||||
@@ -165,6 +165,9 @@ fn lines_of_clusters(lines: &[&[(usize, usize, f64)]], rtl: bool) -> TextLayout
|
||||
bend: None,
|
||||
unturned: Affine::IDENTITY,
|
||||
pivot: Point::new(x, baseline),
|
||||
carets: Vec::new(),
|
||||
width: advance,
|
||||
width_start: 0.0,
|
||||
});
|
||||
x += advance;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,498 @@
|
||||
//! Caret stops inside ligatures: the caret, a click, arrow keys and the selection reach each
|
||||
//! character a ligature glyph stands for, at the font's ligature carets (`GDEF`) or, without them,
|
||||
//! at the glyph's advance divided equally.
|
||||
|
||||
use kurbo::{BezPath, Point, Vec2};
|
||||
use vectorcraft_doc::{CharStyle, TextKind, TextObject};
|
||||
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,
|
||||
};
|
||||
use crate::*;
|
||||
|
||||
/// A database of the bundled fonts and `font`.
|
||||
fn db_with(font: Option<Vec<u8>>) -> FontDb {
|
||||
let db = FontDb::with_font_dirs(vec![]);
|
||||
assert_eq!(db.add_font(font.expect("the test font builds")), 1);
|
||||
db
|
||||
}
|
||||
|
||||
fn point(text: &str, family: &str) -> TextObject {
|
||||
TextObject::point(Point::ZERO, text, CharStyle { font_family: family.into(), size: 100.0, ..CharStyle::default() })
|
||||
}
|
||||
|
||||
/// The caret's x for byte `b`.
|
||||
fn caret_x(l: &TextLayout, b: usize) -> f64 {
|
||||
caret_position(l, b).0.x
|
||||
}
|
||||
|
||||
/// The x of a click at `at` (0 to 1) along glyph `g`'s advance from its left, on its baseline.
|
||||
fn along(g: &PositionedGlyph, at: f64) -> Point {
|
||||
Point::new(g.origin.x + g.advance * at, g.origin.y)
|
||||
}
|
||||
|
||||
/// A font whose caret list for a ligature holds a number of carets other than the one asked for
|
||||
/// gives none.
|
||||
#[test]
|
||||
fn a_caret_list_of_another_length_gives_no_carets() {
|
||||
let db = db_with(ligature_caret_font(Some(150)));
|
||||
let face = db.face(LIGATURE_CARET_FAMILY, "Regular").expect("the test font is added");
|
||||
let gid = u32::from(source_sans_gid(LIGATURE_CARET_GLYPH).expect("Source Sans 3 has the glyph"));
|
||||
assert_eq!(face.ligature_carets(gid, 1), Some(vec![150.0]));
|
||||
assert_eq!(face.ligature_carets(gid, 2), None);
|
||||
assert_eq!(face.ligature_carets(gid, 0), None);
|
||||
}
|
||||
|
||||
/// A variable font's ligature caret with a variation index moves with the face's axis settings: by
|
||||
/// none at the default weight, by the whole delta at the weight its region peaks at.
|
||||
#[test]
|
||||
fn a_variable_ligature_caret_takes_its_delta() {
|
||||
let db = FontDb::with_font_dirs(vec![]);
|
||||
assert_eq!(db.add_font(variable_ligature_caret_font(150, 100).expect("the test font builds")), 2, "Regular and Bold");
|
||||
let gid = u32::from(source_sans_gid(LIGATURE_CARET_GLYPH).expect("Source Sans 3 has the glyph"));
|
||||
let regular = db.face(VARIABLE_LIGATURE_CARET_FAMILY, "Regular").expect("the test font is added");
|
||||
let bold = db.face(VARIABLE_LIGATURE_CARET_FAMILY, "Bold").expect("the test font is added");
|
||||
assert_eq!(regular.ligature_carets(gid, 1), Some(vec![150.0]));
|
||||
assert_eq!(bold.ligature_carets(gid, 1), Some(vec![250.0]));
|
||||
}
|
||||
|
||||
/// The font's ligature caret is where the caret, a click, arrow keys and the selection stop inside
|
||||
/// the ligature, not the middle of the glyph.
|
||||
#[test]
|
||||
fn the_font_ligature_caret_places_the_inner_stop() {
|
||||
let db = db_with(ligature_caret_font(Some(150)));
|
||||
let face = db.face(LIGATURE_CARET_FAMILY, "Regular").expect("the test font is added");
|
||||
let gid = u32::from(source_sans_gid(LIGATURE_CARET_GLYPH).expect("Source Sans 3 has the glyph"));
|
||||
let l = layout(&db, &point("fi", LIGATURE_CARET_FAMILY));
|
||||
assert_eq!(l.glyphs.len(), 1, "f + i is one glyph: {:?}", l.glyphs);
|
||||
let g = &l.glyphs[0];
|
||||
assert_eq!((g.gid, g.len), (gid, 2));
|
||||
let at = 150.0 / face.advance(gid);
|
||||
assert!(at < 0.4, "the caret is well left of the middle: {at}");
|
||||
assert_eq!(g.carets.len(), 1);
|
||||
assert_eq!(g.carets[0].0, 1);
|
||||
assert!((g.carets[0].1 - at).abs() < 1e-9, "{:?} against {at}", g.carets);
|
||||
assert!((caret_x(&l, 1) - g.advance * at).abs() < 1e-6);
|
||||
// A click left or right of the caret goes to it rather than to either end.
|
||||
assert_eq!(hit_byte(&l, along(g, at * 0.4)), 0);
|
||||
assert_eq!(hit_byte(&l, along(g, at * 1.4)), 1);
|
||||
assert_eq!(hit_byte(&l, along(g, 0.9)), 2);
|
||||
// Arrow keys stop at it both ways, and the f alone is selected up to it.
|
||||
assert_eq!(caret_horizontal(&l, 0, true), 1);
|
||||
assert_eq!(caret_horizontal(&l, 1, true), 2);
|
||||
assert_eq!(caret_horizontal(&l, 2, false), 1);
|
||||
let q = selection_quads(&l, 0, 1);
|
||||
assert_eq!(q.len(), 1);
|
||||
assert!((q[0][1].x - q[0][0].x - g.advance * at).abs() < 1e-6, "{q:?}");
|
||||
}
|
||||
|
||||
/// The font's placement of a ligature (`GPOS` x placement) moves its caret with the glyph.
|
||||
#[test]
|
||||
fn a_placed_ligature_keeps_its_caret_on_the_glyph() {
|
||||
let at = |font: Option<Vec<u8>>| {
|
||||
let db = db_with(font);
|
||||
let l = layout(&db, &point("fi", LIGATURE_CARET_FAMILY));
|
||||
let g = l.glyphs.iter().find(|g| g.len == 2).expect("f + i is one glyph").clone();
|
||||
(caret_x(&l, 1) - g.origin.x, g.outline.bounding_box().x0 - g.origin.x, g.width_start, hit_byte(&l, Point::new(caret_x(&l, 1) + 1.0, 0.0)))
|
||||
};
|
||||
let (caret, ink, start, _) = at(ligature_caret_font(Some(150)));
|
||||
let (placed_caret, placed_ink, placed_start, hit) = at(placed_ligature_caret_font(150, 100));
|
||||
// 100 units at 100 pt in a 1000-unit em.
|
||||
assert!(start.abs() < 1e-9 && (placed_start - 10.0).abs() < 1e-9, "{start} {placed_start}");
|
||||
assert!((placed_ink - ink - 10.0).abs() < 1e-6, "the glyph moves right: {placed_ink} against {ink}");
|
||||
assert!((placed_caret - caret - 10.0).abs() < 1e-6, "the caret moves with it: {placed_caret} against {caret}");
|
||||
assert_eq!(hit, 1);
|
||||
}
|
||||
|
||||
/// A ligature whose font lists no carets for it divides its advance equally among its characters.
|
||||
#[test]
|
||||
fn a_ligature_without_font_carets_is_divided_equally() {
|
||||
let db = db_with(ligature_caret_font(None));
|
||||
let l = layout(&db, &point("fi", LIGATURE_CARET_FAMILY));
|
||||
assert_eq!(l.glyphs.len(), 1);
|
||||
let g = &l.glyphs[0];
|
||||
assert_eq!(g.carets, vec![(1, 0.5)]);
|
||||
assert!((caret_x(&l, 1) - g.advance * 0.5).abs() < 1e-6);
|
||||
assert_eq!(hit_byte(&l, along(g, 0.4)), 1);
|
||||
assert_eq!(hit_byte(&l, along(g, 0.2)), 0);
|
||||
// A caret value outside the glyph is not used either.
|
||||
let db = db_with(ligature_caret_font(Some(-20)));
|
||||
let l = layout(&db, &point("fi", LIGATURE_CARET_FAMILY));
|
||||
assert_eq!(l.glyphs[0].carets, vec![(1, 0.5)]);
|
||||
}
|
||||
|
||||
/// Tracking adds space after a ligature's own width: the font's caret stays where the font puts it
|
||||
/// in the glyph, and the caret, a click and the selection use that place, left to right and right
|
||||
/// to left.
|
||||
#[test]
|
||||
fn tracking_does_not_move_a_ligature_caret() {
|
||||
let db = db_with(ligature_caret_font(Some(150)));
|
||||
let face = db.face(LIGATURE_CARET_FAMILY, "Regular").expect("the test font is added");
|
||||
let mut t = point("fi", LIGATURE_CARET_FAMILY);
|
||||
t.runs[0].style.tracking = 50.0;
|
||||
let l = layout(&db, &t);
|
||||
assert_eq!(l.glyphs.len(), 1, "f + i stays one glyph at this tracking: {:?}", l.glyphs);
|
||||
let g = &l.glyphs[0];
|
||||
let k = 100.0 / face.units_per_em();
|
||||
let width = face.advance(g.gid) * k;
|
||||
assert!((g.width - width).abs() < 1e-9, "{} against {width}", g.width);
|
||||
assert!((g.advance - width - 5.0).abs() < 1e-9, "the advance takes in 5 pt of tracking: {}", g.advance);
|
||||
let x = g.origin.x + 150.0 * k;
|
||||
assert!((caret_x(&l, 1) - x).abs() < 1e-6, "{} against {x}", caret_x(&l, 1));
|
||||
assert_eq!(hit_byte(&l, Point::new(g.origin.x + 150.0 * k * 0.45, -10.0)), 0);
|
||||
assert_eq!(hit_byte(&l, Point::new(x + 0.3, -10.0)), 1);
|
||||
let q = selection_quads(&l, 0, 1);
|
||||
assert_eq!(q.len(), 1);
|
||||
assert!((q[0][1].x - x).abs() < 1e-6, "{q:?}");
|
||||
// Right to left the tracking lies to the glyph's right, before its first character: the stop
|
||||
// between lam and alef stays in the middle of the glyph's own width.
|
||||
let db = db_with(lam_alef_font(b"rlig"));
|
||||
let mut t = point("\u{0644}\u{0627}", lam_alef_family(b"rlig"));
|
||||
t.runs[0].style.tracking = 50.0;
|
||||
let l = layout(&db, &t);
|
||||
let g = l.glyphs.iter().find(|g| g.len == 4).expect("lam + alef is one glyph");
|
||||
assert!(g.rtl && g.advance > g.width + 4.0, "{} {}", g.advance, g.width);
|
||||
let mid = g.origin.x + g.width * 0.5;
|
||||
assert!((caret_x(&l, 2) - mid).abs() < 1e-6, "{} against {mid}", caret_x(&l, 2));
|
||||
let q = selection_quads(&l, 2, 4);
|
||||
assert_eq!(q.len(), 1, "{q:?}");
|
||||
let (x0, x1) = (q[0][0].x.min(q[0][1].x), q[0][0].x.max(q[0][1].x));
|
||||
assert!((x0 - g.origin.x).abs() < 1e-6 && (x1 - mid).abs() < 1e-6, "{q:?}");
|
||||
}
|
||||
|
||||
/// Negative kerning can end a ligature's advance before its font caret: the caret still stands at
|
||||
/// the font's place, a click at the advance's end goes to the end of the ligature, and the last
|
||||
/// character's selection runs between the two.
|
||||
#[test]
|
||||
fn negative_kerning_keeps_a_ligature_caret_past_the_advance() {
|
||||
let db = db_with(ligature_caret_font(Some(150)));
|
||||
let face = db.face(LIGATURE_CARET_FAMILY, "Regular").expect("the test font is added");
|
||||
let k = 100.0 / face.units_per_em();
|
||||
let gid = u32::from(source_sans_gid(LIGATURE_CARET_GLYPH).expect("Source Sans 3 has the glyph"));
|
||||
let width = face.advance(gid) * k;
|
||||
// Kerning (thousandths of an em at 100 pt, a tenth of a point each) that leaves an advance of
|
||||
// half the distance to the font caret.
|
||||
let kern = (150.0 * k * 0.5 - width) * 10.0;
|
||||
let mut t = point("fi", LIGATURE_CARET_FAMILY);
|
||||
t.runs[0].style.kerning = Some(kern);
|
||||
let l = layout(&db, &t);
|
||||
assert_eq!(l.glyphs.len(), 1, "f + i stays one glyph with this kerning: {:?}", l.glyphs);
|
||||
let g = &l.glyphs[0];
|
||||
assert!((g.width - width).abs() < 1e-9, "{} against {width}", g.width);
|
||||
assert!((g.advance - 150.0 * k * 0.5).abs() < 1e-6, "the advance ends before the caret: {}", g.advance);
|
||||
let x = g.origin.x + 150.0 * k;
|
||||
let end = g.origin.x + g.advance;
|
||||
assert!((caret_x(&l, 1) - x).abs() < 1e-6, "{} against {x}", caret_x(&l, 1));
|
||||
assert!((caret_x(&l, 2) - end).abs() < 1e-6, "{} against {end}", caret_x(&l, 2));
|
||||
assert_eq!(hit_byte(&l, Point::new(end, -10.0)), 2);
|
||||
assert_eq!(hit_byte(&l, Point::new(x + 0.3, -10.0)), 1);
|
||||
let q = selection_quads(&l, 1, 2);
|
||||
assert_eq!(q.len(), 1, "{q:?}");
|
||||
let (x0, x1) = (q[0][0].x.min(q[0][1].x), q[0][0].x.max(q[0][1].x));
|
||||
assert!((x0 - end).abs() < 1e-6 && (x1 - x).abs() < 1e-6, "{q:?}");
|
||||
// On a path, a click at the advance's end goes to the end of the ligature as well.
|
||||
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 };
|
||||
let l = layout(&db, &t);
|
||||
let g = l.glyphs.iter().find(|g| g.len == 2).expect("f + i is one glyph");
|
||||
let d = Vec2::new(g.angle.cos(), g.angle.sin());
|
||||
assert_eq!(hit_byte(&l, g.origin + d * g.advance), 2);
|
||||
assert_eq!(hit_byte(&l, g.origin + d * (g.width * 150.0 / face.advance(gid) + 0.3)), 1);
|
||||
}
|
||||
|
||||
/// On a path, a ligature whose negative kerning ends its advance before its font caret is followed
|
||||
/// by an i that covers the caret: a click at the caret goes to the caret, not to the i.
|
||||
#[test]
|
||||
fn a_click_at_a_ligature_caret_past_the_advance_on_a_path_reaches_it() {
|
||||
let db = db_with(ligature_caret_font(Some(150)));
|
||||
let face = db.face(LIGATURE_CARET_FAMILY, "Regular").expect("the test font is added");
|
||||
let k = 100.0 / face.units_per_em();
|
||||
let gid = u32::from(source_sans_gid(LIGATURE_CARET_GLYPH).expect("Source Sans 3 has the glyph"));
|
||||
let width = face.advance(gid) * k;
|
||||
let kern = (150.0 * k * 0.5 - width) * 10.0;
|
||||
let mut t = point("fii", LIGATURE_CARET_FAMILY);
|
||||
crate::edit::style_range(&mut t.runs, 0, 2, |st| st.kerning = Some(kern));
|
||||
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 };
|
||||
let l = layout(&db, &t);
|
||||
let g = l.glyphs.iter().find(|g| g.len == 2).expect("f + i is one glyph");
|
||||
let i = l.glyphs.iter().find(|h| h.byte == 2).expect("the second i");
|
||||
let d = Vec2::new(g.angle.cos(), g.angle.sin());
|
||||
let caret = g.origin + d * (g.width * 150.0 / face.advance(gid));
|
||||
let mid = |h: &PositionedGlyph| h.origin + d * (h.advance * 0.5);
|
||||
assert!((caret - mid(i)).hypot() < (caret - mid(g)).hypot(), "the i's middle is nearer the caret than the ligature's");
|
||||
assert_eq!(hit_byte(&l, caret + d * 0.3), 1);
|
||||
}
|
||||
|
||||
/// Tracking of -1000 (one em to the left) leaves Source Serif 4's fi ligature, with standard
|
||||
/// ligatures turned on explicitly, an advance of zero or less: the caret, a click and the selection
|
||||
/// still reach the i at its place in the glyph's own width, across and on a path.
|
||||
#[test]
|
||||
fn a_ligature_without_advance_keeps_its_inner_stop() {
|
||||
let db = FontDb::with_font_dirs(vec![]);
|
||||
let mut t = point("fi", "Source Serif 4");
|
||||
t.runs[0].style.tracking = -1000.0;
|
||||
t.runs[0].style.features = vec!["liga".into()];
|
||||
let l = layout(&db, &t);
|
||||
let g = l.glyphs.iter().find(|g| g.len == 2).expect("f + i is one glyph");
|
||||
assert!(g.advance <= 0.0 && g.width > 0.0, "{} {}", g.advance, g.width);
|
||||
let &[(1, f)] = g.carets.as_slice() else { panic!("one inner stop: {:?}", g.carets) };
|
||||
let x = g.origin.x + g.width * f;
|
||||
let end = g.origin.x + g.advance;
|
||||
assert!((caret_x(&l, 1) - x).abs() < 1e-6, "{} against {x}", caret_x(&l, 1));
|
||||
assert!((caret_x(&l, 2) - end).abs() < 1e-6, "{} against {end}", caret_x(&l, 2));
|
||||
assert_eq!(hit_byte(&l, Point::new(x + 0.3, -10.0)), 1);
|
||||
assert_eq!(hit_byte(&l, Point::new(g.origin.x + 0.3, -10.0)), 0);
|
||||
let q = selection_quads(&l, 0, 1);
|
||||
assert_eq!(q.len(), 1, "{q:?}");
|
||||
let (x0, x1) = (q[0][0].x.min(q[0][1].x), q[0][0].x.max(q[0][1].x));
|
||||
assert!((x0 - g.origin.x).abs() < 1e-6 && (x1 - x).abs() < 1e-6, "{q:?} against {} {x}", g.origin.x);
|
||||
assert_eq!(caret_horizontal(&l, 0, true), 1);
|
||||
// On a path, a click past the inner stop goes to it, and the f alone is selected up to it.
|
||||
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 };
|
||||
let l = layout(&db, &t);
|
||||
let g = l.glyphs.iter().find(|g| g.len == 2).expect("f + i is one glyph");
|
||||
assert!(g.advance <= 0.0, "{}", g.advance);
|
||||
let d = Vec2::new(g.angle.cos(), g.angle.sin());
|
||||
assert_eq!(hit_byte(&l, g.origin + d * (g.width * f + 0.3)), 1);
|
||||
assert_eq!(hit_byte(&l, g.origin + d * 0.3), 0);
|
||||
let q = selection_quads(&l, 0, 1);
|
||||
assert_eq!(q.len(), 1, "{q:?}");
|
||||
assert!(((q[0][1] - q[0][0]).hypot() - g.width * f).abs() < 1e-6, "{q:?}");
|
||||
}
|
||||
|
||||
/// Bundled Source Serif 4's fi ligature has a caret stop that a click and the arrow keys reach.
|
||||
#[test]
|
||||
fn source_serif_fi_and_ffi_have_inner_stops() {
|
||||
let db = FontDb::with_font_dirs(vec![]);
|
||||
for (s, stops) in [("fi", vec![1]), ("ffi", vec![1, 2])] {
|
||||
let l = layout(&db, &point(s, "Source Serif 4"));
|
||||
let lone: Vec<&PositionedGlyph> = l.glyphs.iter().filter(|g| g.len == s.len()).collect();
|
||||
if lone.is_empty() {
|
||||
// The font has no ligature for these letters (ffi in some versions).
|
||||
assert_ne!(s, "fi", "{:?}", l.glyphs);
|
||||
continue;
|
||||
}
|
||||
let g = lone[0];
|
||||
assert_eq!(g.carets.iter().map(|c| c.0).collect::<Vec<_>>(), stops, "{s}");
|
||||
for &(b, f) in &g.carets {
|
||||
let x = g.origin.x + g.advance * f;
|
||||
assert_eq!(hit_byte(&l, Point::new(x + 0.01, -10.0)), b, "{s} byte {b}");
|
||||
assert!((caret_x(&l, b) - x).abs() < 1e-6);
|
||||
}
|
||||
let mut b = 0;
|
||||
let mut seen = vec![];
|
||||
while b < s.len() {
|
||||
b = caret_horizontal(&l, b, true);
|
||||
seen.push(b);
|
||||
}
|
||||
assert_eq!(seen[..stops.len()], stops[..], "{s}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A right-to-left ligature (lam + alef, two bytes each) has its stop between the two characters,
|
||||
/// never inside a character, and its first character is on its right.
|
||||
#[test]
|
||||
fn a_right_to_left_ligature_stops_between_its_characters() {
|
||||
let db = db_with(lam_alef_font(b"rlig"));
|
||||
let l = layout(&db, &point("\u{0644}\u{0627}", lam_alef_family(b"rlig")));
|
||||
let g = l.glyphs.iter().find(|g| g.len == 4).expect("lam + alef is one glyph");
|
||||
assert!(g.rtl);
|
||||
assert_eq!(g.carets, vec![(2, 0.5)]);
|
||||
// Right of the middle is the lam, left of it the alef.
|
||||
assert_eq!(hit_byte(&l, along(g, 0.9)), 0);
|
||||
assert_eq!(hit_byte(&l, along(g, 0.6)), 2);
|
||||
assert_eq!(hit_byte(&l, along(g, 0.1)), 4);
|
||||
assert!((caret_x(&l, 2) - (g.origin.x + g.advance * 0.5)).abs() < 1e-6);
|
||||
assert!(caret_x(&l, 0) > caret_x(&l, 2) && caret_x(&l, 2) > caret_x(&l, 4));
|
||||
// The lam alone is selected on the right half.
|
||||
let q = selection_quads(&l, 0, 2);
|
||||
assert_eq!(q.len(), 1, "{q:?}");
|
||||
let (x0, x1) = (q[0][0].x.min(q[0][1].x), q[0][0].x.max(q[0][1].x));
|
||||
assert!((x0 - (g.origin.x + g.advance * 0.5)).abs() < 1e-6 && (x1 - (g.origin.x + g.advance)).abs() < 1e-6, "{q:?}");
|
||||
// Arrow keys (visual stops) take the inner stop: right to left, left goes forward.
|
||||
assert_eq!(caret_horizontal_cluster(&l, "\u{0644}\u{0627}", 0, false), 2);
|
||||
assert_eq!(caret_horizontal_cluster(&l, "\u{0644}\u{0627}", 2, false), 4);
|
||||
}
|
||||
|
||||
/// Right to left, manual kerning moves a ligature right and puts its space on the left: the inner
|
||||
/// stop moves with the glyph, and the click, the caret and the selection of each character follow.
|
||||
#[test]
|
||||
fn manual_kerning_moves_a_right_to_left_ligature_stop_with_the_glyph() {
|
||||
let db = db_with(lam_alef_font(b"rlig"));
|
||||
let at = |kern: f64| {
|
||||
let mut t = point("\u{0644}\u{0627}", lam_alef_family(b"rlig"));
|
||||
t.runs[0].style.kerning = Some(kern);
|
||||
let l = layout(&db, &t);
|
||||
let g = l.glyphs.iter().find(|g| g.len == 4).expect("lam + alef is one glyph").clone();
|
||||
assert!(g.rtl);
|
||||
(l, g)
|
||||
};
|
||||
let (plain, p) = at(0.0);
|
||||
let (kerned, k) = at(100.0);
|
||||
// 100/1000 em at 100 pt.
|
||||
assert!((k.width_start - 10.0).abs() < 1e-9 && (k.advance - p.advance - 10.0).abs() < 1e-9, "{} {} {}", k.width_start, k.advance, p.advance);
|
||||
let ink = |g: &PositionedGlyph| g.outline.bounding_box().x0;
|
||||
assert!((ink(&k) - k.origin.x - (ink(&p) - p.origin.x) - 10.0).abs() < 1e-6, "the glyph moves right");
|
||||
let mid = k.origin.x + 10.0 + k.width * 0.5;
|
||||
assert!((caret_x(&kerned, 2) - ink(&k) - (caret_x(&plain, 2) - ink(&p))).abs() < 1e-6, "the stop moves with the glyph");
|
||||
assert!((caret_x(&kerned, 2) - mid).abs() < 1e-6, "{} against {mid}", caret_x(&kerned, 2));
|
||||
assert_eq!(hit_byte(&kerned, Point::new(mid + 1.0, k.origin.y)), 2);
|
||||
assert_eq!(hit_byte(&kerned, Point::new(mid + 4.0, k.origin.y)), 2);
|
||||
assert_eq!(hit_byte(&kerned, Point::new(k.origin.x + k.advance - 1.0, k.origin.y)), 0);
|
||||
let q = selection_quads(&kerned, 0, 2);
|
||||
assert_eq!(q.len(), 1, "{q:?}");
|
||||
let x0 = q[0][0].x.min(q[0][1].x);
|
||||
assert!((x0 - mid).abs() < 1e-6, "the lam alone is selected right of the stop: {q:?}");
|
||||
}
|
||||
|
||||
/// A letter with a combining mark is one character: a glyph for it has no inner stop.
|
||||
#[test]
|
||||
fn a_letter_and_its_mark_has_no_inner_stop() {
|
||||
let db = FontDb::with_font_dirs(vec![]);
|
||||
let l = layout(&db, &point("e\u{0301}", "Source Sans 3"));
|
||||
assert!(l.glyphs.iter().all(|g| g.carets.is_empty()), "{:?}", l.glyphs);
|
||||
}
|
||||
|
||||
/// On a path, a click on either half of the ligature goes to its nearer stop, and the selection of
|
||||
/// one character covers its part of the glyph.
|
||||
#[test]
|
||||
fn a_ligature_on_a_path_has_its_inner_stop() {
|
||||
let db = db_with(ligature_caret_font(None));
|
||||
let mut line = BezPath::new();
|
||||
line.move_to((0.0, 0.0));
|
||||
line.line_to((1000.0, 0.0));
|
||||
let mut t = point("fi", LIGATURE_CARET_FAMILY);
|
||||
t.kind = TextKind::OnPath { path: PathData::from_bezpath(&line), start: 0.0, end: None };
|
||||
let l = layout(&db, &t);
|
||||
assert!(l.on_path);
|
||||
let g = l.glyphs.iter().find(|g| g.len == 2).expect("f + i is one glyph");
|
||||
assert_eq!(g.carets, vec![(1, 0.5)]);
|
||||
let at = |f: f64| g.origin + Vec2::new(g.angle.cos(), g.angle.sin()) * (g.advance * f);
|
||||
assert_eq!(hit_byte(&l, at(0.1)), 0);
|
||||
assert_eq!(hit_byte(&l, at(0.4)), 1);
|
||||
assert_eq!(hit_byte(&l, at(0.95)), 2);
|
||||
let q = selection_quads(&l, 1, 2);
|
||||
assert_eq!(q.len(), 1);
|
||||
assert!(((q[0][1] - q[0][0]).hypot() - g.advance * 0.5).abs() < 1e-6, "{q:?}");
|
||||
}
|
||||
|
||||
/// In vertical type, the inner stop of a turned Latin ligature lies down the column between its
|
||||
/// ends.
|
||||
#[test]
|
||||
fn a_ligature_in_vertical_type_has_its_inner_stop() {
|
||||
let db = db_with(ligature_caret_font(None));
|
||||
let mut t = point("fi", LIGATURE_CARET_FAMILY);
|
||||
t.vertical = true;
|
||||
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_eq!(g.carets, vec![(1, 0.5)]);
|
||||
let y = |b| caret_position(&l, b).0.y;
|
||||
assert!(y(0) < y(1) && y(1) < y(2), "{} {} {}", y(0), y(1), y(2));
|
||||
let mid = caret_position(&l, 1);
|
||||
assert_eq!(hit_byte(&l, mid.0.midpoint(mid.1) + Vec2::new(0.0, 0.01)), 1);
|
||||
}
|
||||
|
||||
/// In vertical type, tracking adds space after a turned ligature's own width as it does across:
|
||||
/// the font's caret stays the same distance down the column from the ligature's start, and the
|
||||
/// last character's selection runs from it to the end of the advance.
|
||||
#[test]
|
||||
fn tracking_does_not_move_a_ligature_caret_in_vertical_type() {
|
||||
let db = db_with(ligature_caret_font(Some(150)));
|
||||
let face = db.face(LIGATURE_CARET_FAMILY, "Regular").expect("the test font is added");
|
||||
let k = 100.0 / face.units_per_em();
|
||||
let at = |tracking: f64| {
|
||||
let mut t = point("fi", LIGATURE_CARET_FAMILY);
|
||||
t.vertical = true;
|
||||
t.runs[0].style.tracking = tracking;
|
||||
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) };
|
||||
let width = face.advance(g.gid) * k;
|
||||
assert!((g.width - width).abs() < 1e-9, "{} against {width}", g.width);
|
||||
let y = |b| caret_position(&l, b).0.y;
|
||||
let q = selection_quads(&l, 1, 2);
|
||||
assert_eq!(q.len(), 1, "{q:?}");
|
||||
let ys = q[0].map(|p| p.y);
|
||||
let (y0, y1) = (ys.iter().copied().fold(f64::INFINITY, f64::min), ys.iter().copied().fold(f64::NEG_INFINITY, f64::max));
|
||||
assert!((y0 - y(1)).abs() < 1e-6 && (y1 - y(2)).abs() < 1e-6, "{q:?} {} {}", y(1), y(2));
|
||||
(y(1) - y(0), y(2) - y(0), g.advance)
|
||||
};
|
||||
let (plain, plain_end, _) = at(0.0);
|
||||
let (tracked, tracked_end, advance) = at(50.0);
|
||||
assert!((plain - 150.0 * k).abs() < 1e-6, "{plain} against {}", 150.0 * k);
|
||||
assert!((tracked - plain).abs() < 1e-6, "{tracked} against {plain}");
|
||||
assert!((tracked_end - advance).abs() < 1e-6 && (tracked_end - plain_end - 5.0).abs() < 1e-6, "{tracked_end} {plain_end} {advance}");
|
||||
}
|
||||
|
||||
/// A ligature standing upright in vertical type (Standard Vertical Roman Alignment) takes its font
|
||||
/// caret as a y coordinate: the stop lies that far below the top of its cell, which runs from its
|
||||
/// vertical origin (0.88 em) one em down the column. A caret above the cell is not used.
|
||||
#[test]
|
||||
fn an_upright_ligature_takes_its_caret_down_its_cell() {
|
||||
let at = |caret: i16| {
|
||||
let db = db_with(vertical_ligature_caret_font(caret));
|
||||
let mut t = point("fi", VERTICAL_LIGATURE_CARET_FAMILY);
|
||||
t.vertical = true;
|
||||
t.runs[0].style.standard_vertical_roman = true;
|
||||
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) };
|
||||
let face = db.face(VERTICAL_LIGATURE_CARET_FAMILY, "Regular").expect("the test font is added");
|
||||
let cell = 1000.0 * 100.0 / face.units_per_em();
|
||||
assert!((g.width - cell).abs() < 1e-9, "the glyph stands upright, one em down the column: {} against {cell}", g.width);
|
||||
let &[(1, f)] = g.carets.as_slice() else { panic!("one inner stop: {:?}", g.carets) };
|
||||
let y = |b| caret_position(&l, b).0.y;
|
||||
assert!(((y(1) - y(0)) - g.width * f).abs() < 1e-6, "{} {} against {f}", y(0), y(1));
|
||||
f
|
||||
};
|
||||
// 0.88 em at the top: y = 280 is 600 units down, y = -50 is 930 units down.
|
||||
assert!((at(280) - 0.6).abs() < 1e-9, "{}", at(280));
|
||||
assert!((at(-50) - 0.93).abs() < 1e-9, "{}", at(-50));
|
||||
assert!((at(900) - 0.5).abs() < 1e-9, "{}", at(900));
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[test]
|
||||
fn an_upright_ligature_with_a_negative_advance_keeps_its_cell() {
|
||||
let at = |font: Option<Vec<u8>>, family: &str, tracking: f64| {
|
||||
let db = db_with(font);
|
||||
let mut t = point("fi", family);
|
||||
t.vertical = true;
|
||||
t.runs[0].style.standard_vertical_roman = true;
|
||||
t.runs[0].style.tracking = tracking;
|
||||
// Ligatures turned on by name stay on at any tracking.
|
||||
t.runs[0].style.features = vec!["+liga".into()];
|
||||
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) };
|
||||
let face = db.face(family, "Regular").expect("the test font is added");
|
||||
assert!(tracking == 0.0 || g.advance <= 0.0, "the tracking makes the advance negative: {}", g.advance);
|
||||
let cell = 1000.0 * 100.0 / face.units_per_em();
|
||||
assert!((g.width - cell).abs() < 1e-9, "the glyph stands upright, one em down the column: {} against {cell}", g.width);
|
||||
let y = |b| caret_position(&l, b).0.y;
|
||||
let &[(1, f)] = g.carets.as_slice() else { panic!("one inner stop: {:?}", g.carets) };
|
||||
assert!(((y(1) - y(0)) - g.width * f).abs() < 1e-6, "{} {} against {f}", y(0), y(1));
|
||||
(f, g.outline.bounding_box().y0 - y(0))
|
||||
};
|
||||
let vertical = |tracking| at(vertical_ligature_caret_font(280), VERTICAL_LIGATURE_CARET_FAMILY, tracking);
|
||||
let (f, top) = vertical(0.0);
|
||||
assert!((f - 0.6).abs() < 1e-9, "{f}");
|
||||
let (tracked, tracked_top) = vertical(-1500.0);
|
||||
assert!((tracked - f).abs() < 1e-9, "the caret is read down the cell: {tracked} against {f}");
|
||||
assert!((tracked_top - top).abs() < 1e-6, "{tracked_top} against {top}");
|
||||
let plain = |tracking| at(ligature_caret_font(None), LIGATURE_CARET_FAMILY, tracking);
|
||||
let (f, top) = plain(0.0);
|
||||
assert!((f - 0.5).abs() < 1e-9, "{f}");
|
||||
let (tracked, tracked_top) = plain(-1500.0);
|
||||
assert!((tracked - f).abs() < 1e-9, "{tracked} against {f}");
|
||||
assert!((tracked_top - top).abs() < 1e-6, "the cell starts where it does without tracking: {tracked_top} against {top}");
|
||||
}
|
||||
@@ -764,6 +764,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
While a drag rotates the view, a dial at the center of the canvas shows the angle.
|
||||
- **Editing type:** double-clicking type with a selection tool (Selection or Direct Selection) switches to the Type tool with the caret where it was clicked; pressing Escape finishes editing and switches back to the selection tool used before double-clicking it (when in isolation mode, the first Escape exits text editing while preserving isolation mode, and a second Escape exits isolation).
|
||||
While a Type tool edits type, a click that lands on no type ends the editing without making new type, and that tool stays active; the next click makes new type (#2871, #2854).
|
||||
Inside a ligature (one glyph for several characters, such as fi), the Type tool's caret, clicks, arrow keys and selection stop between its characters, at the font's ligature carets ([OpenType GDEF ligature caret list](https://learn.microsoft.com/en-us/typography/opentype/spec/gdef)) or, when the font does not list one valid caret for each stop, at the glyph's width divided equally.
|
||||
Duplicate Artboards, the Artboard tool's Alt-drag and Paste give the new artboard an id that no other artboard has, also when an artboard's id is the largest possible value (#1718). Deleting the copy or the original then leaves the other's guides.
|
||||
- **Touch Type:** a click with the Touch Type tool (Shift+T) selects one character and shows a box around it. Dragging the box or its lower-left handle moves the character. The upper-left handle scales it vertically, the lower-right handle scales it horizontally, the upper-right handle scales it proportionally, and the upper-middle handle rotates it. Each drag is one undo step that sets the character's kerning, baseline shift, scales or rotation, and the type stays editable (`text.touchType`; behavior reference: [Touch Type tool](https://web.archive.org/web/20261007185617/https://helpx.adobe.com/illustrator/using/tool-techniques/touch-type-tool.html), [Add text with the Type, Area Type, and Touch Type tools](https://web.archive.org/web/20260926095922/https://helpx.adobe.com/illustrator/desktop/design-with-text/add-manage-text/add-text-illustrator-text-tools.html)).
|
||||
A character rotated near or at a quarter turn follows a Touch Type drag across its line as well as along it: baseline shift moves a character up from its line at any rotation, and the character turns about its shifted pen position.
|
||||
|
||||
Reference in New Issue
Block a user