Skip to main content

rdocx_layout/
notes.rs

1//! Footnote and endnote content, laid out once before pagination.
2//!
3//! Notes used to be laid out inside the post-pagination pass that drew them,
4//! which meant pagination could not know how much room they would need and
5//! drew body text straight over them. Laying them out here, ahead of
6//! pagination, lets the paginator reserve exactly the height it will later
7//! draw. Reserve and render read the same lines, so they cannot disagree.
8//!
9//! The marker is shaped here too. The paginator only holds `&FontManager` and
10//! shaping needs `&mut`, so a note that arrives pre-shaped is a note the
11//! paginator can place without touching a font.
12
13use std::collections::HashMap;
14
15use rdocx_oxml::styles::CT_Styles;
16
17use crate::engine::layout_paragraph;
18use crate::input::{LayoutInput, MediaRegistry};
19use crate::style_resolver::NumberingState;
20use oxml_layout::{Color, FontManager, LayoutLine, NoteRef, NoteStream, Result, TextSegment};
21
22/// Point size notes are set at.
23const NOTE_FONT_SIZE: f64 = 8.0;
24/// Horizontal space reserved for the marker, to the left of note text.
25///
26/// Notes are both line-broken and drawn against this, so the two agree.
27pub const NOTE_INDENT: f64 = 12.0;
28/// Vertical gap between the separator rule and the first note line.
29pub const NOTE_SEPARATOR_OFFSET: f64 = 6.0;
30/// Width of the rule above a note that starts on its own page, as a fraction
31/// of the content width.
32pub const SEPARATOR_WIDTH_FRACTION: f64 = 0.33;
33
34/// One note, laid out and ready to place.
35#[derive(Debug, Clone)]
36pub struct NoteLayout {
37    /// The pre-shaped superscript number drawn at the start of the note.
38    pub marker: TextSegment,
39    /// How far above the baseline the marker sits.
40    pub marker_rise: f64,
41    /// The note's lines, flattened across its paragraphs.
42    pub lines: Vec<LayoutLine>,
43}
44
45impl NoteLayout {
46    /// Height of a range of this note's lines.
47    pub fn height_of(&self, first: usize, count: usize) -> f64 {
48        self.lines
49            .iter()
50            .skip(first)
51            .take(count)
52            .map(|line| line.height)
53            .sum()
54    }
55
56    /// Height of every line from `first` onward.
57    pub fn height_from(&self, first: usize) -> f64 {
58        self.height_of(first, self.lines.len())
59    }
60
61    /// Total height of every line.
62    pub fn height(&self) -> f64 {
63        self.height_from(0)
64    }
65}
66
67/// Every note the document defines, laid out once.
68#[derive(Debug, Clone, Default)]
69pub struct NoteRegistry {
70    notes: HashMap<NoteRef, NoteLayout>,
71    continuation_separator: bool,
72}
73
74impl NoteRegistry {
75    /// Lay out every note in the footnote and endnote streams.
76    ///
77    /// `content_width` is the page's content width. Notes are broken at
78    /// `content_width - NOTE_INDENT`, because that is where they are drawn.
79    pub fn build(
80        input: &LayoutInput,
81        styles: &CT_Styles,
82        media: &MediaRegistry,
83        fm: &mut FontManager,
84        num_state: &mut NumberingState,
85        content_width: f64,
86    ) -> Result<Self> {
87        let mut notes = HashMap::new();
88        let mut continuation_separator = false;
89        let note_width = (content_width - NOTE_INDENT).max(1.0);
90
91        // Each stream is keyed separately, so a document numbering a footnote
92        // and an endnote alike keeps both.
93        for (kind, stream) in [
94            (NoteStream::Footnote, input.footnotes.as_ref()),
95            (NoteStream::Endnote, input.endnotes.as_ref()),
96        ]
97        .into_iter()
98        .filter_map(|(kind, stream)| stream.map(|stream| (kind, stream)))
99        {
100            if stream.has_continuation_separator() {
101                continuation_separator = true;
102            }
103
104            for note in &stream.footnotes {
105                // `get_by_id` is the authority on what counts as a real note,
106                // so separators never reach the registry.
107                let key = NoteRef {
108                    stream: kind,
109                    id: note.id,
110                };
111                if stream.get_by_id(note.id).is_none() || notes.contains_key(&key) {
112                    continue;
113                }
114
115                let mut lines = Vec::new();
116                for paragraph in &note.paragraphs {
117                    let block = layout_paragraph(
118                        paragraph, note_width, styles, input, media, fm, num_state,
119                    )?;
120                    lines.extend(block.lines);
121                }
122
123                let Some(marker) = shape_marker(note.id, fm)? else {
124                    continue;
125                };
126
127                notes.insert(
128                    key,
129                    NoteLayout {
130                        marker,
131                        marker_rise: NOTE_FONT_SIZE * 0.33,
132                        lines,
133                    },
134                );
135            }
136        }
137
138        Ok(NoteRegistry {
139            notes,
140            continuation_separator,
141        })
142    }
143
144    pub fn get(&self, note: NoteRef) -> Option<&NoteLayout> {
145        self.notes.get(&note)
146    }
147
148    /// Whether either stream defined the rule drawn above a carried note.
149    pub fn has_continuation_separator(&self) -> bool {
150        self.continuation_separator
151    }
152}
153
154/// Shape a note's number as the superscript marker drawn beside it.
155fn shape_marker(id: i32, fm: &mut FontManager) -> Result<Option<TextSegment>> {
156    let text = id.to_string();
157    let size = NOTE_FONT_SIZE * 0.58;
158
159    let Ok(font_id) = fm.resolve_font(Some("serif"), false, false) else {
160        return Ok(None);
161    };
162    let Ok(shaped) = fm.shape_text(font_id, &text, size) else {
163        return Ok(None);
164    };
165    let metrics = fm.metrics(font_id, size)?;
166
167    Ok(Some(TextSegment {
168        text,
169        font_id,
170        font_size: size,
171        glyph_ids: shaped.glyph_ids,
172        advances: shaped.advances,
173        width: shaped.width,
174        ascent: metrics.ascent,
175        descent: metrics.descent,
176        line_gap: 0.0,
177        color: Color::BLACK,
178        bold: false,
179        italic: false,
180        underline: None,
181        strike: false,
182        dstrike: false,
183        highlight: None,
184        baseline_offset: 0.0,
185        hyperlink_url: None,
186        field_kind: None,
187        note: None,
188    }))
189}