1use std::fmt;
25
26const SUBPX_SCALE: u32 = 256;
35
36fn px_to_subpx(px: f64) -> Option<u32> {
40 if !px.is_finite() || px < 0.0 {
41 return None;
42 }
43 let val = (px * SUBPX_SCALE as f64).round();
44 if val > u32::MAX as f64 {
45 return None;
46 }
47 Some(val as u32)
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59pub struct CellMetrics {
60 pub width_subpx: u32,
62 pub height_subpx: u32,
64}
65
66impl CellMetrics {
67 #[must_use]
71 pub fn new(width_subpx: u32, height_subpx: u32) -> Option<Self> {
72 if width_subpx == 0 || height_subpx == 0 {
73 return None;
74 }
75 Some(Self {
76 width_subpx,
77 height_subpx,
78 })
79 }
80
81 #[must_use]
85 pub fn from_px(width_px: f64, height_px: f64) -> Option<Self> {
86 let w = px_to_subpx(width_px)?;
87 let h = px_to_subpx(height_px)?;
88 Self::new(w, h)
89 }
90
91 #[must_use]
93 pub const fn width_px(&self) -> u32 {
94 self.width_subpx / SUBPX_SCALE
95 }
96
97 #[must_use]
99 pub const fn height_px(&self) -> u32 {
100 self.height_subpx / SUBPX_SCALE
101 }
102
103 pub const MONOSPACE_DEFAULT: Self = Self {
105 width_subpx: 8 * SUBPX_SCALE,
106 height_subpx: 16 * SUBPX_SCALE,
107 };
108
109 pub const LARGE: Self = Self {
111 width_subpx: 10 * SUBPX_SCALE,
112 height_subpx: 20 * SUBPX_SCALE,
113 };
114}
115
116impl Default for CellMetrics {
117 fn default() -> Self {
118 Self::MONOSPACE_DEFAULT
119 }
120}
121
122impl fmt::Display for CellMetrics {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 write!(
125 f,
126 "{}x{}px ({:.2}x{:.2} sub-px)",
127 self.width_px(),
128 self.height_px(),
129 self.width_subpx as f64 / SUBPX_SCALE as f64,
130 self.height_subpx as f64 / SUBPX_SCALE as f64,
131 )
132 }
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
144pub struct ContainerViewport {
145 pub width_px: u32,
147 pub height_px: u32,
149 pub dpr_subpx: u32,
156 pub zoom_subpx: u32,
164}
165
166impl ContainerViewport {
167 #[must_use]
171 pub fn new(width_px: u32, height_px: u32, dpr: f64, zoom: f64) -> Option<Self> {
172 let dpr_subpx = px_to_subpx(dpr)?;
173 let zoom_subpx = px_to_subpx(zoom)?;
174 if width_px == 0 || height_px == 0 || dpr_subpx == 0 || zoom_subpx == 0 {
175 return None;
176 }
177 Some(Self {
178 width_px,
179 height_px,
180 dpr_subpx,
181 zoom_subpx,
182 })
183 }
184
185 #[must_use]
187 pub fn simple(width_px: u32, height_px: u32) -> Option<Self> {
188 Self::new(width_px, height_px, 1.0, 1.0)
189 }
190
191 #[must_use]
197 pub fn effective_width_subpx(&self) -> u32 {
198 let scale3 = (SUBPX_SCALE as u64) * (SUBPX_SCALE as u64) * (SUBPX_SCALE as u64);
200 let numer = (self.width_px as u64) * scale3;
201 let denom = (self.dpr_subpx as u64) * (self.zoom_subpx as u64);
202 if denom == 0 {
203 return 0;
204 }
205 (numer / denom) as u32
206 }
207
208 #[must_use]
210 pub fn effective_height_subpx(&self) -> u32 {
211 let scale3 = (SUBPX_SCALE as u64) * (SUBPX_SCALE as u64) * (SUBPX_SCALE as u64);
212 let numer = (self.height_px as u64) * scale3;
213 let denom = (self.dpr_subpx as u64) * (self.zoom_subpx as u64);
214 if denom == 0 {
215 return 0;
216 }
217 (numer / denom) as u32
218 }
219}
220
221impl fmt::Display for ContainerViewport {
222 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223 write!(
224 f,
225 "{}x{}px @{:.2}x DPR, {:.0}% zoom",
226 self.width_px,
227 self.height_px,
228 self.dpr_subpx as f64 / SUBPX_SCALE as f64,
229 self.zoom_subpx as f64 / SUBPX_SCALE as f64 * 100.0,
230 )
231 }
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
240pub enum FitPolicy {
241 #[default]
245 FitToContainer,
246 Fixed {
250 cols: u16,
252 rows: u16,
254 },
255 FitWithMinimum {
259 min_cols: u16,
261 min_rows: u16,
263 },
264}
265
266#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
272pub struct FitResult {
273 pub cols: u16,
275 pub rows: u16,
277 pub padding_right_subpx: u32,
282 pub padding_bottom_subpx: u32,
284}
285
286impl FitResult {
287 #[must_use]
289 pub fn is_valid(&self) -> bool {
290 self.cols > 0 && self.rows > 0
291 }
292}
293
294impl fmt::Display for FitResult {
295 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296 write!(f, "{}x{} cells", self.cols, self.rows)
297 }
298}
299
300#[derive(Debug, Clone, Copy, PartialEq, Eq)]
306pub enum FitError {
307 ContainerTooSmall,
309 DimensionOverflow,
311}
312
313impl fmt::Display for FitError {
314 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315 match self {
316 Self::ContainerTooSmall => write!(f, "container too small to fit any cells"),
317 Self::DimensionOverflow => write!(f, "computed grid dimensions overflow u16"),
318 }
319 }
320}
321
322pub fn fit_to_container(
332 viewport: &ContainerViewport,
333 cell: &CellMetrics,
334 policy: FitPolicy,
335) -> Result<FitResult, FitError> {
336 match policy {
337 FitPolicy::Fixed { cols, rows } => Ok(FitResult {
338 cols,
339 rows,
340 padding_right_subpx: 0,
341 padding_bottom_subpx: 0,
342 }),
343 FitPolicy::FitToContainer => fit_internal(viewport, cell, 1, 1),
344 FitPolicy::FitWithMinimum { min_cols, min_rows } => {
345 fit_internal(viewport, cell, min_cols.max(1), min_rows.max(1))
346 }
347 }
348}
349
350fn fit_internal(
351 viewport: &ContainerViewport,
352 cell: &CellMetrics,
353 min_cols: u16,
354 min_rows: u16,
355) -> Result<FitResult, FitError> {
356 let eff_w = viewport.effective_width_subpx();
357 let eff_h = viewport.effective_height_subpx();
358
359 let raw_cols = eff_w / cell.width_subpx;
361 let raw_rows = eff_h / cell.height_subpx;
362
363 let cols = raw_cols.max(min_cols as u32);
364 let rows = raw_rows.max(min_rows as u32);
365
366 if cols == 0 || rows == 0 {
367 return Err(FitError::ContainerTooSmall);
368 }
369 if cols > u16::MAX as u32 || rows > u16::MAX as u32 {
370 return Err(FitError::DimensionOverflow);
371 }
372
373 let cols = cols as u16;
374 let rows = rows as u16;
375
376 let used_w = cols as u32 * cell.width_subpx;
377 let used_h = rows as u32 * cell.height_subpx;
378 let pad_r = eff_w.saturating_sub(used_w);
379 let pad_b = eff_h.saturating_sub(used_h);
380
381 Ok(FitResult {
382 cols,
383 rows,
384 padding_right_subpx: pad_r,
385 padding_bottom_subpx: pad_b,
386 })
387}
388
389#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
398pub struct MetricGeneration(u64);
399
400impl MetricGeneration {
401 pub const ZERO: Self = Self(0);
403
404 #[must_use]
406 pub fn next(self) -> Self {
407 Self(self.0.saturating_add(1))
408 }
409
410 #[must_use]
412 pub const fn get(self) -> u64 {
413 self.0
414 }
415}
416
417impl fmt::Display for MetricGeneration {
418 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
419 write!(f, "gen:{}", self.0)
420 }
421}
422
423#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
431pub enum MetricInvalidation {
432 FontLoaded,
434 DprChanged,
436 ZoomChanged,
438 ContainerResized,
440 FontSizeChanged,
442 FullReset,
444}
445
446const METRIC_INVALIDATION_CANONICAL_ORDER: [MetricInvalidation; 6] = [
447 MetricInvalidation::FullReset,
448 MetricInvalidation::FontSizeChanged,
449 MetricInvalidation::DprChanged,
450 MetricInvalidation::ZoomChanged,
451 MetricInvalidation::FontLoaded,
452 MetricInvalidation::ContainerResized,
453];
454
455impl MetricInvalidation {
456 #[must_use]
458 const fn bit(self) -> u8 {
459 match self {
460 Self::FontLoaded => 1 << 0,
461 Self::DprChanged => 1 << 1,
462 Self::ZoomChanged => 1 << 2,
463 Self::ContainerResized => 1 << 3,
464 Self::FontSizeChanged => 1 << 4,
465 Self::FullReset => 1 << 5,
466 }
467 }
468
469 #[must_use]
471 pub fn ordered_pending_from_mask(mask: u8) -> Vec<Self> {
472 let mut ordered = Vec::with_capacity(METRIC_INVALIDATION_CANONICAL_ORDER.len());
473 for reason in METRIC_INVALIDATION_CANONICAL_ORDER {
474 if mask & reason.bit() != 0 {
475 ordered.push(reason);
476 }
477 }
478 ordered
479 }
480
481 #[must_use]
485 pub fn requires_rasterization(&self) -> bool {
486 matches!(
487 self,
488 Self::FontLoaded | Self::DprChanged | Self::FontSizeChanged | Self::FullReset
489 )
490 }
491
492 #[must_use]
494 pub fn requires_refit(&self) -> bool {
495 true
498 }
499}
500
501impl fmt::Display for MetricInvalidation {
502 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
503 match self {
504 Self::FontLoaded => write!(f, "font_loaded"),
505 Self::DprChanged => write!(f, "dpr_changed"),
506 Self::ZoomChanged => write!(f, "zoom_changed"),
507 Self::ContainerResized => write!(f, "container_resized"),
508 Self::FontSizeChanged => write!(f, "font_size_changed"),
509 Self::FullReset => write!(f, "full_reset"),
510 }
511 }
512}
513
514#[derive(Debug, Clone)]
531pub struct MetricLifecycle {
532 cell_metrics: CellMetrics,
534 viewport: Option<ContainerViewport>,
536 policy: FitPolicy,
538 generation: MetricGeneration,
540 pending_refit: bool,
542 last_invalidation: Option<MetricInvalidation>,
544 pending_invalidation_mask: u8,
546 last_fit: Option<FitResult>,
548 total_invalidations: u64,
550 total_refits: u64,
552}
553
554impl MetricLifecycle {
555 #[must_use]
557 pub fn new(cell_metrics: CellMetrics, policy: FitPolicy) -> Self {
558 Self {
559 cell_metrics,
560 viewport: None,
561 policy,
562 generation: MetricGeneration::ZERO,
563 pending_refit: false,
564 last_invalidation: None,
565 pending_invalidation_mask: 0,
566 last_fit: None,
567 total_invalidations: 0,
568 total_refits: 0,
569 }
570 }
571
572 #[must_use]
574 pub fn cell_metrics(&self) -> &CellMetrics {
575 &self.cell_metrics
576 }
577
578 #[must_use]
580 pub fn generation(&self) -> MetricGeneration {
581 self.generation
582 }
583
584 #[must_use]
586 pub fn is_pending(&self) -> bool {
587 self.pending_refit
588 }
589
590 #[must_use]
592 pub fn last_invalidation(&self) -> Option<MetricInvalidation> {
593 self.last_invalidation
594 }
595
596 #[must_use]
598 pub fn pending_invalidations(&self) -> Vec<MetricInvalidation> {
599 MetricInvalidation::ordered_pending_from_mask(self.pending_invalidation_mask)
600 }
601
602 #[must_use]
604 pub fn last_fit(&self) -> Option<&FitResult> {
605 self.last_fit.as_ref()
606 }
607
608 #[must_use]
610 pub fn total_invalidations(&self) -> u64 {
611 self.total_invalidations
612 }
613
614 #[must_use]
616 pub fn total_refits(&self) -> u64 {
617 self.total_refits
618 }
619
620 pub fn invalidate(&mut self, reason: MetricInvalidation, new_metrics: Option<CellMetrics>) {
625 self.generation = self.generation.next();
626 self.pending_refit = true;
627 self.last_invalidation = Some(reason);
628 self.pending_invalidation_mask |= reason.bit();
629 self.total_invalidations += 1;
630
631 if let Some(metrics) = new_metrics {
632 self.cell_metrics = metrics;
633 }
634 }
635
636 pub fn set_viewport(&mut self, viewport: ContainerViewport) {
640 let changed = self.viewport.is_none_or(|v| v != viewport);
641 if changed {
642 let mut primary_reason = MetricInvalidation::ContainerResized;
643 if let Some(previous) = self.viewport {
644 if previous.dpr_subpx != viewport.dpr_subpx {
645 self.pending_invalidation_mask |= MetricInvalidation::DprChanged.bit();
646 primary_reason = MetricInvalidation::DprChanged;
647 }
648 if previous.zoom_subpx != viewport.zoom_subpx {
649 self.pending_invalidation_mask |= MetricInvalidation::ZoomChanged.bit();
650 if primary_reason == MetricInvalidation::ContainerResized {
651 primary_reason = MetricInvalidation::ZoomChanged;
652 }
653 }
654 if previous.width_px != viewport.width_px
655 || previous.height_px != viewport.height_px
656 {
657 self.pending_invalidation_mask |= MetricInvalidation::ContainerResized.bit();
658 }
659 } else {
660 self.pending_invalidation_mask |= MetricInvalidation::ContainerResized.bit();
661 }
662
663 self.generation = self.generation.next();
664 self.pending_refit = true;
665 self.last_invalidation = Some(primary_reason);
666 self.total_invalidations += 1;
667 }
668 self.viewport = Some(viewport);
669 }
670
671 pub fn set_policy(&mut self, policy: FitPolicy) {
673 if self.policy != policy {
674 self.policy = policy;
675 self.pending_refit = true;
676 }
677 }
678
679 pub fn refit(&mut self) -> Option<FitResult> {
686 if !self.pending_refit {
687 return None;
688 }
689 self.pending_refit = false;
690 self.pending_invalidation_mask = 0;
691 self.total_refits += 1;
692
693 let viewport = self.viewport?;
694 let result = fit_to_container(&viewport, &self.cell_metrics, self.policy).ok()?;
695
696 let changed = self
697 .last_fit
698 .is_none_or(|prev| prev.cols != result.cols || prev.rows != result.rows);
699
700 self.last_fit = Some(result);
701
702 if changed { Some(result) } else { None }
703 }
704
705 #[must_use]
707 pub fn snapshot(&self) -> MetricSnapshot {
708 MetricSnapshot {
709 generation: self.generation.get(),
710 pending_refit: self.pending_refit,
711 cell_width_subpx: self.cell_metrics.width_subpx,
712 cell_height_subpx: self.cell_metrics.height_subpx,
713 viewport_width_px: self.viewport.map(|v| v.width_px).unwrap_or(0),
714 viewport_height_px: self.viewport.map(|v| v.height_px).unwrap_or(0),
715 dpr_subpx: self.viewport.map(|v| v.dpr_subpx).unwrap_or(0),
716 zoom_subpx: self.viewport.map(|v| v.zoom_subpx).unwrap_or(0),
717 fit_cols: self.last_fit.map(|f| f.cols).unwrap_or(0),
718 fit_rows: self.last_fit.map(|f| f.rows).unwrap_or(0),
719 pending_invalidation_mask: self.pending_invalidation_mask,
720 pending_invalidation_count: self.pending_invalidation_mask.count_ones() as u8,
721 total_invalidations: self.total_invalidations,
722 total_refits: self.total_refits,
723 }
724 }
725}
726
727#[derive(Debug, Clone, Copy, PartialEq, Eq)]
731pub struct MetricSnapshot {
732 pub generation: u64,
734 pub pending_refit: bool,
736 pub cell_width_subpx: u32,
738 pub cell_height_subpx: u32,
740 pub viewport_width_px: u32,
742 pub viewport_height_px: u32,
744 pub dpr_subpx: u32,
746 pub zoom_subpx: u32,
748 pub fit_cols: u16,
750 pub fit_rows: u16,
752 pub pending_invalidation_mask: u8,
754 pub pending_invalidation_count: u8,
756 pub total_invalidations: u64,
758 pub total_refits: u64,
760}
761
762#[cfg(test)]
767mod tests {
768 use super::*;
769
770 #[test]
773 fn cell_metrics_default_is_monospace() {
774 let m = CellMetrics::default();
775 assert_eq!(m.width_px(), 8);
776 assert_eq!(m.height_px(), 16);
777 }
778
779 #[test]
780 fn cell_metrics_from_px() {
781 let m = CellMetrics::from_px(9.0, 18.0).unwrap();
782 assert_eq!(m.width_px(), 9);
783 assert_eq!(m.height_px(), 18);
784 }
785
786 #[test]
787 fn cell_metrics_from_px_fractional() {
788 let m = CellMetrics::from_px(8.5, 16.75).unwrap();
789 assert_eq!(m.width_subpx, 2176); assert_eq!(m.height_subpx, 4288); assert_eq!(m.width_px(), 8); assert_eq!(m.height_px(), 16);
793 }
794
795 #[test]
796 fn cell_metrics_rejects_zero() {
797 assert!(CellMetrics::new(0, 256).is_none());
798 assert!(CellMetrics::new(256, 0).is_none());
799 assert!(CellMetrics::new(0, 0).is_none());
800 }
801
802 #[test]
803 fn cell_metrics_rejects_negative_px() {
804 assert!(CellMetrics::from_px(-1.0, 16.0).is_none());
805 assert!(CellMetrics::from_px(8.0, -1.0).is_none());
806 }
807
808 #[test]
809 fn cell_metrics_rejects_nan() {
810 assert!(CellMetrics::from_px(f64::NAN, 16.0).is_none());
811 assert!(CellMetrics::from_px(8.0, f64::INFINITY).is_none());
812 }
813
814 #[test]
815 fn cell_metrics_display() {
816 let m = CellMetrics::MONOSPACE_DEFAULT;
817 let s = format!("{m}");
818 assert!(s.contains("8x16px"));
819 }
820
821 #[test]
822 fn cell_metrics_large_preset() {
823 assert_eq!(CellMetrics::LARGE.width_px(), 10);
824 assert_eq!(CellMetrics::LARGE.height_px(), 20);
825 }
826
827 #[test]
830 fn viewport_simple() {
831 let v = ContainerViewport::simple(800, 600).unwrap();
832 assert_eq!(v.width_px, 800);
833 assert_eq!(v.height_px, 600);
834 assert_eq!(v.dpr_subpx, 256); assert_eq!(v.zoom_subpx, 256); }
837
838 #[test]
839 fn viewport_effective_1x_dpr() {
840 let v = ContainerViewport::simple(800, 600).unwrap();
841 assert_eq!(v.effective_width_subpx(), 800 * SUBPX_SCALE);
843 assert_eq!(v.effective_height_subpx(), 600 * SUBPX_SCALE);
844 }
845
846 #[test]
847 fn viewport_effective_2x_dpr() {
848 let v = ContainerViewport::new(1600, 1200, 2.0, 1.0).unwrap();
849 assert_eq!(v.effective_width_subpx(), 800 * SUBPX_SCALE);
851 assert_eq!(v.effective_height_subpx(), 600 * SUBPX_SCALE);
852 }
853
854 #[test]
855 fn viewport_effective_zoom_150() {
856 let v = ContainerViewport::new(800, 600, 1.0, 1.5).unwrap();
857 let eff = v.effective_width_subpx();
860 assert_eq!(eff, 136533);
861 }
862
863 #[test]
864 fn viewport_rejects_zero_dims() {
865 assert!(ContainerViewport::simple(0, 600).is_none());
866 assert!(ContainerViewport::simple(800, 0).is_none());
867 }
868
869 #[test]
870 fn viewport_rejects_zero_dpr() {
871 assert!(ContainerViewport::new(800, 600, 0.0, 1.0).is_none());
872 }
873
874 #[test]
875 fn viewport_display() {
876 let v = ContainerViewport::simple(800, 600).unwrap();
877 let s = format!("{v}");
878 assert!(s.contains("800x600px"));
879 assert!(s.contains("1.00x DPR"));
880 }
881
882 #[test]
885 fn fit_default_80x24_terminal() {
886 let v = ContainerViewport::simple(640, 384).unwrap();
888 let r =
889 fit_to_container(&v, &CellMetrics::MONOSPACE_DEFAULT, FitPolicy::default()).unwrap();
890 assert_eq!(r.cols, 80);
891 assert_eq!(r.rows, 24);
892 assert_eq!(r.padding_right_subpx, 0);
893 assert_eq!(r.padding_bottom_subpx, 0);
894 }
895
896 #[test]
897 fn fit_with_remainder() {
898 let v = ContainerViewport::simple(645, 390).unwrap();
900 let r =
901 fit_to_container(&v, &CellMetrics::MONOSPACE_DEFAULT, FitPolicy::default()).unwrap();
902 assert_eq!(r.cols, 80);
903 assert_eq!(r.rows, 24);
904 assert_eq!(r.padding_right_subpx, 5 * 256);
905 assert_eq!(r.padding_bottom_subpx, 6 * 256);
906 }
907
908 #[test]
909 fn fit_small_container_clamps_to_1x1() {
910 let v = ContainerViewport::simple(4, 8).unwrap();
912 let r =
913 fit_to_container(&v, &CellMetrics::MONOSPACE_DEFAULT, FitPolicy::default()).unwrap();
914 assert_eq!(r.cols, 1);
915 assert_eq!(r.rows, 1);
916 }
917
918 #[test]
919 fn fit_fixed_ignores_container() {
920 let v = ContainerViewport::simple(100, 100).unwrap();
921 let r = fit_to_container(
922 &v,
923 &CellMetrics::MONOSPACE_DEFAULT,
924 FitPolicy::Fixed { cols: 80, rows: 24 },
925 )
926 .unwrap();
927 assert_eq!(r.cols, 80);
928 assert_eq!(r.rows, 24);
929 }
930
931 #[test]
932 fn fit_with_minimum_guarantees_min_size() {
933 let v = ContainerViewport::simple(40, 48).unwrap();
935 let r = fit_to_container(
936 &v,
937 &CellMetrics::MONOSPACE_DEFAULT,
938 FitPolicy::FitWithMinimum {
939 min_cols: 10,
940 min_rows: 5,
941 },
942 )
943 .unwrap();
944 assert_eq!(r.cols, 10);
945 assert_eq!(r.rows, 5);
946 }
947
948 #[test]
949 fn fit_with_minimum_uses_actual_when_larger() {
950 let v = ContainerViewport::simple(800, 600).unwrap();
951 let r = fit_to_container(
952 &v,
953 &CellMetrics::MONOSPACE_DEFAULT,
954 FitPolicy::FitWithMinimum {
955 min_cols: 10,
956 min_rows: 5,
957 },
958 )
959 .unwrap();
960 assert_eq!(r.cols, 100); assert_eq!(r.rows, 37); }
963
964 #[test]
965 fn fit_result_is_valid() {
966 let r = FitResult {
967 cols: 80,
968 rows: 24,
969 padding_right_subpx: 0,
970 padding_bottom_subpx: 0,
971 };
972 assert!(r.is_valid());
973 }
974
975 #[test]
976 fn fit_result_display() {
977 let r = FitResult {
978 cols: 120,
979 rows: 40,
980 padding_right_subpx: 0,
981 padding_bottom_subpx: 0,
982 };
983 assert_eq!(format!("{r}"), "120x40 cells");
984 }
985
986 #[test]
987 fn fit_at_2x_dpr() {
988 let v = ContainerViewport::new(1600, 768, 2.0, 1.0).unwrap();
990 let r =
991 fit_to_container(&v, &CellMetrics::MONOSPACE_DEFAULT, FitPolicy::default()).unwrap();
992 assert_eq!(r.cols, 100);
993 assert_eq!(r.rows, 24); }
995
996 #[test]
997 fn fit_at_3x_dpr() {
998 let v = ContainerViewport::new(2400, 1152, 3.0, 1.0).unwrap();
999 let r =
1000 fit_to_container(&v, &CellMetrics::MONOSPACE_DEFAULT, FitPolicy::default()).unwrap();
1001 assert_eq!(r.cols, 100); assert_eq!(r.rows, 24); }
1004
1005 #[test]
1006 fn fit_deterministic_across_calls() {
1007 let v = ContainerViewport::simple(800, 600).unwrap();
1008 let m = CellMetrics::MONOSPACE_DEFAULT;
1009 let r1 = fit_to_container(&v, &m, FitPolicy::default()).unwrap();
1010 let r2 = fit_to_container(&v, &m, FitPolicy::default()).unwrap();
1011 assert_eq!(r1, r2);
1012 }
1013
1014 #[test]
1015 fn fit_error_display() {
1016 assert!(!format!("{}", FitError::ContainerTooSmall).is_empty());
1017 assert!(!format!("{}", FitError::DimensionOverflow).is_empty());
1018 }
1019
1020 #[test]
1023 fn generation_starts_at_zero() {
1024 assert_eq!(MetricGeneration::ZERO.get(), 0);
1025 }
1026
1027 #[test]
1028 fn generation_increments() {
1029 let g = MetricGeneration::ZERO.next().next();
1030 assert_eq!(g.get(), 2);
1031 }
1032
1033 #[test]
1034 fn generation_display() {
1035 let s = format!("{}", MetricGeneration::ZERO.next());
1036 assert_eq!(s, "gen:1");
1037 }
1038
1039 #[test]
1040 fn generation_ordering() {
1041 let g0 = MetricGeneration::ZERO;
1042 let g1 = g0.next();
1043 assert!(g1 > g0);
1044 }
1045
1046 #[test]
1049 fn invalidation_requires_rasterization() {
1050 assert!(MetricInvalidation::FontLoaded.requires_rasterization());
1051 assert!(MetricInvalidation::DprChanged.requires_rasterization());
1052 assert!(MetricInvalidation::FontSizeChanged.requires_rasterization());
1053 assert!(MetricInvalidation::FullReset.requires_rasterization());
1054 assert!(!MetricInvalidation::ZoomChanged.requires_rasterization());
1055 assert!(!MetricInvalidation::ContainerResized.requires_rasterization());
1056 }
1057
1058 #[test]
1059 fn invalidation_display() {
1060 assert_eq!(format!("{}", MetricInvalidation::FontLoaded), "font_loaded");
1061 assert_eq!(format!("{}", MetricInvalidation::DprChanged), "dpr_changed");
1062 }
1063
1064 #[test]
1067 fn lifecycle_initial_state() {
1068 let lc = MetricLifecycle::new(CellMetrics::default(), FitPolicy::default());
1069 assert_eq!(lc.generation(), MetricGeneration::ZERO);
1070 assert!(!lc.is_pending());
1071 assert!(lc.last_fit().is_none());
1072 assert_eq!(lc.total_invalidations(), 0);
1073 assert_eq!(lc.total_refits(), 0);
1074 }
1075
1076 #[test]
1077 fn lifecycle_invalidate_bumps_generation() {
1078 let mut lc = MetricLifecycle::new(CellMetrics::default(), FitPolicy::default());
1079 lc.invalidate(MetricInvalidation::FontLoaded, None);
1080 assert_eq!(lc.generation().get(), 1);
1081 assert!(lc.is_pending());
1082 assert_eq!(lc.total_invalidations(), 1);
1083 }
1084
1085 #[test]
1086 fn lifecycle_invalidate_with_new_metrics() {
1087 let mut lc = MetricLifecycle::new(CellMetrics::default(), FitPolicy::default());
1088 let new = CellMetrics::LARGE;
1089 lc.invalidate(MetricInvalidation::FontSizeChanged, Some(new));
1090 assert_eq!(*lc.cell_metrics(), new);
1091 }
1092
1093 #[test]
1094 fn lifecycle_set_viewport_marks_pending() {
1095 let mut lc = MetricLifecycle::new(CellMetrics::default(), FitPolicy::default());
1096 let vp = ContainerViewport::simple(800, 600).unwrap();
1097 lc.set_viewport(vp);
1098 assert!(lc.is_pending());
1099 assert_eq!(lc.generation().get(), 1);
1100 }
1101
1102 #[test]
1103 fn lifecycle_set_viewport_same_no_change() {
1104 let mut lc = MetricLifecycle::new(CellMetrics::default(), FitPolicy::default());
1105 let vp = ContainerViewport::simple(800, 600).unwrap();
1106 lc.set_viewport(vp);
1107 let prev_gen = lc.generation();
1108 lc.set_viewport(vp); assert_eq!(lc.generation(), prev_gen); }
1111
1112 #[test]
1113 fn lifecycle_refit_without_viewport_returns_none() {
1114 let mut lc = MetricLifecycle::new(CellMetrics::default(), FitPolicy::default());
1115 lc.invalidate(MetricInvalidation::FontLoaded, None);
1116 assert!(lc.refit().is_none());
1117 assert!(!lc.is_pending()); }
1119
1120 #[test]
1121 fn lifecycle_refit_computes_grid() {
1122 let mut lc = MetricLifecycle::new(CellMetrics::default(), FitPolicy::default());
1123 lc.set_viewport(ContainerViewport::simple(640, 384).unwrap());
1124 let result = lc.refit().unwrap();
1125 assert_eq!(result.cols, 80);
1126 assert_eq!(result.rows, 24);
1127 assert_eq!(lc.total_refits(), 1);
1128 }
1129
1130 #[test]
1131 fn lifecycle_refit_no_change_returns_none() {
1132 let mut lc = MetricLifecycle::new(CellMetrics::default(), FitPolicy::default());
1133 lc.set_viewport(ContainerViewport::simple(640, 384).unwrap());
1134 let _ = lc.refit(); lc.set_viewport(ContainerViewport::simple(640, 384).unwrap());
1137 }
1139
1140 #[test]
1141 fn lifecycle_refit_detects_dimension_change() {
1142 let mut lc = MetricLifecycle::new(CellMetrics::default(), FitPolicy::default());
1143 lc.set_viewport(ContainerViewport::simple(640, 384).unwrap());
1144 let _ = lc.refit();
1145 lc.set_viewport(ContainerViewport::simple(800, 600).unwrap());
1147 let result = lc.refit().unwrap();
1148 assert_eq!(result.cols, 100);
1149 assert_eq!(result.rows, 37);
1150 }
1151
1152 #[test]
1153 fn lifecycle_set_policy_marks_pending() {
1154 let mut lc = MetricLifecycle::new(CellMetrics::default(), FitPolicy::default());
1155 lc.set_policy(FitPolicy::Fixed { cols: 80, rows: 24 });
1156 assert!(lc.is_pending());
1157 }
1158
1159 #[test]
1160 fn lifecycle_snapshot() {
1161 let mut lc = MetricLifecycle::new(CellMetrics::default(), FitPolicy::default());
1162 lc.set_viewport(ContainerViewport::simple(640, 384).unwrap());
1163 let _ = lc.refit();
1164 let snap = lc.snapshot();
1165 assert_eq!(snap.fit_cols, 80);
1166 assert_eq!(snap.fit_rows, 24);
1167 assert_eq!(snap.viewport_width_px, 640);
1168 assert_eq!(snap.viewport_height_px, 384);
1169 assert_eq!(snap.dpr_subpx, 256);
1170 assert_eq!(snap.zoom_subpx, 256);
1171 assert!(!snap.pending_refit);
1172 }
1173
1174 #[test]
1175 fn lifecycle_multiple_invalidations() {
1176 let mut lc = MetricLifecycle::new(CellMetrics::default(), FitPolicy::default());
1177 lc.set_viewport(ContainerViewport::simple(640, 384).unwrap());
1178 lc.invalidate(MetricInvalidation::FontLoaded, None);
1179 lc.invalidate(MetricInvalidation::DprChanged, None);
1180 lc.invalidate(MetricInvalidation::ZoomChanged, None);
1181 assert!(lc.is_pending());
1183 assert_eq!(lc.total_invalidations(), 4); }
1185
1186 #[test]
1187 fn lifecycle_pending_invalidations_are_canonical() {
1188 let mut lc = MetricLifecycle::new(CellMetrics::default(), FitPolicy::default());
1189 lc.set_viewport(ContainerViewport::simple(640, 384).unwrap());
1190 let _ = lc.refit();
1191
1192 lc.invalidate(MetricInvalidation::FontLoaded, None);
1193 lc.invalidate(MetricInvalidation::ZoomChanged, None);
1194 lc.invalidate(MetricInvalidation::FontLoaded, None); lc.invalidate(MetricInvalidation::FullReset, None);
1196 lc.invalidate(MetricInvalidation::ContainerResized, None);
1197
1198 assert_eq!(
1199 lc.pending_invalidations(),
1200 vec![
1201 MetricInvalidation::FullReset,
1202 MetricInvalidation::ZoomChanged,
1203 MetricInvalidation::FontLoaded,
1204 MetricInvalidation::ContainerResized
1205 ]
1206 );
1207 }
1208
1209 #[test]
1210 fn lifecycle_set_viewport_tracks_dpr_and_zoom_invalidations() {
1211 let mut lc = MetricLifecycle::new(CellMetrics::default(), FitPolicy::default());
1212 lc.set_viewport(ContainerViewport::simple(800, 600).unwrap());
1213 let _ = lc.refit();
1214
1215 lc.set_viewport(ContainerViewport::new(1600, 1200, 2.0, 1.25).unwrap());
1217 assert_eq!(lc.last_invalidation(), Some(MetricInvalidation::DprChanged));
1218 assert_eq!(
1219 lc.pending_invalidations(),
1220 vec![
1221 MetricInvalidation::DprChanged,
1222 MetricInvalidation::ZoomChanged,
1223 MetricInvalidation::ContainerResized
1224 ]
1225 );
1226 }
1227
1228 #[test]
1229 fn lifecycle_delayed_font_load_uses_latest_metrics() {
1230 let mut lc = MetricLifecycle::new(CellMetrics::default(), FitPolicy::default());
1231 lc.set_viewport(ContainerViewport::simple(800, 600).unwrap());
1232 let baseline = lc.refit().unwrap();
1233 assert_eq!(baseline.cols, 100);
1234 assert_eq!(baseline.rows, 37);
1235
1236 let fallback = CellMetrics::from_px(9.0, 18.0).unwrap();
1238 lc.invalidate(MetricInvalidation::FontSizeChanged, Some(fallback));
1239 let fallback_fit = lc.refit().unwrap();
1240 assert_eq!(fallback_fit.cols, 88);
1241 assert_eq!(fallback_fit.rows, 33);
1242
1243 lc.invalidate(MetricInvalidation::FontLoaded, Some(CellMetrics::LARGE));
1245 let final_fit = lc.refit().unwrap();
1246 assert_eq!(final_fit.cols, 80);
1247 assert_eq!(final_fit.rows, 30);
1248 assert_eq!(*lc.cell_metrics(), CellMetrics::LARGE);
1249 }
1250
1251 #[test]
1252 fn lifecycle_font_swap_race_orders_invalidations_deterministically() {
1253 let mut lc = MetricLifecycle::new(CellMetrics::default(), FitPolicy::default());
1254 lc.set_viewport(ContainerViewport::simple(800, 600).unwrap());
1255 let _ = lc.refit();
1256
1257 let fallback = CellMetrics::from_px(9.0, 18.0).unwrap();
1258 let swapped = CellMetrics::from_px(11.0, 22.0).unwrap();
1259 lc.invalidate(MetricInvalidation::FontLoaded, Some(fallback));
1260 lc.invalidate(MetricInvalidation::FontLoaded, Some(swapped));
1261 lc.set_viewport(ContainerViewport::new(1600, 1200, 2.0, 1.25).unwrap());
1262
1263 assert_eq!(
1264 lc.pending_invalidations(),
1265 vec![
1266 MetricInvalidation::DprChanged,
1267 MetricInvalidation::ZoomChanged,
1268 MetricInvalidation::FontLoaded,
1269 MetricInvalidation::ContainerResized
1270 ]
1271 );
1272
1273 let fit = lc.refit().unwrap();
1275 assert_eq!(fit.cols, 58); assert_eq!(fit.rows, 21); assert_eq!(*lc.cell_metrics(), swapped);
1278 }
1279
1280 #[test]
1281 fn lifecycle_dynamic_font_event_stream_keeps_fit_in_sync() {
1282 let mut lc = MetricLifecycle::new(CellMetrics::default(), FitPolicy::default());
1283 lc.set_viewport(ContainerViewport::simple(800, 600).unwrap());
1284 let _ = lc.refit();
1285
1286 let fallback = CellMetrics::from_px(9.0, 18.0).unwrap();
1289 lc.invalidate(MetricInvalidation::FontLoaded, Some(fallback));
1290 lc.set_viewport(ContainerViewport::new(1600, 1200, 2.0, 1.25).unwrap());
1291 lc.invalidate(MetricInvalidation::FontLoaded, Some(CellMetrics::LARGE));
1292
1293 let fit = lc.refit().unwrap();
1294 assert_eq!(fit.cols, 64); assert_eq!(fit.rows, 24); let snap = lc.snapshot();
1298 assert_eq!(snap.fit_cols, fit.cols);
1299 assert_eq!(snap.fit_rows, fit.rows);
1300 assert_eq!(snap.pending_invalidation_mask, 0);
1301 assert_eq!(snap.pending_invalidation_count, 0);
1302 }
1303
1304 #[test]
1305 fn lifecycle_font_size_change_affects_fit() {
1306 let mut lc = MetricLifecycle::new(CellMetrics::default(), FitPolicy::default());
1307 lc.set_viewport(ContainerViewport::simple(800, 600).unwrap());
1308 let first = lc.refit().unwrap();
1309 assert_eq!(first.cols, 100); assert_eq!(first.rows, 37); let big = CellMetrics::new(16 * 256, 32 * 256).unwrap();
1314 lc.invalidate(MetricInvalidation::FontSizeChanged, Some(big));
1315 let second = lc.refit().unwrap();
1316 assert_eq!(second.cols, 50); assert_eq!(second.rows, 18); }
1319
1320 #[test]
1321 fn lifecycle_dpr_change_affects_fit() {
1322 let mut lc = MetricLifecycle::new(CellMetrics::default(), FitPolicy::default());
1323 lc.set_viewport(ContainerViewport::simple(800, 600).unwrap());
1324 let first = lc.refit().unwrap();
1325 assert_eq!(first.cols, 100);
1326
1327 let vp2 = ContainerViewport::new(800, 600, 2.0, 1.0).unwrap();
1329 lc.set_viewport(vp2);
1330 let second = lc.refit().unwrap();
1331 assert_eq!(second.cols, 50); }
1333
1334 #[test]
1337 fn subpx_conversion_zero() {
1338 assert_eq!(px_to_subpx(0.0), Some(0));
1339 }
1340
1341 #[test]
1342 fn subpx_conversion_negative() {
1343 assert_eq!(px_to_subpx(-1.0), None);
1344 }
1345
1346 #[test]
1347 fn subpx_conversion_nan() {
1348 assert_eq!(px_to_subpx(f64::NAN), None);
1349 }
1350
1351 #[test]
1352 fn subpx_conversion_infinity() {
1353 assert_eq!(px_to_subpx(f64::INFINITY), None);
1354 }
1355
1356 #[test]
1357 fn subpx_conversion_precise() {
1358 assert_eq!(px_to_subpx(1.0), Some(256));
1359 assert_eq!(px_to_subpx(0.5), Some(128));
1360 assert_eq!(px_to_subpx(2.0), Some(512));
1361 }
1362}