Skip to content

Draw every "choose among N" screen as one list - #160

Merged
aido merged 2 commits into
aido:bip85from
buzzromain:feat/one-idiom-per-screen-family-bip85
Aug 12, 2026
Merged

aido merged 2 commits into
aido:bip85from
buzzromain:feat/one-idiom-per-screen-family-bip85

Conversation

@buzzromain

@buzzromain buzzromain commented Aug 12, 2026 •

Copy link
Copy Markdown

The touch stack carried two idioms for the same question. Four screens ask the
user to pick one of a fixed set of entries -- the menu of intentions, the
BIP-39 phrase length, the PIN length, the list of BIP-85 secrets -- and one of
them was the SDK's list while the other three were hand-built stacks of
nbgl_button_t. This gives all four the same component and writes down the
rule, at the point where the family is built, rather than leaving it to be
rediscovered on the next screen added.

Why the list and not the stack

The stack grows upwards from the bottom margin, so the entry that pushes it
too far is drawn over the title. That is not hypothetical: it is what
happened when the secret list gained a fourth entry, and Flex drew "PIN"
across the second line of the question. Nothing in a test could see it --
Speculos reports every text event at its full height with its full text, which
is the silence reviews.assert_body_clears_button() exists to break. A list
paginates instead.

Two more things go with it: the entries read top-down in the order they are
written (the stack read bottom-up, so the tests counted from the end the code
did not), and there is one back arrow instead of two geometries --
generic_screen_set_back_button() drew a BUTTON_DIAMETER square 4px from
the top, against the SDK's BACK_BUTTON_HEADER_HEIGHT band, and the two
coincided on the three current devices by arithmetic rather than by design.

src/nbgl/layout_generic_screen.c had no callers left and is gone with them.

Measured, not assumed

The objection to the list was that three short entries on a large screen would
look empty. Both forms were captured on all three devices before this was
settled: the list does leave the bottom of the screen empty -- about 45% of
Stax, 35% of apex_p -- and that is the cost. It buys the reading order, the
single back arrow, and the removal of a class of defect nothing could check.
A short list at the top of a large screen is also what the SDK's own settings
screens look like.

The subtitle a bar can carry does not reach this component:
nbgl_layoutBar_t has a subText field, but the nbgl_contentBarsList_t
that a generic configuration takes is texts and tokens and nothing else.

What else changed on those screens

  • No icon. BIP39_ICON and BIP85_ICON were on the two length screens.
    The icons this repository authored name formats, and these screens ask a
    quantity.
  • No emphasised entry. The longest phrase and the longest PIN were drawn
    black. A black control in this application means an act with a consequence;
    "the largest of three amounts" was a third meaning for that signal, and on
    "How long is your Recovery Phrase?" it answered a question about a fact
    with a recommendation. The PIN screen loses a real recommendation by it, and
    the code says so where the entry used to be emphasised.
  • No hand-placed \n. The list header wraps on words by itself. The three
    titles that carried a break carried it because the hand-built title areas
    left wrapping clear and broke on characters.
  • The lengths read ascending, 12/18/24 and 4/6/8, where the stack read
    them downwards because it was written upwards.

Rules written down

One per family, in the file where the family is built:

  • what draws "choose among N", and what that excludes, above
    display_choice_list();
  • when a number is typed rather than chosen, above the keypad screens;
  • what a black control means, when an icon has a place, and which component
    owns a title, at the top of src/nbgl/ui.c -- these three are signals used
    across families, so no single screen could settle them.

Deliberate divergences keep their reason at the site of the divergence: the
menu's absent icon, the Check journey's absent explanation, the grey
tap-to-continue against the black button on explanations, the icons on
verdicts and warnings, and the warning as the review's last page.

Tests

The three screens that changed idiom changed driver: genericlayout.py, which
carried a table of coordinates counted from the bottom, is replaced by
choicelist.py, which carries names only -- ragger's ChoiceList already
knows where the rows are. test_menu_positions.py still holds the mapping the
same way, by touching each constant and asserting the screen it arrives on.

Verification

  • six targets built without a warning;
  • 83/83 unit tests;
  • the functional campaign on the five emulated devices, one device per
    invocation: nanox 13 passed, nanosp 13, stax 32, flex 32, apex_p 32, and
    no failure on any of them;
  • grep -rnE '%\.\*s|%s' src/ clean, clang-format --dry-run --Werror clean
    over all of src/;
  • before/after captures on stax, flex and apex_p, referenced below.

