Skip to content

Commit c536f3e

Browse files
authored
Merge pull request #11 from sandman21vs/nav-back-confirm
One back arrow and one confirm bar, instead of a footer per screen
2 parents b701ff3 + 5c85aff commit c536f3e

7 files changed

Lines changed: 1251 additions & 289 deletions

File tree

‎host/origo_sdl.c‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,33 @@ void seedtool_display_list(const char* title, const char* const* items, const si
140140
present();
141141
}
142142

143+
void seedtool_display_nav_text(
144+
const seedtool_nav_t* nav, const char* title, const char* line1, const char* line2, const char* line3)
145+
{
146+
seedtool_render_nav_text(nav, title, line1, line2, line3);
147+
present();
148+
}
149+
150+
void seedtool_display_nav_rows(const seedtool_nav_t* nav, const char* title, const char* const* rows, const size_t count)
151+
{
152+
seedtool_render_nav_rows(nav, title, rows, count);
153+
present();
154+
}
155+
156+
void seedtool_display_nav_list(
157+
const seedtool_nav_t* nav, const char* title, const char* const* items, const size_t count, const size_t top)
158+
{
159+
seedtool_render_nav_list(nav, title, items, count, top);
160+
present();
161+
}
162+
163+
void seedtool_display_nav_dice(const seedtool_nav_t* nav, const char* title, const char* line1, const char* line2,
164+
const seedtool_progress_t* progress)
165+
{
166+
seedtool_render_nav_dice(nav, title, line1, line2, progress);
167+
present();
168+
}
169+
143170
void seedtool_display_keyboard(const char* title, const char* text, const char* layout, const bool* enabled,
144171
const size_t selected, const size_t position, const size_t total)
145172
{

‎host/origo_simulator.c‎

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1476,6 +1476,297 @@ static bool backup_confirm_screen_lines_do_not_wrap(void)
14761476
return true;
14771477
}
14781478

