Skip to main content

uncurses/buffer/
ops.rs

1//! Buffer insert/delete operations.
2
3use crate::cell::Cell;
4use crate::layout::Position;
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 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 test_insert_lines() {
309        let mut buf = Buffer::new(5, 5);
310        buf.set((0, 0), &Cell::narrow("A"));
311        buf.set((0, 1), &Cell::narrow("B"));
312        buf.set((0, 2), &Cell::narrow("C"));
313
314        buf.insert_lines(1, 2, 5, &Cell::BLANK);
315        assert_eq!(buf.cell(Position::new(0, 0)).unwrap().content(), "A");
316        assert!(buf.cell(Position::new(0, 1)).unwrap().is_blank());
317        assert!(buf.cell(Position::new(0, 2)).unwrap().is_blank());
318        assert_eq!(buf.cell(Position::new(0, 3)).unwrap().content(), "B");
319        assert_eq!(buf.cell(Position::new(0, 4)).unwrap().content(), "C");
320    }
321
322    #[test]
323    fn test_delete_lines() {
324        let mut buf = Buffer::new(5, 5);
325        buf.set((0, 0), &Cell::narrow("A"));
326        buf.set((0, 1), &Cell::narrow("B"));
327        buf.set((0, 2), &Cell::narrow("C"));
328        buf.set((0, 3), &Cell::narrow("D"));
329
330        buf.delete_lines(1, 2, 5, &Cell::BLANK);
331        assert_eq!(buf.cell(Position::new(0, 0)).unwrap().content(), "A");
332        assert_eq!(buf.cell(Position::new(0, 1)).unwrap().content(), "D");
333        assert!(buf.cell(Position::new(0, 2)).unwrap().is_blank());
334    }
335
336    #[test]
337    fn test_insert_cells() {
338        let mut buf = Buffer::new(10, 1);
339        buf.set((0, 0), &Cell::narrow("A"));
340        buf.set((1, 0), &Cell::narrow("B"));
341        buf.set((2, 0), &Cell::narrow("C"));
342
343        buf.insert_cells((1, 0), 2, 10, &Cell::BLANK);
344        assert_eq!(buf.cell(Position::new(0, 0)).unwrap().content(), "A");
345        assert!(buf.cell(Position::new(1, 0)).unwrap().is_blank());
346        assert!(buf.cell(Position::new(2, 0)).unwrap().is_blank());
347        assert_eq!(buf.cell(Position::new(3, 0)).unwrap().content(), "B");
348        assert_eq!(buf.cell(Position::new(4, 0)).unwrap().content(), "C");
349    }
350
351    #[test]
352    fn test_delete_cells() {
353        let mut buf = Buffer::new(10, 1);
354        buf.set((0, 0), &Cell::narrow("A"));
355        buf.set((1, 0), &Cell::narrow("B"));
356        buf.set((2, 0), &Cell::narrow("C"));
357        buf.set((3, 0), &Cell::narrow("D"));
358
359        buf.delete_cells((1, 0), 2, 10, &Cell::BLANK);
360        assert_eq!(buf.cell(Position::new(0, 0)).unwrap().content(), "A");
361        assert_eq!(buf.cell(Position::new(1, 0)).unwrap().content(), "D");
362        assert!(buf.cell(Position::new(2, 0)).unwrap().is_blank());
363    }
364
365    #[test]
366    fn insert_cells_blanks_dangling_continuation_when_primary_shifted_off() {
367        // A wide cell straddling the truncation boundary must not leave a
368        // continuation marker behind once its primary is pushed past the
369        // right edge.
370        let mut buf = Buffer::new(6, 1);
371        buf.set((0, 0), &Cell::narrow("A"));
372        // Wide cell at columns 4-5 (primary at 4, continuation at 5).
373        buf.set((4, 0), &Cell::wide("漢"));
374        assert!(buf.cell(Position::new(5, 0)).unwrap().is_continuation());
375
376        // Insert 1 cell at col 1: primary at 4 shifts to 5, continuation
377        // (originally at 5) falls off. The new cell at col 5 is the wide
378        // primary with no continuation room — set() truncates it to BLANK.
379        buf.insert_cells((1, 0), 1, 6, &Cell::BLANK);
380        assert_eq!(buf.cell(Position::new(0, 0)).unwrap().content(), "A");
381        assert!(buf.cell(Position::new(1, 0)).unwrap().is_blank());
382        // The wide cell's continuation that used to be at col 5 must not
383        // remain as a stale marker.
384        assert!(!buf.cell(Position::new(5, 0)).unwrap().is_continuation());
385    }
386
387    #[test]
388    fn delete_cells_blanks_dangling_primary_at_fill_boundary() {
389        // A wide cell straddling the fill region's left edge must have
390        // its dangling primary cleaned up when the fill writes a blank
391        // over its continuation half.
392        let mut buf = Buffer::new(6, 1);
393        buf.set((0, 0), &Cell::narrow("A"));
394        // Wide cell at columns 4-5 (primary at 4, continuation at 5).
395        buf.set((4, 0), &Cell::wide("漢"));
396        assert!(buf.cell(Position::new(5, 0)).unwrap().is_continuation());
397
398        // Delete 1 cell at col 0 (the "A"). Cells shift left so the wide
399        // pair lands at cols 3-4. The fill at col 5 (now blank from the
400        // shift) is written via set(); col 5 was the continuation prior
401        // to the shift but is now arbitrary — what we really need to
402        // verify is that no orphan continuations leak past the right
403        // boundary.
404        buf.delete_cells((0, 0), 1, 6, &Cell::BLANK);
405        // Last column must be a clean blank, not a stray continuation.
406        let last = buf.cell(Position::new(5, 0)).unwrap();
407        assert!(!last.is_continuation());
408        assert!(last.is_blank());
409    }
410
411    #[test]
412    fn fill_with_wide_cell_steps_by_width() {
413        // When the fill cell is itself wide, each primary must own its
414        // continuation slot — no orphan primaries from stepping by 1.
415        let mut buf = Buffer::new(5, 1);
416        let wide = Cell::wide("漢");
417
418        // Fill via insert_cells with n covering the whole row.
419        buf.insert_cells((0, 0), 5, 5, &wide);
420        // Cols 0,2 are wide primaries; 1,3 are continuations; 4 is a
421        // plain blank (odd trailing slot can't fit another pair).
422        assert_eq!(buf.cell(Position::new(0, 0)).unwrap().width(), 2);
423        assert!(buf.cell(Position::new(1, 0)).unwrap().is_continuation());
424        assert_eq!(buf.cell(Position::new(2, 0)).unwrap().width(), 2);
425        assert!(buf.cell(Position::new(3, 0)).unwrap().is_continuation());
426        assert!(buf.cell(Position::new(4, 0)).unwrap().is_blank());
427        assert!(!buf.cell(Position::new(4, 0)).unwrap().is_continuation());
428    }
429}