1871 lines
57 KiB
C
1871 lines
57 KiB
C
// 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 <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
|
|
// ============================================================
|
|
// 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; }
|
|
}
|