mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-29 20:56:47 -06:00
Borders gain a stroke type (built-in or a document stroke style), cap, join, gap colour and tint, a shape and size per corner, column or text width, top and bottom edges, closed boxes in each part of a split paragraph, and one box for consecutive paragraphs with the same settings. Shading gains the same corners, edges and width, Clip to Frame, and Do not Print or Export. Boxes are drawn once the lines are in place, as filled paths where they aren't plain rectangles. Span and split paragraphs keep at least their space before and after from the column text. Lists have levels that number on their own and restart after a higher level, ^1 to ^9 in the number text, a character style for the label, a bullet font, left, centre or right alignment at the line start, and a tab position for the text after the label. All attributes load from older files with their defaults and round-trip through IDML, apart from the tab position.
216 lines
9.1 KiB
Rust
216 lines
9.1 KiB
Rust
//! Footnote layout: numbering, room at the bottom of columns, and placement.
|
|
//!
|
|
//! When a line holding footnote references is set, each referenced footnote is composed at the
|
|
//! column width and the column's text area shrinks by its height (plus the minimum space before
|
|
//! the first footnote and the space between footnotes). A line that would collide moves to the
|
|
//! next column with its footnotes. After the story is laid out the footnotes of each column are
|
|
//! stacked against the column bottom, with the separator rule above the first one.
|
|
|
|
use std::collections::HashMap;
|
|
use std::ops::Range;
|
|
use std::sync::Arc;
|
|
|
|
use designcraft_doc::notes::NoteRestart;
|
|
use designcraft_doc::{Document, FOOTNOTE_REF, ItemId, Story, TextFrameOptions};
|
|
use designcraft_geom::{Point, Rect};
|
|
|
|
use crate::shape::{Glyph, SubstCtx};
|
|
use crate::{ComposeOptions, ComposedStory, Deco, FrameSpec, PlacedNote};
|
|
|
|
/// A footnote assigned to a column.
|
|
pub(crate) struct Placed {
|
|
k: usize,
|
|
fi: usize,
|
|
col: usize,
|
|
h: f64,
|
|
text: Arc<ComposedStory>,
|
|
}
|
|
|
|
pub(crate) struct Notes {
|
|
/// Byte offsets of the reference characters (index = footnote index).
|
|
refs: Vec<usize>,
|
|
/// Number of each footnote (assigned when its paragraph is shaped).
|
|
numbers: Vec<u32>,
|
|
/// Next number and the restart unit (page / spread / section start) it counts in.
|
|
pub num: u32,
|
|
pub key: Option<usize>,
|
|
pub placed: Vec<Placed>,
|
|
/// Composed footnote text by (index, label, width).
|
|
cache: HashMap<(usize, String, u64), (Arc<ComposedStory>, f64)>,
|
|
notes: Vec<Arc<designcraft_doc::Footnote>>,
|
|
}
|
|
|
|
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() }
|
|
}
|
|
|
|
pub fn active(&self) -> bool {
|
|
!self.refs.is_empty() && self.refs.len() == self.notes.len()
|
|
}
|
|
|
|
/// Number the references in a paragraph (shaped on `page`) and hand their labels to the shaper.
|
|
pub fn number_refs(&mut self, doc: &Document, prange: Range<usize>, page: Option<usize>, sub: &mut SubstCtx) {
|
|
let o = &doc.footnote_options;
|
|
let key = page.and_then(|p| match o.restart {
|
|
NoteRestart::Never => None,
|
|
NoteRestart::Page => Some(p),
|
|
NoteRestart::Spread => doc.page_loc(p).map(|l| l.0),
|
|
NoteRestart::Section => Some(doc.section_of(p).map_or(0, |s| s.start)),
|
|
});
|
|
let a = self.refs.partition_point(|&b| b < prange.start);
|
|
let b = self.refs.partition_point(|&b| b < prange.end);
|
|
for k in a..b {
|
|
if o.restart != NoteRestart::Never && key != self.key {
|
|
self.key = key;
|
|
self.num = o.start_at;
|
|
}
|
|
self.numbers[k] = self.num;
|
|
self.num += 1;
|
|
sub.notes.insert(self.refs[k], o.label(self.numbers[k], false));
|
|
}
|
|
sub.note_position = o.ref_position;
|
|
sub.note_style = (o.ref_char_style != designcraft_doc::NO_CHAR_STYLE).then(|| o.ref_char_style.clone());
|
|
}
|
|
|
|
/// Footnote indices referenced by a line's glyphs.
|
|
pub fn refs_in(&self, glyphs: &[Glyph]) -> Vec<usize> {
|
|
let mut v: Vec<usize> = Vec::new();
|
|
for g in glyphs.iter().filter(|g| g.ch == FOOTNOTE_REF && g.len > 0) {
|
|
if let Ok(k) = self.refs.binary_search(&g.byte)
|
|
&& !v.contains(&k)
|
|
{
|
|
v.push(k);
|
|
}
|
|
}
|
|
v
|
|
}
|
|
|
|
fn compose(&mut self, doc: &Document, k: usize, w: f64, f: &FrameSpec, opts: &ComposeOptions) -> (Arc<ComposedStory>, f64) {
|
|
let o = &doc.footnote_options;
|
|
let label = format!("{}{}", o.label(self.numbers[k], true), o.separator);
|
|
let key = (k, label.clone(), w.to_bits());
|
|
if let Some(c) = self.cache.get(&key) {
|
|
return c.clone();
|
|
}
|
|
let spec = FrameSpec {
|
|
id: ItemId(0),
|
|
area: Rect::new(0.0, 0.0, w.max(1.0), 1.0e6),
|
|
opts: TextFrameOptions { first_baseline: o.first_baseline, first_baseline_min: o.first_baseline_min, ..Default::default() },
|
|
vertical: false,
|
|
exclusions: vec![],
|
|
page_name: f.page_name.clone(),
|
|
page: f.page,
|
|
grid: None,
|
|
left_page: f.left_page,
|
|
page_rect: None,
|
|
};
|
|
let nopts = ComposeOptions {
|
|
page_name: opts.page_name.clone(),
|
|
page: opts.page,
|
|
running: opts.running.clone(),
|
|
label: Some(label),
|
|
xrefs: opts.xrefs.clone(),
|
|
};
|
|
let cs = crate::compose(doc, &self.notes[k].text, std::slice::from_ref(&spec), &nopts);
|
|
let h = cs.frames.first().and_then(|ft| ft.lines.last()).map_or(0.0, |l| l.baseline + l.descent);
|
|
let v = (Arc::new(cs), h);
|
|
self.cache.insert(key, v.clone());
|
|
v
|
|
}
|
|
|
|
/// Height the footnotes of column (fi, col) take, with `extra` footnotes added.
|
|
#[allow(clippy::too_many_arguments)]
|
|
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 mut n = 0usize;
|
|
let mut h = 0.0;
|
|
for p in self.placed.iter().filter(|p| p.fi == fi && p.col == col) {
|
|
n += 1;
|
|
h += p.h;
|
|
}
|
|
for &k in extra {
|
|
n += 1;
|
|
h += self.compose(doc, k, w, f, opts).1;
|
|
}
|
|
if n == 0 { 0.0 } else { h + o.space_between * (n - 1) as f64 + o.space_before }
|
|
}
|
|
|
|
pub fn place(&mut self, doc: &Document, k: usize, fi: usize, col: usize, w: f64, f: &FrameSpec, opts: &ComposeOptions) {
|
|
if self.placed.iter().any(|p| p.k == k) {
|
|
return;
|
|
}
|
|
let (text, h) = self.compose(doc, k, w, f, opts);
|
|
self.placed.push(Placed { k, fi, col, h, text });
|
|
}
|
|
|
|
/// Stack each column's footnotes against its bottom, rule above the first.
|
|
pub fn finish(&mut self, doc: &Document, story: &Story, frames: &[FrameSpec], cols: &[Vec<Rect>], out: &mut ComposedStory) {
|
|
if self.placed.is_empty() {
|
|
return;
|
|
}
|
|
let o = &doc.footnote_options;
|
|
let mut groups: Vec<(usize, usize)> = self.placed.iter().map(|p| (p.fi, 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 total: f64 = here.iter().map(|p| p.h).sum::<f64>() + o.space_between * (here.len() - 1) as f64;
|
|
let top = col.y1 - total;
|
|
let ft = &mut out.frames[fi];
|
|
if o.rule.on && o.rule.weight > 0.0 {
|
|
let x0 = col.x0 + o.rule.left_indent;
|
|
let y = top + o.rule.offset - o.rule.weight;
|
|
ft.decos.push(Deco {
|
|
rect: Rect::new(x0, y, (x0 + o.rule.width).min(col.x1), y + o.rule.weight),
|
|
color: o.rule.color.clone(),
|
|
tint: o.rule.tint,
|
|
..Deco::default()
|
|
});
|
|
}
|
|
let mut y = top;
|
|
for p in here {
|
|
let note = &story.notes[p.k];
|
|
ft.notes.push(PlacedNote {
|
|
id: note.id,
|
|
index: p.k,
|
|
column: ci as u32,
|
|
label: o.label(self.numbers[p.k], true),
|
|
origin: Point::new(col.x0, y),
|
|
rect: Rect::new(col.x0, y, col.x1, y + p.h),
|
|
text: p.text.clone(),
|
|
source: Arc::from(note.text.text.as_str()),
|
|
});
|
|
y += p.h + o.space_between;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The placed footnote with id `id`: (frame index, footnote).
|
|
pub fn find_note(cs: &ComposedStory, id: u64) -> Option<(usize, &PlacedNote)> {
|
|
cs.frames.iter().enumerate().find_map(|(fi, ft)| ft.notes.iter().find(|n| n.id == id).map(|n| (fi, n)))
|
|
}
|
|
|
|
/// Footnote text hit at frame-inner point `p`: (footnote id, byte in the footnote's story).
|
|
pub fn hit_note(cs: &ComposedStory, fi: usize, p: Point) -> Option<(u64, usize)> {
|
|
let ft = cs.frames.get(fi)?;
|
|
let n = ft.notes.iter().find(|n| n.rect.contains(p))?;
|
|
let local = Point::new(p.x - n.origin.x, p.y - n.origin.y);
|
|
Some((n.id, crate::hit(&n.text, 0, local).unwrap_or(0)))
|
|
}
|
|
|
|
/// Caret geometry for byte `pos` of a footnote's story, in frame inner space:
|
|
/// (frame index, x, baseline, ascent, descent).
|
|
pub fn note_caret(cs: &ComposedStory, id: u64, pos: usize) -> Option<(usize, f64, f64, f64, f64)> {
|
|
let (fi, n) = find_note(cs, id)?;
|
|
match crate::caret(&n.text, pos) {
|
|
Some((_, x, bl, a, d)) => Some((fi, x + n.origin.x, bl + n.origin.y, a, d)),
|
|
None => Some((fi, n.origin.x, n.origin.y + 10.0, 9.0, 3.0)),
|
|
}
|
|
}
|