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 for x in clipped.x..clipped.right() {
1108 self.set(x, y, Cell::default());
1109 }
1110 let mut x = clipped.x;
1111 while x < clipped.right() {
1112 self.set(x, y, cell);
1113 x = x.saturating_add(step);
1114 }
1115 }
1116
1117 self.pop_scissor();
1118 }
1119
1120 #[inline]
1122 pub fn clear(&mut self) {
1123 self.cells.fill(Cell::default());
1124 self.mark_all_dirty();
1125 }
1126
1127 pub fn reset_for_frame(&mut self) {
1132 self.scissor_stack.truncate(1);
1133 if let Some(base) = self.scissor_stack.first_mut() {
1134 *base = Rect::from_size(self.width, self.height);
1135 } else {
1136 self.scissor_stack
1137 .push(Rect::from_size(self.width, self.height));
1138 }
1139
1140 self.opacity_stack.truncate(1);
1141 if let Some(base) = self.opacity_stack.first_mut() {
1142 *base = 1.0;
1143 } else {
1144 self.opacity_stack.push(1.0);
1145 }
1146
1147 self.clear();
1148 }
1149
1150 #[inline]
1152 pub fn clear_with(&mut self, cell: Cell) {
1153 if cell.is_continuation() {
1154 self.clear();
1155 return;
1156 }
1157
1158 let width = cell.content.width();
1159 if width <= 1 {
1160 self.cells.fill(cell);
1161 self.mark_all_dirty();
1162 return;
1163 }
1164
1165 self.cells.fill(Cell::default());
1166 let step = width as u16;
1167 for y in 0..self.height {
1168 let row_start = y as usize * self.width as usize;
1169 let mut x = 0u16;
1170 while x.saturating_add(step) <= self.width {
1171 let head_idx = row_start + x as usize;
1172 self.cells[head_idx] = cell;
1173 for off in 1..step {
1174 self.cells[head_idx + off as usize] = Cell::CONTINUATION;
1175 }
1176 x = x.saturating_add(step);
1177 }
1178 }
1179 self.mark_all_dirty();
1180 }
1181
1182 #[inline]
1186 pub fn cells(&self) -> &[Cell] {
1187 &self.cells
1188 }
1189
1190 #[inline]
1194 pub fn cells_mut(&mut self) -> &mut [Cell] {
1195 self.mark_all_dirty();
1196 &mut self.cells
1197 }
1198
1199 #[inline]
1205 pub fn row_cells(&self, y: u16) -> &[Cell] {
1206 let start = y as usize * self.width as usize;
1207 &self.cells[start..start + self.width as usize]
1208 }
1209
1210 #[inline]
1220 pub fn row_cells_mut_span(&mut self, y: u16, x0: u16, x1: u16) -> Option<&mut [Cell]> {
1221 if y >= self.height {
1222 return None;
1223 }
1224 if x0 >= x1 {
1225 return None;
1226 }
1227
1228 let start = x0.min(self.width);
1229 let end = x1.min(self.width);
1230 if start >= end {
1231 return None;
1232 }
1233
1234 self.mark_dirty_span(y, start, end);
1235
1236 let row_start = y as usize * self.width as usize;
1237 let slice_start = row_start + start as usize;
1238 let slice_end = row_start + end as usize;
1239 Some(&mut self.cells[slice_start..slice_end])
1240 }
1241
1242 #[inline]
1249 pub fn push_scissor(&mut self, rect: Rect) {
1250 let current = self.current_scissor();
1251 let intersected = current.intersection(&rect);
1252 self.scissor_stack.push(intersected);
1253 }
1254
1255 #[inline]
1259 pub fn pop_scissor(&mut self) {
1260 if self.scissor_stack.len() > 1 {
1261 self.scissor_stack.pop();
1262 }
1263 }
1264
1265 #[inline]
1267 pub fn current_scissor(&self) -> Rect {
1268 *self
1269 .scissor_stack
1270 .last()
1271 .expect("scissor stack always has at least one element")
1272 }
1273
1274 #[inline]
1276 pub fn scissor_depth(&self) -> usize {
1277 self.scissor_stack.len()
1278 }
1279
1280 #[inline]
1287 pub fn push_opacity(&mut self, opacity: f32) {
1288 let clamped = opacity.clamp(0.0, 1.0);
1289 let current = self.current_opacity();
1290 self.opacity_stack.push(current * clamped);
1291 }
1292
1293 #[inline]
1297 pub fn pop_opacity(&mut self) {
1298 if self.opacity_stack.len() > 1 {
1299 self.opacity_stack.pop();
1300 }
1301 }
1302
1303 #[inline]
1305 pub fn current_opacity(&self) -> f32 {
1306 *self
1307 .opacity_stack
1308 .last()
1309 .expect("opacity stack always has at least one element")
1310 }
1311
1312 #[inline]
1314 pub fn opacity_depth(&self) -> usize {
1315 self.opacity_stack.len()
1316 }
1317
1318 pub fn copy_from(&mut self, src: &Buffer, src_rect: Rect, dst_x: u16, dst_y: u16) {
1325 let copy_bounds = Rect::new(dst_x, dst_y, src_rect.width, src_rect.height);
1328 self.push_scissor(copy_bounds);
1329 let clip = self.current_scissor();
1330
1331 for dy in 0..src_rect.height {
1332 let Some(target_y) = dst_y.checked_add(dy) else {
1334 continue;
1335 };
1336 let Some(sy) = src_rect.y.checked_add(dy) else {
1337 continue;
1338 };
1339
1340 let mut dx = 0u16;
1341 while dx < src_rect.width {
1342 let Some(target_x) = dst_x.checked_add(dx) else {
1344 dx = dx.saturating_add(1);
1345 continue;
1346 };
1347 let Some(sx) = src_rect.x.checked_add(dx) else {
1348 dx = dx.saturating_add(1);
1349 continue;
1350 };
1351
1352 if let Some(cell) = src.get(sx, sy) {
1353 if cell.is_continuation() {
1357 self.set(target_x, target_y, Cell::default());
1358 dx = dx.saturating_add(1);
1359 continue;
1360 }
1361
1362 let width = cell.content.width();
1363 let target_right = target_x.saturating_add(width as u16);
1364
1365 let src_clipped = width > 1 && dx.saturating_add(width as u16) > src_rect.width;
1369 let dst_clipped = target_right > clip.right();
1370
1371 if src_clipped || dst_clipped {
1372 let valid_width = (clip.right().saturating_sub(target_x)).min(width as u16);
1376 for i in 0..valid_width {
1377 self.set(target_x + i, target_y, Cell::default());
1378 }
1379 } else {
1380 self.set(target_x, target_y, *cell);
1381 }
1382
1383 if width > 1 {
1385 dx = dx.saturating_add(width as u16);
1386 } else {
1387 dx = dx.saturating_add(1);
1388 }
1389 } else {
1390 dx = dx.saturating_add(1);
1391 }
1392 }
1393 }
1394
1395 self.pop_scissor();
1396 }
1397
1398 pub fn content_eq(&self, other: &Buffer) -> bool {
1400 self.width == other.width && self.height == other.height && self.cells == other.cells
1401 }
1402}
1403
1404impl Default for Buffer {
1405 fn default() -> Self {
1407 Self::new(1, 1)
1408 }
1409}
1410
1411impl PartialEq for Buffer {
1412 fn eq(&self, other: &Self) -> bool {
1413 self.content_eq(other)
1414 }
1415}
1416
1417impl Eq for Buffer {}
1418
1419#[derive(Debug)]
1438pub struct DoubleBuffer {
1439 buffers: [Buffer; 2],
1440 current_idx: u8,
1442}
1443
1444const ADAPTIVE_GROWTH_FACTOR: f32 = 1.25;
1450
1451const ADAPTIVE_SHRINK_THRESHOLD: f32 = 0.50;
1454
1455const ADAPTIVE_MAX_OVERAGE: u16 = 200;
1457
1458#[derive(Debug)]
1490pub struct AdaptiveDoubleBuffer {
1491 inner: DoubleBuffer,
1493 logical_width: u16,
1495 logical_height: u16,
1497 capacity_width: u16,
1499 capacity_height: u16,
1501 stats: AdaptiveStats,
1503}
1504
1505#[derive(Debug, Clone, Default)]
1507pub struct AdaptiveStats {
1508 pub resize_avoided: u64,
1510 pub resize_reallocated: u64,
1512 pub resize_growth: u64,
1514 pub resize_shrink: u64,
1516}
1517
1518impl AdaptiveStats {
1519 pub fn reset(&mut self) {
1521 *self = Self::default();
1522 }
1523
1524 pub fn avoidance_ratio(&self) -> f64 {
1526 let total = self.resize_avoided + self.resize_reallocated;
1527 if total == 0 {
1528 1.0
1529 } else {
1530 self.resize_avoided as f64 / total as f64
1531 }
1532 }
1533}
1534
1535impl DoubleBuffer {
1536 pub fn new(width: u16, height: u16) -> Self {
1541 Self {
1542 buffers: [Buffer::new(width, height), Buffer::new(width, height)],
1543 current_idx: 0,
1544 }
1545 }
1546
1547 #[inline]
1552 pub fn swap(&mut self) {
1553 self.current_idx = 1 - self.current_idx;
1554 }
1555
1556 #[inline]
1558 pub fn current(&self) -> &Buffer {
1559 &self.buffers[self.current_idx as usize]
1560 }
1561
1562 #[inline]
1564 pub fn current_mut(&mut self) -> &mut Buffer {
1565 &mut self.buffers[self.current_idx as usize]
1566 }
1567
1568 #[inline]
1570 pub fn previous(&self) -> &Buffer {
1571 &self.buffers[(1 - self.current_idx) as usize]
1572 }
1573
1574 #[inline]
1576 pub fn previous_mut(&mut self) -> &mut Buffer {
1577 &mut self.buffers[(1 - self.current_idx) as usize]
1578 }
1579
1580 #[inline]
1582 pub fn width(&self) -> u16 {
1583 self.buffers[0].width()
1584 }
1585
1586 #[inline]
1588 pub fn height(&self) -> u16 {
1589 self.buffers[0].height()
1590 }
1591
1592 pub fn resize(&mut self, width: u16, height: u16) -> bool {
1597 if self.buffers[0].width() == width && self.buffers[0].height() == height {
1598 return false;
1599 }
1600 self.buffers = [Buffer::new(width, height), Buffer::new(width, height)];
1601 self.current_idx = 0;
1602 true
1603 }
1604
1605 #[inline]
1607 pub fn dimensions_match(&self, width: u16, height: u16) -> bool {
1608 self.buffers[0].width() == width && self.buffers[0].height() == height
1609 }
1610}
1611
1612impl AdaptiveDoubleBuffer {
1617 pub fn new(width: u16, height: u16) -> Self {
1622 let (cap_w, cap_h) = Self::compute_capacity(width, height);
1623 Self {
1624 inner: DoubleBuffer::new(cap_w, cap_h),
1625 logical_width: width,
1626 logical_height: height,
1627 capacity_width: cap_w,
1628 capacity_height: cap_h,
1629 stats: AdaptiveStats::default(),
1630 }
1631 }
1632
1633 fn compute_capacity(width: u16, height: u16) -> (u16, u16) {
1637 let extra_w =
1638 ((width as f32 * (ADAPTIVE_GROWTH_FACTOR - 1.0)) as u16).min(ADAPTIVE_MAX_OVERAGE);
1639 let extra_h =
1640 ((height as f32 * (ADAPTIVE_GROWTH_FACTOR - 1.0)) as u16).min(ADAPTIVE_MAX_OVERAGE);
1641
1642 let cap_w = width.saturating_add(extra_w);
1643 let cap_h = height.saturating_add(extra_h);
1644
1645 (cap_w, cap_h)
1646 }
1647
1648 fn needs_reallocation(&self, width: u16, height: u16) -> bool {
1652 if width > self.capacity_width || height > self.capacity_height {
1654 return true;
1655 }
1656
1657 let shrink_threshold_w = (self.capacity_width as f32 * ADAPTIVE_SHRINK_THRESHOLD) as u16;
1659 let shrink_threshold_h = (self.capacity_height as f32 * ADAPTIVE_SHRINK_THRESHOLD) as u16;
1660
1661 width < shrink_threshold_w || height < shrink_threshold_h
1662 }
1663
1664 #[inline]
1669 pub fn swap(&mut self) {
1670 self.inner.swap();
1671 }
1672
1673 #[inline]
1678 pub fn current(&self) -> &Buffer {
1679 self.inner.current()
1680 }
1681
1682 #[inline]
1684 pub fn current_mut(&mut self) -> &mut Buffer {
1685 self.inner.current_mut()
1686 }
1687
1688 #[inline]
1690 pub fn previous(&self) -> &Buffer {
1691 self.inner.previous()
1692 }
1693
1694 #[inline]
1696 pub fn width(&self) -> u16 {
1697 self.logical_width
1698 }
1699
1700 #[inline]
1702 pub fn height(&self) -> u16 {
1703 self.logical_height
1704 }
1705
1706 #[inline]
1708 pub fn capacity_width(&self) -> u16 {
1709 self.capacity_width
1710 }
1711
1712 #[inline]
1714 pub fn capacity_height(&self) -> u16 {
1715 self.capacity_height
1716 }
1717
1718 #[inline]
1720 pub fn stats(&self) -> &AdaptiveStats {
1721 &self.stats
1722 }
1723
1724 pub fn reset_stats(&mut self) {
1726 self.stats.reset();
1727 }
1728
1729 pub fn resize(&mut self, width: u16, height: u16) -> bool {
1741 if width == self.logical_width && height == self.logical_height {
1743 return false;
1744 }
1745
1746 let is_growth = width > self.logical_width || height > self.logical_height;
1747 if is_growth {
1748 self.stats.resize_growth += 1;
1749 } else {
1750 self.stats.resize_shrink += 1;
1751 }
1752
1753 if self.needs_reallocation(width, height) {
1754 let (cap_w, cap_h) = Self::compute_capacity(width, height);
1756 self.inner = DoubleBuffer::new(cap_w, cap_h);
1757 self.capacity_width = cap_w;
1758 self.capacity_height = cap_h;
1759 self.stats.resize_reallocated += 1;
1760 } else {
1761 self.inner.current_mut().clear();
1764 self.inner.previous_mut().clear();
1765 self.stats.resize_avoided += 1;
1766 }
1767
1768 self.logical_width = width;
1769 self.logical_height = height;
1770 true
1771 }
1772
1773 #[inline]
1775 pub fn dimensions_match(&self, width: u16, height: u16) -> bool {
1776 self.logical_width == width && self.logical_height == height
1777 }
1778
1779 #[inline]
1781 pub fn logical_bounds(&self) -> Rect {
1782 Rect::from_size(self.logical_width, self.logical_height)
1783 }
1784
1785 pub fn memory_efficiency(&self) -> f64 {
1787 let logical = self.logical_width as u64 * self.logical_height as u64;
1788 let capacity = self.capacity_width as u64 * self.capacity_height as u64;
1789 if capacity == 0 {
1790 1.0
1791 } else {
1792 logical as f64 / capacity as f64
1793 }
1794 }
1795}
1796
1797#[cfg(test)]
1798mod tests {
1799 use super::*;
1800 use crate::cell::PackedRgba;
1801
1802 #[test]
1803 fn set_composites_background() {
1804 let mut buf = Buffer::new(1, 1);
1805
1806 let red = PackedRgba::rgb(255, 0, 0);
1808 buf.set(0, 0, Cell::default().with_bg(red));
1809
1810 let cell = Cell::from_char('X'); buf.set(0, 0, cell);
1813
1814 let result = buf.get(0, 0).unwrap();
1815 assert_eq!(result.content.as_char(), Some('X'));
1816 assert_eq!(
1817 result.bg, red,
1818 "Background should be preserved (composited)"
1819 );
1820 }
1821
1822 #[test]
1823 fn set_fast_matches_set_for_transparent_bg() {
1824 let red = PackedRgba::rgb(255, 0, 0);
1825 let cell = Cell::from_char('X').with_fg(PackedRgba::rgb(0, 255, 0));
1826
1827 let mut a = Buffer::new(1, 1);
1828 a.set(0, 0, Cell::default().with_bg(red));
1829 a.set(0, 0, cell);
1830
1831 let mut b = Buffer::new(1, 1);
1832 b.set(0, 0, Cell::default().with_bg(red));
1833 b.set_fast(0, 0, cell);
1834
1835 assert_eq!(a.get(0, 0), b.get(0, 0));
1836 }
1837
1838 #[test]
1839 fn set_fast_matches_set_for_opaque_bg() {
1840 let cell = Cell::from_char('X')
1841 .with_fg(PackedRgba::rgb(0, 255, 0))
1842 .with_bg(PackedRgba::rgb(255, 0, 0));
1843
1844 let mut a = Buffer::new(1, 1);
1845 a.set(0, 0, cell);
1846
1847 let mut b = Buffer::new(1, 1);
1848 b.set_fast(0, 0, cell);
1849
1850 assert_eq!(a.get(0, 0), b.get(0, 0));
1851 }
1852
1853 #[test]
1854 fn set_fast_clears_orphaned_tail_like_set() {
1855 let mut slow = Buffer::new(3, 1);
1856 slow.set_raw(0, 0, Cell::from_char('A'));
1857 slow.set_raw(1, 0, Cell::CONTINUATION);
1858 slow.clear_dirty();
1859
1860 let mut fast = slow.clone();
1861
1862 slow.set(0, 0, Cell::from_char('X'));
1863 fast.set_fast(0, 0, Cell::from_char('X'));
1864
1865 assert_eq!(slow.cells(), fast.cells());
1866 assert_eq!(fast.get(1, 0), Some(&Cell::default()));
1867
1868 let spans = fast.dirty_span_row(0).expect("dirty span row").spans();
1869 assert_eq!(spans, &[DirtySpan::new(0, 2)]);
1870 }
1871
1872 #[test]
1873 fn rect_contains() {
1874 let r = Rect::new(5, 5, 10, 10);
1875 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)); }
1881
1882 #[test]
1883 fn rect_intersection() {
1884 let a = Rect::new(0, 0, 10, 10);
1885 let b = Rect::new(5, 5, 10, 10);
1886 let i = a.intersection(&b);
1887 assert_eq!(i, Rect::new(5, 5, 5, 5));
1888
1889 let c = Rect::new(20, 20, 5, 5);
1891 assert_eq!(a.intersection(&c), Rect::default());
1892 }
1893
1894 #[test]
1895 fn buffer_creation() {
1896 let buf = Buffer::new(80, 24);
1897 assert_eq!(buf.width(), 80);
1898 assert_eq!(buf.height(), 24);
1899 assert_eq!(buf.len(), 80 * 24);
1900 }
1901
1902 #[test]
1903 fn content_height_empty_is_zero() {
1904 let buf = Buffer::new(8, 4);
1905 assert_eq!(buf.content_height(), 0);
1906 }
1907
1908 #[test]
1909 fn content_height_tracks_last_non_empty_row() {
1910 let mut buf = Buffer::new(5, 4);
1911 buf.set(0, 0, Cell::from_char('A'));
1912 assert_eq!(buf.content_height(), 1);
1913
1914 buf.set(2, 3, Cell::from_char('Z'));
1915 assert_eq!(buf.content_height(), 4);
1916 }
1917
1918 #[test]
1919 fn buffer_zero_width_clamped_to_one() {
1920 let buf = Buffer::new(0, 24);
1921 assert_eq!(buf.width(), 1);
1922 assert_eq!(buf.height(), 24);
1923 }
1924
1925 #[test]
1926 fn buffer_zero_height_clamped_to_one() {
1927 let buf = Buffer::new(80, 0);
1928 assert_eq!(buf.width(), 80);
1929 assert_eq!(buf.height(), 1);
1930 }
1931
1932 #[test]
1933 fn buffer_get_and_set() {
1934 let mut buf = Buffer::new(10, 10);
1935 let cell = Cell::from_char('X');
1936 buf.set(5, 5, cell);
1937 assert_eq!(buf.get(5, 5).unwrap().content.as_char(), Some('X'));
1938 }
1939
1940 #[test]
1941 fn buffer_out_of_bounds_get() {
1942 let buf = Buffer::new(10, 10);
1943 assert!(buf.get(10, 0).is_none());
1944 assert!(buf.get(0, 10).is_none());
1945 assert!(buf.get(100, 100).is_none());
1946 }
1947
1948 #[test]
1949 fn buffer_out_of_bounds_set_ignored() {
1950 let mut buf = Buffer::new(10, 10);
1951 buf.set(100, 100, Cell::from_char('X')); assert_eq!(buf.cells().iter().filter(|c| !c.is_empty()).count(), 0);
1953 }
1954
1955 #[test]
1956 fn buffer_clear() {
1957 let mut buf = Buffer::new(10, 10);
1958 buf.set(5, 5, Cell::from_char('X'));
1959 buf.clear();
1960 assert!(buf.get(5, 5).unwrap().is_empty());
1961 }
1962
1963 #[test]
1964 fn scissor_stack_basic() {
1965 let mut buf = Buffer::new(20, 20);
1966
1967 assert_eq!(buf.current_scissor(), Rect::from_size(20, 20));
1969 assert_eq!(buf.scissor_depth(), 1);
1970
1971 buf.push_scissor(Rect::new(5, 5, 10, 10));
1973 assert_eq!(buf.current_scissor(), Rect::new(5, 5, 10, 10));
1974 assert_eq!(buf.scissor_depth(), 2);
1975
1976 buf.set(7, 7, Cell::from_char('I'));
1978 assert_eq!(buf.get(7, 7).unwrap().content.as_char(), Some('I'));
1979
1980 buf.set(0, 0, Cell::from_char('O'));
1982 assert!(buf.get(0, 0).unwrap().is_empty());
1983
1984 buf.pop_scissor();
1986 assert_eq!(buf.current_scissor(), Rect::from_size(20, 20));
1987 assert_eq!(buf.scissor_depth(), 1);
1988
1989 buf.set(0, 0, Cell::from_char('N'));
1991 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('N'));
1992 }
1993
1994 #[test]
1995 fn scissor_intersection() {
1996 let mut buf = Buffer::new(20, 20);
1997 buf.push_scissor(Rect::new(5, 5, 10, 10));
1998 buf.push_scissor(Rect::new(8, 8, 10, 10));
1999
2000 assert_eq!(buf.current_scissor(), Rect::new(8, 8, 7, 7));
2003 }
2004
2005 #[test]
2006 fn scissor_base_cannot_be_popped() {
2007 let mut buf = Buffer::new(10, 10);
2008 buf.pop_scissor(); assert_eq!(buf.scissor_depth(), 1);
2010 buf.pop_scissor(); assert_eq!(buf.scissor_depth(), 1);
2012 }
2013
2014 #[test]
2015 fn opacity_stack_basic() {
2016 let mut buf = Buffer::new(10, 10);
2017
2018 assert!((buf.current_opacity() - 1.0).abs() < f32::EPSILON);
2020 assert_eq!(buf.opacity_depth(), 1);
2021
2022 buf.push_opacity(0.5);
2024 assert!((buf.current_opacity() - 0.5).abs() < f32::EPSILON);
2025 assert_eq!(buf.opacity_depth(), 2);
2026
2027 buf.push_opacity(0.5);
2029 assert!((buf.current_opacity() - 0.25).abs() < f32::EPSILON);
2030 assert_eq!(buf.opacity_depth(), 3);
2031
2032 buf.pop_opacity();
2034 assert!((buf.current_opacity() - 0.5).abs() < f32::EPSILON);
2035 }
2036
2037 #[test]
2038 fn opacity_applied_to_cells() {
2039 let mut buf = Buffer::new(10, 10);
2040 buf.push_opacity(0.5);
2041
2042 let cell = Cell::from_char('X').with_fg(PackedRgba::rgba(100, 100, 100, 255));
2043 buf.set(5, 5, cell);
2044
2045 let stored = buf.get(5, 5).unwrap();
2046 assert_eq!(stored.fg.a(), 128);
2048 }
2049
2050 #[test]
2051 fn opacity_composites_background_before_storage() {
2052 let mut buf = Buffer::new(1, 1);
2053
2054 let red = PackedRgba::rgb(255, 0, 0);
2055 let blue = PackedRgba::rgb(0, 0, 255);
2056
2057 buf.set(0, 0, Cell::default().with_bg(red));
2058 buf.push_opacity(0.5);
2059 buf.set(0, 0, Cell::default().with_bg(blue));
2060
2061 let stored = buf.get(0, 0).unwrap();
2062 let expected = blue.with_opacity(0.5).over(red);
2063 assert_eq!(stored.bg, expected);
2064 }
2065
2066 #[test]
2067 fn opacity_clamped() {
2068 let mut buf = Buffer::new(10, 10);
2069 buf.push_opacity(2.0); assert!((buf.current_opacity() - 1.0).abs() < f32::EPSILON);
2071
2072 buf.push_opacity(-1.0); assert!((buf.current_opacity() - 0.0).abs() < f32::EPSILON);
2074 }
2075
2076 #[test]
2077 fn opacity_base_cannot_be_popped() {
2078 let mut buf = Buffer::new(10, 10);
2079 buf.pop_opacity(); assert_eq!(buf.opacity_depth(), 1);
2081 }
2082
2083 #[test]
2084 fn buffer_fill() {
2085 let mut buf = Buffer::new(10, 10);
2086 let cell = Cell::from_char('#');
2087 buf.fill(Rect::new(2, 2, 5, 5), cell);
2088
2089 assert_eq!(buf.get(3, 3).unwrap().content.as_char(), Some('#'));
2091
2092 assert!(buf.get(0, 0).unwrap().is_empty());
2094 }
2095
2096 #[test]
2097 fn buffer_fill_respects_scissor() {
2098 let mut buf = Buffer::new(10, 10);
2099 buf.push_scissor(Rect::new(3, 3, 4, 4));
2100
2101 let cell = Cell::from_char('#');
2102 buf.fill(Rect::new(0, 0, 10, 10), cell);
2103
2104 assert_eq!(buf.get(3, 3).unwrap().content.as_char(), Some('#'));
2106 assert!(buf.get(0, 0).unwrap().is_empty());
2107 assert!(buf.get(7, 7).unwrap().is_empty());
2108 }
2109
2110 #[test]
2111 fn buffer_copy_from() {
2112 let mut src = Buffer::new(10, 10);
2113 src.set(2, 2, Cell::from_char('S'));
2114
2115 let mut dst = Buffer::new(10, 10);
2116 dst.copy_from(&src, Rect::new(0, 0, 5, 5), 3, 3);
2117
2118 assert_eq!(dst.get(5, 5).unwrap().content.as_char(), Some('S'));
2120 }
2121
2122 #[test]
2123 fn copy_from_clips_wide_char_at_boundary() {
2124 let mut src = Buffer::new(10, 1);
2125 src.set(0, 0, Cell::from_char('中'));
2127
2128 let mut dst = Buffer::new(10, 1);
2129 dst.copy_from(&src, Rect::new(0, 0, 1, 1), 0, 0);
2132
2133 assert!(
2142 dst.get(0, 0).unwrap().is_empty(),
2143 "Wide char head should not be written if tail is clipped"
2144 );
2145 assert!(
2146 dst.get(1, 0).unwrap().is_empty(),
2147 "Wide char tail should not be leaked outside copy region"
2148 );
2149 }
2150
2151 #[test]
2152 fn buffer_content_eq() {
2153 let mut buf1 = Buffer::new(10, 10);
2154 let mut buf2 = Buffer::new(10, 10);
2155
2156 assert!(buf1.content_eq(&buf2));
2157
2158 buf1.set(0, 0, Cell::from_char('X'));
2159 assert!(!buf1.content_eq(&buf2));
2160
2161 buf2.set(0, 0, Cell::from_char('X'));
2162 assert!(buf1.content_eq(&buf2));
2163 }
2164
2165 #[test]
2166 fn buffer_bounds() {
2167 let buf = Buffer::new(80, 24);
2168 let bounds = buf.bounds();
2169 assert_eq!(bounds.x, 0);
2170 assert_eq!(bounds.y, 0);
2171 assert_eq!(bounds.width, 80);
2172 assert_eq!(bounds.height, 24);
2173 }
2174
2175 #[test]
2176 fn buffer_set_raw_bypasses_scissor() {
2177 let mut buf = Buffer::new(10, 10);
2178 buf.push_scissor(Rect::new(5, 5, 5, 5));
2179
2180 buf.set(0, 0, Cell::from_char('S'));
2182 assert!(buf.get(0, 0).unwrap().is_empty());
2183
2184 buf.set_raw(0, 0, Cell::from_char('R'));
2186 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('R'));
2187 }
2188
2189 #[test]
2190 fn set_handles_wide_chars() {
2191 let mut buf = Buffer::new(10, 10);
2192
2193 buf.set(0, 0, Cell::from_char('中'));
2195
2196 let head = buf.get(0, 0).unwrap();
2198 assert_eq!(head.content.as_char(), Some('中'));
2199
2200 let cont = buf.get(1, 0).unwrap();
2202 assert!(cont.is_continuation());
2203 assert!(!cont.is_empty());
2204 }
2205
2206 #[test]
2207 fn set_handles_wide_chars_clipped() {
2208 let mut buf = Buffer::new(10, 10);
2209 buf.push_scissor(Rect::new(0, 0, 1, 10)); buf.set(0, 0, Cell::from_char('中'));
2214
2215 assert!(buf.get(0, 0).unwrap().is_empty());
2217 assert!(buf.get(1, 0).unwrap().is_empty());
2219 }
2220
2221 #[test]
2224 fn overwrite_wide_head_with_single_clears_tails() {
2225 let mut buf = Buffer::new(10, 1);
2226
2227 buf.set(0, 0, Cell::from_char('中'));
2229 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2230 assert!(buf.get(1, 0).unwrap().is_continuation());
2231
2232 buf.set(0, 0, Cell::from_char('A'));
2234
2235 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('A'));
2237 assert!(
2239 buf.get(1, 0).unwrap().is_empty(),
2240 "Continuation at x=1 should be cleared when head is overwritten"
2241 );
2242 }
2243
2244 #[test]
2245 fn set_raw_overwrite_wide_head_with_single_clears_tails() {
2246 let mut buf = Buffer::new(10, 1);
2247
2248 buf.set(0, 0, Cell::from_char('中'));
2249 assert!(buf.get(1, 0).unwrap().is_continuation());
2250 buf.clear_dirty();
2251
2252 buf.set_raw(0, 0, Cell::from_char('A'));
2253
2254 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('A'));
2255 assert!(
2256 buf.get(1, 0).unwrap().is_empty(),
2257 "set_raw should clear stale continuation tails when overwriting a wide head"
2258 );
2259 let spans = buf.dirty_span_row(0).expect("dirty span row").spans();
2260 assert_eq!(spans, &[DirtySpan::new(0, 2)]);
2261 }
2262
2263 #[test]
2264 fn set_raw_wide_head_preserves_manual_tail_cells() {
2265 let mut buf = Buffer::new(10, 1);
2266
2267 buf.set_raw(0, 0, Cell::from_char('中'));
2268 buf.set_raw(1, 0, Cell::CONTINUATION);
2269 assert!(buf.get(1, 0).unwrap().is_continuation());
2270 buf.clear_dirty();
2271
2272 buf.set_raw(0, 0, Cell::from_char('日'));
2273
2274 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('日'));
2275 assert!(
2276 buf.get(1, 0).unwrap().is_continuation(),
2277 "set_raw wide-head replacement should not clear caller-managed tails"
2278 );
2279 let spans = buf.dirty_span_row(0).expect("dirty span row").spans();
2280 assert_eq!(spans, &[DirtySpan::new(0, 1)]);
2281 }
2282
2283 #[test]
2284 fn overwrite_continuation_with_single_clears_head_and_tails() {
2285 let mut buf = Buffer::new(10, 1);
2286
2287 buf.set(0, 0, Cell::from_char('中'));
2289 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2290 assert!(buf.get(1, 0).unwrap().is_continuation());
2291
2292 buf.set(1, 0, Cell::from_char('B'));
2294
2295 assert!(
2297 buf.get(0, 0).unwrap().is_empty(),
2298 "Head at x=0 should be cleared when its continuation is overwritten"
2299 );
2300 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('B'));
2302 }
2303
2304 #[test]
2305 fn overwrite_wide_with_another_wide() {
2306 let mut buf = Buffer::new(10, 1);
2307
2308 buf.set(0, 0, Cell::from_char('中'));
2310 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2311 assert!(buf.get(1, 0).unwrap().is_continuation());
2312
2313 buf.set(0, 0, Cell::from_char('日'));
2315
2316 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('日'));
2318 assert!(
2319 buf.get(1, 0).unwrap().is_continuation(),
2320 "Continuation should still exist for new wide char"
2321 );
2322 }
2323
2324 #[test]
2325 fn overwrite_continuation_middle_of_wide_sequence() {
2326 let mut buf = Buffer::new(10, 1);
2327
2328 buf.set(0, 0, Cell::from_char('中'));
2330 buf.set(2, 0, Cell::from_char('日'));
2331
2332 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2333 assert!(buf.get(1, 0).unwrap().is_continuation());
2334 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('日'));
2335 assert!(buf.get(3, 0).unwrap().is_continuation());
2336
2337 buf.set(1, 0, Cell::from_char('X'));
2339
2340 assert!(
2342 buf.get(0, 0).unwrap().is_empty(),
2343 "Head of first wide char should be cleared"
2344 );
2345 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('X'));
2347 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('日'));
2349 assert!(buf.get(3, 0).unwrap().is_continuation());
2350 }
2351
2352 #[test]
2353 fn wide_char_overlapping_previous_wide_char() {
2354 let mut buf = Buffer::new(10, 1);
2355
2356 buf.set(0, 0, Cell::from_char('中'));
2358 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2359 assert!(buf.get(1, 0).unwrap().is_continuation());
2360
2361 buf.set(1, 0, Cell::from_char('日'));
2363
2364 assert!(
2366 buf.get(0, 0).unwrap().is_empty(),
2367 "First wide char head should be cleared when continuation is overwritten by new wide"
2368 );
2369 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('日'));
2371 assert!(buf.get(2, 0).unwrap().is_continuation());
2372 }
2373
2374 #[test]
2375 fn wide_char_at_end_of_buffer_atomic_reject() {
2376 let mut buf = Buffer::new(5, 1);
2377
2378 buf.set(4, 0, Cell::from_char('中'));
2380
2381 assert!(
2383 buf.get(4, 0).unwrap().is_empty(),
2384 "Wide char should be rejected when tail would be out of bounds"
2385 );
2386 }
2387
2388 #[test]
2389 fn three_wide_chars_sequential_cleanup() {
2390 let mut buf = Buffer::new(10, 1);
2391
2392 buf.set(0, 0, Cell::from_char('一'));
2394 buf.set(2, 0, Cell::from_char('二'));
2395 buf.set(4, 0, Cell::from_char('三'));
2396
2397 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('一'));
2399 assert!(buf.get(1, 0).unwrap().is_continuation());
2400 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('二'));
2401 assert!(buf.get(3, 0).unwrap().is_continuation());
2402 assert_eq!(buf.get(4, 0).unwrap().content.as_char(), Some('三'));
2403 assert!(buf.get(5, 0).unwrap().is_continuation());
2404
2405 buf.set(3, 0, Cell::from_char('M'));
2407
2408 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('一'));
2410 assert!(buf.get(1, 0).unwrap().is_continuation());
2411 assert!(buf.get(2, 0).unwrap().is_empty());
2413 assert_eq!(buf.get(3, 0).unwrap().content.as_char(), Some('M'));
2415 assert_eq!(buf.get(4, 0).unwrap().content.as_char(), Some('三'));
2417 assert!(buf.get(5, 0).unwrap().is_continuation());
2418 }
2419
2420 #[test]
2421 fn overwrite_empty_cell_no_cleanup_needed() {
2422 let mut buf = Buffer::new(10, 1);
2423
2424 buf.set(5, 0, Cell::from_char('X'));
2426
2427 assert_eq!(buf.get(5, 0).unwrap().content.as_char(), Some('X'));
2428 assert!(buf.get(4, 0).unwrap().is_empty());
2430 assert!(buf.get(6, 0).unwrap().is_empty());
2431 }
2432
2433 #[test]
2434 fn wide_char_cleanup_with_opacity() {
2435 let mut buf = Buffer::new(10, 1);
2436
2437 buf.set(0, 0, Cell::default().with_bg(PackedRgba::rgb(255, 0, 0)));
2439 buf.set(1, 0, Cell::default().with_bg(PackedRgba::rgb(0, 255, 0)));
2440
2441 buf.set(0, 0, Cell::from_char('中'));
2443
2444 buf.push_opacity(0.5);
2446 buf.set(0, 0, Cell::from_char('A'));
2447 buf.pop_opacity();
2448
2449 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('A'));
2451 assert!(buf.get(1, 0).unwrap().is_empty());
2453 }
2454
2455 #[test]
2456 fn wide_char_continuation_not_treated_as_head() {
2457 let mut buf = Buffer::new(10, 1);
2458
2459 buf.set(0, 0, Cell::from_char('中'));
2461
2462 let cont = buf.get(1, 0).unwrap();
2464 assert!(cont.is_continuation());
2465 assert_eq!(cont.content.width(), 0);
2466
2467 buf.set(1, 0, Cell::from_char('日'));
2469
2470 assert!(buf.get(0, 0).unwrap().is_empty());
2472 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('日'));
2474 assert!(buf.get(2, 0).unwrap().is_continuation());
2475 }
2476
2477 #[test]
2478 fn wide_char_fill_region() {
2479 let mut buf = Buffer::new(10, 3);
2480
2481 let wide_cell = Cell::from_char('中');
2484 buf.fill(Rect::new(0, 0, 4, 2), wide_cell);
2485
2486 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2488 assert!(buf.get(1, 0).unwrap().is_continuation());
2489 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('中'));
2490 assert!(buf.get(3, 0).unwrap().is_continuation());
2491 }
2492
2493 #[test]
2494 fn default_buffer_dimensions() {
2495 let buf = Buffer::default();
2496 assert_eq!(buf.width(), 1);
2497 assert_eq!(buf.height(), 1);
2498 assert_eq!(buf.len(), 1);
2499 }
2500
2501 #[test]
2502 fn buffer_partial_eq_impl() {
2503 let buf1 = Buffer::new(5, 5);
2504 let buf2 = Buffer::new(5, 5);
2505 let mut buf3 = Buffer::new(5, 5);
2506 buf3.set(0, 0, Cell::from_char('X'));
2507
2508 assert_eq!(buf1, buf2);
2509 assert_ne!(buf1, buf3);
2510 }
2511
2512 #[test]
2513 fn degradation_level_accessible() {
2514 let mut buf = Buffer::new(10, 10);
2515 assert_eq!(buf.degradation, DegradationLevel::Full);
2516
2517 buf.degradation = DegradationLevel::SimpleBorders;
2518 assert_eq!(buf.degradation, DegradationLevel::SimpleBorders);
2519 }
2520
2521 #[test]
2524 fn get_mut_modifies_cell() {
2525 let mut buf = Buffer::new(10, 10);
2526 buf.set(3, 3, Cell::from_char('A'));
2527
2528 if let Some(cell) = buf.get_mut(3, 3) {
2529 *cell = Cell::from_char('B');
2530 }
2531
2532 assert_eq!(buf.get(3, 3).unwrap().content.as_char(), Some('B'));
2533 }
2534
2535 #[test]
2536 fn get_mut_out_of_bounds() {
2537 let mut buf = Buffer::new(5, 5);
2538 assert!(buf.get_mut(10, 10).is_none());
2539 }
2540
2541 #[test]
2544 fn clear_with_fills_all_cells() {
2545 let mut buf = Buffer::new(5, 3);
2546 let fill_cell = Cell::from_char('*');
2547 buf.clear_with(fill_cell);
2548
2549 for y in 0..3 {
2550 for x in 0..5 {
2551 assert_eq!(buf.get(x, y).unwrap().content.as_char(), Some('*'));
2552 }
2553 }
2554 }
2555
2556 #[test]
2557 fn clear_with_wide_cell_preserves_head_tail_invariant() {
2558 let mut buf = Buffer::new(5, 2);
2559 buf.clear_with(Cell::from_char('中'));
2560
2561 for y in 0..2 {
2562 assert_eq!(buf.get(0, y).unwrap().content.as_char(), Some('中'));
2563 assert!(buf.get(1, y).unwrap().is_continuation());
2564 assert_eq!(buf.get(2, y).unwrap().content.as_char(), Some('中'));
2565 assert!(buf.get(3, y).unwrap().is_continuation());
2566 assert!(buf.get(4, y).unwrap().is_empty());
2567 }
2568 }
2569
2570 #[test]
2573 fn cells_slice_has_correct_length() {
2574 let buf = Buffer::new(10, 5);
2575 assert_eq!(buf.cells().len(), 50);
2576 }
2577
2578 #[test]
2579 fn cells_mut_allows_direct_modification() {
2580 let mut buf = Buffer::new(3, 2);
2581 let cells = buf.cells_mut();
2582 cells[0] = Cell::from_char('Z');
2583
2584 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('Z'));
2585 }
2586
2587 #[test]
2590 fn row_cells_returns_correct_row() {
2591 let mut buf = Buffer::new(5, 3);
2592 buf.set(2, 1, Cell::from_char('R'));
2593
2594 let row = buf.row_cells(1);
2595 assert_eq!(row.len(), 5);
2596 assert_eq!(row[2].content.as_char(), Some('R'));
2597 }
2598
2599 #[test]
2600 fn row_cells_mut_span_marks_once_and_returns_slice() {
2601 let mut buf = Buffer::new(5, 3);
2602 buf.clear_dirty();
2603
2604 let row = buf
2605 .row_cells_mut_span(1, 1, 4)
2606 .expect("row span should be in bounds");
2607 assert_eq!(row.len(), 3);
2608 row[0] = Cell::from_char('A');
2609 row[1] = Cell::from_char('B');
2610 row[2] = Cell::from_char('C');
2611
2612 assert!(buf.is_row_dirty(1));
2613 let spans = buf.dirty_span_row(1).expect("dirty span row").spans();
2614 assert_eq!(spans, &[DirtySpan::new(1, 4)]);
2615 assert_eq!(buf.get(1, 1).unwrap().content.as_char(), Some('A'));
2616 assert_eq!(buf.get(2, 1).unwrap().content.as_char(), Some('B'));
2617 assert_eq!(buf.get(3, 1).unwrap().content.as_char(), Some('C'));
2618 }
2619
2620 #[test]
2621 fn row_cells_mut_span_clamps_to_buffer_width() {
2622 let mut buf = Buffer::new(5, 1);
2623 buf.clear_dirty();
2624
2625 let row = buf
2626 .row_cells_mut_span(0, 3, 99)
2627 .expect("row span should clamp");
2628 assert_eq!(row.len(), 2);
2629 row[0] = Cell::from_char('X');
2630 row[1] = Cell::from_char('Y');
2631
2632 let spans = buf.dirty_span_row(0).expect("dirty span row").spans();
2633 assert_eq!(spans, &[DirtySpan::new(3, 5)]);
2634 assert_eq!(buf.get(3, 0).unwrap().content.as_char(), Some('X'));
2635 assert_eq!(buf.get(4, 0).unwrap().content.as_char(), Some('Y'));
2636 }
2637
2638 #[test]
2639 fn row_cells_mut_span_rejects_reversed_ranges() {
2640 let mut buf = Buffer::new(5, 1);
2641 buf.clear_dirty();
2642
2643 assert!(buf.row_cells_mut_span(0, 4, 2).is_none());
2644 assert!(
2645 !buf.is_row_dirty(0),
2646 "reversed ranges should not mark rows dirty"
2647 );
2648 assert!(
2649 buf.dirty_span_row(0)
2650 .expect("dirty span row")
2651 .spans()
2652 .is_empty(),
2653 "reversed ranges should not add dirty spans"
2654 );
2655 }
2656
2657 #[test]
2658 #[should_panic]
2659 fn row_cells_out_of_bounds_panics() {
2660 let buf = Buffer::new(5, 3);
2661 let _ = buf.row_cells(5);
2662 }
2663
2664 #[test]
2667 fn buffer_is_not_empty() {
2668 let buf = Buffer::new(1, 1);
2669 assert!(!buf.is_empty());
2670 }
2671
2672 #[test]
2675 fn set_raw_out_of_bounds_is_safe() {
2676 let mut buf = Buffer::new(5, 5);
2677 buf.set_raw(100, 100, Cell::from_char('X'));
2678 }
2680
2681 #[test]
2684 fn copy_from_out_of_bounds_partial() {
2685 let mut src = Buffer::new(5, 5);
2686 src.set(0, 0, Cell::from_char('A'));
2687 src.set(4, 4, Cell::from_char('B'));
2688
2689 let mut dst = Buffer::new(5, 5);
2690 dst.copy_from(&src, Rect::new(0, 0, 5, 5), 3, 3);
2692
2693 assert_eq!(dst.get(3, 3).unwrap().content.as_char(), Some('A'));
2695 assert!(dst.get(4, 4).unwrap().is_empty());
2697 }
2698
2699 #[test]
2702 fn content_eq_different_dimensions() {
2703 let buf1 = Buffer::new(5, 5);
2704 let buf2 = Buffer::new(10, 10);
2705 assert!(!buf1.content_eq(&buf2));
2707 }
2708
2709 mod property {
2712 use super::*;
2713 use proptest::prelude::*;
2714
2715 proptest! {
2716 #[test]
2717 fn buffer_dimensions_are_preserved(width in 1u16..200, height in 1u16..200) {
2718 let buf = Buffer::new(width, height);
2719 prop_assert_eq!(buf.width(), width);
2720 prop_assert_eq!(buf.height(), height);
2721 prop_assert_eq!(buf.len(), width as usize * height as usize);
2722 }
2723
2724 #[test]
2725 fn buffer_get_in_bounds_always_succeeds(width in 1u16..100, height in 1u16..100) {
2726 let buf = Buffer::new(width, height);
2727 for x in 0..width {
2728 for y in 0..height {
2729 prop_assert!(buf.get(x, y).is_some(), "get({x},{y}) failed for {width}x{height} buffer");
2730 }
2731 }
2732 }
2733
2734 #[test]
2735 fn buffer_get_out_of_bounds_returns_none(width in 1u16..50, height in 1u16..50) {
2736 let buf = Buffer::new(width, height);
2737 prop_assert!(buf.get(width, 0).is_none());
2738 prop_assert!(buf.get(0, height).is_none());
2739 prop_assert!(buf.get(width, height).is_none());
2740 }
2741
2742 #[test]
2743 fn buffer_set_get_roundtrip(
2744 width in 5u16..50,
2745 height in 5u16..50,
2746 x in 0u16..5,
2747 y in 0u16..5,
2748 ch_idx in 0u32..26,
2749 ) {
2750 let x = x % width;
2751 let y = y % height;
2752 let ch = char::from_u32('A' as u32 + ch_idx).unwrap();
2753 let mut buf = Buffer::new(width, height);
2754 buf.set(x, y, Cell::from_char(ch));
2755 let got = buf.get(x, y).unwrap();
2756 prop_assert_eq!(got.content.as_char(), Some(ch));
2757 }
2758
2759 #[test]
2760 fn scissor_push_pop_stack_depth(
2761 width in 10u16..50,
2762 height in 10u16..50,
2763 push_count in 1usize..10,
2764 ) {
2765 let mut buf = Buffer::new(width, height);
2766 prop_assert_eq!(buf.scissor_depth(), 1); for i in 0..push_count {
2769 buf.push_scissor(Rect::new(0, 0, width, height));
2770 prop_assert_eq!(buf.scissor_depth(), i + 2);
2771 }
2772
2773 for i in (0..push_count).rev() {
2774 buf.pop_scissor();
2775 prop_assert_eq!(buf.scissor_depth(), i + 1);
2776 }
2777
2778 buf.pop_scissor();
2780 prop_assert_eq!(buf.scissor_depth(), 1);
2781 }
2782
2783 #[test]
2784 fn scissor_monotonic_intersection(
2785 width in 20u16..60,
2786 height in 20u16..60,
2787 ) {
2788 let mut buf = Buffer::new(width, height);
2790 let outer = Rect::new(2, 2, width - 4, height - 4);
2791 buf.push_scissor(outer);
2792 let s1 = buf.current_scissor();
2793
2794 let inner = Rect::new(5, 5, 10, 10);
2795 buf.push_scissor(inner);
2796 let s2 = buf.current_scissor();
2797
2798 prop_assert!(s2.width <= s1.width, "inner width {} > outer width {}", s2.width, s1.width);
2800 prop_assert!(s2.height <= s1.height, "inner height {} > outer height {}", s2.height, s1.height);
2801 }
2802
2803 #[test]
2804 fn opacity_push_pop_stack_depth(
2805 width in 5u16..20,
2806 height in 5u16..20,
2807 push_count in 1usize..10,
2808 ) {
2809 let mut buf = Buffer::new(width, height);
2810 prop_assert_eq!(buf.opacity_depth(), 1);
2811
2812 for i in 0..push_count {
2813 buf.push_opacity(0.9);
2814 prop_assert_eq!(buf.opacity_depth(), i + 2);
2815 }
2816
2817 for i in (0..push_count).rev() {
2818 buf.pop_opacity();
2819 prop_assert_eq!(buf.opacity_depth(), i + 1);
2820 }
2821
2822 buf.pop_opacity();
2823 prop_assert_eq!(buf.opacity_depth(), 1);
2824 }
2825
2826 #[test]
2827 fn opacity_multiplication_is_monotonic(
2828 opacity1 in 0.0f32..=1.0,
2829 opacity2 in 0.0f32..=1.0,
2830 ) {
2831 let mut buf = Buffer::new(5, 5);
2832 buf.push_opacity(opacity1);
2833 let after_first = buf.current_opacity();
2834 buf.push_opacity(opacity2);
2835 let after_second = buf.current_opacity();
2836
2837 prop_assert!(after_second <= after_first + f32::EPSILON,
2839 "opacity increased: {} -> {}", after_first, after_second);
2840 }
2841
2842 #[test]
2843 fn clear_resets_all_cells(width in 1u16..30, height in 1u16..30) {
2844 let mut buf = Buffer::new(width, height);
2845 for x in 0..width {
2847 buf.set_raw(x, 0, Cell::from_char('X'));
2848 }
2849 buf.clear();
2850 for y in 0..height {
2852 for x in 0..width {
2853 prop_assert!(buf.get(x, y).unwrap().is_empty(),
2854 "cell ({x},{y}) not empty after clear");
2855 }
2856 }
2857 }
2858
2859 #[test]
2860 fn content_eq_is_reflexive(width in 1u16..30, height in 1u16..30) {
2861 let buf = Buffer::new(width, height);
2862 prop_assert!(buf.content_eq(&buf));
2863 }
2864
2865 #[test]
2866 fn content_eq_detects_single_change(
2867 width in 5u16..30,
2868 height in 5u16..30,
2869 x in 0u16..5,
2870 y in 0u16..5,
2871 ) {
2872 let x = x % width;
2873 let y = y % height;
2874 let buf1 = Buffer::new(width, height);
2875 let mut buf2 = Buffer::new(width, height);
2876 buf2.set_raw(x, y, Cell::from_char('Z'));
2877 prop_assert!(!buf1.content_eq(&buf2));
2878 }
2879
2880 #[test]
2883 fn dimensions_immutable_through_operations(
2884 width in 5u16..30,
2885 height in 5u16..30,
2886 ) {
2887 let mut buf = Buffer::new(width, height);
2888
2889 buf.set(0, 0, Cell::from_char('A'));
2891 prop_assert_eq!(buf.width(), width);
2892 prop_assert_eq!(buf.height(), height);
2893 prop_assert_eq!(buf.len(), width as usize * height as usize);
2894
2895 buf.push_scissor(Rect::new(1, 1, 3, 3));
2896 prop_assert_eq!(buf.width(), width);
2897 prop_assert_eq!(buf.height(), height);
2898
2899 buf.push_opacity(0.5);
2900 prop_assert_eq!(buf.width(), width);
2901 prop_assert_eq!(buf.height(), height);
2902
2903 buf.pop_scissor();
2904 buf.pop_opacity();
2905 prop_assert_eq!(buf.width(), width);
2906 prop_assert_eq!(buf.height(), height);
2907
2908 buf.clear();
2909 prop_assert_eq!(buf.width(), width);
2910 prop_assert_eq!(buf.height(), height);
2911 prop_assert_eq!(buf.len(), width as usize * height as usize);
2912 }
2913
2914 #[test]
2915 fn scissor_area_never_increases_random_rects(
2916 width in 20u16..60,
2917 height in 20u16..60,
2918 rects in proptest::collection::vec(
2919 (0u16..20, 0u16..20, 1u16..15, 1u16..15),
2920 1..8
2921 ),
2922 ) {
2923 let mut buf = Buffer::new(width, height);
2924 let mut prev_area = (width as u32) * (height as u32);
2925
2926 for (x, y, w, h) in rects {
2927 buf.push_scissor(Rect::new(x, y, w, h));
2928 let s = buf.current_scissor();
2929 let area = (s.width as u32) * (s.height as u32);
2930 prop_assert!(area <= prev_area,
2931 "scissor area increased: {} -> {} after push({},{},{},{})",
2932 prev_area, area, x, y, w, h);
2933 prev_area = area;
2934 }
2935 }
2936
2937 #[test]
2938 fn opacity_range_invariant_random_sequence(
2939 opacities in proptest::collection::vec(0.0f32..=1.0, 1..15),
2940 ) {
2941 let mut buf = Buffer::new(5, 5);
2942
2943 for &op in &opacities {
2944 buf.push_opacity(op);
2945 let current = buf.current_opacity();
2946 prop_assert!(current >= 0.0, "opacity below 0: {}", current);
2947 prop_assert!(current <= 1.0 + f32::EPSILON,
2948 "opacity above 1: {}", current);
2949 }
2950
2951 for _ in &opacities {
2953 buf.pop_opacity();
2954 }
2955 prop_assert!((buf.current_opacity() - 1.0).abs() < f32::EPSILON);
2957 }
2958
2959 #[test]
2960 fn opacity_clamp_out_of_range(
2961 neg in -100.0f32..0.0,
2962 over in 1.01f32..100.0,
2963 ) {
2964 let mut buf = Buffer::new(5, 5);
2965
2966 buf.push_opacity(neg);
2967 prop_assert!(buf.current_opacity() >= 0.0,
2968 "negative opacity not clamped: {}", buf.current_opacity());
2969 buf.pop_opacity();
2970
2971 buf.push_opacity(over);
2972 prop_assert!(buf.current_opacity() <= 1.0 + f32::EPSILON,
2973 "over-1 opacity not clamped: {}", buf.current_opacity());
2974 }
2975
2976 #[test]
2977 fn scissor_stack_always_has_base(
2978 pushes in 0usize..10,
2979 pops in 0usize..15,
2980 ) {
2981 let mut buf = Buffer::new(10, 10);
2982
2983 for _ in 0..pushes {
2984 buf.push_scissor(Rect::new(0, 0, 5, 5));
2985 }
2986 for _ in 0..pops {
2987 buf.pop_scissor();
2988 }
2989
2990 prop_assert!(buf.scissor_depth() >= 1,
2992 "scissor depth dropped below 1 after {} pushes, {} pops",
2993 pushes, pops);
2994 }
2995
2996 #[test]
2997 fn opacity_stack_always_has_base(
2998 pushes in 0usize..10,
2999 pops in 0usize..15,
3000 ) {
3001 let mut buf = Buffer::new(10, 10);
3002
3003 for _ in 0..pushes {
3004 buf.push_opacity(0.5);
3005 }
3006 for _ in 0..pops {
3007 buf.pop_opacity();
3008 }
3009
3010 prop_assert!(buf.opacity_depth() >= 1,
3012 "opacity depth dropped below 1 after {} pushes, {} pops",
3013 pushes, pops);
3014 }
3015
3016 #[test]
3017 fn cells_len_invariant_always_holds(
3018 width in 1u16..50,
3019 height in 1u16..50,
3020 ) {
3021 let mut buf = Buffer::new(width, height);
3022 let expected = width as usize * height as usize;
3023
3024 prop_assert_eq!(buf.cells().len(), expected);
3025
3026 buf.set(0, 0, Cell::from_char('X'));
3028 prop_assert_eq!(buf.cells().len(), expected);
3029
3030 buf.clear();
3031 prop_assert_eq!(buf.cells().len(), expected);
3032 }
3033
3034 #[test]
3035 fn set_outside_scissor_is_noop(
3036 width in 10u16..30,
3037 height in 10u16..30,
3038 ) {
3039 let mut buf = Buffer::new(width, height);
3040 buf.push_scissor(Rect::new(2, 2, 3, 3));
3041
3042 buf.set(0, 0, Cell::from_char('X'));
3044 let cell = buf.get(0, 0).unwrap();
3046 prop_assert!(cell.is_empty(),
3047 "cell (0,0) modified outside scissor region");
3048
3049 buf.set(3, 3, Cell::from_char('Y'));
3051 let cell = buf.get(3, 3).unwrap();
3052 prop_assert_eq!(cell.content.as_char(), Some('Y'));
3053 }
3054
3055 #[test]
3058 fn wide_char_overwrites_cleanup_tails(
3059 width in 10u16..30,
3060 x in 0u16..8,
3061 ) {
3062 let x = x % (width.saturating_sub(2).max(1));
3063 let mut buf = Buffer::new(width, 1);
3064
3065 buf.set(x, 0, Cell::from_char('中'));
3067
3068 if x + 1 < width {
3070 let head = buf.get(x, 0).unwrap();
3071 let tail = buf.get(x + 1, 0).unwrap();
3072
3073 if head.content.as_char() == Some('中') {
3074 prop_assert!(tail.is_continuation(),
3075 "tail at x+1={} should be continuation", x + 1);
3076
3077 buf.set(x, 0, Cell::from_char('A'));
3079 let new_head = buf.get(x, 0).unwrap();
3080 let cleared_tail = buf.get(x + 1, 0).unwrap();
3081
3082 prop_assert_eq!(new_head.content.as_char(), Some('A'));
3083 prop_assert!(cleared_tail.is_empty(),
3084 "tail should be cleared after head overwrite");
3085 }
3086 }
3087 }
3088
3089 #[test]
3090 fn wide_char_atomic_rejection_at_boundary(
3091 width in 3u16..20,
3092 ) {
3093 let mut buf = Buffer::new(width, 1);
3094
3095 let last_pos = width - 1;
3097 buf.set(last_pos, 0, Cell::from_char('中'));
3098
3099 let cell = buf.get(last_pos, 0).unwrap();
3101 prop_assert!(cell.is_empty(),
3102 "wide char at boundary position {} (width {}) should be rejected",
3103 last_pos, width);
3104 }
3105
3106 #[test]
3111 fn double_buffer_swap_is_involution(ops in proptest::collection::vec(proptest::bool::ANY, 0..100)) {
3112 let mut db = DoubleBuffer::new(10, 10);
3113 let initial_idx = db.current_idx;
3114
3115 for do_swap in &ops {
3116 if *do_swap {
3117 db.swap();
3118 }
3119 }
3120
3121 let swap_count = ops.iter().filter(|&&x| x).count();
3122 let expected_idx = if swap_count % 2 == 0 { initial_idx } else { 1 - initial_idx };
3123
3124 prop_assert_eq!(db.current_idx, expected_idx,
3125 "After {} swaps, index should be {} but was {}",
3126 swap_count, expected_idx, db.current_idx);
3127 }
3128
3129 #[test]
3130 fn double_buffer_resize_preserves_invariant(
3131 init_w in 1u16..200,
3132 init_h in 1u16..100,
3133 new_w in 1u16..200,
3134 new_h in 1u16..100,
3135 ) {
3136 let mut db = DoubleBuffer::new(init_w, init_h);
3137 db.resize(new_w, new_h);
3138
3139 prop_assert_eq!(db.width(), new_w);
3140 prop_assert_eq!(db.height(), new_h);
3141 prop_assert!(db.dimensions_match(new_w, new_h));
3142 }
3143
3144 #[test]
3145 fn double_buffer_current_previous_disjoint(
3146 width in 1u16..50,
3147 height in 1u16..50,
3148 ) {
3149 let mut db = DoubleBuffer::new(width, height);
3150
3151 db.current_mut().set(0, 0, Cell::from_char('C'));
3153
3154 prop_assert!(db.previous().get(0, 0).unwrap().is_empty(),
3156 "Previous buffer should not reflect changes to current");
3157
3158 db.swap();
3160 prop_assert_eq!(db.previous().get(0, 0).unwrap().content.as_char(), Some('C'),
3161 "After swap, previous should have the 'C' we wrote");
3162 }
3163
3164 #[test]
3165 fn double_buffer_swap_content_semantics(
3166 width in 5u16..30,
3167 height in 5u16..30,
3168 ) {
3169 let mut db = DoubleBuffer::new(width, height);
3170
3171 db.current_mut().set(0, 0, Cell::from_char('X'));
3173 db.swap();
3174
3175 db.current_mut().set(0, 0, Cell::from_char('Y'));
3177 db.swap();
3178
3179 prop_assert_eq!(db.current().get(0, 0).unwrap().content.as_char(), Some('X'));
3181 prop_assert_eq!(db.previous().get(0, 0).unwrap().content.as_char(), Some('Y'));
3182 }
3183
3184 #[test]
3185 fn double_buffer_resize_clears_both(
3186 w1 in 5u16..30,
3187 h1 in 5u16..30,
3188 w2 in 5u16..30,
3189 h2 in 5u16..30,
3190 ) {
3191 prop_assume!(w1 != w2 || h1 != h2);
3193
3194 let mut db = DoubleBuffer::new(w1, h1);
3195
3196 db.current_mut().set(0, 0, Cell::from_char('A'));
3198 db.swap();
3199 db.current_mut().set(0, 0, Cell::from_char('B'));
3200
3201 db.resize(w2, h2);
3203
3204 prop_assert!(db.current().get(0, 0).unwrap().is_empty(),
3206 "Current buffer should be empty after resize");
3207 prop_assert!(db.previous().get(0, 0).unwrap().is_empty(),
3208 "Previous buffer should be empty after resize");
3209 }
3210 }
3211 }
3212
3213 #[test]
3216 fn dirty_rows_start_dirty() {
3217 let buf = Buffer::new(10, 5);
3219 assert_eq!(buf.dirty_row_count(), 5);
3220 for y in 0..5 {
3221 assert!(buf.is_row_dirty(y));
3222 }
3223 }
3224
3225 #[test]
3226 fn dirty_bitmap_starts_full() {
3227 let buf = Buffer::new(4, 3);
3228 assert!(buf.dirty_all());
3229 assert_eq!(buf.dirty_cell_count(), 12);
3230 }
3231
3232 #[test]
3233 fn dirty_bitmap_tracks_single_cell() {
3234 let mut buf = Buffer::new(4, 3);
3235 buf.clear_dirty();
3236 assert!(!buf.dirty_all());
3237 buf.set_raw(1, 1, Cell::from_char('X'));
3238 let idx = 1 + 4;
3239 assert_eq!(buf.dirty_cell_count(), 1);
3240 assert_eq!(buf.dirty_bits()[idx], 1);
3241 }
3242
3243 #[test]
3244 fn dirty_bitmap_dedupes_cells() {
3245 let mut buf = Buffer::new(4, 3);
3246 buf.clear_dirty();
3247 buf.set_raw(2, 2, Cell::from_char('A'));
3248 buf.set_raw(2, 2, Cell::from_char('B'));
3249 assert_eq!(buf.dirty_cell_count(), 1);
3250 }
3251
3252 #[test]
3253 fn set_marks_row_dirty() {
3254 let mut buf = Buffer::new(10, 5);
3255 buf.clear_dirty(); buf.set(3, 2, Cell::from_char('X'));
3257 assert!(buf.is_row_dirty(2));
3258 assert!(!buf.is_row_dirty(0));
3259 assert!(!buf.is_row_dirty(1));
3260 assert!(!buf.is_row_dirty(3));
3261 assert!(!buf.is_row_dirty(4));
3262 }
3263
3264 #[test]
3265 fn set_raw_marks_row_dirty() {
3266 let mut buf = Buffer::new(10, 5);
3267 buf.clear_dirty(); buf.set_raw(0, 4, Cell::from_char('Z'));
3269 assert!(buf.is_row_dirty(4));
3270 assert_eq!(buf.dirty_row_count(), 1);
3271 }
3272
3273 #[test]
3274 fn clear_marks_all_dirty() {
3275 let mut buf = Buffer::new(10, 5);
3276 buf.clear();
3277 assert_eq!(buf.dirty_row_count(), 5);
3278 }
3279
3280 #[test]
3281 fn clear_dirty_resets_flags() {
3282 let mut buf = Buffer::new(10, 5);
3283 assert_eq!(buf.dirty_row_count(), 5);
3285 buf.clear_dirty();
3286 assert_eq!(buf.dirty_row_count(), 0);
3287
3288 buf.set(0, 0, Cell::from_char('A'));
3290 buf.set(0, 3, Cell::from_char('B'));
3291 assert_eq!(buf.dirty_row_count(), 2);
3292
3293 buf.clear_dirty();
3294 assert_eq!(buf.dirty_row_count(), 0);
3295 }
3296
3297 #[test]
3298 fn clear_dirty_resets_bitmap() {
3299 let mut buf = Buffer::new(4, 3);
3300 buf.clear();
3301 assert!(buf.dirty_all());
3302 buf.clear_dirty();
3303 assert!(!buf.dirty_all());
3304 assert_eq!(buf.dirty_cell_count(), 0);
3305 assert!(buf.dirty_bits().iter().all(|&b| b == 0));
3306 }
3307
3308 #[test]
3309 fn fill_with_wide_cells_leaves_no_stale_content() {
3310 let mut buf = Buffer::new(5, 1);
3311 for x in 0..5 {
3312 buf.set(x, 0, Cell::from_char('X'));
3313 }
3314 let wide = Cell::from_char('世');
3318 assert_eq!(wide.content.width(), 2);
3319 buf.fill(Rect::new(0, 0, 5, 1), wide);
3320 let trailing = *buf.get(4, 0).expect("in bounds");
3321 assert_ne!(
3322 trailing,
3323 Cell::from_char('X'),
3324 "stale content survived fill"
3325 );
3326 assert_eq!(trailing, Cell::default());
3327 assert_eq!(*buf.get(0, 0).unwrap(), wide);
3329 assert_eq!(*buf.get(2, 0).unwrap(), wide);
3330 }
3331
3332 #[test]
3333 fn fill_marks_affected_rows_dirty() {
3334 let mut buf = Buffer::new(10, 10);
3335 buf.clear_dirty(); buf.fill(Rect::new(0, 2, 5, 3), Cell::from_char('.'));
3337 assert!(!buf.is_row_dirty(0));
3339 assert!(!buf.is_row_dirty(1));
3340 assert!(buf.is_row_dirty(2));
3341 assert!(buf.is_row_dirty(3));
3342 assert!(buf.is_row_dirty(4));
3343 assert!(!buf.is_row_dirty(5));
3344 }
3345
3346 #[test]
3347 fn get_mut_marks_row_dirty() {
3348 let mut buf = Buffer::new(10, 5);
3349 buf.clear_dirty(); if let Some(cell) = buf.get_mut(5, 3) {
3351 cell.fg = PackedRgba::rgb(255, 0, 0);
3352 }
3353 assert!(buf.is_row_dirty(3));
3354 assert_eq!(buf.dirty_row_count(), 1);
3355 }
3356
3357 #[test]
3358 fn cells_mut_marks_all_dirty() {
3359 let mut buf = Buffer::new(10, 5);
3360 let _ = buf.cells_mut();
3361 assert_eq!(buf.dirty_row_count(), 5);
3362 }
3363
3364 #[test]
3365 fn dirty_rows_slice_length_matches_height() {
3366 let buf = Buffer::new(10, 7);
3367 assert_eq!(buf.dirty_rows().len(), 7);
3368 }
3369
3370 #[test]
3371 fn out_of_bounds_set_does_not_dirty() {
3372 let mut buf = Buffer::new(10, 5);
3373 buf.clear_dirty(); buf.set(100, 100, Cell::from_char('X'));
3375 assert_eq!(buf.dirty_row_count(), 0);
3376 }
3377
3378 #[test]
3379 fn property_dirty_soundness() {
3380 let mut buf = Buffer::new(20, 10);
3382 let positions = [(3, 0), (5, 2), (0, 9), (19, 5), (10, 7)];
3383 for &(x, y) in &positions {
3384 buf.set(x, y, Cell::from_char('*'));
3385 }
3386 for &(_, y) in &positions {
3387 assert!(
3388 buf.is_row_dirty(y),
3389 "Row {} should be dirty after set({}, {})",
3390 y,
3391 positions.iter().find(|(_, ry)| *ry == y).unwrap().0,
3392 y
3393 );
3394 }
3395 }
3396
3397 #[test]
3398 fn dirty_clear_between_frames() {
3399 let mut buf = Buffer::new(10, 5);
3401
3402 assert_eq!(buf.dirty_row_count(), 5);
3404
3405 buf.clear_dirty();
3407 assert_eq!(buf.dirty_row_count(), 0);
3408
3409 buf.set(0, 0, Cell::from_char('A'));
3411 buf.set(0, 2, Cell::from_char('B'));
3412 assert_eq!(buf.dirty_row_count(), 2);
3413
3414 buf.clear_dirty();
3416 assert_eq!(buf.dirty_row_count(), 0);
3417
3418 buf.set(0, 4, Cell::from_char('C'));
3420 assert_eq!(buf.dirty_row_count(), 1);
3421 assert!(buf.is_row_dirty(4));
3422 assert!(!buf.is_row_dirty(0));
3423 }
3424
3425 #[test]
3428 fn dirty_spans_start_full_dirty() {
3429 let buf = Buffer::new(10, 5);
3430 for y in 0..5 {
3431 let row = buf.dirty_span_row(y).unwrap();
3432 assert!(row.is_full(), "row {y} should start full-dirty");
3433 assert!(row.spans().is_empty(), "row {y} spans should start empty");
3434 }
3435 }
3436
3437 #[test]
3438 fn clear_dirty_resets_spans() {
3439 let mut buf = Buffer::new(10, 5);
3440 buf.clear_dirty();
3441 for y in 0..5 {
3442 let row = buf.dirty_span_row(y).unwrap();
3443 assert!(!row.is_full(), "row {y} should clear full-dirty");
3444 assert!(row.spans().is_empty(), "row {y} spans should be cleared");
3445 }
3446 assert_eq!(buf.dirty_span_overflows, 0);
3447 }
3448
3449 #[test]
3450 fn set_records_dirty_span() {
3451 let mut buf = Buffer::new(20, 2);
3452 buf.clear_dirty();
3453 buf.set(2, 0, Cell::from_char('A'));
3454 let row = buf.dirty_span_row(0).unwrap();
3455 assert_eq!(row.spans(), &[DirtySpan::new(2, 3)]);
3456 assert!(!row.is_full());
3457 }
3458
3459 #[test]
3460 fn set_merges_adjacent_spans() {
3461 let mut buf = Buffer::new(20, 2);
3462 buf.clear_dirty();
3463 buf.set(2, 0, Cell::from_char('A'));
3464 buf.set(3, 0, Cell::from_char('B')); let row = buf.dirty_span_row(0).unwrap();
3466 assert_eq!(row.spans(), &[DirtySpan::new(2, 4)]);
3467 }
3468
3469 #[test]
3470 fn set_merges_close_spans() {
3471 let mut buf = Buffer::new(20, 2);
3472 buf.clear_dirty();
3473 buf.set(2, 0, Cell::from_char('A'));
3474 buf.set(4, 0, Cell::from_char('B')); let row = buf.dirty_span_row(0).unwrap();
3476 assert_eq!(row.spans(), &[DirtySpan::new(2, 5)]);
3477 }
3478
3479 #[test]
3480 fn span_overflow_sets_full_row() {
3481 let width = (DIRTY_SPAN_MAX_SPANS_PER_ROW as u16 + 2) * 3;
3482 let mut buf = Buffer::new(width, 1);
3483 buf.clear_dirty();
3484 for i in 0..(DIRTY_SPAN_MAX_SPANS_PER_ROW + 1) {
3485 let x = (i as u16) * 3;
3486 buf.set(x, 0, Cell::from_char('x'));
3487 }
3488 let row = buf.dirty_span_row(0).unwrap();
3489 assert!(row.is_full());
3490 assert!(row.spans().is_empty());
3491 assert_eq!(buf.dirty_span_overflows, 1);
3492 }
3493
3494 #[test]
3495 fn fill_full_row_marks_full_span() {
3496 let mut buf = Buffer::new(10, 3);
3497 buf.clear_dirty();
3498 let cell = Cell::from_char('x').with_bg(PackedRgba::rgb(0, 0, 0));
3499 buf.fill(Rect::new(0, 1, 10, 1), cell);
3500 let row = buf.dirty_span_row(1).unwrap();
3501 assert!(row.is_full());
3502 assert!(row.spans().is_empty());
3503 }
3504
3505 #[test]
3506 fn get_mut_records_dirty_span() {
3507 let mut buf = Buffer::new(10, 5);
3508 buf.clear_dirty();
3509 let _ = buf.get_mut(5, 3);
3510 let row = buf.dirty_span_row(3).unwrap();
3511 assert_eq!(row.spans(), &[DirtySpan::new(5, 6)]);
3512 }
3513
3514 #[test]
3515 fn cells_mut_marks_all_full_spans() {
3516 let mut buf = Buffer::new(10, 5);
3517 buf.clear_dirty();
3518 let _ = buf.cells_mut();
3519 for y in 0..5 {
3520 let row = buf.dirty_span_row(y).unwrap();
3521 assert!(row.is_full(), "row {y} should be full after cells_mut");
3522 }
3523 }
3524
3525 #[test]
3526 fn dirty_span_config_disabled_skips_rows() {
3527 let mut buf = Buffer::new(10, 1);
3528 buf.clear_dirty();
3529 buf.set_dirty_span_config(DirtySpanConfig::default().with_enabled(false));
3530 buf.set(5, 0, Cell::from_char('x'));
3531 assert!(buf.dirty_span_row(0).is_none());
3532 let stats = buf.dirty_span_stats();
3533 assert_eq!(stats.total_spans, 0);
3534 assert_eq!(stats.span_coverage_cells, 0);
3535 }
3536
3537 #[test]
3538 fn dirty_span_guard_band_expands_span_bounds() {
3539 let mut buf = Buffer::new(10, 1);
3540 buf.clear_dirty();
3541 buf.set_dirty_span_config(DirtySpanConfig::default().with_guard_band(2));
3542 buf.set(5, 0, Cell::from_char('x'));
3543 let row = buf.dirty_span_row(0).unwrap();
3544 assert_eq!(row.spans(), &[DirtySpan::new(3, 8)]);
3545 }
3546
3547 #[test]
3548 fn dirty_span_max_spans_overflow_triggers_full_row() {
3549 let mut buf = Buffer::new(10, 1);
3550 buf.clear_dirty();
3551 buf.set_dirty_span_config(
3552 DirtySpanConfig::default()
3553 .with_max_spans_per_row(1)
3554 .with_merge_gap(0),
3555 );
3556 buf.set(0, 0, Cell::from_char('a'));
3557 buf.set(4, 0, Cell::from_char('b'));
3558 let row = buf.dirty_span_row(0).unwrap();
3559 assert!(row.is_full());
3560 assert!(row.spans().is_empty());
3561 assert_eq!(buf.dirty_span_overflows, 1);
3562 }
3563
3564 #[test]
3565 fn dirty_span_stats_counts_full_rows_and_spans() {
3566 let mut buf = Buffer::new(6, 2);
3567 buf.clear_dirty();
3568 buf.set_dirty_span_config(DirtySpanConfig::default().with_merge_gap(0));
3569 buf.set(1, 0, Cell::from_char('a'));
3570 buf.set(4, 0, Cell::from_char('b'));
3571 buf.mark_dirty_row_full(1);
3572
3573 let stats = buf.dirty_span_stats();
3574 assert_eq!(stats.rows_full_dirty, 1);
3575 assert_eq!(stats.rows_with_spans, 1);
3576 assert_eq!(stats.total_spans, 2);
3577 assert_eq!(stats.max_span_len, 6);
3578 assert_eq!(stats.span_coverage_cells, 8);
3579 }
3580
3581 #[test]
3582 fn dirty_span_stats_reports_overflow_and_full_row() {
3583 let mut buf = Buffer::new(8, 1);
3584 buf.clear_dirty();
3585 buf.set_dirty_span_config(
3586 DirtySpanConfig::default()
3587 .with_max_spans_per_row(1)
3588 .with_merge_gap(0),
3589 );
3590 buf.set(0, 0, Cell::from_char('x'));
3591 buf.set(3, 0, Cell::from_char('y'));
3592
3593 let stats = buf.dirty_span_stats();
3594 assert_eq!(stats.overflows, 1);
3595 assert_eq!(stats.rows_full_dirty, 1);
3596 assert_eq!(stats.total_spans, 0);
3597 assert_eq!(stats.span_coverage_cells, 8);
3598 }
3599
3600 #[test]
3605 fn double_buffer_new_has_matching_dimensions() {
3606 let db = DoubleBuffer::new(80, 24);
3607 assert_eq!(db.width(), 80);
3608 assert_eq!(db.height(), 24);
3609 assert!(db.dimensions_match(80, 24));
3610 assert!(!db.dimensions_match(120, 40));
3611 }
3612
3613 #[test]
3614 fn double_buffer_swap_is_o1() {
3615 let mut db = DoubleBuffer::new(80, 24);
3616
3617 db.current_mut().set(0, 0, Cell::from_char('A'));
3619 assert_eq!(db.current().get(0, 0).unwrap().content.as_char(), Some('A'));
3620
3621 db.swap();
3623 assert_eq!(
3624 db.previous().get(0, 0).unwrap().content.as_char(),
3625 Some('A')
3626 );
3627 assert!(db.current().get(0, 0).unwrap().is_empty());
3629 }
3630
3631 #[test]
3632 fn double_buffer_swap_round_trip() {
3633 let mut db = DoubleBuffer::new(10, 5);
3634
3635 db.current_mut().set(0, 0, Cell::from_char('X'));
3636 db.swap();
3637 db.current_mut().set(0, 0, Cell::from_char('Y'));
3638 db.swap();
3639
3640 assert_eq!(db.current().get(0, 0).unwrap().content.as_char(), Some('X'));
3642 assert_eq!(
3643 db.previous().get(0, 0).unwrap().content.as_char(),
3644 Some('Y')
3645 );
3646 }
3647
3648 #[test]
3649 fn double_buffer_resize_changes_dimensions() {
3650 let mut db = DoubleBuffer::new(80, 24);
3651 assert!(!db.resize(80, 24)); assert!(db.resize(120, 40)); assert_eq!(db.width(), 120);
3654 assert_eq!(db.height(), 40);
3655 assert!(db.dimensions_match(120, 40));
3656 }
3657
3658 #[test]
3659 fn double_buffer_resize_clears_content() {
3660 let mut db = DoubleBuffer::new(10, 5);
3661 db.current_mut().set(0, 0, Cell::from_char('Z'));
3662 db.swap();
3663 db.current_mut().set(0, 0, Cell::from_char('W'));
3664
3665 db.resize(20, 10);
3666
3667 assert!(db.current().get(0, 0).unwrap().is_empty());
3669 assert!(db.previous().get(0, 0).unwrap().is_empty());
3670 }
3671
3672 #[test]
3673 fn double_buffer_current_and_previous_are_distinct() {
3674 let mut db = DoubleBuffer::new(10, 5);
3675 db.current_mut().set(0, 0, Cell::from_char('C'));
3676
3677 assert!(db.previous().get(0, 0).unwrap().is_empty());
3679 assert_eq!(db.current().get(0, 0).unwrap().content.as_char(), Some('C'));
3680 }
3681
3682 #[test]
3687 fn adaptive_buffer_new_has_over_allocation() {
3688 let adb = AdaptiveDoubleBuffer::new(80, 24);
3689
3690 assert_eq!(adb.width(), 80);
3692 assert_eq!(adb.height(), 24);
3693 assert!(adb.dimensions_match(80, 24));
3694
3695 assert!(adb.capacity_width() > 80);
3699 assert!(adb.capacity_height() > 24);
3700 assert_eq!(adb.capacity_width(), 100); assert_eq!(adb.capacity_height(), 30); }
3703
3704 #[test]
3705 fn adaptive_buffer_resize_avoids_reallocation_when_within_capacity() {
3706 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3707
3708 assert!(adb.resize(90, 28)); assert_eq!(adb.width(), 90);
3711 assert_eq!(adb.height(), 28);
3712 assert_eq!(adb.stats().resize_avoided, 1);
3713 assert_eq!(adb.stats().resize_reallocated, 0);
3714 assert_eq!(adb.stats().resize_growth, 1);
3715 }
3716
3717 #[test]
3718 fn adaptive_buffer_resize_reallocates_on_growth_beyond_capacity() {
3719 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3720
3721 assert!(adb.resize(120, 40)); assert_eq!(adb.width(), 120);
3724 assert_eq!(adb.height(), 40);
3725 assert_eq!(adb.stats().resize_reallocated, 1);
3726 assert_eq!(adb.stats().resize_avoided, 0);
3727
3728 assert!(adb.capacity_width() > 120);
3730 assert!(adb.capacity_height() > 40);
3731 }
3732
3733 #[test]
3734 fn adaptive_buffer_resize_reallocates_on_significant_shrink() {
3735 let mut adb = AdaptiveDoubleBuffer::new(100, 50);
3736
3737 assert!(adb.resize(40, 20)); assert_eq!(adb.width(), 40);
3741 assert_eq!(adb.height(), 20);
3742 assert_eq!(adb.stats().resize_reallocated, 1);
3743 assert_eq!(adb.stats().resize_shrink, 1);
3744 }
3745
3746 #[test]
3747 fn adaptive_buffer_resize_avoids_reallocation_on_minor_shrink() {
3748 let mut adb = AdaptiveDoubleBuffer::new(100, 50);
3749
3750 assert!(adb.resize(80, 40));
3754 assert_eq!(adb.width(), 80);
3755 assert_eq!(adb.height(), 40);
3756 assert_eq!(adb.stats().resize_avoided, 1);
3757 assert_eq!(adb.stats().resize_reallocated, 0);
3758 assert_eq!(adb.stats().resize_shrink, 1);
3759 }
3760
3761 #[test]
3762 fn adaptive_buffer_no_change_returns_false() {
3763 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3764
3765 assert!(!adb.resize(80, 24)); assert_eq!(adb.stats().resize_avoided, 0);
3767 assert_eq!(adb.stats().resize_reallocated, 0);
3768 assert_eq!(adb.stats().resize_growth, 0);
3769 assert_eq!(adb.stats().resize_shrink, 0);
3770 }
3771
3772 #[test]
3773 fn adaptive_buffer_swap_works() {
3774 let mut adb = AdaptiveDoubleBuffer::new(10, 5);
3775
3776 adb.current_mut().set(0, 0, Cell::from_char('A'));
3777 assert_eq!(
3778 adb.current().get(0, 0).unwrap().content.as_char(),
3779 Some('A')
3780 );
3781
3782 adb.swap();
3783 assert_eq!(
3784 adb.previous().get(0, 0).unwrap().content.as_char(),
3785 Some('A')
3786 );
3787 assert!(adb.current().get(0, 0).unwrap().is_empty());
3788 }
3789
3790 #[test]
3791 fn adaptive_buffer_stats_reset() {
3792 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3793
3794 adb.resize(90, 28);
3795 adb.resize(120, 40);
3796 assert!(adb.stats().resize_avoided > 0 || adb.stats().resize_reallocated > 0);
3797
3798 adb.reset_stats();
3799 assert_eq!(adb.stats().resize_avoided, 0);
3800 assert_eq!(adb.stats().resize_reallocated, 0);
3801 assert_eq!(adb.stats().resize_growth, 0);
3802 assert_eq!(adb.stats().resize_shrink, 0);
3803 }
3804
3805 #[test]
3806 fn adaptive_buffer_memory_efficiency() {
3807 let adb = AdaptiveDoubleBuffer::new(80, 24);
3808
3809 let efficiency = adb.memory_efficiency();
3810 assert!(efficiency > 0.5);
3814 assert!(efficiency < 1.0);
3815 }
3816
3817 #[test]
3818 fn adaptive_buffer_logical_bounds() {
3819 let adb = AdaptiveDoubleBuffer::new(80, 24);
3820
3821 let bounds = adb.logical_bounds();
3822 assert_eq!(bounds.x, 0);
3823 assert_eq!(bounds.y, 0);
3824 assert_eq!(bounds.width, 80);
3825 assert_eq!(bounds.height, 24);
3826 }
3827
3828 #[test]
3829 fn adaptive_buffer_capacity_clamped_for_large_sizes() {
3830 let adb = AdaptiveDoubleBuffer::new(1000, 500);
3832
3833 assert_eq!(adb.capacity_width(), 1000 + 200); assert_eq!(adb.capacity_height(), 500 + 125); }
3838
3839 #[test]
3840 fn adaptive_stats_avoidance_ratio() {
3841 let mut stats = AdaptiveStats::default();
3842
3843 assert!((stats.avoidance_ratio() - 1.0).abs() < f64::EPSILON);
3845
3846 stats.resize_avoided = 3;
3848 stats.resize_reallocated = 1;
3849 assert!((stats.avoidance_ratio() - 0.75).abs() < f64::EPSILON);
3850
3851 stats.resize_avoided = 0;
3853 stats.resize_reallocated = 5;
3854 assert!((stats.avoidance_ratio() - 0.0).abs() < f64::EPSILON);
3855 }
3856
3857 #[test]
3858 fn adaptive_buffer_resize_storm_simulation() {
3859 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3861
3862 for i in 1..=10 {
3864 adb.resize(80 + i, 24 + (i / 2));
3865 }
3866
3867 let ratio = adb.stats().avoidance_ratio();
3869 assert!(
3870 ratio > 0.5,
3871 "Expected >50% avoidance ratio, got {:.2}",
3872 ratio
3873 );
3874 }
3875
3876 #[test]
3877 fn adaptive_buffer_width_only_growth() {
3878 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3879
3880 assert!(adb.resize(95, 24)); assert_eq!(adb.stats().resize_avoided, 1);
3883 assert_eq!(adb.stats().resize_growth, 1);
3884 }
3885
3886 #[test]
3887 fn adaptive_buffer_height_only_growth() {
3888 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3889
3890 assert!(adb.resize(80, 28)); assert_eq!(adb.stats().resize_avoided, 1);
3893 assert_eq!(adb.stats().resize_growth, 1);
3894 }
3895
3896 #[test]
3897 fn adaptive_buffer_one_dimension_exceeds_capacity() {
3898 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3899
3900 assert!(adb.resize(105, 24)); assert_eq!(adb.stats().resize_reallocated, 1);
3903 }
3904
3905 #[test]
3906 fn adaptive_buffer_current_and_previous_distinct() {
3907 let mut adb = AdaptiveDoubleBuffer::new(10, 5);
3908 adb.current_mut().set(0, 0, Cell::from_char('X'));
3909
3910 assert!(adb.previous().get(0, 0).unwrap().is_empty());
3912 assert_eq!(
3913 adb.current().get(0, 0).unwrap().content.as_char(),
3914 Some('X')
3915 );
3916 }
3917
3918 #[test]
3919 fn adaptive_buffer_resize_within_capacity_clears_previous() {
3920 let mut adb = AdaptiveDoubleBuffer::new(10, 5);
3921 adb.current_mut().set(9, 4, Cell::from_char('X'));
3922 adb.swap();
3923
3924 assert!(adb.resize(8, 4));
3926
3927 assert!(adb.previous().get(9, 4).unwrap().is_empty());
3929 }
3930
3931 #[test]
3933 fn adaptive_buffer_invariant_capacity_geq_logical() {
3934 for width in [1u16, 10, 80, 200, 1000, 5000] {
3936 for height in [1u16, 10, 24, 100, 500, 2000] {
3937 let adb = AdaptiveDoubleBuffer::new(width, height);
3938 assert!(
3939 adb.capacity_width() >= adb.width(),
3940 "capacity_width {} < logical_width {} for ({}, {})",
3941 adb.capacity_width(),
3942 adb.width(),
3943 width,
3944 height
3945 );
3946 assert!(
3947 adb.capacity_height() >= adb.height(),
3948 "capacity_height {} < logical_height {} for ({}, {})",
3949 adb.capacity_height(),
3950 adb.height(),
3951 width,
3952 height
3953 );
3954 }
3955 }
3956 }
3957
3958 #[test]
3959 fn adaptive_buffer_invariant_resize_dimensions_correct() {
3960 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3961
3962 let test_sizes = [
3964 (100, 50),
3965 (40, 20),
3966 (80, 24),
3967 (200, 100),
3968 (10, 5),
3969 (1000, 500),
3970 ];
3971 for (w, h) in test_sizes {
3972 adb.resize(w, h);
3973 assert_eq!(adb.width(), w, "width mismatch for ({}, {})", w, h);
3974 assert_eq!(adb.height(), h, "height mismatch for ({}, {})", w, h);
3975 assert!(
3976 adb.capacity_width() >= w,
3977 "capacity_width < width for ({}, {})",
3978 w,
3979 h
3980 );
3981 assert!(
3982 adb.capacity_height() >= h,
3983 "capacity_height < height for ({}, {})",
3984 w,
3985 h
3986 );
3987 }
3988 }
3989
3990 #[test]
3994 fn adaptive_buffer_no_ghosting_on_shrink() {
3995 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3996
3997 for y in 0..adb.height() {
3999 for x in 0..adb.width() {
4000 adb.current_mut().set(x, y, Cell::from_char('X'));
4001 }
4002 }
4003
4004 adb.resize(60, 20);
4007
4008 for y in 0..adb.height() {
4011 for x in 0..adb.width() {
4012 let cell = adb.current().get(x, y).unwrap();
4013 assert!(
4014 cell.is_empty(),
4015 "Ghost content at ({}, {}): expected empty, got {:?}",
4016 x,
4017 y,
4018 cell.content
4019 );
4020 }
4021 }
4022 }
4023
4024 #[test]
4028 fn adaptive_buffer_no_ghosting_on_reallocation_shrink() {
4029 let mut adb = AdaptiveDoubleBuffer::new(100, 50);
4030
4031 for y in 0..adb.height() {
4033 for x in 0..adb.width() {
4034 adb.current_mut().set(x, y, Cell::from_char('A'));
4035 }
4036 }
4037 adb.swap();
4038 for y in 0..adb.height() {
4039 for x in 0..adb.width() {
4040 adb.current_mut().set(x, y, Cell::from_char('B'));
4041 }
4042 }
4043
4044 adb.resize(30, 15);
4046 assert_eq!(adb.stats().resize_reallocated, 1);
4047
4048 for y in 0..adb.height() {
4050 for x in 0..adb.width() {
4051 assert!(
4052 adb.current().get(x, y).unwrap().is_empty(),
4053 "Ghost in current at ({}, {})",
4054 x,
4055 y
4056 );
4057 assert!(
4058 adb.previous().get(x, y).unwrap().is_empty(),
4059 "Ghost in previous at ({}, {})",
4060 x,
4061 y
4062 );
4063 }
4064 }
4065 }
4066
4067 #[test]
4071 fn adaptive_buffer_no_ghosting_on_growth_reallocation() {
4072 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
4073
4074 for y in 0..adb.height() {
4076 for x in 0..adb.width() {
4077 adb.current_mut().set(x, y, Cell::from_char('Z'));
4078 }
4079 }
4080
4081 adb.resize(150, 60);
4083 assert_eq!(adb.stats().resize_reallocated, 1);
4084
4085 for y in 0..adb.height() {
4087 for x in 0..adb.width() {
4088 assert!(
4089 adb.current().get(x, y).unwrap().is_empty(),
4090 "Ghost at ({}, {}) after growth reallocation",
4091 x,
4092 y
4093 );
4094 }
4095 }
4096 }
4097
4098 #[test]
4100 fn adaptive_buffer_resize_idempotent() {
4101 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
4102 adb.current_mut().set(5, 5, Cell::from_char('K'));
4103
4104 let changed = adb.resize(80, 24);
4106 assert!(!changed);
4107
4108 assert_eq!(
4110 adb.current().get(5, 5).unwrap().content.as_char(),
4111 Some('K')
4112 );
4113 }
4114
4115 #[test]
4120 fn dirty_span_merge_adjacent() {
4121 let mut buf = Buffer::new(100, 1);
4122 buf.clear_dirty(); buf.mark_dirty_span(0, 10, 20);
4126 let spans = buf.dirty_span_row(0).unwrap().spans();
4127 assert_eq!(spans.len(), 1);
4128 assert_eq!(spans[0], DirtySpan::new(10, 20));
4129
4130 buf.mark_dirty_span(0, 20, 30);
4132 let spans = buf.dirty_span_row(0).unwrap().spans();
4133 assert_eq!(spans.len(), 1);
4134 assert_eq!(spans[0], DirtySpan::new(10, 30));
4135 }
4136
4137 #[test]
4138 fn dirty_span_merge_overlapping() {
4139 let mut buf = Buffer::new(100, 1);
4140 buf.clear_dirty();
4141
4142 buf.mark_dirty_span(0, 10, 20);
4144 buf.mark_dirty_span(0, 15, 25);
4146
4147 let spans = buf.dirty_span_row(0).unwrap().spans();
4148 assert_eq!(spans.len(), 1);
4149 assert_eq!(spans[0], DirtySpan::new(10, 25));
4150 }
4151
4152 #[test]
4153 fn dirty_span_merge_with_gap() {
4154 let mut buf = Buffer::new(100, 1);
4155 buf.clear_dirty();
4156
4157 buf.mark_dirty_span(0, 10, 20);
4160 buf.mark_dirty_span(0, 21, 30);
4162
4163 let spans = buf.dirty_span_row(0).unwrap().spans();
4164 assert_eq!(spans.len(), 1);
4165 assert_eq!(spans[0], DirtySpan::new(10, 30));
4166 }
4167
4168 #[test]
4169 fn dirty_span_no_merge_large_gap() {
4170 let mut buf = Buffer::new(100, 1);
4171 buf.clear_dirty();
4172
4173 buf.mark_dirty_span(0, 10, 20);
4175 buf.mark_dirty_span(0, 22, 30);
4177
4178 let spans = buf.dirty_span_row(0).unwrap().spans();
4179 assert_eq!(spans.len(), 2);
4180 assert_eq!(spans[0], DirtySpan::new(10, 20));
4181 assert_eq!(spans[1], DirtySpan::new(22, 30));
4182 }
4183
4184 #[test]
4185 fn dirty_span_overflow_to_full() {
4186 let mut buf = Buffer::new(1000, 1);
4187 buf.clear_dirty();
4188
4189 for i in 0..DIRTY_SPAN_MAX_SPANS_PER_ROW + 10 {
4191 let start = (i * 4) as u16;
4192 buf.mark_dirty_span(0, start, start + 1);
4193 }
4194
4195 let row = buf.dirty_span_row(0).unwrap();
4196 assert!(row.is_full(), "Row should overflow to full scan");
4197 assert!(
4198 row.spans().is_empty(),
4199 "Spans should be cleared on overflow"
4200 );
4201 }
4202
4203 #[test]
4204 fn dirty_span_bounds_clamping() {
4205 let mut buf = Buffer::new(10, 1);
4206 buf.clear_dirty();
4207
4208 buf.mark_dirty_span(0, 15, 20);
4210 let spans = buf.dirty_span_row(0).unwrap().spans();
4211 assert!(spans.is_empty());
4212
4213 buf.mark_dirty_span(0, 8, 15);
4215 let spans = buf.dirty_span_row(0).unwrap().spans();
4216 assert_eq!(spans.len(), 1);
4217 assert_eq!(spans[0], DirtySpan::new(8, 10)); }
4219
4220 #[test]
4221 fn dirty_span_guard_band_clamps_bounds() {
4222 let mut buf = Buffer::new(10, 1);
4223 buf.clear_dirty();
4224 buf.set_dirty_span_config(DirtySpanConfig::default().with_guard_band(5));
4225
4226 buf.mark_dirty_span(0, 2, 3);
4227 let spans = buf.dirty_span_row(0).unwrap().spans();
4228 assert_eq!(spans.len(), 1);
4229 assert_eq!(spans[0], DirtySpan::new(0, 8));
4230
4231 buf.clear_dirty();
4232 buf.mark_dirty_span(0, 8, 10);
4233 let spans = buf.dirty_span_row(0).unwrap().spans();
4234 assert_eq!(spans.len(), 1);
4235 assert_eq!(spans[0], DirtySpan::new(3, 10));
4236 }
4237
4238 #[test]
4239 fn dirty_span_empty_span_is_ignored() {
4240 let mut buf = Buffer::new(10, 1);
4241 buf.clear_dirty();
4242 buf.mark_dirty_span(0, 5, 5);
4243 let spans = buf.dirty_span_row(0).unwrap().spans();
4244 assert!(spans.is_empty());
4245 }
4246
4247 #[test]
4248 fn buffer_fill_wide_char_clipping() {
4249 let mut buf = Buffer::new(10, 5);
4253 let wide_cell = Cell::from_char('🦀'); buf.fill(Rect::new(0, 0, 10, 5), wide_cell);
4257
4258 let head = buf.get(0, 0).unwrap();
4260 assert_eq!(head.content.as_char(), Some('🦀'));
4261 assert_eq!(head.content.width(), 2);
4262
4263 let tail = buf.get(1, 0).unwrap();
4264 assert!(tail.is_continuation());
4265
4266 buf.push_scissor(Rect::new(0, 0, 1, 5));
4269 let x_cell = Cell::from_char('X');
4271 buf.fill(Rect::new(0, 0, 10, 5), x_cell);
4272
4273 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('X'));
4275 assert!(buf.get(1, 0).unwrap().is_empty()); buf.pop_scissor();
4278 }
4279}