turbo-tui — Change History
Project created: Borland Turbo Vision windowing patterns for Ratatui
Command system: CommandId (u16), CommandSet (bitfield), standard command constants
Standard commands: CM_QUIT, CM_OK, CM_CANCEL, CM_YES, CM_NO, CM_CLOSE, CM_SAVE, etc.
INTERNAL_COMMAND_BASE (1000) convention: commands >= 1000 don't close dialogs
4 tests passing
ADR-002 written in four-code documenting architecture decisions
Pattern reference: turbo-vision-4-rust (MIT licensed)
view.rs: View trait, ViewId (atomic counter), StateFlags, OptionFlags, Event system
group.rs: Container with Z-order management, three-phase event dispatch
frame.rs: Window borders with 3 frame types (Window, Dialog, Single), Ratatui rendering
window.rs: Overlapping windows with drag, resize, zoom toggle
desktop.rs: Window manager with tile, cascade, click-to-focus
dialog.rs: Modal dialogs with Escape/Enter handling, INTERNAL_COMMAND_BASE convention
menu_bar.rs: Horizontal menu bar with dropdown activation, X hotkeys, Alt+letter
menu_box.rs: Dropdown menu box with keyboard navigation
status_line.rs: Context-sensitive status bar with OF_PRE_PROCESS, clickable shortcuts
scrollbar.rs: Vertical/horizontal scrollbar with draggable thumb
button.rs: Clickable button with Space/Enter/mouse support
static_text.rs: Non-interactive text label (left-aligned or centered)
msgbox.rs: Pre-built message_box, confirm_box, confirm_cancel_box, error_box factories
157 tests passing, clippy pedantic clean, zero unsafe code
Container: Renamed Group → Container, split into submodules (container/mod.rs, dispatch.rs, draw.rs)
View trait: Added deferred event queue (Event.deferred: Vec<Event>, Event::post()), lifecycle hooks (on_insert, on_remove, on_resize)
Frame: New Smart Border with optional ScrollBar integration (no Box<dyn View>), hit-test methods
Window: Drag/resize state machine, zoom toggle, interior fill, SF_FOCUSED propagation to Frame
Desktop: Window manager with click-to-front, tile, cascade, background rendering
Overlay: OverlayManager with dismiss-on-escape/click, calculate_overlay_bounds() for overflow detection
Application: Central orchestrator — dispatch chain (Overlay→MenuBar→StatusLine→Desktop), deferred event processing, screen resize handling
Dialog: Modal window with Escape→CM_CANCEL, Enter→CM_OK, commands<1000 close dialog
msgbox.rs: Factory functions message_box(), confirm_box(), confirm_cancel_box(), error_box()
demo.rs: Interactive demo using Application struct, MenuBar, StatusLine, 3 windows with buttons
222 tests passing, clippy pedantic clean, zero unsafe code
18 source files, ~8,500+ lines
MenuBar→Overlay dropdown refactor (using Overlay system instead of self-draw) — planned for v0.2.1
HorizontalBar Unification (2026-03-22)
NEW src/horizontal_bar.rs: Unified HorizontalBar struct replacing separate MenuBar and StatusLine
BarEntry enum: Action (direct command) or Dropdown (opens menu box)
DropDirection: reuses overlay::DropDirection — Down for menu bar, Up for status bar
Supports mixed entries: menu bars can have direct actions, status bars can have dropdowns
Full keyboard (F10, Esc, arrows, Enter, Alt+letter, key codes) + mouse handling
26 tests
Refactored menu_bar.rs → thin backward-compat wrapper (1070→276 lines)
MenuItem, Menu types retained
pub type MenuBar = HorizontalBar alias
From<Menu> for BarEntry, menu_bar_from_menus() convenience constructor
Refactored status_line.rs → thin backward-compat wrapper (664→325 lines)
StatusItem, KB_* constants, key_matches() retained
pub type StatusLine = HorizontalBar alias
From<StatusItem> for BarEntry, status_line_from_items() convenience constructor
Updated application.rs and examples/demo.rs to use new API
Design doc: docs/DESIGN-horizontal-bar.md
255 tests passing, clippy pedantic clean, zero unsafe code
Theme Loading Report + Resize Grip Fix (2026-03-22)
NEW ThemeLoadReport struct in theme.rs — per-file success/error tracking for JSON theme loading
has_errors(), loaded_count(), error_summary() helpers
Display impl for formatted output
BREAKING load_themes_from_dir() now returns Result<ThemeLoadReport, io::Error> instead of usize
Directory read failures are propagated as Err (no longer silently swallowed)
Individual file parse errors collected in ThemeLoadReport::errors
Theme loading never fails silently anymore
FIX Resize grip character: changed default from '⋱' (U+22F1) to '◢' (U+25E2) everywhere
Built-in turbo_vision() theme
default_resize_grip_char() in theme_json.rs
Fallback in to_theme() conversion
All 6 JSON theme files already used '◢'
NEW Integration tests: test_load_dark_json_from_disk, test_load_all_theme_files_from_disk
Demo updated: panic! on theme load failures instead of silent fallback
280 tests passing, clippy pedantic clean
Scrollbar Fixes + Reference Analysis (2026-03-22)
Phase 1: Scrollbar inactive styling
3 new theme fields: scrollbar_track_inactive, scrollbar_thumb_inactive, scrollbar_arrows_inactive
ScrollBar::set_active(bool) / is_active() — controls active vs inactive rendering
Window::set_state() propagates SF_FOCUSED to frame scrollbars via set_active()
theme_json.rs: ScrollbarSection gets 3 #[serde(default)] inactive fields + Default impl for StyleValue
Phase 2: Scrollbar hover fix
Frame::update_scrollbar_hover(col, row) — forwards MouseMoved to scrollbars with correct bounds
Frame::clear_scrollbar_hover() — clears hover on all scrollbars
Frame::handle_scrollbar_click(col, row, event) -> bool — handles Down/Drag on scrollbars
Window::handle_event routes Moved, Down, Drag to frame scrollbars
Reference Analysis: Reviewed Ratatui Component Architecture, Event Handling, Widgets, Builder Lite, TachyonFX, tui-rs demo, gping
Architecture decision: View trait stays unified (NOT split into Widget + EventHandler)
Adopted: Builder Lite pattern for Window/FrameConfig construction
Saved to docs/RES-0002-reference-projects-architecture.md
Documentation: Created ARCHITECTURE.md, STANDARDS.md, TESTING.md, ROADMAP.md, PLAN-v0.2.1.md
284 tests passing, clippy pedantic clean
Scrollbar Inactive Fix + Task Bar Shelf (2026-03-22)
Scrollbar Inactive Fix: JSON theme files were missing track_inactive, thumb_inactive, arrows_inactive fields
Serde defaults hard-coded Turbo Vision colors (Blue background) — broke all non-TV themes
Added proper inactive scrollbar colors to all 6 JSON theme files matching each theme's palette
Fixed color typos: dark.json #3c3c3cOD → #3c3c3c, matrix.json #ff505 → #ff5050
Phase 3: Task Bar Shelf (PLAN-v0.2.1)
Desktop gets task_shelf_height: u16 — tracks shelf rows for minimized windows
Desktop::recalculate_shelf() — positions minimized windows left-to-right at desktop bottom
Shelf wraps to multiple rows if minimized windows overflow one row
Desktop::effective_area() — returns desktop bounds minus shelf rows
Window::minimize() simplified — no longer self-positions, Desktop manages shelf layout
Window::minimized_width() — public helper for shelf layout calculation
tile() and cascade() skip minimized windows, use effective_area()
recalculate_shelf() called after add_window, close_window, and all event handling
8 new tests: shelf empty, one minimized, multiple tiled, restore, close, tile/cascade skip, effective area
292 tests passing, clippy pedantic clean
Phase 4 + 5: Builder Lite + Lifecycle Hooks (2026-03-22)
Phase 4a: FrameConfig struct (src/frame.rs)
FrameConfig struct with frame_type, closeable, resizable, minimizable, maximizable, v_scrollbar, h_scrollbar
Named constructors: FrameConfig::window(), FrameConfig::dialog(), FrameConfig::panel()
Builder methods: with_v_scrollbar(), with_h_scrollbar(), with_closeable(), with_resizable(), with_minimizable(), with_maximizable()
Frame::from_config(bounds, title, config) — creates Frame from FrameConfig
Default impl returns window() config
Exported in prelude
Phase 4b: Window Builder Lite (src/window.rs)
Window::with_config(bounds, title, config) — constructor from FrameConfig
Self-consuming builder methods: with_min_size(), with_drag_limits(), with_scrollbars(), with_closeable(), with_resizable(), with_minimized_max_width()
Existing set_*() mutators retained for runtime changes
Phase 4c: Window Presets (src/window.rs)
Window::editor(bounds, title) — vertical scrollbar, min 20×8
Window::palette(bounds, title) — not resizable, not closeable
Window::tool(bounds, title) — compact min 10×5
Phase 5: View Lifecycle Hooks (src/view.rs, src/container/mod.rs)
on_focus(&mut self) — called when view receives focus (SF_FOCUSED set)
on_blur(&mut self) — called when view loses focus (SF_FOCUSED cleared)
Default implementations are no-ops
Container::set_focus_to() calls on_blur() on old child, on_focus() on new child
Phase 7: JSON Theme Files — already complete (inactive scrollbar fields added in previous session)
Demo updated to use Builder Lite, Window presets, and showcase scrollbar focus styling
313 tests passing, clippy pedantic clean
Frame Title Centering Fix (2026-03-22)
FIX Title now centers within full frame width, then clips to button tray boundaries
Previously centered within available tray space only — looked off-center with close/zoom buttons
Adds '…' ellipsis character when title is truncated on left or right edge
Handles edge cases: very narrow frames, titles shorter than available space
4 new tests: centered title, right-truncation ellipsis, no-ellipsis-when-fits, very-narrow-no-crash
321 tests passing, clippy pedantic clean
F1: MenuBar → Overlay Dropdown Refactor
REFACTOR Dropdown rendering moved from HorizontalBar self-draw to OverlayManager + MenuBox
Dropdowns now render above all windows (not clipped by bar's clip area)
~200 lines of duplicate drawing/event code removed from HorizontalBar
MenuBox used as the actual overlay view (already existed as standalone widget)
Phase 1: MenuBox Enhancement
Added owner_bar_id: Option<ViewId> — when set, MenuBox emits commands via event system
confirm_selection() now sets event.kind = EventKind::Command(cmd) when owned by a bar
Left/Right arrows post CM_DROPDOWN_NAVIGATE deferred event (with direction stored in navigate_direction)
Backward compat: standalone MenuBox::result() still works for non-overlay usage
Phase 2: HorizontalBar Simplification
Removed: draw_dropdown(), draw_dropdown_border_row(), draw_dropdown_item_row() (~170 lines)
Removed: dropdown_width(), item_at_position(), move_down(), move_up(), selected_command(), first_selectable_item()
Removed: selected_item field (dropdown item selection now handled by MenuBox)
Added: request_dropdown() posts CM_OPEN_DROPDOWN + stores pending_dropdown for Application
Added: navigate_dropdown() (public) replaces move_entry(), called by Application on CM_DROPDOWN_NAVIGATE
Added: take_pending_dropdown(), dropdown_items_for(), dropdown_anchor() public API for Application
F10/Escape/close now post CM_DROPDOWN_CLOSED so Application can clean up overlays
Phase 3: Application Orchestration
Intercepts CM_OPEN_DROPDOWN: creates MenuBox overlay via OverlayManager with calculate_overlay_bounds()
Intercepts CM_DROPDOWN_CLOSED: pops overlay, resets bar state
Intercepts CM_DROPDOWN_NAVIGATE: reads direction from MenuBox, pops current overlay, navigates bar, opens next
Supports both MenuBar (drops down) and StatusLine (drops up)
Phase 4: OverlayManager Dismiss Callback
Outside-click dismiss now posts CM_DROPDOWN_CLOSED so owning bar resets active_dropdown
Escape dismiss already posted CM_DROPDOWN_CLOSED (added in Phase 3)
Added overlays_iter() for Application to inspect overlay contents
New Commands: CM_OPEN_DROPDOWN (1010), CM_DROPDOWN_CLOSED (1011), CM_DROPDOWN_NAVIGATE (1012)
331 tests passing (was 321), clippy pedantic clean
Plan: docs/PLAN-v0.2.2.md
F2: Minimized Window Tray Fix (2026-03-22)
FIX Frame now draws at height=1 — minimized windows visible in task shelf
Frame::draw() guard changed from height < 2 to height < 1
1-row frame: draws only top border (corners, horizontal line, close button, title)
Side borders, scrollbars, bottom border skipped at height < 2
FIX ButtonTray suppresses minimize/maximize buttons at height ≤ 1
Both draw() and build_button_tray() (hit-testing) consistent
Minimized windows show only: [■] Title [×] (close + title)
FIX Hit-test methods guard against height ≤ 1:
is_minimize_button(), is_maximize_button(), is_resize_handle() → return false
is_close_button() still works (close button valid at any height)
Added has_close_button() public accessor on Frame
4 new tests: height=1 draw, no min/max buttons, close button works, title visible
335 tests passing, clippy pedantic clean