// 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.

// dvx_wm.c  -- Layer 4: Window manager for DVX GUI
//
// This layer manages the window stack (z-order), window chrome rendering
// (title bars, borders, bevels, gadgets, menu bars, scrollbars), and user
// interactions like drag, resize, minimize, maximize, and focus. It sits
// between the compositor (layer 3) and the application event loop (layer 5).
//
// Architecture decisions:
//
// - Z-order is a simple pointer array (windows[0] = back, windows[count-1]
//   = front). This was chosen over a linked list because hit-testing walks
//   the stack front-to-back every mouse event, and array iteration has
//   better cache behavior on 486/Pentium. Raising a window is O(N) shift
//   but N is bounded by the window count and raise is infrequent.
//
// - Window chrome uses a Motif/GEOS Ensemble visual style with fixed 4px
//   outer bevels and 2px inner bevels. The fixed bevel widths avoid per-
//   window style negotiation and let chrome geometry be computed with simple
//   arithmetic from the frame rect, shared between drawing and hit-testing
//   via computeTitleGeom().
//
// - Each window has its own content backbuffer (contentBuf). The WM blits
//   this to the display backbuffer during compositing. This means window
//   content survives being occluded  -- apps don't get expose events and don't
//   need to repaint when uncovered, which hugely simplifies app code and
//   avoids the latency of synchronous repaint-on-expose.
//
// - Scrollbar and menu bar state are allocated on-demand (NULL if absent).
//   This keeps the base WindowT struct small for simple windows while
//   supporting rich chrome when needed.

#include "dvxWm.h"
#include "dvxVideo.h"
#include "dvxDraw.h"
// This TU owns the single definition of the shared 8x16 font table/descriptor.
#define DVX_FONT_IMPL
#include "dvxFont.h"
#include "dvxComp.h"
#include "dvxWgt.h"
#include "dvxPlat.h"
#include "thirdparty/stb_image_wrap.h"

#include <stdlib.h>
#include <string.h>
#include "dvxMem.h"

// ============================================================
// Constants
// ============================================================

// Title bar gadget layout constants. These mirror the GEOS Ensemble/Motif
// look: small beveled squares inset from the title bar edges, with specific
// icon sizes that remain legible at the 16px gadget size (CHROME_TITLE_HEIGHT
// minus 2*GADGET_INSET).
#define GADGET_PAD          2
#define GADGET_INSET        2     // inset from title bar edges
#define MENU_BAR_GAP        8     // horizontal gap between menu bar labels
#define RESIZE_BREAK_INSET  16    // distance from corner to resize groove
#define RESIZE_GRAB_SLOP    2     // extra px beyond visual border for easier edge grab
#define CLOSE_ICON_INSET    3     // close bar inset from gadget edges
#define MAXIMIZE_ICON_INSET 2     // maximize box inset from gadget edges
#define RESTORE_ICON_INSET  4     // restore box inset from gadget edges
#define MINIMIZE_ICON_SIZE  4     // filled square size for minimize icon
#define DRAG_DEADZONE       4     // wmDragMove: pixels before drag activates
#define CONTENT_CLEAR_BYTE  0xFF  // content buffer fill: clean white background
#define MENU_INIT_CAP       8     // initial capacity of menu item / menu bar arrays
#define STACK_INIT_CAP      16    // initial capacity of the window stack array

// Title bar gadget square size, shared by geometry, drawing, and min-size.
#define GADGET_SIZE         (CHROME_TITLE_HEIGHT - GADGET_INSET * 2)

// ============================================================
// Title bar gadget geometry
// ============================================================
//
// Extracted into a struct so that both drawTitleBar() and wmHitTest() compute
// identical geometry from the same code path (computeTitleGeom). Without this,
// a discrepancy between draw and hit-test coordinates would cause clicks to
// land on the wrong gadget  -- a subtle bug that's hard to reproduce visually.

typedef struct {
    int32_t titleX;
    int32_t titleY;
    int32_t titleW;
    int32_t titleH;
    int32_t gadgetS;       // gadget square size
    int32_t gadgetY;       // gadget Y position
    int32_t closeX;        // close gadget X
    int32_t minX;          // minimize gadget X
    int32_t maxX;          // maximize gadget X (-1 if not resizable)
    int32_t textLeftEdge;  // left edge of title text area
    int32_t textRightEdge; // right edge of title text area
} TitleGeomT;

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

static RectT clipSave(const DisplayT *d);
static void clipRestore(DisplayT *d, const RectT *saved);
static void computeMenuBarPositions(WindowT *win, const BitmapFontT *font);
static void computeTitleGeom(const WindowT *win, TitleGeomT *g);
static void copyLabel(char *dst, const char *src, size_t cap);
static void drawBorderFrame(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t w, int32_t h);
static void drawMenuBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, WindowT *win);
static void drawResizeBreaks(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, WindowT *win);
static void drawScaledRect(DisplayT *d, int32_t dstX, int32_t dstY, int32_t dstW, int32_t dstH, const uint8_t *src, int32_t srcW, int32_t srcH, int32_t srcPitch, int32_t bpp);
static void drawTitleBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, WindowT *win);
static void drawTitleGadget(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size);
static void freeMenuItemSubMenu(MenuItemT *item);
static void freeMenuRecursive(MenuT *menu);
static bool menuGrowItems(MenuT *menu);
static int32_t menuLabelWidth(const BitmapFontT *font, const char *label);
static MenuItemT *menuNewItem(MenuT *menu, const char *label);
static ScrollbarT *scrollbarAdd(WindowT *win, ScrollbarT **slot, ScrollbarOrientE orient, const char *orientName, int32_t min, int32_t max, int32_t pageSize);
static void scrollbarCommitValue(WindowT *win, ScrollbarT *sb, DirtyListT *dl, int32_t sbScreenX, int32_t sbScreenY, int32_t oldValue);
static int32_t scrollbarThumbInfo(const ScrollbarT *sb, int32_t *thumbPos, int32_t *thumbSize);
static int32_t wmAdjustIndexForRaise(int32_t index, int32_t raisedSlot, int32_t newTop);
static int32_t wmAdjustIndexForRemoval(int32_t index, int32_t removedSlot);
static void wmDrawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, const ScrollbarT *sb, int32_t winX, int32_t winY);
static void wmEffectiveMaxSize(const WindowT *win, const DisplayT *d, int32_t *maxW, int32_t *maxH);
static MenuItemT *wmMenuFindItem(MenuBarT *bar, int32_t id, MenuT **outMenu);
static MenuItemT *wmMenuFindItemRecursive(MenuT *menu, int32_t id, MenuT **outMenu);


// Restores a clip rect captured by clipSave.
static void clipRestore(DisplayT *d, const RectT *saved) {
    setClipRect(d, saved->x, saved->y, saved->w, saved->h);
}


// Captures the display's current clip rect so a caller that tightens the
// clip for its own drawing can put it back for the caller above it.
static RectT clipSave(const DisplayT *d) {
    RectT r = { d->clipX, d->clipY, d->clipW, d->clipH };

    return r;
}


// Lays out menu bar label positions left-to-right. Each label gets
// padding (CHROME_TITLE_PAD) on both sides and MENU_BAR_GAP between labels.
// Positions are cached and only recomputed when positionsDirty is set (after
// adding/removing a menu), avoiding redundant textWidthAccel calls on every
// paint  -- font metric calculation is expensive relative to a simple flag check.
// barX values are relative to the window, not the screen; the draw path adds
// the window's screen position.
static void computeMenuBarPositions(WindowT *win, const BitmapFontT *font) {
    if (!win->menuBar) {
        return;
    }

    if (!win->menuBar->positionsDirty) {
        return;
    }

    int32_t x = CHROME_TOTAL_SIDE;

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

        menu->barX = x;
        menu->barW = labelW;
        x += labelW + MENU_BAR_GAP;
    }

    win->menuBar->positionsDirty = false;
}


// Compute title bar gadget positions. Used by both drawTitleBar()
// and wmHitTest() to keep geometry in sync.
//
// Layout follows the Motif + GEOS Ensemble convention:
//   [Close] ---- Title Text ---- [Minimize] [Maximize]
// Close is on the far left (also doubles as system menu),
// minimize and maximize pack right-to-left from the far right edge.
// Modal windows suppress the minimize gadget since they must stay visible.
// Non-resizable windows suppress the maximize gadget.
// The text area occupies whatever space remains between the gadgets,
// with the title centered within that span.

static void computeTitleGeom(const WindowT *win, TitleGeomT *g) {
    g->titleX  = win->x + CHROME_BORDER_WIDTH;
    g->titleY  = win->y + CHROME_BORDER_WIDTH;
    g->titleW  = win->w - CHROME_BORDER_WIDTH * 2;
    g->titleH  = CHROME_TITLE_HEIGHT;
    g->gadgetS = GADGET_SIZE;
    g->gadgetY = g->titleY + GADGET_INSET;

    g->closeX = g->titleX + GADGET_PAD;

    // Pack right-side gadgets from right edge inward
    int32_t rightX = g->titleX + g->titleW - GADGET_PAD - g->gadgetS;

    if (win->resizable) {
        g->maxX = rightX;
        rightX -= g->gadgetS + GADGET_PAD;
    } else {
        g->maxX = -1;
    }

    if (win->modal) {
        g->minX = -1;
    } else {
        g->minX = rightX;
        rightX -= g->gadgetS + GADGET_PAD;
    }

    // Text area is the remaining span between close and rightmost gadgets
    g->textLeftEdge  = g->closeX + g->gadgetS + GADGET_PAD;
    g->textRightEdge = rightX + g->gadgetS;
}


// Copies a caller-supplied title/label into a fixed buffer, always NUL-
// terminating and treating a NULL source (a DXE/BASIC app passing an unset
// string) as the empty string.
static void copyLabel(char *dst, const char *src, size_t cap) {
    if (!src) {
        src = "";
    }

    strncpy(dst, src, cap - 1);
    dst[cap - 1] = '\0';
}


// 4px raised Motif-style border using 3 shades:
//   highlight (outer top/left), face (middle), shadow (outer bottom/right)
// with inner crease lines for 3D depth.
//
// Cross-section of the top edge (4 rows, outside to inside):
//   Row 0: highlight  (bright edge catches the "light" from top-left)
//   Row 1: highlight  (doubled for visual weight at low resolutions)
//   Row 2: face       (flat middle band)
//   Row 3: shadow     (inner crease  -- makes the border feel like two beveled
//                       ridges rather than one flat edge)
//
// Bottom/right are mirror-reversed (shadow, shadow, face, highlight). This
// crease pattern is what distinguishes the Motif "ridge" look from a simple
// 2-shade bevel. The 4px width was chosen because it's the minimum that
// shows the crease effect clearly at 640x480; narrower borders lose the
// middle face band and look flat.
//
// Each edge is drawn as individual 1px rectFill calls rather than a single
// drawBevel call because the 3-shade crease pattern doesn't map to the
// 2-shade BevelStyleT. The cost is 16 rectFill calls + 1 interior fill,
// which is negligible since border drawing is clipped to dirty rects.

static void drawBorderFrame(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t w, int32_t h) {
    uint32_t hi   = colors->windowHighlight;
    uint32_t face = colors->windowFace;
    uint32_t sh   = colors->windowShadow;

    // Fill interior with face color
    rectFill(d, ops, x + CHROME_BORDER_WIDTH, y + CHROME_BORDER_WIDTH, w - CHROME_BORDER_WIDTH * 2, h - CHROME_BORDER_WIDTH * 2, face);

    // Top edge (4 rows)
    rectFill(d, ops, x,     y,     w,     1, hi);    // row 0: highlight
    rectFill(d, ops, x + 1, y + 1, w - 2, 1, hi);   // row 1: highlight
    rectFill(d, ops, x + 2, y + 2, w - 4, 1, face);  // row 2: face
    rectFill(d, ops, x + 3, y + 3, w - 6, 1, sh);    // row 3: shadow (inner crease)

    // Left edge (4 cols)
    rectFill(d, ops, x,     y + 1, 1, h - 1, hi);
    rectFill(d, ops, x + 1, y + 2, 1, h - 3, hi);
    rectFill(d, ops, x + 2, y + 3, 1, h - 5, face);
    rectFill(d, ops, x + 3, y + 4, 1, h - 7, sh);

    // Bottom edge (4 rows)
    rectFill(d, ops, x,     y + h - 1, w,     1, sh);   // row 0: shadow
    rectFill(d, ops, x + 1, y + h - 2, w - 2, 1, sh);  // row 1: shadow
    rectFill(d, ops, x + 2, y + h - 3, w - 4, 1, face); // row 2: face
    rectFill(d, ops, x + 3, y + h - 4, w - 6, 1, hi);   // row 3: highlight (inner crease)

    // Right edge (4 cols)
    rectFill(d, ops, x + w - 1, y + 1, 1, h - 2, sh);
    rectFill(d, ops, x + w - 2, y + 2, 1, h - 4, sh);
    rectFill(d, ops, x + w - 3, y + 3, 1, h - 6, face);
    rectFill(d, ops, x + w - 4, y + 4, 1, h - 8, hi);
}


