diff options
Diffstat (limited to 'src/tui/terminal.rs')
| -rw-r--r-- | src/tui/terminal.rs | 321 |
1 files changed, 321 insertions, 0 deletions
diff --git a/src/tui/terminal.rs b/src/tui/terminal.rs new file mode 100644 index 00000000..e64690e9 --- /dev/null +++ b/src/tui/terminal.rs @@ -0,0 +1,321 @@ +use crate::tui::{ + backend::Backend, + buffer::Buffer, + layout::Rect, + widgets::{StatefulWidget, Widget}, +}; +use std::io; + +#[derive(Debug, Clone, PartialEq)] +/// UNSTABLE +enum ResizeBehavior { + Fixed, + Auto, +} + +#[derive(Debug, Clone, PartialEq)] +/// UNSTABLE +pub struct Viewport { + area: Rect, + resize_behavior: ResizeBehavior, +} + +impl Viewport { + /// UNSTABLE + pub fn fixed(area: Rect) -> Viewport { + Viewport { + area, + resize_behavior: ResizeBehavior::Fixed, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +/// Options to pass to [`Terminal::with_options`] +pub struct TerminalOptions { + /// Viewport used to draw to the terminal + pub viewport: Viewport, +} + +/// Interface to the terminal backed by Termion +#[derive(Debug)] +pub struct Terminal<B> +where + B: Backend, +{ + backend: B, + /// Holds the results of the current and previous draw calls. The two are compared at the end + /// of each draw pass to output the necessary updates to the terminal + buffers: [Buffer; 2], + /// Index of the current buffer in the previous array + current: usize, + /// Whether the cursor is currently hidden + hidden_cursor: bool, + /// Viewport + viewport: Viewport, +} + +/// Represents a consistent terminal interface for rendering. +pub struct Frame<'a, B: 'a + Backend> { + terminal: &'a mut Terminal<B>, + + /// Where should the cursor be after drawing this frame? + /// + /// If `None`, the cursor is hidden and its position is controlled by the backend. If `Some((x, + /// y))`, the cursor is shown and placed at `(x, y)` after the call to `Terminal::draw()`. + cursor_position: Option<(u16, u16)>, +} + +impl<'a, B> Frame<'a, B> +where + B: Backend, +{ + /// Terminal size, guaranteed not to change when rendering. + pub fn size(&self) -> Rect { + self.terminal.viewport.area + } + + /// Render a [`Widget`] to the current buffer using [`Widget::render`]. + /// + /// # Examples + /// + /// ```rust + /// # use tui::Terminal; + /// # use tui::backend::TestBackend; + /// # use tui::layout::Rect; + /// # use tui::widgets::Block; + /// # let backend = TestBackend::new(5, 5); + /// # let mut terminal = Terminal::new(backend).unwrap(); + /// let block = Block::default(); + /// let area = Rect::new(0, 0, 5, 5); + /// let mut frame = terminal.get_frame(); + /// frame.render_widget(block, area); + /// ``` + pub fn render_widget<W>(&mut self, widget: W, area: Rect) + where + W: Widget, + { + widget.render(area, self.terminal.current_buffer_mut()); + } + + /// Render a [`StatefulWidget`] to the current buffer using [`StatefulWidget::render`]. + /// + /// The last argument should be an instance of the [`StatefulWidget::State`] associated to the + /// given [`StatefulWidget`]. + /// + /// # Examples + /// + /// ```rust + /// # use tui::Terminal; + /// # use tui::backend::TestBackend; + /// # use tui::layout::Rect; + /// # use tui::widgets::{List, ListItem, ListState}; + /// # let backend = TestBackend::new(5, 5); + /// # let mut terminal = Terminal::new(backend).unwrap(); + /// let mut state = ListState::default(); + /// state.select(Some(1)); + /// let items = vec![ + /// ListItem::new("Item 1"), + /// ListItem::new("Item 2"), + /// ]; + /// let list = List::new(items); + /// let area = Rect::new(0, 0, 5, 5); + /// let mut frame = terminal.get_frame(); + /// frame.render_stateful_widget(list, area, &mut state); + /// ``` + pub fn render_stateful_widget<W>(&mut self, widget: W, area: Rect, state: &mut W::State) + where + W: StatefulWidget, + { + widget.render(area, self.terminal.current_buffer_mut(), state); + } + + /// After drawing this frame, make the cursor visible and put it at the specified (x, y) + /// coordinates. If this method is not called, the cursor will be hidden. + /// + /// Note that this will interfere with calls to `Terminal::hide_cursor()`, + /// `Terminal::show_cursor()`, and `Terminal::set_cursor()`. Pick one of the APIs and stick + /// with it. + pub fn set_cursor(&mut self, x: u16, y: u16) { + self.cursor_position = Some((x, y)); + } +} + +/// `CompletedFrame` represents the state of the terminal after all changes performed in the last +/// [`Terminal::draw`] call have been applied. Therefore, it is only valid until the next call to +/// [`Terminal::draw`]. +pub struct CompletedFrame<'a> { + pub buffer: &'a Buffer, + pub area: Rect, +} + +impl<B> Drop for Terminal<B> +where + B: Backend, +{ + fn drop(&mut self) { + // Attempt to restore the cursor state + if self.hidden_cursor { + if let Err(err) = self.show_cursor() { + eprintln!("Failed to show the cursor: {err}"); + } + } + } +} + +impl<B> Terminal<B> +where + B: Backend, +{ + /// Wrapper around Terminal initialization. Each buffer is initialized with a blank string and + /// default colors for the foreground and the background + pub fn new(backend: B) -> io::Result<Terminal<B>> { + let size = backend.size()?; + Ok(Terminal::with_options( + backend, + TerminalOptions { + viewport: Viewport { + area: size, + resize_behavior: ResizeBehavior::Auto, + }, + }, + )) + } + + /// UNSTABLE + pub fn with_options(backend: B, options: TerminalOptions) -> Terminal<B> { + Terminal { + backend, + buffers: [ + Buffer::empty(options.viewport.area), + Buffer::empty(options.viewport.area), + ], + current: 0, + hidden_cursor: false, + viewport: options.viewport, + } + } + + /// Get a Frame object which provides a consistent view into the terminal state for rendering. + pub fn get_frame(&mut self) -> Frame<B> { + Frame { + terminal: self, + cursor_position: None, + } + } + + pub fn current_buffer_mut(&mut self) -> &mut Buffer { + &mut self.buffers[self.current] + } + + pub fn backend(&self) -> &B { + &self.backend + } + + pub fn backend_mut(&mut self) -> &mut B { + &mut self.backend + } + + /// Obtains a difference between the previous and the current buffer and passes it to the + /// current backend for drawing. + pub fn flush(&mut self) -> io::Result<()> { + let previous_buffer = &self.buffers[1 - self.current]; + let current_buffer = &self.buffers[self.current]; + let updates = previous_buffer.diff(current_buffer); + self.backend.draw(updates.into_iter()) + } + + /// Updates the Terminal so that internal buffers match the requested size. Requested size will + /// be saved so the size can remain consistent when rendering. + /// This leads to a full clear of the screen. + pub fn resize(&mut self, area: Rect) -> io::Result<()> { + self.buffers[self.current].resize(area); + self.buffers[1 - self.current].resize(area); + self.viewport.area = area; + self.clear() + } + + /// Queries the backend for size and resizes if it doesn't match the previous size. + pub fn autoresize(&mut self) -> io::Result<()> { + if self.viewport.resize_behavior == ResizeBehavior::Auto { + let size = self.size()?; + if size != self.viewport.area { + self.resize(size)?; + } + }; + Ok(()) + } + + /// Synchronizes terminal size, calls the rendering closure, flushes the current internal state + /// and prepares for the next draw call. + pub fn draw<F>(&mut self, f: F) -> io::Result<CompletedFrame> + where + F: FnOnce(&mut Frame<B>), + { + // Autoresize - otherwise we get glitches if shrinking or potential desync between widgets + // and the terminal (if growing), which may OOB. + self.autoresize()?; + + let mut frame = self.get_frame(); + f(&mut frame); + // We can't change the cursor position right away because we have to flush the frame to + // stdout first. But we also can't keep the frame around, since it holds a &mut to + // Terminal. Thus, we're taking the important data out of the Frame and dropping it. + let cursor_position = frame.cursor_position; + + // Draw to stdout + self.flush()?; + + match cursor_position { + None => self.hide_cursor()?, + Some((x, y)) => { + self.show_cursor()?; + self.set_cursor(x, y)?; + } + } + + // Swap buffers + self.buffers[1 - self.current].reset(); + self.current = 1 - self.current; + + // Flush + self.backend.flush()?; + Ok(CompletedFrame { + buffer: &self.buffers[1 - self.current], + area: self.viewport.area, + }) + } + + pub fn hide_cursor(&mut self) -> io::Result<()> { + self.backend.hide_cursor()?; + self.hidden_cursor = true; + Ok(()) + } + + pub fn show_cursor(&mut self) -> io::Result<()> { + self.backend.show_cursor()?; + self.hidden_cursor = false; + Ok(()) + } + + pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> { + self.backend.get_cursor() + } + + pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> { + self.backend.set_cursor(x, y) + } + + /// Clear the terminal and force a full redraw on the next draw call. + pub fn clear(&mut self) -> io::Result<()> { + self.backend.clear()?; + // Reset the back buffer to make sure the next update will redraw everything. + self.buffers[1 - self.current].reset(); + Ok(()) + } + + /// Queries the real size of the backend. + pub fn size(&self) -> io::Result<Rect> { + self.backend.size() + } +} |
