From ecb9d8715792c8dc915ecb00f4bda1d32f4d1e0e Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Sat, 19 Sep 2026 09:13:52 +0000 Subject: [PATCH] android, desktop: fix Submit button not visible in passcode view in split screen (#7497) Co-authored-by: Evgeny Poberezkin --- .../common/views/localauth/PasscodeView.kt | 8 +- .../common/views/localauth/PasswordEntry.kt | 3 +- ...-09-12-fix-passcode-submit-split-screen.md | 147 ++++++++++++++++++ 3 files changed, 153 insertions(+), 5 deletions(-) create mode 100644 plans/2026-09-12-fix-passcode-submit-split-screen.md diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasscodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasscodeView.kt index 9b25e9b5e0..669847c3a9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasscodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasscodeView.kt @@ -64,7 +64,7 @@ fun PasscodeView( @Composable fun VerticalLayout() { Column( - Modifier.handleKeyboard().focusRequester(focusRequester), + Modifier.systemBarsPadding().handleKeyboard().focusRequester(focusRequester), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceEvenly ) { @@ -74,7 +74,7 @@ fun PasscodeView( Text(reason, Modifier.padding(top = 5.dp), style = MaterialTheme.typography.subtitle1) } } - PasscodeEntry(passcode, true) + PasscodeEntry(passcode, true, Modifier.weight(1f, fill = false)) Row(Modifier.heightIn(min = 70.dp), verticalAlignment = Alignment.CenterVertically) { SimpleButton(generalGetString(MR.strings.cancel_verb), icon = painterResource(MR.images.ic_close), disabled = !buttonsEnabled.value, click = cancel) Spacer(Modifier.size(20.dp)) @@ -85,9 +85,9 @@ fun PasscodeView( @Composable fun HorizontalLayout() { - Row(Modifier.padding(horizontal = DEFAULT_PADDING).handleKeyboard().focusRequester(focusRequester), horizontalArrangement = Arrangement.Center) { + Row(Modifier.systemBarsPadding().padding(horizontal = DEFAULT_PADDING).handleKeyboard().focusRequester(focusRequester), horizontalArrangement = Arrangement.Center) { Column( - Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING), + Modifier.weight(1f, fill = false).padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceBetween ) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasswordEntry.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasswordEntry.kt index f76b82c31e..d16f0a2a38 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasswordEntry.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasswordEntry.kt @@ -22,8 +22,9 @@ import chat.simplex.res.MR fun PasscodeEntry( password: MutableState, vertical: Boolean, + modifier: Modifier = Modifier, ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { + Column(modifier, horizontalAlignment = Alignment.CenterHorizontally) { PasscodeView(password) BoxWithConstraints { if (vertical) { diff --git a/plans/2026-09-12-fix-passcode-submit-split-screen.md b/plans/2026-09-12-fix-passcode-submit-split-screen.md new file mode 100644 index 0000000000..f5051e6721 --- /dev/null +++ b/plans/2026-09-12-fix-passcode-submit-split-screen.md @@ -0,0 +1,147 @@ +# 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`): + +```kotlin +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`): + +```kotlin +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`.