Skip to main content

rich/
screen.rs

1//! A full-screen renderable.
2//!
3//! Port of `rich/screen.py`. [`Screen`] fills the entire console region
4//! (width × height), rendering its child into it and cropping/padding to an
5//! exact rectangle, optionally under a background [`Style`]. Used with the
6//! alternate screen buffer (see [`Control::alt_screen`](crate::control::Control)).
7
8use crate::console::{Console, ConsoleOptions};
9use crate::protocol::Renderable;
10use crate::segment::Segment;
11use crate::style::{Style, StyleType};
12
13/// A renderable that fills the screen and crops excess. Mirrors
14/// `rich.screen.Screen`.
15pub struct Screen {
16    /// Upstream's `Group(*renderables)`.
17    renderables: Vec<Box<dyn Renderable>>,
18    style: Option<StyleType>,
19    application_mode: bool,
20}
21
22impl Screen {
23    /// A screen filled with `renderable`.
24    pub fn new(renderable: Box<dyn Renderable>) -> Self {
25        Screen::from_renderables(vec![renderable])
26    }
27
28    /// A screen of several renderables, one after another: upstream's
29    /// `Screen(*renderables)`, which groups them.
30    pub fn from_renderables(renderables: Vec<Box<dyn Renderable>>) -> Self {
31        Screen {
32            renderables,
33            style: None,
34            application_mode: false,
35        }
36    }
37
38    /// An empty screen (blank fill).
39    pub fn empty() -> Self {
40        Screen::from_renderables(Vec::new())
41    }
42
43    /// Set a background style applied under the whole screen (upstream
44    /// `style=`): a [`Style`](crate::style::Style), or a theme name or style
45    /// definition resolved at render time.
46    pub fn style(mut self, style: impl Into<StyleType>) -> Self {
47        self.style = Some(style.into());
48        self
49    }
50
51    /// Separate rows with `"\n\r"` rather than `"\n"` (upstream
52    /// `application_mode=`), for a terminal in application mode.
53    pub fn application_mode(mut self, application_mode: bool) -> Self {
54        self.application_mode = application_mode;
55        self
56    }
57}
58
59/// `Group(*renderables)` for [`Screen`]: each renderable rendered in turn,
60/// the outputs joined by a line break.
61struct ScreenGroup<'a>(&'a [Box<dyn Renderable>]);
62
63impl Renderable for ScreenGroup<'_> {
64    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
65        let mut segments = Vec::new();
66        let mut first = true;
67        for renderable in self.0 {
68            let rendered = console.render(renderable.as_ref(), Some(options));
69            if rendered.is_empty() {
70                continue;
71            }
72            if !first {
73                segments.push(Segment::line());
74            }
75            first = false;
76            segments.extend(rendered);
77        }
78        segments
79    }
80}
81
82impl Renderable for Screen {
83    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
84        let style = self
85            .style
86            .as_ref()
87            .map(|style| console.get_style(style).unwrap_or_default())
88            .filter(|style| !style.is_null());
89        render_screen_with(
90            &ScreenGroup(&self.renderables),
91            style,
92            self.application_mode,
93            console,
94            options,
95        )
96    }
97
98    /// Upstream ends a `Screen` with its last row, without a line break, so a
99    /// printed `Screen` leaves the cursor on that row.
100    fn print_end_line(&self, _options: &ConsoleOptions) -> bool {
101        false
102    }
103}
104
105/// `Screen.__rich_console__` over a borrowed renderable, so a `Live` display
106/// in alternate-screen mode can fill the screen with the renderable it owns.
107pub(crate) fn render_screen(
108    renderable: Option<&dyn Renderable>,
109    style: Option<&Style>,
110    console: &Console,
111    options: &ConsoleOptions,
112) -> Vec<Segment> {
113    // `self.renderable or ""`.
114    let content: &dyn Renderable = match renderable {
115        Some(renderable) => renderable,
116        None => &ScreenGroup(&[]),
117    };
118    render_screen_with(content, style.cloned(), false, console, options)
119}
120
121/// The body of `Screen.__rich_console__`, with the style already resolved.
122fn render_screen_with(
123    content: &dyn Renderable,
124    style: Option<Style>,
125    application_mode: bool,
126    console: &Console,
127    options: &ConsoleOptions,
128) -> Vec<Segment> {
129    // `width, height = options.size`.
130    let width = options.size.width;
131    let height = options.size.height;
132    // `console.render_lines(self.renderable or "", render_options,
133    // style=style, pad=True)`.
134    let render_options = options.update_dimensions(width, height);
135    let lines = console.render_lines_styled(content, &render_options, style.as_ref(), true);
136    // `Segment.set_shape(lines, width, height, style=style)`.
137    let pad_style = style.clone();
138    let mut lines: Vec<Vec<Segment>> = lines
139        .into_iter()
140        .take(height)
141        .map(|line| Segment::adjust_line_length(&line, width, pad_style.clone()))
142        .collect();
143    while lines.len() < height {
144        lines.push(vec![Segment::new(" ".repeat(width), pad_style.clone())]);
145    }
146
147    let new_line = if application_mode {
148        Segment::new("\n\r", None)
149    } else {
150        Segment::line()
151    };
152    let mut segments = Vec::new();
153    let last = lines.len().saturating_sub(1);
154    for (index, line) in lines.into_iter().enumerate() {
155        segments.extend(line);
156        if index != last {
157            segments.push(new_line.clone());
158        }
159    }
160    segments
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use crate::color::ColorSystem;
167    use crate::text::Text;
168
169    fn console(width: usize, height: usize) -> Console {
170        Console::builder()
171            .force_terminal(true)
172            .color_system(Some(ColorSystem::Truecolor))
173            .width(width)
174            .height(height)
175            .build()
176    }
177
178    #[test]
179    fn fills_to_width_and_height() {
180        let screen = Screen::new(Box::new(Text::new("hi")));
181        let out = console(6, 3).capture(|c| c.print(&screen));
182        // 3 rows of 6 cells: "hi" then two blank rows, with no line break
183        // after the last, as upstream prints a `Screen`.
184        assert_eq!(out, "hi    \n      \n      ");
185    }
186
187    #[test]
188    fn empty_screen_is_blank_rectangle() {
189        let out = console(4, 2).capture(|c| c.print(&Screen::empty()));
190        assert_eq!(out, "    \n    ");
191    }
192}