Skip to main content

ratatui_core/terminal/
inline.rs

1use crate::backend::Backend;
2use crate::buffer::{Buffer, Cell};
3use crate::layout::{Position, Rect, Size};
4use crate::terminal::{Terminal, Viewport};
5
6impl<B: Backend> Terminal<B> {
7    /// Insert some content before the current inline viewport. This has no effect when the
8    /// viewport is not inline.
9    ///
10    /// This is intended for inline UIs that want to print output (e.g. logs or status messages)
11    /// above the UI without breaking it. See [`Viewport::Inline`] for how inline viewports are
12    /// anchored.
13    ///
14    /// The `draw_fn` closure will be called to draw into a writable `Buffer` that is `height`
15    /// lines tall. The content of that `Buffer` will then be inserted before the viewport.
16    ///
17    /// When Ratatui is built with the `scrolling-regions` feature, this can be done without
18    /// clearing and redrawing the viewport. Without `scrolling-regions`, Ratatui falls back to a
19    /// more portable approach and clears the viewport so the next [`Terminal::draw`] /
20    /// [`Terminal::try_draw`] repaints it.
21    ///
22    /// If the viewport isn't yet at the bottom of the screen, inserted lines will push it towards
23    /// the bottom. Once the viewport is at the bottom of the screen, inserted lines will scroll
24    /// the area of the screen above the viewport upwards.
25    ///
26    /// Before:
27    /// ```text
28    /// +---------------------+
29    /// | pre-existing line 1 |
30    /// | pre-existing line 2 |
31    /// +---------------------+
32    /// |       viewport      |
33    /// +---------------------+
34    /// |                     |
35    /// |                     |
36    /// +---------------------+
37    /// ```
38    ///
39    /// After inserting 2 lines:
40    /// ```text
41    /// +---------------------+
42    /// | pre-existing line 1 |
43    /// | pre-existing line 2 |
44    /// |   inserted line 1   |
45    /// |   inserted line 2   |
46    /// +---------------------+
47    /// |       viewport      |
48    /// +---------------------+
49    /// +---------------------+
50    /// ```
51    ///
52    /// After inserting 2 more lines:
53    /// ```text
54    /// +---------------------+
55    /// | pre-existing line 2 |
56    /// |   inserted line 1   |
57    /// |   inserted line 2   |
58    /// |   inserted line 3   |
59    /// |   inserted line 4   |
60    /// +---------------------+
61    /// |       viewport      |
62    /// +---------------------+
63    /// ```
64    ///
65    /// If more lines are inserted than there is space on the screen, then the top lines will go
66    /// directly into the terminal's scrollback buffer. At the limit, if the viewport takes up the
67    /// whole screen, all lines will be inserted directly into the scrollback buffer.
68    ///
69    /// # Examples
70    ///
71    /// ## Insert a single line before the current viewport
72    ///
73    /// ```rust,no_run
74    /// # mod ratatui {
75    /// #     pub use ratatui_core::backend;
76    /// #     pub use ratatui_core::layout;
77    /// #     pub use ratatui_core::style;
78    /// #     pub use ratatui_core::terminal::{Terminal, TerminalOptions, Viewport};
79    /// #     pub use ratatui_core::text;
80    /// #     pub use ratatui_core::widgets;
81    /// # }
82    /// use ratatui::backend::{Backend, TestBackend};
83    /// use ratatui::layout::Position;
84    /// use ratatui::style::{Color, Style};
85    /// use ratatui::text::{Line, Span};
86    /// use ratatui::widgets::Widget;
87    /// use ratatui::{Terminal, TerminalOptions, Viewport};
88    ///
89    /// let mut backend = TestBackend::new(10, 10);
90    /// // Simulate existing output above the inline UI.
91    /// backend.set_cursor_position(Position::new(0, 3))?;
92    /// let mut terminal = Terminal::with_options(
93    ///     backend,
94    ///     TerminalOptions {
95    ///         viewport: Viewport::Inline(4),
96    ///     },
97    /// )?;
98    ///
99    /// terminal.insert_before(1, |buf| {
100    ///     Line::from(vec![
101    ///         Span::raw("This line will be added "),
102    ///         Span::styled("before", Style::default().fg(Color::Blue)),
103    ///         Span::raw(" the current viewport"),
104    ///     ])
105    ///     .render(buf.area, buf);
106    /// })?;
107    /// # Ok::<(), Box<dyn std::error::Error>>(())
108    /// ```
109    pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> Result<(), B::Error>
110    where
111        F: FnOnce(&mut Buffer),
112    {
113        match self.viewport {
114            #[cfg(feature = "scrolling-regions")]
115            Viewport::Inline(_) => self.insert_before_scrolling_regions(height, draw_fn),
116            #[cfg(not(feature = "scrolling-regions"))]
117            Viewport::Inline(_) => self.insert_before_no_scrolling_regions(height, draw_fn),
118            _ => Ok(()),
119        }
120    }
121
122    /// Implement `Self::insert_before` using standard backend capabilities.
123    ///
124    /// This is the fallback implementation when the `scrolling-regions` feature is disabled. It
125    /// renders the inserted lines into a temporary [`Buffer`], then draws them directly to the
126    /// backend in chunks, scrolling the terminal as needed.
127    ///
128    /// See [`Terminal::insert_before`] for the public API contract.
129    #[cfg(not(feature = "scrolling-regions"))]
130    fn insert_before_no_scrolling_regions(
131        &mut self,
132        height: u16,
133        draw_fn: impl FnOnce(&mut Buffer),
134    ) -> Result<(), B::Error> {
135        let area = Rect {
136            x: 0,
137            y: 0,
138            width: self.viewport_area.width,
139            height,
140        };
141        let mut buffer = Buffer::empty(area);
142        draw_fn(&mut buffer);
143        let mut buffer = buffer.content.as_slice();
144
145        // Use i32 variables so we don't have worry about overflowed u16s when adding, or about
146        // negative results when subtracting.
147        let mut drawn_height: i32 = self.viewport_area.top().into();
148        let mut buffer_height: i32 = height.into();
149        let viewport_height: i32 = self.viewport_area.height.into();
150        let screen_height: i32 = self.last_known_area.height.into();
151
152        // The algorithm here is to loop, drawing large chunks of text (up to a screen-full at a
153        // time), until the remainder of the buffer plus the viewport fits on the screen. We choose
154        // this loop condition because it guarantees that we can write the remainder of the buffer
155        // with just one call to Self::draw_lines().
156        while buffer_height + viewport_height > screen_height {
157            // We will draw as much of the buffer as possible on this iteration in order to make
158            // forward progress. So we have:
159            //
160            //     to_draw = min(buffer_height, screen_height)
161            //
162            // We may need to scroll the screen up to make room to draw. We choose the minimal
163            // possible scroll amount so we don't end up with the viewport sitting in the middle of
164            // the screen when this function is done. The amount to scroll by is:
165            //
166            //     scroll_up = max(0, drawn_height + to_draw - screen_height)
167            //
168            // We want `scroll_up` to be enough so that, after drawing, we have used the whole
169            // screen (drawn_height - scroll_up + to_draw = screen_height). However, there might
170            // already be enough room on the screen to draw without scrolling (drawn_height +
171            // to_draw <= screen_height). In this case, we just don't scroll at all.
172            let to_draw = buffer_height.min(screen_height);
173            let scroll_up = 0.max(drawn_height + to_draw - screen_height);
174            self.scroll_up(scroll_up as u16)?;
175            buffer = self.draw_lines((drawn_height - scroll_up) as u16, to_draw as u16, buffer)?;
176            drawn_height += to_draw - scroll_up;
177            buffer_height -= to_draw;
178        }
179
180        // There is now enough room on the screen for the remaining buffer plus the viewport,
181        // though we may still need to scroll up some of the existing text first. It's possible
182        // that by this point we've drained the buffer, but we may still need to scroll up to make
183        // room for the viewport.
184        //
185        // We want to scroll up the exact amount that will leave us completely filling the screen.
186        // However, it's possible that the viewport didn't start on the bottom of the screen and
187        // the added lines weren't enough to push it all the way to the bottom. We deal with this
188        // case by just ensuring that our scroll amount is non-negative.
189        //
190        // We want:
191        //   screen_height = drawn_height - scroll_up + buffer_height + viewport_height
192        // Or, equivalently:
193        //   scroll_up = drawn_height + buffer_height + viewport_height - screen_height
194        let scroll_up = 0.max(drawn_height + buffer_height + viewport_height - screen_height);
195        self.scroll_up(scroll_up as u16)?;
196        self.draw_lines(
197            (drawn_height - scroll_up) as u16,
198            buffer_height as u16,
199            buffer,
200        )?;
201        drawn_height += buffer_height - scroll_up;
202
203        self.set_viewport_area(Rect {
204            y: drawn_height as u16,
205            ..self.viewport_area
206        });
207
208        // Clear the viewport off the screen. We didn't clear earlier for two reasons. First, it
209        // wasn't necessary because the buffer we drew out of isn't sparse, so it overwrote
210        // whatever was on the screen. Second, there is a weird bug with tmux where a full screen
211        // clear plus immediate scrolling causes some garbage to go into the scrollback.
212        self.clear()?;
213
214        Ok(())
215    }
216
217    /// Implement `Self::insert_before` using scrolling regions.
218    ///
219    /// If a terminal supports scrolling regions, it means that we can define a subset of rows of
220    /// the screen, and then tell the terminal to scroll up or down just within that region. The
221    /// rows outside of the region are not affected.
222    ///
223    /// This function utilizes this feature to avoid having to redraw the viewport. This is done
224    /// either by splitting the screen at the top of the viewport, and then creating a gap by
225    /// either scrolling the viewport down, or scrolling the area above it up. The lines to insert
226    /// are then drawn into the gap created.
227    #[cfg(feature = "scrolling-regions")]
228    fn insert_before_scrolling_regions(
229        &mut self,
230        mut height: u16,
231        draw_fn: impl FnOnce(&mut Buffer),
232    ) -> Result<(), B::Error> {
233        let area = Rect {
234            x: 0,
235            y: 0,
236            width: self.viewport_area.width,
237            height,
238        };
239        let mut buffer = Buffer::empty(area);
240        draw_fn(&mut buffer);
241        let mut buffer = buffer.content.as_slice();
242
243        // Handle the special case where the viewport takes up the whole screen.
244        if self.viewport_area.height == self.last_known_area.height {
245            // "Borrow" the top line of the viewport. Draw over it, then immediately scroll it into
246            // scrollback. Do this repeatedly until the whole buffer has been put into scrollback.
247            let mut first = true;
248            while !buffer.is_empty() {
249                buffer = if first {
250                    self.draw_lines(0, 1, buffer)?
251                } else {
252                    self.draw_lines_over_cleared(0, 1, buffer)?
253                };
254                first = false;
255                self.backend.scroll_region_up(0..1, 1)?;
256            }
257
258            // Redraw the top line of the viewport.
259            let width = self.viewport_area.width as usize;
260            let top_line = self.buffers[1 - self.current].content[0..width].to_vec();
261            self.draw_lines_over_cleared(0, 1, &top_line)?;
262            return Ok(());
263        }
264
265        // Handle the case where the viewport isn't yet at the bottom of the screen.
266        {
267            let viewport_top = self.viewport_area.top();
268            let viewport_bottom = self.viewport_area.bottom();
269            let screen_bottom = self.last_known_area.bottom();
270            if viewport_bottom < screen_bottom {
271                let to_draw = height.min(screen_bottom - viewport_bottom);
272                self.backend
273                    .scroll_region_down(viewport_top..viewport_bottom + to_draw, to_draw)?;
274                buffer = self.draw_lines_over_cleared(viewport_top, to_draw, buffer)?;
275                self.set_viewport_area(Rect {
276                    y: viewport_top + to_draw,
277                    ..self.viewport_area
278                });
279                height -= to_draw;
280            }
281        }
282
283        let viewport_top = self.viewport_area.top();
284        while height > 0 {
285            let to_draw = height.min(viewport_top);
286            self.backend.scroll_region_up(0..viewport_top, to_draw)?;
287            buffer = self.draw_lines_over_cleared(viewport_top - to_draw, to_draw, buffer)?;
288            height -= to_draw;
289        }
290
291        Ok(())
292    }
293
294    /// Draw lines at the given vertical offset. The slice of cells must contain enough cells
295    /// for the requested lines. A slice of the unused cells are returned.
296    ///
297    /// This is a small internal helper used by [`Terminal::insert_before`]. It writes cells
298    /// directly to the backend in terminal coordinates (not viewport coordinates).
299    fn draw_lines<'a>(
300        &mut self,
301        y_offset: u16,
302        lines_to_draw: u16,
303        cells: &'a [Cell],
304    ) -> Result<&'a [Cell], B::Error> {
305        let width: usize = self.last_known_area.width.into();
306        let (to_draw, remainder) = cells.split_at(width * lines_to_draw as usize);
307        if lines_to_draw > 0 {
308            let iter = to_draw
309                .iter()
310                .enumerate()
311                .map(|(i, c)| ((i % width) as u16, y_offset + (i / width) as u16, c));
312            self.backend.draw(iter)?;
313            self.backend.flush()?;
314        }
315        Ok(remainder)
316    }
317
318    /// Draw lines at the given vertical offset, assuming that the lines they are replacing on the
319    /// screen are cleared. The slice of cells must contain enough cells for the requested lines. A
320    /// slice of the unused cells are returned.
321    ///
322    /// This is used by the `scrolling-regions` implementation of [`Terminal::insert_before`] to
323    /// avoid relying on a full-screen clear while updating only part of the terminal.
324    #[cfg(feature = "scrolling-regions")]
325    fn draw_lines_over_cleared<'a>(
326        &mut self,
327        y_offset: u16,
328        lines_to_draw: u16,
329        cells: &'a [Cell],
330    ) -> Result<&'a [Cell], B::Error> {
331        let width: usize = self.last_known_area.width.into();
332        let (to_draw, remainder) = cells.split_at(width * lines_to_draw as usize);
333        if lines_to_draw > 0 {
334            let area = Rect::new(0, y_offset, width as u16, y_offset + lines_to_draw);
335            let old = Buffer::empty(area);
336            let new = Buffer {
337                area,
338                content: to_draw.to_vec(),
339            };
340            self.backend.draw(old.diff_iter(&new))?;
341            self.backend.flush()?;
342        }
343        Ok(remainder)
344    }
345
346    /// Scroll the whole screen up by the given number of lines.
347    ///
348    /// This is used by [`Terminal::insert_before`] when the `scrolling-regions` feature is
349    /// disabled.
350    /// It scrolls by moving the cursor to the last row and calling [`Backend::append_lines`].
351    #[cfg(not(feature = "scrolling-regions"))]
352    fn scroll_up(&mut self, lines_to_scroll: u16) -> Result<(), B::Error> {
353        if lines_to_scroll > 0 {
354            self.set_cursor_position(Position::new(
355                0,
356                self.last_known_area.height.saturating_sub(1),
357            ))?;
358            self.backend.append_lines(lines_to_scroll)?;
359        }
360        Ok(())
361    }
362}
363
364/// Compute the on-screen area for an inline viewport.
365///
366/// This helper is used by [`Terminal::with_options`] (initialization) and [`Terminal::resize`]
367/// (after a terminal resize) to translate `Viewport::Inline(height)` into a concrete [`Rect`].
368///
369/// This returns the computed viewport area and the cursor position observed at the start of the
370/// call.
371///
372/// Inline viewports always start at column 0, span the full terminal width, and are anchored to the
373/// backend cursor row at the time of the call. The requested height is clamped to the current
374/// terminal height.
375///
376/// Ratatui reserves vertical space for the requested height by calling [`Backend::append_lines`].
377/// If the cursor is close enough to the bottom that appending would run past the last row,
378/// terminals scroll; in that case we shift the computed `y` upward by the number of rows scrolled
379/// so the viewport remains fully visible.
380///
381/// `offset_in_previous_viewport` is used by [`Terminal::resize`] to keep the cursor at the same
382/// relative row within the viewport across resizes.
383///
384/// Related viewport code lives in:
385///
386/// - [`Terminal::with_options`] (selects the viewport and computes the initial area)
387/// - [`Terminal::autoresize`] (detects backend size changes during [`Terminal::draw`] /
388///   [`Terminal::try_draw`])
389/// - [`Terminal::resize`] (recomputes the viewport and clears before the next draw)
390pub(crate) fn compute_inline_size<B: Backend>(
391    backend: &mut B,
392    height: u16,
393    size: Size,
394    offset_in_previous_viewport: u16,
395) -> Result<(Rect, Position), B::Error> {
396    let pos = backend.get_cursor_position()?;
397    let mut row = pos.y;
398
399    let max_height = size.height.min(height);
400
401    let lines_after_cursor = height
402        .saturating_sub(offset_in_previous_viewport)
403        .saturating_sub(1);
404
405    backend.append_lines(lines_after_cursor)?;
406
407    let available_lines = size.height.saturating_sub(row).saturating_sub(1);
408    let missing_lines = lines_after_cursor.saturating_sub(available_lines);
409    if missing_lines > 0 {
410        row = row.saturating_sub(missing_lines);
411    }
412    row = row.saturating_sub(offset_in_previous_viewport);
413
414    Ok((
415        Rect {
416            x: 0,
417            y: row,
418            width: size.width,
419            height: max_height,
420        },
421        pos,
422    ))
423}
424
425#[cfg(test)]
426mod tests {
427    use crate::backend::{Backend, TestBackend};
428    use crate::layout::{Position, Rect, Size};
429    use crate::style::Style;
430    use crate::terminal::inline::compute_inline_size;
431    use crate::terminal::{Terminal, TerminalOptions, Viewport};
432
433    #[test]
434    fn compute_inline_size_uses_cursor_offset_when_space_available() {
435        // Diagram (terminal height = 10, requested viewport height = 4):
436        //
437        // Cursor at y=6, previous cursor offset within viewport = 1.
438        //
439        // Before (conceptually):
440        //   0
441        //   1
442        //   2
443        //   3
444        //   4
445        //   5  <- viewport top (expected)
446        //   6  <- cursor row (observed_pos.y)
447        //   7
448        //   8
449        //   9
450        //
451        // After: viewport top y = 5 (6 - 1), height = 4 => rows 5..9 (exclusive).
452        let mut backend = TestBackend::new(10, 10);
453        backend
454            .set_cursor_position(Position { x: 0, y: 6 })
455            .unwrap();
456
457        let (area, observed_pos) =
458            compute_inline_size(&mut backend, 4, Size::new(10, 10), 1).unwrap();
459
460        assert_eq!(observed_pos, Position { x: 0, y: 6 });
461        assert_eq!(area, Rect::new(0, 5, 10, 4));
462    }
463
464    #[test]
465    fn compute_inline_size_saturates_when_offset_exceeds_cursor_row() {
466        // Diagram (terminal height = 10, requested viewport height = 4):
467        //
468        // Cursor at y=0, previous cursor offset within viewport = 5 (nonsensical but possible if
469        // callers pass a stale/oversized offset).
470        //
471        // We saturate so the computed viewport top cannot go negative:
472        //   top = cursor_y.saturating_sub(offset) = 0.saturating_sub(5) = 0
473        //
474        // Expected viewport area:
475        //   y=0..4 (fully pinned to the top)
476        let mut backend = TestBackend::new(10, 10);
477        backend
478            .set_cursor_position(Position { x: 0, y: 0 })
479            .unwrap();
480
481        let (area, _observed_pos) =
482            compute_inline_size(&mut backend, 4, Size::new(10, 10), 5).unwrap();
483
484        assert_eq!(area, Rect::new(0, 0, 10, 4));
485    }
486
487    #[cfg(not(feature = "scrolling-regions"))]
488    mod no_scrolling_regions {
489        use super::*;
490
491        #[test]
492        fn insert_before_is_noop_for_non_inline_viewports() {
493            // Diagram:
494            //
495            // Viewport is fullscreen (not inline), so insert_before() is a no-op.
496            //
497            // Screen before:
498            //   x..
499            //   ...
500            //
501            // Screen after:
502            //   x..
503            //   ...
504            let mut terminal = Terminal::new(TestBackend::new(3, 2)).unwrap();
505            {
506                let frame = terminal.get_frame();
507                frame.buffer[(0, 0)].set_symbol("x");
508            }
509            terminal.flush().unwrap();
510
511            let viewport_area = terminal.viewport_area;
512            terminal
513                .insert_before(1, |buf| {
514                    buf.set_string(0, 0, "zzz", Style::default());
515                })
516                .unwrap();
517
518            assert_eq!(terminal.viewport_area, viewport_area);
519            terminal.backend().assert_buffer_lines(["x  ", "   "]);
520        }
521
522        #[test]
523        fn insert_before_pushes_viewport_down_when_space_available() {
524            // Diagram (screen height = 10, viewport height = 4, cursor row = 3):
525            //
526            // Before:
527            //   0: 0000000000
528            //   1: 1111111111
529            //   2: 2222222222
530            //   3: [viewport top] 3333333333
531            //   4:               4444444444
532            //   5:               5555555555
533            //   6:               6666666666
534            //   7: 7777777777
535            //   8: 8888888888
536            //   9: 9999999999
537            //
538            // After inserting 1 line above an inline viewport (no scrolling regions):
539            // - A line is drawn at the old viewport top (y=3)
540            // - The viewport moves down by 1 row (new top y=4)
541            // - The viewport is cleared so it will be redrawn on the next draw()
542            let mut backend = TestBackend::with_lines([
543                "0000000000",
544                "1111111111",
545                "2222222222",
546                "3333333333",
547                "4444444444",
548                "5555555555",
549                "6666666666",
550                "7777777777",
551                "8888888888",
552                "9999999999",
553            ]);
554            backend
555                .set_cursor_position(Position { x: 0, y: 3 })
556                .unwrap();
557            let mut terminal = Terminal::with_options(
558                backend,
559                TerminalOptions {
560                    viewport: Viewport::Inline(4),
561                },
562            )
563            .unwrap();
564
565            terminal
566                .insert_before(1, |buf| {
567                    buf.set_string(0, 0, "INSERTLINE", Style::default());
568                })
569                .unwrap();
570
571            assert_eq!(terminal.viewport_area, Rect::new(0, 4, 10, 4));
572            terminal.backend().assert_buffer_lines([
573                "0000000000",
574                "1111111111",
575                "2222222222",
576                "INSERTLINE",
577                "          ",
578                "          ",
579                "          ",
580                "          ",
581                "          ",
582                "          ",
583            ]);
584        }
585
586        #[test]
587        fn insert_before_scrolls_when_viewport_is_at_bottom() {
588            // Diagram (screen height = 10, viewport height = 4, cursor row = 6):
589            //
590            // Before:
591            //   0: 0000000000
592            //   1: 1111111111
593            //   2: 2222222222
594            //   3: 3333333333
595            //   4: 4444444444
596            //   5: 5555555555
597            //   6: [viewport top] 6666666666
598            //   7:               7777777777
599            //   8:               8888888888
600            //   9:               9999999999
601            //
602            // After inserting 2 lines:
603            // - The area above the viewport scrolls up to make room
604            // - Inserted lines appear immediately above the viewport
605            // - The viewport is cleared so it will be redrawn on the next draw()
606            let mut backend = TestBackend::with_lines([
607                "0000000000",
608                "1111111111",
609                "2222222222",
610                "3333333333",
611                "4444444444",
612                "5555555555",
613                "6666666666",
614                "7777777777",
615                "8888888888",
616                "9999999999",
617            ]);
618            backend
619                .set_cursor_position(Position { x: 0, y: 6 })
620                .unwrap();
621            let mut terminal = Terminal::with_options(
622                backend,
623                TerminalOptions {
624                    viewport: Viewport::Inline(4),
625                },
626            )
627            .unwrap();
628
629            terminal
630                .insert_before(2, |buf| {
631                    buf.set_string(0, 0, "INSERTED1", Style::default());
632                    buf.set_string(0, 1, "INSERTED2", Style::default());
633                })
634                .unwrap();
635
636            assert_eq!(terminal.viewport_area, Rect::new(0, 6, 10, 4));
637            terminal.backend().assert_buffer_lines([
638                "2222222222",
639                "3333333333",
640                "4444444444",
641                "5555555555",
642                "INSERTED1 ",
643                "INSERTED2 ",
644                "          ",
645                "          ",
646                "          ",
647                "          ",
648            ]);
649        }
650
651        #[test]
652        fn insert_before_then_draw_repaints_cleared_viewport() {
653            // Diagram (screen height = 10, viewport height = 4, cursor row = 6):
654            //
655            // 1) Draw a frame into the inline viewport at the bottom:
656            //   6..9: AAAAAAAAAA
657            //
658            // 2) Insert 2 lines above the viewport:
659            //   - Inserts appear at rows 4..5
660            //   - Viewport is cleared (so it is blank on-screen until the next draw)
661            //
662            // 3) Draw again:
663            //   6..9: BBBBBBBBBB
664            //
665            // Expected final screen:
666            //   4: INSERTED00
667            //   5: INSERTED01
668            //   6..9: BBBBBBBBBB
669            let mut backend = TestBackend::new(10, 10);
670            backend
671                .set_cursor_position(Position { x: 0, y: 6 })
672                .unwrap();
673            let mut terminal = Terminal::with_options(
674                backend,
675                TerminalOptions {
676                    viewport: Viewport::Inline(4),
677                },
678            )
679            .unwrap();
680
681            terminal
682                .draw(|frame| {
683                    let area = frame.area();
684                    for y in area.top()..area.bottom() {
685                        frame
686                            .buffer
687                            .set_string(area.x, y, "AAAAAAAAAA", Style::default());
688                    }
689                })
690                .unwrap();
691
692            terminal
693                .insert_before(2, |buf| {
694                    buf.set_string(0, 0, "INSERTED00", Style::default());
695                    buf.set_string(0, 1, "INSERTED01", Style::default());
696                })
697                .unwrap();
698
699            terminal
700                .draw(|frame| {
701                    let area = frame.area();
702                    for y in area.top()..area.bottom() {
703                        frame
704                            .buffer
705                            .set_string(area.x, y, "BBBBBBBBBB", Style::default());
706                    }
707                })
708                .unwrap();
709
710            terminal.backend().assert_buffer_lines([
711                "          ",
712                "          ",
713                "          ",
714                "          ",
715                "INSERTED00",
716                "INSERTED01",
717                "BBBBBBBBBB",
718                "BBBBBBBBBB",
719                "BBBBBBBBBB",
720                "BBBBBBBBBB",
721            ]);
722        }
723    }
724
725    #[cfg(feature = "scrolling-regions")]
726    mod scrolling_regions {
727        use super::*;
728
729        #[test]
730        fn insert_before_moves_viewport_down_without_clearing() {
731            // Diagram (screen height = 10, viewport height = 4, cursor row = 3):
732            //
733            // With scrolling regions enabled, we can create a gap and draw the inserted line
734            // without clearing the viewport content.
735            //
736            // Before:
737            //   2: 2222222222
738            //   3: [viewport top] 3333333333
739            //   4:               4444444444
740            //
741            // After:
742            //   3: INSERTLINE
743            //   4: 3333333333  (viewport content preserved)
744            let mut backend = TestBackend::with_lines([
745                "0000000000",
746                "1111111111",
747                "2222222222",
748                "3333333333",
749                "4444444444",
750                "5555555555",
751                "6666666666",
752                "7777777777",
753                "8888888888",
754                "9999999999",
755            ]);
756            backend
757                .set_cursor_position(Position { x: 0, y: 3 })
758                .unwrap();
759            let mut terminal = Terminal::with_options(
760                backend,
761                TerminalOptions {
762                    viewport: Viewport::Inline(4),
763                },
764            )
765            .unwrap();
766
767            terminal
768                .insert_before(1, |buf| {
769                    buf.set_string(0, 0, "INSERTLINE", Style::default());
770                })
771                .unwrap();
772
773            assert_eq!(terminal.viewport_area, Rect::new(0, 4, 10, 4));
774            terminal.backend().assert_buffer_lines([
775                "0000000000",
776                "1111111111",
777                "2222222222",
778                "INSERTLINE",
779                "3333333333",
780                "4444444444",
781                "5555555555",
782                "6666666666",
783                "8888888888",
784                "9999999999",
785            ]);
786        }
787
788        #[test]
789        fn insert_before_when_viewport_is_at_bottom_preserves_viewport() {
790            // Diagram (screen height = 10, viewport height = 4, viewport top = 6):
791            //
792            // With scrolling regions enabled and the viewport already at the bottom:
793            // - The region above the viewport (rows 0..6) scrolls up to make room.
794            // - Inserted lines are drawn into the cleared space immediately above the viewport.
795            // - The viewport itself is not cleared and stays on-screen.
796            //
797            // Before (after drawing V into the viewport):
798            //   0: 0000000000
799            //   1: 1111111111
800            //   2: 2222222222
801            //   3: 3333333333
802            //   4: 4444444444
803            //   5: 5555555555
804            //   6..9: VVVVVVVVVV
805            //
806            // After inserting 2 lines:
807            //   0..3: previous 2..5
808            //   4: AAAAAAAAAA
809            //   5: BBBBBBBBBB
810            //   6..9: VVVVVVVVVV
811            //
812            // The scrolled-off lines are appended to scrollback (previous 0 and 1).
813            let mut backend = TestBackend::with_lines([
814                "0000000000",
815                "1111111111",
816                "2222222222",
817                "3333333333",
818                "4444444444",
819                "5555555555",
820                "6666666666",
821                "7777777777",
822                "8888888888",
823                "9999999999",
824            ]);
825            backend
826                .set_cursor_position(Position { x: 0, y: 6 })
827                .unwrap();
828            let mut terminal = Terminal::with_options(
829                backend,
830                TerminalOptions {
831                    viewport: Viewport::Inline(4),
832                },
833            )
834            .unwrap();
835
836            terminal
837                .draw(|frame| {
838                    let area = frame.area();
839                    for y in area.top()..area.bottom() {
840                        frame
841                            .buffer
842                            .set_string(area.x, y, "VVVVVVVVVV", Style::default());
843                    }
844                })
845                .unwrap();
846
847            terminal
848                .insert_before(2, |buf| {
849                    buf.set_string(0, 0, "AAAAAAAAAA", Style::default());
850                    buf.set_string(0, 1, "BBBBBBBBBB", Style::default());
851                })
852                .unwrap();
853
854            terminal.backend().assert_buffer_lines([
855                "2222222222",
856                "3333333333",
857                "4444444444",
858                "5555555555",
859                "AAAAAAAAAA",
860                "BBBBBBBBBB",
861                "VVVVVVVVVV",
862                "VVVVVVVVVV",
863                "VVVVVVVVVV",
864                "VVVVVVVVVV",
865            ]);
866            terminal
867                .backend()
868                .assert_scrollback_lines(["0000000000", "1111111111"]);
869        }
870
871        #[test]
872        fn insert_before_when_viewport_is_fullscreen_appends_to_scrollback() {
873            // Diagram (screen height = 4, viewport height = 4):
874            //
875            // When the viewport takes the whole screen, there is no visible "area above" it.
876            // The scrolling-regions implementation handles this by repeatedly:
877            // - drawing one line over the top row
878            // - immediately scrolling that row into scrollback
879            //
880            // The viewport content stays on-screen; inserted lines end up in scrollback.
881            let mut backend = TestBackend::new(10, 4);
882            backend
883                .set_cursor_position(Position { x: 0, y: 0 })
884                .unwrap();
885            let mut terminal = Terminal::with_options(
886                backend,
887                TerminalOptions {
888                    viewport: Viewport::Inline(4),
889                },
890            )
891            .unwrap();
892
893            terminal
894                .draw(|frame| {
895                    let area = frame.area();
896                    frame
897                        .buffer
898                        .set_string(area.x, area.y, "VIEWLINE00", Style::default());
899                    frame
900                        .buffer
901                        .set_string(area.x, area.y + 1, "VIEWLINE01", Style::default());
902                    frame
903                        .buffer
904                        .set_string(area.x, area.y + 2, "VIEWLINE02", Style::default());
905                    frame
906                        .buffer
907                        .set_string(area.x, area.y + 3, "VIEWLINE03", Style::default());
908                })
909                .unwrap();
910
911            terminal
912                .insert_before(2, |buf| {
913                    buf.set_string(0, 0, "INSERTED00", Style::default());
914                    buf.set_string(0, 1, "INSERTED01", Style::default());
915                })
916                .unwrap();
917
918            terminal.backend().assert_buffer_lines([
919                "VIEWLINE00",
920                "VIEWLINE01",
921                "VIEWLINE02",
922                "VIEWLINE03",
923            ]);
924            terminal
925                .backend()
926                .assert_scrollback_lines(["INSERTED00", "INSERTED01"]);
927        }
928    }
929}