Files
simplex-chat/plans/2026-09-12-fix-passcode-submit-split-screen.md
T

7.7 KiB
Raw Blame History

Fix Submit button not visible in passcode view in split screen

Problem

On Android in split screen, the "Enter Passcode" view shows the title and the keypad but no Cancel/Submit buttons — the app cannot be unlocked with a passcode without leaving split screen.

The same PasscodeView also backs SetAppPasscodeView, so "New Passcode" / "Confirm passcode" (Privacy & security → passcode, and the passcode onboarding in AppLock.setPasscode) are affected identically.

Desktop is affected too whenever the window is short (below ~590dp of height at a typical width), since desktop always uses the vertical layout.

Cause

Two independent defects, both invisible at full screen.

1. The keypad consumes the space the buttons need

PasscodeView.VerticalLayout is a Column of three children — title, keypad, buttons Row — and the keypad's size is derived from the constraints it is offered (PasswordEntry.kt):

val s = if (appPlatform.isAndroid) minOf(maxWidth, maxHeight) / 4 - 1.dp else ...

Column measures unweighted children in order, each with maxHeight reduced by what the previous ones took, so the keypad — the second child — is offered all remaining height and a 4-row grid then occupies exactly that. The buttons Row is measured last, from what is left, and Modifier.heightIn(min = 70.dp) cannot rescue it: heightIn constrains its target into the incoming constraints, so a 70dp minimum against a 1dp maximum yields 1dp.

At full screen the bug is dormant because maxWidth is the smaller term there — the keypad is sized by width and the leftover height goes to the buttons. It appears as soon as the remaining height drops below the width, which every split-screen half does.

Measured with a Compose harness that renders these composables at fixed window sizes (411dp wide, density 1, Surface(Modifier.fillMaxSize()) as in LocalAuthentication.kt):

window keypad maxHeight key size buttons Row height Submit
411×914 (full screen) 805 101.75 70 @ y=763 108×40 ✓
411×520 411 101.75 0 108×0
411×480 371 91.75 0 108×0
411×445 (split half) 336 83.0 1 @ y=444 108×1
411×400 291 71.75 0 108×0

HorizontalLayout has the same defect in the width axis: its keypad is 4 keys wide and sized minOf(maxWidth, maxHeight) / 3.5f, so the keypad's width grows with the available height, and the Cancel/Submit Column — the second child of the Row — gets the remainder. At 411×365 it is offered maxWidth = 24dp and Submit measures 0×0; at 411×445 it is offered 0dp and Submit is placed at x=441, outside a 411dp-wide window.

This branch matters in split screen because windowOrientation() reads Configuration.orientation, which is computed from the activity window bounds, not the device: a split half of a tall phone is PORTRAIT (~411×445) while a half of a 16:9 phone is LANDSCAPE (~411×365). Both layouts are reachable, and both were broken.

2. No window insets

MainActivity calls enableEdgeToEdge(), and this screen never applied insets — at full screen SpaceEvenly happened to leave the buttons ~80dp clear of the navigation bar. Once the buttons are laid out correctly in a split half, they land at the window edge: with fix 1 alone, Submit occupies y=390…430 of a 445dp window, and a 48dp 3-button navigation bar covers y=397…445 — 33 of its 40dp, so the button is drawn but not reliably tappable in the bottom split window.

Fix

Modifier.weight(1f, fill = false) on the keypad in the vertical layout and on the keypad column in the horizontal layout (PasscodeView.kt), which requires a modifier parameter on PasscodeEntry (PasswordEntry.kt):

PasscodeEntry(passcode, true, Modifier.weight(1f, fill = false))

Column/Row measure weighted children after all unweighted ones, so the title and the buttons are measured first at their natural size and the keypad receives what is left — the inverse of the current order, with no size constant to keep in sync. fill = false keeps the keypad at its own size rather than stretching it, and because a weighted child makes the container expand to the incoming bounded maximum, the SpaceEvenly / SpaceBetween distribution at full screen is unchanged.

Plus Modifier.systemBarsPadding() on both layout roots.

Measured after the fix:

window branch key size Submit
411×445, 24dp status bar (top split half) vertical 59.5 108×40 @ y=390…430 ✓
411×445, 48dp nav bar (bottom split half) vertical 53.5 108×40 @ y=342…382, clear of the bar ✓
411×365, 48dp nav bar horizontal 29.3 108×40 @ x=263…371 ✓
411×914 (full screen) vertical 101.75 (unchanged) 108×40, column y=24…866

The insets go on the Column/Row themselves rather than a wrapping Box: Material Surface lays its content out with propagateMinConstraints = true, so the layout root receives the window size as a minimum. A plain Box in between drops that minimum (its own default is false), the Column becomes wrap-content, SpaceEvenly has no slack left to distribute, and the whole screen top-aligns — measured as the buttons moving from y=763 to y=544 at full screen. Keeping the padding on the root preserves the propagation.

Scope / non-goals

  • Both files are commonMain, so the fix covers Android and desktop; on desktop systemBarsPadding() resolves to zero insets and only the weight change has an effect (short desktop windows).
  • The keypad necessarily gets smaller in a small window: 53.5–59.5dp keys in a portrait split half, 29.3dp in the 411×365 horizontal case, where the title, reason and passcode text consume ~170dp before the keypad is measured. Making the passcode screen itself more compact below some height (smaller title, dropping the reason line) is a separate design change and is not attempted here.
  • systemBarsPadding() also shrinks the keypad in full-screen landscape (79.6 → 59.0dp). That is the same 72dp the bars always occupied — previously the keypad's bottom row extended ~8dp under the navigation bar — but it is a visible change on a screen that did not show the reported bug.
  • Not changed: windowOrientation() branching on Configuration.orientation. Selecting the layout from the measured aspect ratio (as iOS does) would be a better fit for resizable windows, but it changes behaviour on every device rather than fixing this bug.

iOS

Not affected, and for a structural reason worth recording: iOS picks its layout from the measured geometry rather than an orientation flag — if g.size.width < g.size.height * 2 / 3 (PasscodeView.swift) — so a short, wide window (the geometry that breaks Android's vertical layout) selects iOS's horizontal layout, whose keypad is sized from height (s = height / 5) with the buttons column explicitly bounded to height / 5 * 3 * 0.97. The vertical layout's width-driven keypad (s = width / 3) is only ever used when the window is at least 1.5× taller than wide, where it fits by construction. No iOS change required.

Verification

  • Compose measurement harness at the window sizes tabulated above, before and after, for both layout branches and for 24dp/48dp/72dp inset combinations.
  • Android arm64 debug APK (bash ~/build/android.sh) — manual check of Submit in both the top and the bottom split-screen half, and no visual change at full screen.
  • Linux x86_64 AppImage (bash ~/build/linux.sh) — manual check with the window resized short, confirming the desktop vertical layout keeps Cancel/Submit visible.
  • Also exercise "New Passcode" / "Confirm passcode" (SetAppPasscodeView) in split screen, which share PasscodeView.