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}