From 1985e5ed7be09cac8e27d30e17300c90e3ac0c73 Mon Sep 17 00:00:00 2001 From: Patrick Stel Date: Sun, 6 Sep 2026 14:39:00 +0200 Subject: [PATCH] Draw the pointer ourselves, open where you left off, and keep an address you cannot land on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issues 78, 79 and 80, all from the same reporter the day after build-d526114. 78 is the one with a fault behind it. "if we load a heavy site cursor start to become less responsive" — the pointer lived in the page, so every D-pad step was one script evaluation that did an elementFromPoint and dispatched a mousemove. That is a forced layout plus whatever the site's own move handlers do, on the busiest thread on the television, and it meant the pointer could only move as fast as the page allowed. The TV's own browser draws its pointer in the compositor, which is why his comparison held. Two halves. The app now draws the pointer — DALi views over the web view on NUI, positioned from the same viewport fraction the script was being sent. NuiCursor's old header said an overlay would have to track scroll and zoom itself; it does not, because the fraction is of the viewport and the viewport is the view's own rectangle, which is why it is kept as a fraction. And the page is told where the pointer is once a tick rather than once a key press, on the 150 ms timer both builds already run. A held key that was twenty hover updates is now six, none of them in front of the drawing. The rule that must not break is that the page and the pointer agree at the moment of a click: PageScript.click() hit-tests wherever move() last left it. So a pending move goes into the click's own script, one evaluation, rather than being sent as a call before it — two evaluations are two chances for that ordering to be wrong, and the failure would be a click that occasionally hits the wrong thing. NUI defaults to drawing it, with a menu row to go back to the page's arrow. The ElmSharp default is unchanged: 78 is a report from a 2025 set, the ewk packages cannot be tested before somebody installs them, and src/nui not being able to break them is the reason the source is split this way. They get the coalescing, which is the half that is safe everywhere; key 2 was already there for the rest. 79 makes the start page three states rather than two. StartPage owns which, the mode is stored beside the address rather than encoded into it, and an install from before this has no mode key so it is derived from whether an address was ever set — nobody's start page moves under them on upgrade. "Where I left off" is the first history entry, which is only the right answer because RecordVisit already refuses our own generated pages and sign-in waypoints. Turning it off goes back to the address if one was set, rather than making somebody retype it. 80 adds a third KeyboardTarget so the same keyboard can finish into ToggleFavourite instead of Navigate. instagram.com/reel redirects to one reel, so key 8 there saves that clip for ever; typing the address keeps the address. It is prefilled with the current page, the entry line reads "Keep as a tile" rather than "Go to", it toggles like key 8 and says which of the two it did, and the tile is named after the site since there is no page title to take. tools/startpage/run.sh now compiles StartPage too and holds it to the three states, to "where I left off" never landing on our own start screen, and to the upgrade. cdpharness re-run for the click path. --- CLAUDE.md | 10 +- README.md | 37 ++++ docs/INTERNALS.md | 124 ++++++++++++- src/common/CursorVisual.cs | 23 +++ src/common/KeyboardTarget.cs | 9 + src/common/RemoteMenu.cs | 2 + src/common/StartPage.cs | 135 ++++++++++++++ src/elm/BrowserApp.cs | 80 ++++++++- src/elm/ElmKeyboard.cs | 18 +- src/elm/VirtualCursor.cs | 62 +++++-- src/nui/NuiBrowserApp.cs | 119 ++++++++++++- src/nui/NuiCursor.cs | 334 +++++++++++++++++++++++++++++++++-- src/nui/NuiKeyboard.cs | 19 +- tools/startpage/Program.cs | 64 +++++++ tools/startpage/run.sh | 8 +- 15 files changed, 996 insertions(+), 48 deletions(-) create mode 100644 src/common/CursorVisual.cs create mode 100644 src/common/StartPage.cs diff --git a/CLAUDE.md b/CLAUDE.md index 6390bf1..4af1904 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -111,8 +111,9 @@ happens. A probe sent to explain a black screen must not be able to cause one. ### The start-page harness -Any change to `Store` or `HomePage` — what gets saved as a visit or favourite, and -the page built from it — is exercised off-device first: +Any change to `Store`, `HomePage` or `StartPage` — what gets saved as a visit or +favourite, the page built from it, and which of the three things the browser opens +at launch — is exercised off-device first: ```sh tools/startpage/run.sh @@ -123,7 +124,10 @@ a row, recording each the way the NUI engine reports it (a `data:` URL carrying page). Issue #53 was that loop with no guard: the start screen recorded itself as a visit on every launch, roughly doubled each time, and after nine launches was past the 2 MB Chromium will load — a black screen with no line anywhere, on a view that -was fine. Needs only the .NET 6 SDK under `~/.dotnet-local`. +was fine. It also holds `StartPage` (issue #79) to the three states, to "where I +left off" landing on the last real visit and never on the start screen, and to an +install made before the mode existed still opening where it always did. Needs only +the .NET 6 SDK under `~/.dotnet-local`. ### The site-rules harness diff --git a/README.md b/README.md index 8bc6473..a57b2ac 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,43 @@ browser simply won't start. Newer sets don't have this restriction. Not every remote sends the colour buttons, and the slim ones have none. **Switch site** is the second row of the menu, so hold OK and it is one press away. +Every digit is spoken for, so these live in the menu only (**hold OK**): + +| Menu row | What it does | +| --- | --- | +| **Keep an address…** | Type a URL and keep *that* as a tile, without going to it | +| **Open where I left off** | Launch straight back into the last page you were on | +| **Forget this site's settings** | Take a site back to your everywhere settings | +| **Pointer style** | Who draws the pointer — Overscan (keeps up) or the page (an arrow) | +| **Ad blocking on/off** | 2025+ package only | + +### Keeping a page you can't land on + +**Keep an address…** in the menu is for URLs that redirect. `https://www.instagram.com/reel` +sends you to one particular reel, so pressing `8` there would save that clip +for ever; type the address instead and the tile is the address. It's prefilled +with wherever you are, so it's usually a matter of deleting the end of it. + +### Where it opens + +By default, the start screen. Two ways to change that: + +- **Open where I left off** in the menu — it launches back into the last page you + were on. +- On the keyboard, **start** makes whatever you typed the fixed opening page. Press + it with nothing typed to go back to the start screen. + +Turning "where I left off" off again returns to your fixed address if you set one, +so you never have to type it twice. + +### The pointer + +On a heavy site the pointer used to slow down, because it was drawn *by the page* +and could only move as fast as the page would let it. Overscan now draws it itself +on the 2025+ package, so it keeps up whatever the site is doing. **Pointer style** +in the menu (key **2** on the older packages) switches back to the page-drawn +arrow, which looks nicer and is only as quick as the page. + ### Each site keeps its own settings Images and identity are remembered **for the site you set them on**. Turn images diff --git a/docs/INTERNALS.md b/docs/INTERNALS.md index dea794b..441b65e 100644 --- a/docs/INTERNALS.md +++ b/docs/INTERNALS.md @@ -1232,6 +1232,55 @@ Details that matter: `input`/`change`, otherwise React-style frameworks ignore it. Enter is sent as real key events, falling back to `form.requestSubmit()`. +### A pointer inside the page moves at the page's speed + +The same reporter again, in #78: "if we load a heavy site cursor start to become +less responsive, when we load light site it is faster, default browser cursor +perform same on all sites." It was ours, not the engine's. + +Everything above describes a pointer that *lives in the page*, and the bill for +that had never been read. Each D-pad step was one script evaluation, and the +script it ran did an `elementFromPoint` and dispatched a `mousemove` — a forced +layout plus whatever the site's own move handlers do, which on Instagram or +Spotify is a great deal. So the pointer could only move as fast as the page's main +thread was willing to run our script, and that thread is the busiest object on the +television. The TV's own browser draws its pointer in the compositor, which is why +his comparison holds and why it always would have. + +Two halves, and neither is sufficient alone: + +- **The app draws the pointer.** DALi views over the web view on NUI, Evas + rectangles on ElmSharp, positioned from the same viewport fraction the script + was being sent. The ElmSharp build has had this behind key `2` since early on + and it was nobody's default; NUI never had it at all, and its old header said an + overlay "would need to track scroll and zoom itself". That was simply wrong — + the position is a fraction of the *viewport*, and the viewport is the view's own + rectangle, which is the entire reason it is kept as a fraction rather than as a + document point. +- **The page hears about it once a tick, not once a key press.** Both builds + already run a 150 ms timer, so a move marks itself pending and the tick flushes + it. A held key that used to be twenty hover updates is now six, and none of them + is in front of the drawing. + +The rule the second half must not break is that **the page and the pointer agree at +the moment of a click**. `PageScript.click()` hit-tests wherever `move()` last left +it, so a click that overtook its own pending move would land where the pointer used +to be. Both builds therefore put the pending move *into the click's own script*, +one evaluation, rather than sending it as a call before — two evaluations are two +chances for that ordering to be wrong, and the failure would be a click that +occasionally hits the wrong thing, which is the worst kind to be sent from a sofa. + +What this costs is up to 150 ms of hover lag behind the drawn pointer, and the +arrow: neither toolkit draws a triangle, so the app-drawn pointer is the ringed dot +the ElmSharp build has always used for its native style, while the page's arrow is +CSS borders. A dot that keeps up beats an arrow that does not, so **NUI defaults to +drawing it itself** and a menu row switches back. **The ElmSharp default is +unchanged**: #78 is a report from a 2025 set, the ewk packages are the ones where a +change cannot be tested before somebody installs it, and `src/nui` not being able +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 one thing a script cannot click: another origin's frame `elementFromPoint` stops at an `