tui_react/
terminal.rs

1//! Derived from TUI-rs, license: MIT, Copyright (c) 2016 Florian Dehau
2use log::error;
3use std::{borrow::Borrow, io};
4
5use tui::{backend::Backend, buffer::Buffer, layout::Rect};
6
7/// A component meant to be rendered by `Terminal::render(...)`.
8/// All other components don't have to implement this trait, and instead
9/// provide a render method by convention, tuned towards their needs using whichever
10/// generic types or lifetimes they need.
11pub trait ToplevelComponent {
12    type Props;
13
14    fn render(&mut self, props: impl Borrow<Self::Props>, area: Rect, buf: &mut Buffer);
15}
16
17#[derive(Debug)]
18pub struct Terminal<B>
19where
20    B: Backend,
21{
22    pub backend: B,
23    buffers: [Buffer; 2],
24    current: usize,
25    hidden_cursor: bool,
26    known_size: Rect,
27}
28
29impl<B> Drop for Terminal<B>
30where
31    B: Backend,
32{
33    fn drop(&mut self) {
34        // Attempt to restore the cursor state
35        if self.hidden_cursor {
36            if let Err(err) = self.show_cursor() {
37                error!("Failed to show the cursor: {}", err);
38            }
39        }
40    }
41}
42
43impl<B> Terminal<B>
44where
45    B: Backend,
46{
47    pub fn new(backend: B) -> io::Result<Terminal<B>> {
48        let size = backend.size()?;
49        Ok(Terminal {
50            backend,
51            buffers: [Buffer::empty(size), Buffer::empty(size)],
52            current: 0,
53            hidden_cursor: false,
54            known_size: size,
55        })
56    }
57
58    pub fn current_buffer_mut(&mut self) -> &mut Buffer {
59        &mut self.buffers[self.current]
60    }
61
62    pub fn reconcile_and_flush(&mut self) -> io::Result<()> {
63        let previous_buffer = &self.buffers[1 - self.current];
64        let current_buffer = &self.buffers[self.current];
65        let updates = previous_buffer.diff(current_buffer);
66        self.backend.draw(updates.into_iter())
67    }
68
69    pub fn resize(&mut self, area: Rect) -> io::Result<()> {
70        self.buffers[self.current].resize(area);
71        self.buffers[1 - self.current].reset();
72        self.buffers[1 - self.current].resize(area);
73        self.known_size = area;
74        self.backend.clear()
75    }
76
77    pub fn autoresize(&mut self) -> io::Result<()> {
78        let size = self.size()?;
79        if self.known_size != size {
80            self.resize(size)?;
81        }
82        Ok(())
83    }
84
85    /// Get ready for rendering and return the maximum display size as `Rect`
86    pub fn pre_render(&mut self) -> io::Result<Rect> {
87        // Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
88        // and the terminal (if growing), which may OOB.
89        self.autoresize()?;
90        Ok(self.known_size)
91    }
92
93    pub fn post_render(&mut self) -> io::Result<()> {
94        self.reconcile_and_flush()?;
95
96        self.buffers[1 - self.current].reset();
97        self.current = 1 - self.current;
98
99        self.backend.flush()?;
100        Ok(())
101    }
102
103    pub fn render<C>(&mut self, component: &mut C, props: impl Borrow<C::Props>) -> io::Result<()>
104    where
105        C: ToplevelComponent,
106    {
107        self.pre_render()?;
108        component.render(props, self.known_size, self.current_buffer_mut());
109        self.post_render()
110    }
111
112    pub fn hide_cursor(&mut self) -> io::Result<()> {
113        self.backend.hide_cursor()?;
114        self.hidden_cursor = true;
115        Ok(())
116    }
117    pub fn show_cursor(&mut self) -> io::Result<()> {
118        self.backend.show_cursor()?;
119        self.hidden_cursor = false;
120        Ok(())
121    }
122    pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
123        self.backend.get_cursor()
124    }
125    pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
126        self.backend.set_cursor(x, y)
127    }
128    pub fn clear(&mut self) -> io::Result<()> {
129        self.backend.clear()
130    }
131    pub fn size(&self) -> io::Result<Rect> {
132        self.backend.size()
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use tui::backend::TestBackend;
140
141    #[expect(dead_code)]
142    #[derive(Default, Clone)]
143    struct ComplexProps {
144        x: usize,
145        y: String,
146    }
147
148    #[derive(Default)]
149    struct StatefulComponent {
150        x: usize,
151    }
152
153    #[derive(Default)]
154    struct StatelessComponent;
155
156    impl ToplevelComponent for StatefulComponent {
157        type Props = usize;
158
159        fn render(&mut self, props: impl Borrow<Self::Props>, _area: Rect, _buf: &mut Buffer) {
160            self.x += *props.borrow();
161        }
162    }
163
164    impl ToplevelComponent for StatelessComponent {
165        type Props = ComplexProps;
166        fn render(&mut self, _props: impl Borrow<Self::Props>, _area: Rect, _buf: &mut Buffer) {
167            // does not matter - we want to see it compiles essentially
168        }
169    }
170
171    #[test]
172    fn it_does_render_with_simple_and_complex_props() {
173        let mut term = Terminal::new(TestBackend::new(20, 20)).unwrap();
174        let mut c = StatefulComponent::default();
175
176        term.render(&mut c, 3usize).ok();
177        assert_eq!(c.x, 3);
178
179        let mut c = StatelessComponent;
180        term.render(&mut c, ComplexProps::default()).ok();
181    }
182}