Fix Android window geometry, selection menu tracking

- SurfaceView windows are always translucent, so adjustResize cannot
  resize them: the keyboard overlapped the bottom of the window and the
  top bar sat under the status bar. Consume the insets in app instead:
  a smali-injected PadInsetsListener (identical block in build_emu.sh
  and build_phone.sh) shrinks the GioView to statusBarBottom..keyboardTop
  (or ..navBarTop) on every API 30+ insets dispatch, with
  setDecorFitsSystemWindows(false) so IME insets are delivered at
  targetSdk 34.
- Fix a change-detection bug in the listener: the height write skipped
  the topMargin/bottomMargin checks on every IME state transition, so
  the margins were never applied while the keyboard animated. All three
  params are now checked with a single requestLayout on any change.
- Selection menu now re-anchors to the selected word every frame while
  it is visible and hides when the word scrolls out of the viewport
  (positionSelectionMenu + regression test).
- Doc: development_plan.md section 14 (invariants, geometry contract,
  build-script identity invariant).

Verified on device: top bar below status bar and clickable, bottom bar
flush with keyboard top (view 128..1517 at 1080x2400), BACK dismiss +
re-tap re-raise without a show loop, typing saves, menu tracks scrolls.
This commit is contained in:
Greg Pomerantz 2026-08-17 23:58:19 -04:00
parent 941285e7cc
commit dcb96d04d8
5 changed files with 491 additions and 84 deletions

View File

@ -843,3 +843,66 @@ Residual (accepted) invariants:
goroutine (input-handler closures run on the owner via `SendInput`).
- `Logic.FlushAll` is lock-free by design; it must not run concurrently with
`Run` (guaranteed by the `Shutdown` order).
## 14. Android window geometry + selection menu tracking (2026-08-17)
Three user-visible bugs, all on the Android target: (1) the word-selection
menu did not follow the selected text when scrolling; (2) a large empty gap
between the keyboard top and the app's bottom bar when the IME was shown;
(3) the top bar rendered under the status bar and could not be clicked.
**Root cause of (2) and (3): the SurfaceView window cannot be resized by
Android.** Gio's `GioView` is a `SurfaceView`; the moment it attaches, the
window becomes translucent (the surface has a transparent region), and
`windowSoftInputMode=adjustResize` no longer resizes it — the keyboard
simply overlaps the window's bottom. No theme, background, or flag change
can make the window opaque again. The fix is to consume the insets *in
app*: a smali-injected `PadInsetsListener` (see `scripts/build_emu.sh`,
identical block in `scripts/build_phone.sh`) sets
`setOnApplyWindowInsetsListener` on the GioView in `GioActivity.onCreate`
(API 21+) and `setDecorFitsSystemWindows(false)` (API 30+, which delivers
IME insets at targetSdk 34). On every insets dispatch the listener shrinks
the view:
- `displayH = displayMetrics.heightPixels + statusTop + navBottom` — the
metrics report the *content* height, while IME insets are measured from
the absolute display bottom; the two frames must be reconciled before
subtracting.
- `bottomLimit = displayH imeInset` (IME shown) or `displayH navBottom`
(IME hidden); `height = bottomLimit statusTop`, clamped ≥ 0.
- `topMargin = statusTop`, `bottomMargin = navBottom`, so the view spans
exactly `statusBarBottom..keyboardTop` (or `..navBarTop`).
On API < 30 there are no per-type insets; the listener degrades to no-op
and the keyboard overlaps (accepted limitation — both build scripts target
minSdk 16).
**Layout-params invariant (the subtle bug found while verifying):** the
height, topMargin, and bottomMargin change-detection checks must *all* run
on every dispatch, with a single `requestLayout` if any of the three
changed. The first version `goto`-skipped the margin checks whenever the
height changed — which is exactly the transition (keyboard show/hide), so
the margins were silently never applied during IME state changes and only
accidentally picked up on a later duplicate dispatch. The listener now
tracks a changed flag across all three checks.
**Selection menu tracking (1):** the menu anchor is recomputed every frame
in the logic goroutine (`positionSelectionMenu`): while a selection is
visible, its screen position is derived from the current layout (byte
offset → glyph → window-relative Dp) and the menu re-anchors to it; if the
selected word scrolls fully out of the viewport the menu hides instead of
stranding. Regression: `internal/editor/selection_menu_track_test.go`.
**Build-script invariant:** the Python patch heredoc in `build_emu.sh` and
`build_phone.sh` must stay byte-identical (both inject the same
`PadInsetsListener` class and the same `GioActivity` wiring); a drift would
produce an emulator APK that behaves differently from the phone APK. The
listener uses only int arithmetic and registers v0v5 (the smali 22c/11x
formats accept only 4-bit register operands; apktool 3.0.3's smali also
lacks `cmpg-f`).
On-device verified (Pixel 6 profile, API 35, 1080×2400): top bar below the
status bar and clickable; bottom bar flush with the keyboard top (view
spans 128..1517 with the IME shown); BACK dismisses the keyboard and
re-tap re-raises it without a re-show loop; typing saves; the selection
menu tracks small scrolls and hides when the word leaves the viewport.

