diff --git a/crates/compose/src/lib.rs b/crates/compose/src/lib.rs index 075dc9872..e902a875c 100644 --- a/crates/compose/src/lib.rs +++ b/crates/compose/src/lib.rs @@ -650,7 +650,7 @@ pub fn compose(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &Compo let spacing = spacing_for(&pp, base_size); // An empty paragraph takes its height from its paragraph mark's character format. let mark_chars = if glyphs.is_empty() { - std::borrow::Cow::Owned(doc.styles.resolve_char(&base_chars, story.char_format_at(prange.start))) + std::borrow::Cow::Owned(doc.styles.resolve_char(&base_chars, story.char_format_of(prange.start))) } else { std::borrow::Cow::Borrowed(&base_chars) }; diff --git a/crates/compose/src/tests.rs b/crates/compose/src/tests.rs index da3e46066..d27f76b6d 100644 --- a/crates/compose/src/tests.rs +++ b/crates/compose/src/tests.rs @@ -1044,3 +1044,22 @@ fn empty_paragraph_takes_its_marks_format() { assert!((lines[1].baseline - lines[0].baseline - 8.0).abs() < 1e-6, "{} {}", lines[0].baseline, lines[1].baseline); assert!((lines[2].baseline - lines[1].baseline - 8.0).abs() < 1e-6, "{} {}", lines[1].baseline, lines[2].baseline); } + +#[test] +fn empty_paragraph_uses_its_own_mark_not_the_previous_one() { + // "One\n" is 20 pt; the empty paragraph's mark (byte 4) and "Two" are 6 pt with 8 pt leading. + // The format run boundary sits exactly at the empty paragraph's start: its line must take the + // 8 pt leading of its own mark, not the previous paragraph's 20 pt (auto 24) mark. + let text = "One\n\nTwo"; + let (mut d, sid, _) = doc_with(text, Rect::new(0.0, 0.0, 200.0, 200.0), ParaAttrs::default()); + let st = d.story_mut(sid).unwrap(); + st.format_chars(0..4, |f| f.over.size = Some(20.0)); + st.format_chars(4..text.len(), |f| { + f.over.size = Some(6.0); + f.over.leading = Some(designcraft_doc::Leading::Points(8.0)); + }); + let cs = compose_story(&d, sid, &ComposeOptions::default()); + let lines = all_lines(&cs); + assert_eq!(lines.len(), 3); + assert!((lines[1].baseline - lines[0].baseline - 8.0).abs() < 1e-6, "{} {}", lines[0].baseline, lines[1].baseline); +} diff --git a/crates/doc/src/story.rs b/crates/doc/src/story.rs index bbb5b6f56..30e86ebd3 100644 --- a/crates/doc/src/story.rs +++ b/crates/doc/src/story.rs @@ -192,6 +192,21 @@ impl Story { self.chars.last().map_or(&NO_FORMAT, |r| &r.format) } + /// Format of the character starting at byte `pos` (the run containing `pos`), unlike + /// [`Self::char_format_at`], which at a run boundary returns the format of the character + /// before the caret. Past the end, the last run's format. + pub fn char_format_of(&self, pos: usize) -> &CharFormat { + let mut start = 0usize; + for r in &self.chars { + let end = start.saturating_add(r.len); + if pos < end { + return &r.format; + } + start = end; + } + self.chars.last().map_or(&NO_FORMAT, |r| &r.format) + } + /// Iterate `(range, format)` of character runs. pub fn runs(&self) -> impl Iterator, &CharFormat)> { let mut start = 0;