Skip to main content

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}