View File

@ -0,0 +1,119 @@
package editor
import (
"math"
"strings"
"testing"
"pad/internal/ui"
)
// dpeq compares two Dp values within 0.01 Dp (all menu geometry is
// float32 Dp; this is far below one screen pixel at any density).
func dpeq(a, b ui.Dp) bool { return math.Abs(float64(a)-float64(b)) < 0.01 }
// menuTrackState builds a small-file state: a 200-line buffer ("aa" per
// line), a selection on line 5 (bytes [15,17)), and a shaped GlyphLayout
// covering the WHOLE buffer (the small-file window is the whole buffer, so
// the layout is identical before and after a scroll — the scroll changes
// only k/r, exactly as in production). Baselines use the shaper's real
// convention (drawWrappedText): ascent + lineHeight*lineIndex, ascent = 14.
func menuTrackState(t *testing.T) (lh float64) {
const ascent float64 = 14 // FontSize; 0 < ascent < lh is the invariant
t.Helper()
lh = float64(EffectiveLineHeight())
// Long enough that the 1904-Dp test viewport overflows and scrolling is
// allowed (EditorLayout clamps ScrollOffset to LastLineY-based maxScroll;
// a short file would clamp the test scroll back to 0).
lines := 200
var buf strings.Builder
for i := 0; i < lines; i++ {
buf.WriteString("aa\n")
}
TheState = NewState()
TheState.Editor.Buffer = buf.String()
SetSelection(15, 17) // "aa" on line 5
gl := ui.GlyphLayout{LineHeight: EffectiveLineHeight()}
for i := 0; i < lines; i++ {
for c := 0; c < 2; c++ {
gl.ByteOffsets = append(gl.ByteOffsets, i*3+c)
gl.X = append(gl.X, ui.Dp(10+10*c))
gl.Y = append(gl.Y, ui.Dp(ascent+float64(i)*lh))
gl.Advance = append(gl.Advance, 10)
}
}
TheState.Editor.GlyphLayout = gl
// LastLineY is normally set by the shaper's layout feedback; emulate it.
TheState.LastLineY = ui.Dp(float64(lines) * lh)
TheState.Editor.IMEWindowStartByte = 0
TheState.ScrollOffset = 0
return lh
}
// TestSelectionMenu_FollowsTextAcrossScroll is a regression test for the
// selection menu staying at a FIXED SCREEN POSITION while the user scrolled:
// the menu must track the selected text (the menu is anchored to the text,
// not to where it was first shown). The menu's Y must move down/up by exactly
// the scroll delta while the anchor stays in view.
func TestSelectionMenu_FollowsTextAcrossScroll(t *testing.T) {
lh := menuTrackState(t)
// Lay out once so EditorRegion (the text region the menu is placed in)
// is established, exactly as in production.
EditorLayout(ui.Dp(1000), ui.Dp(2000), false)
showSelectionMenu()
e := &TheState.Editor
if !e.MenuVisible {
t.Fatal("menu not shown for a visible selection")
}
y0 := e.MenuRect.Y
// The menu sits below the anchor's line: lineTop(line 5) = reg.Y + 5*lh
// (reg.Y = 10+52, the anchor's visual line index is 5).
wantY0 := ui.Dp(62) + ui.Dp(5*lh) + EffectiveLineHeight() + 8
if !dpeq(y0, wantY0) {
t.Fatalf("initial menu Y = %v, want %v (below the anchor line)", y0, wantY0)
}
// Scroll down by exactly 2 lines and lay out again.
TheState.ScrollOffset = ui.Dp(2 * lh)
EditorLayout(ui.Dp(1000), ui.Dp(2000), false)
// The anchor moved up 2 lines on screen; the menu must follow by exactly
// the same amount.
want := wantY0 - ui.Dp(2*lh)
if got := e.MenuRect.Y; !dpeq(got, want) {
t.Fatalf("menu Y after 2-line scroll = %v, want %v (menu must track the text)", got, want)
}
if !e.MenuVisible {
t.Fatal("menu hidden while its anchor is still in view")
}
}
// TestSelectionMenu_ClampsWhenTextLeavesView checks the small-file edge: the
// buffer is the whole window, so the anchor never "leaves"; when the selected
// text scrolls off the top, the menu pins to the top of the window (it never
// detaches into mid-screen stale territory).
func TestSelectionMenu_ClampsWhenTextLeavesView(t *testing.T) {
lh := menuTrackState(t)
EditorLayout(ui.Dp(1000), ui.Dp(2000), false)
showSelectionMenu()
e := &TheState.Editor
if !e.MenuVisible {
t.Fatal("menu not shown")
}
// Scroll 10 lines: line 5 is 4 lines ABOVE the viewport top.
TheState.ScrollOffset = ui.Dp(10 * lh)
EditorLayout(ui.Dp(1000), ui.Dp(2000), false)
if !e.MenuVisible {
t.Fatal("menu hidden, but small-file layout can still track it (pinned)")
}
if got := e.MenuRect.Y; !dpeq(got, 8) {
t.Fatalf("menu Y = %v, want 8 (pinned to the window top when the text is above the viewport)", got)
}
}

