# Conflicts:
#	crates/compose/src/notes.rs
This commit is contained in:
Brandon Thomas
2026-10-14 23:05:24 -04:00
4 changed files with 105 additions and 9 deletions
+52 -7
View File
@@ -10,6 +10,9 @@
//! First Baseline offset below the top of the footnote area, and each later footnote's first
//! baseline sits its first line's leading (plus Space Between Footnotes) below the previous
//! footnote's last baseline. Only the last footnote's descent counts toward the area's height.
//!
//! With Span Footnotes Across Columns the notes of a frame share one area across all its columns,
//! composed at the frame's text width: every column of the frame makes room for it.
use std::collections::HashMap;
use std::ops::Range;
@@ -71,13 +74,39 @@ pub(crate) struct Notes {
/// Composed footnote text by (index, label, width).
cache: HashMap<(usize, String, u64), (Arc<ComposedStory>, NoteMetrics)>,
notes: Vec<Arc<designcraft_doc::Footnote>>,
/// Footnotes span the columns of their frame.
span: bool,
/// Room a frame's earlier columns had to leave for spanning notes placed in a later column;
/// kept across re-lays so the columns before the reference make room on the next pass.
floor: HashMap<usize, f64>,
}
impl Notes {
pub fn new(doc: &Document, story: &Story) -> Notes {
let refs: Vec<usize> = if story.notes.is_empty() { Vec::new() } else { story.text.match_indices(FOOTNOTE_REF).map(|(i, _)| i).collect() };
let start = if refs.is_empty() { 1 } else { doc.footnote_start(story.id) };
Notes { numbers: vec![start; refs.len()], refs, num: start, key: None, placed: Vec::new(), cache: HashMap::new(), notes: story.notes.clone() }
Notes {
numbers: vec![start; refs.len()],
refs,
num: start,
key: None,
placed: Vec::new(),
cache: HashMap::new(),
notes: story.notes.clone(),
span: doc.footnote_options.span_columns,
floor: HashMap::new(),
}
}
/// Whether a note placed in (fi, col) shares the footnote area of column (`fi`, `ci`).
fn same_area(&self, p: &Placed, fi: usize, ci: usize) -> bool {
p.fi == fi && (self.span || p.col == ci)
}
/// The footnote area of column `col` of a frame with columns `cols`: the column, or all of
/// them when notes span columns.
pub fn area(&self, cols: &[Rect], col: usize) -> Option<Rect> {
if self.span { cols.iter().copied().reduce(|a, b| a.union(b)) } else { cols.get(col.min(cols.len().saturating_sub(1))).copied() }
}
pub fn active(&self) -> bool {
@@ -165,11 +194,27 @@ impl Notes {
pub fn reserve(&mut self, doc: &Document, fi: usize, col: usize, extra: &[usize], w: f64, f: &FrameSpec, opts: &ComposeOptions) -> f64 {
let o = &doc.footnote_options;
let (space_between, space_before) = (o.space_between, o.space_before);
let mut ms: Vec<NoteMetrics> = self.placed.iter().filter(|p| p.fi == fi && p.col == col).map(|p| p.m).collect();
let mut ms: Vec<NoteMetrics> = self.placed.iter().filter(|p| self.same_area(p, fi, col)).map(|p| p.m).collect();
for &k in extra {
ms.push(self.compose(doc, k, w, f, opts).1);
}
if ms.is_empty() { 0.0 } else { stack_height(&ms, space_between) + space_before }
let r = if ms.is_empty() { 0.0 } else { stack_height(&ms, space_between) + space_before };
if self.span { r.max(self.floor.get(&fi).copied().unwrap_or(0.0)) } else { r }
}
/// After spanning notes were placed in column `col` of frame `fi`: whether a line of an earlier
/// column now runs into their area. The area is then remembered for the frame, to re-lay it.
pub fn earlier_columns_collide(&mut self, lines: &[crate::Line], fi: usize, col: usize, cols: &[Rect], reserve: f64) -> bool {
if !self.span || col == 0 || reserve <= self.floor.get(&fi).copied().unwrap_or(0.0) + 0.01 {
return false;
}
let hit = lines
.iter()
.any(|l| (l.column as usize) < col && cols.get(l.column as usize).is_some_and(|c| l.baseline + l.descent > c.y1 - reserve + 0.01));
if hit {
self.floor.insert(fi, reserve);
}
hit
}
pub fn place(&mut self, doc: &Document, k: usize, fi: usize, col: usize, w: f64, f: &FrameSpec, opts: &ComposeOptions) {
@@ -186,13 +231,13 @@ impl Notes {
return;
}
let o = &doc.footnote_options;
let mut groups: Vec<(usize, usize)> = self.placed.iter().map(|p| (p.fi, p.col)).collect();
let mut groups: Vec<(usize, usize)> = self.placed.iter().map(|p| (p.fi, if self.span { 0 } else { p.col })).collect();
groups.dedup();
groups.sort_unstable();
groups.dedup();
for (fi, ci) in groups {
let (Some(_), Some(col)) = (frames.get(fi), cols.get(fi).and_then(|c| c.get(ci))) else { continue };
let here: Vec<&Placed> = self.placed.iter().filter(|p| p.fi == fi && p.col == ci).collect();
let (Some(_), Some(col)) = (frames.get(fi), cols.get(fi).filter(|c| ci < c.len()).and_then(|c| self.area(c, ci))) else { continue };
let here: Vec<&Placed> = self.placed.iter().filter(|p| self.same_area(p, fi, ci)).collect();
let total = stack_height(here.iter().map(|p| &p.m), o.space_between);
let top = col.y1 - total;
let ft = &mut out.frames[fi];
@@ -220,7 +265,7 @@ impl Notes {
ft.notes.push(PlacedNote {
id: note.id,
index: p.k,
column: ci as u32,
column: p.col as u32,
label: o.label(self.numbers[p.k], true),
origin: Point::new(col.x0, y),
rect: Rect::new(col.x0, y0, col.x1, y1),