1mod backdrop;
28
29use crate::prelude::*;
30use alloc::rc::Rc;
31use core::ops::Range;
32
33pub use backdrop::{Backdrop, MAX_ZOOM};
34
35use shuyi_assets::font::CellWidths;
36use shuyi_assets::image::ImageId;
37use shuyi_assets::lighting::Light;
38use shuyi_assets::text::NO_BREAK_SPACE;
39
40use crate::frame::color::Rgb;
41use crate::frame::console::ConsoleSpec;
42use crate::frame::gizmo::{GizmoItem, GizmoShape};
43use crate::frame::painted::{PaintedSprite, PaintedTable};
44
45#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
47pub struct Cell {
48 pub glyph: char,
50 pub fg: Rgb,
52 pub bg: Rgb,
54 pub see_through: bool,
58}
59
60impl Cell {
61 pub const fn new(glyph: char, fg: Rgb, bg: Rgb) -> Self {
63 Self {
64 glyph,
65 fg,
66 bg,
67 see_through: false,
68 }
69 }
70
71 pub const fn see_through(glyph: char, fg: Rgb) -> Self {
76 Self {
77 glyph,
78 fg,
79 bg: Rgb::new(0, 0, 0),
80 see_through: true,
81 }
82 }
83
84 pub const fn is_continuation(&self) -> bool {
87 self.glyph == CONTINUATION
88 }
89}
90
91const CONTINUATION: char = '\u{ffff}';
94
95#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
97pub struct Rect {
98 pub x: i32,
100 pub y: i32,
102 pub w: i32,
104 pub h: i32,
106}
107
108impl Rect {
109 pub const fn new(x: i32, y: i32, w: i32, h: i32) -> Self {
111 Self { x, y, w, h }
112 }
113
114 pub const fn is_empty(&self) -> bool {
116 self.w <= 0 || self.h <= 0
117 }
118
119 pub fn contains(&self, x: i32, y: i32) -> bool {
121 let (x, y) = (i64::from(x), i64::from(y));
122 let (left, top) = (i64::from(self.x), i64::from(self.y));
123 x >= left && y >= top && x < left + i64::from(self.w) && y < top + i64::from(self.h)
124 }
125
126 pub fn intersect(&self, other: &Rect) -> Option<Rect> {
128 let x0 = self.x.max(other.x);
129 let y0 = self.y.max(other.y);
130 let x1 =
131 (i64::from(self.x) + i64::from(self.w)).min(i64::from(other.x) + i64::from(other.w));
132 let y1 =
133 (i64::from(self.y) + i64::from(self.h)).min(i64::from(other.y) + i64::from(other.h));
134 let w = i32::try_from(x1 - i64::from(x0)).ok()?;
136 let h = i32::try_from(y1 - i64::from(y0)).ok()?;
137 let r = Rect::new(x0, y0, w, h);
138 (!r.is_empty()).then_some(r)
139 }
140}
141
142pub type PxRect = Rect;
146
147#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
149pub enum Layer {
150 Under,
152 Over,
154}
155
156impl Layer {
157 pub const fn name(self) -> &'static str {
159 match self {
160 Layer::Under => "under",
161 Layer::Over => "over",
162 }
163 }
164}
165
166#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
168pub struct Overlay {
169 pub rect: PxRect,
171 pub color: Rgb,
173 pub layer: Layer,
175}
176
177impl Overlay {
178 pub const fn new(rect: PxRect, color: Rgb, layer: Layer) -> Self {
180 Self { rect, color, layer }
181 }
182}
183
184#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, Default)]
187pub enum Paint {
188 #[default]
190 Image,
191 Solid(Rgb),
194 Dimmed,
197 Shaded(Light),
200}
201
202impl Paint {
203 #[must_use]
205 pub fn apply(self, rgb: Rgb) -> Rgb {
206 match self {
207 Paint::Image => rgb,
208 Paint::Solid(color) => color,
209 Paint::Dimmed => rgb.dimmed(),
210 Paint::Shaded(light) => light.apply([rgb.r, rgb.g, rgb.b]).into(),
211 }
212 }
213}
214
215#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
219pub struct Sprite {
220 pub image: ImageId,
222 pub src: PxRect,
225 pub dest: PxRect,
227 pub clip: PxRect,
231 pub layer: Layer,
233 pub flip_x: bool,
235 pub opacity: u8,
237 pub brightness: u8,
242 pub paint: Paint,
244 pub base: Option<PxRect>,
250}
251
252impl Sprite {
253 pub const fn new(image: ImageId, src: PxRect, dest: PxRect, layer: Layer) -> Self {
255 Self {
256 image,
257 src,
258 dest,
259 clip: dest,
260 layer,
261 flip_x: false,
262 opacity: u8::MAX,
263 brightness: u8::MAX,
264 paint: Paint::Image,
265 base: None,
266 }
267 }
268
269 fn overhang(&self, hole: PxRect) -> Option<PxRect> {
276 let base = self.base?;
277 let cover = hole.intersect(&base)?;
278 let seen = self.clip;
279 let stretch = |cover: (i32, i32), base: (i32, i32), seen: (i32, i32)| {
281 let low = if cover.0 == base.0 { seen.0 } else { cover.0 };
282 let high = if cover.0 + cover.1 == base.0 + base.1 {
283 seen.0 + seen.1
284 } else {
285 cover.0 + cover.1
286 };
287 (low, high)
288 };
289 let (left, right) = stretch((cover.x, cover.w), (base.x, base.w), (seen.x, seen.w));
290 let (top, bottom) = stretch((cover.y, cover.h), (base.y, base.h), (seen.y, seen.h));
291 Some(Rect::new(left, top, right - left, bottom - top))
292 }
293
294 #[must_use]
296 pub const fn painted(self, paint: Paint) -> Self {
297 Self { paint, ..self }
298 }
299
300 pub fn clipped_src(&self) -> [f32; 4] {
304 clipped_src(self.src, self.dest, self.clip, self.flip_x)
305 }
306}
307
308#[allow(clippy::cast_precision_loss)] pub(crate) fn clipped_src(src: PxRect, dest: PxRect, clip: PxRect, flip_x: bool) -> [f32; 4] {
313 let wide = |v: i32| i64::from(v);
314 let inset_x = if flip_x {
316 wide(dest.x) + wide(dest.w) - wide(clip.x) - wide(clip.w)
317 } else {
318 wide(clip.x) - wide(dest.x)
319 };
320 let inset_y = wide(clip.y) - wide(dest.y);
321 let scale_x = src.w as f32 / dest.w as f32;
322 let scale_y = src.h as f32 / dest.h as f32;
323 [
324 src.x as f32 + inset_x as f32 * scale_x,
325 src.y as f32 + inset_y as f32 * scale_y,
326 clip.w as f32 * scale_x,
327 clip.h as f32 * scale_y,
328 ]
329}
330
331#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
334#[non_exhaustive]
335pub enum Item {
336 Rect(Overlay),
338 Sprite(Sprite),
340 Painted(PaintedSprite),
343 Gizmo(GizmoItem),
345}
346
347impl Item {
348 pub const fn layer(&self) -> Layer {
350 match self {
351 Item::Rect(o) => o.layer,
352 Item::Sprite(s) => s.layer,
353 Item::Painted(p) => p.layer,
354 Item::Gizmo(_) => Layer::Over,
355 }
356 }
357
358 pub const fn visible(&self) -> PxRect {
361 match self {
362 Item::Rect(o) => o.rect,
363 Item::Sprite(s) => s.clip,
364 Item::Painted(p) => p.clip,
365 Item::Gizmo(g) => g.clip,
366 }
367 }
368
369 fn overhang(&self, hole: PxRect) -> Option<PxRect> {
372 match self {
373 Item::Rect(_) | Item::Painted(_) | Item::Gizmo(_) => None,
374 Item::Sprite(s) => s.overhang(hole),
375 }
376 }
377
378 const fn with_visible(self, visible: PxRect) -> Self {
380 match self {
381 Item::Rect(o) => Item::Rect(Overlay { rect: visible, ..o }),
382 Item::Sprite(s) => Item::Sprite(Sprite { clip: visible, ..s }),
383 Item::Painted(p) => Item::Painted(PaintedSprite { clip: visible, ..p }),
384 Item::Gizmo(g) => Item::Gizmo(GizmoItem { clip: visible, ..g }),
385 }
386 }
387}
388
389fn offset(r: PxRect, dx: i64, dy: i64) -> Option<PxRect> {
391 let x = i32::try_from(i64::from(r.x) + dx).ok()?;
392 let y = i32::try_from(i64::from(r.y) + dy).ok()?;
393 Some(Rect::new(x, y, r.w, r.h))
394}
395
396fn offset_clipped(r: PxRect, dx: i64, dy: i64, bounds: PxRect) -> Option<PxRect> {
399 let (left, top) = (i64::from(r.x) + dx, i64::from(r.y) + dy);
401 let (x0, y0) = (left.max(0), top.max(0));
402 let x1 = (left + i64::from(r.w)).min(i64::from(bounds.w));
403 let y1 = (top + i64::from(r.h)).min(i64::from(bounds.h));
404 if x1 <= x0 || y1 <= y0 {
405 return None;
406 }
407 let fit = |v: i64| i32::try_from(v).unwrap_or(0);
408 Some(Rect::new(fit(x0), fit(y0), fit(x1 - x0), fit(y1 - y0)))
409}
410
411fn cells_to_px(spec: ConsoleSpec, x: i64, y: i64, w: i64, h: i64) -> Option<PxRect> {
415 let (cw, ch) = (i64::from(spec.cell_w), i64::from(spec.cell_h));
416 let fit = |v: i64| i32::try_from(v).ok();
417 Some(Rect::new(
418 fit(x * cw)?,
419 fit(y * ch)?,
420 fit(w * cw)?,
421 fit(h * ch)?,
422 ))
423}
424
425fn subtract(r: Rect, hole: Rect) -> Vec<Rect> {
427 let Some(i) = r.intersect(&hole) else {
428 return vec![r];
429 };
430 let (r_right, r_bottom) = (r.x + r.w, r.y + r.h);
432 let (i_right, i_bottom) = (i.x + i.w, i.y + i.h);
433 [
434 Rect::new(r.x, r.y, r.w, i.y - r.y),
435 Rect::new(r.x, i_bottom, r.w, r_bottom - i_bottom),
436 Rect::new(r.x, i.y, i.x - r.x, i.h),
437 Rect::new(i_right, i.y, r_right - i_right, i.h),
438 ]
439 .into_iter()
440 .filter(|p| !p.is_empty())
441 .collect()
442}
443
444#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
447pub enum BoxStyle {
448 Single,
450 Double,
452}
453
454impl BoxStyle {
455 const fn glyphs(self) -> [char; 6] {
457 match self {
458 BoxStyle::Single => ['┌', '┐', '└', '┘', '─', '│'],
459 BoxStyle::Double => ['╔', '╗', '╚', '╝', '═', '║'],
460 }
461 }
462}
463
464fn clip_span(start: i32, len: i32, limit: u16) -> core::ops::Range<i32> {
466 let limit = i64::from(limit);
467 let lo = i64::from(start).clamp(0, limit);
468 let hi = (i64::from(start) + i64::from(len)).clamp(lo, limit);
469 let fit = |v: i64| i32::try_from(v).unwrap_or(0);
471 fit(lo)..fit(hi)
472}
473
474#[derive(Clone, PartialEq, Debug)]
476pub struct GlyphBuffer {
477 spec: ConsoleSpec,
479 cells: Vec<Cell>,
480 items: Vec<Item>,
483 backdrop: Option<Backdrop>,
485 widths: CellWidths,
487 pictures: PaintedTable,
490}
491
492impl GlyphBuffer {
493 pub fn new(width: u16, height: u16, fill: Cell) -> Self {
497 Self::with_spec(ConsoleSpec::text(width, height), fill)
498 }
499
500 pub fn with_spec(spec: ConsoleSpec, fill: Cell) -> Self {
503 Self {
504 spec,
505 cells: vec![fill; usize::from(spec.cols) * usize::from(spec.rows)],
506 items: Vec::new(),
507 backdrop: None,
508 widths: CellWidths::NARROW,
509 pictures: PaintedTable::default(),
510 }
511 }
512
513 #[must_use]
517 pub fn with_widths(mut self, widths: CellWidths) -> Self {
518 self.widths = widths;
519 self
520 }
521
522 pub fn set_widths(&mut self, widths: CellWidths) {
525 self.widths = widths;
526 }
527
528 pub fn widths(&self) -> &CellWidths {
531 &self.widths
532 }
533
534 pub fn text_cells(&self, text: &str) -> i32 {
539 let width = shuyi_assets::lang::text_width(text, &self.widths);
540 i32::try_from(width).unwrap_or(i32::MAX)
541 }
542
543 pub fn spec(&self) -> ConsoleSpec {
545 self.spec
546 }
547
548 pub fn backdrop(&self) -> Option<&Backdrop> {
550 self.backdrop.as_ref()
551 }
552
553 pub fn set_backdrop(
560 &mut self,
561 scene: Rc<GlyphBuffer>,
562 clip: Rect,
563 origin_px: (f32, f32),
564 zoom: u8,
565 ) {
566 self.absorb_pictures(&scene);
567 self.backdrop = clip
568 .intersect(&self.bounds())
569 .map(|clip| Backdrop::new(scene, self.spec, clip, origin_px, zoom));
570 }
571
572 pub fn pictures(&self) -> &PaintedTable {
574 &self.pictures
575 }
576
577 pub fn pictures_mut(&mut self) -> &mut PaintedTable {
581 &mut self.pictures
582 }
583
584 pub fn clear_backdrop(&mut self) {
586 self.backdrop = None;
587 }
588
589 pub fn pixel_bounds(&self) -> PxRect {
591 let (w, h) = self.spec.px();
592 Rect::new(0, 0, w, h)
593 }
594
595 pub fn items(&self) -> &[Item] {
598 &self.items
599 }
600
601 pub fn overlays(&self) -> Vec<Overlay> {
603 let rect = |item: &Item| match item {
604 Item::Rect(o) => Some(*o),
605 _ => None,
606 };
607 self.items.iter().filter_map(rect).collect()
608 }
609
610 pub fn sprites(&self) -> Vec<Sprite> {
612 let sprite = |item: &Item| match item {
613 Item::Sprite(s) => Some(*s),
614 _ => None,
615 };
616 self.items.iter().filter_map(sprite).collect()
617 }
618
619 pub fn painted_sprites(&self) -> Vec<PaintedSprite> {
621 let painted = |item: &Item| match item {
622 Item::Painted(p) => Some(*p),
623 _ => None,
624 };
625 self.items.iter().filter_map(painted).collect()
626 }
627
628 pub fn add_overlay(&mut self, overlay: Overlay) {
631 if let Some(rect) = overlay.rect.intersect(&self.pixel_bounds()) {
632 self.items.push(Item::Rect(Overlay { rect, ..overlay }));
633 }
634 }
635
636 pub fn add_sprite(&mut self, sprite: Sprite) {
640 if sprite.src.is_empty() {
641 return;
642 }
643 let clip = sprite
644 .clip
645 .intersect(&sprite.dest)
646 .and_then(|clip| clip.intersect(&self.pixel_bounds()));
647 if let Some(clip) = clip {
648 self.items.push(Item::Sprite(Sprite { clip, ..sprite }));
649 }
650 }
651
652 pub fn add_painted(&mut self, painted: PaintedSprite) {
658 if painted.src.is_empty() {
659 return;
660 }
661 let clip = painted
662 .clip
663 .intersect(&painted.dest)
664 .and_then(|clip| clip.intersect(&self.pixel_bounds()));
665 if let Some(clip) = clip {
666 self.items
667 .push(Item::Painted(PaintedSprite { clip, ..painted }));
668 }
669 }
670
671 pub fn gizmos(&self) -> Vec<GizmoItem> {
673 let gizmo = |item: &Item| match item {
674 Item::Gizmo(g) => Some(*g),
675 _ => None,
676 };
677 self.items.iter().filter_map(gizmo).collect()
678 }
679
680 pub fn add_gizmo(&mut self, gizmo: GizmoItem) {
685 let reach = match gizmo.shape {
686 GizmoShape::Label { .. } => Some(gizmo.clip),
687 shape => gizmo.clip.intersect(&shape.bounds()),
688 };
689 if let Some(clip) = reach.and_then(|clip| clip.intersect(&self.pixel_bounds())) {
690 self.items.push(Item::Gizmo(GizmoItem { clip, ..gizmo }));
691 }
692 }
693
694 fn absorb_pictures(&mut self, other: &GlyphBuffer) {
697 for item in &other.items {
698 if let Item::Painted(p) = item
699 && let Some(picture) = other.pictures.get(p.picture)
700 {
701 self.pictures.absorb(p.picture, picture);
702 }
703 }
704 }
705
706 fn cut_items(&mut self, rect: Rect) {
712 let Some(hole) = rect
713 .intersect(&self.bounds())
714 .and_then(|r| cells_to_px(self.spec, r.x.into(), r.y.into(), r.w.into(), r.h.into()))
715 else {
716 return;
717 };
718 self.items = self
719 .items
720 .iter()
721 .flat_map(|item| {
722 let overhang = item.overhang(hole);
723 subtract(item.visible(), hole)
724 .into_iter()
725 .flat_map(move |part| match overhang {
726 Some(overhang) => subtract(part, overhang),
727 None => vec![part],
728 })
729 .map(move |visible| item.with_visible(visible))
730 })
731 .collect();
732 }
733
734 fn mend_edges(&mut self, other: &GlyphBuffer, dest_x: i32, target: Rect) {
738 let Some(copied) = target.intersect(&self.bounds()) else {
739 return;
740 };
741 let last = copied.x + copied.w - 1;
742 for y in copied.y..copied.y + copied.h {
743 if let Some(c) = self.get_mut(copied.x, y).filter(|c| c.is_continuation()) {
744 c.glyph = ' ';
745 }
746 let after = i64::from(last) + 1 - i64::from(dest_x);
749 let left_behind = i32::try_from(after)
750 .ok()
751 .and_then(|sx| other.get(sx, y - target.y))
752 .is_some_and(Cell::is_continuation);
753 if left_behind && let Some(c) = self.get_mut(last, y) {
754 c.glyph = ' ';
755 }
756 }
757 }
758
759 pub fn width(&self) -> u16 {
761 self.spec.cols
762 }
763
764 pub fn height(&self) -> u16 {
766 self.spec.rows
767 }
768
769 pub fn bounds(&self) -> Rect {
771 Rect::new(0, 0, i32::from(self.spec.cols), i32::from(self.spec.rows))
772 }
773
774 fn index(&self, x: i32, y: i32) -> Option<usize> {
776 let x = u16::try_from(x).ok().filter(|&x| x < self.spec.cols)?;
777 let y = u16::try_from(y).ok().filter(|&y| y < self.spec.rows)?;
778 Some(usize::from(y) * usize::from(self.spec.cols) + usize::from(x))
779 }
780
781 pub fn get(&self, x: i32, y: i32) -> Option<&Cell> {
783 self.index(x, y).map(|i| &self.cells[i])
784 }
785
786 fn get_mut(&mut self, x: i32, y: i32) -> Option<&mut Cell> {
788 self.index(x, y).map(|i| &mut self.cells[i])
789 }
790
791 pub fn set(&mut self, x: i32, y: i32, cell: Cell) {
794 if self.index(x, y).is_some() {
795 self.unpair(y, x..x + 1);
796 }
797 if let Some(c) = self.get_mut(x, y) {
798 *c = cell;
799 }
800 }
801
802 fn unpair(&mut self, y: i32, xs: Range<i32>) {
807 if self.get(xs.start, y).is_some_and(Cell::is_continuation)
808 && let Some(first) = xs.start.checked_sub(1).and_then(|x| self.get_mut(x, y))
809 {
810 first.glyph = ' ';
811 }
812 if let Some(next) = self.get_mut(xs.end, y).filter(|c| c.is_continuation()) {
813 next.glyph = ' ';
814 }
815 }
816
817 fn for_each_in(&mut self, rect: Rect, mut f: impl FnMut(i32, i32, &mut Cell)) {
819 for y in clip_span(rect.y, rect.h, self.spec.rows) {
820 for x in clip_span(rect.x, rect.w, self.spec.cols) {
821 if let Some(c) = self.get_mut(x, y) {
822 f(x, y, c);
823 }
824 }
825 }
826 }
827
828 fn unpair_rect(&mut self, rect: Rect) {
831 let columns = clip_span(rect.x, rect.w, self.spec.cols);
832 if columns.is_empty() {
833 return;
834 }
835 for y in clip_span(rect.y, rect.h, self.spec.rows) {
836 self.unpair(y, columns.clone());
837 }
838 }
839
840 pub fn print(&mut self, x: i32, y: i32, text: &str, fg: Rgb, bg: Rgb) -> u16 {
847 self.print_with(x, y, text, |c, glyph| *c = Cell::new(glyph, fg, bg))
848 }
849
850 pub fn print_fg(&mut self, x: i32, y: i32, text: &str, fg: Rgb) -> u16 {
852 self.print_with(x, y, text, |c, glyph| {
853 c.glyph = glyph;
854 c.fg = fg;
855 })
856 }
857
858 fn print_with(
859 &mut self,
860 x: i32,
861 y: i32,
862 text: &str,
863 mut f: impl FnMut(&mut Cell, char),
864 ) -> u16 {
865 if !(0..i32::from(self.spec.rows)).contains(&y) {
866 return 0;
867 }
868 let columns = i64::from(self.spec.cols);
869 let mut written = 0;
870 let mut cx = i64::from(x);
871 for glyph in text.chars() {
872 let glyph = if glyph == NO_BREAK_SPACE { ' ' } else { glyph };
873 if cx >= columns {
874 break;
875 }
876 let wide = self.widths.is_wide(glyph);
877 let cells: u16 = if wide { 2 } else { 1 };
878 if cx >= 0 && cx + i64::from(cells) <= columns {
879 let at = i32::try_from(cx).unwrap_or(0);
881 self.unpair(y, at..at + i32::from(cells));
882 if let Some(c) = self.get_mut(at, y) {
883 f(c, glyph);
884 }
885 if wide && let Some(c) = self.get_mut(at + 1, y) {
886 f(c, CONTINUATION);
887 }
888 written += cells;
889 }
890 cx += i64::from(cells);
891 }
892 written
893 }
894
895 pub fn glyphs(&self) -> impl Iterator<Item = (i32, i32, &Cell, bool)> + '_ {
899 let (w, h) = (i32::from(self.width()), i32::from(self.height()));
900 (0..h).flat_map(move |y| {
901 (0..w).filter_map(move |x| {
902 let cell = self.get(x, y).filter(|c| !c.is_continuation())?;
903 let wide = self.get(x + 1, y).is_some_and(Cell::is_continuation);
904 Some((x, y, cell, wide))
905 })
906 })
907 }
908
909 pub fn fill_rect(&mut self, rect: Rect, cell: Cell) {
913 self.unpair_rect(rect);
914 self.for_each_in(rect, |_, _, c| *c = cell);
915 self.cut_items(rect);
916 if !cell.see_through && rect.intersect(&self.bounds()) == Some(self.bounds()) {
917 self.backdrop = None;
918 }
919 }
920
921 pub fn draw_box(&mut self, rect: Rect, style: BoxStyle, fg: Rgb, bg: Rgb) {
923 if rect.is_empty() {
924 return;
925 }
926 let [tl, tr, bl, br, horiz, vert] = style.glyphs();
927 let right = i64::from(rect.x) + i64::from(rect.w) - 1;
928 let bottom = i64::from(rect.y) + i64::from(rect.h) - 1;
929 for y in clip_span(rect.y, rect.h, self.spec.rows) {
930 for x in clip_span(rect.x, rect.w, self.spec.cols) {
931 let left_edge = x == rect.x;
932 let right_edge = i64::from(x) == right;
933 let top_edge = y == rect.y;
934 let bottom_edge = i64::from(y) == bottom;
935 let glyph = match (top_edge, bottom_edge, left_edge, right_edge) {
936 (true, _, true, _) => tl,
937 (true, _, _, true) => tr,
938 (_, true, true, _) => bl,
939 (_, true, _, true) => br,
940 (true, _, _, _) | (_, true, _, _) => horiz,
941 (_, _, true, _) | (_, _, _, true) => vert,
942 _ => continue,
943 };
944 self.set(x, y, Cell::new(glyph, fg, bg));
945 }
946 }
947 }
948
949 pub fn blend_bg(&mut self, rect: Rect, color: Rgb, t: f32) {
952 self.for_each_in(rect, |_, _, c| c.bg = c.bg.lerp(color, t));
953 }
954
955 pub fn dim(&mut self, rect: Rect, factor: f32) {
960 self.for_each_in(rect, |_, _, c| {
961 c.fg = c.fg.scale(factor);
962 c.bg = c.bg.scale(factor);
963 });
964 }
965
966 pub fn fade(&mut self, brightness: f32) {
972 let brightness = if brightness.is_nan() {
973 1.0
974 } else {
975 brightness.clamp(0.0, 1.0)
976 };
977 if brightness >= 1.0 {
978 return;
979 }
980 self.dim(self.bounds(), brightness);
981 for item in &mut self.items {
982 match item {
983 Item::Rect(Overlay { color, .. }) | Item::Gizmo(GizmoItem { color, .. }) => {
984 *color = color.scale(brightness);
985 }
986 Item::Sprite(Sprite { brightness: b, .. })
987 | Item::Painted(PaintedSprite { brightness: b, .. }) => {
988 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] let darker = (f32::from(*b) * brightness).round() as u8;
990 *b = darker;
991 }
992 }
993 }
994 if let Some(backdrop) = &mut self.backdrop {
995 backdrop.fade(brightness);
996 }
997 }
998
999 pub fn blit(&mut self, other: &GlyphBuffer, dest_x: i32, dest_y: i32) {
1006 let target = Rect::new(
1007 dest_x,
1008 dest_y,
1009 i32::from(other.width()),
1010 i32::from(other.height()),
1011 );
1012 self.unpair_rect(target);
1013 self.for_each_in(target, |x, y, c| {
1014 if let Some(src) = other.get(x - dest_x, y - dest_y) {
1016 *c = *src;
1017 }
1018 });
1019 self.mend_edges(other, dest_x, target);
1020 self.cut_items(target);
1021 let bounds = self.pixel_bounds();
1022 let (dx, dy) = (
1023 i64::from(dest_x) * i64::from(self.spec.cell_w),
1024 i64::from(dest_y) * i64::from(self.spec.cell_h),
1025 );
1026 for item in &other.items {
1027 let Some(visible) = offset_clipped(item.visible(), dx, dy, bounds) else {
1028 continue;
1029 };
1030 match *item {
1031 Item::Rect(_) => self.items.push(item.with_visible(visible)),
1032 Item::Sprite(s) => {
1033 if let Some(dest) = offset(s.dest, dx, dy) {
1035 let base = s.base.and_then(|base| offset(base, dx, dy));
1036 let moved = Sprite { dest, base, ..s };
1037 self.items.push(Item::Sprite(moved).with_visible(visible));
1038 }
1039 }
1040 Item::Painted(p) => {
1041 if let Some(dest) = offset(p.dest, dx, dy) {
1042 let moved = PaintedSprite { dest, ..p };
1043 self.items.push(Item::Painted(moved).with_visible(visible));
1044 }
1045 }
1046 Item::Gizmo(g) => {
1047 if let Some(shape) = g.shape.moved(dx, dy) {
1048 let moved = GizmoItem { shape, ..g };
1049 self.items.push(Item::Gizmo(moved).with_visible(visible));
1050 }
1051 }
1052 }
1053 }
1054 self.absorb_pictures(other);
1055 }
1056}
1057
1058#[cfg(test)]
1059mod backdrop_tests;
1060#[cfg(test)]
1061mod gizmo_tests;
1062#[cfg(test)]
1063mod sprite_tests;
1064
1065#[cfg(test)]
1066mod tests {
1067 use proptest::prelude::*;
1068
1069 use super::*;
1070
1071 const FG: Rgb = Rgb::new(200, 200, 200);
1072 const BG: Rgb = Rgb::new(0, 0, 0);
1073 const RED: Rgb = Rgb::new(255, 0, 0);
1074 const BLUE: Rgb = Rgb::new(0, 0, 255);
1075 const BLANK: Cell = Cell::new(' ', FG, BG);
1076
1077 fn buf(w: u16, h: u16) -> GlyphBuffer {
1078 GlyphBuffer::new(w, h, BLANK)
1079 }
1080
1081 fn row(b: &GlyphBuffer, y: i32) -> String {
1083 (0..i32::from(b.width()))
1084 .map(|x| b.get(x, y).unwrap().glyph)
1085 .collect()
1086 }
1087
1088 fn glyph(b: &GlyphBuffer, x: i32, y: i32) -> char {
1089 b.get(x, y).unwrap().glyph
1090 }
1091
1092 #[test]
1093 fn new_and_size() {
1094 let b = buf(3, 2);
1095 assert_eq!((b.width(), b.height()), (3, 2));
1096 assert_eq!(b.bounds(), Rect::new(0, 0, 3, 2));
1097 assert_eq!(b.get(2, 1), Some(&BLANK));
1098 }
1099
1100 #[test]
1101 fn get_out_of_bounds() {
1102 let b = buf(3, 2);
1103 for (x, y) in [(3, 0), (0, 2), (-1, 0), (0, -1), (i32::MAX, i32::MIN)] {
1104 assert_eq!(b.get(x, y), None, "({x}, {y})");
1105 }
1106 }
1107
1108 #[test]
1109 fn set_in_and_out_of_bounds() {
1110 let mut b = buf(3, 2);
1111 let c = Cell::new('@', RED, BLUE);
1112 b.set(1, 1, c);
1113 assert_eq!(b.get(1, 1), Some(&c));
1114 assert_eq!(b.get(1, 0), Some(&BLANK));
1115 let before = b.clone();
1116 b.set(3, 1, c);
1117 b.set(-1, 0, c);
1118 b.set(0, 2, c);
1119 assert_eq!(b, before);
1120 }
1121
1122 #[test]
1123 fn rect_contains_edges() {
1124 let r = Rect::new(1, 2, 3, 4);
1125 assert!(r.contains(1, 2));
1126 assert!(r.contains(3, 5));
1127 assert!(!r.contains(0, 2));
1128 assert!(!r.contains(1, 1));
1129 assert!(!r.contains(4, 2));
1130 assert!(!r.contains(1, 6));
1131 assert!(!Rect::new(0, 0, 0, 5).contains(0, 0));
1132 assert!(Rect::new(i32::MAX, 0, i32::MAX, 1).contains(i32::MAX, 0));
1133 }
1134
1135 #[test]
1136 fn rect_is_empty() {
1137 let found = Rect::new(0, 0, 0, 1);
1138 assert!(found.is_empty(), "{found:?}");
1139 let found = Rect::new(0, 0, 1, 0);
1140 assert!(found.is_empty(), "{found:?}");
1141 let found = Rect::new(0, 0, -1, 1);
1142 assert!(found.is_empty(), "{found:?}");
1143 let found = Rect::new(0, 0, 1, 1);
1144 assert!(!found.is_empty(), "{found:?}");
1145 }
1146
1147 #[test]
1148 fn rect_intersect() {
1149 let a = Rect::new(0, 0, 4, 4);
1150 assert_eq!(
1151 a.intersect(&Rect::new(2, 1, 5, 2)),
1152 Some(Rect::new(2, 1, 2, 2))
1153 );
1154 assert_eq!(
1155 a.intersect(&Rect::new(-2, -3, 3, 5)),
1156 Some(Rect::new(0, 0, 1, 2))
1157 );
1158 assert_eq!(a.intersect(&Rect::new(4, 0, 1, 1)), None);
1159 assert_eq!(a.intersect(&Rect::new(0, 4, 1, 1)), None);
1160 assert_eq!(a.intersect(&Rect::new(1, 1, 0, 1)), None);
1161 assert_eq!(Rect::new(1, 1, -5, 3).intersect(&a), None);
1162 assert_eq!(a.intersect(&a), Some(a));
1163 let huge = Rect::new(i32::MIN, i32::MIN, i32::MAX, i32::MAX);
1164 assert_eq!(huge.intersect(&a), None);
1165 let huge = Rect::new(-5, -5, i32::MAX, i32::MAX);
1166 assert_eq!(huge.intersect(&a), Some(a));
1167 }
1168
1169 #[test]
1170 fn clip_span_cases() {
1171 assert_eq!(clip_span(2, 3, 10), 2..5);
1172 assert_eq!(clip_span(-2, 5, 10), 0..3);
1173 assert_eq!(clip_span(8, 5, 10), 8..10);
1174 assert_eq!(clip_span(12, 5, 10), 10..10);
1175 assert_eq!(clip_span(3, -5, 10), 3..3);
1176 assert_eq!(clip_span(i32::MIN, i32::MAX, 10), 0..0);
1177 assert_eq!(clip_span(i32::MAX, i32::MAX, u16::MAX), 65535..65535);
1178 }
1179
1180 #[test]
1181 fn print_writes_and_counts() {
1182 let mut b = buf(5, 2);
1183 assert_eq!(b.print(1, 1, "abc", RED, BLUE), 3);
1184 assert_eq!(row(&b, 1), " abc ");
1185 assert_eq!(b.get(1, 1), Some(&Cell::new('a', RED, BLUE)));
1186 assert_eq!(row(&b, 0), " ");
1187 }
1188
1189 #[test]
1190 fn print_clips_right_left_and_rows() {
1191 let mut b = buf(5, 2);
1192 assert_eq!(b.print(3, 0, "wxyz", RED, BG), 2);
1193 assert_eq!(row(&b, 0), " wx");
1194 assert_eq!(b.print(-2, 1, "abcd", RED, BG), 2);
1195 assert_eq!(row(&b, 1), "cd ");
1196 let before = b.clone();
1197 assert_eq!(b.print(0, 2, "abc", RED, BG), 0);
1198 assert_eq!(b.print(0, -1, "abc", RED, BG), 0);
1199 assert_eq!(b.print(5, 0, "abc", RED, BG), 0);
1200 assert_eq!(b.print(i32::MAX, 0, "abc", RED, BG), 0);
1201 assert_eq!(b.print(i32::MIN, 0, "abc", RED, BG), 0);
1202 assert_eq!(b, before);
1203 }
1204
1205 #[test]
1206 fn a_no_break_space_is_printed_as_a_space() {
1207 let mut b = buf(5, 1);
1208 assert_eq!(b.print(0, 0, "a\u{a0}b", RED, BG), 3);
1209 assert_eq!(row(&b, 0), "a b ");
1210 assert_eq!(glyph(&b, 1, 0), ' ');
1211 }
1212
1213 #[test]
1214 fn print_one_char_per_cell() {
1215 let mut b = buf(4, 1);
1216 assert_eq!(b.print(0, 0, "é♣≈", RED, BG), 3);
1217 assert_eq!(row(&b, 0), "é♣≈ ");
1218 }
1219
1220 fn wide_buf(w: u16, h: u16) -> GlyphBuffer {
1222 buf(w, h).with_widths(CellWidths::wide(['あ', 'い']))
1223 }
1224
1225 fn halves(b: &GlyphBuffer, y: i32) -> String {
1227 (0..i32::from(b.width()))
1228 .map(|x| {
1229 let c = b.get(x, y).unwrap();
1230 if c.is_continuation() { '+' } else { c.glyph }
1231 })
1232 .collect()
1233 }
1234
1235 #[test]
1236 fn a_wide_character_fills_two_cells_and_the_next_follows() {
1237 let mut b = wide_buf(4, 1);
1238 assert_eq!(b.print(0, 0, "あA", RED, BLUE), 3);
1239 assert_eq!(halves(&b, 0), "あ+A ");
1240 assert_eq!(b.get(0, 0), Some(&Cell::new('あ', RED, BLUE)));
1241 let second = b.get(1, 0).unwrap();
1242 assert!(second.is_continuation());
1243 assert_eq!((second.fg, second.bg), (RED, BLUE));
1244 assert!(!b.get(2, 0).unwrap().is_continuation());
1245 assert_eq!(b.text_cells("あA"), 3);
1247 assert_eq!(buf(4, 1).text_cells("あA"), 2);
1248 let mut narrow = buf(4, 1);
1250 assert_eq!(narrow.print(0, 0, "あA", RED, BLUE), 2);
1251 assert_eq!(halves(&narrow, 0), "あA ");
1252 }
1253
1254 #[test]
1255 fn a_wide_character_the_edge_would_cut_is_not_drawn() {
1256 let mut b = wide_buf(4, 1);
1257 assert_eq!(b.print(2, 0, "Aあ", RED, BLUE), 1);
1258 assert_eq!(halves(&b, 0), " A ");
1259 let mut b = wide_buf(4, 1);
1260 assert_eq!(b.print(-1, 0, "あいA", RED, BLUE), 3);
1261 assert_eq!(halves(&b, 0), " い+A");
1262 let mut b = wide_buf(3, 1);
1263 assert_eq!(b.print(0, 0, "Aいあ", RED, BLUE), 3);
1264 assert_eq!(halves(&b, 0), "Aい+");
1265 }
1266
1267 #[test]
1268 fn overwriting_either_half_leaves_no_orphan() {
1269 let mut b = wide_buf(4, 1);
1271 b.print(1, 0, "あ", RED, BLUE);
1272 b.print(1, 0, "B", RED, BLUE);
1273 assert_eq!(halves(&b, 0), " B ");
1274 let mut b = wide_buf(4, 1);
1276 b.print(1, 0, "あ", RED, BLUE);
1277 b.print(2, 0, "C", RED, BLUE);
1278 assert_eq!(halves(&b, 0), " C ");
1279 let mut b = wide_buf(5, 1);
1281 b.print(0, 0, "あい", RED, BLUE);
1282 b.print(1, 0, "あ", FG, BG);
1283 assert_eq!(halves(&b, 0), " あ+ ");
1284 assert_eq!(b.get(3, 0), Some(&Cell::new(' ', RED, BLUE)));
1286 for redraw in [
1288 |b: &mut GlyphBuffer| b.set(2, 0, BLANK),
1289 |b: &mut GlyphBuffer| b.fill_rect(Rect::new(2, 0, 5, 1), BLANK),
1290 |b: &mut GlyphBuffer| b.draw_box(Rect::new(-3, -1, 5, 3), BoxStyle::Single, FG, BG),
1291 ] {
1292 let mut b = wide_buf(4, 1);
1293 b.print(0, 0, "Aあ", RED, BLUE);
1294 redraw(&mut b);
1295 let row = halves(&b, 0);
1296 assert!(!row.contains('+') && !row.contains('あ'), "{row}");
1297 }
1298 let mut b = wide_buf(4, 1);
1300 b.print(0, 0, "あ", RED, BLUE);
1301 b.dim(Rect::new(1, 0, 1, 1), 0.5);
1302 b.blend_bg(Rect::new(0, 0, 1, 1), RED, 0.5);
1303 b.print_fg(0, 0, "", RED);
1304 assert_eq!(halves(&b, 0), "あ+ ");
1305 }
1306
1307 #[test]
1308 fn blit_keeps_a_wide_character_whole_or_drops_it() {
1309 let mut src = wide_buf(4, 1);
1310 src.print(0, 0, "あいB", RED, BLUE);
1311 assert_eq!(halves(&src, 0), "あ+い+");
1312 let mut b = wide_buf(5, 1);
1313 b.blit(&src, 1, 0);
1314 assert_eq!(halves(&b, 0), " あ+い+");
1315 let mut b = wide_buf(4, 1);
1317 b.blit(&src, -1, 0);
1318 assert_eq!(halves(&b, 0), " い+ ");
1319 let mut b = wide_buf(4, 1);
1321 b.blit(&src, 1, 0);
1322 assert_eq!(halves(&b, 0), " あ+ ");
1323 let mut narrow_then_wide = wide_buf(4, 1);
1326 narrow_then_wide.print(0, 0, "abい", RED, BLUE);
1327 let mut b = wide_buf(5, 2);
1328 b.blit(&narrow_then_wide, 2, 1);
1329 assert_eq!(halves(&b, 1), " ab ");
1330 let mut b = wide_buf(4, 1);
1332 b.print(0, 0, "あい", FG, BG);
1333 b.blit(&buf(1, 1), 1, 0);
1334 assert_eq!(halves(&b, 0), " い+");
1335 }
1336
1337 #[test]
1338 fn glyphs_skip_continuations_and_say_which_are_wide() {
1339 let mut b = wide_buf(4, 2);
1340 b.print(0, 0, "あA", RED, BLUE);
1341 let found: Vec<(i32, i32, char, bool)> = b
1342 .glyphs()
1343 .map(|(x, y, c, wide)| (x, y, c.glyph, wide))
1344 .collect();
1345 assert_eq!(
1346 found[..3],
1347 [(0, 0, 'あ', true), (2, 0, 'A', false), (3, 0, ' ', false)]
1348 );
1349 assert_eq!(found.len(), 3 + 4);
1350 }
1351
1352 #[test]
1353 fn print_fg_keeps_bg() {
1354 let mut b = buf(3, 1);
1355 b.set(1, 0, Cell::new('x', FG, BLUE));
1356 assert_eq!(b.print_fg(0, 0, "ab", RED), 2);
1357 assert_eq!(b.get(0, 0), Some(&Cell::new('a', RED, BG)));
1358 assert_eq!(b.get(1, 0), Some(&Cell::new('b', RED, BLUE)));
1359 assert_eq!(b.get(2, 0), Some(&BLANK));
1360 assert_eq!(b.print_fg(2, 0, "yz", RED), 1);
1361 }
1362
1363 #[test]
1364 fn fill_rect_fills_and_clips() {
1365 let mut b = buf(4, 3);
1366 let c = Cell::new('#', RED, BLUE);
1367 b.fill_rect(Rect::new(1, 1, 2, 1), c);
1368 assert_eq!(row(&b, 0), " ");
1369 assert_eq!(row(&b, 1), " ## ");
1370 assert_eq!(row(&b, 2), " ");
1371 b.fill_rect(Rect::new(-1, -1, 2, 2), c);
1372 assert_eq!(row(&b, 0), "# ");
1373 b.fill_rect(Rect::new(3, 2, 10, 10), c);
1374 assert_eq!(row(&b, 2), " #");
1375 }
1376
1377 #[test]
1378 fn draw_box_single_and_double() {
1379 let mut b = buf(5, 4);
1380 b.draw_box(Rect::new(0, 0, 5, 4), BoxStyle::Single, RED, BLUE);
1381 assert_eq!(row(&b, 0), "┌───┐");
1382 assert_eq!(row(&b, 1), "│ │");
1383 assert_eq!(row(&b, 2), "│ │");
1384 assert_eq!(row(&b, 3), "└───┘");
1385 assert_eq!(b.get(0, 0), Some(&Cell::new('┌', RED, BLUE)));
1386 assert_eq!(b.get(1, 1), Some(&BLANK));
1387 let mut b = buf(4, 3);
1388 b.draw_box(Rect::new(1, 0, 3, 3), BoxStyle::Double, RED, BLUE);
1389 assert_eq!(row(&b, 0), " ╔═╗");
1390 assert_eq!(row(&b, 1), " ║ ║");
1391 assert_eq!(row(&b, 2), " ╚═╝");
1392 }
1393
1394 #[test]
1395 fn draw_box_clipped_keeps_corners_of_real_rect() {
1396 let mut b = buf(3, 3);
1397 b.draw_box(Rect::new(-1, -1, 3, 3), BoxStyle::Single, RED, BLUE);
1398 assert_eq!(row(&b, 0), " │ ");
1399 assert_eq!(row(&b, 1), "─┘ ");
1400 assert_eq!(row(&b, 2), " ");
1401 let mut b = buf(3, 3);
1402 b.draw_box(Rect::new(1, 1, 5, 5), BoxStyle::Double, RED, BLUE);
1403 assert_eq!(row(&b, 0), " ");
1404 assert_eq!(row(&b, 1), " ╔═");
1405 assert_eq!(row(&b, 2), " ║ ");
1406 }
1407
1408 #[test]
1409 fn draw_box_corners_each_quadrant() {
1410 let mut b = buf(2, 2);
1411 b.draw_box(Rect::new(0, 0, 2, 2), BoxStyle::Single, RED, BLUE);
1412 assert_eq!(
1413 [
1414 glyph(&b, 0, 0),
1415 glyph(&b, 1, 0),
1416 glyph(&b, 0, 1),
1417 glyph(&b, 1, 1)
1418 ],
1419 ['┌', '┐', '└', '┘']
1420 );
1421 }
1422
1423 #[test]
1424 fn draw_box_degenerate() {
1425 let mut b = buf(3, 3);
1426 b.draw_box(Rect::new(0, 0, 0, 3), BoxStyle::Single, RED, BLUE);
1427 assert_eq!(b, buf(3, 3));
1428 b.draw_box(Rect::new(0, 0, 1, 3), BoxStyle::Single, RED, BLUE);
1429 assert_eq!(
1430 [glyph(&b, 0, 0), glyph(&b, 0, 1), glyph(&b, 0, 2)],
1431 ['┌', '│', '└']
1432 );
1433 b.draw_box(Rect::new(0, 0, 3, 1), BoxStyle::Single, RED, BLUE);
1434 assert_eq!(row(&b, 0), "┌─┐");
1435 }
1436
1437 #[test]
1438 fn blend_bg_tints_only_rect() {
1439 let mut b = buf(3, 1);
1440 b.set(0, 0, Cell::new('x', RED, Rgb::new(0, 0, 100)));
1441 b.blend_bg(Rect::new(0, 0, 2, 1), Rgb::new(100, 0, 0), 0.5);
1442 assert_eq!(b.get(0, 0), Some(&Cell::new('x', RED, Rgb::new(50, 0, 50))));
1443 assert_eq!(b.get(1, 0), Some(&Cell::new(' ', FG, Rgb::new(50, 0, 0))));
1444 assert_eq!(b.get(2, 0), Some(&BLANK));
1445 }
1446
1447 #[test]
1448 fn dim_scales_fg_and_bg() {
1449 let mut b = GlyphBuffer::new(
1450 2,
1451 1,
1452 Cell::new('a', Rgb::new(200, 100, 50), Rgb::new(10, 20, 40)),
1453 );
1454 b.dim(Rect::new(1, 0, 5, 5), 0.5);
1455 assert_eq!(b.get(0, 0).unwrap().fg, Rgb::new(200, 100, 50));
1456 assert_eq!(
1457 b.get(1, 0),
1458 Some(&Cell::new('a', Rgb::new(100, 50, 25), Rgb::new(5, 10, 20)))
1459 );
1460 }
1461
1462 #[test]
1463 fn blit_copies_and_clips() {
1464 let mut src = buf(2, 2);
1465 src.print(0, 0, "ab", RED, BLUE);
1466 src.print(0, 1, "cd", RED, BLUE);
1467 let mut b = buf(3, 3);
1468 b.blit(&src, 1, 1);
1469 assert_eq!(row(&b, 0), " ");
1470 assert_eq!(row(&b, 1), " ab");
1471 assert_eq!(row(&b, 2), " cd");
1472 assert_eq!(b.get(1, 1), Some(&Cell::new('a', RED, BLUE)));
1473 let mut b = buf(3, 3);
1474 b.blit(&src, -1, -1);
1475 assert_eq!(row(&b, 0), "d ");
1476 assert_eq!(row(&b, 1), " ");
1477 let mut b = buf(3, 3);
1478 b.blit(&src, 2, 0);
1479 assert_eq!(row(&b, 0), " a");
1480 assert_eq!(row(&b, 1), " c");
1481 let before = b.clone();
1482 b.blit(&src, i32::MAX, i32::MIN);
1483 b.blit(&src, 3, 0);
1484 assert_eq!(b, before);
1485 }
1486
1487 fn over(x: i32, y: i32, w: i32, h: i32) -> Overlay {
1488 Overlay::new(Rect::new(x, y, w, h), RED, Layer::Over)
1489 }
1490
1491 #[test]
1492 fn overlays_are_added_and_clipped() {
1493 let mut b = buf(3, 2);
1494 assert_eq!(b.pixel_bounds(), Rect::new(0, 0, 24, 32));
1495 let found = b.overlays();
1496 assert!(found.is_empty(), "{found:?}");
1497 b.add_overlay(over(2, 14, 16, 2));
1498 b.add_overlay(Overlay::new(Rect::new(-4, 30, 10, 5), BLUE, Layer::Under));
1499 b.add_overlay(over(20, 0, 10, 1));
1500 b.add_overlay(over(24, 0, 1, 1));
1501 b.add_overlay(over(0, 0, 0, 1));
1502 b.add_overlay(over(i32::MAX, i32::MIN, i32::MAX, 1));
1503 assert_eq!(
1504 b.overlays(),
1505 [
1506 over(2, 14, 16, 2),
1507 Overlay::new(Rect::new(0, 30, 6, 2), BLUE, Layer::Under),
1508 over(20, 0, 4, 1),
1509 ]
1510 );
1511 assert_eq!(Layer::Under.name(), "under");
1512 assert_eq!(Layer::Over.name(), "over");
1513 }
1514
1515 #[test]
1516 fn blit_offsets_and_clips_overlays() {
1517 let mut src = buf(2, 2);
1518 src.add_overlay(over(0, 14, 16, 2));
1519 src.add_overlay(over(8, 16, 8, 16));
1520 let mut b = buf(3, 3);
1521 b.blit(&src, 1, 1);
1522 assert_eq!(b.overlays(), [over(8, 30, 16, 2), over(16, 32, 8, 16)]);
1523 let mut b = buf(3, 3);
1524 b.blit(&src, -1, -1);
1525 assert_eq!(b.overlays(), [over(0, 0, 8, 16)]);
1527 let mut b = buf(3, 3);
1528 b.blit(&src, 2, 2);
1529 assert_eq!(b.overlays(), [over(16, 46, 8, 2)]);
1530 let mut b = buf(3, 3);
1531 b.blit(&src, i32::MAX, i32::MIN);
1532 b.blit(&src, i32::MIN, i32::MAX);
1533 let found = b.overlays();
1534 assert!(found.is_empty(), "{found:?}");
1535 }
1536
1537 #[test]
1538 fn replacing_cells_removes_their_overlays() {
1539 let mut b = buf(4, 2);
1540 b.add_overlay(over(0, 14, 32, 2));
1541 b.add_overlay(Overlay::new(Rect::new(0, 0, 32, 32), BLUE, Layer::Under));
1542 b.fill_rect(Rect::new(1, 0, 2, 1), BLANK);
1543 assert_eq!(
1544 b.overlays(),
1545 [
1546 over(0, 14, 8, 2),
1547 over(24, 14, 8, 2),
1548 Overlay::new(Rect::new(0, 16, 32, 16), BLUE, Layer::Under),
1549 Overlay::new(Rect::new(0, 0, 8, 16), BLUE, Layer::Under),
1550 Overlay::new(Rect::new(24, 0, 8, 16), BLUE, Layer::Under),
1551 ]
1552 );
1553 let before = b.clone();
1555 b.fill_rect(Rect::new(5, 0, 2, 2), BLANK);
1556 b.fill_rect(Rect::new(0, 0, 0, 2), BLANK);
1557 assert_eq!(b, before);
1558 let src = buf(1, 1);
1560 b.blit(&src, 0, 1);
1561 assert!(b.overlays().iter().all(|o| o.rect.y < 16 || o.rect.x >= 8));
1562 b.fill_rect(b.bounds(), BLANK);
1563 let found = b.overlays();
1564 assert!(found.is_empty(), "{found:?}");
1565 }
1566
1567 #[test]
1568 fn subtract_cases() {
1569 let r = Rect::new(0, 0, 10, 10);
1570 assert_eq!(subtract(r, Rect::new(20, 0, 5, 5)), [r]);
1571 let found = subtract(r, Rect::new(-1, -1, 20, 20));
1572 assert!(found.is_empty(), "{found:?}");
1573 assert_eq!(
1574 subtract(r, Rect::new(2, 3, 4, 5)),
1575 [
1576 Rect::new(0, 0, 10, 3),
1577 Rect::new(0, 8, 10, 2),
1578 Rect::new(0, 3, 2, 5),
1579 Rect::new(6, 3, 4, 5),
1580 ]
1581 );
1582 let pc = ConsoleSpec::text(100, 32);
1583 assert_eq!(cells_to_px(pc, 1, 2, 3, 4), Some(Rect::new(8, 32, 24, 64)));
1584 assert_eq!(cells_to_px(pc, i64::from(i32::MAX), 0, 1, 1), None);
1585 let small = ConsoleSpec {
1586 cell_w: 4,
1587 cell_h: 6,
1588 ..pc
1589 };
1590 assert_eq!(
1591 cells_to_px(small, 1, 2, 3, 4),
1592 Some(Rect::new(4, 12, 12, 24))
1593 );
1594 }
1595
1596 fn marked(w: u16, h: u16) -> GlyphBuffer {
1597 let mut b = buf(w, h);
1598 for y in 0..i32::from(h) {
1599 for x in 0..i32::from(w) {
1600 let g = char::from_u32(0x100 + u32::try_from(y * 64 + x).unwrap()).unwrap();
1601 b.set(x, y, Cell::new(g, FG, BG));
1602 }
1603 }
1604 b
1605 }
1606
1607 fn unchanged_outside(before: &GlyphBuffer, after: &GlyphBuffer, target: Rect) {
1609 for y in 0..i32::from(before.height()) {
1610 for x in 0..i32::from(before.width()) {
1611 if !target.contains(x, y) {
1612 assert_eq!(before.get(x, y), after.get(x, y), "cell ({x}, {y}) changed");
1613 }
1614 }
1615 }
1616 }
1617
1618 fn coord() -> impl Strategy<Value = i32> {
1619 prop_oneof![-20..40i32, any::<i32>()]
1620 }
1621
1622 fn rect() -> impl Strategy<Value = Rect> {
1623 (coord(), coord(), coord(), coord()).prop_map(|(x, y, w, h)| Rect::new(x, y, w, h))
1624 }
1625
1626 proptest! {
1627 #[test]
1628 fn print_only_touches_its_row_span(
1629 w in 0u16..30, h in 0u16..10, x in coord(), y in coord(), text in "\\PC{0,40}",
1630 ) {
1631 let before = marked(w, h);
1632 let mut after = before.clone();
1633 let n = after.print(x, y, &text, RED, BLUE);
1634 let len = i32::try_from(text.chars().count()).unwrap();
1635 let target = Rect::new(x, y, len, 1);
1636 unchanged_outside(&before, &after, target);
1637 let inside = target.intersect(&before.bounds()).map_or(0, |r| r.w);
1638 prop_assert_eq!(i32::from(n), inside);
1639 }
1640
1641 #[test]
1644 fn a_continuation_never_lacks_its_first_half(
1645 w in 0u16..12,
1646 prints in proptest::collection::vec(
1647 (coord(), 0i32..3, "[aあいb ]{0,6}", 0u8..3, coord()),
1648 1..8,
1649 ),
1650 ) {
1651 let mut b = wide_buf(w, 3);
1652 let mut src = wide_buf(5, 1);
1653 src.print(0, 0, "aあいb", RED, BLUE);
1654 for (x, y, text, how, other) in &prints {
1655 match how {
1656 0 => { b.print(*x, *y, text, RED, BLUE); }
1657 1 => b.fill_rect(Rect::new(*x, *y, other.rem_euclid(4), 1), BLANK),
1658 _ => b.blit(&src, *x, *y),
1659 }
1660 }
1661 for y in 0..3 {
1662 for x in 0..i32::from(w) {
1663 if b.get(x, y).unwrap().is_continuation() {
1664 let first = x.checked_sub(1).and_then(|x| b.get(x, y));
1665 prop_assert!(
1666 first.is_some_and(|c| !c.is_continuation() && c.glyph != ' '),
1667 "row {} is {:?}", y, halves(&b, y)
1668 );
1669 }
1670 }
1671 }
1672 }
1673
1674 #[test]
1675 fn fill_rect_only_touches_rect(w in 0u16..30, h in 0u16..10, r in rect()) {
1676 let before = marked(w, h);
1677 let mut after = before.clone();
1678 let c = Cell::new('#', RED, BLUE);
1679 after.fill_rect(r, c);
1680 unchanged_outside(&before, &after, r);
1681 if let Some(i) = r.intersect(&before.bounds()) {
1682 prop_assert_eq!(after.get(i.x, i.y), Some(&c));
1683 prop_assert_eq!(after.get(i.x + i.w - 1, i.y + i.h - 1), Some(&c));
1684 }
1685 }
1686
1687 #[test]
1688 fn blit_only_touches_dest(
1689 w in 0u16..30, h in 0u16..10, sw in 0u16..20, sh in 0u16..8, x in coord(), y in coord(),
1690 ) {
1691 let before = marked(w, h);
1692 let mut after = before.clone();
1693 let src = GlyphBuffer::new(sw, sh, Cell::new('s', RED, BLUE));
1694 after.blit(&src, x, y);
1695 unchanged_outside(&before, &after, Rect::new(x, y, i32::from(sw), i32::from(sh)));
1696 }
1697
1698 #[test]
1699 fn other_primitives_only_touch_rect(w in 0u16..30, h in 0u16..10, r in rect(), t in any::<f32>()) {
1700 let before = marked(w, h);
1701 let mut after = before.clone();
1702 after.draw_box(r, BoxStyle::Double, RED, BLUE);
1703 after.blend_bg(r, RED, t);
1704 after.dim(r, t);
1705 unchanged_outside(&before, &after, r);
1706 }
1707
1708 #[test]
1709 fn overlays_stay_inside_the_buffer(
1710 w in 0u16..30, h in 0u16..10, r in rect(), x in coord(), y in coord(), f in rect(),
1711 ) {
1712 let mut src = buf(4, 3);
1713 src.add_overlay(over(r.x, r.y, r.w, r.h));
1714 let mut b = buf(w, h);
1715 b.add_overlay(over(r.x, r.y, r.w, r.h));
1716 b.blit(&src, x, y);
1717 b.fill_rect(f, BLANK);
1718 let px = b.pixel_bounds();
1719 for o in b.overlays() {
1720 prop_assert_eq!(o.rect.intersect(&px), Some(o.rect));
1721 }
1722 }
1723
1724 #[test]
1725 fn get_and_set_never_panic(w in 0u16..30, h in 0u16..10, x in coord(), y in coord()) {
1726 let mut b = marked(w, h);
1727 let inside = b.bounds().contains(x, y);
1728 prop_assert_eq!(b.get(x, y).is_some(), inside);
1729 b.set(x, y, BLANK);
1730 }
1731 }
1732}