Files
lisael-box/main/ui/ui.c
T
Clément Saillant d8cbf35572 feat: WiFi settings tile, 366 words, icon cleanup
Three follow-ups after the reading game landed.

- WiFi: a "Réglages" home tile (gear) opens LISAEL_MODE_WIFI which forces
  the SoftAP + captive portal up on demand (lisael_ui_goto starts them;
  the screen's "Fermer" button tears them down), so the provisioning
  portal can be tested/used without waiting for the station to fail.
- Lire: the content generator now also takes words that have no emoji
  (optional img in the regex) -> 366 words instead of 286 (the extra
  ~80 are abstract words: text + spoken audio, no picture).
- Cleanup: removed 21 baked word icons that became dead weight once the
  reading game started loading its pictures from the SD (~315 KB flash),
  plus their declarations and gen_icons.py specs.
2026-06-14 23:54:58 +02:00

145 lines
4.8 KiB
C

// LVGL UI shell + mode router for Lisael Box (LVGL 9 API).
//
// Screens are built lazily and cached so switching back is instant. Mode
// transitions use lv_screen_load_anim() for a gentle slide. All LVGL access
// from background tasks goes through bsp_display_lock()/unlock(); the functions
// here are called either from app_main during init (lock held by caller) or
// from LVGL event callbacks (already on the LVGL task).
#include "ui/ui.h"
#include "ui/icons.h"
#include "ui/fonts/lisael_fonts.h"
#include "lisael_config.h"
#include "net/wifi.h"
#include "net/wifi_portal.h"
#include "esp_log.h"
#include "bsp/esp-box-3.h"
static const char *TAG = "ui";
static lv_obj_t *s_screens[LISAEL_MODE_COUNT];
static lisael_mode_t s_current = LISAEL_MODE_HOME;
static lv_style_t s_style_tile;
static lv_style_t s_style_button;
static bool s_styles_ready;
static void init_styles(void)
{
if (s_styles_ready) {
return;
}
// Big rounded glossy tiles for the home menu — soft drop shadow for depth,
// a vertical gloss gradient (set per tile in make_tile), rounded corners.
lv_style_init(&s_style_tile);
lv_style_set_radius(&s_style_tile, 22);
lv_style_set_bg_opa(&s_style_tile, LV_OPA_COVER);
lv_style_set_bg_grad_dir(&s_style_tile, LV_GRAD_DIR_VER); // colours per tile
lv_style_set_border_width(&s_style_tile, 0);
lv_style_set_pad_all(&s_style_tile, 6);
lv_style_set_shadow_width(&s_style_tile, 18);
lv_style_set_shadow_ofs_y(&s_style_tile, 8);
lv_style_set_shadow_spread(&s_style_tile, 1);
lv_style_set_shadow_color(&s_style_tile, lv_color_hex(0x5b6b8a));
lv_style_set_shadow_opa(&s_style_tile, LV_OPA_40);
// Generic large touch button.
lv_style_init(&s_style_button);
lv_style_set_radius(&s_style_button, 14);
lv_style_set_pad_all(&s_style_button, 10);
lv_style_set_min_height(&s_style_button, 56); // comfortable for small hands
s_styles_ready = true;
}
const lv_style_t *lisael_ui_style_tile(void)
{
init_styles();
return &s_style_tile;
}
const lv_style_t *lisael_ui_style_button(void)
{
init_styles();
return &s_style_button;
}
static lv_obj_t *build_screen(lisael_mode_t mode)
{
switch (mode) {
case LISAEL_MODE_HOME: return lisael_screen_home();
case LISAEL_MODE_RADIO: return lisael_screen_radio();
case LISAEL_MODE_CALM: return lisael_screen_calm();
case LISAEL_MODE_CLOCK: return lisael_screen_clock();
case LISAEL_MODE_GAMES: return lisael_screen_games();
case LISAEL_MODE_HISTOIRES:return lisael_screen_histoires();
case LISAEL_MODE_LIRE: return lisael_screen_lire();
case LISAEL_MODE_WIFI: return lisael_screen_wifi();
default: return lisael_screen_home();
}
}
void lisael_ui_goto(lisael_mode_t mode)
{
if ((unsigned)mode >= (unsigned)LISAEL_MODE_COUNT) {
return; // borne nette pour GCC 14 (-Werror=array-bounds)
}
if (mode == LISAEL_MODE_WIFI) {
// On-demand provisioning: bring the SoftAP + captive portal up so the
// Wi-Fi can be (re)configured from a phone. Torn down by the screen's
// "Fermer" button. Both calls are idempotent.
lisael_wifi_start_ap();
lisael_wifi_portal_start();
}
if (!s_screens[mode]) {
s_screens[mode] = build_screen(mode);
}
// Slide direction: going "deeper" (home->mode) slides left, back slides right.
lv_screen_load_anim_t anim = (mode == LISAEL_MODE_HOME)
? LV_SCR_LOAD_ANIM_MOVE_RIGHT
: LV_SCR_LOAD_ANIM_MOVE_LEFT;
lv_screen_load_anim(s_screens[mode], anim, 250, 0, false);
s_current = mode;
ESP_LOGI(TAG, "mode -> %d", mode);
}
static void back_cb(lv_event_t *e)
{
(void)e;
lisael_ui_goto(LISAEL_MODE_HOME);
}
lv_obj_t *lisael_ui_add_back_button(lv_obj_t *parent)
{
lv_obj_t *btn = lv_button_create(parent);
lv_obj_add_style(btn, lisael_ui_style_button(), 0);
lv_obj_set_size(btn, 64, 56);
lv_obj_align(btn, LV_ALIGN_TOP_LEFT, 6, 6);
lv_obj_add_event_cb(btn, back_cb, LV_EVENT_CLICKED, NULL);
lv_obj_t *lbl = lv_label_create(btn);
lv_label_set_text(lbl, LV_SYMBOL_LEFT); // monochrome arrow — always renders
lv_obj_center(lbl);
return btn;
}
void lisael_ui_init(void)
{
// Default font -> Fredoka (rounded, kid-friendly, with French accents).
// The default theme applies it to every label unless overridden.
lv_display_t *disp = lv_display_get_default();
lv_theme_t *th = lv_theme_default_init(
disp, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_PINK),
false, &lisael_font_18);
if (th) {
lv_display_set_theme(disp, th);
}
init_styles();
s_screens[LISAEL_MODE_HOME] = lisael_screen_home();
lv_screen_load(s_screens[LISAEL_MODE_HOME]);
s_current = LISAEL_MODE_HOME;
ESP_LOGI(TAG, "UI initialised");
}