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