1479+
/* Lit rows of the nav chrome, scanned band by band. The chrome packs a title
1480+
* bar, three list rows and a confirm bar into 135 pixels with single-digit
1481+
* gaps between them, and draw_centered_box wraps rather than clips - so a
1482+
* title or a button label one word too long lands in the band below instead
1483+
* of running off the glass, exactly the failure the dice-hint check above
1484+
* exists for. Bands are the geometry in seedtool_render.c (NAV_BACK_HEIGHT,
1485+
* LIST_TOP, NAV_ROW_HEIGHT, NAV_BAR_Y), kept in sync with the numbers here. */
1486+
static bool nav_band_is_clear(const int from, const int to)
1487+
{
1488+
const uint16_t* const pixels = seedtool_render_pixels();
1489+
for (int y = from; y < to; ++y) {
1490+
for (int x = 0; x < SEEDTOOL_DISPLAY_WIDTH; ++x) {
1491+
if (pixels[y * SEEDTOOL_DISPLAY_WIDTH + x] != 0x0000) {
1492+
return false;
1493+
}
1494+
}
1495+
}
1496+
return true;
1497+
}
1498+
1499+
/* Nothing lit at or right of `x` in the title band. Measured on the right
1500+
* rather than over the arrow on the left, because that is the side a title too
1501+
* wide for its column shows up on: draw_centered_box centres by an offset it
1502+
* clamps at zero, so an overlong line starts hard against its left edge and
1503+
* runs off the right one. A title that clears this has cleared the arrow. */
1504+
static bool nav_title_stays_in_its_column(const int x)
1505+
{
1506+
const uint16_t* const pixels = seedtool_render_pixels();
1507+
for (int y = 0; y < 20; ++y) {
1508+
for (int column = x; column < SEEDTOOL_DISPLAY_WIDTH; ++column) {
1509+
if (pixels[y * SEEDTOOL_DISPLAY_WIDTH + column] != 0x0000) {
1510+
return false;
1511+
}
1512+
}
1513+
}
1514+
return true;
1515+
}
1516+
1517+
/* The confirm bar's band (NAV_BAR_Y..the bottom of the glass), with its label
1518+
* required to stay a few pixels clear of both edges. */
1519+
/* NAV_BAR_Y in seedtool_render.c, kept in sync with this. */
1520+
#define NAV_BAR_BAND_Y 118
1521+
1522+
static bool nav_bar_label_fits(void)
1523+
{
1524+
const uint16_t* const pixels = seedtool_render_pixels();
1525+
int leftmost = SEEDTOOL_DISPLAY_WIDTH, rightmost = -1;
1526+
for (int y = NAV_BAR_BAND_Y; y < SEEDTOOL_DISPLAY_HEIGHT; ++y) {
1527+
for (int x = 0; x < SEEDTOOL_DISPLAY_WIDTH; ++x) {
1528+
if (pixels[y * SEEDTOOL_DISPLAY_WIDTH + x] == 0x0000) {
1529+
continue;
1530+
}
1531+
/* The rule drawn along the top of an unfilled bar spans the whole
1532+
* width and is not the label; skipped by row, not by colour, so a
1533+
* label sharing the rule's colour still counts. */
1534+
if (y == NAV_BAR_BAND_Y) {
1535+
continue;
1536+
}
1537+
if (x < leftmost) {
1538+
leftmost = x;
1539+
}
1540+
if (x > rightmost) {
1541+
rightmost = x;
1542+
}
1543+
}
1544+
}
1545+
if (rightmost < 0) {
1546+
return false; /* the label never reached the screen */
1547+
}
1548+
return leftmost >= 4 && rightmost <= SEEDTOOL_DISPLAY_WIDTH - 5;
1549+
}
1550+
1551+
static bool nav_chrome_bands_do_not_collide(void)
1552+
{
1553+
/* Twelve rows of the widest label review_and_confirm can build: two
1554+
* digits, a dot, a space and the longest word in the BIP39 list. */
1555+
static const char* const words[12] = { "01. wildcat", "02. abandon", "03. zoo", "04. mosquito", "05. jealous",
1556+
"06. abandon", "07. abandon", "08. abandon", "09. abandon", "10. abandon", "11. abandon", "12. abandon" };
1557+
/* Both titles review_and_confirm shows, and the label on its confirm bar,
1558+
* copied from seedtool_app.c the same way the dice hints above are. */
1559+
static const char* const titles[] = { "Review words", "Review - fix a word" };
1560+
for (size_t i = 0; i < sizeof(titles) / sizeof(titles[0]); ++i) {
1561+
/* Every state the chrome can be drawn in, since each paints something
1562+
* the others do not: the arrow highlighted, an item highlighted with
1563+
* the confirm bar dimmed, and the confirm bar filled. */
1564+
static const size_t selections[] = { SEEDTOOL_NAV_BACK, 0, SEEDTOOL_NAV_CONFIRM };
1565+
for (size_t s = 0; s < sizeof(selections) / sizeof(selections[0]); ++s) {
1566+
/* Item 0 stands for the invalid-checksum screen, the one case
1567+
* where a word is the selection and the bar cannot be taken - so
1568+
* that pass is the one that draws the bar dimmed. */
1569+
const bool confirm_enabled = selections[s] != 0;
1570+
const seedtool_nav_t nav = { .selected = selections[s],
1571+
.confirm = "Continue",
1572+
.confirm_enabled = confirm_enabled,
1573+
.back = true };
1574+
seedtool_render_nav_list(&nav, titles[i], words, 12, 0);
1575+
/* Title bar ends at 20, rows run 21..116 (LIST_TOP + 3 *
1576+
* NAV_ROW_HEIGHT, less the 2px each row leaves under itself), the
1577+
* confirm bar starts at 118. Two gaps must stay dark, or a band
1578+
* has grown into its neighbour. */
1579+
if (!nav_band_is_clear(20, 21) || !nav_band_is_clear(115, 118)) {
1580+
return false;
1581+
}
1582+
/* The title's column is inset by NAV_BACK_X + NAV_BACK_WIDTH at
1583+
* both ends, so its right edge is that far in from the glass. */
1584+
if (!nav_title_stays_in_its_column(SEEDTOOL_DISPLAY_WIDTH - (2 + 20))) {
1585+
return false;
1586+
}
1587+
/* The label on the bar, measured only in the states where the
1588+
* bar is not filled - a filled bar lights every pixel of the band
1589+
* by design, including the bottom row, and would swamp this. The
1590+
* label is drawn on one line by draw_centered_in, which does not
1591+
* wrap, so an overlong one runs off the right edge instead of
1592+
* down; measured on both sides so it is held clear of the glass
1593+
* rather than merely on it. */
1594+
if (selections[s] != SEEDTOOL_NAV_CONFIRM && !nav_bar_label_fits()) {
1595+
return false;
1596+
}
1597+
}
1598+
}
1599+
/* The text screen wearing the same chrome: enter_word_number's check of
1600+
* the number just typed. Its body is drawn by draw_centered, which wraps
1601+
* rather than clips, so line 2 growing to two lines would march down
1602+
* towards the bar - and the bar's own label is the widest this chrome
1603+
* carries anywhere. Strings copied from seedtool_app.c, the same way the
1604+
* dice hints above are. */
1605+
/* A menu wears the chrome with no confirm bar at all: its rows are its
1606+
* actions, so the arrow is the only control and the bar's band must stay
1607+
* dark rather than holding an empty outline. */
1608+
for (size_t s = 0; s < 2; ++s) {
1609+
const seedtool_nav_t nav = { .selected = s ? SEEDTOOL_NAV_BACK : 0, .back = true };
1610+
seedtool_render_nav_list(&nav, "Word entry", words, 2, 0);
1611+
if (!nav_band_is_clear(NAV_BAR_BAND_Y, SEEDTOOL_DISPLAY_HEIGHT)) {
1612+
return false;
1613+
}
1614+
}
1615+
static const struct {
1616+
const char* title;
1617+
const char* line1;
1618+
const char* line2;
1619+
const char* label;
1620+
} screens[] = {
1621+
/* Every screen on the chrome, with the widest text each can build -
1622+
* the longest word in the list, and every digit of the largest word
1623+
* number twice over. Copied from seedtool_app.c the same way the dice
1624+
* hints above are. */
1625+
{ "Word 12/24", "mosquito", "Number 2048 of 2048", "Use this word" },
1626+
{ "Confirm backup", "Retype 8 of the 24 words", "Have your backup ready", "Start quiz" },
1627+
{ "Checksum valid", "BIP39 English", "Derivation unlocked", "Open wallet" },
1628+
{ "Word doesn't match", "Check your backup", NULL, "Try again" },
1629+
{ "Compact SeedQR", "Encodes your ENTIRE seed", "A photo = total loss of funds", "Show QR" },
1630+
{ "QR export", "Account key included", "A photo reveals every address", "Show QR" },
1631+
{ "Descriptor export", "Account key included", "A photo reveals every address", "Show descriptor" },
1632+
{ "Confirm passphrase", "Enter it a second time", "Exact match required", "Enter again" },
1633+
{ "Poor entropy!", "128 of 128 bits", NULL, "Proceed anyway" },
1634+
{ "Pattern detected!", NULL, NULL, "Proceed anyway" },
1635+
};
1636+
for (size_t i = 0; i < sizeof(screens) / sizeof(screens[0]); ++i) {
1637+
for (int on_back = 0; on_back < 2; ++on_back) {
1638+
const seedtool_nav_t nav = { .selected = on_back ? SEEDTOOL_NAV_BACK : SEEDTOOL_NAV_CONFIRM,
1639+
.confirm = screens[i].label,
1640+
.confirm_enabled = true,
1641+
.back = true };
1642+
seedtool_render_nav_text(&nav, screens[i].title, screens[i].line1, screens[i].line2, NULL);
1643+
/* Line 2 sits at 65 and the 16px face is that tall again, so its
1644+
* wraps land at 81, 97, 113 - and 113 is inside the bar's own
1645+
* margin. One wrap is tolerated because the export warnings
1646+
* already take it on the plain screen this chrome inherits its
1647+
* body layout from ("A photo reveals every address" has always
1648+
* been two lines there); a second is what would reach the bar. */
1649+
if (!nav_band_is_clear(20, 21) || !nav_band_is_clear(97, 118)) {
1650+
return false;
1651+
}
1652+
if (!nav_title_stays_in_its_column(SEEDTOOL_DISPLAY_WIDTH - (2 + 20))) {
1653+
return false;
1654+
}
1655+
/* Measured only with the arrow selected, for the same reason as
1656+
* above: a selected bar is filled and would swamp this. */
1657+
if (on_back && !nav_bar_label_fits()) {
1658+
return false;
1659+
}
1660+
}
1661+
}
1662+
/* The same chrome with the entropy quality bar drawn in. Its band is
1663+
* different: the bar occupies DICE_BAR_Y=90..103, so what is checked here
1664+
* is the 104..118 gap between it and the confirm bar. A body line wrapping
1665+
* *into* the quality bar is not checked here and does not need to be -
1666+
* dice_screen_hints_clear_the_bar above already holds these same hints to
1667+
* that, at the same body heights this chrome inherits. */
1668+
static const struct {
1669+
const char* title;
1670+
const char* line1;
1671+
const char* line2;
1672+
const char* label;
1673+
} dice[] = {
1674+
{ "Coins", "256 flips needed", "Red bar = non-random", "Start" },
1675+
{ "D6 dice", "99 rolls needed", "Red bar = non-random", "Start" },
1676+
{ "Diamonds", "99 cards needed", "Return & reshuffle each card", "Start" },
1677+
{ "Coins", "256 of 256 bits", "Looks good", "Generate seed" },
1678+
};
1679+
const seedtool_progress_t full = { .rolls_pct = 100, .entropy_pct = 100, .warn = false, .complete = true };
1680+
for (size_t i = 0; i < sizeof(dice) / sizeof(dice[0]); ++i) {
1681+
for (int on_back = 0; on_back < 2; ++on_back) {
1682+
const seedtool_nav_t nav = { .selected = on_back ? SEEDTOOL_NAV_BACK : SEEDTOOL_NAV_CONFIRM,
1683+
.confirm = dice[i].label,
1684+
.confirm_enabled = true,
1685+
.back = true };
1686+
seedtool_render_nav_dice(&nav, dice[i].title, dice[i].line1, dice[i].line2, &full);
1687+
if (!nav_band_is_clear(20, 21) || !nav_band_is_clear(104, 118)) {
1688+
return false;
1689+
}
1690+
if (!nav_title_stays_in_its_column(SEEDTOOL_DISPLAY_WIDTH - (2 + 20))) {
1691+
return false;
1692+
}
1693+
if (on_back && !nav_bar_label_fits()) {
1694+
return false;
1695+
}
1696+
}
1697+
}
1698+
/* The notices: no arrow, so the title centres across the whole glass and
1699+
* has the full width to fit in - but the bar is still there, and the body
1700+
* still wraps rather than clips. Strings from seedtool_app.c. */
1701+
static const struct {
1702+
const char* title;
1703+
const char* line1;
1704+
const char* line2;
1705+
const char* label;
1706+
} notices[] = {
1707+
{ "Invalid checksum", "Check your words", "Fix one to continue", "Fix a word" },
1708+
{ "Passphrase mismatch", "Nothing was derived", "Try again", "Try again" },
1709+
{ "Backup confirmed", "Words matched", NULL, "Continue" },
1710+
{ "Too long for a QR", "Compact SeedQR", "Read it as text instead", "OK" },
1711+
{ "Error", "Could not derive addresses", NULL, "OK" },
1712+
{ "Error", "Could not compute", "word numbers", "OK" },
1713+
};
1714+
for (size_t i = 0; i < sizeof(notices) / sizeof(notices[0]); ++i) {
1715+
const seedtool_nav_t nav = { .selected = SEEDTOOL_NAV_CONFIRM,
1716+
.confirm = notices[i].label,
1717+
.confirm_enabled = true,
1718+
.back = false };
1719+
seedtool_render_nav_text(&nav, notices[i].title, notices[i].line1, notices[i].line2, NULL);
1720+
if (!nav_band_is_clear(20, 21) || !nav_band_is_clear(97, NAV_BAR_BAND_Y)) {
1721+
return false;
1722+
}
1723+
}
1724+
/* The paged screens. Their bodies run lower than the two-line ones -
1725+
* screen4's fourth row ends at 103 - and the page counter goes in what is
1726+
* left, so the band that must stay clear is only 116..118. Body lines are
1727+
* already held to their own widths by seedtool_render_fit, which is what
1728+
* page_text splits by; what is new here is the counter, and whether the
1729+
* two together clear the bar. */
1730+
static const size_t cursors[] = { SEEDTOOL_NAV_BACK, 0, SEEDTOOL_NAV_CONFIRM };
1731+
for (size_t c = 0; c < sizeof(cursors) / sizeof(cursors[0]); ++c) {
1732+
/* A full-width transcript line, and the widest counter either paged
1733+
* screen can reach: MAX_PAGE_LINES=24 gives page_text eight pages. */
1734+
const seedtool_nav_t paged = { .selected = cursors[c],
1735+
.confirm = "Continue",
1736+
.confirm_enabled = true,
1737+
.back = true,
1738+
.counter = "8/8" };
1739+
seedtool_render_nav_text(&paged, "Canonical transcript", "20-1-2-1-4-1-1-1-1-1-1-1-1-",
1740+
"1-1-1-1-1-1-1-1-1-20-1-1-1-", "1-1-1-1-1-1-1-1-1-1");
1741+
if (!nav_band_is_clear(20, 21) || !nav_band_is_clear(116, 118)) {
1742+
return false;
1743+
}
1744+
if (!nav_title_stays_in_its_column(SEEDTOOL_DISPLAY_WIDTH - (2 + 20))) {
1745+
return false;
1746+
}
1747+
if (cursors[c] != SEEDTOOL_NAV_CONFIRM && !nav_bar_label_fits()) {
1748+
return false;
1749+
}
1750+
static const char* const rows[] = { "21. mosquito", "22. mosquito", "23. mosquito", "24. mosquito" };
1751+
const seedtool_nav_t listed = { .selected = cursors[c],
1752+
.confirm = "Continue",
1753+
.confirm_enabled = true,
1754+
.back = true,
1755+
.counter = "6/6" };
1756+
seedtool_render_nav_rows(&listed, "BIP39 word numbers", rows, 4);
1757+
if (!nav_band_is_clear(20, 21) || !nav_band_is_clear(116, 118)) {
1758+
return false;
1759+
}
1760+
if (!nav_title_stays_in_its_column(SEEDTOOL_DISPLAY_WIDTH - (2 + 20))) {
1761+
return false;
1762+
}
1763+
if (cursors[c] != SEEDTOOL_NAV_CONFIRM && !nav_bar_label_fits()) {
1764+
return false;
1765+
}
1766+
}
1767+
return true;
1768+
}
1769+
14791770
/* Known-good vectors straight from Krux's own base32 test suite
14801771
* (tests/test_bbqr.py, B32_TEST_BYTES/B32_ENCODED_STRINGS, unpadded form):
14811772
* proof this encoder produces byte-for-byte the same output a BBQr-reading
@@ -1787,6 +2078,10 @@ static int self_test(void)
17872078
fputs("Origo backup confirmation screen self-test failed\n", stderr);
17882079
return 1;
17892080
}
2081+
if (!nav_chrome_bands_do_not_collide()) {
2082+
fputs("Origo nav chrome geometry self-test failed\n", stderr);
2083+
return 1;
2084+
}
17902085
/* Every value the QR screen offers must actually encode. The account key
17912086
* payload is the longest thing the device ever puts in a code - and with
17922087
* the account index now user-chosen rather than always "0", the account

0 commit comments

Comments
 (0)