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