From 104b7fb0b73053c1cf227659bb5c4eb2e9f5c8fe Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Sat, 29 Aug 2026 13:54:44 +0200 Subject: [PATCH] Refine Apps and multiboot UI --- App/apps/app_menu.c | 165 +++++++++++++++++++++++++++++++------------- App/ui/multiboot.c | 58 ++++++++-------- 2 files changed, 144 insertions(+), 79 deletions(-) diff --git a/App/apps/app_menu.c b/App/apps/app_menu.c index 18e85e19..b77f6cd0 100644 --- a/App/apps/app_menu.c +++ b/App/apps/app_menu.c @@ -26,16 +26,48 @@ #include "driver/gpio.h" #include "ui/helper.h" -/* Highlight a row (same rounded-invert look as the multiboot selector). */ -static void app_invert_row(uint8_t line) +/* "F4HWN APPS" banner and the same thin separator used by the multiboot + * selector. Bit 3 leaves room for the selected-row capsule's top edge. */ +static void app_status_bar(void) { - gFrameBuffer[line][0] ^= 0x7Fu; - for (uint8_t x = 1u; x < LCD_WIDTH - 1u; x++) + UI_StatusClear(); + GUI_DisplaySmallestInverse("F4HWN APPS", 44, 0, true, true, 84); + + for (uint8_t x = 2u; x < LCD_WIDTH - 2u; x++) + gFrameBuffer[0][x] |= 0x08u; +} + +/* Bottom key hints, matching the multiboot selector. */ +static void app_key_hints(void) +{ + const uint8_t sp = 6u; + const char *act_exit = "QUIT"; + const uint8_t ae = (uint8_t)strlen(act_exit); + const uint8_t xm = 4u; + const uint8_t xe = (uint8_t)(124u - ae * 4u - sp - 16u); + + GUI_DisplaySmallestInverse("MENU", xm, 6, false, true, (uint8_t)(xm + 16u)); + GUI_DisplaySmallest("RUN", (uint8_t)(xm + 16u + sp), 49, false, true); + + GUI_DisplaySmallestInverse("EXIT", xe, 6, false, true, (uint8_t)(xe + 16u)); + GUI_DisplaySmallest(act_exit, (uint8_t)(xe + 16u + sp), 49, false, true); +} + +/* Fixed selection capsule around the primary information (the app name). + * Slot number stays in the normal font; size is plain 3x5 metadata. */ +#define APP_NAME_BOX_START 12u +#define APP_NAME_BOX_END 102u +#define APP_NAME_TEXT_X 14u + +static void app_invert_name(uint8_t line) +{ + gFrameBuffer[line][APP_NAME_BOX_START] ^= 0x7Fu; + for (uint8_t x = APP_NAME_BOX_START + 1u; x < APP_NAME_BOX_END; x++) { gFrameBuffer[line][x] ^= 0xFFu; gFrameBuffer[line - 1u][x] ^= 0x80u; } - gFrameBuffer[line][LCD_WIDTH - 1u] ^= 0x7Fu; + gFrameBuffer[line][APP_NAME_BOX_END] ^= 0x7Fu; } /* Debounced blocking key read, then wait for release (mirrors mb_get_key). */ @@ -82,6 +114,26 @@ static void app_copy(char *dst, uint8_t cap, const char *src, uint8_t src_cap) dst[n] = '\0'; } +/* Display the code payload rounded up to 0.1 KiB, so the compact value never + * understates the space occupied by the app. The space before "KB" is omitted + * because this secondary value is rendered in the tiny 3x5 font. */ +static void app_format_size(char out[6], uint32_t bytes) +{ + if (bytes > APP_OVERLAY_MAX) + { + memcpy(out, "--KB", 5u); + return; + } + + const uint16_t tenths = (uint16_t)((bytes * 10u + 1023u) / 1024u); + out[0] = (char)('0' + tenths / 10u); + out[1] = '.'; + out[2] = (char)('0' + tenths % 10u); + out[3] = 'K'; + out[4] = 'B'; + out[5] = '\0'; +} + /* Human-readable reason for an APP_LaunchOverlay / APP_ValidateSlot failure. */ static const char *app_err_text(uint8_t rc) { @@ -118,87 +170,102 @@ static void app_show_error(const char *name, uint8_t rc) app_get_key(); /* blocking: dismiss on any key */ } -/* Visible app rows (framebuffer lines 1..APP_MENU_ROWS; line 0 is the header). */ -#define APP_MENU_ROWS 6u +/* Five visible slots; line 0 holds the separator and line 6 the key hints. */ +#define APP_MENU_ROWS 5u +#define APP_MENU_SLOT_COUNT 8u + +_Static_assert(APP_MENU_SLOT_COUNT <= APP_SLOT_COUNT, + "APP_MENU_SLOT_COUNT exceeds the physical app slot count"); void APP_MenuOpen(void) { - /* Apps are installed from UV Studio (0x073x) into physical slots 0..N-1 - * (shown to the user as 1..N there). Scan them all; the list is empty until - * the user pushes one ("No apps installed"). */ - app_header_t hdr[APP_SLOT_COUNT]; - uint8_t list[APP_SLOT_COUNT]; /* slot indices of the committed apps */ - uint8_t count = 0; + /* Apps are installed from UV Studio (0x073x) into physical slots 0..N-1, + * shown here as 1..N. Keep empty slots in the list so their location is + * visible and selectable while scrolling. */ + app_header_t hdr[APP_MENU_SLOT_COUNT]; + bool installed[APP_MENU_SLOT_COUNT]; - for (uint8_t slot = 0; slot < APP_SLOT_COUNT; slot++) + for (uint8_t slot = 0; slot < APP_MENU_SLOT_COUNT; slot++) { - if (APP_SlotInfo(slot, &hdr[slot]) == APP_OK && - (hdr[slot].flags & APP_FLAG_COMMITTED)) - list[count++] = slot; + installed[slot] = APP_SlotInfo(slot, &hdr[slot]) == APP_OK && + (hdr[slot].flags & APP_FLAG_COMMITTED); } - /* Remember the cursor across open/close of the Apps menu. Clamp in case the - * installed-app set changed since we were last here. */ + /* Remember the physical slot and scrolling window across menu openings. */ static uint8_t sel = 0; static uint8_t top = 0; /* first visible row of the scrolling window */ - if (count == 0u || sel >= count) + if (sel >= APP_MENU_SLOT_COUNT || top > APP_MENU_SLOT_COUNT - APP_MENU_ROWS) sel = top = 0u; app_wait_release(); for (;;) { UI_DisplayClear(); - UI_StatusClear(); /* wipe the VFO status line (DW, battery, ...) first */ - /* 10 glyphs x ~4 px = 40 px wide, centred: x=(128-40)/2=44, endX=x+40. */ - GUI_DisplaySmallestInverse("F4HWN APPS", 44, 0, true, true, 84); + app_status_bar(); /* also wipes the VFO status line (DW, battery, ...) */ - if (count == 0) - { - UI_PrintStringSmallNormal("No apps installed", 2, 126, 3); - } - else - { - /* Scrolling window: slide [top, top+APP_MENU_ROWS) so it always holds - * the selection, keeping all APP_SLOT_COUNT apps reachable - not just - * the first APP_MENU_ROWS. */ - if (sel < top) - top = sel; - else if (sel >= (uint8_t)(top + APP_MENU_ROWS)) - top = (uint8_t)(sel - APP_MENU_ROWS + 1u); + /* Slide [top, top+APP_MENU_ROWS) so it always contains the selection. */ + if (sel < top) + top = sel; + else if (sel >= (uint8_t)(top + APP_MENU_ROWS)) + top = (uint8_t)(sel - APP_MENU_ROWS + 1u); - for (uint8_t i = top; i < count && (uint8_t)(i - top) < APP_MENU_ROWS; i++) + for (uint8_t slot = top; + slot < APP_MENU_SLOT_COUNT && (uint8_t)(slot - top) < APP_MENU_ROWS; + slot++) + { + char number[2]; + char name[14]; + char size[6]; + const uint8_t visible_number = (uint8_t)(slot + 1u); + const uint8_t fbLine = (uint8_t)(slot - top + 1u); + + number[0] = (char)('0' + visible_number); + number[1] = '\0'; + + UI_PrintStringSmallNormal(number, 2u, 0, fbLine); + if (installed[slot]) { - char line[19]; - app_copy(line, sizeof(line), hdr[list[i]].name, APP_NAME_LEN); - const uint8_t fbLine = (uint8_t)(i - top + 1u); - UI_PrintStringSmallNormal(line, 2, 0, fbLine); - if (i == sel) - app_invert_row(fbLine); + app_copy(name, sizeof(name), hdr[slot].name, APP_NAME_LEN); + app_format_size(size, hdr[slot].code_size); + UI_PrintStringSmallNormal(name, APP_NAME_TEXT_X, 0, fbLine); + GUI_DisplaySmallest(size, + (uint8_t)(LCD_WIDTH - 2u - strlen(size) * 4u), + (uint8_t)(fbLine * 8u + 1u), false, true); } + else + { + UI_PrintStringSmallNormal("Empty", APP_NAME_TEXT_X, 0, fbLine); + } + + if (slot == sel) + app_invert_name(fbLine); } + app_key_hints(); + ST7565_BlitStatusLine(); ST7565_BlitFullScreen(); const KEY_Code_t key = app_get_key(); if (key == KEY_EXIT) return; - if (count == 0) - continue; switch (key) { case KEY_UP: - sel = (sel == 0u) ? (uint8_t)(count - 1u) : (uint8_t)(sel - 1u); + sel = (sel == 0u) ? (uint8_t)(APP_MENU_SLOT_COUNT - 1u) : (uint8_t)(sel - 1u); break; case KEY_DOWN: - sel = (uint8_t)((sel + 1u) % count); + sel = (uint8_t)((sel + 1u) % APP_MENU_SLOT_COUNT); break; case KEY_MENU: { - const uint8_t rc = APP_LaunchOverlay(list[sel]); /* runs until the app exits */ + if (!installed[sel]) + break; + + const uint8_t rc = APP_LaunchOverlay(sel); /* runs until the app exits */ if (rc != APP_OK) - app_show_error(hdr[list[sel]].name, rc); /* no longer silent */ + app_show_error(hdr[sel].name, rc); /* no longer silent */ app_wait_release(); break; } diff --git a/App/ui/multiboot.c b/App/ui/multiboot.c index 3d7f0a0a..8a17232b 100644 --- a/App/ui/multiboot.c +++ b/App/ui/multiboot.c @@ -85,6 +85,8 @@ static uint8_t mb_copy_slot_version(char *dst, uint8_t cap, const mb_slot_header header->fw_version[i + 1u] >= '0' && header->fw_version[i + 1u] <= '9') { + if (n + 1u < cap) + dst[n++] = 'v'; i++; while (n + 1u < cap && i < MB_VERSION_LEN) { @@ -136,15 +138,21 @@ static void mb_key_hints(const char *act_menu, const char *act_exit) GUI_DisplaySmallest(act_exit, (uint8_t)(xe + 16u + sp), 49, false, true); } -static void mb_invert_rounded_row(uint8_t line) +/* Fixed selection capsule around the firmware name. The slot index stays in + * the normal font while the version is plain 3x5 metadata. */ +#define MB_NAME_BOX_START 12u +#define MB_NAME_BOX_END 96u +#define MB_NAME_TEXT_X 14u + +static void mb_invert_name(uint8_t line) { - gFrameBuffer[line][0] ^= 0x7Fu; - for (uint8_t x = 1u; x < LCD_WIDTH - 1u; x++) + gFrameBuffer[line][MB_NAME_BOX_START] ^= 0x7Fu; + for (uint8_t x = MB_NAME_BOX_START + 1u; x < MB_NAME_BOX_END; x++) { gFrameBuffer[line][x] ^= 0xFFu; gFrameBuffer[line - 1u][x] ^= 0x80u; } - gFrameBuffer[line][LCD_WIDTH - 1u] ^= 0x7Fu; + gFrameBuffer[line][MB_NAME_BOX_END] ^= 0x7Fu; } static void mb_show_message(const char *line1, const char *line2, const char *line3) @@ -218,8 +226,8 @@ static void mb_render_slots(uint8_t selected, const mb_slot_header_t headers[MB_SLOT_COUNT], const uint8_t status[MB_SLOT_COUNT]) { - char line[19]; /* 18 glyphs max: 18 * 7 px fits from x=2 to x=126. */ - char version[MB_VERSION_LEN]; + char name[13]; + char version[8]; /* v + up to six version digits/dots in the 3x5 column. */ UI_DisplayClear(); mb_status_bar(); @@ -227,50 +235,40 @@ static void mb_render_slots(uint8_t selected, for (uint8_t slot = 0; slot < MB_SLOT_COUNT; slot++) { const uint8_t fbLine = (uint8_t)(slot + 1u); /* page 1 stays blank */ + char index[2]; uint8_t version_len = 0; uint8_t version_x = 0; - memset(line, 0, sizeof(line)); + memset(name, 0, sizeof(name)); memset(version, 0, sizeof(version)); /* Slot 0 is the auto-backed-up main firmware: label it 'M' (Main) so it * reads apart from the numbered user slots 1..4. */ - line[0] = (slot == 0u) ? 'M' : (char)('0' + slot); - line[1] = ' '; - line[2] = ' '; + index[0] = (slot == 0u) ? 'M' : (char)('0' + slot); + index[1] = '\0'; if (status[slot] == MB_OK) { - uint8_t name_cap = sizeof(line) - 3u; - version_len = mb_copy_slot_version(version, sizeof(version), &headers[slot]); if (version_len) - { - const uint8_t name_x = 2u + 3u * 7u; - uint8_t available; - - version_x = (uint8_t)(LCD_WIDTH - 2u - version_len * 7u); - available = version_x > name_x - ? (uint8_t)((version_x - name_x) / 7u) - : 0u; - if (available + 1u < name_cap) - name_cap = available + 1u; - } + version_x = (uint8_t)(LCD_WIDTH - 2u - version_len * 4u); if (headers[slot].name[0]) - mb_copy_label(&line[3], name_cap, headers[slot].name, MB_NAME_LEN); + mb_copy_label(name, sizeof(name), headers[slot].name, MB_NAME_LEN); else if (!version_len) - mb_copy_label(&line[3], name_cap, headers[slot].fw_version, MB_VERSION_LEN); + mb_copy_label(name, sizeof(name), headers[slot].fw_version, MB_VERSION_LEN); } else - mb_copy_label(&line[3], sizeof(line) - 3u, mb_error_text(status[slot]), 20u); + mb_copy_label(name, sizeof(name), mb_error_text(status[slot]), 20u); - UI_PrintStringSmallNormal(line, 2, 0, fbLine); + UI_PrintStringSmallNormal(index, 2u, 0, fbLine); + UI_PrintStringSmallNormal(name, MB_NAME_TEXT_X, 0, fbLine); if (version_len) - UI_PrintStringSmallNormal(version, version_x, 0, fbLine); + GUI_DisplaySmallest(version, version_x, + (uint8_t)(fbLine * 8u + 1u), false, true); - /* Selected row: full-width rounded inverse capsule. */ + /* Selected row: fixed rounded inverse capsule around the name only. */ if (slot == selected) - mb_invert_rounded_row(fbLine); + mb_invert_name(fbLine); } mb_key_hints("SELECT", "QUIT");