// Renders the menu bar strip immediately below the title bar. The menu bar
// lives inside the window's outer border but above the inner sunken bevel
// and content area.
//
// The clip rect is narrowed to the menu bar area before drawing labels so
// that long label text doesn't bleed into the window border. The clip is
// saved/restored rather than set to the full dirty rect because this
// function is called from wmDrawChrome which has already set the clip to
// the dirty rect  -- we need to intersect with both. The separator line at
// the bottom visually separates the menu bar from the content area.

static void drawMenuBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, WindowT *win) {
    if (!win->menuBar) {
        return;
    }

    int32_t barY = win->y + CHROME_TITLEBAR_BOTTOM;
    int32_t barH = CHROME_MENU_HEIGHT;

    rectFill(d, ops, win->x + CHROME_BORDER_WIDTH, barY,
             win->w - CHROME_BORDER_WIDTH * 2, barH, colors->menuBg);

    // Tighten clip to the menu bar bounds INTERSECTED with the incoming dirty
    // rect (already set by wmDrawChrome) so labels never paint outside either
    // region.  setClipRect only clamps to screen bounds, it does not intersect
    // with the prior clip, so we intersect against it with the shared
    // rectIntersect primitive.
    RectT savedClip = clipSave(d);
    RectT barRect   = { win->x + CHROME_BORDER_WIDTH, barY, win->w - CHROME_BORDER_WIDTH * 2, barH };
    RectT menuClip;

    if (rectIntersect(&barRect, &savedClip, &menuClip)) {
        setClipRect(d, menuClip.x, menuClip.y, menuClip.w, menuClip.h);
    } else {
        // Menu bar lies fully outside the dirty rect: an empty clip rejects
        // every label span while still letting the separator line paint once
        // the clip is restored below.
        setClipRect(d, savedClip.x, savedClip.y, 0, 0);
    }

    for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
        MenuT  *menu  = win->menuBar->menus[i];
        int32_t itemX = win->x + menu->barX;
        int32_t itemW = menu->barW;
        int32_t textX = itemX + CHROME_TITLE_PAD;
        int32_t textY = barY + (barH - font->charHeight) / 2;

        if (i == win->menuBar->activeIdx) {
            // Depressed look for the open menu item
            BevelStyleT sunken = BEVEL_SUNKEN(colors, colors->menuBg, 1);
            drawBevel(d, ops, itemX, barY, itemW, barH - 1, &sunken);
            drawTextAccel(d, ops, font, textX + 1, textY + 1, menu->label,
                          colors->menuFg, colors->menuBg, true);
        } else {
            drawTextAccel(d, ops, font, textX, textY, menu->label,
                          colors->menuFg, colors->menuBg, true);
        }
    }

    clipRestore(d, &savedClip);

    drawHLine(d, ops, win->x + CHROME_BORDER_WIDTH, barY + barH - 1,
              win->w - CHROME_BORDER_WIDTH * 2, colors->windowShadow);
}


// GEOS Ensemble Motif-style resize indicators: perpendicular grooves that
// cut across the window border near each corner. Two breaks per corner  --
// one on each edge meeting at that corner. Each groove is a 2px sunken
// notch (shadow line + highlight line) cutting across the full 4px border
// width, perpendicular to the edge direction.
//
// These serve as a visual affordance telling the user which edges are
// resizable. Unlike CUA/Windows which uses a diagonal hatch in the
// bottom-right corner only, the Motif style marks all four corners
// symmetrically, indicating that all edges are draggable. The breaks
// are positioned RESIZE_BREAK_INSET (16px) from each corner, which
// also defines the boundary between corner-resize (diagonal) and
// edge-resize (single axis) hit zones in wmResizeEdgeHit.

static void drawResizeBreaks(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, WindowT *win) {
    if (!win->resizable) {
        return;
    }

    uint32_t hi    = colors->windowHighlight;
    uint32_t sh    = colors->windowShadow;
    int32_t  bw    = CHROME_BORDER_WIDTH;
    int32_t  inset = RESIZE_BREAK_INSET;
    int32_t  wx    = win->x;
    int32_t  wy    = win->y;
    int32_t  ww    = win->w;
    int32_t  wh    = win->h;

    // Each break is a 2px-wide groove cutting across the full 4px border.
    // The groove is drawn as: shadow line, then highlight line (sunken notch).
    // Over the face-colored middle rows, we also draw the groove.

    // Top-left corner
    // Vertical groove across top border
    drawVLine(d, ops, wx + inset,     wy, bw, sh);
    drawVLine(d, ops, wx + inset + 1, wy, bw, hi);
    // Horizontal groove across left border
    drawHLine(d, ops, wx, wy + inset,     bw, sh);
    drawHLine(d, ops, wx, wy + inset + 1, bw, hi);

    // Top-right corner
    drawVLine(d, ops, wx + ww - inset - 2, wy, bw, sh);
    drawVLine(d, ops, wx + ww - inset - 1, wy, bw, hi);
    drawHLine(d, ops, wx + ww - bw, wy + inset,     bw, sh);
    drawHLine(d, ops, wx + ww - bw, wy + inset + 1, bw, hi);

    // Bottom-left corner
    drawVLine(d, ops, wx + inset,     wy + wh - bw, bw, sh);
    drawVLine(d, ops, wx + inset + 1, wy + wh - bw, bw, hi);
    drawHLine(d, ops, wx, wy + wh - inset - 2, bw, sh);
    drawHLine(d, ops, wx, wy + wh - inset - 1, bw, hi);

    // Bottom-right corner
    drawVLine(d, ops, wx + ww - inset - 2, wy + wh - bw, bw, sh);
    drawVLine(d, ops, wx + ww - inset - 1, wy + wh - bw, bw, hi);
    drawHLine(d, ops, wx + ww - bw, wy + wh - inset - 2, bw, sh);
    drawHLine(d, ops, wx + ww - bw, wy + wh - inset - 1, bw, hi);
}


// Nearest-neighbor scale blit used to render minimized window icons.
// Icons are ICON_SIZE x ICON_SIZE (64x64); the source can be the window's
// content buffer (live thumbnail) or a loaded icon image of any size.
//
// Optimization strategy for 486/Pentium:
// - Fixed-point 16.16 reciprocals replace per-pixel division with
//   multiply+shift. Division is 40+ cycles on 486; multiply is 13.
// - Source coordinate lookup tables (srcXTab/srcYTab) are pre-computed
//   once per call and indexed in the inner loop, eliminating repeated
//   multiply+shift per pixel.
// - Static lookup table buffers avoid stack frame growth (ICON_TOTAL_SIZE=68
//   entries * 4 bytes = 272 bytes per table that would otherwise be allocated
//   on every call during the compositing loop).  Sized to the largest dstW/dstH
//   ever passed (a provided icon scaled to ICON_TOTAL_SIZE), not just ICON_SIZE.
// - The bpp switch is outside the column loop so the branch is taken
//   once per row rather than once per pixel.
// - Clipping is done once up front by adjusting row/col start/end ranges,
//   so the inner loop runs with zero per-pixel clip checks.

static void drawScaledRect(DisplayT *d, int32_t dstX, int32_t dstY, int32_t dstW, int32_t dstH, const uint8_t *src, int32_t srcW, int32_t srcH, int32_t srcPitch, int32_t bpp) {
    int32_t rowStart = 0;
    int32_t rowEnd   = dstH;
    int32_t colStart = 0;
    int32_t colEnd   = dstW;

    if (dstY < d->clipY) {
        rowStart = d->clipY - dstY;
    }

    if (dstY + dstH > d->clipY + d->clipH) {
        rowEnd = d->clipY + d->clipH - dstY;
    }

    if (dstX < d->clipX) {
        colStart = d->clipX - dstX;
    }

    if (dstX + dstW > d->clipX + d->clipW) {
        colEnd = d->clipX + d->clipW - dstX;
    }

    if (rowStart >= rowEnd || colStart >= colEnd) {
        return;
    }

    static int32_t srcXTab[ICON_TOTAL_SIZE];
    static int32_t srcYTab[ICON_TOTAL_SIZE];
    int32_t visibleCols = colEnd - colStart;
    int32_t visibleRows = rowEnd - rowStart;

    // Fixed-point 16.16 scale factors
    uint32_t recipW = ((uint32_t)srcW << 16) / (uint32_t)dstW;
    uint32_t recipH = ((uint32_t)srcH << 16) / (uint32_t)dstH;

    // Pre-multiply X offsets by bpp so the inner loop does a single array index
    for (int32_t dx = 0; dx < visibleCols; dx++) {
        srcXTab[dx] = (int32_t)(((uint32_t)(colStart + dx) * recipW) >> 16) * bpp;
    }

    for (int32_t dy = 0; dy < visibleRows; dy++) {
        srcYTab[dy] = (int32_t)(((uint32_t)(rowStart + dy) * recipH) >> 16);
    }

    for (int32_t dy = rowStart; dy < rowEnd; dy++) {
        int32_t        sy     = srcYTab[dy - rowStart];
        uint8_t       *dstRow = d->backBuf + (dstY + dy) * d->pitch + (dstX + colStart) * bpp;
        const uint8_t *srcRow = src + sy * srcPitch;

        if (bpp == 1) {
            for (int32_t dx = 0; dx < visibleCols; dx++) {
                dstRow[dx] = srcRow[srcXTab[dx]];
            }
        } else if (bpp == 2) {
            for (int32_t dx = 0; dx < visibleCols; dx++) {
                *(uint16_t *)(dstRow + dx * 2) = *(const uint16_t *)(srcRow + srcXTab[dx]);
            }
        } else {
            for (int32_t dx = 0; dx < visibleCols; dx++) {
                *(uint32_t *)(dstRow + dx * 4) = *(const uint32_t *)(srcRow + srcXTab[dx]);
            }
        }
    }
}


// Renders the title bar: background fill, close gadget (left), minimize
// and maximize gadgets (right), and centered title text.
//
// The title bar background uses active/inactive colors to provide a strong
// visual cue for which window has keyboard focus  -- the same convention used
// by Windows 3.x, Motif, and CDE. Only the title bar changes color on
// focus change; the rest of the chrome stays the same. This is why
// wmSetFocus dirties only the title bar area, not the entire window.
//
// Gadget icons are drawn as simple geometric shapes (horizontal bar for
// close, filled square for minimize, box outlines for maximize/restore)
// rather than bitmaps. This avoids loading icon resources, works at any
// color depth, and the shapes are recognizable even at the small 16x16
// gadget size on a 640x480 display.

static void drawTitleBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, WindowT *win) {
    TitleGeomT g;
    computeTitleGeom(win, &g);

    uint32_t bg = win->focused ? colors->activeTitleBg : colors->inactiveTitleBg;
    uint32_t fg = win->focused ? colors->activeTitleFg : colors->inactiveTitleFg;

    // Fill title bar background
    rectFill(d, ops, g.titleX, g.titleY, g.titleW, g.titleH, bg);

    // Close gadget on the LEFT (system menu / close)
    drawTitleGadget(d, ops, colors, g.closeX, g.gadgetY, g.gadgetS);
    // Horizontal bar icon inside close gadget
    drawHLine(d, ops, g.closeX + CLOSE_ICON_INSET, g.gadgetY + g.gadgetS / 2,
              g.gadgetS - CLOSE_ICON_INSET * 2, colors->contentFg);

    if (g.maxX >= 0) {
        // Maximize/restore gadget on the far RIGHT
        drawTitleGadget(d, ops, colors, g.maxX, g.gadgetY, g.gadgetS);

        if (win->maximized) {
            // Restore icon: small box outline
            int32_t bx = g.maxX + RESTORE_ICON_INSET;
            int32_t by = g.gadgetY + RESTORE_ICON_INSET;
            int32_t bs = g.gadgetS - RESTORE_ICON_INSET * 2;
            rectFill(d, ops, bx, by, bs, bs, colors->buttonFace);
            drawRectOutline(d, ops, bx, by, bs, bs, colors->contentFg);
        } else {
            // Maximize icon: larger box outline filling more of the gadget
            int32_t bx = g.maxX + MAXIMIZE_ICON_INSET;
            int32_t by = g.gadgetY + MAXIMIZE_ICON_INSET;
            int32_t bs = g.gadgetS - MAXIMIZE_ICON_INSET * 2;
            rectFill(d, ops, bx, by, bs, bs, colors->buttonFace);
            drawRectOutline(d, ops, bx, by, bs, bs, colors->contentFg);
            drawHLine(d, ops, bx, by + 1, bs, colors->contentFg);
        }
    }

    // Minimize gadget (not on modal windows)
    if (g.minX >= 0) {
        drawTitleGadget(d, ops, colors, g.minX, g.gadgetY, g.gadgetS);
        // Small square centered in minimize gadget
        rectFill(d, ops, g.minX + (g.gadgetS - MINIMIZE_ICON_SIZE) / 2, g.gadgetY + (g.gadgetS - MINIMIZE_ICON_SIZE) / 2,
                 MINIMIZE_ICON_SIZE, MINIMIZE_ICON_SIZE, colors->contentFg);
    }

    // Title text is centered in the available space between gadgets. If the
    // title is too long, it's truncated by character count (not pixel width)
    // since the font is fixed-width. No ellipsis is added  -- at these sizes,
    // ellipsis would consume 3 characters that could show useful text instead.
    int32_t availW = g.textRightEdge - g.textLeftEdge;

    if (availW > 0) {
        int32_t maxChars = availW / font->charWidth;
        int32_t len      = (int32_t)strlen(win->title);

        if (len > maxChars) {
            len = maxChars;
        }

        if (len > 0) {
            // Build truncated title for drawText
            char truncTitle[MAX_TITLE_LEN];
            memcpy(truncTitle, win->title, len);
            truncTitle[len] = '\0';

            int32_t textW = len * font->charWidth;
            int32_t textX = g.textLeftEdge + (availW - textW) / 2;
            int32_t textY = g.titleY + (g.titleH - font->charHeight) / 2;

            drawText(d, ops, font, textX, textY, truncTitle, fg, bg, true);
        }
    }
}


// Draws a small raised beveled square (1px bevel) used as the base for
// close, minimize, and maximize buttons. The icon (bar, box, dot) is drawn
// on top by the caller. Using 1px bevels on gadgets (vs 4px on the frame)
// keeps them visually subordinate to the window border  -- a Motif convention
// that helps users distinguish clickable controls from structural chrome.

static void drawTitleGadget(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size) {
    BevelStyleT bevel;
    bevel.highlight = colors->windowHighlight;
    bevel.shadow    = colors->windowShadow;
    bevel.face      = colors->buttonFace;
    bevel.width     = 1;
    drawBevel(d, ops, x, y, size, size, &bevel);
}


// Frees a single menu item's submenu subtree (if any) and clears the
// pointer.  Only called from menu-tree teardown (freeMenuRecursive); the
// item-level mutation APIs (wmClearMenuItems/wmRemoveMenuItem) must NOT use
// it because an open popup may still reference the subtree.

static void freeMenuItemSubMenu(MenuItemT *item) {
    if (item->subMenu) {
        freeMenuRecursive(item->subMenu);
        free(item->subMenu);
        item->subMenu = NULL;
    }
}


// Walks the submenu tree depth-first, freeing heap-allocated child MenuT
// nodes and the menu's item array.  It does NOT free the MenuT it is passed;
// callers own that pointer (top-level menus are freed by wmDestroyMenuBar,
// submenu children by their parent item).

static void freeMenuRecursive(MenuT *menu) {
    for (int32_t i = 0; i < menu->itemCount; i++) {
        freeMenuItemSubMenu(&menu->items[i]);
    }

    free(menu->items);
    menu->items    = NULL;
    menu->itemCount = 0;
    menu->itemCap   = 0;
}


// Adds a top-level menu to a window's menu bar. Each MenuT is heap-allocated
// and the menu bar holds an array of owned pointers, so a MenuT * returned
// here stays valid even when a later wmAddMenu grows (reallocs) that array.
// The positionsDirty flag is set so that the next paint triggers
// computeMenuBarPositions to lay out all labels.
//
// The accelerator key is parsed from & markers in the label (e.g.
// "&File" -> Alt+F), following the CUA/Windows convention. This is
// stored on the MenuT so the event loop can match keyboard shortcuts
// without re-parsing the label string on every keypress.

static bool menuGrowItems(MenuT *menu) {
    if (menu->itemCount < menu->itemCap) {
        return true;
    }

    int32_t newCap = menu->itemCap ? menu->itemCap * 2 : MENU_INIT_CAP;
    MenuItemT *newBuf = (MenuItemT *)realloc(menu->items, newCap * sizeof(MenuItemT));

    if (!newBuf) {
        return false;
    }

    menu->items   = newBuf;
    menu->itemCap = newCap;
    return true;
}


// Applies a checked state to a menu item, honoring radio-group semantics.
// A radio item being checked unchecks every sibling in its contiguous
// radio-group run (bounded by the first non-radio item on each side) and
// checks only the target; any other case sets the item's checked flag
// directly.  Shared by wmMenuItemSetChecked, wmMenuItemSetCheckedInMenu, and
// the app-level click handler (clickMenuCheckRadio) so the group-scan rule
// has a single implementation.
void menuItemApplyChecked(MenuT *menu, int32_t idx, bool checked) {
    MenuItemT *item = &menu->items[idx];

    if (item->type == MenuItemRadioE && checked) {
        int32_t groupStart = idx;

        while (groupStart > 0 && menu->items[groupStart - 1].type == MenuItemRadioE) {
            groupStart--;
        }

        int32_t groupEnd = idx;

        while (groupEnd < menu->itemCount - 1 && menu->items[groupEnd + 1].type == MenuItemRadioE) {
            groupEnd++;
        }

        for (int32_t g = groupStart; g <= groupEnd; g++) {
            menu->items[g].checked = (g == idx);
        }
    } else {
        item->checked = checked;
    }
}


// Width of a menu bar label including its padding.  Single source of truth
// for computeMenuBarPositions and wmMinWindowSize.
static int32_t menuLabelWidth(const BitmapFontT *font, const char *label) {
    return textWidthAccel(font, label) + CHROME_TITLE_PAD * 2;
}


// Grows the item array, takes the next slot, and initializes the common
// fields (zeroed, label copied + NUL-terminated, enabled, accelerator).
// Returns the new item, or NULL if the array could not grow.
static MenuItemT *menuNewItem(MenuT *menu, const char *label) {
    if (!menuGrowItems(menu)) {
        return NULL;
    }

    MenuItemT *item = &menu->items[menu->itemCount++];
    memset(item, 0, sizeof(*item));
    copyLabel(item->label, label, MAX_MENU_LABEL);
    item->enabled  = true;
    item->accelKey = accelParse(item->label);
    return item;
}


// Allocates and initializes a scrollbar into *slot for the given window and
// orientation.  Shared by wmAddHScrollbar/wmAddVScrollbar so the allocation,
// field init, and content-rect update live in one place.
static ScrollbarT *scrollbarAdd(WindowT *win, ScrollbarT **slot, ScrollbarOrientE orient, const char *orientName, int32_t min, int32_t max, int32_t pageSize) {
    // Free any prior scrollbar so a second add on the same window does not leak.
    free(*slot);
    *slot = (ScrollbarT *)malloc(sizeof(ScrollbarT));

    if (!*slot) {
        dvxLog("WM: failed to allocate %s scrollbar", orientName);
        return NULL;
    }

    // A negative page size would make the thumb math divide by zero or go
    // negative; treat it as "no page" instead.
    if (pageSize < 0) {
        pageSize = 0;
    }

    (*slot)->orient   = orient;
    (*slot)->min      = min;
    (*slot)->max      = max;
    (*slot)->value    = min;
    (*slot)->pageSize = pageSize;

    // Position will be updated by wmUpdateContentRect
    wmUpdateContentRect(win);

    return *slot;
}


// Clamps a scrollbar value to its range and, if it changed, dirties the
// scrollbar area and fires the window's onScroll callback.  Shared tail of
// wmScrollbarClick and wmScrollbarDrag.
static void scrollbarCommitValue(WindowT *win, ScrollbarT *sb, DirtyListT *dl, int32_t sbScreenX, int32_t sbScreenY, int32_t oldValue) {
    if (sb->value < sb->min) {
        sb->value = sb->min;
    }

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

    if (sb->value != oldValue) {
        dirtyListAdd(dl, sbScreenX, sbScreenY,
                     sb->orient == ScrollbarVerticalE ? SCROLLBAR_WIDTH : sb->length,
                     sb->orient == ScrollbarVerticalE ? sb->length : SCROLLBAR_WIDTH);

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


// Computes thumb position and size within the scrollbar track. The track
// is the total scrollbar length minus the two arrow buttons at each end.
//
// Thumb size is proportional to the visible page relative to the total
// scrollable range: thumbSize = trackLen * pageSize / (range + pageSize).
// This gives a thumb that shrinks as content grows, matching user
// expectations from Motif/Windows scrollbars. The minimum thumb size is
// clamped to SCROLLBAR_WIDTH so it remains grabbable even with huge content.
//
// The int64_t casts prevent overflow: trackLen can be ~400px, pageSize and
// range can be arbitrarily large (e.g. a 10000-line text buffer), and
// 400 * 10000 exceeds int32_t range.
//
// When range <= 0 (all content visible), the thumb fills the entire track,
// visually indicating there's nothing to scroll.

static int32_t scrollbarThumbInfo(const ScrollbarT *sb, int32_t *thumbPos, int32_t *thumbSize) {
    int32_t trackLen = sb->length - SCROLLBAR_WIDTH * 2;
    int32_t range    = sb->max - sb->min;

    if (range <= 0 || trackLen <= 0) {
        *thumbPos  = 0;
        *thumbSize = trackLen;
        return trackLen;
    }

    *thumbSize = (int32_t)(((int64_t)sb->pageSize * trackLen) / ((int64_t)range + sb->pageSize));

    if (*thumbSize < SCROLLBAR_WIDTH) {
        *thumbSize = SCROLLBAR_WIDTH;
    }

    // Map value to pixel offset: value maps linearly to the range
    // [0, trackLen - thumbSize]
    *thumbPos = (int32_t)(((int64_t)(sb->value - sb->min) * (trackLen - *thumbSize)) / range);

    return trackLen;
}


// Renders a window-level scrollbar by delegating to the shared draw-layer
// painter.  Computes the screen position from the window origin plus the
// scrollbar's window-relative x/y, derives the thumb geometry from the
// scroll range, and hands everything to drawScrollbar().
//
// winX/winY are the window's screen position; sb->x/y are relative to the
// window origin. This split lets scrollbar positions survive window drags
// without recalculation  -- only winX/winY change.

static void wmDrawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, const ScrollbarT *sb, int32_t winX, int32_t winY) {
    int32_t x = winX + sb->x;
    int32_t y = winY + sb->y;

    int32_t thumbPos;
    int32_t thumbSize;
    scrollbarThumbInfo(sb, &thumbPos, &thumbSize);

    drawScrollbar(d, ops, colors, sb->orient, x, y, sb->length, SCROLLBAR_WIDTH, thumbPos, thumbSize);
}


// Effective maximum frame size for a window: its maxW/maxH constraint
// clamped to the screen, or the full screen when WM_MAX_FROM_SCREEN.
// Shared by wmMaximize and wmResizeMove.
static void wmEffectiveMaxSize(const WindowT *win, const DisplayT *d, int32_t *maxW, int32_t *maxH) {
    *maxW = (win->maxW == WM_MAX_FROM_SCREEN) ? d->width : DVX_MIN(win->maxW, d->width);
    *maxH = (win->maxH == WM_MAX_FROM_SCREEN) ? d->height : DVX_MIN(win->maxH, d->height);
}


// wmMenuFindItem -- find a menu item by command ID anywhere in a menu bar,
// descending into nested submenus to any depth.  When outMenu is non-NULL it
// receives the menu that directly contains the matched item so callers can
// locate radio-group siblings and compute the item index.

static MenuItemT *wmMenuFindItem(MenuBarT *bar, int32_t id, MenuT **outMenu) {
    if (!bar) {
        return NULL;
    }

    for (int32_t m = 0; m < bar->menuCount; m++) {
        MenuItemT *found = wmMenuFindItemRecursive(bar->menus[m], id, outMenu);

        if (found) {
            return found;
        }
    }

    return NULL;
}


// wmMenuFindItemRecursive -- find a menu item by command ID within a single
// menu tree (the menu plus every nested submenu, to any depth).  When outMenu
// is non-NULL it receives the menu that directly contains the matched item.
// Shared id-search used by wmMenuFindItem (menu bars) and wmMenuFindItemInMenu
// (standalone popup/context menus).

static MenuItemT *wmMenuFindItemRecursive(MenuT *menu, int32_t id, MenuT **outMenu) {
    if (!menu) {
        return NULL;
    }

    for (int32_t i = 0; i < menu->itemCount; i++) {
        if (menu->items[i].id == id) {
            if (outMenu) {
                *outMenu = menu;
            }

            return &menu->items[i];
        }

        if (menu->items[i].subMenu) {
            MenuItemT *found = wmMenuFindItemRecursive(menu->items[i].subMenu, id, outMenu);

            if (found) {
                return found;
            }
        }
    }

    return NULL;
}


// Adds a horizontal scrollbar to a window. The scrollbar steals
// SCROLLBAR_WIDTH pixels from the bottom of the content area (handled by
// wmUpdateContentRect). The caller specifies the logical scroll range; the
// WM handles all geometry, rendering, and hit-testing from there.
//
// Calling wmUpdateContentRect immediately ensures that the content area
// shrinks to accommodate the scrollbar and the scrollbar's position/length
// fields are initialized before the next paint. This matters because the
// caller may add both scrollbars before any paint occurs, and the second
// scrollbar's geometry depends on the first one already being accounted for.

ScrollbarT *wmAddHScrollbar(WindowT *win, int32_t min, int32_t max, int32_t pageSize) {
    return scrollbarAdd(win, &win->hScroll, ScrollbarHorizontalE, "horizontal", min, max, pageSize);
}


MenuT *wmAddMenu(MenuBarT *bar, const char *label) {
    if (bar->menuCount >= bar->menuCap) {
        int32_t newCap  = bar->menuCap ? bar->menuCap * 2 : MENU_INIT_CAP;
        MenuT **newBuf  = (MenuT **)realloc(bar->menus, newCap * sizeof(MenuT *));

        if (!newBuf) {
            return NULL;
        }

        bar->menus   = newBuf;
        bar->menuCap = newCap;
    }

    MenuT *menu = (MenuT *)calloc(1, sizeof(MenuT));

    if (!menu) {
        return NULL;
    }

    copyLabel(menu->label, label, MAX_MENU_LABEL);
    menu->accelKey = accelParse(menu->label);
    bar->menus[bar->menuCount] = menu;
    bar->menuCount++;
    bar->positionsDirty = true;

    return menu;
}


// Allocates and attaches a menu bar to a window. The menu bar is heap-
// allocated separately from the window because most windows don't have
// one, and keeping it out of WindowT saves ~550 bytes per window
// wmUpdateContentRect is called to shrink
// the content area by CHROME_MENU_HEIGHT to make room for the bar.

MenuBarT *wmAddMenuBar(WindowT *win) {
    // Allocate the new bar BEFORE tearing down any existing one.  The
    // teardown grows the content rect without reallocating the content
    // buffer (d is NULL), so failing the malloc afterwards would leave
    // contentH grown past the buffer and the next full repaint would
    // overrun the heap.  Allocating first makes the failure path a no-op.
    MenuBarT *bar = (MenuBarT *)malloc(sizeof(MenuBarT));

    if (!bar) {
        dvxLog("WM: failed to allocate menu bar");
        return NULL;
    }

    // Tear down any existing bar (recursive menu cleanup) so a second add does
    // not leak the prior MenuBarT, its menus array, and all MenuT/submenus.
    // The transient content-rect grow is undone by wmUpdateContentRect below
    // before any repaint can observe it.
    wmDestroyMenuBar(win, NULL);

    memset(bar, 0, sizeof(MenuBarT));
    bar->activeIdx = -1;
    win->menuBar   = bar;
    wmUpdateContentRect(win);

    return bar;
}


void wmAddMenuCheckItem(MenuT *menu, const char *label, int32_t id, bool checked) {
    MenuItemT *item = menuNewItem(menu, label);

    if (!item) {
        return;
    }

    item->id      = id;
    item->type    = MenuItemCheckE;
    item->checked = checked;
}


void wmAddMenuItem(MenuT *menu, const char *label, int32_t id) {
    MenuItemT *item = menuNewItem(menu, label);

    if (!item) {
        return;
    }

    item->id = id;
}


void wmAddMenuRadioItem(MenuT *menu, const char *label, int32_t id, bool checked) {
    MenuItemT *item = menuNewItem(menu, label);

    if (!item) {
        return;
    }

    item->id      = id;
    item->type    = MenuItemRadioE;
    item->checked = checked;
}


void wmAddMenuSeparator(MenuT *menu) {
    if (!menuGrowItems(menu)) {
        return;
    }

    MenuItemT *item = &menu->items[menu->itemCount];
    memset(item, 0, sizeof(*item));
    item->separator = true;
    item->enabled   = false;
    menu->itemCount++;
}


// Creates a cascading submenu. Unlike top-level menus (inline in MenuBarT),
// submenus are heap-allocated because the nesting depth is unpredictable and
// the submenu is owned by its parent item. The item's id is set to -1 to
// distinguish it from leaf items during event dispatch  -- clicking a submenu
// item opens the child rather than firing a command.

MenuT *wmAddSubMenu(MenuT *parentMenu, const char *label) {
    MenuT *child = (MenuT *)calloc(1, sizeof(MenuT));

    if (!child) {
        return NULL;
    }

    MenuItemT *item = menuNewItem(parentMenu, label);

    if (!item) {
        free(child);
        return NULL;
    }

    item->id      = -1;
    item->subMenu = child;

    return child;
}


// Adds a vertical scrollbar to a window. Mirrors wmAddHScrollbar; the
// scrollbar steals SCROLLBAR_WIDTH pixels from the right side of the
// content area. See wmAddHScrollbar for design notes.

ScrollbarT *wmAddVScrollbar(WindowT *win, int32_t min, int32_t max, int32_t pageSize) {
    return scrollbarAdd(win, &win->vScroll, ScrollbarVerticalE, "vertical", min, max, pageSize);
}


// Adjusts a stored window-stack index after the window at raisedSlot has been
// raised to newTop and the entries above it shifted down by one.  Mirrors
// wmAdjustIndexForRemoval: the raised window itself moves to newTop, any index
// above the old slot decrements, and lower indices (and inactive -1 sentinels)
// are left unchanged.

static int32_t wmAdjustIndexForRaise(int32_t index, int32_t raisedSlot, int32_t newTop) {
    if (index == raisedSlot) {
        return newTop;
    }

    if (index > raisedSlot) {
        return index - 1;
    }

    return index;
}


// Adjusts a stored window-stack index after the window at removedSlot has
// been removed and higher entries shifted down.  Returns -1 when the stored
// index was the removed window itself -- the caller's interaction (drag,
// resize, scroll) targeted a now-freed window and must be cancelled.

static int32_t wmAdjustIndexForRemoval(int32_t index, int32_t removedSlot) {
    if (index == removedSlot) {
        return -1;
    }

    if (index > removedSlot) {
        return index - 1;
    }

    return index;
}


// wmClearMenuItems -- remove all items from a menu

void wmClearMenuItems(MenuT *menu) {
    if (menu) {
        // Detach only -- do NOT free item->subMenu trees here.  The app-layer
        // popup system holds direct MenuT pointers into an open popup chain
        // (popup.menu / parentStack), and this MenuT*-only API cannot see or
        // reset that state, so freeing a submenu the popup still references
        // would be a use-after-free on the next composite or click.  Detached
        // trees are deliberately orphaned instead (bounded leak); stale
        // subMenu pointers past itemCount are harmless because menuNewItem
        // zeroes reused slots.
        menu->itemCount = 0;
    }
}


// Allocates a standalone menu for use as a context menu (right-click popup).
// Unlike menu bar menus which are embedded in MenuBarT, context menus are
// independently allocated and freed with wmFreeMenu. calloc ensures all
// fields start zeroed (no items, no accel keys).

MenuT *wmCreateMenu(void) {
    MenuT *m = (MenuT *)calloc(1, sizeof(MenuT));
    return m;
}


// Creates a window, allocates its content backbuffer, and places it at the
// top of the z-order stack. The caller specifies the outer frame dimensions
// (x, y, w, h); the content area is computed by subtracting chrome from
// the frame.
//
// Each window gets a unique monotonic ID (static nextId) used for lookup
// by the event loop and DXE app system. IDs are never reused  -- with 2^31
// IDs available and windows being created/destroyed interactively, this
// will never wrap in practice.
//
// The content buffer is initialized to 0xFF (white) so newly created
// windows have a clean background before the app's first onPaint fires.
// maxW/maxH default to -1 meaning "use screen dimensions"  -- apps can
// override this to constrain maximized size (e.g. for dialog-like windows).
//
// New windows are added at stack->count (the top), so they appear in front
// of all existing windows. The caller is responsible for calling wmSetFocus
// and wmRaiseWindow if desired.

WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int32_t x, int32_t y, int32_t w, int32_t h, bool resizable) {
    if (stack->count >= stack->cap) {
        int32_t newCap = stack->cap ? stack->cap * 2 : STACK_INIT_CAP;
        WindowT **newBuf = (WindowT **)realloc(stack->windows, newCap * sizeof(WindowT *));

        if (!newBuf) {
            dvxLog("WM: failed to grow window stack");
            return NULL;
        }

        stack->windows = newBuf;
        stack->cap     = newCap;
    }

    WindowT *win = (WindowT *)malloc(sizeof(WindowT));

    if (!win) {
        dvxLog("WM: failed to allocate window");
        return NULL;
    }

    memset(win, 0, sizeof(*win));

    static int32_t nextId = 1;
    win->id             = nextId++;
    win->x              = x;
    win->y              = y;
    win->w              = w;
    win->h              = h;
    win->visible        = true;
    win->resizable      = resizable;
    win->maxW           = WM_MAX_FROM_SCREEN;
    win->maxH           = WM_MAX_FROM_SCREEN;

    copyLabel(win->title, title, MAX_TITLE_LEN);

    wmUpdateContentRect(win);

    win->contentPitch = win->contentW * d->format.bytesPerPixel;
    int32_t bufSize   = win->contentPitch * win->contentH;

    if (bufSize > 0) {
        win->contentBuf = (uint8_t *)malloc(bufSize);

        if (!win->contentBuf) {
            dvxLog("WM: failed to allocate content buffer (%ld bytes)", (long)bufSize);
            free(win);
            return NULL;
        }

        memset(win->contentBuf, CONTENT_CLEAR_BYTE, bufSize);
    }

    win->paintNeeded = PAINT_FULL;

    stack->windows[stack->count] = win;
    stack->count++;

    return win;
}


void wmDestroyMenuBar(WindowT *win, const DisplayT *d) {
    if (!win || !win->menuBar) {
        return;
    }

    for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
        freeMenuRecursive(win->menuBar->menus[i]);
        free(win->menuBar->menus[i]);
    }

    free(win->menuBar->menus);
    free(win->menuBar);
    win->menuBar = NULL;
    wmUpdateContentRect(win);

    // Removing the bar grows the content area; the buffer must grow with
    // it or the next full repaint overruns it. d is NULL only during
    // window teardown, where the buffer is already gone.
    if (d) {
        wmReallocContentBuf(win, d);
    }
}


// Removes a window from the stack and frees all associated resources.
// The stack is compacted by shifting entries down (O(N), but N <= 64 and
// destroy is infrequent). focusedIdx is adjusted to track the same logical
// window after the shift: if the focused window was above the destroyed one,
// its index decreases by 1; if the destroyed window itself was focused,
// focusedIdx is cleared to -1 so the caller's follow-up wmSetFocus performs a
// full old != new transition and fires onFocus on the new top window.
//
// Resource cleanup order: widget tree first (may reference window fields),
// then content buffer, menus (with recursive submenu cleanup), scrollbars,
// icon data, and finally the window struct itself.

void wmDestroyWindow(WindowStackT *stack, WindowT *win) {
    for (int32_t i = 0; i < stack->count; i++) {
        if (stack->windows[i] == win) {
            for (int32_t j = i; j < stack->count - 1; j++) {
                stack->windows[j] = stack->windows[j + 1];
            }

            stack->count--;

            if (stack->focusedIdx == i) {
                // Clear focus rather than eagerly assigning the new top
                // window. The caller (dvxDestroyWindow) follows up with
                // wmSetFocus, which only fires onFocus/onBlur when the old
                // and new windows differ; pre-assigning here would make
                // oldWin == newWin and suppress the new window's onFocus.
                stack->focusedIdx = -1;
            } else if (stack->focusedIdx > i) {
                stack->focusedIdx--;
            }

            // An in-progress drag/resize/scroll on the destroyed window is
            // cancelled (-1); one above the removed slot shifts down.
            stack->dragWindow   = wmAdjustIndexForRemoval(stack->dragWindow, i);
            stack->resizeWindow = wmAdjustIndexForRemoval(stack->resizeWindow, i);
            stack->scrollWindow = wmAdjustIndexForRemoval(stack->scrollWindow, i);

            break;
        }
    }

    // Destroy widget tree before freeing window
    if (win->widgetRoot) {
        wgtDestroy(win->widgetRoot);
        win->widgetRoot = NULL;
    }

    free(win->contentBuf);
    win->contentBuf = NULL;

    wmDestroyMenuBar(win, NULL);
    wmRemoveScrollbars(win);

    free(win->iconData);
    free(win);
}


// Initiates a window drag by recording the mouse offset from the window
// origin. This offset is maintained throughout the drag so that the window
// tracks the mouse cursor without jumping  -- the window position under the
// cursor stays consistent from mousedown to mouseup.

void wmDragBegin(WindowStackT *stack, int32_t idx, int32_t mouseX, int32_t mouseY) {
    stack->dragWindow = idx;
    stack->dragOffX   = mouseX - stack->windows[idx]->x;
    stack->dragOffY   = mouseY - stack->windows[idx]->y;
    stack->dragStartX = mouseX;
    stack->dragStartY = mouseY;
    stack->dragActive = false;
}


void wmDragEnd(WindowStackT *stack) {
    stack->dragWindow = -1;
    stack->dragActive = false;
}


// Called on each mouse move during a drag. Dirties both the old and new
// positions so the compositor repaints the area the window vacated (exposing
// the desktop or windows behind it) and the area it now occupies. These two
// rects will often overlap and get merged by dirtyListMerge, reducing the
// actual flush work.
//
// Unlike some WMs that use an "outline drag" (drawing a wireframe while
// dragging and moving the real window on mouse-up), we do full content
// dragging. This is feasible because the content buffer is persistent  --
// we don't need to ask the app to repaint during the drag, just blit from
// its buffer at the new position.

void wmDragMove(WindowStackT *stack, DirtyListT *dl, int32_t mouseX, int32_t mouseY, int32_t screenW, int32_t screenH) {
    if (stack->dragWindow < 0 || stack->dragWindow >= stack->count) {
        return;
    }

    // Don't move until the mouse has moved past the deadzone from the
    // initial click point. This allows double-clicks on the title bar
    // without accidentally starting a drag.
    if (!stack->dragActive) {
        int32_t dx = mouseX - stack->dragStartX;
        int32_t dy = mouseY - stack->dragStartY;

        if (dx < -DRAG_DEADZONE || dx > DRAG_DEADZONE ||
            dy < -DRAG_DEADZONE || dy > DRAG_DEADZONE) {
            stack->dragActive = true;
        } else {
            return;
        }
    }

    WindowT *win = stack->windows[stack->dragWindow];

    dirtyListAdd(dl, win->x, win->y, win->w, win->h);

    win->x = mouseX - stack->dragOffX;
    win->y = mouseY - stack->dragOffY;

    win->maximized = false;

    // Clamp: keep title bar reachable
    if (win->y < 0) {
        win->y = 0;
    }
    if (win->y + CHROME_TITLEBAR_BOTTOM > screenH) {
        win->y = screenH - CHROME_TITLEBAR_BOTTOM;
    }
    if (win->x + win->w < DRAG_MIN_VISIBLE) {
        win->x = DRAG_MIN_VISIBLE - win->w;
    }
    if (win->x > screenW - DRAG_MIN_VISIBLE) {
        win->x = screenW - DRAG_MIN_VISIBLE;
    }

    dirtyListAdd(dl, win->x, win->y, win->w, win->h);
}


// Paints all window chrome (everything that's not content) clipped to a
// dirty rect. Called once per window per dirty rect during compositing.
//
// Paint order: outer border -> title bar -> menu bar -> inner bevel ->
// resize breaks. This is bottom-to-top layering: the title bar overwrites
// the border's interior, and resize breaks are drawn last so they cut
// across the already-painted border.
//
// The clip rect is set to the dirty rect so all draw operations are
// automatically clipped. This is the mechanism by which partial chrome
// repaints work  -- if only the title bar is dirty, the border fill runs
// but its output is clipped away for scanlines outside the dirty rect.
// The clip rect is saved/restored because the caller (compositeAndFlush)
// manages its own clip state across multiple windows.

void wmDrawChrome(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, WindowT *win, const RectT *clipTo) {
    RectT savedClip = clipSave(d);

    setClipRect(d, clipTo->x, clipTo->y, clipTo->w, clipTo->h);

    drawBorderFrame(d, ops, colors, win->x, win->y, win->w, win->h);

    drawTitleBar(d, ops, font, colors, win);

    if (win->menuBar) {
        computeMenuBarPositions(win, font);
        drawMenuBar(d, ops, font, colors, win);
    }

    // Inner sunken bevel frames the content area (and scrollbars if present).
    // The bevel extends around scrollbar space so the entire inset area looks
    // recessed, which is the Motif convention for content wells.
    int32_t innerX = win->x + win->contentX - CHROME_INNER_BORDER;
    int32_t innerY = win->y + win->contentY - CHROME_INNER_BORDER;
    int32_t innerW = win->contentW + CHROME_INNER_BORDER * 2;
    int32_t innerH = win->contentH + CHROME_INNER_BORDER * 2;

    if (win->vScroll) {
        innerW += SCROLLBAR_WIDTH;
    }

    if (win->hScroll) {
        innerH += SCROLLBAR_WIDTH;
    }

    BevelStyleT innerBevel;
    innerBevel.highlight = colors->windowShadow;    // swapped for sunken
    innerBevel.shadow    = colors->windowHighlight;
    innerBevel.face      = BEVEL_NO_FILL; // no fill
    innerBevel.width     = CHROME_INNER_BORDER;
    drawBevel(d, ops, innerX, innerY, innerW, innerH, &innerBevel);

    // GEOS Motif-style resize break indicators
    drawResizeBreaks(d, ops, colors, win);

    clipRestore(d, &savedClip);
}


// Blits the window's persistent content buffer to the display backbuffer,
// clipped to the current dirty rect. This is the payoff of the per-window
// content buffer architecture: the app's rendered output is always available
// in contentBuf, so we can composite it at any time without calling back
// into the app. This eliminates expose/repaint events entirely.
//
// The blit uses direct memcpy rather than going through the rectCopy
// drawing primitive, because we've already computed the exact intersection
// and don't need the general-purpose clip logic. On 486/Pentium, memcpy
// compiles to rep movsd which saturates the memory bus  -- no further
// optimization is possible at this level.

void wmDrawContent(DisplayT *d, const BlitOpsT *ops, WindowT *win, const RectT *clipTo) {
    if (__builtin_expect(!win->contentBuf, 0)) {
        return;
    }

    RectT contentRect;
    contentRect.x = win->x + win->contentX;
    contentRect.y = win->y + win->contentY;
    contentRect.w = win->contentW;
    contentRect.h = win->contentH;

    RectT isect;

    if (!rectIntersect(&contentRect, clipTo, &isect)) {
        return;
    }

    int32_t bpp      = ops->bytesPerPixel;
    int32_t srcX     = isect.x - contentRect.x;
    int32_t srcY     = isect.y - contentRect.y;
    int32_t rowBytes = isect.w * bpp;

    const uint8_t *srcRow = win->contentBuf + srcY * win->contentPitch + srcX * bpp;
    uint8_t       *dstRow = d->backBuf + isect.y * d->pitch + isect.x * bpp;

    for (int32_t i = 0; i < isect.h; i++) {
        memcpy(dstRow, srcRow, rowBytes);
        srcRow += win->contentPitch;
        dstRow += d->pitch;
    }
}


// Draws the minimized window icon strip at the bottom of the screen.
// For each minimized window, draws a beveled ICON_TOTAL_SIZE square
// containing either:
//   1. A loaded icon image (scaled to fit via drawScaledRect)
//   2. A live thumbnail of the window's content buffer (also scaled)
//   3. A grey fill if neither is available
//
// The live thumbnail (option 2) shows miniature window contents in the
// icon/task view, giving a quick preview of each minimized window. The thumbnail
// is rendered from the existing content buffer, so no extra rendering
// pass is needed. iconNeedsRefresh tracks whether the content has changed
// since the last icon refresh.
//
// Icons are drawn before windows in the compositing loop (painter's
// algorithm) so that any windows positioned over the icon strip
// correctly occlude the icons.

void wmDrawMinimizedIcons(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, const WindowStackT *stack, const RectT *clipTo) {
    int32_t iconIdx = 0;

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

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

        int32_t ix;
        int32_t iy;
        wmMinimizedIconPos(d, iconIdx, &ix, &iy);
        iconIdx++;

        // Check if icon intersects clip rect
        if (ix + ICON_TOTAL_SIZE <= clipTo->x || ix >= clipTo->x + clipTo->w ||
            iy + ICON_TOTAL_SIZE <= clipTo->y || iy >= clipTo->y + clipTo->h) {
            continue;
        }

        int32_t bpp = d->format.bytesPerPixel;

        if (win->iconData && win->iconW > 0 && win->iconH > 0) {
            // Provided icon image -- draw edge-to-edge, no bevel
            drawScaledRect(d, ix, iy, ICON_TOTAL_SIZE, ICON_TOTAL_SIZE,
                           win->iconData, win->iconW, win->iconH, win->iconPitch, bpp);
        } else {
            // Live thumbnail or grey fill -- draw with beveled border
            BevelStyleT bevel;
            bevel.highlight = colors->windowHighlight;
            bevel.shadow    = colors->windowShadow;
            bevel.face      = colors->windowFace;
            bevel.width     = ICON_BORDER;
            drawBevel(d, ops, ix, iy, ICON_TOTAL_SIZE, ICON_TOTAL_SIZE, &bevel);

            int32_t contentX = ix + ICON_BORDER;
            int32_t contentY = iy + ICON_BORDER;

            if (win->contentBuf && win->contentW > 0 && win->contentH > 0) {
                drawScaledRect(d, contentX, contentY, ICON_SIZE, ICON_SIZE,
                               win->contentBuf, win->contentW, win->contentH, win->contentPitch, bpp);
            } else {
                rectFill(d, ops, contentX, contentY, ICON_SIZE, ICON_SIZE, colors->windowFace);
            }
        }
    }
}


void wmDrawScrollbars(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, WindowT *win, const RectT *clipTo) {
    RectT savedClip = clipSave(d);

    setClipRect(d, clipTo->x, clipTo->y, clipTo->w, clipTo->h);

    if (win->vScroll) {
        wmDrawScrollbar(d, ops, colors, win->vScroll, win->x, win->y);
    }

    if (win->hScroll) {
        wmDrawScrollbar(d, ops, colors, win->hScroll, win->x, win->y);
    }

    clipRestore(d, &savedClip);
}


void wmDrawVScrollbarAt(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t h, int32_t scrollPos, int32_t visibleItems, int32_t totalItems) {
    int32_t sbW       = SCROLLBAR_WIDTH;
    int32_t trackLen  = h - sbW * 2;
    int32_t thumbPos  = 0;
    int32_t thumbSize = 0;

    if (trackLen > 0 && totalItems > visibleItems) {
        int32_t maxScroll = totalItems - visibleItems;
        thumbSize = (int32_t)(((int64_t)visibleItems * trackLen) / totalItems);

        if (thumbSize < sbW) {
            thumbSize = sbW;
        }

        // maxScroll is > 0 here (guaranteed by totalItems > visibleItems above)
        thumbPos = (int32_t)(((int64_t)scrollPos * (trackLen - thumbSize)) / maxScroll);
    }

    drawScrollbar(d, ops, colors, ScrollbarVerticalE, x, y, h, sbW, thumbPos, thumbSize);
}


// Frees a standalone context menu and all its submenus recursively.
// freeMenuRecursive tears down the submenu tree and item array; this also
// frees the root MenuT itself, which the caller owns.

void wmFreeMenu(MenuT *menu) {
    if (!menu) {
        return;
    }

    freeMenuRecursive(menu);
    free(menu);
}


// Determines which window (and which part of that window) is under the
// mouse cursor. Walks the stack front-to-back (highest index = frontmost)
// so the first hit wins, correctly handling overlapping windows.
//
// Hit part codes:
//   0 = content area    (pass events to app)
//   1 = title bar       (initiate drag)
//   2 = close gadget    (close or system menu)
//   3 = resize edge     (initiate resize, edge flags in wmResizeEdgeHit)
//   4 = menu bar        (open menu dropdown)
//   5 = vertical scroll (scrollbar interaction)
//   6 = horiz scroll    (scrollbar interaction)
//   7 = minimize gadget (minimize window)
//   8 = maximize gadget (maximize/restore toggle)
//
// The test order matters: gadgets are tested before the title bar so that
// clicks on close/min/max aren't consumed as drag initiations. Scrollbars
// are tested before resize edges so that the scrollbar area (which overlaps
// the inner border) doesn't trigger a resize. Content is tested last;
// if the point is inside the frame but doesn't match any specific part,
// it falls through to hitPart=1 (title/chrome), which is correct for
// clicks on the border face between the outer and inner bevels.

int32_t wmHitTest(const WindowStackT *stack, int32_t mx, int32_t my, int32_t *hitPart) {
    for (int32_t i = stack->count - 1; i >= 0; i--) {
        const WindowT *win = stack->windows[i];

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

        // Check if point is within window frame
        if (mx < win->x || mx >= win->x + win->w ||
            my < win->y || my >= win->y + win->h) {
            continue;
        }

        TitleGeomT g;
        computeTitleGeom(win, &g);

        // Close gadget (top-left)
        if (mx >= g.closeX && mx < g.closeX + g.gadgetS &&
            my >= g.gadgetY && my < g.gadgetY + g.gadgetS) {
            *hitPart = HIT_CLOSE;
            return i;
        }

        // Maximize gadget (resizable windows only)
        if (g.maxX >= 0 &&
            mx >= g.maxX && mx < g.maxX + g.gadgetS &&
            my >= g.gadgetY && my < g.gadgetY + g.gadgetS) {
            *hitPart = HIT_MAXIMIZE;
            return i;
        }

        // Minimize gadget (not on modal windows)
        if (g.minX >= 0 &&
            mx >= g.minX && mx < g.minX + g.gadgetS &&
            my >= g.gadgetY && my < g.gadgetY + g.gadgetS) {
            *hitPart = HIT_MINIMIZE;
            return i;
        }

        // Title bar (drag area  -- between gadgets)
        if (my >= g.titleY && my < g.titleY + CHROME_TITLE_HEIGHT &&
            mx >= g.titleX && mx < g.titleX + g.titleW) {
            *hitPart = HIT_TITLE;
            return i;
        }

        // Menu bar  -- bounded to the drawn fill region (drawMenuBar) so the
        // side border at menu height stays grabbable for resize / falls
        // through to content
        if (win->menuBar &&
            my >= win->y + CHROME_TITLEBAR_BOTTOM &&
            my < win->y + CHROME_TITLEBAR_BOTTOM + CHROME_MENU_HEIGHT &&
            mx >= win->x + CHROME_BORDER_WIDTH &&
            mx < win->x + win->w - CHROME_BORDER_WIDTH) {
            *hitPart = HIT_MENU;
            return i;
        }

        // Vertical scrollbar
        if (win->vScroll) {
            int32_t sbX = win->x + win->vScroll->x;
            int32_t sbY = win->y + win->vScroll->y;

            if (mx >= sbX && mx < sbX + SCROLLBAR_WIDTH &&
                my >= sbY && my < sbY + win->vScroll->length) {
                *hitPart = HIT_VSCROLL;
                return i;
            }
        }

        // Horizontal scrollbar
        if (win->hScroll) {
            int32_t sbX = win->x + win->hScroll->x;
            int32_t sbY = win->y + win->hScroll->y;

            if (mx >= sbX && mx < sbX + win->hScroll->length &&
                my >= sbY && my < sbY + SCROLLBAR_WIDTH) {
                *hitPart = HIT_HSCROLL;
                return i;
            }
        }

        // Resize edges (if resizable)
        if (win->resizable) {
            int32_t edge = wmResizeEdgeHit(win, mx, my);

            if (edge != RESIZE_NONE) {
                *hitPart = HIT_RESIZE;
                return i;
            }
        }

        // Content area
        if (mx >= win->x + win->contentX &&
            mx < win->x + win->contentX + win->contentW &&
            my >= win->y + win->contentY &&
            my < win->y + win->contentY + win->contentH) {
            *hitPart = HIT_CONTENT;
            return i;
        }

        // Somewhere on the chrome but not a specific part
        *hitPart = HIT_TITLE;
        return i;
    }

    *hitPart = HIT_NONE;
    return -1;
}


// Initializes the window stack to empty state. All tracking indices are set
// to -1 (sentinel for "no active operation"). The stack itself is zeroed
// which sets count=0 and all window pointers to NULL.

void wmInit(WindowStackT *stack) {
    memset(stack, 0, sizeof(*stack));
    stack->focusedIdx   = -1;
    stack->dragWindow   = -1;
    stack->resizeWindow = -1;
    stack->scrollWindow = -1;
}


// Maximizes a window to fill the screen (or up to maxW/maxH if constrained).
// The pre-maximize geometry is saved in preMax* fields so wmRestore can
// return the window to its original position and size.
//
// The content buffer must be reallocated because the content area changes
// size. After reallocation, onResize notifies the app of the new dimensions,
// then onPaint requests a full repaint into the new buffer. This is
// synchronous  -- the maximize completes in one frame, avoiding flicker.
//
// Both old and new positions are dirtied: old to expose what was behind the
// window at its previous size, new to paint the window at its maximized size.
// When maximizing to full screen, the old rect is a subset of the new one,
// so dirtyListMerge will collapse them into a single full-screen rect.

void wmMaximize(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT *win) {
    (void)stack;

    if (win->maximized) {
        return;
    }

    win->preMaxX = win->x;
    win->preMaxY = win->y;
    win->preMaxW = win->w;
    win->preMaxH = win->h;

    dirtyListAdd(dl, win->x, win->y, win->w, win->h);

    int32_t newW;
    int32_t newH;

    wmEffectiveMaxSize(win, d, &newW, &newH);

    win->x         = 0;
    win->y         = 0;
    win->w         = newW;
    win->h         = newH;
    win->maximized = true;

    wmResizeCommit(win, d);

    dirtyListAdd(dl, win->x, win->y, win->w, win->h);
}


// wmMenuFindItemInMenu -- recursively find an item by command ID within a
// single menu tree (the menu itself plus every nested submenu).  Used for
// popup/context menus that are not attached to a menu bar, so the menu-bar
// lookups (wmMenuFindItem) do not apply.
MenuItemT *wmMenuFindItemInMenu(MenuT *menu, int32_t id) {
    return wmMenuFindItemRecursive(menu, id, NULL);
}


bool wmMenuItemIsChecked(MenuBarT *bar, int32_t id) {
    MenuItemT *item = wmMenuFindItem(bar, id, NULL);
    return item ? item->checked : false;
}


void wmMenuItemSetChecked(MenuBarT *bar, int32_t id, bool checked) {
    MenuT *menu = NULL;
    MenuItemT *item = wmMenuFindItem(bar, id, &menu);

    if (!item) {
        return;
    }

    menuItemApplyChecked(menu, (int32_t)(item - menu->items), checked);
}


void wmMenuItemSetCheckedInMenu(MenuT *menu, int32_t id, bool checked) {
    // The item may be nested in a submenu; locate both the item and the menu
    // that directly contains it so radio-group siblings (which live in that
    // same containing menu) can be unchecked.
    if (!menu) {
        return;
    }

    MenuT     *owner = NULL;
    MenuItemT *item  = wmMenuFindItemRecursive(menu, id, &owner);

    if (item) {
        menuItemApplyChecked(owner, (int32_t)(item - owner->items), checked);
    }
}


void wmMenuItemSetEnabled(MenuBarT *bar, int32_t id, bool enabled) {
    MenuItemT *item = wmMenuFindItem(bar, id, NULL);

    if (item) {
        item->enabled = enabled;
    }
}


// Minimizes a window: marks it minimized so it's skipped during compositing
// (except for icon drawing) and moves focus to the next available window.
// The window's geometry and content buffer are preserved  -- no reallocation
// is needed since the window retains its size and will be restored to the
// same position.
//
// Focus is moved to the topmost non-minimized window by walking the stack
// from top down. If no such window exists, focusedIdx becomes -1 (no focus).
// The dirtied area covers the window's full frame so the compositor repaints
// the exposed region behind it.

void wmMinimize(WindowStackT *stack, DirtyListT *dl, WindowT *win) {
    if (win->minimized) {
        return;
    }

    dirtyListAdd(dl, win->x, win->y, win->w, win->h);

    win->minimized = true;

    // Only a focused window gives up focus; minimizing a background window
    // must not fire onBlur/onFocus on unrelated windows.
    if (!win->focused) {
        return;
    }

    for (int32_t i = stack->count - 1; i >= 0; i--) {
        if (stack->windows[i]->visible && !stack->windows[i]->minimized) {
            wmSetFocus(stack, dl, i);
            return;
        }
    }

    if (stack->focusedIdx >= 0 && stack->focusedIdx < stack->count) {
        WindowT *oldWin = stack->windows[stack->focusedIdx];

        // Clear the stale focused flag before dropping focus to -1, mirroring
        // wmSetFocus's invariant that only the focusedIdx window has it set.
        oldWin->focused = false;

        if (oldWin->onBlur) {
            oldWin->onBlur(oldWin);
        }
    }

    stack->focusedIdx = -1;
}


// Tests whether a mouse click landed on a minimized window icon. Returns
// the stack index of the hit window (NOT the icon index) so the caller can
// directly use it with wmRestoreMinimized. The icon index is computed
// internally to get the position, but the stack index is what the caller
// needs to identify the window.

int32_t wmMinimizedIconHit(const WindowStackT *stack, const DisplayT *d, int32_t mx, int32_t my) {
    int32_t iconIdx = 0;

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

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

        int32_t ix;
        int32_t iy;
        wmMinimizedIconPos(d, iconIdx, &ix, &iy);
        iconIdx++;

        if (mx >= ix && mx < ix + ICON_TOTAL_SIZE &&
            my >= iy && my < iy + ICON_TOTAL_SIZE) {
            return i;
        }
    }

    return -1;
}


// Computes the screen position of a minimized window icon. Icons are laid
// out left to right along the bottom of the screen, wrapping to the next
// row upward when the current row is full. This mirrors the Windows 3.x
// convention. If rows wrap past the top of the screen the icons are simply
// clipped by the compositor.
//
// The index is the ordinal among minimized windows (not the stack index),
// so icon positions stay packed when non-minimized windows exist between
// minimized ones in the stack. This avoids gaps in the icon strip.

void wmMinimizedIconPos(const DisplayT *d, int32_t index, int32_t *x, int32_t *y) {
    int32_t cellW   = ICON_TOTAL_SIZE + ICON_SPACING;
    int32_t perRow  = (d->width - ICON_SPACING) / cellW;

    if (perRow < 1) {
        perRow = 1;
    }

    int32_t col = index % perRow;
    int32_t row = index / perRow;

    *x = ICON_SPACING + col * cellW;
    *y = d->height - (row + 1) * (ICON_TOTAL_SIZE + ICON_SPACING);
}


void wmMinimizedIconRect(const WindowStackT *stack, const DisplayT *d, int32_t *ry, int32_t *rh) {
    int32_t count = 0;

    for (int32_t i = 0; i < stack->count; i++) {
        if (stack->windows[i]->visible && stack->windows[i]->minimized) {
            count++;
        }
    }

    if (count == 0) {
        *ry = d->height;
        *rh = 0;
        return;
    }

    // Find the topmost icon position (last icon has highest row index)
    int32_t topX;
    int32_t topY;
    wmMinimizedIconPos(d, count - 1, &topX, &topY);

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

    *ry = topY;
    *rh = d->height - topY;
}


// Computes the minimum allowed window size based on the window's current
// chrome configuration. This is dynamic rather than a fixed constant because
// the minimum depends on which optional chrome elements are present: menu
// bars add minimum width (must show all labels), scrollbars add minimum
// height (must fit arrow buttons + minimum thumb), and the title bar gadget
// set varies between resizable and non-resizable windows.
//
// The minimum width is the larger of:
//   - Title bar minimum: close + 1 char of title + min + (max if resizable)
//   - Menu bar minimum: must show all menu labels (if present)
// The minimum height accounts for top chrome + bottom chrome + 1px content
// + menu bar (if present) + scrollbar space (if present).
//
// This function is called on every resize move event, so it must be cheap.
// No allocations; the only string work is measuring the menu-bar label
// widths (when a menu bar is present) to size the minimum width.

void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH) {
    int32_t titleMinW = GADGET_PAD + GADGET_SIZE + GADGET_PAD + FONT_CHAR_WIDTH + GADGET_PAD + GADGET_SIZE + GADGET_PAD;

    if (win->resizable) {
        titleMinW += GADGET_SIZE + GADGET_PAD;
    }

    *minW = titleMinW + CHROME_BORDER_WIDTH * 2;

    // Menu bar width: compute directly from label text (don't rely on
    // cached barX/barW which may not be computed yet before first paint).
    if (win->menuBar && win->menuBar->menuCount > 0) {
        int32_t menuW = CHROME_TOTAL_SIDE;

        for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
            menuW += menuLabelWidth(&dvxFont8x16, win->menuBar->menus[i]->label);

            if (i < win->menuBar->menuCount - 1) {
                menuW += MENU_BAR_GAP;
            }
        }

        menuW += CHROME_TOTAL_SIDE;

        if (menuW > *minW) {
            *minW = menuW;
        }
    }

    // Minimum height: border + title + inner border + some content + bottom chrome
    *minH = CHROME_TOTAL_TOP + CHROME_TOTAL_BOTTOM + 1;

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

    // If scrollbars present, content area must fit arrow buttons + minimum thumb
    int32_t minScrollLen = SCROLLBAR_WIDTH * 3;  // 2 arrows + 1 thumb

    if (win->vScroll) {
        int32_t minContentH = minScrollLen;

        if (win->hScroll) {
            minContentH += SCROLLBAR_WIDTH;  // room for hscroll track
        }

        int32_t topChrome = CHROME_TOTAL_TOP;

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

        int32_t needH = topChrome + minContentH + CHROME_TOTAL_BOTTOM;

        if (needH > *minH) {
            *minH = needH;
        }

        // vscroll also takes width
        int32_t needW = CHROME_TOTAL_SIDE * 2 + SCROLLBAR_WIDTH + 1;

        if (needW > *minW) {
            *minW = needW;
        }
    }

    if (win->hScroll) {
        int32_t minContentW = minScrollLen;

        if (win->vScroll) {
            minContentW += SCROLLBAR_WIDTH;  // room for vscroll track
        }

        int32_t needW = CHROME_TOTAL_SIDE * 2 + minContentW;

        if (needW > *minW) {
            *minW = needW;
        }

        // hscroll also takes height
        int32_t topChrome = CHROME_TOTAL_TOP;

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

        int32_t needH = topChrome + SCROLLBAR_WIDTH + 1 + CHROME_TOTAL_BOTTOM;

        if (needH > *minH) {
            *minH = needH;
        }
    }
}


// Moves a window to the top of the z-order stack. Implemented by shifting
// all windows above it down by one slot and placing the raised window at
// count-1. This is O(N) but N <= 64 and raise only happens on user clicks,
// so the cost is negligible.
//
// The window area is dirtied because its position in the paint order changed:
// it was previously occluded by windows above it, and now it's on top.
// The compositor will repaint the affected region with the correct z-order.
//
// focusedIdx and dragWindow are index-based references into the stack array,
// so they must be adjusted when entries shift. Any index above the raised
// window's old position decreases by 1 (entries shifted down); the raised
// window itself moves to count-1.

void wmRaiseWindow(WindowStackT *stack, DirtyListT *dl, int32_t idx) {
    if (idx < 0 || idx >= stack->count - 1) {
        return;
    }

    WindowT *win = stack->windows[idx];

    for (int32_t i = idx; i < stack->count - 1; i++) {
        stack->windows[i] = stack->windows[i + 1];
    }

    stack->windows[stack->count - 1] = win;

    dirtyListAdd(dl, win->x, win->y, win->w, win->h);

    // focusedIdx / dragWindow / resizeWindow / scrollWindow are stack-index
    // references, so they must shift with the raise (wmDestroyWindow adjusts
    // the same set on removal): a raise during an active scroll or resize --
    // e.g. an onScroll callback raising a window -- would otherwise leave the
    // next wmResizeMove / wmScrollbarDrag operating on the wrong slot.
    stack->focusedIdx   = wmAdjustIndexForRaise(stack->focusedIdx, idx, stack->count - 1);
    stack->dragWindow   = wmAdjustIndexForRaise(stack->dragWindow, idx, stack->count - 1);
    stack->resizeWindow = wmAdjustIndexForRaise(stack->resizeWindow, idx, stack->count - 1);
    stack->scrollWindow = wmAdjustIndexForRaise(stack->scrollWindow, idx, stack->count - 1);
}


// Frees and reallocates the content buffer to match the current contentW/H.
// Called after any geometry change (resize, maximize, restore). The old
// buffer contents are discarded  -- the caller is expected to trigger
// onResize + onPaint to refill it. This is simpler (and on 486, faster)
// than copying and scaling the old content.
//
// The buffer is initialized to 0xFF (white) so any area the app doesn't
// paint will show a clean background rather than garbage.

int32_t wmReallocContentBuf(WindowT *win, const DisplayT *d) {
    free(win->contentBuf);
    win->contentBuf = NULL;

    win->contentPitch = win->contentW * d->format.bytesPerPixel;
    int32_t bufSize   = win->contentPitch * win->contentH;

    if (bufSize <= 0) {
        return 0;
    }

    win->contentBuf = (uint8_t *)malloc(bufSize);

    if (!win->contentBuf) {
        return -1;
    }

    memset(win->contentBuf, CONTENT_CLEAR_BYTE, bufSize);

    return 0;
}


// wmRemoveMenuItem -- remove a menu item by command ID

bool wmRemoveMenuItem(MenuT *menu, int32_t id) {
    if (!menu) {
        return false;
    }

    for (int32_t i = 0; i < menu->itemCount; i++) {
        if (menu->items[i].id == id && !menu->items[i].separator) {
            // Detach only -- do NOT free the item's subMenu tree here.  An
            // open popup chain may still reference it directly (see the
            // matching comment in wmClearMenuItems), so the tree is
            // deliberately orphaned rather than freed under the popup.
            memmove(&menu->items[i], &menu->items[i + 1], (menu->itemCount - i - 1) * sizeof(MenuItemT));
            menu->itemCount--;
            return true;
        }
    }

    return false;
}


// Frees both window scrollbars (if present) and clears the slots.  The
// content rect is NOT recomputed here; callers that keep the window alive
// (widgetManageScrollbars) call wmUpdateContentRect themselves.
void wmRemoveScrollbars(WindowT *win) {
    free(win->vScroll);
    free(win->hScroll);
    win->vScroll = NULL;
    win->hScroll = NULL;
}


// Initiates a window resize. Unlike drag (which stores mouse-to-origin
// offset), resize stores the absolute mouse position. wmResizeMove computes
// the delta from this position each frame and then resets it to the
// (possibly clamped) edge position, warping the cursor to match.

void wmResizeBegin(WindowStackT *stack, int32_t idx, int32_t edge, int32_t mouseX, int32_t mouseY) {
    stack->resizeWindow = idx;
    stack->resizeEdge   = edge;
    stack->dragOffX     = mouseX;
    stack->dragOffY     = mouseY;
}


// Applies a completed window geometry change: recomputes the content rect,
// reallocates the content buffer, notifies the app via onResize, then
// requests a full repaint into the new buffer. Shared by mouse resize,
// keyboard resize, maximize, and restore so every path repaints the same
// way. PAINT_FULL is required  -- widgetOnPaint only relays out and paints
// the whole widget tree (not just dirty widgets) at that level.

void wmResizeCommit(WindowT *win, const DisplayT *d) {
    wmUpdateContentRect(win);

    if (wmReallocContentBuf(win, d) != 0) {
        return;
    }

    if (win->onResize) {
        win->onResize(win, win->contentW, win->contentH);
    }

    if (win->onPaint) {
        win->paintNeeded = PAINT_FULL;
        RectT fullRect = {0, 0, win->contentW, win->contentH};
        win->onPaint(win, &fullRect);
        // widgetOnPaint clears paintNeeded itself; clear it for raw-paint
        // windows too so the deferred flush doesn't repaint this frame.
        // Callers add their own dirty rects.
        win->paintNeeded      = PAINT_NONE;
        win->iconNeedsRefresh = true;
    }
}


// Determines which edge(s) of a window the mouse is over. Returns a bitmask
// of RESIZE_LEFT/RIGHT/TOP/BOTTOM flags. Corner hits (e.g. top-left) return
// two flags OR'd together, enabling diagonal resize.
//
// The grab area extends 2 pixels beyond the visual border (CHROME_BORDER_WIDTH
// + 2 = 6px) to make edges easier to grab, especially the 4px-wide border
// which would be frustratingly small on its own. This is a common usability
// trick  -- the visual border is narrower than the hit zone.

int32_t wmResizeEdgeHit(const WindowT *win, int32_t mx, int32_t my) {
    int32_t edge   = RESIZE_NONE;
    int32_t border = CHROME_BORDER_WIDTH + RESIZE_GRAB_SLOP;

    if (mx >= win->x && mx < win->x + border) {
        edge |= RESIZE_LEFT;
    }

    if (mx >= win->x + win->w - border && mx < win->x + win->w) {
        edge |= RESIZE_RIGHT;
    }

    if (my >= win->y && my < win->y + border) {
        edge |= RESIZE_TOP;
    }

    if (my >= win->y + win->h - border && my < win->y + win->h) {
        edge |= RESIZE_BOTTOM;
    }

    return edge;
}


void wmResizeEnd(WindowStackT *stack) {
    stack->resizeWindow = -1;
    stack->resizeEdge   = RESIZE_NONE;
}


// Called on each mouse move during a resize operation. Computes the delta
// from the last mouse position (stored in dragOffX/Y) and applies it to
// the appropriate edges based on resizeEdge flags.
//
// Left and top edges are special: resizing from the left/top moves the
// window origin AND changes the size, so both x/y and w/h are adjusted.
// Right and bottom edges only change w/h. Each axis is independently
// clamped to [minW/minH, maxW/maxH].
//
// After resizing, the content buffer is reallocated and the app is notified
// via onResize + onPaint. dragOffX/Y are always reset to the clamped edge
// position, and that position is reported back so the caller can warp the
// cursor onto the edge. Keeping the cursor pinned to the border is what
// prevents a dead zone when the user reverses direction at a size limit.
//
// If the user resizes while maximized, the maximized flag is cleared.
// This prevents wmRestore from snapping back to the pre-maximize geometry,
// which would be confusing  -- the user's manual resize represents their
// new intent.

void wmResizeMove(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, int32_t *mouseX, int32_t *mouseY) {
    if (stack->resizeWindow < 0 || stack->resizeWindow >= stack->count) {
        return;
    }

    WindowT *win = stack->windows[stack->resizeWindow];
    int32_t  mx  = *mouseX;
    int32_t  my  = *mouseY;
    int32_t  dx  = mx - stack->dragOffX;
    int32_t  dy  = my - stack->dragOffY;

    // Compute dynamic minimum size for this window
    int32_t minW;
    int32_t minH;
    wmMinWindowSize(win, &minW, &minH);

    // Compute effective maximum size
    int32_t maxW;
    int32_t maxH;

    wmEffectiveMaxSize(win, d, &maxW, &maxH);

    // Mark old position dirty
    dirtyListAdd(dl, win->x, win->y, win->w, win->h);

    if (stack->resizeEdge & RESIZE_LEFT) {
        int32_t newX = win->x + dx;
        int32_t newW = win->w - dx;

        if (newW > maxW) {
            newX += newW - maxW;
            newW  = maxW;
        }

        if (newX < 0) {
            newW += newX;
            newX  = 0;
        }

        if (newW < minW) {
            newW = minW;
            newX = win->x + win->w - minW;
        }

        // Keep the window reachable: if the right edge is off-screen,
        // dragging the left edge inward must not move the whole window
        // past the right side of the screen.
        if (newX > d->width - DRAG_MIN_VISIBLE) {
            newW += newX - (d->width - DRAG_MIN_VISIBLE);
            newX  = d->width - DRAG_MIN_VISIBLE;
        }

        win->x = newX;
        win->w = newW;
        mx     = newX;
    }

    if (stack->resizeEdge & RESIZE_RIGHT) {
        int32_t newW = win->w + dx;

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

        if (win->x + newW > d->width) {
            newW = d->width - win->x;
        }

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

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

        win->w = newW;
        mx     = win->x + newW;
    }

    if (stack->resizeEdge & RESIZE_TOP) {
        int32_t newY = win->y + dy;
        int32_t newH = win->h - dy;

        if (newH > maxH) {
            newY += newH - maxH;
            newH  = maxH;
        }

        if (newY < 0) {
            newH += newY;
            newY  = 0;
        }

        if (newH < minH) {
            newH = minH;
            newY = win->y + win->h - minH;
        }

        // Keep the title bar reachable: if the bottom edge is off-screen,
        // dragging the top edge down must not push the title bar below
        // the screen.
        if (newY > d->height - CHROME_TITLEBAR_BOTTOM) {
            newH += newY - (d->height - CHROME_TITLEBAR_BOTTOM);
            newY  = d->height - CHROME_TITLEBAR_BOTTOM;
        }

        win->y = newY;
        win->h = newH;
        my     = newY;
    }

    if (stack->resizeEdge & RESIZE_BOTTOM) {
        int32_t newH = win->h + dy;

        if (newH > maxH) {
            newH = maxH;
        }

        if (win->y + newH > d->height) {
            newH = d->height - win->y;
        }

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

        win->h = newH;
        my     = win->y + newH;
    }

    // Resizing clears the maximized state.
    win->maximized = false;

    wmResizeCommit(win, d);

    // Always update dragOff to the clamped position so the next delta
    // is computed from the edge, not from where the mouse wandered.
    stack->dragOffX = mx;
    stack->dragOffY = my;

    // Report clamped position back so the caller can warp the cursor
    *mouseX = mx;
    *mouseY = my;

    // Mark new position dirty
    dirtyListAdd(dl, win->x, win->y, win->w, win->h);
}


// Restores a maximized window to its pre-maximize geometry. This is the
// inverse of wmMaximize: saves nothing (the pre-max geometry was already
// saved), just restores x/y/w/h from preMax* fields, reallocates the
// content buffer, and triggers repaint.

void wmRestore(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT *win) {
    (void)stack;

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

    // Mark current position dirty
    dirtyListAdd(dl, win->x, win->y, win->w, win->h);

    // Restore saved geometry
    win->x         = win->preMaxX;
    win->y         = win->preMaxY;
    win->w         = win->preMaxW;
    win->h         = win->preMaxH;
    win->maximized = false;

    wmResizeCommit(win, d);

    // Mark restored position dirty
    dirtyListAdd(dl, win->x, win->y, win->w, win->h);
}


// Restores a minimized window: clears the minimized flag, dirties the
// icon strip so the old icon is erased, raises the window to the top
// of the z-order, and gives it focus.

void wmRestoreMinimized(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT *win) {
    if (!win->minimized) {
        return;
    }

    // Dirty the icon strip area so the old minimized icon is erased
    int32_t iconY;
    int32_t iconH;
    wmMinimizedIconRect(stack, d, &iconY, &iconH);
    dirtyListAdd(dl, 0, iconY, d->width, iconH);

    win->minimized = false;

    for (int32_t i = 0; i < stack->count; i++) {
        if (stack->windows[i] == win) {
            wmRaiseWindow(stack, dl, i);
            wmSetFocus(stack, dl, stack->count - 1);
            break;
        }
    }

    dirtyListAdd(dl, win->x, win->y, win->w, win->h);
}


// Handles a mouse click on a scrollbar. Determines which sub-region was
// clicked and applies the appropriate scroll action:
//   - Arrow buttons: step by 1 unit
//   - Thumb: begin drag (stores offset, returns without changing value)
//   - Trough above/left of thumb: page up/left
//   - Trough below/right of thumb: page down/right
//
// The thumb drag case is special: it doesn't change the scroll value
// immediately but instead records the drag state in the WindowStackT.
// Subsequent mouse moves are handled by wmScrollbarDrag until mouseup
// calls wmScrollbarEnd.
//
// The scroll value is clamped to [min, max] and the scrollbar area is
// dirtied only if the value actually changed, avoiding unnecessary
// repaints when clicking at the min/max limit.

void wmScrollbarClick(WindowStackT *stack, DirtyListT *dl, int32_t idx, ScrollbarOrientE orient, int32_t mx, int32_t my) {
    if (idx < 0 || idx >= stack->count) {
        return;
    }

    WindowT    *win = stack->windows[idx];
    ScrollbarT *sb  = (orient == ScrollbarVerticalE) ? win->vScroll : win->hScroll;

    if (!sb) {
        return;
    }

    int32_t sbScreenX = win->x + sb->x;
    int32_t sbScreenY = win->y + sb->y;
    int32_t range     = sb->max - sb->min;

    if (range <= 0) {
        return;
    }

    int32_t thumbPos;
    int32_t thumbSize;
    int32_t trackLen = scrollbarThumbInfo(sb, &thumbPos, &thumbSize);

    if (trackLen <= 0) {
        return;
    }

    // Reduce to one axis: the mouse offset along the bar from its origin.
    int32_t oldValue   = sb->value;
    bool    vertical   = (sb->orient == ScrollbarVerticalE);
    int32_t rel        = vertical ? my - sbScreenY : mx - sbScreenX;
    int32_t thumbStart = SCROLLBAR_WIDTH + thumbPos;

    if (rel < SCROLLBAR_WIDTH) {
        // Decrement arrow button
        sb->value -= 1;
    } else if (rel >= sb->length - SCROLLBAR_WIDTH) {
        // Increment arrow button
        sb->value += 1;
    } else if (rel >= thumbStart && rel < thumbStart + thumbSize) {
        // Thumb: begin drag
        stack->scrollWindow  = idx;
        stack->scrollOrient  = sb->orient;
        stack->scrollDragOff = rel - thumbStart;
        return;
    } else if (rel < thumbStart) {
        // Trough before thumb
        sb->value -= sb->pageSize;
    } else {
        // Trough after thumb
        sb->value += sb->pageSize;
    }

    scrollbarCommitValue(win, sb, dl, sbScreenX, sbScreenY, oldValue);
}


// Handles ongoing thumb drag during scrollbar interaction. Converts the
// mouse pixel position (relative to the track) into a scroll value using
// linear interpolation: value = min + (mousePos * range) / (trackLen - thumbSize).
//
// scrollDragOff is the offset from the mouse to the thumb's leading edge,
// captured at drag start. This keeps the thumb anchored to the original
// click point rather than snapping its top/left edge to the cursor.

void wmScrollbarDrag(WindowStackT *stack, DirtyListT *dl, int32_t mx, int32_t my) {
    if (stack->scrollWindow < 0 || stack->scrollWindow >= stack->count) {
        return;
    }

    WindowT    *win = stack->windows[stack->scrollWindow];
    ScrollbarT *sb  = (stack->scrollOrient == ScrollbarVerticalE) ? win->vScroll : win->hScroll;

    if (!sb) {
        wmScrollbarEnd(stack);
        return;
    }

    int32_t sbScreenX = win->x + sb->x;
    int32_t sbScreenY = win->y + sb->y;
    int32_t range     = sb->max - sb->min;

    if (range <= 0) {
        return;
    }

    int32_t thumbPos;
    int32_t thumbSize;
    int32_t trackLen = scrollbarThumbInfo(sb, &thumbPos, &thumbSize);

    if (trackLen <= 0 || trackLen <= thumbSize) {
        return;
    }

    int32_t oldValue = sb->value;
    int32_t mousePos;

    if (sb->orient == ScrollbarVerticalE) {
        mousePos = my - sbScreenY - SCROLLBAR_WIDTH - stack->scrollDragOff;
    } else {
        mousePos = mx - sbScreenX - SCROLLBAR_WIDTH - stack->scrollDragOff;
    }

    // Convert pixel position to value
    sb->value = sb->min + (int32_t)(((int64_t)mousePos * range) / (trackLen - thumbSize));

    scrollbarCommitValue(win, sb, dl, sbScreenX, sbScreenY, oldValue);
}


void wmScrollbarEnd(WindowStackT *stack) {
    stack->scrollWindow = -1;
}


// Transfers keyboard focus from the current window to a new one. Only the
// title bar areas of the old and new windows are dirtied (not the entire
// windows), because focus only changes the title bar color (active vs
// inactive). This is a significant optimization during click-to-focus:
// painting two title bars is much cheaper than repainting two entire windows.

void wmSetFocus(WindowStackT *stack, DirtyListT *dl, int32_t idx) {
    if (idx < 0 || idx >= stack->count) {
        return;
    }

    // A window queued for deferred destruction is INERT: focusing it
    // would fire onFocus into app state that may already be torn down.
    if (stack->windows[idx]->destroyPending) {
        return;
    }

    // Unfocus old window
    WindowT *oldWin = NULL;

    if (stack->focusedIdx >= 0 && stack->focusedIdx < stack->count) {
        oldWin = stack->windows[stack->focusedIdx];
        oldWin->focused = false;

        // Dirty the old title bar
        dirtyListAdd(dl, oldWin->x, oldWin->y,
                     oldWin->w, CHROME_TITLEBAR_BOTTOM);
    }

    // Focus new window
    stack->focusedIdx = idx;
    WindowT *newWin = stack->windows[idx];
    newWin->focused = true;

    // Dirty the new title bar
    dirtyListAdd(dl, newWin->x, newWin->y,
                 newWin->w, CHROME_TITLEBAR_BOTTOM);

    // Fire callbacks (after state is updated)
    if (oldWin && oldWin != newWin && oldWin->onBlur) {
        oldWin->onBlur(oldWin);
    }

    if (newWin != oldWin && newWin->onFocus) {
        newWin->onFocus(newWin);
    }
}


// Loads an icon image from disk and converts it to the display's pixel
// format for fast blitting during minimized icon rendering. stb_image
// handles format decoding (PNG, BMP, etc.) and the RGB->display format
// conversion happens once at load time, so drawScaledRect can blit
// directly without per-pixel format conversion during compositing.
//
// The icon is stored at its original resolution; drawScaledRect handles
// scaling to ICON_SIZE at draw time. This avoids quality loss from
// double-scaling if the icon is a different size than ICON_SIZE.

int32_t wmSetIcon(WindowT *win, const char *path, const DisplayT *d) {
    int imgW;
    int imgH;
    int channels;
    uint8_t *data = stbi_load(path, &imgW, &imgH, &channels, 3);

    if (!data) {
        return -1;
    }

    int32_t bpp   = d->format.bytesPerPixel;
    int32_t pitch = imgW * bpp;
    uint8_t *buf  = (uint8_t *)malloc(pitch * imgH);

    if (!buf) {
        stbi_image_free(data);
        return -1;
    }

    // Convert RGB to display pixel format
    for (int32_t y = 0; y < imgH; y++) {
        for (int32_t x = 0; x < imgW; x++) {
            const uint8_t *src = data + (y * imgW + x) * 3;
            uint32_t color     = packColor(d, src[0], src[1], src[2]);
            uint8_t *dst       = buf + y * pitch + x * bpp;

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

    stbi_image_free(data);

    // Free old icon if present
    free(win->iconData);

    win->iconData  = buf;
    win->iconW     = imgW;
    win->iconH     = imgH;
    win->iconPitch = pitch;

    return 0;
}


// Updates a window's title text and dirties only the title bar area. The
// dirty rect is precisely the title bar strip (border width + title height),
// avoiding a full-window repaint for what is purely a chrome change.

void wmSetTitle(WindowT *win, DirtyListT *dl, const char *title) {
    copyLabel(win->title, title, MAX_TITLE_LEN);

    // Dirty the title bar area
    dirtyListAdd(dl, win->x + CHROME_BORDER_WIDTH,
                 win->y + CHROME_BORDER_WIDTH,
                 win->w - CHROME_BORDER_WIDTH * 2,
                 CHROME_TITLE_HEIGHT);
}


// Recomputes the content area position and dimensions from the window's
// frame dimensions, accounting for all chrome: outer border, title bar,
// optional menu bar, inner border, and optional scrollbars.
//
// The content rect is expressed as an offset (contentX/Y) from the window
// origin plus width/height. This is relative to the window, not the screen,
// so it doesn't change when the window is dragged.
//
// Scrollbar positions are also computed here because they depend on the
// content area geometry. The order matters: vertical scrollbar steals from
// contentW first, then horizontal scrollbar steals from contentH, then
// the vertical scrollbar's length is adjusted to account for the horizontal
// scrollbar's presence. This creates the standard L-shaped layout where the
// scrollbars meet at the bottom-right corner with a small dead zone between
// them (the corner square where both scrollbars would overlap is simply not
// covered  -- it shows the window face color from drawBorderFrame).

void wmUpdateContentRect(WindowT *win) {
    int32_t topChrome = CHROME_TOTAL_TOP;

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

    win->contentX = CHROME_TOTAL_SIDE;
    win->contentY = topChrome;
    win->contentW = win->w - CHROME_TOTAL_SIDE * 2;
    win->contentH = win->h - topChrome - CHROME_TOTAL_BOTTOM;

    if (win->vScroll) {
        win->contentW -= SCROLLBAR_WIDTH;
        win->vScroll->x      = win->contentX + win->contentW;
        win->vScroll->y      = win->contentY;
        win->vScroll->length = win->contentH;
    }

    if (win->hScroll) {
        win->contentH -= SCROLLBAR_WIDTH;
        win->hScroll->x      = win->contentX;
        win->hScroll->y      = win->contentY + win->contentH;
        win->hScroll->length = win->contentW;

        // Vertical scrollbar must stop short of the horizontal scrollbar
        if (win->vScroll) {
            win->vScroll->length = win->contentH;
        }
    }

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

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

    // The content clamps above do not propagate into the scrollbar lengths
    // (assigned from the pre-clamp content size), so a window smaller than its
    // chrome would leave a negative length feeding scrollbarThumbInfo/draw.
    if (win->vScroll && win->vScroll->length < 0) {
        win->vScroll->length = 0;
    }

    if (win->hScroll && win->hScroll->length < 0) {
        win->hScroll->length = 0;
    }
}
