retroglyph_core/surface/draw/cells.rs
1//! Single-cell and whole-region writes: [`put`](Surface::put) and its rect/grid-scale twins.
2
3use crate::color::{Style, Tint};
4use crate::grid::{Grid, Pos, Rect};
5use crate::tile::Tile;
6use unicode_width::UnicodeWidthChar;
7
8use super::Surface;
9
10impl Surface<'_> {
11 /// The whole-rect counterpart to [`shift`](Self::shift): translates a local `rect` (same
12 /// convention as `fill_rect`/`clear_region`, and as `shift`'s own `x`/`y`) into the absolute
13 /// grid rect it covers under this surface's own translate offset and clip, plus the
14 /// columns/rows cropped off its near (left/top) edge, or `None` if none of it lands.
15 ///
16 /// `rect`'s near edge is cropped rather than the whole call dropped whenever it starts left
17 /// of/above the origin after `origin_offset` is subtracted: there is no single coordinate for
18 /// [`shift`](Self::shift) to reject the way it does per cell, only part of `rect`'s footprint
19 /// may be off-screen. The returned crop is `rect`'s own near-edge crop plus whatever the clip
20 /// intersect trims beyond it, so a caller re-deriving a source rect (as
21 /// [`blit`](Self::blit) does) gets the total offset into `rect` in one number per axis.
22 ///
23 /// Never intersects with [`area`](Self::area) before the clip intersect: every surface
24 /// constructor in `geometry.rs` narrows `clip` to at most `area`, so `clip ⊆ area` always
25 /// holds and an extra area-bound intersect would be a no-op.
26 fn map_local_rect(&self, rect: Rect) -> Option<(Rect, (u16, u16))> {
27 let w = rect.width();
28 let h = rect.height();
29 if w == 0 || h == 0 {
30 return None;
31 }
32
33 let sx = i64::from(rect.left()) - i64::from(self.origin_offset.0);
34 let sy = i64::from(rect.top()) - i64::from(self.origin_offset.1);
35 let crop_left = u16::try_from(sx.min(0).unsigned_abs()).unwrap_or(u16::MAX);
36 let crop_top = u16::try_from(sy.min(0).unsigned_abs()).unwrap_or(u16::MAX);
37 if crop_left >= w || crop_top >= h {
38 return None;
39 }
40 let Ok(local_x) = u16::try_from(sx.max(0)) else {
41 return None;
42 };
43 let Ok(local_y) = u16::try_from(sy.max(0)) else {
44 return None;
45 };
46
47 let abs_x = self.area.left().saturating_add(local_x);
48 let abs_y = self.area.top().saturating_add(local_y);
49 let visible_w = (w - crop_left).min(u16::MAX - abs_x);
50 let visible_h = (h - crop_top).min(u16::MAX - abs_y);
51
52 let dst_rect = Rect::new(abs_x, abs_y, visible_w, visible_h).intersect(self.clip);
53 (!dst_rect.is_empty()).then(|| {
54 let crop = (
55 crop_left + (dst_rect.left() - abs_x),
56 crop_top + (dst_rect.top() - abs_y),
57 );
58 (dst_rect, crop)
59 })
60 }
61
62 /// Clips `rect` (in the same coordinate space as `fill_rect`/`clear_region`'s own `rect`
63 /// argument) to what can possibly land on this surface: `(0, 0)..(area.width, area.height)`
64 /// shifted by `origin_offset`, mirroring the subtraction [`shift`](Self::shift) applies per
65 /// cell.
66 ///
67 /// Both methods' per-cell fallback loop runs this first so the loop is bounded to at most
68 /// `area.width * area.height` cells regardless of how much larger `rect` is, rather than
69 /// iterating `rect`'s full width * height (up to ~4.3 billion cells for a `u16`-sized rect)
70 /// and relying on a per-cell check to skip what doesn't land.
71 ///
72 /// The intersection itself is [`Rect::intersect`], not hand-rolled per-field arithmetic,
73 /// widened to `i64` because `origin_offset` can push the shifted area below `0` or above
74 /// `u16::MAX`, neither of which `Rect<u16>` can represent; the result is narrowed back to
75 /// `u16` once [`intersect`](ixy::Rect::intersect) has already bounded it within `rect`'s own
76 /// (already-`u16`) extent.
77 fn clip_local_rect(&self, rect: Rect) -> Rect {
78 let bounds = ixy::Rect::<i64>::new(
79 i64::from(self.origin_offset.0),
80 i64::from(self.origin_offset.1),
81 i64::from(self.area.width()),
82 i64::from(self.area.height()),
83 );
84 let rect = ixy::Rect::<i64>::new(
85 i64::from(rect.left()),
86 i64::from(rect.top()),
87 i64::from(rect.width()),
88 i64::from(rect.height()),
89 )
90 .intersect(bounds);
91 // `intersect` only ever narrows `rect`'s own fields, which started out as `u16`, so
92 // these conversions never fail.
93 let left = u16::try_from(rect.left()).unwrap_or(u16::MAX);
94 let top = u16::try_from(rect.top()).unwrap_or(u16::MAX);
95 let width = u16::try_from(rect.width()).unwrap_or(u16::MAX);
96 let height = u16::try_from(rect.height()).unwrap_or(u16::MAX);
97 Rect::new(left, top, width, height)
98 }
99
100 /// Place `ch` at `pos` in `style`. A no-op if `pos` is outside this surface's clip.
101 ///
102 /// If a pixel backend resolves `ch` to a sprite, that sprite is composited from its own
103 /// pixels: [`style.fg`](Style::fg) does not tint it, and `style.bg` shows through only where
104 /// the sprite is transparent. See [`put_span`](Self::put_span).
105 ///
106 /// # Examples
107 ///
108 /// ```
109 /// use retroglyph_core::color::Style;
110 /// use retroglyph_core::grid::{Grid, Pos, Rect};
111 /// use retroglyph_core::surface::Surface;
112 ///
113 /// let mut grid = Grid::new(4, 4);
114 /// let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0);
115 ///
116 /// surface.put((1, 1), 'X', Style::default());
117 /// // Outside the surface's clip: silently dropped, not a panic.
118 /// surface.put((10, 10), 'X', Style::default());
119 ///
120 /// assert_eq!(grid[Pos::new(1, 1)].glyph(), 'X');
121 /// ```
122 pub fn put(&mut self, pos: impl Into<Pos>, ch: char, style: Style) {
123 let pos = pos.into();
124 let Some((x, y)) = self.shift(pos.x, pos.y) else {
125 return;
126 };
127 self.put_char_at(x, y, ch, style);
128 }
129
130 /// [`put`](Self::put), in coordinates relative to this surface's own area origin, where a
131 /// negative coordinate is expressible and simply falls outside (a no-op, matching `put`'s
132 /// out-of-bounds behavior). A coordinate that stays non-negative but exceeds `u16::MAX` after
133 /// this surface's translate offset is subtracted is dropped the same way: it addresses a cell
134 /// this surface's `u16` grid space cannot name.
135 ///
136 /// Scrolling/camera code (e.g. a viewport over a wider world) computes positions in a
137 /// coordinate space that can go negative relative to the viewport, which [`Pos`] (backed by
138 /// `u16`) cannot even express. `put_signed` takes that arithmetic directly, so a caller no
139 /// longer clip-tests by hand before calling `put`.
140 ///
141 /// # Examples
142 ///
143 /// ```
144 /// use retroglyph_core::color::Style;
145 /// use retroglyph_core::grid::{Grid, 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 /// // Negative in either axis: outside this surface's area, silently dropped.
152 /// surface.put_signed((-1, 1), 'X', Style::default());
153 /// // Non-negative and within bounds: lands like `put`.
154 /// surface.put_signed((1, 1), 'X', Style::default());
155 ///
156 /// assert_eq!(grid[Pos::new(1, 1)].glyph(), 'X');
157 /// assert_eq!(grid[Pos::new(0, 1)].glyph(), ' ');
158 /// ```
159 pub fn put_signed(&mut self, pos: (i32, i32), ch: char, style: Style) {
160 let (x, y) = pos;
161 let Some((x, y)) = self.shift_signed(x, y) else {
162 return;
163 };
164 self.put_char_at(x, y, ch, style);
165 }
166
167 /// Fill `rect` (clipped to this surface's own clip) with `ch` in `style`.
168 ///
169 /// `rect` is local to this surface's own [`area`](Self::area): `(0, 0)` is `area`'s own
170 /// top-left, not the grid's, the same convention [`clear_region`](Self::clear_region) and
171 /// [`print_aligned`](Self::print_aligned) use for their own `rect` (not absolute grid
172 /// coordinates, the convention [`clip`](Self::clip)/[`scope`](Self::scope) use).
173 ///
174 /// # Examples
175 ///
176 /// ```
177 /// use retroglyph_core::color::Style;
178 /// use retroglyph_core::grid::{Grid, Pos, Rect};
179 /// use retroglyph_core::surface::Surface;
180 ///
181 /// let mut grid = Grid::new(4, 4);
182 /// let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0);
183 ///
184 /// // `rect` extends well past the grid on both axes; only the cells inside the
185 /// // surface's own clip are touched, the rest is silently clipped.
186 /// surface.fill_rect(Rect::new(2, 2, 10, 10), '#', Style::default());
187 ///
188 /// assert_eq!(grid[Pos::new(3, 3)].glyph(), '#');
189 /// assert_eq!(grid[Pos::new(0, 0)].glyph(), ' ');
190 /// ```
191 pub fn fill_rect(&mut self, rect: Rect, ch: char, style: Style) {
192 // The batch path below writes a plain `Tile::new(ch, style)` per cell, which matches
193 // `put`'s own per-cell write only when there's no tint to apply and `ch` is a
194 // single-column glyph: `fill_rect` itself refuses (no-op) any `tile.width() != 1` (see
195 // its own doc comment), so this check just avoids paying for a delegation that would
196 // silently do nothing. Anything else (tinted surface, zero/double-width glyph) falls back
197 // to the per-cell loop, unchanged from before this method had a fast path.
198 let single_width = UnicodeWidthChar::width(ch) == Some(1);
199
200 if self.tint == Tint::None
201 && single_width
202 && let Some((abs, _)) = self.map_local_rect(rect)
203 {
204 self.grid.fill_rect(self.layer, abs, Tile::new(ch, style));
205 return;
206 }
207
208 let rect = self.clip_local_rect(rect);
209 for pos in rect {
210 self.put(pos, ch, style);
211 }
212 }
213
214 /// Stamps `grid`'s layer 0 onto this surface's own layer, with its top-left cell at `(x, y)`
215 /// (local to this surface's area, matching [`put`](Self::put)'s convention), clipped to this
216 /// surface's clip.
217 ///
218 /// Always reads `grid`'s layer 0, regardless of which layer this surface itself is currently
219 /// writing to: `grid` is typically a standalone buffer composed elsewhere (e.g.
220 /// `BoxStyle::render`'s output, or `retroglyph-ui`' `join_h`/`join_v`), and per their own
221 /// docs those only ever populate layer 0. Reading this surface's own layer off `grid` instead
222 /// (what [`Grid::blit`]'s single `layer` parameter would do if called directly) finds nothing
223 /// there whenever this surface isn't on layer 0, and the copy silently does nothing.
224 ///
225 /// Unlike a single-cell [`put`](Self::put), a write that starts outside this surface's clip is
226 /// not necessarily dropped whole: the part of `grid` that does land inside the clip is copied,
227 /// matching [`fill_rect`](Self::fill_rect)'s per-cell clipping rather than
228 /// [`put_span`](Self::put_span)'s all-or-nothing footprint check, since `grid` is arbitrary
229 /// composed content rather than one indivisible sprite.
230 ///
231 /// Unlike [`put`](Self::put) and the rest of this surface's single-sprite writes, this does
232 /// not apply [`with_tint`](Self::with_tint)'s tint: a tint lands on one sprite's anchor cell,
233 /// and `grid` is arbitrary composed content with no single anchor to land it on, the same
234 /// reason `Grid::blit_cross_layer` (this method's own cross-layer copy, internal to `Grid`)
235 /// carries no tint either. A tinted surface's `blit` copies `grid` through unchanged.
236 ///
237 /// # Examples
238 ///
239 /// ```
240 /// use retroglyph_core::color::Style;
241 /// use retroglyph_core::grid::{Grid, Rect};
242 /// use retroglyph_core::surface::{Layer, Surface};
243 /// use retroglyph_core::tile::Tile;
244 ///
245 /// let mut src = Grid::new(2, 2);
246 /// src.put_tile(0, (0, 0), Tile::new('x', Style::default()));
247 ///
248 /// let mut dst = Grid::new(4, 4);
249 /// let mut surface = Surface::new(&mut dst, Rect::new(0, 0, 4, 4), Layer::World.as_u8());
250 ///
251 /// // `surface` is on the overlay tier; `src` only ever has layer 0, but `blit` reads that
252 /// // layer regardless, so the copy still lands (unlike `Grid::blit(surface.layer(), ...)`).
253 /// surface.on_tier(Layer::Overlay).blit(&src, 1, 1);
254 ///
255 /// assert_eq!(dst.tile(Layer::Overlay.as_u8(), (1, 1)).map(Tile::glyph), Some('x'));
256 /// ```
257 pub fn blit(&mut self, grid: &Grid, x: u16, y: u16) {
258 let w = grid.width();
259 let h = grid.height();
260 if w == 0 || h == 0 {
261 return;
262 }
263
264 let Some((dst_rect, (crop_left, crop_top))) = self.map_local_rect(Rect::new(x, y, w, h))
265 else {
266 return;
267 };
268
269 let src_rect = Rect::new(crop_left, crop_top, dst_rect.width(), dst_rect.height());
270 self.grid.blit_cross_layer(
271 self.layer,
272 grid,
273 0,
274 src_rect,
275 dst_rect.left(),
276 dst_rect.top(),
277 );
278 }
279
280 /// Clears this surface's own area, intersected with its clip (on its own layer), back to
281 /// [`Tile::default`].
282 pub fn clear(&mut self) {
283 let region = self.area.intersect(self.clip);
284 self.grid.fill_rect(self.layer, region, Tile::default());
285 }
286
287 /// Clears `rect` (clipped to this surface's own clip, on its own layer) back to
288 /// [`Tile::default`].
289 ///
290 /// `rect` is local to this surface's own [`area`](Self::area), the same convention
291 /// [`fill_rect`](Self::fill_rect) and [`print_aligned`](Self::print_aligned) use for their
292 /// own `rect` (not absolute grid coordinates, the convention
293 /// [`clip`](Self::clip)/[`scope`](Self::scope) use).
294 ///
295 /// # Examples
296 ///
297 /// ```
298 /// use retroglyph_core::color::Style;
299 /// use retroglyph_core::grid::{Grid, Pos, Rect};
300 /// use retroglyph_core::surface::Surface;
301 ///
302 /// let mut grid = Grid::new(4, 4);
303 /// let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0);
304 /// surface.fill_rect(Rect::new(0, 0, 4, 4), '#', Style::default());
305 ///
306 /// // `rect` extends past the surface's own clip; only the overlap is cleared.
307 /// surface.clear_region(Rect::new(2, 2, 10, 10));
308 ///
309 /// assert_eq!(grid[Pos::new(2, 2)].glyph(), ' ');
310 /// assert_eq!(grid[Pos::new(1, 1)].glyph(), '#');
311 /// ```
312 pub fn clear_region(&mut self, rect: Rect) {
313 if let Some((abs, _)) = self.map_local_rect(rect) {
314 self.grid.fill_rect(self.layer, abs, Tile::default());
315 }
316 }
317}