// dvx_wm.c — Layer 4: Window manager for DV/X GUI #include "dvxWm.h" #include "dvxVideo.h" #include "dvxDraw.h" #include "dvxComp.h" #include "thirdparty/stb_image.h" #include #include #include // ============================================================ // Prototypes // ============================================================ static void computeMenuBarPositions(WindowT *win, const BitmapFontT *font); 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 // ============================================================ static void computeMenuBarPositions(WindowT *win, const BitmapFontT *font) { if (!win->menuBar) { 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 = (int32_t)strlen(menu->label) * font->charWidth + CHROME_TITLE_PAD * 2; menu->barX = x; menu->barW = labelW; x += labelW; } } // ============================================================ // 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. // // Top/Left, outside to inside: highlight, highlight, face, shadow // Bottom/Right, outside to inside: shadow, shadow, face, highlight 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 gadget (GEOS Motif style). 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 // ============================================================ 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; // Fill menu bar background rectFill(d, ops, win->x + CHROME_BORDER_WIDTH, barY, win->w - CHROME_BORDER_WIDTH * 2, barH, colors->menuBg); // Draw each menu label 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; drawText(d, ops, font, textX, textY, menu->label, colors->menuFg, colors->menuBg, true); } // Draw bottom separator line drawHLine(d, ops, win->x + CHROME_BORDER_WIDTH, barY + barH - 1, win->w - CHROME_BORDER_WIDTH * 2, colors->windowShadow); } // ============================================================ // drawResizeBreaks // ============================================================ // GEOS Ensemble Motif-style: perpendicular grooves that cut across // the window border near each corner to indicate resizable edges. // Two breaks per corner — one on each edge meeting at that corner. // Each groove is a shadow+highlight line pair cutting across the // border width, perpendicular to the edge direction. 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 = 16; // distance from corner to groove 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 // ============================================================ 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) { for (int32_t dy = 0; dy < dstH; dy++) { int32_t sy = (dy * srcH) / dstH; int32_t screenY = dstY + dy; if (screenY < d->clipY || screenY >= d->clipY + d->clipH) { continue; } uint8_t *dstRow = d->backBuf + screenY * d->pitch; const uint8_t *srcRow = src + sy * srcPitch; for (int32_t dx = 0; dx < dstW; dx++) { int32_t sx = (dx * srcW) / dstW; int32_t screenX = dstX + dx; if (screenX < d->clipX || screenX >= d->clipX + d->clipW) { continue; } const uint8_t *srcPx = srcRow + sx * bpp; uint8_t *dstPx = dstRow + screenX * bpp; if (bpp == 1) { *dstPx = *srcPx; } else if (bpp == 2) { *(uint16_t *)dstPx = *(const uint16_t *)srcPx; } else { *(uint32_t *)dstPx = *(const uint32_t *)srcPx; } } } } // ============================================================ // drawScrollbar // ============================================================ 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 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 5-row triangle for (int32_t i = 0; i < 4; i++) { switch (dir) { case 0: // up drawHLine(d, ops, cx - i, cy - 2 + i, 1 + i * 2, fg); break; case 1: // down drawHLine(d, ops, cx - i, cy + 2 - i, 1 + i * 2, fg); break; case 2: // left drawVLine(d, ops, cx - 2 + i, cy - i, 1 + i * 2, fg); break; case 3: // right drawVLine(d, ops, cx + 2 - i, cy - i, 1 + i * 2, fg); break; } } } // ============================================================ // drawTitleBar // ============================================================ static void drawTitleBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, WindowT *win) { int32_t titleX = win->x + CHROME_BORDER_WIDTH; int32_t titleY = win->y + CHROME_BORDER_WIDTH; int32_t titleW = win->w - CHROME_BORDER_WIDTH * 2; int32_t titleH = CHROME_TITLE_HEIGHT; 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, titleX, titleY, titleW, titleH, bg); // GEOS Motif gadget sizing int32_t gadgetS = titleH - 4; // gadget size, fits within title bar int32_t gadgetY = titleY + 2; int32_t gadgetPad = 2; // Close gadget on the LEFT (system menu / close) int32_t closeX = titleX + gadgetPad; drawTitleGadget(d, ops, colors, closeX, gadgetY, gadgetS); // Horizontal bar icon inside close gadget drawHLine(d, ops, closeX + 3, gadgetY + gadgetS / 2, gadgetS - 6, colors->contentFg); // Right edge of title text area int32_t textRightEdge = titleX + titleW - gadgetPad; // Rightmost gadget position int32_t rightGadgetX = titleX + titleW - gadgetPad - gadgetS; if (win->resizable) { // Maximize/restore gadget on the far RIGHT int32_t maxX = rightGadgetX; drawTitleGadget(d, ops, colors, maxX, gadgetY, gadgetS); if (win->maximized) { // Restore icon: larger box outline filling more of the gadget int32_t bx = maxX + 2; int32_t by = gadgetY + 2; int32_t bs = gadgetS - 4; 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); } else { // Maximize icon: small box outline rectFill(d, ops, maxX + 3, gadgetY + 3, gadgetS - 6, gadgetS - 6, colors->buttonFace); drawHLine(d, ops, maxX + 3, gadgetY + 3, gadgetS - 6, colors->contentFg); drawHLine(d, ops, maxX + 3, gadgetY + gadgetS - 4, gadgetS - 6, colors->contentFg); drawVLine(d, ops, maxX + 3, gadgetY + 3, gadgetS - 6, colors->contentFg); drawVLine(d, ops, maxX + gadgetS - 4, gadgetY + 3, gadgetS - 6, colors->contentFg); } rightGadgetX -= gadgetS + gadgetPad; } // Minimize gadget (always present, to the left of maximize or on the far right) int32_t minX = rightGadgetX; drawTitleGadget(d, ops, colors, minX, gadgetY, gadgetS); // Small square centered in minimize gadget int32_t miniSize = 4; rectFill(d, ops, minX + (gadgetS - miniSize) / 2, gadgetY + (gadgetS - miniSize) / 2, miniSize, miniSize, colors->contentFg); textRightEdge = minX - gadgetPad; // Title text — centered between close gadget and right edge, truncated to fit int32_t textLeftEdge = closeX + gadgetS + gadgetPad; int32_t availW = textRightEdge - 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) { int32_t textW = len * font->charWidth; int32_t textX = textLeftEdge + (availW - textW) / 2; int32_t textY = titleY + (titleH - font->charHeight) / 2; // Draw truncated title character by character for (int32_t i = 0; i < len; i++) { drawChar(d, ops, font, textX + i * font->charWidth, textY, win->title[i], fg, bg, true); } } } } // ============================================================ // minimizedIconPos // ============================================================ 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 // ============================================================ 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 = (sb->pageSize * trackLen) / (range + sb->pageSize); if (*thumbSize < SCROLLBAR_WIDTH) { *thumbSize = SCROLLBAR_WIDTH; } *thumbPos = ((sb->value - sb->min) * (trackLen - *thumbSize)) / range; return trackLen; } // ============================================================ // wmAddHScrollbar // ============================================================ 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; // Position will be updated by wmUpdateContentRect wmUpdateContentRect(win); return win->hScroll; } // ============================================================ // wmAddMenu // ============================================================ 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'; bar->menuCount++; return menu; } // ============================================================ // wmAddMenuBar // ============================================================ 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; 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++; } // ============================================================ // wmAddVScrollbar // ============================================================ 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 // ============================================================ 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); // Allocate content buffer 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); // white background } // Add to top of stack stack->windows[stack->count] = win; stack->count++; return win; } // ============================================================ // wmDestroyWindow // ============================================================ void wmDestroyWindow(WindowStackT *stack, WindowT *win) { // Find and remove from stack for (int32_t i = 0; i < stack->count; i++) { if (stack->windows[i] == win) { // Shift remaining windows down for (int32_t j = i; j < stack->count - 1; j++) { stack->windows[j] = stack->windows[j + 1]; } stack->count--; // Adjust focusedIdx if (stack->focusedIdx == i) { stack->focusedIdx = stack->count > 0 ? stack->count - 1 : -1; } else if (stack->focusedIdx > i) { stack->focusedIdx--; } break; } } if (win->contentBuf) { free(win->contentBuf); } if (win->menuBar) { free(win->menuBar); } if (win->vScroll) { free(win->vScroll); } if (win->hScroll) { free(win->hScroll); } if (win->iconData) { free(win->iconData); } free(win); } // ============================================================ // wmDragBegin // ============================================================ 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 // ============================================================ void wmDragMove(WindowStackT *stack, DirtyListT *dl, int32_t mouseX, int32_t mouseY) { if (stack->dragWindow < 0 || stack->dragWindow >= stack->count) { return; } WindowT *win = stack->windows[stack->dragWindow]; // Mark old position dirty dirtyListAdd(dl, win->x, win->y, win->w, win->h); // Update position win->x = mouseX - stack->dragOffX; win->y = mouseY - stack->dragOffY; // Mark new position dirty dirtyListAdd(dl, win->x, win->y, win->w, win->h); } // ============================================================ // wmDrawChrome // ============================================================ void wmDrawChrome(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, WindowT *win, const RectT *clipTo) { // Save and set clip rect 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); // Outer raised border — 4px, 3-shade Motif bevel drawBorderFrame(d, ops, colors, win->x, win->y, win->w, win->h); // Title bar drawTitleBar(d, ops, font, colors, win); // Menu bar (if present) if (win->menuBar) { computeMenuBarPositions(win, font); drawMenuBar(d, ops, font, colors, win); } // Inner sunken bevel around content area 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 // ============================================================ void wmDrawContent(DisplayT *d, const BlitOpsT *ops, WindowT *win, const RectT *clipTo) { if (__builtin_expect(!win->contentBuf, 0)) { return; } // Calculate intersection of content area with clip rect 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; } // Direct blit — we already know the exact intersection, skip clipRect overhead 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 // ============================================================ 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 // ============================================================ int32_t wmHitTest(const WindowStackT *stack, int32_t mx, int32_t my, int32_t *hitPart) { // Walk stack top-to-bottom (front-to-back) 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; } // Title bar gadget geometry (must match drawTitleBar) int32_t titleX = win->x + CHROME_BORDER_WIDTH; int32_t titleY = win->y + CHROME_BORDER_WIDTH; int32_t titleW = win->w - CHROME_BORDER_WIDTH * 2; int32_t gadgetS = CHROME_TITLE_HEIGHT - 4; int32_t gadgetY = titleY + 2; int32_t gadgetP = 2; // Close gadget (top-left) int32_t closeX = titleX + gadgetP; if (mx >= closeX && mx < closeX + gadgetS && my >= gadgetY && my < gadgetY + gadgetS) { *hitPart = 2; return i; } // Rightmost gadget position int32_t rightGadgetX = titleX + titleW - gadgetP - gadgetS; // Maximize gadget only on resizable windows if (win->resizable) { if (mx >= rightGadgetX && mx < rightGadgetX + gadgetS && my >= gadgetY && my < gadgetY + gadgetS) { *hitPart = 8; return i; } rightGadgetX -= gadgetS + gadgetP; } // Minimize gadget (always present) if (mx >= rightGadgetX && mx < rightGadgetX + gadgetS && my >= gadgetY && my < gadgetY + gadgetS) { *hitPart = 7; return i; } // Title bar (drag area — between gadgets) if (my >= titleY && my < titleY + CHROME_TITLE_HEIGHT && mx >= titleX && mx < titleX + 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 // ============================================================ void wmInit(WindowStackT *stack) { memset(stack, 0, sizeof(*stack)); stack->focusedIdx = -1; stack->dragWindow = -1; stack->resizeWindow = -1; stack->scrollWindow = -1; } // ============================================================ // wmMaximize // ============================================================ void wmMaximize(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT *win) { (void)stack; if (win->maximized) { return; } // Save current geometry win->preMaxX = win->x; win->preMaxY = win->y; win->preMaxW = win->w; win->preMaxH = win->h; // Mark old position dirty dirtyListAdd(dl, win->x, win->y, win->w, win->h); // Compute effective maximum size 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; } // Mark new position dirty dirtyListAdd(dl, win->x, win->y, win->w, win->h); } // ============================================================ // wmMinimize // ============================================================ void wmMinimize(WindowStackT *stack, DirtyListT *dl, WindowT *win) { if (win->minimized) { return; } // Mark window area dirty dirtyListAdd(dl, win->x, win->y, win->w, win->h); win->minimized = true; // Find next non-minimized window to focus 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 // ============================================================ 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 // ============================================================ static void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH) { // Title bar: close gadget + padding + 1 char of title + padding int32_t gadgetS = CHROME_TITLE_HEIGHT - 4; int32_t gadgetPad = 2; int32_t charW = 8; // bitmap font char width // Minimum title bar width: close gadget + 1 char + minimize gadget + padding int32_t titleMinW = gadgetPad + gadgetS + gadgetPad + charW + gadgetPad + gadgetS + gadgetPad; if (win->resizable) { // Add maximize gadget + padding titleMinW += gadgetS + gadgetPad; } *minW = titleMinW + CHROME_BORDER_WIDTH * 2; // 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 // ============================================================ void wmRaiseWindow(WindowStackT *stack, DirtyListT *dl, int32_t idx) { if (idx < 0 || idx >= stack->count - 1) { return; // already on top or invalid } WindowT *win = stack->windows[idx]; // Shift everything above it down for (int32_t i = idx; i < stack->count - 1; i++) { stack->windows[i] = stack->windows[i + 1]; } stack->windows[stack->count - 1] = win; // Mark the window area dirty since z-order changed dirtyListAdd(dl, win->x, win->y, win->w, win->h); // Update focusedIdx if it was affected if (stack->focusedIdx == idx) { stack->focusedIdx = stack->count - 1; } else if (stack->focusedIdx > idx) { stack->focusedIdx--; } // Update dragWindow if affected if (stack->dragWindow == idx) { stack->dragWindow = stack->count - 1; } else if (stack->dragWindow > idx) { stack->dragWindow--; } } // ============================================================ // wmReallocContentBuf // ============================================================ 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 // ============================================================ 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 // ============================================================ int32_t wmResizeEdgeHit(const WindowT *win, int32_t mx, int32_t my) { int32_t edge = RESIZE_NONE; int32_t border = CHROME_BORDER_WIDTH + 2; // resize grab area 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 // ============================================================ 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); 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 (newW >= minW) { win->x = newX; win->w = newW; } } if (stack->resizeEdge & RESIZE_RIGHT) { int32_t newW = win->w + dx; if (newW > maxW) { newW = maxW; } if (newW >= minW) { win->w = newW; } } if (stack->resizeEdge & RESIZE_TOP) { int32_t newY = win->y + dy; int32_t newH = win->h - dy; if (newH > maxH) { newY += newH - maxH; newH = maxH; } if (newH >= minH) { win->y = newY; win->h = newH; } } if (stack->resizeEdge & RESIZE_BOTTOM) { int32_t newH = win->h + dy; if (newH > maxH) { newH = maxH; } if (newH >= minH) { win->h = newH; } } // If resized while maximized, consider it no longer maximized if (win->maximized) { win->maximized = false; } wmUpdateContentRect(win); wmReallocContentBuf(win, d); // Call resize callback, then repaint if (win->onResize) { win->onResize(win, win->contentW, win->contentH); } if (win->onPaint) { RectT fullRect = {0, 0, win->contentW, win->contentH}; win->onPaint(win, &fullRect); win->contentDirty = true; } stack->dragOffX = mouseX; stack->dragOffY = mouseY; // Mark new position dirty dirtyListAdd(dl, win->x, win->y, win->w, win->h); } // ============================================================ // wmRestore // ============================================================ 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 // ============================================================ void wmRestoreMinimized(WindowStackT *stack, DirtyListT *dl, WindowT *win) { if (!win->minimized) { return; } // Dirty the icon area at the bottom of the screen // (we don't know our exact icon index here, so dirty the entire icon strip) // The compositeAndFlush will repaint correctly win->minimized = false; // Raise and focus the restored window 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; } } // Dirty the window area dirtyListAdd(dl, win->x, win->y, win->w, win->h); } // ============================================================ // wmScrollbarClick // ============================================================ 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 // ============================================================ 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 // ============================================================ 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 // ============================================================ 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); } // ============================================================ // wmSetTitle // ============================================================ 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 // ============================================================ 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; // Account for scrollbars 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; // If both scrollbars, adjust vertical scrollbar length if (win->vScroll) { win->vScroll->length = win->contentH; } } // Clamp to non-negative if (win->contentW < 0) { win->contentW = 0; } if (win->contentH < 0) { win->contentH = 0; } }