// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.

#define DVX_WIDGET_IMPL
// dvx_app.c  -- Layer 5: Application API for DVX GUI
//
// Top-level layer of the DVX windowing system. This is the only layer
// that application code interacts with directly. It owns the main event
// loop, input polling, popup/context menu system, tooltip management,
// accelerator dispatch, clipboard, and window tiling/cascading.
//
// Architecture: poll-based event dispatch
// ----------------------------------------
// The design uses polling (dvxUpdate) rather than an event queue because:
//   1. The target platform (DOS/DPMI) has no OS-provided event queue.
//      BIOS keyboard and mouse services are inherently polled.
//   2. Polling avoids the need for dynamic memory allocation (no malloc
//      per event, no queue growth) which matters on a 486 with limited RAM.
//   3. It maps directly to the DOS main-loop model: poll hardware, process,
//      composite, repeat. No need for event serialization or priority.
//   4. Modal dialogs (message boxes, file dialogs) simply run a nested
//      dvxUpdate loop, which is trivial with polling but would require
//      re-entrant queue draining with an event queue.
//
// The tradeoff is that all input sources are polled every frame, but on a
// 486 this is actually faster than maintaining queue data structures.
//
// Compositing model: only dirty rectangles are redrawn and flushed to the
// LFB (linear framebuffer). The compositor walks bottom-to-top for each
// dirty rect, so overdraw happens in the backbuffer (system RAM), not
// video memory. The final flush is the only VRAM write per dirty rect,
// which is critical because VRAM writes through the PCI/ISA bus are an
// order of magnitude slower than system RAM writes on period hardware.

#include "dvxApp.h"
#include "dvxDlg.h"
#include "dvxWgt.h"
#include "dvxWgtP.h"
#include "../../../widgets/kpunch/timer/timer.h"
#include "dvxFont.h"
#include "dvxCur.h"

#include "dvxPlat.h"
#include "dvxRes.h"
#include "thirdparty/stb_ds_wrap.h"

#include <string.h>
#include <ctype.h>
#include <time.h>

#include "thirdparty/stb_image_wrap.h"
#include "thirdparty/stb_image_write_wrap.h"

// Double-click timing uses CLOCKS_PER_SEC so it's portable between DJGPP
// (where CLOCKS_PER_SEC is typically 91, from the PIT) and Linux/SDL.
#define DBLCLICK_THRESHOLD     (CLOCKS_PER_SEC / 2)

// Minimized icon thumbnails are refreshed in a round-robin fashion rather
// than all at once, spreading the repaint cost across multiple frames.
// Every 8 frames, one dirty icon gets refreshed.
#define ICON_REFRESH_INTERVAL  8

// Keyboard move/resize uses a fixed pixel step per arrow key press.
// 8 pixels keeps it responsive without being too coarse on a 640x480 screen.
#define KB_MOVE_STEP           8

#define MENU_CHECK_WIDTH       14
#define SUBMENU_ARROW_WIDTH    12
#define SUBMENU_ARROW_HALF     3     // half-size of submenu arrow glyph
#define TOOLTIP_DELAY_MS       500
#define TOOLTIP_PAD            3
#define POPUP_BEVEL_WIDTH      2     // popup menu border bevel thickness
#define POPUP_ITEM_PAD_H       8     // extra horizontal padding in popup items
#define MENU_TAB_GAP_CHARS     3     // char-widths gap between label and shortcut

// Cursor dirty region: covers all cursor shapes (16x16 with hotspot at 0,0 or 7,7)
#define CURSOR_HOTSPOT_X       7
#define CURSOR_HOTSPOT_Y       7
#define CURSOR_DIRTY_SIZE      23

// 16.16 fixed-point constants for bilinear wallpaper scaling
#define FP_ONE                 65536
#define FP_FRAC_SHIFT          8     // shift to extract 8-bit fractional part
#define FP_FRAC_MASK           0xFF  // mask for 8-bit fractional part
#define FP_BLEND_MAX           256   // 1.0 in 8-bit fractional domain

// Wallpaper scaler yields to the event loop every N rows
#define WALLPAPER_YIELD_ROWS   32

// Cascade windows default to 2/3 of screen dimensions
#define CASCADE_SIZE_NUMER     2
#define CASCADE_SIZE_DENOM     3

// RGB pixel stride (bytes per pixel in 24-bit RGB)
#define RGB_CHANNELS           3
#define RGBA_CHANNELS          4
#define ALPHA_OPAQUE_MIN       128   // alpha at or above this is opaque
#define KEY_COLOR_R            255   // magenta transparency key
#define KEY_COLOR_G            0
#define KEY_COLOR_B            255

// Tooltip placement relative to the mouse pointer
#define TOOLTIP_OFFSET_X       12
#define TOOLTIP_OFFSET_Y       16
#define TOOLTIP_ABOVE_GAP      4     // gap above the pointer when flipped up

// Popup menu check-mark / radio glyph geometry (relative to glyph center)
#define CHECK_STROKE_H         2     // height of each check-mark stroke
#define RADIO_DIAMOND_HALF     2     // half-size of the radio diamond

// ============================================================
// Window callback dispatch with app ID tracking
// ============================================================
//
// Sets currentAppId to the window's owning app before calling a
// window callback, then restores the previous value. This ensures
// allocations made during menu handlers, close handlers, paint,
// etc. are attributed to the correct app.

#define WIN_CALLBACK(ctx, win, call) do { \
    int32_t _prevAppId = (ctx)->currentAppId; \
    (ctx)->currentAppId = (win)->appId; \
    call; \
    (ctx)->currentAppId = _prevAppId; \
} while (0)


// ============================================================
// Prototypes
// ============================================================

static void activatePopupItem(AppContextT *ctx, int32_t itemIdx);
static void addSysMenuItem(AppContextT *ctx, const char *label, int32_t cmd, bool enabled);
static void addSysMenuSeparator(AppContextT *ctx);
static uint8_t *bufferToRgb(const DisplayT *d, const uint8_t *buf, int32_t w, int32_t h, int32_t pitch);
static uint8_t *buildWallpaperBuf(AppContextT *ctx, const uint8_t *rgb, int32_t imgW, int32_t imgH, WallpaperModeE mode);
static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw, int32_t *ph);
static bool checkAccelTable(AppContextT *ctx, WindowT *win, int32_t key, int32_t modifiers);
static void clampPopupToScreen(AppContextT *ctx);
static void clearMenuBarActive(AppContextT *ctx);
static void clickMenuCheckRadio(MenuT *menu, int32_t itemIdx);
static void closeAllPopups(AppContextT *ctx);
static void closeOpenPopup(AppContextT *ctx);
static void closePopupLevel(AppContextT *ctx);
static void closeSysMenu(AppContextT *ctx);
static uint32_t *colorSlot(ColorSchemeT *cs, ColorIdE id);
static void compositeAndFlush(AppContextT *ctx);
static void consumeMousePress(AppContextT *ctx);
static uint8_t *convertRgbToNative(const DisplayT *d, const uint8_t *src, int32_t channels, int32_t w, int32_t h, uint32_t keyColor, bool *outHasAlpha, int32_t *outPitch);
static int32_t countVisibleWindows(const AppContextT *ctx);
static int32_t cursorForResizeEdge(int32_t edge);
static void cycleWindowFocus(AppContextT *ctx, bool reverse);
static void deferDestroyWindow(AppContextT *ctx, WindowT *win);
static void destroyWindowPrep(AppContextT *ctx, WindowT *win);
static void destroyWindowSync(AppContextT *ctx, WindowT *win);
static void dirtyCursorArea(AppContextT *ctx, int32_t x, int32_t y);
static bool dispatchAccelKey(AppContextT *ctx, char key);
static void dispatchEvents(AppContextT *ctx);
static void ditherRgb(int32_t *r, int32_t *g, int32_t *b, int32_t row, int32_t col);
static void drawCursorAt(AppContextT *ctx, int32_t x, int32_t y);
static void drawPopupLevel(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, const MenuT *menu, int32_t px, int32_t py, int32_t pw, int32_t ph, int32_t hoverItem, const RectT *clipTo);
static void enumModeCb(int32_t w, int32_t h, int32_t bpp, void *userData);
static void executeSysMenuCmd(AppContextT *ctx, int32_t cmd);
static uint8_t *finishLoadImage(const DisplayT *d, uint8_t *rgb, int32_t imgW, int32_t imgH, int32_t *outW, int32_t *outH, int32_t *outPitch);
static WindowT *findWindowById(AppContextT *ctx, int32_t id);
static void flushPendingDestroys(AppContextT *ctx);
static void focusTopmostAlive(AppContextT *ctx);
static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t buttons);
static void initColorScheme(AppContextT *ctx);
static void interactiveSaveBuffer(AppContextT *ctx, const char *title, const uint8_t *src, int32_t w, int32_t h, int32_t pitch);
static void interactiveScreenshot(AppContextT *ctx);
static void interactiveWindowScreenshot(AppContextT *ctx, WindowT *win);
static void invalidateAllWindows(AppContextT *ctx);
static int32_t nextMenuItem(const bool *firstSeparator, size_t stride, int32_t count, int32_t idx, int32_t dir);
static void openContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t screenX, int32_t screenY);
static void openPopupAtMenu(AppContextT *ctx, WindowT *win, int32_t menuIdx);
static void openSubMenu(AppContextT *ctx);
static void openSysMenu(AppContextT *ctx, WindowT *win);
static void pollKeyboard(AppContextT *ctx);
static void pollMouse(AppContextT *ctx);
static void pollWidgets(AppContextT *ctx);
static void popupHandleOutsideCurrent(AppContextT *ctx, int32_t mx, int32_t my, int32_t buttons, int32_t prevBtn);
static int32_t popupItemAt(const AppContextT *ctx, int32_t my);
static void refreshMinimizedIcons(AppContextT *ctx);
static void repositionWindow(AppContextT *ctx, WindowT *win, int32_t x, int32_t y, int32_t w, int32_t h);
static const char *splitMenuLabel(const char *label, char leftBuf[MAX_MENU_LABEL]);
static void updateCursorShape(AppContextT *ctx);
static void updateTooltip(AppContextT *ctx);


// Button pressed via keyboard  -- shared with widgetEvent.c for Space/Enter.
// Non-static so widgetEvent.c can set it when Space/Enter triggers a button.
// The button stays visually pressed for one frame (see dvxUpdate), then the
// click callback fires. This gives the user visual feedback that the
// keyboard activation was registered, matching Win3.x/Motif behavior.
WidgetT *sKeyPressedBtn = NULL;
void (*sCursorBlinkFn)(void) = NULL;

// Widget-destroy generation captured when the tooltip was shown.  If any
// widget has been destroyed while the tooltip is up, the borrowed
// tooltipText pointer may be dangling -- updateTooltip re-validates
// against this instead of pinning the stale tooltip while the mouse
// sits still.
static uint32_t sTooltipShownGen = 0;

// 4x4 Bayer dithering offsets for ordered dithering when converting
// 24-bit wallpaper images to 15/16-bit pixel formats.
static const int32_t sBayerMatrix[4][4] = {
    { -7,  1, -5,  3},
    {  5, -3,  7, -1},
    { -4,  4, -6,  2},
    {  6, -2,  8,  0}
};


// ============================================================
// activatePopupItem  -- fire an enabled, non-separator popup menu item
// ============================================================
//
// Shared by the mouse click, Enter and mnemonic paths so all three
// toggle check/radio state before onMenu sees the item. The popup is
// closed BEFORE calling onMenu because the handler may open a modal
// dialog, which runs a nested dvxUpdate loop; a still-active popup
// would then be drawn/interacted with while stale.

static void activatePopupItem(AppContextT *ctx, int32_t itemIdx) {
    MenuItemT *item = &ctx->popup.menu->items[itemIdx];

    if (item->type == MenuItemCheckE || item->type == MenuItemRadioE) {
        clickMenuCheckRadio(ctx->popup.menu, itemIdx);
    }

    int32_t  menuId = item->id;
    WindowT *win    = findWindowById(ctx, ctx->popup.windowId);
    closeAllPopups(ctx);

    if (win && win->onMenu) {
        WIN_CALLBACK(ctx, win, win->onMenu(win, menuId));
    }
}


// Appends one item to the window system menu, copying the label (NUL-
// terminated), setting its command/enabled state and parsing its accelerator.
static void addSysMenuItem(AppContextT *ctx, const char *label, int32_t cmd, bool enabled) {
    SysMenuItemT *item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++];
    strncpy(item->label, label, MAX_MENU_LABEL - 1);
    item->label[MAX_MENU_LABEL - 1] = '\0';
    item->cmd       = cmd;
    item->separator = false;
    item->enabled   = enabled;
    item->accelKey  = accelParse(item->label);
}


static void addSysMenuSeparator(AppContextT *ctx) {
    SysMenuItemT *sep = &ctx->sysMenu.items[ctx->sysMenu.itemCount++];
    memset(sep, 0, sizeof(*sep));
    sep->separator = true;
}


// ============================================================
// bufferToRgb  -- convert native pixel format to 24-bit RGB
// ============================================================
//
// Screenshots must produce standard RGB data for stb_image_write, but the
// backbuffer uses whatever native pixel format VESA gave us (8/16/32bpp).
// This function handles the conversion using the display's format metadata
// (shift counts, bit widths) rather than assuming a specific layout.
// The 8bpp path uses the VGA palette for lookup.

static uint8_t *bufferToRgb(const DisplayT *d, const uint8_t *buf, int32_t w, int32_t h, int32_t pitch) {
    uint8_t *rgb = (uint8_t *)malloc((size_t)w * h * RGB_CHANNELS);

    if (!rgb) {
        return NULL;
    }

    int32_t bpp = d->format.bytesPerPixel;
    uint8_t *dst = rgb;

    for (int32_t y = 0; y < h; y++) {
        const uint8_t *row = buf + y * pitch;

        for (int32_t x = 0; x < w; x++) {
            uint32_t pixel;

            if (bpp == 1) {
                pixel = row[x];
            } else if (bpp == 2) {
                pixel = ((const uint16_t *)row)[x];
            } else {
                pixel = ((const uint32_t *)row)[x];
            }

            if (d->format.bitsPerPixel == 8) {
                int32_t idx = pixel & 0xFF;
                dst[0] = d->palette[idx * RGB_CHANNELS + 0];
                dst[1] = d->palette[idx * RGB_CHANNELS + 1];
                dst[2] = d->palette[idx * RGB_CHANNELS + 2];
            } else {
                uint32_t rv = (pixel >> d->format.redShift) & ((1u << d->format.redBits) - 1);
                uint32_t gv = (pixel >> d->format.greenShift) & ((1u << d->format.greenBits) - 1);
                uint32_t bv = (pixel >> d->format.blueShift) & ((1u << d->format.blueBits) - 1);
                dst[0] = (uint8_t)(rv << (8 - d->format.redBits));
                dst[1] = (uint8_t)(gv << (8 - d->format.greenBits));
                dst[2] = (uint8_t)(bv << (8 - d->format.blueBits));
            }

            dst += RGB_CHANNELS;
        }
    }

    return rgb;
}


// ============================================================
// buildWallpaperBuf  -- render full-screen wallpaper buffer from RGB source
// ============================================================

static uint8_t *buildWallpaperBuf(AppContextT *ctx, const uint8_t *rgb, int32_t imgW, int32_t imgH, WallpaperModeE mode) {
    int32_t  screenW    = ctx->display.width;
    int32_t  screenH    = ctx->display.height;
    int32_t  bpp        = ctx->display.format.bitsPerPixel;
    int32_t  bytesPerPx = ctx->display.format.bytesPerPixel;
    int32_t  pitch      = screenW * bytesPerPx;
    int32_t  srcStride  = imgW * RGB_CHANNELS;

    bool dither = (bpp == 15 || bpp == 16);

    uint8_t *buf = (uint8_t *)malloc(pitch * screenH);

    if (!buf) {
        return NULL;
    }

    // Fill entire buffer with desktop color first (used by center mode
    // for the border area, but also ensures no garbage pixels)
    uint32_t bgPx = ctx->colors.desktop;

    if (bytesPerPx == 1) {
        memset(buf, bgPx & 0xFF, pitch * screenH);
    } else if (bytesPerPx == 2) {
        uint16_t px16 = (uint16_t)bgPx;
        uint32_t fill32 = ((uint32_t)px16 << 16) | px16;
        for (int32_t y = 0; y < screenH; y++) {
            uint32_t *row = (uint32_t *)(buf + y * pitch);
            int32_t pairs = screenW / 2;
            for (int32_t i = 0; i < pairs; i++) {
                row[i] = fill32;
            }
            if (screenW & 1) {
                *(uint16_t *)(buf + y * pitch + (screenW - 1) * 2) = px16;
            }
        }
    } else {
        uint32_t *row32 = (uint32_t *)buf;
        for (int32_t i = 0; i < pitch * screenH / 4; i++) {
            row32[i] = bgPx;
        }
    }

    if (mode == WallpaperStretchE) {
        // Bilinear scale to screen dimensions with optional dither
        for (int32_t y = 0; y < screenH; y++) {
            if ((y & (WALLPAPER_YIELD_ROWS - 1)) == 0 && y > 0) {
                dvxUpdate(ctx);
            }

            int32_t  srcYfp = (int32_t)((int64_t)y * imgH * FP_ONE / screenH);
            int32_t  sy0    = srcYfp >> 16;
            int32_t  sy1    = (sy0 + 1 < imgH) ? sy0 + 1 : imgH - 1;
            int32_t  fy     = (srcYfp >> FP_FRAC_SHIFT) & FP_FRAC_MASK;
            int32_t  ify    = FP_BLEND_MAX - fy;
            uint8_t *dst    = buf + y * pitch;
            const uint8_t *row0 = rgb + sy0 * srcStride;
            const uint8_t *row1 = rgb + sy1 * srcStride;

            for (int32_t x = 0; x < screenW; x++) {
                int32_t srcXfp = (int32_t)((int64_t)x * imgW * FP_ONE / screenW);
                int32_t sx0    = srcXfp >> 16;
                int32_t sx1    = (sx0 + 1 < imgW) ? sx0 + 1 : imgW - 1;
                int32_t fx     = (srcXfp >> FP_FRAC_SHIFT) & FP_FRAC_MASK;
                int32_t ifx    = FP_BLEND_MAX - fx;

                const uint8_t *p00 = row0 + sx0 * RGB_CHANNELS;
                const uint8_t *p10 = row0 + sx1 * RGB_CHANNELS;
                const uint8_t *p01 = row1 + sx0 * RGB_CHANNELS;
                const uint8_t *p11 = row1 + sx1 * RGB_CHANNELS;

                int32_t r = (p00[0] * ifx * ify + p10[0] * fx * ify + p01[0] * ifx * fy + p11[0] * fx * fy) >> 16;
                int32_t g = (p00[1] * ifx * ify + p10[1] * fx * ify + p01[1] * ifx * fy + p11[1] * fx * fy) >> 16;
                int32_t b = (p00[2] * ifx * ify + p10[2] * fx * ify + p01[2] * ifx * fy + p11[2] * fx * fy) >> 16;

                if (dither) {
                    ditherRgb(&r, &g, &b, y, x);
                }

                uint32_t px = packColor(&ctx->display, (uint8_t)r, (uint8_t)g, (uint8_t)b);
                if (bytesPerPx == 2) {
                    ((uint16_t *)dst)[x] = (uint16_t)px;
                } else if (bytesPerPx == 4) {
                    ((uint32_t *)dst)[x] = px;
                } else {
                    dst[x] = (uint8_t)px;
                }
            }
        }
    } else if (mode == WallpaperTileE) {
        // Tile: repeat the image at native size across the screen
        for (int32_t y = 0; y < screenH; y++) {
            if ((y & (WALLPAPER_YIELD_ROWS - 1)) == 0 && y > 0) {
                dvxUpdate(ctx);
            }

            int32_t         srcY = y % imgH;
            uint8_t        *dst  = buf + y * pitch;
            const uint8_t  *srcRow = rgb + srcY * srcStride;

            for (int32_t x = 0; x < screenW; x++) {
                int32_t        srcX = x % imgW;
                const uint8_t *src  = srcRow + srcX * RGB_CHANNELS;

                int32_t r = src[0];
                int32_t g = src[1];
                int32_t b = src[2];

                if (dither) {
                    ditherRgb(&r, &g, &b, y, x);
                }

                uint32_t px = packColor(&ctx->display, (uint8_t)r, (uint8_t)g, (uint8_t)b);
                if (bytesPerPx == 2) {
                    ((uint16_t *)dst)[x] = (uint16_t)px;
                } else if (bytesPerPx == 4) {
                    ((uint32_t *)dst)[x] = px;
                } else {
                    dst[x] = (uint8_t)px;
                }
            }
        }
    } else {
        // Center: place at native size in the center of the screen.
        // Buffer was already filled with desktop color above.
        // Clip the source region to only iterate visible pixels.
        int32_t offX = (screenW - imgW) / 2;
        int32_t offY = (screenH - imgH) / 2;

        int32_t srcStartX = (offX < 0) ? -offX : 0;
        int32_t srcStartY = (offY < 0) ? -offY : 0;
        int32_t srcEndX   = imgW;
        int32_t srcEndY   = imgH;

        if (offX + srcEndX > screenW) {
            srcEndX = screenW - offX;
        }

        if (offY + srcEndY > screenH) {
            srcEndY = screenH - offY;
        }

        for (int32_t sy = srcStartY; sy < srcEndY; sy++) {
            if (((sy - srcStartY) & (WALLPAPER_YIELD_ROWS - 1)) == 0 && sy > srcStartY) {
                dvxUpdate(ctx);
            }

            int32_t dy = offY + sy;
            uint8_t       *dst    = buf + dy * pitch;
            const uint8_t *srcRow = rgb + sy * srcStride;

            for (int32_t sx = srcStartX; sx < srcEndX; sx++) {
                int32_t dx = offX + sx;

                const uint8_t *src = srcRow + sx * RGB_CHANNELS;
                int32_t r = src[0];
                int32_t g = src[1];
                int32_t b = src[2];

                if (dither) {
                    ditherRgb(&r, &g, &b, dy, dx);
                }

                uint32_t px = packColor(&ctx->display, (uint8_t)r, (uint8_t)g, (uint8_t)b);
                if (bytesPerPx == 2) {
                    ((uint16_t *)dst)[dx] = (uint16_t)px;
                } else if (bytesPerPx == 4) {
                    ((uint32_t *)dst)[dx] = px;
                } else {
                    dst[dx] = (uint8_t)px;
                }
            }
        }
    }

    return buf;
}


// ============================================================
// calcPopupSize  -- compute popup width and height for a menu
// ============================================================
//
// Popup width is determined by the widest item, plus conditional margins
// for check/radio indicators and submenu arrows. Menu labels use a tab
// character to separate the item text from the keyboard shortcut string
// (e.g., "&Save\tCtrl+S"), which are measured and laid out independently
// so shortcuts right-align within the popup. The '&' prefix in labels
// marks the accelerator underline character and is excluded from width
// measurement by textWidthAccel.

static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw, int32_t *ph) {
    int32_t maxW     = 0;
    bool    hasSub   = false;
    bool    hasCheck = false;

    for (int32_t k = 0; k < menu->itemCount; k++) {
        const char *label = menu->items[k].label;
        char        leftBuf[MAX_MENU_LABEL];
        const char *shortcut = splitMenuLabel(label, leftBuf);
        int32_t     itemW;

        if (shortcut) {
            // Left part (with accel underline) + gap + right part (shortcut text)
            itemW = textWidthAccel(&ctx->font, leftBuf) + ctx->font.charWidth * MENU_TAB_GAP_CHARS + textWidth(&ctx->font, shortcut);
        } else {
            itemW = textWidthAccel(&ctx->font, label);
        }

        if (itemW > maxW) {
            maxW = itemW;
        }

        if (menu->items[k].subMenu) {
            hasSub = true;
        }

        if (menu->items[k].type == MenuItemCheckE || menu->items[k].type == MenuItemRadioE) {
            hasCheck = true;
        }
    }

    // Width includes: padding, text, check margin (if any items are check/radio),
    // submenu arrow space (if any items have submenus). All items in the popup
    // share the same width for visual consistency, even if only some have checks.
    *pw = maxW + CHROME_TITLE_PAD * 2 + POPUP_ITEM_PAD_H + (hasSub ? SUBMENU_ARROW_WIDTH : 0) + (hasCheck ? MENU_CHECK_WIDTH : 0);
    *ph = menu->itemCount * ctx->font.charHeight + POPUP_BEVEL_WIDTH * 2;
}


