use std::collections::HashMap;
use rdocx_oxml::styles::CT_Styles;
use crate::WordStory;
use crate::block::ParagraphBlock;
use crate::engine::{SourceRegistry, layout_paragraph_with_source_and_direction};
use crate::input::{LayoutInput, MediaRegistry};
use crate::style_resolver::NumberingState;
use oxml_layout::{
Color, Diagnostic, FontManager, LayoutLine, NoteRef, NoteStream, Result, TextDirection,
TextSegment,
};
const NOTE_FONT_SIZE: f64 = 8.0;
pub const NOTE_INDENT: f64 = 12.0;
pub const NOTE_SEPARATOR_OFFSET: f64 = 6.0;
pub const SEPARATOR_WIDTH_FRACTION: f64 = 0.33;
#[derive(Debug, Clone)]
pub struct NoteLayout {
pub marker: TextSegment,
pub marker_rise: f64,
pub lines: Vec<LayoutLine>,
pub revision_ranges: Vec<std::ops::Range<usize>>,
}
impl NoteLayout {
pub fn height_of(&self, first: usize, count: usize) -> f64 {
self.lines
.iter()
.skip(first)
.take(count)
.map(|line| line.height)
.sum()
}
pub fn height_from(&self, first: usize) -> f64 {
self.height_of(first, self.lines.len())
}
pub fn height(&self) -> f64 {
self.height_from(0)
}
}
type NoteKey = (NoteRef, u64);
#[derive(Debug, Clone, Default)]
pub struct NoteRegistry {
notes: HashMap<NoteKey, NoteEntry>,
continuation_separator: bool,
}
#[derive(Debug, Clone)]
struct NoteEntry {
layout: NoteLayout,
paragraphs: Vec<NoteRenderParagraph>,
}
#[derive(Debug, Clone)]
pub(crate) struct NoteRenderParagraph {
pub block: ParagraphBlock,
pub direction: TextDirection,
pub lines: std::ops::Range<usize>,
}
impl NoteRegistry {
pub(crate) fn build(
input: &LayoutInput,
styles: &CT_Styles,
media: &MediaRegistry,
fm: &mut FontManager,
num_state: &mut NumberingState,
content_widths: &[f64],
diagnostics: &mut Vec<Diagnostic>,
sources: Option<&SourceRegistry>,
) -> Result<Self> {
let mut notes = HashMap::new();
let mut continuation_separator = false;
for (kind, stream) in [
(NoteStream::Footnote, input.footnotes.as_ref()),
(NoteStream::Endnote, input.endnotes.as_ref()),
]
.into_iter()
.filter_map(|(kind, stream)| stream.map(|stream| (kind, stream)))
{
if stream.has_continuation_separator() {
continuation_separator = true;
}
for note in &stream.footnotes {
if stream.get_by_id(note.id).is_none() {
continue;
}
let note_ref = NoteRef {
stream: kind,
id: note.id,
};
let counters_before = num_state.clone();
let mut laid_out = false;
for &content_width in content_widths {
let key = (note_ref, content_width.to_bits());
if notes.contains_key(&key) {
continue;
}
if laid_out {
*num_state = counters_before.clone();
}
laid_out = true;
let note_width = (content_width - NOTE_INDENT).max(1.0);
let mut lines = Vec::new();
let mut revision_ranges = Vec::new();
let mut render_paragraphs = Vec::new();
let story = match kind {
NoteStream::Footnote => WordStory::Footnote { id: note.id },
NoteStream::Endnote => WordStory::Endnote { id: note.id },
};
for (paragraph_index, paragraph) in note.paragraphs.iter().enumerate() {
let source =
sources.and_then(|sources| sources.id(&story, &[paragraph_index]));
let (block, direction) = layout_paragraph_with_source_and_direction(
paragraph,
note_width,
styles,
input,
media,
fm,
num_state,
diagnostics,
source,
)?;
let first = lines.len();
if block.has_visible_revision && !block.lines.is_empty() {
revision_ranges.push(first..first + block.lines.len());
}
lines.extend(block.lines.iter().cloned());
let last = lines.len();
render_paragraphs.push(NoteRenderParagraph {
block,
direction,
lines: first..last,
});
}
let Some(marker) = shape_marker(note.id, fm)? else {
continue;
};
notes.insert(
key,
NoteEntry {
layout: NoteLayout {
marker,
marker_rise: NOTE_FONT_SIZE * 0.33,
lines,
revision_ranges,
},
paragraphs: render_paragraphs,
},
);
}
}
}
Ok(NoteRegistry {
notes,
continuation_separator,
})
}
pub fn get(&self, note: NoteRef, content_width: f64) -> Option<&NoteLayout> {
self.notes
.get(&(note, content_width.to_bits()))
.map(|entry| &entry.layout)
}
pub(crate) fn get_render(
&self,
note: NoteRef,
content_width: f64,
) -> Option<(&NoteLayout, &[NoteRenderParagraph])> {
self.notes
.get(&(note, content_width.to_bits()))
.map(|entry| (&entry.layout, entry.paragraphs.as_slice()))
}
pub fn has_continuation_separator(&self) -> bool {
self.continuation_separator
}
}
fn shape_marker(id: i32, fm: &mut FontManager) -> Result<Option<TextSegment>> {
let text = id.to_string();
let size = NOTE_FONT_SIZE * 0.58;
let Ok(font_id) = fm.resolve_font(Some("serif"), false, false) else {
return Ok(None);
};
let Ok(shaped) = fm.shape_text(font_id, &text, size) else {
return Ok(None);
};
let metrics = fm.metrics(font_id, size)?;
Ok(Some(TextSegment {
text,
direction: oxml_layout::TextDirection::Auto,
source: None,
font_id,
font_size: size,
glyph_ids: shaped.glyph_ids,
advances: shaped.advances,
width: shaped.width,
ascent: metrics.ascent,
descent: metrics.descent,
line_gap: 0.0,
color: Color::BLACK,
bold: false,
italic: false,
underline: None,
strike: false,
dstrike: false,
highlight: None,
baseline_offset: 0.0,
hyperlink_url: None,
field_kind: None,
note: None,
}))
}
#[cfg(test)]
mod tests {
use super::*;
use rdocx_oxml::footnotes::{CT_Footnote, CT_Footnotes, NoteType};
use rdocx_oxml::text::CT_P;
fn input_with_one_note() -> LayoutInput {
let mut note = CT_P::new();
note.add_run(
"A note long enough that the measure it is broken to decides how \
many lines it occupies rather than leaving it on a single line.",
);
LayoutInput {
revision_view: crate::input::RevisionView::Accepted,
automatic_hyphenation: false,
document: rdocx_oxml::document::CT_Document::new(),
styles: CT_Styles::new_default(),
numbering: None,
headers: HashMap::new(),
footers: HashMap::new(),
images: HashMap::new(),
charts: HashMap::new(),
chart_theme: oxml_drawing::theme::CT_OfficeStyleSheet::office_default(),
chart_color_map: oxml_drawing::color::ColorMap::default(),
core_properties: None,
hyperlink_urls: HashMap::new(),
footnotes: Some(CT_Footnotes {
footnotes: vec![CT_Footnote {
id: 1,
note_type: NoteType::Normal,
paragraphs: vec![note],
}],
}),
endnotes: None,
theme: None,
fonts: Vec::new(),
}
}
fn build_at(widths: &[f64]) -> NoteRegistry {
let input = input_with_one_note();
let media = MediaRegistry::new(&HashMap::new());
let mut fm = FontManager::new();
let mut num_state = NumberingState::new();
let mut diagnostics = Vec::new();
NoteRegistry::build(
&input,
&input.styles,
&media,
&mut fm,
&mut num_state,
widths,
&mut diagnostics,
None,
)
.expect("the registry builds")
}
const NOTE_ONE: NoteRef = NoteRef {
stream: NoteStream::Footnote,
id: 1,
};
#[test]
fn the_registry_lays_a_note_out_once_per_distinct_width() {
let registry = build_at(&[468.0, 1044.0]);
let narrow = registry.get(NOTE_ONE, 468.0).expect("narrow is registered");
let wide = registry.get(NOTE_ONE, 1044.0).expect("wide is registered");
assert!(
wide.lines.len() < narrow.lines.len(),
"one layout was reused for both widths, {} lines against {}",
wide.lines.len(),
narrow.lines.len()
);
}
#[test]
fn a_repeated_width_is_registered_once_and_still_found() {
let repeated = build_at(&[468.0, 468.0]);
let once = build_at(&[468.0]);
let from_repeated = repeated.get(NOTE_ONE, 468.0).expect("still registered");
let from_once = once.get(NOTE_ONE, 468.0).expect("registered");
assert_eq!(from_repeated.lines.len(), from_once.lines.len());
}
#[test]
fn an_unregistered_width_has_no_layout() {
let registry = build_at(&[468.0]);
assert!(registry.get(NOTE_ONE, 1044.0).is_none());
}
}