retroglyph_core/surface/mod.rs
1//! [`Surface`](crate::surface::Surface): an area-clipped, single-layer view over a [`Grid`](crate::grid::Grid).
2//!
3//! `Surface` is the workspace's one grid-drawing primitive. [`Terminal`](crate::terminal::Terminal)'s
4//! [`draw`](crate::terminal::Terminal::draw)/[`surface`](crate::terminal::Terminal::surface) hand out a `Surface`
5//! scoped to the whole grid, and `retroglyph-ui` renders every widget into a `Surface`
6//! scoped to a sub-[`Rect`](crate::grid::Rect): there is no separate stateful drawing API on `Terminal` itself.
7//!
8//! Place characters directly with [`put`](crate::surface::Surface::put) (or [`print`](crate::surface::Surface::print) for a
9//! string, which handles newlines and wide characters), style-aware spans with
10//! [`print_line`](crate::surface::Surface::print_line), or a whole styled run with
11//! [`with_style`](crate::surface::Surface::with_style) so repeated calls don't need to pass the same [`Style`](crate::color::Style)
12//! each time. [`clear`](crate::surface::Surface::clear)/[`clear_region`](crate::surface::Surface::clear_region) blank the active
13//! layer (in full, or a rectangular region); switch layers with
14//! [`on_layer`](crate::surface::Surface::on_layer). Or bypass the builder entirely and reach the [`Grid`](crate::grid::Grid)
15//! directly via [`grid_mut`](crate::surface::Surface::grid_mut).
16//!
17//! For why `Surface` uses `put_*`/`print_*` where `Grid` uses `put_*`/`write_*`, see
18//! ["Naming"](crate::grid#naming-put_write_print_) in the `grid` module docs.
19
20use crate::color::Tint;
21use crate::grid::{Grid, Rect};
22
23mod draw;
24mod geometry;
25mod styled;
26
27#[cfg(test)]
28mod tests;
29
30pub use styled::StyledSurface;
31
32/// The render target for every drawing call in the workspace: a mutable reference to a
33/// [`Grid`](crate::grid::Grid) plus a fixed `layer`, scoped to an `area` and clipped to a `clip` rect.
34///
35/// A `Surface` is typically created once per frame, scoped to the whole drawing surface (e.g.
36/// via [`Terminal::draw`](crate::terminal::Terminal::draw)), and handed to every subsystem/widget in turn;
37/// each caller's own `area: Rect` (a sub-rect of the surface's own area, e.g. one produced by a
38/// layout split) is relative to this surface's own `area` origin, not to the underlying grid.
39/// [`Surface::put`](crate::surface::Surface::put)/[`Surface::print`](crate::surface::Surface::print)/... take coordinates in that same local space, where
40/// `(0, 0)` is `area`'s top-left corner, and silently drop any write that falls outside
41/// [`Surface::clip_rect`](crate::surface::Surface::clip_rect), matching the rest of the workspace's clip-on-draw policy for
42/// out-of-bounds drawing.
43///
44/// `area` and `clip_rect` answer two different questions. `area` is the region this surface
45/// *represents*: what a widget lays itself out in, and what [`width`](Self::width)/
46/// [`height`](Self::height) report. `clip_rect` is the subset of `area` that is actually
47/// *visible*: what every write is bounds-checked against. The two start out equal (see
48/// [`Surface::new`](crate::surface::Surface::new)) and diverge once [`Surface::clip`](crate::surface::Surface::clip) or [`Surface::scope`](crate::surface::Surface::scope) is used.
49///
50/// [`Surface::clip`](crate::surface::Surface::clip) narrows what is visible without changing what this surface represents:
51/// `clip_rect` is intersected with the given rect, `area` is untouched. [`Surface::scope`](crate::surface::Surface::scope) does
52/// both: `area` becomes the given rect and `clip_rect` is intersected with it, which is what a
53/// widget's own sub-surface needs when it should be laid out against a new rect but still bounded
54/// by whatever was already visible. Both narrow monotonically: neither can widen `clip_rect`
55/// beyond what the parent surface already allowed.
56///
57/// A caller that genuinely needs more than one layer at once (e.g. a modal dimming layer 0 while
58/// drawing its own content on layer 1) switches layers with [`Surface::on_layer`](crate::surface::Surface::on_layer)/[`Surface::on_tier`](crate::surface::Surface::on_tier)
59/// rather than being restricted to the layer it was constructed with.
60pub struct Surface<'a> {
61 grid: &'a mut Grid,
62 area: Rect,
63 clip: Rect,
64 layer: u8,
65 tint: Tint,
66 origin_offset: (i32, i32),
67}
68
69/// A named z-order tier for [`Surface::on_tier`](crate::surface::Surface::on_tier), covering the split most apps with overlapping
70/// UI actually need.
71///
72/// Layers are how overlapping UI avoids depending on draw order: a caller who paints a dropdown
73/// on [`Layer::Overlay`](crate::surface::Layer::Overlay) gets it on top of the active screen regardless of whether the screen or
74/// the dropdown drew first this frame, so the two don't have to agree on an ordering (contrast
75/// with painting both through the same layer, where whichever call happens to run last wins).
76///
77/// `Layer` derives [`Ord`] over its declaration order, so `Layer::World < Layer::Hud <
78/// Layer::Overlay < Layer::Debug` holds without spelling out the underlying grid layer ids --
79/// the same relationship [`Surface::on_tier`](crate::surface::Surface::on_tier) relies on to keep `Layer::Debug` the top-most tier
80/// no matter what else is open.
81///
82/// This is a convention, not a restriction: [`Surface::on_layer`](crate::surface::Surface::on_layer) still accepts any `u8`, and a
83/// tile map or sprite-heavy app with its own multi-layer scheme (terrain/items/actors/...) has no
84/// reason to route through `Layer` at all. `Layer` exists for the overlapping-*UI* case:
85/// chrome, popups, debug HUDs, where a small, shared, named split is worth more than 256 open
86/// numeric ids.
87///
88/// # Examples
89///
90/// A persistent HUD bar and a dropdown that must paint over it, in either order, because they're
91/// on different tiers rather than racing to draw last:
92///
93/// ```
94/// use retroglyph_core::color::Style;
95/// use retroglyph_core::grid::{Grid, Rect};
96/// use retroglyph_core::surface::{Layer, Surface};
97///
98/// let area = Rect::new(0, 0, 20, 5);
99/// let mut grid = Grid::new(20, 5);
100/// let mut surface = Surface::new(&mut grid, area, Layer::World.as_u8());
101///
102/// // The active screen draws on `World`.
103/// surface.print((0, 0), "screen content", Style::default());
104///
105/// // Chrome draws on `Hud`, above the screen.
106/// surface.on_tier(Layer::Hud).print((0, 0), "File Edit View", Style::default());
107///
108/// // A dropdown draws on `Overlay`, above the HUD: painting it before or after the two calls
109/// // above makes no difference, because it's on a higher tier, not drawn later.
110/// surface.on_tier(Layer::Overlay).print((0, 1), "New", Style::default());
111/// ```
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
113#[non_exhaustive]
114pub enum Layer {
115 /// The active screen: terrain, entities, game/app content. Grid layer 0.
116 #[default]
117 World,
118 /// Persistent chrome: menu bars, status lines, HUD. Grid layer 1.
119 Hud,
120 /// Popups, dropdowns, modals, painted over [`Layer::World`](crate::surface::Layer::World) and [`Layer::Hud`](crate::surface::Layer::Hud) regardless
121 /// of draw order. Grid layer 2.
122 Overlay,
123 /// Debug and dev tooling. Always the top-most tier, so it stays visible over an open
124 /// [`Layer::Overlay`](crate::surface::Layer::Overlay) rather than being hidden underneath one. Grid layer 3.
125 ///
126 /// `retroglyph-ui`' `PerfOverlayApp` default layer is defined as `Layer::Debug.as_u8()`
127 /// for exactly this reason: a perf HUD that a popup could paint over would be useless
128 /// whenever an app actually has a popup open.
129 Debug,
130}
131
132impl Layer {
133 /// This tier's underlying grid layer id, for [`Surface::on_layer`](crate::surface::Surface::on_layer)/[`Grid`](crate::grid::Grid) APIs that take a
134 /// raw `u8`.
135 #[must_use]
136 pub const fn as_u8(self) -> u8 {
137 self as u8
138 }
139}
140
141impl From<Layer> for u8 {
142 fn from(layer: Layer) -> Self {
143 layer.as_u8()
144 }
145}
146
147impl<'a> Surface<'a> {
148 /// A surface over `grid`, scoped to `area` on `layer`, tinting nothing. `area` starts out
149 /// fully visible: [`area`](Self::area) and [`clip_rect`](Self::clip_rect) are equal until
150 /// [`clip`](Self::clip) or [`scope`](Self::scope) narrows the latter.
151 pub const fn new(grid: &'a mut Grid, area: Rect, layer: u8) -> Self {
152 Self {
153 grid,
154 area,
155 clip: area,
156 layer,
157 tint: Tint::None,
158 origin_offset: (0, 0),
159 }
160 }
161}