feat: v0.20.0 — features + perf + breaking + v0.19.x patch sweep#239
Merged
Conversation
Add a stable, deterministic serializer for the render buffer that captures both text and styles in a format compatible with `insta::assert_snapshot!`. Default-style cells emit raw text; non-default runs are wrapped as `[fg=...,bg=...,mods]"text"[/]`. Named palette colors use short codes (`red`, `light_blue`); RGB → `#rrggbb`; indexed palette → `idx<N>`. Modifiers emitted in canonical order (bold, dim, italic, underline, reversed, strikethrough) so two equivalent values always serialize identically. Format pinned by `tests/snapshot_format_stability.rs` with 9 hand-crafted scenarios (default-only, color runs, modifier ordering, fg+bg, indexed, RGB, escape handling, determinism, wide-char trailing). Hand-rolled formatter for `Color` / `Modifiers` so upstream `Debug` derive changes cannot silently break locked snapshots. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…serts Three additive `TestBackend` features for the v0.20 test-utils foundation, all isolated to `src/test_utils.rs` plus the lib re-exports for the new public types. #229 record_frames + FrameRecord - `TestBackend::record_frames()` enables an opt-in per-render history. Disabled by default → zero allocation on the hot path. - `tb.frames()` returns `&[FrameRecord]`; `FrameRecord` carries both the styled snapshot string (via `Buffer::snapshot_format`) and the per-row trimmed text view, plus `assert_contains` / `to_string_trimmed` / `line` helpers. #230 sequence builder + type_string - `TestBackend::sequence()` returns a `TestSequence<'a>` with `.tick()`, `.key()`, `.type_string()`, `.events()`, `.run()`. Steps execute in order with FrameState advancing naturally between them so callers no longer thread `focus_index` / `prev_focus_count` manually. - Backend-level `tb.type_string(s, render)` fires one render per char. - Sequence-level `.type_string(s, render)` collapses all chars into a single render step — different ergonomics for different needs. #232 negative assertions - `assert_not_contains(&str)` — fails listing offending row indices. - `assert_line_not_contains(y, &str)` — per-row negation. - `assert_empty_line(y)` — row is blank after trim. - `assert_style_at(x, y, Style)` — cell-level Style equality with `(x, y, expected, actual)` panic message on mismatch. 19 unit tests + 5 doctests cover all three issues. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Self-contained showcase that exercises every new v0.20 test-utils API in a single file, plus a sibling test that re-imports the demo via `#[path]` and locks its rendered output. - examples/v020_test_utils.rs — runnable demo with deterministic render functions for each new feature. - tests/v020_test_utils_demo.rs — 7 regression tests (3 happy-path, 2 stability / determinism, 2 should_panic) sharing the demo's render code so the example and its tests cannot drift. - CHANGELOG.md — Added section under [Unreleased] with one line per issue. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces v0.19's six-Option `Constraints` struct (36 bytes) with two enum
fields: `width: WidthSpec` and `height: HeightSpec` (24 bytes total, 33 %
reduction). Each axis has five variants — Auto, Fixed(u32), Pct(u8),
Ratio(u16, u16), MinMax { min: u32, max: u32 } — covering everything the
old struct could express plus exact integer-fraction sizing via the new
`w_ratio` / `h_ratio` builders.
Layout resolution in `flexbox.rs::compute_body` and the `layout_row` /
`layout_column` resolution sites collapse from three separate
`if let Some(pct)` blocks into a single `match` per axis (new
`resolve_axis_specs` helper).
Existing builder methods (`min_w`, `max_w`, `min_h`, `max_h`, `w`, `h`,
`w_pct`, `h_pct`) are preserved as ergonomic shortcuts that set the
appropriate variant. New imperative setters (`set_min_width`,
`set_max_width`, `set_width_pct`, …) cover the rare imperative-mutation
sites previously written as `c.min_width = Some(value)`. New accessors
(`min_width()`, `max_width()`, `width_pct()`, …) replace direct field
reads with parenthesized method calls and return the same `Option<u32>` /
`Option<u8>` shape.
The `MinMax` variant uses sentinel encoding (`min = 0` ≡ no minimum,
`max = u32::MAX` ≡ no maximum) so the variant fits in 12 bytes — the
only way to hit the 24-byte total without losing expressivity. Sentinels
are isolated behind the public API; users see `Option<u32>` semantics via
the accessors.
Closes #207, #219 (per #237 super-issue resolution). The breaking change
is documented in CHANGELOG with a 3-line migration recipe.
- src/style.rs — WidthSpec / HeightSpec enums + Constraints struct;
builders, accessors, setters; `_ASSERT_CONSTRAINTS_SIZE` const guard
- src/layout/flexbox.rs — single-match `resolve_axis_specs` per axis;
call sites in `compute_body` / `layout_row` / `layout_column` updated
- src/layout/tree.rs — accessor calls in `min_width` / `min_height` and
`raw_draw` constructor
- src/context/widgets_interactive/collections.rs — struct literals →
builder chain
- src/context/container.rs, src/context/widgets_display/text.rs —
imperative setter migration to keep build green for downstream agents
- src/lib.rs — public re-exports for WidthSpec, HeightSpec
- tests/widthspec_v020.rs — 13 tests (variants, ratios, size budget,
serde round-trip for all 5 variants of each spec, imperative setters)
- examples/v020_widthspec.rs — visual showcase of all 5 variants
- tests/v020_widthspec_demo.rs — 80×25 TestBackend snapshot test
- CHANGELOG.md — Breaking section with migration recipe
- Cargo.toml — serde_json dev-dependency for serde round-trip tests
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…itty Vec clone, modal-aware dim Implements 4 hot-path perf issues for v0.20.0: - #204 FrameState reuse 6 per-frame Vec/HashSet allocations via mem::take. context_stack, deferred_draws, rollback.group_stack, rollback.text_color_stack, pending_tooltips, hovered_groups now follow the same swap/reclaim pattern established in #150 / #155. 100-frame steady-state alloc count is now bounded (~13 allocs/frame measured; previously scaled with the six fields). - #205 wrap_segments String allocation. Pre-size each style-run String with with_capacity computed from the remaining bytes in the source segment, capped at max_width * 4 to bound over-allocation. No behavioral change. - #206 InlineTerminal::flush kitty Vec<KittyPlacement> clone. Pass row_offset: u32 directly to KittyImageManager::flush instead of materializing a translated copy. prev_placements now stores post-offset y for the diff fast-path. Stable steady-state flushes allocate only once (verified — 1 alloc across 100 flushes). - #228 dim_buffer modal-aware diff path. Replaces the unconditional O(W*H) buffer scan with dim_buffer_around(modal_rect) that walks only the four strips outside the modal rect. Original dim_entire_buffer is retained as the zero-size-modal fallback. Visible output unchanged. Deliverables: - src/lib.rs: 6 new FrameState fields + reclaim path in run_frame_kernel - src/context/runtime.rs: Context::new swap-in via mem::take - src/layout/tree.rs: with_capacity in wrap_segments - src/terminal.rs: KittyImageManager::flush takes row_offset; new __BenchKittyFixture for tests - src/layout/render.rs: dim_buffer_around new path; dim_entire_buffer kept - tests/v020_perf_alloc.rs: 8 alloc-budget tests using a counting global allocator - tests/v020_perf_demo.rs: 5 visual-integrity tests - examples/v020_perf_audit.rs: timing breakdown demo for all 4 hot paths - CHANGELOG.md: 4 perf entries under Unreleased / Performance Quality gates: fmt, check (--all-features), clippy (-D warnings), test (--all-features), examples — all green. Extended gate: typos, no-default, WASM target — all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements 5 v0.20.0 issues for context state/hooks/focus: - #208 Response right_clicked / gained_focus / lost_focus signals - #215 use_state_keyed (BREAKING: State<T> Copy removal — theoretical only, audit confirmed zero current call sites depended on Copy) - #216 use_effect with dependency-tracked side effects - #217 register_focusable_named + focus_by_name + focused_name - #218 key_presses_when(active) + public consume_event(idx) State<T> drops Copy to allow `StateKey::Keyed(String)`. Existing `let s = ui.use_state(...); s.get(ui)` patterns continue to compile; audit grep over src/, tests/, examples/ found zero implicit-copy uses. `gained_focus` / `lost_focus` are populated through begin_widget_interaction using a new `last_focusable_id` rollback marker recorded by register_focusable. prev_focus_index lives on FocusState (Option<usize>, None on first frame so widgets don't falsely report gained_focus). focus_by_name resolves against the previous frame's focus_name_map; when the name isn't there yet, the request is held pending so a later frame that registers the name can still receive focus. 26 new tests + 3 demos. All 5 Core Gates pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Agent 7 — widget additions and return-type unification for v0.20.0. Added: - split_pane / vsplit_pane (#223) — resizable container with mouse-drag handle and arrow-key adjustment via SplitPaneState - gauge / gauge_w / gauge_colored / line_gauge (#224) — block-fill and single-line progress indicators with inline labels and color tiering - scrollable_with_gutter (#235) + ScrollState highlight API (set_highlights, highlight_next, highlight_previous, clear_highlights) with new HighlightRange type — grep-style search navigation in scrollable content Breaking: - spinner / progress / progress_bar / progress_bar_colored now return Response (was &mut Self) (#212) — enables hover/tooltip/scrubber wiring on these feedback widgets. toast continues to return &mut Self. - breadcrumb collapsed to a single BreadcrumbResponse (#213) — replaced 4-variant API with breadcrumb() / breadcrumb_sep(). Response derefs to Response so .hovered/.rect/.focused continue to work after migration. Tests: 20 unit tests in tests/v020_widgets.rs, 7 snapshot smoke tests in tests/v020_widgets_demos.rs, 5 internal gauge string-shape tests. Demos: examples/v020_*.rs covering each issue. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds four ergonomic helpers that compress common immediate-mode patterns without changing semantics: - `Response::on_hover(ui, text)` / `on_hover_ui(ui, f)` — chain a tooltip directly onto a widget response, instead of relying on the order- sensitive post-call `ui.tooltip(...)`. Skips alloc when the widget is not hovered or the text is empty. - `Context::animate_bool(id, value)` / `animate_value(id, target, dur)` — zero-boilerplate implicit animation keyed by `&'static str`, stored in `named_states`. First call snaps to target with no visible pop; subsequent retargets resume from the current interpolated value. Returns `f64` to match SLT's animation type convention. - `ContainerBuilder::fill()` — self-documenting alias for `.grow(1)` (CSS `flex: 1`, ratatui `Constraint::Fill(1)`). - `Rect::center_in(parent)` / `center_horizontally_in` / `center_vertically_in` — `const fn` helpers for the inverse of `Rect::centered`, useful in `draw_raw` callbacks. `pub const DEFAULT_ANIMATE_TICKS: u64 = 12` and the internal `AnimState` type live in `src/anim.rs`. `wrap_tooltip_text` visibility in `src/context/widgets_display.rs` is widened to `pub(super)` so `Response::on_hover` can reuse it. Tests: - `tests/v020_dx_shortcuts.rs` (18 unit/integration tests covering hover, animation transitions, retarget, fill==grow(1) parity, and rect centering). - `tests/v020_dx_shortcuts_demo.rs` (insta visual snapshot). - `examples/v020_dx_shortcuts.rs` exercises all four helpers in one screen with a `pub fn render` entry point reused by the snapshot. Quality gates: fmt, check (--all-features), clippy (lib + new tests/example), test (--all-features), check --examples — all clean. The pre-existing clippy errors in `examples/demo_infoviz.rs` and `src/style/theme.rs:1064` are unrelated to this change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ivation Issues: #225 ModalOptions::tab_trap, #226 ContainerBuilder::theme(), #227 Theme::spacing density presets #225 Modal focus trap (WCAG 2.1 SC 2.4.3) - New ModalOptions struct with tab_trap field; default = true - New Context::modal_with(opts, f) — opt-in focus trap - Plain Context::modal(f) preserves legacy non-trapping behavior - modal_with clamps focus_index back into [start, start+count) when set_focus_index left it outside the modal range #226 Per-subtree theme override - New ContainerBuilder::theme(theme) method - finish() swaps ctx.theme + dark_mode flag for the duration of the closure body, restoring on exit (panic-safe via catch_unwind) - Nested .theme() calls compose correctly: inner overrides outer - Independent of provide / use_context #227 Theme::spacing activation (BREAKING) - New Theme::compact() / comfortable() / spacious() density presets - New Theme::with_spacing(spacing) helper for any preset - Wired theme.spacing.xs() / sm() into widgets: code_block, code_block_numbered, accordion, tooltip, help, help_colored, tabs, checkbox, toggle, select trigger, calendar header, text_input, suggestion box, command palette, markdown code blocks - Default spacing remains Spacing::new(1) → v0.19-identical visuals on the 10 stock presets - BREAKING: customized themes with non-default spacing now affect widget padding/gap. Migration: set ThemeBuilder::spacing() explicitly Tests added (21 total) - tests/v020_theme_modal.rs: 16 tests covering theme override, nesting, panic safety, tab_trap on/off, legacy modal() behavior, Tab cycling, spacing presets, theme color propagation - tests/v020_theme_modal_demos.rs: 5 demo snapshot tests Demos added (3) - examples/v020_theme_subtree.rs — 4-panel theme override - examples/v020_modal_trap.rs — focus trap with bg widgets - examples/v020_spacing_scale.rs — compact/comfortable/spacious Quality gates: fmt --check, check, clippy -D warnings, test, examples Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#233: ui.static_log(line) — append-only scrollback widget API - `Context::static_log(impl Into<String>)` queues lines that flush above the inline dynamic area in `slt::run_static` / `run_static_with`. - Other run modes (full-screen, inline, async) drop the buffer with a `cfg(debug_assertions)` warning. - `Context::take_static_log()` accessor for tests / custom backends. - Plumbing routes through `FrameState::named_states` under a reserved sentinel key — disjoint from Agent 3's perf alloc-reuse fields. #236: WidgetKeyHelp trait + auto help overlay - `WidgetKeyHelp::key_help() -> &'static [(&'static str, &'static str)]` trait for widgets to publish their bindings (allocation-free, const array per widget). - `Context::publish_keymap(name, bindings)` registers entries for the current frame; `published_keymaps()` returns them. - `Context::keymap_help_overlay(open)` renders an automatic modal listing every registered keymap — wire to `?` / `F1` for instant discoverability. - Registry cleared at frame start by `run_frame_kernel` so entries do not leak across frames. - New `PublishedKeymap` struct (re-exported at crate root). #238: RunConfig::handle_ctrl_c(bool) opt-out - New field on `RunConfig`; defaults to `true` (preserves v0.19 behavior). - Builder `RunConfig::handle_ctrl_c(bool)`. - Threaded through `poll_events` and all 4 entry points (`run_with`, `run_inline_with`, `run_static_with`, `run_async_loop`). - When `false`, Ctrl+C is delivered to the frame closure as a regular `Event::Key` with `KeyModifiers::CONTROL` — matches RataTUI's raw-mode semantics. - `run()` rustdoc updated: notes that wrapping with `crossterm::enable_raw_mode()` / `disable_raw_mode()` is redundant (SLT enters raw mode automatically) and references the new opt-out for RataTUI parity. Tests: 16 unit tests in `tests/v020_lib_toplevel.rs` covering static_log accumulation/drain semantics, keymap registry frame-clearing, the const-friendly `PublishedKeymap::new`, and `RunConfig::handle_ctrl_c` builder + opt-out event delivery. Demos: `examples/v020_static_log.rs`, `examples/v020_keymap_help.rs`, `examples/v020_ctrl_c_passthrough.rs`. Snapshot tests in `tests/v020_lib_demos.rs` with baselines committed. Quality gates passed: fmt, check, clippy, test (all 1000+), check --examples, no-default-features, WASM, cargo-hack each-feature. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts: # CHANGELOG.md
…h reclaim) Addresses critical issues from Reviewer A + C audits: 1. Sentinel key constants: removed duplicate definitions in context::runtime, now imported from lib.rs to prevent drift. Dropped the (now redundant) sentinel_key_tests module. Removed #[cfg(feature = "crossterm")] gate on STATIC_LOG_NAMED_STATE_KEY since it's referenced from the unconditional runtime path. (Reviewer A #9, C) 2. split.rs: extracted RATIO_GROW_SCALE = 1000.0 const replacing two magic-number literals; documented why 1000 (~0.1% precision). (Reviewer A #7) 3. SplitPaneState: extracted DEFAULT_SPLIT_MIN_RATIO const; aligned doc-comment with code (was [0.05, 0.95], now correctly [0.10, 0.90]). (Reviewer A #4) 4. run_frame_kernel quit-path: mirrored the alloc-reuse reclaim block (issue #204's 6 buffers) into the early-return quit path so TestBackend reuse across render() calls doesn't silently revert to v0.19 per-frame allocation. (Reviewer C Critical) 5. CHANGELOG: documented the deliberate modal()/modal_with(default) tab_trap default asymmetry for migration clarity. (Reviewer A #5, C) All gates pass: fmt, check, clippy -D warnings, test (1080+), examples.
Codifies the API design rules going forward after the v0.20 retrospective. The v0.20 release exposed inconsistent shapes across sibling widgets (gauge family, scrollable_with_gutter, breadcrumb, HighlightRange) which caused both human and AI coders to guess the public surface incorrectly on first read. Document covers: - Builder pattern for optional fields (Rule 1) - f64 everywhere on public API (Rule 2) - Opts struct at 4+ args (Rule 3) - &mut StateType newtype for stateful widgets (Rule 4) - Response shape: Response or XxxResponse: Deref<Response> (Rule 5) - v0.20 retrospective table mapping each mistake to its fix - PR reviewer checklist - Conflict resolution priority Linked from README.md "Learn The Library" table. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The five widgets added in v0.20.0 (`gauge`, `line_gauge`, `breadcrumb`, `scrollable_with_gutter`, `HighlightRange`) shipped with five different argument styles. This collapses them onto a single design language so v0.20 doesn't expose five mismatched mental models for callers to learn: - `gauge` / `line_gauge` / `breadcrumb` become Egui-style chainable builders that auto-render on Drop. `.show()` captures the response. Deprecated shims (`gauge_w`, `gauge_colored`, `breadcrumb_sep`, `line_gauge_with`) remain for one minor cycle. - `scrollable_with_gutter` takes `GutterOpts<G>` instead of three positional bookkeeping args. `GutterOpts::line_numbers(total, viewport)` is the 90% case; users rarely write the closure manually. - `HighlightRange::single(i)` added as an idiomatic alias for `line(i)`. - `gauge` / `line_gauge` ratio widened from `f32` to `f64` to match `animate_value`, chart APIs, and `progress_bar`. `GaugeResponse.ratio` is now `f64`. Demos: - `examples/v020_showcase.rs` — single-screen tour of breadcrumb, WidthSpec, theme override, animate_bool, on_hover, gauge family, named_focus, gutter highlights. - `examples/v020_regression_panel.rs` — visual regression check combining overlay_anchor, modal+tab_trap, chart, sparkline, table, scrollable_with_gutter, error_boundary, keymap_help_overlay. Migration recipes recorded in CHANGELOG `Unreleased > Breaking`. All existing v0.19 tests + v0.20 tests pass; 11 new tests cover the builder APIs, deprecation shims, drop-vs-show ergonomics, and f64 precision. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds scripts/ghostty_demos.sh: an interactive launcher that discovers v020_*.rs examples, lets the user pick (numbered menu, name list, or 'all'), and opens each chosen demo in a fresh Ghostty.app window via `open -na Ghostty.app -e`. Falls back to Terminal.app via osascript when Ghostty isn't installed. Flags: --showcase launches v020_showcase + v020_regression_panel --features launches the 17 individual feature demos --build-first pre-builds all examples so windows skip cargo logs --list / --help utility flags all launches every v020_*.rs found on disk Also adds a "v0.20 Demo Catalog" subsection to README.md under the existing Representative Examples table, mapping each demo to its issue cluster and listing the launcher's most common invocations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…emplate Apply the v0.20 demo consistency standard from docs/API_DESIGN.md to: - v020_use_state_keyed (#215) - v020_use_effect (#216) - v020_named_focus (#217) - v020_theme_subtree (#226) Each demo now follows the same shape: - Header: feature line, Demonstrates: #issue, Run cmd, Keys block, Layout art - Imports: single alphabetized use block - main(): minimal, delegates to render() - pub fn render(): public so snapshot tests can pin a frame - Spacing through ui.spacing().{xs,sm,md}() — no magic numbers - DemoState newtype + handle_input split where the demo has mutable state - Comments explain WHY (borrow ordering, dep semantics, theme scope), not WHAT No behaviour change: existing v020_hooks_demos and v020_theme_modal_demos parity tests still pass unchanged because they render their own fragments and don't import the demos' render() function. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apply the v0.20.0 art-level consistency standard to the five widget demos covered by the post-consistency-pass APIs (#212, #213, #223, #224, #235): - Standardized doc-header (purpose / issues / run / keys / layout) - Alphabetized imports, KeyModifiers/RunConfig from `slt` - Minimal `main()` delegating to `pub fn render(ui, &mut state)` so the same render function powers both the live binary and snapshot tests - `theme.spacing.{xs,sm,md}()` via `ui.spacing()` instead of magic 1/2 ints - Builder API verified for the five widgets: gauge: ui.gauge(ratio).label().width() line_gauge: ui.line_gauge(ratio).label().width().filled() breadcrumb: ui.breadcrumb(&[…]).separator().color().show() scrollable_with_gutter: GutterOpts::line_numbers(total, viewport) progress / spinner: Response captured for hover wiring - Migrated `HighlightRange::single` → `HighlightRange::line` (canonical name) - Ratios are `f64` end-to-end; `SplitPaneResponse::ratio` widened at the formatting boundary via `f64::from` - Snapshot tests in `tests/v020_widgets_demos.rs` continue to pass Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrites the five v0.20.0 demos owned by the lib + modal scope to a
uniform header / template / spacing standard so reviewers can scan all of
them with the same eye:
- examples/v020_static_log.rs (74 -> 80 lines)
- examples/v020_keymap_help.rs (102 -> 105 lines)
- examples/v020_ctrl_c_passthrough.rs (81 -> 80 lines)
- examples/v020_modal_trap.rs (88 -> 146 lines)
- examples/v020_spacing_scale.rs (53 -> 84 lines)
Standard applied:
- Header: feature one-liner + Demonstrates: #issue + Run: + Keys: +
optional Layout: ASCII art
- Single alphabetised `use slt::{...}` import
- main() minimal — delegates to a shared body() and pub fn render(...)
- theme.spacing.{xs,sm,md}() for paddings; no magic numbers in p()/gap()
- Comments explain WHY (constants, raw_key_code rationale, snapshot
fixture intent), not WHAT
- No commented-out code, no unwrap() outside main()
Behaviour preserved: pub fn render(ui: &mut Context) signatures
unchanged, so tests/v020_lib_demos.rs snapshot pins continue to match
byte-for-byte and tests/v020_theme_modal_demos.rs assertions still hold.
Verification (per task scope):
- cargo check on the five examples
- cargo clippy --example ... -- -D warnings
- cargo fmt -- --check
- cargo test --test v020_lib_demos (3 snapshots green)
- cargo test --test v020_theme_modal_demos (5 assertions green)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per Reviewer D's audit, several v0.20.0 acceptance criteria were advertised but lacked direct tests. This commit adds the missing coverage: #206 — kitty_flush_resize_reemits: verify InlineTerminal-style row_offset changes invalidate the manager's fast-path and re-emit placements. #220 — ContainerStyle const-fn shorthand: confirm `ContainerStyle::new() .grow(1)` is const-evaluable and produces output identical to `ContainerBuilder::fill()`. (NOTE: a `ContainerStyle::fill()` const-fn shorthand is not yet exposed; documented in test.) #223 — split_pane mouse drag: drive MouseDown + MouseDrag through the widget's prev-frame hit-map and assert state.dragging + ratio update. Complements the keyboard-only test that already shipped. #233 — static_log full-screen discard: drain via Context::take_static_log and verify the buffer does not leak across frames. The runtime- layer `discard_static_log` cannot be reached from `tests/` without exposing a helper; gap documented in test. #236 — WidgetKeyHelp user impl: verify a USER `WidgetKeyHelp` impl round-trips through Context::publish_keymap into the registry, and that `keymap_help_overlay(true)` renders the user's bindings. #204 — error_boundary buffer rollback: verify a panic inside a nested `group()` does not leave group_stack/text_color_stack/ deferred_draws/pending_tooltips in an inconsistent state, by rendering siblings after the boundary and a clean follow-up frame (kernel debug_asserts catch any leak). All 4 files compile clean with `cargo clippy --tests --all-features -- -D warnings`. Pre-existing clippy errors in `examples/demo_infoviz.rs` and a const-block warning in `src/style/theme.rs` (visible in the all-features clippy run) predate this branch and are out of scope. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two v0.20.1-deferred perf optimizations absorbed into v0.20.0. ## Fix 1: `use_state_keyed` double-clone (Reviewer A #6) `use_state_keyed` allocated TWO Strings per call instead of the one its docstring promised: 1. `let key: String = id.into();` — first allocation 2. `entry(key.clone())` — second clone for the HashMap entry Switched to a `contains_key(&str)` lookup followed by an insert only on the first-encounter path. The hot path now allocates exactly the `id.into()` String (which is identity / no-op when the caller already passes a `String`), eliminating the per-call extra clone. Updated the docstring to reflect the new actual behavior (zero string allocations beyond the call-site `Into<String>` conversion on the hot path). ## Fix 2: `consume_split_pane_keys` per-event match hoist (Reviewer A #7) The arrow-key consumption loop was computing four `matches!` arms per pending key event even though `orientation` is invariant across the loop. Hoisted the orientation match outside the loop and switched to a `(neg, pos)` tuple of `KeyCode`s, so the per-event work shrinks to two equality compares. Also switched `delta != 0.0` to `delta.abs() > f64::EPSILON` for clarity (noted by Reviewer A as a nit). Behavior is unchanged for the realistic input range — `delta` is a sum of exact 0.05 increments, so any non-zero result is well above EPSILON. ## Tests Added two alloc-budget tests using the existing global counting allocator infrastructure in `tests/v020_perf_alloc.rs`: - `use_state_keyed_allocates_one_string_per_call`: asserts the marginal cost of 100 cache-hit calls stays at <= 1.5 N alloc delta over the baseline frame. Pre-fix would be >= 2 N. - `use_state_keyed_cache_hit_scales_one_per_call`: asserts the marginal cost of going from 10 to 100 calls stays at <= 1.5 * 90. Pre-fix would be >= 2 * 90. The existing `issue_223_split_pane_arrow_keys_adjust_ratio_when_focused` test continues to pass byte-for-byte. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts: # tests/v020_perf_alloc.rs
User reported clicks did not reach widgets in spacing_scale, theme_subtree, etc. — those demos used `slt::run(...)` which defaults to mouse=false. Switch to `run_with` with `.mouse(true)`. Also replace U+2014 EM DASH in titles with ASCII colon to avoid wide-char cell-width drift in bordered() box-drawing.
Sweep the five v0.20 widget demos for the interactions they advertise and align the exit-key policy with the macOS Ctrl-C constraint (copy is bound to Ctrl-C on most macOS terminals, so the previous `Ctrl-Q | Esc` minimum is not enough — `q`, `Esc`, and `Ctrl-Q` all quit now). Per-demo changes - v020_split_pane: doc-comment now lists mouse drag explicitly; status row also surfaces `state.split.dragging` so users can see the Down/Up flag flip in real time alongside `drag_active`. - v020_gauge: exit-key policy + clippy fix (drop `&format!` on the `.label` arg). Add three static rows (25%, 65%, 90%) so all three color tiers — success / warning / error — are visible simultaneously regardless of the animated values. - v020_breadcrumb_response: exit-key policy. `r` now resets the path back to the full chain so the demo isn't a one-shot after the user clicks "Home" (which left them stuck on a single non-clickable bold segment). Status row shows depth (`current+1 / total`). - v020_progress_response: exit-key policy. `Space` toggles pause; ←/→ nudges the ratio by 5% (auto-pauses on nudge). Adds a static-variants block (0%/25%/50%/75%/100%) so the API surface is visible without waiting for the animation to traverse it. - v020_gutter_highlights: exit-key policy. **Bugfix**: `N` (Shift+n) -> `p` for previous-highlight navigation, matching the `n`/`p` convention the user spec calls for and the convention used elsewhere in v0.20. Doc-comment "Keys:" block standardized across all five files to `q / Esc / Ctrl-Q — quit`. All `tests/v020_widgets_demos.rs` snapshots still pass; `cargo check --examples --all-features` and `cargo clippy` on the five demos are clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Functional audit of the three v0.20 theme/modal demos. All required interactions (theme override, density step, modal focus trap) verified against TestBackend snapshots and existing #225/#226/#227 unit tests. Standardised the exit-key policy across all three demos to: if ui.key('q') || ui.key_code(KeyCode::Esc) || ui.key_mod('q', KeyModifiers::CONTROL) instead of relying on Ctrl-C (which macOS Terminal binds to copy by default). The doc-comment "Keys:" block in each file is updated to match. Plain `q`, `Esc`, and Ctrl-Q now all quit cleanly. For modal_trap specifically: - Added an `M` key binding that opens the modal (keyboard-accessible alternative to clicking the "Open modal" button). - Quit branch is gated on `!state.show_modal` so Esc inside the modal dismisses it instead of quitting the app — `key_code()` is already blocked by the modal/overlay guard inside the event helpers, but the explicit `!show_modal` check is belt-and-suspenders. The Esc-to-dismiss-modal path still uses `raw_key_code(KeyCode::Esc)` because that bypasses the focus-filter guard so Esc works even when a modal button has focus. No conflict with the outside-modal quit branch. Verified: - cargo check --example v020_{modal_trap,theme_subtree,spacing_scale} - cargo clippy --example v020_{modal_trap,theme_subtree,spacing_scale} -- -D warnings - cargo test --test v020_theme_modal --test v020_theme_modal_demos - cargo fmt --check on the three files Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…endly redesign
Functional audit of the six v0.20 lib/utils demos. Two are stdout-only
and already exit cleanly (perf_audit, test_utils — no changes). The four
interactive demos (static_log, keymap_help, ctrl_c_passthrough, widthspec)
all needed the macOS-Ctrl-C-as-copy fix in their exit-key policy; the
keymap_help overlay had a real interaction bug; and ctrl_c_passthrough
itself was untestable on macOS as written.
Per-demo summary
- v020_static_log: standardise quit to `q / Esc / Ctrl-Q` (was `q / Esc`),
document the macOS Ctrl-C-as-copy gotcha in the doc-comment, update the
inline status hint and the snapshot.
- v020_keymap_help: **bugfix**: pressing `?` opens the help overlay, but
the overlay is a modal — so on the next frame the regular `key('?')` /
`key_code(Esc)` checks are blocked by the modal/overlay guard, leaving
the user trapped. Switch the toggle/close handlers to `raw_key_mod` /
`raw_key_code` so '?' and Esc keep working while the overlay is up.
Also adopt the standard `q / Ctrl-Q` quit policy and document the
modal-guard rationale in the doc-comment.
- v020_ctrl_c_passthrough: **redesign**. macOS terminals (Ghostty,
iTerm2, Terminal.app) bind Ctrl-C to Copy by default — the keystroke
never reaches the app, making the original "press Ctrl-C three times"
demo untestable on stock macOS. Keep the headline API showcase
(`RunConfig::handle_ctrl_c(false)`), but add three input sources that
all funnel through the same strike counter:
1. Real Ctrl-C — works on Linux/Windows and macOS terminals where
the user has unbound Copy.
2. Ctrl-G — convention is that Ctrl-G is not bound to any clipboard
command, so it always reaches the closure as a plain Char('g')
+ CONTROL key event. macOS-friendly fallback.
3. "Send Ctrl+C" button — synthesises the same state transition a
real Ctrl-C would produce, so the demo is testable even when
both Ctrl-C and Ctrl-G are intercepted.
Doc-comment now spells out the macOS gotcha and explains that the API
contract (`handle_ctrl_c(false)`) is independent of whether the
terminal lets the keystroke through. Yellow banner inside the running
UI surfaces the same warning. Quit policy: `q / Esc / Ctrl-Q`.
Snapshot updated to match the new layout.
- v020_widthspec: replace the `q / Ctrl-C / Esc` exit-key policy with
the standard `q / Esc / Ctrl-Q`. Update the in-frame quit hint and
document the macOS-Ctrl-C gotcha in the doc-comment.
Verified
- cargo fmt -- --check
- cargo check --examples --all-features
- cargo clippy --example v020_{static_log,keymap_help,ctrl_c_passthrough,widthspec,perf_audit,test_utils} -- -D warnings
- cargo test --tests --all-features (all suites pass, including the
v020_lib_demos snapshot tests with the two updated snapshots)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit finding: every v0.20 hooks demo had at least one interaction in its `Keys:` doc-comment that did not actually function in the running program. This commit closes those gaps and standardises the quit policy across all three demos so a macOS user (where Ctrl-C is bound to copy) is never trapped in the demo. v020_named_focus (#217) - Wrap each input row in a clickable container so clicking the row (not just the [Focus N] button) routes through `focus_by_name`. Native text_input does not claim focus on click, so without this wrapping the mouse experience contradicted the doc-comment. - Add 1/2/3 numeric shortcuts as keyboard analogues of the [Focus N] buttons, so the headline `focus_by_name` feature is reachable from the keyboard alone. - Numeric shortcuts and plain `q` are checked AFTER the body renders so a focused text_input consumes typed digits/'q' first; Esc and Ctrl-Q still quit unconditionally before render. - Update Keys: to reflect the new bindings. v020_use_state_keyed (#215) - Add per-row [-] / [+] buttons that bump/drop that row's counter independently of the global cursor — concretely demonstrates that every keyed-state slot is addressable, not just the selected one. - Standardise quit to `q || Esc || Ctrl-Q`. - Update Keys: + help line. v020_use_effect (#216) - Standardise quit to `q || Esc || Ctrl-Q`. - Update Keys: + help line. (Logic itself was already correct.) Quit-key standardisation - Drop `key_mod('c', CONTROL)` which collides with the macOS copy binding. All three demos now offer `q`, `Esc`, and `Ctrl-Q` as redundant exits. Verification on this branch: - cargo check --examples --all-features: pass - cargo clippy on the three target demos: pass (-D warnings) - cargo fmt -- --check: clean - tests/v020_hooks_demos.rs: 5/5 pass - tests/v020_hooks_focus.rs: 24/24 pass - cargo test --tests --all-features (serial): all green
Functional audit of the three v0.20 integration demos (showcase,
regression_panel, dx_shortcuts). All required interactions (hover, click,
modal toggle, help overlay, gutter highlight nav, text input typing) are
now reachable from a real keyboard/mouse session.
Standardised the exit-key policy across all three demos to:
if ui.key('q') || ui.key_code(KeyCode::Esc) || ui.key_mod('q', KeyModifiers::CONTROL)
instead of relying on Ctrl-C, which macOS Terminal and iTerm2 bind to copy
by default. Each demo's doc-comment "Keys:" block now mentions the macOS
gotcha explicitly.
Per-demo summary
- v020_dx_shortcuts: standardise exit policy to q/Esc/Ctrl-Q, refresh the
inline status hint, regenerate the snapshot. Hover-tooltips on Save/Open
buttons and the centered help overlay (?) were already wired correctly
and verified through render path.
- v020_regression_panel: same exit-key fix, plus a real bugfix —
- When the modal opens, `key('m')`/`key('?')`/`key_code(Esc)` are blocked
by the modal/overlay guard inside the event helpers, so on the next
frame the user could not close the modal or toggle the help overlay
with the same key that opened it. Switched the close path to
`raw_key_code` (which bypasses the modal guard) so M/?/Esc actually
dismiss the overlay they opened.
- Quit branch is gated on `!any_overlay` so Esc inside the modal/help
dismisses it instead of quitting the app.
- n/p highlight nav is also gated on `!any_overlay` so it doesn't fire
while the user is reading the help.
- Doc-comment "Keys:" block reformatted to match the standard table-style
used by the other v0.20 demos.
- v020_showcase: same exit-key fix, plus a UX correction —
- Render now runs BEFORE the global key handlers so the focused
text_input (Name/Email) gets first crack at the keystream. Without
this, typing 'q'/'space'/'n'/'p' in a text input would also fire the
global quit/toggle/highlight handlers — bare `q` would quit while the
user was typing.
- Footer text and ASCII layout updated to match the new key list. The
misleading "? help" entry (no `?` handler existed in the showcase)
was removed.
- Doc-comment "Keys:" block expanded to call out every advertised
interaction (Tab cycle, click breadcrumb, hover Save/Cancel/"Hover me",
Space toggle, n/p nav, q/Esc/Ctrl-Q quit) plus a note that bare `q`
quits ONLY when no text input has focus.
Snapshot
- tests/snapshots/visual__v020_dx_shortcuts.snap: regenerated for the
refreshed inline status hint ("Ctrl-Q to quit." → "q / Esc / Ctrl-Q
to quit.").
Gates
- cargo fmt -- --check ✓
- cargo check --examples --all-features ✓
- cargo clippy on the three touched examples ✓
- cargo test --tests --all-features ✓ (pre-existing rust-1.95 clippy
warnings on perf_regression.rs / demo_infoviz.rs / theme.rs are NOT in
this audit's scope and were not introduced by this commit.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts: # examples/v020_named_focus.rs # examples/v020_use_effect.rs # examples/v020_use_state_keyed.rs
…e + demo §2 + 6 tours) Cumulative integration on top of the 56 commits already on release/v0.20.0: - F1 follow-up (#217): register_focusable_named eager-allocate-with-reuse pattern. The previous deferred-attach pattern made standalone calls a silent no-op (5 unit tests failed); now both shapes work — "name + widget" and "name alone" — and `register_focusable()` reuses the reserved slot when called immediately after. 24/24 v020_hooks_focus tests pass. - Chart label ellipsis truncation: shared `truncate_label(text, max_cols)` helper used by chart legend and treemap. Replaces bare-truncated forms ("Memor", "TypeS") with ellipsis ("Memo…", "Type…") or drops the label when budget < 3 cells. - examples/demo.rs §2 fix: lifted 66 per-frame `let mut state = ...` into a pub `DemoState` owned by `main()`. Tab clicks now persist instead of flashing for ~0.1s before snapping back to the first tab. - §2 sweep on 4 v020 demos (keymap_help / gutter_highlights / ctrl_c_passthrough / dx_shortcuts): each now exposes `pub fn render(ui, &mut state)` plus a snapshot variant for tests. - 6 new tour binaries (v020/cookbook/showcase/canvas/text/system_tour) with scrollable wrap so tall tabs stay reachable on small terminals. - cookbook_dashboard buffer prefill — frame-0 renders the full sine waveform instead of an empty chart and three flat sparklines. - autoexamples = false + 16 explicit [[example]] entries (53 → 16 examples). Source files for the demos that compose into tours stay in examples/ but are reached via #[path = ...] mod includes. - 5 new docs: DESIGN_PRINCIPLES.md, ARCHITECTURE.md, NAMING.md, RUSTDOC_GUIDE.md, DEMO_GUIDE.md. - scripts/api_audit.sh — V1–V7 lint heuristics (Two-paths, Mixed verbs, Length info, Missing rustdoc, Outer grow missing, Fallback divergence, Wide-char title drift). - typos whitelist: Pytho / Memor (intentional bare-truncation test prefixes used in tests/widgets.rs). - Bump to 0.20.0: Cargo.toml + crates/slt-wasm/Cargo.toml + README + CHANGELOG ([Unreleased] → [0.20.0] - 2026-04-29). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… cycle) (#98) Add `FilePickerState::selected_file()` that returns `Option<&PathBuf>` — the resolved file path the user picked. Mark the existing `selected()` method `#[deprecated(since = "0.20.0")]` and delegate it to the new name. `FilePickerState` had two members spelled `selected` — `pub selected: usize` (entry index) and `pub fn selected() -> Option<&PathBuf>` (resolved file). The identical names returned different types, which is legal Rust but confusing. All other state types use `selected_item()`, `selected_label()`, or `selected_row()` — `selected()` was the only outlier. Update the in-crate caller (`examples/demo.rs`) to use `selected_file()`. Tests already access the `selected_file` field directly so no churn there. The `selected()` method stays callable until v1.0 with a compiler warning that points at the new name. Closes #98 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a snapshot-based undo/redo history stack to `TextareaState`. The widget
pushes a `TextareaSnapshot { lines, cursor_row, cursor_col }` before every
destructive mutation (char insert, delete, Enter, Backspace, paste). Rapid
character typing coalesces into a single undoable batch — only the first
char of a typing burst pushes a snapshot, tracked via `last_was_char_insert`.
A paste is one snapshot regardless of length.
`Ctrl+Z` (`undo()`) walks `history_index` backward and restores the snapshot
content; the first undo after a typing burst captures the live tip so the
redo round-trip is symmetric. `Ctrl+Y` (`redo()`) walks the index forward
when not at the tip. Any new edit after an undo truncates the redo branch.
History is capped at `history_max` (default 100) by evicting the oldest
snapshot via `Vec::remove(0)` — the cap is small enough that the O(n)
shift is bounded.
Pure additive — no public field changed type or visibility. Adds two
read-only getters (`history_len`, `history_cap`) so integration tests can
observe the cap, and a builder setter (`history_max`) to override it.
Tests in `tests/textarea_undo.rs`:
- `textarea_undo_redo_roundtrip` — type "hi", Ctrl+Z → empty, Ctrl+Y → "hi"
- `textarea_rapid_typing_coalesces_into_one_undo` — 5 chars, one Ctrl+Z clears
- `textarea_undo_past_beginning_is_noop` — three Ctrl+Z on empty state
- `textarea_history_capped_at_max` — 20 edits with cap=4 stays bounded
- `textarea_redo_invalidated_by_new_edit` — fresh edit drops the redo tail
Closes #102
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…drag handling (#184) Returns Response::none() for now — establishes the API extension point so adding click-to-scroll or drag handling later does not require a further breaking change. All in-crate callers ignore the return value, so this is compile-compatible at every existing call site. Closes #184 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#149) Add `#[doc(hidden)]` to `ContainerBuilder::scroll_offset` (Option B from the issue). The method remains `pub` so existing callers and cargo-semver-checks tracking are preserved, but it is removed from the public rustdoc surface where it does not belong. Background: v0.19.1 reverted an earlier `pub(crate)` tightening because it was a breaking change in a patch release (commit 5788dca). v0.20.0 is a minor release where breaking is allowed, but the issue prefers the `#[doc(hidden)]` interim — promote to `pub(crate)` only at v1.0 — so semver tracking remains intact and backwards compatibility is unconditional. The accompanying compile-time test `scroll_offset_is_crate_internal_api` is updated to describe the new `#[doc(hidden)] pub` arrangement instead of the previous `pub(crate)` claim. Verification: - `cargo fmt -- --check` clean - `cargo check --all-features` clean - `cargo clippy --all-features -- -D warnings` clean - `cargo test --all-features --lib` 388 passed - `bash scripts/api_audit.sh` 0 violations Closes #149 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hoist `line_segs` out of the per-line `while` loop in `wrap_segments`. Previously each iteration paid `Vec::with_capacity(segments.len().min(16))` once per output line; now the buffer is allocated once per call and reused via `std::mem::replace` after each line is moved into `lines`. The empty buffer left behind keeps the same `scratch_hint` capacity so the first push on the next line still skips the grow-from-zero path. Net effect on the alloc test: `wrap_segments_alloc_count_low_via_bench_helper` stays well within its 25_000-call budget (12 perf-alloc tests pass). The behavior is byte-identical to the prior implementation — only the allocation lifecycle changes — and the wrap_segments_* unit tests continue to validate output equality. Closes #157 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`virtual_list` previously derived `start = selected - vh + 1`, which pinned the cursor to the bottom row of the viewport at all times. Pressing `Up` shifted the entire viewport with the cursor, so the cursor never moved relative to the visible window — the opposite of how every other TUI library behaves. This change adds a `viewport_offset: usize` field to `ListState` (default `0`, `pub(crate)`) and switches `virtual_list` to a sticky-viewport pattern: `viewport_offset` is only adjusted when `selected` would otherwise leave the visible range. `Default` and `new()` initialize it to `0`, so existing callers see no behavior change for first-frame rendering. Patch-safe: the new field is `pub(crate)`, no public surface changed. Closes #192 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#193) Calendar `h`/`l` previously mapped to `prev_month`/`next_month`, which contradicted vim convention where `h`/`l` move the cursor by one unit. Vim users pressing `h` expected to move back one day inside the current month and instead jumped a whole month back. Remap so the calendar behaves like every other vim-flavored widget: - `h` and `l` — cursor by ±1 day (same as `Left`/`Right` arrows) - `[` and `]` — previous/next month (was `h`/`l`) - `Up`/`Down` — ±7 days (unchanged) - `Enter`/`Space` — select cursor day (unchanged) Mouse navigation on the title row is unchanged. Mouse hit-testing for `[`/`]` is not added — the title-row arrows already provide a mouse path for month navigation. Migration: workflows that pressed `h`/`l` to jump months must switch to `[`/`]`. The function signature is unchanged; this is a keybinding-meaning change only and is treated as a behavior bug fix under semver patch tradition. The widget rustdoc now enumerates the full keybinding table. Closes #193 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`build_tree` previously took `commands: Vec<Command>` and consumed it via `into_iter()`, dropping the buffer at the end of every frame. Combined with the `commands_buf` field on `FrameState` introduced earlier in the v0.20.0 perf wave, this meant the `mem::take` from `state.commands_buf` in `Context::new` only ever swapped in an empty `Default` Vec, and the empty shell that landed back in `state.commands_buf` lacked the capacity used by the previous frame — so the per-frame log2(N) Vec growth churn (8 reallocs at ~200 commands/frame, ~480 reallocs/sec at 60fps) was never actually amortized. Switch `build_tree` to `&mut Vec<Command>` and `commands.drain(..)` so the caller retains ownership of the allocation. After the drain, `len == 0` but `capacity` is preserved. `run_frame_kernel` now reclaims the drained Vec into `state.commands_buf` at frame end (mirroring the #204 reclamation pattern) on both the normal and quit paths so `TestBackend`-style reuse also benefits. Verified impact (serial measurement to avoid global-allocator-counter contamination from concurrent tests): - `framestate_reuse_steady_state_alloc_count_low` 100-frame total drops 1300 → 1100 allocations (-15%). - New regression test `build_tree_drains_in_place_preserving_capacity` pins the contract: post-`build_tree`, `len == 0` and `capacity` is preserved, and a second `build_tree` call within prior capacity does not realloc. Closes #150 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `ContainerBuilder::shrink()` — opt-in CSS-style proportional shrink. Default behavior unchanged: containers without `.shrink()` keep their historic overflow-by-design size. Children with `.shrink()` participate in a `min(available, shrink_total) / fixed_width` scaling pass when the parent row/column overflows. Implementation - New `Command::ShrinkMarker` variant — pushed by `ContainerBuilder .finish()` just before `BeginContainer` / `BeginScrollable` when `shrink_flag == true`. Mirrors the existing `FocusMarker` / `InteractionMarker` pattern, so widget-internal `BeginContainerArgs` constructions across the codebase are untouched. - `LayoutNode::shrink: bool` field (default `false` in every constructor). Set by `build_children` when a `ShrinkMarker` is buffered into `pending_shrink`. - `layout_row` / `layout_column` apply `floor(min_widths.get(i) * scale)` to children where `child.shrink && child.grow == 0` when `fixed_width > available`. Grow children consume leftover and ignore the flag. Non-shrink children keep their natural width / height. Tests in `src/layout/tests.rs` — three regression tests pinning the spec contract: - `flex_shrink_default_off_preserves_overflow` — no `.shrink()` → both children render at width 20 in a 30-cell row (historic overflow). - `flex_shrink_all_children_proportional_distribution` — both children flagged → each scales to `floor(20 * 0.75) = 15`, total 30 (exact fit). - `flex_shrink_mixed_only_flagged_scale` — only first flagged → shrinker becomes 10, non-shrinker keeps 20. Closes #161
Static-screen flushes (0% dirty) on a 200×60 buffer dropped from ~125 µs to ~130 ns (≈1000× speedup) by skipping the per-cell scan on rows that match the previous frame. Bench gate before implementing: `flush/static_200x60` measured 113-138 µs on 200×60 — well above the 50 µs GO threshold from the issue spec. Added the new bench so the gate is reproducible from `cargo bench`. How it works - `Buffer` gains `line_hashes: Vec<u64>` and `line_dirty: Vec<bool>`, both sized to `area.height`. Cell-write paths (`set_string_inner`, `set_char`) flag the touched row dirty. - `recompute_line_hashes` rehashes every dirty row and clears the flag. Hashing uses `std::collections::hash_map::DefaultHasher` — no new dependency, sufficient quality for equality detection. - `flush_buffer_diff` short-circuits a row when both `current.row_clean(y)` AND `current.row_hash(y) == previous.row_hash(y)`. Either failure falls through to the legacy per-cell scan, so the skip is a pure short-circuit without any correctness change. - `Terminal::flush` and `InlineTerminal::flush` call `recompute_line_hashes` on both buffers before invoking `flush_buffer_diff`, matching the bench's mutable entry point. Bench results (200×60, M-series Apple Silicon) - `flush/static_200x60` 113-138 µs → 130-136 ns (~1000× faster) - `flush/sparse_change` 158-162 µs → 151-153 µs (-2.3% in noise) - `flush/full_redraw` 293-385 µs → 284-294 µs (no regression) Tests - 8 new unit tests in `src/buffer.rs` covering dirty-flag lifecycle, hash determinism, style-sensitivity, resize sync. - 2 new flush regression tests in `src/terminal.rs` validating zero output for fully-matching frames and selective per-row skip in mixed diffs. - All 12 perf-alloc regression tests pass (`framestate_reuse`, `kitty_placement_flush_*`, `use_state_keyed_*`). Closes #171 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Issue #201's earlier wave shipped the `DebugLayer` enum and the `Context::debug_layer()` / `Context::set_debug_layer(layer)` API, plus the render-side fix that walks `node.overlays` from F12. This patch closes the remaining gaps identified in the spec: 1. **Shift+F12 cycles the active layer** — previously F12 only flipped the on/off toggle and there was no keyboard path to scope the outline to a single layer at runtime. * Plain F12 keeps the legacy on/off toggle (modifier-strict — `KeyModifiers::NONE`). * Shift+F12 cycles `All → TopMost → BaseOnly → All` without touching the on/off flag. * The two arms guard against double-firing on the same press by matching modifiers explicitly (the previous `..` pattern would have fired both branches). 2. **Per-variant docstrings on `DebugLayer`** — each variant explains what's outlined and when to pick it, matching `docs/RUSTDOC_GUIDE.md`. 3. **`# Example` blocks** — added to `DebugLayer`, `Context::debug_layer`, and `Context::set_debug_layer` per the rustdoc guide. Each example compiles via `slt::run`. Implementation - Extracted `process_run_loop_event` from `poll_events` (was a nested fn) so the keybinding behavior is unit-testable without standing up a real crossterm event source. Marked `pub(crate)` — no public surface change. - Added 4 unit tests in `run_loop_tests` covering: plain-F12 toggle, Shift+F12 cycle, Shift+F12 does not toggle overlay, plain-F12 does not cycle layer. No breaking changes — `DebugLayer::All` remains the default, and the existing F12 toggle behavior is preserved bit-for-bit when no modifier is held. Closes #201 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surfaced by extending the release gate from `clippy --all-features` (lib only) to `clippy --all-features --lib --tests --examples -- -D warnings`. None of these alters runtime behavior: - `examples/demo.rs:1463` — `let _ = ui.scrollbar(scroll)` after the v0.20 signature change `() -> Response` (#184) makes the call use the response explicitly. Other call sites already ignore via statement form. - `examples/v020_{ctrl_c_passthrough,dx_shortcuts,keymap_help}.rs` — replace 3 hand-written `impl Default for DemoState` blocks with `#[derive(Default)]` (clippy::derivable_impls). The hand-written impls returned exactly `Default::default()` for every field, so the derive is byte-equivalent. - `src/style/theme.rs:1151` — wrap `assert!(!_CONST_LIGHT.is_dark)` in a `const { … }` block (clippy::assertions_on_constants). The const-evaluation regression test still pins the same property; the const block now also surfaces failure at compile time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…closed-already + v0.21 deferrals - ### Fixed expanded with the 11 newly-applied fixes (#149, #150, #157, #161, #171, #184, #192, #193, #201, plus #98/#102 already in HEAD). - ### Closed (verified already applied in v0.19.3) lists the 8 v0.19.x issues whose fix landed in PR #202 (#134/#146/#147/#148/#152/#153/#155/#162). No separate v0.20 commit was needed; PR body will tell GitHub to close these so the v0.19.x milestone count goes to zero. - ### Known v0.21 migration notes documents the 4 critical drifts the design-discipline audit surfaced that genuinely need a breaking change to resolve: Hook-family naming asymmetry, scrollbar/separator return shape, status-family fake Response::none(), ScrollState::progress f32 outlier. Plus a brief note on the patch-safe doc-only gaps deferred to a 0.20.x follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eaningful `tests/v020_perf_alloc.rs` uses a global `MEASURING: AtomicBool` flag plus a counting global allocator. CI runners have higher test parallelism than a local laptop, so other parallel tests' allocations contaminate the counter during the measurement window — the test then trips its 1500-call budget even though the actual single-thread count is ~1100 (-15% vs the v0.19.3 baseline of ~1300, post-#150 drain). The honest fix is single-threaded execution at the workflow level. Adds ~10s to the CI test step but keeps the perf budgets catching real regressions instead of being inflated to absorb the noise. Same change applied to `release.yml` so the release gate enforces the same execution model. A v0.21 follow-up tracking issue: switch the perf-alloc test infrastructure to thread-local counters or `serial_test` so the workflow flag is no longer required. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
v0.20.0 minor release. Single big bump that wraps:
DESIGN_PRINCIPLES.md,ARCHITECTURE.md,NAMING.md,RUSTDOC_GUIDE.md,DEMO_GUIDE.md) andscripts/api_audit.sh(V1–V7 lints).autoexamples = false+ 16 explicit[[example]]entries (53 → 16 cargo example targets); the rest reach users viamodincludes inside 6 new tour binaries (v020_tour,cookbook_tour,showcase_tour,canvas_tour,text_tour,system_tour).CHANGELOG covers everything in detail. This summary is the short version.
Issues closed by this PR
v0.20.0 milestone (29 OPEN → CLOSED + 5 already-CLOSED transitively)
Closes #204
Closes #205
Closes #206
Closes #208
Closes #209
Closes #210
Closes #212
Closes #213
Closes #215
Closes #216
Closes #217
Closes #218
Closes #220
Closes #221
Closes #223
Closes #224
Closes #225
Closes #226
Closes #227
Closes #228
Closes #229
Closes #230
Closes #231
Closes #232
Closes #233
Closes #235
Closes #236
Closes #237
Closes #238
v0.19.x — patch-safe newly applied this release
Closes #98
Closes #102
Closes #149
Closes #150
Closes #157
Closes #161
Closes #171
Closes #184
Closes #192
Closes #193
Closes #201
v0.19.x — verified already applied in PR #202 (v0.19.3)
Closes #134
Closes #146
Closes #147
Closes #148
Closes #152
Closes #153
Closes #155
Closes #162
Breaking changes (semver minor allowed for v0.x)
gauge/line_gauge/breadcrumb/scrollable_with_gutterunified into chainable builders +GutterOpts. No deprecated shims (no callers in the wild).f32 → f64widening across the ratio surface (gauge / line_gauge / split_pane).Constraintsredesign withWidthSpec/HeightSpec(refactor(style): unified WidthSpec/HeightSpec — Constraints redesign #237 closes perf(style): shrink Constraints from 36 to 20 bytes via sentinel u32 #207, feat(style): add Constraints w_ratio / h_ratio for exact integer fractions #219).register_focusable_namedsemantics now eager-allocate-with-reuse instead of deferred-attach (the v0.20-preview behavior was a silent no-op for standalone calls; see Fixed section in CHANGELOG).Known v0.21 migration
The audit surfaced 4 critical drifts that need a breaking change to fully fix. Documented in CHANGELOG
### Known v0.21 migration notes:use_state_namedvsuse_state_named_withvsuse_state_keyedvsuse_state_keyed_default).scrollbar/separatorreturn shape (() / &mut Self→Response).statusfamilyResponse::none()→ realinteraction()-populated Response.ScrollState::progress() -> f32→f64.Patch-safe doc-only gaps from the audit (missing
# Example/# Panicssections) will land as a0.20.xfollow-up PR rather than block this release.Test plan
cargo fmt -- --checkcargo check --all-featurescargo clippy --all-features --lib --tests --examples -- -D warnings← extended from lib-onlycargo test --all-features(30+ test suites, ~600 tests)typoscargo check -p superlighttui --no-default-featurescargo check -p slt-wasm --target wasm32-unknown-unknowncargo hack check -p superlighttui --each-feature --no-dev-depscargo audit(1 allowed: proptest → rand 0.9.2 dev-only RUSTSEC-2026-0097)cargo deny checkbash scripts/api_audit.sh— 0 violationscargo run --example demo(tab persistence),--example showcase_tour(Infoviz chart label ellipsis), textarea undo/redo via Ctrl+Z/Y, virtual_list cursor mid-viewport, calendar h/l day-step🤖 Generated with Claude Code