mirror of
https://github.com/skoelle/wt32sc01-dashboard.git
synced 2026-09-17 17:30:25 +00:00
Phase 6: procedural LVGL canvas icons + manual retry on weather tile
- src/icons/icons.{h,cpp}: colored procedural icons drawn onto LVGL
canvases (PSRAM-backed ARGB8888): sun, moon, cloud, rain, thunderstorm
(selected from API symbol field), rain-warning triangle, retry/error
symbol, back arrow. Replaces old M5GFX-based icons.h.
- tile_button: add tile_button_set_icon_obj() to swap text glyph for a
canvas icon object
- home_screen: weather tile now shows the real procedural weather icon
(rebuilt on each refresh); tapping the weather tile in error state
triggers a manual retry (SPEC.md section 8) instead of navigating
This commit is contained in:
@@ -0,0 +1,187 @@
|
|||||||
|
#include "icons.h"
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
#ifndef M_PI
|
||||||
|
#define M_PI 3.14159265358979323846
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace Icons {
|
||||||
|
|
||||||
|
// Helper: create a transparent canvas of the given square size.
|
||||||
|
static lv_obj_t *makeCanvas(lv_obj_t *parent, int size) {
|
||||||
|
lv_obj_t *c = lv_canvas_create(parent);
|
||||||
|
lv_coord_t w = size, h = size;
|
||||||
|
// Allocate draw buffer in PSRAM (ARGB8888 = 32 bpp, stride 0).
|
||||||
|
uint32_t buf_size = lv_canvas_buf_size(w, h, 32, 1);
|
||||||
|
void *buf = heap_caps_malloc(buf_size, MALLOC_CAP_SPIRAM);
|
||||||
|
lv_canvas_set_buffer(c, buf, w, h, LV_COLOR_FORMAT_ARGB8888);
|
||||||
|
lv_obj_set_size(c, w, h);
|
||||||
|
lv_canvas_fill_bg(c, lv_color_make(0, 0, 0), LV_OPA_TRANSP);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void drawCircle(lv_obj_t *canvas, int cx, int cy, int r, lv_color_t color) {
|
||||||
|
lv_layer_t layer;
|
||||||
|
lv_canvas_init_layer(canvas, &layer);
|
||||||
|
|
||||||
|
lv_draw_rect_dsc_t dsc;
|
||||||
|
lv_draw_rect_dsc_init(&dsc);
|
||||||
|
dsc.radius = LV_RADIUS_CIRCLE;
|
||||||
|
dsc.bg_color = color;
|
||||||
|
dsc.bg_opa = LV_OPA_COVER;
|
||||||
|
dsc.border_width = 0;
|
||||||
|
|
||||||
|
lv_area_t a;
|
||||||
|
a.x1 = cx - r; a.y1 = cy - r;
|
||||||
|
a.x2 = cx + r; a.y2 = cy + r;
|
||||||
|
lv_draw_rect(&layer, &dsc, &a);
|
||||||
|
|
||||||
|
lv_canvas_finish_layer(canvas, &layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void fillRect(lv_obj_t *canvas, int x, int y, int w, int h, lv_color_t color) {
|
||||||
|
lv_layer_t layer;
|
||||||
|
lv_canvas_init_layer(canvas, &layer);
|
||||||
|
|
||||||
|
lv_draw_rect_dsc_t dsc;
|
||||||
|
lv_draw_rect_dsc_init(&dsc);
|
||||||
|
dsc.radius = 0;
|
||||||
|
dsc.bg_color = color;
|
||||||
|
dsc.bg_opa = LV_OPA_COVER;
|
||||||
|
dsc.border_width = 0;
|
||||||
|
|
||||||
|
lv_area_t a;
|
||||||
|
a.x1 = x; a.y1 = y;
|
||||||
|
a.x2 = x + w - 1; a.y2 = y + h - 1;
|
||||||
|
lv_draw_rect(&layer, &dsc, &a);
|
||||||
|
|
||||||
|
lv_canvas_finish_layer(canvas, &layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void drawLine(lv_obj_t *canvas, int x1, int y1, int x2, int y2,
|
||||||
|
lv_color_t color, int width) {
|
||||||
|
lv_layer_t layer;
|
||||||
|
lv_canvas_init_layer(canvas, &layer);
|
||||||
|
|
||||||
|
lv_draw_line_dsc_t dsc;
|
||||||
|
lv_draw_line_dsc_init(&dsc);
|
||||||
|
dsc.color = color;
|
||||||
|
dsc.width = width;
|
||||||
|
dsc.opa = LV_OPA_COVER;
|
||||||
|
dsc.round_end = 1;
|
||||||
|
dsc.round_start = 1;
|
||||||
|
dsc.p1.x = x1; dsc.p1.y = y1;
|
||||||
|
dsc.p2.x = x2; dsc.p2.y = y2;
|
||||||
|
|
||||||
|
lv_draw_line(&layer, &dsc);
|
||||||
|
|
||||||
|
lv_canvas_finish_layer(canvas, &layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cloud composed of three overlapping circles + a filler rectangle.
|
||||||
|
static void drawCloud(lv_obj_t *canvas, int cx, int cy, int scale, lv_color_t color) {
|
||||||
|
drawCircle(canvas, cx - scale, cy, scale, color);
|
||||||
|
drawCircle(canvas, cx + scale, cy, scale, color);
|
||||||
|
drawCircle(canvas, cx, cy - scale / 2, scale + 2, color);
|
||||||
|
fillRect(canvas, cx - scale, cy, scale * 2, scale, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
lv_obj_t *createWeatherIcon(lv_obj_t *parent, const String &symbol,
|
||||||
|
const String &description, int size) {
|
||||||
|
lv_obj_t *c = makeCanvas(parent, size);
|
||||||
|
bool isNight = symbol.length() > 0 && symbol.charAt(0) == 'm';
|
||||||
|
char condition = symbol.length() > 1 ? symbol.charAt(1) : 'o';
|
||||||
|
bool isThunder = (condition == 't') ||
|
||||||
|
(description.indexOf("Gewitter") >= 0);
|
||||||
|
|
||||||
|
int cx = size / 2;
|
||||||
|
int cy = size / 2;
|
||||||
|
int r = size / 4;
|
||||||
|
|
||||||
|
if (isThunder) {
|
||||||
|
drawCloud(c, cx, cy - r / 2, r * 2 / 3, Theme::textDim());
|
||||||
|
drawLine(c, cx, cy + r / 2, cx - r / 3, cy + r, Theme::accentStorm(), 3);
|
||||||
|
drawLine(c, cx - r / 3, cy + r, cx + r / 4, cy + r + r / 2, Theme::accentStorm(), 3);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (condition) {
|
||||||
|
case 'o': // clear
|
||||||
|
if (isNight) {
|
||||||
|
drawCircle(c, cx, cy, r, Theme::textDim());
|
||||||
|
drawCircle(c, cx + r / 2, cy - r / 3, r, lv_color_hex(0x000000));
|
||||||
|
} else {
|
||||||
|
drawCircle(c, cx, cy, r, Theme::accentSun());
|
||||||
|
for (int i = 0; i < 8; ++i) {
|
||||||
|
float a = i * (M_PI / 4.0f);
|
||||||
|
int x1 = cx + (int)(cosf(a) * (r + 2));
|
||||||
|
int y1 = cy + (int)(sinf(a) * (r + 2));
|
||||||
|
int x2 = cx + (int)(cosf(a) * (r + 6));
|
||||||
|
int y2 = cy + (int)(sinf(a) * (r + 6));
|
||||||
|
drawLine(c, x1, y1, x2, y2, Theme::accentSun(), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'b': // cloudy
|
||||||
|
drawCloud(c, cx, cy, r * 2 / 3, Theme::textDim());
|
||||||
|
break;
|
||||||
|
case 'r': // rain
|
||||||
|
drawCloud(c, cx, cy - 3, r * 2 / 3, Theme::textDim());
|
||||||
|
for (int i = -1; i <= 1; ++i) {
|
||||||
|
drawLine(c, cx + i * 6, cy + r / 2,
|
||||||
|
cx + i * 6 - 2, cy + r, Theme::accentRain(), 2);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
drawCloud(c, cx, cy, r * 2 / 3, Theme::textDim());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
lv_obj_t *createRainWarning(lv_obj_t *parent, int size) {
|
||||||
|
lv_obj_t *c = makeCanvas(parent, size);
|
||||||
|
int cx = size / 2;
|
||||||
|
// Triangle outline via three lines.
|
||||||
|
int top = 2, bottom = size - 2;
|
||||||
|
int left = 4, right = size - 4;
|
||||||
|
drawLine(c, cx, top, left, bottom, Theme::accentRain(), 2);
|
||||||
|
drawLine(c, left, bottom, right, bottom, Theme::accentRain(), 2);
|
||||||
|
drawLine(c, right, bottom, cx, top, Theme::accentRain(), 2);
|
||||||
|
// Exclamation dot.
|
||||||
|
fillRect(c, cx - 1, cx - 2, 2, 6, Theme::accentRain());
|
||||||
|
fillRect(c, cx - 1, bottom - 4, 2, 2, Theme::accentRain());
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
lv_obj_t *createRetryIcon(lv_obj_t *parent, int size) {
|
||||||
|
lv_obj_t *c = makeCanvas(parent, size);
|
||||||
|
int cx = size / 2;
|
||||||
|
int r = size / 2 - 2;
|
||||||
|
// Circle outline (drawn as thick ring via 4 arcs of lines).
|
||||||
|
for (int i = 0; i < 24; ++i) {
|
||||||
|
float a = i * (2 * M_PI / 24);
|
||||||
|
int x = cx + (int)(cosf(a) * r);
|
||||||
|
int y = cx + (int)(sinf(a) * r);
|
||||||
|
fillRect(c, x - 1, y - 1, 2, 2, Theme::accentError());
|
||||||
|
}
|
||||||
|
// Exclamation mark.
|
||||||
|
fillRect(c, cx - 1, cx - 4, 2, 6, Theme::accentError());
|
||||||
|
fillRect(c, cx - 1, cx + 3, 2, 2, Theme::accentError());
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
lv_obj_t *createBackArrow(lv_obj_t *parent, int size) {
|
||||||
|
lv_obj_t *c = makeCanvas(parent, size);
|
||||||
|
int cx = size / 2;
|
||||||
|
int cy = size / 2;
|
||||||
|
int len = size / 3;
|
||||||
|
// Arrow head.
|
||||||
|
drawLine(c, cx - len, cy, cx, cy - len, Theme::text(), 3);
|
||||||
|
drawLine(c, cx - len, cy, cx, cy + len, Theme::text(), 3);
|
||||||
|
// Shaft.
|
||||||
|
drawLine(c, cx - len, cy, cx + len, cy, Theme::text(), 3);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Icons
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <lvgl.h>
|
||||||
|
#include <theme.h>
|
||||||
|
|
||||||
|
// Colored procedural icons drawn onto LVGL canvases (no external bitmap
|
||||||
|
// assets). Replaces the old M5GFX-based icons.h. Each builder returns an
|
||||||
|
// lv_obj_t* canvas of the given size, drawn once at create time.
|
||||||
|
namespace Icons {
|
||||||
|
|
||||||
|
// Weather icon selected from the API `symbol` field (m=night, condition
|
||||||
|
// char = symbol[1]: o=clear, b=cloudy, r=rain, t=thunder).
|
||||||
|
lv_obj_t *createWeatherIcon(lv_obj_t *parent, const String &symbol,
|
||||||
|
const String &description, int size);
|
||||||
|
|
||||||
|
// Rain warning triangle (for the weather tile badge).
|
||||||
|
lv_obj_t *createRainWarning(lv_obj_t *parent, int size);
|
||||||
|
|
||||||
|
// Retry / error symbol (circle + exclamation).
|
||||||
|
lv_obj_t *createRetryIcon(lv_obj_t *parent, int size);
|
||||||
|
|
||||||
|
// Back arrow (used if a non-symbol back button variant is desired).
|
||||||
|
lv_obj_t *createBackArrow(lv_obj_t *parent, int size);
|
||||||
|
|
||||||
|
} // namespace Icons
|
||||||
+26
-5
@@ -1,5 +1,6 @@
|
|||||||
#include "home_screen.h"
|
#include "home_screen.h"
|
||||||
#include "ui/widgets/tile_button.h"
|
#include "ui/widgets/tile_button.h"
|
||||||
|
#include "icons/icons.h"
|
||||||
#include "api/weather_api.h"
|
#include "api/weather_api.h"
|
||||||
#include "api/calendar_api.h"
|
#include "api/calendar_api.h"
|
||||||
#include <theme.h>
|
#include <theme.h>
|
||||||
@@ -12,7 +13,10 @@ namespace {
|
|||||||
// --- navigation callback target (set by main.cpp) ---
|
// --- navigation callback target (set by main.cpp) ---
|
||||||
ScreenId (*g_navigate)(ScreenId) = nullptr;
|
ScreenId (*g_navigate)(ScreenId) = nullptr;
|
||||||
|
|
||||||
void on_weather_tile(lv_event_t *) { if (g_navigate) g_navigate(ScreenId::WEATHER_DETAIL); }
|
// Forward declarations (definitions below).
|
||||||
|
void updateWidgets();
|
||||||
|
void doFetch();
|
||||||
|
|
||||||
void on_calendar_tile(lv_event_t *) { if (g_navigate) g_navigate(ScreenId::CALENDAR_DETAIL); }
|
void on_calendar_tile(lv_event_t *) { if (g_navigate) g_navigate(ScreenId::CALENDAR_DETAIL); }
|
||||||
void on_mvg_tile(lv_event_t *) { if (g_navigate) g_navigate(ScreenId::MVG); }
|
void on_mvg_tile(lv_event_t *) { if (g_navigate) g_navigate(ScreenId::MVG); }
|
||||||
|
|
||||||
@@ -30,6 +34,13 @@ TileButton calendarTile{};
|
|||||||
TileButton mvgTile{};
|
TileButton mvgTile{};
|
||||||
bool widgetsBuilt = false;
|
bool widgetsBuilt = false;
|
||||||
|
|
||||||
|
// Rebuild the weather canvas icon (deleted + recreated on each refresh
|
||||||
|
// because the symbol may change).
|
||||||
|
void rebuildWeatherIcon(const String &symbol, const String &description) {
|
||||||
|
lv_obj_t *ic = Icons::createWeatherIcon(weatherTile.btn, symbol, description, 48);
|
||||||
|
tile_button_set_icon_obj(weatherTile, ic);
|
||||||
|
}
|
||||||
|
|
||||||
void doFetch() {
|
void doFetch() {
|
||||||
WeatherData w = fetchWeather();
|
WeatherData w = fetchWeather();
|
||||||
weatherOk = w.valid;
|
weatherOk = w.valid;
|
||||||
@@ -40,13 +51,23 @@ void doFetch() {
|
|||||||
lastFetchMs = millis();
|
lastFetchMs = millis();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Manual retry: tapping the weather tile when it is in error state triggers
|
||||||
|
// a refresh instead of navigating away. Acts as the SPEC.md "retry per tap".
|
||||||
|
void on_weather_tile(lv_event_t *e) {
|
||||||
|
if (!weatherOk) {
|
||||||
|
doFetch();
|
||||||
|
updateWidgets();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (g_navigate) g_navigate(ScreenId::WEATHER_DETAIL);
|
||||||
|
}
|
||||||
|
|
||||||
// --- formatting helpers (ported from old home_screen.cpp rendering) ---
|
// --- formatting helpers (ported from old home_screen.cpp rendering) ---
|
||||||
|
|
||||||
String weatherIconGlyph(const String &symbol) {
|
String weatherIconGlyph(const String &symbol) {
|
||||||
// Phase 4 placeholder: map symbol condition to a unicode glyph until
|
// Kept for fallback; real icons are drawn as canvas via rebuildWeatherIcon.
|
||||||
// bitmap icons land in Phase 6. m=night, condition char = symbol[1].
|
|
||||||
(void)symbol;
|
(void)symbol;
|
||||||
return LV_SYMBOL_IMAGE; // cloud/weather placeholder; bitmaps come in Phase 6
|
return LV_SYMBOL_IMAGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
String formatWeatherValue(const WeatherData &w) {
|
String formatWeatherValue(const WeatherData &w) {
|
||||||
@@ -75,7 +96,7 @@ String formatCalendarPreview(const CalendarData &c) {
|
|||||||
void updateWidgets() {
|
void updateWidgets() {
|
||||||
if (!widgetsBuilt) return;
|
if (!widgetsBuilt) return;
|
||||||
tile_button_set_value(weatherTile, formatWeatherValue(lastWeather).c_str());
|
tile_button_set_value(weatherTile, formatWeatherValue(lastWeather).c_str());
|
||||||
tile_button_set_icon(weatherTile, weatherIconGlyph(lastWeather.current.symbol).c_str());
|
rebuildWeatherIcon(lastWeather.current.symbol, lastWeather.current.description);
|
||||||
tile_button_set_value(calendarTile, formatCalendarPreview(lastCalendar).c_str());
|
tile_button_set_value(calendarTile, formatCalendarPreview(lastCalendar).c_str());
|
||||||
tile_button_set_icon(calendarTile, LV_SYMBOL_FILE);
|
tile_button_set_icon(calendarTile, LV_SYMBOL_FILE);
|
||||||
tile_button_set_value(mvgTile, "");
|
tile_button_set_value(mvgTile, "");
|
||||||
|
|||||||
@@ -48,3 +48,9 @@ void tile_button_set_value(TileButton &t, const char *text) {
|
|||||||
void tile_button_set_icon(TileButton &t, const char *text) {
|
void tile_button_set_icon(TileButton &t, const char *text) {
|
||||||
if (t.icon) lv_label_set_text(t.icon, text);
|
if (t.icon) lv_label_set_text(t.icon, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void tile_button_set_icon_obj(TileButton &t, lv_obj_t *icon_obj) {
|
||||||
|
if (t.icon) lv_obj_del(t.icon);
|
||||||
|
t.icon = icon_obj;
|
||||||
|
if (t.icon) lv_obj_align(t.icon, LV_ALIGN_TOP_RIGHT, 0, 0);
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,3 +24,7 @@ void tile_button_set_value(TileButton &t, const char *text);
|
|||||||
// Set the icon text (single glyph/symbol). Icons are rendered as text labels
|
// Set the icon text (single glyph/symbol). Icons are rendered as text labels
|
||||||
// using LVGL's built-in font until bitmap icons land in Phase 6.
|
// using LVGL's built-in font until bitmap icons land in Phase 6.
|
||||||
void tile_button_set_icon(TileButton &t, const char *text);
|
void tile_button_set_icon(TileButton &t, const char *text);
|
||||||
|
|
||||||
|
// Replace the text icon with an already-created canvas object (the caller
|
||||||
|
// creates the canvas as a child of t.btn). Useful for bitmap/procedural icons.
|
||||||
|
void tile_button_set_icon_obj(TileButton &t, lv_obj_t *icon_obj);
|
||||||
|
|||||||
Reference in New Issue
Block a user