Skip to content

Migrate to gpui-kit 0.6.4: window seam on GPUI geometry, touch selection, GPUI-owned touch scrolling - #476

Merged
Tryanks merged 28 commits into
mainfrom
gpui-kit-0.6.4
Sep 20, 2026
Merged

Tryanks merged 28 commits into
mainfrom
gpui-kit-0.6.4

Conversation

@Tryanks

@Tryanks Tryanks commented Sep 20, 2026 •

Copy link
Copy Markdown
Owner

Moves the whole tree to gpui-kit 0.6.4 / gpui-pre 0.3.5 and finishes the migration that upgrade makes possible: the window seam reads GPUI's window geometry, touch text selection gets handles and an edit menu, and Tcode's touch-scroll capture router is deleted in favour of GPUI's own scrolling composed with gpui-base's masks.

Dependencies

  • gpui-base 0.6.1 → 0.6.4, gpui-kit-assets / gpui-component-macros 0.6.0 → 0.6.4, gpui-wry 0.6.1 → 0.6.4, every gpui-pre-* crate 0.3.3 → 0.3.5 (workspace crates plus the excluded crates/platform/gpui-ios and gpui-android). Only the gpui entries in Cargo.lock changed; futures 0.3.34 is forced by gpui-pre 0.3.5.

Platform hooks (gpui-ios, gpui-android)

  • The new required PlatformWindow::visibility / on_visibility_change are implemented from state each backend already tracks (Android: the surface exists exactly while the activity is on screen; iOS: hidden while the view is detached or the scene is in the background). Platform::on_system_sleep is stored next to on_system_wake; prevent_idle_sleep returns a no-op guard.
  • The keyboard cover is reported as the mobile visual viewport, and the backends' shell-facing accessors (insets, keyboard, mobile flags) are gone: GPUI now carries that state.

Window seam → Window API

  • window_seam.rs no longer caches, polls or installs anything. The safe content rectangle is GPUI's fully visible bounds — viewport ∩ visual viewport, inset by the effective system insets — read from the Window at render time, so the layout follows the keyboard the frame the platform reports it. Bottom avoidance stays max(safe area, keyboard).
  • is_mobile() is the one compact/touch capability query, with a test override; pages, sheets, toasts and the palette compose the same rectangle.

Touch selection (what a user gets)

  • A long press in any input or selectable Markdown body selects the word under the finger and keeps following it; in a field a double tap selects the word too. Releasing shows an edit menu.
  • Handles at each end of the selection (2px bar, 10px knob, 44pt touch target); dragging a handle moves that end only, and dragging past the other end swaps them. Input handles float above the field; Markdown paints its handles in place.
  • The edit menu is a T3 pill of ghost buttons. Editable text offers Cut, Copy, Paste and Select All, each only when it applies (Paste never reads the clipboard just to decide, which would raise the iOS paste banner); read-only text offers Copy and Select All. Select All takes the whole field or the whole message, past the virtualized viewport, and keeps the controls over the result.
  • One touch selection at a time; typing, moving the caret or a press elsewhere ends it. While a finger scrolls a message or a field, the menu steps aside and returns over the handles when the finger lifts.
  • Owners: gpui-base owns the gesture and geometry; crates/ui/src/touch_selection draws them with Tcode's tokens; widgets/input.rs mounts the field overlay and markdown/view.rs the message one. New strings in both locales.

