1#![forbid(unsafe_code)]
2
3use smallvec::SmallVec;
56
57use crate::budget::DegradationLevel;
58use crate::cell::Cell;
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 {
252 assert!(width > 0, "buffer width must be > 0");
253 assert!(height > 0, "buffer height must be > 0");
254
255 let size = width as usize * height as usize;
256 let cells = vec![Cell::default(); size];
257
258 let dirty_spans = (0..height)
259 .map(|_| DirtySpanRow::new_full())
260 .collect::<Vec<_>>();
261 let dirty_bits = vec![0u8; size];
262 let dirty_cells = size;
263 let dirty_all = true;
264
265 Self {
266 width,
267 height,
268 cells,
269 scissor_stack: vec![Rect::from_size(width, height)],
270 opacity_stack: vec![1.0],
271 degradation: DegradationLevel::Full,
272 dirty_rows: vec![true; height as usize],
275 dirty_spans,
277 dirty_span_config: DirtySpanConfig::default(),
278 dirty_span_overflows: 0,
279 dirty_bits,
280 dirty_cells,
281 dirty_all,
282 }
283 }
284
285 #[inline]
287 pub const fn width(&self) -> u16 {
288 self.width
289 }
290
291 #[inline]
293 pub const fn height(&self) -> u16 {
294 self.height
295 }
296
297 #[inline]
299 pub fn len(&self) -> usize {
300 self.cells.len()
301 }
302
303 #[inline]
305 pub fn is_empty(&self) -> bool {
306 self.cells.is_empty()
307 }
308
309 #[inline]
311 pub const fn bounds(&self) -> Rect {
312 Rect::from_size(self.width, self.height)
313 }
314
315 #[inline]
320 pub fn content_height(&self) -> u16 {
321 let default_cell = Cell::default();
322 let width = self.width as usize;
323 for y in (0..self.height).rev() {
324 let row_start = y as usize * width;
325 let row_end = row_start + width;
326 if self.cells[row_start..row_end]
327 .iter()
328 .any(|cell| *cell != default_cell)
329 {
330 return y + 1;
331 }
332 }
333 0
334 }
335
336 #[inline]
343 fn mark_dirty_row(&mut self, y: u16) {
344 if let Some(slot) = self.dirty_rows.get_mut(y as usize) {
345 *slot = true;
346 }
347 }
348
349 #[inline]
351 fn mark_dirty_bits_range(&mut self, y: u16, start: u16, end: u16) {
352 if self.dirty_all {
353 return;
354 }
355 if y >= self.height {
356 return;
357 }
358
359 let width = self.width;
360 if start >= width {
361 return;
362 }
363 let end = end.min(width);
364 if start >= end {
365 return;
366 }
367
368 let row_start = y as usize * width as usize;
369 let slice = &mut self.dirty_bits[row_start + start as usize..row_start + end as usize];
370 let newly_dirty = slice.iter().filter(|&&b| b == 0).count();
371 slice.fill(1);
372 self.dirty_cells = self.dirty_cells.saturating_add(newly_dirty);
373 }
374
375 #[inline]
377 fn mark_dirty_bits_row(&mut self, y: u16) {
378 self.mark_dirty_bits_range(y, 0, self.width);
379 }
380
381 #[inline]
383 fn mark_dirty_row_full(&mut self, y: u16) {
384 self.mark_dirty_row(y);
385 if self.dirty_span_config.enabled
386 && let Some(row) = self.dirty_spans.get_mut(y as usize)
387 {
388 row.set_full();
389 }
390 self.mark_dirty_bits_row(y);
391 }
392
393 #[inline]
395 fn mark_dirty_span(&mut self, y: u16, x0: u16, x1: u16) {
396 self.mark_dirty_row(y);
397 let width = self.width;
398 let (start, mut end) = if x0 <= x1 { (x0, x1) } else { (x1, x0) };
399 if start >= width {
400 return;
401 }
402 if end > width {
403 end = width;
404 }
405 if start >= end {
406 return;
407 }
408
409 self.mark_dirty_bits_range(y, start, end);
410
411 if !self.dirty_span_config.enabled {
412 return;
413 }
414
415 let guard_band = self.dirty_span_config.guard_band;
416 let span_start = start.saturating_sub(guard_band);
417 let mut span_end = end.saturating_add(guard_band);
418 if span_end > width {
419 span_end = width;
420 }
421 if span_start >= span_end {
422 return;
423 }
424
425 let Some(row) = self.dirty_spans.get_mut(y as usize) else {
426 return;
427 };
428
429 if row.is_full() {
430 return;
431 }
432
433 let new_span = DirtySpan::new(span_start, span_end);
434 let spans = &mut row.spans;
435 let insert_at = spans.partition_point(|span| span.x0 <= new_span.x0);
436 spans.insert(insert_at, new_span);
437
438 let merge_gap = self.dirty_span_config.merge_gap;
440 let mut i = if insert_at > 0 { insert_at - 1 } else { 0 };
441 while i + 1 < spans.len() {
442 let current = spans[i];
443 let next = spans[i + 1];
444 let merge_limit = current.x1.saturating_add(merge_gap);
445 if merge_limit >= next.x0 {
446 spans[i].x1 = current.x1.max(next.x1);
447 spans.remove(i + 1);
448 continue;
449 }
450 i += 1;
451 }
452
453 if spans.len() > self.dirty_span_config.max_spans_per_row {
454 row.set_full();
455 self.dirty_span_overflows = self.dirty_span_overflows.saturating_add(1);
456 }
457 }
458
459 #[inline]
461 pub fn mark_all_dirty(&mut self) {
462 self.dirty_rows.fill(true);
463 if self.dirty_span_config.enabled {
464 for row in &mut self.dirty_spans {
465 row.set_full();
466 }
467 } else {
468 for row in &mut self.dirty_spans {
469 row.clear();
470 }
471 }
472 self.dirty_all = true;
473 self.dirty_cells = self.cells.len();
474 }
475
476 #[inline]
480 pub fn clear_dirty(&mut self) {
481 self.dirty_rows.fill(false);
482 for row in &mut self.dirty_spans {
483 row.clear();
484 }
485 self.dirty_span_overflows = 0;
486 self.dirty_bits.fill(0);
487 self.dirty_cells = 0;
488 self.dirty_all = false;
489 }
490
491 #[inline]
493 pub fn is_row_dirty(&self, y: u16) -> bool {
494 self.dirty_rows.get(y as usize).copied().unwrap_or(false)
495 }
496
497 #[inline]
502 pub fn dirty_rows(&self) -> &[bool] {
503 &self.dirty_rows
504 }
505
506 #[inline]
508 pub fn dirty_row_count(&self) -> usize {
509 self.dirty_rows.iter().filter(|&&d| d).count()
510 }
511
512 #[inline]
514 #[allow(dead_code)]
515 pub(crate) fn dirty_bits(&self) -> &[u8] {
516 &self.dirty_bits
517 }
518
519 #[inline]
521 #[allow(dead_code)]
522 pub(crate) fn dirty_cell_count(&self) -> usize {
523 self.dirty_cells
524 }
525
526 #[inline]
528 #[allow(dead_code)]
529 pub(crate) fn dirty_all(&self) -> bool {
530 self.dirty_all
531 }
532
533 #[inline]
535 #[allow(dead_code)]
536 pub(crate) fn dirty_span_row(&self, y: u16) -> Option<&DirtySpanRow> {
537 if !self.dirty_span_config.enabled {
538 return None;
539 }
540 self.dirty_spans.get(y as usize)
541 }
542
543 pub fn dirty_span_stats(&self) -> DirtySpanStats {
545 if !self.dirty_span_config.enabled {
546 return DirtySpanStats {
547 rows_full_dirty: 0,
548 rows_with_spans: 0,
549 total_spans: 0,
550 overflows: 0,
551 span_coverage_cells: 0,
552 max_span_len: 0,
553 max_spans_per_row: self.dirty_span_config.max_spans_per_row,
554 };
555 }
556
557 let mut rows_full_dirty = 0usize;
558 let mut rows_with_spans = 0usize;
559 let mut total_spans = 0usize;
560 let mut span_coverage_cells = 0usize;
561 let mut max_span_len = 0usize;
562
563 for row in &self.dirty_spans {
564 if row.is_full() {
565 rows_full_dirty += 1;
566 span_coverage_cells += self.width as usize;
567 max_span_len = max_span_len.max(self.width as usize);
568 continue;
569 }
570 if !row.spans().is_empty() {
571 rows_with_spans += 1;
572 }
573 total_spans += row.spans().len();
574 for span in row.spans() {
575 span_coverage_cells += span.len();
576 max_span_len = max_span_len.max(span.len());
577 }
578 }
579
580 DirtySpanStats {
581 rows_full_dirty,
582 rows_with_spans,
583 total_spans,
584 overflows: self.dirty_span_overflows,
585 span_coverage_cells,
586 max_span_len,
587 max_spans_per_row: self.dirty_span_config.max_spans_per_row,
588 }
589 }
590
591 #[inline]
593 pub fn dirty_span_config(&self) -> DirtySpanConfig {
594 self.dirty_span_config
595 }
596
597 pub fn set_dirty_span_config(&mut self, config: DirtySpanConfig) {
599 if self.dirty_span_config == config {
600 return;
601 }
602 self.dirty_span_config = config;
603 for row in &mut self.dirty_spans {
604 row.clear();
605 }
606 self.dirty_span_overflows = 0;
607 }
608
609 #[inline]
615 fn index(&self, x: u16, y: u16) -> Option<usize> {
616 if x < self.width && y < self.height {
617 Some(y as usize * self.width as usize + x as usize)
618 } else {
619 None
620 }
621 }
622
623 #[inline]
629 fn index_unchecked(&self, x: u16, y: u16) -> usize {
630 debug_assert!(x < self.width && y < self.height);
631 y as usize * self.width as usize + x as usize
632 }
633
634 #[inline]
638 #[must_use]
639 pub fn get(&self, x: u16, y: u16) -> Option<&Cell> {
640 self.index(x, y).map(|i| &self.cells[i])
641 }
642
643 #[inline]
648 #[must_use]
649 pub fn get_mut(&mut self, x: u16, y: u16) -> Option<&mut Cell> {
650 let idx = self.index(x, y)?;
651 self.mark_dirty_span(y, x, x.saturating_add(1));
652 Some(&mut self.cells[idx])
653 }
654
655 #[inline]
662 pub fn get_unchecked(&self, x: u16, y: u16) -> &Cell {
663 let i = self.index_unchecked(x, y);
664 &self.cells[i]
665 }
666
667 #[inline]
671 fn cleanup_overlap(&mut self, x: u16, y: u16, new_cell: &Cell) -> Option<DirtySpan> {
672 let idx = self.index(x, y)?;
673 let current = self.cells[idx];
674 let mut touched = false;
675 let mut min_x = x;
676 let mut max_x = x;
677
678 if current.content.width() > 1 {
680 let width = current.content.width();
681 for i in 1..width {
686 let Some(cx) = x.checked_add(i as u16) else {
687 break;
688 };
689 if let Some(tail_idx) = self.index(cx, y)
690 && self.cells[tail_idx].is_continuation()
691 {
692 self.cells[tail_idx] = Cell::default();
693 touched = true;
694 min_x = min_x.min(cx);
695 max_x = max_x.max(cx);
696 }
697 }
698 }
699 else if current.is_continuation() && !new_cell.is_continuation() {
701 let mut back_x = x;
702 let limit = x.saturating_sub(127);
705
706 while back_x > limit {
707 back_x -= 1;
708 if let Some(h_idx) = self.index(back_x, y) {
709 let h_cell = self.cells[h_idx];
710 if !h_cell.is_continuation() {
711 let width = h_cell.content.width();
713 if (back_x as usize + width) > x as usize {
714 self.cells[h_idx] = Cell::default();
717 touched = true;
718 min_x = min_x.min(back_x);
719 max_x = max_x.max(back_x);
720
721 for i in 1..width {
724 let Some(cx) = back_x.checked_add(i as u16) else {
725 break;
726 };
727 if let Some(tail_idx) = self.index(cx, y) {
728 if self.cells[tail_idx].is_continuation() {
731 self.cells[tail_idx] = Cell::default();
732 touched = true;
733 min_x = min_x.min(cx);
734 max_x = max_x.max(cx);
735 }
736 }
737 }
738 }
739 break;
740 }
741 }
742 }
743 }
744
745 if touched {
746 Some(DirtySpan::new(min_x, max_x.saturating_add(1)))
747 } else {
748 None
749 }
750 }
751
752 #[inline]
759 fn cleanup_orphaned_tails(&mut self, start_x: u16, y: u16) {
760 if start_x >= self.width {
761 return;
762 }
763
764 let Some(idx) = self.index(start_x, y) else {
766 return;
767 };
768 if !self.cells[idx].is_continuation() {
769 return;
770 }
771
772 let mut x = start_x;
774 let mut max_x = x;
775 let row_end_idx = (y as usize * self.width as usize) + self.width as usize;
776 let mut curr_idx = idx;
777
778 while curr_idx < row_end_idx && self.cells[curr_idx].is_continuation() {
779 self.cells[curr_idx] = Cell::default();
780 max_x = x;
781 x = x.saturating_add(1);
782 curr_idx += 1;
783 }
784
785 self.mark_dirty_span(y, start_x, max_x.saturating_add(1));
787 }
788
789 #[inline]
804 pub fn set_fast(&mut self, x: u16, y: u16, cell: Cell) {
805 let bg_a = cell.bg.a();
812 if cell.content.width() > 1 || cell.is_continuation() || (bg_a != 255 && bg_a != 0) {
813 return self.set(x, y, cell);
814 }
815
816 if self.scissor_stack.len() != 1 || self.opacity_stack.len() != 1 {
818 return self.set(x, y, cell);
819 }
820
821 let Some(idx) = self.index(x, y) else {
823 return;
824 };
825
826 let existing = self.cells[idx];
830 if existing.content.width() > 1 || existing.is_continuation() {
831 return self.set(x, y, cell);
832 }
833
834 let mut final_cell = cell;
840 if bg_a == 0 {
841 final_cell.bg = existing.bg;
842 }
843
844 self.cells[idx] = final_cell;
845 self.mark_dirty_span(y, x, x.saturating_add(1));
846 }
847
848 #[inline]
860 pub fn set(&mut self, x: u16, y: u16, cell: Cell) {
861 let width = cell.content.width();
862
863 if width <= 1 {
865 let Some(idx) = self.index(x, y) else {
867 return;
868 };
869
870 if !self.current_scissor().contains(x, y) {
872 return;
873 }
874
875 let mut span_start = x;
877 let mut span_end = x.saturating_add(1);
878 if let Some(span) = self.cleanup_overlap(x, y, &cell) {
879 span_start = span_start.min(span.x0);
880 span_end = span_end.max(span.x1);
881 }
882
883 let existing_bg = self.cells[idx].bg;
884
885 let mut final_cell = if self.current_opacity() < 1.0 {
887 let opacity = self.current_opacity();
888 Cell {
889 fg: cell.fg.with_opacity(opacity),
890 bg: cell.bg.with_opacity(opacity),
891 ..cell
892 }
893 } else {
894 cell
895 };
896
897 final_cell.bg = final_cell.bg.over(existing_bg);
898
899 self.cells[idx] = final_cell;
900 self.mark_dirty_span(y, span_start, span_end);
901 self.cleanup_orphaned_tails(x.saturating_add(1), y);
902 return;
903 }
904
905 let scissor = self.current_scissor();
908 for i in 0..width {
909 let Some(cx) = x.checked_add(i as u16) else {
910 return;
911 };
912 if cx >= self.width || y >= self.height {
914 return;
915 }
916 if !scissor.contains(cx, y) {
918 return;
919 }
920 }
921
922 let mut span_start = x;
926 let mut span_end = x.saturating_add(width as u16);
927 if let Some(span) = self.cleanup_overlap(x, y, &cell) {
928 span_start = span_start.min(span.x0);
929 span_end = span_end.max(span.x1);
930 }
931 for i in 1..width {
932 if let Some(span) = self.cleanup_overlap(x + i as u16, y, &Cell::CONTINUATION) {
934 span_start = span_start.min(span.x0);
935 span_end = span_end.max(span.x1);
936 }
937 }
938
939 let idx = self.index_unchecked(x, y);
941 let old_cell = self.cells[idx];
942 let mut final_cell = if self.current_opacity() < 1.0 {
943 let opacity = self.current_opacity();
944 Cell {
945 fg: cell.fg.with_opacity(opacity),
946 bg: cell.bg.with_opacity(opacity),
947 ..cell
948 }
949 } else {
950 cell
951 };
952
953 final_cell.bg = final_cell.bg.over(old_cell.bg);
955
956 self.cells[idx] = final_cell;
957
958 for i in 1..width {
961 let idx = self.index_unchecked(x + i as u16, y);
962 self.cells[idx] = Cell::CONTINUATION;
963 }
964 self.mark_dirty_span(y, span_start, span_end);
965 self.cleanup_orphaned_tails(x.saturating_add(width as u16), y);
966 }
967
968 #[inline]
973 pub fn set_raw(&mut self, x: u16, y: u16, cell: Cell) {
974 if let Some(idx) = self.index(x, y) {
975 self.cells[idx] = cell;
976 self.mark_dirty_span(y, x, x.saturating_add(1));
977 }
978 }
979
980 #[inline]
984 pub fn fill(&mut self, rect: Rect, cell: Cell) {
985 let clipped = self.current_scissor().intersection(&rect);
986 if clipped.is_empty() {
987 return;
988 }
989
990 let cell_width = cell.content.width();
993 if cell_width <= 1
994 && !cell.is_continuation()
995 && self.current_opacity() >= 1.0
996 && cell.bg.a() == 255
997 && clipped.x == 0
998 && clipped.width == self.width
999 {
1000 let row_width = self.width as usize;
1001 for y in clipped.y..clipped.bottom() {
1002 let row_start = y as usize * row_width;
1003 let row_end = row_start + row_width;
1004 self.cells[row_start..row_end].fill(cell);
1005 self.mark_dirty_row_full(y);
1006 }
1007 return;
1008 }
1009
1010 if cell_width <= 1
1014 && !cell.is_continuation()
1015 && self.current_opacity() >= 1.0
1016 && cell.bg.a() == 255
1017 && self.scissor_stack.len() == 1
1018 {
1019 let row_width = self.width as usize;
1020 let x_start = clipped.x as usize;
1021 let x_end = clipped.right() as usize;
1022 for y in clipped.y..clipped.bottom() {
1023 let row_start = y as usize * row_width;
1024 let mut dirty_left = clipped.x;
1025 let mut dirty_right = clipped.right();
1026
1027 if x_start > 0 && self.cells[row_start + x_start].is_continuation() {
1030 for hx in (0..x_start).rev() {
1031 let c = self.cells[row_start + hx];
1032 if c.is_continuation() {
1033 self.cells[row_start + hx] = Cell::default();
1034 dirty_left = hx as u16;
1035 } else {
1036 if c.content.width() > 1 {
1037 self.cells[row_start + hx] = Cell::default();
1038 dirty_left = hx as u16;
1039 }
1040 break;
1041 }
1042 }
1043 }
1044
1045 {
1048 let mut cx = x_end;
1049 while cx < row_width && self.cells[row_start + cx].is_continuation() {
1050 self.cells[row_start + cx] = Cell::default();
1051 dirty_right = (cx as u16).saturating_add(1);
1052 cx += 1;
1053 }
1054 }
1055
1056 self.cells[row_start + x_start..row_start + x_end].fill(cell);
1057 self.mark_dirty_span(y, dirty_left, dirty_right);
1058 }
1059 return;
1060 }
1061
1062 for y in clipped.y..clipped.bottom() {
1063 for x in clipped.x..clipped.right() {
1064 self.set(x, y, cell);
1065 }
1066 }
1067 }
1068
1069 #[inline]
1071 pub fn clear(&mut self) {
1072 self.cells.fill(Cell::default());
1073 self.mark_all_dirty();
1074 }
1075
1076 pub fn reset_for_frame(&mut self) {
1081 self.scissor_stack.truncate(1);
1082 if let Some(base) = self.scissor_stack.first_mut() {
1083 *base = Rect::from_size(self.width, self.height);
1084 } else {
1085 self.scissor_stack
1086 .push(Rect::from_size(self.width, self.height));
1087 }
1088
1089 self.opacity_stack.truncate(1);
1090 if let Some(base) = self.opacity_stack.first_mut() {
1091 *base = 1.0;
1092 } else {
1093 self.opacity_stack.push(1.0);
1094 }
1095
1096 self.clear();
1097 }
1098
1099 #[inline]
1101 pub fn clear_with(&mut self, cell: Cell) {
1102 self.cells.fill(cell);
1103 self.mark_all_dirty();
1104 }
1105
1106 #[inline]
1110 pub fn cells(&self) -> &[Cell] {
1111 &self.cells
1112 }
1113
1114 #[inline]
1118 pub fn cells_mut(&mut self) -> &mut [Cell] {
1119 self.mark_all_dirty();
1120 &mut self.cells
1121 }
1122
1123 #[inline]
1129 pub fn row_cells(&self, y: u16) -> &[Cell] {
1130 let start = y as usize * self.width as usize;
1131 &self.cells[start..start + self.width as usize]
1132 }
1133
1134 #[inline]
1141 pub fn push_scissor(&mut self, rect: Rect) {
1142 let current = self.current_scissor();
1143 let intersected = current.intersection(&rect);
1144 self.scissor_stack.push(intersected);
1145 }
1146
1147 #[inline]
1151 pub fn pop_scissor(&mut self) {
1152 if self.scissor_stack.len() > 1 {
1153 self.scissor_stack.pop();
1154 }
1155 }
1156
1157 #[inline]
1159 pub fn current_scissor(&self) -> Rect {
1160 *self
1161 .scissor_stack
1162 .last()
1163 .expect("scissor stack always has at least one element")
1164 }
1165
1166 #[inline]
1168 pub fn scissor_depth(&self) -> usize {
1169 self.scissor_stack.len()
1170 }
1171
1172 #[inline]
1179 pub fn push_opacity(&mut self, opacity: f32) {
1180 let clamped = opacity.clamp(0.0, 1.0);
1181 let current = self.current_opacity();
1182 self.opacity_stack.push(current * clamped);
1183 }
1184
1185 #[inline]
1189 pub fn pop_opacity(&mut self) {
1190 if self.opacity_stack.len() > 1 {
1191 self.opacity_stack.pop();
1192 }
1193 }
1194
1195 #[inline]
1197 pub fn current_opacity(&self) -> f32 {
1198 *self
1199 .opacity_stack
1200 .last()
1201 .expect("opacity stack always has at least one element")
1202 }
1203
1204 #[inline]
1206 pub fn opacity_depth(&self) -> usize {
1207 self.opacity_stack.len()
1208 }
1209
1210 pub fn copy_from(&mut self, src: &Buffer, src_rect: Rect, dst_x: u16, dst_y: u16) {
1217 let copy_bounds = Rect::new(dst_x, dst_y, src_rect.width, src_rect.height);
1220 self.push_scissor(copy_bounds);
1221
1222 for dy in 0..src_rect.height {
1223 let Some(target_y) = dst_y.checked_add(dy) else {
1225 continue;
1226 };
1227 let Some(sy) = src_rect.y.checked_add(dy) else {
1228 continue;
1229 };
1230
1231 let mut dx = 0u16;
1232 while dx < src_rect.width {
1233 let Some(target_x) = dst_x.checked_add(dx) else {
1235 dx = dx.saturating_add(1);
1236 continue;
1237 };
1238 let Some(sx) = src_rect.x.checked_add(dx) else {
1239 dx = dx.saturating_add(1);
1240 continue;
1241 };
1242
1243 if let Some(cell) = src.get(sx, sy) {
1244 if cell.is_continuation() {
1248 self.set(target_x, target_y, Cell::default());
1249 dx = dx.saturating_add(1);
1250 continue;
1251 }
1252
1253 let width = cell.content.width();
1254
1255 if width > 1 && dx.saturating_add(width as u16) > src_rect.width {
1259 self.set(target_x, target_y, Cell::default());
1260 } else {
1261 self.set(target_x, target_y, *cell);
1262 }
1263
1264 if width > 1 {
1266 dx = dx.saturating_add(width as u16);
1267 } else {
1268 dx = dx.saturating_add(1);
1269 }
1270 } else {
1271 dx = dx.saturating_add(1);
1272 }
1273 }
1274 }
1275
1276 self.pop_scissor();
1277 }
1278
1279 pub fn content_eq(&self, other: &Buffer) -> bool {
1281 self.width == other.width && self.height == other.height && self.cells == other.cells
1282 }
1283}
1284
1285impl Default for Buffer {
1286 fn default() -> Self {
1288 Self::new(1, 1)
1289 }
1290}
1291
1292impl PartialEq for Buffer {
1293 fn eq(&self, other: &Self) -> bool {
1294 self.content_eq(other)
1295 }
1296}
1297
1298impl Eq for Buffer {}
1299
1300#[derive(Debug)]
1319pub struct DoubleBuffer {
1320 buffers: [Buffer; 2],
1321 current_idx: u8,
1323}
1324
1325const ADAPTIVE_GROWTH_FACTOR: f32 = 1.25;
1331
1332const ADAPTIVE_SHRINK_THRESHOLD: f32 = 0.50;
1335
1336const ADAPTIVE_MAX_OVERAGE: u16 = 200;
1338
1339#[derive(Debug)]
1371pub struct AdaptiveDoubleBuffer {
1372 inner: DoubleBuffer,
1374 logical_width: u16,
1376 logical_height: u16,
1378 capacity_width: u16,
1380 capacity_height: u16,
1382 stats: AdaptiveStats,
1384}
1385
1386#[derive(Debug, Clone, Default)]
1388pub struct AdaptiveStats {
1389 pub resize_avoided: u64,
1391 pub resize_reallocated: u64,
1393 pub resize_growth: u64,
1395 pub resize_shrink: u64,
1397}
1398
1399impl AdaptiveStats {
1400 pub fn reset(&mut self) {
1402 *self = Self::default();
1403 }
1404
1405 pub fn avoidance_ratio(&self) -> f64 {
1407 let total = self.resize_avoided + self.resize_reallocated;
1408 if total == 0 {
1409 1.0
1410 } else {
1411 self.resize_avoided as f64 / total as f64
1412 }
1413 }
1414}
1415
1416impl DoubleBuffer {
1417 pub fn new(width: u16, height: u16) -> Self {
1425 Self {
1426 buffers: [Buffer::new(width, height), Buffer::new(width, height)],
1427 current_idx: 0,
1428 }
1429 }
1430
1431 #[inline]
1436 pub fn swap(&mut self) {
1437 self.current_idx = 1 - self.current_idx;
1438 }
1439
1440 #[inline]
1442 pub fn current(&self) -> &Buffer {
1443 &self.buffers[self.current_idx as usize]
1444 }
1445
1446 #[inline]
1448 pub fn current_mut(&mut self) -> &mut Buffer {
1449 &mut self.buffers[self.current_idx as usize]
1450 }
1451
1452 #[inline]
1454 pub fn previous(&self) -> &Buffer {
1455 &self.buffers[(1 - self.current_idx) as usize]
1456 }
1457
1458 #[inline]
1460 pub fn previous_mut(&mut self) -> &mut Buffer {
1461 &mut self.buffers[(1 - self.current_idx) as usize]
1462 }
1463
1464 #[inline]
1466 pub fn width(&self) -> u16 {
1467 self.buffers[0].width()
1468 }
1469
1470 #[inline]
1472 pub fn height(&self) -> u16 {
1473 self.buffers[0].height()
1474 }
1475
1476 pub fn resize(&mut self, width: u16, height: u16) -> bool {
1481 if self.buffers[0].width() == width && self.buffers[0].height() == height {
1482 return false;
1483 }
1484 self.buffers = [Buffer::new(width, height), Buffer::new(width, height)];
1485 self.current_idx = 0;
1486 true
1487 }
1488
1489 #[inline]
1491 pub fn dimensions_match(&self, width: u16, height: u16) -> bool {
1492 self.buffers[0].width() == width && self.buffers[0].height() == height
1493 }
1494}
1495
1496impl AdaptiveDoubleBuffer {
1501 pub fn new(width: u16, height: u16) -> Self {
1509 let (cap_w, cap_h) = Self::compute_capacity(width, height);
1510 Self {
1511 inner: DoubleBuffer::new(cap_w, cap_h),
1512 logical_width: width,
1513 logical_height: height,
1514 capacity_width: cap_w,
1515 capacity_height: cap_h,
1516 stats: AdaptiveStats::default(),
1517 }
1518 }
1519
1520 fn compute_capacity(width: u16, height: u16) -> (u16, u16) {
1524 let extra_w =
1525 ((width as f32 * (ADAPTIVE_GROWTH_FACTOR - 1.0)) as u16).min(ADAPTIVE_MAX_OVERAGE);
1526 let extra_h =
1527 ((height as f32 * (ADAPTIVE_GROWTH_FACTOR - 1.0)) as u16).min(ADAPTIVE_MAX_OVERAGE);
1528
1529 let cap_w = width.saturating_add(extra_w);
1530 let cap_h = height.saturating_add(extra_h);
1531
1532 (cap_w, cap_h)
1533 }
1534
1535 fn needs_reallocation(&self, width: u16, height: u16) -> bool {
1539 if width > self.capacity_width || height > self.capacity_height {
1541 return true;
1542 }
1543
1544 let shrink_threshold_w = (self.capacity_width as f32 * ADAPTIVE_SHRINK_THRESHOLD) as u16;
1546 let shrink_threshold_h = (self.capacity_height as f32 * ADAPTIVE_SHRINK_THRESHOLD) as u16;
1547
1548 width < shrink_threshold_w || height < shrink_threshold_h
1549 }
1550
1551 #[inline]
1556 pub fn swap(&mut self) {
1557 self.inner.swap();
1558 }
1559
1560 #[inline]
1565 pub fn current(&self) -> &Buffer {
1566 self.inner.current()
1567 }
1568
1569 #[inline]
1571 pub fn current_mut(&mut self) -> &mut Buffer {
1572 self.inner.current_mut()
1573 }
1574
1575 #[inline]
1577 pub fn previous(&self) -> &Buffer {
1578 self.inner.previous()
1579 }
1580
1581 #[inline]
1583 pub fn width(&self) -> u16 {
1584 self.logical_width
1585 }
1586
1587 #[inline]
1589 pub fn height(&self) -> u16 {
1590 self.logical_height
1591 }
1592
1593 #[inline]
1595 pub fn capacity_width(&self) -> u16 {
1596 self.capacity_width
1597 }
1598
1599 #[inline]
1601 pub fn capacity_height(&self) -> u16 {
1602 self.capacity_height
1603 }
1604
1605 #[inline]
1607 pub fn stats(&self) -> &AdaptiveStats {
1608 &self.stats
1609 }
1610
1611 pub fn reset_stats(&mut self) {
1613 self.stats.reset();
1614 }
1615
1616 pub fn resize(&mut self, width: u16, height: u16) -> bool {
1628 if width == self.logical_width && height == self.logical_height {
1630 return false;
1631 }
1632
1633 let is_growth = width > self.logical_width || height > self.logical_height;
1634 if is_growth {
1635 self.stats.resize_growth += 1;
1636 } else {
1637 self.stats.resize_shrink += 1;
1638 }
1639
1640 if self.needs_reallocation(width, height) {
1641 let (cap_w, cap_h) = Self::compute_capacity(width, height);
1643 self.inner = DoubleBuffer::new(cap_w, cap_h);
1644 self.capacity_width = cap_w;
1645 self.capacity_height = cap_h;
1646 self.stats.resize_reallocated += 1;
1647 } else {
1648 self.inner.current_mut().clear();
1651 self.inner.previous_mut().clear();
1652 self.stats.resize_avoided += 1;
1653 }
1654
1655 self.logical_width = width;
1656 self.logical_height = height;
1657 true
1658 }
1659
1660 #[inline]
1662 pub fn dimensions_match(&self, width: u16, height: u16) -> bool {
1663 self.logical_width == width && self.logical_height == height
1664 }
1665
1666 #[inline]
1668 pub fn logical_bounds(&self) -> Rect {
1669 Rect::from_size(self.logical_width, self.logical_height)
1670 }
1671
1672 pub fn memory_efficiency(&self) -> f64 {
1674 let logical = self.logical_width as u64 * self.logical_height as u64;
1675 let capacity = self.capacity_width as u64 * self.capacity_height as u64;
1676 if capacity == 0 {
1677 1.0
1678 } else {
1679 logical as f64 / capacity as f64
1680 }
1681 }
1682}
1683
1684#[cfg(test)]
1685mod tests {
1686 use super::*;
1687 use crate::cell::PackedRgba;
1688
1689 #[test]
1690 fn set_composites_background() {
1691 let mut buf = Buffer::new(1, 1);
1692
1693 let red = PackedRgba::rgb(255, 0, 0);
1695 buf.set(0, 0, Cell::default().with_bg(red));
1696
1697 let cell = Cell::from_char('X'); buf.set(0, 0, cell);
1700
1701 let result = buf.get(0, 0).unwrap();
1702 assert_eq!(result.content.as_char(), Some('X'));
1703 assert_eq!(
1704 result.bg, red,
1705 "Background should be preserved (composited)"
1706 );
1707 }
1708
1709 #[test]
1710 fn set_fast_matches_set_for_transparent_bg() {
1711 let red = PackedRgba::rgb(255, 0, 0);
1712 let cell = Cell::from_char('X').with_fg(PackedRgba::rgb(0, 255, 0));
1713
1714 let mut a = Buffer::new(1, 1);
1715 a.set(0, 0, Cell::default().with_bg(red));
1716 a.set(0, 0, cell);
1717
1718 let mut b = Buffer::new(1, 1);
1719 b.set(0, 0, Cell::default().with_bg(red));
1720 b.set_fast(0, 0, cell);
1721
1722 assert_eq!(a.get(0, 0), b.get(0, 0));
1723 }
1724
1725 #[test]
1726 fn set_fast_matches_set_for_opaque_bg() {
1727 let cell = Cell::from_char('X')
1728 .with_fg(PackedRgba::rgb(0, 255, 0))
1729 .with_bg(PackedRgba::rgb(255, 0, 0));
1730
1731 let mut a = Buffer::new(1, 1);
1732 a.set(0, 0, cell);
1733
1734 let mut b = Buffer::new(1, 1);
1735 b.set_fast(0, 0, cell);
1736
1737 assert_eq!(a.get(0, 0), b.get(0, 0));
1738 }
1739
1740 #[test]
1741 fn rect_contains() {
1742 let r = Rect::new(5, 5, 10, 10);
1743 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)); }
1749
1750 #[test]
1751 fn rect_intersection() {
1752 let a = Rect::new(0, 0, 10, 10);
1753 let b = Rect::new(5, 5, 10, 10);
1754 let i = a.intersection(&b);
1755 assert_eq!(i, Rect::new(5, 5, 5, 5));
1756
1757 let c = Rect::new(20, 20, 5, 5);
1759 assert_eq!(a.intersection(&c), Rect::default());
1760 }
1761
1762 #[test]
1763 fn buffer_creation() {
1764 let buf = Buffer::new(80, 24);
1765 assert_eq!(buf.width(), 80);
1766 assert_eq!(buf.height(), 24);
1767 assert_eq!(buf.len(), 80 * 24);
1768 }
1769
1770 #[test]
1771 fn content_height_empty_is_zero() {
1772 let buf = Buffer::new(8, 4);
1773 assert_eq!(buf.content_height(), 0);
1774 }
1775
1776 #[test]
1777 fn content_height_tracks_last_non_empty_row() {
1778 let mut buf = Buffer::new(5, 4);
1779 buf.set(0, 0, Cell::from_char('A'));
1780 assert_eq!(buf.content_height(), 1);
1781
1782 buf.set(2, 3, Cell::from_char('Z'));
1783 assert_eq!(buf.content_height(), 4);
1784 }
1785
1786 #[test]
1787 #[should_panic(expected = "width must be > 0")]
1788 fn buffer_zero_width_panics() {
1789 Buffer::new(0, 24);
1790 }
1791
1792 #[test]
1793 #[should_panic(expected = "height must be > 0")]
1794 fn buffer_zero_height_panics() {
1795 Buffer::new(80, 0);
1796 }
1797
1798 #[test]
1799 fn buffer_get_and_set() {
1800 let mut buf = Buffer::new(10, 10);
1801 let cell = Cell::from_char('X');
1802 buf.set(5, 5, cell);
1803 assert_eq!(buf.get(5, 5).unwrap().content.as_char(), Some('X'));
1804 }
1805
1806 #[test]
1807 fn buffer_out_of_bounds_get() {
1808 let buf = Buffer::new(10, 10);
1809 assert!(buf.get(10, 0).is_none());
1810 assert!(buf.get(0, 10).is_none());
1811 assert!(buf.get(100, 100).is_none());
1812 }
1813
1814 #[test]
1815 fn buffer_out_of_bounds_set_ignored() {
1816 let mut buf = Buffer::new(10, 10);
1817 buf.set(100, 100, Cell::from_char('X')); assert_eq!(buf.cells().iter().filter(|c| !c.is_empty()).count(), 0);
1819 }
1820
1821 #[test]
1822 fn buffer_clear() {
1823 let mut buf = Buffer::new(10, 10);
1824 buf.set(5, 5, Cell::from_char('X'));
1825 buf.clear();
1826 assert!(buf.get(5, 5).unwrap().is_empty());
1827 }
1828
1829 #[test]
1830 fn scissor_stack_basic() {
1831 let mut buf = Buffer::new(20, 20);
1832
1833 assert_eq!(buf.current_scissor(), Rect::from_size(20, 20));
1835 assert_eq!(buf.scissor_depth(), 1);
1836
1837 buf.push_scissor(Rect::new(5, 5, 10, 10));
1839 assert_eq!(buf.current_scissor(), Rect::new(5, 5, 10, 10));
1840 assert_eq!(buf.scissor_depth(), 2);
1841
1842 buf.set(7, 7, Cell::from_char('I'));
1844 assert_eq!(buf.get(7, 7).unwrap().content.as_char(), Some('I'));
1845
1846 buf.set(0, 0, Cell::from_char('O'));
1848 assert!(buf.get(0, 0).unwrap().is_empty());
1849
1850 buf.pop_scissor();
1852 assert_eq!(buf.current_scissor(), Rect::from_size(20, 20));
1853 assert_eq!(buf.scissor_depth(), 1);
1854
1855 buf.set(0, 0, Cell::from_char('N'));
1857 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('N'));
1858 }
1859
1860 #[test]
1861 fn scissor_intersection() {
1862 let mut buf = Buffer::new(20, 20);
1863 buf.push_scissor(Rect::new(5, 5, 10, 10));
1864 buf.push_scissor(Rect::new(8, 8, 10, 10));
1865
1866 assert_eq!(buf.current_scissor(), Rect::new(8, 8, 7, 7));
1869 }
1870
1871 #[test]
1872 fn scissor_base_cannot_be_popped() {
1873 let mut buf = Buffer::new(10, 10);
1874 buf.pop_scissor(); assert_eq!(buf.scissor_depth(), 1);
1876 buf.pop_scissor(); assert_eq!(buf.scissor_depth(), 1);
1878 }
1879
1880 #[test]
1881 fn opacity_stack_basic() {
1882 let mut buf = Buffer::new(10, 10);
1883
1884 assert!((buf.current_opacity() - 1.0).abs() < f32::EPSILON);
1886 assert_eq!(buf.opacity_depth(), 1);
1887
1888 buf.push_opacity(0.5);
1890 assert!((buf.current_opacity() - 0.5).abs() < f32::EPSILON);
1891 assert_eq!(buf.opacity_depth(), 2);
1892
1893 buf.push_opacity(0.5);
1895 assert!((buf.current_opacity() - 0.25).abs() < f32::EPSILON);
1896 assert_eq!(buf.opacity_depth(), 3);
1897
1898 buf.pop_opacity();
1900 assert!((buf.current_opacity() - 0.5).abs() < f32::EPSILON);
1901 }
1902
1903 #[test]
1904 fn opacity_applied_to_cells() {
1905 let mut buf = Buffer::new(10, 10);
1906 buf.push_opacity(0.5);
1907
1908 let cell = Cell::from_char('X').with_fg(PackedRgba::rgba(100, 100, 100, 255));
1909 buf.set(5, 5, cell);
1910
1911 let stored = buf.get(5, 5).unwrap();
1912 assert_eq!(stored.fg.a(), 128);
1914 }
1915
1916 #[test]
1917 fn opacity_composites_background_before_storage() {
1918 let mut buf = Buffer::new(1, 1);
1919
1920 let red = PackedRgba::rgb(255, 0, 0);
1921 let blue = PackedRgba::rgb(0, 0, 255);
1922
1923 buf.set(0, 0, Cell::default().with_bg(red));
1924 buf.push_opacity(0.5);
1925 buf.set(0, 0, Cell::default().with_bg(blue));
1926
1927 let stored = buf.get(0, 0).unwrap();
1928 let expected = blue.with_opacity(0.5).over(red);
1929 assert_eq!(stored.bg, expected);
1930 }
1931
1932 #[test]
1933 fn opacity_clamped() {
1934 let mut buf = Buffer::new(10, 10);
1935 buf.push_opacity(2.0); assert!((buf.current_opacity() - 1.0).abs() < f32::EPSILON);
1937
1938 buf.push_opacity(-1.0); assert!((buf.current_opacity() - 0.0).abs() < f32::EPSILON);
1940 }
1941
1942 #[test]
1943 fn opacity_base_cannot_be_popped() {
1944 let mut buf = Buffer::new(10, 10);
1945 buf.pop_opacity(); assert_eq!(buf.opacity_depth(), 1);
1947 }
1948
1949 #[test]
1950 fn buffer_fill() {
1951 let mut buf = Buffer::new(10, 10);
1952 let cell = Cell::from_char('#');
1953 buf.fill(Rect::new(2, 2, 5, 5), cell);
1954
1955 assert_eq!(buf.get(3, 3).unwrap().content.as_char(), Some('#'));
1957
1958 assert!(buf.get(0, 0).unwrap().is_empty());
1960 }
1961
1962 #[test]
1963 fn buffer_fill_respects_scissor() {
1964 let mut buf = Buffer::new(10, 10);
1965 buf.push_scissor(Rect::new(3, 3, 4, 4));
1966
1967 let cell = Cell::from_char('#');
1968 buf.fill(Rect::new(0, 0, 10, 10), cell);
1969
1970 assert_eq!(buf.get(3, 3).unwrap().content.as_char(), Some('#'));
1972 assert!(buf.get(0, 0).unwrap().is_empty());
1973 assert!(buf.get(7, 7).unwrap().is_empty());
1974 }
1975
1976 #[test]
1977 fn buffer_copy_from() {
1978 let mut src = Buffer::new(10, 10);
1979 src.set(2, 2, Cell::from_char('S'));
1980
1981 let mut dst = Buffer::new(10, 10);
1982 dst.copy_from(&src, Rect::new(0, 0, 5, 5), 3, 3);
1983
1984 assert_eq!(dst.get(5, 5).unwrap().content.as_char(), Some('S'));
1986 }
1987
1988 #[test]
1989 fn copy_from_clips_wide_char_at_boundary() {
1990 let mut src = Buffer::new(10, 1);
1991 src.set(0, 0, Cell::from_char('中'));
1993
1994 let mut dst = Buffer::new(10, 1);
1995 dst.copy_from(&src, Rect::new(0, 0, 1, 1), 0, 0);
1998
1999 assert!(
2008 dst.get(0, 0).unwrap().is_empty(),
2009 "Wide char head should not be written if tail is clipped"
2010 );
2011 assert!(
2012 dst.get(1, 0).unwrap().is_empty(),
2013 "Wide char tail should not be leaked outside copy region"
2014 );
2015 }
2016
2017 #[test]
2018 fn buffer_content_eq() {
2019 let mut buf1 = Buffer::new(10, 10);
2020 let mut buf2 = Buffer::new(10, 10);
2021
2022 assert!(buf1.content_eq(&buf2));
2023
2024 buf1.set(0, 0, Cell::from_char('X'));
2025 assert!(!buf1.content_eq(&buf2));
2026
2027 buf2.set(0, 0, Cell::from_char('X'));
2028 assert!(buf1.content_eq(&buf2));
2029 }
2030
2031 #[test]
2032 fn buffer_bounds() {
2033 let buf = Buffer::new(80, 24);
2034 let bounds = buf.bounds();
2035 assert_eq!(bounds.x, 0);
2036 assert_eq!(bounds.y, 0);
2037 assert_eq!(bounds.width, 80);
2038 assert_eq!(bounds.height, 24);
2039 }
2040
2041 #[test]
2042 fn buffer_set_raw_bypasses_scissor() {
2043 let mut buf = Buffer::new(10, 10);
2044 buf.push_scissor(Rect::new(5, 5, 5, 5));
2045
2046 buf.set(0, 0, Cell::from_char('S'));
2048 assert!(buf.get(0, 0).unwrap().is_empty());
2049
2050 buf.set_raw(0, 0, Cell::from_char('R'));
2052 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('R'));
2053 }
2054
2055 #[test]
2056 fn set_handles_wide_chars() {
2057 let mut buf = Buffer::new(10, 10);
2058
2059 buf.set(0, 0, Cell::from_char('中'));
2061
2062 let head = buf.get(0, 0).unwrap();
2064 assert_eq!(head.content.as_char(), Some('中'));
2065
2066 let cont = buf.get(1, 0).unwrap();
2068 assert!(cont.is_continuation());
2069 assert!(!cont.is_empty());
2070 }
2071
2072 #[test]
2073 fn set_handles_wide_chars_clipped() {
2074 let mut buf = Buffer::new(10, 10);
2075 buf.push_scissor(Rect::new(0, 0, 1, 10)); buf.set(0, 0, Cell::from_char('中'));
2080
2081 assert!(buf.get(0, 0).unwrap().is_empty());
2083 assert!(buf.get(1, 0).unwrap().is_empty());
2085 }
2086
2087 #[test]
2090 fn overwrite_wide_head_with_single_clears_tails() {
2091 let mut buf = Buffer::new(10, 1);
2092
2093 buf.set(0, 0, Cell::from_char('中'));
2095 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2096 assert!(buf.get(1, 0).unwrap().is_continuation());
2097
2098 buf.set(0, 0, Cell::from_char('A'));
2100
2101 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('A'));
2103 assert!(
2105 buf.get(1, 0).unwrap().is_empty(),
2106 "Continuation at x=1 should be cleared when head is overwritten"
2107 );
2108 }
2109
2110 #[test]
2111 fn overwrite_continuation_with_single_clears_head_and_tails() {
2112 let mut buf = Buffer::new(10, 1);
2113
2114 buf.set(0, 0, Cell::from_char('中'));
2116 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2117 assert!(buf.get(1, 0).unwrap().is_continuation());
2118
2119 buf.set(1, 0, Cell::from_char('B'));
2121
2122 assert!(
2124 buf.get(0, 0).unwrap().is_empty(),
2125 "Head at x=0 should be cleared when its continuation is overwritten"
2126 );
2127 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('B'));
2129 }
2130
2131 #[test]
2132 fn overwrite_wide_with_another_wide() {
2133 let mut buf = Buffer::new(10, 1);
2134
2135 buf.set(0, 0, Cell::from_char('中'));
2137 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2138 assert!(buf.get(1, 0).unwrap().is_continuation());
2139
2140 buf.set(0, 0, Cell::from_char('日'));
2142
2143 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('日'));
2145 assert!(
2146 buf.get(1, 0).unwrap().is_continuation(),
2147 "Continuation should still exist for new wide char"
2148 );
2149 }
2150
2151 #[test]
2152 fn overwrite_continuation_middle_of_wide_sequence() {
2153 let mut buf = Buffer::new(10, 1);
2154
2155 buf.set(0, 0, Cell::from_char('中'));
2157 buf.set(2, 0, Cell::from_char('日'));
2158
2159 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2160 assert!(buf.get(1, 0).unwrap().is_continuation());
2161 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('日'));
2162 assert!(buf.get(3, 0).unwrap().is_continuation());
2163
2164 buf.set(1, 0, Cell::from_char('X'));
2166
2167 assert!(
2169 buf.get(0, 0).unwrap().is_empty(),
2170 "Head of first wide char should be cleared"
2171 );
2172 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('X'));
2174 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('日'));
2176 assert!(buf.get(3, 0).unwrap().is_continuation());
2177 }
2178
2179 #[test]
2180 fn wide_char_overlapping_previous_wide_char() {
2181 let mut buf = Buffer::new(10, 1);
2182
2183 buf.set(0, 0, Cell::from_char('中'));
2185 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2186 assert!(buf.get(1, 0).unwrap().is_continuation());
2187
2188 buf.set(1, 0, Cell::from_char('日'));
2190
2191 assert!(
2193 buf.get(0, 0).unwrap().is_empty(),
2194 "First wide char head should be cleared when continuation is overwritten by new wide"
2195 );
2196 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('日'));
2198 assert!(buf.get(2, 0).unwrap().is_continuation());
2199 }
2200
2201 #[test]
2202 fn wide_char_at_end_of_buffer_atomic_reject() {
2203 let mut buf = Buffer::new(5, 1);
2204
2205 buf.set(4, 0, Cell::from_char('中'));
2207
2208 assert!(
2210 buf.get(4, 0).unwrap().is_empty(),
2211 "Wide char should be rejected when tail would be out of bounds"
2212 );
2213 }
2214
2215 #[test]
2216 fn three_wide_chars_sequential_cleanup() {
2217 let mut buf = Buffer::new(10, 1);
2218
2219 buf.set(0, 0, Cell::from_char('一'));
2221 buf.set(2, 0, Cell::from_char('二'));
2222 buf.set(4, 0, Cell::from_char('三'));
2223
2224 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('一'));
2226 assert!(buf.get(1, 0).unwrap().is_continuation());
2227 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('二'));
2228 assert!(buf.get(3, 0).unwrap().is_continuation());
2229 assert_eq!(buf.get(4, 0).unwrap().content.as_char(), Some('三'));
2230 assert!(buf.get(5, 0).unwrap().is_continuation());
2231
2232 buf.set(3, 0, Cell::from_char('M'));
2234
2235 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('一'));
2237 assert!(buf.get(1, 0).unwrap().is_continuation());
2238 assert!(buf.get(2, 0).unwrap().is_empty());
2240 assert_eq!(buf.get(3, 0).unwrap().content.as_char(), Some('M'));
2242 assert_eq!(buf.get(4, 0).unwrap().content.as_char(), Some('三'));
2244 assert!(buf.get(5, 0).unwrap().is_continuation());
2245 }
2246
2247 #[test]
2248 fn overwrite_empty_cell_no_cleanup_needed() {
2249 let mut buf = Buffer::new(10, 1);
2250
2251 buf.set(5, 0, Cell::from_char('X'));
2253
2254 assert_eq!(buf.get(5, 0).unwrap().content.as_char(), Some('X'));
2255 assert!(buf.get(4, 0).unwrap().is_empty());
2257 assert!(buf.get(6, 0).unwrap().is_empty());
2258 }
2259
2260 #[test]
2261 fn wide_char_cleanup_with_opacity() {
2262 let mut buf = Buffer::new(10, 1);
2263
2264 buf.set(0, 0, Cell::default().with_bg(PackedRgba::rgb(255, 0, 0)));
2266 buf.set(1, 0, Cell::default().with_bg(PackedRgba::rgb(0, 255, 0)));
2267
2268 buf.set(0, 0, Cell::from_char('中'));
2270
2271 buf.push_opacity(0.5);
2273 buf.set(0, 0, Cell::from_char('A'));
2274 buf.pop_opacity();
2275
2276 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('A'));
2278 assert!(buf.get(1, 0).unwrap().is_empty());
2280 }
2281
2282 #[test]
2283 fn wide_char_continuation_not_treated_as_head() {
2284 let mut buf = Buffer::new(10, 1);
2285
2286 buf.set(0, 0, Cell::from_char('中'));
2288
2289 let cont = buf.get(1, 0).unwrap();
2291 assert!(cont.is_continuation());
2292 assert_eq!(cont.content.width(), 0);
2293
2294 buf.set(1, 0, Cell::from_char('日'));
2296
2297 assert!(buf.get(0, 0).unwrap().is_empty());
2299 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('日'));
2301 assert!(buf.get(2, 0).unwrap().is_continuation());
2302 }
2303
2304 #[test]
2305 fn wide_char_fill_region() {
2306 let mut buf = Buffer::new(10, 3);
2307
2308 let wide_cell = Cell::from_char('中');
2311 buf.fill(Rect::new(0, 0, 4, 2), wide_cell);
2312
2313 assert_eq!(buf.get(3, 0).unwrap().content.as_char(), Some('中'));
2335 }
2336
2337 #[test]
2338 fn default_buffer_dimensions() {
2339 let buf = Buffer::default();
2340 assert_eq!(buf.width(), 1);
2341 assert_eq!(buf.height(), 1);
2342 assert_eq!(buf.len(), 1);
2343 }
2344
2345 #[test]
2346 fn buffer_partial_eq_impl() {
2347 let buf1 = Buffer::new(5, 5);
2348 let buf2 = Buffer::new(5, 5);
2349 let mut buf3 = Buffer::new(5, 5);
2350 buf3.set(0, 0, Cell::from_char('X'));
2351
2352 assert_eq!(buf1, buf2);
2353 assert_ne!(buf1, buf3);
2354 }
2355
2356 #[test]
2357 fn degradation_level_accessible() {
2358 let mut buf = Buffer::new(10, 10);
2359 assert_eq!(buf.degradation, DegradationLevel::Full);
2360
2361 buf.degradation = DegradationLevel::SimpleBorders;
2362 assert_eq!(buf.degradation, DegradationLevel::SimpleBorders);
2363 }
2364
2365 #[test]
2368 fn get_mut_modifies_cell() {
2369 let mut buf = Buffer::new(10, 10);
2370 buf.set(3, 3, Cell::from_char('A'));
2371
2372 if let Some(cell) = buf.get_mut(3, 3) {
2373 *cell = Cell::from_char('B');
2374 }
2375
2376 assert_eq!(buf.get(3, 3).unwrap().content.as_char(), Some('B'));
2377 }
2378
2379 #[test]
2380 fn get_mut_out_of_bounds() {
2381 let mut buf = Buffer::new(5, 5);
2382 assert!(buf.get_mut(10, 10).is_none());
2383 }
2384
2385 #[test]
2388 fn clear_with_fills_all_cells() {
2389 let mut buf = Buffer::new(5, 3);
2390 let fill_cell = Cell::from_char('*');
2391 buf.clear_with(fill_cell);
2392
2393 for y in 0..3 {
2394 for x in 0..5 {
2395 assert_eq!(buf.get(x, y).unwrap().content.as_char(), Some('*'));
2396 }
2397 }
2398 }
2399
2400 #[test]
2403 fn cells_slice_has_correct_length() {
2404 let buf = Buffer::new(10, 5);
2405 assert_eq!(buf.cells().len(), 50);
2406 }
2407
2408 #[test]
2409 fn cells_mut_allows_direct_modification() {
2410 let mut buf = Buffer::new(3, 2);
2411 let cells = buf.cells_mut();
2412 cells[0] = Cell::from_char('Z');
2413
2414 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('Z'));
2415 }
2416
2417 #[test]
2420 fn row_cells_returns_correct_row() {
2421 let mut buf = Buffer::new(5, 3);
2422 buf.set(2, 1, Cell::from_char('R'));
2423
2424 let row = buf.row_cells(1);
2425 assert_eq!(row.len(), 5);
2426 assert_eq!(row[2].content.as_char(), Some('R'));
2427 }
2428
2429 #[test]
2430 #[should_panic]
2431 fn row_cells_out_of_bounds_panics() {
2432 let buf = Buffer::new(5, 3);
2433 let _ = buf.row_cells(5);
2434 }
2435
2436 #[test]
2439 fn buffer_is_not_empty() {
2440 let buf = Buffer::new(1, 1);
2441 assert!(!buf.is_empty());
2442 }
2443
2444 #[test]
2447 fn set_raw_out_of_bounds_is_safe() {
2448 let mut buf = Buffer::new(5, 5);
2449 buf.set_raw(100, 100, Cell::from_char('X'));
2450 }
2452
2453 #[test]
2456 fn copy_from_out_of_bounds_partial() {
2457 let mut src = Buffer::new(5, 5);
2458 src.set(0, 0, Cell::from_char('A'));
2459 src.set(4, 4, Cell::from_char('B'));
2460
2461 let mut dst = Buffer::new(5, 5);
2462 dst.copy_from(&src, Rect::new(0, 0, 5, 5), 3, 3);
2464
2465 assert_eq!(dst.get(3, 3).unwrap().content.as_char(), Some('A'));
2467 assert!(dst.get(4, 4).unwrap().is_empty());
2469 }
2470
2471 #[test]
2474 fn content_eq_different_dimensions() {
2475 let buf1 = Buffer::new(5, 5);
2476 let buf2 = Buffer::new(10, 10);
2477 assert!(!buf1.content_eq(&buf2));
2479 }
2480
2481 mod property {
2484 use super::*;
2485 use proptest::prelude::*;
2486
2487 proptest! {
2488 #[test]
2489 fn buffer_dimensions_are_preserved(width in 1u16..200, height in 1u16..200) {
2490 let buf = Buffer::new(width, height);
2491 prop_assert_eq!(buf.width(), width);
2492 prop_assert_eq!(buf.height(), height);
2493 prop_assert_eq!(buf.len(), width as usize * height as usize);
2494 }
2495
2496 #[test]
2497 fn buffer_get_in_bounds_always_succeeds(width in 1u16..100, height in 1u16..100) {
2498 let buf = Buffer::new(width, height);
2499 for x in 0..width {
2500 for y in 0..height {
2501 prop_assert!(buf.get(x, y).is_some(), "get({x},{y}) failed for {width}x{height} buffer");
2502 }
2503 }
2504 }
2505
2506 #[test]
2507 fn buffer_get_out_of_bounds_returns_none(width in 1u16..50, height in 1u16..50) {
2508 let buf = Buffer::new(width, height);
2509 prop_assert!(buf.get(width, 0).is_none());
2510 prop_assert!(buf.get(0, height).is_none());
2511 prop_assert!(buf.get(width, height).is_none());
2512 }
2513
2514 #[test]
2515 fn buffer_set_get_roundtrip(
2516 width in 5u16..50,
2517 height in 5u16..50,
2518 x in 0u16..5,
2519 y in 0u16..5,
2520 ch_idx in 0u32..26,
2521 ) {
2522 let x = x % width;
2523 let y = y % height;
2524 let ch = char::from_u32('A' as u32 + ch_idx).unwrap();
2525 let mut buf = Buffer::new(width, height);
2526 buf.set(x, y, Cell::from_char(ch));
2527 let got = buf.get(x, y).unwrap();
2528 prop_assert_eq!(got.content.as_char(), Some(ch));
2529 }
2530
2531 #[test]
2532 fn scissor_push_pop_stack_depth(
2533 width in 10u16..50,
2534 height in 10u16..50,
2535 push_count in 1usize..10,
2536 ) {
2537 let mut buf = Buffer::new(width, height);
2538 prop_assert_eq!(buf.scissor_depth(), 1); for i in 0..push_count {
2541 buf.push_scissor(Rect::new(0, 0, width, height));
2542 prop_assert_eq!(buf.scissor_depth(), i + 2);
2543 }
2544
2545 for i in (0..push_count).rev() {
2546 buf.pop_scissor();
2547 prop_assert_eq!(buf.scissor_depth(), i + 1);
2548 }
2549
2550 buf.pop_scissor();
2552 prop_assert_eq!(buf.scissor_depth(), 1);
2553 }
2554
2555 #[test]
2556 fn scissor_monotonic_intersection(
2557 width in 20u16..60,
2558 height in 20u16..60,
2559 ) {
2560 let mut buf = Buffer::new(width, height);
2562 let outer = Rect::new(2, 2, width - 4, height - 4);
2563 buf.push_scissor(outer);
2564 let s1 = buf.current_scissor();
2565
2566 let inner = Rect::new(5, 5, 10, 10);
2567 buf.push_scissor(inner);
2568 let s2 = buf.current_scissor();
2569
2570 prop_assert!(s2.width <= s1.width, "inner width {} > outer width {}", s2.width, s1.width);
2572 prop_assert!(s2.height <= s1.height, "inner height {} > outer height {}", s2.height, s1.height);
2573 }
2574
2575 #[test]
2576 fn opacity_push_pop_stack_depth(
2577 width in 5u16..20,
2578 height in 5u16..20,
2579 push_count in 1usize..10,
2580 ) {
2581 let mut buf = Buffer::new(width, height);
2582 prop_assert_eq!(buf.opacity_depth(), 1);
2583
2584 for i in 0..push_count {
2585 buf.push_opacity(0.9);
2586 prop_assert_eq!(buf.opacity_depth(), i + 2);
2587 }
2588
2589 for i in (0..push_count).rev() {
2590 buf.pop_opacity();
2591 prop_assert_eq!(buf.opacity_depth(), i + 1);
2592 }
2593
2594 buf.pop_opacity();
2595 prop_assert_eq!(buf.opacity_depth(), 1);
2596 }
2597
2598 #[test]
2599 fn opacity_multiplication_is_monotonic(
2600 opacity1 in 0.0f32..=1.0,
2601 opacity2 in 0.0f32..=1.0,
2602 ) {
2603 let mut buf = Buffer::new(5, 5);
2604 buf.push_opacity(opacity1);
2605 let after_first = buf.current_opacity();
2606 buf.push_opacity(opacity2);
2607 let after_second = buf.current_opacity();
2608
2609 prop_assert!(after_second <= after_first + f32::EPSILON,
2611 "opacity increased: {} -> {}", after_first, after_second);
2612 }
2613
2614 #[test]
2615 fn clear_resets_all_cells(width in 1u16..30, height in 1u16..30) {
2616 let mut buf = Buffer::new(width, height);
2617 for x in 0..width {
2619 buf.set_raw(x, 0, Cell::from_char('X'));
2620 }
2621 buf.clear();
2622 for y in 0..height {
2624 for x in 0..width {
2625 prop_assert!(buf.get(x, y).unwrap().is_empty(),
2626 "cell ({x},{y}) not empty after clear");
2627 }
2628 }
2629 }
2630
2631 #[test]
2632 fn content_eq_is_reflexive(width in 1u16..30, height in 1u16..30) {
2633 let buf = Buffer::new(width, height);
2634 prop_assert!(buf.content_eq(&buf));
2635 }
2636
2637 #[test]
2638 fn content_eq_detects_single_change(
2639 width in 5u16..30,
2640 height in 5u16..30,
2641 x in 0u16..5,
2642 y in 0u16..5,
2643 ) {
2644 let x = x % width;
2645 let y = y % height;
2646 let buf1 = Buffer::new(width, height);
2647 let mut buf2 = Buffer::new(width, height);
2648 buf2.set_raw(x, y, Cell::from_char('Z'));
2649 prop_assert!(!buf1.content_eq(&buf2));
2650 }
2651
2652 #[test]
2655 fn dimensions_immutable_through_operations(
2656 width in 5u16..30,
2657 height in 5u16..30,
2658 ) {
2659 let mut buf = Buffer::new(width, height);
2660
2661 buf.set(0, 0, Cell::from_char('A'));
2663 prop_assert_eq!(buf.width(), width);
2664 prop_assert_eq!(buf.height(), height);
2665 prop_assert_eq!(buf.len(), width as usize * height as usize);
2666
2667 buf.push_scissor(Rect::new(1, 1, 3, 3));
2668 prop_assert_eq!(buf.width(), width);
2669 prop_assert_eq!(buf.height(), height);
2670
2671 buf.push_opacity(0.5);
2672 prop_assert_eq!(buf.width(), width);
2673 prop_assert_eq!(buf.height(), height);
2674
2675 buf.pop_scissor();
2676 buf.pop_opacity();
2677 prop_assert_eq!(buf.width(), width);
2678 prop_assert_eq!(buf.height(), height);
2679
2680 buf.clear();
2681 prop_assert_eq!(buf.width(), width);
2682 prop_assert_eq!(buf.height(), height);
2683 prop_assert_eq!(buf.len(), width as usize * height as usize);
2684 }
2685
2686 #[test]
2687 fn scissor_area_never_increases_random_rects(
2688 width in 20u16..60,
2689 height in 20u16..60,
2690 rects in proptest::collection::vec(
2691 (0u16..20, 0u16..20, 1u16..15, 1u16..15),
2692 1..8
2693 ),
2694 ) {
2695 let mut buf = Buffer::new(width, height);
2696 let mut prev_area = (width as u32) * (height as u32);
2697
2698 for (x, y, w, h) in rects {
2699 buf.push_scissor(Rect::new(x, y, w, h));
2700 let s = buf.current_scissor();
2701 let area = (s.width as u32) * (s.height as u32);
2702 prop_assert!(area <= prev_area,
2703 "scissor area increased: {} -> {} after push({},{},{},{})",
2704 prev_area, area, x, y, w, h);
2705 prev_area = area;
2706 }
2707 }
2708
2709 #[test]
2710 fn opacity_range_invariant_random_sequence(
2711 opacities in proptest::collection::vec(0.0f32..=1.0, 1..15),
2712 ) {
2713 let mut buf = Buffer::new(5, 5);
2714
2715 for &op in &opacities {
2716 buf.push_opacity(op);
2717 let current = buf.current_opacity();
2718 prop_assert!(current >= 0.0, "opacity below 0: {}", current);
2719 prop_assert!(current <= 1.0 + f32::EPSILON,
2720 "opacity above 1: {}", current);
2721 }
2722
2723 for _ in &opacities {
2725 buf.pop_opacity();
2726 }
2727 prop_assert!((buf.current_opacity() - 1.0).abs() < f32::EPSILON);
2729 }
2730
2731 #[test]
2732 fn opacity_clamp_out_of_range(
2733 neg in -100.0f32..0.0,
2734 over in 1.01f32..100.0,
2735 ) {
2736 let mut buf = Buffer::new(5, 5);
2737
2738 buf.push_opacity(neg);
2739 prop_assert!(buf.current_opacity() >= 0.0,
2740 "negative opacity not clamped: {}", buf.current_opacity());
2741 buf.pop_opacity();
2742
2743 buf.push_opacity(over);
2744 prop_assert!(buf.current_opacity() <= 1.0 + f32::EPSILON,
2745 "over-1 opacity not clamped: {}", buf.current_opacity());
2746 }
2747
2748 #[test]
2749 fn scissor_stack_always_has_base(
2750 pushes in 0usize..10,
2751 pops in 0usize..15,
2752 ) {
2753 let mut buf = Buffer::new(10, 10);
2754
2755 for _ in 0..pushes {
2756 buf.push_scissor(Rect::new(0, 0, 5, 5));
2757 }
2758 for _ in 0..pops {
2759 buf.pop_scissor();
2760 }
2761
2762 prop_assert!(buf.scissor_depth() >= 1,
2764 "scissor depth dropped below 1 after {} pushes, {} pops",
2765 pushes, pops);
2766 }
2767
2768 #[test]
2769 fn opacity_stack_always_has_base(
2770 pushes in 0usize..10,
2771 pops in 0usize..15,
2772 ) {
2773 let mut buf = Buffer::new(10, 10);
2774
2775 for _ in 0..pushes {
2776 buf.push_opacity(0.5);
2777 }
2778 for _ in 0..pops {
2779 buf.pop_opacity();
2780 }
2781
2782 prop_assert!(buf.opacity_depth() >= 1,
2784 "opacity depth dropped below 1 after {} pushes, {} pops",
2785 pushes, pops);
2786 }
2787
2788 #[test]
2789 fn cells_len_invariant_always_holds(
2790 width in 1u16..50,
2791 height in 1u16..50,
2792 ) {
2793 let mut buf = Buffer::new(width, height);
2794 let expected = width as usize * height as usize;
2795
2796 prop_assert_eq!(buf.cells().len(), expected);
2797
2798 buf.set(0, 0, Cell::from_char('X'));
2800 prop_assert_eq!(buf.cells().len(), expected);
2801
2802 buf.clear();
2803 prop_assert_eq!(buf.cells().len(), expected);
2804 }
2805
2806 #[test]
2807 fn set_outside_scissor_is_noop(
2808 width in 10u16..30,
2809 height in 10u16..30,
2810 ) {
2811 let mut buf = Buffer::new(width, height);
2812 buf.push_scissor(Rect::new(2, 2, 3, 3));
2813
2814 buf.set(0, 0, Cell::from_char('X'));
2816 let cell = buf.get(0, 0).unwrap();
2818 prop_assert!(cell.is_empty(),
2819 "cell (0,0) modified outside scissor region");
2820
2821 buf.set(3, 3, Cell::from_char('Y'));
2823 let cell = buf.get(3, 3).unwrap();
2824 prop_assert_eq!(cell.content.as_char(), Some('Y'));
2825 }
2826
2827 #[test]
2830 fn wide_char_overwrites_cleanup_tails(
2831 width in 10u16..30,
2832 x in 0u16..8,
2833 ) {
2834 let x = x % (width.saturating_sub(2).max(1));
2835 let mut buf = Buffer::new(width, 1);
2836
2837 buf.set(x, 0, Cell::from_char('中'));
2839
2840 if x + 1 < width {
2842 let head = buf.get(x, 0).unwrap();
2843 let tail = buf.get(x + 1, 0).unwrap();
2844
2845 if head.content.as_char() == Some('中') {
2846 prop_assert!(tail.is_continuation(),
2847 "tail at x+1={} should be continuation", x + 1);
2848
2849 buf.set(x, 0, Cell::from_char('A'));
2851 let new_head = buf.get(x, 0).unwrap();
2852 let cleared_tail = buf.get(x + 1, 0).unwrap();
2853
2854 prop_assert_eq!(new_head.content.as_char(), Some('A'));
2855 prop_assert!(cleared_tail.is_empty(),
2856 "tail should be cleared after head overwrite");
2857 }
2858 }
2859 }
2860
2861 #[test]
2862 fn wide_char_atomic_rejection_at_boundary(
2863 width in 3u16..20,
2864 ) {
2865 let mut buf = Buffer::new(width, 1);
2866
2867 let last_pos = width - 1;
2869 buf.set(last_pos, 0, Cell::from_char('中'));
2870
2871 let cell = buf.get(last_pos, 0).unwrap();
2873 prop_assert!(cell.is_empty(),
2874 "wide char at boundary position {} (width {}) should be rejected",
2875 last_pos, width);
2876 }
2877
2878 #[test]
2883 fn double_buffer_swap_is_involution(ops in proptest::collection::vec(proptest::bool::ANY, 0..100)) {
2884 let mut db = DoubleBuffer::new(10, 10);
2885 let initial_idx = db.current_idx;
2886
2887 for do_swap in &ops {
2888 if *do_swap {
2889 db.swap();
2890 }
2891 }
2892
2893 let swap_count = ops.iter().filter(|&&x| x).count();
2894 let expected_idx = if swap_count % 2 == 0 { initial_idx } else { 1 - initial_idx };
2895
2896 prop_assert_eq!(db.current_idx, expected_idx,
2897 "After {} swaps, index should be {} but was {}",
2898 swap_count, expected_idx, db.current_idx);
2899 }
2900
2901 #[test]
2902 fn double_buffer_resize_preserves_invariant(
2903 init_w in 1u16..200,
2904 init_h in 1u16..100,
2905 new_w in 1u16..200,
2906 new_h in 1u16..100,
2907 ) {
2908 let mut db = DoubleBuffer::new(init_w, init_h);
2909 db.resize(new_w, new_h);
2910
2911 prop_assert_eq!(db.width(), new_w);
2912 prop_assert_eq!(db.height(), new_h);
2913 prop_assert!(db.dimensions_match(new_w, new_h));
2914 }
2915
2916 #[test]
2917 fn double_buffer_current_previous_disjoint(
2918 width in 1u16..50,
2919 height in 1u16..50,
2920 ) {
2921 let mut db = DoubleBuffer::new(width, height);
2922
2923 db.current_mut().set(0, 0, Cell::from_char('C'));
2925
2926 prop_assert!(db.previous().get(0, 0).unwrap().is_empty(),
2928 "Previous buffer should not reflect changes to current");
2929
2930 db.swap();
2932 prop_assert_eq!(db.previous().get(0, 0).unwrap().content.as_char(), Some('C'),
2933 "After swap, previous should have the 'C' we wrote");
2934 }
2935
2936 #[test]
2937 fn double_buffer_swap_content_semantics(
2938 width in 5u16..30,
2939 height in 5u16..30,
2940 ) {
2941 let mut db = DoubleBuffer::new(width, height);
2942
2943 db.current_mut().set(0, 0, Cell::from_char('X'));
2945 db.swap();
2946
2947 db.current_mut().set(0, 0, Cell::from_char('Y'));
2949 db.swap();
2950
2951 prop_assert_eq!(db.current().get(0, 0).unwrap().content.as_char(), Some('X'));
2953 prop_assert_eq!(db.previous().get(0, 0).unwrap().content.as_char(), Some('Y'));
2954 }
2955
2956 #[test]
2957 fn double_buffer_resize_clears_both(
2958 w1 in 5u16..30,
2959 h1 in 5u16..30,
2960 w2 in 5u16..30,
2961 h2 in 5u16..30,
2962 ) {
2963 prop_assume!(w1 != w2 || h1 != h2);
2965
2966 let mut db = DoubleBuffer::new(w1, h1);
2967
2968 db.current_mut().set(0, 0, Cell::from_char('A'));
2970 db.swap();
2971 db.current_mut().set(0, 0, Cell::from_char('B'));
2972
2973 db.resize(w2, h2);
2975
2976 prop_assert!(db.current().get(0, 0).unwrap().is_empty(),
2978 "Current buffer should be empty after resize");
2979 prop_assert!(db.previous().get(0, 0).unwrap().is_empty(),
2980 "Previous buffer should be empty after resize");
2981 }
2982 }
2983 }
2984
2985 #[test]
2988 fn dirty_rows_start_dirty() {
2989 let buf = Buffer::new(10, 5);
2991 assert_eq!(buf.dirty_row_count(), 5);
2992 for y in 0..5 {
2993 assert!(buf.is_row_dirty(y));
2994 }
2995 }
2996
2997 #[test]
2998 fn dirty_bitmap_starts_full() {
2999 let buf = Buffer::new(4, 3);
3000 assert!(buf.dirty_all());
3001 assert_eq!(buf.dirty_cell_count(), 12);
3002 }
3003
3004 #[test]
3005 fn dirty_bitmap_tracks_single_cell() {
3006 let mut buf = Buffer::new(4, 3);
3007 buf.clear_dirty();
3008 assert!(!buf.dirty_all());
3009 buf.set_raw(1, 1, Cell::from_char('X'));
3010 let idx = 1 + 4;
3011 assert_eq!(buf.dirty_cell_count(), 1);
3012 assert_eq!(buf.dirty_bits()[idx], 1);
3013 }
3014
3015 #[test]
3016 fn dirty_bitmap_dedupes_cells() {
3017 let mut buf = Buffer::new(4, 3);
3018 buf.clear_dirty();
3019 buf.set_raw(2, 2, Cell::from_char('A'));
3020 buf.set_raw(2, 2, Cell::from_char('B'));
3021 assert_eq!(buf.dirty_cell_count(), 1);
3022 }
3023
3024 #[test]
3025 fn set_marks_row_dirty() {
3026 let mut buf = Buffer::new(10, 5);
3027 buf.clear_dirty(); buf.set(3, 2, Cell::from_char('X'));
3029 assert!(buf.is_row_dirty(2));
3030 assert!(!buf.is_row_dirty(0));
3031 assert!(!buf.is_row_dirty(1));
3032 assert!(!buf.is_row_dirty(3));
3033 assert!(!buf.is_row_dirty(4));
3034 }
3035
3036 #[test]
3037 fn set_raw_marks_row_dirty() {
3038 let mut buf = Buffer::new(10, 5);
3039 buf.clear_dirty(); buf.set_raw(0, 4, Cell::from_char('Z'));
3041 assert!(buf.is_row_dirty(4));
3042 assert_eq!(buf.dirty_row_count(), 1);
3043 }
3044
3045 #[test]
3046 fn clear_marks_all_dirty() {
3047 let mut buf = Buffer::new(10, 5);
3048 buf.clear();
3049 assert_eq!(buf.dirty_row_count(), 5);
3050 }
3051
3052 #[test]
3053 fn clear_dirty_resets_flags() {
3054 let mut buf = Buffer::new(10, 5);
3055 assert_eq!(buf.dirty_row_count(), 5);
3057 buf.clear_dirty();
3058 assert_eq!(buf.dirty_row_count(), 0);
3059
3060 buf.set(0, 0, Cell::from_char('A'));
3062 buf.set(0, 3, Cell::from_char('B'));
3063 assert_eq!(buf.dirty_row_count(), 2);
3064
3065 buf.clear_dirty();
3066 assert_eq!(buf.dirty_row_count(), 0);
3067 }
3068
3069 #[test]
3070 fn clear_dirty_resets_bitmap() {
3071 let mut buf = Buffer::new(4, 3);
3072 buf.clear();
3073 assert!(buf.dirty_all());
3074 buf.clear_dirty();
3075 assert!(!buf.dirty_all());
3076 assert_eq!(buf.dirty_cell_count(), 0);
3077 assert!(buf.dirty_bits().iter().all(|&b| b == 0));
3078 }
3079
3080 #[test]
3081 fn fill_marks_affected_rows_dirty() {
3082 let mut buf = Buffer::new(10, 10);
3083 buf.clear_dirty(); buf.fill(Rect::new(0, 2, 5, 3), Cell::from_char('.'));
3085 assert!(!buf.is_row_dirty(0));
3087 assert!(!buf.is_row_dirty(1));
3088 assert!(buf.is_row_dirty(2));
3089 assert!(buf.is_row_dirty(3));
3090 assert!(buf.is_row_dirty(4));
3091 assert!(!buf.is_row_dirty(5));
3092 }
3093
3094 #[test]
3095 fn get_mut_marks_row_dirty() {
3096 let mut buf = Buffer::new(10, 5);
3097 buf.clear_dirty(); if let Some(cell) = buf.get_mut(5, 3) {
3099 cell.fg = PackedRgba::rgb(255, 0, 0);
3100 }
3101 assert!(buf.is_row_dirty(3));
3102 assert_eq!(buf.dirty_row_count(), 1);
3103 }
3104
3105 #[test]
3106 fn cells_mut_marks_all_dirty() {
3107 let mut buf = Buffer::new(10, 5);
3108 let _ = buf.cells_mut();
3109 assert_eq!(buf.dirty_row_count(), 5);
3110 }
3111
3112 #[test]
3113 fn dirty_rows_slice_length_matches_height() {
3114 let buf = Buffer::new(10, 7);
3115 assert_eq!(buf.dirty_rows().len(), 7);
3116 }
3117
3118 #[test]
3119 fn out_of_bounds_set_does_not_dirty() {
3120 let mut buf = Buffer::new(10, 5);
3121 buf.clear_dirty(); buf.set(100, 100, Cell::from_char('X'));
3123 assert_eq!(buf.dirty_row_count(), 0);
3124 }
3125
3126 #[test]
3127 fn property_dirty_soundness() {
3128 let mut buf = Buffer::new(20, 10);
3130 let positions = [(3, 0), (5, 2), (0, 9), (19, 5), (10, 7)];
3131 for &(x, y) in &positions {
3132 buf.set(x, y, Cell::from_char('*'));
3133 }
3134 for &(_, y) in &positions {
3135 assert!(
3136 buf.is_row_dirty(y),
3137 "Row {} should be dirty after set({}, {})",
3138 y,
3139 positions.iter().find(|(_, ry)| *ry == y).unwrap().0,
3140 y
3141 );
3142 }
3143 }
3144
3145 #[test]
3146 fn dirty_clear_between_frames() {
3147 let mut buf = Buffer::new(10, 5);
3149
3150 assert_eq!(buf.dirty_row_count(), 5);
3152
3153 buf.clear_dirty();
3155 assert_eq!(buf.dirty_row_count(), 0);
3156
3157 buf.set(0, 0, Cell::from_char('A'));
3159 buf.set(0, 2, Cell::from_char('B'));
3160 assert_eq!(buf.dirty_row_count(), 2);
3161
3162 buf.clear_dirty();
3164 assert_eq!(buf.dirty_row_count(), 0);
3165
3166 buf.set(0, 4, Cell::from_char('C'));
3168 assert_eq!(buf.dirty_row_count(), 1);
3169 assert!(buf.is_row_dirty(4));
3170 assert!(!buf.is_row_dirty(0));
3171 }
3172
3173 #[test]
3176 fn dirty_spans_start_full_dirty() {
3177 let buf = Buffer::new(10, 5);
3178 for y in 0..5 {
3179 let row = buf.dirty_span_row(y).unwrap();
3180 assert!(row.is_full(), "row {y} should start full-dirty");
3181 assert!(row.spans().is_empty(), "row {y} spans should start empty");
3182 }
3183 }
3184
3185 #[test]
3186 fn clear_dirty_resets_spans() {
3187 let mut buf = Buffer::new(10, 5);
3188 buf.clear_dirty();
3189 for y in 0..5 {
3190 let row = buf.dirty_span_row(y).unwrap();
3191 assert!(!row.is_full(), "row {y} should clear full-dirty");
3192 assert!(row.spans().is_empty(), "row {y} spans should be cleared");
3193 }
3194 assert_eq!(buf.dirty_span_overflows, 0);
3195 }
3196
3197 #[test]
3198 fn set_records_dirty_span() {
3199 let mut buf = Buffer::new(20, 2);
3200 buf.clear_dirty();
3201 buf.set(2, 0, Cell::from_char('A'));
3202 let row = buf.dirty_span_row(0).unwrap();
3203 assert_eq!(row.spans(), &[DirtySpan::new(2, 3)]);
3204 assert!(!row.is_full());
3205 }
3206
3207 #[test]
3208 fn set_merges_adjacent_spans() {
3209 let mut buf = Buffer::new(20, 2);
3210 buf.clear_dirty();
3211 buf.set(2, 0, Cell::from_char('A'));
3212 buf.set(3, 0, Cell::from_char('B')); let row = buf.dirty_span_row(0).unwrap();
3214 assert_eq!(row.spans(), &[DirtySpan::new(2, 4)]);
3215 }
3216
3217 #[test]
3218 fn set_merges_close_spans() {
3219 let mut buf = Buffer::new(20, 2);
3220 buf.clear_dirty();
3221 buf.set(2, 0, Cell::from_char('A'));
3222 buf.set(4, 0, Cell::from_char('B')); let row = buf.dirty_span_row(0).unwrap();
3224 assert_eq!(row.spans(), &[DirtySpan::new(2, 5)]);
3225 }
3226
3227 #[test]
3228 fn span_overflow_sets_full_row() {
3229 let width = (DIRTY_SPAN_MAX_SPANS_PER_ROW as u16 + 2) * 3;
3230 let mut buf = Buffer::new(width, 1);
3231 buf.clear_dirty();
3232 for i in 0..(DIRTY_SPAN_MAX_SPANS_PER_ROW + 1) {
3233 let x = (i as u16) * 3;
3234 buf.set(x, 0, Cell::from_char('x'));
3235 }
3236 let row = buf.dirty_span_row(0).unwrap();
3237 assert!(row.is_full());
3238 assert!(row.spans().is_empty());
3239 assert_eq!(buf.dirty_span_overflows, 1);
3240 }
3241
3242 #[test]
3243 fn fill_full_row_marks_full_span() {
3244 let mut buf = Buffer::new(10, 3);
3245 buf.clear_dirty();
3246 let cell = Cell::from_char('x').with_bg(PackedRgba::rgb(0, 0, 0));
3247 buf.fill(Rect::new(0, 1, 10, 1), cell);
3248 let row = buf.dirty_span_row(1).unwrap();
3249 assert!(row.is_full());
3250 assert!(row.spans().is_empty());
3251 }
3252
3253 #[test]
3254 fn get_mut_records_dirty_span() {
3255 let mut buf = Buffer::new(10, 5);
3256 buf.clear_dirty();
3257 let _ = buf.get_mut(5, 3);
3258 let row = buf.dirty_span_row(3).unwrap();
3259 assert_eq!(row.spans(), &[DirtySpan::new(5, 6)]);
3260 }
3261
3262 #[test]
3263 fn cells_mut_marks_all_full_spans() {
3264 let mut buf = Buffer::new(10, 5);
3265 buf.clear_dirty();
3266 let _ = buf.cells_mut();
3267 for y in 0..5 {
3268 let row = buf.dirty_span_row(y).unwrap();
3269 assert!(row.is_full(), "row {y} should be full after cells_mut");
3270 }
3271 }
3272
3273 #[test]
3274 fn dirty_span_config_disabled_skips_rows() {
3275 let mut buf = Buffer::new(10, 1);
3276 buf.clear_dirty();
3277 buf.set_dirty_span_config(DirtySpanConfig::default().with_enabled(false));
3278 buf.set(5, 0, Cell::from_char('x'));
3279 assert!(buf.dirty_span_row(0).is_none());
3280 let stats = buf.dirty_span_stats();
3281 assert_eq!(stats.total_spans, 0);
3282 assert_eq!(stats.span_coverage_cells, 0);
3283 }
3284
3285 #[test]
3286 fn dirty_span_guard_band_expands_span_bounds() {
3287 let mut buf = Buffer::new(10, 1);
3288 buf.clear_dirty();
3289 buf.set_dirty_span_config(DirtySpanConfig::default().with_guard_band(2));
3290 buf.set(5, 0, Cell::from_char('x'));
3291 let row = buf.dirty_span_row(0).unwrap();
3292 assert_eq!(row.spans(), &[DirtySpan::new(3, 8)]);
3293 }
3294
3295 #[test]
3296 fn dirty_span_max_spans_overflow_triggers_full_row() {
3297 let mut buf = Buffer::new(10, 1);
3298 buf.clear_dirty();
3299 buf.set_dirty_span_config(
3300 DirtySpanConfig::default()
3301 .with_max_spans_per_row(1)
3302 .with_merge_gap(0),
3303 );
3304 buf.set(0, 0, Cell::from_char('a'));
3305 buf.set(4, 0, Cell::from_char('b'));
3306 let row = buf.dirty_span_row(0).unwrap();
3307 assert!(row.is_full());
3308 assert!(row.spans().is_empty());
3309 assert_eq!(buf.dirty_span_overflows, 1);
3310 }
3311
3312 #[test]
3313 fn dirty_span_stats_counts_full_rows_and_spans() {
3314 let mut buf = Buffer::new(6, 2);
3315 buf.clear_dirty();
3316 buf.set_dirty_span_config(DirtySpanConfig::default().with_merge_gap(0));
3317 buf.set(1, 0, Cell::from_char('a'));
3318 buf.set(4, 0, Cell::from_char('b'));
3319 buf.mark_dirty_row_full(1);
3320
3321 let stats = buf.dirty_span_stats();
3322 assert_eq!(stats.rows_full_dirty, 1);
3323 assert_eq!(stats.rows_with_spans, 1);
3324 assert_eq!(stats.total_spans, 2);
3325 assert_eq!(stats.max_span_len, 6);
3326 assert_eq!(stats.span_coverage_cells, 8);
3327 }
3328
3329 #[test]
3330 fn dirty_span_stats_reports_overflow_and_full_row() {
3331 let mut buf = Buffer::new(8, 1);
3332 buf.clear_dirty();
3333 buf.set_dirty_span_config(
3334 DirtySpanConfig::default()
3335 .with_max_spans_per_row(1)
3336 .with_merge_gap(0),
3337 );
3338 buf.set(0, 0, Cell::from_char('x'));
3339 buf.set(3, 0, Cell::from_char('y'));
3340
3341 let stats = buf.dirty_span_stats();
3342 assert_eq!(stats.overflows, 1);
3343 assert_eq!(stats.rows_full_dirty, 1);
3344 assert_eq!(stats.total_spans, 0);
3345 assert_eq!(stats.span_coverage_cells, 8);
3346 }
3347
3348 #[test]
3353 fn double_buffer_new_has_matching_dimensions() {
3354 let db = DoubleBuffer::new(80, 24);
3355 assert_eq!(db.width(), 80);
3356 assert_eq!(db.height(), 24);
3357 assert!(db.dimensions_match(80, 24));
3358 assert!(!db.dimensions_match(120, 40));
3359 }
3360
3361 #[test]
3362 fn double_buffer_swap_is_o1() {
3363 let mut db = DoubleBuffer::new(80, 24);
3364
3365 db.current_mut().set(0, 0, Cell::from_char('A'));
3367 assert_eq!(db.current().get(0, 0).unwrap().content.as_char(), Some('A'));
3368
3369 db.swap();
3371 assert_eq!(
3372 db.previous().get(0, 0).unwrap().content.as_char(),
3373 Some('A')
3374 );
3375 assert!(db.current().get(0, 0).unwrap().is_empty());
3377 }
3378
3379 #[test]
3380 fn double_buffer_swap_round_trip() {
3381 let mut db = DoubleBuffer::new(10, 5);
3382
3383 db.current_mut().set(0, 0, Cell::from_char('X'));
3384 db.swap();
3385 db.current_mut().set(0, 0, Cell::from_char('Y'));
3386 db.swap();
3387
3388 assert_eq!(db.current().get(0, 0).unwrap().content.as_char(), Some('X'));
3390 assert_eq!(
3391 db.previous().get(0, 0).unwrap().content.as_char(),
3392 Some('Y')
3393 );
3394 }
3395
3396 #[test]
3397 fn double_buffer_resize_changes_dimensions() {
3398 let mut db = DoubleBuffer::new(80, 24);
3399 assert!(!db.resize(80, 24)); assert!(db.resize(120, 40)); assert_eq!(db.width(), 120);
3402 assert_eq!(db.height(), 40);
3403 assert!(db.dimensions_match(120, 40));
3404 }
3405
3406 #[test]
3407 fn double_buffer_resize_clears_content() {
3408 let mut db = DoubleBuffer::new(10, 5);
3409 db.current_mut().set(0, 0, Cell::from_char('Z'));
3410 db.swap();
3411 db.current_mut().set(0, 0, Cell::from_char('W'));
3412
3413 db.resize(20, 10);
3414
3415 assert!(db.current().get(0, 0).unwrap().is_empty());
3417 assert!(db.previous().get(0, 0).unwrap().is_empty());
3418 }
3419
3420 #[test]
3421 fn double_buffer_current_and_previous_are_distinct() {
3422 let mut db = DoubleBuffer::new(10, 5);
3423 db.current_mut().set(0, 0, Cell::from_char('C'));
3424
3425 assert!(db.previous().get(0, 0).unwrap().is_empty());
3427 assert_eq!(db.current().get(0, 0).unwrap().content.as_char(), Some('C'));
3428 }
3429
3430 #[test]
3435 fn adaptive_buffer_new_has_over_allocation() {
3436 let adb = AdaptiveDoubleBuffer::new(80, 24);
3437
3438 assert_eq!(adb.width(), 80);
3440 assert_eq!(adb.height(), 24);
3441 assert!(adb.dimensions_match(80, 24));
3442
3443 assert!(adb.capacity_width() > 80);
3447 assert!(adb.capacity_height() > 24);
3448 assert_eq!(adb.capacity_width(), 100); assert_eq!(adb.capacity_height(), 30); }
3451
3452 #[test]
3453 fn adaptive_buffer_resize_avoids_reallocation_when_within_capacity() {
3454 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3455
3456 assert!(adb.resize(90, 28)); assert_eq!(adb.width(), 90);
3459 assert_eq!(adb.height(), 28);
3460 assert_eq!(adb.stats().resize_avoided, 1);
3461 assert_eq!(adb.stats().resize_reallocated, 0);
3462 assert_eq!(adb.stats().resize_growth, 1);
3463 }
3464
3465 #[test]
3466 fn adaptive_buffer_resize_reallocates_on_growth_beyond_capacity() {
3467 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3468
3469 assert!(adb.resize(120, 40)); assert_eq!(adb.width(), 120);
3472 assert_eq!(adb.height(), 40);
3473 assert_eq!(adb.stats().resize_reallocated, 1);
3474 assert_eq!(adb.stats().resize_avoided, 0);
3475
3476 assert!(adb.capacity_width() > 120);
3478 assert!(adb.capacity_height() > 40);
3479 }
3480
3481 #[test]
3482 fn adaptive_buffer_resize_reallocates_on_significant_shrink() {
3483 let mut adb = AdaptiveDoubleBuffer::new(100, 50);
3484
3485 assert!(adb.resize(40, 20)); assert_eq!(adb.width(), 40);
3489 assert_eq!(adb.height(), 20);
3490 assert_eq!(adb.stats().resize_reallocated, 1);
3491 assert_eq!(adb.stats().resize_shrink, 1);
3492 }
3493
3494 #[test]
3495 fn adaptive_buffer_resize_avoids_reallocation_on_minor_shrink() {
3496 let mut adb = AdaptiveDoubleBuffer::new(100, 50);
3497
3498 assert!(adb.resize(80, 40));
3502 assert_eq!(adb.width(), 80);
3503 assert_eq!(adb.height(), 40);
3504 assert_eq!(adb.stats().resize_avoided, 1);
3505 assert_eq!(adb.stats().resize_reallocated, 0);
3506 assert_eq!(adb.stats().resize_shrink, 1);
3507 }
3508
3509 #[test]
3510 fn adaptive_buffer_no_change_returns_false() {
3511 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3512
3513 assert!(!adb.resize(80, 24)); assert_eq!(adb.stats().resize_avoided, 0);
3515 assert_eq!(adb.stats().resize_reallocated, 0);
3516 assert_eq!(adb.stats().resize_growth, 0);
3517 assert_eq!(adb.stats().resize_shrink, 0);
3518 }
3519
3520 #[test]
3521 fn adaptive_buffer_swap_works() {
3522 let mut adb = AdaptiveDoubleBuffer::new(10, 5);
3523
3524 adb.current_mut().set(0, 0, Cell::from_char('A'));
3525 assert_eq!(
3526 adb.current().get(0, 0).unwrap().content.as_char(),
3527 Some('A')
3528 );
3529
3530 adb.swap();
3531 assert_eq!(
3532 adb.previous().get(0, 0).unwrap().content.as_char(),
3533 Some('A')
3534 );
3535 assert!(adb.current().get(0, 0).unwrap().is_empty());
3536 }
3537
3538 #[test]
3539 fn adaptive_buffer_stats_reset() {
3540 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3541
3542 adb.resize(90, 28);
3543 adb.resize(120, 40);
3544 assert!(adb.stats().resize_avoided > 0 || adb.stats().resize_reallocated > 0);
3545
3546 adb.reset_stats();
3547 assert_eq!(adb.stats().resize_avoided, 0);
3548 assert_eq!(adb.stats().resize_reallocated, 0);
3549 assert_eq!(adb.stats().resize_growth, 0);
3550 assert_eq!(adb.stats().resize_shrink, 0);
3551 }
3552
3553 #[test]
3554 fn adaptive_buffer_memory_efficiency() {
3555 let adb = AdaptiveDoubleBuffer::new(80, 24);
3556
3557 let efficiency = adb.memory_efficiency();
3558 assert!(efficiency > 0.5);
3562 assert!(efficiency < 1.0);
3563 }
3564
3565 #[test]
3566 fn adaptive_buffer_logical_bounds() {
3567 let adb = AdaptiveDoubleBuffer::new(80, 24);
3568
3569 let bounds = adb.logical_bounds();
3570 assert_eq!(bounds.x, 0);
3571 assert_eq!(bounds.y, 0);
3572 assert_eq!(bounds.width, 80);
3573 assert_eq!(bounds.height, 24);
3574 }
3575
3576 #[test]
3577 fn adaptive_buffer_capacity_clamped_for_large_sizes() {
3578 let adb = AdaptiveDoubleBuffer::new(1000, 500);
3580
3581 assert_eq!(adb.capacity_width(), 1000 + 200); assert_eq!(adb.capacity_height(), 500 + 125); }
3586
3587 #[test]
3588 fn adaptive_stats_avoidance_ratio() {
3589 let mut stats = AdaptiveStats::default();
3590
3591 assert!((stats.avoidance_ratio() - 1.0).abs() < f64::EPSILON);
3593
3594 stats.resize_avoided = 3;
3596 stats.resize_reallocated = 1;
3597 assert!((stats.avoidance_ratio() - 0.75).abs() < f64::EPSILON);
3598
3599 stats.resize_avoided = 0;
3601 stats.resize_reallocated = 5;
3602 assert!((stats.avoidance_ratio() - 0.0).abs() < f64::EPSILON);
3603 }
3604
3605 #[test]
3606 fn adaptive_buffer_resize_storm_simulation() {
3607 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3609
3610 for i in 1..=10 {
3612 adb.resize(80 + i, 24 + (i / 2));
3613 }
3614
3615 let ratio = adb.stats().avoidance_ratio();
3617 assert!(
3618 ratio > 0.5,
3619 "Expected >50% avoidance ratio, got {:.2}",
3620 ratio
3621 );
3622 }
3623
3624 #[test]
3625 fn adaptive_buffer_width_only_growth() {
3626 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3627
3628 assert!(adb.resize(95, 24)); assert_eq!(adb.stats().resize_avoided, 1);
3631 assert_eq!(adb.stats().resize_growth, 1);
3632 }
3633
3634 #[test]
3635 fn adaptive_buffer_height_only_growth() {
3636 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3637
3638 assert!(adb.resize(80, 28)); assert_eq!(adb.stats().resize_avoided, 1);
3641 assert_eq!(adb.stats().resize_growth, 1);
3642 }
3643
3644 #[test]
3645 fn adaptive_buffer_one_dimension_exceeds_capacity() {
3646 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3647
3648 assert!(adb.resize(105, 24)); assert_eq!(adb.stats().resize_reallocated, 1);
3651 }
3652
3653 #[test]
3654 fn adaptive_buffer_current_and_previous_distinct() {
3655 let mut adb = AdaptiveDoubleBuffer::new(10, 5);
3656 adb.current_mut().set(0, 0, Cell::from_char('X'));
3657
3658 assert!(adb.previous().get(0, 0).unwrap().is_empty());
3660 assert_eq!(
3661 adb.current().get(0, 0).unwrap().content.as_char(),
3662 Some('X')
3663 );
3664 }
3665
3666 #[test]
3667 fn adaptive_buffer_resize_within_capacity_clears_previous() {
3668 let mut adb = AdaptiveDoubleBuffer::new(10, 5);
3669 adb.current_mut().set(9, 4, Cell::from_char('X'));
3670 adb.swap();
3671
3672 assert!(adb.resize(8, 4));
3674
3675 assert!(adb.previous().get(9, 4).unwrap().is_empty());
3677 }
3678
3679 #[test]
3681 fn adaptive_buffer_invariant_capacity_geq_logical() {
3682 for width in [1u16, 10, 80, 200, 1000, 5000] {
3684 for height in [1u16, 10, 24, 100, 500, 2000] {
3685 let adb = AdaptiveDoubleBuffer::new(width, height);
3686 assert!(
3687 adb.capacity_width() >= adb.width(),
3688 "capacity_width {} < logical_width {} for ({}, {})",
3689 adb.capacity_width(),
3690 adb.width(),
3691 width,
3692 height
3693 );
3694 assert!(
3695 adb.capacity_height() >= adb.height(),
3696 "capacity_height {} < logical_height {} for ({}, {})",
3697 adb.capacity_height(),
3698 adb.height(),
3699 width,
3700 height
3701 );
3702 }
3703 }
3704 }
3705
3706 #[test]
3707 fn adaptive_buffer_invariant_resize_dimensions_correct() {
3708 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3709
3710 let test_sizes = [
3712 (100, 50),
3713 (40, 20),
3714 (80, 24),
3715 (200, 100),
3716 (10, 5),
3717 (1000, 500),
3718 ];
3719 for (w, h) in test_sizes {
3720 adb.resize(w, h);
3721 assert_eq!(adb.width(), w, "width mismatch for ({}, {})", w, h);
3722 assert_eq!(adb.height(), h, "height mismatch for ({}, {})", w, h);
3723 assert!(
3724 adb.capacity_width() >= w,
3725 "capacity_width < width for ({}, {})",
3726 w,
3727 h
3728 );
3729 assert!(
3730 adb.capacity_height() >= h,
3731 "capacity_height < height for ({}, {})",
3732 w,
3733 h
3734 );
3735 }
3736 }
3737
3738 #[test]
3742 fn adaptive_buffer_no_ghosting_on_shrink() {
3743 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3744
3745 for y in 0..adb.height() {
3747 for x in 0..adb.width() {
3748 adb.current_mut().set(x, y, Cell::from_char('X'));
3749 }
3750 }
3751
3752 adb.resize(60, 20);
3755
3756 for y in 0..adb.height() {
3759 for x in 0..adb.width() {
3760 let cell = adb.current().get(x, y).unwrap();
3761 assert!(
3762 cell.is_empty(),
3763 "Ghost content at ({}, {}): expected empty, got {:?}",
3764 x,
3765 y,
3766 cell.content
3767 );
3768 }
3769 }
3770 }
3771
3772 #[test]
3776 fn adaptive_buffer_no_ghosting_on_reallocation_shrink() {
3777 let mut adb = AdaptiveDoubleBuffer::new(100, 50);
3778
3779 for y in 0..adb.height() {
3781 for x in 0..adb.width() {
3782 adb.current_mut().set(x, y, Cell::from_char('A'));
3783 }
3784 }
3785 adb.swap();
3786 for y in 0..adb.height() {
3787 for x in 0..adb.width() {
3788 adb.current_mut().set(x, y, Cell::from_char('B'));
3789 }
3790 }
3791
3792 adb.resize(30, 15);
3794 assert_eq!(adb.stats().resize_reallocated, 1);
3795
3796 for y in 0..adb.height() {
3798 for x in 0..adb.width() {
3799 assert!(
3800 adb.current().get(x, y).unwrap().is_empty(),
3801 "Ghost in current at ({}, {})",
3802 x,
3803 y
3804 );
3805 assert!(
3806 adb.previous().get(x, y).unwrap().is_empty(),
3807 "Ghost in previous at ({}, {})",
3808 x,
3809 y
3810 );
3811 }
3812 }
3813 }
3814
3815 #[test]
3819 fn adaptive_buffer_no_ghosting_on_growth_reallocation() {
3820 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3821
3822 for y in 0..adb.height() {
3824 for x in 0..adb.width() {
3825 adb.current_mut().set(x, y, Cell::from_char('Z'));
3826 }
3827 }
3828
3829 adb.resize(150, 60);
3831 assert_eq!(adb.stats().resize_reallocated, 1);
3832
3833 for y in 0..adb.height() {
3835 for x in 0..adb.width() {
3836 assert!(
3837 adb.current().get(x, y).unwrap().is_empty(),
3838 "Ghost at ({}, {}) after growth reallocation",
3839 x,
3840 y
3841 );
3842 }
3843 }
3844 }
3845
3846 #[test]
3848 fn adaptive_buffer_resize_idempotent() {
3849 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3850 adb.current_mut().set(5, 5, Cell::from_char('K'));
3851
3852 let changed = adb.resize(80, 24);
3854 assert!(!changed);
3855
3856 assert_eq!(
3858 adb.current().get(5, 5).unwrap().content.as_char(),
3859 Some('K')
3860 );
3861 }
3862
3863 #[test]
3868 fn dirty_span_merge_adjacent() {
3869 let mut buf = Buffer::new(100, 1);
3870 buf.clear_dirty(); buf.mark_dirty_span(0, 10, 20);
3874 let spans = buf.dirty_span_row(0).unwrap().spans();
3875 assert_eq!(spans.len(), 1);
3876 assert_eq!(spans[0], DirtySpan::new(10, 20));
3877
3878 buf.mark_dirty_span(0, 20, 30);
3880 let spans = buf.dirty_span_row(0).unwrap().spans();
3881 assert_eq!(spans.len(), 1);
3882 assert_eq!(spans[0], DirtySpan::new(10, 30));
3883 }
3884
3885 #[test]
3886 fn dirty_span_merge_overlapping() {
3887 let mut buf = Buffer::new(100, 1);
3888 buf.clear_dirty();
3889
3890 buf.mark_dirty_span(0, 10, 20);
3892 buf.mark_dirty_span(0, 15, 25);
3894
3895 let spans = buf.dirty_span_row(0).unwrap().spans();
3896 assert_eq!(spans.len(), 1);
3897 assert_eq!(spans[0], DirtySpan::new(10, 25));
3898 }
3899
3900 #[test]
3901 fn dirty_span_merge_with_gap() {
3902 let mut buf = Buffer::new(100, 1);
3903 buf.clear_dirty();
3904
3905 buf.mark_dirty_span(0, 10, 20);
3908 buf.mark_dirty_span(0, 21, 30);
3910
3911 let spans = buf.dirty_span_row(0).unwrap().spans();
3912 assert_eq!(spans.len(), 1);
3913 assert_eq!(spans[0], DirtySpan::new(10, 30));
3914 }
3915
3916 #[test]
3917 fn dirty_span_no_merge_large_gap() {
3918 let mut buf = Buffer::new(100, 1);
3919 buf.clear_dirty();
3920
3921 buf.mark_dirty_span(0, 10, 20);
3923 buf.mark_dirty_span(0, 22, 30);
3925
3926 let spans = buf.dirty_span_row(0).unwrap().spans();
3927 assert_eq!(spans.len(), 2);
3928 assert_eq!(spans[0], DirtySpan::new(10, 20));
3929 assert_eq!(spans[1], DirtySpan::new(22, 30));
3930 }
3931
3932 #[test]
3933 fn dirty_span_overflow_to_full() {
3934 let mut buf = Buffer::new(1000, 1);
3935 buf.clear_dirty();
3936
3937 for i in 0..DIRTY_SPAN_MAX_SPANS_PER_ROW + 10 {
3939 let start = (i * 4) as u16;
3940 buf.mark_dirty_span(0, start, start + 1);
3941 }
3942
3943 let row = buf.dirty_span_row(0).unwrap();
3944 assert!(row.is_full(), "Row should overflow to full scan");
3945 assert!(
3946 row.spans().is_empty(),
3947 "Spans should be cleared on overflow"
3948 );
3949 }
3950
3951 #[test]
3952 fn dirty_span_bounds_clamping() {
3953 let mut buf = Buffer::new(10, 1);
3954 buf.clear_dirty();
3955
3956 buf.mark_dirty_span(0, 15, 20);
3958 let spans = buf.dirty_span_row(0).unwrap().spans();
3959 assert!(spans.is_empty());
3960
3961 buf.mark_dirty_span(0, 8, 15);
3963 let spans = buf.dirty_span_row(0).unwrap().spans();
3964 assert_eq!(spans.len(), 1);
3965 assert_eq!(spans[0], DirtySpan::new(8, 10)); }
3967
3968 #[test]
3969 fn dirty_span_guard_band_clamps_bounds() {
3970 let mut buf = Buffer::new(10, 1);
3971 buf.clear_dirty();
3972 buf.set_dirty_span_config(DirtySpanConfig::default().with_guard_band(5));
3973
3974 buf.mark_dirty_span(0, 2, 3);
3975 let spans = buf.dirty_span_row(0).unwrap().spans();
3976 assert_eq!(spans.len(), 1);
3977 assert_eq!(spans[0], DirtySpan::new(0, 8));
3978
3979 buf.clear_dirty();
3980 buf.mark_dirty_span(0, 8, 10);
3981 let spans = buf.dirty_span_row(0).unwrap().spans();
3982 assert_eq!(spans.len(), 1);
3983 assert_eq!(spans[0], DirtySpan::new(3, 10));
3984 }
3985
3986 #[test]
3987 fn dirty_span_empty_span_is_ignored() {
3988 let mut buf = Buffer::new(10, 1);
3989 buf.clear_dirty();
3990 buf.mark_dirty_span(0, 5, 5);
3991 let spans = buf.dirty_span_row(0).unwrap().spans();
3992 assert!(spans.is_empty());
3993 }
3994}