Skip to main content

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