Skip to content

Scroll a number in place instead of hunting for it on a keypad - #7

Merged
oroderico merged 3 commits into
oroderico:mainfrom
sandman21vs:scrolled-number-entry
Aug 15, 2026
Merged

oroderico merged 3 commits into
oroderico:mainfrom
sandman21vs:scrolled-number-entry

Conversation

@sandman21vs

Copy link
Copy Markdown
Collaborator

Word numbers are typed on a twelve-key keypad. The two buttons walk a cursor
across it, so both gestures go on navigation and the reader has to find each
digit, then find Accept. A number is not a position in a grid.

This scrolls the value in place instead: up and down are the digit.

            Word 3/12
              ▲
          ┌───┬───┬───┬───┐
          │ 2 │ 4 │ 0 │   │
          └───┴───┴───┴───┘
              ▼
       BOTH continue   Up/Down back

Boxes before the active one are set, the active one scrolls, the ones after are
still to come. Adapted from Blockstream Jade's PIN and index entry
(main/ui/digit_entry.c), which shows the same three states.

Dice rolls scroll in one wider box for the same reason, keeping their quality
bar underneath. A card keeps the text screen: A Clubs does not fit a box, and
its running transcript wants the room.

Where a number ends

The ring for the active box carries the digits that still lead somewhere, then
OK once what is typed already names a word, then backspace.

That OK is the part that matters, and the first version of this branch got it
wrong by leaving it out. With a fixed four-box field, 240 is a dead end — no
fourth digit takes 240x into 1..2048 — so the reader who typed it found a box
holding nothing but backspace, and the only way to mean 240 was to have known
to type 0240 from the start. Ending a number early is not a convenience here;
without it, three-digit numbers are not reachable at all except through a
leading zero the reader has no reason to expect.

Two consequences worth stating, because neither is arbitrary:

  • OK is absent on an empty field, since nothing is named yet.
  • OK is present on a prefix like 20, because 20 is both a word number
    and a step towards 200x. Both readings are live, so the reader is the one who
    says which was meant rather than the field guessing.

A fourth digit still confirms itself — there is no fifth box to put an OK in.
Going back from the confirmation reopens that fourth digit when it was the
fourth digit that confirmed, and returns to the field untouched when the reader
ended with OK.

Digit pruning is unchanged: seedtool_next_digits already offered only digits
that leave a word number reachable, which is what keeps 2049 untypeable while
2048 stays reachable. The ring is built from its answer.

What moved

  • seedtool_render_digits — the row of boxes, and seedtool_render_value_box
    for a value scrolled whole rather than digit by digit.
  • seedtool_render_dice_screen's bar moved into draw_progress_bar, so the
    dice screen and the scrolled field draw the same bar instead of each carrying
    a copy.
  • enter_word_number rewritten around the ring; enter_value routes a plain
    number to the box and leaves formatted values (cards) on the text screen.

The account-index keypad is deliberately untouched. Its prefixes are all
already valid in-range values, so it has nothing to prune and no early-ending
question to answer — the keypad costs it nothing. Converting it would be
consistency for its own sake, and is easy to add later if that reads better.

Testing

./tools/run-simulator.sh --self-test     # passes
python3 -m unittest discover -s tests    # 47 passed

Neither suite reaches rendering or navigation, so this change is almost
entirely outside what they cover
. It was driven by hand in the simulator, and
the 240 dead end above was found that way rather than by any test — after the
first version had already been pushed. Worth knowing when reviewing: the box
geometry (34x32 boxes, 11x7 arrows, OK centred in a box) is arithmetic that
has only been checked by eye on the simulator, not measured.

The firmware build and its reproducibility check have not been run here — only
the host build and the Python suite.

Notes

Branched from main. Merges cleanly with #3 and #6.

Conflicts with #4, and not trivially: that PR threads a checksum filter
through enter_word_number, and this rewrites that function. If #4 lands
first I will reapply the filter on top of the ring — it fits well there, since
the ring is built per box and would simply drop digits that cannot lead to an
allowed word, making the impossible last words unreachable on the number pad
the same way they are on the letters.

