Running headers: place each story by its first hit on the page

Review fix (CodeRabbit): a story was placed on the page by its highest
hit, so a heading at the top of its later column put the whole story ahead
of another story whose heading sits higher than the first one's. Each story
is now placed by its first hit there in text order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgGsJkVe5GYG9qK6VFdECa
This commit is contained in:
Claude
2026-10-19 17:48:02 +00:00
parent db0bc7ada0
commit 7388c42d51
2 changed files with 42 additions and 3 deletions
+5 -3
View File
@@ -66,13 +66,15 @@ struct Hit {
/// second column comes after its first, however high its entries sit), and stories by where their
/// first entry on the page is (top to bottom, then left to right).
fn in_page_order(hits: Vec<Hit>) -> Vec<(usize, String)> {
let mut first: HashMap<(usize, StoryId), (i64, i64)> = HashMap::new();
// Each story on a page is placed by its first hit there in text order (not its highest hit,
// which in a later column could put the whole story ahead of another).
let mut first: HashMap<(usize, StoryId), (usize, (i64, i64))> = HashMap::new();
for h in &hits {
let (page, y, x) = h.at;
first.entry((page, h.story)).and_modify(|f| *f = (*f).min((y, x))).or_insert((y, x));
first.entry((page, h.story)).and_modify(|f| *f = (*f).min((h.pos, (y, x)))).or_insert((h.pos, (y, x)));
}
let mut hits: Vec<_> =
hits.into_iter().map(|h| (h.at.0, first.get(&(h.at.0, h.story)).copied().unwrap_or((h.at.1, h.at.2)), h.story, h.pos, h.text)).collect();
hits.into_iter().map(|h| (h.at.0, first.get(&(h.at.0, h.story)).map_or((h.at.1, h.at.2), |f| f.1), h.story, h.pos, h.text)).collect();
hits.sort_by_key(|a| (a.0, a.1, a.2.0, a.3));
hits.into_iter().map(|(page, _, _, _, text)| (page, text)).collect()
}
+37
View File
@@ -386,4 +386,41 @@ mod tests {
assert_eq!(idx.value("Heading", false, designcraft_doc::vars::Use::FirstOnPage, 0), "Alpha");
assert_eq!(idx.value("Heading", false, designcraft_doc::vars::Use::LastOnPage, 0), "Gamma");
}
/// Stories on one page are ordered by where each one's text starts there: a story whose first
/// heading sits low in its first column comes after another story's heading above it, even
/// though its later column starts higher still.
#[test]
fn running_header_orders_stories_by_their_first_heading() {
use designcraft_doc::story::COLUMN_BREAK;
let mut s = Session::new();
s.execute("file.new", &json!({"pages": 1, "facingPages": false})).unwrap();
s.execute("style.paragraph.create", &json!({"name": "Heading"})).ok();
// Story A, two columns: filler, then Alpha low in column 1; Gamma at the top of column 2.
let a_text = format!("{}Alpha{COLUMN_BREAK}\nGamma\ny", "x\n".repeat(8));
let a = s.execute("frame.create", &json!({"rect": [72, 72, 300, 400], "content": "text", "text": a_text})).unwrap();
s.execute("object.textFrameOptions", &json!({"ids": [a["id"]], "columns": 2, "gutter": 12})).unwrap();
// Story B beside it: Beta between Gamma's height and Alpha's.
let b_text = "Beta".to_string();
let b = s.execute("frame.create", &json!({"rect": [320, 120, 560, 160], "content": "text", "text": b_text})).unwrap();
let story =
|s: &Session, id: &Value| s.doc().unwrap().doc.item(designcraft_doc::ItemId(id.as_u64().unwrap())).unwrap().text_frame().unwrap().story;
let (sa, sb) = (story(&s, &a["id"]), story(&s, &b["id"]));
for (sid, text, word) in [(sa, &a_text, "Alpha"), (sa, &a_text, "Gamma"), (sb, &b_text, "Beta")] {
let at = text.find(word).unwrap();
s.execute("text.select", &json!({"story": sid.0, "anchor": at, "focus": at + word.len()})).unwrap();
s.execute("style.paragraph.apply", &json!({"name": "Heading"})).unwrap();
}
s.execute("variables.define", &json!({"name": "Entry", "type": "runningHeader", "style": "Heading"})).unwrap();
let d = s.doc().unwrap().doc.clone();
let y = |sid, text: &str, word: &str| {
let cs = s.cache.get(&d, sid, None);
let (fi, _, b, ..) = crate::compose::caret(&cs, text.find(word).unwrap()).unwrap();
d.item(cs.frames[fi].frame).unwrap().bounds().y0 + b
};
let (ya, yg, yb) = (y(sa, &a_text, "Alpha"), y(sa, &a_text, "Gamma"), y(sb, &b_text, "Beta"));
assert!(yg < yb && yb < ya, "Gamma {yg} above Beta {yb} above Alpha {ya}");
let idx = s.cache.running_index(&d);
assert_eq!(idx.value("Heading", false, designcraft_doc::vars::Use::FirstOnPage, 0), "Beta");
assert_eq!(idx.value("Heading", false, designcraft_doc::vars::Use::LastOnPage, 0), "Gamma");
}
}