jzland docs
Riferimento tecnico

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

  1. Senza dati deve avere senso. create disegna uno stato vuoto o degli zeri; on_msg riempie. Lo stato vuoto usa jz_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.
  2. demo() costruisce i messaggi come li manderebbe il companion e li passa a on_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).
  3. Una cosa sola per schermata. Il contenuto sta in 480×384. Se non entra, è un altro widget.
  4. Un solo bottone JZ_BTN_PRIMARY per vista. Gli altri sono SECONDARY, OUTLINE, GHOST. DESTRUCTIVE solo per azioni distruttive, e sempre con jz_confirm (conferma al secondo tap).
  5. Il colore ha un significato fisso. JZ_C_SUCCESS fatto, JZ_C_WARNING attenzione, JZ_C_DESTRUCTIVE errore, JZ_C_INFO informazione (raro). JZ_C_LOGO (Ruggine) mai nei widget. Gli stati non si affidano al solo colore: c'è sempre un'icona o una parola.
  6. Niente ombre, sfumature, trasparenze su aree grandi, animazioni a schermo intero. Movimento solo con jz_sediment (entrate) e jz_stagger (liste), oppure jz_anim_int per i valori.
  7. Aggiorna solo ciò che cambia. In on_msg e tick cambia il testo delle label esistenti (lv_label_set_text, jz_label_set_text per 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.
  8. Niente malloc nel tick, niente stringhe oltre 255 byte, niente float dove basta un int.
  9. 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).
  10. 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 o jz_toast("Fatto").

La cassetta degli attrezzi (jz/theme.h e jz/widget.h)

CosaFunzione
Pagina scorrevole con paddingjz_page(root)
Griglia di tile che riempie lo schermojz_tile_grid(root, cols, rows) + jz_tile(...) + lv_obj_set_grid_cell
Contenitorijz_box, jz_row(parent, gap), jz_col(parent, gap), jz_card, jz_fill(obj)
Testojz_label(parent, txt, JZ_T_*), jz_label_w (troncata), jz_label_oneline, jz_label_color
Valore grande con etichettajz_stat(parent, "etichetta", "42", JZ_C_NONE) → ritorna la label del valore
Icone`jz_icon(parent, JZ_ICON_MIC, 20
Bottonijz_button(parent, "Testo", icona_o_NULL, JZ_BTN_*), jz_icon_button, jz_tile, jz_button_set_active
Statijz_badge(parent, "in corso", JZ_C_SUCCESS), jz_dot, jz_kbd(parent, "⌘D")
Progressojz_progress(parent, v, max, ruolo), jz_arc(parent, v, max, size, ruolo), jz_dots(parent, fatti, totali, ruolo)
Controllijz_switch(parent, on), jz_slider(parent, min, max, v)
Listejz_list_item(parent, icona, titolo, sottotitolo, valore), jz_separator, jz_section(parent, "titolo")
Vuotojz_empty(parent, icona, "testo")
Feedbackjz_toast("…"), jz_confirm(btn, "Confermi?")
Archivio localejz_store_get(w, "chiave", &buf, sizeof buf), jz_store_set(...)
Tempojz_time_now() (struct con ora locale), jz_ms(), jz_epoch()
Audiojz_platform_sound(JZ_SOUND_DONE), jz_platform_record_start/stop, jz_platform_mic_level()
Graficilv_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 vuole lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE).
  • Le label a riga singola (jz_label_w, jz_label_oneline, quelle dentro jz_tile e jz_list_item) tengono il testo originale nello user_data: non usarlo per altro. Aggiornale con jz_label_set_text.
  • jz_confirm riporta il bottone a SECONDARY dopo il timeout: parti da un bottone SECONDARY. jz_confirm usa lo user_data del 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_XL hanno solo cifre, :, -, ., %, °: niente virgola, niente lettere.
  • Un valore con il font cambiato (jz_label_set_style) dentro una label a riga singola vuole anche lv_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), la jz_page non cresce oltre i 384 px.
  • Due righe con i puntini: LV_LABEL_LONG_DOT con altezza fissa 2 × line_height.

Verifica

cd sim/build && ninja && ./jz_sim --widget <id> --screenshot /tmp/<id>.png

Guarda 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.

On this page