//! The active screen as a ring buffer: origin arithmetic, row addressing, //! blanking, and the resize that unrolls it into a fresh buffer. //! //! A scroll moves the origin rather than the rows, so every reader that wants //! visible row `r` goes through [`Grid::phys_row`] to find the physical one. use crate::{Cell, Grid}; impl Grid { pub(crate) fn active_cells(&self) -> &[Cell] { if self.on_alt { &self.alt } else { &self.main } } pub(crate) fn active_cells_mut(&mut self) -> &mut [Cell] { if self.on_alt { &mut self.alt } else { &mut self.main } } pub(crate) fn active_origin(&self) -> u16 { if self.on_alt { self.alt_origin } else { self.main_origin } } /// Physical row index backing logical row `r`. pub(crate) fn phys_row(&self, r: u16) -> u16 { let phys = if self.is_partial_region() && r >= self.scroll_top && r <= self.scroll_bottom { // In partial region: active_origin is guaranteed 0 by unroll on // transition, so we rotate only within the region. let region_size = (self.scroll_bottom - self.scroll_top + 1) as u32; let region_r = (r - self.scroll_top) as u32; let phys_in_region = (self.region_origin as u32 + region_r) % region_size; self.scroll_top as u32 + phys_in_region } else { (self.active_origin() as u32 + r as u32) % self.rows as u32 }; phys as u16 } /// Physical byte offset for the start of logical row `r`. pub(crate) fn row_start(&self, r: u16) -> usize { self.phys_row(r) as usize * self.cols as usize } fn active_wrapped_mut(&mut self) -> &mut [bool] { if self.on_alt { &mut self.alt_wrapped } else { &mut self.main_wrapped } } /// Does logical row `r` continue onto row `r + 1`? /// /// True only when the shell's output ran off the right edge, so a copy /// spanning the two rows should join them without a newline. A row that /// filled exactly and then got an explicit CR/LF reads false. pub fn row_wrapped(&self, r: u16) -> bool { if let Some(h) = self.history_row(r) { return h.wrapped; } let phys = self.phys_row(self.live_row(r)) as usize; let flags = if self.on_alt { &self.alt_wrapped } else { &self.main_wrapped }; flags.get(phys).copied().unwrap_or(false) } pub(crate) fn set_row_wrapped(&mut self, r: u16, wrapped: bool) { let phys = self.phys_row(r) as usize; if let Some(slot) = self.active_wrapped_mut().get_mut(phys) { *slot = wrapped; } } pub(crate) fn is_partial_region(&self) -> bool { self.scroll_top != 0 || self.scroll_bottom != self.rows - 1 } /// Rotate the active screen's cells so `active_origin` becomes 0. /// Cheap: one `slice::rotate_left`. Called before entering partial-region /// mode so rows outside the region are at logical=physical positions. pub(crate) fn unroll_active_ring(&mut self) { let origin = self.active_origin(); if origin == 0 { return; } let cols = self.cols as usize; let cells = self.active_cells_mut(); cells.rotate_left(origin as usize * cols); self.active_wrapped_mut().rotate_left(origin as usize); if self.on_alt { self.alt_origin = 0; } else { self.main_origin = 0; } } /// Rotate the current partial region so `region_origin` becomes 0. /// Called before exiting partial-region mode (or entering a different /// region) so region contents are back at logical positions. pub(crate) fn unroll_region(&mut self) { if self.region_origin == 0 { return; } let cols = self.cols as usize; let top = self.scroll_top as usize; let region_rows = (self.scroll_bottom - self.scroll_top + 1) as usize; let region_len = region_rows * cols; let shift = self.region_origin as usize; let cells = self.active_cells_mut(); cells[top * cols..top * cols + region_len].rotate_left(shift * cols); self.active_wrapped_mut()[top..top + region_rows].rotate_left(shift); self.region_origin = 0; } /// Advance the active screen's ring origin. Positive `n` = scroll up /// (logical row 0 shows what was logical row `n`); negative = scroll /// down. Blanking of newly-exposed rows is the caller's job. pub(crate) fn advance_origin(&mut self, n: i32) { let rows = self.rows as i32; let origin = if self.on_alt { &mut self.alt_origin } else { &mut self.main_origin }; let new = (*origin as i32 + n).rem_euclid(rows); *origin = new as u16; } /// Zero one physical row's cells. pub(crate) fn blank_physical_row(&mut self, phys: u16) { let cols = self.cols as usize; let start = phys as usize * cols; let cells = self.active_cells_mut(); for cell in &mut cells[start..start + cols] { *cell = Cell::default(); } if let Some(slot) = self.active_wrapped_mut().get_mut(phys as usize) { *slot = false; } } /// Zero one logical row's cells. pub(crate) fn blank_logical_row(&mut self, r: u16) { let cols = self.cols as usize; let start = self.row_start(r); let cells = self.active_cells_mut(); for cell in &mut cells[start..start + cols] { *cell = Cell::default(); } self.set_row_wrapped(r, false); } /// Resize the grid, preserving as much of the top-left of the live screen /// as fits and rewrapping scrollback to the new width. /// /// The live screen is truncated, not reflowed: it is whatever an /// application last painted, and it is about to be told the new size and /// repaint. History has no one to repaint it, so it is rewrapped — see /// [`Grid::rewrap_history`]. pub fn resize(&mut self, cols: u16, rows: u16) { let cols = cols.max(1); let rows = rows.max(1); if cols == self.cols && rows == self.rows { return; } let old_cols = self.cols; self.main = resize_buf( &self.main, self.main_origin, self.cols, self.rows, cols, rows, ); self.alt = resize_buf(&self.alt, self.alt_origin, self.cols, self.rows, cols, rows); // The live screen is clipped rather than reflowed, so every recorded // wrap point on it is now a lie about where the text runs off the edge. // Drop them all rather than carry wrong ones into a copy. History keeps // its flags: the rewrap is what makes them true again. self.main_wrapped = vec![false; rows as usize]; self.alt_wrapped = vec![false; rows as usize]; // The viewport survives a height change, but it cannot point further // back than history goes. self.view_offset = self.view_offset.min(self.history_len_u16()); self.main_origin = 0; self.alt_origin = 0; self.region_origin = 0; self.cols = cols; self.rows = rows; if cols != old_cols { self.rewrap_history(old_cols); } self.scroll_top = 0; self.scroll_bottom = rows - 1; self.cursor.row = self.cursor.row.min(rows - 1); self.cursor.col = self.cursor.col.min(cols - 1); self.cursor.wrap_next = false; // Resize invalidates any per-row cache; caller wipes on receipt. self.row_dirty = vec![true; rows as usize]; self.pending_resize = true; self.pending_scroll = 0; self.invalidate_cur_row(); } } fn resize_buf( old: &[Cell], old_origin: u16, old_cols: u16, old_rows: u16, new_cols: u16, new_rows: u16, ) -> Vec { let mut new = vec![Cell::default(); new_cols as usize * new_rows as usize]; let copy_cols = old_cols.min(new_cols) as usize; let copy_rows = old_rows.min(new_rows) as usize; for r in 0..copy_rows { // Ring-map the old logical row to its physical offset. let src_phys = (old_origin as u32 + r as u32) % old_rows as u32; let src_start = src_phys as usize * old_cols as usize; let dst_start = r * new_cols as usize; new[dst_start..dst_start + copy_cols] .copy_from_slice(&old[src_start..src_start + copy_cols]); // Narrowing can cut a wide character in half at the new right edge. // The live screen is about to be repainted at the new size anyway, so // the lead is simply dropped rather than carried as half a character. if let Some(last) = new[dst_start..dst_start + copy_cols].last_mut() && last.is_wide() { *last = Cell::default(); } } new } #[cfg(test)] mod tests { use crate::testutil::{assert_cursor, feed, row_str}; use crate::*; // ---- resize -------------------------------------------------------- #[test] fn resize_grow_preserves_top_left() { let mut g = Grid::new(4, 2); feed(&mut g, b"AB\r\nCD"); g.resize(6, 3); assert_eq!(row_str(&g, 0), "AB"); assert_eq!(row_str(&g, 1), "CD"); } #[test] fn resize_shrink_truncates() { let mut g = Grid::new(6, 3); feed(&mut g, b"ABCDEF\r\nGHIJKL\r\nMNOPQR"); g.resize(3, 2); assert_eq!(row_str(&g, 0), "ABC"); assert_eq!(row_str(&g, 1), "GHI"); } #[test] fn resize_clamps_cursor() { let mut g = Grid::new(10, 5); feed(&mut g, b"\x1b[5;10H"); // (4, 9) assert_cursor(&g, 4, 9); g.resize(4, 2); let c = g.cursor(); assert!(c.row < 2 && c.col < 4); } // ---- wrapped-row flag ---------------------------------------------- #[test] fn deferred_wrap_marks_the_row_it_left() { let mut g = Grid::new(4, 3); feed(&mut g, b"abcdef"); assert!(g.row_wrapped(0)); assert!(!g.row_wrapped(1)); } #[test] fn filling_a_row_exactly_does_not_mark_it_wrapped() { let mut g = Grid::new(4, 3); feed(&mut g, b"abcd"); assert!(!g.row_wrapped(0), "wrap_next alone is not a wrap"); feed(&mut g, b"\r\nefgh"); assert!(!g.row_wrapped(0)); } #[test] fn the_wrapped_flag_rides_the_scroll_ring() { let mut g = Grid::new(4, 3); // "abcd" wraps onto "ef", one row down from the top. feed(&mut g, b"xy\r\nabcdef"); assert!(g.row_wrapped(1)); // Scrolling carries the wrapped row up to row 0 — the flag is indexed // physically, so it has to arrive with it. feed(&mut g, b"\r\n"); assert_eq!(row_str(&g, 0), "abcd"); assert!(g.row_wrapped(0)); // One more scroll and it leaves the screen entirely. feed(&mut g, b"\r\n"); assert_eq!(row_str(&g, 0), "ef"); assert!(!g.row_wrapped(0), "the wrapped row scrolled off the top"); } #[test] fn a_blank_row_exposed_by_a_scroll_is_not_wrapped() { let mut g = Grid::new(4, 2); feed(&mut g, b"abcdef\r\n\r\n\r\n"); for r in 0..g.rows() { assert!(!g.row_wrapped(r), "row {r} came back wrapped"); } } #[test] fn resize_drops_every_wrap_point() { let mut g = Grid::new(4, 3); feed(&mut g, b"abcdef"); assert!(g.row_wrapped(0)); g.resize(8, 3); assert!(!g.row_wrapped(0), "the wrap point is meaningless at 8 cols"); } #[test] fn alt_screen_keeps_its_own_wrap_points() { let mut g = Grid::new(4, 3); feed(&mut g, b"abcdef"); feed(&mut g, b"\x1b[?1049h"); assert!(!g.row_wrapped(0), "alt screen starts clean"); feed(&mut g, b"\x1b[?1049l"); assert!(g.row_wrapped(0), "main screen's wrap point survived"); } // ---- erase against a rotated ring ---------------------------------- #[test] fn erase_line_targets_the_right_row_after_a_scroll() { // Scroll far enough that the ring origin is non-zero, then erase the // cursor's line. Erasing by logical row without the ring mapping // would blank some other row entirely. let mut g = Grid::new(6, 3); feed(&mut g, b"one\r\ntwo\r\nthree\r\nfour\r\nfive"); assert_eq!(row_str(&g, 0), "three"); assert_eq!(row_str(&g, 1), "four"); assert_eq!(row_str(&g, 2), "five"); feed(&mut g, b"\x1b[2;1H\x1b[2K"); // row 1, erase whole line assert_eq!(row_str(&g, 0), "three"); assert_eq!(row_str(&g, 1), ""); assert_eq!(row_str(&g, 2), "five"); } #[test] fn erase_to_end_of_line_targets_the_right_row_after_a_scroll() { let mut g = Grid::new(6, 3); feed(&mut g, b"one\r\ntwo\r\nthree\r\nfour\r\nfive"); feed(&mut g, b"\x1b[3;3H\x1b[K"); // row 2 col 2, erase to end assert_eq!(row_str(&g, 0), "three"); assert_eq!(row_str(&g, 1), "four"); assert_eq!(row_str(&g, 2), "fi"); } }