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 while back_x > 0 {
703 back_x -= 1;
704 if let Some(h_idx) = self.index(back_x, y) {
705 let h_cell = self.cells[h_idx];
706 if !h_cell.is_continuation() {
707 let width = h_cell.content.width();
709 if (back_x as usize + width) > x as usize {
710 self.cells[h_idx] = Cell::default();
713 touched = true;
714 min_x = min_x.min(back_x);
715 max_x = max_x.max(back_x);
716
717 for i in 1..width {
720 let Some(cx) = back_x.checked_add(i as u16) else {
721 break;
722 };
723 if let Some(tail_idx) = self.index(cx, y) {
724 if self.cells[tail_idx].is_continuation() {
727 self.cells[tail_idx] = Cell::default();
728 touched = true;
729 min_x = min_x.min(cx);
730 max_x = max_x.max(cx);
731 }
732 }
733 }
734 }
735 break;
736 }
737 }
738 }
739 }
740
741 if touched {
742 Some(DirtySpan::new(min_x, max_x.saturating_add(1)))
743 } else {
744 None
745 }
746 }
747
748 #[inline]
763 pub fn set_fast(&mut self, x: u16, y: u16, cell: Cell) {
764 let bg_a = cell.bg.a();
771 if cell.content.width() > 1 || cell.is_continuation() || (bg_a != 255 && bg_a != 0) {
772 return self.set(x, y, cell);
773 }
774
775 if self.scissor_stack.len() != 1 || self.opacity_stack.len() != 1 {
777 return self.set(x, y, cell);
778 }
779
780 let Some(idx) = self.index(x, y) else {
782 return;
783 };
784
785 let existing = self.cells[idx];
789 if existing.content.width() > 1 || existing.is_continuation() {
790 return self.set(x, y, cell);
791 }
792
793 let mut final_cell = cell;
799 if bg_a == 0 {
800 final_cell.bg = existing.bg;
801 }
802
803 self.cells[idx] = final_cell;
804 self.mark_dirty_span(y, x, x.saturating_add(1));
805 }
806
807 #[inline]
819 pub fn set(&mut self, x: u16, y: u16, cell: Cell) {
820 let width = cell.content.width();
821
822 if width <= 1 {
824 let Some(idx) = self.index(x, y) else {
826 return;
827 };
828
829 if !self.current_scissor().contains(x, y) {
831 return;
832 }
833
834 let mut span_start = x;
836 let mut span_end = x.saturating_add(1);
837 if let Some(span) = self.cleanup_overlap(x, y, &cell) {
838 span_start = span_start.min(span.x0);
839 span_end = span_end.max(span.x1);
840 }
841
842 let existing_bg = self.cells[idx].bg;
843
844 let mut final_cell = if self.current_opacity() < 1.0 {
846 let opacity = self.current_opacity();
847 Cell {
848 fg: cell.fg.with_opacity(opacity),
849 bg: cell.bg.with_opacity(opacity),
850 ..cell
851 }
852 } else {
853 cell
854 };
855
856 final_cell.bg = final_cell.bg.over(existing_bg);
857
858 self.cells[idx] = final_cell;
859 self.mark_dirty_span(y, span_start, span_end);
860 return;
861 }
862
863 let scissor = self.current_scissor();
866 for i in 0..width {
867 let Some(cx) = x.checked_add(i as u16) else {
868 return;
869 };
870 if cx >= self.width || y >= self.height {
872 return;
873 }
874 if !scissor.contains(cx, y) {
876 return;
877 }
878 }
879
880 let mut span_start = x;
884 let mut span_end = x.saturating_add(width as u16);
885 if let Some(span) = self.cleanup_overlap(x, y, &cell) {
886 span_start = span_start.min(span.x0);
887 span_end = span_end.max(span.x1);
888 }
889 for i in 1..width {
890 if let Some(span) = self.cleanup_overlap(x + i as u16, y, &Cell::CONTINUATION) {
892 span_start = span_start.min(span.x0);
893 span_end = span_end.max(span.x1);
894 }
895 }
896
897 let idx = self.index_unchecked(x, y);
899 let old_cell = self.cells[idx];
900 let mut final_cell = if self.current_opacity() < 1.0 {
901 let opacity = self.current_opacity();
902 Cell {
903 fg: cell.fg.with_opacity(opacity),
904 bg: cell.bg.with_opacity(opacity),
905 ..cell
906 }
907 } else {
908 cell
909 };
910
911 final_cell.bg = final_cell.bg.over(old_cell.bg);
913
914 self.cells[idx] = final_cell;
915
916 for i in 1..width {
919 let idx = self.index_unchecked(x + i as u16, y);
920 self.cells[idx] = Cell::CONTINUATION;
921 }
922 self.mark_dirty_span(y, span_start, span_end);
923 }
924
925 #[inline]
930 pub fn set_raw(&mut self, x: u16, y: u16, cell: Cell) {
931 if let Some(idx) = self.index(x, y) {
932 self.cells[idx] = cell;
933 self.mark_dirty_span(y, x, x.saturating_add(1));
934 }
935 }
936
937 #[inline]
941 pub fn fill(&mut self, rect: Rect, cell: Cell) {
942 let clipped = self.current_scissor().intersection(&rect);
943 if clipped.is_empty() {
944 return;
945 }
946
947 let cell_width = cell.content.width();
950 if cell_width <= 1
951 && !cell.is_continuation()
952 && self.current_opacity() >= 1.0
953 && cell.bg.a() == 255
954 && clipped.x == 0
955 && clipped.width == self.width
956 {
957 let row_width = self.width as usize;
958 for y in clipped.y..clipped.bottom() {
959 let row_start = y as usize * row_width;
960 let row_end = row_start + row_width;
961 self.cells[row_start..row_end].fill(cell);
962 self.mark_dirty_row_full(y);
963 }
964 return;
965 }
966
967 if cell_width <= 1
971 && !cell.is_continuation()
972 && self.current_opacity() >= 1.0
973 && cell.bg.a() == 255
974 && self.scissor_stack.len() == 1
975 {
976 let row_width = self.width as usize;
977 let x_start = clipped.x as usize;
978 let x_end = clipped.right() as usize;
979 for y in clipped.y..clipped.bottom() {
980 let row_start = y as usize * row_width;
981 let mut dirty_left = clipped.x;
982 let mut dirty_right = clipped.right();
983
984 if x_start > 0 && self.cells[row_start + x_start].is_continuation() {
987 for hx in (0..x_start).rev() {
988 let c = self.cells[row_start + hx];
989 if c.is_continuation() {
990 self.cells[row_start + hx] = Cell::default();
991 dirty_left = hx as u16;
992 } else {
993 if c.content.width() > 1 {
994 self.cells[row_start + hx] = Cell::default();
995 dirty_left = hx as u16;
996 }
997 break;
998 }
999 }
1000 }
1001
1002 {
1005 let mut cx = x_end;
1006 while cx < row_width && self.cells[row_start + cx].is_continuation() {
1007 self.cells[row_start + cx] = Cell::default();
1008 dirty_right = (cx as u16).saturating_add(1);
1009 cx += 1;
1010 }
1011 }
1012
1013 self.cells[row_start + x_start..row_start + x_end].fill(cell);
1014 self.mark_dirty_span(y, dirty_left, dirty_right);
1015 }
1016 return;
1017 }
1018
1019 for y in clipped.y..clipped.bottom() {
1020 for x in clipped.x..clipped.right() {
1021 self.set(x, y, cell);
1022 }
1023 }
1024 }
1025
1026 #[inline]
1028 pub fn clear(&mut self) {
1029 self.cells.fill(Cell::default());
1030 self.mark_all_dirty();
1031 }
1032
1033 pub fn reset_for_frame(&mut self) {
1038 self.scissor_stack.truncate(1);
1039 if let Some(base) = self.scissor_stack.first_mut() {
1040 *base = Rect::from_size(self.width, self.height);
1041 } else {
1042 self.scissor_stack
1043 .push(Rect::from_size(self.width, self.height));
1044 }
1045
1046 self.opacity_stack.truncate(1);
1047 if let Some(base) = self.opacity_stack.first_mut() {
1048 *base = 1.0;
1049 } else {
1050 self.opacity_stack.push(1.0);
1051 }
1052
1053 self.clear();
1054 }
1055
1056 #[inline]
1058 pub fn clear_with(&mut self, cell: Cell) {
1059 self.cells.fill(cell);
1060 self.mark_all_dirty();
1061 }
1062
1063 #[inline]
1067 pub fn cells(&self) -> &[Cell] {
1068 &self.cells
1069 }
1070
1071 #[inline]
1075 pub fn cells_mut(&mut self) -> &mut [Cell] {
1076 self.mark_all_dirty();
1077 &mut self.cells
1078 }
1079
1080 #[inline]
1086 pub fn row_cells(&self, y: u16) -> &[Cell] {
1087 let start = y as usize * self.width as usize;
1088 &self.cells[start..start + self.width as usize]
1089 }
1090
1091 #[inline]
1098 pub fn push_scissor(&mut self, rect: Rect) {
1099 let current = self.current_scissor();
1100 let intersected = current.intersection(&rect);
1101 self.scissor_stack.push(intersected);
1102 }
1103
1104 #[inline]
1108 pub fn pop_scissor(&mut self) {
1109 if self.scissor_stack.len() > 1 {
1110 self.scissor_stack.pop();
1111 }
1112 }
1113
1114 #[inline]
1116 pub fn current_scissor(&self) -> Rect {
1117 *self
1118 .scissor_stack
1119 .last()
1120 .expect("scissor stack always has at least one element")
1121 }
1122
1123 #[inline]
1125 pub fn scissor_depth(&self) -> usize {
1126 self.scissor_stack.len()
1127 }
1128
1129 #[inline]
1136 pub fn push_opacity(&mut self, opacity: f32) {
1137 let clamped = opacity.clamp(0.0, 1.0);
1138 let current = self.current_opacity();
1139 self.opacity_stack.push(current * clamped);
1140 }
1141
1142 #[inline]
1146 pub fn pop_opacity(&mut self) {
1147 if self.opacity_stack.len() > 1 {
1148 self.opacity_stack.pop();
1149 }
1150 }
1151
1152 #[inline]
1154 pub fn current_opacity(&self) -> f32 {
1155 *self
1156 .opacity_stack
1157 .last()
1158 .expect("opacity stack always has at least one element")
1159 }
1160
1161 #[inline]
1163 pub fn opacity_depth(&self) -> usize {
1164 self.opacity_stack.len()
1165 }
1166
1167 pub fn copy_from(&mut self, src: &Buffer, src_rect: Rect, dst_x: u16, dst_y: u16) {
1174 let copy_bounds = Rect::new(dst_x, dst_y, src_rect.width, src_rect.height);
1177 self.push_scissor(copy_bounds);
1178
1179 for dy in 0..src_rect.height {
1180 let Some(target_y) = dst_y.checked_add(dy) else {
1182 continue;
1183 };
1184 let Some(sy) = src_rect.y.checked_add(dy) else {
1185 continue;
1186 };
1187
1188 let mut dx = 0u16;
1189 while dx < src_rect.width {
1190 let Some(target_x) = dst_x.checked_add(dx) else {
1192 dx = dx.saturating_add(1);
1193 continue;
1194 };
1195 let Some(sx) = src_rect.x.checked_add(dx) else {
1196 dx = dx.saturating_add(1);
1197 continue;
1198 };
1199
1200 if let Some(cell) = src.get(sx, sy) {
1201 if cell.is_continuation() {
1205 self.set(target_x, target_y, Cell::default());
1206 dx = dx.saturating_add(1);
1207 continue;
1208 }
1209
1210 let width = cell.content.width();
1211
1212 if width > 1 && dx.saturating_add(width as u16) > src_rect.width {
1216 self.set(target_x, target_y, Cell::default());
1217 } else {
1218 self.set(target_x, target_y, *cell);
1219 }
1220
1221 if width > 1 {
1223 dx = dx.saturating_add(width as u16);
1224 } else {
1225 dx = dx.saturating_add(1);
1226 }
1227 } else {
1228 dx = dx.saturating_add(1);
1229 }
1230 }
1231 }
1232
1233 self.pop_scissor();
1234 }
1235
1236 pub fn content_eq(&self, other: &Buffer) -> bool {
1238 self.width == other.width && self.height == other.height && self.cells == other.cells
1239 }
1240}
1241
1242impl Default for Buffer {
1243 fn default() -> Self {
1245 Self::new(1, 1)
1246 }
1247}
1248
1249impl PartialEq for Buffer {
1250 fn eq(&self, other: &Self) -> bool {
1251 self.content_eq(other)
1252 }
1253}
1254
1255impl Eq for Buffer {}
1256
1257#[derive(Debug)]
1276pub struct DoubleBuffer {
1277 buffers: [Buffer; 2],
1278 current_idx: u8,
1280}
1281
1282const ADAPTIVE_GROWTH_FACTOR: f32 = 1.25;
1288
1289const ADAPTIVE_SHRINK_THRESHOLD: f32 = 0.50;
1292
1293const ADAPTIVE_MAX_OVERAGE: u16 = 200;
1295
1296#[derive(Debug)]
1328pub struct AdaptiveDoubleBuffer {
1329 inner: DoubleBuffer,
1331 logical_width: u16,
1333 logical_height: u16,
1335 capacity_width: u16,
1337 capacity_height: u16,
1339 stats: AdaptiveStats,
1341}
1342
1343#[derive(Debug, Clone, Default)]
1345pub struct AdaptiveStats {
1346 pub resize_avoided: u64,
1348 pub resize_reallocated: u64,
1350 pub resize_growth: u64,
1352 pub resize_shrink: u64,
1354}
1355
1356impl AdaptiveStats {
1357 pub fn reset(&mut self) {
1359 *self = Self::default();
1360 }
1361
1362 pub fn avoidance_ratio(&self) -> f64 {
1364 let total = self.resize_avoided + self.resize_reallocated;
1365 if total == 0 {
1366 1.0
1367 } else {
1368 self.resize_avoided as f64 / total as f64
1369 }
1370 }
1371}
1372
1373impl DoubleBuffer {
1374 pub fn new(width: u16, height: u16) -> Self {
1382 Self {
1383 buffers: [Buffer::new(width, height), Buffer::new(width, height)],
1384 current_idx: 0,
1385 }
1386 }
1387
1388 #[inline]
1393 pub fn swap(&mut self) {
1394 self.current_idx = 1 - self.current_idx;
1395 }
1396
1397 #[inline]
1399 pub fn current(&self) -> &Buffer {
1400 &self.buffers[self.current_idx as usize]
1401 }
1402
1403 #[inline]
1405 pub fn current_mut(&mut self) -> &mut Buffer {
1406 &mut self.buffers[self.current_idx as usize]
1407 }
1408
1409 #[inline]
1411 pub fn previous(&self) -> &Buffer {
1412 &self.buffers[(1 - self.current_idx) as usize]
1413 }
1414
1415 #[inline]
1417 pub fn previous_mut(&mut self) -> &mut Buffer {
1418 &mut self.buffers[(1 - self.current_idx) as usize]
1419 }
1420
1421 #[inline]
1423 pub fn width(&self) -> u16 {
1424 self.buffers[0].width()
1425 }
1426
1427 #[inline]
1429 pub fn height(&self) -> u16 {
1430 self.buffers[0].height()
1431 }
1432
1433 pub fn resize(&mut self, width: u16, height: u16) -> bool {
1438 if self.buffers[0].width() == width && self.buffers[0].height() == height {
1439 return false;
1440 }
1441 self.buffers = [Buffer::new(width, height), Buffer::new(width, height)];
1442 self.current_idx = 0;
1443 true
1444 }
1445
1446 #[inline]
1448 pub fn dimensions_match(&self, width: u16, height: u16) -> bool {
1449 self.buffers[0].width() == width && self.buffers[0].height() == height
1450 }
1451}
1452
1453impl AdaptiveDoubleBuffer {
1458 pub fn new(width: u16, height: u16) -> Self {
1466 let (cap_w, cap_h) = Self::compute_capacity(width, height);
1467 Self {
1468 inner: DoubleBuffer::new(cap_w, cap_h),
1469 logical_width: width,
1470 logical_height: height,
1471 capacity_width: cap_w,
1472 capacity_height: cap_h,
1473 stats: AdaptiveStats::default(),
1474 }
1475 }
1476
1477 fn compute_capacity(width: u16, height: u16) -> (u16, u16) {
1481 let extra_w =
1482 ((width as f32 * (ADAPTIVE_GROWTH_FACTOR - 1.0)) as u16).min(ADAPTIVE_MAX_OVERAGE);
1483 let extra_h =
1484 ((height as f32 * (ADAPTIVE_GROWTH_FACTOR - 1.0)) as u16).min(ADAPTIVE_MAX_OVERAGE);
1485
1486 let cap_w = width.saturating_add(extra_w);
1487 let cap_h = height.saturating_add(extra_h);
1488
1489 (cap_w, cap_h)
1490 }
1491
1492 fn needs_reallocation(&self, width: u16, height: u16) -> bool {
1496 if width > self.capacity_width || height > self.capacity_height {
1498 return true;
1499 }
1500
1501 let shrink_threshold_w = (self.capacity_width as f32 * ADAPTIVE_SHRINK_THRESHOLD) as u16;
1503 let shrink_threshold_h = (self.capacity_height as f32 * ADAPTIVE_SHRINK_THRESHOLD) as u16;
1504
1505 width < shrink_threshold_w || height < shrink_threshold_h
1506 }
1507
1508 #[inline]
1513 pub fn swap(&mut self) {
1514 self.inner.swap();
1515 }
1516
1517 #[inline]
1522 pub fn current(&self) -> &Buffer {
1523 self.inner.current()
1524 }
1525
1526 #[inline]
1528 pub fn current_mut(&mut self) -> &mut Buffer {
1529 self.inner.current_mut()
1530 }
1531
1532 #[inline]
1534 pub fn previous(&self) -> &Buffer {
1535 self.inner.previous()
1536 }
1537
1538 #[inline]
1540 pub fn width(&self) -> u16 {
1541 self.logical_width
1542 }
1543
1544 #[inline]
1546 pub fn height(&self) -> u16 {
1547 self.logical_height
1548 }
1549
1550 #[inline]
1552 pub fn capacity_width(&self) -> u16 {
1553 self.capacity_width
1554 }
1555
1556 #[inline]
1558 pub fn capacity_height(&self) -> u16 {
1559 self.capacity_height
1560 }
1561
1562 #[inline]
1564 pub fn stats(&self) -> &AdaptiveStats {
1565 &self.stats
1566 }
1567
1568 pub fn reset_stats(&mut self) {
1570 self.stats.reset();
1571 }
1572
1573 pub fn resize(&mut self, width: u16, height: u16) -> bool {
1585 if width == self.logical_width && height == self.logical_height {
1587 return false;
1588 }
1589
1590 let is_growth = width > self.logical_width || height > self.logical_height;
1591 if is_growth {
1592 self.stats.resize_growth += 1;
1593 } else {
1594 self.stats.resize_shrink += 1;
1595 }
1596
1597 if self.needs_reallocation(width, height) {
1598 let (cap_w, cap_h) = Self::compute_capacity(width, height);
1600 self.inner = DoubleBuffer::new(cap_w, cap_h);
1601 self.capacity_width = cap_w;
1602 self.capacity_height = cap_h;
1603 self.stats.resize_reallocated += 1;
1604 } else {
1605 self.inner.current_mut().clear();
1608 self.inner.previous_mut().clear();
1609 self.stats.resize_avoided += 1;
1610 }
1611
1612 self.logical_width = width;
1613 self.logical_height = height;
1614 true
1615 }
1616
1617 #[inline]
1619 pub fn dimensions_match(&self, width: u16, height: u16) -> bool {
1620 self.logical_width == width && self.logical_height == height
1621 }
1622
1623 #[inline]
1625 pub fn logical_bounds(&self) -> Rect {
1626 Rect::from_size(self.logical_width, self.logical_height)
1627 }
1628
1629 pub fn memory_efficiency(&self) -> f64 {
1631 let logical = self.logical_width as u64 * self.logical_height as u64;
1632 let capacity = self.capacity_width as u64 * self.capacity_height as u64;
1633 if capacity == 0 {
1634 1.0
1635 } else {
1636 logical as f64 / capacity as f64
1637 }
1638 }
1639}
1640
1641#[cfg(test)]
1642mod tests {
1643 use super::*;
1644 use crate::cell::PackedRgba;
1645
1646 #[test]
1647 fn set_composites_background() {
1648 let mut buf = Buffer::new(1, 1);
1649
1650 let red = PackedRgba::rgb(255, 0, 0);
1652 buf.set(0, 0, Cell::default().with_bg(red));
1653
1654 let cell = Cell::from_char('X'); buf.set(0, 0, cell);
1657
1658 let result = buf.get(0, 0).unwrap();
1659 assert_eq!(result.content.as_char(), Some('X'));
1660 assert_eq!(
1661 result.bg, red,
1662 "Background should be preserved (composited)"
1663 );
1664 }
1665
1666 #[test]
1667 fn set_fast_matches_set_for_transparent_bg() {
1668 let red = PackedRgba::rgb(255, 0, 0);
1669 let cell = Cell::from_char('X').with_fg(PackedRgba::rgb(0, 255, 0));
1670
1671 let mut a = Buffer::new(1, 1);
1672 a.set(0, 0, Cell::default().with_bg(red));
1673 a.set(0, 0, cell);
1674
1675 let mut b = Buffer::new(1, 1);
1676 b.set(0, 0, Cell::default().with_bg(red));
1677 b.set_fast(0, 0, cell);
1678
1679 assert_eq!(a.get(0, 0), b.get(0, 0));
1680 }
1681
1682 #[test]
1683 fn set_fast_matches_set_for_opaque_bg() {
1684 let cell = Cell::from_char('X')
1685 .with_fg(PackedRgba::rgb(0, 255, 0))
1686 .with_bg(PackedRgba::rgb(255, 0, 0));
1687
1688 let mut a = Buffer::new(1, 1);
1689 a.set(0, 0, cell);
1690
1691 let mut b = Buffer::new(1, 1);
1692 b.set_fast(0, 0, cell);
1693
1694 assert_eq!(a.get(0, 0), b.get(0, 0));
1695 }
1696
1697 #[test]
1698 fn rect_contains() {
1699 let r = Rect::new(5, 5, 10, 10);
1700 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)); }
1706
1707 #[test]
1708 fn rect_intersection() {
1709 let a = Rect::new(0, 0, 10, 10);
1710 let b = Rect::new(5, 5, 10, 10);
1711 let i = a.intersection(&b);
1712 assert_eq!(i, Rect::new(5, 5, 5, 5));
1713
1714 let c = Rect::new(20, 20, 5, 5);
1716 assert_eq!(a.intersection(&c), Rect::default());
1717 }
1718
1719 #[test]
1720 fn buffer_creation() {
1721 let buf = Buffer::new(80, 24);
1722 assert_eq!(buf.width(), 80);
1723 assert_eq!(buf.height(), 24);
1724 assert_eq!(buf.len(), 80 * 24);
1725 }
1726
1727 #[test]
1728 fn content_height_empty_is_zero() {
1729 let buf = Buffer::new(8, 4);
1730 assert_eq!(buf.content_height(), 0);
1731 }
1732
1733 #[test]
1734 fn content_height_tracks_last_non_empty_row() {
1735 let mut buf = Buffer::new(5, 4);
1736 buf.set(0, 0, Cell::from_char('A'));
1737 assert_eq!(buf.content_height(), 1);
1738
1739 buf.set(2, 3, Cell::from_char('Z'));
1740 assert_eq!(buf.content_height(), 4);
1741 }
1742
1743 #[test]
1744 #[should_panic(expected = "width must be > 0")]
1745 fn buffer_zero_width_panics() {
1746 Buffer::new(0, 24);
1747 }
1748
1749 #[test]
1750 #[should_panic(expected = "height must be > 0")]
1751 fn buffer_zero_height_panics() {
1752 Buffer::new(80, 0);
1753 }
1754
1755 #[test]
1756 fn buffer_get_and_set() {
1757 let mut buf = Buffer::new(10, 10);
1758 let cell = Cell::from_char('X');
1759 buf.set(5, 5, cell);
1760 assert_eq!(buf.get(5, 5).unwrap().content.as_char(), Some('X'));
1761 }
1762
1763 #[test]
1764 fn buffer_out_of_bounds_get() {
1765 let buf = Buffer::new(10, 10);
1766 assert!(buf.get(10, 0).is_none());
1767 assert!(buf.get(0, 10).is_none());
1768 assert!(buf.get(100, 100).is_none());
1769 }
1770
1771 #[test]
1772 fn buffer_out_of_bounds_set_ignored() {
1773 let mut buf = Buffer::new(10, 10);
1774 buf.set(100, 100, Cell::from_char('X')); assert_eq!(buf.cells().iter().filter(|c| !c.is_empty()).count(), 0);
1776 }
1777
1778 #[test]
1779 fn buffer_clear() {
1780 let mut buf = Buffer::new(10, 10);
1781 buf.set(5, 5, Cell::from_char('X'));
1782 buf.clear();
1783 assert!(buf.get(5, 5).unwrap().is_empty());
1784 }
1785
1786 #[test]
1787 fn scissor_stack_basic() {
1788 let mut buf = Buffer::new(20, 20);
1789
1790 assert_eq!(buf.current_scissor(), Rect::from_size(20, 20));
1792 assert_eq!(buf.scissor_depth(), 1);
1793
1794 buf.push_scissor(Rect::new(5, 5, 10, 10));
1796 assert_eq!(buf.current_scissor(), Rect::new(5, 5, 10, 10));
1797 assert_eq!(buf.scissor_depth(), 2);
1798
1799 buf.set(7, 7, Cell::from_char('I'));
1801 assert_eq!(buf.get(7, 7).unwrap().content.as_char(), Some('I'));
1802
1803 buf.set(0, 0, Cell::from_char('O'));
1805 assert!(buf.get(0, 0).unwrap().is_empty());
1806
1807 buf.pop_scissor();
1809 assert_eq!(buf.current_scissor(), Rect::from_size(20, 20));
1810 assert_eq!(buf.scissor_depth(), 1);
1811
1812 buf.set(0, 0, Cell::from_char('N'));
1814 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('N'));
1815 }
1816
1817 #[test]
1818 fn scissor_intersection() {
1819 let mut buf = Buffer::new(20, 20);
1820 buf.push_scissor(Rect::new(5, 5, 10, 10));
1821 buf.push_scissor(Rect::new(8, 8, 10, 10));
1822
1823 assert_eq!(buf.current_scissor(), Rect::new(8, 8, 7, 7));
1826 }
1827
1828 #[test]
1829 fn scissor_base_cannot_be_popped() {
1830 let mut buf = Buffer::new(10, 10);
1831 buf.pop_scissor(); assert_eq!(buf.scissor_depth(), 1);
1833 buf.pop_scissor(); assert_eq!(buf.scissor_depth(), 1);
1835 }
1836
1837 #[test]
1838 fn opacity_stack_basic() {
1839 let mut buf = Buffer::new(10, 10);
1840
1841 assert!((buf.current_opacity() - 1.0).abs() < f32::EPSILON);
1843 assert_eq!(buf.opacity_depth(), 1);
1844
1845 buf.push_opacity(0.5);
1847 assert!((buf.current_opacity() - 0.5).abs() < f32::EPSILON);
1848 assert_eq!(buf.opacity_depth(), 2);
1849
1850 buf.push_opacity(0.5);
1852 assert!((buf.current_opacity() - 0.25).abs() < f32::EPSILON);
1853 assert_eq!(buf.opacity_depth(), 3);
1854
1855 buf.pop_opacity();
1857 assert!((buf.current_opacity() - 0.5).abs() < f32::EPSILON);
1858 }
1859
1860 #[test]
1861 fn opacity_applied_to_cells() {
1862 let mut buf = Buffer::new(10, 10);
1863 buf.push_opacity(0.5);
1864
1865 let cell = Cell::from_char('X').with_fg(PackedRgba::rgba(100, 100, 100, 255));
1866 buf.set(5, 5, cell);
1867
1868 let stored = buf.get(5, 5).unwrap();
1869 assert_eq!(stored.fg.a(), 128);
1871 }
1872
1873 #[test]
1874 fn opacity_composites_background_before_storage() {
1875 let mut buf = Buffer::new(1, 1);
1876
1877 let red = PackedRgba::rgb(255, 0, 0);
1878 let blue = PackedRgba::rgb(0, 0, 255);
1879
1880 buf.set(0, 0, Cell::default().with_bg(red));
1881 buf.push_opacity(0.5);
1882 buf.set(0, 0, Cell::default().with_bg(blue));
1883
1884 let stored = buf.get(0, 0).unwrap();
1885 let expected = blue.with_opacity(0.5).over(red);
1886 assert_eq!(stored.bg, expected);
1887 }
1888
1889 #[test]
1890 fn opacity_clamped() {
1891 let mut buf = Buffer::new(10, 10);
1892 buf.push_opacity(2.0); assert!((buf.current_opacity() - 1.0).abs() < f32::EPSILON);
1894
1895 buf.push_opacity(-1.0); assert!((buf.current_opacity() - 0.0).abs() < f32::EPSILON);
1897 }
1898
1899 #[test]
1900 fn opacity_base_cannot_be_popped() {
1901 let mut buf = Buffer::new(10, 10);
1902 buf.pop_opacity(); assert_eq!(buf.opacity_depth(), 1);
1904 }
1905
1906 #[test]
1907 fn buffer_fill() {
1908 let mut buf = Buffer::new(10, 10);
1909 let cell = Cell::from_char('#');
1910 buf.fill(Rect::new(2, 2, 5, 5), cell);
1911
1912 assert_eq!(buf.get(3, 3).unwrap().content.as_char(), Some('#'));
1914
1915 assert!(buf.get(0, 0).unwrap().is_empty());
1917 }
1918
1919 #[test]
1920 fn buffer_fill_respects_scissor() {
1921 let mut buf = Buffer::new(10, 10);
1922 buf.push_scissor(Rect::new(3, 3, 4, 4));
1923
1924 let cell = Cell::from_char('#');
1925 buf.fill(Rect::new(0, 0, 10, 10), cell);
1926
1927 assert_eq!(buf.get(3, 3).unwrap().content.as_char(), Some('#'));
1929 assert!(buf.get(0, 0).unwrap().is_empty());
1930 assert!(buf.get(7, 7).unwrap().is_empty());
1931 }
1932
1933 #[test]
1934 fn buffer_copy_from() {
1935 let mut src = Buffer::new(10, 10);
1936 src.set(2, 2, Cell::from_char('S'));
1937
1938 let mut dst = Buffer::new(10, 10);
1939 dst.copy_from(&src, Rect::new(0, 0, 5, 5), 3, 3);
1940
1941 assert_eq!(dst.get(5, 5).unwrap().content.as_char(), Some('S'));
1943 }
1944
1945 #[test]
1946 fn copy_from_clips_wide_char_at_boundary() {
1947 let mut src = Buffer::new(10, 1);
1948 src.set(0, 0, Cell::from_char('中'));
1950
1951 let mut dst = Buffer::new(10, 1);
1952 dst.copy_from(&src, Rect::new(0, 0, 1, 1), 0, 0);
1955
1956 assert!(
1965 dst.get(0, 0).unwrap().is_empty(),
1966 "Wide char head should not be written if tail is clipped"
1967 );
1968 assert!(
1969 dst.get(1, 0).unwrap().is_empty(),
1970 "Wide char tail should not be leaked outside copy region"
1971 );
1972 }
1973
1974 #[test]
1975 fn buffer_content_eq() {
1976 let mut buf1 = Buffer::new(10, 10);
1977 let mut buf2 = Buffer::new(10, 10);
1978
1979 assert!(buf1.content_eq(&buf2));
1980
1981 buf1.set(0, 0, Cell::from_char('X'));
1982 assert!(!buf1.content_eq(&buf2));
1983
1984 buf2.set(0, 0, Cell::from_char('X'));
1985 assert!(buf1.content_eq(&buf2));
1986 }
1987
1988 #[test]
1989 fn buffer_bounds() {
1990 let buf = Buffer::new(80, 24);
1991 let bounds = buf.bounds();
1992 assert_eq!(bounds.x, 0);
1993 assert_eq!(bounds.y, 0);
1994 assert_eq!(bounds.width, 80);
1995 assert_eq!(bounds.height, 24);
1996 }
1997
1998 #[test]
1999 fn buffer_set_raw_bypasses_scissor() {
2000 let mut buf = Buffer::new(10, 10);
2001 buf.push_scissor(Rect::new(5, 5, 5, 5));
2002
2003 buf.set(0, 0, Cell::from_char('S'));
2005 assert!(buf.get(0, 0).unwrap().is_empty());
2006
2007 buf.set_raw(0, 0, Cell::from_char('R'));
2009 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('R'));
2010 }
2011
2012 #[test]
2013 fn set_handles_wide_chars() {
2014 let mut buf = Buffer::new(10, 10);
2015
2016 buf.set(0, 0, Cell::from_char('中'));
2018
2019 let head = buf.get(0, 0).unwrap();
2021 assert_eq!(head.content.as_char(), Some('中'));
2022
2023 let cont = buf.get(1, 0).unwrap();
2025 assert!(cont.is_continuation());
2026 assert!(!cont.is_empty());
2027 }
2028
2029 #[test]
2030 fn set_handles_wide_chars_clipped() {
2031 let mut buf = Buffer::new(10, 10);
2032 buf.push_scissor(Rect::new(0, 0, 1, 10)); buf.set(0, 0, Cell::from_char('中'));
2037
2038 assert!(buf.get(0, 0).unwrap().is_empty());
2040 assert!(buf.get(1, 0).unwrap().is_empty());
2042 }
2043
2044 #[test]
2047 fn overwrite_wide_head_with_single_clears_tails() {
2048 let mut buf = Buffer::new(10, 1);
2049
2050 buf.set(0, 0, Cell::from_char('中'));
2052 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2053 assert!(buf.get(1, 0).unwrap().is_continuation());
2054
2055 buf.set(0, 0, Cell::from_char('A'));
2057
2058 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('A'));
2060 assert!(
2062 buf.get(1, 0).unwrap().is_empty(),
2063 "Continuation at x=1 should be cleared when head is overwritten"
2064 );
2065 }
2066
2067 #[test]
2068 fn overwrite_continuation_with_single_clears_head_and_tails() {
2069 let mut buf = Buffer::new(10, 1);
2070
2071 buf.set(0, 0, Cell::from_char('中'));
2073 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2074 assert!(buf.get(1, 0).unwrap().is_continuation());
2075
2076 buf.set(1, 0, Cell::from_char('B'));
2078
2079 assert!(
2081 buf.get(0, 0).unwrap().is_empty(),
2082 "Head at x=0 should be cleared when its continuation is overwritten"
2083 );
2084 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('B'));
2086 }
2087
2088 #[test]
2089 fn overwrite_wide_with_another_wide() {
2090 let mut buf = Buffer::new(10, 1);
2091
2092 buf.set(0, 0, Cell::from_char('中'));
2094 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2095 assert!(buf.get(1, 0).unwrap().is_continuation());
2096
2097 buf.set(0, 0, Cell::from_char('日'));
2099
2100 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('日'));
2102 assert!(
2103 buf.get(1, 0).unwrap().is_continuation(),
2104 "Continuation should still exist for new wide char"
2105 );
2106 }
2107
2108 #[test]
2109 fn overwrite_continuation_middle_of_wide_sequence() {
2110 let mut buf = Buffer::new(10, 1);
2111
2112 buf.set(0, 0, Cell::from_char('中'));
2114 buf.set(2, 0, Cell::from_char('日'));
2115
2116 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2117 assert!(buf.get(1, 0).unwrap().is_continuation());
2118 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('日'));
2119 assert!(buf.get(3, 0).unwrap().is_continuation());
2120
2121 buf.set(1, 0, Cell::from_char('X'));
2123
2124 assert!(
2126 buf.get(0, 0).unwrap().is_empty(),
2127 "Head of first wide char should be cleared"
2128 );
2129 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('X'));
2131 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('日'));
2133 assert!(buf.get(3, 0).unwrap().is_continuation());
2134 }
2135
2136 #[test]
2137 fn wide_char_overlapping_previous_wide_char() {
2138 let mut buf = Buffer::new(10, 1);
2139
2140 buf.set(0, 0, Cell::from_char('中'));
2142 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2143 assert!(buf.get(1, 0).unwrap().is_continuation());
2144
2145 buf.set(1, 0, Cell::from_char('日'));
2147
2148 assert!(
2150 buf.get(0, 0).unwrap().is_empty(),
2151 "First wide char head should be cleared when continuation is overwritten by new wide"
2152 );
2153 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('日'));
2155 assert!(buf.get(2, 0).unwrap().is_continuation());
2156 }
2157
2158 #[test]
2159 fn wide_char_at_end_of_buffer_atomic_reject() {
2160 let mut buf = Buffer::new(5, 1);
2161
2162 buf.set(4, 0, Cell::from_char('中'));
2164
2165 assert!(
2167 buf.get(4, 0).unwrap().is_empty(),
2168 "Wide char should be rejected when tail would be out of bounds"
2169 );
2170 }
2171
2172 #[test]
2173 fn three_wide_chars_sequential_cleanup() {
2174 let mut buf = Buffer::new(10, 1);
2175
2176 buf.set(0, 0, Cell::from_char('一'));
2178 buf.set(2, 0, Cell::from_char('二'));
2179 buf.set(4, 0, Cell::from_char('三'));
2180
2181 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('一'));
2183 assert!(buf.get(1, 0).unwrap().is_continuation());
2184 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('二'));
2185 assert!(buf.get(3, 0).unwrap().is_continuation());
2186 assert_eq!(buf.get(4, 0).unwrap().content.as_char(), Some('三'));
2187 assert!(buf.get(5, 0).unwrap().is_continuation());
2188
2189 buf.set(3, 0, Cell::from_char('M'));
2191
2192 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('一'));
2194 assert!(buf.get(1, 0).unwrap().is_continuation());
2195 assert!(buf.get(2, 0).unwrap().is_empty());
2197 assert_eq!(buf.get(3, 0).unwrap().content.as_char(), Some('M'));
2199 assert_eq!(buf.get(4, 0).unwrap().content.as_char(), Some('三'));
2201 assert!(buf.get(5, 0).unwrap().is_continuation());
2202 }
2203
2204 #[test]
2205 fn overwrite_empty_cell_no_cleanup_needed() {
2206 let mut buf = Buffer::new(10, 1);
2207
2208 buf.set(5, 0, Cell::from_char('X'));
2210
2211 assert_eq!(buf.get(5, 0).unwrap().content.as_char(), Some('X'));
2212 assert!(buf.get(4, 0).unwrap().is_empty());
2214 assert!(buf.get(6, 0).unwrap().is_empty());
2215 }
2216
2217 #[test]
2218 fn wide_char_cleanup_with_opacity() {
2219 let mut buf = Buffer::new(10, 1);
2220
2221 buf.set(0, 0, Cell::default().with_bg(PackedRgba::rgb(255, 0, 0)));
2223 buf.set(1, 0, Cell::default().with_bg(PackedRgba::rgb(0, 255, 0)));
2224
2225 buf.set(0, 0, Cell::from_char('中'));
2227
2228 buf.push_opacity(0.5);
2230 buf.set(0, 0, Cell::from_char('A'));
2231 buf.pop_opacity();
2232
2233 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('A'));
2235 assert!(buf.get(1, 0).unwrap().is_empty());
2237 }
2238
2239 #[test]
2240 fn wide_char_continuation_not_treated_as_head() {
2241 let mut buf = Buffer::new(10, 1);
2242
2243 buf.set(0, 0, Cell::from_char('中'));
2245
2246 let cont = buf.get(1, 0).unwrap();
2248 assert!(cont.is_continuation());
2249 assert_eq!(cont.content.width(), 0);
2250
2251 buf.set(1, 0, Cell::from_char('日'));
2253
2254 assert!(buf.get(0, 0).unwrap().is_empty());
2256 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('日'));
2258 assert!(buf.get(2, 0).unwrap().is_continuation());
2259 }
2260
2261 #[test]
2262 fn wide_char_fill_region() {
2263 let mut buf = Buffer::new(10, 3);
2264
2265 let wide_cell = Cell::from_char('中');
2268 buf.fill(Rect::new(0, 0, 4, 2), wide_cell);
2269
2270 assert_eq!(buf.get(3, 0).unwrap().content.as_char(), Some('中'));
2292 }
2293
2294 #[test]
2295 fn default_buffer_dimensions() {
2296 let buf = Buffer::default();
2297 assert_eq!(buf.width(), 1);
2298 assert_eq!(buf.height(), 1);
2299 assert_eq!(buf.len(), 1);
2300 }
2301
2302 #[test]
2303 fn buffer_partial_eq_impl() {
2304 let buf1 = Buffer::new(5, 5);
2305 let buf2 = Buffer::new(5, 5);
2306 let mut buf3 = Buffer::new(5, 5);
2307 buf3.set(0, 0, Cell::from_char('X'));
2308
2309 assert_eq!(buf1, buf2);
2310 assert_ne!(buf1, buf3);
2311 }
2312
2313 #[test]
2314 fn degradation_level_accessible() {
2315 let mut buf = Buffer::new(10, 10);
2316 assert_eq!(buf.degradation, DegradationLevel::Full);
2317
2318 buf.degradation = DegradationLevel::SimpleBorders;
2319 assert_eq!(buf.degradation, DegradationLevel::SimpleBorders);
2320 }
2321
2322 #[test]
2325 fn get_mut_modifies_cell() {
2326 let mut buf = Buffer::new(10, 10);
2327 buf.set(3, 3, Cell::from_char('A'));
2328
2329 if let Some(cell) = buf.get_mut(3, 3) {
2330 *cell = Cell::from_char('B');
2331 }
2332
2333 assert_eq!(buf.get(3, 3).unwrap().content.as_char(), Some('B'));
2334 }
2335
2336 #[test]
2337 fn get_mut_out_of_bounds() {
2338 let mut buf = Buffer::new(5, 5);
2339 assert!(buf.get_mut(10, 10).is_none());
2340 }
2341
2342 #[test]
2345 fn clear_with_fills_all_cells() {
2346 let mut buf = Buffer::new(5, 3);
2347 let fill_cell = Cell::from_char('*');
2348 buf.clear_with(fill_cell);
2349
2350 for y in 0..3 {
2351 for x in 0..5 {
2352 assert_eq!(buf.get(x, y).unwrap().content.as_char(), Some('*'));
2353 }
2354 }
2355 }
2356
2357 #[test]
2360 fn cells_slice_has_correct_length() {
2361 let buf = Buffer::new(10, 5);
2362 assert_eq!(buf.cells().len(), 50);
2363 }
2364
2365 #[test]
2366 fn cells_mut_allows_direct_modification() {
2367 let mut buf = Buffer::new(3, 2);
2368 let cells = buf.cells_mut();
2369 cells[0] = Cell::from_char('Z');
2370
2371 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('Z'));
2372 }
2373
2374 #[test]
2377 fn row_cells_returns_correct_row() {
2378 let mut buf = Buffer::new(5, 3);
2379 buf.set(2, 1, Cell::from_char('R'));
2380
2381 let row = buf.row_cells(1);
2382 assert_eq!(row.len(), 5);
2383 assert_eq!(row[2].content.as_char(), Some('R'));
2384 }
2385
2386 #[test]
2387 #[should_panic]
2388 fn row_cells_out_of_bounds_panics() {
2389 let buf = Buffer::new(5, 3);
2390 let _ = buf.row_cells(5);
2391 }
2392
2393 #[test]
2396 fn buffer_is_not_empty() {
2397 let buf = Buffer::new(1, 1);
2398 assert!(!buf.is_empty());
2399 }
2400
2401 #[test]
2404 fn set_raw_out_of_bounds_is_safe() {
2405 let mut buf = Buffer::new(5, 5);
2406 buf.set_raw(100, 100, Cell::from_char('X'));
2407 }
2409
2410 #[test]
2413 fn copy_from_out_of_bounds_partial() {
2414 let mut src = Buffer::new(5, 5);
2415 src.set(0, 0, Cell::from_char('A'));
2416 src.set(4, 4, Cell::from_char('B'));
2417
2418 let mut dst = Buffer::new(5, 5);
2419 dst.copy_from(&src, Rect::new(0, 0, 5, 5), 3, 3);
2421
2422 assert_eq!(dst.get(3, 3).unwrap().content.as_char(), Some('A'));
2424 assert!(dst.get(4, 4).unwrap().is_empty());
2426 }
2427
2428 #[test]
2431 fn content_eq_different_dimensions() {
2432 let buf1 = Buffer::new(5, 5);
2433 let buf2 = Buffer::new(10, 10);
2434 assert!(!buf1.content_eq(&buf2));
2436 }
2437
2438 mod property {
2441 use super::*;
2442 use proptest::prelude::*;
2443
2444 proptest! {
2445 #[test]
2446 fn buffer_dimensions_are_preserved(width in 1u16..200, height in 1u16..200) {
2447 let buf = Buffer::new(width, height);
2448 prop_assert_eq!(buf.width(), width);
2449 prop_assert_eq!(buf.height(), height);
2450 prop_assert_eq!(buf.len(), width as usize * height as usize);
2451 }
2452
2453 #[test]
2454 fn buffer_get_in_bounds_always_succeeds(width in 1u16..100, height in 1u16..100) {
2455 let buf = Buffer::new(width, height);
2456 for x in 0..width {
2457 for y in 0..height {
2458 prop_assert!(buf.get(x, y).is_some(), "get({x},{y}) failed for {width}x{height} buffer");
2459 }
2460 }
2461 }
2462
2463 #[test]
2464 fn buffer_get_out_of_bounds_returns_none(width in 1u16..50, height in 1u16..50) {
2465 let buf = Buffer::new(width, height);
2466 prop_assert!(buf.get(width, 0).is_none());
2467 prop_assert!(buf.get(0, height).is_none());
2468 prop_assert!(buf.get(width, height).is_none());
2469 }
2470
2471 #[test]
2472 fn buffer_set_get_roundtrip(
2473 width in 5u16..50,
2474 height in 5u16..50,
2475 x in 0u16..5,
2476 y in 0u16..5,
2477 ch_idx in 0u32..26,
2478 ) {
2479 let x = x % width;
2480 let y = y % height;
2481 let ch = char::from_u32('A' as u32 + ch_idx).unwrap();
2482 let mut buf = Buffer::new(width, height);
2483 buf.set(x, y, Cell::from_char(ch));
2484 let got = buf.get(x, y).unwrap();
2485 prop_assert_eq!(got.content.as_char(), Some(ch));
2486 }
2487
2488 #[test]
2489 fn scissor_push_pop_stack_depth(
2490 width in 10u16..50,
2491 height in 10u16..50,
2492 push_count in 1usize..10,
2493 ) {
2494 let mut buf = Buffer::new(width, height);
2495 prop_assert_eq!(buf.scissor_depth(), 1); for i in 0..push_count {
2498 buf.push_scissor(Rect::new(0, 0, width, height));
2499 prop_assert_eq!(buf.scissor_depth(), i + 2);
2500 }
2501
2502 for i in (0..push_count).rev() {
2503 buf.pop_scissor();
2504 prop_assert_eq!(buf.scissor_depth(), i + 1);
2505 }
2506
2507 buf.pop_scissor();
2509 prop_assert_eq!(buf.scissor_depth(), 1);
2510 }
2511
2512 #[test]
2513 fn scissor_monotonic_intersection(
2514 width in 20u16..60,
2515 height in 20u16..60,
2516 ) {
2517 let mut buf = Buffer::new(width, height);
2519 let outer = Rect::new(2, 2, width - 4, height - 4);
2520 buf.push_scissor(outer);
2521 let s1 = buf.current_scissor();
2522
2523 let inner = Rect::new(5, 5, 10, 10);
2524 buf.push_scissor(inner);
2525 let s2 = buf.current_scissor();
2526
2527 prop_assert!(s2.width <= s1.width, "inner width {} > outer width {}", s2.width, s1.width);
2529 prop_assert!(s2.height <= s1.height, "inner height {} > outer height {}", s2.height, s1.height);
2530 }
2531
2532 #[test]
2533 fn opacity_push_pop_stack_depth(
2534 width in 5u16..20,
2535 height in 5u16..20,
2536 push_count in 1usize..10,
2537 ) {
2538 let mut buf = Buffer::new(width, height);
2539 prop_assert_eq!(buf.opacity_depth(), 1);
2540
2541 for i in 0..push_count {
2542 buf.push_opacity(0.9);
2543 prop_assert_eq!(buf.opacity_depth(), i + 2);
2544 }
2545
2546 for i in (0..push_count).rev() {
2547 buf.pop_opacity();
2548 prop_assert_eq!(buf.opacity_depth(), i + 1);
2549 }
2550
2551 buf.pop_opacity();
2552 prop_assert_eq!(buf.opacity_depth(), 1);
2553 }
2554
2555 #[test]
2556 fn opacity_multiplication_is_monotonic(
2557 opacity1 in 0.0f32..=1.0,
2558 opacity2 in 0.0f32..=1.0,
2559 ) {
2560 let mut buf = Buffer::new(5, 5);
2561 buf.push_opacity(opacity1);
2562 let after_first = buf.current_opacity();
2563 buf.push_opacity(opacity2);
2564 let after_second = buf.current_opacity();
2565
2566 prop_assert!(after_second <= after_first + f32::EPSILON,
2568 "opacity increased: {} -> {}", after_first, after_second);
2569 }
2570
2571 #[test]
2572 fn clear_resets_all_cells(width in 1u16..30, height in 1u16..30) {
2573 let mut buf = Buffer::new(width, height);
2574 for x in 0..width {
2576 buf.set_raw(x, 0, Cell::from_char('X'));
2577 }
2578 buf.clear();
2579 for y in 0..height {
2581 for x in 0..width {
2582 prop_assert!(buf.get(x, y).unwrap().is_empty(),
2583 "cell ({x},{y}) not empty after clear");
2584 }
2585 }
2586 }
2587
2588 #[test]
2589 fn content_eq_is_reflexive(width in 1u16..30, height in 1u16..30) {
2590 let buf = Buffer::new(width, height);
2591 prop_assert!(buf.content_eq(&buf));
2592 }
2593
2594 #[test]
2595 fn content_eq_detects_single_change(
2596 width in 5u16..30,
2597 height in 5u16..30,
2598 x in 0u16..5,
2599 y in 0u16..5,
2600 ) {
2601 let x = x % width;
2602 let y = y % height;
2603 let buf1 = Buffer::new(width, height);
2604 let mut buf2 = Buffer::new(width, height);
2605 buf2.set_raw(x, y, Cell::from_char('Z'));
2606 prop_assert!(!buf1.content_eq(&buf2));
2607 }
2608
2609 #[test]
2612 fn dimensions_immutable_through_operations(
2613 width in 5u16..30,
2614 height in 5u16..30,
2615 ) {
2616 let mut buf = Buffer::new(width, height);
2617
2618 buf.set(0, 0, Cell::from_char('A'));
2620 prop_assert_eq!(buf.width(), width);
2621 prop_assert_eq!(buf.height(), height);
2622 prop_assert_eq!(buf.len(), width as usize * height as usize);
2623
2624 buf.push_scissor(Rect::new(1, 1, 3, 3));
2625 prop_assert_eq!(buf.width(), width);
2626 prop_assert_eq!(buf.height(), height);
2627
2628 buf.push_opacity(0.5);
2629 prop_assert_eq!(buf.width(), width);
2630 prop_assert_eq!(buf.height(), height);
2631
2632 buf.pop_scissor();
2633 buf.pop_opacity();
2634 prop_assert_eq!(buf.width(), width);
2635 prop_assert_eq!(buf.height(), height);
2636
2637 buf.clear();
2638 prop_assert_eq!(buf.width(), width);
2639 prop_assert_eq!(buf.height(), height);
2640 prop_assert_eq!(buf.len(), width as usize * height as usize);
2641 }
2642
2643 #[test]
2644 fn scissor_area_never_increases_random_rects(
2645 width in 20u16..60,
2646 height in 20u16..60,
2647 rects in proptest::collection::vec(
2648 (0u16..20, 0u16..20, 1u16..15, 1u16..15),
2649 1..8
2650 ),
2651 ) {
2652 let mut buf = Buffer::new(width, height);
2653 let mut prev_area = (width as u32) * (height as u32);
2654
2655 for (x, y, w, h) in rects {
2656 buf.push_scissor(Rect::new(x, y, w, h));
2657 let s = buf.current_scissor();
2658 let area = (s.width as u32) * (s.height as u32);
2659 prop_assert!(area <= prev_area,
2660 "scissor area increased: {} -> {} after push({},{},{},{})",
2661 prev_area, area, x, y, w, h);
2662 prev_area = area;
2663 }
2664 }
2665
2666 #[test]
2667 fn opacity_range_invariant_random_sequence(
2668 opacities in proptest::collection::vec(0.0f32..=1.0, 1..15),
2669 ) {
2670 let mut buf = Buffer::new(5, 5);
2671
2672 for &op in &opacities {
2673 buf.push_opacity(op);
2674 let current = buf.current_opacity();
2675 prop_assert!(current >= 0.0, "opacity below 0: {}", current);
2676 prop_assert!(current <= 1.0 + f32::EPSILON,
2677 "opacity above 1: {}", current);
2678 }
2679
2680 for _ in &opacities {
2682 buf.pop_opacity();
2683 }
2684 prop_assert!((buf.current_opacity() - 1.0).abs() < f32::EPSILON);
2686 }
2687
2688 #[test]
2689 fn opacity_clamp_out_of_range(
2690 neg in -100.0f32..0.0,
2691 over in 1.01f32..100.0,
2692 ) {
2693 let mut buf = Buffer::new(5, 5);
2694
2695 buf.push_opacity(neg);
2696 prop_assert!(buf.current_opacity() >= 0.0,
2697 "negative opacity not clamped: {}", buf.current_opacity());
2698 buf.pop_opacity();
2699
2700 buf.push_opacity(over);
2701 prop_assert!(buf.current_opacity() <= 1.0 + f32::EPSILON,
2702 "over-1 opacity not clamped: {}", buf.current_opacity());
2703 }
2704
2705 #[test]
2706 fn scissor_stack_always_has_base(
2707 pushes in 0usize..10,
2708 pops in 0usize..15,
2709 ) {
2710 let mut buf = Buffer::new(10, 10);
2711
2712 for _ in 0..pushes {
2713 buf.push_scissor(Rect::new(0, 0, 5, 5));
2714 }
2715 for _ in 0..pops {
2716 buf.pop_scissor();
2717 }
2718
2719 prop_assert!(buf.scissor_depth() >= 1,
2721 "scissor depth dropped below 1 after {} pushes, {} pops",
2722 pushes, pops);
2723 }
2724
2725 #[test]
2726 fn opacity_stack_always_has_base(
2727 pushes in 0usize..10,
2728 pops in 0usize..15,
2729 ) {
2730 let mut buf = Buffer::new(10, 10);
2731
2732 for _ in 0..pushes {
2733 buf.push_opacity(0.5);
2734 }
2735 for _ in 0..pops {
2736 buf.pop_opacity();
2737 }
2738
2739 prop_assert!(buf.opacity_depth() >= 1,
2741 "opacity depth dropped below 1 after {} pushes, {} pops",
2742 pushes, pops);
2743 }
2744
2745 #[test]
2746 fn cells_len_invariant_always_holds(
2747 width in 1u16..50,
2748 height in 1u16..50,
2749 ) {
2750 let mut buf = Buffer::new(width, height);
2751 let expected = width as usize * height as usize;
2752
2753 prop_assert_eq!(buf.cells().len(), expected);
2754
2755 buf.set(0, 0, Cell::from_char('X'));
2757 prop_assert_eq!(buf.cells().len(), expected);
2758
2759 buf.clear();
2760 prop_assert_eq!(buf.cells().len(), expected);
2761 }
2762
2763 #[test]
2764 fn set_outside_scissor_is_noop(
2765 width in 10u16..30,
2766 height in 10u16..30,
2767 ) {
2768 let mut buf = Buffer::new(width, height);
2769 buf.push_scissor(Rect::new(2, 2, 3, 3));
2770
2771 buf.set(0, 0, Cell::from_char('X'));
2773 let cell = buf.get(0, 0).unwrap();
2775 prop_assert!(cell.is_empty(),
2776 "cell (0,0) modified outside scissor region");
2777
2778 buf.set(3, 3, Cell::from_char('Y'));
2780 let cell = buf.get(3, 3).unwrap();
2781 prop_assert_eq!(cell.content.as_char(), Some('Y'));
2782 }
2783
2784 #[test]
2787 fn wide_char_overwrites_cleanup_tails(
2788 width in 10u16..30,
2789 x in 0u16..8,
2790 ) {
2791 let x = x % (width.saturating_sub(2).max(1));
2792 let mut buf = Buffer::new(width, 1);
2793
2794 buf.set(x, 0, Cell::from_char('中'));
2796
2797 if x + 1 < width {
2799 let head = buf.get(x, 0).unwrap();
2800 let tail = buf.get(x + 1, 0).unwrap();
2801
2802 if head.content.as_char() == Some('中') {
2803 prop_assert!(tail.is_continuation(),
2804 "tail at x+1={} should be continuation", x + 1);
2805
2806 buf.set(x, 0, Cell::from_char('A'));
2808 let new_head = buf.get(x, 0).unwrap();
2809 let cleared_tail = buf.get(x + 1, 0).unwrap();
2810
2811 prop_assert_eq!(new_head.content.as_char(), Some('A'));
2812 prop_assert!(cleared_tail.is_empty(),
2813 "tail should be cleared after head overwrite");
2814 }
2815 }
2816 }
2817
2818 #[test]
2819 fn wide_char_atomic_rejection_at_boundary(
2820 width in 3u16..20,
2821 ) {
2822 let mut buf = Buffer::new(width, 1);
2823
2824 let last_pos = width - 1;
2826 buf.set(last_pos, 0, Cell::from_char('中'));
2827
2828 let cell = buf.get(last_pos, 0).unwrap();
2830 prop_assert!(cell.is_empty(),
2831 "wide char at boundary position {} (width {}) should be rejected",
2832 last_pos, width);
2833 }
2834
2835 #[test]
2840 fn double_buffer_swap_is_involution(ops in proptest::collection::vec(proptest::bool::ANY, 0..100)) {
2841 let mut db = DoubleBuffer::new(10, 10);
2842 let initial_idx = db.current_idx;
2843
2844 for do_swap in &ops {
2845 if *do_swap {
2846 db.swap();
2847 }
2848 }
2849
2850 let swap_count = ops.iter().filter(|&&x| x).count();
2851 let expected_idx = if swap_count % 2 == 0 { initial_idx } else { 1 - initial_idx };
2852
2853 prop_assert_eq!(db.current_idx, expected_idx,
2854 "After {} swaps, index should be {} but was {}",
2855 swap_count, expected_idx, db.current_idx);
2856 }
2857
2858 #[test]
2859 fn double_buffer_resize_preserves_invariant(
2860 init_w in 1u16..200,
2861 init_h in 1u16..100,
2862 new_w in 1u16..200,
2863 new_h in 1u16..100,
2864 ) {
2865 let mut db = DoubleBuffer::new(init_w, init_h);
2866 db.resize(new_w, new_h);
2867
2868 prop_assert_eq!(db.width(), new_w);
2869 prop_assert_eq!(db.height(), new_h);
2870 prop_assert!(db.dimensions_match(new_w, new_h));
2871 }
2872
2873 #[test]
2874 fn double_buffer_current_previous_disjoint(
2875 width in 1u16..50,
2876 height in 1u16..50,
2877 ) {
2878 let mut db = DoubleBuffer::new(width, height);
2879
2880 db.current_mut().set(0, 0, Cell::from_char('C'));
2882
2883 prop_assert!(db.previous().get(0, 0).unwrap().is_empty(),
2885 "Previous buffer should not reflect changes to current");
2886
2887 db.swap();
2889 prop_assert_eq!(db.previous().get(0, 0).unwrap().content.as_char(), Some('C'),
2890 "After swap, previous should have the 'C' we wrote");
2891 }
2892
2893 #[test]
2894 fn double_buffer_swap_content_semantics(
2895 width in 5u16..30,
2896 height in 5u16..30,
2897 ) {
2898 let mut db = DoubleBuffer::new(width, height);
2899
2900 db.current_mut().set(0, 0, Cell::from_char('X'));
2902 db.swap();
2903
2904 db.current_mut().set(0, 0, Cell::from_char('Y'));
2906 db.swap();
2907
2908 prop_assert_eq!(db.current().get(0, 0).unwrap().content.as_char(), Some('X'));
2910 prop_assert_eq!(db.previous().get(0, 0).unwrap().content.as_char(), Some('Y'));
2911 }
2912
2913 #[test]
2914 fn double_buffer_resize_clears_both(
2915 w1 in 5u16..30,
2916 h1 in 5u16..30,
2917 w2 in 5u16..30,
2918 h2 in 5u16..30,
2919 ) {
2920 prop_assume!(w1 != w2 || h1 != h2);
2922
2923 let mut db = DoubleBuffer::new(w1, h1);
2924
2925 db.current_mut().set(0, 0, Cell::from_char('A'));
2927 db.swap();
2928 db.current_mut().set(0, 0, Cell::from_char('B'));
2929
2930 db.resize(w2, h2);
2932
2933 prop_assert!(db.current().get(0, 0).unwrap().is_empty(),
2935 "Current buffer should be empty after resize");
2936 prop_assert!(db.previous().get(0, 0).unwrap().is_empty(),
2937 "Previous buffer should be empty after resize");
2938 }
2939 }
2940 }
2941
2942 #[test]
2945 fn dirty_rows_start_dirty() {
2946 let buf = Buffer::new(10, 5);
2948 assert_eq!(buf.dirty_row_count(), 5);
2949 for y in 0..5 {
2950 assert!(buf.is_row_dirty(y));
2951 }
2952 }
2953
2954 #[test]
2955 fn dirty_bitmap_starts_full() {
2956 let buf = Buffer::new(4, 3);
2957 assert!(buf.dirty_all());
2958 assert_eq!(buf.dirty_cell_count(), 12);
2959 }
2960
2961 #[test]
2962 fn dirty_bitmap_tracks_single_cell() {
2963 let mut buf = Buffer::new(4, 3);
2964 buf.clear_dirty();
2965 assert!(!buf.dirty_all());
2966 buf.set_raw(1, 1, Cell::from_char('X'));
2967 let idx = 1 + 4;
2968 assert_eq!(buf.dirty_cell_count(), 1);
2969 assert_eq!(buf.dirty_bits()[idx], 1);
2970 }
2971
2972 #[test]
2973 fn dirty_bitmap_dedupes_cells() {
2974 let mut buf = Buffer::new(4, 3);
2975 buf.clear_dirty();
2976 buf.set_raw(2, 2, Cell::from_char('A'));
2977 buf.set_raw(2, 2, Cell::from_char('B'));
2978 assert_eq!(buf.dirty_cell_count(), 1);
2979 }
2980
2981 #[test]
2982 fn set_marks_row_dirty() {
2983 let mut buf = Buffer::new(10, 5);
2984 buf.clear_dirty(); buf.set(3, 2, Cell::from_char('X'));
2986 assert!(buf.is_row_dirty(2));
2987 assert!(!buf.is_row_dirty(0));
2988 assert!(!buf.is_row_dirty(1));
2989 assert!(!buf.is_row_dirty(3));
2990 assert!(!buf.is_row_dirty(4));
2991 }
2992
2993 #[test]
2994 fn set_raw_marks_row_dirty() {
2995 let mut buf = Buffer::new(10, 5);
2996 buf.clear_dirty(); buf.set_raw(0, 4, Cell::from_char('Z'));
2998 assert!(buf.is_row_dirty(4));
2999 assert_eq!(buf.dirty_row_count(), 1);
3000 }
3001
3002 #[test]
3003 fn clear_marks_all_dirty() {
3004 let mut buf = Buffer::new(10, 5);
3005 buf.clear();
3006 assert_eq!(buf.dirty_row_count(), 5);
3007 }
3008
3009 #[test]
3010 fn clear_dirty_resets_flags() {
3011 let mut buf = Buffer::new(10, 5);
3012 assert_eq!(buf.dirty_row_count(), 5);
3014 buf.clear_dirty();
3015 assert_eq!(buf.dirty_row_count(), 0);
3016
3017 buf.set(0, 0, Cell::from_char('A'));
3019 buf.set(0, 3, Cell::from_char('B'));
3020 assert_eq!(buf.dirty_row_count(), 2);
3021
3022 buf.clear_dirty();
3023 assert_eq!(buf.dirty_row_count(), 0);
3024 }
3025
3026 #[test]
3027 fn clear_dirty_resets_bitmap() {
3028 let mut buf = Buffer::new(4, 3);
3029 buf.clear();
3030 assert!(buf.dirty_all());
3031 buf.clear_dirty();
3032 assert!(!buf.dirty_all());
3033 assert_eq!(buf.dirty_cell_count(), 0);
3034 assert!(buf.dirty_bits().iter().all(|&b| b == 0));
3035 }
3036
3037 #[test]
3038 fn fill_marks_affected_rows_dirty() {
3039 let mut buf = Buffer::new(10, 10);
3040 buf.clear_dirty(); buf.fill(Rect::new(0, 2, 5, 3), Cell::from_char('.'));
3042 assert!(!buf.is_row_dirty(0));
3044 assert!(!buf.is_row_dirty(1));
3045 assert!(buf.is_row_dirty(2));
3046 assert!(buf.is_row_dirty(3));
3047 assert!(buf.is_row_dirty(4));
3048 assert!(!buf.is_row_dirty(5));
3049 }
3050
3051 #[test]
3052 fn get_mut_marks_row_dirty() {
3053 let mut buf = Buffer::new(10, 5);
3054 buf.clear_dirty(); if let Some(cell) = buf.get_mut(5, 3) {
3056 cell.fg = PackedRgba::rgb(255, 0, 0);
3057 }
3058 assert!(buf.is_row_dirty(3));
3059 assert_eq!(buf.dirty_row_count(), 1);
3060 }
3061
3062 #[test]
3063 fn cells_mut_marks_all_dirty() {
3064 let mut buf = Buffer::new(10, 5);
3065 let _ = buf.cells_mut();
3066 assert_eq!(buf.dirty_row_count(), 5);
3067 }
3068
3069 #[test]
3070 fn dirty_rows_slice_length_matches_height() {
3071 let buf = Buffer::new(10, 7);
3072 assert_eq!(buf.dirty_rows().len(), 7);
3073 }
3074
3075 #[test]
3076 fn out_of_bounds_set_does_not_dirty() {
3077 let mut buf = Buffer::new(10, 5);
3078 buf.clear_dirty(); buf.set(100, 100, Cell::from_char('X'));
3080 assert_eq!(buf.dirty_row_count(), 0);
3081 }
3082
3083 #[test]
3084 fn property_dirty_soundness() {
3085 let mut buf = Buffer::new(20, 10);
3087 let positions = [(3, 0), (5, 2), (0, 9), (19, 5), (10, 7)];
3088 for &(x, y) in &positions {
3089 buf.set(x, y, Cell::from_char('*'));
3090 }
3091 for &(_, y) in &positions {
3092 assert!(
3093 buf.is_row_dirty(y),
3094 "Row {} should be dirty after set({}, {})",
3095 y,
3096 positions.iter().find(|(_, ry)| *ry == y).unwrap().0,
3097 y
3098 );
3099 }
3100 }
3101
3102 #[test]
3103 fn dirty_clear_between_frames() {
3104 let mut buf = Buffer::new(10, 5);
3106
3107 assert_eq!(buf.dirty_row_count(), 5);
3109
3110 buf.clear_dirty();
3112 assert_eq!(buf.dirty_row_count(), 0);
3113
3114 buf.set(0, 0, Cell::from_char('A'));
3116 buf.set(0, 2, Cell::from_char('B'));
3117 assert_eq!(buf.dirty_row_count(), 2);
3118
3119 buf.clear_dirty();
3121 assert_eq!(buf.dirty_row_count(), 0);
3122
3123 buf.set(0, 4, Cell::from_char('C'));
3125 assert_eq!(buf.dirty_row_count(), 1);
3126 assert!(buf.is_row_dirty(4));
3127 assert!(!buf.is_row_dirty(0));
3128 }
3129
3130 #[test]
3133 fn dirty_spans_start_full_dirty() {
3134 let buf = Buffer::new(10, 5);
3135 for y in 0..5 {
3136 let row = buf.dirty_span_row(y).unwrap();
3137 assert!(row.is_full(), "row {y} should start full-dirty");
3138 assert!(row.spans().is_empty(), "row {y} spans should start empty");
3139 }
3140 }
3141
3142 #[test]
3143 fn clear_dirty_resets_spans() {
3144 let mut buf = Buffer::new(10, 5);
3145 buf.clear_dirty();
3146 for y in 0..5 {
3147 let row = buf.dirty_span_row(y).unwrap();
3148 assert!(!row.is_full(), "row {y} should clear full-dirty");
3149 assert!(row.spans().is_empty(), "row {y} spans should be cleared");
3150 }
3151 assert_eq!(buf.dirty_span_overflows, 0);
3152 }
3153
3154 #[test]
3155 fn set_records_dirty_span() {
3156 let mut buf = Buffer::new(20, 2);
3157 buf.clear_dirty();
3158 buf.set(2, 0, Cell::from_char('A'));
3159 let row = buf.dirty_span_row(0).unwrap();
3160 assert_eq!(row.spans(), &[DirtySpan::new(2, 3)]);
3161 assert!(!row.is_full());
3162 }
3163
3164 #[test]
3165 fn set_merges_adjacent_spans() {
3166 let mut buf = Buffer::new(20, 2);
3167 buf.clear_dirty();
3168 buf.set(2, 0, Cell::from_char('A'));
3169 buf.set(3, 0, Cell::from_char('B')); let row = buf.dirty_span_row(0).unwrap();
3171 assert_eq!(row.spans(), &[DirtySpan::new(2, 4)]);
3172 }
3173
3174 #[test]
3175 fn set_merges_close_spans() {
3176 let mut buf = Buffer::new(20, 2);
3177 buf.clear_dirty();
3178 buf.set(2, 0, Cell::from_char('A'));
3179 buf.set(4, 0, Cell::from_char('B')); let row = buf.dirty_span_row(0).unwrap();
3181 assert_eq!(row.spans(), &[DirtySpan::new(2, 5)]);
3182 }
3183
3184 #[test]
3185 fn span_overflow_sets_full_row() {
3186 let width = (DIRTY_SPAN_MAX_SPANS_PER_ROW as u16 + 2) * 3;
3187 let mut buf = Buffer::new(width, 1);
3188 buf.clear_dirty();
3189 for i in 0..(DIRTY_SPAN_MAX_SPANS_PER_ROW + 1) {
3190 let x = (i as u16) * 3;
3191 buf.set(x, 0, Cell::from_char('x'));
3192 }
3193 let row = buf.dirty_span_row(0).unwrap();
3194 assert!(row.is_full());
3195 assert!(row.spans().is_empty());
3196 assert_eq!(buf.dirty_span_overflows, 1);
3197 }
3198
3199 #[test]
3200 fn fill_full_row_marks_full_span() {
3201 let mut buf = Buffer::new(10, 3);
3202 buf.clear_dirty();
3203 let cell = Cell::from_char('x').with_bg(PackedRgba::rgb(0, 0, 0));
3204 buf.fill(Rect::new(0, 1, 10, 1), cell);
3205 let row = buf.dirty_span_row(1).unwrap();
3206 assert!(row.is_full());
3207 assert!(row.spans().is_empty());
3208 }
3209
3210 #[test]
3211 fn get_mut_records_dirty_span() {
3212 let mut buf = Buffer::new(10, 5);
3213 buf.clear_dirty();
3214 let _ = buf.get_mut(5, 3);
3215 let row = buf.dirty_span_row(3).unwrap();
3216 assert_eq!(row.spans(), &[DirtySpan::new(5, 6)]);
3217 }
3218
3219 #[test]
3220 fn cells_mut_marks_all_full_spans() {
3221 let mut buf = Buffer::new(10, 5);
3222 buf.clear_dirty();
3223 let _ = buf.cells_mut();
3224 for y in 0..5 {
3225 let row = buf.dirty_span_row(y).unwrap();
3226 assert!(row.is_full(), "row {y} should be full after cells_mut");
3227 }
3228 }
3229
3230 #[test]
3231 fn dirty_span_config_disabled_skips_rows() {
3232 let mut buf = Buffer::new(10, 1);
3233 buf.clear_dirty();
3234 buf.set_dirty_span_config(DirtySpanConfig::default().with_enabled(false));
3235 buf.set(5, 0, Cell::from_char('x'));
3236 assert!(buf.dirty_span_row(0).is_none());
3237 let stats = buf.dirty_span_stats();
3238 assert_eq!(stats.total_spans, 0);
3239 assert_eq!(stats.span_coverage_cells, 0);
3240 }
3241
3242 #[test]
3243 fn dirty_span_guard_band_expands_span_bounds() {
3244 let mut buf = Buffer::new(10, 1);
3245 buf.clear_dirty();
3246 buf.set_dirty_span_config(DirtySpanConfig::default().with_guard_band(2));
3247 buf.set(5, 0, Cell::from_char('x'));
3248 let row = buf.dirty_span_row(0).unwrap();
3249 assert_eq!(row.spans(), &[DirtySpan::new(3, 8)]);
3250 }
3251
3252 #[test]
3253 fn dirty_span_max_spans_overflow_triggers_full_row() {
3254 let mut buf = Buffer::new(10, 1);
3255 buf.clear_dirty();
3256 buf.set_dirty_span_config(
3257 DirtySpanConfig::default()
3258 .with_max_spans_per_row(1)
3259 .with_merge_gap(0),
3260 );
3261 buf.set(0, 0, Cell::from_char('a'));
3262 buf.set(4, 0, Cell::from_char('b'));
3263 let row = buf.dirty_span_row(0).unwrap();
3264 assert!(row.is_full());
3265 assert!(row.spans().is_empty());
3266 assert_eq!(buf.dirty_span_overflows, 1);
3267 }
3268
3269 #[test]
3270 fn dirty_span_stats_counts_full_rows_and_spans() {
3271 let mut buf = Buffer::new(6, 2);
3272 buf.clear_dirty();
3273 buf.set_dirty_span_config(DirtySpanConfig::default().with_merge_gap(0));
3274 buf.set(1, 0, Cell::from_char('a'));
3275 buf.set(4, 0, Cell::from_char('b'));
3276 buf.mark_dirty_row_full(1);
3277
3278 let stats = buf.dirty_span_stats();
3279 assert_eq!(stats.rows_full_dirty, 1);
3280 assert_eq!(stats.rows_with_spans, 1);
3281 assert_eq!(stats.total_spans, 2);
3282 assert_eq!(stats.max_span_len, 6);
3283 assert_eq!(stats.span_coverage_cells, 8);
3284 }
3285
3286 #[test]
3287 fn dirty_span_stats_reports_overflow_and_full_row() {
3288 let mut buf = Buffer::new(8, 1);
3289 buf.clear_dirty();
3290 buf.set_dirty_span_config(
3291 DirtySpanConfig::default()
3292 .with_max_spans_per_row(1)
3293 .with_merge_gap(0),
3294 );
3295 buf.set(0, 0, Cell::from_char('x'));
3296 buf.set(3, 0, Cell::from_char('y'));
3297
3298 let stats = buf.dirty_span_stats();
3299 assert_eq!(stats.overflows, 1);
3300 assert_eq!(stats.rows_full_dirty, 1);
3301 assert_eq!(stats.total_spans, 0);
3302 assert_eq!(stats.span_coverage_cells, 8);
3303 }
3304
3305 #[test]
3310 fn double_buffer_new_has_matching_dimensions() {
3311 let db = DoubleBuffer::new(80, 24);
3312 assert_eq!(db.width(), 80);
3313 assert_eq!(db.height(), 24);
3314 assert!(db.dimensions_match(80, 24));
3315 assert!(!db.dimensions_match(120, 40));
3316 }
3317
3318 #[test]
3319 fn double_buffer_swap_is_o1() {
3320 let mut db = DoubleBuffer::new(80, 24);
3321
3322 db.current_mut().set(0, 0, Cell::from_char('A'));
3324 assert_eq!(db.current().get(0, 0).unwrap().content.as_char(), Some('A'));
3325
3326 db.swap();
3328 assert_eq!(
3329 db.previous().get(0, 0).unwrap().content.as_char(),
3330 Some('A')
3331 );
3332 assert!(db.current().get(0, 0).unwrap().is_empty());
3334 }
3335
3336 #[test]
3337 fn double_buffer_swap_round_trip() {
3338 let mut db = DoubleBuffer::new(10, 5);
3339
3340 db.current_mut().set(0, 0, Cell::from_char('X'));
3341 db.swap();
3342 db.current_mut().set(0, 0, Cell::from_char('Y'));
3343 db.swap();
3344
3345 assert_eq!(db.current().get(0, 0).unwrap().content.as_char(), Some('X'));
3347 assert_eq!(
3348 db.previous().get(0, 0).unwrap().content.as_char(),
3349 Some('Y')
3350 );
3351 }
3352
3353 #[test]
3354 fn double_buffer_resize_changes_dimensions() {
3355 let mut db = DoubleBuffer::new(80, 24);
3356 assert!(!db.resize(80, 24)); assert!(db.resize(120, 40)); assert_eq!(db.width(), 120);
3359 assert_eq!(db.height(), 40);
3360 assert!(db.dimensions_match(120, 40));
3361 }
3362
3363 #[test]
3364 fn double_buffer_resize_clears_content() {
3365 let mut db = DoubleBuffer::new(10, 5);
3366 db.current_mut().set(0, 0, Cell::from_char('Z'));
3367 db.swap();
3368 db.current_mut().set(0, 0, Cell::from_char('W'));
3369
3370 db.resize(20, 10);
3371
3372 assert!(db.current().get(0, 0).unwrap().is_empty());
3374 assert!(db.previous().get(0, 0).unwrap().is_empty());
3375 }
3376
3377 #[test]
3378 fn double_buffer_current_and_previous_are_distinct() {
3379 let mut db = DoubleBuffer::new(10, 5);
3380 db.current_mut().set(0, 0, Cell::from_char('C'));
3381
3382 assert!(db.previous().get(0, 0).unwrap().is_empty());
3384 assert_eq!(db.current().get(0, 0).unwrap().content.as_char(), Some('C'));
3385 }
3386
3387 #[test]
3392 fn adaptive_buffer_new_has_over_allocation() {
3393 let adb = AdaptiveDoubleBuffer::new(80, 24);
3394
3395 assert_eq!(adb.width(), 80);
3397 assert_eq!(adb.height(), 24);
3398 assert!(adb.dimensions_match(80, 24));
3399
3400 assert!(adb.capacity_width() > 80);
3404 assert!(adb.capacity_height() > 24);
3405 assert_eq!(adb.capacity_width(), 100); assert_eq!(adb.capacity_height(), 30); }
3408
3409 #[test]
3410 fn adaptive_buffer_resize_avoids_reallocation_when_within_capacity() {
3411 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3412
3413 assert!(adb.resize(90, 28)); assert_eq!(adb.width(), 90);
3416 assert_eq!(adb.height(), 28);
3417 assert_eq!(adb.stats().resize_avoided, 1);
3418 assert_eq!(adb.stats().resize_reallocated, 0);
3419 assert_eq!(adb.stats().resize_growth, 1);
3420 }
3421
3422 #[test]
3423 fn adaptive_buffer_resize_reallocates_on_growth_beyond_capacity() {
3424 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3425
3426 assert!(adb.resize(120, 40)); assert_eq!(adb.width(), 120);
3429 assert_eq!(adb.height(), 40);
3430 assert_eq!(adb.stats().resize_reallocated, 1);
3431 assert_eq!(adb.stats().resize_avoided, 0);
3432
3433 assert!(adb.capacity_width() > 120);
3435 assert!(adb.capacity_height() > 40);
3436 }
3437
3438 #[test]
3439 fn adaptive_buffer_resize_reallocates_on_significant_shrink() {
3440 let mut adb = AdaptiveDoubleBuffer::new(100, 50);
3441
3442 assert!(adb.resize(40, 20)); assert_eq!(adb.width(), 40);
3446 assert_eq!(adb.height(), 20);
3447 assert_eq!(adb.stats().resize_reallocated, 1);
3448 assert_eq!(adb.stats().resize_shrink, 1);
3449 }
3450
3451 #[test]
3452 fn adaptive_buffer_resize_avoids_reallocation_on_minor_shrink() {
3453 let mut adb = AdaptiveDoubleBuffer::new(100, 50);
3454
3455 assert!(adb.resize(80, 40));
3459 assert_eq!(adb.width(), 80);
3460 assert_eq!(adb.height(), 40);
3461 assert_eq!(adb.stats().resize_avoided, 1);
3462 assert_eq!(adb.stats().resize_reallocated, 0);
3463 assert_eq!(adb.stats().resize_shrink, 1);
3464 }
3465
3466 #[test]
3467 fn adaptive_buffer_no_change_returns_false() {
3468 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3469
3470 assert!(!adb.resize(80, 24)); assert_eq!(adb.stats().resize_avoided, 0);
3472 assert_eq!(adb.stats().resize_reallocated, 0);
3473 assert_eq!(adb.stats().resize_growth, 0);
3474 assert_eq!(adb.stats().resize_shrink, 0);
3475 }
3476
3477 #[test]
3478 fn adaptive_buffer_swap_works() {
3479 let mut adb = AdaptiveDoubleBuffer::new(10, 5);
3480
3481 adb.current_mut().set(0, 0, Cell::from_char('A'));
3482 assert_eq!(
3483 adb.current().get(0, 0).unwrap().content.as_char(),
3484 Some('A')
3485 );
3486
3487 adb.swap();
3488 assert_eq!(
3489 adb.previous().get(0, 0).unwrap().content.as_char(),
3490 Some('A')
3491 );
3492 assert!(adb.current().get(0, 0).unwrap().is_empty());
3493 }
3494
3495 #[test]
3496 fn adaptive_buffer_stats_reset() {
3497 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3498
3499 adb.resize(90, 28);
3500 adb.resize(120, 40);
3501 assert!(adb.stats().resize_avoided > 0 || adb.stats().resize_reallocated > 0);
3502
3503 adb.reset_stats();
3504 assert_eq!(adb.stats().resize_avoided, 0);
3505 assert_eq!(adb.stats().resize_reallocated, 0);
3506 assert_eq!(adb.stats().resize_growth, 0);
3507 assert_eq!(adb.stats().resize_shrink, 0);
3508 }
3509
3510 #[test]
3511 fn adaptive_buffer_memory_efficiency() {
3512 let adb = AdaptiveDoubleBuffer::new(80, 24);
3513
3514 let efficiency = adb.memory_efficiency();
3515 assert!(efficiency > 0.5);
3519 assert!(efficiency < 1.0);
3520 }
3521
3522 #[test]
3523 fn adaptive_buffer_logical_bounds() {
3524 let adb = AdaptiveDoubleBuffer::new(80, 24);
3525
3526 let bounds = adb.logical_bounds();
3527 assert_eq!(bounds.x, 0);
3528 assert_eq!(bounds.y, 0);
3529 assert_eq!(bounds.width, 80);
3530 assert_eq!(bounds.height, 24);
3531 }
3532
3533 #[test]
3534 fn adaptive_buffer_capacity_clamped_for_large_sizes() {
3535 let adb = AdaptiveDoubleBuffer::new(1000, 500);
3537
3538 assert_eq!(adb.capacity_width(), 1000 + 200); assert_eq!(adb.capacity_height(), 500 + 125); }
3543
3544 #[test]
3545 fn adaptive_stats_avoidance_ratio() {
3546 let mut stats = AdaptiveStats::default();
3547
3548 assert!((stats.avoidance_ratio() - 1.0).abs() < f64::EPSILON);
3550
3551 stats.resize_avoided = 3;
3553 stats.resize_reallocated = 1;
3554 assert!((stats.avoidance_ratio() - 0.75).abs() < f64::EPSILON);
3555
3556 stats.resize_avoided = 0;
3558 stats.resize_reallocated = 5;
3559 assert!((stats.avoidance_ratio() - 0.0).abs() < f64::EPSILON);
3560 }
3561
3562 #[test]
3563 fn adaptive_buffer_resize_storm_simulation() {
3564 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3566
3567 for i in 1..=10 {
3569 adb.resize(80 + i, 24 + (i / 2));
3570 }
3571
3572 let ratio = adb.stats().avoidance_ratio();
3574 assert!(
3575 ratio > 0.5,
3576 "Expected >50% avoidance ratio, got {:.2}",
3577 ratio
3578 );
3579 }
3580
3581 #[test]
3582 fn adaptive_buffer_width_only_growth() {
3583 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3584
3585 assert!(adb.resize(95, 24)); assert_eq!(adb.stats().resize_avoided, 1);
3588 assert_eq!(adb.stats().resize_growth, 1);
3589 }
3590
3591 #[test]
3592 fn adaptive_buffer_height_only_growth() {
3593 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3594
3595 assert!(adb.resize(80, 28)); assert_eq!(adb.stats().resize_avoided, 1);
3598 assert_eq!(adb.stats().resize_growth, 1);
3599 }
3600
3601 #[test]
3602 fn adaptive_buffer_one_dimension_exceeds_capacity() {
3603 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3604
3605 assert!(adb.resize(105, 24)); assert_eq!(adb.stats().resize_reallocated, 1);
3608 }
3609
3610 #[test]
3611 fn adaptive_buffer_current_and_previous_distinct() {
3612 let mut adb = AdaptiveDoubleBuffer::new(10, 5);
3613 adb.current_mut().set(0, 0, Cell::from_char('X'));
3614
3615 assert!(adb.previous().get(0, 0).unwrap().is_empty());
3617 assert_eq!(
3618 adb.current().get(0, 0).unwrap().content.as_char(),
3619 Some('X')
3620 );
3621 }
3622
3623 #[test]
3624 fn adaptive_buffer_resize_within_capacity_clears_previous() {
3625 let mut adb = AdaptiveDoubleBuffer::new(10, 5);
3626 adb.current_mut().set(9, 4, Cell::from_char('X'));
3627 adb.swap();
3628
3629 assert!(adb.resize(8, 4));
3631
3632 assert!(adb.previous().get(9, 4).unwrap().is_empty());
3634 }
3635
3636 #[test]
3638 fn adaptive_buffer_invariant_capacity_geq_logical() {
3639 for width in [1u16, 10, 80, 200, 1000, 5000] {
3641 for height in [1u16, 10, 24, 100, 500, 2000] {
3642 let adb = AdaptiveDoubleBuffer::new(width, height);
3643 assert!(
3644 adb.capacity_width() >= adb.width(),
3645 "capacity_width {} < logical_width {} for ({}, {})",
3646 adb.capacity_width(),
3647 adb.width(),
3648 width,
3649 height
3650 );
3651 assert!(
3652 adb.capacity_height() >= adb.height(),
3653 "capacity_height {} < logical_height {} for ({}, {})",
3654 adb.capacity_height(),
3655 adb.height(),
3656 width,
3657 height
3658 );
3659 }
3660 }
3661 }
3662
3663 #[test]
3664 fn adaptive_buffer_invariant_resize_dimensions_correct() {
3665 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3666
3667 let test_sizes = [
3669 (100, 50),
3670 (40, 20),
3671 (80, 24),
3672 (200, 100),
3673 (10, 5),
3674 (1000, 500),
3675 ];
3676 for (w, h) in test_sizes {
3677 adb.resize(w, h);
3678 assert_eq!(adb.width(), w, "width mismatch for ({}, {})", w, h);
3679 assert_eq!(adb.height(), h, "height mismatch for ({}, {})", w, h);
3680 assert!(
3681 adb.capacity_width() >= w,
3682 "capacity_width < width for ({}, {})",
3683 w,
3684 h
3685 );
3686 assert!(
3687 adb.capacity_height() >= h,
3688 "capacity_height < height for ({}, {})",
3689 w,
3690 h
3691 );
3692 }
3693 }
3694
3695 #[test]
3699 fn adaptive_buffer_no_ghosting_on_shrink() {
3700 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3701
3702 for y in 0..adb.height() {
3704 for x in 0..adb.width() {
3705 adb.current_mut().set(x, y, Cell::from_char('X'));
3706 }
3707 }
3708
3709 adb.resize(60, 20);
3712
3713 for y in 0..adb.height() {
3716 for x in 0..adb.width() {
3717 let cell = adb.current().get(x, y).unwrap();
3718 assert!(
3719 cell.is_empty(),
3720 "Ghost content at ({}, {}): expected empty, got {:?}",
3721 x,
3722 y,
3723 cell.content
3724 );
3725 }
3726 }
3727 }
3728
3729 #[test]
3733 fn adaptive_buffer_no_ghosting_on_reallocation_shrink() {
3734 let mut adb = AdaptiveDoubleBuffer::new(100, 50);
3735
3736 for y in 0..adb.height() {
3738 for x in 0..adb.width() {
3739 adb.current_mut().set(x, y, Cell::from_char('A'));
3740 }
3741 }
3742 adb.swap();
3743 for y in 0..adb.height() {
3744 for x in 0..adb.width() {
3745 adb.current_mut().set(x, y, Cell::from_char('B'));
3746 }
3747 }
3748
3749 adb.resize(30, 15);
3751 assert_eq!(adb.stats().resize_reallocated, 1);
3752
3753 for y in 0..adb.height() {
3755 for x in 0..adb.width() {
3756 assert!(
3757 adb.current().get(x, y).unwrap().is_empty(),
3758 "Ghost in current at ({}, {})",
3759 x,
3760 y
3761 );
3762 assert!(
3763 adb.previous().get(x, y).unwrap().is_empty(),
3764 "Ghost in previous at ({}, {})",
3765 x,
3766 y
3767 );
3768 }
3769 }
3770 }
3771
3772 #[test]
3776 fn adaptive_buffer_no_ghosting_on_growth_reallocation() {
3777 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
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('Z'));
3783 }
3784 }
3785
3786 adb.resize(150, 60);
3788 assert_eq!(adb.stats().resize_reallocated, 1);
3789
3790 for y in 0..adb.height() {
3792 for x in 0..adb.width() {
3793 assert!(
3794 adb.current().get(x, y).unwrap().is_empty(),
3795 "Ghost at ({}, {}) after growth reallocation",
3796 x,
3797 y
3798 );
3799 }
3800 }
3801 }
3802
3803 #[test]
3805 fn adaptive_buffer_resize_idempotent() {
3806 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3807 adb.current_mut().set(5, 5, Cell::from_char('K'));
3808
3809 let changed = adb.resize(80, 24);
3811 assert!(!changed);
3812
3813 assert_eq!(
3815 adb.current().get(5, 5).unwrap().content.as_char(),
3816 Some('K')
3817 );
3818 }
3819
3820 #[test]
3825 fn dirty_span_merge_adjacent() {
3826 let mut buf = Buffer::new(100, 1);
3827 buf.clear_dirty(); buf.mark_dirty_span(0, 10, 20);
3831 let spans = buf.dirty_span_row(0).unwrap().spans();
3832 assert_eq!(spans.len(), 1);
3833 assert_eq!(spans[0], DirtySpan::new(10, 20));
3834
3835 buf.mark_dirty_span(0, 20, 30);
3837 let spans = buf.dirty_span_row(0).unwrap().spans();
3838 assert_eq!(spans.len(), 1);
3839 assert_eq!(spans[0], DirtySpan::new(10, 30));
3840 }
3841
3842 #[test]
3843 fn dirty_span_merge_overlapping() {
3844 let mut buf = Buffer::new(100, 1);
3845 buf.clear_dirty();
3846
3847 buf.mark_dirty_span(0, 10, 20);
3849 buf.mark_dirty_span(0, 15, 25);
3851
3852 let spans = buf.dirty_span_row(0).unwrap().spans();
3853 assert_eq!(spans.len(), 1);
3854 assert_eq!(spans[0], DirtySpan::new(10, 25));
3855 }
3856
3857 #[test]
3858 fn dirty_span_merge_with_gap() {
3859 let mut buf = Buffer::new(100, 1);
3860 buf.clear_dirty();
3861
3862 buf.mark_dirty_span(0, 10, 20);
3865 buf.mark_dirty_span(0, 21, 30);
3867
3868 let spans = buf.dirty_span_row(0).unwrap().spans();
3869 assert_eq!(spans.len(), 1);
3870 assert_eq!(spans[0], DirtySpan::new(10, 30));
3871 }
3872
3873 #[test]
3874 fn dirty_span_no_merge_large_gap() {
3875 let mut buf = Buffer::new(100, 1);
3876 buf.clear_dirty();
3877
3878 buf.mark_dirty_span(0, 10, 20);
3880 buf.mark_dirty_span(0, 22, 30);
3882
3883 let spans = buf.dirty_span_row(0).unwrap().spans();
3884 assert_eq!(spans.len(), 2);
3885 assert_eq!(spans[0], DirtySpan::new(10, 20));
3886 assert_eq!(spans[1], DirtySpan::new(22, 30));
3887 }
3888
3889 #[test]
3890 fn dirty_span_overflow_to_full() {
3891 let mut buf = Buffer::new(1000, 1);
3892 buf.clear_dirty();
3893
3894 for i in 0..DIRTY_SPAN_MAX_SPANS_PER_ROW + 10 {
3896 let start = (i * 4) as u16;
3897 buf.mark_dirty_span(0, start, start + 1);
3898 }
3899
3900 let row = buf.dirty_span_row(0).unwrap();
3901 assert!(row.is_full(), "Row should overflow to full scan");
3902 assert!(
3903 row.spans().is_empty(),
3904 "Spans should be cleared on overflow"
3905 );
3906 }
3907
3908 #[test]
3909 fn dirty_span_bounds_clamping() {
3910 let mut buf = Buffer::new(10, 1);
3911 buf.clear_dirty();
3912
3913 buf.mark_dirty_span(0, 15, 20);
3915 let spans = buf.dirty_span_row(0).unwrap().spans();
3916 assert!(spans.is_empty());
3917
3918 buf.mark_dirty_span(0, 8, 15);
3920 let spans = buf.dirty_span_row(0).unwrap().spans();
3921 assert_eq!(spans.len(), 1);
3922 assert_eq!(spans[0], DirtySpan::new(8, 10)); }
3924
3925 #[test]
3926 fn dirty_span_guard_band_clamps_bounds() {
3927 let mut buf = Buffer::new(10, 1);
3928 buf.clear_dirty();
3929 buf.set_dirty_span_config(DirtySpanConfig::default().with_guard_band(5));
3930
3931 buf.mark_dirty_span(0, 2, 3);
3932 let spans = buf.dirty_span_row(0).unwrap().spans();
3933 assert_eq!(spans.len(), 1);
3934 assert_eq!(spans[0], DirtySpan::new(0, 8));
3935
3936 buf.clear_dirty();
3937 buf.mark_dirty_span(0, 8, 10);
3938 let spans = buf.dirty_span_row(0).unwrap().spans();
3939 assert_eq!(spans.len(), 1);
3940 assert_eq!(spans[0], DirtySpan::new(3, 10));
3941 }
3942
3943 #[test]
3944 fn dirty_span_empty_span_is_ignored() {
3945 let mut buf = Buffer::new(10, 1);
3946 buf.clear_dirty();
3947 buf.mark_dirty_span(0, 5, 5);
3948 let spans = buf.dirty_span_row(0).unwrap().spans();
3949 assert!(spans.is_empty());
3950 }
3951}