// ============================================================
// checkAccelTable  -- test key against window's accelerator table
// ============================================================
//
// Accelerator tables map key+modifier combos (e.g., Ctrl+S) to menu
// command IDs. Keys are pre-normalized to uppercase at registration time
// (in dvxAddAccel) so matching here is a simple linear scan with no
// allocation. Linear scan is fine because accelerator tables are small
// (typically <20 entries) and this runs at most once per keypress.
//
// BIOS keyboard quirk: Ctrl+A through Ctrl+Z come through as ASCII
// 0x01..0x1A rather than 'A'..'Z'. We reverse that mapping here so the
// user-facing accelerator definition can use plain letter keys.

static bool checkAccelTable(AppContextT *ctx, WindowT *win, int32_t key, int32_t modifiers) {
    if (!win->accelTable || !win->onMenu) {
        return false;
    }

    // Normalize Ctrl+letter: BIOS returns ASCII 0x01-0x1A for Ctrl+A..Z
    // Map back to uppercase letter for matching
    int32_t matchKey = key;

    if ((modifiers & ACCEL_CTRL) && matchKey >= KEY_ASCII_CTRL_A && matchKey <= KEY_ASCII_CTRL_Z) {
        matchKey = matchKey - KEY_ASCII_CTRL_A + 'A';
    }

    // Uppercase for case-insensitive letter matching
    if (matchKey >= 'a' && matchKey <= 'z') {
        matchKey = toupper(matchKey);
    }

    int32_t      requiredMods = modifiers & (ACCEL_CTRL | ACCEL_ALT);
    AccelTableT *table        = win->accelTable;

    for (int32_t i = 0; i < table->count; i++) {
        AccelEntryT *e = &table->entries[i];

        if (e->normKey == matchKey && e->normMods == requiredMods) {
            WIN_CALLBACK(ctx, win, win->onMenu(win, e->cmdId));
            return true;
        }
    }

    return false;
}


// Keeps the active popup rect on-screen.  Shared by the context-menu,
// menu-bar dropdown, and submenu open paths so off-screen items (and
// thus unreachable clicks, since the mouse can't leave the screen)
// can't happen near the bottom/right edges.
static void clampPopupToScreen(AppContextT *ctx) {
    if (ctx->popup.popupX + ctx->popup.popupW > ctx->display.width) {
        ctx->popup.popupX = ctx->display.width - ctx->popup.popupW;
    }

    if (ctx->popup.popupY + ctx->popup.popupH > ctx->display.height) {
        ctx->popup.popupY = ctx->display.height - ctx->popup.popupH;
    }

    if (ctx->popup.popupX < 0) {
        ctx->popup.popupX = 0;
    }

    if (ctx->popup.popupY < 0) {
        ctx->popup.popupY = 0;
    }
}


// Clears the depressed (open) menu-bar item on the popup's owning window and
// dirties the title/menu band so it repaints raised.  Shared by the popup
// close paths.
static void clearMenuBarActive(AppContextT *ctx) {
    WindowT *popupWin = findWindowById(ctx, ctx->popup.windowId);

    if (popupWin && popupWin->menuBar && popupWin->menuBar->activeIdx >= 0) {
        popupWin->menuBar->activeIdx = -1;
        dirtyListAdd(&ctx->dirty, popupWin->x, popupWin->y, popupWin->w, CHROME_TITLEBAR_BOTTOM + CHROME_MENU_HEIGHT);
    }
}


// Applies a click to a check or radio item: check items toggle, radio items
// select.  The implicit contiguous-group uncheck is delegated to the shared
// menuItemApplyChecked so the grouping rule lives in exactly one place.
static void clickMenuCheckRadio(MenuT *menu, int32_t itemIdx) {
    MenuItemT *item = &menu->items[itemIdx];

    if (item->type == MenuItemCheckE) {
        menuItemApplyChecked(menu, itemIdx, !item->checked);
    } else if (item->type == MenuItemRadioE) {
        menuItemApplyChecked(menu, itemIdx, true);
    }
}


// ============================================================
// closeAllPopups  -- dirty all popup levels and deactivate
// ============================================================
//
// Popup menus can be nested (submenus). The popup system uses a stack
// (parentStack) where the current level is always in popup.menu/popupX/etc.
// and parent levels are saved in parentStack[0..depth-1]. Closing all
// popups means dirtying every level's screen area so the compositor
// repaints those regions, then resetting state. We must dirty every
// level individually because submenus may not overlap their parents.

static void closeAllPopups(AppContextT *ctx) {
    if (!ctx->popup.active) {
        return;
    }

    // Dirty current (deepest) level
    dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
                 ctx->popup.popupW, ctx->popup.popupH);

    // Dirty all parent levels
    for (int32_t i = 0; i < ctx->popup.depth; i++) {
        PopupLevelT *pl = &ctx->popup.parentStack[i];
        dirtyListAdd(&ctx->dirty, pl->popupX, pl->popupY, pl->popupW, pl->popupH);
    }

    // Clear the depressed menu bar item
    clearMenuBarActive(ctx);

    ctx->popup.active = false;
    ctx->popup.depth  = 0;
}


// Close the open dropdown/combobox list popup (sOpenPopup), dirtying its
// on-screen area so the overlay is erased.  Used by the ESC key and by the
// focus-change paths (Tab, accelerator) so a popup never lingers -- with its
// d->open flag stale -- after focus moves to another widget.  No-op when no
// popup is open.
static void closeOpenPopup(AppContextT *ctx) {
    if (!sOpenPopup) {
        return;
    }

    WindowT *popWin = sOpenPopup->window;
    int32_t  popX;
    int32_t  popY;
    int32_t  popW;
    int32_t  popH;

    wclsGetPopupRect(sOpenPopup, &ctx->font, popWin->contentH, &popX, &popY, &popW, &popH);
    dirtyListAdd(&ctx->dirty, popWin->x + popWin->contentX + popX, popWin->y + popWin->contentY + popY, popW, popH);

    WidgetT *closing = sOpenPopup;
    sOpenPopup = NULL;

    wclsClosePopup(closing);
    wgtInvalidate(closing);
}


// ============================================================
// closePopupLevel  -- close one submenu level (or deactivate if top)
// ============================================================
//
// Pops one level off the popup stack. If we're already at the top
// level (depth==0), the entire popup system is deactivated. Otherwise,
// the parent level's state is restored as the new "current" level.
// This enables Left Arrow to close a submenu and return to the parent,
// matching standard Windows/Motif keyboard navigation.

static void closePopupLevel(AppContextT *ctx) {
    if (!ctx->popup.active) {
        return;
    }

    // Dirty current level
    dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
                 ctx->popup.popupW, ctx->popup.popupH);

    if (ctx->popup.depth > 0) {
        // Pop parent
        ctx->popup.depth--;
        PopupLevelT *pl  = &ctx->popup.parentStack[ctx->popup.depth];
        ctx->popup.menu      = pl->menu;
        ctx->popup.menuIdx   = pl->menuIdx;
        ctx->popup.popupX    = pl->popupX;
        ctx->popup.popupY    = pl->popupY;
        ctx->popup.popupW    = pl->popupW;
        ctx->popup.popupH    = pl->popupH;
        ctx->popup.hoverItem = pl->hoverItem;
    } else {
        // Clear the depressed menu bar item
        clearMenuBarActive(ctx);

        ctx->popup.active = false;
    }
}


// ============================================================
// closeSysMenu
// ============================================================

static void closeSysMenu(AppContextT *ctx) {
    if (ctx->sysMenu.active) {
        dirtyListAdd(&ctx->dirty, ctx->sysMenu.popupX, ctx->sysMenu.popupY,
                     ctx->sysMenu.popupW, ctx->sysMenu.popupH);
        ctx->sysMenu.active = false;
    }
}


// ============================================================
// Color scheme  -- name table and indexed access
// ============================================================

// INI/theme key names (must not change  -- used for save/load)
static const char *sColorNames[ColorCountE] = {
    "desktop",           "windowFace",        "windowHighlight",
    "windowShadow",      "activeTitleBg",     "activeTitleFg",
    "inactiveTitleBg",   "inactiveTitleFg",   "contentBg",
    "contentFg",         "menuBg",            "menuFg",
    "menuHighlightBg",   "menuHighlightFg",   "buttonFace",
    "scrollbarBg",       "scrollbarFg",       "scrollbarTrough",
    "cursorColor",       "cursorOutline"
};

// Human-readable display names for the UI
static const char *sColorLabels[ColorCountE] = {
    "Desktop",               "Window Face",           "Window Highlight",
    "Window Shadow",         "Active Title Bar",      "Active Title Text",
    "Inactive Title Bar",    "Inactive Title Text",   "Content Background",
    "Content Text",          "Menu Background",       "Menu Text",
    "Menu Highlight",        "Menu Highlight Text",   "Button Face",
    "Scrollbar Background",  "Scrollbar Foreground",  "Scrollbar Trough",
    "Cursor Color",          "Cursor Outline"
};

// Default GEOS Ensemble Motif-style colors (RGB triplets)
static const uint8_t sDefaultColors[ColorCountE][3] = {
    {  0, 128, 128},  // desktop  -- GEOS teal
    {192, 192, 192},  // windowFace
    {255, 255, 255},  // windowHighlight
    {128, 128, 128},  // windowShadow
    { 48,  48,  48},  // activeTitleBg  -- dark charcoal
    {255, 255, 255},  // activeTitleFg
    {160, 160, 160},  // inactiveTitleBg
    { 64,  64,  64},  // inactiveTitleFg
    {192, 192, 192},  // contentBg
    {  0,   0,   0},  // contentFg
    {192, 192, 192},  // menuBg
    {  0,   0,   0},  // menuFg
    { 48,  48,  48},  // menuHighlightBg
    {255, 255, 255},  // menuHighlightFg
    {192, 192, 192},  // buttonFace
    {192, 192, 192},  // scrollbarBg
    {128, 128, 128},  // scrollbarFg
    {160, 160, 160},  // scrollbarTrough
    {255, 255, 255},  // cursorFg  -- white
    {  0,   0,   0},  // cursorBg  -- black
};