Flash and RAM

RAM is _ebss - _bss; the bss column of size is a fixed region here.

target flash before flash after RAM before RAM after
nanos 42696 42696 2760 2760
nanos2 49736 49736 5265 5265
nanox 49736 49736 5265 5265
stax 83569 82033 7846 7850
flex 84109 82573 7786 7790
apex_p 79473 78449 7786 7790

The three Nano targets are byte-identical: nothing in this touches the
two-button stack.

Captures

On the orphan branch screenshots/one-idiom-per-choice of the fork, as the
other capture branches are. Each screen on stax, flex and apex_p, at half the
device's real width so the three stay comparable to each other.

The menu of intentions

Four entries. The header wraps the question on words by itself, where the hand-built title area broke on characters and carried a placed \n.

before after
Stax before menu stax after menu stax
Flex before menu flex after menu flex
Apex before menu apex_p after menu apex_p

The BIP-39 phrase length

The icon named a format over a question about a quantity; the black entry answered a question about a fact with a recommendation. The lengths now read upwards, 12 / 18 / 24, where the stack read them down because it was built up.

before after
Stax before phrase-length stax after phrase-length stax
Flex before phrase-length flex after phrase-length flex
Apex before phrase-length apex_p after phrase-length apex_p

The BIP-85 secret list

Unchanged -- already this component. It is here because it is what the other three now look like, and because its fourth entry is what ended the stack.

before after
Stax before which-secret stax after which-secret stax
Flex before which-secret flex after which-secret flex
Apex before which-secret apex_p after which-secret apex_p

The PIN length

Same two removals as the phrase length. This is the screen that loses a real recommendation by it; see the note at the end.

before after
Stax before pin-length stax after pin-length stax
Flex before pin-length flex after pin-length flex
Apex before pin-length apex_p after pin-length apex_p

One thing left open for review

The PIN length screen loses "8 digits" drawn black, which said "the safest of
the three". A bar carries one line of text and no emphasis, and the one
meaning left for a black control is an act with a consequence -- so the
recommendation is not said anywhere now. Restoring it would mean a
CHOICES_LIST with an initChoice, or putting it in the label; neither is in
this change, and the comment where the emphasis used to be says the loss is
paid rather than overlooked.

Four screens ask the user to pick one of a fixed set of entries -- the menu
of intentions, the BIP-39 phrase length, the PIN length, and the list of
BIP-85 secrets. One of them was the SDK's list; the other three were stacks
of nbgl_button_t built by hand. They are all the list now, through
display_choice_list(), and the rule is written above it rather than left to
be rediscovered on the next screen added.

The stack grows upwards from the bottom margin, so the entry that pushes it
too far is drawn over the title, and no test can see it: Speculos reports
every text event at its full height with its full text. That is what the
secret list hit when it gained a fourth entry. A list paginates instead.

Two more things go with the component. The entries read top-down in the
order they are written, where the stack read bottom-up. And there is one
back arrow, the SDK's, where generic_screen_set_back_button() drew a second
of its own geometry that coincided with it on today's three devices by
arithmetic rather than by design. layout_generic_screen.c has no callers
left and goes with them.

The two length screens lose their icon and their black entry. The icons
this repository authored name formats and these screens ask a quantity; a
black control in this application means an act with a consequence, and "the
largest of three amounts" was a third meaning for that signal -- on "How
long is your Recovery Phrase?" it answered a question about a fact with a
recommendation. The PIN screen loses a real one by it, and the comment
where the emphasis used to be says so.

Three titles lose the "\n" they carried: the list header wraps on words by
itself, where the hand-built title areas left `wrapping` clear and broke on
characters.

Measured before concluding, on captures of both forms on all three devices:
a list of three entries does leave the bottom of the screen empty, about 45%
of Stax and 35% of apex_p. That is the cost, and it is written down with
what it buys.
Three of the four "choose among N" screens changed idiom, so their tests
change driver. genericlayout.py carried a table of touch coordinates counted
from the bottom of the screen, because the hand-built stack grew upwards
from the bottom margin and its first entry was the last line drawn. Nothing
counts backwards any more: ragger's ChoiceList already knows where the rows
of an SDK list are, so choicelist.py replaces the table with names alone --
the four intentions, the four secrets, the three phrase lengths and the
three PIN lengths.

