normordis_pdf/elements/
fixed_text.rs1use super::{
2 paragraph::ParagraphContent,
3 Element, LayoutMode, RenderContext,
4};
5use crate::{
6 layout::{FixedBox, OverflowPolicy, TextAlign},
7 richtext::marks::TextRun,
8};
9
10#[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#[derive(Debug, Clone)]
25pub struct FixedTextBox {
26 pub text_box: FixedBox,
27 pub content: ParagraphContent,
28 pub alignment: TextAlign,
29 pub font_size: Option<f64>,
31 pub vertical_align: VerticalAlign,
32}
33
34impl FixedTextBox {
35 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 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 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}