Merge pull request #7409 from dotCooCoo/fix/rtl-tab-end-trim

Type: right-to-left tabbed lines are measured with their line-end trim where it is drawn
This commit is contained in:
BFlat
2026-11-15 03:20:46 +07:00
committed by GitHub
3 changed files with 46 additions and 8 deletions
+9 -4
View File
@@ -243,11 +243,16 @@ pub(super) fn drawn_after_tab<'g>(sg: &'g [SGlyph], drawn: &'g [usize], k: usize
/// them) set on a line whose content starts `x_start` from the tab origin: tabs of the text take
/// what [`tab_advance`] gives them from where the glyphs drawn before them end, aligning the glyphs
/// drawn after them, not the width of their glyph. The tab of a list marker (length 0) keeps its
/// own width.
pub(super) fn drawn_width(sg: &[SGlyph], drawn: &[usize], tabs: &[vectorcraft_doc::TabStop], x_start: f64) -> f64 {
/// own width. With `end_trim` (a glyph index and an amount), that glyph is set that much narrower
/// where it is drawn (a line's end trim), before the tabs drawn after it take their stops.
pub(super) fn drawn_width(sg: &[SGlyph], drawn: &[usize], tabs: &[vectorcraft_doc::TabStop], x_start: f64, end_trim: Option<(usize, f64)>) -> f64 {
let mut x = x_start;
for (k, g) in drawn.iter().enumerate().filter_map(|(k, &j)| sg.get(j).map(|g| (k, g))) {
x += if g.ch == '\t' && g.len > 0 { tab_advance(tabs, 0.0, x, drawn_after_tab(sg, drawn, k)).0 } else { g.adv };
for (k, j, g) in drawn.iter().enumerate().filter_map(|(k, &j)| sg.get(j).map(|g| (k, j, g))) {
x += if g.ch == '\t' && g.len > 0 {
tab_advance(tabs, 0.0, x, drawn_after_tab(sg, drawn, k)).0
} else {
g.adv - end_trim.filter(|&(at, _)| at == j).map_or(0.0, |(_, trim)| trim)
};
}
x - x_start
}
+7 -4
View File
@@ -459,8 +459,9 @@ fn place_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, context: &mut PlacementC
.filter(|&j| j < trimmed)
.collect();
let hyphen = if hyphenated { end.checked_sub(1).and_then(|k| glyphs.get(k)).map_or(0.0, |g| hyphen_glyph(g).adv) } else { 0.0 };
let credit = trimmed.checked_sub(1).filter(|&k| k >= start).and_then(|k| line_end.get(k)).copied().unwrap_or(0.0);
drawn_width(glyphs, &drawn, &para.tabs, ind_l) + hyphen - credit <= width + EPS
// The last glyph's end trim comes off where it is drawn, before the tabs after it.
let credit = trimmed.checked_sub(1).filter(|&k| k >= start).and_then(|k| line_end.get(k).map(|&trim| (k, trim)));
drawn_width(glyphs, &drawn, &para.tabs, ind_l, credit) + hyphen <= width + EPS
};
let fits: Option<&dyn Fn(usize, bool) -> bool> = (!cx.vertical && bidi.is_some_and(|b| b.has_rtl())).then_some(&drawn_fits);
let line = |ty| {
@@ -710,9 +711,11 @@ fn prepare_line_geometry(cx: &Ctx<'_>, t: &TextObject, paragraph: &ParagraphData
let line_end = sg.get(position.end).map_or(paragraph.pr.end, |g| g.byte);
let order = visual_order(paragraph.bidi.as_ref().filter(|_| !cx.vertical), paragraph.pr.start, line_start..line_end, &sg[index..position.end]);
// The line is as wide as it is drawn: its glyphs before its trailing spaces in drawing order,
// each tab up to its stop from where the glyphs drawn before it end.
// each tab up to its stop from where the glyphs drawn before it end, with the last glyph's end
// trim taken off where it is drawn.
let measured: Vec<usize> = order.iter().map(|&offset| index + offset).filter(|&j| j < trimmed).collect();
let measured_width = drawn_width(sg, &measured, &para.tabs, position.ind_l) + hyphen.as_ref().map_or(0.0, |g| g.adv) - end_trim;
let trim_at = trimmed.checked_sub(1).filter(|&k| k >= index).map(|k| (k, end_trim));
let measured_width = drawn_width(sg, &measured, &para.tabs, position.ind_l, trim_at) + hyphen.as_ref().map_or(0.0, |g| g.adv);
// The glyphs before `stop` in visual order, each with the advance it is set with. A list
// marker does not hang: it is counted with no width.
let visible = |stop: usize| -> (Vec<usize>, Vec<(&SGlyph, f64)>) {
+30
View File
@@ -668,6 +668,36 @@ fn a_right_to_left_line_with_a_tab_and_many_breaks_wraps_when_it_does_not_fit_as
}
}
/// A right-to-left line with a tab whose last character loses its Japanese–Latin space at the
/// line's end is measured with that space taken off where the character is drawn, before the tab:
/// right aligned, it ends on the frame's right edge, and it wraps when it does not fit as drawn.
#[test]
fn a_right_to_left_tabbed_line_is_measured_without_its_line_end_space() {
use vectorcraft_doc::Mojikumi;
// א (0), ב (2), tab (4), ג (5), 漢 (7), then a Hebrew word too long for the frame: the first
// line ends after 漢, which loses the quarter em it had before the Hebrew word.
let text = format!("אב\tג漢{}", "ד".repeat(40));
let lay = |width: f64| {
let mut t = area(&text, width, Justify::Right, no_ranges());
t.para.mojikumi = Mojikumi::GyomatsuYakumonoZenkaku;
t.para.tabs = vec![TabStop { position: 150.0, align: TabAlign::Left, leader: String::new(), align_on: '.' }];
layout(db(), &t)
};
let l = lay(300.0);
assert!(l.lines.len() >= 2, "wraps: {} line(s)", l.lines.len());
assert!(l.glyphs.iter().filter(|g| g.line == 0).all(|g| g.byte < 10), "the first line ends after 漢");
let (_, right) = drawn_extent(&l, &text, 0);
assert!((right - 300.0).abs() < 1e-6, "ends on the right edge: {right}");
assert!((l.lines[0].x1 - right).abs() < 1e-6, "the line's record ends where it is drawn: {}", l.lines[0].x1);
// Drawn, that line is as wide as the stop and the text after the tab (אב). In a frame a point
// narrower, less than the quarter em, it does not fit and the first line stays in the frame.
let after_tab: f64 = l.glyphs.iter().filter(|g| g.byte < 4).map(|g| g.advance).sum();
let width = 150.0 + after_tab - 1.0;
let l = lay(width);
let (left, right) = drawn_extent(&l, &text, 0);
assert!(left >= -1e-6 && right <= width + 1e-6, "the first line stays in the frame: {left} {right} (frame {width})");
}
/// Point type has no measure to fill: a justified line with a tab keeps its natural width.
#[test]
fn justified_point_type_with_a_tab_keeps_its_width() {