use std::fmt;
use crate::justification::{JustificationControl, JustifyMode};
use crate::vertical_metrics::{VerticalMetrics, VerticalPolicy};
use crate::wrap::ParagraphObjective;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
pub enum LayoutTier {
Emergency = 0,
Fast = 1,
#[default]
Balanced = 2,
Quality = 3,
}
impl LayoutTier {
#[must_use]
pub const fn degrade(&self) -> Option<Self> {
match self {
Self::Quality => Some(Self::Balanced),
Self::Balanced => Some(Self::Fast),
Self::Fast => Some(Self::Emergency),
Self::Emergency => None,
}
}
#[must_use]
pub fn degradation_chain(&self) -> Vec<Self> {
let mut chain = vec![*self];
let mut current = *self;
while let Some(next) = current.degrade() {
chain.push(next);
current = next;
}
chain
}
}
impl fmt::Display for LayoutTier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Emergency => write!(f, "emergency"),
Self::Fast => write!(f, "fast"),
Self::Balanced => write!(f, "balanced"),
Self::Quality => write!(f, "quality"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct RuntimeCapability {
pub proportional_fonts: bool,
pub subpixel_positioning: bool,
pub hyphenation_available: bool,
pub tracking_support: bool,
pub ligature_support: bool,
pub max_paragraph_words: usize,
}
impl RuntimeCapability {
pub const FULL: Self = Self {
proportional_fonts: true,
subpixel_positioning: true,
hyphenation_available: true,
tracking_support: true,
ligature_support: true,
max_paragraph_words: 0,
};
pub const TERMINAL: Self = Self {
proportional_fonts: false,
subpixel_positioning: false,
hyphenation_available: false,
tracking_support: false,
ligature_support: false,
max_paragraph_words: 0,
};
pub const WEB: Self = Self {
proportional_fonts: true,
subpixel_positioning: true,
hyphenation_available: true,
tracking_support: false,
ligature_support: true,
max_paragraph_words: 0,
};
#[must_use]
pub fn supports_tier(&self, tier: LayoutTier) -> bool {
match tier {
LayoutTier::Emergency => true, LayoutTier::Fast => true, LayoutTier::Balanced => true, LayoutTier::Quality => {
self.proportional_fonts
}
}
}
#[must_use]
pub fn best_tier(&self) -> LayoutTier {
if self.supports_tier(LayoutTier::Quality) {
LayoutTier::Quality
} else if self.supports_tier(LayoutTier::Balanced) {
LayoutTier::Balanced
} else {
LayoutTier::Fast
}
}
}
impl fmt::Display for RuntimeCapability {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"proportional={} subpixel={} hyphen={} tracking={} ligature={}",
self.proportional_fonts,
self.subpixel_positioning,
self.hyphenation_available,
self.tracking_support,
self.ligature_support
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LayoutPolicy {
pub tier: LayoutTier,
pub allow_degradation: bool,
pub justify_override: Option<JustifyMode>,
pub vertical_override: Option<VerticalPolicy>,
pub line_height_subpx: u32,
}
const DEFAULT_LINE_HEIGHT_SUBPX: u32 = 16 * 256;
impl LayoutPolicy {
pub const EMERGENCY: Self = Self {
tier: LayoutTier::Emergency,
allow_degradation: false, justify_override: None,
vertical_override: None,
line_height_subpx: 0,
};
pub const FAST: Self = Self {
tier: LayoutTier::Fast,
allow_degradation: true,
justify_override: None,
vertical_override: None,
line_height_subpx: 0,
};
pub const BALANCED: Self = Self {
tier: LayoutTier::Balanced,
allow_degradation: true,
justify_override: None,
vertical_override: None,
line_height_subpx: 0,
};
pub const QUALITY: Self = Self {
tier: LayoutTier::Quality,
allow_degradation: true,
justify_override: None,
vertical_override: None,
line_height_subpx: 0,
};
#[must_use]
pub const fn effective_line_height(&self) -> u32 {
if self.line_height_subpx == 0 {
DEFAULT_LINE_HEIGHT_SUBPX
} else {
self.line_height_subpx
}
}
pub fn resolve(&self, caps: &RuntimeCapability) -> Result<ResolvedPolicy, PolicyError> {
let mut effective_tier = self.tier;
if !caps.supports_tier(effective_tier) {
if self.allow_degradation {
effective_tier = caps.best_tier();
} else {
return Err(PolicyError::CapabilityInsufficient {
requested: self.tier,
best_available: caps.best_tier(),
});
}
}
let line_h = self.effective_line_height();
let objective = match effective_tier {
LayoutTier::Emergency | LayoutTier::Fast => ParagraphObjective::terminal(),
LayoutTier::Balanced => ParagraphObjective::default(),
LayoutTier::Quality => ParagraphObjective::typographic(),
};
let vertical_policy = self.vertical_override.unwrap_or(match effective_tier {
LayoutTier::Emergency | LayoutTier::Fast => VerticalPolicy::Compact,
LayoutTier::Balanced => VerticalPolicy::Readable,
LayoutTier::Quality => VerticalPolicy::Typographic,
});
let vertical = vertical_policy.resolve(line_h);
let mut justification = match effective_tier {
LayoutTier::Emergency | LayoutTier::Fast => JustificationControl::TERMINAL,
LayoutTier::Balanced => JustificationControl::READABLE,
LayoutTier::Quality => JustificationControl::TYPOGRAPHIC,
};
if let Some(mode) = self.justify_override {
justification.mode = mode;
}
if !caps.tracking_support {
justification.char_space = crate::justification::GlueSpec::rigid(0);
}
if !caps.proportional_fonts {
justification.word_space =
crate::justification::GlueSpec::rigid(crate::justification::SUBCELL_SCALE);
justification.sentence_space =
crate::justification::GlueSpec::rigid(crate::justification::SUBCELL_SCALE);
justification.char_space = crate::justification::GlueSpec::rigid(0);
}
let degraded = effective_tier != self.tier;
Ok(ResolvedPolicy {
requested_tier: self.tier,
effective_tier,
degraded,
objective,
vertical,
justification,
use_hyphenation: caps.hyphenation_available && effective_tier >= LayoutTier::Balanced,
use_optimal_breaking: effective_tier >= LayoutTier::Balanced,
line_height_subpx: line_h,
})
}
}
impl Default for LayoutPolicy {
fn default() -> Self {
Self::BALANCED
}
}
impl fmt::Display for LayoutPolicy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "tier={} degrade={}", self.tier, self.allow_degradation)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ResolvedPolicy {
pub requested_tier: LayoutTier,
pub effective_tier: LayoutTier,
pub degraded: bool,
pub objective: ParagraphObjective,
pub vertical: VerticalMetrics,
pub justification: JustificationControl,
pub use_hyphenation: bool,
pub use_optimal_breaking: bool,
pub line_height_subpx: u32,
}
impl ResolvedPolicy {
#[must_use]
pub fn is_justified(&self) -> bool {
self.justification.mode.requires_justification()
}
#[must_use]
pub fn feature_summary(&self) -> Vec<&'static str> {
let mut features = Vec::new();
if self.use_optimal_breaking {
features.push("optimal-breaking");
} else {
features.push("greedy-wrapping");
}
if self.is_justified() {
features.push("justified");
}
if self.use_hyphenation {
features.push("hyphenation");
}
if self.vertical.baseline_grid.is_active() {
features.push("baseline-grid");
}
if self.vertical.first_line_indent_subpx > 0 {
features.push("first-line-indent");
}
if !self.justification.char_space.is_rigid() {
features.push("tracking");
}
features
}
}
impl fmt::Display for ResolvedPolicy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} (requested {}{})",
self.effective_tier,
self.requested_tier,
if self.degraded { ", degraded" } else { "" }
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PolicyError {
CapabilityInsufficient {
requested: LayoutTier,
best_available: LayoutTier,
},
}
impl fmt::Display for PolicyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::CapabilityInsufficient {
requested,
best_available,
} => write!(
f,
"requested tier '{}' not supported; best available is '{}'",
requested, best_available
),
}
}
}
impl std::error::Error for PolicyError {}
#[cfg(test)]
mod tests {
use super::*;
use crate::justification::JustifyMode;
use crate::vertical_metrics::VerticalPolicy;
#[test]
fn tier_ordering() {
assert!(LayoutTier::Emergency < LayoutTier::Fast);
assert!(LayoutTier::Fast < LayoutTier::Balanced);
assert!(LayoutTier::Balanced < LayoutTier::Quality);
}
#[test]
fn tier_degrade_quality() {
assert_eq!(LayoutTier::Quality.degrade(), Some(LayoutTier::Balanced));
}
#[test]
fn tier_degrade_balanced() {
assert_eq!(LayoutTier::Balanced.degrade(), Some(LayoutTier::Fast));
}
#[test]
fn tier_degrade_fast_is_emergency() {
assert_eq!(LayoutTier::Fast.degrade(), Some(LayoutTier::Emergency));
}
#[test]
fn tier_degrade_emergency_is_none() {
assert_eq!(LayoutTier::Emergency.degrade(), None);
}
#[test]
fn tier_degradation_chain_quality() {
let chain = LayoutTier::Quality.degradation_chain();
assert_eq!(
chain,
vec![
LayoutTier::Quality,
LayoutTier::Balanced,
LayoutTier::Fast,
LayoutTier::Emergency,
]
);
}
#[test]
fn tier_degradation_chain_fast() {
let chain = LayoutTier::Fast.degradation_chain();
assert_eq!(chain, vec![LayoutTier::Fast, LayoutTier::Emergency]);
}
#[test]
fn tier_degradation_chain_emergency() {
let chain = LayoutTier::Emergency.degradation_chain();
assert_eq!(chain, vec![LayoutTier::Emergency]);
}
#[test]
fn tier_default_is_balanced() {
assert_eq!(LayoutTier::default(), LayoutTier::Balanced);
}
#[test]
fn tier_display() {
assert_eq!(format!("{}", LayoutTier::Emergency), "emergency");
assert_eq!(format!("{}", LayoutTier::Quality), "quality");
assert_eq!(format!("{}", LayoutTier::Balanced), "balanced");
assert_eq!(format!("{}", LayoutTier::Fast), "fast");
}
#[test]
fn terminal_caps_support_fast() {
assert!(RuntimeCapability::TERMINAL.supports_tier(LayoutTier::Fast));
}
#[test]
fn terminal_caps_support_balanced() {
assert!(RuntimeCapability::TERMINAL.supports_tier(LayoutTier::Balanced));
}
#[test]
fn terminal_caps_not_quality() {
assert!(!RuntimeCapability::TERMINAL.supports_tier(LayoutTier::Quality));
}
#[test]
fn full_caps_support_all() {
assert!(RuntimeCapability::FULL.supports_tier(LayoutTier::Quality));
}
#[test]
fn terminal_best_tier_is_balanced() {
assert_eq!(
RuntimeCapability::TERMINAL.best_tier(),
LayoutTier::Balanced
);
}
#[test]
fn full_best_tier_is_quality() {
assert_eq!(RuntimeCapability::FULL.best_tier(), LayoutTier::Quality);
}
#[test]
fn web_best_tier_is_quality() {
assert_eq!(RuntimeCapability::WEB.best_tier(), LayoutTier::Quality);
}
#[test]
fn default_caps_are_terminal() {
let caps = RuntimeCapability::default();
assert!(!caps.proportional_fonts);
assert!(!caps.subpixel_positioning);
assert!(!caps.ligature_support);
}
#[test]
fn capability_display() {
let s = format!("{}", RuntimeCapability::FULL);
assert!(s.contains("proportional=true"));
assert!(s.contains("ligature=true"));
}
#[test]
fn fast_resolves_with_terminal_caps() {
let result = LayoutPolicy::FAST.resolve(&RuntimeCapability::TERMINAL);
let resolved = result.unwrap();
assert_eq!(resolved.effective_tier, LayoutTier::Fast);
assert!(!resolved.degraded);
assert!(!resolved.use_optimal_breaking);
}
#[test]
fn balanced_resolves_with_terminal_caps() {
let result = LayoutPolicy::BALANCED.resolve(&RuntimeCapability::TERMINAL);
let resolved = result.unwrap();
assert_eq!(resolved.effective_tier, LayoutTier::Balanced);
assert!(!resolved.degraded);
assert!(resolved.use_optimal_breaking);
}
#[test]
fn quality_degrades_on_terminal() {
let result = LayoutPolicy::QUALITY.resolve(&RuntimeCapability::TERMINAL);
let resolved = result.unwrap();
assert!(resolved.degraded);
assert_eq!(resolved.effective_tier, LayoutTier::Balanced);
assert_eq!(resolved.requested_tier, LayoutTier::Quality);
}
#[test]
fn quality_resolves_with_full_caps() {
let result = LayoutPolicy::QUALITY.resolve(&RuntimeCapability::FULL);
let resolved = result.unwrap();
assert_eq!(resolved.effective_tier, LayoutTier::Quality);
assert!(!resolved.degraded);
assert!(resolved.is_justified());
assert!(resolved.use_hyphenation);
}
#[test]
fn degradation_disabled_returns_error() {
let policy = LayoutPolicy {
tier: LayoutTier::Quality,
allow_degradation: false,
..LayoutPolicy::QUALITY
};
let result = policy.resolve(&RuntimeCapability::TERMINAL);
assert!(result.is_err());
if let Err(PolicyError::CapabilityInsufficient {
requested,
best_available,
}) = result
{
assert_eq!(requested, LayoutTier::Quality);
assert_eq!(best_available, LayoutTier::Balanced);
}
}
#[test]
fn justify_override_applied() {
let policy = LayoutPolicy {
justify_override: Some(JustifyMode::Center),
..LayoutPolicy::BALANCED
};
let resolved = policy.resolve(&RuntimeCapability::TERMINAL).unwrap();
assert_eq!(resolved.justification.mode, JustifyMode::Center);
}
#[test]
fn vertical_override_applied() {
let policy = LayoutPolicy {
vertical_override: Some(VerticalPolicy::Typographic),
..LayoutPolicy::FAST
};
let resolved = policy.resolve(&RuntimeCapability::TERMINAL).unwrap();
assert!(resolved.vertical.baseline_grid.is_active());
}
#[test]
fn custom_line_height() {
let policy = LayoutPolicy {
line_height_subpx: 20 * 256, ..LayoutPolicy::BALANCED
};
let resolved = policy.resolve(&RuntimeCapability::TERMINAL).unwrap();
assert_eq!(resolved.line_height_subpx, 20 * 256);
}
#[test]
fn default_line_height_is_16px() {
let policy = LayoutPolicy::BALANCED;
assert_eq!(policy.effective_line_height(), 16 * 256);
}
#[test]
fn no_tracking_disables_char_space() {
let caps = RuntimeCapability {
proportional_fonts: true,
tracking_support: false,
..RuntimeCapability::FULL
};
let resolved = LayoutPolicy::QUALITY.resolve(&caps).unwrap();
assert!(resolved.justification.char_space.is_rigid());
}
#[test]
fn monospace_makes_spaces_rigid() {
let resolved = LayoutPolicy::BALANCED
.resolve(&RuntimeCapability::TERMINAL)
.unwrap();
assert!(resolved.justification.word_space.is_rigid());
assert!(resolved.justification.sentence_space.is_rigid());
}
#[test]
fn no_hyphenation_dict_disables_hyphenation() {
let caps = RuntimeCapability {
proportional_fonts: true,
hyphenation_available: false,
..RuntimeCapability::FULL
};
let resolved = LayoutPolicy::QUALITY.resolve(&caps).unwrap();
assert!(!resolved.use_hyphenation);
}
#[test]
fn fast_not_justified() {
let resolved = LayoutPolicy::FAST
.resolve(&RuntimeCapability::TERMINAL)
.unwrap();
assert!(!resolved.is_justified());
}
#[test]
fn quality_is_justified() {
let resolved = LayoutPolicy::QUALITY
.resolve(&RuntimeCapability::FULL)
.unwrap();
assert!(resolved.is_justified());
}
#[test]
fn feature_summary_fast() {
let resolved = LayoutPolicy::FAST
.resolve(&RuntimeCapability::TERMINAL)
.unwrap();
let features = resolved.feature_summary();
assert!(features.contains(&"greedy-wrapping"));
assert!(!features.contains(&"justified"));
}
#[test]
fn feature_summary_quality() {
let resolved = LayoutPolicy::QUALITY
.resolve(&RuntimeCapability::FULL)
.unwrap();
let features = resolved.feature_summary();
assert!(features.contains(&"optimal-breaking"));
assert!(features.contains(&"justified"));
assert!(features.contains(&"hyphenation"));
assert!(features.contains(&"baseline-grid"));
assert!(features.contains(&"first-line-indent"));
assert!(features.contains(&"tracking"));
}
#[test]
fn resolved_display_no_degradation() {
let resolved = LayoutPolicy::BALANCED
.resolve(&RuntimeCapability::TERMINAL)
.unwrap();
let s = format!("{resolved}");
assert!(s.contains("balanced"));
assert!(!s.contains("degraded"));
}
#[test]
fn resolved_display_with_degradation() {
let resolved = LayoutPolicy::QUALITY
.resolve(&RuntimeCapability::TERMINAL)
.unwrap();
let s = format!("{resolved}");
assert!(s.contains("degraded"));
}
#[test]
fn error_display() {
let err = PolicyError::CapabilityInsufficient {
requested: LayoutTier::Quality,
best_available: LayoutTier::Fast,
};
let s = format!("{err}");
assert!(s.contains("quality"));
assert!(s.contains("fast"));
}
#[test]
fn error_is_error_trait() {
let err = PolicyError::CapabilityInsufficient {
requested: LayoutTier::Quality,
best_available: LayoutTier::Fast,
};
let _: &dyn std::error::Error = &err;
}
#[test]
fn default_policy_is_balanced() {
assert_eq!(LayoutPolicy::default(), LayoutPolicy::BALANCED);
}
#[test]
fn policy_display() {
let s = format!("{}", LayoutPolicy::QUALITY);
assert!(s.contains("quality"));
}
#[test]
fn same_inputs_same_resolution() {
let p1 = LayoutPolicy::QUALITY
.resolve(&RuntimeCapability::FULL)
.unwrap();
let p2 = LayoutPolicy::QUALITY
.resolve(&RuntimeCapability::FULL)
.unwrap();
assert_eq!(p1, p2);
}
#[test]
fn same_degradation_same_result() {
let p1 = LayoutPolicy::QUALITY
.resolve(&RuntimeCapability::TERMINAL)
.unwrap();
let p2 = LayoutPolicy::QUALITY
.resolve(&RuntimeCapability::TERMINAL)
.unwrap();
assert_eq!(p1, p2);
}
#[test]
fn emergency_resolves_with_terminal_caps() {
let result = LayoutPolicy::EMERGENCY.resolve(&RuntimeCapability::TERMINAL);
let resolved = result.unwrap();
assert_eq!(resolved.effective_tier, LayoutTier::Emergency);
assert!(!resolved.degraded);
assert!(!resolved.use_optimal_breaking);
assert!(!resolved.use_hyphenation);
assert!(!resolved.is_justified());
}
#[test]
fn emergency_caps_supported() {
assert!(RuntimeCapability::TERMINAL.supports_tier(LayoutTier::Emergency));
assert!(RuntimeCapability::FULL.supports_tier(LayoutTier::Emergency));
}
#[test]
fn fast_with_full_caps_stays_fast() {
let resolved = LayoutPolicy::FAST
.resolve(&RuntimeCapability::FULL)
.unwrap();
assert_eq!(resolved.effective_tier, LayoutTier::Fast);
assert!(!resolved.degraded);
}
#[test]
fn quality_with_justify_left_override() {
let policy = LayoutPolicy {
justify_override: Some(JustifyMode::Left),
..LayoutPolicy::QUALITY
};
let resolved = policy.resolve(&RuntimeCapability::FULL).unwrap();
assert!(!resolved.is_justified());
assert_eq!(resolved.effective_tier, LayoutTier::Quality);
}
}