Migrate to gpui-kit 0.6.4: window seam on GPUI geometry, touch selection, GPUI-owned touch scrolling - #476
Merged
Conversation
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
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-base0.6.1 → 0.6.4,gpui-kit-assets/gpui-component-macros0.6.0 → 0.6.4,gpui-wry0.6.1 → 0.6.4, everygpui-pre-*crate 0.3.3 → 0.3.5 (workspace crates plus the excludedcrates/platform/gpui-iosandgpui-android). Only the gpui entries inCargo.lockchanged;futures0.3.34 is forced by gpui-pre 0.3.5.Platform hooks (
gpui-ios,gpui-android)PlatformWindow::visibility/on_visibility_changeare 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_sleepis stored next toon_system_wake;prevent_idle_sleepreturns a no-op guard.Window seam → Window API
window_seam.rsno 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 theWindowat render time, so the layout follows the keyboard the frame the platform reports it. Bottom avoidance staysmax(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)
gpui-baseowns the gesture and geometry;crates/ui/src/touch_selectiondraws them with Tcode's tokens;widgets/input.rsmounts the field overlay andmarkdown/view.rsthe message one. New strings in both locales.Scrolling
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 aswheel_easing.rs.scroll.rs.page_viewport(page-level vertical viewports: easing +ScrollBounce),overflow_y_scroll_area/overflow_y_scrollbar(bounded vertical areas with aScrollableMask),overflow_x_scroll_area(horizontal strips). Scrollbar thumbs can be dragged by touch.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-ui430 passed)cargo-machete✅ no unused dependenciesIPHONEOS_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✅