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 total width.
190    ///
191    /// **Note**: This method assumes single-line content. For multi-line content,
192    /// the maximum will include the total width of all lines combined rather than
193    /// the width of the widest line. Handle multi-line content by splitting into
194    /// lines first and taking the union of per-line measurements.
195    pub fn from_segments(segments: &Segments) -> Self {
196        let mut total_width = 0;
197        let mut max_word_width = 0;
198        let mut current_word_width = 0;
199
200        for segment in segments.iter() {
201            for c in segment.text.chars() {
202                let char_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
203                total_width += char_width;
204
205                if c.is_whitespace() || c == '\n' {
206                    max_word_width = max_word_width.max(current_word_width);
207                    current_word_width = 0;
208                } else {
209                    current_word_width += char_width;
210                }
211            }
212        }
213        // Don't forget the last word
214        max_word_width = max_word_width.max(current_word_width);
215
216        Measurement {
217            minimum: max_word_width,
218            maximum: total_width,
219        }
220    }
221}
222
223/// Get a combined measurement for multiple renderables.
224///
225/// Returns a measurement that would fit all the given renderables by taking
226/// the maximum of all minimums and the maximum of all maximums.
227///
228/// # Examples
229///
230/// ```ignore
231/// use rich_rs::{Console, ConsoleOptions, measure_renderables};
232///
233/// let console = Console::new();
234/// let options = ConsoleOptions::default();
235/// let renderables: Vec<&dyn Renderable> = vec![&"Hello", &"World!"];
236/// let measurement = measure_renderables(&console, &options, &renderables);
237/// ```
238pub fn measure_renderables(
239    console: &Console,
240    options: &ConsoleOptions,
241    renderables: &[&dyn Renderable],
242) -> Measurement {
243    if renderables.is_empty() {
244        return Measurement::new(0, 0);
245    }
246
247    let mut max_minimum = 0;
248    let mut max_maximum = 0;
249
250    for renderable in renderables {
251        let measurement = renderable.measure(console, options);
252        max_minimum = max_minimum.max(measurement.minimum);
253        max_maximum = max_maximum.max(measurement.maximum);
254    }
255
256    Measurement {
257        minimum: max_minimum,
258        maximum: max_maximum,
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn test_measurement_basic() {
268        let m = Measurement::new(10, 50);
269        assert_eq!(m.minimum, 10);
270        assert_eq!(m.maximum, 50);
271    }
272
273    #[test]
274    fn test_measurement_exact() {
275        let m = Measurement::exact(25);
276        assert_eq!(m.minimum, 25);
277        assert_eq!(m.maximum, 25);
278        assert_eq!(m.span(), 0);
279    }
280
281    #[test]
282    fn test_span() {
283        let m = Measurement::new(10, 50);
284        assert_eq!(m.span(), 40);
285
286        // span uses saturating_sub, so inverted returns 0
287        let inverted = Measurement::new(50, 10);
288        assert_eq!(inverted.span(), 0);
289    }
290
291    #[test]
292    fn test_normalize() {
293        // Normal measurement stays the same
294        let m = Measurement::new(10, 50);
295        let normalized = m.normalize();
296        assert_eq!(normalized.minimum, 10);
297        assert_eq!(normalized.maximum, 50);
298
299        // Inverted measurement gets minimum clamped to maximum
300        let inverted = Measurement::new(50, 10);
301        let normalized = inverted.normalize();
302        assert_eq!(normalized.minimum, 10);
303        assert_eq!(normalized.maximum, 10);
304
305        // Equal values stay equal
306        let equal = Measurement::new(25, 25);
307        let normalized = equal.normalize();
308        assert_eq!(normalized.minimum, 25);
309        assert_eq!(normalized.maximum, 25);
310    }
311
312    #[test]
313    fn test_with_maximum() {
314        let m = Measurement::new(10, 50);
315
316        // Width greater than maximum - no change
317        let result = m.with_maximum(100);
318        assert_eq!(result.minimum, 10);
319        assert_eq!(result.maximum, 50);
320
321        // Width between min and max - only max changes
322        let result = m.with_maximum(30);
323        assert_eq!(result.minimum, 10);
324        assert_eq!(result.maximum, 30);
325
326        // Width less than minimum - both get clamped
327        let result = m.with_maximum(5);
328        assert_eq!(result.minimum, 5);
329        assert_eq!(result.maximum, 5);
330
331        // Width equals minimum
332        let result = m.with_maximum(10);
333        assert_eq!(result.minimum, 10);
334        assert_eq!(result.maximum, 10);
335    }
336
337    #[test]
338    fn test_with_minimum() {
339        let m = Measurement::new(10, 50);
340
341        // Width less than minimum - no change
342        let result = m.with_minimum(5);
343        assert_eq!(result.minimum, 10);
344        assert_eq!(result.maximum, 50);
345
346        // Width between min and max - only min changes
347        let result = m.with_minimum(30);
348        assert_eq!(result.minimum, 30);
349        assert_eq!(result.maximum, 50);
350
351        // Width greater than maximum - both get raised
352        let result = m.with_minimum(60);
353        assert_eq!(result.minimum, 60);
354        assert_eq!(result.maximum, 60);
355
356        // Width equals maximum
357        let result = m.with_minimum(50);
358        assert_eq!(result.minimum, 50);
359        assert_eq!(result.maximum, 50);
360    }
361
362    #[test]
363    fn test_clamp_bounds() {
364        let m = Measurement::new(10, 50);
365
366        // Both bounds
367        let clamped = m.clamp_bounds(Some(15), Some(40));
368        assert_eq!(clamped.minimum, 15);
369        assert_eq!(clamped.maximum, 40);
370
371        // Only min bound
372        let clamped = m.clamp_bounds(Some(20), None);
373        assert_eq!(clamped.minimum, 20);
374        assert_eq!(clamped.maximum, 50);
375
376        // Only max bound
377        let clamped = m.clamp_bounds(None, Some(30));
378        assert_eq!(clamped.minimum, 10);
379        assert_eq!(clamped.maximum, 30);
380
381        // No bounds - no change
382        let clamped = m.clamp_bounds(None, None);
383        assert_eq!(clamped.minimum, 10);
384        assert_eq!(clamped.maximum, 50);
385
386        // Bounds that make min > max get corrected by ordering
387        // with_minimum(40) -> (40, 50), then with_maximum(30) -> (30, 30)
388        let clamped = m.clamp_bounds(Some(40), Some(30));
389        assert_eq!(clamped.minimum, 30);
390        assert_eq!(clamped.maximum, 30);
391    }
392
393    #[test]
394    fn test_clamp_width() {
395        let m = Measurement::new(10, 50);
396        assert_eq!(m.clamp_width(5), 10); // Below minimum
397        assert_eq!(m.clamp_width(10), 10); // At minimum
398        assert_eq!(m.clamp_width(30), 30); // Within bounds
399        assert_eq!(m.clamp_width(50), 50); // At maximum
400        assert_eq!(m.clamp_width(100), 50); // Above maximum
401    }
402
403    #[test]
404    fn test_union() {
405        let m1 = Measurement::new(10, 50);
406        let m2 = Measurement::new(15, 40);
407        let combined = m1.union(&m2);
408        assert_eq!(combined.minimum, 15);
409        assert_eq!(combined.maximum, 50);
410
411        let m3 = Measurement::new(5, 60);
412        let combined = m1.union(&m3);
413        assert_eq!(combined.minimum, 10);
414        assert_eq!(combined.maximum, 60);
415    }
416
417    #[test]
418    fn test_default() {
419        let m = Measurement::default();
420        assert_eq!(m.minimum, 0);
421        assert_eq!(m.maximum, 0);
422    }
423
424    #[test]
425    fn test_measure_renderables_empty() {
426        let console = Console::new();
427        let options = ConsoleOptions::default();
428        let renderables: Vec<&dyn Renderable> = vec![];
429        let measurement = measure_renderables(&console, &options, &renderables);
430        assert_eq!(measurement.minimum, 0);
431        assert_eq!(measurement.maximum, 0);
432    }
433
434    #[test]
435    fn test_measure_renderables_single() {
436        let console = Console::new();
437        let options = ConsoleOptions::default();
438        let text = String::from("Hello");
439        let renderables: Vec<&dyn Renderable> = vec![&text];
440        let measurement = measure_renderables(&console, &options, &renderables);
441        // "Hello" has no spaces, so minimum == maximum == 5
442        assert_eq!(measurement.minimum, 5);
443        assert_eq!(measurement.maximum, 5);
444    }
445
446    #[test]
447    fn test_measure_renderables_multiple() {
448        let console = Console::new();
449        let options = ConsoleOptions::default();
450        let short = String::from("Hi");
451        let long = String::from("Hello World");
452        let renderables: Vec<&dyn Renderable> = vec![&short, &long];
453        let measurement = measure_renderables(&console, &options, &renderables);
454        // short: min=2, max=2
455        // long: "Hello World" has min=5 (longest word), max=11
456        // Combined: max(2,5)=5, max(2,11)=11
457        assert_eq!(measurement.minimum, 5);
458        assert_eq!(measurement.maximum, 11);
459    }
460
461    #[test]
462    fn test_measure_renderables_takes_max_of_measurements() {
463        let console = Console::new();
464        let options = ConsoleOptions::default();
465        let a = String::from("ABCDEFGHIJ"); // min=10, max=10 (no spaces)
466        let b = String::from("XY Z"); // min=2 ("XY"), max=4
467        let c = String::from("12345 67"); // min=5 ("12345"), max=8
468        let renderables: Vec<&dyn Renderable> = vec![&a, &b, &c];
469        let measurement = measure_renderables(&console, &options, &renderables);
470        // max of minimums: max(10, 2, 5) = 10
471        // max of maximums: max(10, 4, 8) = 10
472        assert_eq!(measurement.minimum, 10);
473        assert_eq!(measurement.maximum, 10);
474    }
475}