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}