Fix WebView system bar and keyboard insets

This commit is contained in:
2026-08-09 22:44:13 +02:00
parent 164bd2130a
commit f417fb7331
4 changed files with 58 additions and 7 deletions

View File

@@ -46,7 +46,7 @@ conversation and start fresh.
- **A build machine** with the Android SDK + NDK. Easiest path: open the project - **A build machine** with the Android SDK + NDK. Easiest path: open the project
in **Android Studio** (it will offer to install the matching NDK `27.2.12479018` in **Android Studio** (it will offer to install the matching NDK `27.2.12479018`
and CMake `3.22.1`). and CMake `3.22.1`).
- **A phone:** 64-bit ARM (`arm64-v8a`), Android 8.0+ (minSdk 26), and enough free - **A phone:** 64-bit ARM (`arm64-v8a`), Android 11+ (minSdk 30), and enough free
RAM to hold a ~1.5 GB Q6 model (~4 GB RAM device recommended). RAM to hold a ~1.5 GB Q6 model (~4 GB RAM device recommended).
- Network on first launch to download the model. - Network on first launch to download the model.

View File

@@ -13,10 +13,10 @@ android {
defaultConfig { defaultConfig {
applicationId = "monster.autisme.tsjetpiti" applicationId = "monster.autisme.tsjetpiti"
minSdk = 26 minSdk = 30
targetSdk = 35 targetSdk = 35
versionCode = 1 versionCode = 2
versionName = "0.1.0" versionName = "0.1.1"
// On-device LLM => 64-bit ARM only. Keeps the APK from ballooning. // On-device LLM => 64-bit ARM only. Keeps the APK from ballooning.
ndk { ndk {

View File

@@ -7,6 +7,12 @@
--bubble: #141414; --bubble: #141414;
--bubble-line: #242424; --bubble-line: #242424;
/* Window insets (status/nav bar, cutout, keyboard) — set from Android. */
--inset-top: 0px;
--inset-right: 0px;
--inset-bottom: 0px;
--inset-left: 0px;
/* Generic, corporate/IT sans stack — resolves offline on any device. */ /* Generic, corporate/IT sans stack — resolves offline on any device. */
--font: "Segoe UI", "Helvetica Neue", Helvetica, Arial, "Liberation Sans", --font: "Segoe UI", "Helvetica Neue", Helvetica, Arial, "Liberation Sans",
system-ui, sans-serif; system-ui, sans-serif;
@@ -37,7 +43,8 @@ body {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: max(env(safe-area-inset-top), 12px) 16px 12px; padding: calc(var(--inset-top) + 12px) calc(var(--inset-right) + 16px) 12px
calc(var(--inset-left) + 16px);
background: var(--panel); background: var(--panel);
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
flex: 0 0 auto; flex: 0 0 auto;
@@ -64,7 +71,7 @@ body {
#chat { #chat {
flex: 1 1 auto; flex: 1 1 auto;
overflow-y: auto; overflow-y: auto;
padding: 18px 16px; padding: 18px calc(var(--inset-right) + 16px) 18px calc(var(--inset-left) + 16px);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
@@ -99,7 +106,8 @@ body {
display: flex; display: flex;
gap: 10px; gap: 10px;
align-items: flex-end; align-items: flex-end;
padding: 12px 14px calc(env(safe-area-inset-bottom) + 12px); padding: 12px calc(var(--inset-right) + 14px) calc(var(--inset-bottom) + 12px)
calc(var(--inset-left) + 14px);
background: var(--panel); background: var(--panel);
border-top: 1px solid var(--line); border-top: 1px solid var(--line);
flex: 0 0 auto; flex: 0 0 auto;

View File

@@ -2,7 +2,9 @@ package monster.autisme.tsjetpiti
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.view.WindowInsets
import android.webkit.JavascriptInterface import android.webkit.JavascriptInterface
import android.webkit.WebSettings import android.webkit.WebSettings
import android.webkit.WebView import android.webkit.WebView
@@ -32,6 +34,12 @@ class MainActivity : Activity() {
@Volatile private var handle: Long = 0L @Volatile private var handle: Long = 0L
@Volatile private var setupStarted = false @Volatile private var setupStarted = false
// Window insets (status/nav bar, cutout, keyboard) in CSS px, pushed to the page.
private var insetTop = 0
private var insetBottom = 0
private var insetLeft = 0
private var insetRight = 0
companion object { companion object {
private const val N_CTX = 4096 private const val N_CTX = 4096
private const val MAX_TOKENS = 220 private const val MAX_TOKENS = 220
@@ -55,6 +63,30 @@ class MainActivity : Activity() {
webView = WebView(this) webView = WebView(this)
setContentView(webView) setContentView(webView)
// Edge-to-edge (default on Android 15+): draw behind the system bars, then
// inset the WebView by the status bar, nav bar, display cutout, and IME so
// none of them ever cover the chat UI (portrait or landscape).
window.setDecorFitsSystemWindows(false)
webView.setBackgroundColor(Color.BLACK)
// WebView.setPadding doesn't inset HTML content (Chromium sizes 100dvh to the
// full view), so feed the insets to CSS as variables instead. This also tracks
// the IME: when the keyboard shows, bottom grows and the composer rises.
webView.setOnApplyWindowInsetsListener { _, insets ->
val bars = insets.getInsets(
WindowInsets.Type.systemBars() or
WindowInsets.Type.ime() or
WindowInsets.Type.displayCutout()
)
val d = resources.displayMetrics.density
insetTop = (bars.top / d).toInt()
insetBottom = (bars.bottom / d).toInt()
insetLeft = (bars.left / d).toInt()
insetRight = (bars.right / d).toInt()
pushInsets()
insets
}
webView.requestApplyInsets()
webView.settings.apply { webView.settings.apply {
javaScriptEnabled = true javaScriptEnabled = true
domStorageEnabled = true domStorageEnabled = true
@@ -63,6 +95,7 @@ class MainActivity : Activity() {
webView.addJavascriptInterface(Bridge(), "TsjetNative") webView.addJavascriptInterface(Bridge(), "TsjetNative")
webView.webViewClient = object : WebViewClient() { webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) { override fun onPageFinished(view: WebView?, url: String?) {
pushInsets() // apply insets now that the DOM exists
if (!setupStarted) { if (!setupStarted) {
setupStarted = true setupStarted = true
startSetup() startSetup()
@@ -80,6 +113,16 @@ class MainActivity : Activity() {
} }
} }
/** Push the current window insets to the page as CSS variables on :root. */
private fun pushInsets() {
val js = "var s=document.documentElement.style;" +
"s.setProperty('--inset-top','${insetTop}px');" +
"s.setProperty('--inset-bottom','${insetBottom}px');" +
"s.setProperty('--inset-left','${insetLeft}px');" +
"s.setProperty('--inset-right','${insetRight}px');"
runOnUiThread { webView.evaluateJavascript(js, null) }
}
private fun startSetup() = setupExecutor.execute { private fun startSetup() = setupExecutor.execute {
try { try {
callJs("onStatus", "Waking the tsjet…") callJs("onStatus", "Waking the tsjet…")