feat(tui): #127 responsive/adaptive layout #154

Merged
charles merged 2 commits from tui/responsive-layout into main 2026-04-16 11:13:04 +00:00
Collaborator

Closes charles/loom#127

Summary

  • Centralized TermLayout breakpoint system (Compact/Medium/Wide width, Short/Normal height) threaded through AppCtx and recomputed each frame
  • Minimum terminal enforcement (60x15) — renders "Terminal too small" message and early-returns
  • Sidebar auto-collapse when width < 100 columns (user's Ctrl+B toggle preserved)
  • Overlay clamping: centered_rect() now clamps to min 40x10 / max 120x40, never overflows terminal
  • Gallery: dynamic column count (width / 20).clamp(1, 6) instead of hardcoded 4
  • Generate: height-adaptive panel sizes (params 8→5, LoRA 6→3 on short terminals); compact width (< 60) renders single-column params
  • Entities: 3-column on wide (≥120), 2-column with inline kind tabs on medium/compact
  • Settings: fixed-width section sidebar (22 or 18) instead of percentage-based
  • Model browser: adaptive card_height (3–6) and thumb_width (0–16) based on breakpoints

Test plan

  • cargo fmt --all
  • cargo build (full workspace)
  • cargo test -p loom-tui — 211 tests pass (193 lib + 18 integration), including new TermLayout unit tests
  • cargo clippy -p loom-tui -- -D warnings
  • Manual: resize terminal to 60x15, 80x24, 120x40, 200x60 — verify each screen adapts
  • Manual: shrink below 60x15 — verify "too small" message
  • Manual: shrink below 100 cols — verify sidebar auto-hides

🤖 Generated with Claude Code

Closes charles/loom#127 ## Summary - Centralized `TermLayout` breakpoint system (`Compact`/`Medium`/`Wide` width, `Short`/`Normal` height) threaded through `AppCtx` and recomputed each frame - Minimum terminal enforcement (60x15) — renders "Terminal too small" message and early-returns - Sidebar auto-collapse when width < 100 columns (user's Ctrl+B toggle preserved) - Overlay clamping: `centered_rect()` now clamps to min 40x10 / max 120x40, never overflows terminal - **Gallery**: dynamic column count `(width / 20).clamp(1, 6)` instead of hardcoded 4 - **Generate**: height-adaptive panel sizes (params 8→5, LoRA 6→3 on short terminals); compact width (< 60) renders single-column params - **Entities**: 3-column on wide (≥120), 2-column with inline kind tabs on medium/compact - **Settings**: fixed-width section sidebar (22 or 18) instead of percentage-based - **Model browser**: adaptive card_height (3–6) and thumb_width (0–16) based on breakpoints ## Test plan - [x] `cargo fmt --all` - [x] `cargo build` (full workspace) - [x] `cargo test -p loom-tui` — 211 tests pass (193 lib + 18 integration), including new TermLayout unit tests - [x] `cargo clippy -p loom-tui -- -D warnings` - [ ] Manual: resize terminal to 60x15, 80x24, 120x40, 200x60 — verify each screen adapts - [ ] Manual: shrink below 60x15 — verify "too small" message - [ ] Manual: shrink below 100 cols — verify sidebar auto-hides 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Add centralized TermLayout breakpoint system (Compact/Medium/Wide width,
Short/Normal height) threaded through AppCtx. Screens adapt their layouts
based on terminal dimensions:

- Minimum terminal enforcement (60x15) with "too small" message
- Sidebar auto-collapse when width < 100 columns
- Overlay clamping (min 40x10, max 120x40, never overflow)
- Gallery: dynamic column count based on terminal width
- Generate: height-adaptive panel sizes, compact single-column params
- Entities: 3-column (wide) / 2-column with kind tabs (medium/compact)
- Settings: fixed-width section sidebar
- Model browser: adaptive card height and thumbnail width

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
claude-desktop left a comment

