-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathui.cpp
More file actions
652 lines (566 loc) · 24.7 KB
/
Copy pathui.cpp
File metadata and controls
652 lines (566 loc) · 24.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
#include "ui.h"
#include <lvgl.h>
#include <cstdint>
#include <cstdio>
#include "ui_theme.h"
#include "storage.h"
#include "i18n.h"
#include "fonts_extra.h"
#include "debug.h"
// Definita in main.cpp: riavvia la trasmissione del pannello RGB per azzerare il
// drift dopo una scrittura flash. Dichiarata a scope globale (NON in namespace ui).
void displayResync();
namespace ui {
static sudoku::GameSession *S = nullptr;
// forward
static void showSplash();
static void showMenu();
static void showGame();
static void showPause();
static void showWin();
static void showRestoreDialog();
static void flushSave();
// ===================== stato schermata di gioco =====================
static lv_obj_t *g_cell[81];
static lv_obj_t *g_cellLabel[81];
static lv_obj_t *g_numBtn[10]; // tasti tastierino 1..9 (indice = cifra)
static lv_obj_t *g_notesBtn = nullptr; // toggle modalita' appunti
static bool g_notesMode = false; // true = i tasti scrivono appunti
static sudoku::Difficulty g_pendingDiff = sudoku::Difficulty::Medium; // livello scelto in attesa di conferma
static lv_obj_t *g_timerLabel = nullptr;
static lv_timer_t *g_tick = nullptr;
static lv_timer_t *g_flash = nullptr;
static lv_timer_t *g_autosaveTimer = nullptr; // autosave periodico (60s)
static lv_obj_t *g_saveStatus = nullptr; // label di stato nell'header ("Salvataggio.../Salvato")
static lv_timer_t *g_saveStatusTimer = nullptr;// nasconde "Salvato" dopo 5s
static bool g_saveDirty = false; // ci sono mosse non ancora salvate
static bool g_justSaved = false; // mostra "Salvato" nell'header alla ricostruzione
static uint32_t g_cellSig[81]; // "firma" per cella: ridisegna solo se cambia
static int8_t g_numDone[10]; // stato 'cifra completata' per tasto (evita invalidazioni)
static const int CELL = 38; // lato cella (px)
static const int BLOCKGAP = 4; // gap extra ai confini 3x3
static int cellX(int c) { return c * CELL + (c / 3) * BLOCKGAP; }
static int cellY(int r) { return r * CELL + (r / 3) * BLOCKGAP; }
static void fmtTime(uint32_t ms, char *buf, size_t n) {
uint32_t s = ms / 1000;
snprintf(buf, n, "%02u:%02u", (unsigned)(s / 60), (unsigned)(s % 60));
}
static const char *difName(sudoku::Difficulty d) {
switch (d) {
case sudoku::Difficulty::Easy: return i18n::tr(i18n::K_EASY);
case sudoku::Difficulty::Medium: return i18n::tr(i18n::K_MEDIUM);
case sudoku::Difficulty::Hard: return i18n::tr(i18n::K_HARD);
}
return i18n::tr(i18n::K_MEDIUM);
}
// --- Autosalvataggio periodico (ogni 60s) ----------------------------------
// La scrittura NVS (flash) blocca per decine di ms l'ISR del bounce buffer del
// pannello RGB -> underrun DMA -> drift, che NON si azzera "in posto". L'unica
// cosa che lo elimina e' il caricamento di una schermata (come fa la pausa).
// Strategia: salviamo di rado (ogni minuto), poi RICARICHIAMO la schermata di
// gioco (azzera il drift) e mostriamo "Salvato" in alto per 5s.
static void showGame(); // (gia' dichiarata sopra; usata qui per la ricarica)
static void writeSaveNow() {
if (!S) return;
sudoku::GameState st = S->state();
if (st != sudoku::GameState::Playing && st != sudoku::GameState::Paused) {
g_saveDirty = false;
return;
}
storage::saveGameAsync(S->snapshot()); // scrittura su task dedicato: niente blocco del rendering
g_saveDirty = false;
DBG("NVS save queued");
}
static void saveStatusHide_cb(lv_timer_t *t) {
if (g_saveStatus) lv_label_set_text(g_saveStatus, "");
g_saveStatusTimer = nullptr;
lv_timer_del(t);
}
static void autosave_tick_cb(lv_timer_t *) {
if (!g_saveDirty || !S || S->state() != sudoku::GameState::Playing) return;
writeSaveNow(); // accoda la scrittura (async): niente blocco, niente ricarica schermata
if (g_saveStatus) {
lv_label_set_text(g_saveStatus, i18n::tr(i18n::K_SAVED));
lv_obj_set_style_text_color(g_saveStatus, theme::good(), 0);
if (g_saveStatusTimer) lv_timer_del(g_saveStatusTimer);
g_saveStatusTimer = lv_timer_create(saveStatusHide_cb, 5000, nullptr);
}
}
static void flushSave() { // salvataggio silenzioso (pausa / uscita dal gioco)
if (g_saveDirty) writeSaveNow();
}
static void killTimers() {
if (g_tick) { lv_timer_del(g_tick); g_tick = nullptr; }
if (g_flash) { lv_timer_del(g_flash); g_flash = nullptr; }
if (g_autosaveTimer) { lv_timer_del(g_autosaveTimer); g_autosaveTimer = nullptr; }
if (g_saveStatusTimer) { lv_timer_del(g_saveStatusTimer); g_saveStatusTimer = nullptr; }
g_saveStatus = nullptr; // verra' ricreato dalla schermata di gioco
flushSave(); // persiste un eventuale salvataggio in sospeso prima di cambiare schermata
}
// stile comune: niente bordo/pad/scroll, raggio piccolo
static void plainStyle(lv_obj_t *o, lv_color_t bg, int radius) {
lv_obj_set_style_bg_color(o, bg, 0);
lv_obj_set_style_bg_opa(o, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(o, 0, 0);
lv_obj_set_style_radius(o, radius, 0);
lv_obj_set_style_pad_all(o, 0, 0);
lv_obj_clear_flag(o, LV_OBJ_FLAG_SCROLLABLE);
}
// Costruisce la stringa 3x3 degli appunti ("1 2 3\n4 5 6\n7 8 9", assente = spazio).
static void buildNotesText(uint16_t mask, char *buf) {
int p = 0;
for (int r = 0; r < 3; r++) {
for (int c = 0; c < 3; c++) {
int d = r * 3 + c + 1;
buf[p++] = (mask & (1u << (d - 1))) ? (char)('0' + d) : ' ';
if (c < 2) buf[p++] = ' ';
}
if (r < 2) buf[p++] = '\n';
}
buf[p] = 0;
}
// ===================== refresh griglia =====================
static void refreshGame() {
if (!S) return;
int sel = S->selectedCell();
int selR = sel >= 0 ? sel / 9 : -1;
int selC = sel >= 0 ? sel % 9 : -1;
uint8_t selVal = sel >= 0 ? S->board().value(sel) : 0;
char t[2] = {0, 0};
char notesBuf[20]; // "d d d\nd d d\nd d d\0" = 18 byte (16 era troppo corto -> stack smash)
for (int i = 0; i < 81; i++) {
uint8_t v = S->board().value(i);
uint16_t notes = S->board().notes(i);
bool given = S->board().isGiven(i);
int r = i / 9, c = i % 9;
uint8_t bgstate = 0; // 0 normale, 1 peer, 2 stesso numero, 3 selezionata
bool peer = (sel >= 0) &&
(r == selR || c == selC ||
((r / 3) == (selR / 3) && (c / 3) == (selC / 3)));
bool same = (selVal > 0 && v == selVal);
if (peer) bgstate = 1;
if (same) bgstate = 2;
if (i == sel) bgstate = 3;
// Firma della cella: se invariata, NON tocchiamo nulla (niente invalidazione
// -> niente ridisegno). Cosi' toccando la stessa cella si ridisegna solo quella.
uint32_t sig = (uint32_t)bgstate | ((uint32_t)given << 2) |
((uint32_t)v << 3) | ((uint32_t)notes << 8);
if (sig == g_cellSig[i]) continue;
g_cellSig[i] = sig;
if (v) {
t[0] = (char)('0' + v);
lv_label_set_text(g_cellLabel[i], t);
lv_obj_set_style_text_font(g_cellLabel[i], &lv_font_montserrat_28, 0);
lv_obj_set_style_text_color(g_cellLabel[i],
given ? theme::ink() : theme::userNum(), 0);
} else if (notes) {
buildNotesText(notes, notesBuf);
lv_label_set_text(g_cellLabel[i], notesBuf);
lv_obj_set_style_text_font(g_cellLabel[i], &lv_font_unscii_8, 0);
lv_obj_set_style_text_line_space(g_cellLabel[i], 2, 0);
lv_obj_set_style_text_letter_space(g_cellLabel[i], -3, 0);
lv_obj_set_style_text_align(g_cellLabel[i], LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_text_color(g_cellLabel[i], theme::noteInk(), 0);
} else {
lv_label_set_text(g_cellLabel[i], "");
}
lv_color_t bg = theme::cell();
if (bgstate == 1) bg = theme::peer();
else if (bgstate == 2) bg = theme::same();
else if (bgstate == 3) bg = theme::cellSel();
lv_obj_set_style_bg_color(g_cell[i], bg, 0);
}
// Tasti del tastierino: aggiorna solo quando lo stato "completato" cambia.
for (int d = 1; d <= 9; d++) {
if (!g_numBtn[d]) continue;
int8_t done = S->board().isDigitComplete((uint8_t)d) ? 1 : 0;
if (done == g_numDone[d]) continue;
g_numDone[d] = done;
lv_obj_set_style_opa(g_numBtn[d], done ? LV_OPA_40 : LV_OPA_COVER, 0);
if (done) lv_obj_clear_flag(g_numBtn[d], LV_OBJ_FLAG_CLICKABLE);
else lv_obj_add_flag(g_numBtn[d], LV_OBJ_FLAG_CLICKABLE);
}
}
// ===================== callbacks =====================
static void tick_cb(lv_timer_t *) {
if (!g_timerLabel || !S) return;
char b[8];
fmtTime(S->elapsedMs(), b, sizeof(b));
lv_label_set_text(g_timerLabel, b);
}
static void flash_clear_cb(lv_timer_t *t) {
refreshGame();
lv_timer_del(t);
g_flash = nullptr;
}
static void cell_cb(lv_event_t *e) {
int idx = (int)(intptr_t)lv_event_get_user_data(e);
DBG("tap cell %d", idx);
S->selectCell(idx);
refreshGame();
}
static void notes_cb(lv_event_t *) {
g_notesMode = !g_notesMode;
if (g_notesBtn)
lv_obj_set_style_bg_color(g_notesBtn,
g_notesMode ? theme::accent() : theme::accent2(), 0);
}
static void num_cb(lv_event_t *e) {
int d = (int)(intptr_t)lv_event_get_user_data(e); // 0 = cancella
DBG("num d=%d notes=%d", d, (int)g_notesMode);
if (g_notesMode) {
int sel = S->selectedCell();
if (sel >= 0) {
if (d == 0) {
// backspace in modalita' appunti: svuota gli appunti della cella
uint16_t m = S->board().notes(sel);
for (int n = 1; n <= 9; n++)
if (m & (1u << (n - 1))) S->toggleNote((uint8_t)n);
} else {
S->toggleNote((uint8_t)d);
}
}
refreshGame();
return;
}
S->enterValue((uint8_t)d);
if (S->state() == sudoku::GameState::Won) { showWin(); return; }
g_saveDirty = true; // salvato dall'autosave periodico (60s) o alla pausa/uscita
refreshGame();
// griglia piena ma non risolta -> lampeggio rosso sulle celle in conflitto
if (S->board().isComplete()) {
bool conf[81];
S->board().conflicts(conf);
for (int i = 0; i < 81; i++)
if (conf[i]) {
lv_obj_set_style_bg_color(g_cell[i], theme::danger(), 0);
g_cellSig[i] = 0xFFFFFFFF; // forza il ripristino al prossimo refreshGame
}
if (g_flash) lv_timer_del(g_flash);
g_flash = lv_timer_create(flash_clear_cb, 1400, nullptr);
}
}
static void pause_cb(lv_event_t *) {
DBG("btn pause");
S->pause();
// Non forziamo una scrittura flash ad ogni pausa (pausa-spam -> drift): se ci sono
// mosse non ancora salvate, le persiste comunque flushSave() in killTimers().
showPause();
}
static void new_cb(lv_event_t *) { DBG("btn MENU"); showMenu(); }
static void resume_cb(lv_event_t *) {
DBG("btn resume");
S->resume();
showGame();
}
static void diff_cb(lv_event_t *e) {
int d = (int)(intptr_t)lv_event_get_user_data(e);
DBG("btn diff %d", d);
// Se c'e' una partita salvata, chiedi se riprenderla o iniziarne una nuova.
if (storage::hasSavedGame()) {
g_pendingDiff = (sudoku::Difficulty) d;
showRestoreDialog();
return;
}
uint32_t t0 = millis();
S->newGame((sudoku::Difficulty)d);
DBG("newGame %lums", (unsigned long)(millis() - t0));
showGame();
}
static void resumeSaved_cb(lv_event_t *) {
DBG("btn resume-saved");
sudoku::GameSession::Snapshot snap;
if (storage::loadGame(snap) && S->restore(snap)) {
S->resume();
showGame();
}
}
// "Nuova partita" dal dialogo di ripristino: scarta il salvataggio e parte
// dal livello scelto (memorizzato in g_pendingDiff).
static void newPending_cb(lv_event_t *) {
storage::clearSavedGame();
uint32_t t0 = millis();
S->newGame(g_pendingDiff);
DBG("newGame(pending) %lums", (unsigned long)(millis() - t0));
showGame();
}
static void toMenu_cb(lv_event_t *) { showMenu(); }
// ===================== helper bottoni =====================
static lv_obj_t *makeButton(lv_obj_t *parent, const char *txt, const lv_font_t *font,
lv_color_t bg, lv_event_cb_t cb, int udata) {
lv_obj_t *b = lv_btn_create(parent);
plainStyle(b, bg, 10);
lv_obj_add_event_cb(b, cb, LV_EVENT_CLICKED, (void *)(intptr_t)udata);
lv_obj_t *l = lv_label_create(b);
lv_label_set_text(l, txt);
lv_obj_set_style_text_color(l, theme::ink(), 0);
lv_obj_set_style_text_font(l, font, 0);
lv_obj_center(l);
return b;
}
// ===================== MENU =====================
static void showMenu() {
killTimers();
lv_obj_t *scr = lv_obj_create(nullptr);
plainStyle(scr, theme::bg(), 0);
lv_obj_t *title = lv_label_create(scr);
lv_label_set_text(title, "SUDOKU");
lv_obj_set_style_text_color(title, theme::ink(), 0);
lv_obj_set_style_text_font(title, &lv_font_montserrat_48, 0);
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 40);
static const sudoku::Difficulty diffs[3] = {
sudoku::Difficulty::Easy, sudoku::Difficulty::Medium, sudoku::Difficulty::Hard };
int y = 130;
for (int i = 0; i < 3; i++) {
lv_obj_t *b = makeButton(scr, difName(diffs[i]), &lv_font_montserrat_28,
theme::accent2(), diff_cb, (int)diffs[i]);
lv_obj_set_size(b, 300, 56);
lv_obj_align(b, LV_ALIGN_TOP_MID, 0, y);
y += 66;
}
// Riprendi (solo se c'e' una partita salvata valida)
if (storage::hasSavedGame()) {
lv_obj_t *b = makeButton(scr, i18n::tr(i18n::K_RESUME), &lv_font_montserrat_24,
theme::accent(), resumeSaved_cb, 0);
lv_obj_set_size(b, 300, 50);
lv_obj_align(b, LV_ALIGN_TOP_MID, 0, y + 6);
}
// Record per livello
char rec[96];
char e[8], m[8], h[8];
uint32_t re = storage::bestTime(sudoku::Difficulty::Easy);
uint32_t rm = storage::bestTime(sudoku::Difficulty::Medium);
uint32_t rh = storage::bestTime(sudoku::Difficulty::Hard);
fmtTime(re * 1000, e, sizeof(e));
fmtTime(rm * 1000, m, sizeof(m));
fmtTime(rh * 1000, h, sizeof(h));
snprintf(rec, sizeof(rec), i18n::tr(i18n::K_RECORDS_FMT),
re ? e : "--:--", rm ? m : "--:--", rh ? h : "--:--");
lv_obj_t *recL = lv_label_create(scr);
lv_label_set_text(recL, rec);
lv_obj_set_style_text_color(recL, theme::muted(), 0);
lv_obj_set_style_text_font(recL, &lv_font_montserrat_16, 0);
lv_obj_align(recL, LV_ALIGN_BOTTOM_MID, 0, -16);
lv_scr_load_anim(scr, LV_SCR_LOAD_ANIM_NONE, 150, 0, true);
}
// ===================== DIALOGO RIPRISTINO =====================
// Mostrato quando si sceglie un livello ma esiste una partita salvata:
// "Riprendi" la riprende, "Nuova partita" la scarta e parte dal livello scelto.
static void showRestoreDialog() {
killTimers();
lv_obj_t *scr = lv_obj_create(nullptr);
plainStyle(scr, theme::bg(), 0);
lv_obj_t *t = lv_label_create(scr);
lv_label_set_text(t, i18n::tr(i18n::K_SAVED_FOUND));
lv_obj_set_style_text_color(t, theme::ink(), 0);
lv_obj_set_style_text_font(t, &lv_font_montserrat_28, 0);
lv_obj_align(t, LV_ALIGN_TOP_MID, 0, 130);
lv_obj_t *bResume = makeButton(scr, i18n::tr(i18n::K_RESUME), &lv_font_montserrat_28,
theme::accent(), resumeSaved_cb, 0);
lv_obj_set_size(bResume, 300, 56);
lv_obj_align(bResume, LV_ALIGN_CENTER, 0, 0);
lv_obj_t *bNew = makeButton(scr, i18n::tr(i18n::K_NEW_GAME), &lv_font_montserrat_28,
theme::accent2(), newPending_cb, 0);
lv_obj_set_size(bNew, 300, 56);
lv_obj_align(bNew, LV_ALIGN_CENTER, 0, 76);
lv_scr_load_anim(scr, LV_SCR_LOAD_ANIM_NONE, 120, 0, true);
}
// ===================== GIOCO =====================
static void showGame() {
killTimers();
lv_obj_t *scr = lv_obj_create(nullptr);
plainStyle(scr, theme::bg(), 0);
// --- top bar ---
lv_obj_t *bar = lv_obj_create(scr);
plainStyle(bar, theme::panel(), 0);
lv_obj_set_size(bar, 480, 50);
lv_obj_align(bar, LV_ALIGN_TOP_MID, 0, 0);
g_timerLabel = lv_label_create(bar);
lv_label_set_text(g_timerLabel, "00:00");
lv_obj_set_style_text_color(g_timerLabel, theme::ink(), 0);
lv_obj_set_style_text_font(g_timerLabel, &lv_font_montserrat_28, 0);
lv_obj_align(g_timerLabel, LV_ALIGN_LEFT_MID, 14, 0);
// Stato salvataggio ("Salvataggio..." / "Salvato") al centro dell'header
g_saveStatus = lv_label_create(bar);
lv_label_set_text(g_saveStatus, "");
lv_obj_set_style_text_font(g_saveStatus, &lv_font_montserrat_16, 0);
lv_obj_set_style_text_color(g_saveStatus, theme::muted(), 0);
lv_obj_align(g_saveStatus, LV_ALIGN_CENTER, 0, 0);
lv_obj_t *bNew = makeButton(bar, "MENU", &lv_font_montserrat_18,
theme::accent2(), new_cb, 0);
lv_obj_set_size(bNew, 74, 38);
lv_obj_align(bNew, LV_ALIGN_RIGHT_MID, -8, 0);
lv_obj_t *bPause = makeButton(bar, LV_SYMBOL_PAUSE, &lv_font_montserrat_24,
theme::accent2(), pause_cb, 0);
lv_obj_set_size(bPause, 46, 38);
lv_obj_align(bPause, LV_ALIGN_RIGHT_MID, -88, 0);
// toggle appunti (matita): evidenziato quando attivo
g_notesMode = false;
g_notesBtn = makeButton(bar, LV_SYMBOL_EDIT, &lv_font_montserrat_24,
theme::accent2(), notes_cb, 0);
lv_obj_set_size(g_notesBtn, 46, 38);
lv_obj_align(g_notesBtn, LV_ALIGN_RIGHT_MID, -140, 0);
// --- griglia ---
int gw = cellX(8) + CELL; // larghezza/altezza totale griglia
lv_obj_t *grid = lv_obj_create(scr);
plainStyle(grid, theme::thick(), 2);
lv_obj_set_size(grid, gw, gw);
lv_obj_align(grid, LV_ALIGN_TOP_MID, 0, 56);
for (int i = 0; i < 81; i++) {
int r = i / 9, c = i % 9;
lv_obj_t *cell = lv_obj_create(grid);
plainStyle(cell, theme::cell(), 2);
lv_obj_set_size(cell, CELL - 2, CELL - 2);
lv_obj_set_pos(cell, cellX(c) + 1, cellY(r) + 1);
lv_obj_add_flag(cell, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(cell, cell_cb, LV_EVENT_CLICKED, (void *)(intptr_t)i);
g_cell[i] = cell;
lv_obj_t *lab = lv_label_create(cell);
lv_obj_set_style_text_font(lab, &lv_font_montserrat_28, 0);
lv_obj_center(lab);
g_cellLabel[i] = lab;
}
// --- tastierino ---
int n = 10; // 1..9 + cancella
int bw = 44, gap = 4;
int totalW = n * bw + (n - 1) * gap;
int startX = (480 - totalW) / 2;
int padY = 56 + gw + 10; // sotto la griglia
for (int i = 0; i < 10; i++) g_numBtn[i] = nullptr;
for (int k = 0; k < n; k++) {
char digit[2] = {0, 0};
const char *txt = "";
int udata = (k < 9) ? (k + 1) : 0;
if (k < 9) { digit[0] = (char)('1' + k); txt = digit; }
lv_obj_t *b = makeButton(scr, txt, &lv_font_montserrat_28,
theme::accent2(), num_cb, udata);
lv_obj_set_size(b, bw, 58);
lv_obj_set_pos(b, startX + k * (bw + gap), padY);
if (k < 9) {
g_numBtn[k + 1] = b;
} else {
// tasto cancella: glifo mdi-eraser (font generato)
lv_obj_t *lbl = lv_obj_get_child(b, 0);
lv_label_set_text(lbl, SYM_ERASER);
lv_obj_set_style_text_font(lbl, &font_eraser, 0);
lv_obj_set_style_text_color(lbl, theme::ink(), 0);
}
}
// invalida le cache: la griglia e' appena stata ricreata, va disegnata tutta
for (int i = 0; i < 81; i++) g_cellSig[i] = 0xFFFFFFFF;
for (int d = 0; d < 10; d++) g_numDone[d] = -1;
refreshGame();
g_tick = lv_timer_create(tick_cb, 250, nullptr);
g_autosaveTimer = lv_timer_create(autosave_tick_cb, 60000, nullptr); // autosave ogni 60s
// Se appena salvato (ricarica post-autosave), mostra "Salvato" per 5s.
if (g_justSaved) {
g_justSaved = false;
lv_label_set_text(g_saveStatus, i18n::tr(i18n::K_SAVED));
lv_obj_set_style_text_color(g_saveStatus, theme::good(), 0);
g_saveStatusTimer = lv_timer_create(saveStatusHide_cb, 5000, nullptr);
}
lv_scr_load_anim(scr, LV_SCR_LOAD_ANIM_NONE, 120, 0, true);
}
// ===================== PAUSA =====================
static void showPause() {
killTimers();
lv_obj_t *scr = lv_obj_create(nullptr);
plainStyle(scr, theme::bg(), 0);
lv_obj_add_flag(scr, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(scr, resume_cb, LV_EVENT_CLICKED, nullptr);
lv_obj_t *t = lv_label_create(scr);
lv_label_set_text(t, i18n::tr(i18n::K_PAUSED));
lv_obj_set_style_text_color(t, theme::ink(), 0);
lv_obj_set_style_text_font(t, &lv_font_montserrat_48, 0);
lv_obj_align(t, LV_ALIGN_CENTER, 0, -20);
lv_obj_t *s = lv_label_create(scr);
lv_label_set_text(s, i18n::tr(i18n::K_TAP_RESUME));
lv_obj_set_style_text_color(s, theme::muted(), 0);
lv_obj_set_style_text_font(s, &lv_font_montserrat_18, 0);
lv_obj_align(s, LV_ALIGN_CENTER, 0, 40);
lv_scr_load_anim(scr, LV_SCR_LOAD_ANIM_NONE, 120, 0, true);
}
// ===================== VITTORIA =====================
static void showWin() {
DBG("WIN");
killTimers();
uint32_t secs = S->elapsedMs() / 1000;
sudoku::Difficulty d = S->difficulty();
uint32_t best = storage::bestTime(d);
bool record = (best == 0 || secs < best);
if (record) storage::setBestTime(d, secs);
storage::clearSavedGame(); // partita finita
lv_obj_t *scr = lv_obj_create(nullptr);
plainStyle(scr, theme::bg(), 0);
lv_obj_t *t = lv_label_create(scr);
lv_label_set_text(t, i18n::tr(i18n::K_YOU_WON));
lv_obj_set_style_text_color(t, theme::good(), 0);
lv_obj_set_style_text_font(t, &lv_font_montserrat_48, 0);
lv_obj_align(t, LV_ALIGN_TOP_MID, 0, 80);
char tbuf[8];
fmtTime(secs * 1000, tbuf, sizeof(tbuf));
lv_obj_t *time = lv_label_create(scr);
lv_label_set_text_fmt(time, "%s - %s", difName(d), tbuf);
lv_obj_set_style_text_color(time, theme::ink(), 0);
lv_obj_set_style_text_font(time, &lv_font_montserrat_28, 0);
lv_obj_align(time, LV_ALIGN_CENTER, 0, -10);
if (record) {
lv_obj_t *rec = lv_label_create(scr);
lv_label_set_text(rec, i18n::tr(i18n::K_NEW_RECORD));
lv_obj_set_style_text_color(rec, theme::good(), 0);
lv_obj_set_style_text_font(rec, &lv_font_montserrat_24, 0);
lv_obj_align(rec, LV_ALIGN_CENTER, 0, 40);
}
lv_obj_t *b = makeButton(scr, i18n::tr(i18n::K_MENU), &lv_font_montserrat_28,
theme::accent(), toMenu_cb, 0);
lv_obj_set_size(b, 220, 56);
lv_obj_align(b, LV_ALIGN_BOTTOM_MID, 0, -40);
lv_scr_load_anim(scr, LV_SCR_LOAD_ANIM_NONE, 150, 0, true);
}
// ===================== SPLASH / SCELTA LINGUA =====================
static void lang_cb(lv_event_t *e) {
int l = (int)(intptr_t)lv_event_get_user_data(e);
i18n::setLang((i18n::Lang) l);
storage::setLanguage((uint8_t) l);
showMenu();
}
static void showSplash() {
killTimers();
lv_obj_t *scr = lv_obj_create(nullptr);
plainStyle(scr, theme::bg(), 0);
// "sol levante": cerchio rosso con i kanji 数独 al centro
lv_obj_t *sun = lv_obj_create(scr);
plainStyle(sun, lv_color_hex(0xd83a3a), 0);
lv_obj_set_size(sun, 180, 180);
lv_obj_set_style_radius(sun, LV_RADIUS_CIRCLE, 0);
lv_obj_align(sun, LV_ALIGN_TOP_MID, 0, 34);
lv_obj_t *k = lv_label_create(sun);
lv_label_set_text(k, SYM_KANJI_SUDOKU); // 数独
lv_obj_set_style_text_font(k, &font_jp56, 0);
lv_obj_set_style_text_color(k, theme::ink(), 0);
lv_obj_center(k);
lv_obj_t *title = lv_label_create(scr);
lv_label_set_text(title, "SUDOKU");
lv_obj_set_style_text_font(title, &lv_font_montserrat_48, 0);
lv_obj_set_style_text_color(title, theme::ink(), 0);
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 232);
// scelta lingua
lv_obj_t *bEn = makeButton(scr, "English", &lv_font_montserrat_28,
theme::accent2(), lang_cb, (int) i18n::EN);
lv_obj_set_size(bEn, 280, 54);
lv_obj_align(bEn, LV_ALIGN_TOP_MID, 0, 318);
lv_obj_t *bIt = makeButton(scr, "Italiano", &lv_font_montserrat_28,
theme::accent2(), lang_cb, (int) i18n::IT);
lv_obj_set_size(bIt, 280, 54);
lv_obj_align(bIt, LV_ALIGN_TOP_MID, 0, 384);
lv_scr_load_anim(scr, LV_SCR_LOAD_ANIM_NONE, 200, 0, true);
}
// ===================== init =====================
void init(sudoku::GameSession *session) {
S = session;
// lingua salvata come default (verra' comunque riscelta nella splash)
uint8_t saved = storage::language();
if (saved == 0 || saved == 1) i18n::setLang((i18n::Lang) saved);
showSplash();
}
} // namespace ui