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 Fast = 0,
44 #[default]
47 Balanced = 1,
48 Quality = 2,
51}
52
53impl LayoutTier {
54 #[must_use]
56 pub const fn degrade(&self) -> Option<Self> {
57 match self {
58 Self::Quality => Some(Self::Balanced),
59 Self::Balanced => Some(Self::Fast),
60 Self::Fast => None,
61 }
62 }
63
64 #[must_use]
66 pub fn degradation_chain(&self) -> Vec<Self> {
67 let mut chain = vec![*self];
68 let mut current = *self;
69 while let Some(next) = current.degrade() {
70 chain.push(next);
71 current = next;
72 }
73 chain
74 }
75}
76
77impl fmt::Display for LayoutTier {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 match self {
80 Self::Fast => write!(f, "fast"),
81 Self::Balanced => write!(f, "balanced"),
82 Self::Quality => write!(f, "quality"),
83 }
84 }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
96pub struct RuntimeCapability {
97 pub proportional_fonts: bool,
100
101 pub subpixel_positioning: bool,
104
105 pub hyphenation_available: bool,
108
109 pub tracking_support: bool,
111
112 pub max_paragraph_words: usize,
115}
116
117impl RuntimeCapability {
118 pub const FULL: Self = Self {
120 proportional_fonts: true,
121 subpixel_positioning: true,
122 hyphenation_available: true,
123 tracking_support: true,
124 max_paragraph_words: 0,
125 };
126
127 pub const TERMINAL: Self = Self {
129 proportional_fonts: false,
130 subpixel_positioning: false,
131 hyphenation_available: false,
132 tracking_support: false,
133 max_paragraph_words: 0,
134 };
135
136 pub const WEB: Self = Self {
138 proportional_fonts: true,
139 subpixel_positioning: true,
140 hyphenation_available: true,
141 tracking_support: false,
142 max_paragraph_words: 0,
143 };
144
145 #[must_use]
147 pub fn supports_tier(&self, tier: LayoutTier) -> bool {
148 match tier {
149 LayoutTier::Fast => true, LayoutTier::Balanced => true, LayoutTier::Quality => {
152 self.proportional_fonts
154 }
155 }
156 }
157
158 #[must_use]
160 pub fn best_tier(&self) -> LayoutTier {
161 if self.supports_tier(LayoutTier::Quality) {
162 LayoutTier::Quality
163 } else if self.supports_tier(LayoutTier::Balanced) {
164 LayoutTier::Balanced
165 } else {
166 LayoutTier::Fast
167 }
168 }
169}
170
171impl fmt::Display for RuntimeCapability {
172 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173 write!(
174 f,
175 "proportional={} subpixel={} hyphen={} tracking={}",
176 self.proportional_fonts,
177 self.subpixel_positioning,
178 self.hyphenation_available,
179 self.tracking_support
180 )
181 }
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
193pub struct LayoutPolicy {
194 pub tier: LayoutTier,
196 pub allow_degradation: bool,
199 pub justify_override: Option<JustifyMode>,
201 pub vertical_override: Option<VerticalPolicy>,
203 pub line_height_subpx: u32,
206}
207
208const DEFAULT_LINE_HEIGHT_SUBPX: u32 = 16 * 256;
210
211impl LayoutPolicy {
212 pub const FAST: Self = Self {
214 tier: LayoutTier::Fast,
215 allow_degradation: true,
216 justify_override: None,
217 vertical_override: None,
218 line_height_subpx: 0,
219 };
220
221 pub const BALANCED: Self = Self {
223 tier: LayoutTier::Balanced,
224 allow_degradation: true,
225 justify_override: None,
226 vertical_override: None,
227 line_height_subpx: 0,
228 };
229
230 pub const QUALITY: Self = Self {
232 tier: LayoutTier::Quality,
233 allow_degradation: true,
234 justify_override: None,
235 vertical_override: None,
236 line_height_subpx: 0,
237 };
238
239 #[must_use]
241 pub const fn effective_line_height(&self) -> u32 {
242 if self.line_height_subpx == 0 {
243 DEFAULT_LINE_HEIGHT_SUBPX
244 } else {
245 self.line_height_subpx
246 }
247 }
248
249 pub fn resolve(&self, caps: &RuntimeCapability) -> Result<ResolvedPolicy, PolicyError> {
259 let mut effective_tier = self.tier;
260
261 if !caps.supports_tier(effective_tier) {
263 if self.allow_degradation {
264 effective_tier = caps.best_tier();
265 } else {
266 return Err(PolicyError::CapabilityInsufficient {
267 requested: self.tier,
268 best_available: caps.best_tier(),
269 });
270 }
271 }
272
273 let line_h = self.effective_line_height();
274
275 let objective = match effective_tier {
277 LayoutTier::Fast => ParagraphObjective::terminal(),
278 LayoutTier::Balanced => ParagraphObjective::default(),
279 LayoutTier::Quality => ParagraphObjective::typographic(),
280 };
281
282 let vertical_policy = self.vertical_override.unwrap_or(match effective_tier {
283 LayoutTier::Fast => VerticalPolicy::Compact,
284 LayoutTier::Balanced => VerticalPolicy::Readable,
285 LayoutTier::Quality => VerticalPolicy::Typographic,
286 });
287
288 let vertical = vertical_policy.resolve(line_h);
289
290 let mut justification = match effective_tier {
291 LayoutTier::Fast => JustificationControl::TERMINAL,
292 LayoutTier::Balanced => JustificationControl::READABLE,
293 LayoutTier::Quality => JustificationControl::TYPOGRAPHIC,
294 };
295
296 if let Some(mode) = self.justify_override {
298 justification.mode = mode;
299 }
300
301 if !caps.tracking_support {
303 justification.char_space = crate::justification::GlueSpec::rigid(0);
305 }
306
307 if !caps.proportional_fonts {
308 justification.word_space =
310 crate::justification::GlueSpec::rigid(crate::justification::SUBCELL_SCALE);
311 justification.sentence_space =
312 crate::justification::GlueSpec::rigid(crate::justification::SUBCELL_SCALE);
313 justification.char_space = crate::justification::GlueSpec::rigid(0);
314 }
315
316 let degraded = effective_tier != self.tier;
317
318 Ok(ResolvedPolicy {
319 requested_tier: self.tier,
320 effective_tier,
321 degraded,
322 objective,
323 vertical,
324 justification,
325 use_hyphenation: caps.hyphenation_available && effective_tier >= LayoutTier::Balanced,
326 use_optimal_breaking: effective_tier >= LayoutTier::Balanced,
327 line_height_subpx: line_h,
328 })
329 }
330}
331
332impl Default for LayoutPolicy {
333 fn default() -> Self {
334 Self::BALANCED
335 }
336}
337
338impl fmt::Display for LayoutPolicy {
339 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340 write!(f, "tier={} degrade={}", self.tier, self.allow_degradation)
341 }
342}
343
344#[derive(Debug, Clone, PartialEq)]
353pub struct ResolvedPolicy {
354 pub requested_tier: LayoutTier,
356 pub effective_tier: LayoutTier,
358 pub degraded: bool,
360
361 pub objective: ParagraphObjective,
363 pub vertical: VerticalMetrics,
365 pub justification: JustificationControl,
367
368 pub use_hyphenation: bool,
370 pub use_optimal_breaking: bool,
373 pub line_height_subpx: u32,
375}
376
377impl ResolvedPolicy {
378 #[must_use]
380 pub fn is_justified(&self) -> bool {
381 self.justification.mode.requires_justification()
382 }
383
384 #[must_use]
386 pub fn feature_summary(&self) -> Vec<&'static str> {
387 let mut features = Vec::new();
388
389 if self.use_optimal_breaking {
390 features.push("optimal-breaking");
391 } else {
392 features.push("greedy-wrapping");
393 }
394
395 if self.is_justified() {
396 features.push("justified");
397 }
398
399 if self.use_hyphenation {
400 features.push("hyphenation");
401 }
402
403 if self.vertical.baseline_grid.is_active() {
404 features.push("baseline-grid");
405 }
406
407 if self.vertical.first_line_indent_subpx > 0 {
408 features.push("first-line-indent");
409 }
410
411 if !self.justification.char_space.is_rigid() {
412 features.push("tracking");
413 }
414
415 features
416 }
417}
418
419impl fmt::Display for ResolvedPolicy {
420 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
421 write!(
422 f,
423 "{} (requested {}{})",
424 self.effective_tier,
425 self.requested_tier,
426 if self.degraded { ", degraded" } else { "" }
427 )
428 }
429}
430
431#[derive(Debug, Clone, PartialEq, Eq)]
437pub enum PolicyError {
438 CapabilityInsufficient {
440 requested: LayoutTier,
442 best_available: LayoutTier,
444 },
445}
446
447impl fmt::Display for PolicyError {
448 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
449 match self {
450 Self::CapabilityInsufficient {
451 requested,
452 best_available,
453 } => write!(
454 f,
455 "requested tier '{}' not supported; best available is '{}'",
456 requested, best_available
457 ),
458 }
459 }
460}
461
462impl std::error::Error for PolicyError {}
463
464#[cfg(test)]
469mod tests {
470 use super::*;
471 use crate::justification::JustifyMode;
472 use crate::vertical_metrics::VerticalPolicy;
473
474 #[test]
477 fn tier_ordering() {
478 assert!(LayoutTier::Fast < LayoutTier::Balanced);
479 assert!(LayoutTier::Balanced < LayoutTier::Quality);
480 }
481
482 #[test]
483 fn tier_degrade_quality() {
484 assert_eq!(LayoutTier::Quality.degrade(), Some(LayoutTier::Balanced));
485 }
486
487 #[test]
488 fn tier_degrade_balanced() {
489 assert_eq!(LayoutTier::Balanced.degrade(), Some(LayoutTier::Fast));
490 }
491
492 #[test]
493 fn tier_degrade_fast_is_none() {
494 assert_eq!(LayoutTier::Fast.degrade(), None);
495 }
496
497 #[test]
498 fn tier_degradation_chain_quality() {
499 let chain = LayoutTier::Quality.degradation_chain();
500 assert_eq!(
501 chain,
502 vec![LayoutTier::Quality, LayoutTier::Balanced, LayoutTier::Fast]
503 );
504 }
505
506 #[test]
507 fn tier_degradation_chain_fast() {
508 let chain = LayoutTier::Fast.degradation_chain();
509 assert_eq!(chain, vec![LayoutTier::Fast]);
510 }
511
512 #[test]
513 fn tier_default_is_balanced() {
514 assert_eq!(LayoutTier::default(), LayoutTier::Balanced);
515 }
516
517 #[test]
518 fn tier_display() {
519 assert_eq!(format!("{}", LayoutTier::Quality), "quality");
520 assert_eq!(format!("{}", LayoutTier::Balanced), "balanced");
521 assert_eq!(format!("{}", LayoutTier::Fast), "fast");
522 }
523
524 #[test]
527 fn terminal_caps_support_fast() {
528 assert!(RuntimeCapability::TERMINAL.supports_tier(LayoutTier::Fast));
529 }
530
531 #[test]
532 fn terminal_caps_support_balanced() {
533 assert!(RuntimeCapability::TERMINAL.supports_tier(LayoutTier::Balanced));
534 }
535
536 #[test]
537 fn terminal_caps_not_quality() {
538 assert!(!RuntimeCapability::TERMINAL.supports_tier(LayoutTier::Quality));
539 }
540
541 #[test]
542 fn full_caps_support_all() {
543 assert!(RuntimeCapability::FULL.supports_tier(LayoutTier::Quality));
544 }
545
546 #[test]
547 fn terminal_best_tier_is_balanced() {
548 assert_eq!(
549 RuntimeCapability::TERMINAL.best_tier(),
550 LayoutTier::Balanced
551 );
552 }
553
554 #[test]
555 fn full_best_tier_is_quality() {
556 assert_eq!(RuntimeCapability::FULL.best_tier(), LayoutTier::Quality);
557 }
558
559 #[test]
560 fn web_best_tier_is_quality() {
561 assert_eq!(RuntimeCapability::WEB.best_tier(), LayoutTier::Quality);
562 }
563
564 #[test]
565 fn default_caps_are_terminal() {
566 let caps = RuntimeCapability::default();
567 assert!(!caps.proportional_fonts);
568 assert!(!caps.subpixel_positioning);
569 }
570
571 #[test]
572 fn capability_display() {
573 let s = format!("{}", RuntimeCapability::FULL);
574 assert!(s.contains("proportional=true"));
575 }
576
577 #[test]
580 fn fast_resolves_with_terminal_caps() {
581 let result = LayoutPolicy::FAST.resolve(&RuntimeCapability::TERMINAL);
582 let resolved = result.unwrap();
583 assert_eq!(resolved.effective_tier, LayoutTier::Fast);
584 assert!(!resolved.degraded);
585 assert!(!resolved.use_optimal_breaking);
586 }
587
588 #[test]
589 fn balanced_resolves_with_terminal_caps() {
590 let result = LayoutPolicy::BALANCED.resolve(&RuntimeCapability::TERMINAL);
591 let resolved = result.unwrap();
592 assert_eq!(resolved.effective_tier, LayoutTier::Balanced);
593 assert!(!resolved.degraded);
594 assert!(resolved.use_optimal_breaking);
595 }
596
597 #[test]
598 fn quality_degrades_on_terminal() {
599 let result = LayoutPolicy::QUALITY.resolve(&RuntimeCapability::TERMINAL);
600 let resolved = result.unwrap();
601 assert!(resolved.degraded);
602 assert_eq!(resolved.effective_tier, LayoutTier::Balanced);
603 assert_eq!(resolved.requested_tier, LayoutTier::Quality);
604 }
605
606 #[test]
607 fn quality_resolves_with_full_caps() {
608 let result = LayoutPolicy::QUALITY.resolve(&RuntimeCapability::FULL);
609 let resolved = result.unwrap();
610 assert_eq!(resolved.effective_tier, LayoutTier::Quality);
611 assert!(!resolved.degraded);
612 assert!(resolved.is_justified());
613 assert!(resolved.use_hyphenation);
614 }
615
616 #[test]
617 fn degradation_disabled_returns_error() {
618 let policy = LayoutPolicy {
619 tier: LayoutTier::Quality,
620 allow_degradation: false,
621 ..LayoutPolicy::QUALITY
622 };
623 let result = policy.resolve(&RuntimeCapability::TERMINAL);
624 assert!(result.is_err());
625 if let Err(PolicyError::CapabilityInsufficient {
626 requested,
627 best_available,
628 }) = result
629 {
630 assert_eq!(requested, LayoutTier::Quality);
631 assert_eq!(best_available, LayoutTier::Balanced);
632 }
633 }
634
635 #[test]
638 fn justify_override_applied() {
639 let policy = LayoutPolicy {
640 justify_override: Some(JustifyMode::Center),
641 ..LayoutPolicy::BALANCED
642 };
643 let resolved = policy.resolve(&RuntimeCapability::TERMINAL).unwrap();
644 assert_eq!(resolved.justification.mode, JustifyMode::Center);
645 }
646
647 #[test]
648 fn vertical_override_applied() {
649 let policy = LayoutPolicy {
650 vertical_override: Some(VerticalPolicy::Typographic),
651 ..LayoutPolicy::FAST
652 };
653 let resolved = policy.resolve(&RuntimeCapability::TERMINAL).unwrap();
654 assert!(resolved.vertical.baseline_grid.is_active());
656 }
657
658 #[test]
659 fn custom_line_height() {
660 let policy = LayoutPolicy {
661 line_height_subpx: 20 * 256, ..LayoutPolicy::BALANCED
663 };
664 let resolved = policy.resolve(&RuntimeCapability::TERMINAL).unwrap();
665 assert_eq!(resolved.line_height_subpx, 20 * 256);
666 }
667
668 #[test]
669 fn default_line_height_is_16px() {
670 let policy = LayoutPolicy::BALANCED;
671 assert_eq!(policy.effective_line_height(), 16 * 256);
672 }
673
674 #[test]
677 fn no_tracking_disables_char_space() {
678 let caps = RuntimeCapability {
679 proportional_fonts: true,
680 tracking_support: false,
681 ..RuntimeCapability::FULL
682 };
683 let resolved = LayoutPolicy::QUALITY.resolve(&caps).unwrap();
684 assert!(resolved.justification.char_space.is_rigid());
685 }
686
687 #[test]
688 fn monospace_makes_spaces_rigid() {
689 let resolved = LayoutPolicy::BALANCED
690 .resolve(&RuntimeCapability::TERMINAL)
691 .unwrap();
692 assert!(resolved.justification.word_space.is_rigid());
693 assert!(resolved.justification.sentence_space.is_rigid());
694 }
695
696 #[test]
697 fn no_hyphenation_dict_disables_hyphenation() {
698 let caps = RuntimeCapability {
699 proportional_fonts: true,
700 hyphenation_available: false,
701 ..RuntimeCapability::FULL
702 };
703 let resolved = LayoutPolicy::QUALITY.resolve(&caps).unwrap();
704 assert!(!resolved.use_hyphenation);
705 }
706
707 #[test]
710 fn fast_not_justified() {
711 let resolved = LayoutPolicy::FAST
712 .resolve(&RuntimeCapability::TERMINAL)
713 .unwrap();
714 assert!(!resolved.is_justified());
715 }
716
717 #[test]
718 fn quality_is_justified() {
719 let resolved = LayoutPolicy::QUALITY
720 .resolve(&RuntimeCapability::FULL)
721 .unwrap();
722 assert!(resolved.is_justified());
723 }
724
725 #[test]
726 fn feature_summary_fast() {
727 let resolved = LayoutPolicy::FAST
728 .resolve(&RuntimeCapability::TERMINAL)
729 .unwrap();
730 let features = resolved.feature_summary();
731 assert!(features.contains(&"greedy-wrapping"));
732 assert!(!features.contains(&"justified"));
733 }
734
735 #[test]
736 fn feature_summary_quality() {
737 let resolved = LayoutPolicy::QUALITY
738 .resolve(&RuntimeCapability::FULL)
739 .unwrap();
740 let features = resolved.feature_summary();
741 assert!(features.contains(&"optimal-breaking"));
742 assert!(features.contains(&"justified"));
743 assert!(features.contains(&"hyphenation"));
744 assert!(features.contains(&"baseline-grid"));
745 assert!(features.contains(&"first-line-indent"));
746 assert!(features.contains(&"tracking"));
747 }
748
749 #[test]
750 fn resolved_display_no_degradation() {
751 let resolved = LayoutPolicy::BALANCED
752 .resolve(&RuntimeCapability::TERMINAL)
753 .unwrap();
754 let s = format!("{resolved}");
755 assert!(s.contains("balanced"));
756 assert!(!s.contains("degraded"));
757 }
758
759 #[test]
760 fn resolved_display_with_degradation() {
761 let resolved = LayoutPolicy::QUALITY
762 .resolve(&RuntimeCapability::TERMINAL)
763 .unwrap();
764 let s = format!("{resolved}");
765 assert!(s.contains("degraded"));
766 }
767
768 #[test]
771 fn error_display() {
772 let err = PolicyError::CapabilityInsufficient {
773 requested: LayoutTier::Quality,
774 best_available: LayoutTier::Fast,
775 };
776 let s = format!("{err}");
777 assert!(s.contains("quality"));
778 assert!(s.contains("fast"));
779 }
780
781 #[test]
782 fn error_is_error_trait() {
783 let err = PolicyError::CapabilityInsufficient {
784 requested: LayoutTier::Quality,
785 best_available: LayoutTier::Fast,
786 };
787 let _: &dyn std::error::Error = &err;
788 }
789
790 #[test]
793 fn default_policy_is_balanced() {
794 assert_eq!(LayoutPolicy::default(), LayoutPolicy::BALANCED);
795 }
796
797 #[test]
798 fn policy_display() {
799 let s = format!("{}", LayoutPolicy::QUALITY);
800 assert!(s.contains("quality"));
801 }
802
803 #[test]
806 fn same_inputs_same_resolution() {
807 let p1 = LayoutPolicy::QUALITY
808 .resolve(&RuntimeCapability::FULL)
809 .unwrap();
810 let p2 = LayoutPolicy::QUALITY
811 .resolve(&RuntimeCapability::FULL)
812 .unwrap();
813 assert_eq!(p1, p2);
814 }
815
816 #[test]
817 fn same_degradation_same_result() {
818 let p1 = LayoutPolicy::QUALITY
819 .resolve(&RuntimeCapability::TERMINAL)
820 .unwrap();
821 let p2 = LayoutPolicy::QUALITY
822 .resolve(&RuntimeCapability::TERMINAL)
823 .unwrap();
824 assert_eq!(p1, p2);
825 }
826
827 #[test]
830 fn fast_with_full_caps_stays_fast() {
831 let resolved = LayoutPolicy::FAST
832 .resolve(&RuntimeCapability::FULL)
833 .unwrap();
834 assert_eq!(resolved.effective_tier, LayoutTier::Fast);
835 assert!(!resolved.degraded);
836 }
837
838 #[test]
839 fn quality_with_justify_left_override() {
840 let policy = LayoutPolicy {
841 justify_override: Some(JustifyMode::Left),
842 ..LayoutPolicy::QUALITY
843 };
844 let resolved = policy.resolve(&RuntimeCapability::FULL).unwrap();
845 assert!(!resolved.is_justified());
846 assert_eq!(resolved.effective_tier, LayoutTier::Quality);
848 }
849}