1use std::collections::HashMap;
14
15use rdocx_oxml::styles::CT_Styles;
16
17use crate::WordStory;
18use crate::block::ParagraphBlock;
19use crate::engine::{SourceRegistry, layout_paragraph_with_source_and_direction};
20use crate::input::{LayoutInput, MediaRegistry};
21use crate::style_resolver::NumberingState;
22use oxml_layout::{
23 Color, Diagnostic, FontManager, LayoutLine, NoteRef, NoteStream, Result, TextDirection,
24 TextSegment,
25};
26
27const NOTE_FONT_SIZE: f64 = 8.0;
29pub const NOTE_INDENT: f64 = 12.0;
33pub const NOTE_SEPARATOR_OFFSET: f64 = 6.0;
35pub const SEPARATOR_WIDTH_FRACTION: f64 = 0.33;
38
39#[derive(Debug, Clone)]
41pub struct NoteLayout {
42 pub marker: TextSegment,
44 pub marker_rise: f64,
46 pub lines: Vec<LayoutLine>,
48 pub revision_ranges: Vec<std::ops::Range<usize>>,
50}
51
52impl NoteLayout {
53 pub fn height_of(&self, first: usize, count: usize) -> f64 {
55 self.lines
56 .iter()
57 .skip(first)
58 .take(count)
59 .map(|line| line.height)
60 .sum()
61 }
62
63 pub fn height_from(&self, first: usize) -> f64 {
65 self.height_of(first, self.lines.len())
66 }
67
68 pub fn height(&self) -> f64 {
70 self.height_from(0)
71 }
72}
73
74type NoteKey = (NoteRef, u64);
81
82#[derive(Debug, Clone, Default)]
84pub struct NoteRegistry {
85 notes: HashMap<NoteKey, NoteEntry>,
86 continuation_separator: bool,
87}
88
89#[derive(Debug, Clone)]
90struct NoteEntry {
91 layout: NoteLayout,
92 paragraphs: Vec<NoteRenderParagraph>,
93}
94
95#[derive(Debug, Clone)]
96pub(crate) struct NoteRenderParagraph {
97 pub block: ParagraphBlock,
98 pub direction: TextDirection,
99 pub lines: std::ops::Range<usize>,
100}
101
102impl NoteRegistry {
103 pub(crate) fn build(
112 input: &LayoutInput,
113 styles: &CT_Styles,
114 media: &MediaRegistry,
115 fm: &mut FontManager,
116 num_state: &mut NumberingState,
117 content_widths: &[f64],
118 diagnostics: &mut Vec<Diagnostic>,
119 sources: Option<&SourceRegistry>,
120 ) -> Result<Self> {
121 let mut notes = HashMap::new();
122 let mut continuation_separator = false;
123
124 for (kind, stream) in [
127 (NoteStream::Footnote, input.footnotes.as_ref()),
128 (NoteStream::Endnote, input.endnotes.as_ref()),
129 ]
130 .into_iter()
131 .filter_map(|(kind, stream)| stream.map(|stream| (kind, stream)))
132 {
133 if stream.has_continuation_separator() {
134 continuation_separator = true;
135 }
136
137 for note in &stream.footnotes {
138 if stream.get_by_id(note.id).is_none() {
141 continue;
142 }
143 let note_ref = NoteRef {
144 stream: kind,
145 id: note.id,
146 };
147
148 let counters_before = num_state.clone();
154 let mut laid_out = false;
155
156 for &content_width in content_widths {
157 let key = (note_ref, content_width.to_bits());
158 if notes.contains_key(&key) {
159 continue;
160 }
161 if laid_out {
162 *num_state = counters_before.clone();
163 }
164 laid_out = true;
165 let note_width = (content_width - NOTE_INDENT).max(1.0);
166
167 let mut lines = Vec::new();
168 let mut revision_ranges = Vec::new();
169 let mut render_paragraphs = Vec::new();
170 let story = match kind {
171 NoteStream::Footnote => WordStory::Footnote { id: note.id },
172 NoteStream::Endnote => WordStory::Endnote { id: note.id },
173 };
174 for (paragraph_index, paragraph) in note.paragraphs.iter().enumerate() {
175 let source =
176 sources.and_then(|sources| sources.id(&story, &[paragraph_index]));
177 let (block, direction) = layout_paragraph_with_source_and_direction(
178 paragraph,
179 note_width,
180 styles,
181 input,
182 media,
183 fm,
184 num_state,
185 diagnostics,
186 source,
187 )?;
188 let first = lines.len();
189 if block.has_visible_revision && !block.lines.is_empty() {
190 revision_ranges.push(first..first + block.lines.len());
191 }
192 lines.extend(block.lines.iter().cloned());
193 let last = lines.len();
194 render_paragraphs.push(NoteRenderParagraph {
195 block,
196 direction,
197 lines: first..last,
198 });
199 }
200
201 let Some(marker) = shape_marker(note.id, fm)? else {
202 continue;
203 };
204
205 notes.insert(
206 key,
207 NoteEntry {
208 layout: NoteLayout {
209 marker,
210 marker_rise: NOTE_FONT_SIZE * 0.33,
211 lines,
212 revision_ranges,
213 },
214 paragraphs: render_paragraphs,
215 },
216 );
217 }
218 }
219 }
220
221 Ok(NoteRegistry {
222 notes,
223 continuation_separator,
224 })
225 }
226
227 pub fn get(&self, note: NoteRef, content_width: f64) -> Option<&NoteLayout> {
229 self.notes
230 .get(&(note, content_width.to_bits()))
231 .map(|entry| &entry.layout)
232 }
233
234 pub(crate) fn get_render(
235 &self,
236 note: NoteRef,
237 content_width: f64,
238 ) -> Option<(&NoteLayout, &[NoteRenderParagraph])> {
239 self.notes
240 .get(&(note, content_width.to_bits()))
241 .map(|entry| (&entry.layout, entry.paragraphs.as_slice()))
242 }
243
244 pub fn has_continuation_separator(&self) -> bool {
246 self.continuation_separator
247 }
248}
249
250fn shape_marker(id: i32, fm: &mut FontManager) -> Result<Option<TextSegment>> {
252 let text = id.to_string();
253 let size = NOTE_FONT_SIZE * 0.58;
254
255 let Ok(font_id) = fm.resolve_font(Some("serif"), false, false) else {
256 return Ok(None);
257 };
258 let Ok(shaped) = fm.shape_text(font_id, &text, size) else {
259 return Ok(None);
260 };
261 let metrics = fm.metrics(font_id, size)?;
262
263 Ok(Some(TextSegment {
264 text,
265 direction: oxml_layout::TextDirection::Auto,
266 source: None,
267 font_id,
268 font_size: size,
269 glyph_ids: shaped.glyph_ids,
270 advances: shaped.advances,
271 width: shaped.width,
272 ascent: metrics.ascent,
273 descent: metrics.descent,
274 line_gap: 0.0,
275 color: Color::BLACK,
276 bold: false,
277 italic: false,
278 underline: None,
279 strike: false,
280 dstrike: false,
281 highlight: None,
282 baseline_offset: 0.0,
283 hyperlink_url: None,
284 field_kind: None,
285 note: None,
286 }))
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use rdocx_oxml::footnotes::{CT_Footnote, CT_Footnotes, NoteType};
293 use rdocx_oxml::text::CT_P;
294
295 fn input_with_one_note() -> LayoutInput {
298 let mut note = CT_P::new();
299 note.add_run(
300 "A note long enough that the measure it is broken to decides how \
301 many lines it occupies rather than leaving it on a single line.",
302 );
303
304 LayoutInput {
305 revision_view: crate::input::RevisionView::Accepted,
306 automatic_hyphenation: false,
307 math_properties: None,
308 document: rdocx_oxml::document::CT_Document::new(),
309 styles: CT_Styles::new_default(),
310 numbering: None,
311 headers: HashMap::new(),
312 footers: HashMap::new(),
313 images: HashMap::new(),
314 charts: HashMap::new(),
315 chart_theme: oxml_drawing::theme::CT_OfficeStyleSheet::office_default(),
316 chart_color_map: oxml_drawing::color::ColorMap::default(),
317 core_properties: None,
318 hyperlink_urls: HashMap::new(),
319 footnotes: Some(CT_Footnotes {
320 footnotes: vec![CT_Footnote {
321 id: 1,
322 note_type: NoteType::Normal,
323 paragraphs: vec![note],
324 }],
325 }),
326 endnotes: None,
327 theme: None,
328 fonts: Vec::new(),
329 }
330 }
331
332 fn build_at(widths: &[f64]) -> NoteRegistry {
333 let input = input_with_one_note();
334 let media = MediaRegistry::new(&HashMap::new());
335 let mut fm = FontManager::new();
336 let mut num_state = NumberingState::new();
337 let mut diagnostics = Vec::new();
338 NoteRegistry::build(
339 &input,
340 &input.styles,
341 &media,
342 &mut fm,
343 &mut num_state,
344 widths,
345 &mut diagnostics,
346 None,
347 )
348 .expect("the registry builds")
349 }
350
351 const NOTE_ONE: NoteRef = NoteRef {
352 stream: NoteStream::Footnote,
353 id: 1,
354 };
355
356 #[test]
357 fn the_registry_lays_a_note_out_once_per_distinct_width() {
358 let registry = build_at(&[468.0, 1044.0]);
359
360 let narrow = registry.get(NOTE_ONE, 468.0).expect("narrow is registered");
361 let wide = registry.get(NOTE_ONE, 1044.0).expect("wide is registered");
362
363 assert!(
364 wide.lines.len() < narrow.lines.len(),
365 "one layout was reused for both widths, {} lines against {}",
366 wide.lines.len(),
367 narrow.lines.len()
368 );
369 }
370
371 #[test]
372 fn a_repeated_width_is_registered_once_and_still_found() {
373 let repeated = build_at(&[468.0, 468.0]);
374 let once = build_at(&[468.0]);
375
376 let from_repeated = repeated.get(NOTE_ONE, 468.0).expect("still registered");
377 let from_once = once.get(NOTE_ONE, 468.0).expect("registered");
378 assert_eq!(from_repeated.lines.len(), from_once.lines.len());
379 }
380
381 #[test]
382 fn an_unregistered_width_has_no_layout() {
383 let registry = build_at(&[468.0]);
387 assert!(registry.get(NOTE_ONE, 1044.0).is_none());
388 }
389}