diff --git a/CONTRIBUTING_IDEAS.md b/CONTRIBUTING_IDEAS.md index 8b56b77..1a3dc7d 100644 --- a/CONTRIBUTING_IDEAS.md +++ b/CONTRIBUTING_IDEAS.md @@ -125,8 +125,15 @@ October 2026; "fork" means the fix is on one of the branches listed in - Not started: flags and `❤️` (VS16) still don't render with the bundled emoji font; flags come from the system's COLRv1 `NotoColorEmojiFlags.ttf`. - - Not started: safe-area insets. The plan is in - [docs/safe-area-insets.md](docs/safe-area-insets.md). + - Fork: safe-area insets. The host reports the system bars, the cutout + and the keyboard through `host::insets_changed` (gpui-mobile + `android-host-insets`). They become GPUI's `PlatformWindow::insets`, so + `Window::fully_visible_bounds()` works on Android. The lab is edge to + edge, its shell pads by them, Kit inputs reveal above the keyboard + (`input-reveal-on-focus`), and Diagnostics shows them. Upstream draft in + [docs/upstream/safe-area-insets.md](docs/upstream/safe-area-insets.md). + Next: Kit sheets and notifications still lay out against the whole + window, under the bars. - Not started: system fonts. 8. **Upstream the fork fixes.** 22 branches in [MOBILE_PATCHES.md](MOBILE_PATCHES.md) are still fork-only: 11 in diff --git a/Cargo.lock b/Cargo.lock index b9c8ef5..ba762f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2296,7 +2296,7 @@ dependencies = [ [[package]] name = "gpui-base" version = "0.7.1" -source = "git+https://github.com/Bombatomica64/gpui-kit?rev=d47dd0bde03345977db286f9bc8224bf1199cb60#d47dd0bde03345977db286f9bc8224bf1199cb60" +source = "git+https://github.com/Bombatomica64/gpui-kit?rev=e8b28164ac7fa7e1f7d393d441beb738c286bbc8#e8b28164ac7fa7e1f7d393d441beb738c286bbc8" dependencies = [ "aho-corasick", "anyhow", @@ -2336,7 +2336,7 @@ dependencies = [ [[package]] name = "gpui-component" version = "0.7.1" -source = "git+https://github.com/Bombatomica64/gpui-kit?rev=d47dd0bde03345977db286f9bc8224bf1199cb60#d47dd0bde03345977db286f9bc8224bf1199cb60" +source = "git+https://github.com/Bombatomica64/gpui-kit?rev=e8b28164ac7fa7e1f7d393d441beb738c286bbc8#e8b28164ac7fa7e1f7d393d441beb738c286bbc8" dependencies = [ "anyhow", "chrono", @@ -2379,7 +2379,7 @@ dependencies = [ [[package]] name = "gpui-component-macros" version = "0.7.1" -source = "git+https://github.com/Bombatomica64/gpui-kit?rev=d47dd0bde03345977db286f9bc8224bf1199cb60#d47dd0bde03345977db286f9bc8224bf1199cb60" +source = "git+https://github.com/Bombatomica64/gpui-kit?rev=e8b28164ac7fa7e1f7d393d441beb738c286bbc8#e8b28164ac7fa7e1f7d393d441beb738c286bbc8" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2390,7 +2390,7 @@ dependencies = [ [[package]] name = "gpui-fps" version = "0.7.1" -source = "git+https://github.com/Bombatomica64/gpui-kit?rev=d47dd0bde03345977db286f9bc8224bf1199cb60#d47dd0bde03345977db286f9bc8224bf1199cb60" +source = "git+https://github.com/Bombatomica64/gpui-kit?rev=e8b28164ac7fa7e1f7d393d441beb738c286bbc8#e8b28164ac7fa7e1f7d393d441beb738c286bbc8" dependencies = [ "core-graphics 0.24.0", "gpui-pre", @@ -2407,7 +2407,7 @@ dependencies = [ [[package]] name = "gpui-kit" version = "0.7.1" -source = "git+https://github.com/Bombatomica64/gpui-kit?rev=d47dd0bde03345977db286f9bc8224bf1199cb60#d47dd0bde03345977db286f9bc8224bf1199cb60" +source = "git+https://github.com/Bombatomica64/gpui-kit?rev=e8b28164ac7fa7e1f7d393d441beb738c286bbc8#e8b28164ac7fa7e1f7d393d441beb738c286bbc8" dependencies = [ "gpui-base", "gpui-component", @@ -2420,7 +2420,7 @@ dependencies = [ [[package]] name = "gpui-kit-assets" version = "0.7.1" -source = "git+https://github.com/Bombatomica64/gpui-kit?rev=d47dd0bde03345977db286f9bc8224bf1199cb60#d47dd0bde03345977db286f9bc8224bf1199cb60" +source = "git+https://github.com/Bombatomica64/gpui-kit?rev=e8b28164ac7fa7e1f7d393d441beb738c286bbc8#e8b28164ac7fa7e1f7d393d441beb738c286bbc8" dependencies = [ "anyhow", "gpui-pre", @@ -2706,7 +2706,7 @@ dependencies = [ [[package]] name = "gpui-pre-mobile" version = "0.1.0" -source = "git+https://github.com/Bombatomica64/gpui-mobile?rev=2456b8e91ca98f37cea00c8e6a1a3e1b0f8a2410#2456b8e91ca98f37cea00c8e6a1a3e1b0f8a2410" +source = "git+https://github.com/Bombatomica64/gpui-mobile?rev=c71a31aaa12660552ca7a772f6fdd890d9c8cf7a#c71a31aaa12660552ca7a772f6fdd890d9c8cf7a" dependencies = [ "accesskit_android", "android-activity", diff --git a/Cargo.toml b/Cargo.toml index 8e5fdc9..175f8b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,8 +14,8 @@ crate-type = ["cdylib"] # the gpui-mobile fork; [patch.crates-io] swaps in 0.3.8 with our touch fixes. Kit and gpui-mobile come from our forks, pinned by rev # (see MOBILE_PATCHES.md for what each carries on top of upstream). gpui = { package = "gpui-pre", version = "=0.3.8", default-features = false } -gpui-kit = { git = "https://github.com/Bombatomica64/gpui-kit", rev = "d47dd0bde03345977db286f9bc8224bf1199cb60", default-features = false, features = ["component", "assets"] } -gpui-mobile = { package = "gpui-pre-mobile", git = "https://github.com/Bombatomica64/gpui-mobile", rev = "2456b8e91ca98f37cea00c8e6a1a3e1b0f8a2410", default-features = false, features = [ +gpui-kit = { git = "https://github.com/Bombatomica64/gpui-kit", rev = "e8b28164ac7fa7e1f7d393d441beb738c286bbc8", default-features = false, features = ["component", "assets"] } +gpui-mobile = { package = "gpui-pre-mobile", git = "https://github.com/Bombatomica64/gpui-mobile", rev = "c71a31aaa12660552ca7a772f6fdd890d9c8cf7a", default-features = false, features = [ "clipboard", "share", # Exercised by the Platform APIs screen; build.sh copies their Java helpers from this checkout. "file_selector", "image_picker", "local_auth", "permission_handler", "calendar", "contacts", @@ -23,7 +23,7 @@ gpui-mobile = { package = "gpui-pre-mobile", git = "https://github.com/Bombatomi "microphone", "deeplink", ] } # Kit's FPS / frame-time HUD (enables gpui's `profiler` frame trace). -gpui-fps = { git = "https://github.com/Bombatomica64/gpui-kit", rev = "d47dd0bde03345977db286f9bc8224bf1199cb60" } +gpui-fps = { git = "https://github.com/Bombatomica64/gpui-kit", rev = "e8b28164ac7fa7e1f7d393d441beb738c286bbc8" } anyhow = "1" log = "0.4" # Already in the tree through gpui; the Platform APIs screen reads picked images' sizes. diff --git a/MOBILE_PATCHES.md b/MOBILE_PATCHES.md index ee21c8a..fcc601a 100644 --- a/MOBILE_PATCHES.md +++ b/MOBILE_PATCHES.md @@ -83,6 +83,20 @@ Send then hide the keyboard. The purpose half needs the GPUI branch. [amount keyboard](docs/demos/android-amount-keyboard.png), [Search key, Enter](docs/demos/android-ime-actions.png). +### Let host-driven apps report safe-area and keyboard insets — branch `android-host-insets` + +The host path had no way to pass insets in, so an edge-to-edge host could +not tell GPUI where the system bars, the cutout and the keyboard are: the +Android window returned GPUI's default, zero, `PlatformWindow::insets`. +`host::insets_changed(host, SafeAreaInsets, ime)` takes them in physical +pixels. The render thread stores them on that host's window, or keeps them +until the window opens. The window implements `insets()` and +`on_insets_changed`, so apps read them through GPUI +(`Window::fully_visible_bounds`) and GPUI refreshes the window when they +change. `safe_area_insets()` and `keyboard_height()` return them too, as on +iOS. LabActivity calls it from its `OnApplyWindowInsetsListener`. +Draft upstream PR: [docs/upstream/safe-area-insets.md](docs/upstream/safe-area-insets.md). + ### Clear the Java exceptions the accessibility bridge leaves pending — branch `android-a11y-clear-exceptions` `accesskit_android`'s jni 0.21 API leaves a thrown Java exception pending, @@ -140,8 +154,10 @@ press on a focused input calls `Window::request_virtual_keyboard`. When the IME opens, `adjustResize` shrinks the viewport, but nothing scrolled the focused input back into view. A focused input calls `Window::request_autoscroll` for its text area (the caret line in a -textarea) when it gains focus or the viewport size changes. Needs the GPUI -autoscroll fix below to work in plain scroll containers. +textarea) when it gains focus or `Window::fully_visible_bounds()` changes: +the viewport resizing under `adjustResize`, or the keyboard arriving as an +inset on an edge-to-edge window. Needs the GPUI autoscroll fix below to work +in plain scroll containers. ### Tell the keyboard how to assist each input — branches `input-text-input-configuration`, `input-purpose` diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 43d78c2..68462fb 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -9,7 +9,7 @@ android { defaultConfig { applicationId = "dev.gpui.mobile.lab" minSdk = 26 - // 34 keeps adjustResize semantics (35 forces edge-to-edge). + // 34: LabActivity goes edge-to-edge itself; 35 would force it. targetSdk = 34 versionCode = 3 versionName = "0.1.2" diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 43fa928..684d8c9 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -43,14 +43,13 @@ android:supportsRtl="true" android:theme="@style/LabTheme"> - diff --git a/android/app/src/main/java/dev/gpui/mobile/lab/LabActivity.java b/android/app/src/main/java/dev/gpui/mobile/lab/LabActivity.java index 84bb084..8d000d9 100644 --- a/android/app/src/main/java/dev/gpui/mobile/lab/LabActivity.java +++ b/android/app/src/main/java/dev/gpui/mobile/lab/LabActivity.java @@ -2,7 +2,6 @@ import android.app.Activity; import android.content.pm.ApplicationInfo; -import android.graphics.Rect; import android.os.Build; import android.os.Bundle; import android.text.Editable; @@ -15,12 +14,17 @@ import android.view.SurfaceView; import android.view.View; import android.view.ViewGroup; +import android.view.WindowManager; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputConnectionWrapper; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowCompat; +import androidx.core.view.WindowInsetsCompat; import java.io.File; @@ -29,8 +33,8 @@ * * A plain Activity owning a SurfaceView: the Rust side keeps one process-lived * render thread and GPUI App, so recreating this Activity only swaps the surface. - * The window uses adjustResize, so the SurfaceView (and GPUI's viewport) shrinks - * above the software keyboard. + * The SurfaceView fills the window, behind the system bars, display cutout and + * software keyboard; their insets go to Rust, and the app shell pads by them. * * The IME proxy is adapted from gpui-mobile's GpuiInputActivity: an invisible * EditText whose InputConnection forwards composing/commit/delete to Rust. @@ -77,19 +81,29 @@ protected void onCreate(Bundle state) { surface.setOnTouchListener((view, event) -> forwardMotion(event)); setContentView(surface); - // The keyboard shrinks the content via adjustResize; report its height so the - // diagnostics screen can show it next to GPUI's own viewport. - final View root = getWindow().getDecorView(); - root.getViewTreeObserver().addOnGlobalLayoutListener(() -> { - Rect visible = new Rect(); - root.getWindowVisibleDisplayFrame(visible); - int hidden = root.getHeight() - visible.bottom; - boolean shown = hidden > root.getHeight() / 5; + // Edge to edge: with the decor no longer fitting system windows, adjustResize + // stops shrinking the window for the keyboard, so the IME is an inset too. + WindowCompat.setDecorFitsSystemWindows(getWindow(), false); + if (Build.VERSION.SDK_INT >= 28) { + // Also under the cutout in landscape, rather than letterboxed beside it. + getWindow().getAttributes().layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + } + if (Build.VERSION.SDK_INT >= 29) { + // No scrim over 3-button navigation: GPUI paints what is underneath. + getWindow().setNavigationBarContrastEnforced(false); + } + ViewCompat.setOnApplyWindowInsetsListener(surface, (view, insets) -> { + Insets bars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()); + int ime = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom; + boolean shown = insets.isVisible(WindowInsetsCompat.Type.ime()); if (shown != keyboardVisible) { keyboardVisible = shown; - Log.i(TAG, "software keyboard " + (shown ? "shown" : "hidden") + " (" + hidden + " px)"); + Log.i(TAG, "software keyboard " + (shown ? "shown" : "hidden") + " (" + ime + " px)"); } - nativeKeyboardInsets(shown, shown ? hidden : 0); + nativeInsets(hostId, bars.top, bars.bottom, bars.left, bars.right, shown ? ime : 0); + return insets; }); nativeOnCreate(this, Build.VERSION.SDK_INT); @@ -207,6 +221,15 @@ private static boolean isNavigationOrShortcut(KeyEvent event) { } } + /** Dark status and navigation bar icons over a light app theme. Called from Rust. */ + public void gpuiSetLightSystemBars(boolean light) { + runOnUiThread(() -> { + View decor = getWindow().getDecorView(); + WindowCompat.getInsetsController(getWindow(), decor).setAppearanceLightStatusBars(light); + WindowCompat.getInsetsController(getWindow(), decor).setAppearanceLightNavigationBars(light); + }); + } + // ── IME bridge (called from Rust on the render thread) ───────────────── /** @@ -439,7 +462,7 @@ private void endEdit() { private static native void nativeHostDestroyed(long host); private static native void nativeResumed(long host); private static native void nativePaused(long host); - private static native void nativeKeyboardInsets(boolean visible, int heightPx); + private static native void nativeInsets(long host, int top, int bottom, int left, int right, int ime); private static native boolean nativeMotion(int action, int actionIndex, int[] ids, float[] xs, float[] ys); private static native void nativeKey(int keyCode, int action, int metaState); private static native void nativeIme(long session, int kind, String text, int start, int end); diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index d0121fd..75c7426 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -2,7 +2,7 @@ diff --git a/docs/demos/safe-area-keyboard.png b/docs/demos/safe-area-keyboard.png new file mode 100644 index 0000000..51a4025 Binary files /dev/null and b/docs/demos/safe-area-keyboard.png differ diff --git a/docs/demos/safe-area-landscape.png b/docs/demos/safe-area-landscape.png new file mode 100644 index 0000000..de71021 Binary files /dev/null and b/docs/demos/safe-area-landscape.png differ diff --git a/docs/demos/safe-area-portrait.png b/docs/demos/safe-area-portrait.png new file mode 100644 index 0000000..3b419c2 Binary files /dev/null and b/docs/demos/safe-area-portrait.png differ diff --git a/docs/safe-area-insets.md b/docs/safe-area-insets.md index c15b0f9..d348944 100644 --- a/docs/safe-area-insets.md +++ b/docs/safe-area-insets.md @@ -1,6 +1,25 @@ # Safe-area insets on the host-driven path: what it would take -Status: not started. The pieces are known; they need their own PRs. +Status: done on the fork. Steps 1–2 are gpui-mobile branch +`android-host-insets` (draft upstream PR in +[upstream/safe-area-insets.md](upstream/safe-area-insets.md)); steps 3–5 are +in the lab. Three choices differ from the plan below: + +- GPUI does have a safe-area concept: gpui-pre 0.3.8 has + `PlatformWindow::insets()` (`WindowInsets { safe_area, ime }`), + `on_insets_changed` and `Window::fully_visible_bounds()`. gpui-mobile + implements them, and the lab shell pads by `fully_visible_bounds()`. The + plan below missed this. +- The keyboard is passed in as a fifth value (`ime`): it becomes GPUI's `ime` + inset and feeds `gpui_mobile::keyboard_height()`, which iOS already fills. + Shrinking the `SurfaceView` in Java would have rebuilt the swapchain on + every keyboard toggle, and apps would get a different contract on Android + than on iOS. Kit's input reveal had to follow `fully_visible_bounds()` + instead of the viewport size (Kit `input-reveal-on-focus`). +- `safe_area_insets()` reads the active window from the platform, not a + host-path global, so the `android-activity` path gets it as well. + +The original plan follows. ## Where things stand diff --git a/docs/upstream/safe-area-insets.md b/docs/upstream/safe-area-insets.md new file mode 100644 index 0000000..b512076 --- /dev/null +++ b/docs/upstream/safe-area-insets.md @@ -0,0 +1,99 @@ +# Draft: longbridge/gpui-mobile — safe-area and keyboard insets on the host path + +Not posted. Branch +[`Bombatomica64/gpui-mobile:android-host-insets`](https://github.com/Bombatomica64/gpui-mobile/tree/android-host-insets), +one commit on upstream `main` (f9fe5a7), 4 files, +155 −6. It applies to +upstream as is; on our fork `main` it only conflicts next to +`configuration_changed` (#30). + +--- + +**Title:** android: Let host-driven apps report safe-area and keyboard insets + +### What + +A host that draws behind the system bars (edge to edge, which SDK 35 makes +the default) has no way to tell GPUI where the bars, the display cutout and +the software keyboard are. GPUI already models this +(`PlatformWindow::insets()` → `WindowInsets { safe_area, ime }`, +`on_insets_changed`, `Window::fully_visible_bounds()`), but the Android +window returns the default, zero, insets, and +`gpui_mobile::safe_area_insets()` returns zeros on Android. + +This adds one entry point to `android::host`, in the style of the other host +calls: + +```rust +pub fn insets_changed(host: HostId, safe_area: SafeAreaInsets, ime: f32) +``` + +- Physical pixels relative to the surface, from + `View.OnApplyWindowInsetsListener` (`Type.systemBars() | Type.displayCutout()` + and `Type.ime()`). +- The render thread stores them on that host's window. If the window does + not exist yet, they are kept until it opens. +- `AndroidPlatformWindow` implements `insets()` and `on_insets_changed`, so + GPUI refreshes the window when they change (an inset change does not + always come with a resize) and `Window::fully_visible_bounds()` excludes + them. +- `safe_area_insets()` returns the active Android window's insets (from + either entry point), and the attached host's keyboard feeds + `keyboard_height()`, so those two calls also match iOS. + +A host that keeps its surface between the bars does not call it, and +everything stays zero, as today. + +### Host side (Java) + +```java +WindowCompat.setDecorFitsSystemWindows(getWindow(), false); +ViewCompat.setOnApplyWindowInsetsListener(surfaceView, (view, insets) -> { + Insets bars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()); + int ime = insets.isVisible(WindowInsetsCompat.Type.ime()) + ? insets.getInsets(WindowInsetsCompat.Type.ime()).bottom : 0; + nativeInsets(hostId, bars.top, bars.bottom, bars.left, bars.right, ime); + return insets; +}); +``` + +and in Rust: + +```rust +host::insets_changed(host, SafeAreaInsets { top, bottom, left, right }, ime); +``` + +An app pads by what GPUI reports: + +```rust +let viewport = window.viewport_size(); +let visible = window.fully_visible_bounds(); +div() + .size_full() + .pt(visible.top()) + .pb(viewport.height - visible.bottom()) + .pl(visible.left()) + .pr(viewport.width - visible.right()) +``` + +### On a device + +OnePlus CPH2581 (Android 16, punch-hole cutout), in the +[GPUI Mobile Lab](https://github.com/Bombatomica64/Gpui-android). The app shell +pads by the insets and paints its title bar color underneath them. The +Diagnostics row shows the values GPUI gets, in logical pixels. + +| Portrait | Landscape (cutout on the left) | Keyboard open | +| --- | --- | --- | +| ![portrait](../demos/safe-area-portrait.png) | ![landscape](../demos/safe-area-landscape.png) | ![keyboard](../demos/safe-area-keyboard.png) | + +### Not in this PR + +- GPUI Kit's focused-input reveal followed the viewport size, so with the + keyboard as an inset it no longer scrolled the field into view. That is a + separate Kit change (reveal on `fully_visible_bounds()`), used in the + screenshots. +- Animating with the keyboard (`WindowInsetsAnimation`). The inset arrives + once the keyboard has settled. + +Thanks for looking at this. diff --git a/src/app.rs b/src/app.rs index 167fec7..98a3d9c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -40,6 +40,8 @@ pub struct LabApp { screen: Option, catalog_scroll: ScrollHandle, screen_scroll: ScrollHandle, + /// Last bar appearance sent to the host, so it is only sent on change. + light_system_bars: Option, _subscriptions: Vec, } @@ -82,6 +84,7 @@ impl LabApp { screen: None, catalog_scroll: ScrollHandle::new(), screen_scroll: ScrollHandle::new(), + light_system_bars: None, _subscriptions: vec![subscription, appearance], } } @@ -412,19 +415,39 @@ impl LabApp { if window.focused(cx).is_none() { self.focus.focus(window, cx); } + let light = !cx.theme().is_dark(); + if self.light_system_bars != Some(light) { + self.light_system_bars = Some(light); + #[cfg(target_os = "android")] + crate::host::set_light_system_bars(light); + } let body = match &self.screen { Some(screen) => self.render_screen(screen, cx).into_any_element(), None => self.render_catalog(window, cx).into_any_element(), }; - v_flex() - .key_context(CONTEXT) - .track_focus(&self.focus) - .on_action(cx.listener(Self::go_back)) + // The window is edge to edge: keep the content inside the part GPUI knows + // is clear of the system bars, the cutout and the keyboard, with the + // title bar color underneath them. + let viewport = window.viewport_size(); + let visible = window.fully_visible_bounds(); + div() .size_full() - .bg(cx.theme().background) - .text_color(cx.theme().foreground) - .font_family(cx.theme().font_family.clone()) - .child(self.render_header(window, cx)) - .child(body) + .pt(visible.top()) + .pb(viewport.height - visible.bottom()) + .pl(visible.left()) + .pr(viewport.width - visible.right()) + .bg(cx.theme().title_bar) + .child( + v_flex() + .key_context(CONTEXT) + .track_focus(&self.focus) + .on_action(cx.listener(Self::go_back)) + .size_full() + .bg(cx.theme().background) + .text_color(cx.theme().foreground) + .font_family(cx.theme().font_family.clone()) + .child(self.render_header(window, cx)) + .child(body), + ) } } diff --git a/src/host.rs b/src/host.rs index cf45c12..4bfd4d8 100644 --- a/src/host.rs +++ b/src/host.rs @@ -165,14 +165,27 @@ pub extern "system" fn Java_dev_gpui_mobile_lab_LabActivity_nativePaused<'local> host::paused(host as host::HostId); } +/// Physical pixels relative to the `SurfaceView`, which fills the window: the +/// system bars and display cutout, and the software keyboard (0 when hidden). #[unsafe(no_mangle)] -pub extern "system" fn Java_dev_gpui_mobile_lab_LabActivity_nativeKeyboardInsets<'local>( +pub extern "system" fn Java_dev_gpui_mobile_lab_LabActivity_nativeInsets<'local>( _env: EnvUnowned<'local>, _this: JObject<'local>, - visible: bool, - height_px: i32, + host: i64, + top: i32, + bottom: i32, + left: i32, + right: i32, + ime: i32, ) { - diagnostics::set_keyboard(visible, height_px); + diagnostics::set_keyboard(ime > 0, ime); + let safe_area = gpui_mobile::android::SafeAreaInsets { + top: top as f32, + bottom: bottom as f32, + left: left as f32, + right: right as f32, + }; + host::insets_changed(host as host::HostId, safe_area, ime as f32); } /// `ids`, `xs`, `ys` hold every pointer of the `MotionEvent` in index order, @@ -272,3 +285,25 @@ pub fn move_task_to_back() { log::error!("moveTaskToBack failed: {err}"); } } + +/// Dark system bar icons for a light theme, light ones for a dark theme: the +/// window is edge to edge, so the bars sit over GPUI's own colors. +pub fn set_light_system_bars(light: bool) { + let result = mobile_jni::with_env(|env| { + let activity = mobile_jni::activity(env)?; + env.call_method( + &activity, + jni::jni_str!("gpuiSetLightSystemBars"), + jni::jni_sig!("(Z)V"), + &[jni::objects::JValue::Bool(light)], + ) + .map_err(|e| { + env.exception_clear(); + e.to_string() + })?; + Ok(()) + }); + if let Err(err) = result { + log::error!("gpuiSetLightSystemBars failed: {err}"); + } +} diff --git a/src/lib.rs b/src/lib.rs index fa88856..4ea9cae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,7 +15,7 @@ use gpui::{App, AppContext as _, WindowOptions}; pub const GPUI_KIT_VERSION: &str = "0.7.1"; pub const GPUI_VERSION: &str = "gpui-pre 0.3.8"; -pub const GPUI_MOBILE_REVISION: &str = "Bombatomica64/gpui-mobile 2456b8e"; +pub const GPUI_MOBILE_REVISION: &str = "Bombatomica64/gpui-mobile c71a31a"; /// Runs on the GPUI render thread once the first surface exists. pub fn launch(cx: &mut App) { diff --git a/src/screens/diagnostics.rs b/src/screens/diagnostics.rs index 98f6268..2a18145 100644 --- a/src/screens/diagnostics.rs +++ b/src/screens/diagnostics.rs @@ -73,6 +73,8 @@ impl DiagnosticsScreen { let viewport = window.viewport_size(); let visual = window.visual_viewport_bounds(); let (keyboard, keyboard_px) = host::keyboard(); + let (top, bottom, left, right) = gpui_mobile::safe_area_insets(); + let visible_area = window.fully_visible_bounds(); let focused_input = window.focused_input(cx).is_some(); let focused = window.focused(cx).map(|handle| format!("{handle:?}")); let touch = host::last_touch(); @@ -121,6 +123,23 @@ impl DiagnosticsScreen { "Keyboard (host)", format!("visible={keyboard}, {keyboard_px} px"), ), + ( + "Safe area (logical)", + format!( + "top {top:.0} · bottom {bottom:.0} · left {left:.0} · right {right:.0} · keyboard {:.0}", + gpui_mobile::keyboard_height() + ), + ), + ( + "Fully visible (GPUI)", + format!( + "origin {:.0},{:.0} size {:.0} × {:.0}", + visible_area.origin.x.as_f32(), + visible_area.origin.y.as_f32(), + visible_area.size.width.as_f32(), + visible_area.size.height.as_f32() + ), + ), ("Focused text input", focused_input.to_string()), ("Focused handle", focused.unwrap_or_else(|| "none".into())), ("Screen scroll y", format!("{} pt", host::screen_scroll())),