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}