mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 14:46:03 +00:00
Type: Mojikumi spacing at line ends counts when lines break
With Line-end Punctuation Half Width, a closing bracket, comma or full stop ending a line is set half width, but both composers measured it at full width. A closing bracket that fit only at half width went to the next line with the character before it, and so did a comma or full stop with Burasagari None. The single-line and every-line composers now measure each candidate line with its last character set as narrow as Mojikumi sets it, using the same rule the line is drawn with. A mark that already lost its half to the punctuation after it and a line ending in a hyphen take nothing more off, and the quarter em between Japanese and Latin text counts only where the line ends. Kinsoku Shori Type measures the pushed-in and pushed-out lines the same way. Right-to-left lines and Mojikumi None break as before. A test font sets ideographs and Japanese punctuation one em wide, so the layout tests run without system fonts.
This commit is contained in:
@@ -116,7 +116,9 @@ fn adjustment_ratio(natural: f64, width: f64, stretch: f64, shrink: f64, last: b
|
||||
|
||||
/// Compose the paragraph `g` into lines. `width(k)` is the available width of line `k`;
|
||||
/// `start_credit(i)` is the width a wrapped line starting at glyph `i` gives back (Mojikumi sets
|
||||
/// an opening bracket there flush with the line's start); `cands` are the break opportunities
|
||||
/// an opening bracket there flush with the line's start); `end_credit(t)` is the width a line whose
|
||||
/// last character is glyph `t - 1` gives back (Mojikumi sets a closing mark there half width), at
|
||||
/// most that glyph's advance, and nothing at a hyphenation point; `cands` are the break opportunities
|
||||
/// in increasing order (the paragraph end is added); `flex` holds how far each glyph may stretch
|
||||
/// and shrink in a justified line.
|
||||
/// Returns `(end, hyphen)` per line, or `None` if no set of breaks fits within the tolerance
|
||||
@@ -125,6 +127,7 @@ pub(crate) fn compose(
|
||||
g: &[SGlyph],
|
||||
width: &dyn Fn(usize) -> f64,
|
||||
start_credit: &dyn Fn(usize) -> f64,
|
||||
end_credit: &dyn Fn(usize) -> f64,
|
||||
cands: &[Breakpoint],
|
||||
flex: &[Flex],
|
||||
p: &Params,
|
||||
@@ -153,6 +156,11 @@ pub(crate) fn compose(
|
||||
}
|
||||
b
|
||||
};
|
||||
let line_end_credit = |t: usize| {
|
||||
let credit = end_credit(t);
|
||||
let most = t.checked_sub(1).and_then(|k| g.get(k)).map_or(0.0, |gl| gl.adv.max(0.0));
|
||||
if credit.is_finite() { credit.clamp(0.0, most) } else { 0.0 }
|
||||
};
|
||||
let mut all: Vec<Breakpoint> = cands.iter().copied().filter(|c| c.end > 0 && c.end < n).collect();
|
||||
all.push(Breakpoint { end: n, hyphen: 0.0 });
|
||||
let (adj_demerits, hyphen_penalty) =
|
||||
@@ -170,7 +178,10 @@ pub(crate) fn compose(
|
||||
let Some(a) = nodes.get(ai) else { continue };
|
||||
let t = trim(bp.end, a.pos);
|
||||
let credit = if a.pos > 0 { start_credit(a.pos) } else { 0.0 };
|
||||
let natural = w[t] - w[a.pos] - credit + bp.hyphen;
|
||||
// A line that ends with a hyphen ends with it, not with its last character. At most the
|
||||
// last glyph's advance, so a longer line from the same node is never narrower.
|
||||
let end = if hyphen || t <= a.pos { 0.0 } else { line_end_credit(t) };
|
||||
let natural = w[t] - w[a.pos] - credit - end + bp.hyphen;
|
||||
let lw = width(a.line);
|
||||
// Adjustment ratio, badness and whether the line ends the active node's reach.
|
||||
let Some(ratio) = adjustment_ratio(natural, lw, st[t] - st[a.pos], sh[t] - sh[a.pos], last, p) else { continue };
|
||||
|
||||
@@ -274,6 +274,8 @@ pub(super) fn word_width(g: &[SGlyph], i: usize) -> f64 {
|
||||
/// keeps the line from breaking before the character that overflows, `kinsoku_type` decides
|
||||
/// whether the line pushes characters in ([`push_in`]) or out; `hanging` is true when Optical
|
||||
/// Margin Alignment or Roman Hanging Punctuation is on, and `vertical` in vertical type.
|
||||
/// `line_end[k]` is how much narrower a line ending with glyph `k` is set (Mojikumi); a cluster
|
||||
/// that fits only that narrower stays on the line when the line may end after it (empty: none).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn break_line(
|
||||
text: &str,
|
||||
@@ -288,6 +290,7 @@ pub(super) fn break_line(
|
||||
kinsoku_type: KinsokuShoriType,
|
||||
hanging: bool,
|
||||
vertical: bool,
|
||||
line_end: &[f64],
|
||||
) -> (usize, bool) {
|
||||
if !width.is_finite() {
|
||||
if let Some(pos) = g[i..].iter().position(|gl| gl.ch == '\u{2028}') {
|
||||
@@ -329,7 +332,11 @@ pub(super) fn break_line(
|
||||
if gl.ch == '\u{2028}' {
|
||||
return (cluster_end, false);
|
||||
}
|
||||
if j > first && !gl.is_space() && x + cluster_width > width + EPS {
|
||||
// Measured as the line is set when it ends with this cluster: only the cluster's last glyph
|
||||
// is narrowed, and only where the line may end.
|
||||
let end_credit =
|
||||
Some(line_end_credit(g, line_end, cluster_end)).filter(|&c| c > 0.0 && may_end_after(g, cluster_end, kinsoku)).unwrap_or(0.0);
|
||||
if j > first && !gl.is_space() && x + cluster_width - 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) {
|
||||
let mut end = cluster_end;
|
||||
@@ -338,8 +345,8 @@ pub(super) fn break_line(
|
||||
}
|
||||
return (end, false);
|
||||
}
|
||||
if let Some(end) =
|
||||
push_in(g, i, j, cluster_end, x + cluster_width, width, last_break.map(|b| b.0), kinsoku, kinsoku_type, hanging, vertical)
|
||||
let pushed_out = last_break.map(|b| b.0);
|
||||
if let Some(end) = push_in(g, i, j, cluster_end, x + cluster_width, width, pushed_out, kinsoku, kinsoku_type, hanging, vertical, line_end)
|
||||
{
|
||||
return (end, false);
|
||||
}
|
||||
@@ -482,6 +489,30 @@ pub(super) fn optical_hangs_rtl(line: &[(&SGlyph, f64)], share: fn(&SGlyph) -> f
|
||||
(left, right, left_glyphs, right_glyphs)
|
||||
}
|
||||
|
||||
/// How much narrower a line whose last character is glyph `end - 1` is set: `line_end` of that
|
||||
/// glyph ([`break_line`]), at most its advance, and 0 when it is not finite.
|
||||
fn line_end_credit(g: &[SGlyph], line_end: &[f64], end: usize) -> f64 {
|
||||
let Some(k) = end.checked_sub(1) else { return 0.0 };
|
||||
match (line_end.get(k), g.get(k)) {
|
||||
(Some(&credit), Some(gl)) if credit.is_finite() && credit > 0.0 => credit.min(gl.adv.max(0.0)),
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// May [`break_line`] end a line after glyph `end - 1` (and the spaces after it)? At the paragraph's
|
||||
/// end or a forced line break, and where it records a break without a hyphen.
|
||||
fn may_end_after(g: &[SGlyph], end: usize, kinsoku: Kinsoku) -> bool {
|
||||
let mut e = end;
|
||||
while g.get(e).is_some_and(SGlyph::is_space) {
|
||||
e += 1;
|
||||
}
|
||||
if e >= g.len() || g[e].ch == '\u{2028}' {
|
||||
return true;
|
||||
}
|
||||
let Some(last) = e.checked_sub(1).and_then(|k| g.get(k)) else { return false };
|
||||
last.break_after() && last.len > 0 && !last.is_soft_hyphen() && kinsoku_allows(g, e - 1, kinsoku) && !splits_no_break(g, e)
|
||||
}
|
||||
|
||||
/// Would a line ending at glyph index `end` split a No Break run?
|
||||
fn splits_no_break(g: &[SGlyph], end: usize) -> bool {
|
||||
end > 0 && g.get(end - 1).is_some_and(|a| a.no_break) && g.get(end).is_some_and(|b| b.no_break)
|
||||
@@ -526,7 +557,8 @@ const PUSH_IN_RUN: usize = 16;
|
||||
/// neither does a line holding a tab or right-to-left text, nor one where the characters taken in
|
||||
/// reach a space after a character that may not end a line. With `hanging` (Optical Margin
|
||||
/// Alignment or Roman Hanging Punctuation), the gaps that hanging punctuation keeps open are not
|
||||
/// counted. `vertical`: the line is a column of vertical type ([`squeeze_em`]).
|
||||
/// counted. `vertical`: the line is a column of vertical type ([`squeeze_em`]). Both lines are
|
||||
/// measured with their last character set as narrow as `line_end` sets it ([`break_line`]).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn push_in(
|
||||
g: &[SGlyph],
|
||||
@@ -540,6 +572,7 @@ fn push_in(
|
||||
kinsoku_type: KinsokuShoriType,
|
||||
hanging: bool,
|
||||
vertical: bool,
|
||||
line_end: &[f64],
|
||||
) -> Option<usize> {
|
||||
if kinsoku == Kinsoku::None || kinsoku_type == KinsokuShoriType::PushOutOnly {
|
||||
return None;
|
||||
@@ -591,7 +624,7 @@ fn push_in(
|
||||
|| (a + 3 == last && punct(last - 2) && quote(last - 1)))
|
||||
};
|
||||
let room: f64 = (i..end - 1).filter(|&a| closes(g, a) && !may_hang(a)).map(|a| PUSH_IN_SQUEEZE * squeeze_em(&g[a], vertical)).sum();
|
||||
if x - width > room + EPS {
|
||||
if x - line_end_credit(g, line_end, last) - width > room + EPS {
|
||||
return None;
|
||||
}
|
||||
if kinsoku_type == KinsokuShoriType::PushOutFirst {
|
||||
@@ -599,7 +632,8 @@ fn push_in(
|
||||
while kept > i && g[kept - 1].is_space() {
|
||||
kept -= 1;
|
||||
}
|
||||
let short = width - g[i..kept].iter().map(|h| h.adv).sum::<f64>();
|
||||
let set = if kept > i { line_end_credit(g, line_end, kept) } else { 0.0 };
|
||||
let short = width - (g[i..kept].iter().map(|h| h.adv).sum::<f64>() - set);
|
||||
if short <= squeeze_em(before, vertical) + EPS {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -23,11 +23,14 @@ const MAX_COMPOSE_GLYPHS: usize = 200_000;
|
||||
/// Every-line composition of paragraph glyphs `sg` if applicable (area text, justified or ragged,
|
||||
/// with uniform line metrics or in rectangular cells without text wrap); `None` falls back to the
|
||||
/// greedy single-line composer. With `continued`, the paragraph continues from the frame before,
|
||||
/// and its first composed line is not its first line.
|
||||
/// and its first composed line is not its first line. `line_end` is how much narrower a line
|
||||
/// ending with each glyph is set ([`ParagraphData::line_end`]).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn compose_para(
|
||||
cx: &Ctx<'_>,
|
||||
sg: &[SGlyph],
|
||||
flex: &[Flex],
|
||||
line_end: &[f64],
|
||||
para: &ParaStyle,
|
||||
pen: &Pen<'_>,
|
||||
rtl: bool,
|
||||
@@ -109,7 +112,10 @@ fn compose_para(
|
||||
let flush = !rtl && para.mojikumi == Mojikumi::LineEndHalf;
|
||||
sg.get(i).filter(|g| flush && g.lead <= 0.0).and_then(|g| punct_half(g, Punct::Opening)).unwrap_or(0.0)
|
||||
};
|
||||
compose(sg, &width, &start_credit, &cands, flex, ¶ms(strict)).or_else(|| compose(sg, &width, &start_credit, &cands, flex, ¶ms(loose)))
|
||||
// …and its last character is set narrower, as the line loop sets it.
|
||||
let end_credit = |end: usize| end.checked_sub(1).and_then(|k| line_end.get(k)).copied().unwrap_or(0.0);
|
||||
let pass = |p: &Params| compose(sg, &width, &start_credit, &end_credit, &cands, flex, p);
|
||||
pass(¶ms(strict)).or_else(|| pass(¶ms(loose)))
|
||||
}
|
||||
|
||||
/// The list marker of each of the paragraphs `paras` of `t` (None for none): bullets, and numbers
|
||||
@@ -183,6 +189,10 @@ struct ParagraphData<'a, 'text> {
|
||||
sg: Vec<SGlyph>,
|
||||
compressed: Vec<bool>,
|
||||
wakan: Vec<f64>,
|
||||
/// How much narrower a line ending with each glyph is set ([`line_end_trim`]), for measuring
|
||||
/// where lines break; empty when the paragraph's lines are measured at full width (Mojikumi
|
||||
/// None, or right-to-left text).
|
||||
line_end: Vec<f64>,
|
||||
flex: Vec<Flex>,
|
||||
marker: usize,
|
||||
pm: Metrics,
|
||||
@@ -253,7 +263,13 @@ fn prepare_paragraph<'a, 'text>(
|
||||
if pi > 0 {
|
||||
pen.pending += para.space_before;
|
||||
}
|
||||
let composed = compose_para(cx, &sg, &flex, para, pen, rtl, continued);
|
||||
// A line fits when it fits as it is set: with its last character narrowed by Mojikumi.
|
||||
let line_end = if para.mojikumi == Mojikumi::LineEndHalf && !rtl {
|
||||
(0..sg.len()).map(|k| line_end_trim(&sg, &compressed, &wakan, k)).collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let composed = compose_para(cx, &sg, &flex, &line_end, para, pen, rtl, continued);
|
||||
// Hyphenation options: the Hyphenation Zone applies with the Single-line Composer, and the
|
||||
// Hyphen Limit counts the lines in a row that end with a hyphen.
|
||||
let zone = if cx.opts.composer.unwrap_or(para.composer) == Composer::SingleLine { para.hyphenation.zone } else { 0.0 };
|
||||
@@ -269,6 +285,7 @@ fn prepare_paragraph<'a, 'text>(
|
||||
sg,
|
||||
compressed,
|
||||
wakan,
|
||||
line_end,
|
||||
flex,
|
||||
marker,
|
||||
pm,
|
||||
@@ -284,6 +301,8 @@ struct PlacementContext<'a> {
|
||||
para: &'a ParaStyle,
|
||||
pr: &'a Range<usize>,
|
||||
sg: &'a mut [SGlyph],
|
||||
/// [`ParagraphData::line_end`].
|
||||
line_end: &'a [f64],
|
||||
continued: bool,
|
||||
rtl: bool,
|
||||
mirror: bool,
|
||||
@@ -312,8 +331,23 @@ struct LinePosition {
|
||||
}
|
||||
|
||||
fn place_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, context: &mut PlacementContext<'_>) -> Option<LinePosition> {
|
||||
let PlacementContext { para, pr, sg, continued, rtl, mirror, pm, composed, hyphen_run, hyphen_limit, zone, index: i, para_line, hanging } =
|
||||
context;
|
||||
let PlacementContext {
|
||||
para,
|
||||
pr,
|
||||
sg,
|
||||
line_end,
|
||||
continued,
|
||||
rtl,
|
||||
mirror,
|
||||
pm,
|
||||
composed,
|
||||
hyphen_run,
|
||||
hyphen_limit,
|
||||
zone,
|
||||
index: i,
|
||||
para_line,
|
||||
hanging,
|
||||
} = context;
|
||||
let n = sg.len();
|
||||
let est = if *i < n { Metrics::of(&sg[*i]) } else { *pm };
|
||||
let first_line = *para_line == 0 && !*continued;
|
||||
@@ -345,8 +379,23 @@ fn place_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, context: &mut PlacementC
|
||||
let (end, hyph, pushed_in) = match composed.as_ref().and_then(|lines| lines.get(*para_line)) {
|
||||
Some(&(end, hyph)) if end > *i => (end, hyph, false),
|
||||
_ if *i < n => {
|
||||
let line =
|
||||
|ty| break_line(cx.text, sg, *i, width, ¶.tabs, ind_l, hyphenate, para.burasagari, para.kinsoku, ty, *hanging, cx.vertical);
|
||||
let line = |ty| {
|
||||
break_line(
|
||||
cx.text,
|
||||
sg,
|
||||
*i,
|
||||
width,
|
||||
¶.tabs,
|
||||
ind_l,
|
||||
hyphenate,
|
||||
para.burasagari,
|
||||
para.kinsoku,
|
||||
ty,
|
||||
*hanging,
|
||||
cx.vertical,
|
||||
line_end,
|
||||
)
|
||||
};
|
||||
let (end, hyph) = line(para.kinsoku_type);
|
||||
// The line pushed characters in where it ends after the line that pushes out.
|
||||
let pushed_in = pushes_in(para) && end > line(KinsokuShoriType::PushOutOnly).0;
|
||||
@@ -439,6 +488,7 @@ fn run_paragraph_lines(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, t: &TextObject, mut
|
||||
para: paragraph.para,
|
||||
pr: ¶graph.pr,
|
||||
sg: &mut paragraph.sg,
|
||||
line_end: ¶graph.line_end,
|
||||
continued: paragraph.continued,
|
||||
rtl: paragraph.rtl,
|
||||
mirror: paragraph.mirror,
|
||||
@@ -473,6 +523,18 @@ fn run_paragraph_lines(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, t: &TextObject, mut
|
||||
true
|
||||
}
|
||||
|
||||
/// Mojikumi (Line-end Punctuation Half Width): how much narrower a line ending with glyph `k` of
|
||||
/// `sg` is set. A closing bracket, comma or full stop ending the line is set half width, unless it
|
||||
/// already lost that half to the punctuation after it (`compressed`), and no Japanese–Latin space
|
||||
/// (`wakan`) is left after the line's last character. Never more than the glyph's advance, and 0
|
||||
/// for an advance or a space that is not finite.
|
||||
pub(super) fn line_end_trim(sg: &[SGlyph], compressed: &[bool], wakan: &[f64], k: usize) -> f64 {
|
||||
let Some(g) = sg.get(k) else { return 0.0 };
|
||||
let half = if compressed.get(k).copied().unwrap_or(false) { None } else { punct_half(g, Punct::Closing) };
|
||||
let trim = half.unwrap_or(0.0) + wakan.get(k).copied().unwrap_or(0.0);
|
||||
if trim.is_finite() && g.adv.is_finite() { trim.min(g.adv).max(0.0) } else { 0.0 }
|
||||
}
|
||||
|
||||
/// The geometry of the line of `paragraph` from glyph `index`, placed at `position`: where it
|
||||
/// ends with its trailing spaces trimmed, its hyphen, its room and margins, the glyphs that hang or
|
||||
/// stay out of justification, and whether it is the paragraph's last line.
|
||||
@@ -488,12 +550,10 @@ fn prepare_line_geometry(cx: &Ctx<'_>, t: &TextObject, paragraph: &ParagraphData
|
||||
trimmed -= 1;
|
||||
}
|
||||
let hyphen = (position.hyph && position.end > index).then(|| hyphen_glyph(&sg[position.end - 1]));
|
||||
// Mojikumi: a closing bracket, comma or full stop ending the line is set half width, and
|
||||
// no Japanese–Latin space is left at the end of a line.
|
||||
let end_trim = trimmed.checked_sub(1).filter(|&k| k >= index && para.mojikumi == Mojikumi::LineEndHalf).map_or(0.0, |k| {
|
||||
let half = if paragraph.compressed.get(k).copied().unwrap_or(false) { None } else { sg.get(k).and_then(|g| punct_half(g, Punct::Closing)) };
|
||||
half.unwrap_or(0.0) + paragraph.wakan.get(k).copied().unwrap_or(0.0)
|
||||
});
|
||||
let end_trim = trimmed
|
||||
.checked_sub(1)
|
||||
.filter(|&k| k >= index && para.mojikumi == Mojikumi::LineEndHalf)
|
||||
.map_or(0.0, |k| line_end_trim(sg, ¶graph.compressed, ¶graph.wakan, k));
|
||||
let measured_width = set_width(sg, index, trimmed, ¶.tabs, position.ind_l) + hyphen.as_ref().map_or(0.0, |g| g.adv) - end_trim;
|
||||
let line_start = sg.get(index).map_or(paragraph.pr.start, |g| g.byte);
|
||||
let line_end = sg.get(position.end).map_or(paragraph.pr.end, |g| g.byte);
|
||||
|
||||
@@ -92,14 +92,15 @@ fn indic_hyphenation_reaches_both_composers_without_splitting_marks() {
|
||||
KinsokuShoriType::PushOutOnly,
|
||||
false,
|
||||
false,
|
||||
&[],
|
||||
);
|
||||
assert_eq!((end, hyphen), (4, true), "{tag}");
|
||||
let params = Params { justify_last: false, ragged: true, tolerance: 1.0, uniform_from: 0, hyphen_limit: 0 };
|
||||
let lines = compose(&glyphs, &|_| 4.5, &|_| 0.0, &cands, &[], ¶ms).expect("every-line composition");
|
||||
let lines = compose(&glyphs, &|_| 4.5, &|_| 0.0, &|_| 0.0, &cands, &[], ¶ms).expect("every-line composition");
|
||||
assert!(lines.iter().any(|(_, hyphen)| *hyphen), "{tag}");
|
||||
assert!(lines.iter().all(|(end, _)| end % 2 == 0), "{tag}: {lines:?}");
|
||||
let limited = Params { hyphen_limit: 1, ..params };
|
||||
if let Some(lines) = compose(&glyphs, &|_| 4.5, &|_| 0.0, &cands, &[], &limited) {
|
||||
if let Some(lines) = compose(&glyphs, &|_| 4.5, &|_| 0.0, &|_| 0.0, &cands, &[], &limited) {
|
||||
assert!(lines.windows(2).all(|pair| !pair[0].1 || !pair[1].1), "{tag}: {lines:?}");
|
||||
}
|
||||
}
|
||||
@@ -164,7 +165,7 @@ fn greedy_wrap_keeps_an_oversized_first_cluster_and_its_source_range() {
|
||||
let glyphs = synthetic_glyphs(&[(0, 4, 2.0, 'ب'), (0, 4, 2.0, 'ب'), (4, 2, 1.0, 'ت')]);
|
||||
|
||||
let (end, hyphenated) =
|
||||
break_line(text, &glyphs, 0, 3.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, KinsokuShoriType::PushOutOnly, false, false);
|
||||
break_line(text, &glyphs, 0, 3.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, KinsokuShoriType::PushOutOnly, false, false, &[]);
|
||||
|
||||
assert!(!hyphenated);
|
||||
assert_eq!(end, 2, "oversized base-plus-mark cluster must stay together");
|
||||
@@ -178,12 +179,12 @@ fn greedy_wrap_keeps_an_oversized_cluster_after_a_line_start_atomic() {
|
||||
let glyphs = synthetic_glyphs(&[(0, 4, 1.0, 'ب'), (4, 4, 2.0, 'ت'), (4, 4, 2.0, 'ت'), (8, 2, 1.0, 'ث')]);
|
||||
|
||||
let (first_end, _) =
|
||||
break_line(text, &glyphs, 0, 3.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, KinsokuShoriType::PushOutOnly, false, false);
|
||||
break_line(text, &glyphs, 0, 3.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, KinsokuShoriType::PushOutOnly, false, false, &[]);
|
||||
assert_eq!(first_end, 1);
|
||||
assert_eq!(source_range(&glyphs, 0, first_end), 0..4);
|
||||
|
||||
let (second_end, hyphenated) =
|
||||
break_line(text, &glyphs, first_end, 3.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, KinsokuShoriType::PushOutOnly, false, false);
|
||||
break_line(text, &glyphs, first_end, 3.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, KinsokuShoriType::PushOutOnly, false, false, &[]);
|
||||
|
||||
assert!(!hyphenated);
|
||||
assert_eq!(second_end, 3, "oversized cluster at the next line start must stay together");
|
||||
@@ -239,7 +240,7 @@ fn no_break_keeps_marked_characters_on_one_line() {
|
||||
let text = "A B";
|
||||
let plain = synthetic_glyphs(&[(0, 1, 10.0, 'A'), (1, 1, 4.0, ' '), (2, 1, 10.0, 'B')]);
|
||||
let (end, hyphen) =
|
||||
break_line(text, &plain, 0, 16.0, &[], 0.0, None, Burasagari::None, Kinsoku::None, KinsokuShoriType::PushOutOnly, false, false);
|
||||
break_line(text, &plain, 0, 16.0, &[], 0.0, None, Burasagari::None, Kinsoku::None, KinsokuShoriType::PushOutOnly, false, false, &[]);
|
||||
assert!(!hyphen);
|
||||
assert_eq!(end, 2, "without the mark the line ends after the space");
|
||||
|
||||
@@ -248,7 +249,7 @@ fn no_break_keeps_marked_characters_on_one_line() {
|
||||
g.no_break = true;
|
||||
}
|
||||
let (end, hyphen) =
|
||||
break_line(text, &marked, 0, 16.0, &[], 0.0, None, Burasagari::None, Kinsoku::None, KinsokuShoriType::PushOutOnly, false, false);
|
||||
break_line(text, &marked, 0, 16.0, &[], 0.0, None, Burasagari::None, Kinsoku::None, KinsokuShoriType::PushOutOnly, false, false, &[]);
|
||||
assert!(!hyphen);
|
||||
assert_eq!(end, 3, "the marked words stay together even though they are wider than the line");
|
||||
|
||||
@@ -257,9 +258,11 @@ fn no_break_keeps_marked_characters_on_one_line() {
|
||||
let mut glyphs = synthetic_glyphs(&[(0, 1, 8.0, 'A'), (1, 1, 2.0, ' '), (2, 3, 10.0, '日'), (5, 3, 10.0, '本')]);
|
||||
glyphs[2].no_break = true;
|
||||
glyphs[3].no_break = true;
|
||||
let (end, _) = break_line(text, &glyphs, 0, 22.0, &[], 0.0, None, Burasagari::None, Kinsoku::None, KinsokuShoriType::PushOutOnly, false, false);
|
||||
let (end, _) =
|
||||
break_line(text, &glyphs, 0, 22.0, &[], 0.0, None, Burasagari::None, Kinsoku::None, KinsokuShoriType::PushOutOnly, false, false, &[]);
|
||||
assert_eq!(end, 2, "the marked pair moves to the next line together");
|
||||
let (end, _) = break_line(text, &glyphs, end, 15.0, &[], 0.0, None, Burasagari::None, Kinsoku::None, KinsokuShoriType::PushOutOnly, false, false);
|
||||
let (end, _) =
|
||||
break_line(text, &glyphs, end, 15.0, &[], 0.0, None, Burasagari::None, Kinsoku::None, KinsokuShoriType::PushOutOnly, false, false, &[]);
|
||||
assert_eq!(end, 4, "on its own line the pair stays together even when it is wider than the line");
|
||||
|
||||
let breaks = candidates(text, &glyphs, None, Kinsoku::None);
|
||||
@@ -286,7 +289,7 @@ fn full_width(text: &str) -> Vec<SGlyph> {
|
||||
fn kinsoku_shori_type_pushes_a_closing_bracket_in_or_out() {
|
||||
use KinsokuShoriType::{PushInFirst, PushOutFirst, PushOutOnly};
|
||||
let hung = |text: &str, width: f64, kinsoku: Kinsoku, ty: KinsokuShoriType, hanging: bool| {
|
||||
break_line(text, &full_width(text), 0, width, &[], 0.0, None, Burasagari::None, kinsoku, ty, hanging, false).0
|
||||
break_line(text, &full_width(text), 0, width, &[], 0.0, None, Burasagari::None, kinsoku, ty, hanging, false, &[]).0
|
||||
};
|
||||
let end = |text: &str, width: f64, kinsoku: Kinsoku, ty: KinsokuShoriType| hung(text, width, kinsoku, ty, false);
|
||||
let text = "一二三四五六七八九十一二三四五」六";
|
||||
@@ -351,7 +354,8 @@ fn kinsoku_shori_type_counts_the_gaps_that_close() {
|
||||
let mixed = "一二三א五六七八九十一二三四五」六";
|
||||
let mut glyphs = full_width(mixed);
|
||||
glyphs[3].level = unicode_bidi::Level::rtl();
|
||||
let mixed_end = |ty: KinsokuShoriType| break_line(mixed, &glyphs, 0, 150.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, ty, false, false).0;
|
||||
let mixed_end =
|
||||
|ty: KinsokuShoriType| break_line(mixed, &glyphs, 0, 150.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, ty, false, false, &[]).0;
|
||||
assert_eq!(mixed_end(PushInFirst), 14, "right-to-left text on the line: 五 is pushed out with 」");
|
||||
assert_eq!(mixed_end(PushOutOnly), 14);
|
||||
|
||||
@@ -362,7 +366,8 @@ fn kinsoku_shori_type_counts_the_gaps_that_close() {
|
||||
for g in &mut glyphs {
|
||||
g.sy *= 2.0;
|
||||
}
|
||||
let tall_end = |ty: KinsokuShoriType| break_line(tall, &glyphs, 0, 50.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, ty, false, false).0;
|
||||
let tall_end =
|
||||
|ty: KinsokuShoriType| break_line(tall, &glyphs, 0, 50.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, ty, false, false, &[]).0;
|
||||
assert_eq!(tall_end(PushInFirst), 4, "Vertical Scale does not widen the gaps: 五 is pushed out with 」");
|
||||
|
||||
// No gap closes inside a shaped cluster. 二 is two glyphs here, so the line has five gaps to
|
||||
@@ -371,7 +376,97 @@ fn kinsoku_shori_type_counts_the_gaps_that_close() {
|
||||
glyphs[1].adv = 5.0;
|
||||
let half = glyphs[1].clone();
|
||||
glyphs.insert(2, half);
|
||||
let split_end = |width: f64| break_line(tall, &glyphs, 0, width, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, PushInFirst, false, false).0;
|
||||
let split_end =
|
||||
|width: f64| break_line(tall, &glyphs, 0, width, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, PushInFirst, false, false, &[]).0;
|
||||
assert_eq!(split_end(54.0), 5, "五 is pushed out with 」");
|
||||
assert_eq!(split_end(55.0), 7, "five gaps close by 5 pt: 」 is pushed in");
|
||||
}
|
||||
|
||||
/// Mojikumi: the greedy breaker measures a line as it is set, with its last character narrowed
|
||||
/// (`line_end`): 」 set half width ends a measure of three and a half characters. Only the last
|
||||
/// glyph of the last cluster counts, and only where the line may end.
|
||||
#[test]
|
||||
fn greedy_wrap_counts_the_narrower_line_end() {
|
||||
use KinsokuShoriType::{PushInFirst, PushOutFirst, PushOutOnly};
|
||||
let text = "一二三」四五六";
|
||||
let glyphs = full_width(text);
|
||||
let mut credit = vec![0.0; glyphs.len()];
|
||||
credit[3] = 5.0;
|
||||
let end = |glyphs: &[SGlyph], width: f64, burasagari: Burasagari, line_end: &[f64]| {
|
||||
break_line(text, glyphs, 0, width, &[], 0.0, None, burasagari, Kinsoku::Hard, PushOutOnly, false, false, line_end).0
|
||||
};
|
||||
assert_eq!(end(&glyphs, 35.0, Burasagari::None, &credit), 4, "」 half width fits");
|
||||
assert_eq!(end(&glyphs, 35.0, Burasagari::None, &[]), 2, "」 full width does not: 三 goes with it");
|
||||
assert_eq!(end(&glyphs, 34.0, Burasagari::None, &credit), 2, "half width is still too wide");
|
||||
// A credit that isn't finite counts for nothing, and one larger than the glyph for its advance.
|
||||
assert_eq!(end(&glyphs, 35.0, Burasagari::None, &[0.0, 0.0, 0.0, f64::INFINITY]), 2);
|
||||
assert_eq!(end(&glyphs, 35.0, Burasagari::None, &[0.0, 0.0, 0.0, f64::NAN]), 2);
|
||||
assert_eq!(end(&glyphs, 30.0, Burasagari::None, &[0.0, 0.0, 0.0, 100.0]), 4, "at most 10 pt off");
|
||||
assert_eq!(end(&glyphs, 29.0, Burasagari::None, &[0.0, 0.0, 0.0, 100.0]), 2);
|
||||
|
||||
// The line may not end after 三 (、 follows it): its credit does not keep it on the line, so 、
|
||||
// does not hang after a line already wider than the measure.
|
||||
let comma = "一二三、";
|
||||
let glyphs = full_width(comma);
|
||||
let end = |line_end: &[f64]| {
|
||||
break_line(comma, &glyphs, 0, 25.0, &[], 0.0, None, Burasagari::Standard, Kinsoku::Hard, PushOutOnly, false, false, line_end).0
|
||||
};
|
||||
assert_eq!(end(&[0.0, 0.0, 5.0, 0.0]), 2);
|
||||
|
||||
// Kinsoku Shori Type measures both lines as they are set. Pushing 。 in leaves the line 5 pt
|
||||
// wider than the measure with 。 half width, which its five gaps close.
|
||||
let pushed = "一二三四」。六";
|
||||
let glyphs = full_width(pushed);
|
||||
let end = |ty: KinsokuShoriType, line_end: &[f64]| {
|
||||
break_line(pushed, &glyphs, 0, 50.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, ty, false, false, line_end).0
|
||||
};
|
||||
let stop = [0.0, 0.0, 0.0, 0.0, 0.0, 5.0, 0.0];
|
||||
assert_eq!(end(PushInFirst, &stop), 6, "。 is pushed in");
|
||||
assert_eq!(end(PushInFirst, &[]), 3, "full width, the gaps can't close enough");
|
||||
// Push Out First: pushing out to after 」, set half width, leaves the line 15 pt short, more
|
||||
// than an em, so 四 is pushed in.
|
||||
let pushed = format!("{}二」「四", "一".repeat(10));
|
||||
let glyphs = full_width(&pushed);
|
||||
let end = |line_end: &[f64]| {
|
||||
break_line(&pushed, &glyphs, 0, 130.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, PushOutFirst, false, false, line_end).0
|
||||
};
|
||||
let mut bracket = vec![0.0; glyphs.len()];
|
||||
bracket[11] = 5.0;
|
||||
assert_eq!(end(&bracket), 14, "四 is pushed in");
|
||||
assert_eq!(end(&[]), 12, "full width, pushing out leaves the line one em short: 「四 go on");
|
||||
|
||||
// 三 is two glyphs of one cluster: the credit of its first glyph does not count.
|
||||
let text = "一二三";
|
||||
let mut glyphs = full_width(text);
|
||||
glyphs[2].adv = 5.0;
|
||||
let half = glyphs[2].clone();
|
||||
glyphs.insert(3, half);
|
||||
let end =
|
||||
|line_end: &[f64]| break_line(text, &glyphs, 0, 26.0, &[], 0.0, None, Burasagari::None, Kinsoku::None, PushOutOnly, false, false, line_end).0;
|
||||
assert_eq!(end(&[0.0, 0.0, 4.0, 0.0]), 2, "the cluster stays whole and goes to the next line");
|
||||
assert_eq!(end(&[0.0, 0.0, 0.0, 4.0]), 4, "its last glyph's credit keeps it on the line");
|
||||
}
|
||||
|
||||
/// Mojikumi: the every-line composer measures each candidate line with its last character narrowed
|
||||
/// (`end_credit`), never by more than that glyph's advance, and a line ending in a hyphen ends with
|
||||
/// the hyphen.
|
||||
#[test]
|
||||
fn composition_counts_the_narrower_line_end() {
|
||||
let text = "一二三」四五六";
|
||||
let glyphs = full_width(text);
|
||||
let cands = candidates(text, &glyphs, None, Kinsoku::Hard);
|
||||
let params = Params { justify_last: false, ragged: true, tolerance: f64::INFINITY, uniform_from: 0, hyphen_limit: 0 };
|
||||
let lines =
|
||||
|width: f64, end_credit: &dyn Fn(usize) -> f64, cands: &[Breakpoint]| compose(&glyphs, &|_| width, &|_| 0.0, end_credit, cands, &[], ¶ms);
|
||||
let half = |t: usize| if t == 4 { 5.0 } else { 0.0 };
|
||||
assert_eq!(lines(35.0, &half, &cands), Some(vec![(4, false), (7, false)]));
|
||||
let solid = lines(35.0, &|_| 0.0, &cands).expect("composed");
|
||||
assert!(solid.iter().all(|&(end, _)| end != 4), "」 full width does not end the first line: {solid:?}");
|
||||
// At most the glyph's advance; a credit that isn't finite counts for nothing.
|
||||
assert_eq!(lines(30.0, &|t| if t == 4 { 100.0 } else { 0.0 }, &cands).and_then(|l| l.first().copied()), Some((4, false)));
|
||||
assert!(lines(35.0, &|_| f64::NAN, &cands).is_some_and(|l| l.iter().all(|&(end, _)| end != 4)));
|
||||
// No credit for a line ending in a hyphen.
|
||||
let hyphenated = [Breakpoint { end: 4, hyphen: 1.0 }];
|
||||
assert_eq!(lines(38.0, &half, &hyphenated), None);
|
||||
assert_eq!(lines(38.0, &half, &[Breakpoint { end: 4, hyphen: 0.0 }]), Some(vec![(4, false), (7, false)]));
|
||||
}
|
||||
|
||||
@@ -414,6 +414,41 @@ pub fn vertical_punctuation_font(alternates: bool) -> Option<Vec<u8>> {
|
||||
Some(builder.build())
|
||||
}
|
||||
|
||||
/// The family of [`full_width_font`].
|
||||
pub const FULL_WIDTH_FAMILY: &str = "Fullwidthtest Sans";
|
||||
|
||||
/// The ideographs and Japanese punctuation [`full_width_font`] sets one em wide.
|
||||
pub const FULL_WIDTH_CHARS: &str = "一二三四五六「」、。";
|
||||
|
||||
/// A test-only cmap over bundled Source Sans 3: [`FULL_WIDTH_CHARS`] use the one-em-wide glyph of
|
||||
/// U+2318, so Japanese composition is tested without OS fonts; Latin letters and the space keep
|
||||
/// their own glyphs.
|
||||
pub fn full_width_font() -> Option<Vec<u8>> {
|
||||
use write_fonts::tables::cmap::Cmap;
|
||||
|
||||
const BASE: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf");
|
||||
let font = skrifa::FontRef::new(BASE).ok()?;
|
||||
let charmap = font.charmap();
|
||||
let wide = write_fonts::types::GlyphId::new(charmap.map('\u{2318}')?.to_u32());
|
||||
let latin = ('A'..='Z').chain('a'..='z').chain([' ']);
|
||||
let mut mappings: Vec<(char, write_fonts::types::GlyphId)> = FULL_WIDTH_CHARS.chars().map(|c| (c, wide)).collect();
|
||||
for c in latin {
|
||||
mappings.push((c, write_fonts::types::GlyphId::new(charmap.map(c)?.to_u32())));
|
||||
}
|
||||
let cmap = Cmap::from_mappings(mappings).ok()?;
|
||||
let name = name_table(&[(1, FULL_WIDTH_FAMILY), (2, "Regular"), (4, "Fullwidthtest Sans Regular"), (6, "FullwidthtestSans-Regular")])?;
|
||||
let mut builder = FontBuilder::new();
|
||||
builder.add_table(&cmap).ok()?;
|
||||
builder.add_raw(Tag::new(b"name"), name);
|
||||
for r in font.table_directory.table_records() {
|
||||
let tag = Tag::new(&r.tag().to_be_bytes());
|
||||
if tag != Tag::new(b"GSUB") && !builder.contains(tag) {
|
||||
builder.add_raw(tag, font.table_data(r.tag())?.as_bytes());
|
||||
}
|
||||
}
|
||||
Some(builder.build())
|
||||
}
|
||||
|
||||
/// The family of [`half_form_font`].
|
||||
pub const HALF_FORM_FAMILY: &str = "Halftest Sans";
|
||||
|
||||
|
||||
@@ -1075,6 +1075,76 @@ fn mojikumi_sets_an_opening_bracket_flush_at_the_start_of_a_wrapped_line() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Mojikumi: with Line-end Punctuation Half Width, a line is measured as it is set. A closing
|
||||
/// bracket, comma or full stop that fits only at half width ends the line in a measure of three
|
||||
/// and a half ems, with both composers, horizontal and vertical; with Mojikumi None it goes to the
|
||||
/// next line with the character before it. A closing mark that already lost its half to the
|
||||
/// punctuation after it takes nothing more off, and the space between Japanese and Latin text
|
||||
/// counts only where the line ends. Uses a test font whose ideographs and punctuation are one em.
|
||||
#[test]
|
||||
fn mojikumi_line_end_half_width_counts_when_a_line_breaks() {
|
||||
use crate::test_fonts::{FULL_WIDTH_FAMILY, full_width_font};
|
||||
use vectorcraft_doc::{Burasagari, Composer, Kinsoku, KinsokuShoriType, Mojikumi};
|
||||
let db = FontDb::with_font_dirs(vec![]);
|
||||
db.add_font(full_width_font().expect("test font"));
|
||||
let lay = |text: &str, m: Mojikumi, composer: Composer, burasagari: Burasagari, vertical_type: bool, measure: f64| {
|
||||
let style = CharStyle { size: 20.0, font_family: FULL_WIDTH_FAMILY.into(), font_style: "Regular".into(), ..CharStyle::default() };
|
||||
let mut t = TextObject::point(Point::ZERO, text, style);
|
||||
t.xf = Affine::IDENTITY;
|
||||
t.vertical = vertical_type;
|
||||
t.para.mojikumi = m;
|
||||
t.para.composer = composer;
|
||||
t.para.burasagari = burasagari;
|
||||
t.para.kinsoku = Kinsoku::Hard;
|
||||
// Push Out Only lets the every-line composer take the paragraph.
|
||||
if composer == Composer::EveryLine {
|
||||
t.para.kinsoku_type = KinsokuShoriType::PushOutOnly;
|
||||
}
|
||||
let frame = if vertical_type { Rect::new(0.0, 0.0, 200.0, measure) } else { Rect::new(0.0, 0.0, measure, 200.0) };
|
||||
t.kind = TextKind::Area { frame: PathData::from_bezpath(&frame.to_path(0.1)) };
|
||||
layout(&db, &t)
|
||||
};
|
||||
let ends = |l: &TextLayout, text: &str| l.lines.iter().map(|line| text[line.start..line.end].to_string()).collect::<Vec<_>>();
|
||||
let widest = |l: &TextLayout| {
|
||||
l.lines.iter().map(|line| l.glyphs[line.glyph_start..line.glyph_end].iter().map(|g| g.advance).sum::<f64>()).fold(0.0, f64::max)
|
||||
};
|
||||
assert!((lay("一", Mojikumi::None, Composer::SingleLine, Burasagari::None, false, 70.0).glyphs[0].advance - 20.0).abs() < 1e-9, "one em");
|
||||
for vertical_type in [false, true] {
|
||||
for composer in [Composer::SingleLine, Composer::EveryLine] {
|
||||
let at = format!("vertical {vertical_type} {composer:?}");
|
||||
let marks = [("一二三」四五六", "一二三」"), ("一二三。四五六", "一二三。"), ("一二三、四五六", "一二三、")];
|
||||
for (text, first) in marks {
|
||||
let l = lay(text, Mojikumi::LineEndHalf, composer, Burasagari::None, vertical_type, 70.0);
|
||||
assert_eq!(ends(&l, text), [first, "四五六"], "{at}: {text}");
|
||||
assert!((l.glyphs[3].advance - 10.0).abs() < 1e-9, "{at}: {text}: the mark ends the line half width");
|
||||
assert!(widest(&l) <= 70.0 + 1e-9, "{at}: {text}: {}", widest(&l));
|
||||
// Standard burasagari: the mark fits inside, so it doesn't hang.
|
||||
let standard = lay(text, Mojikumi::LineEndHalf, composer, Burasagari::Standard, vertical_type, 70.0);
|
||||
assert_eq!(ends(&standard, text), [first, "四五六"], "{at}: {text} standard");
|
||||
assert!(widest(&standard) <= 70.0 + 1e-9, "{at}: {text} standard");
|
||||
// Full width it doesn't fit: kinsoku takes 三 to the next line with it.
|
||||
let solid = lay(text, Mojikumi::None, composer, Burasagari::None, vertical_type, 70.0);
|
||||
assert_eq!(ends(&solid, text).first().map(String::as_str), Some("一二"), "{at}: {text} none");
|
||||
}
|
||||
// 」 lost its half to 。 after it: the line would be four ems, so 三」。 go on.
|
||||
let text = "一二三」。四五";
|
||||
let l = lay(text, Mojikumi::LineEndHalf, composer, Burasagari::None, vertical_type, 70.0);
|
||||
assert_eq!(ends(&l, text).first().map(String::as_str), Some("一二"), "{at}: compressed");
|
||||
assert!(widest(&l) <= 70.0 + 1e-9, "{at}: compressed");
|
||||
}
|
||||
}
|
||||
// The quarter em between 三 and A goes where the line ends there: 一二三 fits three ems and a
|
||||
// tenth, with or without the space at its end.
|
||||
for composer in [Composer::SingleLine, Composer::EveryLine] {
|
||||
let text = "一二三ABC";
|
||||
let on = lay(text, Mojikumi::LineEndHalf, composer, Burasagari::None, false, 62.0);
|
||||
assert_eq!(ends(&on, text).first().map(String::as_str), Some("一二三"), "{composer:?}");
|
||||
assert!((on.glyphs[2].advance - 20.0).abs() < 1e-9, "{composer:?}: no space at the line's end");
|
||||
let wide = lay(text, Mojikumi::LineEndHalf, composer, Burasagari::None, false, 200.0);
|
||||
assert!((wide.glyphs[2].advance - 25.0).abs() < 1e-9, "{composer:?}: the space inside the line");
|
||||
}
|
||||
}
|
||||
|
||||
/// Burasagari: in a measure of exactly five ems, a comma that would be the sixth character goes to
|
||||
/// the next line with the character before it (None) or hangs outside the line (Standard, Forced);
|
||||
/// a full stop that is the fifth character stays inside (None, Standard) or hangs while the four
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user