From 72366eff64b397bfcbf3d6c697132c8185e804ed Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Thu, 2 Oct 2025 13:05:45 -0700 Subject: [PATCH 01/27] implement OpMode cycle, allows easier modularity when adding new OpModes --- openrtx/src/ui/default/ui.c | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/openrtx/src/ui/default/ui.c b/openrtx/src/ui/default/ui.c index 1b7085c38..779e56c15 100644 --- a/openrtx/src/ui/default/ui.c +++ b/openrtx/src/ui/default/ui.c @@ -253,6 +253,29 @@ static const char *symbols_ITU_T_E161_callsign[] = "" }; +static const uint8_t opmode_cycle[] = +{ + OPMODE_FM, +#ifdef CONFIG_DMR + OPMODE_DMR, +#endif +#ifdef CONFIG_M17 + OPMODE_M17, +#endif +}; + +#define OPMODE_CYCLE_COUNT (sizeof(opmode_cycle)/sizeof(opmode_cycle[0])) + +static uint8_t next_opmode(uint8_t cur) { + for (unsigned i = 0; i < OPMODE_CYCLE_COUNT; i++) { + if (opmode_cycle[i] == cur) { + return opmode_cycle[(i + 1) % OPMODE_CYCLE_COUNT]; + } + } + // If the current mode isn't in the cycle, reset to first. + return opmode_cycle[0]; +} + // Calculate number of menu entries const uint8_t menu_num = sizeof(menu_items)/sizeof(menu_items[0]); const uint8_t settings_num = sizeof(settings_items)/sizeof(settings_items[0]); @@ -993,14 +1016,7 @@ static void _ui_fsm_menuMacro(kbd_msg_t msg, bool *sync_rtx) break; case 5: // Cycle through radio modes - #ifdef CONFIG_M17 - if(state.channel.mode == OPMODE_FM) - state.channel.mode = OPMODE_M17; - else if(state.channel.mode == OPMODE_M17) - state.channel.mode = OPMODE_FM; - else //catch any invalid states so they don't get locked out - #endif - state.channel.mode = OPMODE_FM; + state.channel.mode = next_opmode(state.channel.mode); *sync_rtx = true; vp_announceRadioMode(state.channel.mode, queueFlags); break; From eacb5dec7ceb8c0db42619b324d8a886299b6201 Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Wed, 5 Nov 2025 21:16:56 -0800 Subject: [PATCH 02/27] first draft UI architecture --- meson.build | 4 + openrtx/include/ui/ui_menu.h | 44 ++++ openrtx/include/ui/ui_new.h | 19 ++ openrtx/include/ui/ui_screen.h | 37 +++ openrtx/src/core/threads.c | 109 +++++--- openrtx/src/ui/new/ui.c | 54 ++++ openrtx/src/ui/new/ui_compat.c | 76 ++++++ openrtx/src/ui/new/ui_core.c | 35 +++ openrtx/src/ui/new/ui_menu.c | 258 +++++++++++++++++++ openrtx/src/ui/new/ui_menu_tree.c | 49 ++++ platform/targets/linux/emulator/sdl_engine.c | 4 + 11 files changed, 653 insertions(+), 36 deletions(-) create mode 100644 openrtx/include/ui/ui_menu.h create mode 100644 openrtx/include/ui/ui_new.h create mode 100644 openrtx/include/ui/ui_screen.h create mode 100644 openrtx/src/ui/new/ui.c create mode 100644 openrtx/src/ui/new/ui_compat.c create mode 100644 openrtx/src/ui/new/ui_core.c create mode 100644 openrtx/src/ui/new/ui_menu.c create mode 100644 openrtx/src/ui/new/ui_menu_tree.c diff --git a/meson.build b/meson.build index 9c5a84ce2..c28d69973 100644 --- a/meson.build +++ b/meson.build @@ -78,6 +78,10 @@ openrtx_inc = ['openrtx/include', 'platform'] ui_src_default = ['openrtx/src/ui/default/ui.c', 'openrtx/src/ui/default/ui_main.c', 'openrtx/src/ui/default/ui_menu.c', + 'openrtx/src/ui/new/ui_core.c', + 'openrtx/src/ui/new/ui_compat.c', + 'openrtx/src/ui/new/ui_menu.c', + 'openrtx/src/ui/new/ui_menu_tree.c', 'openrtx/src/ui/default/ui_strings.c'] ui_src_module17 = ['openrtx/src/ui/module17/ui.c', diff --git a/openrtx/include/ui/ui_menu.h b/openrtx/include/ui/ui_menu.h new file mode 100644 index 000000000..d00688d2d --- /dev/null +++ b/openrtx/include/ui/ui_menu.h @@ -0,0 +1,44 @@ +/* ui_menu.h */ +#ifndef UI_MENU_H +#define UI_MENU_H + +#include +#include "ui/ui_screen.h" + +typedef enum { + MENU_NODE_FOLDER, + MENU_NODE_ACTION, + MENU_NODE_VALUE, +} MenuNodeKind; + +typedef enum { + MENU_CMD_DRAW, + MENU_CMD_SELECT, + MENU_CMD_GETSTATE, + MENU_CMD_EDIT_BEGIN, + MENU_CMD_EDIT_APPLY, + MENU_CMD_EDIT_CANCEL, +} MenuCmd; + +typedef int (*MenuCb)(MenuCmd cmd, int arg, void *user); + +typedef struct MenuItem MenuItem; +struct MenuItem { + MenuNodeKind kind; + const char *label; + uint8_t child_count; + const MenuItem *children; // only for FOLDER + MenuCb cb; // for values/actions + void *user; +}; + +/* The menu screen itself */ +UiScreen *ui_get_menu_screen(void); + +/* Helper to reset state to root and push the menu screen on the stack */ +void ui_menu_open_root(void); + +/* Root menu entry point (global tree) */ +extern const MenuItem g_root_menu; + +#endif \ No newline at end of file diff --git a/openrtx/include/ui/ui_new.h b/openrtx/include/ui/ui_new.h new file mode 100644 index 000000000..ddf03548a --- /dev/null +++ b/openrtx/include/ui/ui_new.h @@ -0,0 +1,19 @@ +#ifndef UI_NEW_H +#define UI_NEW_H + +#include "core/input.h" +#include "ui/ui_screen.h" + +#define UI_ALIGN_LEFT 0x00 +#define UI_ALIGN_RIGHT 0x01 +#define UI_ALIGN_CENTER 0x02 + +#define UI_TYPE_UPPERCASE 0x01 +#define UI_TYPE_LOWERCASE 0x02 +#define UI_TYPE_NUMBER 0x04 +#define UI_TYPE_HEX 0x08 +#define UI_TYPE_TEXT UI_TYPE_LOWERCASE | UI_TYPE_UPPERCASE +#define UI_TYPE_ALL UI_TYPE_LOWERCASE | UI_TYPE_UPPERCASE | UI_TYPE_NUMBER + +bool ui_build_event_from_kbd(const kbd_msg_t *kbd, UiEvent *ev); +#endif \ No newline at end of file diff --git a/openrtx/include/ui/ui_screen.h b/openrtx/include/ui/ui_screen.h new file mode 100644 index 000000000..1627160f4 --- /dev/null +++ b/openrtx/include/ui/ui_screen.h @@ -0,0 +1,37 @@ +#ifndef UI_SCREEN_H +#define UI_SCREEN_H + +/* ui_screen.h */ + +/* Full screen interface controlled by main UI thread */ +/* OpModes/Apps define `UiScreen` instances */ + +#include + +#include "interfaces/keyboard.h" + +typedef struct UiScreen UiScreen; + +typedef struct { + uint16_t type; + enum key key; +} UiEvent; + +typedef void (*UiScreenTick)(UiScreen *self, const UiEvent *ev); +typedef void (*UiScreenDraw)(UiScreen *self); + +struct UiScreen { + UiScreenTick tick; + UiScreenDraw draw; + + + /// @brief screen-local state + void *ctx; +}; + +/* Stack management owned by ui_core.c */ +void ui_push_screen(UiScreen *s); +void ui_pop_screen(void); +UiScreen *ui_current_screen(void); + +#endif \ No newline at end of file diff --git a/openrtx/src/core/threads.c b/openrtx/src/core/threads.c index 537c23be0..1529db832 100644 --- a/openrtx/src/core/threads.c +++ b/openrtx/src/core/threads.c @@ -21,11 +21,17 @@ #include "core/backup.h" #include "core/gps.h" #include "core/voicePrompts.h" +#include "ui/ui_screen.h" +#include "ui/ui_new.h" #if defined(PLATFORM_TTWRPLUS) #include "pmu.h" #endif +/* from ui_compat.c */ +extern UiScreen *ui_get_compat_screen(void); +extern void *ui_get_compat_state(void); // returns CompatState* + /* Mutex for concurrent access to RTX state variable */ pthread_mutex_t rtx_mutex; @@ -36,10 +42,9 @@ void *ui_threadFunc(void *arg) { (void) arg; - kbd_msg_t kbd_msg; - rtxStatus_t rtx_cfg = { 0 }; - bool sync_rtx = true; - long long time = 0; + kbd_msg_t kbd_msg; + rtxStatus_t rtx_cfg = { 0 }; + long long time = 0; // Load initial state and update the UI ui_saveState(); @@ -49,57 +54,89 @@ void *ui_threadFunc(void *arg) sleepFor(1u, 0u); gfx_render(); + // Initialize the new screen system with the legacy-compat screen on the stack + ui_push_screen(ui_get_compat_screen()); + while(state.devStatus != SHUTDOWN) { time = getTick(); + // Keyboard -> UI event + bool have_ev = false; + UiEvent ev; + if(input_scanKeyboard(&kbd_msg)) { + // Feed the old event path for the legacy FSM (ui_updateFSM) ui_pushEvent(EVENT_KBD, kbd_msg.value); + + // Build a UiEvent for new-style screens + if(ui_build_event_from_kbd(&kbd_msg, &ev)) + { + have_ev = true; + } } + // Run the active screen's FSM under state_mutex pthread_mutex_lock(&state_mutex); // Lock r/w access to radio state - ui_updateFSM(&sync_rtx); // Update UI FSM - ui_saveState(); // Save local state copy + + // sync_rtx handling stays as it is for now (compat screen still sets it) + UiScreen *scr = ui_current_screen(); + if (scr != NULL) + { + scr->tick(scr, have_ev ? &ev : NULL); + ui_saveState(); // Keep using existing snapshot mechanism + } + pthread_mutex_unlock(&state_mutex); // Unlock r/w access to radio state vp_tick(); // continue playing voice prompts in progress if any. // If synchronization needed take mutex and update RTX configuration - if(sync_rtx) + // The compat screen owns the sync flag (CompatState.sync_rtx) { - pthread_mutex_lock(&rtx_mutex); - rtx_cfg.opMode = state.channel.mode; - rtx_cfg.bandwidth = state.channel.bandwidth; - rtx_cfg.rxFrequency = state.channel.rx_frequency; - rtx_cfg.txFrequency = state.channel.tx_frequency; - rtx_cfg.txPower = state.channel.power; - rtx_cfg.sqlLevel = state.settings.sqlLevel; - rtx_cfg.rxToneEn = state.channel.fm.rxToneEn; - rtx_cfg.rxTone = ctcss_tone[state.channel.fm.rxTone]; - rtx_cfg.txToneEn = state.channel.fm.txToneEn; - rtx_cfg.txTone = ctcss_tone[state.channel.fm.txTone]; - rtx_cfg.toneEn = state.tone_enabled; - - // Enable Tx if channel allows it and we are in UI main screen - rtx_cfg.txDisable = state.channel.rx_only || state.txDisable; - - // Copy new M17 CAN, source and destination addresses - rtx_cfg.can = state.settings.m17_can; - rtx_cfg.canRxEn = state.settings.m17_can_rx; - strncpy(rtx_cfg.source_address, state.settings.callsign, 10); - strncpy(rtx_cfg.destination_address, state.settings.m17_dest, 10); - - pthread_mutex_unlock(&rtx_mutex); - - rtx_configure(&rtx_cfg); - sync_rtx = false; + typedef struct { + bool sync_rtx; + } CompatState; + + CompatState *cst = (CompatState *)ui_get_compat_state(); + + if(cst && cst->sync_rtx) + { + pthread_mutex_lock(&rtx_mutex); + + rtx_cfg.opMode = state.channel.mode; + rtx_cfg.bandwidth = state.channel.bandwidth; + rtx_cfg.rxFrequency = state.channel.rx_frequency; + rtx_cfg.txFrequency = state.channel.tx_frequency; + rtx_cfg.txPower = state.channel.power; + rtx_cfg.sqlLevel = state.settings.sqlLevel; + rtx_cfg.rxToneEn = state.channel.fm.rxToneEn; + rtx_cfg.rxTone = ctcss_tone[state.channel.fm.rxTone]; + rtx_cfg.txToneEn = state.channel.fm.txToneEn; + rtx_cfg.txTone = ctcss_tone[state.channel.fm.txTone]; + rtx_cfg.toneEn = state.tone_enabled; + + // Enable Tx if channel allows it and we are in UI main screen + rtx_cfg.txDisable = state.channel.rx_only || state.txDisable; + + // Copy new M17 CAN, source and destination addresses + rtx_cfg.can = state.settings.m17_can; + rtx_cfg.canRxEn = state.settings.m17_can_rx; + strncpy(rtx_cfg.source_address, state.settings.callsign, 10); + strncpy(rtx_cfg.destination_address, state.settings.m17_dest, 10); + + pthread_mutex_unlock(&rtx_mutex); + + rtx_configure(&rtx_cfg); + cst->sync_rtx = false; + } } - // Update UI and render on screen, if necessary - if(ui_updateGUI() == true) + // Draw the active screen + if(scr != NULL) { - gfx_render(); + scr->draw(scr); } // 40Hz update rate for keyboard and UI diff --git a/openrtx/src/ui/new/ui.c b/openrtx/src/ui/new/ui.c new file mode 100644 index 000000000..98bca7fc2 --- /dev/null +++ b/openrtx/src/ui/new/ui.c @@ -0,0 +1,54 @@ +#include "ui/ui_new.h" + +/*** + +Current menu structure: + +- Banks + - All channels + - (banks...) +- Channels + - (channels...) +- Contacts + - (contacts...) +- GPS + - GPS UI Screen +- Settings + - Display + - Brightness (0-100,5) + - Timer (Enum) + - Battery Icon (Bool) + - GPS + - GPS Enabled (Bool) + - GPS Set Time (Bool) + - UTC Timezone (-Inf-Inf,0.5) + - Radio + - Offset (Input) + - Direction (Enum) + - Step (Enum) + - M17 + - Callsign (Input) + - CAN (0-15,1) + - CAN RX Check (Bool) + - FM + - CTCSS Tone (Enum) + - CTCSS En. (Enum) + - Accessibility + - Macro Latch (Bool) + - Voice (Bool) + - Phonetic (Bool) + - Default Settings + - Are you sure UI +- Info + - (commit ver string) + - Bat. Voltage + - Bat. Charge + - RSSI + - Used heap + - Band + - VHF + - UHF + - Hw Version +- About + - About Screen UI + */ \ No newline at end of file diff --git a/openrtx/src/ui/new/ui_compat.c b/openrtx/src/ui/new/ui_compat.c new file mode 100644 index 000000000..c0cf08f34 --- /dev/null +++ b/openrtx/src/ui/new/ui_compat.c @@ -0,0 +1,76 @@ +/* compatibility UiScreen implementation for legacy UI */ +#include "core/graphics.h" +#include "core/state.h" +#include "core/ui.h" +#include "interfaces/keyboard.h" +#include "ui/ui_screen.h" +#include "ui/ui_menu.h" + +typedef struct { + bool sync_rtx; /* set true by FSM when radio config should be updated */ +} CompatState; + +static void compat_tick(UiScreen *self, const UiEvent *ev); +static void compat_draw(UiScreen *self); + +/* Single global compat state + screen */ +static CompatState g_compat_state = { + .sync_rtx = true, /* force one initial rtx_configure after boot */ +}; + +static UiScreen g_compat_screen = { + .tick = compat_tick, + .draw = compat_draw, + .ctx = &g_compat_state, +}; + +/* Public accessors used by ui_threadFunc */ + +UiScreen *ui_get_compat_screen(void) +{ + return &g_compat_screen; +} + +/* Returns a pointer that ui_threadFunc casts back to CompatState */ +void *ui_get_compat_state(void) +{ + return &g_compat_state; +} + +/* --- UIScreen implementation --- */ + +static void compat_tick(UiScreen *self, const UiEvent *ev) +{ + CompatState *st = (CompatState *)self->ctx; + + if (ev && ev->key == KEY_F3) { + ui_menu_open_root(); + return; + } + + /** + * Old world: + * ui_updateFSM(&sync_rtx); + * was called directly from ui_threadFunc. + * + * New world: + * ui_threadFunc calls scr->tick(), and this wrapper calls the + * legacy FSM exactly as before. + */ + ui_updateFSM(&st->sync_rtx); +} + +/** + * Drawing wrapper: + * - ui_threadFunc calls scr->draw(scr); + * - compat_draw uses the existing ui_updateGUI() and gfx_render() + * so nothing else in the old UI has to change yet. + */ +static void compat_draw(UiScreen *self) { + (void)self; + + if (ui_updateGUI()) + { + gfx_render(); + } +} diff --git a/openrtx/src/ui/new/ui_core.c b/openrtx/src/ui/new/ui_core.c new file mode 100644 index 000000000..19906773d --- /dev/null +++ b/openrtx/src/ui/new/ui_core.c @@ -0,0 +1,35 @@ +#include +#include +#include // For NULL + +#include "core/input.h" +#include "ui/ui_screen.h" + +// TODO: Figure this out +#define UI_SCREEN_STACK_MAX 4 + +static UiScreen *screen_stack[UI_SCREEN_STACK_MAX]; +static uint8_t screen_top = 0; + +void ui_push_screen(UiScreen *s) { + if (screen_top < UI_SCREEN_STACK_MAX) { + screen_stack[screen_top++] = s; + } +} + +void ui_pop_screen(void) { + if (screen_top > 0) { + screen_top--; + } +} + +UiScreen *ui_current_screen(void) { + return (screen_top > 0) ? screen_stack[screen_top - 1] : NULL; +} + +bool ui_build_event_from_kbd(const kbd_msg_t *kbd, UiEvent *ev) { + if (!kbd) return false; + ev->type = 1; + ev->key = kbd->keys; + return true; +} \ No newline at end of file diff --git a/openrtx/src/ui/new/ui_menu.c b/openrtx/src/ui/new/ui_menu.c new file mode 100644 index 000000000..ef178b69e --- /dev/null +++ b/openrtx/src/ui/new/ui_menu.c @@ -0,0 +1,258 @@ +#include +#include +#include // for NULL + +#include "ui/ui_menu.h" +#include "ui/ui_screen.h" +#include "interfaces/keyboard.h" //TODO: For keycodes + +/* menu_draw includes */ +#include "hwconfig.h" +#include "core/graphics.h" + +/* How many rows we plan to show at once; for scrolling logic */ +#define MENU_VISIBLE_ROWS 6 + +typedef struct { + const MenuItem *menu; // pointer to current folder node + uint8_t pos; // selected index within menu->children + uint8_t first; // index of first visible child +} MenuFrame; + +//TODO: Determine size at runtime, for now use a safe fixed bound +#define MENU_MAX_DEPTH 8 + +typedef struct { + MenuFrame stack[MENU_MAX_DEPTH]; + uint8_t depth; + bool dirty; + // TODO: more, e.g. edit mode? +} MenuState; + +static void menu_tick(UiScreen *self, const UiEvent *ev); +static void menu_draw(UiScreen *self); + +/* Global menu state + screen object */ +static MenuState g_menu_state; + +static UiScreen g_menu_screen = { + .tick = menu_tick, + .draw = menu_draw, + .ctx = &g_menu_state, +}; + +/* Root menu defined in ui_menu_tree.c */ +extern const MenuItem g_root_menu; + +/* --- Internal helpers --- */ +static void menu_reset_to_root(MenuState *st) +{ + st->depth = 1; + + st->stack[0].menu = &g_root_menu; + st->stack[0].pos = 0; + st->stack[0].first = 0; + + st->dirty = true; +} + +/* Clamp first so that pos is always visible in [first, first + MENU_VISIBLE_ROWS - 1] */ +static void menu_ensure_visible(MenuFrame *frame) +{ + if (frame->pos < frame->first) { + frame->first = frame->pos; + } else if (frame->pos >= (uint8_t)(frame->first + MENU_VISIBLE_ROWS)) { + frame->first = frame->pos - (MENU_VISIBLE_ROWS - 1); + } +} + +/* --- Public API --- */ +UiScreen *ui_get_menu_screen(void) +{ + return &g_menu_screen; +} + +void ui_menu_open_root(void) +{ + MenuState *st = &g_menu_state; + menu_reset_to_root(st); + ui_push_screen(&g_menu_screen); +} + +/* --- UiScreen implementation --- */ + +static void menu_tick(UiScreen *self, const UiEvent *ev) +{ + MenuState *st = (MenuState *)self->ctx; + + /* If somehow unitialized, reset to root */ + if (st->depth == 0) { + menu_reset_to_root(st); + } + + /* No event? nothing to do this tick */ + if (!ev) { + return; + } + + /* For now we treat all events as key events */ + enum key key = ev->key; + + MenuFrame *frame = &st->stack[st->depth - 1]; + const MenuItem *menu = frame->menu; + + if (!menu || menu->child_count == 0 || !menu->children) { + /* Empty folder: Back should just pop, anything else ignored */ + if (key == KEY_ESC) { + if (st->depth > 1) { + st->depth--; + st->dirty = true; + } else { + ui_pop_screen(); + } + } + return; + } + + switch (key) { + case KEY_UP: + { + if (frame->pos > 0) { + frame->pos--; + } else { + frame->pos = menu->child_count - 1; // wrap to last + } + menu_ensure_visible(frame); + st->dirty = true; + break; + } + + case KEY_DOWN: + { + if (frame->pos + 1 < menu->child_count) { + frame->pos++; + } else { + frame->pos = 0; // wrap to first + } + menu_ensure_visible(frame); + st->dirty = true; + break; + } + + case KEY_ENTER: + { + const MenuItem *item = &menu->children[frame->pos]; + + if (item->kind == MENU_NODE_FOLDER && item->child_count > 0 && item->children != NULL) { + /* Descend into child folder */ + if (st->depth < MENU_MAX_DEPTH) { + MenuFrame *child = &st->stack[st->depth]; + child->menu = item; + child->pos = 0; + child->first = 0; + st->depth++; + st->dirty = true; + } + } else { + /* Action or Value node: invoke callback if present */ + if (item->cb) { + (void)item->cb(MENU_CMD_SELECT, 0, item->user); + } + } + break; + } + + case KEY_ESC: + { + if (st->depth > 1) { + /* Go up one folder */ + st->depth--; + st->dirty = true; + } else { + /* At root: leave menu back to previous screen */ + ui_pop_screen(); + } + break; + } + + default: + { + /* Ignore other keys for now */ + break; + } + } +} + +static void menu_draw(UiScreen *self) +{ + // consts borrowed from existing code for large displays + // TODO: copy over `layout_t` and helpers + const uint16_t text_v_offset = 1; + const uint16_t status_v_pad = 2; + const uint16_t top_h = 16; + const uint16_t top_pad = 4; + const uint16_t line1_h = 20; + const uint16_t small_line_v_pad = 2; + const uint16_t horizontal_pad = 4; + const uint16_t top_pos_y = top_h - status_v_pad - text_v_offset; + const uint16_t line1_pos_y = top_h + top_pad + line1_h - small_line_v_pad - text_v_offset; + const uint16_t menu_h = 16; + const point_t top_pos = {horizontal_pad, top_pos_y}; + const point_t line1_pos = {horizontal_pad, line1_pos_y}; + const fontSize_t top_font = FONT_SIZE_8PT; + const fontSize_t menu_font = FONT_SIZE_8PT; + const color_t color_white = {255, 255, 255, 255}; + const color_t color_black = {0, 0, 0, 255}; + + MenuState *st = (MenuState *)self->ctx; + + if(!st || !st->dirty) + { + return; + } + + if (st->depth == 0) + { + return; + } + + MenuFrame *frame = &st->stack[st->depth - 1]; + const MenuItem *menu = frame->menu; + + if (!menu) + { + return; + } + + gfx_clearScreen(); + + // Header + const char *title = menu->label ? menu->label : ""; + gfx_print(top_pos, top_font, TEXT_ALIGN_CENTER, color_white, title); + + // Menu items + point_t pos = line1_pos; + int first = frame->first; + int count = menu->child_count; + + for(int idx = first; idx < first + MENU_VISIBLE_ROWS && idx < count; ++idx) + { + const MenuItem *item = &menu->children[idx]; + const char *label = item->label ? item->label : ""; + + color_t text_color = color_white; + + if (idx == frame->pos) + { + text_color = color_black; + point_t rect_pos = {0, pos.y - menu_h + 3}; + gfx_drawRect(rect_pos, CONFIG_SCREEN_WIDTH, menu_h, color_white, true); + // announceMenuItemIfNeeded() + } + gfx_print(pos, menu_font, TEXT_ALIGN_LEFT, text_color, label); + pos.y += menu_h; + } + + gfx_render(); + st->dirty = false; +} \ No newline at end of file diff --git a/openrtx/src/ui/new/ui_menu_tree.c b/openrtx/src/ui/new/ui_menu_tree.c new file mode 100644 index 000000000..bdb4465ed --- /dev/null +++ b/openrtx/src/ui/new/ui_menu_tree.c @@ -0,0 +1,49 @@ +#include +#include + +#include "ui/ui_menu.h" + +/* include menu items here, gate with compile flags */ + +static const MenuItem display_children[] = { + { MENU_NODE_FOLDER, "Brightness", 0, NULL, NULL, NULL }, + { MENU_NODE_FOLDER, "Timer", 0, NULL, NULL, NULL }, + { MENU_NODE_FOLDER, "Battery Icon", 0, NULL, NULL, NULL }, +}; + +static const MenuItem display_settings_menu = { + .kind = MENU_NODE_FOLDER, + .label = "Display", + .child_count = sizeof(display_children)/sizeof(display_children[0]), + .children = display_children, + .cb = NULL, + .user = NULL, +}; + +static const MenuItem settings_children[] = { + display_settings_menu, +}; + +static const MenuItem root_menu_children[] = { + { MENU_NODE_FOLDER, "Banks", 0, NULL, NULL, NULL }, + { MENU_NODE_FOLDER, "Channels", 0, NULL, NULL, NULL }, + { MENU_NODE_FOLDER, "Contacts", 0, NULL, NULL, NULL }, + { MENU_NODE_FOLDER, "GPS", 0, NULL, NULL, NULL }, + { MENU_NODE_FOLDER, "Settings", + .child_count = sizeof(settings_children)/sizeof(settings_children[0]), + .children = settings_children, + .cb = NULL, + .user = NULL, + }, + { MENU_NODE_FOLDER, "Info", 0, NULL, NULL, NULL }, + { MENU_NODE_FOLDER, "About", 0, NULL, NULL, NULL }, +}; + +const MenuItem g_root_menu = { + .kind = MENU_NODE_FOLDER, + .label = "Menu", + .child_count = sizeof(root_menu_children)/sizeof(display_children[0]), + .children = root_menu_children, + .cb = NULL, + .user = NULL, +}; \ No newline at end of file diff --git a/platform/targets/linux/emulator/sdl_engine.c b/platform/targets/linux/emulator/sdl_engine.c index 850686f75..ca53c7d26 100644 --- a/platform/targets/linux/emulator/sdl_engine.c +++ b/platform/targets/linux/emulator/sdl_engine.c @@ -97,6 +97,10 @@ static bool sdk_key_code_to_key(SDL_Keycode sym, keyboard_t *key) case SDLK_m: *key = KEY_MONI; return true; + + case SDLK_b: + *key = KEY_F3; + return true; case SDLK_PAGEUP: *key = KNOB_LEFT; From 3e44bc33935919f41a93c57beffc147122cb5485 Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Thu, 6 Nov 2025 14:38:42 -0800 Subject: [PATCH 03/27] implement bool settings --- openrtx/include/ui/ui_menu.h | 12 +++ openrtx/include/ui/ui_menu_dsl.h | 7 ++ openrtx/src/ui/new/ui_menu.c | 139 +++++++++++++++++++++++++------ 3 files changed, 134 insertions(+), 24 deletions(-) create mode 100644 openrtx/include/ui/ui_menu_dsl.h diff --git a/openrtx/include/ui/ui_menu.h b/openrtx/include/ui/ui_menu.h index d00688d2d..fb3815877 100644 --- a/openrtx/include/ui/ui_menu.h +++ b/openrtx/include/ui/ui_menu.h @@ -41,4 +41,16 @@ void ui_menu_open_root(void); /* Root menu entry point (global tree) */ extern const MenuItem g_root_menu; +typedef enum { + MENU_VAL_BOOL, + // TODO: more later: MENU_VAL_I32, MENU_VAL_ENUM, etc. +} MenuValueKind; + +typedef struct { + MenuValueKind kind; + void *ptr; // pointer to the underlying value + + void (*on_change)(void *ptr); // optional side-effect on change +} MenuValueBinding; + #endif \ No newline at end of file diff --git a/openrtx/include/ui/ui_menu_dsl.h b/openrtx/include/ui/ui_menu_dsl.h new file mode 100644 index 000000000..1f0cb6190 --- /dev/null +++ b/openrtx/include/ui/ui_menu_dsl.h @@ -0,0 +1,7 @@ +#ifndef UI_MENU_DSL_H +#define UI_MENU_DSL_H + +#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0])) + +/* TODO: Introduce declarative sort of DSL */ +#endif \ No newline at end of file diff --git a/openrtx/src/ui/new/ui_menu.c b/openrtx/src/ui/new/ui_menu.c index ef178b69e..813eef36e 100644 --- a/openrtx/src/ui/new/ui_menu.c +++ b/openrtx/src/ui/new/ui_menu.c @@ -1,5 +1,6 @@ -#include #include +#include +#include #include // for NULL #include "ui/ui_menu.h" @@ -13,6 +14,47 @@ /* How many rows we plan to show at once; for scrolling logic */ #define MENU_VISIBLE_ROWS 6 +static void menu_value_adjust(MenuValueBinding *b, bool inc) +{ + (void)inc; // unused for bool; kept for future symmetry + + if (!b || !b->ptr) { + return; + } + + switch (b->kind) { + case MENU_VAL_BOOL: { + bool *p = (bool *)b->ptr; + *p = !*p; + break; + } + default: + break; + } + + if (b->on_change) { + b->on_change(b->ptr); + } +} + +static void menu_value_format(const MenuValueBinding *b, char *buf, size_t n) +{ + if (!b || !b->ptr || !buf || n == 0) { + return; + } + + switch (b->kind) { + case MENU_VAL_BOOL: { + bool v = *(bool *)b->ptr; + sniprintf(buf, n, "%s", v ? "On" : "Off"); + break; + } + default: + sniprintf(buf, n, "?"); + break; + } +} + typedef struct { const MenuItem *menu; // pointer to current folder node uint8_t pos; // selected index within menu->children @@ -26,7 +68,7 @@ typedef struct { MenuFrame stack[MENU_MAX_DEPTH]; uint8_t depth; bool dirty; - // TODO: more, e.g. edit mode? + bool edit; } MenuState; static void menu_tick(UiScreen *self, const UiEvent *ev); @@ -54,6 +96,7 @@ static void menu_reset_to_root(MenuState *st) st->stack[0].first = 0; st->dirty = true; + st->edit = false; } /* Clamp first so that pos is always visible in [first, first + MENU_VISIBLE_ROWS - 1] */ @@ -85,16 +128,15 @@ static void menu_tick(UiScreen *self, const UiEvent *ev) { MenuState *st = (MenuState *)self->ctx; + if (!st || !ev) { + return; + } + /* If somehow unitialized, reset to root */ if (st->depth == 0) { menu_reset_to_root(st); } - /* No event? nothing to do this tick */ - if (!ev) { - return; - } - /* For now we treat all events as key events */ enum key key = ev->key; @@ -114,9 +156,12 @@ static void menu_tick(UiScreen *self, const UiEvent *ev) return; } - switch (key) { - case KEY_UP: + /* Handle keypress */ + if (!st->edit) { + /* --- NORMAL NAVIGATION MODE --- */ + switch (key) { + case KEY_UP: if (frame->pos > 0) { frame->pos--; } else { @@ -125,10 +170,8 @@ static void menu_tick(UiScreen *self, const UiEvent *ev) menu_ensure_visible(frame); st->dirty = true; break; - } case KEY_DOWN: - { if (frame->pos + 1 < menu->child_count) { frame->pos++; } else { @@ -137,12 +180,11 @@ static void menu_tick(UiScreen *self, const UiEvent *ev) menu_ensure_visible(frame); st->dirty = true; break; - } - case KEY_ENTER: - { + case KEY_ENTER: { const MenuItem *item = &menu->children[frame->pos]; + /* Enter non-empty folder*/ if (item->kind == MENU_NODE_FOLDER && item->child_count > 0 && item->children != NULL) { /* Descend into child folder */ if (st->depth < MENU_MAX_DEPTH) { @@ -153,17 +195,20 @@ static void menu_tick(UiScreen *self, const UiEvent *ev) st->depth++; st->dirty = true; } - } else { - /* Action or Value node: invoke callback if present */ - if (item->cb) { - (void)item->cb(MENU_CMD_SELECT, 0, item->user); - } + } + /* Activate edit mode on value node */ + else if (item->kind == MENU_NODE_VALUE && item->user) { + st->edit = true; + st->dirty = true; + } + else if (item->cb) { + // TODO: Action node with callback?? + (void)item->cb(MENU_CMD_SELECT, 0, item->user); } break; } - + case KEY_ESC: - { if (st->depth > 1) { /* Go up one folder */ st->depth--; @@ -173,11 +218,41 @@ static void menu_tick(UiScreen *self, const UiEvent *ev) ui_pop_screen(); } break; + + default: + break; } + } else { + /* ---------- EDIT MODE ----------*/ + const MenuItem *item = &menu->children[frame->pos]; + MenuValueBinding *b = (item->kind == MENU_NODE_VALUE && item->user) + ? (MenuValueBinding *)item->user + : NULL; + switch (key) + { + case KEY_UP: + case KEY_DOWN: + if (b) { + bool inc = (key == KEY_UP); + menu_value_adjust(b, inc); + st->dirty = true; + } + break; + + case KEY_ENTER: + // Accept edit; leave edit mode + st->edit = false; + st->dirty = true; + break; + + case KEY_ESC: + // For now: also leave edit mode + // TODO: restore previous value + st->edit = false; + st->dirty = true; + break; default: - { - /* Ignore other keys for now */ break; } } @@ -242,14 +317,30 @@ static void menu_draw(UiScreen *self) color_t text_color = color_white; + /* Highlight if selected */ if (idx == frame->pos) { text_color = color_black; + bool full_rect = true; + /* If in edit mode, draw a hollow rectangle */ + if(st->edit) + { + text_color = color_white; + full_rect = false; + } point_t rect_pos = {0, pos.y - menu_h + 3}; - gfx_drawRect(rect_pos, CONFIG_SCREEN_WIDTH, menu_h, color_white, true); + gfx_drawRect(rect_pos, CONFIG_SCREEN_WIDTH, menu_h, color_white, full_rect); // announceMenuItemIfNeeded() } gfx_print(pos, menu_font, TEXT_ALIGN_LEFT, text_color, label); + + /* Value on the right if this is a VALUE node */ + if (item->kind == MENU_NODE_VALUE && item->user) { + // TODO: Evaluate value buffer size + char buf[16]; + menu_value_format((const MenuValueBinding *)item->user, buf, sizeof buf); + gfx_print(pos, menu_font, TEXT_ALIGN_RIGHT, text_color, buf); + } pos.y += menu_h; } From 06ed8601f8314631cf5be4da852082c3d8271228 Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Thu, 6 Nov 2025 15:20:21 -0800 Subject: [PATCH 04/27] alter menu API; wire sample setting --- meson.build | 3 +- openrtx/include/ui/ui_menu.h | 12 ++--- openrtx/src/ui/new/ui_menu.c | 6 +-- openrtx/src/ui/new/ui_menu_tree.c | 53 ++++++++++---------- openrtx/src/ui/new/ui_settings_display.c | 62 ++++++++++++++++++++++++ 5 files changed, 98 insertions(+), 38 deletions(-) create mode 100644 openrtx/src/ui/new/ui_settings_display.c diff --git a/meson.build b/meson.build index c28d69973..0ef0861c9 100644 --- a/meson.build +++ b/meson.build @@ -81,6 +81,7 @@ ui_src_default = ['openrtx/src/ui/default/ui.c', 'openrtx/src/ui/new/ui_core.c', 'openrtx/src/ui/new/ui_compat.c', 'openrtx/src/ui/new/ui_menu.c', + 'openrtx/src/ui/new/ui_settings_display.c', 'openrtx/src/ui/new/ui_menu_tree.c', 'openrtx/src/ui/default/ui_strings.c'] @@ -360,7 +361,7 @@ linux_l_args = ['-lm', '-lreadline'] if build_machine.system() == 'darwin' linux_l_args += '-Wl,-dead_strip' else - linux_l_args += '-Wl,--gc-sections' + #linux_l_args += '-Wl,--gc-sections' endif # Add AddressSanitizer if required diff --git a/openrtx/include/ui/ui_menu.h b/openrtx/include/ui/ui_menu.h index fb3815877..d9b766cac 100644 --- a/openrtx/include/ui/ui_menu.h +++ b/openrtx/include/ui/ui_menu.h @@ -24,12 +24,12 @@ typedef int (*MenuCb)(MenuCmd cmd, int arg, void *user); typedef struct MenuItem MenuItem; struct MenuItem { - MenuNodeKind kind; - const char *label; - uint8_t child_count; - const MenuItem *children; // only for FOLDER - MenuCb cb; // for values/actions - void *user; + MenuNodeKind kind; + const char *label; + uint8_t child_count; + const MenuItem *const *children; // only for FOLDER + MenuCb cb; // for values/actions + void *user; }; /* The menu screen itself */ diff --git a/openrtx/src/ui/new/ui_menu.c b/openrtx/src/ui/new/ui_menu.c index 813eef36e..5fea7e97c 100644 --- a/openrtx/src/ui/new/ui_menu.c +++ b/openrtx/src/ui/new/ui_menu.c @@ -182,7 +182,7 @@ static void menu_tick(UiScreen *self, const UiEvent *ev) break; case KEY_ENTER: { - const MenuItem *item = &menu->children[frame->pos]; + const MenuItem *item = menu->children[frame->pos]; /* Enter non-empty folder*/ if (item->kind == MENU_NODE_FOLDER && item->child_count > 0 && item->children != NULL) { @@ -224,7 +224,7 @@ static void menu_tick(UiScreen *self, const UiEvent *ev) } } else { /* ---------- EDIT MODE ----------*/ - const MenuItem *item = &menu->children[frame->pos]; + const MenuItem *item = menu->children[frame->pos]; MenuValueBinding *b = (item->kind == MENU_NODE_VALUE && item->user) ? (MenuValueBinding *)item->user : NULL; @@ -312,7 +312,7 @@ static void menu_draw(UiScreen *self) for(int idx = first; idx < first + MENU_VISIBLE_ROWS && idx < count; ++idx) { - const MenuItem *item = &menu->children[idx]; + const MenuItem *item = menu->children[idx]; const char *label = item->label ? item->label : ""; color_t text_color = color_white; diff --git a/openrtx/src/ui/new/ui_menu_tree.c b/openrtx/src/ui/new/ui_menu_tree.c index bdb4465ed..935943b8b 100644 --- a/openrtx/src/ui/new/ui_menu_tree.c +++ b/openrtx/src/ui/new/ui_menu_tree.c @@ -2,47 +2,44 @@ #include #include "ui/ui_menu.h" +#include "ui/ui_menu_dsl.h" /* include menu items here, gate with compile flags */ -static const MenuItem display_children[] = { - { MENU_NODE_FOLDER, "Brightness", 0, NULL, NULL, NULL }, - { MENU_NODE_FOLDER, "Timer", 0, NULL, NULL, NULL }, - { MENU_NODE_FOLDER, "Battery Icon", 0, NULL, NULL, NULL }, -}; +/* from ui_settings_display.c */ +extern const MenuItem g_display_settings_menu; -static const MenuItem display_settings_menu = { - .kind = MENU_NODE_FOLDER, - .label = "Display", - .child_count = sizeof(display_children)/sizeof(display_children[0]), - .children = display_children, - .cb = NULL, - .user = NULL, +static const MenuItem *const settings_children[] = { + &g_display_settings_menu, }; -static const MenuItem settings_children[] = { - display_settings_menu, +static const MenuItem m_banks = { MENU_NODE_FOLDER, "Banks", 0, NULL, NULL, NULL }; +static const MenuItem m_channels = { MENU_NODE_FOLDER, "Channels", 0, NULL, NULL, NULL }; +static const MenuItem m_contacts = { MENU_NODE_FOLDER, "Contacts", 0, NULL, NULL, NULL }; +static const MenuItem m_gps = { MENU_NODE_FOLDER, "GPS", 0, NULL, NULL, NULL }; +static const MenuItem m_settings = { MENU_NODE_FOLDER, "Settings", + .child_count = ARRAY_LEN(settings_children), + .children = settings_children, + .cb = NULL, + .user = NULL, }; +static const MenuItem m_info = { MENU_NODE_FOLDER, "Info", 0, NULL, NULL, NULL }; +static const MenuItem m_about = { MENU_NODE_FOLDER, "About", 0, NULL, NULL, NULL }; -static const MenuItem root_menu_children[] = { - { MENU_NODE_FOLDER, "Banks", 0, NULL, NULL, NULL }, - { MENU_NODE_FOLDER, "Channels", 0, NULL, NULL, NULL }, - { MENU_NODE_FOLDER, "Contacts", 0, NULL, NULL, NULL }, - { MENU_NODE_FOLDER, "GPS", 0, NULL, NULL, NULL }, - { MENU_NODE_FOLDER, "Settings", - .child_count = sizeof(settings_children)/sizeof(settings_children[0]), - .children = settings_children, - .cb = NULL, - .user = NULL, - }, - { MENU_NODE_FOLDER, "Info", 0, NULL, NULL, NULL }, - { MENU_NODE_FOLDER, "About", 0, NULL, NULL, NULL }, +static const MenuItem *const root_menu_children[] = { + &m_banks, + &m_channels, + &m_contacts, + &m_gps, + &m_settings, + &m_info, + &m_about, }; const MenuItem g_root_menu = { .kind = MENU_NODE_FOLDER, .label = "Menu", - .child_count = sizeof(root_menu_children)/sizeof(display_children[0]), + .child_count = ARRAY_LEN(root_menu_children), .children = root_menu_children, .cb = NULL, .user = NULL, diff --git a/openrtx/src/ui/new/ui_settings_display.c b/openrtx/src/ui/new/ui_settings_display.c new file mode 100644 index 000000000..d36ccb0c5 --- /dev/null +++ b/openrtx/src/ui/new/ui_settings_display.c @@ -0,0 +1,62 @@ +#include +#include + +#include "ui/ui_menu.h" +#include "ui/ui_menu_dsl.h" +#include "core/state.h" + +static void battery_icon_on_change(void *ptr) +{ + (void)ptr; + /* TODO: do something */ +} + +static MenuValueBinding battery_icon_binding = { + .kind = MENU_VAL_BOOL, + .ptr = &state.settings.showBatteryIcon, + .on_change = battery_icon_on_change, +}; + +/* Leaf nodes */ +static const MenuItem m_brightness = { + .kind = MENU_NODE_VALUE, + .label = "Brightness", + .child_count = 0, + .children = NULL, + .cb = NULL, + .user = NULL, +}; + +static const MenuItem m_timer = { + .kind = MENU_NODE_VALUE, + .label = "Timer", + .child_count = 0, + .children = NULL, + .cb = NULL, + .user = NULL, +}; + +static const MenuItem m_battery_icon = { + .kind = MENU_NODE_VALUE, + .label = "Battery Icon", + .child_count = 0, + .children = NULL, + .cb = NULL, + .user = &battery_icon_binding, +}; + +/* Pointer array of children for this folder */ +static const MenuItem *const display_children[] = { + &m_brightness, + &m_timer, + &m_battery_icon, +}; + +const MenuItem g_display_settings_menu = { + .kind = MENU_NODE_FOLDER, + .label = "Display", + .child_count = ARRAY_LEN(display_children), + .children = display_children, + .cb = NULL, + .user = NULL, +}; From a2c533bdd7a1da71e80632c8b2de4668312b095d Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Thu, 6 Nov 2025 16:32:14 -0800 Subject: [PATCH 05/27] implement u8 dial --- openrtx/include/ui/ui_menu.h | 10 ++- openrtx/src/ui/new/ui_menu.c | 82 +++++++++++++++++++++++- openrtx/src/ui/new/ui_settings_display.c | 62 ++++++++++++++---- 3 files changed, 138 insertions(+), 16 deletions(-) diff --git a/openrtx/include/ui/ui_menu.h b/openrtx/include/ui/ui_menu.h index d9b766cac..60d64e997 100644 --- a/openrtx/include/ui/ui_menu.h +++ b/openrtx/include/ui/ui_menu.h @@ -43,13 +43,21 @@ extern const MenuItem g_root_menu; typedef enum { MENU_VAL_BOOL, - // TODO: more later: MENU_VAL_I32, MENU_VAL_ENUM, etc. + MENU_VAL_I32, + MENU_VAL_U8, + MENU_VAL_ENUM, } MenuValueKind; typedef struct { MenuValueKind kind; void *ptr; // pointer to the underlying value + union { + struct { int32_t min, max, step; bool wrap; } i32; + struct { uint8_t min, max, step; bool wrap; } u8; + struct { const char *const *names; uint8_t count; } enm; + } u; + void (*on_change)(void *ptr); // optional side-effect on change } MenuValueBinding; diff --git a/openrtx/src/ui/new/ui_menu.c b/openrtx/src/ui/new/ui_menu.c index 5fea7e97c..8652ba7a0 100644 --- a/openrtx/src/ui/new/ui_menu.c +++ b/openrtx/src/ui/new/ui_menu.c @@ -16,8 +16,6 @@ static void menu_value_adjust(MenuValueBinding *b, bool inc) { - (void)inc; // unused for bool; kept for future symmetry - if (!b || !b->ptr) { return; } @@ -28,6 +26,81 @@ static void menu_value_adjust(MenuValueBinding *b, bool inc) *p = !*p; break; } + case MENU_VAL_U8: { + uint8_t *p = (uint8_t *)b->ptr; + uint8_t v = *p; + + uint8_t min = b->u.u8.min; + uint8_t max = b->u.u8.max; + uint8_t step = b->u.u8.step; + bool wrap = b->u.u8.wrap; + + /* TODO: Evaluate responsibility + // Sanity check: if min > max, swap + if (min > max) { + uint8_t tmp = min; + min = max; + max = tmp; + } + + if (step == 0) { + step = 1; + } + */ + + // Normalize any out-of-range value + if (v < min) { + v = min; + } else if (v > max) { + v = max; + } + + if (inc) { + // Increment + if (v >= max) { + if (wrap) { + v = min; + } else { + v = max; + } + } else { + // Do arithmetic in a wider type to avoid overflow + unsigned int tmp = (unsigned int)v + (unsigned int)step; + if (tmp > max) { + if (wrap) { + v = min; + } else { + v = max; + } + } else { + v = (uint8_t)tmp; + } + } + } else { + // Decrement + if (v <= min) { + if (wrap) { + v = max; + } else { + v = min; + } + } else { + int tmp = (int)v - (int)step; + if (tmp < (int)min) { + if (wrap) { + v = max; + } else { + v = min; + } + } else { + v = (uint8_t)tmp; + } + } + } + + *p = v; + break; + } default: break; } @@ -49,6 +122,11 @@ static void menu_value_format(const MenuValueBinding *b, char *buf, size_t n) sniprintf(buf, n, "%s", v ? "On" : "Off"); break; } + case MENU_VAL_U8: { + uint8_t v = *(uint8_t *)b->ptr; + sniprintf(buf, n, "%u", v); + break; + } default: sniprintf(buf, n, "?"); break; diff --git a/openrtx/src/ui/new/ui_settings_display.c b/openrtx/src/ui/new/ui_settings_display.c index d36ccb0c5..f6188f528 100644 --- a/openrtx/src/ui/new/ui_settings_display.c +++ b/openrtx/src/ui/new/ui_settings_display.c @@ -4,36 +4,58 @@ #include "ui/ui_menu.h" #include "ui/ui_menu_dsl.h" #include "core/state.h" +#include "interfaces/display.h" -static void battery_icon_on_change(void *ptr) -{ - (void)ptr; - /* TODO: do something */ +#ifdef CONFIG_SCREEN_BRIGHTNESS +static void brightness_on_change(void *ptr) { + uint8_t *p = (uint8_t *)ptr; + display_setBacklightLevel(*p); } -static MenuValueBinding battery_icon_binding = { - .kind = MENU_VAL_BOOL, - .ptr = &state.settings.showBatteryIcon, - .on_change = battery_icon_on_change, +static MenuValueBinding brightness_binding = { + .kind = MENU_VAL_U8, + .ptr = &state.settings.brightness, + .u.u8 = { .min = 0, .max = 100, .step = 5, .wrap = false }, + .on_change = brightness_on_change, }; -/* Leaf nodes */ static const MenuItem m_brightness = { .kind = MENU_NODE_VALUE, .label = "Brightness", .child_count = 0, .children = NULL, .cb = NULL, - .user = NULL, + .user = &brightness_binding, }; +#endif -static const MenuItem m_timer = { +#ifdef CONFIG_SCREEN_CONTRAST +static void contrast_on_change(void *ptr) { + uint8_t *p = (uint8_t *)ptr; + display_setContrast(*p); +} + +static MenuValueBinding contrast_binding = { + .kind = MENU_VAL_U8, + .ptr = &state.settings.contrast, + .u.u8 = { .min = 0, .max = 255, .step = 4, .wrap = false }, + .on_change = contrast_on_change, +}; + +static const MenuItem m_contrast = { .kind = MENU_NODE_VALUE, - .label = "Timer", + .label = "Contrast", .child_count = 0, .children = NULL, .cb = NULL, - .user = NULL, + .user = &contrast_binding, +}; +#endif + +static MenuValueBinding battery_icon_binding = { + .kind = MENU_VAL_BOOL, + .ptr = &state.settings.showBatteryIcon, + .on_change = NULL, }; static const MenuItem m_battery_icon = { @@ -45,9 +67,23 @@ static const MenuItem m_battery_icon = { .user = &battery_icon_binding, }; +static const MenuItem m_timer = { + .kind = MENU_NODE_VALUE, + .label = "Timer", + .child_count = 0, + .children = NULL, + .cb = NULL, + .user = NULL, +}; + /* Pointer array of children for this folder */ static const MenuItem *const display_children[] = { +#ifdef CONFIG_SCREEN_BRIGHTNESS &m_brightness, +#endif +#ifdef CONFIG_SCREEN_CONTRAST + &m_contrast, +#endif &m_timer, &m_battery_icon, }; From 09736c84113e826849c6142ddb368b5ceceae23c Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Thu, 6 Nov 2025 17:00:00 -0800 Subject: [PATCH 06/27] add gps settings page --- meson.build | 5 +- openrtx/src/ui/new/ui_menu_tree.c | 2 + openrtx/src/ui/new/ui_settings_gps.c | 74 ++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 openrtx/src/ui/new/ui_settings_gps.c diff --git a/meson.build b/meson.build index 0ef0861c9..55e37fad6 100644 --- a/meson.build +++ b/meson.build @@ -78,12 +78,13 @@ openrtx_inc = ['openrtx/include', 'platform'] ui_src_default = ['openrtx/src/ui/default/ui.c', 'openrtx/src/ui/default/ui_main.c', 'openrtx/src/ui/default/ui_menu.c', + 'openrtx/src/ui/default/ui_strings.c', 'openrtx/src/ui/new/ui_core.c', 'openrtx/src/ui/new/ui_compat.c', 'openrtx/src/ui/new/ui_menu.c', 'openrtx/src/ui/new/ui_settings_display.c', - 'openrtx/src/ui/new/ui_menu_tree.c', - 'openrtx/src/ui/default/ui_strings.c'] + 'openrtx/src/ui/new/ui_settings_gps.c', + 'openrtx/src/ui/new/ui_menu_tree.c'] ui_src_module17 = ['openrtx/src/ui/module17/ui.c', 'openrtx/src/ui/module17/ui_main.c', diff --git a/openrtx/src/ui/new/ui_menu_tree.c b/openrtx/src/ui/new/ui_menu_tree.c index 935943b8b..d5f060a01 100644 --- a/openrtx/src/ui/new/ui_menu_tree.c +++ b/openrtx/src/ui/new/ui_menu_tree.c @@ -8,9 +8,11 @@ /* from ui_settings_display.c */ extern const MenuItem g_display_settings_menu; +extern const MenuItem g_gps_settings_menu; static const MenuItem *const settings_children[] = { &g_display_settings_menu, + &g_gps_settings_menu, }; static const MenuItem m_banks = { MENU_NODE_FOLDER, "Banks", 0, NULL, NULL, NULL }; diff --git a/openrtx/src/ui/new/ui_settings_gps.c b/openrtx/src/ui/new/ui_settings_gps.c new file mode 100644 index 000000000..a1dd79de6 --- /dev/null +++ b/openrtx/src/ui/new/ui_settings_gps.c @@ -0,0 +1,74 @@ +#include +#include + +#include "ui/ui_menu.h" +#include "ui/ui_menu_dsl.h" +#include "core/state.h" + +#ifdef CONFIG_GPS +static MenuValueBinding gps_en_binding = { + .kind = MENU_VAL_BOOL, + .ptr = &state.settings.gps_enabled, + .on_change = NULL, +}; + +static const MenuItem m_gps_en = { + .kind = MENU_NODE_VALUE, + .label = "GPS Enabled", + .child_count = 0, + .children = NULL, + .cb = NULL, + .user = &gps_en_binding, +}; +#endif + +#ifdef CONFIG_RTC +static MenuValueBinding gps_set_time_binding = { + .kind = MENU_VAL_BOOL, + .ptr = &state.settings.gpsSetTime, + .on_change = NULL, +}; + +static const MenuItem m_gps_set_time = { + .kind = MENU_NODE_VALUE, + .label = "GPS Set Time", + .child_count = 0, + .children = NULL, + .cb = NULL, + .user = &gps_set_time_binding, +}; + +static MenuValueBinding utc_timezone_binding = { + .kind = MENU_VAL_I32, //TODO: Not implemented yet + .ptr = &state.settings.gpsSetTime, + .on_change = NULL, +}; + +static const MenuItem m_utc_timezone = { + .kind = MENU_NODE_VALUE, + .label = "UTC Timezone", + .child_count = 0, + .children = NULL, + .cb = NULL, + .user = &utc_timezone_binding, +}; +#endif + +static const MenuItem *const gps_children[] = { +#ifdef CONFIG_GPS + &m_gps_en, +#endif +#ifdef CONFIG_RTC + &m_gps_set_time, + &m_utc_timezone, +#endif +}; + +const MenuItem g_gps_settings_menu = { + .kind = MENU_NODE_FOLDER, + .label = "GPS", + .child_count = ARRAY_LEN(gps_children), + .children = gps_children, + .cb = NULL, + .user = NULL, +}; \ No newline at end of file From 31b7895fadde2cf22852c1aa0ed4d1a2a30e688d Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Thu, 6 Nov 2025 17:20:12 -0800 Subject: [PATCH 07/27] add accessibility settings stub --- meson.build | 1 + openrtx/src/ui/new/ui_menu_tree.c | 4 +- .../src/ui/new/ui_settings_accessibility.c | 72 +++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 openrtx/src/ui/new/ui_settings_accessibility.c diff --git a/meson.build b/meson.build index 55e37fad6..dbfea5e7f 100644 --- a/meson.build +++ b/meson.build @@ -82,6 +82,7 @@ ui_src_default = ['openrtx/src/ui/default/ui.c', 'openrtx/src/ui/new/ui_core.c', 'openrtx/src/ui/new/ui_compat.c', 'openrtx/src/ui/new/ui_menu.c', + 'openrtx/src/ui/new/ui_settings_accessibility.c', 'openrtx/src/ui/new/ui_settings_display.c', 'openrtx/src/ui/new/ui_settings_gps.c', 'openrtx/src/ui/new/ui_menu_tree.c'] diff --git a/openrtx/src/ui/new/ui_menu_tree.c b/openrtx/src/ui/new/ui_menu_tree.c index d5f060a01..fddb905db 100644 --- a/openrtx/src/ui/new/ui_menu_tree.c +++ b/openrtx/src/ui/new/ui_menu_tree.c @@ -6,13 +6,15 @@ /* include menu items here, gate with compile flags */ -/* from ui_settings_display.c */ +/* from ui_settings_*.c */ extern const MenuItem g_display_settings_menu; extern const MenuItem g_gps_settings_menu; +extern const MenuItem g_accessibility_settings_menu; static const MenuItem *const settings_children[] = { &g_display_settings_menu, &g_gps_settings_menu, + &g_accessibility_settings_menu, }; static const MenuItem m_banks = { MENU_NODE_FOLDER, "Banks", 0, NULL, NULL, NULL }; diff --git a/openrtx/src/ui/new/ui_settings_accessibility.c b/openrtx/src/ui/new/ui_settings_accessibility.c new file mode 100644 index 000000000..1c632cf46 --- /dev/null +++ b/openrtx/src/ui/new/ui_settings_accessibility.c @@ -0,0 +1,72 @@ +#include + +#include "ui/ui_menu.h" +#include "ui/ui_menu_dsl.h" + +/* TODO: Replace fake values with real ones */ + +static bool macrolatch = false; +static bool phonetic = false; +static bool voice = false; + +/* End TODO:--------------------------------*/ + +static MenuValueBinding macro_latch_binding = { + .kind = MENU_VAL_BOOL, + .ptr = ¯olatch, + .on_change = NULL, +}; + +static const MenuItem m_macro_latch = { + .kind = MENU_NODE_VALUE, + .label = "Macro Latch", + .child_count = 0, + .children = NULL, + .cb = NULL, + .user = ¯o_latch_binding, +}; + +static MenuValueBinding voice_binding = { + .kind = MENU_VAL_BOOL, + .ptr = &voice, + .on_change = NULL, +}; + +static const MenuItem m_voice = { + .kind = MENU_NODE_VALUE, + .label = "Voice", + .child_count = 0, + .children = NULL, + .cb = NULL, + .user = &voice_binding, +}; + +static MenuValueBinding phonetic_binding = { + .kind = MENU_VAL_BOOL, + .ptr = &phonetic, + .on_change = NULL, +}; + +static const MenuItem m_phonetic = { + .kind = MENU_NODE_VALUE, + .label = "Phonetic", + .child_count = 0, + .children = NULL, + .cb = NULL, + .user = &phonetic_binding, +}; + +static const MenuItem *const accessibility_children[] = { + &m_macro_latch, + &m_voice, + &m_phonetic, +}; + +const MenuItem g_accessibility_settings_menu = { + .kind = MENU_NODE_FOLDER, + .label = "Accessibility", + .child_count = ARRAY_LEN(accessibility_children), + .children = accessibility_children, + .cb = NULL, + .user = NULL, +}; \ No newline at end of file From 07b40a08c889425aaaf4370f423bec867111981e Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Fri, 7 Nov 2025 14:15:35 -0800 Subject: [PATCH 08/27] flesh out menu item callbacks, test with FM CTCSS --- meson.build | 3 +- openrtx/include/ui/ui_menu.h | 73 ++++++--- openrtx/src/ui/new/ui_menu.c | 97 +++++++----- openrtx/src/ui/new/ui_menu_tree.c | 20 ++- .../src/ui/new/ui_settings_accessibility.c | 26 ++-- openrtx/src/ui/new/ui_settings_display.c | 41 ++--- openrtx/src/ui/new/ui_settings_fm.c | 141 ++++++++++++++++++ openrtx/src/ui/new/ui_settings_gps.c | 26 ++-- 8 files changed, 322 insertions(+), 105 deletions(-) create mode 100644 openrtx/src/ui/new/ui_settings_fm.c diff --git a/meson.build b/meson.build index dbfea5e7f..a6a51469e 100644 --- a/meson.build +++ b/meson.build @@ -82,9 +82,10 @@ ui_src_default = ['openrtx/src/ui/default/ui.c', 'openrtx/src/ui/new/ui_core.c', 'openrtx/src/ui/new/ui_compat.c', 'openrtx/src/ui/new/ui_menu.c', - 'openrtx/src/ui/new/ui_settings_accessibility.c', 'openrtx/src/ui/new/ui_settings_display.c', 'openrtx/src/ui/new/ui_settings_gps.c', + 'openrtx/src/ui/new/ui_settings_fm.c', + 'openrtx/src/ui/new/ui_settings_accessibility.c', 'openrtx/src/ui/new/ui_menu_tree.c'] ui_src_module17 = ['openrtx/src/ui/module17/ui.c', diff --git a/openrtx/include/ui/ui_menu.h b/openrtx/include/ui/ui_menu.h index 60d64e997..9690058b7 100644 --- a/openrtx/include/ui/ui_menu.h +++ b/openrtx/include/ui/ui_menu.h @@ -2,44 +2,50 @@ #ifndef UI_MENU_H #define UI_MENU_H +#include #include + #include "ui/ui_screen.h" typedef enum { MENU_NODE_FOLDER, MENU_NODE_ACTION, MENU_NODE_VALUE, + MENU_NODE_UNIMPLEMENTED, // Marker which shows up in the UI } MenuNodeKind; typedef enum { - MENU_CMD_DRAW, + /// @brief Ask the callback to format the right-hand value text. + /// @param arg `MenuDrawValueArgs*` + MENU_CMD_DRAW_VALUE = 0, + + /// @brief Called when user presses ENTER on this item while *not* in edit mode. + /// @param arg `const UiEvent*` (can be NULL if you don't need it) MENU_CMD_SELECT, - MENU_CMD_GETSTATE, + + /// @brief Called when we're about to enter the generic edit mode on this item. + /// @param arg `NULL` MENU_CMD_EDIT_BEGIN, - MENU_CMD_EDIT_APPLY, - MENU_CMD_EDIT_CANCEL, -} MenuCmd; -typedef int (*MenuCb)(MenuCmd cmd, int arg, void *user); + /// @brief A key press while this item is in edit mode. + /// @param arg `const UiEvent*` + MENU_CMD_EDIT_KEY, -typedef struct MenuItem MenuItem; -struct MenuItem { - MenuNodeKind kind; - const char *label; - uint8_t child_count; - const MenuItem *const *children; // only for FOLDER - MenuCb cb; // for values/actions - void *user; -}; + /// @brief User cancelled edit (ESC). + /// @param arg `NULL` + MENU_CMD_EDIT_CANCEL, -/* The menu screen itself */ -UiScreen *ui_get_menu_screen(void); + /// @brief User accepted edit (ESC). + /// @param arg `NULL` + MENU_CMD_EDIT_APPLY, +} MenuCmd; -/* Helper to reset state to root and push the menu screen on the stack */ -void ui_menu_open_root(void); +typedef struct { + char *buf; + size_t buf_len; +} MenuDrawValueArgs; -/* Root menu entry point (global tree) */ -extern const MenuItem g_root_menu; +typedef int (*MenuCb)(MenuCmd cmd, void *arg, void *cb_ctx); typedef enum { MENU_VAL_BOOL, @@ -51,14 +57,35 @@ typedef enum { typedef struct { MenuValueKind kind; void *ptr; // pointer to the underlying value + void (*on_change)(void *ptr); // optional side-effect on change union { struct { int32_t min, max, step; bool wrap; } i32; struct { uint8_t min, max, step; bool wrap; } u8; struct { const char *const *names; uint8_t count; } enm; } u; - - void (*on_change)(void *ptr); // optional side-effect on change } MenuValueBinding; +typedef struct MenuItem MenuItem; +struct MenuItem { + MenuNodeKind kind; + const char *label; + uint8_t child_count; + const MenuItem *const *children; // only for FOLDER + + const MenuValueBinding *binding; // generic value (if any) + + MenuCb cb; // optional custom callback + void *cb_ctx; // context pointer for cb +}; + +/* The menu screen itself */ +UiScreen *ui_get_menu_screen(void); + +/* Helper to reset state to root and push the menu screen on the stack */ +void ui_menu_open_root(void); + +/* Root menu entry point (global tree) */ +extern const MenuItem g_root_menu; + #endif \ No newline at end of file diff --git a/openrtx/src/ui/new/ui_menu.c b/openrtx/src/ui/new/ui_menu.c index 8652ba7a0..16620c4b7 100644 --- a/openrtx/src/ui/new/ui_menu.c +++ b/openrtx/src/ui/new/ui_menu.c @@ -2,6 +2,7 @@ #include #include #include // for NULL +#include #include "ui/ui_menu.h" #include "ui/ui_screen.h" @@ -14,7 +15,7 @@ /* How many rows we plan to show at once; for scrolling logic */ #define MENU_VISIBLE_ROWS 6 -static void menu_value_adjust(MenuValueBinding *b, bool inc) +static void menu_value_adjust(const MenuValueBinding *b, bool inc) { if (!b || !b->ptr) { return; @@ -122,9 +123,14 @@ static void menu_value_format(const MenuValueBinding *b, char *buf, size_t n) sniprintf(buf, n, "%s", v ? "On" : "Off"); break; } + case MENU_VAL_I32: { + int32_t v = *(int32_t *)b->ptr; + sniprintf(buf, n, "%"PRIi32, v); + break; + } case MENU_VAL_U8: { uint8_t v = *(uint8_t *)b->ptr; - sniprintf(buf, n, "%u", v); + sniprintf(buf, n, "%"PRIu8, v); break; } default: @@ -275,13 +281,14 @@ static void menu_tick(UiScreen *self, const UiEvent *ev) } } /* Activate edit mode on value node */ - else if (item->kind == MENU_NODE_VALUE && item->user) { - st->edit = true; + else if (item->kind == MENU_NODE_VALUE && item->binding) { + st->edit = true; st->dirty = true; } - else if (item->cb) { - // TODO: Action node with callback?? - (void)item->cb(MENU_CMD_SELECT, 0, item->user); + else if (item->kind == MENU_NODE_VALUE && item->cb) { + item->cb(MENU_CMD_EDIT_BEGIN, NULL, item->cb_ctx); + st->edit = true; + st->dirty = true; } break; } @@ -303,36 +310,40 @@ static void menu_tick(UiScreen *self, const UiEvent *ev) } else { /* ---------- EDIT MODE ----------*/ const MenuItem *item = menu->children[frame->pos]; - MenuValueBinding *b = (item->kind == MENU_NODE_VALUE && item->user) - ? (MenuValueBinding *)item->user - : NULL; - switch (key) - { - case KEY_UP: - case KEY_DOWN: - if (b) { - bool inc = (key == KEY_UP); - menu_value_adjust(b, inc); + + if (item->binding) { + /* Generic value */ + bool inc = (key == KEY_UP); + bool dec = (key == KEY_DOWN); + + if ((item->kind == MENU_NODE_VALUE) && (inc || dec)) { + menu_value_adjust(item->binding, inc); st->dirty = true; + return; } - break; - - case KEY_ENTER: - // Accept edit; leave edit mode - st->edit = false; + } + + if (item->cb) { + /* Let callback handle keys in edit mode */ + item->cb(MENU_CMD_EDIT_KEY, (void *)ev, item->cb_ctx); st->dirty = true; - break; + } - case KEY_ESC: - // For now: also leave edit mode - // TODO: restore previous value + if (key == KEY_ESC) { + if (item->cb) { + item->cb(MENU_CMD_EDIT_CANCEL, NULL, item->cb_ctx); + } + st->edit = false; + st->dirty = true; + } else if (key == KEY_ENTER) { + if (item->cb) { + item->cb(MENU_CMD_EDIT_APPLY, NULL, item->cb_ctx); + } st->edit = false; st->dirty = true; - break; - - default: - break; } + + return; } } @@ -413,11 +424,29 @@ static void menu_draw(UiScreen *self) gfx_print(pos, menu_font, TEXT_ALIGN_LEFT, text_color, label); /* Value on the right if this is a VALUE node */ - if (item->kind == MENU_NODE_VALUE && item->user) { + if (item->kind == MENU_NODE_VALUE) { // TODO: Evaluate value buffer size - char buf[16]; - menu_value_format((const MenuValueBinding *)item->user, buf, sizeof buf); - gfx_print(pos, menu_font, TEXT_ALIGN_RIGHT, text_color, buf); + char buf[16] = {0}; + + if (item->binding) { + /* Generic value binding */ + menu_value_format((const MenuValueBinding *)item->binding, buf, sizeof buf); + } else if (item->cb) { + /* Custom value: let the callback format it */ + MenuDrawValueArgs args = { + .buf = buf, + .buf_len = sizeof buf + }; + item->cb(MENU_CMD_DRAW_VALUE, &args, item->cb_ctx); + } + if (buf[0] != '\0') { + gfx_print(pos, menu_font, TEXT_ALIGN_RIGHT, text_color, buf); + } + } + + /* Show if node is unimplemented */ + if (item->kind == MENU_NODE_UNIMPLEMENTED) { + gfx_print(pos, menu_font, TEXT_ALIGN_RIGHT, text_color, ":("); } pos.y += menu_h; } diff --git a/openrtx/src/ui/new/ui_menu_tree.c b/openrtx/src/ui/new/ui_menu_tree.c index fddb905db..ca94c3c8a 100644 --- a/openrtx/src/ui/new/ui_menu_tree.c +++ b/openrtx/src/ui/new/ui_menu_tree.c @@ -9,26 +9,29 @@ /* from ui_settings_*.c */ extern const MenuItem g_display_settings_menu; extern const MenuItem g_gps_settings_menu; +extern const MenuItem g_fm_settings_menu; extern const MenuItem g_accessibility_settings_menu; static const MenuItem *const settings_children[] = { &g_display_settings_menu, &g_gps_settings_menu, + &g_fm_settings_menu, &g_accessibility_settings_menu, }; -static const MenuItem m_banks = { MENU_NODE_FOLDER, "Banks", 0, NULL, NULL, NULL }; -static const MenuItem m_channels = { MENU_NODE_FOLDER, "Channels", 0, NULL, NULL, NULL }; -static const MenuItem m_contacts = { MENU_NODE_FOLDER, "Contacts", 0, NULL, NULL, NULL }; -static const MenuItem m_gps = { MENU_NODE_FOLDER, "GPS", 0, NULL, NULL, NULL }; +static const MenuItem m_banks = { MENU_NODE_UNIMPLEMENTED, "Banks", 0, NULL, NULL, NULL, NULL }; +static const MenuItem m_channels = { MENU_NODE_UNIMPLEMENTED, "Channels", 0, NULL, NULL, NULL, NULL }; +static const MenuItem m_contacts = { MENU_NODE_UNIMPLEMENTED, "Contacts", 0, NULL, NULL, NULL, NULL }; +static const MenuItem m_gps = { MENU_NODE_UNIMPLEMENTED, "GPS", 0, NULL, NULL, NULL, NULL }; static const MenuItem m_settings = { MENU_NODE_FOLDER, "Settings", .child_count = ARRAY_LEN(settings_children), .children = settings_children, + .binding = NULL, .cb = NULL, - .user = NULL, + .cb_ctx = NULL, }; -static const MenuItem m_info = { MENU_NODE_FOLDER, "Info", 0, NULL, NULL, NULL }; -static const MenuItem m_about = { MENU_NODE_FOLDER, "About", 0, NULL, NULL, NULL }; +static const MenuItem m_info = { MENU_NODE_UNIMPLEMENTED, "Info", 0, NULL, NULL, NULL, NULL }; +static const MenuItem m_about = { MENU_NODE_UNIMPLEMENTED, "About", 0, NULL, NULL, NULL, NULL }; static const MenuItem *const root_menu_children[] = { &m_banks, @@ -45,6 +48,7 @@ const MenuItem g_root_menu = { .label = "Menu", .child_count = ARRAY_LEN(root_menu_children), .children = root_menu_children, + .binding = NULL, .cb = NULL, - .user = NULL, + .cb_ctx = NULL, }; \ No newline at end of file diff --git a/openrtx/src/ui/new/ui_settings_accessibility.c b/openrtx/src/ui/new/ui_settings_accessibility.c index 1c632cf46..713ddb889 100644 --- a/openrtx/src/ui/new/ui_settings_accessibility.c +++ b/openrtx/src/ui/new/ui_settings_accessibility.c @@ -22,8 +22,9 @@ static const MenuItem m_macro_latch = { .label = "Macro Latch", .child_count = 0, .children = NULL, + .binding = ¯o_latch_binding, .cb = NULL, - .user = ¯o_latch_binding, + .cb_ctx = NULL, }; static MenuValueBinding voice_binding = { @@ -33,12 +34,13 @@ static MenuValueBinding voice_binding = { }; static const MenuItem m_voice = { - .kind = MENU_NODE_VALUE, + .kind = MENU_NODE_UNIMPLEMENTED, .label = "Voice", .child_count = 0, .children = NULL, + .binding = &voice_binding, .cb = NULL, - .user = &voice_binding, + .cb_ctx = NULL, }; static MenuValueBinding phonetic_binding = { @@ -48,12 +50,13 @@ static MenuValueBinding phonetic_binding = { }; static const MenuItem m_phonetic = { - .kind = MENU_NODE_VALUE, + .kind = MENU_NODE_UNIMPLEMENTED, .label = "Phonetic", .child_count = 0, .children = NULL, + .binding = &phonetic_binding, .cb = NULL, - .user = &phonetic_binding, + .cb_ctx = NULL, }; static const MenuItem *const accessibility_children[] = { @@ -63,10 +66,11 @@ static const MenuItem *const accessibility_children[] = { }; const MenuItem g_accessibility_settings_menu = { - .kind = MENU_NODE_FOLDER, - .label = "Accessibility", - .child_count = ARRAY_LEN(accessibility_children), - .children = accessibility_children, - .cb = NULL, - .user = NULL, + .kind = MENU_NODE_FOLDER, + .label = "Accessibility", + .child_count = ARRAY_LEN(accessibility_children), + .children = accessibility_children, + .binding = NULL, + .cb = NULL, + .cb_ctx = NULL, }; \ No newline at end of file diff --git a/openrtx/src/ui/new/ui_settings_display.c b/openrtx/src/ui/new/ui_settings_display.c index f6188f528..df23421f2 100644 --- a/openrtx/src/ui/new/ui_settings_display.c +++ b/openrtx/src/ui/new/ui_settings_display.c @@ -24,8 +24,9 @@ static const MenuItem m_brightness = { .label = "Brightness", .child_count = 0, .children = NULL, + .binding = &brightness_binding, .cb = NULL, - .user = &brightness_binding, + .cb_ctx = NULL, }; #endif @@ -47,11 +48,22 @@ static const MenuItem m_contrast = { .label = "Contrast", .child_count = 0, .children = NULL, + .binding = &contrast_binding, .cb = NULL, - .user = &contrast_binding, + .cb_ctx = NULL, }; #endif +static const MenuItem m_timer = { + .kind = MENU_NODE_UNIMPLEMENTED, + .label = "Timer", + .child_count = 0, + .children = NULL, + .binding = NULL, + .cb = NULL, + .cb_ctx = NULL, +}; + static MenuValueBinding battery_icon_binding = { .kind = MENU_VAL_BOOL, .ptr = &state.settings.showBatteryIcon, @@ -63,17 +75,9 @@ static const MenuItem m_battery_icon = { .label = "Battery Icon", .child_count = 0, .children = NULL, + .binding = &battery_icon_binding, .cb = NULL, - .user = &battery_icon_binding, -}; - -static const MenuItem m_timer = { - .kind = MENU_NODE_VALUE, - .label = "Timer", - .child_count = 0, - .children = NULL, - .cb = NULL, - .user = NULL, + .cb_ctx = NULL, }; /* Pointer array of children for this folder */ @@ -89,10 +93,11 @@ static const MenuItem *const display_children[] = { }; const MenuItem g_display_settings_menu = { - .kind = MENU_NODE_FOLDER, - .label = "Display", - .child_count = ARRAY_LEN(display_children), - .children = display_children, - .cb = NULL, - .user = NULL, + .kind = MENU_NODE_FOLDER, + .label = "Display", + .child_count = ARRAY_LEN(display_children), + .children = display_children, + .binding = NULL, + .cb = NULL, + .cb_ctx = NULL, }; diff --git a/openrtx/src/ui/new/ui_settings_fm.c b/openrtx/src/ui/new/ui_settings_fm.c new file mode 100644 index 000000000..033bb6586 --- /dev/null +++ b/openrtx/src/ui/new/ui_settings_fm.c @@ -0,0 +1,141 @@ +#include +#include +#include + +#include "ui/ui_menu.h" +#include "ui/ui_menu_dsl.h" + +#include "core/state.h" + +static const MenuItem m_fm_ctcss_tone = { + .kind = MENU_NODE_UNIMPLEMENTED, + .label = "CTCSS Tone", + .child_count = 0, + .children = NULL, + .binding = NULL, + .cb = NULL, + .cb_ctx = NULL, +}; + +typedef enum { + CTC_EN_NONE = 0, + CTC_EN_BOTH, + CTC_EN_ENCODE, + CTC_EN_DECODE, +} CtcEnMode; + +static CtcEnMode fm_ctcss_mode_from_state() +{ + bool tx = state.channel.fm.txToneEn; + bool rx = state.channel.fm.rxToneEn; + + if (!tx && !rx) return CTC_EN_NONE; + if (tx && rx) return CTC_EN_BOTH; + if (tx && !rx) return CTC_EN_ENCODE; + if (!tx && rx) return CTC_EN_DECODE; + + return CTC_EN_NONE; +} + +static void fm_ctcss_mode_to_state(CtcEnMode m) +{ + switch (m) { + case CTC_EN_NONE: + state.channel.fm.txToneEn = false; + state.channel.fm.rxToneEn = false; + break; + case CTC_EN_BOTH: + state.channel.fm.txToneEn = true; + state.channel.fm.rxToneEn = true; + break; + case CTC_EN_ENCODE: + state.channel.fm.txToneEn = true; + state.channel.fm.rxToneEn = false; + break; + case CTC_EN_DECODE: + state.channel.fm.txToneEn = false; + state.channel.fm.rxToneEn = true; + break; + } +} + +static int fm_ctcss_en_cb(MenuCmd cmd, void *arg, void *cb_ctx) +{ + (void)cb_ctx; + + switch (cmd) { + case MENU_CMD_DRAW_VALUE: { + MenuDrawValueArgs *a = (MenuDrawValueArgs *)arg; + CtcEnMode m = fm_ctcss_mode_from_state(); + const char *txt = "ERR"; + + switch (m) { + case CTC_EN_NONE: txt = "None"; break; + case CTC_EN_BOTH: txt = "Both"; break; + case CTC_EN_ENCODE: txt = "Encode"; break; + case CTC_EN_DECODE: txt = "Decode"; break; + } + + sniprintf(a->buf, a->buf_len, "%s", txt); + return 1; + } + + case MENU_CMD_EDIT_BEGIN: + // Nothing to do + return 0; + + case MENU_CMD_EDIT_KEY: { + const UiEvent *ev = (const UiEvent *)arg; + CtcEnMode m = fm_ctcss_mode_from_state(); + + if (!ev) return 0; + + if (ev->key == KEY_UP || ev->key == KEY_DOWN) { + bool inc = ev->key == KEY_UP; + m = (CtcEnMode)((m + (inc ? 1 : -1)) % 4); + fm_ctcss_mode_to_state(m); + return 1; + } + return 0; + } + + case MENU_CMD_EDIT_CANCEL: + // Optional: re-sync from some saved copy or just leave it + return 0; + + case MENU_CMD_EDIT_APPLY: + // Nothing to do, we already wrote into state + return 0; + + case MENU_CMD_SELECT: + /* TODO: Reconsider this one, doesn't make much sense. */ + return 0; + } + + return 0; +} + +static const MenuItem m_fm_ctcss_en = { + .kind = MENU_NODE_VALUE, + .label = "CTCSS En.", + .child_count = 0, + .children = NULL, + .binding = NULL, + .cb = fm_ctcss_en_cb, + .cb_ctx = NULL, +}; + +static const MenuItem *const fm_children[] = { + &m_fm_ctcss_tone, + &m_fm_ctcss_en, +}; + +const MenuItem g_fm_settings_menu = { + .kind = MENU_NODE_FOLDER, + .label = "FM", + .child_count = ARRAY_LEN(fm_children), + .children = fm_children, + .binding = NULL, + .cb = NULL, + .cb_ctx = NULL, +}; \ No newline at end of file diff --git a/openrtx/src/ui/new/ui_settings_gps.c b/openrtx/src/ui/new/ui_settings_gps.c index a1dd79de6..bba4df01e 100644 --- a/openrtx/src/ui/new/ui_settings_gps.c +++ b/openrtx/src/ui/new/ui_settings_gps.c @@ -17,8 +17,9 @@ static const MenuItem m_gps_en = { .label = "GPS Enabled", .child_count = 0, .children = NULL, + .binding = &gps_en_binding, .cb = NULL, - .user = &gps_en_binding, + .cb_ctx = NULL, }; #endif @@ -34,23 +35,27 @@ static const MenuItem m_gps_set_time = { .label = "GPS Set Time", .child_count = 0, .children = NULL, + .binding = &gps_set_time_binding, .cb = NULL, - .user = &gps_set_time_binding, + .cb_ctx = NULL, }; +/* static MenuValueBinding utc_timezone_binding = { .kind = MENU_VAL_I32, //TODO: Not implemented yet .ptr = &state.settings.gpsSetTime, .on_change = NULL, }; +*/ static const MenuItem m_utc_timezone = { - .kind = MENU_NODE_VALUE, + .kind = MENU_NODE_UNIMPLEMENTED, .label = "UTC Timezone", .child_count = 0, .children = NULL, + .binding = NULL, .cb = NULL, - .user = &utc_timezone_binding, + .cb_ctx = NULL, }; #endif @@ -65,10 +70,11 @@ static const MenuItem *const gps_children[] = { }; const MenuItem g_gps_settings_menu = { - .kind = MENU_NODE_FOLDER, - .label = "GPS", - .child_count = ARRAY_LEN(gps_children), - .children = gps_children, - .cb = NULL, - .user = NULL, + .kind = MENU_NODE_FOLDER, + .label = "GPS", + .child_count = ARRAY_LEN(gps_children), + .children = gps_children, + .binding = NULL, + .cb = NULL, + .cb_ctx = NULL, }; \ No newline at end of file From cc2eaf34422f6a4897da67da5aa8d9a10718e54c Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Sat, 8 Nov 2025 13:53:57 -0800 Subject: [PATCH 09/27] introduce menu declaration macros --- openrtx/include/ui/ui_menu_dsl.h | 51 ++++++++++++++++- openrtx/src/ui/new/ui_menu_tree.c | 23 +++----- .../src/ui/new/ui_settings_accessibility.c | 22 ++------ openrtx/src/ui/new/ui_settings_display.c | 56 ++++--------------- openrtx/src/ui/new/ui_settings_fm.c | 33 ++--------- openrtx/src/ui/new/ui_settings_gps.c | 44 +++------------ 6 files changed, 86 insertions(+), 143 deletions(-) diff --git a/openrtx/include/ui/ui_menu_dsl.h b/openrtx/include/ui/ui_menu_dsl.h index 1f0cb6190..32498f35b 100644 --- a/openrtx/include/ui/ui_menu_dsl.h +++ b/openrtx/include/ui/ui_menu_dsl.h @@ -3,5 +3,52 @@ #define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0])) -/* TODO: Introduce declarative sort of DSL */ -#endif \ No newline at end of file +/// @brief Simple value item driven entirely by a MenuValueBinding +#define MENU_ITEM_VALUE_BINDING(_label, _binding_ptr) \ + { \ + .kind = MENU_NODE_VALUE, \ + .label = (_label), \ + .child_count = 0, \ + .children = NULL, \ + .binding = (_binding_ptr), \ + .cb = NULL, \ + .cb_ctx = NULL, \ + } + +/// @brief Value item that is fully custom via callback +#define MENU_ITEM_VALUE_CB(_label, _cb, _ctx) \ + { \ + .kind = MENU_NODE_VALUE, \ + .label = (_label), \ + .child_count = 0, \ + .children = NULL, \ + .binding = NULL, \ + .cb = (_cb), \ + .cb_ctx = (_ctx), \ + } + +/// @brief Folder whose children were declared as a separate array +#define MENU_FOLDER_FROM_CHILDREN(_label, _children_array) \ + { \ + .kind = MENU_NODE_FOLDER, \ + .label = (_label), \ + .child_count = ARRAY_LEN(_children_array), \ + .children = (_children_array), \ + .binding = NULL, \ + .cb = NULL, \ + .cb_ctx = NULL, \ + } + +/// @brief Placeholder unimplemented item +#define MENU_ITEM_UNIMPLEMENTED(_label) \ + { \ + .kind = MENU_NODE_UNIMPLEMENTED, \ + .label = (_label), \ + .child_count = 0, \ + .children = NULL, \ + .binding = NULL, \ + .cb = NULL, \ + .cb_ctx = NULL, \ + } + +#endif diff --git a/openrtx/src/ui/new/ui_menu_tree.c b/openrtx/src/ui/new/ui_menu_tree.c index ca94c3c8a..0b6cd10e4 100644 --- a/openrtx/src/ui/new/ui_menu_tree.c +++ b/openrtx/src/ui/new/ui_menu_tree.c @@ -19,10 +19,10 @@ static const MenuItem *const settings_children[] = { &g_accessibility_settings_menu, }; -static const MenuItem m_banks = { MENU_NODE_UNIMPLEMENTED, "Banks", 0, NULL, NULL, NULL, NULL }; -static const MenuItem m_channels = { MENU_NODE_UNIMPLEMENTED, "Channels", 0, NULL, NULL, NULL, NULL }; -static const MenuItem m_contacts = { MENU_NODE_UNIMPLEMENTED, "Contacts", 0, NULL, NULL, NULL, NULL }; -static const MenuItem m_gps = { MENU_NODE_UNIMPLEMENTED, "GPS", 0, NULL, NULL, NULL, NULL }; +static const MenuItem m_banks = MENU_ITEM_UNIMPLEMENTED("Banks"); +static const MenuItem m_channels = MENU_ITEM_UNIMPLEMENTED("Channels"); +static const MenuItem m_contacts = MENU_ITEM_UNIMPLEMENTED("Contacts"); +static const MenuItem m_gps = MENU_ITEM_UNIMPLEMENTED("GPS"); static const MenuItem m_settings = { MENU_NODE_FOLDER, "Settings", .child_count = ARRAY_LEN(settings_children), .children = settings_children, @@ -30,8 +30,8 @@ static const MenuItem m_settings = { MENU_NODE_FOLDER, "Settings", .cb = NULL, .cb_ctx = NULL, }; -static const MenuItem m_info = { MENU_NODE_UNIMPLEMENTED, "Info", 0, NULL, NULL, NULL, NULL }; -static const MenuItem m_about = { MENU_NODE_UNIMPLEMENTED, "About", 0, NULL, NULL, NULL, NULL }; +static const MenuItem m_info = MENU_ITEM_UNIMPLEMENTED("Info"); +static const MenuItem m_about = MENU_ITEM_UNIMPLEMENTED("About"); static const MenuItem *const root_menu_children[] = { &m_banks, @@ -43,12 +43,5 @@ static const MenuItem *const root_menu_children[] = { &m_about, }; -const MenuItem g_root_menu = { - .kind = MENU_NODE_FOLDER, - .label = "Menu", - .child_count = ARRAY_LEN(root_menu_children), - .children = root_menu_children, - .binding = NULL, - .cb = NULL, - .cb_ctx = NULL, -}; \ No newline at end of file +const MenuItem g_root_menu = + MENU_FOLDER_FROM_CHILDREN("Menu", root_menu_children); diff --git a/openrtx/src/ui/new/ui_settings_accessibility.c b/openrtx/src/ui/new/ui_settings_accessibility.c index 713ddb889..07da7d0cf 100644 --- a/openrtx/src/ui/new/ui_settings_accessibility.c +++ b/openrtx/src/ui/new/ui_settings_accessibility.c @@ -17,15 +17,8 @@ static MenuValueBinding macro_latch_binding = { .on_change = NULL, }; -static const MenuItem m_macro_latch = { - .kind = MENU_NODE_VALUE, - .label = "Macro Latch", - .child_count = 0, - .children = NULL, - .binding = ¯o_latch_binding, - .cb = NULL, - .cb_ctx = NULL, -}; +static const MenuItem m_macro_latch = + MENU_ITEM_VALUE_BINDING("Macro Latch", ¯o_latch_binding); static MenuValueBinding voice_binding = { .kind = MENU_VAL_BOOL, @@ -65,12 +58,5 @@ static const MenuItem *const accessibility_children[] = { &m_phonetic, }; -const MenuItem g_accessibility_settings_menu = { - .kind = MENU_NODE_FOLDER, - .label = "Accessibility", - .child_count = ARRAY_LEN(accessibility_children), - .children = accessibility_children, - .binding = NULL, - .cb = NULL, - .cb_ctx = NULL, -}; \ No newline at end of file +const MenuItem g_accessibility_settings_menu = + MENU_FOLDER_FROM_CHILDREN("Accessibility", accessibility_children); diff --git a/openrtx/src/ui/new/ui_settings_display.c b/openrtx/src/ui/new/ui_settings_display.c index df23421f2..7c275fbdf 100644 --- a/openrtx/src/ui/new/ui_settings_display.c +++ b/openrtx/src/ui/new/ui_settings_display.c @@ -19,15 +19,9 @@ static MenuValueBinding brightness_binding = { .on_change = brightness_on_change, }; -static const MenuItem m_brightness = { - .kind = MENU_NODE_VALUE, - .label = "Brightness", - .child_count = 0, - .children = NULL, - .binding = &brightness_binding, - .cb = NULL, - .cb_ctx = NULL, -}; +static const MenuItem m_brightness = + MENU_ITEM_VALUE_BINDING("Brightness", &brightness_binding); + #endif #ifdef CONFIG_SCREEN_CONTRAST @@ -43,26 +37,12 @@ static MenuValueBinding contrast_binding = { .on_change = contrast_on_change, }; -static const MenuItem m_contrast = { - .kind = MENU_NODE_VALUE, - .label = "Contrast", - .child_count = 0, - .children = NULL, - .binding = &contrast_binding, - .cb = NULL, - .cb_ctx = NULL, -}; +static const MenuItem m_contrast = + MENU_ITEM_VALUE_BINDING("Contrast", &contrast_binding); #endif -static const MenuItem m_timer = { - .kind = MENU_NODE_UNIMPLEMENTED, - .label = "Timer", - .child_count = 0, - .children = NULL, - .binding = NULL, - .cb = NULL, - .cb_ctx = NULL, -}; +static const MenuItem m_timer = + MENU_ITEM_UNIMPLEMENTED("Timer"); static MenuValueBinding battery_icon_binding = { .kind = MENU_VAL_BOOL, @@ -70,15 +50,8 @@ static MenuValueBinding battery_icon_binding = { .on_change = NULL, }; -static const MenuItem m_battery_icon = { - .kind = MENU_NODE_VALUE, - .label = "Battery Icon", - .child_count = 0, - .children = NULL, - .binding = &battery_icon_binding, - .cb = NULL, - .cb_ctx = NULL, -}; +static const MenuItem m_battery_icon = + MENU_ITEM_VALUE_BINDING("Battery Icon", &battery_icon_binding); /* Pointer array of children for this folder */ static const MenuItem *const display_children[] = { @@ -92,12 +65,5 @@ static const MenuItem *const display_children[] = { &m_battery_icon, }; -const MenuItem g_display_settings_menu = { - .kind = MENU_NODE_FOLDER, - .label = "Display", - .child_count = ARRAY_LEN(display_children), - .children = display_children, - .binding = NULL, - .cb = NULL, - .cb_ctx = NULL, -}; +const MenuItem g_display_settings_menu = + MENU_FOLDER_FROM_CHILDREN("Display", display_children); diff --git a/openrtx/src/ui/new/ui_settings_fm.c b/openrtx/src/ui/new/ui_settings_fm.c index 033bb6586..7367f450a 100644 --- a/openrtx/src/ui/new/ui_settings_fm.c +++ b/openrtx/src/ui/new/ui_settings_fm.c @@ -7,15 +7,8 @@ #include "core/state.h" -static const MenuItem m_fm_ctcss_tone = { - .kind = MENU_NODE_UNIMPLEMENTED, - .label = "CTCSS Tone", - .child_count = 0, - .children = NULL, - .binding = NULL, - .cb = NULL, - .cb_ctx = NULL, -}; +static const MenuItem m_fm_ctcss_tone = + MENU_ITEM_UNIMPLEMENTED("CTCSS Tone"); typedef enum { CTC_EN_NONE = 0, @@ -115,27 +108,13 @@ static int fm_ctcss_en_cb(MenuCmd cmd, void *arg, void *cb_ctx) return 0; } -static const MenuItem m_fm_ctcss_en = { - .kind = MENU_NODE_VALUE, - .label = "CTCSS En.", - .child_count = 0, - .children = NULL, - .binding = NULL, - .cb = fm_ctcss_en_cb, - .cb_ctx = NULL, -}; +static const MenuItem m_fm_ctcss_en = + MENU_ITEM_VALUE_CB("CTCSS En.", fm_ctcss_en_cb, NULL); static const MenuItem *const fm_children[] = { &m_fm_ctcss_tone, &m_fm_ctcss_en, }; -const MenuItem g_fm_settings_menu = { - .kind = MENU_NODE_FOLDER, - .label = "FM", - .child_count = ARRAY_LEN(fm_children), - .children = fm_children, - .binding = NULL, - .cb = NULL, - .cb_ctx = NULL, -}; \ No newline at end of file +const MenuItem g_fm_settings_menu = + MENU_FOLDER_FROM_CHILDREN("FM", fm_children); diff --git a/openrtx/src/ui/new/ui_settings_gps.c b/openrtx/src/ui/new/ui_settings_gps.c index bba4df01e..2bc4f2910 100644 --- a/openrtx/src/ui/new/ui_settings_gps.c +++ b/openrtx/src/ui/new/ui_settings_gps.c @@ -12,15 +12,8 @@ static MenuValueBinding gps_en_binding = { .on_change = NULL, }; -static const MenuItem m_gps_en = { - .kind = MENU_NODE_VALUE, - .label = "GPS Enabled", - .child_count = 0, - .children = NULL, - .binding = &gps_en_binding, - .cb = NULL, - .cb_ctx = NULL, -}; +static const MenuItem m_gps_en = + MENU_ITEM_VALUE_BINDING("GPS Enabled", &gps_en_binding); #endif #ifdef CONFIG_RTC @@ -30,15 +23,8 @@ static MenuValueBinding gps_set_time_binding = { .on_change = NULL, }; -static const MenuItem m_gps_set_time = { - .kind = MENU_NODE_VALUE, - .label = "GPS Set Time", - .child_count = 0, - .children = NULL, - .binding = &gps_set_time_binding, - .cb = NULL, - .cb_ctx = NULL, -}; +static const MenuItem m_gps_set_time = + MENU_ITEM_VALUE_BINDING("GPS Set Time", &gps_set_time_binding); /* static MenuValueBinding utc_timezone_binding = { @@ -48,15 +34,8 @@ static MenuValueBinding utc_timezone_binding = { }; */ -static const MenuItem m_utc_timezone = { - .kind = MENU_NODE_UNIMPLEMENTED, - .label = "UTC Timezone", - .child_count = 0, - .children = NULL, - .binding = NULL, - .cb = NULL, - .cb_ctx = NULL, -}; +static const MenuItem m_utc_timezone = + MENU_ITEM_UNIMPLEMENTED("UTC Timezone"); #endif static const MenuItem *const gps_children[] = { @@ -69,12 +48,5 @@ static const MenuItem *const gps_children[] = { #endif }; -const MenuItem g_gps_settings_menu = { - .kind = MENU_NODE_FOLDER, - .label = "GPS", - .child_count = ARRAY_LEN(gps_children), - .children = gps_children, - .binding = NULL, - .cb = NULL, - .cb_ctx = NULL, -}; \ No newline at end of file +const MenuItem g_gps_settings_menu = + MENU_FOLDER_FROM_CHILDREN("GPS", gps_children); From 3a551e760c7091af6a4244f2e8391b7d276690c7 Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Sun, 9 Nov 2025 10:04:11 -0800 Subject: [PATCH 10/27] draft: text edit widget --- meson.build | 4 +- openrtx/include/core/graphics.h | 7 + openrtx/include/ui/ui_menu.h | 28 +++ openrtx/include/ui/ui_new.h | 11 - openrtx/include/ui/ui_textedit.h | 33 +++ openrtx/src/core/graphics.c | 25 ++ openrtx/src/ui/new/ui_menu.c | 27 +++ openrtx/src/ui/new/ui_menu_tree.c | 2 + openrtx/src/ui/new/ui_settings_m17.c | 59 +++++ openrtx/src/ui/new/ui_textedit.c | 329 +++++++++++++++++++++++++++ 10 files changed, 513 insertions(+), 12 deletions(-) create mode 100644 openrtx/include/ui/ui_textedit.h create mode 100644 openrtx/src/ui/new/ui_settings_m17.c create mode 100644 openrtx/src/ui/new/ui_textedit.c diff --git a/meson.build b/meson.build index a6a51469e..67eeda9b1 100644 --- a/meson.build +++ b/meson.build @@ -82,11 +82,13 @@ ui_src_default = ['openrtx/src/ui/default/ui.c', 'openrtx/src/ui/new/ui_core.c', 'openrtx/src/ui/new/ui_compat.c', 'openrtx/src/ui/new/ui_menu.c', + 'openrtx/src/ui/new/ui_menu_tree.c', 'openrtx/src/ui/new/ui_settings_display.c', 'openrtx/src/ui/new/ui_settings_gps.c', + 'openrtx/src/ui/new/ui_settings_m17.c', 'openrtx/src/ui/new/ui_settings_fm.c', 'openrtx/src/ui/new/ui_settings_accessibility.c', - 'openrtx/src/ui/new/ui_menu_tree.c'] + 'openrtx/src/ui/new/ui_textedit.c'] ui_src_module17 = ['openrtx/src/ui/module17/ui.c', 'openrtx/src/ui/module17/ui_main.c', diff --git a/openrtx/include/core/graphics.h b/openrtx/include/core/graphics.h index 2db280105..a3ae4a636 100644 --- a/openrtx/include/core/graphics.h +++ b/openrtx/include/core/graphics.h @@ -201,6 +201,13 @@ void gfx_drawCircle(point_t start, uint16_t r, color_t color); */ uint8_t gfx_getFontHeight(fontSize_t size); +/** + * Measure text + * @param size: text font size, defined as enum. + * @return text width and height as point_t coordinates + */ +point_t gfx_textSize(fontSize_t size, const char *buf); + /** * Prints text on the screen at the specified coordinates. * Reads text from a given char buffer diff --git a/openrtx/include/ui/ui_menu.h b/openrtx/include/ui/ui_menu.h index 9690058b7..25b8e6b09 100644 --- a/openrtx/include/ui/ui_menu.h +++ b/openrtx/include/ui/ui_menu.h @@ -47,11 +47,34 @@ typedef struct { typedef int (*MenuCb)(MenuCmd cmd, void *arg, void *cb_ctx); +/* type mask values for MENU_VAL_STR */ +#define UI_TYPE_UPPERCASE 0x01 +#define UI_TYPE_LOWERCASE 0x02 +#define UI_TYPE_NUMBER 0x04 +#define UI_TYPE_HEX 0x08 +#define UI_TYPE_TEXT UI_TYPE_LOWERCASE | UI_TYPE_UPPERCASE +#define UI_TYPE_ALL UI_TYPE_LOWERCASE | UI_TYPE_UPPERCASE | UI_TYPE_NUMBER + +typedef enum { + + /// @brief general text entry + UI_STR_PROFILE_TEXT, + + /// @brief callsigns / IDs + UI_STR_PROFILE_CALLSIGN, + + /// @brief PINs, purely digits + UI_STR_PROFILE_NUMERIC, + + /* TODO: Maybe more later? */ +} UiStrProfile; + typedef enum { MENU_VAL_BOOL, MENU_VAL_I32, MENU_VAL_U8, MENU_VAL_ENUM, + MENU_VAL_STR, } MenuValueKind; typedef struct { @@ -63,6 +86,11 @@ typedef struct { struct { int32_t min, max, step; bool wrap; } i32; struct { uint8_t min, max, step; bool wrap; } u8; struct { const char *const *names; uint8_t count; } enm; + struct { + uint8_t max_len; + //uint8_t type_mask; TODO: Use this or `profile`? + UiStrProfile profile; + } str; } u; } MenuValueBinding; diff --git a/openrtx/include/ui/ui_new.h b/openrtx/include/ui/ui_new.h index ddf03548a..6c59f90d6 100644 --- a/openrtx/include/ui/ui_new.h +++ b/openrtx/include/ui/ui_new.h @@ -4,16 +4,5 @@ #include "core/input.h" #include "ui/ui_screen.h" -#define UI_ALIGN_LEFT 0x00 -#define UI_ALIGN_RIGHT 0x01 -#define UI_ALIGN_CENTER 0x02 - -#define UI_TYPE_UPPERCASE 0x01 -#define UI_TYPE_LOWERCASE 0x02 -#define UI_TYPE_NUMBER 0x04 -#define UI_TYPE_HEX 0x08 -#define UI_TYPE_TEXT UI_TYPE_LOWERCASE | UI_TYPE_UPPERCASE -#define UI_TYPE_ALL UI_TYPE_LOWERCASE | UI_TYPE_UPPERCASE | UI_TYPE_NUMBER - bool ui_build_event_from_kbd(const kbd_msg_t *kbd, UiEvent *ev); #endif \ No newline at end of file diff --git a/openrtx/include/ui/ui_textedit.h b/openrtx/include/ui/ui_textedit.h new file mode 100644 index 000000000..a71f5eeb3 --- /dev/null +++ b/openrtx/include/ui/ui_textedit.h @@ -0,0 +1,33 @@ +#ifndef UI_TEXTEDIT_H +#define UI_TEXTEDIT_H + +#include +#include + +#include "ui/ui_screen.h" +#include "ui/ui_menu.h" + +typedef struct { + /// @brief target buffer (existing value on entry) + char *buf; + + /// @brief not including '\0' + uint8_t max_len; + + /// @brief entry type + UiStrProfile profile; + + /// @brief shown as header + const char *title; + + /// @brief ioptional completion callback (called on apply or cancel) + void (*on_done)(bool applied, void *user); + void *user; +} UiTextEditParams; + +#define UI_TEXTEDIT_KEY_TIMEOUT 700 + +/// @brief Push the text editor UiScreen onto the screen stack +void ui_open_textedit(const UiTextEditParams *p); + +#endif diff --git a/openrtx/src/core/graphics.c b/openrtx/src/core/graphics.c index 5d7d2a46d..175191a53 100644 --- a/openrtx/src/core/graphics.c +++ b/openrtx/src/core/graphics.c @@ -439,6 +439,31 @@ uint8_t gfx_getFontHeight(fontSize_t size) return glyph.height; } +point_t gfx_textSize(fontSize_t size, const char *buf) +{ + GFXfont f = fonts[size]; + size_t len = strlen(buf); + uint16_t w = 0; + uint16_t h = 0; + + for (size_t i = 0; i < len; ++i) + { + char c = buf[i]; + + if (c == '\n' || c == '\r') break; + if (c < f.first || c > f.last) continue; + + GFXglyph glyph = f.glyph[c - f.first]; + w += glyph.xAdvance; + if (glyph.height > h) { + h = glyph.height; + } + } + + point_t sz = { w, h }; + return sz; +} + point_t gfx_printBuffer(point_t start, fontSize_t size, textAlign_t alignment, color_t color, const char *buf) { diff --git a/openrtx/src/ui/new/ui_menu.c b/openrtx/src/ui/new/ui_menu.c index 16620c4b7..4e1bcfe93 100644 --- a/openrtx/src/ui/new/ui_menu.c +++ b/openrtx/src/ui/new/ui_menu.c @@ -6,6 +6,7 @@ #include "ui/ui_menu.h" #include "ui/ui_screen.h" +#include "ui/ui_textedit.h" #include "interfaces/keyboard.h" //TODO: For keycodes /* menu_draw includes */ @@ -193,6 +194,14 @@ static void menu_ensure_visible(MenuFrame *frame) } } +static void menu_textedit_done(bool applied, void *user) +{ + MenuValueBinding *b = (MenuValueBinding *)user; + if (applied && b && b->on_change) { + b->on_change(b->ptr); + } +} + /* --- Public API --- */ UiScreen *ui_get_menu_screen(void) { @@ -282,6 +291,24 @@ static void menu_tick(UiScreen *self, const UiEvent *ev) } /* Activate edit mode on value node */ else if (item->kind == MENU_NODE_VALUE && item->binding) { + MenuValueBinding *b = item->binding; + + if (b->kind == MENU_VAL_STR) { + /* Launch text editor screen instead of inline edit */ + UiTextEditParams p = { + .buf = (char *)b->ptr, + .max_len = b->u.str.max_len, + .profile = b->u.str.profile, + .title = item->label, + .on_done = menu_textedit_done, + .user = b, + }; + ui_open_textedit(&p); + st->dirty = true; // TODO: overwritten with `menu_draw()` + return; + } + + /* Non-string values; normal inline edit mode */ st->edit = true; st->dirty = true; } diff --git a/openrtx/src/ui/new/ui_menu_tree.c b/openrtx/src/ui/new/ui_menu_tree.c index 0b6cd10e4..257a93202 100644 --- a/openrtx/src/ui/new/ui_menu_tree.c +++ b/openrtx/src/ui/new/ui_menu_tree.c @@ -9,12 +9,14 @@ /* from ui_settings_*.c */ extern const MenuItem g_display_settings_menu; extern const MenuItem g_gps_settings_menu; +extern const MenuItem g_m17_settings_menu; extern const MenuItem g_fm_settings_menu; extern const MenuItem g_accessibility_settings_menu; static const MenuItem *const settings_children[] = { &g_display_settings_menu, &g_gps_settings_menu, + &g_m17_settings_menu, &g_fm_settings_menu, &g_accessibility_settings_menu, }; diff --git a/openrtx/src/ui/new/ui_settings_m17.c b/openrtx/src/ui/new/ui_settings_m17.c new file mode 100644 index 000000000..3fae78034 --- /dev/null +++ b/openrtx/src/ui/new/ui_settings_m17.c @@ -0,0 +1,59 @@ +#include +#include +#include + +#include "ui/ui_menu.h" +#include "ui/ui_menu_dsl.h" + +#include "core/state.h" + +/* TODO: Use real buffer instead of fake one */ +static char callsign_buf[10] = {0}; +static uint8_t m17_can = 0; +static bool m17_canrxcheck = false; + +static void callsign_on_change(void *ptr) { + (void)ptr; + // Do nothing +} + +static MenuValueBinding callsign_binding = { + .kind = MENU_VAL_STR, + .ptr = callsign_buf, + .on_change = callsign_on_change, + .u.str = { + .max_len = sizeof(callsign_buf) - 1, + .profile = UI_STR_PROFILE_CALLSIGN, + }, +}; + +static const MenuItem m_callsign = + MENU_ITEM_VALUE_BINDING("Callsign", &callsign_binding); + +static MenuValueBinding can_binding = { + .kind = MENU_VAL_U8, + .ptr = &m17_can, + .on_change = NULL, + .u.u8 = { .min = 0, .max = 15, .step = 1, .wrap = true }, +}; + +static const MenuItem m_can = + MENU_ITEM_VALUE_BINDING("CAN", &can_binding); + +static MenuValueBinding can_rx_check_binding = { + .kind = MENU_VAL_BOOL, + .ptr = &m17_canrxcheck, + .on_change = NULL, +}; + +static const MenuItem m_can_rx_check = + MENU_ITEM_VALUE_BINDING("CAN RX Check", &can_rx_check_binding); + +static const MenuItem *const m17_children[] = { + &m_callsign, + &m_can, + &m_can_rx_check, +}; + +const MenuItem g_m17_settings_menu = + MENU_FOLDER_FROM_CHILDREN("M17", m17_children); diff --git a/openrtx/src/ui/new/ui_textedit.c b/openrtx/src/ui/new/ui_textedit.c new file mode 100644 index 000000000..edabe97e3 --- /dev/null +++ b/openrtx/src/ui/new/ui_textedit.c @@ -0,0 +1,329 @@ +#include + +#include "ui/ui_textedit.h" +#include "ui/ui_screen.h" + +#include "core/graphics.h" +#include "core/input.h" +#include "interfaces/delays.h" +#include "interfaces/keyboard.h" +#include "hwconfig.h" + +static const char *symbols_ITU_T_E161[] = +{ + " 0", + ",.?1", + "abc2ABC", + "def3DEF", + "ghi4GHI", + "jkl5JKL", + "mno6MNO", + "pqrs7PQRS", + "tuv8TUV", + "wxyz9WXYZ", + "-/*", + "#" +}; + +static const char *symbols_ITU_T_E161_callsign[] = +{ + "0 ", + "1", + "ABC2", + "DEF3", + "GHI4", + "JKL5", + "MNO6", + "PQRS7", + "TUV8", + "WXYZ9", + "-/", + "" +}; + +#define TEXTEDIT_SCRATCH_SIZE 256 + +typedef struct { + char scratch[256]; //TODO: evaluate size + char *target; // final buffer (binding->ptr) + uint8_t max_len; // max chars (no '\0') + uint8_t cursor; // index 0..len + uint8_t len; // current length + + // multi-tap state + uint8_t active_key; // last numeric key + uint8_t active_idx; // index into symbols_...[active_key] + long long last_keypress; // getTick() of last keypress + + bool dirty; + + const char *title; + + const char *const *symbols; +} TexteditState; + +static TexteditState g_textedit_state; + +static void textedit_tick(UiScreen *self, const UiEvent *ev); +static void textedit_draw(UiScreen *self); + +static UiScreen g_textedit_screen = { + .tick = textedit_tick, + .draw = textedit_draw, + .ctx = &g_textedit_state, +}; + +/* --- Internal helpers --- */ + +static void textedit_reset(TexteditState *st) +{ + st->cursor = 0; + st->len = 0; + st->active_key = 0; + st->active_idx = 0; + st->last_keypress = 0; + st->dirty = true; + + st->scratch[0] = '\0'; + g_textedit_state.last_keypress = 0; +} + +static void textedit_keypad(TexteditState *st, kbd_msg_t msg) +{ + long long now = getTick(); + uint8_t num_key = input_getPressedChar(msg); + + bool same_key = (st->active_key == num_key); + bool key_timeout = ((now - st->last_keypress) >= UI_TEXTEDIT_KEY_TIMEOUT); + + const char *set = st->symbols[num_key]; + uint8_t num_symbols = set ? strlen(set) : 0; + if (num_symbols == 0) { + return; + } + + // If we're at max len and we'd need to advance cursor, bail + if (st->len >= st->max_len) { + return; + } + + if (st->last_keypress != 0 && same_key && !key_timeout) { + // Same key, short interval: just cycle current candidate + st->active_idx = (st->active_idx + 1u) % num_symbols; + } else { + // New key or timeout: start editing a new char at the end + st->active_idx = 0; + st->cursor = st->len; + st->len++; + } + + // Place candidate at cursor and keep buffer terminated + char c = set[st->active_idx]; + st->scratch[st->cursor] = c; + st->scratch[st->len] = '\0'; + + //vp_announceInputChar(c); + + st->active_key = num_key; + st->last_keypress = now; + st->dirty = true; +} + +static void textedit_del(TexteditState *st) +{ + if (st->len == 0) { + // nothing to delete + st->active_key = 0; + st->last_keypress = 0; + st->active_idx = 0; + return; + } + + // If we are in the middle of a word, shift everything left + if (st->cursor > 0 && st->cursor <= st->len){ + char c = st->scratch[st->cursor - 1]; + if (c != '\0') { + //vp_announceInputChar(c); + } + + // shift from cursor..len-1 left by one + size_t n = (st->len - st->cursor) + 1; // includes '\0' + memmove( + &st->scratch[st->cursor - 1], + &st->scratch[st->cursor], + n + ); + st->len--; + st->cursor--; + } else { + // cursor == 0: just delete first char + char c = st->scratch[0]; + if (c != '\0') { + //vp_announceInputChar(c); + } + size_t n = (st->len - 1) + 1; // chars after index 0 plus '\0' + memmove(&st->scratch[0], &st->scratch[1], n); + st->len = (st->len > 0) ? st->len - 1 : 0; + st->cursor = 0; + } + + st->scratch[st->len] = '\0'; + + // reset multi-tap context + st->active_key = 0; + st->active_idx = 0; + st->last_keypress = 0; +} + +/* --- Public API --- */ +void ui_open_textedit(const UiTextEditParams *p) +{ + if (!p || !p->buf || !p->title) { + return; + } + textedit_reset(&g_textedit_state); + + strncpy(g_textedit_state.scratch, p->buf, p->max_len); + g_textedit_state.title = p->title ? p->title : ""; + g_textedit_state.max_len = p->max_len; + g_textedit_state.target = p->buf; + g_textedit_state.len = strlen(p->buf); // TODO: evaluate safety + + switch (p->profile) + { + case UI_STR_PROFILE_CALLSIGN: + g_textedit_state.symbols = symbols_ITU_T_E161_callsign; + break; + + default: + g_textedit_state.symbols = symbols_ITU_T_E161; + break; + } + + ui_push_screen(&g_textedit_screen); +} + +/* --- UiScreen implementation --- */ +static void textedit_tick(UiScreen *self, const UiEvent *ev) +{ + TexteditState *st = (TexteditState *)self->ctx; + + if (!st) { + return; + } + + long long now = getTick(); + + if (st->candidate_active && (now - st->last_keypress) >= UI_TEXTEDIT_KEY_TIMEOUT) { + st->candidate_active = false; + st->dirty = true; + } + + if (!ev) { + return; + } + + enum key key = ev->key; + + //TODO: hacky + kbd_msg_t msg = { + .long_press = 0, + .keys = key, + }; + + if (key == KEY_ESC) + { + /* cancel everything */ + ui_pop_screen(); + } + else if(key == KEY_ENTER) + { + strncpy(st->target, st->scratch, st->max_len); + ui_pop_screen(); + } + else if (key == KEY_UP || key == KEY_DOWN) + { + textedit_del(st); + st->dirty = true; + } + else if(input_isCharPressed(msg)) + { + textedit_keypad(st, msg); + } +} + +static void textedit_draw(UiScreen *self) +{ + // consts borrowed from existing code for large displays + // TODO: copy over `layout_t` and helpers + const uint16_t text_v_offset = 1; + const uint16_t status_v_pad = 2; + const uint16_t top_h = 16; + const uint16_t top_pad = 4; + const uint16_t line1_h = 20; + const uint16_t small_line_v_pad = 2; + const uint16_t horizontal_pad = 4; + const uint16_t top_pos_y = top_h - status_v_pad - text_v_offset; + const uint16_t line1_pos_y = top_h + top_pad + line1_h - small_line_v_pad - text_v_offset; + const uint16_t menu_h = 16; + const uint16_t bottom_h = 23; + const point_t top_pos = {horizontal_pad, top_pos_y}; + const point_t line1_pos = {horizontal_pad, line1_pos_y}; + const fontSize_t top_font = FONT_SIZE_8PT; + const fontSize_t menu_font = FONT_SIZE_8PT; + const color_t color_white = {255, 255, 255, 255}; + const color_t color_black = {0, 0, 0, 255}; + + TexteditState *st = (TexteditState *)self->ctx; + + if(!st || !st->dirty) + { + return; + } + + gfx_clearScreen(); + + // Header + gfx_print(top_pos, top_font, TEXT_ALIGN_CENTER, color_white, st->title); + + // Edit box + uint16_t rect_width = CONFIG_SCREEN_WIDTH - (horizontal_pad * 2); + uint16_t rect_height = (CONFIG_SCREEN_HEIGHT - (top_h + bottom_h))/2; + point_t rect_origin = {(CONFIG_SCREEN_WIDTH - rect_width) / 2, + (CONFIG_SCREEN_HEIGHT - rect_height) / 2}; + gfx_drawRect(rect_origin, rect_width, rect_height, color_white, false); + + // Text + point_t pos = { + rect_origin.x + horizontal_pad, + rect_origin.y + top_h + 24, + }; + + for (uint8_t i = 0; i < st->len; ++i) { + char c = (i < st->len) ? st->scratch[i] : ' '; + bool is_cursor = (i == st->cursor); + + point_t sz; + + if (is_cursor) { + /* Draw inverted cell */ + // measure width of c + char tmp[2] = { c ? c : ' ', 0 }; + sz = gfx_textSize(top_font, tmp); + point_t rect = { pos.x, pos.y - 11 }; + gfx_drawRect(rect, sz.x+1, sz.y+2, color_white, true); + gfx_printBuffer(pos, top_font, TEXT_ALIGN_LEFT, color_black, tmp); + } else { + char tmp[2] = { c, 0 }; + sz = gfx_textSize(top_font, tmp); + gfx_printBuffer(pos, top_font, TEXT_ALIGN_LEFT, color_white, tmp); + } + + pos.x += sz.x; + } + + gfx_render(); + st->dirty = false; + +} + From f3a6ba1c499d0fc638f2144aae9696fc46f148d6 Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Mon, 10 Nov 2025 11:10:14 -0800 Subject: [PATCH 11/27] text editor working --- openrtx/src/ui/new/ui_textedit.c | 181 ++++++++++++++++--------------- 1 file changed, 96 insertions(+), 85 deletions(-) diff --git a/openrtx/src/ui/new/ui_textedit.c b/openrtx/src/ui/new/ui_textedit.c index edabe97e3..3b7086f0c 100644 --- a/openrtx/src/ui/new/ui_textedit.c +++ b/openrtx/src/ui/new/ui_textedit.c @@ -44,21 +44,21 @@ static const char *symbols_ITU_T_E161_callsign[] = #define TEXTEDIT_SCRATCH_SIZE 256 typedef struct { - char scratch[256]; //TODO: evaluate size + char scratch[TEXTEDIT_SCRATCH_SIZE]; //TODO: evaluate size char *target; // final buffer (binding->ptr) uint8_t max_len; // max chars (no '\0') - uint8_t cursor; // index 0..len + uint8_t cursor; // index 0..len (not needed for now) uint8_t len; // current length // multi-tap state uint8_t active_key; // last numeric key uint8_t active_idx; // index into symbols_...[active_key] long long last_keypress; // getTick() of last keypress + bool candidate_active; bool dirty; const char *title; - const char *const *symbols; } TexteditState; @@ -82,10 +82,10 @@ static void textedit_reset(TexteditState *st) st->active_key = 0; st->active_idx = 0; st->last_keypress = 0; + st->candidate_active = false; st->dirty = true; st->scratch[0] = '\0'; - g_textedit_state.last_keypress = 0; } static void textedit_keypad(TexteditState *st, kbd_msg_t msg) @@ -93,86 +93,68 @@ static void textedit_keypad(TexteditState *st, kbd_msg_t msg) long long now = getTick(); uint8_t num_key = input_getPressedChar(msg); - bool same_key = (st->active_key == num_key); - bool key_timeout = ((now - st->last_keypress) >= UI_TEXTEDIT_KEY_TIMEOUT); - const char *set = st->symbols[num_key]; uint8_t num_symbols = set ? strlen(set) : 0; if (num_symbols == 0) { return; } + bool same_key = (st->candidate_active && st->active_key == num_key); + bool timed_out = st->candidate_active && + ((now - st->last_keypress) >= UI_TEXTEDIT_KEY_TIMEOUT); + // If we're at max len and we'd need to advance cursor, bail - if (st->len >= st->max_len) { - return; - } + if (!same_key || timed_out) { + // Changing key or previous candidate expired. + // In either case, we want a new candidate at the end. + if (st->len >= st->max_len) { + // Can't append + return; + } - if (st->last_keypress != 0 && same_key && !key_timeout) { - // Same key, short interval: just cycle current candidate - st->active_idx = (st->active_idx + 1u) % num_symbols; - } else { - // New key or timeout: start editing a new char at the end - st->active_idx = 0; - st->cursor = st->len; + st->candidate_active = true; + st->active_key = num_key; + st->active_idx = 0; + + // Append new candidate char at end + st->scratch[st->len] = set[0]; st->len++; + st->scratch[st->len] = '\0'; + } else { + // same key, within timeout: cycle candidate + // The candidate is at `st->len - 1` + st->active_idx = (uint8_t)((st->active_idx + 1u) % num_symbols); + if (st->len > 0) { + st->scratch[st->len - 1] = set[st->active_idx]; + } } - // Place candidate at cursor and keep buffer terminated - char c = set[st->active_idx]; - st->scratch[st->cursor] = c; - st->scratch[st->len] = '\0'; + //vp_announceInputChar(st->scratch[st->len - 1]); - //vp_announceInputChar(c); - - st->active_key = num_key; st->last_keypress = now; - st->dirty = true; + st->dirty = true; } static void textedit_del(TexteditState *st) { if (st->len == 0) { // nothing to delete - st->active_key = 0; - st->last_keypress = 0; - st->active_idx = 0; + st->candidate_active = false; + st->active_key = 0; + st->active_key = 0; + st->last_keypress = 0; return; } - // If we are in the middle of a word, shift everything left - if (st->cursor > 0 && st->cursor <= st->len){ - char c = st->scratch[st->cursor - 1]; - if (c != '\0') { - //vp_announceInputChar(c); - } - - // shift from cursor..len-1 left by one - size_t n = (st->len - st->cursor) + 1; // includes '\0' - memmove( - &st->scratch[st->cursor - 1], - &st->scratch[st->cursor], - n - ); - st->len--; - st->cursor--; - } else { - // cursor == 0: just delete first char - char c = st->scratch[0]; - if (c != '\0') { - //vp_announceInputChar(c); - } - size_t n = (st->len - 1) + 1; // chars after index 0 plus '\0' - memmove(&st->scratch[0], &st->scratch[1], n); - st->len = (st->len > 0) ? st->len - 1 : 0; - st->cursor = 0; - } - + // The last character is always removed, whether is was a candidate or not + st->len--; st->scratch[st->len] = '\0'; - // reset multi-tap context - st->active_key = 0; - st->active_idx = 0; - st->last_keypress = 0; + // Deletion always ends the multi-tap sequence + st->candidate_active = false; + st->active_key = 0; + st->active_idx = 0; + st->last_keypress = 0; } /* --- Public API --- */ @@ -183,11 +165,13 @@ void ui_open_textedit(const UiTextEditParams *p) } textedit_reset(&g_textedit_state); - strncpy(g_textedit_state.scratch, p->buf, p->max_len); g_textedit_state.title = p->title ? p->title : ""; g_textedit_state.max_len = p->max_len; g_textedit_state.target = p->buf; - g_textedit_state.len = strlen(p->buf); // TODO: evaluate safety + + strncpy(g_textedit_state.scratch, p->buf, g_textedit_state.max_len); + g_textedit_state.scratch[g_textedit_state.max_len] = '\0'; + g_textedit_state.len = strlen(g_textedit_state.scratch); switch (p->profile) { @@ -214,11 +198,21 @@ static void textedit_tick(UiScreen *self, const UiEvent *ev) long long now = getTick(); - if (st->candidate_active && (now - st->last_keypress) >= UI_TEXTEDIT_KEY_TIMEOUT) { - st->candidate_active = false; - st->dirty = true; + // 1: Time-based expiry of candidate + if (st->candidate_active) { + long long dt = now - st->last_keypress; + if (dt >= UI_TEXTEDIT_KEY_TIMEOUT) { + // Commit the candidate visually: keep it in scratch, + // but stop treating it as "live" multi-tap. + st->candidate_active = false; + st->active_key = 0; + st->active_idx = 0; + st->last_keypress = 0; + st->dirty = true; + } } + // 2: If no event, nothing else to do this tick. if (!ev) { return; } @@ -231,24 +225,29 @@ static void textedit_tick(UiScreen *self, const UiEvent *ev) .keys = key, }; + // 3: Handle keys if (key == KEY_ESC) { /* cancel everything */ ui_pop_screen(); + return; } else if(key == KEY_ENTER) { strncpy(st->target, st->scratch, st->max_len); ui_pop_screen(); + return; } else if (key == KEY_UP || key == KEY_DOWN) { textedit_del(st); st->dirty = true; + return; } else if(input_isCharPressed(msg)) { textedit_keypad(st, msg); + return; } } @@ -270,7 +269,7 @@ static void textedit_draw(UiScreen *self) const point_t top_pos = {horizontal_pad, top_pos_y}; const point_t line1_pos = {horizontal_pad, line1_pos_y}; const fontSize_t top_font = FONT_SIZE_8PT; - const fontSize_t menu_font = FONT_SIZE_8PT; + const fontSize_t text_font = FONT_SIZE_8PT; const color_t color_white = {255, 255, 255, 255}; const color_t color_black = {0, 0, 0, 255}; @@ -298,32 +297,44 @@ static void textedit_draw(UiScreen *self) rect_origin.x + horizontal_pad, rect_origin.y + top_h + 24, }; + uint16_t baseline_y = pos.y; + int caret_x0 = -1; + int caret_x1 = -1; + + // Draw all characters and track caret spans for (uint8_t i = 0; i < st->len; ++i) { - char c = (i < st->len) ? st->scratch[i] : ' '; - bool is_cursor = (i == st->cursor); - - point_t sz; - - if (is_cursor) { - /* Draw inverted cell */ - // measure width of c - char tmp[2] = { c ? c : ' ', 0 }; - sz = gfx_textSize(top_font, tmp); - point_t rect = { pos.x, pos.y - 11 }; - gfx_drawRect(rect, sz.x+1, sz.y+2, color_white, true); - gfx_printBuffer(pos, top_font, TEXT_ALIGN_LEFT, color_black, tmp); - } else { - char tmp[2] = { c, 0 }; - sz = gfx_textSize(top_font, tmp); - gfx_printBuffer(pos, top_font, TEXT_ALIGN_LEFT, color_white, tmp); + char c = st->scratch[i]; + if (!c) + break; + + char tmp[2] = { c, 0 }; + point_t sz = gfx_textSize(text_font, tmp); + + gfx_printBuffer(pos, text_font, TEXT_ALIGN_LEFT, color_white, tmp); + + if (st->candidate_active && i == st->len - 1) { + // Underline span for candidate char + caret_x0 = pos.x; + caret_x1 = pos.x + sz.x; } pos.x += sz.x; } + // If no active candidate, caret is a short bar at the end + if (!st->candidate_active) { + caret_x0 = pos.x; + caret_x1 = pos.x + 6; + } + + if (caret_x0 >= 0 && caret_x1 > caret_x0) { + uint16_t ul_y = baseline_y + 2; + point_t ul_origin = { (uint16_t)caret_x0, ul_y }; + uint16_t ul_width = (uint16_t)(caret_x1 - caret_x0); + gfx_drawRect(ul_origin, ul_width, 1, color_white, true); + } + gfx_render(); st->dirty = false; - } - From b9818bbd0b8136e51941298538bc78039ca80bfe Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Mon, 10 Nov 2025 11:41:57 -0800 Subject: [PATCH 12/27] add UiScreenEvent type for focus; fixes screen paint issue --- openrtx/include/ui/ui_screen.h | 11 ++++++-- openrtx/src/ui/new/ui_core.c | 46 ++++++++++++++++++++++++++------ openrtx/src/ui/new/ui_menu.c | 23 ++++++++++++++-- openrtx/src/ui/new/ui_textedit.c | 13 +++++++++ 4 files changed, 81 insertions(+), 12 deletions(-) diff --git a/openrtx/include/ui/ui_screen.h b/openrtx/include/ui/ui_screen.h index 1627160f4..b6bbc102f 100644 --- a/openrtx/include/ui/ui_screen.h +++ b/openrtx/include/ui/ui_screen.h @@ -12,9 +12,16 @@ typedef struct UiScreen UiScreen; +typedef enum { + UI_EVENT_NONE = 0, + UI_EVENT_KEY = 1, + UI_EVENT_FOCUS_GAIN = 2, + UI_EVENT_FOCUS_LOST = 3, +} UiEventType; + typedef struct { - uint16_t type; - enum key key; + UiEventType type; + enum key key; // valid only when type == UI_EVENT_KEY } UiEvent; typedef void (*UiScreenTick)(UiScreen *self, const UiEvent *ev); diff --git a/openrtx/src/ui/new/ui_core.c b/openrtx/src/ui/new/ui_core.c index 19906773d..be6ba336e 100644 --- a/openrtx/src/ui/new/ui_core.c +++ b/openrtx/src/ui/new/ui_core.c @@ -11,25 +11,55 @@ static UiScreen *screen_stack[UI_SCREEN_STACK_MAX]; static uint8_t screen_top = 0; +static void ui_send_focus_event(UiScreen *s, UiEventType type) { + if (!s) return; + UiEvent ev = { + .type = type, + .key = 0, // unused + }; + s->tick(s, &ev); +} + +UiScreen *ui_current_screen(void) { + return (screen_top > 0) ? screen_stack[screen_top - 1] : NULL; +} + void ui_push_screen(UiScreen *s) { - if (screen_top < UI_SCREEN_STACK_MAX) { - screen_stack[screen_top++] = s; + if (!s) return; + if (screen_top >= UI_SCREEN_STACK_MAX) { + //TODO: maybe log/assert? + return; + } + + UiScreen *old = ui_current_screen(); + if (old) { + ui_send_focus_event(old, UI_EVENT_FOCUS_LOST); } + + screen_stack[screen_top++] = s; + + ui_send_focus_event(s, UI_EVENT_FOCUS_GAIN); } void ui_pop_screen(void) { - if (screen_top > 0) { - screen_top--; + if (screen_top == 0) { + return; } -} -UiScreen *ui_current_screen(void) { - return (screen_top > 0) ? screen_stack[screen_top - 1] : NULL; + UiScreen *old = screen_stack[screen_top - 1]; + ui_send_focus_event(old, UI_EVENT_FOCUS_LOST); + + screen_top--; + + UiScreen *now = ui_current_screen(); + if (now) { + ui_send_focus_event(now, UI_EVENT_FOCUS_GAIN); + } } bool ui_build_event_from_kbd(const kbd_msg_t *kbd, UiEvent *ev) { if (!kbd) return false; - ev->type = 1; + ev->type = UI_EVENT_KEY; ev->key = kbd->keys; return true; } \ No newline at end of file diff --git a/openrtx/src/ui/new/ui_menu.c b/openrtx/src/ui/new/ui_menu.c index 4e1bcfe93..d6361992c 100644 --- a/openrtx/src/ui/new/ui_menu.c +++ b/openrtx/src/ui/new/ui_menu.c @@ -134,6 +134,11 @@ static void menu_value_format(const MenuValueBinding *b, char *buf, size_t n) sniprintf(buf, n, "%"PRIu8, v); break; } + case MENU_VAL_STR: { + char *v = (char *)b->ptr; + sniprintf(buf, n, "%s", v); + break; + } default: sniprintf(buf, n, "?"); break; @@ -221,7 +226,7 @@ static void menu_tick(UiScreen *self, const UiEvent *ev) { MenuState *st = (MenuState *)self->ctx; - if (!st || !ev) { + if (!st) { return; } @@ -230,7 +235,21 @@ static void menu_tick(UiScreen *self, const UiEvent *ev) menu_reset_to_root(st); } - /* For now we treat all events as key events */ + switch (ev->type) { + case UI_EVENT_FOCUS_GAIN: + st->dirty = true; + return; + + case UI_EVENT_FOCUS_LOST: + // Nothing special for now + return; + case UI_EVENT_KEY: + break; // handle below + default: + return; + } + + // It's a key event enum key key = ev->key; MenuFrame *frame = &st->stack[st->depth - 1]; diff --git a/openrtx/src/ui/new/ui_textedit.c b/openrtx/src/ui/new/ui_textedit.c index 3b7086f0c..4e17f87e1 100644 --- a/openrtx/src/ui/new/ui_textedit.c +++ b/openrtx/src/ui/new/ui_textedit.c @@ -217,6 +217,19 @@ static void textedit_tick(UiScreen *self, const UiEvent *ev) return; } + switch (ev->type) { + case UI_EVENT_FOCUS_GAIN: + st->dirty = true; + return; + case UI_EVENT_FOCUS_LOST: + // Nothing for now + return; + case UI_EVENT_KEY: + break; // handled below + default: + return; + } + enum key key = ev->key; //TODO: hacky From a68e122bbf3a6fe9fad2e1399548aa90c988bf77 Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Mon, 10 Nov 2025 11:50:22 -0800 Subject: [PATCH 13/27] ui_menu: fix compiler warning --- openrtx/src/ui/new/ui_menu.c | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/openrtx/src/ui/new/ui_menu.c b/openrtx/src/ui/new/ui_menu.c index d6361992c..cc1c1066e 100644 --- a/openrtx/src/ui/new/ui_menu.c +++ b/openrtx/src/ui/new/ui_menu.c @@ -310,7 +310,7 @@ static void menu_tick(UiScreen *self, const UiEvent *ev) } /* Activate edit mode on value node */ else if (item->kind == MENU_NODE_VALUE && item->binding) { - MenuValueBinding *b = item->binding; + const MenuValueBinding *b = item->binding; if (b->kind == MENU_VAL_STR) { /* Launch text editor screen instead of inline edit */ @@ -320,10 +320,9 @@ static void menu_tick(UiScreen *self, const UiEvent *ev) .profile = b->u.str.profile, .title = item->label, .on_done = menu_textedit_done, - .user = b, + .user = (void *)b, }; ui_open_textedit(&p); - st->dirty = true; // TODO: overwritten with `menu_draw()` return; } @@ -471,7 +470,13 @@ static void menu_draw(UiScreen *self) /* Value on the right if this is a VALUE node */ if (item->kind == MENU_NODE_VALUE) { - // TODO: Evaluate value buffer size + /** + * TODO: Evaluate value buffer size + * - We don't want the printed value to take up too much room in the + * screen, taking away from the item title. + * - Variable length fonts make this tricky. + * - Truncating could be misleading + */ char buf[16] = {0}; if (item->binding) { From d90df3df81a8b74ada801f2ef41baeb48b585add Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Mon, 10 Nov 2025 15:31:46 -0800 Subject: [PATCH 14/27] executive decision: un-bitfield settings_t --- meson.build | 1 + openrtx/include/core/settings.h | 45 ++++++++++----------------------- openrtx/src/core/settings.c | 24 ++++++++++++++++++ 3 files changed, 39 insertions(+), 31 deletions(-) create mode 100644 openrtx/src/core/settings.c diff --git a/meson.build b/meson.build index 67eeda9b1..f3b73b8ac 100644 --- a/meson.build +++ b/meson.build @@ -34,6 +34,7 @@ openrtx_def += {'FONT_UBUNTU_REGULAR': ''} ## openrtx_src = ['openrtx/src/core/state.c', + 'openrtx/src/core/settings.c', 'openrtx/src/core/threads.c', 'openrtx/src/core/battery.c', 'openrtx/src/core/graphics.c', diff --git a/openrtx/include/core/settings.h b/openrtx/include/core/settings.h index 8bdde3272..9cc4f24bd 100644 --- a/openrtx/include/core/settings.h +++ b/openrtx/include/core/settings.h @@ -9,6 +9,7 @@ #include "hwconfig.h" #include +#include typedef enum { @@ -40,45 +41,27 @@ typedef struct int8_t utc_timezone; // Timezone, in units of half hours bool gps_enabled; // GPS active char callsign[10]; // Plaintext callsign - uint8_t display_timer : 4, // Standby timer - m17_can : 4; // M17 CAN - uint8_t vpLevel : 3, // Voice prompt level - vpPhoneticSpell : 1, // Phonetic spell enabled - macroMenuLatch : 1, // Automatic latch of macro menu - _reserved : 3; + uint8_t display_timer; // Standby timer + uint8_t m17_can; // M17 CAN + uint8_t vpLevel; // Voice prompt level + uint8_t vpPhoneticSpell; // Phonetic spell enabled + bool macroMenuLatch; // Automatic latch of macro menu bool m17_can_rx; // Check M17 CAN on RX char m17_dest[10]; // M17 destination bool showBatteryIcon; // Battery display true: icon, false: percentage bool gpsSetTime; // Use GPS to ajust RTC time char M17_meta_text[53]; // M17 Meta Text to send } -__attribute__((packed)) settings_t; +settings_t; +#ifdef __cplusplus +extern "C" { +#endif -static const settings_t default_settings = -{ - 100, // Brightness -#ifdef CONFIG_SCREEN_CONTRAST - CONFIG_DEFAULT_CONTRAST, // Contrast -#else - 255, // Contrast +extern const settings_t default_settings; + +#ifdef __cplusplus +} #endif - 4, // Squelch level, 4 = S3 - 0, // Vox level - 0, // UTC Timezone - false, // GPS enabled - "", // Empty callsign - TIMER_30S, // 30 seconds - 0, // M17 CAN - 0, // Voice prompts off - 0, // Phonetic spell off - 1, // Automatic latch of macro menu enabled - 0, // not used - false, // Check M17 CAN on RX - "", // Empty M17 destination - false, // Display battery icon - false, // Update RTC with GPS - "OpenRTX", // Default M17 meta text -}; #endif /* SETTINGS_H */ diff --git a/openrtx/src/core/settings.c b/openrtx/src/core/settings.c new file mode 100644 index 000000000..5a27d4e33 --- /dev/null +++ b/openrtx/src/core/settings.c @@ -0,0 +1,24 @@ +#include "core/settings.h" + +const settings_t default_settings = { + .brightness = 100, +#ifdef CONFIG_SCREEN_CONTRAST + .contrast = CONFIG_DEFAULT_CONTRAST, +#else + .contrast = 255, +#endif + .sqlLevel = 4, + .voxLevel = 0, + .utc_timezone = 0, + .gps_enabled = false, + .callsign = "", + .display_timer = TIMER_30S, + .m17_can = 0, + .vpLevel = 0, + .vpPhoneticSpell = 0, + .macroMenuLatch = true, + .m17_can_rx = false, + .m17_dest = "", + .showBatteryIcon = false, + .gpsSetTime = false, +}; From 3d784b03c3d4072ed4aea10698037344036707c4 Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Mon, 10 Nov 2025 16:57:47 -0800 Subject: [PATCH 15/27] finish MENU_VAL_ENUM, display settings --- openrtx/src/ui/new/ui_menu.c | 172 +++++++++++++---------- openrtx/src/ui/new/ui_settings_display.c | 29 +++- 2 files changed, 128 insertions(+), 73 deletions(-) diff --git a/openrtx/src/ui/new/ui_menu.c b/openrtx/src/ui/new/ui_menu.c index cc1c1066e..e9010452c 100644 --- a/openrtx/src/ui/new/ui_menu.c +++ b/openrtx/src/ui/new/ui_menu.c @@ -16,6 +16,75 @@ /* How many rows we plan to show at once; for scrolling logic */ #define MENU_VISIBLE_ROWS 6 +static void u8_adjust(uint8_t *data, bool inc, uint8_t min, uint8_t max, uint8_t step, bool wrap) { + uint8_t v = *data; + + /* TODO: Evaluate responsibility + // Sanity check: if min > max, swap + if (min > max) { + uint8_t tmp = min; + min = max; + max = tmp; + } + + if (step == 0) { + step = 1; + } + */ + + // Normalize any out-of-range value + if (v < min) { + v = min; + } else if (v > max) { + v = max; + } + + if (inc) { + // Increment + if (v >= max) { + if (wrap) { + v = min; + } else { + v = max; + } + } else { + // Do arithmetic in a wider type to avoid overflow + unsigned int tmp = (unsigned int)v + (unsigned int)step; + if (tmp > max) { + if (wrap) { + v = min; + } else { + v = max; + } + } else { + v = (uint8_t)tmp; + } + } + } else { + // Decrement + if (v <= min) { + if (wrap) { + v = max; + } else { + v = min; + } + } else { + int tmp = (int)v - (int)step; + if (tmp < (int)min) { + if (wrap) { + v = max; + } else { + v = min; + } + } else { + v = (uint8_t)tmp; + } + } + } + + *data = v; +} + static void menu_value_adjust(const MenuValueBinding *b, bool inc) { if (!b || !b->ptr) { @@ -30,77 +99,26 @@ static void menu_value_adjust(const MenuValueBinding *b, bool inc) } case MENU_VAL_U8: { uint8_t *p = (uint8_t *)b->ptr; - uint8_t v = *p; - - uint8_t min = b->u.u8.min; - uint8_t max = b->u.u8.max; - uint8_t step = b->u.u8.step; - bool wrap = b->u.u8.wrap; - - /* TODO: Evaluate responsibility - // Sanity check: if min > max, swap - if (min > max) { - uint8_t tmp = min; - min = max; - max = tmp; - } - - if (step == 0) { - step = 1; - } - */ - - // Normalize any out-of-range value - if (v < min) { - v = min; - } else if (v > max) { - v = max; - } - - if (inc) { - // Increment - if (v >= max) { - if (wrap) { - v = min; - } else { - v = max; - } - } else { - // Do arithmetic in a wider type to avoid overflow - unsigned int tmp = (unsigned int)v + (unsigned int)step; - if (tmp > max) { - if (wrap) { - v = min; - } else { - v = max; - } - } else { - v = (uint8_t)tmp; - } - } - } else { - // Decrement - if (v <= min) { - if (wrap) { - v = max; - } else { - v = min; - } - } else { - int tmp = (int)v - (int)step; - if (tmp < (int)min) { - if (wrap) { - v = max; - } else { - v = min; - } - } else { - v = (uint8_t)tmp; - } - } - } - - *p = v; + u8_adjust( + p, + inc, + b->u.u8.min, + b->u.u8.max, + b->u.u8.step, + b->u.u8.wrap + ); + break; + } + case MENU_VAL_ENUM: { + uint8_t *p = (uint8_t *)b->ptr; + u8_adjust( + p, + inc, + 0, + b->u.enm.count-1, + 1, + true + ); break; } default: @@ -121,7 +139,7 @@ static void menu_value_format(const MenuValueBinding *b, char *buf, size_t n) switch (b->kind) { case MENU_VAL_BOOL: { bool v = *(bool *)b->ptr; - sniprintf(buf, n, "%s", v ? "On" : "Off"); + sniprintf(buf, n, "%s", v ? "ON" : "OFF"); break; } case MENU_VAL_I32: { @@ -134,6 +152,12 @@ static void menu_value_format(const MenuValueBinding *b, char *buf, size_t n) sniprintf(buf, n, "%"PRIu8, v); break; } + case MENU_VAL_ENUM: { + char **names = (char **)b->u.enm.names; + char *v = names[*((uint8_t*)b->ptr)]; + sniprintf(buf, n, "%s", v); + break; + } case MENU_VAL_STR: { char *v = (char *)b->ptr; sniprintf(buf, n, "%s", v); @@ -235,6 +259,10 @@ static void menu_tick(UiScreen *self, const UiEvent *ev) menu_reset_to_root(st); } + if (!ev) { + return; + } + switch (ev->type) { case UI_EVENT_FOCUS_GAIN: st->dirty = true; diff --git a/openrtx/src/ui/new/ui_settings_display.c b/openrtx/src/ui/new/ui_settings_display.c index 7c275fbdf..f570d79ba 100644 --- a/openrtx/src/ui/new/ui_settings_display.c +++ b/openrtx/src/ui/new/ui_settings_display.c @@ -41,8 +41,35 @@ static const MenuItem m_contrast = MENU_ITEM_VALUE_BINDING("Contrast", &contrast_binding); #endif +static const char *timer_names[] = +{ + "OFF", + "5 s", + "10 s", + "15 s", + "20 s", + "25 s", + "30 s", + "1 min", + "2 min", + "3 min", + "4 min", + "5 min", + "15 min", + "30 min", + "45 min", + "1 hour" +}; + +static MenuValueBinding timer_binding = { + .kind = MENU_VAL_ENUM, + .ptr = &state.settings.display_timer, + .u.enm = { .names = timer_names, .count = ARRAY_LEN(timer_names) }, + .on_change = NULL, +}; + static const MenuItem m_timer = - MENU_ITEM_UNIMPLEMENTED("Timer"); + MENU_ITEM_VALUE_BINDING("Timer", &timer_binding); static MenuValueBinding battery_icon_binding = { .kind = MENU_VAL_BOOL, From 50979b3062412be198bc11b2ffaccd1f531819b3 Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Thu, 13 Nov 2025 13:50:21 -0800 Subject: [PATCH 16/27] wire up more settings --- meson.build | 4 +- openrtx/include/ui/ui_menu.h | 4 +- openrtx/src/ui/new/ui_menu.c | 65 ++++++++++++++-- openrtx/src/ui/new/ui_menu_tree.c | 13 ++-- .../src/ui/new/ui_settings_accessibility.c | 49 +++++------- openrtx/src/ui/new/ui_settings_fm.c | 3 + openrtx/src/ui/new/ui_settings_gps.c | 77 +++++++++++++++++-- openrtx/src/ui/new/ui_settings_m17.c | 25 +++--- openrtx/src/ui/new/ui_settings_radio.c | 28 +++++++ 9 files changed, 201 insertions(+), 67 deletions(-) create mode 100644 openrtx/src/ui/new/ui_settings_radio.c diff --git a/meson.build b/meson.build index f3b73b8ac..8b094d686 100644 --- a/meson.build +++ b/meson.build @@ -86,6 +86,7 @@ ui_src_default = ['openrtx/src/ui/default/ui.c', 'openrtx/src/ui/new/ui_menu_tree.c', 'openrtx/src/ui/new/ui_settings_display.c', 'openrtx/src/ui/new/ui_settings_gps.c', + 'openrtx/src/ui/new/ui_settings_radio.c', 'openrtx/src/ui/new/ui_settings_m17.c', 'openrtx/src/ui/new/ui_settings_fm.c', 'openrtx/src/ui/new/ui_settings_accessibility.c', @@ -810,7 +811,8 @@ cs7000p_opts = { 'c_args' : cs7000p_args, 'cpp_args' : cs7000p_args, 'link_args' : ['-Wl,-T../platform/mcu/STM32H7xx/linker_script_cs7000p.ld', - '-Wl,--print-memory-usage'] + '-Wl,--print-memory-usage', + '-Wl,-Map=firmware.map'] } dm1701_opts = { diff --git a/openrtx/include/ui/ui_menu.h b/openrtx/include/ui/ui_menu.h index 25b8e6b09..81731c331 100644 --- a/openrtx/include/ui/ui_menu.h +++ b/openrtx/include/ui/ui_menu.h @@ -71,8 +71,8 @@ typedef enum { typedef enum { MENU_VAL_BOOL, - MENU_VAL_I32, MENU_VAL_U8, + MENU_VAL_I32, MENU_VAL_ENUM, MENU_VAL_STR, } MenuValueKind; @@ -83,8 +83,8 @@ typedef struct { void (*on_change)(void *ptr); // optional side-effect on change union { - struct { int32_t min, max, step; bool wrap; } i32; struct { uint8_t min, max, step; bool wrap; } u8; + struct { int32_t min, max, step; bool wrap; } i32; struct { const char *const *names; uint8_t count; } enm; struct { uint8_t max_len; diff --git a/openrtx/src/ui/new/ui_menu.c b/openrtx/src/ui/new/ui_menu.c index e9010452c..ae465ee56 100644 --- a/openrtx/src/ui/new/ui_menu.c +++ b/openrtx/src/ui/new/ui_menu.c @@ -85,6 +85,49 @@ static void u8_adjust(uint8_t *data, bool inc, uint8_t min, uint8_t max, uint8_t *data = v; } + +static void i32_adjust(int32_t *data, bool inc, + int32_t min, int32_t max, + int32_t step, bool wrap) +{ + int32_t v = *data; + + // Normalize any out-of-range value + if (v < min) { + v = min; + } else if (v > max) { + v = max; + } + + if (inc) { + // Increment + if (v >= max) { + v = wrap ? min : max; + } else { + int64_t tmp = (int64_t)v + (int64_t)step; + if (tmp > (int64_t)max) { + v = wrap ? min : max; + } else { + v = (int32_t)tmp; + } + } + } else { + // Decrement + if (v <= min) { + v = wrap ? max : min; + } else { + int64_t tmp = (int64_t)v - (int64_t)step; + if (tmp < (int64_t)min) { + v = wrap ? max : min; + } else { + v = (int32_t)tmp; + } + } + } + + *data = v; +} + static void menu_value_adjust(const MenuValueBinding *b, bool inc) { if (!b || !b->ptr) { @@ -109,6 +152,18 @@ static void menu_value_adjust(const MenuValueBinding *b, bool inc) ); break; } + case MENU_VAL_I32: { + int32_t *p = (int32_t *)b->ptr; + i32_adjust( + p, + inc, + b->u.i32.min, + b->u.i32.max, + b->u.i32.step, + b->u.i32.wrap + ); + break; + } case MENU_VAL_ENUM: { uint8_t *p = (uint8_t *)b->ptr; u8_adjust( @@ -142,16 +197,16 @@ static void menu_value_format(const MenuValueBinding *b, char *buf, size_t n) sniprintf(buf, n, "%s", v ? "ON" : "OFF"); break; } - case MENU_VAL_I32: { - int32_t v = *(int32_t *)b->ptr; - sniprintf(buf, n, "%"PRIi32, v); - break; - } case MENU_VAL_U8: { uint8_t v = *(uint8_t *)b->ptr; sniprintf(buf, n, "%"PRIu8, v); break; } + case MENU_VAL_I32: { + int32_t v = *(int32_t *)b->ptr; + sniprintf(buf, n, "%"PRIi32, v); + break; + } case MENU_VAL_ENUM: { char **names = (char **)b->u.enm.names; char *v = names[*((uint8_t*)b->ptr)]; diff --git a/openrtx/src/ui/new/ui_menu_tree.c b/openrtx/src/ui/new/ui_menu_tree.c index 257a93202..f0f606554 100644 --- a/openrtx/src/ui/new/ui_menu_tree.c +++ b/openrtx/src/ui/new/ui_menu_tree.c @@ -9,29 +9,28 @@ /* from ui_settings_*.c */ extern const MenuItem g_display_settings_menu; extern const MenuItem g_gps_settings_menu; +extern const MenuItem g_radio_settings_menu; extern const MenuItem g_m17_settings_menu; extern const MenuItem g_fm_settings_menu; extern const MenuItem g_accessibility_settings_menu; +static const MenuItem m_default_settings = MENU_ITEM_UNIMPLEMENTED("Default Settings"); + static const MenuItem *const settings_children[] = { &g_display_settings_menu, &g_gps_settings_menu, + &g_radio_settings_menu, &g_m17_settings_menu, &g_fm_settings_menu, &g_accessibility_settings_menu, + &m_default_settings, }; static const MenuItem m_banks = MENU_ITEM_UNIMPLEMENTED("Banks"); static const MenuItem m_channels = MENU_ITEM_UNIMPLEMENTED("Channels"); static const MenuItem m_contacts = MENU_ITEM_UNIMPLEMENTED("Contacts"); static const MenuItem m_gps = MENU_ITEM_UNIMPLEMENTED("GPS"); -static const MenuItem m_settings = { MENU_NODE_FOLDER, "Settings", - .child_count = ARRAY_LEN(settings_children), - .children = settings_children, - .binding = NULL, - .cb = NULL, - .cb_ctx = NULL, -}; +static const MenuItem m_settings = MENU_FOLDER_FROM_CHILDREN("Settings", settings_children); static const MenuItem m_info = MENU_ITEM_UNIMPLEMENTED("Info"); static const MenuItem m_about = MENU_ITEM_UNIMPLEMENTED("About"); diff --git a/openrtx/src/ui/new/ui_settings_accessibility.c b/openrtx/src/ui/new/ui_settings_accessibility.c index 07da7d0cf..c21dcbaae 100644 --- a/openrtx/src/ui/new/ui_settings_accessibility.c +++ b/openrtx/src/ui/new/ui_settings_accessibility.c @@ -1,56 +1,45 @@ #include +#include "core/state.h" #include "ui/ui_menu.h" #include "ui/ui_menu_dsl.h" -/* TODO: Replace fake values with real ones */ - -static bool macrolatch = false; -static bool phonetic = false; -static bool voice = false; - -/* End TODO:--------------------------------*/ - static MenuValueBinding macro_latch_binding = { .kind = MENU_VAL_BOOL, - .ptr = ¯olatch, + .ptr = &state.settings.macroMenuLatch, .on_change = NULL, }; static const MenuItem m_macro_latch = MENU_ITEM_VALUE_BINDING("Macro Latch", ¯o_latch_binding); +static const char *voice_names[] = +{ + "OFF", + "Beep", + "1", + "2", + "3", +}; + static MenuValueBinding voice_binding = { - .kind = MENU_VAL_BOOL, - .ptr = &voice, + .kind = MENU_VAL_ENUM, + .ptr = &state.settings.vpLevel, + .u.enm = { .names = voice_names, .count = ARRAY_LEN(voice_names) }, .on_change = NULL, }; -static const MenuItem m_voice = { - .kind = MENU_NODE_UNIMPLEMENTED, - .label = "Voice", - .child_count = 0, - .children = NULL, - .binding = &voice_binding, - .cb = NULL, - .cb_ctx = NULL, -}; +static const MenuItem m_voice = + MENU_ITEM_VALUE_BINDING("Voice", &voice_binding); static MenuValueBinding phonetic_binding = { .kind = MENU_VAL_BOOL, - .ptr = &phonetic, + .ptr = &state.settings.vpPhoneticSpell, .on_change = NULL, }; -static const MenuItem m_phonetic = { - .kind = MENU_NODE_UNIMPLEMENTED, - .label = "Phonetic", - .child_count = 0, - .children = NULL, - .binding = &phonetic_binding, - .cb = NULL, - .cb_ctx = NULL, -}; +static const MenuItem m_phonetic = + MENU_ITEM_VALUE_BINDING("Phonetic", &phonetic_binding); static const MenuItem *const accessibility_children[] = { &m_macro_latch, diff --git a/openrtx/src/ui/new/ui_settings_fm.c b/openrtx/src/ui/new/ui_settings_fm.c index 7367f450a..49ae5eb48 100644 --- a/openrtx/src/ui/new/ui_settings_fm.c +++ b/openrtx/src/ui/new/ui_settings_fm.c @@ -50,6 +50,9 @@ static void fm_ctcss_mode_to_state(CtcEnMode m) state.channel.fm.rxToneEn = true; break; } + + //TODO: old UI says `*sync_rtx = true`, + // need to update the rtx thread } static int fm_ctcss_en_cb(MenuCmd cmd, void *arg, void *cb_ctx) diff --git a/openrtx/src/ui/new/ui_settings_gps.c b/openrtx/src/ui/new/ui_settings_gps.c index 2bc4f2910..daedc0176 100644 --- a/openrtx/src/ui/new/ui_settings_gps.c +++ b/openrtx/src/ui/new/ui_settings_gps.c @@ -1,4 +1,5 @@ #include +#include #include #include "ui/ui_menu.h" @@ -26,16 +27,76 @@ static MenuValueBinding gps_set_time_binding = { static const MenuItem m_gps_set_time = MENU_ITEM_VALUE_BINDING("GPS Set Time", &gps_set_time_binding); -/* -static MenuValueBinding utc_timezone_binding = { - .kind = MENU_VAL_I32, //TODO: Not implemented yet - .ptr = &state.settings.gpsSetTime, - .on_change = NULL, -}; -*/ +static int utc_timezone_cb(MenuCmd cmd, void *arg, void *cb_ctx) +{ + (void)cb_ctx; + int8_t *tz_setting = &state.settings.utc_timezone; + + switch (cmd) { + case MENU_CMD_DRAW_VALUE: { + MenuDrawValueArgs *a = (MenuDrawValueArgs *)arg; + + if (*tz_setting == 0) { + sniprintf(a->buf, a->buf_len, "UTC"); + return 1; + } + + int8_t tz_hr = *tz_setting / 2; + int8_t tz_min = *tz_setting & 1 ? 30 : 0; + char sign; + + if(*tz_setting > 0) + { + sign = '+'; + } + else + { + sign = '-'; + tz_hr *= (-1); + } + + sniprintf(a->buf, a->buf_len, "UTC%c%d:%02d", sign, tz_hr, tz_min); + return 1; + } + + case MENU_CMD_EDIT_BEGIN: + // Nothing to do + return 0; + + case MENU_CMD_EDIT_KEY: { + const UiEvent *ev = (const UiEvent *)arg; + if (!ev) return 0; + + if (ev->key == KEY_UP || ev->key == KEY_DOWN) { + bool inc = ev->key == KEY_UP; + if (inc) { + *tz_setting += 1; + } else { + *tz_setting -= 1; + } + return 1; + } + return 0; + } + + case MENU_CMD_EDIT_CANCEL: + // Optional: re-sync from some saved copy or just leave it + return 0; + + case MENU_CMD_EDIT_APPLY: + // Nothing to do, we already wrote into state + return 0; + + case MENU_CMD_SELECT: + /* TODO: Reconsider this one, doesn't make much sense. */ + return 0; + } + + return 0; +} static const MenuItem m_utc_timezone = - MENU_ITEM_UNIMPLEMENTED("UTC Timezone"); + MENU_ITEM_VALUE_CB("Timezone", utc_timezone_cb, NULL); #endif static const MenuItem *const gps_children[] = { diff --git a/openrtx/src/ui/new/ui_settings_m17.c b/openrtx/src/ui/new/ui_settings_m17.c index 3fae78034..e606a0e80 100644 --- a/openrtx/src/ui/new/ui_settings_m17.c +++ b/openrtx/src/ui/new/ui_settings_m17.c @@ -5,24 +5,21 @@ #include "ui/ui_menu.h" #include "ui/ui_menu_dsl.h" +#include "core/settings.h" #include "core/state.h" -/* TODO: Use real buffer instead of fake one */ -static char callsign_buf[10] = {0}; -static uint8_t m17_can = 0; -static bool m17_canrxcheck = false; - -static void callsign_on_change(void *ptr) { +static void on_change(void *ptr) { (void)ptr; - // Do nothing + //TODO: old UI says `*sync_rtx = true`, + // need to update the rtx thread } static MenuValueBinding callsign_binding = { .kind = MENU_VAL_STR, - .ptr = callsign_buf, - .on_change = callsign_on_change, + .ptr = &state.settings.callsign, + .on_change = on_change, .u.str = { - .max_len = sizeof(callsign_buf) - 1, + .max_len = sizeof(((settings_t *)0)->callsign) - 1, .profile = UI_STR_PROFILE_CALLSIGN, }, }; @@ -32,8 +29,8 @@ static const MenuItem m_callsign = static MenuValueBinding can_binding = { .kind = MENU_VAL_U8, - .ptr = &m17_can, - .on_change = NULL, + .ptr = &state.settings.m17_can, + .on_change = on_change, .u.u8 = { .min = 0, .max = 15, .step = 1, .wrap = true }, }; @@ -42,8 +39,8 @@ static const MenuItem m_can = static MenuValueBinding can_rx_check_binding = { .kind = MENU_VAL_BOOL, - .ptr = &m17_canrxcheck, - .on_change = NULL, + .ptr = &state.settings.m17_can_rx, + .on_change = on_change, }; static const MenuItem m_can_rx_check = diff --git a/openrtx/src/ui/new/ui_settings_radio.c b/openrtx/src/ui/new/ui_settings_radio.c new file mode 100644 index 000000000..25088f027 --- /dev/null +++ b/openrtx/src/ui/new/ui_settings_radio.c @@ -0,0 +1,28 @@ +#include "ui/ui_menu.h" +#include "ui/ui_menu_dsl.h" + +/* +static void on_change(void *ptr) { + (void)ptr; + //TODO: old UI says `*sync_rtx = true`, + // need to update the rtx thread +} +*/ + +static const MenuItem m_offset = + MENU_ITEM_UNIMPLEMENTED("Offset"); + +static const MenuItem m_direction = + MENU_ITEM_UNIMPLEMENTED("Direction"); + +static const MenuItem m_step = + MENU_ITEM_UNIMPLEMENTED("Step"); + +static const MenuItem *const radio_children[] = { + &m_offset, + &m_direction, + &m_step, +}; + +const MenuItem g_radio_settings_menu = + MENU_FOLDER_FROM_CHILDREN("Radio", radio_children); From 77d9a993c07e6602bfb6e8c821307cb899e80461 Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Thu, 13 Nov 2025 14:40:13 -0800 Subject: [PATCH 17/27] add GPS status screen --- meson.build | 1 + openrtx/src/ui/new/ui_gps.c | 202 ++++++++++++++++++++++++++++++ openrtx/src/ui/new/ui_menu.c | 37 ++++-- openrtx/src/ui/new/ui_menu_tree.c | 8 +- 4 files changed, 235 insertions(+), 13 deletions(-) create mode 100644 openrtx/src/ui/new/ui_gps.c diff --git a/meson.build b/meson.build index 8b094d686..20f62e5d6 100644 --- a/meson.build +++ b/meson.build @@ -82,6 +82,7 @@ ui_src_default = ['openrtx/src/ui/default/ui.c', 'openrtx/src/ui/default/ui_strings.c', 'openrtx/src/ui/new/ui_core.c', 'openrtx/src/ui/new/ui_compat.c', + 'openrtx/src/ui/new/ui_gps.c', 'openrtx/src/ui/new/ui_menu.c', 'openrtx/src/ui/new/ui_menu_tree.c', 'openrtx/src/ui/new/ui_settings_display.c', diff --git a/openrtx/src/ui/new/ui_gps.c b/openrtx/src/ui/new/ui_gps.c new file mode 100644 index 000000000..7e655bcde --- /dev/null +++ b/openrtx/src/ui/new/ui_gps.c @@ -0,0 +1,202 @@ +#include + +#include "ui/ui_menu.h" +#include "ui/ui_screen.h" +#include "interfaces/keyboard.h" //TODO: For keycodes + +#include "hwconfig.h" +#include "ui/ui_strings.h" +#include "core/state.h" +#include "core/graphics.h" + +static void gps_tick(UiScreen *self, const UiEvent *ev); +static void gps_draw(UiScreen *self); + +static UiScreen g_gps_screen = { + .tick = gps_tick, + .draw = gps_draw, + .ctx = NULL, +}; + +/* --- Public API --- */ +void ui_gps_screen(void) +{ + ui_push_screen(&g_gps_screen); +} + +/* --- UiScreen implementation --- */ +static void gps_tick(UiScreen *self, const UiEvent *ev) +{ + (void)self; + if (!ev) { + return; + } + + if (ev->type != UI_EVENT_KEY) + { + return; + } + + // It's a key event + + if (ev->key == KEY_ESC) + { + ui_pop_screen(); + } +} + +static void gps_draw(UiScreen *self) +{ + (void)self; + // consts borrowed from existing code for large displays + // TODO: copy over `layout_t` and helpers + const uint16_t text_v_offset = 1; + const uint16_t status_v_pad = 2; + const uint16_t top_h = 16; + const uint16_t top_pad = 4; + const uint16_t line1_h = 20; + const uint16_t line2_h = 20; + const uint16_t small_line_v_pad = 2; + const uint16_t horizontal_pad = 4; + const uint16_t top_pos_y = top_h - status_v_pad - text_v_offset; + const uint16_t line1_pos_y = top_h + top_pad + line1_h - small_line_v_pad - text_v_offset; + const uint16_t line2_pos_y = top_h + top_pad + line1_h + line2_h - small_line_v_pad - text_v_offset; + const uint16_t bottom_pad = top_pad; + const uint16_t bottom_pos_y = CONFIG_SCREEN_HEIGHT - bottom_pad - status_v_pad - text_v_offset; + const point_t top_pos = {horizontal_pad, top_pos_y}; + const point_t line1_pos = {horizontal_pad, line1_pos_y}; + const point_t line2_pos = {horizontal_pad, line2_pos_y}; + const point_t bottom_pos = {horizontal_pad, bottom_pos_y}; + const fontSize_t top_font = FONT_SIZE_8PT; + const fontSize_t line3_large_font = FONT_SIZE_16PT; + const fontSize_t bottom_font = FONT_SIZE_8PT; + const color_t color_white = {255, 255, 255, 255}; + + const char *fix_buf, *type_buf; + const char *status_msg = NULL; + gfx_clearScreen(); + + // Print "GPS" on top bar + gfx_print(top_pos, top_font, TEXT_ALIGN_CENTER, color_white, currentLanguage->gps); + point_t fix_pos = {line2_pos.x, CONFIG_SCREEN_HEIGHT * 2 / 5}; + + // Print GPS status, if no fix, hide details + if(!state.gpsDetected) + status_msg = currentLanguage->noGps; + else if (!state.settings.gps_enabled) + status_msg = currentLanguage->gpsOff; + else if (state.gps_data.fix_quality == FIX_QUALITY_NO_FIX) + status_msg = currentLanguage->noFix; + else if (state.gps_data.fix_quality == FIX_QUALITY_ESTIMATED) + status_msg = currentLanguage->fixLost; + + if (status_msg) { + gfx_print(fix_pos, line3_large_font, TEXT_ALIGN_CENTER, color_white, status_msg); + } else { + switch(state.gps_data.fix_quality) + { + case FIX_QUALITY_GPS: + fix_buf = "GPS"; + break; + case FIX_QUALITY_DGPS: + fix_buf = "DGPS"; + break; + case FIX_QUALITY_PPS: + fix_buf = "PPS"; + break; + case FIX_QUALITY_RTK: + case FIX_QUALITY_RTK_FLOAT: + fix_buf = "RTK"; + break; + default: + fix_buf = currentLanguage->error; + break; + } + + switch (state.gps_data.fix_type) + { + case FIX_TYPE_NOT_AVAIL: + type_buf = ""; + break; + case FIX_TYPE_2D: + type_buf = "2D"; + break; + case FIX_TYPE_3D: + type_buf = "3D"; + break; + default: + type_buf = currentLanguage->error; + } + + gfx_print(line1_pos, top_font, TEXT_ALIGN_LEFT, color_white, fix_buf); + gfx_print(line2_pos, top_font, TEXT_ALIGN_LEFT, color_white, type_buf); + + // Convert from signed lat/lon to unsigned + direction + int32_t latitude = abs(state.gps_data.latitude); + uint8_t latitude_int = latitude / 1000000; + int32_t latitude_dec = latitude % 1000000; + char direction_lat = (state.gps_data.latitude < 0) ? 'S' : 'N'; + + int32_t longitude = abs(state.gps_data.longitude); + uint8_t longitude_int = longitude / 1000000; + int32_t longitude_dec = longitude % 1000000; + char direction_lon = (state.gps_data.longitude < 0) ? 'W' : 'E'; + + gfx_print(line1_pos, top_font, TEXT_ALIGN_RIGHT, color_white, "%d.%.6d%c", latitude_int, latitude_dec, direction_lat); + gfx_print(line2_pos, top_font, TEXT_ALIGN_RIGHT, color_white, "%d.%.6d%c", longitude_int, longitude_dec, direction_lon); + + gfx_print(bottom_pos, bottom_font, TEXT_ALIGN_CENTER, color_white, + "S %dkm/h A %dm", + state.gps_data.speed, + state.gps_data.altitude + ); + } + + // Draw compass + point_t compass_pos = { horizontal_pad * 2, CONFIG_SCREEN_HEIGHT / 2 }; + gfx_drawGPScompass(compass_pos, + CONFIG_SCREEN_WIDTH / 9 + 2, + state.gps_data.tmg_true, + state.gps_data.fix_quality != 0 && + state.gps_data.fix_quality != 6 + ); + + // Draw satellites bar graph + point_t bar_pos = { horizontal_pad + CONFIG_SCREEN_WIDTH * 1 / 3, + CONFIG_SCREEN_HEIGHT / 2 }; + gfx_drawGPSgraph(bar_pos, + (CONFIG_SCREEN_WIDTH * 2 / 3) - horizontal_pad, + CONFIG_SCREEN_HEIGHT / 3, + state.gps_data.satellites, + state.gps_data.active_sats + ); + + gfx_render(); +} + +/* MenuItem implementation */ + +static int gps_menuitem_cb(MenuCmd cmd, void *arg, void *cb_ctx) +{ + (void)cb_ctx; + (void)arg; + + switch (cmd) { + case MENU_CMD_SELECT: { + ui_gps_screen(); + return 1; + } + default: + return 0; + } +} + +const MenuItem g_gps_menu = { + .kind = MENU_NODE_ACTION, + .label = "GPS", + .child_count = 0, + .children = NULL, + .binding = NULL, + .cb = gps_menuitem_cb, + .cb_ctx = NULL, +}; diff --git a/openrtx/src/ui/new/ui_menu.c b/openrtx/src/ui/new/ui_menu.c index ae465ee56..9476ea309 100644 --- a/openrtx/src/ui/new/ui_menu.c +++ b/openrtx/src/ui/new/ui_menu.c @@ -418,6 +418,10 @@ static void menu_tick(UiScreen *self, const UiEvent *ev) st->edit = true; st->dirty = true; } + else if (item->kind == MENU_NODE_ACTION && item->cb) { + item->cb(MENU_CMD_SELECT, NULL, item->cb_ctx); + st->dirty = true; + } break; } @@ -551,8 +555,14 @@ static void menu_draw(UiScreen *self) } gfx_print(pos, menu_font, TEXT_ALIGN_LEFT, text_color, label); + char value_buf[16] = {0}; + /* Show if node is unimplemented */ + if (item->kind == MENU_NODE_UNIMPLEMENTED) { + gfx_print(pos, menu_font, TEXT_ALIGN_RIGHT, text_color, ":("); + } + /* Value on the right if this is a VALUE node */ - if (item->kind == MENU_NODE_VALUE) { + else if (item->kind == MENU_NODE_VALUE) { /** * TODO: Evaluate value buffer size * - We don't want the printed value to take up too much room in the @@ -560,28 +570,33 @@ static void menu_draw(UiScreen *self) * - Variable length fonts make this tricky. * - Truncating could be misleading */ - char buf[16] = {0}; if (item->binding) { /* Generic value binding */ - menu_value_format((const MenuValueBinding *)item->binding, buf, sizeof buf); + menu_value_format((const MenuValueBinding *)item->binding, value_buf, sizeof value_buf); } else if (item->cb) { /* Custom value: let the callback format it */ MenuDrawValueArgs args = { - .buf = buf, - .buf_len = sizeof buf + .buf = value_buf, + .buf_len = sizeof value_buf }; item->cb(MENU_CMD_DRAW_VALUE, &args, item->cb_ctx); } - if (buf[0] != '\0') { - gfx_print(pos, menu_font, TEXT_ALIGN_RIGHT, text_color, buf); - } } - /* Show if node is unimplemented */ - if (item->kind == MENU_NODE_UNIMPLEMENTED) { - gfx_print(pos, menu_font, TEXT_ALIGN_RIGHT, text_color, ":("); + /* Show value if MENU_NODE_ACTION supports it */ + else if (item->kind == MENU_NODE_ACTION && item->cb) { + MenuDrawValueArgs args = { + .buf = value_buf, + .buf_len = sizeof value_buf + }; + item->cb(MENU_CMD_DRAW_VALUE, &args, item->cb_ctx); } + + if (value_buf[0] != '\0') { + gfx_print(pos, menu_font, TEXT_ALIGN_RIGHT, text_color, value_buf); + } + pos.y += menu_h; } diff --git a/openrtx/src/ui/new/ui_menu_tree.c b/openrtx/src/ui/new/ui_menu_tree.c index f0f606554..4a1a26e46 100644 --- a/openrtx/src/ui/new/ui_menu_tree.c +++ b/openrtx/src/ui/new/ui_menu_tree.c @@ -1,6 +1,7 @@ #include #include +#include "hwconfig.h" #include "ui/ui_menu.h" #include "ui/ui_menu_dsl.h" @@ -14,6 +15,10 @@ extern const MenuItem g_m17_settings_menu; extern const MenuItem g_fm_settings_menu; extern const MenuItem g_accessibility_settings_menu; +#ifdef CONFIG_GPS +extern const MenuItem g_gps_menu; +#endif + static const MenuItem m_default_settings = MENU_ITEM_UNIMPLEMENTED("Default Settings"); static const MenuItem *const settings_children[] = { @@ -29,7 +34,6 @@ static const MenuItem *const settings_children[] = { static const MenuItem m_banks = MENU_ITEM_UNIMPLEMENTED("Banks"); static const MenuItem m_channels = MENU_ITEM_UNIMPLEMENTED("Channels"); static const MenuItem m_contacts = MENU_ITEM_UNIMPLEMENTED("Contacts"); -static const MenuItem m_gps = MENU_ITEM_UNIMPLEMENTED("GPS"); static const MenuItem m_settings = MENU_FOLDER_FROM_CHILDREN("Settings", settings_children); static const MenuItem m_info = MENU_ITEM_UNIMPLEMENTED("Info"); static const MenuItem m_about = MENU_ITEM_UNIMPLEMENTED("About"); @@ -38,7 +42,7 @@ static const MenuItem *const root_menu_children[] = { &m_banks, &m_channels, &m_contacts, - &m_gps, + &g_gps_menu, &m_settings, &m_info, &m_about, From e68bc732619ef41d581a9b1a152d76e5167f6489 Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Thu, 13 Nov 2025 15:25:23 -0800 Subject: [PATCH 18/27] ui_menu: add a scrollbar --- openrtx/src/ui/new/ui_menu.c | 44 +++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/openrtx/src/ui/new/ui_menu.c b/openrtx/src/ui/new/ui_menu.c index 9476ea309..283e764e5 100644 --- a/openrtx/src/ui/new/ui_menu.c +++ b/openrtx/src/ui/new/ui_menu.c @@ -500,6 +500,9 @@ static void menu_draw(UiScreen *self) const color_t color_white = {255, 255, 255, 255}; const color_t color_black = {0, 0, 0, 255}; + const uint16_t scrollbar_pad = 4; + const uint16_t scrollbar_width = 2; + MenuState *st = (MenuState *)self->ctx; if(!st || !st->dirty) @@ -528,6 +531,8 @@ static void menu_draw(UiScreen *self) // Menu items point_t pos = line1_pos; + point_t pos_val = { line1_pos.x + scrollbar_pad, line1_pos.y }; + int first = frame->first; int count = menu->child_count; @@ -550,7 +555,7 @@ static void menu_draw(UiScreen *self) full_rect = false; } point_t rect_pos = {0, pos.y - menu_h + 3}; - gfx_drawRect(rect_pos, CONFIG_SCREEN_WIDTH, menu_h, color_white, full_rect); + gfx_drawRect(rect_pos, CONFIG_SCREEN_WIDTH - scrollbar_pad, menu_h, color_white, full_rect); // announceMenuItemIfNeeded() } gfx_print(pos, menu_font, TEXT_ALIGN_LEFT, text_color, label); @@ -558,7 +563,7 @@ static void menu_draw(UiScreen *self) char value_buf[16] = {0}; /* Show if node is unimplemented */ if (item->kind == MENU_NODE_UNIMPLEMENTED) { - gfx_print(pos, menu_font, TEXT_ALIGN_RIGHT, text_color, ":("); + gfx_print(pos_val, menu_font, TEXT_ALIGN_RIGHT, text_color, ":("); } /* Value on the right if this is a VALUE node */ @@ -594,12 +599,45 @@ static void menu_draw(UiScreen *self) } if (value_buf[0] != '\0') { - gfx_print(pos, menu_font, TEXT_ALIGN_RIGHT, text_color, value_buf); + gfx_print(pos_val, menu_font, TEXT_ALIGN_RIGHT, text_color, value_buf); } pos.y += menu_h; + pos_val.y = pos.y; + } + + // Scroll bar + int max_first_visible = count - MENU_VISIBLE_ROWS; + int16_t thumb_px, thumb_pos_px; + const int16_t track_px_start = 16 + 4; + const int16_t track_px = CONFIG_SCREEN_HEIGHT - track_px_start; // total height of scrollbar track in pixels + if (count <= MENU_VISIBLE_ROWS || count == 0) { + thumb_px = track_px; + } else { + int32_t num = (int32_t)track_px * (int32_t)MENU_VISIBLE_ROWS; + int32_t denom = (int32_t)count; + + // Add denom/2 for simple "round to nearest" instead of truncating + thumb_px = (int16_t)((num + denom / 2) / denom); } + if (max_first_visible < 1) { + // nothing to scroll + thumb_pos_px = 0; + } else { + int track_travel_px = track_px - thumb_px; + if (track_travel_px < 0) + track_travel_px = 0; + + int32_t num = (int32_t)track_travel_px * (int32_t)first; + int32_t denom = (int32_t)max_first_visible; + + thumb_pos_px = (int16_t)((num + denom / 2) / denom); + } + + point_t scrollbar_pos = { CONFIG_SCREEN_WIDTH - scrollbar_width, track_px_start + thumb_pos_px }; + gfx_drawRect(scrollbar_pos, scrollbar_width, thumb_px, color_white, true); + gfx_render(); st->dirty = false; } \ No newline at end of file From 6aabab495266f63d861fea4d98d92450ecc65107 Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Fri, 14 Nov 2025 12:15:21 -0800 Subject: [PATCH 19/27] ui_menu: clean up scrollbar --- openrtx/src/ui/new/ui_menu.c | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/openrtx/src/ui/new/ui_menu.c b/openrtx/src/ui/new/ui_menu.c index 283e764e5..0f6487efe 100644 --- a/openrtx/src/ui/new/ui_menu.c +++ b/openrtx/src/ui/new/ui_menu.c @@ -608,31 +608,28 @@ static void menu_draw(UiScreen *self) // Scroll bar int max_first_visible = count - MENU_VISIBLE_ROWS; - int16_t thumb_px, thumb_pos_px; - const int16_t track_px_start = 16 + 4; - const int16_t track_px = CONFIG_SCREEN_HEIGHT - track_px_start; // total height of scrollbar track in pixels - if (count <= MENU_VISIBLE_ROWS || count == 0) { + uint16_t thumb_px, thumb_pos_px; + const uint16_t track_px_start = top_h + top_pad; + const uint16_t track_px = CONFIG_SCREEN_HEIGHT - track_px_start; // total height of scrollbar track in pixels + + if (count <= 0 || max_first_visible <= 0) { + // No scrolling: everything fits, or nothing to show thumb_px = track_px; } else { int32_t num = (int32_t)track_px * (int32_t)MENU_VISIBLE_ROWS; int32_t denom = (int32_t)count; // Add denom/2 for simple "round to nearest" instead of truncating - thumb_px = (int16_t)((num + denom / 2) / denom); - } + thumb_px = (uint16_t)((num + denom / 2) / denom); - if (max_first_visible < 1) { - // nothing to scroll - thumb_pos_px = 0; - } else { int track_travel_px = track_px - thumb_px; if (track_travel_px < 0) track_travel_px = 0; - int32_t num = (int32_t)track_travel_px * (int32_t)first; - int32_t denom = (int32_t)max_first_visible; + num = (int32_t)track_travel_px * (int32_t)first; + denom = (int32_t)max_first_visible; - thumb_pos_px = (int16_t)((num + denom / 2) / denom); + thumb_pos_px = (uint16_t)((num + denom / 2) / denom); } point_t scrollbar_pos = { CONFIG_SCREEN_WIDTH - scrollbar_width, track_px_start + thumb_pos_px }; From a4fa16711271927f4b80b49c806a7b29353fae28 Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Fri, 14 Nov 2025 14:31:02 -0800 Subject: [PATCH 20/27] ui_textedit: add multi-line, character counter --- openrtx/src/ui/new/ui_textedit.c | 254 ++++++++++++++++++++++++------- 1 file changed, 199 insertions(+), 55 deletions(-) diff --git a/openrtx/src/ui/new/ui_textedit.c b/openrtx/src/ui/new/ui_textedit.c index 4e17f87e1..e885c472a 100644 --- a/openrtx/src/ui/new/ui_textedit.c +++ b/openrtx/src/ui/new/ui_textedit.c @@ -41,7 +41,8 @@ static const char *symbols_ITU_T_E161_callsign[] = "" }; -#define TEXTEDIT_SCRATCH_SIZE 256 +#define TEXTEDIT_SCRATCH_SIZE 256 +#define TEXTEDIT_CARET_PERIOD_MS 500 typedef struct { char scratch[TEXTEDIT_SCRATCH_SIZE]; //TODO: evaluate size @@ -60,6 +61,10 @@ typedef struct { const char *title; const char *const *symbols; + + // caret blink state + bool caret_visible; + long long last_blink; } TexteditState; static TexteditState g_textedit_state; @@ -84,6 +89,9 @@ static void textedit_reset(TexteditState *st) st->last_keypress = 0; st->candidate_active = false; st->dirty = true; + + st->caret_visible = true; + st->last_blink = getTick(); st->scratch[0] = '\0'; } @@ -208,10 +216,30 @@ static void textedit_tick(UiScreen *self, const UiEvent *ev) st->active_key = 0; st->active_idx = 0; st->last_keypress = 0; + + // Bring caret back immediately + st->caret_visible = true; + st->last_blink = now; st->dirty = true; } } + // 1b: caret blink when no candidate is active + if (!st->candidate_active) { + long long dt_caret = now - st->last_blink; + if (dt_caret >= TEXTEDIT_CARET_PERIOD_MS) { + st->caret_visible = !st->caret_visible; + st->last_blink = now; + st->dirty = true; + } + } else { + // Hide the caret bar while multi-tap is active + if (st->caret_visible) { + st->caret_visible = false; + st->dirty = true; + } + } + // 2: If no event, nothing else to do this tick. if (!ev) { return; @@ -251,40 +279,43 @@ static void textedit_tick(UiScreen *self, const UiEvent *ev) ui_pop_screen(); return; } - else if (key == KEY_UP || key == KEY_DOWN) + else if (key == KEY_F5) { textedit_del(st); - st->dirty = true; + // reset caret blink on edit + st->caret_visible = true; + st->last_blink = now; + st->dirty = true; return; } else if(input_isCharPressed(msg)) { textedit_keypad(st, msg); + st->dirty = true; return; } } +#define TEXTEDIT_TEXT_Y_ADJUST 12 //TODO: fix the existance of this magic number + static void textedit_draw(UiScreen *self) { - // consts borrowed from existing code for large displays - // TODO: copy over `layout_t` and helpers - const uint16_t text_v_offset = 1; - const uint16_t status_v_pad = 2; - const uint16_t top_h = 16; - const uint16_t top_pad = 4; - const uint16_t line1_h = 20; + const uint16_t text_v_offset = 1; + const uint16_t status_v_pad = 2; + const uint16_t top_h = 16; + const uint16_t top_pad = 4; + const uint16_t line1_h = 20; const uint16_t small_line_v_pad = 2; - const uint16_t horizontal_pad = 4; - const uint16_t top_pos_y = top_h - status_v_pad - text_v_offset; - const uint16_t line1_pos_y = top_h + top_pad + line1_h - small_line_v_pad - text_v_offset; - const uint16_t menu_h = 16; - const uint16_t bottom_h = 23; - const point_t top_pos = {horizontal_pad, top_pos_y}; - const point_t line1_pos = {horizontal_pad, line1_pos_y}; - const fontSize_t top_font = FONT_SIZE_8PT; - const fontSize_t text_font = FONT_SIZE_8PT; - const color_t color_white = {255, 255, 255, 255}; - const color_t color_black = {0, 0, 0, 255}; + const uint16_t horizontal_pad = 4; + const uint16_t bottom_h = 23; + const uint8_t lines_visible = 3; // how many lines fit in the box + const uint8_t line_spacing = 2; // vertical pixels between baselines + + const fontSize_t top_font = FONT_SIZE_8PT; + const fontSize_t text_font = FONT_SIZE_8PT; + + const color_t color_white = (color_t){255, 255, 255, 255}; + const color_t color_black = (color_t){0 , 0 , 0 , 255}; TexteditState *st = (TexteditState *)self->ctx; @@ -296,58 +327,171 @@ static void textedit_draw(UiScreen *self) gfx_clearScreen(); // Header + const uint16_t top_pos_y = top_h - status_v_pad - text_v_offset; + const point_t top_pos = { horizontal_pad, top_pos_y }; gfx_print(top_pos, top_font, TEXT_ALIGN_CENTER, color_white, st->title); - // Edit box - uint16_t rect_width = CONFIG_SCREEN_WIDTH - (horizontal_pad * 2); - uint16_t rect_height = (CONFIG_SCREEN_HEIGHT - (top_h + bottom_h))/2; - point_t rect_origin = {(CONFIG_SCREEN_WIDTH - rect_width) / 2, - (CONFIG_SCREEN_HEIGHT - rect_height) / 2}; + // Status bar + const uint16_t status_bar_pos_y = top_h + top_pad + 8; + const point_t status_bar_pos = { horizontal_pad, status_bar_pos_y - status_v_pad - text_v_offset }; + gfx_print(status_bar_pos, FONT_SIZE_6PT, TEXT_ALIGN_RIGHT, color_white, "%" PRIu8 "/%" PRIu8, st->len, st->max_len); + + /* --- Edit box geometry --- */ + + // Font metrics + uint8_t font_h = gfx_getFontHeight(text_font); + + // Outer rectangle margins on screen + const uint16_t rect_margin_x = 4; + const uint16_t rect_margin_top = status_bar_pos_y + top_pad; + + // Inner padding between rect border and text + const uint16_t inner_pad_x = 4; + const uint16_t inner_pad_y = 4; + + // Height for multiple lines: + // top pad = N*font + (N-1)*spacing + bottom pad + uint16_t rect_width = CONFIG_SCREEN_WIDTH - (rect_margin_x * 2); + uint16_t rect_height = + (uint16_t)(inner_pad_y * 2 + + lines_visible * font_h + + (lines_visible - 1) * line_spacing); + + point_t rect_origin = { + (int16_t)rect_margin_x, + (int16_t)rect_margin_top + }; + gfx_drawRect(rect_origin, rect_width, rect_height, color_white, false); - // Text - point_t pos = { - rect_origin.x + horizontal_pad, - rect_origin.y + top_h + 24, - }; - uint16_t baseline_y = pos.y; + // Base glyph box for the first line + int16_t base_glyph_top_line0 = (int16_t)(rect_origin.y + inner_pad_y); + + // Text drawing area + int16_t text_start_x = (int16_t)rect_origin.x + inner_pad_x; + int16_t text_area_right = (int16_t)(rect_origin.x + rect_width - inner_pad_x); + + /* --- First pass: lay out characters into lines --- */ + uint8_t char_line[TEXTEDIT_SCRATCH_SIZE]; + int16_t char_x[TEXTEDIT_SCRATCH_SIZE]; + + uint8_t total_lines = 1; + uint8_t cur_line = 0; + int16_t cur_x = text_start_x; - int caret_x0 = -1; - int caret_x1 = -1; - - // Draw all characters and track caret spans for (uint8_t i = 0; i < st->len; ++i) { char c = st->scratch[i]; - if (!c) + if (!c) { break; - + } + char tmp[2] = { c, 0 }; point_t sz = gfx_textSize(text_font, tmp); - gfx_printBuffer(pos, text_font, TEXT_ALIGN_LEFT, color_white, tmp); - - if (st->candidate_active && i == st->len - 1) { - // Underline span for candidate char - caret_x0 = pos.x; - caret_x1 = pos.x + sz.x; + // If this glyph would overflow the text area, wrap to next line + if ((cur_x + (int16_t)sz.x > text_area_right) && (cur_x != text_start_x)) { + cur_line++; + total_lines = (uint8_t)(cur_line + 1); + cur_x = text_start_x; } - pos.x += sz.x; + char_line[i] = cur_line; + char_x[i] = cur_x; + + cur_x = (int16_t)(cur_x + sz.x); } - // If no active candidate, caret is a short bar at the end - if (!st->candidate_active) { - caret_x0 = pos.x; - caret_x1 = pos.x + 6; + if (st->len == 0) { + total_lines = 1; } - if (caret_x0 >= 0 && caret_x1 > caret_x0) { - uint16_t ul_y = baseline_y + 2; - point_t ul_origin = { (uint16_t)caret_x0, ul_y }; - uint16_t ul_width = (uint16_t)(caret_x1 - caret_x0); - gfx_drawRect(ul_origin, ul_width, 1, color_white, true); + // Decide which lines to show: always show the last `lines_visible` lines + uint8_t first_visible_line = 0; + if (total_lines > lines_visible) { + first_visible_line = (uint8_t)(total_lines - lines_visible); + } + + /* --- Second pass: draw visible characters --- */ + + for (uint8_t i = 0; i < st->len; ++i) { + char c = st->scratch[i]; + if (!c) { + break; + } + + uint8_t line_idx = char_line[i]; + if (line_idx < first_visible_line) { + continue; // scrolled off the top + } + + uint8_t rel_line = (uint8_t)(line_idx - first_visible_line); + + // Base glyph box for this line + int16_t base_glyph_top = + (int16_t)(base_glyph_top_line0 + + rel_line * (font_h + line_spacing)); + + // Actual text baseline for this line + int16_t glyph_top = (int16_t)(base_glyph_top + TEXTEDIT_TEXT_Y_ADJUST); + + point_t pos = { + char_x[i], + glyph_top + }; + + char tmp[2] = { c, 0 }; + gfx_printBuffer(pos, text_font, TEXT_ALIGN_LEFT, color_white, tmp); + } + + /* --- Caret drawing --- */ + + if (!st->candidate_active && st->caret_visible) { + int caret_x = text_start_x; + uint8_t caret_line_idx = 0; + + if (st->len == 0) { + // empty buffer: caret at start of last visible line + caret_line_idx = (uint8_t)(total_lines - 1); + } else { + uint8_t last_idx = (uint8_t)(st->len - 1); + caret_line_idx = char_line[last_idx]; + + // recompute width of last char to place caret after it + char last_c[2] = { st->scratch[last_idx], 0 }; + point_t last_sz = gfx_textSize(text_font, last_c); + + caret_x = (int)(char_x[last_idx] + last_sz.x + 1); + } + + // Clamp caret line to visible region + if (caret_line_idx < first_visible_line) { + caret_line_idx = first_visible_line; + } + if (caret_line_idx >= (uint8_t)(first_visible_line + lines_visible)) { + caret_line_idx = (uint8_t)(first_visible_line + lines_visible - 1); + } + + uint8_t caret_rel_line = (uint8_t)(caret_line_idx - first_visible_line); + + // Clamp caret horizontally to inside the rectangle + int rect_left = rect_origin.x; + int rect_right = rect_origin.y + (int)rect_width - 1; + + if (caret_x < rect_left) caret_x = rect_left; + if (caret_x > rect_right) caret_x = rect_right; + + // Base glyph box for caret's line + int16_t caret_base_glyph_top = + (int16_t)(base_glyph_top_line0 + + caret_rel_line * (font_h + line_spacing)); + + uint16_t caret_height = (uint16_t)(font_h + 2); + uint16_t caret_top = (uint16_t)(caret_base_glyph_top - 1); + point_t caret_pos = { (int16_t)caret_x, (int16_t)caret_top }; + + gfx_drawRect(caret_pos, 1, caret_height, color_white, true); } gfx_render(); st->dirty = false; -} +} \ No newline at end of file From 0099fd4eaa18781dad6917afa349c2ef42e44dc5 Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Mon, 17 Nov 2025 14:03:10 -0800 Subject: [PATCH 21/27] ui_textedit: allow inserting characters, fixup safety --- openrtx/include/ui/ui_textedit.h | 2 +- openrtx/src/ui/new/ui_textedit.c | 297 +++++++++++++++++++++---------- 2 files changed, 204 insertions(+), 95 deletions(-) diff --git a/openrtx/include/ui/ui_textedit.h b/openrtx/include/ui/ui_textedit.h index a71f5eeb3..de5c901c8 100644 --- a/openrtx/include/ui/ui_textedit.h +++ b/openrtx/include/ui/ui_textedit.h @@ -25,7 +25,7 @@ typedef struct { void *user; } UiTextEditParams; -#define UI_TEXTEDIT_KEY_TIMEOUT 700 +#define UI_TEXTEDIT_KEY_TIMEOUT 700 //TODO: Make this a setting in the menu /// @brief Push the text editor UiScreen onto the screen stack void ui_open_textedit(const UiTextEditParams *p); diff --git a/openrtx/src/ui/new/ui_textedit.c b/openrtx/src/ui/new/ui_textedit.c index e885c472a..7794dbbc3 100644 --- a/openrtx/src/ui/new/ui_textedit.c +++ b/openrtx/src/ui/new/ui_textedit.c @@ -80,6 +80,16 @@ static UiScreen g_textedit_screen = { /* --- Internal helpers --- */ +static void textedit_end_multitap(TexteditState *st, long long *now) +{ + st->candidate_active = false; + st->active_key = 0; + st->active_idx = 0; + st->last_keypress = 0; + st->last_blink = *now; + st->caret_visible = true; +} + static void textedit_reset(TexteditState *st) { st->cursor = 0; @@ -96,6 +106,33 @@ static void textedit_reset(TexteditState *st) st->scratch[0] = '\0'; } +static bool textedit_insert_char_at_cursor(TexteditState *st, char ch) +{ + if (st->len >= st->max_len) { + return false; // no room + } + + // Clamp cursor to valid range + if (st->cursor > st->len) { + st->cursor = st->len; + } + + // Tail length: characters from cursor to end excluding '\0' + size_t tail_len = (size_t)(st->len - st->cursor); + + // Move tail + '\0' right by one byte + memmove(&st->scratch[st->cursor + 1], + &st->scratch[st->cursor], + tail_len + 1 + ); + + st->scratch[st->cursor] = ch; + st->cursor++; + st->len++; + + return true; +} + static void textedit_keypad(TexteditState *st, kbd_msg_t msg) { long long now = getTick(); @@ -111,30 +148,31 @@ static void textedit_keypad(TexteditState *st, kbd_msg_t msg) bool timed_out = st->candidate_active && ((now - st->last_keypress) >= UI_TEXTEDIT_KEY_TIMEOUT); - // If we're at max len and we'd need to advance cursor, bail if (!same_key || timed_out) { // Changing key or previous candidate expired. - // In either case, we want a new candidate at the end. - if (st->len >= st->max_len) { + // In either case, we want a new candidate at the caret. + if (!textedit_insert_char_at_cursor(st, set[0])) { // Can't append + textedit_end_multitap(st, &now); return; } st->candidate_active = true; st->active_key = num_key; st->active_idx = 0; - - // Append new candidate char at end - st->scratch[st->len] = set[0]; - st->len++; - st->scratch[st->len] = '\0'; } else { + // Continuing the same key within timeout + // - cycle the candidate character in place + // - candidate is always at cursor-1 while multi-tap is active + if (st->cursor == 0) { + textedit_end_multitap(st, &now); + } + // same key, within timeout: cycle candidate - // The candidate is at `st->len - 1` + uint8_t candidate_idx = (uint8_t)(st->cursor - 1); + st->active_idx = (uint8_t)((st->active_idx + 1u) % num_symbols); - if (st->len > 0) { - st->scratch[st->len - 1] = set[st->active_idx]; - } + st->scratch[candidate_idx] = set[st->active_idx]; } //vp_announceInputChar(st->scratch[st->len - 1]); @@ -143,20 +181,32 @@ static void textedit_keypad(TexteditState *st, kbd_msg_t msg) st->dirty = true; } -static void textedit_del(TexteditState *st) +static void textedit_backspace(TexteditState *st) { - if (st->len == 0) { + if (st->len == 0 || st->cursor == 0) { // nothing to delete st->candidate_active = false; st->active_key = 0; - st->active_key = 0; + st->active_idx = 0; st->last_keypress = 0; return; } - // The last character is always removed, whether is was a candidate or not + // Index of the character we're removing + uint8_t remove_idx = (uint8_t)(st->cursor - 1); + + // Number of characters after the cursor (not counting the removed one) + // e.g.: len=5, cursor=3 -> removing index 2 -> characters at 3,4 (2 chars) + size_t tail_len = (size_t)(st->len - st->cursor); + + // Move tail (plus '\0') left by one + memmove(&st->scratch[remove_idx], + &st->scratch[st->cursor], + tail_len + 1 + ); + + st->cursor--; st->len--; - st->scratch[st->len] = '\0'; // Deletion always ends the multi-tap sequence st->candidate_active = false; @@ -175,11 +225,17 @@ void ui_open_textedit(const UiTextEditParams *p) g_textedit_state.title = p->title ? p->title : ""; g_textedit_state.max_len = p->max_len; + if (g_textedit_state.max_len >= TEXTEDIT_SCRATCH_SIZE) { + g_textedit_state.max_len = TEXTEDIT_SCRATCH_SIZE - 1; + } + g_textedit_state.target = p->buf; strncpy(g_textedit_state.scratch, p->buf, g_textedit_state.max_len); g_textedit_state.scratch[g_textedit_state.max_len] = '\0'; - g_textedit_state.len = strlen(g_textedit_state.scratch); + + g_textedit_state.len = (uint8_t)strlen(g_textedit_state.scratch); + g_textedit_state.cursor = g_textedit_state.len; switch (p->profile) { @@ -212,14 +268,7 @@ static void textedit_tick(UiScreen *self, const UiEvent *ev) if (dt >= UI_TEXTEDIT_KEY_TIMEOUT) { // Commit the candidate visually: keep it in scratch, // but stop treating it as "live" multi-tap. - st->candidate_active = false; - st->active_key = 0; - st->active_idx = 0; - st->last_keypress = 0; - - // Bring caret back immediately - st->caret_visible = true; - st->last_blink = now; + textedit_end_multitap(st, &now); st->dirty = true; } } @@ -275,19 +324,46 @@ static void textedit_tick(UiScreen *self, const UiEvent *ev) } else if(key == KEY_ENTER) { - strncpy(st->target, st->scratch, st->max_len); + // Enforce invariant + if (st->len > st->max_len) { + st->len = st->max_len; + } + + // Copy string including '\0' + memcpy(st->target, st->scratch, (size_t)st->len + 1); + ui_pop_screen(); return; } else if (key == KEY_F5) { - textedit_del(st); + textedit_backspace(st); // reset caret blink on edit st->caret_visible = true; st->last_blink = now; st->dirty = true; return; } + else if (key == KEY_DOWN) // cursor left + { + if (st->cursor > 0) { + st->cursor--; + st->dirty = true; + } + // moving the curosr commits multi-tap + textedit_end_multitap(st, &now); + return; + } + else if (key == KEY_UP) // cursor right + { + if (st->cursor < st->len) { + st->cursor++; + st->dirty = true; + } + // moving the curosr commits multi-tap + textedit_end_multitap(st, &now); + return; + } else if(input_isCharPressed(msg)) { textedit_keypad(st, msg); @@ -308,14 +384,13 @@ static void textedit_draw(UiScreen *self) const uint16_t small_line_v_pad = 2; const uint16_t horizontal_pad = 4; const uint16_t bottom_h = 23; - const uint8_t lines_visible = 3; // how many lines fit in the box + const uint8_t lines_visible = 4; // how many lines fit in the box const uint8_t line_spacing = 2; // vertical pixels between baselines const fontSize_t top_font = FONT_SIZE_8PT; const fontSize_t text_font = FONT_SIZE_8PT; const color_t color_white = (color_t){255, 255, 255, 255}; - const color_t color_black = (color_t){0 , 0 , 0 , 255}; TexteditState *st = (TexteditState *)self->ctx; @@ -336,6 +411,11 @@ static void textedit_draw(UiScreen *self) const point_t status_bar_pos = { horizontal_pad, status_bar_pos_y - status_v_pad - text_v_offset }; gfx_print(status_bar_pos, FONT_SIZE_6PT, TEXT_ALIGN_RIGHT, color_white, "%" PRIu8 "/%" PRIu8, st->len, st->max_len); + // Softkey labels + const uint16_t softkey_pos_y = CONFIG_SCREEN_HEIGHT - status_v_pad; + const point_t softkey_pos = { horizontal_pad, softkey_pos_y }; + gfx_print(softkey_pos, FONT_SIZE_6PT, TEXT_ALIGN_RIGHT, color_white, "Delete"); + /* --- Edit box geometry --- */ // Font metrics @@ -371,98 +451,126 @@ static void textedit_draw(UiScreen *self) int16_t text_start_x = (int16_t)rect_origin.x + inner_pad_x; int16_t text_area_right = (int16_t)(rect_origin.x + rect_width - inner_pad_x); - /* --- First pass: lay out characters into lines --- */ - uint8_t char_line[TEXTEDIT_SCRATCH_SIZE]; - int16_t char_x[TEXTEDIT_SCRATCH_SIZE]; + /* --- First pass: lay out to find total_lines and caret position --- */ uint8_t total_lines = 1; uint8_t cur_line = 0; int16_t cur_x = text_start_x; - for (uint8_t i = 0; i < st->len; ++i) { - char c = st->scratch[i]; - if (!c) { - break; - } + uint8_t caret_line_idx = 0; + int16_t caret_x_full = text_start_x; + bool caret_set = false; - char tmp[2] = { c, 0 }; - point_t sz = gfx_textSize(text_font, tmp); - - // If this glyph would overflow the text area, wrap to next line - if ((cur_x + (int16_t)sz.x > text_area_right) && (cur_x != text_start_x)) { - cur_line++; - total_lines = (uint8_t)(cur_line + 1); - cur_x = text_start_x; + if (st->len == 0) { + // Empty buffer: caret at start of first line + total_lines = 1; + caret_line_idx = 0; + caret_x_full = text_start_x; + caret_set = true; + } else { + for (uint8_t i = 0; i < st->len; ++i) { + char c = st->scratch[i]; + if (!c) { + break; + } + + char tmp[2] = { c, 0 }; + point_t sz = gfx_textSize(text_font, tmp); + + // If this glyph would overflow the text area, wrap to next line + if ((cur_x + (int16_t)sz.x > text_area_right) && (cur_x != text_start_x)) { + cur_line++; + total_lines = (uint8_t)(cur_line + 1); + cur_x = text_start_x; + } + + // If cursor is before the first character and not yet set + if (!caret_set && st->cursor == 0 && i == 0) { + caret_line_idx = cur_line; + caret_x_full = text_start_x; + caret_set = true; + } + + // If cursor is after this character (typical case cursor = i+1) + if (!caret_set && st->cursor == (uint8_t)(i+1)) { + caret_line_idx = cur_line; + caret_x_full = (int16_t)(cur_x + sz.x); // end of glyph + caret_set = true; + } + + cur_x = (int16_t)(cur_x + sz.x); } - char_line[i] = cur_line; - char_x[i] = cur_x; - - cur_x = (int16_t)(cur_x + sz.x); - } - - if (st->len == 0) { - total_lines = 1; + // Cursor at end of text and never set in the loop (e.g. len == cursor but last + // character was '\0' short-circuiting): fall back to end-of-line. + if (!caret_set) { + caret_line_idx = cur_line; + caret_x_full = cur_x; + caret_set = true; + } } - // Decide which lines to show: always show the last `lines_visible` lines + /* --- Decide which lines to show so the caret is visible --- */ uint8_t first_visible_line = 0; - if (total_lines > lines_visible) { - first_visible_line = (uint8_t)(total_lines - lines_visible); + if (total_lines <= lines_visible) { + first_visible_line = 0; + } else { + // Try to keep the caret on the bottom visible line + int min_start = (int)caret_line_idx - (int)(lines_visible - 1); + if (min_start < 0) + min_start = 0; + if (min_start > (int)total_lines - (int)lines_visible) + min_start = (int)total_lines - (int)lines_visible; + first_visible_line = (uint8_t)min_start; } /* --- Second pass: draw visible characters --- */ + cur_line = 0; + cur_x = text_start_x; + for (uint8_t i = 0; i < st->len; ++i) { char c = st->scratch[i]; if (!c) { break; } - uint8_t line_idx = char_line[i]; - if (line_idx < first_visible_line) { - continue; // scrolled off the top - } - - uint8_t rel_line = (uint8_t)(line_idx - first_visible_line); + char tmp[2] = { c, 0 }; + point_t sz = gfx_textSize(text_font, tmp); - // Base glyph box for this line - int16_t base_glyph_top = - (int16_t)(base_glyph_top_line0 + - rel_line * (font_h + line_spacing)); - - // Actual text baseline for this line - int16_t glyph_top = (int16_t)(base_glyph_top + TEXTEDIT_TEXT_Y_ADJUST); + // If this glyph would overflow the text area, wrap to next line + if ((cur_x + (int16_t)sz.x > text_area_right) && (cur_x != text_start_x)) { + cur_line++; + cur_x = text_start_x; + } - point_t pos = { - char_x[i], - glyph_top - }; + if (cur_line >= first_visible_line && + cur_line < (uint8_t)(first_visible_line + lines_visible)) { + + uint8_t rel_line = (uint8_t)(cur_line - first_visible_line); + + // Base glyph box for this line + int16_t base_glyph_top = + (int16_t)(base_glyph_top_line0 + + rel_line * (font_h + line_spacing)); + + // Actual text baseline for this line + int16_t glyph_top = (int16_t)(base_glyph_top + TEXTEDIT_TEXT_Y_ADJUST); + + point_t pos = { + cur_x, + glyph_top + }; + + gfx_printBuffer(pos, text_font, TEXT_ALIGN_LEFT, color_white, tmp); + } - char tmp[2] = { c, 0 }; - gfx_printBuffer(pos, text_font, TEXT_ALIGN_LEFT, color_white, tmp); + cur_x = (int16_t)(cur_x + sz.x); } /* --- Caret drawing --- */ if (!st->candidate_active && st->caret_visible) { - int caret_x = text_start_x; - uint8_t caret_line_idx = 0; - - if (st->len == 0) { - // empty buffer: caret at start of last visible line - caret_line_idx = (uint8_t)(total_lines - 1); - } else { - uint8_t last_idx = (uint8_t)(st->len - 1); - caret_line_idx = char_line[last_idx]; - - // recompute width of last char to place caret after it - char last_c[2] = { st->scratch[last_idx], 0 }; - point_t last_sz = gfx_textSize(text_font, last_c); - - caret_x = (int)(char_x[last_idx] + last_sz.x + 1); - } - // Clamp caret line to visible region if (caret_line_idx < first_visible_line) { caret_line_idx = first_visible_line; @@ -474,6 +582,7 @@ static void textedit_draw(UiScreen *self) uint8_t caret_rel_line = (uint8_t)(caret_line_idx - first_visible_line); // Clamp caret horizontally to inside the rectangle + int caret_x = caret_x_full; int rect_left = rect_origin.x; int rect_right = rect_origin.y + (int)rect_width - 1; @@ -494,4 +603,4 @@ static void textedit_draw(UiScreen *self) gfx_render(); st->dirty = false; -} \ No newline at end of file +} From 2f10bacbde2d655f81b0e4390e560465d29d0098 Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Mon, 17 Nov 2025 14:04:11 -0800 Subject: [PATCH 22/27] ui_menu: fix uninitialized variable --- openrtx/src/ui/new/ui_menu.c | 1 + 1 file changed, 1 insertion(+) diff --git a/openrtx/src/ui/new/ui_menu.c b/openrtx/src/ui/new/ui_menu.c index 0f6487efe..a12789aee 100644 --- a/openrtx/src/ui/new/ui_menu.c +++ b/openrtx/src/ui/new/ui_menu.c @@ -615,6 +615,7 @@ static void menu_draw(UiScreen *self) if (count <= 0 || max_first_visible <= 0) { // No scrolling: everything fits, or nothing to show thumb_px = track_px; + thumb_pos_px = 0; } else { int32_t num = (int32_t)track_px * (int32_t)MENU_VISIBLE_ROWS; int32_t denom = (int32_t)count; From 24030906c7a40e4ffbd87716f6f612965e081650 Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Tue, 18 Nov 2025 16:18:40 -0800 Subject: [PATCH 23/27] ui_settings: cleanup, add module17 stub --- openrtx/src/ui/new/ui_settings_display.c | 13 ++++++++----- openrtx/src/ui/new/ui_settings_gps.c | 9 ++++----- openrtx/src/ui/new/ui_settings_module17.c | 23 +++++++++++++++++++++++ 3 files changed, 35 insertions(+), 10 deletions(-) create mode 100644 openrtx/src/ui/new/ui_settings_module17.c diff --git a/openrtx/src/ui/new/ui_settings_display.c b/openrtx/src/ui/new/ui_settings_display.c index f570d79ba..5d0b6ab7a 100644 --- a/openrtx/src/ui/new/ui_settings_display.c +++ b/openrtx/src/ui/new/ui_settings_display.c @@ -21,8 +21,7 @@ static MenuValueBinding brightness_binding = { static const MenuItem m_brightness = MENU_ITEM_VALUE_BINDING("Brightness", &brightness_binding); - -#endif +#endif // CONFIG_SCREEN_BRIGHTNESS #ifdef CONFIG_SCREEN_CONTRAST static void contrast_on_change(void *ptr) { @@ -39,7 +38,7 @@ static MenuValueBinding contrast_binding = { static const MenuItem m_contrast = MENU_ITEM_VALUE_BINDING("Contrast", &contrast_binding); -#endif +#endif // CONFIG_SCREEN_CONTRAST static const char *timer_names[] = { @@ -71,6 +70,7 @@ static MenuValueBinding timer_binding = { static const MenuItem m_timer = MENU_ITEM_VALUE_BINDING("Timer", &timer_binding); +#ifndef CONFIG_BAT_NONE static MenuValueBinding battery_icon_binding = { .kind = MENU_VAL_BOOL, .ptr = &state.settings.showBatteryIcon, @@ -79,17 +79,20 @@ static MenuValueBinding battery_icon_binding = { static const MenuItem m_battery_icon = MENU_ITEM_VALUE_BINDING("Battery Icon", &battery_icon_binding); +#endif // CONFIG_BAT_NONE /* Pointer array of children for this folder */ static const MenuItem *const display_children[] = { #ifdef CONFIG_SCREEN_BRIGHTNESS &m_brightness, -#endif +#endif // CONFIG_SCREEN_BRIGHTNESS #ifdef CONFIG_SCREEN_CONTRAST &m_contrast, -#endif +#endif // CONFIG_SCREEN_CONTRAST &m_timer, +#ifndef CONFIG_BAT_NONE &m_battery_icon, +#endif // CONFIG_BAT_NONE }; const MenuItem g_display_settings_menu = diff --git a/openrtx/src/ui/new/ui_settings_gps.c b/openrtx/src/ui/new/ui_settings_gps.c index daedc0176..397d5f229 100644 --- a/openrtx/src/ui/new/ui_settings_gps.c +++ b/openrtx/src/ui/new/ui_settings_gps.c @@ -15,7 +15,6 @@ static MenuValueBinding gps_en_binding = { static const MenuItem m_gps_en = MENU_ITEM_VALUE_BINDING("GPS Enabled", &gps_en_binding); -#endif #ifdef CONFIG_RTC static MenuValueBinding gps_set_time_binding = { @@ -97,17 +96,17 @@ static int utc_timezone_cb(MenuCmd cmd, void *arg, void *cb_ctx) static const MenuItem m_utc_timezone = MENU_ITEM_VALUE_CB("Timezone", utc_timezone_cb, NULL); -#endif +#endif // CONFIG_RTC static const MenuItem *const gps_children[] = { -#ifdef CONFIG_GPS &m_gps_en, -#endif #ifdef CONFIG_RTC &m_gps_set_time, &m_utc_timezone, -#endif +#endif // CONFIG_RTC }; const MenuItem g_gps_settings_menu = MENU_FOLDER_FROM_CHILDREN("GPS", gps_children); + +#endif // CONFIG_GPS diff --git a/openrtx/src/ui/new/ui_settings_module17.c b/openrtx/src/ui/new/ui_settings_module17.c new file mode 100644 index 000000000..622b671b5 --- /dev/null +++ b/openrtx/src/ui/new/ui_settings_module17.c @@ -0,0 +1,23 @@ +#include "ui/ui_menu.h" +#include "ui/ui_menu_dsl.h" + +static const MenuItem m_mic_gain = MENU_ITEM_UNIMPLEMENTED("Mic Gain"); +static const MenuItem m_ptt_in = MENU_ITEM_UNIMPLEMENTED("PTT In"); +static const MenuItem m_ptt_out = MENU_ITEM_UNIMPLEMENTED("PTT Out"); +static const MenuItem m_tx_phase = MENU_ITEM_UNIMPLEMENTED("TX Phase"); +static const MenuItem m_rx_phase = MENU_ITEM_UNIMPLEMENTED("RX Phase"); +static const MenuItem m_tx_softpot = MENU_ITEM_UNIMPLEMENTED("TX Softpot"); +static const MenuItem m_rx_softpot = MENU_ITEM_UNIMPLEMENTED("RX Softpot"); + +static const MenuItem *const module17_children[] = { + &m_mic_gain, + &m_ptt_in, + &m_ptt_out, + &m_tx_phase, + &m_rx_phase, + &m_tx_softpot, + &m_rx_softpot, +}; + +const MenuItem g_module17_settings_menu = + MENU_FOLDER_FROM_CHILDREN("Module 17", module17_children); From 3eaa50f2e2257b67715c0066a0bcec9658782efa Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Tue, 18 Nov 2025 16:19:41 -0800 Subject: [PATCH 24/27] graphics: introduce font metrics query, drawTextRect function --- openrtx/include/core/graphics.h | 38 ++++++ openrtx/src/core/graphics.c | 199 +++++++++++++++++++++++++++++--- 2 files changed, 218 insertions(+), 19 deletions(-) diff --git a/openrtx/include/core/graphics.h b/openrtx/include/core/graphics.h index a3ae4a636..c54a4fe94 100644 --- a/openrtx/include/core/graphics.h +++ b/openrtx/include/core/graphics.h @@ -54,6 +54,17 @@ typedef struct point_t int16_t y; } point_t; +/** + * Structure that represents the X,Y coordinates, width and height of a rectangle. + */ +typedef struct +{ + int16_t x; + int16_t y; + uint16_t w; + uint16_t h; +} gfx_rect_t; + /** * Structure that represents a single color in the RGB 8 bit per channel format */ @@ -92,6 +103,19 @@ typedef enum TEXT_ALIGN_RIGHT } textAlign_t; +typedef enum +{ + TEXT_VALIGN_TOP = 0, + TEXT_VALIGN_MIDDLE, + TEXT_VALIGN_BOTTOM +} textValign_t; + +typedef struct +{ + uint8_t ascent; // pixels above baseline + uint8_t descent; // pixels below baseline + uint8_t line_height; // ascent + descent +} gfx_font_metrics_t; /** * This function calls the correspondent method of the low level interface display.h @@ -186,6 +210,8 @@ void gfx_drawVLine(int16_t x, uint16_t width, color_t color); void gfx_drawRect(point_t start, uint16_t width, uint16_t height, color_t color, bool fill); +void gfx_drawRectRect(gfx_rect_t rect, color_t color, bool fill); + /** * Draw the outline of a circle of specified radius and color. * @param start: screen position of the center of the circle, in pixels @@ -201,6 +227,11 @@ void gfx_drawCircle(point_t start, uint16_t r, color_t color); */ uint8_t gfx_getFontHeight(fontSize_t size); +/** + * Get font metrics for a given font + */ +void gfx_getFontMetrics(fontSize_t size, gfx_font_metrics_t *out); + /** * Measure text * @param size: text font size, defined as enum. @@ -251,6 +282,13 @@ point_t gfx_printLine(uint8_t cur, uint8_t tot, int16_t startY, int16_t endY, int16_t startX, fontSize_t size, textAlign_t alignment, color_t color, const char* fmt, ... ); +point_t gfx_drawTextRect(gfx_rect_t rect, + fontSize_t size, + textAlign_t halign, + textValign_t valign, + color_t color, + const char *buf); + /** * Prints an error message surrounded by a red box on the screen. * @param text: text to print. diff --git a/openrtx/src/core/graphics.c b/openrtx/src/core/graphics.c index 175191a53..8dc83d11d 100644 --- a/openrtx/src/core/graphics.c +++ b/openrtx/src/core/graphics.c @@ -77,6 +77,8 @@ static const GFXfont fonts[] = { TomThumb, // 5pt Symbols8pt7b // 8pt }; +static gfx_font_metrics_t font_metrics[FONT_SIZE_NUM]; + #ifdef CONFIG_PIX_FMT_RGB565 /* This specialization is meant for an RGB565 little endian pixel format. @@ -147,6 +149,29 @@ static PIXEL_T __attribute__((section(".bss.fb"))) framebuffer[FB_SIZE]; #endif static char text[32]; +static void gfx_init_font_metrics(void) +{ + for (int i = 0; i < FONT_SIZE_NUM; ++i) { + const GFXfont *f = &fonts[i]; + + int16_t ascent = 0; + int16_t descent = 0; + + for (uint16_t c = f->first; c <= f->last; ++c) { + const GFXglyph *g = &f->glyph[c - f->first]; + + int16_t top = g->yOffset; + int16_t bottom = g->yOffset + g->height; + + if (-top > ascent) ascent = -top; + if (bottom > descent) descent = bottom; + } + + font_metrics[i].ascent = (uint8_t)ascent; + font_metrics[i].descent = (uint8_t)descent; + font_metrics[i].line_height = (uint8_t)(ascent + descent); + } +} void gfx_init() { @@ -154,6 +179,9 @@ void gfx_init() // Clear text buffer memset(text, 0x00, 32); + + // Compute font metrics + gfx_init_font_metrics(); } void gfx_terminate() @@ -319,6 +347,12 @@ void gfx_drawRect(point_t start, uint16_t width, uint16_t height, color_t color, } } +void gfx_drawRectRect(gfx_rect_t rect, color_t color, bool fill) +{ + point_t start = (point_t){ rect.x, rect.y }; + gfx_drawRect(start, rect.w, rect.h, color, fill); +} + void gfx_drawCircle(point_t start, uint16_t r, color_t color) { int16_t f = 1 - r; @@ -434,33 +468,37 @@ static inline uint16_t get_reset_x(textAlign_t alignment, uint16_t line_size, uint8_t gfx_getFontHeight(fontSize_t size) { - GFXfont f = fonts[size]; - GFXglyph glyph = f.glyph['|' - f.first]; - return glyph.height; + gfx_font_metrics_t m; + gfx_getFontMetrics(size, &m); + return m.line_height; } -point_t gfx_textSize(fontSize_t size, const char *buf) +void gfx_getFontMetrics(fontSize_t size, gfx_font_metrics_t *out) { - GFXfont f = fonts[size]; - size_t len = strlen(buf); - uint16_t w = 0; - uint16_t h = 0; + if (!out) return; + *out = font_metrics[size]; +} - for (size_t i = 0; i < len; ++i) - { - char c = buf[i]; +point_t gfx_textSize(fontSize_t size, const char *buf) +{ + const GFXfont *f = &fonts[size]; + const gfx_font_metrics_t *fm = &font_metrics[size]; - if (c == '\n' || c == '\r') break; - if (c < f.first || c > f.last) continue; + uint16_t w = 0; + size_t len = strlen(buf); - GFXglyph glyph = f.glyph[c - f.first]; - w += glyph.xAdvance; - if (glyph.height > h) { - h = glyph.height; - } + for (size_t i = 0; i < len; ++i) { + unsigned char c = (unsigned char)buf[i]; + if (c == '\n' || c == '\r') + break; + if (c < f->first || c > f->last) + continue; + + const GFXglyph *g = &f->glyph[c - f->first]; + w += g->xAdvance; } - point_t sz = { w, h }; + point_t sz = { (int16_t)w, (int16_t)fm->line_height }; return sz; } @@ -600,6 +638,129 @@ point_t gfx_printLine(uint8_t cur, uint8_t tot, int16_t startY, int16_t endY, return gfx_printBuffer(start, size, alignment, color, text); } +// Draw a single line of text at a given baseline, no wrapping, no alignment. +// Returns width actually drawn. +static uint16_t gfx_drawTextBaseline(point_t baseline, + const GFXfont *f, + color_t color, + const char *buf) +{ + size_t len = strlen(buf); + int16_t x = baseline.x; + int16_t y = baseline.y; + uint16_t total_w = 0; + + for (size_t i = 0; i < len; ++i) { + unsigned char c = (unsigned char)buf[i]; + if (c == '\n' || c == '\r') + break; + if (c < f->first || c > f->last) + continue; + + const GFXglyph *g = &f->glyph[c - f->first]; + const uint8_t *bitmap = f->bitmap; + + uint16_t bo = g->bitmapOffset; + uint8_t w = g->width; + uint8_t h = g->height; + int8_t xo = g->xOffset; + int8_t yo = g->yOffset; + + uint8_t xx, yy, bits = 0, bit = 0; + + // Blit glyph bitmap + for (yy = 0; yy < h; ++yy) { + for (xx = 0; xx < w; ++xx) { + if (!(bit++ & 7)) { + bits = bitmap[bo++]; + } + + if (bits & 0x80) { + int16_t px = x + xo + xx; + int16_t py = y + yo + yy; + if (px >= 0 && px < CONFIG_SCREEN_WIDTH && + py >= 0 && py < CONFIG_SCREEN_HEIGHT) { + + point_t pos = { px, py }; + gfx_setPixel(pos, color); + } + } + + bits <<= 1; + } + } + + x += g->xAdvance; + total_w += g->xAdvance; + } + + return total_w; +} + +point_t gfx_drawTextRect(gfx_rect_t rect, + fontSize_t size, + textAlign_t halign, + textValign_t valign, + color_t color, + const char *buf) +{ + const GFXfont *f = &fonts[size]; + const gfx_font_metrics_t *fm = &font_metrics[size]; + + // Measure text + point_t ext = gfx_textSize(size, buf); + uint16_t text_w = ext.x; + uint16_t text_h = ext.y; + + // Horizontal origin (top-left of text box) + int16_t origin_x; + switch (halign) { + case TEXT_ALIGN_LEFT: + origin_x = rect.x; + break; + case TEXT_ALIGN_CENTER: + origin_x = rect.x + (int16_t)(rect.w - text_w) / 2; + break; + case TEXT_ALIGN_RIGHT: + origin_x = rect.x + (int16_t)rect.w - (int16_t)text_w; + break; + default: + origin_x = rect.x; + break; + } + + // Vertical: compute baseline y + int16_t baseline_y; + switch (valign) { + case TEXT_VALIGN_TOP: { + // top of line at rect.y; baseline is top + ascent + baseline_y = rect.y + fm->ascent; + break; + } + case TEXT_VALIGN_MIDDLE: { + int16_t top = rect.y + (int16_t)(rect.h - text_h) / 2; + baseline_y = top + fm->ascent; + break; + } + case TEXT_VALIGN_BOTTOM: { + // bottom at rect.y + rect.h; baseline is bottom - descent + int16_t bottom = rect.y + (int16_t)rect.h; + baseline_y = bottom - fm->descent; + break; + } + default: + baseline_y = rect.y + fm->ascent; + break; + } + + point_t baseline = { origin_x, baseline_y }; + uint16_t drawn_w = gfx_drawTextBaseline(baseline, f, color, buf); + + // Return bounding size + point_t out = { (int16_t)drawn_w, (int16_t)text_h }; + return out; +} + // Print an error message to the center of the screen, surronded by a red (when possible) box void gfx_printError(const char *text, fontSize_t size) { From 8e303dd6f675742912fae26af6349635066224c8 Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Tue, 18 Nov 2025 18:26:29 -0800 Subject: [PATCH 25/27] ui_new: introduce newlayout_t struct --- meson.build | 1 + openrtx/include/ui/ui_new.h | 37 +++++++++- openrtx/src/ui/new/ui.c | 142 +++++++++++++++++++++++------------- 3 files changed, 127 insertions(+), 53 deletions(-) diff --git a/meson.build b/meson.build index 20f62e5d6..f00755a84 100644 --- a/meson.build +++ b/meson.build @@ -80,6 +80,7 @@ ui_src_default = ['openrtx/src/ui/default/ui.c', 'openrtx/src/ui/default/ui_main.c', 'openrtx/src/ui/default/ui_menu.c', 'openrtx/src/ui/default/ui_strings.c', + 'openrtx/src/ui/new/ui.c', 'openrtx/src/ui/new/ui_core.c', 'openrtx/src/ui/new/ui_compat.c', 'openrtx/src/ui/new/ui_gps.c', diff --git a/openrtx/include/ui/ui_new.h b/openrtx/include/ui/ui_new.h index 6c59f90d6..a7143cf58 100644 --- a/openrtx/include/ui/ui_new.h +++ b/openrtx/include/ui/ui_new.h @@ -1,8 +1,43 @@ #ifndef UI_NEW_H #define UI_NEW_H +#include "core/graphics.h" #include "core/input.h" #include "ui/ui_screen.h" bool ui_build_event_from_kbd(const kbd_msg_t *kbd, UiEvent *ev); -#endif \ No newline at end of file + +/* + * ┌─────────────────────────┐ + * │ status_bar_height │ status_bar_font + * ├─────────────────────────┤ <- user_start_y (Start coordinate) + * │ ┆ │ + * │ user_screen_size │ User available screen size (Y-dimension) + * │ ┆ │ + * │ ┆ │ + * │ ┆ │ + * │ ┆ │ + * │ ┆ │ + * └─────────────────────────┘ + */ +typedef struct newlayout_t +{ + uint16_t status_bar_height; // Height of the status bar area + point_t status_bar_pos; // Status bar text position + fontSize_t status_bar_font; // Status bar text font + + uint16_t user_start_y; // Start of user screen area + uint16_t user_screen_size; // User available screen size (Y-dimension) + + fontSize_t small_font; // Small font size + fontSize_t text_font; // Typical font size + fontSize_t large_font; // Large font size + + uint16_t horizontal_pad; +} newlayout_t; + +extern const newlayout_t *ui_layout; + +void ui_drawStatusBar(const char *text); + +#endif // UI_NEW_H \ No newline at end of file diff --git a/openrtx/src/ui/new/ui.c b/openrtx/src/ui/new/ui.c index 98bca7fc2..edd39dde1 100644 --- a/openrtx/src/ui/new/ui.c +++ b/openrtx/src/ui/new/ui.c @@ -1,54 +1,92 @@ +#include "core/graphics.h" #include "ui/ui_new.h" +#include "hwconfig.h" -/*** - -Current menu structure: - -- Banks - - All channels - - (banks...) -- Channels - - (channels...) -- Contacts - - (contacts...) -- GPS - - GPS UI Screen -- Settings - - Display - - Brightness (0-100,5) - - Timer (Enum) - - Battery Icon (Bool) - - GPS - - GPS Enabled (Bool) - - GPS Set Time (Bool) - - UTC Timezone (-Inf-Inf,0.5) - - Radio - - Offset (Input) - - Direction (Enum) - - Step (Enum) - - M17 - - Callsign (Input) - - CAN (0-15,1) - - CAN RX Check (Bool) - - FM - - CTCSS Tone (Enum) - - CTCSS En. (Enum) - - Accessibility - - Macro Latch (Bool) - - Voice (Bool) - - Phonetic (Bool) - - Default Settings - - Are you sure UI -- Info - - (commit ver string) - - Bat. Voltage - - Bat. Charge - - RSSI - - Used heap - - Band - - VHF - - UHF - - Hw Version -- About - - About Screen UI - */ \ No newline at end of file +// Calculate UI layout depending on vertical resolution +// Tytera MD380, MD-UV380 +#if CONFIG_SCREEN_HEIGHT > 127 + +// Height and padding shown in diagram at beginning of file +static const uint16_t top_h = 16; +static const uint16_t status_v_pad = 2; +static const uint16_t status_bar_height = top_h + status_v_pad; +static const uint16_t horizontal_pad = 4; + +static const fontSize_t status_bar_font = FONT_SIZE_8PT; + +static const fontSize_t small_font = FONT_SIZE_6PT; +static const fontSize_t text_font = FONT_SIZE_8PT; +static const fontSize_t large_font = FONT_SIZE_12PT; + +// Radioddity GD-77 +#elif CONFIG_SCREEN_HEIGHT > 63 + +static const uint16_t top_h = 11; +static const uint16_t status_v_pad = 1; +static const uint16_t status_bar_height = top_h + status_v_pad; +static const uint16_t horizontal_pad = 4; + +static const fontSize_t status_bar_font = FONT_SIZE_6PT; + +static const fontSize_t small_font = FONT_SIZE_5PT; +static const fontSize_t text_font = FONT_SIZE_6PT; +static const fontSize_t large_font = FONT_SIZE_10PT; + +// Radioddity RD-5R +#elif CONFIG_SCREEN_HEIGHT > 47 + +static const uint16_t top_h = 11; +static const uint16_t status_v_pad = 1; +static const uint16_t status_bar_height = top_h + status_v_pad; +static const uint16_t horizontal_pad = 4; + +static const fontSize_t status_bar_font = FONT_SIZE_6PT; + +static const fontSize_t small_font = FONT_SIZE_5PT; +static const fontSize_t text_font = FONT_SIZE_6PT; +static const fontSize_t large_font = FONT_SIZE_10PT; + +#else +#error Unsupported vertical resolution! +#endif + +// Calculate printing positions +static const uint16_t user_start_y = status_bar_height; +static const uint16_t user_screen_size = CONFIG_SCREEN_HEIGHT - user_start_y; + +static const newlayout_t new_layout = +{ + .status_bar_height = top_h, + .status_bar_font = status_bar_font, + + .user_start_y = user_start_y, + .user_screen_size = user_screen_size, + + .small_font = small_font, + .text_font = text_font, + .large_font = large_font, + + .horizontal_pad = horizontal_pad, +}; + +const newlayout_t *ui_layout = &new_layout; + +void ui_drawStatusBar(const char *text) +{ + color_t white = { 255, 255, 255, 255 }; + gfx_rect_t status_bar_pos = { + .x = 0, + .y = 0, + .w = CONFIG_SCREEN_WIDTH, + .h = ui_layout->status_bar_height, + }; + + gfx_drawTextRect( + status_bar_pos, + ui_layout->status_bar_font, + TEXT_ALIGN_CENTER, + TEXT_VALIGN_MIDDLE, + white, + text + ); +} From 8f5a3fbaf84e399f0e259b8a8f5181bef5b762c0 Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Tue, 18 Nov 2025 18:29:36 -0800 Subject: [PATCH 26/27] ui_textedit: make layout dynamic to screen/font size --- openrtx/src/ui/new/ui_textedit.c | 213 ++++++++++++++++++++----------- 1 file changed, 135 insertions(+), 78 deletions(-) diff --git a/openrtx/src/ui/new/ui_textedit.c b/openrtx/src/ui/new/ui_textedit.c index 7794dbbc3..2090a9fe6 100644 --- a/openrtx/src/ui/new/ui_textedit.c +++ b/openrtx/src/ui/new/ui_textedit.c @@ -1,5 +1,7 @@ +#include #include +#include "ui/ui_new.h" #include "ui/ui_textedit.h" #include "ui/ui_screen.h" @@ -41,6 +43,7 @@ static const char *symbols_ITU_T_E161_callsign[] = "" }; +#define TEXTEDIT_LINE_SPACING 2 #define TEXTEDIT_SCRATCH_SIZE 256 #define TEXTEDIT_CARET_PERIOD_MS 500 @@ -65,6 +68,12 @@ typedef struct { // caret blink state bool caret_visible; long long last_blink; + + // geometry + gfx_rect_t status_rect; // top strip: "input mode, len/max" + gfx_rect_t text_rect; // middle: text entry + gfx_rect_t softkey_rect; // bottom strip + uint8_t lines_visible; } TexteditState; static TexteditState g_textedit_state; @@ -90,6 +99,74 @@ static void textedit_end_multitap(TexteditState *st, long long *now) st->caret_visible = true; } +static void textedit_compute_geometry(TexteditState *st) +{ + const uint16_t user_y = ui_layout->user_start_y; + const uint16_t user_h = ui_layout->user_screen_size; + const uint16_t screen_w = CONFIG_SCREEN_WIDTH - (ui_layout->horizontal_pad * 2); + + // Small font for status+softkeys + const uint8_t small_h = gfx_getFontHeight(ui_layout->small_font); + + // Text font metrics for the edit area + gfx_font_metrics_t tfm; + gfx_getFontMetrics(ui_layout->text_font, &tfm); + const uint8_t text_line_h = tfm.line_height; + + // Vertical padding inside the small strips + const uint8_t status_pad_y = 2; + const uint8_t softkey_pad_y = 2; + + uint16_t status_h = (uint16_t)(small_h + 2 * status_pad_y); + uint16_t softkey_h = (uint16_t)(small_h + 2 * softkey_pad_y); + + // Make sure we have at least one text line worth of height + if (status_h + softkey_h + text_line_h > user_h) { + // Clamp status/softkey to avoid going negative; worst case we still + // give the text area at least text_line_h height. + uint16_t available_for_strips = user_h > text_line_h ? (user_h - text_line_h) : 0; + if (available_for_strips < status_h + softkey_h) { + // Split evenly-ish + status_h = available_for_strips / 2; + softkey_h = available_for_strips - status_h; + } + } + + st->status_rect.x = 0; + st->status_rect.y = (int16_t)user_y; + st->status_rect.w = screen_w; + st->status_rect.h = status_h; + + st->softkey_rect.x = 0; + st->softkey_rect.y = (int16_t)(user_y + user_h - softkey_h); + st->softkey_rect.w = screen_w; + st->softkey_rect.h = softkey_h; + + st->text_rect.x = ui_layout->horizontal_pad; + st->text_rect.y = (int16_t)(st->status_rect.y + st->status_rect.h); + st->text_rect.w = screen_w; + st->text_rect.h = (uint16_t)(st->softkey_rect.y - st->text_rect.y); + + // Compute how many lines fit vertically in the text_rect, accounting + // for some inner padding and line spacing. + const uint8_t inner_pad_y = 2; // inside text box + + uint16_t usable_h = st->text_rect.h; + if (usable_h > 2 * inner_pad_y) + usable_h = (uint16_t)(usable_h - 2 * inner_pad_y); + + uint8_t max_lines = 1; + const uint16_t per_line = (uint16_t)(text_line_h + TEXTEDIT_LINE_SPACING); + if (per_line > 0) { + max_lines = (uint8_t)(usable_h / per_line); + if (max_lines == 0) { + max_lines = 1; + } + } + + st->lines_visible = max_lines; +} + static void textedit_reset(TexteditState *st) { st->cursor = 0; @@ -104,6 +181,8 @@ static void textedit_reset(TexteditState *st) st->last_blink = getTick(); st->scratch[0] = '\0'; + + textedit_compute_geometry(st); } static bool textedit_insert_char_at_cursor(TexteditState *st, char ch) @@ -372,24 +451,8 @@ static void textedit_tick(UiScreen *self, const UiEvent *ev) } } -#define TEXTEDIT_TEXT_Y_ADJUST 12 //TODO: fix the existance of this magic number - static void textedit_draw(UiScreen *self) { - const uint16_t text_v_offset = 1; - const uint16_t status_v_pad = 2; - const uint16_t top_h = 16; - const uint16_t top_pad = 4; - const uint16_t line1_h = 20; - const uint16_t small_line_v_pad = 2; - const uint16_t horizontal_pad = 4; - const uint16_t bottom_h = 23; - const uint8_t lines_visible = 4; // how many lines fit in the box - const uint8_t line_spacing = 2; // vertical pixels between baselines - - const fontSize_t top_font = FONT_SIZE_8PT; - const fontSize_t text_font = FONT_SIZE_8PT; - const color_t color_white = (color_t){255, 255, 255, 255}; TexteditState *st = (TexteditState *)self->ctx; @@ -402,54 +465,53 @@ static void textedit_draw(UiScreen *self) gfx_clearScreen(); // Header - const uint16_t top_pos_y = top_h - status_v_pad - text_v_offset; - const point_t top_pos = { horizontal_pad, top_pos_y }; - gfx_print(top_pos, top_font, TEXT_ALIGN_CENTER, color_white, st->title); - - // Status bar - const uint16_t status_bar_pos_y = top_h + top_pad + 8; - const point_t status_bar_pos = { horizontal_pad, status_bar_pos_y - status_v_pad - text_v_offset }; - gfx_print(status_bar_pos, FONT_SIZE_6PT, TEXT_ALIGN_RIGHT, color_white, "%" PRIu8 "/%" PRIu8, st->len, st->max_len); + ui_drawStatusBar(st->title); + + // Status strip + char status_buf[16]; + sniprintf(status_buf, sizeof status_buf, "%" PRIu8 "/%" PRIu8, st->len, st->max_len); + + gfx_drawTextRect( + st->status_rect, + ui_layout->small_font, + TEXT_ALIGN_RIGHT, + TEXT_VALIGN_MIDDLE, + color_white, + status_buf + ); - // Softkey labels - const uint16_t softkey_pos_y = CONFIG_SCREEN_HEIGHT - status_v_pad; - const point_t softkey_pos = { horizontal_pad, softkey_pos_y }; - gfx_print(softkey_pos, FONT_SIZE_6PT, TEXT_ALIGN_RIGHT, color_white, "Delete"); + // Softkey strip + gfx_drawTextRect( + st->softkey_rect, + ui_layout->small_font, + TEXT_ALIGN_RIGHT, + TEXT_VALIGN_MIDDLE, + color_white, + "Delete" + ); /* --- Edit box geometry --- */ // Font metrics - uint8_t font_h = gfx_getFontHeight(text_font); - - // Outer rectangle margins on screen - const uint16_t rect_margin_x = 4; - const uint16_t rect_margin_top = status_bar_pos_y + top_pad; + gfx_font_metrics_t tfm; + gfx_getFontMetrics(ui_layout->text_font, &tfm); + const uint8_t font_h = tfm.line_height; + const uint8_t line_spacing = TEXTEDIT_LINE_SPACING; // Inner padding between rect border and text const uint16_t inner_pad_x = 4; const uint16_t inner_pad_y = 4; + + // Borders around text area + gfx_drawRectRect(st->text_rect, color_white, false); - // Height for multiple lines: - // top pad = N*font + (N-1)*spacing + bottom pad - uint16_t rect_width = CONFIG_SCREEN_WIDTH - (rect_margin_x * 2); - uint16_t rect_height = - (uint16_t)(inner_pad_y * 2 + - lines_visible * font_h + - (lines_visible - 1) * line_spacing); - - point_t rect_origin = { - (int16_t)rect_margin_x, - (int16_t)rect_margin_top - }; - - gfx_drawRect(rect_origin, rect_width, rect_height, color_white, false); - - // Base glyph box for the first line - int16_t base_glyph_top_line0 = (int16_t)(rect_origin.y + inner_pad_y); + // Horizontal text area inside the rect + const int16_t text_start_x = (int16_t)(st->text_rect.x + inner_pad_x); + const int16_t text_area_right = (int16_t)(st->text_rect.x + st->text_rect.w - inner_pad_x); - // Text drawing area - int16_t text_start_x = (int16_t)rect_origin.x + inner_pad_x; - int16_t text_area_right = (int16_t)(rect_origin.x + rect_width - inner_pad_x); + // Baseline of the first text line + const int16_t first_baseline_y = + (int16_t)(st->text_rect.y + inner_pad_y + tfm.ascent); /* --- First pass: lay out to find total_lines and caret position --- */ @@ -475,7 +537,7 @@ static void textedit_draw(UiScreen *self) } char tmp[2] = { c, 0 }; - point_t sz = gfx_textSize(text_font, tmp); + point_t sz = gfx_textSize(ui_layout->text_font, tmp); // If this glyph would overflow the text area, wrap to next line if ((cur_x + (int16_t)sz.x > text_area_right) && (cur_x != text_start_x)) { @@ -512,15 +574,15 @@ static void textedit_draw(UiScreen *self) /* --- Decide which lines to show so the caret is visible --- */ uint8_t first_visible_line = 0; - if (total_lines <= lines_visible) { + if (total_lines <= st->lines_visible) { first_visible_line = 0; } else { // Try to keep the caret on the bottom visible line - int min_start = (int)caret_line_idx - (int)(lines_visible - 1); + int min_start = (int)caret_line_idx - (int)(st->lines_visible - 1); if (min_start < 0) min_start = 0; - if (min_start > (int)total_lines - (int)lines_visible) - min_start = (int)total_lines - (int)lines_visible; + if (min_start > (int)total_lines - (int)st->lines_visible) + min_start = (int)total_lines - (int)st->lines_visible; first_visible_line = (uint8_t)min_start; } @@ -536,7 +598,7 @@ static void textedit_draw(UiScreen *self) } char tmp[2] = { c, 0 }; - point_t sz = gfx_textSize(text_font, tmp); + point_t sz = gfx_textSize(ui_layout->text_font, tmp); // If this glyph would overflow the text area, wrap to next line if ((cur_x + (int16_t)sz.x > text_area_right) && (cur_x != text_start_x)) { @@ -545,24 +607,19 @@ static void textedit_draw(UiScreen *self) } if (cur_line >= first_visible_line && - cur_line < (uint8_t)(first_visible_line + lines_visible)) { + cur_line < (uint8_t)(first_visible_line + st->lines_visible)) { uint8_t rel_line = (uint8_t)(cur_line - first_visible_line); // Base glyph box for this line - int16_t base_glyph_top = - (int16_t)(base_glyph_top_line0 + + int16_t line_baseline_y = + (int16_t)(first_baseline_y + rel_line * (font_h + line_spacing)); - // Actual text baseline for this line - int16_t glyph_top = (int16_t)(base_glyph_top + TEXTEDIT_TEXT_Y_ADJUST); - - point_t pos = { - cur_x, - glyph_top - }; + point_t pos = { cur_x, line_baseline_y }; - gfx_printBuffer(pos, text_font, TEXT_ALIGN_LEFT, color_white, tmp); + // pos.y is the baseline; gfx_printBuffer uses yOffset from there + gfx_printBuffer(pos, ui_layout->text_font, TEXT_ALIGN_LEFT, color_white, tmp); } cur_x = (int16_t)(cur_x + sz.x); @@ -575,27 +632,27 @@ static void textedit_draw(UiScreen *self) if (caret_line_idx < first_visible_line) { caret_line_idx = first_visible_line; } - if (caret_line_idx >= (uint8_t)(first_visible_line + lines_visible)) { - caret_line_idx = (uint8_t)(first_visible_line + lines_visible - 1); + if (caret_line_idx >= (uint8_t)(first_visible_line + st->lines_visible)) { + caret_line_idx = (uint8_t)(first_visible_line + st->lines_visible - 1); } uint8_t caret_rel_line = (uint8_t)(caret_line_idx - first_visible_line); // Clamp caret horizontally to inside the rectangle int caret_x = caret_x_full; - int rect_left = rect_origin.x; - int rect_right = rect_origin.y + (int)rect_width - 1; + int rect_left = st->text_rect.x + inner_pad_x; + int rect_right = st->text_rect.x + (int)st->text_rect.w - inner_pad_x; if (caret_x < rect_left) caret_x = rect_left; if (caret_x > rect_right) caret_x = rect_right; // Base glyph box for caret's line - int16_t caret_base_glyph_top = - (int16_t)(base_glyph_top_line0 + + int16_t caret_baseline_y = + (int16_t)(first_baseline_y + caret_rel_line * (font_h + line_spacing)); - uint16_t caret_height = (uint16_t)(font_h + 2); - uint16_t caret_top = (uint16_t)(caret_base_glyph_top - 1); + uint16_t caret_height = tfm.line_height; + uint16_t caret_top = (uint16_t)(caret_baseline_y - tfm.ascent); point_t caret_pos = { (int16_t)caret_x, (int16_t)caret_top }; gfx_drawRect(caret_pos, 1, caret_height, color_white, true); From 76a8f522961e8a93adc0ddf37a529b6654f821ce Mon Sep 17 00:00:00 2001 From: Anthony Guerrero Date: Tue, 18 Nov 2025 18:39:08 -0800 Subject: [PATCH 27/27] ui_menu: make layout dynamic to screen/font size --- openrtx/src/ui/new/ui_menu.c | 125 +++++++++++++++++++++++------------ 1 file changed, 84 insertions(+), 41 deletions(-) diff --git a/openrtx/src/ui/new/ui_menu.c b/openrtx/src/ui/new/ui_menu.c index a12789aee..e815ba0eb 100644 --- a/openrtx/src/ui/new/ui_menu.c +++ b/openrtx/src/ui/new/ui_menu.c @@ -4,6 +4,7 @@ #include // for NULL #include +#include "ui/ui_new.h" #include "ui/ui_menu.h" #include "ui/ui_screen.h" #include "ui/ui_textedit.h" @@ -13,9 +14,6 @@ #include "hwconfig.h" #include "core/graphics.h" -/* How many rows we plan to show at once; for scrolling logic */ -#define MENU_VISIBLE_ROWS 6 - static void u8_adjust(uint8_t *data, bool inc, uint8_t min, uint8_t max, uint8_t step, bool wrap) { uint8_t v = *data; @@ -238,6 +236,13 @@ typedef struct { uint8_t depth; bool dirty; bool edit; + + // Geometry + uint16_t menu_size; + uint16_t menu_start_y; + uint16_t menu_item_h; + uint16_t menu_item_rows; + fontSize_t menu_item_font; } MenuState; static void menu_tick(UiScreen *self, const UiEvent *ev); @@ -266,15 +271,40 @@ static void menu_reset_to_root(MenuState *st) st->dirty = true; st->edit = false; + + // Calculate menu geometry + st->menu_item_font = ui_layout->status_bar_font; + + uint8_t line_h = gfx_getFontHeight(st->menu_item_font); + + // Vertical padding TODO: Investigate on different sized displays + const uint8_t row_padding = 0; + st->menu_item_h = line_h + row_padding; + + // How many rows fit in the user area: + uint16_t num_rows = ui_layout->user_screen_size / st->menu_item_h; + if (num_rows == 0) { + num_rows = 1; + } + + st->menu_item_rows = num_rows; + st->menu_size = num_rows * st->menu_item_h; + + // Vertically center the menu within the user area + st->menu_start_y = + ui_layout->user_start_y + + (ui_layout->user_screen_size - st->menu_size) / 2; } -/* Clamp first so that pos is always visible in [first, first + MENU_VISIBLE_ROWS - 1] */ -static void menu_ensure_visible(MenuFrame *frame) +/* Clamp first so that pos is always visible in [first, first + st->menu_item_rows - 1] */ +static void menu_ensure_visible(MenuState *st) { + MenuFrame *frame = &st->stack[st->depth - 1]; + if (frame->pos < frame->first) { frame->first = frame->pos; - } else if (frame->pos >= (uint8_t)(frame->first + MENU_VISIBLE_ROWS)) { - frame->first = frame->pos - (MENU_VISIBLE_ROWS - 1); + } else if (frame->pos >= (uint8_t)(frame->first + st->menu_item_rows)) { + frame->first = frame->pos - (st->menu_item_rows - 1); } } @@ -362,7 +392,7 @@ static void menu_tick(UiScreen *self, const UiEvent *ev) } else { frame->pos = menu->child_count - 1; // wrap to last } - menu_ensure_visible(frame); + menu_ensure_visible(st); st->dirty = true; break; @@ -372,7 +402,7 @@ static void menu_tick(UiScreen *self, const UiEvent *ev) } else { frame->pos = 0; // wrap to first } - menu_ensure_visible(frame); + menu_ensure_visible(st); st->dirty = true; break; @@ -481,22 +511,6 @@ static void menu_tick(UiScreen *self, const UiEvent *ev) static void menu_draw(UiScreen *self) { - // consts borrowed from existing code for large displays - // TODO: copy over `layout_t` and helpers - const uint16_t text_v_offset = 1; - const uint16_t status_v_pad = 2; - const uint16_t top_h = 16; - const uint16_t top_pad = 4; - const uint16_t line1_h = 20; - const uint16_t small_line_v_pad = 2; - const uint16_t horizontal_pad = 4; - const uint16_t top_pos_y = top_h - status_v_pad - text_v_offset; - const uint16_t line1_pos_y = top_h + top_pad + line1_h - small_line_v_pad - text_v_offset; - const uint16_t menu_h = 16; - const point_t top_pos = {horizontal_pad, top_pos_y}; - const point_t line1_pos = {horizontal_pad, line1_pos_y}; - const fontSize_t top_font = FONT_SIZE_8PT; - const fontSize_t menu_font = FONT_SIZE_8PT; const color_t color_white = {255, 255, 255, 255}; const color_t color_black = {0, 0, 0, 255}; @@ -527,16 +541,27 @@ static void menu_draw(UiScreen *self) // Header const char *title = menu->label ? menu->label : ""; - gfx_print(top_pos, top_font, TEXT_ALIGN_CENTER, color_white, title); + ui_drawStatusBar(title); // Menu items - point_t pos = line1_pos; - point_t pos_val = { line1_pos.x + scrollbar_pad, line1_pos.y }; + gfx_rect_t row_rect = { + .x = 0, + .y = st->menu_start_y, + .w = CONFIG_SCREEN_WIDTH - scrollbar_pad, + .h = st->menu_item_h, + }; + + gfx_rect_t label_rect = row_rect; + label_rect.x += ui_layout->horizontal_pad; + label_rect.w -= ui_layout->horizontal_pad + 32; // TODO: consider space for value column + + gfx_rect_t value_rect = row_rect; + value_rect.w -= ui_layout->horizontal_pad; int first = frame->first; int count = menu->child_count; - for(int idx = first; idx < first + MENU_VISIBLE_ROWS && idx < count; ++idx) + for(int idx = first; idx < first + st->menu_item_rows && idx < count; ++idx) { const MenuItem *item = menu->children[idx]; const char *label = item->label ? item->label : ""; @@ -554,16 +579,27 @@ static void menu_draw(UiScreen *self) text_color = color_white; full_rect = false; } - point_t rect_pos = {0, pos.y - menu_h + 3}; - gfx_drawRect(rect_pos, CONFIG_SCREEN_WIDTH - scrollbar_pad, menu_h, color_white, full_rect); + gfx_drawRectRect(row_rect, color_white, full_rect); // announceMenuItemIfNeeded() } - gfx_print(pos, menu_font, TEXT_ALIGN_LEFT, text_color, label); + gfx_drawTextRect(label_rect, + st->menu_item_font, + TEXT_ALIGN_LEFT, + TEXT_VALIGN_MIDDLE, + text_color, + label + ); char value_buf[16] = {0}; /* Show if node is unimplemented */ if (item->kind == MENU_NODE_UNIMPLEMENTED) { - gfx_print(pos_val, menu_font, TEXT_ALIGN_RIGHT, text_color, ":("); + gfx_drawTextRect(value_rect, + st->menu_item_font, + TEXT_ALIGN_RIGHT, + TEXT_VALIGN_MIDDLE, + text_color, + ":(" + ); } /* Value on the right if this is a VALUE node */ @@ -599,25 +635,32 @@ static void menu_draw(UiScreen *self) } if (value_buf[0] != '\0') { - gfx_print(pos_val, menu_font, TEXT_ALIGN_RIGHT, text_color, value_buf); + gfx_drawTextRect(value_rect, + st->menu_item_font, + TEXT_ALIGN_RIGHT, + TEXT_VALIGN_MIDDLE, + text_color, + value_buf + ); } - pos.y += menu_h; - pos_val.y = pos.y; + row_rect.y += st->menu_item_h; + label_rect.y += st->menu_item_h; + value_rect.y += st->menu_item_h; } // Scroll bar - int max_first_visible = count - MENU_VISIBLE_ROWS; + int max_first_visible = count - st->menu_item_rows; uint16_t thumb_px, thumb_pos_px; - const uint16_t track_px_start = top_h + top_pad; - const uint16_t track_px = CONFIG_SCREEN_HEIGHT - track_px_start; // total height of scrollbar track in pixels + const uint16_t track_px_start = st->menu_start_y; + const uint16_t track_px = st->menu_size; // total height of scrollbar track in pixels if (count <= 0 || max_first_visible <= 0) { // No scrolling: everything fits, or nothing to show thumb_px = track_px; thumb_pos_px = 0; } else { - int32_t num = (int32_t)track_px * (int32_t)MENU_VISIBLE_ROWS; + int32_t num = (int32_t)track_px * (int32_t)st->menu_item_rows; int32_t denom = (int32_t)count; // Add denom/2 for simple "round to nearest" instead of truncating @@ -638,4 +681,4 @@ static void menu_draw(UiScreen *self) gfx_render(); st->dirty = false; -} \ No newline at end of file +}