Skip to main content

uncurses/buffer/
ops.rs

1//! Buffer insert/delete operations.
2
3use crate::cell::Cell;
4use crate::layout::{Position, overruns};
5
6use super::{Buffer, fill_line_into};
7
8/// Replace row `y` entirely with `fill`. Wide fills are stepped by
9/// `fill.width()` so primaries own their continuation slots. Whole-row
10/// replacement means we don't need to clean up wide cells in the row
11/// being overwritten — they're discarded as the row is rewritten.
12fn fill_row(buf: &mut Buffer, y: u16, fill: &Cell) {
13    if let Some(line) = buf.line_mut(y) {
14        fill_line_into(line, fill);
15    }
16}
17
18/// Fill `[lo, hi)` on row `y` with `fill`, routing through `set()` so
19/// wide-cell semantics around the fill region (and any wide cells in
20/// the slots being overwritten) are honored. Wide fills are laid down
21/// one primary per `fill.width()` slots; any trailing partial slot gets
22/// a plain blank.
23fn fill_range(buf: &mut Buffer, y: u16, lo: u16, hi: u16, fill: &Cell) {
24    if lo >= hi {
25        return;
26    }
27    let step = fill.width().max(1) as u16;
28    let mut x = lo;
29    while !overruns(x, step, hi) {
30        buf.set((x, y), fill);
31        x += step;
32    }
33    while x < hi {
34        buf.set((x, y), &Cell::BLANK);
35        x += 1;
36    }
37}
38
39impl Buffer {
40    /// Insert rows within a bounded vertical region.
41    ///
42    /// Existing rows in `[y, bounds_bottom)` are pushed downward by `n`.
43    /// Rows pushed past `bounds_bottom` are discarded, and the newly opened
44    /// rows starting at `y` are filled with `fill`.
45    ///
46    /// # Parameters
47    ///
48    /// - `y`: zero-based row where insertion begins.
49    /// - `n`: number of rows to insert.
50    /// - `bounds_bottom`: exclusive lower row bound for the affected region;
51    ///   clamped to the buffer height.
52    /// - `fill`: cell used to fill inserted rows.
53    ///
54    /// # Returns
55    ///
56    /// Nothing.
57    ///
58    /// # Panics
59    ///
60    /// Never panics.
61    ///
62    /// # Usage notes
63    ///
64    /// Calls with `y >= bounds_bottom` are no-ops. Whole rows are swapped in
65    /// row-major storage, so wide-cell pairs inside moved rows remain
66    /// structurally unchanged.
67    pub fn insert_lines(&mut self, y: u16, n: u16, bounds_bottom: u16, fill: &Cell) {
68        let bottom = bounds_bottom.min(self.height) as usize;
69        let y_usize = y as usize;
70        let n = n as usize;
71
72        if y_usize >= bottom {
73            return;
74        }
75
76        // Shift lines down
77        let shift_end = bottom;
78        let shift_start = (y_usize + n).min(shift_end);
79
80        // Move from bottom to top to avoid overwrites
81        for i in (shift_start..shift_end).rev() {
82            let src = i - n;
83            if src >= y_usize {
84                self.swap_rows(i, src);
85            }
86        }
87
88        // Clear the inserted lines
89        let fill_end = y_usize + n.min(shift_end - y_usize);
90        for i in y_usize..fill_end {
91            fill_row(self, i as u16, fill);
92        }
93    }
94
95    /// Delete rows within a bounded vertical region.
96    ///
97    /// Existing rows below the deleted range are pulled upward within
98    /// `[y, bounds_bottom)`. The freed rows at the bottom of the region are
99    /// filled with `fill`.
100    ///
101    /// # Parameters
102    ///
103    /// - `y`: zero-based first row to delete.
104    /// - `n`: number of rows to delete.
105    /// - `bounds_bottom`: exclusive lower row bound for the affected region;
106    ///   clamped to the buffer height.
107    /// - `fill`: cell used to fill freed bottom rows.
108    ///
109    /// # Returns
110    ///
111    /// Nothing.
112    ///
113    /// # Panics
114    ///
115    /// Never panics.
116    ///
117    /// # Usage notes
118    ///
119    /// Calls with `y >= bounds_bottom` are no-ops. Deleting more rows than
120    /// remain in the region is equivalent to deleting through
121    /// `bounds_bottom`.
122    pub fn delete_lines(&mut self, y: u16, n: u16, bounds_bottom: u16, fill: &Cell) {
123        let bottom = bounds_bottom.min(self.height) as usize;
124        let y_usize = y as usize;
125        let n = n as usize;
126
127        if y_usize >= bottom {
128            return;
129        }
130
131        let n = n.min(bottom - y_usize);
132
133        // Shift lines up
134        for i in y_usize..bottom - n {
135            self.swap_rows(i, i + n);
136        }
137
138        // Clear the bottom lines
139        for i in bottom - n..bottom {
140            fill_row(self, i as u16, fill);
141        }
142    }
143
144    /// Insert cells within one row.
145    ///
146    /// Cells in `[pos.x, bounds_right)` on `pos.y` are pushed right by `n`.
147    /// Cells pushed past `bounds_right` are discarded, and the newly opened
148    /// slots starting at `pos.x` are filled with `fill`.
149    ///
150    /// # Parameters
151    ///
152    /// - `pos`: row and starting column for insertion.
153    /// - `n`: number of cells to insert.
154    /// - `bounds_right`: exclusive right column bound for the affected row
155    ///   region; clamped to the buffer width.
156    /// - `fill`: cell used to fill newly opened slots.
157    ///
158    /// # Returns
159    ///
160    /// Nothing.
161    ///
162    /// # Panics
163    ///
164    /// Never panics.
165    ///
166    /// # Usage notes
167    ///
168    /// Calls with `n == 0`, an out-of-bounds row, or `pos.x >= bounds_right`
169    /// are no-ops. Width-1 fills are written directly into the freed span.
170    /// Wide fills are routed through [`Buffer::set`] so primary and
171    /// continuation columns are placed consistently.
172    pub fn insert_cells(
173        &mut self,
174        pos: impl Into<Position>,
175        n: u16,
176        bounds_right: u16,
177        fill: &Cell,
178    ) {
179        let pos = pos.into();
180        let y = pos.y as usize;
181        let x = pos.x as usize;
182        let n = n as usize;
183        let right = bounds_right.min(self.width) as usize;
184
185        if y >= self.height() as usize || x >= right || n == 0 {
186            return;
187        }
188
189        let n = n.min(right - x);
190
191        let needs_wide_fill = fill.is_wide();
192        {
193            // Shift the row's `[x, right)` window right by `n` via an
194            // in-place rotate. This is a `memmove`-shaped operation,
195            // so we avoid an O(W-n) loop of `Cell::clone` plus a heap
196            // allocation per non-empty hyperlink.
197            let line = self.line_mut(pos.y).expect("row in range");
198            line[x..right].rotate_right(n);
199
200            if !needs_wide_fill {
201                // Width-1 fill: single sweep over the freed region.
202                // This overwrites any stray continuation marker the
203                // rotate parked here, so no separate blank pre-pass is
204                // needed.
205                line[x..x + n].fill(fill.clone());
206            } else {
207                // Wide fill: raw-blank the freed slots so a parked
208                // continuation can't fool `fill_range`'s `set()` calls
209                // into walking back and clobbering an unrelated
210                // primary cell.
211                for cell in &mut line[x..x + n] {
212                    *cell = Cell::BLANK;
213                }
214            }
215        }
216
217        if needs_wide_fill {
218            // Wide fill: route through fill_range so primary /
219            // continuation pairs are laid down correctly.
220            fill_range(self, pos.y, x as u16, (x + n) as u16, fill);
221        }
222    }
223
224    /// Delete cells within one row.
225    ///
226    /// Cells in `[pos.x + n, bounds_right)` on `pos.y` are pulled left by
227    /// `n`. The freed slots at the right edge of the affected region are
228    /// filled with `fill`.
229    ///
230    /// # Parameters
231    ///
232    /// - `pos`: row and starting column for deletion.
233    /// - `n`: number of cells to delete.
234    /// - `bounds_right`: exclusive right column bound for the affected row
235    ///   region; clamped to the buffer width.
236    /// - `fill`: cell used to fill freed right-edge slots.
237    ///
238    /// # Returns
239    ///
240    /// Nothing.
241    ///
242    /// # Panics
243    ///
244    /// Never panics.
245    ///
246    /// # Usage notes
247    ///
248    /// Calls with `n == 0`, an out-of-bounds row, or `pos.x >= bounds_right`
249    /// are no-ops. Width-1 fills are written directly into the freed span.
250    /// Wide fills are routed through [`Buffer::set`] so primary and
251    /// continuation columns are placed consistently.
252    pub fn delete_cells(
253        &mut self,
254        pos: impl Into<Position>,
255        n: u16,
256        bounds_right: u16,
257        fill: &Cell,
258    ) {
259        let pos = pos.into();
260        let y = pos.y as usize;
261        let x = pos.x as usize;
262        let n = n as usize;
263        let right = bounds_right.min(self.width) as usize;
264
265        if y >= self.height() as usize || x >= right || n == 0 {
266            return;
267        }
268
269        let n = n.min(right - x);
270
271        let needs_wide_fill = fill.is_wide();
272        {
273            // Shift the row's `[x, right)` window left by `n` via an
274            // in-place rotate (memmove-shaped). Cleanup happens below
275            // when the freed right-edge slots are filled: width-1 fills
276            // overwrite parked continuations directly, and wide fills
277            // blank before routing through fill_range.
278            let line = self.line_mut(pos.y).expect("row in range");
279            line[x..right].rotate_left(n);
280
281            if !needs_wide_fill {
282                // Width-1 fill: single sweep over the freed right-edge
283                // region. Any stray continuation marker parked there by
284                // the rotate is overwritten in this same pass.
285                line[right - n..right].fill(fill.clone());
286            } else {
287                // Wide fill: raw-blank the freed slots before routing
288                // through fill_range, so its `set()` calls don't walk
289                // back into still-live shifted data.
290                for cell in &mut line[right - n..right] {
291                    *cell = Cell::BLANK;
292                }
293            }
294        }
295
296        if needs_wide_fill {
297            fill_range(self, pos.y, (right - n) as u16, right as u16, fill);
298        }
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305    use crate::buffer::Surface;
306
307    #[test]
308    fn a_wide_fill_at_the_last_addressable_column_does_not_overflow() {
309        // A cell can claim 255 columns, so one column short of the end of
310        // the address space the column plus that width no longer fits a
311        // `u16`. The freed slot is narrower than the fill either way, so
312        // it takes a blank.
313        let mut buf = Buffer::new(u16::MAX, 1);
314        let wide = Cell::new("\u{1f468}\u{200d}\u{1f469}", 255);
315
316        buf.insert_cells((u16::MAX - 1, 0), 1, u16::MAX, &wide);
317        assert!(!buf.cell(Position::new(u16::MAX - 1, 0)).unwrap().is_wide());
318
319        buf.delete_cells((u16::MAX - 1, 0), 1, u16::MAX, &wide);
320        assert!(!buf.cell(Position::new(u16::MAX - 1, 0)).unwrap().is_wide());
321    }
322
323    #[test]
324    fn test_insert_lines() {
325        let mut buf = Buffer::new(5, 5);
326        buf.set((0, 0), &Cell::new("A", 1));
327        buf.set((0, 1), &Cell::new("B", 1));
328        buf.set((0, 2), &Cell::new("C", 1));
329
330        buf.insert_lines(1, 2, 5, &Cell::BLANK);
331        assert_eq!(buf.cell(Position::new(0, 0)).unwrap().content(), "A");
332        assert!(buf.cell(Position::new(0, 1)).unwrap().is_blank());
333        assert!(buf.cell(Position::new(0, 2)).unwrap().is_blank());
334        assert_eq!(buf.cell(Position::new(0, 3)).unwrap().content(), "B");
335        assert_eq!(buf.cell(Position::new(0, 4)).unwrap().content(), "C");
336    }
337
338    #[test]
339    fn test_delete_lines() {
340        let mut buf = Buffer::new(5, 5);
341        buf.set((0, 0), &Cell::new("A", 1));
342        buf.set((0, 1), &Cell::new("B", 1));
343        buf.set((0, 2), &Cell::new("C", 1));
344        buf.set((0, 3), &Cell::new("D", 1));
345
346        buf.delete_lines(1, 2, 5, &Cell::BLANK);
347        assert_eq!(buf.cell(Position::new(0, 0)).unwrap().content(), "A");
348        assert_eq!(buf.cell(Position::new(0, 1)).unwrap().content(), "D");
349        assert!(buf.cell(Position::new(0, 2)).unwrap().is_blank());
350    }
351
352    #[test]
353    fn test_insert_cells() {
354        let mut buf = Buffer::new(10, 1);
355        buf.set((0, 0), &Cell::new("A", 1));
356        buf.set((1, 0), &Cell::new("B", 1));
357        buf.set((2, 0), &Cell::new("C", 1));
358
359        buf.insert_cells((1, 0), 2, 10, &Cell::BLANK);
360        assert_eq!(buf.cell(Position::new(0, 0)).unwrap().content(), "A");
361        assert!(buf.cell(Position::new(1, 0)).unwrap().is_blank());
362        assert!(buf.cell(Position::new(2, 0)).unwrap().is_blank());
363        assert_eq!(buf.cell(Position::new(3, 0)).unwrap().content(), "B");
364        assert_eq!(buf.cell(Position::new(4, 0)).unwrap().content(), "C");
365    }
366
367    #[test]
368    fn test_delete_cells() {
369        let mut buf = Buffer::new(10, 1);
370        buf.set((0, 0), &Cell::new("A", 1));
371        buf.set((1, 0), &Cell::new("B", 1));
372        buf.set((2, 0), &Cell::new("C", 1));
373        buf.set((3, 0), &Cell::new("D", 1));
374
375        buf.delete_cells((1, 0), 2, 10, &Cell::BLANK);
376        assert_eq!(buf.cell(Position::new(0, 0)).unwrap().content(), "A");
377        assert_eq!(buf.cell(Position::new(1, 0)).unwrap().content(), "D");
378        assert!(buf.cell(Position::new(2, 0)).unwrap().is_blank());
379    }
380
381    #[test]
382    fn insert_cells_blanks_dangling_continuation_when_primary_shifted_off() {
383        // A wide cell straddling the truncation boundary must not leave a
384        // continuation marker behind once its primary is pushed past the
385        // right edge.
386        let mut buf = Buffer::new(6, 1);
387        buf.set((0, 0), &Cell::new("A", 1));
388        // Wide cell at columns 4-5 (primary at 4, continuation at 5).
389        buf.set((4, 0), &Cell::new("漢", 2));
390        assert!(buf.cell(Position::new(5, 0)).unwrap().is_continuation());
391
392        // Insert 1 cell at col 1: primary at 4 shifts to 5, continuation
393        // (originally at 5) falls off. The new cell at col 5 is the wide
394        // primary with no continuation room — set() truncates it to BLANK.
395        buf.insert_cells((1, 0), 1, 6, &Cell::BLANK);
396        assert_eq!(buf.cell(Position::new(0, 0)).unwrap().content(), "A");
397        assert!(buf.cell(Position::new(1, 0)).unwrap().is_blank());
398        // The wide cell's continuation that used to be at col 5 must not
399        // remain as a stale marker.
400        assert!(!buf.cell(Position::new(5, 0)).unwrap().is_continuation());
401    }
402
403    #[test]
404    fn delete_cells_blanks_dangling_primary_at_fill_boundary() {
405        // A wide cell straddling the fill region's left edge must have
406        // its dangling primary cleaned up when the fill writes a blank
407        // over its continuation half.
408        let mut buf = Buffer::new(6, 1);
409        buf.set((0, 0), &Cell::new("A", 1));
410        // Wide cell at columns 4-5 (primary at 4, continuation at 5).
411        buf.set((4, 0), &Cell::new("漢", 2));
412        assert!(buf.cell(Position::new(5, 0)).unwrap().is_continuation());
413
414        // Delete 1 cell at col 0 (the "A"). Cells shift left so the wide
415        // pair lands at cols 3-4. The fill at col 5 (now blank from the
416        // shift) is written via set(); col 5 was the continuation prior
417        // to the shift but is now arbitrary — what we really need to
418        // verify is that no orphan continuations leak past the right
419        // boundary.
420        buf.delete_cells((0, 0), 1, 6, &Cell::BLANK);
421        // Last column must be a clean blank, not a stray continuation.
422        let last = buf.cell(Position::new(5, 0)).unwrap();
423        assert!(!last.is_continuation());
424        assert!(last.is_blank());
425    }
426
427    #[test]
428    fn fill_with_wide_cell_steps_by_width() {
429        // When the fill cell is itself wide, each primary must own its
430        // continuation slot — no orphan primaries from stepping by 1.
431        let mut buf = Buffer::new(5, 1);
432        let wide = Cell::new("漢", 2);
433
434        // Fill via insert_cells with n covering the whole row.
435        buf.insert_cells((0, 0), 5, 5, &wide);
436        // Cols 0,2 are wide primaries; 1,3 are continuations; 4 is a
437        // plain blank (odd trailing slot can't fit another pair).
438        assert_eq!(buf.cell(Position::new(0, 0)).unwrap().width(), 2);
439        assert!(buf.cell(Position::new(1, 0)).unwrap().is_continuation());
440        assert_eq!(buf.cell(Position::new(2, 0)).unwrap().width(), 2);
441        assert!(buf.cell(Position::new(3, 0)).unwrap().is_continuation());
442        assert!(buf.cell(Position::new(4, 0)).unwrap().is_blank());
443        assert!(!buf.cell(Position::new(4, 0)).unwrap().is_continuation());
444    }
445}