1use 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
22const NOTE_FONT_SIZE: f64 = 8.0;
24pub const NOTE_INDENT: f64 = 12.0;
28pub const NOTE_SEPARATOR_OFFSET: f64 = 6.0;
30pub const SEPARATOR_WIDTH_FRACTION: f64 = 0.33;
33
34#[derive(Debug, Clone)]
36pub struct NoteLayout {
37 pub marker: TextSegment,
39 pub marker_rise: f64,
41 pub lines: Vec<LayoutLine>,
43}
44
45impl NoteLayout {
46 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 pub fn height_from(&self, first: usize) -> f64 {
58 self.height_of(first, self.lines.len())
59 }
60
61 pub fn height(&self) -> f64 {
63 self.height_from(0)
64 }
65}
66
67#[derive(Debug, Clone, Default)]
69pub struct NoteRegistry {
70 notes: HashMap<NoteRef, NoteLayout>,
71 continuation_separator: bool,
72}
73
74impl NoteRegistry {
75 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 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 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 ¬e.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(¬e)
146 }
147
148 pub fn has_continuation_separator(&self) -> bool {
150 self.continuation_separator
151 }
152}
153
154fn 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}