Skip to main content

max / shop

13.1 KB · 372 lines History Blame Raw
1 //! The active screen as a ring buffer: origin arithmetic, row addressing,
2 //! blanking, and the resize that unrolls it into a fresh buffer.
3 //!
4 //! A scroll moves the origin rather than the rows, so every reader that wants
5 //! visible row `r` goes through [`Grid::phys_row`] to find the physical one.
6
7 use crate::{Cell, Grid};
8
9 impl Grid {
10 pub(crate) fn active_cells(&self) -> &[Cell] {
11 if self.on_alt { &self.alt } else { &self.main }
12 }
13
14 pub(crate) fn active_cells_mut(&mut self) -> &mut [Cell] {
15 if self.on_alt {
16 &mut self.alt
17 } else {
18 &mut self.main
19 }
20 }
21
22 pub(crate) fn active_origin(&self) -> u16 {
23 if self.on_alt {
24 self.alt_origin
25 } else {
26 self.main_origin
27 }
28 }
29
30 /// Physical row index backing logical row `r`.
31 pub(crate) fn phys_row(&self, r: u16) -> u16 {
32 let phys = if self.is_partial_region() && r >= self.scroll_top && r <= self.scroll_bottom {
33 // In partial region: active_origin is guaranteed 0 by unroll on
34 // transition, so we rotate only within the region.
35 let region_size = (self.scroll_bottom - self.scroll_top + 1) as u32;
36 let region_r = (r - self.scroll_top) as u32;
37 let phys_in_region = (self.region_origin as u32 + region_r) % region_size;
38 self.scroll_top as u32 + phys_in_region
39 } else {
40 (self.active_origin() as u32 + r as u32) % self.rows as u32
41 };
42 phys as u16
43 }
44
45 /// Physical byte offset for the start of logical row `r`.
46 pub(crate) fn row_start(&self, r: u16) -> usize {
47 self.phys_row(r) as usize * self.cols as usize
48 }
49
50 fn active_wrapped_mut(&mut self) -> &mut [bool] {
51 if self.on_alt {
52 &mut self.alt_wrapped
53 } else {
54 &mut self.main_wrapped
55 }
56 }
57
58 /// Does logical row `r` continue onto row `r + 1`?
59 ///
60 /// True only when the shell's output ran off the right edge, so a copy
61 /// spanning the two rows should join them without a newline. A row that
62 /// filled exactly and then got an explicit CR/LF reads false.
63 pub fn row_wrapped(&self, r: u16) -> bool {
64 if let Some(h) = self.history_row(r) {
65 return h.wrapped;
66 }
67 let phys = self.phys_row(self.live_row(r)) as usize;
68 let flags = if self.on_alt {
69 &self.alt_wrapped
70 } else {
71 &self.main_wrapped
72 };
73 flags.get(phys).copied().unwrap_or(false)
74 }
75
76 pub(crate) fn set_row_wrapped(&mut self, r: u16, wrapped: bool) {
77 let phys = self.phys_row(r) as usize;
78 if let Some(slot) = self.active_wrapped_mut().get_mut(phys) {
79 *slot = wrapped;
80 }
81 }
82
83 pub(crate) fn is_partial_region(&self) -> bool {
84 self.scroll_top != 0 || self.scroll_bottom != self.rows - 1
85 }
86
87 /// Rotate the active screen's cells so `active_origin` becomes 0.
88 /// Cheap: one `slice::rotate_left`. Called before entering partial-region
89 /// mode so rows outside the region are at logical=physical positions.
90 pub(crate) fn unroll_active_ring(&mut self) {
91 let origin = self.active_origin();
92 if origin == 0 {
93 return;
94 }
95 let cols = self.cols as usize;
96 let cells = self.active_cells_mut();
97 cells.rotate_left(origin as usize * cols);
98 self.active_wrapped_mut().rotate_left(origin as usize);
99 if self.on_alt {
100 self.alt_origin = 0;
101 } else {
102 self.main_origin = 0;
103 }
104 }
105
106 /// Rotate the current partial region so `region_origin` becomes 0.
107 /// Called before exiting partial-region mode (or entering a different
108 /// region) so region contents are back at logical positions.
109 pub(crate) fn unroll_region(&mut self) {
110 if self.region_origin == 0 {
111 return;
112 }
113 let cols = self.cols as usize;
114 let top = self.scroll_top as usize;
115 let region_rows = (self.scroll_bottom - self.scroll_top + 1) as usize;
116 let region_len = region_rows * cols;
117 let shift = self.region_origin as usize;
118 let cells = self.active_cells_mut();
119 cells[top * cols..top * cols + region_len].rotate_left(shift * cols);
120 self.active_wrapped_mut()[top..top + region_rows].rotate_left(shift);
121 self.region_origin = 0;
122 }
123
124 /// Advance the active screen's ring origin. Positive `n` = scroll up
125 /// (logical row 0 shows what was logical row `n`); negative = scroll
126 /// down. Blanking of newly-exposed rows is the caller's job.
127 pub(crate) fn advance_origin(&mut self, n: i32) {
128 let rows = self.rows as i32;
129 let origin = if self.on_alt {
130 &mut self.alt_origin
131 } else {
132 &mut self.main_origin
133 };
134 let new = (*origin as i32 + n).rem_euclid(rows);
135 *origin = new as u16;
136 }
137
138 /// Zero one physical row's cells.
139 pub(crate) fn blank_physical_row(&mut self, phys: u16) {
140 let cols = self.cols as usize;
141 let start = phys as usize * cols;
142 let cells = self.active_cells_mut();
143 for cell in &mut cells[start..start + cols] {
144 *cell = Cell::default();
145 }
146 if let Some(slot) = self.active_wrapped_mut().get_mut(phys as usize) {
147 *slot = false;
148 }
149 }
150
151 /// Zero one logical row's cells.
152 pub(crate) fn blank_logical_row(&mut self, r: u16) {
153 let cols = self.cols as usize;
154 let start = self.row_start(r);
155 let cells = self.active_cells_mut();
156 for cell in &mut cells[start..start + cols] {
157 *cell = Cell::default();
158 }
159 self.set_row_wrapped(r, false);
160 }
161
162 /// Resize the grid, preserving as much of the top-left of the live screen
163 /// as fits and rewrapping scrollback to the new width.
164 ///
165 /// The live screen is truncated, not reflowed: it is whatever an
166 /// application last painted, and it is about to be told the new size and
167 /// repaint. History has no one to repaint it, so it is rewrapped — see
168 /// [`Grid::rewrap_history`].
169 pub fn resize(&mut self, cols: u16, rows: u16) {
170 let cols = cols.max(1);
171 let rows = rows.max(1);
172 if cols == self.cols && rows == self.rows {
173 return;
174 }
175 let old_cols = self.cols;
176 self.main = resize_buf(
177 &self.main,
178 self.main_origin,
179 self.cols,
180 self.rows,
181 cols,
182 rows,
183 );
184 self.alt = resize_buf(&self.alt, self.alt_origin, self.cols, self.rows, cols, rows);
185 // The live screen is clipped rather than reflowed, so every recorded
186 // wrap point on it is now a lie about where the text runs off the edge.
187 // Drop them all rather than carry wrong ones into a copy. History keeps
188 // its flags: the rewrap is what makes them true again.
189 self.main_wrapped = vec![false; rows as usize];
190 self.alt_wrapped = vec![false; rows as usize];
191 // The viewport survives a height change, but it cannot point further
192 // back than history goes.
193 self.view_offset = self.view_offset.min(self.history_len_u16());
194 self.main_origin = 0;
195 self.alt_origin = 0;
196 self.region_origin = 0;
197 self.cols = cols;
198 self.rows = rows;
199 if cols != old_cols {
200 self.rewrap_history(old_cols);
201 }
202 self.scroll_top = 0;
203 self.scroll_bottom = rows - 1;
204 self.cursor.row = self.cursor.row.min(rows - 1);
205 self.cursor.col = self.cursor.col.min(cols - 1);
206 self.cursor.wrap_next = false;
207 // Resize invalidates any per-row cache; caller wipes on receipt.
208 self.row_dirty = vec![true; rows as usize];
209 self.pending_resize = true;
210 self.pending_scroll = 0;
211 self.invalidate_cur_row();
212 }
213 }
214
215 fn resize_buf(
216 old: &[Cell],
217 old_origin: u16,
218 old_cols: u16,
219 old_rows: u16,
220 new_cols: u16,
221 new_rows: u16,
222 ) -> Vec<Cell> {
223 let mut new = vec![Cell::default(); new_cols as usize * new_rows as usize];
224 let copy_cols = old_cols.min(new_cols) as usize;
225 let copy_rows = old_rows.min(new_rows) as usize;
226 for r in 0..copy_rows {
227 // Ring-map the old logical row to its physical offset.
228 let src_phys = (old_origin as u32 + r as u32) % old_rows as u32;
229 let src_start = src_phys as usize * old_cols as usize;
230 let dst_start = r * new_cols as usize;
231 new[dst_start..dst_start + copy_cols]
232 .copy_from_slice(&old[src_start..src_start + copy_cols]);
233 // Narrowing can cut a wide character in half at the new right edge.
234 // The live screen is about to be repainted at the new size anyway, so
235 // the lead is simply dropped rather than carried as half a character.
236 if let Some(last) = new[dst_start..dst_start + copy_cols].last_mut()
237 && last.is_wide()
238 {
239 *last = Cell::default();
240 }
241 }
242 new
243 }
244
245 #[cfg(test)]
246 mod tests {
247 use crate::testutil::{assert_cursor, feed, row_str};
248 use crate::*;
249
250 // ---- resize --------------------------------------------------------
251
252 #[test]
253 fn resize_grow_preserves_top_left() {
254 let mut g = Grid::new(4, 2);
255 feed(&mut g, b"AB\r\nCD");
256 g.resize(6, 3);
257 assert_eq!(row_str(&g, 0), "AB");
258 assert_eq!(row_str(&g, 1), "CD");
259 }
260
261 #[test]
262 fn resize_shrink_truncates() {
263 let mut g = Grid::new(6, 3);
264 feed(&mut g, b"ABCDEF\r\nGHIJKL\r\nMNOPQR");
265 g.resize(3, 2);
266 assert_eq!(row_str(&g, 0), "ABC");
267 assert_eq!(row_str(&g, 1), "GHI");
268 }
269
270 #[test]
271 fn resize_clamps_cursor() {
272 let mut g = Grid::new(10, 5);
273 feed(&mut g, b"\x1b[5;10H"); // (4, 9)
274 assert_cursor(&g, 4, 9);
275 g.resize(4, 2);
276 let c = g.cursor();
277 assert!(c.row < 2 && c.col < 4);
278 }
279
280 // ---- wrapped-row flag ----------------------------------------------
281
282 #[test]
283 fn deferred_wrap_marks_the_row_it_left() {
284 let mut g = Grid::new(4, 3);
285 feed(&mut g, b"abcdef");
286 assert!(g.row_wrapped(0));
287 assert!(!g.row_wrapped(1));
288 }
289
290 #[test]
291 fn filling_a_row_exactly_does_not_mark_it_wrapped() {
292 let mut g = Grid::new(4, 3);
293 feed(&mut g, b"abcd");
294 assert!(!g.row_wrapped(0), "wrap_next alone is not a wrap");
295 feed(&mut g, b"\r\nefgh");
296 assert!(!g.row_wrapped(0));
297 }
298
299 #[test]
300 fn the_wrapped_flag_rides_the_scroll_ring() {
301 let mut g = Grid::new(4, 3);
302 // "abcd" wraps onto "ef", one row down from the top.
303 feed(&mut g, b"xy\r\nabcdef");
304 assert!(g.row_wrapped(1));
305 // Scrolling carries the wrapped row up to row 0 — the flag is indexed
306 // physically, so it has to arrive with it.
307 feed(&mut g, b"\r\n");
308 assert_eq!(row_str(&g, 0), "abcd");
309 assert!(g.row_wrapped(0));
310 // One more scroll and it leaves the screen entirely.
311 feed(&mut g, b"\r\n");
312 assert_eq!(row_str(&g, 0), "ef");
313 assert!(!g.row_wrapped(0), "the wrapped row scrolled off the top");
314 }
315
316 #[test]
317 fn a_blank_row_exposed_by_a_scroll_is_not_wrapped() {
318 let mut g = Grid::new(4, 2);
319 feed(&mut g, b"abcdef\r\n\r\n\r\n");
320 for r in 0..g.rows() {
321 assert!(!g.row_wrapped(r), "row {r} came back wrapped");
322 }
323 }
324
325 #[test]
326 fn resize_drops_every_wrap_point() {
327 let mut g = Grid::new(4, 3);
328 feed(&mut g, b"abcdef");
329 assert!(g.row_wrapped(0));
330 g.resize(8, 3);
331 assert!(!g.row_wrapped(0), "the wrap point is meaningless at 8 cols");
332 }
333
334 #[test]
335 fn alt_screen_keeps_its_own_wrap_points() {
336 let mut g = Grid::new(4, 3);
337 feed(&mut g, b"abcdef");
338 feed(&mut g, b"\x1b[?1049h");
339 assert!(!g.row_wrapped(0), "alt screen starts clean");
340 feed(&mut g, b"\x1b[?1049l");
341 assert!(g.row_wrapped(0), "main screen's wrap point survived");
342 }
343
344 // ---- erase against a rotated ring ----------------------------------
345
346 #[test]
347 fn erase_line_targets_the_right_row_after_a_scroll() {
348 // Scroll far enough that the ring origin is non-zero, then erase the
349 // cursor's line. Erasing by logical row without the ring mapping
350 // would blank some other row entirely.
351 let mut g = Grid::new(6, 3);
352 feed(&mut g, b"one\r\ntwo\r\nthree\r\nfour\r\nfive");
353 assert_eq!(row_str(&g, 0), "three");
354 assert_eq!(row_str(&g, 1), "four");
355 assert_eq!(row_str(&g, 2), "five");
356 feed(&mut g, b"\x1b[2;1H\x1b[2K"); // row 1, erase whole line
357 assert_eq!(row_str(&g, 0), "three");
358 assert_eq!(row_str(&g, 1), "");
359 assert_eq!(row_str(&g, 2), "five");
360 }
361
362 #[test]
363 fn erase_to_end_of_line_targets_the_right_row_after_a_scroll() {
364 let mut g = Grid::new(6, 3);
365 feed(&mut g, b"one\r\ntwo\r\nthree\r\nfour\r\nfive");
366 feed(&mut g, b"\x1b[3;3H\x1b[K"); // row 2 col 2, erase to end
367 assert_eq!(row_str(&g, 0), "three");
368 assert_eq!(row_str(&g, 1), "four");
369 assert_eq!(row_str(&g, 2), "fi");
370 }
371 }
372