1use std::fmt;
26
27use crate::justification::{JustificationControl, JustifyMode};
28use crate::vertical_metrics::{VerticalMetrics, VerticalPolicy};
29use crate::wrap::ParagraphObjective;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
40pub enum LayoutTier {
41 Emergency = 0,
45 Fast = 1,
48 #[default]
51 Balanced = 2,
52 Quality = 3,
55}
56
57impl LayoutTier {
58 #[must_use]
60 pub const fn degrade(&self) -> Option<Self> {
61 match self {
62 Self::Quality => Some(Self::Balanced),
63 Self::Balanced => Some(Self::Fast),
64 Self::Fast => Some(Self::Emergency),
65 Self::Emergency => None,
66 }
67 }
68
69 #[must_use]
71 pub fn degradation_chain(&self) -> Vec<Self> {
72 let mut chain = vec![*self];
73 let mut current = *self;
74 while let Some(next) = current.degrade() {
75 chain.push(next);
76 current = next;
77 }
78 chain
79 }
80}
81
82impl fmt::Display for LayoutTier {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 match self {
85 Self::Emergency => write!(f, "emergency"),
86 Self::Fast => write!(f, "fast"),
87 Self::Balanced => write!(f, "balanced"),
88 Self::Quality => write!(f, "quality"),
89 }
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
102pub struct RuntimeCapability {
103 pub proportional_fonts: bool,
106
107 pub subpixel_positioning: bool,
110
111 pub hyphenation_available: bool,
114
115 pub tracking_support: bool,
117
118 pub max_paragraph_words: usize,
121}
122
123impl RuntimeCapability {
124 pub const FULL: Self = Self {
126 proportional_fonts: true,
127 subpixel_positioning: true,
128 hyphenation_available: true,
129 tracking_support: true,
130 max_paragraph_words: 0,
131 };
132
133 pub const TERMINAL: Self = Self {
135 proportional_fonts: false,
136 subpixel_positioning: false,
137 hyphenation_available: false,
138 tracking_support: false,
139 max_paragraph_words: 0,
140 };
141
142 pub const WEB: Self = Self {
144 proportional_fonts: true,
145 subpixel_positioning: true,
146 hyphenation_available: true,
147 tracking_support: false,
148 max_paragraph_words: 0,
149 };
150
151 #[must_use]
153 pub fn supports_tier(&self, tier: LayoutTier) -> bool {
154 match tier {
155 LayoutTier::Emergency => true, LayoutTier::Fast => true, LayoutTier::Balanced => true, LayoutTier::Quality => {
159 self.proportional_fonts
161 }
162 }
163 }
164
165 #[must_use]
167 pub fn best_tier(&self) -> LayoutTier {
168 if self.supports_tier(LayoutTier::Quality) {
169 LayoutTier::Quality
170 } else if self.supports_tier(LayoutTier::Balanced) {
171 LayoutTier::Balanced
172 } else {
173 LayoutTier::Fast
174 }
175 }
176}
177
178impl fmt::Display for RuntimeCapability {
179 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180 write!(
181 f,
182 "proportional={} subpixel={} hyphen={} tracking={}",
183 self.proportional_fonts,
184 self.subpixel_positioning,
185 self.hyphenation_available,
186 self.tracking_support
187 )
188 }
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
200pub struct LayoutPolicy {
201 pub tier: LayoutTier,
203 pub allow_degradation: bool,
206 pub justify_override: Option<JustifyMode>,
208 pub vertical_override: Option<VerticalPolicy>,
210 pub line_height_subpx: u32,
213}
214
215const DEFAULT_LINE_HEIGHT_SUBPX: u32 = 16 * 256;
217
218impl LayoutPolicy {
219 pub const EMERGENCY: Self = Self {
221 tier: LayoutTier::Emergency,
222 allow_degradation: false, justify_override: None,
224 vertical_override: None,
225 line_height_subpx: 0,
226 };
227
228 pub const FAST: Self = Self {
230 tier: LayoutTier::Fast,
231 allow_degradation: true,
232 justify_override: None,
233 vertical_override: None,
234 line_height_subpx: 0,
235 };
236
237 pub const BALANCED: Self = Self {
239 tier: LayoutTier::Balanced,
240 allow_degradation: true,
241 justify_override: None,
242 vertical_override: None,
243 line_height_subpx: 0,
244 };
245
246 pub const QUALITY: Self = Self {
248 tier: LayoutTier::Quality,
249 allow_degradation: true,
250 justify_override: None,
251 vertical_override: None,
252 line_height_subpx: 0,
253 };
254
255 #[must_use]
257 pub const fn effective_line_height(&self) -> u32 {
258 if self.line_height_subpx == 0 {
259 DEFAULT_LINE_HEIGHT_SUBPX
260 } else {
261 self.line_height_subpx
262 }
263 }
264
265 pub fn resolve(&self, caps: &RuntimeCapability) -> Result<ResolvedPolicy, PolicyError> {
275 let mut effective_tier = self.tier;
276
277 if !caps.supports_tier(effective_tier) {
279 if self.allow_degradation {
280 effective_tier = caps.best_tier();
281 } else {
282 return Err(PolicyError::CapabilityInsufficient {
283 requested: self.tier,
284 best_available: caps.best_tier(),
285 });
286 }
287 }
288
289 let line_h = self.effective_line_height();
290
291 let objective = match effective_tier {
293 LayoutTier::Emergency | LayoutTier::Fast => ParagraphObjective::terminal(),
294 LayoutTier::Balanced => ParagraphObjective::default(),
295 LayoutTier::Quality => ParagraphObjective::typographic(),
296 };
297
298 let vertical_policy = self.vertical_override.unwrap_or(match effective_tier {
299 LayoutTier::Emergency | LayoutTier::Fast => VerticalPolicy::Compact,
300 LayoutTier::Balanced => VerticalPolicy::Readable,
301 LayoutTier::Quality => VerticalPolicy::Typographic,
302 });
303
304 let vertical = vertical_policy.resolve(line_h);
305
306 let mut justification = match effective_tier {
307 LayoutTier::Emergency | LayoutTier::Fast => JustificationControl::TERMINAL,
308 LayoutTier::Balanced => JustificationControl::READABLE,
309 LayoutTier::Quality => JustificationControl::TYPOGRAPHIC,
310 };
311
312 if let Some(mode) = self.justify_override {
314 justification.mode = mode;
315 }
316
317 if !caps.tracking_support {
319 justification.char_space = crate::justification::GlueSpec::rigid(0);
321 }
322
323 if !caps.proportional_fonts {
324 justification.word_space =
326 crate::justification::GlueSpec::rigid(crate::justification::SUBCELL_SCALE);
327 justification.sentence_space =
328 crate::justification::GlueSpec::rigid(crate::justification::SUBCELL_SCALE);
329 justification.char_space = crate::justification::GlueSpec::rigid(0);
330 }
331
332 let degraded = effective_tier != self.tier;
333
334 Ok(ResolvedPolicy {
335 requested_tier: self.tier,
336 effective_tier,
337 degraded,
338 objective,
339 vertical,
340 justification,
341 use_hyphenation: caps.hyphenation_available && effective_tier >= LayoutTier::Balanced,
342 use_optimal_breaking: effective_tier >= LayoutTier::Balanced,
343 line_height_subpx: line_h,
344 })
345 }
346}
347
348impl Default for LayoutPolicy {
349 fn default() -> Self {
350 Self::BALANCED
351 }
352}
353
354impl fmt::Display for LayoutPolicy {
355 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
356 write!(f, "tier={} degrade={}", self.tier, self.allow_degradation)
357 }
358}
359
360#[derive(Debug, Clone, PartialEq)]
369pub struct ResolvedPolicy {
370 pub requested_tier: LayoutTier,
372 pub effective_tier: LayoutTier,
374 pub degraded: bool,
376
377 pub objective: ParagraphObjective,
379 pub vertical: VerticalMetrics,
381 pub justification: JustificationControl,
383
384 pub use_hyphenation: bool,
386 pub use_optimal_breaking: bool,
389 pub line_height_subpx: u32,
391}
392
393impl ResolvedPolicy {
394 #[must_use]
396 pub fn is_justified(&self) -> bool {
397 self.justification.mode.requires_justification()
398 }
399
400 #[must_use]
402 pub fn feature_summary(&self) -> Vec<&'static str> {
403 let mut features = Vec::new();
404
405 if self.use_optimal_breaking {
406 features.push("optimal-breaking");
407 } else {
408 features.push("greedy-wrapping");
409 }
410
411 if self.is_justified() {
412 features.push("justified");
413 }
414
415 if self.use_hyphenation {
416 features.push("hyphenation");
417 }
418
419 if self.vertical.baseline_grid.is_active() {
420 features.push("baseline-grid");
421 }
422
423 if self.vertical.first_line_indent_subpx > 0 {
424 features.push("first-line-indent");
425 }
426
427 if !self.justification.char_space.is_rigid() {
428 features.push("tracking");
429 }
430
431 features
432 }
433}
434
435impl fmt::Display for ResolvedPolicy {
436 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
437 write!(
438 f,
439 "{} (requested {}{})",
440 self.effective_tier,
441 self.requested_tier,
442 if self.degraded { ", degraded" } else { "" }
443 )
444 }
445}
446
447#[derive(Debug, Clone, PartialEq, Eq)]
453pub enum PolicyError {
454 CapabilityInsufficient {
456 requested: LayoutTier,
458 best_available: LayoutTier,
460 },
461}
462
463impl fmt::Display for PolicyError {
464 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
465 match self {
466 Self::CapabilityInsufficient {
467 requested,
468 best_available,
469 } => write!(
470 f,
471 "requested tier '{}' not supported; best available is '{}'",
472 requested, best_available
473 ),
474 }
475 }
476}
477
478impl std::error::Error for PolicyError {}
479
480#[cfg(test)]
485mod tests {
486 use super::*;
487 use crate::justification::JustifyMode;
488 use crate::vertical_metrics::VerticalPolicy;
489
490 #[test]
493 fn tier_ordering() {
494 assert!(LayoutTier::Emergency < LayoutTier::Fast);
495 assert!(LayoutTier::Fast < LayoutTier::Balanced);
496 assert!(LayoutTier::Balanced < LayoutTier::Quality);
497 }
498
499 #[test]
500 fn tier_degrade_quality() {
501 assert_eq!(LayoutTier::Quality.degrade(), Some(LayoutTier::Balanced));
502 }
503
504 #[test]
505 fn tier_degrade_balanced() {
506 assert_eq!(LayoutTier::Balanced.degrade(), Some(LayoutTier::Fast));
507 }
508
509 #[test]
510 fn tier_degrade_fast_is_emergency() {
511 assert_eq!(LayoutTier::Fast.degrade(), Some(LayoutTier::Emergency));
512 }
513
514 #[test]
515 fn tier_degrade_emergency_is_none() {
516 assert_eq!(LayoutTier::Emergency.degrade(), None);
517 }
518
519 #[test]
520 fn tier_degradation_chain_quality() {
521 let chain = LayoutTier::Quality.degradation_chain();
522 assert_eq!(
523 chain,
524 vec![
525 LayoutTier::Quality,
526 LayoutTier::Balanced,
527 LayoutTier::Fast,
528 LayoutTier::Emergency,
529 ]
530 );
531 }
532
533 #[test]
534 fn tier_degradation_chain_fast() {
535 let chain = LayoutTier::Fast.degradation_chain();
536 assert_eq!(chain, vec![LayoutTier::Fast, LayoutTier::Emergency]);
537 }
538
539 #[test]
540 fn tier_degradation_chain_emergency() {
541 let chain = LayoutTier::Emergency.degradation_chain();
542 assert_eq!(chain, vec![LayoutTier::Emergency]);
543 }
544
545 #[test]
546 fn tier_default_is_balanced() {
547 assert_eq!(LayoutTier::default(), LayoutTier::Balanced);
548 }
549
550 #[test]
551 fn tier_display() {
552 assert_eq!(format!("{}", LayoutTier::Emergency), "emergency");
553 assert_eq!(format!("{}", LayoutTier::Quality), "quality");
554 assert_eq!(format!("{}", LayoutTier::Balanced), "balanced");
555 assert_eq!(format!("{}", LayoutTier::Fast), "fast");
556 }
557
558 #[test]
561 fn terminal_caps_support_fast() {
562 assert!(RuntimeCapability::TERMINAL.supports_tier(LayoutTier::Fast));
563 }
564
565 #[test]
566 fn terminal_caps_support_balanced() {
567 assert!(RuntimeCapability::TERMINAL.supports_tier(LayoutTier::Balanced));
568 }
569
570 #[test]
571 fn terminal_caps_not_quality() {
572 assert!(!RuntimeCapability::TERMINAL.supports_tier(LayoutTier::Quality));
573 }
574
575 #[test]
576 fn full_caps_support_all() {
577 assert!(RuntimeCapability::FULL.supports_tier(LayoutTier::Quality));
578 }
579
580 #[test]
581 fn terminal_best_tier_is_balanced() {
582 assert_eq!(
583 RuntimeCapability::TERMINAL.best_tier(),
584 LayoutTier::Balanced
585 );
586 }
587
588 #[test]
589 fn full_best_tier_is_quality() {
590 assert_eq!(RuntimeCapability::FULL.best_tier(), LayoutTier::Quality);
591 }
592
593 #[test]
594 fn web_best_tier_is_quality() {
595 assert_eq!(RuntimeCapability::WEB.best_tier(), LayoutTier::Quality);
596 }
597
598 #[test]
599 fn default_caps_are_terminal() {
600 let caps = RuntimeCapability::default();
601 assert!(!caps.proportional_fonts);
602 assert!(!caps.subpixel_positioning);
603 }
604
605 #[test]
606 fn capability_display() {
607 let s = format!("{}", RuntimeCapability::FULL);
608 assert!(s.contains("proportional=true"));
609 }
610
611 #[test]
614 fn fast_resolves_with_terminal_caps() {
615 let result = LayoutPolicy::FAST.resolve(&RuntimeCapability::TERMINAL);
616 let resolved = result.unwrap();
617 assert_eq!(resolved.effective_tier, LayoutTier::Fast);
618 assert!(!resolved.degraded);
619 assert!(!resolved.use_optimal_breaking);
620 }
621
622 #[test]
623 fn balanced_resolves_with_terminal_caps() {
624 let result = LayoutPolicy::BALANCED.resolve(&RuntimeCapability::TERMINAL);
625 let resolved = result.unwrap();
626 assert_eq!(resolved.effective_tier, LayoutTier::Balanced);
627 assert!(!resolved.degraded);
628 assert!(resolved.use_optimal_breaking);
629 }
630
631 #[test]
632 fn quality_degrades_on_terminal() {
633 let result = LayoutPolicy::QUALITY.resolve(&RuntimeCapability::TERMINAL);
634 let resolved = result.unwrap();
635 assert!(resolved.degraded);
636 assert_eq!(resolved.effective_tier, LayoutTier::Balanced);
637 assert_eq!(resolved.requested_tier, LayoutTier::Quality);
638 }
639
640 #[test]
641 fn quality_resolves_with_full_caps() {
642 let result = LayoutPolicy::QUALITY.resolve(&RuntimeCapability::FULL);
643 let resolved = result.unwrap();
644 assert_eq!(resolved.effective_tier, LayoutTier::Quality);
645 assert!(!resolved.degraded);
646 assert!(resolved.is_justified());
647 assert!(resolved.use_hyphenation);
648 }
649
650 #[test]
651 fn degradation_disabled_returns_error() {
652 let policy = LayoutPolicy {
653 tier: LayoutTier::Quality,
654 allow_degradation: false,
655 ..LayoutPolicy::QUALITY
656 };
657 let result = policy.resolve(&RuntimeCapability::TERMINAL);
658 assert!(result.is_err());
659 if let Err(PolicyError::CapabilityInsufficient {
660 requested,
661 best_available,
662 }) = result
663 {
664 assert_eq!(requested, LayoutTier::Quality);
665 assert_eq!(best_available, LayoutTier::Balanced);
666 }
667 }
668
669 #[test]
672 fn justify_override_applied() {
673 let policy = LayoutPolicy {
674 justify_override: Some(JustifyMode::Center),
675 ..LayoutPolicy::BALANCED
676 };
677 let resolved = policy.resolve(&RuntimeCapability::TERMINAL).unwrap();
678 assert_eq!(resolved.justification.mode, JustifyMode::Center);
679 }
680
681 #[test]
682 fn vertical_override_applied() {
683 let policy = LayoutPolicy {
684 vertical_override: Some(VerticalPolicy::Typographic),
685 ..LayoutPolicy::FAST
686 };
687 let resolved = policy.resolve(&RuntimeCapability::TERMINAL).unwrap();
688 assert!(resolved.vertical.baseline_grid.is_active());
690 }
691
692 #[test]
693 fn custom_line_height() {
694 let policy = LayoutPolicy {
695 line_height_subpx: 20 * 256, ..LayoutPolicy::BALANCED
697 };
698 let resolved = policy.resolve(&RuntimeCapability::TERMINAL).unwrap();
699 assert_eq!(resolved.line_height_subpx, 20 * 256);
700 }
701
702 #[test]
703 fn default_line_height_is_16px() {
704 let policy = LayoutPolicy::BALANCED;
705 assert_eq!(policy.effective_line_height(), 16 * 256);
706 }
707
708 #[test]
711 fn no_tracking_disables_char_space() {
712 let caps = RuntimeCapability {
713 proportional_fonts: true,
714 tracking_support: false,
715 ..RuntimeCapability::FULL
716 };
717 let resolved = LayoutPolicy::QUALITY.resolve(&caps).unwrap();
718 assert!(resolved.justification.char_space.is_rigid());
719 }
720
721 #[test]
722 fn monospace_makes_spaces_rigid() {
723 let resolved = LayoutPolicy::BALANCED
724 .resolve(&RuntimeCapability::TERMINAL)
725 .unwrap();
726 assert!(resolved.justification.word_space.is_rigid());
727 assert!(resolved.justification.sentence_space.is_rigid());
728 }
729
730 #[test]
731 fn no_hyphenation_dict_disables_hyphenation() {
732 let caps = RuntimeCapability {
733 proportional_fonts: true,
734 hyphenation_available: false,
735 ..RuntimeCapability::FULL
736 };
737 let resolved = LayoutPolicy::QUALITY.resolve(&caps).unwrap();
738 assert!(!resolved.use_hyphenation);
739 }
740
741 #[test]
744 fn fast_not_justified() {
745 let resolved = LayoutPolicy::FAST
746 .resolve(&RuntimeCapability::TERMINAL)
747 .unwrap();
748 assert!(!resolved.is_justified());
749 }
750
751 #[test]
752 fn quality_is_justified() {
753 let resolved = LayoutPolicy::QUALITY
754 .resolve(&RuntimeCapability::FULL)
755 .unwrap();
756 assert!(resolved.is_justified());
757 }
758
759 #[test]
760 fn feature_summary_fast() {
761 let resolved = LayoutPolicy::FAST
762 .resolve(&RuntimeCapability::TERMINAL)
763 .unwrap();
764 let features = resolved.feature_summary();
765 assert!(features.contains(&"greedy-wrapping"));
766 assert!(!features.contains(&"justified"));
767 }
768
769 #[test]
770 fn feature_summary_quality() {
771 let resolved = LayoutPolicy::QUALITY
772 .resolve(&RuntimeCapability::FULL)
773 .unwrap();
774 let features = resolved.feature_summary();
775 assert!(features.contains(&"optimal-breaking"));
776 assert!(features.contains(&"justified"));
777 assert!(features.contains(&"hyphenation"));
778 assert!(features.contains(&"baseline-grid"));
779 assert!(features.contains(&"first-line-indent"));
780 assert!(features.contains(&"tracking"));
781 }
782
783 #[test]
784 fn resolved_display_no_degradation() {
785 let resolved = LayoutPolicy::BALANCED
786 .resolve(&RuntimeCapability::TERMINAL)
787 .unwrap();
788 let s = format!("{resolved}");
789 assert!(s.contains("balanced"));
790 assert!(!s.contains("degraded"));
791 }
792
793 #[test]
794 fn resolved_display_with_degradation() {
795 let resolved = LayoutPolicy::QUALITY
796 .resolve(&RuntimeCapability::TERMINAL)
797 .unwrap();
798 let s = format!("{resolved}");
799 assert!(s.contains("degraded"));
800 }
801
802 #[test]
805 fn error_display() {
806 let err = PolicyError::CapabilityInsufficient {
807 requested: LayoutTier::Quality,
808 best_available: LayoutTier::Fast,
809 };
810 let s = format!("{err}");
811 assert!(s.contains("quality"));
812 assert!(s.contains("fast"));
813 }
814
815 #[test]
816 fn error_is_error_trait() {
817 let err = PolicyError::CapabilityInsufficient {
818 requested: LayoutTier::Quality,
819 best_available: LayoutTier::Fast,
820 };
821 let _: &dyn std::error::Error = &err;
822 }
823
824 #[test]
827 fn default_policy_is_balanced() {
828 assert_eq!(LayoutPolicy::default(), LayoutPolicy::BALANCED);
829 }
830
831 #[test]
832 fn policy_display() {
833 let s = format!("{}", LayoutPolicy::QUALITY);
834 assert!(s.contains("quality"));
835 }
836
837 #[test]
840 fn same_inputs_same_resolution() {
841 let p1 = LayoutPolicy::QUALITY
842 .resolve(&RuntimeCapability::FULL)
843 .unwrap();
844 let p2 = LayoutPolicy::QUALITY
845 .resolve(&RuntimeCapability::FULL)
846 .unwrap();
847 assert_eq!(p1, p2);
848 }
849
850 #[test]
851 fn same_degradation_same_result() {
852 let p1 = LayoutPolicy::QUALITY
853 .resolve(&RuntimeCapability::TERMINAL)
854 .unwrap();
855 let p2 = LayoutPolicy::QUALITY
856 .resolve(&RuntimeCapability::TERMINAL)
857 .unwrap();
858 assert_eq!(p1, p2);
859 }
860
861 #[test]
864 fn emergency_resolves_with_terminal_caps() {
865 let result = LayoutPolicy::EMERGENCY.resolve(&RuntimeCapability::TERMINAL);
866 let resolved = result.unwrap();
867 assert_eq!(resolved.effective_tier, LayoutTier::Emergency);
868 assert!(!resolved.degraded);
869 assert!(!resolved.use_optimal_breaking);
870 assert!(!resolved.use_hyphenation);
871 assert!(!resolved.is_justified());
872 }
873
874 #[test]
875 fn emergency_caps_supported() {
876 assert!(RuntimeCapability::TERMINAL.supports_tier(LayoutTier::Emergency));
877 assert!(RuntimeCapability::FULL.supports_tier(LayoutTier::Emergency));
878 }
879
880 #[test]
881 fn fast_with_full_caps_stays_fast() {
882 let resolved = LayoutPolicy::FAST
883 .resolve(&RuntimeCapability::FULL)
884 .unwrap();
885 assert_eq!(resolved.effective_tier, LayoutTier::Fast);
886 assert!(!resolved.degraded);
887 }
888
889 #[test]
890 fn quality_with_justify_left_override() {
891 let policy = LayoutPolicy {
892 justify_override: Some(JustifyMode::Left),
893 ..LayoutPolicy::QUALITY
894 };
895 let resolved = policy.resolve(&RuntimeCapability::FULL).unwrap();
896 assert!(!resolved.is_justified());
897 assert_eq!(resolved.effective_tier, LayoutTier::Quality);
899 }
900}