Skip to main content

normordis_pdf/elements/
fixed_text.rs

1use super::{
2    paragraph::ParagraphContent,
3    Element, LayoutMode, RenderContext,
4};
5use crate::{
6    layout::{FixedBox, OverflowPolicy, TextAlign},
7    richtext::marks::TextRun,
8};
9
10/// Vertical alignment of content within a `FixedTextBox` or table cell.
11#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum VerticalAlign {
14    #[default]
15    Top,
16    Middle,
17    Bottom,
18}
19
20/// A text element rendered inside a fixed rectangular area.
21///
22/// Does not participate in `PageFlow` — `layout_mode()` returns
23/// `LayoutMode::Fixed`.
24#[derive(Debug, Clone)]
25pub struct FixedTextBox {
26    pub text_box: FixedBox,
27    pub content: ParagraphContent,
28    pub alignment: TextAlign,
29    /// Override the body font size.  `None` uses `DocumentStyle.font_size_body`.
30    pub font_size: Option<f64>,
31    pub vertical_align: VerticalAlign,
32}
33
34impl FixedTextBox {
35    /// Y starting position of the first text line given the rendered content height.
36    pub fn content_y_start_mm(&self, content_height_mm: f64) -> f64 {
37        let inner_h = self.text_box.inner_height_mm();
38        match self.vertical_align {
39            VerticalAlign::Top => self.text_box.inner_y_top_mm(),
40            VerticalAlign::Middle => {
41                self.text_box.inner_y_top_mm() - ((inner_h - content_height_mm) / 2.0).max(0.0)
42            }
43            VerticalAlign::Bottom => {
44                self.text_box.y_mm + self.text_box.padding_mm + content_height_mm
45            }
46        }
47    }
48
49    /// Effective font size after applying `Shrink` overflow policy.
50    pub fn effective_font_size(&self, ctx: &RenderContext) -> f64 {
51        let base_fs = self.font_size.unwrap_or(ctx.style.font_size_body);
52        if self.text_box.overflow != OverflowPolicy::Shrink {
53            return base_fs;
54        }
55        let inner_w = self.text_box.inner_width_mm().max(1.0);
56        let inner_h = self.text_box.inner_height_mm();
57        let runs = self.content_runs();
58        let mut fs = base_fs;
59        loop {
60            let r = ctx.layout_engine.layout_runs(&ctx.fonts, &runs, inner_w, self.alignment, fs, &[]);
61            if r.total_height_mm <= inner_h || fs <= 6.0 {
62                return fs;
63            }
64            fs = (fs - 0.5).max(6.0);
65        }
66    }
67
68    fn content_runs(&self) -> Vec<TextRun> {
69        match &self.content {
70            ParagraphContent::Plain(text) => vec![TextRun::plain(text)],
71            ParagraphContent::Runs(runs) => runs.clone(),
72        }
73    }
74}
75
76impl Element for FixedTextBox {
77    fn layout_mode(&self) -> LayoutMode {
78        LayoutMode::Fixed(self.text_box.clone())
79    }
80
81    fn estimated_height_mm(&self) -> f64 {
82        0.0
83    }
84
85    fn render(&self, ctx: &mut RenderContext) -> crate::Result<super::RenderResult> {
86        let inner_w = self.text_box.inner_width_mm().max(1.0);
87        let runs = self.content_runs();
88        let effective_fs = self.effective_font_size(ctx);
89        let ua = ctx.ua_config.enabled;
90
91        // UA-2: tag or mark as Artifact based on ua_role
92        if ua {
93            match &self.text_box.ua_role {
94                Some(tag) => {
95                    let mcid = ctx.ua_tag_element(tag.clone(), self.text_box.ua_alt.clone());
96                    ctx.backend.begin_tagged_content(tag.pdf_name().as_bytes(), mcid);
97                }
98                None => {
99                    ctx.backend.begin_artifact_content();
100                }
101            }
102        }
103
104        let result = ctx.layout_engine.layout_runs(
105            &ctx.fonts, &runs, inner_w, self.alignment, effective_fs, &[],
106        );
107        let y_start = self.content_y_start_mm(result.total_height_mm);
108
109        let tc = ctx.style.text_color.clone();
110        let line_h = ctx.layout_engine.line_height_mm(&ctx.fonts, effective_fs);
111        let bottom_y = self.text_box.y_mm + self.text_box.padding_mm;
112        let mut y = y_start;
113
114        for line in &result.lines {
115            if self.text_box.overflow == OverflowPolicy::Truncate && y - line_h < bottom_y {
116                break;
117            }
118            for seg in &line.segments {
119                if seg.text.is_empty() {
120                    continue;
121                }
122                let Some(font_ref) = ctx.get_font_ref(seg.style.bold, seg.style.italic) else {
123                    continue;
124                };
125                let x = self.text_box.inner_x_mm() + seg.x_offset_mm;
126                ctx.draw_text(&seg.text, x, y, effective_fs, font_ref, &tc)?;
127            }
128            y -= line_h;
129        }
130
131        if ua { ctx.backend.end_tagged_content(); }
132        Ok(super::RenderResult::done())
133    }
134}