Scrivere un widget
Il contratto di un widget in C, le regole, la cassetta degli attrezzi e come verificarlo.
Un widget è un file C in ui/widgets/<id>.c. Niente altro da registrare: il build cerca il simbolo
jz_wdef_<id> e lo mette nel registro. Il widget gira identico sul simulatore macOS e sulla scheda.
Il contratto
#include "jz/widget.h"
typedef struct { lv_obj_t *value; int count; } state_t; // azzerato prima di create
static void create(jz_widget_t *w, lv_obj_t *root); // costruisce la UI dentro root (480×384)
static void on_msg(jz_widget_t *w, const jz_msg_t *m); // stato dal companion
static void tick(jz_widget_t *w, uint32_t now_ms); // ogni 250 ms mentre è visibile (opzionale)
static void demo(jz_widget_t *w); // dati finti per la preview
static void destroy(jz_widget_t *w); // opzionale: ferma timer/animazioni
const jz_widget_def_t jz_wdef_<id> = {
.id = "<id>", // snake_case, è il seme del mon e il nome nel protocollo
.name = "Nome breve", // va nella barra in alto e nel selettore
.summary = "Una riga.",
.category = JZ_CAT_..., // vedi jz/widget.h
.sources = JZ_SRC_LOCAL | JZ_SRC_HID | JZ_SRC_COMPANION,
.catalog = 42, // numero in docs/widget.md (0 per i sette di Jacopo)
.state_size = sizeof(state_t),
.create = create, .on_msg = on_msg, .tick = tick, .demo = demo,
};w->state è il tuo state_t. w->root è il contenitore. I figli di root vengono distrutti da LVGL
quando si cambia widget: non serve liberarli a mano. Se crei lv_timer o animazioni tue, fermali in
destroy.
Regole
- Senza dati deve avere senso.
createdisegna uno stato vuoto o degli zeri;on_msgriempie. Lo stato vuoto usajz_empty(parent, icona, "testo"). Il testo può avere un po' di ironia (voce di jz.land: "Terreno libero.", "Qui non passa nessuno."), mai negli errori. demo()costruisce i messaggi come li manderebbe il companion e li passa aon_msg. È la preview del simulatore e la documentazione del protocollo: in testa al file, in un commento, elenca le chiavi dei dati (host → device) e le azioni degli eventi (device → host).- Una cosa sola per schermata. Il contenuto sta in 480×384. Se non entra, è un altro widget.
- Un solo bottone
JZ_BTN_PRIMARYper vista. Gli altri sonoSECONDARY,OUTLINE,GHOST.DESTRUCTIVEsolo per azioni distruttive, e sempre conjz_confirm(conferma al secondo tap). - Il colore ha un significato fisso.
JZ_C_SUCCESSfatto,JZ_C_WARNINGattenzione,JZ_C_DESTRUCTIVEerrore,JZ_C_INFOinformazione (raro).JZ_C_LOGO(Ruggine) mai nei widget. Gli stati non si affidano al solo colore: c'è sempre un'icona o una parola. - Niente ombre, sfumature, trasparenze su aree grandi, animazioni a schermo intero. Movimento
solo con
jz_sediment(entrate) ejz_stagger(liste), oppurejz_anim_intper i valori. - Aggiorna solo ciò che cambia. In
on_msgetickcambia il testo delle label esistenti (lv_label_set_text,jz_label_set_textper le label a riga singola), non ricostruire tutto. Ricostruire la lista (lv_obj_clean+ ricreare) va bene quando arriva un messaggio nuovo, non ogni tick. - Niente
mallocnel tick, niente stringhe oltre 255 byte, niente float dove basta un int. - Testi in italiano, iniziale maiuscola solo sulla prima parola dei bottoni, verbo all'inizio
("Avvia", "Apri", "Segna fatto"). Niente emoji. Etichette tecniche in minuscolo (
JZ_T_LABEL). - Le azioni verso fuori:
jz_hid_key(JZ_MOD_CMD, 'd'),jz_hid_media(JZ_MEDIA_PLAY_PAUSE),jz_hid_type("npm test\n"),jz_event_int(w, AZIONE, valore). Un evento con azione 0 è riservato (= widget mostrato). Dopo un'azione dai un riscontro: cambia lo stato del bottone ojz_toast("Fatto").
La cassetta degli attrezzi (jz/theme.h e jz/widget.h)
| Cosa | Funzione |
|---|---|
| Pagina scorrevole con padding | jz_page(root) |
| Griglia di tile che riempie lo schermo | jz_tile_grid(root, cols, rows) + jz_tile(...) + lv_obj_set_grid_cell |
| Contenitori | jz_box, jz_row(parent, gap), jz_col(parent, gap), jz_card, jz_fill(obj) |
| Testo | jz_label(parent, txt, JZ_T_*), jz_label_w (troncata), jz_label_oneline, jz_label_color |
| Valore grande con etichetta | jz_stat(parent, "etichetta", "42", JZ_C_NONE) → ritorna la label del valore |
| Icone | `jz_icon(parent, JZ_ICON_MIC, 20 |
| Bottoni | jz_button(parent, "Testo", icona_o_NULL, JZ_BTN_*), jz_icon_button, jz_tile, jz_button_set_active |
| Stati | jz_badge(parent, "in corso", JZ_C_SUCCESS), jz_dot, jz_kbd(parent, "⌘D") |
| Progresso | jz_progress(parent, v, max, ruolo), jz_arc(parent, v, max, size, ruolo), jz_dots(parent, fatti, totali, ruolo) |
| Controlli | jz_switch(parent, on), jz_slider(parent, min, max, v) |
| Liste | jz_list_item(parent, icona, titolo, sottotitolo, valore), jz_separator, jz_section(parent, "titolo") |
| Vuoto | jz_empty(parent, icona, "testo") |
| Feedback | jz_toast("…"), jz_confirm(btn, "Confermi?") |
| Archivio locale | jz_store_get(w, "chiave", &buf, sizeof buf), jz_store_set(...) |
| Tempo | jz_time_now() (struct con ora locale), jz_ms(), jz_epoch() |
| Audio | jz_platform_sound(JZ_SOUND_DONE), jz_platform_record_start/stop, jz_platform_mic_level() |
| Grafici | lv_chart con jz_chart_color(i) per le serie |
Font: JZ_T_H1 44, JZ_T_H2 30, JZ_T_H3 22, JZ_T_BODY 20, JZ_T_SMALL 18, JZ_T_DATA 18
tabellare, JZ_T_LABEL 14 (Chakra Petch, minuscolo), JZ_T_META 18 (Chakra Petch),
JZ_T_CODE 16 mono, JZ_T_NUM_L 64 e JZ_T_NUM_XL 96 (solo cifre, :, -, ., %, °).
Le icone disponibili sono in tools/icons.txt (nomi Lucide). Per aggiungerne una: riga nel file e
node tools/gen-icons.mjs. Spazi: JZ_SP1..JZ_SP16 (4, 8, 12, 16, 24, 32, 48, 64).
Messaggi
// lettura
int32_t n = jz_msg_int(m, 0, 0);
const char *s = jz_msg_str(m, 1, "");
int items = jz_msg_count(m); // JZ_F_COUNT
for (int i = 0; i < items && i < JZ_LIST_MAX; i++) // al massimo 14 elementi
jz_msg_str(m, JZ_K(i, 0), ""); // campo 0 dell'elemento i
// costruzione (demo)
jz_msg_t msg; jz_msg_init(&msg, w->index, JZ_KIND_DATA);
jz_msg_add_int(&msg, 0, 3); jz_msg_add_str(&msg, JZ_K(0, 0), "titolo");
on_msg(w, &msg);Chiavi scalari da 0 a 14. Le liste usano JZ_K(i, campo). Immagini (copertine): il companion le
manda come blob già in RGB565 e arrivano in on_blob(w, slot, data, len); si mostrano con
lv_image_dsc_t + lv_image_set_src.
Il tasto BOOT
Il tasto BOOT apre la barra dei widget confermati: un widget non riceve il tasto. Per far comparire
un widget nella barra (e in testa al mission control) gli si dà .category = JZ_CAT_CONFERMATI,
quando è stato provato dal vero con il companion.
Cose imparate sul campo
- I contenitori semplici (
jz_box,jz_row,jz_col,jz_card) non catturano i tap: il tocco arriva alla riga o al bottone cliccabile sotto. Una riga cliccabile vuolelv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE). - Le label a riga singola (
jz_label_w,jz_label_oneline, quelle dentrojz_tileejz_list_item) tengono il testo originale nellouser_data: non usarlo per altro. Aggiornale conjz_label_set_text. jz_confirmriporta il bottone aSECONDARYdopo il timeout: parti da un bottoneSECONDARY.jz_confirmusa louser_datadel bottone: passa i tuoi dati come user data dell'evento, non dell'oggetto.- I font non hanno ⌘ ⇧ ⌃ ⌥ → ₂ µ (nel label font): scrivi "cmd shift 5", "da X a Y", "co2".
I font
NUM_L/NUM_XLhanno solo cifre,:,-,.,%,°: niente virgola, niente lettere. - Un valore con il font cambiato (
jz_label_set_style) dentro una label a riga singola vuole anchelv_obj_set_height(l, lv_font_get_line_height(font)), altrimenti ritaglia. - Uno spaziatore in una riga:
lv_obj_t *sp = jz_box(row); lv_obj_set_size(sp, 0, 1); jz_fill(sp);. - Una lista che può superare lo schermo: rendila scorrevole lei (
LV_OBJ_FLAG_SCROLLABLE,LV_DIR_VER), lajz_pagenon cresce oltre i 384 px. - Due righe con i puntini:
LV_LABEL_LONG_DOTcon altezza fissa2 × line_height.
Verifica
cd sim/build && ninja && ./jz_sim --widget <id> --screenshot /tmp/<id>.pngGuarda il PNG. Prova anche --light. Per vederli tutti insieme: ./sim/build/jz_sim --all docs/shots
e poi node tools/contact-sheet.mjs docs/shots /tmp/sheets (fogli da 12). Lo screenshot viene preso 700 ms dopo l'apertura, con i dati
di demo(). Il widget deve essere leggibile a un metro di distanza: niente testi sotto i 14 px,
bottoni alti almeno 44 px.