From 7c8630abbeb365e0b648ed27a6f16a8fc8e11d19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Saillant?= Date: Mon, 15 Jun 2026 09:13:33 +0200 Subject: [PATCH] feat(box): activity carousel for home menu The 7 mode tiles overflowed the grid and scrolled. Replace the wrapping tile grid with the same Lunii-style horizontal carousel as Histoires: one big activity tile per full-width slide, snap-scroll swipe, tap arrows, a dot per activity. Header (date/time/weather) unchanged. Drop the now-unused make_tile. --- main/modes/home.c | 148 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 118 insertions(+), 30 deletions(-) diff --git a/main/modes/home.c b/main/modes/home.c index 4f44a4d..39d6d55 100644 --- a/main/modes/home.c +++ b/main/modes/home.c @@ -50,35 +50,93 @@ static void tile_cb(lv_event_t *e) lisael_ui_goto(mode); } -static lv_obj_t *make_tile(lv_obj_t *parent, const tile_def_t *def) +// --- activity carousel (Lunii/Merlin style launcher) ------------------------- +// One big activity tile per full-width slide; swipe (or arrows) to move one at a +// time, tap the tile to launch. Dots show the position. +#define HOME_SLIDE_W 320 + +static lv_obj_t *s_track; +static lv_obj_t *s_adots[8]; +static int s_acur; +static int s_n_acts; + +static void adots_update(int idx) { - lv_obj_t *tile = lv_button_create(parent); + for (int i = 0; i < s_n_acts; i++) { + if (s_adots[i]) { + lv_obj_set_style_bg_opa(s_adots[i], i == idx ? LV_OPA_COVER : LV_OPA_30, 0); + } + } +} + +static void home_scroll_cb(lv_event_t *e) +{ + lv_obj_t *t = lv_event_get_target(e); + int idx = (lv_obj_get_scroll_x(t) + HOME_SLIDE_W / 2) / HOME_SLIDE_W; + if (idx < 0) idx = 0; + if (idx >= s_n_acts) idx = s_n_acts - 1; + s_acur = idx; + adots_update(idx); +} + +static void home_arrow_cb(lv_event_t *e) +{ + int idx = s_acur + (int)(intptr_t)lv_event_get_user_data(e); + if (idx < 0) idx = 0; + if (idx >= s_n_acts) idx = s_n_acts - 1; + s_acur = idx; + lv_obj_scroll_to_x(s_track, idx * HOME_SLIDE_W, LV_ANIM_ON); + adots_update(idx); +} + +static void home_arrow(lv_obj_t *parent, const char *sym, lv_align_t al, int dir) +{ + lv_obj_t *b = lv_button_create(parent); + lv_obj_remove_style_all(b); + lv_obj_set_size(b, 38, 60); + lv_obj_set_style_radius(b, 19, 0); + lv_obj_set_style_bg_opa(b, LV_OPA_40, 0); + lv_obj_set_style_bg_color(b, lv_color_white(), 0); + lv_obj_align(b, al, (al == LV_ALIGN_LEFT_MID) ? 2 : -2, 28); + lv_obj_add_event_cb(b, home_arrow_cb, LV_EVENT_CLICKED, (void *)(intptr_t)dir); + lv_obj_t *l = lv_label_create(b); + lv_label_set_text(l, sym); + lv_obj_set_style_text_color(l, lv_color_hex(0x495057), 0); + lv_obj_center(l); +} + +static void add_act_slide(lv_obj_t *track, const tile_def_t *def) +{ + lv_obj_t *slide = lv_obj_create(track); + lv_obj_remove_style_all(slide); + lv_obj_set_size(slide, HOME_SLIDE_W, LV_PCT(100)); + lv_obj_clear_flag(slide, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_flex_flow(slide, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(slide, LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + + lv_obj_t *tile = lv_button_create(slide); lv_obj_add_style(tile, lisael_ui_style_tile(), 0); - // Glossy 3D look: lighter at the top fading to the tile colour (the shared - // style sets LV_GRAD_DIR_VER + the soft drop shadow). lv_obj_set_style_bg_color(tile, lv_color_lighten(def->color, 90), 0); lv_obj_set_style_bg_grad_color(tile, def->color, 0); - // Press feedback: the tile sinks a little. lv_obj_set_style_translate_y(tile, 4, LV_STATE_PRESSED); lv_obj_set_style_shadow_ofs_y(tile, 3, LV_STATE_PRESSED); - lv_obj_set_size(tile, 94, 72); - lv_obj_add_event_cb(tile, tile_cb, LV_EVENT_CLICKED, - (void *)(intptr_t)def->mode); - - lv_obj_t *col = lv_obj_create(tile); - lv_obj_remove_style_all(col); - lv_obj_set_size(col, LV_PCT(100), LV_PCT(100)); - lv_obj_set_flex_flow(col, LV_FLEX_FLOW_COLUMN); - lv_obj_set_flex_align(col, LV_FLEX_ALIGN_CENTER, + lv_obj_set_size(tile, 176, 122); + lv_obj_set_flex_flow(tile, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(tile, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_clear_flag(col, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_style_pad_row(tile, 6, 0); + lv_obj_clear_flag(tile, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_event_cb(tile, tile_cb, LV_EVENT_CLICKED, (void *)(intptr_t)def->mode); - lv_obj_t *icon = lv_image_create(col); + lv_obj_t *icon = lv_image_create(tile); lv_image_set_src(icon, def->icon); + lv_obj_set_size(icon, 64, 64); // scale-to-fit by size (reliable, unlike set_scale) + lv_image_set_inner_align(icon, LV_IMAGE_ALIGN_CONTAIN); - lv_obj_t *txt = lv_label_create(col); + lv_obj_t *txt = lv_label_create(tile); lv_label_set_text(txt, def->label); - return tile; + lv_obj_set_style_text_font(txt, &lisael_font_24, 0); } // --- internal temperature sensor (graceful fallback) ------------------------ @@ -135,16 +193,7 @@ lv_obj_t *lisael_screen_home(void) lv_label_set_text(s_sensor_lbl, ""); lv_obj_align(s_sensor_lbl, LV_ALIGN_TOP_RIGHT, -12, 58); - // --- mode tiles ---------------------------------------------------------- - lv_obj_t *grid = lv_obj_create(s_root); - lv_obj_remove_style_all(grid); - lv_obj_set_size(grid, LV_PCT(100), 160); - lv_obj_align(grid, LV_ALIGN_BOTTOM_MID, 0, -2); - lv_obj_set_flex_flow(grid, LV_FLEX_FLOW_ROW_WRAP); - lv_obj_set_flex_align(grid, LV_FLEX_ALIGN_SPACE_EVENLY, - LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_SPACE_EVENLY); - lv_obj_set_style_pad_row(grid, 8, 0); - + // --- activities: horizontal carousel (one big tile per slide) ----------- static const tile_def_t tiles[] = { { &lisael_ic_histoires, "Histoires", LISAEL_MODE_HISTOIRES, {0} }, { &lisael_ic_book, "Lire", LISAEL_MODE_LIRE, {0} }, @@ -157,12 +206,51 @@ lv_obj_t *lisael_screen_home(void) static const uint32_t colors[] = { 0xFFE08A, 0xFFD8A8, 0xB2F2BB, 0xA5D8FF, 0xD0BFFF, 0xFFC9C9, 0xCED4DA, }; - for (size_t i = 0; i < sizeof(tiles) / sizeof(tiles[0]); i++) { + s_n_acts = (int)(sizeof(tiles) / sizeof(tiles[0])); + + s_track = lv_obj_create(s_root); + lv_obj_remove_style_all(s_track); + lv_obj_set_size(s_track, HOME_SLIDE_W, 134); + lv_obj_align(s_track, LV_ALIGN_TOP_MID, 0, 80); + lv_obj_set_flex_flow(s_track, LV_FLEX_FLOW_ROW); + lv_obj_set_style_pad_all(s_track, 0, 0); + lv_obj_set_style_pad_column(s_track, 0, 0); + lv_obj_set_scroll_dir(s_track, LV_DIR_HOR); + lv_obj_set_scroll_snap_x(s_track, LV_SCROLL_SNAP_CENTER); + lv_obj_set_scrollbar_mode(s_track, LV_SCROLLBAR_MODE_OFF); + lv_obj_add_event_cb(s_track, home_scroll_cb, LV_EVENT_SCROLL_END, NULL); + + for (int i = 0; i < s_n_acts; i++) { tile_def_t t = tiles[i]; t.color = lv_color_hex(colors[i]); - make_tile(grid, &t); + add_act_slide(s_track, &t); } + lv_obj_t *dotrow = lv_obj_create(s_root); + lv_obj_remove_style_all(dotrow); + lv_obj_set_size(dotrow, LV_PCT(90), 16); + lv_obj_align(dotrow, LV_ALIGN_BOTTOM_MID, 0, -6); + lv_obj_set_flex_flow(dotrow, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(dotrow, LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_clear_flag(dotrow, LV_OBJ_FLAG_SCROLLABLE); + for (int i = 0; i < s_n_acts; i++) { + lv_obj_t *d = lv_obj_create(dotrow); + lv_obj_remove_style_all(d); + lv_obj_set_size(d, 9, 9); + lv_obj_set_style_radius(d, 5, 0); + lv_obj_set_style_margin_left(d, 3, 0); + lv_obj_set_style_margin_right(d, 3, 0); + lv_obj_set_style_bg_color(d, lv_color_hex(0x1971C2), 0); + lv_obj_set_style_bg_opa(d, i == 0 ? LV_OPA_COVER : LV_OPA_30, 0); + s_adots[i] = d; + } + + home_arrow(s_root, LV_SYMBOL_LEFT, LV_ALIGN_LEFT_MID, -1); + home_arrow(s_root, LV_SYMBOL_RIGHT, LV_ALIGN_RIGHT_MID, +1); + s_acur = 0; + adots_update(0); + // NB: the live values are filled in shortly by the tick/net tasks via // lisael_home_update_*(). We do NOT call them here: this builder runs while // the caller already holds the display lock, and those helpers take the