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}
612
613#[cfg(test)]
614mod autotest_generated {
615 use std::sync::atomic::{AtomicUsize, Ordering};
616
617 use super::super::display_list::DisplayListItem;
618 use super::*;
619
620 fn decode_roman(s: &str) -> Option<i64> {
628 let mut vals = Vec::new();
629 for c in s.chars() {
630 vals.push(match c {
631 'i' => 1_i64,
632 'v' => 5,
633 'x' => 10,
634 'l' => 50,
635 'c' => 100,
636 'd' => 500,
637 'm' => 1000,
638 _ => return None,
639 });
640 }
641 let mut total = 0_i64;
642 for i in 0..vals.len() {
643 if i + 1 < vals.len() && vals[i] < vals[i + 1] {
644 total -= vals[i];
645 } else {
646 total += vals[i];
647 }
648 }
649 Some(total)
650 }
651
652 fn decode_alpha(s: &str) -> Option<usize> {
654 if s.is_empty() {
655 return None;
656 }
657 let mut n = 0_usize;
658 for c in s.chars() {
659 let digit = match c {
660 'a'..='z' => c as usize - 'a' as usize + 1,
661 _ => return None,
662 };
663 n = n.checked_mul(26)?.checked_add(digit)?;
664 }
665 Some(n)
666 }
667
668 fn decode_greek(s: &str) -> Option<usize> {
670 const LOWER: &[char] = &[
671 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'ο', 'π', 'ρ',
672 'σ', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω',
673 ];
674 if s.is_empty() {
675 return None;
676 }
677 let mut n = 0_usize;
678 for c in s.chars() {
679 let digit = LOWER.iter().position(|l| *l == c)? + 1;
680 n = n.checked_mul(LOWER.len())?.checked_add(digit)?;
681 }
682 Some(n)
683 }
684
685 fn table(start_y: f32, end_y: f32, thead_height: f32) -> TableHeaderInfo {
686 TableHeaderInfo {
687 table_node_index: 0,
688 table_start_y: start_y,
689 table_end_y: end_y,
690 thead_items: vec![DisplayListItem::PopClip],
691 thead_height,
692 thead_offset_y: 0.0,
693 }
694 }
695
696 #[test]
701 fn counter_format_decimal_handles_zero_and_usize_max() {
702 assert_eq!(CounterFormat::Decimal.format(0), "0");
703 assert_eq!(CounterFormat::Decimal.format(1), "1");
704 assert_eq!(
705 CounterFormat::Decimal.format(usize::MAX),
706 usize::MAX.to_string()
707 );
708 }
709
710 #[test]
711 fn counter_format_default_is_decimal_and_does_not_panic_on_zero() {
712 let default = CounterFormat::default();
713 assert_eq!(default, CounterFormat::Decimal);
714 assert_eq!(default.format(0), "0");
715 }
716
717 #[test]
718 fn counter_format_leading_zero_pads_to_two_but_never_truncates() {
719 assert_eq!(CounterFormat::DecimalLeadingZero.format(0), "00");
720 assert_eq!(CounterFormat::DecimalLeadingZero.format(7), "07");
721 assert_eq!(CounterFormat::DecimalLeadingZero.format(9), "09");
722 assert_eq!(CounterFormat::DecimalLeadingZero.format(10), "10");
723 assert_eq!(CounterFormat::DecimalLeadingZero.format(12345), "12345");
725 assert_eq!(
726 CounterFormat::DecimalLeadingZero.format(usize::MAX),
727 usize::MAX.to_string()
728 );
729 }
730
731 #[test]
732 fn counter_format_every_variant_survives_zero_one_and_usize_max() {
733 let variants = [
736 CounterFormat::Decimal,
737 CounterFormat::DecimalLeadingZero,
738 CounterFormat::LowerRoman,
739 CounterFormat::UpperRoman,
740 CounterFormat::LowerAlpha,
741 CounterFormat::UpperAlpha,
742 CounterFormat::LowerGreek,
743 ];
744 for v in variants {
745 for n in [0_usize, 1, 25, 26, 27, 3999, 4000, usize::MAX] {
746 let s = v.format(n);
747 assert!(
748 s.is_char_boundary(0) && s.is_char_boundary(s.len()),
749 "{v:?}.format({n}) produced a malformed string"
750 );
751 }
752 }
753 }
754
755 #[test]
756 fn counter_format_alphabetic_and_greek_return_empty_at_zero() {
757 assert_eq!(CounterFormat::LowerAlpha.format(0), "");
764 assert_eq!(CounterFormat::UpperAlpha.format(0), "");
765 assert_eq!(CounterFormat::LowerGreek.format(0), "");
766 assert_eq!(CounterFormat::LowerRoman.format(0), "0");
767 assert_eq!(CounterFormat::UpperRoman.format(0), "0");
768 }
769
770 #[test]
771 fn counter_format_roman_falls_back_to_decimal_past_3999() {
772 assert_eq!(CounterFormat::LowerRoman.format(3999), "mmmcmxcix");
773 assert_eq!(CounterFormat::UpperRoman.format(3999), "MMMCMXCIX");
774 assert_eq!(CounterFormat::LowerRoman.format(4000), "4000");
776 assert_eq!(CounterFormat::UpperRoman.format(4000), "4000");
777 assert_eq!(
778 CounterFormat::UpperRoman.format(usize::MAX),
779 usize::MAX.to_string()
780 );
781 }
782
783 #[test]
784 fn counter_format_roman_round_trips_over_its_whole_representable_range() {
785 for n in 1..=3999_usize {
786 let lower = CounterFormat::LowerRoman.format(n);
787 let upper = CounterFormat::UpperRoman.format(n);
788 assert_eq!(
789 decode_roman(&lower),
790 Some(n as i64),
791 "lower-roman round-trip failed for {n} -> {lower}"
792 );
793 assert_eq!(upper, lower.to_uppercase(), "casing mismatch for {n}");
794 }
795 }
796
797 #[test]
798 fn counter_format_alphabetic_round_trips_and_is_injective() {
799 let mut seen = std::collections::HashSet::new();
800 for n in 1..=3000_usize {
801 let lower = CounterFormat::LowerAlpha.format(n);
802 let upper = CounterFormat::UpperAlpha.format(n);
803 assert_eq!(
804 decode_alpha(&lower),
805 Some(n),
806 "lower-alpha round-trip failed for {n} -> {lower}"
807 );
808 assert_eq!(upper, lower.to_uppercase(), "casing mismatch for {n}");
809 assert!(seen.insert(lower), "two page numbers collided at {n}");
810 }
811 assert_eq!(CounterFormat::LowerAlpha.format(26), "z");
813 assert_eq!(CounterFormat::LowerAlpha.format(27), "aa");
814 assert_eq!(CounterFormat::LowerAlpha.format(52), "az");
815 assert_eq!(CounterFormat::LowerAlpha.format(53), "ba");
816 }
817
818 #[test]
819 fn counter_format_greek_round_trips_and_emits_whole_code_points() {
820 for n in 1..=2000_usize {
821 let s = CounterFormat::LowerGreek.format(n);
822 assert_eq!(
823 decode_greek(&s),
824 Some(n),
825 "lower-greek round-trip failed for {n} -> {s}"
826 );
827 assert_eq!(s.len(), s.chars().count() * 2, "sliced a code point at {n}");
830 }
831 assert_eq!(CounterFormat::LowerGreek.format(24), "ω");
832 assert_eq!(CounterFormat::LowerGreek.format(25), "αα");
833 }
834
835 #[test]
836 fn counter_format_usize_max_terminates_for_the_positional_styles() {
837 let alpha = CounterFormat::LowerAlpha.format(usize::MAX);
839 let greek = CounterFormat::LowerGreek.format(usize::MAX);
840 assert!(!alpha.is_empty() && alpha.chars().all(|c| c.is_ascii_lowercase()));
841 assert!(!greek.is_empty());
842 assert_eq!(greek.len(), greek.chars().count() * 2);
843 }
844
845 #[test]
850 fn page_info_new_sets_flags_for_a_representative_page() {
851 let info = PageInfo::new(1, 3);
852 assert_eq!(info.page_number, 1);
853 assert_eq!(info.total_pages, 3);
854 assert!(info.is_first);
855 assert!(!info.is_last);
856 assert!(info.is_right, "page 1 (odd) is a recto page");
857 assert!(!info.is_left);
858 assert!(!info.is_blank);
859 }
860
861 #[test]
862 fn page_info_left_and_right_are_always_mutually_exclusive() {
863 for n in [0_usize, 1, 2, 3, 100, 101, usize::MAX - 1, usize::MAX] {
864 let info = PageInfo::new(n, 0);
865 assert!(
866 info.is_left != info.is_right,
867 "page {n} claimed to be both/neither verso and recto"
868 );
869 assert_eq!(info.is_left, n % 2 == 0);
870 assert!(!info.is_blank, "new() must never fabricate a blank page");
871 }
872 }
873
874 #[test]
875 fn page_info_is_last_is_false_when_the_total_is_unknown() {
876 for n in [0_usize, 1, 7, usize::MAX] {
878 assert!(!PageInfo::new(n, 0).is_last, "page {n} of 0 claimed is_last");
879 }
880 }
881
882 #[test]
883 fn page_info_page_zero_is_degenerate_but_does_not_panic() {
884 let info = PageInfo::new(0, 0);
885 assert!(!info.is_first, "1-indexed: page 0 is not the first page");
886 assert!(!info.is_last);
887 assert!(info.is_left, "0 is even, so it lands on the verso branch");
888 }
889
890 #[test]
891 fn page_info_out_of_range_page_number_does_not_claim_to_be_last() {
892 let info = PageInfo::new(9, 3);
895 assert!(!info.is_last);
896 assert!(!info.is_first);
897 }
898
899 #[test]
900 fn page_info_usize_max_extremes_do_not_overflow() {
901 let info = PageInfo::new(usize::MAX, usize::MAX);
902 assert!(info.is_last, "the final page of a MAX-page document is last");
903 assert!(!info.is_first);
904 assert!(info.is_right, "usize::MAX is odd");
905 let single = PageInfo::new(1, 1);
906 assert!(single.is_first && single.is_last);
907 }
908
909 #[test]
914 fn header_footer_default_renders_nothing_on_any_page() {
915 let cfg = HeaderFooterConfig::default();
916 assert!(!cfg.show_header && !cfg.show_footer);
917 assert!(matches!(cfg.header_content, MarginBoxContent::None));
918 assert!(matches!(cfg.footer_content, MarginBoxContent::None));
919 assert_eq!(cfg.header_text(PageInfo::new(1, 1)), "");
920 assert_eq!(cfg.footer_text(PageInfo::new(usize::MAX, usize::MAX)), "");
921 assert_eq!(cfg.text_color.a, 255);
922 }
923
924 #[test]
925 fn header_footer_with_page_numbers_only_enables_the_footer() {
926 let cfg = HeaderFooterConfig::with_page_numbers();
927 assert!(cfg.show_footer);
928 assert!(!cfg.show_header, "with_page_numbers must not enable a header");
929 assert_eq!(cfg.footer_text(PageInfo::new(2, 7)), "Page 2 of 7");
930 assert_eq!(cfg.header_text(PageInfo::new(2, 7)), "");
931 }
932
933 #[test]
934 fn header_footer_unknown_total_renders_a_question_mark_not_a_zero() {
935 let cfg = HeaderFooterConfig::with_page_numbers();
936 assert_eq!(cfg.footer_text(PageInfo::new(1, 0)), "Page 1 of ?");
937 }
938
939 #[test]
940 fn header_footer_with_header_and_footer_page_numbers_fills_both() {
941 let cfg = HeaderFooterConfig::with_header_and_footer_page_numbers();
942 assert!(cfg.show_header && cfg.show_footer);
943 let info = PageInfo::new(3, 10);
944 assert_eq!(cfg.header_text(info), "Page 3");
945 assert_eq!(cfg.footer_text(info), "Page 3 of 10");
946 let extreme = PageInfo::new(usize::MAX, usize::MAX);
948 assert_eq!(
949 cfg.header_text(extreme),
950 format!("Page {}", usize::MAX)
951 );
952 }
953
954 #[test]
955 fn header_footer_with_text_enables_the_box_and_preserves_unicode_exactly() {
956 let text = "Ünïcödé — 日本語 🎉\u{200b}\u{0}";
957 let cfg = HeaderFooterConfig::default()
958 .with_header_text(text)
959 .with_footer_text(text);
960 assert!(cfg.show_header && cfg.show_footer);
961 let info = PageInfo::new(1, 1);
962 assert_eq!(cfg.header_text(info), text);
964 assert_eq!(cfg.footer_text(info), text);
965 assert_eq!(cfg.header_text(info).len(), text.len());
966 }
967
968 #[test]
969 fn header_footer_empty_text_still_switches_the_box_on() {
970 let cfg = HeaderFooterConfig::default().with_header_text("");
974 assert!(cfg.show_header);
975 assert_eq!(cfg.header_text(PageInfo::new(1, 1)), "");
976 assert!(cfg.header_height > 0.0);
977 }
978
979 #[test]
980 fn header_footer_text_of_huge_length_round_trips_without_truncation() {
981 let huge = "x".repeat(200_000);
982 let cfg = HeaderFooterConfig::default().with_footer_text(huge.clone());
983 assert_eq!(cfg.footer_text(PageInfo::new(1, 1)).len(), huge.len());
984 }
985
986 #[test]
987 fn header_footer_last_builder_call_wins() {
988 let cfg = HeaderFooterConfig::default()
989 .with_header_text("first")
990 .with_header_text("second");
991 assert_eq!(cfg.header_text(PageInfo::new(1, 1)), "second");
992 }
993
994 #[test]
995 fn header_footer_skip_first_page_blanks_only_page_one() {
996 let mut cfg = HeaderFooterConfig::with_header_and_footer_page_numbers();
997 cfg.skip_first_page = true;
998 assert_eq!(cfg.header_text(PageInfo::new(1, 5)), "");
999 assert_eq!(cfg.footer_text(PageInfo::new(1, 5)), "");
1000 assert_eq!(cfg.header_text(PageInfo::new(2, 5)), "Page 2");
1001 assert_eq!(cfg.footer_text(PageInfo::new(2, 5)), "Page 2 of 5");
1002 let mut forged = PageInfo::new(4, 5);
1005 forged.is_first = true;
1006 assert_eq!(cfg.header_text(forged), "");
1007 }
1008
1009 #[test]
1010 fn header_footer_generate_content_covers_every_margin_box_variant() {
1011 let cfg = HeaderFooterConfig::default();
1012 let info = PageInfo::new(4, 9);
1013 assert_eq!(cfg.generate_content(&MarginBoxContent::None, info), "");
1014 assert_eq!(
1015 cfg.generate_content(&MarginBoxContent::Text(String::new()), info),
1016 ""
1017 );
1018 assert_eq!(cfg.generate_content(&MarginBoxContent::PageCounter, info), "4");
1019 assert_eq!(
1020 cfg.generate_content(&MarginBoxContent::PagesCounter, info),
1021 "9"
1022 );
1023 assert_eq!(
1024 cfg.generate_content(
1025 &MarginBoxContent::PageCounterFormatted {
1026 format: CounterFormat::LowerRoman
1027 },
1028 info
1029 ),
1030 "iv"
1031 );
1032 assert_eq!(
1034 cfg.generate_content(&MarginBoxContent::NamedString("chapter".into()), info),
1035 "[string:chapter]"
1036 );
1037 assert_eq!(
1038 cfg.generate_content(&MarginBoxContent::RunningElement("hdr".into()), info),
1039 "[element:hdr]"
1040 );
1041 }
1042
1043 #[test]
1044 fn header_footer_generate_content_of_an_empty_combined_is_empty() {
1045 let cfg = HeaderFooterConfig::default();
1046 assert_eq!(
1047 cfg.generate_content(&MarginBoxContent::Combined(Vec::new()), PageInfo::new(1, 1)),
1048 ""
1049 );
1050 }
1051
1052 #[test]
1053 fn header_footer_generate_content_recurses_through_nested_combined() {
1054 let cfg = HeaderFooterConfig::default();
1059 let mut content = MarginBoxContent::PageCounter;
1060 for _ in 0..128 {
1061 content = MarginBoxContent::Combined(vec![content]);
1062 }
1063 assert_eq!(cfg.generate_content(&content, PageInfo::new(42, 99)), "42");
1064 }
1065
1066 #[test]
1067 fn header_footer_generate_content_calls_a_custom_hook_exactly_once() {
1068 let calls = Arc::new(AtomicUsize::new(0));
1069 let seen = Arc::clone(&calls);
1070 let content = MarginBoxContent::Custom(Arc::new(move |info: PageInfo| {
1071 seen.fetch_add(1, Ordering::SeqCst);
1072 format!("{}/{}", info.page_number, info.total_pages)
1073 }));
1074 let cfg = HeaderFooterConfig::default();
1075 assert_eq!(cfg.generate_content(&content, PageInfo::new(2, 5)), "2/5");
1076 assert_eq!(calls.load(Ordering::SeqCst), 1);
1077
1078 let combined = MarginBoxContent::Combined(vec![
1080 MarginBoxContent::Text("[".to_string()),
1081 content,
1082 MarginBoxContent::Text("]".to_string()),
1083 ]);
1084 assert_eq!(cfg.generate_content(&combined, PageInfo::new(2, 5)), "[2/5]");
1085 assert_eq!(calls.load(Ordering::SeqCst), 2);
1086 }
1087
1088 #[test]
1089 fn header_footer_hidden_box_short_circuits_before_generating_content() {
1090 let calls = Arc::new(AtomicUsize::new(0));
1093 let seen = Arc::clone(&calls);
1094 let mut cfg = HeaderFooterConfig {
1095 header_content: MarginBoxContent::Custom(Arc::new(move |_| {
1096 seen.fetch_add(1, Ordering::SeqCst);
1097 "leaked".to_string()
1098 })),
1099 ..Default::default()
1100 };
1101 assert_eq!(cfg.header_text(PageInfo::new(1, 1)), "");
1102 assert_eq!(calls.load(Ordering::SeqCst), 0);
1103 }
1104
1105 #[test]
1110 fn fake_page_new_is_inert_and_matches_default() {
1111 let cfg = FakePageConfig::new();
1112 assert!(!cfg.show_header && !cfg.show_footer);
1113 assert!(cfg.header_text.is_none() && cfg.footer_text.is_none());
1114 assert!(!cfg.header_page_number && !cfg.footer_page_number);
1115 assert!(!cfg.header_total_pages && !cfg.footer_total_pages);
1116 assert!(!cfg.skip_first_page);
1117 assert_eq!(cfg.number_format, CounterFormat::Decimal);
1118
1119 let hf = cfg.to_header_footer_config();
1120 assert!(matches!(hf.header_content, MarginBoxContent::None));
1121 assert!(matches!(hf.footer_content, MarginBoxContent::None));
1122 assert_eq!(hf.header_text(PageInfo::new(1, 1)), "");
1123 assert_eq!(hf.footer_text(PageInfo::new(1, 1)), "");
1124 }
1125
1126 #[test]
1127 fn fake_page_footer_page_numbers_render_page_x_of_y() {
1128 let hf = FakePageConfig::new()
1129 .with_footer_page_numbers()
1130 .to_header_footer_config();
1131 assert!(hf.show_footer && !hf.show_header);
1132 assert_eq!(hf.footer_text(PageInfo::new(2, 7)), "Page 2 of 7");
1133 assert_eq!(hf.footer_text(PageInfo::new(2, 0)), "Page 2 of ?");
1134 }
1135
1136 #[test]
1137 fn fake_page_header_page_numbers_omit_the_total() {
1138 let hf = FakePageConfig::new()
1139 .with_header_page_numbers()
1140 .to_header_footer_config();
1141 assert_eq!(hf.header_text(PageInfo::new(3, 7)), "Page 3");
1142 assert_eq!(hf.footer_text(PageInfo::new(3, 7)), "");
1143 }
1144
1145 #[test]
1146 fn fake_page_header_and_footer_page_numbers_agree_with_the_pair_of_setters() {
1147 let both = FakePageConfig::new()
1148 .with_header_and_footer_page_numbers()
1149 .to_header_footer_config();
1150 let info = PageInfo::new(5, 11);
1151 assert_eq!(both.header_text(info), "Page 5");
1152 assert_eq!(both.footer_text(info), "Page 5 of 11");
1153 }
1154
1155 #[test]
1156 fn fake_page_text_and_page_number_are_joined_by_a_separator() {
1157 let mut cfg = FakePageConfig::new()
1158 .with_header_text("My Document")
1159 .with_header_page_numbers();
1160 cfg.header_total_pages = true;
1161 let hf = cfg.to_header_footer_config();
1162 assert_eq!(
1163 hf.header_text(PageInfo::new(4, 9)),
1164 "My Document - Page 4 of 9"
1165 );
1166 }
1167
1168 #[test]
1169 fn fake_page_number_format_flows_into_the_rendered_counter() {
1170 let hf = FakePageConfig::new()
1171 .with_footer_page_numbers()
1172 .with_number_format(CounterFormat::LowerRoman)
1173 .to_header_footer_config();
1174 assert_eq!(hf.footer_text(PageInfo::new(4, 9)), "Page iv of 9");
1177
1178 let greek = FakePageConfig::new()
1179 .with_header_page_numbers()
1180 .with_number_format(CounterFormat::LowerGreek)
1181 .to_header_footer_config();
1182 assert_eq!(greek.header_text(PageInfo::new(2, 9)), "Page β");
1183 }
1184
1185 #[test]
1186 fn fake_page_decimal_format_takes_the_unformatted_counter_branch() {
1187 let decimal = FakePageConfig::new().with_header_page_numbers();
1188 match decimal.to_header_footer_config().header_content {
1189 MarginBoxContent::Combined(parts) => {
1190 assert_eq!(parts.len(), 2);
1191 assert!(matches!(parts[1], MarginBoxContent::PageCounter));
1192 }
1193 other => panic!("expected Combined, got {other:?}"),
1194 }
1195 let roman = FakePageConfig::new()
1196 .with_header_page_numbers()
1197 .with_number_format(CounterFormat::UpperRoman);
1198 match roman.to_header_footer_config().header_content {
1199 MarginBoxContent::Combined(parts) => {
1200 assert!(matches!(
1201 parts[1],
1202 MarginBoxContent::PageCounterFormatted {
1203 format: CounterFormat::UpperRoman
1204 }
1205 ));
1206 }
1207 other => panic!("expected Combined, got {other:?}"),
1208 }
1209 }
1210
1211 #[test]
1212 fn fake_page_total_pages_without_page_number_is_silently_dropped() {
1213 let mut cfg = FakePageConfig::new();
1218 cfg.show_footer = true;
1219 cfg.footer_total_pages = true;
1220 let hf = cfg.to_header_footer_config();
1221 assert!(matches!(hf.footer_content, MarginBoxContent::None));
1222 assert_eq!(hf.footer_text(PageInfo::new(1, 5)), "");
1223 }
1224
1225 #[test]
1226 fn fake_page_single_text_part_is_not_wrapped_in_a_combined() {
1227 let hf = FakePageConfig::new()
1228 .with_footer_text("plain")
1229 .to_header_footer_config();
1230 assert!(matches!(hf.footer_content, MarginBoxContent::Text(ref s) if s == "plain"));
1231 assert_eq!(hf.footer_text(PageInfo::new(1, 1)), "plain");
1232 }
1233
1234 #[test]
1235 fn fake_page_empty_text_plus_page_number_leaks_a_leading_separator() {
1236 let hf = FakePageConfig::new()
1239 .with_header_text("")
1240 .with_header_page_numbers()
1241 .to_header_footer_config();
1242 assert_eq!(hf.header_text(PageInfo::new(1, 1)), " - Page 1");
1243 }
1244
1245 #[test]
1246 fn fake_page_unicode_text_survives_the_bridge_byte_for_byte() {
1247 let text = "Ünïcödé — 日本語 🎉";
1248 let hf = FakePageConfig::new()
1249 .with_header_text(text)
1250 .with_footer_text(text)
1251 .to_header_footer_config();
1252 let info = PageInfo::new(1, 1);
1253 assert_eq!(hf.header_text(info), text);
1254 assert_eq!(hf.footer_text(info), text);
1255 }
1256
1257 #[test]
1258 fn fake_page_huge_text_survives_the_bridge() {
1259 let huge = "λ".repeat(100_000);
1260 let hf = FakePageConfig::new()
1261 .with_footer_text(huge.clone())
1262 .to_header_footer_config();
1263 let out = hf.footer_text(PageInfo::new(1, 1));
1264 assert_eq!(out.len(), huge.len());
1265 assert_eq!(out.chars().count(), 100_000);
1266 }
1267
1268 #[test]
1269 fn fake_page_skip_first_page_toggles_both_ways() {
1270 let on = FakePageConfig::new()
1271 .with_footer_page_numbers()
1272 .skip_first_page(true)
1273 .to_header_footer_config();
1274 assert!(on.skip_first_page);
1275 assert_eq!(on.footer_text(PageInfo::new(1, 3)), "");
1276 assert_eq!(on.footer_text(PageInfo::new(2, 3)), "Page 2 of 3");
1277
1278 let off = FakePageConfig::new()
1279 .with_footer_page_numbers()
1280 .skip_first_page(true)
1281 .skip_first_page(false)
1282 .to_header_footer_config();
1283 assert!(!off.skip_first_page);
1284 assert_eq!(off.footer_text(PageInfo::new(1, 3)), "Page 1 of 3");
1285 }
1286
1287 #[test]
1288 fn fake_page_non_finite_geometry_is_stored_and_forwarded_verbatim() {
1289 let cfg = FakePageConfig::new()
1293 .with_header_height(f32::NAN)
1294 .with_footer_height(f32::INFINITY)
1295 .with_font_size(-0.0);
1296 assert!(cfg.header_height.is_nan());
1297 let hf = cfg.to_header_footer_config();
1298 assert!(hf.header_height.is_nan(), "NaN header height was swallowed");
1299 assert_eq!(hf.footer_height, f32::INFINITY);
1300 assert_eq!(hf.font_size, -0.0);
1301 }
1302
1303 #[test]
1304 fn fake_page_extreme_but_finite_geometry_is_preserved_exactly() {
1305 let hf = FakePageConfig::new()
1306 .with_header_height(f32::MAX)
1307 .with_footer_height(f32::MIN)
1308 .with_font_size(f32::MIN_POSITIVE)
1309 .to_header_footer_config();
1310 assert_eq!(hf.header_height, f32::MAX);
1311 assert_eq!(hf.footer_height, f32::MIN);
1312 assert_eq!(hf.font_size, f32::MIN_POSITIVE);
1313
1314 let negative = FakePageConfig::new()
1315 .with_header_height(-100.0)
1316 .to_header_footer_config();
1317 assert_eq!(negative.header_height, -100.0);
1318 }
1319
1320 #[test]
1321 fn fake_page_text_color_crosses_the_bridge_unchanged() {
1322 let color = ColorU {
1323 r: 1,
1324 g: 2,
1325 b: 3,
1326 a: 0,
1327 };
1328 let hf = FakePageConfig::new()
1329 .with_text_color(color)
1330 .to_header_footer_config();
1331 assert_eq!(hf.text_color, color);
1332 assert_eq!(hf.text_color.a, 0, "a fully transparent color must survive");
1333 }
1334
1335 #[test]
1336 fn fake_page_to_header_footer_config_is_a_pure_read() {
1337 let cfg = FakePageConfig::new()
1340 .with_header_and_footer_page_numbers()
1341 .with_number_format(CounterFormat::UpperAlpha)
1342 .skip_first_page(true);
1343 let info = PageInfo::new(3, 4);
1344 let first = cfg.to_header_footer_config();
1345 let second = cfg.to_header_footer_config();
1346 assert_eq!(first.header_text(info), second.header_text(info));
1347 assert_eq!(first.footer_text(info), second.footer_text(info));
1348 assert_eq!(first.header_text(info), "Page C");
1349 assert!(cfg.show_header && cfg.show_footer && cfg.skip_first_page);
1350 }
1351
1352 #[test]
1353 fn fake_page_build_margin_content_shapes_match_the_part_count() {
1354 assert!(matches!(
1357 FakePageConfig::build_margin_content(None, false, false, CounterFormat::Decimal),
1358 MarginBoxContent::None
1359 ));
1360 assert!(matches!(
1361 FakePageConfig::build_margin_content(None, false, true, CounterFormat::Decimal),
1362 MarginBoxContent::None
1363 ));
1364 assert!(matches!(
1365 FakePageConfig::build_margin_content(Some("t"), false, false, CounterFormat::Decimal),
1366 MarginBoxContent::Text(ref s) if s == "t"
1367 ));
1368 assert!(matches!(
1369 FakePageConfig::build_margin_content(Some(""), false, true, CounterFormat::Decimal),
1370 MarginBoxContent::Text(ref s) if s.is_empty()
1371 ));
1372 match FakePageConfig::build_margin_content(
1373 Some("Doc"),
1374 true,
1375 true,
1376 CounterFormat::LowerRoman,
1377 ) {
1378 MarginBoxContent::Combined(parts) => {
1379 assert_eq!(parts.len(), 6, "text + sep + label + counter + of + total");
1380 assert!(matches!(parts[1], MarginBoxContent::Text(ref s) if s == " - "));
1381 assert!(matches!(
1382 parts[3],
1383 MarginBoxContent::PageCounterFormatted { .. }
1384 ));
1385 assert!(matches!(parts[5], MarginBoxContent::PagesCounter));
1386 }
1387 other => panic!("expected Combined, got {other:?}"),
1388 }
1389 }
1390
1391 #[test]
1392 fn fake_page_build_header_and_footer_content_read_their_own_fields() {
1393 let mut cfg = FakePageConfig::new();
1396 cfg.header_text = Some("H".to_string());
1397 cfg.footer_text = Some("F".to_string());
1398 cfg.header_page_number = true;
1399 cfg.footer_page_number = false;
1400 let hf = HeaderFooterConfig::default();
1401 let info = PageInfo::new(2, 4);
1402 assert_eq!(
1403 hf.generate_content(&cfg.build_header_content(), info),
1404 "H - Page 2"
1405 );
1406 assert_eq!(hf.generate_content(&cfg.build_footer_content(), info), "F");
1407 }
1408
1409 #[test]
1414 fn tracker_new_is_empty_and_matches_default() {
1415 let tracker = TableHeaderTracker::new();
1416 assert!(tracker.tables.is_empty());
1417 assert!(TableHeaderTracker::default().tables.is_empty());
1418 assert!(tracker
1419 .get_repeated_headers_for_page(0, 0.0, 0.0)
1420 .is_empty());
1421 }
1422
1423 #[test]
1424 fn tracker_empty_returns_nothing_for_every_extreme_page_geometry() {
1425 let tracker = TableHeaderTracker::new();
1426 for page_index in [0_usize, 1, usize::MAX] {
1427 for y in [
1428 0.0_f32,
1429 -0.0,
1430 f32::MIN,
1431 f32::MAX,
1432 f32::INFINITY,
1433 f32::NEG_INFINITY,
1434 f32::NAN,
1435 ] {
1436 assert!(
1437 tracker.get_repeated_headers_for_page(page_index, y, y).is_empty(),
1438 "empty tracker produced a header at page {page_index}, y={y}"
1439 );
1440 }
1441 }
1442 }
1443
1444 #[test]
1445 fn tracker_register_appends_in_order_and_allows_duplicates() {
1446 let mut tracker = TableHeaderTracker::new();
1447 for i in 0..1000 {
1448 let mut info = table(0.0, 100.0, 10.0);
1449 info.table_node_index = i;
1450 tracker.register_table_header(info);
1451 }
1452 tracker.register_table_header(table(0.0, 100.0, 10.0));
1454 tracker.register_table_header(table(0.0, 100.0, 10.0));
1455 assert_eq!(tracker.tables.len(), 1002);
1456 assert_eq!(tracker.tables[0].table_node_index, 0);
1457 assert_eq!(tracker.tables[999].table_node_index, 999);
1458 }
1459
1460 #[test]
1461 fn tracker_repeats_only_tables_that_straddle_the_page_top() {
1462 let mut tracker = TableHeaderTracker::new();
1463 tracker.register_table_header(table(0.0, 500.0, 20.0)); tracker.register_table_header(table(0.0, 50.0, 20.0)); tracker.register_table_header(table(200.0, 500.0, 20.0)); let headers = tracker.get_repeated_headers_for_page(1, 100.0, 900.0);
1467 assert_eq!(headers.len(), 1);
1468 let (offset, items, height) = headers[0];
1469 assert_eq!(offset, 0.0, "a repeated thead sits at the page top");
1470 assert_eq!(height, 20.0);
1471 assert_eq!(items.len(), 1);
1472 assert!(matches!(items[0], DisplayListItem::PopClip));
1473 }
1474
1475 #[test]
1476 fn tracker_page_boundary_comparisons_are_strict() {
1477 let mut tracker = TableHeaderTracker::new();
1478 tracker.register_table_header(table(100.0, 500.0, 20.0));
1481 assert!(tracker
1482 .get_repeated_headers_for_page(1, 100.0, 900.0)
1483 .is_empty());
1484
1485 let mut tracker = TableHeaderTracker::new();
1487 tracker.register_table_header(table(0.0, 100.0, 20.0));
1488 assert!(tracker
1489 .get_repeated_headers_for_page(1, 100.0, 900.0)
1490 .is_empty());
1491
1492 let below = f32::from_bits(100.0_f32.to_bits() - 1);
1496 let above = f32::from_bits(100.0_f32.to_bits() + 1);
1497 assert!(below < 100.0 && above > 100.0, "ulp step collapsed");
1498 let mut tracker = TableHeaderTracker::new();
1499 tracker.register_table_header(table(below, above, 20.0));
1500 assert_eq!(
1501 tracker.get_repeated_headers_for_page(1, 100.0, 900.0).len(),
1502 1
1503 );
1504 }
1505
1506 #[test]
1507 fn tracker_nan_page_top_yields_no_headers_and_no_panic() {
1508 let mut tracker = TableHeaderTracker::new();
1512 tracker.register_table_header(table(0.0, 500.0, 20.0));
1513 assert!(tracker
1514 .get_repeated_headers_for_page(1, f32::NAN, 900.0)
1515 .is_empty());
1516 assert_eq!(
1518 tracker
1519 .get_repeated_headers_for_page(1, 100.0, f32::NAN)
1520 .len(),
1521 1
1522 );
1523 }
1524
1525 #[test]
1526 fn tracker_nan_table_geometry_yields_no_headers() {
1527 let mut tracker = TableHeaderTracker::new();
1528 tracker.register_table_header(table(f32::NAN, 500.0, 20.0));
1529 tracker.register_table_header(table(0.0, f32::NAN, 20.0));
1530 tracker.register_table_header(table(f32::NAN, f32::NAN, 20.0));
1531 assert!(
1532 tracker
1533 .get_repeated_headers_for_page(1, 100.0, 900.0)
1534 .is_empty(),
1535 "a NaN-positioned table must not be repeated"
1536 );
1537 }
1538
1539 #[test]
1540 fn tracker_infinite_page_top_behaves_deterministically() {
1541 let mut tracker = TableHeaderTracker::new();
1542 tracker.register_table_header(table(0.0, 500.0, 20.0));
1543 assert!(tracker
1546 .get_repeated_headers_for_page(1, f32::INFINITY, f32::INFINITY)
1547 .is_empty());
1548 assert!(tracker
1550 .get_repeated_headers_for_page(1, f32::NEG_INFINITY, 900.0)
1551 .is_empty());
1552 }
1553
1554 #[test]
1555 fn tracker_infinite_table_extent_repeats_forever_without_overflow() {
1556 let mut tracker = TableHeaderTracker::new();
1557 tracker.register_table_header(table(f32::NEG_INFINITY, f32::INFINITY, f32::INFINITY));
1558 let headers = tracker.get_repeated_headers_for_page(usize::MAX, f32::MAX, f32::MAX);
1559 assert_eq!(headers.len(), 1);
1560 assert!(headers[0].2.is_infinite());
1562 }
1563
1564 #[test]
1565 fn tracker_nan_thead_height_is_forwarded_not_sanitized() {
1566 let mut tracker = TableHeaderTracker::new();
1567 tracker.register_table_header(table(0.0, 500.0, f32::NAN));
1568 let headers = tracker.get_repeated_headers_for_page(1, 100.0, 900.0);
1569 assert_eq!(headers.len(), 1);
1570 assert!(headers[0].2.is_nan(), "height NaN was silently rewritten");
1571 }
1572
1573 #[test]
1574 fn tracker_page_index_is_ignored_by_the_current_implementation() {
1575 let mut tracker = TableHeaderTracker::new();
1579 tracker.register_table_header(table(0.0, 500.0, 20.0));
1580 let a = tracker.get_repeated_headers_for_page(0, 100.0, 900.0);
1581 let b = tracker.get_repeated_headers_for_page(usize::MAX, 100.0, 900.0);
1582 assert_eq!(a.len(), b.len());
1583 assert_eq!(a.len(), 1);
1584 }
1585
1586 #[test]
1587 fn tracker_page_bottom_is_ignored_even_when_the_page_is_inverted() {
1588 let mut tracker = TableHeaderTracker::new();
1591 tracker.register_table_header(table(0.0, 500.0, 20.0));
1592 assert_eq!(
1593 tracker.get_repeated_headers_for_page(1, 100.0, -900.0).len(),
1594 1
1595 );
1596 assert_eq!(
1597 tracker.get_repeated_headers_for_page(1, 100.0, 100.0).len(),
1598 1
1599 );
1600 }
1601
1602 #[test]
1603 fn tracker_preserves_registration_order_across_many_straddling_tables() {
1604 let mut tracker = TableHeaderTracker::new();
1605 for i in 0..64_u32 {
1606 #[allow(clippy::cast_precision_loss)]
1607 tracker.register_table_header(table(0.0, 500.0, i as f32));
1608 }
1609 let headers = tracker.get_repeated_headers_for_page(3, 100.0, 900.0);
1610 assert_eq!(headers.len(), 64);
1611 for (i, (offset, _, height)) in headers.iter().enumerate() {
1612 assert_eq!(*offset, 0.0);
1613 #[allow(clippy::cast_precision_loss)]
1614 let expected = i as f32;
1615 assert_eq!(*height, expected, "headers came back out of order");
1616 }
1617 }
1618
1619 #[test]
1620 fn tracker_zero_height_page_returns_nothing() {
1621 let mut tracker = TableHeaderTracker::new();
1624 tracker.register_table_header(table(0.0, 500.0, 20.0));
1625 assert!(tracker
1626 .get_repeated_headers_for_page(0, 0.0, 0.0)
1627 .is_empty());
1628 let mut tracker = TableHeaderTracker::new();
1630 tracker.register_table_header(table(-10.0, 500.0, 20.0));
1631 assert_eq!(tracker.get_repeated_headers_for_page(0, 0.0, 0.0).len(), 1);
1632 }
1633}