mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 21:44:57 +00:00
Type: justification spaces a right-to-left warichu block, and the caret keeps to its block line
A justified line opened no gap before a right-to-left warichu block: the glyph drawn first after the text before it is not the block's first glyph, and it was taken as continuing the block. The block now starts at the first of its glyphs the line draws, and blocks the bidi order interleaves keep their starts. The caret next to a character of a block spanned the whole line. It now spans the band of the character's block line, as the selection does.
This commit is contained in:
@@ -544,6 +544,9 @@ struct LineJustification {
|
||||
fit: LineFit,
|
||||
start_x: f64,
|
||||
after: Vec<Option<usize>>,
|
||||
/// The warichu glyphs drawn inside a block already started, by offset from the line's first
|
||||
/// glyph ([`blocks_begun`]).
|
||||
begun: Vec<bool>,
|
||||
kashidas: Vec<(usize, f64)>,
|
||||
/// Right-aligned area type: the line's last tab, which takes the text after it to the right
|
||||
/// edge (the text before it sits at its stops, from the left edge).
|
||||
@@ -805,14 +808,14 @@ fn visible_after(index: usize, glyph: usize, after: &[Option<usize>], sg: &[SGly
|
||||
next
|
||||
}
|
||||
|
||||
// The glyph after which a gap between drawn glyphs opens, and the visible glyph that follows it:
|
||||
// not a held glyph, and never a zero-width format control on either side.
|
||||
fn gap_pair<'a>(glyph: usize, held: &[usize], after: &[Option<usize>], index: usize, sg: &'a [SGlyph]) -> Option<(&'a SGlyph, &'a SGlyph)> {
|
||||
// The glyph after which a gap between drawn glyphs opens, and the visible glyph that follows it
|
||||
// with its index: not a held glyph, and never a zero-width format control on either side.
|
||||
fn gap_pair<'a>(glyph: usize, held: &[usize], after: &[Option<usize>], index: usize, sg: &'a [SGlyph]) -> Option<(&'a SGlyph, &'a SGlyph, usize)> {
|
||||
if held.contains(&glyph) {
|
||||
return None;
|
||||
}
|
||||
let a = sg.get(glyph).filter(|a| !is_format_control(a.ch))?;
|
||||
visible_after(index, glyph, after, sg).and_then(|next| sg.get(next)).map(|b| (a, b))
|
||||
visible_after(index, glyph, after, sg).and_then(|next| sg.get(next).map(|b| (a, b, next)))
|
||||
}
|
||||
|
||||
// Are `a` and `b` glyphs of the same warichu block? Right to left, the bidi order can draw a
|
||||
@@ -821,18 +824,61 @@ fn same_warichu_block(a: &SGlyph, b: &SGlyph) -> bool {
|
||||
a.wari.zip(b.wari).is_some_and(|(first, second)| first.start == second.start)
|
||||
}
|
||||
|
||||
// Is glyph `next` drawn inside a block the line has already started: a tate-chu-yoko glyph after
|
||||
// its block's first, or a warichu glyph drawn after another glyph of its block (`begun`, by offset
|
||||
// from the line's first glyph, from [`blocks_begun`])? A warichu block starts at the first of its
|
||||
// glyphs drawn on the line, which right to left is not the block's first glyph.
|
||||
fn continues_drawn_block(next: usize, index: usize, begun: &[bool], sg: &[SGlyph]) -> bool {
|
||||
let Some(b) = sg.get(next) else { return false };
|
||||
match (b.wari, next.checked_sub(index).and_then(|k| begun.get(k))) {
|
||||
(Some(_), Some(&begun)) => begun,
|
||||
_ => b.continues_block(),
|
||||
}
|
||||
}
|
||||
|
||||
// The warichu glyphs of `line` drawn after another glyph of their block, by offset from the line's
|
||||
// first glyph. A block the bidi order interleaves with another keeps its start.
|
||||
fn blocks_begun(line: &LineGeometry, sg: &[SGlyph]) -> Vec<bool> {
|
||||
let len = line.body.saturating_sub(line.index);
|
||||
// Each glyph's block, as the offset of the block's first glyph: a block's glyphs are neighbors
|
||||
// in `sg`, and a block stays on one line.
|
||||
let mut first: Vec<usize> = Vec::with_capacity(len);
|
||||
for k in 0..len {
|
||||
let pair = k.checked_sub(1).and_then(|p| sg.get(line.index + p)).zip(sg.get(line.index + k));
|
||||
let block = match first.last() {
|
||||
Some(&f) if pair.is_some_and(|(a, b)| same_warichu_block(a, b)) => f,
|
||||
_ => k,
|
||||
};
|
||||
first.push(block);
|
||||
}
|
||||
let mut started = vec![false; len];
|
||||
let mut begun = vec![false; len];
|
||||
for &j in &line.drawn {
|
||||
let Some(k) = j.checked_sub(line.index).filter(|_| sg.get(j).is_some_and(|g| g.wari.is_some())) else { continue };
|
||||
let Some(&f) = first.get(k) else { continue };
|
||||
if let (Some(seen), Some(slot)) = (started.get(f).copied(), begun.get_mut(k)) {
|
||||
*slot = seen;
|
||||
}
|
||||
if let Some(seen) = started.get_mut(f) {
|
||||
*seen = true;
|
||||
}
|
||||
}
|
||||
begun
|
||||
}
|
||||
|
||||
// Japanese (and Chinese) lines are justified between their characters (JLREQ 3.8): the
|
||||
// gaps next to a CJK character, not inside a Latin word, a tate-chu-yoko or warichu block or a
|
||||
// shaped cluster.
|
||||
pub(super) fn is_cjk_gap(glyph: usize, held: &[usize], after: &[Option<usize>], index: usize, sg: &[SGlyph]) -> bool {
|
||||
gap_pair(glyph, held, after, index, sg).is_some_and(|(a, b)| {
|
||||
// shaped cluster. `begun` holds the warichu glyphs drawn inside a block already started
|
||||
// ([`blocks_begun`]).
|
||||
pub(super) fn is_cjk_gap(glyph: usize, held: &[usize], (after, begun): (&[Option<usize>], &[bool]), index: usize, sg: &[SGlyph]) -> bool {
|
||||
gap_pair(glyph, held, after, index, sg).is_some_and(|(a, b, next)| {
|
||||
a.byte != b.byte
|
||||
&& !a.is_space()
|
||||
&& !b.is_space()
|
||||
&& !b.continues_block()
|
||||
&& !same_warichu_block(a, b)
|
||||
&& a.ch != '\t'
|
||||
&& (is_cjk(a.ch) || is_cjk(b.ch))
|
||||
&& !continues_drawn_block(next, index, begun, sg)
|
||||
}) && !in_pair(glyph, index, after, sg)
|
||||
}
|
||||
|
||||
@@ -861,13 +907,20 @@ fn in_pair(glyph: usize, index: usize, after: &[Option<usize>], sg: &[SGlyph]) -
|
||||
// its drawn glyphs: not after a held glyph, nor inside a tate-chu-yoko block (one cell) or a
|
||||
// warichu block (set as one piece), a shaped cluster, whose glyphs (a letter and its combining marks, a conjunct and its matra) stay
|
||||
// together, or an inseparable pair. On a line that ends in a drawn hyphen, the gap before the
|
||||
// hyphen counts too: it follows the line's last visible glyph.
|
||||
pub(super) fn is_letter_gap(glyph: usize, held: &[usize], after: &[Option<usize>], index: usize, sg: &[SGlyph], hyphen: bool) -> bool {
|
||||
// hyphen counts too: it follows the line's last visible glyph. `begun` is as for [`is_cjk_gap`].
|
||||
pub(super) fn is_letter_gap(
|
||||
glyph: usize,
|
||||
held: &[usize],
|
||||
(after, begun): (&[Option<usize>], &[bool]),
|
||||
index: usize,
|
||||
sg: &[SGlyph],
|
||||
hyphen: bool,
|
||||
) -> bool {
|
||||
if held.contains(&glyph) || sg.get(glyph).is_none_or(|a| is_format_control(a.ch)) || in_pair(glyph, index, after, sg) {
|
||||
return false;
|
||||
}
|
||||
match gap_pair(glyph, held, after, index, sg) {
|
||||
Some((a, b)) => a.byte != b.byte && !b.continues_block() && !same_warichu_block(a, b),
|
||||
Some((a, b, next)) => a.byte != b.byte && !same_warichu_block(a, b) && !continues_drawn_block(next, index, begun, sg),
|
||||
None => hyphen,
|
||||
}
|
||||
}
|
||||
@@ -974,7 +1027,8 @@ fn justify_flow_line(cx: &Ctx<'_>, paragraph: &ParagraphData<'_, '_>, line: &Lin
|
||||
*slot = Some(b);
|
||||
}
|
||||
}
|
||||
let cjk_gaps = (line.index..line.body).filter(|&j| free(j) && is_cjk_gap(j, held, &after, line.index, sg)).count();
|
||||
let begun = blocks_begun(line, sg);
|
||||
let cjk_gaps = (line.index..line.body).filter(|&j| free(j) && is_cjk_gap(j, held, (&after, &begun), line.index, sg)).count();
|
||||
// Single Word Justification: a justified line holding one word (no word space) is
|
||||
// spread between its letters (Full Justify) or aligned. A tab separates two words.
|
||||
let single_word = justify && fixed.is_empty() && spaces == 0 && cjk_gaps == 0;
|
||||
@@ -1013,7 +1067,7 @@ fn justify_flow_line(cx: &Ctx<'_>, paragraph: &ParagraphData<'_, '_>, line: &Lin
|
||||
let pushed_in = line.position.pushed_in && paragraph.regions.is_some() && cjk_gaps > 0 && width > room + EPS;
|
||||
if pushed_in {
|
||||
let ems: f64 = (line.index..line.body)
|
||||
.filter(|&j| free(j) && is_cjk_gap(j, held, &after, line.index, sg) && may_squeeze(&sg[j]))
|
||||
.filter(|&j| free(j) && is_cjk_gap(j, held, (&after, &begun), line.index, sg) && may_squeeze(&sg[j]))
|
||||
.map(|j| squeeze_em(&sg[j], cx.vertical))
|
||||
.sum();
|
||||
if ems > EPS {
|
||||
@@ -1044,7 +1098,7 @@ fn justify_flow_line(cx: &Ctx<'_>, paragraph: &ParagraphData<'_, '_>, line: &Lin
|
||||
let used: f64 = flex.iter().map(|f| fit.letter(f) + fit.glyph(f)).sum();
|
||||
let rest = room - width - used;
|
||||
let hyphen = line.hyphen.is_some();
|
||||
let gaps = (line.index..line.body).filter(|&j| free(j) && is_letter_gap(j, held, &after, line.index, sg, hyphen)).count();
|
||||
let gaps = (line.index..line.body).filter(|&j| free(j) && is_letter_gap(j, held, (&after, &begun), line.index, sg, hyphen)).count();
|
||||
if gaps > 0 && rest > EPS {
|
||||
per_gap = rest / gaps as f64;
|
||||
}
|
||||
@@ -1076,7 +1130,7 @@ fn justify_flow_line(cx: &Ctx<'_>, paragraph: &ParagraphData<'_, '_>, line: &Lin
|
||||
_ => line.ax1 + line.opt_r - width,
|
||||
}
|
||||
};
|
||||
LineJustification { per_space, per_gap, per_cjk, squeeze, fit, start_x, after, kashidas, last_tab, trailing, fixed }
|
||||
LineJustification { per_space, per_gap, per_cjk, squeeze, fit, start_x, after, begun, kashidas, last_tab, trailing, fixed }
|
||||
}
|
||||
|
||||
/// The advance and leader of tab `glyph_index` of `line`, drawn at `order_index` of
|
||||
@@ -1203,10 +1257,10 @@ fn emit_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, paragraph: &ParagraphData
|
||||
advance += justification.fit.letter(&flex) + justification.fit.glyph(&flex);
|
||||
scale = justification.fit.glyph_scale(&flex);
|
||||
if justification.per_cjk != 0.0 || justification.squeeze != 0.0 {
|
||||
if is_cjk_gap(glyph_index, &line.held, &justification.after, index, sg) {
|
||||
if is_cjk_gap(glyph_index, &line.held, (&justification.after, &justification.begun), index, sg) {
|
||||
advance += justification.per_cjk + squeeze_after(glyph, justification.squeeze, cx.vertical);
|
||||
}
|
||||
} else if is_letter_gap(glyph_index, &line.held, &justification.after, index, sg, line.hyphen.is_some()) {
|
||||
} else if is_letter_gap(glyph_index, &line.held, (&justification.after, &justification.begun), index, sg, line.hyphen.is_some()) {
|
||||
advance += justification.per_gap;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1173,11 +1173,14 @@ fn justification_adds_no_gap_inside_a_warichu_block_drawn_right_to_left() {
|
||||
let mut glyphs = synthetic_glyphs(&[(0, 2, 10.0, first), (2, 2, 0.0, second)]);
|
||||
block(&mut glyphs[0], 0, 0.0);
|
||||
block(&mut glyphs[1], 0, 10.0);
|
||||
assert!(!flow::is_letter_gap(1, &[], &after, 0, &glyphs, false), "{first}{second}");
|
||||
assert!(!flow::is_cjk_gap(1, &[], &after, 0, &glyphs), "{first}{second}");
|
||||
// Glyph 0 is drawn after glyph 1 of its block.
|
||||
let begun = [true, false];
|
||||
assert!(!flow::is_letter_gap(1, &[], (&after, &begun), 0, &glyphs, false), "{first}{second}");
|
||||
assert!(!flow::is_cjk_gap(1, &[], (&after, &begun), 0, &glyphs), "{first}{second}");
|
||||
// The same two glyphs as the first glyphs of two blocks.
|
||||
block(&mut glyphs[1], 2, 0.0);
|
||||
assert!(flow::is_letter_gap(1, &[], &after, 0, &glyphs, false), "{first}{second}");
|
||||
assert_eq!(flow::is_cjk_gap(1, &[], &after, 0, &glyphs), first == '漢', "{first}{second}");
|
||||
let begun = [false, false];
|
||||
assert!(flow::is_letter_gap(1, &[], (&after, &begun), 0, &glyphs, false), "{first}{second}");
|
||||
assert_eq!(flow::is_cjk_gap(1, &[], (&after, &begun), 0, &glyphs), first == '漢', "{first}{second}");
|
||||
}
|
||||
}
|
||||
|
||||
+27
-10
@@ -556,25 +556,32 @@ 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`]). Inside a ligature the
|
||||
/// caret stands at its caret stops ([`PositionedGlyph::carets`]).
|
||||
/// caret stands at its caret stops ([`PositionedGlyph::carets`]). Next to a character of a warichu
|
||||
/// block the caret spans the band of its block line, as its highlight does.
|
||||
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);
|
||||
};
|
||||
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 (pos, angle, glyph) = if let Some(i) = glyphs.iter().position(|g| g.byte <= byte && g.byte + g.len > byte) {
|
||||
let g = &glyphs[i];
|
||||
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),
|
||||
};
|
||||
let off = g.caret_offset(byte, advance);
|
||||
(origin + dir(g.angle) * if g.rtl { advance - off } else { off }, g.angle)
|
||||
(origin + dir(g.angle) * if g.rtl { advance - off } else { off }, g.angle, Some(g))
|
||||
} 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)
|
||||
(g.origin + dir(g.angle) * if g.rtl { 0.0 } else { g.advance }, g.angle, Some(g))
|
||||
} else {
|
||||
(layout.physical_point(Point::new(line.x0, line.baseline)), if layout.vertical { std::f64::consts::FRAC_PI_2 } else { 0.0 })
|
||||
(layout.physical_point(Point::new(line.x0, line.baseline)), if layout.vertical { std::f64::consts::FRAC_PI_2 } else { 0.0 }, None)
|
||||
};
|
||||
if let Some((_, top, bottom)) =
|
||||
glyph.filter(|g| g.warichu.is_some() && !layout.on_path).and_then(|g| block_band(&warichu_rows(layout, line), line, g))
|
||||
{
|
||||
let x = layout.logical_point(pos).x;
|
||||
return (layout.physical_point(Point::new(x, top)), layout.physical_point(Point::new(x, bottom)));
|
||||
}
|
||||
// Up vector in y-down space, rotated with the baseline.
|
||||
let d = dir(angle);
|
||||
let up = Vec2::new(d.y, -d.x);
|
||||
@@ -786,6 +793,20 @@ fn warichu_rows(layout: &TextLayout, l: &LineInfo) -> Vec<(usize, f64, f64)> {
|
||||
rows
|
||||
}
|
||||
|
||||
/// The band across line `l` (text space, from the top) of the block line that warichu glyph `g` is
|
||||
/// on, with that block line's index in its block: out to halfway to the block lines next to it,
|
||||
/// and to the line's top or bottom past the block's outer lines. `rows` are the line's
|
||||
/// [`warichu_rows`]. None for a glyph of no block.
|
||||
fn block_band(rows: &[(usize, f64, f64)], l: &LineInfo, g: &PositionedGlyph) -> Option<(usize, f64, f64)> {
|
||||
let block = g.warichu?;
|
||||
let lines = rows.get(rows.partition_point(|r| r.0 < block)..rows.partition_point(|r| r.0 <= block)).unwrap_or_default();
|
||||
let row = lines.partition_point(|r| r.1 < g.warichu_line - 1e-9);
|
||||
let own = lines.get(row).filter(|r| (r.1 - g.warichu_line).abs() < 1e-9)?;
|
||||
let top = row.checked_sub(1).and_then(|k| lines.get(k)).map_or(l.baseline - l.ascent, |above| (above.2 + own.2) * 0.5);
|
||||
let bottom = lines.get(row + 1).map_or(l.baseline + l.descent, |below| (below.2 + own.2) * 0.5);
|
||||
Some((row, top, bottom))
|
||||
}
|
||||
|
||||
/// A glyph of a warichu block line, for [`selection_quads`]: its block and block line, its extent
|
||||
/// along the line and its band across it (text space), and whether the selection covers it.
|
||||
struct BlockPiece {
|
||||
@@ -829,11 +850,7 @@ pub fn selection_quads(layout: &TextLayout, a: usize, b: usize) -> Vec<[Point; 4
|
||||
let mut pieces: Vec<BlockPiece> = Vec::new();
|
||||
for (i, g) in layout.glyphs.iter().enumerate().take(l.glyph_end).skip(l.glyph_start) {
|
||||
let Some(block) = g.warichu else { continue };
|
||||
let lines = rows.get(rows.partition_point(|r| r.0 < block)..rows.partition_point(|r| r.0 <= block)).unwrap_or_default();
|
||||
let row = lines.partition_point(|r| r.1 < g.warichu_line - 1e-9);
|
||||
let Some(own) = lines.get(row).filter(|r| (r.1 - g.warichu_line).abs() < 1e-9) else { continue };
|
||||
let top = row.checked_sub(1).and_then(|k| lines.get(k)).map_or(l.baseline - l.ascent, |above| (above.2 + own.2) * 0.5);
|
||||
let bottom = lines.get(row + 1).map_or(l.baseline + l.descent, |below| (below.2 + own.2) * 0.5);
|
||||
let Some((row, top, bottom)) = block_band(&rows, l, g) else { continue };
|
||||
let x = layout.logical_point(g.origin).x;
|
||||
let selected = selected(i, g);
|
||||
let (f0, f1) = if selected { covered(g) } else { (0.0, g.advance) };
|
||||
|
||||
@@ -433,6 +433,51 @@ fn blocks_the_bidi_order_interleaves_keep_their_places() {
|
||||
assert!(c.x < bet.x && bet.x < z.x, "{c:?} {bet:?} {z:?}");
|
||||
}
|
||||
|
||||
/// A justified line spreads its letters with the same gap before a right-to-left block as before a
|
||||
/// left-to-right one: the block starts at the first of its glyphs the line draws.
|
||||
#[test]
|
||||
fn justification_opens_the_same_gap_before_a_right_to_left_block() {
|
||||
for inside in ["cd", "אב"] {
|
||||
let at = |justify: Justify| {
|
||||
let mut t = area("", style(20.0), Rect::new(0.0, 0.0, 200.0, 400.0), justify);
|
||||
t.runs = vec![TextRun::new("ab", style(20.0)), TextRun::new(inside, marked(20.0)), TextRun::new("z", style(20.0))];
|
||||
let l = layout(db(), &t);
|
||||
let x = |byte: usize| l.glyphs.iter().filter(|g| g.byte == byte).map(|g| g.origin.x).fold(f64::INFINITY, f64::min);
|
||||
let block = l.glyphs.iter().filter(|g| g.warichu.is_some()).map(|g| g.origin.x).fold(f64::INFINITY, f64::min);
|
||||
(x(0), x(1), block, x(2 + inside.len()))
|
||||
};
|
||||
let (a, b, block, z) = at(Justify::Left);
|
||||
let (a2, b2, block2, z2) = at(Justify::JustifyAll);
|
||||
let gap = (b2 - a2) - (b - a);
|
||||
assert!(gap > 1.0, "{inside}: the line is spread: {gap}");
|
||||
assert!(((block2 - b2) - (block - b) - gap).abs() < 1e-6, "{inside}: the gap before the block: {} {gap}", (block2 - b2) - (block - b));
|
||||
assert!(((z2 - block2) - (z - block) - gap).abs() < 1e-6, "{inside}: the gap after the block: {} {gap}", (z2 - block2) - (z - block));
|
||||
}
|
||||
}
|
||||
|
||||
/// The caret next to a character of a block spans the band of the character's block line, not
|
||||
/// the whole line.
|
||||
#[test]
|
||||
fn the_caret_in_a_block_spans_its_block_line() {
|
||||
let l = layout(db(), &with_block("cdef", marked(20.0)));
|
||||
let (c, e) = (&l.glyphs[2], &l.glyphs[4]);
|
||||
let middle = (c.origin.y + e.origin.y) * 0.5;
|
||||
let line = &l.lines[0];
|
||||
// "c" and "d", on the upper line.
|
||||
for byte in [2, 3] {
|
||||
let (top, bottom) = caret_position(&l, byte);
|
||||
assert!((top.y - (line.baseline - line.ascent)).abs() < 1e-6 && (bottom.y - middle).abs() < 1e-6, "{byte}: {top:?} {bottom:?} {middle}");
|
||||
}
|
||||
// "e" and "f", on the lower line.
|
||||
for byte in [4, 5] {
|
||||
let (top, bottom) = caret_position(&l, byte);
|
||||
assert!((top.y - middle).abs() < 1e-6 && (bottom.y - (line.baseline + line.descent)).abs() < 1e-6, "{byte}: {top:?} {bottom:?} {middle}");
|
||||
}
|
||||
// Outside the block the caret spans the line.
|
||||
let (top, bottom) = caret_position(&l, 0);
|
||||
assert!((top.y - (line.baseline - line.ascent)).abs() < 1e-6 && (bottom.y - (line.baseline + line.descent)).abs() < 1e-6);
|
||||
}
|
||||
|
||||
/// 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]
|
||||
|
||||
Reference in New Issue
Block a user