2642 lines
93 KiB
C
2642 lines
93 KiB
C
// 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 <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdio.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 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 textX = win->x + menu->barX + CHROME_TITLE_PAD;
|
|
int32_t textY = barY + (barH - font->charHeight) / 2;
|
|
|
|
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));
|
|
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 = -1;
|
|
win->maxH = -1;
|
|
|
|
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 = 2;
|
|
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 = 8;
|
|
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 = 7;
|
|
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 = 1;
|
|
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 = 4;
|
|
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 = 5;
|
|
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 = 6;
|
|
return i;
|
|
}
|
|
}
|
|
|
|
// Resize edges (if resizable)
|
|
if (win->resizable) {
|
|
int32_t edge = wmResizeEdgeHit(win, mx, my);
|
|
|
|
if (edge != RESIZE_NONE) {
|
|
*hitPart = 3;
|
|
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 = 0;
|
|
return i;
|
|
}
|
|
|
|
// Somewhere on the chrome but not a specific part
|
|
*hitPart = 1;
|
|
return i;
|
|
}
|
|
|
|
*hitPart = -1;
|
|
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 < 0) ? d->width : DVX_MIN(win->maxW, d->width);
|
|
int32_t newH = (win->maxH < 0) ? 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 dx = mouseX - stack->dragOffX;
|
|
int32_t dy = mouseY - 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 < 0) ? d->width : DVX_MIN(win->maxW, d->width);
|
|
int32_t maxH = (win->maxH < 0) ? d->height : DVX_MIN(win->maxH, d->height);
|
|
|
|
// Mark old position dirty
|
|
dirtyListAdd(dl, win->x, win->y, win->w, win->h);
|
|
|
|
// Track whether each axis actually changed, so we only update
|
|
// dragOff on axes where the resize was applied. If clamped, leaving
|
|
// dragOff unchanged makes the border "stick" to the mouse when the
|
|
// user reverses direction, instead of creating a dead zone.
|
|
bool appliedX = false;
|
|
bool appliedY = false;
|
|
|
|
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) {
|
|
win->x = newX;
|
|
win->w = newW;
|
|
appliedX = true;
|
|
}
|
|
}
|
|
|
|
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) {
|
|
win->w = newW;
|
|
appliedX = true;
|
|
}
|
|
}
|
|
|
|
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) {
|
|
win->y = newY;
|
|
win->h = newH;
|
|
appliedY = true;
|
|
}
|
|
}
|
|
|
|
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) {
|
|
win->h = newH;
|
|
appliedY = true;
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
if (appliedX) {
|
|
stack->dragOffX = mouseX;
|
|
}
|
|
|
|
if (appliedY) {
|
|
stack->dragOffY = mouseY;
|
|
}
|
|
|
|
// 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 == 0) ? 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; }
|
|
}
|