The two length screens keep the numbers they had, which is a coincidence
worth naming rather than relying on: the stack was written 12, 18, 24 and
drawn bottom-up, so the shortest was already row 1 from the bottom and is
row 1 from the top now. The callers say WORDS_12 and DIGITS_6 instead, so
nobody has to know that.

test_menu_positions.py still holds the mapping the only way that settles it,
by touching each constant and asserting the screen it arrives on.

The back arrow of the PIN length screen is the SDK's header arrow now, so
its test drives UseCaseSubSettings().exit() -- and the comment warning that
the application's own square could stop overlapping the SDK's header on a
future device goes with the square.
@buzzromain

buzzromain commented Aug 12, 2026 •

Copy link
Copy Markdown
Author

Every screen of the four journeys, on the three touch devices, on this
branch's tree -- so the changed screens can be read in the company they are
actually seen in. Screens this pull request changed are marked. No screen
of any walk overprints its text on any device.

Check — Straight from the menu to the length: entering the Phrase *is* the task, so this walk has no explanation in front of it. (5 screens)
step stax flex apex_p
01 Home home stax home flex home apex_p
02 The menu ⬅ changed menu stax menu flex menu apex_p
03 Phrase length ⬅ changed phrase-length stax phrase-length flex phrase-length apex_p
04 Typing the Phrase keyboard stax keyboard flex keyboard apex_p
05 The verdict verdict stax verdict flex verdict apex_p
Backup — The long one, and the one that crosses the most families: two explanations, two keypads, and a review whose last page is the warning. (11 screens)
step stax flex apex_p
01 Home home stax home flex home apex_p
02 The menu ⬅ changed menu stax menu flex menu apex_p
03 What a backup is explain-backup stax explain-backup flex explain-backup apex_p
04 Phrase length ⬅ changed phrase-length stax phrase-length flex phrase-length apex_p
05 The verdict verdict stax verdict flex verdict apex_p
06 How many Shares explain-shares stax explain-shares flex explain-shares apex_p
07 Share count share-count-keypad stax share-count-keypad flex share-count-keypad apex_p
08 What a threshold is explain-threshold stax explain-threshold flex explain-threshold apex_p
09 Threshold threshold-keypad stax threshold-keypad flex threshold-keypad apex_p
10 The review review stax review flex review apex_p
11 The warning, last page of the review review-warning stax review-warning flex review-warning apex_p
Recover — Short as far as the keyboard; what follows is twenty-nine ByteWords per share. (4 screens)
step stax flex apex_p
01 Home home stax home flex home apex_p
02 The menu ⬅ changed menu stax menu flex menu apex_p
03 How recovery works explain-recover stax explain-recover flex explain-recover apex_p
04 Typing the ByteWords bytewords-keyboard stax bytewords-keyboard flex bytewords-keyboard apex_p
Derive — The only walk that crosses **two** screens of the family in a row -- the secret list, then the PIN length. (11 screens)
step stax flex apex_p
01 Home home stax home flex home apex_p
02 The menu ⬅ changed menu stax menu flex menu apex_p
03 What BIP85 is explain-bip85 stax explain-bip85 flex explain-bip85 apex_p
04 Which secret ⬅ changed which-secret stax which-secret flex which-secret apex_p
05 PIN length ⬅ changed pin-length stax pin-length flex pin-length apex_p
06 What an index is explain-index stax explain-index flex explain-index apex_p
07 Index index-keypad stax index-keypad flex index-keypad apex_p
08 The review review stax review flex review apex_p
09 The warning, last page of the review review-warning stax review-warning flex review-warning apex_p
10 The derived secret result stax result flex result apex_p
11 Back home back-home stax back-home flex back-home apex_p

@aido

aido commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Hi @buzzromain,

A lot of these recent changes will make it easier to migrate the Nano X and Nano S+ to nbgl in the future.
Again, its a pity Ledger are not merging the BIP85 PR as these changes are really giving the app a bit of polish and improving the UX.

@aido
aido merged commit 7dcba51 into aido:bip85 Aug 12, 2026
10 checks passed
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