From 32c3ea1637f9753ad7e0a67964ce5569b590cd58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B6=82=E9=98=B3=E5=B7=9D?= Date: Wed, 16 Sep 2026 18:28:21 +0800 Subject: [PATCH] android: Use Android's scroll physics, and let hosts register an AssetSource MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gaps a host app hits on the `android::host` path. **Flings used iOS physics.** `AndroidPlatform` never implemented `Platform::gestures`, so GPUI fell back to `GestureTuning::default()` — whose `scroll_physics` is `UIScrollView`'s exponential decay. Every Android app built on gpui-mobile has therefore been flinging with iOS physics: the content coasts much further than the platform's own scroll views, which reads as the list not stopping where you expect. Return `ScrollPhysics::android()` instead, the AOSP `OverScroller` friction spline that already lives in `gestures.rs`. GPUI flings in logical pixels and a window's scale factor is Android's display density, so logical pixels are density-independent pixels and the constructor's nominal 160 dpi pairing is the right one. Measured on a Huawei P40 (1080x2340 @3x), same conversation and gesture script, 2.5s after release: fling 80ms iOS physics: 71 frames Android physics: 58 frames fling 300ms iOS physics: 60 frames Android physics: 41 frames Fewer frames because the spline sheds speed faster and the fling travels a shorter distance — the point of the change, not a regression. Frame pacing is unchanged (16ms intervals throughout, no dropped frames). The rest of `ViewConfiguration` (touch slop, tap timeouts) stays on GPUI's defaults; reading the real values over JNI is a separate change. **A host could not install an asset source.** `host::start` hands the launch closure a `&mut App`, which is one step too late: `Application::with_assets` consumes the builder, and the builder is created inside `on_surface_created` where the host cannot reach it. A host that renders SVGs or icons through the asset source gets empty results with no error — `AssetSource::load` returning `None` is a valid answer, so nothing logs and the icons simply do not appear. `start_with_assets` stores the configuration step in a static that `on_surface_created` consumes when it builds the `Application`, right before `run_embedded`; `start` keeps its current shape for hosts that do not need one. --- src/android/host.rs | 28 +++++++++++++++++++++++++++- src/android/platform.rs | 39 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/src/android/host.rs b/src/android/host.rs index eb32110..ec706de 100644 --- a/src/android/host.rs +++ b/src/android/host.rs @@ -122,6 +122,12 @@ pub struct Pointer { } type Launch = Box; +/// Applied to the `Application` before `run_embedded`, so a host can register things +/// that must exist before the first frame — an asset source, most importantly. +type Configure = Box Application + Send>; + +/// Set by [`start_with_assets`], consumed when the app is built. +static CONFIGURE: Mutex> = Mutex::new(None); /// GPUI's application, owned by the render thread. struct HostApp { @@ -145,6 +151,23 @@ fn wake_render_thread() { } } +/// Like [`start`], but also registers an [`gpui::AssetSource`] on the `Application` +/// GPUI builds on the render thread. +/// +/// `Application::with_assets` consumes the builder before `run_embedded`, so a host that +/// only gets `&mut App` in its launch closure has no way to install one — anything +/// resolved through the asset source (icons, SVGs) would silently come back empty. +pub fn start_with_assets(assets: A, launch: F) +where + A: gpui::AssetSource, + F: FnOnce(&mut App) + Send + 'static, +{ + *CONFIGURE.lock().expect("poisoned") = Some(Box::new(move |application: Application| { + application.with_assets(assets) + })); + start(launch); +} + /// Start the render thread. Idempotent — later calls are no-ops, which is what makes /// an Activity recreation cheap: the thread, the platform and the GPUI `App` all survive. /// @@ -305,8 +328,11 @@ fn on_surface_created( // Without an `AndroidApp` to drive, `AndroidPlatform::run` invokes the // callback immediately and returns — the shape `run_embedded` expects. // The handle is what keeps the `App` alive afterwards. - let application = + let mut application = Application::with_platform(SharedPlatform::new(Arc::clone(platform)).into_rc()); + if let Some(configure) = CONFIGURE.lock().expect("poisoned").take() { + application = configure(application); + } app.handle = Some(application.run_embedded(launch)); } } diff --git a/src/android/platform.rs b/src/android/platform.rs index a3ea821..0cdea3a 100644 --- a/src/android/platform.rs +++ b/src/android/platform.rs @@ -36,9 +36,10 @@ use anyhow::Result; use futures::channel::oneshot; use gpui::{ Action, ActivityGuard, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, - ForegroundExecutor, KeybindingKeystroke, Keymap, Keystroke, Menu, MenuItem, PathPromptOptions, - Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, - PlatformWindow, Task, ThermalState, WindowAppearance, WindowParams, + ForegroundExecutor, GestureTuning, KeybindingKeystroke, Keymap, Keystroke, Menu, MenuItem, + PathPromptOptions, Platform, PlatformDisplay, PlatformGestures, PlatformKeyboardLayout, + PlatformKeyboardMapper, PlatformTextSystem, PlatformWindow, ScrollPhysics, Task, ThermalState, + WindowAppearance, WindowParams, }; use gpui_wgpu::CosmicTextSystem; use parking_lot::Mutex; @@ -952,6 +953,31 @@ impl AndroidPlatform { } } +/// Android's feel constants for GPUI's portable gesture recognizers. +/// +/// Without this GPUI falls back to [`GestureTuning::default`], whose scroll +/// physics are `UIScrollView`'s — an exponential decay that coasts noticeably +/// longer than Android's, so a fling reads as unresponsive to anyone used to +/// the platform. +struct AndroidGestures; + +impl PlatformGestures for AndroidGestures { + fn tuning(&self) -> GestureTuning { + GestureTuning { + // AOSP's `OverScroller` friction spline. GPUI flings in logical + // pixels, and this window's scale factor is Android's display + // density, so logical pixels are density-independent pixels and + // the nominal 160 dpi pairing this constructor documents is the + // right one. + scroll_physics: ScrollPhysics::android(), + // The rest of `ViewConfiguration` (touch slop, tap timeouts) is + // close enough to GPUI's defaults to leave alone; reading the real + // values over JNI is a separate change. + ..GestureTuning::default() + } + } +} + // ── impl Platform ───────────────────────────────────────────────────────────── // // Implementation of the GPUI `Platform` trait for Android. @@ -961,6 +987,10 @@ impl AndroidPlatform { // file pickers, etc.) are no-ops or return sensible defaults. impl Platform for AndroidPlatform { + fn gestures(&self) -> Option> { + Some(Rc::new(AndroidGestures)) + } + fn background_executor(&self) -> BackgroundExecutor { let dispatcher: Arc = self.state.lock().dispatcher.clone(); BackgroundExecutor::new(dispatcher) @@ -1310,6 +1340,9 @@ impl SharedPlatform { /// so the compiler never accidentally picks the inherent method (which may /// have a different return type). impl Platform for SharedPlatform { + fn gestures(&self) -> Option> { + ::gestures(&self.0) + } fn background_executor(&self) -> BackgroundExecutor { ::background_executor(&self.0) }