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 ligature_support: bool,
123
124 pub max_paragraph_words: usize,
127}
128
129impl RuntimeCapability {
130 pub const FULL: Self = Self {
132 proportional_fonts: true,
133 subpixel_positioning: true,
134 hyphenation_available: true,
135 tracking_support: true,
136 ligature_support: true,
137 max_paragraph_words: 0,
138 };
139
140 pub const TERMINAL: Self = Self {
142 proportional_fonts: false,
143 subpixel_positioning: false,
144 hyphenation_available: false,
145 tracking_support: false,
146 ligature_support: false,
147 max_paragraph_words: 0,
148 };
149
150 pub const WEB: Self = Self {
152 proportional_fonts: true,
153 subpixel_positioning: true,
154 hyphenation_available: true,
155 tracking_support: false,
156 ligature_support: true,
157 max_paragraph_words: 0,
158 };
159
160 #[must_use]
162 pub fn supports_tier(&self, tier: LayoutTier) -> bool {
163 match tier {
164 LayoutTier::Emergency => true, LayoutTier::Fast => true, LayoutTier::Balanced => true, LayoutTier::Quality => {
168 self.proportional_fonts
170 }
171 }
172 }
173
174 #[must_use]
176 pub fn best_tier(&self) -> LayoutTier {
177 if self.supports_tier(LayoutTier::Quality) {
178 LayoutTier::Quality
179 } else if self.supports_tier(LayoutTier::Balanced) {
180 LayoutTier::Balanced
181 } else {
182 LayoutTier::Fast
183 }
184 }
185}
186
187impl fmt::Display for RuntimeCapability {
188 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189 write!(
190 f,
191 "proportional={} subpixel={} hyphen={} tracking={} ligature={}",
192 self.proportional_fonts,
193 self.subpixel_positioning,
194 self.hyphenation_available,
195 self.tracking_support,
196 self.ligature_support
197 )
198 }
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
210pub struct LayoutPolicy {
211 pub tier: LayoutTier,
213 pub allow_degradation: bool,
216 pub justify_override: Option<JustifyMode>,
218 pub vertical_override: Option<VerticalPolicy>,
220 pub line_height_subpx: u32,
223}
224
225const DEFAULT_LINE_HEIGHT_SUBPX: u32 = 16 * 256;
227
228impl LayoutPolicy {
229 pub const EMERGENCY: Self = Self {
231 tier: LayoutTier::Emergency,
232 allow_degradation: false, justify_override: None,
234 vertical_override: None,
235 line_height_subpx: 0,
236 };
237
238 pub const FAST: Self = Self {
240 tier: LayoutTier::Fast,
241 allow_degradation: true,
242 justify_override: None,
243 vertical_override: None,
244 line_height_subpx: 0,
245 };
246
247 pub const BALANCED: Self = Self {
249 tier: LayoutTier::Balanced,
250 allow_degradation: true,
251 justify_override: None,
252 vertical_override: None,
253 line_height_subpx: 0,
254 };
255
256 pub const QUALITY: Self = Self {
258 tier: LayoutTier::Quality,
259 allow_degradation: true,
260 justify_override: None,
261 vertical_override: None,
262 line_height_subpx: 0,
263 };
264
265 #[must_use]
267 pub const fn effective_line_height(&self) -> u32 {
268 if self.line_height_subpx == 0 {
269 DEFAULT_LINE_HEIGHT_SUBPX
270 } else {
271 self.line_height_subpx
272 }
273 }
274
275 pub fn resolve(&self, caps: &RuntimeCapability) -> Result<ResolvedPolicy, PolicyError> {
285 let mut effective_tier = self.tier;
286
287 if !caps.supports_tier(effective_tier) {
289 if self.allow_degradation {
290 effective_tier = caps.best_tier();
291 } else {
292 return Err(PolicyError::CapabilityInsufficient {
293 requested: self.tier,
294 best_available: caps.best_tier(),
295 });
296 }
297 }
298
299 let line_h = self.effective_line_height();
300
301 let objective = match effective_tier {
303 LayoutTier::Emergency | LayoutTier::Fast => ParagraphObjective::terminal(),
304 LayoutTier::Balanced => ParagraphObjective::default(),
305 LayoutTier::Quality => ParagraphObjective::typographic(),
306 };
307
308 let vertical_policy = self.vertical_override.unwrap_or(match effective_tier {
309 LayoutTier::Emergency | LayoutTier::Fast => VerticalPolicy::Compact,
310 LayoutTier::Balanced => VerticalPolicy::Readable,
311 LayoutTier::Quality => VerticalPolicy::Typographic,
312 });
313
314 let vertical = vertical_policy.resolve(line_h);
315
316 let mut justification = match effective_tier {
317 LayoutTier::Emergency | LayoutTier::Fast => JustificationControl::TERMINAL,
318 LayoutTier::Balanced => JustificationControl::READABLE,
319 LayoutTier::Quality => JustificationControl::TYPOGRAPHIC,
320 };
321
322 if let Some(mode) = self.justify_override {
324 justification.mode = mode;
325 }
326
327 if !caps.tracking_support {
329 justification.char_space = crate::justification::GlueSpec::rigid(0);
331 }
332
333 if !caps.proportional_fonts {
334 justification.word_space =
336 crate::justification::GlueSpec::rigid(crate::justification::SUBCELL_SCALE);
337 justification.sentence_space =
338 crate::justification::GlueSpec::rigid(crate::justification::SUBCELL_SCALE);
339 justification.char_space = crate::justification::GlueSpec::rigid(0);
340 }
341
342 let degraded = effective_tier != self.tier;
343
344 Ok(ResolvedPolicy {
345 requested_tier: self.tier,
346 effective_tier,
347 degraded,
348 objective,
349 vertical,
350 justification,
351 use_hyphenation: caps.hyphenation_available && effective_tier >= LayoutTier::Balanced,
352 use_optimal_breaking: effective_tier >= LayoutTier::Balanced,
353 line_height_subpx: line_h,
354 })
355 }
356}
357
358impl Default for LayoutPolicy {
359 fn default() -> Self {
360 Self::BALANCED
361 }
362}
363
364impl fmt::Display for LayoutPolicy {
365 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
366 write!(f, "tier={} degrade={}", self.tier, self.allow_degradation)
367 }
368}
369
370#[derive(Debug, Clone, PartialEq)]
379pub struct ResolvedPolicy {
380 pub requested_tier: LayoutTier,
382 pub effective_tier: LayoutTier,
384 pub degraded: bool,
386
387 pub objective: ParagraphObjective,
389 pub vertical: VerticalMetrics,
391 pub justification: JustificationControl,
393
394 pub use_hyphenation: bool,
396 pub use_optimal_breaking: bool,
399 pub line_height_subpx: u32,
401}
402
403impl ResolvedPolicy {
404 #[must_use]
406 pub fn is_justified(&self) -> bool {
407 self.justification.mode.requires_justification()
408 }
409
410 #[must_use]
412 pub fn feature_summary(&self) -> Vec<&'static str> {
413 let mut features = Vec::new();
414
415 if self.use_optimal_breaking {
416 features.push("optimal-breaking");
417 } else {
418 features.push("greedy-wrapping");
419 }
420
421 if self.is_justified() {
422 features.push("justified");
423 }
424
425 if self.use_hyphenation {
426 features.push("hyphenation");
427 }
428
429 if self.vertical.baseline_grid.is_active() {
430 features.push("baseline-grid");
431 }
432
433 if self.vertical.first_line_indent_subpx > 0 {
434 features.push("first-line-indent");
435 }
436
437 if !self.justification.char_space.is_rigid() {
438 features.push("tracking");
439 }
440
441 features
442 }
443}
444
445impl fmt::Display for ResolvedPolicy {
446 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
447 write!(
448 f,
449 "{} (requested {}{})",
450 self.effective_tier,
451 self.requested_tier,
452 if self.degraded { ", degraded" } else { "" }
453 )
454 }
455}
456
457#[derive(Debug, Clone, PartialEq, Eq)]
463pub enum PolicyError {
464 CapabilityInsufficient {
466 requested: LayoutTier,
468 best_available: LayoutTier,
470 },
471}
472
473impl fmt::Display for PolicyError {
474 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
475 match self {
476 Self::CapabilityInsufficient {
477 requested,
478 best_available,
479 } => write!(
480 f,
481 "requested tier '{}' not supported; best available is '{}'",
482 requested, best_available
483 ),
484 }
485 }
486}
487
488impl std::error::Error for PolicyError {}
489
490#[cfg(test)]
495mod tests {
496 use super::*;
497 use crate::justification::JustifyMode;
498 use crate::vertical_metrics::VerticalPolicy;
499
500 #[test]
503 fn tier_ordering() {
504 assert!(LayoutTier::Emergency < LayoutTier::Fast);
505 assert!(LayoutTier::Fast < LayoutTier::Balanced);
506 assert!(LayoutTier::Balanced < LayoutTier::Quality);
507 }
508
509 #[test]
510 fn tier_degrade_quality() {
511 assert_eq!(LayoutTier::Quality.degrade(), Some(LayoutTier::Balanced));
512 }
513
514 #[test]
515 fn tier_degrade_balanced() {
516 assert_eq!(LayoutTier::Balanced.degrade(), Some(LayoutTier::Fast));
517 }
518
519 #[test]
520 fn tier_degrade_fast_is_emergency() {
521 assert_eq!(LayoutTier::Fast.degrade(), Some(LayoutTier::Emergency));
522 }
523
524 #[test]
525 fn tier_degrade_emergency_is_none() {
526 assert_eq!(LayoutTier::Emergency.degrade(), None);
527 }
528
529 #[test]
530 fn tier_degradation_chain_quality() {
531 let chain = LayoutTier::Quality.degradation_chain();
532 assert_eq!(
533 chain,
534 vec![
535 LayoutTier::Quality,
536 LayoutTier::Balanced,
537 LayoutTier::Fast,
538 LayoutTier::Emergency,
539 ]
540 );
541 }
542
543 #[test]
544 fn tier_degradation_chain_fast() {
545 let chain = LayoutTier::Fast.degradation_chain();
546 assert_eq!(chain, vec![LayoutTier::Fast, LayoutTier::Emergency]);
547 }
548
549 #[test]
550 fn tier_degradation_chain_emergency() {
551 let chain = LayoutTier::Emergency.degradation_chain();
552 assert_eq!(chain, vec![LayoutTier::Emergency]);
553 }
554
555 #[test]
556 fn tier_default_is_balanced() {
557 assert_eq!(LayoutTier::default(), LayoutTier::Balanced);
558 }
559
560 #[test]
561 fn tier_display() {
562 assert_eq!(format!("{}", LayoutTier::Emergency), "emergency");
563 assert_eq!(format!("{}", LayoutTier::Quality), "quality");
564 assert_eq!(format!("{}", LayoutTier::Balanced), "balanced");
565 assert_eq!(format!("{}", LayoutTier::Fast), "fast");
566 }
567
568 #[test]
571 fn terminal_caps_support_fast() {
572 assert!(RuntimeCapability::TERMINAL.supports_tier(LayoutTier::Fast));
573 }
574
575 #[test]
576 fn terminal_caps_support_balanced() {
577 assert!(RuntimeCapability::TERMINAL.supports_tier(LayoutTier::Balanced));
578 }
579
580 #[test]
581 fn terminal_caps_not_quality() {
582 assert!(!RuntimeCapability::TERMINAL.supports_tier(LayoutTier::Quality));
583 }
584
585 #[test]
586 fn full_caps_support_all() {
587 assert!(RuntimeCapability::FULL.supports_tier(LayoutTier::Quality));
588 }
589
590 #[test]
591 fn terminal_best_tier_is_balanced() {
592 assert_eq!(
593 RuntimeCapability::TERMINAL.best_tier(),
594 LayoutTier::Balanced
595 );
596 }
597
598 #[test]
599 fn full_best_tier_is_quality() {
600 assert_eq!(RuntimeCapability::FULL.best_tier(), LayoutTier::Quality);
601 }
602
603 #[test]
604 fn web_best_tier_is_quality() {
605 assert_eq!(RuntimeCapability::WEB.best_tier(), LayoutTier::Quality);
606 }
607
608 #[test]
609 fn default_caps_are_terminal() {
610 let caps = RuntimeCapability::default();
611 assert!(!caps.proportional_fonts);
612 assert!(!caps.subpixel_positioning);
613 assert!(!caps.ligature_support);
614 }
615
616 #[test]
617 fn capability_display() {
618 let s = format!("{}", RuntimeCapability::FULL);
619 assert!(s.contains("proportional=true"));
620 assert!(s.contains("ligature=true"));
621 }
622
623 #[test]
626 fn fast_resolves_with_terminal_caps() {
627 let result = LayoutPolicy::FAST.resolve(&RuntimeCapability::TERMINAL);
628 let resolved = result.unwrap();
629 assert_eq!(resolved.effective_tier, LayoutTier::Fast);
630 assert!(!resolved.degraded);
631 assert!(!resolved.use_optimal_breaking);
632 }
633
634 #[test]
635 fn balanced_resolves_with_terminal_caps() {
636 let result = LayoutPolicy::BALANCED.resolve(&RuntimeCapability::TERMINAL);
637 let resolved = result.unwrap();
638 assert_eq!(resolved.effective_tier, LayoutTier::Balanced);
639 assert!(!resolved.degraded);
640 assert!(resolved.use_optimal_breaking);
641 }
642
643 #[test]
644 fn quality_degrades_on_terminal() {
645 let result = LayoutPolicy::QUALITY.resolve(&RuntimeCapability::TERMINAL);
646 let resolved = result.unwrap();
647 assert!(resolved.degraded);
648 assert_eq!(resolved.effective_tier, LayoutTier::Balanced);
649 assert_eq!(resolved.requested_tier, LayoutTier::Quality);
650 }
651
652 #[test]
653 fn quality_resolves_with_full_caps() {
654 let result = LayoutPolicy::QUALITY.resolve(&RuntimeCapability::FULL);
655 let resolved = result.unwrap();
656 assert_eq!(resolved.effective_tier, LayoutTier::Quality);
657 assert!(!resolved.degraded);
658 assert!(resolved.is_justified());
659 assert!(resolved.use_hyphenation);
660 }
661
662 #[test]
663 fn degradation_disabled_returns_error() {
664 let policy = LayoutPolicy {
665 tier: LayoutTier::Quality,
666 allow_degradation: false,
667 ..LayoutPolicy::QUALITY
668 };
669 let result = policy.resolve(&RuntimeCapability::TERMINAL);
670 assert!(result.is_err());
671 if let Err(PolicyError::CapabilityInsufficient {
672 requested,
673 best_available,
674 }) = result
675 {
676 assert_eq!(requested, LayoutTier::Quality);
677 assert_eq!(best_available, LayoutTier::Balanced);
678 }
679 }
680
681 #[test]
684 fn justify_override_applied() {
685 let policy = LayoutPolicy {
686 justify_override: Some(JustifyMode::Center),
687 ..LayoutPolicy::BALANCED
688 };
689 let resolved = policy.resolve(&RuntimeCapability::TERMINAL).unwrap();
690 assert_eq!(resolved.justification.mode, JustifyMode::Center);
691 }
692
693 #[test]
694 fn vertical_override_applied() {
695 let policy = LayoutPolicy {
696 vertical_override: Some(VerticalPolicy::Typographic),
697 ..LayoutPolicy::FAST
698 };
699 let resolved = policy.resolve(&RuntimeCapability::TERMINAL).unwrap();
700 assert!(resolved.vertical.baseline_grid.is_active());
702 }
703
704 #[test]
705 fn custom_line_height() {
706 let policy = LayoutPolicy {
707 line_height_subpx: 20 * 256, ..LayoutPolicy::BALANCED
709 };
710 let resolved = policy.resolve(&RuntimeCapability::TERMINAL).unwrap();
711 assert_eq!(resolved.line_height_subpx, 20 * 256);
712 }
713
714 #[test]
715 fn default_line_height_is_16px() {
716 let policy = LayoutPolicy::BALANCED;
717 assert_eq!(policy.effective_line_height(), 16 * 256);
718 }
719
720 #[test]
723 fn no_tracking_disables_char_space() {
724 let caps = RuntimeCapability {
725 proportional_fonts: true,
726 tracking_support: false,
727 ..RuntimeCapability::FULL
728 };
729 let resolved = LayoutPolicy::QUALITY.resolve(&caps).unwrap();
730 assert!(resolved.justification.char_space.is_rigid());
731 }
732
733 #[test]
734 fn monospace_makes_spaces_rigid() {
735 let resolved = LayoutPolicy::BALANCED
736 .resolve(&RuntimeCapability::TERMINAL)
737 .unwrap();
738 assert!(resolved.justification.word_space.is_rigid());
739 assert!(resolved.justification.sentence_space.is_rigid());
740 }
741
742 #[test]
743 fn no_hyphenation_dict_disables_hyphenation() {
744 let caps = RuntimeCapability {
745 proportional_fonts: true,
746 hyphenation_available: false,
747 ..RuntimeCapability::FULL
748 };
749 let resolved = LayoutPolicy::QUALITY.resolve(&caps).unwrap();
750 assert!(!resolved.use_hyphenation);
751 }
752
753 #[test]
756 fn fast_not_justified() {
757 let resolved = LayoutPolicy::FAST
758 .resolve(&RuntimeCapability::TERMINAL)
759 .unwrap();
760 assert!(!resolved.is_justified());
761 }
762
763 #[test]
764 fn quality_is_justified() {
765 let resolved = LayoutPolicy::QUALITY
766 .resolve(&RuntimeCapability::FULL)
767 .unwrap();
768 assert!(resolved.is_justified());
769 }
770
771 #[test]
772 fn feature_summary_fast() {
773 let resolved = LayoutPolicy::FAST
774 .resolve(&RuntimeCapability::TERMINAL)
775 .unwrap();
776 let features = resolved.feature_summary();
777 assert!(features.contains(&"greedy-wrapping"));
778 assert!(!features.contains(&"justified"));
779 }
780
781 #[test]
782 fn feature_summary_quality() {
783 let resolved = LayoutPolicy::QUALITY
784 .resolve(&RuntimeCapability::FULL)
785 .unwrap();
786 let features = resolved.feature_summary();
787 assert!(features.contains(&"optimal-breaking"));
788 assert!(features.contains(&"justified"));
789 assert!(features.contains(&"hyphenation"));
790 assert!(features.contains(&"baseline-grid"));
791 assert!(features.contains(&"first-line-indent"));
792 assert!(features.contains(&"tracking"));
793 }
794
795 #[test]
796 fn resolved_display_no_degradation() {
797 let resolved = LayoutPolicy::BALANCED
798 .resolve(&RuntimeCapability::TERMINAL)
799 .unwrap();
800 let s = format!("{resolved}");
801 assert!(s.contains("balanced"));
802 assert!(!s.contains("degraded"));
803 }
804
805 #[test]
806 fn resolved_display_with_degradation() {
807 let resolved = LayoutPolicy::QUALITY
808 .resolve(&RuntimeCapability::TERMINAL)
809 .unwrap();
810 let s = format!("{resolved}");
811 assert!(s.contains("degraded"));
812 }
813
814 #[test]
817 fn error_display() {
818 let err = PolicyError::CapabilityInsufficient {
819 requested: LayoutTier::Quality,
820 best_available: LayoutTier::Fast,
821 };
822 let s = format!("{err}");
823 assert!(s.contains("quality"));
824 assert!(s.contains("fast"));
825 }
826
827 #[test]
828 fn error_is_error_trait() {
829 let err = PolicyError::CapabilityInsufficient {
830 requested: LayoutTier::Quality,
831 best_available: LayoutTier::Fast,
832 };
833 let _: &dyn std::error::Error = &err;
834 }
835
836 #[test]
839 fn default_policy_is_balanced() {
840 assert_eq!(LayoutPolicy::default(), LayoutPolicy::BALANCED);
841 }
842
843 #[test]
844 fn policy_display() {
845 let s = format!("{}", LayoutPolicy::QUALITY);
846 assert!(s.contains("quality"));
847 }
848
849 #[test]
852 fn same_inputs_same_resolution() {
853 let p1 = LayoutPolicy::QUALITY
854 .resolve(&RuntimeCapability::FULL)
855 .unwrap();
856 let p2 = LayoutPolicy::QUALITY
857 .resolve(&RuntimeCapability::FULL)
858 .unwrap();
859 assert_eq!(p1, p2);
860 }
861
862 #[test]
863 fn same_degradation_same_result() {
864 let p1 = LayoutPolicy::QUALITY
865 .resolve(&RuntimeCapability::TERMINAL)
866 .unwrap();
867 let p2 = LayoutPolicy::QUALITY
868 .resolve(&RuntimeCapability::TERMINAL)
869 .unwrap();
870 assert_eq!(p1, p2);
871 }
872
873 #[test]
876 fn emergency_resolves_with_terminal_caps() {
877 let result = LayoutPolicy::EMERGENCY.resolve(&RuntimeCapability::TERMINAL);
878 let resolved = result.unwrap();
879 assert_eq!(resolved.effective_tier, LayoutTier::Emergency);
880 assert!(!resolved.degraded);
881 assert!(!resolved.use_optimal_breaking);
882 assert!(!resolved.use_hyphenation);
883 assert!(!resolved.is_justified());
884 }
885
886 #[test]
887 fn emergency_caps_supported() {
888 assert!(RuntimeCapability::TERMINAL.supports_tier(LayoutTier::Emergency));
889 assert!(RuntimeCapability::FULL.supports_tier(LayoutTier::Emergency));
890 }
891
892 #[test]
893 fn fast_with_full_caps_stays_fast() {
894 let resolved = LayoutPolicy::FAST
895 .resolve(&RuntimeCapability::FULL)
896 .unwrap();
897 assert_eq!(resolved.effective_tier, LayoutTier::Fast);
898 assert!(!resolved.degraded);
899 }
900
901 #[test]
902 fn quality_with_justify_left_override() {
903 let policy = LayoutPolicy {
904 justify_override: Some(JustifyMode::Left),
905 ..LayoutPolicy::QUALITY
906 };
907 let resolved = policy.resolve(&RuntimeCapability::FULL).unwrap();
908 assert!(!resolved.is_justified());
909 assert_eq!(resolved.effective_tier, LayoutTier::Quality);
911 }
912}