mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-08 11:31:41 +00:00
Compose: an empty paragraph's format comes from its own mark
Story::char_format_at returns the preceding run at a boundary (the format typing takes), so a run ending at the empty paragraph's start gave it the previous paragraph's mark format. Add Story::char_format_of (the run containing the byte) and use it; regression test with differing marks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
60712dedb2
commit
593848bf17
3 files changed
+35
-1
No files matched your search
@@ -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)
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<Item = (std::ops::Range<usize>, &CharFormat)> {
|
||||
let mut start = 0;
|
||||
|
||||
Reference in new issue
Block a user