ratatui_core/terminal/terminal.rs
1use crate::backend::{Backend, ClearType};
2use crate::buffer::{Buffer, Cell};
3use crate::layout::{Position, Rect, Size};
4use crate::terminal::{CompletedFrame, Frame, TerminalOptions, Viewport};
5
6/// An interface to interact and draw [`Frame`]s on the user's terminal.
7///
8/// This is the main entry point for Ratatui. It is responsible for drawing and maintaining the
9/// state of the buffers, cursor and viewport.
10///
11/// The [`Terminal`] is generic over a [`Backend`] implementation which is used to interface with
12/// the underlying terminal library. The [`Backend`] trait is implemented for three popular Rust
13/// terminal libraries: [Crossterm], [Termion] and [Termwiz]. See the [`backend`] module for more
14/// information.
15///
16/// The `Terminal` struct maintains two buffers: the current and the previous.
17/// When the widgets are drawn, the changes are accumulated in the current buffer.
18/// At the end of each draw pass, the two buffers are compared, and only the changes
19/// between these buffers are written to the terminal, avoiding any redundant operations.
20/// After flushing these changes, the buffers are swapped to prepare for the next draw cycle.
21///
22/// The terminal also has a viewport which is the area of the terminal that is currently visible to
23/// the user. It can be either fullscreen, inline or fixed. See [`Viewport`] for more information.
24///
25/// Applications should detect terminal resizes and call [`Terminal::draw`] to redraw the
26/// application with the new size. This will automatically resize the internal buffers to match the
27/// new size for inline and fullscreen viewports. Fixed viewports are not resized automatically.
28///
29/// # Initialization
30///
31/// For most applications, consider using the convenience functions `ratatui::run()`,
32/// `ratatui::init()`, and `ratatui::restore()` (available since version 0.28.1) along with the
33/// `DefaultTerminal` type alias instead of constructing `Terminal` instances manually. These
34/// functions handle the common setup and teardown tasks automatically. Manual construction
35/// using `Terminal::new()` or `Terminal::with_options()` is still supported for applications
36/// that need fine-grained control over initialization.
37///
38/// # Examples
39///
40/// ## Using convenience functions (recommended for most applications)
41///
42/// ```rust,ignore
43/// // Modern approach using convenience functions
44/// ratatui::run(|terminal| {
45/// terminal.draw(|frame| {
46/// let area = frame.area();
47/// frame.render_widget(Paragraph::new("Hello World!"), area);
48/// })?;
49/// Ok(())
50/// })?;
51/// ```
52///
53/// ## Manual construction (for fine-grained control)
54///
55/// ```rust,ignore
56/// use std::io::stdout;
57///
58/// use ratatui::{backend::CrosstermBackend, widgets::Paragraph, Terminal};
59///
60/// let backend = CrosstermBackend::new(stdout());
61/// let mut terminal = Terminal::new(backend)?;
62/// terminal.draw(|frame| {
63/// let area = frame.area();
64/// frame.render_widget(Paragraph::new("Hello World!"), area);
65/// })?;
66/// # std::io::Result::Ok(())
67/// ```
68///
69/// [Crossterm]: https://crates.io/crates/crossterm
70/// [Termion]: https://crates.io/crates/termion
71/// [Termwiz]: https://crates.io/crates/termwiz
72/// [`backend`]: crate::backend
73/// [`Backend`]: crate::backend::Backend
74/// [`Buffer`]: crate::buffer::Buffer
75#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
76pub struct Terminal<B>
77where
78 B: Backend,
79{
80 /// The backend used to interface with the terminal
81 backend: B,
82 /// Holds the results of the current and previous draw calls. The two are compared at the end
83 /// of each draw pass to output the necessary updates to the terminal
84 buffers: [Buffer; 2],
85 /// Index of the current buffer in the previous array
86 current: usize,
87 /// Whether the cursor is currently hidden
88 hidden_cursor: bool,
89 /// Viewport
90 viewport: Viewport,
91 /// Area of the viewport
92 viewport_area: Rect,
93 /// Last known area of the terminal. Used to detect if the internal buffers have to be resized.
94 last_known_area: Rect,
95 /// Last known position of the cursor. Used to find the new area when the viewport is inlined
96 /// and the terminal resized.
97 last_known_cursor_pos: Position,
98 /// Number of frames rendered up until current time.
99 frame_count: usize,
100}
101
102/// Options to pass to [`Terminal::with_options`]
103#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
104pub struct Options {
105 /// Viewport used to draw to the terminal
106 pub viewport: Viewport,
107}
108
109impl<B> Drop for Terminal<B>
110where
111 B: Backend,
112{
113 fn drop(&mut self) {
114 // Attempt to restore the cursor state
115 if self.hidden_cursor {
116 #[allow(unused_variables)]
117 if let Err(err) = self.show_cursor() {
118 #[cfg(feature = "std")]
119 std::eprintln!("Failed to show the cursor: {err}");
120 }
121 }
122 }
123}
124
125impl<B> Terminal<B>
126where
127 B: Backend,
128{
129 /// Creates a new [`Terminal`] with the given [`Backend`] with a full screen viewport.
130 ///
131 /// Note that unlike `ratatui::init`, this does not install a panic hook, so it is recommended
132 /// to do that manually when using this function, otherwise any panic messages will be printed
133 /// to the alternate screen and the terminal may be left in an unusable state.
134 ///
135 /// See [how to set up panic hooks](https://ratatui.rs/recipes/apps/panic-hooks/) and
136 /// [`better-panic` example](https://ratatui.rs/recipes/apps/better-panic/) for more
137 /// information.
138 ///
139 /// # Example
140 ///
141 /// ```rust,ignore
142 /// use std::io::stdout;
143 ///
144 /// use ratatui::{backend::CrosstermBackend, Terminal};
145 ///
146 /// let backend = CrosstermBackend::new(stdout());
147 /// let terminal = Terminal::new(backend)?;
148 ///
149 /// // Optionally set up a panic hook to restore the terminal on panic.
150 /// let old_hook = std::panic::take_hook();
151 /// std::panic::set_hook(Box::new(move |info| {
152 /// ratatui::restore();
153 /// old_hook(info);
154 /// }));
155 /// # std::io::Result::Ok(())
156 /// ```
157 pub fn new(backend: B) -> Result<Self, B::Error> {
158 Self::with_options(
159 backend,
160 TerminalOptions {
161 viewport: Viewport::Fullscreen,
162 },
163 )
164 }
165
166 /// Creates a new [`Terminal`] with the given [`Backend`] and [`TerminalOptions`].
167 ///
168 /// # Example
169 ///
170 /// ```rust,ignore
171 /// use std::io::stdout;
172 ///
173 /// use ratatui::{backend::CrosstermBackend, layout::Rect, Terminal, TerminalOptions, Viewport};
174 ///
175 /// let backend = CrosstermBackend::new(stdout());
176 /// let viewport = Viewport::Fixed(Rect::new(0, 0, 10, 10));
177 /// let terminal = Terminal::with_options(backend, TerminalOptions { viewport })?;
178 /// # std::io::Result::Ok(())
179 /// ```
180 pub fn with_options(mut backend: B, options: TerminalOptions) -> Result<Self, B::Error> {
181 let area = match options.viewport {
182 Viewport::Fullscreen | Viewport::Inline(_) => backend.size()?.into(),
183 Viewport::Fixed(area) => area,
184 };
185 let (viewport_area, cursor_pos) = match options.viewport {
186 Viewport::Fullscreen => (area, Position::ORIGIN),
187 Viewport::Inline(height) => {
188 compute_inline_size(&mut backend, height, area.as_size(), 0)?
189 }
190 Viewport::Fixed(area) => (area, area.as_position()),
191 };
192 Ok(Self {
193 backend,
194 buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
195 current: 0,
196 hidden_cursor: false,
197 viewport: options.viewport,
198 viewport_area,
199 last_known_area: area,
200 last_known_cursor_pos: cursor_pos,
201 frame_count: 0,
202 })
203 }
204
205 /// Get a Frame object which provides a consistent view into the terminal state for rendering.
206 ///
207 /// # Note
208 ///
209 /// This exists to support more advanced use cases. Most cases should be fine using
210 /// [`Terminal::draw`].
211 ///
212 /// [`Terminal::get_frame`] should be used when you need direct access to the frame buffer
213 /// outside of draw closure, for example:
214 ///
215 /// - Unit testing widgets
216 /// - Buffer state inspection
217 /// - Cursor manipulation
218 /// - Multiple rendering passes/Buffer Manipulation
219 /// - Custom frame lifecycle management
220 /// - Buffer exporting
221 ///
222 /// # Example
223 ///
224 /// Getting the buffer and asserting on some cells after rendering a widget.
225 ///
226 /// ```rust,ignore
227 /// use ratatui::{backend::TestBackend, Terminal};
228 /// use ratatui::widgets::Paragraph;
229 /// let backend = TestBackend::new(30, 5);
230 /// let mut terminal = Terminal::new(backend).unwrap();
231 /// {
232 /// let mut frame = terminal.get_frame();
233 /// frame.render_widget(Paragraph::new("Hello"), frame.area());
234 /// }
235 /// // When not using `draw`, present the buffer manually:
236 /// terminal.flush().unwrap();
237 /// terminal.swap_buffers();
238 /// terminal.backend_mut().flush().unwrap();
239 /// ```
240 pub const fn get_frame(&mut self) -> Frame<'_> {
241 let count = self.frame_count;
242 Frame {
243 cursor_position: None,
244 viewport_area: self.viewport_area,
245 buffer: self.current_buffer_mut(),
246 count,
247 }
248 }
249
250 /// Gets the current buffer as a mutable reference.
251 pub const fn current_buffer_mut(&mut self) -> &mut Buffer {
252 &mut self.buffers[self.current]
253 }
254
255 /// Gets the backend
256 pub const fn backend(&self) -> &B {
257 &self.backend
258 }
259
260 /// Gets the backend as a mutable reference
261 pub const fn backend_mut(&mut self) -> &mut B {
262 &mut self.backend
263 }
264
265 /// Obtains a difference between the previous and the current buffer and passes it to the
266 /// current backend for drawing.
267 pub fn flush(&mut self) -> Result<(), B::Error> {
268 let previous_buffer = &self.buffers[1 - self.current];
269 let current_buffer = &self.buffers[self.current];
270 let updates = previous_buffer.diff(current_buffer);
271 if let Some((col, row, _)) = updates.last() {
272 self.last_known_cursor_pos = Position { x: *col, y: *row };
273 }
274 self.backend.draw(updates.into_iter())
275 }
276
277 /// Updates the Terminal so that internal buffers match the requested area.
278 ///
279 /// Requested area will be saved to remain consistent when rendering. This leads to a full clear
280 /// of the screen.
281 pub fn resize(&mut self, area: Rect) -> Result<(), B::Error> {
282 let next_area = match self.viewport {
283 Viewport::Inline(height) => {
284 let offset_in_previous_viewport = self
285 .last_known_cursor_pos
286 .y
287 .saturating_sub(self.viewport_area.top());
288 compute_inline_size(
289 &mut self.backend,
290 height,
291 area.as_size(),
292 offset_in_previous_viewport,
293 )?
294 .0
295 }
296 Viewport::Fixed(_) | Viewport::Fullscreen => area,
297 };
298 self.set_viewport_area(next_area);
299 self.clear()?;
300
301 self.last_known_area = area;
302 Ok(())
303 }
304
305 fn set_viewport_area(&mut self, area: Rect) {
306 self.buffers[self.current].resize(area);
307 self.buffers[1 - self.current].resize(area);
308 self.viewport_area = area;
309 }
310
311 /// Queries the backend for size and resizes if it doesn't match the previous size.
312 pub fn autoresize(&mut self) -> Result<(), B::Error> {
313 // fixed viewports do not get autoresized
314 if matches!(self.viewport, Viewport::Fullscreen | Viewport::Inline(_)) {
315 let area = self.size()?.into();
316 if area != self.last_known_area {
317 self.resize(area)?;
318 }
319 }
320 Ok(())
321 }
322
323 /// Draws a single frame to the terminal.
324 ///
325 /// Returns a [`CompletedFrame`] if successful, otherwise a [`std::io::Error`].
326 ///
327 /// If the render callback passed to this method can fail, use [`try_draw`] instead.
328 ///
329 /// Applications should call `draw` or [`try_draw`] in a loop to continuously render the
330 /// terminal. These methods are the main entry points for drawing to the terminal.
331 ///
332 /// [`try_draw`]: Terminal::try_draw
333 ///
334 /// This method will:
335 ///
336 /// - autoresize the terminal if necessary
337 /// - call the render callback, passing it a [`Frame`] reference to render to
338 /// - flush the current internal state by copying the current buffer to the backend
339 /// - move the cursor to the last known position if it was set during the rendering closure
340 /// - return a [`CompletedFrame`] with the current buffer and the area of the terminal
341 ///
342 /// The [`CompletedFrame`] returned by this method can be useful for debugging or testing
343 /// purposes, but it is often not used in regular applications.
344 ///
345 /// The render callback should fully render the entire frame when called, including areas that
346 /// are unchanged from the previous frame. This is because each frame is compared to the
347 /// previous frame to determine what has changed, and only the changes are written to the
348 /// terminal. If the render callback does not fully render the frame, the terminal will not be
349 /// in a consistent state.
350 ///
351 /// # Examples
352 ///
353 /// ```rust,ignore
354 /// # let backend = ratatui::backend::TestBackend::new(10, 10);
355 /// # let mut terminal = ratatui::Terminal::new(backend)?;
356 /// use ratatui::{layout::Position, widgets::Paragraph};
357 ///
358 /// // with a closure
359 /// terminal.draw(|frame| {
360 /// let area = frame.area();
361 /// frame.render_widget(Paragraph::new("Hello World!"), area);
362 /// frame.set_cursor_position(Position { x: 0, y: 0 });
363 /// })?;
364 ///
365 /// // or with a function
366 /// terminal.draw(render)?;
367 ///
368 /// fn render(frame: &mut ratatui::Frame) {
369 /// frame.render_widget(Paragraph::new("Hello World!"), frame.area());
370 /// }
371 /// # std::io::Result::Ok(())
372 /// ```
373 pub fn draw<F>(&mut self, render_callback: F) -> Result<CompletedFrame<'_>, B::Error>
374 where
375 F: FnOnce(&mut Frame),
376 {
377 self.try_draw(|frame| {
378 render_callback(frame);
379 Ok::<(), B::Error>(())
380 })
381 }
382
383 /// Tries to draw a single frame to the terminal.
384 ///
385 /// Returns [`Result::Ok`] containing a [`CompletedFrame`] if successful, otherwise
386 /// [`Result::Err`] containing the [`std::io::Error`] that caused the failure.
387 ///
388 /// This is the equivalent of [`Terminal::draw`] but the render callback is a function or
389 /// closure that returns a `Result` instead of nothing.
390 ///
391 /// Applications should call `try_draw` or [`draw`] in a loop to continuously render the
392 /// terminal. These methods are the main entry points for drawing to the terminal.
393 ///
394 /// [`draw`]: Terminal::draw
395 ///
396 /// This method will:
397 ///
398 /// - autoresize the terminal if necessary
399 /// - call the render callback, passing it a [`Frame`] reference to render to
400 /// - flush the current internal state by copying the current buffer to the backend
401 /// - move the cursor to the last known position if it was set during the rendering closure
402 /// - return a [`CompletedFrame`] with the current buffer and the area of the terminal
403 ///
404 /// The render callback passed to `try_draw` can return any [`Result`] with an error type that
405 /// can be converted into an [`std::io::Error`] using the [`Into`] trait. This makes it possible
406 /// to use the `?` operator to propagate errors that occur during rendering. If the render
407 /// callback returns an error, the error will be returned from `try_draw` as an
408 /// [`std::io::Error`] and the terminal will not be updated.
409 ///
410 /// The [`CompletedFrame`] returned by this method can be useful for debugging or testing
411 /// purposes, but it is often not used in regular applications.
412 ///
413 /// The render callback should fully render the entire frame when called, including areas that
414 /// are unchanged from the previous frame. This is because each frame is compared to the
415 /// previous frame to determine what has changed, and only the changes are written to the
416 /// terminal. If the render function does not fully render the frame, the terminal will not be
417 /// in a consistent state.
418 ///
419 /// # Examples
420 ///
421 /// ```ignore
422 /// # use ratatui::layout::Position;;
423 /// # let backend = ratatui::backend::TestBackend::new(10, 10);
424 /// # let mut terminal = ratatui::Terminal::new(backend)?;
425 /// use std::io;
426 ///
427 /// use ratatui::widgets::Paragraph;
428 ///
429 /// // with a closure
430 /// terminal.try_draw(|frame| {
431 /// let value: u8 = "not a number".parse().map_err(io::Error::other)?;
432 /// let area = frame.area();
433 /// frame.render_widget(Paragraph::new("Hello World!"), area);
434 /// frame.set_cursor_position(Position { x: 0, y: 0 });
435 /// io::Result::Ok(())
436 /// })?;
437 ///
438 /// // or with a function
439 /// terminal.try_draw(render)?;
440 ///
441 /// fn render(frame: &mut ratatui::Frame) -> io::Result<()> {
442 /// let value: u8 = "not a number".parse().map_err(io::Error::other)?;
443 /// frame.render_widget(Paragraph::new("Hello World!"), frame.area());
444 /// Ok(())
445 /// }
446 /// # io::Result::Ok(())
447 /// ```
448 pub fn try_draw<F, E>(&mut self, render_callback: F) -> Result<CompletedFrame<'_>, B::Error>
449 where
450 F: FnOnce(&mut Frame) -> Result<(), E>,
451 E: Into<B::Error>,
452 {
453 // Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
454 // and the terminal (if growing), which may OOB.
455 self.autoresize()?;
456
457 let mut frame = self.get_frame();
458
459 render_callback(&mut frame).map_err(Into::into)?;
460
461 // We can't change the cursor position right away because we have to flush the frame to
462 // stdout first. But we also can't keep the frame around, since it holds a &mut to
463 // Buffer. Thus, we're taking the important data out of the Frame and dropping it.
464 let cursor_position = frame.cursor_position;
465
466 // Draw to stdout
467 self.flush()?;
468
469 match cursor_position {
470 None => self.hide_cursor()?,
471 Some(position) => {
472 self.show_cursor()?;
473 self.set_cursor_position(position)?;
474 }
475 }
476
477 self.swap_buffers();
478
479 // Flush
480 self.backend.flush()?;
481
482 let completed_frame = CompletedFrame {
483 buffer: &self.buffers[1 - self.current],
484 area: self.last_known_area,
485 count: self.frame_count,
486 };
487
488 // increment frame count before returning from draw
489 self.frame_count = self.frame_count.wrapping_add(1);
490
491 Ok(completed_frame)
492 }
493
494 /// Hides the cursor.
495 pub fn hide_cursor(&mut self) -> Result<(), B::Error> {
496 self.backend.hide_cursor()?;
497 self.hidden_cursor = true;
498 Ok(())
499 }
500
501 /// Shows the cursor.
502 pub fn show_cursor(&mut self) -> Result<(), B::Error> {
503 self.backend.show_cursor()?;
504 self.hidden_cursor = false;
505 Ok(())
506 }
507
508 /// Gets the current cursor position.
509 ///
510 /// This is the position of the cursor after the last draw call and is returned as a tuple of
511 /// `(x, y)` coordinates.
512 #[deprecated = "use `get_cursor_position()` instead which returns `Result<Position>`"]
513 pub fn get_cursor(&mut self) -> Result<(u16, u16), B::Error> {
514 let Position { x, y } = self.get_cursor_position()?;
515 Ok((x, y))
516 }
517
518 /// Sets the cursor position.
519 #[deprecated = "use `set_cursor_position((x, y))` instead which takes `impl Into<Position>`"]
520 pub fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), B::Error> {
521 self.set_cursor_position(Position { x, y })
522 }
523
524 /// Gets the current cursor position.
525 ///
526 /// This is the position of the cursor after the last draw call.
527 pub fn get_cursor_position(&mut self) -> Result<Position, B::Error> {
528 self.backend.get_cursor_position()
529 }
530
531 /// Sets the cursor position.
532 pub fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> Result<(), B::Error> {
533 let position = position.into();
534 self.backend.set_cursor_position(position)?;
535 self.last_known_cursor_pos = position;
536 Ok(())
537 }
538
539 /// Clear the terminal and force a full redraw on the next draw call.
540 pub fn clear(&mut self) -> Result<(), B::Error> {
541 match self.viewport {
542 Viewport::Fullscreen => self.backend.clear_region(ClearType::All)?,
543 Viewport::Inline(_) => {
544 self.backend
545 .set_cursor_position(self.viewport_area.as_position())?;
546 self.backend.clear_region(ClearType::AfterCursor)?;
547 }
548 Viewport::Fixed(_) => {
549 let area = self.viewport_area;
550 for y in area.top()..area.bottom() {
551 self.backend.set_cursor_position(Position { x: 0, y })?;
552 self.backend.clear_region(ClearType::AfterCursor)?;
553 }
554 }
555 }
556 // Reset the back buffer to make sure the next update will redraw everything.
557 self.buffers[1 - self.current].reset();
558 Ok(())
559 }
560
561 /// Clears the inactive buffer and swaps it with the current buffer
562 pub fn swap_buffers(&mut self) {
563 self.buffers[1 - self.current].reset();
564 self.current = 1 - self.current;
565 }
566
567 /// Queries the real size of the backend.
568 pub fn size(&self) -> Result<Size, B::Error> {
569 self.backend.size()
570 }
571
572 /// Insert some content before the current inline viewport. This has no effect when the
573 /// viewport is not inline.
574 ///
575 /// The `draw_fn` closure will be called to draw into a writable `Buffer` that is `height`
576 /// lines tall. The content of that `Buffer` will then be inserted before the viewport.
577 ///
578 /// If the viewport isn't yet at the bottom of the screen, inserted lines will push it towards
579 /// the bottom. Once the viewport is at the bottom of the screen, inserted lines will scroll
580 /// the area of the screen above the viewport upwards.
581 ///
582 /// Before:
583 /// ```ignore
584 /// +---------------------+
585 /// | pre-existing line 1 |
586 /// | pre-existing line 2 |
587 /// +---------------------+
588 /// | viewport |
589 /// +---------------------+
590 /// | |
591 /// | |
592 /// +---------------------+
593 /// ```
594 ///
595 /// After inserting 2 lines:
596 /// ```ignore
597 /// +---------------------+
598 /// | pre-existing line 1 |
599 /// | pre-existing line 2 |
600 /// | inserted line 1 |
601 /// | inserted line 2 |
602 /// +---------------------+
603 /// | viewport |
604 /// +---------------------+
605 /// +---------------------+
606 /// ```
607 ///
608 /// After inserting 2 more lines:
609 /// ```ignore
610 /// +---------------------+
611 /// | pre-existing line 2 |
612 /// | inserted line 1 |
613 /// | inserted line 2 |
614 /// | inserted line 3 |
615 /// | inserted line 4 |
616 /// +---------------------+
617 /// | viewport |
618 /// +---------------------+
619 /// ```
620 ///
621 /// If more lines are inserted than there is space on the screen, then the top lines will go
622 /// directly into the terminal's scrollback buffer. At the limit, if the viewport takes up the
623 /// whole screen, all lines will be inserted directly into the scrollback buffer.
624 ///
625 /// # Examples
626 ///
627 /// ## Insert a single line before the current viewport
628 ///
629 /// ```rust,ignore
630 /// use ratatui::{
631 /// backend::TestBackend,
632 /// style::{Color, Style},
633 /// text::{Line, Span},
634 /// widgets::{Paragraph, Widget},
635 /// Terminal,
636 /// };
637 /// # let backend = TestBackend::new(10, 10);
638 /// # let mut terminal = Terminal::new(backend).unwrap();
639 /// terminal.insert_before(1, |buf| {
640 /// Paragraph::new(Line::from(vec![
641 /// Span::raw("This line will be added "),
642 /// Span::styled("before", Style::default().fg(Color::Blue)),
643 /// Span::raw(" the current viewport"),
644 /// ]))
645 /// .render(buf.area, buf);
646 /// });
647 /// ```
648 pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> Result<(), B::Error>
649 where
650 F: FnOnce(&mut Buffer),
651 {
652 match self.viewport {
653 #[cfg(feature = "scrolling-regions")]
654 Viewport::Inline(_) => self.insert_before_scrolling_regions(height, draw_fn),
655 #[cfg(not(feature = "scrolling-regions"))]
656 Viewport::Inline(_) => self.insert_before_no_scrolling_regions(height, draw_fn),
657 _ => Ok(()),
658 }
659 }
660
661 /// Implement `Self::insert_before` using standard backend capabilities.
662 #[cfg(not(feature = "scrolling-regions"))]
663 fn insert_before_no_scrolling_regions(
664 &mut self,
665 height: u16,
666 draw_fn: impl FnOnce(&mut Buffer),
667 ) -> Result<(), B::Error> {
668 // The approach of this function is to first render all of the lines to insert into a
669 // temporary buffer, and then to loop drawing chunks from the buffer to the screen. drawing
670 // this buffer onto the screen.
671 let area = Rect {
672 x: 0,
673 y: 0,
674 width: self.viewport_area.width,
675 height,
676 };
677 let mut buffer = Buffer::empty(area);
678 draw_fn(&mut buffer);
679 let mut buffer = buffer.content.as_slice();
680
681 // Use i32 variables so we don't have worry about overflowed u16s when adding, or about
682 // negative results when subtracting.
683 let mut drawn_height: i32 = self.viewport_area.top().into();
684 let mut buffer_height: i32 = height.into();
685 let viewport_height: i32 = self.viewport_area.height.into();
686 let screen_height: i32 = self.last_known_area.height.into();
687
688 // The algorithm here is to loop, drawing large chunks of text (up to a screen-full at a
689 // time), until the remainder of the buffer plus the viewport fits on the screen. We choose
690 // this loop condition because it guarantees that we can write the remainder of the buffer
691 // with just one call to Self::draw_lines().
692 while buffer_height + viewport_height > screen_height {
693 // We will draw as much of the buffer as possible on this iteration in order to make
694 // forward progress. So we have:
695 //
696 // to_draw = min(buffer_height, screen_height)
697 //
698 // We may need to scroll the screen up to make room to draw. We choose the minimal
699 // possible scroll amount so we don't end up with the viewport sitting in the middle of
700 // the screen when this function is done. The amount to scroll by is:
701 //
702 // scroll_up = max(0, drawn_height + to_draw - screen_height)
703 //
704 // We want `scroll_up` to be enough so that, after drawing, we have used the whole
705 // screen (drawn_height - scroll_up + to_draw = screen_height). However, there might
706 // already be enough room on the screen to draw without scrolling (drawn_height +
707 // to_draw <= screen_height). In this case, we just don't scroll at all.
708 let to_draw = buffer_height.min(screen_height);
709 let scroll_up = 0.max(drawn_height + to_draw - screen_height);
710 self.scroll_up(scroll_up as u16)?;
711 buffer = self.draw_lines((drawn_height - scroll_up) as u16, to_draw as u16, buffer)?;
712 drawn_height += to_draw - scroll_up;
713 buffer_height -= to_draw;
714 }
715
716 // There is now enough room on the screen for the remaining buffer plus the viewport,
717 // though we may still need to scroll up some of the existing text first. It's possible
718 // that by this point we've drained the buffer, but we may still need to scroll up to make
719 // room for the viewport.
720 //
721 // We want to scroll up the exact amount that will leave us completely filling the screen.
722 // However, it's possible that the viewport didn't start on the bottom of the screen and
723 // the added lines weren't enough to push it all the way to the bottom. We deal with this
724 // case by just ensuring that our scroll amount is non-negative.
725 //
726 // We want:
727 // screen_height = drawn_height - scroll_up + buffer_height + viewport_height
728 // Or, equivalently:
729 // scroll_up = drawn_height + buffer_height + viewport_height - screen_height
730 let scroll_up = 0.max(drawn_height + buffer_height + viewport_height - screen_height);
731 self.scroll_up(scroll_up as u16)?;
732 self.draw_lines(
733 (drawn_height - scroll_up) as u16,
734 buffer_height as u16,
735 buffer,
736 )?;
737 drawn_height += buffer_height - scroll_up;
738
739 self.set_viewport_area(Rect {
740 y: drawn_height as u16,
741 ..self.viewport_area
742 });
743
744 // Clear the viewport off the screen. We didn't clear earlier for two reasons. First, it
745 // wasn't necessary because the buffer we drew out of isn't sparse, so it overwrote
746 // whatever was on the screen. Second, there is a weird bug with tmux where a full screen
747 // clear plus immediate scrolling causes some garbage to go into the scrollback.
748 self.clear()?;
749
750 Ok(())
751 }
752
753 /// Implement `Self::insert_before` using scrolling regions.
754 ///
755 /// If a terminal supports scrolling regions, it means that we can define a subset of rows of
756 /// the screen, and then tell the terminal to scroll up or down just within that region. The
757 /// rows outside of the region are not affected.
758 ///
759 /// This function utilizes this feature to avoid having to redraw the viewport. This is done
760 /// either by splitting the screen at the top of the viewport, and then creating a gap by
761 /// either scrolling the viewport down, or scrolling the area above it up. The lines to insert
762 /// are then drawn into the gap created.
763 #[cfg(feature = "scrolling-regions")]
764 fn insert_before_scrolling_regions(
765 &mut self,
766 mut height: u16,
767 draw_fn: impl FnOnce(&mut Buffer),
768 ) -> Result<(), B::Error> {
769 // The approach of this function is to first render all of the lines to insert into a
770 // temporary buffer, and then to loop drawing chunks from the buffer to the screen. drawing
771 // this buffer onto the screen.
772 let area = Rect {
773 x: 0,
774 y: 0,
775 width: self.viewport_area.width,
776 height,
777 };
778 let mut buffer = Buffer::empty(area);
779 draw_fn(&mut buffer);
780 let mut buffer = buffer.content.as_slice();
781
782 // Handle the special case where the viewport takes up the whole screen.
783 if self.viewport_area.height == self.last_known_area.height {
784 // "Borrow" the top line of the viewport. Draw over it, then immediately scroll it into
785 // scrollback. Do this repeatedly until the whole buffer has been put into scrollback.
786 let mut first = true;
787 while !buffer.is_empty() {
788 buffer = if first {
789 self.draw_lines(0, 1, buffer)?
790 } else {
791 self.draw_lines_over_cleared(0, 1, buffer)?
792 };
793 first = false;
794 self.backend.scroll_region_up(0..1, 1)?;
795 }
796
797 // Redraw the top line of the viewport.
798 let width = self.viewport_area.width as usize;
799 let top_line = self.buffers[1 - self.current].content[0..width].to_vec();
800 self.draw_lines_over_cleared(0, 1, &top_line)?;
801 return Ok(());
802 }
803
804 // Handle the case where the viewport isn't yet at the bottom of the screen.
805 {
806 let viewport_top = self.viewport_area.top();
807 let viewport_bottom = self.viewport_area.bottom();
808 let screen_bottom = self.last_known_area.bottom();
809 if viewport_bottom < screen_bottom {
810 let to_draw = height.min(screen_bottom - viewport_bottom);
811 self.backend
812 .scroll_region_down(viewport_top..viewport_bottom + to_draw, to_draw)?;
813 buffer = self.draw_lines_over_cleared(viewport_top, to_draw, buffer)?;
814 self.set_viewport_area(Rect {
815 y: viewport_top + to_draw,
816 ..self.viewport_area
817 });
818 height -= to_draw;
819 }
820 }
821
822 let viewport_top = self.viewport_area.top();
823 while height > 0 {
824 let to_draw = height.min(viewport_top);
825 self.backend.scroll_region_up(0..viewport_top, to_draw)?;
826 buffer = self.draw_lines_over_cleared(viewport_top - to_draw, to_draw, buffer)?;
827 height -= to_draw;
828 }
829
830 Ok(())
831 }
832
833 /// Draw lines at the given vertical offset. The slice of cells must contain enough cells
834 /// for the requested lines. A slice of the unused cells are returned.
835 fn draw_lines<'a>(
836 &mut self,
837 y_offset: u16,
838 lines_to_draw: u16,
839 cells: &'a [Cell],
840 ) -> Result<&'a [Cell], B::Error> {
841 let width: usize = self.last_known_area.width.into();
842 let (to_draw, remainder) = cells.split_at(width * lines_to_draw as usize);
843 if lines_to_draw > 0 {
844 let iter = to_draw
845 .iter()
846 .enumerate()
847 .map(|(i, c)| ((i % width) as u16, y_offset + (i / width) as u16, c));
848 self.backend.draw(iter)?;
849 self.backend.flush()?;
850 }
851 Ok(remainder)
852 }
853
854 /// Draw lines at the given vertical offset, assuming that the lines they are replacing on the
855 /// screen are cleared. The slice of cells must contain enough cells for the requested lines. A
856 /// slice of the unused cells are returned.
857 #[cfg(feature = "scrolling-regions")]
858 fn draw_lines_over_cleared<'a>(
859 &mut self,
860 y_offset: u16,
861 lines_to_draw: u16,
862 cells: &'a [Cell],
863 ) -> Result<&'a [Cell], B::Error> {
864 let width: usize = self.last_known_area.width.into();
865 let (to_draw, remainder) = cells.split_at(width * lines_to_draw as usize);
866 if lines_to_draw > 0 {
867 let area = Rect::new(0, y_offset, width as u16, y_offset + lines_to_draw);
868 let old = Buffer::empty(area);
869 let new = Buffer {
870 area,
871 content: to_draw.to_vec(),
872 };
873 self.backend.draw(old.diff(&new).into_iter())?;
874 self.backend.flush()?;
875 }
876 Ok(remainder)
877 }
878
879 /// Scroll the whole screen up by the given number of lines.
880 #[cfg(not(feature = "scrolling-regions"))]
881 fn scroll_up(&mut self, lines_to_scroll: u16) -> Result<(), B::Error> {
882 if lines_to_scroll > 0 {
883 self.set_cursor_position(Position::new(
884 0,
885 self.last_known_area.height.saturating_sub(1),
886 ))?;
887 self.backend.append_lines(lines_to_scroll)?;
888 }
889 Ok(())
890 }
891}
892
893fn compute_inline_size<B: Backend>(
894 backend: &mut B,
895 height: u16,
896 size: Size,
897 offset_in_previous_viewport: u16,
898) -> Result<(Rect, Position), B::Error> {
899 let pos = backend.get_cursor_position()?;
900 let mut row = pos.y;
901
902 let max_height = size.height.min(height);
903
904 let lines_after_cursor = height
905 .saturating_sub(offset_in_previous_viewport)
906 .saturating_sub(1);
907
908 backend.append_lines(lines_after_cursor)?;
909
910 let available_lines = size.height.saturating_sub(row).saturating_sub(1);
911 let missing_lines = lines_after_cursor.saturating_sub(available_lines);
912 if missing_lines > 0 {
913 row = row.saturating_sub(missing_lines);
914 }
915 row = row.saturating_sub(offset_in_previous_viewport);
916
917 Ok((
918 Rect {
919 x: 0,
920 y: row,
921 width: size.width,
922 height: max_height,
923 },
924 pos,
925 ))
926}