// 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 #include #include #include #include // ============================================================ // 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; }