mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 20:36:02 +00:00
Merge pull request #7415 from dotCooCoo/fix/warichu-rtl-gap-caret
Type: justification spaces a right-to-left warichu block, and the caret keeps to its block line
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