// Access the packed color value in ColorSchemeT by index.
_Static_assert(offsetof(ColorSchemeT, desktop)         == (size_t)ColorDesktopE         * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: desktop");
_Static_assert(offsetof(ColorSchemeT, windowFace)      == (size_t)ColorWindowFaceE      * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: windowFace");
_Static_assert(offsetof(ColorSchemeT, windowHighlight) == (size_t)ColorWindowHighlightE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: windowHighlight");
_Static_assert(offsetof(ColorSchemeT, windowShadow)    == (size_t)ColorWindowShadowE    * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: windowShadow");
_Static_assert(offsetof(ColorSchemeT, activeTitleBg)   == (size_t)ColorActiveTitleBgE   * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: activeTitleBg");
_Static_assert(offsetof(ColorSchemeT, activeTitleFg)   == (size_t)ColorActiveTitleFgE   * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: activeTitleFg");
_Static_assert(offsetof(ColorSchemeT, inactiveTitleBg) == (size_t)ColorInactiveTitleBgE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: inactiveTitleBg");
_Static_assert(offsetof(ColorSchemeT, inactiveTitleFg) == (size_t)ColorInactiveTitleFgE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: inactiveTitleFg");
_Static_assert(offsetof(ColorSchemeT, contentBg)       == (size_t)ColorContentBgE       * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: contentBg");
_Static_assert(offsetof(ColorSchemeT, contentFg)       == (size_t)ColorContentFgE       * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: contentFg");
_Static_assert(offsetof(ColorSchemeT, menuBg)          == (size_t)ColorMenuBgE          * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: menuBg");
_Static_assert(offsetof(ColorSchemeT, menuFg)          == (size_t)ColorMenuFgE          * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: menuFg");
_Static_assert(offsetof(ColorSchemeT, menuHighlightBg) == (size_t)ColorMenuHighlightBgE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: menuHighlightBg");
_Static_assert(offsetof(ColorSchemeT, menuHighlightFg) == (size_t)ColorMenuHighlightFgE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: menuHighlightFg");
_Static_assert(offsetof(ColorSchemeT, buttonFace)      == (size_t)ColorButtonFaceE      * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: buttonFace");
_Static_assert(offsetof(ColorSchemeT, scrollbarBg)     == (size_t)ColorScrollbarBgE     * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: scrollbarBg");
_Static_assert(offsetof(ColorSchemeT, scrollbarFg)     == (size_t)ColorScrollbarFgE     * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: scrollbarFg");
_Static_assert(offsetof(ColorSchemeT, scrollbarTrough) == (size_t)ColorScrollbarTroughE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: scrollbarTrough");
_Static_assert(offsetof(ColorSchemeT, cursorFg)        == (size_t)ColorCursorFgE        * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: cursorFg");
_Static_assert(offsetof(ColorSchemeT, cursorBg)        == (size_t)ColorCursorBgE        * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: cursorBg");


static uint32_t *colorSlot(ColorSchemeT *cs, ColorIdE id) {
    return ((uint32_t *)cs) + (int32_t)id;
}


// ============================================================
// compositeAndFlush
// ============================================================
//
// The compositor is the heart of the rendering pipeline. For each dirty
// rectangle, it redraws the entire Z-ordered scene into the backbuffer
// (system RAM) then flushes that rectangle to the LFB (video memory).
//
// Rendering order per dirty rect (painter's algorithm, back-to-front):
//   1. Desktop background fill
//   2. Minimized window icons (always below all windows)
//   3. Non-minimized windows, bottom-to-top (chrome + content + scrollbars)
//   4. Popup menus (all levels, parent first, then current/deepest)
//   4b. System menu (window close-gadget menu)
//   5. Tooltip
//   6. Hardware cursor (software-rendered, always on top)
//   7. Flush dirty rect from backbuffer to LFB
//
// Pre-filtering the visible window list avoids redundant minimized/hidden
// checks in the inner loop. The clip rect is set to each dirty rect so
// draw calls outside the rect are automatically clipped by the draw layer,
// avoiding unnecessary pixel writes.

static void compositeAndFlush(AppContextT *ctx) {
    DisplayT     *d   = &ctx->display;
    BlitOpsT     *ops = &ctx->blitOps;
    DirtyListT   *dl  = &ctx->dirty;
    WindowStackT *ws  = &ctx->stack;

    // Merge overlapping dirty rects to reduce flush count
    dirtyListMerge(dl);

    // Pre-filter visible, non-minimized windows once to avoid
    // re-checking visibility in the inner dirty-rect loop
    int32_t visibleIdx[ws->count > 0 ? ws->count : 1];
    int32_t visibleCount = 0;

    for (int32_t j = 0; j < ws->count; j++) {
        WindowT *win = ws->windows[j];

        if (win->visible && !win->minimized) {
            visibleIdx[visibleCount++] = j;
        }
    }

    for (int32_t i = 0; i < dl->count; i++) {
        RectT *dr = &dl->rects[i];

        // Clip dirty rect to screen bounds
        if (dr->x < 0) { dr->w += dr->x; dr->x = 0; }
        if (dr->y < 0) { dr->h += dr->y; dr->y = 0; }
        if (dr->x + dr->w > d->width)  { dr->w = d->width - dr->x; }
        if (dr->y + dr->h > d->height) { dr->h = d->height - dr->y; }
        if (dr->w <= 0 || dr->h <= 0) { continue; }

        // Set clip rect to this dirty rect
        setClipRect(d, dr->x, dr->y, dr->w, dr->h);

        // 1. Draw desktop background (wallpaper or solid color)
        if (ctx->wallpaperBuf) {
            int32_t bpp = d->format.bytesPerPixel;

            for (int32_t row = dr->y; row < dr->y + dr->h; row++) {
                uint8_t *src = ctx->wallpaperBuf + row * ctx->wallpaperPitch + dr->x * bpp;
                uint8_t *dst = d->backBuf + row * d->pitch + dr->x * bpp;
                memcpy(dst, src, dr->w * bpp);
            }
        } else {
            rectFill(d, ops, dr->x, dr->y, dr->w, dr->h, ctx->colors.desktop);
        }

        // 2. Draw minimized window icons (under all windows)
        wmDrawMinimizedIcons(d, ops, &ctx->colors, ws, dr);

        // 3. Walk pre-filtered visible window list bottom-to-top
        for (int32_t vi = 0; vi < visibleCount; vi++) {
            WindowT *win = ws->windows[visibleIdx[vi]];

            // Check if window intersects this dirty rect
            RectT winRect = {win->x, win->y, win->w, win->h};
            RectT isect;

            if (!rectIntersect(dr, &winRect, &isect)) {
                continue;
            }

            wmDrawChrome(d, ops, &ctx->font, &ctx->colors, win, dr);
            wmDrawContent(d, ops, win, dr);
            if (win->vScroll || win->hScroll) {
                wmDrawScrollbars(d, ops, &ctx->colors, win, dr);
            }
        }

        // 4. Draw popup menu if active (all levels)
        if (ctx->popup.active) {
            // Draw parent levels first (bottom to top)
            for (int32_t lvl = 0; lvl < ctx->popup.depth; lvl++) {
                PopupLevelT *pl = &ctx->popup.parentStack[lvl];
                drawPopupLevel(ctx, d, ops, pl->menu, pl->popupX, pl->popupY, pl->popupW, pl->popupH, pl->hoverItem, dr);
            }

            // Draw current (deepest) level
            drawPopupLevel(ctx, d, ops, ctx->popup.menu, ctx->popup.popupX, ctx->popup.popupY, ctx->popup.popupW, ctx->popup.popupH, ctx->popup.hoverItem, dr);
        }

        // 4b. Draw system menu if active
        if (ctx->sysMenu.active) {
            RectT smRect = { ctx->sysMenu.popupX, ctx->sysMenu.popupY, ctx->sysMenu.popupW, ctx->sysMenu.popupH };
            RectT smIsect;

            if (rectIntersect(dr, &smRect, &smIsect)) {
                BevelStyleT smBevel = BEVEL_POPUP(&ctx->colors, POPUP_BEVEL_WIDTH);
                drawBevel(d, ops, ctx->sysMenu.popupX, ctx->sysMenu.popupY, ctx->sysMenu.popupW, ctx->sysMenu.popupH, &smBevel);

                int32_t itemY = ctx->sysMenu.popupY + POPUP_BEVEL_WIDTH;

                for (int32_t k = 0; k < ctx->sysMenu.itemCount; k++) {
                    SysMenuItemT *item = &ctx->sysMenu.items[k];

                    if (item->separator) {
                        drawHLine(d, ops, ctx->sysMenu.popupX + POPUP_BEVEL_WIDTH, itemY + ctx->font.charHeight / 2, ctx->sysMenu.popupW - POPUP_BEVEL_WIDTH * 2, ctx->colors.windowShadow);
                        itemY += ctx->font.charHeight;
                        continue;
                    }

                    uint32_t bg = ctx->colors.menuBg;
                    uint32_t fg = item->enabled ? ctx->colors.menuFg : ctx->colors.windowShadow;

                    if (k == ctx->sysMenu.hoverItem && item->enabled) {
                        bg = ctx->colors.menuHighlightBg;
                        fg = ctx->colors.menuHighlightFg;
                    }

                    rectFill(d, ops, ctx->sysMenu.popupX + POPUP_BEVEL_WIDTH, itemY, ctx->sysMenu.popupW - POPUP_BEVEL_WIDTH * 2, ctx->font.charHeight, bg);
                    drawTextAccel(d, ops, &ctx->font, ctx->sysMenu.popupX + CHROME_TITLE_PAD + POPUP_BEVEL_WIDTH, itemY, item->label, fg, bg, true);

                    itemY += ctx->font.charHeight;
                }
            }
        }

        // 5. Draw tooltip
        if (ctx->tooltipText) {
            RectT ttRect = { ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH };
            RectT ttIsect;

            if (rectIntersect(dr, &ttRect, &ttIsect)) {
                rectFill(d, ops, ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH, ctx->colors.menuBg);
                drawRectOutline(d, ops, ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH, ctx->colors.contentFg);
                drawText(d, ops, &ctx->font, ctx->tooltipX + TOOLTIP_PAD, ctx->tooltipY + TOOLTIP_PAD, ctx->tooltipText, ctx->colors.menuFg, ctx->colors.menuBg, true);
            }
        }

        // 6. Draw cursor
        drawCursorAt(ctx, ctx->mouseX, ctx->mouseY);

        // 7. Flush this dirty rect to LFB
        flushRect(d, dr);
    }

    resetClipRect(d);
    dirtyListClear(dl);
}


// Consumes the current left-button press: marks the edge already-seen and
// swallows the matching release.  Called before a press handler pumps a
// nested dvxUpdate loop (sys-menu command, popup-menu command) so the
// still-held button is not re-dispatched as a fresh MouseDown, then
// released onto whatever sits under the now-closed menu.
static void consumeMousePress(AppContextT *ctx) {
    ctx->prevMouseButtons   |= MOUSE_LEFT;
    ctx->suppressNextMouseUp = true;
}


// ============================================================
// convertRgbToNative  -- pack an stb_image RGB/RGBA buffer to display format
// ============================================================
//
// channels is 3 (RGB) or 4 (RGBA). For RGBA, pixels below the opaque
// threshold are replaced by keyColor and *outHasAlpha is set. Returns a
// malloc'd buffer (pitch in *outPitch) or NULL on allocation failure.

static uint8_t *convertRgbToNative(const DisplayT *d, const uint8_t *src, int32_t channels, int32_t w, int32_t h, uint32_t keyColor, bool *outHasAlpha, int32_t *outPitch) {
    int32_t  bpp      = d->format.bytesPerPixel;
    int32_t  pitch    = w * bpp;
    bool     hasAlpha = false;
    uint8_t *buf      = (uint8_t *)malloc((size_t)pitch * h);

    if (!buf) {
        return NULL;
    }

    for (int32_t y = 0; y < h; y++) {
        for (int32_t x = 0; x < w; x++) {
            const uint8_t *px  = src + (y * w + x) * channels;
            uint8_t       *dst = buf + y * pitch + x * bpp;
            uint32_t       color;

            if (channels == RGBA_CHANNELS && px[RGB_CHANNELS] < ALPHA_OPAQUE_MIN) {
                color    = keyColor;
                hasAlpha = true;
            } else {
                color = packColor(d, px[0], px[1], px[2]);
            }

            if (bpp == 1) {
                *dst = (uint8_t)color;
            } else if (bpp == 2) {
                *(uint16_t *)dst = (uint16_t)color;
            } else {
                *(uint32_t *)dst = color;
            }
        }
    }

    if (outHasAlpha) {
        *outHasAlpha = hasAlpha;
    }

    *outPitch = pitch;

    return buf;
}


// ============================================================
// countVisibleWindows  -- count non-minimized visible windows
// ============================================================

static int32_t countVisibleWindows(const AppContextT *ctx) {
    int32_t count = 0;

    for (int32_t i = 0; i < ctx->stack.count; i++) {
        WindowT *win = ctx->stack.windows[i];

        if (!win->minimized && win->visible) {
            count++;
        }
    }

    return count;
}


// ============================================================
// cursorForResizeEdge  -- map a RESIZE_* edge mask to a cursor shape
// ============================================================

static int32_t cursorForResizeEdge(int32_t edge) {
    bool horiz = (edge & (RESIZE_LEFT | RESIZE_RIGHT)) != 0;
    bool vert  = (edge & (RESIZE_TOP | RESIZE_BOTTOM)) != 0;

    if (horiz && vert) {
        if ((edge & RESIZE_LEFT && edge & RESIZE_TOP) ||
            (edge & RESIZE_RIGHT && edge & RESIZE_BOTTOM)) {
            return CURSOR_RESIZE_DIAG_NWSE;
        }

        return CURSOR_RESIZE_DIAG_NESW;
    }

    if (horiz) {
        return CURSOR_RESIZE_H;
    }

    if (vert) {
        return CURSOR_RESIZE_V;
    }

    return CURSOR_ARROW;
}


// ============================================================
// cycleWindowFocus  -- Alt+Tab / Shift+Alt+Tab window cycling
// ============================================================
//
// Unlike Windows, there's no task-switcher overlay here  -- each press
// immediately rotates the window stack and focuses the new top.
// Alt+Tab sends the top window to the bottom of the stack and raises
// the next visible window below it. Shift+Alt+Tab does the reverse,
// pulling the bottom-most visible window to the top. Hidden and
// deferred-destroyed windows are never candidates, and a modal dialog
// blocks cycling entirely (matching the mouse input gate).

static void cycleWindowFocus(AppContextT *ctx, bool reverse) {
    WindowStackT *stack  = &ctx->stack;
    int32_t       top    = stack->count - 1;
    int32_t       target = -1;

    if (stack->count < 2 || ctx->modalWindow) {
        return;
    }

    if (reverse) {
        for (int32_t i = 0; i < top; i++) {
            if (stack->windows[i]->visible && !stack->windows[i]->destroyPending) {
                target = i;
                break;
            }
        }
    } else {
        for (int32_t i = top - 1; i >= 0; i--) {
            if (stack->windows[i]->visible && !stack->windows[i]->destroyPending) {
                target = i;
                break;
            }
        }
    }

    if (target < 0) {
        return;
    }

    if (!reverse) {
        // Rotate: move top to bottom, shift everything else up. The
        // focused-index reference follows the moved window so wmSetFocus
        // below still sees it as the outgoing window and fires onBlur.
        WindowT *topWin = stack->windows[top];
        dirtyListAdd(&ctx->dirty, topWin->x, topWin->y, topWin->w, topWin->h);

        for (int32_t i = top; i > 0; i--) {
            stack->windows[i] = stack->windows[i - 1];
        }

        stack->windows[0] = topWin;

        if (stack->focusedIdx == top) {
            stack->focusedIdx = 0;
        }

        target++;
    }

    wmRaiseWindow(stack, &ctx->dirty, target);
    wmSetFocus(stack, &ctx->dirty, top);
}


// ============================================================
// deferDestroyWindow
// ============================================================
//
// Queues a window for destruction at the end of the current dispatch
// pass (see dvxUpdate).  Called by dvxDestroyWindow when a user callback
// is on the stack: freeing the window immediately would leave the
// dispatch code touching freed widgets when the callback returns.  The
// window is hidden right away -- it vanishes and stops receiving input
// this frame -- but its memory stays valid until flushPendingDestroys.

static void deferDestroyWindow(AppContextT *ctx, WindowT *win) {
    if (win->destroyPending) {
        return;
    }

    win->destroyPending = true;
    ctx->pendingDestroyCount++;

    // Hide the window now so this frame already composites without it.
    if (win->visible) {
        dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);
        win->visible = false;
    }

    if (win->minimized) {
        int32_t iconY;
        int32_t iconH;
        wmMinimizedIconRect(&ctx->stack, &ctx->display, &iconY, &iconH);
        dirtyListAdd(&ctx->dirty, 0, iconY, ctx->display.width, iconH);
    }

    // Cut off every input route into the dying window: drop the widget
    // focus/drag/popup/key-press globals aimed at its (still-allocated)
    // widget tree -- its userData may be freed before the flush runs.
    widgetDetachWindowReferences(win);

    // Move WM focus off the dying window.  Silent drop, matching
    // wmDestroyWindow: never fire the dying window's onBlur into
    // torn-down app state.  With focusedIdx pre-cleared, wmSetFocus sees
    // oldWin == NULL and fires only the successor's onFocus.  If no
    // successor exists, focusedIdx stays -1 -- the established
    // zero-windows state every input path already tolerates.
    if (win->focused) {
        win->focused          = false;
        ctx->stack.focusedIdx = -1;
        focusTopmostAlive(ctx);
    }
}


// ============================================================
// destroyWindowPrep
// ============================================================
//
// Pre-teardown cleanup shared by dvxDestroyWindow and
// dvxDestroyWindowNow: drops every context-level pointer that may
// reference the window or its widgets, whether the teardown itself is
// immediate or deferred.

static void destroyWindowPrep(AppContextT *ctx, WindowT *win) {
    // Close any open popup or sys menu owned by this window FIRST.  The
    // popup state holds raw MenuT pointers into win->menuBar, which dies
    // with the window, and compositeAndFlush dereferences them
    // unconditionally while popup.active is set -- a guaranteed same-frame
    // use-after-free without this.
    if (ctx->popup.active && ctx->popup.windowId == win->id) {
        closeAllPopups(ctx);
    }

    if (ctx->sysMenu.active && ctx->sysMenu.windowId == win->id) {
        closeSysMenu(ctx);
    }

    // Clear the modal pointer if it was this window, even when deferring
    // -- a hidden pending-destroy modal must not keep eating input.
    if (ctx->modalWindow == win) {
        ctx->modalWindow = NULL;
    }

    // The tooltip text may point into a widget tooltip or a window title
    // that wgtDestroy is about to free; drop it (and dirty its old area)
    // before any teardown -- the string can be freed independently of the
    // deferred-destroy path.
    dvxInvalidateTooltip(ctx, NULL);
}


// ============================================================
// destroyWindowSync
// ============================================================
//
// Immediate window teardown: dirties the region, destroys the window and
// its widget tree, and refocuses the topmost surviving window.  Callers
// must run destroyWindowPrep first.

static void destroyWindowSync(AppContextT *ctx, WindowT *win) {
    dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);

    // Cache the minimized flag before wmDestroyWindow frees win, so the
    // post-destroy icon-strip dirty below does not read freed memory.
    bool wasMinimized = win->minimized;

    // If the window is minimized, dirty the icon strip so the icon
    // disappears and remaining icons repack correctly.
    if (wasMinimized) {
        int32_t iconY;
        int32_t iconH;
        wmMinimizedIconRect(&ctx->stack, &ctx->display, &iconY, &iconH);
        dirtyListAdd(&ctx->dirty, 0, iconY, ctx->display.width, iconH);
    }

    wmDestroyWindow(&ctx->stack, win);

    // Dirty icon area again with the updated count (one fewer icon)
    if (wasMinimized) {
        int32_t iconY;
        int32_t iconH;
        wmMinimizedIconRect(&ctx->stack, &ctx->display, &iconY, &iconH);
        dirtyListAdd(&ctx->dirty, 0, iconY, ctx->display.width, iconH);
    }

    // Focus the new top window, skipping any still queued for destroy
    focusTopmostAlive(ctx);
}


// ============================================================
// dirtyCursorArea
// ============================================================
//
// Dirties a 23x23 pixel area centered on the worst-case cursor bounds.
// We use a fixed size that covers ALL cursor shapes rather than the
// current shape's exact bounds. This handles the case where the cursor
// shape changes between frames (e.g., arrow to resize)  -- we need to
// erase the old shape AND draw the new one, and both might have
// different hotspot offsets. The 23x23 area is the union of all possible
// cursor footprints (16x16 with hotspot at 0,0 or 7,7).

static void dirtyCursorArea(AppContextT *ctx, int32_t x, int32_t y) {
    dirtyListAdd(&ctx->dirty, x - CURSOR_HOTSPOT_X, y - CURSOR_HOTSPOT_Y, CURSOR_DIRTY_SIZE, CURSOR_DIRTY_SIZE);
}


// ============================================================
// dispatchAccelKey  -- route Alt+key to menu or widget
// ============================================================
//
// Handles Alt+letter keypresses. Menu bar accelerators are checked first
// (e.g., Alt+F for File menu), then widget tree accelerators (e.g.,
// Alt+O for an "&OK" button). Labels use the '&' convention to mark the
// accelerator character, matching Windows/Motif conventions.
//
// For non-focusable widgets like labels and frames, the accelerator
// transfers focus to the next focusable sibling  -- this lets labels act
// as access keys for adjacent input fields, following standard GUI idiom.

static bool dispatchAccelKey(AppContextT *ctx, char key) {
    if (ctx->stack.focusedIdx < 0) {
        return false;
    }

    WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];

    // Menu bar accelerators take priority over widget accelerators
    if (win->menuBar) {
        for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
            if (win->menuBar->menus[i]->accelKey == key) {
                openPopupAtMenu(ctx, win, i);
                return true;
            }
        }
    }

    // Check widget tree
    if (win->widgetRoot) {
        WidgetT *target = widgetFindByAccel(win->widgetRoot, key);

        if (target && target->wclass) {
            if (target->wclass->flags & WCLASS_FOCUS_FORWARD) {
                // Label/Frame: focus-forward to next focusable
                WidgetT *next = widgetFindNextFocusable(win->widgetRoot, target);

                if (next) {
                    closeOpenPopup(ctx);

                    // A blur/focus callback may destroy widgets -- next is
                    // dangling then and must not be touched.
                    if (widgetTransferFocus(next)) {
                        wclsOnAccelActivate(next, win->widgetRoot);
                    }
                }
            } else if (wclsHas(target, WGT_METHOD_ON_ACCEL_ACTIVATE)) {
                closeOpenPopup(ctx);

                // A blur/focus callback may destroy widgets -- target is
                // dangling then and must not be touched.
                if (widgetTransferFocus(target)) {
                    wclsOnAccelActivate(target, win->widgetRoot);
                }
            }

            return true;
        }
    }

    return false;
}


// ============================================================
// dispatchEvents
// ============================================================
//
// Central event dispatcher, called once per frame after polling. Handles
// mouse and input state changes in a priority chain:
//   1. Cursor position changes (always dirty old+new positions)
//   2. Active drag/resize/scroll operations (exclusive capture)
//   3. System menu interaction
//   4. Popup menu interaction (with cascading submenu support)
//   5. Left-button press (window chrome hit testing and content delivery)
//   6. Right-button press (context menu support)
//   7. Button release and mouse-move events to focused window
//
// The priority chain means that once a drag is active, all mouse events
// go to the drag handler until the button is released. This is simpler
// and more robust than a general-purpose event capture mechanism.

static void dispatchEvents(AppContextT *ctx) {
    int32_t mx      = ctx->mouseX;
    int32_t my      = ctx->mouseY;
    int32_t buttons = ctx->mouseButtons;
    int32_t prevBtn = ctx->prevMouseButtons;

    // Self-heal a stale modal pointer.  A dialog's prevModal save/restore
    // can resurrect a modal window that was hidden or queued for destroy
    // while the dialog's nested loop ran; gating input on a window that
    // hit-testing can never reach would silently swallow every click.
    if (ctx->modalWindow && (!ctx->modalWindow->visible || ctx->modalWindow->destroyPending)) {
        ctx->modalWindow = NULL;
    }

    // Mouse movement always dirties old and new cursor positions
    if (mx != ctx->prevMouseX || my != ctx->prevMouseY) {
        dirtyCursorArea(ctx, ctx->prevMouseX, ctx->prevMouseY);
        dirtyCursorArea(ctx, mx, my);

        // Update cursor shape based on what the mouse is hovering over
        updateCursorShape(ctx);
    }

    // Handle active drag
    if (ctx->stack.dragWindow >= 0) {
        if (buttons & MOUSE_LEFT) {
            wmDragMove(&ctx->stack, &ctx->dirty, mx, my, ctx->display.width, ctx->display.height);
        } else {
            wmDragEnd(&ctx->stack);
        }
        return;
    }

    // Handle active resize
    if (ctx->stack.resizeWindow >= 0) {
        if (buttons & MOUSE_LEFT) {
            int32_t clampX = mx;
            int32_t clampY = my;
            wmResizeMove(&ctx->stack, &ctx->dirty, &ctx->display, &clampX, &clampY);

            if (clampX != mx || clampY != my) {
                platformMouseWarp(clampX, clampY);
                ctx->mouseX = clampX;
                ctx->mouseY = clampY;
            }
        } else {
            // Resize drag ended.  Call onPaint one final time with
            // resizeWindow == -1 so the app can do deferred work
            // (e.g. image viewer rescale).
            WindowT *rWin = ctx->stack.windows[ctx->stack.resizeWindow];
            wmResizeEnd(&ctx->stack);

            if (rWin->onPaint) {
                RectT fullRect = {0, 0, rWin->contentW, rWin->contentH};
                WIN_CALLBACK(ctx, rWin, rWin->onPaint(rWin, &fullRect));
                rWin->iconNeedsRefresh = true;
            }

            dirtyListAdd(&ctx->dirty, rWin->x, rWin->y, rWin->w, rWin->h);
        }
        return;
    }

    // Handle active scrollbar thumb drag
    if (ctx->stack.scrollWindow >= 0) {
        if (buttons & MOUSE_LEFT) {
            wmScrollbarDrag(&ctx->stack, &ctx->dirty, mx, my);
        } else {
            wmScrollbarEnd(&ctx->stack);
        }
        return;
    }

    // Handle system menu interaction
    if (ctx->sysMenu.active) {
        if (mx >= ctx->sysMenu.popupX && mx < ctx->sysMenu.popupX + ctx->sysMenu.popupW &&
            my >= ctx->sysMenu.popupY && my < ctx->sysMenu.popupY + ctx->sysMenu.popupH) {

            // Hover tracking
            int32_t relY    = my - ctx->sysMenu.popupY - POPUP_BEVEL_WIDTH;
            int32_t itemIdx = (relY >= 0) ? (int32_t)((uint32_t)relY * ctx->charHeightRecip >> 16) : 0;

            if (itemIdx >= 0 && itemIdx < ctx->sysMenu.itemCount && ctx->sysMenu.items[itemIdx].separator) {
                itemIdx = -1;
            }

            if (itemIdx != ctx->sysMenu.hoverItem) {
                ctx->sysMenu.hoverItem = itemIdx;
                dirtyListAdd(&ctx->dirty, ctx->sysMenu.popupX, ctx->sysMenu.popupY, ctx->sysMenu.popupW, ctx->sysMenu.popupH);
            }

            // Click on item
            if ((buttons & MOUSE_LEFT) && !(prevBtn & MOUSE_LEFT)) {
                if (itemIdx >= 0 && itemIdx < ctx->sysMenu.itemCount) {
                    SysMenuItemT *item = &ctx->sysMenu.items[itemIdx];

                    if (item->enabled && !item->separator) {
                        // Consume the press and swallow the matching release.
                        // executeSysMenuCmd may pump a nested dvxUpdate loop
                        // (Close confirm dialog, Screenshot file dialog);
                        // without this the still-held button is re-seen as a
                        // fresh MouseDown by the nested loop and the stray
                        // release fires on whatever sits under the (now-closed)
                        // sys menu.  Mirrors the popup-menu click path.
                        consumeMousePress(ctx);
                        executeSysMenuCmd(ctx, item->cmd);
                    }
                }
            }

            return;
        }

        // Click outside system menu  -- close it, let event fall through
        if ((buttons & MOUSE_LEFT) && !(prevBtn & MOUSE_LEFT)) {
            closeSysMenu(ctx);
        }
    }

    // Handle popup menu interaction (with cascading submenu support).
    // Popup menus form a stack (parentStack) with the deepest submenu as
    // "current". Hit testing checks the current level first. If the mouse
    // is in a parent level instead, all deeper levels are closed (popped)
    // so the user can navigate back up the submenu tree by moving the
    // mouse to a parent menu. This matches Win3.x cascading menu behavior.
    if (ctx->popup.active) {
        bool inCurrent = (mx >= ctx->popup.popupX && mx < ctx->popup.popupX + ctx->popup.popupW &&
                          my >= ctx->popup.popupY && my < ctx->popup.popupY + ctx->popup.popupH);

        if (inCurrent) {
            // Hover tracking
            int32_t itemIdx = popupItemAt(ctx, my);

            if (itemIdx != ctx->popup.hoverItem) {
                ctx->popup.hoverItem = itemIdx;
                dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
                             ctx->popup.popupW, ctx->popup.popupH);

                // If hovering a submenu item, open the submenu
                if (ctx->popup.menu && itemIdx >= 0 && itemIdx < ctx->popup.menu->itemCount) {
                    MenuItemT *hItem = &ctx->popup.menu->items[itemIdx];

                    if (hItem->subMenu && hItem->enabled) {
                        openSubMenu(ctx);
                    }
                }
            }

            // Click on item in current level.  The hover block above may
            // have opened a submenu, replacing ctx->popup.menu AND its
            // geometry, so containment and item index must be re-evaluated
            // against the CURRENT popup -- reusing the stale itemIdx would
            // execute whatever command sits at that row in the new submenu.
            if ((buttons & MOUSE_LEFT) && !(prevBtn & MOUSE_LEFT)) {
                bool clickIn = (mx >= ctx->popup.popupX && mx < ctx->popup.popupX + ctx->popup.popupW &&
                                my >= ctx->popup.popupY && my < ctx->popup.popupY + ctx->popup.popupH);

                if (!clickIn) {
                    // The press physically landed outside the (new)
                    // current popup -- route it through the same
                    // parent-level/outside handling a stale hover would
                    // get, instead of silently swallowing it.
                    popupHandleOutsideCurrent(ctx, mx, my, buttons, prevBtn);
                    return;
                }

                int32_t clickIdx = popupItemAt(ctx, my);

                if (ctx->popup.menu && clickIdx >= 0 && clickIdx < ctx->popup.menu->itemCount) {
                    MenuItemT *item = &ctx->popup.menu->items[clickIdx];

                    if (item->subMenu && item->enabled) {
                        // Clicking a submenu item opens it (already open from hover, but ensure)
                        openSubMenu(ctx);
                    } else if (item->enabled && !item->separator) {
                        // Consume the press so that if the menu handler
                        // re-enters the event loop (CreateForm/Show, modal
                        // dialog) the still-held button isn't re-dispatched
                        // as a fresh MouseDown to whatever widget is under
                        // the (now-closed) menu dropdown.  The matching
                        // release is also swallowed via suppressNextMouseUp.
                        consumeMousePress(ctx);
                        activatePopupItem(ctx, clickIdx);
                    }
                }
            }
        } else {
            popupHandleOutsideCurrent(ctx, mx, my, buttons, prevBtn);
        }

        return;
    }

    // Block all input while busy
    if (ctx->busy) {
        return;
    }

    // Handle left button press. Consume the edge immediately so that if
    // the click handler calls back into the event loop (e.g., BASIC's
    // DoEvents yielding while a QueryUnload handler runs), the same
    // physical click isn't re-seen as a fresh press.
    if ((buttons & MOUSE_LEFT) && !(prevBtn & MOUSE_LEFT)) {
        ctx->prevMouseButtons |= MOUSE_LEFT;
        handleMouseButton(ctx, mx, my, buttons);
    }

    // Handle right button press  -- context menus.
    // Context menu resolution walks UP the widget tree from the hit widget
    // to find the nearest ancestor with a contextMenu set, then falls back
    // to the window-level context menu. This lets containers provide menus
    // that apply to all their children without requiring each child to have
    // its own menu, while still allowing per-widget overrides.
    if ((buttons & MOUSE_RIGHT) && !(prevBtn & MOUSE_RIGHT)) {
        ctx->prevMouseButtons |= MOUSE_RIGHT;
        int32_t hitPart;
        int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart);

        if (hitIdx >= 0 && hitPart == HIT_CONTENT) {
            WindowT *win = ctx->stack.windows[hitIdx];

            // A modal dialog captures all input: a right-click on another
            // window must not raise/focus it or open its context menu.
            if (ctx->modalWindow && win != ctx->modalWindow) {
                return;
            }

            if (hitIdx != ctx->stack.focusedIdx) {
                wmRaiseWindow(&ctx->stack, &ctx->dirty, hitIdx);
                hitIdx = ctx->stack.count - 1;
                wmSetFocus(&ctx->stack, &ctx->dirty, hitIdx);
                win = ctx->stack.windows[hitIdx];
            }

            // Dispatch right-click to the widget system for MouseDown events
            if (win->onMouse) {
                int32_t relX = mx - win->x - win->contentX;
                int32_t relY = my - win->y - win->contentY;
                WIN_CALLBACK(ctx, win, win->onMouse(win, relX, relY, buttons));
            }

            // Then check for context menus
            MenuT *ctxMenu = NULL;

            if (win->widgetRoot) {
                int32_t relX = mx - win->x - win->contentX;
                int32_t relY = my - win->y - win->contentY;
                WidgetT *hit = widgetHitTest(win->widgetRoot, relX, relY);

                while (hit && !hit->contextMenu) {
                    hit = hit->parent;
                }

                if (hit) {
                    ctxMenu = hit->contextMenu;
                }
            }

            if (!ctxMenu) {
                ctxMenu = win->contextMenu;
            }

            if (ctxMenu) {
                openContextMenu(ctx, win, ctxMenu, mx, my);
            }
        }
    }

    // Handle button release on content  -- send to focused window.
    // Skip if a menu click consumed this press (prevents the stray
    // release from firing as a click on whatever widget sits under
    // where the menu dropdown was).
    if (!(buttons & MOUSE_LEFT) && (prevBtn & MOUSE_LEFT)) {
        if (ctx->suppressNextMouseUp) {
            ctx->suppressNextMouseUp = false;
        } else if (ctx->stack.focusedIdx >= 0) {
            WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];

            if (win->onMouse) {
                int32_t relX = mx - win->x - win->contentX;
                int32_t relY = my - win->y - win->contentY;
                WIN_CALLBACK(ctx, win, win->onMouse(win, relX, relY, buttons));
            }
        }
    }

    // Deliver right-release and middle press/release transitions to the
    // window under the mouse (right-press is handled above with context
    // menus, left-press/release and movement elsewhere).  Middle presses
    // never raise or refocus a window, so the focused window cannot be
    // assumed -- hit-test like the press paths do, or an occluded widget
    // in the focused window would receive a click that visually landed on
    // the overlapping window.  The widget layer detects the specific
    // button edge from its own prevMouseButtons, so a bare dispatch on
    // the transition is enough for onMouseDown/onMouseUp(2/3).
    bool rightRelease = !(buttons & MOUSE_RIGHT) && (prevBtn & MOUSE_RIGHT);
    bool middleChange = ((buttons & MOUSE_MIDDLE) != (prevBtn & MOUSE_MIDDLE));

    if (rightRelease || middleChange) {
        int32_t hitPart;
        int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart);

        if (hitIdx >= 0 && hitPart == HIT_CONTENT) {
            WindowT *win = ctx->stack.windows[hitIdx];

            // A modal dialog captures all input.
            if ((!ctx->modalWindow || win == ctx->modalWindow) && win->onMouse) {
                int32_t relX = mx - win->x - win->contentX;
                int32_t relY = my - win->y - win->contentY;
                WIN_CALLBACK(ctx, win, win->onMouse(win, relX, relY, buttons));
            }
        }
    }

    // Mouse movement in content area  -- send to focused window
    if ((mx != ctx->prevMouseX || my != ctx->prevMouseY) &&
        ctx->stack.focusedIdx >= 0) {
        WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];

        if (win->onMouse) {
            int32_t relX = mx - win->x - win->contentX;
            int32_t relY = my - win->y - win->contentY;

            WIN_CALLBACK(ctx, win, win->onMouse(win, relX, relY, buttons));
        }
    }

    // Mouse wheel  -- scroll the focused window.  First try the focused
    // widget's key handler with Up/Down arrows (which scrollable widgets
    // like ListBox, TextArea, TreeView handle natively).  Fall back to
    // WM-level window scrollbars if no widget consumes it.
    if (ctx->mouseWheel != 0 && ctx->stack.focusedIdx >= 0) {
        WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];

        // If a scrollable widget has focus, forward wheel to it.
        // Otherwise fall through to WM-level window scrollbars.
        bool consumed = false;

        if (win->widgetRoot) {
            WidgetT *focus = wgtGetFocused();

            if (focus && wclsHas(focus, WGT_METHOD_ON_KEY)) {
                // Find the scrollable target: either the focused widget
                // itself or a ScrollPane ancestor that contains it.
                WidgetT *target = NULL;

                if (focus->wclass && (focus->wclass->flags & WCLASS_SCROLLABLE)) {
                    target = focus;
                } else {
                    // Walk up to find a ScrollPane ancestor
                    for (WidgetT *p = focus->parent; p; p = p->parent) {
                        if (p->wclass && (p->wclass->flags & WCLASS_SCROLL_CONTAINER)) {
                            target = p;
                            break;
                        }
                    }
                }

                if (target && wclsHas(target, WGT_METHOD_ON_KEY)) {
                    int32_t  delta    = ctx->mouseWheel * ctx->wheelDirection;
                    int32_t  arrowKey = (delta > 0) ? KEY_DOWN : KEY_UP;
                    int32_t  steps    = abs(delta) * ctx->wheelStep;
                    uint32_t gen      = sWidgetGen;

                    for (int32_t s = 0; s < steps; s++) {
                        wclsOnKey(target, arrowKey, 0);

                        // A key handler may fire a user Change callback that
                        // destroys widgets -- target is dangling then.
                        if (sWidgetGen != gen) {
                            break;
                        }
                    }

                    // Ensure the window repaints even if the widget's
                    // onKey didn't invalidate (belt and suspenders)
                    if (sWidgetGen == gen) {
                        wgtInvalidatePaint(target);
                    }

                    consumed = true;
                }
            }

            // Fire onScroll callback on the focused widget.  Re-fetch the
            // pointer: the key dispatch above may have destroyed the widget
            // captured before the loop (a Change handler unloading its
            // form), leaving the earlier pointer dangling.
            focus = wgtGetFocused();

            if (focus && focus->onScroll) {
                WIN_CALLBACK(ctx, win, focus->onScroll(focus, ctx->mouseWheel * ctx->wheelDirection));
            }
        }

        ScrollbarT *sb = !consumed ? (win->vScroll ? win->vScroll : win->hScroll) : NULL;

        if (sb) {
            int32_t oldValue = sb->value;
            sb->value += ctx->mouseWheel * ctx->wheelDirection * ctx->wheelStep;

            if (sb->value < sb->min) {
                sb->value = sb->min;
            }

            if (sb->value > sb->max) {
                sb->value = sb->max;
            }

            if (sb->value != oldValue) {
                int32_t sbScreenX = win->x + sb->x;
                int32_t sbScreenY = win->y + sb->y;
                dirtyListAdd(&ctx->dirty, sbScreenX, sbScreenY,
                             sb->orient == ScrollbarVerticalE ? SCROLLBAR_WIDTH : sb->length,
                             sb->orient == ScrollbarVerticalE ? sb->length : SCROLLBAR_WIDTH);

                if (win->onScroll) {
                    WIN_CALLBACK(ctx, win, win->onScroll(win, sb->orient, sb->value));
                }
            }
        }
    }
}


// ============================================================
// ditherRgb  -- apply Bayer ordered dithering to an RGB triplet
// ============================================================

static void ditherRgb(int32_t *r, int32_t *g, int32_t *b, int32_t row, int32_t col) {
    int32_t d = sBayerMatrix[row & 3][col & 3];
    *r += d;
    *g += d;
    *b += d;

    if (*r < 0) {
        *r = 0;
    } else if (*r > 255) {
        *r = 255;
    }

    if (*g < 0) {
        *g = 0;
    } else if (*g > 255) {
        *g = 255;
    }

    if (*b < 0) {
        *b = 0;
    } else if (*b > 255) {
        *b = 255;
    }
}


// ============================================================
// drawCursorAt
// ============================================================
//
// Software cursor rendering using AND/XOR mask pairs, the same format
// Windows uses for cursor resources. The AND mask determines transparency
// (0=opaque, 1=transparent) and the XOR mask determines color. This
// lets cursors have transparent pixels without needing alpha blending,
// which would be expensive on a 486.

static void drawCursorAt(AppContextT *ctx, int32_t x, int32_t y) {
    const CursorT *cur = &ctx->cursors[ctx->cursorId];

    drawMaskedBitmap(&ctx->display, &ctx->blitOps, x - cur->hotX, y - cur->hotY, cur->width, cur->height, cur->andMask, cur->xorData, ctx->colors.cursorFg, ctx->colors.cursorBg);
}


// ============================================================
// drawPopupLevel  -- draw one popup menu (bevel + items)
// ============================================================
//
// Draws a single popup menu level (the compositor calls this for each
// level in the popup stack, parent-first). Each item gets:
//   - Full-width highlight bar when hovered
//   - Optional check/radio glyph in a left margin column
//   - Tab-split label: left part with underlined accelerator, right-aligned shortcut
//   - Submenu arrow (right-pointing triangle) if item has a subMenu
//   - Separators drawn as horizontal lines
//
// The check margin is conditional: only present if any item in the menu
// has check or radio type, keeping non-checkable menus compact.

static void drawPopupLevel(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, const MenuT *menu, int32_t px, int32_t py, int32_t pw, int32_t ph, int32_t hoverItem, const RectT *clipTo) {
    RectT popRect = { px, py, pw, ph };
    RectT popIsect;

    if (!rectIntersect(clipTo, &popRect, &popIsect)) {
        return;
    }

    // Detect if menu has check/radio items (for left margin)
    bool hasCheck = false;

    for (int32_t k = 0; k < menu->itemCount; k++) {
        if (menu->items[k].type == MenuItemCheckE || menu->items[k].type == MenuItemRadioE) {
            hasCheck = true;
            break;
        }
    }

    int32_t checkMargin = hasCheck ? MENU_CHECK_WIDTH : 0;

    // Draw popup background
    BevelStyleT popBevel = BEVEL_POPUP(&ctx->colors, POPUP_BEVEL_WIDTH);
    drawBevel(d, ops, px, py, pw, ph, &popBevel);

    // Draw menu items
    int32_t itemY = py + POPUP_BEVEL_WIDTH;

    for (int32_t k = 0; k < menu->itemCount; k++) {
        const MenuItemT *item = &menu->items[k];

        if (item->separator) {
            drawHLine(d, ops, px + POPUP_BEVEL_WIDTH, itemY + ctx->font.charHeight / 2, pw - POPUP_BEVEL_WIDTH * 2, ctx->colors.windowShadow);
            itemY += ctx->font.charHeight;
            continue;
        }

        uint32_t bg = ctx->colors.menuBg;
        uint32_t fg = item->enabled ? ctx->colors.menuFg : ctx->colors.windowShadow;

        if (k == hoverItem && item->enabled) {
            bg = ctx->colors.menuHighlightBg;
            fg = ctx->colors.menuHighlightFg;
        }

        rectFill(d, ops, px + POPUP_BEVEL_WIDTH, itemY, pw - POPUP_BEVEL_WIDTH * 2, ctx->font.charHeight, bg);

        // Split label at tab: left part is the menu text, right part is the shortcut
        char        leftBuf[MAX_MENU_LABEL];
        const char *shortcut = splitMenuLabel(item->label, leftBuf);

        if (shortcut) {
            drawTextAccel(d, ops, &ctx->font, px + CHROME_TITLE_PAD + POPUP_BEVEL_WIDTH + checkMargin, itemY, leftBuf, fg, bg, true);

            int32_t rightW = textWidth(&ctx->font, shortcut);
            int32_t rightX = px + pw - rightW - CHROME_TITLE_PAD - POPUP_BEVEL_WIDTH * 2;

            if (item->subMenu) {
                rightX -= SUBMENU_ARROW_WIDTH;
            }

            drawText(d, ops, &ctx->font, rightX, itemY, shortcut, fg, bg, true);
        } else {
            drawTextAccel(d, ops, &ctx->font, px + CHROME_TITLE_PAD + POPUP_BEVEL_WIDTH + checkMargin, itemY, item->label, fg, bg, true);
        }

        // Draw check/radio indicator
        if (item->checked) {
            int32_t cy = itemY + ctx->font.charHeight / 2;
            int32_t cx = px + POPUP_BEVEL_WIDTH + MENU_CHECK_WIDTH / 2;

            if (item->type == MenuItemCheckE) {
                // Checkmark: small tick shape, one short vertical stroke
                // per column; the y offsets trace the tick outline.
                static const int32_t tickDx[] = {-2, -1,  0,  1,  2,  3};
                static const int32_t tickDy[] = {-1,  0,  1,  0, -1, -2};

                for (size_t k = 0; k < sizeof(tickDx) / sizeof(tickDx[0]); k++) {
                    drawVLine(d, ops, cx + tickDx[k], cy + tickDy[k], CHECK_STROKE_H, fg);
                }
            } else if (item->type == MenuItemRadioE) {
                // Filled diamond bullet
                for (int32_t row = -RADIO_DIAMOND_HALF; row <= RADIO_DIAMOND_HALF; row++) {
                    int32_t half = RADIO_DIAMOND_HALF - (row < 0 ? -row : row);

                    drawHLine(d, ops, cx - half, cy + row, half * 2 + 1, fg);
                }
            }
        }

        // Draw submenu arrow indicator
        if (item->subMenu) {
            int32_t arrowX = px + pw - SUBMENU_ARROW_WIDTH - POPUP_BEVEL_WIDTH;
            int32_t arrowY = itemY + ctx->font.charHeight / 2;

            for (int32_t row = -SUBMENU_ARROW_HALF; row <= SUBMENU_ARROW_HALF; row++) {
                int32_t len = SUBMENU_ARROW_HALF + 1 - (row < 0 ? -row : row);

                if (len > 0) {
                    drawHLine(d, ops, arrowX + (row < 0 ? SUBMENU_ARROW_HALF + row : SUBMENU_ARROW_HALF - row), arrowY + row, len, fg);
                }
            }
        }

        itemY += ctx->font.charHeight;
    }
}


// ============================================================
// enumModeCb  -- used during dvxInit to capture available modes
// ============================================================

static void enumModeCb(int32_t w, int32_t h, int32_t bpp, void *userData) {
    if (w < DVX_DEFAULT_VIDEO_W || h < DVX_DEFAULT_VIDEO_H) {
        return;
    }

    AppContextT *ctx = (AppContextT *)userData;
    VideoModeInfoT m;
    m.w   = w;
    m.h   = h;
    m.bpp = bpp;
    arrput(ctx->videoModes, m);
}


// ============================================================
// executeSysMenuCmd
// ============================================================
//
// Executes a system menu (window control menu) command. The system menu
// is the DVX equivalent of the Win3.x control-menu box  -- it
// provides Restore, Move, Size, Minimize, Maximize, and Close. Keyboard
// move/resize mode is entered by setting kbMoveResize state, which causes
// pollKeyboard to intercept arrow keys until Enter/Esc.

static void executeSysMenuCmd(AppContextT *ctx, int32_t cmd) {
    WindowT *win = findWindowById(ctx, ctx->sysMenu.windowId);

    closeSysMenu(ctx);

    if (!win) {
        return;
    }

    switch (cmd) {
        case SysMenuRestoreE:
            if (win->maximized) {
                wmRestore(&ctx->stack, &ctx->dirty, &ctx->display, win);
            }
            break;

        case SysMenuMoveE:
            ctx->kbMoveResize.mode     = KbModeMoveE;
            ctx->kbMoveResize.windowId = win->id;
            ctx->kbMoveResize.origX    = win->x;
            ctx->kbMoveResize.origY    = win->y;
            break;

        case SysMenuSizeE:
            ctx->kbMoveResize.mode     = KbModeResizeE;
            ctx->kbMoveResize.windowId = win->id;
            ctx->kbMoveResize.origX    = win->x;
            ctx->kbMoveResize.origY    = win->y;
            ctx->kbMoveResize.origW    = win->w;
            ctx->kbMoveResize.origH    = win->h;
            break;

        case SysMenuMinimizeE:
            if (ctx->modalWindow != win) {
                dvxMinimizeWindow(ctx, win);
            }
            break;

        case SysMenuMaximizeE:
            if (win->resizable && !win->maximized) {
                wmMaximize(&ctx->stack, &ctx->dirty, &ctx->display, win);
            }
            break;

        case SysMenuScreenshotE:
            // Sys menu is already closed.  Composite a clean frame first.
            compositeAndFlush(ctx);
            interactiveScreenshot(ctx);
            break;

        case SysMenuWinScreenshotE:
            interactiveWindowScreenshot(ctx, win);
            break;

        case SysMenuCloseE:
            // Sys menu is already closed.  Composite a clean frame first
            // so the menu's pixels are erased before onClose runs -- the
            // close handler may open a modal dialog and we don't want the
            // sys menu popup peeking out from under it.
            compositeAndFlush(ctx);

            if (win->onClose) {
                WIN_CALLBACK(ctx, win, win->onClose(win));
            } else {
                dvxDestroyWindow(ctx, win);
            }

            break;
    }
}


// ============================================================
// findWindowById
// ============================================================

static WindowT *findWindowById(AppContextT *ctx, int32_t id) {
    for (int32_t i = 0; i < ctx->stack.count; i++) {
        if (ctx->stack.windows[i]->id == id) {
            return ctx->stack.windows[i];
        }
    }

    return NULL;
}


// ============================================================
// finishLoadImage  -- shared tail of dvxLoadImage / dvxLoadImageFromMemory
// ============================================================
//
// Takes ownership of the stb_image RGB buffer (NULL if decoding failed)
// and returns the display-format copy, filling the optional outputs.

static uint8_t *finishLoadImage(const DisplayT *d, uint8_t *rgb, int32_t imgW, int32_t imgH, int32_t *outW, int32_t *outH, int32_t *outPitch) {
    if (!rgb) {
        return NULL;
    }

    int32_t  pitch;
    uint8_t *buf = convertRgbToNative(d, rgb, RGB_CHANNELS, imgW, imgH, 0, NULL, &pitch);

    stbi_image_free(rgb);

    if (!buf) {
        return NULL;
    }

    if (outW) {
        *outW = imgW;
    }

    if (outH) {
        *outH = imgH;
    }

    if (outPitch) {
        *outPitch = pitch;
    }

    return buf;
}


// ============================================================
// flushPendingDestroys
// ============================================================
//
// Destroys every window flagged by deferDestroyWindow.  Called once
// dispatchDepth reaches zero, i.e. no user callback is on the stack any
// more, so freeing widget memory is safe.  Scans the stack for the
// destroyPending flag (the single source of truth) rather than keeping a
// parallel queue: destroying a window compacts the stack, so the scan
// restarts after each destroy.  Self-healing against any hint-counter
// desync (e.g. after crash recovery).

static void flushPendingDestroys(AppContextT *ctx) {
    bool found = true;

    while (found) {
        found = false;

        for (int32_t i = ctx->stack.count - 1; i >= 0; i--) {
            if (ctx->stack.windows[i]->destroyPending) {
                dvxDestroyWindow(ctx, ctx->stack.windows[i]);
                found = true;
                break;
            }
        }
    }

    ctx->pendingDestroyCount = 0;
}


// Moves window-manager focus to the topmost window not queued for
// destruction.  Shared by the immediate (destroyWindowSync) and deferred
// (deferDestroyWindow) teardown paths so the survivor-selection rule lives
// in one place.
static void focusTopmostAlive(AppContextT *ctx) {
    for (int32_t i = ctx->stack.count - 1; i >= 0; i--) {
        if (!ctx->stack.windows[i]->destroyPending && ctx->stack.windows[i]->visible) {
            wmSetFocus(&ctx->stack, &ctx->dirty, i);
            break;
        }
    }
}


// ============================================================
// handleMouseButton
// ============================================================
//
// Handles a left-button press that is not consumed by a drag, popup, or
// system menu. Uses wmHitTest to determine what part of what window was
// clicked:
//   hitPart 0: content area (forwarded to window's onMouse callback)
//   hitPart 1: title bar (begins mouse drag)
//   hitPart 2: close/sys-menu gadget (single-click opens sys menu,
//              double-click closes  -- DVX convention)
//   hitPart 3: resize border (begins edge/corner resize)
//   hitPart 4: menu bar (opens popup for clicked menu)
//   hitPart 5/6: vertical/horizontal scrollbar
//   hitPart 7: minimize button
//   hitPart 8: maximize/restore button
//
// Windows are raised-and-focused on click regardless of which part was
// hit, ensuring the clicked window always comes to the front.

static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t buttons) {
    int32_t hitPart;
    int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart);

    // Modal window gating: only the modal window receives clicks
    if (ctx->modalWindow) {
        // A modal dialog captures all input: block clicks on any other
        // window AND desktop/minimized-icon clicks (hitIdx < 0), so a
        // minimized-icon restore can't raise a window over the modal.
        if (hitIdx < 0 || ctx->stack.windows[hitIdx] != ctx->modalWindow) {
            return;
        }
    }

    // Check for click on minimized icon, but only if no window covers the point.
    // Icons are drawn below windows, so a window at this position takes priority.
    int32_t iconIdx = -1;

    if (hitIdx < 0) {
        iconIdx = wmMinimizedIconHit(&ctx->stack, &ctx->display, mx, my);
    }

    if (iconIdx >= 0) {
        WindowT *iconWin = ctx->stack.windows[iconIdx];
        clock_t  now     = clock();

        if (ctx->lastIconClickId == iconWin->id &&
            (now - ctx->lastIconClickTime) < ctx->dblClickTicks) {
            // Double-click: restore minimized window
            wmRestoreMinimized(&ctx->stack, &ctx->dirty, &ctx->display, iconWin);
            ctx->lastIconClickId = -1;
        } else {
            // First click  -- record for double-click detection
            ctx->lastIconClickTime = now;
            ctx->lastIconClickId   = iconWin->id;
        }

        return;
    }

    if (hitIdx < 0) {
        return; // clicked on desktop
    }

    WindowT *win = ctx->stack.windows[hitIdx];

    // Raise and focus if not already
    if (hitIdx != ctx->stack.focusedIdx) {
        wmRaiseWindow(&ctx->stack, &ctx->dirty, hitIdx);
        // After raise, the window is now at count-1
        hitIdx = ctx->stack.count - 1;
        wmSetFocus(&ctx->stack, &ctx->dirty, hitIdx);
    }

    // Clear widget focus for non-content clicks (scrollbars, title bar, etc.)
    // Content clicks clear focus inside widgetOnMouse instead.  Selection is
    // intentionally kept: chrome clicks are transient (focus auto-returns on
    // the next repaint) and menu commands like Edit > Copy need the selection
    // to survive the click that opened the menu.
    if (hitPart != HIT_CONTENT && sFocusedWidget) {
        WidgetT *prev = sFocusedWidget;

        sFocusedWidget = NULL;
        wgtInvalidatePaint(prev);
        widgetFireFocusChange(prev, NULL);
    }

    switch (hitPart) {
        case HIT_CONTENT:
            if (win->onMouse) {
                int32_t relX = mx - win->x - win->contentX;
                int32_t relY = my - win->y - win->contentY;
                WIN_CALLBACK(ctx, win, win->onMouse(win, relX, relY, buttons));
            }
            break;

        case HIT_TITLE:
            {
                clock_t now = clock();

                if (win->resizable &&
                    ctx->lastTitleClickId == win->id &&
                    (now - ctx->lastTitleClickTime) < ctx->dblClickTicks) {
                    // Double-click: toggle maximize/restore
                    ctx->lastTitleClickId = -1;

                    if (win->maximized) {
                        wmRestore(&ctx->stack, &ctx->dirty, &ctx->display, win);
                    } else {
                        wmMaximize(&ctx->stack, &ctx->dirty, &ctx->display, win);
                    }
                } else {
                    ctx->lastTitleClickTime = now;
                    ctx->lastTitleClickId   = win->id;
                    wmDragBegin(&ctx->stack, hitIdx, mx, my);
                }
            }
            break;

        case HIT_CLOSE:
            {
                clock_t now = clock();

                if (ctx->lastCloseClickId == win->id &&
                    (now - ctx->lastCloseClickTime) < ctx->dblClickTicks) {
                    ctx->lastCloseClickId = -1;
                    closeSysMenu(ctx);

                    // Composite a clean frame first so the sys menu is
                    // fully gone before onClose runs -- the close handler
                    // may open a modal dialog and we don't want the menu
                    // peeking out from under it.
                    compositeAndFlush(ctx);

                    if (win->onClose) {
                        WIN_CALLBACK(ctx, win, win->onClose(win));
                    } else {
                        dvxDestroyWindow(ctx, win);
                    }

                    // Ensure sys menu is closed even if onClose re-opened it
                    closeSysMenu(ctx);
                } else {
                    ctx->lastCloseClickTime = now;
                    ctx->lastCloseClickId   = win->id;
                    openSysMenu(ctx, win);
                }
            }
            break;

        case HIT_RESIZE:
            {
                int32_t edge = wmResizeEdgeHit(win, mx, my);
                wmResizeBegin(&ctx->stack, hitIdx, edge, mx, my);
            }
            break;

        case HIT_MENU:
            {
                if (!win->menuBar) {
                    break;
                }

                int32_t relX = mx - win->x;

                for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
                    MenuT *menu = win->menuBar->menus[i];

                    if (relX >= menu->barX && relX < menu->barX + menu->barW) {
                        openPopupAtMenu(ctx, win, i);
                        break;
                    }
                }
            }
            break;

        case HIT_VSCROLL:
            wmScrollbarClick(&ctx->stack, &ctx->dirty, hitIdx, ScrollbarVerticalE, mx, my);
            break;

        case HIT_HSCROLL:
            wmScrollbarClick(&ctx->stack, &ctx->dirty, hitIdx, ScrollbarHorizontalE, mx, my);
            break;

        case HIT_MINIMIZE:
            if (ctx->modalWindow != win) {
                dvxMinimizeWindow(ctx, win);
            }
            break;

        case HIT_MAXIMIZE:
            if (win->maximized) {
                wmRestore(&ctx->stack, &ctx->dirty, &ctx->display, win);
            } else {
                wmMaximize(&ctx->stack, &ctx->dirty, &ctx->display, win);
            }
            break;
    }
}


// ============================================================
// initColorScheme
// ============================================================
//
// Colors are pre-packed to native pixel format at init time so no
// per-pixel conversion is needed during drawing. The scheme is inspired
// by GEOS Ensemble with Motif-style 3D bevels: teal desktop, grey window
// chrome with white highlights and dark shadows to create the raised/sunken
// illusion. The dark charcoal active title bar distinguishes it from
// GEOS's blue, giving DVX its own identity.

static void initColorScheme(AppContextT *ctx) {
    // Load defaults into the RGB source array, then pack all at once
    memcpy(ctx->colorRgb, sDefaultColors, sizeof(sDefaultColors));
    dvxApplyColorScheme(ctx);
}


// ============================================================
// interactiveScreenshot  -- snapshot screen, prompt for save path
// ============================================================

// Snapshot a pixel buffer, prompt for a save path, and write it.  Shared by
// the full-screen and per-window interactive screenshot commands.
static void interactiveSaveBuffer(AppContextT *ctx, const char *title, const uint8_t *src, int32_t w, int32_t h, int32_t pitch) {
    int32_t  size = pitch * h;
    uint8_t *snap = (uint8_t *)malloc(size);

    if (snap) {
        FileFilterT filters[] = {
            { "PNG Images (*.png)" },
            { "BMP Images (*.bmp)" }
        };
        char path[DVX_MAX_PATH];

        memcpy(snap, src, size);

        if (dvxFileDialog(ctx, title, FD_SAVE, NULL, filters, 2, path, sizeof(path))) {
            dvxSetBusy(ctx, true);
            dvxSaveImage(ctx, snap, w, h, pitch, path);
            dvxSetBusy(ctx, false);
        }

        free(snap);
    }
}


static void interactiveScreenshot(AppContextT *ctx) {
    interactiveSaveBuffer(ctx, "Save Screenshot", ctx->display.backBuf, ctx->display.width, ctx->display.height, ctx->display.pitch);
}


// ============================================================
// interactiveWindowScreenshot  -- snapshot window content, prompt for save path
// ============================================================

static void interactiveWindowScreenshot(AppContextT *ctx, WindowT *win) {
    if (!win || !win->contentBuf) {
        return;
    }

    interactiveSaveBuffer(ctx, "Save Window Screenshot", win->contentBuf, win->contentW, win->contentH, win->contentPitch);
}


// ============================================================
// invalidateAllWindows  -- repaint all windows and desktop
// ============================================================

static void invalidateAllWindows(AppContextT *ctx) {
    for (int32_t i = 0; i < ctx->stack.count; i++) {
        dvxInvalidateWindow(ctx, ctx->stack.windows[i]);
    }

    dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height);
}


// ============================================================
// nextMenuItem  -- step a menu hover index past separators
// ============================================================
//
// firstSeparator points at the separator flag of item 0; stride is the
// item size, so this serves both SysMenuItemT and MenuItemT arrays.
// Steps idx by dir (+1/-1) with wraparound, skipping separators; gives
// up after count steps so an all-separator menu cannot loop forever.

static int32_t nextMenuItem(const bool *firstSeparator, size_t stride, int32_t count, int32_t idx, int32_t dir) {
    for (int32_t tries = 0; tries < count; tries++) {
        idx += dir;

        if (idx < 0) {
            idx = count - 1;
        } else if (idx >= count) {
            idx = 0;
        }

        if (!*(const bool *)((const uint8_t *)firstSeparator + (size_t)idx * stride)) {
            break;
        }
    }

    return idx;
}


// ============================================================
// openContextMenu  -- open a context menu at a screen position
// ============================================================
//
// Context menus reuse the same popup system as menu bar popups but with
// isContextMenu=true. The difference affects dismiss behavior: context
// menus close on any click outside (since there's no menu bar to switch
// to), while menu bar popups allow horizontal mouse movement to switch
// between top-level menus. Position is clamped to screen edges so the
// popup doesn't go off-screen.

void dvxShowContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t screenX, int32_t screenY) {
    openContextMenu(ctx, win, menu, screenX, screenY);
}


static void openContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t screenX, int32_t screenY) {
    if (!menu || menu->itemCount <= 0) {
        return;
    }

    closeAllPopups(ctx);
    closeSysMenu(ctx);

    ctx->popup.active        = true;
    ctx->popup.isContextMenu = true;
    ctx->popup.windowId      = win->id;
    ctx->popup.menuIdx       = -1;
    ctx->popup.menu          = menu;
    ctx->popup.hoverItem     = -1;
    ctx->popup.depth         = 0;

    calcPopupSize(ctx, menu, &ctx->popup.popupW, &ctx->popup.popupH);

    // Position at mouse, clamped to screen
    ctx->popup.popupX = screenX;
    ctx->popup.popupY = screenY;
    clampPopupToScreen(ctx);

    dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
                 ctx->popup.popupW, ctx->popup.popupH);
}


// ============================================================
// openPopupAtMenu  -- open top-level popup for a menu bar menu
// ============================================================
//
// Opens the dropdown for a menu bar item (e.g., "File", "Edit"). Any
// existing popup chain is closed first, then a new top-level popup is
// positioned directly below the menu bar item, aligned with its barX
// coordinate. This is called both from mouse clicks on the menu bar
// and from keyboard navigation (Alt+key, Left/Right arrows).

static void openPopupAtMenu(AppContextT *ctx, WindowT *win, int32_t menuIdx) {
    if (!win->menuBar || menuIdx < 0 || menuIdx >= win->menuBar->menuCount) {
        return;
    }

    // Close any existing popup chain first
    closeAllPopups(ctx);

    MenuT *menu = win->menuBar->menus[menuIdx];

    ctx->popup.active        = true;
    ctx->popup.isContextMenu = false;
    ctx->popup.windowId      = win->id;
    ctx->popup.menuIdx       = menuIdx;
    ctx->popup.menu          = menu;
    ctx->popup.popupX        = win->x + menu->barX;
    ctx->popup.popupY        = win->y + CHROME_TITLEBAR_BOTTOM + CHROME_MENU_HEIGHT;

    // Mark the menu bar item as active (depressed look)
    win->menuBar->activeIdx = menuIdx;
    dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, CHROME_TITLEBAR_BOTTOM + CHROME_MENU_HEIGHT);
    ctx->popup.hoverItem     = -1;
    ctx->popup.depth         = 0;

    calcPopupSize(ctx, menu, &ctx->popup.popupW, &ctx->popup.popupH);
    clampPopupToScreen(ctx);

    dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
                 ctx->popup.popupW, ctx->popup.popupH);
}


// ============================================================
// openSubMenu  -- open submenu for the currently hovered item
// ============================================================
//
// Pushes the current popup state onto parentStack and opens the submenu
// as the new current level. The submenu is positioned at the right edge
// of the current popup, vertically aligned with the hovered item.
static void openSubMenu(AppContextT *ctx) {
    if (!ctx->popup.active || !ctx->popup.menu) {
        return;
    }

    int32_t idx = ctx->popup.hoverItem;

    if (idx < 0 || idx >= ctx->popup.menu->itemCount) {
        return;
    }

    MenuItemT *item = &ctx->popup.menu->items[idx];

    if (!item->subMenu) {
        return;
    }

    // Grow parent stack if needed
    if (ctx->popup.depth >= ctx->popup.parentCap) {
        int32_t newCap = ctx->popup.parentCap ? ctx->popup.parentCap * 2 : 4;
        PopupLevelT *newBuf = (PopupLevelT *)realloc(ctx->popup.parentStack, newCap * sizeof(PopupLevelT));

        if (!newBuf) {
            return;
        }

        ctx->popup.parentStack = newBuf;
        ctx->popup.parentCap   = newCap;
    }

    // Push current state to parent stack
    PopupLevelT *pl = &ctx->popup.parentStack[ctx->popup.depth];
    pl->menu      = ctx->popup.menu;
    pl->menuIdx   = ctx->popup.menuIdx;
    pl->popupX    = ctx->popup.popupX;
    pl->popupY    = ctx->popup.popupY;
    pl->popupW    = ctx->popup.popupW;
    pl->popupH    = ctx->popup.popupH;
    pl->hoverItem = ctx->popup.hoverItem;
    ctx->popup.depth++;

    // Open submenu at right edge of current popup, aligned with hovered item
    ctx->popup.menu      = item->subMenu;
    ctx->popup.popupX    = pl->popupX + pl->popupW - POPUP_BEVEL_WIDTH;
    ctx->popup.popupY    = pl->popupY + POPUP_BEVEL_WIDTH + idx * ctx->font.charHeight;
    ctx->popup.hoverItem = -1;

    calcPopupSize(ctx, ctx->popup.menu, &ctx->popup.popupW, &ctx->popup.popupH);
    clampPopupToScreen(ctx);

    dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
                 ctx->popup.popupW, ctx->popup.popupH);
}


