# KB2040 Hidden Signer — Display Screens Reference for the on-device UI so we can iterate on screen designs. - Panel: **SSD1327 128x128 grayscale OLED** (I2C, STEMMA QT, addr `0x3D`). - Rendering source: [`display.cpp`](./display.cpp), driven by call sites in [`kb2040_hidden_signer.ino`](./kb2040_hidden_signer.ino). - Grayscale levels in use: - dim (headers / outlines): `0x04` - bar fill: `0x08` - bright text: `0x0F` - Each screen historically ended with a bottom **status line** at `y=118`, drawn by `drawStatusLine()`. The status-line writes are currently commented out in the sketch (they were being overwritten by debug/diagnostic prints), so that line now stays effectively empty unless re-enabled. The boxes below are schematic (the panel is 128x128 px, not character-aligned). They show layout intent, relative position, and the text/labels actually printed. --- ## 1. Volume View Source: `drawVolumeView()` in [`display.cpp`](./display.cpp:60). Top-level "media mode" screen. Header switches between `MODE: MEDIA` / `MODE: SIGNER`. ``` +--------------------------------+ | MODE: MEDIA | <- tiny, dim (size 1) | | | VOL | <- size 2, dim | | | | | #### #### | | ## ## # ## | <- "50" centered, size 6 | ## ## # ## | (huge digits) | ## ## # ## | | #### #### | | | | +------------------------+ | <- volume bar outline | |########## | | fill = volume % | +------------------------+ | | | | [status line] | <- currently disabled +--------------------------------+ ``` --- ## 2. Signer Menu Source: `drawSignerMenu()` in [`display.cpp`](./display.cpp:102). Two items; `>` marks the current selection (shown here on index 0). ``` +--------------------------------+ | SIGNER MENU | <- size 1, dim | | | | | > Generate new | <- size 2, selected | | | | | Enter mnemonic | <- size 2 | | | | | | | | | [status line] | <- currently disabled +--------------------------------+ ``` --- ## 3. Enter Mnemonic (in-place 2-column grid) Source (planned): `drawEnterMnemonic()` in [`display.cpp`](./display.cpp). Replaces the old separate `PICK FIRST LETTER` modal + `WORD PICK` modal with a single grid that looks like the Generated Phrase screen but is filled in by the user. Header reads `ENTER MNEMONIC`. Layout: 2 columns x 6 rows (slots 1-6 left, 7-12 right), same geometry as the Generated Phrase screen. All 12 numbers are shown; words are blank until entered. A cursor marks the active slot (starts at slot 1). ``` +--------------------------------+ | ENTER MNEMONIC | <- size 1, dim |> 1 m 7 | <- cursor '>' on active slot | 2 8 | slot 1 building: "m" | 3 9 | | 4 10 | | 5 11 | | 6 12 | | | | back letter | <- footer legend (stage-dependent) +--------------------------------+ ``` ### Per-word 3-stage entry (in the active slot) Only the letters chosen so far are shown; a full word appears only at stage 3. | Stage | Scroll wheel cycles | Active slot shows | PLAY does | | ----- | ---------------------------------- | ----------------- | -------------------------- | | 1 | first letter `a`..`z` | `m` | lock letter 1 -> stage 2 | | 2 | valid 2nd letters for letter 1 | `mo` | lock letter 2 -> stage 3 | | 3 | words matching first two letters | `mosaic` | select word -> next slot | - After slot 12's word is selected: build phrase, run `mnemonic_validate_basic()`, then go to Signer Ready (or INVALID modal). ### BACK behavior (context-sensitive erase) `BACK` = the NEXT button (PREV stays unused here), consistent with other entry screens. | Position | BACK action | | -------------------------------- | --------------------------------------------- | | Stage 3 (choosing word) | erase 2nd letter -> return to stage 2 | | Stage 2 (choosing 2nd letter) | erase 1st letter -> return to stage 1 | | Stage 1, slot N > 1 | clear previous slot's word, move cursor back to slot N-1 at stage 1 | | Stage 1, slot 1 | exit to Signer Menu | ### Footer legend (stage-dependent) | Stage | PREV (left) | PLAY (center) | NEXT (right) | | ------- | ----------- | ------------- | ------------ | | Stage 1 | `-` | `letter` | `back` | | Stage 2 | `-` | `letter` | `back` | | Stage 3 | `-` | `select` | `back` | ### New wordlist helpers required (in `mnemonic_kb.cpp` / `.h`) - `mnemonic_second_letters(char first, char *out, uint8_t *count)` -> set of valid 2nd letters given the 1st letter. - `mnemonic_prefix_range(char first, char second, uint16_t *start, uint16_t *count)` -> wordlist index range for all words starting with those two letters. (`mnemonic_letter_range()` already provides the first-letter range and can back the stage-1 -> stage-2 transition.) --- ## 4. Mnemonic Page (Generated Phrase) Source: `drawMnemonicPage()` in [`display.cpp`](./display.cpp:152). Shows generated phrase in one screen: 2 columns × 6 rows (all 12 words, no spacer rows). ``` +--------------------------------+ | GENERATED PHRASE | <- size 1, dim | 1 abandon 7 issue | <- size 1, tight rows | 2 ability 8 jungle | | 3 able 9 kitchen | | 4 about 10 lemon | | 5 above 11 mosquito | | 6 absent 12 napkin | | | | PLAY confirm NEXT cancel | <- size 1 | | | [status line] | <- currently disabled +--------------------------------+ ``` --- ## 5. Message (generic 2-line modal) Source: `drawMessage()` in [`display.cpp`](./display.cpp:193). Generic title + 2 lines. Reused for several prompts. Example — **Approval prompt** (title `APPROVAL`): ``` +--------------------------------+ | APPROVAL | <- size 1, dim | | | | | PLAY=Allow | <- size 2 | | | PREV=Deny | <- size 2 | | | | | | | | | | | [status line] | <- currently disabled +--------------------------------+ ``` Other current uses of this same screen: - `PICK FIRST LETTER` with lines `First letter` / `Letter: a` - `INVALID` with `Bad mnemonic` / `NEXT to return` - `ERROR` with `Generate failed` / `Try again` --- ## 6. Signer Ready Source: `drawSignerReady()` in [`display.cpp`](./display.cpp:210). Shown while waiting for the host to request signing. ``` +--------------------------------+ | SIGNER READY | <- size 1, dim | | | +------------------------+ | | | Awaiting | | <- size 2 | | | | | | host | | <- size 2 | +------------------------+ | | | | NEXT/PLAY: menu | <- size 1 | | | [status line] | <- currently disabled +--------------------------------+ ``` --- ## Notes / observations for redesign - The bottom status line frequently **duplicated** the in-body hint text (e.g. Word Pick showed `ENC scroll PLAY select` both in the body and in the status line). With status writes disabled, that redundancy is gone. - `PICK FIRST LETTER` reuses the generic Message screen rather than having a dedicated layout — a candidate for a purpose-built screen. - All hint text (`ENC` / `PLAY` / `NEXT` / `PREV`) refers to the physical controls: rotary encoder + the PLAY/NEXT/PREV buttons. ``` --- ## Screen ↔ render-function map | Screen | Render function | display.cpp | | --------------- | --------------------- | ----------- | | Volume View | `drawVolumeView()` | [60](./display.cpp:60) | | Signer Menu | `drawSignerMenu()` | [102](./display.cpp:102) | | Word Pick | `drawWordPick()` | [123](./display.cpp:123) | | Mnemonic Page | `drawMnemonicPage()` | [152](./display.cpp:152) | | Message (modal) | `drawMessage()` | [193](./display.cpp:193) | | Signer Ready | `drawSignerReady()` | [210](./display.cpp:210) | | Status line | `drawStatusLine()` | [53](./display.cpp:53) | --- # PROPOSAL: three-zone button legend footer The three physical buttons sit directly below the screen in this order: ``` PREV PLAY/PAUSE NEXT ^ ^ ^ (left) (center) (right) ``` Idea: replace the free-text status line with a fixed **3-zone legend** along the bottom of every screen. Each zone shows a single short verb for what that button does on the current screen — left-aligned, center-aligned, right-aligned so each label sits over its physical button. No need to print the button name; position implies it. An empty zone means that button does nothing on that screen. ### Layout math (size 1 font, 6 px/char on a 128 px panel) - Left zone: left-aligned at `x = 2`. - Center zone: centered around `x = 64` (use `getTextBounds` to center). - Right zone: right-aligned to `x = 126` (compute `x = 126 - width`). - Budget ~7 chars per zone worst case (7 x 6 = 42 px x 3 = 126 px). Keep verbs <= 7 chars (`select`, `confirm`, `cancel`, `back`, `deny`, `allow`, `next`, `prev`) to guarantee no overlap. - Optional: a thin divider line above the legend at `y ~= 112`, and 1 px vertical ticks between zones to reinforce the 3-button mapping. ``` +--------------------------------+ | ...screen body... | | | | -------------------------------| <- optional divider (y~112) | prev confirm next | <- 3 zones (left/center/right) +--------------------------------+ ^PREV ^PLAY ^NEXT (physical buttons below glass) ``` ### Per-screen legend mapping (derived from `signer_mode_tick()`) Encoder still scrolls/selects on every screen; the legend only describes the 3 buttons. `-` = button does nothing on that screen. | Screen / state | PREV (left) | PLAY (center) | NEXT (right) | | -------------------------------------- | ----------- | ------------- | ------------ | | Volume / Media view | `prev` | `play` | `next` | | Signer Menu (`SIGNER_UI_MENU`) | `-` | `select` | `-` | | Generated Phrase (`GENERATE_REVIEW`) | `cancel` | `confirm` | `new` | | Signer Ready (`READY`) | `-` | `-` | `menu` | | Letter Pick (`ENTER_LETTER`) | `-` | `confirm` | `back` | | Word Pick (`ENTER_PICK`) | `-` | `select` | `back` | | Approval prompt (Message modal) | `deny` | `allow` | `-` | Notes: - `PREV` is currently unused in most signer entry screens; it is meaningful in Media view (`prev` track), the Approval prompt (`deny`), and now the Generated Phrase screen (`cancel`). ### Behavior change required: Generated Phrase (`SIGNER_UI_GENERATE_REVIEW`) The legend wording above implies a **functional change** in [`signer_mode_tick()`](./kb2040_hidden_signer.ino:657), not just labels: | Button | Current behavior | Proposed behavior | | ------ | --------------------------- | ------------------------------------------ | | PREV | (unused) | `cancel` -> return to Signer Menu | | PLAY | confirm -> save seed | `confirm` -> save seed (unchanged) | | NEXT | cancel -> Signer Menu | `new` -> regenerate a fresh 12-word phrase and refresh this screen | Implementation detail for `new`: - On `s_evt_next_pressed`, call `generate_mnemonic_12(s_generated_mnemonic, ...)` again, then `signer_enter_review_screen()` to redraw with the new words. - On `s_evt_prev_pressed`, call `signer_enter_menu_screen()` (the old NEXT path). > Dependency check (resolved): `s_evt_prev_pressed` already exists and is wired > the same way as PLAY/NEXT — declared at [line 112](./kb2040_hidden_signer.ino:112), > reset in [`update_button_events()`](./kb2040_hidden_signer.ino:560), set on PREV > press at [line 581](./kb2040_hidden_signer.ino:581). No new plumbing needed. ### What this replaces on each screen - Word Pick body lines `ENC scroll PLAY select` / `NEXT back` -> removed; the encoder action stays implicit, and PLAY/NEXT move to the footer legend. - Mnemonic Page body line `PLAY confirm NEXT cancel` -> moves to the footer legend (frees a body row). - Signer Ready body line `NEXT/PLAY: menu` -> becomes a single `menu` in the NEXT zone. - Approval modal `PLAY=Allow` / `PREV=Deny` body lines -> footer legend (`deny` left / `allow` center). ### Implementation sketch 1. Add a helper `drawButtonLegend(const char *left, const char *center, const char *right)` in [`display.cpp`](./display.cpp) that renders the 3 aligned zones (plus the optional divider/ticks) at the bottom. 2. Give each screen a small static legend (or pass it in) and call `drawButtonLegend(...)` instead of `drawStatusLine()`. 3. Remove the now-redundant in-body hint rows listed above. 4. Keep `drawStatusLine()` available (commented/disabled) for transient debug. ### Open design decisions (need your call) 1. **NEXT "back" wording**: use the word `back` vs a glyph like `<` / `<<`. 2. **Divider/ticks**: include the thin divider line + inter-zone ticks, or keep it text-only for a cleaner look. 3. **Empty zones**: render nothing, or a subtle `·` placeholder so the user can tell the button is intentionally inactive. 4. **Media view**: apply the same legend (`prev` / `play` / `next`), or leave the Volume view visually as-is since its meaning is already obvious.