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}