diff --git a/crates/text/src/layout/flow.rs b/crates/text/src/layout/flow.rs index 1ee9a3fe50..e7753c8c04 100644 --- a/crates/text/src/layout/flow.rs +++ b/crates/text/src/layout/flow.rs @@ -544,6 +544,9 @@ struct LineJustification { fit: LineFit, start_x: f64, after: Vec>, + /// The warichu glyphs drawn inside a block already started, by offset from the line's first + /// glyph ([`blocks_begun`]). + begun: Vec, 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], 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], 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], 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 { + 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 = 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], 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], &[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], 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], 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], &[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; } } diff --git a/crates/text/src/layout/wrapping_tests.rs b/crates/text/src/layout/wrapping_tests.rs index eae22bab8a..3a83a5673b 100644 --- a/crates/text/src/layout/wrapping_tests.rs +++ b/crates/text/src/layout/wrapping_tests.rs @@ -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}"); } } diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index 6ae85931ca..362a1c2408 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -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 = 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) }; diff --git a/crates/text/src/tests/warichu.rs b/crates/text/src/tests/warichu.rs index d68cb8fccc..6aee052cbd 100644 --- a/crates/text/src/tests/warichu.rs +++ b/crates/text/src/tests/warichu.rs @@ -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]