Scrolling

  • Router removed. crates/ui/src/touch_scroll.rs (the capture router that selected a viewport on touch-down and applied pan deltas itself) is deleted. Nothing in Tcode intercepts touch events any more: GPUI recognizes pans, locks them to their starting axis and delivers scroll events to the elements under the finger, with momentum. Wheel easing survives as wheel_easing.rs.
  • Compositions in scroll.rs. page_viewport (page-level vertical viewports: easing + ScrollBounce), overflow_y_scroll_area / overflow_y_scrollbar (bounded vertical areas with a ScrollableMask), overflow_x_scroll_area (horizontal strips). Scrollbar thumbs can be dragged by touch.
  • Interaction differences from the router:
    • Nested vertical areas (menus, option lists, approval and toast details, inline diffs, disclosure bodies) move alone while they can and chain to their ancestor at an edge, instead of one gesture being captured by whichever viewport it started in.
    • Horizontal strips (tables, inline diff rows, the terminal key bar, segmented tracks) own horizontal movement even at their ends, so a sideways gesture never moves the page, and leave vertical movement to the view behind them.
    • Page-level vertical viewports bounce at their edges on iOS and Android (displacement only; scroll position stays clamped; reduced motion disables it). Bounded inner areas do not bounce.
    • The composer textarea scrolls through gpui-base input's own handler, so its edit menu steps aside for a pan like a message's does (covered by a new test).
  • The touch picker sheet content is a bounded vertical scroll area with its own resolved height.

docs/DESIGN.md ("Scrolling contract", "The window seam", "Touch and typography", "Touch selection", "Bottom sheets") and the platform READMEs describe the result.

Checks run locally (macOS)

  • cargo fmt --all --check ✅
  • cargo clippy --workspace --all-targets --locked -- -D warnings ✅
  • cargo test --workspace --locked ✅ (all suites pass, 0 failed; tcode-ui 430 passed)
  • cargo-machete ✅ no unused dependencies
  • IPHONEOS_DEPLOYMENT_TARGET=26.0 RUSTFLAGS='-D warnings' cargo check -p tcode-ios --target aarch64-apple-ios-sim --locked ✅
  • RUSTFLAGS='-D warnings' cargo check -p tcode-web --target wasm32-unknown-unknown --locked ✅
  • RUSTFLAGS='-D warnings' cargo ndk -t arm64-v8a check -p tcode-android --locked ✅

gpui-base 0.6.1 -> 0.6.4, gpui-kit-assets and gpui-component-macros
0.6.0 -> 0.6.4, gpui-wry 0.6.1 -> 0.6.4, and every gpui-pre crate
0.3.3 -> 0.3.5, including the workspace-excluded iOS and Android
backends. Only the gpui-* entries in Cargo.lock were updated; the
futures/futures-executor 0.3.34 bump is forced by gpui-pre 0.3.5's
`futures = "^0.3.34"` requirement.
`PlatformWindow::visibility` and `on_visibility_change` map to the state
each backend already tracks: on Android the ANativeWindow surface exists
exactly while the activity is on screen, so InitWindow/TerminateWindow
report the transition; on iOS the window is hidden while the UIView is
detached or the scene is in the background, mirroring when the Swift
host pauses its display link. Only transitions since the callback was
registered are reported, as the desktop backends do.

`on_system_sleep` stores its callback next to the existing never-fired
`on_system_wake`, and `prevent_idle_sleep` returns a no-op guard.
The markdown selection adapter ignores the new
`TextSelectionEvent::TouchSelectionChanged`; it does not paint touch
handles yet, so no extra frame is needed. `App::fetch_asset` now returns
the cached result directly, so the host image test asserts invalidation
through `has_asset` and reads the result after the host reply.
GPUI 0.3.5 refreshes a window from its own insets and visual-viewport
callbacks, so the iOS and Android backends no longer request or schedule
a frame of their own when insets move. Both now also report the visible
viewport as the window minus the IME cover, which is what
Window::visual_viewport_bounds documents, and notify GPUI when that
cover changes.
The WindowSeam global forwarded platform accessor closures for insets,
the software keyboard and app lifecycle. GPUI 0.3.5 owns the first two:
Window::fully_visible_bounds is the viewport intersected with the visual
viewport and inset by max(safe area, keyboard), and
Window::request_virtual_keyboard reaches the platform keyboard directly.
window_seam is now a pub(crate) module of pure functions over &Window:
content_insets, window_is_compact (fully visible width < 900),
keyboard_covers_window (the visual viewport ends above the window's
bottom), and is_mobile forwarding to gpui_base::is_mobile with the
existing test override. Bootstraps no longer build a seam; the mobile
ones hand run_shell their platform through ShellOptions::lifecycle so
the reconnect-on-foreground policy stays with the bootstrap.

