tui/terminal.rs
1use crate::{
2 backend::Backend,
3 buffer::Buffer,
4 layout::Rect,
5 widgets::{StatefulWidget, Widget},
6};
7use std::io;
8
9#[derive(Debug, Clone, PartialEq)]
10/// UNSTABLE
11enum ResizeBehavior {
12 Fixed,
13 Auto,
14}
15
16#[derive(Debug, Clone, PartialEq)]
17/// UNSTABLE
18pub struct Viewport {
19 area: Rect,
20 resize_behavior: ResizeBehavior,
21}
22
23impl Viewport {
24 /// UNSTABLE
25 pub fn fixed(area: Rect) -> Viewport {
26 Viewport {
27 area,
28 resize_behavior: ResizeBehavior::Fixed,
29 }
30 }
31}
32
33#[derive(Debug, Clone, PartialEq)]
34/// Options to pass to [`Terminal::with_options`]
35pub struct TerminalOptions {
36 /// Viewport used to draw to the terminal
37 pub viewport: Viewport,
38}
39
40/// Interface to the terminal backed by Termion
41#[derive(Debug)]
42pub struct Terminal<B>
43where
44 B: Backend,
45{
46 backend: B,
47 /// Holds the results of the current and previous draw calls. The two are compared at the end
48 /// of each draw pass to output the necessary updates to the terminal
49 buffers: [Buffer; 2],
50 /// Index of the current buffer in the previous array
51 current: usize,
52 /// Whether the cursor is currently hidden
53 hidden_cursor: bool,
54 /// Viewport
55 viewport: Viewport,
56}
57
58/// Represents a consistent terminal interface for rendering.
59pub struct Frame<'a, B: 'a>
60where
61 B: Backend,
62{
63 terminal: &'a mut Terminal<B>,
64
65 /// Where should the cursor be after drawing this frame?
66 ///
67 /// If `None`, the cursor is hidden and its position is controlled by the backend. If `Some((x,
68 /// y))`, the cursor is shown and placed at `(x, y)` after the call to `Terminal::draw()`.
69 cursor_position: Option<(u16, u16)>,
70}
71
72impl<'a, B> Frame<'a, B>
73where
74 B: Backend,
75{
76 /// Terminal size, guaranteed not to change when rendering.
77 pub fn size(&self) -> Rect {
78 self.terminal.viewport.area
79 }
80
81 /// Render a [`Widget`] to the current buffer using [`Widget::render`].
82 ///
83 /// # Examples
84 ///
85 /// ```rust
86 /// # use tui::Terminal;
87 /// # use tui::backend::TestBackend;
88 /// # use tui::layout::Rect;
89 /// # use tui::widgets::Block;
90 /// # let backend = TestBackend::new(5, 5);
91 /// # let mut terminal = Terminal::new(backend).unwrap();
92 /// let block = Block::default();
93 /// let area = Rect::new(0, 0, 5, 5);
94 /// let mut frame = terminal.get_frame();
95 /// frame.render_widget(block, area);
96 /// ```
97 pub fn render_widget<W>(&mut self, widget: W, area: Rect)
98 where
99 W: Widget,
100 {
101 widget.render(area, self.terminal.current_buffer_mut());
102 }
103
104 /// Render a [`StatefulWidget`] to the current buffer using [`StatefulWidget::render`].
105 ///
106 /// The last argument should be an instance of the [`StatefulWidget::State`] associated to the
107 /// given [`StatefulWidget`].
108 ///
109 /// # Examples
110 ///
111 /// ```rust
112 /// # use tui::Terminal;
113 /// # use tui::backend::TestBackend;
114 /// # use tui::layout::Rect;
115 /// # use tui::widgets::{List, ListItem, ListState};
116 /// # let backend = TestBackend::new(5, 5);
117 /// # let mut terminal = Terminal::new(backend).unwrap();
118 /// let mut state = ListState::default();
119 /// state.select(Some(1));
120 /// let items = vec![
121 /// ListItem::new("Item 1"),
122 /// ListItem::new("Item 2"),
123 /// ];
124 /// let list = List::new(items);
125 /// let area = Rect::new(0, 0, 5, 5);
126 /// let mut frame = terminal.get_frame();
127 /// frame.render_stateful_widget(list, area, &mut state);
128 /// ```
129 pub fn render_stateful_widget<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
130 where
131 W: StatefulWidget,
132 {
133 widget.render(area, self.terminal.current_buffer_mut(), state);
134 }
135
136 /// After drawing this frame, make the cursor visible and put it at the specified (x, y)
137 /// coordinates. If this method is not called, the cursor will be hidden.
138 ///
139 /// Note that this will interfere with calls to `Terminal::hide_cursor()`,
140 /// `Terminal::show_cursor()`, and `Terminal::set_cursor()`. Pick one of the APIs and stick
141 /// with it.
142 pub fn set_cursor(&mut self, x: u16, y: u16) {
143 self.cursor_position = Some((x, y));
144 }
145}
146
147/// CompletedFrame represents the state of the terminal after all changes performed in the last
148/// [`Terminal::draw`] call have been applied. Therefore, it is only valid until the next call to
149/// [`Terminal::draw`].
150pub struct CompletedFrame<'a> {
151 pub buffer: &'a Buffer,
152 pub area: Rect,
153}
154
155impl<B> Drop for Terminal<B>
156where
157 B: Backend,
158{
159 fn drop(&mut self) {
160 // Attempt to restore the cursor state
161 if self.hidden_cursor {
162 if let Err(err) = self.show_cursor() {
163 eprintln!("Failed to show the cursor: {}", err);
164 }
165 }
166 }
167}
168
169impl<B> Terminal<B>
170where
171 B: Backend,
172{
173 /// Wrapper around Terminal initialization. Each buffer is initialized with a blank string and
174 /// default colors for the foreground and the background
175 pub fn new(backend: B) -> io::Result<Terminal<B>> {
176 let size = backend.size()?;
177 Terminal::with_options(
178 backend,
179 TerminalOptions {
180 viewport: Viewport {
181 area: size,
182 resize_behavior: ResizeBehavior::Auto,
183 },
184 },
185 )
186 }
187
188 /// UNSTABLE
189 pub fn with_options(backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
190 Ok(Terminal {
191 backend,
192 buffers: [
193 Buffer::empty(options.viewport.area),
194 Buffer::empty(options.viewport.area),
195 ],
196 current: 0,
197 hidden_cursor: false,
198 viewport: options.viewport,
199 })
200 }
201
202 /// Get a Frame object which provides a consistent view into the terminal state for rendering.
203 pub fn get_frame(&mut self) -> Frame<B> {
204 Frame {
205 terminal: self,
206 cursor_position: None,
207 }
208 }
209
210 pub fn current_buffer_mut(&mut self) -> &mut Buffer {
211 &mut self.buffers[self.current]
212 }
213
214 pub fn backend(&self) -> &B {
215 &self.backend
216 }
217
218 pub fn backend_mut(&mut self) -> &mut B {
219 &mut self.backend
220 }
221
222 /// Obtains a difference between the previous and the current buffer and passes it to the
223 /// current backend for drawing.
224 pub fn flush(&mut self) -> io::Result<()> {
225 let previous_buffer = &self.buffers[1 - self.current];
226 let current_buffer = &self.buffers[self.current];
227 let updates = previous_buffer.diff(current_buffer);
228 self.backend.draw(updates.into_iter())
229 }
230
231 /// Updates the Terminal so that internal buffers match the requested size. Requested size will
232 /// be saved so the size can remain consistent when rendering.
233 /// This leads to a full clear of the screen.
234 pub fn resize(&mut self, area: Rect) -> io::Result<()> {
235 self.buffers[self.current].resize(area);
236 self.buffers[1 - self.current].resize(area);
237 self.viewport.area = area;
238 self.clear()
239 }
240
241 /// Queries the backend for size and resizes if it doesn't match the previous size.
242 pub fn autoresize(&mut self) -> io::Result<()> {
243 if self.viewport.resize_behavior == ResizeBehavior::Auto {
244 let size = self.size()?;
245 if size != self.viewport.area {
246 self.resize(size)?;
247 }
248 };
249 Ok(())
250 }
251
252 /// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
253 /// and prepares for the next draw call.
254 pub fn draw<F>(&mut self, f: F) -> io::Result<CompletedFrame>
255 where
256 F: FnOnce(&mut Frame<B>),
257 {
258 // Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
259 // and the terminal (if growing), which may OOB.
260 self.autoresize()?;
261
262 let mut frame = self.get_frame();
263 f(&mut frame);
264 // We can't change the cursor position right away because we have to flush the frame to
265 // stdout first. But we also can't keep the frame around, since it holds a &mut to
266 // Terminal. Thus, we're taking the important data out of the Frame and dropping it.
267 let cursor_position = frame.cursor_position;
268
269 // Draw to stdout
270 self.flush()?;
271
272 match cursor_position {
273 None => self.hide_cursor()?,
274 Some((x, y)) => {
275 self.show_cursor()?;
276 self.set_cursor(x, y)?;
277 }
278 }
279
280 // Swap buffers
281 self.buffers[1 - self.current].reset();
282 self.current = 1 - self.current;
283
284 // Flush
285 self.backend.flush()?;
286 Ok(CompletedFrame {
287 buffer: &self.buffers[1 - self.current],
288 area: self.viewport.area,
289 })
290 }
291
292 pub fn hide_cursor(&mut self) -> io::Result<()> {
293 self.backend.hide_cursor()?;
294 self.hidden_cursor = true;
295 Ok(())
296 }
297
298 pub fn show_cursor(&mut self) -> io::Result<()> {
299 self.backend.show_cursor()?;
300 self.hidden_cursor = false;
301 Ok(())
302 }
303
304 pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
305 self.backend.get_cursor()
306 }
307
308 pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
309 self.backend.set_cursor(x, y)
310 }
311
312 /// Clear the terminal and force a full redraw on the next draw call.
313 pub fn clear(&mut self) -> io::Result<()> {
314 self.backend.clear()?;
315 // Reset the back buffer to make sure the next update will redraw everything.
316 self.buffers[1 - self.current].reset();
317 Ok(())
318 }
319
320 /// Queries the real size of the backend.
321 pub fn size(&self) -> io::Result<Rect> {
322 self.backend.size()
323 }
324}