// widgetTimer.c -- Invisible timer widget // // Fires onChange callbacks at a configurable interval. Supports both // one-shot and repeating modes. The timer is invisible -- it takes no // space in layout and produces no visual output. // // Active timers are tracked via a module-level linked list (using the // stb_ds dynamic array) so wgtUpdateTimers can check only timers, not // walk the entire widget tree each frame. #include "widgetInternal.h" #include #include "thirdparty/stb_ds.h" // Active timer list -- avoids walking the widget tree per frame static WidgetT **sActiveTimers = NULL; // ============================================================ // widgetTimerCalcMinSize // ============================================================ void widgetTimerCalcMinSize(WidgetT *w, const BitmapFontT *font) { (void)font; w->calcMinW = 0; w->calcMinH = 0; } // ============================================================ // widgetTimerDestroy // ============================================================ void widgetTimerDestroy(WidgetT *w) { // Remove from active list for (int32_t i = 0; i < arrlen(sActiveTimers); i++) { if (sActiveTimers[i] == w) { arrdel(sActiveTimers, i); break; } } } // ============================================================ // addToActiveList / removeFromActiveList // ============================================================ static void addToActiveList(WidgetT *w) { // Check if already present for (int32_t i = 0; i < arrlen(sActiveTimers); i++) { if (sActiveTimers[i] == w) { return; } } arrput(sActiveTimers, w); } static void removeFromActiveList(WidgetT *w) { for (int32_t i = 0; i < arrlen(sActiveTimers); i++) { if (sActiveTimers[i] == w) { arrdel(sActiveTimers, i); return; } } } // ============================================================ // wgtTimer // ============================================================ WidgetT *wgtTimer(WidgetT *parent, int32_t intervalMs, bool repeat) { WidgetT *w = widgetAlloc(parent, WidgetTimerE); if (w) { w->visible = false; w->as.timer.intervalMs = intervalMs; w->as.timer.repeat = repeat; w->as.timer.running = false; w->as.timer.lastFire = 0; } return w; } // ============================================================ // wgtTimerIsRunning // ============================================================ bool wgtTimerIsRunning(const WidgetT *w) { if (!w || w->type != WidgetTimerE) { return false; } return w->as.timer.running; } // ============================================================ // wgtTimerSetInterval // ============================================================ void wgtTimerSetInterval(WidgetT *w, int32_t intervalMs) { if (!w || w->type != WidgetTimerE) { return; } w->as.timer.intervalMs = intervalMs; } // ============================================================ // wgtTimerStart // ============================================================ void wgtTimerStart(WidgetT *w) { if (!w || w->type != WidgetTimerE) { return; } w->as.timer.running = true; w->as.timer.lastFire = clock(); addToActiveList(w); } // ============================================================ // wgtTimerStop // ============================================================ void wgtTimerStop(WidgetT *w) { if (!w || w->type != WidgetTimerE) { return; } w->as.timer.running = false; removeFromActiveList(w); } // ============================================================ // wgtUpdateTimers // ============================================================ // // Called once per frame from dvxUpdate. Iterates the active timer // list (not the widget tree) so cost is proportional to timer count, // not total widget count. One-shot timers are removed after firing. void wgtUpdateTimers(void) { clock_t now = clock(); // Iterate backwards so arrdel doesn't skip entries for (int32_t i = arrlen(sActiveTimers) - 1; i >= 0; i--) { WidgetT *w = sActiveTimers[i]; if (!w->as.timer.running) { arrdel(sActiveTimers, i); continue; } clock_t elapsed = now - w->as.timer.lastFire; clock_t interval = (clock_t)w->as.timer.intervalMs * CLOCKS_PER_SEC / 1000; if (elapsed >= interval) { w->as.timer.lastFire = now; if (w->onChange) { w->onChange(w); } if (!w->as.timer.repeat) { w->as.timer.running = false; arrdel(sActiveTimers, i); } } } }