Overall this is a solid implementation — the centralized TermLayout breakpoint system is clean and each screen adapts well. No blockers, but a few suggestions below for consistency and robustness.

Overall this is a solid implementation — the centralized `TermLayout` breakpoint system is clean and each screen adapts well. No blockers, but a few suggestions below for consistency and robustness.
@ -203,0 +266,4 @@
/// Sidebar should auto-hide when terminal is narrow.
pub fn sidebar_auto_hidden(&self) -> bool {
self.width < 100
Author
Collaborator

sidebar_auto_hidden() triggers at width < 100, but WidthClass::Medium starts at 80. This means Medium terminals 80–99 cols get auto-hidden sidebar while 100–119 don't — a threshold that doesn't align with the breakpoint system. Consider using WidthClass::Compact for auto-hide, or adding a doc comment explaining why 100 was chosen independently of the breakpoints.

`sidebar_auto_hidden()` triggers at `width < 100`, but `WidthClass::Medium` starts at 80. This means Medium terminals 80–99 cols get auto-hidden sidebar while 100–119 don't — a threshold that doesn't align with the breakpoint system. Consider using `WidthClass::Compact` for auto-hide, or adding a doc comment explaining why 100 was chosen independently of the breakpoints.
@ -815,0 +797,4 @@
.map(|(i, k)| {
let style = if *k == self.kind {
Style::default()
.bg(Color::DarkGray)
Author
Collaborator

The kind-item style computation and rendering is duplicated between the wide branch (ListItem construction) and the non-wide branch (tab spans). ~30 lines could be saved by extracting the shared "is this kind selected" style into a small helper.

The kind-item style computation and rendering is duplicated between the wide branch (ListItem construction) and the non-wide branch (tab spans). ~30 lines could be saved by extracting the shared "is this kind selected" style into a small helper.
@ -692,2 +692,4 @@
fn render(&mut self, ctx: &AppCtx, frame: &mut Frame<'_>, area: Rect) {
// Adaptive column count based on terminal width.
self.columns = ctx.term.gallery_columns() as u8;
Author
Collaborator

Mutating self.columns during render() is a side-effect that could cause issues if columns is read between frames (e.g. move_selection for grid wrap-around — the column count would be stale until the next render). Consider making gallery_columns() the authoritative source at point-of-use rather than caching it here, or at minimum documenting this coupling.

Mutating `self.columns` during `render()` is a side-effect that could cause issues if `columns` is read between frames (e.g. `move_selection` for grid wrap-around — the column count would be stale until the next render). Consider making `gallery_columns()` the authoritative source at point-of-use rather than caching it here, or at minimum documenting this coupling.
@ -1610,1 +1604,4 @@
};
let seed_display = if screen.params.seed == -1 {
"random".to_string()
} else {
Author
Collaborator

This uses a raw inner.width < 60 check instead of the centralized breakpoint system. Other screens use ctx.term.width_class. For consistency, consider checking WidthClass::Compact here or adding a method to TermLayout for compact-panel detection. Same applies to settings.rs:463.

This uses a raw `inner.width < 60` check instead of the centralized breakpoint system. Other screens use `ctx.term.width_class`. For consistency, consider checking `WidthClass::Compact` here or adding a method to `TermLayout` for compact-panel detection. Same applies to `settings.rs:463`.
fix(tui): address PR #154 review feedback
All checks were successful
qa / qa (pull_request) Successful in 17m26s
fbe7ee4c98
- Gallery: document self.columns mutation coupling with move_selection
- Entities: extract kind_style closure to deduplicate wide/tab styling
- Sidebar: document why auto-hide uses 100-col threshold (not breakpoints)
- Generate: pass compact flag from ctx.term instead of raw inner.width check
- Settings: use ctx.term.width_class instead of raw inner.width check

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
charles deleted branch tui/responsive-layout 2026-04-16 11:13:04 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/loom!154
No description provided.