Tests drive occlusion through the test window's visual viewport, the
only geometry GPUI's test support exposes; the explicit-tap keyboard
requests are no longer observable from outside GPUI, so those two
assertions are gone with the closure they counted.
gpui_ios::insets, gpui_ios::set_keyboard_visible, gpui_android::insets
and gpui_android::show_keyboard only served the removed WindowSeam; the
same host callbacks are reached through PlatformWindow::insets and
show_soft_keyboard.
The capture-phase router in touch_scroll.rs swallowed every touch pan
before GPUI's scroll containers, lists, inputs, masks and scrollbars
could see it. gpui-pre 0.3.5 recognizes pans itself and delivers them
as ScrollWheelEvents with touch phases to the element under the finger,
so the router, its registry and the touch_overflow_* wrappers go.

What remains of the module is the desktop mouse-wheel easing, renamed to
wheel_easing with the minimal viewport registration it needs.

scroll.rs now owns the compositions every viewport uses:
- page_viewport: a page-level viewport scrolled by its own handler,
  registered for wheel easing and wrapped in gpui-base ScrollBounce on
  iOS/Android (chat timeline, sidebar lists, settings, palette, plan,
  hosts page, diff list).
- overflow_y_scroll_area / overflow_y_scrollbar: bounded vertical
  viewports with a vertical ScrollableMask sibling, so a nested scroller
  moves alone while it can and chains to its ancestor at an edge.
- overflow_x_scroll_area: horizontal strips (markdown tables, key bar,
  segmented tracks, inline diffs) with a horizontal mask, replacing the
  markdown renderer's own HorizontalScrollState/route_gesture policy.

The composer textarea keeps gpui-base input's own scrolling.
…disclosure card

- scroll.rs: a strip and a bounded area inside a list own only their axis;
  the area chains to the list on the next gesture at its edge.
- chat: a finger pan pauses tail-following on its first step and shows
  the pill; panning back resumes it. A finger drag on the scrollbar thumb
  scrolls the conversation instead of panning it. The timeline stretches
  past its edge on a touch platform without moving its scroll position or
  the composer.
- disclosure cards block clicks only, so a pan their body cannot use
  reaches the timeline behind them.
The scrolling contract and the touch section no longer describe a capture
router: pans reach the element under the finger, masks resolve nesting
and horizontal strips, page-level viewports bounce on iOS/Android, and
scrollbar thumbs accept touch drags. Following and jump-to-latest keep
their wording.
gpui-base 0.6.4 runs the touch selection gesture inside every
InputState and TextareaState and in the window text selection, but
draws nothing for it. Port the styled layer from gpui-component 0.6.4
with tcode's own tokens: floating handles painted with TouchHandle in
the theme selection color, and a T3 pill of ghost buttons in 32px rows
for Cut, Copy, Paste and Select All, each offered only when it applies.

The Input wrapper mounts handles and menu for Input and Textarea; Cut,
Copy and Paste dispatch through the focused input's actions so the
composer's image-paste capture sees them too. OverlayHost mounts the
window selection's menu once per window, after the content, offering
Copy and Select All for read-only text; Select All is dispatched to the
focused participant, since the engine's own would stop at the laid-out
runs of a virtualized Markdown message.

OverlayHost now also mounts the TextSelectionLayer, instead of each
shell route: as the first child of the window its capture-phase scroll
handler runs before the touch pan capture, so the menu steps aside
while a finger scrolls. The shell test fixture initializes gpui-base
itself, as run_shell does, now that no layer does it lazily.

