1#![forbid(unsafe_code)]
2
3use crate::budget::DegradationLevel;
56use crate::cell::Cell;
57use ftui_core::geometry::Rect;
58
59const DIRTY_SPAN_MAX_SPANS_PER_ROW: usize = 64;
61const DIRTY_SPAN_MERGE_GAP: u16 = 1;
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub struct DirtySpanConfig {
67 pub enabled: bool,
69 pub max_spans_per_row: usize,
71 pub merge_gap: u16,
73 pub guard_band: u16,
75}
76
77impl Default for DirtySpanConfig {
78 fn default() -> Self {
79 Self {
80 enabled: true,
81 max_spans_per_row: DIRTY_SPAN_MAX_SPANS_PER_ROW,
82 merge_gap: DIRTY_SPAN_MERGE_GAP,
83 guard_band: 0,
84 }
85 }
86}
87
88impl DirtySpanConfig {
89 pub fn with_enabled(mut self, enabled: bool) -> Self {
91 self.enabled = enabled;
92 self
93 }
94
95 pub fn with_max_spans_per_row(mut self, max_spans: usize) -> Self {
97 self.max_spans_per_row = max_spans;
98 self
99 }
100
101 pub fn with_merge_gap(mut self, merge_gap: u16) -> Self {
103 self.merge_gap = merge_gap;
104 self
105 }
106
107 pub fn with_guard_band(mut self, guard_band: u16) -> Self {
109 self.guard_band = guard_band;
110 self
111 }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub(crate) struct DirtySpan {
117 pub x0: u16,
118 pub x1: u16,
119}
120
121impl DirtySpan {
122 #[inline]
123 pub const fn new(x0: u16, x1: u16) -> Self {
124 Self { x0, x1 }
125 }
126
127 #[inline]
128 pub const fn len(self) -> usize {
129 self.x1.saturating_sub(self.x0) as usize
130 }
131}
132
133#[derive(Debug, Default, Clone)]
134pub(crate) struct DirtySpanRow {
135 overflow: bool,
136 spans: Vec<DirtySpan>,
137}
138
139impl DirtySpanRow {
140 #[inline]
141 fn new_full() -> Self {
142 Self {
143 overflow: true,
144 spans: Vec::new(),
145 }
146 }
147
148 #[inline]
149 fn clear(&mut self) {
150 self.overflow = false;
151 self.spans.clear();
152 }
153
154 #[inline]
155 fn set_full(&mut self) {
156 self.overflow = true;
157 self.spans.clear();
158 }
159
160 #[inline]
161 pub(crate) fn spans(&self) -> &[DirtySpan] {
162 &self.spans
163 }
164
165 #[inline]
166 pub(crate) fn is_full(&self) -> bool {
167 self.overflow
168 }
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub struct DirtySpanStats {
174 pub rows_full_dirty: usize,
176 pub rows_with_spans: usize,
178 pub total_spans: usize,
180 pub overflows: usize,
182 pub span_coverage_cells: usize,
184 pub max_span_len: usize,
186 pub max_spans_per_row: usize,
188}
189
190#[derive(Debug, Clone)]
203pub struct Buffer {
204 width: u16,
205 height: u16,
206 cells: Vec<Cell>,
207 scissor_stack: Vec<Rect>,
208 opacity_stack: Vec<f32>,
209 pub degradation: DegradationLevel,
214 dirty_rows: Vec<bool>,
221 dirty_spans: Vec<DirtySpanRow>,
223 dirty_span_config: DirtySpanConfig,
225 dirty_span_overflows: usize,
227 dirty_bits: Vec<u8>,
229 dirty_cells: usize,
231 dirty_all: bool,
233}
234
235impl Buffer {
236 pub fn new(width: u16, height: u16) -> Self {
245 assert!(width > 0, "buffer width must be > 0");
246 assert!(height > 0, "buffer height must be > 0");
247
248 let size = width as usize * height as usize;
249 let cells = vec![Cell::default(); size];
250
251 let dirty_spans = (0..height)
252 .map(|_| DirtySpanRow::new_full())
253 .collect::<Vec<_>>();
254 let dirty_bits = vec![0u8; size];
255 let dirty_cells = size;
256 let dirty_all = true;
257
258 Self {
259 width,
260 height,
261 cells,
262 scissor_stack: vec![Rect::from_size(width, height)],
263 opacity_stack: vec![1.0],
264 degradation: DegradationLevel::Full,
265 dirty_rows: vec![true; height as usize],
268 dirty_spans,
270 dirty_span_config: DirtySpanConfig::default(),
271 dirty_span_overflows: 0,
272 dirty_bits,
273 dirty_cells,
274 dirty_all,
275 }
276 }
277
278 #[inline]
280 pub const fn width(&self) -> u16 {
281 self.width
282 }
283
284 #[inline]
286 pub const fn height(&self) -> u16 {
287 self.height
288 }
289
290 #[inline]
292 pub fn len(&self) -> usize {
293 self.cells.len()
294 }
295
296 #[inline]
298 pub fn is_empty(&self) -> bool {
299 self.cells.is_empty()
300 }
301
302 #[inline]
304 pub const fn bounds(&self) -> Rect {
305 Rect::from_size(self.width, self.height)
306 }
307
308 pub fn content_height(&self) -> u16 {
313 let default_cell = Cell::default();
314 let width = self.width as usize;
315 for y in (0..self.height).rev() {
316 let row_start = y as usize * width;
317 let row_end = row_start + width;
318 if self.cells[row_start..row_end]
319 .iter()
320 .any(|cell| *cell != default_cell)
321 {
322 return y + 1;
323 }
324 }
325 0
326 }
327
328 #[inline]
335 fn mark_dirty_row(&mut self, y: u16) {
336 if let Some(slot) = self.dirty_rows.get_mut(y as usize) {
337 *slot = true;
338 }
339 }
340
341 #[inline]
343 fn mark_dirty_bits_range(&mut self, y: u16, start: u16, end: u16) {
344 if self.dirty_all {
345 return;
346 }
347 if y >= self.height {
348 return;
349 }
350
351 let width = self.width;
352 if start >= width {
353 return;
354 }
355 let end = end.min(width);
356 if start >= end {
357 return;
358 }
359
360 let row_start = y as usize * width as usize;
361 for x in start..end {
362 let idx = row_start + x as usize;
363 let slot = &mut self.dirty_bits[idx];
364 if *slot == 0 {
365 *slot = 1;
366 self.dirty_cells = self.dirty_cells.saturating_add(1);
367 }
368 }
369 }
370
371 #[inline]
373 fn mark_dirty_bits_row(&mut self, y: u16) {
374 self.mark_dirty_bits_range(y, 0, self.width);
375 }
376
377 #[inline]
379 fn mark_dirty_row_full(&mut self, y: u16) {
380 self.mark_dirty_row(y);
381 if self.dirty_span_config.enabled
382 && let Some(row) = self.dirty_spans.get_mut(y as usize)
383 {
384 row.set_full();
385 }
386 self.mark_dirty_bits_row(y);
387 }
388
389 #[inline]
391 fn mark_dirty_span(&mut self, y: u16, x0: u16, x1: u16) {
392 self.mark_dirty_row(y);
393 let width = self.width;
394 let (start, mut end) = if x0 <= x1 { (x0, x1) } else { (x1, x0) };
395 if start >= width {
396 return;
397 }
398 if end > width {
399 end = width;
400 }
401 if start >= end {
402 return;
403 }
404
405 self.mark_dirty_bits_range(y, start, end);
406
407 if !self.dirty_span_config.enabled {
408 return;
409 }
410
411 let guard_band = self.dirty_span_config.guard_band;
412 let span_start = start.saturating_sub(guard_band);
413 let mut span_end = end.saturating_add(guard_band);
414 if span_end > width {
415 span_end = width;
416 }
417 if span_start >= span_end {
418 return;
419 }
420
421 let Some(row) = self.dirty_spans.get_mut(y as usize) else {
422 return;
423 };
424
425 if row.is_full() {
426 return;
427 }
428
429 let new_span = DirtySpan::new(span_start, span_end);
430 let spans = &mut row.spans;
431 let insert_at = spans.partition_point(|span| span.x0 <= new_span.x0);
432 spans.insert(insert_at, new_span);
433
434 let merge_gap = self.dirty_span_config.merge_gap;
436 let mut i = if insert_at > 0 { insert_at - 1 } else { 0 };
437 while i + 1 < spans.len() {
438 let current = spans[i];
439 let next = spans[i + 1];
440 let merge_limit = current.x1.saturating_add(merge_gap);
441 if merge_limit >= next.x0 {
442 spans[i].x1 = current.x1.max(next.x1);
443 spans.remove(i + 1);
444 continue;
445 }
446 i += 1;
447 }
448
449 if spans.len() > self.dirty_span_config.max_spans_per_row {
450 row.set_full();
451 self.dirty_span_overflows = self.dirty_span_overflows.saturating_add(1);
452 }
453 }
454
455 #[inline]
457 pub fn mark_all_dirty(&mut self) {
458 self.dirty_rows.fill(true);
459 if self.dirty_span_config.enabled {
460 for row in &mut self.dirty_spans {
461 row.set_full();
462 }
463 } else {
464 for row in &mut self.dirty_spans {
465 row.clear();
466 }
467 }
468 self.dirty_all = true;
469 self.dirty_cells = self.cells.len();
470 }
471
472 #[inline]
476 pub fn clear_dirty(&mut self) {
477 self.dirty_rows.fill(false);
478 for row in &mut self.dirty_spans {
479 row.clear();
480 }
481 self.dirty_span_overflows = 0;
482 self.dirty_bits.fill(0);
483 self.dirty_cells = 0;
484 self.dirty_all = false;
485 }
486
487 #[inline]
489 pub fn is_row_dirty(&self, y: u16) -> bool {
490 self.dirty_rows.get(y as usize).copied().unwrap_or(false)
491 }
492
493 #[inline]
498 pub fn dirty_rows(&self) -> &[bool] {
499 &self.dirty_rows
500 }
501
502 pub fn dirty_row_count(&self) -> usize {
504 self.dirty_rows.iter().filter(|&&d| d).count()
505 }
506
507 #[inline]
509 #[allow(dead_code)]
510 pub(crate) fn dirty_bits(&self) -> &[u8] {
511 &self.dirty_bits
512 }
513
514 #[inline]
516 #[allow(dead_code)]
517 pub(crate) fn dirty_cell_count(&self) -> usize {
518 self.dirty_cells
519 }
520
521 #[inline]
523 #[allow(dead_code)]
524 pub(crate) fn dirty_all(&self) -> bool {
525 self.dirty_all
526 }
527
528 #[inline]
530 #[allow(dead_code)]
531 pub(crate) fn dirty_span_row(&self, y: u16) -> Option<&DirtySpanRow> {
532 if !self.dirty_span_config.enabled {
533 return None;
534 }
535 self.dirty_spans.get(y as usize)
536 }
537
538 pub fn dirty_span_stats(&self) -> DirtySpanStats {
540 if !self.dirty_span_config.enabled {
541 return DirtySpanStats {
542 rows_full_dirty: 0,
543 rows_with_spans: 0,
544 total_spans: 0,
545 overflows: 0,
546 span_coverage_cells: 0,
547 max_span_len: 0,
548 max_spans_per_row: self.dirty_span_config.max_spans_per_row,
549 };
550 }
551
552 let mut rows_full_dirty = 0usize;
553 let mut rows_with_spans = 0usize;
554 let mut total_spans = 0usize;
555 let mut span_coverage_cells = 0usize;
556 let mut max_span_len = 0usize;
557
558 for row in &self.dirty_spans {
559 if row.is_full() {
560 rows_full_dirty += 1;
561 span_coverage_cells += self.width as usize;
562 max_span_len = max_span_len.max(self.width as usize);
563 continue;
564 }
565 if !row.spans().is_empty() {
566 rows_with_spans += 1;
567 }
568 total_spans += row.spans().len();
569 for span in row.spans() {
570 span_coverage_cells += span.len();
571 max_span_len = max_span_len.max(span.len());
572 }
573 }
574
575 DirtySpanStats {
576 rows_full_dirty,
577 rows_with_spans,
578 total_spans,
579 overflows: self.dirty_span_overflows,
580 span_coverage_cells,
581 max_span_len,
582 max_spans_per_row: self.dirty_span_config.max_spans_per_row,
583 }
584 }
585
586 pub fn dirty_span_config(&self) -> DirtySpanConfig {
588 self.dirty_span_config
589 }
590
591 pub fn set_dirty_span_config(&mut self, config: DirtySpanConfig) {
593 if self.dirty_span_config == config {
594 return;
595 }
596 self.dirty_span_config = config;
597 for row in &mut self.dirty_spans {
598 row.clear();
599 }
600 self.dirty_span_overflows = 0;
601 }
602
603 #[inline]
609 fn index(&self, x: u16, y: u16) -> Option<usize> {
610 if x < self.width && y < self.height {
611 Some(y as usize * self.width as usize + x as usize)
612 } else {
613 None
614 }
615 }
616
617 #[inline]
623 fn index_unchecked(&self, x: u16, y: u16) -> usize {
624 debug_assert!(x < self.width && y < self.height);
625 y as usize * self.width as usize + x as usize
626 }
627
628 #[inline]
632 pub fn get(&self, x: u16, y: u16) -> Option<&Cell> {
633 self.index(x, y).map(|i| &self.cells[i])
634 }
635
636 #[inline]
641 pub fn get_mut(&mut self, x: u16, y: u16) -> Option<&mut Cell> {
642 let idx = self.index(x, y)?;
643 self.mark_dirty_span(y, x, x.saturating_add(1));
644 Some(&mut self.cells[idx])
645 }
646
647 #[inline]
654 pub fn get_unchecked(&self, x: u16, y: u16) -> &Cell {
655 let i = self.index_unchecked(x, y);
656 &self.cells[i]
657 }
658
659 fn cleanup_overlap(&mut self, x: u16, y: u16, new_cell: &Cell) -> Option<DirtySpan> {
663 let idx = self.index(x, y)?;
664 let current = self.cells[idx];
665 let mut touched = false;
666 let mut min_x = x;
667 let mut max_x = x;
668
669 if current.content.width() > 1 {
671 let width = current.content.width();
672 for i in 1..width {
677 let Some(cx) = x.checked_add(i as u16) else {
678 break;
679 };
680 if let Some(tail_idx) = self.index(cx, y)
681 && self.cells[tail_idx].is_continuation()
682 {
683 self.cells[tail_idx] = Cell::default();
684 touched = true;
685 min_x = min_x.min(cx);
686 max_x = max_x.max(cx);
687 }
688 }
689 }
690 else if current.is_continuation() && !new_cell.is_continuation() {
692 let mut back_x = x;
693 while back_x > 0 {
694 back_x -= 1;
695 if let Some(h_idx) = self.index(back_x, y) {
696 let h_cell = self.cells[h_idx];
697 if !h_cell.is_continuation() {
698 let width = h_cell.content.width();
700 if (back_x as usize + width) > x as usize {
701 self.cells[h_idx] = Cell::default();
704 touched = true;
705 min_x = min_x.min(back_x);
706 max_x = max_x.max(back_x);
707
708 for i in 1..width {
711 let Some(cx) = back_x.checked_add(i as u16) else {
712 break;
713 };
714 if let Some(tail_idx) = self.index(cx, y) {
715 if self.cells[tail_idx].is_continuation() {
718 self.cells[tail_idx] = Cell::default();
719 touched = true;
720 min_x = min_x.min(cx);
721 max_x = max_x.max(cx);
722 }
723 }
724 }
725 }
726 break;
727 }
728 }
729 }
730 }
731
732 if touched {
733 Some(DirtySpan::new(min_x, max_x.saturating_add(1)))
734 } else {
735 None
736 }
737 }
738
739 #[inline]
751 pub fn set(&mut self, x: u16, y: u16, cell: Cell) {
752 let width = cell.content.width();
753
754 if width <= 1 {
756 let Some(idx) = self.index(x, y) else {
758 return;
759 };
760
761 if !self.current_scissor().contains(x, y) {
763 return;
764 }
765
766 let mut span_start = x;
768 let mut span_end = x.saturating_add(1);
769 if let Some(span) = self.cleanup_overlap(x, y, &cell) {
770 span_start = span_start.min(span.x0);
771 span_end = span_end.max(span.x1);
772 }
773
774 let existing_bg = self.cells[idx].bg;
775
776 let mut final_cell = if self.current_opacity() < 1.0 {
778 let opacity = self.current_opacity();
779 Cell {
780 fg: cell.fg.with_opacity(opacity),
781 bg: cell.bg.with_opacity(opacity),
782 ..cell
783 }
784 } else {
785 cell
786 };
787
788 final_cell.bg = final_cell.bg.over(existing_bg);
789
790 self.cells[idx] = final_cell;
791 self.mark_dirty_span(y, span_start, span_end);
792 return;
793 }
794
795 let scissor = self.current_scissor();
798 for i in 0..width {
799 let Some(cx) = x.checked_add(i as u16) else {
800 return;
801 };
802 if cx >= self.width || y >= self.height {
804 return;
805 }
806 if !scissor.contains(cx, y) {
808 return;
809 }
810 }
811
812 let mut span_start = x;
816 let mut span_end = x.saturating_add(width as u16);
817 if let Some(span) = self.cleanup_overlap(x, y, &cell) {
818 span_start = span_start.min(span.x0);
819 span_end = span_end.max(span.x1);
820 }
821 for i in 1..width {
822 if let Some(span) = self.cleanup_overlap(x + i as u16, y, &Cell::CONTINUATION) {
824 span_start = span_start.min(span.x0);
825 span_end = span_end.max(span.x1);
826 }
827 }
828
829 let idx = self.index_unchecked(x, y);
831 let old_cell = self.cells[idx];
832 let mut final_cell = if self.current_opacity() < 1.0 {
833 let opacity = self.current_opacity();
834 Cell {
835 fg: cell.fg.with_opacity(opacity),
836 bg: cell.bg.with_opacity(opacity),
837 ..cell
838 }
839 } else {
840 cell
841 };
842
843 final_cell.bg = final_cell.bg.over(old_cell.bg);
845
846 self.cells[idx] = final_cell;
847
848 for i in 1..width {
851 let idx = self.index_unchecked(x + i as u16, y);
852 self.cells[idx] = Cell::CONTINUATION;
853 }
854 self.mark_dirty_span(y, span_start, span_end);
855 }
856
857 #[inline]
862 pub fn set_raw(&mut self, x: u16, y: u16, cell: Cell) {
863 if let Some(idx) = self.index(x, y) {
864 self.cells[idx] = cell;
865 self.mark_dirty_span(y, x, x.saturating_add(1));
866 }
867 }
868
869 pub fn fill(&mut self, rect: Rect, cell: Cell) {
873 let clipped = self.current_scissor().intersection(&rect);
874 if clipped.is_empty() {
875 return;
876 }
877
878 let cell_width = cell.content.width();
881 if cell_width <= 1
882 && !cell.is_continuation()
883 && self.current_opacity() >= 1.0
884 && cell.bg.a() == 255
885 && clipped.x == 0
886 && clipped.width == self.width
887 {
888 let row_width = self.width as usize;
889 for y in clipped.y..clipped.bottom() {
890 let row_start = y as usize * row_width;
891 let row_end = row_start + row_width;
892 self.cells[row_start..row_end].fill(cell);
893 self.mark_dirty_row_full(y);
894 }
895 return;
896 }
897
898 for y in clipped.y..clipped.bottom() {
899 for x in clipped.x..clipped.right() {
900 self.set(x, y, cell);
901 }
902 }
903 }
904
905 pub fn clear(&mut self) {
907 self.cells.fill(Cell::default());
908 self.mark_all_dirty();
909 }
910
911 pub fn reset_for_frame(&mut self) {
916 self.scissor_stack.truncate(1);
917 if let Some(base) = self.scissor_stack.first_mut() {
918 *base = Rect::from_size(self.width, self.height);
919 } else {
920 self.scissor_stack
921 .push(Rect::from_size(self.width, self.height));
922 }
923
924 self.opacity_stack.truncate(1);
925 if let Some(base) = self.opacity_stack.first_mut() {
926 *base = 1.0;
927 } else {
928 self.opacity_stack.push(1.0);
929 }
930
931 self.clear();
932 }
933
934 pub fn clear_with(&mut self, cell: Cell) {
936 self.cells.fill(cell);
937 self.mark_all_dirty();
938 }
939
940 #[inline]
944 pub fn cells(&self) -> &[Cell] {
945 &self.cells
946 }
947
948 #[inline]
952 pub fn cells_mut(&mut self) -> &mut [Cell] {
953 self.mark_all_dirty();
954 &mut self.cells
955 }
956
957 #[inline]
963 pub fn row_cells(&self, y: u16) -> &[Cell] {
964 let start = y as usize * self.width as usize;
965 &self.cells[start..start + self.width as usize]
966 }
967
968 pub fn push_scissor(&mut self, rect: Rect) {
975 let current = self.current_scissor();
976 let intersected = current.intersection(&rect);
977 self.scissor_stack.push(intersected);
978 }
979
980 pub fn pop_scissor(&mut self) {
984 if self.scissor_stack.len() > 1 {
985 self.scissor_stack.pop();
986 }
987 }
988
989 #[inline]
991 pub fn current_scissor(&self) -> Rect {
992 *self.scissor_stack.last().unwrap()
994 }
995
996 #[inline]
998 pub fn scissor_depth(&self) -> usize {
999 self.scissor_stack.len()
1000 }
1001
1002 pub fn push_opacity(&mut self, opacity: f32) {
1009 let clamped = opacity.clamp(0.0, 1.0);
1010 let current = self.current_opacity();
1011 self.opacity_stack.push(current * clamped);
1012 }
1013
1014 pub fn pop_opacity(&mut self) {
1018 if self.opacity_stack.len() > 1 {
1019 self.opacity_stack.pop();
1020 }
1021 }
1022
1023 #[inline]
1025 pub fn current_opacity(&self) -> f32 {
1026 *self.opacity_stack.last().unwrap()
1028 }
1029
1030 #[inline]
1032 pub fn opacity_depth(&self) -> usize {
1033 self.opacity_stack.len()
1034 }
1035
1036 pub fn copy_from(&mut self, src: &Buffer, src_rect: Rect, dst_x: u16, dst_y: u16) {
1043 let copy_bounds = Rect::new(dst_x, dst_y, src_rect.width, src_rect.height);
1046 self.push_scissor(copy_bounds);
1047
1048 for dy in 0..src_rect.height {
1049 let Some(target_y) = dst_y.checked_add(dy) else {
1051 continue;
1052 };
1053 let Some(sy) = src_rect.y.checked_add(dy) else {
1054 continue;
1055 };
1056
1057 let mut dx = 0u16;
1058 while dx < src_rect.width {
1059 let Some(target_x) = dst_x.checked_add(dx) else {
1061 dx = dx.saturating_add(1);
1062 continue;
1063 };
1064 let Some(sx) = src_rect.x.checked_add(dx) else {
1065 dx = dx.saturating_add(1);
1066 continue;
1067 };
1068
1069 if let Some(cell) = src.get(sx, sy) {
1070 if cell.is_continuation() {
1074 self.set(target_x, target_y, Cell::default());
1075 dx = dx.saturating_add(1);
1076 continue;
1077 }
1078
1079 self.set(target_x, target_y, *cell);
1081
1082 let width = cell.content.width();
1086 if width > 1 {
1087 dx = dx.saturating_add(width as u16);
1088 } else {
1089 dx = dx.saturating_add(1);
1090 }
1091 } else {
1092 dx = dx.saturating_add(1);
1093 }
1094 }
1095 }
1096
1097 self.pop_scissor();
1098 }
1099
1100 pub fn content_eq(&self, other: &Buffer) -> bool {
1102 self.width == other.width && self.height == other.height && self.cells == other.cells
1103 }
1104}
1105
1106impl Default for Buffer {
1107 fn default() -> Self {
1109 Self::new(1, 1)
1110 }
1111}
1112
1113impl PartialEq for Buffer {
1114 fn eq(&self, other: &Self) -> bool {
1115 self.content_eq(other)
1116 }
1117}
1118
1119impl Eq for Buffer {}
1120
1121#[derive(Debug)]
1140pub struct DoubleBuffer {
1141 buffers: [Buffer; 2],
1142 current_idx: u8,
1144}
1145
1146const ADAPTIVE_GROWTH_FACTOR: f32 = 1.25;
1152
1153const ADAPTIVE_SHRINK_THRESHOLD: f32 = 0.50;
1156
1157const ADAPTIVE_MAX_OVERAGE: u16 = 200;
1159
1160#[derive(Debug)]
1192pub struct AdaptiveDoubleBuffer {
1193 inner: DoubleBuffer,
1195 logical_width: u16,
1197 logical_height: u16,
1199 capacity_width: u16,
1201 capacity_height: u16,
1203 stats: AdaptiveStats,
1205}
1206
1207#[derive(Debug, Clone, Default)]
1209pub struct AdaptiveStats {
1210 pub resize_avoided: u64,
1212 pub resize_reallocated: u64,
1214 pub resize_growth: u64,
1216 pub resize_shrink: u64,
1218}
1219
1220impl AdaptiveStats {
1221 pub fn reset(&mut self) {
1223 *self = Self::default();
1224 }
1225
1226 pub fn avoidance_ratio(&self) -> f64 {
1228 let total = self.resize_avoided + self.resize_reallocated;
1229 if total == 0 {
1230 1.0
1231 } else {
1232 self.resize_avoided as f64 / total as f64
1233 }
1234 }
1235}
1236
1237impl DoubleBuffer {
1238 pub fn new(width: u16, height: u16) -> Self {
1246 Self {
1247 buffers: [Buffer::new(width, height), Buffer::new(width, height)],
1248 current_idx: 0,
1249 }
1250 }
1251
1252 #[inline]
1257 pub fn swap(&mut self) {
1258 self.current_idx = 1 - self.current_idx;
1259 }
1260
1261 #[inline]
1263 pub fn current(&self) -> &Buffer {
1264 &self.buffers[self.current_idx as usize]
1265 }
1266
1267 #[inline]
1269 pub fn current_mut(&mut self) -> &mut Buffer {
1270 &mut self.buffers[self.current_idx as usize]
1271 }
1272
1273 #[inline]
1275 pub fn previous(&self) -> &Buffer {
1276 &self.buffers[(1 - self.current_idx) as usize]
1277 }
1278
1279 #[inline]
1281 pub fn previous_mut(&mut self) -> &mut Buffer {
1282 &mut self.buffers[(1 - self.current_idx) as usize]
1283 }
1284
1285 #[inline]
1287 pub fn width(&self) -> u16 {
1288 self.buffers[0].width()
1289 }
1290
1291 #[inline]
1293 pub fn height(&self) -> u16 {
1294 self.buffers[0].height()
1295 }
1296
1297 pub fn resize(&mut self, width: u16, height: u16) -> bool {
1302 if self.buffers[0].width() == width && self.buffers[0].height() == height {
1303 return false;
1304 }
1305 self.buffers = [Buffer::new(width, height), Buffer::new(width, height)];
1306 self.current_idx = 0;
1307 true
1308 }
1309
1310 #[inline]
1312 pub fn dimensions_match(&self, width: u16, height: u16) -> bool {
1313 self.buffers[0].width() == width && self.buffers[0].height() == height
1314 }
1315}
1316
1317impl AdaptiveDoubleBuffer {
1322 pub fn new(width: u16, height: u16) -> Self {
1330 let (cap_w, cap_h) = Self::compute_capacity(width, height);
1331 Self {
1332 inner: DoubleBuffer::new(cap_w, cap_h),
1333 logical_width: width,
1334 logical_height: height,
1335 capacity_width: cap_w,
1336 capacity_height: cap_h,
1337 stats: AdaptiveStats::default(),
1338 }
1339 }
1340
1341 fn compute_capacity(width: u16, height: u16) -> (u16, u16) {
1345 let extra_w =
1346 ((width as f32 * (ADAPTIVE_GROWTH_FACTOR - 1.0)) as u16).min(ADAPTIVE_MAX_OVERAGE);
1347 let extra_h =
1348 ((height as f32 * (ADAPTIVE_GROWTH_FACTOR - 1.0)) as u16).min(ADAPTIVE_MAX_OVERAGE);
1349
1350 let cap_w = width.saturating_add(extra_w);
1351 let cap_h = height.saturating_add(extra_h);
1352
1353 (cap_w, cap_h)
1354 }
1355
1356 fn needs_reallocation(&self, width: u16, height: u16) -> bool {
1360 if width > self.capacity_width || height > self.capacity_height {
1362 return true;
1363 }
1364
1365 let shrink_threshold_w = (self.capacity_width as f32 * ADAPTIVE_SHRINK_THRESHOLD) as u16;
1367 let shrink_threshold_h = (self.capacity_height as f32 * ADAPTIVE_SHRINK_THRESHOLD) as u16;
1368
1369 width < shrink_threshold_w || height < shrink_threshold_h
1370 }
1371
1372 #[inline]
1377 pub fn swap(&mut self) {
1378 self.inner.swap();
1379 }
1380
1381 #[inline]
1386 pub fn current(&self) -> &Buffer {
1387 self.inner.current()
1388 }
1389
1390 #[inline]
1392 pub fn current_mut(&mut self) -> &mut Buffer {
1393 self.inner.current_mut()
1394 }
1395
1396 #[inline]
1398 pub fn previous(&self) -> &Buffer {
1399 self.inner.previous()
1400 }
1401
1402 #[inline]
1404 pub fn width(&self) -> u16 {
1405 self.logical_width
1406 }
1407
1408 #[inline]
1410 pub fn height(&self) -> u16 {
1411 self.logical_height
1412 }
1413
1414 #[inline]
1416 pub fn capacity_width(&self) -> u16 {
1417 self.capacity_width
1418 }
1419
1420 #[inline]
1422 pub fn capacity_height(&self) -> u16 {
1423 self.capacity_height
1424 }
1425
1426 #[inline]
1428 pub fn stats(&self) -> &AdaptiveStats {
1429 &self.stats
1430 }
1431
1432 pub fn reset_stats(&mut self) {
1434 self.stats.reset();
1435 }
1436
1437 pub fn resize(&mut self, width: u16, height: u16) -> bool {
1449 if width == self.logical_width && height == self.logical_height {
1451 return false;
1452 }
1453
1454 let is_growth = width > self.logical_width || height > self.logical_height;
1455 if is_growth {
1456 self.stats.resize_growth += 1;
1457 } else {
1458 self.stats.resize_shrink += 1;
1459 }
1460
1461 if self.needs_reallocation(width, height) {
1462 let (cap_w, cap_h) = Self::compute_capacity(width, height);
1464 self.inner = DoubleBuffer::new(cap_w, cap_h);
1465 self.capacity_width = cap_w;
1466 self.capacity_height = cap_h;
1467 self.stats.resize_reallocated += 1;
1468 } else {
1469 self.inner.current_mut().clear();
1472 self.inner.previous_mut().clear();
1473 self.stats.resize_avoided += 1;
1474 }
1475
1476 self.logical_width = width;
1477 self.logical_height = height;
1478 true
1479 }
1480
1481 #[inline]
1483 pub fn dimensions_match(&self, width: u16, height: u16) -> bool {
1484 self.logical_width == width && self.logical_height == height
1485 }
1486
1487 #[inline]
1489 pub fn logical_bounds(&self) -> Rect {
1490 Rect::from_size(self.logical_width, self.logical_height)
1491 }
1492
1493 pub fn memory_efficiency(&self) -> f64 {
1495 let logical = self.logical_width as u64 * self.logical_height as u64;
1496 let capacity = self.capacity_width as u64 * self.capacity_height as u64;
1497 if capacity == 0 {
1498 1.0
1499 } else {
1500 logical as f64 / capacity as f64
1501 }
1502 }
1503}
1504
1505#[cfg(test)]
1506mod tests {
1507 use super::*;
1508 use crate::cell::PackedRgba;
1509
1510 #[test]
1511 fn set_composites_background() {
1512 let mut buf = Buffer::new(1, 1);
1513
1514 let red = PackedRgba::rgb(255, 0, 0);
1516 buf.set(0, 0, Cell::default().with_bg(red));
1517
1518 let cell = Cell::from_char('X'); buf.set(0, 0, cell);
1521
1522 let result = buf.get(0, 0).unwrap();
1523 assert_eq!(result.content.as_char(), Some('X'));
1524 assert_eq!(
1525 result.bg, red,
1526 "Background should be preserved (composited)"
1527 );
1528 }
1529
1530 #[test]
1531 fn rect_contains() {
1532 let r = Rect::new(5, 5, 10, 10);
1533 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)); }
1539
1540 #[test]
1541 fn rect_intersection() {
1542 let a = Rect::new(0, 0, 10, 10);
1543 let b = Rect::new(5, 5, 10, 10);
1544 let i = a.intersection(&b);
1545 assert_eq!(i, Rect::new(5, 5, 5, 5));
1546
1547 let c = Rect::new(20, 20, 5, 5);
1549 assert_eq!(a.intersection(&c), Rect::default());
1550 }
1551
1552 #[test]
1553 fn buffer_creation() {
1554 let buf = Buffer::new(80, 24);
1555 assert_eq!(buf.width(), 80);
1556 assert_eq!(buf.height(), 24);
1557 assert_eq!(buf.len(), 80 * 24);
1558 }
1559
1560 #[test]
1561 fn content_height_empty_is_zero() {
1562 let buf = Buffer::new(8, 4);
1563 assert_eq!(buf.content_height(), 0);
1564 }
1565
1566 #[test]
1567 fn content_height_tracks_last_non_empty_row() {
1568 let mut buf = Buffer::new(5, 4);
1569 buf.set(0, 0, Cell::from_char('A'));
1570 assert_eq!(buf.content_height(), 1);
1571
1572 buf.set(2, 3, Cell::from_char('Z'));
1573 assert_eq!(buf.content_height(), 4);
1574 }
1575
1576 #[test]
1577 #[should_panic(expected = "width must be > 0")]
1578 fn buffer_zero_width_panics() {
1579 Buffer::new(0, 24);
1580 }
1581
1582 #[test]
1583 #[should_panic(expected = "height must be > 0")]
1584 fn buffer_zero_height_panics() {
1585 Buffer::new(80, 0);
1586 }
1587
1588 #[test]
1589 fn buffer_get_and_set() {
1590 let mut buf = Buffer::new(10, 10);
1591 let cell = Cell::from_char('X');
1592 buf.set(5, 5, cell);
1593 assert_eq!(buf.get(5, 5).unwrap().content.as_char(), Some('X'));
1594 }
1595
1596 #[test]
1597 fn buffer_out_of_bounds_get() {
1598 let buf = Buffer::new(10, 10);
1599 assert!(buf.get(10, 0).is_none());
1600 assert!(buf.get(0, 10).is_none());
1601 assert!(buf.get(100, 100).is_none());
1602 }
1603
1604 #[test]
1605 fn buffer_out_of_bounds_set_ignored() {
1606 let mut buf = Buffer::new(10, 10);
1607 buf.set(100, 100, Cell::from_char('X')); assert_eq!(buf.cells().iter().filter(|c| !c.is_empty()).count(), 0);
1609 }
1610
1611 #[test]
1612 fn buffer_clear() {
1613 let mut buf = Buffer::new(10, 10);
1614 buf.set(5, 5, Cell::from_char('X'));
1615 buf.clear();
1616 assert!(buf.get(5, 5).unwrap().is_empty());
1617 }
1618
1619 #[test]
1620 fn scissor_stack_basic() {
1621 let mut buf = Buffer::new(20, 20);
1622
1623 assert_eq!(buf.current_scissor(), Rect::from_size(20, 20));
1625 assert_eq!(buf.scissor_depth(), 1);
1626
1627 buf.push_scissor(Rect::new(5, 5, 10, 10));
1629 assert_eq!(buf.current_scissor(), Rect::new(5, 5, 10, 10));
1630 assert_eq!(buf.scissor_depth(), 2);
1631
1632 buf.set(7, 7, Cell::from_char('I'));
1634 assert_eq!(buf.get(7, 7).unwrap().content.as_char(), Some('I'));
1635
1636 buf.set(0, 0, Cell::from_char('O'));
1638 assert!(buf.get(0, 0).unwrap().is_empty());
1639
1640 buf.pop_scissor();
1642 assert_eq!(buf.current_scissor(), Rect::from_size(20, 20));
1643 assert_eq!(buf.scissor_depth(), 1);
1644
1645 buf.set(0, 0, Cell::from_char('N'));
1647 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('N'));
1648 }
1649
1650 #[test]
1651 fn scissor_intersection() {
1652 let mut buf = Buffer::new(20, 20);
1653 buf.push_scissor(Rect::new(5, 5, 10, 10));
1654 buf.push_scissor(Rect::new(8, 8, 10, 10));
1655
1656 assert_eq!(buf.current_scissor(), Rect::new(8, 8, 7, 7));
1659 }
1660
1661 #[test]
1662 fn scissor_base_cannot_be_popped() {
1663 let mut buf = Buffer::new(10, 10);
1664 buf.pop_scissor(); assert_eq!(buf.scissor_depth(), 1);
1666 buf.pop_scissor(); assert_eq!(buf.scissor_depth(), 1);
1668 }
1669
1670 #[test]
1671 fn opacity_stack_basic() {
1672 let mut buf = Buffer::new(10, 10);
1673
1674 assert!((buf.current_opacity() - 1.0).abs() < f32::EPSILON);
1676 assert_eq!(buf.opacity_depth(), 1);
1677
1678 buf.push_opacity(0.5);
1680 assert!((buf.current_opacity() - 0.5).abs() < f32::EPSILON);
1681 assert_eq!(buf.opacity_depth(), 2);
1682
1683 buf.push_opacity(0.5);
1685 assert!((buf.current_opacity() - 0.25).abs() < f32::EPSILON);
1686 assert_eq!(buf.opacity_depth(), 3);
1687
1688 buf.pop_opacity();
1690 assert!((buf.current_opacity() - 0.5).abs() < f32::EPSILON);
1691 }
1692
1693 #[test]
1694 fn opacity_applied_to_cells() {
1695 let mut buf = Buffer::new(10, 10);
1696 buf.push_opacity(0.5);
1697
1698 let cell = Cell::from_char('X').with_fg(PackedRgba::rgba(100, 100, 100, 255));
1699 buf.set(5, 5, cell);
1700
1701 let stored = buf.get(5, 5).unwrap();
1702 assert_eq!(stored.fg.a(), 128);
1704 }
1705
1706 #[test]
1707 fn opacity_composites_background_before_storage() {
1708 let mut buf = Buffer::new(1, 1);
1709
1710 let red = PackedRgba::rgb(255, 0, 0);
1711 let blue = PackedRgba::rgb(0, 0, 255);
1712
1713 buf.set(0, 0, Cell::default().with_bg(red));
1714 buf.push_opacity(0.5);
1715 buf.set(0, 0, Cell::default().with_bg(blue));
1716
1717 let stored = buf.get(0, 0).unwrap();
1718 let expected = blue.with_opacity(0.5).over(red);
1719 assert_eq!(stored.bg, expected);
1720 }
1721
1722 #[test]
1723 fn opacity_clamped() {
1724 let mut buf = Buffer::new(10, 10);
1725 buf.push_opacity(2.0); assert!((buf.current_opacity() - 1.0).abs() < f32::EPSILON);
1727
1728 buf.push_opacity(-1.0); assert!((buf.current_opacity() - 0.0).abs() < f32::EPSILON);
1730 }
1731
1732 #[test]
1733 fn opacity_base_cannot_be_popped() {
1734 let mut buf = Buffer::new(10, 10);
1735 buf.pop_opacity(); assert_eq!(buf.opacity_depth(), 1);
1737 }
1738
1739 #[test]
1740 fn buffer_fill() {
1741 let mut buf = Buffer::new(10, 10);
1742 let cell = Cell::from_char('#');
1743 buf.fill(Rect::new(2, 2, 5, 5), cell);
1744
1745 assert_eq!(buf.get(3, 3).unwrap().content.as_char(), Some('#'));
1747
1748 assert!(buf.get(0, 0).unwrap().is_empty());
1750 }
1751
1752 #[test]
1753 fn buffer_fill_respects_scissor() {
1754 let mut buf = Buffer::new(10, 10);
1755 buf.push_scissor(Rect::new(3, 3, 4, 4));
1756
1757 let cell = Cell::from_char('#');
1758 buf.fill(Rect::new(0, 0, 10, 10), cell);
1759
1760 assert_eq!(buf.get(3, 3).unwrap().content.as_char(), Some('#'));
1762 assert!(buf.get(0, 0).unwrap().is_empty());
1763 assert!(buf.get(7, 7).unwrap().is_empty());
1764 }
1765
1766 #[test]
1767 fn buffer_copy_from() {
1768 let mut src = Buffer::new(10, 10);
1769 src.set(2, 2, Cell::from_char('S'));
1770
1771 let mut dst = Buffer::new(10, 10);
1772 dst.copy_from(&src, Rect::new(0, 0, 5, 5), 3, 3);
1773
1774 assert_eq!(dst.get(5, 5).unwrap().content.as_char(), Some('S'));
1776 }
1777
1778 #[test]
1779 fn copy_from_clips_wide_char_at_boundary() {
1780 let mut src = Buffer::new(10, 1);
1781 src.set(0, 0, Cell::from_char('中'));
1783
1784 let mut dst = Buffer::new(10, 1);
1785 dst.copy_from(&src, Rect::new(0, 0, 1, 1), 0, 0);
1788
1789 assert!(
1798 dst.get(0, 0).unwrap().is_empty(),
1799 "Wide char head should not be written if tail is clipped"
1800 );
1801 assert!(
1802 dst.get(1, 0).unwrap().is_empty(),
1803 "Wide char tail should not be leaked outside copy region"
1804 );
1805 }
1806
1807 #[test]
1808 fn buffer_content_eq() {
1809 let mut buf1 = Buffer::new(10, 10);
1810 let mut buf2 = Buffer::new(10, 10);
1811
1812 assert!(buf1.content_eq(&buf2));
1813
1814 buf1.set(0, 0, Cell::from_char('X'));
1815 assert!(!buf1.content_eq(&buf2));
1816
1817 buf2.set(0, 0, Cell::from_char('X'));
1818 assert!(buf1.content_eq(&buf2));
1819 }
1820
1821 #[test]
1822 fn buffer_bounds() {
1823 let buf = Buffer::new(80, 24);
1824 let bounds = buf.bounds();
1825 assert_eq!(bounds.x, 0);
1826 assert_eq!(bounds.y, 0);
1827 assert_eq!(bounds.width, 80);
1828 assert_eq!(bounds.height, 24);
1829 }
1830
1831 #[test]
1832 fn buffer_set_raw_bypasses_scissor() {
1833 let mut buf = Buffer::new(10, 10);
1834 buf.push_scissor(Rect::new(5, 5, 5, 5));
1835
1836 buf.set(0, 0, Cell::from_char('S'));
1838 assert!(buf.get(0, 0).unwrap().is_empty());
1839
1840 buf.set_raw(0, 0, Cell::from_char('R'));
1842 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('R'));
1843 }
1844
1845 #[test]
1846 fn set_handles_wide_chars() {
1847 let mut buf = Buffer::new(10, 10);
1848
1849 buf.set(0, 0, Cell::from_char('中'));
1851
1852 let head = buf.get(0, 0).unwrap();
1854 assert_eq!(head.content.as_char(), Some('中'));
1855
1856 let cont = buf.get(1, 0).unwrap();
1858 assert!(cont.is_continuation());
1859 assert!(!cont.is_empty());
1860 }
1861
1862 #[test]
1863 fn set_handles_wide_chars_clipped() {
1864 let mut buf = Buffer::new(10, 10);
1865 buf.push_scissor(Rect::new(0, 0, 1, 10)); buf.set(0, 0, Cell::from_char('中'));
1870
1871 assert!(buf.get(0, 0).unwrap().is_empty());
1873 assert!(buf.get(1, 0).unwrap().is_empty());
1875 }
1876
1877 #[test]
1880 fn overwrite_wide_head_with_single_clears_tails() {
1881 let mut buf = Buffer::new(10, 1);
1882
1883 buf.set(0, 0, Cell::from_char('中'));
1885 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
1886 assert!(buf.get(1, 0).unwrap().is_continuation());
1887
1888 buf.set(0, 0, Cell::from_char('A'));
1890
1891 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('A'));
1893 assert!(
1895 buf.get(1, 0).unwrap().is_empty(),
1896 "Continuation at x=1 should be cleared when head is overwritten"
1897 );
1898 }
1899
1900 #[test]
1901 fn overwrite_continuation_with_single_clears_head_and_tails() {
1902 let mut buf = Buffer::new(10, 1);
1903
1904 buf.set(0, 0, Cell::from_char('中'));
1906 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
1907 assert!(buf.get(1, 0).unwrap().is_continuation());
1908
1909 buf.set(1, 0, Cell::from_char('B'));
1911
1912 assert!(
1914 buf.get(0, 0).unwrap().is_empty(),
1915 "Head at x=0 should be cleared when its continuation is overwritten"
1916 );
1917 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('B'));
1919 }
1920
1921 #[test]
1922 fn overwrite_wide_with_another_wide() {
1923 let mut buf = Buffer::new(10, 1);
1924
1925 buf.set(0, 0, Cell::from_char('中'));
1927 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
1928 assert!(buf.get(1, 0).unwrap().is_continuation());
1929
1930 buf.set(0, 0, Cell::from_char('日'));
1932
1933 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('日'));
1935 assert!(
1936 buf.get(1, 0).unwrap().is_continuation(),
1937 "Continuation should still exist for new wide char"
1938 );
1939 }
1940
1941 #[test]
1942 fn overwrite_continuation_middle_of_wide_sequence() {
1943 let mut buf = Buffer::new(10, 1);
1944
1945 buf.set(0, 0, Cell::from_char('中'));
1947 buf.set(2, 0, Cell::from_char('日'));
1948
1949 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
1950 assert!(buf.get(1, 0).unwrap().is_continuation());
1951 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('日'));
1952 assert!(buf.get(3, 0).unwrap().is_continuation());
1953
1954 buf.set(1, 0, Cell::from_char('X'));
1956
1957 assert!(
1959 buf.get(0, 0).unwrap().is_empty(),
1960 "Head of first wide char should be cleared"
1961 );
1962 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('X'));
1964 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('日'));
1966 assert!(buf.get(3, 0).unwrap().is_continuation());
1967 }
1968
1969 #[test]
1970 fn wide_char_overlapping_previous_wide_char() {
1971 let mut buf = Buffer::new(10, 1);
1972
1973 buf.set(0, 0, Cell::from_char('中'));
1975 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
1976 assert!(buf.get(1, 0).unwrap().is_continuation());
1977
1978 buf.set(1, 0, Cell::from_char('日'));
1980
1981 assert!(
1983 buf.get(0, 0).unwrap().is_empty(),
1984 "First wide char head should be cleared when continuation is overwritten by new wide"
1985 );
1986 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('日'));
1988 assert!(buf.get(2, 0).unwrap().is_continuation());
1989 }
1990
1991 #[test]
1992 fn wide_char_at_end_of_buffer_atomic_reject() {
1993 let mut buf = Buffer::new(5, 1);
1994
1995 buf.set(4, 0, Cell::from_char('中'));
1997
1998 assert!(
2000 buf.get(4, 0).unwrap().is_empty(),
2001 "Wide char should be rejected when tail would be out of bounds"
2002 );
2003 }
2004
2005 #[test]
2006 fn three_wide_chars_sequential_cleanup() {
2007 let mut buf = Buffer::new(10, 1);
2008
2009 buf.set(0, 0, Cell::from_char('一'));
2011 buf.set(2, 0, Cell::from_char('二'));
2012 buf.set(4, 0, Cell::from_char('三'));
2013
2014 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('一'));
2016 assert!(buf.get(1, 0).unwrap().is_continuation());
2017 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('二'));
2018 assert!(buf.get(3, 0).unwrap().is_continuation());
2019 assert_eq!(buf.get(4, 0).unwrap().content.as_char(), Some('三'));
2020 assert!(buf.get(5, 0).unwrap().is_continuation());
2021
2022 buf.set(3, 0, Cell::from_char('M'));
2024
2025 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('一'));
2027 assert!(buf.get(1, 0).unwrap().is_continuation());
2028 assert!(buf.get(2, 0).unwrap().is_empty());
2030 assert_eq!(buf.get(3, 0).unwrap().content.as_char(), Some('M'));
2032 assert_eq!(buf.get(4, 0).unwrap().content.as_char(), Some('三'));
2034 assert!(buf.get(5, 0).unwrap().is_continuation());
2035 }
2036
2037 #[test]
2038 fn overwrite_empty_cell_no_cleanup_needed() {
2039 let mut buf = Buffer::new(10, 1);
2040
2041 buf.set(5, 0, Cell::from_char('X'));
2043
2044 assert_eq!(buf.get(5, 0).unwrap().content.as_char(), Some('X'));
2045 assert!(buf.get(4, 0).unwrap().is_empty());
2047 assert!(buf.get(6, 0).unwrap().is_empty());
2048 }
2049
2050 #[test]
2051 fn wide_char_cleanup_with_opacity() {
2052 let mut buf = Buffer::new(10, 1);
2053
2054 buf.set(0, 0, Cell::default().with_bg(PackedRgba::rgb(255, 0, 0)));
2056 buf.set(1, 0, Cell::default().with_bg(PackedRgba::rgb(0, 255, 0)));
2057
2058 buf.set(0, 0, Cell::from_char('中'));
2060
2061 buf.push_opacity(0.5);
2063 buf.set(0, 0, Cell::from_char('A'));
2064 buf.pop_opacity();
2065
2066 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('A'));
2068 assert!(buf.get(1, 0).unwrap().is_empty());
2070 }
2071
2072 #[test]
2073 fn wide_char_continuation_not_treated_as_head() {
2074 let mut buf = Buffer::new(10, 1);
2075
2076 buf.set(0, 0, Cell::from_char('中'));
2078
2079 let cont = buf.get(1, 0).unwrap();
2081 assert!(cont.is_continuation());
2082 assert_eq!(cont.content.width(), 0);
2083
2084 buf.set(1, 0, Cell::from_char('日'));
2086
2087 assert!(buf.get(0, 0).unwrap().is_empty());
2089 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('日'));
2091 assert!(buf.get(2, 0).unwrap().is_continuation());
2092 }
2093
2094 #[test]
2095 fn wide_char_fill_region() {
2096 let mut buf = Buffer::new(10, 3);
2097
2098 let wide_cell = Cell::from_char('中');
2101 buf.fill(Rect::new(0, 0, 4, 2), wide_cell);
2102
2103 assert_eq!(buf.get(3, 0).unwrap().content.as_char(), Some('中'));
2125 }
2126
2127 #[test]
2128 fn default_buffer_dimensions() {
2129 let buf = Buffer::default();
2130 assert_eq!(buf.width(), 1);
2131 assert_eq!(buf.height(), 1);
2132 assert_eq!(buf.len(), 1);
2133 }
2134
2135 #[test]
2136 fn buffer_partial_eq_impl() {
2137 let buf1 = Buffer::new(5, 5);
2138 let buf2 = Buffer::new(5, 5);
2139 let mut buf3 = Buffer::new(5, 5);
2140 buf3.set(0, 0, Cell::from_char('X'));
2141
2142 assert_eq!(buf1, buf2);
2143 assert_ne!(buf1, buf3);
2144 }
2145
2146 #[test]
2147 fn degradation_level_accessible() {
2148 let mut buf = Buffer::new(10, 10);
2149 assert_eq!(buf.degradation, DegradationLevel::Full);
2150
2151 buf.degradation = DegradationLevel::SimpleBorders;
2152 assert_eq!(buf.degradation, DegradationLevel::SimpleBorders);
2153 }
2154
2155 #[test]
2158 fn get_mut_modifies_cell() {
2159 let mut buf = Buffer::new(10, 10);
2160 buf.set(3, 3, Cell::from_char('A'));
2161
2162 if let Some(cell) = buf.get_mut(3, 3) {
2163 *cell = Cell::from_char('B');
2164 }
2165
2166 assert_eq!(buf.get(3, 3).unwrap().content.as_char(), Some('B'));
2167 }
2168
2169 #[test]
2170 fn get_mut_out_of_bounds() {
2171 let mut buf = Buffer::new(5, 5);
2172 assert!(buf.get_mut(10, 10).is_none());
2173 }
2174
2175 #[test]
2178 fn clear_with_fills_all_cells() {
2179 let mut buf = Buffer::new(5, 3);
2180 let fill_cell = Cell::from_char('*');
2181 buf.clear_with(fill_cell);
2182
2183 for y in 0..3 {
2184 for x in 0..5 {
2185 assert_eq!(buf.get(x, y).unwrap().content.as_char(), Some('*'));
2186 }
2187 }
2188 }
2189
2190 #[test]
2193 fn cells_slice_has_correct_length() {
2194 let buf = Buffer::new(10, 5);
2195 assert_eq!(buf.cells().len(), 50);
2196 }
2197
2198 #[test]
2199 fn cells_mut_allows_direct_modification() {
2200 let mut buf = Buffer::new(3, 2);
2201 let cells = buf.cells_mut();
2202 cells[0] = Cell::from_char('Z');
2203
2204 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('Z'));
2205 }
2206
2207 #[test]
2210 fn row_cells_returns_correct_row() {
2211 let mut buf = Buffer::new(5, 3);
2212 buf.set(2, 1, Cell::from_char('R'));
2213
2214 let row = buf.row_cells(1);
2215 assert_eq!(row.len(), 5);
2216 assert_eq!(row[2].content.as_char(), Some('R'));
2217 }
2218
2219 #[test]
2220 #[should_panic]
2221 fn row_cells_out_of_bounds_panics() {
2222 let buf = Buffer::new(5, 3);
2223 let _ = buf.row_cells(5);
2224 }
2225
2226 #[test]
2229 fn buffer_is_not_empty() {
2230 let buf = Buffer::new(1, 1);
2231 assert!(!buf.is_empty());
2232 }
2233
2234 #[test]
2237 fn set_raw_out_of_bounds_is_safe() {
2238 let mut buf = Buffer::new(5, 5);
2239 buf.set_raw(100, 100, Cell::from_char('X'));
2240 }
2242
2243 #[test]
2246 fn copy_from_out_of_bounds_partial() {
2247 let mut src = Buffer::new(5, 5);
2248 src.set(0, 0, Cell::from_char('A'));
2249 src.set(4, 4, Cell::from_char('B'));
2250
2251 let mut dst = Buffer::new(5, 5);
2252 dst.copy_from(&src, Rect::new(0, 0, 5, 5), 3, 3);
2254
2255 assert_eq!(dst.get(3, 3).unwrap().content.as_char(), Some('A'));
2257 assert!(dst.get(4, 4).unwrap().is_empty());
2259 }
2260
2261 #[test]
2264 fn content_eq_different_dimensions() {
2265 let buf1 = Buffer::new(5, 5);
2266 let buf2 = Buffer::new(10, 10);
2267 assert!(!buf1.content_eq(&buf2));
2269 }
2270
2271 mod property {
2274 use super::*;
2275 use proptest::prelude::*;
2276
2277 proptest! {
2278 #[test]
2279 fn buffer_dimensions_are_preserved(width in 1u16..200, height in 1u16..200) {
2280 let buf = Buffer::new(width, height);
2281 prop_assert_eq!(buf.width(), width);
2282 prop_assert_eq!(buf.height(), height);
2283 prop_assert_eq!(buf.len(), width as usize * height as usize);
2284 }
2285
2286 #[test]
2287 fn buffer_get_in_bounds_always_succeeds(width in 1u16..100, height in 1u16..100) {
2288 let buf = Buffer::new(width, height);
2289 for x in 0..width {
2290 for y in 0..height {
2291 prop_assert!(buf.get(x, y).is_some(), "get({x},{y}) failed for {width}x{height} buffer");
2292 }
2293 }
2294 }
2295
2296 #[test]
2297 fn buffer_get_out_of_bounds_returns_none(width in 1u16..50, height in 1u16..50) {
2298 let buf = Buffer::new(width, height);
2299 prop_assert!(buf.get(width, 0).is_none());
2300 prop_assert!(buf.get(0, height).is_none());
2301 prop_assert!(buf.get(width, height).is_none());
2302 }
2303
2304 #[test]
2305 fn buffer_set_get_roundtrip(
2306 width in 5u16..50,
2307 height in 5u16..50,
2308 x in 0u16..5,
2309 y in 0u16..5,
2310 ch_idx in 0u32..26,
2311 ) {
2312 let x = x % width;
2313 let y = y % height;
2314 let ch = char::from_u32('A' as u32 + ch_idx).unwrap();
2315 let mut buf = Buffer::new(width, height);
2316 buf.set(x, y, Cell::from_char(ch));
2317 let got = buf.get(x, y).unwrap();
2318 prop_assert_eq!(got.content.as_char(), Some(ch));
2319 }
2320
2321 #[test]
2322 fn scissor_push_pop_stack_depth(
2323 width in 10u16..50,
2324 height in 10u16..50,
2325 push_count in 1usize..10,
2326 ) {
2327 let mut buf = Buffer::new(width, height);
2328 prop_assert_eq!(buf.scissor_depth(), 1); for i in 0..push_count {
2331 buf.push_scissor(Rect::new(0, 0, width, height));
2332 prop_assert_eq!(buf.scissor_depth(), i + 2);
2333 }
2334
2335 for i in (0..push_count).rev() {
2336 buf.pop_scissor();
2337 prop_assert_eq!(buf.scissor_depth(), i + 1);
2338 }
2339
2340 buf.pop_scissor();
2342 prop_assert_eq!(buf.scissor_depth(), 1);
2343 }
2344
2345 #[test]
2346 fn scissor_monotonic_intersection(
2347 width in 20u16..60,
2348 height in 20u16..60,
2349 ) {
2350 let mut buf = Buffer::new(width, height);
2352 let outer = Rect::new(2, 2, width - 4, height - 4);
2353 buf.push_scissor(outer);
2354 let s1 = buf.current_scissor();
2355
2356 let inner = Rect::new(5, 5, 10, 10);
2357 buf.push_scissor(inner);
2358 let s2 = buf.current_scissor();
2359
2360 prop_assert!(s2.width <= s1.width, "inner width {} > outer width {}", s2.width, s1.width);
2362 prop_assert!(s2.height <= s1.height, "inner height {} > outer height {}", s2.height, s1.height);
2363 }
2364
2365 #[test]
2366 fn opacity_push_pop_stack_depth(
2367 width in 5u16..20,
2368 height in 5u16..20,
2369 push_count in 1usize..10,
2370 ) {
2371 let mut buf = Buffer::new(width, height);
2372 prop_assert_eq!(buf.opacity_depth(), 1);
2373
2374 for i in 0..push_count {
2375 buf.push_opacity(0.9);
2376 prop_assert_eq!(buf.opacity_depth(), i + 2);
2377 }
2378
2379 for i in (0..push_count).rev() {
2380 buf.pop_opacity();
2381 prop_assert_eq!(buf.opacity_depth(), i + 1);
2382 }
2383
2384 buf.pop_opacity();
2385 prop_assert_eq!(buf.opacity_depth(), 1);
2386 }
2387
2388 #[test]
2389 fn opacity_multiplication_is_monotonic(
2390 opacity1 in 0.0f32..=1.0,
2391 opacity2 in 0.0f32..=1.0,
2392 ) {
2393 let mut buf = Buffer::new(5, 5);
2394 buf.push_opacity(opacity1);
2395 let after_first = buf.current_opacity();
2396 buf.push_opacity(opacity2);
2397 let after_second = buf.current_opacity();
2398
2399 prop_assert!(after_second <= after_first + f32::EPSILON,
2401 "opacity increased: {} -> {}", after_first, after_second);
2402 }
2403
2404 #[test]
2405 fn clear_resets_all_cells(width in 1u16..30, height in 1u16..30) {
2406 let mut buf = Buffer::new(width, height);
2407 for x in 0..width {
2409 buf.set_raw(x, 0, Cell::from_char('X'));
2410 }
2411 buf.clear();
2412 for y in 0..height {
2414 for x in 0..width {
2415 prop_assert!(buf.get(x, y).unwrap().is_empty(),
2416 "cell ({x},{y}) not empty after clear");
2417 }
2418 }
2419 }
2420
2421 #[test]
2422 fn content_eq_is_reflexive(width in 1u16..30, height in 1u16..30) {
2423 let buf = Buffer::new(width, height);
2424 prop_assert!(buf.content_eq(&buf));
2425 }
2426
2427 #[test]
2428 fn content_eq_detects_single_change(
2429 width in 5u16..30,
2430 height in 5u16..30,
2431 x in 0u16..5,
2432 y in 0u16..5,
2433 ) {
2434 let x = x % width;
2435 let y = y % height;
2436 let buf1 = Buffer::new(width, height);
2437 let mut buf2 = Buffer::new(width, height);
2438 buf2.set_raw(x, y, Cell::from_char('Z'));
2439 prop_assert!(!buf1.content_eq(&buf2));
2440 }
2441
2442 #[test]
2445 fn dimensions_immutable_through_operations(
2446 width in 5u16..30,
2447 height in 5u16..30,
2448 ) {
2449 let mut buf = Buffer::new(width, height);
2450
2451 buf.set(0, 0, Cell::from_char('A'));
2453 prop_assert_eq!(buf.width(), width);
2454 prop_assert_eq!(buf.height(), height);
2455 prop_assert_eq!(buf.len(), width as usize * height as usize);
2456
2457 buf.push_scissor(Rect::new(1, 1, 3, 3));
2458 prop_assert_eq!(buf.width(), width);
2459 prop_assert_eq!(buf.height(), height);
2460
2461 buf.push_opacity(0.5);
2462 prop_assert_eq!(buf.width(), width);
2463 prop_assert_eq!(buf.height(), height);
2464
2465 buf.pop_scissor();
2466 buf.pop_opacity();
2467 prop_assert_eq!(buf.width(), width);
2468 prop_assert_eq!(buf.height(), height);
2469
2470 buf.clear();
2471 prop_assert_eq!(buf.width(), width);
2472 prop_assert_eq!(buf.height(), height);
2473 prop_assert_eq!(buf.len(), width as usize * height as usize);
2474 }
2475
2476 #[test]
2477 fn scissor_area_never_increases_random_rects(
2478 width in 20u16..60,
2479 height in 20u16..60,
2480 rects in proptest::collection::vec(
2481 (0u16..20, 0u16..20, 1u16..15, 1u16..15),
2482 1..8
2483 ),
2484 ) {
2485 let mut buf = Buffer::new(width, height);
2486 let mut prev_area = (width as u32) * (height as u32);
2487
2488 for (x, y, w, h) in rects {
2489 buf.push_scissor(Rect::new(x, y, w, h));
2490 let s = buf.current_scissor();
2491 let area = (s.width as u32) * (s.height as u32);
2492 prop_assert!(area <= prev_area,
2493 "scissor area increased: {} -> {} after push({},{},{},{})",
2494 prev_area, area, x, y, w, h);
2495 prev_area = area;
2496 }
2497 }
2498
2499 #[test]
2500 fn opacity_range_invariant_random_sequence(
2501 opacities in proptest::collection::vec(0.0f32..=1.0, 1..15),
2502 ) {
2503 let mut buf = Buffer::new(5, 5);
2504
2505 for &op in &opacities {
2506 buf.push_opacity(op);
2507 let current = buf.current_opacity();
2508 prop_assert!(current >= 0.0, "opacity below 0: {}", current);
2509 prop_assert!(current <= 1.0 + f32::EPSILON,
2510 "opacity above 1: {}", current);
2511 }
2512
2513 for _ in &opacities {
2515 buf.pop_opacity();
2516 }
2517 prop_assert!((buf.current_opacity() - 1.0).abs() < f32::EPSILON);
2519 }
2520
2521 #[test]
2522 fn opacity_clamp_out_of_range(
2523 neg in -100.0f32..0.0,
2524 over in 1.01f32..100.0,
2525 ) {
2526 let mut buf = Buffer::new(5, 5);
2527
2528 buf.push_opacity(neg);
2529 prop_assert!(buf.current_opacity() >= 0.0,
2530 "negative opacity not clamped: {}", buf.current_opacity());
2531 buf.pop_opacity();
2532
2533 buf.push_opacity(over);
2534 prop_assert!(buf.current_opacity() <= 1.0 + f32::EPSILON,
2535 "over-1 opacity not clamped: {}", buf.current_opacity());
2536 }
2537
2538 #[test]
2539 fn scissor_stack_always_has_base(
2540 pushes in 0usize..10,
2541 pops in 0usize..15,
2542 ) {
2543 let mut buf = Buffer::new(10, 10);
2544
2545 for _ in 0..pushes {
2546 buf.push_scissor(Rect::new(0, 0, 5, 5));
2547 }
2548 for _ in 0..pops {
2549 buf.pop_scissor();
2550 }
2551
2552 prop_assert!(buf.scissor_depth() >= 1,
2554 "scissor depth dropped below 1 after {} pushes, {} pops",
2555 pushes, pops);
2556 }
2557
2558 #[test]
2559 fn opacity_stack_always_has_base(
2560 pushes in 0usize..10,
2561 pops in 0usize..15,
2562 ) {
2563 let mut buf = Buffer::new(10, 10);
2564
2565 for _ in 0..pushes {
2566 buf.push_opacity(0.5);
2567 }
2568 for _ in 0..pops {
2569 buf.pop_opacity();
2570 }
2571
2572 prop_assert!(buf.opacity_depth() >= 1,
2574 "opacity depth dropped below 1 after {} pushes, {} pops",
2575 pushes, pops);
2576 }
2577
2578 #[test]
2579 fn cells_len_invariant_always_holds(
2580 width in 1u16..50,
2581 height in 1u16..50,
2582 ) {
2583 let mut buf = Buffer::new(width, height);
2584 let expected = width as usize * height as usize;
2585
2586 prop_assert_eq!(buf.cells().len(), expected);
2587
2588 buf.set(0, 0, Cell::from_char('X'));
2590 prop_assert_eq!(buf.cells().len(), expected);
2591
2592 buf.clear();
2593 prop_assert_eq!(buf.cells().len(), expected);
2594 }
2595
2596 #[test]
2597 fn set_outside_scissor_is_noop(
2598 width in 10u16..30,
2599 height in 10u16..30,
2600 ) {
2601 let mut buf = Buffer::new(width, height);
2602 buf.push_scissor(Rect::new(2, 2, 3, 3));
2603
2604 buf.set(0, 0, Cell::from_char('X'));
2606 let cell = buf.get(0, 0).unwrap();
2608 prop_assert!(cell.is_empty(),
2609 "cell (0,0) modified outside scissor region");
2610
2611 buf.set(3, 3, Cell::from_char('Y'));
2613 let cell = buf.get(3, 3).unwrap();
2614 prop_assert_eq!(cell.content.as_char(), Some('Y'));
2615 }
2616
2617 #[test]
2620 fn wide_char_overwrites_cleanup_tails(
2621 width in 10u16..30,
2622 x in 0u16..8,
2623 ) {
2624 let x = x % (width.saturating_sub(2).max(1));
2625 let mut buf = Buffer::new(width, 1);
2626
2627 buf.set(x, 0, Cell::from_char('中'));
2629
2630 if x + 1 < width {
2632 let head = buf.get(x, 0).unwrap();
2633 let tail = buf.get(x + 1, 0).unwrap();
2634
2635 if head.content.as_char() == Some('中') {
2636 prop_assert!(tail.is_continuation(),
2637 "tail at x+1={} should be continuation", x + 1);
2638
2639 buf.set(x, 0, Cell::from_char('A'));
2641 let new_head = buf.get(x, 0).unwrap();
2642 let cleared_tail = buf.get(x + 1, 0).unwrap();
2643
2644 prop_assert_eq!(new_head.content.as_char(), Some('A'));
2645 prop_assert!(cleared_tail.is_empty(),
2646 "tail should be cleared after head overwrite");
2647 }
2648 }
2649 }
2650
2651 #[test]
2652 fn wide_char_atomic_rejection_at_boundary(
2653 width in 3u16..20,
2654 ) {
2655 let mut buf = Buffer::new(width, 1);
2656
2657 let last_pos = width - 1;
2659 buf.set(last_pos, 0, Cell::from_char('中'));
2660
2661 let cell = buf.get(last_pos, 0).unwrap();
2663 prop_assert!(cell.is_empty(),
2664 "wide char at boundary position {} (width {}) should be rejected",
2665 last_pos, width);
2666 }
2667
2668 #[test]
2673 fn double_buffer_swap_is_involution(ops in proptest::collection::vec(proptest::bool::ANY, 0..100)) {
2674 let mut db = DoubleBuffer::new(10, 10);
2675 let initial_idx = db.current_idx;
2676
2677 for do_swap in &ops {
2678 if *do_swap {
2679 db.swap();
2680 }
2681 }
2682
2683 let swap_count = ops.iter().filter(|&&x| x).count();
2684 let expected_idx = if swap_count % 2 == 0 { initial_idx } else { 1 - initial_idx };
2685
2686 prop_assert_eq!(db.current_idx, expected_idx,
2687 "After {} swaps, index should be {} but was {}",
2688 swap_count, expected_idx, db.current_idx);
2689 }
2690
2691 #[test]
2692 fn double_buffer_resize_preserves_invariant(
2693 init_w in 1u16..200,
2694 init_h in 1u16..100,
2695 new_w in 1u16..200,
2696 new_h in 1u16..100,
2697 ) {
2698 let mut db = DoubleBuffer::new(init_w, init_h);
2699 db.resize(new_w, new_h);
2700
2701 prop_assert_eq!(db.width(), new_w);
2702 prop_assert_eq!(db.height(), new_h);
2703 prop_assert!(db.dimensions_match(new_w, new_h));
2704 }
2705
2706 #[test]
2707 fn double_buffer_current_previous_disjoint(
2708 width in 1u16..50,
2709 height in 1u16..50,
2710 ) {
2711 let mut db = DoubleBuffer::new(width, height);
2712
2713 db.current_mut().set(0, 0, Cell::from_char('C'));
2715
2716 prop_assert!(db.previous().get(0, 0).unwrap().is_empty(),
2718 "Previous buffer should not reflect changes to current");
2719
2720 db.swap();
2722 prop_assert_eq!(db.previous().get(0, 0).unwrap().content.as_char(), Some('C'),
2723 "After swap, previous should have the 'C' we wrote");
2724 }
2725
2726 #[test]
2727 fn double_buffer_swap_content_semantics(
2728 width in 5u16..30,
2729 height in 5u16..30,
2730 ) {
2731 let mut db = DoubleBuffer::new(width, height);
2732
2733 db.current_mut().set(0, 0, Cell::from_char('X'));
2735 db.swap();
2736
2737 db.current_mut().set(0, 0, Cell::from_char('Y'));
2739 db.swap();
2740
2741 prop_assert_eq!(db.current().get(0, 0).unwrap().content.as_char(), Some('X'));
2743 prop_assert_eq!(db.previous().get(0, 0).unwrap().content.as_char(), Some('Y'));
2744 }
2745
2746 #[test]
2747 fn double_buffer_resize_clears_both(
2748 w1 in 5u16..30,
2749 h1 in 5u16..30,
2750 w2 in 5u16..30,
2751 h2 in 5u16..30,
2752 ) {
2753 prop_assume!(w1 != w2 || h1 != h2);
2755
2756 let mut db = DoubleBuffer::new(w1, h1);
2757
2758 db.current_mut().set(0, 0, Cell::from_char('A'));
2760 db.swap();
2761 db.current_mut().set(0, 0, Cell::from_char('B'));
2762
2763 db.resize(w2, h2);
2765
2766 prop_assert!(db.current().get(0, 0).unwrap().is_empty(),
2768 "Current buffer should be empty after resize");
2769 prop_assert!(db.previous().get(0, 0).unwrap().is_empty(),
2770 "Previous buffer should be empty after resize");
2771 }
2772 }
2773 }
2774
2775 #[test]
2778 fn dirty_rows_start_dirty() {
2779 let buf = Buffer::new(10, 5);
2781 assert_eq!(buf.dirty_row_count(), 5);
2782 for y in 0..5 {
2783 assert!(buf.is_row_dirty(y));
2784 }
2785 }
2786
2787 #[test]
2788 fn dirty_bitmap_starts_full() {
2789 let buf = Buffer::new(4, 3);
2790 assert!(buf.dirty_all());
2791 assert_eq!(buf.dirty_cell_count(), 12);
2792 }
2793
2794 #[test]
2795 fn dirty_bitmap_tracks_single_cell() {
2796 let mut buf = Buffer::new(4, 3);
2797 buf.clear_dirty();
2798 assert!(!buf.dirty_all());
2799 buf.set_raw(1, 1, Cell::from_char('X'));
2800 let idx = 1 + 4;
2801 assert_eq!(buf.dirty_cell_count(), 1);
2802 assert_eq!(buf.dirty_bits()[idx], 1);
2803 }
2804
2805 #[test]
2806 fn dirty_bitmap_dedupes_cells() {
2807 let mut buf = Buffer::new(4, 3);
2808 buf.clear_dirty();
2809 buf.set_raw(2, 2, Cell::from_char('A'));
2810 buf.set_raw(2, 2, Cell::from_char('B'));
2811 assert_eq!(buf.dirty_cell_count(), 1);
2812 }
2813
2814 #[test]
2815 fn set_marks_row_dirty() {
2816 let mut buf = Buffer::new(10, 5);
2817 buf.clear_dirty(); buf.set(3, 2, Cell::from_char('X'));
2819 assert!(buf.is_row_dirty(2));
2820 assert!(!buf.is_row_dirty(0));
2821 assert!(!buf.is_row_dirty(1));
2822 assert!(!buf.is_row_dirty(3));
2823 assert!(!buf.is_row_dirty(4));
2824 }
2825
2826 #[test]
2827 fn set_raw_marks_row_dirty() {
2828 let mut buf = Buffer::new(10, 5);
2829 buf.clear_dirty(); buf.set_raw(0, 4, Cell::from_char('Z'));
2831 assert!(buf.is_row_dirty(4));
2832 assert_eq!(buf.dirty_row_count(), 1);
2833 }
2834
2835 #[test]
2836 fn clear_marks_all_dirty() {
2837 let mut buf = Buffer::new(10, 5);
2838 buf.clear();
2839 assert_eq!(buf.dirty_row_count(), 5);
2840 }
2841
2842 #[test]
2843 fn clear_dirty_resets_flags() {
2844 let mut buf = Buffer::new(10, 5);
2845 assert_eq!(buf.dirty_row_count(), 5);
2847 buf.clear_dirty();
2848 assert_eq!(buf.dirty_row_count(), 0);
2849
2850 buf.set(0, 0, Cell::from_char('A'));
2852 buf.set(0, 3, Cell::from_char('B'));
2853 assert_eq!(buf.dirty_row_count(), 2);
2854
2855 buf.clear_dirty();
2856 assert_eq!(buf.dirty_row_count(), 0);
2857 }
2858
2859 #[test]
2860 fn clear_dirty_resets_bitmap() {
2861 let mut buf = Buffer::new(4, 3);
2862 buf.clear();
2863 assert!(buf.dirty_all());
2864 buf.clear_dirty();
2865 assert!(!buf.dirty_all());
2866 assert_eq!(buf.dirty_cell_count(), 0);
2867 assert!(buf.dirty_bits().iter().all(|&b| b == 0));
2868 }
2869
2870 #[test]
2871 fn fill_marks_affected_rows_dirty() {
2872 let mut buf = Buffer::new(10, 10);
2873 buf.clear_dirty(); buf.fill(Rect::new(0, 2, 5, 3), Cell::from_char('.'));
2875 assert!(!buf.is_row_dirty(0));
2877 assert!(!buf.is_row_dirty(1));
2878 assert!(buf.is_row_dirty(2));
2879 assert!(buf.is_row_dirty(3));
2880 assert!(buf.is_row_dirty(4));
2881 assert!(!buf.is_row_dirty(5));
2882 }
2883
2884 #[test]
2885 fn get_mut_marks_row_dirty() {
2886 let mut buf = Buffer::new(10, 5);
2887 buf.clear_dirty(); if let Some(cell) = buf.get_mut(5, 3) {
2889 cell.fg = PackedRgba::rgb(255, 0, 0);
2890 }
2891 assert!(buf.is_row_dirty(3));
2892 assert_eq!(buf.dirty_row_count(), 1);
2893 }
2894
2895 #[test]
2896 fn cells_mut_marks_all_dirty() {
2897 let mut buf = Buffer::new(10, 5);
2898 let _ = buf.cells_mut();
2899 assert_eq!(buf.dirty_row_count(), 5);
2900 }
2901
2902 #[test]
2903 fn dirty_rows_slice_length_matches_height() {
2904 let buf = Buffer::new(10, 7);
2905 assert_eq!(buf.dirty_rows().len(), 7);
2906 }
2907
2908 #[test]
2909 fn out_of_bounds_set_does_not_dirty() {
2910 let mut buf = Buffer::new(10, 5);
2911 buf.clear_dirty(); buf.set(100, 100, Cell::from_char('X'));
2913 assert_eq!(buf.dirty_row_count(), 0);
2914 }
2915
2916 #[test]
2917 fn property_dirty_soundness() {
2918 let mut buf = Buffer::new(20, 10);
2920 let positions = [(3, 0), (5, 2), (0, 9), (19, 5), (10, 7)];
2921 for &(x, y) in &positions {
2922 buf.set(x, y, Cell::from_char('*'));
2923 }
2924 for &(_, y) in &positions {
2925 assert!(
2926 buf.is_row_dirty(y),
2927 "Row {} should be dirty after set({}, {})",
2928 y,
2929 positions.iter().find(|(_, ry)| *ry == y).unwrap().0,
2930 y
2931 );
2932 }
2933 }
2934
2935 #[test]
2936 fn dirty_clear_between_frames() {
2937 let mut buf = Buffer::new(10, 5);
2939
2940 assert_eq!(buf.dirty_row_count(), 5);
2942
2943 buf.clear_dirty();
2945 assert_eq!(buf.dirty_row_count(), 0);
2946
2947 buf.set(0, 0, Cell::from_char('A'));
2949 buf.set(0, 2, Cell::from_char('B'));
2950 assert_eq!(buf.dirty_row_count(), 2);
2951
2952 buf.clear_dirty();
2954 assert_eq!(buf.dirty_row_count(), 0);
2955
2956 buf.set(0, 4, Cell::from_char('C'));
2958 assert_eq!(buf.dirty_row_count(), 1);
2959 assert!(buf.is_row_dirty(4));
2960 assert!(!buf.is_row_dirty(0));
2961 }
2962
2963 #[test]
2966 fn dirty_spans_start_full_dirty() {
2967 let buf = Buffer::new(10, 5);
2968 for y in 0..5 {
2969 let row = buf.dirty_span_row(y).unwrap();
2970 assert!(row.is_full(), "row {y} should start full-dirty");
2971 assert!(row.spans().is_empty(), "row {y} spans should start empty");
2972 }
2973 }
2974
2975 #[test]
2976 fn clear_dirty_resets_spans() {
2977 let mut buf = Buffer::new(10, 5);
2978 buf.clear_dirty();
2979 for y in 0..5 {
2980 let row = buf.dirty_span_row(y).unwrap();
2981 assert!(!row.is_full(), "row {y} should clear full-dirty");
2982 assert!(row.spans().is_empty(), "row {y} spans should be cleared");
2983 }
2984 assert_eq!(buf.dirty_span_overflows, 0);
2985 }
2986
2987 #[test]
2988 fn set_records_dirty_span() {
2989 let mut buf = Buffer::new(20, 2);
2990 buf.clear_dirty();
2991 buf.set(2, 0, Cell::from_char('A'));
2992 let row = buf.dirty_span_row(0).unwrap();
2993 assert_eq!(row.spans(), &[DirtySpan::new(2, 3)]);
2994 assert!(!row.is_full());
2995 }
2996
2997 #[test]
2998 fn set_merges_adjacent_spans() {
2999 let mut buf = Buffer::new(20, 2);
3000 buf.clear_dirty();
3001 buf.set(2, 0, Cell::from_char('A'));
3002 buf.set(3, 0, Cell::from_char('B')); let row = buf.dirty_span_row(0).unwrap();
3004 assert_eq!(row.spans(), &[DirtySpan::new(2, 4)]);
3005 }
3006
3007 #[test]
3008 fn set_merges_close_spans() {
3009 let mut buf = Buffer::new(20, 2);
3010 buf.clear_dirty();
3011 buf.set(2, 0, Cell::from_char('A'));
3012 buf.set(4, 0, Cell::from_char('B')); let row = buf.dirty_span_row(0).unwrap();
3014 assert_eq!(row.spans(), &[DirtySpan::new(2, 5)]);
3015 }
3016
3017 #[test]
3018 fn span_overflow_sets_full_row() {
3019 let width = (DIRTY_SPAN_MAX_SPANS_PER_ROW as u16 + 2) * 3;
3020 let mut buf = Buffer::new(width, 1);
3021 buf.clear_dirty();
3022 for i in 0..(DIRTY_SPAN_MAX_SPANS_PER_ROW + 1) {
3023 let x = (i as u16) * 3;
3024 buf.set(x, 0, Cell::from_char('x'));
3025 }
3026 let row = buf.dirty_span_row(0).unwrap();
3027 assert!(row.is_full());
3028 assert!(row.spans().is_empty());
3029 assert_eq!(buf.dirty_span_overflows, 1);
3030 }
3031
3032 #[test]
3033 fn fill_full_row_marks_full_span() {
3034 let mut buf = Buffer::new(10, 3);
3035 buf.clear_dirty();
3036 let cell = Cell::from_char('x').with_bg(PackedRgba::rgb(0, 0, 0));
3037 buf.fill(Rect::new(0, 1, 10, 1), cell);
3038 let row = buf.dirty_span_row(1).unwrap();
3039 assert!(row.is_full());
3040 assert!(row.spans().is_empty());
3041 }
3042
3043 #[test]
3044 fn get_mut_records_dirty_span() {
3045 let mut buf = Buffer::new(10, 5);
3046 buf.clear_dirty();
3047 let _ = buf.get_mut(5, 3);
3048 let row = buf.dirty_span_row(3).unwrap();
3049 assert_eq!(row.spans(), &[DirtySpan::new(5, 6)]);
3050 }
3051
3052 #[test]
3053 fn cells_mut_marks_all_full_spans() {
3054 let mut buf = Buffer::new(10, 5);
3055 buf.clear_dirty();
3056 let _ = buf.cells_mut();
3057 for y in 0..5 {
3058 let row = buf.dirty_span_row(y).unwrap();
3059 assert!(row.is_full(), "row {y} should be full after cells_mut");
3060 }
3061 }
3062
3063 #[test]
3064 fn dirty_span_config_disabled_skips_rows() {
3065 let mut buf = Buffer::new(10, 1);
3066 buf.clear_dirty();
3067 buf.set_dirty_span_config(DirtySpanConfig::default().with_enabled(false));
3068 buf.set(5, 0, Cell::from_char('x'));
3069 assert!(buf.dirty_span_row(0).is_none());
3070 let stats = buf.dirty_span_stats();
3071 assert_eq!(stats.total_spans, 0);
3072 assert_eq!(stats.span_coverage_cells, 0);
3073 }
3074
3075 #[test]
3076 fn dirty_span_guard_band_expands_span_bounds() {
3077 let mut buf = Buffer::new(10, 1);
3078 buf.clear_dirty();
3079 buf.set_dirty_span_config(DirtySpanConfig::default().with_guard_band(2));
3080 buf.set(5, 0, Cell::from_char('x'));
3081 let row = buf.dirty_span_row(0).unwrap();
3082 assert_eq!(row.spans(), &[DirtySpan::new(3, 8)]);
3083 }
3084
3085 #[test]
3086 fn dirty_span_max_spans_overflow_triggers_full_row() {
3087 let mut buf = Buffer::new(10, 1);
3088 buf.clear_dirty();
3089 buf.set_dirty_span_config(
3090 DirtySpanConfig::default()
3091 .with_max_spans_per_row(1)
3092 .with_merge_gap(0),
3093 );
3094 buf.set(0, 0, Cell::from_char('a'));
3095 buf.set(4, 0, Cell::from_char('b'));
3096 let row = buf.dirty_span_row(0).unwrap();
3097 assert!(row.is_full());
3098 assert!(row.spans().is_empty());
3099 assert_eq!(buf.dirty_span_overflows, 1);
3100 }
3101
3102 #[test]
3103 fn dirty_span_stats_counts_full_rows_and_spans() {
3104 let mut buf = Buffer::new(6, 2);
3105 buf.clear_dirty();
3106 buf.set_dirty_span_config(DirtySpanConfig::default().with_merge_gap(0));
3107 buf.set(1, 0, Cell::from_char('a'));
3108 buf.set(4, 0, Cell::from_char('b'));
3109 buf.mark_dirty_row_full(1);
3110
3111 let stats = buf.dirty_span_stats();
3112 assert_eq!(stats.rows_full_dirty, 1);
3113 assert_eq!(stats.rows_with_spans, 1);
3114 assert_eq!(stats.total_spans, 2);
3115 assert_eq!(stats.max_span_len, 6);
3116 assert_eq!(stats.span_coverage_cells, 8);
3117 }
3118
3119 #[test]
3120 fn dirty_span_stats_reports_overflow_and_full_row() {
3121 let mut buf = Buffer::new(8, 1);
3122 buf.clear_dirty();
3123 buf.set_dirty_span_config(
3124 DirtySpanConfig::default()
3125 .with_max_spans_per_row(1)
3126 .with_merge_gap(0),
3127 );
3128 buf.set(0, 0, Cell::from_char('x'));
3129 buf.set(3, 0, Cell::from_char('y'));
3130
3131 let stats = buf.dirty_span_stats();
3132 assert_eq!(stats.overflows, 1);
3133 assert_eq!(stats.rows_full_dirty, 1);
3134 assert_eq!(stats.total_spans, 0);
3135 assert_eq!(stats.span_coverage_cells, 8);
3136 }
3137
3138 #[test]
3143 fn double_buffer_new_has_matching_dimensions() {
3144 let db = DoubleBuffer::new(80, 24);
3145 assert_eq!(db.width(), 80);
3146 assert_eq!(db.height(), 24);
3147 assert!(db.dimensions_match(80, 24));
3148 assert!(!db.dimensions_match(120, 40));
3149 }
3150
3151 #[test]
3152 fn double_buffer_swap_is_o1() {
3153 let mut db = DoubleBuffer::new(80, 24);
3154
3155 db.current_mut().set(0, 0, Cell::from_char('A'));
3157 assert_eq!(db.current().get(0, 0).unwrap().content.as_char(), Some('A'));
3158
3159 db.swap();
3161 assert_eq!(
3162 db.previous().get(0, 0).unwrap().content.as_char(),
3163 Some('A')
3164 );
3165 assert!(db.current().get(0, 0).unwrap().is_empty());
3167 }
3168
3169 #[test]
3170 fn double_buffer_swap_round_trip() {
3171 let mut db = DoubleBuffer::new(10, 5);
3172
3173 db.current_mut().set(0, 0, Cell::from_char('X'));
3174 db.swap();
3175 db.current_mut().set(0, 0, Cell::from_char('Y'));
3176 db.swap();
3177
3178 assert_eq!(db.current().get(0, 0).unwrap().content.as_char(), Some('X'));
3180 assert_eq!(
3181 db.previous().get(0, 0).unwrap().content.as_char(),
3182 Some('Y')
3183 );
3184 }
3185
3186 #[test]
3187 fn double_buffer_resize_changes_dimensions() {
3188 let mut db = DoubleBuffer::new(80, 24);
3189 assert!(!db.resize(80, 24)); assert!(db.resize(120, 40)); assert_eq!(db.width(), 120);
3192 assert_eq!(db.height(), 40);
3193 assert!(db.dimensions_match(120, 40));
3194 }
3195
3196 #[test]
3197 fn double_buffer_resize_clears_content() {
3198 let mut db = DoubleBuffer::new(10, 5);
3199 db.current_mut().set(0, 0, Cell::from_char('Z'));
3200 db.swap();
3201 db.current_mut().set(0, 0, Cell::from_char('W'));
3202
3203 db.resize(20, 10);
3204
3205 assert!(db.current().get(0, 0).unwrap().is_empty());
3207 assert!(db.previous().get(0, 0).unwrap().is_empty());
3208 }
3209
3210 #[test]
3211 fn double_buffer_current_and_previous_are_distinct() {
3212 let mut db = DoubleBuffer::new(10, 5);
3213 db.current_mut().set(0, 0, Cell::from_char('C'));
3214
3215 assert!(db.previous().get(0, 0).unwrap().is_empty());
3217 assert_eq!(db.current().get(0, 0).unwrap().content.as_char(), Some('C'));
3218 }
3219
3220 #[test]
3225 fn adaptive_buffer_new_has_over_allocation() {
3226 let adb = AdaptiveDoubleBuffer::new(80, 24);
3227
3228 assert_eq!(adb.width(), 80);
3230 assert_eq!(adb.height(), 24);
3231 assert!(adb.dimensions_match(80, 24));
3232
3233 assert!(adb.capacity_width() > 80);
3237 assert!(adb.capacity_height() > 24);
3238 assert_eq!(adb.capacity_width(), 100); assert_eq!(adb.capacity_height(), 30); }
3241
3242 #[test]
3243 fn adaptive_buffer_resize_avoids_reallocation_when_within_capacity() {
3244 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3245
3246 assert!(adb.resize(90, 28)); assert_eq!(adb.width(), 90);
3249 assert_eq!(adb.height(), 28);
3250 assert_eq!(adb.stats().resize_avoided, 1);
3251 assert_eq!(adb.stats().resize_reallocated, 0);
3252 assert_eq!(adb.stats().resize_growth, 1);
3253 }
3254
3255 #[test]
3256 fn adaptive_buffer_resize_reallocates_on_growth_beyond_capacity() {
3257 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3258
3259 assert!(adb.resize(120, 40)); assert_eq!(adb.width(), 120);
3262 assert_eq!(adb.height(), 40);
3263 assert_eq!(adb.stats().resize_reallocated, 1);
3264 assert_eq!(adb.stats().resize_avoided, 0);
3265
3266 assert!(adb.capacity_width() > 120);
3268 assert!(adb.capacity_height() > 40);
3269 }
3270
3271 #[test]
3272 fn adaptive_buffer_resize_reallocates_on_significant_shrink() {
3273 let mut adb = AdaptiveDoubleBuffer::new(100, 50);
3274
3275 assert!(adb.resize(40, 20)); assert_eq!(adb.width(), 40);
3279 assert_eq!(adb.height(), 20);
3280 assert_eq!(adb.stats().resize_reallocated, 1);
3281 assert_eq!(adb.stats().resize_shrink, 1);
3282 }
3283
3284 #[test]
3285 fn adaptive_buffer_resize_avoids_reallocation_on_minor_shrink() {
3286 let mut adb = AdaptiveDoubleBuffer::new(100, 50);
3287
3288 assert!(adb.resize(80, 40));
3292 assert_eq!(adb.width(), 80);
3293 assert_eq!(adb.height(), 40);
3294 assert_eq!(adb.stats().resize_avoided, 1);
3295 assert_eq!(adb.stats().resize_reallocated, 0);
3296 assert_eq!(adb.stats().resize_shrink, 1);
3297 }
3298
3299 #[test]
3300 fn adaptive_buffer_no_change_returns_false() {
3301 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3302
3303 assert!(!adb.resize(80, 24)); assert_eq!(adb.stats().resize_avoided, 0);
3305 assert_eq!(adb.stats().resize_reallocated, 0);
3306 assert_eq!(adb.stats().resize_growth, 0);
3307 assert_eq!(adb.stats().resize_shrink, 0);
3308 }
3309
3310 #[test]
3311 fn adaptive_buffer_swap_works() {
3312 let mut adb = AdaptiveDoubleBuffer::new(10, 5);
3313
3314 adb.current_mut().set(0, 0, Cell::from_char('A'));
3315 assert_eq!(
3316 adb.current().get(0, 0).unwrap().content.as_char(),
3317 Some('A')
3318 );
3319
3320 adb.swap();
3321 assert_eq!(
3322 adb.previous().get(0, 0).unwrap().content.as_char(),
3323 Some('A')
3324 );
3325 assert!(adb.current().get(0, 0).unwrap().is_empty());
3326 }
3327
3328 #[test]
3329 fn adaptive_buffer_stats_reset() {
3330 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3331
3332 adb.resize(90, 28);
3333 adb.resize(120, 40);
3334 assert!(adb.stats().resize_avoided > 0 || adb.stats().resize_reallocated > 0);
3335
3336 adb.reset_stats();
3337 assert_eq!(adb.stats().resize_avoided, 0);
3338 assert_eq!(adb.stats().resize_reallocated, 0);
3339 assert_eq!(adb.stats().resize_growth, 0);
3340 assert_eq!(adb.stats().resize_shrink, 0);
3341 }
3342
3343 #[test]
3344 fn adaptive_buffer_memory_efficiency() {
3345 let adb = AdaptiveDoubleBuffer::new(80, 24);
3346
3347 let efficiency = adb.memory_efficiency();
3348 assert!(efficiency > 0.5);
3352 assert!(efficiency < 1.0);
3353 }
3354
3355 #[test]
3356 fn adaptive_buffer_logical_bounds() {
3357 let adb = AdaptiveDoubleBuffer::new(80, 24);
3358
3359 let bounds = adb.logical_bounds();
3360 assert_eq!(bounds.x, 0);
3361 assert_eq!(bounds.y, 0);
3362 assert_eq!(bounds.width, 80);
3363 assert_eq!(bounds.height, 24);
3364 }
3365
3366 #[test]
3367 fn adaptive_buffer_capacity_clamped_for_large_sizes() {
3368 let adb = AdaptiveDoubleBuffer::new(1000, 500);
3370
3371 assert_eq!(adb.capacity_width(), 1000 + 200); assert_eq!(adb.capacity_height(), 500 + 125); }
3376
3377 #[test]
3378 fn adaptive_stats_avoidance_ratio() {
3379 let mut stats = AdaptiveStats::default();
3380
3381 assert!((stats.avoidance_ratio() - 1.0).abs() < f64::EPSILON);
3383
3384 stats.resize_avoided = 3;
3386 stats.resize_reallocated = 1;
3387 assert!((stats.avoidance_ratio() - 0.75).abs() < f64::EPSILON);
3388
3389 stats.resize_avoided = 0;
3391 stats.resize_reallocated = 5;
3392 assert!((stats.avoidance_ratio() - 0.0).abs() < f64::EPSILON);
3393 }
3394
3395 #[test]
3396 fn adaptive_buffer_resize_storm_simulation() {
3397 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3399
3400 for i in 1..=10 {
3402 adb.resize(80 + i, 24 + (i / 2));
3403 }
3404
3405 let ratio = adb.stats().avoidance_ratio();
3407 assert!(
3408 ratio > 0.5,
3409 "Expected >50% avoidance ratio, got {:.2}",
3410 ratio
3411 );
3412 }
3413
3414 #[test]
3415 fn adaptive_buffer_width_only_growth() {
3416 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3417
3418 assert!(adb.resize(95, 24)); assert_eq!(adb.stats().resize_avoided, 1);
3421 assert_eq!(adb.stats().resize_growth, 1);
3422 }
3423
3424 #[test]
3425 fn adaptive_buffer_height_only_growth() {
3426 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3427
3428 assert!(adb.resize(80, 28)); assert_eq!(adb.stats().resize_avoided, 1);
3431 assert_eq!(adb.stats().resize_growth, 1);
3432 }
3433
3434 #[test]
3435 fn adaptive_buffer_one_dimension_exceeds_capacity() {
3436 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3437
3438 assert!(adb.resize(105, 24)); assert_eq!(adb.stats().resize_reallocated, 1);
3441 }
3442
3443 #[test]
3444 fn adaptive_buffer_current_and_previous_distinct() {
3445 let mut adb = AdaptiveDoubleBuffer::new(10, 5);
3446 adb.current_mut().set(0, 0, Cell::from_char('X'));
3447
3448 assert!(adb.previous().get(0, 0).unwrap().is_empty());
3450 assert_eq!(
3451 adb.current().get(0, 0).unwrap().content.as_char(),
3452 Some('X')
3453 );
3454 }
3455
3456 #[test]
3457 fn adaptive_buffer_resize_within_capacity_clears_previous() {
3458 let mut adb = AdaptiveDoubleBuffer::new(10, 5);
3459 adb.current_mut().set(9, 4, Cell::from_char('X'));
3460 adb.swap();
3461
3462 assert!(adb.resize(8, 4));
3464
3465 assert!(adb.previous().get(9, 4).unwrap().is_empty());
3467 }
3468
3469 #[test]
3471 fn adaptive_buffer_invariant_capacity_geq_logical() {
3472 for width in [1u16, 10, 80, 200, 1000, 5000] {
3474 for height in [1u16, 10, 24, 100, 500, 2000] {
3475 let adb = AdaptiveDoubleBuffer::new(width, height);
3476 assert!(
3477 adb.capacity_width() >= adb.width(),
3478 "capacity_width {} < logical_width {} for ({}, {})",
3479 adb.capacity_width(),
3480 adb.width(),
3481 width,
3482 height
3483 );
3484 assert!(
3485 adb.capacity_height() >= adb.height(),
3486 "capacity_height {} < logical_height {} for ({}, {})",
3487 adb.capacity_height(),
3488 adb.height(),
3489 width,
3490 height
3491 );
3492 }
3493 }
3494 }
3495
3496 #[test]
3497 fn adaptive_buffer_invariant_resize_dimensions_correct() {
3498 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3499
3500 let test_sizes = [
3502 (100, 50),
3503 (40, 20),
3504 (80, 24),
3505 (200, 100),
3506 (10, 5),
3507 (1000, 500),
3508 ];
3509 for (w, h) in test_sizes {
3510 adb.resize(w, h);
3511 assert_eq!(adb.width(), w, "width mismatch for ({}, {})", w, h);
3512 assert_eq!(adb.height(), h, "height mismatch for ({}, {})", w, h);
3513 assert!(
3514 adb.capacity_width() >= w,
3515 "capacity_width < width for ({}, {})",
3516 w,
3517 h
3518 );
3519 assert!(
3520 adb.capacity_height() >= h,
3521 "capacity_height < height for ({}, {})",
3522 w,
3523 h
3524 );
3525 }
3526 }
3527
3528 #[test]
3532 fn adaptive_buffer_no_ghosting_on_shrink() {
3533 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3534
3535 for y in 0..adb.height() {
3537 for x in 0..adb.width() {
3538 adb.current_mut().set(x, y, Cell::from_char('X'));
3539 }
3540 }
3541
3542 adb.resize(60, 20);
3545
3546 for y in 0..adb.height() {
3549 for x in 0..adb.width() {
3550 let cell = adb.current().get(x, y).unwrap();
3551 assert!(
3552 cell.is_empty(),
3553 "Ghost content at ({}, {}): expected empty, got {:?}",
3554 x,
3555 y,
3556 cell.content
3557 );
3558 }
3559 }
3560 }
3561
3562 #[test]
3566 fn adaptive_buffer_no_ghosting_on_reallocation_shrink() {
3567 let mut adb = AdaptiveDoubleBuffer::new(100, 50);
3568
3569 for y in 0..adb.height() {
3571 for x in 0..adb.width() {
3572 adb.current_mut().set(x, y, Cell::from_char('A'));
3573 }
3574 }
3575 adb.swap();
3576 for y in 0..adb.height() {
3577 for x in 0..adb.width() {
3578 adb.current_mut().set(x, y, Cell::from_char('B'));
3579 }
3580 }
3581
3582 adb.resize(30, 15);
3584 assert_eq!(adb.stats().resize_reallocated, 1);
3585
3586 for y in 0..adb.height() {
3588 for x in 0..adb.width() {
3589 assert!(
3590 adb.current().get(x, y).unwrap().is_empty(),
3591 "Ghost in current at ({}, {})",
3592 x,
3593 y
3594 );
3595 assert!(
3596 adb.previous().get(x, y).unwrap().is_empty(),
3597 "Ghost in previous at ({}, {})",
3598 x,
3599 y
3600 );
3601 }
3602 }
3603 }
3604
3605 #[test]
3609 fn adaptive_buffer_no_ghosting_on_growth_reallocation() {
3610 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3611
3612 for y in 0..adb.height() {
3614 for x in 0..adb.width() {
3615 adb.current_mut().set(x, y, Cell::from_char('Z'));
3616 }
3617 }
3618
3619 adb.resize(150, 60);
3621 assert_eq!(adb.stats().resize_reallocated, 1);
3622
3623 for y in 0..adb.height() {
3625 for x in 0..adb.width() {
3626 assert!(
3627 adb.current().get(x, y).unwrap().is_empty(),
3628 "Ghost at ({}, {}) after growth reallocation",
3629 x,
3630 y
3631 );
3632 }
3633 }
3634 }
3635
3636 #[test]
3638 fn adaptive_buffer_resize_idempotent() {
3639 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3640 adb.current_mut().set(5, 5, Cell::from_char('K'));
3641
3642 let changed = adb.resize(80, 24);
3644 assert!(!changed);
3645
3646 assert_eq!(
3648 adb.current().get(5, 5).unwrap().content.as_char(),
3649 Some('K')
3650 );
3651 }
3652
3653 #[test]
3658 fn dirty_span_merge_adjacent() {
3659 let mut buf = Buffer::new(100, 1);
3660 buf.clear_dirty(); buf.mark_dirty_span(0, 10, 20);
3664 let spans = buf.dirty_span_row(0).unwrap().spans();
3665 assert_eq!(spans.len(), 1);
3666 assert_eq!(spans[0], DirtySpan::new(10, 20));
3667
3668 buf.mark_dirty_span(0, 20, 30);
3670 let spans = buf.dirty_span_row(0).unwrap().spans();
3671 assert_eq!(spans.len(), 1);
3672 assert_eq!(spans[0], DirtySpan::new(10, 30));
3673 }
3674
3675 #[test]
3676 fn dirty_span_merge_overlapping() {
3677 let mut buf = Buffer::new(100, 1);
3678 buf.clear_dirty();
3679
3680 buf.mark_dirty_span(0, 10, 20);
3682 buf.mark_dirty_span(0, 15, 25);
3684
3685 let spans = buf.dirty_span_row(0).unwrap().spans();
3686 assert_eq!(spans.len(), 1);
3687 assert_eq!(spans[0], DirtySpan::new(10, 25));
3688 }
3689
3690 #[test]
3691 fn dirty_span_merge_with_gap() {
3692 let mut buf = Buffer::new(100, 1);
3693 buf.clear_dirty();
3694
3695 buf.mark_dirty_span(0, 10, 20);
3698 buf.mark_dirty_span(0, 21, 30);
3700
3701 let spans = buf.dirty_span_row(0).unwrap().spans();
3702 assert_eq!(spans.len(), 1);
3703 assert_eq!(spans[0], DirtySpan::new(10, 30));
3704 }
3705
3706 #[test]
3707 fn dirty_span_no_merge_large_gap() {
3708 let mut buf = Buffer::new(100, 1);
3709 buf.clear_dirty();
3710
3711 buf.mark_dirty_span(0, 10, 20);
3713 buf.mark_dirty_span(0, 22, 30);
3715
3716 let spans = buf.dirty_span_row(0).unwrap().spans();
3717 assert_eq!(spans.len(), 2);
3718 assert_eq!(spans[0], DirtySpan::new(10, 20));
3719 assert_eq!(spans[1], DirtySpan::new(22, 30));
3720 }
3721
3722 #[test]
3723 fn dirty_span_overflow_to_full() {
3724 let mut buf = Buffer::new(1000, 1);
3725 buf.clear_dirty();
3726
3727 for i in 0..DIRTY_SPAN_MAX_SPANS_PER_ROW + 10 {
3729 let start = (i * 4) as u16;
3730 buf.mark_dirty_span(0, start, start + 1);
3731 }
3732
3733 let row = buf.dirty_span_row(0).unwrap();
3734 assert!(row.is_full(), "Row should overflow to full scan");
3735 assert!(
3736 row.spans().is_empty(),
3737 "Spans should be cleared on overflow"
3738 );
3739 }
3740
3741 #[test]
3742 fn dirty_span_bounds_clamping() {
3743 let mut buf = Buffer::new(10, 1);
3744 buf.clear_dirty();
3745
3746 buf.mark_dirty_span(0, 15, 20);
3748 let spans = buf.dirty_span_row(0).unwrap().spans();
3749 assert!(spans.is_empty());
3750
3751 buf.mark_dirty_span(0, 8, 15);
3753 let spans = buf.dirty_span_row(0).unwrap().spans();
3754 assert_eq!(spans.len(), 1);
3755 assert_eq!(spans[0], DirtySpan::new(8, 10)); }
3757
3758 #[test]
3759 fn dirty_span_guard_band_clamps_bounds() {
3760 let mut buf = Buffer::new(10, 1);
3761 buf.clear_dirty();
3762 buf.set_dirty_span_config(DirtySpanConfig::default().with_guard_band(5));
3763
3764 buf.mark_dirty_span(0, 2, 3);
3765 let spans = buf.dirty_span_row(0).unwrap().spans();
3766 assert_eq!(spans.len(), 1);
3767 assert_eq!(spans[0], DirtySpan::new(0, 8));
3768
3769 buf.clear_dirty();
3770 buf.mark_dirty_span(0, 8, 10);
3771 let spans = buf.dirty_span_row(0).unwrap().spans();
3772 assert_eq!(spans.len(), 1);
3773 assert_eq!(spans[0], DirtySpan::new(3, 10));
3774 }
3775
3776 #[test]
3777 fn dirty_span_empty_span_is_ignored() {
3778 let mut buf = Buffer::new(10, 1);
3779 buf.clear_dirty();
3780 buf.mark_dirty_span(0, 5, 5);
3781 let spans = buf.dirty_span_row(0).unwrap().spans();
3782 assert!(spans.is_empty());
3783 }
3784}