DVX_GUI/apps/clock/clock.c

222 lines
7.8 KiB
C

// clock.c — Clock DXE application (main-loop with tsYield)
//
// This is a main-loop DXE app (hasMainLoop = true), in contrast to callback-
// only apps like notepad or progman. The difference is fundamental:
//
// Callback-only: appMain creates UI, returns. Shell calls callbacks.
// Main-loop: appMain runs forever in its own task, calling tsYield()
// to cooperatively yield to the shell and other tasks.
//
// A main-loop app is needed when the app has ongoing work that can't be
// expressed purely as event callbacks — here, polling the system clock
// every second and repainting. The shell allocates a dedicated task stack
// and schedules this task alongside the main event loop.
//
// The onPaint/onClose callbacks still execute in task 0 during dvxUpdate(),
// not in this task. The main loop only handles the clock-tick polling; the
// actual rendering and window management happen through the normal callback
// path. tsYield() is what makes this cooperative — without it, this task
// would starve the shell and all other apps.
#include "dvxApp.h"
#include "dvxWidget.h"
#include "dvxDraw.h"
#include "dvxVideo.h"
#include "shellApp.h"
#include "taskswitch.h"
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
// ============================================================
// Module state
// ============================================================
typedef struct {
bool quit;
char timeStr[32];
char dateStr[32];
time_t lastUpdate;
} ClockStateT;
static DxeAppContextT *sCtx = NULL;
static WindowT *sWin = NULL;
static ClockStateT sState;
// ============================================================
// Prototypes
// ============================================================
int32_t appMain(DxeAppContextT *ctx);
void appShutdown(void);
static void onClose(WindowT *win);
static void onPaint(WindowT *win, RectT *dirty);
static void updateTime(void);
// ============================================================
// App descriptor
// ============================================================
// hasMainLoop = true tells the shell to create a dedicated task for appMain.
// TS_PRIORITY_LOW because clock updates are cosmetic and should never
// preempt interactive apps or the shell's event processing.
AppDescriptorT appDescriptor = {
.name = "Clock",
.hasMainLoop = true,
.stackSize = 0,
.priority = TS_PRIORITY_LOW
};
// ============================================================
// Callbacks (fire in task 0 during dvxUpdate)
// ============================================================
// Setting quit = true tells the main loop (running in a separate task) to
// exit. The main loop then destroys the window and returns from appMain,
// which causes the shell to reap this task and unload the DXE.
static void onClose(WindowT *win) {
(void)win;
sState.quit = true;
}
// onPaint demonstrates the raw paint callback approach (no widget tree).
// Instead of using wgtInitWindow + widgets, this app renders directly into
// the window's content buffer. This is the lower-level alternative to the
// widget system — useful for custom rendering like this centered clock display.
//
// We create a temporary DisplayT struct pointing at the window's content buffer
// so the drawing primitives (rectFill, drawText) can operate on the window
// content directly rather than the backbuffer.
static void onPaint(WindowT *win, RectT *dirty) {
(void)dirty;
AppContextT *ac = sCtx->shellCtx;
const BlitOpsT *ops = dvxGetBlitOps(ac);
const BitmapFontT *font = dvxGetFont(ac);
const ColorSchemeT *colors = dvxGetColors(ac);
// Shallow copy of the display, redirected to the window's content buffer.
// This lets us reuse the standard drawing API without modifying the
// global display state.
DisplayT cd = *dvxGetDisplay(ac);
cd.backBuf = win->contentBuf;
cd.width = win->contentW;
cd.height = win->contentH;
cd.pitch = win->contentPitch;
cd.clipX = 0;
cd.clipY = 0;
cd.clipW = win->contentW;
cd.clipH = win->contentH;
// Background
rectFill(&cd, ops, 0, 0, win->contentW, win->contentH, colors->contentBg);
// Time string (large, centered)
int32_t timeW = textWidth(font, sState.timeStr);
int32_t timeX = (win->contentW - timeW) / 2;
int32_t timeY = win->contentH / 3;
drawText(&cd, ops, font, timeX, timeY, sState.timeStr, colors->contentFg, colors->contentBg, true);
// Date string (centered below)
int32_t dateW = textWidth(font, sState.dateStr);
int32_t dateX = (win->contentW - dateW) / 2;
int32_t dateY = timeY + font->charHeight + 8;
drawText(&cd, ops, font, dateX, dateY, sState.dateStr, colors->contentFg, colors->contentBg, true);
}
// ============================================================
// Time update
// ============================================================
static void updateTime(void) {
time_t now = time(NULL);
struct tm *tm = localtime(&now);
if (!tm) {
snprintf(sState.timeStr, sizeof(sState.timeStr), "--:--:--");
snprintf(sState.dateStr, sizeof(sState.dateStr), "----:--:--");
sState.lastUpdate = now;
return;
}
snprintf(sState.timeStr, sizeof(sState.timeStr), "%02d:%02d:%02d", tm->tm_hour, tm->tm_min, tm->tm_sec);
snprintf(sState.dateStr, sizeof(sState.dateStr), "%04d-%02d-%02d", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday);
sState.lastUpdate = now;
}
// ============================================================
// Shutdown hook (optional DXE export)
// ============================================================
// The shell calls appShutdown (if exported) when force-killing an app via
// Task Manager or during shell shutdown. For main-loop apps, this is the
// signal to break out of the main loop. Without this, shellForceKillApp
// would have to terminate the task forcibly, potentially leaking resources.
void appShutdown(void) {
sState.quit = true;
}
// ============================================================
// Entry point (runs in its own task)
// ============================================================
// This runs in its own task. The shell creates the task before calling
// appMain, and reaps it when appMain returns.
int32_t appMain(DxeAppContextT *ctx) {
sCtx = ctx;
AppContextT *ac = ctx->shellCtx;
memset(&sState, 0, sizeof(sState));
updateTime();
// Position in the upper-right corner, out of the way of other windows
int32_t winW = 200;
int32_t winH = 100;
int32_t winX = ac->display.width - winW - 40;
int32_t winY = 40;
// resizable=false: clock has a fixed size, no resize handle
sWin = dvxCreateWindow(ac, "Clock", winX, winY, winW, winH, false);
if (!sWin) {
return -1;
}
sWin->onClose = onClose;
sWin->onPaint = onPaint;
// Initial paint into content buffer
RectT full = {0, 0, sWin->contentW, sWin->contentH};
onPaint(sWin, &full);
dvxInvalidateWindow(ac, sWin);
// Main loop: check if the second has changed, repaint if so, then yield.
// tsYield() transfers control back to the shell's task scheduler.
// On a 486, time() resolution is 1 second, so we yield many times per
// second between actual updates — this keeps CPU usage near zero.
// dvxInvalidateWindow marks the window dirty so the compositor will
// flush it to the LFB on the next composite pass.
while (!sState.quit) {
time_t now = time(NULL);
if (now != sState.lastUpdate) {
updateTime();
onPaint(sWin, &full);
dvxInvalidateWindow(ac, sWin);
}
tsYield();
}
// Cleanup
if (sWin) {
dvxDestroyWindow(ac, sWin);
sWin = NULL;
}
return 0;
}