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