// 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 MAX_WINDOWS=64 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" #include "dvxComp.h" #include "dvxWidget.h" #include "thirdparty/stb_image.h" #include #include #include // ============================================================ // 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 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 SB_ARROW_ROWS 4 // number of rows in scrollbar arrow glyph #define SB_ARROW_HALF 2 // half-width of scrollbar arrow tip // ============================================================ // 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 void computeMenuBarPositions(WindowT *win, const BitmapFontT *font); static void computeTitleGeom(const WindowT *win, TitleGeomT *g); static void freeMenuRecursive(MenuT *menu); 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 drawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, const ScrollbarT *sb, int32_t winX, int32_t winY); static void drawScrollbarArrow(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size, int32_t dir); 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 minimizedIconPos(const DisplayT *d, int32_t index, int32_t *x, int32_t *y); static int32_t scrollbarThumbInfo(const ScrollbarT *sb, int32_t *thumbPos, int32_t *thumbSize); static void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH); // ============================================================ // computeMenuBarPositions // ============================================================ // 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 = textWidthAccel(font, menu->label) + CHROME_TITLE_PAD * 2; menu->barX = x; menu->barW = labelW; x += labelW + MENU_BAR_GAP; } win->menuBar->positionsDirty = false; } // ============================================================ // computeTitleGeom // ============================================================ // // Compute title bar gadget positions. Used by both drawTitleBar() // and wmHitTest() to keep geometry in sync. // // Layout follows the DESQview/X + GEOS Ensemble convention: // [Close] ---- Title Text ---- [Minimize] [Maximize] // Close is on the far left (also doubles as system menu in DV/X style), // 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 = g->titleH - GADGET_INSET * 2; 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; } // ============================================================ // drawBorderFrame // ============================================================ // // 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 + 4, y + 4, w - 8, h - 8, 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); } // ============================================================ // drawTitleGadget // ============================================================ // // 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); } // ============================================================ // drawMenuBar // ============================================================ // // 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_BORDER_WIDTH + CHROME_TITLE_HEIGHT; 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 menu bar bounds to prevent label overflow int32_t savedClipX = d->clipX; int32_t savedClipY = d->clipY; int32_t savedClipW = d->clipW; int32_t savedClipH = d->clipH; setClipRect(d, win->x + CHROME_BORDER_WIDTH, barY, win->w - CHROME_BORDER_WIDTH * 2, barH); 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); } } setClipRect(d, savedClipX, savedClipY, savedClipW, savedClipH); drawHLine(d, ops, win->x + CHROME_BORDER_WIDTH, barY + barH - 1, win->w - CHROME_BORDER_WIDTH * 2, colors->windowShadow); } // ============================================================ // drawResizeBreaks // ============================================================ // // 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); } // ============================================================ // drawScaledRect // ============================================================ // // 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_SIZE=64 // entries * 4 bytes = 256 bytes that would otherwise be allocated on // every call during the compositing loop). // - 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_SIZE]; static int32_t srcYTab[ICON_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]); } } } } // ============================================================ // drawScrollbar // ============================================================ // // Renders a complete scrollbar: trough background, sunken bevel around the // trough, arrow buttons at each end, and a draggable thumb. // // The layering order matters: trough fill first, then sunken bevel over it // (so bevel edges aren't overwritten), then arrow button bevels + glyphs // over the trough ends, and finally the thumb bevel in the middle. Arrow // buttons and thumb use 1px raised bevels for a clickable appearance; // the trough uses a 1px sunken bevel (swapped highlight/shadow) to appear // recessed — standard Motif scrollbar convention. // // 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 drawScrollbar(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; BevelStyleT btnBevel; btnBevel.highlight = colors->windowHighlight; btnBevel.shadow = colors->windowShadow; btnBevel.face = colors->buttonFace; btnBevel.width = 1; if (sb->orient == ScrollbarVerticalE) { // Trough: dithered pattern (GEOS Motif style) rectFill(d, ops, x, y, SCROLLBAR_WIDTH, sb->length, colors->scrollbarTrough); // Sunken bevel around trough BevelStyleT troughBevel; troughBevel.highlight = colors->windowShadow; troughBevel.shadow = colors->windowHighlight; troughBevel.face = 0; troughBevel.width = 1; drawBevel(d, ops, x, y, SCROLLBAR_WIDTH, sb->length, &troughBevel); // Up arrow button drawBevel(d, ops, x, y, SCROLLBAR_WIDTH, SCROLLBAR_WIDTH, &btnBevel); drawScrollbarArrow(d, ops, colors, x, y, SCROLLBAR_WIDTH, 0); // Down arrow button int32_t downY = y + sb->length - SCROLLBAR_WIDTH; drawBevel(d, ops, x, downY, SCROLLBAR_WIDTH, SCROLLBAR_WIDTH, &btnBevel); drawScrollbarArrow(d, ops, colors, x, downY, SCROLLBAR_WIDTH, 1); // Thumb int32_t thumbPos; int32_t thumbSize; scrollbarThumbInfo(sb, &thumbPos, &thumbSize); drawBevel(d, ops, x, y + SCROLLBAR_WIDTH + thumbPos, SCROLLBAR_WIDTH, thumbSize, &btnBevel); } else { // Trough rectFill(d, ops, x, y, sb->length, SCROLLBAR_WIDTH, colors->scrollbarTrough); BevelStyleT troughBevel; troughBevel.highlight = colors->windowShadow; troughBevel.shadow = colors->windowHighlight; troughBevel.face = 0; troughBevel.width = 1; drawBevel(d, ops, x, y, sb->length, SCROLLBAR_WIDTH, &troughBevel); // Left arrow button drawBevel(d, ops, x, y, SCROLLBAR_WIDTH, SCROLLBAR_WIDTH, &btnBevel); drawScrollbarArrow(d, ops, colors, x, y, SCROLLBAR_WIDTH, 2); // Right arrow button int32_t rightX = x + sb->length - SCROLLBAR_WIDTH; drawBevel(d, ops, rightX, y, SCROLLBAR_WIDTH, SCROLLBAR_WIDTH, &btnBevel); drawScrollbarArrow(d, ops, colors, rightX, y, SCROLLBAR_WIDTH, 3); // Thumb int32_t thumbPos; int32_t thumbSize; scrollbarThumbInfo(sb, &thumbPos, &thumbSize); drawBevel(d, ops, x + SCROLLBAR_WIDTH + thumbPos, y, thumbSize, SCROLLBAR_WIDTH, &btnBevel); } } // ============================================================ // drawScrollbarArrow // ============================================================ // // Draws a small triangle arrow glyph inside a scrollbar button. // dir: 0=up, 1=down, 2=left, 3=right // // The triangle is built row-by-row from the tip outward: each successive // row is 2 pixels wider (1 pixel on each side), producing a symmetric // isoceles triangle. SB_ARROW_ROWS (4) rows gives a 7-pixel-wide base, // which fits well inside SCROLLBAR_WIDTH (16px) buttons. The glyph is // centered on the button using integer division — no sub-pixel alignment // needed at these sizes. static void drawScrollbarArrow(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size, int32_t dir) { int32_t cx = x + size / 2; int32_t cy = y + size / 2; uint32_t fg = colors->contentFg; // Draw a small triangle for (int32_t i = 0; i < SB_ARROW_ROWS; i++) { switch (dir) { case 0: // up drawHLine(d, ops, cx - i, cy - SB_ARROW_HALF + i, 1 + i * 2, fg); break; case 1: // down drawHLine(d, ops, cx - i, cy + SB_ARROW_HALF - i, 1 + i * 2, fg); break; case 2: // left drawVLine(d, ops, cx - SB_ARROW_HALF + i, cy - i, 1 + i * 2, fg); break; case 3: // right drawVLine(d, ops, cx + SB_ARROW_HALF - i, cy - i, 1 + i * 2, fg); break; } } } // ============================================================ // drawTitleBar // ============================================================ // // 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); drawHLine(d, ops, bx, by, bs, colors->contentFg); drawHLine(d, ops, bx, by + bs - 1, bs, colors->contentFg); drawVLine(d, ops, bx, by, bs, colors->contentFg); drawVLine(d, ops, bx + bs - 1, by, 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); drawHLine(d, ops, bx, by, bs, colors->contentFg); drawHLine(d, ops, bx, by + 1, bs, colors->contentFg); drawHLine(d, ops, bx, by + bs - 1, bs, colors->contentFg); drawVLine(d, ops, bx, by, bs, colors->contentFg); drawVLine(d, ops, bx + bs - 1, by, 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); } } } // ============================================================ // freeMenuRecursive // ============================================================ // // Walks the submenu tree depth-first, freeing heap-allocated child MenuT // nodes. The top-level MenuT structs are embedded in the MenuBarT array // and are freed when the MenuBarT is freed, so this only needs to handle // dynamically allocated submenu children. static void freeMenuRecursive(MenuT *menu) { for (int32_t i = 0; i < menu->itemCount; i++) { if (menu->items[i].subMenu) { freeMenuRecursive(menu->items[i].subMenu); free(menu->items[i].subMenu); menu->items[i].subMenu = NULL; } } } // ============================================================ // minimizedIconPos // ============================================================ // // Computes the screen position of a minimized window icon. Icons are laid // out in a horizontal strip along the bottom of the screen, left to right. // This mirrors the DESQview/X and Windows 3.x convention of showing minimized // windows as icons at the bottom of the desktop. // // 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. static void minimizedIconPos(const DisplayT *d, int32_t index, int32_t *x, int32_t *y) { *x = ICON_SPACING + index * (ICON_TOTAL_SIZE + ICON_SPACING); *y = d->height - ICON_TOTAL_SIZE - ICON_SPACING; } // ============================================================ // scrollbarThumbInfo // ============================================================ // // 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) / (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; } // ============================================================ // wmAddHScrollbar // ============================================================ // // 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) { win->hScroll = (ScrollbarT *)malloc(sizeof(ScrollbarT)); if (!win->hScroll) { return NULL; } win->hScroll->orient = ScrollbarHorizontalE; win->hScroll->min = min; win->hScroll->max = max; win->hScroll->value = min; win->hScroll->pageSize = pageSize; wmUpdateContentRect(win); return win->hScroll; } // ============================================================ // wmAddMenu // ============================================================ // // Adds a top-level menu to a window's menu bar. The MenuT struct is // stored inline in the MenuBarT's fixed-size array (MAX_MENUS=8) rather // than heap-allocated, since menus are created once at window setup and // the count is small. 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. MenuT *wmAddMenu(MenuBarT *bar, const char *label) { if (bar->menuCount >= MAX_MENUS) { return NULL; } MenuT *menu = &bar->menus[bar->menuCount]; memset(menu, 0, sizeof(*menu)); strncpy(menu->label, label, MAX_MENU_LABEL - 1); menu->label[MAX_MENU_LABEL - 1] = '\0'; menu->accelKey = accelParse(label); bar->menuCount++; bar->positionsDirty = true; return menu; } // ============================================================ // wmAddMenuBar // ============================================================ // // 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 // (MAX_MENUS * sizeof(MenuT)). wmUpdateContentRect is called to shrink // the content area by CHROME_MENU_HEIGHT to make room for the bar. MenuBarT *wmAddMenuBar(WindowT *win) { win->menuBar = (MenuBarT *)malloc(sizeof(MenuBarT)); if (!win->menuBar) { return NULL; } memset(win->menuBar, 0, sizeof(MenuBarT)); win->menuBar->activeIdx = -1; wmUpdateContentRect(win); return win->menuBar; } // ============================================================ // wmAddMenuItem // ============================================================ void wmAddMenuItem(MenuT *menu, const char *label, int32_t id) { if (menu->itemCount >= MAX_MENU_ITEMS) { return; } MenuItemT *item = &menu->items[menu->itemCount]; memset(item, 0, sizeof(*item)); strncpy(item->label, label, MAX_MENU_LABEL - 1); item->label[MAX_MENU_LABEL - 1] = '\0'; item->id = id; item->separator = false; item->enabled = true; item->checked = false; item->accelKey = accelParse(label); menu->itemCount++; } // ============================================================ // wmAddMenuCheckItem // ============================================================ void wmAddMenuCheckItem(MenuT *menu, const char *label, int32_t id, bool checked) { if (menu->itemCount >= MAX_MENU_ITEMS) { return; } MenuItemT *item = &menu->items[menu->itemCount]; memset(item, 0, sizeof(*item)); strncpy(item->label, label, MAX_MENU_LABEL - 1); item->label[MAX_MENU_LABEL - 1] = '\0'; item->id = id; item->type = MenuItemCheckE; item->enabled = true; item->checked = checked; item->accelKey = accelParse(label); menu->itemCount++; } // ============================================================ // wmAddMenuRadioItem // ============================================================ void wmAddMenuRadioItem(MenuT *menu, const char *label, int32_t id, bool checked) { if (menu->itemCount >= MAX_MENU_ITEMS) { return; } MenuItemT *item = &menu->items[menu->itemCount]; memset(item, 0, sizeof(*item)); strncpy(item->label, label, MAX_MENU_LABEL - 1); item->label[MAX_MENU_LABEL - 1] = '\0'; item->id = id; item->type = MenuItemRadioE; item->enabled = true; item->checked = checked; item->accelKey = accelParse(label); menu->itemCount++; } // ============================================================ // wmAddMenuSeparator // ============================================================ void wmAddMenuSeparator(MenuT *menu) { if (menu->itemCount >= MAX_MENU_ITEMS) { return; } MenuItemT *item = &menu->items[menu->itemCount]; memset(item, 0, sizeof(*item)); item->separator = true; item->enabled = false; menu->itemCount++; } // ============================================================ // wmAddSubMenu // ============================================================ // // 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) { if (parentMenu->itemCount >= MAX_MENU_ITEMS) { return NULL; } MenuT *child = (MenuT *)calloc(1, sizeof(MenuT)); if (!child) { return NULL; } MenuItemT *item = &parentMenu->items[parentMenu->itemCount]; memset(item, 0, sizeof(*item)); strncpy(item->label, label, MAX_MENU_LABEL - 1); item->label[MAX_MENU_LABEL - 1] = '\0'; item->id = -1; item->separator = false; item->enabled = true; item->accelKey = accelParse(label); item->subMenu = child; parentMenu->itemCount++; return child; } // ============================================================ // wmAddVScrollbar // ============================================================ // // 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) { win->vScroll = (ScrollbarT *)malloc(sizeof(ScrollbarT)); if (!win->vScroll) { return NULL; } win->vScroll->orient = ScrollbarVerticalE; win->vScroll->min = min; win->vScroll->max = max; win->vScroll->value = min; win->vScroll->pageSize = pageSize; // Position will be updated by wmUpdateContentRect wmUpdateContentRect(win); return win->vScroll; } // ============================================================ // wmCreateWindow // ============================================================ // // 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 >= MAX_WINDOWS) { fprintf(stderr, "WM: Maximum windows (%d) reached\n", MAX_WINDOWS); return NULL; } WindowT *win = (WindowT *)malloc(sizeof(WindowT)); if (!win) { fprintf(stderr, "WM: Failed to allocate window\n"); 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->focused = false; win->minimized = false; win->maximized = false; win->resizable = resizable; win->maxW = WM_MAX_FROM_SCREEN; win->maxH = WM_MAX_FROM_SCREEN; strncpy(win->title, title, MAX_TITLE_LEN - 1); win->title[MAX_TITLE_LEN - 1] = '\0'; 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) { fprintf(stderr, "WM: Failed to allocate content buffer (%ld bytes)\n", (long)bufSize); free(win); return NULL; } memset(win->contentBuf, 0xFF, bufSize); } stack->windows[stack->count] = win; stack->count++; return win; } // ============================================================ // wmDestroyWindow // ============================================================ // // 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 was focused, focus moves // to the new topmost 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) { stack->focusedIdx = stack->count > 0 ? stack->count - 1 : -1; } else if (stack->focusedIdx > i) { stack->focusedIdx--; } break; } } // Destroy widget tree before freeing window if (win->widgetRoot) { wgtDestroy(win->widgetRoot); win->widgetRoot = NULL; } if (win->contentBuf) { free(win->contentBuf); } if (win->menuBar) { for (int32_t i = 0; i < win->menuBar->menuCount; i++) { freeMenuRecursive(&win->menuBar->menus[i]); } free(win->menuBar); } if (win->vScroll) { free(win->vScroll); } if (win->hScroll) { free(win->hScroll); } if (win->iconData) { free(win->iconData); } free(win); } // ============================================================ // wmDragBegin // ============================================================ // // 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; } // ============================================================ // wmDragEnd // ============================================================ void wmDragEnd(WindowStackT *stack) { stack->dragWindow = -1; } // ============================================================ // wmDragMove // ============================================================ // // 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; } WindowT *win = stack->windows[stack->dragWindow]; int32_t minVisible = 50; dirtyListAdd(dl, win->x, win->y, win->w, win->h); win->x = mouseX - stack->dragOffX; win->y = mouseY - stack->dragOffY; // Clamp: keep title bar reachable if (win->y < 0) { win->y = 0; } if (win->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT > screenH) { win->y = screenH - CHROME_BORDER_WIDTH - CHROME_TITLE_HEIGHT; } if (win->x + win->w < minVisible) { win->x = minVisible - win->w; } if (win->x > screenW - minVisible) { win->x = screenW - minVisible; } dirtyListAdd(dl, win->x, win->y, win->w, win->h); } // ============================================================ // wmDrawChrome // ============================================================ // // 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) { int32_t savedClipX = d->clipX; int32_t savedClipY = d->clipY; int32_t savedClipW = d->clipW; int32_t savedClipH = d->clipH; 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 = 0; // 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); // Restore clip rect setClipRect(d, savedClipX, savedClipY, savedClipW, savedClipH); } // ============================================================ // wmDrawContent // ============================================================ // // 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; } } // ============================================================ // wmDrawMinimizedIcons // ============================================================ // // 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 approach (option 2) is a DESQview/X homage — DV/X // showed miniature window contents in its icon/task view. The thumbnail // is rendered from the existing content buffer, so no extra rendering // pass is needed. contentDirty 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; minimizedIconPos(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; } // Draw 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); // Draw icon content int32_t contentX = ix + ICON_BORDER; int32_t contentY = iy + ICON_BORDER; int32_t bpp = d->format.bytesPerPixel; if (win->iconData && win->iconW > 0 && win->iconH > 0) { // Draw loaded icon image, scaled to ICON_SIZE x ICON_SIZE drawScaledRect(d, contentX, contentY, ICON_SIZE, ICON_SIZE, win->iconData, win->iconW, win->iconH, win->iconPitch, bpp); } else if (win->contentBuf && win->contentW > 0 && win->contentH > 0) { // Draw scaled copy of window contents drawScaledRect(d, contentX, contentY, ICON_SIZE, ICON_SIZE, win->contentBuf, win->contentW, win->contentH, win->contentPitch, bpp); } else { // No content — draw grey fill rectFill(d, ops, contentX, contentY, ICON_SIZE, ICON_SIZE, colors->windowFace); } } } // ============================================================ // wmDrawScrollbars // ============================================================ void wmDrawScrollbars(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, WindowT *win, const RectT *clipTo) { int32_t savedClipX = d->clipX; int32_t savedClipY = d->clipY; int32_t savedClipW = d->clipW; int32_t savedClipH = d->clipH; setClipRect(d, clipTo->x, clipTo->y, clipTo->w, clipTo->h); if (win->vScroll) { drawScrollbar(d, ops, colors, win->vScroll, win->x, win->y); } if (win->hScroll) { drawScrollbar(d, ops, colors, win->hScroll, win->x, win->y); } setClipRect(d, savedClipX, savedClipY, savedClipW, savedClipH); } // ============================================================ // wmHitTest // ============================================================ // // 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 if (win->menuBar && my >= win->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT && my < win->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT + CHROME_MENU_HEIGHT) { *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; } // ============================================================ // wmInit // ============================================================ // // 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; } // ============================================================ // wmMaximize // ============================================================ // // 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 = (win->maxW == WM_MAX_FROM_SCREEN) ? d->width : DVX_MIN(win->maxW, d->width); int32_t newH = (win->maxH == WM_MAX_FROM_SCREEN) ? d->height : DVX_MIN(win->maxH, d->height); win->x = 0; win->y = 0; win->w = newW; win->h = newH; win->maximized = true; wmUpdateContentRect(win); wmReallocContentBuf(win, d); if (win->onResize) { win->onResize(win, win->contentW, win->contentH); } if (win->onPaint) { RectT fullRect = {0, 0, win->contentW, win->contentH}; win->onPaint(win, &fullRect); win->contentDirty = true; } dirtyListAdd(dl, win->x, win->y, win->w, win->h); } // ============================================================ // wmMinimize // ============================================================ // // 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; for (int32_t i = stack->count - 1; i >= 0; i--) { if (stack->windows[i]->visible && !stack->windows[i]->minimized) { wmSetFocus(stack, dl, i); return; } } stack->focusedIdx = -1; } // ============================================================ // wmMinimizedIconHit // ============================================================ // // 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; minimizedIconPos(d, iconIdx, &ix, &iy); iconIdx++; if (mx >= ix && mx < ix + ICON_TOTAL_SIZE && my >= iy && my < iy + ICON_TOTAL_SIZE) { return i; } } return -1; } // ============================================================ // wmMinWindowSize // ============================================================ // // 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, no string operations — just arithmetic on cached values. static void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH) { int32_t gadgetS = CHROME_TITLE_HEIGHT - GADGET_INSET * 2; int32_t gadgetPad = GADGET_PAD; int32_t charW = FONT_CHAR_WIDTH; int32_t titleMinW = gadgetPad + gadgetS + gadgetPad + charW + gadgetPad + gadgetS + gadgetPad; if (win->resizable) { titleMinW += gadgetS + gadgetPad; } *minW = titleMinW + CHROME_BORDER_WIDTH * 2; // Menu bar width: ensure window is wide enough to show all menu labels if (win->menuBar && win->menuBar->menuCount > 0) { MenuT *last = &win->menuBar->menus[win->menuBar->menuCount - 1]; int32_t menuW = last->barX + last->barW + 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; } } } // ============================================================ // wmRaiseWindow // ============================================================ // // 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); if (stack->focusedIdx == idx) { stack->focusedIdx = stack->count - 1; } else if (stack->focusedIdx > idx) { stack->focusedIdx--; } if (stack->dragWindow == idx) { stack->dragWindow = stack->count - 1; } else if (stack->dragWindow > idx) { stack->dragWindow--; } } // ============================================================ // wmReallocContentBuf // ============================================================ // // 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) { if (win->contentBuf) { 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, 0xFF, bufSize); return 0; } // ============================================================ // wmResizeBegin // ============================================================ // // Initiates a window resize. Unlike drag (which stores mouse-to-origin // offset), resize stores the absolute mouse position. wmResizeMove computes // delta from this position each frame, then conditionally resets it only // on axes where the resize was applied. When clamped, the delta accumulates // so the border sticks to the mouse when the user reverses direction. 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; } // ============================================================ // wmResizeEdgeHit // ============================================================ // // 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 + 2; 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; } // ============================================================ // wmResizeEnd // ============================================================ void wmResizeEnd(WindowStackT *stack) { stack->resizeWindow = -1; stack->resizeEdge = RESIZE_NONE; } // ============================================================ // wmResizeMove // ============================================================ // // 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 reset to the current mouse position // only on axes where the resize was actually applied. If clamped (window at // min/max size), dragOff is NOT updated on that axis, so the accumulated // delta tracks how far the mouse moved past the border. When the user // reverses direction, the border immediately follows — it "sticks" to // the mouse pointer instead of creating a dead zone. // // 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 = (win->maxW == WM_MAX_FROM_SCREEN) ? d->width : DVX_MIN(win->maxW, d->width); int32_t maxH = (win->maxH == WM_MAX_FROM_SCREEN) ? d->height : DVX_MIN(win->maxH, d->height); // 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; } 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; } 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; } 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; } // If resized while maximized, consider it no longer maximized if (win->maximized) { win->maximized = false; } wmUpdateContentRect(win); wmReallocContentBuf(win, d); // Call resize callback, then repaint if (win->onResize) { win->onResize(win, win->contentW, win->contentH); } if (win->onPaint) { RectT fullRect = {0, 0, win->contentW, win->contentH}; win->onPaint(win, &fullRect); win->contentDirty = true; } // 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); } // ============================================================ // wmRestore // ============================================================ // // 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; wmUpdateContentRect(win); wmReallocContentBuf(win, d); if (win->onResize) { win->onResize(win, win->contentW, win->contentH); } if (win->onPaint) { RectT fullRect = {0, 0, win->contentW, win->contentH}; win->onPaint(win, &fullRect); win->contentDirty = true; } // Mark restored position dirty dirtyListAdd(dl, win->x, win->y, win->w, win->h); } // ============================================================ // wmRestoreMinimized // ============================================================ // // Restores a minimized window: clears the minimized flag, raises the window // to the top of the z-order, and gives it focus. No content buffer // reallocation is needed because the buffer was preserved while minimized. // // The icon strip area is implicitly dirtied by the raise/focus operations // and by the window area dirty at the end. The compositor repaints the // entire icon strip on any dirty rect that intersects it, so icon positions // are always correct after restore even though we don't dirty the icon // strip explicitly. void wmRestoreMinimized(WindowStackT *stack, DirtyListT *dl, WindowT *win) { if (!win->minimized) { return; } 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); } // ============================================================ // wmScrollbarClick // ============================================================ // // 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, int32_t orient, int32_t mx, int32_t my) { if (idx < 0 || idx >= stack->count) { return; } WindowT *win = stack->windows[idx]; ScrollbarT *sb = (orient == SCROLL_VERTICAL) ? 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; } int32_t oldValue = sb->value; if (sb->orient == ScrollbarVerticalE) { int32_t relY = my - sbScreenY; // Up arrow button if (relY < SCROLLBAR_WIDTH) { sb->value -= 1; } // Down arrow button else if (relY >= sb->length - SCROLLBAR_WIDTH) { sb->value += 1; } // Thumb else if (relY >= SCROLLBAR_WIDTH + thumbPos && relY < SCROLLBAR_WIDTH + thumbPos + thumbSize) { stack->scrollWindow = idx; stack->scrollOrient = 0; stack->scrollDragOff = my - (sbScreenY + SCROLLBAR_WIDTH + thumbPos); return; } // Trough above thumb else if (relY < SCROLLBAR_WIDTH + thumbPos) { sb->value -= sb->pageSize; } // Trough below thumb else { sb->value += sb->pageSize; } } else { int32_t relX = mx - sbScreenX; // Left arrow button if (relX < SCROLLBAR_WIDTH) { sb->value -= 1; } // Right arrow button else if (relX >= sb->length - SCROLLBAR_WIDTH) { sb->value += 1; } // Thumb else if (relX >= SCROLLBAR_WIDTH + thumbPos && relX < SCROLLBAR_WIDTH + thumbPos + thumbSize) { stack->scrollWindow = idx; stack->scrollOrient = 1; stack->scrollDragOff = mx - (sbScreenX + SCROLLBAR_WIDTH + thumbPos); return; } // Trough left of thumb else if (relX < SCROLLBAR_WIDTH + thumbPos) { sb->value -= sb->pageSize; } // Trough right of thumb else { sb->value += sb->pageSize; } } // Clamp value if (sb->value < sb->min) { sb->value = sb->min; } if (sb->value > sb->max) { sb->value = sb->max; } // Dirty the scrollbar area and fire callback 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); } } } // ============================================================ // wmScrollbarDrag // ============================================================ // // 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 == 0) ? 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 + (mousePos * range) / (trackLen - thumbSize); // Clamp 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); } } } // ============================================================ // wmScrollbarEnd // ============================================================ void wmScrollbarEnd(WindowStackT *stack) { stack->scrollWindow = -1; } // ============================================================ // wmSetIcon // ============================================================ // // 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 if (win->iconData) { free(win->iconData); } win->iconData = buf; win->iconW = imgW; win->iconH = imgH; win->iconPitch = pitch; return 0; } // ============================================================ // wmSetFocus // ============================================================ // // 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; } // Unfocus old window if (stack->focusedIdx >= 0 && stack->focusedIdx < stack->count) { WindowT *oldWin = stack->windows[stack->focusedIdx]; oldWin->focused = false; // Dirty the old title bar dirtyListAdd(dl, oldWin->x, oldWin->y, oldWin->w, CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT); } // 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_BORDER_WIDTH + CHROME_TITLE_HEIGHT); } // ============================================================ // wmCreateMenu // ============================================================ // // 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; } // ============================================================ // wmFreeMenu // ============================================================ // // Frees a standalone context menu and all its submenus recursively. // Unlike freeMenuRecursive (which only frees submenu children because the // top-level struct is embedded), this also frees the root MenuT itself. void wmFreeMenu(MenuT *menu) { if (!menu) { return; } // Free submenus recursively for (int32_t i = 0; i < menu->itemCount; i++) { if (menu->items[i].subMenu) { wmFreeMenu(menu->items[i].subMenu); } } free(menu); } // ============================================================ // wmSetTitle // ============================================================ // // 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) { strncpy(win->title, title, MAX_TITLE_LEN - 1); win->title[MAX_TITLE_LEN - 1] = '\0'; // 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); } // ============================================================ // wmUpdateContentRect // ============================================================ // // 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; } }