cryptnox-pos 1.0.0
Standalone USDC payment terminal firmware (ESP32 + Cryptnox smart card)
Loading...
Searching...
No Matches
ui_widgets.cpp
Go to the documentation of this file.
1/*
2 * SPDX-License-Identifier: LGPL-3.0-or-later
3 * Copyright (c) 2026 Cryptnox SA
4 */
5
11
12#include "ui_internal.h"
13
14/******************************************************************
15 * 7. Widget helpers
16 ******************************************************************/
17lv_obj_t *make_button(lv_obj_t *parent, const char *label, lv_color_t bg,
18 lv_color_t fg, lv_coord_t w, lv_coord_t h,
19 lv_align_t align, lv_coord_t x, lv_coord_t y,
20 BtnAction act, const lv_font_t *font) {
21 lv_obj_t *btn = lv_btn_create(parent);
22 lv_obj_set_size(btn, w, h);
23 lv_obj_align(btn, align, x, y);
24
25 /* Base look — FLAT fill (kill the default theme's gradient, which washes a
26 * dark fill toward light), no default border. BTN_RADIUS 6 keeps an action
27 * button a softened rectangle; the full-radius pill is for the asset
28 * selector and chips, which are not actions. */
29 lv_obj_set_style_bg_color(btn, bg, LV_PART_MAIN);
30 lv_obj_set_style_bg_grad_dir(btn, LV_GRAD_DIR_NONE, LV_PART_MAIN);
31 lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, LV_PART_MAIN);
32 lv_obj_set_style_radius(btn, BTN_RADIUS, LV_PART_MAIN);
33 lv_obj_set_style_border_width(btn, 0, LV_PART_MAIN);
34
35 /* Flat — no drop shadow (light, minimal look). */
36 lv_obj_set_style_shadow_width(btn, 0, LV_PART_MAIN);
37
38 /* Secondary (surface) buttons get a hairline border to lift them off the bg.
39 * (Compare the raw RGB565 value — lv_color_eq isn't in this LVGL build.) */
40 if (bg.full == COL_SURFACE.full) {
41 lv_obj_set_style_border_width(btn, 1, LV_PART_MAIN);
42 lv_obj_set_style_border_color(btn, COL_BORDER, LV_PART_MAIN);
43 }
44
45 /* Pressed feedback: darken the fill — except on the near-black COL_ACTION,
46 * where darker than #101F2E is no visible change at all, so it lightens
47 * instead. The press is the only thing a resistive panel says back. */
48 lv_obj_set_style_bg_color(btn,
49 (bg.full == COL_ACTION.full)
50 ? lv_color_mix(lv_color_white(), bg, 40)
51 : lv_color_mix(lv_color_black(), bg, 70),
52 LV_PART_MAIN | LV_STATE_PRESSED);
53
54 lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED,
55 reinterpret_cast<void *>(static_cast<intptr_t>(act)));
56
57 lv_obj_t *lbl = lv_label_create(btn);
58 lv_label_set_text(lbl, label);
59 lv_obj_set_style_text_color(lbl, fg, LV_PART_MAIN);
60 lv_obj_set_style_text_font(lbl, font, LV_PART_MAIN);
61 lv_obj_center(lbl);
62 return btn;
63}
64lv_obj_t *make_label(lv_obj_t *parent, const char *txt, lv_color_t color,
65 const lv_font_t *font, lv_align_t align,
66 lv_coord_t x, lv_coord_t y) {
67 lv_obj_t *lbl = lv_label_create(parent);
68 lv_label_set_text(lbl, txt);
69 lv_obj_set_style_text_color(lbl, color, LV_PART_MAIN);
70 lv_obj_set_style_text_font(lbl, font, LV_PART_MAIN);
71 lv_obj_align(lbl, align, x, y);
72 return lbl;
73}
74
75/* The sale's secondary button: card-white fill with a hairline border. Cancel
76 * costs nothing, so it should read as available without competing with the
77 * button that commits; an outline is the weakest thing that still reads as one. */
78lv_obj_t *make_ghost_button(lv_obj_t *parent, const char *label,
79 lv_coord_t w, lv_align_t align,
80 lv_coord_t x, lv_coord_t y, BtnAction act) {
81 lv_obj_t *b = make_button(parent, label, COL_BG, COL_TEXT, w,
83 align, x, y, act, &font_pjs_20_semibold);
84 lv_obj_set_style_border_width(b, 1, LV_PART_MAIN);
85 lv_obj_set_style_border_color(b, COL_BORDER, LV_PART_MAIN);
86 lv_obj_set_style_border_opa(b, LV_OPA_COVER, LV_PART_MAIN);
87 return b;
88}
89
90/* Thin horizontal rule under a screen title, for visual structure. */
91void make_divider(lv_obj_t *parent, lv_coord_t y) {
92 lv_obj_t *d = lv_obj_create(parent);
93 lv_obj_set_size(d, SCR_W - 48, 2);
94 lv_obj_align(d, LV_ALIGN_TOP_MID, 0, y);
95 lv_obj_set_style_bg_color(d, COL_BORDER, LV_PART_MAIN);
96 lv_obj_set_style_border_width(d, 0, LV_PART_MAIN);
97 lv_obj_set_style_radius(d, 1, LV_PART_MAIN);
98 lv_obj_set_style_pad_all(d, 0, LV_PART_MAIN);
99 lv_obj_clear_flag(d, LV_OBJ_FLAG_SCROLLABLE);
100}
101#define CHIP_SZ 16
102#define BADGE_SZ (COIN_SZ + 6) /* room for the chip to hang off the corner */
103lv_obj_t *make_glyph_disc(lv_obj_t *parent, const char *sym,
104 lv_color_t bg, lv_coord_t sz) {
105 lv_obj_t *d = lv_obj_create(parent);
106 lv_obj_remove_style_all(d);
107 lv_obj_set_size(d, sz, sz);
108 lv_obj_set_style_bg_color(d, bg, LV_PART_MAIN);
109 lv_obj_set_style_bg_opa(d, LV_OPA_COVER, LV_PART_MAIN);
110 lv_obj_set_style_radius(d, LV_RADIUS_CIRCLE, LV_PART_MAIN);
111 lv_obj_clear_flag(d, LV_OBJ_FLAG_SCROLLABLE);
112 lv_obj_clear_flag(d, LV_OBJ_FLAG_CLICKABLE);
113 lv_obj_t *l = lv_label_create(d);
114 lv_label_set_text(l, sym);
115 lv_obj_set_style_text_color(l, COL_BG, LV_PART_MAIN);
116 lv_obj_set_style_text_font(l, &font_inter_14, LV_PART_MAIN);
117 lv_obj_center(l);
118 return d;
119}
120
121/* Coin mark, optionally with a network chip on its bottom-right corner, the way
122 * wallets badge a token with the chain it lives on — so "USDC" (Sepolia) and a
123 * TRC-20 are told apart by the icon and not only by the subtitle. The chips
124 * carry their white ring in the bitmap (tools/gen_chain_icons.py).
125 *
126 * @p chip may be NULL, and the box then shrinks to COIN_SZ. The coin is centred
127 * in the box and the chip hangs off the corner, so centring the box centres the
128 * mark with no per-site nudge. Returns the box for the caller to position. */
129static lv_obj_t *make_icon_box(lv_obj_t *parent, const lv_img_dsc_t *coin_src,
130 const lv_img_dsc_t *chip_src) {
131 lv_obj_t *box = lv_obj_create(parent);
132 lv_obj_remove_style_all(box);
133 lv_obj_set_size(box, (chip_src != NULL) ? BADGE_SZ : COIN_SZ,
134 (chip_src != NULL) ? BADGE_SZ : COIN_SZ);
135 lv_obj_clear_flag(box, LV_OBJ_FLAG_SCROLLABLE);
136 lv_obj_clear_flag(box, LV_OBJ_FLAG_CLICKABLE);
137
138 lv_obj_t *coin = lv_img_create(box);
139 lv_img_set_src(coin, coin_src);
140 lv_obj_center(coin);
141
142 if (chip_src != NULL) {
143 lv_obj_t *chip = lv_img_create(box);
144 lv_img_set_src(chip, chip_src);
145 lv_obj_align(chip, LV_ALIGN_BOTTOM_RIGHT, 0, 0);
146 }
147 return box;
148}
149
150/* Network marks and chips, indexed by pos_net_t (order follows assets.h). Kept
151 * out of the asset table because they are LVGL types and that table is
152 * host-testable. */
153static const lv_img_dsc_t *const NET_ICON[POS_NET__COUNT] = {
155};
156static const lv_img_dsc_t *const NET_CHIP[POS_NET__COUNT] = {
158};
159
160/* A token's own mark, by the row's pos_coin_t. */
161static const lv_img_dsc_t *coin_icon(const pos_asset_t *a) {
162 switch (a->coin) {
163 case POS_COIN_USDC: return &icon_usdc;
164 case POS_COIN_USDT: return &icon_usdt;
165 default: return NET_ICON[(int)a->net];
166 }
167}
168
169/* The selected asset. A native coin IS its network, so it carries no chip —
170 * the chip only says "this token lives over there". */
171lv_obj_t *make_asset_badge(lv_obj_t *parent, pos_chain_t chain) {
172 const pos_asset_t *a = pos_asset_of(chain);
173 const int n = (int)a->net;
174 if (a->native) { return make_icon_box(parent, NET_ICON[n], NULL); }
175 return make_icon_box(parent, coin_icon(a), NET_CHIP[n]);
176}
177
178/* The asset selector itself: the badge above turned into a tappable pill, at the
179 * right-hand end of the amount's row — mark, ticker, chevron. A flex row at
180 * LV_SIZE_CONTENT because the ticker's width varies: LVGL measures it, the align
181 * pins the right edge, and amount_row_place() reads back the result. */
182lv_obj_t *make_asset_button(lv_obj_t *parent) {
183 s_asset_btn = lv_btn_create(parent);
184 lv_obj_set_size(s_asset_btn, LV_SIZE_CONTENT, ASSET_BTN_H);
185 lv_obj_align(s_asset_btn, LV_ALIGN_TOP_RIGHT, ASSET_BTN_X, ASSET_BTN_Y);
186 lv_obj_set_style_bg_color(s_asset_btn, COL_SURFACE, LV_PART_MAIN);
187 lv_obj_set_style_bg_grad_dir(s_asset_btn, LV_GRAD_DIR_NONE, LV_PART_MAIN);
188 lv_obj_set_style_bg_opa(s_asset_btn, LV_OPA_COVER, LV_PART_MAIN);
189 lv_obj_set_style_radius(s_asset_btn, LV_RADIUS_CIRCLE, LV_PART_MAIN);
190 lv_obj_set_style_border_width(s_asset_btn, 0, LV_PART_MAIN);
191 lv_obj_set_style_shadow_width(s_asset_btn, 0, LV_PART_MAIN);
192 lv_obj_set_style_bg_color(s_asset_btn,
193 lv_color_mix(lv_color_black(), COL_SURFACE, 20),
194 LV_PART_MAIN | LV_STATE_PRESSED);
195 lv_obj_add_event_cb(s_asset_btn, btn_event_cb, LV_EVENT_CLICKED,
196 reinterpret_cast<void *>(
197 static_cast<intptr_t>(ACT_NET_PICK)));
198
199 /* Pads and gap set explicitly: the flex row obeys them, and the theme's
200 * button padding would otherwise push the badge onto the chevron. */
201 lv_obj_set_flex_flow(s_asset_btn, LV_FLEX_FLOW_ROW);
202 lv_obj_set_flex_align(s_asset_btn, LV_FLEX_ALIGN_CENTER,
203 LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
204 lv_obj_set_style_pad_all(s_asset_btn, ASSET_BTN_PAD, LV_PART_MAIN);
205 lv_obj_set_style_pad_column(s_asset_btn, 5, LV_PART_MAIN);
206 lv_obj_clear_flag(s_asset_btn, LV_OBJ_FLAG_SCROLLABLE);
207
210 LV_ALIGN_DEFAULT, 0, 0);
211 s_asset_arrow = make_label(s_asset_btn, LV_SYMBOL_RIGHT, COL_DIM,
212 &font_inter_14, LV_ALIGN_DEFAULT, 0, 0);
213 return s_asset_btn;
214}
215
216/* Chevron off once there is an amount, so the digits get its width back; the
217 * ticker stays, as it says which money. Hidden, not deleted: the right-aligned
218 * pill re-measures and only its left edge moves. */
219void asset_btn_set_compact(bool compact) {
220 if (s_asset_arrow == NULL) { return; } /* not the amount screen */
221 if (compact) { lv_obj_add_flag(s_asset_arrow, LV_OBJ_FLAG_HIDDEN); }
222 else { lv_obj_clear_flag(s_asset_arrow, LV_OBJ_FLAG_HIDDEN); }
223}
224
225/* The bare network mark, for the network picker's own rows — no coin is chosen
226 * at that step, so there is nothing to badge it with. */
227lv_obj_t *make_net_badge(lv_obj_t *parent, pos_net_t net) {
228 return make_icon_box(parent, NET_ICON[(int)net], NULL);
229}
230
231/* "Tap here" mark — the contactless waves and a hand presenting a card, in an
232 * oval. The artwork is assets/contactless-icon.png; tools/gen_tap_icon.py
233 * downsamples it into main/tap_icon.c, so change the asset and re-run the
234 * script. An asset rather than geometry: a hand drawn from rectangles and arcs
235 * at this size does not read as a hand.
236 *
237 * Provenance: the drawing is UXWing's, licensed for commercial use without
238 * attribution (a licence, not a PD dedication; this file is the copy those terms
239 * applied to). EMVCo's Contactless Indicator is a trademark, which no copyright
240 * licence covers; it is licensed through the product's scheme certification.
241 *
242 * ALPHA_8BIT, recoloured from img_recolor. LVGL's fast path for that format draws
243 * nothing under angle or zoom, so this must stay untransformed. Aligned TOP_MID
244 * at @p y. TAP_MARK_W/H are set by the generator from the artwork, for reading
245 * only; build_card_wait() centres the mark using them. */
246#define TAP_MARK_W 109
247
248/* The mark's ink, and the one place to change it. It tracks the artwork's
249 * weight: on this artwork grey reads as washed-out or disabled, which is wrong
250 * for the one thing the customer is being asked to act on, so it is black. */
251#define COL_TAP_MARK COL_TEXT
252void make_tap_mark(lv_obj_t *parent, lv_coord_t y) {
253 lv_obj_t *img = lv_img_create(parent);
254 lv_img_set_src(img, &tap_icon);
255 lv_obj_clear_flag(img, LV_OBJ_FLAG_CLICKABLE);
256 /* The mask carries shape only; this is where its colour comes from. */
257 lv_obj_set_style_img_recolor(img, COL_TAP_MARK, LV_PART_MAIN);
258 lv_obj_set_style_img_recolor_opa(img, LV_OPA_COVER, LV_PART_MAIN);
259 lv_obj_align(img, LV_ALIGN_TOP_MID, 0, y);
260}
261#define PILL_TEXT_X 52 /* clears the BADGE_SZ icon at PILL_ICON_X */
262/* Right-hand reserve for the chevron (drawn at -14, ~8px wide). 24 clears the
263 * glyph without dot-eliding "Tron Nile TRC-20" in a 196px pill. */
264#define PILL_TEXT_PAD_R 24
265lv_obj_t *make_pill(lv_obj_t *parent, const char *title, const char *sub,
266 lv_coord_t w, lv_coord_t y, BtnAction act,
267 bool leaf) {
268 lv_obj_t *btn = lv_btn_create(parent);
269 lv_obj_set_size(btn, w, PILL_H);
270 lv_obj_align(btn, LV_ALIGN_TOP_MID, 0, y);
271 lv_obj_set_style_bg_color(btn, COL_SURFACE, LV_PART_MAIN);
272 lv_obj_set_style_bg_grad_dir(btn, LV_GRAD_DIR_NONE, LV_PART_MAIN);
273 lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, LV_PART_MAIN);
274 lv_obj_set_style_radius(btn, LV_RADIUS_CIRCLE, LV_PART_MAIN);
275 lv_obj_set_style_border_width(btn, 0, LV_PART_MAIN);
276 lv_obj_set_style_shadow_width(btn, 0, LV_PART_MAIN);
277 lv_obj_set_style_pad_all(btn, 0, LV_PART_MAIN);
278 lv_obj_set_style_bg_color(btn,
279 lv_color_mix(lv_color_black(), COL_SURFACE, 20),
280 LV_PART_MAIN | LV_STATE_PRESSED);
281 lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED,
282 reinterpret_cast<void *>(static_cast<intptr_t>(act)));
283
284 if (sub != NULL) {
286 LV_ALIGN_TOP_LEFT, PILL_TEXT_X, 7);
287 lv_obj_t *sl = make_label(btn, sub, COL_DIM, &font_inter_14,
288 LV_ALIGN_TOP_LEFT, PILL_TEXT_X, 30);
289 /* A leaf row's subtitle is left at LV_SIZE_CONTENT — it is one of our
290 * own fixed strings and it must be readable in full. Only the rows that
291 * open something get a width cap, because those show an SSID, which can
292 * be 32 characters and has to elide somewhere. */
293 if (!leaf) {
294 lv_obj_set_width(sl, w - PILL_TEXT_X - PILL_TEXT_PAD_R);
295 lv_label_set_long_mode(sl, LV_LABEL_LONG_DOT);
296 }
297 } else {
299 LV_ALIGN_LEFT_MID, PILL_TEXT_X, 0);
300 }
301 /* The chevron means "this opens something". A leaf row is the choice
302 * itself, so it gets no chevron — and hands the space to the subtitle. */
303 if (!leaf) {
304 make_label(btn, LV_SYMBOL_RIGHT, COL_DIM, &font_inter_14,
305 LV_ALIGN_RIGHT_MID, -14, 0);
306 }
307 return btn;
308}
309
310/* A read-only row on the settings tabs: dim caption over its value, as one
311 * flex item. The tabs are flex columns (see build_settings), so a row sits where
312 * it was appended. Returns the value label, for callers that update it in place.
313 *
314 * Values wrap by default: these are addresses. An SSID wants LV_LABEL_LONG_DOT
315 * instead, which the caller sets. */
316lv_obj_t *make_field(lv_obj_t *parent, const char *caption,
317 const char *value, lv_color_t col) {
318 lv_obj_t *box = lv_obj_create(parent);
319 lv_obj_remove_style_all(box);
320 lv_obj_set_size(box, TAB_W, LV_SIZE_CONTENT);
321 lv_obj_clear_flag(box, LV_OBJ_FLAG_SCROLLABLE);
322 lv_obj_set_flex_flow(box, LV_FLEX_FLOW_COLUMN);
323 lv_obj_set_style_pad_row(box, 4, LV_PART_MAIN);
324
325 make_label(box, caption, COL_DIM, &font_inter_14,
326 LV_ALIGN_DEFAULT, 0, 0);
327 lv_obj_t *v = make_label(box, value, col, &font_inter_14_medium,
328 LV_ALIGN_DEFAULT, 0, 0);
329 lv_obj_set_width(v, TAB_W);
330 lv_label_set_long_mode(v, LV_LABEL_LONG_WRAP);
331 return v;
332}
333
334static void sheet_y_cb(void *obj, int32_t v) {
335 lv_obj_set_y(static_cast<lv_obj_t *>(obj), static_cast<lv_coord_t>(v));
336}
337
338/* Full-screen opaque panel parked one screen-height below the fold. Square
339 * corners: a sheet flush with all four screen edges would show the page behind
340 * through each rounded corner. The rise is what reads as a sheet. */
341static lv_obj_t *sheet_open(void) {
342 lv_obj_t *sh = lv_obj_create(lv_scr_act());
343 lv_obj_remove_style_all(sh); /* radius 0 among the rest — see above */
344 lv_obj_set_size(sh, SCR_W, SCR_H);
345 lv_obj_set_pos(sh, 0, SCR_H);
346 lv_obj_set_style_bg_color(sh, COL_BG, LV_PART_MAIN);
347 lv_obj_set_style_bg_opa(sh, LV_OPA_COVER, LV_PART_MAIN);
348 lv_obj_clear_flag(sh, LV_OBJ_FLAG_SCROLLABLE);
349 /* Clickable (the LVGL default) on purpose: it must swallow taps meant for
350 * the screen it is covering, which is still fully built underneath. */
351 return sh;
352}
353
354static void sheet_slide_in(lv_obj_t *sh) {
355 lv_anim_t a;
356 lv_anim_init(&a);
357 lv_anim_set_var(&a, sh);
358 lv_anim_set_exec_cb(&a, sheet_y_cb);
359 lv_anim_set_values(&a, SCR_H, 0);
360 lv_anim_set_time(&a, 260);
361 /* Decelerating, like every sheet anybody has used on a phone — a linear
362 * slide reads as mechanical at this size. */
363 lv_anim_set_path_cb(&a, lv_anim_path_ease_out);
364 lv_anim_start(&a);
365}
366
367/* Scrub a secret field's heap strings before lv_obj_clean frees them unwiped:
368 * the real text (pwd_tmp while masked, the label while revealed) and the label,
369 * which holds the last digit in clear during its echo. Copies LVGL's per-digit
370 * reallocs left behind are out of reach. */
371static void code_field_wipe(lv_obj_t *ta) {
372 if (ta == NULL) { return; }
373 char *t = const_cast<char *>(lv_textarea_get_text(ta));
374 if (t != NULL) { CW_Utils::secure_wipe(reinterpret_cast<uint8_t *>(t), strlen(t)); }
375 char *l = lv_label_get_text(lv_textarea_get_label(ta));
376 if (l != NULL) { CW_Utils::secure_wipe(reinterpret_cast<uint8_t *>(l), strlen(l)); }
377}
378void clear_screen(void) {
379 lv_obj_t *scr = lv_scr_act();
383 lv_obj_clean(scr);
384 lv_obj_set_style_bg_color(scr, COL_BG, LV_PART_MAIN);
385 /* build_page() leaves a gradient on the screen, and the screen object
386 * outlives the screens built on it — without this the ramp follows onto
387 * the flat white pages. */
388 lv_obj_set_style_bg_grad_dir(scr, LV_GRAD_DIR_NONE, LV_PART_MAIN);
389 lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, LV_PART_MAIN);
390 s_amount_label = NULL;
392 s_amount_row = NULL;
393 s_asset_btn = NULL;
394 s_asset_arrow = NULL;
395 s_charge_btn = NULL;
396 s_charge_busy = false; /* the button the flag was about is gone */
397 s_clock_lbl = NULL;
398 s_pin_ta = NULL; /* deleted by lv_obj_clean — drop the dangling ref */
399 s_pin_eye_lbl = NULL;
400 s_wifi_pass_ta = NULL;
401 s_wifi_eye_lbl = NULL;
402 s_boot_step_lbl = NULL;
403 s_tx_info_lbl = NULL;
404 s_cal_countdown = NULL;
405 s_page_card = NULL;
406 s_fee_max_lbl = NULL;
407 s_fee_prio_lbl = NULL;
408 s_admin_ta = NULL;
409 s_admin_note_lbl = NULL;
410 s_reset_btn = NULL;
411 s_close_btn = NULL;
412}
413
414/* The UTC offset the clock adds, cached: settings_get_tz_offset_min() opens
415 * NVS and clock_refresh() runs every three seconds. Reloaded through
416 * ui_clock_changed() when the config page stores a new one. */
417static int16_t s_tz_off_min = 0;
418static uint8_t s_tz_dst = 0;
419volatile bool s_tz_dirty = true;
420
431static void clock_refresh(void) {
432 if (s_clock_lbl == NULL) { return; }
433
434 if (s_tz_dirty) {
435 s_tz_dirty = false;
438 }
439
440 const time_t utc = time(NULL);
441 /* Nov 2023, before any real sale. Tested on the unshifted instant, so a
442 * negative offset cannot drag a just-set clock back under the threshold. */
443 if (utc < (time_t)1700000000) {
444 lv_label_set_text(s_clock_lbl, "--:--");
445 return;
446 }
447
448 const time_t local = utc + ((time_t)civil_local_offset_min(
449 utc, s_tz_off_min, s_tz_dst) * 60);
450 struct tm tmv;
451 if (gmtime_r(&local, &tmv) == NULL) {
452 lv_label_set_text(s_clock_lbl, "--:--");
453 return;
454 }
455 char b[8];
456 (void)snprintf(b, sizeof(b), "%02d:%02d", tmv.tm_hour, tmv.tm_min);
457 lv_label_set_text(s_clock_lbl, b);
458}
459
467/* The page ramp, on whatever acts as the page (the screen, or the rising admin
468 * sheet). LVGL defaults are opaque, so anything over it that should let the
469 * ramp show through must say so — see build_settings(). */
470void paint_page(lv_obj_t *obj) {
471 lv_obj_set_style_bg_color(obj, COL_PAGE, LV_PART_MAIN);
472 lv_obj_set_style_bg_grad_color(obj, COL_PAGE_GRAD, LV_PART_MAIN);
473 lv_obj_set_style_bg_grad_dir(obj, LV_GRAD_DIR_VER, LV_PART_MAIN);
474 lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, LV_PART_MAIN);
475}
476
477/* Deliberately no paint_panel(): the admin panel is white, which clear_screen()
478 * already leaves, gradient reset included. */
479
480/* The sale flow's white card, on @p parent (the screen, or the admin sheet). */
481lv_obj_t *make_card(lv_obj_t *parent) {
482 lv_obj_t *card = lv_obj_create(parent);
483 lv_obj_remove_style_all(card);
484 lv_obj_set_size(card, CARD_W, CARD_H);
485 lv_obj_set_pos(card, CARD_X, CARD_Y);
486 lv_obj_set_style_bg_color(card, COL_BG, LV_PART_MAIN);
487 lv_obj_set_style_bg_opa(card, LV_OPA_COVER, LV_PART_MAIN);
488 lv_obj_set_style_radius(card, 14, LV_PART_MAIN);
489 lv_obj_clear_flag(card, LV_OBJ_FLAG_SCROLLABLE);
490 return card;
491}
492
493/* @p band false for the first-run setup screens that borrow the card: no
494 * clock and no home bar. Nothing is configured yet — the clock would read
495 * --:-- and the swipe the bar advertises is off anywhere but the amount screen. */
496lv_obj_t *build_page(int step, bool band) {
497 (void)step; /* ponytail: no progress indicator on the band */
498 clear_screen();
499 paint_page(lv_scr_act());
500
501 /* The clock, left end of the status band; retexted by the status timer
502 * rather than by rebuilding the screen every minute. */
503 if (band) {
504 s_clock_lbl = make_label(lv_scr_act(), "", COL_TITLE,
506 LV_ALIGN_TOP_LEFT, CLOCK_X, CLOCK_Y);
508 }
509
510 lv_obj_t *card = make_card(lv_scr_act());
511
512 /* The handle the admin panel is behind, drawn because a gesture with
513 * nothing on screen saying it exists is a gesture nobody finds. */
514 if (band) {
515 lv_obj_t *home = lv_obj_create(lv_scr_act());
516 lv_obj_remove_style_all(home);
517 lv_obj_set_size(home, 84, 5);
518 lv_obj_align(home, LV_ALIGN_BOTTOM_MID, 0, -9);
519 lv_obj_set_style_radius(home, LV_RADIUS_CIRCLE, LV_PART_MAIN);
520 lv_obj_set_style_bg_opa(home, LV_OPA_COVER, LV_PART_MAIN);
521 lv_obj_set_style_bg_color(home, COL_HOME_BAR, LV_PART_MAIN);
522 }
523
524 s_page_card = card;
525 return card;
526}
527
528/******************************************************************
529 * 8. Screen builders
530 ******************************************************************/
531/* Borderless icon button (top-left) — just the glyph, no box/shadow. */
532lv_obj_t *make_icon_button(const char *sym, BtnAction act,
533 lv_obj_t *parent) {
534 lv_obj_t *btn = lv_btn_create((parent != NULL) ? parent : lv_scr_act());
535 lv_obj_set_size(btn, MENU_BTN_W, MENU_BTN_H);
536 lv_obj_align(btn, LV_ALIGN_TOP_LEFT, MENU_BTN_X, MENU_BTN_Y);
537 lv_obj_set_style_bg_opa(btn, LV_OPA_TRANSP, LV_PART_MAIN);
538 lv_obj_set_style_border_width(btn, 0, LV_PART_MAIN);
539 lv_obj_set_style_shadow_width(btn, 0, LV_PART_MAIN);
540 lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED,
541 reinterpret_cast<void *>(static_cast<intptr_t>(act)));
542 lv_obj_t *lbl = lv_label_create(btn);
543 lv_label_set_text(lbl, sym);
544 lv_obj_set_style_text_color(lbl, COL_TEXT, LV_PART_MAIN);
545 lv_obj_set_style_text_font(lbl, &font_pjs_20_medium, LV_PART_MAIN);
546 lv_obj_center(lbl);
547 return btn;
548}
549
550/* Which deployment, on the screen that takes the money. Nothing else in the
551 * payment flow says test or production, and a firmware update with a new
552 * BUILD_ID wipes NVS and brings the unit back on mainnet — so a terminal
553 * somebody configured for testnet can start taking real payments with every
554 * screen looking normal. Production stays silent, the same rule
555 * settings_net_str follows: mainnet names carry no suffix, so the chip's
556 * presence IS the warning.
557 *
558 * In the status band, not on the amount's row: amount_row_place() gives the
559 * figure whatever the pill leaves, so a chip there would take width off the
560 * digits. */
561void add_test_chip(void) {
562 if (settings_get_mainnet()) { return; }
563 lv_obj_t *chip = lv_label_create(lv_scr_act());
564 lv_label_set_text(chip, "TEST");
565 lv_obj_set_style_text_color(chip, COL_BG, LV_PART_MAIN);
566 lv_obj_set_style_text_font(chip, &font_inter_14_medium, LV_PART_MAIN);
567 lv_obj_set_style_bg_color(chip, COL_DANGER, LV_PART_MAIN);
568 lv_obj_set_style_bg_opa(chip, LV_OPA_COVER, LV_PART_MAIN);
569 lv_obj_set_style_pad_hor(chip, 7, LV_PART_MAIN);
570 lv_obj_set_style_pad_ver(chip, 1, LV_PART_MAIN); /* 18px line: 20 tall */
571 lv_obj_set_style_radius(chip, LV_RADIUS_CIRCLE, LV_PART_MAIN);
572 /* Straight after the clock. Not right-aligned: that end is the Wi-Fi mark's,
573 * which is on the top layer and would draw over the chip. */
574 lv_obj_align(chip, LV_ALIGN_TOP_LEFT, CHIP_X, CHIP_Y);
575}
576
586lv_obj_t *make_title(const char *txt, bool has_icon,
587 lv_obj_t *parent) {
588 /* Symmetric, so the title stays optically centred. Width comes from the
589 * parent when there is one (the sale flow's card), not from SCR_W. */
590 lv_obj_t *host = (parent != NULL) ? parent : lv_scr_act();
591 lv_obj_update_layout(host);
592 lv_coord_t hw = lv_obj_get_width(host);
593 if (hw <= 0) { hw = SCR_W; } /* not laid out yet — assume full width */
594 /* Cleared from the ~20px glyph, not the 42px hit box: clearing the whole box
595 * leaves too little width for "Authorize browser" even at 14px. */
596 const lv_coord_t gutter = has_icon ? (MENU_BTN_X + ((MENU_BTN_W + 20) / 2) + 4)
597 : 8;
598 const lv_coord_t avail = hw - (2 * gutter);
599
600 const bool small = lv_txt_get_width(txt, strlen(txt), &font_pjs_20_medium,
601 0, LV_TEXT_FLAG_NONE) > avail;
602 /* +2 keeps the shorter 14px cap optically level with the 20px arrow glyph
603 * beside it (Inter's 18px line box already puts its cap 2 lower). */
604 lv_obj_t *l = make_label(host, txt, COL_TITLE,
605 small ? &font_inter_14_medium
607 LV_ALIGN_TOP_MID, 0,
608 small ? (HDR_TITLE_Y + 2) : HDR_TITLE_Y);
609 lv_obj_set_width(l, avail);
610 lv_label_set_long_mode(l, LV_LABEL_LONG_DOT);
611 lv_obj_set_style_text_align(l, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
612 return l;
613}
614
615/* Title + divider only — the burger (settings) lives solely on the amount
616 * screen so settings can't be opened mid-transaction. */
617void build_header(const char *title) {
618 (void)make_title(title, false);
619 make_divider(lv_scr_act(), HDR_DIVIDER_Y);
620}
621
622/******************************************************************
623 * 8b. Wi-Fi signal — the wave, top right of every screen
624 *
625 * Three arcs over a dot, read at a glance from across a counter. The dot is at
626 * least 4px: LVGL clamps the radius to half the side, so 3px draws a square.
627 *
628 * The sizes are one system. The two gaps must be equal, or the dot reads as
629 * stuck to the first ring:
630 * dot to first ring: SIG_R0 - (SIG_AW / 2) - (SIG_DOT / 2)
631 * ring to ring: SIG_RSTEP - SIG_AW
632 *
633 * On the top layer so it survives screen swaps; a modal, created on the same
634 * layer later, covers it.
635 ******************************************************************/
636#define SIG_ARCS 3
637#define SIG_AW 2 /* arc thickness */
638/* Mark is 22x13; both gaps above are 1px. At r=4 the inner ring's 140-degree
639 * cut still bends 2.6px, so it reads as an arc rather than as a dash. */
640#define SIG_R0 4 /* innermost arc radius; each ring adds SIG_RSTEP */
641#define SIG_RSTEP 3
642/* Must stay even: an arc object is 2r + SIG_AW wide, so the fan's centre is a
643 * whole coordinate, and only an even dot centres on it (odd sits half a pixel
644 * off). */
645#define SIG_DOT 4
646/* Per-ring sweep, innermost first, centred on 12 o'clock. Inner rings get more
647 * of their circle so their sagitta r·(1 - cos(sweep/2)) reads as a curve, not a
648 * dash. The sweeps also set the icon's width (2r·sin(sweep/2)) without growing
649 * its height. Past ~140° the inner ring looks like most of a circle; to widen
650 * further, raise SIG_R0. */
651static const uint16_t SIG_SWEEP[SIG_ARCS] = { 140, 110, 100 };
652
653/* The outermost ring's outer edge, which is the icon's half-width and, with the
654 * dot's bottom half, its height. Everything below is derived from it. */
655#define SIG_REACH (SIG_R0 + ((SIG_ARCS - 1) * SIG_RSTEP) + (SIG_AW / 2))
656#define SIG_W (2 * SIG_REACH)
657#define SIG_H (SIG_REACH + ((SIG_DOT + 1) / 2))
658/* Inset from the right edge. */
659#define SIG_INSET 14
660
661static lv_obj_t *s_sig_arc[SIG_ARCS];
662static lv_obj_t *s_sig_dot = NULL;
663static lv_obj_t *s_sig_box = NULL;
664
665static void signal_refresh(lv_timer_t *t) {
666 (void)t;
667 int8_t rssi = 0;
668 /* net_wifi_rssi() fails when the station is not associated — grey dot, no
669 * arc, which is the answer the operator needs before blaming the card.
670 * The cuts match the words on the settings page (Good / Fair / Weak), so
671 * the icon and the "Signal" field there cannot disagree. */
672 const bool up = net_wifi_rssi(&rssi);
673 const int lit = !up ? 0 : (rssi >= -60) ? 3 : (rssi >= -70) ? 2 : 1;
674 for (int i = 0; i < SIG_ARCS; i++) {
675 /* Unlit is COL_DIM, not COL_BORDER: the sale flow's page IS COL_BORDER,
676 * so a COL_BORDER arc would vanish into it. See COL_PAGE. */
677 lv_obj_set_style_arc_color(s_sig_arc[i],
678 (i < lit) ? COL_TEXT : COL_DIM,
679 LV_PART_MAIN);
680 }
681 /* Amber while the network is down and being re-joined in the background
682 * (net.cpp): it tells the operator to wait rather than to go and fix
683 * something. */
684 lv_obj_set_style_bg_color(s_sig_dot,
685 up ? COL_TEXT
687 LV_PART_MAIN);
688 /* Hidden on the splash (nothing connected yet), the calibration screen (it
689 * would cover a corner target), and the admin screens, whose tab bar owns
690 * the top 42px; the Wi-Fi tab reports the signal in words instead. */
691 const bool show = (s_req_screen != UI_SCREEN_SPLASH) &&
696 /* the first-run setup flow: nothing to report yet */
704 if (show) { lv_obj_clear_flag(s_sig_box, LV_OBJ_FLAG_HIDDEN); }
705 else { lv_obj_add_flag(s_sig_box, LV_OBJ_FLAG_HIDDEN); }
706
707 /* Same cadence, same reason — the band's two occupants refresh together. */
709}
710void signal_init(void) {
711 s_sig_box = lv_obj_create(lv_layer_top());
712 lv_obj_remove_style_all(s_sig_box);
713 lv_obj_set_size(s_sig_box, SIG_W, SIG_H);
714 lv_obj_align(s_sig_box, LV_ALIGN_TOP_RIGHT, -SIG_INSET, 6);
715 lv_obj_clear_flag(s_sig_box, LV_OBJ_FLAG_SCROLLABLE);
716 lv_obj_clear_flag(s_sig_box, LV_OBJ_FLAG_CLICKABLE);
717
718 /* Every ring is struck from the same point — the dot — so each arc object is
719 * sized to its own ring and then centred on that point, not on the box. */
720 const lv_coord_t cx = SIG_W / 2, cy = SIG_REACH;
721 for (int i = 0; i < SIG_ARCS; i++) {
722 const lv_coord_t d = (2 * (SIG_R0 + (i * SIG_RSTEP))) + SIG_AW;
723 lv_obj_t *a = lv_arc_create(s_sig_box);
724 lv_obj_remove_style_all(a); /* also kills the knob and the
725 * value indicator: both draw at
726 * the default arc width of 0 */
727 lv_obj_set_size(a, d, d);
728 lv_obj_set_pos(a, cx - (d / 2), cy - (d / 2));
729 /* 0° is 3 o'clock and angles run clockwise, so 270 is 12 o'clock and each
730 * ring is cut symmetrically about it. */
731 lv_arc_set_bg_angles(a, 270 - (SIG_SWEEP[i] / 2), 270 + (SIG_SWEEP[i] / 2));
732 lv_obj_set_style_arc_width(a, SIG_AW, LV_PART_MAIN);
733 /* Square-cut: a rounded cap on a 2px stroke anti-aliases into a darker
734 * pixel past each tip. */
735 lv_obj_set_style_arc_rounded(a, false, LV_PART_MAIN);
736 lv_obj_clear_flag(a, LV_OBJ_FLAG_CLICKABLE);
737 s_sig_arc[i] = a;
738 }
739
740 s_sig_dot = lv_obj_create(s_sig_box);
741 lv_obj_remove_style_all(s_sig_dot);
742 lv_obj_set_size(s_sig_dot, SIG_DOT, SIG_DOT);
743 lv_obj_set_pos(s_sig_dot, cx - (SIG_DOT / 2), cy - (SIG_DOT / 2));
744 lv_obj_set_style_bg_opa(s_sig_dot, LV_OPA_COVER, LV_PART_MAIN);
745 lv_obj_set_style_radius(s_sig_dot, LV_RADIUS_CIRCLE, LV_PART_MAIN);
746 lv_obj_clear_flag(s_sig_dot, LV_OBJ_FLAG_CLICKABLE);
747
748 /* ponytail: 3 s poll. An event-driven update would mean a Wi-Fi handler
749 * poking LVGL from the event task — add one only if the lag shows. */
750 (void)lv_timer_create(signal_refresh, 3000, NULL);
751 signal_refresh(NULL);
752}
754 /* A modal lives on the top layer, so it would survive the screen swap and
755 * sit there swallowing every touch. Nothing wants that. */
756 close_modal();
757
758 /* The swipe's answer: build the admin code screen into a sheet below the
759 * fold and slide it up over the sale screen. Only for the gesture; every
760 * other route to this screen is an ordinary rebuild. */
761 lv_obj_t *sheet = NULL;
762 if (s_sheet_pending) {
763 s_sheet_pending = false;
765 sheet = sheet_open();
766 s_sheet = sheet;
767 }
768 }
769
770 switch (s_req_screen) {
771 case UI_SCREEN_SPLASH: build_splash(); break;
772 case UI_SCREEN_AMOUNT: build_amount(); break;
773 case UI_SCREEN_CONFIRM: build_confirm(); break;
774 case UI_SCREEN_PIN: build_pin(); break;
778 case UI_SCREEN_SETTINGS: build_settings(); break;
783 case UI_SCREEN_WELCOME: build_welcome(); break;
784 case UI_SCREEN_PROV: build_prov(); break;
787 }
788
789 /* The pointer's job ends with the dispatch — a later rebuild of the same
790 * screen (a wrong code, say) must go back through the ordinary clearing
791 * path rather than stacking a second set of widgets into this one. */
792 s_sheet = NULL;
793 if (sheet != NULL) { sheet_slide_in(sheet); }
794
795 /* Guard the freshly built screen against a tap carried over from the
796 * previous one (see indev_read). The "Tap card" screen gets a longer
797 * window because its Cancel button is destructive. */
798 uint32_t lockout = 450U;
801 lockout = 900U;
802 }
803 s_input_block_until = lv_tick_get() + lockout;
804 s_wait_release = true;
805
806 /* Now, not on the next poll, so the mark's visibility matches the new
807 * screen without a three-second lag. */
808 if (s_sig_box != NULL) { signal_refresh(NULL); }
809}
static const pos_asset_t * pos_asset_of(pos_chain_t chain)
The row describing chain.
Definition assets.h:145
pos_net_t
A network, as the picker's first step offers it.
Definition assets.h:51
@ POS_NET__COUNT
Definition assets.h:55
@ POS_COIN_USDC
Definition assets.h:61
@ POS_COIN_USDT
Definition assets.h:62
const lv_img_dsc_t chip_eth
const lv_img_dsc_t chip_tron
const lv_img_dsc_t icon_eth
const lv_img_dsc_t icon_usdt
const lv_img_dsc_t icon_tron
const lv_img_dsc_t icon_poly
const lv_img_dsc_t icon_usdc
const lv_img_dsc_t chip_poly
int civil_local_offset_min(int64_t utc, int std_off_min, int rule)
Offset from UTC in force at utc, DST included.
lv_font_t font_inter_14
lv_font_t font_inter_14_medium
lv_font_t font_pjs_20_medium
lv_font_t font_pjs_20_semibold
bool net_wifi_rssi(int8_t *rssi_out)
Read the RSSI of the currently associated access point.
Definition net.cpp:448
bool net_wifi_reconnecting(void)
true while a kept network is down and the re-join is running.
Definition net.cpp:443
pos_chain_t settings_get_chain(void)
Selected chain, or POS_CHAIN_ETH_USDC if never set.
Definition settings.cpp:165
int16_t settings_get_tz_offset_min(void)
The panel clock's standard (winter) offset from UTC, in minutes east.
Definition settings.cpp:226
uint8_t settings_get_tz_dst(void)
The clock's DST rule, a civil_dst_t; CIVIL_DST_NONE when unset.
Definition settings.cpp:243
bool settings_get_mainnet(void)
true when the terminal is on the production networks.
Definition settings.cpp:189
pos_chain_t
Which chain (and therefore which asset) the terminal charges in.
Definition settings.h:33
One selectable asset.
Definition assets.h:66
pos_net_t net
Definition assets.h:78
bool native
Definition assets.h:79
pos_coin_t coin
Definition assets.h:81
const lv_img_dsc_t tap_icon
Definition tap_icon.c:486
lv_obj_t * s_sheet
Definition ui.cpp:62
lv_obj_t * s_clock_lbl
Definition ui.cpp:90
bool s_charge_busy
Definition ui.cpp:94
lv_obj_t * s_fee_max_lbl
Definition ui.cpp:126
lv_obj_t * s_asset_arrow
Definition ui.cpp:82
void btn_event_cb(lv_event_t *e)
Definition ui.cpp:306
lv_obj_t * s_pin_ta
Definition ui.cpp:98
lv_obj_t * s_amount_row
Definition ui.cpp:76
volatile bool s_sheet_pending
Definition ui.cpp:63
lv_obj_t * s_cal_countdown
Definition ui.cpp:57
ui_tx_state_t s_tx_state
Definition ui.cpp:46
lv_obj_t * s_charge_btn
Definition ui.cpp:86
lv_obj_t * s_amount_cents_label
Definition ui.cpp:73
lv_obj_t * s_fee_prio_lbl
Definition ui.cpp:127
lv_obj_t * s_tx_info_lbl
Definition ui.cpp:48
lv_obj_t * s_pin_eye_lbl
Definition ui.cpp:108
lv_obj_t * s_amount_label
Definition ui.cpp:72
lv_obj_t * s_wifi_eye_lbl
Definition ui.cpp:107
lv_obj_t * s_reset_btn
Definition ui.cpp:171
bool s_pin_for_card
Definition ui.cpp:164
lv_obj_t * s_boot_step_lbl
Definition ui.cpp:120
lv_obj_t * s_admin_ta
Definition ui.cpp:145
volatile ui_screen_t s_req_screen
Definition ui.cpp:34
const char * asset_name(pos_chain_t c)
Definition ui.cpp:194
lv_obj_t * s_close_btn
Definition ui.cpp:172
lv_obj_t * s_asset_btn
Definition ui.cpp:81
lv_obj_t * s_admin_note_lbl
Definition ui.cpp:146
lv_obj_t * s_wifi_pass_ta
Definition ui.cpp:106
@ UI_SCREEN_TOUCH_CAL
Definition ui.h:51
@ UI_SCREEN_AMOUNT
Definition ui.h:37
@ UI_SCREEN_BOOT_ERROR
Definition ui.h:45
@ UI_SCREEN_WIFI_PASS
Definition ui.h:41
@ UI_SCREEN_ADMIN_SET
Definition ui.h:46
@ UI_SCREEN_SETTINGS
Definition ui.h:43
@ UI_SCREEN_ADMIN_UNLOCK
Definition ui.h:47
@ UI_SCREEN_WIFI_LIST
Definition ui.h:40
@ UI_SCREEN_SPLASH
Definition ui.h:36
@ UI_SCREEN_CONFIRM
Definition ui.h:38
@ UI_SCREEN_TX_STATUS
Definition ui.h:44
@ UI_SCREEN_PROV
Definition ui.h:49
@ UI_SCREEN_WELCOME
Definition ui.h:48
@ UI_SCREEN_PIN
Definition ui.h:39
@ UI_SCREEN_CARD_WAIT
Definition ui.h:50
@ UI_SCREEN_WIFI_CONNECTING
Definition ui.h:42
@ UI_TX_STATE_PLACE_CARD
Definition ui.h:95
void build_admin_set(void)
Definition ui_admin.cpp:922
void build_admin_unlock(void)
Definition ui_admin.cpp:929
void close_modal(void)
Definition ui_admin.cpp:424
void build_settings(void)
Definition ui_admin.cpp:64
void build_touch_cal(void)
Definition ui_admin.cpp:331
lv_obj_t * s_page_card
Definition ui_hal.cpp:84
bool s_wait_release
Definition ui_hal.cpp:80
uint32_t s_input_block_until
Definition ui_hal.cpp:79
Private to the ui*.cpp files: the state and helpers they share.
#define CHIP_Y
#define ASSET_BTN_PAD
#define CLOCK_X
void build_wifi_list(void)
Definition ui_setup.cpp:314
void build_pin(void)
Definition ui_sale.cpp:643
#define MENU_BTN_H
#define CHIP_X
#define COL_DANGER
#define HDR_DIVIDER_Y
#define HDR_TITLE_Y
void build_prov(void)
Definition ui_setup.cpp:65
#define ASSET_BTN_H
#define COL_HOME_BAR
#define SCR_H
void build_splash(void)
Definition ui_sale.cpp:295
#define COL_WARN
void build_wifi_pass(void)
Definition ui_setup.cpp:375
volatile bool s_tz_dirty
void build_card_wait(void)
Definition ui_setup.cpp:271
#define COL_SURFACE
#define MENU_BTN_X
#define COL_PAGE_GRAD
#define COL_DIM
#define MENU_BTN_W
#define BTN_RADIUS
#define SCR_W
void build_tx_status(void)
Definition ui_sale.cpp:734
void build_boot_error(void)
Definition ui_sale.cpp:871
#define CARD_X
#define ASSET_BTN_X
#define COL_TEXT
#define CLOCK_Y
#define COL_TITLE
#define PILL_H
#define CARD_H
void build_wifi_connecting(void)
Definition ui_setup.cpp:409
#define ASSET_BTN_Y
#define TAB_W
void build_amount(void)
Definition ui_sale.cpp:330
BtnAction
@ ACT_NET_PICK
#define COIN_SZ
#define CARD_BTN_H
#define COL_PAGE
void build_welcome(void)
Definition ui_setup.cpp:17
#define CARD_Y
#define COL_BG
#define COL_ACTION
#define CARD_W
#define MENU_BTN_Y
#define COL_BORDER
void build_confirm(void)
Definition ui_sale.cpp:400
lv_obj_t * make_glyph_disc(lv_obj_t *parent, const char *sym, lv_color_t bg, lv_coord_t sz)
static void sheet_slide_in(lv_obj_t *sh)
static lv_obj_t * s_sig_box
#define SIG_REACH
#define SIG_R0
#define SIG_INSET
lv_obj_t * make_asset_badge(lv_obj_t *parent, pos_chain_t chain)
#define SIG_ARCS
static const uint16_t SIG_SWEEP[SIG_ARCS]
lv_obj_t * make_button(lv_obj_t *parent, const char *label, lv_color_t bg, lv_color_t fg, lv_coord_t w, lv_coord_t h, lv_align_t align, lv_coord_t x, lv_coord_t y, BtnAction act, const lv_font_t *font)
void asset_btn_set_compact(bool compact)
static const lv_img_dsc_t *const NET_CHIP[POS_NET__COUNT]
void make_divider(lv_obj_t *parent, lv_coord_t y)
lv_obj_t * make_net_badge(lv_obj_t *parent, pos_net_t net)
static lv_obj_t * s_sig_arc[SIG_ARCS]
static lv_obj_t * sheet_open(void)
lv_obj_t * make_ghost_button(lv_obj_t *parent, const char *label, lv_coord_t w, lv_align_t align, lv_coord_t x, lv_coord_t y, BtnAction act)
lv_obj_t * make_label(lv_obj_t *parent, const char *txt, lv_color_t color, const lv_font_t *font, lv_align_t align, lv_coord_t x, lv_coord_t y)
static int16_t s_tz_off_min
#define PILL_TEXT_X
#define SIG_RSTEP
static void clock_refresh(void)
Retext the band's clock from the system clock plus the stored offset.
#define SIG_DOT
static const lv_img_dsc_t *const NET_ICON[POS_NET__COUNT]
static void sheet_y_cb(void *obj, int32_t v)
void make_tap_mark(lv_obj_t *parent, lv_coord_t y)
lv_obj_t * make_card(lv_obj_t *parent)
lv_obj_t * make_title(const char *txt, bool has_icon, lv_obj_t *parent)
Screen title, centred but never underneath the top-left icon button.
void render_requested_screen(void)
#define SIG_AW
static lv_obj_t * make_icon_box(lv_obj_t *parent, const lv_img_dsc_t *coin_src, const lv_img_dsc_t *chip_src)
void signal_init(void)
static const lv_img_dsc_t * coin_icon(const pos_asset_t *a)
void paint_page(lv_obj_t *obj)
Lay the sale-flow chrome and return the white card to build into.
#define BADGE_SZ
void build_header(const char *title)
static lv_obj_t * s_sig_dot
lv_obj_t * build_page(int step, bool band)
void clear_screen(void)
#define PILL_TEXT_PAD_R
static uint8_t s_tz_dst
static void code_field_wipe(lv_obj_t *ta)
lv_obj_t * make_field(lv_obj_t *parent, const char *caption, const char *value, lv_color_t col)
#define SIG_H
lv_obj_t * make_icon_button(const char *sym, BtnAction act, lv_obj_t *parent)
static void signal_refresh(lv_timer_t *t)
lv_obj_t * make_pill(lv_obj_t *parent, const char *title, const char *sub, lv_coord_t w, lv_coord_t y, BtnAction act, bool leaf)
void add_test_chip(void)
lv_obj_t * make_asset_button(lv_obj_t *parent)
#define COL_TAP_MARK
#define SIG_W