Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
85 changes: 80 additions & 5 deletions docs/INTERNALS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<iframe>`, and a click dispatched on the frame
Expand Down Expand Up @@ -1559,23 +1588,52 @@ Shift releases itself after one letter, as on a phone: one capital is what a nam
or a password rule usually wants, and leaving it latched turns the rest of the word
into shouting.

Every grid is deliberately the same shape (rows of 10, 10, 10, 10, 14): the
Every grid is deliberately the same shape (rows of 10, 10, 10, 10, 16): the
keyboards build their cells once in the constructor and only swap the labels when
the grid changes, so a grid with different row lengths would leave cells pointing
at keys that are no longer there. That is also why the shifted variants and the
symbol page are built to the same rows-of-ten shape rather than being packed
tighter. Whatever letters a layout leaves over, the row is padded out with `-`,
`_`, `?`, `=` so every key exists in every layout.

The action row grew from 11 keys to 14 (`@`, `shift`, `sym`, `start`), which is why
a key is 104px wide rather than 116 — fourteen of the old ones overflow a 1080p
panel. Rows are centred rather than left-aligned, or a 10-key letter row leaves a
ragged hole beside a 14-key action row.
The action row grew from 11 keys to 14 (`@`, `shift`, `sym`, `start`) and then to
16 (the two caret arrows, below), which is why a key is 100px wide rather than
116 — fourteen of the old ones already overflowed a 1080p panel, and sixteen of
the new ones plus their gaps are the 1736px fourteen old ones were. Rows are
centred rather than left-aligned, or a 10-key letter row leaves a ragged hole
beside a 16-key action row.

`@` sits on the action row and not on the symbol page: signing in to anything needs
it constantly, and having to find a second page for it was the substance of
issue #15.

### The selector follows the screen, not the index

Issue #92, from the 2025 set: "when we move selector from last 2nd row to last
row in keyboard it moves 2 character back and moves 2 character forward when
move from last to 1st row". Both keyboards moved between rows by keeping the
column index and clamping it to the new row's length. With the rows centred and
the action row four keys wider than the letter rows, the same index is two keys
to the left on the way down and two to the right on the way up — exactly his
numbers, and it had been so since the action row first outgrew the letters.

`KeyboardLayouts.ColumnAcross` does the move by position: the keys sit on one
pitch whatever the row, so a key's centre in key units is its column plus half
the difference between the widest row and its own, and the key in the other row
whose centre that falls in is the one the selector lands on. It is in key units
and not pixels so the two keyboards, which draw at different sizes, agree, and it
is in `src/common` so the ewk builds get the same fix untested — the maths has
no toolkit in it, which is what makes that acceptable.

The same report asked for arrow keys, and it meant the entry, which was
append-only: a typo three characters back in an address meant deleting
everything after it and typing it again on a D-pad. `KeyboardEntry` holds the
text and a caret for both keyboards, and `←`/`→` on the action row move it;
typing, `.com`, `space` and `back` act at the caret. The keys are *named*
`left`/`right` in the grid and wear the arrows as labels, because `<` and `>` are
characters on the symbol page and a key's name is what `Press` switches on.
`tools/keyboard/run.sh` holds all of this off-device.

### Where the browser opens

Three states, and `StartPage` owns which: the generated start screen, one fixed
Expand Down Expand Up @@ -2734,6 +2792,23 @@ the one its report has to come from. The state is:
cannot land on* above; both are fixed in `build-42ed14f` and his files are healed
on load.

- **The keyboard's selector jumping between rows, arrow keys in the entry, and
two pointers at launch — #92 and #91, built together.** Both are things a
browser gets asked for once it stops breaking, and both had a fault behind
them. #92's jump was the column index kept across rows of different widths on a
centred grid; the selector now moves by position, and the entry has a caret
with `←`/`→` on the action row. #91's second pointer was the page's arrow
coming back on a `visibilitychange` after the app had hidden it to draw its
own; the hide is remembered now. See *The selector follows the screen, not the
index* and *The page's arrow has to remember it was told to go away* above.
Both fixes are in `src/common`, so the ewk builds get them too. **Waiting on:**
his report from the build. Neither needs a question answered — the keyboard
either lands under the key or it does not, and either one pointer shows at
launch or two do. If two still do, the `pointer` line on the report says who is
drawing the one we mean, and the next thing to look for is a second
`__ovs_cursor` element in the DOM rather than a shown one: that would be a
frame or a second document, not this.

Five things about that set are settled and should not be re-derived: **key `5` is
his, not ours** — the engine's overlay path is the only one that gives him a
picture, so a report of black or silent video is the in-page path failing and not a
Expand Down
102 changes: 102 additions & 0 deletions src/common/KeyboardEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
namespace Overscan
{
/// <summary>
/// The line of text the on-screen keyboard is building, and where in it the
/// next character lands.
///
/// Shared by the ElmSharp and NUI keyboards so both edit the same way. Until
/// issue #92 the entry was append-only: a typo three characters back in an
/// address meant deleting everything after it and typing it again, on a
/// D-pad, which is what the reporter was asking to be spared when he asked for
/// arrow keys. The caret is an index into <see cref="Text"/>; everything the
/// keyboards do to the text goes through here so the two cannot drift apart.
/// </summary>
internal sealed class KeyboardEntry
{
private string _text = string.Empty;
private int _caret;

/// <summary>What has been typed so far.</summary>
public string Text
{
get { return _text; }
}

/// <summary>Where the next character goes: 0 is before the first one.</summary>
public int Caret
{
get { return _caret; }
}

/// <summary>
/// The text with the caret drawn into it as a bar, for the entry line. An
/// empty entry is a bar on its own, as it always was.
/// </summary>
public string Display
{
get { return _text.Substring(0, _caret) + "|" + _text.Substring(_caret); }
}

/// <summary>
/// Starts over with <paramref name="text"/> and the caret at its end: the
/// keyboard opens on an address to add to it, or on nothing at all.
/// </summary>
public void Reset(string text)
{
_text = text ?? string.Empty;
_caret = _text.Length;
}

/// <summary>Puts <paramref name="value"/> in at the caret and moves past it.</summary>
public void Insert(string value)
{
if (string.IsNullOrEmpty(value))
{
return;
}

_text = _text.Substring(0, _caret) + value + _text.Substring(_caret);
_caret += value.Length;
}

/// <summary>
/// Removes the character before the caret. False when there was none —
/// the keyboards close on a remote Back key that has nothing left to delete.
/// </summary>
public bool Backspace()
{
if (_caret == 0)
{
return false;
}

_text = _text.Substring(0, _caret - 1) + _text.Substring(_caret);
_caret--;
return true;
}

public void Clear()
{
_text = string.Empty;
_caret = 0;
}

/// <summary>One character towards the start; stays put at the start.</summary>
public void Left()
{
if (_caret > 0)
{
_caret--;
}
}

/// <summary>One character towards the end; stays put at the end.</summary>
public void Right()
{
if (_caret < _text.Length)
{
_caret++;
}
}
}
}
59 changes: 57 additions & 2 deletions src/common/KeyboardLayouts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ namespace Overscan
/// because the keyboards build their cells once and only swap the labels when
/// the grid changes. A grid of a different shape would leave those cells
/// pointing at keys that are no longer there.
///
/// The rows are not the same width, and they are centred, so a column index
/// is not a position on the screen: <see cref="ColumnAcross"/> is how the
/// selector moves between rows without appearing to jump (issue #92).
/// </summary>
internal static class KeyboardLayouts
{
Expand All @@ -32,6 +36,17 @@ internal static class KeyboardLayouts
/// <summary>Saves what was typed as the page to open at start-up.</summary>
public const string StartPageKey = "start";

/// <summary>
/// Moves the caret in the entry one character back or forward, so a typo
/// in the middle of an address can be reached without deleting everything
/// after it (issue #92). Named rather than drawn: the arrows the keys wear
/// are labels, and `&lt;` and `&gt;` are characters on the symbol page.
/// </summary>
public const string CaretLeftKey = "left";

/// <summary>See <see cref="CaretLeftKey"/>.</summary>
public const string CaretRightKey = "right";

private const string SettingKey = "keyboardLayout";

private static readonly string[] LayoutNames = { "QWERTY", "AZERTY", "QWERTZ", "ABCDEF" };
Expand Down Expand Up @@ -156,6 +171,45 @@ public static string[][] ReleaseShift()
return Rows;
}

/// <summary>
/// The column in row <paramref name="toRow"/> that sits nearest, on the
/// screen, to column <paramref name="fromColumn"/> of row
/// <paramref name="fromRow"/>.
///
/// Rows are centred and not all the same length: the letter rows are 10
/// keys and the action row is 16, so the action row starts three keys to
/// the left of the letters above it. Carrying the column index across
/// unchanged, which is what both keyboards did, moved the selector three
/// keys back on the way down and three forward on the way up — issue #92,
/// reported as "2 characters" when the row was 14 wide. The keys are laid
/// out on one pitch (a key plus a gap) whatever the row, so a key's centre
/// in units of that pitch is its column plus half, plus half the difference
/// between the widest row and its own; the nearest key in the other row is
/// the one whose centre that lands in. The maths is in key units and not
/// pixels so the two keyboards, which draw at different sizes, agree.
/// </summary>
public static int ColumnAcross(string[][] rows, int fromRow, int fromColumn, int toRow)
{
int widest = 0;
foreach (string[] row in rows)
{
widest = System.Math.Max(widest, row.Length);
}

int fromLength = rows[fromRow].Length;
int toLength = rows[toRow].Length;

// Twice the centre, to stay in integers: 2 * (offset + column + 0.5).
int centre2 = (widest - fromLength) + (2 * fromColumn) + 1;
int target = (centre2 - (widest - toLength) - 1) / 2;
if (target < 0)
{
return 0;
}

return target >= toLength ? toLength - 1 : target;
}

/// <summary>
/// Builds a grid from its three letter rows. The digits and the action row
/// are the same whatever the letters do, so they are not repeated per layout.
Expand Down Expand Up @@ -209,13 +263,14 @@ private static string[][][] BuildShifted()
/// The bottom row, identical on every grid. `@` sits here rather than on the
/// symbol page because signing in to anything needs it; `shift` and `sym`
/// are next to each other so the two ways of reaching a different character
/// are in one place.
/// are in one place; the caret arrows sit beside `back`, which is the key
/// they are most often used with.
/// </summary>
private static string[] ActionRow()
{
return new[]
{
".", "/", ":", "@", "space", ".com", "back", "clear",
".", "/", ":", "@", "space", ".com", CaretLeftKey, CaretRightKey, "back", "clear",
ShiftKey, SymbolsKey, StartPageKey, "GO", "close", CycleKey,
};
}
Expand Down
Loading
Loading