mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-11 04:50:53 +00:00
Compose: jidori runs keep their width when their spaces could stretch; tests for nonbreaking-space lines
This commit is contained in:
1 parent
42cfc8b06c
commit
0474a8b5ec
4 files changed
+50
-4
No files matched your search
@@ -92,9 +92,10 @@ impl Spacing {
|
||||
[0.0, g.space * (self.letter_desired - self.letter_min).max(0.0), natural * (self.glyph_desired - self.glyph_min).max(0.0)],
|
||||
)
|
||||
}
|
||||
/// Word-space stretch and shrink of space glyph `g` (see [`Glyph::is_word_space`]).
|
||||
/// Word-space stretch and shrink of space glyph `g` (see [`Glyph::is_word_space`]); none
|
||||
/// inside a jidori run, whose width is fitted.
|
||||
fn space_elastic(&self, g: &Glyph) -> (f64, f64) {
|
||||
if !g.is_word_space() {
|
||||
if !g.is_word_space() || g.locked_advance {
|
||||
return (0.0, 0.0);
|
||||
}
|
||||
(g.space * (self.word_max - self.word_desired).max(0.0), g.space * (self.word_desired - self.word_min).max(0.0))
|
||||
|
||||
@@ -2260,7 +2260,8 @@ fn layout_line(
|
||||
// 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_justify_space()).map(|(i, _)| i).collect();
|
||||
// Spaces inside a jidori run keep the run's fitted width, as in the breaker.
|
||||
let spaces: Vec<usize> = line.iter().enumerate().skip(seg).filter(|(_, g)| g.is_justify_space() && !g.locked_advance).map(|(i, _)| i).collect();
|
||||
let align = match pp.align {
|
||||
Align::TowardsSpine => {
|
||||
if left_page {
|
||||
|
||||
@@ -71,7 +71,8 @@ impl Glyph {
|
||||
matches!(self.ch, ' ' | '\u{2002}'..='\u{200A}') || (self.ch == '\u{3000}' && self.ideographic_space_elastic)
|
||||
}
|
||||
/// A space whose width justification and word spacing vary: U+0020, the nonbreaking space
|
||||
/// U+00A0 and an elastic ideographic space. Fixed-width spaces (U+2002–U+200A, U+202F) keep theirs.
|
||||
/// U+00A0 and an elastic ideographic space. Fixed-width spaces (U+2002–U+200A, U+202F) get no
|
||||
/// word-space stretch or shrink.
|
||||
pub fn is_word_space(&self) -> bool {
|
||||
matches!(self.ch, ' ' | NBSP) || (self.ch == '\u{3000}' && self.ideographic_space_elastic)
|
||||
}
|
||||
|
||||
@@ -386,6 +386,49 @@ fn nonbreaking_space_stretches_and_fixed_width_one_does_not() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jidori_run_keeps_its_width_on_a_justified_line() {
|
||||
fn first_line(text: &str, width: f64, para: ParaAttrs, run: std::ops::Range<usize>) -> Line {
|
||||
let (mut d, sid, _) = doc_with(text, Rect::new(0.0, 0.0, width, 1000.0), para);
|
||||
d.story_mut(sid).unwrap().format_chars(run, |f| f.over.jidori = Some(6));
|
||||
let cs = compose_story(&d, sid, &ComposeOptions::default());
|
||||
all_lines(&cs)[0].clone()
|
||||
}
|
||||
for sp in ['\u{A0}', ' '] {
|
||||
let text = format!("aa{sp}aa bbbb cccc dddd eeee ffff gggg hhhh");
|
||||
let run = 0..4 + sp.len_utf8();
|
||||
let run_width = |l: &Line| l.glyphs.iter().filter(|g| run.contains(&g.byte)).map(|g| g.adv).sum::<f64>();
|
||||
let ragged = first_line(&text, 1000.0, ParaAttrs::default(), run.clone());
|
||||
let gap = ragged.glyphs.iter().find(|g| g.byte == text.find(" dddd").unwrap()).unwrap();
|
||||
let width = gap.x - ragged.x0 + gap.adv + 2.0 * ragged.x0;
|
||||
let para = ParaAttrs { align: Some(Align::LeftJustified), word_space_max: Some(4.0), ..Default::default() };
|
||||
let line = first_line(&text, width, para, run.clone());
|
||||
assert_eq!(text[line.range.clone()].trim_end(), format!("aa{sp}aa bbbb cccc"));
|
||||
assert!((line.end_x - line.x1).abs() < 0.1, "{sp:?}: line ends at {} not {}", line.end_x, line.x1);
|
||||
assert!((run_width(&line) - run_width(&ragged)).abs() < 1e-6, "{sp:?}: run is {} not {}", run_width(&line), run_width(&ragged));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonbreaking_space_alone_fills_a_justified_line() {
|
||||
let text = "10\u{A0}km 1234";
|
||||
let (d, sid, _) = doc_with(text, Rect::new(0.0, 0.0, 1000.0, 100.0), ParaAttrs::default());
|
||||
let cs = compose_story(&d, sid, &ComposeOptions::default());
|
||||
let ragged = all_lines(&cs)[0];
|
||||
let natural = ragged.glyphs.iter().map(|g| (g.byte, g.adv)).collect::<std::collections::HashMap<_, _>>();
|
||||
let m = ragged.glyphs.iter().find(|g| g.byte == 5).unwrap();
|
||||
let width = m.x - ragged.x0 + m.adv + 15.0 + 2.0 * ragged.x0;
|
||||
let line = justified_first_line(text, width, None);
|
||||
assert_eq!(text[line.range.clone()].trim_end(), "10\u{A0}km");
|
||||
for g in line.glyphs.iter().filter(|g| g.byte < 6) {
|
||||
if g.byte == 2 {
|
||||
assert!((g.adv - natural[&2] - 15.0).abs() < 0.1, "U+00A0 is {}", g.adv);
|
||||
} else {
|
||||
assert!((g.adv - natural[&g.byte]).abs() < 1e-6, "glyph at {} is {}", g.byte, g.adv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Story of `n` one-line filler paragraphs followed by `extra` paragraphs, in a 2-column frame.
|
||||
fn keep_doc(fillers: usize, extra: &[&str], height: f64) -> (Document, StoryId, Vec<usize>) {
|
||||
let mut parts: Vec<String> = (0..fillers).map(|k| format!("Filler {k}")).collect();
|
||||
|
||||
Reference in new issue
Block a user