From 901afdbcd1b8f0cacb9b4d54459f421cd1b4b85a Mon Sep 17 00:00:00 2001 From: Patrick Stel Date: Wed, 9 Sep 2026 19:17:41 +0200 Subject: [PATCH] Move the keyboard selector by position and remember the hidden arrow Issue #92: moving between the letter rows and the action row shifted the selector two keys, because both keyboards kept the column index across rows of different widths on a centred grid. KeyboardLayouts.ColumnAcross now picks the key underneath. The same report asked for arrow keys: a shared KeyboardEntry gives the entry a caret, and left/right on the action row move it, so a typo in the middle of an address no longer means retyping everything after it. The action row is 16 keys, so a key is 100px wide (1736px, the width the 14-key row had at 116). Issue #91: two pointers, one on top of the other, at launch. The page script re-installs its arrow on every visibilitychange and install() set it visible unconditionally, so on a set where the app draws the pointer itself the arrow came back under the dot. hide() is remembered now, install() honours it, and show() is the one way back; both cursor classes ask for the arrow by name when switching to the page-drawn pointer, and the NUI reinstall sends install and hide in one evaluation. tools/keyboard and tools/pointer hold both fixes off-device; the pointer harness fails on the previous PageScript.cs at its third check. Co-Authored-By: Claude Fable 5.1 --- CLAUDE.md | 37 ++++++++ docs/INTERNALS.md | 85 ++++++++++++++++- src/common/KeyboardEntry.cs | 102 ++++++++++++++++++++ src/common/KeyboardLayouts.cs | 59 +++++++++++- src/common/PageScript.cs | 32 ++++++- src/elm/ElmKeyboard.cs | 71 ++++++++------ src/elm/VirtualCursor.cs | 8 +- src/nui/NuiCursor.cs | 44 ++++++--- src/nui/NuiKeyboard.cs | 69 ++++++++------ tools/keyboard/Program.cs | 174 ++++++++++++++++++++++++++++++++++ tools/keyboard/run.sh | 58 ++++++++++++ tools/pointer/build-page.py | 101 ++++++++++++++++++++ tools/pointer/run.sh | 53 +++++++++++ 13 files changed, 815 insertions(+), 78 deletions(-) create mode 100644 src/common/KeyboardEntry.cs create mode 100644 tools/keyboard/Program.cs create mode 100755 tools/keyboard/run.sh create mode 100644 tools/pointer/build-page.py create mode 100755 tools/pointer/run.sh diff --git a/CLAUDE.md b/CLAUDE.md index 4af1904..2ab2924 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -129,6 +129,43 @@ left off" landing on the last real visit and never on the start screen, and to a install made before the mode existed still opening where it always did. Needs only the .NET 6 SDK under `~/.dotnet-local`. +### The keyboard harness + +Any change to `KeyboardLayouts` or `KeyboardEntry` — the grids the on-screen +keyboard shows, the move between rows, and the text with its caret — is exercised +off-device first: + +```sh +tools/keyboard/run.sh +``` + +It compiles the shipping files against a stub store and log and holds them to the +shape both keyboards build their cells for, to a move between rows landing on the +key underneath rather than the key with the same index (issue #92: the rows are +centred and the action row is wider, so the same index is two keys away), and to +the caret typing, deleting and stopping where it should. Both files are in +`src/common`, so a mistake is in all six packages, and the ewk ones cannot be +tried before somebody installs them. Needs only the .NET 6 SDK under +`~/.dotnet-local`. + +### The pointer harness + +Any change to the pointer half of `PageScript` — `install`, `hide`, `show`, `move` +and the `visibilitychange` hook — is exercised against desktop chromium first: + +```sh +tools/pointer/run.sh +``` + +It lifts the shipping script out of the `.cs` and holds it to one contract: install +puts the arrow in the DOM, hide and show decide whether it is seen, and nothing the +page does on its own (a visibilitychange, a re-install, a move, a single-page +navigation wiping the overlay, the whole script run again) changes that decision. +Issue #91 was that contract not existing: the app hid the arrow to draw its own +pointer, the page brought it back on the first visibilitychange, and the reporter +saw two pointers one on top of the other. From a TV that is indistinguishable +from the app having drawn two. + ### The site-rules harness Any change to `SiteRules` — the per-site images/identity rules, and the "which diff --git a/docs/INTERNALS.md b/docs/INTERNALS.md index 6c76158..db065fd 100644 --- a/docs/INTERNALS.md +++ b/docs/INTERNALS.md @@ -1307,6 +1307,35 @@ to break them is the whole reason the source is split this way. They get the coalescing, which is the half that is safe everywhere, and key `2` was already there for the rest. +### The page's arrow has to remember it was told to go away + +Issue #91, from the same set once NUI was drawing the pointer itself: "sometimes +when load app where it shows two cursor one on top of other". Two pointers, one +exactly under the other, is the app's dot and the page's arrow both showing at +the same viewport fraction — and the app had asked the page to hide the arrow, +so the question was who put it back. + +The page script did, on its own. Its `visibilitychange` listener re-runs +`install()`, which is there because a single-page navigation wipes the overlay +out of the DOM; `install()` set the arrow `display:block` unconditionally, and +nothing in the script remembered that `hide()` had been called. `visibilitychange` +fires on a great deal more than a navigation — the app coming to the front at +launch, the set's own menus going over it, the screen blanking — which is the +"sometimes" and the "when load app". The ElmSharp builds had the same fault +behind key `2` and nobody had used it long enough to see it. + +So `hide()` is remembered (`st.hidden`), `install()` honours it, and `show()` is +the only call that clears it. Both cursor classes ask for the arrow by name when +they switch to the page-drawn pointer, rather than relying on a re-install to +bring it back — which on the ElmSharp build it never reliably did: toggling to +the page's arrow after the native one had hidden it left it hidden until the next +page load. On NUI, `Reinstall` also sends the install and the hide in **one** +evaluation, so there is no ordering between them to be wrong. The shape of the +page script's contract is now: install puts the arrow in the DOM, hide and show +decide whether it is seen, and the two are independent. `tools/pointer/run.sh` +runs the shipping script in desktop chromium and fails on the old file at the +third check. + ### The one thing a script cannot click: another origin's frame `elementFromPoint` stops at an `