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}