@@ -322,7 +322,7 @@ static bool wire_order_is_big_endian(void)
322322 .confirm = NULL ,
323323 .confirm_enabled = false,
324324 .back = false,
325- .confirm_as_tick = true };
325+ .confirm_style = SEEDTOOL_CONFIRM_TICK };
326326
327327 seedtool_render_nav_list (& nav , "Origo" , items , 3 , 0 );
328328 const uint16_t * const pixels = seedtool_render_pixels ();
@@ -1149,7 +1149,7 @@ static seedtool_nav_t stackbit_test_nav(void)
11491149 .confirm = NULL ,
11501150 .confirm_enabled = false,
11511151 .back = true,
1152- .confirm_as_tick = true };
1152+ .confirm_style = SEEDTOOL_CONFIRM_TICK };
11531153 return nav ;
11541154}
11551155
@@ -1714,6 +1714,92 @@ static bool grouped_paging_preserves_the_value(void)
17141714 return true;
17151715}
17161716
1717+ /* The QR screen draws the address beside the code, and the margin it has is
1718+ * not always enough: a taproot address is 62 characters against the 48 two
1719+ * columns of four hold there. seedtool_render_qr_address must refuse those
1720+ * outright rather than draw as far as it reaches, because an address cut off
1721+ * mid-value looks exactly like one that ended there and nothing on screen
1722+ * says otherwise. This is the check grouped_paging_preserves_the_value does
1723+ * not make: that one walks seedtool_render_fit_grouped, the paged path at the
1724+ * body face, and says nothing about groups drawn by any other route. The first
1725+ * version of this screen truncated a taproot address to 48 of its characters
1726+ * with every other check still passing.
1727+ *
1728+ * Proving the tail is drawn without restating the renderer's own geometry:
1729+ * change only the value's last group and the margin must change with it. The
1730+ * margin is everything left of the code, found by looking for the first column
1731+ * carrying a run of white tall enough to be the code itself rather than a line
1732+ * of text. */
1733+ static int qr_code_left_edge (void )
1734+ {
1735+ const uint16_t * const pixels = seedtool_render_pixels ();
1736+ for (int x = 0 ; x < SEEDTOOL_DISPLAY_WIDTH ; ++ x ) {
1737+ int run = 0 ;
1738+ for (int y = 0 ; y < SEEDTOOL_DISPLAY_HEIGHT ; ++ y ) {
1739+ run = pixels [y * SEEDTOOL_DISPLAY_WIDTH + x ] == 0xffff ? run + 1 : 0 ;
1740+ if (run > 40 ) {
1741+ return x ;
1742+ }
1743+ }
1744+ }
1745+ return -1 ;
1746+ }
1747+
1748+ static bool qr_address_draws_every_group (void )
1749+ {
1750+ static const char * const fits [] = {
1751+ "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4" ,
1752+ "1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2" ,
1753+ "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy" ,
1754+ };
1755+ static uint16_t before [SEEDTOOL_DISPLAY_WIDTH * SEEDTOOL_DISPLAY_HEIGHT ];
1756+
1757+ for (size_t v = 0 ; v < sizeof (fits ) / sizeof (fits [0 ]); ++ v ) {
1758+ const char * const value = fits [v ];
1759+ if (!seedtool_render_qr_address ("m/84'/0'/0'/0/0" , value )) {
1760+ return false; /* these do fit; refusing them is its own failure */
1761+ }
1762+ const int edge = qr_code_left_edge ();
1763+ if (edge <= 0 ) {
1764+ return false;
1765+ }
1766+ memcpy (before , seedtool_render_pixels (), sizeof (before ));
1767+
1768+ /* The same value with its last group altered, same length so the code
1769+ * keeps its version and the margin keeps its width. */
1770+ char altered [80 ] = { 0 };
1771+ const size_t total = strlen (value );
1772+ if (total + 1 > sizeof (altered ) || total < SEEDTOOL_GROUP_LEN ) {
1773+ return false;
1774+ }
1775+ memcpy (altered , value , total );
1776+ for (size_t i = total - SEEDTOOL_GROUP_LEN ; i < total ; ++ i ) {
1777+ altered [i ] = altered [i ] == 'q' ? 'p' : 'q' ;
1778+ }
1779+ if (!seedtool_render_qr_address ("m/84'/0'/0'/0/0" , altered )) {
1780+ return false;
1781+ }
1782+ const uint16_t * const after = seedtool_render_pixels ();
1783+
1784+ bool margin_changed = false;
1785+ for (int y = 0 ; y < SEEDTOOL_DISPLAY_HEIGHT && !margin_changed ; ++ y ) {
1786+ for (int x = 0 ; x < edge ; ++ x ) {
1787+ if (before [y * SEEDTOOL_DISPLAY_WIDTH + x ] != after [y * SEEDTOOL_DISPLAY_WIDTH + x ]) {
1788+ margin_changed = true;
1789+ break ;
1790+ }
1791+ }
1792+ }
1793+ if (!margin_changed ) {
1794+ return false; /* the tail was never drawn */
1795+ }
1796+ }
1797+
1798+ /* And the one the margin cannot hold, which must say so rather than clip. */
1799+ return !seedtool_render_qr_address (
1800+ "m/86'/0'/0'/0/0" , "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr" );
1801+ }
1802+
17171803/* The grouped line is centred with its gaps counted in, so it can run off an
17181804 * edge in a way the plain centred line cannot. Measured, not assumed. */
17191805static bool grouped_lines_clear_the_edges (void )
@@ -1766,7 +1852,7 @@ static bool nav_chrome_bands_do_not_collide(void)
17661852 .confirm = "Continue" ,
17671853 .confirm_enabled = confirm_enabled ,
17681854 .back = true,
1769- .confirm_as_tick = true };
1855+ .confirm_style = SEEDTOOL_CONFIRM_TICK };
17701856 seedtool_render_nav_list (& nav , titles [i ], words , 12 , 0 );
17711857 /* Title bar ends at 20, rows run 21..116 (LIST_TOP + 3 *
17721858 * NAV_ROW_HEIGHT, less the 2px each row leaves under itself), the
@@ -1803,13 +1889,13 @@ static bool nav_chrome_bands_do_not_collide(void)
18031889 /* A menu wears the chrome with no confirm at all: its rows are its
18041890 * actions, so the arrow is the only control. Both places the confirm could
18051891 * appear must stay empty - the band along the bottom, and the title bar's
1806- * right slot. The slot is the one that caught a real bug: confirm_as_tick
1892+ * right slot. The slot is the one that caught a real bug: the tick
18071893 * is set for every list, so the tick drew on the Origo and Wallet menus,
18081894 * offering an answer to a question those screens never ask. Nothing here
18091895 * noticed, because every check only ever asserted the tick was present. */
18101896 for (size_t s = 0 ; s < 2 ; ++ s ) {
18111897 const seedtool_nav_t nav
1812- = { .selected = s ? SEEDTOOL_NAV_BACK : 0 , .back = true, .confirm_as_tick = true };
1898+ = { .selected = s ? SEEDTOOL_NAV_BACK : 0 , .back = true, .confirm_style = SEEDTOOL_CONFIRM_TICK };
18131899 seedtool_render_nav_list (& nav , "Word entry" , words , 2 , 0 );
18141900 if (!nav_band_is_clear (NAV_BAR_BAND_Y , SEEDTOOL_DISPLAY_HEIGHT ) || nav_right_slot_is_drawn ()) {
18151901 return false;
@@ -1847,15 +1933,15 @@ static bool nav_chrome_bands_do_not_collide(void)
18471933 };
18481934 for (size_t i = 0 ; i < sizeof (screens ) / sizeof (screens [0 ]); ++ i ) {
18491935 for (int on_back = 0 ; on_back < 2 ; ++ on_back ) {
1850- /* confirm_as_tick , because that is what nav_screen sets and these
1936+ /* confirm_style , because that is what nav_screen sets and these
18511937 * are its screens: the confirm is a box in the title bar's right
18521938 * slot and no bar is drawn. Rendering them with a bar would have
18531939 * this table checking a layout the firmware stopped shipping. */
18541940 const seedtool_nav_t nav = { .selected = on_back ? SEEDTOOL_NAV_BACK : SEEDTOOL_NAV_CONFIRM ,
18551941 .confirm = screens [i ].label ,
18561942 .confirm_enabled = true,
18571943 .back = true,
1858- .confirm_as_tick = true };
1944+ .confirm_style = SEEDTOOL_CONFIRM_TICK };
18591945 seedtool_render_nav_text (& nav , screens [i ].title , screens [i ].line1 , screens [i ].line2 , NULL );
18601946 /* Line 2 sits at 65 and the 16px face is that tall again, so its
18611947 * wraps land at 81, 97, 113 - and 113 is inside the bar's own
@@ -1914,7 +2000,7 @@ static bool nav_chrome_bands_do_not_collide(void)
19142000 .confirm = dice [i ].label ,
19152001 .confirm_enabled = true,
19162002 .back = true,
1917- .confirm_as_tick = true };
2003+ .confirm_style = SEEDTOOL_CONFIRM_TICK };
19182004 seedtool_render_nav_dice (& nav , dice [i ].title , dice [i ].line1 , dice [i ].line2 , & full );
19192005 if (!nav_band_is_clear (20 , 21 ) || !nav_band_is_clear (104 , 118 )) {
19202006 return false;
@@ -1954,7 +2040,7 @@ static bool nav_chrome_bands_do_not_collide(void)
19542040 .confirm = NULL ,
19552041 .confirm_enabled = true,
19562042 .back = true,
1957- .confirm_as_tick = true };
2043+ .confirm_style = SEEDTOOL_CONFIRM_TICK };
19582044 seedtool_render_nav_text (& nav , notices [i ].title , notices [i ].line1 , notices [i ].line2 , NULL );
19592045 /* A notice's only control is now the arrow - if that failed to draw the
19602046 * screen would be one the reader cannot leave at all, which is worth
@@ -1981,7 +2067,7 @@ static bool nav_chrome_bands_do_not_collide(void)
19812067 .confirm_enabled = true,
19822068 .back = true,
19832069 .counter = "8/8" ,
1984- .confirm_as_tick = true };
2070+ .confirm_style = SEEDTOOL_CONFIRM_TICK };
19852071 seedtool_render_nav_text (& paged , "Canonical transcript" , "20-1-2-1-4-1-1-1-1-1-1-1-1-" ,
19862072 "1-1-1-1-1-1-1-1-1-20-1-1-1-" , "1-1-1-1-1-1-1-1-1-1" );
19872073 if (!nav_band_is_clear (20 , 21 ) || !nav_band_is_clear (116 , 118 )) {
@@ -2005,7 +2091,7 @@ static bool nav_chrome_bands_do_not_collide(void)
20052091 .confirm_enabled = true,
20062092 .back = true,
20072093 .counter = "6/6" ,
2008- .confirm_as_tick = true };
2094+ .confirm_style = SEEDTOOL_CONFIRM_TICK };
20092095 seedtool_render_nav_rows (& listed , "BIP39 word numbers" , rows , 4 );
20102096 if (!nav_band_is_clear (20 , 21 ) || !nav_band_is_clear (116 , 118 )) {
20112097 return false;
@@ -2347,6 +2433,10 @@ static int self_test(void)
23472433 fputs ("Origo grouped address geometry self-test failed\n" , stderr );
23482434 return 1 ;
23492435 }
2436+ if (!qr_address_draws_every_group ()) {
2437+ fputs ("Origo address-beside-QR self-test failed\n" , stderr );
2438+ return 1 ;
2439+ }
23502440 if (!nav_chrome_bands_do_not_collide ()) {
23512441 fputs ("Origo nav chrome geometry self-test failed\n" , stderr );
23522442 return 1 ;
@@ -2388,12 +2478,12 @@ static int self_test(void)
23882478 .confirm = NULL ,
23892479 .confirm_enabled = false,
23902480 .back = true,
2391- .confirm_as_tick = true };
2481+ .confirm_style = SEEDTOOL_CONFIRM_TICK };
23922482 const seedtool_nav_t at_start = { .selected = 0 ,
23932483 .confirm = NULL ,
23942484 .confirm_enabled = false,
23952485 .back = true,
2396- .confirm_as_tick = true };
2486+ .confirm_style = SEEDTOOL_CONFIRM_TICK };
23972487 seedtool_render_nav_list (& scrolled , "Wallet" , menu_labels , 7 , 4 );
23982488 seedtool_render_nav_list (& at_start , "Word 3/12 aba" , menu_labels , MENU_LABEL_COUNT , 0 );
23992489 /* Draw the real layouts, each opened at its centre, so one that overflows a
0 commit comments