Skip to main content

rusty_rich/
constrain.rs

1//! Constrain the width of a renderable.
2//!
3//! Equivalent to Python Rich's `constrain.py`. Wraps any renderable and
4//! limits its rendered width to a specified maximum, applying the chosen
5//! overflow behaviour when the content exceeds the constraint.
6
7use crate::console::{ConsoleOptions, DynRenderable, OverflowMethod, RenderResult, Renderable};
8use crate::measure::Measurement;
9
10/// Constrains a renderable to a maximum width.
11///
12/// Wraps an inner renderable and caps its rendering width to `width`.
13/// If the content is wider, the `overflow` method determines how it is
14/// handled (fold, crop, ellipsis, or ignore).
15#[derive(Debug, Clone)]
16pub struct Constrain {
17    /// The inner renderable.
18    renderable: DynRenderable,
19    /// The maximum width to constrain to.
20    width: Option<usize>,
21    /// How to handle overflow when content exceeds the constraint.
22    overflow: OverflowMethod,
23}
24
25impl Constrain {
26    /// Create a new `Constrain` wrapping the given renderable.
27    pub fn new(renderable: impl Renderable + Send + Sync + 'static) -> Self {
28        Self {
29            renderable: DynRenderable::new(renderable),
30            width: None,
31            overflow: OverflowMethod::Fold,
32        }
33    }
34
35    /// Builder: set the maximum width.
36    pub fn width(mut self, width: usize) -> Self {
37        self.width = Some(width);
38        self
39    }
40
41    /// Builder: set the overflow method.
42    pub fn overflow(mut self, overflow: OverflowMethod) -> Self {
43        self.overflow = overflow;
44        self
45    }
46}
47
48impl Renderable for Constrain {
49    fn render(&self, options: &ConsoleOptions) -> RenderResult {
50        let constrained_width = self.width.unwrap_or(options.max_width);
51        let constrained_opts = ConsoleOptions {
52            max_width: constrained_width,
53            overflow: Some(self.overflow),
54            ..options.clone()
55        };
56        self.renderable.render(&constrained_opts)
57    }
58
59    fn measure(&self, options: &ConsoleOptions) -> Option<Measurement> {
60        let m = self.renderable.measure(options)?;
61        let max = match self.width {
62            Some(w) => m.maximum.min(w),
63            None => m.maximum,
64        };
65        Some(Measurement::new(m.minimum, max))
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use crate::console::ConsoleOptions;
73
74    #[test]
75    fn test_constrain_defaults() {
76        let c = Constrain::new("Hello, World!");
77        let opts = ConsoleOptions::default();
78        let result = c.render(&opts);
79        let ansi = result.to_ansi();
80        assert!(ansi.contains("Hello, World!"));
81    }
82
83    #[test]
84    fn test_constrain_width() {
85        // Constrain with a string — strings don't implement measure (returns None)
86        let text = "Short";
87        let c = Constrain::new(text.to_string()).width(10);
88        let opts = ConsoleOptions::default();
89        let result = c.render(&opts);
90        // Verify rendering respects the constraint
91        let ansi = result.to_ansi();
92        assert!(ansi.contains("Short"));
93    }
94
95    #[test]
96    fn test_constrain_render_respects_width() {
97        let c = Constrain::new("Hello!").width(3);
98        let opts = ConsoleOptions::default();
99        let result = c.render(&opts);
100        // The inner renderable's max_width is constrained to 3
101        let _ = result.to_ansi();
102    }
103
104    #[test]
105    fn test_constrain_overflow_method() {
106        let c = Constrain::new("Hello")
107            .width(3)
108            .overflow(OverflowMethod::Crop);
109        assert!(matches!(c.overflow, OverflowMethod::Crop));
110    }
111
112    #[test]
113    fn test_constrain_no_width() {
114        let c = Constrain::new("Hello");
115        assert!(c.width.is_none());
116    }
117
118    #[test]
119    fn test_constrain_builder_chain() {
120        let c = Constrain::new("text")
121            .width(20)
122            .overflow(OverflowMethod::Ellipsis);
123        assert_eq!(c.width, Some(20));
124        assert!(matches!(c.overflow, OverflowMethod::Ellipsis));
125    }
126}