1#![forbid(unsafe_code)]
2
3use smallvec::SmallVec;
56
57use crate::budget::DegradationLevel;
58use crate::cell::{Cell, GraphemeId};
59use ftui_core::geometry::Rect;
60
61const DIRTY_SPAN_MAX_SPANS_PER_ROW: usize = 64;
63const DIRTY_SPAN_MERGE_GAP: u16 = 1;
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub struct DirtySpanConfig {
69 pub enabled: bool,
71 pub max_spans_per_row: usize,
73 pub merge_gap: u16,
75 pub guard_band: u16,
77}
78
79impl Default for DirtySpanConfig {
80 fn default() -> Self {
81 Self {
82 enabled: true,
83 max_spans_per_row: DIRTY_SPAN_MAX_SPANS_PER_ROW,
84 merge_gap: DIRTY_SPAN_MERGE_GAP,
85 guard_band: 0,
86 }
87 }
88}
89
90impl DirtySpanConfig {
91 #[must_use]
93 pub fn with_enabled(mut self, enabled: bool) -> Self {
94 self.enabled = enabled;
95 self
96 }
97
98 #[must_use]
100 pub fn with_max_spans_per_row(mut self, max_spans: usize) -> Self {
101 self.max_spans_per_row = max_spans;
102 self
103 }
104
105 #[must_use]
107 pub fn with_merge_gap(mut self, merge_gap: u16) -> Self {
108 self.merge_gap = merge_gap;
109 self
110 }
111
112 #[must_use]
114 pub fn with_guard_band(mut self, guard_band: u16) -> Self {
115 self.guard_band = guard_band;
116 self
117 }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub(crate) struct DirtySpan {
123 pub x0: u16,
124 pub x1: u16,
125}
126
127impl DirtySpan {
128 #[inline]
129 pub const fn new(x0: u16, x1: u16) -> Self {
130 Self { x0, x1 }
131 }
132
133 #[inline]
134 pub const fn len(self) -> usize {
135 self.x1.saturating_sub(self.x0) as usize
136 }
137}
138
139#[derive(Debug, Default, Clone)]
140pub(crate) struct DirtySpanRow {
141 overflow: bool,
142 spans: SmallVec<[DirtySpan; 4]>,
144}
145
146impl DirtySpanRow {
147 #[inline]
148 fn new_full() -> Self {
149 Self {
150 overflow: true,
151 spans: SmallVec::new(),
152 }
153 }
154
155 #[inline]
156 fn clear(&mut self) {
157 self.overflow = false;
158 self.spans.clear();
159 }
160
161 #[inline]
162 fn set_full(&mut self) {
163 self.overflow = true;
164 self.spans.clear();
165 }
166
167 #[inline]
168 pub(crate) fn spans(&self) -> &[DirtySpan] {
169 &self.spans
170 }
171
172 #[inline]
173 pub(crate) fn is_full(&self) -> bool {
174 self.overflow
175 }
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub struct DirtySpanStats {
181 pub rows_full_dirty: usize,
183 pub rows_with_spans: usize,
185 pub total_spans: usize,
187 pub overflows: usize,
189 pub span_coverage_cells: usize,
191 pub max_span_len: usize,
193 pub max_spans_per_row: usize,
195}
196
197#[derive(Debug, Clone)]
210pub struct Buffer {
211 width: u16,
212 height: u16,
213 cells: Vec<Cell>,
214 scissor_stack: Vec<Rect>,
215 opacity_stack: Vec<f32>,
216 pub degradation: DegradationLevel,
221 dirty_rows: Vec<bool>,
228 dirty_spans: Vec<DirtySpanRow>,
230 dirty_span_config: DirtySpanConfig,
232 dirty_span_overflows: usize,
234 dirty_bits: Vec<u8>,
236 dirty_cells: usize,
238 dirty_all: bool,
240}
241
242impl Buffer {
243 pub fn new(width: u16, height: u16) -> Self {
251 let width = width.max(1);
252 let height = height.max(1);
253
254 let size = width as usize * height as usize;
255 let cells = vec![Cell::default(); size];
256
257 let dirty_spans = (0..height)
258 .map(|_| DirtySpanRow::new_full())
259 .collect::<Vec<_>>();
260 let dirty_bits = vec![0u8; size];
261 let dirty_cells = size;
262 let dirty_all = true;
263
264 Self {
265 width,
266 height,
267 cells,
268 scissor_stack: vec![Rect::from_size(width, height)],
269 opacity_stack: vec![1.0],
270 degradation: DegradationLevel::Full,
271 dirty_rows: vec![true; height as usize],
274 dirty_spans,
276 dirty_span_config: DirtySpanConfig::default(),
277 dirty_span_overflows: 0,
278 dirty_bits,
279 dirty_cells,
280 dirty_all,
281 }
282 }
283
284 #[inline]
286 pub const fn width(&self) -> u16 {
287 self.width
288 }
289
290 #[inline]
292 pub const fn height(&self) -> u16 {
293 self.height
294 }
295
296 #[inline]
298 pub fn len(&self) -> usize {
299 self.cells.len()
300 }
301
302 #[inline]
304 pub fn is_empty(&self) -> bool {
305 self.cells.is_empty()
306 }
307
308 #[inline]
310 pub const fn bounds(&self) -> Rect {
311 Rect::from_size(self.width, self.height)
312 }
313
314 #[inline]
319 pub fn content_height(&self) -> u16 {
320 let default_cell = Cell::default();
321 let width = self.width as usize;
322 for y in (0..self.height).rev() {
323 let row_start = y as usize * width;
324 let row_end = row_start + width;
325 if self.cells[row_start..row_end]
326 .iter()
327 .any(|cell| *cell != default_cell)
328 {
329 return y + 1;
330 }
331 }
332 0
333 }
334
335 #[inline]
342 fn mark_dirty_row(&mut self, y: u16) {
343 if let Some(slot) = self.dirty_rows.get_mut(y as usize) {
344 *slot = true;
345 }
346 }
347
348 #[inline]
350 fn mark_dirty_bits_range(&mut self, y: u16, start: u16, end: u16) {
351 if self.dirty_all {
352 return;
353 }
354 if y >= self.height {
355 return;
356 }
357
358 let width = self.width;
359 if start >= width {
360 return;
361 }
362 let end = end.min(width);
363 if start >= end {
364 return;
365 }
366
367 let row_start = y as usize * width as usize;
368 let slice = &mut self.dirty_bits[row_start + start as usize..row_start + end as usize];
369 let newly_dirty = slice.iter().filter(|&&b| b == 0).count();
370 slice.fill(1);
371 self.dirty_cells = self.dirty_cells.saturating_add(newly_dirty);
372 }
373
374 #[inline]
376 fn mark_dirty_bits_row(&mut self, y: u16) {
377 self.mark_dirty_bits_range(y, 0, self.width);
378 }
379
380 #[inline]
382 fn mark_dirty_row_full(&mut self, y: u16) {
383 self.mark_dirty_row(y);
384 if self.dirty_span_config.enabled
385 && let Some(row) = self.dirty_spans.get_mut(y as usize)
386 {
387 row.set_full();
388 }
389 self.mark_dirty_bits_row(y);
390 }
391
392 #[inline]
394 pub(crate) fn mark_dirty_span(&mut self, y: u16, x0: u16, x1: u16) {
395 self.mark_dirty_row(y);
396 let width = self.width;
397 let (start, mut end) = if x0 <= x1 { (x0, x1) } else { (x1, x0) };
398 if start >= width {
399 return;
400 }
401 if end > width {
402 end = width;
403 }
404 if start >= end {
405 return;
406 }
407
408 self.mark_dirty_bits_range(y, start, end);
409
410 if !self.dirty_span_config.enabled {
411 return;
412 }
413
414 let guard_band = self.dirty_span_config.guard_band;
415 let span_start = start.saturating_sub(guard_band);
416 let mut span_end = end.saturating_add(guard_band);
417 if span_end > width {
418 span_end = width;
419 }
420 if span_start >= span_end {
421 return;
422 }
423
424 let Some(row) = self.dirty_spans.get_mut(y as usize) else {
425 return;
426 };
427
428 if row.is_full() {
429 return;
430 }
431
432 let new_span = DirtySpan::new(span_start, span_end);
433 let spans = &mut row.spans;
434 let insert_at = spans.partition_point(|span| span.x0 <= new_span.x0);
435 spans.insert(insert_at, new_span);
436
437 let merge_gap = self.dirty_span_config.merge_gap;
439 let mut i = if insert_at > 0 { insert_at - 1 } else { 0 };
440 while i + 1 < spans.len() {
441 let current = spans[i];
442 let next = spans[i + 1];
443 let merge_limit = current.x1.saturating_add(merge_gap);
444 if merge_limit >= next.x0 {
445 spans[i].x1 = current.x1.max(next.x1);
446 spans.remove(i + 1);
447 continue;
448 }
449 i += 1;
450 }
451
452 if spans.len() > self.dirty_span_config.max_spans_per_row {
453 row.set_full();
454 self.dirty_span_overflows = self.dirty_span_overflows.saturating_add(1);
455 }
456 }
457
458 #[inline]
460 pub fn mark_all_dirty(&mut self) {
461 self.dirty_rows.fill(true);
462 if self.dirty_span_config.enabled {
463 for row in &mut self.dirty_spans {
464 row.set_full();
465 }
466 } else {
467 for row in &mut self.dirty_spans {
468 row.clear();
469 }
470 }
471 self.dirty_all = true;
472 self.dirty_cells = self.cells.len();
473 }
474
475 #[inline]
479 pub fn clear_dirty(&mut self) {
480 self.dirty_rows.fill(false);
481 for row in &mut self.dirty_spans {
482 row.clear();
483 }
484 self.dirty_span_overflows = 0;
485 self.dirty_bits.fill(0);
486 self.dirty_cells = 0;
487 self.dirty_all = false;
488 }
489
490 #[inline]
492 pub fn is_row_dirty(&self, y: u16) -> bool {
493 self.dirty_rows.get(y as usize).copied().unwrap_or(false)
494 }
495
496 #[inline]
501 pub fn dirty_rows(&self) -> &[bool] {
502 &self.dirty_rows
503 }
504
505 #[inline]
507 pub fn dirty_row_count(&self) -> usize {
508 self.dirty_rows.iter().filter(|&&d| d).count()
509 }
510
511 #[inline]
513 #[allow(dead_code)]
514 pub(crate) fn dirty_bits(&self) -> &[u8] {
515 &self.dirty_bits
516 }
517
518 #[inline]
520 #[allow(dead_code)]
521 pub(crate) fn dirty_cell_count(&self) -> usize {
522 self.dirty_cells
523 }
524
525 #[inline]
527 #[allow(dead_code)]
528 pub(crate) fn dirty_all(&self) -> bool {
529 self.dirty_all
530 }
531
532 #[inline]
534 #[allow(dead_code)]
535 pub(crate) fn dirty_span_row(&self, y: u16) -> Option<&DirtySpanRow> {
536 if !self.dirty_span_config.enabled {
537 return None;
538 }
539 self.dirty_spans.get(y as usize)
540 }
541
542 pub fn dirty_span_stats(&self) -> DirtySpanStats {
544 if !self.dirty_span_config.enabled {
545 return DirtySpanStats {
546 rows_full_dirty: 0,
547 rows_with_spans: 0,
548 total_spans: 0,
549 overflows: 0,
550 span_coverage_cells: 0,
551 max_span_len: 0,
552 max_spans_per_row: self.dirty_span_config.max_spans_per_row,
553 };
554 }
555
556 let mut rows_full_dirty = 0usize;
557 let mut rows_with_spans = 0usize;
558 let mut total_spans = 0usize;
559 let mut span_coverage_cells = 0usize;
560 let mut max_span_len = 0usize;
561
562 for row in &self.dirty_spans {
563 if row.is_full() {
564 rows_full_dirty += 1;
565 span_coverage_cells += self.width as usize;
566 max_span_len = max_span_len.max(self.width as usize);
567 continue;
568 }
569 if !row.spans().is_empty() {
570 rows_with_spans += 1;
571 }
572 total_spans += row.spans().len();
573 for span in row.spans() {
574 span_coverage_cells += span.len();
575 max_span_len = max_span_len.max(span.len());
576 }
577 }
578
579 DirtySpanStats {
580 rows_full_dirty,
581 rows_with_spans,
582 total_spans,
583 overflows: self.dirty_span_overflows,
584 span_coverage_cells,
585 max_span_len,
586 max_spans_per_row: self.dirty_span_config.max_spans_per_row,
587 }
588 }
589
590 #[inline]
592 pub fn dirty_span_config(&self) -> DirtySpanConfig {
593 self.dirty_span_config
594 }
595
596 pub fn set_dirty_span_config(&mut self, config: DirtySpanConfig) {
598 if self.dirty_span_config == config {
599 return;
600 }
601 self.dirty_span_config = config;
602 for row in &mut self.dirty_spans {
603 row.clear();
604 }
605 self.dirty_span_overflows = 0;
606 }
607
608 #[inline]
614 fn index(&self, x: u16, y: u16) -> Option<usize> {
615 if x < self.width && y < self.height {
616 Some(y as usize * self.width as usize + x as usize)
617 } else {
618 None
619 }
620 }
621
622 #[inline]
628 pub(crate) fn index_unchecked(&self, x: u16, y: u16) -> usize {
629 debug_assert!(x < self.width && y < self.height);
630 y as usize * self.width as usize + x as usize
631 }
632
633 #[inline]
639 pub(crate) fn cell_mut_unchecked(&mut self, idx: usize) -> &mut Cell {
640 &mut self.cells[idx]
641 }
642
643 #[inline]
647 #[must_use]
648 pub fn get(&self, x: u16, y: u16) -> Option<&Cell> {
649 self.index(x, y).map(|i| &self.cells[i])
650 }
651
652 #[inline]
657 #[must_use]
658 pub fn get_mut(&mut self, x: u16, y: u16) -> Option<&mut Cell> {
659 let idx = self.index(x, y)?;
660 self.mark_dirty_span(y, x, x.saturating_add(1));
661 Some(&mut self.cells[idx])
662 }
663
664 #[inline]
671 pub fn get_unchecked(&self, x: u16, y: u16) -> &Cell {
672 let i = self.index_unchecked(x, y);
673 &self.cells[i]
674 }
675
676 #[inline]
680 fn cleanup_overlap(&mut self, x: u16, y: u16, new_cell: &Cell) -> Option<DirtySpan> {
681 let idx = self.index(x, y)?;
682 let current = self.cells[idx];
683 let mut touched = false;
684 let mut min_x = x;
685 let mut max_x = x;
686
687 if current.content.width() > 1 {
689 let width = current.content.width();
690 for i in 1..width {
695 let Some(cx) = x.checked_add(i as u16) else {
696 break;
697 };
698 if let Some(tail_idx) = self.index(cx, y)
699 && self.cells[tail_idx].is_continuation()
700 {
701 self.cells[tail_idx] = Cell::default();
702 touched = true;
703 min_x = min_x.min(cx);
704 max_x = max_x.max(cx);
705 }
706 }
707 }
708 else if current.is_continuation() && !new_cell.is_continuation() {
710 let mut back_x = x;
711 let limit = x.saturating_sub(GraphemeId::MAX_WIDTH as u16);
714
715 while back_x > limit {
716 back_x -= 1;
717 if let Some(h_idx) = self.index(back_x, y) {
718 let h_cell = self.cells[h_idx];
719 if !h_cell.is_continuation() {
720 let width = h_cell.content.width();
722 if (back_x as usize + width) > x as usize {
723 self.cells[h_idx] = Cell::default();
726 touched = true;
727 min_x = min_x.min(back_x);
728 max_x = max_x.max(back_x);
729
730 for i in 1..width {
733 let Some(cx) = back_x.checked_add(i as u16) else {
734 break;
735 };
736 if let Some(tail_idx) = self.index(cx, y) {
737 if self.cells[tail_idx].is_continuation() {
740 self.cells[tail_idx] = Cell::default();
741 touched = true;
742 min_x = min_x.min(cx);
743 max_x = max_x.max(cx);
744 }
745 }
746 }
747 }
748 break;
749 }
750 }
751 }
752 }
753
754 if touched {
755 Some(DirtySpan::new(min_x, max_x.saturating_add(1)))
756 } else {
757 None
758 }
759 }
760
761 #[inline]
768 fn cleanup_orphaned_tails(&mut self, start_x: u16, y: u16) {
769 if start_x >= self.width {
770 return;
771 }
772
773 let Some(idx) = self.index(start_x, y) else {
775 return;
776 };
777 if !self.cells[idx].is_continuation() {
778 return;
779 }
780
781 let mut x = start_x;
783 let mut max_x = x;
784 let row_end_idx = (y as usize * self.width as usize) + self.width as usize;
785 let mut curr_idx = idx;
786
787 while curr_idx < row_end_idx && self.cells[curr_idx].is_continuation() {
788 self.cells[curr_idx] = Cell::default();
789 max_x = x;
790 x = x.saturating_add(1);
791 curr_idx += 1;
792 }
793
794 self.mark_dirty_span(y, start_x, max_x.saturating_add(1));
796 }
797
798 #[inline]
813 pub fn set_fast(&mut self, x: u16, y: u16, cell: Cell) {
814 let bg_a = cell.bg.a();
821 if cell.content.width() > 1 || cell.is_continuation() || (bg_a != 255 && bg_a != 0) {
822 return self.set(x, y, cell);
823 }
824
825 if self.scissor_stack.len() != 1 || self.opacity_stack.len() != 1 {
827 return self.set(x, y, cell);
828 }
829
830 let Some(idx) = self.index(x, y) else {
832 return;
833 };
834
835 let existing = self.cells[idx];
839 if existing.content.width() > 1 || existing.is_continuation() {
840 return self.set(x, y, cell);
841 }
842
843 let mut final_cell = cell;
849 if bg_a == 0 {
850 final_cell.bg = existing.bg;
851 }
852
853 self.cells[idx] = final_cell;
854 self.mark_dirty_span(y, x, x.saturating_add(1));
855 self.cleanup_orphaned_tails(x.saturating_add(1), y);
856 }
857
858 #[inline]
870 pub fn set(&mut self, x: u16, y: u16, cell: Cell) {
871 let width = cell.content.width();
872
873 if width <= 1 {
875 let Some(idx) = self.index(x, y) else {
877 return;
878 };
879
880 if !self.current_scissor().contains(x, y) {
882 return;
883 }
884
885 let mut span_start = x;
887 let mut span_end = x.saturating_add(1);
888 if let Some(span) = self.cleanup_overlap(x, y, &cell) {
889 span_start = span_start.min(span.x0);
890 span_end = span_end.max(span.x1);
891 }
892
893 let existing_bg = self.cells[idx].bg;
894
895 let mut final_cell = if self.current_opacity() < 1.0 {
897 let opacity = self.current_opacity();
898 Cell {
899 fg: cell.fg.with_opacity(opacity),
900 bg: cell.bg.with_opacity(opacity),
901 ..cell
902 }
903 } else {
904 cell
905 };
906
907 final_cell.bg = final_cell.bg.over(existing_bg);
908
909 self.cells[idx] = final_cell;
910 self.mark_dirty_span(y, span_start, span_end);
911 self.cleanup_orphaned_tails(x.saturating_add(1), y);
912 return;
913 }
914
915 let scissor = self.current_scissor();
918 for i in 0..width {
919 let Some(cx) = x.checked_add(i as u16) else {
920 return;
921 };
922 if cx >= self.width || y >= self.height {
924 return;
925 }
926 if !scissor.contains(cx, y) {
928 return;
929 }
930 }
931
932 let mut span_start = x;
936 let mut span_end = x.saturating_add(width as u16);
937 if let Some(span) = self.cleanup_overlap(x, y, &cell) {
938 span_start = span_start.min(span.x0);
939 span_end = span_end.max(span.x1);
940 }
941 for i in 1..width {
942 if let Some(span) = self.cleanup_overlap(x + i as u16, y, &Cell::CONTINUATION) {
944 span_start = span_start.min(span.x0);
945 span_end = span_end.max(span.x1);
946 }
947 }
948
949 let idx = self.index_unchecked(x, y);
951 let old_cell = self.cells[idx];
952 let mut final_cell = if self.current_opacity() < 1.0 {
953 let opacity = self.current_opacity();
954 Cell {
955 fg: cell.fg.with_opacity(opacity),
956 bg: cell.bg.with_opacity(opacity),
957 ..cell
958 }
959 } else {
960 cell
961 };
962
963 final_cell.bg = final_cell.bg.over(old_cell.bg);
965
966 self.cells[idx] = final_cell;
967
968 for i in 1..width {
971 let idx = self.index_unchecked(x + i as u16, y);
972 self.cells[idx] = Cell::CONTINUATION;
973 }
974 self.mark_dirty_span(y, span_start, span_end);
975 self.cleanup_orphaned_tails(x.saturating_add(width as u16), y);
976 }
977
978 #[inline]
991 pub fn set_raw(&mut self, x: u16, y: u16, cell: Cell) {
992 if let Some(idx) = self.index(x, y) {
993 let mut span = DirtySpan::new(x, x.saturating_add(1));
994 let raw_wide_head = cell.content.width() > 1 && !cell.is_continuation();
995
996 if !raw_wide_head && let Some(cleanup_span) = self.cleanup_overlap(x, y, &cell) {
997 span = DirtySpan::new(span.x0.min(cleanup_span.x0), span.x1.max(cleanup_span.x1));
998 }
999 self.cells[idx] = cell;
1000 self.mark_dirty_span(y, span.x0, span.x1);
1001 if !raw_wide_head {
1002 self.cleanup_orphaned_tails(x.saturating_add(1), y);
1003 }
1004 }
1005 }
1006
1007 #[inline]
1011 pub fn fill(&mut self, rect: Rect, cell: Cell) {
1012 let clipped = self.current_scissor().intersection(&rect);
1013 if clipped.is_empty() {
1014 return;
1015 }
1016
1017 let cell_width = cell.content.width();
1020 if cell_width <= 1
1021 && !cell.is_continuation()
1022 && self.current_opacity() >= 1.0
1023 && cell.bg.a() == 255
1024 && clipped.x == 0
1025 && clipped.width == self.width
1026 {
1027 let row_width = self.width as usize;
1028 for y in clipped.y..clipped.bottom() {
1029 let row_start = y as usize * row_width;
1030 let row_end = row_start + row_width;
1031 self.cells[row_start..row_end].fill(cell);
1032 self.mark_dirty_row_full(y);
1033 }
1034 return;
1035 }
1036
1037 if cell_width <= 1
1041 && !cell.is_continuation()
1042 && self.current_opacity() >= 1.0
1043 && cell.bg.a() == 255
1044 && self.scissor_stack.len() == 1
1045 {
1046 let row_width = self.width as usize;
1047 let x_start = clipped.x as usize;
1048 let x_end = clipped.right() as usize;
1049 for y in clipped.y..clipped.bottom() {
1050 let row_start = y as usize * row_width;
1051 let mut dirty_left = clipped.x;
1052 let mut dirty_right = clipped.right();
1053
1054 if x_start > 0 && self.cells[row_start + x_start].is_continuation() {
1057 let mut head_found = None;
1058 for hx in (0..x_start).rev() {
1059 if !self.cells[row_start + hx].is_continuation() {
1060 head_found = Some(hx);
1061 break;
1062 }
1063 }
1064
1065 if let Some(hx) = head_found {
1066 let c = self.cells[row_start + hx];
1067 let width = c.content.width();
1068 if width > 1 && hx + width > x_start {
1070 for cx in hx..x_start {
1073 self.cells[row_start + cx] = Cell::default();
1074 }
1075 dirty_left = hx as u16;
1076 }
1077 }
1078 }
1079
1080 {
1083 let mut cx = x_end;
1084 while cx < row_width && self.cells[row_start + cx].is_continuation() {
1085 self.cells[row_start + cx] = Cell::default();
1086 dirty_right = (cx as u16).saturating_add(1);
1087 cx += 1;
1088 }
1089 }
1090
1091 self.cells[row_start + x_start..row_start + x_end].fill(cell);
1092 self.mark_dirty_span(y, dirty_left, dirty_right);
1093 }
1094 return;
1095 }
1096
1097 self.push_scissor(clipped);
1099
1100 let step = cell.content.width().max(1) as u16;
1101 for y in clipped.y..clipped.bottom() {
1102 let mut x = clipped.x;
1103 while x < clipped.right() {
1104 self.set(x, y, cell);
1105 x = x.saturating_add(step);
1106 }
1107 }
1108
1109 self.pop_scissor();
1110 }
1111
1112 #[inline]
1114 pub fn clear(&mut self) {
1115 self.cells.fill(Cell::default());
1116 self.mark_all_dirty();
1117 }
1118
1119 pub fn reset_for_frame(&mut self) {
1124 self.scissor_stack.truncate(1);
1125 if let Some(base) = self.scissor_stack.first_mut() {
1126 *base = Rect::from_size(self.width, self.height);
1127 } else {
1128 self.scissor_stack
1129 .push(Rect::from_size(self.width, self.height));
1130 }
1131
1132 self.opacity_stack.truncate(1);
1133 if let Some(base) = self.opacity_stack.first_mut() {
1134 *base = 1.0;
1135 } else {
1136 self.opacity_stack.push(1.0);
1137 }
1138
1139 self.clear();
1140 }
1141
1142 #[inline]
1144 pub fn clear_with(&mut self, cell: Cell) {
1145 if cell.is_continuation() {
1146 self.clear();
1147 return;
1148 }
1149
1150 let width = cell.content.width();
1151 if width <= 1 {
1152 self.cells.fill(cell);
1153 self.mark_all_dirty();
1154 return;
1155 }
1156
1157 self.cells.fill(Cell::default());
1158 let step = width as u16;
1159 for y in 0..self.height {
1160 let row_start = y as usize * self.width as usize;
1161 let mut x = 0u16;
1162 while x.saturating_add(step) <= self.width {
1163 let head_idx = row_start + x as usize;
1164 self.cells[head_idx] = cell;
1165 for off in 1..step {
1166 self.cells[head_idx + off as usize] = Cell::CONTINUATION;
1167 }
1168 x = x.saturating_add(step);
1169 }
1170 }
1171 self.mark_all_dirty();
1172 }
1173
1174 #[inline]
1178 pub fn cells(&self) -> &[Cell] {
1179 &self.cells
1180 }
1181
1182 #[inline]
1186 pub fn cells_mut(&mut self) -> &mut [Cell] {
1187 self.mark_all_dirty();
1188 &mut self.cells
1189 }
1190
1191 #[inline]
1197 pub fn row_cells(&self, y: u16) -> &[Cell] {
1198 let start = y as usize * self.width as usize;
1199 &self.cells[start..start + self.width as usize]
1200 }
1201
1202 #[inline]
1212 pub fn row_cells_mut_span(&mut self, y: u16, x0: u16, x1: u16) -> Option<&mut [Cell]> {
1213 if y >= self.height {
1214 return None;
1215 }
1216 if x0 >= x1 {
1217 return None;
1218 }
1219
1220 let start = x0.min(self.width);
1221 let end = x1.min(self.width);
1222 if start >= end {
1223 return None;
1224 }
1225
1226 self.mark_dirty_span(y, start, end);
1227
1228 let row_start = y as usize * self.width as usize;
1229 let slice_start = row_start + start as usize;
1230 let slice_end = row_start + end as usize;
1231 Some(&mut self.cells[slice_start..slice_end])
1232 }
1233
1234 #[inline]
1241 pub fn push_scissor(&mut self, rect: Rect) {
1242 let current = self.current_scissor();
1243 let intersected = current.intersection(&rect);
1244 self.scissor_stack.push(intersected);
1245 }
1246
1247 #[inline]
1251 pub fn pop_scissor(&mut self) {
1252 if self.scissor_stack.len() > 1 {
1253 self.scissor_stack.pop();
1254 }
1255 }
1256
1257 #[inline]
1259 pub fn current_scissor(&self) -> Rect {
1260 *self
1261 .scissor_stack
1262 .last()
1263 .expect("scissor stack always has at least one element")
1264 }
1265
1266 #[inline]
1268 pub fn scissor_depth(&self) -> usize {
1269 self.scissor_stack.len()
1270 }
1271
1272 #[inline]
1279 pub fn push_opacity(&mut self, opacity: f32) {
1280 let clamped = opacity.clamp(0.0, 1.0);
1281 let current = self.current_opacity();
1282 self.opacity_stack.push(current * clamped);
1283 }
1284
1285 #[inline]
1289 pub fn pop_opacity(&mut self) {
1290 if self.opacity_stack.len() > 1 {
1291 self.opacity_stack.pop();
1292 }
1293 }
1294
1295 #[inline]
1297 pub fn current_opacity(&self) -> f32 {
1298 *self
1299 .opacity_stack
1300 .last()
1301 .expect("opacity stack always has at least one element")
1302 }
1303
1304 #[inline]
1306 pub fn opacity_depth(&self) -> usize {
1307 self.opacity_stack.len()
1308 }
1309
1310 pub fn copy_from(&mut self, src: &Buffer, src_rect: Rect, dst_x: u16, dst_y: u16) {
1317 let copy_bounds = Rect::new(dst_x, dst_y, src_rect.width, src_rect.height);
1320 self.push_scissor(copy_bounds);
1321 let clip = self.current_scissor();
1322
1323 for dy in 0..src_rect.height {
1324 let Some(target_y) = dst_y.checked_add(dy) else {
1326 continue;
1327 };
1328 let Some(sy) = src_rect.y.checked_add(dy) else {
1329 continue;
1330 };
1331
1332 let mut dx = 0u16;
1333 while dx < src_rect.width {
1334 let Some(target_x) = dst_x.checked_add(dx) else {
1336 dx = dx.saturating_add(1);
1337 continue;
1338 };
1339 let Some(sx) = src_rect.x.checked_add(dx) else {
1340 dx = dx.saturating_add(1);
1341 continue;
1342 };
1343
1344 if let Some(cell) = src.get(sx, sy) {
1345 if cell.is_continuation() {
1349 self.set(target_x, target_y, Cell::default());
1350 dx = dx.saturating_add(1);
1351 continue;
1352 }
1353
1354 let width = cell.content.width();
1355 let target_right = target_x.saturating_add(width as u16);
1356
1357 let src_clipped = width > 1 && dx.saturating_add(width as u16) > src_rect.width;
1361 let dst_clipped = target_right > clip.right();
1362
1363 if src_clipped || dst_clipped {
1364 let valid_width = (clip.right().saturating_sub(target_x)).min(width as u16);
1368 for i in 0..valid_width {
1369 self.set(target_x + i, target_y, Cell::default());
1370 }
1371 } else {
1372 self.set(target_x, target_y, *cell);
1373 }
1374
1375 if width > 1 {
1377 dx = dx.saturating_add(width as u16);
1378 } else {
1379 dx = dx.saturating_add(1);
1380 }
1381 } else {
1382 dx = dx.saturating_add(1);
1383 }
1384 }
1385 }
1386
1387 self.pop_scissor();
1388 }
1389
1390 pub fn content_eq(&self, other: &Buffer) -> bool {
1392 self.width == other.width && self.height == other.height && self.cells == other.cells
1393 }
1394}
1395
1396impl Default for Buffer {
1397 fn default() -> Self {
1399 Self::new(1, 1)
1400 }
1401}
1402
1403impl PartialEq for Buffer {
1404 fn eq(&self, other: &Self) -> bool {
1405 self.content_eq(other)
1406 }
1407}
1408
1409impl Eq for Buffer {}
1410
1411#[derive(Debug)]
1430pub struct DoubleBuffer {
1431 buffers: [Buffer; 2],
1432 current_idx: u8,
1434}
1435
1436const ADAPTIVE_GROWTH_FACTOR: f32 = 1.25;
1442
1443const ADAPTIVE_SHRINK_THRESHOLD: f32 = 0.50;
1446
1447const ADAPTIVE_MAX_OVERAGE: u16 = 200;
1449
1450#[derive(Debug)]
1482pub struct AdaptiveDoubleBuffer {
1483 inner: DoubleBuffer,
1485 logical_width: u16,
1487 logical_height: u16,
1489 capacity_width: u16,
1491 capacity_height: u16,
1493 stats: AdaptiveStats,
1495}
1496
1497#[derive(Debug, Clone, Default)]
1499pub struct AdaptiveStats {
1500 pub resize_avoided: u64,
1502 pub resize_reallocated: u64,
1504 pub resize_growth: u64,
1506 pub resize_shrink: u64,
1508}
1509
1510impl AdaptiveStats {
1511 pub fn reset(&mut self) {
1513 *self = Self::default();
1514 }
1515
1516 pub fn avoidance_ratio(&self) -> f64 {
1518 let total = self.resize_avoided + self.resize_reallocated;
1519 if total == 0 {
1520 1.0
1521 } else {
1522 self.resize_avoided as f64 / total as f64
1523 }
1524 }
1525}
1526
1527impl DoubleBuffer {
1528 pub fn new(width: u16, height: u16) -> Self {
1533 Self {
1534 buffers: [Buffer::new(width, height), Buffer::new(width, height)],
1535 current_idx: 0,
1536 }
1537 }
1538
1539 #[inline]
1544 pub fn swap(&mut self) {
1545 self.current_idx = 1 - self.current_idx;
1546 }
1547
1548 #[inline]
1550 pub fn current(&self) -> &Buffer {
1551 &self.buffers[self.current_idx as usize]
1552 }
1553
1554 #[inline]
1556 pub fn current_mut(&mut self) -> &mut Buffer {
1557 &mut self.buffers[self.current_idx as usize]
1558 }
1559
1560 #[inline]
1562 pub fn previous(&self) -> &Buffer {
1563 &self.buffers[(1 - self.current_idx) as usize]
1564 }
1565
1566 #[inline]
1568 pub fn previous_mut(&mut self) -> &mut Buffer {
1569 &mut self.buffers[(1 - self.current_idx) as usize]
1570 }
1571
1572 #[inline]
1574 pub fn width(&self) -> u16 {
1575 self.buffers[0].width()
1576 }
1577
1578 #[inline]
1580 pub fn height(&self) -> u16 {
1581 self.buffers[0].height()
1582 }
1583
1584 pub fn resize(&mut self, width: u16, height: u16) -> bool {
1589 if self.buffers[0].width() == width && self.buffers[0].height() == height {
1590 return false;
1591 }
1592 self.buffers = [Buffer::new(width, height), Buffer::new(width, height)];
1593 self.current_idx = 0;
1594 true
1595 }
1596
1597 #[inline]
1599 pub fn dimensions_match(&self, width: u16, height: u16) -> bool {
1600 self.buffers[0].width() == width && self.buffers[0].height() == height
1601 }
1602}
1603
1604impl AdaptiveDoubleBuffer {
1609 pub fn new(width: u16, height: u16) -> Self {
1614 let (cap_w, cap_h) = Self::compute_capacity(width, height);
1615 Self {
1616 inner: DoubleBuffer::new(cap_w, cap_h),
1617 logical_width: width,
1618 logical_height: height,
1619 capacity_width: cap_w,
1620 capacity_height: cap_h,
1621 stats: AdaptiveStats::default(),
1622 }
1623 }
1624
1625 fn compute_capacity(width: u16, height: u16) -> (u16, u16) {
1629 let extra_w =
1630 ((width as f32 * (ADAPTIVE_GROWTH_FACTOR - 1.0)) as u16).min(ADAPTIVE_MAX_OVERAGE);
1631 let extra_h =
1632 ((height as f32 * (ADAPTIVE_GROWTH_FACTOR - 1.0)) as u16).min(ADAPTIVE_MAX_OVERAGE);
1633
1634 let cap_w = width.saturating_add(extra_w);
1635 let cap_h = height.saturating_add(extra_h);
1636
1637 (cap_w, cap_h)
1638 }
1639
1640 fn needs_reallocation(&self, width: u16, height: u16) -> bool {
1644 if width > self.capacity_width || height > self.capacity_height {
1646 return true;
1647 }
1648
1649 let shrink_threshold_w = (self.capacity_width as f32 * ADAPTIVE_SHRINK_THRESHOLD) as u16;
1651 let shrink_threshold_h = (self.capacity_height as f32 * ADAPTIVE_SHRINK_THRESHOLD) as u16;
1652
1653 width < shrink_threshold_w || height < shrink_threshold_h
1654 }
1655
1656 #[inline]
1661 pub fn swap(&mut self) {
1662 self.inner.swap();
1663 }
1664
1665 #[inline]
1670 pub fn current(&self) -> &Buffer {
1671 self.inner.current()
1672 }
1673
1674 #[inline]
1676 pub fn current_mut(&mut self) -> &mut Buffer {
1677 self.inner.current_mut()
1678 }
1679
1680 #[inline]
1682 pub fn previous(&self) -> &Buffer {
1683 self.inner.previous()
1684 }
1685
1686 #[inline]
1688 pub fn width(&self) -> u16 {
1689 self.logical_width
1690 }
1691
1692 #[inline]
1694 pub fn height(&self) -> u16 {
1695 self.logical_height
1696 }
1697
1698 #[inline]
1700 pub fn capacity_width(&self) -> u16 {
1701 self.capacity_width
1702 }
1703
1704 #[inline]
1706 pub fn capacity_height(&self) -> u16 {
1707 self.capacity_height
1708 }
1709
1710 #[inline]
1712 pub fn stats(&self) -> &AdaptiveStats {
1713 &self.stats
1714 }
1715
1716 pub fn reset_stats(&mut self) {
1718 self.stats.reset();
1719 }
1720
1721 pub fn resize(&mut self, width: u16, height: u16) -> bool {
1733 if width == self.logical_width && height == self.logical_height {
1735 return false;
1736 }
1737
1738 let is_growth = width > self.logical_width || height > self.logical_height;
1739 if is_growth {
1740 self.stats.resize_growth += 1;
1741 } else {
1742 self.stats.resize_shrink += 1;
1743 }
1744
1745 if self.needs_reallocation(width, height) {
1746 let (cap_w, cap_h) = Self::compute_capacity(width, height);
1748 self.inner = DoubleBuffer::new(cap_w, cap_h);
1749 self.capacity_width = cap_w;
1750 self.capacity_height = cap_h;
1751 self.stats.resize_reallocated += 1;
1752 } else {
1753 self.inner.current_mut().clear();
1756 self.inner.previous_mut().clear();
1757 self.stats.resize_avoided += 1;
1758 }
1759
1760 self.logical_width = width;
1761 self.logical_height = height;
1762 true
1763 }
1764
1765 #[inline]
1767 pub fn dimensions_match(&self, width: u16, height: u16) -> bool {
1768 self.logical_width == width && self.logical_height == height
1769 }
1770
1771 #[inline]
1773 pub fn logical_bounds(&self) -> Rect {
1774 Rect::from_size(self.logical_width, self.logical_height)
1775 }
1776
1777 pub fn memory_efficiency(&self) -> f64 {
1779 let logical = self.logical_width as u64 * self.logical_height as u64;
1780 let capacity = self.capacity_width as u64 * self.capacity_height as u64;
1781 if capacity == 0 {
1782 1.0
1783 } else {
1784 logical as f64 / capacity as f64
1785 }
1786 }
1787}
1788
1789#[cfg(test)]
1790mod tests {
1791 use super::*;
1792 use crate::cell::PackedRgba;
1793
1794 #[test]
1795 fn set_composites_background() {
1796 let mut buf = Buffer::new(1, 1);
1797
1798 let red = PackedRgba::rgb(255, 0, 0);
1800 buf.set(0, 0, Cell::default().with_bg(red));
1801
1802 let cell = Cell::from_char('X'); buf.set(0, 0, cell);
1805
1806 let result = buf.get(0, 0).unwrap();
1807 assert_eq!(result.content.as_char(), Some('X'));
1808 assert_eq!(
1809 result.bg, red,
1810 "Background should be preserved (composited)"
1811 );
1812 }
1813
1814 #[test]
1815 fn set_fast_matches_set_for_transparent_bg() {
1816 let red = PackedRgba::rgb(255, 0, 0);
1817 let cell = Cell::from_char('X').with_fg(PackedRgba::rgb(0, 255, 0));
1818
1819 let mut a = Buffer::new(1, 1);
1820 a.set(0, 0, Cell::default().with_bg(red));
1821 a.set(0, 0, cell);
1822
1823 let mut b = Buffer::new(1, 1);
1824 b.set(0, 0, Cell::default().with_bg(red));
1825 b.set_fast(0, 0, cell);
1826
1827 assert_eq!(a.get(0, 0), b.get(0, 0));
1828 }
1829
1830 #[test]
1831 fn set_fast_matches_set_for_opaque_bg() {
1832 let cell = Cell::from_char('X')
1833 .with_fg(PackedRgba::rgb(0, 255, 0))
1834 .with_bg(PackedRgba::rgb(255, 0, 0));
1835
1836 let mut a = Buffer::new(1, 1);
1837 a.set(0, 0, cell);
1838
1839 let mut b = Buffer::new(1, 1);
1840 b.set_fast(0, 0, cell);
1841
1842 assert_eq!(a.get(0, 0), b.get(0, 0));
1843 }
1844
1845 #[test]
1846 fn set_fast_clears_orphaned_tail_like_set() {
1847 let mut slow = Buffer::new(3, 1);
1848 slow.set_raw(0, 0, Cell::from_char('A'));
1849 slow.set_raw(1, 0, Cell::CONTINUATION);
1850 slow.clear_dirty();
1851
1852 let mut fast = slow.clone();
1853
1854 slow.set(0, 0, Cell::from_char('X'));
1855 fast.set_fast(0, 0, Cell::from_char('X'));
1856
1857 assert_eq!(slow.cells(), fast.cells());
1858 assert_eq!(fast.get(1, 0), Some(&Cell::default()));
1859
1860 let spans = fast.dirty_span_row(0).expect("dirty span row").spans();
1861 assert_eq!(spans, &[DirtySpan::new(0, 2)]);
1862 }
1863
1864 #[test]
1865 fn rect_contains() {
1866 let r = Rect::new(5, 5, 10, 10);
1867 assert!(r.contains(5, 5)); assert!(r.contains(14, 14)); assert!(!r.contains(4, 5)); assert!(!r.contains(15, 5)); assert!(!r.contains(5, 15)); }
1873
1874 #[test]
1875 fn rect_intersection() {
1876 let a = Rect::new(0, 0, 10, 10);
1877 let b = Rect::new(5, 5, 10, 10);
1878 let i = a.intersection(&b);
1879 assert_eq!(i, Rect::new(5, 5, 5, 5));
1880
1881 let c = Rect::new(20, 20, 5, 5);
1883 assert_eq!(a.intersection(&c), Rect::default());
1884 }
1885
1886 #[test]
1887 fn buffer_creation() {
1888 let buf = Buffer::new(80, 24);
1889 assert_eq!(buf.width(), 80);
1890 assert_eq!(buf.height(), 24);
1891 assert_eq!(buf.len(), 80 * 24);
1892 }
1893
1894 #[test]
1895 fn content_height_empty_is_zero() {
1896 let buf = Buffer::new(8, 4);
1897 assert_eq!(buf.content_height(), 0);
1898 }
1899
1900 #[test]
1901 fn content_height_tracks_last_non_empty_row() {
1902 let mut buf = Buffer::new(5, 4);
1903 buf.set(0, 0, Cell::from_char('A'));
1904 assert_eq!(buf.content_height(), 1);
1905
1906 buf.set(2, 3, Cell::from_char('Z'));
1907 assert_eq!(buf.content_height(), 4);
1908 }
1909
1910 #[test]
1911 fn buffer_zero_width_clamped_to_one() {
1912 let buf = Buffer::new(0, 24);
1913 assert_eq!(buf.width(), 1);
1914 assert_eq!(buf.height(), 24);
1915 }
1916
1917 #[test]
1918 fn buffer_zero_height_clamped_to_one() {
1919 let buf = Buffer::new(80, 0);
1920 assert_eq!(buf.width(), 80);
1921 assert_eq!(buf.height(), 1);
1922 }
1923
1924 #[test]
1925 fn buffer_get_and_set() {
1926 let mut buf = Buffer::new(10, 10);
1927 let cell = Cell::from_char('X');
1928 buf.set(5, 5, cell);
1929 assert_eq!(buf.get(5, 5).unwrap().content.as_char(), Some('X'));
1930 }
1931
1932 #[test]
1933 fn buffer_out_of_bounds_get() {
1934 let buf = Buffer::new(10, 10);
1935 assert!(buf.get(10, 0).is_none());
1936 assert!(buf.get(0, 10).is_none());
1937 assert!(buf.get(100, 100).is_none());
1938 }
1939
1940 #[test]
1941 fn buffer_out_of_bounds_set_ignored() {
1942 let mut buf = Buffer::new(10, 10);
1943 buf.set(100, 100, Cell::from_char('X')); assert_eq!(buf.cells().iter().filter(|c| !c.is_empty()).count(), 0);
1945 }
1946
1947 #[test]
1948 fn buffer_clear() {
1949 let mut buf = Buffer::new(10, 10);
1950 buf.set(5, 5, Cell::from_char('X'));
1951 buf.clear();
1952 assert!(buf.get(5, 5).unwrap().is_empty());
1953 }
1954
1955 #[test]
1956 fn scissor_stack_basic() {
1957 let mut buf = Buffer::new(20, 20);
1958
1959 assert_eq!(buf.current_scissor(), Rect::from_size(20, 20));
1961 assert_eq!(buf.scissor_depth(), 1);
1962
1963 buf.push_scissor(Rect::new(5, 5, 10, 10));
1965 assert_eq!(buf.current_scissor(), Rect::new(5, 5, 10, 10));
1966 assert_eq!(buf.scissor_depth(), 2);
1967
1968 buf.set(7, 7, Cell::from_char('I'));
1970 assert_eq!(buf.get(7, 7).unwrap().content.as_char(), Some('I'));
1971
1972 buf.set(0, 0, Cell::from_char('O'));
1974 assert!(buf.get(0, 0).unwrap().is_empty());
1975
1976 buf.pop_scissor();
1978 assert_eq!(buf.current_scissor(), Rect::from_size(20, 20));
1979 assert_eq!(buf.scissor_depth(), 1);
1980
1981 buf.set(0, 0, Cell::from_char('N'));
1983 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('N'));
1984 }
1985
1986 #[test]
1987 fn scissor_intersection() {
1988 let mut buf = Buffer::new(20, 20);
1989 buf.push_scissor(Rect::new(5, 5, 10, 10));
1990 buf.push_scissor(Rect::new(8, 8, 10, 10));
1991
1992 assert_eq!(buf.current_scissor(), Rect::new(8, 8, 7, 7));
1995 }
1996
1997 #[test]
1998 fn scissor_base_cannot_be_popped() {
1999 let mut buf = Buffer::new(10, 10);
2000 buf.pop_scissor(); assert_eq!(buf.scissor_depth(), 1);
2002 buf.pop_scissor(); assert_eq!(buf.scissor_depth(), 1);
2004 }
2005
2006 #[test]
2007 fn opacity_stack_basic() {
2008 let mut buf = Buffer::new(10, 10);
2009
2010 assert!((buf.current_opacity() - 1.0).abs() < f32::EPSILON);
2012 assert_eq!(buf.opacity_depth(), 1);
2013
2014 buf.push_opacity(0.5);
2016 assert!((buf.current_opacity() - 0.5).abs() < f32::EPSILON);
2017 assert_eq!(buf.opacity_depth(), 2);
2018
2019 buf.push_opacity(0.5);
2021 assert!((buf.current_opacity() - 0.25).abs() < f32::EPSILON);
2022 assert_eq!(buf.opacity_depth(), 3);
2023
2024 buf.pop_opacity();
2026 assert!((buf.current_opacity() - 0.5).abs() < f32::EPSILON);
2027 }
2028
2029 #[test]
2030 fn opacity_applied_to_cells() {
2031 let mut buf = Buffer::new(10, 10);
2032 buf.push_opacity(0.5);
2033
2034 let cell = Cell::from_char('X').with_fg(PackedRgba::rgba(100, 100, 100, 255));
2035 buf.set(5, 5, cell);
2036
2037 let stored = buf.get(5, 5).unwrap();
2038 assert_eq!(stored.fg.a(), 128);
2040 }
2041
2042 #[test]
2043 fn opacity_composites_background_before_storage() {
2044 let mut buf = Buffer::new(1, 1);
2045
2046 let red = PackedRgba::rgb(255, 0, 0);
2047 let blue = PackedRgba::rgb(0, 0, 255);
2048
2049 buf.set(0, 0, Cell::default().with_bg(red));
2050 buf.push_opacity(0.5);
2051 buf.set(0, 0, Cell::default().with_bg(blue));
2052
2053 let stored = buf.get(0, 0).unwrap();
2054 let expected = blue.with_opacity(0.5).over(red);
2055 assert_eq!(stored.bg, expected);
2056 }
2057
2058 #[test]
2059 fn opacity_clamped() {
2060 let mut buf = Buffer::new(10, 10);
2061 buf.push_opacity(2.0); assert!((buf.current_opacity() - 1.0).abs() < f32::EPSILON);
2063
2064 buf.push_opacity(-1.0); assert!((buf.current_opacity() - 0.0).abs() < f32::EPSILON);
2066 }
2067
2068 #[test]
2069 fn opacity_base_cannot_be_popped() {
2070 let mut buf = Buffer::new(10, 10);
2071 buf.pop_opacity(); assert_eq!(buf.opacity_depth(), 1);
2073 }
2074
2075 #[test]
2076 fn buffer_fill() {
2077 let mut buf = Buffer::new(10, 10);
2078 let cell = Cell::from_char('#');
2079 buf.fill(Rect::new(2, 2, 5, 5), cell);
2080
2081 assert_eq!(buf.get(3, 3).unwrap().content.as_char(), Some('#'));
2083
2084 assert!(buf.get(0, 0).unwrap().is_empty());
2086 }
2087
2088 #[test]
2089 fn buffer_fill_respects_scissor() {
2090 let mut buf = Buffer::new(10, 10);
2091 buf.push_scissor(Rect::new(3, 3, 4, 4));
2092
2093 let cell = Cell::from_char('#');
2094 buf.fill(Rect::new(0, 0, 10, 10), cell);
2095
2096 assert_eq!(buf.get(3, 3).unwrap().content.as_char(), Some('#'));
2098 assert!(buf.get(0, 0).unwrap().is_empty());
2099 assert!(buf.get(7, 7).unwrap().is_empty());
2100 }
2101
2102 #[test]
2103 fn buffer_copy_from() {
2104 let mut src = Buffer::new(10, 10);
2105 src.set(2, 2, Cell::from_char('S'));
2106
2107 let mut dst = Buffer::new(10, 10);
2108 dst.copy_from(&src, Rect::new(0, 0, 5, 5), 3, 3);
2109
2110 assert_eq!(dst.get(5, 5).unwrap().content.as_char(), Some('S'));
2112 }
2113
2114 #[test]
2115 fn copy_from_clips_wide_char_at_boundary() {
2116 let mut src = Buffer::new(10, 1);
2117 src.set(0, 0, Cell::from_char('中'));
2119
2120 let mut dst = Buffer::new(10, 1);
2121 dst.copy_from(&src, Rect::new(0, 0, 1, 1), 0, 0);
2124
2125 assert!(
2134 dst.get(0, 0).unwrap().is_empty(),
2135 "Wide char head should not be written if tail is clipped"
2136 );
2137 assert!(
2138 dst.get(1, 0).unwrap().is_empty(),
2139 "Wide char tail should not be leaked outside copy region"
2140 );
2141 }
2142
2143 #[test]
2144 fn buffer_content_eq() {
2145 let mut buf1 = Buffer::new(10, 10);
2146 let mut buf2 = Buffer::new(10, 10);
2147
2148 assert!(buf1.content_eq(&buf2));
2149
2150 buf1.set(0, 0, Cell::from_char('X'));
2151 assert!(!buf1.content_eq(&buf2));
2152
2153 buf2.set(0, 0, Cell::from_char('X'));
2154 assert!(buf1.content_eq(&buf2));
2155 }
2156
2157 #[test]
2158 fn buffer_bounds() {
2159 let buf = Buffer::new(80, 24);
2160 let bounds = buf.bounds();
2161 assert_eq!(bounds.x, 0);
2162 assert_eq!(bounds.y, 0);
2163 assert_eq!(bounds.width, 80);
2164 assert_eq!(bounds.height, 24);
2165 }
2166
2167 #[test]
2168 fn buffer_set_raw_bypasses_scissor() {
2169 let mut buf = Buffer::new(10, 10);
2170 buf.push_scissor(Rect::new(5, 5, 5, 5));
2171
2172 buf.set(0, 0, Cell::from_char('S'));
2174 assert!(buf.get(0, 0).unwrap().is_empty());
2175
2176 buf.set_raw(0, 0, Cell::from_char('R'));
2178 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('R'));
2179 }
2180
2181 #[test]
2182 fn set_handles_wide_chars() {
2183 let mut buf = Buffer::new(10, 10);
2184
2185 buf.set(0, 0, Cell::from_char('中'));
2187
2188 let head = buf.get(0, 0).unwrap();
2190 assert_eq!(head.content.as_char(), Some('中'));
2191
2192 let cont = buf.get(1, 0).unwrap();
2194 assert!(cont.is_continuation());
2195 assert!(!cont.is_empty());
2196 }
2197
2198 #[test]
2199 fn set_handles_wide_chars_clipped() {
2200 let mut buf = Buffer::new(10, 10);
2201 buf.push_scissor(Rect::new(0, 0, 1, 10)); buf.set(0, 0, Cell::from_char('中'));
2206
2207 assert!(buf.get(0, 0).unwrap().is_empty());
2209 assert!(buf.get(1, 0).unwrap().is_empty());
2211 }
2212
2213 #[test]
2216 fn overwrite_wide_head_with_single_clears_tails() {
2217 let mut buf = Buffer::new(10, 1);
2218
2219 buf.set(0, 0, Cell::from_char('中'));
2221 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2222 assert!(buf.get(1, 0).unwrap().is_continuation());
2223
2224 buf.set(0, 0, Cell::from_char('A'));
2226
2227 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('A'));
2229 assert!(
2231 buf.get(1, 0).unwrap().is_empty(),
2232 "Continuation at x=1 should be cleared when head is overwritten"
2233 );
2234 }
2235
2236 #[test]
2237 fn set_raw_overwrite_wide_head_with_single_clears_tails() {
2238 let mut buf = Buffer::new(10, 1);
2239
2240 buf.set(0, 0, Cell::from_char('中'));
2241 assert!(buf.get(1, 0).unwrap().is_continuation());
2242 buf.clear_dirty();
2243
2244 buf.set_raw(0, 0, Cell::from_char('A'));
2245
2246 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('A'));
2247 assert!(
2248 buf.get(1, 0).unwrap().is_empty(),
2249 "set_raw should clear stale continuation tails when overwriting a wide head"
2250 );
2251 let spans = buf.dirty_span_row(0).expect("dirty span row").spans();
2252 assert_eq!(spans, &[DirtySpan::new(0, 2)]);
2253 }
2254
2255 #[test]
2256 fn set_raw_wide_head_preserves_manual_tail_cells() {
2257 let mut buf = Buffer::new(10, 1);
2258
2259 buf.set_raw(0, 0, Cell::from_char('中'));
2260 buf.set_raw(1, 0, Cell::CONTINUATION);
2261 assert!(buf.get(1, 0).unwrap().is_continuation());
2262 buf.clear_dirty();
2263
2264 buf.set_raw(0, 0, Cell::from_char('日'));
2265
2266 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('日'));
2267 assert!(
2268 buf.get(1, 0).unwrap().is_continuation(),
2269 "set_raw wide-head replacement should not clear caller-managed tails"
2270 );
2271 let spans = buf.dirty_span_row(0).expect("dirty span row").spans();
2272 assert_eq!(spans, &[DirtySpan::new(0, 1)]);
2273 }
2274
2275 #[test]
2276 fn overwrite_continuation_with_single_clears_head_and_tails() {
2277 let mut buf = Buffer::new(10, 1);
2278
2279 buf.set(0, 0, Cell::from_char('中'));
2281 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2282 assert!(buf.get(1, 0).unwrap().is_continuation());
2283
2284 buf.set(1, 0, Cell::from_char('B'));
2286
2287 assert!(
2289 buf.get(0, 0).unwrap().is_empty(),
2290 "Head at x=0 should be cleared when its continuation is overwritten"
2291 );
2292 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('B'));
2294 }
2295
2296 #[test]
2297 fn overwrite_wide_with_another_wide() {
2298 let mut buf = Buffer::new(10, 1);
2299
2300 buf.set(0, 0, Cell::from_char('中'));
2302 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2303 assert!(buf.get(1, 0).unwrap().is_continuation());
2304
2305 buf.set(0, 0, Cell::from_char('日'));
2307
2308 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('日'));
2310 assert!(
2311 buf.get(1, 0).unwrap().is_continuation(),
2312 "Continuation should still exist for new wide char"
2313 );
2314 }
2315
2316 #[test]
2317 fn overwrite_continuation_middle_of_wide_sequence() {
2318 let mut buf = Buffer::new(10, 1);
2319
2320 buf.set(0, 0, Cell::from_char('中'));
2322 buf.set(2, 0, Cell::from_char('日'));
2323
2324 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2325 assert!(buf.get(1, 0).unwrap().is_continuation());
2326 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('日'));
2327 assert!(buf.get(3, 0).unwrap().is_continuation());
2328
2329 buf.set(1, 0, Cell::from_char('X'));
2331
2332 assert!(
2334 buf.get(0, 0).unwrap().is_empty(),
2335 "Head of first wide char should be cleared"
2336 );
2337 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('X'));
2339 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('日'));
2341 assert!(buf.get(3, 0).unwrap().is_continuation());
2342 }
2343
2344 #[test]
2345 fn wide_char_overlapping_previous_wide_char() {
2346 let mut buf = Buffer::new(10, 1);
2347
2348 buf.set(0, 0, Cell::from_char('中'));
2350 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2351 assert!(buf.get(1, 0).unwrap().is_continuation());
2352
2353 buf.set(1, 0, Cell::from_char('日'));
2355
2356 assert!(
2358 buf.get(0, 0).unwrap().is_empty(),
2359 "First wide char head should be cleared when continuation is overwritten by new wide"
2360 );
2361 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('日'));
2363 assert!(buf.get(2, 0).unwrap().is_continuation());
2364 }
2365
2366 #[test]
2367 fn wide_char_at_end_of_buffer_atomic_reject() {
2368 let mut buf = Buffer::new(5, 1);
2369
2370 buf.set(4, 0, Cell::from_char('中'));
2372
2373 assert!(
2375 buf.get(4, 0).unwrap().is_empty(),
2376 "Wide char should be rejected when tail would be out of bounds"
2377 );
2378 }
2379
2380 #[test]
2381 fn three_wide_chars_sequential_cleanup() {
2382 let mut buf = Buffer::new(10, 1);
2383
2384 buf.set(0, 0, Cell::from_char('一'));
2386 buf.set(2, 0, Cell::from_char('二'));
2387 buf.set(4, 0, Cell::from_char('三'));
2388
2389 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('一'));
2391 assert!(buf.get(1, 0).unwrap().is_continuation());
2392 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('二'));
2393 assert!(buf.get(3, 0).unwrap().is_continuation());
2394 assert_eq!(buf.get(4, 0).unwrap().content.as_char(), Some('三'));
2395 assert!(buf.get(5, 0).unwrap().is_continuation());
2396
2397 buf.set(3, 0, Cell::from_char('M'));
2399
2400 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('一'));
2402 assert!(buf.get(1, 0).unwrap().is_continuation());
2403 assert!(buf.get(2, 0).unwrap().is_empty());
2405 assert_eq!(buf.get(3, 0).unwrap().content.as_char(), Some('M'));
2407 assert_eq!(buf.get(4, 0).unwrap().content.as_char(), Some('三'));
2409 assert!(buf.get(5, 0).unwrap().is_continuation());
2410 }
2411
2412 #[test]
2413 fn overwrite_empty_cell_no_cleanup_needed() {
2414 let mut buf = Buffer::new(10, 1);
2415
2416 buf.set(5, 0, Cell::from_char('X'));
2418
2419 assert_eq!(buf.get(5, 0).unwrap().content.as_char(), Some('X'));
2420 assert!(buf.get(4, 0).unwrap().is_empty());
2422 assert!(buf.get(6, 0).unwrap().is_empty());
2423 }
2424
2425 #[test]
2426 fn wide_char_cleanup_with_opacity() {
2427 let mut buf = Buffer::new(10, 1);
2428
2429 buf.set(0, 0, Cell::default().with_bg(PackedRgba::rgb(255, 0, 0)));
2431 buf.set(1, 0, Cell::default().with_bg(PackedRgba::rgb(0, 255, 0)));
2432
2433 buf.set(0, 0, Cell::from_char('中'));
2435
2436 buf.push_opacity(0.5);
2438 buf.set(0, 0, Cell::from_char('A'));
2439 buf.pop_opacity();
2440
2441 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('A'));
2443 assert!(buf.get(1, 0).unwrap().is_empty());
2445 }
2446
2447 #[test]
2448 fn wide_char_continuation_not_treated_as_head() {
2449 let mut buf = Buffer::new(10, 1);
2450
2451 buf.set(0, 0, Cell::from_char('中'));
2453
2454 let cont = buf.get(1, 0).unwrap();
2456 assert!(cont.is_continuation());
2457 assert_eq!(cont.content.width(), 0);
2458
2459 buf.set(1, 0, Cell::from_char('日'));
2461
2462 assert!(buf.get(0, 0).unwrap().is_empty());
2464 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('日'));
2466 assert!(buf.get(2, 0).unwrap().is_continuation());
2467 }
2468
2469 #[test]
2470 fn wide_char_fill_region() {
2471 let mut buf = Buffer::new(10, 3);
2472
2473 let wide_cell = Cell::from_char('中');
2476 buf.fill(Rect::new(0, 0, 4, 2), wide_cell);
2477
2478 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2480 assert!(buf.get(1, 0).unwrap().is_continuation());
2481 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('中'));
2482 assert!(buf.get(3, 0).unwrap().is_continuation());
2483 }
2484
2485 #[test]
2486 fn default_buffer_dimensions() {
2487 let buf = Buffer::default();
2488 assert_eq!(buf.width(), 1);
2489 assert_eq!(buf.height(), 1);
2490 assert_eq!(buf.len(), 1);
2491 }
2492
2493 #[test]
2494 fn buffer_partial_eq_impl() {
2495 let buf1 = Buffer::new(5, 5);
2496 let buf2 = Buffer::new(5, 5);
2497 let mut buf3 = Buffer::new(5, 5);
2498 buf3.set(0, 0, Cell::from_char('X'));
2499
2500 assert_eq!(buf1, buf2);
2501 assert_ne!(buf1, buf3);
2502 }
2503
2504 #[test]
2505 fn degradation_level_accessible() {
2506 let mut buf = Buffer::new(10, 10);
2507 assert_eq!(buf.degradation, DegradationLevel::Full);
2508
2509 buf.degradation = DegradationLevel::SimpleBorders;
2510 assert_eq!(buf.degradation, DegradationLevel::SimpleBorders);
2511 }
2512
2513 #[test]
2516 fn get_mut_modifies_cell() {
2517 let mut buf = Buffer::new(10, 10);
2518 buf.set(3, 3, Cell::from_char('A'));
2519
2520 if let Some(cell) = buf.get_mut(3, 3) {
2521 *cell = Cell::from_char('B');
2522 }
2523
2524 assert_eq!(buf.get(3, 3).unwrap().content.as_char(), Some('B'));
2525 }
2526
2527 #[test]
2528 fn get_mut_out_of_bounds() {
2529 let mut buf = Buffer::new(5, 5);
2530 assert!(buf.get_mut(10, 10).is_none());
2531 }
2532
2533 #[test]
2536 fn clear_with_fills_all_cells() {
2537 let mut buf = Buffer::new(5, 3);
2538 let fill_cell = Cell::from_char('*');
2539 buf.clear_with(fill_cell);
2540
2541 for y in 0..3 {
2542 for x in 0..5 {
2543 assert_eq!(buf.get(x, y).unwrap().content.as_char(), Some('*'));
2544 }
2545 }
2546 }
2547
2548 #[test]
2549 fn clear_with_wide_cell_preserves_head_tail_invariant() {
2550 let mut buf = Buffer::new(5, 2);
2551 buf.clear_with(Cell::from_char('中'));
2552
2553 for y in 0..2 {
2554 assert_eq!(buf.get(0, y).unwrap().content.as_char(), Some('中'));
2555 assert!(buf.get(1, y).unwrap().is_continuation());
2556 assert_eq!(buf.get(2, y).unwrap().content.as_char(), Some('中'));
2557 assert!(buf.get(3, y).unwrap().is_continuation());
2558 assert!(buf.get(4, y).unwrap().is_empty());
2559 }
2560 }
2561
2562 #[test]
2565 fn cells_slice_has_correct_length() {
2566 let buf = Buffer::new(10, 5);
2567 assert_eq!(buf.cells().len(), 50);
2568 }
2569
2570 #[test]
2571 fn cells_mut_allows_direct_modification() {
2572 let mut buf = Buffer::new(3, 2);
2573 let cells = buf.cells_mut();
2574 cells[0] = Cell::from_char('Z');
2575
2576 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('Z'));
2577 }
2578
2579 #[test]
2582 fn row_cells_returns_correct_row() {
2583 let mut buf = Buffer::new(5, 3);
2584 buf.set(2, 1, Cell::from_char('R'));
2585
2586 let row = buf.row_cells(1);
2587 assert_eq!(row.len(), 5);
2588 assert_eq!(row[2].content.as_char(), Some('R'));
2589 }
2590
2591 #[test]
2592 fn row_cells_mut_span_marks_once_and_returns_slice() {
2593 let mut buf = Buffer::new(5, 3);
2594 buf.clear_dirty();
2595
2596 let row = buf
2597 .row_cells_mut_span(1, 1, 4)
2598 .expect("row span should be in bounds");
2599 assert_eq!(row.len(), 3);
2600 row[0] = Cell::from_char('A');
2601 row[1] = Cell::from_char('B');
2602 row[2] = Cell::from_char('C');
2603
2604 assert!(buf.is_row_dirty(1));
2605 let spans = buf.dirty_span_row(1).expect("dirty span row").spans();
2606 assert_eq!(spans, &[DirtySpan::new(1, 4)]);
2607 assert_eq!(buf.get(1, 1).unwrap().content.as_char(), Some('A'));
2608 assert_eq!(buf.get(2, 1).unwrap().content.as_char(), Some('B'));
2609 assert_eq!(buf.get(3, 1).unwrap().content.as_char(), Some('C'));
2610 }
2611
2612 #[test]
2613 fn row_cells_mut_span_clamps_to_buffer_width() {
2614 let mut buf = Buffer::new(5, 1);
2615 buf.clear_dirty();
2616
2617 let row = buf
2618 .row_cells_mut_span(0, 3, 99)
2619 .expect("row span should clamp");
2620 assert_eq!(row.len(), 2);
2621 row[0] = Cell::from_char('X');
2622 row[1] = Cell::from_char('Y');
2623
2624 let spans = buf.dirty_span_row(0).expect("dirty span row").spans();
2625 assert_eq!(spans, &[DirtySpan::new(3, 5)]);
2626 assert_eq!(buf.get(3, 0).unwrap().content.as_char(), Some('X'));
2627 assert_eq!(buf.get(4, 0).unwrap().content.as_char(), Some('Y'));
2628 }
2629
2630 #[test]
2631 fn row_cells_mut_span_rejects_reversed_ranges() {
2632 let mut buf = Buffer::new(5, 1);
2633 buf.clear_dirty();
2634
2635 assert!(buf.row_cells_mut_span(0, 4, 2).is_none());
2636 assert!(
2637 !buf.is_row_dirty(0),
2638 "reversed ranges should not mark rows dirty"
2639 );
2640 assert!(
2641 buf.dirty_span_row(0)
2642 .expect("dirty span row")
2643 .spans()
2644 .is_empty(),
2645 "reversed ranges should not add dirty spans"
2646 );
2647 }
2648
2649 #[test]
2650 #[should_panic]
2651 fn row_cells_out_of_bounds_panics() {
2652 let buf = Buffer::new(5, 3);
2653 let _ = buf.row_cells(5);
2654 }
2655
2656 #[test]
2659 fn buffer_is_not_empty() {
2660 let buf = Buffer::new(1, 1);
2661 assert!(!buf.is_empty());
2662 }
2663
2664 #[test]
2667 fn set_raw_out_of_bounds_is_safe() {
2668 let mut buf = Buffer::new(5, 5);
2669 buf.set_raw(100, 100, Cell::from_char('X'));
2670 }
2672
2673 #[test]
2676 fn copy_from_out_of_bounds_partial() {
2677 let mut src = Buffer::new(5, 5);
2678 src.set(0, 0, Cell::from_char('A'));
2679 src.set(4, 4, Cell::from_char('B'));
2680
2681 let mut dst = Buffer::new(5, 5);
2682 dst.copy_from(&src, Rect::new(0, 0, 5, 5), 3, 3);
2684
2685 assert_eq!(dst.get(3, 3).unwrap().content.as_char(), Some('A'));
2687 assert!(dst.get(4, 4).unwrap().is_empty());
2689 }
2690
2691 #[test]
2694 fn content_eq_different_dimensions() {
2695 let buf1 = Buffer::new(5, 5);
2696 let buf2 = Buffer::new(10, 10);
2697 assert!(!buf1.content_eq(&buf2));
2699 }
2700
2701 mod property {
2704 use super::*;
2705 use proptest::prelude::*;
2706
2707 proptest! {
2708 #[test]
2709 fn buffer_dimensions_are_preserved(width in 1u16..200, height in 1u16..200) {
2710 let buf = Buffer::new(width, height);
2711 prop_assert_eq!(buf.width(), width);
2712 prop_assert_eq!(buf.height(), height);
2713 prop_assert_eq!(buf.len(), width as usize * height as usize);
2714 }
2715
2716 #[test]
2717 fn buffer_get_in_bounds_always_succeeds(width in 1u16..100, height in 1u16..100) {
2718 let buf = Buffer::new(width, height);
2719 for x in 0..width {
2720 for y in 0..height {
2721 prop_assert!(buf.get(x, y).is_some(), "get({x},{y}) failed for {width}x{height} buffer");
2722 }
2723 }
2724 }
2725
2726 #[test]
2727 fn buffer_get_out_of_bounds_returns_none(width in 1u16..50, height in 1u16..50) {
2728 let buf = Buffer::new(width, height);
2729 prop_assert!(buf.get(width, 0).is_none());
2730 prop_assert!(buf.get(0, height).is_none());
2731 prop_assert!(buf.get(width, height).is_none());
2732 }
2733
2734 #[test]
2735 fn buffer_set_get_roundtrip(
2736 width in 5u16..50,
2737 height in 5u16..50,
2738 x in 0u16..5,
2739 y in 0u16..5,
2740 ch_idx in 0u32..26,
2741 ) {
2742 let x = x % width;
2743 let y = y % height;
2744 let ch = char::from_u32('A' as u32 + ch_idx).unwrap();
2745 let mut buf = Buffer::new(width, height);
2746 buf.set(x, y, Cell::from_char(ch));
2747 let got = buf.get(x, y).unwrap();
2748 prop_assert_eq!(got.content.as_char(), Some(ch));
2749 }
2750
2751 #[test]
2752 fn scissor_push_pop_stack_depth(
2753 width in 10u16..50,
2754 height in 10u16..50,
2755 push_count in 1usize..10,
2756 ) {
2757 let mut buf = Buffer::new(width, height);
2758 prop_assert_eq!(buf.scissor_depth(), 1); for i in 0..push_count {
2761 buf.push_scissor(Rect::new(0, 0, width, height));
2762 prop_assert_eq!(buf.scissor_depth(), i + 2);
2763 }
2764
2765 for i in (0..push_count).rev() {
2766 buf.pop_scissor();
2767 prop_assert_eq!(buf.scissor_depth(), i + 1);
2768 }
2769
2770 buf.pop_scissor();
2772 prop_assert_eq!(buf.scissor_depth(), 1);
2773 }
2774
2775 #[test]
2776 fn scissor_monotonic_intersection(
2777 width in 20u16..60,
2778 height in 20u16..60,
2779 ) {
2780 let mut buf = Buffer::new(width, height);
2782 let outer = Rect::new(2, 2, width - 4, height - 4);
2783 buf.push_scissor(outer);
2784 let s1 = buf.current_scissor();
2785
2786 let inner = Rect::new(5, 5, 10, 10);
2787 buf.push_scissor(inner);
2788 let s2 = buf.current_scissor();
2789
2790 prop_assert!(s2.width <= s1.width, "inner width {} > outer width {}", s2.width, s1.width);
2792 prop_assert!(s2.height <= s1.height, "inner height {} > outer height {}", s2.height, s1.height);
2793 }
2794
2795 #[test]
2796 fn opacity_push_pop_stack_depth(
2797 width in 5u16..20,
2798 height in 5u16..20,
2799 push_count in 1usize..10,
2800 ) {
2801 let mut buf = Buffer::new(width, height);
2802 prop_assert_eq!(buf.opacity_depth(), 1);
2803
2804 for i in 0..push_count {
2805 buf.push_opacity(0.9);
2806 prop_assert_eq!(buf.opacity_depth(), i + 2);
2807 }
2808
2809 for i in (0..push_count).rev() {
2810 buf.pop_opacity();
2811 prop_assert_eq!(buf.opacity_depth(), i + 1);
2812 }
2813
2814 buf.pop_opacity();
2815 prop_assert_eq!(buf.opacity_depth(), 1);
2816 }
2817
2818 #[test]
2819 fn opacity_multiplication_is_monotonic(
2820 opacity1 in 0.0f32..=1.0,
2821 opacity2 in 0.0f32..=1.0,
2822 ) {
2823 let mut buf = Buffer::new(5, 5);
2824 buf.push_opacity(opacity1);
2825 let after_first = buf.current_opacity();
2826 buf.push_opacity(opacity2);
2827 let after_second = buf.current_opacity();
2828
2829 prop_assert!(after_second <= after_first + f32::EPSILON,
2831 "opacity increased: {} -> {}", after_first, after_second);
2832 }
2833
2834 #[test]
2835 fn clear_resets_all_cells(width in 1u16..30, height in 1u16..30) {
2836 let mut buf = Buffer::new(width, height);
2837 for x in 0..width {
2839 buf.set_raw(x, 0, Cell::from_char('X'));
2840 }
2841 buf.clear();
2842 for y in 0..height {
2844 for x in 0..width {
2845 prop_assert!(buf.get(x, y).unwrap().is_empty(),
2846 "cell ({x},{y}) not empty after clear");
2847 }
2848 }
2849 }
2850
2851 #[test]
2852 fn content_eq_is_reflexive(width in 1u16..30, height in 1u16..30) {
2853 let buf = Buffer::new(width, height);
2854 prop_assert!(buf.content_eq(&buf));
2855 }
2856
2857 #[test]
2858 fn content_eq_detects_single_change(
2859 width in 5u16..30,
2860 height in 5u16..30,
2861 x in 0u16..5,
2862 y in 0u16..5,
2863 ) {
2864 let x = x % width;
2865 let y = y % height;
2866 let buf1 = Buffer::new(width, height);
2867 let mut buf2 = Buffer::new(width, height);
2868 buf2.set_raw(x, y, Cell::from_char('Z'));
2869 prop_assert!(!buf1.content_eq(&buf2));
2870 }
2871
2872 #[test]
2875 fn dimensions_immutable_through_operations(
2876 width in 5u16..30,
2877 height in 5u16..30,
2878 ) {
2879 let mut buf = Buffer::new(width, height);
2880
2881 buf.set(0, 0, Cell::from_char('A'));
2883 prop_assert_eq!(buf.width(), width);
2884 prop_assert_eq!(buf.height(), height);
2885 prop_assert_eq!(buf.len(), width as usize * height as usize);
2886
2887 buf.push_scissor(Rect::new(1, 1, 3, 3));
2888 prop_assert_eq!(buf.width(), width);
2889 prop_assert_eq!(buf.height(), height);
2890
2891 buf.push_opacity(0.5);
2892 prop_assert_eq!(buf.width(), width);
2893 prop_assert_eq!(buf.height(), height);
2894
2895 buf.pop_scissor();
2896 buf.pop_opacity();
2897 prop_assert_eq!(buf.width(), width);
2898 prop_assert_eq!(buf.height(), height);
2899
2900 buf.clear();
2901 prop_assert_eq!(buf.width(), width);
2902 prop_assert_eq!(buf.height(), height);
2903 prop_assert_eq!(buf.len(), width as usize * height as usize);
2904 }
2905
2906 #[test]
2907 fn scissor_area_never_increases_random_rects(
2908 width in 20u16..60,
2909 height in 20u16..60,
2910 rects in proptest::collection::vec(
2911 (0u16..20, 0u16..20, 1u16..15, 1u16..15),
2912 1..8
2913 ),
2914 ) {
2915 let mut buf = Buffer::new(width, height);
2916 let mut prev_area = (width as u32) * (height as u32);
2917
2918 for (x, y, w, h) in rects {
2919 buf.push_scissor(Rect::new(x, y, w, h));
2920 let s = buf.current_scissor();
2921 let area = (s.width as u32) * (s.height as u32);
2922 prop_assert!(area <= prev_area,
2923 "scissor area increased: {} -> {} after push({},{},{},{})",
2924 prev_area, area, x, y, w, h);
2925 prev_area = area;
2926 }
2927 }
2928
2929 #[test]
2930 fn opacity_range_invariant_random_sequence(
2931 opacities in proptest::collection::vec(0.0f32..=1.0, 1..15),
2932 ) {
2933 let mut buf = Buffer::new(5, 5);
2934
2935 for &op in &opacities {
2936 buf.push_opacity(op);
2937 let current = buf.current_opacity();
2938 prop_assert!(current >= 0.0, "opacity below 0: {}", current);
2939 prop_assert!(current <= 1.0 + f32::EPSILON,
2940 "opacity above 1: {}", current);
2941 }
2942
2943 for _ in &opacities {
2945 buf.pop_opacity();
2946 }
2947 prop_assert!((buf.current_opacity() - 1.0).abs() < f32::EPSILON);
2949 }
2950
2951 #[test]
2952 fn opacity_clamp_out_of_range(
2953 neg in -100.0f32..0.0,
2954 over in 1.01f32..100.0,
2955 ) {
2956 let mut buf = Buffer::new(5, 5);
2957
2958 buf.push_opacity(neg);
2959 prop_assert!(buf.current_opacity() >= 0.0,
2960 "negative opacity not clamped: {}", buf.current_opacity());
2961 buf.pop_opacity();
2962
2963 buf.push_opacity(over);
2964 prop_assert!(buf.current_opacity() <= 1.0 + f32::EPSILON,
2965 "over-1 opacity not clamped: {}", buf.current_opacity());
2966 }
2967
2968 #[test]
2969 fn scissor_stack_always_has_base(
2970 pushes in 0usize..10,
2971 pops in 0usize..15,
2972 ) {
2973 let mut buf = Buffer::new(10, 10);
2974
2975 for _ in 0..pushes {
2976 buf.push_scissor(Rect::new(0, 0, 5, 5));
2977 }
2978 for _ in 0..pops {
2979 buf.pop_scissor();
2980 }
2981
2982 prop_assert!(buf.scissor_depth() >= 1,
2984 "scissor depth dropped below 1 after {} pushes, {} pops",
2985 pushes, pops);
2986 }
2987
2988 #[test]
2989 fn opacity_stack_always_has_base(
2990 pushes in 0usize..10,
2991 pops in 0usize..15,
2992 ) {
2993 let mut buf = Buffer::new(10, 10);
2994
2995 for _ in 0..pushes {
2996 buf.push_opacity(0.5);
2997 }
2998 for _ in 0..pops {
2999 buf.pop_opacity();
3000 }
3001
3002 prop_assert!(buf.opacity_depth() >= 1,
3004 "opacity depth dropped below 1 after {} pushes, {} pops",
3005 pushes, pops);
3006 }
3007
3008 #[test]
3009 fn cells_len_invariant_always_holds(
3010 width in 1u16..50,
3011 height in 1u16..50,
3012 ) {
3013 let mut buf = Buffer::new(width, height);
3014 let expected = width as usize * height as usize;
3015
3016 prop_assert_eq!(buf.cells().len(), expected);
3017
3018 buf.set(0, 0, Cell::from_char('X'));
3020 prop_assert_eq!(buf.cells().len(), expected);
3021
3022 buf.clear();
3023 prop_assert_eq!(buf.cells().len(), expected);
3024 }
3025
3026 #[test]
3027 fn set_outside_scissor_is_noop(
3028 width in 10u16..30,
3029 height in 10u16..30,
3030 ) {
3031 let mut buf = Buffer::new(width, height);
3032 buf.push_scissor(Rect::new(2, 2, 3, 3));
3033
3034 buf.set(0, 0, Cell::from_char('X'));
3036 let cell = buf.get(0, 0).unwrap();
3038 prop_assert!(cell.is_empty(),
3039 "cell (0,0) modified outside scissor region");
3040
3041 buf.set(3, 3, Cell::from_char('Y'));
3043 let cell = buf.get(3, 3).unwrap();
3044 prop_assert_eq!(cell.content.as_char(), Some('Y'));
3045 }
3046
3047 #[test]
3050 fn wide_char_overwrites_cleanup_tails(
3051 width in 10u16..30,
3052 x in 0u16..8,
3053 ) {
3054 let x = x % (width.saturating_sub(2).max(1));
3055 let mut buf = Buffer::new(width, 1);
3056
3057 buf.set(x, 0, Cell::from_char('中'));
3059
3060 if x + 1 < width {
3062 let head = buf.get(x, 0).unwrap();
3063 let tail = buf.get(x + 1, 0).unwrap();
3064
3065 if head.content.as_char() == Some('中') {
3066 prop_assert!(tail.is_continuation(),
3067 "tail at x+1={} should be continuation", x + 1);
3068
3069 buf.set(x, 0, Cell::from_char('A'));
3071 let new_head = buf.get(x, 0).unwrap();
3072 let cleared_tail = buf.get(x + 1, 0).unwrap();
3073
3074 prop_assert_eq!(new_head.content.as_char(), Some('A'));
3075 prop_assert!(cleared_tail.is_empty(),
3076 "tail should be cleared after head overwrite");
3077 }
3078 }
3079 }
3080
3081 #[test]
3082 fn wide_char_atomic_rejection_at_boundary(
3083 width in 3u16..20,
3084 ) {
3085 let mut buf = Buffer::new(width, 1);
3086
3087 let last_pos = width - 1;
3089 buf.set(last_pos, 0, Cell::from_char('中'));
3090
3091 let cell = buf.get(last_pos, 0).unwrap();
3093 prop_assert!(cell.is_empty(),
3094 "wide char at boundary position {} (width {}) should be rejected",
3095 last_pos, width);
3096 }
3097
3098 #[test]
3103 fn double_buffer_swap_is_involution(ops in proptest::collection::vec(proptest::bool::ANY, 0..100)) {
3104 let mut db = DoubleBuffer::new(10, 10);
3105 let initial_idx = db.current_idx;
3106
3107 for do_swap in &ops {
3108 if *do_swap {
3109 db.swap();
3110 }
3111 }
3112
3113 let swap_count = ops.iter().filter(|&&x| x).count();
3114 let expected_idx = if swap_count % 2 == 0 { initial_idx } else { 1 - initial_idx };
3115
3116 prop_assert_eq!(db.current_idx, expected_idx,
3117 "After {} swaps, index should be {} but was {}",
3118 swap_count, expected_idx, db.current_idx);
3119 }
3120
3121 #[test]
3122 fn double_buffer_resize_preserves_invariant(
3123 init_w in 1u16..200,
3124 init_h in 1u16..100,
3125 new_w in 1u16..200,
3126 new_h in 1u16..100,
3127 ) {
3128 let mut db = DoubleBuffer::new(init_w, init_h);
3129 db.resize(new_w, new_h);
3130
3131 prop_assert_eq!(db.width(), new_w);
3132 prop_assert_eq!(db.height(), new_h);
3133 prop_assert!(db.dimensions_match(new_w, new_h));
3134 }
3135
3136 #[test]
3137 fn double_buffer_current_previous_disjoint(
3138 width in 1u16..50,
3139 height in 1u16..50,
3140 ) {
3141 let mut db = DoubleBuffer::new(width, height);
3142
3143 db.current_mut().set(0, 0, Cell::from_char('C'));
3145
3146 prop_assert!(db.previous().get(0, 0).unwrap().is_empty(),
3148 "Previous buffer should not reflect changes to current");
3149
3150 db.swap();
3152 prop_assert_eq!(db.previous().get(0, 0).unwrap().content.as_char(), Some('C'),
3153 "After swap, previous should have the 'C' we wrote");
3154 }
3155
3156 #[test]
3157 fn double_buffer_swap_content_semantics(
3158 width in 5u16..30,
3159 height in 5u16..30,
3160 ) {
3161 let mut db = DoubleBuffer::new(width, height);
3162
3163 db.current_mut().set(0, 0, Cell::from_char('X'));
3165 db.swap();
3166
3167 db.current_mut().set(0, 0, Cell::from_char('Y'));
3169 db.swap();
3170
3171 prop_assert_eq!(db.current().get(0, 0).unwrap().content.as_char(), Some('X'));
3173 prop_assert_eq!(db.previous().get(0, 0).unwrap().content.as_char(), Some('Y'));
3174 }
3175
3176 #[test]
3177 fn double_buffer_resize_clears_both(
3178 w1 in 5u16..30,
3179 h1 in 5u16..30,
3180 w2 in 5u16..30,
3181 h2 in 5u16..30,
3182 ) {
3183 prop_assume!(w1 != w2 || h1 != h2);
3185
3186 let mut db = DoubleBuffer::new(w1, h1);
3187
3188 db.current_mut().set(0, 0, Cell::from_char('A'));
3190 db.swap();
3191 db.current_mut().set(0, 0, Cell::from_char('B'));
3192
3193 db.resize(w2, h2);
3195
3196 prop_assert!(db.current().get(0, 0).unwrap().is_empty(),
3198 "Current buffer should be empty after resize");
3199 prop_assert!(db.previous().get(0, 0).unwrap().is_empty(),
3200 "Previous buffer should be empty after resize");
3201 }
3202 }
3203 }
3204
3205 #[test]
3208 fn dirty_rows_start_dirty() {
3209 let buf = Buffer::new(10, 5);
3211 assert_eq!(buf.dirty_row_count(), 5);
3212 for y in 0..5 {
3213 assert!(buf.is_row_dirty(y));
3214 }
3215 }
3216
3217 #[test]
3218 fn dirty_bitmap_starts_full() {
3219 let buf = Buffer::new(4, 3);
3220 assert!(buf.dirty_all());
3221 assert_eq!(buf.dirty_cell_count(), 12);
3222 }
3223
3224 #[test]
3225 fn dirty_bitmap_tracks_single_cell() {
3226 let mut buf = Buffer::new(4, 3);
3227 buf.clear_dirty();
3228 assert!(!buf.dirty_all());
3229 buf.set_raw(1, 1, Cell::from_char('X'));
3230 let idx = 1 + 4;
3231 assert_eq!(buf.dirty_cell_count(), 1);
3232 assert_eq!(buf.dirty_bits()[idx], 1);
3233 }
3234
3235 #[test]
3236 fn dirty_bitmap_dedupes_cells() {
3237 let mut buf = Buffer::new(4, 3);
3238 buf.clear_dirty();
3239 buf.set_raw(2, 2, Cell::from_char('A'));
3240 buf.set_raw(2, 2, Cell::from_char('B'));
3241 assert_eq!(buf.dirty_cell_count(), 1);
3242 }
3243
3244 #[test]
3245 fn set_marks_row_dirty() {
3246 let mut buf = Buffer::new(10, 5);
3247 buf.clear_dirty(); buf.set(3, 2, Cell::from_char('X'));
3249 assert!(buf.is_row_dirty(2));
3250 assert!(!buf.is_row_dirty(0));
3251 assert!(!buf.is_row_dirty(1));
3252 assert!(!buf.is_row_dirty(3));
3253 assert!(!buf.is_row_dirty(4));
3254 }
3255
3256 #[test]
3257 fn set_raw_marks_row_dirty() {
3258 let mut buf = Buffer::new(10, 5);
3259 buf.clear_dirty(); buf.set_raw(0, 4, Cell::from_char('Z'));
3261 assert!(buf.is_row_dirty(4));
3262 assert_eq!(buf.dirty_row_count(), 1);
3263 }
3264
3265 #[test]
3266 fn clear_marks_all_dirty() {
3267 let mut buf = Buffer::new(10, 5);
3268 buf.clear();
3269 assert_eq!(buf.dirty_row_count(), 5);
3270 }
3271
3272 #[test]
3273 fn clear_dirty_resets_flags() {
3274 let mut buf = Buffer::new(10, 5);
3275 assert_eq!(buf.dirty_row_count(), 5);
3277 buf.clear_dirty();
3278 assert_eq!(buf.dirty_row_count(), 0);
3279
3280 buf.set(0, 0, Cell::from_char('A'));
3282 buf.set(0, 3, Cell::from_char('B'));
3283 assert_eq!(buf.dirty_row_count(), 2);
3284
3285 buf.clear_dirty();
3286 assert_eq!(buf.dirty_row_count(), 0);
3287 }
3288
3289 #[test]
3290 fn clear_dirty_resets_bitmap() {
3291 let mut buf = Buffer::new(4, 3);
3292 buf.clear();
3293 assert!(buf.dirty_all());
3294 buf.clear_dirty();
3295 assert!(!buf.dirty_all());
3296 assert_eq!(buf.dirty_cell_count(), 0);
3297 assert!(buf.dirty_bits().iter().all(|&b| b == 0));
3298 }
3299
3300 #[test]
3301 fn fill_marks_affected_rows_dirty() {
3302 let mut buf = Buffer::new(10, 10);
3303 buf.clear_dirty(); buf.fill(Rect::new(0, 2, 5, 3), Cell::from_char('.'));
3305 assert!(!buf.is_row_dirty(0));
3307 assert!(!buf.is_row_dirty(1));
3308 assert!(buf.is_row_dirty(2));
3309 assert!(buf.is_row_dirty(3));
3310 assert!(buf.is_row_dirty(4));
3311 assert!(!buf.is_row_dirty(5));
3312 }
3313
3314 #[test]
3315 fn get_mut_marks_row_dirty() {
3316 let mut buf = Buffer::new(10, 5);
3317 buf.clear_dirty(); if let Some(cell) = buf.get_mut(5, 3) {
3319 cell.fg = PackedRgba::rgb(255, 0, 0);
3320 }
3321 assert!(buf.is_row_dirty(3));
3322 assert_eq!(buf.dirty_row_count(), 1);
3323 }
3324
3325 #[test]
3326 fn cells_mut_marks_all_dirty() {
3327 let mut buf = Buffer::new(10, 5);
3328 let _ = buf.cells_mut();
3329 assert_eq!(buf.dirty_row_count(), 5);
3330 }
3331
3332 #[test]
3333 fn dirty_rows_slice_length_matches_height() {
3334 let buf = Buffer::new(10, 7);
3335 assert_eq!(buf.dirty_rows().len(), 7);
3336 }
3337
3338 #[test]
3339 fn out_of_bounds_set_does_not_dirty() {
3340 let mut buf = Buffer::new(10, 5);
3341 buf.clear_dirty(); buf.set(100, 100, Cell::from_char('X'));
3343 assert_eq!(buf.dirty_row_count(), 0);
3344 }
3345
3346 #[test]
3347 fn property_dirty_soundness() {
3348 let mut buf = Buffer::new(20, 10);
3350 let positions = [(3, 0), (5, 2), (0, 9), (19, 5), (10, 7)];
3351 for &(x, y) in &positions {
3352 buf.set(x, y, Cell::from_char('*'));
3353 }
3354 for &(_, y) in &positions {
3355 assert!(
3356 buf.is_row_dirty(y),
3357 "Row {} should be dirty after set({}, {})",
3358 y,
3359 positions.iter().find(|(_, ry)| *ry == y).unwrap().0,
3360 y
3361 );
3362 }
3363 }
3364
3365 #[test]
3366 fn dirty_clear_between_frames() {
3367 let mut buf = Buffer::new(10, 5);
3369
3370 assert_eq!(buf.dirty_row_count(), 5);
3372
3373 buf.clear_dirty();
3375 assert_eq!(buf.dirty_row_count(), 0);
3376
3377 buf.set(0, 0, Cell::from_char('A'));
3379 buf.set(0, 2, Cell::from_char('B'));
3380 assert_eq!(buf.dirty_row_count(), 2);
3381
3382 buf.clear_dirty();
3384 assert_eq!(buf.dirty_row_count(), 0);
3385
3386 buf.set(0, 4, Cell::from_char('C'));
3388 assert_eq!(buf.dirty_row_count(), 1);
3389 assert!(buf.is_row_dirty(4));
3390 assert!(!buf.is_row_dirty(0));
3391 }
3392
3393 #[test]
3396 fn dirty_spans_start_full_dirty() {
3397 let buf = Buffer::new(10, 5);
3398 for y in 0..5 {
3399 let row = buf.dirty_span_row(y).unwrap();
3400 assert!(row.is_full(), "row {y} should start full-dirty");
3401 assert!(row.spans().is_empty(), "row {y} spans should start empty");
3402 }
3403 }
3404
3405 #[test]
3406 fn clear_dirty_resets_spans() {
3407 let mut buf = Buffer::new(10, 5);
3408 buf.clear_dirty();
3409 for y in 0..5 {
3410 let row = buf.dirty_span_row(y).unwrap();
3411 assert!(!row.is_full(), "row {y} should clear full-dirty");
3412 assert!(row.spans().is_empty(), "row {y} spans should be cleared");
3413 }
3414 assert_eq!(buf.dirty_span_overflows, 0);
3415 }
3416
3417 #[test]
3418 fn set_records_dirty_span() {
3419 let mut buf = Buffer::new(20, 2);
3420 buf.clear_dirty();
3421 buf.set(2, 0, Cell::from_char('A'));
3422 let row = buf.dirty_span_row(0).unwrap();
3423 assert_eq!(row.spans(), &[DirtySpan::new(2, 3)]);
3424 assert!(!row.is_full());
3425 }
3426
3427 #[test]
3428 fn set_merges_adjacent_spans() {
3429 let mut buf = Buffer::new(20, 2);
3430 buf.clear_dirty();
3431 buf.set(2, 0, Cell::from_char('A'));
3432 buf.set(3, 0, Cell::from_char('B')); let row = buf.dirty_span_row(0).unwrap();
3434 assert_eq!(row.spans(), &[DirtySpan::new(2, 4)]);
3435 }
3436
3437 #[test]
3438 fn set_merges_close_spans() {
3439 let mut buf = Buffer::new(20, 2);
3440 buf.clear_dirty();
3441 buf.set(2, 0, Cell::from_char('A'));
3442 buf.set(4, 0, Cell::from_char('B')); let row = buf.dirty_span_row(0).unwrap();
3444 assert_eq!(row.spans(), &[DirtySpan::new(2, 5)]);
3445 }
3446
3447 #[test]
3448 fn span_overflow_sets_full_row() {
3449 let width = (DIRTY_SPAN_MAX_SPANS_PER_ROW as u16 + 2) * 3;
3450 let mut buf = Buffer::new(width, 1);
3451 buf.clear_dirty();
3452 for i in 0..(DIRTY_SPAN_MAX_SPANS_PER_ROW + 1) {
3453 let x = (i as u16) * 3;
3454 buf.set(x, 0, Cell::from_char('x'));
3455 }
3456 let row = buf.dirty_span_row(0).unwrap();
3457 assert!(row.is_full());
3458 assert!(row.spans().is_empty());
3459 assert_eq!(buf.dirty_span_overflows, 1);
3460 }
3461
3462 #[test]
3463 fn fill_full_row_marks_full_span() {
3464 let mut buf = Buffer::new(10, 3);
3465 buf.clear_dirty();
3466 let cell = Cell::from_char('x').with_bg(PackedRgba::rgb(0, 0, 0));
3467 buf.fill(Rect::new(0, 1, 10, 1), cell);
3468 let row = buf.dirty_span_row(1).unwrap();
3469 assert!(row.is_full());
3470 assert!(row.spans().is_empty());
3471 }
3472
3473 #[test]
3474 fn get_mut_records_dirty_span() {
3475 let mut buf = Buffer::new(10, 5);
3476 buf.clear_dirty();
3477 let _ = buf.get_mut(5, 3);
3478 let row = buf.dirty_span_row(3).unwrap();
3479 assert_eq!(row.spans(), &[DirtySpan::new(5, 6)]);
3480 }
3481
3482 #[test]
3483 fn cells_mut_marks_all_full_spans() {
3484 let mut buf = Buffer::new(10, 5);
3485 buf.clear_dirty();
3486 let _ = buf.cells_mut();
3487 for y in 0..5 {
3488 let row = buf.dirty_span_row(y).unwrap();
3489 assert!(row.is_full(), "row {y} should be full after cells_mut");
3490 }
3491 }
3492
3493 #[test]
3494 fn dirty_span_config_disabled_skips_rows() {
3495 let mut buf = Buffer::new(10, 1);
3496 buf.clear_dirty();
3497 buf.set_dirty_span_config(DirtySpanConfig::default().with_enabled(false));
3498 buf.set(5, 0, Cell::from_char('x'));
3499 assert!(buf.dirty_span_row(0).is_none());
3500 let stats = buf.dirty_span_stats();
3501 assert_eq!(stats.total_spans, 0);
3502 assert_eq!(stats.span_coverage_cells, 0);
3503 }
3504
3505 #[test]
3506 fn dirty_span_guard_band_expands_span_bounds() {
3507 let mut buf = Buffer::new(10, 1);
3508 buf.clear_dirty();
3509 buf.set_dirty_span_config(DirtySpanConfig::default().with_guard_band(2));
3510 buf.set(5, 0, Cell::from_char('x'));
3511 let row = buf.dirty_span_row(0).unwrap();
3512 assert_eq!(row.spans(), &[DirtySpan::new(3, 8)]);
3513 }
3514
3515 #[test]
3516 fn dirty_span_max_spans_overflow_triggers_full_row() {
3517 let mut buf = Buffer::new(10, 1);
3518 buf.clear_dirty();
3519 buf.set_dirty_span_config(
3520 DirtySpanConfig::default()
3521 .with_max_spans_per_row(1)
3522 .with_merge_gap(0),
3523 );
3524 buf.set(0, 0, Cell::from_char('a'));
3525 buf.set(4, 0, Cell::from_char('b'));
3526 let row = buf.dirty_span_row(0).unwrap();
3527 assert!(row.is_full());
3528 assert!(row.spans().is_empty());
3529 assert_eq!(buf.dirty_span_overflows, 1);
3530 }
3531
3532 #[test]
3533 fn dirty_span_stats_counts_full_rows_and_spans() {
3534 let mut buf = Buffer::new(6, 2);
3535 buf.clear_dirty();
3536 buf.set_dirty_span_config(DirtySpanConfig::default().with_merge_gap(0));
3537 buf.set(1, 0, Cell::from_char('a'));
3538 buf.set(4, 0, Cell::from_char('b'));
3539 buf.mark_dirty_row_full(1);
3540
3541 let stats = buf.dirty_span_stats();
3542 assert_eq!(stats.rows_full_dirty, 1);
3543 assert_eq!(stats.rows_with_spans, 1);
3544 assert_eq!(stats.total_spans, 2);
3545 assert_eq!(stats.max_span_len, 6);
3546 assert_eq!(stats.span_coverage_cells, 8);
3547 }
3548
3549 #[test]
3550 fn dirty_span_stats_reports_overflow_and_full_row() {
3551 let mut buf = Buffer::new(8, 1);
3552 buf.clear_dirty();
3553 buf.set_dirty_span_config(
3554 DirtySpanConfig::default()
3555 .with_max_spans_per_row(1)
3556 .with_merge_gap(0),
3557 );
3558 buf.set(0, 0, Cell::from_char('x'));
3559 buf.set(3, 0, Cell::from_char('y'));
3560
3561 let stats = buf.dirty_span_stats();
3562 assert_eq!(stats.overflows, 1);
3563 assert_eq!(stats.rows_full_dirty, 1);
3564 assert_eq!(stats.total_spans, 0);
3565 assert_eq!(stats.span_coverage_cells, 8);
3566 }
3567
3568 #[test]
3573 fn double_buffer_new_has_matching_dimensions() {
3574 let db = DoubleBuffer::new(80, 24);
3575 assert_eq!(db.width(), 80);
3576 assert_eq!(db.height(), 24);
3577 assert!(db.dimensions_match(80, 24));
3578 assert!(!db.dimensions_match(120, 40));
3579 }
3580
3581 #[test]
3582 fn double_buffer_swap_is_o1() {
3583 let mut db = DoubleBuffer::new(80, 24);
3584
3585 db.current_mut().set(0, 0, Cell::from_char('A'));
3587 assert_eq!(db.current().get(0, 0).unwrap().content.as_char(), Some('A'));
3588
3589 db.swap();
3591 assert_eq!(
3592 db.previous().get(0, 0).unwrap().content.as_char(),
3593 Some('A')
3594 );
3595 assert!(db.current().get(0, 0).unwrap().is_empty());
3597 }
3598
3599 #[test]
3600 fn double_buffer_swap_round_trip() {
3601 let mut db = DoubleBuffer::new(10, 5);
3602
3603 db.current_mut().set(0, 0, Cell::from_char('X'));
3604 db.swap();
3605 db.current_mut().set(0, 0, Cell::from_char('Y'));
3606 db.swap();
3607
3608 assert_eq!(db.current().get(0, 0).unwrap().content.as_char(), Some('X'));
3610 assert_eq!(
3611 db.previous().get(0, 0).unwrap().content.as_char(),
3612 Some('Y')
3613 );
3614 }
3615
3616 #[test]
3617 fn double_buffer_resize_changes_dimensions() {
3618 let mut db = DoubleBuffer::new(80, 24);
3619 assert!(!db.resize(80, 24)); assert!(db.resize(120, 40)); assert_eq!(db.width(), 120);
3622 assert_eq!(db.height(), 40);
3623 assert!(db.dimensions_match(120, 40));
3624 }
3625
3626 #[test]
3627 fn double_buffer_resize_clears_content() {
3628 let mut db = DoubleBuffer::new(10, 5);
3629 db.current_mut().set(0, 0, Cell::from_char('Z'));
3630 db.swap();
3631 db.current_mut().set(0, 0, Cell::from_char('W'));
3632
3633 db.resize(20, 10);
3634
3635 assert!(db.current().get(0, 0).unwrap().is_empty());
3637 assert!(db.previous().get(0, 0).unwrap().is_empty());
3638 }
3639
3640 #[test]
3641 fn double_buffer_current_and_previous_are_distinct() {
3642 let mut db = DoubleBuffer::new(10, 5);
3643 db.current_mut().set(0, 0, Cell::from_char('C'));
3644
3645 assert!(db.previous().get(0, 0).unwrap().is_empty());
3647 assert_eq!(db.current().get(0, 0).unwrap().content.as_char(), Some('C'));
3648 }
3649
3650 #[test]
3655 fn adaptive_buffer_new_has_over_allocation() {
3656 let adb = AdaptiveDoubleBuffer::new(80, 24);
3657
3658 assert_eq!(adb.width(), 80);
3660 assert_eq!(adb.height(), 24);
3661 assert!(adb.dimensions_match(80, 24));
3662
3663 assert!(adb.capacity_width() > 80);
3667 assert!(adb.capacity_height() > 24);
3668 assert_eq!(adb.capacity_width(), 100); assert_eq!(adb.capacity_height(), 30); }
3671
3672 #[test]
3673 fn adaptive_buffer_resize_avoids_reallocation_when_within_capacity() {
3674 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3675
3676 assert!(adb.resize(90, 28)); assert_eq!(adb.width(), 90);
3679 assert_eq!(adb.height(), 28);
3680 assert_eq!(adb.stats().resize_avoided, 1);
3681 assert_eq!(adb.stats().resize_reallocated, 0);
3682 assert_eq!(adb.stats().resize_growth, 1);
3683 }
3684
3685 #[test]
3686 fn adaptive_buffer_resize_reallocates_on_growth_beyond_capacity() {
3687 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3688
3689 assert!(adb.resize(120, 40)); assert_eq!(adb.width(), 120);
3692 assert_eq!(adb.height(), 40);
3693 assert_eq!(adb.stats().resize_reallocated, 1);
3694 assert_eq!(adb.stats().resize_avoided, 0);
3695
3696 assert!(adb.capacity_width() > 120);
3698 assert!(adb.capacity_height() > 40);
3699 }
3700
3701 #[test]
3702 fn adaptive_buffer_resize_reallocates_on_significant_shrink() {
3703 let mut adb = AdaptiveDoubleBuffer::new(100, 50);
3704
3705 assert!(adb.resize(40, 20)); assert_eq!(adb.width(), 40);
3709 assert_eq!(adb.height(), 20);
3710 assert_eq!(adb.stats().resize_reallocated, 1);
3711 assert_eq!(adb.stats().resize_shrink, 1);
3712 }
3713
3714 #[test]
3715 fn adaptive_buffer_resize_avoids_reallocation_on_minor_shrink() {
3716 let mut adb = AdaptiveDoubleBuffer::new(100, 50);
3717
3718 assert!(adb.resize(80, 40));
3722 assert_eq!(adb.width(), 80);
3723 assert_eq!(adb.height(), 40);
3724 assert_eq!(adb.stats().resize_avoided, 1);
3725 assert_eq!(adb.stats().resize_reallocated, 0);
3726 assert_eq!(adb.stats().resize_shrink, 1);
3727 }
3728
3729 #[test]
3730 fn adaptive_buffer_no_change_returns_false() {
3731 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3732
3733 assert!(!adb.resize(80, 24)); assert_eq!(adb.stats().resize_avoided, 0);
3735 assert_eq!(adb.stats().resize_reallocated, 0);
3736 assert_eq!(adb.stats().resize_growth, 0);
3737 assert_eq!(adb.stats().resize_shrink, 0);
3738 }
3739
3740 #[test]
3741 fn adaptive_buffer_swap_works() {
3742 let mut adb = AdaptiveDoubleBuffer::new(10, 5);
3743
3744 adb.current_mut().set(0, 0, Cell::from_char('A'));
3745 assert_eq!(
3746 adb.current().get(0, 0).unwrap().content.as_char(),
3747 Some('A')
3748 );
3749
3750 adb.swap();
3751 assert_eq!(
3752 adb.previous().get(0, 0).unwrap().content.as_char(),
3753 Some('A')
3754 );
3755 assert!(adb.current().get(0, 0).unwrap().is_empty());
3756 }
3757
3758 #[test]
3759 fn adaptive_buffer_stats_reset() {
3760 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3761
3762 adb.resize(90, 28);
3763 adb.resize(120, 40);
3764 assert!(adb.stats().resize_avoided > 0 || adb.stats().resize_reallocated > 0);
3765
3766 adb.reset_stats();
3767 assert_eq!(adb.stats().resize_avoided, 0);
3768 assert_eq!(adb.stats().resize_reallocated, 0);
3769 assert_eq!(adb.stats().resize_growth, 0);
3770 assert_eq!(adb.stats().resize_shrink, 0);
3771 }
3772
3773 #[test]
3774 fn adaptive_buffer_memory_efficiency() {
3775 let adb = AdaptiveDoubleBuffer::new(80, 24);
3776
3777 let efficiency = adb.memory_efficiency();
3778 assert!(efficiency > 0.5);
3782 assert!(efficiency < 1.0);
3783 }
3784
3785 #[test]
3786 fn adaptive_buffer_logical_bounds() {
3787 let adb = AdaptiveDoubleBuffer::new(80, 24);
3788
3789 let bounds = adb.logical_bounds();
3790 assert_eq!(bounds.x, 0);
3791 assert_eq!(bounds.y, 0);
3792 assert_eq!(bounds.width, 80);
3793 assert_eq!(bounds.height, 24);
3794 }
3795
3796 #[test]
3797 fn adaptive_buffer_capacity_clamped_for_large_sizes() {
3798 let adb = AdaptiveDoubleBuffer::new(1000, 500);
3800
3801 assert_eq!(adb.capacity_width(), 1000 + 200); assert_eq!(adb.capacity_height(), 500 + 125); }
3806
3807 #[test]
3808 fn adaptive_stats_avoidance_ratio() {
3809 let mut stats = AdaptiveStats::default();
3810
3811 assert!((stats.avoidance_ratio() - 1.0).abs() < f64::EPSILON);
3813
3814 stats.resize_avoided = 3;
3816 stats.resize_reallocated = 1;
3817 assert!((stats.avoidance_ratio() - 0.75).abs() < f64::EPSILON);
3818
3819 stats.resize_avoided = 0;
3821 stats.resize_reallocated = 5;
3822 assert!((stats.avoidance_ratio() - 0.0).abs() < f64::EPSILON);
3823 }
3824
3825 #[test]
3826 fn adaptive_buffer_resize_storm_simulation() {
3827 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3829
3830 for i in 1..=10 {
3832 adb.resize(80 + i, 24 + (i / 2));
3833 }
3834
3835 let ratio = adb.stats().avoidance_ratio();
3837 assert!(
3838 ratio > 0.5,
3839 "Expected >50% avoidance ratio, got {:.2}",
3840 ratio
3841 );
3842 }
3843
3844 #[test]
3845 fn adaptive_buffer_width_only_growth() {
3846 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3847
3848 assert!(adb.resize(95, 24)); assert_eq!(adb.stats().resize_avoided, 1);
3851 assert_eq!(adb.stats().resize_growth, 1);
3852 }
3853
3854 #[test]
3855 fn adaptive_buffer_height_only_growth() {
3856 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3857
3858 assert!(adb.resize(80, 28)); assert_eq!(adb.stats().resize_avoided, 1);
3861 assert_eq!(adb.stats().resize_growth, 1);
3862 }
3863
3864 #[test]
3865 fn adaptive_buffer_one_dimension_exceeds_capacity() {
3866 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3867
3868 assert!(adb.resize(105, 24)); assert_eq!(adb.stats().resize_reallocated, 1);
3871 }
3872
3873 #[test]
3874 fn adaptive_buffer_current_and_previous_distinct() {
3875 let mut adb = AdaptiveDoubleBuffer::new(10, 5);
3876 adb.current_mut().set(0, 0, Cell::from_char('X'));
3877
3878 assert!(adb.previous().get(0, 0).unwrap().is_empty());
3880 assert_eq!(
3881 adb.current().get(0, 0).unwrap().content.as_char(),
3882 Some('X')
3883 );
3884 }
3885
3886 #[test]
3887 fn adaptive_buffer_resize_within_capacity_clears_previous() {
3888 let mut adb = AdaptiveDoubleBuffer::new(10, 5);
3889 adb.current_mut().set(9, 4, Cell::from_char('X'));
3890 adb.swap();
3891
3892 assert!(adb.resize(8, 4));
3894
3895 assert!(adb.previous().get(9, 4).unwrap().is_empty());
3897 }
3898
3899 #[test]
3901 fn adaptive_buffer_invariant_capacity_geq_logical() {
3902 for width in [1u16, 10, 80, 200, 1000, 5000] {
3904 for height in [1u16, 10, 24, 100, 500, 2000] {
3905 let adb = AdaptiveDoubleBuffer::new(width, height);
3906 assert!(
3907 adb.capacity_width() >= adb.width(),
3908 "capacity_width {} < logical_width {} for ({}, {})",
3909 adb.capacity_width(),
3910 adb.width(),
3911 width,
3912 height
3913 );
3914 assert!(
3915 adb.capacity_height() >= adb.height(),
3916 "capacity_height {} < logical_height {} for ({}, {})",
3917 adb.capacity_height(),
3918 adb.height(),
3919 width,
3920 height
3921 );
3922 }
3923 }
3924 }
3925
3926 #[test]
3927 fn adaptive_buffer_invariant_resize_dimensions_correct() {
3928 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3929
3930 let test_sizes = [
3932 (100, 50),
3933 (40, 20),
3934 (80, 24),
3935 (200, 100),
3936 (10, 5),
3937 (1000, 500),
3938 ];
3939 for (w, h) in test_sizes {
3940 adb.resize(w, h);
3941 assert_eq!(adb.width(), w, "width mismatch for ({}, {})", w, h);
3942 assert_eq!(adb.height(), h, "height mismatch for ({}, {})", w, h);
3943 assert!(
3944 adb.capacity_width() >= w,
3945 "capacity_width < width for ({}, {})",
3946 w,
3947 h
3948 );
3949 assert!(
3950 adb.capacity_height() >= h,
3951 "capacity_height < height for ({}, {})",
3952 w,
3953 h
3954 );
3955 }
3956 }
3957
3958 #[test]
3962 fn adaptive_buffer_no_ghosting_on_shrink() {
3963 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3964
3965 for y in 0..adb.height() {
3967 for x in 0..adb.width() {
3968 adb.current_mut().set(x, y, Cell::from_char('X'));
3969 }
3970 }
3971
3972 adb.resize(60, 20);
3975
3976 for y in 0..adb.height() {
3979 for x in 0..adb.width() {
3980 let cell = adb.current().get(x, y).unwrap();
3981 assert!(
3982 cell.is_empty(),
3983 "Ghost content at ({}, {}): expected empty, got {:?}",
3984 x,
3985 y,
3986 cell.content
3987 );
3988 }
3989 }
3990 }
3991
3992 #[test]
3996 fn adaptive_buffer_no_ghosting_on_reallocation_shrink() {
3997 let mut adb = AdaptiveDoubleBuffer::new(100, 50);
3998
3999 for y in 0..adb.height() {
4001 for x in 0..adb.width() {
4002 adb.current_mut().set(x, y, Cell::from_char('A'));
4003 }
4004 }
4005 adb.swap();
4006 for y in 0..adb.height() {
4007 for x in 0..adb.width() {
4008 adb.current_mut().set(x, y, Cell::from_char('B'));
4009 }
4010 }
4011
4012 adb.resize(30, 15);
4014 assert_eq!(adb.stats().resize_reallocated, 1);
4015
4016 for y in 0..adb.height() {
4018 for x in 0..adb.width() {
4019 assert!(
4020 adb.current().get(x, y).unwrap().is_empty(),
4021 "Ghost in current at ({}, {})",
4022 x,
4023 y
4024 );
4025 assert!(
4026 adb.previous().get(x, y).unwrap().is_empty(),
4027 "Ghost in previous at ({}, {})",
4028 x,
4029 y
4030 );
4031 }
4032 }
4033 }
4034
4035 #[test]
4039 fn adaptive_buffer_no_ghosting_on_growth_reallocation() {
4040 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
4041
4042 for y in 0..adb.height() {
4044 for x in 0..adb.width() {
4045 adb.current_mut().set(x, y, Cell::from_char('Z'));
4046 }
4047 }
4048
4049 adb.resize(150, 60);
4051 assert_eq!(adb.stats().resize_reallocated, 1);
4052
4053 for y in 0..adb.height() {
4055 for x in 0..adb.width() {
4056 assert!(
4057 adb.current().get(x, y).unwrap().is_empty(),
4058 "Ghost at ({}, {}) after growth reallocation",
4059 x,
4060 y
4061 );
4062 }
4063 }
4064 }
4065
4066 #[test]
4068 fn adaptive_buffer_resize_idempotent() {
4069 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
4070 adb.current_mut().set(5, 5, Cell::from_char('K'));
4071
4072 let changed = adb.resize(80, 24);
4074 assert!(!changed);
4075
4076 assert_eq!(
4078 adb.current().get(5, 5).unwrap().content.as_char(),
4079 Some('K')
4080 );
4081 }
4082
4083 #[test]
4088 fn dirty_span_merge_adjacent() {
4089 let mut buf = Buffer::new(100, 1);
4090 buf.clear_dirty(); buf.mark_dirty_span(0, 10, 20);
4094 let spans = buf.dirty_span_row(0).unwrap().spans();
4095 assert_eq!(spans.len(), 1);
4096 assert_eq!(spans[0], DirtySpan::new(10, 20));
4097
4098 buf.mark_dirty_span(0, 20, 30);
4100 let spans = buf.dirty_span_row(0).unwrap().spans();
4101 assert_eq!(spans.len(), 1);
4102 assert_eq!(spans[0], DirtySpan::new(10, 30));
4103 }
4104
4105 #[test]
4106 fn dirty_span_merge_overlapping() {
4107 let mut buf = Buffer::new(100, 1);
4108 buf.clear_dirty();
4109
4110 buf.mark_dirty_span(0, 10, 20);
4112 buf.mark_dirty_span(0, 15, 25);
4114
4115 let spans = buf.dirty_span_row(0).unwrap().spans();
4116 assert_eq!(spans.len(), 1);
4117 assert_eq!(spans[0], DirtySpan::new(10, 25));
4118 }
4119
4120 #[test]
4121 fn dirty_span_merge_with_gap() {
4122 let mut buf = Buffer::new(100, 1);
4123 buf.clear_dirty();
4124
4125 buf.mark_dirty_span(0, 10, 20);
4128 buf.mark_dirty_span(0, 21, 30);
4130
4131 let spans = buf.dirty_span_row(0).unwrap().spans();
4132 assert_eq!(spans.len(), 1);
4133 assert_eq!(spans[0], DirtySpan::new(10, 30));
4134 }
4135
4136 #[test]
4137 fn dirty_span_no_merge_large_gap() {
4138 let mut buf = Buffer::new(100, 1);
4139 buf.clear_dirty();
4140
4141 buf.mark_dirty_span(0, 10, 20);
4143 buf.mark_dirty_span(0, 22, 30);
4145
4146 let spans = buf.dirty_span_row(0).unwrap().spans();
4147 assert_eq!(spans.len(), 2);
4148 assert_eq!(spans[0], DirtySpan::new(10, 20));
4149 assert_eq!(spans[1], DirtySpan::new(22, 30));
4150 }
4151
4152 #[test]
4153 fn dirty_span_overflow_to_full() {
4154 let mut buf = Buffer::new(1000, 1);
4155 buf.clear_dirty();
4156
4157 for i in 0..DIRTY_SPAN_MAX_SPANS_PER_ROW + 10 {
4159 let start = (i * 4) as u16;
4160 buf.mark_dirty_span(0, start, start + 1);
4161 }
4162
4163 let row = buf.dirty_span_row(0).unwrap();
4164 assert!(row.is_full(), "Row should overflow to full scan");
4165 assert!(
4166 row.spans().is_empty(),
4167 "Spans should be cleared on overflow"
4168 );
4169 }
4170
4171 #[test]
4172 fn dirty_span_bounds_clamping() {
4173 let mut buf = Buffer::new(10, 1);
4174 buf.clear_dirty();
4175
4176 buf.mark_dirty_span(0, 15, 20);
4178 let spans = buf.dirty_span_row(0).unwrap().spans();
4179 assert!(spans.is_empty());
4180
4181 buf.mark_dirty_span(0, 8, 15);
4183 let spans = buf.dirty_span_row(0).unwrap().spans();
4184 assert_eq!(spans.len(), 1);
4185 assert_eq!(spans[0], DirtySpan::new(8, 10)); }
4187
4188 #[test]
4189 fn dirty_span_guard_band_clamps_bounds() {
4190 let mut buf = Buffer::new(10, 1);
4191 buf.clear_dirty();
4192 buf.set_dirty_span_config(DirtySpanConfig::default().with_guard_band(5));
4193
4194 buf.mark_dirty_span(0, 2, 3);
4195 let spans = buf.dirty_span_row(0).unwrap().spans();
4196 assert_eq!(spans.len(), 1);
4197 assert_eq!(spans[0], DirtySpan::new(0, 8));
4198
4199 buf.clear_dirty();
4200 buf.mark_dirty_span(0, 8, 10);
4201 let spans = buf.dirty_span_row(0).unwrap().spans();
4202 assert_eq!(spans.len(), 1);
4203 assert_eq!(spans[0], DirtySpan::new(3, 10));
4204 }
4205
4206 #[test]
4207 fn dirty_span_empty_span_is_ignored() {
4208 let mut buf = Buffer::new(10, 1);
4209 buf.clear_dirty();
4210 buf.mark_dirty_span(0, 5, 5);
4211 let spans = buf.dirty_span_row(0).unwrap().spans();
4212 assert!(spans.is_empty());
4213 }
4214
4215 #[test]
4216 fn buffer_fill_wide_char_clipping() {
4217 let mut buf = Buffer::new(10, 5);
4221 let wide_cell = Cell::from_char('🦀'); buf.fill(Rect::new(0, 0, 10, 5), wide_cell);
4225
4226 let head = buf.get(0, 0).unwrap();
4228 assert_eq!(head.content.as_char(), Some('🦀'));
4229 assert_eq!(head.content.width(), 2);
4230
4231 let tail = buf.get(1, 0).unwrap();
4232 assert!(tail.is_continuation());
4233
4234 buf.push_scissor(Rect::new(0, 0, 1, 5));
4237 let x_cell = Cell::from_char('X');
4239 buf.fill(Rect::new(0, 0, 10, 5), x_cell);
4240
4241 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('X'));
4243 assert!(buf.get(1, 0).unwrap().is_empty()); buf.pop_scissor();
4246 }
4247}