# retroglyph-ui - Complete API Documentation > Immediate-mode terminal UI toolkit for retroglyph: widgets, layout, input/focus, theming, and animation **Version:** 0.0.0 **Authors:** Matan Lurey **License:** MIT **Repository:** https://github.com/crates-lurey-io/retroglyph **Keywords:** roguelike, terminal, grid, gamedev Generated: 2026-08-05 20:21:10 UTC Created by: [cargo-llms-txt](https://github.com/masinc/cargo-llms-txt) ## Table of Contents ### src/align.rs - pub use retroglyph_core::layout::HAlign as Align ### src/block.rs - pub fn join_h - pub fn join_v ### src/style.rs - pub struct Sides - impl Sides - pub struct BoxStyle - impl BoxStyle - pub struct Boxed - impl BoxStyle - impl Widget for Boxed ### src/lib.rs - pub mod align - pub mod animate - pub mod block - pub mod camera - pub mod draw - pub mod interact - pub mod layout - pub mod perf - pub mod state - pub mod style - pub mod text - pub mod theme - pub mod ui - pub mod widget - pub use align::Align - pub use animate::{Easing, Tween, oscillate, oscillate_with_phase} - pub use block::{join_h, join_v} - pub use camera::Camera - pub use draw::{fill_rect, offset_for_pos, thumb_geometry} - pub use interact::{Consumed, DEFAULT_DRAG_THRESHOLD, Density, FocusRing, HitTester, Interaction, Pointer, Response, Sense, Shortcuts} - pub use layout::{Constraint, Flex, Side, Spacing, anchored_rect, centered_rect, split_h, split_h_flex, split_h_n, split_h_n_flex, split_h_n_spaced, split_h_spaced, split_v, split_v_flex, split_v_n, split_v_n_flex, split_v_n_spaced, split_v_spaced} - pub use perf::{DEFAULT_LAYER, DefaultPerfRenderer, FRAME_HISTORY, PerfOverlayApp, PerfOverlayMode, PerfRenderer, default_is_toggle_key} - pub use retroglyph_core::surface::{Layer, StyledSurface, Surface} - pub use state::{ListState, ScrollPhysics, ScrollState, SelectionWrap, TextInputState} - pub use style::{BoxStyle, Sides} - pub use text::{draw_clipped, truncate, truncate_owned} - pub use theme::Theme - pub use ui::Ui - pub use widget::{AnimatedPerfOverlay, AnimatedWidget, BorderType, BoxBorder, Button, Gauge, HighlightSpacing, InteractiveWidget, List, ListDirection, Log, Measure, Meter, Modal, Panel, PanelTitle, Paragraph, PerfOverlay, PrintLine, ProgressBar, Scrollbar, Sparkline, StatBar, StatefulWidget, Table, Tabs, Text, TextInput, TitlePosition, Widget} ### src/animate/mod.rs - pub use easing::Easing - pub use oscillate::{oscillate, oscillate_with_phase} - pub use tween::Tween ### src/animate/easing.rs - pub enum Easing - impl Easing ### src/animate/oscillate.rs - pub fn oscillate - pub fn oscillate_with_phase ### src/animate/tween.rs - pub struct Tween - impl Tween ### src/theme.rs - pub struct Theme - impl Theme ### src/interact/mod.rs - pub use consumed::Consumed - pub use density::Density - pub use focus::FocusRing - pub use hit::HitTester - pub use pointer::Pointer - pub use response::Response - pub use sense::Sense - pub use shortcuts::Shortcuts - pub const DEFAULT_DRAG_THRESHOLD - pub const DEFAULT_DOUBLE_CLICK_WINDOW - pub struct Interaction - impl Interaction - impl Interaction - impl Default for Interaction ### src/interact/focus.rs - pub struct FocusRing - impl FocusRing - impl FocusRing - impl Default for FocusRing ### src/interact/sense.rs - pub struct Sense - impl Sense - impl BitOr for Sense - impl BitOrAssign for Sense ### src/interact/shortcuts.rs - pub struct Shortcuts - impl Shortcuts - impl Shortcuts - impl Default for Shortcuts ### src/interact/response.rs - pub struct Response - impl Default for Response - impl Response - impl Response ### src/interact/consumed.rs - pub enum Consumed - impl Consumed - impl From for Consumed ### src/interact/density.rs - pub enum Density - impl Density ### src/interact/pointer.rs - pub struct Pointer - impl Pointer ### src/interact/hit.rs - pub struct HitTester - impl HitTester - impl HitTester - impl Default for HitTester ### src/camera/mod.rs - pub struct Camera - impl Camera ### src/camera/transform.rs - impl Camera ### src/widget/paragraph.rs - pub struct Paragraph - impl Paragraph - impl Measure for Paragraph - impl Measure for Paragraph - impl Widget for Paragraph - impl Widget for Paragraph ### src/widget/table.rs - pub struct Table - impl Table - impl StatefulWidget for Table - impl Measure for Table ### src/widget/mod.rs - pub use border_type::BorderType - pub use box_border::BoxBorder - pub use button::Button - pub use gauge::Gauge - pub use highlight_spacing::HighlightSpacing - pub use list::List - pub use list_direction::ListDirection - pub use log::Log - pub use meter::Meter - pub use modal::Modal - pub use panel::{Panel, PanelTitle, TitlePosition} - pub use paragraph::Paragraph - pub use perf_overlay::{AnimatedPerfOverlay, PerfOverlay} - pub use print_line::PrintLine - pub use progress_bar::ProgressBar - pub use scrollbar::Scrollbar - pub use sparkline::Sparkline - pub use stat_bar::StatBar - pub use table::Table - pub use tabs::Tabs - pub use text::Text - pub use text_input::TextInput - pub trait Widget - pub trait StatefulWidget - pub trait Measure - pub trait AnimatedWidget - pub trait InteractiveWidget - impl InteractiveWidget for interactive_widget_tests::Dot ### src/widget/stat_bar.rs - pub struct StatBar - impl StatBar - impl Widget for StatBar ### src/widget/meter.rs - pub struct Meter - impl Meter ### src/widget/progress_bar.rs - pub struct ProgressBar - impl ProgressBar - impl Widget for ProgressBar ### src/widget/scrollbar.rs - pub struct Scrollbar - impl Scrollbar - impl Scrollbar - impl Widget for Scrollbar - impl InteractiveWidget for Scrollbar - impl AnimatedWidget for Scrollbar ### src/widget/gauge.rs - pub struct Gauge - impl Gauge - impl Widget for Gauge ### src/widget/button.rs - pub struct Button - impl Button - impl InteractiveWidget for Button - impl Widget for Button ### src/widget/print_line.rs - pub struct PrintLine - impl PrintLine - impl Widget for PrintLine ### src/widget/text_input.rs - pub struct TextInput - impl TextInput - impl Default for TextInput - impl StatefulWidget for TextInput ### src/widget/box_border.rs - pub struct BoxBorder - impl BoxBorder - impl Widget for BoxBorder ### src/widget/tabs.rs - pub struct Tabs - impl Tabs - impl Tabs - impl Widget for Tabs - impl InteractiveWidget for Tabs ### src/widget/list.rs - pub struct List - impl List - impl List - impl StatefulWidget for List - impl Measure for List - impl InteractiveWidget for List ### src/widget/bar.rs - impl ReadoutBuf - impl fmt::Write for ReadoutBuf ### src/widget/perf_overlay.rs - pub struct PerfOverlay - impl PerfOverlay - impl Widget for PerfOverlay - pub struct AnimatedPerfOverlay - impl Default for AnimatedPerfOverlay - impl AnimatedPerfOverlay - impl AnimatedWidget for AnimatedPerfOverlay ### src/widget/border_type.rs - pub enum BorderType - impl BorderType ### src/widget/panel.rs - pub enum TitlePosition - pub struct PanelTitle - pub struct Panel - impl Panel - impl Measure for Panel - impl Widget for Panel - impl TitleCursor ### src/widget/list_direction.rs - pub enum ListDirection ### src/widget/modal.rs - pub struct Modal - impl Modal ### src/widget/log.rs - pub struct Log - impl Log - impl Measure for Log - impl Widget for Log - impl Log ### src/widget/highlight_spacing.rs - pub enum HighlightSpacing ### src/widget/text.rs - pub struct Text - impl Text - impl Widget for Text ### src/widget/sparkline.rs - pub struct Sparkline - impl Sparkline - impl Widget for Sparkline ### src/draw/mod.rs - pub use primitives::fill_rect - pub use scrollbar::{offset_for_pos, thumb_geometry} ### src/draw/primitives.rs - pub fn fill_rect ### src/draw/scrollbar.rs - impl TrackMap - pub fn thumb_geometry - pub fn offset_for_pos ### src/layout.rs - pub enum Constraint - impl Constraint - impl SmallBuf - impl core::ops::Deref for SmallBuf - impl core::ops::DerefMut for SmallBuf - impl core::ops::Index for SmallBuf - impl core::ops::IndexMut for SmallBuf - pub fn split_v - pub fn split_v_n - pub fn split_h - pub fn split_h_n - pub enum Spacing - impl Spacing - impl From for Spacing - pub fn split_h_spaced - pub fn split_v_spaced - pub fn split_h_n_spaced - pub fn split_v_n_spaced - pub enum Flex - pub fn split_v_flex - pub fn split_v_n_flex - pub fn split_h_flex - pub fn split_h_n_flex - pub fn centered_rect - pub enum Side - impl Side - pub fn anchored_rect ### src/perf/app.rs - pub fn default_is_toggle_key - pub struct PerfOverlayApp - impl PerfOverlayApp - impl PerfOverlayApp - impl PerfOverlayApp - impl App for PerfOverlayApp - impl App for tests::CountingApp - impl Output for tests::NoPushEventBackend - impl Input for tests::NoPushEventBackend - impl Cursor for tests::NoPushEventBackend - impl App for tests::SeesInput ### src/perf/mod.rs - pub use app::{PerfOverlayApp, default_is_toggle_key} - pub use mode::PerfOverlayMode - pub use renderer::{DefaultPerfRenderer, PerfRenderer} - pub const FRAME_HISTORY - pub const DEFAULT_LAYER ### src/perf/renderer.rs - pub trait PerfRenderer - impl PerfRenderer for F - impl FixedBuf - impl fmt::Write for FixedBuf - pub struct DefaultPerfRenderer - impl Default for DefaultPerfRenderer - impl DefaultPerfRenderer - impl PerfRenderer for DefaultPerfRenderer ### src/perf/mode.rs - pub enum PerfOverlayMode - impl PerfOverlayMode ### src/state/mod.rs - pub use list::{ListState, SelectionWrap} - pub use scroll::{ScrollPhysics, ScrollState} - pub use text_input::TextInputState ### src/state/scroll.rs - pub struct ScrollPhysics - impl ScrollPhysics - impl Default for ScrollPhysics - pub struct ScrollState - impl Default for ScrollState - impl ScrollState ### src/state/text_input.rs - pub struct TextInputState - impl TextInputState ### src/state/list.rs - pub enum SelectionWrap - pub struct ListState - impl ListState ### src/text.rs - pub fn truncate - pub fn truncate_owned - pub fn draw_clipped ### src/ui.rs - impl Cursor - pub struct Ui - impl Ui - impl Ui - impl InteractiveWidget for tests::Dot - impl Widget for tests::Fill - impl Widget for tests::FixedHeight - impl InteractiveWidget for tests::FixedHeight - impl Measure for tests::FixedHeight --- ## README.md ### retroglyph-ui Immediate-mode terminal UI toolkit for retroglyph: widgets, layout, input/focus, theming, and animation Part of the [retroglyph](https://github.com/crates-lurey-io/retroglyph) workspace. --- ## src/align.rs ### retroglyph_core::layout::HAlign as Align ```rust pub use retroglyph_core::layout::HAlign as Align; ``` Horizontal alignment of one line of text within the columns it's rendered into. A builder knob on the single-line text widgets ([`Text`](crate::Text), [`PrintLine`](crate::PrintLine)) and on the titles of [`Panel`](crate::Panel) and [`Modal`](crate::Modal). Text widgets default to `Left` (their long-standing behavior); panel/modal titles default to `Center` (theirs). A plain re-export of [`retroglyph_core::layout::HAlign`], not a separate type: `core::align` needs nothing from the `egc` feature, so there's no reason for `widgets` to keep its own copy of the enum or of [`offset`](retroglyph_core::layout::HAlign::offset)'s formula. Interoperates directly with [`Surface::print_aligned`](retroglyph_core::surface::Surface::print_aligned) and [`TextLayout`](retroglyph_core::layout::TextLayout), no conversion needed. ## src/block.rs ### join_h ```rust pub fn join_h(grids: &[Grid]) -> Grid ``` Concatenate `grids` left-to-right into one [`Grid`] (layer 0 only). The result's width is the sum of the input widths; its height is the tallest input. Each grid is placed top-aligned; cells below a shorter grid are left untouched (empty, per [`Grid::new`]'s default tiles). For an empty slice, returns a 1-wide, 0-tall grid: [`Grid::new`] panics on a width of zero (it divides by width internally), so a 1×0 grid is as close to "empty" as an actual `Grid` can represent. ### join_v ```rust pub fn join_v(grids: &[Grid]) -> Grid ``` Stack `grids` top-to-bottom into one [`Grid`] (layer 0 only). The result's height is the sum of the input heights; its width is the widest input. Each grid is placed left-aligned; cells past a narrower grid's width are left untouched (empty, per [`Grid::new`]'s default tiles). For an empty slice, returns a 1-wide, 0-tall grid: see [`join_h`] for why a zero-width grid isn't representable. ## src/style.rs ### Sides ```rust #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct Sides { pub top: u16, pub right: u16, pub bottom: u16, pub left: u16, } ``` CSS-style box-model sides: top/right/bottom/left, in terminal cells. ### impl Sides ```rust impl Sides { pub const ZERO: Type; pub fn all(n: u16) -> Self; pub fn symmetric(vertical: u16, horizontal: u16) -> Self; pub fn top(self, top: u16) -> Self; pub fn right(self, right: u16) -> Self; pub fn bottom(self, bottom: u16) -> Self; pub fn left(self, left: u16) -> Self; } ``` ### BoxStyle ```rust #[derive(Clone, Copy, Debug)] pub struct BoxStyle { } ``` A box-model wrapper: content, padding, an optional single-line border, and margin, rendered into a standalone [`Grid`] via [`BoxStyle::render`]. Layers from the inside out: content -> padding -> border -> margin. Margin cells are left empty (transparent, per [`Grid::new`]'s default tiles), matching CSS margin being outside the box's own background. #### Examples ``` use retroglyph_core::color::Style; use retroglyph_core::grid::Pos; use retroglyph_ui::{BoxStyle, Sides}; let grid = BoxStyle::new(Style::new()) .border(true) .padding(Sides::all(1)) .render("hi"); assert_eq!(grid[Pos::new(2, 2)].glyph(), 'h'); // 1 border + 1 padding cell in from the corner ``` ### impl BoxStyle ```rust impl BoxStyle { pub fn new(style: Style) -> Self; pub fn padding(self, padding: Sides) -> Self; pub fn margin(self, margin: Sides) -> Self; pub fn border(self, border: bool) -> Self; pub fn width(self, width: u16) -> Self; pub fn height(self, height: u16) -> Self; pub fn render(&self, text: &str) -> Grid; pub fn render_wrapped(&self, text: &str) -> Grid; } ``` ### Boxed ```rust #[derive(Clone, Copy, Debug)] pub struct Boxed<'a> { } ``` Pairs a [`BoxStyle`] with the text it should render, so the pair can implement [`Widget`] (which has no room for a text parameter). Build one via [`BoxStyle::text`]. [`Widget::render`] places the box at `area`'s top-left corner, sized to the style's own explicit-or-content-fit dimensions: it does not stretch or clip to fill `area`. It always uses [`BoxStyle::render`] (not `BoxStyle::render_wrapped`, behind the `egc` feature); for wrapped content, call `render_wrapped` directly and [`Surface::blit`](retroglyph_core::surface::Surface::blit) the result yourself. ### impl BoxStyle ```rust impl BoxStyle { pub fn text(self, text: &str) -> Boxed<'_>; } ``` ### impl Widget for Boxed ```rust impl Widget for Boxed { } ``` ## src/lib.rs ### align::Align ```rust pub use align::Align; ``` ### animate::{Easing, Tween, oscillate, oscillate_with_phase} ```rust pub use animate::{Easing, Tween, oscillate, oscillate_with_phase}; ``` ### block::{join_h, join_v} ```rust pub use block::{join_h, join_v}; ``` ### camera::Camera ```rust pub use camera::Camera; ``` ### draw::{fill_rect, offset_for_pos, thumb_geometry} ```rust pub use draw::{fill_rect, offset_for_pos, thumb_geometry}; ``` ### interact::{Consumed, DEFAULT_DRAG_THRESHOLD, Density, FocusRing, HitTester, Interaction, Pointer, Response, Sense, Shortcuts} ```rust pub use interact::{Consumed, DEFAULT_DRAG_THRESHOLD, Density, FocusRing, HitTester, Interaction, Pointer, Response, Sense, Shortcuts}; ``` ### layout::{Constraint, Flex, Side, Spacing, anchored_rect, centered_rect, split_h, split_h_flex, split_h_n, split_h_n_flex, split_h_n_spaced, split_h_spaced, split_v, split_v_flex, split_v_n, split_v_n_flex, split_v_n_spaced, split_v_spaced} ```rust pub use layout::{Constraint, Flex, Side, Spacing, anchored_rect, centered_rect, split_h, split_h_flex, split_h_n, split_h_n_flex, split_h_n_spaced, split_h_spaced, split_v, split_v_flex, split_v_n, split_v_n_flex, split_v_n_spaced, split_v_spaced}; ``` ### perf::{DEFAULT_LAYER, DefaultPerfRenderer, FRAME_HISTORY, PerfOverlayApp, PerfOverlayMode, PerfRenderer, default_is_toggle_key} ```rust pub use perf::{DEFAULT_LAYER, DefaultPerfRenderer, FRAME_HISTORY, PerfOverlayApp, PerfOverlayMode, PerfRenderer, default_is_toggle_key}; ``` ### retroglyph_core::surface::{Layer, StyledSurface, Surface} ```rust pub use retroglyph_core::surface::{Layer, StyledSurface, Surface}; ``` ### state::{ListState, ScrollPhysics, ScrollState, SelectionWrap, TextInputState} ```rust pub use state::{ListState, ScrollPhysics, ScrollState, SelectionWrap, TextInputState}; ``` ### style::{BoxStyle, Sides} ```rust pub use style::{BoxStyle, Sides}; ``` ### text::{draw_clipped, truncate, truncate_owned} ```rust pub use text::{draw_clipped, truncate, truncate_owned}; ``` ### theme::Theme ```rust pub use theme::Theme; ``` ### ui::Ui ```rust pub use ui::Ui; ``` ### widget::{AnimatedPerfOverlay, AnimatedWidget, BorderType, BoxBorder, Button, Gauge, HighlightSpacing, InteractiveWidget, List, ListDirection, Log, Measure, Meter, Modal, Panel, PanelTitle, Paragraph, PerfOverlay, PrintLine, ProgressBar, Scrollbar, Sparkline, StatBar, StatefulWidget, Table, Tabs, Text, TextInput, TitlePosition, Widget} ```rust pub use widget::{AnimatedPerfOverlay, AnimatedWidget, BorderType, BoxBorder, Button, Gauge, HighlightSpacing, InteractiveWidget, List, ListDirection, Log, Measure, Meter, Modal, Panel, PanelTitle, Paragraph, PerfOverlay, PrintLine, ProgressBar, Scrollbar, Sparkline, StatBar, StatefulWidget, Table, Tabs, Text, TextInput, TitlePosition, Widget}; ``` ## src/animate/mod.rs ### easing::Easing ```rust pub use easing::Easing; ``` ### oscillate::{oscillate, oscillate_with_phase} ```rust pub use oscillate::{oscillate, oscillate_with_phase}; ``` ### tween::Tween ```rust pub use tween::Tween; ``` ## src/animate/easing.rs ### Easing ```rust #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub enum Easing { Linear, EaseInQuad, EaseOutQuad, EaseInOutQuad, EaseInCubic, EaseOutCubic, EaseInOutCubic, EaseInSine, EaseOutSine, EaseInOutSine, EaseOutElastic, EaseOutBounce, } ``` A normalized easing curve: reshapes a linear progress fraction (`0.0..=1.0`) into an eased one, the same named curves as CSS transitions and . [`Linear`](Self::Linear) is the default. The `In` variants start slow, `Out` variants end slow, and `InOut` variants do both (matching the usual naming convention: "In" describes the *start* of the motion, not a direction). [`EaseOutElastic`](Self::EaseOutElastic) and [`EaseOutBounce`](Self::EaseOutBounce) are the only curves in their families: both are used for a settle/overshoot effect at the *end* of a motion, and the in/in-out variants (the same shape mirrored to the start) are uncommon enough in practice that this curated set omits them. ### impl Easing ```rust impl Easing { pub fn apply(self, t: f32) -> f32; } ``` ## src/animate/oscillate.rs ### oscillate ```rust pub fn oscillate(elapsed: Duration, period: Duration) -> f32 ``` A continuous sine wave sampled at `elapsed`, completing one full cycle every `period`, mapped from its natural `-1.0..=1.0` range to `0.0..=1.0`. For a finite transition that starts, runs once, and stops, see [`Tween`](super::Tween) instead. This is for motion with no start or end (a pulsing indicator, a breathing effect): keep accumulating `elapsed` every frame and re-sample. Equivalent to [`oscillate_with_phase`](crate::animate::oscillate_with_phase) with `phase` `0.0`. Two callers driving independent pulses (say, a row of status dots) that both call plain `oscillate` with the same `elapsed` and `period` will always be perfectly in sync; use [`oscillate_with_phase`](crate::animate::oscillate_with_phase) to stagger them. ``` use core::time::Duration; use retroglyph_ui::oscillate; let period = Duration::from_secs(2); assert_eq!(oscillate(Duration::ZERO, period), 0.5); // sin(0) == 0, remapped to the midpoint assert!((oscillate(Duration::from_millis(1500), period) - 0.0).abs() < 1e-6); // 3/4 through ``` ### oscillate_with_phase ```rust pub fn oscillate_with_phase(elapsed: Duration, period: Duration, phase: f32) -> f32 ``` [`oscillate`](crate::animate::oscillate), offset by `phase` full cycles. `phase` shifts where in the cycle sampling starts: `0.0` matches plain [`oscillate`](crate::animate::oscillate), `0.25` starts a quarter-cycle ahead, and so on. Only `phase`'s fractional part matters (a whole number of cycles is no offset at all), and negative values are fine. This is what lets several callers share one clock's `elapsed` and `period` while still pulsing out of sync with each other, e.g. a row of status dots each given a different `phase`. ``` use core::time::Duration; use retroglyph_ui::oscillate_with_phase; let period = Duration::from_secs(2); // A quarter cycle ahead of `elapsed = 0` is the same as `elapsed = period / 4` with no phase. let a = oscillate_with_phase(Duration::ZERO, period, 0.25); let b = oscillate_with_phase(Duration::from_millis(500), period, 0.0); assert!((a - b).abs() < 1e-6); ``` ## src/animate/tween.rs ### Tween ```rust #[derive(Debug, Clone, Copy, PartialEq)] pub struct Tween { } ``` A retargetable animation from one `f32` value to another over a fixed duration, reshaped by an [`Easing`](crate::animate::Easing) curve. See the `08_animation` example for `Tween` in action: . ``` use core::time::Duration; use retroglyph_ui::{Easing, Tween}; let mut fade = Tween::new(0.0, 1.0) .duration(Duration::from_millis(200)) .easing(Easing::EaseOutCubic); fade.update(Duration::from_millis(100)); // halfway through, by elapsed time assert!(fade.value() > 0.5); // EaseOutCubic front-loads motion, so it's already past halfway assert!(!fade.is_finished()); fade.update(Duration::from_millis(100)); // now fully elapsed assert_eq!(fade.value(), 1.0); assert!(fade.is_finished()); ``` ### impl Tween ```rust impl Tween { pub const DEFAULT_DURATION: Type; pub fn new(from: f32, to: f32) -> Self; pub fn duration(self, duration: Duration) -> Self; pub fn easing(self, easing: Easing) -> Self; pub fn update(&mut self, dt: Duration); pub fn progress(&self) -> f32; pub fn value(&self) -> f32; pub fn is_finished(&self) -> bool; pub fn target(&self) -> f32; pub fn origin(&self) -> f32; pub fn retarget(&mut self, target: f32); } ``` ## src/theme.rs ### Theme ```rust #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Theme { pub bg: Color, pub panel_bg: Color, pub border: Color, pub title_bg: Color, pub fg: Color, pub accent: Color, pub hover_bg: Color, pub press_bg: Color, pub dim: Color, } ``` A palette of named color roles, rather than a CSS-style cascade: draw code picks the role it means (`theme.accent`, `theme.border`) and the active [`Theme`] decides what color that resolves to. This crate has no opinion on *how* an app picks between [`DARK`](Self::DARK) and [`LIGHT`](Self::LIGHT) (a manual toggle key, a [`SystemTheme`](retroglyph_core::event::SystemTheme) from [`Event::ThemeChanged`](retroglyph_core::event::Event::ThemeChanged), or just always the same one): it only owns the two palettes themselves, so an app doesn't have to invent one from scratch. #### Examples ``` use retroglyph_ui::Theme; let theme = Theme::DARK; assert_eq!(theme.fg, Theme::DARK.fg); assert_ne!(theme.bg, Theme::LIGHT.bg); ``` ### impl Theme ```rust impl Theme { pub const DARK: Type; pub const LIGHT: Type; pub fn bg_for(&self, response: &Response, base: Color) -> Color; pub fn fg_for(&self, response: &Response) -> Color; pub fn style_for(&self, response: &Response, base: Color) -> Style; } ``` ## src/interact/mod.rs ### consumed::Consumed ```rust pub use consumed::Consumed; ``` ### density::Density ```rust pub use density::Density; ``` ### focus::FocusRing ```rust pub use focus::FocusRing; ``` ### hit::HitTester ```rust pub use hit::HitTester; ``` ### pointer::Pointer ```rust pub use pointer::Pointer; ``` ### response::Response ```rust pub use response::Response; ``` ### sense::Sense ```rust pub use sense::Sense; ``` ### shortcuts::Shortcuts ```rust pub use shortcuts::Shortcuts; ``` ### DEFAULT_DRAG_THRESHOLD ```rust pub const DEFAULT_DRAG_THRESHOLD: u16 ``` Default [`Interaction::with_drag_threshold`]. The pointer must move strictly farther than this many cells from its press-down position (see `past_drag_threshold`'s `>` comparison) before a [`Sense::DRAG`] widget reports [`Response::dragging`] instead of a click-in-progress, so at the default of `1` the pointer has to reach a cell at least two away from the origin. Terminal pointer positions are already cell-quantized, so there is no sub-cell jitter to absorb the way a pixel-based UI needs to: `1` exists only to let a click that wanders to an immediately adjacent cell still count as a click, while a move to the next cell out is a deliberate drag. Lower (`0`) turns any single-cell move into a drag and makes shaky clicks hard to land; higher delays drag recognition by that many extra cells. Picked by feel, not measured. ### DEFAULT_DOUBLE_CLICK_WINDOW ```rust pub const DEFAULT_DOUBLE_CLICK_WINDOW: u16 ``` Default [`Interaction::with_double_click_window`]. A second [`Response::clicked`] within this many [`begin_frame`](Interaction::begin_frame) calls of the first counts as [`Response::double_clicked`]. Measured in frames rather than a `Duration` because this module has no wall clock (see the field comment on `double_click_window`). `30` frames is about half a second at a 60 fps redraw rate, matching the double-click timing desktop environments use; because it is counted in frames, the effective window scales inversely with frame rate (a 30 fps app gets ~1 s, a 120 fps app ~250 ms), so an app that runs far from 60 fps should override it via [`Interaction::with_double_click_window`]. Too small drops deliberate but slow double-clicks; too large pairs clicks a user meant as separate. Chosen to land near half a second at 60 fps, not otherwise measured. ### Interaction ```rust #[derive(Debug, Clone)] pub struct Interaction { } ``` Ties [`Pointer`], [`HitTester`], and [`FocusRing`] together into the one piece of state a draw pass needs to make its widgets interactive. #### Frame lifecycle [`frame`](Self::frame) is the documented way to drive one frame: it wraps a closure with [`begin_frame`](Self::begin_frame) and [`end_frame`](Self::end_frame), and hands the closure a [`Ui`] pairing the surface passed in with `self`. ``` use retroglyph_core::backend::{Backend, Headless}; use retroglyph_core::grid::Rect; use retroglyph_core::terminal::Terminal; use retroglyph_ui::{Interaction, Sense}; #[derive(Clone, Copy, PartialEq, Eq)] enum WidgetId { SaveButton, } let mut term = Terminal::new(Headless::new(20, 10)); let mut interaction = Interaction::::new(); let clicked = interaction.frame(&mut term.surface(), |ui| { let area = Rect::new(0, 0, 10, 1); let response = ui.interaction().interact(area, WidgetId::SaveButton, Sense::click()); // ... draw the button, using response.hovered()/focused() to pick a style ... response.clicked() }); assert!(!clicked); // nothing clicked yet: no input was fed in ``` `begin_frame`/`handle_event`/`end_frame` stay public for callers driving the lifecycle themselves (e.g. to interleave event handling between frames rather than all at once), but `frame` is what each step below describes: ```text interaction.begin_frame(); // 1 for event in poll_events() { interaction.handle_event(&event); // 2 } draw(&mut term, &mut interaction, &state); // 3: calls interaction.interact(...) interaction.end_frame(); // 4 ``` 1. [`begin_frame`](Self::begin_frame) snapshots which id (if any) is under the pointer, and whether it pressed/released/scrolled, using *last* frame's hit registrations and pointer events: this frame's registrations aren't complete until step 3 finishes, and this frame's events haven't arrived yet (they're step 2), so every [`Response`] in a given frame is one frame stale relative to what's being drawn/fed in *this* frame: uniformly for hover, press, release, click, and scroll, all resolved from that one snapshot. At typical redraw rates this is imperceptible; it's the same kind of trade-off [`ListState::ensure_visible`](crate::ListState::ensure_visible) documents for a different reason (only the caller knows the current viewport height), applied here because only the *previous* frame knows the full hit list and the pointer's position as of the input that's about to be processed. `dragging` and [`Response::held`] are exceptions: both re-check the pointer's *live* position (via [`Pointer::pos`]/[`Pointer::is_down`]) rather than the frame-stale snapshot, because a drag-in-progress or a press-cancel needs to react the instant the pointer moves, not one frame later. Keyboard focus is the remaining exception: [`Response::focused`] and Enter/Space activation read [`FocusRing`]'s `current` live, since it's plain level state with no hit-testing involved: no staleness to trade off. 2. [`handle_event`](Self::handle_event) updates pointer position/buttons and, by default, cycles focus on Tab/Shift+Tab, then reports whether this interaction [claimed the event](Consumed), resolved against the same last-frame registrations step 1 just read: an app juggling more than one [`Interaction`] can stop routing an event the moment one of them claims it, without waiting for step 3 to run. 3. Each widget calls [`interact`](Self::interact) with its rect, a caller-chosen id, and a [`Sense`] describing what it cares about; it gets back a [`Response`] and, as a side effect, registers itself for step 1 of the *next* frame. 4. [`end_frame`](Self::end_frame) releases the active widget if step 1 saw the pointer go up. One consequence worth knowing: a full press-then-release gesture that arrives as two events in the *same* [`handle_event`](Self::handle_event) batch (both fed in during step 2 of one frame, e.g. a synthetic test firing them back to back) takes an extra frame to resolve versus a realistic press and release arriving in separate frames, because step 1's hover snapshot for that frame still reflects the pointer's position from *before* those events. Real input rarely lands this way (a physical click's down and up are milliseconds apart, i.e. several frames at typical redraw rates), so this only tends to show up in tests. #### Why `Id` is a type parameter, not a hash Immediate-mode toolkits like egui derive a widget's identity from its call-site source location (optionally salted with data) hashed down to an opaque integer, flexible, but it means two widgets can collide onto the same id at runtime with no compile-time signal, and the id carries no meaning a debugger can show you. `Interaction` instead asks the app for whatever id type it already has lying around: typically a small `Copy` enum like the hand-rolled hit-target enum an app would otherwise define anyway. Collisions become unrepresentable if the enum is exhaustive, and `{:?}`-printing an id tells you exactly which widget it is. The cost is one generic parameter; `Id: Copy + PartialEq` is all any of this module asks for. Consistently with that: everything here holds its state in a plain, explicitly-owned struct threaded through `&mut self`, the same convention [`ListState`](crate::ListState) uses, rather than the interior-mutability/global-context pattern egui's `Memory` relies on to keep its implicit ids from needing to be threaded everywhere. ### impl Interaction ```rust impl Interaction { pub fn new() -> Self; pub fn with_drag_threshold(self, cells: u16) -> Self; pub fn with_double_click_window(self, frames: u16) -> Self; pub fn pointer(&self) -> &Pointer; pub fn focus(&self) -> &FocusRing; pub fn focus_mut(&mut self) -> &mut FocusRing; } ``` ### impl Interaction ```rust impl Interaction { pub fn hovered(&self) -> Option; pub fn frame(&mut self, surface: &mut Surface<'_>, f: impl Trait) -> R; pub fn begin_frame(&mut self); pub fn handle_event(&mut self, event: &Event) -> Consumed; pub fn wants_pointer(&self) -> bool; pub fn wants_keyboard(&self) -> bool; pub fn animate(&mut self, id: Id, target: bool, duration: Duration, frame: &Frame) -> f32; pub fn push_barrier(&mut self, rect: Rect); pub fn interact(&mut self, rect: Rect, id: Id, sense: Sense) -> Response; pub fn end_frame(&mut self); } ``` ### impl Default for Interaction ```rust impl Default for Interaction { } ``` ## src/interact/focus.rs ### FocusRing ```rust #[derive(Debug, Clone)] pub struct FocusRing { } ``` Which id currently holds keyboard focus, plus Tab/Shift+Tab cycling through the ids [`register`](Self::register)ed as focusable. Like [`HitTester`](crate::HitTester), registrations are per-frame and draw-ordered, but [`advance`](Self::advance)/[`retreat`](Self::retreat) always walk *last* frame's finalized order: this frame's registrations aren't complete until the draw pass finishes. `current` itself, unlike the order, persists across frames like any other piece of app state, until focus moves or is [`clear`](Self::clear)ed. If the currently focused id isn't in the order being cycled (e.g. it scrolled out of a list, or its widget wasn't drawn this frame), the next [`advance`](Self::advance)/[`retreat`](Self::retreat) treats that the same as nothing being focused, landing on the first/last registered id rather than getting stuck. ### impl FocusRing ```rust impl FocusRing { pub fn new() -> Self; pub fn begin_frame(&mut self); pub fn clear(&mut self); } ``` ### impl FocusRing ```rust impl FocusRing { pub fn register(&mut self, id: Id); pub fn focused(&self) -> Option; pub fn is_focused(&self, id: Id) -> bool; pub fn has_order(&self) -> bool; pub fn request(&mut self, id: Id); pub fn advance(&mut self); pub fn retreat(&mut self); pub fn handle_event(&mut self, event: &Event); } ``` ### impl Default for FocusRing ```rust impl Default for FocusRing { } ``` ## src/interact/sense.rs ### Sense ```rust #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub struct Sense(); ``` Which of a [`Response`](crate::Response)'s fields [`Interaction::interact`](crate::Interaction::interact) should actually populate for a given widget call. A manual bitflag over `u8`: mirrors [`KeyModifiers`](retroglyph_core::event::KeyModifiers)'s shape rather than pulling in the `bitflags` crate for a handful of bits. Combine raw flags with `|` (`Sense::HOVER | Sense::FOCUSABLE`), or reach for one of the named constructors ([`click`](Self::click), [`drag`](Self::drag), [`hover`](Self::hover), [`scroll`](Self::scroll)) for the common cases. [`DISABLED`](Self::DISABLED) is the exception: it's a modifier over an existing sense, not a capability of its own. ### impl Sense ```rust impl Sense { pub const HOVER: Type; pub const CLICK: Type; pub const DRAG: Type; pub const FOCUSABLE: Type; pub const SCROLL: Type; pub const SECONDARY_CLICK: Type; pub const DISABLED: Type; pub const NONE: Type; pub fn click() -> Self; pub fn drag() -> Self; pub fn hover() -> Self; pub fn scroll() -> Self; pub fn secondary_click() -> Self; pub fn disabled_if(self, disabled: bool) -> Self; pub fn contains(self, other: Self) -> bool; pub fn is_disabled(self) -> bool; pub fn wants_pointer(self) -> bool; } ``` ### impl BitOr for Sense ```rust impl BitOr for Sense { } ``` ### impl BitOrAssign for Sense ```rust impl BitOrAssign for Sense { } ``` ## src/interact/shortcuts.rs ### Shortcuts ```rust #[derive(Debug, Clone)] pub struct Shortcuts { } ``` Maps key combinations to app-defined `Action`s, the same way [`HitTester`](crate::HitTester) maps a pointer position to a widget id. A lookup table an app consults, not something that owns input handling. Bindings are either global (fire regardless of focus) or scoped to a single [`FocusRing`](crate::FocusRing) id (fire only while that id holds focus); [`resolve`](Self::resolve) checks the scoped binding first, so a widget can shadow a global shortcut for the same key while it's focused. This does not replace ad hoc `match key.code { .. }` handling for widget-specific navigation (arrow keys meaning "move selection" only while a particular id is focused, say): that kind of binding usually carries extra context (list length, current offset) that doesn't fit a flat `Action` enum. `Shortcuts` is for the simple case: one key, always the same `Action`, wherever it's in scope. Bindings are a fixed table set up once (there's no per-frame `begin_frame`/registration step like [`FocusRing`](crate::FocusRing)'s, a key combination either exists or it doesn't, regardless of what happened to be drawn this frame). #### Examples ``` use retroglyph_core::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use retroglyph_ui::Shortcuts; #[derive(Clone, Copy, PartialEq, Eq)] enum Id { SearchBox, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Action { ToggleTheme, ClearSearch, } let mut shortcuts = Shortcuts::new(); shortcuts.bind_global(KeyCode::Char('t'), KeyModifiers::NONE, Action::ToggleTheme); shortcuts.bind_scoped( Id::SearchBox, KeyCode::Escape, KeyModifiers::NONE, Action::ClearSearch, ); let escape = Event::Key(KeyEvent::new(KeyCode::Escape, KeyModifiers::NONE)); assert_eq!(shortcuts.resolve(&escape, Some(Id::SearchBox)), Some(Action::ClearSearch)); assert_eq!(shortcuts.resolve(&escape, None), None); // scoped binding, nothing focused let t = Event::Key(KeyEvent::new(KeyCode::Char('t'), KeyModifiers::NONE)); assert_eq!(shortcuts.resolve(&t, None), Some(Action::ToggleTheme)); // global, focus-independent ``` ### impl Shortcuts ```rust impl Shortcuts { pub fn new() -> Self; } ``` ### impl Shortcuts ```rust impl Shortcuts { pub fn bind_global(&mut self, code: KeyCode, modifiers: KeyModifiers, action: Action); pub fn bind_scoped(&mut self, id: Id, code: KeyCode, modifiers: KeyModifiers, action: Action); pub fn resolve(&self, event: &Event, focused: Option) -> Option; } ``` ### impl Default for Shortcuts ```rust impl Default for Shortcuts { } ``` ## src/interact/response.rs ### Response ```rust #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Response { } ``` What happened to a widget this frame, as reported by [`Interaction::interact`](crate::Interaction::interact). Every field is scoped to *this* frame only (e.g. [`clicked`](Self::clicked) is `true` for exactly the one frame the release lands on), except [`focused`](Self::focused), which stays `true` across frames until focus moves elsewhere. Fields a widget didn't ask for via [`Sense`](crate::Sense) are always `false`/`0`: a widget sensed with only [`Sense::HOVER`](crate::Sense::HOVER) never reports [`clicked`](Self::clicked), for instance. ### impl Default for Response ```rust impl Default for Response { } ``` ### impl Response ```rust impl Response { pub fn id(&self) -> Id; } ``` ### impl Response ```rust impl Response { pub fn hovered(&self) -> bool; pub fn pressed(&self) -> bool; pub fn released(&self) -> bool; pub fn clicked(&self) -> bool; pub fn double_clicked(&self) -> bool; pub fn held(&self) -> bool; pub fn dragging(&self) -> bool; pub fn focused(&self) -> bool; pub fn gained_focus(&self) -> bool; pub fn lost_focus(&self) -> bool; pub fn secondary_clicked(&self) -> bool; pub fn scroll_delta(&self) -> i32; pub fn disabled(&self) -> bool; pub fn pointer_pos(&self) -> Option; pub fn press_origin(&self) -> Option; pub fn drag_delta(&self) -> Option<(i32, i32)>; pub fn rect(&self) -> Rect; } ``` ## src/interact/consumed.rs ### Consumed ```rust #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Consumed { No, Yes, } ``` Whether an [`Interaction`](crate::Interaction) claimed an event. Reported by [`handle_event`](crate::Interaction::handle_event), so a caller with more than one interaction context (a menu bar above a screen stack, a dropdown above a form) knows whether to keep routing the same event further down. A coarse substitute, like "is the menu currently open", answers a different and less useful question: it stays `true` for events the menu doesn't care about (`Resize`, `Paste`, a key it doesn't bind), and it depends on the caller keeping that flag in sync with state that can change out from under it (closing the menu without also clearing the flag). `Consumed` instead reports, per event, whether *this* call actually used it. ### impl Consumed ```rust impl Consumed { pub fn is_yes(self) -> bool; pub fn is_no(self) -> bool; } ``` ### impl From for Consumed ```rust impl From for Consumed { } ``` ## src/interact/density.rs ### Density ```rust #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Density { Touch, Mouse, } ``` How much room an interactive widget's hit target should claim. It exists so an app choosing between a phone-sized and a desktop-sized layout has one place to ask "how big should this button/row/slider be", rather than inventing its own ad hoc breakpoint constants per widget. An [`InteractiveWidget`](crate::InteractiveWidget) reads [`min_target_size`](Self::min_target_size) the same way it reads [`sense`](crate::InteractiveWidget::sense); [`for_width`](Self::for_width) is the other half, turning a terminal width into a `Density` in the first place. ### impl Density ```rust impl Density { pub const DEFAULT_BREAKPOINT_WIDTH: Type; pub fn min_target_size(self) -> Size; pub fn for_width(width: u16) -> Self; } ``` ## src/interact/pointer.rs ### Pointer ```rust #[derive(Debug, Clone, Copy, Default)] pub struct Pointer { } ``` Cell-grid pointer position and per-button state, updated by feeding it every [`Event`] you receive. Tracks all three [`MouseButton`] variants independently (unlike [`Interaction`](crate::Interaction)'s higher-level click/drag/focus resolution, which only ever resolves the primary button plus a narrower secondary-click signal; see [`Sense::SECONDARY_CLICK`](crate::Sense::SECONDARY_CLICK)). Mirrors [`KeyState`](retroglyph_core::event::KeyState)'s "feed events in, query state out" shape. [`pressed`](Self::pressed)/[`released`](Self::released)/[`scroll_delta`](Self::scroll_delta) are one-shot: populated only for the frame the underlying event arrived in, then cleared by [`end_frame`](Self::end_frame). [`pos`](Self::pos)/[`is_down`](Self::is_down) are level state that persists until the next change. ### impl Pointer ```rust impl Pointer { pub fn new() -> Self; pub fn handle_event(&mut self, event: &Event); pub fn end_frame(&mut self); pub fn pos(&self) -> Option; pub fn is_down(&self, button: MouseButton) -> bool; pub fn pressed(&self, button: MouseButton) -> bool; pub fn released(&self, button: MouseButton) -> bool; pub fn scroll_delta(&self) -> i32; } ``` ## src/interact/hit.rs ### HitTester ```rust #[derive(Debug, Clone)] pub struct HitTester { } ``` A per-frame registry of `(Rect, Id)` pairs, queried by pointer position to find the topmost widget under a point. Standalone and headless: no [`Backend`](retroglyph_core::backend::Backend) dependency, so it's usable (and unit-testable) without a [`Terminal`](retroglyph_core::terminal::Terminal) or any drawing at all, e.g. for hand-rolled hit-testing outside of [`Interaction`](crate::Interaction). Registrations are draw-ordered: a later [`push`](Self::push) means drawn (and therefore visually on top) later, so [`topmost_at`](Self::topmost_at) scans back-to-front and returns the *last* match. This mirrors the painter's algorithm every widget in this crate already draws with. A [`push_barrier`](Self::push_barrier)ed rect additionally stops that scan: a point inside a barrier's rect never resolves to anything registered before the barrier, regardless of overlap (an overlay region claims everything under it, not just what it happens to draw over), while a point outside the barrier's rect is unaffected by it and keeps scanning past it normally. ### impl HitTester ```rust impl HitTester { pub fn new() -> Self; pub fn push(&mut self, rect: Rect, id: Id); pub fn push_barrier(&mut self, rect: Rect); pub fn clear(&mut self); pub fn len(&self) -> usize; pub fn is_empty(&self) -> bool; } ``` ### impl HitTester ```rust impl HitTester { pub fn topmost_at(&self, pos: Pos) -> Option; } ``` ### impl Default for HitTester ```rust impl Default for HitTester { } ``` ## src/camera/mod.rs ### Camera ```rust #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Camera { } ``` A rectangular viewport onto a larger world, with world/screen conversions. ### impl Camera ```rust impl Camera { pub fn new(viewport: Rect, world: Size) -> Self; pub fn viewport(&self) -> Rect; pub fn world(&self) -> Size; pub fn origin(&self) -> Pos; pub fn set_viewport(&mut self, viewport: Rect); pub fn set_world(&mut self, world: Size); pub fn set_viewport_fitted(&mut self, viewport: Rect); pub fn center_on(&mut self, target: Pos); pub fn set_origin(&mut self, origin: Pos); pub fn scroll_by(&mut self, dx: i32, dy: i32); } ``` ## src/camera/transform.rs ### impl Camera ```rust impl Camera { pub fn visible_bounds(&self) -> Rect; pub fn world_to_screen(&self, world: Pos) -> Option; pub fn world_to_offset(&self, world: Pos) -> (i32, i32); pub fn surface<'a>(&self, surface: &'a mut Surface<'_>) -> Surface<'a>; pub fn screen_to_world(&self, screen: Pos) -> Option; pub fn screen_to_world_signed(&self, screen: Pos) -> (i32, i32); pub fn cells(&self) -> impl Trait; } ``` ## src/widget/paragraph.rs ### Paragraph ```rust #[derive(Clone, Copy, Debug)] pub struct Paragraph<'a> { } ``` Word-wrapped text in a single [`Style`]. `Paragraph::new(text)` wraps `text` to whatever width it is rendered at (via [`Widget::render`]), or reports the height it would need at a given width without rendering (via [`Measure::height_for`]) so a caller can size its pane to fit instead of guessing a fixed height. `style` defaults to [`Style::new()`]; set it with [`Paragraph::style`]. Without the `egc` feature, wrapping is `char`-boundary-safe and breaks on ASCII whitespace only: no grapheme-cluster segmentation, so a combining mark or wide CJK run can land on either side of a wrap point. Enabling `egc` upgrades wrapping to [`retroglyph_core::layout::TextLayout`], which is grapheme-cluster-aware; every other text-bearing widget in this crate (`List`, `Table`, `Log`) already has this same gap regardless of `egc`. Unlike [`super::BoxBorder`], [`super::Gauge`], [`super::StatBar`], [`super::Table`], and [`super::Button`], `Paragraph` has no `theme()`/ `theme_on()` pair: word-wrapped text has no single semantic [`Theme`](crate::Theme) role to map onto, so callers set `style` directly. #### Examples ``` use retroglyph_core::grid::{Grid, Rect}; use retroglyph_ui::{Measure, Paragraph, Surface, Widget}; let p = Paragraph::new("the quick brown fox jumps"); let height = p.height_for(10); // rows needed to wrap at 10 columns let area = Rect::new(0, 0, 10, height); let mut grid = Grid::new(10, height); p.render(&mut Surface::new(&mut grid, area, 0)); ``` ### impl Paragraph ```rust impl<'a> Paragraph { pub fn new(text: &'a str) -> Self; pub fn style(self, style: Style) -> Self; } ``` ### impl Measure for Paragraph ```rust impl Measure for Paragraph { } ``` ### impl Measure for Paragraph ```rust impl Measure for Paragraph { } ``` ### impl Widget for Paragraph ```rust impl Widget for Paragraph { } ``` ### impl Widget for Paragraph ```rust impl Widget for Paragraph { } ``` ## src/widget/table.rs ### Table ```rust #[derive(Clone, Copy, Debug)] pub struct Table<'a> { } ``` A fixed-column, scrollable table with a [`ListState`]-driven highlighted row. `headers` render on the first row of the area it's rendered into; `rows` follow, one per line, clipped to that area. `widths` gives each column's cell width; columns are space-separated and truncated to fit. `state.offset()` is the index of the first row drawn below the header -- rendering draws whatever window `offset` names and does not clamp or auto-scroll it, matching [`ListState`]'s existing "only the caller knows the viewport height" design. Call [`state.ensure_visible(visible_row_count)`](ListState::ensure_visible) before rendering to keep `state.selected()` on-screen. If `selected()` is `Some` and its row falls within the visible window, that row is drawn with an inverted highlight background; if it has scrolled out of view, no row is highlighted. `header_style`, `row_style`, and `selected_style` default to [`Theme::DARK`] (as if [`Table::theme`] had been called); set them with [`Table::header_style`], [`Table::row_style`], and [`Table::selected_style`]. `column_spacing` defaults to `1` (a single blank column between cells); set it with [`Table::column_spacing`]. #### Examples ``` use retroglyph_core::grid::{Grid, Rect}; use retroglyph_ui::{ListState, StatefulWidget, Surface, Table}; let headers = ["Name", "Score"]; let widths = [10u16, 6]; let rows: [&[&str]; 2] = [&["Alpha", "10"], &["Bravo", "20"]]; let mut state = ListState::new(); state.select(Some(1)); let area = Rect::new(0, 0, 20, 3); let mut grid = Grid::new(20, 3); Table::new(&headers, &widths, &rows).render(&mut Surface::new(&mut grid, area, 0), &mut state); ``` ### impl Table ```rust impl<'a> Table { pub fn new(headers: &'a [&'a str], widths: &'a [u16], rows: &'a [&'a [&'a str]]) -> Self; pub fn header_style(self, style: Style) -> Self; pub fn row_style(self, style: Style) -> Self; pub fn selected_style(self, style: Style) -> Self; pub fn column_spacing(self, spacing: u16) -> Self; pub fn theme(self, theme: Theme) -> Self; pub fn theme_on(self, theme: Theme, bg: Color) -> Self; } ``` ### impl StatefulWidget for Table ```rust impl StatefulWidget for Table { } ``` ### impl Measure for Table ```rust impl Measure for Table { } ``` ## src/widget/mod.rs ### border_type::BorderType ```rust pub use border_type::BorderType; ``` ### box_border::BoxBorder ```rust pub use box_border::BoxBorder; ``` ### button::Button ```rust pub use button::Button; ``` ### gauge::Gauge ```rust pub use gauge::Gauge; ``` ### highlight_spacing::HighlightSpacing ```rust pub use highlight_spacing::HighlightSpacing; ``` ### list::List ```rust pub use list::List; ``` ### list_direction::ListDirection ```rust pub use list_direction::ListDirection; ``` ### log::Log ```rust pub use log::Log; ``` ### meter::Meter ```rust pub use meter::Meter; ``` ### modal::Modal ```rust pub use modal::Modal; ``` ### panel::{Panel, PanelTitle, TitlePosition} ```rust pub use panel::{Panel, PanelTitle, TitlePosition}; ``` ### paragraph::Paragraph ```rust pub use paragraph::Paragraph; ``` ### perf_overlay::{AnimatedPerfOverlay, PerfOverlay} ```rust pub use perf_overlay::{AnimatedPerfOverlay, PerfOverlay}; ``` ### print_line::PrintLine ```rust pub use print_line::PrintLine; ``` ### progress_bar::ProgressBar ```rust pub use progress_bar::ProgressBar; ``` ### scrollbar::Scrollbar ```rust pub use scrollbar::Scrollbar; ``` ### sparkline::Sparkline ```rust pub use sparkline::Sparkline; ``` ### stat_bar::StatBar ```rust pub use stat_bar::StatBar; ``` ### table::Table ```rust pub use table::Table; ``` ### tabs::Tabs ```rust pub use tabs::Tabs; ``` ### text::Text ```rust pub use text::Text; ``` ### text_input::TextInput ```rust pub use text_input::TextInput; ``` ### Widget ```rust pub trait Widget { fn render(&self, surface: &mut Surface<'_>); } ``` A type that draws itself into a [`Surface`], without retaining any state: the minimal shape shared by every widget-like consumer. #### Examples ``` use retroglyph_core::color::Style; use retroglyph_core::grid::{Grid, Rect}; use retroglyph_ui::{Surface, Widget}; struct Marker(char); impl Widget for Marker { fn render(&self, surface: &mut Surface<'_>) { surface.put((0, 0), self.0, Style::new()); } } let area = Rect::new(0, 0, 4, 1); let mut grid = Grid::new(4, 1); Marker('*').render(&mut Surface::new(&mut grid, area, 0)); ``` ### StatefulWidget ```rust pub trait StatefulWidget { type State; fn render(&self, surface: &mut Surface<'_>, state: &mut Self::State); } ``` Like [`Widget`], but for widgets that read (and may update) externally owned state (a selection index, a scroll offset) that outlives a single render call. See [`crate::ListState`]. #### Examples ``` use retroglyph_core::color::Style; use retroglyph_core::grid::{Grid, Rect}; use retroglyph_ui::{Surface, StatefulWidget}; struct Counter; impl StatefulWidget for Counter { type State = u32; fn render(&self, surface: &mut Surface<'_>, state: &mut Self::State) { *state += 1; surface.put((0, 0), 'x', Style::new()); } } let area = Rect::new(0, 0, 4, 1); let mut grid = Grid::new(4, 1); let mut renders = 0; Counter.render(&mut Surface::new(&mut grid, area, 0), &mut renders); assert_eq!(renders, 1); ``` ### Measure ```rust pub trait Measure { fn height_for(&self, width: u16) -> u16; } ``` A widget that can report the height it needs for a given width, before ever being rendered. Lets a caller size a pane to fit content (e.g. a wrapped `Paragraph`) instead of guessing a fixed height up front. Sizing is pure content math, not drawing. #### Examples ``` use retroglyph_ui::Measure; struct FixedHeight(u16); impl Measure for FixedHeight { fn height_for(&self, _width: u16) -> u16 { self.0 } } assert_eq!(FixedHeight(3).height_for(80), 3); ``` ### AnimatedWidget ```rust pub trait AnimatedWidget { type State; fn render(&self, surface: &mut Surface<'_>, state: &mut Self::State, frame: &Frame); } ``` Like [`StatefulWidget`], but for widgets whose state evolves with wall-clock time. Covers state like [`crate::ScrollState`]'s momentum/rubber-band physics or a [`Tween`](crate::animate::Tween)-driven transition, which advance on their own rather than only in response to input. [`StatefulWidget`] has no way to reach the [`Frame`] an [`App`](retroglyph_core::app::App) already receives every frame, so a widget with time-based state has nowhere to advance it: not in `render` (no `Frame` parameter), and not in a second, app-defined call, because nothing enforces that call happening before `render` rather than after it: the two orders differ by one frame of animation, silently. `AnimatedWidget` closes that gap with a single call that both advances and draws, so the ordering question doesn't arise. See [`Scrollbar`]'s impl for a worked example: it ticks [`crate::ScrollState`]'s physics forward by `frame.delta`, then draws the thumb at the resulting offset, in one call. A sibling of [`StatefulWidget`], not a replacement: a widget with no time-based state (a selection index that only moves on a keypress, say) has no use for `frame` and should keep implementing [`StatefulWidget`] instead. Nothing stops a widget from implementing both, the way [`Scrollbar`] implements [`Widget`] (a plain, offset-at-a-fixed-value track+thumb) alongside this trait (an animated one driven by [`crate::ScrollState`]). #### Examples ``` use core::time::Duration; use retroglyph_core::app::Frame; use retroglyph_core::grid::{Grid, Rect}; use retroglyph_ui::{AnimatedWidget, Surface}; struct Blinker; impl AnimatedWidget for Blinker { type State = Duration; fn render(&self, surface: &mut Surface<'_>, state: &mut Self::State, frame: &Frame) { *state += frame.delta; let on = state.as_millis() / 500 % 2 == 0; surface.put((0, 0), if on { '*' } else { ' ' }, retroglyph_core::color::Style::new()); } } let area = Rect::new(0, 0, 4, 1); let mut grid = Grid::new(4, 1); let mut state = Duration::ZERO; let frame = Frame { delta: Duration::from_millis(100), frame: 0 }; Blinker.render(&mut Surface::new(&mut grid, area, 0), &mut state, &frame); assert_eq!(state, Duration::from_millis(100)); ``` ### InteractiveWidget ```rust pub trait InteractiveWidget { type State; fn sense(&self) -> Sense; fn render(&self, surface: &mut Surface<'_>, state: &mut Self::State, response: Response); } ``` A widget that renders itself styled by an already-resolved [`Response`], with the [`Sense`] it needs fixed by the widget rather than chosen at the call site. A composite widget like [`Button`], [`Scrollbar`], [`List`], or [`Tabs`] needs to know what happened to it this frame (hovered, pressed, clicked, dragged) to pick its style and resolve its own hit-testing, but never calls [`Interaction::interact`](crate::Interaction::interact) itself: doing so would let it register the wrong rect, or let a call site register it with a [`Sense`] its presentation doesn't match (a click handler drawn without ever showing a hover state, say). [`sense`](Self::sense) fixes what the widget needs so a call site can't get that pairing wrong, and [`render`](Self::render) takes the resulting [`Response`] as a plain argument rather than calling `interact` itself: the widget never receives an [`Interaction`](crate::Interaction), and has no `Id` type parameter, so it can't call `interact` with the wrong rect because it has nothing to call `interact` on. `type State` covers widgets with no state ([`Button`], [`Tabs`]: `()`), a scroll position ([`Scrollbar`]: [`ScrollState`](crate::ScrollState)), or a selection/scroll index ([`List`]: [`ListState`](crate::ListState)), the same [`Widget`]/[`StatefulWidget`] split applied to interactive widgets rather than a separate `InteractiveStatefulWidget` trait. Has no generic method, so `dyn InteractiveWidget` is object-safe, e.g. a `Vec>>` of heterogeneous stateless widgets. `Id` is the trait's own type parameter (not a generic method) precisely so that stays true. #### Examples ``` use retroglyph_core::color::Style; use retroglyph_core::grid::{Grid, Rect}; use retroglyph_ui::{InteractiveWidget, Response, Sense, Surface}; struct Marker(char); impl InteractiveWidget for Marker { type State = (); fn sense(&self) -> Sense { Sense::click() } fn render(&self, surface: &mut Surface<'_>, _state: &mut Self::State, response: Response) { let style = if response.hovered() { Style::new().bg(retroglyph_core::color::Color::RED) } else { Style::new() }; surface.put((0, 0), self.0, style); } } ``` ### impl InteractiveWidget for Dot ```rust impl InteractiveWidget for Dot { } ``` ## src/widget/stat_bar.rs ### StatBar ```rust #[derive(Clone, Copy, Debug)] pub struct StatBar<'a> { } ``` A labeled stat bar: `label`, a bar filling `current / max` of the remaining width colored by [`super::Meter`], and a trailing `"current/max"` readout. Only the first row of `area` is used. Same layout and coloring as [`super::Gauge`], but for integer `current`/`max` pairs (health, mana, stamina) with a literal readout instead of a percentage: `"45/100"` reads as a stat, not a load. `max == 0` renders as an empty, unfilled bar with a `"0/0"` readout rather than a special-cased blank output. If `current` exceeds `max` (e.g. a temporarily buffed stat), the bar fill still caps at 100%, but the readout shows the true, uncapped numbers (`"120/100"`) so the overflow stays visible in text. `label_style` defaults to [`Theme::DARK`]'s `dim` role (as if [`StatBar::theme`] had been called); set it with [`StatBar::label_style`]. The fill (and readout) color defaults to [`super::Meter`]'s green→yellow→red load ramp, which reads backwards for a health/mana/stamina stat (full renders as danger, not safe); override it with [`StatBar::fill_color`]. #### Examples ``` use retroglyph_core::grid::{Grid, Rect}; use retroglyph_ui::{StatBar, Surface, Widget}; let area = Rect::new(0, 0, 20, 1); let mut grid = Grid::new(20, 1); StatBar::new("HP", 45, 100).render(&mut Surface::new(&mut grid, area, 0)); ``` ### impl StatBar ```rust impl<'a> StatBar { pub fn new(label: &'a str, current: u32, max: u32) -> Self; pub fn label_style(self, style: Style) -> Self; pub fn fill_color(self, fill_color: Unknown) -> Self; pub fn theme(self, theme: Theme) -> Self; pub fn theme_on(self, theme: Theme, bg: Color) -> Self; } ``` ### impl Widget for StatBar ```rust impl Widget for StatBar { } ``` ## src/widget/meter.rs ### Meter ```rust #[derive(Clone, Copy, Debug, PartialEq)] pub struct Meter { } ``` A load ratio in `0.0..=1.0`, mapped to a green→yellow→red color ramp. Low load is green, mid load yellow, high load red. Values outside the range are clamped. Delegates to [`Color::lerp`] (backed by `gem`) rather than hand-rolling RGB interpolation. Not a drawing widget: there's no [`Terminal`](retroglyph_core::terminal::Terminal) involved, just a ratio-to-color mapping, but kept as its own small struct rather than a free function so [`Gauge`](super::Gauge), [`StatBar`](super::StatBar), and [`Sparkline`](super::Sparkline) share one place that owns the ramp. #### Examples ``` use retroglyph_ui::Meter; let meter = Meter::new(0.9); assert_ne!(meter.color(), Meter::new(0.1).color()); ``` ### impl Meter ```rust impl Meter { pub fn new(ratio: f32) -> Self; pub fn color(self) -> Color; } ``` ## src/widget/progress_bar.rs ### ProgressBar ```rust #[derive(Clone, Copy, Debug)] pub struct ProgressBar { } ``` A horizontal progress bar that fills `value / max` of the area it's rendered into. `filled_style`/`empty_style` default to [`Theme::DARK`] (as if [`ProgressBar::theme`] had been called); set them with [`ProgressBar::filled_style`]/[`ProgressBar::empty_style`]. `area.height()` is ignored; only the first row is drawn. #### Examples ``` use retroglyph_core::grid::{Grid, Rect}; use retroglyph_ui::{ProgressBar, Surface, Widget}; let area = Rect::new(0, 0, 10, 1); let mut grid = Grid::new(10, 1); ProgressBar::new(5, 10).render(&mut Surface::new(&mut grid, area, 0)); ``` ### impl ProgressBar ```rust impl ProgressBar { pub fn new(value: u32, max: u32) -> Self; pub fn filled_style(self, style: Style) -> Self; pub fn empty_style(self, style: Style) -> Self; pub fn theme(self, theme: Theme) -> Self; pub fn theme_on(self, theme: Theme, bg: Color) -> Self; } ``` ### impl Widget for ProgressBar ```rust impl Widget for ProgressBar { } ``` ## src/widget/scrollbar.rs ### Scrollbar ```rust #[derive(Clone, Copy, Debug)] pub struct Scrollbar { } ``` A vertical scrollbar (typically one cell wide) covering `total_len` items in a `visible_len`-row viewport. `offset` defaults to `0`; `track_style`/`thumb_style` default to [`Theme::DARK`] (as if [`Scrollbar::theme`] had been called). Set whichever a caller needs via [`Scrollbar::offset`]/[`Scrollbar::track_style`]/[`Scrollbar::thumb_style`]. `track_style` fills the whole strip, then [`crate::draw::thumb_geometry`]'s span (if any) is redrawn with `thumb_style` on top. Draws just the plain track, with no thumb, if there's nothing to scroll: see [`crate::draw::thumb_geometry`]. As a plain [`Widget`], `Scrollbar` is purely a display: `offset` is whatever the caller last set, unaffected by the pointer. [`InteractiveWidget`]'s `type State = ScrollState` makes it draggable and wheel-scrollable instead: `sense()` is [Sense::drag]() | [Sense::CLICK](crate::Sense::CLICK) | [Sense::SCROLL](crate::Sense::SCROLL), and its `render` reads the thumb's position from `state.integer_offset()`, drives a drag via [`offset_for_pos`]/[`ScrollState::update_drag`]/ [`ScrollState::end_drag`] using [`Response::pointer_pos`], and applies wheel input via [`ScrollState::apply`]. #### Examples ``` use retroglyph_core::grid::{Grid, Rect}; use retroglyph_ui::{Scrollbar, Surface, Widget}; let area = Rect::new(0, 0, 1, 10); let mut grid = Grid::new(1, 10); let scrollbar = Scrollbar::new(100, 10).offset(20); Widget::render(&scrollbar, &mut Surface::new(&mut grid, area, 0)); ``` ### impl Scrollbar ```rust impl Scrollbar { pub fn new(total_len: usize, visible_len: usize) -> Self; pub fn offset(self, offset: usize) -> Self; pub fn track_style(self, style: Style) -> Self; pub fn thumb_style(self, style: Style) -> Self; pub fn theme(self, theme: Theme) -> Self; pub fn theme_on(self, theme: Theme, bg: Color) -> Self; } ``` ### impl Scrollbar ```rust impl Scrollbar { } ``` ### impl Widget for Scrollbar ```rust impl Widget for Scrollbar { } ``` ### impl InteractiveWidget for Scrollbar ```rust impl InteractiveWidget for Scrollbar { } ``` ### impl AnimatedWidget for Scrollbar ```rust impl AnimatedWidget for Scrollbar { } ``` ## src/widget/gauge.rs ### Gauge ```rust #[derive(Clone, Copy, Debug)] pub struct Gauge<'a> { } ``` A labeled gauge: a `label`, then a bar filling `ratio` (0.0-1.0) of the remaining width, colored by [`super::Meter`], with a trailing percentage. Only the first row of `area` is used. Generalizes [`ProgressBar`](super::ProgressBar) with a load-colored fill and inline label/readout. For a `current`/`max` integer stat (health, mana) rather than a `0.0..=1.0` load ratio, see [`super::StatBar`]. `label_style` defaults to [`Theme::DARK`]'s `dim` role (as if [`Gauge::theme`] had been called); set it with [`Gauge::label_style`]. The fill (and readout) color defaults to [`super::Meter`]'s green→yellow→red load ramp; override it with [`Gauge::fill_color`] for a gauge that isn't load-shaped (e.g. a health bar, where full should read as safe, not danger). #### Examples ``` use retroglyph_core::grid::{Grid, Rect}; use retroglyph_ui::{Gauge, Surface, Widget}; let area = Rect::new(0, 0, 20, 1); let mut grid = Grid::new(20, 1); Gauge::new("CPU", 0.75).render(&mut Surface::new(&mut grid, area, 0)); ``` ### impl Gauge ```rust impl<'a> Gauge { pub fn new(label: &'a str, ratio: f32) -> Self; pub fn label_style(self, style: Style) -> Self; pub fn fill_color(self, fill_color: Unknown) -> Self; pub fn theme(self, theme: Theme) -> Self; pub fn theme_on(self, theme: Theme, bg: Color) -> Self; } ``` ### impl Widget for Gauge ```rust impl Widget for Gauge { } ``` ## src/widget/button.rs ### Button ```rust #[derive(Clone, Copy, Debug)] pub struct Button<'a> { } ``` A filled, centered `label`, styled by a [`Response`] the caller resolves via [`Interaction::interact`](crate::Interaction::interact) (or, through [`InteractiveWidget`], has resolved automatically). `Button` is pure presentation, not a new source of truth: it never calls `interact` itself and has no `Id` type parameter, unlike `Interaction`. The app still owns the `Interaction` context and decides the button's id: the same division of labor as every other widget here (state lives outside; the widget only reads it). [`InteractiveWidget::sense`] fixes the [`Sense`](crate::Sense) this button needs ([`Sense::click`](crate::Sense::click)), so a call site can't mismatch it: ``` use retroglyph_core::grid::{Grid, Rect}; use retroglyph_ui::{Button, InteractiveWidget, Interaction, Surface}; #[derive(Clone, Copy, PartialEq, Eq)] enum Id { Save, } let mut grid = Grid::new(20, 10); let mut interaction = Interaction::::new(); interaction.begin_frame(); let area = Rect::new(0, 0, 10, 1); let button = Button::new("Save"); let response = interaction.interact(area, Id::Save, InteractiveWidget::::sense(&button)); InteractiveWidget::render(&button, &mut Surface::new(&mut grid, area, 0), &mut (), response); interaction.end_frame(); ``` Precedence when more than one [`Response`] flag is set at once: [`disabled`](Response::disabled) > [`pressed`](Response::pressed) > [`hovered`](Response::hovered) > [`focused`](Response::focused) > the default `style`: matching the conventional `:disabled` > `:active` > `:hover` > `:focus` ordering, so a disabled button always reads as muted regardless of a stale hover/press, a press always reads as pressed even while still hovered, and a keyboard-focused-but-not-hovered button still shows something distinct from idle. `style`, `hovered_style`, `pressed_style`, `focused_style`, and `disabled_style` default to [`Theme::DARK`], as if [`Button::theme`] had been called; set them with [`Button::style`]/[`Button::hovered_style`]/[`Button::pressed_style`]/ [`Button::focused_style`]/[`Button::disabled_style`]. ### impl Button ```rust impl<'a> Button { pub fn new(label: &'a str) -> Self; pub fn style(self, style: Style) -> Self; pub fn hovered_style(self, style: Style) -> Self; pub fn pressed_style(self, style: Style) -> Self; pub fn focused_style(self, style: Style) -> Self; pub fn disabled_style(self, style: Style) -> Self; pub fn theme(self, theme: Theme) -> Self; pub fn theme_on(self, theme: Theme, bg: Color) -> Self; } ``` ### impl InteractiveWidget for Button ```rust impl InteractiveWidget for Button { } ``` ### impl Widget for Button ```rust impl Widget for Button { } ``` ## src/widget/print_line.rs ### PrintLine ```rust #[derive(Clone, Copy, Debug)] pub struct PrintLine<'a> { } ``` A [`Line`], drawn on the first row of the area it's rendered into and clipped to `area.width()` columns. Only the first row is used. `align` defaults to [`Align::Left`] (drawn at the left edge); set it with [`PrintLine::align`] to right-align or center the whole line's spans as a unit within `area.width()` columns. #### Examples ``` use retroglyph_core::backend::Headless; use retroglyph_core::text::Line; use retroglyph_core::terminal::Terminal; use retroglyph_ui::{PrintLine, Widget}; let mut term = Terminal::new(Headless::new(20, 1)); let line = Line::raw("hello"); term.draw(|surface| { PrintLine::new(&line).render(surface); }) .unwrap(); ``` ### impl PrintLine ```rust impl<'a> PrintLine { pub fn new(line: &'a Line) -> Self; pub fn align(self, align: Align) -> Self; } ``` ### impl Widget for PrintLine ```rust impl Widget for PrintLine { } ``` ## src/widget/text_input.rs ### TextInput ```rust #[derive(Clone, Copy, Debug)] pub struct TextInput<'a> { } ``` A single-line editable text field: the stateless drawing half of [`TextInputState`], the same split [`List`](super::List) has with [`ListState`](crate::ListState). Draws `state.value()` (masked with `mask` if set, or `placeholder` while `state.value()` is empty), scrolled horizontally by `state.scroll()` and clipped to `surface.area()`'s width, with a caret cell at the cursor's display column. Neither scrolling nor the caret's column is byte- or char-based: both go through `retroglyph_core::text::width_usize`, the same display-width measurement [`truncate`](crate::text::truncate) uses, so a value containing a double-width character (CJK, most emoji) still puts the caret in the right screen column. This widget does not call [`TextInputState::ensure_visible`]: like [`List`](super::List)/[`ListState::ensure_visible`](crate::ListState::ensure_visible), that's the caller's job, once per frame, with the actual current field width (which can change on resize). The caret always renders as an inverted-color cell (`caret_style`), not by driving a real terminal cursor via a backend's `Cursor` facet: `render` only has a [`Surface`], not a `Backend`, and a cell-drawn caret renders identically (including in headless snapshot tests) on every backend. An app that wants a blinking, backend-native caret instead can position one itself from `state.cursor()`/`state.scroll()` alongside this widget. This widget draws one field, nothing more: which field is focused (and therefore routed input), what Enter does, validation, and layout are all the app's job. IME/text composition and multi-line editing are out of scope for this crate entirely; see `docs/ROADMAP.md`. #### Examples ``` use retroglyph_core::grid::{Grid, Rect}; use retroglyph_ui::{StatefulWidget, Surface, TextInput, TextInputState}; let mut state = TextInputState::new(); state.set_value("hello"); let area = Rect::new(0, 0, 10, 1); let mut grid = Grid::new(10, 1); TextInput::new().render(&mut Surface::new(&mut grid, area, 0), &mut state); ``` ### impl TextInput ```rust impl<'a> TextInput { pub fn new() -> Self; pub fn placeholder(self, placeholder: &'a str) -> Self; pub fn mask(self, mask: char) -> Self; pub fn style(self, style: Style) -> Self; pub fn placeholder_style(self, style: Style) -> Self; pub fn caret_style(self, style: Style) -> Self; pub fn theme(self, theme: Theme) -> Self; pub fn theme_on(self, theme: Theme, bg: Color) -> Self; } ``` ### impl Default for TextInput ```rust impl Default for TextInput { } ``` ### impl StatefulWidget for TextInput ```rust impl StatefulWidget for TextInput { } ``` ## src/widget/box_border.rs ### BoxBorder ```rust #[derive(Clone, Copy, Debug, Default)] pub struct BoxBorder { } ``` A single-line box border drawn around a [`Rect`](retroglyph_core::grid::Rect). The interior of the rectangle is not touched. `area` must be at least 2×2, or [`Widget::render`] is a no-op. `style` defaults to [`Theme::DARK`] (as if [`BoxBorder::theme`] had been called); set it with [`BoxBorder::style`]. #### Examples ``` use retroglyph_core::grid::{Grid, Rect}; use retroglyph_ui::{BoxBorder, Surface, Widget}; let area = Rect::new(0, 0, 10, 4); let mut grid = Grid::new(10, 4); BoxBorder::new().render(&mut Surface::new(&mut grid, area, 0)); ``` ### impl BoxBorder ```rust impl BoxBorder { pub fn new() -> Self; pub fn style(self, style: Style) -> Self; pub fn border_type(self, border_type: BorderType) -> Self; pub fn theme(self, theme: Theme) -> Self; pub fn theme_on(self, theme: Theme, bg: Color) -> Self; } ``` ### impl Widget for BoxBorder ```rust impl Widget for BoxBorder { } ``` ## src/widget/tabs.rs ### Tabs ```rust #[derive(Clone, Copy, Debug)] pub struct Tabs<'a> { } ``` A horizontal strip of `titles` with the tab at `selected` highlighted. Unlike [`Table`](super::Table)/[`List`](super::List), `Tabs` is a plain [`Widget`], not a [`StatefulWidget`](super::StatefulWidget): there is no scroll offset for a tab strip, only a selected index, so it takes `selected: Option` directly (set via [`Tabs::select`]) rather than a [`ListState`](crate::ListState): the app is free to drive that index however it likes (a plain `usize` it owns, a [`FocusRing`](crate::FocusRing), whatever fits), the same "app- or interaction-machinery-driven, widget just reads it" division of labor as every other widget here. Titles render left to right, `column_spacing` blank columns apart (default `1`, matching [`Table::column_spacing`](super::Table::column_spacing)), with an optional single-character `divider` (default `None`, i.e. no divider) centered in that spacing: set with [`Tabs::divider`]. Drawing stops once a title would start past the area's right edge; there is no horizontal scrolling. `style` and `selected_style` default to [`Theme::DARK`] (as if [`Tabs::theme`] had been called); set them with [`Tabs::style`]/[`Tabs::selected_style`]. As an [`InteractiveWidget`], `type State = usize` (the same index a caller already threads into [`Tabs::select`] for the plain [`Widget`] path): a single id covers the whole strip, and a click selects the tab whose column range contains [`Response::pointer_pos`], resolved against the very same per-tab column layout the drawing routine uses, so the two can't diverge. #### Examples ``` use retroglyph_core::grid::{Grid, Rect}; use retroglyph_ui::{Surface, Tabs, Widget}; let titles = ["Overview", "Details", "Settings"]; let area = Rect::new(0, 0, 30, 1); let mut grid = Grid::new(30, 1); Tabs::new(&titles) .select(Some(0)) .render(&mut Surface::new(&mut grid, area, 0)); ``` ### impl Tabs ```rust impl<'a> Tabs { pub fn new(titles: &'a [&'a str]) -> Self; pub fn select(self, index: Option) -> Self; pub fn style(self, style: Style) -> Self; pub fn selected_style(self, style: Style) -> Self; pub fn column_spacing(self, spacing: u16) -> Self; pub fn divider(self, divider: Option) -> Self; pub fn theme(self, theme: Theme) -> Self; pub fn theme_on(self, theme: Theme, bg: Color) -> Self; } ``` ### impl Tabs ```rust impl Tabs { } ``` ### impl Widget for Tabs ```rust impl Widget for Tabs { } ``` ### impl InteractiveWidget for Tabs ```rust impl InteractiveWidget for Tabs { } ``` ## src/widget/list.rs ### List ```rust #[derive(Clone, Copy, Debug)] pub struct List<'a> { } ``` A scrollable, single-column list of plain-text items with a [`ListState`]-driven highlighted item: `Table`'s single-column sibling, sharing its windowing and selection story. One `item` renders per line, top-aligned in the area it's rendered into and clipped to `area.width()`. `state.offset()` is the index of the first item drawn: rendering draws whatever window `offset` names and does not clamp or auto-scroll it, matching [`Table`](super::Table)'s and [`ListState`]'s existing "only the caller knows the viewport height" design. Call [`state.ensure_visible(visible_item_count)`](ListState::ensure_visible) before rendering to keep `state.selected()` on-screen. If `selected()` is `Some` and its item falls within the visible window, that item is drawn with an inverted highlight background; if it has scrolled out of view, nothing is highlighted. `item_style` and `selected_style` default to [`Theme::DARK`], as if [`List::theme`] had been called; set a different [`Theme`] with [`List::theme`]/[`List::theme_on`] or override a single field with [`List::item_style`]/[`List::selected_style`]. As an [`InteractiveWidget`], a single id covers the whole list: a click selects the row under [`Response::pointer_pos`], resolved from this list's own row geometry (`state.offset()` plus the pointer's row within `surface.area()`) rather than from a separate id per row, and the wheel scrolls it via [`Response::scroll_delta`]/[`ListState::scroll_by`]. Selection is style-only by default (a swap to `selected_style`), the same as before [`List::highlight_symbol`] existed. Set it to prefix the selected item with a marker like `"> "`, the one selection signal that survives `set_plain_mode`'s style stripping, or that reads clearly on a 16-color backend where `selected_style`'s background isn't distinct enough. [`List::highlight_spacing`] controls whether that marker column reserves width even when nothing is selected, and [`List::direction`] draws the same windowed items from the bottom of the area upward instead of the top downward, for a chat/log-style list that grows upward. #### Examples ``` use retroglyph_core::grid::{Grid, Rect}; use retroglyph_ui::{List, ListState, StatefulWidget, Surface}; let items = ["Alpha", "Bravo", "Charlie"]; let mut state = ListState::new(); state.select(Some(1)); let area = Rect::new(0, 0, 20, 3); let mut grid = Grid::new(20, 3); List::new(&items).render(&mut Surface::new(&mut grid, area, 0), &mut state); ``` ### impl List ```rust impl<'a> List { pub fn new(items: &'a [&'a str]) -> Self; pub fn item_style(self, style: Style) -> Self; pub fn selected_style(self, style: Style) -> Self; pub fn highlight_symbol(self, symbol: &'a str) -> Self; pub fn highlight_spacing(self, spacing: HighlightSpacing) -> Self; pub fn direction(self, direction: ListDirection) -> Self; pub fn theme(self, theme: Theme) -> Self; pub fn theme_on(self, theme: Theme, bg: Color) -> Self; } ``` ### impl List ```rust impl List { } ``` ### impl StatefulWidget for List ```rust impl StatefulWidget for List { } ``` ### impl Measure for List ```rust impl Measure for List { } ``` ### impl InteractiveWidget for List ```rust impl InteractiveWidget for List { } ``` ## src/widget/bar.rs ### impl ReadoutBuf ```rust impl ReadoutBuf { } ``` ### impl fmt::Write for ReadoutBuf ```rust impl fmt::Write for ReadoutBuf { } ``` ## src/widget/perf_overlay.rs ### PerfOverlay ```rust #[derive(Clone, Copy, Debug)] pub struct PerfOverlay<'a, const N: usize> { } ``` A bordered panel showing live [`FrameStats`]. An `NNNfps MM.Mms minMM.M maxMM.M ` readout, any extra caller-supplied metric rows (`VSync` state, resolution, render backend details, ...), and a scrolling frame-time [`Sparkline`]. The richer counterpart to [`DefaultPerfRenderer`](crate::DefaultPerfRenderer): composed entirely from existing widgets ([`Panel`], [`Text`], [`Sparkline`]), it draws through [`Surface`] like every other widget in this crate, so it already works on every backend. Hand it to [`PerfOverlayApp::with_closure`](crate::PerfOverlayApp::with_closure) as a closure to use it instead of the built-in renderer: ``` #### #[cfg(feature = "std")] #### { use retroglyph_core::app::{App, Flow, Frame}; use retroglyph_core::backend::{Backend, Headless}; use retroglyph_core::grid::Size; use retroglyph_core::terminal::Terminal; use retroglyph_ui::{PerfOverlay, PerfOverlayApp, Widget}; struct MyGame; impl App for MyGame { fn update(&mut self, _term: &mut Terminal, frame: &Frame) -> Flow { if frame.frame >= 1 { Flow::Exit } else { Flow::Continue } } } let term = Terminal::new(Headless::new(60, 12)); let app = PerfOverlayApp::with_closure(MyGame, "software", |stats, backend, area, surface| { PerfOverlay::new(stats) .backend(backend) .metrics(&[("res", "1920x1080"), ("vsync", "on")]) .render(&mut surface.scope(area)); }) .size(Size::new(34, 8)); retroglyph_core::app::run_blocking(term, app).expect("run_blocking"); #### } // `run_blocking` is `std`-only; a no-op under `--no-default-features`. ``` `N` must match the [`FrameStats`] window it's built from; this crate's [`PerfOverlayApp`](crate::PerfOverlayApp) always uses 120 samples ([`FRAME_HISTORY`](crate::FRAME_HISTORY)), the default here too. #### As an [`AnimatedWidget`] This type is read-only: it borrows an already-updated [`FrameStats`] (`new(stats)`), which is exactly what [`Widget`] rendering wants but is the wrong shape for [`AnimatedWidget`], whose `state: &mut FrameStats` argument needs to *record into* the same data a draw call reads -- an immutable borrow baked into `self` and a mutable one for `state` can't coexist. Use [`AnimatedPerfOverlay`] instead for a call site that owns one [`FrameStats`] field and wants to record and draw in a single call, with no [`PerfOverlayApp`](crate::PerfOverlayApp) decorator wrapping the app. Rows beyond the panel's available interior height are silently dropped: the readout row draws first, then one row per [`metrics`](Self::metrics) entry, then the sparkline, each only if there's still room, so a caller that under-sizes the area loses the least important rows first rather than panicking or overflowing the border. ### impl PerfOverlay ```rust impl<'a, const N: usize> PerfOverlay { pub fn new(stats: &'a FrameStats) -> Self; pub fn backend(self, backend: &'a str) -> Self; pub fn title(self, title: &'a str) -> Self; pub fn metrics(self, metrics: &'a [(&'a str, &'a str)]) -> Self; pub fn text_style(self, style: Style) -> Self; pub fn sparkline_style(self, style: Style) -> Self; pub fn theme(self, theme: Theme) -> Self; } ``` ### impl Widget for PerfOverlay ```rust impl Widget for PerfOverlay { } ``` ### AnimatedPerfOverlay ```rust #[derive(Clone, Copy, Debug)] pub struct AnimatedPerfOverlay<'a, const N: usize> { } ``` [`PerfOverlay`]'s [`AnimatedWidget`] counterpart. The same readout, extra metric rows, and frame-time sparkline, but reading its [`FrameStats`] from `state` at render time instead of borrowing one up front. [`PerfOverlay::new`] takes `stats: &'a FrameStats`, which [`Widget`] rendering (a pure read of already-current data) wants but [`AnimatedWidget::render`] can't offer: its `state: &mut FrameStats` needs to record a fresh sample into the same data a draw call then reads, and an immutable borrow baked into `self` can't coexist with a mutable one passed as `state` in the same call. `AnimatedPerfOverlay` holds no stats reference of its own, only the same backend/title/metrics/style knobs [`PerfOverlay`] has, so there's nothing to alias. Replaces routing a `Duration` through [`PerfOverlayApp`](crate::PerfOverlayApp) just to reach this widget: an app that owns one [`FrameStats`] field can record into it and draw in a single call, no decorator wrapping the app at all. [`PerfOverlayApp`](crate::PerfOverlayApp) remains the right choice for an app that also wants its toggle-key handling, mode cycling, and event draining done generically, across any wrapped [`App`](retroglyph_core::app::App). This is only for the (now unblocked) case that doesn't need any of that. #### Examples ``` #### #[cfg(feature = "std")] #### { use retroglyph_core::app::{App, Flow, Frame}; use retroglyph_core::backend::{Backend, Headless}; use retroglyph_core::frames::FrameStats; use retroglyph_core::grid::Rect; use retroglyph_core::terminal::Terminal; use retroglyph_ui::{AnimatedPerfOverlay, AnimatedWidget}; struct MyGame { stats: FrameStats, } impl App for MyGame { fn update(&mut self, term: &mut Terminal, frame: &Frame) -> Flow { let area = Rect::new(0, 0, 34, 8); let mut surface = term.surface(); AnimatedPerfOverlay::new() .backend("software") .render(&mut surface.scope(area), &mut self.stats, frame); if frame.frame >= 1 { Flow::Exit } else { Flow::Continue } } } let term = Terminal::new(Headless::new(60, 12)); let app = MyGame { stats: FrameStats::new() }; retroglyph_core::app::run_blocking(term, app).expect("run_blocking"); #### } // `run_blocking` is `std`-only; a no-op under `--no-default-features`. ``` ### impl Default for AnimatedPerfOverlay ```rust impl Default for AnimatedPerfOverlay { } ``` ### impl AnimatedPerfOverlay ```rust impl<'a, const N: usize> AnimatedPerfOverlay { pub fn new() -> Self; pub fn backend(self, backend: &'a str) -> Self; pub fn title(self, title: &'a str) -> Self; pub fn metrics(self, metrics: &'a [(&'a str, &'a str)]) -> Self; pub fn text_style(self, style: Style) -> Self; pub fn sparkline_style(self, style: Style) -> Self; pub fn theme(self, theme: Theme) -> Self; } ``` ### impl AnimatedWidget for AnimatedPerfOverlay ```rust impl AnimatedWidget for AnimatedPerfOverlay { } ``` ## src/widget/border_type.rs ### BorderType ```rust #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] pub enum BorderType { Plain, Rounded, Double, Thick, } ``` The box-drawing glyph set [`BoxBorder`](super::BoxBorder), [`Panel`](super::Panel), and [`Modal`](super::Modal) draw their corners and edges with. [`BorderType::Plain`] is the default: the single-line set every one of these widgets drew before this type existed, so adding it is purely additive. The other three variants exist to make nested or stateful boxes visually distinct at a glance: an outer [`BorderType::Double`] around an inner [`BorderType::Plain`] reads instantly, where two identical single lines don't, and double/thick borders are a legible way to mark an active pane even on a 16-color terminal where [`Theme`](crate::Theme) alone can't carry that distinction. Covers four of ratatui's six `BorderType` variants (`Plain`, `Rounded`, `Double`, `Thick`); `QuadrantInside`/`QuadrantOutside` are omitted since nothing here has a use for them yet. Each variant maps directly to a [`BorderSet`](retroglyph_core::symbols::BorderSet) in [`retroglyph_core::symbols::border`], the shared glyph tables every crate draws borders from. #### Examples ``` use retroglyph_ui::{BorderType, BoxBorder}; let border = BoxBorder::new().border_type(BorderType::Rounded); ``` ### impl BorderType ```rust impl BorderType { } ``` ## src/widget/panel.rs ### TitlePosition ```rust #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] pub enum TitlePosition { Top, Bottom, } ``` Which border edge a [`PanelTitle`] is drawn into. ### PanelTitle ```rust #[derive(Clone, Copy, Debug)] pub struct PanelTitle<'a> { } ``` One title added via [`Panel::add_title`]: its text, which edge it's drawn on, and its alignment. Not constructed directly; built up through [`Panel::add_title`]'s arguments instead, the same as [`Panel::title`]/[`Panel::title_align`] build the single implicit top title. ### Panel ```rust #[derive(Clone, Debug, Default)] pub struct Panel<'a> { } ``` A bordered panel: a filled background with a box border and, optionally, one or more titles along the top and/or bottom edge. `border_style` (the box outline and titles) and `fill_style` (the interior background) both default to [`Theme::DARK`] (as if [`Panel::theme`] had been called); there is no title by default, and the title set by [`Panel::title`] (if any) defaults to [`Align::Center`]. Set whichever of these a caller needs via [`Panel::border_style`]/[`Panel::fill_style`]/[`Panel::title`]/[`Panel::title_align`]. [`Panel::title`] is sugar for the common case: one top, centered (by default) title. For anything past that (a bottom title, more than one title on an edge, or a title aligned other than via `title_align`), use [`Panel::add_title`], which is fully additive: it never disturbs `title`/`title_align`, and multiple `add_title` calls stack rather than overwrite each other. #### Examples ``` use retroglyph_core::grid::{Grid, Rect}; use retroglyph_ui::{Align, Panel, Surface, TitlePosition, Widget}; let area = Rect::new(0, 0, 20, 5); let mut grid = Grid::new(20, 5); Panel::new() .title("Status") .add_title("3 / 10", TitlePosition::Bottom, Align::Right) .render(&mut Surface::new(&mut grid, area, 0)); ``` Not [`Copy`] (unlike most other widgets here): [`Panel::add_title`] stores its titles in a `Vec`, so an unbounded number of them is exactly as cheap, and as fallible in the same ways (only an allocation failure, never a silently dropped title), as pushing onto any other `Vec`. ### impl Panel ```rust impl<'a> Panel { pub fn new() -> Self; pub fn title(self, title: &'a str) -> Self; pub fn title_align(self, align: Align) -> Self; pub fn add_title(self, title: &'a str, position: TitlePosition, align: Align) -> Self; pub fn border_style(self, style: Style) -> Self; pub fn fill_style(self, style: Style) -> Self; pub fn border_type(self, border_type: BorderType) -> Self; pub fn padding(self, padding: Sides) -> Self; pub fn inner(&self, area: Rect) -> Rect; pub fn theme(self, theme: Theme) -> Self; pub fn theme_on(self, theme: Theme, bg: Color) -> Self; } ``` ### impl Measure for Panel ```rust impl Measure for Panel { } ``` ### impl Widget for Panel ```rust impl Widget for Panel { } ``` ### impl TitleCursor ```rust impl TitleCursor { } ``` ## src/widget/list_direction.rs ### ListDirection ```rust #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] pub enum ListDirection { TopToBottom, BottomToTop, } ``` Which end of a [`List`](super::List)'s area its items render from. [`ListDirection::TopToBottom`] is the default: `items[state.offset()]` draws at the top row and later items draw downward, the list's only behavior before this type existed. [`ListDirection::BottomToTop`] draws the same window from the bottom of the area instead: `items[state.offset()]` draws at the bottom row and later items draw upward, for a chat/log-style list that grows toward the top. ## src/widget/modal.rs ### Modal ```rust #[derive(Clone, Copy, Debug)] pub struct Modal<'a> { } ``` A bordered, filled box centered in a screen [`Rect`]. Shorthand for a [`Panel`] sized `width` x `height` and centered via [`centered_rect`]. `border_style`/`fill_style` default to [`Theme::DARK`] (as if [`Modal::theme`] had been called) and there is no title by default: set whichever a caller needs via [`Modal::border_style`]/[`Modal::fill_style`]/[`Modal::title`], the same as [`Panel`]. [`Modal::render`] returns the inner content [`Rect`] (via [`Panel::inner`], the border inset plus this modal's [`Modal::padding`]) ready to hand to another widget (e.g. [`super::Log`]). Draws only the box itself; everything outside it is left untouched (no dimming or backdrop fill, that would need to read and blend existing cells, a separate feature from this thin layout convenience). Not a [`Widget`]: [`Widget::render`] can't return a value, and the inner content rect is part of this type's contract. [`Modal::render`] draws through whatever [`Surface`] it's given, on whatever layer that surface is already scoped to: it has no layer of its own to default. A modal painted over an active screen should be given a surface on [`Layer::Overlay`](retroglyph_core::surface::Layer::Overlay) (`surface.on_tier(Layer::Overlay)`), so it paints on top regardless of whether the screen or the modal renders first this frame; see [`Layer`](retroglyph_core::surface::Layer)'s docs for why that beats ordering the two draw calls. #### Examples ``` use retroglyph_core::grid::{Grid, Rect}; use retroglyph_core::surface::Layer; use retroglyph_ui::{Modal, Surface}; let screen = Rect::new(0, 0, 20, 10); let mut grid = Grid::new(20, 10); let mut surface = Surface::new(&mut grid, screen, Layer::World.as_u8()); let inner = Modal::new(10, 4) .title("Confirm") .render(screen, &mut surface.on_tier(Layer::Overlay)); // `inner` is ready to hand to another widget, e.g. a `Log` or `Text`. assert_eq!(inner.width(), 8); ``` ### impl Modal ```rust impl<'a> Modal { pub fn new(width: u16, height: u16) -> Self; pub fn title(self, title: &'a str) -> Self; pub fn title_align(self, align: Align) -> Self; pub fn border_style(self, style: Style) -> Self; pub fn fill_style(self, style: Style) -> Self; pub fn border_type(self, border_type: BorderType) -> Self; pub fn padding(self, padding: Sides) -> Self; pub fn theme(self, theme: Theme) -> Self; pub fn theme_on(self, theme: Theme, bg: Color) -> Self; pub fn render(self, screen: Rect, surface: &mut Surface<'_>) -> Rect; } ``` ## src/widget/window.rs ## src/widget/log.rs ### Log ```rust #[derive(Clone, Copy, Debug)] pub struct Log<'a> { } ``` The tail of `messages` that fits in the area it's rendered into, oldest at top, newest at the bottom, each line clipped to `area.width()` via [`PrintLine`]. `offset` (set via [`Log::offset`], default `0`) scrolls back through history: `0` shows the most recent messages, and each increment moves the window one message further into the past. Like [`Table`](super::Table)'s `state.offset()`, this does not clamp `offset`: scrolling back past the start of `messages` shows fewer (or zero) lines rather than wrapping or panicking, and it's the caller's responsibility to stop incrementing `offset` past `messages.len()` if that's undesired. This is a different windowing direction than `Table`'s (anchored to the start and counting forward), so it isn't expressed as the same shared helper. `messages` is a plain slice the caller owns and appends to (the same division of labor as [`ListState`](crate::ListState) for selection): this widget only reads it. Rows beyond the available messages are left untouched: compose with [`fill_rect`](crate::draw::fill_rect) first for a solid background if one is wanted. By default each message is truncated to one row via [`PrintLine`], which clips anything past `area.width()`. [`Log::wrap`] (requires the `egc` feature) switches to word-wrapping each message across as many rows as it needs, via [`retroglyph_core::layout::wrap`], while keeping `offset` counting messages rather than rows: the window still fills from the newest message backward, but a message is only included if all of its wrapped rows fit in what's left of the surface, since there's no supported way to render only the bottom rows of an overflowing wrapped message. A message that doesn't fully fit is left out entirely, the same "rows beyond the available messages are left untouched" behavior as running out of messages. #### Examples ``` use retroglyph_core::grid::Rect; use retroglyph_core::text::Line; use retroglyph_core::grid::Grid; use retroglyph_ui::{Log, Surface, Widget}; let messages = [Line::raw("connected"), Line::raw("joined #general")]; let area = Rect::new(0, 0, 20, 2); let mut grid = Grid::new(20, 2); Log::new(&messages).render(&mut Surface::new(&mut grid, area, 0)); ``` ### impl Log ```rust impl<'a> Log { pub fn new(messages: &'a [Line]) -> Self; pub fn offset(self, offset: usize) -> Self; pub fn wrap(self, wrap: bool) -> Self; } ``` ### impl Measure for Log ```rust impl Measure for Log { } ``` ### impl Widget for Log ```rust impl Widget for Log { } ``` ### impl Log ```rust impl Log { } ``` ## src/widget/highlight_spacing.rs ### HighlightSpacing ```rust #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] pub enum HighlightSpacing { Always, WhenSelected, Never, } ``` Whether [`List`](super::List)'s [`highlight_symbol`](super::List::highlight_symbol) marker column reserves width even when [`ListState::selected`](crate::ListState::selected) is `None`. [`HighlightSpacing::WhenSelected`] is the default: with nothing selected, the list renders no marker column at all, and one only appears (shifting every item's text right by the symbol's width) the moment something becomes selected. [`HighlightSpacing::Always`] reserves the column unconditionally, so item text never shifts as selection comes and goes. [`HighlightSpacing::Never`] never reserves it, so the symbol never renders even while something is selected. ## src/widget/text.rs ### Text ```rust #[derive(Clone, Copy, Debug)] pub struct Text<'a> { } ``` A single line of text in one [`Style`], clipped (not wrapped) to `area.width()` columns. Only the first row of `area` is used. The plain-content cousin of [`PrintLine`](super::PrintLine) (which prints a multi-span [`Line`](retroglyph_core::text::Line), for mixed styling within one line) and [`Paragraph`](super::Paragraph) (which word-wraps across multiple lines): reach for `Text` for a single already-one-line label or readout in a single style, with no wrapping and no per-span styling. `style` defaults to [`Style::new()`] and `align` to [`Align::Left`]; set them with [`Text::style`]/[`Text::align`]. Unlike [`super::BoxBorder`], [`super::Gauge`], [`super::StatBar`], [`super::Table`], and [`super::Button`], `Text` has no `theme()`/ `theme_on()` pair: a line of plain text has no single semantic [`Theme`](crate::Theme) role to map onto, so callers set `style` directly. #### Examples ``` use retroglyph_core::grid::{Grid, Rect}; use retroglyph_ui::{Align, Surface, Text, Widget}; let area = Rect::new(0, 0, 10, 1); let mut grid = Grid::new(10, 1); Text::new("OK") .align(Align::Right) .render(&mut Surface::new(&mut grid, area, 0)); ``` ### impl Text ```rust impl<'a> Text { pub fn new(content: &'a str) -> Self; pub fn style(self, style: Style) -> Self; pub fn align(self, align: Align) -> Self; } ``` ### impl Widget for Text ```rust impl Widget for Text { } ``` ## src/widget/sparkline.rs ### Sparkline ```rust #[derive(Clone, Copy, Debug)] pub struct Sparkline<'a> { } ``` A single-row sparkline of `samples`, scaled to the sample max, using the eight vertical block glyphs `▁▂▃▄▅▆▇█`. The most recent samples are right-aligned so the graph scrolls left as new data arrives. By default, bar height *and color* track each sample's fraction of the max via [`Meter`] (a green-to-red load ramp); call [`Sparkline::style`] to draw every bar in one fixed color instead, height only, the right choice once the color channel would otherwise imply something the data doesn't mean (e.g. a frame-time graph, where "tallest bar in view" isn't the same thing as "bad": [`super::PerfOverlay`] does this). Only the first row of `area` is drawn. Unlike [`super::BoxBorder`], [`super::Gauge`], [`super::StatBar`], [`super::Table`], and [`super::Button`], `Sparkline` has no `theme()`/ `theme_on()` pair: [`Meter`] already gives its bars a semantic green-to-red color, and a fixed [`Sparkline::style`] override has no single [`Theme`](crate::Theme) role to map onto either. #### Examples ``` use retroglyph_core::grid::{Grid, Rect}; use retroglyph_ui::{Surface, Sparkline, Widget}; let samples = [1.0, 3.0, 2.0, 4.0, 1.5]; let mut grid = Grid::new(10, 1); let area = Rect::new(0, 0, 10, 1); Sparkline::new(&samples).render(&mut Surface::new(&mut grid, area, 0)); ``` ### impl Sparkline ```rust impl<'a> Sparkline { pub fn new(samples: &'a [f32]) -> Self; pub fn style(self, style: Style) -> Self; } ``` ### impl Widget for Sparkline ```rust impl Widget for Sparkline { } ``` ## src/draw/mod.rs ### primitives::fill_rect ```rust pub use primitives::fill_rect; ``` ### scrollbar::{offset_for_pos, thumb_geometry} ```rust pub use scrollbar::{offset_for_pos, thumb_geometry}; ``` ## src/draw/primitives.rs ### fill_rect ```rust pub fn fill_rect(surface: &mut Surface<'_>, rect: Rect, ch: char, style: Style) ``` Fill `rect` with `ch` in the given `style`. The entire rectangle including corners is overwritten. Kept as a plain function rather than a widget: there's no configuration to build up beyond the two arguments already here, and it's a building block other widgets (`Panel`, `Table`, `Scrollbar`) call directly. ## src/draw/scrollbar.rs ### impl TrackMap ```rust impl TrackMap { } ``` ### thumb_geometry ```rust pub fn thumb_geometry(area: Rect, total_len: usize, visible_len: usize, offset: usize) -> Option<(u16, u16)> ``` The thumb's row span within `area` (`(start, len)`, both relative to `area.top()`) for a vertical scrollbar covering `total_len` items in a `visible_len`-row viewport currently starting at `offset`. `None` if there's nothing to scroll (`area` has no rows, `visible_len` is zero, or `total_len <= visible_len`, the whole track already fits in the viewport). [`widget::Scrollbar`](crate::widget::Scrollbar) falls back to drawing a plain, thumb-less track in that case. The thumb is sized proportionally to `visible_len / total_len` (clamped to at least one row so it's never invisible) and positioned proportionally to `offset` within the remaining scrollable range. ### offset_for_pos ```rust pub fn offset_for_pos(area: Rect, total_len: usize, visible_len: usize, pos: Pos) -> Option ``` The offset a vertical scrollbar should jump to for a click/drag at `pos`. Covers `total_len` items in a `visible_len`-row `area`; useful for click-to-jump or drag-to-scroll interactions built on top of [`thumb_geometry`], whose exact inverse it is (both share the same private row/offset conversion internally). `None` if `pos` falls outside `area`, or (mirroring [`thumb_geometry`]) there's nothing to scroll. ## src/layout.rs ### Constraint ```rust #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Constraint { Fixed(u16), Percent(u16), Ratio(u16, u16), Fill(u16), Min(u16), Max(u16), } ``` How a single pane claims space along the split axis. ### impl Constraint ```rust impl Constraint { } ``` ### impl SmallBuf ```rust impl SmallBuf { } ``` ### impl core::ops::Deref for SmallBuf ```rust impl core::ops::Deref for SmallBuf { } ``` ### impl core::ops::DerefMut for SmallBuf ```rust impl core::ops::DerefMut for SmallBuf { } ``` ### impl core::ops::Index for SmallBuf ```rust impl core::ops::Index for SmallBuf { } ``` ### impl core::ops::IndexMut for SmallBuf ```rust impl core::ops::IndexMut for SmallBuf { } ``` ### split_v ```rust pub fn split_v(area: Rect, constraints: &[Constraint]) -> Vec ``` Split `area` into stacked rows top-to-bottom. Returns one [`Rect`] per constraint; empty panes (zero height) are still returned so indices line up with `constraints`. Never panics: a degenerate `area` (zero height, zero width, or both) resolves every constraint to a zero-height pane via [`saturating_sub`](u16::saturating_sub) arithmetic rather than under/overflowing, and an empty `constraints` slice simply returns an empty `Vec`. #### Examples ``` use retroglyph_core::grid::Rect; use retroglyph_ui::{Constraint, split_v}; let area = Rect::new(0, 0, 20, 10); let panes = split_v(area, &[Constraint::Fixed(1), Constraint::Fill(1), Constraint::Fixed(1)]); assert_eq!(panes.iter().map(Rect::height).collect::>(), vec![1, 8, 1]); ``` ### split_v_n ```rust pub fn split_v_n(area: Rect, constraints: [Constraint; N]) -> [Rect; N] ``` Split `area` into stacked rows top-to-bottom, like [`split_v`], but sized to a compile-time pane count `N`: takes `[Constraint; N]` and returns `[Rect; N]` instead of allocating a `Vec`. The array length ties the constraint count to the return type, so a caller that destructures the result (`let [header, body, footer] = split_v_n(area, [..]);`) gets a compile error if it adds or removes a constraint without updating the destructuring pattern, rather than a silently out-of-sync index into a `Vec`. Never allocates, for any `N`. Never panics, for the same reason as [`split_v`]. #### Examples ``` use retroglyph_core::grid::Rect; use retroglyph_ui::{Constraint, split_v_n}; let area = Rect::new(0, 0, 20, 10); let [header, body, footer] = split_v_n(area, [Constraint::Fixed(1), Constraint::Fill(1), Constraint::Fixed(1)]); assert_eq!((header.height(), body.height(), footer.height()), (1, 8, 1)); ``` ### split_h ```rust pub fn split_h(area: Rect, constraints: &[Constraint]) -> Vec ``` Split `area` into columns left-to-right. Returns one [`Rect`] per constraint; empty panes (zero width) are still returned so indices line up with `constraints`. Never panics, for the same reason as [`split_v`]: a degenerate `area` resolves every constraint to a zero-width pane instead of under/overflowing, and an empty `constraints` slice returns an empty `Vec`. #### Examples ``` use retroglyph_core::grid::Rect; use retroglyph_ui::{Constraint, split_h}; let area = Rect::new(0, 0, 100, 5); let panes = split_h(area, &[Constraint::Percent(30), Constraint::Fill(1)]); assert_eq!(panes.iter().map(Rect::width).collect::>(), vec![30, 70]); ``` ### split_h_n ```rust pub fn split_h_n(area: Rect, constraints: [Constraint; N]) -> [Rect; N] ``` Split `area` into columns left-to-right, like [`split_h`], but sized to a compile-time pane count `N`: takes `[Constraint; N]` and returns `[Rect; N]` instead of allocating a `Vec`. See [`split_v_n`] for why the array-sized signature is worth it over indexing a `Vec`. Never allocates, for any `N`; never panics, for the same reason as [`split_h`]. #### Examples ``` use retroglyph_core::grid::Rect; use retroglyph_ui::{Constraint, split_h_n}; let area = Rect::new(0, 0, 100, 5); let [left, right] = split_h_n(area, [Constraint::Percent(30), Constraint::Fill(1)]); assert_eq!((left.width(), right.width()), (30, 70)); ``` ### Spacing ```rust #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Spacing { Space(u16), Overlap(u16), } ``` How adjacent panes relate along the split axis in a `_spaced` split: a fixed gap between them, or a shared, overlapping edge. A plain `u16` (the historic `spacing` parameter) converts to [`Space`](Self::Space) via [`From`], so existing callers of [`split_h_spaced`]/[`split_v_spaced`]/[`split_h_n_spaced`]/ [`split_v_n_spaced`] keep compiling unchanged. ### impl Spacing ```rust impl Spacing { } ``` ### impl From for Spacing ```rust impl From for Spacing { } ``` ### split_h_spaced ```rust pub fn split_h_spaced(area: Rect, constraints: &[Constraint], spacing: impl Trait) -> (Vec, Vec) ``` Split `area` into columns left-to-right, like [`split_h`], but with a `spacing`-cell gap or overlap between every adjacent pair of panes; see [`Spacing`]. `spacing`'s cell count is reserved from (for [`Spacing::Space`]) or added back to (for [`Spacing::Overlap`]) `area` before `constraints` are resolved, so [`Fill`](Constraint::Fill)/[`Percent`](Constraint::Percent) panes share only what's left after every gap is reserved, or the full extra room an overlap frees up. No-op (falls back to [`split_h`], with an empty gap `Vec`) with fewer than two panes or zero spacing/overlap. Returns `(panes, gaps)`: `gaps` has one entry between every adjacent pair of panes (so `constraints.len() - 1` gaps, none leading or trailing), in the same left-to-right order as `panes`. For [`Spacing::Space`] a gap rect is the empty cell(s) between the two panes it separates; for [`Spacing::Overlap`] it is the shared cell(s) both adjacent panes draw over (whichever pane is drawn last wins there), which is exactly the region a caller would want to draw a shared border into. #### Examples ``` use retroglyph_core::grid::Rect; use retroglyph_ui::{Constraint, Spacing, split_h_spaced}; let area = Rect::new(0, 0, 59, 6); let (panes, gaps) = split_h_spaced(area, &[Constraint::Fill(1); 3], 1); assert_eq!(panes.iter().map(Rect::width).collect::>(), vec![19, 19, 19]); assert_eq!(panes[1].left(), panes[0].right() + 1); // one gap cell between panes assert_eq!(gaps.len(), 2); // one gap between each adjacent pair of panes assert_eq!(gaps[0], Rect::new(19, 0, 1, 6)); let (panes, gaps) = split_h_spaced(area, &[Constraint::Fill(1); 2], Spacing::Overlap(1)); assert_eq!(panes[1].left(), panes[0].right() - 1); // panes share one border column assert_eq!(gaps[0], Rect::new(panes[0].right() - 1, 0, 1, 6)); // the shared column ``` ### split_v_spaced ```rust pub fn split_v_spaced(area: Rect, constraints: &[Constraint], spacing: impl Trait) -> (Vec, Vec) ``` Split `area` into stacked rows top-to-bottom, like [`split_v`], but with a `spacing`-cell gap or overlap between every adjacent pair of panes; see [`Spacing`]. See [`split_h_spaced`] for the full behavior, including the shape and meaning of the returned `(panes, gaps)`; this is the same operation along the vertical axis. ### split_h_n_spaced ```rust pub fn split_h_n_spaced(area: Rect, constraints: [Constraint; N], spacing: impl Trait) -> ([Rect; N], Vec) ``` Split `area` into columns left-to-right, like [`split_h_n`], but with a `spacing`-cell gap or overlap between every adjacent pair of panes, like [`split_h_spaced`]; see [`Spacing`]. Reserves or hands back `spacing.cells() * (N - 1)` cells up front (equivalent to [`split_h_spaced`]) and solves the remaining panes against what's left, so [`Fill`](Constraint::Fill)/[`Percent`](Constraint::Percent) panes share only the space after every gap is reserved, same as [`split_h_spaced`]. No-op (falls back to [`split_h_n`], with an empty gap `Vec`) with fewer than two panes or zero spacing/overlap. Returns `(panes, gaps)`, same meaning as [`split_h_spaced`]'s return value: `panes` is the array of `N` content panes (never allocates, unlike the `Vec`-returning [`split_h_spaced`]), and `gaps` is a `Vec` of the `N - 1` gap rects between them. `gaps` is a `Vec` rather than a second const-generic array because its length (`N - 1`) is not expressible as a fixed-size array tied to `N` on stable Rust. #### Examples ``` use retroglyph_core::grid::Rect; use retroglyph_ui::{Constraint, split_h_n_spaced}; let area = Rect::new(0, 0, 59, 6); let ([a, b, c], gaps) = split_h_n_spaced(area, [Constraint::Fill(1); 3], 1); assert_eq!((a.width(), b.width(), c.width()), (19, 19, 19)); assert_eq!(b.left(), a.right() + 1); assert_eq!(gaps.len(), 2); ``` ### split_v_n_spaced ```rust pub fn split_v_n_spaced(area: Rect, constraints: [Constraint; N], spacing: impl Trait) -> ([Rect; N], Vec) ``` Split `area` into stacked rows top-to-bottom, like [`split_v_n`], but with a `spacing`-cell gap or overlap between every adjacent pair of panes, like [`split_v_spaced`]; see [`Spacing`]. See [`split_h_n_spaced`] for the full behavior, including why `gaps` is a `Vec` rather than a second const-generic array; this is the same operation along the vertical axis. ### Flex ```rust #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum Flex { Start, End, Center, SpaceBetween, SpaceAround, } ``` How leftover space is placed along the split axis, once [`Constraint`]s are resolved. Only matters when the resolved pane sizes sum to less than `area`'s length; passed to [`split_v_flex`]/[`split_h_flex`]. [`split_v`]/[`split_h`] always behave like [`Start`](Self::Start): any leftover space trails after the last pane, unclaimed. This matches their existing documented behavior, so adding `Flex` does not change them. ### split_v_flex ```rust pub fn split_v_flex(area: Rect, constraints: &[Constraint], flex: Flex) -> Vec ``` Split `area` into stacked rows top-to-bottom, like [`split_v`], but with explicit control over how leftover space is placed via [`Flex`]. Never panics, for the same reason as [`split_v`]: every offset is computed with [`saturating_add`](u16::saturating_add)/[`saturating_sub`](u16::saturating_sub). ### split_v_n_flex ```rust pub fn split_v_n_flex(area: Rect, constraints: [Constraint; N], flex: Flex) -> [Rect; N] ``` Split `area` into stacked rows top-to-bottom, like [`split_v_n`], but with explicit control over how leftover space is placed via [`Flex`], like [`split_v_flex`]. Never allocates, for any `N`; never panics, for the same reason as [`split_v_flex`]. ### split_h_flex ```rust pub fn split_h_flex(area: Rect, constraints: &[Constraint], flex: Flex) -> Vec ``` Split `area` into columns left-to-right, like [`split_h`], but with explicit control over how leftover space is placed via [`Flex`]. Never panics, for the same reason as [`split_h`]: every offset is computed with [`saturating_add`](u16::saturating_add)/[`saturating_sub`](u16::saturating_sub). ### split_h_n_flex ```rust pub fn split_h_n_flex(area: Rect, constraints: [Constraint; N], flex: Flex) -> [Rect; N] ``` Split `area` into columns left-to-right, like [`split_h_n`], but with explicit control over how leftover space is placed via [`Flex`], like [`split_h_flex`]. Never allocates, for any `N`; never panics, for the same reason as [`split_h_flex`]. ### centered_rect ```rust pub fn centered_rect(screen: Rect, width: u16, height: u16) -> Rect ``` Compute a `width`×`height` [`Rect`] centered within `screen`. `width`/`height` are clamped down to `screen`'s own dimensions if larger, so the result never extends past `screen`'s edges: a modal, dialog, or tooltip box built from this is always fully on-screen, even on a terminal too small to fit the box's requested size. Pure layout math: no drawing, no `Terminal`. Pairs with `panel`/`modal` in `retroglyph-ui` (the `draw` module) for a centered, bordered box. Never panics: the clamp and centering offsets are computed with saturating arithmetic, so a zero-size `screen`, `width`, or `height` resolves to a zero-size or edge-pinned rect instead of under/overflowing. ### Side ```rust #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Side { Above, Below, Left, Right, } ``` Which side of an anchor rect a panel prefers to open on, for [`anchored_rect`]. ### impl Side ```rust impl Side { } ``` ### anchored_rect ```rust pub fn anchored_rect(anchor: Rect, size: Size, preferred: Side, bounds: Rect) -> Rect ``` Place a `size` panel adjacent to `anchor`, preferring `side`, flipping to the opposite side when there isn't room, and clamped to stay within `bounds`. `side` decides which edge of `anchor` the panel opens from: [`Side::Below`]/[`Side::Above`] place the panel's left edge at `anchor`'s left edge and stack it vertically off `anchor`'s bottom/top edge; [`Side::Right`]/[`Side::Left`] place the panel's top edge at `anchor`'s top edge and lay it out horizontally off `anchor`'s right/left edge. If the preferred side doesn't have enough room within `bounds` (the panel's far edge would fall outside `bounds` on that axis) but the opposite side does, the panel opens on the opposite side instead; if neither side has room, the preferred side is kept and clamped like the fitting case. Once a side is chosen, the panel is clamped along the perpendicular axis so it never runs past `bounds`' edges: this is the three-line clamp a hand-rolled dropdown would otherwise repeat (`x.min(bounds.right() - width).max(bounds.left())`), applied to whichever axis `side` didn't already pin. `size` is clamped down to `bounds`' own dimensions if larger, so the result is always fully within `bounds`, the same guarantee [`centered_rect`] makes for a centered box. Pure layout math: no drawing, no `Terminal`. Callers still own sizing (deciding `size` from content, with a floor/ceiling) and overflow (scrolling when content is taller than the resulting rect); this only answers where the rect goes. Never panics: every offset is computed with saturating arithmetic, so a degenerate `anchor`, `size`, or `bounds` (zero width/height, or `anchor` outside `bounds`) resolves to a clamped, zero-size-or-larger rect instead of under/overflowing. #### Examples ``` use retroglyph_core::grid::{Rect, Size}; use retroglyph_ui::{Side, anchored_rect}; let bounds = Rect::new(0, 0, 40, 20); let anchor = Rect::new(5, 5, 10, 1); // e.g. a menu label let rect = anchored_rect(anchor, Size::new(12, 4), Side::Below, bounds); assert_eq!(rect, Rect::new(5, 6, 12, 4)); ``` ## src/perf/app.rs ### default_is_toggle_key ```rust pub fn default_is_toggle_key(event: &Event) -> bool ``` Whether `event` is the overlay's default toggle key. Backtick, or F1 as an alias, on a press (not a repeat or a release, so a backend that reports releases doesn't toggle twice per physical key press). Override with [`PerfOverlayApp::toggle_key`]. ### PerfOverlayApp ```rust pub struct PerfOverlayApp { } ``` Wraps an [`App`] with a toggleable perf overlay. See the [module docs](super). ### impl PerfOverlayApp ```rust impl PerfOverlayApp { pub fn new(inner: A, backend: &'static str) -> Self; } ``` ### impl PerfOverlayApp ```rust impl PerfOverlayApp { pub fn with_closure(inner: A, backend: &'static str, renderer: F) -> Self; } ``` ### impl PerfOverlayApp ```rust impl PerfOverlayApp { pub fn with_renderer(inner: A, backend: &'static str, renderer: R) -> Self; pub fn cycle_with(self, size: Size, renderer: F) -> Self where F: FnMut + 'static; pub fn visible(self, visible: bool) -> Self; pub fn layer(self, layer: u8) -> Self; pub fn size(self, size: Size) -> Self; pub fn toggle_key(self, matches: Unknown) -> Self; pub fn stats(&self) -> &FrameStats; pub fn is_visible(&self) -> bool; pub fn mode(&self) -> PerfOverlayMode; pub fn set_mode(&mut self, mode: PerfOverlayMode); pub fn toggle(&mut self); pub fn inner(&self) -> &A; pub fn inner_mut(&mut self) -> &mut A; pub fn into_inner(self) -> A; } ``` ### impl App for PerfOverlayApp ```rust impl App for PerfOverlayApp { } ``` ### impl App for CountingApp ```rust impl App for CountingApp { } ``` ### impl Output for NoPushEventBackend ```rust impl Output for NoPushEventBackend { } ``` ### impl Input for NoPushEventBackend ```rust impl Input for NoPushEventBackend { } ``` ### impl Cursor for NoPushEventBackend ```rust impl Cursor for NoPushEventBackend { } ``` ### impl App for SeesInput ```rust impl App for SeesInput { } ``` ## src/perf/mod.rs ### app::{PerfOverlayApp, default_is_toggle_key} ```rust pub use app::{PerfOverlayApp, default_is_toggle_key}; ``` ### mode::PerfOverlayMode ```rust pub use mode::PerfOverlayMode; ``` ### renderer::{DefaultPerfRenderer, PerfRenderer} ```rust pub use renderer::{DefaultPerfRenderer, PerfRenderer}; ``` ### FRAME_HISTORY ```rust pub const FRAME_HISTORY: usize ``` How many frames [`PerfOverlayApp`]'s internal [`FrameStats`](retroglyph_core::frames::FrameStats) remembers. About two seconds at 60fps. Not configurable per instance: pick a bigger window by building a `FrameStats` directly and rendering it through a custom [`PerfRenderer`] closure instead of [`PerfOverlayApp`], if a specific app genuinely needs one. ### DEFAULT_LAYER ```rust pub const DEFAULT_LAYER: u8 ``` [`PerfOverlayApp`]'s default overlay layer: [`Layer::Debug`]. The workspace's named top-most UI tier, so a perf HUD stays visible over whatever else is on screen (including an open [`Layer::Overlay`] popup) rather than risking a lower, app-chosen layer hiding it. Override with [`PerfOverlayApp::layer`] if an app's own content already reaches this layer. ## src/perf/renderer.rs ### PerfRenderer ```rust pub trait PerfRenderer { fn render(&mut self, stats: &FrameStats, backend: &str, area: Rect, surface: &mut Surface<'_>); } ``` Draws a [`super::PerfOverlayApp`]'s stats into a rectangular area of a [`Surface`]. Implemented for any `FnMut(&FrameStats, &str, Rect, &mut Surface<'_>)` (pass such a closure to [`PerfOverlayApp::with_closure`](super::PerfOverlayApp::with_closure); see its docs for why that constructor exists instead of just accepting `impl PerfRenderer` everywhere), so a plain closure is enough for a custom overlay; see the [module docs](super) for composing one out of this crate's widgets. [`DefaultPerfRenderer`] is the built-in implementation, used by [`PerfOverlayApp::new`](super::PerfOverlayApp::new). ### impl PerfRenderer for F ```rust impl PerfRenderer for F { } ``` ### impl FixedBuf ```rust impl FixedBuf { } ``` ### impl fmt::Write for FixedBuf ```rust impl fmt::Write for FixedBuf { } ``` ### DefaultPerfRenderer ```rust #[derive(Debug, Clone, Copy)] pub struct DefaultPerfRenderer { } ``` The built-in [`PerfRenderer`], used by [`PerfOverlayApp::new`](super::PerfOverlayApp::new). A single-row `NNNfps MM.Mms minMM.M maxMM.M ` readout, right-aligned within its area, on a solid background. Colored from a [`Theme`] (see [`DefaultPerfRenderer::theme`]), so a caller matching a [`Theme`]-driven UI elsewhere doesn't get a hardcoded, unrelated palette here. A no-op before the first frame is recorded, or if the readout doesn't fit `area`'s width. ### impl Default for DefaultPerfRenderer ```rust impl Default for DefaultPerfRenderer { } ``` ### impl DefaultPerfRenderer ```rust impl DefaultPerfRenderer { pub fn new() -> Self; pub fn theme(self, theme: Theme) -> Self; } ``` ### impl PerfRenderer for DefaultPerfRenderer ```rust impl PerfRenderer for DefaultPerfRenderer { } ``` ## src/perf/mode.rs ### PerfOverlayMode ```rust #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum PerfOverlayMode { Off, Compact, Full, } ``` How much detail [`super::PerfOverlayApp`] currently shows, advanced by the toggle key. Cycles `Off -> Compact -> Full -> Off`. [`Full`](Self::Full) is only reachable once [`PerfOverlayApp::cycle_with`](super::PerfOverlayApp::cycle_with) has registered a second renderer; without one, the cycle skips straight from [`Compact`](Self::Compact) back to `Off`, i.e. the plain two-state toggle from before this enum existed. ### impl PerfOverlayMode ```rust impl PerfOverlayMode { } ``` ## src/state/mod.rs ### list::{ListState, SelectionWrap} ```rust pub use list::{ListState, SelectionWrap}; ``` ### scroll::{ScrollPhysics, ScrollState} ```rust pub use scroll::{ScrollPhysics, ScrollState}; ``` ### text_input::TextInputState ```rust pub use text_input::TextInputState; ``` ## src/state/scroll.rs ### ScrollPhysics ```rust #[derive(Clone, Copy, Debug, PartialEq)] pub struct ScrollPhysics { pub friction: f32, pub stiffness: f32, pub damping: f32, pub rubber_band_limit: f32, } ``` Configurable physics constants for [`ScrollState`]. Not `#[non_exhaustive]`: unlike this crate's enums, this type's whole point is direct struct- literal construction, which `#[non_exhaustive]` would forbid for external crates (including via functional update syntax). Construct a custom value off [`ScrollPhysics::DEFAULT`] with functional update syntax instead of naming every field, so a field added here later needs at most a call-site addition rather than a rewrite: ``` use retroglyph_ui::ScrollPhysics; let physics = ScrollPhysics { friction: 6.0, ..ScrollPhysics::DEFAULT }; ``` ### impl ScrollPhysics ```rust impl ScrollPhysics { pub const DEFAULT: Type; } ``` ### impl Default for ScrollPhysics ```rust impl Default for ScrollPhysics { } ``` ### ScrollState ```rust #[derive(Clone, Debug, PartialEq)] pub struct ScrollState { } ``` Scroll state for smooth, momentum-based scrolling with rubber-banding. Keeps track of the current fractional scroll offset, velocity, and drag-to-scroll gestures. Completely separate from drawing, and generic over time: takes a time delta step to decay velocity or animate snap-back, making it deterministic and suitable for unit tests. For a *row*-based viewport (a menu, a list of fixed-height items) where content scrolls a whole row at a time and there's no momentum to animate, reach for [`crate::ListState`] instead: its `offset` is a plain `usize`, clamped only at zero, with no velocity or physics step. The two don't compose into one type on purpose (see [`crate::ListState`]'s own doc comment): pick whichever one matches what's actually scrolling: continuous/pixel-ish content reaches for `ScrollState`, a discrete item list reaches for `ListState`. ### impl Default for ScrollState ```rust impl Default for ScrollState { } ``` ### impl ScrollState ```rust impl ScrollState { pub fn new() -> Self; pub fn with_physics(physics: ScrollPhysics) -> Self; pub fn offset(&self) -> f32; pub fn set_offset(&mut self, offset: f32, max_offset: f32); pub fn velocity(&self) -> f32; pub fn dragging(&self) -> bool; pub fn integer_offset(&self) -> usize; pub fn fractional_offset(&self) -> f32; pub fn tick(&mut self, dt: core::time::Duration, max_offset: f32); pub fn begin_drag(&mut self, y: f32); pub fn update_drag(&mut self, y: f32, max_offset: f32); pub fn end_drag(&mut self); pub fn scroll_by_wheel(&mut self, delta: f32); pub fn apply(&mut self, response: &Response); } ``` ## src/state/text_input.rs ### TextInputState ```rust #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct TextInputState { } ``` A `String` value, a byte cursor into it, and a horizontal scroll offset. The state a single-line editable text field needs, mirroring [`ListState`](crate::ListState)'s split between "what's selected/typed" and "how it's drawn". Holds no reference to any widget: the same `TextInputState` can be reused across frames (and across a resized field) the way `ListState` is reused across a resized list. `cursor` is a byte index into `value`, not a char or display-column index, and every mutating method here maintains the invariant that it always lands on a char boundary: `insert`/ `insert_str`/`backspace`/`delete` never split a multi-byte character, and `move_left`/ `move_right` step by whole `char`s. Display-column math (where the caret actually draws, and how far the field has scrolled) is a separate concern handled by [`ensure_visible`](Self::ensure_visible) and the [`TextInput`](crate::TextInput) widget itself, via `retroglyph_core::text::width_usize`: a byte or char count is the wrong unit once the value contains a double-width character. Handles typed characters (`Event::Key(KeyCode::Char(c))`) and pasted text (`Event::Paste`) only, not IME/text composition or multi-line editing. Cursor movement and deletion step by Unicode `char` (codepoint), not by grapheme cluster: a base character plus a combining mark is two `char`s, so `backspace` there removes only the mark and `move_left` stops between the two. A field over text with combining marks or emoji-ZWJ sequences will show per-codepoint, not per-glyph, editing. ### impl TextInputState ```rust impl TextInputState { pub fn new() -> Self; pub fn value(&self) -> &str; pub fn set_value(&mut self, s: impl Trait); pub fn cursor(&self) -> usize; pub fn scroll(&self) -> u16; pub fn insert(&mut self, c: char); pub fn insert_str(&mut self, s: &str); pub fn backspace(&mut self); pub fn delete(&mut self); pub fn move_left(&mut self); pub fn move_right(&mut self); pub fn move_home(&mut self); pub fn move_end(&mut self); pub fn handle_event(&mut self, event: &Event) -> bool; pub fn ensure_visible(&mut self, width: u16); } ``` ## src/state/list.rs ### SelectionWrap ```rust #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum SelectionWrap { Clamp, Wrap, } ``` How [`ListState::select_next`]/[`select_previous`](ListState::select_previous) behave when the selection is already at the first/last item. Defaults to [`Clamp`](Self::Clamp), matching ratatui's `ListState` (`select_next`/ `select_previous` `saturating_add`/clamp at the ends; wraparound is left to the caller, e.g. via `(selected + 1) % len`). Older `tui-rs`-style wraparound is available via [`Wrap`](Self::Wrap) for callers that want circular menu navigation instead. ### ListState ```rust #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct ListState { } ``` Selection index and scroll offset for a selectable, scrollable list. Holds no reference to the list's actual items: `len` is passed in to each selection-movement method, so `select_next`/`select_previous`/`select_first`/`select_last` and the scroll offset stay valid across lists that change size (menus, reward pools, deck views, ...). A selection set directly with [`select`](Self::select) is the exception: it is stored unchecked and can outlive a shrunk list. See that method. Selection movement clamps at `len`'s ends by default; see [`SelectionWrap`] (set via [`ListState::set_wrap`]) to switch to wraparound instead. Scrolling is a separate, unbounded-above counter (clamped only at zero) since only the caller knows the content length and viewport height needed to clamp it from above. `offset` is always a whole row: there's no momentum, velocity, or sub-row position here. For continuous/pixel-ish scrolling with momentum and rubber-banding (a smoothly-scrolled log or panel, not a discrete item list), reach for [`crate::ScrollState`] instead, and drive this type's `offset` from [`crate::ScrollState::integer_offset`] if a list needs both a selection cursor and smooth scrolling together. ### impl ListState ```rust impl ListState { pub fn new() -> Self; pub fn selected(&self) -> Option; pub fn offset(&self) -> usize; pub fn wrap(&self) -> SelectionWrap; pub fn set_wrap(&mut self, wrap: SelectionWrap); pub fn select(&mut self, index: Option); pub fn set_offset(&mut self, offset: usize); pub fn reset(&mut self); pub fn ensure_visible(&mut self, visible_height: usize); pub fn scroll_by(&mut self, delta: i32); pub fn select_next(&mut self, len: usize); pub fn select_previous(&mut self, len: usize); pub fn select_first(&mut self, len: usize); pub fn select_last(&mut self, len: usize); } ``` ## src/text.rs ### truncate ```rust pub fn truncate(s: &str, max_cols: impl Trait) -> &str ``` Truncate `s` so its display width is at most `max_cols` terminal columns. Truncates on a whole-character boundary; a character that would push the total over `max_cols` is dropped along with the rest of the string. A thin wrapper over `retroglyph_core::text::split_at_width`; `max_cols` is saturated to `u16::MAX` before splitting, matching that function's own saturation. Returns a borrowed slice of `s`, so this allocates nothing. See [`truncate_owned`] if you need an owned `String` (e.g. to store past the lifetime of `s`). `max_cols` takes `impl Into` so a `Rect` dimension (`u16`) can be passed directly, alongside a plain `usize`. ### truncate_owned ```rust pub fn truncate_owned(s: &str, max_cols: impl Trait) -> String ``` Owned variant of [`truncate`]: truncate `s` to `max_cols` display columns and copy the surviving prefix into a new `String`. Prefer [`truncate`] on hot paths (it borrows instead of allocating); reach for this only when an owned `String` is actually needed. ### draw_clipped ```rust pub fn draw_clipped(surface: &mut Surface<'_>, at: impl Trait, width: u16, text: &str, align: Align, style: Style) -> u16 ``` Truncate `text` to `width` columns, align it within those columns per `align`, and print it into `surface` at `at`. Returns the printed text's display width in columns. This is the truncate -> align -> print sequence every single-line widget in this crate needs, collapsed to one call: [`truncate_measured`] to fit `width` and get its own display width back in the same pass (a wide character can make the truncated text narrower than `width`, never wider, so that width isn't just `width` itself), then [`Surface::print_aligned`](retroglyph_core::surface::Surface::print_aligned) to place and print it. `Align` is a plain re-export of the `HAlign` that `print_aligned` itself takes, so no conversion is needed. Delegating keeps the offset math in one place instead of a second copy here: `text` is already fitted to `width` before it reaches `print_aligned`, so its own internal width measurement agrees with `clipped_width` and produces the same offset. Reach for this instead of re-deriving the sequence by hand, the same way [`fill_rect`](crate::fill_rect) is reached for instead of a hand-rolled fill loop; see [`Text`](crate::Text)'s and [`PrintLine`](crate::PrintLine)'s own `render` for the base case. ## src/ui.rs ### impl Cursor ```rust impl Cursor { } ``` ### Ui ```rust pub struct Ui<'s, 'g, Id> { } ``` One frame's drawing surface and interaction state, together, so a call site names an `area`/`id` once and gets both hit-testing and drawing from it: see [`show`](Self::show). #### Why two lifetimes `Surface<'g>` holds a `&'g mut Grid`, which makes `Surface` invariant in `'g`: nothing can shrink or otherwise reinterpret that lifetime once it is fixed. The surface borrow and the grid borrow are therefore kept as two separate lifetime parameters here, `'s` (how long this `Ui` itself, and the `&'s mut Surface` it holds, lives) and `'g` (how long the underlying grid is borrowed for). Collapsing them into one, e.g. writing the field as `&'a mut Surface<'a>`, forces `'a` to cover both uses at once: the invariance in `'g` then makes the borrow of the surface last exactly as long as the grid borrow it is invariant over, so the surface (and the grid behind it) stay borrowed, and therefore unusable, for the rest of `'a` even after the `Ui` that held them is dropped. Two parameters let `'s` end (releasing the `Ui`'s borrow of the surface) while `'g` keeps going, which is exactly what [`Interaction::frame`] relies on: the surface passed in is usable again once the closure returns. ### impl Ui ```rust impl<'s, 'g, Id> Ui { pub fn new(surface: &'s mut Surface<'g>, interaction: &'s mut Interaction) -> Self; pub fn is_enabled(&self) -> bool; pub fn surface(&mut self) -> &mut Surface<'g>; pub fn interaction(&mut self) -> &mut Interaction; pub fn area(&self) -> Rect; } ``` ### impl Ui ```rust impl<'g, Id> Ui { pub fn enabled(&mut self, enabled: bool) -> Ui<'_, 'g, Id>; pub fn show(&mut self, area: Rect, id: Id, widget: &impl Trait) -> Response; pub fn show_stateful(&mut self, area: Rect, id: Id, widget: &W, state: &mut W::State) -> Response; pub fn draw(&mut self, area: Rect, widget: &impl Trait); pub fn draw_stateful(&mut self, area: Rect, widget: &W, state: &mut W::State); pub fn region(&mut self, area: Rect, id: Id, sense: Sense) -> (Response, Surface<'_>); pub fn modal(&mut self, area: Rect, f: impl Trait) -> R; pub fn vertical(&mut self, f: impl Trait) -> R; pub fn horizontal(&mut self, f: impl Trait) -> R; pub fn vertical_sized(&mut self, height: u16, f: impl Trait) -> R; pub fn horizontal_sized(&mut self, width: u16, f: impl Trait) -> R; pub fn show_sized(&mut self, id: Id, widget: &impl Trait, size: u16) -> Response; pub fn draw_sized(&mut self, widget: &impl Trait, size: u16); pub fn show_auto(&mut self, id: Id, widget: &Unknown) -> Response; pub fn draw_auto(&mut self, widget: &Unknown); } ``` ### impl InteractiveWidget for Dot ```rust impl InteractiveWidget for Dot { } ``` ### impl Widget for Fill ```rust impl Widget for Fill { } ``` ### impl Widget for FixedHeight ```rust impl Widget for FixedHeight { } ``` ### impl InteractiveWidget for FixedHeight ```rust impl InteractiveWidget for FixedHeight { } ``` ### impl Measure for FixedHeight ```rust impl Measure for FixedHeight { } ```