mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 06:31:41 +00:00
Type: Warichu blocks keep their places, clicks and line height in mixed text
- Insert Kashidas skips the characters of a warichu block. - Blocks whose characters the bidi order draws in turn keep where each block starts. - A click on a block line goes to that line's characters. - Digits marked Warichu in vertical type on a path stay across the column. - A block of several fonts gives its line the tallest ascent and the deepest descent among them. - Warichu Settings on characters not marked Warichu do not split shaping. - Warichu Settings leaves a value outside its range as it is unless it is edited.
This commit is contained in:
@@ -104,8 +104,8 @@ pub(crate) const MAX_TATWEELS: f64 = 64.0;
|
||||
/// The kashidas of the glyphs `line` (indices into `sg`, a paragraph's glyphs in logical order;
|
||||
/// `text` is the plain text) of a justified line with `room` points to fill: for each Arabic
|
||||
/// word with joined letters, at its last pair of them, the byte offset of the cluster after the kashida and
|
||||
/// the kashida's length, in text order. Words end at spaces, inline graphics, and characters other than letters
|
||||
/// and marks (punctuation, digits, symbols). Every kashida takes an equal share of `room`, up to
|
||||
/// the kashida's length, in text order. Words end at spaces, inline graphics, warichu blocks, and characters other
|
||||
/// than letters and marks (punctuation, digits, symbols). Every kashida takes an equal share of `room`, up to
|
||||
/// `kind`'s longest in ems of the letter after it (as wide as its horizontal scale draws it); a
|
||||
/// Stylistic kashida is a whole number of tatweels, at most [`MAX_TATWEELS`], and a word whose
|
||||
/// share is shorter than one tatweel gets none. A line of words without joined letters, or whose
|
||||
@@ -124,7 +124,8 @@ pub(crate) fn plan(text: &str, sg: &[SGlyph], line: Range<usize>, kind: Kashida,
|
||||
let end = (j + 1..line.end).find(|&e| sg.get(e).is_none_or(|n| n.byte != g.byte)).unwrap_or(line.end);
|
||||
let first = text.get(g.byte..g.byte + g.len).and_then(|s| s.chars().next());
|
||||
let not_a_letter = first.is_some_and(|c| !c.is_alphabetic() && joining(c) != Joining::Transparent);
|
||||
if g.is_space() || g.inline.is_some() || not_a_letter {
|
||||
// A warichu block is set as one piece at the length its lines set: it ends a word too.
|
||||
if g.is_space() || g.inline.is_some() || g.wari.is_some() || not_a_letter {
|
||||
picks.extend(word.take());
|
||||
prev = None;
|
||||
} else {
|
||||
|
||||
@@ -244,7 +244,9 @@ impl Ctx<'_> {
|
||||
if self.vertical {
|
||||
tate_chu_yoko(v, self.system_layout || !self.fractional_widths, |g| {
|
||||
let st = style(g);
|
||||
TcyStyle { size: st.size, marked: st.tate_chu_yoko, up: st.tcy_up_down, right: st.tcy_left_right, warichu: st.warichu }
|
||||
// Type on a path sets warichu characters as they are, its digits included.
|
||||
let warichu = st.warichu && !self.on_path;
|
||||
TcyStyle { size: st.size, marked: st.tate_chu_yoko, up: st.tcy_up_down, right: st.tcy_left_right, warichu }
|
||||
});
|
||||
// An upright glyph advances down the column by its vertical advance (the font's vertical
|
||||
// metrics; without them at least one em, as CJK fonts' is), centred across it: a narrow
|
||||
@@ -619,6 +621,7 @@ impl Ctx<'_> {
|
||||
},
|
||||
width,
|
||||
width_start,
|
||||
warichu: g.wari.map(|w| w.start),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -962,14 +962,15 @@ fn tab_glyph<'p>(
|
||||
/// Emit `glyph` of a line placed by `transform` at `origin` with `advance` along the line, its
|
||||
/// Insert Aki before it `aki` and the glyph scaling a justified line adds to it `scale`. A warichu
|
||||
/// glyph is drawn at its place in its block instead: the block starts where the first of its glyphs
|
||||
/// drawn on the line is placed (`block` holds the byte naming that block and where it starts), and
|
||||
/// the glyph sits on the baseline of its block line, measured from where Character Alignment sets
|
||||
/// the block on the line (`origin`), with its own advance for the caret, selection and hit testing.
|
||||
/// drawn on the line is placed (`blocks` holds the byte naming each block of the line and where it
|
||||
/// starts, kept when the bidi order draws another block's glyphs between two of its own), and the
|
||||
/// glyph sits on the baseline of its block line, measured from where Character Alignment sets the
|
||||
/// block on the line (`origin`), with its own advance for the caret, selection and hit testing.
|
||||
fn emit_line_glyph(
|
||||
cx: &mut Ctx<'_>,
|
||||
glyph: &SGlyph,
|
||||
(transform, origin, advance): (Affine, Point, f64),
|
||||
block: &mut Option<(usize, f64)>,
|
||||
blocks: &mut Vec<(usize, f64)>,
|
||||
line_index: usize,
|
||||
(aki, scale): (f64, f64),
|
||||
) {
|
||||
@@ -983,10 +984,10 @@ fn emit_line_glyph(
|
||||
}
|
||||
return;
|
||||
};
|
||||
let start = match *block {
|
||||
Some((byte, at)) if byte == w.start => at,
|
||||
_ => {
|
||||
*block = Some((w.start, origin.x));
|
||||
let start = match blocks.iter().find(|(byte, _)| *byte == w.start) {
|
||||
Some(&(_, at)) => at,
|
||||
None => {
|
||||
blocks.push((w.start, origin.x));
|
||||
origin.x
|
||||
}
|
||||
};
|
||||
@@ -1025,8 +1026,8 @@ fn emit_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, paragraph: &ParagraphData
|
||||
// The glyphs drawn for the list marker, and the index of its tab's glyph, when the line starts the paragraph.
|
||||
let mut marker_glyphs: Option<Range<usize>> = None;
|
||||
let mut marker_tab = 0;
|
||||
// The warichu block being drawn: the byte that names it and where it starts on the line.
|
||||
let mut wari_block: Option<(usize, f64)> = None;
|
||||
// The warichu blocks drawn on the line: the byte that names each and where it starts.
|
||||
let mut wari_blocks: Vec<(usize, f64)> = Vec::new();
|
||||
for (order_index, &offset) in order.iter().enumerate() {
|
||||
let glyph_index = index + offset;
|
||||
let Some(glyph) = sg.get(glyph_index) else { continue };
|
||||
@@ -1081,7 +1082,7 @@ fn emit_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, paragraph: &ParagraphData
|
||||
}
|
||||
}
|
||||
let glyph_scale = paragraph.flex.get(glyph_index).map_or(1.0, |flex| flex.scale) * scale;
|
||||
emit_line_glyph(cx, glyph, (transform, Point::new(x, y), advance), &mut wari_block, line_index, (aki, glyph_scale));
|
||||
emit_line_glyph(cx, glyph, (transform, Point::new(x, y), advance), &mut wari_blocks, line_index, (aki, glyph_scale));
|
||||
if !leader.is_empty() {
|
||||
// The leader belongs to the tab's glyph: the gap stays one character for the
|
||||
// caret, selection and hit testing.
|
||||
|
||||
@@ -377,5 +377,6 @@ fn ruby_glyph(g: &PositionedGlyph, m: Affine, run: usize, byte: usize, line: usi
|
||||
carets: Vec::new(),
|
||||
width: 0.0,
|
||||
width_start: 0.0,
|
||||
warichu: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ pub(super) fn tate_chu_yoko(g: &mut [SGlyph], whole: bool, style: impl Fn(&SGlyp
|
||||
|
||||
/// What [`tate_chu_yoko`] reads from a glyph's character style: its size, whether it is marked
|
||||
/// Tate-chu-yoko, its Tate-chu-yoko Settings Up/Down and Left/Right in points, and whether it is
|
||||
/// marked Warichu.
|
||||
/// set as warichu (marked Warichu, and not on a path).
|
||||
pub(super) struct TcyStyle {
|
||||
pub size: f64,
|
||||
pub marked: bool,
|
||||
|
||||
@@ -182,7 +182,10 @@ fn place_block(g: &mut [SGlyph], s: &WariStyle, (vertical, whole): (bool, bool))
|
||||
let Some(first) = g.first() else { return };
|
||||
let start = first.byte;
|
||||
let ic = first.face.ideographic_centre();
|
||||
let (ascent, descent) = (first.ascent, first.descent);
|
||||
// The block's tallest ascent and deepest descent: its glyphs may come from several faces (runs
|
||||
// in other fonts, the classes of a composite font, a fallback face).
|
||||
let ascent = g.iter().map(|gl| gl.ascent).fold(first.ascent, f64::max);
|
||||
let descent = g.iter().map(|gl| gl.descent).fold(first.descent, f64::max);
|
||||
let clusters = clusters(g);
|
||||
// Each glyph's advance without its Insert Aki.
|
||||
let own = |gl: &SGlyph| gl.adv - gl.aki;
|
||||
|
||||
+43
-4
@@ -180,6 +180,10 @@ pub struct PositionedGlyph {
|
||||
/// path stands with its top toward the path's start, and its cell starts at its top: 0.12 em
|
||||
/// before the pen without the font's vertical metrics.
|
||||
pub width_start: f64,
|
||||
/// A glyph of a warichu block: the byte offset of the block's first character, which names the
|
||||
/// block. The block's lines share its line of text, so hit testing takes the block line nearest
|
||||
/// the point before the nearest caret stop along it. `None` for any other glyph.
|
||||
pub warichu: Option<usize>,
|
||||
}
|
||||
|
||||
/// How a glyph on a path is bent along it: a point of its pen space (x along its advance from its
|
||||
@@ -631,11 +635,18 @@ pub fn hit_byte(layout: &TextLayout, p: Point) -> usize {
|
||||
}) else {
|
||||
return 0;
|
||||
};
|
||||
byte_in_line(layout, li, p.x)
|
||||
byte_near(layout, li, p.x, Some(p.y))
|
||||
}
|
||||
|
||||
/// Byte offset nearest to `x` on line `li`.
|
||||
pub fn byte_in_line(layout: &TextLayout, li: usize, x: f64) -> usize {
|
||||
byte_near(layout, li, x, None)
|
||||
}
|
||||
|
||||
/// Byte offset nearest to `x` on line `li`; with `y` (text space, as `x`), a warichu block's caret
|
||||
/// stops count only on the block line whose baseline is nearest `y`, since the block's lines share
|
||||
/// one line of text.
|
||||
fn byte_near(layout: &TextLayout, li: usize, x: f64, y: Option<f64>) -> usize {
|
||||
let Some(line) = layout.lines.get(li) else { return 0 };
|
||||
let glyphs = &layout.glyphs[line.glyph_start..line.glyph_end];
|
||||
// A cluster drawn with several adjacent glyphs (a Devanagari pre-base matra and its consonant,
|
||||
@@ -653,11 +664,39 @@ pub fn byte_in_line(layout: &TextLayout, li: usize, x: f64) -> usize {
|
||||
_ => clusters.push((left, right, g)),
|
||||
}
|
||||
}
|
||||
// Each warichu block's distance from `y` to the nearest baseline of its lines.
|
||||
let mut rows: Vec<(usize, f64)> = Vec::new();
|
||||
if let Some(y) = y {
|
||||
for g in glyphs {
|
||||
let Some(block) = g.warichu else { continue };
|
||||
let d = (layout.logical_point(g.origin).y - y).abs();
|
||||
match rows.iter_mut().find(|(b, _)| *b == block) {
|
||||
Some((_, best)) => *best = best.min(d),
|
||||
None => rows.push((block, d)),
|
||||
}
|
||||
}
|
||||
}
|
||||
let on_row = |g: &PositionedGlyph| match (g.warichu, y) {
|
||||
(Some(block), Some(y)) => {
|
||||
let d = (layout.logical_point(g.origin).y - y).abs();
|
||||
rows.iter().find(|(b, _)| *b == block).is_none_or(|(_, best)| d <= best + 1e-9)
|
||||
}
|
||||
_ => true,
|
||||
};
|
||||
// A stop of the block line taken wins over a stop of the text around the block at the same
|
||||
// place: at the block's start the text before it ends where both of its lines start.
|
||||
let nearest = clusters
|
||||
.iter()
|
||||
.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)
|
||||
.filter(|(_, _, g)| on_row(g))
|
||||
.flat_map(|&(left, right, g)| {
|
||||
let row = y.is_some() && g.warichu.is_some();
|
||||
g.stops_along(right - left).map(move |(b, d)| (if g.rtl { right - d } else { left + d }, b, row))
|
||||
})
|
||||
.min_by(|a, b| {
|
||||
let (da, db) = ((a.0 - x).abs(), (b.0 - x).abs());
|
||||
if a.2 != b.2 && (da - db).abs() <= 1e-9 { b.2.cmp(&a.2) } else { da.total_cmp(&db) }
|
||||
});
|
||||
nearest.map_or(line.start, |(_, byte, _)| byte)
|
||||
}
|
||||
|
||||
/// Caret byte at the end of line `li`: a soft-wrapped line ends before its trailing space so the
|
||||
|
||||
+28
-22
@@ -971,7 +971,7 @@ fn source_run_at(runs: &[(Range<usize>, &CharStyle)], span: &Range<usize>, byte:
|
||||
/// Horizontal type does not read Standard Vertical Roman Alignment or the tate-chu-yoko offsets.
|
||||
/// The space before a line's first character ([`space_before`]) and Insert Aki ([`aki`]) come
|
||||
/// from the cluster's first character, so they do not split a ligature or a letter from its marks
|
||||
/// either.
|
||||
/// either. Warichu Settings count only on characters marked Warichu ([`warichu_alike`]).
|
||||
fn drawn_alike(a: &CharStyle, b: &CharStyle, vertical: bool) -> bool {
|
||||
// Exhaustive, like `shapes_alike`: compare borrowed fields without copying feature lists.
|
||||
let CharStyle {
|
||||
@@ -1016,11 +1016,12 @@ fn drawn_alike(a: &CharStyle, b: &CharStyle, vertical: bool) -> bool {
|
||||
tcy_up_down,
|
||||
tcy_left_right,
|
||||
standard_vertical_roman,
|
||||
warichu,
|
||||
warichu_lines,
|
||||
warichu_gap,
|
||||
warichu_scale,
|
||||
warichu_align,
|
||||
// Compared by `warichu_alike`.
|
||||
warichu: _,
|
||||
warichu_lines: _,
|
||||
warichu_gap: _,
|
||||
warichu_scale: _,
|
||||
warichu_align: _,
|
||||
digits,
|
||||
font_height_reference,
|
||||
no_break,
|
||||
@@ -1068,11 +1069,7 @@ fn drawn_alike(a: &CharStyle, b: &CharStyle, vertical: bool) -> bool {
|
||||
&& kashidas == &b.kashidas
|
||||
&& proportional_metrics == &b.proportional_metrics
|
||||
&& tate_chu_yoko == &b.tate_chu_yoko
|
||||
&& warichu == &b.warichu
|
||||
&& warichu_lines == &b.warichu_lines
|
||||
&& warichu_gap == &b.warichu_gap
|
||||
&& warichu_scale == &b.warichu_scale
|
||||
&& warichu_align == &b.warichu_align
|
||||
&& warichu_alike(a, b)
|
||||
&& digits == &b.digits
|
||||
&& font_height_reference == &b.font_height_reference
|
||||
&& no_break == &b.no_break
|
||||
@@ -1080,6 +1077,18 @@ fn drawn_alike(a: &CharStyle, b: &CharStyle, vertical: bool) -> bool {
|
||||
&& glyph_alternate == &b.glyph_alternate
|
||||
}
|
||||
|
||||
/// Do characters in styles `a` and `b` set warichu alike: neither marked Warichu, whatever their
|
||||
/// Warichu Settings (which then take no part in setting them), or both marked with the same
|
||||
/// Warichu Settings?
|
||||
pub(crate) fn warichu_alike(a: &CharStyle, b: &CharStyle) -> bool {
|
||||
a.warichu == b.warichu
|
||||
&& (!a.warichu
|
||||
|| (a.warichu_lines == b.warichu_lines
|
||||
&& a.warichu_gap == b.warichu_gap
|
||||
&& a.warichu_scale == b.warichu_scale
|
||||
&& a.warichu_align == b.warichu_align))
|
||||
}
|
||||
|
||||
/// Do text runs in styles `a` and `b` shape alike, so that they are shaped as one piece of text?
|
||||
/// They do when they differ only in paint and stroke, underline and strikethrough, overprint, the
|
||||
/// character style's name, leading, baseline shift, rotation, Character Alignment, Character
|
||||
@@ -1135,12 +1144,13 @@ fn shapes_alike(a: &CharStyle, b: &CharStyle) -> bool {
|
||||
tcy_left_right: _,
|
||||
standard_vertical_roman: _,
|
||||
// A warichu block is shaped apart from the text around it and from a block with other
|
||||
// settings: its characters are set at their own size and stacked by its settings.
|
||||
warichu,
|
||||
warichu_lines,
|
||||
warichu_gap,
|
||||
warichu_scale,
|
||||
warichu_align,
|
||||
// settings: its characters are set at their own size and stacked by its settings
|
||||
// ([`warichu_alike`]).
|
||||
warichu: _,
|
||||
warichu_lines: _,
|
||||
warichu_gap: _,
|
||||
warichu_scale: _,
|
||||
warichu_align: _,
|
||||
digits,
|
||||
font_height_reference,
|
||||
no_break,
|
||||
@@ -1148,11 +1158,7 @@ fn shapes_alike(a: &CharStyle, b: &CharStyle) -> bool {
|
||||
glyph_alternate,
|
||||
} = a;
|
||||
*font_family == b.font_family
|
||||
&& *warichu == b.warichu
|
||||
&& *warichu_lines == b.warichu_lines
|
||||
&& *warichu_gap == b.warichu_gap
|
||||
&& *warichu_scale == b.warichu_scale
|
||||
&& *warichu_align == b.warichu_align
|
||||
&& warichu_alike(a, b)
|
||||
&& *font_style == b.font_style
|
||||
&& *font_version == b.font_version
|
||||
&& *font_axes == b.font_axes
|
||||
|
||||
@@ -702,6 +702,36 @@ pub fn lam_alef_font(feature: &[u8; 4]) -> Option<Vec<u8>> {
|
||||
Some(builder.build())
|
||||
}
|
||||
|
||||
/// The family of [`tatweel_font`].
|
||||
pub const TATWEEL_FAMILY: &str = "Tatweeltest Sans";
|
||||
|
||||
/// A test-only Arabic cmap over bundled Source Sans 3, without Arabic shaping: beh uses b, teh t,
|
||||
/// dal d, the tatweel the hyphen and the space the space, so that a justified line takes kashidas
|
||||
/// without an OS font. `None` when it can't be built.
|
||||
pub fn tatweel_font() -> Option<Vec<u8>> {
|
||||
use write_fonts::tables::cmap::Cmap;
|
||||
|
||||
const BASE: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf");
|
||||
let font = skrifa::FontRef::new(BASE).ok()?;
|
||||
let charmap = font.charmap();
|
||||
let mapping = [(' ', ' '), ('\u{0628}', 'b'), ('\u{062A}', 't'), ('\u{062F}', 'd'), ('\u{0640}', '-')];
|
||||
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 full = format!("{TATWEEL_FAMILY} Regular");
|
||||
let postscript = format!("{}-Regular", TATWEEL_FAMILY.replace(' ', ""));
|
||||
let name = name_table(&[(1, TATWEEL_FAMILY), (2, "Regular"), (4, &full), (6, &postscript)])?;
|
||||
let mut builder = FontBuilder::new();
|
||||
builder.add_table(&cmap).ok()?;
|
||||
builder.add_raw(Tag::new(b"name"), name);
|
||||
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 [`alternates_font`] with an `aalt` feature, and without one.
|
||||
pub fn alternates_family(aalt: bool) -> &'static str {
|
||||
if aalt { "Alttest Sans" } else { "Alttest Sans Noaalt" }
|
||||
|
||||
@@ -168,6 +168,7 @@ fn lines_of_clusters(lines: &[&[(usize, usize, f64)]], rtl: bool) -> TextLayout
|
||||
carets: Vec::new(),
|
||||
width: advance,
|
||||
width_start: 0.0,
|
||||
warichu: None,
|
||||
});
|
||||
x += advance;
|
||||
}
|
||||
|
||||
@@ -609,6 +609,7 @@ fn duplicate_authored_glyphs_keep_occurrence_order() {
|
||||
carets: Vec::new(),
|
||||
width: 0.0,
|
||||
width_start: 0.0,
|
||||
warichu: None,
|
||||
};
|
||||
let before = TextLayout { glyphs: vec![glyph(0), glyph(1)], ..TextLayout::default() };
|
||||
assert_eq!(assert_kashida_ownership(&text, &before, &before, &red(), "duplicate glyphs", false), 0);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Warichu (Character panel menu › Warichu and Warichu Settings): marked characters set smaller
|
||||
//! and stacked on lines inside their line. Bundled fonts: in vertical type `§` stands upright like
|
||||
//! CJK.
|
||||
use super::{area, db, point, style};
|
||||
use super::{area, db, point, serif, style};
|
||||
use crate::*;
|
||||
use kurbo::Shape;
|
||||
use vectorcraft_doc::{CharStyle, Justify, TextKind, TextRun, WarichuAlign};
|
||||
@@ -412,3 +412,124 @@ fn shrink_text_to_fit_scales_the_warichu_line_gap() {
|
||||
let l = layout(db(), &s);
|
||||
assert!((l.glyphs[4].origin.y - l.glyphs[2].origin.y - (5.0 + 5.0)).abs() < 1e-6);
|
||||
}
|
||||
|
||||
/// Two blocks whose characters the bidi order draws in turn (Latin, then Hebrew in the first;
|
||||
/// Hebrew, then Latin in the second) each keep where they start: both lines of a block start there.
|
||||
#[test]
|
||||
fn blocks_the_bidi_order_interleaves_keep_their_places() {
|
||||
let mut t = with_block("", marked(20.0));
|
||||
t.runs = vec![
|
||||
TextRun::new("ab", style(20.0)),
|
||||
TextRun::new("cא", marked(20.0)),
|
||||
TextRun::new("בd", CharStyle { warichu_lines: 3, ..marked(20.0) }),
|
||||
TextRun::new("z", style(20.0)),
|
||||
];
|
||||
let l = layout(db(), &t);
|
||||
let at = |byte: usize| l.glyphs.iter().find(|g| g.byte == byte).unwrap().origin;
|
||||
let (c, alef, bet, d, z) = (at(2), at(3), at(5), at(7), at(8));
|
||||
assert!(alef.y > c.y && d.y > bet.y, "two lines each: {c:?} {alef:?} {bet:?} {d:?}");
|
||||
assert!((alef.x - c.x).abs() < 1e-9, "the first block's lines start together: {c:?} {alef:?}");
|
||||
assert!((d.x - bet.x).abs() < 1e-9, "the second block's lines start together: {bet:?} {d:?}");
|
||||
assert!(c.x < bet.x && bet.x < z.x, "{c:?} {bet:?} {z:?}");
|
||||
}
|
||||
|
||||
/// A click on a block's lower line goes to that line's characters, not to the character of the
|
||||
/// upper line at the same place along the block.
|
||||
#[test]
|
||||
fn a_click_on_a_block_line_goes_to_its_own_characters() {
|
||||
let l = layout(db(), &with_block("cdef", marked(20.0)));
|
||||
let (c, d, e, f) = (&l.glyphs[2], &l.glyphs[3], &l.glyphs[4], &l.glyphs[5]);
|
||||
let near = |g: &PositionedGlyph, share: f64| Point::new(g.origin.x + g.advance * share, g.origin.y - 2.0);
|
||||
assert_eq!(hit_byte(&l, near(e, 0.25)), 4);
|
||||
assert_eq!(hit_byte(&l, near(f, 0.75)), 6);
|
||||
assert_eq!(hit_byte(&l, near(c, 0.25)), 2);
|
||||
assert_eq!(hit_byte(&l, near(d, 0.75)), 4);
|
||||
// Without a point across the line, the nearest stop along it, as before.
|
||||
assert_eq!(byte_in_line(&l, 0, l.glyphs[0].origin.x), 0);
|
||||
}
|
||||
|
||||
/// Digits marked Warichu in vertical type on a path stand across the column as digits without the
|
||||
/// mark do: type on a path does not stack warichu.
|
||||
#[test]
|
||||
fn vertical_type_on_a_path_keeps_its_digits_across_the_column() {
|
||||
let on_path = |st: CharStyle| {
|
||||
let mut t = point("", style(20.0));
|
||||
t.xf = Affine::IDENTITY;
|
||||
t.vertical = true;
|
||||
t.runs = vec![TextRun::new("12", st)];
|
||||
t.kind = TextKind::OnPath { path: PathData::from_bezpath(&kurbo::Line::new((0.0, 0.0), (0.0, 400.0)).to_path(0.1)), start: 0.0, end: None };
|
||||
let l = layout(db(), &t);
|
||||
(ink(&l, 0), ink(&l, 1))
|
||||
};
|
||||
assert_eq!(on_path(marked(20.0)), on_path(style(20.0)));
|
||||
}
|
||||
|
||||
/// A block whose characters come from fonts of different heights gives its line the tallest ascent
|
||||
/// and the deepest descent among them, whichever font comes first.
|
||||
#[test]
|
||||
fn a_block_of_several_fonts_keeps_the_tallest_of_them() {
|
||||
let fonts = ["Source Sans 3", "Source Serif 4", "Inter", "JetBrains Mono"];
|
||||
let st = |family: &str, warichu: bool| CharStyle { font_family: family.into(), warichu, ..style(20.0) };
|
||||
let line = |runs: Vec<TextRun>| {
|
||||
let mut t = point("", style(20.0));
|
||||
t.xf = Affine::IDENTITY;
|
||||
t.runs = runs;
|
||||
let l = layout(db(), &t);
|
||||
(l.lines[0].ascent, l.lines[0].descent)
|
||||
};
|
||||
let plain: Vec<(f64, f64)> = fonts.iter().map(|f| line(vec![TextRun::new("x", st(f, false))])).collect();
|
||||
// The two fonts whose ascents differ most.
|
||||
let mut pair = (0, 0);
|
||||
for i in 0..fonts.len() {
|
||||
for j in 0..fonts.len() {
|
||||
if plain[i].0 - plain[j].0 > plain[pair.0].0 - plain[pair.1].0 {
|
||||
pair = (i, j);
|
||||
}
|
||||
}
|
||||
}
|
||||
let (tall, short) = (fonts[pair.0], fonts[pair.1]);
|
||||
assert!(plain[pair.0].0 - plain[pair.1].0 > 0.1, "the bundled fonts differ in height: {plain:?}");
|
||||
let first_short = line(vec![TextRun::new("c", st(short, true)), TextRun::new("d", st(tall, true))]);
|
||||
let first_tall = line(vec![TextRun::new("c", st(tall, true)), TextRun::new("d", st(short, true))]);
|
||||
assert!((first_short.0 - first_tall.0).abs() < 1e-6 && (first_short.1 - first_tall.1).abs() < 1e-6, "{first_short:?} {first_tall:?}");
|
||||
let deepest = plain[pair.0].1.max(plain[pair.1].1);
|
||||
assert!(first_short.0 >= plain[pair.0].0 - 1e-6 && first_short.1 >= deepest - 1e-6, "{first_short:?} {plain:?}");
|
||||
}
|
||||
|
||||
/// Warichu Settings on characters not marked Warichu leave their shaping alone: "fi" keeps its
|
||||
/// ligature when only the f has other settings.
|
||||
#[test]
|
||||
fn warichu_settings_without_the_mark_keep_a_ligature() {
|
||||
let fi = |f: CharStyle| {
|
||||
let mut t = point("", serif(20.0));
|
||||
t.runs = vec![TextRun::new("f", f), TextRun::new("i", serif(20.0))];
|
||||
layout(db(), &t)
|
||||
};
|
||||
let glyphs = |l: &TextLayout| l.glyphs.iter().map(|g| (g.gid, g.len)).collect::<Vec<_>>();
|
||||
let plain = fi(serif(20.0));
|
||||
assert!(plain.glyphs.iter().any(|g| g.len == 2), "the bundled serif joins f and i: {:?}", glyphs(&plain));
|
||||
let settings = CharStyle { warichu_lines: 3, warichu_gap: 2.0, warichu_scale: 70.0, warichu_align: WarichuAlign::Center, ..serif(20.0) };
|
||||
assert_eq!(glyphs(&fi(settings)), glyphs(&plain));
|
||||
}
|
||||
|
||||
/// Insert Kashidas leaves a warichu block alone: a justified line lengthens its other Arabic words
|
||||
/// with kashidas, and the block keeps the length its lines set.
|
||||
#[test]
|
||||
fn kashidas_leave_a_warichu_block_alone() {
|
||||
use crate::test_fonts::{TATWEEL_FAMILY, tatweel_font};
|
||||
use vectorcraft_doc::text::Kashida;
|
||||
let db = FontDb::with_font_dirs(vec![]);
|
||||
db.add_font(tatweel_font().expect("the test font builds"));
|
||||
let face = db.face(TATWEEL_FAMILY, "Regular").expect("the test face");
|
||||
let arabic = |warichu: bool| CharStyle { font_family: TATWEEL_FAMILY.into(), warichu, ..style(20.0) };
|
||||
let (before, block) = ("تبد تبد ", "بتب");
|
||||
let mut t = area("", arabic(false), Rect::new(0.0, 0.0, 300.0, 400.0), Justify::JustifyAll);
|
||||
t.para.kashida = Kashida::Medium;
|
||||
t.runs = vec![TextRun::new(before, arabic(false)), TextRun::new(block, arabic(true)), TextRun::new(" تبد", arabic(false))];
|
||||
let l = layout(&db, &t);
|
||||
let tatweel = face.glyph_for('\u{0640}');
|
||||
let kashidas: Vec<usize> = l.glyphs.iter().filter(|g| g.len == 0 && g.gid == tatweel).map(|g| g.byte).collect();
|
||||
assert!(!kashidas.is_empty(), "the words around the block take kashidas");
|
||||
let inside = before.len()..before.len() + block.len();
|
||||
assert!(kashidas.iter().all(|b| !inside.contains(b)), "{kashidas:?} {inside:?}");
|
||||
}
|
||||
|
||||
@@ -127,12 +127,15 @@ fn number(d: &Dialog, key: &str, name: &str) -> Result<Option<f64>, String> {
|
||||
/// `text.setFormat` parameters: the fields that differ from the values the dialog opened with.
|
||||
/// Lines is rounded to a whole number and kept within 1..6, Line Gap is in points (a length typed
|
||||
/// with its unit, "1 mm", is converted) and kept within 0..100, and Scale is kept within 10..100.
|
||||
/// A blank field is left alone; a field that is not a number is an error.
|
||||
/// A blank field is left alone; a field that is not a number is an error. A number is compared
|
||||
/// with the value the dialog opened with before it is rounded or kept within its range, so OK
|
||||
/// leaves a value outside the range (Line Gap on a scaled object) as it is when it was not edited.
|
||||
fn params(d: &Dialog) -> Result<Map<String, Value>, String> {
|
||||
let initial = d.fields.get(INITIAL).and_then(Value::as_object);
|
||||
let unchanged = |key: &str, v: &Value| initial.and_then(|m| m.get(key)) == Some(v);
|
||||
let opened_with = |key: &str, v: f64| initial.and_then(|m| m.get(key)).and_then(Value::as_f64) == Some(v);
|
||||
let mut p = Map::new();
|
||||
if let Some(n) = number(d, "warichuLines", "Lines")? {
|
||||
if let Some(n) = number(d, "warichuLines", "Lines")?.filter(|&n| !opened_with("warichuLines", n)) {
|
||||
let n = n.round().clamp(f64::from(*WARICHU_LINES.start()), f64::from(*WARICHU_LINES.end()));
|
||||
// Within 1..6 after the clamp, so the cast keeps the value.
|
||||
let v = json!(n as u8);
|
||||
@@ -147,14 +150,14 @@ fn params(d: &Dialog) -> Result<Map<String, Value>, String> {
|
||||
Some(Value::String(s)) => Some(vectorcraft_doc::Unit::Points.parse(s).filter(|v| v.is_finite()).ok_or("Line Gap must be a number")?),
|
||||
Some(_) => return Err("Line Gap must be a number".into()),
|
||||
};
|
||||
if let Some(v) = gap.map(|v| json!(v.clamp(0.0, MAX_WARICHU_GAP)))
|
||||
&& initial.and_then(|m| m.get("warichuGap")).and_then(Value::as_f64) != v.as_f64()
|
||||
if let Some(v) = gap.filter(|&v| !opened_with("warichuGap", v)).map(|v| v.clamp(0.0, MAX_WARICHU_GAP))
|
||||
&& !opened_with("warichuGap", v)
|
||||
{
|
||||
p.insert("warichuGap".into(), v);
|
||||
p.insert("warichuGap".into(), json!(v));
|
||||
}
|
||||
if let Some(n) = number(d, "warichuScale", "Scale")? {
|
||||
if let Some(n) = number(d, "warichuScale", "Scale")?.filter(|&n| !opened_with("warichuScale", n)) {
|
||||
let v = n.clamp(*WARICHU_SCALE.start(), *WARICHU_SCALE.end());
|
||||
if initial.and_then(|m| m.get("warichuScale")).and_then(Value::as_f64) != Some(v) {
|
||||
if !opened_with("warichuScale", v) {
|
||||
p.insert("warichuScale".into(), json!(v));
|
||||
}
|
||||
}
|
||||
@@ -241,6 +244,19 @@ mod tests {
|
||||
assert!(app.ui.dialog.is_none());
|
||||
}
|
||||
|
||||
/// A value it opened with outside its range (Line Gap of 200 pt on an object scaled by 2) is
|
||||
/// left alone when OK is pressed without editing it; an edited one is kept within the range.
|
||||
#[test]
|
||||
fn warichu_settings_dialog_leaves_an_unedited_value_outside_its_range() {
|
||||
let opened = json!({"warichuLines": 9, "warichuGap": 200.0, "warichuScale": 5.0});
|
||||
let mut fields = opened.clone();
|
||||
fields[INITIAL] = opened;
|
||||
let mut d = Dialog::new(KIND, fields);
|
||||
assert_eq!(params(&d), Ok(Map::new()));
|
||||
d.fields.insert("warichuGap".into(), json!(150.0));
|
||||
assert_eq!(params(&d).unwrap().get("warichuGap"), Some(&json!(MAX_WARICHU_GAP)));
|
||||
}
|
||||
|
||||
/// Characters that differ open a blank field; OK leaves that value alone and sets the others.
|
||||
#[test]
|
||||
fn warichu_settings_dialog_leaves_mixed_values_blank() {
|
||||
|
||||
Reference in New Issue
Block a user