mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-02 19:02:00 +00:00
ACCENT_DIM (the accent mixed toward black) came out brown on selected tabs, chips, segments and own bubbles. They take the accent with dark text now, as primary actions and the quick panel tiles do: styleSelected() sets the fill, the pressed shade and every label under it. Own bubbles: dark text, age and delivery mark (green or red can't be read on amber); the melody editor's selected note is outlined, the sounding one filled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
513 lines
22 KiB
C++
513 lines
22 KiB
C++
#pragma once
|
|
// Clock tools (Home > Clock): alarm, countdown timer, stopwatch and this
|
|
// month's calendar -- ui-new's Tools > Clock on a touch screen (the calendar
|
|
// as ui-new's e-ink has it under the clock). The alarm (NodePrefs alarm_*) and the
|
|
// countdown run in the Core's ClockEngine, so they fire on any screen and while
|
|
// the display sleeps; the stopwatch is view state here, as in ui-new. Whatever
|
|
// fires brings up a full-screen card with Dismiss (silent: the L2 speaker has
|
|
// no driver yet).
|
|
//
|
|
// Single-TU fragment: included by ui-lvgl/UITask.cpp after NavMap.h.
|
|
|
|
namespace clockview {
|
|
|
|
enum : uint8_t { TAB_ALARM, TAB_TIMER, TAB_STOPWATCH, TAB_CALENDAR, TAB_COUNT };
|
|
enum : uint8_t { ACT_START_STOP, ACT_RESET };
|
|
|
|
static uint8_t s_tab = TAB_ALARM;
|
|
static uint8_t s_timer_h = 0, s_timer_m = 5, s_timer_s = 0; // countdown to start
|
|
static bool s_sw_running = false;
|
|
static uint32_t s_sw_start = 0, s_sw_accum = 0;
|
|
static bool s_shown_running = false; // timer state the screen was built for
|
|
static lv_obj_t* s_big = nullptr; // timer remaining / stopwatch reading
|
|
static lv_obj_t* s_go_lbl = nullptr; // Start / Stop
|
|
static lv_obj_t* s_ring = nullptr; // "Timer done" card on the top layer
|
|
static lv_obj_t* s_ring_lbl = nullptr;
|
|
static lv_obj_t* s_alarm_sw = nullptr; // follows a time change (setting it arms the alarm)
|
|
|
|
static struct tm s_cal_tm; // the day the calendar was drawn for (local)
|
|
static uint32_t s_cal_marks = 0; // its alarm days, bit (day - 1)
|
|
static lv_obj_t* s_cal = nullptr;
|
|
|
|
// The month as a grid, weeks from Monday: the weekday names, today in an
|
|
// accent box, the alarm's days with an accent bar under them, the weekend
|
|
// dimmer.
|
|
static void onCalendarDraw(lv_event_t* e) {
|
|
lv_obj_t* o = (lv_obj_t*)lv_event_get_target(e);
|
|
lv_layer_t* layer = lv_event_get_layer(e);
|
|
lv_area_t a;
|
|
lv_obj_get_coords(o, &a);
|
|
const struct tm& t = s_cal_tm;
|
|
static const char* const NAMES[] = { "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su" };
|
|
const int W = a.x2 - a.x1 + 1, cw = W / 7, x0 = a.x1 + (W - 7 * cw) / 2;
|
|
const int head = 16, rows = calmath::weeks(t), ch = (a.y2 - a.y1 + 1 - head) / rows;
|
|
lv_draw_label_dsc_t td;
|
|
lv_draw_label_dsc_init(&td);
|
|
td.align = LV_TEXT_ALIGN_CENTER;
|
|
td.font = THEME_FONT_SMALL;
|
|
for (int c = 0; c < 7; c++) {
|
|
td.color = lv_color_hex(theme::TEXT_MUTED);
|
|
td.text = NAMES[c];
|
|
lv_area_t la = { x0 + c * cw, a.y1, x0 + (c + 1) * cw - 1, a.y1 + head - 1 };
|
|
lv_draw_label(layer, &td, &la);
|
|
}
|
|
td.font = THEME_FONT_BODY;
|
|
const int fh = lv_font_get_line_height(td.font), col0 = calmath::firstCol(t), n = calmath::daysIn(t.tm_year, t.tm_mon);
|
|
char buf[3];
|
|
for (int day = 1; day <= n; day++) {
|
|
const int k = col0 + day - 1, cx = x0 + (k % 7) * cw, cy = a.y1 + head + (k / 7) * ch;
|
|
const bool today = day == t.tm_mday;
|
|
if (today) {
|
|
lv_draw_rect_dsc_t rd;
|
|
lv_draw_rect_dsc_init(&rd);
|
|
rd.radius = theme::RADIUS_SM;
|
|
rd.bg_color = lv_color_hex(theme::ACCENT);
|
|
const int bh = ch - 2 < fh + 2 ? ch - 2 : fh + 2, by = cy + (ch - bh) / 2;
|
|
lv_area_t ra = { cx + 4, by, cx + cw - 5, by + bh - 1 };
|
|
lv_draw_rect(layer, &rd, &ra);
|
|
}
|
|
snprintf(buf, sizeof(buf), "%d", day);
|
|
td.text = buf;
|
|
td.color = lv_color_hex(today ? theme::BG : k % 7 >= 5 ? theme::TEXT_MUTED : theme::TEXT);
|
|
lv_area_t la = { cx, cy + (ch - fh) / 2, cx + cw - 1, cy + (ch + fh) / 2 };
|
|
lv_draw_label(layer, &td, &la);
|
|
if (s_cal_marks & (1UL << (day - 1))) { // under the number
|
|
lv_draw_rect_dsc_t md;
|
|
lv_draw_rect_dsc_init(&md);
|
|
md.radius = 1;
|
|
md.bg_color = lv_color_hex(today ? theme::BG : theme::ACCENT);
|
|
lv_area_t ma = { cx + cw / 2 - 6, cy + ch - 2, cx + cw / 2 + 6, cy + ch - 1 };
|
|
lv_draw_rect(layer, &md, &ma);
|
|
}
|
|
}
|
|
}
|
|
|
|
static uint32_t swElapsed() { return s_sw_accum + (s_sw_running ? millis() - s_sw_start : 0); }
|
|
|
|
// Countdown: rounds up, so "00:01" shows until it actually fires.
|
|
static void fmtRemaining(char* b, int n, uint32_t ms) {
|
|
uint32_t t = (ms + 999) / 1000;
|
|
if (t >= 3600) snprintf(b, n, "%lu:%02lu:%02lu", (unsigned long)(t / 3600), (unsigned long)(t / 60 % 60), (unsigned long)(t % 60));
|
|
else snprintf(b, n, "%02lu:%02lu", (unsigned long)(t / 60), (unsigned long)(t % 60));
|
|
}
|
|
static void fmtStopwatch(char* b, int n, uint32_t ms) {
|
|
uint32_t t = ms / 1000;
|
|
if (t >= 3600) snprintf(b, n, "%lu:%02lu:%02lu", (unsigned long)(t / 3600), (unsigned long)(t / 60 % 60), (unsigned long)(t % 60));
|
|
else snprintf(b, n, "%02lu:%02lu.%lu", (unsigned long)(t / 60), (unsigned long)(t % 60), (unsigned long)(ms / 100 % 10));
|
|
}
|
|
|
|
// A number wheel, 0..n-1 in a loop with the middle row selected: an infinite
|
|
// lv_roller's look and touch, drawn by hand. lv_roller measures its whole
|
|
// option text (repeated three times) on every style change, layout pass and
|
|
// frame: some 40 of the ~100 ms this screen took to open went on that.
|
|
struct Wheel {
|
|
uint8_t n;
|
|
int32_t pos; // the selected row, in 1/256 rows: dragging moves it between rows
|
|
int32_t vel; // the finger's recent speed, px per input read (smoothed)
|
|
int32_t drag; // px the finger has moved since it went down
|
|
};
|
|
static const int32_t WHEEL_LINE = 4; // between rows
|
|
static char s_nums[60][3]; // "00".."59"
|
|
|
|
static int32_t wheelPitch() { return lv_font_get_line_height(THEME_FONT_TITLE) + WHEEL_LINE; }
|
|
static Wheel* wheelOf(lv_obj_t* o) { return (Wheel*)lv_obj_get_user_data(o); }
|
|
static int wheelValue(lv_obj_t* o) {
|
|
const Wheel* w = wheelOf(o);
|
|
int32_t r = (w->pos + 128) >> 8;
|
|
return (int)(((r % w->n) + w->n) % w->n);
|
|
}
|
|
|
|
static void wheelAnim(void* o, int32_t v) {
|
|
wheelOf((lv_obj_t*)o)->pos = v;
|
|
lv_obj_invalidate((lv_obj_t*)o);
|
|
}
|
|
static void wheelAnimDone(lv_anim_t* a) { // back to the first loop, so pos never grows
|
|
Wheel* w = wheelOf((lv_obj_t*)a->var);
|
|
w->pos = (int32_t)wheelValue((lv_obj_t*)a->var) << 8;
|
|
}
|
|
// Rolls to row `to` (whole rows from where it is now) and says so.
|
|
static void wheelGo(lv_obj_t* o, int32_t to) {
|
|
Wheel* w = wheelOf(o);
|
|
int32_t from = w->pos, end = to * 256;
|
|
lv_anim_delete(o, wheelAnim);
|
|
lv_anim_t a;
|
|
lv_anim_init(&a);
|
|
lv_anim_set_var(&a, o);
|
|
lv_anim_set_exec_cb(&a, wheelAnim);
|
|
lv_anim_set_values(&a, from, end);
|
|
int32_t rows = LV_ABS(end - from) >> 8;
|
|
lv_anim_set_duration(&a, LV_MIN(200 + rows * 20, 600));
|
|
lv_anim_set_path_cb(&a, lv_anim_path_ease_out);
|
|
lv_anim_set_completed_cb(&a, wheelAnimDone);
|
|
w->pos = end; // the value is the target already: read by the event below
|
|
lv_obj_send_event(o, LV_EVENT_VALUE_CHANGED, NULL);
|
|
w->pos = from;
|
|
lv_anim_start(&a);
|
|
}
|
|
|
|
static void wheelDraw(lv_event_t* e) {
|
|
lv_obj_t* o = lv_event_get_current_target_obj(e);
|
|
const Wheel* w = wheelOf(o);
|
|
lv_layer_t* layer = lv_event_get_layer(e);
|
|
lv_area_t a;
|
|
lv_obj_get_coords(o, &a);
|
|
int32_t pitch = wheelPitch(), mid = (a.y1 + a.y2 + 1) / 2;
|
|
int32_t h_main = lv_font_get_line_height(THEME_FONT_TITLE), h_sel = lv_font_get_line_height(THEME_FONT_LARGE);
|
|
int32_t band_h = (h_main + h_sel) / 2 + WHEEL_LINE;
|
|
lv_area_t band = { a.x1, mid - band_h / 2, a.x2, mid - band_h / 2 + band_h - 1 };
|
|
|
|
lv_draw_rect_dsc_t rd;
|
|
lv_draw_rect_dsc_init(&rd);
|
|
rd.bg_color = lv_color_hex(theme::SURFACE_2);
|
|
lv_draw_rect(layer, &rd, &band);
|
|
|
|
// Above the band, the band, below it: the band's rows in the larger font.
|
|
const lv_area_t clips[3] = { { a.x1, a.y1, a.x2, band.y1 - 1 }, band, { a.x1, band.y2 + 1, a.x2, a.y2 } };
|
|
const lv_area_t clip_ori = layer->_clip_area;
|
|
int32_t base = w->pos >> 8, frac = w->pos & 255;
|
|
int32_t span = lv_area_get_height(&a) / 2 / pitch + 2;
|
|
for (int c = 0; c < 3; c++) {
|
|
if (!lv_area_intersect(&layer->_clip_area, &clip_ori, &clips[c])) continue;
|
|
bool sel = c == 1;
|
|
lv_draw_label_dsc_t td;
|
|
lv_draw_label_dsc_init(&td);
|
|
td.font = sel ? THEME_FONT_LARGE : THEME_FONT_TITLE;
|
|
td.color = lv_color_hex(sel ? theme::ACCENT : theme::TEXT_MUTED);
|
|
td.align = LV_TEXT_ALIGN_CENTER;
|
|
int32_t fh = sel ? h_sel : h_main;
|
|
for (int32_t k = -span; k <= span; k++) {
|
|
int32_t y = mid + ((k << 8) - frac) * pitch / 256; // the row's middle
|
|
if (y + fh < layer->_clip_area.y1 || y - fh > layer->_clip_area.y2) continue;
|
|
td.text = s_nums[(((base + k) % w->n) + w->n) % w->n];
|
|
lv_area_t ta = { a.x1, y - fh / 2, a.x2, y - fh / 2 + fh - 1 };
|
|
lv_draw_label(layer, &td, &ta);
|
|
}
|
|
}
|
|
layer->_clip_area = clip_ori;
|
|
}
|
|
|
|
static void wheelEvent(lv_event_t* e) {
|
|
lv_obj_t* o = lv_event_get_current_target_obj(e);
|
|
Wheel* w = wheelOf(o);
|
|
lv_indev_t* indev = lv_indev_active();
|
|
switch (lv_event_get_code(e)) {
|
|
case LV_EVENT_PRESSED:
|
|
lv_anim_delete(o, wheelAnim);
|
|
w->vel = w->drag = 0;
|
|
break;
|
|
case LV_EVENT_PRESSING: {
|
|
lv_point_t v = { 0, 0 };
|
|
if (indev) lv_indev_get_vect(indev, &v);
|
|
w->vel = (w->vel + v.y) / 2;
|
|
w->drag += LV_ABS(v.y);
|
|
if (v.y) {
|
|
w->pos -= v.y * 256 / wheelPitch();
|
|
lv_obj_invalidate(o);
|
|
}
|
|
break;
|
|
}
|
|
case LV_EVENT_RELEASED:
|
|
case LV_EVENT_PRESS_LOST: {
|
|
int32_t pitch = wheelPitch(), to;
|
|
if (w->drag > 4) { // a throw carries on for ~10 reads' worth (as lv_roller's), then the nearest row
|
|
to = (w->pos - w->vel * 10 * 256 / pitch + 128) >> 8;
|
|
} else { // a tap: that row to the middle
|
|
lv_point_t p = { 0, 0 };
|
|
if (indev) lv_indev_get_point(indev, &p);
|
|
lv_area_t a;
|
|
lv_obj_get_coords(o, &a);
|
|
int32_t dy = p.y - (a.y1 + a.y2 + 1) / 2;
|
|
to = ((w->pos + 128) >> 8) + (dy + (dy < 0 ? -pitch / 2 : pitch / 2)) / pitch;
|
|
}
|
|
wheelGo(o, to);
|
|
break;
|
|
}
|
|
case LV_EVENT_DELETE:
|
|
lv_anim_delete(o, wheelAnim);
|
|
lv_free(w);
|
|
break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
static lv_obj_t* wheel(lv_obj_t* parent, uint8_t n, int sel, lv_event_cb_t cb, uintptr_t which) {
|
|
if (!s_nums[0][0]) for (int i = 0; i < 60; i++) snprintf(s_nums[i], sizeof(s_nums[i]), "%02d", i);
|
|
Wheel* w = (Wheel*)lv_malloc(sizeof(Wheel));
|
|
*w = { n, (int32_t)(sel % n) * 256, 0, 0 };
|
|
lv_obj_t* o = lv_obj_create(parent);
|
|
styleSurface(o, theme::SURFACE);
|
|
lv_obj_set_style_radius(o, theme::RADIUS_SM, 0);
|
|
lv_obj_remove_flag(o, (lv_obj_flag_t)(LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_SCROLL_CHAIN));
|
|
lv_obj_set_size(o, 60, 84); // three rows; leaves room for the button below
|
|
lv_obj_set_user_data(o, w);
|
|
lv_obj_add_event_cb(o, wheelDraw, LV_EVENT_DRAW_MAIN, NULL);
|
|
lv_obj_add_event_cb(o, wheelEvent, LV_EVENT_ALL, NULL);
|
|
lv_obj_add_event_cb(o, cb, LV_EVENT_VALUE_CHANGED, (void*)which);
|
|
return o;
|
|
}
|
|
|
|
static lv_obj_t* colon(lv_obj_t* parent) { return label(parent, ":", THEME_FONT_LARGE, theme::TEXT_MUTED); }
|
|
|
|
} // namespace clockview
|
|
|
|
static void onOpenClock(lv_event_t* e) { (void)e; s_ui->showClock(); }
|
|
static void onClockTab(lv_event_t* e) { s_ui->clockTab((int)(uintptr_t)lv_event_get_user_data(e)); }
|
|
static void onClockAct(lv_event_t* e) { s_ui->clockAction((uint8_t)(uintptr_t)lv_event_get_user_data(e)); }
|
|
static void onRingDismiss(lv_event_t* e) { (void)e; s_ui->dismissRing(); }
|
|
|
|
// Alarm: hour / minute wheels, on switch, repeat. Saved on every change.
|
|
static void onAlarmWheel(lv_event_t* e) {
|
|
s_ui->setAlarm((int)(uintptr_t)lv_event_get_user_data(e), clockview::wheelValue((lv_obj_t*)lv_event_get_target(e)));
|
|
}
|
|
static void onAlarmSwitch(lv_event_t* e) {
|
|
s_ui->setAlarm(2, lv_obj_has_state((lv_obj_t*)lv_event_get_target(e), LV_STATE_CHECKED) ? 1 : 0);
|
|
}
|
|
static void onAlarmRepeat(lv_event_t* e) {
|
|
s_ui->setAlarm(3, choiceSelected((lv_obj_t*)lv_event_get_target(e)));
|
|
}
|
|
static void onTimerWheel(lv_event_t* e) {
|
|
int v = clockview::wheelValue((lv_obj_t*)lv_event_get_target(e));
|
|
switch ((uintptr_t)lv_event_get_user_data(e)) {
|
|
case 0: clockview::s_timer_h = (uint8_t)v; break;
|
|
case 1: clockview::s_timer_m = (uint8_t)v; break;
|
|
default: clockview::s_timer_s = (uint8_t)v; break;
|
|
}
|
|
}
|
|
|
|
void UITask::showClock() {
|
|
_screen = SCR_CLOCK;
|
|
buildClock();
|
|
}
|
|
|
|
void UITask::clockTab(int tab) {
|
|
if (tab < 0 || tab >= clockview::TAB_COUNT) return;
|
|
clockview::s_tab = (uint8_t)tab;
|
|
buildClock();
|
|
}
|
|
|
|
void UITask::buildClock() {
|
|
using namespace clockview;
|
|
lv_obj_t* body = newScreen("Clock", true);
|
|
lv_obj_set_style_pad_row(body, 6, 0);
|
|
lv_obj_remove_flag(body, LV_OBJ_FLAG_SCROLLABLE);
|
|
s_big = s_go_lbl = s_alarm_sw = s_cal = nullptr;
|
|
|
|
// Tabs
|
|
lv_obj_t* tabs = centerRow(body);
|
|
static const char* const NAMES[TAB_COUNT] = { "Alarm", "Timer", "Stopwatch", "Calendar" };
|
|
for (int t = 0; t < TAB_COUNT; t++) {
|
|
lv_obj_t* b = lv_button_create(tabs);
|
|
lv_obj_set_height(b, 32);
|
|
lv_obj_set_flex_grow(b, 1);
|
|
lv_obj_set_style_shadow_width(b, 0, 0);
|
|
lv_obj_set_style_radius(b, theme::RADIUS, 0);
|
|
lv_obj_add_event_cb(b, onClockTab, LV_EVENT_CLICKED, (void*)(uintptr_t)t);
|
|
lv_obj_center(label(b, NAMES[t], THEME_FONT_SMALL, theme::TEXT));
|
|
styleSelected(b, t == s_tab);
|
|
}
|
|
|
|
if (s_tab == TAB_ALARM) {
|
|
lv_obj_t* r = centerRow(body);
|
|
wheel(r, 24, _prefs->alarm_hour % 24, onAlarmWheel, 0);
|
|
colon(r);
|
|
wheel(r, 60, _prefs->alarm_min % 60, onAlarmWheel, 1);
|
|
lv_obj_t* col = lv_obj_create(r);
|
|
styleSurface(col, theme::BG);
|
|
lv_obj_remove_flag(col, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_set_size(col, 120, LV_SIZE_CONTENT);
|
|
lv_obj_set_flex_flow(col, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_set_flex_align(col, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
|
|
lv_obj_set_style_pad_row(col, 4, 0);
|
|
lv_obj_set_style_pad_left(col, 8, 0);
|
|
lv_obj_t* on = flexBox(col, LV_FLEX_FLOW_ROW); // the switch and its label: one line
|
|
lv_obj_set_flex_align(on, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
lv_obj_set_style_pad_column(on, 8, 0);
|
|
lv_obj_set_style_pad_bottom(on, 4, 0);
|
|
lv_obj_t* sw = s_alarm_sw = lv_switch_create(on);
|
|
label(on, "Alarm on", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
|
lv_obj_set_size(sw, 50, 26);
|
|
if (_prefs->alarm_on) lv_obj_add_state(sw, LV_STATE_CHECKED);
|
|
lv_obj_add_event_cb(sw, onAlarmSwitch, LV_EVENT_VALUE_CHANGED, NULL);
|
|
label(col, "Repeat", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
|
char opts[48];
|
|
int o = 0;
|
|
for (uint8_t i = 0; i < NodePrefs::ALARM_REPEAT_COUNT; i++)
|
|
o += snprintf(opts + o, sizeof(opts) - o, "%s%s", i ? "\n" : "", i ? NodePrefs::alarmRepeatLabel(i) : "Once");
|
|
lv_obj_t* dd = choiceCreate(col, opts, NodePrefs::alarmRepeatIdxForMask(_prefs->alarm_repeat_mask), "Repeat");
|
|
lv_obj_set_width(dd, 112);
|
|
lv_obj_add_event_cb(dd, onAlarmRepeat, LV_EVENT_VALUE_CHANGED, NULL);
|
|
bool synced = rtc_clock.getCurrentTime() > 1000000000UL;
|
|
label(body, synced ? "Rings at this local time (Settings time zone)." : "The clock isn't set yet: the alarm waits for a time sync.",
|
|
THEME_FONT_SMALL, theme::TEXT_MUTED);
|
|
return;
|
|
}
|
|
|
|
if (s_tab == TAB_CALENDAR) {
|
|
struct tm t;
|
|
bool synced = localTime(_prefs, t);
|
|
if (!synced) {
|
|
label(body, "The clock isn't set yet: no date to show.", THEME_FONT_BODY, theme::TEXT_MUTED);
|
|
return;
|
|
}
|
|
static const char* const LONG_MONTHS[] = { "January", "February", "March", "April", "May", "June", "July",
|
|
"August", "September", "October", "November", "December" };
|
|
char title[24]; // the month on the header's right: the grid gets the height
|
|
snprintf(title, sizeof(title), "%s %d", LONG_MONTHS[t.tm_mon], t.tm_year + 1900);
|
|
if (_header) lv_obj_align(label(_header, title, THEME_FONT_TITLE, theme::TEXT_MUTED), LV_ALIGN_RIGHT_MID, -theme::PAD, 0);
|
|
s_cal_tm = t;
|
|
s_cal_marks = calmath::alarmDays(t, _prefs->alarm_on, _prefs->alarm_repeat_mask, _prefs->alarm_hour, _prefs->alarm_min);
|
|
s_cal = lv_obj_create(body);
|
|
lv_obj_remove_style_all(s_cal);
|
|
lv_obj_remove_flag(s_cal, LV_OBJ_FLAG_CLICKABLE);
|
|
fillWidth(s_cal);
|
|
lv_obj_set_flex_grow(s_cal, 1);
|
|
lv_obj_add_event_cb(s_cal, onCalendarDraw, LV_EVENT_DRAW_MAIN_END, NULL);
|
|
return;
|
|
}
|
|
|
|
if (s_tab == TAB_TIMER) {
|
|
s_shown_running = _core->clock.isTimerRunning();
|
|
if (s_shown_running) {
|
|
s_big = label(body, "", THEME_FONT_CLOCK, theme::TEXT);
|
|
lv_obj_set_width(s_big, LV_PCT(100));
|
|
lv_obj_set_style_text_align(s_big, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_set_style_pad_ver(s_big, 18, 0);
|
|
} else {
|
|
lv_obj_t* r = centerRow(body);
|
|
wheel(r, 24, s_timer_h, onTimerWheel, 0);
|
|
colon(r);
|
|
wheel(r, 60, s_timer_m, onTimerWheel, 1);
|
|
colon(r);
|
|
wheel(r, 60, s_timer_s, onTimerWheel, 2);
|
|
}
|
|
} else { // stopwatch
|
|
s_big = label(body, "", THEME_FONT_CLOCK, theme::TEXT);
|
|
lv_obj_set_width(s_big, LV_PCT(100));
|
|
lv_obj_set_style_text_align(s_big, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_set_style_pad_ver(s_big, 18, 0);
|
|
}
|
|
|
|
lv_obj_t* acts = centerRow(body);
|
|
lv_obj_t* go = lv_button_create(acts);
|
|
lv_obj_set_size(go, 150, 40);
|
|
lv_obj_set_style_shadow_width(go, 0, 0);
|
|
lv_obj_set_style_radius(go, theme::RADIUS, 0);
|
|
lv_obj_add_event_cb(go, onClockAct, LV_EVENT_CLICKED, (void*)(uintptr_t)ACT_START_STOP);
|
|
s_go_lbl = label(go, "", THEME_FONT_BODY, theme::TEXT);
|
|
lv_obj_center(s_go_lbl);
|
|
stylePrimary(go);
|
|
if (s_tab == TAB_STOPWATCH) {
|
|
lv_obj_t* rs = lv_button_create(acts);
|
|
lv_obj_set_size(rs, 100, 40);
|
|
lv_obj_set_style_shadow_width(rs, 0, 0);
|
|
lv_obj_set_style_radius(rs, theme::RADIUS, 0);
|
|
lv_obj_set_style_bg_color(rs, lv_color_hex(theme::SURFACE), 0);
|
|
lv_obj_add_event_cb(rs, onClockAct, LV_EVENT_CLICKED, (void*)(uintptr_t)ACT_RESET);
|
|
lv_obj_center(label(rs, LV_SYMBOL_REFRESH " Reset", THEME_FONT_BODY, theme::TEXT));
|
|
}
|
|
refreshClock();
|
|
}
|
|
|
|
// From loop() while SCR_CLOCK is shown: the running readouts.
|
|
void UITask::refreshClock() {
|
|
using namespace clockview;
|
|
if (s_tab == TAB_CALENDAR) { // past midnight, or the alarm changed: a new day to box or mark
|
|
struct tm t;
|
|
if (s_cal && localTime(_prefs, t)
|
|
&& (t.tm_mday != s_cal_tm.tm_mday || t.tm_mon != s_cal_tm.tm_mon
|
|
|| calmath::alarmDays(t, _prefs->alarm_on, _prefs->alarm_repeat_mask, _prefs->alarm_hour, _prefs->alarm_min) != s_cal_marks))
|
|
buildClock();
|
|
return;
|
|
}
|
|
if (s_tab == TAB_TIMER) {
|
|
bool running = _core->clock.isTimerRunning();
|
|
if (running != s_shown_running) { buildClock(); return; } // started elsewhere / just fired
|
|
if (s_big) {
|
|
char b[16];
|
|
fmtRemaining(b, sizeof(b), _core->clock.timerRemainingMs());
|
|
setText(s_big, b);
|
|
}
|
|
if (s_go_lbl) setText(s_go_lbl, running ? LV_SYMBOL_STOP " Stop" : LV_SYMBOL_PLAY " Start");
|
|
} else if (s_tab == TAB_STOPWATCH) {
|
|
if (s_big) {
|
|
char b[16];
|
|
fmtStopwatch(b, sizeof(b), swElapsed());
|
|
setText(s_big, b);
|
|
}
|
|
if (s_go_lbl) setText(s_go_lbl, s_sw_running ? LV_SYMBOL_PAUSE " Stop" : LV_SYMBOL_PLAY " Start");
|
|
}
|
|
}
|
|
|
|
void UITask::clockAction(uint8_t act) {
|
|
using namespace clockview;
|
|
if (s_tab == TAB_TIMER) {
|
|
if (_core->clock.isTimerRunning()) _core->clock.stopTimer();
|
|
else {
|
|
uint32_t ms = ((uint32_t)s_timer_h * 3600 + s_timer_m * 60 + s_timer_s) * 1000UL;
|
|
if (ms == 0) { showToast("Set a time first"); return; }
|
|
_core->clock.startTimer(ms);
|
|
}
|
|
buildClock();
|
|
} else if (s_tab == TAB_STOPWATCH) {
|
|
if (act == ACT_RESET) { s_sw_accum = 0; s_sw_start = millis(); }
|
|
else if (s_sw_running) { s_sw_accum += millis() - s_sw_start; s_sw_running = false; }
|
|
else { s_sw_start = millis(); s_sw_running = true; }
|
|
refreshClock();
|
|
}
|
|
}
|
|
|
|
// which: 0 hour, 1 minute, 2 on/off, 3 repeat preset.
|
|
void UITask::setAlarm(int which, int v) {
|
|
if (!_prefs) return;
|
|
switch (which) {
|
|
case 0: _prefs->alarm_hour = (uint8_t)v; _prefs->alarm_on = 1; break; // setting a time arms it
|
|
case 1: _prefs->alarm_min = (uint8_t)v; _prefs->alarm_on = 1; break;
|
|
case 2: _prefs->alarm_on = (uint8_t)v; break;
|
|
case 3: _prefs->alarm_repeat_mask = NodePrefs::alarmRepeatMaskForIdx((uint8_t)v); break;
|
|
}
|
|
_core->clock.onAlarmChanged();
|
|
prefsSave();
|
|
if (which < 2 && clockview::s_alarm_sw) lv_obj_add_state(clockview::s_alarm_sw, LV_STATE_CHECKED);
|
|
}
|
|
|
|
// ── Ring card (any screen) ────────────────────────────────────────────────────
|
|
|
|
void UITask::showRing(const char* text) {
|
|
using namespace clockview;
|
|
wake();
|
|
if (!s_ring) {
|
|
s_ring = lv_obj_create(lv_layer_top());
|
|
lv_obj_remove_style_all(s_ring);
|
|
lv_obj_set_size(s_ring, LV_PCT(100), LV_PCT(100));
|
|
lv_obj_set_style_bg_color(s_ring, lv_color_hex(theme::BG), 0);
|
|
lv_obj_set_style_bg_opa(s_ring, LV_OPA_COVER, 0);
|
|
lv_obj_add_flag(s_ring, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_obj_set_flex_flow(s_ring, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_set_flex_align(s_ring, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
lv_obj_set_style_pad_row(s_ring, 14, 0);
|
|
label(s_ring, LV_SYMBOL_BELL, THEME_FONT_LARGE, theme::ACCENT);
|
|
s_ring_lbl = label(s_ring, "", THEME_FONT_LARGE, theme::TEXT);
|
|
lv_obj_t* b = lv_button_create(s_ring);
|
|
lv_obj_set_size(b, 200, 56);
|
|
lv_obj_set_style_shadow_width(b, 0, 0);
|
|
lv_obj_set_style_radius(b, theme::RADIUS, 0);
|
|
lv_obj_add_event_cb(b, onRingDismiss, LV_EVENT_CLICKED, NULL);
|
|
lv_obj_center(label(b, "Dismiss", THEME_FONT_LARGE, theme::TEXT));
|
|
stylePrimary(b);
|
|
}
|
|
lv_label_set_text(s_ring_lbl, text);
|
|
lv_obj_remove_flag(s_ring, LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_move_foreground(s_ring); // over the lock screen too
|
|
}
|
|
|
|
void UITask::dismissRing() {
|
|
stopMelody();
|
|
_core->clock.dismissRing();
|
|
hideRing();
|
|
}
|
|
|
|
void UITask::hideRing() {
|
|
if (clockview::s_ring) lv_obj_add_flag(clockview::s_ring, LV_OBJ_FLAG_HIDDEN);
|
|
if (_screen == SCR_CLOCK) buildClock(); // e.g. the timer's Start again
|
|
}
|