use super::{Element, RenderContext, RenderResult};
use crate::styles::RgbColor;
pub(crate) const FOOTNOTE_SEPARATOR_HEIGHT_MM: f64 = 2.5;
pub const FOOTNOTE_SEPARATOR_THICKNESS_MM: f64 = 0.25;
#[derive(Debug, Clone, Copy, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FootnoteMarkStyle {
#[default]
Numeric,
Alpha,
Symbol,
}
impl FootnoteMarkStyle {
pub fn mark_text(self, number: u32) -> String {
match self {
FootnoteMarkStyle::Numeric => number.to_string(),
FootnoteMarkStyle::Alpha => {
let idx = ((number.saturating_sub(1)) % 26) as u8;
char::from(b'a' + idx).to_string()
}
FootnoteMarkStyle::Symbol => {
let symbols = ['*', '†', '‡', '§', '¶', '#'];
let idx = ((number.saturating_sub(1)) % 6) as usize;
symbols[idx].to_string()
}
}
}
}
#[derive(Debug, Clone)]
pub struct FootnoteRef {
pub number: u32,
pub mark_style: FootnoteMarkStyle,
}
impl FootnoteRef {
pub fn new(number: u32) -> Self {
Self { number, mark_style: FootnoteMarkStyle::Numeric }
}
pub fn with_style(mut self, style: FootnoteMarkStyle) -> Self {
self.mark_style = style;
self
}
pub fn mark_text(&self) -> String {
self.mark_style.mark_text(self.number)
}
}
impl Element for FootnoteRef {
fn estimated_height_mm(&self) -> f64 {
0.0
}
fn render(&self, ctx: &mut RenderContext) -> crate::Result<RenderResult> {
let mark = self.mark_text();
let p = crate::elements::paragraph::Paragraph::new(mark)
.font_size(ctx.style.font_size_small * 0.75);
p.render(ctx)
}
}
#[derive(Default)]
pub struct FootnoteAccumulator {
pub pending: Vec<(u32, Vec<String>)>,
pub reserved_height_mm: f64,
}
impl FootnoteAccumulator {
pub fn new() -> Self {
Self::default()
}
pub fn reserve(&mut self, number: u32, texts: Vec<String>, line_height_mm: f64) -> f64 {
let n_lines = texts.iter().map(|t| {
((t.len() as f64 / 60.0).ceil() as usize).max(1)
}).sum::<usize>();
let text_h = n_lines as f64 * line_height_mm;
let extra = if self.pending.is_empty() { FOOTNOTE_SEPARATOR_HEIGHT_MM } else { 0.0 };
let height = extra + text_h;
self.reserved_height_mm += height;
self.pending.push((number, texts));
height
}
pub fn render_pending(&mut self, ctx: &mut RenderContext) -> crate::Result<()> {
if self.pending.is_empty() {
return Ok(());
}
let footnote_top_y = ctx.layout.margin_bottom_mm + self.reserved_height_mm;
ctx.flow.cursor_y_mm = footnote_top_y;
render_separator(ctx);
ctx.flow.cursor_y_mm -= FOOTNOTE_SEPARATOR_HEIGHT_MM;
let pending = std::mem::take(&mut self.pending);
for (number, texts) in &pending {
for (i, text) in texts.iter().enumerate() {
let line = if i == 0 {
format!("{number}. {text}")
} else {
format!(" {text}")
};
let p = crate::elements::paragraph::Paragraph::new(line)
.style("footnote");
p.render(ctx)?;
}
}
self.pending.clear();
self.reserved_height_mm = 0.0;
Ok(())
}
pub fn is_empty(&self) -> bool {
self.pending.is_empty()
}
pub fn clear(&mut self) {
self.pending.clear();
self.reserved_height_mm = 0.0;
}
}
fn render_separator(ctx: &mut RenderContext) {
let x0 = ctx.layout.content_x_mm;
let x1 = x0 + ctx.layout.content_width_mm / 3.0;
let y = ctx.flow.cursor_y_mm - FOOTNOTE_SEPARATOR_HEIGHT_MM * 0.5;
let width_pt = (FOOTNOTE_SEPARATOR_THICKNESS_MM / 25.4 * 72.0) as f32;
let color = RgbColor { r: 0.4, g: 0.4, b: 0.4 };
if ctx.ua_config.enabled { ctx.backend.begin_artifact_content(); }
let _ = ctx.backend.draw_line(x0, y, x1, y, width_pt, &color);
if ctx.ua_config.enabled { ctx.backend.end_tagged_content(); }
}