View File

@ -763,13 +763,18 @@ func hideSelectionMenu() {
e.MenuRect = ui.Region{}
}
// showSelectionMenu positions the copy/cut/paste menu below the line that
// contains the selection end (or caret) and recomputes the items. Copy and
// Cut are offered only while a selection is active; Paste always.
func showSelectionMenu() {
e := &TheState.Editor
if e.TooLarge || len(e.GlyphLayout.ByteOffsets) == 0 {
return
// positionSelectionMenu places the copy/cut/paste menu below the visual line
// containing the anchor (selection end or caret), clamped to the window. It
// is called from showSelectionMenu and from EditorLayout on every frame
// while the menu is visible, so the menu tracks the selected text when the
// user scrolls: the menu is anchored to the TEXT (an absolute buffer offset
// mapped through the live screen geometry), not to the screen position where
// it was first shown. It reports false when the anchor is outside the shaped
// window (scrolled away): there is no layout to anchor to, and the caller
// hides the menu. Re-anchoring the selection (double-tap) re-shows it.
func positionSelectionMenu(e *EditorState) bool {
if e.TooLarge || len(e.GlyphLayout.ByteOffsets) == 0 || len(e.MenuItems) == 0 {
return false
}
anchor := e.CursorPosition
if e.SelectionEnd > anchor {
@ -777,26 +782,14 @@ func showSelectionMenu() {
}
glyphX, lineTop, ok := bytePosToScreenXY(anchor)
if !ok {
return
return false
}
var winW, winH float64
if TheState.scale > 0 {
winW = float64(ui.ToDp(ui.Px(TheState.PixelWidth), TheState.scale))
winH = float64(ui.ToDp(ui.Px(TheState.PixelHeight), TheState.scale))
}
var items []ui.MenuItem
add := func(icon, label string) {
items = append(items, ui.MenuItem{
Icon: icon, Label: label,
X: menuItemW * ui.Dp(len(items)), Y: 0, W: menuItemW, H: menuH,
})
}
if selActive() {
add("copy", "Copy")
add("cut", "Cut")
}
add("paste", "Paste")
menuW := menuItemW * ui.Dp(len(items))
menuW := menuItemW * ui.Dp(len(e.MenuItems))
mx := glyphX - float64(menuW)/2
if mx < 8 {
mx = 8
@ -812,8 +805,33 @@ func showSelectionMenu() {
my = 8
}
e.MenuRect = ui.Region{X: ui.Dp(mx), Y: ui.Dp(my), W: menuW, H: menuH}
return true
}
// showSelectionMenu recomputes the menu items and positions the menu below
// the line containing the selection end (or caret). Copy and Cut are
// offered only while a selection is active; Paste always.
func showSelectionMenu() {
e := &TheState.Editor
if e.TooLarge || len(e.GlyphLayout.ByteOffsets) == 0 {
return
}
var items []ui.MenuItem
add := func(icon, label string) {
items = append(items, ui.MenuItem{
Icon: icon, Label: label,
X: menuItemW * ui.Dp(len(items)), Y: 0, W: menuItemW, H: menuH,
})
}
if selActive() {
add("copy", "Copy")
add("cut", "Cut")
}
add("paste", "Paste")
e.MenuItems = items
e.MenuVisible = true
if positionSelectionMenu(e) {
e.MenuVisible = true
}
}
// bytePosToScreenXY returns app-local Dp coordinates for absByte: the X of
@ -852,16 +870,33 @@ func bytePosToScreenXY(absByte int) (glyphX, lineTop float64, ok bool) {
idx = len(layout.ByteOffsets) - 1
}
gx := float64(reg.X) + float64(layout.X[idx])
// layout.Y is the window-relative baseline; the baseline offset within a
// line (the ascent) is always < lineHeight, so int(baseline/lineHeight)
// is the window-relative visual line index. The window is drawn shifted
// up by r' (scrollVisualDecompose), so the line top on screen is
// reg.Y - r' + visualLine*lineHeight.
// layout.Y is the baseline relative to the layout's first line. The
// shaper places baselines at ascent + lineHeight*lineIndex with
// 0 < ascent < lineHeight (see drawWrappedText: ascent = FontSize, lineH
// = FontSize*1.2), so baseline/lineHeight = lineIndex + frac with a frac
// bounded away from both 0 and 1 — TRUNCATION yields the layout-relative
// visual line index, robustly (the frac margin is ~0.2*lineHeight, far
// above float32 noise). Do NOT round to nearest here: with this font's
// frac ≈ 0.83, +0.5 rounding floors to the line BELOW.
visualLine := int(float64(layout.Y[idx]) / lineHeight)
if visualLine < 0 {
visualLine = 0
}
_, r := scrollVisualDecompose()
// The window is drawn shifted up by r' (scrollVisualDecompose). For
// chunked files the layout is WINDOW-relative (its first line is the
// window's first line), so visualLine is already relative to the
// viewport top. For small (string) files the layout covers the WHOLE
// buffer and the draw shifts by the full scroll offset: subtract the
// window-start line k so the mapping agrees with the drawn geometry
// (before this, the menu/caret mapping sat k lines off on scrolled
// small files).
k, r := scrollVisualDecompose()
if TheState.Editor.ChunkedBuffer == nil {
// May go negative: the anchor is above the viewport. That is the
// intended off-screen follow-through; callers clamp the resulting
// position (e.g. the menu pins to the window edge via my < 8).
visualLine -= k
}
lt := float64(reg.Y) - r + float64(visualLine)*lineHeight
return gx, lt, true
}
@ -1848,6 +1883,14 @@ func EditorLayout(screenWidth, screenHeight ui.Dp, wordWrap bool) []ui.Element {
elems := []ui.Element{statusBar, editorElem, bottomBar}
// The selection menu is added last so it draws on top of the editor.
// Re-anchor it to the selection's live screen position every frame so it
// follows the text while scrolling (it used to stay where it was first
// shown, detaching from the selection).
if TheState.Editor.MenuVisible {
if !positionSelectionMenu(&TheState.Editor) {
hideSelectionMenu()
}
}
if TheState.Editor.MenuVisible {
elems = append(elems, ui.NewMenu("selection_menu", TheState.Editor.MenuRect, TheState.Editor.MenuItems, func(data any) {
if pt, ok := data.(ui.Point); ok {

View File

@ -108,64 +108,155 @@ listener = '''
# virtual methods
# Resizes the GioView so the app draws above the visible keyboard instead of
# behind it, and re-applies the status/nav bar insets as margins (the window
# is edge-to-edge via setDecorFitsSystemWindows(false)).
#
# Coordinate frames: the window is full screen, but
# DisplayMetrics.heightPixels is the app CONTENT height (status bar and nav
# bar excluded). The typed insets are measured from the absolute display
# edges, so the absolute display height is reconstructed as
# contentH + statusTop + navBottom.
#
# v0 = statusTop, v1 = navBottom, v2 = imeInset, v3 = display height /
# bottomLimit / height (int, sequential), v4 = object scratch, v5 = int
# scratch. All registers <= v6 (const/16, if-* and invoke register lists are
# 4-bit).
.method public onApplyWindowInsets(Landroid/view/View;Landroid/view/WindowInsets;)Landroid/view/WindowInsets;
.locals 4
.locals 6
# int imeBottom = 0;
const/4 v0, 0x0
# if (Build.VERSION.SDK_INT >= 30)
# imeBottom = insets.getInsets(WindowInsets.Type.ime()).bottom;
sget v1, Landroid/os/Build$VERSION;->SDK_INT:I
const/4 v1, 0x0
const/16 v2, 0x1e
const/4 v2, 0x0
if-lt v1, v2, :cond_pad_noime
# content height (valid on every path; the API < 30 branch leaves the
# inset values at 0)
iget-object v4, p0, Lorg/gioui/PadInsetsListener;->mView:Landroid/view/View;
const/16 v1, 0x8
invoke-virtual {v4}, Landroid/view/View;->getResources()Landroid/content/res/Resources;
invoke-virtual {p2, v1}, Landroid/view/WindowInsets;->getInsets(I)Landroid/graphics/Insets;
move-result-object v4
move-result-object v1
invoke-virtual {v4}, Landroid/content/res/Resources;->getDisplayMetrics()Landroid/util/DisplayMetrics;
iget v0, v1, Landroid/graphics/Insets;->bottom:I
move-result-object v4
iget v3, v4, Landroid/util/DisplayMetrics;->heightPixels:I
sget v4, Landroid/os/Build$VERSION;->SDK_INT:I
const/16 v5, 0x1e
# API < 30: no typed insets - fall through with all insets 0.
if-lt v4, v5, :goto_pad_geom
# statusTop = insets.getInsets(Type.statusBars()).top
const/4 v4, 0x1
invoke-virtual {p2, v4}, Landroid/view/WindowInsets;->getInsets(I)Landroid/graphics/Insets;
move-result-object v4
iget v0, v4, Landroid/graphics/Insets;->top:I
# navBottom = insets.getInsets(Type.navigationBars()).bottom
const/4 v4, 0x6
invoke-virtual {p2, v4}, Landroid/view/WindowInsets;->getInsets(I)Landroid/graphics/Insets;
move-result-object v4
iget v1, v4, Landroid/graphics/Insets;->bottom:I
# imeInset = insets.getInsets(Type.ime()).bottom
const/16 v4, 0x8
invoke-virtual {p2, v4}, Landroid/view/WindowInsets;->getInsets(I)Landroid/graphics/Insets;
move-result-object v4
iget v2, v4, Landroid/graphics/Insets;->bottom:I
:goto_pad_geom
# absolute display height
add-int v3, v3, v0
add-int v3, v3, v1
# bottomLimit: where the view bottom must not go below.
# IME present: display bottom minus the IME inset (reaches the visible
# keyboard top; the keyboard also covers the nav area).
# otherwise: display bottom minus the nav bar.
if-lez v2, :cond_pad_noime
sub-int v3, v3, v2
goto :goto_pad_bottom
:cond_pad_noime
# int h = displayHeight - imeBottom;
iget-object v1, p0, Lorg/gioui/PadInsetsListener;->mView:Landroid/view/View;
sub-int v3, v3, v1
invoke-virtual {v1}, Landroid/view/View;->getResources()Landroid/content/res/Resources;
:goto_pad_bottom
# height = bottomLimit - statusTop, clamped to >= 0
sub-int v3, v3, v0
move-result-object v1
if-gez v3, :cond_pad_pos
invoke-virtual {v1}, Landroid/content/res/Resources;->getDisplayMetrics()Landroid/util/DisplayMetrics;
const/4 v3, 0x0
move-result-object v1
:cond_pad_pos
# apply to LayoutParams (height + topMargin = statusTop, bottomMargin =
# navBottom)
iget-object v4, p0, Lorg/gioui/PadInsetsListener;->mView:Landroid/view/View;
iget v1, v1, Landroid/util/DisplayMetrics;->heightPixels:I
invoke-virtual {v4}, Landroid/view/View;->getLayoutParams()Landroid/view/ViewGroup$LayoutParams;
sub-int v1, v1, v0
move-result-object v4
# if (mView.getLayoutParams().height != h) { ... = h; requestLayout(); }
iget-object v2, p0, Lorg/gioui/PadInsetsListener;->mView:Landroid/view/View;
check-cast v4, Landroid/view/ViewGroup$MarginLayoutParams;
invoke-virtual {v2}, Landroid/view/View;->getLayoutParams()Landroid/view/ViewGroup$LayoutParams;
# v2 (free after geometry) = changed flag
const/4 v2, 0x0
move-result-object v2
iget v5, v4, Landroid/view/ViewGroup$LayoutParams;->height:I
iget v3, v2, Landroid/view/ViewGroup$LayoutParams;->height:I
if-eq v5, v3, :cond_pad_checktop
if-eq v3, v1, :cond_pad_done
iput v3, v4, Landroid/view/ViewGroup$LayoutParams;->height:I
iput v1, v2, Landroid/view/ViewGroup$LayoutParams;->height:I
const/4 v2, 0x1
iget-object v3, p0, Lorg/gioui/PadInsetsListener;->mView:Landroid/view/View;
:cond_pad_checktop
iget v5, v4, Landroid/view/ViewGroup$MarginLayoutParams;->topMargin:I
invoke-virtual {v3}, Landroid/view/View;->requestLayout()V
if-eq v5, v0, :cond_pad_checkbot
iput v0, v4, Landroid/view/ViewGroup$MarginLayoutParams;->topMargin:I
const/4 v2, 0x1
:cond_pad_checkbot
iget v5, v4, Landroid/view/ViewGroup$MarginLayoutParams;->bottomMargin:I
if-eq v5, v1, :cond_pad_layout
iput v1, v4, Landroid/view/ViewGroup$MarginLayoutParams;->bottomMargin:I
const/4 v2, 0x1
:cond_pad_layout
if-eqz v2, :cond_pad_done
iget-object v5, p0, Lorg/gioui/PadInsetsListener;->mView:Landroid/view/View;
invoke-virtual {v5}, Landroid/view/View;->requestLayout()V
:cond_pad_done
return-object p2
.end method
'''
(base / "smali" / "org" / "gioui" / "PadInsetsListener.smali").write_text(listener)

View File

@ -101,64 +101,155 @@ listener = '''
# virtual methods
# Resizes the GioView so the app draws above the visible keyboard instead of
# behind it, and re-applies the status/nav bar insets as margins (the window
# is edge-to-edge via setDecorFitsSystemWindows(false)).
#
# Coordinate frames: the window is full screen, but
# DisplayMetrics.heightPixels is the app CONTENT height (status bar and nav
# bar excluded). The typed insets are measured from the absolute display
# edges, so the absolute display height is reconstructed as
# contentH + statusTop + navBottom.
#
# v0 = statusTop, v1 = navBottom, v2 = imeInset, v3 = display height /
# bottomLimit / height (int, sequential), v4 = object scratch, v5 = int
# scratch. All registers <= v6 (const/16, if-* and invoke register lists are
# 4-bit).
.method public onApplyWindowInsets(Landroid/view/View;Landroid/view/WindowInsets;)Landroid/view/WindowInsets;
.locals 4
.locals 6
# int imeBottom = 0;
const/4 v0, 0x0
# if (Build.VERSION.SDK_INT >= 30)
# imeBottom = insets.getInsets(WindowInsets.Type.ime()).bottom;
sget v1, Landroid/os/Build$VERSION;->SDK_INT:I
const/4 v1, 0x0
const/16 v2, 0x1e
const/4 v2, 0x0
if-lt v1, v2, :cond_pad_noime
# content height (valid on every path; the API < 30 branch leaves the
# inset values at 0)
iget-object v4, p0, Lorg/gioui/PadInsetsListener;->mView:Landroid/view/View;
const/16 v1, 0x8
invoke-virtual {v4}, Landroid/view/View;->getResources()Landroid/content/res/Resources;
invoke-virtual {p2, v1}, Landroid/view/WindowInsets;->getInsets(I)Landroid/graphics/Insets;
move-result-object v4
move-result-object v1
invoke-virtual {v4}, Landroid/content/res/Resources;->getDisplayMetrics()Landroid/util/DisplayMetrics;
iget v0, v1, Landroid/graphics/Insets;->bottom:I
move-result-object v4
iget v3, v4, Landroid/util/DisplayMetrics;->heightPixels:I
sget v4, Landroid/os/Build$VERSION;->SDK_INT:I
const/16 v5, 0x1e
# API < 30: no typed insets - fall through with all insets 0.
if-lt v4, v5, :goto_pad_geom
# statusTop = insets.getInsets(Type.statusBars()).top
const/4 v4, 0x1
invoke-virtual {p2, v4}, Landroid/view/WindowInsets;->getInsets(I)Landroid/graphics/Insets;
move-result-object v4
iget v0, v4, Landroid/graphics/Insets;->top:I
# navBottom = insets.getInsets(Type.navigationBars()).bottom
const/4 v4, 0x6
invoke-virtual {p2, v4}, Landroid/view/WindowInsets;->getInsets(I)Landroid/graphics/Insets;
move-result-object v4
iget v1, v4, Landroid/graphics/Insets;->bottom:I
# imeInset = insets.getInsets(Type.ime()).bottom
const/16 v4, 0x8
invoke-virtual {p2, v4}, Landroid/view/WindowInsets;->getInsets(I)Landroid/graphics/Insets;
move-result-object v4
iget v2, v4, Landroid/graphics/Insets;->bottom:I
:goto_pad_geom
# absolute display height
add-int v3, v3, v0
add-int v3, v3, v1
# bottomLimit: where the view bottom must not go below.
# IME present: display bottom minus the IME inset (reaches the visible
# keyboard top; the keyboard also covers the nav area).
# otherwise: display bottom minus the nav bar.
if-lez v2, :cond_pad_noime
sub-int v3, v3, v2
goto :goto_pad_bottom
:cond_pad_noime
# int h = displayHeight - imeBottom;
iget-object v1, p0, Lorg/gioui/PadInsetsListener;->mView:Landroid/view/View;
sub-int v3, v3, v1
invoke-virtual {v1}, Landroid/view/View;->getResources()Landroid/content/res/Resources;
:goto_pad_bottom
# height = bottomLimit - statusTop, clamped to >= 0
sub-int v3, v3, v0
move-result-object v1
if-gez v3, :cond_pad_pos
invoke-virtual {v1}, Landroid/content/res/Resources;->getDisplayMetrics()Landroid/util/DisplayMetrics;
const/4 v3, 0x0
move-result-object v1
:cond_pad_pos
# apply to LayoutParams (height + topMargin = statusTop, bottomMargin =
# navBottom)
iget-object v4, p0, Lorg/gioui/PadInsetsListener;->mView:Landroid/view/View;
iget v1, v1, Landroid/util/DisplayMetrics;->heightPixels:I
invoke-virtual {v4}, Landroid/view/View;->getLayoutParams()Landroid/view/ViewGroup$LayoutParams;
sub-int v1, v1, v0
move-result-object v4
# if (mView.getLayoutParams().height != h) { ... = h; requestLayout(); }
iget-object v2, p0, Lorg/gioui/PadInsetsListener;->mView:Landroid/view/View;
check-cast v4, Landroid/view/ViewGroup$MarginLayoutParams;
invoke-virtual {v2}, Landroid/view/View;->getLayoutParams()Landroid/view/ViewGroup$LayoutParams;
# v2 (free after geometry) = changed flag
const/4 v2, 0x0
move-result-object v2
iget v5, v4, Landroid/view/ViewGroup$LayoutParams;->height:I
iget v3, v2, Landroid/view/ViewGroup$LayoutParams;->height:I
if-eq v5, v3, :cond_pad_checktop
if-eq v3, v1, :cond_pad_done
iput v3, v4, Landroid/view/ViewGroup$LayoutParams;->height:I
iput v1, v2, Landroid/view/ViewGroup$LayoutParams;->height:I
const/4 v2, 0x1
iget-object v3, p0, Lorg/gioui/PadInsetsListener;->mView:Landroid/view/View;
:cond_pad_checktop
iget v5, v4, Landroid/view/ViewGroup$MarginLayoutParams;->topMargin:I
invoke-virtual {v3}, Landroid/view/View;->requestLayout()V
if-eq v5, v0, :cond_pad_checkbot
iput v0, v4, Landroid/view/ViewGroup$MarginLayoutParams;->topMargin:I
const/4 v2, 0x1
:cond_pad_checkbot
iget v5, v4, Landroid/view/ViewGroup$MarginLayoutParams;->bottomMargin:I
if-eq v5, v1, :cond_pad_layout
iput v1, v4, Landroid/view/ViewGroup$MarginLayoutParams;->bottomMargin:I
const/4 v2, 0x1
:cond_pad_layout
if-eqz v2, :cond_pad_done
iget-object v5, p0, Lorg/gioui/PadInsetsListener;->mView:Landroid/view/View;
invoke-virtual {v5}, Landroid/view/View;->requestLayout()V
:cond_pad_done
return-object p2
.end method
'''
(base / "smali" / "org" / "gioui" / "PadInsetsListener.smali").write_text(listener)