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