retroglyph_core/surface/geometry.rs
1use crate::color::Style;
2use crate::color::Tint;
3use crate::grid::{Grid, Rect};
4
5use super::styled::StyledSurface;
6use super::{Layer, Surface};
7
8impl<'a> Surface<'a> {
9 /// The region this surface represents, e.g. for a widget to lay itself out in.
10 ///
11 /// Unlike [`clip_rect`](Self::clip_rect), this is never narrowed by [`clip`](Self::clip): it
12 /// only changes when [`scope`](Self::scope) sets a new one. A widget that reads its own area
13 /// off the surface after being clipped (e.g. while partially offscreen) sees the region it
14 /// was given, not the visible sliver of it, so it can still center itself correctly and let
15 /// the clip take care of what actually lands.
16 ///
17 /// Every drawing method on this surface ([`put`](Self::put), [`print`](Self::print),
18 /// [`fill_rect`](Self::fill_rect), ...) takes coordinates local to this surface, where
19 /// `(0, 0)` is this area's own top-left corner, not the underlying grid's. `area()` itself is
20 /// absolute grid space, so `surface.put((surface.area().left(), ...), ...)` only lands
21 /// correctly for a surface whose area happens to start at the grid origin; anywhere else it
22 /// silently misses. A widget that wants to place itself relative to its own bounds (e.g. a
23 /// label in a corner) should reach for `area().at_origin()`, or just [`width`](Self::width)/
24 /// [`height`](Self::height) directly, and never for `area()`'s own [`left`](Rect::left)/
25 /// [`top`](Rect::top).
26 ///
27 /// # Examples
28 ///
29 /// ```
30 /// use retroglyph_core::grid::{Grid, Rect};
31 /// use retroglyph_core::surface::Surface;
32 ///
33 /// let mut grid = Grid::new(10, 10);
34 /// let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 10, 10), 0);
35 /// let mut scoped = surface.scope(Rect::new(3, 3, 4, 4));
36 ///
37 /// assert_eq!(scoped.area(), Rect::new(3, 3, 4, 4));
38 /// assert_eq!(scoped.area().at_origin(), Rect::new(0, 0, 4, 4));
39 /// ```
40 #[must_use]
41 pub const fn area(&self) -> Rect {
42 self.area
43 }
44
45 /// The visible subset of [`area`](Self::area). Every write this surface accepts is
46 /// bounds-checked against this rect, not `area`.
47 #[must_use]
48 pub const fn clip_rect(&self) -> Rect {
49 self.clip
50 }
51
52 /// The width of this surface's area, in columns.
53 #[must_use]
54 pub const fn width(&self) -> u16 {
55 self.area.width()
56 }
57
58 /// The height of this surface's area, in rows.
59 #[must_use]
60 pub const fn height(&self) -> u16 {
61 self.area.height()
62 }
63
64 /// The grid layer this surface writes to.
65 #[must_use]
66 pub const fn layer(&self) -> u8 {
67 self.layer
68 }
69
70 /// A new surface over the same grid, area, and clip, but writing to `layer` instead.
71 #[must_use]
72 pub const fn on_layer(&mut self, layer: u8) -> Surface<'_> {
73 Surface {
74 grid: self.grid,
75 area: self.area,
76 clip: self.clip,
77 layer,
78 tint: self.tint,
79 origin_offset: self.origin_offset,
80 }
81 }
82
83 /// Equivalent to [`self.on_layer(tier.as_u8())`](crate::surface::Surface::on_layer), for switching to one of
84 /// the workspace's named [`Layer`](crate::surface::Layer) tiers instead of a raw layer id. See [`Layer`](crate::surface::Layer)'s docs for
85 /// when to reach for this over a numeric [`Surface::on_layer`](crate::surface::Surface::on_layer) call.
86 #[must_use]
87 pub const fn on_tier(&mut self, tier: Layer) -> Surface<'_> {
88 self.on_layer(tier.as_u8())
89 }
90
91 /// The tint every sprite drawn through this surface is recoloured by.
92 #[must_use]
93 pub const fn tint(&self) -> Tint {
94 self.tint
95 }
96
97 /// The offset [`translate`](Self::translate) has accumulated on this surface, `(0, 0)` if it
98 /// has never been called.
99 ///
100 /// Every coordinate a caller passes to a coordinate-taking method has this subtracted from it
101 /// before the usual bounds check (see [`translate`](Self::translate)'s doc), so a callee
102 /// handed a `&mut Surface` can use this to tell whether it is in a translated coordinate
103 /// space, compose a further offset relative to the current one without over- or
104 /// undershooting, or convert a local coordinate it read back off the surface into the
105 /// caller's own coordinate space by adding this back in.
106 #[must_use]
107 pub const fn origin(&self) -> (i32, i32) {
108 self.origin_offset
109 }
110
111 /// A new surface over the same grid, area, and layer, recolouring every sprite it draws by
112 /// `tint`.
113 ///
114 /// Substituted rather than combined: unlike [`clip`](Self::clip), which can only narrow,
115 /// a tint replaces whatever the parent surface carried. Two tints do not compose into a
116 /// third meaningful one, and silently multiplying an inherited shadow into a caller's damage
117 /// flash would be harder to predict than replacing it.
118 ///
119 /// Applies to sprites only. A cell backend has no sprite to recolour and draws the cell's
120 /// glyph in its own [`Style`](crate::color::Style), tinted or not, so this is invisible there. See [`Tint`](crate::color::Tint).
121 ///
122 /// This tint composes with the sheet's own colour treatment; see
123 /// `retroglyph_window::tileset::SheetColor` and `retroglyph_window::sprite_cache::SpriteTint`
124 /// for the two-stage resolution (retroglyph-core has no dependency on retroglyph-window, so
125 /// these are plain names, not intra-doc links).
126 ///
127 /// For a multi-cell span the tint lands on the anchor cell, which is where a pixel backend
128 /// draws the sprite from. [`blit`](Self::blit) has no such anchor (`grid` is arbitrary
129 /// composed content, not one sprite) and does not apply this tint at all; see its own doc.
130 ///
131 /// # Examples
132 ///
133 /// ```
134 /// # fn main() {
135 /// # fn run() -> Option<()> {
136 /// use retroglyph_core::color::{Style, Tint};
137 /// use retroglyph_core::grid::{Grid, Rect};
138 /// use retroglyph_core::surface::Surface;
139 ///
140 /// let mut grid = Grid::new(8, 4);
141 /// let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 8, 4), 0);
142 ///
143 /// // One grass sprite, drawn twice: once as itself, once dimmed into shadow.
144 /// let grass = '\u{E000}';
145 /// surface.put_span_uniform((0, 0), (2, 1), grass, ' ', Style::default())?;
146 /// surface
147 /// .with_tint(Tint::multiply(128, 128, 128))
148 /// .put_span_uniform((2, 0), (2, 1), grass, ' ', Style::default())?;
149 ///
150 /// assert_eq!(grid.tint(0, 0, 0), Tint::None);
151 /// assert_eq!(grid.tint(0, 2, 0), Tint::multiply(128, 128, 128));
152 /// # Some(())
153 /// # }
154 /// # run().unwrap();
155 /// # }
156 /// ```
157 #[must_use]
158 pub const fn with_tint(&mut self, tint: Tint) -> Surface<'_> {
159 Surface {
160 grid: self.grid,
161 area: self.area,
162 clip: self.clip,
163 layer: self.layer,
164 tint,
165 origin_offset: self.origin_offset,
166 }
167 }
168
169 /// A new surface over the same grid, layer, and [`area`](Self::area), whose
170 /// [`clip_rect`](Self::clip_rect) is narrowed to `rect` intersected with this surface's own
171 /// clip. What this surface *represents* is unchanged; only what is visible shrinks.
172 ///
173 /// `rect` is in absolute grid coordinates (it intersects [`clip_rect`](Self::clip_rect),
174 /// itself absolute), not local to this surface's own [`area`](Self::area) the way
175 /// [`fill_rect`](Self::fill_rect), [`clear_region`](Self::clear_region), and
176 /// [`print_aligned`](Self::print_aligned)'s own `rect` are. Coordinates are otherwise
177 /// unchanged: the sub-surface addresses the same space this one does, so a
178 /// sub-rect computed against [`Surface::area`](crate::surface::Surface::area) (e.g. by a [`layout`](crate::layout) split)
179 /// can be passed straight in. Because the clip is intersected rather than substituted,
180 /// narrowing is monotonic: handing a surface down a layout tree can only ever tighten what a
181 /// callee is able to draw into, never widen it.
182 ///
183 /// Clipping is also how the clip-sensitive calls are told what they are drawing into:
184 ///
185 /// - [`print`](Self::print) wraps overflow onto the next row. Clipped to a one-row bar, the
186 /// wrapped remainder falls outside the clip and is dropped, which is what a single-line
187 /// bar wants.
188 /// - [`put_span`](Self::put_span) and [`put_span_uniform`](Self::put_span_uniform) refuse a
189 /// footprint that leaves the clip. Clipped to a content rect, "fits" stops meaning "fits
190 /// the screen" and starts meaning "does not reserve cells in the status bar below".
191 ///
192 /// A sub-surface that should instead *represent* `rect` (e.g. a widget's own region, laid out
193 /// and centered against `rect` rather than the parent's wider area) wants
194 /// [`scope`](Self::scope), not this.
195 ///
196 /// # Examples
197 ///
198 /// ```
199 /// use retroglyph_core::color::Style;
200 /// use retroglyph_core::grid::{Grid, Pos, Rect};
201 /// use retroglyph_core::surface::Surface;
202 ///
203 /// let mut grid = Grid::new(6, 2);
204 /// let mut screen = Surface::new(&mut grid, Rect::new(0, 0, 6, 2), 0);
205 ///
206 /// // A title too long for the one-row bar at the top: the remainder wraps out of the
207 /// // clip instead of onto the map below.
208 /// screen
209 /// .clip(Rect::new(0, 0, 6, 1))
210 /// .print((0, 0), "retroglyph", Style::default());
211 ///
212 /// assert_eq!(grid[Pos::new(0, 0)].glyph(), 'r');
213 /// assert_eq!(grid[Pos::new(0, 1)].glyph(), ' ');
214 /// ```
215 #[must_use]
216 pub fn clip(&mut self, rect: Rect) -> Surface<'_> {
217 Surface {
218 area: self.area,
219 clip: self.clip.intersect(rect),
220 grid: self.grid,
221 layer: self.layer,
222 tint: self.tint,
223 origin_offset: self.origin_offset,
224 }
225 }
226
227 /// A new surface over the same grid and layer, that *represents* `rect`: its
228 /// [`area`](Self::area) becomes `rect`, and its [`clip_rect`](Self::clip_rect) is narrowed to
229 /// `rect` intersected with this surface's own clip.
230 ///
231 /// This is the primitive a widget's own region is built from: a sub-widget laid out against
232 /// `rect` should center, align, and measure itself against `rect` (via [`area`](Self::area)),
233 /// while still being unable to draw outside whatever was already visible in the parent. A
234 /// clip alone cannot do this, because [`clip`](Self::clip) leaves `area` untouched; `scope`
235 /// is what a caller reaches for when handing a sub-rect down to something that is going to
236 /// read that rect back off the surface.
237 ///
238 /// Like [`clip`](Self::clip), the clip narrows monotonically: a `rect` that reaches outside
239 /// this surface's own clip only ever tightens what the returned surface can draw into, never
240 /// widens it, even though `area` itself becomes exactly `rect`.
241 ///
242 /// # Examples
243 ///
244 /// ```
245 /// use retroglyph_core::grid::{Grid, Rect};
246 /// use retroglyph_core::surface::Surface;
247 ///
248 /// let mut grid = Grid::new(8, 4);
249 /// let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 8, 4), 0);
250 ///
251 /// let mut clipped = surface.clip(Rect::new(0, 0, 4, 4));
252 /// // `scope` widens `area` to a rect the parent's clip does not fully cover...
253 /// let scoped = clipped.scope(Rect::new(2, 0, 4, 4));
254 /// assert_eq!(scoped.area(), Rect::new(2, 0, 4, 4));
255 /// // ...but the visible region still cannot exceed the parent's own clip.
256 /// assert_eq!(scoped.clip_rect(), Rect::new(2, 0, 2, 4));
257 /// ```
258 #[must_use]
259 pub fn scope(&mut self, rect: Rect) -> Surface<'_> {
260 Surface {
261 area: rect,
262 clip: self.clip.intersect(rect),
263 grid: self.grid,
264 layer: self.layer,
265 tint: self.tint,
266 origin_offset: self.origin_offset,
267 }
268 }
269
270 /// A view whose `(0, 0)` sits at `origin` relative to this surface's own coordinate space, so
271 /// a caller can draw in a shifted (e.g. world/camera) coordinate space and let the surface do
272 /// the clipping, rather than subtracting `origin` from every coordinate by hand.
273 ///
274 /// Every coordinate-taking method on the returned surface ([`put`](Self::put),
275 /// [`put_signed`](Self::put_signed), [`print`](Self::print), [`print_line`](Self::print_line),
276 /// [`fill_rect`](Self::fill_rect), [`put_offset`](Self::put_offset),
277 /// [`put_span`](Self::put_span), [`put_span_uniform`](Self::put_span_uniform), and
278 /// [`clear_region`](Self::clear_region)) subtracts `origin` (composed with any outstanding
279 /// translate) from the coordinate it is given before applying its usual bounds check. Only
280 /// [`clear`](Self::clear), which takes no coordinate and always clears this surface's whole
281 /// area, is unaffected.
282 ///
283 /// This does not touch [`area`](Self::area) or [`clip_rect`](Self::clip_rect), so both, along
284 /// with [`width`](Self::width) and [`height`](Self::height), keep reporting the same thing
285 /// before and after translating: only the coordinate a caller must pass to land a write
286 /// shifts, never what the surface itself covers or what is visible in it. This composes with
287 /// [`scope`](Self::scope) the same order it is called in: `scope(...).translate(...)` first
288 /// narrows the area and clip, then shifts the coordinate space that still-narrowed area is
289 /// addressed in, so a coordinate that goes negative after the shift can land inside the
290 /// pre-narrowed area.
291 ///
292 /// The offset accumulates with saturating arithmetic: chaining enough `translate` calls in one
293 /// direction clamps `origin` at `i32::MIN`/`i32::MAX` instead of wrapping or panicking. A
294 /// coordinate that only lands after an offset larger than that was never addressable in this
295 /// surface's `u16` grid space to begin with.
296 ///
297 /// # Examples
298 ///
299 /// ```
300 /// use retroglyph_core::color::Style;
301 /// use retroglyph_core::grid::{Grid, Pos, Rect};
302 /// use retroglyph_core::surface::Surface;
303 ///
304 /// let mut grid = Grid::new(10, 10);
305 /// let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 10, 10), 0);
306 ///
307 /// // Narrow to a 4x4 viewport, then shift its coordinate space by (-5, -5): translating
308 /// // does not move or resize the viewport itself.
309 /// let mut scoped = surface.scope(Rect::new(5, 5, 4, 4));
310 /// let mut view = scoped.translate((-5, -5));
311 /// assert_eq!(view.area(), Rect::new(5, 5, 4, 4));
312 ///
313 /// // (-5, -5) minus the translate offset (-5, -5) is (0, 0): the viewport's own local
314 /// // origin, which lands at the viewport's top-left grid cell (5, 5).
315 /// view.put_signed((-5, -5), 'X', Style::default());
316 ///
317 /// assert_eq!(grid[Pos::new(5, 5)].glyph(), 'X');
318 /// ```
319 #[must_use]
320 pub const fn translate(&mut self, origin: (i32, i32)) -> Surface<'_> {
321 Surface {
322 grid: self.grid,
323 area: self.area,
324 clip: self.clip,
325 layer: self.layer,
326 tint: self.tint,
327 origin_offset: (
328 self.origin_offset.0.saturating_add(origin.0),
329 self.origin_offset.1.saturating_add(origin.1),
330 ),
331 }
332 }
333
334 /// [`clip`](Self::clip) to `area`, then [`translate`](Self::translate) by `origin`, in one
335 /// call -- except that unlike plain [`clip`](Self::clip), the returned surface's
336 /// [`area`](Self::area) is `area` intersected with this surface's own area, not `area`
337 /// verbatim.
338 ///
339 /// Chaining `clip(...).translate(...)` directly works when the result is used right where
340 /// it's produced (both `clip` and `translate` return a `Surface<'_>` borrowing the previous
341 /// step for exactly that call), but a helper that hands the composed view back to its own
342 /// caller (for example a scrolling-camera widget's own `surface` method) needs the two
343 /// narrowings applied against a single `&mut self` borrow instead, so the returned surface
344 /// can outlive the call. This does that.
345 ///
346 /// This intersects `area` with this surface's own area rather than replacing it the way
347 /// [`scope`](Self::scope) does, so [`area`](Self::area)/[`width`](Self::width)/
348 /// [`height`](Self::height) on the result can report something smaller than the `area`
349 /// argument. A scrolling-camera widget's `surface` method relies on exactly this: when the
350 /// world is smaller than the viewport, it hands in a viewport-sized `area` and depends on the
351 /// intersection to shrink it back down to the world's own size. A caller that wants `area`
352 /// to become exactly its argument, even reaching outside the parent's current area, should
353 /// use [`scope`](Self::scope) followed by [`translate`](Self::translate) instead.
354 ///
355 /// The offset accumulates with saturating arithmetic, as in [`translate`](Self::translate).
356 ///
357 /// # Examples
358 ///
359 /// ```
360 /// use retroglyph_core::color::Style;
361 /// use retroglyph_core::grid::{Grid, Pos, Rect};
362 /// use retroglyph_core::surface::Surface;
363 ///
364 /// let mut grid = Grid::new(10, 10);
365 /// let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 10, 10), 0);
366 ///
367 /// let mut view = surface.clip_translate(Rect::new(5, 5, 4, 4), (-5, -5));
368 /// assert_eq!(view.area(), Rect::new(5, 5, 4, 4));
369 ///
370 /// view.put_signed((-5, -5), 'X', Style::default());
371 /// assert_eq!(grid[Pos::new(5, 5)].glyph(), 'X');
372 /// ```
373 #[must_use]
374 pub fn clip_translate(&mut self, area: Rect, origin: (i32, i32)) -> Surface<'_> {
375 let area = self.area.intersect(area);
376 Surface {
377 area,
378 clip: self.clip.intersect(area),
379 grid: self.grid,
380 layer: self.layer,
381 tint: self.tint,
382 origin_offset: (
383 self.origin_offset.0.saturating_add(origin.0),
384 self.origin_offset.1.saturating_add(origin.1),
385 ),
386 }
387 }
388
389 /// A styled view over this surface: same area and layer, but every draw call uses `style`
390 /// without needing to pass it each time. Handy for a run of same-styled writes (e.g. filling
391 /// in a wall glyph over many cells) without repeating the [`Style`](crate::color::Style) at every call site.
392 pub const fn with_style(&mut self, style: Style) -> StyledSurface<'_, 'a> {
393 StyledSurface {
394 surface: self,
395 style,
396 }
397 }
398
399 /// Borrows the underlying [`Grid`](crate::grid::Grid) directly, with no clipping.
400 ///
401 /// Escape hatch for multi-layer or whole-grid operations (e.g. [`Grid::blit`](crate::grid::Grid::blit)) that don't fit
402 /// this surface's clipped, single-layer model. Drawing into a sub-rect is not one of those:
403 /// [`clip`](Self::clip) and [`scope`](Self::scope) narrow a surface without handing out the
404 /// unclipped grid to do it.
405 pub const fn grid_mut(&mut self) -> &mut Grid {
406 self.grid
407 }
408
409 /// Read-only counterpart of [`grid_mut`](Self::grid_mut).
410 #[must_use]
411 pub const fn grid(&self) -> &Grid {
412 self.grid
413 }
414}