mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-27 00:39:00 +00:00
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
148 lines
7.7 KiB
Markdown
148 lines
7.7 KiB
Markdown
# 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`.
|