Skip to main content

shuyi_ui/frame/
glyph_buffer.rs

1//! The [`GlyphBuffer`] virtual console every screen draws into (game ADR-0003).
2//!
3//! Positions are `i32` so callers can draw partly (or wholly) off-buffer:
4//! every primitive clips to the buffer and never panics.
5//!
6//! Besides cells, a buffer holds [`Item`]s, placed in console pixels:
7//! [`Overlay`]s, coloured rectangles for what whole cells can't draw (HP
8//! bars, the path line; game ADR-0018), [`Sprite`]s, pictures from image
9//! files (game ADR-0038), pictures a skin painted
10//! ([`PaintedSprite`], ADR-0078) and the debug
11//! view's [`GizmoItem`]s (ADR-0085), over everything. Within a layer, items are drawn in the order they were
12//! added. They belong to the cells they were drawn with: replacing cells
13//! ([`GlyphBuffer::fill_rect`], [`GlyphBuffer::blit`]) removes the parts of
14//! items over them.
15//!
16//! A buffer may also hold one [`Backdrop`] (game ADR-0048): a second buffer, the
17//! *scene*, shown through a window of cells at a pixel offset and a whole
18//! zoom, behind the cells marked [`Cell::see_through`].
19//!
20//! Text is drawn left to right, one character to a cell, but a wide one
21//! (a Japanese or Chinese character, as the buffer's [`CellWidths`] say)
22//! takes two: the first holds the character, the second a continuation
23//! ([`Cell::is_continuation`]) in the same colours, and a renderer draws
24//! one glyph over both. Replacing either half of a wide character makes
25//! the other a space, so a continuation always follows its first half.
26
27mod 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/// One console cell: a glyph with foreground and background colours.
46#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
47pub struct Cell {
48    /// The character drawn in the cell.
49    pub glyph: char,
50    /// Glyph colour.
51    pub fg: Rgb,
52    /// Background colour.
53    pub bg: Rgb,
54    /// No background is drawn: the buffer's [`Backdrop`] shows behind the
55    /// glyph (the console's clear colour where there is none). `bg` is
56    /// kept but not shown.
57    pub see_through: bool,
58}
59
60impl Cell {
61    /// Builds a cell with a solid background.
62    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    /// A cell with no background (game ADR-0048): `glyph` in `fg` over the
72    /// buffer's [`Backdrop`]. A space is a plain hole.
73    /// [`GlyphBuffer::print_fg`] over it keeps it see-through; anything
74    /// that sets a whole cell makes it solid again.
75    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    /// Whether this is the second cell of a wide character, drawn by the
85    /// first: never a glyph of its own.
86    pub const fn is_continuation(&self) -> bool {
87        self.glyph == CONTINUATION
88    }
89}
90
91/// What the second cell of a wide character holds (a noncharacter, never
92/// in text). Ask [`Cell::is_continuation`], never compare with this.
93const CONTINUATION: char = '\u{ffff}';
94
95/// A rectangle of cells. A non-positive `w` or `h` means empty.
96#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
97pub struct Rect {
98    /// Left column.
99    pub x: i32,
100    /// Top row.
101    pub y: i32,
102    /// Width in cells.
103    pub w: i32,
104    /// Height in cells.
105    pub h: i32,
106}
107
108impl Rect {
109    /// Builds a rectangle.
110    pub const fn new(x: i32, y: i32, w: i32, h: i32) -> Self {
111        Self { x, y, w, h }
112    }
113
114    /// Whether the rectangle has no cells.
115    pub const fn is_empty(&self) -> bool {
116        self.w <= 0 || self.h <= 0
117    }
118
119    /// Whether cell `(x, y)` lies inside.
120    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    /// The overlap of two rectangles, or `None` if they don't overlap.
127    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        // The overlap is no wider than either input, so it fits in i32.
135        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
142/// A rectangle in console pixels (the logical space before scaling: a cell
143/// is its buffer's [`ConsoleSpec::cell_px`]). Same shape as a cell
144/// [`Rect`].
145pub type PxRect = Rect;
146
147/// When an [`Item`] is drawn relative to the cells' glyphs.
148#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
149pub enum Layer {
150    /// After cell backgrounds, before glyphs (e.g. the path line).
151    Under,
152    /// After glyphs (e.g. HP bars).
153    Over,
154}
155
156impl Layer {
157    /// Lowercase name, as in snapshots.
158    pub const fn name(self) -> &'static str {
159        match self {
160            Layer::Under => "under",
161            Layer::Over => "over",
162        }
163    }
164}
165
166/// A solid rectangle drawn on top of the cell grid (game ADR-0018).
167#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
168pub struct Overlay {
169    /// Where, in console pixels.
170    pub rect: PxRect,
171    /// Fill colour.
172    pub color: Rgb,
173    /// Under or over the glyphs.
174    pub layer: Layer,
175}
176
177impl Overlay {
178    /// Builds an overlay.
179    pub const fn new(rect: PxRect, color: Rgb, layer: Layer) -> Self {
180        Self { rect, color, layer }
181    }
182}
183
184/// How a [`Sprite`]'s pixels are coloured (game ADR-0049). Whichever it is, a
185/// pixel is as see-through as the image has it.
186#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, Default)]
187pub enum Paint {
188    /// The image's own colours.
189    #[default]
190    Image,
191    /// Every pixel in this one colour: the picture's silhouette (a unit's
192    /// outline, a hit flash).
193    Solid(Rgb),
194    /// Each pixel [dimmed](Rgb::dimmed): grey and darker (a unit that has
195    /// acted).
196    Dimmed,
197    /// Each pixel as it is in this light (game ADR-0058): the ground of a map
198    /// at dusk.
199    Shaded(Light),
200}
201
202impl Paint {
203    /// The colour a pixel of the image's colour `rgb` is drawn in.
204    #[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/// A picture on top of the cell grid (game ADR-0038): part of an image file,
216/// scaled into a rectangle of console pixels. `app` draws it from a texture
217/// with nearest-pixel sampling, so whole-number scales stay sharp.
218#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
219pub struct Sprite {
220    /// Which image (`ImageTable::id`).
221    pub image: ImageId,
222    /// The part of the image shown, in image pixels. It must lie inside
223    /// the image.
224    pub src: PxRect,
225    /// Where `src` is stretched to, in console pixels.
226    pub dest: PxRect,
227    /// The part of `dest` that is drawn. Clipping and cutting only ever
228    /// shrink this; `src` and `dest` stay as given, so the picture never
229    /// shifts or rescales.
230    pub clip: PxRect,
231    /// Under or over the glyphs.
232    pub layer: Layer,
233    /// Mirrored left to right within `dest`.
234    pub flip_x: bool,
235    /// 255 = solid; lower lets what is under it show through.
236    pub opacity: u8,
237    /// How bright its pixels are drawn: 255 = as the image and the paint
238    /// have them, lower = that much darker, 0 = black where it shows.
239    /// Unlike a lower opacity, a picture over another (a tile's edge over
240    /// its ground) darkens as much as one alone ([`GlyphBuffer::fade`]).
241    pub brightness: u8,
242    /// How its pixels are coloured.
243    pub paint: Paint,
244    /// The console pixels it stands on, for a picture that reaches above
245    /// them (a unit's head over the tile above its own): where cells
246    /// replace its base up to one of the base's edges, what it shows past
247    /// that edge goes too, so a menu over a unit doesn't leave its head
248    /// showing above the menu.
249    pub base: Option<PxRect>,
250}
251
252impl Sprite {
253    /// A solid, unflipped sprite showing all of `dest`.
254    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    /// What goes of this sprite, beyond `hole` itself, when the pixels
270    /// `hole` are replaced: the part of its [`base`](Self::base) that
271    /// `hole` covers, stretched outwards, past each edge of the base it
272    /// reaches, to this sprite's own edge (up over the head, when it
273    /// covers the base's top). `None` if it has no base or `hole` misses
274    /// it. The rectangle may be empty: the sprite then shows nothing there.
275    fn overhang(&self, hole: PxRect) -> Option<PxRect> {
276        let base = self.base?;
277        let cover = hole.intersect(&base)?;
278        let seen = self.clip;
279        // The low and the high edge of the stretched cover along one axis.
280        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    /// This sprite coloured as `paint`.
295    #[must_use]
296    pub const fn painted(self, paint: Paint) -> Self {
297        Self { paint, ..self }
298    }
299
300    /// The part of `src` that maps to `clip`, as `[x, y, w, h]` in image
301    /// pixels (fractions of a pixel when `clip` cuts through a scaled
302    /// one). With `flip_x`, the left of `clip` shows the right of `src`.
303    pub fn clipped_src(&self) -> [f32; 4] {
304        clipped_src(self.src, self.dest, self.clip, self.flip_x)
305    }
306}
307
308/// The part of `src` that maps to `clip` when `src` is stretched to
309/// `dest` (mirrored if `flip_x`), as `[x, y, w, h]` in the picture's
310/// pixels: [`Sprite::clipped_src`], and a painted picture's.
311#[allow(clippy::cast_precision_loss)] // pixel coordinates are small
312pub(crate) fn clipped_src(src: PxRect, dest: PxRect, clip: PxRect, flip_x: bool) -> [f32; 4] {
313    let wide = |v: i32| i64::from(v);
314    // From `dest`'s edge to `clip`'s, on the side `src` starts from.
315    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/// Something drawn on top of the cell grid, in console pixels. More kinds
332/// may come: a `match` on it has a `_` arm.
333#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
334#[non_exhaustive]
335pub enum Item {
336    /// A solid rectangle.
337    Rect(Overlay),
338    /// A picture from an image file.
339    Sprite(Sprite),
340    /// A picture a skin painted at run time (ADR-0078), from the frame's
341    /// [`PaintedTable`].
342    Painted(PaintedSprite),
343    /// A shape of the debug view (ADR-0085), always over the glyphs.
344    Gizmo(GizmoItem),
345}
346
347impl Item {
348    /// Under or over the glyphs.
349    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    /// The console pixels the item is drawn in: a rectangle's `rect`, a
359    /// sprite's, a painted picture's or a gizmo's `clip`.
360    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    /// What goes of the item, beyond `hole` itself, when the pixels
370    /// `hole` are replaced: a sprite's [`overhang`](Sprite::overhang).
371    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    /// The item drawn only in `visible`.
379    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
389/// `r` moved by `(dx, dy)`, or `None` if it no longer fits in `i32`.
390fn 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
396/// The part of `r`, moved by `(dx, dy)`, that lies inside `bounds` (which
397/// starts at the origin), or `None` if nothing does.
398fn offset_clipped(r: PxRect, dx: i64, dy: i64, bounds: PxRect) -> Option<PxRect> {
399    // Clip in i64 first: the offset can push `x` past `i32`.
400    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
411/// The pixel rectangle covered by cells `x..x + w`, `y..y + h` of a
412/// `spec` console, or `None` if it doesn't fit in `i32` (only far off any
413/// buffer).
414fn 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
425/// The parts of `r` outside `hole` (up to four rectangles).
426fn subtract(r: Rect, hole: Rect) -> Vec<Rect> {
427    let Some(i) = r.intersect(&hole) else {
428        return vec![r];
429    };
430    // Every edge below lies within `r`, so none of this overflows.
431    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/// Border style for [`GlyphBuffer::draw_box`] (game ADR-0012: single for panels,
445/// double for focus/modal).
446#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
447pub enum BoxStyle {
448    /// `┌─┐│└┘`
449    Single,
450    /// `╔═╗║╚╝`
451    Double,
452}
453
454impl BoxStyle {
455    /// Glyphs: top-left, top-right, bottom-left, bottom-right, horizontal, vertical.
456    const fn glyphs(self) -> [char; 6] {
457        match self {
458            BoxStyle::Single => ['┌', '┐', '└', '┘', '─', '│'],
459            BoxStyle::Double => ['╔', '╗', '╚', '╝', '═', '║'],
460        }
461    }
462}
463
464/// The part of `start..start + len` inside `0..limit`.
465fn 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    // Both lie in 0..=u16::MAX, so they fit in i32.
470    let fit = |v: i64| i32::try_from(v).unwrap_or(0);
471    fit(lo)..fit(hi)
472}
473
474/// A grid of [`Cell`]s, row-major.
475#[derive(Clone, PartialEq, Debug)]
476pub struct GlyphBuffer {
477    /// Its size in cells and the size of a cell.
478    spec: ConsoleSpec,
479    cells: Vec<Cell>,
480    /// In drawing order; each one's [`Item::visible`] lies inside
481    /// [`pixel_bounds`](Self::pixel_bounds).
482    items: Vec<Item>,
483    /// The scene behind the see-through cells, if any.
484    backdrop: Option<Backdrop>,
485    /// Which characters take two cells.
486    widths: CellWidths,
487    /// The pictures skins paint (ADR-0078): kept when the frame is drawn
488    /// again, so the frame's owner keeps them between frames.
489    pictures: PaintedTable,
490}
491
492impl GlyphBuffer {
493    /// A `width × height` buffer of 8 × 16-pixel cells
494    /// ([`ConsoleSpec::text`]) with every cell set to `fill`. Nothing is
495    /// wide in it ([`with_widths`](Self::with_widths)).
496    pub fn new(width: u16, height: u16, fill: Cell) -> Self {
497        Self::with_spec(ConsoleSpec::text(width, height), fill)
498    }
499
500    /// A buffer the size of a `spec` console, with every cell set to
501    /// `fill`. Nothing is wide in it ([`with_widths`](Self::with_widths)).
502    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    /// The same buffer, printing the characters `widths` has wide in two
514    /// cells: those of the font atlas it is drawn with
515    /// ([`CellWidths::of`]).
516    #[must_use]
517    pub fn with_widths(mut self, widths: CellWidths) -> Self {
518        self.widths = widths;
519        self
520    }
521
522    /// Prints the characters `widths` has wide in two cells from now on;
523    /// what is already printed stays.
524    pub fn set_widths(&mut self, widths: CellWidths) {
525        self.widths = widths;
526    }
527
528    /// Which characters it prints in two cells: what text drawn into it is
529    /// measured with ([`text_width`](shuyi_assets::lang::text_width)).
530    pub fn widths(&self) -> &CellWidths {
531        &self.widths
532    }
533
534    /// How many cells `text` takes here ([`text_width`] with
535    /// [`widths`](Self::widths)), as an `i32` for a position (saturated).
536    ///
537    /// [`text_width`]: shuyi_assets::lang::text_width
538    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    /// Its size in cells and the size of a cell, in pixels.
544    pub fn spec(&self) -> ConsoleSpec {
545        self.spec
546    }
547
548    /// The scene shown behind the see-through cells, if one is set.
549    pub fn backdrop(&self) -> Option<&Backdrop> {
550        self.backdrop.as_ref()
551    }
552
553    /// Shows `scene` behind the see-through cells of `clip` (console
554    /// cells, clipped to the buffer), replacing any backdrop already set:
555    /// the scene pixel `origin_px` is at the top-left of `clip`, and each
556    /// scene pixel is `zoom` console pixels big (1 to [`MAX_ZOOM`]; other
557    /// values are clamped). A `clip` wholly outside the buffer (or empty)
558    /// removes the backdrop. A backdrop `scene` has of its own is not drawn.
559    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    /// The pictures skins paint (ADR-0078), which its painted items show.
573    pub fn pictures(&self) -> &PaintedTable {
574        &self.pictures
575    }
576
577    /// The pictures to paint: what a skin writes a picture's pixels in
578    /// before it places it ([`add_painted`](Self::add_painted)). Drawing
579    /// the buffer again keeps them.
580    pub fn pictures_mut(&mut self) -> &mut PaintedTable {
581        &mut self.pictures
582    }
583
584    /// Removes the backdrop.
585    pub fn clear_backdrop(&mut self) {
586        self.backdrop = None;
587    }
588
589    /// The whole buffer in console pixels.
590    pub fn pixel_bounds(&self) -> PxRect {
591        let (w, h) = self.spec.px();
592        Rect::new(0, 0, w, h)
593    }
594
595    /// The rectangles and sprites, in drawing order (within a layer, later
596    /// ones on top).
597    pub fn items(&self) -> &[Item] {
598        &self.items
599    }
600
601    /// Only the rectangles, in drawing order.
602    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    /// Only the sprites, in drawing order.
611    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    /// Only the painted pictures' items, in drawing order.
620    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    /// Adds `overlay`, clipped to the buffer; one wholly outside (or empty)
629    /// is dropped.
630    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    /// Adds `sprite` with its `clip` clipped to its `dest` and to the
637    /// buffer; `src` and `dest` are kept as given. One with nothing to show
638    /// (wholly outside, or an empty `src`, `dest` or `clip`) is dropped.
639    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    /// Adds `painted` with its `clip` clipped to its `dest` and to the
653    /// buffer, as [`add_sprite`](Self::add_sprite) does; one with nothing
654    /// to show is dropped. Its picture is looked up in
655    /// [`pictures`](Self::pictures) when the frame is drawn: write it
656    /// before or after, but in the same frame.
657    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    /// The debug view's gizmos, in drawing order.
672    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    /// Adds `gizmo` over everything drawn so far, its `clip` clipped to its
681    /// shape's [`bounds`](GizmoShape::bounds) (but a label's, which is the
682    /// cells it was printed in) and to the buffer; one with nothing to show
683    /// is dropped.
684    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    /// Copies into this buffer's pictures those that `other`'s painted
695    /// items show (ADR-0078 §3).
696    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    /// Removes the parts of items over the cells of `rect`: the cells there
707    /// were just replaced, and items belong to the cells they were drawn
708    /// with. A sprite is split into up to four with smaller `clip`s; one
709    /// with a `base` also loses what it shows past the base's edges
710    /// beside the cells replaced.
711    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    /// After `other` was copied into `target` from column `dest_x`: a wide
735    /// character whose other half was left behind, off this buffer,
736    /// becomes a space.
737    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            // The source's cell after the last one copied: in `other`'s
747            // `u16` range, so no overflow.
748            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    /// Width in cells.
760    pub fn width(&self) -> u16 {
761        self.spec.cols
762    }
763
764    /// Height in cells.
765    pub fn height(&self) -> u16 {
766        self.spec.rows
767    }
768
769    /// The whole buffer as a rectangle.
770    pub fn bounds(&self) -> Rect {
771        Rect::new(0, 0, i32::from(self.spec.cols), i32::from(self.spec.rows))
772    }
773
774    /// Index of `(x, y)` in `cells`, or `None` if out of bounds.
775    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    /// The cell at `(x, y)`, or `None` if out of bounds.
782    pub fn get(&self, x: i32, y: i32) -> Option<&Cell> {
783        self.index(x, y).map(|i| &self.cells[i])
784    }
785
786    /// Mutable access to the cell at `(x, y)`, or `None` if out of bounds.
787    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    /// Sets the cell at `(x, y)`; out of bounds is silently ignored. A wide
792    /// character's other half, if this was one, becomes a space.
793    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    /// Before cells `xs` of row `y` (inside the buffer) are replaced: the
803    /// half outside them of a wide character cut by their edges becomes a
804    /// space, so no continuation is left without its first half and no
805    /// first half without its continuation.
806    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    /// Calls `f` on every cell of `rect` that lies inside the buffer.
818    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    /// [`unpair`](Self::unpair)s each row of `rect`, before its cells are
829    /// replaced.
830    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    /// Writes `text` left to right from `(x, y)`, clipped to the buffer:
841    /// one `char` per cell, and a wide one ([`widths`](Self::widths)) in
842    /// two, its second a continuation in the same colours. A no-break
843    /// space ([`NO_BREAK_SPACE`]) is written as a space. A wide one
844    /// that the buffer's edge would cut isn't drawn. Returns how many
845    /// cells were written.
846    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    /// Like [`print`](Self::print) but keeps each cell's background.
851    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                // Inside the buffer: below `columns`, a `u16`.
880                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    /// Each cell's glyph as a renderer draws it, row by row: `(x, y, cell,
896    /// wide)`, where `wide` says it is drawn over this cell and the next
897    /// (the continuation of a wide character). Continuations are left out.
898    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    /// Sets every cell of `rect` to `cell`, removing the parts of items
910    /// (rectangles and sprites) over it. Filling the whole buffer with a
911    /// solid cell (how a frame is cleared) also removes the backdrop.
912    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    /// Draws the border of `rect` in `style`; the interior is untouched.
922    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    /// Tints the background of every cell in `rect` towards `color` by `t`
950    /// (`0` = unchanged, `1` = `color`); used for range overlays.
951    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    /// Scales foreground and background of every cell in `rect` by `factor`
956    /// (e.g. to dim an inactive portrait). Cells only: rectangles and
957    /// sprites over them are not touched (a sprite is dimmed by its
958    /// `opacity`).
959    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    /// Darkens the whole frame towards black by `brightness` (1 = as it
967    /// is, 0 = black; clamped, and not a number counts as 1): every cell's
968    /// colours, every rectangle's colour, every sprite's `brightness`, and
969    /// the backdrop's scene the same way. Unlike [`dim`](Self::dim) it
970    /// leaves nothing out (game ADR-0060, game ADR-0075).
971    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)] // 0..=255
989                    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    /// Copies all of `other` so its top-left lands at `(dest_x, dest_y)`,
1000    /// clipped to this buffer. Items already over the copied area are
1001    /// removed, and `other`'s items come along, offset and clipped (a
1002    /// sprite's `dest` moves with it, its `clip` is clipped). Both buffers
1003    /// have cells of the same size. A wide character comes whole or not
1004    /// at all: one this buffer's edge cuts becomes a space.
1005    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            // In-bounds of `target`, so the source offsets are in `other`.
1015            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                    // A `dest` whose visible part is on the buffer fits.
1034                    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    /// The glyphs of row `y` as a string.
1082    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    /// A `w × h` buffer where `あ` and `い` are wide.
1221    fn wide_buf(w: u16, h: u16) -> GlyphBuffer {
1222        buf(w, h).with_widths(CellWidths::wide(['あ', 'い']))
1223    }
1224
1225    /// Row `y` as text: a continuation is `+`, so a test sees where one is.
1226    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        // Measured as it is printed.
1246        assert_eq!(b.text_cells("あA"), 3);
1247        assert_eq!(buf(4, 1).text_cells("あA"), 2);
1248        // A narrow buffer prints it in one cell, as before.
1249        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        // The first half.
1270        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        // The continuation.
1275        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        // A wide one half over another.
1280        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        // The colours of the half left stay: only its glyph goes.
1285        assert_eq!(b.get(3, 0), Some(&Cell::new(' ', RED, BLUE)));
1286        // Set, a fill and a box edge unpair too.
1287        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        // Colours alone keep the pair.
1299        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        // Cut on the left: its continuation alone is not copied.
1316        let mut b = wide_buf(4, 1);
1317        b.blit(&src, -1, 0);
1318        assert_eq!(halves(&b, 0), " い+ ");
1319        // Cut on the right: its first half alone is not either.
1320        let mut b = wide_buf(4, 1);
1321        b.blit(&src, 1, 0);
1322        assert_eq!(halves(&b, 0), " あ+ ");
1323        // Cut on the right below the top row, its source's cell before
1324        // the cut not a continuation: the first half alone still goes.
1325        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        // Over a pair of the buffer's own, cutting it.
1331        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        // First: x 0..16 → -8..8, y 14..16 → -2..0: gone. Second: 0..8, 0..16.
1526        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        // Off-buffer and empty fills leave overlays alone.
1554        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        // Blitting over cells replaces their overlays with the source's.
1559        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    /// Asserts every cell outside `target` is unchanged.
1608    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        /// However narrow and wide text is printed, filled and blitted, a
1642        /// continuation always follows its first half.
1643        #[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}