Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 4 additions & 1 deletion scripts/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading