Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(terminal): add Terminal::try_draw() method #1209

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 118 additions & 14 deletions src/terminal/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,39 +234,143 @@ where
Ok(())
}

/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
/// and prepares for the next draw call.
/// Draws a single frame to the terminal.
///
/// This is the main entry point for drawing to the terminal.
/// Returns a [`CompletedFrame`] if successful, otherwise a [`std::io::Error`].
joshka marked this conversation as resolved.
Show resolved Hide resolved
///
/// The changes drawn to the frame are applied only to the current [`Buffer`]. After the closure
/// returns, the current buffer is compared to the previous buffer and only the changes are
/// applied to the terminal.
/// If the render function passed to this method can fail, use [`try_draw`] instead.
///
/// Applications should call `draw` or [`try_draw`] in a loop to continuously render the
/// terminal. These methods are the main entry points for drawing to the terminal.
///
/// [`try_draw`]: Terminal::try_draw
///
/// This method will:
///
/// - autoresize the terminal if necessary
/// - call the rendering parameter, passing it a [`Frame`] reference to render to
/// - flush the current internal state by copying the current buffer to the backend
/// - move the cursor to the last known position if it was set during the rendering closure
/// - return a [`CompletedFrame`] with the current buffer and the area of the terminal
///
/// The [`CompletedFrame`] returned by this method can be useful for debugging or testing
/// purposes, but it is often not used in regular applicationss.
///
/// The render function should fully render the entire frame when called, including areas that
/// are unchanged from the previous frame. This is because each frame is compared to the
/// previous frame to determine what has changed, and only the changes are written to the
/// terminal. If the render function does not fully render the frame, the terminal will not be
/// in a consistent state.
///
/// # Examples
///
/// ```rust,no_run
/// # use std::io::stdout;
/// # use ratatui::{prelude::*, widgets::Paragraph};
/// let backend = CrosstermBackend::new(stdout());
/// let mut terminal = Terminal::new(backend)?;
/// ```
/// # let backend = ratatui::backend::TestBackend::new(10, 10);
/// # let mut terminal = ratatui::Terminal::new(backend)?;
/// use std::io;
///
/// use ratatui::widgets::Paragraph;
///
/// // with a closure
/// terminal.draw(|frame| {
/// let area = frame.size();
/// frame.render_widget(Paragraph::new("Hello World!"), area);
/// frame.set_cursor(0, 0);
/// })?;
/// # std::io::Result::Ok(())
///
/// // or with a function
/// terminal.draw(render)?;
///
/// fn render(frame: &mut ratatui::Frame) {
/// frame.render_widget(Paragraph::new("Hello World!"), frame.size());
/// }
/// # io::Result::Ok(())
/// ```
pub fn draw<F>(&mut self, f: F) -> io::Result<CompletedFrame>
pub fn draw<F>(&mut self, render: F) -> io::Result<CompletedFrame>
where
F: FnOnce(&mut Frame),
{
self.try_draw(|frame| {
render(frame);
joshka marked this conversation as resolved.
Show resolved Hide resolved
io::Result::Ok(())
})
}

/// Tries to draw a single frame to the terminal.
///
/// Returns [`Result::Ok`] containing a [`CompletedFrame`] if successful, otherwise
/// [`Result::Err`] containing the [`std::io::Error`] that caused the failure.
///
/// This is the equivalent of [`Terminal::draw`] but accepts a function or closure that returns
/// a `Result` instead of nothing.
EdJoPaTo marked this conversation as resolved.
Show resolved Hide resolved
///
/// Applications should call `try_draw` or [`draw`] in a loop to continuously render the
/// terminal. These methods are the main entry points for drawing to the terminal.
///
/// [`draw`]: Terminal::draw
///
/// This method will:
///
/// - autoresize the terminal if necessary
/// - call the rendering parameter, passing it a [`Frame`] reference to render to
/// - flush the current internal state by copying the current buffer to the backend
/// - move the cursor to the last known position if it was set during the rendering closure
/// - return a [`CompletedFrame`] with the current buffer and the area of the terminal
///
/// The render function passed to `try_draw` can return any a [`Result`] with an error type that
/// can be converted into an [`std::io::Error`] using the [`Into`] trait. This makes it possible
/// to use the `?` operator to propagate errors that occur during rendering. If the render
/// function returns an error, the error will be returned from `try_draw` as an
/// [`std::io::Error`] and the terminal will not be updated.
///
/// The [`CompletedFrame`] returned by this method can be useful for debugging or testing
/// purposes, but it is often not used in regular applicationss.
///
/// The render function should fully render the entire frame when called, including areas that
/// are unchanged from the previous frame. This is because each frame is compared to the
/// previous frame to determine what has changed, and only the changes are written to the
/// terminal. If the render function does not fully render the frame, the terminal will not be
/// in a consistent state.
///
/// # Examples
///
/// ```should_panic
/// # let backend = ratatui::backend::TestBackend::new(10, 10);
/// # let mut terminal = ratatui::Terminal::new(backend)?;
/// use std::io;
///
/// use ratatui::widgets::Paragraph;
///
/// // with a closure
/// terminal.try_draw(|frame| {
/// let value: u8 = "not a number".parse().map_err(io::Error::other)?;
/// let area = frame.size();
/// frame.render_widget(Paragraph::new("Hello World!"), area);
/// frame.set_cursor(0, 0);
/// io::Result::Ok(())
/// })?;
///
/// // or with a function
/// terminal.try_draw(render)?;
///
Comment on lines +353 to +355
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either empty lines between both or no empty lines. It took me a moment to realise where render is defined as the comment looked like its somewhere completely else.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you looking at the rendered docs?

image

/// fn render(frame: &mut ratatui::Frame) -> io::Result<()> {
/// let value: u8 = "not a number".parse().map_err(io::Error::other)?;
/// frame.render_widget(Paragraph::new("Hello World!"), frame.size());
/// Ok(())
/// }
/// # io::Result::Ok(())
/// ```
pub fn try_draw<F, E>(&mut self, render: F) -> io::Result<CompletedFrame>
where
F: FnOnce(&mut Frame) -> Result<(), E>,
E: Into<io::Error>,
kdheepak marked this conversation as resolved.
Show resolved Hide resolved
{
// 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);
render(&mut frame).map_err(Into::into)?;
// 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
// Buffer. Thus, we're taking the important data out of the Frame and dropping it.
Expand Down