fix(tui): the grid and the tile bar scroll by the width they are drawn in (improvise-gtn7.1)

Grid: App::cmd_context counted visible_cols from term_width while
draw_content gave the grid term_width minus the 32-column side panel, so
with any panel open the cursor was scrolled past the last drawn column.
viewport::grid_width, reached through App::grid_width, is now the one
answer: draw_content splits on it and the keypress path counts on it.

Tile bar: the scroll measured from the end of the prefix while the draw
began one column later, two once scrolled (the left arrow), so the
selected tile could be dropped. tiles_left(start) is now where both the
scroll (first_tile) and the draw begin.

The grid defect dates from b5418f2e; it became the launch state when
50e4a372 opened the category panel by default, which is why it read as
a regression. Mixed-width columns still lose the cursor with no panel
open: improvise-gtn7.2. Reasoning: context/code/tui.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
Edward Langley
2026-09-24 18:32:47 -07:00
parent b945a7f881
commit 241cc5f384
6 changed files with 181 additions and 64 deletions
+39
View File
@@ -220,6 +220,45 @@ belong in `advertised()` keyed by step, which would make them derived like
everything else and let the status bar carry them correctly instead of being
switched off.
### The grid's width is one answer, and a scroll is measured where it is drawn
Two parts of the TUI scroll horizontally, and both lost their cursor for the
same reason: **the code that chose the scroll offset measured a different width
from the code that drew it** (improvise-gtn7.1).
- **The grid.** `App::cmd_context` counts `visible_cols` for the keypress path,
and `GridWidget` counts again from the `Rect` it is handed. The keypress path
counted from `term_width`, while `draw_content` gave the grid `term_width`
minus the 32-column side panel. So with any of the three panels open the
cursor was scrolled to a column the widget never drew: it went "behind the
panel". Now `viewport::grid_width(term_width, side_panel_open)` is the one
answer, reached through `App::grid_width`. `draw_content` splits the content
row on it, and `cmd_context` counts on it. `App::side_panel_open` is the one
answer to "is a panel open", so a panel that did not count toward the width
would not be drawn either, and the two sides still agree.
- **The tile bar.** `TileBar` chose its first tile against the width after
the prefix, but drew tiles one column later, and two columns later once
scrolled, to leave room for `◀`. When the selected tile fitted within those
one or two columns, the scroll stopped too early and the draw loop dropped the
tile. `tiles_left(start)` is now where both the scroll (`first_tile`) and the
draw begin.
**Why it looked like a regression.** The grid defect dates from `b5418f2e`
(2026-04-06), when `compute_visible_cols` replaced a rough estimate and took
`term_width`. It was always wrong with a panel open. But until `50e4a372`
(2026-09-20) no panel was open at launch. That commit opened the category panel
by default, in the shared `ViewState::default()`, and from then on every TUI
session started in the broken configuration. The tile bar's offset has been
wrong since `4686f470` (2026-04-09), when it first scrolled.
**What is still wrong: improvise-gtn7.2.** `visible_cols` is a *count*, taken at
the *old* offset, and `viewport_effects` applies it at the *new* one. Across
columns of mixed width no single count is right, so the cursor can still leave
the grid with no panel open. The fix is to measure the new offset against the
widths, as `first_tile` does. It is not in this change because
`resolve.rs::scrolled_to` mirrors `viewport_effects` over a window counted in
columns, and `lockstep.rs` holds the two in agreement.
### The caches are built here, per frame
`project::formulas_of`, `category_tree_of`, `view_list_of`, `tile_bar_of`,