diff --git a/AGENTS.md b/AGENTS.md index d9393992..6ac07578 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -203,7 +203,7 @@ The bundle mirrors Ghostty.app: `Contents/Resources/ghostty/{themes,shell-integr The motion pane, directly under Appearance in the Settings sidebar. Four toggles: **Smooth scrolling** and **Animate splits** ship **on**, the two cursor shaders are opt-in. It was Settings → Experimental until the first two graduated; anything still settling in belongs here too, off by default, until it does. - **Smooth scrolling** is fork patch 0005 (`.github/downstream/0005-smooth-scroll.patch`) behind the fork key `smooth-scroll`, which the toggle writes into the overrides via `MactermConfig.Animations` (`Preferences.smoothScrolling`, default on; the ghostty key's own default stays off). libghostty's `scrollCallback` already accumulates precise trackpad deltas in pixels and commits whole rows; with the key on, the patch keeps the sub-row remainder on `Screen.viewport_pixel_offset`, captures the rows being revealed as upstream's `RenderState` overscan (ghostty-org/ghostty#14400, the first step of upstream's own smooth scrolling; the fork's `beginShiftedUpdate` sets the request, `overscan.above` rows at the top or one below, fork PR 16) and shifts the grid by a `scroll_offset` uniform, clipping to the visible grid sized from `grid_size × cell_size` — **never from `grid_padding`'s bottom/right**, which hold the leftover measured at the last resize and go stale after the incubator→pane growth. Any row-level viewport move goes through `Screen.scroll`, which zeroes the remainder. Macterm has no part in the routing of a *wheel* event (every one reaches libghostty untouched, #393), which is exactly why the gate is a ghostty key and not a `Preferences` read. The alternate screen has no scrollback, so there the key animates a program scrolling a region of its screen by rows instead (DECSTBM/DECSLRM with SU/SD, or IND/RI at the margin, which is what `less` does): the renderer draws the region's new content shifted by the scrolled distance and eases it home in about a quarter second, with the rows that scrolled out sliding away as ghost rows (fork PR 11, carried in 0005 since fork PR 14 after a sync dropped it). A program that repaints every row still moves by rows. - **A resize moves the viewport by pixels too** (fork PR 9): a grid only comes in whole rows, so the leftover height of a viewport grows as a pane is resized until it is a full row and the viewport takes one — and at that instant everything on screen jumps a cell, because the row it gains is pulled in from scrollback at the top. With the key on, the renderer hands the render state that leftover (`RenderState.Geometry`), which adds to whatever remainder a gesture left, and the grid is drawn that much lower with the row above partly revealed. So a divider drag and every split animation slide the content instead of stepping it, at the cost of a partial row at the top of any pane whose height isn't a whole number of cells. The two offsets adding is why more than one row can be revealed above and `scroll_offset` carries the leftover in `.z` — the shifted grid draws onto it, so the clip extends by it. + **A resize moves the viewport by pixels too** (fork PR 9): a grid only comes in whole rows, so the leftover height of a viewport grows as a pane is resized until it is a full row and the viewport takes one — and at that instant everything on screen jumps a cell, because the row it gains is pulled in from scrollback at the top. With the key on, the renderer hands the render state that leftover (`RenderState.Geometry`), which adds to whatever remainder a gesture left, and the grid is drawn that much lower with the row above partly revealed. So a divider drag and every split animation slide the content instead of stepping it, at the cost of a partial row at the top of any pane whose height isn't a whole number of cells — a scrollback row, so after a clear it is the old prompt line. That row selects like any other (`RenderState.Shift.rowsAboveAt`, fork PR 17): selection resolves the pointer to a pin, because a viewport coordinate cannot go above the viewport's first row and clamped a press there to the row below. The two offsets adding is why more than one row can be revealed above and `scroll_offset` carries the leftover in `.z` — the shifted grid draws onto it, so the clip extends by it. **A scroller drag is the one exception**, because `scroll_to_row` lands on whole rows and `Screen.scroll` zeroes the remainder on the way: `SurfaceScrollView` sends the row, then hands the sub-row leftover to the core as a synthetic precision scroll (`GhosttyTerminalNSView.applySubRowScrollOffset`). It can aim because `ScrollAccumulator` mirrors `Surface.mouse.pending_scroll_y` — unreadable over the C API, but `scrollCallback` is its only producer and Macterm is the only caller of `ghostty_surface_mouse_scroll`, so mirroring every delta we send reproduces it. The nudge is sized to land the accumulator exactly on the wanted remainder, which is under a cell and so commits no row of its own; being wrong costs at most a row of offset until the next drag update. The multiplier it divides out is the user's `mouse-scroll-multiplier`, read from raw config text (`MouseScrollMultiplier`) because the key is a Zig struct with no C shape. Skipped with the toggle off or with no scrollback to drag through. **Two rules the render state enforces, each found by measuring frames** (see `RenderState.resolveShift` in the fork). The gesture's remainder is validated *on its own* before the resize leftover is added: a remainder with no row to reveal is dropped and the leftover still shifts the grid. Validating the sum instead made a scroll pinned at the bottom bob the content by up to the leftover's height on every event, since the remainder cycles through a cell as row after row fails to commit. And the leftover is measured *by the render state against the terminal's row count* (`Geometry.terminal_height`), never by the renderer against its own grid: the renderer's size and the terminal's rows change on different threads, and a frame caught between them drew the grid a whole cell off — every divider drag showed it as a slide-then-snap-back. **Anything that turns a pixel into a row takes the shift from the render state** (`RenderState.resolveShift`, fork PR 13): the surface's hit test (selection, clicks, links), mouse reports and the IME point subtract exactly the shift a frame draws, validation and all. A copy of the arithmetic in `Surface.posToViewport` skipped those checks and put the pointer a row off wherever they drop the shift: below it at the bottom of scrollback after a scroll, above it in a pane without scrollback, on the alternate screen and at the top of history (#433). A region-scroll animation's offsets are per-region renderer state, so the renderer publishes them (`RenderState.RegionShifts`, fork PR 15) and the same three undo them: the offsets of the frame the layer is showing (its contents change on the main thread, where hit tests run, so neither drawing nor presenting a frame decides), with every region scroll the terminal has done since that frame was drawn folded in. A click during the quarter-second ease, or mid-momentum, lands on the row drawn under the pointer; one on a row the region has scrolled out of resolves to the region's edge row. diff --git a/scripts/setup.sh b/scripts/setup.sh index a0266ea7..4787dc1e 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -23,7 +23,10 @@ XCFRAMEWORK_DIR="GhosttyKit.xcframework" # hit tests were A/B'd pixel-identical against build-2026-09-25. Upstream also # added GHOSTTY_ACTION_RESIZE_WINDOW (CSI 8 t, behind its off-by-default # `vt-window-resize-allowed`), which Macterm doesn't handle, so the key does -# nothing here yet; the tag renumbers OUTPUT_ACTIVITY. Any +# nothing here yet; the tag renumbers OUTPUT_ACTIVITY. Re-cut onto +# thdxg/ghostty#17: the partly revealed scrollback row at the top of a shifted +# grid (the old prompt line after a clear) is selectable instead of clamping a +# press to the row below it. Any # same-day push to the fork's main — the nightly sync included — deletes and # recreates a daily tag with different bytes, the asset-swap-under-a-pin hazard # documented in AGENTS.md; the stamp below can't tell copies apart, so a