diff --git a/meson.build b/meson.build index 9c5a84ce20..f00755a84f 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', @@ -78,7 +79,20 @@ 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/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', + '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_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', + 'openrtx/src/ui/new/ui_textedit.c'] ui_src_module17 = ['openrtx/src/ui/module17/ui.c', 'openrtx/src/ui/module17/ui_main.c', @@ -356,7 +370,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 @@ -799,7 +813,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/core/graphics.h b/openrtx/include/core/graphics.h index 2db2801058..c54a4fe940 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,18 @@ 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. + * @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 @@ -244,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/include/core/settings.h b/openrtx/include/core/settings.h index 8bdde32728..9cc4f24bde 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/include/ui/ui_menu.h b/openrtx/include/ui/ui_menu.h new file mode 100644 index 0000000000..81731c331e --- /dev/null +++ b/openrtx/include/ui/ui_menu.h @@ -0,0 +1,119 @@ +/* ui_menu.h */ +#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 { + /// @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, + + /// @brief Called when we're about to enter the generic edit mode on this item. + /// @param arg `NULL` + MENU_CMD_EDIT_BEGIN, + + /// @brief A key press while this item is in edit mode. + /// @param arg `const UiEvent*` + MENU_CMD_EDIT_KEY, + + /// @brief User cancelled edit (ESC). + /// @param arg `NULL` + MENU_CMD_EDIT_CANCEL, + + /// @brief User accepted edit (ESC). + /// @param arg `NULL` + MENU_CMD_EDIT_APPLY, +} MenuCmd; + +typedef struct { + char *buf; + size_t buf_len; +} MenuDrawValueArgs; + +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_U8, + MENU_VAL_I32, + MENU_VAL_ENUM, + MENU_VAL_STR, +} MenuValueKind; + +typedef struct { + MenuValueKind kind; + void *ptr; // pointer to the underlying value + void (*on_change)(void *ptr); // optional side-effect on change + + union { + 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; + //uint8_t type_mask; TODO: Use this or `profile`? + UiStrProfile profile; + } str; + } u; +} 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/include/ui/ui_menu_dsl.h b/openrtx/include/ui/ui_menu_dsl.h new file mode 100644 index 0000000000..32498f35bb --- /dev/null +++ b/openrtx/include/ui/ui_menu_dsl.h @@ -0,0 +1,54 @@ +#ifndef UI_MENU_DSL_H +#define UI_MENU_DSL_H + +#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0])) + +/// @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/include/ui/ui_new.h b/openrtx/include/ui/ui_new.h new file mode 100644 index 0000000000..a7143cf583 --- /dev/null +++ b/openrtx/include/ui/ui_new.h @@ -0,0 +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); + +/* + * ┌─────────────────────────┐ + * │ 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/include/ui/ui_screen.h b/openrtx/include/ui/ui_screen.h new file mode 100644 index 0000000000..b6bbc102fd --- /dev/null +++ b/openrtx/include/ui/ui_screen.h @@ -0,0 +1,44 @@ +#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 enum { + UI_EVENT_NONE = 0, + UI_EVENT_KEY = 1, + UI_EVENT_FOCUS_GAIN = 2, + UI_EVENT_FOCUS_LOST = 3, +} UiEventType; + +typedef struct { + UiEventType type; + enum key key; // valid only when type == UI_EVENT_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/include/ui/ui_textedit.h b/openrtx/include/ui/ui_textedit.h new file mode 100644 index 0000000000..de5c901c85 --- /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 //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); + +#endif diff --git a/openrtx/src/core/graphics.c b/openrtx/src/core/graphics.c index 5d7d2a46dd..8dc83d11d1 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,9 +468,38 @@ 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; +} + +void gfx_getFontMetrics(fontSize_t size, gfx_font_metrics_t *out) +{ + if (!out) return; + *out = font_metrics[size]; +} + +point_t gfx_textSize(fontSize_t size, const char *buf) +{ + const GFXfont *f = &fonts[size]; + const gfx_font_metrics_t *fm = &font_metrics[size]; + + uint16_t w = 0; + size_t len = strlen(buf); + + 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 = { (int16_t)w, (int16_t)fm->line_height }; + return sz; } point_t gfx_printBuffer(point_t start, fontSize_t size, textAlign_t alignment, @@ -575,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) { diff --git a/openrtx/src/core/settings.c b/openrtx/src/core/settings.c new file mode 100644 index 0000000000..5a27d4e33b --- /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, +}; diff --git a/openrtx/src/core/threads.c b/openrtx/src/core/threads.c index 537c23be0e..1529db8321 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/default/ui.c b/openrtx/src/ui/default/ui.c index 1b7085c381..779e56c158 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; diff --git a/openrtx/src/ui/new/ui.c b/openrtx/src/ui/new/ui.c new file mode 100644 index 0000000000..edd39dde1b --- /dev/null +++ b/openrtx/src/ui/new/ui.c @@ -0,0 +1,92 @@ +#include "core/graphics.h" +#include "ui/ui_new.h" +#include "hwconfig.h" + +// 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 + ); +} diff --git a/openrtx/src/ui/new/ui_compat.c b/openrtx/src/ui/new/ui_compat.c new file mode 100644 index 0000000000..c0cf08f341 --- /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 0000000000..be6ba336e9 --- /dev/null +++ b/openrtx/src/ui/new/ui_core.c @@ -0,0 +1,65 @@ +#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; + +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 (!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) { + return; + } + + 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 = UI_EVENT_KEY; + ev->key = kbd->keys; + return true; +} \ No newline at end of file diff --git a/openrtx/src/ui/new/ui_gps.c b/openrtx/src/ui/new/ui_gps.c new file mode 100644 index 0000000000..7e655bcde3 --- /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 new file mode 100644 index 0000000000..e815ba0eb0 --- /dev/null +++ b/openrtx/src/ui/new/ui_menu.c @@ -0,0 +1,684 @@ +#include +#include +#include +#include // for NULL +#include + +#include "ui/ui_new.h" +#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 */ +#include "hwconfig.h" +#include "core/graphics.h" + +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 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) { + return; + } + + switch (b->kind) { + case MENU_VAL_BOOL: { + bool *p = (bool *)b->ptr; + *p = !*p; + break; + } + case MENU_VAL_U8: { + uint8_t *p = (uint8_t *)b->ptr; + u8_adjust( + p, + inc, + b->u.u8.min, + b->u.u8.max, + b->u.u8.step, + b->u.u8.wrap + ); + 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( + p, + inc, + 0, + b->u.enm.count-1, + 1, + true + ); + 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; + } + 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)]; + sniprintf(buf, n, "%s", v); + break; + } + case MENU_VAL_STR: { + char *v = (char *)b->ptr; + sniprintf(buf, n, "%s", v); + break; + } + default: + sniprintf(buf, n, "?"); + break; + } +} + +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; + 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); +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; + 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 + 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 + st->menu_item_rows)) { + frame->first = frame->pos - (st->menu_item_rows - 1); + } +} + +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) +{ + 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 (!st) { + return; + } + + /* If somehow unitialized, reset to root */ + if (st->depth == 0) { + menu_reset_to_root(st); + } + + if (!ev) { + return; + } + + 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]; + 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; + } + + /* Handle keypress */ + if (!st->edit) { + /* --- NORMAL NAVIGATION MODE --- */ + switch (key) + { + case KEY_UP: + if (frame->pos > 0) { + frame->pos--; + } else { + frame->pos = menu->child_count - 1; // wrap to last + } + menu_ensure_visible(st); + 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(st); + st->dirty = true; + break; + + 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) { + MenuFrame *child = &st->stack[st->depth]; + child->menu = item; + child->pos = 0; + child->first = 0; + st->depth++; + st->dirty = true; + } + } + /* Activate edit mode on value node */ + else if (item->kind == MENU_NODE_VALUE && item->binding) { + const 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 = (void *)b, + }; + ui_open_textedit(&p); + return; + } + + /* Non-string values; normal inline edit mode */ + st->edit = true; + st->dirty = true; + } + 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; + } + else if (item->kind == MENU_NODE_ACTION && item->cb) { + item->cb(MENU_CMD_SELECT, NULL, item->cb_ctx); + st->dirty = true; + } + 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: + break; + } + } else { + /* ---------- EDIT MODE ----------*/ + const MenuItem *item = menu->children[frame->pos]; + + 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; + } + } + + if (item->cb) { + /* Let callback handle keys in edit mode */ + item->cb(MENU_CMD_EDIT_KEY, (void *)ev, item->cb_ctx); + st->dirty = true; + } + + 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; + } + + return; + } +} + +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) + { + 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 : ""; + ui_drawStatusBar(title); + + // Menu items + 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 + st->menu_item_rows && idx < count; ++idx) + { + const MenuItem *item = menu->children[idx]; + const char *label = item->label ? item->label : ""; + + 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; + } + gfx_drawRectRect(row_rect, color_white, full_rect); + // announceMenuItemIfNeeded() + } + 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_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 */ + 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 + * screen, taking away from the item title. + * - Variable length fonts make this tricky. + * - Truncating could be misleading + */ + + if (item->binding) { + /* Generic value binding */ + 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 = value_buf, + .buf_len = sizeof value_buf + }; + item->cb(MENU_CMD_DRAW_VALUE, &args, item->cb_ctx); + } + } + + /* 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_drawTextRect(value_rect, + st->menu_item_font, + TEXT_ALIGN_RIGHT, + TEXT_VALIGN_MIDDLE, + text_color, + value_buf + ); + } + + 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 - st->menu_item_rows; + uint16_t thumb_px, thumb_pos_px; + 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)st->menu_item_rows; + int32_t denom = (int32_t)count; + + // Add denom/2 for simple "round to nearest" instead of truncating + thumb_px = (uint16_t)((num + denom / 2) / denom); + + int track_travel_px = track_px - thumb_px; + if (track_travel_px < 0) + track_travel_px = 0; + + num = (int32_t)track_travel_px * (int32_t)first; + denom = (int32_t)max_first_visible; + + thumb_pos_px = (uint16_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; +} 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 0000000000..4a1a26e465 --- /dev/null +++ b/openrtx/src/ui/new/ui_menu_tree.c @@ -0,0 +1,52 @@ +#include +#include + +#include "hwconfig.h" +#include "ui/ui_menu.h" +#include "ui/ui_menu_dsl.h" + +/* include menu items here, gate with compile flags */ + +/* 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; + +#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[] = { + &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_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"); + +static const MenuItem *const root_menu_children[] = { + &m_banks, + &m_channels, + &m_contacts, + &g_gps_menu, + &m_settings, + &m_info, + &m_about, +}; + +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 new file mode 100644 index 0000000000..c21dcbaae5 --- /dev/null +++ b/openrtx/src/ui/new/ui_settings_accessibility.c @@ -0,0 +1,51 @@ +#include + +#include "core/state.h" +#include "ui/ui_menu.h" +#include "ui/ui_menu_dsl.h" + +static MenuValueBinding macro_latch_binding = { + .kind = MENU_VAL_BOOL, + .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_ENUM, + .ptr = &state.settings.vpLevel, + .u.enm = { .names = voice_names, .count = ARRAY_LEN(voice_names) }, + .on_change = NULL, +}; + +static const MenuItem m_voice = + MENU_ITEM_VALUE_BINDING("Voice", &voice_binding); + +static MenuValueBinding phonetic_binding = { + .kind = MENU_VAL_BOOL, + .ptr = &state.settings.vpPhoneticSpell, + .on_change = NULL, +}; + +static const MenuItem m_phonetic = + MENU_ITEM_VALUE_BINDING("Phonetic", &phonetic_binding); + +static const MenuItem *const accessibility_children[] = { + &m_macro_latch, + &m_voice, + &m_phonetic, +}; + +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 new file mode 100644 index 0000000000..5d0b6ab7a5 --- /dev/null +++ b/openrtx/src/ui/new/ui_settings_display.c @@ -0,0 +1,99 @@ +#include +#include + +#include "ui/ui_menu.h" +#include "ui/ui_menu_dsl.h" +#include "core/state.h" +#include "interfaces/display.h" + +#ifdef CONFIG_SCREEN_BRIGHTNESS +static void brightness_on_change(void *ptr) { + uint8_t *p = (uint8_t *)ptr; + display_setBacklightLevel(*p); +} + +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, +}; + +static const MenuItem m_brightness = + MENU_ITEM_VALUE_BINDING("Brightness", &brightness_binding); +#endif // CONFIG_SCREEN_BRIGHTNESS + +#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 = + MENU_ITEM_VALUE_BINDING("Contrast", &contrast_binding); +#endif // CONFIG_SCREEN_CONTRAST + +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_VALUE_BINDING("Timer", &timer_binding); + +#ifndef CONFIG_BAT_NONE +static MenuValueBinding battery_icon_binding = { + .kind = MENU_VAL_BOOL, + .ptr = &state.settings.showBatteryIcon, + .on_change = NULL, +}; + +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 // CONFIG_SCREEN_BRIGHTNESS +#ifdef CONFIG_SCREEN_CONTRAST + &m_contrast, +#endif // CONFIG_SCREEN_CONTRAST + &m_timer, +#ifndef CONFIG_BAT_NONE + &m_battery_icon, +#endif // CONFIG_BAT_NONE +}; + +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 new file mode 100644 index 0000000000..49ae5eb489 --- /dev/null +++ b/openrtx/src/ui/new/ui_settings_fm.c @@ -0,0 +1,123 @@ +#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 = + MENU_ITEM_UNIMPLEMENTED("CTCSS Tone"); + +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; + } + + //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) +{ + (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 = + 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 = + 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 new file mode 100644 index 0000000000..397d5f2290 --- /dev/null +++ b/openrtx/src/ui/new/ui_settings_gps.c @@ -0,0 +1,112 @@ +#include +#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 = + MENU_ITEM_VALUE_BINDING("GPS Enabled", &gps_en_binding); + +#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 = + MENU_ITEM_VALUE_BINDING("GPS Set Time", &gps_set_time_binding); + +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_VALUE_CB("Timezone", utc_timezone_cb, NULL); +#endif // CONFIG_RTC + +static const MenuItem *const gps_children[] = { + &m_gps_en, +#ifdef CONFIG_RTC + &m_gps_set_time, + &m_utc_timezone, +#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_m17.c b/openrtx/src/ui/new/ui_settings_m17.c new file mode 100644 index 0000000000..e606a0e803 --- /dev/null +++ b/openrtx/src/ui/new/ui_settings_m17.c @@ -0,0 +1,56 @@ +#include +#include +#include + +#include "ui/ui_menu.h" +#include "ui/ui_menu_dsl.h" + +#include "core/settings.h" +#include "core/state.h" + +static void on_change(void *ptr) { + (void)ptr; + //TODO: old UI says `*sync_rtx = true`, + // need to update the rtx thread +} + +static MenuValueBinding callsign_binding = { + .kind = MENU_VAL_STR, + .ptr = &state.settings.callsign, + .on_change = on_change, + .u.str = { + .max_len = sizeof(((settings_t *)0)->callsign) - 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 = &state.settings.m17_can, + .on_change = on_change, + .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 = &state.settings.m17_can_rx, + .on_change = on_change, +}; + +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_settings_module17.c b/openrtx/src/ui/new/ui_settings_module17.c new file mode 100644 index 0000000000..622b671b5d --- /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); 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 0000000000..25088f0272 --- /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); diff --git a/openrtx/src/ui/new/ui_textedit.c b/openrtx/src/ui/new/ui_textedit.c new file mode 100644 index 0000000000..2090a9fe6d --- /dev/null +++ b/openrtx/src/ui/new/ui_textedit.c @@ -0,0 +1,663 @@ +#include +#include + +#include "ui/ui_new.h" +#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_LINE_SPACING 2 +#define TEXTEDIT_SCRATCH_SIZE 256 +#define TEXTEDIT_CARET_PERIOD_MS 500 + +typedef struct { + 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 (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; + + // 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; + +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_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_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; + st->len = 0; + st->active_key = 0; + st->active_idx = 0; + st->last_keypress = 0; + st->candidate_active = false; + st->dirty = true; + + st->caret_visible = true; + st->last_blink = getTick(); + + st->scratch[0] = '\0'; + + textedit_compute_geometry(st); +} + +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(); + uint8_t num_key = input_getPressedChar(msg); + + 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 (!same_key || timed_out) { + // Changing key or previous candidate expired. + // 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; + } 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 + uint8_t candidate_idx = (uint8_t)(st->cursor - 1); + + st->active_idx = (uint8_t)((st->active_idx + 1u) % num_symbols); + st->scratch[candidate_idx] = set[st->active_idx]; + } + + //vp_announceInputChar(st->scratch[st->len - 1]); + + st->last_keypress = now; + st->dirty = true; +} + +static void textedit_backspace(TexteditState *st) +{ + if (st->len == 0 || st->cursor == 0) { + // nothing to delete + st->candidate_active = false; + st->active_key = 0; + st->active_idx = 0; + st->last_keypress = 0; + return; + } + + // 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--; + + // 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 --- */ +void ui_open_textedit(const UiTextEditParams *p) +{ + if (!p || !p->buf || !p->title) { + return; + } + textedit_reset(&g_textedit_state); + + 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 = (uint8_t)strlen(g_textedit_state.scratch); + g_textedit_state.cursor = g_textedit_state.len; + + 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(); + + // 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. + textedit_end_multitap(st, &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; + } + + 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 + kbd_msg_t msg = { + .long_press = 0, + .keys = key, + }; + + // 3: Handle keys + if (key == KEY_ESC) + { + /* cancel everything */ + ui_pop_screen(); + return; + } + else if(key == KEY_ENTER) + { + // 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_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); + st->dirty = true; + return; + } +} + +static void textedit_draw(UiScreen *self) +{ + const color_t color_white = (color_t){255, 255, 255, 255}; + + TexteditState *st = (TexteditState *)self->ctx; + + if(!st || !st->dirty) + { + return; + } + + gfx_clearScreen(); + + // Header + 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 strip + gfx_drawTextRect( + st->softkey_rect, + ui_layout->small_font, + TEXT_ALIGN_RIGHT, + TEXT_VALIGN_MIDDLE, + color_white, + "Delete" + ); + + /* --- Edit box geometry --- */ + + // Font metrics + 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); + + // 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); + + // 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 --- */ + + uint8_t total_lines = 1; + uint8_t cur_line = 0; + int16_t cur_x = text_start_x; + + uint8_t caret_line_idx = 0; + int16_t caret_x_full = text_start_x; + bool caret_set = false; + + 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(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)) { + 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); + } + + // 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 so the caret is visible --- */ + uint8_t first_visible_line = 0; + 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)(st->lines_visible - 1); + if (min_start < 0) + min_start = 0; + 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; + } + + /* --- 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; + } + + char tmp[2] = { c, 0 }; + 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)) { + cur_line++; + cur_x = text_start_x; + } + + if (cur_line >= first_visible_line && + 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 line_baseline_y = + (int16_t)(first_baseline_y + + rel_line * (font_h + line_spacing)); + + point_t pos = { cur_x, line_baseline_y }; + + // 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); + } + + /* --- Caret drawing --- */ + + if (!st->candidate_active && st->caret_visible) { + // 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 + 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 = 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_baseline_y = + (int16_t)(first_baseline_y + + caret_rel_line * (font_h + line_spacing)); + + 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); + } + + gfx_render(); + st->dirty = false; +} diff --git a/platform/targets/linux/emulator/sdl_engine.c b/platform/targets/linux/emulator/sdl_engine.c index 850686f753..ca53c7d260 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;