Skip to content

Commit f78c51b

Browse files
Size the landing-floated overlay to its full computed height
Fix permission and operator overlays not showing their choices on the landing screen
2 parents d864400 + 3734a57 commit f78c51b

2 files changed

Lines changed: 55 additions & 16 deletions

File tree

‎src/tui-opentui/landing.test.ts‎

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
isLanding,
1717
paintLanding,
1818
} from "./shell"
19-
import { openOperatorOverlay } from "./overlays"
19+
import { makeOperatorQuestion, openOperatorOverlay } from "./overlays"
2020
import {
2121
LANDING_HINTS,
2222
LANDING_SUGGESTIONS,
@@ -328,7 +328,7 @@ describe("landing screen", () => {
328328
)
329329
})
330330

331-
test("an overlay covers the landing instead of moving it", async () => {
331+
test("an overlay covers the landing, sliding it only as far as its content needs", async () => {
332332
await withTestRenderer(
333333
async (h) => {
334334
const shell = createAppShell(h.renderer, {
@@ -345,15 +345,25 @@ describe("landing screen", () => {
345345
before.findIndex((row) => row.includes(text)),
346346
)
347347
expect(was.every((index) => index > 0)).toBe(true)
348+
// The anchors are listed top to bottom, so their positions climb
349+
// together before the overlay opens.
350+
expect(was).toEqual([...was].sort((a, b) => a - b))
348351

349352
openOperatorOverlay(shell)
350353
await settle(h)
351354
const after = rows(h)
352-
// Every landing anchor is on the row it was on: the overlay covers
353-
// the composition, it does not push it around.
354-
expect(
355-
anchors.map((text) => after.findIndex((row) => row.includes(text))),
356-
).toEqual(was)
355+
// Every landing anchor is still on screen and in the same relative
356+
// order: the overlay is not letting the composition it covers spill
357+
// off the viewport, overlap itself, or reshuffle. It may still
358+
// slide the composition (up or down a little, as the mark re-grids
359+
// for its new tier) when its own content needs more room than the
360+
// even top/bottom split would otherwise leave it.
361+
const nowAt = anchors.map((text) =>
362+
after.findIndex((row) => row.includes(text)),
363+
)
364+
expect(nowAt.every((index) => index > 0)).toBe(true)
365+
expect(nowAt).toEqual([...nowAt].sort((a, b) => a - b))
366+
expect(new Set(nowAt).size).toBe(nowAt.length)
357367
expect(h.captureCharFrame()).toContain("operator")
358368
} finally {
359369
shell.dispose()
@@ -363,6 +373,35 @@ describe("landing screen", () => {
363373
)
364374
})
365375

376+
// A question with more choices than the even top/bottom split would leave
377+
// room for used to get its list starved down to whatever that split
378+
// happened to allow — as little as one or two choices — because the float
379+
// only asked the split for one choice row of headroom. It now asks for the
380+
// overlay's real, already fraction-capped content height, so a terminal
381+
// tall enough for that content shows every choice without scrolling.
382+
test("a landing overlay with many choices shows them all when there is room", async () => {
383+
await withTestRenderer(
384+
async (h) => {
385+
const shell = createAppShell(h.renderer, {
386+
terminal: { columns: 100, rows: 36 },
387+
wireKeys: false,
388+
run: "idle",
389+
})
390+
try {
391+
openOperatorOverlay(shell)
392+
await settle(h)
393+
const frame = h.captureCharFrame()
394+
for (const choice of makeOperatorQuestion().choices) {
395+
expect(frame).toContain(choice)
396+
}
397+
} finally {
398+
shell.dispose()
399+
}
400+
},
401+
{ width: 100, height: 36 },
402+
)
403+
})
404+
366405
test("a short or narrow terminal shrinks the mark, never the prompt box", async () => {
367406
for (const size of [
368407
{ width: 100, height: 30 },

‎src/tui-opentui/shell.ts‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1515,8 +1515,9 @@ function meterEquals(a: CostContextMeter | null, b: CostContextMeter | null): bo
15151515
* A floated overlay is clipped to the rows above the box so it never covers the
15161516
* thing the operator types into. Losing the tail of a long body to that clip is
15171517
* survivable; losing every choice is not, because then the surface cannot be
1518-
* answered. So the box slides down just far enough to keep the overlay's chrome
1519-
* and one choice on screen, and the starters below it pay for the move.
1518+
* answered. So the box slides down just far enough to keep the overlay's full,
1519+
* already fraction-capped height on screen, and the starters below it pay for
1520+
* the move.
15201521
*/
15211522
function landingSplitFor(
15221523
landingRows: number,
@@ -1572,14 +1573,13 @@ export function applyLayout(shell: AppShell, layout: GeometryLayout): void {
15721573
const bag = internals.get(shell)
15731574
const landing = bag?.landing ?? null
15741575
const landingRows = transcriptH - padH - bottomPadH + (landing === null ? 0 : overlayH)
1576+
// The resolver already sized overlayH to the overlay's real content (list
1577+
// included) and capped it against the fraction/floor limits, so it is the
1578+
// correct minimum to ask the landing split to make room for — asking for
1579+
// less (e.g. just enough for one choice row) starves the list underneath
1580+
// the title down to nearly nothing once floatOverlayHost pins the host to it.
15751581
const split =
1576-
landing === null
1577-
? null
1578-
: landingSplitFor(
1579-
landingRows,
1580-
overlayH > 0 ? overlayHostRows(shell, shell.overlayBodyLines.length, 1) : 0,
1581-
padH,
1582-
)
1582+
landing === null ? null : landingSplitFor(landingRows, overlayH, padH)
15831583
if (bag !== undefined && landing !== null && split !== null) {
15841584
landing.above.box.height = Math.max(1, split.above)
15851585
// A new zone can seat a different tier, and a tier is a different grid, so

0 commit comments

Comments
 (0)