sandman21vs and others added 3 commits August 15, 2026 20:21
Word numbers were typed on a twelve-key keypad: the two buttons walked a cursor
across it and the reader had to find each digit, then find Accept. A number is
not a position in a grid, and the buttons were spending both their gestures on
navigation rather than on the value.

Four boxes now, scrolled one at a time - up and down *are* the digit. Adapted
from Blockstream Jade's PIN and index entry (main/ui/digit_entry.c), which
shows the same three states: a box still to come, the box being set, and a box
already set.

Always four boxes. A printed wordlist pads to four digits and whoever reads
`0004` off it types all four, so a fixed width costs nothing and removes the
Accept key along with the question of when a number is finished. Only digits
that still leave a word number reachable are on the ring, which is what keeps
2049 untypeable while 2048 stays reachable; the ring carries a backspace past
the last digit, the way enter_value's ring already carried [back]. Going back
from the confirmation reopens the last digit rather than clearing the number.

Dice rolls scroll in one wider box for the same reason, keeping their quality
bar underneath. A card keeps the text screen: "A Clubs" does not fit a box, and
its running transcript wants the room.

seedtool_render_dice_screen's bar moved into draw_progress_bar so both screens
draw the same one rather than each carrying a copy.
Four fixed boxes made 240 a dead end. No fourth digit takes 240x into 1..2048,
so the reader who typed it found a box holding nothing but backspace, and the
only way to mean 240 was to have known to type 0240 from the start.

The field is variable length now. The ring carries the digits that still lead
somewhere, then OK once what is typed already names a word, then backspace -
the same ring Jade's index entry uses, where a short number also has to be able
to stop early. 240 is three presses and an OK.

Ending early is not a convenience here. Without it, three-digit numbers are not
reachable at all except by a leading zero the reader has no reason to expect.

OK appears only once the digits already name a word, so an empty field does not
offer it. On a prefix like 20 it does appear, because 20 is both a number and a
step towards 200x - both readings are live, which is why the reader is the one
who says which was meant. A fourth digit still confirms itself: there is no
fifth box to put an OK in.

Going back from the confirmation reopens the fourth digit when it was the
fourth digit that confirmed, and returns to the field untouched when the reader
ended with OK.
The branch predated oroderico#4, and rewriting enter_word_number around the scrolled
field dropped the wordset the letter keyboard narrows by: the signature lost
`allowed` and the body called the unnarrowed seedtool_word_number. On a
mnemonic's last word that leaves the number pad accepting words the letters
refuse - the exact asymmetry oroderico#4 exists to prevent, and worse than having
neither, since the reader who used the number pad has been told nothing.

Nothing caught it. The self-test proves seedtool_word_number_in exact against
all 2048 words, but that proves the function; the screen quietly not calling it
leaves every test green. So the filter is back in three places and pinned in a
fourth:

- the ring is built from seedtool_next_digits_in, so a digit that cannot begin
  an allowed word is never scrollable to in the first place;
- OK appears only when seedtool_word_number_in says the digits already name an
  allowed word;
- the confirmation gate narrows too. The ring should already have made a
  disallowed number untypeable, but this is where a number becomes a word, and
  a gate inheriting its guarantee from the screen upstream is one bug away from
  accepting what that screen was supposed to refuse - the same "verified rather
  than inherited" the address index gets in browse_addresses;
- a source-grep test asserts enter_word_number takes the set and calls neither
  plain function, proved by putting the unnarrowed call back and watching it
  fail by name.

The box geometry was arithmetic checked by eye. Four _Static_asserts now hold
it: the widest field against the display, and the arrows against the boxes,
the title and the footer. The row is centred, so boxes that do not fit run off
both edges where fill_rect clips them silently - a field that looks wrong on
the device and fails nowhere. SEEDTOOL_DIGIT_BOXES_MAX names what the renderer
can draw, and the app asserts a word number's digits fit inside it, so the two
ends are held together rather than agreeing by coincidence. Proved by widening
a box to 60px and watching the build fail.

Verified with the host self-test and 50 Python tests.
@oroderico
oroderico merged commit 86bbf7c into oroderico:main Aug 15, 2026
3 checks passed
@sandman21vs
sandman21vs deleted the scrolled-number-entry branch August 15, 2026 20:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants