From 563dfea368aeba00047482e951ee22a8fb4deaf4 Mon Sep 17 00:00:00 2001 From: Claude Worker claude2 Date: Wed, 10 Jun 2026 22:20:28 +0200 Subject: [PATCH] feat(ui): Workbench shell + boot zoom intro --- .../components/display_ui/display_ui.cpp | 180 +++++++++++++++++- .../display_ui/include/display_ui.h | 6 +- 2 files changed, 181 insertions(+), 5 deletions(-) diff --git a/idf_zacus/components/display_ui/display_ui.cpp b/idf_zacus/components/display_ui/display_ui.cpp index 4be881d..b6fb41c 100644 --- a/idf_zacus/components/display_ui/display_ui.cpp +++ b/idf_zacus/components/display_ui/display_ui.cpp @@ -201,6 +201,34 @@ static SemaphoreHandle_t s_cam_mutex; static volatile bool s_cam_fresh = false; static bool s_cam_visible = false; +// ─── Amiga Workbench shell (port of ui_amiga_shell, same grid metrics) ─────── +#define SHELL_GRID_COLS 4 // = AmigaUIShell::GRID_COLS +#define SHELL_ICON_SIZE 64 // = ICON_SIZE +#define SHELL_ICON_SPACING 16 // = ICON_SPACING +#define SHELL_GRID_X 16 // = GRID_START_X +#define SHELL_GRID_Y 48 // GRID_START_Y(32) + our taller title bar +#define SHELL_APP_COUNT 4 +static lv_obj_t *s_scr_shell; +static lv_obj_t *s_shell_tiles[SHELL_APP_COUNT]; +// Written by the buttons task, applied by the display task. +static volatile uint8_t s_shell_sel = 0; +static volatile bool s_shell_open = false; +// Accent colors from the original shell palette (ui_amiga_shell.cpp:280-287). +static const struct { const char *name; uint32_t color; } +kShellApps[SHELL_APP_COUNT] = { + {"Statut", 0x00FFFF}, // cyan + {"Scene", 0xFF8800}, // orange + {"Auto", 0x00FF88}, // green + {"Lumiere", 0xFFFF00}, // yellow +}; + +// ─── Intro (zoom/fade evocation of the original 3D intro sequence) ─────────── +// The original ui_manager_intro.cpp (2475 lines, parallax planes) is NOT +// ported; this is the boot-zoom essence on LVGL anims. +static lv_obj_t *s_scr_intro; +static lv_obj_t *s_intro_title; +static volatile bool s_intro_done = false; + static lv_obj_t *make_row(lv_obj_t *parent, int y, const char *label_txt, dui_row_t *out) { lv_obj_t *lbl = lv_label_create(parent); @@ -360,6 +388,81 @@ static void build_scene_screen(void) { lv_obj_add_flag(s_gyro_arc, LV_OBJ_FLAG_HIDDEN); } +static void build_shell_screen(void) { + s_scr_shell = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_scr_shell, lv_color_hex(COL_BG), 0); + lv_obj_set_style_bg_opa(s_scr_shell, LV_OPA_COVER, 0); + + lv_obj_t *bar = lv_obj_create(s_scr_shell); + lv_obj_remove_style_all(bar); + lv_obj_set_size(bar, DUI_HOR_RES, 36); + lv_obj_set_style_bg_color(bar, lv_color_hex(COL_TITLEBAR), 0); + lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, 0); + lv_obj_t *t = lv_label_create(bar); + lv_obj_set_style_text_color(t, lv_color_hex(COL_TITLE_TXT), 0); + lv_obj_set_style_text_font(t, &lv_font_montserrat_24, 0); + lv_label_set_text(t, "Zacus Workbench"); + lv_obj_align(t, LV_ALIGN_LEFT_MID, 8, 0); + + for (int i = 0; i < SHELL_APP_COUNT; i++) { + const int col = i % SHELL_GRID_COLS; + const int row = i / SHELL_GRID_COLS; + lv_obj_t *tile = lv_obj_create(s_scr_shell); + lv_obj_remove_style_all(tile); + lv_obj_set_size(tile, SHELL_ICON_SIZE, SHELL_ICON_SIZE); + lv_obj_set_pos(tile, + SHELL_GRID_X + col * (SHELL_ICON_SIZE + SHELL_ICON_SPACING), + SHELL_GRID_Y + row * (SHELL_ICON_SIZE + SHELL_ICON_SPACING)); + lv_obj_set_style_bg_color(tile, lv_color_hex(kShellApps[i].color), 0); + lv_obj_set_style_bg_opa(tile, LV_OPA_COVER, 0); + lv_obj_set_style_radius(tile, 4, 0); // like the original + lv_obj_set_style_border_color(tile, lv_color_white(), 0); + lv_obj_set_style_border_width(tile, 1, 0); + + lv_obj_t *lbl = lv_label_create(tile); + lv_obj_set_style_text_color(lbl, lv_color_hex(0x000000), 0); + lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0); + lv_label_set_text(lbl, kShellApps[i].name); + lv_obj_align(lbl, LV_ALIGN_BOTTOM_MID, 0, -4); + + s_shell_tiles[i] = tile; + } +} + +// Highlight the selected tile (display task only). +static void apply_shell_selection(void) { + for (int i = 0; i < SHELL_APP_COUNT; i++) { + lv_obj_set_style_border_width(s_shell_tiles[i], + (i == s_shell_sel) ? 4 : 1, 0); + } +} + +static void intro_zoom_cb(void *obj, int32_t v) { + lv_obj_set_style_transform_zoom((lv_obj_t *) obj, (uint16_t) v, 0); + lv_obj_set_style_opa((lv_obj_t *) obj, (lv_opa_t) ((v * 255) / 256), 0); +} + +static void build_intro_screen(void) { + s_scr_intro = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_scr_intro, lv_color_hex(0x000000), 0); + lv_obj_set_style_bg_opa(s_scr_intro, LV_OPA_COVER, 0); + + s_intro_title = lv_label_create(s_scr_intro); + lv_obj_set_style_text_color(s_intro_title, lv_color_hex(COL_CODE), 0); + lv_obj_set_style_text_font(s_intro_title, &lv_font_orbitron_40, 0); + lv_label_set_text(s_intro_title, "ZACUS"); + lv_obj_center(s_intro_title); + + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, s_intro_title); + lv_anim_set_exec_cb(&a, intro_zoom_cb); + lv_anim_set_values(&a, 32, 256); // 12.5% → 100% zoom-in + lv_anim_set_time(&a, 1400); + lv_anim_set_path_cb(&a, lv_anim_path_ease_out); + lv_anim_start(&a); +} + static void build_status_screen(void) { lv_obj_t *scr = s_scr_status; lv_obj_set_style_bg_color(scr, lv_color_hex(COL_BG), 0); @@ -454,7 +557,11 @@ static void apply_status(const display_status_t *s) { // original ui_manager default transition (SceneTransition::kFade, 240). extern volatile uint8_t g_dui_view_override; lv_obj_t *want; - switch (g_dui_view_override) { + if (!s_intro_done) { + want = s_scr_intro; // boot intro plays out first + } else if (s_shell_open) { + want = s_scr_shell; // Workbench shell on top + } else switch (g_dui_view_override) { case 1: want = s_scr_status; break; case 2: want = s_scr_scene; break; default: want = s->step_id[0] ? s_scr_scene : s_scr_status; break; @@ -556,13 +663,36 @@ static void display_task(void *arg) { s_scr_status = lv_scr_act(); build_status_screen(); build_scene_screen(); + build_shell_screen(); + build_intro_screen(); + + // Boot intro: shown immediately, hands over after ~1.8 s. + lv_scr_load(s_scr_intro); + TickType_t intro_until = xTaskGetTickCount() + pdMS_TO_TICKS(1800); display_status_t local_status; memset(&local_status, 0, sizeof(local_status)); TickType_t last_cam = 0; + uint8_t shell_sel_applied = 0xFF; + bool shell_open_applied = false; for (;;) { + // Intro handover. + if (!s_intro_done && xTaskGetTickCount() >= intro_until) { + s_intro_done = true; + s_dirty = true; // re-evaluate the wanted screen + } + + // Shell state (selection highlight / open-close) from the buttons task. + if (s_shell_sel != shell_sel_applied || + s_shell_open != shell_open_applied) { + shell_sel_applied = s_shell_sel; + shell_open_applied = s_shell_open; + apply_shell_selection(); + s_dirty = true; + } + if (s_dirty && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(20)) == pdTRUE) { memcpy(&local_status, &s_status, sizeof(local_status)); @@ -694,6 +824,50 @@ extern "C" void display_ui_set_status(const display_status_t *s) { extern "C" void display_ui_handle_key(uint8_t key) { static uint8_t s_brightness = 100; + + // ── Shell open: navigate the Workbench grid (original key semantics: + // 1=SELECT launch, 2=DOWN row, 3=MENU close, 4=LEFT/RIGHT, 5=UP row). + if (s_shell_open) { + switch (key) { + case 1: { // SELECT — launch tile + const uint8_t sel = s_shell_sel; + s_shell_open = false; + switch (sel) { + case 0: g_dui_view_override = 1; break; // Statut + case 1: g_dui_view_override = 2; break; // Scene + case 2: g_dui_view_override = 0; break; // Auto + case 3: // Lumiere — cycle + s_brightness = (s_brightness <= 20) ? 100 + : (uint8_t)(s_brightness - 40); + display_ui_set_brightness(s_brightness); + s_shell_open = true; // stay in the shell for feedback + break; + default: break; + } + break; + } + case 2: // DOWN — next row + if (s_shell_sel + SHELL_GRID_COLS < SHELL_APP_COUNT) + s_shell_sel = (uint8_t)(s_shell_sel + SHELL_GRID_COLS); + break; + case 5: // UP — previous row + if (s_shell_sel >= SHELL_GRID_COLS) + s_shell_sel = (uint8_t)(s_shell_sel - SHELL_GRID_COLS); + break; + case 4: // LEFT/RIGHT — next column (wraps) + s_shell_sel = (uint8_t)((s_shell_sel + 1) % SHELL_APP_COUNT); + break; + case 3: // MENU — close shell + s_shell_open = false; + break; + default: + break; + } + s_dirty = true; + return; + } + + // ── Shell closed: view shortcuts + brightness. switch (key) { case 1: // SELECT — toggle scene↔status manually case 4: { // LEFT/RIGHT — same toggle @@ -702,8 +876,8 @@ extern "C" void display_ui_handle_key(uint8_t key) { s_dirty = true; // wake the display task decision break; } - case 3: // MENU — back to automatic view selection - g_dui_view_override = 0; + case 3: // MENU — open the Workbench shell + s_shell_open = true; s_dirty = true; break; case 5: // UP — brightness + diff --git a/idf_zacus/components/display_ui/include/display_ui.h b/idf_zacus/components/display_ui/include/display_ui.h index 61aa48f..62c5c7c 100644 --- a/idf_zacus/components/display_ui/include/display_ui.h +++ b/idf_zacus/components/display_ui/include/display_ui.h @@ -68,8 +68,10 @@ void display_ui_camera_frame(const uint8_t *gray, int width, int height); * @brief Handle a 5-way key press (called from the buttons scan task). * * Original key numbering: 1=SELECT 2=DOWN 3=MENU 4=LEFT/RIGHT 5=UP. - * SELECT / LEFT-RIGHT toggle the scene↔status view manually; MENU returns - * to automatic view selection; UP/DOWN step the backlight brightness. + * Shell closed: SELECT / LEFT-RIGHT toggle scene↔status; MENU opens the + * Workbench shell; UP/DOWN step the backlight brightness. + * Shell open: grid navigation per the original ui_amiga_shell semantics + * (SELECT launches the tile — Statut / Scene / Auto / Lumiere; MENU closes). * Thread-safe (atomic request flags consumed by the display task; the * brightness path drives LEDC directly, which has its own locking). */