Menu items neither take focus nor a tab stop, so pressing one leaves
the keyboard on the field; Button grows per-corner radii for the pill's
end items and a focusable toggle for that. A long press in a field also
drops the window selection a message holds, as a tap would, and a field
shows its controls only while it has focus, so there is one touch
selection at a time.
Port the TextView side of gpui-base 0.6.1 to 0.6.4 into the Markdown
fork: each inline reports the caret boxes at the ends of the selection
it painted, the adapter registers the first start and the last end with
the window selection, and the view lays out the handles' hitboxes in
prepaint and paints them after its content, so whatever covers the text
covers its handles. A TouchSelectionChanged event renders the view once
more, as the handles are laid out from the ends painted last frame.

The edit menu's Select All is answered here as a view-local selection
that keeps the window selection the long press made: the inlines paint
everything selected and report the new ends, from which the handles
drag on, back into a point selection. Unlike the keyboard's Select All
this does not clear the window selection first, which would drop the
handles and the menu.

Tests port upstream's long-press regression tests to MarkdownView, with
the handle drag going through the painted hitbox as a TouchDragEvent,
and cover the window overlay through OverlayHost: Copy and Select All
past the viewport of a message taller than the window, one touch
selection at a time between a message and the composer, and the menu
stepping aside for a touch pan under the shell's scroll capture.
With the touch-scroll router gone, gpui-base input's own scroll handler
sees the pan and steps the edit menu aside until the finger lifts, as a
message does. Cover it, and drop the field-specific caveat from the
touch selection lifetime in DESIGN.md.

The sheet note in DESIGN.md is also updated for the bounded content
area below.
The sheet content was a plain overflow_y_scroll, so a diagonal pan could
wander. Compose it as a vertical ScrollArea with its own resolved
height, like every other bounded area inside a page or dialog.
Nesting is resolved by gpui-base's scrollable mask composed in scroll.rs;
describe that instead of the capture listener that no longer exists.
@Tryanks Tryanks changed the title Upgrade to gpui-kit 0.6.4 and gpui-pre 0.3.5 Migrate to gpui-kit 0.6.4: window seam on GPUI geometry, touch selection, GPUI-owned touch scrolling Sep 20, 2026
@Tryanks
Tryanks marked this pull request as ready for review September 20, 2026 13:18
A desktop build is always the wide layout, however narrow its window:
tiled to half a screen under a tiling window manager it used to fall into
the compact shell and get stuck on a hosts page it could never leave. A
mobile build keeps the 900px rule over the window's fully visible width,
so a phone is compact and a tablet in landscape is wide.

`window_is_compact(window, cx)` is now `is_mobile(cx) && compact_for(width)`;
`compact_for` stays pure and every consumer goes through the rule. The
build-kind override is one GPUI global: tests set either value, and the
new preview-only `force_mobile_layout` lets the desktop `phone` example
keep standing in for a phone. `gpui_base::is_mobile()` itself is untouched.

Tests that exercised the compact shell on a narrow desktop test window
now declare the mobile build they were standing in for; the two that
needed a hardware-keyboard focus in the same run tap the composer or
switch the override to the desktop build for that step.
A tablet in landscape has no local host and no sidebar to switch routes
with, so before it attaches the wide shell showed the bare hosts body:
no title, no Windows caption, and Add a machine had no way back. The
unattached wide window now renders the same hosts and pair pages the
compact stack does, chosen by the window's destination, so Pair carries
the Back that returns to Machines and nothing else is reachable until
`attach` enters a workspace. Attaching and detaching already move the
history through `enter_workspace` / `leave_workspace`; the test pins the
whole round trip and that stray destinations still show the hosts page.
Desktop builds are always wide; iOS and Android are compact under 900px
of fully visible width and wide at or above it; a wide unattached window
is the Machines page until it attaches. Still never persisted, and
crossing the breakpoint on a tablet is still only a layout change.
@Tryanks
Tryanks merged commit b6f8780 into main Sep 20, 2026
6 checks passed
@Tryanks
Tryanks deleted the gpui-kit-0.6.4 branch September 20, 2026 15:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant