Skip to main content

rich_rs/
measure.rs

1//! Measurement: width requirements for renderables.
2
3use crate::segment::Segments;
4use crate::{Console, ConsoleOptions, Renderable};
5
6/// The minimum and maximum width requirements of a renderable.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8pub struct Measurement {
9    /// Minimum width required (content won't fit in less).
10    pub minimum: usize,
11    /// Maximum width the content would use if given unlimited space.
12    pub maximum: usize,
13}
14
15impl Measurement {
16    /// Create a new measurement.
17    pub fn new(minimum: usize, maximum: usize) -> Self {
18        Measurement { minimum, maximum }
19    }
20
21    /// Create a measurement with both min and max set to the same value.
22    pub fn exact(width: usize) -> Self {
23        Measurement {
24            minimum: width,
25            maximum: width,
26        }
27    }
28
29    /// Get the span (difference between max and min).
30    pub fn span(&self) -> usize {
31        self.maximum.saturating_sub(self.minimum)
32    }
33
34    /// Normalize the measurement ensuring minimum <= maximum and both >= 0.
35    ///
36    /// Since we use `usize`, values are always >= 0, but this ensures
37    /// the minimum does not exceed the maximum.
38    ///
39    /// # Examples
40    ///
41    /// ```
42    /// use rich_rs::Measurement;
43    ///
44    /// // Inverted measurement gets corrected
45    /// let m = Measurement::new(50, 10);
46    /// let normalized = m.normalize();
47    /// assert_eq!(normalized.minimum, 10);
48    /// assert_eq!(normalized.maximum, 10);
49    /// ```
50    pub fn normalize(&self) -> Self {
51        let minimum = self.minimum.min(self.maximum);
52        Measurement {
53            minimum,
54            maximum: self.maximum,
55        }
56    }
57
58    /// Get a measurement where both widths are <= the given width.
59    ///
60    /// # Examples
61    ///
62    /// ```
63    /// use rich_rs::Measurement;
64    ///
65    /// let m = Measurement::new(10, 50);
66    /// let constrained = m.with_maximum(30);
67    /// assert_eq!(constrained.minimum, 10);
68    /// assert_eq!(constrained.maximum, 30);
69    ///
70    /// // When width is less than minimum, both get clamped
71    /// let m = Measurement::new(20, 50);
72    /// let constrained = m.with_maximum(15);
73    /// assert_eq!(constrained.minimum, 15);
74    /// assert_eq!(constrained.maximum, 15);
75    /// ```
76    pub fn with_maximum(&self, width: usize) -> Self {
77        Measurement {
78            minimum: self.minimum.min(width),
79            maximum: self.maximum.min(width),
80        }
81    }
82
83    /// Get a measurement where both widths are >= the given width.
84    ///
85    /// # Examples
86    ///
87    /// ```
88    /// use rich_rs::Measurement;
89    ///
90    /// let m = Measurement::new(10, 50);
91    /// let constrained = m.with_minimum(20);
92    /// assert_eq!(constrained.minimum, 20);
93    /// assert_eq!(constrained.maximum, 50);
94    ///
95    /// // When width is greater than maximum, both get raised
96    /// let m = Measurement::new(10, 30);
97    /// let constrained = m.with_minimum(40);
98    /// assert_eq!(constrained.minimum, 40);
99    /// assert_eq!(constrained.maximum, 40);
100    /// ```
101    pub fn with_minimum(&self, width: usize) -> Self {
102        Measurement {
103            minimum: self.minimum.max(width),
104            maximum: self.maximum.max(width),
105        }
106    }
107
108    /// Clamp the measurement within optional min and max bounds.
109    ///
110    /// This clamps the measurement itself (both minimum and maximum fields),
111    /// not a width value. Use `clamp_width` to clamp a width within measurement bounds.
112    ///
113    /// # Examples
114    ///
115    /// ```
116    /// use rich_rs::Measurement;
117    ///
118    /// let m = Measurement::new(10, 50);
119    ///
120    /// // Clamp with both bounds
121    /// let clamped = m.clamp_bounds(Some(15), Some(40));
122    /// assert_eq!(clamped.minimum, 15);
123    /// assert_eq!(clamped.maximum, 40);
124    ///
125    /// // Clamp with only max bound
126    /// let clamped = m.clamp_bounds(None, Some(30));
127    /// assert_eq!(clamped.minimum, 10);
128    /// assert_eq!(clamped.maximum, 30);
129    ///
130    /// // Clamp with only min bound
131    /// let clamped = m.clamp_bounds(Some(20), None);
132    /// assert_eq!(clamped.minimum, 20);
133    /// assert_eq!(clamped.maximum, 50);
134    /// ```
135    pub fn clamp_bounds(&self, min_width: Option<usize>, max_width: Option<usize>) -> Self {
136        let mut result = *self;
137        if let Some(min_w) = min_width {
138            result = result.with_minimum(min_w);
139        }
140        if let Some(max_w) = max_width {
141            result = result.with_maximum(max_w);
142        }
143        result
144    }
145
146    /// Clamp a width value to within the measurement bounds.
147    ///
148    /// Returns a width that is >= minimum and <= maximum.
149    ///
150    /// # Panics
151    ///
152    /// Panics if the measurement invariant is violated (i.e., `minimum > maximum`).
153    /// In debug builds, a `debug_assert!` provides a clearer error message.
154    /// Use [`normalize`](Self::normalize) to fix invalid measurements before
155    /// calling this method.
156    ///
157    /// # Examples
158    ///
159    /// ```
160    /// use rich_rs::Measurement;
161    ///
162    /// let m = Measurement::new(10, 50);
163    /// assert_eq!(m.clamp_width(5), 10);   // Below minimum
164    /// assert_eq!(m.clamp_width(30), 30);  // Within bounds
165    /// assert_eq!(m.clamp_width(100), 50); // Above maximum
166    /// ```
167    #[track_caller]
168    pub fn clamp_width(&self, width: usize) -> usize {
169        debug_assert!(
170            self.minimum <= self.maximum,
171            "Measurement invariant violated: minimum ({}) > maximum ({})",
172            self.minimum,
173            self.maximum
174        );
175        width.clamp(self.minimum, self.maximum)
176    }
177
178    /// Combine with another measurement, taking the max of mins and maxes.
179    pub fn union(&self, other: &Measurement) -> Self {
180        Measurement {
181            minimum: self.minimum.max(other.minimum),
182            maximum: self.maximum.max(other.maximum),
183        }
184    }
185
186    /// Create a measurement from rendered segments.
187    ///
188    /// This is the default measurement strategy: render and measure the result.
189    /// The minimum is the longest word, maximum is the widest rendered line.
190    pub fn from_segments(segments: &Segments) -> Self {
191        let mut max_line_width = 0;
192        let mut current_line_width = 0;
193        let mut max_word_width = 0;
194        let mut current_word_width = 0;
195
196        for segment in segments.iter() {
197            for c in segment.text.chars() {
198                if c == '\n' {
199                    max_line_width = max_line_width.max(current_line_width);
200                    max_word_width = max_word_width.max(current_word_width);
201                    current_line_width = 0;
202                    current_word_width = 0;
203                    continue;
204                }
205
206                let char_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
207                current_line_width += char_width;
208
209                if c.is_whitespace() {
210                    max_word_width = max_word_width.max(current_word_width);
211                    current_word_width = 0;
212                } else {
213                    current_word_width += char_width;
214                }
215            }
216        }
217
218        // Account for trailing line / word when input doesn't end with '\n'.
219        max_line_width = max_line_width.max(current_line_width);
220        max_word_width = max_word_width.max(current_word_width);
221
222        Measurement {
223            minimum: max_word_width,
224            maximum: max_line_width,
225        }
226    }
227}
228
229/// Get a combined measurement for multiple renderables.
230///
231/// Returns a measurement that would fit all the given renderables by taking
232/// the maximum of all minimums and the maximum of all maximums.
233///
234/// # Examples
235///
236/// ```ignore
237/// use rich_rs::{Console, ConsoleOptions, measure_renderables};
238///
239/// let console = Console::new();
240/// let options = ConsoleOptions::default();
241/// let renderables: Vec<&dyn Renderable> = vec![&"Hello", &"World!"];
242/// let measurement = measure_renderables(&console, &options, &renderables);
243/// ```
244pub fn measure_renderables(
245    console: &Console,
246    options: &ConsoleOptions,
247    renderables: &[&dyn Renderable],
248) -> Measurement {
249    if renderables.is_empty() {
250        return Measurement::new(0, 0);
251    }
252
253    let mut max_minimum = 0;
254    let mut max_maximum = 0;
255
256    for renderable in renderables {
257        let measurement = renderable.measure(console, options);
258        max_minimum = max_minimum.max(measurement.minimum);
259        max_maximum = max_maximum.max(measurement.maximum);
260    }
261
262    Measurement {
263        minimum: max_minimum,
264        maximum: max_maximum,
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271    use crate::segment::Segment;
272
273    #[test]
274    fn test_measurement_basic() {
275        let m = Measurement::new(10, 50);
276        assert_eq!(m.minimum, 10);
277        assert_eq!(m.maximum, 50);
278    }
279
280    #[test]
281    fn test_measurement_exact() {
282        let m = Measurement::exact(25);
283        assert_eq!(m.minimum, 25);
284        assert_eq!(m.maximum, 25);
285        assert_eq!(m.span(), 0);
286    }
287
288    #[test]
289    fn test_span() {
290        let m = Measurement::new(10, 50);
291        assert_eq!(m.span(), 40);
292
293        // span uses saturating_sub, so inverted returns 0
294        let inverted = Measurement::new(50, 10);
295        assert_eq!(inverted.span(), 0);
296    }
297
298    #[test]
299    fn test_normalize() {
300        // Normal measurement stays the same
301        let m = Measurement::new(10, 50);
302        let normalized = m.normalize();
303        assert_eq!(normalized.minimum, 10);
304        assert_eq!(normalized.maximum, 50);
305
306        // Inverted measurement gets minimum clamped to maximum
307        let inverted = Measurement::new(50, 10);
308        let normalized = inverted.normalize();
309        assert_eq!(normalized.minimum, 10);
310        assert_eq!(normalized.maximum, 10);
311
312        // Equal values stay equal
313        let equal = Measurement::new(25, 25);
314        let normalized = equal.normalize();
315        assert_eq!(normalized.minimum, 25);
316        assert_eq!(normalized.maximum, 25);
317    }
318
319    #[test]
320    fn test_with_maximum() {
321        let m = Measurement::new(10, 50);
322
323        // Width greater than maximum - no change
324        let result = m.with_maximum(100);
325        assert_eq!(result.minimum, 10);
326        assert_eq!(result.maximum, 50);
327
328        // Width between min and max - only max changes
329        let result = m.with_maximum(30);
330        assert_eq!(result.minimum, 10);
331        assert_eq!(result.maximum, 30);
332
333        // Width less than minimum - both get clamped
334        let result = m.with_maximum(5);
335        assert_eq!(result.minimum, 5);
336        assert_eq!(result.maximum, 5);
337
338        // Width equals minimum
339        let result = m.with_maximum(10);
340        assert_eq!(result.minimum, 10);
341        assert_eq!(result.maximum, 10);
342    }
343
344    #[test]
345    fn test_with_minimum() {
346        let m = Measurement::new(10, 50);
347
348        // Width less than minimum - no change
349        let result = m.with_minimum(5);
350        assert_eq!(result.minimum, 10);
351        assert_eq!(result.maximum, 50);
352
353        // Width between min and max - only min changes
354        let result = m.with_minimum(30);
355        assert_eq!(result.minimum, 30);
356        assert_eq!(result.maximum, 50);
357
358        // Width greater than maximum - both get raised
359        let result = m.with_minimum(60);
360        assert_eq!(result.minimum, 60);
361        assert_eq!(result.maximum, 60);
362
363        // Width equals maximum
364        let result = m.with_minimum(50);
365        assert_eq!(result.minimum, 50);
366        assert_eq!(result.maximum, 50);
367    }
368
369    #[test]
370    fn test_clamp_bounds() {
371        let m = Measurement::new(10, 50);
372
373        // Both bounds
374        let clamped = m.clamp_bounds(Some(15), Some(40));
375        assert_eq!(clamped.minimum, 15);
376        assert_eq!(clamped.maximum, 40);
377
378        // Only min bound
379        let clamped = m.clamp_bounds(Some(20), None);
380        assert_eq!(clamped.minimum, 20);
381        assert_eq!(clamped.maximum, 50);
382
383        // Only max bound
384        let clamped = m.clamp_bounds(None, Some(30));
385        assert_eq!(clamped.minimum, 10);
386        assert_eq!(clamped.maximum, 30);
387
388        // No bounds - no change
389        let clamped = m.clamp_bounds(None, None);
390        assert_eq!(clamped.minimum, 10);
391        assert_eq!(clamped.maximum, 50);
392
393        // Bounds that make min > max get corrected by ordering
394        // with_minimum(40) -> (40, 50), then with_maximum(30) -> (30, 30)
395        let clamped = m.clamp_bounds(Some(40), Some(30));
396        assert_eq!(clamped.minimum, 30);
397        assert_eq!(clamped.maximum, 30);
398    }
399
400    #[test]
401    fn test_clamp_width() {
402        let m = Measurement::new(10, 50);
403        assert_eq!(m.clamp_width(5), 10); // Below minimum
404        assert_eq!(m.clamp_width(10), 10); // At minimum
405        assert_eq!(m.clamp_width(30), 30); // Within bounds
406        assert_eq!(m.clamp_width(50), 50); // At maximum
407        assert_eq!(m.clamp_width(100), 50); // Above maximum
408    }
409
410    #[test]
411    fn test_union() {
412        let m1 = Measurement::new(10, 50);
413        let m2 = Measurement::new(15, 40);
414        let combined = m1.union(&m2);
415        assert_eq!(combined.minimum, 15);
416        assert_eq!(combined.maximum, 50);
417
418        let m3 = Measurement::new(5, 60);
419        let combined = m1.union(&m3);
420        assert_eq!(combined.minimum, 10);
421        assert_eq!(combined.maximum, 60);
422    }
423
424    #[test]
425    fn test_default() {
426        let m = Measurement::default();
427        assert_eq!(m.minimum, 0);
428        assert_eq!(m.maximum, 0);
429    }
430
431    #[test]
432    fn test_measure_renderables_empty() {
433        let console = Console::new();
434        let options = ConsoleOptions::default();
435        let renderables: Vec<&dyn Renderable> = vec![];
436        let measurement = measure_renderables(&console, &options, &renderables);
437        assert_eq!(measurement.minimum, 0);
438        assert_eq!(measurement.maximum, 0);
439    }
440
441    #[test]
442    fn test_measure_renderables_single() {
443        let console = Console::new();
444        let options = ConsoleOptions::default();
445        let text = String::from("Hello");
446        let renderables: Vec<&dyn Renderable> = vec![&text];
447        let measurement = measure_renderables(&console, &options, &renderables);
448        // "Hello" has no spaces, so minimum == maximum == 5
449        assert_eq!(measurement.minimum, 5);
450        assert_eq!(measurement.maximum, 5);
451    }
452
453    #[test]
454    fn test_measure_renderables_multiple() {
455        let console = Console::new();
456        let options = ConsoleOptions::default();
457        let short = String::from("Hi");
458        let long = String::from("Hello World");
459        let renderables: Vec<&dyn Renderable> = vec![&short, &long];
460        let measurement = measure_renderables(&console, &options, &renderables);
461        // short: min=2, max=2
462        // long: "Hello World" has min=5 (longest word), max=11
463        // Combined: max(2,5)=5, max(2,11)=11
464        assert_eq!(measurement.minimum, 5);
465        assert_eq!(measurement.maximum, 11);
466    }
467
468    #[test]
469    fn test_measure_renderables_takes_max_of_measurements() {
470        let console = Console::new();
471        let options = ConsoleOptions::default();
472        let a = String::from("ABCDEFGHIJ"); // min=10, max=10 (no spaces)
473        let b = String::from("XY Z"); // min=2 ("XY"), max=4
474        let c = String::from("12345 67"); // min=5 ("12345"), max=8
475        let renderables: Vec<&dyn Renderable> = vec![&a, &b, &c];
476        let measurement = measure_renderables(&console, &options, &renderables);
477        // max of minimums: max(10, 2, 5) = 10
478        // max of maximums: max(10, 4, 8) = 10
479        assert_eq!(measurement.minimum, 10);
480        assert_eq!(measurement.maximum, 10);
481    }
482
483    #[test]
484    fn test_from_segments_multiline_uses_widest_line() {
485        let segments: Segments = vec![Segment::new("abcd\nef")].into();
486        let measurement = Measurement::from_segments(&segments);
487
488        assert_eq!(measurement.minimum, 4);
489        assert_eq!(measurement.maximum, 4);
490    }
491}