1use std::sync::Arc;
25
26use azul_css::props::basic::ColorU;
27
28#[derive(Clone)]
33pub enum MarginBoxContent {
34 None,
36 RunningElement(String),
38 NamedString(String),
40 PageCounter,
42 PagesCounter,
44 PageCounterFormatted { format: CounterFormat },
46 Combined(Vec<MarginBoxContent>),
48 Text(String),
50 Custom(Arc<dyn Fn(PageInfo) -> String + Send + Sync>),
52}
53
54impl std::fmt::Debug for MarginBoxContent {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 match self {
57 Self::None => write!(f, "None"),
58 Self::RunningElement(s) => f.debug_tuple("RunningElement").field(s).finish(),
59 Self::NamedString(s) => f.debug_tuple("NamedString").field(s).finish(),
60 Self::PageCounter => write!(f, "PageCounter"),
61 Self::PagesCounter => write!(f, "PagesCounter"),
62 Self::PageCounterFormatted { format } => f
63 .debug_struct("PageCounterFormatted")
64 .field("format", format)
65 .finish(),
66 Self::Combined(v) => f.debug_tuple("Combined").field(v).finish(),
67 Self::Text(s) => f.debug_tuple("Text").field(s).finish(),
68 Self::Custom(_) => write!(f, "Custom(<fn>)"),
69 }
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum CounterFormat {
76 Decimal,
77 DecimalLeadingZero,
78 LowerRoman,
79 UpperRoman,
80 LowerAlpha,
81 UpperAlpha,
82 LowerGreek,
83}
84
85impl Default for CounterFormat {
86 fn default() -> Self {
87 Self::Decimal
88 }
89}
90
91impl CounterFormat {
92 #[must_use] pub fn format(&self, n: usize) -> String {
94 use super::counters::{to_alphabetic, to_greek, to_roman};
95 match self {
96 Self::Decimal => n.to_string(),
97 Self::DecimalLeadingZero => format!("{n:02}"),
98 Self::LowerRoman => to_roman(n, false),
99 Self::UpperRoman => to_roman(n, true),
100 Self::LowerAlpha => to_alphabetic(n, false),
101 Self::UpperAlpha => to_alphabetic(n, true),
102 Self::LowerGreek => to_greek(n, false),
103 }
104 }
105}
106
107#[derive(Debug, Clone, Copy)]
109#[allow(clippy::struct_excessive_bools)] pub struct PageInfo {
111 pub page_number: usize,
113 pub total_pages: usize,
115 pub is_first: bool,
117 pub is_last: bool,
119 pub is_left: bool,
121 pub is_right: bool,
123 pub is_blank: bool,
125}
126
127impl PageInfo {
128 #[must_use] pub const fn new(page_number: usize, total_pages: usize) -> Self {
130 Self {
131 page_number,
132 total_pages,
133 is_first: page_number == 1,
134 is_last: total_pages > 0 && page_number == total_pages,
135 is_left: page_number.is_multiple_of(2), is_right: page_number % 2 == 1, is_blank: false,
138 }
139 }
140}
141
142const DEFAULT_HEADER_FOOTER_HEIGHT: f32 = 30.0;
144
145const DEFAULT_HEADER_FOOTER_FONT_SIZE: f32 = 10.0;
147
148#[derive(Debug, Clone)]
153pub struct HeaderFooterConfig {
154 pub show_header: bool,
156 pub show_footer: bool,
158 pub header_height: f32,
160 pub footer_height: f32,
162 pub header_content: MarginBoxContent,
164 pub footer_content: MarginBoxContent,
166 pub font_size: f32,
168 pub text_color: ColorU,
170 pub skip_first_page: bool,
172}
173
174impl Default for HeaderFooterConfig {
175 fn default() -> Self {
176 Self {
177 show_header: false,
178 show_footer: false,
179 header_height: DEFAULT_HEADER_FOOTER_HEIGHT,
180 footer_height: DEFAULT_HEADER_FOOTER_HEIGHT,
181 header_content: MarginBoxContent::None,
182 footer_content: MarginBoxContent::None,
183 font_size: DEFAULT_HEADER_FOOTER_FONT_SIZE,
184 text_color: ColorU {
185 r: 0,
186 g: 0,
187 b: 0,
188 a: 255,
189 },
190 skip_first_page: false,
191 }
192 }
193}
194
195impl HeaderFooterConfig {
196 #[must_use] pub fn with_page_numbers() -> Self {
198 Self {
199 show_footer: true,
200 footer_content: MarginBoxContent::Combined(vec![
201 MarginBoxContent::Text("Page ".to_string()),
202 MarginBoxContent::PageCounter,
203 MarginBoxContent::Text(" of ".to_string()),
204 MarginBoxContent::PagesCounter,
205 ]),
206 ..Default::default()
207 }
208 }
209
210 #[must_use] pub fn with_header_and_footer_page_numbers() -> Self {
212 Self {
213 show_header: true,
214 show_footer: true,
215 header_content: MarginBoxContent::Combined(vec![
216 MarginBoxContent::Text("Page ".to_string()),
217 MarginBoxContent::PageCounter,
218 ]),
219 footer_content: MarginBoxContent::Combined(vec![
220 MarginBoxContent::Text("Page ".to_string()),
221 MarginBoxContent::PageCounter,
222 MarginBoxContent::Text(" of ".to_string()),
223 MarginBoxContent::PagesCounter,
224 ]),
225 ..Default::default()
226 }
227 }
228
229 #[must_use]
231 pub fn with_header_text(mut self, text: impl Into<String>) -> Self {
232 self.show_header = true;
233 self.header_content = MarginBoxContent::Text(text.into());
234 self
235 }
236
237 #[must_use]
239 pub fn with_footer_text(mut self, text: impl Into<String>) -> Self {
240 self.show_footer = true;
241 self.footer_content = MarginBoxContent::Text(text.into());
242 self
243 }
244
245 #[allow(clippy::only_used_in_recursion)]
249 #[must_use] pub fn generate_content(&self, content: &MarginBoxContent, info: PageInfo) -> String {
250 match content {
251 MarginBoxContent::None => String::new(),
252 MarginBoxContent::Text(s) => s.clone(),
253 MarginBoxContent::PageCounter => info.page_number.to_string(),
254 MarginBoxContent::PagesCounter => {
255 if info.total_pages > 0 {
256 info.total_pages.to_string()
257 } else {
258 "?".to_string()
259 }
260 }
261 MarginBoxContent::PageCounterFormatted { format } => format.format(info.page_number),
262 MarginBoxContent::Combined(parts) => parts
263 .iter()
264 .map(|p| self.generate_content(p, info))
265 .collect(),
266 MarginBoxContent::NamedString(name) => {
267 format!("[string:{name}]")
269 }
270 MarginBoxContent::RunningElement(name) => {
271 format!("[element:{name}]")
273 }
274 MarginBoxContent::Custom(f) => f(info),
275 }
276 }
277
278 #[must_use] pub fn header_text(&self, info: PageInfo) -> String {
280 if !self.show_header {
281 return String::new();
282 }
283 if self.skip_first_page && info.is_first {
284 return String::new();
285 }
286 self.generate_content(&self.header_content, info)
287 }
288
289 #[must_use] pub fn footer_text(&self, info: PageInfo) -> String {
291 if !self.show_footer {
292 return String::new();
293 }
294 if self.skip_first_page && info.is_first {
295 return String::new();
296 }
297 self.generate_content(&self.footer_content, info)
298 }
299}
300
301#[derive(Debug, Clone)]
326#[allow(clippy::struct_excessive_bools)] pub struct FakePageConfig {
328 pub show_header: bool,
330 pub show_footer: bool,
332 pub header_text: Option<String>,
334 pub footer_text: Option<String>,
336 pub header_page_number: bool,
338 pub footer_page_number: bool,
340 pub header_total_pages: bool,
342 pub footer_total_pages: bool,
344 pub number_format: CounterFormat,
346 pub skip_first_page: bool,
348 pub header_height: f32,
350 pub footer_height: f32,
352 pub font_size: f32,
354 pub text_color: ColorU,
356}
357
358impl Default for FakePageConfig {
359 fn default() -> Self {
360 Self {
361 show_header: false,
362 show_footer: false,
363 header_text: None,
364 footer_text: None,
365 header_page_number: false,
366 footer_page_number: false,
367 header_total_pages: false,
368 footer_total_pages: false,
369 number_format: CounterFormat::Decimal,
370 skip_first_page: false,
371 header_height: DEFAULT_HEADER_FOOTER_HEIGHT,
372 footer_height: DEFAULT_HEADER_FOOTER_HEIGHT,
373 font_size: DEFAULT_HEADER_FOOTER_FONT_SIZE,
374 text_color: ColorU {
375 r: 0,
376 g: 0,
377 b: 0,
378 a: 255,
379 },
380 }
381 }
382}
383
384impl FakePageConfig {
385 #[must_use] pub fn new() -> Self {
387 Self::default()
388 }
389
390 #[must_use] pub const fn with_footer_page_numbers(mut self) -> Self {
392 self.show_footer = true;
393 self.footer_page_number = true;
394 self.footer_total_pages = true;
395 self
396 }
397
398 #[must_use] pub const fn with_header_page_numbers(mut self) -> Self {
400 self.show_header = true;
401 self.header_page_number = true;
402 self
403 }
404
405 #[must_use] pub const fn with_header_and_footer_page_numbers(mut self) -> Self {
407 self.show_header = true;
408 self.show_footer = true;
409 self.header_page_number = true;
410 self.footer_page_number = true;
411 self.footer_total_pages = true;
412 self
413 }
414
415 #[must_use]
417 pub fn with_header_text(mut self, text: impl Into<String>) -> Self {
418 self.show_header = true;
419 self.header_text = Some(text.into());
420 self
421 }
422
423 #[must_use]
425 pub fn with_footer_text(mut self, text: impl Into<String>) -> Self {
426 self.show_footer = true;
427 self.footer_text = Some(text.into());
428 self
429 }
430
431 #[must_use] pub const fn with_number_format(mut self, format: CounterFormat) -> Self {
433 self.number_format = format;
434 self
435 }
436
437 #[must_use] pub const fn skip_first_page(mut self, skip: bool) -> Self {
439 self.skip_first_page = skip;
440 self
441 }
442
443 #[must_use] pub const fn with_header_height(mut self, height: f32) -> Self {
445 self.header_height = height;
446 self
447 }
448
449 #[must_use] pub const fn with_footer_height(mut self, height: f32) -> Self {
451 self.footer_height = height;
452 self
453 }
454
455 #[must_use] pub const fn with_font_size(mut self, size: f32) -> Self {
457 self.font_size = size;
458 self
459 }
460
461 #[must_use] pub const fn with_text_color(mut self, color: ColorU) -> Self {
463 self.text_color = color;
464 self
465 }
466
467 #[must_use] pub fn to_header_footer_config(&self) -> HeaderFooterConfig {
472 HeaderFooterConfig {
473 show_header: self.show_header,
474 show_footer: self.show_footer,
475 header_height: self.header_height,
476 footer_height: self.footer_height,
477 header_content: self.build_header_content(),
478 footer_content: self.build_footer_content(),
479 skip_first_page: self.skip_first_page,
480 font_size: self.font_size,
481 text_color: self.text_color,
482 }
483 }
484
485 fn build_header_content(&self) -> MarginBoxContent {
487 Self::build_margin_content(
488 self.header_text.as_deref(),
489 self.header_page_number,
490 self.header_total_pages,
491 self.number_format,
492 )
493 }
494
495 fn build_footer_content(&self) -> MarginBoxContent {
497 Self::build_margin_content(
498 self.footer_text.as_deref(),
499 self.footer_page_number,
500 self.footer_total_pages,
501 self.number_format,
502 )
503 }
504
505 fn build_margin_content(
507 text: Option<&str>,
508 page_number: bool,
509 total_pages: bool,
510 number_format: CounterFormat,
511 ) -> MarginBoxContent {
512 let mut parts = Vec::new();
513
514 if let Some(text) = text {
515 parts.push(MarginBoxContent::Text(text.to_string()));
516 if page_number {
517 parts.push(MarginBoxContent::Text(" - ".to_string()));
518 }
519 }
520
521 if page_number {
522 parts.push(MarginBoxContent::Text("Page ".to_string()));
523 if number_format == CounterFormat::Decimal {
524 parts.push(MarginBoxContent::PageCounter);
525 } else {
526 parts.push(MarginBoxContent::PageCounterFormatted {
527 format: number_format,
528 });
529 }
530
531 if total_pages {
532 parts.push(MarginBoxContent::Text(" of ".to_string()));
533 parts.push(MarginBoxContent::PagesCounter);
534 }
535 }
536
537 if parts.is_empty() {
538 MarginBoxContent::None
539 } else if parts.len() == 1 {
540 parts.pop().unwrap()
541 } else {
542 MarginBoxContent::Combined(parts)
543 }
544 }
545}
546
547#[derive(Debug, Clone)]
549pub struct TableHeaderInfo {
550 pub table_node_index: usize,
552 pub table_start_y: f32,
554 pub table_end_y: f32,
556 pub thead_items: Vec<super::display_list::DisplayListItem>,
558 pub thead_height: f32,
560 pub thead_offset_y: f32,
562}
563
564#[derive(Debug, Default, Clone)]
566pub struct TableHeaderTracker {
567 pub tables: Vec<TableHeaderInfo>,
569}
570
571impl TableHeaderTracker {
572 #[must_use] pub fn new() -> Self {
573 Self::default()
574 }
575
576 pub fn register_table_header(&mut self, info: TableHeaderInfo) {
578 self.tables.push(info);
579 }
580
581 #[must_use] pub fn get_repeated_headers_for_page(
586 &self,
587 page_index: usize,
588 page_top_y: f32,
589 page_bottom_y: f32,
590 ) -> Vec<(f32, &[super::display_list::DisplayListItem], f32)> {
591 let mut headers = Vec::new();
592
593 for table in &self.tables {
594 let table_starts_before_page = table.table_start_y < page_top_y;
596 let table_continues_on_page = table.table_end_y > page_top_y;
597
598 if table_starts_before_page && table_continues_on_page {
599 headers.push((
602 0.0, table.thead_items.as_slice(),
604 table.thead_height,
605 ));
606 }
607 }
608
609 headers
610 }
611}