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;
24use retroglyph_core::grid::Size;
25
26use crate::Surface;
27use crate::interact::Density;
28use crate::interact::Response;
29use crate::interact::Sense;
30
31mod bar;
32mod border_type;
33mod box_border;
34mod button;
35mod gauge;
36mod highlight_spacing;
37mod list;
38mod list_direction;
39mod log;
40mod menu;
41mod menu_bar;
42mod meter;
43mod modal;
44mod panel;
45mod paragraph;
46mod perf_overlay;
47mod print_line;
48mod progress_bar;
49mod scrollbar;
50mod sparkline;
51mod stat_bar;
52mod strip;
53mod table;
54mod tabs;
55mod text;
56mod text_input;
57mod window;
58
59pub use border_type::BorderType;
60pub use box_border::BoxBorder;
61pub use button::Button;
62pub use gauge::Gauge;
63pub use highlight_spacing::HighlightSpacing;
64pub use list::List;
65pub use list_direction::ListDirection;
66pub use log::Log;
67pub use menu::{Marker, Menu, MenuRow};
68pub use menu_bar::{MenuBar, MenuBarIds};
69pub use meter::Meter;
70pub use modal::Modal;
71pub use panel::{Panel, PanelTitle, TitlePosition};
72pub use paragraph::Paragraph;
73pub use perf_overlay::{AnimatedPerfOverlay, PerfOverlay};
74pub use print_line::PrintLine;
75pub use progress_bar::ProgressBar;
76pub use scrollbar::Scrollbar;
77pub use sparkline::Sparkline;
78pub use stat_bar::StatBar;
79pub use table::Table;
80pub use tabs::Tabs;
81pub use text::Text;
82pub use text_input::TextInput;
83
84/// A type that draws itself into a [`Surface`], without retaining any
85/// state: the minimal shape shared by every widget-like consumer.
86///
87/// # Examples
88///
89/// ```
90/// use retroglyph_core::color::Style;
91/// use retroglyph_core::grid::{Grid, Rect};
92/// use retroglyph_ui::widget::Widget;
93/// use retroglyph_ui::Surface;
94///
95/// struct Marker(char);
96///
97/// impl Widget for Marker {
98/// fn render(&self, surface: &mut Surface<'_>) {
99/// surface.put((0, 0), self.0, Style::new());
100/// }
101/// }
102///
103/// let area = Rect::new(0, 0, 4, 1);
104/// let mut grid = Grid::new(4, 1);
105/// Marker('*').render(&mut Surface::new(&mut grid, area, 0));
106/// ```
107pub trait Widget {
108 /// Draw this widget into `surface`, filling `surface.area()`.
109 ///
110 /// Coordinates on every `Surface` drawing method are local to `surface` itself, where
111 /// `(0, 0)` is `surface.area()`'s own top-left corner, not the underlying grid's. Placement
112 /// should therefore be built from [`Surface::width`]/[`Surface::height`] (or
113 /// `surface.area().at_origin()` for a rect), never from `surface.area()`'s own
114 /// [`left`](retroglyph_core::grid::Rect::left)/[`top`](retroglyph_core::grid::Rect::top): those are
115 /// absolute grid coordinates, and passing them straight to a drawing call only lands
116 /// correctly for a surface whose area happens to start at the grid origin.
117 fn render(&self, surface: &mut Surface<'_>);
118}
119
120/// Like [`Widget`], but for widgets that read (and may update) externally
121/// owned state (a selection index, a scroll offset) that outlives a
122/// single render call. See [`crate::state::ListState`].
123///
124/// # Examples
125///
126/// ```
127/// use retroglyph_core::color::Style;
128/// use retroglyph_core::grid::{Grid, Rect};
129/// use retroglyph_ui::widget::StatefulWidget;
130/// use retroglyph_ui::Surface;
131///
132/// struct Counter;
133///
134/// impl StatefulWidget for Counter {
135/// type State = u32;
136///
137/// fn render(&self, surface: &mut Surface<'_>, state: &mut Self::State) {
138/// *state += 1;
139/// surface.put((0, 0), 'x', Style::new());
140/// }
141/// }
142///
143/// let area = Rect::new(0, 0, 4, 1);
144/// let mut grid = Grid::new(4, 1);
145/// let mut renders = 0;
146/// Counter.render(&mut Surface::new(&mut grid, area, 0), &mut renders);
147/// assert_eq!(renders, 1);
148/// ```
149pub trait StatefulWidget {
150 /// The externally owned state this widget reads and/or updates while
151 /// rendering.
152 type State;
153
154 /// Draw this widget into `surface`, filling `surface.area()`, using
155 /// and/or updating `state`.
156 ///
157 /// See [`Widget::render`]'s doc for why placement should come from
158 /// [`Surface::width`]/[`Surface::height`]/`surface.area().at_origin()`, not `surface.area()`'s own
159 /// [`left`](retroglyph_core::grid::Rect::left)/[`top`](retroglyph_core::grid::Rect::top).
160 fn render(&self, surface: &mut Surface<'_>, state: &mut Self::State);
161}
162
163/// A widget that can report the height it needs for a given width, before
164/// ever being rendered.
165///
166/// Lets a caller size a pane to fit content (e.g. a wrapped `Paragraph`)
167/// instead of guessing a fixed height up front.
168/// Sizing is pure content math, not drawing.
169///
170/// # Examples
171///
172/// ```
173/// use retroglyph_ui::widget::Measure;
174///
175/// struct FixedHeight(u16);
176///
177/// impl Measure for FixedHeight {
178/// fn height_for(&self, _width: u16) -> u16 {
179/// self.0
180/// }
181/// }
182///
183/// assert_eq!(FixedHeight(3).height_for(80), 3);
184/// ```
185pub trait Measure {
186 /// The number of rows this widget would need to render at `width`
187 /// columns.
188 fn height_for(&self, width: u16) -> u16;
189}
190
191/// A widget that can report the smallest size its content and [`Density`] allow, before ever
192/// being rendered.
193///
194/// This is a different question from [`Measure::height_for`]: `height_for` answers "how tall are
195/// you, given this width" (content wrapped into a fixed width); `min_size` answers "how small
196/// can you be at all" (a floor driven by both content and the [`Density`] a caller is targeting).
197/// Conflating the two into one trait/method would force every `Measure` impl to also take a
198/// `Density` it doesn't need, and every `MinSize` impl to accept a `width` it can't use to answer
199/// "how small can you get". Kept separate on purpose; a widget can implement either, both, or
200/// neither.
201///
202/// Interactive widgets ([`Button`], [`Tabs`], [`Scrollbar`], [`TextInput`]) are the intended
203/// implementors: [`Density::min_target_size`] exists specifically so a caller sizing a
204/// clickable/tappable control doesn't have to invent its own touch-target floor, and `min_size`
205/// is how a widget applies that floor to its own content.
206///
207/// # Examples
208///
209/// ```
210/// use retroglyph_core::grid::Size;
211/// use retroglyph_ui::interact::Density;
212/// use retroglyph_ui::widget::MinSize;
213///
214/// struct FixedSize(Size);
215///
216/// impl MinSize for FixedSize {
217/// fn min_size(&self, density: Density) -> Size {
218/// self.0.max(density.min_target_size())
219/// }
220/// }
221///
222/// let small = FixedSize(Size::new(1, 1));
223/// assert_eq!(small.min_size(Density::Mouse), Density::Mouse.min_target_size());
224/// ```
225pub trait MinSize {
226 /// The smallest size, in cells, this widget can be drawn at without clipping its content or
227 /// falling below `density`'s minimum interactive target size.
228 fn min_size(&self, density: Density) -> Size;
229}
230
231/// Like [`StatefulWidget`], but for widgets whose state evolves with wall-clock time.
232///
233/// Covers state like [`crate::state::ScrollState`]'s momentum/rubber-band physics or a
234/// [`Tween`](crate::animate::Tween)-driven transition, which advance on their own rather than
235/// only in response to input.
236///
237/// [`StatefulWidget`] has no way to reach the [`Frame`] an [`App`](retroglyph_core::app::App) already
238/// receives every frame, so a widget with time-based state has nowhere to advance it: not in
239/// `render` (no `Frame` parameter), and not in a second, app-defined call, because nothing
240/// enforces that call happening before `render` rather than after it: the two orders differ by
241/// one frame of animation, silently. `AnimatedWidget` closes that gap with a single call that both
242/// advances and draws, so the ordering question doesn't arise. See [`Scrollbar`]'s impl for a
243/// worked example: it ticks [`crate::state::ScrollState`]'s physics forward by `frame.delta`, then draws
244/// the thumb at the resulting offset, in one call.
245///
246/// A sibling of [`StatefulWidget`], not a replacement: a widget with no time-based state (a
247/// selection index that only moves on a keypress, say) has no use for `frame` and should keep
248/// implementing [`StatefulWidget`] instead. Nothing stops a widget from implementing both, the way
249/// [`Scrollbar`] implements [`Widget`] (a plain, offset-at-a-fixed-value track+thumb) alongside
250/// this trait (an animated one driven by [`crate::state::ScrollState`]).
251///
252/// # Examples
253///
254/// ```
255/// use core::time::Duration;
256/// use retroglyph_core::app::Frame;
257/// use retroglyph_core::grid::{Grid, Rect};
258/// use retroglyph_ui::widget::AnimatedWidget;
259/// use retroglyph_ui::Surface;
260///
261/// struct Blinker;
262///
263/// impl AnimatedWidget for Blinker {
264/// type State = Duration;
265///
266/// fn render(&self, surface: &mut Surface<'_>, state: &mut Self::State, frame: &Frame) {
267/// *state += frame.delta;
268/// let on = state.as_millis() / 500 % 2 == 0;
269/// surface.put((0, 0), if on { '*' } else { ' ' }, retroglyph_core::color::Style::new());
270/// }
271/// }
272///
273/// let area = Rect::new(0, 0, 4, 1);
274/// let mut grid = Grid::new(4, 1);
275/// let mut state = Duration::ZERO;
276/// let frame = Frame::new(Duration::from_millis(100), 0);
277/// Blinker.render(&mut Surface::new(&mut grid, area, 0), &mut state, &frame);
278/// assert_eq!(state, Duration::from_millis(100));
279/// ```
280pub trait AnimatedWidget {
281 /// The externally owned, time-evolving state this widget reads and/or updates while
282 /// rendering, e.g. [`crate::state::ScrollState`].
283 type State;
284
285 /// Advances `state` by `frame.delta`, then draws this widget into `surface.area()`, both in
286 /// the same call, so there's exactly one place, not two independently ordered ones, where
287 /// time-based state moves forward.
288 fn render(&self, surface: &mut Surface<'_>, state: &mut Self::State, frame: &Frame);
289}
290
291/// A widget that renders itself styled by an already-resolved [`Response`], with the [`Sense`]
292/// it needs fixed by the widget rather than chosen at the call site.
293///
294/// A composite widget like [`Button`], [`Scrollbar`], [`List`], or [`Tabs`] needs to know what
295/// happened to it this frame (hovered, pressed, clicked, dragged) to pick its style and resolve
296/// its own hit-testing, but never calls
297/// [`Interaction::interact`](crate::interact::Interaction::interact) itself: doing so would let it register
298/// the wrong rect, or let a call site register it with a [`Sense`] its presentation doesn't
299/// match (a click handler drawn without ever showing a hover state, say). [`sense`](Self::sense)
300/// fixes what the widget needs so a call site can't get that pairing wrong, and
301/// [`render`](Self::render) takes the resulting [`Response`] as a plain argument rather than
302/// calling `interact` itself: the widget never receives an
303/// [`Interaction`](crate::interact::Interaction), and has no `Id` type parameter, so it can't call
304/// `interact` with the wrong rect because it has nothing to call `interact` on.
305///
306/// `type State` covers widgets with no state ([`Button`], [`Tabs`]: `()`), a scroll position
307/// ([`Scrollbar`]: [`ScrollState`](crate::state::ScrollState)), or a selection/scroll index ([`List`]:
308/// [`ListState`](crate::state::ListState)), the same [`Widget`]/[`StatefulWidget`] split applied to
309/// interactive widgets rather than a separate `InteractiveStatefulWidget` trait.
310///
311/// Has no generic method, so `dyn InteractiveWidget<Id, State = ()>` is object-safe,
312/// e.g. a `Vec<Box<dyn InteractiveWidget<Id, State = ()>>>` of heterogeneous stateless widgets.
313/// `Id` is the trait's own type parameter (not a generic method) precisely so that stays true.
314///
315/// # Examples
316///
317/// ```
318/// use retroglyph_core::color::Style;
319/// use retroglyph_core::grid::{Grid, Rect};
320/// use retroglyph_ui::interact::{Response, Sense};
321/// use retroglyph_ui::widget::InteractiveWidget;
322/// use retroglyph_ui::Surface;
323///
324/// struct Marker(char);
325///
326/// impl<Id> InteractiveWidget<Id> for Marker {
327/// type State = ();
328///
329/// fn sense(&self) -> Sense {
330/// Sense::click()
331/// }
332///
333/// fn render(&self, surface: &mut Surface<'_>, _state: &mut Self::State, response: Response<Id>) {
334/// let style = if response.hovered() { Style::new().bg(retroglyph_core::color::Color::RED) } else { Style::new() };
335/// surface.put((0, 0), self.0, style);
336/// }
337/// }
338/// ```
339pub trait InteractiveWidget<Id> {
340 /// State that outlives one render call, e.g. a selection index or scroll offset. `()` for
341 /// widgets that have none.
342 type State;
343
344 /// What this widget needs from the pointer and keyboard, fixed by the widget: a call site
345 /// cannot register it with a [`Sense`] its presentation doesn't match.
346 fn sense(&self) -> Sense;
347
348 /// Draw into `surface`, styled by `response`, which the caller already resolved (via
349 /// [`Interaction::interact`](crate::interact::Interaction::interact)) for `surface.area()` and
350 /// [`sense`](Self::sense).
351 fn render(&self, surface: &mut Surface<'_>, state: &mut Self::State, response: Response<Id>);
352}
353
354#[cfg(test)]
355mod interactive_widget_tests {
356 use alloc::boxed::Box;
357 use alloc::vec;
358 use alloc::vec::Vec;
359
360 use retroglyph_core::grid::{Grid, Rect};
361
362 use super::{InteractiveWidget, Response, Sense, Surface};
363
364 struct Dot;
365
366 impl<Id> InteractiveWidget<Id> for Dot {
367 type State = ();
368
369 fn sense(&self) -> Sense {
370 Sense::click()
371 }
372
373 fn render(
374 &self,
375 surface: &mut Surface<'_>,
376 _state: &mut Self::State,
377 response: Response<Id>,
378 ) {
379 let glyph = if response.hovered() { '*' } else { '.' };
380 surface.put((0, 0), glyph, retroglyph_core::color::Style::new());
381 }
382 }
383
384 /// `InteractiveWidget<Id, State = ()>` must be object-safe: no generic method.
385 #[test]
386 fn is_object_safe() {
387 let widgets: Vec<Box<dyn InteractiveWidget<(), State = ()>>> =
388 vec![Box::new(Dot), Box::new(Dot)];
389
390 let area = Rect::new(0, 0, 1, 1);
391 let mut grid = Grid::new(1, 1);
392 for widget in &widgets {
393 widget.render(
394 &mut Surface::new(&mut grid, area, 0),
395 &mut (),
396 Response::default(),
397 );
398 }
399 assert_eq!(grid[retroglyph_core::grid::Pos::new(0, 0)].glyph(), '.');
400 }
401}