Skip to main content

retroglyph_core/surface/draw/
spans.rs

1//! Multi-cell sprite spans: [`put_span`](Surface::put_span) and its uniform/pixel-offset twins.
2
3use crate::color::Style;
4use crate::grid::{HasSize, Offset, Pos, Size};
5
6use super::Surface;
7
8impl Surface<'_> {
9    /// Writes a multi-cell span at `pos` on this surface's layer in `style`: one piece of
10    /// artwork occupying a block of cells rather than one, the [`Surface`] twin of
11    /// [`Grid::write_span`](crate::grid::Grid::write_span).
12    ///
13    /// `rows` holds one string per row of the footprint. Its first character is the **anchor**
14    /// glyph, which a pixel backend looks up in its sprite cache; the rest are the span's **text
15    /// fallback**, printed by cell backends and skipped by pixel backends. Any `AsRef<str>` row
16    /// works, so a literal footprint (`&["[==]", "|__|"]`) and a computed one (`&Vec<String>`)
17    /// both pass without a borrowing pass over the rows; for the uniform case, see
18    /// [`put_span_uniform`](Self::put_span_uniform).
19    ///
20    /// See [`Grid::write_span`](crate::grid::Grid::write_span) for the full write semantics, and
21    /// [`Grid::span_owner`](crate::grid::Grid::span_owner) to hit-test the whole footprint.
22    ///
23    /// # `style` applies to the text fallback, not to the sprite
24    ///
25    /// A sprite is composited from its own pixels. [`style.fg`](Style::fg) does not tint it;
26    /// `style.bg` is still painted behind it, so it shows through wherever the sprite is
27    /// transparent. Recoloring a shared sprite per cell is therefore not possible: draw a
28    /// variant of the artwork instead, which is the usual tileset idiom.
29    ///
30    /// `style` is not dead on such a cell, because the same span drawn by a *cell* backend
31    /// renders the text fallback in it. The consequence is that `fg` reads very differently
32    /// depending on the backend, and that a glyph missing from the sprite cache silently falls
33    /// back to a font glyph that *is* `fg`-colored, which looks a lot like a tint working.
34    ///
35    /// # Returns
36    ///
37    /// `Some(())` once the whole span is written, or `None` having written nothing at all when
38    /// `rows` is empty or ragged, either axis exceeds 255 cells, or the footprint does not fit
39    /// entirely within this surface's own clip (not just the grid) at `pos`. The surface has
40    /// strictly more ways to refuse a span than
41    /// [`Grid::write_span`](crate::grid::Grid::write_span) does, so a sprite that did not draw is
42    /// answered here rather than in the backend.
43    pub fn put_span<S: AsRef<str>>(
44        &mut self,
45        pos: impl Into<Pos>,
46        rows: &[S],
47        style: Style,
48    ) -> Option<()> {
49        let pos = pos.into();
50        let (x, y) = self.shift(pos.x, pos.y)?;
51        let cols = rows.first()?.as_ref().chars().count();
52        let w = u16::try_from(cols).ok()?;
53        let h = u16::try_from(rows.len()).ok()?;
54        if !self.span_fits(Pos::new(x, y), w, h) {
55            return None;
56        }
57        self.grid.write_span(self.layer, x, y, rows, style)?;
58        // The anchor only: a pixel backend draws the whole footprint from that one cell, so the
59        // covered cells have no sprite of their own to recolour.
60        self.apply_tint(x, y);
61        Some(())
62    }
63
64    /// Writes a `size` multi-cell span at `pos` on this surface's layer in `style`: `anchor` in
65    /// the anchor cell, `fill` in every other cell of the footprint, the [`Surface`] twin of
66    /// [`Grid::write_span_uniform`](crate::grid::Grid::write_span_uniform).
67    ///
68    /// The uniform case of [`put_span`](Self::put_span), and what a sheet-driven renderer usually
69    /// wants: one sprite, chosen at runtime, with the cells it covers blanked so nothing shows
70    /// through its transparent pixels. `fill` is the text fallback a *cell* backend prints for
71    /// those covered cells, so `' '` blanks them and a visible character keeps the footprint
72    /// legible in a terminal.
73    ///
74    /// `style` reads exactly as it does for [`put_span`](Self::put_span): it applies to the text
75    /// fallback, never to the sprite.
76    ///
77    /// # Returns
78    ///
79    /// `Some(())` once the whole span is written, or `None` having written nothing at all when
80    /// either axis of `size` is `0` or exceeds 255 cells, or the footprint does not fit entirely
81    /// within this surface's own clip at `pos`.
82    ///
83    /// # Examples
84    ///
85    /// ```
86    /// # fn main() {
87    /// # fn run() -> Option<()> {
88    /// use retroglyph_core::color::Style;
89    /// use retroglyph_core::grid::{Grid, Rect};
90    /// use retroglyph_core::surface::Surface;
91    ///
92    /// let mut grid = Grid::new(8, 4);
93    /// let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 8, 4), 0);
94    ///
95    /// // A 16x16 sprite over a 2x1 block of 8x16 cells, anchored at a runtime glyph.
96    /// let anchor = '\u{E000}';
97    /// surface.put_span_uniform((1, 1), (2, 1), anchor, ' ', Style::default())?;
98    /// # Some(())
99    /// # }
100    /// # run().unwrap();
101    /// # }
102    /// ```
103    pub fn put_span_uniform(
104        &mut self,
105        pos: impl Into<Pos>,
106        size: impl Into<Size>,
107        anchor: char,
108        fill: char,
109        style: Style,
110    ) -> Option<()> {
111        let pos = pos.into();
112        let (x, y) = self.shift(pos.x, pos.y)?;
113        let pos = Pos::new(x, y);
114        let size = size.into();
115        if !self.span_fits(pos, size.width(), size.height()) {
116            return None;
117        }
118        self.grid
119            .write_span_uniform(self.layer, pos, size, anchor, fill, style)?;
120        self.apply_tint(pos.x, pos.y);
121        Some(())
122    }
123
124    /// `true` if a `w` x `h` footprint at `pos` lies entirely within this surface's clip.
125    ///
126    /// A span is all-or-nothing rather than clipped like the per-cell writes, because a
127    /// footprint half outside the clip would reserve cells the caller does not own.
128    fn span_fits(&self, pos: Pos, w: u16, h: u16) -> bool {
129        pos.x >= self.clip.left()
130            && pos.y >= self.clip.top()
131            && pos.x.saturating_add(w) <= self.clip.right()
132            && pos.y.saturating_add(h) <= self.clip.bottom()
133    }
134
135    /// Place `ch` at `pos` with a sub-cell pixel `offset`, in `style`.
136    ///
137    /// Sub-cell offsets are visual only: they do not affect grid logic or hit-testing.
138    /// Backends that cannot represent pixel offsets (e.g. `CrosstermBackend`) ignore them. A
139    /// no-op if `pos` is outside this surface's clip.
140    ///
141    /// # Examples
142    ///
143    /// ```
144    /// use retroglyph_core::color::Style;
145    /// use retroglyph_core::grid::{Grid, Offset, Pos, Rect};
146    /// use retroglyph_core::surface::Surface;
147    ///
148    /// let mut grid = Grid::new(4, 4);
149    /// let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0);
150    ///
151    /// // A large offset still lands the glyph in cell (1, 1): the offset is a pixel nudge
152    /// // for a pixel backend, never a coordinate shift.
153    /// surface.put_offset((1, 1), Offset::new(12, -12), 'X', Style::default());
154    /// // Outside the surface's clip: silently dropped, matching `put`.
155    /// surface.put_offset((10, 10), Offset::default(), 'X', Style::default());
156    ///
157    /// assert_eq!(grid[Pos::new(1, 1)].glyph(), 'X');
158    /// ```
159    pub fn put_offset(
160        &mut self,
161        pos: impl Into<Pos>,
162        offset: impl Into<Offset>,
163        ch: char,
164        style: Style,
165    ) {
166        let pos = pos.into();
167        let Some((x, y)) = self.shift(pos.x, pos.y) else {
168            return;
169        };
170        let offset = offset.into();
171        let wrote = self.put_char_at(x, y, ch, style);
172        // A refused write (e.g. a wide glyph whose spacer falls outside the clip, or
173        // `put_tile` declining an out-of-grid/unallocatable-layer write) leaves `(x, y)`
174        // holding whatever tile a *different* draw call put there. Setting the offset on it
175        // would move a cell this call never touched, so bail out before `tile_mut` below.
176        if !wrote {
177            return;
178        }
179        // The offset is a pixel nudge on the tile the write above just landed, not part of
180        // `write_grapheme`'s contract (it has no offset parameter): set it directly via
181        // `tile_mut` rather than widening `Grid`'s public write API for a `Surface`-only concern.
182        if let Some(tile) = self.grid.tile_mut(self.layer, (x, y)) {
183            tile.dx = offset.dx;
184            tile.dy = offset.dy;
185        }
186    }
187}