mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-25 10:46:50 -06:00
Merge #284: Text: a justified line with a tab justifies the text after its last tab
Conflict: crates/compose/src/lib.rs: tab loop keeps #282's tab_advance and records #284's last tab (no stop = default left tab); squeeze_last keeps next's mojikumi condition with #284's seg_justifies; single-word gaps keep next's moji filter after #284's skip(seg) Conflict: crates/compose/src/tests.rs: kept both
This commit is contained in:
@@ -2217,6 +2217,8 @@ fn layout_line(
|
||||
let mut x = 0.0;
|
||||
let mut i = 0;
|
||||
let mut leaders: Vec<(usize, String)> = Vec::new();
|
||||
// The line's last tab and whether it is a left tab (only text after a left tab justifies).
|
||||
let mut last_tab: Option<(usize, bool)> = None;
|
||||
while i < line.len() {
|
||||
if line[i].ch == '\t' {
|
||||
let (w, stop) = tab_advance(&pp.tabs, x0 + x - tab_origin, line.get(i + 1..).unwrap_or_default());
|
||||
@@ -2226,9 +2228,11 @@ fn layout_line(
|
||||
leaders.push((i, t.leader.clone()));
|
||||
}
|
||||
line[i].adv = w;
|
||||
last_tab = Some((i, stop.is_none_or(|t| t.align == TabAlign::Left)));
|
||||
} else if line[i].ch == story::RIGHT_INDENT_TAB {
|
||||
let rest: f64 = line[i + 1..].iter().map(|g| g.adv).sum();
|
||||
line[i].adv = (measure - x - rest).max(0.0);
|
||||
last_tab = Some((i, false));
|
||||
}
|
||||
x += line[i].adv;
|
||||
i += 1;
|
||||
@@ -2253,7 +2257,11 @@ fn layout_line(
|
||||
}
|
||||
let natural: f64 = line.iter().map(|g| g.adv).sum();
|
||||
let mut extra = measure - natural;
|
||||
let spaces: Vec<usize> = line.iter().enumerate().filter(|(_, g)| g.is_space() && !g.no_break).map(|(i, _)| i).collect();
|
||||
// Justification adjusts only the text after the last tab (InDesign keeps tab stops aligned);
|
||||
// a line whose last tab is a right, centre, character or right-indent tab is not justified.
|
||||
let seg = last_tab.map_or(0, |(i, _)| i + 1);
|
||||
let seg_justifies = last_tab.is_none_or(|(_, left)| left);
|
||||
let spaces: Vec<usize> = line.iter().enumerate().skip(seg).filter(|(_, g)| g.is_space() && !g.no_break).map(|(i, _)| i).collect();
|
||||
let align = match pp.align {
|
||||
Align::TowardsSpine => {
|
||||
if left_page {
|
||||
@@ -2271,10 +2279,10 @@ fn layout_line(
|
||||
}
|
||||
a => a,
|
||||
};
|
||||
let justify_this = align.is_justified() && (!last || align == Align::FullyJustified || forced_mid) && !has_tab;
|
||||
let justify_this = align.is_justified() && (!last || align == Align::FullyJustified || forced_mid) && seg_justifies;
|
||||
// A justified paragraph's last line may have been composed with shrunk spaces: shrink it too.
|
||||
let squeeze_last = !justify_this
|
||||
&& !has_tab
|
||||
&& seg_justifies
|
||||
&& extra < 0.0
|
||||
&& (align.is_justified() || line.iter().any(|g| g.moji.shrink > 0.0 || mojikumi::start_elastic(g, false, true)[1] > 0.0));
|
||||
// Extra advance per glyph (word spaces and letter gaps) and horizontal scale per glyph.
|
||||
@@ -2284,7 +2292,7 @@ fn layout_line(
|
||||
// Kashidas: in justified Arabic, the joins of words take the extra length first.
|
||||
let mut kashidas: Vec<(usize, f64)> = Vec::new();
|
||||
if justify_this && extra > 0.0 && pp.kashidas && pp.arabic_justification != "DefaultJustification" {
|
||||
let points = kashida_points(&line);
|
||||
let points: Vec<usize> = kashida_points(&line).into_iter().filter(|&i| i >= seg).collect();
|
||||
if !points.is_empty() {
|
||||
let total = extra.min(points.iter().map(|&i| line[i].size * 1.5).sum());
|
||||
let per = total / points.len() as f64;
|
||||
@@ -2299,8 +2307,11 @@ fn layout_line(
|
||||
extra = mojikumi::distribute(&mut line, extra, &mut add);
|
||||
}
|
||||
if (justify_this || squeeze_last) && !spaces.is_empty() {
|
||||
distribute(&line, &spaces, extra, sp, &mut add, &mut scale);
|
||||
} else if justify_this && spaces.is_empty() && line.len() > 1 && !last {
|
||||
let rebased: Vec<usize> = spaces.iter().map(|&i| i - seg).collect();
|
||||
let (seg_line, seg_add, seg_scale) =
|
||||
(line.get(seg..).unwrap_or(&[]), add.get_mut(seg..).unwrap_or(&mut []), scale.get_mut(seg..).unwrap_or(&mut []));
|
||||
distribute(seg_line, &rebased, extra, sp, seg_add, seg_scale);
|
||||
} else if justify_this && spaces.is_empty() && line.len() > seg + 1 && !last {
|
||||
// Single word: Single Word Justification.
|
||||
match pp.single_word_justify {
|
||||
Align::FullyJustified => {
|
||||
@@ -2308,6 +2319,7 @@ fn layout_line(
|
||||
.iter()
|
||||
.enumerate()
|
||||
.take(line.len() - 1)
|
||||
.skip(seg)
|
||||
.filter(|(_, g)| !g.moji.active && (!g.locked_advance || g.break_after != Some(false)))
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
@@ -2318,8 +2330,14 @@ fn layout_line(
|
||||
}
|
||||
}
|
||||
}
|
||||
Align::Center => offset = extra / 2.0,
|
||||
Align::Right => offset = extra,
|
||||
// After a tab the word moves by widening the tab, so text before it stays put.
|
||||
Align::Center | Align::Right => {
|
||||
let shift = if pp.single_word_justify == Align::Center { extra / 2.0 } else { extra };
|
||||
match seg.checked_sub(1).and_then(|t| add.get_mut(t)) {
|
||||
Some(tab) => *tab += shift,
|
||||
None => offset = shift,
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -3962,3 +3962,43 @@ fn column_rules_break_around_a_spanning_paragraph_between_bands() {
|
||||
assert!(here.iter().any(|r| (r.y0 - bottom).abs() < 1e-9), "rule resumes below the heading at {x}: {here:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn justified_line_with_a_tab_justifies_the_text_after_its_last_tab() {
|
||||
// InDesign justifies only the text after a line's last (left) tab; tab stops stay aligned.
|
||||
let stop = |align| designcraft_doc::TabStop { position: 100.0, align, leader: String::new(), align_on: String::new() };
|
||||
let text = "Name\t42 and some words\u{2028}more";
|
||||
let x_of = |l: &Line, byte: usize| l.glyphs.iter().find(|g| g.byte == byte && g.len > 0).map(|g| g.x).unwrap();
|
||||
let left = ParaAttrs { align: Some(Align::LeftJustified), tabs: Some(vec![stop(TabAlign::Left)]), ..Default::default() };
|
||||
let (d, sid, _) = doc_with(text, Rect::new(0.0, 0.0, 300.0, 100.0), left.clone());
|
||||
let cs = compose_story(&d, sid, &ComposeOptions::default());
|
||||
let l = all_lines(&cs)[0];
|
||||
assert!((l.end_x - l.x1).abs() < 0.6, "line with a left tab ends at {} not {}", l.end_x, l.x1);
|
||||
assert!((x_of(l, 5) - 100.0).abs() < 0.5, "text after the tab starts at the stop: {}", x_of(l, 5));
|
||||
let (d, sid, _) = doc_with(text, Rect::new(0.0, 0.0, 300.0, 100.0), ParaAttrs { align: Some(Align::Left), ..left });
|
||||
let ragged = compose_story(&d, sid, &ComposeOptions::default());
|
||||
let r = all_lines(&ragged)[0];
|
||||
for byte in 0..4 {
|
||||
assert!((x_of(l, byte) - x_of(r, byte)).abs() < 1e-6, "text before the tab keeps its natural position");
|
||||
}
|
||||
|
||||
// After a right tab the line stays as set.
|
||||
let right = ParaAttrs { align: Some(Align::LeftJustified), tabs: Some(vec![stop(TabAlign::Right)]), ..Default::default() };
|
||||
let (d, sid, _) = doc_with("Name\t42\u{2028}more", Rect::new(0.0, 0.0, 300.0, 100.0), right);
|
||||
let cs = compose_story(&d, sid, &ComposeOptions::default());
|
||||
let l = all_lines(&cs)[0];
|
||||
assert!((l.end_x - 100.0).abs() < 0.5, "right-tab line ends at its stop: {}", l.end_x);
|
||||
|
||||
// A footnote's number and tab separator: its first line is justified like the others.
|
||||
let (mut d, sid, fid) = doc_with("Body text.", Rect::new(0.0, 0.0, 200.0, 400.0), ParaAttrs::default());
|
||||
let note = ParaFormat { para: ParaAttrs { align: Some(Align::LeftJustified), ..Default::default() }, ..Default::default() };
|
||||
d.story_mut(sid).unwrap().insert_note(4, LOREM, note);
|
||||
let cs = compose_story(&d, sid, &ComposeOptions::default());
|
||||
let n = &cs.frame(fid).unwrap().notes[0];
|
||||
let lines = all_lines(&n.text);
|
||||
assert!(lines.len() > 2);
|
||||
for l in &lines[..lines.len() - 1] {
|
||||
assert!((l.end_x - l.x1).abs() < 0.6, "footnote line ends at {} not {}", l.end_x, l.x1);
|
||||
}
|
||||
assert!((x_of(lines[0], 0) - 36.0).abs() < 0.5, "note text starts at the default tab stop: {}", x_of(lines[0], 0));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user