Rebuild the terminal's keypad, and fix what a device run showed
ci / build and test (push) Failing after 3m0s
ci / desktop nightly (push) Skipped
ci / api image (push) Skipped
ci / android head (push) Successful in 3m56s

Six defects, all found by running this head on an emulator against a local API rather than by reading it.
They are in one commit because two of the files carry several of them and splitting those would mean
hand-editing patches; each is argued at its own site.

── the keypad, which is most of this ───────────────────────────────────────────────────────────────────
It was eleven 30-by-44 keys in a horizontal ScrollViewer, and it was wrong twice over. A pan and a press
begin with the same few pixels of travel, so a thumb coming down at an angle — which is how a thumb comes
down — was read as the start of a pan and the press was never delivered; the row slid slightly instead.
And 30 tall is well under the 48 Android asks for, so the presses that did arrive were hard to aim. The
arrows, the keys pressed most and in sequence, were the ones parked past the right-hand edge.

Termux, Termius, JuiceSSH, ConnectBot and Blink all ship this row and not one of them scrolls it: they
divide the width evenly and add a second row when the set does not fit. That is not convention for its own
sake — a key whose position is fixed can be hit without looking, which is the whole point of a modifier row
used while reading the output above it.

So: two UniformGrid rows, every key an equal share of the width, nothing scrollable, 40 tall. Every key
kept. Arrows on the lower row where the thumb rests, reordered to ← ↓ ↑ → so the pairs that undo each other
are neighbours. It costs about 51 pixels of terminal, four or five lines, and that is the right trade: the
row that was cheaper did not work.

The keys also gain a pressed state, which they had none of. They are Focusable="False" so that using one
does not take the keyboard off the terminal, and a button that never takes focus never shows the adorner
Fluent would otherwise give it — leaving no feedback at all on a control whose only job is to be pressed.
Their looks move from assignments in the code-behind to Button.hotkey, which is what lets a style say
:pressed, and the Ctrl latch becomes a class rather than two brushes written by hand.

── and five smaller ones ────────────────────────────────────────────────────────────────────────────────
The keyboard's inset was never cleared when the app was resumed without the keyboard it was applied for.
Android takes the keyboard away as the activity stops and Avalonia's input pane raises no StateChanged,
the pane going with the window rather than closing in front of one — so the interface came back holding a
whole keyboard's height of empty canvas. 838 of 2340 pixels on a Pixel 5, on every screen, until something
opened and closed the keyboard. MainActivity now raises PhoneEnvironment.Resumed and PhoneShell re-asks the
pane. It asks rather than assuming zero: a permission prompt or the biometric sheet can hand the activity
back with the keyboard genuinely still up, and flattening the inset there would strand the focused box
under a real one.

TextBlock.detail never wrapped, where .body beside it always has. A block that neither wraps nor trims lays
out at its full single-line width and the parent clips it, with no ellipsis to say anything is missing — so
the add sheet read "A machine to connect to, filed under a group if you" and stopped. Which of the fifty-odd
.detail blocks overflows is a function of width and font scale rather than of the string, so this is a style
rather than an attribute on the handful visibly wrong on one phone. The nine that genuinely want one line
now say NoWrap beside their TextTrimming, which is the pairing that was always meant.

The two port boxes are NumericUpDowns and were the only unthemed controls on this head — Fluent's border and
fill, next to a TextBox.field sharing their row. App.axaml has styled NumericUpDown for the desktop all
along; this head never had one.

Three buttons classed "icon accent" were painting Fluent's own Button.accent, #6275AC, a blue in no palette
here. The desktop overrides that style and this head did not, so the same class read correctly there and not
here.

The host key sheet said "First contact with 10.0.2.2 · 2222 . Nothing in this keychain…" — Avalonia puts a
space between adjacent Runs and the last one began with the full stop. A gap before a full stop, on the one
dialogue whose whole job is to be read carefully before it is answered.

── checks ──────────────────────────────────────────────────────────────────────────────────────────────
manual-checks gains 10.4a and 11.10f. 10.4 already covered dismissing the keyboard with back or the
chevron but not leaving the app with it up, which is the path the inset bug shipped through; 10.4a covers
both directions, since a fix that flattened the inset on every resume would pass one half and fail the
other. 11.10f guards the keypad against a ScrollViewer being put back around it.
This commit is contained in:
jaap-jan committed 2026-08-26 13:28:59 +02:00
1 parent 91f7586b22
commit aff776cb42
13 files changed
+488 -108

No files matched your search

+40
View File
@@ -1810,6 +1810,27 @@ bottom and no scroll position left part way down.
**Failure means:** the inset is being applied but not cleared — the closed state is not being read from the
event, or the margin is only ever added to.
### 10.4a Nothing is stranded when the keyboard is still up on the way *out* · **the one 10.4 missed**
Focus any box, and with the keyboard still on screen leave the application — Home, or the task switcher,
or anything that puts another app in front. Come back to it.
**Pass:** the interface fills the screen. Do it from the terminal too, where the keypad and the shell should
both be where they were.
**Failure means:** `PhoneShell` is not re-reading the inset on the way back in. This is a separate path from
10.4 and it is the one that has actually shipped broken: Android takes the keyboard away when the activity
stops and Avalonia's input pane raises no `StateChanged` for it, because the pane goes with the window
rather than closing in front of one. So the last value applied was for a keyboard that was up, and the
application returns holding a whole keyboard's height of empty canvas — measured at 838 of 2340 pixels on a
Pixel 5, on every screen, and staying that way until something opens and closes the keyboard. See
`PhoneShell.OnActivityResumed` and `PhoneEnvironment.Resumed`.
Worth doing in both directions: a resume that *should* keep the inset is the other half. Trigger a
permission prompt or the biometric sheet with the keyboard up, dismiss it, and check the box being typed
into is still clear of a keyboard that is genuinely still there — a fix that flattened the inset on every
resume would pass the paragraph above and fail this one.
### 10.5 Rotating with the keyboard up
Focus a passphrase box, then turn the phone sideways.
@@ -2060,6 +2081,25 @@ cause would look unrelated. See the correction in [ADR 0015](adr/0015-android-te
Worth pressing Enter on an empty prompt a few times too: each one should give a fresh prompt and nothing
else. A doubled prompt per press means a line ending is arriving as both CR and LF.
### 11.10f Every key on the keypad is there, and pressing one presses it
With a shell open, look at the two rows above the keyboard without touching anything: **esc tab ctrl | - ~ /**
on the upper row and **← ↓ ↑ →** with **A−** and **A+** on the lower one. All thirteen, at once, on a 360dp
phone in portrait.
Then press them with a thumb rather than a fingertip, at an angle, twenty or so times across both rows —
including the keys at the far left and far right ends.
**Pass:** every press registers, the key darkens under the thumb while it is down, and **nothing slides
sideways**. The row is exactly where it was after twenty presses as it was before the first.
**Failure means:** a key that misses is a target that has got smaller — `Button.hotkey` is 40 tall for a
reason and Android asks for 48. A row that *moves* is the one that matters: it means something has put a
`ScrollViewer` back around the keys. That is how this row shipped originally and it does not survive contact
with a thumb — a pan and a press begin with the same few pixels of travel, the scroller claims them, and the
press is never delivered. If the set of keys outgrows the width, add a third row; do not make it scroll.
See the keypad's own remark in `TerminalScreen.axaml`.
### 11.11 Closing a connection and opening a new one both take you somewhere real
Open a shell, close its tab, then open a different one from HOSTS.