1#![forbid(unsafe_code)]
2
3use smallvec::SmallVec;
56
57use crate::budget::DegradationLevel;
58use crate::cell::{Cell, GraphemeId};
59use ftui_core::geometry::Rect;
60
61const DIRTY_SPAN_MAX_SPANS_PER_ROW: usize = 64;
63const DIRTY_SPAN_MERGE_GAP: u16 = 1;
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub struct DirtySpanConfig {
69 pub enabled: bool,
71 pub max_spans_per_row: usize,
73 pub merge_gap: u16,
75 pub guard_band: u16,
77}
78
79impl Default for DirtySpanConfig {
80 fn default() -> Self {
81 Self {
82 enabled: true,
83 max_spans_per_row: DIRTY_SPAN_MAX_SPANS_PER_ROW,
84 merge_gap: DIRTY_SPAN_MERGE_GAP,
85 guard_band: 0,
86 }
87 }
88}
89
90impl DirtySpanConfig {
91 #[must_use]
93 pub fn with_enabled(mut self, enabled: bool) -> Self {
94 self.enabled = enabled;
95 self
96 }
97
98 #[must_use]
100 pub fn with_max_spans_per_row(mut self, max_spans: usize) -> Self {
101 self.max_spans_per_row = max_spans;
102 self
103 }
104
105 #[must_use]
107 pub fn with_merge_gap(mut self, merge_gap: u16) -> Self {
108 self.merge_gap = merge_gap;
109 self
110 }
111
112 #[must_use]
114 pub fn with_guard_band(mut self, guard_band: u16) -> Self {
115 self.guard_band = guard_band;
116 self
117 }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub(crate) struct DirtySpan {
123 pub x0: u16,
124 pub x1: u16,
125}
126
127impl DirtySpan {
128 #[inline]
129 pub const fn new(x0: u16, x1: u16) -> Self {
130 Self { x0, x1 }
131 }
132
133 #[inline]
134 pub const fn len(self) -> usize {
135 self.x1.saturating_sub(self.x0) as usize
136 }
137}
138
139#[derive(Debug, Default, Clone)]
140pub(crate) struct DirtySpanRow {
141 overflow: bool,
142 spans: SmallVec<[DirtySpan; 4]>,
144}
145
146impl DirtySpanRow {
147 #[inline]
148 fn new_full() -> Self {
149 Self {
150 overflow: true,
151 spans: SmallVec::new(),
152 }
153 }
154
155 #[inline]
156 fn clear(&mut self) {
157 self.overflow = false;
158 self.spans.clear();
159 }
160
161 #[inline]
162 fn set_full(&mut self) {
163 self.overflow = true;
164 self.spans.clear();
165 }
166
167 #[inline]
168 pub(crate) fn spans(&self) -> &[DirtySpan] {
169 &self.spans
170 }
171
172 #[inline]
173 pub(crate) fn is_full(&self) -> bool {
174 self.overflow
175 }
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub struct DirtySpanStats {
181 pub rows_full_dirty: usize,
183 pub rows_with_spans: usize,
185 pub total_spans: usize,
187 pub overflows: usize,
189 pub span_coverage_cells: usize,
191 pub max_span_len: usize,
193 pub max_spans_per_row: usize,
195}
196
197#[derive(Debug, Clone)]
210pub struct Buffer {
211 width: u16,
212 height: u16,
213 cells: Vec<Cell>,
214 scissor_stack: Vec<Rect>,
215 opacity_stack: Vec<f32>,
216 pub degradation: DegradationLevel,
221 dirty_rows: Vec<bool>,
228 dirty_spans: Vec<DirtySpanRow>,
230 dirty_span_config: DirtySpanConfig,
232 dirty_span_overflows: usize,
234 dirty_bits: Vec<u8>,
236 dirty_cells: usize,
238 dirty_all: bool,
240}
241
242impl Buffer {
243 pub fn new(width: u16, height: u16) -> Self {
251 let width = width.max(1);
252 let height = height.max(1);
253
254 let size = width as usize * height as usize;
255 let cells = vec![Cell::default(); size];
256
257 let dirty_spans = (0..height)
258 .map(|_| DirtySpanRow::new_full())
259 .collect::<Vec<_>>();
260 let dirty_bits = vec![0u8; size];
261 let dirty_cells = size;
262 let dirty_all = true;
263
264 Self {
265 width,
266 height,
267 cells,
268 scissor_stack: vec![Rect::from_size(width, height)],
269 opacity_stack: vec![1.0],
270 degradation: DegradationLevel::Full,
271 dirty_rows: vec![true; height as usize],
274 dirty_spans,
276 dirty_span_config: DirtySpanConfig::default(),
277 dirty_span_overflows: 0,
278 dirty_bits,
279 dirty_cells,
280 dirty_all,
281 }
282 }
283
284 #[inline]
286 pub const fn width(&self) -> u16 {
287 self.width
288 }
289
290 #[inline]
292 pub const fn height(&self) -> u16 {
293 self.height
294 }
295
296 #[inline]
298 pub fn len(&self) -> usize {
299 self.cells.len()
300 }
301
302 #[inline]
304 pub fn is_empty(&self) -> bool {
305 self.cells.is_empty()
306 }
307
308 #[inline]
310 pub const fn bounds(&self) -> Rect {
311 Rect::from_size(self.width, self.height)
312 }
313
314 #[inline]
319 pub fn content_height(&self) -> u16 {
320 let default_cell = Cell::default();
321 let width = self.width as usize;
322 for y in (0..self.height).rev() {
323 let row_start = y as usize * width;
324 let row_end = row_start + width;
325 if self.cells[row_start..row_end]
326 .iter()
327 .any(|cell| *cell != default_cell)
328 {
329 return y + 1;
330 }
331 }
332 0
333 }
334
335 #[inline]
342 fn mark_dirty_row(&mut self, y: u16) {
343 if let Some(slot) = self.dirty_rows.get_mut(y as usize) {
344 *slot = true;
345 }
346 }
347
348 #[inline]
350 fn mark_dirty_bits_range(&mut self, y: u16, start: u16, end: u16) {
351 if self.dirty_all {
352 return;
353 }
354 if y >= self.height {
355 return;
356 }
357
358 let width = self.width;
359 if start >= width {
360 return;
361 }
362 let end = end.min(width);
363 if start >= end {
364 return;
365 }
366
367 let row_start = y as usize * width as usize;
368 let slice = &mut self.dirty_bits[row_start + start as usize..row_start + end as usize];
369 let newly_dirty = slice.iter().filter(|&&b| b == 0).count();
370 slice.fill(1);
371 self.dirty_cells = self.dirty_cells.saturating_add(newly_dirty);
372 }
373
374 #[inline]
376 fn mark_dirty_bits_row(&mut self, y: u16) {
377 self.mark_dirty_bits_range(y, 0, self.width);
378 }
379
380 #[inline]
382 fn mark_dirty_row_full(&mut self, y: u16) {
383 self.mark_dirty_row(y);
384 if self.dirty_span_config.enabled
385 && let Some(row) = self.dirty_spans.get_mut(y as usize)
386 {
387 row.set_full();
388 }
389 self.mark_dirty_bits_row(y);
390 }
391
392 #[inline]
394 pub(crate) fn mark_dirty_span(&mut self, y: u16, x0: u16, x1: u16) {
395 self.mark_dirty_row(y);
396 let width = self.width;
397 let (start, mut end) = if x0 <= x1 { (x0, x1) } else { (x1, x0) };
398 if start >= width {
399 return;
400 }
401 if end > width {
402 end = width;
403 }
404 if start >= end {
405 return;
406 }
407
408 self.mark_dirty_bits_range(y, start, end);
409
410 if !self.dirty_span_config.enabled {
411 return;
412 }
413
414 let guard_band = self.dirty_span_config.guard_band;
415 let span_start = start.saturating_sub(guard_band);
416 let mut span_end = end.saturating_add(guard_band);
417 if span_end > width {
418 span_end = width;
419 }
420 if span_start >= span_end {
421 return;
422 }
423
424 let Some(row) = self.dirty_spans.get_mut(y as usize) else {
425 return;
426 };
427
428 if row.is_full() {
429 return;
430 }
431
432 let new_span = DirtySpan::new(span_start, span_end);
433 let spans = &mut row.spans;
434 let insert_at = spans.partition_point(|span| span.x0 <= new_span.x0);
435 spans.insert(insert_at, new_span);
436
437 let merge_gap = self.dirty_span_config.merge_gap;
439 let mut i = if insert_at > 0 { insert_at - 1 } else { 0 };
440 while i + 1 < spans.len() {
441 let current = spans[i];
442 let next = spans[i + 1];
443 let merge_limit = current.x1.saturating_add(merge_gap);
444 if merge_limit >= next.x0 {
445 spans[i].x1 = current.x1.max(next.x1);
446 spans.remove(i + 1);
447 continue;
448 }
449 i += 1;
450 }
451
452 if spans.len() > self.dirty_span_config.max_spans_per_row {
453 row.set_full();
454 self.dirty_span_overflows = self.dirty_span_overflows.saturating_add(1);
455 }
456 }
457
458 #[inline]
460 pub fn mark_all_dirty(&mut self) {
461 self.dirty_rows.fill(true);
462 if self.dirty_span_config.enabled {
463 for row in &mut self.dirty_spans {
464 row.set_full();
465 }
466 } else {
467 for row in &mut self.dirty_spans {
468 row.clear();
469 }
470 }
471 self.dirty_all = true;
472 self.dirty_cells = self.cells.len();
473 }
474
475 #[inline]
479 pub fn clear_dirty(&mut self) {
480 self.dirty_rows.fill(false);
481 for row in &mut self.dirty_spans {
482 row.clear();
483 }
484 self.dirty_span_overflows = 0;
485 self.dirty_bits.fill(0);
486 self.dirty_cells = 0;
487 self.dirty_all = false;
488 }
489
490 #[inline]
492 pub fn is_row_dirty(&self, y: u16) -> bool {
493 self.dirty_rows.get(y as usize).copied().unwrap_or(false)
494 }
495
496 #[inline]
501 pub fn dirty_rows(&self) -> &[bool] {
502 &self.dirty_rows
503 }
504
505 #[inline]
507 pub fn dirty_row_count(&self) -> usize {
508 self.dirty_rows.iter().filter(|&&d| d).count()
509 }
510
511 #[must_use]
514 pub fn dirty_row_indices(&self) -> Vec<u16> {
515 self.dirty_rows
516 .iter()
517 .enumerate()
518 .filter_map(|(y, &dirty)| dirty.then_some(y as u16))
519 .collect()
520 }
521
522 #[inline]
524 #[allow(dead_code)]
525 pub(crate) fn dirty_bits(&self) -> &[u8] {
526 &self.dirty_bits
527 }
528
529 #[inline]
531 #[allow(dead_code)]
532 pub(crate) fn dirty_cell_count(&self) -> usize {
533 self.dirty_cells
534 }
535
536 #[inline]
538 #[allow(dead_code)]
539 pub(crate) fn dirty_all(&self) -> bool {
540 self.dirty_all
541 }
542
543 #[inline]
545 #[allow(dead_code)]
546 pub(crate) fn dirty_span_row(&self, y: u16) -> Option<&DirtySpanRow> {
547 if !self.dirty_span_config.enabled {
548 return None;
549 }
550 self.dirty_spans.get(y as usize)
551 }
552
553 pub fn dirty_span_stats(&self) -> DirtySpanStats {
555 if !self.dirty_span_config.enabled {
556 return DirtySpanStats {
557 rows_full_dirty: 0,
558 rows_with_spans: 0,
559 total_spans: 0,
560 overflows: 0,
561 span_coverage_cells: 0,
562 max_span_len: 0,
563 max_spans_per_row: self.dirty_span_config.max_spans_per_row,
564 };
565 }
566
567 let mut rows_full_dirty = 0usize;
568 let mut rows_with_spans = 0usize;
569 let mut total_spans = 0usize;
570 let mut span_coverage_cells = 0usize;
571 let mut max_span_len = 0usize;
572
573 for row in &self.dirty_spans {
574 if row.is_full() {
575 rows_full_dirty += 1;
576 span_coverage_cells += self.width as usize;
577 max_span_len = max_span_len.max(self.width as usize);
578 continue;
579 }
580 if !row.spans().is_empty() {
581 rows_with_spans += 1;
582 }
583 total_spans += row.spans().len();
584 for span in row.spans() {
585 span_coverage_cells += span.len();
586 max_span_len = max_span_len.max(span.len());
587 }
588 }
589
590 DirtySpanStats {
591 rows_full_dirty,
592 rows_with_spans,
593 total_spans,
594 overflows: self.dirty_span_overflows,
595 span_coverage_cells,
596 max_span_len,
597 max_spans_per_row: self.dirty_span_config.max_spans_per_row,
598 }
599 }
600
601 #[inline]
603 pub fn dirty_span_config(&self) -> DirtySpanConfig {
604 self.dirty_span_config
605 }
606
607 pub fn set_dirty_span_config(&mut self, config: DirtySpanConfig) {
617 if self.dirty_span_config == config {
618 return;
619 }
620 self.dirty_span_config = config;
621 for (y, row) in self.dirty_spans.iter_mut().enumerate() {
622 if self.dirty_rows.get(y).copied().unwrap_or(false) {
623 row.set_full();
624 } else {
625 row.clear();
626 }
627 }
628 self.dirty_span_overflows = 0;
629 }
630
631 #[inline]
637 fn index(&self, x: u16, y: u16) -> Option<usize> {
638 if x < self.width && y < self.height {
639 Some(y as usize * self.width as usize + x as usize)
640 } else {
641 None
642 }
643 }
644
645 #[inline]
651 pub(crate) fn index_unchecked(&self, x: u16, y: u16) -> usize {
652 debug_assert!(x < self.width && y < self.height);
653 y as usize * self.width as usize + x as usize
654 }
655
656 #[inline]
662 pub(crate) fn cell_mut_unchecked(&mut self, idx: usize) -> &mut Cell {
663 &mut self.cells[idx]
664 }
665
666 #[inline]
670 #[must_use]
671 pub fn get(&self, x: u16, y: u16) -> Option<&Cell> {
672 self.index(x, y).map(|i| &self.cells[i])
673 }
674
675 #[inline]
680 #[must_use]
681 pub fn get_mut(&mut self, x: u16, y: u16) -> Option<&mut Cell> {
682 let idx = self.index(x, y)?;
683 self.mark_dirty_span(y, x, x.saturating_add(1));
684 Some(&mut self.cells[idx])
685 }
686
687 #[inline]
694 pub fn get_unchecked(&self, x: u16, y: u16) -> &Cell {
695 let i = self.index_unchecked(x, y);
696 &self.cells[i]
697 }
698
699 #[inline]
703 fn cleanup_overlap(&mut self, x: u16, y: u16, new_cell: &Cell) -> Option<DirtySpan> {
704 let idx = self.index(x, y)?;
705 let current = self.cells[idx];
706 let mut touched = false;
707 let mut min_x = x;
708 let mut max_x = x;
709
710 if current.content.width() > 1 {
712 let width = current.content.width();
713 for i in 1..width {
718 let Some(cx) = x.checked_add(i as u16) else {
719 break;
720 };
721 if let Some(tail_idx) = self.index(cx, y)
722 && self.cells[tail_idx].is_continuation()
723 {
724 self.cells[tail_idx] = Cell::default();
725 touched = true;
726 min_x = min_x.min(cx);
727 max_x = max_x.max(cx);
728 }
729 }
730 }
731 else if current.is_continuation() && !new_cell.is_continuation() {
733 let mut back_x = x;
734 let limit = x.saturating_sub(GraphemeId::MAX_WIDTH as u16);
737
738 while back_x > limit {
739 back_x -= 1;
740 if let Some(h_idx) = self.index(back_x, y) {
741 let h_cell = self.cells[h_idx];
742 if !h_cell.is_continuation() {
743 let width = h_cell.content.width();
745 if (back_x as usize + width) > x as usize {
746 self.cells[h_idx] = Cell::default();
749 touched = true;
750 min_x = min_x.min(back_x);
751 max_x = max_x.max(back_x);
752
753 for i in 1..width {
756 let Some(cx) = back_x.checked_add(i as u16) else {
757 break;
758 };
759 if let Some(tail_idx) = self.index(cx, y) {
760 if self.cells[tail_idx].is_continuation() {
763 self.cells[tail_idx] = Cell::default();
764 touched = true;
765 min_x = min_x.min(cx);
766 max_x = max_x.max(cx);
767 }
768 }
769 }
770 }
771 break;
772 }
773 }
774 }
775 }
776
777 if touched {
778 Some(DirtySpan::new(min_x, max_x.saturating_add(1)))
779 } else {
780 None
781 }
782 }
783
784 #[inline]
791 fn cleanup_orphaned_tails(&mut self, start_x: u16, y: u16) {
792 if start_x >= self.width {
793 return;
794 }
795
796 let Some(idx) = self.index(start_x, y) else {
798 return;
799 };
800 if !self.cells[idx].is_continuation() {
801 return;
802 }
803
804 let mut x = start_x;
806 let mut max_x = x;
807 let row_end_idx = (y as usize * self.width as usize) + self.width as usize;
808 let mut curr_idx = idx;
809
810 while curr_idx < row_end_idx && self.cells[curr_idx].is_continuation() {
811 self.cells[curr_idx] = Cell::default();
812 max_x = x;
813 x = x.saturating_add(1);
814 curr_idx += 1;
815 }
816
817 self.mark_dirty_span(y, start_x, max_x.saturating_add(1));
819 }
820
821 #[inline]
836 pub fn set_fast(&mut self, x: u16, y: u16, cell: Cell) {
837 let bg_a = cell.bg.a();
844 if cell.content.width() > 1 || cell.is_continuation() || (bg_a != 255 && bg_a != 0) {
845 return self.set(x, y, cell);
846 }
847
848 if self.scissor_stack.len() != 1 || self.opacity_stack.len() != 1 {
850 return self.set(x, y, cell);
851 }
852
853 let Some(idx) = self.index(x, y) else {
855 return;
856 };
857
858 let existing = self.cells[idx];
862 if existing.content.width() > 1 || existing.is_continuation() {
863 return self.set(x, y, cell);
864 }
865
866 let mut final_cell = cell;
872 if bg_a == 0 {
873 final_cell.bg = existing.bg;
874 }
875
876 self.cells[idx] = final_cell;
877 self.mark_dirty_span(y, x, x.saturating_add(1));
878 self.cleanup_orphaned_tails(x.saturating_add(1), y);
879 }
880
881 #[inline]
893 pub fn set(&mut self, x: u16, y: u16, cell: Cell) {
894 let width = cell.content.width();
895
896 if width <= 1 {
898 let Some(idx) = self.index(x, y) else {
900 return;
901 };
902
903 if !self.current_scissor().contains(x, y) {
905 return;
906 }
907
908 let mut span_start = x;
910 let mut span_end = x.saturating_add(1);
911 if let Some(span) = self.cleanup_overlap(x, y, &cell) {
912 span_start = span_start.min(span.x0);
913 span_end = span_end.max(span.x1);
914 }
915
916 let existing_bg = self.cells[idx].bg;
917
918 let mut final_cell = if self.current_opacity() < 1.0 {
920 let opacity = self.current_opacity();
921 Cell {
922 fg: cell.fg.with_opacity(opacity),
923 bg: cell.bg.with_opacity(opacity),
924 ..cell
925 }
926 } else {
927 cell
928 };
929
930 final_cell.bg = final_cell.bg.over(existing_bg);
931
932 self.cells[idx] = final_cell;
933 self.mark_dirty_span(y, span_start, span_end);
934 self.cleanup_orphaned_tails(x.saturating_add(1), y);
935 return;
936 }
937
938 let scissor = self.current_scissor();
941 for i in 0..width {
942 let Some(cx) = x.checked_add(i as u16) else {
943 return;
944 };
945 if cx >= self.width || y >= self.height {
947 return;
948 }
949 if !scissor.contains(cx, y) {
951 return;
952 }
953 }
954
955 let mut span_start = x;
959 let mut span_end = x.saturating_add(width as u16);
960 if let Some(span) = self.cleanup_overlap(x, y, &cell) {
961 span_start = span_start.min(span.x0);
962 span_end = span_end.max(span.x1);
963 }
964 for i in 1..width {
965 if let Some(span) = self.cleanup_overlap(x + i as u16, y, &Cell::CONTINUATION) {
967 span_start = span_start.min(span.x0);
968 span_end = span_end.max(span.x1);
969 }
970 }
971
972 let idx = self.index_unchecked(x, y);
974 let old_cell = self.cells[idx];
975 let mut final_cell = if self.current_opacity() < 1.0 {
976 let opacity = self.current_opacity();
977 Cell {
978 fg: cell.fg.with_opacity(opacity),
979 bg: cell.bg.with_opacity(opacity),
980 ..cell
981 }
982 } else {
983 cell
984 };
985
986 final_cell.bg = final_cell.bg.over(old_cell.bg);
988
989 self.cells[idx] = final_cell;
990
991 for i in 1..width {
994 let idx = self.index_unchecked(x + i as u16, y);
995 self.cells[idx] = Cell::CONTINUATION;
996 }
997 self.mark_dirty_span(y, span_start, span_end);
998 self.cleanup_orphaned_tails(x.saturating_add(width as u16), y);
999 }
1000
1001 #[inline]
1014 pub fn set_raw(&mut self, x: u16, y: u16, cell: Cell) {
1015 if let Some(idx) = self.index(x, y) {
1016 let mut span = DirtySpan::new(x, x.saturating_add(1));
1017 let raw_wide_head = cell.content.width() > 1 && !cell.is_continuation();
1018
1019 if !raw_wide_head && let Some(cleanup_span) = self.cleanup_overlap(x, y, &cell) {
1020 span = DirtySpan::new(span.x0.min(cleanup_span.x0), span.x1.max(cleanup_span.x1));
1021 }
1022 self.cells[idx] = cell;
1023 self.mark_dirty_span(y, span.x0, span.x1);
1024 if !raw_wide_head {
1025 let sweep_from = if cell.is_continuation() {
1034 self.continuation_owner_extent(x, y)
1035 .unwrap_or_else(|| x.saturating_add(1))
1036 } else {
1037 x.saturating_add(1)
1038 };
1039 self.cleanup_orphaned_tails(sweep_from, y);
1040 }
1041 }
1042 }
1043
1044 fn continuation_owner_extent(&self, x: u16, y: u16) -> Option<u16> {
1048 let limit = x.saturating_sub(GraphemeId::MAX_WIDTH as u16);
1049 let mut back_x = x;
1050 while back_x > limit {
1051 back_x -= 1;
1052 let idx = self.index(back_x, y)?;
1053 let cell = self.cells[idx];
1054 if !cell.is_continuation() {
1055 let end = back_x.saturating_add(cell.content.width() as u16);
1056 return (end > x).then_some(end);
1057 }
1058 }
1059 None
1060 }
1061
1062 #[inline]
1066 pub fn fill(&mut self, rect: Rect, cell: Cell) {
1067 let clipped = self.current_scissor().intersection(&rect);
1068 if clipped.is_empty() {
1069 return;
1070 }
1071
1072 let cell_width = cell.content.width();
1075 if cell_width <= 1
1076 && !cell.is_continuation()
1077 && self.current_opacity() >= 1.0
1078 && cell.bg.a() == 255
1079 && clipped.x == 0
1080 && clipped.width == self.width
1081 {
1082 let row_width = self.width as usize;
1083 for y in clipped.y..clipped.bottom() {
1084 let row_start = y as usize * row_width;
1085 let row_end = row_start + row_width;
1086 self.cells[row_start..row_end].fill(cell);
1087 self.mark_dirty_row_full(y);
1088 }
1089 return;
1090 }
1091
1092 if cell_width <= 1
1096 && !cell.is_continuation()
1097 && self.current_opacity() >= 1.0
1098 && cell.bg.a() == 255
1099 && self.scissor_stack.len() == 1
1100 {
1101 let row_width = self.width as usize;
1102 let x_start = clipped.x as usize;
1103 let x_end = clipped.right() as usize;
1104 for y in clipped.y..clipped.bottom() {
1105 let row_start = y as usize * row_width;
1106 let mut dirty_left = clipped.x;
1107 let mut dirty_right = clipped.right();
1108
1109 if x_start > 0 && self.cells[row_start + x_start].is_continuation() {
1112 let mut head_found = None;
1113 for hx in (0..x_start).rev() {
1114 if !self.cells[row_start + hx].is_continuation() {
1115 head_found = Some(hx);
1116 break;
1117 }
1118 }
1119
1120 if let Some(hx) = head_found {
1121 let c = self.cells[row_start + hx];
1122 let width = c.content.width();
1123 if width > 1 && hx + width > x_start {
1125 for cx in hx..x_start {
1128 self.cells[row_start + cx] = Cell::default();
1129 }
1130 dirty_left = hx as u16;
1131 }
1132 }
1133 }
1134
1135 {
1138 let mut cx = x_end;
1139 while cx < row_width && self.cells[row_start + cx].is_continuation() {
1140 self.cells[row_start + cx] = Cell::default();
1141 dirty_right = (cx as u16).saturating_add(1);
1142 cx += 1;
1143 }
1144 }
1145
1146 self.cells[row_start + x_start..row_start + x_end].fill(cell);
1147 self.mark_dirty_span(y, dirty_left, dirty_right);
1148 }
1149 return;
1150 }
1151
1152 self.push_scissor(clipped);
1154
1155 let step = cell.content.width().max(1) as u16;
1156 for y in clipped.y..clipped.bottom() {
1157 for x in clipped.x..clipped.right() {
1163 self.set(x, y, Cell::default());
1164 }
1165 let mut x = clipped.x;
1166 while x < clipped.right() {
1167 self.set(x, y, cell);
1168 x = x.saturating_add(step);
1169 }
1170 }
1171
1172 self.pop_scissor();
1173 }
1174
1175 #[inline]
1177 pub fn clear(&mut self) {
1178 self.cells.fill(Cell::default());
1179 self.mark_all_dirty();
1180 }
1181
1182 pub fn reset_for_frame(&mut self) {
1187 self.scissor_stack.truncate(1);
1188 if let Some(base) = self.scissor_stack.first_mut() {
1189 *base = Rect::from_size(self.width, self.height);
1190 } else {
1191 self.scissor_stack
1192 .push(Rect::from_size(self.width, self.height));
1193 }
1194
1195 self.opacity_stack.truncate(1);
1196 if let Some(base) = self.opacity_stack.first_mut() {
1197 *base = 1.0;
1198 } else {
1199 self.opacity_stack.push(1.0);
1200 }
1201
1202 self.clear();
1203 }
1204
1205 #[inline]
1207 pub fn clear_with(&mut self, cell: Cell) {
1208 if cell.is_continuation() {
1209 self.clear();
1210 return;
1211 }
1212
1213 let width = cell.content.width();
1214 if width <= 1 {
1215 self.cells.fill(cell);
1216 self.mark_all_dirty();
1217 return;
1218 }
1219
1220 self.cells.fill(Cell::default());
1221 let step = width as u16;
1222 for y in 0..self.height {
1223 let row_start = y as usize * self.width as usize;
1224 let mut x = 0u16;
1225 while x.saturating_add(step) <= self.width {
1226 let head_idx = row_start + x as usize;
1227 self.cells[head_idx] = cell;
1228 for off in 1..step {
1229 self.cells[head_idx + off as usize] = Cell::CONTINUATION;
1230 }
1231 x = x.saturating_add(step);
1232 }
1233 }
1234 self.mark_all_dirty();
1235 }
1236
1237 #[inline]
1241 pub fn cells(&self) -> &[Cell] {
1242 &self.cells
1243 }
1244
1245 #[inline]
1249 pub fn cells_mut(&mut self) -> &mut [Cell] {
1250 self.mark_all_dirty();
1251 &mut self.cells
1252 }
1253
1254 #[inline]
1260 pub fn row_cells(&self, y: u16) -> &[Cell] {
1261 let start = y as usize * self.width as usize;
1262 &self.cells[start..start + self.width as usize]
1263 }
1264
1265 #[inline]
1275 pub fn row_cells_mut_span(&mut self, y: u16, x0: u16, x1: u16) -> Option<&mut [Cell]> {
1276 if y >= self.height {
1277 return None;
1278 }
1279 if x0 >= x1 {
1280 return None;
1281 }
1282
1283 let start = x0.min(self.width);
1284 let end = x1.min(self.width);
1285 if start >= end {
1286 return None;
1287 }
1288
1289 self.mark_dirty_span(y, start, end);
1290
1291 let row_start = y as usize * self.width as usize;
1292 let slice_start = row_start + start as usize;
1293 let slice_end = row_start + end as usize;
1294 Some(&mut self.cells[slice_start..slice_end])
1295 }
1296
1297 #[inline]
1304 pub fn push_scissor(&mut self, rect: Rect) {
1305 let current = self.current_scissor();
1306 let intersected = current.intersection(&rect);
1307 self.scissor_stack.push(intersected);
1308 }
1309
1310 #[inline]
1314 pub fn pop_scissor(&mut self) {
1315 if self.scissor_stack.len() > 1 {
1316 self.scissor_stack.pop();
1317 }
1318 }
1319
1320 #[inline]
1322 pub fn current_scissor(&self) -> Rect {
1323 *self
1324 .scissor_stack
1325 .last()
1326 .expect("scissor stack always has at least one element")
1327 }
1328
1329 #[inline]
1331 pub fn scissor_depth(&self) -> usize {
1332 self.scissor_stack.len()
1333 }
1334
1335 #[inline]
1342 pub fn push_opacity(&mut self, opacity: f32) {
1343 let clamped = opacity.clamp(0.0, 1.0);
1344 let current = self.current_opacity();
1345 self.opacity_stack.push(current * clamped);
1346 }
1347
1348 #[inline]
1352 pub fn pop_opacity(&mut self) {
1353 if self.opacity_stack.len() > 1 {
1354 self.opacity_stack.pop();
1355 }
1356 }
1357
1358 #[inline]
1360 pub fn current_opacity(&self) -> f32 {
1361 *self
1362 .opacity_stack
1363 .last()
1364 .expect("opacity stack always has at least one element")
1365 }
1366
1367 #[inline]
1369 pub fn opacity_depth(&self) -> usize {
1370 self.opacity_stack.len()
1371 }
1372
1373 pub fn copy_from(&mut self, src: &Buffer, src_rect: Rect, dst_x: u16, dst_y: u16) {
1380 let copy_bounds = Rect::new(dst_x, dst_y, src_rect.width, src_rect.height);
1383 self.push_scissor(copy_bounds);
1384 let clip = self.current_scissor();
1385
1386 for dy in 0..src_rect.height {
1387 let Some(target_y) = dst_y.checked_add(dy) else {
1389 continue;
1390 };
1391 let Some(sy) = src_rect.y.checked_add(dy) else {
1392 continue;
1393 };
1394
1395 let mut dx = 0u16;
1396 while dx < src_rect.width {
1397 let Some(target_x) = dst_x.checked_add(dx) else {
1399 dx = dx.saturating_add(1);
1400 continue;
1401 };
1402 let Some(sx) = src_rect.x.checked_add(dx) else {
1403 dx = dx.saturating_add(1);
1404 continue;
1405 };
1406
1407 if let Some(cell) = src.get(sx, sy) {
1408 if cell.is_continuation() {
1412 self.set(target_x, target_y, Cell::default());
1413 dx = dx.saturating_add(1);
1414 continue;
1415 }
1416
1417 let width = cell.content.width();
1418 let target_right = target_x.saturating_add(width as u16);
1419
1420 let src_clipped = width > 1 && dx.saturating_add(width as u16) > src_rect.width;
1429 let dst_clipped = target_right > clip.right()
1430 || (width > 1 && target_x < clip.left() && target_right > clip.left());
1431
1432 if src_clipped || dst_clipped {
1433 let valid_width = (clip.right().saturating_sub(target_x)).min(width as u16);
1439 for i in 0..valid_width {
1440 self.set(target_x + i, target_y, Cell::default());
1441 }
1442 } else {
1443 self.set(target_x, target_y, *cell);
1444 }
1445
1446 if width > 1 {
1448 dx = dx.saturating_add(width as u16);
1449 } else {
1450 dx = dx.saturating_add(1);
1451 }
1452 } else {
1453 dx = dx.saturating_add(1);
1454 }
1455 }
1456 }
1457
1458 self.pop_scissor();
1459 }
1460
1461 pub fn content_eq(&self, other: &Buffer) -> bool {
1463 self.width == other.width && self.height == other.height && self.cells == other.cells
1464 }
1465}
1466
1467impl Default for Buffer {
1468 fn default() -> Self {
1470 Self::new(1, 1)
1471 }
1472}
1473
1474impl PartialEq for Buffer {
1475 fn eq(&self, other: &Self) -> bool {
1476 self.content_eq(other)
1477 }
1478}
1479
1480impl Eq for Buffer {}
1481
1482#[derive(Debug)]
1501pub struct DoubleBuffer {
1502 buffers: [Buffer; 2],
1503 current_idx: u8,
1505}
1506
1507const ADAPTIVE_GROWTH_FACTOR: f32 = 1.25;
1513
1514const ADAPTIVE_SHRINK_THRESHOLD: f32 = 0.50;
1517
1518const ADAPTIVE_MAX_OVERAGE: u16 = 200;
1520
1521#[derive(Debug)]
1553pub struct AdaptiveDoubleBuffer {
1554 inner: DoubleBuffer,
1556 logical_width: u16,
1558 logical_height: u16,
1560 capacity_width: u16,
1562 capacity_height: u16,
1564 stats: AdaptiveStats,
1566}
1567
1568#[derive(Debug, Clone, Default)]
1570pub struct AdaptiveStats {
1571 pub resize_avoided: u64,
1573 pub resize_reallocated: u64,
1575 pub resize_growth: u64,
1577 pub resize_shrink: u64,
1579}
1580
1581impl AdaptiveStats {
1582 pub fn reset(&mut self) {
1584 *self = Self::default();
1585 }
1586
1587 pub fn avoidance_ratio(&self) -> f64 {
1589 let total = self.resize_avoided + self.resize_reallocated;
1590 if total == 0 {
1591 1.0
1592 } else {
1593 self.resize_avoided as f64 / total as f64
1594 }
1595 }
1596}
1597
1598impl DoubleBuffer {
1599 pub fn new(width: u16, height: u16) -> Self {
1604 Self {
1605 buffers: [Buffer::new(width, height), Buffer::new(width, height)],
1606 current_idx: 0,
1607 }
1608 }
1609
1610 #[inline]
1615 pub fn swap(&mut self) {
1616 self.current_idx = 1 - self.current_idx;
1617 }
1618
1619 #[inline]
1621 pub fn current(&self) -> &Buffer {
1622 &self.buffers[self.current_idx as usize]
1623 }
1624
1625 #[inline]
1627 pub fn current_mut(&mut self) -> &mut Buffer {
1628 &mut self.buffers[self.current_idx as usize]
1629 }
1630
1631 #[inline]
1633 pub fn previous(&self) -> &Buffer {
1634 &self.buffers[(1 - self.current_idx) as usize]
1635 }
1636
1637 #[inline]
1639 pub fn previous_mut(&mut self) -> &mut Buffer {
1640 &mut self.buffers[(1 - self.current_idx) as usize]
1641 }
1642
1643 #[inline]
1645 pub fn width(&self) -> u16 {
1646 self.buffers[0].width()
1647 }
1648
1649 #[inline]
1651 pub fn height(&self) -> u16 {
1652 self.buffers[0].height()
1653 }
1654
1655 pub fn resize(&mut self, width: u16, height: u16) -> bool {
1660 let width = width.max(1);
1664 let height = height.max(1);
1665 if self.buffers[0].width() == width && self.buffers[0].height() == height {
1666 return false;
1667 }
1668 self.buffers = [Buffer::new(width, height), Buffer::new(width, height)];
1669 self.current_idx = 0;
1670 true
1671 }
1672
1673 #[inline]
1676 pub fn dimensions_match(&self, width: u16, height: u16) -> bool {
1677 self.buffers[0].width() == width.max(1) && self.buffers[0].height() == height.max(1)
1678 }
1679}
1680
1681impl AdaptiveDoubleBuffer {
1686 pub fn new(width: u16, height: u16) -> Self {
1691 let (cap_w, cap_h) = Self::compute_capacity(width, height);
1692 Self {
1693 inner: DoubleBuffer::new(cap_w, cap_h),
1694 logical_width: width,
1695 logical_height: height,
1696 capacity_width: cap_w,
1697 capacity_height: cap_h,
1698 stats: AdaptiveStats::default(),
1699 }
1700 }
1701
1702 fn compute_capacity(width: u16, height: u16) -> (u16, u16) {
1706 let extra_w =
1707 ((width as f32 * (ADAPTIVE_GROWTH_FACTOR - 1.0)) as u16).min(ADAPTIVE_MAX_OVERAGE);
1708 let extra_h =
1709 ((height as f32 * (ADAPTIVE_GROWTH_FACTOR - 1.0)) as u16).min(ADAPTIVE_MAX_OVERAGE);
1710
1711 let cap_w = width.saturating_add(extra_w);
1712 let cap_h = height.saturating_add(extra_h);
1713
1714 (cap_w, cap_h)
1715 }
1716
1717 fn needs_reallocation(&self, width: u16, height: u16) -> bool {
1721 if width > self.capacity_width || height > self.capacity_height {
1723 return true;
1724 }
1725
1726 let shrink_threshold_w = (self.capacity_width as f32 * ADAPTIVE_SHRINK_THRESHOLD) as u16;
1728 let shrink_threshold_h = (self.capacity_height as f32 * ADAPTIVE_SHRINK_THRESHOLD) as u16;
1729
1730 width < shrink_threshold_w || height < shrink_threshold_h
1731 }
1732
1733 #[inline]
1738 pub fn swap(&mut self) {
1739 self.inner.swap();
1740 }
1741
1742 #[inline]
1747 pub fn current(&self) -> &Buffer {
1748 self.inner.current()
1749 }
1750
1751 #[inline]
1753 pub fn current_mut(&mut self) -> &mut Buffer {
1754 self.inner.current_mut()
1755 }
1756
1757 #[inline]
1759 pub fn previous(&self) -> &Buffer {
1760 self.inner.previous()
1761 }
1762
1763 #[inline]
1765 pub fn width(&self) -> u16 {
1766 self.logical_width
1767 }
1768
1769 #[inline]
1771 pub fn height(&self) -> u16 {
1772 self.logical_height
1773 }
1774
1775 #[inline]
1777 pub fn capacity_width(&self) -> u16 {
1778 self.capacity_width
1779 }
1780
1781 #[inline]
1783 pub fn capacity_height(&self) -> u16 {
1784 self.capacity_height
1785 }
1786
1787 #[inline]
1789 pub fn stats(&self) -> &AdaptiveStats {
1790 &self.stats
1791 }
1792
1793 pub fn reset_stats(&mut self) {
1795 self.stats.reset();
1796 }
1797
1798 pub fn resize(&mut self, width: u16, height: u16) -> bool {
1810 if width == self.logical_width && height == self.logical_height {
1812 return false;
1813 }
1814
1815 let is_growth = width > self.logical_width || height > self.logical_height;
1816 if is_growth {
1817 self.stats.resize_growth += 1;
1818 } else {
1819 self.stats.resize_shrink += 1;
1820 }
1821
1822 if self.needs_reallocation(width, height) {
1823 let (cap_w, cap_h) = Self::compute_capacity(width, height);
1825 self.inner = DoubleBuffer::new(cap_w, cap_h);
1826 self.capacity_width = cap_w;
1827 self.capacity_height = cap_h;
1828 self.stats.resize_reallocated += 1;
1829 } else {
1830 self.inner.current_mut().clear();
1833 self.inner.previous_mut().clear();
1834 self.stats.resize_avoided += 1;
1835 }
1836
1837 self.logical_width = width;
1838 self.logical_height = height;
1839 true
1840 }
1841
1842 #[inline]
1844 pub fn dimensions_match(&self, width: u16, height: u16) -> bool {
1845 self.logical_width == width && self.logical_height == height
1846 }
1847
1848 #[inline]
1850 pub fn logical_bounds(&self) -> Rect {
1851 Rect::from_size(self.logical_width, self.logical_height)
1852 }
1853
1854 pub fn memory_efficiency(&self) -> f64 {
1856 let logical = self.logical_width as u64 * self.logical_height as u64;
1857 let capacity = self.capacity_width as u64 * self.capacity_height as u64;
1858 if capacity == 0 {
1859 1.0
1860 } else {
1861 logical as f64 / capacity as f64
1862 }
1863 }
1864}
1865
1866#[cfg(test)]
1867mod tests {
1868 use super::*;
1869 use crate::cell::{CellContent, PackedRgba};
1870
1871 #[test]
1872 fn set_composites_background() {
1873 let mut buf = Buffer::new(1, 1);
1874
1875 let red = PackedRgba::rgb(255, 0, 0);
1877 buf.set(0, 0, Cell::default().with_bg(red));
1878
1879 let cell = Cell::from_char('X'); buf.set(0, 0, cell);
1882
1883 let result = buf.get(0, 0).unwrap();
1884 assert_eq!(result.content.as_char(), Some('X'));
1885 assert_eq!(
1886 result.bg, red,
1887 "Background should be preserved (composited)"
1888 );
1889 }
1890
1891 #[test]
1892 fn set_fast_matches_set_for_transparent_bg() {
1893 let red = PackedRgba::rgb(255, 0, 0);
1894 let cell = Cell::from_char('X').with_fg(PackedRgba::rgb(0, 255, 0));
1895
1896 let mut a = Buffer::new(1, 1);
1897 a.set(0, 0, Cell::default().with_bg(red));
1898 a.set(0, 0, cell);
1899
1900 let mut b = Buffer::new(1, 1);
1901 b.set(0, 0, Cell::default().with_bg(red));
1902 b.set_fast(0, 0, cell);
1903
1904 assert_eq!(a.get(0, 0), b.get(0, 0));
1905 }
1906
1907 #[test]
1908 fn set_fast_matches_set_for_opaque_bg() {
1909 let cell = Cell::from_char('X')
1910 .with_fg(PackedRgba::rgb(0, 255, 0))
1911 .with_bg(PackedRgba::rgb(255, 0, 0));
1912
1913 let mut a = Buffer::new(1, 1);
1914 a.set(0, 0, cell);
1915
1916 let mut b = Buffer::new(1, 1);
1917 b.set_fast(0, 0, cell);
1918
1919 assert_eq!(a.get(0, 0), b.get(0, 0));
1920 }
1921
1922 #[test]
1923 fn set_fast_clears_orphaned_tail_like_set() {
1924 let mut slow = Buffer::new(3, 1);
1925 slow.set_raw(0, 0, Cell::from_char('A'));
1926 slow.set_raw(1, 0, Cell::CONTINUATION);
1927 slow.clear_dirty();
1928
1929 let mut fast = slow.clone();
1930
1931 slow.set(0, 0, Cell::from_char('X'));
1932 fast.set_fast(0, 0, Cell::from_char('X'));
1933
1934 assert_eq!(slow.cells(), fast.cells());
1935 assert_eq!(fast.get(1, 0), Some(&Cell::default()));
1936
1937 let spans = fast.dirty_span_row(0).expect("dirty span row").spans();
1938 assert_eq!(spans, &[DirtySpan::new(0, 2)]);
1939 }
1940
1941 #[test]
1942 fn rect_contains() {
1943 let r = Rect::new(5, 5, 10, 10);
1944 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)); }
1950
1951 #[test]
1952 fn rect_intersection() {
1953 let a = Rect::new(0, 0, 10, 10);
1954 let b = Rect::new(5, 5, 10, 10);
1955 let i = a.intersection(&b);
1956 assert_eq!(i, Rect::new(5, 5, 5, 5));
1957
1958 let c = Rect::new(20, 20, 5, 5);
1960 assert_eq!(a.intersection(&c), Rect::default());
1961 }
1962
1963 #[test]
1964 fn buffer_creation() {
1965 let buf = Buffer::new(80, 24);
1966 assert_eq!(buf.width(), 80);
1967 assert_eq!(buf.height(), 24);
1968 assert_eq!(buf.len(), 80 * 24);
1969 }
1970
1971 #[test]
1972 fn content_height_empty_is_zero() {
1973 let buf = Buffer::new(8, 4);
1974 assert_eq!(buf.content_height(), 0);
1975 }
1976
1977 #[test]
1978 fn content_height_tracks_last_non_empty_row() {
1979 let mut buf = Buffer::new(5, 4);
1980 buf.set(0, 0, Cell::from_char('A'));
1981 assert_eq!(buf.content_height(), 1);
1982
1983 buf.set(2, 3, Cell::from_char('Z'));
1984 assert_eq!(buf.content_height(), 4);
1985 }
1986
1987 #[test]
1988 fn buffer_zero_width_clamped_to_one() {
1989 let buf = Buffer::new(0, 24);
1990 assert_eq!(buf.width(), 1);
1991 assert_eq!(buf.height(), 24);
1992 }
1993
1994 #[test]
1995 fn buffer_zero_height_clamped_to_one() {
1996 let buf = Buffer::new(80, 0);
1997 assert_eq!(buf.width(), 80);
1998 assert_eq!(buf.height(), 1);
1999 }
2000
2001 #[test]
2002 fn buffer_get_and_set() {
2003 let mut buf = Buffer::new(10, 10);
2004 let cell = Cell::from_char('X');
2005 buf.set(5, 5, cell);
2006 assert_eq!(buf.get(5, 5).unwrap().content.as_char(), Some('X'));
2007 }
2008
2009 #[test]
2010 fn buffer_out_of_bounds_get() {
2011 let buf = Buffer::new(10, 10);
2012 assert!(buf.get(10, 0).is_none());
2013 assert!(buf.get(0, 10).is_none());
2014 assert!(buf.get(100, 100).is_none());
2015 }
2016
2017 #[test]
2018 fn buffer_out_of_bounds_set_ignored() {
2019 let mut buf = Buffer::new(10, 10);
2020 buf.set(100, 100, Cell::from_char('X')); assert_eq!(buf.cells().iter().filter(|c| !c.is_empty()).count(), 0);
2022 }
2023
2024 #[test]
2025 fn buffer_clear() {
2026 let mut buf = Buffer::new(10, 10);
2027 buf.set(5, 5, Cell::from_char('X'));
2028 buf.clear();
2029 assert!(buf.get(5, 5).unwrap().is_empty());
2030 }
2031
2032 #[test]
2033 fn scissor_stack_basic() {
2034 let mut buf = Buffer::new(20, 20);
2035
2036 assert_eq!(buf.current_scissor(), Rect::from_size(20, 20));
2038 assert_eq!(buf.scissor_depth(), 1);
2039
2040 buf.push_scissor(Rect::new(5, 5, 10, 10));
2042 assert_eq!(buf.current_scissor(), Rect::new(5, 5, 10, 10));
2043 assert_eq!(buf.scissor_depth(), 2);
2044
2045 buf.set(7, 7, Cell::from_char('I'));
2047 assert_eq!(buf.get(7, 7).unwrap().content.as_char(), Some('I'));
2048
2049 buf.set(0, 0, Cell::from_char('O'));
2051 assert!(buf.get(0, 0).unwrap().is_empty());
2052
2053 buf.pop_scissor();
2055 assert_eq!(buf.current_scissor(), Rect::from_size(20, 20));
2056 assert_eq!(buf.scissor_depth(), 1);
2057
2058 buf.set(0, 0, Cell::from_char('N'));
2060 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('N'));
2061 }
2062
2063 #[test]
2064 fn scissor_intersection() {
2065 let mut buf = Buffer::new(20, 20);
2066 buf.push_scissor(Rect::new(5, 5, 10, 10));
2067 buf.push_scissor(Rect::new(8, 8, 10, 10));
2068
2069 assert_eq!(buf.current_scissor(), Rect::new(8, 8, 7, 7));
2072 }
2073
2074 #[test]
2075 fn scissor_base_cannot_be_popped() {
2076 let mut buf = Buffer::new(10, 10);
2077 buf.pop_scissor(); assert_eq!(buf.scissor_depth(), 1);
2079 buf.pop_scissor(); assert_eq!(buf.scissor_depth(), 1);
2081 }
2082
2083 #[test]
2084 fn opacity_stack_basic() {
2085 let mut buf = Buffer::new(10, 10);
2086
2087 assert!((buf.current_opacity() - 1.0).abs() < f32::EPSILON);
2089 assert_eq!(buf.opacity_depth(), 1);
2090
2091 buf.push_opacity(0.5);
2093 assert!((buf.current_opacity() - 0.5).abs() < f32::EPSILON);
2094 assert_eq!(buf.opacity_depth(), 2);
2095
2096 buf.push_opacity(0.5);
2098 assert!((buf.current_opacity() - 0.25).abs() < f32::EPSILON);
2099 assert_eq!(buf.opacity_depth(), 3);
2100
2101 buf.pop_opacity();
2103 assert!((buf.current_opacity() - 0.5).abs() < f32::EPSILON);
2104 }
2105
2106 #[test]
2107 fn opacity_applied_to_cells() {
2108 let mut buf = Buffer::new(10, 10);
2109 buf.push_opacity(0.5);
2110
2111 let cell = Cell::from_char('X').with_fg(PackedRgba::rgba(100, 100, 100, 255));
2112 buf.set(5, 5, cell);
2113
2114 let stored = buf.get(5, 5).unwrap();
2115 assert_eq!(stored.fg.a(), 128);
2117 }
2118
2119 #[test]
2120 fn opacity_composites_background_before_storage() {
2121 let mut buf = Buffer::new(1, 1);
2122
2123 let red = PackedRgba::rgb(255, 0, 0);
2124 let blue = PackedRgba::rgb(0, 0, 255);
2125
2126 buf.set(0, 0, Cell::default().with_bg(red));
2127 buf.push_opacity(0.5);
2128 buf.set(0, 0, Cell::default().with_bg(blue));
2129
2130 let stored = buf.get(0, 0).unwrap();
2131 let expected = blue.with_opacity(0.5).over(red);
2132 assert_eq!(stored.bg, expected);
2133 }
2134
2135 #[test]
2136 fn opacity_clamped() {
2137 let mut buf = Buffer::new(10, 10);
2138 buf.push_opacity(2.0); assert!((buf.current_opacity() - 1.0).abs() < f32::EPSILON);
2140
2141 buf.push_opacity(-1.0); assert!((buf.current_opacity() - 0.0).abs() < f32::EPSILON);
2143 }
2144
2145 #[test]
2146 fn opacity_base_cannot_be_popped() {
2147 let mut buf = Buffer::new(10, 10);
2148 buf.pop_opacity(); assert_eq!(buf.opacity_depth(), 1);
2150 }
2151
2152 #[test]
2153 fn buffer_fill() {
2154 let mut buf = Buffer::new(10, 10);
2155 let cell = Cell::from_char('#');
2156 buf.fill(Rect::new(2, 2, 5, 5), cell);
2157
2158 assert_eq!(buf.get(3, 3).unwrap().content.as_char(), Some('#'));
2160
2161 assert!(buf.get(0, 0).unwrap().is_empty());
2163 }
2164
2165 #[test]
2166 fn buffer_fill_respects_scissor() {
2167 let mut buf = Buffer::new(10, 10);
2168 buf.push_scissor(Rect::new(3, 3, 4, 4));
2169
2170 let cell = Cell::from_char('#');
2171 buf.fill(Rect::new(0, 0, 10, 10), cell);
2172
2173 assert_eq!(buf.get(3, 3).unwrap().content.as_char(), Some('#'));
2175 assert!(buf.get(0, 0).unwrap().is_empty());
2176 assert!(buf.get(7, 7).unwrap().is_empty());
2177 }
2178
2179 #[test]
2180 fn buffer_copy_from() {
2181 let mut src = Buffer::new(10, 10);
2182 src.set(2, 2, Cell::from_char('S'));
2183
2184 let mut dst = Buffer::new(10, 10);
2185 dst.copy_from(&src, Rect::new(0, 0, 5, 5), 3, 3);
2186
2187 assert_eq!(dst.get(5, 5).unwrap().content.as_char(), Some('S'));
2189 }
2190
2191 #[test]
2192 fn copy_from_clips_wide_char_at_boundary() {
2193 let mut src = Buffer::new(10, 1);
2194 src.set(0, 0, Cell::from_char('中'));
2196
2197 let mut dst = Buffer::new(10, 1);
2198 dst.copy_from(&src, Rect::new(0, 0, 1, 1), 0, 0);
2201
2202 assert!(
2211 dst.get(0, 0).unwrap().is_empty(),
2212 "Wide char head should not be written if tail is clipped"
2213 );
2214 assert!(
2215 dst.get(1, 0).unwrap().is_empty(),
2216 "Wide char tail should not be leaked outside copy region"
2217 );
2218 }
2219
2220 #[test]
2221 fn buffer_content_eq() {
2222 let mut buf1 = Buffer::new(10, 10);
2223 let mut buf2 = Buffer::new(10, 10);
2224
2225 assert!(buf1.content_eq(&buf2));
2226
2227 buf1.set(0, 0, Cell::from_char('X'));
2228 assert!(!buf1.content_eq(&buf2));
2229
2230 buf2.set(0, 0, Cell::from_char('X'));
2231 assert!(buf1.content_eq(&buf2));
2232 }
2233
2234 #[test]
2235 fn buffer_bounds() {
2236 let buf = Buffer::new(80, 24);
2237 let bounds = buf.bounds();
2238 assert_eq!(bounds.x, 0);
2239 assert_eq!(bounds.y, 0);
2240 assert_eq!(bounds.width, 80);
2241 assert_eq!(bounds.height, 24);
2242 }
2243
2244 #[test]
2245 fn buffer_set_raw_bypasses_scissor() {
2246 let mut buf = Buffer::new(10, 10);
2247 buf.push_scissor(Rect::new(5, 5, 5, 5));
2248
2249 buf.set(0, 0, Cell::from_char('S'));
2251 assert!(buf.get(0, 0).unwrap().is_empty());
2252
2253 buf.set_raw(0, 0, Cell::from_char('R'));
2255 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('R'));
2256 }
2257
2258 #[test]
2259 fn set_handles_wide_chars() {
2260 let mut buf = Buffer::new(10, 10);
2261
2262 buf.set(0, 0, Cell::from_char('中'));
2264
2265 let head = buf.get(0, 0).unwrap();
2267 assert_eq!(head.content.as_char(), Some('中'));
2268
2269 let cont = buf.get(1, 0).unwrap();
2271 assert!(cont.is_continuation());
2272 assert!(!cont.is_empty());
2273 }
2274
2275 #[test]
2276 fn set_handles_wide_chars_clipped() {
2277 let mut buf = Buffer::new(10, 10);
2278 buf.push_scissor(Rect::new(0, 0, 1, 10)); buf.set(0, 0, Cell::from_char('中'));
2283
2284 assert!(buf.get(0, 0).unwrap().is_empty());
2286 assert!(buf.get(1, 0).unwrap().is_empty());
2288 }
2289
2290 #[test]
2293 fn overwrite_wide_head_with_single_clears_tails() {
2294 let mut buf = Buffer::new(10, 1);
2295
2296 buf.set(0, 0, Cell::from_char('中'));
2298 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2299 assert!(buf.get(1, 0).unwrap().is_continuation());
2300
2301 buf.set(0, 0, Cell::from_char('A'));
2303
2304 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('A'));
2306 assert!(
2308 buf.get(1, 0).unwrap().is_empty(),
2309 "Continuation at x=1 should be cleared when head is overwritten"
2310 );
2311 }
2312
2313 #[test]
2314 fn set_raw_overwrite_wide_head_with_single_clears_tails() {
2315 let mut buf = Buffer::new(10, 1);
2316
2317 buf.set(0, 0, Cell::from_char('中'));
2318 assert!(buf.get(1, 0).unwrap().is_continuation());
2319 buf.clear_dirty();
2320
2321 buf.set_raw(0, 0, Cell::from_char('A'));
2322
2323 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('A'));
2324 assert!(
2325 buf.get(1, 0).unwrap().is_empty(),
2326 "set_raw should clear stale continuation tails when overwriting a wide head"
2327 );
2328 let spans = buf.dirty_span_row(0).expect("dirty span row").spans();
2329 assert_eq!(spans, &[DirtySpan::new(0, 2)]);
2330 }
2331
2332 #[test]
2333 fn set_raw_wide_head_preserves_manual_tail_cells() {
2334 let mut buf = Buffer::new(10, 1);
2335
2336 buf.set_raw(0, 0, Cell::from_char('中'));
2337 buf.set_raw(1, 0, Cell::CONTINUATION);
2338 assert!(buf.get(1, 0).unwrap().is_continuation());
2339 buf.clear_dirty();
2340
2341 buf.set_raw(0, 0, Cell::from_char('日'));
2342
2343 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('日'));
2344 assert!(
2345 buf.get(1, 0).unwrap().is_continuation(),
2346 "set_raw wide-head replacement should not clear caller-managed tails"
2347 );
2348 let spans = buf.dirty_span_row(0).expect("dirty span row").spans();
2349 assert_eq!(spans, &[DirtySpan::new(0, 1)]);
2350 }
2351
2352 #[test]
2353 fn overwrite_continuation_with_single_clears_head_and_tails() {
2354 let mut buf = Buffer::new(10, 1);
2355
2356 buf.set(0, 0, Cell::from_char('中'));
2358 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2359 assert!(buf.get(1, 0).unwrap().is_continuation());
2360
2361 buf.set(1, 0, Cell::from_char('B'));
2363
2364 assert!(
2366 buf.get(0, 0).unwrap().is_empty(),
2367 "Head at x=0 should be cleared when its continuation is overwritten"
2368 );
2369 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('B'));
2371 }
2372
2373 #[test]
2374 fn overwrite_wide_with_another_wide() {
2375 let mut buf = Buffer::new(10, 1);
2376
2377 buf.set(0, 0, Cell::from_char('中'));
2379 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2380 assert!(buf.get(1, 0).unwrap().is_continuation());
2381
2382 buf.set(0, 0, Cell::from_char('日'));
2384
2385 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('日'));
2387 assert!(
2388 buf.get(1, 0).unwrap().is_continuation(),
2389 "Continuation should still exist for new wide char"
2390 );
2391 }
2392
2393 #[test]
2394 fn overwrite_continuation_middle_of_wide_sequence() {
2395 let mut buf = Buffer::new(10, 1);
2396
2397 buf.set(0, 0, Cell::from_char('中'));
2399 buf.set(2, 0, Cell::from_char('日'));
2400
2401 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2402 assert!(buf.get(1, 0).unwrap().is_continuation());
2403 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('日'));
2404 assert!(buf.get(3, 0).unwrap().is_continuation());
2405
2406 buf.set(1, 0, Cell::from_char('X'));
2408
2409 assert!(
2411 buf.get(0, 0).unwrap().is_empty(),
2412 "Head of first wide char should be cleared"
2413 );
2414 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('X'));
2416 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('日'));
2418 assert!(buf.get(3, 0).unwrap().is_continuation());
2419 }
2420
2421 #[test]
2422 fn wide_char_overlapping_previous_wide_char() {
2423 let mut buf = Buffer::new(10, 1);
2424
2425 buf.set(0, 0, Cell::from_char('中'));
2427 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2428 assert!(buf.get(1, 0).unwrap().is_continuation());
2429
2430 buf.set(1, 0, Cell::from_char('日'));
2432
2433 assert!(
2435 buf.get(0, 0).unwrap().is_empty(),
2436 "First wide char head should be cleared when continuation is overwritten by new wide"
2437 );
2438 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('日'));
2440 assert!(buf.get(2, 0).unwrap().is_continuation());
2441 }
2442
2443 #[test]
2444 fn wide_char_at_end_of_buffer_atomic_reject() {
2445 let mut buf = Buffer::new(5, 1);
2446
2447 buf.set(4, 0, Cell::from_char('中'));
2449
2450 assert!(
2452 buf.get(4, 0).unwrap().is_empty(),
2453 "Wide char should be rejected when tail would be out of bounds"
2454 );
2455 }
2456
2457 #[test]
2458 fn three_wide_chars_sequential_cleanup() {
2459 let mut buf = Buffer::new(10, 1);
2460
2461 buf.set(0, 0, Cell::from_char('一'));
2463 buf.set(2, 0, Cell::from_char('二'));
2464 buf.set(4, 0, Cell::from_char('三'));
2465
2466 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('一'));
2468 assert!(buf.get(1, 0).unwrap().is_continuation());
2469 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('二'));
2470 assert!(buf.get(3, 0).unwrap().is_continuation());
2471 assert_eq!(buf.get(4, 0).unwrap().content.as_char(), Some('三'));
2472 assert!(buf.get(5, 0).unwrap().is_continuation());
2473
2474 buf.set(3, 0, Cell::from_char('M'));
2476
2477 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('一'));
2479 assert!(buf.get(1, 0).unwrap().is_continuation());
2480 assert!(buf.get(2, 0).unwrap().is_empty());
2482 assert_eq!(buf.get(3, 0).unwrap().content.as_char(), Some('M'));
2484 assert_eq!(buf.get(4, 0).unwrap().content.as_char(), Some('三'));
2486 assert!(buf.get(5, 0).unwrap().is_continuation());
2487 }
2488
2489 #[test]
2490 fn overwrite_empty_cell_no_cleanup_needed() {
2491 let mut buf = Buffer::new(10, 1);
2492
2493 buf.set(5, 0, Cell::from_char('X'));
2495
2496 assert_eq!(buf.get(5, 0).unwrap().content.as_char(), Some('X'));
2497 assert!(buf.get(4, 0).unwrap().is_empty());
2499 assert!(buf.get(6, 0).unwrap().is_empty());
2500 }
2501
2502 #[test]
2503 fn wide_char_cleanup_with_opacity() {
2504 let mut buf = Buffer::new(10, 1);
2505
2506 buf.set(0, 0, Cell::default().with_bg(PackedRgba::rgb(255, 0, 0)));
2508 buf.set(1, 0, Cell::default().with_bg(PackedRgba::rgb(0, 255, 0)));
2509
2510 buf.set(0, 0, Cell::from_char('中'));
2512
2513 buf.push_opacity(0.5);
2515 buf.set(0, 0, Cell::from_char('A'));
2516 buf.pop_opacity();
2517
2518 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('A'));
2520 assert!(buf.get(1, 0).unwrap().is_empty());
2522 }
2523
2524 #[test]
2525 fn wide_char_continuation_not_treated_as_head() {
2526 let mut buf = Buffer::new(10, 1);
2527
2528 buf.set(0, 0, Cell::from_char('中'));
2530
2531 let cont = buf.get(1, 0).unwrap();
2533 assert!(cont.is_continuation());
2534 assert_eq!(cont.content.width(), 0);
2535
2536 buf.set(1, 0, Cell::from_char('日'));
2538
2539 assert!(buf.get(0, 0).unwrap().is_empty());
2541 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('日'));
2543 assert!(buf.get(2, 0).unwrap().is_continuation());
2544 }
2545
2546 #[test]
2547 fn wide_char_fill_region() {
2548 let mut buf = Buffer::new(10, 3);
2549
2550 let wide_cell = Cell::from_char('中');
2553 buf.fill(Rect::new(0, 0, 4, 2), wide_cell);
2554
2555 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('中'));
2557 assert!(buf.get(1, 0).unwrap().is_continuation());
2558 assert_eq!(buf.get(2, 0).unwrap().content.as_char(), Some('中'));
2559 assert!(buf.get(3, 0).unwrap().is_continuation());
2560 }
2561
2562 #[test]
2563 fn default_buffer_dimensions() {
2564 let buf = Buffer::default();
2565 assert_eq!(buf.width(), 1);
2566 assert_eq!(buf.height(), 1);
2567 assert_eq!(buf.len(), 1);
2568 }
2569
2570 #[test]
2571 fn buffer_partial_eq_impl() {
2572 let buf1 = Buffer::new(5, 5);
2573 let buf2 = Buffer::new(5, 5);
2574 let mut buf3 = Buffer::new(5, 5);
2575 buf3.set(0, 0, Cell::from_char('X'));
2576
2577 assert_eq!(buf1, buf2);
2578 assert_ne!(buf1, buf3);
2579 }
2580
2581 #[test]
2582 fn degradation_level_accessible() {
2583 let mut buf = Buffer::new(10, 10);
2584 assert_eq!(buf.degradation, DegradationLevel::Full);
2585
2586 buf.degradation = DegradationLevel::SimpleBorders;
2587 assert_eq!(buf.degradation, DegradationLevel::SimpleBorders);
2588 }
2589
2590 #[test]
2593 fn get_mut_modifies_cell() {
2594 let mut buf = Buffer::new(10, 10);
2595 buf.set(3, 3, Cell::from_char('A'));
2596
2597 if let Some(cell) = buf.get_mut(3, 3) {
2598 *cell = Cell::from_char('B');
2599 }
2600
2601 assert_eq!(buf.get(3, 3).unwrap().content.as_char(), Some('B'));
2602 }
2603
2604 #[test]
2605 fn get_mut_out_of_bounds() {
2606 let mut buf = Buffer::new(5, 5);
2607 assert!(buf.get_mut(10, 10).is_none());
2608 }
2609
2610 #[test]
2613 fn clear_with_fills_all_cells() {
2614 let mut buf = Buffer::new(5, 3);
2615 let fill_cell = Cell::from_char('*');
2616 buf.clear_with(fill_cell);
2617
2618 for y in 0..3 {
2619 for x in 0..5 {
2620 assert_eq!(buf.get(x, y).unwrap().content.as_char(), Some('*'));
2621 }
2622 }
2623 }
2624
2625 #[test]
2626 fn clear_with_wide_cell_preserves_head_tail_invariant() {
2627 let mut buf = Buffer::new(5, 2);
2628 buf.clear_with(Cell::from_char('中'));
2629
2630 for y in 0..2 {
2631 assert_eq!(buf.get(0, y).unwrap().content.as_char(), Some('中'));
2632 assert!(buf.get(1, y).unwrap().is_continuation());
2633 assert_eq!(buf.get(2, y).unwrap().content.as_char(), Some('中'));
2634 assert!(buf.get(3, y).unwrap().is_continuation());
2635 assert!(buf.get(4, y).unwrap().is_empty());
2636 }
2637 }
2638
2639 #[test]
2642 fn cells_slice_has_correct_length() {
2643 let buf = Buffer::new(10, 5);
2644 assert_eq!(buf.cells().len(), 50);
2645 }
2646
2647 #[test]
2648 fn cells_mut_allows_direct_modification() {
2649 let mut buf = Buffer::new(3, 2);
2650 let cells = buf.cells_mut();
2651 cells[0] = Cell::from_char('Z');
2652
2653 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('Z'));
2654 }
2655
2656 #[test]
2659 fn row_cells_returns_correct_row() {
2660 let mut buf = Buffer::new(5, 3);
2661 buf.set(2, 1, Cell::from_char('R'));
2662
2663 let row = buf.row_cells(1);
2664 assert_eq!(row.len(), 5);
2665 assert_eq!(row[2].content.as_char(), Some('R'));
2666 }
2667
2668 #[test]
2669 fn row_cells_mut_span_marks_once_and_returns_slice() {
2670 let mut buf = Buffer::new(5, 3);
2671 buf.clear_dirty();
2672
2673 let row = buf
2674 .row_cells_mut_span(1, 1, 4)
2675 .expect("row span should be in bounds");
2676 assert_eq!(row.len(), 3);
2677 row[0] = Cell::from_char('A');
2678 row[1] = Cell::from_char('B');
2679 row[2] = Cell::from_char('C');
2680
2681 assert!(buf.is_row_dirty(1));
2682 let spans = buf.dirty_span_row(1).expect("dirty span row").spans();
2683 assert_eq!(spans, &[DirtySpan::new(1, 4)]);
2684 assert_eq!(buf.get(1, 1).unwrap().content.as_char(), Some('A'));
2685 assert_eq!(buf.get(2, 1).unwrap().content.as_char(), Some('B'));
2686 assert_eq!(buf.get(3, 1).unwrap().content.as_char(), Some('C'));
2687 }
2688
2689 #[test]
2690 fn row_cells_mut_span_clamps_to_buffer_width() {
2691 let mut buf = Buffer::new(5, 1);
2692 buf.clear_dirty();
2693
2694 let row = buf
2695 .row_cells_mut_span(0, 3, 99)
2696 .expect("row span should clamp");
2697 assert_eq!(row.len(), 2);
2698 row[0] = Cell::from_char('X');
2699 row[1] = Cell::from_char('Y');
2700
2701 let spans = buf.dirty_span_row(0).expect("dirty span row").spans();
2702 assert_eq!(spans, &[DirtySpan::new(3, 5)]);
2703 assert_eq!(buf.get(3, 0).unwrap().content.as_char(), Some('X'));
2704 assert_eq!(buf.get(4, 0).unwrap().content.as_char(), Some('Y'));
2705 }
2706
2707 #[test]
2708 fn row_cells_mut_span_rejects_reversed_ranges() {
2709 let mut buf = Buffer::new(5, 1);
2710 buf.clear_dirty();
2711
2712 assert!(buf.row_cells_mut_span(0, 4, 2).is_none());
2713 assert!(
2714 !buf.is_row_dirty(0),
2715 "reversed ranges should not mark rows dirty"
2716 );
2717 assert!(
2718 buf.dirty_span_row(0)
2719 .expect("dirty span row")
2720 .spans()
2721 .is_empty(),
2722 "reversed ranges should not add dirty spans"
2723 );
2724 }
2725
2726 #[test]
2727 #[should_panic]
2728 fn row_cells_out_of_bounds_panics() {
2729 let buf = Buffer::new(5, 3);
2730 let _ = buf.row_cells(5);
2731 }
2732
2733 #[test]
2736 fn buffer_is_not_empty() {
2737 let buf = Buffer::new(1, 1);
2738 assert!(!buf.is_empty());
2739 }
2740
2741 #[test]
2744 fn set_raw_out_of_bounds_is_safe() {
2745 let mut buf = Buffer::new(5, 5);
2746 buf.set_raw(100, 100, Cell::from_char('X'));
2747 }
2749
2750 #[test]
2753 fn copy_from_out_of_bounds_partial() {
2754 let mut src = Buffer::new(5, 5);
2755 src.set(0, 0, Cell::from_char('A'));
2756 src.set(4, 4, Cell::from_char('B'));
2757
2758 let mut dst = Buffer::new(5, 5);
2759 dst.copy_from(&src, Rect::new(0, 0, 5, 5), 3, 3);
2761
2762 assert_eq!(dst.get(3, 3).unwrap().content.as_char(), Some('A'));
2764 assert!(dst.get(4, 4).unwrap().is_empty());
2766 }
2767
2768 #[test]
2771 fn content_eq_different_dimensions() {
2772 let buf1 = Buffer::new(5, 5);
2773 let buf2 = Buffer::new(10, 10);
2774 assert!(!buf1.content_eq(&buf2));
2776 }
2777
2778 mod property {
2781 use super::*;
2782 use proptest::prelude::*;
2783
2784 proptest! {
2785 #[test]
2786 fn buffer_dimensions_are_preserved(width in 1u16..200, height in 1u16..200) {
2787 let buf = Buffer::new(width, height);
2788 prop_assert_eq!(buf.width(), width);
2789 prop_assert_eq!(buf.height(), height);
2790 prop_assert_eq!(buf.len(), width as usize * height as usize);
2791 }
2792
2793 #[test]
2794 fn buffer_get_in_bounds_always_succeeds(width in 1u16..100, height in 1u16..100) {
2795 let buf = Buffer::new(width, height);
2796 for x in 0..width {
2797 for y in 0..height {
2798 prop_assert!(buf.get(x, y).is_some(), "get({x},{y}) failed for {width}x{height} buffer");
2799 }
2800 }
2801 }
2802
2803 #[test]
2804 fn buffer_get_out_of_bounds_returns_none(width in 1u16..50, height in 1u16..50) {
2805 let buf = Buffer::new(width, height);
2806 prop_assert!(buf.get(width, 0).is_none());
2807 prop_assert!(buf.get(0, height).is_none());
2808 prop_assert!(buf.get(width, height).is_none());
2809 }
2810
2811 #[test]
2812 fn buffer_set_get_roundtrip(
2813 width in 5u16..50,
2814 height in 5u16..50,
2815 x in 0u16..5,
2816 y in 0u16..5,
2817 ch_idx in 0u32..26,
2818 ) {
2819 let x = x % width;
2820 let y = y % height;
2821 let ch = char::from_u32('A' as u32 + ch_idx).unwrap();
2822 let mut buf = Buffer::new(width, height);
2823 buf.set(x, y, Cell::from_char(ch));
2824 let got = buf.get(x, y).unwrap();
2825 prop_assert_eq!(got.content.as_char(), Some(ch));
2826 }
2827
2828 #[test]
2829 fn scissor_push_pop_stack_depth(
2830 width in 10u16..50,
2831 height in 10u16..50,
2832 push_count in 1usize..10,
2833 ) {
2834 let mut buf = Buffer::new(width, height);
2835 prop_assert_eq!(buf.scissor_depth(), 1); for i in 0..push_count {
2838 buf.push_scissor(Rect::new(0, 0, width, height));
2839 prop_assert_eq!(buf.scissor_depth(), i + 2);
2840 }
2841
2842 for i in (0..push_count).rev() {
2843 buf.pop_scissor();
2844 prop_assert_eq!(buf.scissor_depth(), i + 1);
2845 }
2846
2847 buf.pop_scissor();
2849 prop_assert_eq!(buf.scissor_depth(), 1);
2850 }
2851
2852 #[test]
2853 fn scissor_monotonic_intersection(
2854 width in 20u16..60,
2855 height in 20u16..60,
2856 ) {
2857 let mut buf = Buffer::new(width, height);
2859 let outer = Rect::new(2, 2, width - 4, height - 4);
2860 buf.push_scissor(outer);
2861 let s1 = buf.current_scissor();
2862
2863 let inner = Rect::new(5, 5, 10, 10);
2864 buf.push_scissor(inner);
2865 let s2 = buf.current_scissor();
2866
2867 prop_assert!(s2.width <= s1.width, "inner width {} > outer width {}", s2.width, s1.width);
2869 prop_assert!(s2.height <= s1.height, "inner height {} > outer height {}", s2.height, s1.height);
2870 }
2871
2872 #[test]
2873 fn opacity_push_pop_stack_depth(
2874 width in 5u16..20,
2875 height in 5u16..20,
2876 push_count in 1usize..10,
2877 ) {
2878 let mut buf = Buffer::new(width, height);
2879 prop_assert_eq!(buf.opacity_depth(), 1);
2880
2881 for i in 0..push_count {
2882 buf.push_opacity(0.9);
2883 prop_assert_eq!(buf.opacity_depth(), i + 2);
2884 }
2885
2886 for i in (0..push_count).rev() {
2887 buf.pop_opacity();
2888 prop_assert_eq!(buf.opacity_depth(), i + 1);
2889 }
2890
2891 buf.pop_opacity();
2892 prop_assert_eq!(buf.opacity_depth(), 1);
2893 }
2894
2895 #[test]
2896 fn opacity_multiplication_is_monotonic(
2897 opacity1 in 0.0f32..=1.0,
2898 opacity2 in 0.0f32..=1.0,
2899 ) {
2900 let mut buf = Buffer::new(5, 5);
2901 buf.push_opacity(opacity1);
2902 let after_first = buf.current_opacity();
2903 buf.push_opacity(opacity2);
2904 let after_second = buf.current_opacity();
2905
2906 prop_assert!(after_second <= after_first + f32::EPSILON,
2908 "opacity increased: {} -> {}", after_first, after_second);
2909 }
2910
2911 #[test]
2912 fn clear_resets_all_cells(width in 1u16..30, height in 1u16..30) {
2913 let mut buf = Buffer::new(width, height);
2914 for x in 0..width {
2916 buf.set_raw(x, 0, Cell::from_char('X'));
2917 }
2918 buf.clear();
2919 for y in 0..height {
2921 for x in 0..width {
2922 prop_assert!(buf.get(x, y).unwrap().is_empty(),
2923 "cell ({x},{y}) not empty after clear");
2924 }
2925 }
2926 }
2927
2928 #[test]
2929 fn content_eq_is_reflexive(width in 1u16..30, height in 1u16..30) {
2930 let buf = Buffer::new(width, height);
2931 prop_assert!(buf.content_eq(&buf));
2932 }
2933
2934 #[test]
2935 fn content_eq_detects_single_change(
2936 width in 5u16..30,
2937 height in 5u16..30,
2938 x in 0u16..5,
2939 y in 0u16..5,
2940 ) {
2941 let x = x % width;
2942 let y = y % height;
2943 let buf1 = Buffer::new(width, height);
2944 let mut buf2 = Buffer::new(width, height);
2945 buf2.set_raw(x, y, Cell::from_char('Z'));
2946 prop_assert!(!buf1.content_eq(&buf2));
2947 }
2948
2949 #[test]
2952 fn dimensions_immutable_through_operations(
2953 width in 5u16..30,
2954 height in 5u16..30,
2955 ) {
2956 let mut buf = Buffer::new(width, height);
2957
2958 buf.set(0, 0, Cell::from_char('A'));
2960 prop_assert_eq!(buf.width(), width);
2961 prop_assert_eq!(buf.height(), height);
2962 prop_assert_eq!(buf.len(), width as usize * height as usize);
2963
2964 buf.push_scissor(Rect::new(1, 1, 3, 3));
2965 prop_assert_eq!(buf.width(), width);
2966 prop_assert_eq!(buf.height(), height);
2967
2968 buf.push_opacity(0.5);
2969 prop_assert_eq!(buf.width(), width);
2970 prop_assert_eq!(buf.height(), height);
2971
2972 buf.pop_scissor();
2973 buf.pop_opacity();
2974 prop_assert_eq!(buf.width(), width);
2975 prop_assert_eq!(buf.height(), height);
2976
2977 buf.clear();
2978 prop_assert_eq!(buf.width(), width);
2979 prop_assert_eq!(buf.height(), height);
2980 prop_assert_eq!(buf.len(), width as usize * height as usize);
2981 }
2982
2983 #[test]
2984 fn scissor_area_never_increases_random_rects(
2985 width in 20u16..60,
2986 height in 20u16..60,
2987 rects in proptest::collection::vec(
2988 (0u16..20, 0u16..20, 1u16..15, 1u16..15),
2989 1..8
2990 ),
2991 ) {
2992 let mut buf = Buffer::new(width, height);
2993 let mut prev_area = (width as u32) * (height as u32);
2994
2995 for (x, y, w, h) in rects {
2996 buf.push_scissor(Rect::new(x, y, w, h));
2997 let s = buf.current_scissor();
2998 let area = (s.width as u32) * (s.height as u32);
2999 prop_assert!(area <= prev_area,
3000 "scissor area increased: {} -> {} after push({},{},{},{})",
3001 prev_area, area, x, y, w, h);
3002 prev_area = area;
3003 }
3004 }
3005
3006 #[test]
3007 fn opacity_range_invariant_random_sequence(
3008 opacities in proptest::collection::vec(0.0f32..=1.0, 1..15),
3009 ) {
3010 let mut buf = Buffer::new(5, 5);
3011
3012 for &op in &opacities {
3013 buf.push_opacity(op);
3014 let current = buf.current_opacity();
3015 prop_assert!(current >= 0.0, "opacity below 0: {}", current);
3016 prop_assert!(current <= 1.0 + f32::EPSILON,
3017 "opacity above 1: {}", current);
3018 }
3019
3020 for _ in &opacities {
3022 buf.pop_opacity();
3023 }
3024 prop_assert!((buf.current_opacity() - 1.0).abs() < f32::EPSILON);
3026 }
3027
3028 #[test]
3029 fn opacity_clamp_out_of_range(
3030 neg in -100.0f32..0.0,
3031 over in 1.01f32..100.0,
3032 ) {
3033 let mut buf = Buffer::new(5, 5);
3034
3035 buf.push_opacity(neg);
3036 prop_assert!(buf.current_opacity() >= 0.0,
3037 "negative opacity not clamped: {}", buf.current_opacity());
3038 buf.pop_opacity();
3039
3040 buf.push_opacity(over);
3041 prop_assert!(buf.current_opacity() <= 1.0 + f32::EPSILON,
3042 "over-1 opacity not clamped: {}", buf.current_opacity());
3043 }
3044
3045 #[test]
3046 fn scissor_stack_always_has_base(
3047 pushes in 0usize..10,
3048 pops in 0usize..15,
3049 ) {
3050 let mut buf = Buffer::new(10, 10);
3051
3052 for _ in 0..pushes {
3053 buf.push_scissor(Rect::new(0, 0, 5, 5));
3054 }
3055 for _ in 0..pops {
3056 buf.pop_scissor();
3057 }
3058
3059 prop_assert!(buf.scissor_depth() >= 1,
3061 "scissor depth dropped below 1 after {} pushes, {} pops",
3062 pushes, pops);
3063 }
3064
3065 #[test]
3066 fn opacity_stack_always_has_base(
3067 pushes in 0usize..10,
3068 pops in 0usize..15,
3069 ) {
3070 let mut buf = Buffer::new(10, 10);
3071
3072 for _ in 0..pushes {
3073 buf.push_opacity(0.5);
3074 }
3075 for _ in 0..pops {
3076 buf.pop_opacity();
3077 }
3078
3079 prop_assert!(buf.opacity_depth() >= 1,
3081 "opacity depth dropped below 1 after {} pushes, {} pops",
3082 pushes, pops);
3083 }
3084
3085 #[test]
3086 fn cells_len_invariant_always_holds(
3087 width in 1u16..50,
3088 height in 1u16..50,
3089 ) {
3090 let mut buf = Buffer::new(width, height);
3091 let expected = width as usize * height as usize;
3092
3093 prop_assert_eq!(buf.cells().len(), expected);
3094
3095 buf.set(0, 0, Cell::from_char('X'));
3097 prop_assert_eq!(buf.cells().len(), expected);
3098
3099 buf.clear();
3100 prop_assert_eq!(buf.cells().len(), expected);
3101 }
3102
3103 #[test]
3104 fn set_outside_scissor_is_noop(
3105 width in 10u16..30,
3106 height in 10u16..30,
3107 ) {
3108 let mut buf = Buffer::new(width, height);
3109 buf.push_scissor(Rect::new(2, 2, 3, 3));
3110
3111 buf.set(0, 0, Cell::from_char('X'));
3113 let cell = buf.get(0, 0).unwrap();
3115 prop_assert!(cell.is_empty(),
3116 "cell (0,0) modified outside scissor region");
3117
3118 buf.set(3, 3, Cell::from_char('Y'));
3120 let cell = buf.get(3, 3).unwrap();
3121 prop_assert_eq!(cell.content.as_char(), Some('Y'));
3122 }
3123
3124 #[test]
3127 fn wide_char_overwrites_cleanup_tails(
3128 width in 10u16..30,
3129 x in 0u16..8,
3130 ) {
3131 let x = x % (width.saturating_sub(2).max(1));
3132 let mut buf = Buffer::new(width, 1);
3133
3134 buf.set(x, 0, Cell::from_char('中'));
3136
3137 if x + 1 < width {
3139 let head = buf.get(x, 0).unwrap();
3140 let tail = buf.get(x + 1, 0).unwrap();
3141
3142 if head.content.as_char() == Some('中') {
3143 prop_assert!(tail.is_continuation(),
3144 "tail at x+1={} should be continuation", x + 1);
3145
3146 buf.set(x, 0, Cell::from_char('A'));
3148 let new_head = buf.get(x, 0).unwrap();
3149 let cleared_tail = buf.get(x + 1, 0).unwrap();
3150
3151 prop_assert_eq!(new_head.content.as_char(), Some('A'));
3152 prop_assert!(cleared_tail.is_empty(),
3153 "tail should be cleared after head overwrite");
3154 }
3155 }
3156 }
3157
3158 #[test]
3159 fn wide_char_atomic_rejection_at_boundary(
3160 width in 3u16..20,
3161 ) {
3162 let mut buf = Buffer::new(width, 1);
3163
3164 let last_pos = width - 1;
3166 buf.set(last_pos, 0, Cell::from_char('中'));
3167
3168 let cell = buf.get(last_pos, 0).unwrap();
3170 prop_assert!(cell.is_empty(),
3171 "wide char at boundary position {} (width {}) should be rejected",
3172 last_pos, width);
3173 }
3174
3175 #[test]
3180 fn double_buffer_swap_is_involution(ops in proptest::collection::vec(proptest::bool::ANY, 0..100)) {
3181 let mut db = DoubleBuffer::new(10, 10);
3182 let initial_idx = db.current_idx;
3183
3184 for do_swap in &ops {
3185 if *do_swap {
3186 db.swap();
3187 }
3188 }
3189
3190 let swap_count = ops.iter().filter(|&&x| x).count();
3191 let expected_idx = if swap_count % 2 == 0 { initial_idx } else { 1 - initial_idx };
3192
3193 prop_assert_eq!(db.current_idx, expected_idx,
3194 "After {} swaps, index should be {} but was {}",
3195 swap_count, expected_idx, db.current_idx);
3196 }
3197
3198 #[test]
3199 fn double_buffer_resize_preserves_invariant(
3200 init_w in 1u16..200,
3201 init_h in 1u16..100,
3202 new_w in 1u16..200,
3203 new_h in 1u16..100,
3204 ) {
3205 let mut db = DoubleBuffer::new(init_w, init_h);
3206 db.resize(new_w, new_h);
3207
3208 prop_assert_eq!(db.width(), new_w);
3209 prop_assert_eq!(db.height(), new_h);
3210 prop_assert!(db.dimensions_match(new_w, new_h));
3211 }
3212
3213 #[test]
3214 fn double_buffer_current_previous_disjoint(
3215 width in 1u16..50,
3216 height in 1u16..50,
3217 ) {
3218 let mut db = DoubleBuffer::new(width, height);
3219
3220 db.current_mut().set(0, 0, Cell::from_char('C'));
3222
3223 prop_assert!(db.previous().get(0, 0).unwrap().is_empty(),
3225 "Previous buffer should not reflect changes to current");
3226
3227 db.swap();
3229 prop_assert_eq!(db.previous().get(0, 0).unwrap().content.as_char(), Some('C'),
3230 "After swap, previous should have the 'C' we wrote");
3231 }
3232
3233 #[test]
3234 fn double_buffer_swap_content_semantics(
3235 width in 5u16..30,
3236 height in 5u16..30,
3237 ) {
3238 let mut db = DoubleBuffer::new(width, height);
3239
3240 db.current_mut().set(0, 0, Cell::from_char('X'));
3242 db.swap();
3243
3244 db.current_mut().set(0, 0, Cell::from_char('Y'));
3246 db.swap();
3247
3248 prop_assert_eq!(db.current().get(0, 0).unwrap().content.as_char(), Some('X'));
3250 prop_assert_eq!(db.previous().get(0, 0).unwrap().content.as_char(), Some('Y'));
3251 }
3252
3253 #[test]
3254 fn double_buffer_resize_clears_both(
3255 w1 in 5u16..30,
3256 h1 in 5u16..30,
3257 w2 in 5u16..30,
3258 h2 in 5u16..30,
3259 ) {
3260 prop_assume!(w1 != w2 || h1 != h2);
3262
3263 let mut db = DoubleBuffer::new(w1, h1);
3264
3265 db.current_mut().set(0, 0, Cell::from_char('A'));
3267 db.swap();
3268 db.current_mut().set(0, 0, Cell::from_char('B'));
3269
3270 db.resize(w2, h2);
3272
3273 prop_assert!(db.current().get(0, 0).unwrap().is_empty(),
3275 "Current buffer should be empty after resize");
3276 prop_assert!(db.previous().get(0, 0).unwrap().is_empty(),
3277 "Previous buffer should be empty after resize");
3278 }
3279 }
3280 }
3281
3282 #[test]
3285 fn dirty_rows_start_dirty() {
3286 let buf = Buffer::new(10, 5);
3288 assert_eq!(buf.dirty_row_count(), 5);
3289 for y in 0..5 {
3290 assert!(buf.is_row_dirty(y));
3291 }
3292 }
3293
3294 #[test]
3295 fn dirty_bitmap_starts_full() {
3296 let buf = Buffer::new(4, 3);
3297 assert!(buf.dirty_all());
3298 assert_eq!(buf.dirty_cell_count(), 12);
3299 }
3300
3301 #[test]
3302 fn dirty_bitmap_tracks_single_cell() {
3303 let mut buf = Buffer::new(4, 3);
3304 buf.clear_dirty();
3305 assert!(!buf.dirty_all());
3306 buf.set_raw(1, 1, Cell::from_char('X'));
3307 let idx = 1 + 4;
3308 assert_eq!(buf.dirty_cell_count(), 1);
3309 assert_eq!(buf.dirty_bits()[idx], 1);
3310 }
3311
3312 #[test]
3313 fn dirty_bitmap_dedupes_cells() {
3314 let mut buf = Buffer::new(4, 3);
3315 buf.clear_dirty();
3316 buf.set_raw(2, 2, Cell::from_char('A'));
3317 buf.set_raw(2, 2, Cell::from_char('B'));
3318 assert_eq!(buf.dirty_cell_count(), 1);
3319 }
3320
3321 #[test]
3322 fn set_marks_row_dirty() {
3323 let mut buf = Buffer::new(10, 5);
3324 buf.clear_dirty(); buf.set(3, 2, Cell::from_char('X'));
3326 assert!(buf.is_row_dirty(2));
3327 assert!(!buf.is_row_dirty(0));
3328 assert!(!buf.is_row_dirty(1));
3329 assert!(!buf.is_row_dirty(3));
3330 assert!(!buf.is_row_dirty(4));
3331 }
3332
3333 #[test]
3334 fn set_raw_marks_row_dirty() {
3335 let mut buf = Buffer::new(10, 5);
3336 buf.clear_dirty(); buf.set_raw(0, 4, Cell::from_char('Z'));
3338 assert!(buf.is_row_dirty(4));
3339 assert_eq!(buf.dirty_row_count(), 1);
3340 }
3341
3342 #[test]
3343 fn clear_marks_all_dirty() {
3344 let mut buf = Buffer::new(10, 5);
3345 buf.clear();
3346 assert_eq!(buf.dirty_row_count(), 5);
3347 }
3348
3349 #[test]
3350 fn clear_dirty_resets_flags() {
3351 let mut buf = Buffer::new(10, 5);
3352 assert_eq!(buf.dirty_row_count(), 5);
3354 buf.clear_dirty();
3355 assert_eq!(buf.dirty_row_count(), 0);
3356
3357 buf.set(0, 0, Cell::from_char('A'));
3359 buf.set(0, 3, Cell::from_char('B'));
3360 assert_eq!(buf.dirty_row_count(), 2);
3361
3362 buf.clear_dirty();
3363 assert_eq!(buf.dirty_row_count(), 0);
3364 }
3365
3366 #[test]
3367 fn clear_dirty_resets_bitmap() {
3368 let mut buf = Buffer::new(4, 3);
3369 buf.clear();
3370 assert!(buf.dirty_all());
3371 buf.clear_dirty();
3372 assert!(!buf.dirty_all());
3373 assert_eq!(buf.dirty_cell_count(), 0);
3374 assert!(buf.dirty_bits().iter().all(|&b| b == 0));
3375 }
3376
3377 #[test]
3378 fn fill_with_wide_cells_leaves_no_stale_content() {
3379 let mut buf = Buffer::new(5, 1);
3380 for x in 0..5 {
3381 buf.set(x, 0, Cell::from_char('X'));
3382 }
3383 let wide = Cell::from_char('世');
3387 assert_eq!(wide.content.width(), 2);
3388 buf.fill(Rect::new(0, 0, 5, 1), wide);
3389 let trailing = *buf.get(4, 0).expect("in bounds");
3390 assert_ne!(
3391 trailing,
3392 Cell::from_char('X'),
3393 "stale content survived fill"
3394 );
3395 assert_eq!(trailing, Cell::default());
3396 assert_eq!(*buf.get(0, 0).unwrap(), wide);
3398 assert_eq!(*buf.get(2, 0).unwrap(), wide);
3399 }
3400
3401 #[test]
3402 fn fill_marks_affected_rows_dirty() {
3403 let mut buf = Buffer::new(10, 10);
3404 buf.clear_dirty(); buf.fill(Rect::new(0, 2, 5, 3), Cell::from_char('.'));
3406 assert!(!buf.is_row_dirty(0));
3408 assert!(!buf.is_row_dirty(1));
3409 assert!(buf.is_row_dirty(2));
3410 assert!(buf.is_row_dirty(3));
3411 assert!(buf.is_row_dirty(4));
3412 assert!(!buf.is_row_dirty(5));
3413 }
3414
3415 #[test]
3416 fn get_mut_marks_row_dirty() {
3417 let mut buf = Buffer::new(10, 5);
3418 buf.clear_dirty(); if let Some(cell) = buf.get_mut(5, 3) {
3420 cell.fg = PackedRgba::rgb(255, 0, 0);
3421 }
3422 assert!(buf.is_row_dirty(3));
3423 assert_eq!(buf.dirty_row_count(), 1);
3424 }
3425
3426 #[test]
3427 fn cells_mut_marks_all_dirty() {
3428 let mut buf = Buffer::new(10, 5);
3429 let _ = buf.cells_mut();
3430 assert_eq!(buf.dirty_row_count(), 5);
3431 }
3432
3433 #[test]
3434 fn dirty_rows_slice_length_matches_height() {
3435 let buf = Buffer::new(10, 7);
3436 assert_eq!(buf.dirty_rows().len(), 7);
3437 }
3438
3439 #[test]
3440 fn out_of_bounds_set_does_not_dirty() {
3441 let mut buf = Buffer::new(10, 5);
3442 buf.clear_dirty(); buf.set(100, 100, Cell::from_char('X'));
3444 assert_eq!(buf.dirty_row_count(), 0);
3445 }
3446
3447 #[test]
3448 fn property_dirty_soundness() {
3449 let mut buf = Buffer::new(20, 10);
3451 let positions = [(3, 0), (5, 2), (0, 9), (19, 5), (10, 7)];
3452 for &(x, y) in &positions {
3453 buf.set(x, y, Cell::from_char('*'));
3454 }
3455 for &(_, y) in &positions {
3456 assert!(
3457 buf.is_row_dirty(y),
3458 "Row {} should be dirty after set({}, {})",
3459 y,
3460 positions.iter().find(|(_, ry)| *ry == y).unwrap().0,
3461 y
3462 );
3463 }
3464 }
3465
3466 #[test]
3467 fn dirty_clear_between_frames() {
3468 let mut buf = Buffer::new(10, 5);
3470
3471 assert_eq!(buf.dirty_row_count(), 5);
3473
3474 buf.clear_dirty();
3476 assert_eq!(buf.dirty_row_count(), 0);
3477
3478 buf.set(0, 0, Cell::from_char('A'));
3480 buf.set(0, 2, Cell::from_char('B'));
3481 assert_eq!(buf.dirty_row_count(), 2);
3482
3483 buf.clear_dirty();
3485 assert_eq!(buf.dirty_row_count(), 0);
3486
3487 buf.set(0, 4, Cell::from_char('C'));
3489 assert_eq!(buf.dirty_row_count(), 1);
3490 assert!(buf.is_row_dirty(4));
3491 assert!(!buf.is_row_dirty(0));
3492 }
3493
3494 #[test]
3497 fn dirty_spans_start_full_dirty() {
3498 let buf = Buffer::new(10, 5);
3499 for y in 0..5 {
3500 let row = buf.dirty_span_row(y).unwrap();
3501 assert!(row.is_full(), "row {y} should start full-dirty");
3502 assert!(row.spans().is_empty(), "row {y} spans should start empty");
3503 }
3504 }
3505
3506 #[test]
3507 fn clear_dirty_resets_spans() {
3508 let mut buf = Buffer::new(10, 5);
3509 buf.clear_dirty();
3510 for y in 0..5 {
3511 let row = buf.dirty_span_row(y).unwrap();
3512 assert!(!row.is_full(), "row {y} should clear full-dirty");
3513 assert!(row.spans().is_empty(), "row {y} spans should be cleared");
3514 }
3515 assert_eq!(buf.dirty_span_overflows, 0);
3516 }
3517
3518 #[test]
3519 fn set_records_dirty_span() {
3520 let mut buf = Buffer::new(20, 2);
3521 buf.clear_dirty();
3522 buf.set(2, 0, Cell::from_char('A'));
3523 let row = buf.dirty_span_row(0).unwrap();
3524 assert_eq!(row.spans(), &[DirtySpan::new(2, 3)]);
3525 assert!(!row.is_full());
3526 }
3527
3528 #[test]
3529 fn set_merges_adjacent_spans() {
3530 let mut buf = Buffer::new(20, 2);
3531 buf.clear_dirty();
3532 buf.set(2, 0, Cell::from_char('A'));
3533 buf.set(3, 0, Cell::from_char('B')); let row = buf.dirty_span_row(0).unwrap();
3535 assert_eq!(row.spans(), &[DirtySpan::new(2, 4)]);
3536 }
3537
3538 #[test]
3539 fn set_merges_close_spans() {
3540 let mut buf = Buffer::new(20, 2);
3541 buf.clear_dirty();
3542 buf.set(2, 0, Cell::from_char('A'));
3543 buf.set(4, 0, Cell::from_char('B')); let row = buf.dirty_span_row(0).unwrap();
3545 assert_eq!(row.spans(), &[DirtySpan::new(2, 5)]);
3546 }
3547
3548 #[test]
3549 fn span_overflow_sets_full_row() {
3550 let width = (DIRTY_SPAN_MAX_SPANS_PER_ROW as u16 + 2) * 3;
3551 let mut buf = Buffer::new(width, 1);
3552 buf.clear_dirty();
3553 for i in 0..(DIRTY_SPAN_MAX_SPANS_PER_ROW + 1) {
3554 let x = (i as u16) * 3;
3555 buf.set(x, 0, Cell::from_char('x'));
3556 }
3557 let row = buf.dirty_span_row(0).unwrap();
3558 assert!(row.is_full());
3559 assert!(row.spans().is_empty());
3560 assert_eq!(buf.dirty_span_overflows, 1);
3561 }
3562
3563 #[test]
3564 fn fill_full_row_marks_full_span() {
3565 let mut buf = Buffer::new(10, 3);
3566 buf.clear_dirty();
3567 let cell = Cell::from_char('x').with_bg(PackedRgba::rgb(0, 0, 0));
3568 buf.fill(Rect::new(0, 1, 10, 1), cell);
3569 let row = buf.dirty_span_row(1).unwrap();
3570 assert!(row.is_full());
3571 assert!(row.spans().is_empty());
3572 }
3573
3574 #[test]
3575 fn get_mut_records_dirty_span() {
3576 let mut buf = Buffer::new(10, 5);
3577 buf.clear_dirty();
3578 let _ = buf.get_mut(5, 3);
3579 let row = buf.dirty_span_row(3).unwrap();
3580 assert_eq!(row.spans(), &[DirtySpan::new(5, 6)]);
3581 }
3582
3583 #[test]
3584 fn cells_mut_marks_all_full_spans() {
3585 let mut buf = Buffer::new(10, 5);
3586 buf.clear_dirty();
3587 let _ = buf.cells_mut();
3588 for y in 0..5 {
3589 let row = buf.dirty_span_row(y).unwrap();
3590 assert!(row.is_full(), "row {y} should be full after cells_mut");
3591 }
3592 }
3593
3594 #[test]
3595 fn dirty_span_config_disabled_skips_rows() {
3596 let mut buf = Buffer::new(10, 1);
3597 buf.clear_dirty();
3598 buf.set_dirty_span_config(DirtySpanConfig::default().with_enabled(false));
3599 buf.set(5, 0, Cell::from_char('x'));
3600 assert!(buf.dirty_span_row(0).is_none());
3601 let stats = buf.dirty_span_stats();
3602 assert_eq!(stats.total_spans, 0);
3603 assert_eq!(stats.span_coverage_cells, 0);
3604 }
3605
3606 #[test]
3607 fn set_dirty_span_config_keeps_dirty_rows_full() {
3608 let mut buf = Buffer::new(10, 2);
3615 buf.clear_dirty();
3616 buf.set(7, 0, Cell::from_char('B')); buf.set_dirty_span_config(DirtySpanConfig::default().with_merge_gap(3));
3619
3620 let row = buf.dirty_span_row(0).expect("row 0 must have span state");
3622 assert!(
3623 row.is_full(),
3624 "dirty row must go full-row on config change, got spans {:?}",
3625 row.spans()
3626 );
3627 assert!(!buf.is_row_dirty(1));
3629
3630 buf.set(2, 0, Cell::from_char('A'));
3633 let row = buf.dirty_span_row(0).unwrap();
3634 assert!(row.is_full(), "full-row flag must survive later marks");
3635 }
3636
3637 #[test]
3638 fn set_raw_rewriting_tail_preserves_other_tails_of_wide_glyph() {
3639 let mut buf = Buffer::new(10, 1);
3644 let head = Cell::new(CellContent::from_grapheme(GraphemeId::new(0, 0, 3)));
3647 buf.set_raw(2, 0, head);
3648 buf.set_raw(3, 0, Cell::CONTINUATION);
3649 buf.set_raw(4, 0, Cell::CONTINUATION);
3650 assert!(buf.get(4, 0).unwrap().is_continuation());
3651
3652 buf.set_raw(3, 0, Cell::CONTINUATION);
3654 assert!(
3655 buf.get(4, 0).unwrap().is_continuation(),
3656 "second tail of the width-3 glyph must survive a tail rewrite"
3657 );
3658
3659 let head2 = Cell::new(CellContent::from_grapheme(GraphemeId::new(1, 0, 2)));
3662 buf.set_raw(2, 0, head2);
3663 buf.set_raw(3, 0, Cell::CONTINUATION);
3664 assert!(
3665 !buf.get(4, 0).unwrap().is_continuation(),
3666 "stale tail beyond a narrower rebuilt glyph must be swept"
3667 );
3668 }
3669
3670 #[test]
3671 fn dirty_span_guard_band_expands_span_bounds() {
3672 let mut buf = Buffer::new(10, 1);
3673 buf.clear_dirty();
3674 buf.set_dirty_span_config(DirtySpanConfig::default().with_guard_band(2));
3675 buf.set(5, 0, Cell::from_char('x'));
3676 let row = buf.dirty_span_row(0).unwrap();
3677 assert_eq!(row.spans(), &[DirtySpan::new(3, 8)]);
3678 }
3679
3680 #[test]
3681 fn dirty_span_max_spans_overflow_triggers_full_row() {
3682 let mut buf = Buffer::new(10, 1);
3683 buf.clear_dirty();
3684 buf.set_dirty_span_config(
3685 DirtySpanConfig::default()
3686 .with_max_spans_per_row(1)
3687 .with_merge_gap(0),
3688 );
3689 buf.set(0, 0, Cell::from_char('a'));
3690 buf.set(4, 0, Cell::from_char('b'));
3691 let row = buf.dirty_span_row(0).unwrap();
3692 assert!(row.is_full());
3693 assert!(row.spans().is_empty());
3694 assert_eq!(buf.dirty_span_overflows, 1);
3695 }
3696
3697 #[test]
3698 fn dirty_span_stats_counts_full_rows_and_spans() {
3699 let mut buf = Buffer::new(6, 2);
3700 buf.clear_dirty();
3701 buf.set_dirty_span_config(DirtySpanConfig::default().with_merge_gap(0));
3702 buf.set(1, 0, Cell::from_char('a'));
3703 buf.set(4, 0, Cell::from_char('b'));
3704 buf.mark_dirty_row_full(1);
3705
3706 let stats = buf.dirty_span_stats();
3707 assert_eq!(stats.rows_full_dirty, 1);
3708 assert_eq!(stats.rows_with_spans, 1);
3709 assert_eq!(stats.total_spans, 2);
3710 assert_eq!(stats.max_span_len, 6);
3711 assert_eq!(stats.span_coverage_cells, 8);
3712 }
3713
3714 #[test]
3715 fn dirty_span_stats_reports_overflow_and_full_row() {
3716 let mut buf = Buffer::new(8, 1);
3717 buf.clear_dirty();
3718 buf.set_dirty_span_config(
3719 DirtySpanConfig::default()
3720 .with_max_spans_per_row(1)
3721 .with_merge_gap(0),
3722 );
3723 buf.set(0, 0, Cell::from_char('x'));
3724 buf.set(3, 0, Cell::from_char('y'));
3725
3726 let stats = buf.dirty_span_stats();
3727 assert_eq!(stats.overflows, 1);
3728 assert_eq!(stats.rows_full_dirty, 1);
3729 assert_eq!(stats.total_spans, 0);
3730 assert_eq!(stats.span_coverage_cells, 8);
3731 }
3732
3733 #[test]
3738 fn double_buffer_new_has_matching_dimensions() {
3739 let db = DoubleBuffer::new(80, 24);
3740 assert_eq!(db.width(), 80);
3741 assert_eq!(db.height(), 24);
3742 assert!(db.dimensions_match(80, 24));
3743 assert!(!db.dimensions_match(120, 40));
3744 }
3745
3746 #[test]
3747 fn double_buffer_swap_is_o1() {
3748 let mut db = DoubleBuffer::new(80, 24);
3749
3750 db.current_mut().set(0, 0, Cell::from_char('A'));
3752 assert_eq!(db.current().get(0, 0).unwrap().content.as_char(), Some('A'));
3753
3754 db.swap();
3756 assert_eq!(
3757 db.previous().get(0, 0).unwrap().content.as_char(),
3758 Some('A')
3759 );
3760 assert!(db.current().get(0, 0).unwrap().is_empty());
3762 }
3763
3764 #[test]
3765 fn double_buffer_swap_round_trip() {
3766 let mut db = DoubleBuffer::new(10, 5);
3767
3768 db.current_mut().set(0, 0, Cell::from_char('X'));
3769 db.swap();
3770 db.current_mut().set(0, 0, Cell::from_char('Y'));
3771 db.swap();
3772
3773 assert_eq!(db.current().get(0, 0).unwrap().content.as_char(), Some('X'));
3775 assert_eq!(
3776 db.previous().get(0, 0).unwrap().content.as_char(),
3777 Some('Y')
3778 );
3779 }
3780
3781 #[test]
3782 fn double_buffer_resize_changes_dimensions() {
3783 let mut db = DoubleBuffer::new(80, 24);
3784 assert!(!db.resize(80, 24)); assert!(db.resize(120, 40)); assert_eq!(db.width(), 120);
3787 assert_eq!(db.height(), 40);
3788 assert!(db.dimensions_match(120, 40));
3789 }
3790
3791 #[test]
3792 fn double_buffer_resize_clears_content() {
3793 let mut db = DoubleBuffer::new(10, 5);
3794 db.current_mut().set(0, 0, Cell::from_char('Z'));
3795 db.swap();
3796 db.current_mut().set(0, 0, Cell::from_char('W'));
3797
3798 db.resize(20, 10);
3799
3800 assert!(db.current().get(0, 0).unwrap().is_empty());
3802 assert!(db.previous().get(0, 0).unwrap().is_empty());
3803 }
3804
3805 #[test]
3806 fn double_buffer_current_and_previous_are_distinct() {
3807 let mut db = DoubleBuffer::new(10, 5);
3808 db.current_mut().set(0, 0, Cell::from_char('C'));
3809
3810 assert!(db.previous().get(0, 0).unwrap().is_empty());
3812 assert_eq!(db.current().get(0, 0).unwrap().content.as_char(), Some('C'));
3813 }
3814
3815 #[test]
3820 fn adaptive_buffer_new_has_over_allocation() {
3821 let adb = AdaptiveDoubleBuffer::new(80, 24);
3822
3823 assert_eq!(adb.width(), 80);
3825 assert_eq!(adb.height(), 24);
3826 assert!(adb.dimensions_match(80, 24));
3827
3828 assert!(adb.capacity_width() > 80);
3832 assert!(adb.capacity_height() > 24);
3833 assert_eq!(adb.capacity_width(), 100); assert_eq!(adb.capacity_height(), 30); }
3836
3837 #[test]
3838 fn adaptive_buffer_resize_avoids_reallocation_when_within_capacity() {
3839 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3840
3841 assert!(adb.resize(90, 28)); assert_eq!(adb.width(), 90);
3844 assert_eq!(adb.height(), 28);
3845 assert_eq!(adb.stats().resize_avoided, 1);
3846 assert_eq!(adb.stats().resize_reallocated, 0);
3847 assert_eq!(adb.stats().resize_growth, 1);
3848 }
3849
3850 #[test]
3851 fn adaptive_buffer_resize_reallocates_on_growth_beyond_capacity() {
3852 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3853
3854 assert!(adb.resize(120, 40)); assert_eq!(adb.width(), 120);
3857 assert_eq!(adb.height(), 40);
3858 assert_eq!(adb.stats().resize_reallocated, 1);
3859 assert_eq!(adb.stats().resize_avoided, 0);
3860
3861 assert!(adb.capacity_width() > 120);
3863 assert!(adb.capacity_height() > 40);
3864 }
3865
3866 #[test]
3867 fn adaptive_buffer_resize_reallocates_on_significant_shrink() {
3868 let mut adb = AdaptiveDoubleBuffer::new(100, 50);
3869
3870 assert!(adb.resize(40, 20)); assert_eq!(adb.width(), 40);
3874 assert_eq!(adb.height(), 20);
3875 assert_eq!(adb.stats().resize_reallocated, 1);
3876 assert_eq!(adb.stats().resize_shrink, 1);
3877 }
3878
3879 #[test]
3880 fn adaptive_buffer_resize_avoids_reallocation_on_minor_shrink() {
3881 let mut adb = AdaptiveDoubleBuffer::new(100, 50);
3882
3883 assert!(adb.resize(80, 40));
3887 assert_eq!(adb.width(), 80);
3888 assert_eq!(adb.height(), 40);
3889 assert_eq!(adb.stats().resize_avoided, 1);
3890 assert_eq!(adb.stats().resize_reallocated, 0);
3891 assert_eq!(adb.stats().resize_shrink, 1);
3892 }
3893
3894 #[test]
3895 fn adaptive_buffer_no_change_returns_false() {
3896 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3897
3898 assert!(!adb.resize(80, 24)); assert_eq!(adb.stats().resize_avoided, 0);
3900 assert_eq!(adb.stats().resize_reallocated, 0);
3901 assert_eq!(adb.stats().resize_growth, 0);
3902 assert_eq!(adb.stats().resize_shrink, 0);
3903 }
3904
3905 #[test]
3906 fn adaptive_buffer_swap_works() {
3907 let mut adb = AdaptiveDoubleBuffer::new(10, 5);
3908
3909 adb.current_mut().set(0, 0, Cell::from_char('A'));
3910 assert_eq!(
3911 adb.current().get(0, 0).unwrap().content.as_char(),
3912 Some('A')
3913 );
3914
3915 adb.swap();
3916 assert_eq!(
3917 adb.previous().get(0, 0).unwrap().content.as_char(),
3918 Some('A')
3919 );
3920 assert!(adb.current().get(0, 0).unwrap().is_empty());
3921 }
3922
3923 #[test]
3924 fn adaptive_buffer_stats_reset() {
3925 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3926
3927 adb.resize(90, 28);
3928 adb.resize(120, 40);
3929 assert!(adb.stats().resize_avoided > 0 || adb.stats().resize_reallocated > 0);
3930
3931 adb.reset_stats();
3932 assert_eq!(adb.stats().resize_avoided, 0);
3933 assert_eq!(adb.stats().resize_reallocated, 0);
3934 assert_eq!(adb.stats().resize_growth, 0);
3935 assert_eq!(adb.stats().resize_shrink, 0);
3936 }
3937
3938 #[test]
3939 fn adaptive_buffer_memory_efficiency() {
3940 let adb = AdaptiveDoubleBuffer::new(80, 24);
3941
3942 let efficiency = adb.memory_efficiency();
3943 assert!(efficiency > 0.5);
3947 assert!(efficiency < 1.0);
3948 }
3949
3950 #[test]
3951 fn adaptive_buffer_logical_bounds() {
3952 let adb = AdaptiveDoubleBuffer::new(80, 24);
3953
3954 let bounds = adb.logical_bounds();
3955 assert_eq!(bounds.x, 0);
3956 assert_eq!(bounds.y, 0);
3957 assert_eq!(bounds.width, 80);
3958 assert_eq!(bounds.height, 24);
3959 }
3960
3961 #[test]
3962 fn adaptive_buffer_capacity_clamped_for_large_sizes() {
3963 let adb = AdaptiveDoubleBuffer::new(1000, 500);
3965
3966 assert_eq!(adb.capacity_width(), 1000 + 200); assert_eq!(adb.capacity_height(), 500 + 125); }
3971
3972 #[test]
3973 fn adaptive_stats_avoidance_ratio() {
3974 let mut stats = AdaptiveStats::default();
3975
3976 assert!((stats.avoidance_ratio() - 1.0).abs() < f64::EPSILON);
3978
3979 stats.resize_avoided = 3;
3981 stats.resize_reallocated = 1;
3982 assert!((stats.avoidance_ratio() - 0.75).abs() < f64::EPSILON);
3983
3984 stats.resize_avoided = 0;
3986 stats.resize_reallocated = 5;
3987 assert!((stats.avoidance_ratio() - 0.0).abs() < f64::EPSILON);
3988 }
3989
3990 #[test]
3991 fn adaptive_buffer_resize_storm_simulation() {
3992 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
3994
3995 for i in 1..=10 {
3997 adb.resize(80 + i, 24 + (i / 2));
3998 }
3999
4000 let ratio = adb.stats().avoidance_ratio();
4002 assert!(
4003 ratio > 0.5,
4004 "Expected >50% avoidance ratio, got {:.2}",
4005 ratio
4006 );
4007 }
4008
4009 #[test]
4010 fn adaptive_buffer_width_only_growth() {
4011 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
4012
4013 assert!(adb.resize(95, 24)); assert_eq!(adb.stats().resize_avoided, 1);
4016 assert_eq!(adb.stats().resize_growth, 1);
4017 }
4018
4019 #[test]
4020 fn adaptive_buffer_height_only_growth() {
4021 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
4022
4023 assert!(adb.resize(80, 28)); assert_eq!(adb.stats().resize_avoided, 1);
4026 assert_eq!(adb.stats().resize_growth, 1);
4027 }
4028
4029 #[test]
4030 fn adaptive_buffer_one_dimension_exceeds_capacity() {
4031 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
4032
4033 assert!(adb.resize(105, 24)); assert_eq!(adb.stats().resize_reallocated, 1);
4036 }
4037
4038 #[test]
4039 fn adaptive_buffer_current_and_previous_distinct() {
4040 let mut adb = AdaptiveDoubleBuffer::new(10, 5);
4041 adb.current_mut().set(0, 0, Cell::from_char('X'));
4042
4043 assert!(adb.previous().get(0, 0).unwrap().is_empty());
4045 assert_eq!(
4046 adb.current().get(0, 0).unwrap().content.as_char(),
4047 Some('X')
4048 );
4049 }
4050
4051 #[test]
4052 fn adaptive_buffer_resize_within_capacity_clears_previous() {
4053 let mut adb = AdaptiveDoubleBuffer::new(10, 5);
4054 adb.current_mut().set(9, 4, Cell::from_char('X'));
4055 adb.swap();
4056
4057 assert!(adb.resize(8, 4));
4059
4060 assert!(adb.previous().get(9, 4).unwrap().is_empty());
4062 }
4063
4064 #[test]
4066 fn adaptive_buffer_invariant_capacity_geq_logical() {
4067 for width in [1u16, 10, 80, 200, 1000, 5000] {
4069 for height in [1u16, 10, 24, 100, 500, 2000] {
4070 let adb = AdaptiveDoubleBuffer::new(width, height);
4071 assert!(
4072 adb.capacity_width() >= adb.width(),
4073 "capacity_width {} < logical_width {} for ({}, {})",
4074 adb.capacity_width(),
4075 adb.width(),
4076 width,
4077 height
4078 );
4079 assert!(
4080 adb.capacity_height() >= adb.height(),
4081 "capacity_height {} < logical_height {} for ({}, {})",
4082 adb.capacity_height(),
4083 adb.height(),
4084 width,
4085 height
4086 );
4087 }
4088 }
4089 }
4090
4091 #[test]
4092 fn adaptive_buffer_invariant_resize_dimensions_correct() {
4093 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
4094
4095 let test_sizes = [
4097 (100, 50),
4098 (40, 20),
4099 (80, 24),
4100 (200, 100),
4101 (10, 5),
4102 (1000, 500),
4103 ];
4104 for (w, h) in test_sizes {
4105 adb.resize(w, h);
4106 assert_eq!(adb.width(), w, "width mismatch for ({}, {})", w, h);
4107 assert_eq!(adb.height(), h, "height mismatch for ({}, {})", w, h);
4108 assert!(
4109 adb.capacity_width() >= w,
4110 "capacity_width < width for ({}, {})",
4111 w,
4112 h
4113 );
4114 assert!(
4115 adb.capacity_height() >= h,
4116 "capacity_height < height for ({}, {})",
4117 w,
4118 h
4119 );
4120 }
4121 }
4122
4123 #[test]
4127 fn adaptive_buffer_no_ghosting_on_shrink() {
4128 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
4129
4130 for y in 0..adb.height() {
4132 for x in 0..adb.width() {
4133 adb.current_mut().set(x, y, Cell::from_char('X'));
4134 }
4135 }
4136
4137 adb.resize(60, 20);
4140
4141 for y in 0..adb.height() {
4144 for x in 0..adb.width() {
4145 let cell = adb.current().get(x, y).unwrap();
4146 assert!(
4147 cell.is_empty(),
4148 "Ghost content at ({}, {}): expected empty, got {:?}",
4149 x,
4150 y,
4151 cell.content
4152 );
4153 }
4154 }
4155 }
4156
4157 #[test]
4161 fn adaptive_buffer_no_ghosting_on_reallocation_shrink() {
4162 let mut adb = AdaptiveDoubleBuffer::new(100, 50);
4163
4164 for y in 0..adb.height() {
4166 for x in 0..adb.width() {
4167 adb.current_mut().set(x, y, Cell::from_char('A'));
4168 }
4169 }
4170 adb.swap();
4171 for y in 0..adb.height() {
4172 for x in 0..adb.width() {
4173 adb.current_mut().set(x, y, Cell::from_char('B'));
4174 }
4175 }
4176
4177 adb.resize(30, 15);
4179 assert_eq!(adb.stats().resize_reallocated, 1);
4180
4181 for y in 0..adb.height() {
4183 for x in 0..adb.width() {
4184 assert!(
4185 adb.current().get(x, y).unwrap().is_empty(),
4186 "Ghost in current at ({}, {})",
4187 x,
4188 y
4189 );
4190 assert!(
4191 adb.previous().get(x, y).unwrap().is_empty(),
4192 "Ghost in previous at ({}, {})",
4193 x,
4194 y
4195 );
4196 }
4197 }
4198 }
4199
4200 #[test]
4204 fn adaptive_buffer_no_ghosting_on_growth_reallocation() {
4205 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
4206
4207 for y in 0..adb.height() {
4209 for x in 0..adb.width() {
4210 adb.current_mut().set(x, y, Cell::from_char('Z'));
4211 }
4212 }
4213
4214 adb.resize(150, 60);
4216 assert_eq!(adb.stats().resize_reallocated, 1);
4217
4218 for y in 0..adb.height() {
4220 for x in 0..adb.width() {
4221 assert!(
4222 adb.current().get(x, y).unwrap().is_empty(),
4223 "Ghost at ({}, {}) after growth reallocation",
4224 x,
4225 y
4226 );
4227 }
4228 }
4229 }
4230
4231 #[test]
4233 fn adaptive_buffer_resize_idempotent() {
4234 let mut adb = AdaptiveDoubleBuffer::new(80, 24);
4235 adb.current_mut().set(5, 5, Cell::from_char('K'));
4236
4237 let changed = adb.resize(80, 24);
4239 assert!(!changed);
4240
4241 assert_eq!(
4243 adb.current().get(5, 5).unwrap().content.as_char(),
4244 Some('K')
4245 );
4246 }
4247
4248 #[test]
4253 fn dirty_span_merge_adjacent() {
4254 let mut buf = Buffer::new(100, 1);
4255 buf.clear_dirty(); buf.mark_dirty_span(0, 10, 20);
4259 let spans = buf.dirty_span_row(0).unwrap().spans();
4260 assert_eq!(spans.len(), 1);
4261 assert_eq!(spans[0], DirtySpan::new(10, 20));
4262
4263 buf.mark_dirty_span(0, 20, 30);
4265 let spans = buf.dirty_span_row(0).unwrap().spans();
4266 assert_eq!(spans.len(), 1);
4267 assert_eq!(spans[0], DirtySpan::new(10, 30));
4268 }
4269
4270 #[test]
4271 fn dirty_span_merge_overlapping() {
4272 let mut buf = Buffer::new(100, 1);
4273 buf.clear_dirty();
4274
4275 buf.mark_dirty_span(0, 10, 20);
4277 buf.mark_dirty_span(0, 15, 25);
4279
4280 let spans = buf.dirty_span_row(0).unwrap().spans();
4281 assert_eq!(spans.len(), 1);
4282 assert_eq!(spans[0], DirtySpan::new(10, 25));
4283 }
4284
4285 #[test]
4286 fn dirty_span_merge_with_gap() {
4287 let mut buf = Buffer::new(100, 1);
4288 buf.clear_dirty();
4289
4290 buf.mark_dirty_span(0, 10, 20);
4293 buf.mark_dirty_span(0, 21, 30);
4295
4296 let spans = buf.dirty_span_row(0).unwrap().spans();
4297 assert_eq!(spans.len(), 1);
4298 assert_eq!(spans[0], DirtySpan::new(10, 30));
4299 }
4300
4301 #[test]
4302 fn dirty_span_no_merge_large_gap() {
4303 let mut buf = Buffer::new(100, 1);
4304 buf.clear_dirty();
4305
4306 buf.mark_dirty_span(0, 10, 20);
4308 buf.mark_dirty_span(0, 22, 30);
4310
4311 let spans = buf.dirty_span_row(0).unwrap().spans();
4312 assert_eq!(spans.len(), 2);
4313 assert_eq!(spans[0], DirtySpan::new(10, 20));
4314 assert_eq!(spans[1], DirtySpan::new(22, 30));
4315 }
4316
4317 #[test]
4318 fn dirty_span_overflow_to_full() {
4319 let mut buf = Buffer::new(1000, 1);
4320 buf.clear_dirty();
4321
4322 for i in 0..DIRTY_SPAN_MAX_SPANS_PER_ROW + 10 {
4324 let start = (i * 4) as u16;
4325 buf.mark_dirty_span(0, start, start + 1);
4326 }
4327
4328 let row = buf.dirty_span_row(0).unwrap();
4329 assert!(row.is_full(), "Row should overflow to full scan");
4330 assert!(
4331 row.spans().is_empty(),
4332 "Spans should be cleared on overflow"
4333 );
4334 }
4335
4336 #[test]
4337 fn dirty_span_bounds_clamping() {
4338 let mut buf = Buffer::new(10, 1);
4339 buf.clear_dirty();
4340
4341 buf.mark_dirty_span(0, 15, 20);
4343 let spans = buf.dirty_span_row(0).unwrap().spans();
4344 assert!(spans.is_empty());
4345
4346 buf.mark_dirty_span(0, 8, 15);
4348 let spans = buf.dirty_span_row(0).unwrap().spans();
4349 assert_eq!(spans.len(), 1);
4350 assert_eq!(spans[0], DirtySpan::new(8, 10)); }
4352
4353 #[test]
4354 fn dirty_span_guard_band_clamps_bounds() {
4355 let mut buf = Buffer::new(10, 1);
4356 buf.clear_dirty();
4357 buf.set_dirty_span_config(DirtySpanConfig::default().with_guard_band(5));
4358
4359 buf.mark_dirty_span(0, 2, 3);
4360 let spans = buf.dirty_span_row(0).unwrap().spans();
4361 assert_eq!(spans.len(), 1);
4362 assert_eq!(spans[0], DirtySpan::new(0, 8));
4363
4364 buf.clear_dirty();
4365 buf.mark_dirty_span(0, 8, 10);
4366 let spans = buf.dirty_span_row(0).unwrap().spans();
4367 assert_eq!(spans.len(), 1);
4368 assert_eq!(spans[0], DirtySpan::new(3, 10));
4369 }
4370
4371 #[test]
4372 fn dirty_span_empty_span_is_ignored() {
4373 let mut buf = Buffer::new(10, 1);
4374 buf.clear_dirty();
4375 buf.mark_dirty_span(0, 5, 5);
4376 let spans = buf.dirty_span_row(0).unwrap().spans();
4377 assert!(spans.is_empty());
4378 }
4379
4380 #[test]
4381 fn buffer_fill_wide_char_clipping() {
4382 let mut buf = Buffer::new(10, 5);
4386 let wide_cell = Cell::from_char('🦀'); buf.fill(Rect::new(0, 0, 10, 5), wide_cell);
4390
4391 let head = buf.get(0, 0).unwrap();
4393 assert_eq!(head.content.as_char(), Some('🦀'));
4394 assert_eq!(head.content.width(), 2);
4395
4396 let tail = buf.get(1, 0).unwrap();
4397 assert!(tail.is_continuation());
4398
4399 buf.push_scissor(Rect::new(0, 0, 1, 5));
4402 let x_cell = Cell::from_char('X');
4404 buf.fill(Rect::new(0, 0, 10, 5), x_cell);
4405
4406 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('X'));
4408 assert!(buf.get(1, 0).unwrap().is_empty()); buf.pop_scissor();
4411 }
4412}