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