Skip to main content

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}