// ============================================================
// openSysMenu
// ============================================================
//
// The system menu is a separate popup from the regular menu system
// because it has different semantics: it's tied to the window's close
// gadget (top-left icon), uses its own SysMenuItemT type with
// enabled/disabled state, and dispatches to executeSysMenuCmd rather
// than the window's onMenu callback. Items are dynamically enabled
// based on window state (e.g., Restore is only enabled when maximized,
// Size is disabled when maximized or non-resizable). Triggered by
// single-click on the close gadget or Alt+Space.

static void openSysMenu(AppContextT *ctx, WindowT *win) {
    closeAllPopups(ctx);

    if (ctx->sysMenu.active) {
        closeSysMenu(ctx);
        return;
    }

    ctx->sysMenu.itemCount = 0;
    ctx->sysMenu.windowId  = win->id;
    // Restore  -- enabled only when maximized
    addSysMenuItem(ctx, "&Restore", SysMenuRestoreE, win->maximized);

    // Move  -- disabled when maximized
    addSysMenuItem(ctx, "&Move", SysMenuMoveE, !win->maximized);

    // Size  -- only if resizable and not maximized
    addSysMenuItem(ctx, "&Size", SysMenuSizeE, win->resizable && !win->maximized);

    // Minimize  -- not available on modal windows
    addSysMenuItem(ctx, "Mi&nimize", SysMenuMinimizeE, !win->modal);

    // Maximize  -- only if resizable and not maximized
    addSysMenuItem(ctx, "Ma&ximize", SysMenuMaximizeE, win->resizable && !win->maximized);

    // Separator
    addSysMenuSeparator(ctx);

    // Screenshot (full screen)
    addSysMenuItem(ctx, "Scree&nshot...", SysMenuScreenshotE, true);

    // Screenshot (this window)
    addSysMenuItem(ctx, "&Window Shot...", SysMenuWinScreenshotE, true);

    // Separator
    addSysMenuSeparator(ctx);

    // Close
    addSysMenuItem(ctx, "&Close", SysMenuCloseE, true);

    // Compute popup geometry  -- position below the close gadget
    ctx->sysMenu.popupX = win->x + CHROME_BORDER_WIDTH;
    ctx->sysMenu.popupY = win->y + CHROME_TITLEBAR_BOTTOM;

    int32_t maxW = 0;

    for (int32_t i = 0; i < ctx->sysMenu.itemCount; i++) {
        if (!ctx->sysMenu.items[i].separator) {
            int32_t w = textWidthAccel(&ctx->font, ctx->sysMenu.items[i].label);

            if (w > maxW) {
                maxW = w;
            }
        }
    }

    ctx->sysMenu.popupW    = maxW + CHROME_TITLE_PAD * 2 + POPUP_ITEM_PAD_H;
    ctx->sysMenu.popupH    = ctx->sysMenu.itemCount * ctx->font.charHeight + POPUP_BEVEL_WIDTH * 2;
    ctx->sysMenu.hoverItem = -1;
    ctx->sysMenu.active    = true;

    dirtyListAdd(&ctx->dirty, ctx->sysMenu.popupX, ctx->sysMenu.popupY, ctx->sysMenu.popupW, ctx->sysMenu.popupH);
}


// ============================================================
// pollKeyboard
// ============================================================
//
// Drains the keyboard buffer and dispatches each key through a priority
// chain. The priority order is important  -- higher priority handlers
// consume the key and skip lower ones via 'continue':
//
//   1. Alt+Tab / Shift+Alt+Tab  -- window cycling (always works)
//   2. Alt+F4  -- close focused window
//   3. Ctrl+F12 / Ctrl+Shift+F12  -- screenshot (full / window)
//   4. Ctrl+Esc  -- system-wide hotkey (task manager)
//   5. F10  -- activate/toggle menu bar
//   4. Keyboard move/resize mode (arrow keys captured exclusively)
//   5. Alt+Space  -- system menu toggle
//   6. System menu keyboard navigation (arrows, enter, esc, accel)
//   7. Alt+key  -- menu bar / widget accelerator dispatch
//   8. Popup menu keyboard navigation (arrows, enter, esc, accel)
//   9. Accelerator table on focused window (Ctrl+S, etc.)
//  10. Tab/Shift+Tab  -- widget focus cycling
//  11. Fall-through to focused window's onKey callback
//
// Key encoding: ASCII keys use their ASCII value; extended keys (arrows,
// function keys) use scancode | 0x100 to distinguish from ASCII 0.
// This avoids needing a separate "is_extended" flag.

static void pollKeyboard(AppContextT *ctx) {
    int32_t shiftFlags = platformKeyboardGetModifiers();
    ctx->keyModifiers  = shiftFlags;
    bool    shiftHeld  = (shiftFlags & KEY_MOD_SHIFT) != 0;

    // While busy, input is blocked (see dvxApp.h). Drain and discard any
    // pending keys so they cannot re-enter dvxSetWallpaper/dvxChangeVideoMode
    // during the buildWallpaperBuf yield pump, and so a burst does not replay
    // the instant busy clears. Matches the dispatchEvents mouse gate.
    if (ctx->busy) {
        PlatformKeyEventT drain;

        while (platformKeyboardRead(&drain)) {
            // discard
        }

        // The INT 9 ISR keeps queueing break codes regardless of busy, so
        // the key-up ring must be drained too -- otherwise the releases of
        // keys typed during busy replay as phantom onKeyUp events the
        // instant busy clears.
        while (platformKeyUpRead(&drain)) {
            // discard
        }

        return;
    }

    PlatformKeyEventT evt;

    while (platformKeyboardRead(&evt)) {
        int32_t scancode = evt.scancode;
        int32_t ascii    = evt.ascii;

        // Alt+Tab / Shift+Alt+Tab  -- cycle windows (see cycleWindowFocus)
        if (ascii == 0 && scancode == KEY_SCAN_ALT_TAB) {
            cycleWindowFocus(ctx, shiftHeld);
            continue;
        }

        // Alt+F4  -- close focused window. A modal dialog captures all
        // input, so only the modal window itself may be closed this way.
        if (ascii == 0 && scancode == KEY_SCAN_ALT_F4) {
            if (ctx->stack.focusedIdx >= 0) {
                WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];

                if (ctx->modalWindow && win != ctx->modalWindow) {
                    continue;
                }

                if (win->onClose) {
                    WIN_CALLBACK(ctx, win, win->onClose(win));
                } else {
                    dvxDestroyWindow(ctx, win);
                }
            }

            continue;
        }

        // Ctrl+F12  -- save full screen screenshot
        // Ctrl+Shift+F12  -- save focused window screenshot
        // BIOS returns scancode 0x58 for F12; Ctrl+F12 = scancode 0x8A.
        if (ascii == 0 && scancode == KEY_SCAN_CTRL_F12 && (shiftFlags & KEY_MOD_CTRL)) {
            if (shiftHeld && ctx->stack.focusedIdx >= 0) {
                interactiveWindowScreenshot(ctx, ctx->stack.windows[ctx->stack.focusedIdx]);
            } else {
                interactiveScreenshot(ctx);
            }

            continue;
        }

        // Ctrl+Esc  -- system-wide hotkey (e.g. task manager)
        if (scancode == KEY_SCAN_ESC && ascii == KEY_ESCAPE && (shiftFlags & KEY_MOD_CTRL)) {
            if (ctx->onCtrlEsc) {
                ctx->onCtrlEsc(ctx->ctrlEscCtx);
            }

            continue;
        }

        // F1  -- system-wide help
        if (ascii == 0 && scancode == KEY_SCAN_F1 && !(shiftFlags & (KEY_MOD_CTRL | KEY_MOD_ALT | KEY_MOD_SHIFT))) {
            if (ctx->onF1) {
                ctx->onF1(ctx->f1Ctx);
            }

            continue;
        }

        // F10  -- activate menu bar
        if (ascii == 0 && scancode == KEY_SCAN_F10) {
            if (ctx->stack.focusedIdx >= 0) {
                WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];

                if (win->menuBar && win->menuBar->menuCount > 0) {
                    if (ctx->popup.active) {
                        closeAllPopups(ctx);
                    } else {
                        dispatchAccelKey(ctx, win->menuBar->menus[0]->accelKey);
                    }
                }
            }

            continue;
        }

        // Keyboard move/resize mode  -- intercept all keys
        if (ctx->kbMoveResize.mode != KbModeNoneE) {
            WindowT *kbWin = findWindowById(ctx, ctx->kbMoveResize.windowId);

            if (!kbWin) {
                ctx->kbMoveResize.mode = KbModeNoneE;
                continue;
            }

            if (ascii == KEY_ESCAPE) {
                // Cancel  -- restore original position/size
                dirtyListAdd(&ctx->dirty, kbWin->x, kbWin->y, kbWin->w, kbWin->h);
                kbWin->x = ctx->kbMoveResize.origX;
                kbWin->y = ctx->kbMoveResize.origY;

                if (ctx->kbMoveResize.mode == KbModeResizeE) {
                    kbWin->w = ctx->kbMoveResize.origW;
                    kbWin->h = ctx->kbMoveResize.origH;
                    wmResizeCommit(kbWin, &ctx->display);
                }

                dirtyListAdd(&ctx->dirty, kbWin->x, kbWin->y, kbWin->w, kbWin->h);
                ctx->kbMoveResize.mode = KbModeNoneE;
                continue;
            }

            if (ascii == KEY_ENTER) {
                // Confirm
                ctx->kbMoveResize.mode = KbModeNoneE;
                continue;
            }

            if (ctx->kbMoveResize.mode == KbModeMoveE) {
                int32_t oldX = kbWin->x;
                int32_t oldY = kbWin->y;

                if (ascii == 0 && scancode == KEY_SCAN_UP) {
                    kbWin->y -= KB_MOVE_STEP;
                } else if (ascii == 0 && scancode == KEY_SCAN_DOWN) {
                    kbWin->y += KB_MOVE_STEP;
                } else if (ascii == 0 && scancode == KEY_SCAN_LEFT) {
                    kbWin->x -= KB_MOVE_STEP;
                } else if (ascii == 0 && scancode == KEY_SCAN_RIGHT) {
                    kbWin->x += KB_MOVE_STEP;
                }

                // Clamp: keep title bar reachable
                int32_t screenW = ctx->display.width;
                int32_t screenH = ctx->display.height;

                if (kbWin->y < 0) {
                    kbWin->y = 0;
                }
                if (kbWin->y + CHROME_TITLEBAR_BOTTOM > screenH) {
                    kbWin->y = screenH - CHROME_TITLEBAR_BOTTOM;
                }
                if (kbWin->x + kbWin->w < DRAG_MIN_VISIBLE) {
                    kbWin->x = DRAG_MIN_VISIBLE - kbWin->w;
                }
                if (kbWin->x > screenW - DRAG_MIN_VISIBLE) {
                    kbWin->x = screenW - DRAG_MIN_VISIBLE;
                }

                if (kbWin->x != oldX || kbWin->y != oldY) {
                    kbWin->maximized = false;
                    dirtyListAdd(&ctx->dirty, oldX, oldY, kbWin->w, kbWin->h);
                    dirtyListAdd(&ctx->dirty, kbWin->x, kbWin->y, kbWin->w, kbWin->h);
                }
            } else {
                // KbModeResizeE
                int32_t newW = kbWin->w;
                int32_t newH = kbWin->h;

                if (ascii == 0 && scancode == KEY_SCAN_RIGHT) {
                    newW += KB_MOVE_STEP;
                } else if (ascii == 0 && scancode == KEY_SCAN_LEFT) {
                    newW -= KB_MOVE_STEP;
                } else if (ascii == 0 && scancode == KEY_SCAN_DOWN) {
                    newH += KB_MOVE_STEP;
                } else if (ascii == 0 && scancode == KEY_SCAN_UP) {
                    newH -= KB_MOVE_STEP;
                }

                // Same dynamic minimum as mouse resize (title gadgets,
                // menu bar) rather than the bare MIN_WINDOW_W/H floor.
                int32_t minW;
                int32_t minH;
                wmMinWindowSize(kbWin, &minW, &minH);

                if (newW < minW) {
                    newW = minW;
                }

                if (newH < minH) {
                    newH = minH;
                }

                if (kbWin->maxW > 0 && newW > kbWin->maxW) {
                    newW = kbWin->maxW;
                }

                if (kbWin->maxH > 0 && newH > kbWin->maxH) {
                    newH = kbWin->maxH;
                }

                // Clamp to screen boundaries
                int32_t screenW = ctx->display.width;
                int32_t screenH = ctx->display.height;

                if (kbWin->x + newW > screenW) {
                    newW = screenW - kbWin->x;
                }

                if (kbWin->y + newH > screenH) {
                    newH = screenH - kbWin->y;
                }

                // Keep the window reachable: with x off-screen left,
                // shrinking must not push the whole window off-screen.
                if (kbWin->x + newW < DRAG_MIN_VISIBLE) {
                    newW = DRAG_MIN_VISIBLE - kbWin->x;
                }

                if (newW != kbWin->w || newH != kbWin->h) {
                    kbWin->maximized = false;
                    dirtyListAdd(&ctx->dirty, kbWin->x, kbWin->y, kbWin->w, kbWin->h);
                    kbWin->w = newW;
                    kbWin->h = newH;
                    wmResizeCommit(kbWin, &ctx->display);
                    dirtyListAdd(&ctx->dirty, kbWin->x, kbWin->y, kbWin->w, kbWin->h);
                }
            }

            continue;
        }

        // Alt+Space  -- open/close system menu
        // Enhanced INT 16h: Alt+Space returns the plain Space scancode/ascii
        // pair, so the Alt modifier flag is what distinguishes it.
        if (scancode == KEY_SCAN_SPACE && ascii == KEY_SPACE && (shiftFlags & KEY_MOD_ALT)) {
            if (ctx->sysMenu.active) {
                closeSysMenu(ctx);
            } else if (ctx->stack.focusedIdx >= 0) {
                WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
                openSysMenu(ctx, win);
            }
            continue;
        }

        // System menu keyboard navigation
        if (ctx->sysMenu.active) {
            // Alt+key  -- close system menu and let it fall through to accel dispatch
            if (ascii == 0 && platformAltScanToChar(scancode)) {
                closeSysMenu(ctx);
                // Fall through to dispatchAccelKey below
            } else if (ascii == KEY_ESCAPE) {
                closeSysMenu(ctx);
                continue;
            } else if (ascii == 0 && (scancode == KEY_SCAN_UP || scancode == KEY_SCAN_DOWN)) {
                ctx->sysMenu.hoverItem = nextMenuItem(&ctx->sysMenu.items[0].separator, sizeof(ctx->sysMenu.items[0]), ctx->sysMenu.itemCount, ctx->sysMenu.hoverItem, scancode == KEY_SCAN_UP ? -1 : 1);
                dirtyListAdd(&ctx->dirty, ctx->sysMenu.popupX, ctx->sysMenu.popupY, ctx->sysMenu.popupW, ctx->sysMenu.popupH);
                continue;
            } else if (ascii == KEY_ENTER) {
                // Enter  -- execute selected item
                if (ctx->sysMenu.hoverItem >= 0 && ctx->sysMenu.hoverItem < ctx->sysMenu.itemCount) {
                    SysMenuItemT *item = &ctx->sysMenu.items[ctx->sysMenu.hoverItem];

                    if (item->enabled && !item->separator) {
                        executeSysMenuCmd(ctx, item->cmd);
                    }
                } else {
                    closeSysMenu(ctx);
                }
                continue;
            } else if (ascii != 0) {
                // Accelerator key match
                char lc = (char)tolower(ascii);

                for (int32_t k = 0; k < ctx->sysMenu.itemCount; k++) {
                    if (ctx->sysMenu.items[k].accelKey == lc && ctx->sysMenu.items[k].enabled && !ctx->sysMenu.items[k].separator) {
                        executeSysMenuCmd(ctx, ctx->sysMenu.items[k].cmd);
                        goto nextKey;
                    }
                }

                // No sys menu match  -- try menu bar accelerators
                closeSysMenu(ctx);
                if (ctx->stack.focusedIdx >= 0) {
                    WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];

                    if (win->menuBar) {
                        for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
                            if (win->menuBar->menus[i]->accelKey == lc) {
                                dispatchAccelKey(ctx, lc);
                                goto nextKey;
                            }
                        }
                    }
                }
                continue;
            } else {
                continue;
            }
        }

        // Check for Alt+key (BIOS returns ascii=0 with specific scancodes)
        if (ascii == 0 && platformAltScanToChar(scancode)) {
            char accelKey = platformAltScanToChar(scancode);

            if (dispatchAccelKey(ctx, accelKey)) {
                continue;
            }
        }

        // Popup menu keyboard navigation (arrows, enter, esc)
        if (ctx->popup.active && ascii == 0) {
            MenuT *curMenu = ctx->popup.menu;

            // Up / Down arrow
            if (scancode == KEY_SCAN_UP || scancode == KEY_SCAN_DOWN) {
                if (curMenu && curMenu->itemCount > 0) {
                    ctx->popup.hoverItem = nextMenuItem(&curMenu->items[0].separator, sizeof(curMenu->items[0]), curMenu->itemCount, ctx->popup.hoverItem, scancode == KEY_SCAN_UP ? -1 : 1);
                    dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY, ctx->popup.popupW, ctx->popup.popupH);
                }

                continue;
            }

            // Left arrow  -- close submenu, or switch to previous top-level menu
            if (scancode == KEY_SCAN_LEFT) {
                if (ctx->popup.depth > 0) {
                    closePopupLevel(ctx);
                } else {
                    WindowT *win = findWindowById(ctx, ctx->popup.windowId);

                    if (win && win->menuBar && win->menuBar->menuCount > 1) {
                        int32_t newIdx = ctx->popup.menuIdx - 1;

                        if (newIdx < 0) {
                            newIdx = win->menuBar->menuCount - 1;
                        }

                        openPopupAtMenu(ctx, win, newIdx);
                    }
                }

                continue;
            }

            // Right arrow  -- open submenu, or switch to next top-level menu
            if (scancode == KEY_SCAN_RIGHT) {
                // If hovered item has a submenu, open it
                if (curMenu && ctx->popup.hoverItem >= 0 && ctx->popup.hoverItem < curMenu->itemCount) {
                    MenuItemT *hItem = &curMenu->items[ctx->popup.hoverItem];

                    if (hItem->subMenu && hItem->enabled) {
                        openSubMenu(ctx);
                        continue;
                    }
                }

                // Otherwise switch to next top-level menu
                WindowT *win = findWindowById(ctx, ctx->popup.windowId);

                if (win && win->menuBar && win->menuBar->menuCount > 1) {
                    int32_t newIdx = ctx->popup.menuIdx + 1;

                    if (newIdx >= win->menuBar->menuCount) {
                        newIdx = 0;
                    }

                    openPopupAtMenu(ctx, win, newIdx);
                }

                continue;
            }
        }

        // Enter executes highlighted popup menu item (or opens submenu)
        if (ctx->popup.active && ascii == KEY_ENTER) {
            MenuT *curMenu = ctx->popup.menu;

            if (curMenu && ctx->popup.hoverItem >= 0 && ctx->popup.hoverItem < curMenu->itemCount) {
                MenuItemT *item = &curMenu->items[ctx->popup.hoverItem];

                if (item->subMenu && item->enabled) {
                    openSubMenu(ctx);
                } else if (item->enabled && !item->separator) {
                    activatePopupItem(ctx, ctx->popup.hoverItem);
                }
            } else {
                closeAllPopups(ctx);
            }

            continue;
        }

        // Check for plain key accelerator in open popup menu
        if (ctx->popup.active && ascii != 0) {
            char   lc      = (char)tolower(ascii);
            MenuT *curMenu = ctx->popup.menu;

            // Try matching an item in the current popup
            if (curMenu) {
                for (int32_t k = 0; k < curMenu->itemCount; k++) {
                    MenuItemT *item = &curMenu->items[k];

                    if (item->accelKey == lc && item->enabled && !item->separator) {
                        if (item->subMenu) {
                            ctx->popup.hoverItem = k;
                            dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY, ctx->popup.popupW, ctx->popup.popupH);
                            openSubMenu(ctx);
                        } else {
                            activatePopupItem(ctx, k);
                        }

                        goto nextKey;
                    }
                }
            }

            // No match in current popup  -- try switching to another top-level menu
            WindowT *win = findWindowById(ctx, ctx->popup.windowId);

            if (win && win->menuBar) {
                for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
                    if (win->menuBar->menus[i]->accelKey == lc && i != ctx->popup.menuIdx) {
                        openPopupAtMenu(ctx, win, i);
                        goto nextKey;
                    }
                }
            }
        }

        // ESC closes open dropdown/combobox popup
        if (sOpenPopup && ascii == KEY_ESCAPE) {
            closeOpenPopup(ctx);
            continue;
        }

        // ESC closes one popup level (or all if at top level)
        if (ctx->popup.active && ascii == KEY_ESCAPE) {
            closePopupLevel(ctx);
            continue;
        }

        // Check accelerator table on focused window
        if (ctx->stack.focusedIdx >= 0) {
            WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
            int32_t  key = ascii ? ascii : (scancode | KEY_EXT_FLAG);

            if (checkAccelTable(ctx, win, key, shiftFlags)) {
                continue;
            }
        }

        // Tab / Shift-Tab  -- cycle focus between widgets
        // Tab arrives with the Tab ascii code, Shift-Tab with ascii 0.
        if (scancode == KEY_SCAN_TAB && (ascii == KEY_TAB || ascii == 0)) {
            if (ctx->stack.focusedIdx >= 0) {
                WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];

                if (win->widgetRoot) {
                    // Find the currently focused widget
                    WidgetT *current = NULL;
                    WidgetT **fstack = NULL;
                    arrput(fstack, win->widgetRoot);

                    while (arrlen(fstack) > 0) {
                        WidgetT *w = fstack[arrlen(fstack) - 1];
                        arrsetlen(fstack, arrlen(fstack) - 1);

                        if (w == sFocusedWidget && widgetIsFocusable(w->type)) {
                            current = w;
                            break;
                        }

                        for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
                            if (c->visible) {
                                arrput(fstack, c);
                            }
                        }
                    }

                    // If the focused widget wants Tab, send it there
                    // instead of cycling focus.
                    if (current && (current->swallowTab ||
                        (current->wclass && (current->wclass->flags & WCLASS_SWALLOWS_TAB)))) {
                        if (win->onKey) {
                            WIN_CALLBACK(ctx, win, win->onKey(win, ascii ? ascii : (scancode | KEY_EXT_FLAG), shiftFlags));
                        }

                        arrfree(fstack);
                        continue;
                    }

                    WidgetT *next;

                    if (ascii == KEY_TAB) {
                        next = widgetFindNextFocusable(win->widgetRoot, current);
                    } else {
                        next = widgetFindPrevFocusable(win->widgetRoot, current);
                    }

                    if (next) {
                        // Close any open dropdown popup so its d->open flag
                        // is not left stale when focus moves off it.
                        closeOpenPopup(ctx);

                        // A blur/focus callback may destroy widgets (a
                        // BASIC Change handler unloading its form) -- next
                        // is dangling then and must not be dereferenced.
                        if (widgetTransferFocus(next)) {
                            // Scroll the widget into view if needed
                            int32_t scrollX = win->hScroll ? win->hScroll->value : 0;
                            int32_t scrollY = win->vScroll ? win->vScroll->value : 0;
                            int32_t virtX   = next->x + scrollX;
                            int32_t virtY   = next->y + scrollY;

                            if (win->vScroll) {
                                if (virtY < win->vScroll->value) {
                                    win->vScroll->value = virtY;
                                } else if (virtY + next->h > win->vScroll->value + win->contentH) {
                                    win->vScroll->value = virtY + next->h - win->contentH;
                                }

                                win->vScroll->value = clampInt(win->vScroll->value, win->vScroll->min, win->vScroll->max);
                            }

                            if (win->hScroll) {
                                if (virtX < win->hScroll->value) {
                                    win->hScroll->value = virtX;
                                } else if (virtX + next->w > win->hScroll->value + win->contentW) {
                                    win->hScroll->value = virtX + next->w - win->contentW;
                                }

                                win->hScroll->value = clampInt(win->hScroll->value, win->hScroll->min, win->hScroll->max);
                            }

                            // Scroll into view inside any scroll container ancestor
                            for (WidgetT *p = next->parent; p; p = p->parent) {
                                if (p->wclass && (p->wclass->flags & WCLASS_SCROLL_CONTAINER) &&
                                    wclsHas(p, WGT_METHOD_SCROLL_CHILD_INTO_VIEW)) {
                                    wclsScrollChildIntoView(p, next);
                                    wgtInvalidate(p);
                                    break;
                                }
                            }
                        }
                    }

                    arrfree(fstack);
                }
            }

            continue;
        }

        // Send to focused window
        if (ctx->stack.focusedIdx >= 0) {
            WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];

            if (win->onKey) {
                WIN_CALLBACK(ctx, win, win->onKey(win, ascii ? ascii : (scancode | KEY_EXT_FLAG), shiftFlags));
            }
        }

        continue;
nextKey:;
    }

    // Key-up events: dispatch to the focused window's onKeyUp callback
    PlatformKeyEventT upEvt;

    while (platformKeyUpRead(&upEvt)) {
        if (ctx->stack.focusedIdx >= 0) {
            WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
            int32_t mod = platformKeyboardGetModifiers();

            if (win->onKeyUp) {
                WIN_CALLBACK(ctx, win, win->onKeyUp(win, upEvt.scancode | KEY_EXT_FLAG, mod));
            }
        }
    }
}


// ============================================================
// pollMouse
// ============================================================

static void pollMouse(AppContextT *ctx) {
    int32_t mx;
    int32_t my;
    int32_t buttons;

    platformMousePoll(&mx, &my, &buttons);

    ctx->mouseX       = mx;
    ctx->mouseY       = my;
    ctx->mouseButtons = buttons;
    ctx->mouseWheel   = platformMouseWheelPoll();
}


// ============================================================
// pollWidgets  -- poll widgets that need periodic updates
// ============================================================
//
// Widgets with WCLASS_NEEDS_POLL (e.g. ANSI terminals with asynchronous
// data sources) need periodic polling between frames. This function walks
// every window's widget tree and calls the poll vtable method on matching
// widgets. The poll method handles data ingestion and targeted dirty
// rect generation for efficient repainting.

static void pollWidgets(AppContextT *ctx) {
    // Poll handlers ingest data and add dirty rects; they do not grow the
    // poll list, and any that destroy a poll widget bump sWidgetGen and
    // break the loop below before the count is reused -- so the length is
    // loop-invariant and derived once here rather than every iteration.
    int32_t count = (int32_t)arrlen(sPollWidgets);

    for (int32_t i = 0; i < count; i++) {
        WidgetT *w   = sPollWidgets[i];
        WindowT *win = w->window;

        if (!win) {
            continue;
        }

        // A deferred-destroyed window is inert: its widget tree is still
        // allocated but its backing app state may already be freed, so no
        // poll callback may run into it.
        if (win->destroyPending) {
            continue;
        }

        // Snapshot the destroy generation: wclsPoll can run app-supplied
        // callbacks (e.g. a terminal comm read function) that may destroy
        // this widget or its window's widget tree.  w is dangling then and
        // arrdel has shifted sPollWidgets under the loop, so stop this
        // pass -- the survivors are polled again next frame.
        uint32_t gen = sWidgetGen;

        wclsPoll(w, win);

        if (sWidgetGen != gen) {
            break;
        }

        if (!wclsHas(w, WGT_METHOD_QUICK_REPAINT)) {
            continue;
        }

        int32_t dirtyY = 0;
        int32_t dirtyH = 0;

        if (wclsQuickRepaint(w, &dirtyY, &dirtyH) > 0) {
            win->iconNeedsRefresh = true;

            if (!win->minimized) {
                int32_t scrollY = win->vScroll ? win->vScroll->value : 0;
                int32_t rectX   = win->x + win->contentX;
                int32_t rectY   = win->y + win->contentY + dirtyY - scrollY;
                int32_t rectW   = win->contentW;
                dirtyListAdd(&ctx->dirty, rectX, rectY, rectW, dirtyH);
            }
        }
    }
}




// ============================================================
// popupHandleOutsideCurrent
// ============================================================
//
// Handles a mouse event that is not inside the CURRENT popup level:
// checks parent levels (deepest first) and pops back to the level the
// mouse is in, re-opening a hovered submenu; otherwise handles the
// outside click (menu-bar switching / close-all).  Shared by the
// not-in-current branch of dispatchEvents and by the press-edge path
// when a same-frame submenu open moved the current popup out from
// under the press.

static void popupHandleOutsideCurrent(AppContextT *ctx, int32_t mx, int32_t my, int32_t buttons, int32_t prevBtn) {
    // Check parent levels (deepest first)
    bool inParent = false;

    for (int32_t lvl = ctx->popup.depth - 1; lvl >= 0; lvl--) {
        PopupLevelT *pl = &ctx->popup.parentStack[lvl];

        if (mx >= pl->popupX && mx < pl->popupX + pl->popupW &&
            my >= pl->popupY && my < pl->popupY + pl->popupH) {

            // Close all levels deeper than this one
            while (ctx->popup.depth > lvl + 1) {
                closePopupLevel(ctx);
            }

            // Now close the level that was the "current" when we entered this parent
            closePopupLevel(ctx);

            // Now current level IS this parent  -- update hover
            int32_t itemIdx = popupItemAt(ctx, my);

            ctx->popup.hoverItem = itemIdx;
            dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
                         ctx->popup.popupW, ctx->popup.popupH);

            // If the newly hovered item has a submenu, open it
            if (ctx->popup.menu && itemIdx >= 0 && itemIdx < ctx->popup.menu->itemCount) {
                MenuItemT *hItem = &ctx->popup.menu->items[itemIdx];

                if (hItem->subMenu && hItem->enabled) {
                    openSubMenu(ctx);
                }
            }

            inParent = true;
            break;
        }
    }

    if (!inParent) {
        if (ctx->popup.isContextMenu) {
            // Context menu: any click outside closes it
            if ((buttons & MOUSE_LEFT) && !(prevBtn & MOUSE_LEFT)) {
                closeAllPopups(ctx);
            }
        } else {
            // Menu bar popup: check if mouse is on the menu bar for switching
            WindowT *win = findWindowById(ctx, ctx->popup.windowId);

            if (win && win->menuBar) {
                int32_t barY = win->y + CHROME_TITLEBAR_BOTTOM;

                if (my >= barY && my < barY + CHROME_MENU_HEIGHT &&
                    mx >= win->x + CHROME_TOTAL_SIDE &&
                    mx < win->x + win->w - CHROME_TOTAL_SIDE) {

                    int32_t relX = mx - win->x;

                    for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
                        MenuT *menu = win->menuBar->menus[i];

                        if (relX >= menu->barX && relX < menu->barX + menu->barW) {
                            if ((buttons & MOUSE_LEFT) && !(prevBtn & MOUSE_LEFT) && i == ctx->popup.menuIdx && ctx->popup.depth == 0) {
                                // Clicking the same menu bar entry closes the menu
                                closeAllPopups(ctx);
                            } else if (i != ctx->popup.menuIdx || ctx->popup.depth > 0) {
                                openPopupAtMenu(ctx, win, i);
                            }

                            break;
                        }
                    }
                } else if ((buttons & MOUSE_LEFT) && !(prevBtn & MOUSE_LEFT)) {
                    closeAllPopups(ctx);
                }
            } else if ((buttons & MOUSE_LEFT) && !(prevBtn & MOUSE_LEFT)) {
                closeAllPopups(ctx);
            }
        }
    }
}


// ============================================================
// popupItemAt
// ============================================================
//
// Converts a mouse Y position to an item index in the CURRENT popup
// level, clamped to the menu's item range.  Uses a fixed-point
// reciprocal multiply instead of integer divide, which costs 40+
// cycles on a 486.  Single source of the row formula for the hover,
// click, and parent-level-scan sites.

static int32_t popupItemAt(const AppContextT *ctx, int32_t my) {
    int32_t relY    = my - ctx->popup.popupY - POPUP_BEVEL_WIDTH;
    int32_t itemIdx = (relY >= 0) ? (int32_t)((uint32_t)relY * ctx->charHeightRecip >> 16) : 0;

    if (ctx->popup.menu && itemIdx >= ctx->popup.menu->itemCount) {
        itemIdx = ctx->popup.menu->itemCount - 1;
    }

    return itemIdx;
}


// ============================================================
// refreshMinimizedIcons
// ============================================================
//
// Minimized windows show a thumbnail of their content. When the content
// changes (e.g., a terminal receives output while minimized), the icon
// thumbnail needs updating. Rather than refreshing all dirty icons every
// frame (which could cause a burst of repaints), this function refreshes
// at most ONE icon per call, using a round-robin index (iconRefreshIdx)
// so each dirty icon gets its turn. Called every ICON_REFRESH_INTERVAL
// frames, this spreads the cost across time. Windows with custom iconData
// (loaded from .bmp/.png) are skipped since their thumbnails don't change.

static void refreshMinimizedIcons(AppContextT *ctx) {
    WindowStackT *ws = &ctx->stack;
    DisplayT     *d  = &ctx->display;
    int32_t       count   = 0;
    int32_t       iconIdx = 0;

    for (int32_t i = 0; i < ws->count; i++) {
        WindowT *win = ws->windows[i];

        if (!win->visible || !win->minimized) {
            continue;
        }

        if (!win->iconData && win->iconNeedsRefresh) {
            if (count >= ctx->iconRefreshIdx) {
                int32_t ix;
                int32_t iy;
                wmMinimizedIconPos(d, iconIdx, &ix, &iy);
                dirtyListAdd(&ctx->dirty, ix, iy, ICON_TOTAL_SIZE, ICON_TOTAL_SIZE);
                win->iconNeedsRefresh   = false;
                ctx->iconRefreshIdx = count + 1;
                return;
            }

            count++;
        }

        iconIdx++;
    }

    // Wrapped past the end  -- reset for next cycle
    ctx->iconRefreshIdx = 0;
}


// ============================================================
// repositionWindow  -- move/resize a window, dirty old & new, fire callbacks
// ============================================================
//
// Shared helper for tiling/cascading. Dirties both the old and new
// positions (the old area needs repainting because the window moved away,
// the new area needs repainting because the window appeared there).
// Also reallocates the content buffer and fires onResize/onPaint so the
// window's content scales to the new dimensions.

static void repositionWindow(AppContextT *ctx, WindowT *win, int32_t x, int32_t y, int32_t w, int32_t h) {
    // Dirty old position
    dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);

    // Explicit reposition drops maximized state.
    win->maximized = false;

    win->x = x;
    win->y = y;
    win->w = w;
    win->h = h;

    WIN_CALLBACK(ctx, win, wmResizeCommit(win, &ctx->display));

    // Dirty new position
    dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);
}


// Splits a menu label at the tab into the left (menu text) part, copied into
// leftBuf, and returns a pointer to the right (shortcut) part.  Returns NULL
// when the label has no tab.  Shared by the popup measure and draw paths.
static const char *splitMenuLabel(const char *label, char leftBuf[MAX_MENU_LABEL]) {
    const char *tab = strchr(label, '\t');
    int32_t     leftLen;

    if (!tab) {
        return NULL;
    }

    leftLen = (int32_t)(tab - label);

    if (leftLen >= MAX_MENU_LABEL) {
        leftLen = MAX_MENU_LABEL - 1;
    }

    memcpy(leftBuf, label, leftLen);
    leftBuf[leftLen] = '\0';

    return tab + 1;
}


// ============================================================
// updateCursorShape
// ============================================================
//
// Updates the software cursor shape based on what the mouse is hovering
// over. The cursor is software-rendered (drawn in the compositor pass)
// rather than using a hardware cursor because VESA VBE doesn't provide
// hardware cursor support, and hardware cursors on VGA are limited to
// text mode. The shape priority is:
//   1. Active resize drag  -- keep the edge-specific resize cursor
//   2. ListView column resize drag
//   3. Splitter drag
//   4. Hover over resize edge  -- show directional resize cursor
//   5. Hover over ListView column border  -- horizontal resize cursor
//   6. Hover over splitter bar  -- orientation-specific resize cursor
//   7. Default arrow cursor

static void updateCursorShape(AppContextT *ctx) {
    // Busy state overrides all cursor logic
    if (ctx->busy) {
        ctx->cursorId = CURSOR_BUSY;
        return;
    }

    int32_t newCursor = CURSOR_ARROW;
    int32_t mx        = ctx->mouseX;
    int32_t my        = ctx->mouseY;

    // Popup menus override all cursor logic -- always arrow
    if (ctx->popup.active || ctx->sysMenu.active) {
        ctx->cursorId = CURSOR_ARROW;
        return;
    }

    // During active resize, keep the resize cursor
    if (ctx->stack.resizeWindow >= 0) {
        newCursor = cursorForResizeEdge(ctx->stack.resizeEdge);
    }
    // Active widget drag -- query cursor shape from the dragged widget
    else if (sDragWidget) {
        int32_t shape = wclsGetCursorShape(sDragWidget, 0, 0);

        if (shape > 0) {
            newCursor = shape;
        }
    }
    // Not in an active drag/resize  -- check what we're hovering
    else if (ctx->stack.dragWindow < 0 && ctx->stack.scrollWindow < 0) {
        int32_t hitPart;
        int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart);

        if (hitIdx >= 0 && hitPart == HIT_RESIZE) {
            // Hovering over a resize edge
            WindowT *win = ctx->stack.windows[hitIdx];

            newCursor = cursorForResizeEdge(wmResizeEdgeHit(win, mx, my));
        } else if (hitIdx >= 0 && hitPart == HIT_CONTENT) {
            WindowT *win = ctx->stack.windows[hitIdx];

            // Window-level cursor query (e.g. form designer handles)
            if (win->onCursorQuery) {
                int32_t cx = mx - win->x - win->contentX;
                int32_t cy = my - win->y - win->contentY;
                int32_t shape;
                WIN_CALLBACK(ctx, win, shape = win->onCursorQuery(win, cx, cy));

                if (shape > 0) {
                    newCursor = shape;
                }
            }

            // Widget-level cursor query (e.g. ListView column border)
            if (newCursor == CURSOR_ARROW && win->widgetRoot) {
                int32_t cx = mx - win->x - win->contentX;
                int32_t cy = my - win->y - win->contentY;
                int32_t scrollX = win->hScroll ? win->hScroll->value : 0;
                int32_t scrollY = win->vScroll ? win->vScroll->value : 0;
                int32_t vx = cx + scrollX;
                int32_t vy = cy + scrollY;

                WidgetT *hit = widgetHitTest(win->widgetRoot, vx, vy);

                if (hit) {
                    int32_t shape = wclsGetCursorShape(hit, vx, vy);

                    if (shape > 0) {
                        newCursor = shape;
                    }
                }
            }
        }
    }

    // If cursor shape changed, dirty the cursor area
    if (newCursor != ctx->cursorId) {
        dirtyCursorArea(ctx, mx, my);
        ctx->cursorId = newCursor;
    }
}


// ============================================================
// updateTooltip  -- show/hide tooltip based on hover state
// ============================================================
//
// Tooltip lifecycle: when the mouse stops moving over a widget that has
// a tooltip string set, a timer starts. After TOOLTIP_DELAY_MS (500ms),
// the tooltip appears. Any mouse movement or button press hides it and
// resets the timer. This avoids tooltip flicker during normal mouse use
// while still being responsive when the user hovers intentionally.
//
// The widget lookup walks into NO_HIT_RECURSE containers (like toolbars)
// to find the deepest child with a tooltip, so toolbar buttons can have
// individual tooltips even though the toolbar itself handles hit testing.

static void updateTooltip(AppContextT *ctx) {
    clock_t now       = clock();
    clock_t threshold = (clock_t)TOOLTIP_DELAY_MS * CLOCKS_PER_SEC / 1000;
    int32_t mx        = ctx->mouseX;
    int32_t my        = ctx->mouseY;

    // Mouse moved or button pressed  -- hide tooltip and reset timer
    if (mx != ctx->prevMouseX || my != ctx->prevMouseY || ctx->mouseButtons) {
        dvxInvalidateTooltip(ctx, NULL);
        ctx->tooltipHoverStart = now;
        return;
    }

    // Already showing a tooltip.  Re-validate before pinning it: if any
    // widget has been destroyed since it was shown, the borrowed text
    // pointer may be dangling -- hide it and fall through to re-run the
    // hit test against the surviving widgets.
    if (ctx->tooltipText) {
        if (sWidgetGen == sTooltipShownGen) {
            return;
        }

        dvxInvalidateTooltip(ctx, NULL);
    }

    // Not enough time has passed
    if ((now - ctx->tooltipHoverStart) < threshold) {
        return;
    }

    // Don't show tooltips while popups/menus are active
    if (ctx->popup.active || ctx->sysMenu.active) {
        return;
    }

    // Check minimized icons, but only if no window covers the point
    const char *tipText = NULL;
    int32_t iconIdx = -1;
    {
        int32_t hitPart;
        int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart);

        if (hitIdx < 0) {
            iconIdx = wmMinimizedIconHit(&ctx->stack, &ctx->display, mx, my);
        }
    }

    if (iconIdx >= 0) {
        tipText = ctx->stack.windows[iconIdx]->title;
    }

    // Otherwise check widgets in the content area of a window
    if (!tipText) {
        int32_t hitPart;
        int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart);

        if (hitIdx < 0 || hitPart != HIT_CONTENT) {
            return;
        }

        WindowT *win = ctx->stack.windows[hitIdx];

        if (!win->widgetRoot) {
            return;
        }

        int32_t cx = mx - win->x - win->contentX;
        int32_t cy = my - win->y - win->contentY;
        int32_t scrollX = win->hScroll ? win->hScroll->value : 0;
        int32_t scrollY = win->vScroll ? win->vScroll->value : 0;
        int32_t vx = cx + scrollX;
        int32_t vy = cy + scrollY;

        WidgetT *hit = widgetHitTest(win->widgetRoot, vx, vy);

        // Walk into NO_HIT_RECURSE containers to find deepest child
        while (hit && hit->wclass && (hit->wclass->flags & WCLASS_NO_HIT_RECURSE)) {
            WidgetT *inner = NULL;

            for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) {
                if (c->visible && vx >= c->x && vx < c->x + c->w && vy >= c->y && vy < c->y + c->h) {
                    inner = c;
                }
            }

            if (!inner) {
                break;
            }

            if (inner->tooltip) {
                hit = inner;
                break;
            }

            if (inner->wclass && (inner->wclass->flags & WCLASS_NO_HIT_RECURSE)) {
                hit = inner;
            } else {
                WidgetT *deep = widgetHitTest(inner, vx, vy);
                hit = deep ? deep : inner;
                break;
            }
        }

        if (!hit || !hit->tooltip) {
            return;
        }

        tipText = hit->tooltip;
    }

    // Show the tooltip
    ctx->tooltipText = tipText;
    sTooltipShownGen = sWidgetGen;

    int32_t tw = textWidth(&ctx->font, tipText) + TOOLTIP_PAD * 2;
    int32_t th = ctx->font.charHeight + TOOLTIP_PAD * 2;

    // Position below and right of cursor
    ctx->tooltipX = mx + TOOLTIP_OFFSET_X;
    ctx->tooltipY = my + TOOLTIP_OFFSET_Y;

    // Keep on screen
    if (ctx->tooltipX + tw > ctx->display.width) {
        ctx->tooltipX = ctx->display.width - tw;
    }

    if (ctx->tooltipY + th > ctx->display.height) {
        ctx->tooltipY = my - th - TOOLTIP_ABOVE_GAP;
    }

    ctx->tooltipW = tw;
    ctx->tooltipH = th;

    // Dirty the tooltip area
    dirtyListAdd(&ctx->dirty, ctx->tooltipX, ctx->tooltipY, tw, th);
}


// ============================================================
// dvxAddAccel
// ============================================================
//
// Accelerator entries are pre-normalized at registration time: the key
// is uppercased and modifier bits are masked to just Ctrl|Alt. This
// moves the normalization cost from the hot path (every keypress) to
// the cold path (one-time setup), so checkAccelTable can do a simple
// integer compare per entry.

void dvxAddAccel(AccelTableT *table, int32_t key, int32_t modifiers, int32_t cmdId) {
    if (!table) {
        return;
    }

    if (table->count >= table->cap) {
        int32_t newCap = table->cap ? table->cap * 2 : 8;
        AccelEntryT *newBuf = (AccelEntryT *)realloc(table->entries, newCap * sizeof(AccelEntryT));

        if (!newBuf) {
            return;
        }

        table->entries = newBuf;
        table->cap     = newCap;
    }

    int32_t normKey = key;

    if (normKey >= 'a' && normKey <= 'z') {
        normKey = normKey - 32;
    }

    AccelEntryT *e = &table->entries[table->count++];
    e->key       = key;
    e->modifiers = modifiers;
    e->cmdId     = cmdId;
    e->normKey   = normKey;
    e->normMods  = modifiers & (ACCEL_CTRL | ACCEL_ALT);
}


// ============================================================
// dvxApplyColorScheme
// ============================================================

void dvxApplyColorScheme(AppContextT *ctx) {
    DisplayT *d = &ctx->display;

    for (int32_t i = 0; i < ColorCountE; i++) {
        *colorSlot(&ctx->colors, (ColorIdE)i) = packColor(d, ctx->colorRgb[i][0], ctx->colorRgb[i][1], ctx->colorRgb[i][2]);
    }

    invalidateAllWindows(ctx);
}


// ============================================================
// dvxCascadeWindows
// ============================================================
//
// Arranges windows in the classic cascade pattern: each window is the same
// size (2/3 of screen), offset diagonally by the title bar height so each
// title bar remains visible. When the cascade would go off-screen, it wraps
// back to (0,0). This matches the Windows 3.x cascade behavior.
// The step size is title_height + border_width so exactly one title bar's
// worth of the previous window peeks out above and to the left.

void dvxCascadeWindows(AppContextT *ctx) {
    int32_t screenW = ctx->display.width;
    int32_t screenH = ctx->display.height;
    int32_t offsetX = 0;
    int32_t offsetY = 0;
    int32_t step    = CHROME_TITLEBAR_BOTTOM;

    int32_t winW = screenW * CASCADE_SIZE_NUMER / CASCADE_SIZE_DENOM;
    int32_t winH = screenH * CASCADE_SIZE_NUMER / CASCADE_SIZE_DENOM;

    if (winW < MIN_WINDOW_W) {
        winW = MIN_WINDOW_W;
    }

    if (winH < MIN_WINDOW_H) {
        winH = MIN_WINDOW_H;
    }

    for (int32_t i = 0; i < ctx->stack.count; i++) {
        WindowT *win = ctx->stack.windows[i];

        if (win->minimized || !win->visible) {
            continue;
        }

        repositionWindow(ctx, win, offsetX, offsetY, winW, winH);

        offsetX += step;
        offsetY += step;

        // Wrap around if we'd go off screen
        if (offsetX + winW > screenW || offsetY + winH > screenH) {
            offsetX = 0;
            offsetY = 0;
        }
    }
}


// ============================================================
// dvxChangeVideoMode
// ============================================================
//
// Live video mode switch.  Saves the old display state, attempts to
// set the new mode, and if successful, reinitializes all dependent
// subsystems: blit ops, colors, cursors, mouse range, wallpaper,
// and all window content buffers.  Windows larger than the new
// screen are clamped.  On failure, the old mode is restored.

int32_t dvxChangeVideoMode(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) {
    // Save old state for rollback
    DisplayT oldDisplay = ctx->display;

    // Stash old wallpaper (don't free  -- we may need it for rollback)
    uint8_t *oldWpBuf   = ctx->wallpaperBuf;
    int32_t  oldWpPitch = ctx->wallpaperPitch;
    ctx->wallpaperBuf   = NULL;
    ctx->wallpaperPitch = 0;

    // Free old video buffers (no text mode restore)
    platformVideoFreeBuffers(&ctx->display);

    // Try the new mode
    if (videoInit(&ctx->display, requestedW, requestedH, preferredBpp) != 0) {
        // Restore old mode
        ctx->display = oldDisplay;
        ctx->display.backBuf = NULL;
        ctx->display.palette = NULL;

        if (videoInit(&ctx->display, oldDisplay.width, oldDisplay.height, oldDisplay.format.bitsPerPixel) != 0) {
            // Both failed  -- catastrophic
            return -1;
        }

        // Restore wallpaper
        ctx->wallpaperBuf   = oldWpBuf;
        ctx->wallpaperPitch = oldWpPitch;
        drawInit(&ctx->blitOps, &ctx->display);
        dvxApplyColorScheme(ctx);
        return -1;
    }

    // New mode succeeded  -- free old wallpaper buffer
    free(oldWpBuf);

    // Reinit blit ops for new pixel format
    drawInit(&ctx->blitOps, &ctx->display);

    // Repack all colors for new pixel format
    dvxApplyColorScheme(ctx);

    // Reinit mouse range
    platformMouseInit(ctx->display.width, ctx->display.height);
    ctx->hasMouseWheel = platformMouseWheelInit();

    // Clamp mouse position to new screen
    if (ctx->mouseX >= ctx->display.width) {
        ctx->mouseX = ctx->display.width - 1;
    }

    if (ctx->mouseY >= ctx->display.height) {
        ctx->mouseY = ctx->display.height - 1;
    }

    // Clamp and reallocate all window content buffers
    for (int32_t i = 0; i < ctx->stack.count; i++) {
        WindowT *win = ctx->stack.windows[i];

        // Clamp window position to new screen bounds
        if (win->x + win->w > ctx->display.width) {
            win->x = ctx->display.width - win->w;
        }

        if (win->x < 0) {
            win->x = 0;
            win->w = ctx->display.width;
        }

        if (win->y + win->h > ctx->display.height) {
            win->y = ctx->display.height - win->h;
        }

        if (win->y < 0) {
            win->y = 0;
            win->h = ctx->display.height;
        }

        // Clear maximized flag since screen size changed
        win->maximized = false;

        WIN_CALLBACK(ctx, win, wmResizeCommit(win, &ctx->display));
    }

    // Reload wallpaper at the new resolution/bpp
    if (ctx->wallpaperPath[0]) {
        dvxSetWallpaper(ctx, ctx->wallpaperPath);
    }

    // Reset clip and dirty the full screen
    resetClipRect(&ctx->display);
    dirtyListInit(&ctx->dirty);
    dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height);

    return 0;
}


// ============================================================
// dvxClipboardCopy / dvxClipboardGet
// ============================================================
//
// The clipboard is a simple in-process text buffer managed by the
// platform layer (clipboardCopy/clipboardGet). There is no inter-process
// clipboard because DVX runs as a single-process windowing system  -- all
// windows share the same address space. The thin wrappers here exist to
// keep the platform layer out of application code's include path.

void dvxClipboardCopy(const char *text, int32_t len) {
    clipboardCopy(text, len);
}


const char *dvxClipboardGet(int32_t *outLen) {
    return clipboardGet(outLen);
}


// ============================================================
// dvxColorLabel
// ============================================================

const char *dvxColorLabel(ColorIdE id) {
    if (id < 0 || id >= ColorCountE) {
        return "Unknown";
    }

    return sColorLabels[id];
}


// ============================================================
// dvxColorName
// ============================================================

const char *dvxColorName(ColorIdE id) {
    if (id < 0 || id >= ColorCountE) {
        return "unknown";
    }

    return sColorNames[id];
}


// ============================================================
// dvxCreateAccelTable
// ============================================================

AccelTableT *dvxCreateAccelTable(void) {
    AccelTableT *table = (AccelTableT *)calloc(1, sizeof(AccelTableT));
    return table;
}


// ============================================================
// dvxCreateWindow
// ============================================================

WindowT *dvxCreateWindow(AppContextT *ctx, const char *title, int32_t x, int32_t y, int32_t w, int32_t h, bool resizable) {
    // Auto-cascade: if another window already occupies this exact position,
    // offset diagonally by the title bar height so the new window doesn't
    // sit directly on top.  Keeps offsetting while collisions exist, wrapping
    // back to the origin if we'd go off screen.
    int32_t step = CHROME_TITLEBAR_BOTTOM;

    for (;;) {
        bool collision = false;

        for (int32_t i = 0; i < ctx->stack.count; i++) {
            WindowT *other = ctx->stack.windows[i];

            if (other->x == x && other->y == y) {
                collision = true;
                break;
            }
        }

        if (!collision) {
            break;
        }

        x += step;
        y += step;

        if (x + w > ctx->display.width || y + h > ctx->display.height) {
            x = step;
            y = step;
            break;
        }
    }

    // Clamp to screen so the window is fully visible
    if (x + w > ctx->display.width) {
        x = ctx->display.width - w;
    }

    if (y + h > ctx->display.height) {
        y = ctx->display.height - h;
    }

    if (x < 0) {
        x = 0;
    }

    if (y < 0) {
        y = 0;
    }

    WindowT *win = wmCreateWindow(&ctx->stack, &ctx->display, title, x, y, w, h, resizable);

    if (win) {
        // Stamp window with the current app ID (set by the shell before
        // calling app entry points). Enables per-app window tracking for
        // cleanup on crash/termination.
        win->appId = ctx->currentAppId;

        // Raise and focus
        int32_t idx = ctx->stack.count - 1;
        wmSetFocus(&ctx->stack, &ctx->dirty, idx);
        dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);
    }

    return win;
}


// ============================================================
// dvxCreateWindowCentered
// ============================================================

WindowT *dvxCreateWindowCentered(AppContextT *ctx, const char *title, int32_t w, int32_t h, bool resizable) {
    int32_t x = (ctx->display.width - w) / 2;
    int32_t y = (ctx->display.height - h) / 2;

    return dvxCreateWindow(ctx, title, x, y, w, h, resizable);
}


void dvxDestroyWindow(AppContextT *ctx, WindowT *win) {
    destroyWindowPrep(ctx, win);

    // A user callback is on the stack: defer the teardown so the
    // dispatch code's widget pointers stay valid until it unwinds.
    if (ctx->dispatchDepth > 0) {
        deferDestroyWindow(ctx, win);
        return;
    }

    destroyWindowSync(ctx, win);
}


void dvxDestroyWindowNow(AppContextT *ctx, WindowT *win) {
    destroyWindowPrep(ctx, win);

    // A previously deferred window is destroyed here and now; drop its
    // queued state so the flush hint stays balanced.
    if (win->destroyPending) {
        win->destroyPending = false;

        if (ctx->pendingDestroyCount > 0) {
            ctx->pendingDestroyCount--;
        }
    }

    destroyWindowSync(ctx, win);
}


// Resizes a window to exactly fit its widget tree's minimum size,
// accounting for chrome overhead (title bar, borders, optional menu bar).
// Used after building a dialog's widget tree to size the dialog
// automatically rather than requiring the caller to compute sizes manually.

void dvxFitWindow(AppContextT *ctx, WindowT *win) {
    if (!ctx || !win || !win->widgetRoot) {
        return;
    }

    widgetCalcMinSizeTree(win->widgetRoot, &ctx->font);

    int32_t topChrome = CHROME_TOTAL_TOP;

    if (win->menuBar) {
        topChrome += CHROME_MENU_HEIGHT;
    }

    int32_t newW = win->widgetRoot->calcMinW + CHROME_TOTAL_SIDE * 2;
    int32_t newH = win->widgetRoot->calcMinH + topChrome + CHROME_TOTAL_BOTTOM;

    // Ensure the window is at least as wide/tall as the WM minimum
    // (accounts for menu bar width, title bar gadgets, etc.)
    int32_t wmMinW;
    int32_t wmMinH;
    wmMinWindowSize(win, &wmMinW, &wmMinH);

    if (newW < wmMinW) {
        newW = wmMinW;
    }

    if (newH < wmMinH) {
        newH = wmMinH;
    }

    dvxResizeWindow(ctx, win, newW, newH);
}


void dvxFitWindowW(AppContextT *ctx, WindowT *win) {
    if (!ctx || !win || !win->widgetRoot) {
        return;
    }

    widgetCalcMinSizeTree(win->widgetRoot, &ctx->font);

    int32_t newW = win->widgetRoot->calcMinW + CHROME_TOTAL_SIDE * 2;

    dvxResizeWindow(ctx, win, newW, win->h);
}


void dvxFitWindowH(AppContextT *ctx, WindowT *win) {
    if (!ctx || !win || !win->widgetRoot) {
        return;
    }

    widgetCalcMinSizeTree(win->widgetRoot, &ctx->font);

    int32_t topChrome = CHROME_TOTAL_TOP;

    if (win->menuBar) {
        topChrome += CHROME_MENU_HEIGHT;
    }

    int32_t newH = win->widgetRoot->calcMinH + topChrome + CHROME_TOTAL_BOTTOM;

    dvxResizeWindow(ctx, win, win->w, newH);
}


// ============================================================
// dvxFreeAccelTable
// ============================================================

void dvxFreeAccelTable(AccelTableT *table) {
    if (table) {
        free(table->entries);
    }

    free(table);
}


// ============================================================
// dvxFreeImage
// ============================================================

void dvxFreeImage(uint8_t *data) {
    free(data);
}


const BlitOpsT *dvxGetBlitOps(const AppContextT *ctx) {
    return &ctx->blitOps;
}


// ============================================================
// dvxGetColor
// ============================================================

void dvxGetColor(const AppContextT *ctx, ColorIdE id, uint8_t *r, uint8_t *g, uint8_t *b) {
    if (id < 0 || id >= ColorCountE) {
        *r = *g = *b = 0;
        return;
    }

    *r = ctx->colorRgb[id][0];
    *g = ctx->colorRgb[id][1];
    *b = ctx->colorRgb[id][2];
}


// ============================================================
// dvxGetColors
// ============================================================

const ColorSchemeT *dvxGetColors(const AppContextT *ctx) {
    return &ctx->colors;
}


// ============================================================
// dvxGetDisplay
// ============================================================

DisplayT *dvxGetDisplay(AppContextT *ctx) {
    return &ctx->display;
}


// ============================================================
// dvxGetFont
// ============================================================

const BitmapFontT *dvxGetFont(const AppContextT *ctx) {
    return &ctx->font;
}


// ============================================================
// dvxGetVideoModes
// ============================================================

const VideoModeInfoT *dvxGetVideoModes(const AppContextT *ctx, int32_t *count) {
    *count = (int32_t)arrlen(ctx->videoModes);
    return ctx->videoModes;
}


// ============================================================
// dvxInit
// ============================================================
//
// One-shot initialization of all GUI subsystems. The layered init order
// matters: video must be up before draw ops can be selected (since draw
// ops depend on pixel format), and colors must be packed after the
// display format is known.

int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) {
    memset(ctx, 0, sizeof(*ctx));

    platformInit();
    platformKeyUpInit();

    // Enumerate available video modes BEFORE setting one.  Some VBE
    // BIOSes return a stale or truncated mode list once a graphics
    // mode is active, so we must query while still in text mode.
    // (ctx was memset above, so videoModes is already NULL.)
    platformVideoEnumModes(enumModeCb, ctx);

    if (videoInit(&ctx->display, requestedW, requestedH, preferredBpp) != 0) {
        dvxLog("App: video init failed for %dx%d@%dbpp", requestedW, requestedH, preferredBpp);
        return -1;
    }

    // Draw ops are pixel-format-dependent function pointers (e.g., 16bpp
    // vs 32bpp span fill). Selected once here, then used everywhere.
    drawInit(&ctx->blitOps, &ctx->display);

    wmInit(&ctx->stack);
    dirtyListInit(&ctx->dirty);

    // 8x16 is the only font size currently supported. Fixed-width bitmap
    // fonts are used throughout because variable-width text measurement
    // would add complexity and cost on every text draw without much
    // benefit at 640x480 resolution.
    ctx->font = dvxFont8x16;

    memcpy(ctx->cursors, dvxCursors, sizeof(dvxCursors));
    ctx->cursorId = CURSOR_ARROW;

    initColorScheme(ctx);

    platformMouseInit(ctx->display.width, ctx->display.height);
    ctx->hasMouseWheel = platformMouseWheelInit();
    ctx->mouseX        = ctx->display.width / 2;
    ctx->mouseY        = ctx->display.height / 2;
    ctx->prevMouseX    = ctx->mouseX;
    ctx->prevMouseY    = ctx->mouseY;

    ctx->running            = true;
    ctx->lastIconClickId    = -1;
    ctx->lastIconClickTime  = 0;
    ctx->lastCloseClickId   = -1;
    ctx->lastCloseClickTime = 0;
    ctx->lastTitleClickId   = -1;
    ctx->lastTitleClickTime = 0;
    ctx->wheelDirection     = 1;
    ctx->wheelStep          = MOUSE_WHEEL_STEP_DEFAULT;
    ctx->dblClickTicks      = DBLCLICK_THRESHOLD;
    sDblClickTicks          = DBLCLICK_THRESHOLD;

    // Pre-compute fixed-point 16.16 reciprocal of character height so
    // popup menu item index calculation can use multiply+shift instead
    // of division. On a 486, integer divide is 40+ cycles; this
    // reciprocal trick reduces it to ~10 cycles (imul + shr).
    ctx->charHeightRecip    = ((uint32_t)FP_ONE + (uint32_t)ctx->font.charHeight - 1) / (uint32_t)ctx->font.charHeight;

    // Dirty the entire screen so the first compositeAndFlush paints everything
    dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height);

    return 0;
}


void dvxHideWindow(AppContextT *ctx, WindowT *win) {
    if (!win) {
        return;
    }

    // A hidden window must not stay registered as the modal window, or
    // the modal input gate would drop all clicks and deadlock input.
    // The callee owns this invariant (mirroring dvxDestroyWindow), and
    // it runs before the visibility early-return so the clear is
    // idempotent even for already-hidden windows.
    if (ctx->modalWindow == win) {
        ctx->modalWindow = NULL;
    }

    if (!win->visible) {
        return;
    }

    dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);
    win->visible = false;

    // Keyboard input follows focusedIdx unconditionally, so a hidden
    // window must not keep it. Move focus to the topmost visible window;
    // if there is none, drop focus entirely (blurring this window).
    if (ctx->stack.focusedIdx >= 0 && ctx->stack.windows[ctx->stack.focusedIdx] == win) {
        focusTopmostAlive(ctx);

        if (ctx->stack.windows[ctx->stack.focusedIdx] == win) {
            ctx->stack.focusedIdx = -1;
            win->focused          = false;

            if (win->onBlur) {
                WIN_CALLBACK(ctx, win, win->onBlur(win));
            }
        }
    }
}


bool dvxImageInfo(const char *path, int32_t *outW, int32_t *outH) {
    if (!path) {
        return false;
    }

    int w = 0;
    int h = 0;
    int comp = 0;

    if (stbi_info(path, &w, &h, &comp)) {
        if (outW) { *outW = w; }
        if (outH) { *outH = h; }
        return true;
    }

    return false;
}


void dvxInvalidateRect(AppContextT *ctx, WindowT *win, int32_t x, int32_t y, int32_t w, int32_t h) {
    // Convert from content-relative to screen coordinates
    int32_t screenX = win->x + win->contentX + x;
    int32_t screenY = win->y + win->contentY + y;

    dirtyListAdd(&ctx->dirty, screenX, screenY, w, h);
}


// ============================================================
// dvxInvalidateTooltip
// ============================================================
//
// Hides the on-screen tooltip when it borrows `text` (pass NULL to hide
// unconditionally).  ctx->tooltipText is a borrowed pointer: widget
// destruction and wgtSetTooltip reassignment can free the string it
// points at, so every such site must invalidate it here or the
// compositor would keep drawing freed memory until the mouse moves.

void dvxInvalidateTooltip(AppContextT *ctx, const char *text) {
    if (!ctx || !ctx->tooltipText) {
        return;
    }

    if (text && ctx->tooltipText != text) {
        return;
    }

    dirtyListAdd(&ctx->dirty, ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH);
    ctx->tooltipText = NULL;
}


// ============================================================
// dvxInvalidateWindow
// ============================================================

void dvxInvalidateWindow(AppContextT *ctx, WindowT *win) {
    // A window queued for deferred destruction is inert: its app state
    // (widget userData etc.) may already be torn down, so never paint it.
    if (win->destroyPending) {
        return;
    }

    // Call the window's paint callback to update the content buffer
    // before marking the screen dirty. This means raw-paint apps only
    // need to call dvxInvalidateWindow  -- onPaint fires automatically.
    // Set PAINT_FULL for widget-managed windows: widgetOnPaint only
    // relays out + background-clears when paintNeeded >= PAINT_FULL,
    // and callers invalidating "the whole window" (theme changes,
    // scroll, color-scheme swaps) need that full repaint rather than
    // the dirty-widget-only partial repaint the default would give.
    win->paintNeeded = PAINT_FULL;

    // Depth-bracket the callback (and the trailing writes) so an onPaint
    // that destroys its own window defers -- the writes below then land
    // on still-live memory and the flush reclaims the window afterwards.
    ctx->dispatchDepth++;

    if (win->onPaint) {
        RectT fullRect = {0, 0, win->contentW, win->contentH};
        WIN_CALLBACK(ctx, win, win->onPaint(win, &fullRect));
    }

    win->iconNeedsRefresh = true;
    dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);

    ctx->dispatchDepth--;

    if (ctx->dispatchDepth == 0 && ctx->pendingDestroyCount > 0) {
        flushPendingDestroys(ctx);
    }
}


// ============================================================
// dvxLoadImage
// ============================================================
//
// Public image loading API. Loads any image file supported by stb_image
// (BMP, PNG, JPEG, GIF, etc.) and converts the RGB pixels to the
// display's native pixel format for direct use with rectCopy, wgtImage,
// wgtImageButton, or any other pixel-data consumer. The caller owns the
// returned buffer and must free it with dvxFreeImage().

uint8_t *dvxLoadImage(const AppContextT *ctx, const char *path, int32_t *outW, int32_t *outH, int32_t *outPitch) {
    if (!ctx || !path) {
        return NULL;
    }

    int imgW;
    int imgH;
    int channels;
    uint8_t *rgb = stbi_load(path, &imgW, &imgH, &channels, RGB_CHANNELS);

    return finishLoadImage(&ctx->display, rgb, imgW, imgH, outW, outH, outPitch);
}


uint8_t *dvxLoadImageAlpha(const AppContextT *ctx, const uint8_t *data, int32_t dataLen, int32_t *outW, int32_t *outH, int32_t *outPitch, bool *outHasAlpha, uint32_t *outKeyColor) {
    if (!ctx || !data || dataLen <= 0) {
        return NULL;
    }

    const DisplayT *d = &ctx->display;

    int imgW;
    int imgH;
    int channels;
    uint8_t *rgba = stbi_load_from_memory(data, dataLen, &imgW, &imgH, &channels, RGBA_CHANNELS);

    if (!rgba) {
        return NULL;
    }

    uint32_t keyColor = packColor(d, KEY_COLOR_R, KEY_COLOR_G, KEY_COLOR_B);
    int32_t  pitch;
    uint8_t *buf      = convertRgbToNative(d, rgba, RGBA_CHANNELS, imgW, imgH, keyColor, outHasAlpha, &pitch);

    stbi_image_free(rgba);

    if (!buf) {
        return NULL;
    }

    if (outW) {
        *outW = imgW;
    }

    if (outH) {
        *outH = imgH;
    }

    if (outPitch) {
        *outPitch = pitch;
    }

    if (outKeyColor) {
        *outKeyColor = keyColor;
    }

    return buf;
}


uint8_t *dvxLoadImageFromMemory(const AppContextT *ctx, const uint8_t *data, int32_t dataLen, int32_t *outW, int32_t *outH, int32_t *outPitch) {
    if (!ctx || !data || dataLen <= 0) {
        return NULL;
    }

    int imgW;
    int imgH;
    int channels;
    uint8_t *rgb = stbi_load_from_memory(data, dataLen, &imgW, &imgH, &channels, RGB_CHANNELS);

    return finishLoadImage(&ctx->display, rgb, imgW, imgH, outW, outH, outPitch);
}


