retroglyph-ui 0.5.1

Immediate-mode terminal UI toolkit for retroglyph: widgets, layout, input/focus, theming, and animation
Documentation
# retroglyph-ui

[![crates.io](https://img.shields.io/crates/v/retroglyph-ui.svg)](https://crates.io/crates/retroglyph-ui)
[![docs.rs](https://img.shields.io/docsrs/retroglyph-ui)](https://docs.rs/retroglyph-ui)
[![coverage](https://codecov.io/gh/crates-lurey-io/retroglyph/graph/badge.svg?token=z8BBUp8fiY&flag=widgets)](https://app.codecov.io/gh/crates-lurey-io/retroglyph/flags)
[![license](https://img.shields.io/crates/l/retroglyph-ui.svg)](https://github.com/crates-lurey-io/retroglyph/blob/main/LICENSE)

Immediate-mode drawing helpers for [retroglyph](https://github.com/crates-lurey-io/retroglyph): box
borders, filled panels, gauges, tables, lists, tab strips, buttons, sparklines, and a small
constraint-based layout splitter (`split_h`/`split_v` with ratatui-style
`Fixed`/`Percent`/`Fill`/`Min`/`Max` constraints, plus const-generic `split_h_n`/`split_v_n`
siblings that return `[Rect; N]` instead of allocating a `Vec`), plus hover/click/drag/focus
interaction tracking. Every widget (`Panel`, `Gauge`, `Table`, `Sparkline`, `BoxBorder`, `List`,
`Tabs`, `Button`, `Scrollbar`, `ProgressBar`, `Modal`, `Menu`, `StatBar`, `Meter`, `Log`,
`TextInput`, ...) is a builder struct that draws itself into a `Surface` (an area-relative view over
a `Grid`) via `Widget`/`StatefulWidget` and retains no state of its own; state that outlives one
render call (a selection index, a scroll offset, a text field's value and cursor) lives in
`ListState`/`MenuState`/`TextInputState` instead. A handful of things that are genuinely just
functions (`fill_rect`, `thumb_geometry`/`offset_for_pos`, `truncate`/`truncate_owned`) stay free
functions rather than pretending to be widgets. Depends only on
[`retroglyph-core`](https://crates.io/crates/retroglyph-core), so games that draw manually never
pull it in.

Alongside the widgets is the constraint-based `Rect` splitter above. `Layout::vertical`/
`Layout::horizontal` build on `split_v`/`split_h` with two optional builder calls: `.spacing(n)`
carves a fixed-cell gap (or overlap) between panes, and `.flex(Flex)` controls where leftover space
goes, with `Flex` alignment (`Start`/`End`/`Center`/`SpaceBetween`/`SpaceAround`) similar to
[ratatui](https://ratatui.rs)'s layout system. `Fill(weight)` claims a share of the leftover space
proportional to `weight` relative to the other `Fill`/`Min`/`Max` panes in the same split (`Fill(1)`
reproduces plain equal distribution).

Three more independent layers build on top:

- `Widget`/`StatefulWidget` traits let callers box or store heterogeneous widgets, e.g. a
  `Vec<Box<dyn Widget>>` of panes to render each frame, backed by `ListState` for selection and
  scroll position. `AnimatedWidget`, a sibling of `StatefulWidget`, is for state that evolves with
  wall-clock time instead (`ScrollState`'s momentum/rubber-band physics, a `Tween`-driven
  transition), taking a `Frame` (the same one `App::update` already receives) alongside the state,
  so advancing and drawing happen in one call instead of two independently ordered ones. `Scrollbar`
  implements it directly, ticking `ScrollState` before drawing the thumb at the result.
  `ScrollState::apply` feeds a frame's resolved `Response::scroll_delta` straight into the wheel
  impulse, so a scrollable widget doesn't have to re-derive wheel handling from raw mouse events
  (see `13_combat_log`, which wires wheel scrolling into its `Log`/`Scrollbar` pair this way).
- `BoxStyle`, a Lip-Gloss-style box model (padding, border, margin) rendered into a standalone
  `Grid`. `Paragraph` (behind the `egc` feature) word-wraps text via `retroglyph-core`'s
  `TextLayout` and implements a `Measure` trait so a caller can size a pane to fit before rendering.
- `join_h`/`join_v` to compose several `Grid`s (e.g. `BoxStyle::render` output) into one;
  `retroglyph-core`'s `Surface::blit` stamps the result onto a surface.
- `Theme` (`Theme::DARK`/`Theme::LIGHT`, or a caller-built palette): named color roles (`border`,
  `accent`, `hover_bg`, ...) that every widget with a style knob can pick up via a `.theme(Theme)`
  builder method, optionally: a manual `.border_style(...)`/etc. call after `.theme(...)` still
  wins, and nothing requires a `Theme` at all.

See the `09_widgets_dashboard` and `15_outpost_dashboard` examples for all of the above wired
together in one UI, `17_theme_switch` for `Theme::DARK`/`Theme::LIGHT` switched live at runtime by a
keypress, or `19_weighted_fill` for `Fill(weight)`'s proportional splits.

## Quick start

```sh
cargo add retroglyph-core retroglyph-ui
```

```rust
use retroglyph_core::grid::{Grid, Rect};
use retroglyph_core::surface::Surface;
use retroglyph_ui::widget::{Gauge, Widget};

let area = Rect::new(0, 0, 20, 1);
let mut grid = Grid::new(20, 1);
Gauge::new("HP", 0.75).render(&mut Surface::new(&mut grid, area, 0));
```

A clickable widget (`Button`, `Scrollbar`, `List`, `Tabs`, or any `InteractiveWidget`) is shown
through `Ui`, which pairs one frame's `Surface` with an `Interaction<Id>` so a call site names an
area and an id once and gets both hit-testing and drawing from it:

```rust
use retroglyph_core::grid::{Grid, Rect};
use retroglyph_core::surface::Surface;
use retroglyph_ui::interact::Interaction;
use retroglyph_ui::widget::Button;

#[derive(Clone, Copy, PartialEq, Eq)]
enum WidgetId {
    Save,
}

let mut grid = Grid::new(20, 1);
let mut interaction = Interaction::<WidgetId>::new();
let clicked = interaction.frame(&mut Surface::new(&mut grid, Rect::new(0, 0, 20, 1), 0), |ui| {
    ui.show(Rect::new(0, 0, 10, 1), WidgetId::Save, &Button::new("Save")).clicked()
});
assert!(!clicked); // nothing clicked yet: no input was fed in
```

A control that exists but can't be used right now ("Save" with no game loaded, say) disables a whole
`Ui` subtree via `Ui::enabled`, not a per-widget flag: the returned `Response` still reports
`hovered`, so a call site can explain why, but never an activation:

```rust
use retroglyph_core::grid::{Grid, Rect};
use retroglyph_core::surface::Surface;
use retroglyph_ui::interact::Interaction;
use retroglyph_ui::widget::Button;

#[derive(Clone, Copy, PartialEq, Eq)]
enum WidgetId {
    Save,
}

let has_unsaved_changes = false;
let mut grid = Grid::new(20, 1);
let mut interaction = Interaction::<WidgetId>::new();
interaction.frame(&mut Surface::new(&mut grid, Rect::new(0, 0, 20, 1), 0), |ui| {
    let mut ui = ui.enabled(has_unsaved_changes);
    let response = ui.show(Rect::new(0, 0, 10, 1), WidgetId::Save, &Button::new("Save"));
    if response.hovered() && response.disabled() {
        // draw a tooltip: "Nothing to save"
    }
});
```

## Features

<!-- gen-features:start -->
<details>

<summary>Default features: `std`.</summary>

### `dev`

⚪ Optional.

Forwards `retroglyph-core`'s `dev` feature, which forces development diagnostics on in a build that
would otherwise compile them out (see `retroglyph_core::dev`).

### `egc`

⚪ Optional.

Forwards to `retroglyph-core`'s `egc` feature.

Upgrades `Paragraph`'s word-wrap (always available) to grapheme-cluster-aware correctness.

### `libm`

⚪ Optional.

Uses `retroglyph-core`'s `libm` feature (the `no_std` float backend: easing curves and tweens,
scrollbar geometry, gauge/sparkline/bar percentage rounding, scroll momentum decay) instead of
`std`'s own float intrinsics. See `std` below; a build needs exactly one of the two.

### `serde`

⚪ Optional.

Adds `Serialize`/`Deserialize` impls for `Theme` and `Density`, forwarding to `retroglyph-core`'s
`serde` feature.

`Theme` round-trips through `Color`'s own `serde` impl.

### `std`

🟢 Enabled by default.

Enables `retroglyph-core/std`, whose float intrinsics back this crate's own float use (see `libm`
above for the `no_std` alternative).

Disabling this feature (`--no-default-features`) builds this crate `no_std` (requires an allocator
and one of `std`/`libm`; see the crate-level `compile_error!` in `src/lib.rs`).

### `testing`

⚪ Optional.

Enables `testing`'s `WidgetHarness`, the sibling of `retroglyph-core`'s `testing::TestHarness` for
driving a bare `Interaction<Id>`/`Ui` (no `App` required) against `Headless` for tests, with
synthetic input queuing and a `step` that drains it.

Test-only surface, off by default so it never ships in a release build by accident. `Headless` is
unconditionally available in `retroglyph-core`, so this needs no feature forwarding there.

</details>
<!-- gen-features:end -->

See [docs.rs](https://docs.rs/retroglyph-ui) for the full API, or the
[workspace README](https://github.com/crates-lurey-io/retroglyph#readme) for the crate list.