retroglyph_core/surface.rs
1//! [`Surface`]: an area-clipped, single-layer view over a [`Grid`].
2//!
3//! `Surface` is the workspace's one grid-drawing primitive. [`Terminal`](crate::Terminal)'s
4//! [`draw`](crate::Terminal::draw)/[`surface`](crate::Terminal::surface) hand out a `Surface`
5//! scoped to the whole grid, and `retroglyph-widgets` renders every widget into a `Surface`
6//! scoped to a sub-[`Rect`]: there is no separate stateful drawing API on `Terminal` itself.
7
8use crate::color::Color;
9use crate::grid::{Grid, Offset, Pos, Rect, Size};
10use crate::style::Style;
11use crate::text::Line;
12use crate::tile::Tile;
13use crate::tint::Tint;
14#[cfg(not(feature = "egc"))]
15use unicode_width::UnicodeWidthChar;
16
17/// The render target for every drawing call in the workspace: a mutable reference to a
18/// [`Grid`] plus a fixed `layer`, scoped to one area.
19///
20/// A `Surface` is typically created once per frame, scoped to the whole drawing surface (e.g.
21/// via [`Terminal::draw`](crate::Terminal::draw)), and handed to every subsystem/widget in turn;
22/// each caller's own `area: Rect` (a sub-rect of the surface's own area, e.g. one produced by a
23/// layout split) is in the same coordinate space as [`Surface::area`] itself.
24/// [`Surface::put`]/[`Surface::print`]/... take coordinates in that same space and silently clip
25/// any write that falls outside [`Surface::area`], matching the rest of the workspace's
26/// clip-on-draw policy for out-of-bounds drawing.
27///
28/// [`Surface::clip`] turns a sub-rect into a surface of its own, so a subsystem that should not
29/// draw outside one is bounded by the type rather than trusted to respect an `area` handed to it
30/// alongside a wider surface. The clip is intersected, never substituted, so narrowing only ever
31/// tightens.
32///
33/// A caller that genuinely needs more than one layer at once (e.g. a modal dimming layer 0 while
34/// drawing its own content on layer 1) switches layers with [`Surface::on_layer`] rather than
35/// being restricted to the layer it was constructed with.
36pub struct Surface<'a> {
37 grid: &'a mut Grid,
38 area: Rect,
39 layer: u8,
40 tint: Tint,
41 origin_offset: (i32, i32),
42}
43
44impl<'a> Surface<'a> {
45 /// A surface over `grid`, scoped to `area` on `layer`, tinting nothing.
46 pub const fn new(grid: &'a mut Grid, area: Rect, layer: u8) -> Self {
47 Self {
48 grid,
49 area,
50 layer,
51 tint: Tint::None,
52 origin_offset: (0, 0),
53 }
54 }
55
56 /// The area this surface clips writes to.
57 #[must_use]
58 pub const fn area(&self) -> Rect {
59 self.area
60 }
61
62 /// The width of this surface's area, in columns.
63 #[must_use]
64 pub const fn width(&self) -> u16 {
65 self.area.width()
66 }
67
68 /// The height of this surface's area, in rows.
69 #[must_use]
70 pub const fn height(&self) -> u16 {
71 self.area.height()
72 }
73
74 /// The grid layer this surface writes to.
75 #[must_use]
76 pub const fn layer(&self) -> u8 {
77 self.layer
78 }
79
80 /// A new surface over the same grid and area, but writing to `layer` instead.
81 #[must_use]
82 pub const fn on_layer(&mut self, layer: u8) -> Surface<'_> {
83 Surface {
84 grid: self.grid,
85 area: self.area,
86 layer,
87 tint: self.tint,
88 origin_offset: self.origin_offset,
89 }
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 /// A new surface over the same grid, area, and layer, recolouring every sprite it draws by
99 /// `tint`.
100 ///
101 /// Substituted rather than combined: unlike [`clip`](Self::clip), which can only narrow,
102 /// a tint replaces whatever the parent surface carried. Two tints do not compose into a
103 /// third meaningful one, and silently multiplying an inherited shadow into a caller's damage
104 /// flash would be harder to predict than replacing it.
105 ///
106 /// Applies to sprites only. A cell backend has no sprite to recolour and draws the cell's
107 /// glyph in its own [`Style`], tinted or not, so this is invisible there. See [`Tint`].
108 ///
109 /// This tint composes with the sheet's own colour treatment; see
110 /// `retroglyph_window::tileset::SheetColor` and `retroglyph_window::sprite_cache::SpriteTint`
111 /// for the two-stage resolution (retroglyph-core has no dependency on retroglyph-window, so
112 /// these are plain names, not intra-doc links).
113 ///
114 /// For a multi-cell span the tint lands on the anchor cell, which is where a pixel backend
115 /// draws the sprite from.
116 ///
117 /// # Examples
118 ///
119 /// ```
120 /// # fn main() {
121 /// # fn run() -> Option<()> {
122 /// use retroglyph_core::{Grid, Rect, Style, Surface, Tint};
123 ///
124 /// let mut grid = Grid::new(8, 4);
125 /// let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 8, 4), 0);
126 ///
127 /// // One grass sprite, drawn twice: once as itself, once dimmed into shadow.
128 /// let grass = '\u{E000}';
129 /// surface.put_span_uniform((0, 0), (2, 1), grass, ' ', Style::default())?;
130 /// surface
131 /// .with_tint(Tint::multiply(128, 128, 128))
132 /// .put_span_uniform((2, 0), (2, 1), grass, ' ', Style::default())?;
133 ///
134 /// assert_eq!(grid.tint(0, 0, 0), Tint::None);
135 /// assert_eq!(grid.tint(0, 2, 0), Tint::multiply(128, 128, 128));
136 /// # Some(())
137 /// # }
138 /// # run().unwrap();
139 /// # }
140 /// ```
141 #[must_use]
142 pub const fn with_tint(&mut self, tint: Tint) -> Surface<'_> {
143 Surface {
144 grid: self.grid,
145 area: self.area,
146 layer: self.layer,
147 tint,
148 origin_offset: self.origin_offset,
149 }
150 }
151
152 /// A new surface over the same grid and layer, clipped to `area` intersected with this
153 /// surface's own area.
154 ///
155 /// Coordinates are unchanged: the sub-surface addresses the same space this one does, so a
156 /// sub-rect computed against [`Surface::area`] (e.g. by a [`layout`](crate::layout) split)
157 /// can be passed straight in. Because `area` is intersected rather than substituted,
158 /// narrowing is monotonic: handing a surface down a layout tree can only ever tighten what a
159 /// callee is able to touch.
160 ///
161 /// Clipping is also how the area-sensitive calls are told what they are drawing into:
162 ///
163 /// - [`print`](Self::print) wraps overflow onto the next row. Clipped to a one-row bar, the
164 /// wrapped remainder falls outside the area and is dropped, which is what a single-line
165 /// bar wants.
166 /// - [`put_span`](Self::put_span) and [`put_span_uniform`](Self::put_span_uniform) refuse a
167 /// footprint that leaves the area. Clipped to a content rect, "fits" stops meaning "fits
168 /// the screen" and starts meaning "does not reserve cells in the status bar below".
169 ///
170 /// # Examples
171 ///
172 /// ```
173 /// use retroglyph_core::{Grid, Pos, Rect, Style, Surface};
174 ///
175 /// let mut grid = Grid::new(6, 2);
176 /// let mut screen = Surface::new(&mut grid, Rect::new(0, 0, 6, 2), 0);
177 ///
178 /// // A title too long for the one-row bar at the top: the remainder wraps out of the
179 /// // clip instead of onto the map below.
180 /// screen
181 /// .clip(Rect::new(0, 0, 6, 1))
182 /// .print((0, 0), "retroglyph", Style::default());
183 ///
184 /// assert_eq!(grid[Pos::new(0, 0)].glyph(), 'r');
185 /// assert_eq!(grid[Pos::new(0, 1)].glyph(), ' ');
186 /// ```
187 #[must_use]
188 pub fn clip(&mut self, area: Rect) -> Surface<'_> {
189 Surface {
190 area: self.area.intersect(area),
191 grid: self.grid,
192 layer: self.layer,
193 tint: self.tint,
194 origin_offset: self.origin_offset,
195 }
196 }
197
198 /// A view whose `(0, 0)` sits at `origin` relative to this surface's own coordinate space, so
199 /// a caller can draw in a shifted (e.g. world/camera) coordinate space and let the surface do
200 /// the clipping, rather than subtracting `origin` from every coordinate by hand.
201 ///
202 /// Every coordinate-taking method on the returned surface -- [`put`](Self::put),
203 /// [`put_signed`](Self::put_signed), [`print`](Self::print), [`print_line`](Self::print_line),
204 /// [`fill_rect`](Self::fill_rect), [`put_offset`](Self::put_offset),
205 /// [`put_span`](Self::put_span), [`put_span_uniform`](Self::put_span_uniform), and
206 /// [`clear_region`](Self::clear_region) -- subtracts `origin` (composed with any outstanding
207 /// translate) from the coordinate it is given before applying its usual bounds check. Only
208 /// [`clear`](Self::clear), which takes no coordinate and always clears this surface's whole
209 /// area, is unaffected.
210 ///
211 /// This does not touch [`area`](Self::area), so [`area`](Self::area), [`width`](Self::width),
212 /// and [`height`](Self::height) keep reporting the same thing before and after translating:
213 /// only the coordinate a caller must pass to land a write shifts, never what the surface
214 /// itself covers. This composes with [`clip`](Self::clip) the same order it is called in:
215 /// `clip(...).translate(...)` first narrows the area, then shifts the coordinate space that
216 /// still-narrowed area is addressed in, so a coordinate that goes negative after the shift can
217 /// land inside the pre-narrowed area.
218 ///
219 /// # Examples
220 ///
221 /// ```
222 /// use retroglyph_core::{Grid, Pos, Rect, Style, Surface};
223 ///
224 /// let mut grid = Grid::new(10, 10);
225 /// let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 10, 10), 0);
226 ///
227 /// // Narrow to a 4x4 viewport, then shift its coordinate space by (-5, -5): translating
228 /// // does not move or resize the viewport itself.
229 /// let mut clipped = surface.clip(Rect::new(5, 5, 4, 4));
230 /// let mut view = clipped.translate((-5, -5));
231 /// assert_eq!(view.area(), Rect::new(5, 5, 4, 4));
232 ///
233 /// // (-5, -5) minus the translate offset (-5, -5) is (0, 0): the viewport's own local
234 /// // origin, which lands at the viewport's top-left grid cell (5, 5).
235 /// view.put_signed((-5, -5), 'X', Style::default());
236 ///
237 /// assert_eq!(grid[Pos::new(5, 5)].glyph(), 'X');
238 /// ```
239 #[must_use]
240 pub const fn translate(&mut self, origin: (i32, i32)) -> Surface<'_> {
241 Surface {
242 grid: self.grid,
243 area: self.area,
244 layer: self.layer,
245 tint: self.tint,
246 origin_offset: (
247 self.origin_offset.0.saturating_add(origin.0),
248 self.origin_offset.1.saturating_add(origin.1),
249 ),
250 }
251 }
252
253 /// A styled view over this surface: same area and layer, but every draw call uses `style`
254 /// without needing to pass it each time. Handy for a run of same-styled writes (e.g. filling
255 /// in a wall glyph over many cells) without repeating the [`Style`] at every call site.
256 pub const fn with_style(&mut self, style: Style) -> StyledSurface<'_, 'a> {
257 StyledSurface {
258 surface: self,
259 style,
260 }
261 }
262
263 /// Borrows the underlying [`Grid`] directly, with no clipping.
264 ///
265 /// Escape hatch for multi-layer or whole-grid operations (e.g. [`Grid::blit`]) that don't fit
266 /// this surface's clipped, single-layer model. Drawing into a sub-rect is not one of those:
267 /// [`clip`](Self::clip) narrows a surface without handing out the unclipped grid to do it.
268 pub const fn grid_mut(&mut self) -> &mut Grid {
269 self.grid
270 }
271
272 /// Read-only counterpart of [`grid_mut`](Self::grid_mut).
273 #[must_use]
274 pub const fn grid(&self) -> &Grid {
275 self.grid
276 }
277
278 /// The tile at `pos` on this surface's layer, if any.
279 ///
280 /// Respects this surface's layer but not its area clip, mirroring [`grid_mut`](Self::grid_mut)
281 /// in that sense: a caller wanting an area-clipped read should check
282 /// [`self.area().contains(...)`](Rect::contains) first.
283 ///
284 /// # Examples
285 ///
286 /// ```
287 /// use retroglyph_core::{Grid, Rect, Style, Surface};
288 ///
289 /// let mut grid = Grid::new(4, 4);
290 /// let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0);
291 /// surface.put((1, 1), 'X', Style::default());
292 ///
293 /// assert_eq!(surface.tile((1, 1)).map(|t| t.glyph()), Some('X'));
294 /// assert_eq!(surface.tile((0, 0)).map(|t| t.glyph()), Some(' '));
295 /// ```
296 #[must_use]
297 pub fn tile(&self, pos: impl Into<Pos>) -> Option<&Tile> {
298 self.grid.tile(self.layer, pos.into())
299 }
300
301 /// The background colour at `pos` on this surface's layer, or `None` if there's no tile
302 /// there.
303 ///
304 /// A read-only read of a cell's own background lets a caller blend a new draw with what's
305 /// already there (e.g. `surface.background(pos).unwrap_or(default)`) without the mutable
306 /// borrow [`grid_mut`](Self::grid_mut) would otherwise force.
307 ///
308 /// # Examples
309 ///
310 /// ```
311 /// use retroglyph_core::{Color, Grid, Rect, Style, Surface};
312 ///
313 /// let mut grid = Grid::new(4, 4);
314 /// let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0);
315 /// surface.put((1, 1), 'X', Style::new().bg(Color::RED));
316 ///
317 /// assert_eq!(surface.background((1, 1)), Some(Color::RED));
318 /// // Out of the grid entirely: no tile there to read a background from.
319 /// assert_eq!(surface.background((10, 10)), None);
320 /// ```
321 #[must_use]
322 pub fn background(&self, pos: impl Into<Pos>) -> Option<Color> {
323 self.tile(pos).map(|t| t.style().background())
324 }
325
326 /// Shifts `(x, y)` by this surface's translate offset (see [`translate`](Self::translate)),
327 /// returning the coordinate to actually write at if the shift still lands inside this
328 /// surface's own area, or `None` otherwise.
329 fn shift(&self, x: u16, y: u16) -> Option<(u16, u16)> {
330 let sx = i32::from(x).checked_sub(self.origin_offset.0)?;
331 let sy = i32::from(y).checked_sub(self.origin_offset.1)?;
332 let sx = u16::try_from(sx).ok()?;
333 let sy = u16::try_from(sy).ok()?;
334 self.area.contains(sx, sy).then_some((sx, sy))
335 }
336
337 /// Applies this surface's tint to the cell just written at `(x, y)`.
338 ///
339 /// Called after a write rather than as part of one, because a glyph write drops whatever
340 /// tint the cell held (see [`Grid::set_tint`]); doing it in the other order would erase the
341 /// tint being applied. Untinted surfaces skip the call entirely, so the ordinary text path
342 /// never touches the side table.
343 fn apply_tint(&mut self, x: u16, y: u16) {
344 if self.tint != Tint::None {
345 self.grid.set_tint(self.layer, x, y, self.tint);
346 }
347 }
348
349 /// Writes `grapheme` (already a single extended grapheme cluster) at `(x, y)`. A no-op if
350 /// out of this surface's area.
351 #[cfg(feature = "egc")]
352 fn put_grapheme(&mut self, x: u16, y: u16, grapheme: &str, style: Style) {
353 let Some((x, y)) = self.shift(x, y) else {
354 return;
355 };
356 self.grid.write_grapheme(self.layer, x, y, grapheme, style);
357 self.apply_tint(x, y);
358 }
359
360 /// Place `ch` at `pos` in `style`. A no-op if `pos` is outside this surface's area.
361 ///
362 /// If a pixel backend resolves `ch` to a sprite, that sprite is composited from its own
363 /// pixels: [`style.fg`](Style::fg) does not tint it, and `style.bg` shows through only where
364 /// the sprite is transparent. See [`put_span`](Self::put_span).
365 ///
366 /// # Examples
367 ///
368 /// ```
369 /// use retroglyph_core::{Grid, Pos, Rect, Style, Surface};
370 ///
371 /// let mut grid = Grid::new(4, 4);
372 /// let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0);
373 ///
374 /// surface.put((1, 1), 'X', Style::default());
375 /// // Outside the surface's area: silently dropped, not a panic.
376 /// surface.put((10, 10), 'X', Style::default());
377 ///
378 /// assert_eq!(grid[Pos::new(1, 1)].glyph(), 'X');
379 /// ```
380 pub fn put(&mut self, pos: impl Into<Pos>, ch: char, style: Style) {
381 let pos = pos.into();
382 #[cfg(feature = "egc")]
383 {
384 let mut buf = [0u8; 4];
385 let s = ch.encode_utf8(&mut buf);
386 self.put_grapheme(pos.x, pos.y, s, style);
387 }
388 #[cfg(not(feature = "egc"))]
389 {
390 let Some((x, y)) = self.shift(pos.x, pos.y) else {
391 return;
392 };
393 let tile = Tile::new(ch, style);
394 self.grid.put_tile(self.layer, (x, y), tile);
395 self.apply_tint(x, y);
396 }
397 }
398
399 /// [`put`](Self::put), in coordinates relative to this surface's own area origin, where a
400 /// negative coordinate is expressible and simply falls outside (a no-op, matching `put`'s
401 /// out-of-bounds behavior).
402 ///
403 /// Scrolling/camera code (e.g. a viewport over a wider world) computes positions in a
404 /// coordinate space that can go negative relative to the viewport, which [`Pos`] (backed by
405 /// `u16`) cannot even express. `put_signed` takes that arithmetic directly, so a caller no
406 /// longer clip-tests by hand before calling `put`.
407 ///
408 /// # Examples
409 ///
410 /// ```
411 /// use retroglyph_core::{Grid, Pos, Rect, Style, Surface};
412 ///
413 /// let mut grid = Grid::new(4, 4);
414 /// let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0);
415 ///
416 /// // Negative in either axis: outside this surface's area, silently dropped.
417 /// surface.put_signed((-1, 1), 'X', Style::default());
418 /// // Non-negative and within bounds: lands like `put`.
419 /// surface.put_signed((1, 1), 'X', Style::default());
420 ///
421 /// assert_eq!(grid[Pos::new(1, 1)].glyph(), 'X');
422 /// assert_eq!(grid[Pos::new(0, 1)].glyph(), ' ');
423 /// ```
424 pub fn put_signed(&mut self, pos: (i32, i32), ch: char, style: Style) {
425 let (x, y) = pos;
426 let x = x.saturating_sub(self.origin_offset.0);
427 let y = y.saturating_sub(self.origin_offset.1);
428 if x < 0 || y < 0 {
429 return;
430 }
431 let Ok(x) = u16::try_from(x) else {
432 return;
433 };
434 let Ok(y) = u16::try_from(y) else {
435 return;
436 };
437 if x >= self.width() || y >= self.height() {
438 return;
439 }
440 let abs_x = self.area.left() + x;
441 let abs_y = self.area.top() + y;
442 let tile = Tile::new(ch, style);
443 self.grid.put_tile(self.layer, (abs_x, abs_y), tile);
444 self.apply_tint(abs_x, abs_y);
445 }
446
447 /// Print `text` starting at `pos` in `style`.
448 ///
449 /// `\n` advances to the next row at the original column. Text that would extend beyond this
450 /// surface's area wraps to the next row at the original column; cells outside the area
451 /// (either axis) are clipped. When the `egc` feature is enabled, `text` is split into
452 /// extended grapheme clusters (so combining marks and ZWJ sequences write as one cell each);
453 /// otherwise it is split by `char`.
454 ///
455 /// # Examples
456 ///
457 /// ```
458 /// use retroglyph_core::backend::Headless;
459 /// use retroglyph_core::{Style, Terminal};
460 ///
461 /// let mut term = Terminal::new(Headless::new(6, 3));
462 /// term.draw(|s| s.print((0, 0), "hello wrapped world", Style::default()))
463 /// .unwrap();
464 ///
465 /// // Wraps back to column 0 every 6 cells; the surface is only 3 rows tall, so
466 /// // the remainder past row 2 is clipped rather than growing the grid.
467 /// assert_eq!(
468 /// term.backend().format_view(),
469 /// "hello·\nwrappe\nd·worl\n",
470 /// );
471 /// ```
472 pub fn print(&mut self, pos: impl Into<Pos>, text: &str, style: Style) {
473 let pos = pos.into();
474 #[cfg(feature = "egc")]
475 self.print_egc(pos, text, style);
476 #[cfg(not(feature = "egc"))]
477 self.print_chars(pos, text, style);
478 }
479
480 /// [`print`](Self::print) implementation used when `egc` is enabled: splits on extended
481 /// grapheme clusters rather than `char`.
482 #[cfg(feature = "egc")]
483 fn print_egc(&mut self, pos: Pos, text: &str, style: Style) {
484 use unicode_segmentation::UnicodeSegmentation;
485 use unicode_width::UnicodeWidthStr;
486
487 let right = self.area.right();
488 let mut cx = pos.x;
489 let mut cy = pos.y;
490 for grapheme in text.graphemes(true) {
491 if grapheme == "\n" {
492 cx = pos.x;
493 cy = cy.saturating_add(1);
494 continue;
495 }
496 // A single grapheme's display width is 0, 1, or 2 per `unicode-width` (see
497 // `Tile::width`'s doc comment), never anywhere near `u16::MAX`.
498 #[allow(clippy::cast_possible_truncation)]
499 let w = grapheme.width() as u16;
500 if w == 0 {
501 continue;
502 }
503 self.put_grapheme(cx, cy, grapheme, style);
504 cx = cx.saturating_add(w);
505 if cx >= right {
506 cx = pos.x;
507 cy = cy.saturating_add(1);
508 }
509 }
510 }
511
512 /// [`print`](Self::print) implementation used when `egc` is disabled: splits on `char`.
513 #[cfg(not(feature = "egc"))]
514 fn print_chars(&mut self, pos: Pos, text: &str, style: Style) {
515 let right = self.area.right();
516 let mut cx = pos.x;
517 let mut cy = pos.y;
518 for ch in text.chars() {
519 if ch == '\n' {
520 cx = pos.x;
521 cy = cy.saturating_add(1);
522 continue;
523 }
524 // A single char's display width is 0, 1, or 2 per `unicode-width` (see `Tile::width`'s
525 // doc comment), never anywhere near `u16::MAX`.
526 #[allow(clippy::cast_possible_truncation)]
527 let w = UnicodeWidthChar::width(ch).unwrap_or(1) as u16;
528 if w == 0 {
529 continue;
530 }
531 self.put((cx, cy), ch, style);
532 cx = cx.saturating_add(w);
533 if cx >= right {
534 cx = pos.x;
535 cy = cy.saturating_add(1);
536 }
537 }
538 }
539
540 /// Print `line`'s styled spans starting at `pos`, one row, each span in its own style.
541 /// Stops once a span would start past this surface's area.
542 ///
543 /// # Examples
544 ///
545 /// ```
546 /// use retroglyph_core::backend::Headless;
547 /// use retroglyph_core::text::{Line, Span};
548 /// use retroglyph_core::Terminal;
549 ///
550 /// let mut term = Terminal::new(Headless::new(5, 2));
551 /// let line = Line::from(vec![Span::raw("hello"), Span::raw("world")]);
552 /// term.draw(|s| s.print_line((0, 0), &line)).unwrap();
553 ///
554 /// // The first span exactly fills the one-row area. The second span would start at
555 /// // column 5, past the area, so it is skipped entirely rather than wrapped onto the
556 /// // next row the way `print` would wrap.
557 /// assert_eq!(term.backend().format_view(), "hello\n·····\n");
558 /// ```
559 pub fn print_line(&mut self, pos: impl Into<Pos>, line: &Line) {
560 use unicode_width::UnicodeWidthStr;
561
562 let pos = pos.into();
563 let right = self.area.right();
564 let mut cx = pos.x;
565 for span in &line.spans {
566 if cx >= right {
567 break;
568 }
569 self.print((cx, pos.y), &span.content, span.style);
570 // A single span wider than `u16::MAX` columns would already be unaddressable in this
571 // crate's `u16` coordinate space; `cx` still saturates rather than overflowing even if
572 // this cast wraps.
573 #[allow(clippy::cast_possible_truncation)]
574 let w = UnicodeWidthStr::width(span.content.as_str()) as u16;
575 cx = cx.saturating_add(w);
576 }
577 }
578
579 /// [`print`](Self::print), horizontally aligned within `rect` (clipped to this surface's own
580 /// area) and measured in display columns (via `unicode_width`), not bytes.
581 ///
582 /// Wants a per-frame redrawn UI label (a status line, a centred title bar) that should not
583 /// allocate: unlike [`TextLayout`](crate::layout::TextLayout), which only accepts a
584 /// [`Line`] (forcing an allocation to build one for every call), this
585 /// takes `&str` directly.
586 ///
587 /// The starting column is computed with saturating arithmetic, so `text` wider than `rect`
588 /// does not panic or underflow: it simply left-aligns and lets [`print`](Self::print) clip
589 /// the overflow, for every [`HAlign`](crate::layout::HAlign) (matching how
590 /// [`HAlign::Center`](crate::layout::HAlign::Center) itself saturates in
591 /// [`TextLayout`](crate::layout::TextLayout)).
592 ///
593 /// # Examples
594 ///
595 /// ```
596 /// use retroglyph_core::backend::Headless;
597 /// use retroglyph_core::layout::HAlign;
598 /// use retroglyph_core::{Rect, Style, Terminal};
599 ///
600 /// let mut term = Terminal::new(Headless::new(6, 1));
601 /// term.draw(|s| {
602 /// s.print_aligned(Rect::new(0, 0, 6, 1), "hi", HAlign::Center, Style::default())
603 /// })
604 /// .unwrap();
605 ///
606 /// // "hi" is 2 columns wide in a 6-column rect: (6 - 2) / 2 == 2 columns of left padding.
607 /// assert_eq!(term.backend().format_view(), "··hi··\n");
608 /// ```
609 #[cfg(feature = "egc")]
610 pub fn print_aligned(
611 &mut self,
612 rect: Rect,
613 text: &str,
614 align: crate::layout::HAlign,
615 style: Style,
616 ) {
617 use crate::layout::HAlign;
618 use unicode_width::UnicodeWidthStr;
619
620 // A single line's display width is never anywhere near `u16::MAX` (see `print_line`'s
621 // own use of this same cast for a single span).
622 #[allow(clippy::cast_possible_truncation)]
623 let text_width = UnicodeWidthStr::width(text) as u16;
624 let x_offset = match align {
625 HAlign::Left => 0,
626 HAlign::Center => rect.width().saturating_sub(text_width) / 2,
627 HAlign::Right => rect.width().saturating_sub(text_width),
628 };
629 let pos = (rect.left().saturating_add(x_offset), rect.top());
630 self.clip(rect).print(pos, text, style);
631 }
632
633 /// Fill `rect` (clipped to this surface's own area) with `ch` in `style`.
634 ///
635 /// # Examples
636 ///
637 /// ```
638 /// use retroglyph_core::{Grid, Pos, Rect, Style, Surface};
639 ///
640 /// let mut grid = Grid::new(4, 4);
641 /// let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0);
642 ///
643 /// // `rect` extends well past the grid on both axes; only the cells inside the
644 /// // surface's own area are touched, the rest is silently clipped.
645 /// surface.fill_rect(Rect::new(2, 2, 10, 10), '#', Style::default());
646 ///
647 /// assert_eq!(grid[Pos::new(3, 3)].glyph(), '#');
648 /// assert_eq!(grid[Pos::new(0, 0)].glyph(), ' ');
649 /// ```
650 pub fn fill_rect(&mut self, rect: Rect, ch: char, style: Style) {
651 for y in rect.top()..rect.bottom() {
652 for x in rect.left()..rect.right() {
653 self.put((x, y), ch, style);
654 }
655 }
656 }
657
658 /// Writes a multi-cell span at `pos` on this surface's layer in `style`: one piece of
659 /// artwork occupying a block of cells rather than one, the [`Surface`] twin of
660 /// [`Grid::write_span`].
661 ///
662 /// `rows` holds one string per row of the footprint. Its first character is the **anchor**
663 /// glyph, which a pixel backend looks up in its sprite cache; the rest are the span's **text
664 /// fallback**, printed by cell backends and skipped by pixel backends. Any `AsRef<str>` row
665 /// works, so a literal footprint (`&["[==]", "|__|"]`) and a computed one (`&Vec<String>`)
666 /// both pass without a borrowing pass over the rows; for the uniform case, see
667 /// [`put_span_uniform`](Self::put_span_uniform).
668 ///
669 /// See [`Grid::write_span`] for the full write semantics, and [`Grid::span_owner`] to
670 /// hit-test the whole footprint.
671 ///
672 /// # `style` applies to the text fallback, not to the sprite
673 ///
674 /// A sprite is composited from its own pixels. [`style.fg`](Style::fg) does not tint it;
675 /// `style.bg` is still painted behind it, so it shows through wherever the sprite is
676 /// transparent. Recoloring a shared sprite per cell is therefore not possible: draw a
677 /// variant of the artwork instead, which is the usual tileset idiom.
678 ///
679 /// `style` is not dead on such a cell, because the same span drawn by a *cell* backend
680 /// renders the text fallback in it. The consequence is that `fg` reads very differently
681 /// depending on the backend, and that a glyph missing from the sprite cache silently falls
682 /// back to a font glyph that *is* `fg`-colored, which looks a lot like a tint working.
683 ///
684 /// # Returns
685 ///
686 /// `Some(())` once the whole span is written, or `None` having written nothing at all when
687 /// `rows` is empty or ragged, either axis exceeds 255 cells, or the footprint does not fit
688 /// entirely within this surface's own area (not just the grid) at `pos`. The surface has
689 /// strictly more ways to refuse a span than [`Grid::write_span`] does, so a sprite that did
690 /// not draw is answered here rather than in the backend.
691 pub fn put_span<S: AsRef<str>>(
692 &mut self,
693 pos: impl Into<Pos>,
694 rows: &[S],
695 style: Style,
696 ) -> Option<()> {
697 let pos = pos.into();
698 let (x, y) = self.shift(pos.x, pos.y)?;
699 let cols = rows.first()?.as_ref().chars().count();
700 let w = u16::try_from(cols).ok()?;
701 let h = u16::try_from(rows.len()).ok()?;
702 if !self.span_fits(Pos::new(x, y), w, h) {
703 return None;
704 }
705 self.grid.write_span(self.layer, x, y, rows, style)?;
706 // The anchor only: a pixel backend draws the whole footprint from that one cell, so the
707 // covered cells have no sprite of their own to recolour.
708 self.apply_tint(x, y);
709 Some(())
710 }
711
712 /// Writes a `size` multi-cell span at `pos` on this surface's layer in `style`: `anchor` in
713 /// the anchor cell, `fill` in every other cell of the footprint, the [`Surface`] twin of
714 /// [`Grid::write_span_uniform`].
715 ///
716 /// The uniform case of [`put_span`](Self::put_span), and what a sheet-driven renderer usually
717 /// wants: one sprite, chosen at runtime, with the cells it covers blanked so nothing shows
718 /// through its transparent pixels. `fill` is the text fallback a *cell* backend prints for
719 /// those covered cells, so `' '` blanks them and a visible character keeps the footprint
720 /// legible in a terminal.
721 ///
722 /// `style` reads exactly as it does for [`put_span`](Self::put_span): it applies to the text
723 /// fallback, never to the sprite.
724 ///
725 /// # Returns
726 ///
727 /// `Some(())` once the whole span is written, or `None` having written nothing at all when
728 /// either axis of `size` is `0` or exceeds 255 cells, or the footprint does not fit entirely
729 /// within this surface's own area at `pos`.
730 ///
731 /// # Examples
732 ///
733 /// ```
734 /// # fn main() {
735 /// # fn run() -> Option<()> {
736 /// use retroglyph_core::{Grid, Rect, Style, Surface};
737 ///
738 /// let mut grid = Grid::new(8, 4);
739 /// let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 8, 4), 0);
740 ///
741 /// // A 16x16 sprite over a 2x1 block of 8x16 cells, anchored at a runtime glyph.
742 /// let anchor = '\u{E000}';
743 /// surface.put_span_uniform((1, 1), (2, 1), anchor, ' ', Style::default())?;
744 /// # Some(())
745 /// # }
746 /// # run().unwrap();
747 /// # }
748 /// ```
749 pub fn put_span_uniform(
750 &mut self,
751 pos: impl Into<Pos>,
752 size: impl Into<Size>,
753 anchor: char,
754 fill: char,
755 style: Style,
756 ) -> Option<()> {
757 let pos = pos.into();
758 let (x, y) = self.shift(pos.x, pos.y)?;
759 let pos = Pos::new(x, y);
760 let size = size.into();
761 if !self.span_fits(pos, size.width, size.height) {
762 return None;
763 }
764 self.grid
765 .write_span_uniform(self.layer, pos, size, anchor, fill, style)?;
766 self.apply_tint(pos.x, pos.y);
767 Some(())
768 }
769
770 /// `true` if a `w` x `h` footprint at `pos` lies entirely within this surface's area.
771 ///
772 /// A span is all-or-nothing rather than clipped like the per-cell writes, because a
773 /// footprint half outside the area would reserve cells the caller does not own.
774 fn span_fits(&self, pos: Pos, w: u16, h: u16) -> bool {
775 pos.x >= self.area.left()
776 && pos.y >= self.area.top()
777 && pos.x.saturating_add(w) <= self.area.right()
778 && pos.y.saturating_add(h) <= self.area.bottom()
779 }
780
781 /// Place `ch` at `pos` with a sub-cell pixel `offset`, in `style`.
782 ///
783 /// Sub-cell offsets are visual only: they do not affect grid logic or hit-testing.
784 /// Backends that cannot represent pixel offsets (e.g. `CrosstermBackend`) ignore them. A
785 /// no-op if `pos` is outside this surface's area.
786 ///
787 /// # Examples
788 ///
789 /// ```
790 /// use retroglyph_core::{Grid, Offset, Pos, Rect, Style, Surface};
791 ///
792 /// let mut grid = Grid::new(4, 4);
793 /// let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0);
794 ///
795 /// // A large offset still lands the glyph in cell (1, 1): the offset is a pixel nudge
796 /// // for a pixel backend, never a coordinate shift.
797 /// surface.put_offset((1, 1), Offset::new(12, -12), 'X', Style::default());
798 /// // Outside the surface's area: silently dropped, matching `put`.
799 /// surface.put_offset((10, 10), Offset::default(), 'X', Style::default());
800 ///
801 /// assert_eq!(grid[Pos::new(1, 1)].glyph(), 'X');
802 /// ```
803 pub fn put_offset(
804 &mut self,
805 pos: impl Into<Pos>,
806 offset: impl Into<Offset>,
807 ch: char,
808 style: Style,
809 ) {
810 let pos = pos.into();
811 let Some((x, y)) = self.shift(pos.x, pos.y) else {
812 return;
813 };
814 let offset = offset.into();
815 let tile = Tile::new(ch, style).with_offset(offset.dx, offset.dy);
816 self.grid.put_tile(self.layer, (x, y), tile);
817 }
818
819 /// Clears this surface's entire area (on its own layer) back to [`Tile::default`].
820 pub fn clear(&mut self) {
821 let area = self.area;
822 for y in area.top()..area.bottom() {
823 for x in area.left()..area.right() {
824 self.grid.put_tile(self.layer, (x, y), Tile::default());
825 }
826 }
827 }
828
829 /// Clears `rect` (clipped to this surface's own area, on its own layer) back to
830 /// [`Tile::default`].
831 ///
832 /// # Examples
833 ///
834 /// ```
835 /// use retroglyph_core::{Grid, Pos, Rect, Style, Surface};
836 ///
837 /// let mut grid = Grid::new(4, 4);
838 /// let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0);
839 /// surface.fill_rect(Rect::new(0, 0, 4, 4), '#', Style::default());
840 ///
841 /// // `rect` extends past the surface's own area; only the overlap is cleared.
842 /// surface.clear_region(Rect::new(2, 2, 10, 10));
843 ///
844 /// assert_eq!(grid[Pos::new(2, 2)].glyph(), ' ');
845 /// assert_eq!(grid[Pos::new(1, 1)].glyph(), '#');
846 /// ```
847 pub fn clear_region(&mut self, rect: Rect) {
848 for y in rect.top()..rect.bottom() {
849 for x in rect.left()..rect.right() {
850 if let Some((x, y)) = self.shift(x, y) {
851 self.grid.put_tile(self.layer, (x, y), Tile::default());
852 }
853 }
854 }
855 }
856}
857
858/// A [`Surface`] with a [`Style`] bound in, returned by [`Surface::with_style`].
859///
860/// Every draw call omits the `style` argument the underlying [`Surface`] method would otherwise
861/// need, using the bound style instead. Reach back to the underlying surface (e.g. to call
862/// [`Surface::print_line`], whose per-span styles make a bound style meaningless) via
863/// [`StyledSurface::surface`].
864pub struct StyledSurface<'s, 'a> {
865 surface: &'s mut Surface<'a>,
866 style: Style,
867}
868
869impl<'a> StyledSurface<'_, 'a> {
870 /// The style every draw call on this view uses.
871 #[must_use]
872 pub const fn style(&self) -> Style {
873 self.style
874 }
875
876 /// Borrows the underlying [`Surface`] directly, for calls that need an explicit style (e.g.
877 /// [`Surface::print_line`]) or a capability [`StyledSurface`] doesn't expose.
878 pub const fn surface(&mut self) -> &mut Surface<'a> {
879 self.surface
880 }
881
882 /// [`Surface::put`] using this view's bound style.
883 pub fn put(&mut self, pos: impl Into<Pos>, ch: char) {
884 self.surface.put(pos, ch, self.style);
885 }
886
887 /// [`Surface::print`] using this view's bound style.
888 pub fn print(&mut self, pos: impl Into<Pos>, text: &str) {
889 self.surface.print(pos, text, self.style);
890 }
891
892 /// [`Surface::fill_rect`] using this view's bound style.
893 pub fn fill_rect(&mut self, rect: Rect, ch: char) {
894 self.surface.fill_rect(rect, ch, self.style);
895 }
896
897 /// [`Surface::put_span`] using this view's bound style.
898 pub fn put_span<S: AsRef<str>>(&mut self, pos: impl Into<Pos>, rows: &[S]) -> Option<()> {
899 self.surface.put_span(pos, rows, self.style)
900 }
901
902 /// [`Surface::put_span_uniform`] using this view's bound style.
903 pub fn put_span_uniform(
904 &mut self,
905 pos: impl Into<Pos>,
906 size: impl Into<Size>,
907 anchor: char,
908 fill: char,
909 ) -> Option<()> {
910 self.surface
911 .put_span_uniform(pos, size, anchor, fill, self.style)
912 }
913
914 /// [`Surface::put_offset`] using this view's bound style.
915 pub fn put_offset(&mut self, pos: impl Into<Pos>, offset: impl Into<Offset>, ch: char) {
916 self.surface.put_offset(pos, offset, ch, self.style);
917 }
918}
919
920#[cfg(test)]
921mod tests {
922 use super::*;
923
924 fn screen(grid: &mut Grid) -> Surface<'_> {
925 let area = Rect::new(0, 0, grid.width(), grid.height());
926 Surface::new(grid, area, 0)
927 }
928
929 #[test]
930 fn put_span_takes_any_as_ref_str_row() {
931 let mut grid = Grid::new(4, 4);
932 // A footprint computed at runtime: owned rows, no borrowing pass over them.
933 let rows: Vec<String> = (0..2)
934 .map(|row| {
935 (0..2)
936 .map(|col| if (row, col) == (0, 0) { 'C' } else { ' ' })
937 .collect()
938 })
939 .collect();
940
941 assert_eq!(
942 screen(&mut grid).put_span((0, 0), &rows, Style::default()),
943 Some(())
944 );
945 assert_eq!(grid[Pos::new(0, 0)].span(), (2, 2));
946 }
947
948 #[test]
949 fn put_span_reports_why_a_span_did_not_draw() {
950 let mut grid = Grid::new(4, 4);
951 let area = Rect::new(0, 0, 2, 2);
952 let mut surface = Surface::new(&mut grid, area, 0);
953 let style = Style::default();
954
955 assert_eq!(surface.put_span((0, 0), &[] as &[&str], style), None);
956 assert_eq!(surface.put_span((0, 0), &[""], style), None);
957 // Ragged rows are refused by the grid, and that answer is passed through.
958 assert_eq!(surface.put_span((0, 0), &["ab", "c"], style), None);
959 // Fits the grid, but leaves the surface's own area.
960 assert_eq!(surface.put_span((1, 1), &["ab"], style), None);
961 assert_eq!(surface.put_span((0, 0), &["ab"], style), Some(()));
962 }
963
964 #[test]
965 fn put_span_uniform_writes_the_anchor_once_and_fills_the_rest() {
966 let mut grid = Grid::new(4, 4);
967 assert_eq!(
968 screen(&mut grid).put_span_uniform((1, 1), (2, 2), 'C', '.', Style::default()),
969 Some(())
970 );
971
972 assert_eq!(grid[Pos::new(1, 1)].glyph(), 'C');
973 assert_eq!(grid[Pos::new(1, 1)].span(), (2, 2));
974 assert_eq!(grid[Pos::new(2, 2)].glyph(), '.');
975 assert_eq!(grid.span_owner(0, 2, 2), Some(Pos::new(1, 1)));
976 }
977
978 #[test]
979 fn put_span_uniform_writes_to_this_surfaces_layer() {
980 let mut grid = Grid::new(4, 4);
981 {
982 let mut surface = screen(&mut grid);
983 surface
984 .on_layer(2)
985 .put_span_uniform((0, 0), (2, 1), 'C', ' ', Style::default())
986 .expect("span write");
987 }
988
989 assert_eq!(grid.span_owner(2, 1, 0), Some(Pos::new(0, 0)));
990 assert_eq!(grid.span_owner(0, 1, 0), None);
991 }
992
993 #[test]
994 fn put_span_uniform_refuses_a_footprint_that_leaves_the_surfaces_area() {
995 let mut grid = Grid::new(4, 4);
996 let area = Rect::new(0, 0, 2, 2);
997 let mut surface = Surface::new(&mut grid, area, 0);
998 let style = Style::default();
999
1000 // Both fit the grid; neither fits the area.
1001 assert_eq!(
1002 surface.put_span_uniform((1, 0), (2, 1), 'C', ' ', style),
1003 None
1004 );
1005 assert_eq!(
1006 surface.put_span_uniform((0, 1), (1, 2), 'C', ' ', style),
1007 None
1008 );
1009 assert_eq!(
1010 surface.put_span_uniform((0, 0), (0, 1), 'C', ' ', style),
1011 None
1012 );
1013 assert_eq!(
1014 surface.put_span_uniform((0, 0), (2, 2), 'C', ' ', style),
1015 Some(())
1016 );
1017 }
1018
1019 #[test]
1020 fn styled_surface_forwards_both_span_calls() {
1021 let mut grid = Grid::new(4, 4);
1022 {
1023 let mut surface = screen(&mut grid);
1024 let mut styled = surface.with_style(Style::new().fg(Color::RED));
1025 styled.put_span((0, 0), &["ab"]).expect("span write");
1026 styled
1027 .put_span_uniform((0, 1), (2, 1), 'C', ' ')
1028 .expect("span write");
1029 }
1030
1031 assert_eq!(grid[Pos::new(0, 0)].style().foreground(), Color::RED);
1032 assert_eq!(grid[Pos::new(0, 1)].style().foreground(), Color::RED);
1033 assert_eq!(grid[Pos::new(0, 1)].span(), (2, 1));
1034 }
1035
1036 #[test]
1037 fn with_tint_applies_to_the_cell_it_writes() {
1038 let mut grid = Grid::new(4, 4);
1039 {
1040 let mut surface = screen(&mut grid);
1041 surface
1042 .with_tint(Tint::multiply(128, 64, 32))
1043 .put((1, 1), '@', Style::default());
1044 }
1045
1046 assert_eq!(grid[Pos::new(1, 1)].glyph(), '@');
1047 assert_eq!(grid.tint(0, 1, 1), Tint::multiply(128, 64, 32));
1048 }
1049
1050 #[test]
1051 fn an_untinted_surface_leaves_the_side_table_alone() {
1052 let mut grid = Grid::new(4, 4);
1053 screen(&mut grid).put((1, 1), '@', Style::default());
1054
1055 assert_eq!(grid.tint(0, 1, 1), Tint::None);
1056 }
1057
1058 #[test]
1059 fn with_tint_lands_on_the_span_anchor_only() {
1060 let mut grid = Grid::new(4, 4);
1061 {
1062 let mut surface = screen(&mut grid);
1063 surface
1064 .with_tint(Tint::multiply(200, 200, 200))
1065 .put_span((0, 0), &["ab", "cd"], Style::default())
1066 .expect("span write");
1067 }
1068
1069 // A pixel backend draws the whole footprint from the anchor, so that is the only cell
1070 // with a sprite to recolour.
1071 assert_eq!(grid.tint(0, 0, 0), Tint::multiply(200, 200, 200));
1072 assert_eq!(grid.tint(0, 1, 0), Tint::None);
1073 assert_eq!(grid.tint(0, 1, 1), Tint::None);
1074 }
1075
1076 #[test]
1077 fn with_tint_applies_to_a_uniform_span_anchor() {
1078 let mut grid = Grid::new(4, 4);
1079 {
1080 let mut surface = screen(&mut grid);
1081 surface
1082 .with_tint(Tint::mix(255, 0, 0, 128))
1083 .put_span_uniform((1, 1), (2, 2), 'C', '.', Style::default())
1084 .expect("span write");
1085 }
1086
1087 assert_eq!(grid.tint(0, 1, 1), Tint::mix(255, 0, 0, 128));
1088 assert_eq!(grid.tint(0, 2, 2), Tint::None);
1089 }
1090
1091 #[test]
1092 fn with_tint_is_not_applied_to_a_refused_span() {
1093 let mut grid = Grid::new(4, 4);
1094 let area = Rect::new(0, 0, 2, 2);
1095 {
1096 let mut surface = Surface::new(&mut grid, area, 0);
1097 // Fits the grid, leaves the area: nothing is written, so nothing is tinted.
1098 assert_eq!(
1099 surface.with_tint(Tint::multiply(1, 2, 3)).put_span(
1100 (1, 1),
1101 &["ab"],
1102 Style::default()
1103 ),
1104 None
1105 );
1106 }
1107
1108 assert_eq!(grid.tint(0, 1, 1), Tint::None);
1109 }
1110
1111 #[test]
1112 fn with_tint_survives_clip_and_on_layer() {
1113 let mut grid = Grid::new(8, 4);
1114 {
1115 let mut surface = screen(&mut grid);
1116 let mut tinted = surface.with_tint(Tint::multiply(9, 9, 9));
1117 assert_eq!(tinted.tint(), Tint::multiply(9, 9, 9));
1118 assert_eq!(
1119 tinted.clip(Rect::new(0, 0, 4, 4)).tint(),
1120 Tint::multiply(9, 9, 9)
1121 );
1122 assert_eq!(tinted.on_layer(2).tint(), Tint::multiply(9, 9, 9));
1123
1124 tinted.on_layer(2).put((1, 1), '@', Style::default());
1125 }
1126
1127 assert_eq!(grid.tint(2, 1, 1), Tint::multiply(9, 9, 9));
1128 }
1129
1130 #[test]
1131 fn with_tint_replaces_rather_than_composes() {
1132 let mut grid = Grid::new(4, 4);
1133 {
1134 let mut surface = screen(&mut grid);
1135 let mut outer = surface.with_tint(Tint::multiply(128, 128, 128));
1136 // Unlike `clip`, a nested tint substitutes: two tints have no meaningful product.
1137 outer
1138 .with_tint(Tint::mix(255, 0, 0, 64))
1139 .put((0, 0), '@', Style::default());
1140 }
1141
1142 assert_eq!(grid.tint(0, 0, 0), Tint::mix(255, 0, 0, 64));
1143 }
1144
1145 #[test]
1146 fn clip_narrows_the_area_and_keeps_the_coordinate_space() {
1147 let mut grid = Grid::new(8, 4);
1148 let mut surface = screen(&mut grid);
1149 let sub = surface.clip(Rect::new(2, 1, 4, 2));
1150
1151 assert_eq!(sub.area(), Rect::new(2, 1, 4, 2));
1152 assert_eq!(sub.width(), 4);
1153 assert_eq!(sub.height(), 2);
1154 }
1155
1156 #[test]
1157 fn clip_keeps_the_layer() {
1158 let mut grid = Grid::new(4, 4);
1159 let mut surface = screen(&mut grid);
1160 let mut layer1 = surface.on_layer(1);
1161
1162 assert_eq!(layer1.clip(Rect::new(0, 0, 2, 2)).layer(), 1);
1163 }
1164
1165 #[test]
1166 fn clip_intersects_rather_than_replaces_so_it_cannot_widen() {
1167 let mut grid = Grid::new(8, 4);
1168 let area = Rect::new(2, 1, 4, 2);
1169 let mut surface = Surface::new(&mut grid, area, 0);
1170
1171 // A rect reaching outside the surface's own area only ever tightens it.
1172 assert_eq!(surface.clip(Rect::new(0, 0, 8, 4)).area(), area);
1173 assert_eq!(
1174 surface.clip(Rect::new(0, 0, 4, 4)).area(),
1175 Rect::new(2, 1, 2, 2)
1176 );
1177 }
1178
1179 #[test]
1180 fn clip_writes_outside_the_sub_rect_are_dropped() {
1181 let mut grid = Grid::new(4, 2);
1182 {
1183 let mut surface = screen(&mut grid);
1184 let mut top = surface.clip(Rect::new(0, 0, 4, 1));
1185 top.put((1, 0), 'a', Style::default());
1186 // Inside the surface's own area, outside the clip.
1187 top.put((1, 1), 'b', Style::default());
1188 }
1189
1190 assert_eq!(grid[Pos::new(1, 0)].glyph(), 'a');
1191 assert_eq!(grid[Pos::new(1, 1)].glyph(), ' ');
1192 }
1193
1194 #[test]
1195 fn clip_to_one_row_drops_print_overflow_instead_of_wrapping_it() {
1196 let mut grid = Grid::new(4, 2);
1197 {
1198 let mut surface = screen(&mut grid);
1199 surface
1200 .clip(Rect::new(0, 0, 4, 1))
1201 .print((0, 0), "abcdef", Style::default());
1202 }
1203
1204 assert_eq!(grid[Pos::new(3, 0)].glyph(), 'd');
1205 // "ef" wrapped onto row 1, which the clip excludes.
1206 assert_eq!(grid[Pos::new(0, 1)].glyph(), ' ');
1207 }
1208
1209 #[test]
1210 fn clip_makes_put_span_measure_its_footprint_against_the_sub_rect() {
1211 let mut grid = Grid::new(4, 3);
1212 {
1213 let mut surface = screen(&mut grid);
1214 // Fits the grid, but reserves a cell on the bottom row the clip excludes.
1215 surface
1216 .clip(Rect::new(0, 0, 4, 2))
1217 .put_span((0, 1), &["ab", "cd"], Style::default());
1218 }
1219
1220 assert_eq!(grid[Pos::new(0, 1)].glyph(), ' ');
1221
1222 let mut surface = screen(&mut grid);
1223 surface
1224 .clip(Rect::new(0, 0, 4, 2))
1225 .put_span((0, 0), &["ab", "cd"], Style::default());
1226
1227 assert_eq!(grid[Pos::new(0, 0)].span(), (2, 2));
1228 }
1229
1230 #[test]
1231 fn clip_makes_put_span_uniform_measure_its_footprint_against_the_sub_rect() {
1232 let mut grid = Grid::new(4, 3);
1233 let style = Style::default();
1234 {
1235 let mut surface = screen(&mut grid);
1236 let mut content = surface.clip(Rect::new(0, 0, 4, 2));
1237 // Fits the grid, but reserves a cell on the bottom row the clip excludes.
1238 assert_eq!(
1239 content.put_span_uniform((0, 1), (2, 2), 'C', '.', style),
1240 None
1241 );
1242 assert_eq!(
1243 content.put_span_uniform((0, 0), (2, 2), 'C', '.', style),
1244 Some(())
1245 );
1246 }
1247
1248 assert_eq!(grid[Pos::new(0, 0)].span(), (2, 2));
1249 assert_eq!(grid[Pos::new(0, 2)].glyph(), ' ');
1250 }
1251
1252 #[test]
1253 fn clip_to_a_disjoint_rect_is_empty_and_drops_every_write() {
1254 let mut grid = Grid::new(8, 4);
1255 {
1256 let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0);
1257 let mut sub = surface.clip(Rect::new(4, 0, 4, 4));
1258 assert_eq!(sub.area(), Rect::EMPTY);
1259 sub.print((0, 0), "abc", Style::default());
1260 }
1261
1262 assert_eq!(grid[Pos::new(0, 0)].glyph(), ' ');
1263 }
1264
1265 #[test]
1266 fn put_signed_drops_a_negative_coordinate() {
1267 let mut grid = Grid::new(4, 4);
1268 let mut surface = screen(&mut grid);
1269
1270 surface.put_signed((-1, 0), 'X', Style::default());
1271 surface.put_signed((0, -1), 'X', Style::default());
1272
1273 assert_eq!(grid[Pos::new(0, 0)].glyph(), ' ');
1274 }
1275
1276 #[test]
1277 fn put_signed_lands_a_valid_coordinate_at_the_area_origin() {
1278 let mut grid = Grid::new(4, 4);
1279 let area = Rect::new(1, 1, 2, 2);
1280 let mut surface = Surface::new(&mut grid, area, 0);
1281
1282 // (0, 0) relative to the area's own origin is grid position (1, 1).
1283 surface.put_signed((0, 0), 'X', Style::default());
1284
1285 assert_eq!(grid[Pos::new(1, 1)].glyph(), 'X');
1286 }
1287
1288 #[test]
1289 fn put_signed_drops_a_coordinate_past_this_surfaces_width_or_height() {
1290 let mut grid = Grid::new(4, 4);
1291 let area = Rect::new(0, 0, 2, 2);
1292 let mut surface = Surface::new(&mut grid, area, 0);
1293
1294 // Fits the grid, but not this surface's own (relative) width/height.
1295 surface.put_signed((2, 0), 'X', Style::default());
1296 surface.put_signed((0, 2), 'X', Style::default());
1297
1298 assert_eq!(grid[Pos::new(2, 0)].glyph(), ' ');
1299 assert_eq!(grid[Pos::new(0, 2)].glyph(), ' ');
1300 }
1301
1302 #[test]
1303 fn translate_does_not_change_area_width_or_height() {
1304 let mut grid = Grid::new(10, 10);
1305 let mut surface = screen(&mut grid);
1306 let mut clipped = surface.clip(Rect::new(5, 5, 4, 4));
1307 let view = clipped.translate((-5, -5));
1308
1309 assert_eq!(view.area(), Rect::new(5, 5, 4, 4));
1310 assert_eq!(view.width(), 4);
1311 assert_eq!(view.height(), 4);
1312 }
1313
1314 #[test]
1315 fn translate_shifts_put_by_subtracting_the_origin() {
1316 let mut grid = Grid::new(10, 10);
1317 {
1318 let mut surface = screen(&mut grid);
1319 let mut view = surface.translate((3, 3));
1320
1321 // (3, 3) minus the translate origin (3, 3) is (0, 0).
1322 view.put((3, 3), 'A', Style::default());
1323 // (2, 3) minus (3, 3) is negative on the x axis: out of bounds, dropped.
1324 view.put((2, 3), 'B', Style::default());
1325 }
1326
1327 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'A');
1328 assert_eq!(grid[Pos::new(0, 3)].glyph(), ' ');
1329 }
1330
1331 #[test]
1332 fn translate_composes_with_clip_and_lets_a_negative_signed_coordinate_land() {
1333 let mut grid = Grid::new(10, 10);
1334 {
1335 let mut surface = screen(&mut grid);
1336 let mut clipped = surface.clip(Rect::new(5, 5, 4, 4));
1337 let mut view = clipped.translate((-5, -5));
1338
1339 // -5 minus the translate origin (-5) is 0: the viewport's own local origin, landing
1340 // at the clipped area's top-left grid cell.
1341 view.put_signed((-5, -5), 'X', Style::default());
1342 // -6 minus -5 is still -1: still negative, so still out of bounds.
1343 view.put_signed((-6, -6), 'Y', Style::default());
1344 }
1345
1346 assert_eq!(grid[Pos::new(5, 5)].glyph(), 'X');
1347 assert_eq!(grid[Pos::new(4, 4)].glyph(), ' ');
1348 }
1349
1350 #[test]
1351 fn translate_composes_additively_across_two_calls() {
1352 let mut grid = Grid::new(10, 10);
1353 {
1354 let mut surface = screen(&mut grid);
1355 let mut once = surface.translate((2, 0));
1356 let mut twice = once.translate((1, 0));
1357
1358 // Composed origin is (3, 0): (3, 0) minus (3, 0) is (0, 0).
1359 twice.put((3, 0), 'A', Style::default());
1360 }
1361
1362 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'A');
1363 }
1364
1365 #[test]
1366 fn translate_shifts_fill_rect_print_and_clear_region_via_put() {
1367 let mut grid = Grid::new(10, 10);
1368 {
1369 let mut surface = screen(&mut grid);
1370 let mut view = surface.translate((5, 5));
1371 view.fill_rect(Rect::new(5, 5, 2, 2), '#', Style::default());
1372 view.print((5, 6), "a", Style::default());
1373 }
1374
1375 assert_eq!(grid[Pos::new(0, 0)].glyph(), '#');
1376 assert_eq!(grid[Pos::new(1, 1)].glyph(), '#');
1377 assert_eq!(grid[Pos::new(0, 1)].glyph(), 'a');
1378 }
1379
1380 #[test]
1381 fn translate_shifts_clear_region() {
1382 let mut grid = Grid::new(10, 10);
1383 {
1384 let mut surface = screen(&mut grid);
1385 surface.fill_rect(Rect::new(0, 0, 4, 4), '#', Style::default());
1386 let mut view = surface.translate((2, 2));
1387 // Clears grid (0..2, 0..2) once shifted by the translate origin.
1388 view.clear_region(Rect::new(2, 2, 2, 2));
1389 }
1390
1391 assert_eq!(grid[Pos::new(0, 0)].glyph(), ' ');
1392 assert_eq!(grid[Pos::new(1, 1)].glyph(), ' ');
1393 assert_eq!(grid[Pos::new(2, 2)].glyph(), '#');
1394 }
1395
1396 #[test]
1397 fn translate_shifts_put_span_and_put_span_uniform() {
1398 let mut grid = Grid::new(10, 10);
1399 {
1400 let mut surface = screen(&mut grid);
1401 let mut view = surface.translate((4, 4));
1402 assert_eq!(view.put_span((4, 4), &["ab"], Style::default()), Some(()));
1403 assert_eq!(
1404 view.put_span_uniform((6, 4), (2, 1), 'C', ' ', Style::default()),
1405 Some(())
1406 );
1407 }
1408
1409 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'a');
1410 assert_eq!(grid[Pos::new(2, 0)].glyph(), 'C');
1411 }
1412
1413 #[test]
1414 fn clear_is_unaffected_by_translate() {
1415 let mut grid = Grid::new(4, 4);
1416 {
1417 let mut surface = screen(&mut grid);
1418 surface.fill_rect(Rect::new(0, 0, 4, 4), '#', Style::default());
1419 let mut view = surface.translate((100, 100));
1420 // `clear` takes no coordinate, so the translate offset does not apply to it: it
1421 // always clears this surface's own area.
1422 view.clear();
1423 }
1424
1425 assert_eq!(grid[Pos::new(0, 0)].glyph(), ' ');
1426 assert_eq!(grid[Pos::new(3, 3)].glyph(), ' ');
1427 }
1428
1429 #[test]
1430 fn grid_is_the_read_only_counterpart_of_grid_mut() {
1431 let mut grid = Grid::new(4, 4);
1432 let mut surface = screen(&mut grid);
1433 surface.put((1, 1), 'X', Style::default());
1434
1435 assert_eq!(surface.grid()[Pos::new(1, 1)].glyph(), 'X');
1436 }
1437
1438 #[test]
1439 fn tile_reads_a_written_cell_without_a_mutable_borrow() {
1440 let mut grid = Grid::new(4, 4);
1441 let mut surface = screen(&mut grid);
1442 surface.put((1, 1), 'X', Style::default());
1443
1444 assert_eq!(surface.tile((1, 1)).map(Tile::glyph), Some('X'));
1445 assert_eq!(surface.tile((0, 0)).map(Tile::glyph), Some(' '));
1446 assert_eq!(surface.tile((10, 10)), None);
1447 }
1448
1449 #[test]
1450 fn background_reads_the_styles_background_colour() {
1451 let mut grid = Grid::new(4, 4);
1452 let mut surface = screen(&mut grid);
1453 surface.put((1, 1), 'X', Style::new().bg(Color::RED));
1454
1455 assert_eq!(surface.background((1, 1)), Some(Color::RED));
1456 assert_eq!(surface.background((10, 10)), None);
1457 }
1458
1459 #[test]
1460 #[cfg(feature = "egc")]
1461 fn print_aligned_left_aligns_by_default() {
1462 let mut grid = Grid::new(8, 1);
1463 {
1464 let mut surface = screen(&mut grid);
1465 surface.print_aligned(
1466 Rect::new(0, 0, 8, 1),
1467 "hi",
1468 crate::layout::HAlign::Left,
1469 Style::default(),
1470 );
1471 }
1472
1473 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'h');
1474 assert_eq!(grid[Pos::new(1, 0)].glyph(), 'i');
1475 assert_eq!(grid[Pos::new(2, 0)].glyph(), ' ');
1476 }
1477
1478 #[test]
1479 #[cfg(feature = "egc")]
1480 fn print_aligned_centers_matching_text_layouts_own_saturating_formula() {
1481 let mut grid = Grid::new(6, 1);
1482 {
1483 let mut surface = screen(&mut grid);
1484 surface.print_aligned(
1485 Rect::new(0, 0, 6, 1),
1486 "hi",
1487 crate::layout::HAlign::Center,
1488 Style::default(),
1489 );
1490 }
1491
1492 // (6 - 2) / 2 == 2 columns of left padding, matching `HAlign::Center` in `layout.rs`.
1493 assert_eq!(grid[Pos::new(2, 0)].glyph(), 'h');
1494 assert_eq!(grid[Pos::new(3, 0)].glyph(), 'i');
1495 }
1496
1497 #[test]
1498 #[cfg(feature = "egc")]
1499 fn print_aligned_right_aligns_flush_to_the_rects_right_edge() {
1500 let mut grid = Grid::new(6, 1);
1501 {
1502 let mut surface = screen(&mut grid);
1503 surface.print_aligned(
1504 Rect::new(0, 0, 6, 1),
1505 "hi",
1506 crate::layout::HAlign::Right,
1507 Style::default(),
1508 );
1509 }
1510
1511 assert_eq!(grid[Pos::new(4, 0)].glyph(), 'h');
1512 assert_eq!(grid[Pos::new(5, 0)].glyph(), 'i');
1513 }
1514
1515 #[test]
1516 #[cfg(feature = "egc")]
1517 fn print_aligned_does_not_panic_or_underflow_on_text_wider_than_the_rect() {
1518 let mut grid = Grid::new(4, 1);
1519 {
1520 let mut surface = screen(&mut grid);
1521 // "hello" is wider than the 4-column rect on every alignment: this must not panic
1522 // (a plain `rect.width() - text_width` would underflow) and instead left-aligns and
1523 // lets `print` clip the overflow.
1524 surface.print_aligned(
1525 Rect::new(0, 0, 4, 1),
1526 "hello",
1527 crate::layout::HAlign::Center,
1528 Style::default(),
1529 );
1530 }
1531
1532 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'h');
1533 assert_eq!(grid[Pos::new(3, 0)].glyph(), 'l');
1534 }
1535
1536 #[test]
1537 #[cfg(feature = "egc")]
1538 fn print_aligned_clips_to_this_surfaces_own_area_as_well_as_rect() {
1539 let mut grid = Grid::new(4, 1);
1540 {
1541 let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 2, 1), 0);
1542 // `rect` extends past this surface's own area; the write is still clipped to it.
1543 surface.print_aligned(
1544 Rect::new(0, 0, 4, 1),
1545 "hi",
1546 crate::layout::HAlign::Right,
1547 Style::default(),
1548 );
1549 }
1550
1551 assert_eq!(grid[Pos::new(0, 0)].glyph(), ' ');
1552 assert_eq!(grid[Pos::new(1, 0)].glyph(), ' ');
1553 }
1554
1555 #[test]
1556 fn clip_nests_monotonically() {
1557 let mut grid = Grid::new(8, 4);
1558 let mut surface = screen(&mut grid);
1559 let mut outer = surface.clip(Rect::new(1, 1, 4, 2));
1560 let inner = outer.clip(Rect::new(0, 0, 8, 4));
1561
1562 assert_eq!(inner.area(), Rect::new(1, 1, 4, 2));
1563 }
1564}