bool dvxLoadTheme(AppContextT *ctx, const char *filename) {
    FILE *fp = fopen(filename, "rb");

    if (!fp) {
        return false;
    }

    char line[256];

    while (fgets(line, sizeof(line), fp)) {
        // Strip trailing whitespace
        char *end = line + strlen(line) - 1;

        while (end >= line && (*end == '\r' || *end == '\n' || *end == ' ')) {
            *end-- = '\0';
        }

        char *p = (char *)dvxSkipWs(line);

        // Skip comments, blank lines, section headers
        if (*p == '\0' || *p == ';' || *p == '#' || *p == '[') {
            continue;
        }

        // Parse key = r,g,b
        char *eq = strchr(p, '=');

        if (!eq) {
            continue;
        }

        *eq = '\0';

        // Trim key
        char *key = p;
        char *keyEnd = eq - 1;

        while (keyEnd >= key && (*keyEnd == ' ' || *keyEnd == '\t')) {
            *keyEnd-- = '\0';
        }

        // Parse r,g,b
        const char *val = dvxSkipWs(eq + 1);

        int r;
        int g;
        int b;

        if (sscanf(val, "%d,%d,%d", &r, &g, &b) != 3) {
            continue;
        }

        // Find matching color name
        for (int32_t i = 0; i < ColorCountE; i++) {
            if (strcmp(key, sColorNames[i]) == 0) {
                ctx->colorRgb[i][0] = (uint8_t)clampInt(r, 0, UINT8_MAX);
                ctx->colorRgb[i][1] = (uint8_t)clampInt(g, 0, UINT8_MAX);
                ctx->colorRgb[i][2] = (uint8_t)clampInt(b, 0, UINT8_MAX);
                break;
            }
        }
    }

    fclose(fp);
    dvxApplyColorScheme(ctx);
    return true;
}


// ============================================================
// dvxMaximizeWindow
// ============================================================

void dvxMaximizeWindow(AppContextT *ctx, WindowT *win) {
    wmMaximize(&ctx->stack, &ctx->dirty, &ctx->display, win);
}


// ============================================================
// dvxMinimizeWindow
// ============================================================

void dvxMinimizeWindow(AppContextT *ctx, WindowT *win) {
    wmMinimize(&ctx->stack, &ctx->dirty, win);

    // wmMinimize only dirties the window's former rect; the icon strip
    // must be dirtied too or the new minimized icon never composites
    // (matches the sys-menu and minimize-gadget interactive paths).
    int32_t iconY;
    int32_t iconH;
    wmMinimizedIconRect(&ctx->stack, &ctx->display, &iconY, &iconH);
    dirtyListAdd(&ctx->dirty, 0, iconY, ctx->display.width, iconH);
}


// ============================================================
// dvxQuit
// ============================================================

void dvxQuit(AppContextT *ctx) {
    ctx->running = false;
}


void dvxRaiseWindow(AppContextT *ctx, WindowT *win) {
    // A window queued for deferred destruction is INERT: raising it would
    // refocus it and fire callbacks into torn-down app state.
    if (!win || win->destroyPending) {
        return;
    }

    for (int32_t i = 0; i < ctx->stack.count; i++) {
        if (ctx->stack.windows[i] == win) {
            wmRaiseWindow(&ctx->stack, &ctx->dirty, i);
            wmSetFocus(&ctx->stack, &ctx->dirty, ctx->stack.count - 1);
            return;
        }
    }
}


void dvxResetColorScheme(AppContextT *ctx) {
    memcpy(ctx->colorRgb, sDefaultColors, sizeof(sDefaultColors));
    dvxApplyColorScheme(ctx);
}


void dvxResizeWindow(AppContextT *ctx, WindowT *win, int32_t newW, int32_t newH) {
    // Dirty old position
    dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);

    // Resize
    win->w = newW;
    win->h = newH;

    // Shift position so the window stays fully on screen
    if (win->x + newW > ctx->display.width) {
        win->x = ctx->display.width - newW;
    }

    if (win->y + newH > ctx->display.height) {
        win->y = ctx->display.height - newH;
    }

    if (win->x < 0) {
        win->x = 0;
    }

    if (win->y < 0) {
        win->y = 0;
    }

    // Commit the new geometry eagerly (content rect, buffer, onResize) but
    // DEFER the repaint: paintNeeded = PAINT_FULL lets the deferred-paint
    // flush in dvxUpdate coalesce repeated programmatic resizes (e.g. a
    // BASIC form setting Width then Height in one tick) into a single
    // paint per frame.  The flush fires onPaint for raw-paint windows too,
    // so they are not left blank like the old wgtInvalidate-only path.
    // Depth-bracketed: onResize may destroy this window; the bracket
    // defers the destroy past the trailing writes so they never touch
    // freed memory.
    ctx->dispatchDepth++;

    wmUpdateContentRect(win);

    if (wmReallocContentBuf(win, &ctx->display) == 0) {
        if (win->onResize) {
            WIN_CALLBACK(ctx, win, win->onResize(win, win->contentW, win->contentH));
        }

        win->paintNeeded = PAINT_FULL;
    }

    // Dirty new position
    dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);

    ctx->dispatchDepth--;

    if (ctx->dispatchDepth == 0 && ctx->pendingDestroyCount > 0) {
        flushPendingDestroys(ctx);
    }
}


void *dvxResLoadData(const char *dxePath, const char *resName, uint32_t *outSize) {
    if (!dxePath || !resName) {
        return NULL;
    }

    DvxResHandleT *res = dvxResOpen(dxePath);

    if (!res) {
        return NULL;
    }

    void *data = dvxResRead(res, resName, outSize);
    dvxResClose(res);
    return data;
}


uint8_t *dvxResLoadIcon(AppContextT *ctx, const char *dxePath, const char *resName, int32_t *outW, int32_t *outH, int32_t *outPitch) {
    if (!ctx || !dxePath || !resName) {
        return NULL;
    }

    DvxResHandleT *res = dvxResOpen(dxePath);

    if (!res) {
        return NULL;
    }

    uint32_t size = 0;
    void *buf = dvxResRead(res, resName, &size);
    dvxResClose(res);

    if (!buf) {
        return NULL;
    }

    uint8_t *pixels = dvxLoadImageFromMemory(ctx, (const uint8_t *)buf, (int32_t)size, outW, outH, outPitch);
    free(buf);
    return pixels;
}


bool dvxResLoadText(const char *dxePath, const char *resName, char *buf, int32_t bufSize) {
    if (!dxePath || !resName || !buf || bufSize <= 0) {
        return false;
    }

    DvxResHandleT *res = dvxResOpen(dxePath);

    if (!res) {
        return false;
    }

    uint32_t size = 0;
    void *data = dvxResRead(res, resName, &size);
    dvxResClose(res);

    if (!data) {
        return false;
    }

    int32_t copyLen = (int32_t)size < bufSize - 1 ? (int32_t)size : bufSize - 1;
    memcpy(buf, data, copyLen);
    buf[copyLen] = '\0';
    free(data);
    return true;
}


void dvxRun(AppContextT *ctx) {
    while (dvxUpdate(ctx)) {
        // dvxUpdate returns false when the GUI wants to exit
    }
}


// ============================================================
// dvxSaveImage
// ============================================================
//
// Save native-format pixel data to a PNG file. Converts from the
// display's native pixel format to RGB, then encodes as PNG via
// stb_image_write. This is the general-purpose image save function;
// dvxScreenshot and dvxWindowScreenshot are convenience wrappers
// around it for common use cases.

int32_t dvxSaveImage(const AppContextT *ctx, const uint8_t *data, int32_t w, int32_t h, int32_t pitch, const char *path) {
    if (!ctx || !data || !path || w <= 0 || h <= 0) {
        return -1;
    }

    uint8_t *rgb = bufferToRgb(&ctx->display, data, w, h, pitch);

    if (!rgb) {
        return -1;
    }

    int32_t result = stbi_write_png(path, w, h, RGB_CHANNELS, rgb, w * RGB_CHANNELS) ? 0 : -1;

    free(rgb);

    return result;
}


// ============================================================
// dvxSaveTheme
// ============================================================

bool dvxSaveTheme(const AppContextT *ctx, const char *filename) {
    FILE *fp = fopen(filename, "wb");

    if (!fp) {
        return false;
    }

    fprintf(fp, "; DVX Color Theme\r\n\r\n[colors]\r\n");

    for (int32_t i = 0; i < ColorCountE; i++) {
        fprintf(fp, "%-20s = %d,%d,%d\r\n", sColorNames[i], ctx->colorRgb[i][0], ctx->colorRgb[i][1], ctx->colorRgb[i][2]);
    }

    fclose(fp);
    return true;
}


// ============================================================
// dvxScreenshot
// ============================================================
//
// Save the entire screen (backbuffer) to a PNG file. Uses the backbuffer
// rather than the LFB because reading from video memory through PCI/ISA
// is extremely slow on period hardware (uncacheable MMIO reads). The
// backbuffer is in system RAM and is always coherent with the LFB since
// we only write to the LFB, never read.

int32_t dvxScreenshot(AppContextT *ctx, const char *path) {
    DisplayT *d = &ctx->display;
    int32_t   result;

    dvxSetBusy(ctx, true);
    result = dvxSaveImage(ctx, d->backBuf, d->width, d->height, d->pitch, path);
    dvxSetBusy(ctx, false);

    return result;
}


// ============================================================
// dvxSetBusy
// ============================================================

void dvxSetBusy(AppContextT *ctx, bool busy) {
    ctx->busy = busy;

    if (busy) {
        ctx->cursorId = CURSOR_BUSY;
    } else {
        ctx->cursorId = CURSOR_ARROW;
    }

    // dirtyCursorArea covers the worst-case union of all cursor shapes at the
    // pointer position, so a single call erases the old shape and exposes the
    // new one (both are drawn at the same mouseX/mouseY).
    dirtyCursorArea(ctx, ctx->mouseX, ctx->mouseY);

    // Flush immediately so the cursor change is visible before
    // the blocking operation starts (or after it ends).
    // Skip if display isn't initialized yet (startup wallpaper load).
    if (ctx->display.backBuf) {
        compositeAndFlush(ctx);
    }
}


// ============================================================
// dvxSetColor
// ============================================================

void dvxSetColor(AppContextT *ctx, ColorIdE id, uint8_t r, uint8_t g, uint8_t b) {
    if (id < 0 || id >= ColorCountE) {
        return;
    }

    ctx->colorRgb[id][0] = r;
    ctx->colorRgb[id][1] = g;
    ctx->colorRgb[id][2] = b;

    uint32_t packed = packColor(&ctx->display, r, g, b);
    *colorSlot(&ctx->colors, id) = packed;

    invalidateAllWindows(ctx);
}


// ============================================================
// dvxSetMouseConfig
// ============================================================

void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, int32_t accelThreshold, int32_t mickeyRatio, int32_t wheelStep) {
    ctx->wheelDirection = (wheelDir < 0) ? -1 : 1;
    ctx->dblClickTicks  = (clock_t)dblClickMs * CLOCKS_PER_SEC / 1000;
    sDblClickTicks      = ctx->dblClickTicks;

    if (wheelStep < MOUSE_WHEEL_STEP_MIN) {
        wheelStep = MOUSE_WHEEL_STEP_MIN;
    }

    if (wheelStep > MOUSE_WHEEL_STEP_MAX) {
        wheelStep = MOUSE_WHEEL_STEP_MAX;
    }

    ctx->wheelStep = wheelStep;

    // Cache the applied values on the context so callers (e.g. the control
    // panel snapshot) read live state instead of re-parsing the INI, keeping
    // a single source of truth. A non-positive value means "leave unchanged",
    // so the previously applied value is preserved.
    if (accelThreshold > 0) {
        ctx->accelThreshold = accelThreshold;
        platformMouseSetAccel(accelThreshold);
    }

    if (mickeyRatio > 0) {
        ctx->mickeyRatio = mickeyRatio;
        platformMouseSetMickeys(mickeyRatio, mickeyRatio);
    }
}


// ============================================================
// dvxSetTitle
// ============================================================

void dvxSetTitle(AppContextT *ctx, WindowT *win, const char *title) {
    wmSetTitle(win, &ctx->dirty, title);

    if (ctx->onTitleChange) {
        ctx->onTitleChange(ctx->titleChangeCtx);
    }
}


// ============================================================
// dvxSetWallpaper
// ============================================================

bool dvxSetWallpaper(AppContextT *ctx, const char *path) {
    // buildWallpaperBuf pumps dvxUpdate to keep the UI alive, and a timer
    // or idle callback fired from there may call back in here. A nested
    // call would free the buffer being built and clear busy early, so
    // reject it outright.
    static bool sBuilding = false;

    if (sBuilding) {
        return false;
    }

    free(ctx->wallpaperBuf);
    ctx->wallpaperBuf   = NULL;
    ctx->wallpaperPitch = 0;

    if (!path) {
        ctx->wallpaperPath[0] = '\0';
        dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height);
        return true;
    }

    strncpy(ctx->wallpaperPath, path, sizeof(ctx->wallpaperPath) - 1);
    ctx->wallpaperPath[sizeof(ctx->wallpaperPath) - 1] = '\0';

    dvxSetBusy(ctx, true);

    int imgW;
    int imgH;
    int channels;
    uint8_t *rgb = stbi_load(path, &imgW, &imgH, &channels, 3);

    if (!rgb) {
        dvxSetBusy(ctx, false);
        return false;
    }

    int32_t pitch = ctx->display.width * ctx->display.format.bytesPerPixel;

    sBuilding           = true;
    ctx->wallpaperBuf   = buildWallpaperBuf(ctx, rgb, imgW, imgH, ctx->wallpaperMode);
    ctx->wallpaperPitch = pitch;
    sBuilding           = false;
    dvxSetBusy(ctx, false);

    stbi_image_free(rgb);

    if (!ctx->wallpaperBuf) {
        return false;
    }

    dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height);
    return true;
}


// ============================================================
// dvxSetWallpaperMode
// ============================================================

void dvxSetWallpaperMode(AppContextT *ctx, WallpaperModeE mode) {
    ctx->wallpaperMode = mode;

    if (ctx->wallpaperPath[0]) {
        dvxSetWallpaper(ctx, ctx->wallpaperPath);
    }
}


// ============================================================
// dvxSetWindowIcon
// ============================================================

int32_t dvxSetWindowIcon(AppContextT *ctx, WindowT *win, const char *path) {
    return wmSetIcon(win, path, &ctx->display);
}


void dvxShowWindow(AppContextT *ctx, WindowT *win) {
    // A window queued for deferred destruction is INERT: re-showing it
    // would let hit-testing and focus fire callbacks into app state that
    // may already be torn down.
    if (!win || win->visible || win->destroyPending) {
        return;
    }

    win->visible = true;
    dvxInvalidateWindow(ctx, win);
}


void dvxShutdown(AppContextT *ctx) {
    platformKeyUpShutdown();

    // Destroy all remaining windows
    while (ctx->stack.count > 0) {
        wmDestroyWindow(&ctx->stack, ctx->stack.windows[ctx->stack.count - 1]);
    }

    free(ctx->stack.windows);
    ctx->stack.windows = NULL;
    ctx->stack.count   = 0;
    ctx->stack.cap     = 0;

    free(ctx->popup.parentStack);
    ctx->popup.parentStack = NULL;
    ctx->popup.parentCap   = 0;

    free(ctx->dirty.rects);
    ctx->dirty.rects = NULL;
    ctx->dirty.count = 0;
    ctx->dirty.cap   = 0;

    free(ctx->wallpaperBuf);
    ctx->wallpaperBuf = NULL;
    arrfree(ctx->videoModes);
    ctx->videoModes = NULL;
    videoShutdown(&ctx->display);
}


uint32_t dvxTextHash(const char *text) {
    if (!text) {
        return 0;
    }

    uint32_t h = 5381;

    while (*text) {
        h = ((h << 5) + h) ^ (uint8_t)*text;
        text++;
    }

    return h;
}


// Tile windows in a grid. The grid dimensions are chosen so columns =
// ceil(sqrt(n)), which produces a roughly square grid. This is better than
// always using rows or columns because it maximizes the minimum dimension
// of each tile (a 1xN or Nx1 layout makes windows very narrow or short).
// The last row may have fewer windows; those get wider tiles to fill the
// remaining screen width, avoiding dead space.
//
// The integer sqrt is computed by a simple loop rather than calling sqrt()
// to avoid pulling in floating-point math on DJGPP targets.

void dvxTileWindows(AppContextT *ctx) {
    int32_t screenW = ctx->display.width;
    int32_t screenH = ctx->display.height;
    int32_t count   = countVisibleWindows(ctx);

    if (count == 0) {
        return;
    }

    // Integer ceil(sqrt(count)) for column count
    int32_t cols = 1;

    while (cols * cols < count) {
        cols++;
    }

    int32_t rows = (count + cols - 1) / cols;
    int32_t tileW = screenW / cols;
    int32_t tileH = screenH / rows;

    if (tileW < MIN_WINDOW_W) {
        tileW = MIN_WINDOW_W;
    }

    if (tileH < MIN_WINDOW_H) {
        tileH = MIN_WINDOW_H;
    }

    int32_t slot = 0;

    for (int32_t i = 0; i < ctx->stack.count; i++) {
        WindowT *win = ctx->stack.windows[i];

        if (win->minimized || !win->visible) {
            continue;
        }

        int32_t row = slot / cols;
        int32_t col = slot % cols;

        // Last row: fewer windows get wider tiles
        int32_t remaining = count - row * cols;
        int32_t rowCols   = (remaining < cols) ? remaining : cols;
        int32_t cellW     = screenW / rowCols;

        repositionWindow(ctx, win, col * cellW, row * tileH, cellW, tileH);

        slot++;
    }
}


// ============================================================
// dvxTileWindowsH
// ============================================================
//
// Horizontal tiling: windows side by side left to right, each the full
// screen height. Good for comparing two documents or viewing output
// alongside source. With many windows the tiles become very narrow, but
// MIN_WINDOW_W prevents them from becoming unusably small.

void dvxTileWindowsH(AppContextT *ctx) {
    int32_t screenW = ctx->display.width;
    int32_t screenH = ctx->display.height;
    int32_t count   = countVisibleWindows(ctx);

    if (count == 0) {
        return;
    }

    int32_t tileW = screenW / count;

    if (tileW < MIN_WINDOW_W) {
        tileW = MIN_WINDOW_W;
    }

    int32_t slot = 0;

    for (int32_t i = 0; i < ctx->stack.count; i++) {
        WindowT *win = ctx->stack.windows[i];

        if (win->minimized || !win->visible) {
            continue;
        }

        repositionWindow(ctx, win, slot * tileW, 0, tileW, screenH);

        slot++;
    }
}


// ============================================================
// dvxTileWindowsV
// ============================================================
//
// Vertical tiling: windows stacked top to bottom, each the full screen
// width. The complement of dvxTileWindowsH.

void dvxTileWindowsV(AppContextT *ctx) {
    int32_t screenW = ctx->display.width;
    int32_t screenH = ctx->display.height;
    int32_t count   = countVisibleWindows(ctx);

    if (count == 0) {
        return;
    }

    int32_t tileH = screenH / count;

    if (tileH < MIN_WINDOW_H) {
        tileH = MIN_WINDOW_H;
    }

    int32_t slot = 0;

    for (int32_t i = 0; i < ctx->stack.count; i++) {
        WindowT *win = ctx->stack.windows[i];

        if (win->minimized || !win->visible) {
            continue;
        }

        repositionWindow(ctx, win, 0, slot * tileH, screenW, tileH);

        slot++;
    }
}


// ============================================================
// dvxUpdate
// ============================================================
//
// Single iteration of the main event loop. This is the fundamental
// heartbeat of the GUI. The sequence is:
//   1. Poll hardware (mouse position/buttons, keyboard buffer)
//   2. Dispatch events (route input to windows, menus, widgets)
//   3. Update tooltip visibility
//   4. Poll ANSI terminal widgets (check for new data from PTYs)
//   5. Periodic tasks (minimized icon thumbnail refresh)
//   6. Composite dirty regions and flush to LFB
//   7. If nothing was dirty: run idle callback or yield CPU
//
// The idle callback mechanism exists so applications can do background
// work (e.g., polling serial ports, processing network data) when the
// GUI has nothing to paint. Without it, the loop would busy-wait or
// yield the CPU slice. With it, the application gets a callback to do
// useful work. platformYield is the fallback  -- it calls INT 28h (DOS
// idle) or SDL_Delay (Linux) to avoid burning CPU when truly idle.

bool dvxUpdate(AppContextT *ctx) {
    if (!ctx->running) {
        return false;
    }

    // Everything inside this depth-tracked section may run user
    // callbacks.  dvxDestroyWindow defers actual teardown while
    // dispatchDepth > 0 (see deferDestroyWindow); nested dvxUpdate
    // calls from modal dialog loops stack the depth, so the flush only
    // happens once every callback frame has unwound.
    ctx->dispatchDepth++;

    pollMouse(ctx);
    pollKeyboard(ctx);
    dispatchEvents(ctx);
    updateTooltip(ctx);
    pollWidgets(ctx);
    if (sCursorBlinkFn) {
        sCursorBlinkFn();
    }

    wgtUpdateTimers();

    // Flush deferred paints. paintNeeded is set by wgtInvalidatePaint
    // (PARTIAL) or wgtInvalidate (FULL). Multiple calls per frame are
    // batched into one paint -- the highest level wins.  This loop runs
    // INSIDE the depth-tracked section so an onPaint that destroys its
    // own window defers: the paintNeeded/icon writes below then land on
    // live memory and the stack is never compacted mid-iteration.
    // Deferred-destroyed windows are skipped -- their app state may
    // already be torn down (this also covers nested dvxUpdate loops,
    // where the flush below is suppressed).
    for (int32_t i = 0; i < ctx->stack.count; i++) {
        WindowT *win = ctx->stack.windows[i];

        if (win->destroyPending) {
            continue;
        }

        if (win->paintNeeded && win->onPaint) {
            RectT fullRect = {0, 0, win->contentW, win->contentH};
            WIN_CALLBACK(ctx, win, win->onPaint(win, &fullRect));
            // widgetOnPaint clears paintNeeded for widget windows.
            // For raw-paint windows, clear it here.
            win->paintNeeded      = PAINT_NONE;
            win->iconNeedsRefresh = true;
            dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);
        }
    }

    ctx->dispatchDepth--;

    if (ctx->dispatchDepth == 0 && ctx->pendingDestroyCount > 0) {
        flushPendingDestroys(ctx);
    }

    ctx->frameCount++;

    if (ctx->frameCount % ICON_REFRESH_INTERVAL == 0) {
        refreshMinimizedIcons(ctx);
    }

    if (ctx->dirty.count > 0) {
        compositeAndFlush(ctx);
    } else if (ctx->idleCallback) {
        ctx->idleCallback(ctx->idleCtx);
    } else {
        platformYield();
    }

    // Release key-pressed button after one frame. The button was set to
    // "pressed" state in dispatchAccelKey; here we clear it and fire
    // onClick. The one-frame delay ensures the pressed visual state
    // renders before the callback runs (which may open a dialog, etc.).
    if (sKeyPressedBtn) {
        if (wclsHas(sKeyPressedBtn, WGT_METHOD_ON_DRAG_END)) {
            // Pass button center as coordinates so bounds check succeeds and onClick fires
            WidgetT *root = sKeyPressedBtn->window ? sKeyPressedBtn->window->widgetRoot : sKeyPressedBtn;
            wclsOnDragEnd(sKeyPressedBtn, root,
                          sKeyPressedBtn->x + sKeyPressedBtn->w / 2,
                          sKeyPressedBtn->y + sKeyPressedBtn->h / 2);
        }

        wgtInvalidatePaint(sKeyPressedBtn);
        sKeyPressedBtn = NULL;
    }

    ctx->prevMouseX       = ctx->mouseX;
    ctx->prevMouseY       = ctx->mouseY;
    ctx->prevMouseButtons = ctx->mouseButtons;

    return ctx->running;
}


// ============================================================
// dvxWindowScreenshot
// ============================================================
//
// Save a window's content buffer to a PNG file. Because each window has
// its own persistent content buffer (not a shared backbuffer), this
// captures the full content even if the window is partially or fully
// occluded by other windows. This is a unique advantage of the per-window
// content buffer architecture.

int32_t dvxWindowScreenshot(AppContextT *ctx, WindowT *win, const char *path) {
    int32_t result;

    if (!win || !win->contentBuf) {
        return -1;
    }

    dvxSetBusy(ctx, true);
    result = dvxSaveImage(ctx, win->contentBuf, win->contentW, win->contentH, win->contentPitch, path);
    dvxSetBusy(ctx, false);

    return result;
}
