line_ui/
render.rs

1/*
2 * Copyright (c) 2025 Jasmine Tai. All rights reserved.
3 */
4
5use std::io::{self, Write};
6
7use termion::style::Reset;
8use termion::{clear, cursor};
9
10use crate::Style;
11use crate::element::Element;
12
13/// A chunk of text with a constant style to be rendered.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct RenderChunk<'s> {
16    /// The content of this chunk.
17    pub(crate) value: &'s str,
18    /// The width of this chunk.
19    pub(crate) width: usize,
20    /// The style of this chunk.
21    pub(crate) style: Style,
22    /// Whether to display the cursor at the start of this chunk. If this is
23    /// true, then `value` must be `""`, `width` must be `0`, and `style` must
24    /// be `Style::EMPTY`.
25    pub(crate) cursor: bool,
26}
27
28impl<'s> RenderChunk<'s> {
29    pub const CURSOR: RenderChunk<'static> = RenderChunk {
30        value: "",
31        width: 0,
32        style: Style::EMPTY,
33        cursor: true,
34    };
35
36    pub fn new(value: &'s str, style: Style) -> Self {
37        RenderChunk::with_known_width(value, crate::width(value), style)
38    }
39
40    pub(crate) fn with_known_width(value: &'s str, width: usize, style: Style) -> Self {
41        debug_assert_eq!(crate::width(value), width);
42        RenderChunk {
43            value,
44            width,
45            style,
46            cursor: false,
47        }
48    }
49}
50
51impl<'s> From<&'s str> for RenderChunk<'s> {
52    fn from(value: &'s str) -> Self {
53        RenderChunk::new(value, Style::EMPTY)
54    }
55}
56
57/// A struct that outputs lines to a [writer](Write).
58pub struct Renderer<W: Write> {
59    pub(crate) writer: W,
60    lines_rendered: u16,
61    desired_cursor: Option<(u16, u16)>,
62    is_dirty: bool, // flag for debugging
63}
64
65impl<W: Write> Renderer<W> {
66    /// Creates a new [`Renderer`] that writes to the given writer.
67    pub fn new(writer: W) -> Self {
68        Renderer {
69            writer,
70            lines_rendered: 0,
71            desired_cursor: None,
72            is_dirty: false,
73        }
74    }
75
76    /// Resets the renderer's state.
77    fn reset_state(&mut self) {
78        self.lines_rendered = 0;
79        self.desired_cursor = None;
80        self.is_dirty = false;
81    }
82
83    /// Resets the cursor position, allowing rendering to start over.
84    pub fn reset(&mut self) -> io::Result<&mut Self> {
85        assert!(!self.is_dirty, "finalize() must be called after rendering");
86        // Reset the cursor to the top-left.
87        let current_cursor_line = match self.desired_cursor {
88            // If there's a desired cursor position, the cursor is there.
89            Some((line, _)) => line,
90            // Otherwise, it's the last line rendered.
91            None => self.lines_rendered.saturating_sub(1),
92        };
93        if current_cursor_line != 0 {
94            write!(self.writer, "{}", cursor::Up(current_cursor_line))?;
95        }
96        write!(self.writer, "\r")?;
97
98        self.reset_state();
99        Ok(self)
100    }
101
102    /// Clears the UI, resetting the terminal back to its initial state.
103    ///
104    /// Note that this method is automatically called when the `Renderer` is
105    /// [dropped](Drop).
106    pub fn clear(&mut self) -> io::Result<()> {
107        assert!(!self.is_dirty, "finalize() must be called after rendering");
108        self.reset()?;
109        write!(self.writer, "{}{}", clear::AfterCursor, cursor::Show)
110    }
111
112    /// Renders a line.
113    pub fn render<E: Element>(&mut self, line: E) -> io::Result<&mut Self> {
114        self.is_dirty = true;
115        // If this isn't the first line, then move to the next line.
116        if self.lines_rendered != 0 {
117            write!(self.writer, "\n\r")?;
118        }
119        // Render each chunk.
120        let mut column = 0;
121        for chunk in line.render() {
122            if chunk.cursor {
123                debug_assert_eq!(chunk.value, "");
124                debug_assert_eq!(chunk.width, 0);
125                self.desired_cursor = Some((self.lines_rendered, column as u16));
126            } else {
127                write!(self.writer, "{}{}{Reset}", chunk.style, chunk.value)?;
128                column += chunk.width;
129            }
130        }
131        write!(self.writer, "{}", clear::UntilNewline)?;
132        self.lines_rendered += 1;
133        Ok(self)
134    }
135
136    /// Finishes rendering. This should be called immediately after the
137    /// [`render`](Self::render) calls are complete.
138    pub fn finish(&mut self) -> io::Result<()> {
139        self.is_dirty = false;
140        if let Some((line, column)) = self.desired_cursor {
141            let up = self.lines_rendered - line - 1;
142            if up != 0 {
143                write!(self.writer, "{}", cursor::Up(up))?;
144            }
145            write!(self.writer, "\r")?;
146            if column != 0 {
147                write!(self.writer, "{}", cursor::Right(column))?;
148            }
149            write!(self.writer, "{}", cursor::Show)?;
150        } else {
151            write!(self.writer, "{}", cursor::Hide)?;
152        }
153        self.writer.flush()
154    }
155
156    /// Leaves the currently-rendered text, making it impossible to clear.
157    ///
158    /// This method may be used if you want to dispose of this `Renderer`
159    /// without clearing the currently-rendered text. This should be called
160    /// after [`finish`](Self::finish).
161    pub fn leave(&mut self) -> io::Result<()> {
162        assert!(!self.is_dirty, "finalize() must be called after rendering");
163        if self.lines_rendered == 0 {
164            return Ok(());
165        }
166        let down = match self.desired_cursor {
167            Some((row, _)) => self.lines_rendered - row - 1,
168            None => 0,
169        };
170        if down != 0 {
171            write!(self.writer, "{}", cursor::Down(down))?;
172        }
173        write!(self.writer, "\n\r")?;
174        self.reset_state();
175        Ok(())
176    }
177}
178
179impl<W: Write> Drop for Renderer<W> {
180    fn drop(&mut self) {
181        let _ = self.clear();
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use crate::element::{Cursor, IntoElement};
188
189    use super::*;
190
191    #[test]
192    fn empty() -> io::Result<()> {
193        let mut r = Renderer::new(vec![]);
194        for _ in 0..3 {
195            r.writer.clear();
196            r.reset()?.finish()?;
197            assert_eq!(r.writer, b"\r\x1b[?25l");
198        }
199        Ok(())
200    }
201
202    #[test]
203    fn empty_line() -> io::Result<()> {
204        let mut r = Renderer::new(vec![]);
205        for _ in 0..3 {
206            r.writer.clear();
207            r.reset()?.render(())?.finish()?;
208            assert_eq!(r.writer, b"\r\x1b[K\x1b[?25l");
209        }
210        Ok(())
211    }
212
213    #[test]
214    fn one_line() -> io::Result<()> {
215        let mut r = Renderer::new(vec![]);
216        for _ in 0..3 {
217            r.writer.clear();
218            r.reset()?.render("trans rights".into_element())?.finish()?;
219            assert_eq!(r.writer, b"\rtrans rights\x1b[m\x1b[K\x1b[?25l");
220        }
221        Ok(())
222    }
223
224    #[test]
225    fn two_lines() -> io::Result<()> {
226        let mut r = Renderer::new(vec![]);
227        r.reset()?
228            .render("trans rights".into_element())?
229            .render("enby rights".into_element())?
230            .finish()?;
231        assert_eq!(
232            r.writer,
233            b"\rtrans rights\x1b[m\x1b[K\n\renby rights\x1b[m\x1b[K\x1b[?25l",
234        );
235
236        for _ in 0..3 {
237            r.writer.clear();
238            r.reset()?
239                .render("trans rights".into_element())?
240                .render("enby rights".into_element())?
241                .finish()?;
242            assert_eq!(
243                r.writer,
244                b"\x1b[1A\rtrans rights\x1b[m\x1b[K\n\renby rights\x1b[m\x1b[K\x1b[?25l",
245            );
246        }
247        Ok(())
248    }
249
250    #[test]
251    fn drop() {
252        let mut out = vec![];
253        Renderer::new(&mut out);
254        assert_eq!(out, b"\r\x1b[J\x1b[?25h");
255    }
256
257    #[test]
258    fn cursor_at_start_of_last_line() -> io::Result<()> {
259        let mut r = Renderer::new(vec![]);
260        r.reset()?
261            .render("trans rights".into_element())?
262            .render((Cursor, "enby rights".into_element()))?
263            .finish()?;
264        assert_eq!(
265            r.writer,
266            b"\rtrans rights\x1b[m\x1b[K\n\renby rights\x1b[m\x1b[K\r\x1b[?25h",
267        );
268        Ok(())
269    }
270
271    #[test]
272    fn cursor_in_last_line() -> io::Result<()> {
273        let mut r = Renderer::new(vec![]);
274        r.reset()?
275            .render("trans rights".into_element())?
276            .render(("enby ".into_element(), Cursor, "rights".into_element()))?
277            .finish()?;
278        assert_eq!(
279            r.writer,
280            b"\rtrans rights\x1b[m\x1b[K\n\renby \x1b[mrights\x1b[m\x1b[K\r\x1b[5C\x1b[?25h",
281        );
282        Ok(())
283    }
284
285    #[test]
286    fn cursor_in_previous_line() -> io::Result<()> {
287        let mut r = Renderer::new(vec![]);
288        r.reset()?
289            .render(("trans rights".into_element(), Cursor))?
290            .render("enby rights".into_element())?
291            .finish()?;
292        assert_eq!(
293            r.writer,
294            b"\rtrans rights\x1b[m\x1b[K\n\renby rights\x1b[m\x1b[K\x1b[1A\r\x1b[12C\x1b[?25h",
295        );
296        Ok(())
297    }
298
299    #[test]
300    fn leave_empty() -> io::Result<()> {
301        let mut r = Renderer::new(vec![]);
302        r.reset()?.finish()?;
303        r.writer.clear();
304        r.leave()?;
305        assert_eq!(r.writer, b"");
306        Ok(())
307    }
308
309    #[test]
310    fn leave() -> io::Result<()> {
311        let mut r = Renderer::new(vec![]);
312        r.reset()?
313            .render("trans rights".into_element())?
314            .render("enby rights".into_element())?
315            .finish()?;
316        r.writer.clear();
317        r.leave()?;
318        r.clear()?;
319        assert_eq!(r.writer, b"\n\r\r\x1b[J\x1b[?25h");
320        Ok(())
321    }
322
323    #[test]
324    fn leave_with_cursor() -> io::Result<()> {
325        let mut r = Renderer::new(vec![]);
326        r.reset()?
327            .render(("trans rights".into_element(), Cursor))?
328            .render("enby rights".into_element())?
329            .finish()?;
330        r.writer.clear();
331        r.leave()?;
332        r.clear()?;
333        assert_eq!(r.writer, b"\x1b[1B\n\r\r\x1b[J\x1b[?25h");
334        Ok(())
335    }
336}