Skip to main content

retroglyph_core/surface/draw/
mod.rs

1//! Private geometry and single-cell write helpers shared by the [`draw`](super) submodules.
2
3use crate::color::{Style, Tint};
4#[cfg(not(feature = "egc"))]
5use crate::tile::Tile;
6#[cfg(not(feature = "egc"))]
7use unicode_width::UnicodeWidthChar;
8
9use super::Surface;
10
11mod cells;
12mod spans;
13mod text;
14
15impl Surface<'_> {
16    /// Shifts `(x, y)` by this surface's translate offset (see [`translate`](Self::translate)),
17    /// returning the coordinate to actually write at if the shift still lands inside this
18    /// surface's own clip, or `None` otherwise.
19    ///
20    /// The subtracted result is a coordinate local to this surface's area (`(0, 0)` is the
21    /// area's own top-left, matching [`put_signed`](Self::put_signed)'s convention), not an
22    /// absolute grid coordinate: a local check against `(0, 0)..(width, height)` here, followed
23    /// by re-adding [`area`](Self::area)'s own top-left, so a clipped area that does not itself
24    /// start at grid `(0, 0)` (e.g. a scrolling-camera widget's `clip_translate`-based
25    /// `surface` method) still resolves to the right absolute cell. The result is then checked
26    /// against [`clip_rect`](Self::clip_rect), not `area`, since the clip, never the area, is
27    /// what decides whether a write lands.
28    pub(super) fn shift(&self, x: u16, y: u16) -> Option<(u16, u16)> {
29        self.shift_signed(i32::from(x), i32::from(y))
30    }
31
32    /// [`shift`](Self::shift), taking `(x, y)` as signed coordinates that may already be
33    /// negative before the offset is even subtracted: [`put_signed`](Self::put_signed)'s own
34    /// entry point, where a caller's arithmetic (e.g. a scrolling camera) can go negative
35    /// relative to the viewport before this surface's `origin_offset` is applied at all, a case
36    /// `shift`'s `u16` parameters cannot express.
37    ///
38    /// A `checked_sub` failure (the signed offset arithmetic overflowing `i32`) is treated the
39    /// same as a shifted coordinate landing outside this surface, matching `shift`'s own
40    /// out-of-bounds handling: both are just a `None`.
41    pub(super) fn shift_signed(&self, x: i32, y: i32) -> Option<(u16, u16)> {
42        let sx = x.checked_sub(self.origin_offset.0)?;
43        let sy = y.checked_sub(self.origin_offset.1)?;
44        if sx < 0 || sy < 0 {
45            return None;
46        }
47        let sx = u16::try_from(sx).ok()?;
48        let sy = u16::try_from(sy).ok()?;
49        if sx >= self.area.width() || sy >= self.area.height() {
50            return None;
51        }
52        let gx = self.area.left() + sx;
53        let gy = self.area.top() + sy;
54        self.clip.contains(gx, gy).then_some((gx, gy))
55    }
56
57    /// The exclusive right column, in this surface's own (possibly translated) coordinate space,
58    /// past which `print`/`print_line` stop a row: they wrap onto the next row, or skip the
59    /// remaining spans, once the cursor reaches it.
60    ///
61    /// `shift` subtracts `origin_offset` from every incoming coordinate before bounds-checking it,
62    /// so the cursor the text writers advance lives in that shifted space. The threshold has to
63    /// live there too, or a translated surface (any `Camera::surface`, or a plain `translate`)
64    /// wraps or skips early by exactly the offset. The result is `i64` because folding the offset
65    /// back into a `u16` clip edge can land outside the `u16` range in either direction.
66    pub(super) fn wrap_right(&self) -> i64 {
67        i64::from(self.clip.right()) - i64::from(self.area.left()) + i64::from(self.origin_offset.0)
68    }
69
70    /// Applies this surface's tint to the cell just written at `(x, y)`.
71    ///
72    /// Called after a write rather than as part of one, because a glyph write drops whatever
73    /// tint the cell held (see [`Grid::set_tint`]); doing it in the other order would erase the
74    /// tint being applied. Untinted surfaces skip the call entirely, so the ordinary text path
75    /// never touches the side table.
76    pub(super) fn apply_tint(&mut self, x: u16, y: u16) {
77        if self.tint != Tint::None {
78            self.grid.set_tint(self.layer, x, y, self.tint);
79        }
80    }
81
82    /// Writes `grapheme` (already a single extended grapheme cluster, e.g. an emoji plus a
83    /// variation selector, a combining sequence, or a flag) at `(x, y)`, in this surface's own
84    /// local coordinate space (matching [`put`](Self::put)'s convention). A no-op if out of this
85    /// surface's clip.
86    ///
87    /// A 2-column grapheme also needs its spacer cell (`x + 1`) inside the clip: `shift` only
88    /// checks the primary cell, and `Grid::write_grapheme` only refuses the spacer at the
89    /// *grid*'s own edge, not the clip's, so without this the spacer would land one column past
90    /// the clip. Refusing the whole write here (rather than writing a primary cell with no
91    /// spacer) matches this surface's span-writing methods' reasoning: a footprint half outside
92    /// the clip would reserve a cell the caller does not own.
93    ///
94    /// Only present when the `egc` feature is enabled: without it, a `char` (as [`put`](Self::put)
95    /// already takes) is the only glyph unit this surface can address.
96    ///
97    /// # Examples
98    ///
99    /// ```
100    /// use retroglyph_core::color::Style;
101    /// use retroglyph_core::grid::{Grid, Pos, Rect};
102    /// use retroglyph_core::surface::Surface;
103    ///
104    /// let mut grid = Grid::new(4, 4);
105    /// let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0);
106    ///
107    /// // A combining sequence: 'e' followed by U+0301 COMBINING ACUTE ACCENT.
108    /// surface.put_grapheme(1, 1, "e\u{0301}", Style::default());
109    ///
110    /// assert_eq!(grid[Pos::new(1, 1)].glyph(), 'e');
111    /// ```
112    #[cfg(feature = "egc")]
113    pub fn put_grapheme(&mut self, x: u16, y: u16, grapheme: &str, style: Style) {
114        let Some((x, y)) = self.shift(x, y) else {
115            return;
116        };
117        self.put_grapheme_at(x, y, grapheme, style);
118    }
119
120    /// Writes `grapheme` at the already-*absolute* grid coordinate `(x, y)` (post-[`shift`],
121    /// or an equivalent translation a caller had to do by hand): the width-2 spacer-in-clip
122    /// check, [`Grid::write_grapheme`]'s wide-char bookkeeping, and this surface's tint.
123    ///
124    /// Shared by [`put_grapheme`](Self::put_grapheme) and [`put_char_at`](Self::put_char_at)
125    /// (the latter only under `egc`, on behalf of every plain-`char` writer: `put`, `put_signed`,
126    /// `put_offset`), both of which already have an *absolute* coordinate in hand and would
127    /// otherwise have to repeat the wide-spacer check, grid write, and tint themselves.
128    ///
129    /// Returns whether the write actually landed, so a caller that also needs to touch the
130    /// written tile afterward (e.g. `put_offset` setting a pixel offset) can tell a refused write
131    /// apart from a successful one instead of blindly poking whatever tile is already at
132    /// `(x, y)`. [`Grid::write_grapheme`] can refuse on its own (e.g. `(x, y)` outside the grid,
133    /// reachable when this surface's clip/area is wider than the grid itself), distinct from the
134    /// clip check above, so `apply_tint` is gated on its own `bool` too rather than assumed to
135    /// always land once `wide_spacer_fits` passes.
136    #[cfg(feature = "egc")]
137    pub(super) fn put_grapheme_at(&mut self, x: u16, y: u16, grapheme: &str, style: Style) -> bool {
138        use unicode_width::UnicodeWidthStr;
139
140        if !self.wide_spacer_fits(x, y, grapheme.width()) {
141            return false;
142        }
143        let wrote = self
144            .grid
145            .write_grapheme(self.layer, x, y, grapheme, style)
146            .is_some();
147        if wrote {
148            self.apply_tint(x, y);
149        }
150        wrote
151    }
152
153    /// `true` unless `width` is 2 and the spacer cell it would need at `x + 1` falls outside
154    /// this surface's clip.
155    ///
156    /// [`Grid::put_tile`]/[`Grid::write_grapheme`] only refuse a wide write at the *grid*'s own
157    /// right edge, not the clip's, so every wide write site (both the `egc` grapheme path via
158    /// [`put_grapheme_at`](Self::put_grapheme_at) and the plain-`char` path in
159    /// [`put`](Self::put)/[`put_signed`](Self::put_signed)/[`put_offset`](Self::put_offset))
160    /// calls this first: without it, a clip narrower than the surface's own area would let a
161    /// wide glyph's spacer land one column past the clip, silently overwriting whatever is
162    /// there.
163    pub(super) fn wide_spacer_fits(&self, x: u16, y: u16, width: usize) -> bool {
164        width != 2 || self.clip.contains(x.saturating_add(1), y)
165    }
166
167    /// Writes `ch` at the already-*absolute* grid coordinate `(x, y)` (post-[`shift`]/
168    /// [`shift_signed`]): the single glyph-write sequence every one of this surface's per-cell
169    /// writers shares, gated on the `egc` feature since a grapheme cluster and a plain `char`
170    /// need different [`Grid`] write calls.
171    ///
172    /// With `egc` enabled, a `char` is just a one-codepoint grapheme, so this defers to
173    /// [`put_grapheme_at`](Self::put_grapheme_at) rather than repeating its wide-spacer check,
174    /// grid write, and tint. Without it, the sequence is inlined directly: `wide_spacer_fits`,
175    /// then [`Grid::put_tile`], then [`apply_tint`](Self::apply_tint) gated on the write having
176    /// actually landed (`put_tile` can still refuse, e.g. out-of-grid).
177    ///
178    /// Returns whether the write landed, same reason [`put_grapheme_at`](Self::put_grapheme_at)
179    /// does: `put_offset` needs to tell a refused write apart from a successful one before
180    /// touching the tile's pixel offset. Callers that don't need that (`put`, `put_signed`)
181    /// simply discard it, so this is deliberately not `#[must_use]`.
182    pub(super) fn put_char_at(&mut self, x: u16, y: u16, ch: char, style: Style) -> bool {
183        #[cfg(feature = "egc")]
184        {
185            let mut buf = [0u8; 4];
186            let s = ch.encode_utf8(&mut buf);
187            self.put_grapheme_at(x, y, s, style)
188        }
189        #[cfg(not(feature = "egc"))]
190        {
191            if !self.wide_spacer_fits(x, y, ch.width().unwrap_or(1)) {
192                return false;
193            }
194            let wrote = self
195                .grid
196                .put_tile(self.layer, (x, y), Tile::new(ch, style))
197                .is_some();
198            if wrote {
199                self.apply_tint(x, y);
200            }
201            wrote
202        }
203    }
204}