mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 21:57:04 +00:00
Merge pull request #7810 from dotCooCoo/fix/burasagari-and-push-in-on-a-tabbed-right-to-left
Type: a burasagari mark hangs only where it is drawn at the line's right end
This commit is contained in:
@@ -395,8 +395,29 @@ pub(super) fn word_width(g: &[SGlyph], i: usize, line_end: &[f64], kinsoku: Kins
|
||||
if end >= g.len() { set(w, trailing, ink) } else { w }
|
||||
}
|
||||
|
||||
/// Whether the cluster `j..end` is drawn last, at the right end, of the left-to-right line of
|
||||
/// glyphs `i..end`: the line's glyphs are reordered by their levels (UAX #9 L2), with each tab and
|
||||
/// the spaces before it at the paragraph's level (L1).
|
||||
fn drawn_last(g: &[SGlyph], i: usize, j: usize, end: usize) -> bool {
|
||||
let line = g.get(i..end).unwrap_or_default();
|
||||
let mut levels: Vec<unicode_bidi::Level> = line.iter().map(|h| h.level).collect();
|
||||
let mut before_tab = false;
|
||||
for (level, h) in levels.iter_mut().zip(line).rev() {
|
||||
if h.ch == '\t' && h.len > 0 {
|
||||
before_tab = true;
|
||||
} else if !h.is_space() {
|
||||
before_tab = false;
|
||||
}
|
||||
if before_tab {
|
||||
*level = unicode_bidi::Level::ltr();
|
||||
}
|
||||
}
|
||||
unicode_bidi::BidiInfo::reorder_visual(&levels).last().is_some_and(|&k| i + k >= j)
|
||||
}
|
||||
|
||||
/// Greedy break: returns (end glyph index, hyphenated) for a line starting at `i` of `width`.
|
||||
/// With burasagari, a comma or full stop that doesn't fit ends the line, hanging outside it.
|
||||
/// With burasagari, a comma or full stop that doesn't fit ends the line, hanging outside it,
|
||||
/// unless No Break or Bunri-Kinshi keeps characters after it on its line.
|
||||
/// `hyphenate` holds the paragraph's Hyphenation options and the Hyphenation Zone in force when
|
||||
/// the line may hyphenate a word; a line whose text before the overflowing word already ends
|
||||
/// within the zone breaks before that word. A tab is as wide as it is set: up to its next stop
|
||||
@@ -416,7 +437,9 @@ 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 in the visual order of the line's levels ([`drawn_last`]) and the line before the mark fits
|
||||
/// as drawn.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn break_line(
|
||||
text: &str,
|
||||
@@ -543,8 +566,24 @@ 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, when the mark is all the characters kept
|
||||
// together (the line's last character is the mark, which is what hangs). 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 (its cluster is last in the line's visual order, [`drawn_last`]) 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 alone = g[j..cluster_end].iter().all(|h| h.byte == gl.byte);
|
||||
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;
|
||||
}
|
||||
drawn_last(g, i, j, cluster_end) && line_end_credit(g, line_end, body) == 0.0 && fits(j, false)
|
||||
}
|
||||
};
|
||||
if burasagari != Burasagari::None && hangs(gl) && alone && 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;
|
||||
|
||||
@@ -474,7 +474,9 @@ fn place_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, context: &mut PlacementC
|
||||
¶.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,
|
||||
|
||||
@@ -477,6 +477,95 @@ 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 (last in the visual order of the line's levels, whatever its
|
||||
/// own level) 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");
|
||||
let level = |n: u8| unicode_bidi::Level::new(n).unwrap_or_else(|_| panic!("level {n}"));
|
||||
glyphs[6].level = level(2);
|
||||
assert_eq!(end(&glyphs, &[], Some(&all_fit)), (7, false), "。 set left to right in right-to-left text is drawn at the right end");
|
||||
glyphs[6].level = level(1);
|
||||
assert_eq!(end(&glyphs, &[], Some(&all_fit)), (7, false), "。 alone right to left after 漢 is drawn at the right end");
|
||||
glyphs[5].level = level(1);
|
||||
assert_eq!(end(&glyphs, &[], Some(&all_fit)), (5, false), "。 right to left after 漢 in the same run is drawn inside the line");
|
||||
glyphs[6].level = level(2);
|
||||
assert_eq!(end(&glyphs, &[], Some(&all_fit)), (5, false), "。 left to right inside the right-to-left run of 漢 is drawn inside the line");
|
||||
}
|
||||
|
||||
/// A burasagari full stop that No Break keeps with the character after it does not hang: the line
|
||||
/// would end with that character, which is drawn inside the line, so it ends at an earlier break.
|
||||
#[test]
|
||||
fn a_burasagari_mark_kept_with_the_next_character_does_not_hang() {
|
||||
let text = "漢漢。漢";
|
||||
let specs: Vec<_> = text.char_indices().map(|(byte, ch)| (byte, ch.len_utf8(), 10.0, ch)).collect();
|
||||
let mut glyphs = synthetic_glyphs(&specs);
|
||||
let end = |glyphs: &[SGlyph]| {
|
||||
break_line(text, glyphs, 0, 25.0, &[], 0.0, None, Burasagari::Standard, Kinsoku::Hard, KinsokuShoriType::PushOutOnly, false, false, &[], None)
|
||||
};
|
||||
assert_eq!(end(&glyphs), (3, false), "alone, 。 overflows by 5 pt and hangs");
|
||||
glyphs[2].no_break = true;
|
||||
glyphs[3].no_break = true;
|
||||
assert_eq!(end(&glyphs), (1, false), "kept with the 漢 after it, 。 does not end 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]
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user