ratatui_core/terminal/render.rs
1use crate::backend::Backend;
2use crate::layout::Position;
3use crate::terminal::{CompletedFrame, Frame, Terminal};
4
5impl<B: Backend> Terminal<B> {
6 /// Draws a single frame to the terminal.
7 ///
8 /// Returns a [`CompletedFrame`] if successful, otherwise a backend error (`B::Error`).
9 ///
10 /// If the render callback passed to this method can fail, use [`try_draw`] instead.
11 ///
12 /// Applications should call `draw` or [`try_draw`] in a loop to continuously render the
13 /// terminal. These methods are the main entry points for drawing to the terminal.
14 ///
15 /// [`try_draw`]: Terminal::try_draw
16 ///
17 /// The [`Frame`] passed to the render callback represents the currently configured
18 /// [`Viewport`] (see [`Frame::area`] and [`Terminal::with_options`]).
19 ///
20 /// Build layout relative to the [`Rect`] returned by [`Frame::area`] rather than assuming the
21 /// origin is `(0, 0)`, so the same rendering code works for fixed and inline viewports.
22 ///
23 /// [`Frame::area`]: crate::terminal::Frame::area
24 /// [`Rect`]: crate::layout::Rect
25 /// [`Viewport`]: crate::terminal::Viewport
26 ///
27 /// This method will:
28 ///
29 /// - call [`Terminal::autoresize`] if necessary
30 /// - call the render callback, passing it a [`Frame`] reference to render to
31 /// - call [`Terminal::flush`] to apply the current buffer diff to the backend
32 /// - show/hide the cursor based on [`Frame::set_cursor_position`]
33 /// - call [`Terminal::swap_buffers`] to prepare for the next render pass
34 /// - call [`Backend::flush`] to flush any buffered backend output
35 /// - return a [`CompletedFrame`] with the current buffer and the area used for rendering
36 ///
37 /// If any backend step fails, the error is returned immediately and later steps in the render
38 /// pass are skipped.
39 ///
40 /// The [`CompletedFrame`] returned by this method can be useful for debugging or testing
41 /// purposes, but it is often not used in regular applications.
42 ///
43 /// The render callback should fully render the entire frame when called, including areas that
44 /// are unchanged from the previous frame. This is because each frame is compared to the
45 /// previous frame to determine what has changed, and only the changes are written to the
46 /// terminal. If the render callback does not fully render the frame, the terminal will not be
47 /// in a consistent state.
48 ///
49 /// # Examples
50 ///
51 /// ```rust,no_run
52 /// # mod ratatui {
53 /// # pub use ratatui_core::backend;
54 /// # pub use ratatui_core::layout;
55 /// # pub use ratatui_core::terminal::{Frame, Terminal};
56 /// # }
57 /// use ratatui::backend::TestBackend;
58 /// use ratatui::layout::Position;
59 /// use ratatui::{Frame, Terminal};
60 ///
61 /// let backend = TestBackend::new(10, 10);
62 /// let mut terminal = Terminal::new(backend)?;
63 ///
64 /// // With a closure.
65 /// terminal.draw(|frame| {
66 /// let area = frame.area();
67 /// frame.render_widget("Hello World!", area);
68 /// frame.set_cursor_position(Position { x: 0, y: 0 });
69 /// })?;
70 ///
71 /// // Or with a function.
72 /// terminal.draw(render)?;
73 ///
74 /// fn render(frame: &mut Frame<'_>) {
75 /// frame.render_widget("Hello World!", frame.area());
76 /// }
77 /// # Ok::<(), Box<dyn std::error::Error>>(())
78 /// ```
79 ///
80 /// [`Backend::flush`]: crate::backend::Backend::flush
81 pub fn draw<F>(&mut self, render_callback: F) -> Result<CompletedFrame<'_>, B::Error>
82 where
83 F: FnOnce(&mut Frame),
84 {
85 self.try_draw(|frame| {
86 render_callback(frame);
87 Ok::<(), B::Error>(())
88 })
89 }
90
91 /// Tries to draw a single frame to the terminal.
92 ///
93 /// Returns [`Result::Ok`] containing a [`CompletedFrame`] if successful, otherwise
94 /// [`Result::Err`] containing the backend error (`B::Error`) that caused the failure.
95 ///
96 /// This is the equivalent of [`Terminal::draw`] but the render callback is a function or
97 /// closure that returns a `Result` instead of nothing.
98 ///
99 /// Applications should call `try_draw` or [`draw`] in a loop to continuously render the
100 /// terminal. These methods are the main entry points for drawing to the terminal.
101 ///
102 /// [`draw`]: Terminal::draw
103 ///
104 /// The [`Frame`] passed to the render callback represents the currently configured
105 /// [`Viewport`] (see [`Frame::area`] and [`Terminal::with_options`]).
106 ///
107 /// Build layout relative to the [`Rect`] returned by [`Frame::area`] rather than assuming the
108 /// origin is `(0, 0)`, so the same rendering code works for fixed and inline viewports.
109 ///
110 /// [`Frame::area`]: crate::terminal::Frame::area
111 /// [`Rect`]: crate::layout::Rect
112 /// [`Viewport`]: crate::terminal::Viewport
113 ///
114 /// This method will:
115 ///
116 /// - call [`Terminal::autoresize`] if necessary
117 /// - call the render callback, passing it a [`Frame`] reference to render to
118 /// - call [`Terminal::flush`] to apply the current buffer diff to the backend
119 /// - show/hide the cursor based on [`Frame::set_cursor_position`]
120 /// - call [`Terminal::swap_buffers`] to prepare for the next render pass
121 /// - call [`Backend::flush`] to flush any buffered backend output
122 /// - return a [`CompletedFrame`] with the current buffer and the area used for rendering
123 ///
124 /// If the render callback returns an error, Ratatui leaves the backend, buffers, cursor state,
125 /// and frame count unchanged.
126 ///
127 /// The render callback passed to `try_draw` can return any [`Result`] with an error type that
128 /// can be converted into `B::Error` using the [`Into`] trait. This makes it possible to use the
129 /// `?` operator to propagate errors that occur during rendering. If the render callback returns
130 /// an error, the error will be returned from `try_draw` and the terminal will not be updated.
131 ///
132 /// The [`CompletedFrame`] returned by this method can be useful for debugging or testing
133 /// purposes, but it is often not used in regular applications.
134 ///
135 /// The render callback should fully render the entire frame when called, including areas that
136 /// are unchanged from the previous frame. This is because each frame is compared to the
137 /// previous frame to determine what has changed, and only the changes are written to the
138 /// terminal. If the render function does not fully render the frame, the terminal will not be
139 /// in a consistent state.
140 ///
141 /// # Examples
142 ///
143 /// ```rust,no_run
144 /// # #![allow(unexpected_cfgs)]
145 /// # #[cfg(feature = "crossterm")]
146 /// # {
147 /// use std::io;
148 ///
149 /// use ratatui::backend::CrosstermBackend;
150 /// use ratatui::layout::Position;
151 /// use ratatui::{Frame, Terminal};
152 ///
153 /// let backend = CrosstermBackend::new(std::io::stdout());
154 /// let mut terminal = Terminal::new(backend)?;
155 ///
156 /// // With a closure that returns `Result`.
157 /// terminal.try_draw(|frame| -> io::Result<()> {
158 /// let _value: u8 = "42".parse().map_err(io::Error::other)?;
159 /// let area = frame.area();
160 /// frame.render_widget("Hello World!", area);
161 /// frame.set_cursor_position(Position { x: 0, y: 0 });
162 /// Ok(())
163 /// })?;
164 ///
165 /// // Or with a function.
166 /// terminal.try_draw(render)?;
167 ///
168 /// fn render(frame: &mut Frame<'_>) -> io::Result<()> {
169 /// frame.render_widget("Hello World!", frame.area());
170 /// Ok(())
171 /// }
172 /// # }
173 /// # #[cfg(not(feature = "crossterm"))]
174 /// # {
175 /// # use ratatui_core::{backend::TestBackend, terminal::Terminal};
176 /// # let backend = TestBackend::new(10, 10);
177 /// # let mut terminal = Terminal::new(backend)?;
178 /// # terminal
179 /// # .try_draw(|frame| {
180 /// # frame.render_widget("Hello World!", frame.area());
181 /// # Ok::<(), core::convert::Infallible>(())
182 /// # })
183 /// # ?;
184 /// # }
185 /// # Ok::<(), Box<dyn std::error::Error>>(())
186 /// ```
187 ///
188 /// [`Backend::flush`]: crate::backend::Backend::flush
189 pub fn try_draw<F, E>(&mut self, render_callback: F) -> Result<CompletedFrame<'_>, B::Error>
190 where
191 F: FnOnce(&mut Frame) -> Result<(), E>,
192 E: Into<B::Error>,
193 {
194 // Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
195 // and the terminal (if growing), which may OOB.
196 self.autoresize()?;
197
198 let mut frame = self.get_frame();
199
200 render_callback(&mut frame).map_err(Into::into)?;
201
202 let cursor_position = frame.cursor_position;
203
204 self.apply_buffer_with_cursor(cursor_position)
205 }
206
207 /// A low-level function that applies and flushes the current buffer to the backend.
208 ///
209 /// This calls [`Terminal::apply_buffer_with_cursor`] with [`None`], which hides the cursor.
210 ///
211 /// # Examples
212 ///
213 /// ```rust,no_run
214 /// # #![allow(unexpected_cfgs)]
215 /// # #[cfg(feature = "crossterm")]
216 /// # {
217 /// use std::io;
218 ///
219 /// use ratatui::Terminal;
220 /// use ratatui::backend::CrosstermBackend;
221 /// use ratatui::buffer::Buffer;
222 /// use ratatui::widgets::Widget;
223 ///
224 /// let backend = CrosstermBackend::new(io::stdout());
225 /// let mut terminal = Terminal::new(backend)?;
226 ///
227 /// terminal.autoresize()?;
228 ///
229 /// let mut custom_buffer = Buffer::default();
230 /// custom_buffer.resize(terminal.get_frame().area());
231 /// custom_buffer.reset();
232 ///
233 /// "Hello World!".render(custom_buffer.area, &mut custom_buffer);
234 ///
235 /// terminal.current_buffer_mut().merge(&custom_buffer);
236 /// terminal.apply_buffer()?;
237 /// # }
238 /// ```
239 pub fn apply_buffer(&mut self) -> Result<CompletedFrame<'_>, B::Error> {
240 self.apply_buffer_with_cursor(None)
241 }
242
243 /// A low-level function that applies and flushes the current buffer to the backend and
244 /// re-positions the cursor. This function is useful if you need to manage your own custom
245 /// draw lifecycle and buffer.
246 ///
247 /// Returns [`Result::Ok`] containing a [`CompletedFrame`] if successful, otherwise
248 /// [`Result::Err`] containing the backend error (`B::Error`) that caused the failure.
249 ///
250 /// This method will:
251 ///
252 /// - show/hide the cursor based on `cursor_position` ([`None`] will hide the cursor)
253 /// - call [`Terminal::swap_buffers`] to prepare for the next render pass
254 /// - call [`Backend::flush`] to flush any buffered backend output
255 /// - return a [`CompletedFrame`] with the current buffer and the area used for rendering
256 ///
257 /// The [`CompletedFrame`] returned by this method can be useful for debugging or testing
258 /// purposes, but it is often not used in regular applications.
259 ///
260 /// # Examples
261 ///
262 /// ```rust,no_run
263 /// # #![allow(unexpected_cfgs)]
264 /// # #[cfg(feature = "crossterm")]
265 /// # {
266 /// use std::io;
267 ///
268 /// use ratatui::Terminal;
269 /// use ratatui::backend::CrosstermBackend;
270 /// use ratatui::buffer::Buffer;
271 /// use ratatui::widgets::Widget;
272 ///
273 /// let backend = CrosstermBackend::new(io::stdout());
274 /// let mut terminal = Terminal::new(backend)?;
275 ///
276 /// terminal.autoresize()?;
277 ///
278 /// let mut custom_buffer = Buffer::default();
279 /// custom_buffer.resize(terminal.get_frame().area());
280 /// custom_buffer.reset();
281 ///
282 /// "Hello World!".render(custom_buffer.area, &mut custom_buffer);
283 ///
284 /// terminal.current_buffer_mut().merge(&custom_buffer);
285 /// terminal.apply_buffer_with_cursor(None)?;
286 /// # }
287 /// ```
288 pub fn apply_buffer_with_cursor(
289 &mut self,
290 cursor_position: Option<Position>,
291 ) -> Result<CompletedFrame<'_>, B::Error> {
292 // Apply the buffer diff to the backend (this is the terminal's "flush" step, distinct
293 // from `Backend::flush` below which flushes the backend's output).
294 self.flush()?;
295
296 // The cursor position can only be changed after the frame is flushed to stdout.
297 match cursor_position {
298 None => self.hide_cursor()?,
299 Some(position) => {
300 self.show_cursor()?;
301 self.set_cursor_position(position)?;
302 }
303 }
304
305 self.swap_buffers();
306
307 // Flush any buffered backend output.
308 self.backend.flush()?;
309
310 let completed_frame = CompletedFrame {
311 buffer: &self.buffers[1 - self.current],
312 area: self.last_known_area,
313 count: self.frame_count,
314 };
315
316 // increment frame count before returning from draw
317 self.frame_count = self.frame_count.wrapping_add(1);
318
319 Ok(completed_frame)
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use core::fmt;
326
327 use crate::backend::{Backend, ClearType, TestBackend, WindowSize};
328 use crate::buffer::{Buffer, Cell};
329 use crate::layout::{Position, Rect};
330 use crate::terminal::{Terminal, TerminalOptions, Viewport};
331
332 #[derive(Debug, Clone, Eq, PartialEq)]
333 struct TestError(&'static str);
334
335 impl fmt::Display for TestError {
336 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
337 write!(f, "{}", self.0)
338 }
339 }
340
341 impl core::error::Error for TestError {}
342
343 /// A thin wrapper around [`TestBackend`] with a fallible error type.
344 ///
345 /// [`TestBackend`] uses [`core::convert::Infallible`] as its associated `Backend::Error`, which
346 /// is ideal for most tests but makes it impossible to write a `try_draw` callback that returns
347 /// an error (because `E: Into<B::Error>` would require converting a real error into
348 /// `Infallible`). This wrapper keeps the same observable backend behavior (buffer + cursor)
349 /// while allowing tests to exercise `Terminal::try_draw`'s error path.
350 #[derive(Debug, Clone, Eq, PartialEq)]
351 struct FallibleTestBackend {
352 inner: TestBackend,
353 }
354
355 impl FallibleTestBackend {
356 fn new(inner: TestBackend) -> Self {
357 Self { inner }
358 }
359 }
360
361 impl Backend for FallibleTestBackend {
362 type Error = TestError;
363
364 fn draw<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
365 where
366 I: Iterator<Item = (u16, u16, &'a crate::buffer::Cell)>,
367 {
368 self.inner.draw(content).map_err(|err| match err {})
369 }
370
371 fn append_lines(&mut self, n: u16) -> Result<(), Self::Error> {
372 self.inner.append_lines(n).map_err(|err| match err {})
373 }
374
375 fn hide_cursor(&mut self) -> Result<(), Self::Error> {
376 self.inner.hide_cursor().map_err(|err| match err {})
377 }
378
379 fn show_cursor(&mut self) -> Result<(), Self::Error> {
380 self.inner.show_cursor().map_err(|err| match err {})
381 }
382
383 fn get_cursor_position(&mut self) -> Result<Position, Self::Error> {
384 self.inner.get_cursor_position().map_err(|err| match err {})
385 }
386
387 fn set_cursor_position<P: Into<Position>>(
388 &mut self,
389 position: P,
390 ) -> Result<(), Self::Error> {
391 self.inner
392 .set_cursor_position(position)
393 .map_err(|err| match err {})
394 }
395
396 fn clear(&mut self) -> Result<(), Self::Error> {
397 self.inner.clear().map_err(|err| match err {})
398 }
399
400 fn clear_region(&mut self, clear_type: ClearType) -> Result<(), Self::Error> {
401 self.inner
402 .clear_region(clear_type)
403 .map_err(|err| match err {})
404 }
405
406 fn size(&self) -> Result<crate::layout::Size, Self::Error> {
407 self.inner.size().map_err(|err| match err {})
408 }
409
410 fn window_size(&mut self) -> Result<WindowSize, Self::Error> {
411 self.inner.window_size().map_err(|err| match err {})
412 }
413
414 fn flush(&mut self) -> Result<(), Self::Error> {
415 self.inner.flush().map_err(|err| match err {})
416 }
417
418 #[cfg(feature = "scrolling-regions")]
419 fn scroll_region_up(
420 &mut self,
421 region: core::ops::Range<u16>,
422 line_count: u16,
423 ) -> Result<(), Self::Error> {
424 self.inner
425 .scroll_region_up(region, line_count)
426 .map_err(|err| match err {})
427 }
428
429 #[cfg(feature = "scrolling-regions")]
430 fn scroll_region_down(
431 &mut self,
432 region: core::ops::Range<u16>,
433 line_count: u16,
434 ) -> Result<(), Self::Error> {
435 self.inner
436 .scroll_region_down(region, line_count)
437 .map_err(|err| match err {})
438 }
439 }
440
441 /// `draw` hides the cursor when the frame does not request a cursor position.
442 ///
443 /// This asserts the end-to-end effect on the backend (buffer contents + cursor state) as well
444 /// as internal frame counting.
445 #[test]
446 fn draw_hides_cursor_when_frame_cursor_is_not_set() {
447 let backend = TestBackend::new(3, 2);
448 let mut terminal = Terminal::new(backend).unwrap();
449
450 terminal.show_cursor().unwrap();
451
452 let completed = terminal
453 .draw(|frame| {
454 // Ensure the frame produces updates so `Terminal::flush` writes to the backend.
455 frame.buffer_mut()[(0, 0)] = Cell::new("x");
456 })
457 .unwrap();
458
459 assert_eq!(completed.count, 0, "first draw returns count 0");
460 assert_eq!(
461 completed.area,
462 Rect::new(0, 0, 3, 2),
463 "completed area matches terminal size in fullscreen mode"
464 );
465 assert_eq!(
466 completed.buffer,
467 &Buffer::with_lines(["x ", " "]),
468 "completed buffer contains the rendered content"
469 );
470
471 assert!(terminal.hidden_cursor);
472 assert!(!terminal.backend().cursor_visible());
473 assert_eq!(
474 terminal.frame_count, 1,
475 "successful draw increments frame_count"
476 );
477 }
478
479 /// `draw` applies the cursor requested by `Frame::set_cursor_position`.
480 ///
481 /// The cursor is updated after rendering has been flushed, so it appears on top of the drawn
482 /// UI.
483 #[test]
484 fn draw_shows_and_positions_cursor_when_frame_cursor_is_set() {
485 let backend = TestBackend::new(3, 2);
486 let mut terminal = Terminal::new(backend).unwrap();
487
488 terminal.hide_cursor().unwrap();
489
490 terminal
491 .draw(|frame| {
492 // The cursor is applied after the frame is flushed.
493 frame.set_cursor_position(Position { x: 2, y: 1 });
494 frame.buffer_mut()[(1, 0)] = Cell::new("y");
495 })
496 .unwrap();
497
498 assert!(!terminal.hidden_cursor);
499 assert!(terminal.backend().cursor_visible());
500 assert_eq!(
501 terminal.backend().cursor_position(),
502 Position { x: 2, y: 1 },
503 "backend cursor is positioned after flushing"
504 );
505 assert_eq!(
506 terminal.last_known_cursor_pos,
507 Position { x: 2, y: 1 },
508 "terminal cursor tracking matches the final cursor position"
509 );
510 }
511
512 /// When the render callback returns an error, `try_draw` does not update the terminal.
513 ///
514 /// This is a characterization of the "no partial updates" behavior: backend contents and
515 /// cursor state are unchanged and `frame_count` does not advance.
516 #[test]
517 fn try_draw_propagates_render_errors_without_updating_backend() {
518 let backend = FallibleTestBackend::new(TestBackend::with_lines(["aaa", "bbb"]));
519 let mut terminal = Terminal::new(backend).unwrap();
520
521 terminal.show_cursor().unwrap();
522
523 let was_hidden = terminal.hidden_cursor;
524 let cursor_visible = terminal.backend().inner.cursor_visible();
525 let cursor_position = terminal.backend().inner.cursor_position();
526
527 let result = terminal.try_draw(|_frame| Err::<(), _>(TestError("render failed")));
528
529 assert_eq!(
530 result.unwrap_err(),
531 TestError("render failed"),
532 "try_draw returns the render callback error"
533 );
534
535 assert_eq!(terminal.frame_count, 0, "frame_count is unchanged on error");
536 assert_eq!(
537 terminal.backend().inner.buffer(),
538 &Buffer::with_lines(["aaa", "bbb"]),
539 "backend buffer is unchanged on error"
540 );
541 assert_eq!(
542 terminal.hidden_cursor, was_hidden,
543 "terminal cursor state is unchanged on error"
544 );
545 assert_eq!(
546 terminal.backend().inner.cursor_visible(),
547 cursor_visible,
548 "backend cursor visibility is unchanged on error"
549 );
550 assert_eq!(
551 terminal.backend().inner.cursor_position(),
552 cursor_position,
553 "backend cursor position is unchanged on error"
554 );
555 }
556
557 /// `draw` autoresizes fullscreen terminals and clears before rendering.
558 ///
559 /// This simulates the backend resizing between draw calls; `draw` runs `autoresize()` first
560 /// (which calls `resize()` and clears) so the frame renders into a fresh, correctly-sized
561 /// region.
562 #[test]
563 fn draw_clears_on_fullscreen_resize_before_rendering() {
564 let backend = TestBackend::with_lines(["xxx", "yyy"]);
565 let mut terminal = Terminal::new(backend).unwrap();
566
567 terminal.backend_mut().resize(4, 3);
568
569 terminal
570 .draw(|frame| {
571 // Render a marker to show we rendered after the clear.
572 frame.buffer_mut()[(0, 0)] = Cell::new("x");
573 })
574 .unwrap();
575
576 assert_eq!(
577 terminal.viewport_area,
578 Rect::new(0, 0, 4, 3),
579 "viewport area tracks the resized terminal size"
580 );
581 assert_eq!(
582 terminal.last_known_area,
583 Rect::new(0, 0, 4, 3),
584 "last_known_area tracks the resized terminal size"
585 );
586 terminal
587 .backend()
588 .assert_buffer_lines(["x ", " ", " "]);
589 }
590
591 /// In fixed viewports, `Frame::area` is an absolute terminal rectangle.
592 ///
593 /// This asserts that rendering at `frame.area().x/y` updates the backend at that absolute
594 /// position.
595 #[test]
596 fn draw_uses_fixed_viewport_coordinates() {
597 let backend = TestBackend::new(5, 3);
598 let mut terminal = Terminal::with_options(
599 backend,
600 TerminalOptions {
601 viewport: Viewport::Fixed(Rect::new(2, 1, 2, 1)),
602 },
603 )
604 .unwrap();
605
606 terminal
607 .draw(|frame| {
608 assert_eq!(
609 frame.area(),
610 Rect::new(2, 1, 2, 1),
611 "frame area matches the configured fixed viewport"
612 );
613 let area = frame.area();
614 frame.buffer_mut()[(area.x, area.y)] = Cell::new("z");
615 })
616 .unwrap();
617
618 terminal
619 .backend()
620 .assert_buffer_lines([" ", " z ", " "]);
621 }
622
623 /// Inline viewports render into a sub-rectangle, but `CompletedFrame::area` reports terminal
624 /// size.
625 ///
626 /// This asserts that the `CompletedFrame` returned from `draw` reports the full terminal
627 /// size while its buffer is sized to the inline viewport, and that rendering uses the inline
628 /// viewport's absolute origin.
629 #[test]
630 fn draw_inline_completed_frame_reports_terminal_size() {
631 let mut inner = TestBackend::new(6, 5);
632 inner.set_cursor_position((0, 2)).unwrap();
633 let mut terminal = Terminal::with_options(
634 inner,
635 TerminalOptions {
636 viewport: Viewport::Inline(3),
637 },
638 )
639 .unwrap();
640
641 let viewport_area = terminal.viewport_area;
642 {
643 // `CompletedFrame` borrows the terminal, so backend assertions happen after it drops.
644 let completed = terminal
645 .draw(|frame| {
646 assert_eq!(
647 frame.area(),
648 viewport_area,
649 "inline frame area matches the computed viewport"
650 );
651 frame.buffer_mut()[(viewport_area.x, viewport_area.y)] = Cell::new("i");
652 })
653 .unwrap();
654
655 assert_eq!(
656 completed.area,
657 Rect::new(0, 0, 6, 5),
658 "completed area reports the full terminal size"
659 );
660 assert_eq!(
661 completed.buffer.area, viewport_area,
662 "completed buffer is sized to the inline viewport"
663 );
664 }
665
666 assert_eq!(
667 terminal.backend().buffer()[(viewport_area.x, viewport_area.y)].symbol(),
668 "i"
669 );
670 }
671
672 /// Inline viewports are autoresized during `draw`.
673 ///
674 /// This asserts that when the backend reports a different terminal size, `draw` recomputes the
675 /// inline viewport rectangle and renders into the new viewport area.
676 #[test]
677 fn draw_inline_autoresize_recomputes_viewport_on_grow() {
678 let mut backend = TestBackend::new(6, 5);
679 backend
680 .set_cursor_position(Position { x: 0, y: 2 })
681 .unwrap();
682 let mut terminal = Terminal::with_options(
683 backend,
684 TerminalOptions {
685 viewport: Viewport::Inline(3),
686 },
687 )
688 .unwrap();
689
690 terminal
691 .draw(|frame| {
692 let area = frame.area();
693 frame.set_cursor_position(Position {
694 x: area.x,
695 y: area.y.saturating_add(1),
696 });
697 frame.buffer_mut()[(area.x, area.y)] = Cell::new("a");
698 })
699 .unwrap();
700
701 terminal.backend_mut().resize(8, 7);
702 let new_area = Rect::new(0, 0, 8, 7);
703
704 let previous_viewport = terminal.viewport_area;
705 terminal
706 .draw(|frame| {
707 let area = frame.area();
708 frame.buffer_mut()[(area.x, area.y)] = Cell::new("g");
709 })
710 .unwrap();
711
712 assert_eq!(
713 terminal.last_known_area, new_area,
714 "inline last_known_area tracks the resized terminal size"
715 );
716 assert_eq!(
717 terminal.viewport_area.width, 8,
718 "inline viewport width tracks the resized terminal width"
719 );
720 assert_eq!(
721 terminal.viewport_area.height, 3,
722 "inline viewport height is capped by the configured inline height"
723 );
724 assert_eq!(
725 terminal.viewport_area.y, previous_viewport.y,
726 "inline viewport stays anchored relative to the cursor across a grow"
727 );
728 assert_eq!(
729 terminal.backend().buffer()[(terminal.viewport_area.x, terminal.viewport_area.y)]
730 .symbol(),
731 "g",
732 "render output lands at the recomputed viewport origin"
733 );
734 }
735
736 /// Inline viewports are autoresized during `draw`.
737 ///
738 /// This asserts that shrinking the backend terminal size causes `draw` to recompute the inline
739 /// viewport origin so it stays visible, and that rendering uses the new viewport origin.
740 #[test]
741 fn draw_inline_autoresize_recomputes_viewport_on_shrink() {
742 let mut backend = TestBackend::new(6, 6);
743 backend
744 .set_cursor_position(Position { x: 0, y: 4 })
745 .unwrap();
746 let mut terminal = Terminal::with_options(
747 backend,
748 TerminalOptions {
749 viewport: Viewport::Inline(4),
750 },
751 )
752 .unwrap();
753
754 terminal
755 .draw(|frame| {
756 let area = frame.area();
757 frame.set_cursor_position(Position {
758 x: area.x,
759 y: area.y.saturating_add(2),
760 });
761 frame.buffer_mut()[(area.x, area.y)] = Cell::new("a");
762 })
763 .unwrap();
764
765 terminal.backend_mut().resize(6, 5);
766 let new_area = Rect::new(0, 0, 6, 5);
767
768 terminal
769 .draw(|frame| {
770 let area = frame.area();
771 frame.buffer_mut()[(area.x, area.y)] = Cell::new("s");
772 })
773 .unwrap();
774
775 assert_eq!(
776 terminal.last_known_area, new_area,
777 "inline last_known_area tracks the resized terminal size"
778 );
779 assert_eq!(
780 terminal.viewport_area,
781 Rect::new(0, 1, 6, 4),
782 "inline viewport is recomputed to stay visible after a shrink"
783 );
784 assert_eq!(
785 terminal.backend().buffer()[(terminal.viewport_area.x, terminal.viewport_area.y)]
786 .symbol(),
787 "s",
788 "render output lands at the recomputed viewport origin"
789 );
790 }
791
792 /// `CompletedFrame` is only valid until the next draw call.
793 ///
794 /// This asserts that each `draw` returns the buffer for the frame that was just rendered
795 /// and that the count increments after each successful draw.
796 #[test]
797 fn draw_returns_completed_frame_for_current_render_pass() {
798 let backend = TestBackend::new(3, 2);
799 let mut terminal = Terminal::new(backend).unwrap();
800
801 {
802 // `CompletedFrame` borrows the terminal, and is only valid until the next draw call.
803 let first = terminal
804 .draw(|frame| {
805 frame.buffer_mut()[(0, 0)] = Cell::new("a");
806 })
807 .unwrap();
808
809 assert_eq!(first.count, 0, "first CompletedFrame has count 0");
810 assert_eq!(
811 first.buffer,
812 &Buffer::with_lines(["a ", " "]),
813 "first frame's buffer contains the first render output"
814 );
815 }
816
817 let second = terminal
818 .draw(|frame| {
819 frame.buffer_mut()[(0, 0)] = Cell::new("b");
820 })
821 .unwrap();
822
823 assert_eq!(second.count, 1, "second CompletedFrame has count 1");
824 assert_eq!(
825 second.buffer,
826 &Buffer::with_lines(["b ", " "]),
827 "second frame's buffer contains the second render output"
828 );
829 }
830
831 #[test]
832 fn apply_buffer_hides_cursor() {
833 let backend = TestBackend::new(3, 2);
834 let mut terminal = Terminal::new(backend).unwrap();
835
836 terminal.show_cursor().unwrap();
837 terminal.autoresize().unwrap();
838
839 let mut external_buffer = Buffer::default();
840 external_buffer.resize(terminal.get_frame().area());
841 external_buffer[(0, 0)] = Cell::new("b");
842
843 terminal.current_buffer_mut().merge(&external_buffer);
844 let completed = terminal.apply_buffer().unwrap();
845
846 assert_eq!(completed.count, 0, "first draw returns count 0");
847 assert_eq!(
848 completed.area,
849 Rect::new(0, 0, 3, 2),
850 "completed area matches terminal size in fullscreen mode"
851 );
852 assert_eq!(
853 completed.buffer,
854 &Buffer::with_lines(["b ", " "]),
855 "completed buffer contains the rendered content"
856 );
857
858 assert!(terminal.hidden_cursor);
859 assert!(!terminal.backend().cursor_visible());
860 assert_eq!(
861 terminal.frame_count, 1,
862 "successful draw increments frame_count"
863 );
864 }
865}