mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-31 13:46:52 -06:00
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user