fast_rich/
measure.rs

1//! Measure API for calculating renderable dimensions.
2//!
3//! Allows measuring how a renderable will be rendered without actually rendering it.
4
5use crate::console::RenderContext;
6use crate::renderable::Renderable;
7
8/// Measurement of a renderable's dimensions.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct Measurement {
11    /// Minimum width required
12    pub minimum: usize,
13    /// Maximum width that will be used
14    pub maximum: usize,
15    /// Number of lines when rendered
16    pub lines: usize,
17}
18
19impl Measurement {
20    /// Create a new measurement.
21    pub fn new(minimum: usize, maximum: usize, lines: usize) -> Self {
22        Measurement {
23            minimum,
24            maximum,
25            lines,
26        }
27    }
28
29    /// Measure a renderable at a given width.
30    pub fn measure(renderable: &impl Renderable, width: usize) -> Self {
31        let context = RenderContext {
32            width,
33            height: None,
34        };
35        let segments = renderable.render(&context);
36
37        let lines = segments.len();
38        let minimum = renderable.min_width();
39        let maximum = renderable.max_width().min(width);
40
41        Measurement {
42            minimum,
43            maximum,
44            lines,
45        }
46    }
47
48    /// Get the aspect ratio (width / height).
49    pub fn aspect_ratio(&self) -> f64 {
50        if self.lines == 0 {
51            0.0
52        } else {
53            self.maximum as f64 / self.lines as f64
54        }
55    }
56
57    /// Check if the measurement fits within given dimensions.
58    pub fn fits(&self, width: usize, height: usize) -> bool {
59        self.minimum <= width && self.lines <= height
60    }
61
62    /// Get the total area (width * height).
63    pub fn area(&self) -> usize {
64        self.maximum * self.lines
65    }
66}
67
68/// Helper trait for measuring renderables.
69pub trait Measurable {
70    /// Measure this renderable at the given width.
71    fn measure(&self, width: usize) -> Measurement;
72}
73
74impl<T: Renderable> Measurable for T {
75    fn measure(&self, width: usize) -> Measurement {
76        Measurement::measure(self, width)
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use crate::text::Text;
84
85    #[test]
86    fn test_measurement_creation() {
87        let m = Measurement::new(10, 50, 3);
88        assert_eq!(m.minimum, 10);
89        assert_eq!(m.maximum, 50);
90        assert_eq!(m.lines, 3);
91    }
92
93    #[test]
94    fn test_measure_text() {
95        let text = Text::plain("Hello, World!");
96        let m = Measurement::measure(&text, 80);
97
98        assert!(m.minimum > 0);
99        assert!(m.lines > 0);
100    }
101
102    #[test]
103    fn test_aspect_ratio() {
104        let m = Measurement::new(20, 40, 5);
105        assert_eq!(m.aspect_ratio(), 8.0);
106    }
107
108    #[test]
109    fn test_fits() {
110        let m = Measurement::new(10, 40, 3);
111        assert!(m.fits(50, 5));
112        assert!(!m.fits(5, 5)); // Too narrow
113        assert!(!m.fits(50, 2)); // Too short
114    }
115
116    #[test]
117    fn test_area() {
118        let m = Measurement::new(10, 20, 5);
119        assert_eq!(m.area(), 100); // 20 * 5
120    }
121}