retroglyph_ui/widget/mod.rs
1//! `Widget`/`StatefulWidget` structs: one file per widget, each a builder
2//! that owns its own drawing logic.
3//!
4//! `new()` takes only the arguments a widget cannot mean anything without
5//! (the content: a value, a label, a slice of samples/rows). Every other
6//! knob (styles, offsets, titles) has a default and is set through a
7//! chainable `#[must_use] fn field(mut self, ...) -> Self` method, the same
8//! shape as [`Panel::title`] or [`Log::offset`]. See `crates/ui/AGENTS.md`
9//! for the rule this is enforcing and why.
10//!
11//! A few widgets share logic: [`Gauge`] and [`StatBar`] both delegate to a
12//! crate-private `bar` module, and [`Sparkline`]/[`Gauge`]/[`StatBar`] all
13//! use [`Meter`] for their ratio-to-color ramp. [`Paragraph`], [`List`],
14//! [`Table`], [`Log`], and [`Panel`] additionally implement [`Measure`], so
15//! a caller can report a height before rendering instead of guessing a
16//! fixed height or a full-remaining-space fill: [`Paragraph`] reports its
17//! wrapped line count (with the `egc` feature enabled, via
18//! `retroglyph_core::layout::TextLayout`'s grapheme-aware word-wrap,
19//! otherwise via its own `char`-boundary-safe fallback); [`List`]/[`Table`]/
20//! [`Log`] report their item/row/message count directly, since none of them
21//! wrap; [`Panel`] reports its border-plus-padding chrome height, since it
22//! owns no content of its own to measure.
23use retroglyph_core::app::Frame;
24
25use crate::Response;
26use crate::Sense;
27use crate::Surface;
28
29mod bar;
30mod border_type;
31mod box_border;
32mod button;
33mod gauge;
34mod highlight_spacing;
35mod list;
36mod list_direction;
37mod log;
38mod meter;
39mod modal;
40mod panel;
41mod paragraph;
42mod perf_overlay;
43mod print_line;
44mod progress_bar;
45mod scrollbar;
46mod sparkline;
47mod stat_bar;
48mod table;
49mod tabs;
50mod text;
51mod text_input;
52mod window;
53
54pub use border_type::BorderType;
55pub use box_border::BoxBorder;
56pub use button::Button;
57pub use gauge::Gauge;
58pub use highlight_spacing::HighlightSpacing;
59pub use list::List;
60pub use list_direction::ListDirection;
61pub use log::Log;
62pub use meter::Meter;
63pub use modal::Modal;
64pub use panel::{Panel, PanelTitle, TitlePosition};
65pub use paragraph::Paragraph;
66pub use perf_overlay::{AnimatedPerfOverlay, PerfOverlay};
67pub use print_line::PrintLine;
68pub use progress_bar::ProgressBar;
69pub use scrollbar::Scrollbar;
70pub use sparkline::Sparkline;
71pub use stat_bar::StatBar;
72pub use table::Table;
73pub use tabs::Tabs;
74pub use text::Text;
75pub use text_input::TextInput;
76
77/// A type that draws itself into a [`Surface`], without retaining any
78/// state: the minimal shape shared by every widget-like consumer.
79///
80/// # Examples
81///
82/// ```
83/// use retroglyph_core::color::Style;
84/// use retroglyph_core::grid::{Grid, Rect};
85/// use retroglyph_ui::{Surface, Widget};
86///
87/// struct Marker(char);
88///
89/// impl Widget for Marker {
90/// fn render(&self, surface: &mut Surface<'_>) {
91/// surface.put((0, 0), self.0, Style::new());
92/// }
93/// }
94///
95/// let area = Rect::new(0, 0, 4, 1);
96/// let mut grid = Grid::new(4, 1);
97/// Marker('*').render(&mut Surface::new(&mut grid, area, 0));
98/// ```
99pub trait Widget {
100 /// Draw this widget into `surface`, filling `surface.area()`.
101 ///
102 /// Coordinates on every `Surface` drawing method are local to `surface` itself, where
103 /// `(0, 0)` is `surface.area()`'s own top-left corner, not the underlying grid's. Placement
104 /// should therefore be built from [`Surface::width`]/[`Surface::height`] (or
105 /// `surface.area().at_origin()` for a rect), never from `surface.area()`'s own
106 /// [`left`](retroglyph_core::grid::Rect::left)/[`top`](retroglyph_core::grid::Rect::top): those are
107 /// absolute grid coordinates, and passing them straight to a drawing call only lands
108 /// correctly for a surface whose area happens to start at the grid origin.
109 fn render(&self, surface: &mut Surface<'_>);
110}
111
112/// Like [`Widget`], but for widgets that read (and may update) externally
113/// owned state (a selection index, a scroll offset) that outlives a
114/// single render call. See [`crate::ListState`].
115///
116/// # Examples
117///
118/// ```
119/// use retroglyph_core::color::Style;
120/// use retroglyph_core::grid::{Grid, Rect};
121/// use retroglyph_ui::{Surface, StatefulWidget};
122///
123/// struct Counter;
124///
125/// impl StatefulWidget for Counter {
126/// type State = u32;
127///
128/// fn render(&self, surface: &mut Surface<'_>, state: &mut Self::State) {
129/// *state += 1;
130/// surface.put((0, 0), 'x', Style::new());
131/// }
132/// }
133///
134/// let area = Rect::new(0, 0, 4, 1);
135/// let mut grid = Grid::new(4, 1);
136/// let mut renders = 0;
137/// Counter.render(&mut Surface::new(&mut grid, area, 0), &mut renders);
138/// assert_eq!(renders, 1);
139/// ```
140pub trait StatefulWidget {
141 /// The externally owned state this widget reads and/or updates while
142 /// rendering.
143 type State;
144
145 /// Draw this widget into `surface`, filling `surface.area()`, using
146 /// and/or updating `state`.
147 ///
148 /// See [`Widget::render`]'s doc for why placement should come from
149 /// [`Surface::width`]/[`Surface::height`]/`surface.area().at_origin()`, not `surface.area()`'s own
150 /// [`left`](retroglyph_core::grid::Rect::left)/[`top`](retroglyph_core::grid::Rect::top).
151 fn render(&self, surface: &mut Surface<'_>, state: &mut Self::State);
152}
153
154/// A widget that can report the height it needs for a given width, before
155/// ever being rendered.
156///
157/// Lets a caller size a pane to fit content (e.g. a wrapped `Paragraph`)
158/// instead of guessing a fixed height up front.
159/// Sizing is pure content math, not drawing.
160///
161/// # Examples
162///
163/// ```
164/// use retroglyph_ui::Measure;
165///
166/// struct FixedHeight(u16);
167///
168/// impl Measure for FixedHeight {
169/// fn height_for(&self, _width: u16) -> u16 {
170/// self.0
171/// }
172/// }
173///
174/// assert_eq!(FixedHeight(3).height_for(80), 3);
175/// ```
176pub trait Measure {
177 /// The number of rows this widget would need to render at `width`
178 /// columns.
179 fn height_for(&self, width: u16) -> u16;
180}
181
182/// Like [`StatefulWidget`], but for widgets whose state evolves with wall-clock time.
183///
184/// Covers state like [`crate::ScrollState`]'s momentum/rubber-band physics or a
185/// [`Tween`](crate::animate::Tween)-driven transition, which advance on their own rather than
186/// only in response to input.
187///
188/// [`StatefulWidget`] has no way to reach the [`Frame`] an [`App`](retroglyph_core::app::App) already
189/// receives every frame, so a widget with time-based state has nowhere to advance it: not in
190/// `render` (no `Frame` parameter), and not in a second, app-defined call, because nothing
191/// enforces that call happening before `render` rather than after it: the two orders differ by
192/// one frame of animation, silently. `AnimatedWidget` closes that gap with a single call that both
193/// advances and draws, so the ordering question doesn't arise. See [`Scrollbar`]'s impl for a
194/// worked example: it ticks [`crate::ScrollState`]'s physics forward by `frame.delta`, then draws
195/// the thumb at the resulting offset, in one call.
196///
197/// A sibling of [`StatefulWidget`], not a replacement: a widget with no time-based state (a
198/// selection index that only moves on a keypress, say) has no use for `frame` and should keep
199/// implementing [`StatefulWidget`] instead. Nothing stops a widget from implementing both, the way
200/// [`Scrollbar`] implements [`Widget`] (a plain, offset-at-a-fixed-value track+thumb) alongside
201/// this trait (an animated one driven by [`crate::ScrollState`]).
202///
203/// # Examples
204///
205/// ```
206/// use core::time::Duration;
207/// use retroglyph_core::app::Frame;
208/// use retroglyph_core::grid::{Grid, Rect};
209/// use retroglyph_ui::{AnimatedWidget, Surface};
210///
211/// struct Blinker;
212///
213/// impl AnimatedWidget for Blinker {
214/// type State = Duration;
215///
216/// fn render(&self, surface: &mut Surface<'_>, state: &mut Self::State, frame: &Frame) {
217/// *state += frame.delta;
218/// let on = state.as_millis() / 500 % 2 == 0;
219/// surface.put((0, 0), if on { '*' } else { ' ' }, retroglyph_core::color::Style::new());
220/// }
221/// }
222///
223/// let area = Rect::new(0, 0, 4, 1);
224/// let mut grid = Grid::new(4, 1);
225/// let mut state = Duration::ZERO;
226/// let frame = Frame { delta: Duration::from_millis(100), frame: 0 };
227/// Blinker.render(&mut Surface::new(&mut grid, area, 0), &mut state, &frame);
228/// assert_eq!(state, Duration::from_millis(100));
229/// ```
230pub trait AnimatedWidget {
231 /// The externally owned, time-evolving state this widget reads and/or updates while
232 /// rendering, e.g. [`crate::ScrollState`].
233 type State;
234
235 /// Advances `state` by `frame.delta`, then draws this widget into `surface.area()`, both in
236 /// the same call, so there's exactly one place, not two independently ordered ones, where
237 /// time-based state moves forward.
238 fn render(&self, surface: &mut Surface<'_>, state: &mut Self::State, frame: &Frame);
239}
240
241/// A widget that renders itself styled by an already-resolved [`Response`], with the [`Sense`]
242/// it needs fixed by the widget rather than chosen at the call site.
243///
244/// A composite widget like [`Button`], [`Scrollbar`], [`List`], or [`Tabs`] needs to know what
245/// happened to it this frame (hovered, pressed, clicked, dragged) to pick its style and resolve
246/// its own hit-testing, but never calls
247/// [`Interaction::interact`](crate::Interaction::interact) itself: doing so would let it register
248/// the wrong rect, or let a call site register it with a [`Sense`] its presentation doesn't
249/// match (a click handler drawn without ever showing a hover state, say). [`sense`](Self::sense)
250/// fixes what the widget needs so a call site can't get that pairing wrong, and
251/// [`render`](Self::render) takes the resulting [`Response`] as a plain argument rather than
252/// calling `interact` itself: the widget never receives an
253/// [`Interaction`](crate::Interaction), and has no `Id` type parameter, so it can't call
254/// `interact` with the wrong rect because it has nothing to call `interact` on.
255///
256/// `type State` covers widgets with no state ([`Button`], [`Tabs`]: `()`), a scroll position
257/// ([`Scrollbar`]: [`ScrollState`](crate::ScrollState)), or a selection/scroll index ([`List`]:
258/// [`ListState`](crate::ListState)), the same [`Widget`]/[`StatefulWidget`] split applied to
259/// interactive widgets rather than a separate `InteractiveStatefulWidget` trait.
260///
261/// Has no generic method, so `dyn InteractiveWidget<Id, State = ()>` is object-safe,
262/// e.g. a `Vec<Box<dyn InteractiveWidget<Id, State = ()>>>` of heterogeneous stateless widgets.
263/// `Id` is the trait's own type parameter (not a generic method) precisely so that stays true.
264///
265/// # Examples
266///
267/// ```
268/// use retroglyph_core::color::Style;
269/// use retroglyph_core::grid::{Grid, Rect};
270/// use retroglyph_ui::{InteractiveWidget, Response, Sense, Surface};
271///
272/// struct Marker(char);
273///
274/// impl<Id> InteractiveWidget<Id> for Marker {
275/// type State = ();
276///
277/// fn sense(&self) -> Sense {
278/// Sense::click()
279/// }
280///
281/// fn render(&self, surface: &mut Surface<'_>, _state: &mut Self::State, response: Response<Id>) {
282/// let style = if response.hovered() { Style::new().bg(retroglyph_core::color::Color::RED) } else { Style::new() };
283/// surface.put((0, 0), self.0, style);
284/// }
285/// }
286/// ```
287pub trait InteractiveWidget<Id> {
288 /// State that outlives one render call, e.g. a selection index or scroll offset. `()` for
289 /// widgets that have none.
290 type State;
291
292 /// What this widget needs from the pointer and keyboard, fixed by the widget: a call site
293 /// cannot register it with a [`Sense`] its presentation doesn't match.
294 fn sense(&self) -> Sense;
295
296 /// Draw into `surface`, styled by `response`, which the caller already resolved (via
297 /// [`Interaction::interact`](crate::Interaction::interact)) for `surface.area()` and
298 /// [`sense`](Self::sense).
299 fn render(&self, surface: &mut Surface<'_>, state: &mut Self::State, response: Response<Id>);
300}
301
302#[cfg(test)]
303mod interactive_widget_tests {
304 use alloc::boxed::Box;
305 use alloc::vec;
306 use alloc::vec::Vec;
307
308 use retroglyph_core::grid::{Grid, Rect};
309
310 use super::{InteractiveWidget, Response, Sense, Surface};
311
312 struct Dot;
313
314 impl<Id> InteractiveWidget<Id> for Dot {
315 type State = ();
316
317 fn sense(&self) -> Sense {
318 Sense::click()
319 }
320
321 fn render(
322 &self,
323 surface: &mut Surface<'_>,
324 _state: &mut Self::State,
325 response: Response<Id>,
326 ) {
327 let glyph = if response.hovered() { '*' } else { '.' };
328 surface.put((0, 0), glyph, retroglyph_core::color::Style::new());
329 }
330 }
331
332 /// `InteractiveWidget<Id, State = ()>` must be object-safe: no generic method.
333 #[test]
334 fn is_object_safe() {
335 let widgets: Vec<Box<dyn InteractiveWidget<(), State = ()>>> =
336 vec![Box::new(Dot), Box::new(Dot)];
337
338 let area = Rect::new(0, 0, 1, 1);
339 let mut grid = Grid::new(1, 1);
340 for widget in &widgets {
341 widget.render(
342 &mut Surface::new(&mut grid, area, 0),
343 &mut (),
344 Response::default(),
345 );
346 }
347 assert_eq!(grid[retroglyph_core::grid::Pos::new(0, 0)].glyph(), '.');
348 }
349}