Type: burasagari keeps right-to-left and tabbed mixed lines inside the frame

A right-to-left paragraph draws a comma or full stop inside its line,
but the breaker still kept an overflowing mark on the line as if it
hung, so the line was drawn past the frame. The breaker now ends such a
line at a break where it fits with the mark.

In a left-to-right line with right-to-left text and a tab, the breaker
hung an overflowing mark without measuring the line as drawn. It now
hangs the mark only when the mark is drawn at the line's right end and
the line before it fits as drawn; otherwise the line ends at the latest
legal break that fits as drawn.
This commit is contained in:
dotCooCoo
2026-11-14 18:27:58 -08:00
parent 6016d6200f
commit d43d3e84df
5 changed files with 122 additions and 4 deletions
+18 -3
View File
@@ -416,7 +416,8 @@ pub(super) fn word_width(g: &[SGlyph], i: usize, line_end: &[f64], kinsoku: Kins
/// line holds a glyph with a negative advance and has been wider than `width` in the order of its
/// text, it reads at most [`MAX_DRAWN_CHECKS_STEPPING_BACK`] legal breaks further, and a forced
/// line break right after the last of them, and ends at the latest of the breaks read that fits as
/// drawn.
/// drawn. Such a line with a tab hangs a burasagari mark only when the mark is drawn at its right
/// end and the line before the mark fits as drawn.
#[allow(clippy::too_many_arguments)]
pub(super) fn break_line(
text: &str,
@@ -543,8 +544,22 @@ pub(super) fn break_line(
let measured = if tabbed && fits.is_some() { (ink + cluster_ink).max(stops_reached) } else { x + cluster_width };
overflowed |= tabbed && fits.is_some() && j > first && !gl.is_space() && x + cluster_width - end_credit > width + EPS;
if j > first && !gl.is_space() && measured - end_credit > width + EPS {
// It ends the line with the spaces after it.
if burasagari != Burasagari::None && hangs(gl) && kinsoku_allows(g, cluster_end - 1, kinsoku) {
// It ends the line with the spaces after it. Once the line holds a tab and is measured as
// drawn, the mark hangs only where it is drawn at the line's right end (it and the rest of
// its cluster are set left to right at the paragraph's level) and the line before it fits
// as drawn with no line-end trim on its last glyph; otherwise the line ends at a legal
// break that fits as drawn.
let hangs_as_drawn = || match fits.filter(|_| tabbed) {
None => true,
Some(fits) => {
let mut body = j;
while body > i && g[body - 1].is_space() {
body -= 1;
}
g[j..cluster_end].iter().all(|h| h.level.number() == 0) && line_end_credit(g, line_end, body) == 0.0 && fits(j, false)
}
};
if burasagari != Burasagari::None && hangs(gl) && kinsoku_allows(g, cluster_end - 1, kinsoku) && hangs_as_drawn() {
let mut end = cluster_end;
while g.get(end).is_some_and(SGlyph::is_space) {
end += 1;
+3 -1
View File
@@ -474,7 +474,9 @@ fn place_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, context: &mut PlacementC
&para.tabs,
ind_l,
hyphenate,
para.burasagari,
// A right-to-left paragraph draws a burasagari mark inside its line
// ([`prepare_line_geometry`]): the line ends where it fits with the mark.
if *rtl { Burasagari::None } else { para.burasagari },
para.kinsoku,
ty,
*hanging,
+65
View File
@@ -477,6 +477,71 @@ fn a_tabbed_line_ends_at_the_latest_break_that_fits_as_drawn() {
assert_eq!(ended, (8, false));
}
/// A tabbed line measured as drawn hangs a burasagari full stop that overflows only when the stop
/// is drawn at the line's right end and the line before it fits as drawn with no line-end trim on
/// its last glyph; otherwise it ends at the latest legal break that fits as drawn.
#[test]
fn a_tabbed_burasagari_mark_hangs_only_at_the_drawn_end_of_a_fitting_line() {
let text = "a \t\u{05D0} 漢。";
let specs: Vec<_> = text.char_indices().map(|(byte, ch)| (byte, ch.len_utf8(), 10.0, ch)).collect();
let mut glyphs = synthetic_glyphs(&specs);
glyphs[3].level = unicode_bidi::Level::rtl();
// The text without its tab is 60 pt wide at 。 (glyph 6), past 55 pt; kinsoku keeps 。 off a line
// start, so the latest legal break before it ends the line before 漢 (glyph 5).
let end = |glyphs: &[SGlyph], line_end: &[f64], fits: Option<&dyn Fn(usize, bool) -> bool>| {
break_line(
text,
glyphs,
0,
55.0,
&[],
0.0,
None,
Burasagari::Standard,
Kinsoku::Hard,
KinsokuShoriType::PushOutOnly,
false,
false,
line_end,
fits,
)
};
let all_fit = |_end: usize, _hyphenated: bool| true;
assert_eq!(end(&glyphs, &[], Some(&all_fit)), (7, false), "drawn at the right end of a line that fits: 。 hangs");
let body_too_wide = |end: usize, _hyphenated: bool| end <= 5;
assert_eq!(end(&glyphs, &[], Some(&body_too_wide)), (5, false), "the line before 。 is too wide as drawn");
let mut trimmed = vec![0.0; glyphs.len()];
trimmed[5] = 2.0;
assert_eq!(end(&glyphs, &trimmed, Some(&all_fit)), (5, false), "the line before 。 fits only with 漢 trimmed");
glyphs[6].level = unicode_bidi::Level::rtl();
assert_eq!(end(&glyphs, &[], Some(&all_fit)), (5, false), "。 set right to left is drawn inside the line");
}
/// A line with a tab and right-to-left text pushes a closing bracket out with every Kinsoku Shori
/// Type, where the same widths without them push it in.
#[test]
fn push_in_leaves_a_tabbed_right_to_left_line_to_push_out() {
use vectorcraft_doc::{TabAlign, TabStop};
let tabbed = "\u{05D0}\t一二三四五」六";
let mut glyphs = full_width(tabbed);
glyphs[0].level = unicode_bidi::Level::rtl();
glyphs[1].adv = 2.0;
let tabs = [TabStop { position: 12.0, align: TabAlign::Left, leader: String::new(), align_on: '.' }];
let all_fit = |_end: usize, _hyphenated: bool| true;
let fits: Option<&dyn Fn(usize, bool) -> bool> = Some(&all_fit);
// 」 (glyph 7) ends 72 pt along, 5 pt past the measure: five gaps close by 1 pt each.
let end = |ty| break_line(tabbed, &glyphs, 0, 67.0, &tabs, 0.0, None, Burasagari::None, Kinsoku::Hard, ty, false, false, &[], fits);
for ty in [KinsokuShoriType::PushInFirst, KinsokuShoriType::PushOutFirst, KinsokuShoriType::PushOutOnly] {
assert_eq!(end(ty), (6, false), "{ty:?}: 五 is pushed out with 」");
}
let plain = "ax一二三四五」六";
let mut glyphs = full_width(plain);
glyphs[1].adv = 2.0;
let pushed =
break_line(plain, &glyphs, 0, 67.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, KinsokuShoriType::PushInFirst, false, false, &[], None);
assert_eq!(pushed, (8, false), "without the tab and right-to-left text, 」 is pushed in");
}
/// Past the breaks a tabbed line measures one by one, it ends at the latest break without a hyphen
/// that fits as drawn, whatever the breaks with a hyphen between them measure.
#[test]
+35
View File
@@ -293,3 +293,38 @@ fn burasagari_counts_the_hung_start() {
assert!((quote.origin.x + quote.advance).abs() < EPS, "the quote hangs: {:?}", quote.origin);
assert!((comma.origin.x + comma.advance - 100.0).abs() < EPS, "、 inside, on the edge: {:?} {}", comma.origin, comma.advance);
}
/// A right-to-left paragraph draws a burasagari full stop inside its line, so the line breaks where
/// it fits with the stop: in a frame too narrow for the whole line by half the stop, the line ends
/// after the first word and nothing is drawn past the frame.
#[test]
fn right_to_left_burasagari_keeps_the_line_inside_the_frame() {
use vectorcraft_doc::{Burasagari, Kinsoku, ParaDirection};
const EPS: f64 = 1e-6;
let text = "\u{05D0}\u{05D1} \u{05D2}\u{05D3}\u{3002}";
let lay = |width: f64| {
let mut t = area(text, 20.0, Rect::new(0.0, 0.0, width, 400.0), Justify::Auto);
t.para.direction = Some(ParaDirection::RightToLeft);
t.para.burasagari = Burasagari::Standard;
t.para.kinsoku = Kinsoku::Hard;
let l = layout(db(), &t);
(t, l)
};
let (_, wide) = lay(1000.0);
assert_eq!(wide.lines.len(), 1);
let full: f64 = wide.glyphs.iter().map(|g| g.advance).sum();
let Some(stop) = wide.glyphs.iter().find(|g| g.byte == text.len() - '\u{3002}'.len_utf8()) else {
panic!("。 is set as a glyph of its own");
};
let (t, l) = lay(full - stop.advance / 2.0);
assert!(!l.overflow);
assert_eq!(l.lines.len(), 2, "the line ends after the first word");
for (li, gs) in inked(&t, &l) {
// A right-to-left line's trailing space is set at its left, outside the aligned text.
for g in gs.into_iter().filter(|g| !is_space(ch(&t, g))) {
let c = ch(&t, g);
assert!(g.origin.x >= li.avail.0 - EPS, "{c:?} starts at {} before {}", g.origin.x, li.avail.0);
assert!(g.origin.x + g.advance <= li.avail.1 + EPS, "{c:?} ends at {} past {}", g.origin.x + g.advance, li.avail.1);
}
}
}
File diff suppressed because one or more lines are too long