Skip to main content

rusty_rich/
styled.rs

1//! A pre-styled renderable wrapper.
2//!
3//! Equivalent to Python Rich's `styled.py`. Wraps any renderable and
4//! applies a fixed [`Style`] to all of its output, combining with any
5//! existing styles on the inner segments via [`Style::combine`].
6
7use crate::console::{ConsoleOptions, DynRenderable, RenderResult, Renderable};
8use crate::style::Style;
9
10/// Wraps a renderable with a pre-applied style.
11///
12/// The style is applied to every segment produced by the inner renderable,
13/// combining with whatever style the segment already has (the wrapper style
14/// takes precedence via [`Style::combine`]).
15#[derive(Debug, Clone)]
16pub struct Styled {
17    /// The inner renderable.
18    renderable: DynRenderable,
19    /// The style to apply to all output.
20    style: Style,
21}
22
23impl Styled {
24    /// Create a new `Styled` wrapper.
25    ///
26    /// The `style` is applied to every segment rendered by `renderable`.
27    pub fn new(renderable: impl Renderable + Send + Sync + 'static, style: Style) -> Self {
28        Self {
29            renderable: DynRenderable::new(renderable),
30            style,
31        }
32    }
33
34    /// Builder: replace the applied style.
35    pub fn style(mut self, style: Style) -> Self {
36        self.style = style;
37        self
38    }
39}
40
41impl Renderable for Styled {
42    fn render(&self, options: &ConsoleOptions) -> RenderResult {
43        let mut result = self.renderable.render(options);
44        let default_style = Style::new();
45
46        // Apply the wrapper style to every segment in every line
47        for line in &mut result.lines {
48            for seg in line.iter_mut() {
49                let existing_style = seg.style.as_ref().unwrap_or(&default_style);
50                seg.style = Some(self.style.combine(existing_style));
51            }
52        }
53
54        // Also apply to items (nested renderables are not modified —
55        // they will be styled when they render)
56        for item in &mut result.items {
57            if let crate::console::RenderItem::Segment(ref mut seg) = item {
58                let existing_style = seg.style.as_ref().unwrap_or(&default_style);
59                seg.style = Some(self.style.combine(existing_style));
60            }
61        }
62
63        result
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use crate::color::Color;
71    use crate::console::ConsoleOptions;
72
73    #[test]
74    fn test_styled_default() {
75        let style = Style::new().bold(true);
76        let styled = Styled::new("Hello", style);
77        let opts = ConsoleOptions::default();
78        let result = styled.render(&opts);
79        let ansi = result.to_ansi();
80        assert!(ansi.contains("Hello"));
81        // Bold ANSI code is \x1b[1m
82        assert!(ansi.contains("\x1b[1m") || ansi.contains("[1m"));
83    }
84
85    #[test]
86    fn test_styled_with_color() {
87        let color = Color::parse("red").unwrap();
88        let style = Style::new().color(color);
89        let styled = Styled::new("Red Text", style);
90        let opts = ConsoleOptions::default();
91        let result = styled.render(&opts);
92        let ansi = result.to_ansi();
93        assert!(ansi.contains("Red Text"));
94    }
95
96    #[test]
97    fn test_styled_builder_replace_style() {
98        let s1 = Style::new().bold(true);
99        let s2 = Style::new().italic(true);
100        let styled = Styled::new("text", s1).style(s2);
101        let opts = ConsoleOptions::default();
102        let result = styled.render(&opts);
103        let ansi = result.to_ansi();
104        assert!(ansi.contains("text"));
105    }
106
107    #[test]
108    fn test_styled_style_override() {
109        // Inner style has underline, wrapper has bold
110        let outer_style = Style::new().bold(true);
111        let styled = Styled::new("test", outer_style);
112        let opts = ConsoleOptions::default();
113        let result = styled.render(&opts);
114        let ansi = result.to_ansi();
115        assert!(ansi.contains("test"));
116    }
117
118    #[test]
119    fn test_styled_multiple_segments() {
120        let style = Style::new().color(Color::parse("green").unwrap());
121        let text = "Hello\nWorld";
122        let styled = Styled::new(text, style);
123        let opts = ConsoleOptions::default();
124        let result = styled.render(&opts);
125        let ansi = result.to_ansi();
126        assert!(ansi.contains("Hello"));
127        assert!(ansi.contains("World"));
128    }
129}