More widgets!

This commit is contained in:
Scott Duensing 2026-03-09 22:50:06 -05:00
parent 67900cbd7f
commit 76a955a9e7
33 changed files with 6610 additions and 1649 deletions

View file

@ -42,7 +42,7 @@ Supporting files:
| `dvxFont.h` | Built-in 8x14 bitmap font glyph data |
| `dvxCursor.h` | Mouse cursor bitmask data (5 shapes) |
| `dvxPalette.h` | Default VGA palette for 8-bit mode |
| `dvxIcon.c` | stb_image implementation unit (BMP/TGA/PNG) |
| `dvxIcon.c` | stb_image implementation unit (BMP/PNG/JPEG/GIF) |
| `thirdparty/stb_image.h` | Third-party single-header image loader |
## Quick start
@ -379,7 +379,17 @@ WidgetT *wgtHBox(WidgetT *parent); // horizontal stack
WidgetT *wgtFrame(WidgetT *parent, const char *title); // titled border
```
Containers hold child widgets and control layout direction. `wgtFrame`
draws a beveled border with a title label and lays out children vertically.
draws a styled border with a title label and lays out children vertically.
Frame styles (set `w->as.frame.style` after creation):
| Style | Description |
|-------|-------------|
| `FrameInE` | Beveled inward / sunken (default) |
| `FrameOutE` | Beveled outward / raised |
| `FrameFlatE` | Solid color line; set `w->as.frame.color` (0 = windowShadow) |
The title text is vertically centered on the top border line.
Container properties (set directly on the returned `WidgetT *`):

View file

@ -6,25 +6,58 @@ CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586
LDFLAGS = -lm
OBJDIR = ../obj
WOBJDIR = ../obj/widgets
BINDIR = ../bin
SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxIcon.c dvxWidget.c dvxApp.c demo.c
SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxIcon.c dvxImageWrite.c dvxApp.c demo.c
WSRCS = widgets/widgetCore.c \
widgets/widgetLayout.c \
widgets/widgetEvent.c \
widgets/widgetOps.c \
widgets/widgetBox.c \
widgets/widgetButton.c \
widgets/widgetCheckbox.c \
widgets/widgetComboBox.c \
widgets/widgetDropdown.c \
widgets/widgetCanvas.c \
widgets/widgetImage.c \
widgets/widgetLabel.c \
widgets/widgetListBox.c \
widgets/widgetProgressBar.c \
widgets/widgetRadio.c \
widgets/widgetSeparator.c \
widgets/widgetSlider.c \
widgets/widgetSpacer.c \
widgets/widgetStatusBar.c \
widgets/widgetTabControl.c \
widgets/widgetTextInput.c \
widgets/widgetToolbar.c \
widgets/widgetTreeView.c
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
WOBJS = $(patsubst widgets/%.c,$(WOBJDIR)/%.o,$(WSRCS))
TARGET = $(BINDIR)/demo.exe
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS) | $(BINDIR)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS)
$(TARGET): $(OBJS) $(WOBJS) | $(BINDIR)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS) $(WOBJS)
$(OBJDIR)/%.o: %.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(WOBJDIR)/%.o: widgets/%.c | $(WOBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR):
mkdir -p $(OBJDIR)
$(WOBJDIR):
mkdir -p $(WOBJDIR)
$(BINDIR):
mkdir -p $(BINDIR)
@ -34,9 +67,35 @@ $(OBJDIR)/dvxDraw.o: dvxDraw.c dvxDraw.h dvxTypes.h
$(OBJDIR)/dvxComp.o: dvxComp.c dvxComp.h dvxTypes.h
$(OBJDIR)/dvxWm.o: dvxWm.c dvxWm.h dvxTypes.h dvxDraw.h dvxComp.h dvxVideo.h thirdparty/stb_image.h
$(OBJDIR)/dvxIcon.o: dvxIcon.c thirdparty/stb_image.h
$(OBJDIR)/dvxWidget.o: dvxWidget.c dvxWidget.h dvxTypes.h dvxApp.h dvxDraw.h dvxWm.h dvxVideo.h
$(OBJDIR)/dvxImageWrite.o: dvxImageWrite.c thirdparty/stb_image_write.h
$(OBJDIR)/dvxApp.o: dvxApp.c dvxApp.h dvxTypes.h dvxVideo.h dvxDraw.h dvxComp.h dvxWm.h dvxFont.h dvxCursor.h
$(OBJDIR)/demo.o: demo.c dvxApp.h dvxWidget.h
# Widget file dependencies
WIDGET_DEPS = widgets/widgetInternal.h dvxWidget.h dvxTypes.h dvxApp.h dvxDraw.h dvxWm.h dvxVideo.h
$(WOBJDIR)/widgetCore.o: widgets/widgetCore.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetLayout.o: widgets/widgetLayout.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetEvent.o: widgets/widgetEvent.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetOps.o: widgets/widgetOps.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetBox.o: widgets/widgetBox.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetCanvas.o: widgets/widgetCanvas.c $(WIDGET_DEPS) thirdparty/stb_image.h thirdparty/stb_image_write.h
$(WOBJDIR)/widgetButton.o: widgets/widgetButton.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetCheckbox.o: widgets/widgetCheckbox.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetComboBox.o: widgets/widgetComboBox.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetDropdown.o: widgets/widgetDropdown.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetImage.o: widgets/widgetImage.c $(WIDGET_DEPS) thirdparty/stb_image.h
$(WOBJDIR)/widgetLabel.o: widgets/widgetLabel.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetListBox.o: widgets/widgetListBox.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetProgressBar.o: widgets/widgetProgressBar.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetRadio.o: widgets/widgetRadio.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetSeparator.o: widgets/widgetSeparator.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetSlider.o: widgets/widgetSlider.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetSpacer.o: widgets/widgetSpacer.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetStatusBar.o: widgets/widgetStatusBar.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetTabControl.o: widgets/widgetTabControl.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetTextInput.o: widgets/widgetTextInput.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetToolbar.o: widgets/widgetToolbar.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetTreeView.o: widgets/widgetTreeView.c $(WIDGET_DEPS)
clean:
rm -rf $(OBJDIR) $(BINDIR)

View file

@ -26,6 +26,7 @@ static void onOkClick(WidgetT *w);
static void onPaintColor(WindowT *win, RectT *dirtyArea);
static void onPaintPattern(WindowT *win, RectT *dirtyArea);
static void onPaintText(WindowT *win, RectT *dirtyArea);
static void setupWidgetDemo2(AppContextT *ctx);
static void setupWindows(AppContextT *ctx);
@ -249,6 +250,92 @@ static void onPaintText(WindowT *win, RectT *dirtyArea) {
}
// ============================================================
// setupWidgetDemo2
// ============================================================
static const char *colorItems[] = {"Red", "Green", "Blue", "Yellow", "Cyan", "Magenta"};
static const char *sizeItems[] = {"Small", "Medium", "Large", "Extra Large"};
static void setupWidgetDemo2(AppContextT *ctx) {
WindowT *win = dvxCreateWindow(ctx, "Advanced Widgets", 380, 50, 320, 400, true);
if (!win) {
return;
}
win->userData = ctx;
win->onClose = onCloseCb;
WidgetT *root = wgtInitWindow(ctx, win);
// TabControl at top
WidgetT *tabs = wgtTabControl(root);
// --- Tab 1: Controls ---
WidgetT *page1 = wgtTabPage(tabs, "Controls");
WidgetT *ddRow = wgtHBox(page1);
wgtLabel(ddRow, "Color:");
WidgetT *dd = wgtDropdown(ddRow);
wgtDropdownSetItems(dd, colorItems, 6);
wgtDropdownSetSelected(dd, 0);
WidgetT *cbRow = wgtHBox(page1);
wgtLabel(cbRow, "Size:");
WidgetT *cb = wgtComboBox(cbRow, 32);
wgtComboBoxSetItems(cb, sizeItems, 4);
wgtComboBoxSetSelected(cb, 1);
wgtHSeparator(page1);
wgtLabel(page1, "Progress:");
WidgetT *pb = wgtProgressBar(page1);
wgtProgressBarSetValue(pb, 65);
wgtLabel(page1, "Volume:");
wgtSlider(page1, 0, 100);
// --- Tab 2: Tree ---
WidgetT *page2 = wgtTabPage(tabs, "Tree");
WidgetT *tree = wgtTreeView(page2);
WidgetT *docs = wgtTreeItem(tree, "Documents");
wgtTreeItemSetExpanded(docs, true);
wgtTreeItem(docs, "README.md");
wgtTreeItem(docs, "DESIGN.md");
WidgetT *src = wgtTreeItem(docs, "src");
wgtTreeItemSetExpanded(src, true);
wgtTreeItem(src, "main.c");
wgtTreeItem(src, "utils.c");
wgtTreeItem(src, "render.c");
WidgetT *images = wgtTreeItem(tree, "Images");
wgtTreeItem(images, "logo.png");
wgtTreeItem(images, "icon.bmp");
WidgetT *config = wgtTreeItem(tree, "Config");
wgtTreeItem(config, "settings.ini");
wgtTreeItem(config, "palette.dat");
// --- Tab 3: Toolbar ---
WidgetT *page3 = wgtTabPage(tabs, "Toolbar");
WidgetT *tb = wgtToolbar(page3);
wgtButton(tb, "New");
wgtButton(tb, "Open");
wgtButton(tb, "Save");
wgtLabel(page3, "Toolbar with buttons above.");
// Status bar at bottom (outside tabs)
WidgetT *sb = wgtStatusBar(root);
WidgetT *sbLabel = wgtLabel(sb, "Ready");
sbLabel->weight = 100;
wgtLabel(sb, "Line 1, Col 1");
wgtInvalidate(root);
}
// ============================================================
// setupWindows
// ============================================================
@ -386,6 +473,7 @@ int main(void) {
}
setupWindows(&ctx);
setupWidgetDemo2(&ctx);
dvxRun(&ctx);

View file

@ -703,12 +703,12 @@ static void initColorScheme(AppContextT *ctx) {
ctx->colors.desktop = packColor(d, 0, 128, 128); // GEOS teal desktop
ctx->colors.windowFace = packColor(d, 192, 192, 192); // standard Motif grey
ctx->colors.windowHighlight = packColor(d, 255, 255, 255);
ctx->colors.windowShadow = packColor(d, 80, 80, 80);
ctx->colors.windowShadow = packColor(d, 128, 128, 128);
ctx->colors.activeTitleBg = packColor(d, 48, 48, 48); // GEOS dark charcoal
ctx->colors.activeTitleFg = packColor(d, 255, 255, 255);
ctx->colors.inactiveTitleBg = packColor(d, 160, 160, 160); // lighter grey
ctx->colors.inactiveTitleFg = packColor(d, 64, 64, 64);
ctx->colors.contentBg = packColor(d, 255, 255, 255);
ctx->colors.contentBg = packColor(d, 192, 192, 192);
ctx->colors.contentFg = packColor(d, 0, 0, 0);
ctx->colors.menuBg = packColor(d, 192, 192, 192);
ctx->colors.menuFg = packColor(d, 0, 0, 0);

View file

@ -4,8 +4,9 @@
#pragma GCC diagnostic ignored "-Wunused-function"
#define STBI_ONLY_BMP
#define STBI_ONLY_TGA
#define STBI_ONLY_PNG
#define STBI_ONLY_JPEG
#define STBI_ONLY_GIF
#define STBI_NO_SIMD
#define STB_IMAGE_IMPLEMENTATION
#include "thirdparty/stb_image.h"

10
dvx/dvxImageWrite.c Normal file
View file

@ -0,0 +1,10 @@
// dvxImageWrite.c — stb_image_write implementation for DV/X GUI
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-function"
#define STBI_WRITE_NO_SIMD
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "thirdparty/stb_image_write.h"
#pragma GCC diagnostic pop

File diff suppressed because it is too large Load diff

View file

@ -50,7 +50,19 @@ typedef enum {
WidgetListBoxE,
WidgetSpacerE,
WidgetSeparatorE,
WidgetFrameE
WidgetFrameE,
WidgetDropdownE,
WidgetComboBoxE,
WidgetProgressBarE,
WidgetSliderE,
WidgetTabControlE,
WidgetTabPageE,
WidgetStatusBarE,
WidgetToolbarE,
WidgetTreeViewE,
WidgetTreeItemE,
WidgetImageE,
WidgetCanvasE
} WidgetTypeE;
// ============================================================
@ -67,6 +79,16 @@ typedef enum {
AlignEndE
} WidgetAlignE;
// ============================================================
// Frame style enum
// ============================================================
typedef enum {
FrameInE, // beveled inward (sunken) — default
FrameOutE, // beveled outward (raised)
FrameFlatE // solid color line
} FrameStyleE;
// ============================================================
// Widget structure
// ============================================================
@ -108,6 +130,10 @@ typedef struct WidgetT {
int32_t spacing; // tagged size for spacing between children (0 = default)
int32_t padding; // tagged size for internal padding (0 = default)
// Colors (0 = use color scheme defaults)
uint32_t fgColor;
uint32_t bgColor;
// State
bool visible;
bool enabled;
@ -173,7 +199,80 @@ typedef struct WidgetT {
struct {
const char *title;
FrameStyleE style; // FrameInE (default), FrameOutE, FrameFlatE
uint32_t color; // border color for FrameFlatE (0 = use windowShadow)
} frame;
struct {
const char **items;
int32_t itemCount;
int32_t selectedIdx;
bool open;
int32_t hoverIdx;
int32_t scrollPos;
} dropdown;
struct {
char *buf;
int32_t bufSize;
int32_t len;
int32_t cursorPos;
int32_t scrollOff;
const char **items;
int32_t itemCount;
int32_t selectedIdx;
bool open;
int32_t hoverIdx;
int32_t listScrollPos;
} comboBox;
struct {
int32_t value;
int32_t maxValue;
} progressBar;
struct {
int32_t value;
int32_t minValue;
int32_t maxValue;
bool vertical;
} slider;
struct {
int32_t activeTab;
} tabControl;
struct {
const char *title;
} tabPage;
struct {
int32_t scrollPos;
} treeView;
struct {
const char *text;
bool expanded;
} treeItem;
struct {
uint8_t *data; // pixel buffer in display format
int32_t imgW;
int32_t imgH;
int32_t imgPitch;
bool pressed;
} image;
struct {
uint8_t *data; // pixel buffer in display format
int32_t canvasW;
int32_t canvasH;
int32_t canvasPitch;
uint32_t penColor;
int32_t penSize;
int32_t lastX;
int32_t lastY;
} canvas;
} as;
} WidgetT;
@ -224,6 +323,98 @@ WidgetT *wgtVSeparator(WidgetT *parent);
WidgetT *wgtListBox(WidgetT *parent);
WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen);
// ============================================================
// Dropdown and ComboBox
// ============================================================
WidgetT *wgtDropdown(WidgetT *parent);
void wgtDropdownSetItems(WidgetT *w, const char **items, int32_t count);
int32_t wgtDropdownGetSelected(const WidgetT *w);
void wgtDropdownSetSelected(WidgetT *w, int32_t idx);
WidgetT *wgtComboBox(WidgetT *parent, int32_t maxLen);
void wgtComboBoxSetItems(WidgetT *w, const char **items, int32_t count);
int32_t wgtComboBoxGetSelected(const WidgetT *w);
void wgtComboBoxSetSelected(WidgetT *w, int32_t idx);
// ============================================================
// ProgressBar
// ============================================================
WidgetT *wgtProgressBar(WidgetT *parent);
void wgtProgressBarSetValue(WidgetT *w, int32_t value);
int32_t wgtProgressBarGetValue(const WidgetT *w);
// ============================================================
// Slider (TrackBar)
// ============================================================
WidgetT *wgtSlider(WidgetT *parent, int32_t minVal, int32_t maxVal);
void wgtSliderSetValue(WidgetT *w, int32_t value);
int32_t wgtSliderGetValue(const WidgetT *w);
// ============================================================
// TabControl
// ============================================================
WidgetT *wgtTabControl(WidgetT *parent);
WidgetT *wgtTabPage(WidgetT *parent, const char *title);
void wgtTabControlSetActive(WidgetT *w, int32_t idx);
int32_t wgtTabControlGetActive(const WidgetT *w);
// ============================================================
// StatusBar and Toolbar
// ============================================================
WidgetT *wgtStatusBar(WidgetT *parent);
WidgetT *wgtToolbar(WidgetT *parent);
// ============================================================
// TreeView
// ============================================================
WidgetT *wgtTreeView(WidgetT *parent);
WidgetT *wgtTreeItem(WidgetT *parent, const char *text);
void wgtTreeItemSetExpanded(WidgetT *w, bool expanded);
bool wgtTreeItemIsExpanded(const WidgetT *w);
// ============================================================
// Image
// ============================================================
// Create an image widget from raw pixel data (display format).
// Takes ownership of the data buffer (freed on destroy).
WidgetT *wgtImage(WidgetT *parent, uint8_t *data, int32_t w, int32_t h, int32_t pitch);
// Load an image widget from a file (BMP, PNG, JPEG, GIF).
// Returns NULL on load failure.
WidgetT *wgtImageFromFile(WidgetT *parent, const char *path);
// Replace the image data. Takes ownership of the new buffer.
void wgtImageSetData(WidgetT *w, uint8_t *data, int32_t imgW, int32_t imgH, int32_t pitch);
// ============================================================
// Canvas
// ============================================================
// Create a drawable canvas widget with the given dimensions.
WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h);
// Clear the canvas to the specified color.
void wgtCanvasClear(WidgetT *w, uint32_t color);
// Set the pen color (in display pixel format).
void wgtCanvasSetPenColor(WidgetT *w, uint32_t color);
// Set the pen size in pixels (diameter).
void wgtCanvasSetPenSize(WidgetT *w, int32_t size);
// Save the canvas to a PNG file. Returns 0 on success, -1 on failure.
int32_t wgtCanvasSave(WidgetT *w, const char *path);
// Load a PNG file onto the canvas. Returns 0 on success, -1 on failure.
int32_t wgtCanvasLoad(WidgetT *w, const char *path);
// ============================================================
// Operations
// ============================================================

1724
dvx/thirdparty/stb_image_write.h vendored Normal file

File diff suppressed because it is too large Load diff

97
dvx/widgets/widgetBox.c Normal file
View file

@ -0,0 +1,97 @@
// widgetBox.c — VBox, HBox, and Frame container widgets
#include "widgetInternal.h"
// ============================================================
// widgetFramePaint
// ============================================================
void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops,
const BitmapFontT *font, const ColorSchemeT *colors) {
int32_t fb = widgetFrameBorderWidth(w);
int32_t boxY = w->y + font->charHeight / 2;
int32_t boxH = w->h - font->charHeight / 2;
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
if (w->as.frame.style == FrameFlatE) {
// Flat: solid color rectangle outline
uint32_t fc = w->as.frame.color ? w->as.frame.color : colors->windowShadow;
drawHLine(d, ops, w->x, boxY, w->w, fc);
drawHLine(d, ops, w->x, boxY + boxH - 1, w->w, fc);
drawVLine(d, ops, w->x, boxY, boxH, fc);
drawVLine(d, ops, w->x + w->w - 1, boxY, boxH, fc);
} else {
// Beveled groove/ridge: two nested 1px bevels
BevelStyleT outer;
BevelStyleT inner;
if (w->as.frame.style == FrameInE) {
outer.highlight = colors->windowShadow;
outer.shadow = colors->windowHighlight;
inner.highlight = colors->windowHighlight;
inner.shadow = colors->windowShadow;
} else {
outer.highlight = colors->windowHighlight;
outer.shadow = colors->windowShadow;
inner.highlight = colors->windowShadow;
inner.shadow = colors->windowHighlight;
}
outer.face = 0;
outer.width = 1;
inner.face = 0;
inner.width = 1;
drawBevel(d, ops, w->x, boxY, w->w, boxH, &outer);
drawBevel(d, ops, w->x + 1, boxY + 1, w->w - 2, boxH - 2, &inner);
}
// Draw title centered vertically on the top border line
if (w->as.frame.title && w->as.frame.title[0]) {
int32_t titleW = (int32_t)strlen(w->as.frame.title) * font->charWidth;
int32_t titleX = w->x + DEFAULT_PADDING + fb;
int32_t titleY = boxY + (fb - font->charHeight) / 2;
rectFill(d, ops, titleX - 2, titleY,
titleW + 4, font->charHeight, bg);
drawText(d, ops, font, titleX, titleY,
w->as.frame.title, fg, bg, true);
}
}
// ============================================================
// wgtFrame
// ============================================================
WidgetT *wgtFrame(WidgetT *parent, const char *title) {
WidgetT *w = widgetAlloc(parent, WidgetFrameE);
if (w) {
w->as.frame.title = title;
w->as.frame.style = FrameInE;
w->as.frame.color = 0;
}
return w;
}
// ============================================================
// wgtHBox
// ============================================================
WidgetT *wgtHBox(WidgetT *parent) {
return widgetAlloc(parent, WidgetHBoxE);
}
// ============================================================
// wgtVBox
// ============================================================
WidgetT *wgtVBox(WidgetT *parent) {
return widgetAlloc(parent, WidgetVBoxE);
}

View file

@ -0,0 +1,77 @@
// widgetButton.c — Button widget
#include "widgetInternal.h"
// ============================================================
// wgtButton
// ============================================================
WidgetT *wgtButton(WidgetT *parent, const char *text) {
WidgetT *w = widgetAlloc(parent, WidgetButtonE);
if (w) {
w->as.button.text = text;
w->as.button.pressed = false;
}
return w;
}
// ============================================================
// widgetButtonCalcMinSize
// ============================================================
void widgetButtonCalcMinSize(WidgetT *w, const BitmapFontT *font) {
w->calcMinW = (int32_t)strlen(w->as.button.text) * font->charWidth + BUTTON_PAD_H * 2;
w->calcMinH = font->charHeight + BUTTON_PAD_V * 2;
}
// ============================================================
// widgetButtonOnMouse
// ============================================================
void widgetButtonOnMouse(WidgetT *hit) {
hit->as.button.pressed = true;
wgtInvalidate(hit);
if (hit->onClick) {
hit->onClick(hit);
}
hit->as.button.pressed = false;
}
// ============================================================
// widgetButtonPaint
// ============================================================
void widgetButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops,
const BitmapFontT *font, const ColorSchemeT *colors) {
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
uint32_t bgFace = w->bgColor ? w->bgColor : colors->buttonFace;
BevelStyleT bevel;
bevel.highlight = w->as.button.pressed ? colors->windowShadow : colors->windowHighlight;
bevel.shadow = w->as.button.pressed ? colors->windowHighlight : colors->windowShadow;
bevel.face = bgFace;
bevel.width = 2;
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
int32_t textW = (int32_t)strlen(w->as.button.text) * font->charWidth;
int32_t textX = w->x + (w->w - textW) / 2;
int32_t textY = w->y + (w->h - font->charHeight) / 2;
if (w->as.button.pressed) {
textX++;
textY++;
}
drawText(d, ops, font, textX, textY,
w->as.button.text,
w->enabled ? fg : colors->windowShadow,
bgFace, true);
}

469
dvx/widgets/widgetCanvas.c Normal file
View file

@ -0,0 +1,469 @@
// widgetCanvas.c — Drawable canvas widget (freehand draw, PNG save/load)
#include "widgetInternal.h"
#include "../thirdparty/stb_image.h"
#include "../thirdparty/stb_image_write.h"
#define CANVAS_BORDER 2
// ============================================================
// Prototypes
// ============================================================
static void canvasDrawDot(WidgetT *w, int32_t cx, int32_t cy);
static void canvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1);
static void canvasUnpackColor(const DisplayT *d, uint32_t pixel, uint8_t *r, uint8_t *g, uint8_t *b);
// ============================================================
// canvasDrawDot
// ============================================================
//
// Draw a filled circle of diameter penSize at (cx, cy) in canvas coords.
static void canvasDrawDot(WidgetT *w, int32_t cx, int32_t cy) {
int32_t bpp = w->as.canvas.canvasPitch / w->as.canvas.canvasW;
int32_t pitch = w->as.canvas.canvasPitch;
uint8_t *data = w->as.canvas.data;
int32_t cw = w->as.canvas.canvasW;
int32_t ch = w->as.canvas.canvasH;
uint32_t color = w->as.canvas.penColor;
int32_t rad = w->as.canvas.penSize / 2;
if (rad < 1) {
// Single pixel
if (cx >= 0 && cx < cw && cy >= 0 && cy < ch) {
uint8_t *dst = data + cy * pitch + cx * bpp;
if (bpp == 1) {
*dst = (uint8_t)color;
} else if (bpp == 2) {
*(uint16_t *)dst = (uint16_t)color;
} else {
*(uint32_t *)dst = color;
}
}
return;
}
// Filled circle via bounding box + radius check
int32_t r2 = rad * rad;
for (int32_t dy = -rad; dy <= rad; dy++) {
int32_t py = cy + dy;
if (py < 0 || py >= ch) {
continue;
}
for (int32_t dx = -rad; dx <= rad; dx++) {
int32_t px = cx + dx;
if (px < 0 || px >= cw) {
continue;
}
if (dx * dx + dy * dy <= r2) {
uint8_t *dst = data + py * pitch + px * bpp;
if (bpp == 1) {
*dst = (uint8_t)color;
} else if (bpp == 2) {
*(uint16_t *)dst = (uint16_t)color;
} else {
*(uint32_t *)dst = color;
}
}
}
}
}
// ============================================================
// canvasDrawLine
// ============================================================
//
// Bresenham line from (x0,y0) to (x1,y1), placing dots along the path.
static void canvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1) {
int32_t dx = x1 - x0;
int32_t dy = y1 - y0;
int32_t sx = (dx >= 0) ? 1 : -1;
int32_t sy = (dy >= 0) ? 1 : -1;
if (dx < 0) { dx = -dx; }
if (dy < 0) { dy = -dy; }
int32_t err = dx - dy;
for (;;) {
canvasDrawDot(w, x0, y0);
if (x0 == x1 && y0 == y1) {
break;
}
int32_t e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x0 += sx;
}
if (e2 < dx) {
err += dx;
y0 += sy;
}
}
}
// ============================================================
// canvasUnpackColor
// ============================================================
//
// Reverse of packColor — extract RGB from a display-format pixel.
static void canvasUnpackColor(const DisplayT *d, uint32_t pixel, uint8_t *r, uint8_t *g, uint8_t *b) {
if (d->format.bitsPerPixel == 8) {
// 8-bit paletted — look up the palette entry
int32_t idx = pixel & 0xFF;
*r = d->palette[idx * 3 + 0];
*g = d->palette[idx * 3 + 1];
*b = d->palette[idx * 3 + 2];
return;
}
uint32_t rv = (pixel >> d->format.redShift) & ((1u << d->format.redBits) - 1);
uint32_t gv = (pixel >> d->format.greenShift) & ((1u << d->format.greenBits) - 1);
uint32_t bv = (pixel >> d->format.blueShift) & ((1u << d->format.blueBits) - 1);
// Scale back up to 8 bits
*r = (uint8_t)(rv << (8 - d->format.redBits));
*g = (uint8_t)(gv << (8 - d->format.greenBits));
*b = (uint8_t)(bv << (8 - d->format.blueBits));
}
// ============================================================
// wgtCanvas
// ============================================================
WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h) {
if (!parent || w <= 0 || h <= 0) {
return NULL;
}
// Find the AppContextT to get display format
WidgetT *root = parent;
while (root->parent) {
root = root->parent;
}
AppContextT *ctx = (AppContextT *)root->userData;
if (!ctx) {
return NULL;
}
const DisplayT *d = &ctx->display;
int32_t bpp = d->format.bytesPerPixel;
int32_t pitch = w * bpp;
uint8_t *data = (uint8_t *)malloc(pitch * h);
if (!data) {
return NULL;
}
// Fill with white
uint32_t white = packColor(d, 255, 255, 255);
for (int32_t y = 0; y < h; y++) {
for (int32_t x = 0; x < w; x++) {
uint8_t *dst = data + y * pitch + x * bpp;
if (bpp == 1) {
*dst = (uint8_t)white;
} else if (bpp == 2) {
*(uint16_t *)dst = (uint16_t)white;
} else {
*(uint32_t *)dst = white;
}
}
}
WidgetT *wgt = widgetAlloc(parent, WidgetCanvasE);
if (wgt) {
wgt->as.canvas.data = data;
wgt->as.canvas.canvasW = w;
wgt->as.canvas.canvasH = h;
wgt->as.canvas.canvasPitch = pitch;
wgt->as.canvas.penColor = packColor(d, 0, 0, 0);
wgt->as.canvas.penSize = 2;
wgt->as.canvas.lastX = -1;
wgt->as.canvas.lastY = -1;
} else {
free(data);
}
return wgt;
}
// ============================================================
// wgtCanvasClear
// ============================================================
void wgtCanvasClear(WidgetT *w, uint32_t color) {
if (!w || w->type != WidgetCanvasE || !w->as.canvas.data) {
return;
}
int32_t bpp = w->as.canvas.canvasPitch / w->as.canvas.canvasW;
int32_t pitch = w->as.canvas.canvasPitch;
int32_t cw = w->as.canvas.canvasW;
int32_t ch = w->as.canvas.canvasH;
for (int32_t y = 0; y < ch; y++) {
for (int32_t x = 0; x < cw; x++) {
uint8_t *dst = w->as.canvas.data + 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;
}
}
}
}
// ============================================================
// wgtCanvasLoad
// ============================================================
int32_t wgtCanvasLoad(WidgetT *w, const char *path) {
if (!w || w->type != WidgetCanvasE || !path) {
return -1;
}
// Find the AppContextT to get display format
WidgetT *root = w;
while (root->parent) {
root = root->parent;
}
AppContextT *ctx = (AppContextT *)root->userData;
if (!ctx) {
return -1;
}
const DisplayT *d = &ctx->display;
int imgW;
int imgH;
int channels;
uint8_t *rgb = stbi_load(path, &imgW, &imgH, &channels, 3);
if (!rgb) {
return -1;
}
int32_t bpp = d->format.bytesPerPixel;
int32_t pitch = imgW * bpp;
uint8_t *data = (uint8_t *)malloc(pitch * imgH);
if (!data) {
stbi_image_free(rgb);
return -1;
}
for (int32_t y = 0; y < imgH; y++) {
for (int32_t x = 0; x < imgW; x++) {
const uint8_t *src = rgb + (y * imgW + x) * 3;
uint32_t color = packColor(d, src[0], src[1], src[2]);
uint8_t *dst = data + 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(rgb);
free(w->as.canvas.data);
w->as.canvas.data = data;
w->as.canvas.canvasW = imgW;
w->as.canvas.canvasH = imgH;
w->as.canvas.canvasPitch = pitch;
return 0;
}
// ============================================================
// wgtCanvasSave
// ============================================================
int32_t wgtCanvasSave(WidgetT *w, const char *path) {
if (!w || w->type != WidgetCanvasE || !path || !w->as.canvas.data) {
return -1;
}
// Find the AppContextT to get display format
WidgetT *root = w;
while (root->parent) {
root = root->parent;
}
AppContextT *ctx = (AppContextT *)root->userData;
if (!ctx) {
return -1;
}
const DisplayT *d = &ctx->display;
int32_t cw = w->as.canvas.canvasW;
int32_t ch = w->as.canvas.canvasH;
int32_t bpp = d->format.bytesPerPixel;
int32_t pitch = w->as.canvas.canvasPitch;
// Convert display format back to RGB
uint8_t *rgb = (uint8_t *)malloc(cw * ch * 3);
if (!rgb) {
return -1;
}
for (int32_t y = 0; y < ch; y++) {
for (int32_t x = 0; x < cw; x++) {
const uint8_t *src = w->as.canvas.data + y * pitch + x * bpp;
uint32_t pixel;
if (bpp == 1) {
pixel = *src;
} else if (bpp == 2) {
pixel = *(const uint16_t *)src;
} else {
pixel = *(const uint32_t *)src;
}
uint8_t *dst = rgb + (y * cw + x) * 3;
canvasUnpackColor(d, pixel, &dst[0], &dst[1], &dst[2]);
}
}
int32_t result = stbi_write_png(path, cw, ch, 3, rgb, cw * 3);
free(rgb);
return result ? 0 : -1;
}
// ============================================================
// wgtCanvasSetPenColor
// ============================================================
void wgtCanvasSetPenColor(WidgetT *w, uint32_t color) {
if (w && w->type == WidgetCanvasE) {
w->as.canvas.penColor = color;
}
}
// ============================================================
// wgtCanvasSetPenSize
// ============================================================
void wgtCanvasSetPenSize(WidgetT *w, int32_t size) {
if (w && w->type == WidgetCanvasE && size > 0) {
w->as.canvas.penSize = size;
}
}
// ============================================================
// widgetCanvasCalcMinSize
// ============================================================
void widgetCanvasCalcMinSize(WidgetT *w, const BitmapFontT *font) {
(void)font;
w->calcMinW = w->as.canvas.canvasW + CANVAS_BORDER * 2;
w->calcMinH = w->as.canvas.canvasH + CANVAS_BORDER * 2;
}
// ============================================================
// widgetCanvasOnMouse
// ============================================================
void widgetCanvasOnMouse(WidgetT *hit, int32_t vx, int32_t vy) {
// Convert widget coords to canvas coords
int32_t cx = vx - hit->x - CANVAS_BORDER;
int32_t cy = vy - hit->y - CANVAS_BORDER;
if (sDrawingCanvas == hit) {
// Continuation of a drag stroke — draw line from last to current
if (hit->as.canvas.lastX >= 0) {
canvasDrawLine(hit, hit->as.canvas.lastX, hit->as.canvas.lastY, cx, cy);
} else {
canvasDrawDot(hit, cx, cy);
}
} else {
// First click — start drawing, place a dot
sDrawingCanvas = hit;
canvasDrawDot(hit, cx, cy);
}
hit->as.canvas.lastX = cx;
hit->as.canvas.lastY = cy;
wgtInvalidate(hit);
}
// ============================================================
// widgetCanvasPaint
// ============================================================
void widgetCanvasPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops,
const BitmapFontT *font, const ColorSchemeT *colors) {
(void)font;
if (!w->as.canvas.data) {
return;
}
// Draw a sunken bevel border around the canvas
BevelStyleT sunken;
sunken.highlight = colors->windowShadow;
sunken.shadow = colors->windowHighlight;
sunken.face = 0;
sunken.width = CANVAS_BORDER;
drawBevel(d, ops, w->x, w->y, w->w, w->h, &sunken);
// Blit the canvas data inside the border
int32_t imgW = w->as.canvas.canvasW;
int32_t imgH = w->as.canvas.canvasH;
int32_t dx = w->x + CANVAS_BORDER;
int32_t dy = w->y + CANVAS_BORDER;
rectCopy(d, ops, dx, dy,
w->as.canvas.data, w->as.canvas.canvasPitch,
0, 0, imgW, imgH);
}

View file

@ -0,0 +1,81 @@
// widgetCheckbox.c — Checkbox widget
#include "widgetInternal.h"
// ============================================================
// wgtCheckbox
// ============================================================
WidgetT *wgtCheckbox(WidgetT *parent, const char *text) {
WidgetT *w = widgetAlloc(parent, WidgetCheckboxE);
if (w) {
w->as.checkbox.text = text;
w->as.checkbox.checked = false;
}
return w;
}
// ============================================================
// widgetCheckboxCalcMinSize
// ============================================================
void widgetCheckboxCalcMinSize(WidgetT *w, const BitmapFontT *font) {
w->calcMinW = CHECKBOX_BOX_SIZE + CHECKBOX_GAP +
(int32_t)strlen(w->as.checkbox.text) * font->charWidth;
w->calcMinH = DVX_MAX(CHECKBOX_BOX_SIZE, font->charHeight);
}
// ============================================================
// widgetCheckboxOnMouse
// ============================================================
void widgetCheckboxOnMouse(WidgetT *hit) {
hit->as.checkbox.checked = !hit->as.checkbox.checked;
if (hit->onChange) {
hit->onChange(hit);
}
}
// ============================================================
// widgetCheckboxPaint
// ============================================================
void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops,
const BitmapFontT *font, const ColorSchemeT *colors) {
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
int32_t boxY = w->y + (w->h - CHECKBOX_BOX_SIZE) / 2;
// Draw checkbox box
BevelStyleT bevel;
bevel.highlight = colors->windowShadow;
bevel.shadow = colors->windowHighlight;
bevel.face = bg;
bevel.width = 1;
drawBevel(d, ops, w->x, boxY, CHECKBOX_BOX_SIZE, CHECKBOX_BOX_SIZE, &bevel);
// Draw check mark if checked
if (w->as.checkbox.checked) {
int32_t cx = w->x + 3;
int32_t cy = boxY + 3;
int32_t cs = CHECKBOX_BOX_SIZE - 6;
for (int32_t i = 0; i < cs; i++) {
drawHLine(d, ops, cx + i, cy + i, 1, fg);
drawHLine(d, ops, cx + cs - 1 - i, cy + i, 1, fg);
}
}
// Draw label
drawText(d, ops, font,
w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP,
w->y + (w->h - font->charHeight) / 2,
w->as.checkbox.text, fg, bg, false);
}

View file

@ -0,0 +1,248 @@
// widgetComboBox.c — ComboBox widget (editable text + dropdown list)
#include "widgetInternal.h"
// ============================================================
// wgtComboBox
// ============================================================
WidgetT *wgtComboBox(WidgetT *parent, int32_t maxLen) {
WidgetT *w = widgetAlloc(parent, WidgetComboBoxE);
if (w) {
int32_t bufSize = maxLen > 0 ? maxLen + 1 : 256;
w->as.comboBox.buf = (char *)malloc(bufSize);
w->as.comboBox.bufSize = bufSize;
if (w->as.comboBox.buf) {
w->as.comboBox.buf[0] = '\0';
}
w->as.comboBox.selectedIdx = -1;
w->weight = 100;
}
return w;
}
// ============================================================
// wgtComboBoxGetSelected
// ============================================================
int32_t wgtComboBoxGetSelected(const WidgetT *w) {
if (!w || w->type != WidgetComboBoxE) {
return -1;
}
return w->as.comboBox.selectedIdx;
}
// ============================================================
// wgtComboBoxSetItems
// ============================================================
void wgtComboBoxSetItems(WidgetT *w, const char **items, int32_t count) {
if (!w || w->type != WidgetComboBoxE) {
return;
}
w->as.comboBox.items = items;
w->as.comboBox.itemCount = count;
if (w->as.comboBox.selectedIdx >= count) {
w->as.comboBox.selectedIdx = -1;
}
}
// ============================================================
// wgtComboBoxSetSelected
// ============================================================
void wgtComboBoxSetSelected(WidgetT *w, int32_t idx) {
if (!w || w->type != WidgetComboBoxE) {
return;
}
w->as.comboBox.selectedIdx = idx;
// Copy selected item text to buffer
if (idx >= 0 && idx < w->as.comboBox.itemCount && w->as.comboBox.buf) {
strncpy(w->as.comboBox.buf, w->as.comboBox.items[idx], w->as.comboBox.bufSize - 1);
w->as.comboBox.buf[w->as.comboBox.bufSize - 1] = '\0';
w->as.comboBox.len = (int32_t)strlen(w->as.comboBox.buf);
w->as.comboBox.cursorPos = w->as.comboBox.len;
w->as.comboBox.scrollOff = 0;
}
}
// ============================================================
// widgetComboBoxCalcMinSize
// ============================================================
void widgetComboBoxCalcMinSize(WidgetT *w, const BitmapFontT *font) {
int32_t maxItemW = font->charWidth * 8;
for (int32_t i = 0; i < w->as.comboBox.itemCount; i++) {
int32_t iw = (int32_t)strlen(w->as.comboBox.items[i]) * font->charWidth;
if (iw > maxItemW) {
maxItemW = iw;
}
}
w->calcMinW = maxItemW + DROPDOWN_BTN_WIDTH + TEXT_INPUT_PAD * 2 + 4;
w->calcMinH = font->charHeight + TEXT_INPUT_PAD * 2;
}
// ============================================================
// widgetComboBoxOnMouse
// ============================================================
void widgetComboBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx) {
// Check if click is on the button area
int32_t textAreaW = hit->w - DROPDOWN_BTN_WIDTH;
if (vx >= hit->x + textAreaW) {
// Button click — toggle popup
hit->as.comboBox.open = !hit->as.comboBox.open;
hit->as.comboBox.hoverIdx = hit->as.comboBox.selectedIdx;
sOpenPopup = hit->as.comboBox.open ? hit : NULL;
} else {
// Text area click — focus for editing
hit->focused = true;
AppContextT *ctx = (AppContextT *)root->userData;
const BitmapFontT *font = &ctx->font;
int32_t relX = vx - hit->x - TEXT_INPUT_PAD;
int32_t charPos = relX / font->charWidth + hit->as.comboBox.scrollOff;
if (charPos < 0) {
charPos = 0;
}
if (charPos > hit->as.comboBox.len) {
charPos = hit->as.comboBox.len;
}
hit->as.comboBox.cursorPos = charPos;
}
}
// ============================================================
// widgetComboBoxPaint
// ============================================================
void widgetComboBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops,
const BitmapFontT *font, const ColorSchemeT *colors) {
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
// Sunken text area
int32_t textAreaW = w->w - DROPDOWN_BTN_WIDTH;
BevelStyleT bevel;
bevel.highlight = colors->windowShadow;
bevel.shadow = colors->windowHighlight;
bevel.face = bg;
bevel.width = 2;
drawBevel(d, ops, w->x, w->y, textAreaW, w->h, &bevel);
// Draw text content
if (w->as.comboBox.buf) {
int32_t textX = w->x + TEXT_INPUT_PAD;
int32_t textY = w->y + (w->h - font->charHeight) / 2;
int32_t maxChars = (textAreaW - TEXT_INPUT_PAD * 2 - 4) / font->charWidth;
int32_t off = w->as.comboBox.scrollOff;
int32_t len = w->as.comboBox.len - off;
if (len > maxChars) {
len = maxChars;
}
for (int32_t i = 0; i < len; i++) {
drawChar(d, ops, font, textX + i * font->charWidth, textY,
w->as.comboBox.buf[off + i], fg, bg, true);
}
// Draw cursor
if (w->focused && !w->as.comboBox.open) {
int32_t cursorX = textX + (w->as.comboBox.cursorPos - off) * font->charWidth;
if (cursorX >= w->x + TEXT_INPUT_PAD &&
cursorX < w->x + textAreaW - TEXT_INPUT_PAD) {
drawVLine(d, ops, cursorX, textY, font->charHeight, fg);
}
}
}
// Drop button
BevelStyleT btnBevel;
btnBevel.highlight = w->as.comboBox.open ? colors->windowShadow : colors->windowHighlight;
btnBevel.shadow = w->as.comboBox.open ? colors->windowHighlight : colors->windowShadow;
btnBevel.face = colors->buttonFace;
btnBevel.width = 2;
drawBevel(d, ops, w->x + textAreaW, w->y, DROPDOWN_BTN_WIDTH, w->h, &btnBevel);
// Down arrow
int32_t arrowX = w->x + textAreaW + DROPDOWN_BTN_WIDTH / 2;
int32_t arrowY = w->y + w->h / 2 - 1;
for (int32_t i = 0; i < 4; i++) {
drawHLine(d, ops, arrowX - 3 + i, arrowY + i, 7 - i * 2, colors->contentFg);
}
}
// ============================================================
// widgetComboBoxPaintPopup
// ============================================================
void widgetComboBoxPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops,
const BitmapFontT *font, const ColorSchemeT *colors) {
int32_t popX;
int32_t popY;
int32_t popW;
int32_t popH;
widgetDropdownPopupRect(w, font, d->clipH, &popX, &popY, &popW, &popH);
// Draw popup border
BevelStyleT bevel;
bevel.highlight = colors->windowHighlight;
bevel.shadow = colors->windowShadow;
bevel.face = colors->contentBg;
bevel.width = 2;
drawBevel(d, ops, popX, popY, popW, popH, &bevel);
// Draw items
int32_t itemCount = w->as.comboBox.itemCount;
const char **items = w->as.comboBox.items;
int32_t selIdx = w->as.comboBox.selectedIdx;
int32_t hoverIdx = w->as.comboBox.hoverIdx;
int32_t scrollPos = w->as.comboBox.listScrollPos;
int32_t visibleItems = popH / font->charHeight;
int32_t textX = popX + TEXT_INPUT_PAD;
int32_t textY = popY + 2;
int32_t textW = popW - TEXT_INPUT_PAD * 2 - 4;
for (int32_t i = 0; i < visibleItems && (scrollPos + i) < itemCount; i++) {
int32_t idx = scrollPos + i;
int32_t iy = textY + i * font->charHeight;
uint32_t ifg = colors->contentFg;
uint32_t ibg = colors->contentBg;
if (idx == hoverIdx || idx == selIdx) {
ifg = colors->menuHighlightFg;
ibg = colors->menuHighlightBg;
rectFill(d, ops, popX + 2, iy, textW + TEXT_INPUT_PAD * 2, font->charHeight, ibg);
}
drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, idx == hoverIdx || idx == selIdx);
}
}

258
dvx/widgets/widgetCore.c Normal file
View file

@ -0,0 +1,258 @@
// widgetCore.c — Core widget infrastructure (alloc, tree ops, helpers)
#include "widgetInternal.h"
// ============================================================
// Global state for drag and popup tracking
// ============================================================
WidgetT *sOpenPopup = NULL;
WidgetT *sDragSlider = NULL;
WidgetT *sDrawingCanvas = NULL;
int32_t sDragOffset = 0;
// ============================================================
// widgetAddChild
// ============================================================
void widgetAddChild(WidgetT *parent, WidgetT *child) {
child->parent = parent;
child->nextSibling = NULL;
if (parent->lastChild) {
parent->lastChild->nextSibling = child;
parent->lastChild = child;
} else {
parent->firstChild = child;
parent->lastChild = child;
}
}
// ============================================================
// widgetAlloc
// ============================================================
WidgetT *widgetAlloc(WidgetT *parent, WidgetTypeE type) {
WidgetT *w = (WidgetT *)malloc(sizeof(WidgetT));
if (!w) {
return NULL;
}
memset(w, 0, sizeof(*w));
w->type = type;
w->visible = true;
w->enabled = true;
if (parent) {
w->window = parent->window;
widgetAddChild(parent, w);
}
return w;
}
// ============================================================
// widgetCountVisibleChildren
// ============================================================
int32_t widgetCountVisibleChildren(const WidgetT *w) {
int32_t count = 0;
for (const WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (c->visible) {
count++;
}
}
return count;
}
// ============================================================
// widgetDestroyChildren
// ============================================================
void widgetDestroyChildren(WidgetT *w) {
WidgetT *child = w->firstChild;
while (child) {
WidgetT *next = child->nextSibling;
widgetDestroyChildren(child);
if (child->type == WidgetTextInputE) {
free(child->as.textInput.buf);
} else if (child->type == WidgetTextAreaE) {
free(child->as.textArea.buf);
} else if (child->type == WidgetComboBoxE) {
free(child->as.comboBox.buf);
} else if (child->type == WidgetImageE) {
free(child->as.image.data);
} else if (child->type == WidgetCanvasE) {
free(child->as.canvas.data);
}
// Clear popup/drag references if they point to destroyed widgets
if (sOpenPopup == child) {
sOpenPopup = NULL;
}
if (sDragSlider == child) {
sDragSlider = NULL;
}
if (sDrawingCanvas == child) {
sDrawingCanvas = NULL;
}
free(child);
child = next;
}
w->firstChild = NULL;
w->lastChild = NULL;
}
// ============================================================
// widgetDropdownPopupRect
// ============================================================
//
// Calculate the rectangle for a dropdown/combobox popup list.
void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t contentH,
int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH) {
int32_t itemCount = 0;
if (w->type == WidgetDropdownE) {
itemCount = w->as.dropdown.itemCount;
} else if (w->type == WidgetComboBoxE) {
itemCount = w->as.comboBox.itemCount;
}
int32_t visibleItems = itemCount;
if (visibleItems > DROPDOWN_MAX_VISIBLE) {
visibleItems = DROPDOWN_MAX_VISIBLE;
}
if (visibleItems < 1) {
visibleItems = 1;
}
*popX = w->x;
*popW = w->w;
*popH = visibleItems * font->charHeight + 4; // 2px border each side
// Try below first, then above if no room
if (w->y + w->h + *popH <= contentH) {
*popY = w->y + w->h;
} else {
*popY = w->y - *popH;
if (*popY < 0) {
*popY = 0;
}
}
}
// ============================================================
// widgetFrameBorderWidth
// ============================================================
int32_t widgetFrameBorderWidth(const WidgetT *w) {
if (w->type != WidgetFrameE) {
return 0;
}
if (w->as.frame.style == FrameFlatE) {
return FRAME_FLAT_BORDER;
}
return FRAME_BEVEL_BORDER;
}
// ============================================================
// widgetHitTest
// ============================================================
WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y) {
if (!w->visible) {
return NULL;
}
if (x < w->x || x >= w->x + w->w || y < w->y || y >= w->y + w->h) {
return NULL;
}
// Check children — take the last match (topmost in Z-order)
WidgetT *hit = NULL;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
WidgetT *childHit = widgetHitTest(c, x, y);
if (childHit) {
hit = childHit;
}
}
return hit ? hit : w;
}
// ============================================================
// widgetIsBoxContainer
// ============================================================
//
// Returns true for widget types that use the generic box layout.
bool widgetIsBoxContainer(WidgetTypeE type) {
return type == WidgetVBoxE || type == WidgetHBoxE || type == WidgetFrameE ||
type == WidgetRadioGroupE || type == WidgetTabPageE ||
type == WidgetStatusBarE || type == WidgetToolbarE;
}
// ============================================================
// widgetIsHorizContainer
// ============================================================
//
// Returns true for container types that lay out children horizontally.
bool widgetIsHorizContainer(WidgetTypeE type) {
return type == WidgetHBoxE || type == WidgetStatusBarE || type == WidgetToolbarE;
}
// ============================================================
// widgetRemoveChild
// ============================================================
void widgetRemoveChild(WidgetT *parent, WidgetT *child) {
WidgetT *prev = NULL;
for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) {
if (c == child) {
if (prev) {
prev->nextSibling = c->nextSibling;
} else {
parent->firstChild = c->nextSibling;
}
if (parent->lastChild == child) {
parent->lastChild = prev;
}
child->nextSibling = NULL;
child->parent = NULL;
return;
}
prev = c;
}
}

View file

@ -0,0 +1,188 @@
// widgetDropdown.c — Dropdown (select) widget
#include "widgetInternal.h"
// ============================================================
// wgtDropdown
// ============================================================
WidgetT *wgtDropdown(WidgetT *parent) {
WidgetT *w = widgetAlloc(parent, WidgetDropdownE);
if (w) {
w->as.dropdown.selectedIdx = -1;
w->as.dropdown.hoverIdx = -1;
}
return w;
}
// ============================================================
// wgtDropdownGetSelected
// ============================================================
int32_t wgtDropdownGetSelected(const WidgetT *w) {
if (!w || w->type != WidgetDropdownE) {
return -1;
}
return w->as.dropdown.selectedIdx;
}
// ============================================================
// wgtDropdownSetItems
// ============================================================
void wgtDropdownSetItems(WidgetT *w, const char **items, int32_t count) {
if (!w || w->type != WidgetDropdownE) {
return;
}
w->as.dropdown.items = items;
w->as.dropdown.itemCount = count;
if (w->as.dropdown.selectedIdx >= count) {
w->as.dropdown.selectedIdx = -1;
}
}
// ============================================================
// wgtDropdownSetSelected
// ============================================================
void wgtDropdownSetSelected(WidgetT *w, int32_t idx) {
if (!w || w->type != WidgetDropdownE) {
return;
}
w->as.dropdown.selectedIdx = idx;
}
// ============================================================
// widgetDropdownCalcMinSize
// ============================================================
void widgetDropdownCalcMinSize(WidgetT *w, const BitmapFontT *font) {
// Width: widest item + button width + border
int32_t maxItemW = font->charWidth * 8;
for (int32_t i = 0; i < w->as.dropdown.itemCount; i++) {
int32_t iw = (int32_t)strlen(w->as.dropdown.items[i]) * font->charWidth;
if (iw > maxItemW) {
maxItemW = iw;
}
}
w->calcMinW = maxItemW + DROPDOWN_BTN_WIDTH + TEXT_INPUT_PAD * 2 + 4;
w->calcMinH = font->charHeight + TEXT_INPUT_PAD * 2;
}
// ============================================================
// widgetDropdownOnMouse
// ============================================================
void widgetDropdownOnMouse(WidgetT *hit) {
hit->as.dropdown.open = !hit->as.dropdown.open;
hit->as.dropdown.hoverIdx = hit->as.dropdown.selectedIdx;
sOpenPopup = hit->as.dropdown.open ? hit : NULL;
}
// ============================================================
// widgetDropdownPaint
// ============================================================
void widgetDropdownPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops,
const BitmapFontT *font, const ColorSchemeT *colors) {
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
// Sunken text area
int32_t textAreaW = w->w - DROPDOWN_BTN_WIDTH;
BevelStyleT bevel;
bevel.highlight = colors->windowShadow;
bevel.shadow = colors->windowHighlight;
bevel.face = bg;
bevel.width = 2;
drawBevel(d, ops, w->x, w->y, textAreaW, w->h, &bevel);
// Draw selected item text
if (w->as.dropdown.selectedIdx >= 0 && w->as.dropdown.selectedIdx < w->as.dropdown.itemCount) {
drawText(d, ops, font, w->x + TEXT_INPUT_PAD,
w->y + (w->h - font->charHeight) / 2,
w->as.dropdown.items[w->as.dropdown.selectedIdx], fg, bg, true);
}
// Drop button
BevelStyleT btnBevel;
btnBevel.highlight = w->as.dropdown.open ? colors->windowShadow : colors->windowHighlight;
btnBevel.shadow = w->as.dropdown.open ? colors->windowHighlight : colors->windowShadow;
btnBevel.face = colors->buttonFace;
btnBevel.width = 2;
drawBevel(d, ops, w->x + textAreaW, w->y, DROPDOWN_BTN_WIDTH, w->h, &btnBevel);
// Down arrow in button
int32_t arrowX = w->x + textAreaW + DROPDOWN_BTN_WIDTH / 2;
int32_t arrowY = w->y + w->h / 2 - 1;
for (int32_t i = 0; i < 4; i++) {
drawHLine(d, ops, arrowX - 3 + i, arrowY + i, 7 - i * 2, colors->contentFg);
}
}
// ============================================================
// widgetDropdownPaintPopup
// ============================================================
void widgetDropdownPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops,
const BitmapFontT *font, const ColorSchemeT *colors) {
int32_t popX;
int32_t popY;
int32_t popW;
int32_t popH;
widgetDropdownPopupRect(w, font, d->clipH, &popX, &popY, &popW, &popH);
// Draw popup border
BevelStyleT bevel;
bevel.highlight = colors->windowHighlight;
bevel.shadow = colors->windowShadow;
bevel.face = colors->contentBg;
bevel.width = 2;
drawBevel(d, ops, popX, popY, popW, popH, &bevel);
// Draw items
int32_t itemCount = w->as.dropdown.itemCount;
const char **items = w->as.dropdown.items;
int32_t selIdx = w->as.dropdown.selectedIdx;
int32_t hoverIdx = w->as.dropdown.hoverIdx;
int32_t scrollPos = w->as.dropdown.scrollPos;
int32_t visibleItems = popH / font->charHeight;
int32_t textX = popX + TEXT_INPUT_PAD;
int32_t textY = popY + 2;
int32_t textW = popW - TEXT_INPUT_PAD * 2 - 4;
for (int32_t i = 0; i < visibleItems && (scrollPos + i) < itemCount; i++) {
int32_t idx = scrollPos + i;
int32_t iy = textY + i * font->charHeight;
uint32_t ifg = colors->contentFg;
uint32_t ibg = colors->contentBg;
if (idx == hoverIdx || idx == selIdx) {
ifg = colors->menuHighlightFg;
ibg = colors->menuHighlightBg;
rectFill(d, ops, popX + 2, iy, textW + TEXT_INPUT_PAD * 2, font->charHeight, ibg);
}
drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, idx == hoverIdx || idx == selIdx);
}
}

570
dvx/widgets/widgetEvent.c Normal file
View file

@ -0,0 +1,570 @@
// widgetEvent.c — Window event handlers for widget system
#include "widgetInternal.h"
// ============================================================
// widgetManageScrollbars
// ============================================================
//
// Checks whether the widget tree's minimum size exceeds the
// window content area. Adds or removes WM scrollbars as needed,
// then relayouts the widget tree at the virtual content size.
void widgetManageScrollbars(WindowT *win, AppContextT *ctx) {
WidgetT *root = win->widgetRoot;
if (!root) {
return;
}
// Measure the tree without any layout pass
widgetCalcMinSizeTree(root, &ctx->font);
// Save old scroll positions before removing scrollbars
int32_t oldVValue = win->vScroll ? win->vScroll->value : 0;
int32_t oldHValue = win->hScroll ? win->hScroll->value : 0;
bool hadVScroll = (win->vScroll != NULL);
bool hadHScroll = (win->hScroll != NULL);
// Remove existing scrollbars to measure full available area
if (hadVScroll) {
free(win->vScroll);
win->vScroll = NULL;
}
if (hadHScroll) {
free(win->hScroll);
win->hScroll = NULL;
}
wmUpdateContentRect(win);
int32_t availW = win->contentW;
int32_t availH = win->contentH;
int32_t minW = root->calcMinW;
int32_t minH = root->calcMinH;
bool needV = (minH > availH);
bool needH = (minW > availW);
// Adding one scrollbar reduces space, which may require the other
if (needV && !needH) {
needH = (minW > availW - SCROLLBAR_WIDTH);
}
if (needH && !needV) {
needV = (minH > availH - SCROLLBAR_WIDTH);
}
bool changed = (needV != hadVScroll) || (needH != hadHScroll);
if (needV) {
int32_t pageV = needH ? availH - SCROLLBAR_WIDTH : availH;
int32_t maxV = minH - pageV;
if (maxV < 0) {
maxV = 0;
}
wmAddVScrollbar(win, 0, maxV, pageV);
win->vScroll->value = DVX_MIN(oldVValue, maxV);
}
if (needH) {
int32_t pageH = needV ? availW - SCROLLBAR_WIDTH : availW;
int32_t maxH = minW - pageH;
if (maxH < 0) {
maxH = 0;
}
wmAddHScrollbar(win, 0, maxH, pageH);
win->hScroll->value = DVX_MIN(oldHValue, maxH);
}
if (changed) {
// wmAddVScrollbar/wmAddHScrollbar already call wmUpdateContentRect
wmReallocContentBuf(win, &ctx->display);
}
// Install scroll handler
win->onScroll = widgetOnScroll;
// Layout at the virtual content size (the larger of content area and min size)
int32_t layoutW = DVX_MAX(win->contentW, minW);
int32_t layoutH = DVX_MAX(win->contentH, minH);
wgtLayout(root, layoutW, layoutH, &ctx->font);
}
// ============================================================
// widgetOnKey
// ============================================================
void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
(void)mod;
WidgetT *root = win->widgetRoot;
if (!root) {
return;
}
// Find the focused widget
WidgetT *focus = NULL;
WidgetT *stack[64];
int32_t top = 0;
stack[top++] = root;
while (top > 0) {
WidgetT *w = stack[--top];
if (w->focused && (w->type == WidgetTextInputE || w->type == WidgetComboBoxE)) {
focus = w;
break;
}
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (top < 64) {
stack[top++] = c;
}
}
}
if (!focus) {
return;
}
// Handle text input for TextInput and ComboBox
char *buf = NULL;
int32_t bufSize = 0;
int32_t *pLen = NULL;
int32_t *pCursor = NULL;
int32_t *pScrollOff = NULL;
if (focus->type == WidgetTextInputE) {
buf = focus->as.textInput.buf;
bufSize = focus->as.textInput.bufSize;
pLen = &focus->as.textInput.len;
pCursor = &focus->as.textInput.cursorPos;
pScrollOff = &focus->as.textInput.scrollOff;
} else if (focus->type == WidgetComboBoxE) {
buf = focus->as.comboBox.buf;
bufSize = focus->as.comboBox.bufSize;
pLen = &focus->as.comboBox.len;
pCursor = &focus->as.comboBox.cursorPos;
pScrollOff = &focus->as.comboBox.scrollOff;
}
if (!buf) {
return;
}
if (key >= 32 && key < 127) {
// Printable character
if (*pLen < bufSize - 1) {
int32_t pos = *pCursor;
memmove(buf + pos + 1, buf + pos, *pLen - pos + 1);
buf[pos] = (char)key;
(*pLen)++;
(*pCursor)++;
if (focus->onChange) {
focus->onChange(focus);
}
}
} else if (key == 8) {
// Backspace
if (*pCursor > 0) {
int32_t pos = *pCursor;
memmove(buf + pos - 1, buf + pos, *pLen - pos + 1);
(*pLen)--;
(*pCursor)--;
if (focus->onChange) {
focus->onChange(focus);
}
}
} else if (key == (0x4B | 0x100)) {
// Left arrow
if (*pCursor > 0) {
(*pCursor)--;
}
} else if (key == (0x4D | 0x100)) {
// Right arrow
if (*pCursor < *pLen) {
(*pCursor)++;
}
} else if (key == (0x47 | 0x100)) {
// Home
*pCursor = 0;
} else if (key == (0x4F | 0x100)) {
// End
*pCursor = *pLen;
} else if (key == (0x53 | 0x100)) {
// Delete
if (*pCursor < *pLen) {
int32_t pos = *pCursor;
memmove(buf + pos, buf + pos + 1, *pLen - pos);
(*pLen)--;
if (focus->onChange) {
focus->onChange(focus);
}
}
}
// Adjust scroll offset to keep cursor visible
AppContextT *ctx = (AppContextT *)root->userData;
const BitmapFontT *font = &ctx->font;
int32_t fieldW = focus->w;
if (focus->type == WidgetComboBoxE) {
fieldW -= DROPDOWN_BTN_WIDTH;
}
int32_t visibleChars = (fieldW - TEXT_INPUT_PAD * 2) / font->charWidth;
if (*pCursor < *pScrollOff) {
*pScrollOff = *pCursor;
}
if (*pCursor >= *pScrollOff + visibleChars) {
*pScrollOff = *pCursor - visibleChars + 1;
}
// Repaint the window
wgtInvalidate(focus);
}
// ============================================================
// widgetOnMouse
// ============================================================
void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
WidgetT *root = win->widgetRoot;
if (!root) {
return;
}
// Close popups from other windows
if (sOpenPopup && sOpenPopup->window != win) {
if (sOpenPopup->type == WidgetDropdownE) {
sOpenPopup->as.dropdown.open = false;
} else if (sOpenPopup->type == WidgetComboBoxE) {
sOpenPopup->as.comboBox.open = false;
}
sOpenPopup = NULL;
}
// Handle canvas drawing release
if (sDrawingCanvas && !(buttons & 1)) {
sDrawingCanvas->as.canvas.lastX = -1;
sDrawingCanvas->as.canvas.lastY = -1;
sDrawingCanvas = NULL;
wgtInvalidate(root);
return;
}
// Handle canvas drawing (mouse move while pressed)
if (sDrawingCanvas && (buttons & 1)) {
widgetCanvasOnMouse(sDrawingCanvas, x, y);
wgtInvalidate(root);
return;
}
// Handle slider drag release
if (sDragSlider && !(buttons & 1)) {
sDragSlider = NULL;
wgtInvalidate(root);
return;
}
// Handle slider drag (mouse move while pressed)
if (sDragSlider && (buttons & 1)) {
int32_t range = sDragSlider->as.slider.maxValue - sDragSlider->as.slider.minValue;
if (range > 0) {
int32_t newVal;
if (sDragSlider->as.slider.vertical) {
int32_t thumbRange = sDragSlider->h - SLIDER_THUMB_W;
int32_t relY = y - sDragSlider->y - sDragOffset;
newVal = sDragSlider->as.slider.minValue + (relY * range) / thumbRange;
} else {
int32_t thumbRange = sDragSlider->w - SLIDER_THUMB_W;
int32_t relX = x - sDragSlider->x - sDragOffset;
newVal = sDragSlider->as.slider.minValue + (relX * range) / thumbRange;
}
if (newVal < sDragSlider->as.slider.minValue) {
newVal = sDragSlider->as.slider.minValue;
}
if (newVal > sDragSlider->as.slider.maxValue) {
newVal = sDragSlider->as.slider.maxValue;
}
if (newVal != sDragSlider->as.slider.value) {
sDragSlider->as.slider.value = newVal;
if (sDragSlider->onChange) {
sDragSlider->onChange(sDragSlider);
}
wgtInvalidate(root);
}
}
return;
}
// Handle open popup clicks
if (sOpenPopup && (buttons & 1)) {
AppContextT *ctx = (AppContextT *)root->userData;
const BitmapFontT *font = &ctx->font;
int32_t popX;
int32_t popY;
int32_t popW;
int32_t popH;
widgetDropdownPopupRect(sOpenPopup, font, win->contentH, &popX, &popY, &popW, &popH);
if (x >= popX && x < popX + popW && y >= popY && y < popY + popH) {
// Click on popup item
int32_t itemIdx = (y - popY - 2) / font->charHeight;
int32_t scrollP = 0;
if (sOpenPopup->type == WidgetDropdownE) {
scrollP = sOpenPopup->as.dropdown.scrollPos;
} else {
scrollP = sOpenPopup->as.comboBox.listScrollPos;
}
itemIdx += scrollP;
if (sOpenPopup->type == WidgetDropdownE) {
if (itemIdx >= 0 && itemIdx < sOpenPopup->as.dropdown.itemCount) {
sOpenPopup->as.dropdown.selectedIdx = itemIdx;
sOpenPopup->as.dropdown.open = false;
if (sOpenPopup->onChange) {
sOpenPopup->onChange(sOpenPopup);
}
}
} else if (sOpenPopup->type == WidgetComboBoxE) {
if (itemIdx >= 0 && itemIdx < sOpenPopup->as.comboBox.itemCount) {
sOpenPopup->as.comboBox.selectedIdx = itemIdx;
sOpenPopup->as.comboBox.open = false;
// Copy selected item text to buffer
const char *itemText = sOpenPopup->as.comboBox.items[itemIdx];
strncpy(sOpenPopup->as.comboBox.buf, itemText, sOpenPopup->as.comboBox.bufSize - 1);
sOpenPopup->as.comboBox.buf[sOpenPopup->as.comboBox.bufSize - 1] = '\0';
sOpenPopup->as.comboBox.len = (int32_t)strlen(sOpenPopup->as.comboBox.buf);
sOpenPopup->as.comboBox.cursorPos = sOpenPopup->as.comboBox.len;
sOpenPopup->as.comboBox.scrollOff = 0;
if (sOpenPopup->onChange) {
sOpenPopup->onChange(sOpenPopup);
}
}
}
sOpenPopup = NULL;
wgtInvalidate(root);
return;
}
// Click outside popup — close it
if (sOpenPopup->type == WidgetDropdownE) {
sOpenPopup->as.dropdown.open = false;
} else if (sOpenPopup->type == WidgetComboBoxE) {
sOpenPopup->as.comboBox.open = false;
}
sOpenPopup = NULL;
wgtInvalidate(root);
// Fall through to normal click handling
}
if (!(buttons & 1)) {
return;
}
// Adjust mouse coordinates for scroll offset
int32_t scrollX = win->hScroll ? win->hScroll->value : 0;
int32_t scrollY = win->vScroll ? win->vScroll->value : 0;
int32_t vx = x + scrollX;
int32_t vy = y + scrollY;
WidgetT *hit = widgetHitTest(root, vx, vy);
if (!hit) {
return;
}
// Clear focus from all widgets, set focus on clicked widget
WidgetT *fstack[64];
int32_t ftop = 0;
fstack[ftop++] = root;
while (ftop > 0) {
WidgetT *w = fstack[--ftop];
w->focused = false;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (ftop < 64) {
fstack[ftop++] = c;
}
}
}
// Dispatch to per-widget mouse handlers
if (hit->type == WidgetTextInputE) {
widgetTextInputOnMouse(hit, root, vx);
}
if (hit->type == WidgetButtonE && hit->enabled) {
widgetButtonOnMouse(hit);
}
if (hit->type == WidgetCheckboxE && hit->enabled) {
widgetCheckboxOnMouse(hit);
}
if (hit->type == WidgetRadioE && hit->enabled) {
widgetRadioOnMouse(hit);
}
if (hit->type == WidgetImageE && hit->enabled) {
widgetImageOnMouse(hit);
}
if (hit->type == WidgetCanvasE && hit->enabled) {
widgetCanvasOnMouse(hit, vx, vy);
}
if (hit->type == WidgetDropdownE && hit->enabled) {
widgetDropdownOnMouse(hit);
}
if (hit->type == WidgetComboBoxE && hit->enabled) {
widgetComboBoxOnMouse(hit, root, vx);
}
if (hit->type == WidgetSliderE && hit->enabled) {
widgetSliderOnMouse(hit, vx, vy);
}
if (hit->type == WidgetTabControlE && hit->enabled) {
widgetTabControlOnMouse(hit, root, vx, vy);
}
if (hit->type == WidgetTreeViewE && hit->enabled) {
widgetTreeViewOnMouse(hit, root, vx, vy);
}
wgtInvalidate(root);
}
// ============================================================
// widgetOnPaint
// ============================================================
void widgetOnPaint(WindowT *win, RectT *dirtyArea) {
(void)dirtyArea;
WidgetT *root = win->widgetRoot;
if (!root) {
return;
}
// Get context from root's userData
AppContextT *ctx = (AppContextT *)root->userData;
if (!ctx) {
return;
}
// Set up a display context pointing at the content buffer
DisplayT cd = ctx->display;
cd.backBuf = win->contentBuf;
cd.width = win->contentW;
cd.height = win->contentH;
cd.pitch = win->contentPitch;
cd.clipX = 0;
cd.clipY = 0;
cd.clipW = win->contentW;
cd.clipH = win->contentH;
// Clear background
rectFill(&cd, &ctx->blitOps, 0, 0, win->contentW, win->contentH, ctx->colors.contentBg);
// Apply scroll offset — layout at virtual size, positioned at -scroll
int32_t scrollX = win->hScroll ? win->hScroll->value : 0;
int32_t scrollY = win->vScroll ? win->vScroll->value : 0;
int32_t layoutW = DVX_MAX(win->contentW, root->calcMinW);
int32_t layoutH = DVX_MAX(win->contentH, root->calcMinH);
root->x = -scrollX;
root->y = -scrollY;
root->w = layoutW;
root->h = layoutH;
widgetLayoutChildren(root, &ctx->font);
// Paint widget tree (clip rect limits drawing to visible area)
wgtPaint(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors);
// Paint overlay popups (dropdown/combobox)
widgetPaintOverlays(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors);
}
// ============================================================
// widgetOnResize
// ============================================================
void widgetOnResize(WindowT *win, int32_t newW, int32_t newH) {
(void)newW;
(void)newH;
WidgetT *root = win->widgetRoot;
if (!root) {
return;
}
AppContextT *ctx = (AppContextT *)root->userData;
if (!ctx) {
return;
}
widgetManageScrollbars(win, ctx);
}
// ============================================================
// widgetOnScroll
// ============================================================
void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value) {
(void)orient;
(void)value;
// Repaint with new scroll position
if (win->onPaint) {
RectT fullRect = {0, 0, win->contentW, win->contentH};
win->onPaint(win, &fullRect);
}
}

170
dvx/widgets/widgetImage.c Normal file
View file

@ -0,0 +1,170 @@
// widgetImage.c — Image widget (displays bitmap, responds to clicks)
#include "widgetInternal.h"
#include "../thirdparty/stb_image.h"
// ============================================================
// wgtImage
// ============================================================
//
// Create an image widget from raw pixel data already in display format.
// Takes ownership of the data buffer (freed on destroy).
WidgetT *wgtImage(WidgetT *parent, uint8_t *data, int32_t w, int32_t h, int32_t pitch) {
WidgetT *wgt = widgetAlloc(parent, WidgetImageE);
if (wgt) {
wgt->as.image.data = data;
wgt->as.image.imgW = w;
wgt->as.image.imgH = h;
wgt->as.image.imgPitch = pitch;
wgt->as.image.pressed = false;
}
return wgt;
}
// ============================================================
// wgtImageFromFile
// ============================================================
//
// Load an image from a file (BMP, PNG, JPEG, GIF), convert to
// display pixel format, and create an image widget.
WidgetT *wgtImageFromFile(WidgetT *parent, const char *path) {
if (!parent || !path) {
return NULL;
}
// Find the AppContextT to get display format
WidgetT *root = parent;
while (root->parent) {
root = root->parent;
}
AppContextT *ctx = (AppContextT *)root->userData;
if (!ctx) {
return NULL;
}
const DisplayT *d = &ctx->display;
// Load image via stb_image (force RGB)
int imgW;
int imgH;
int channels;
uint8_t *rgb = stbi_load(path, &imgW, &imgH, &channels, 3);
if (!rgb) {
return NULL;
}
// Convert RGB to display pixel format
int32_t bpp = d->format.bytesPerPixel;
int32_t pitch = imgW * bpp;
uint8_t *buf = (uint8_t *)malloc(pitch * imgH);
if (!buf) {
stbi_image_free(rgb);
return NULL;
}
for (int32_t y = 0; y < imgH; y++) {
for (int32_t x = 0; x < imgW; x++) {
const uint8_t *src = rgb + (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(rgb);
return wgtImage(parent, buf, imgW, imgH, pitch);
}
// ============================================================
// wgtImageSetData
// ============================================================
void wgtImageSetData(WidgetT *w, uint8_t *data, int32_t imgW, int32_t imgH, int32_t pitch) {
if (!w || w->type != WidgetImageE) {
return;
}
free(w->as.image.data);
w->as.image.data = data;
w->as.image.imgW = imgW;
w->as.image.imgH = imgH;
w->as.image.imgPitch = pitch;
}
// ============================================================
// widgetImageCalcMinSize
// ============================================================
void widgetImageCalcMinSize(WidgetT *w, const BitmapFontT *font) {
(void)font;
w->calcMinW = w->as.image.imgW;
w->calcMinH = w->as.image.imgH;
}
// ============================================================
// widgetImageOnMouse
// ============================================================
void widgetImageOnMouse(WidgetT *hit) {
hit->as.image.pressed = true;
wgtInvalidate(hit);
if (hit->onClick) {
hit->onClick(hit);
}
hit->as.image.pressed = false;
}
// ============================================================
// widgetImagePaint
// ============================================================
void widgetImagePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops,
const BitmapFontT *font, const ColorSchemeT *colors) {
(void)font;
(void)colors;
if (!w->as.image.data) {
return;
}
// Center the image within the widget bounds
int32_t imgW = w->as.image.imgW;
int32_t imgH = w->as.image.imgH;
int32_t dx = w->x + (w->w - imgW) / 2;
int32_t dy = w->y + (w->h - imgH) / 2;
// Offset by 1px when pressed (button-press effect)
if (w->as.image.pressed) {
dx++;
dy++;
}
rectCopy(d, ops, dx, dy,
w->as.image.data, w->as.image.imgPitch,
0, 0, imgW, imgH);
}

View file

@ -0,0 +1,158 @@
// widgetInternal.h — Shared internal header for widget implementation files
#ifndef WIDGET_INTERNAL_H
#define WIDGET_INTERNAL_H
#include "../dvxWidget.h"
#include "../dvxApp.h"
#include "../dvxDraw.h"
#include "../dvxWm.h"
#include "../dvxVideo.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
// ============================================================
// Constants
// ============================================================
#define DEFAULT_SPACING 4
#define DEFAULT_PADDING 4
#define SEPARATOR_THICKNESS 2
#define BUTTON_PAD_H 8
#define BUTTON_PAD_V 4
#define CHECKBOX_BOX_SIZE 12
#define CHECKBOX_GAP 4
#define FRAME_BEVEL_BORDER 2
#define FRAME_FLAT_BORDER 1
#define TEXT_INPUT_PAD 3
#define DROPDOWN_BTN_WIDTH 16
#define DROPDOWN_MAX_VISIBLE 8
#define SLIDER_TRACK_H 4
#define SLIDER_THUMB_W 11
#define TAB_PAD_H 8
#define TAB_PAD_V 4
#define TAB_BORDER 2
#define TREE_INDENT 16
#define TREE_EXPAND_SIZE 9
#define TREE_ICON_GAP 4
#define TREE_BORDER 2
// ============================================================
// Shared state (defined in widgetCore.c)
// ============================================================
extern WidgetT *sOpenPopup;
extern WidgetT *sDragSlider;
extern WidgetT *sDrawingCanvas;
extern int32_t sDragOffset;
// ============================================================
// Core functions (widgetCore.c)
// ============================================================
void widgetAddChild(WidgetT *parent, WidgetT *child);
WidgetT *widgetAlloc(WidgetT *parent, WidgetTypeE type);
int32_t widgetCountVisibleChildren(const WidgetT *w);
void widgetDestroyChildren(WidgetT *w);
void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH);
int32_t widgetFrameBorderWidth(const WidgetT *w);
WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y);
bool widgetIsBoxContainer(WidgetTypeE type);
bool widgetIsHorizContainer(WidgetTypeE type);
void widgetRemoveChild(WidgetT *parent, WidgetT *child);
// ============================================================
// Layout functions (widgetLayout.c)
// ============================================================
void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font);
void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font);
void widgetLayoutBox(WidgetT *w, const BitmapFontT *font);
void widgetLayoutChildren(WidgetT *w, const BitmapFontT *font);
// ============================================================
// Event functions (widgetEvent.c)
// ============================================================
void widgetManageScrollbars(WindowT *win, AppContextT *ctx);
void widgetOnKey(WindowT *win, int32_t key, int32_t mod);
void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons);
void widgetOnPaint(WindowT *win, RectT *dirtyArea);
void widgetOnResize(WindowT *win, int32_t newW, int32_t newH);
void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value);
// ============================================================
// Paint/ops functions (widgetOps.c)
// ============================================================
void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetPaintOverlays(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
// ============================================================
// Per-widget paint functions
// ============================================================
void widgetButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetCanvasPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetComboBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetComboBoxPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetDropdownPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetDropdownPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetImagePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetLabelPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetProgressBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetSeparatorPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetStatusBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetToolbarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetTreeViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
// ============================================================
// Per-widget calcMinSize functions
// ============================================================
void widgetButtonCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetCanvasCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetCheckboxCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetComboBoxCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetDropdownCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetImageCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetLabelCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetProgressBarCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetSliderCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetSpacerCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetTabControlCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetTextInputCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetTreeViewCalcMinSize(WidgetT *w, const BitmapFontT *font);
// ============================================================
// Per-widget layout functions (for special containers)
// ============================================================
void widgetTabControlLayout(WidgetT *w, const BitmapFontT *font);
void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font);
// ============================================================
// Per-widget mouse functions
// ============================================================
void widgetButtonOnMouse(WidgetT *hit);
void widgetCanvasOnMouse(WidgetT *hit, int32_t vx, int32_t vy);
void widgetCheckboxOnMouse(WidgetT *hit);
void widgetComboBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx);
void widgetDropdownOnMouse(WidgetT *hit);
void widgetImageOnMouse(WidgetT *hit);
void widgetRadioOnMouse(WidgetT *hit);
void widgetSliderOnMouse(WidgetT *hit, int32_t vx, int32_t vy);
void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy);
void widgetTextInputOnMouse(WidgetT *hit, WidgetT *root, int32_t vx);
void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy);
#endif // WIDGET_INTERNAL_H

42
dvx/widgets/widgetLabel.c Normal file
View file

@ -0,0 +1,42 @@
// widgetLabel.c — Label widget
#include "widgetInternal.h"
// ============================================================
// wgtLabel
// ============================================================
WidgetT *wgtLabel(WidgetT *parent, const char *text) {
WidgetT *w = widgetAlloc(parent, WidgetLabelE);
if (w) {
w->as.label.text = text;
}
return w;
}
// ============================================================
// widgetLabelCalcMinSize
// ============================================================
void widgetLabelCalcMinSize(WidgetT *w, const BitmapFontT *font) {
w->calcMinW = (int32_t)strlen(w->as.label.text) * font->charWidth;
w->calcMinH = font->charHeight;
}
// ============================================================
// widgetLabelPaint
// ============================================================
void widgetLabelPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops,
const BitmapFontT *font, const ColorSchemeT *colors) {
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
drawText(d, ops, font, w->x, w->y + (w->h - font->charHeight) / 2,
w->as.label.text, fg, bg, false);
}

389
dvx/widgets/widgetLayout.c Normal file
View file

@ -0,0 +1,389 @@
// widgetLayout.c — Layout engine (measure + arrange)
#include "widgetInternal.h"
// ============================================================
// widgetCalcMinSizeBox
// ============================================================
void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font) {
bool horiz = widgetIsHorizContainer(w->type);
int32_t pad = wgtResolveSize(w->padding, 0, font->charWidth);
int32_t gap = wgtResolveSize(w->spacing, 0, font->charWidth);
int32_t mainSize = 0;
int32_t crossSize = 0;
int32_t count = 0;
if (pad == 0) {
pad = DEFAULT_PADDING;
}
if (gap == 0) {
gap = DEFAULT_SPACING;
}
// Frame: box starts at y + charHeight/2 so the title sits on the top line
int32_t frameExtraTop = 0;
if (w->type == WidgetFrameE) {
frameExtraTop = font->charHeight / 2;
pad = DEFAULT_PADDING;
}
// Toolbar and StatusBar use tighter padding
if (w->type == WidgetToolbarE || w->type == WidgetStatusBarE) {
pad = 2;
gap = 2;
}
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (!c->visible) {
continue;
}
widgetCalcMinSizeTree(c, font);
if (horiz) {
mainSize += c->calcMinW;
crossSize = DVX_MAX(crossSize, c->calcMinH);
} else {
mainSize += c->calcMinH;
crossSize = DVX_MAX(crossSize, c->calcMinW);
}
count++;
}
// Add spacing between children
if (count > 1) {
mainSize += gap * (count - 1);
}
// Add padding
mainSize += pad * 2;
crossSize += pad * 2;
if (horiz) {
w->calcMinW = mainSize;
w->calcMinH = crossSize + frameExtraTop;
} else {
w->calcMinW = crossSize;
w->calcMinH = mainSize + frameExtraTop;
}
// Frame border
if (w->type == WidgetFrameE) {
int32_t fb = widgetFrameBorderWidth(w);
w->calcMinW += fb * 2;
w->calcMinH += fb * 2;
}
}
// ============================================================
// widgetCalcMinSizeTree
// ============================================================
//
// Top-level measure dispatcher. Recurses through the widget tree.
void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font) {
if (widgetIsBoxContainer(w->type)) {
widgetCalcMinSizeBox(w, font);
} else if (w->type == WidgetTabControlE) {
widgetTabControlCalcMinSize(w, font);
} else if (w->type == WidgetTreeViewE) {
widgetTreeViewCalcMinSize(w, font);
} else {
// Leaf widgets
switch (w->type) {
case WidgetLabelE:
widgetLabelCalcMinSize(w, font);
break;
case WidgetButtonE:
widgetButtonCalcMinSize(w, font);
break;
case WidgetCheckboxE:
widgetCheckboxCalcMinSize(w, font);
break;
case WidgetRadioE:
widgetRadioCalcMinSize(w, font);
break;
case WidgetTextInputE:
widgetTextInputCalcMinSize(w, font);
break;
case WidgetSpacerE:
widgetSpacerCalcMinSize(w, font);
break;
case WidgetDropdownE:
widgetDropdownCalcMinSize(w, font);
break;
case WidgetImageE:
widgetImageCalcMinSize(w, font);
break;
case WidgetCanvasE:
widgetCanvasCalcMinSize(w, font);
break;
case WidgetComboBoxE:
widgetComboBoxCalcMinSize(w, font);
break;
case WidgetProgressBarE:
widgetProgressBarCalcMinSize(w, font);
break;
case WidgetSliderE:
widgetSliderCalcMinSize(w, font);
break;
case WidgetSeparatorE:
if (w->as.separator.vertical) {
w->calcMinW = SEPARATOR_THICKNESS;
w->calcMinH = 0;
} else {
w->calcMinW = 0;
w->calcMinH = SEPARATOR_THICKNESS;
}
break;
case WidgetTreeItemE:
w->calcMinW = 0;
w->calcMinH = 0;
break;
default:
w->calcMinW = 0;
w->calcMinH = 0;
break;
}
}
// Apply size hints (override calculated minimum)
if (w->minW) {
int32_t hintW = wgtResolveSize(w->minW, 0, font->charWidth);
if (hintW > w->calcMinW) {
w->calcMinW = hintW;
}
}
if (w->minH) {
int32_t hintH = wgtResolveSize(w->minH, 0, font->charWidth);
if (hintH > w->calcMinH) {
w->calcMinH = hintH;
}
}
}
// ============================================================
// widgetLayoutBox
// ============================================================
void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) {
bool horiz = widgetIsHorizContainer(w->type);
int32_t pad = wgtResolveSize(w->padding, 0, font->charWidth);
int32_t gap = wgtResolveSize(w->spacing, 0, font->charWidth);
if (pad == 0) {
pad = DEFAULT_PADDING;
}
if (gap == 0) {
gap = DEFAULT_SPACING;
}
// Frame adjustments
int32_t frameExtraTop = 0;
int32_t fb = 0;
if (w->type == WidgetFrameE) {
frameExtraTop = font->charHeight / 2;
fb = widgetFrameBorderWidth(w);
pad = DEFAULT_PADDING;
}
// Toolbar and StatusBar use tighter padding
if (w->type == WidgetToolbarE || w->type == WidgetStatusBarE) {
pad = 2;
gap = 2;
}
int32_t innerX = w->x + pad + fb;
int32_t innerY = w->y + pad + fb + frameExtraTop;
int32_t innerW = w->w - pad * 2 - fb * 2;
int32_t innerH = w->h - pad * 2 - fb * 2 - frameExtraTop;
if (innerW < 0) { innerW = 0; }
if (innerH < 0) { innerH = 0; }
int32_t count = widgetCountVisibleChildren(w);
if (count == 0) {
return;
}
int32_t totalGap = gap * (count - 1);
int32_t availMain = horiz ? (innerW - totalGap) : (innerH - totalGap);
int32_t availCross = horiz ? innerH : innerW;
// First pass: sum minimum sizes and total weight
int32_t totalMin = 0;
int32_t totalWeight = 0;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (!c->visible) {
continue;
}
int32_t cmin = horiz ? c->calcMinW : c->calcMinH;
totalMin += cmin;
totalWeight += c->weight;
}
int32_t extraSpace = availMain - totalMin;
if (extraSpace < 0) {
extraSpace = 0;
}
// Compute alignment offset for main axis
int32_t alignOffset = 0;
if (totalWeight == 0 && extraSpace > 0) {
if (w->align == AlignCenterE) {
alignOffset = extraSpace / 2;
} else if (w->align == AlignEndE) {
alignOffset = extraSpace;
}
}
// Second pass: assign positions and sizes
int32_t pos = (horiz ? innerX : innerY) + alignOffset;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (!c->visible) {
continue;
}
int32_t cmin = horiz ? c->calcMinW : c->calcMinH;
int32_t mainSize = cmin;
// Distribute extra space by weight
if (totalWeight > 0 && c->weight > 0 && extraSpace > 0) {
mainSize += (extraSpace * c->weight) / totalWeight;
}
// Apply max size constraint
if (horiz && c->maxW) {
int32_t maxPx = wgtResolveSize(c->maxW, innerW, font->charWidth);
if (mainSize > maxPx) {
mainSize = maxPx;
}
} else if (!horiz && c->maxH) {
int32_t maxPx = wgtResolveSize(c->maxH, innerH, font->charWidth);
if (mainSize > maxPx) {
mainSize = maxPx;
}
}
// Assign geometry
if (horiz) {
c->x = pos;
c->y = innerY;
c->w = mainSize;
c->h = availCross;
} else {
c->x = innerX;
c->y = pos;
c->w = availCross;
c->h = mainSize;
}
// Apply preferred/max on cross axis
if (horiz && c->maxH) {
int32_t maxPx = wgtResolveSize(c->maxH, innerH, font->charWidth);
if (c->h > maxPx) {
c->h = maxPx;
}
} else if (!horiz && c->maxW) {
int32_t maxPx = wgtResolveSize(c->maxW, innerW, font->charWidth);
if (c->w > maxPx) {
c->w = maxPx;
}
}
pos += mainSize + gap;
// Recurse into child containers
widgetLayoutChildren(c, font);
}
}
// ============================================================
// widgetLayoutChildren
// ============================================================
//
// Top-level layout dispatcher.
void widgetLayoutChildren(WidgetT *w, const BitmapFontT *font) {
if (widgetIsBoxContainer(w->type)) {
widgetLayoutBox(w, font);
} else if (w->type == WidgetTabControlE) {
widgetTabControlLayout(w, font);
} else if (w->type == WidgetTreeViewE) {
widgetTreeViewLayout(w, font);
}
}
// ============================================================
// wgtLayout
// ============================================================
void wgtLayout(WidgetT *root, int32_t availW, int32_t availH,
const BitmapFontT *font) {
if (!root) {
return;
}
// Measure pass
widgetCalcMinSizeTree(root, font);
// Layout pass
root->x = 0;
root->y = 0;
root->w = availW;
root->h = availH;
widgetLayoutChildren(root, font);
}
// ============================================================
// wgtResolveSize
// ============================================================
int32_t wgtResolveSize(int32_t taggedSize, int32_t parentSize, int32_t charWidth) {
if (taggedSize == 0) {
return 0;
}
uint32_t sizeType = (uint32_t)taggedSize & WGT_SIZE_TYPE_MASK;
int32_t value = taggedSize & WGT_SIZE_VAL_MASK;
switch (sizeType) {
case WGT_SIZE_PIXELS:
return value;
case WGT_SIZE_CHARS:
return value * charWidth;
case WGT_SIZE_PERCENT:
return (parentSize * value) / 100;
default:
return value;
}
}

View file

@ -0,0 +1,62 @@
// widgetListBox.c — ListBox widget
#include "widgetInternal.h"
// ============================================================
// wgtListBox
// ============================================================
WidgetT *wgtListBox(WidgetT *parent) {
WidgetT *w = widgetAlloc(parent, WidgetListBoxE);
if (w) {
w->as.listBox.selectedIdx = -1;
}
return w;
}
// ============================================================
// wgtListBoxGetSelected
// ============================================================
int32_t wgtListBoxGetSelected(const WidgetT *w) {
if (!w || w->type != WidgetListBoxE) {
return -1;
}
return w->as.listBox.selectedIdx;
}
// ============================================================
// wgtListBoxSetItems
// ============================================================
void wgtListBoxSetItems(WidgetT *w, const char **items, int32_t count) {
if (!w || w->type != WidgetListBoxE) {
return;
}
w->as.listBox.items = items;
w->as.listBox.itemCount = count;
if (w->as.listBox.selectedIdx >= count) {
w->as.listBox.selectedIdx = -1;
}
}
// ============================================================
// wgtListBoxSetSelected
// ============================================================
void wgtListBoxSetSelected(WidgetT *w, int32_t idx) {
if (!w || w->type != WidgetListBoxE) {
return;
}
w->as.listBox.selectedIdx = idx;
}

388
dvx/widgets/widgetOps.c Normal file
View file

@ -0,0 +1,388 @@
// widgetOps.c — Paint dispatcher and public widget operations
#include "widgetInternal.h"
// ============================================================
// widgetPaintOne
// ============================================================
//
// Paint a single widget and its children. Dispatches to per-widget
// paint functions defined in their respective files.
void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops,
const BitmapFontT *font, const ColorSchemeT *colors) {
if (!w->visible) {
return;
}
switch (w->type) {
case WidgetVBoxE:
case WidgetHBoxE:
// Containers are transparent — just paint children
break;
case WidgetFrameE:
widgetFramePaint(w, d, ops, font, colors);
break;
case WidgetImageE:
widgetImagePaint(w, d, ops, font, colors);
break;
case WidgetCanvasE:
widgetCanvasPaint(w, d, ops, font, colors);
break;
case WidgetLabelE:
widgetLabelPaint(w, d, ops, font, colors);
break;
case WidgetButtonE:
widgetButtonPaint(w, d, ops, font, colors);
break;
case WidgetCheckboxE:
widgetCheckboxPaint(w, d, ops, font, colors);
break;
case WidgetRadioE:
widgetRadioPaint(w, d, ops, font, colors);
break;
case WidgetTextInputE:
widgetTextInputPaint(w, d, ops, font, colors);
break;
case WidgetSpacerE:
// Invisible — draws nothing
break;
case WidgetSeparatorE:
widgetSeparatorPaint(w, d, ops, font, colors);
break;
case WidgetDropdownE:
widgetDropdownPaint(w, d, ops, font, colors);
break;
case WidgetComboBoxE:
widgetComboBoxPaint(w, d, ops, font, colors);
break;
case WidgetProgressBarE:
widgetProgressBarPaint(w, d, ops, font, colors);
break;
case WidgetSliderE:
widgetSliderPaint(w, d, ops, font, colors);
break;
case WidgetTabControlE:
widgetTabControlPaint(w, d, ops, font, colors);
return; // handles its own children
case WidgetStatusBarE:
widgetStatusBarPaint(w, d, ops, font, colors);
break;
case WidgetToolbarE:
widgetToolbarPaint(w, d, ops, font, colors);
break;
case WidgetTreeViewE:
widgetTreeViewPaint(w, d, ops, font, colors);
return; // handles its own children
default:
break;
}
// Paint children (TabControl and TreeView return early above)
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
widgetPaintOne(c, d, ops, font, colors);
}
}
// ============================================================
// widgetPaintOverlays
// ============================================================
//
// Paints popup overlays (open dropdowns/comboboxes) on top of
// the widget tree. Called after the main paint pass.
void widgetPaintOverlays(WidgetT *root, DisplayT *d, const BlitOpsT *ops,
const BitmapFontT *font, const ColorSchemeT *colors) {
if (!sOpenPopup) {
return;
}
// Verify the popup belongs to this widget tree
WidgetT *check = sOpenPopup;
while (check->parent) {
check = check->parent;
}
if (check != root) {
return;
}
if (sOpenPopup->type == WidgetDropdownE) {
widgetDropdownPaintPopup(sOpenPopup, d, ops, font, colors);
} else if (sOpenPopup->type == WidgetComboBoxE) {
widgetComboBoxPaintPopup(sOpenPopup, d, ops, font, colors);
}
}
// ============================================================
// wgtDestroy
// ============================================================
void wgtDestroy(WidgetT *w) {
if (!w) {
return;
}
if (w->parent) {
widgetRemoveChild(w->parent, w);
}
widgetDestroyChildren(w);
if (w->type == WidgetTextInputE) {
free(w->as.textInput.buf);
} else if (w->type == WidgetTextAreaE) {
free(w->as.textArea.buf);
} else if (w->type == WidgetComboBoxE) {
free(w->as.comboBox.buf);
} else if (w->type == WidgetImageE) {
free(w->as.image.data);
} else if (w->type == WidgetCanvasE) {
free(w->as.canvas.data);
}
// Clear static references
if (sOpenPopup == w) {
sOpenPopup = NULL;
}
if (sDragSlider == w) {
sDragSlider = NULL;
}
if (sDrawingCanvas == w) {
sDrawingCanvas = NULL;
}
// If this is the root, clear the window's reference
if (w->window && w->window->widgetRoot == w) {
w->window->widgetRoot = NULL;
}
free(w);
}
// ============================================================
// wgtFind
// ============================================================
WidgetT *wgtFind(WidgetT *root, const char *name) {
if (!root || !name) {
return NULL;
}
if (root->name[0] && strcmp(root->name, name) == 0) {
return root;
}
for (WidgetT *c = root->firstChild; c; c = c->nextSibling) {
WidgetT *found = wgtFind(c, name);
if (found) {
return found;
}
}
return NULL;
}
// ============================================================
// wgtGetText
// ============================================================
const char *wgtGetText(const WidgetT *w) {
if (!w) {
return "";
}
switch (w->type) {
case WidgetLabelE: return w->as.label.text;
case WidgetButtonE: return w->as.button.text;
case WidgetCheckboxE: return w->as.checkbox.text;
case WidgetRadioE: return w->as.radio.text;
case WidgetTextInputE: return w->as.textInput.buf ? w->as.textInput.buf : "";
case WidgetComboBoxE: return w->as.comboBox.buf ? w->as.comboBox.buf : "";
case WidgetTreeItemE: return w->as.treeItem.text ? w->as.treeItem.text : "";
case WidgetDropdownE:
if (w->as.dropdown.selectedIdx >= 0 && w->as.dropdown.selectedIdx < w->as.dropdown.itemCount) {
return w->as.dropdown.items[w->as.dropdown.selectedIdx];
}
return "";
default: return "";
}
}
// ============================================================
// wgtInitWindow
// ============================================================
WidgetT *wgtInitWindow(AppContextT *ctx, WindowT *win) {
WidgetT *root = widgetAlloc(NULL, WidgetVBoxE);
if (!root) {
return NULL;
}
root->window = win;
root->userData = ctx;
win->widgetRoot = root;
win->onPaint = widgetOnPaint;
win->onMouse = widgetOnMouse;
win->onKey = widgetOnKey;
win->onResize = widgetOnResize;
return root;
}
// ============================================================
// wgtInvalidate
// ============================================================
void wgtInvalidate(WidgetT *w) {
if (!w || !w->window) {
return;
}
// Find the root
WidgetT *root = w;
while (root->parent) {
root = root->parent;
}
AppContextT *ctx = (AppContextT *)root->userData;
if (!ctx) {
return;
}
// Manage scrollbars (measures, adds/removes scrollbars, relayouts)
widgetManageScrollbars(w->window, ctx);
// Repaint
RectT fullRect = {0, 0, w->window->contentW, w->window->contentH};
widgetOnPaint(w->window, &fullRect);
// Dirty the window on screen
dvxInvalidateWindow(ctx, w->window);
}
// ============================================================
// wgtPaint
// ============================================================
void wgtPaint(WidgetT *root, DisplayT *d, const BlitOpsT *ops,
const BitmapFontT *font, const ColorSchemeT *colors) {
if (!root) {
return;
}
widgetPaintOne(root, d, ops, font, colors);
}
// ============================================================
// wgtSetEnabled
// ============================================================
void wgtSetEnabled(WidgetT *w, bool enabled) {
if (w) {
w->enabled = enabled;
}
}
// ============================================================
// wgtSetText
// ============================================================
void wgtSetText(WidgetT *w, const char *text) {
if (!w) {
return;
}
switch (w->type) {
case WidgetLabelE:
w->as.label.text = text;
break;
case WidgetButtonE:
w->as.button.text = text;
break;
case WidgetCheckboxE:
w->as.checkbox.text = text;
break;
case WidgetRadioE:
w->as.radio.text = text;
break;
case WidgetTextInputE:
if (w->as.textInput.buf) {
strncpy(w->as.textInput.buf, text, w->as.textInput.bufSize - 1);
w->as.textInput.buf[w->as.textInput.bufSize - 1] = '\0';
w->as.textInput.len = (int32_t)strlen(w->as.textInput.buf);
w->as.textInput.cursorPos = w->as.textInput.len;
w->as.textInput.scrollOff = 0;
}
break;
case WidgetComboBoxE:
if (w->as.comboBox.buf) {
strncpy(w->as.comboBox.buf, text, w->as.comboBox.bufSize - 1);
w->as.comboBox.buf[w->as.comboBox.bufSize - 1] = '\0';
w->as.comboBox.len = (int32_t)strlen(w->as.comboBox.buf);
w->as.comboBox.cursorPos = w->as.comboBox.len;
w->as.comboBox.scrollOff = 0;
}
break;
case WidgetTreeItemE:
w->as.treeItem.text = text;
break;
default:
break;
}
}
// ============================================================
// wgtSetVisible
// ============================================================
void wgtSetVisible(WidgetT *w, bool visible) {
if (w) {
w->visible = visible;
}
}

View file

@ -0,0 +1,102 @@
// widgetProgressBar.c — ProgressBar widget
#include "widgetInternal.h"
// ============================================================
// wgtProgressBar
// ============================================================
WidgetT *wgtProgressBar(WidgetT *parent) {
WidgetT *w = widgetAlloc(parent, WidgetProgressBarE);
if (w) {
w->as.progressBar.value = 0;
w->as.progressBar.maxValue = 100;
}
return w;
}
// ============================================================
// wgtProgressBarGetValue
// ============================================================
int32_t wgtProgressBarGetValue(const WidgetT *w) {
if (!w || w->type != WidgetProgressBarE) {
return 0;
}
return w->as.progressBar.value;
}
// ============================================================
// wgtProgressBarSetValue
// ============================================================
void wgtProgressBarSetValue(WidgetT *w, int32_t value) {
if (!w || w->type != WidgetProgressBarE) {
return;
}
if (value < 0) {
value = 0;
}
if (value > w->as.progressBar.maxValue) {
value = w->as.progressBar.maxValue;
}
w->as.progressBar.value = value;
}
// ============================================================
// widgetProgressBarCalcMinSize
// ============================================================
void widgetProgressBarCalcMinSize(WidgetT *w, const BitmapFontT *font) {
w->calcMinW = font->charWidth * 12;
w->calcMinH = font->charHeight + 4;
}
// ============================================================
// widgetProgressBarPaint
// ============================================================
void widgetProgressBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops,
const BitmapFontT *font, const ColorSchemeT *colors) {
(void)font;
uint32_t fg = w->fgColor ? w->fgColor : colors->activeTitleBg;
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
// Sunken border
BevelStyleT bevel;
bevel.highlight = colors->windowShadow;
bevel.shadow = colors->windowHighlight;
bevel.face = bg;
bevel.width = 2;
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
// Fill bar
int32_t maxVal = w->as.progressBar.maxValue;
if (maxVal <= 0) {
maxVal = 100;
}
int32_t innerW = w->w - 4;
int32_t innerH = w->h - 4;
int32_t fillW = (innerW * w->as.progressBar.value) / maxVal;
if (fillW > innerW) {
fillW = innerW;
}
if (fillW > 0) {
rectFill(d, ops, w->x + 2, w->y + 2, fillW, innerH, fg);
}
}

102
dvx/widgets/widgetRadio.c Normal file
View file

@ -0,0 +1,102 @@
// widgetRadio.c — RadioGroup and Radio button widgets
#include "widgetInternal.h"
// ============================================================
// wgtRadio
// ============================================================
WidgetT *wgtRadio(WidgetT *parent, const char *text) {
WidgetT *w = widgetAlloc(parent, WidgetRadioE);
if (w) {
w->as.radio.text = text;
// Auto-assign index based on position in parent
int32_t idx = 0;
for (WidgetT *c = parent->firstChild; c != w; c = c->nextSibling) {
if (c->type == WidgetRadioE) {
idx++;
}
}
w->as.radio.index = idx;
}
return w;
}
// ============================================================
// wgtRadioGroup
// ============================================================
WidgetT *wgtRadioGroup(WidgetT *parent) {
WidgetT *w = widgetAlloc(parent, WidgetRadioGroupE);
if (w) {
w->as.radioGroup.selectedIdx = 0;
}
return w;
}
// ============================================================
// widgetRadioCalcMinSize
// ============================================================
void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font) {
w->calcMinW = CHECKBOX_BOX_SIZE + CHECKBOX_GAP +
(int32_t)strlen(w->as.radio.text) * font->charWidth;
w->calcMinH = DVX_MAX(CHECKBOX_BOX_SIZE, font->charHeight);
}
// ============================================================
// widgetRadioOnMouse
// ============================================================
void widgetRadioOnMouse(WidgetT *hit) {
if (hit->parent && hit->parent->type == WidgetRadioGroupE) {
hit->parent->as.radioGroup.selectedIdx = hit->as.radio.index;
if (hit->parent->onChange) {
hit->parent->onChange(hit->parent);
}
}
}
// ============================================================
// widgetRadioPaint
// ============================================================
void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops,
const BitmapFontT *font, const ColorSchemeT *colors) {
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
int32_t boxY = w->y + (w->h - CHECKBOX_BOX_SIZE) / 2;
// Draw radio box
BevelStyleT bevel;
bevel.highlight = colors->windowShadow;
bevel.shadow = colors->windowHighlight;
bevel.face = bg;
bevel.width = 1;
drawBevel(d, ops, w->x, boxY, CHECKBOX_BOX_SIZE, CHECKBOX_BOX_SIZE, &bevel);
// Draw filled dot if selected
if (w->parent && w->parent->type == WidgetRadioGroupE &&
w->parent->as.radioGroup.selectedIdx == w->as.radio.index) {
rectFill(d, ops, w->x + 3, boxY + 3,
CHECKBOX_BOX_SIZE - 6, CHECKBOX_BOX_SIZE - 6, fg);
}
drawText(d, ops, font,
w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP,
w->y + (w->h - font->charHeight) / 2,
w->as.radio.text, fg, bg, false);
}

View file

@ -0,0 +1,53 @@
// widgetSeparator.c — Separator widget (horizontal and vertical)
#include "widgetInternal.h"
// ============================================================
// wgtHSeparator
// ============================================================
WidgetT *wgtHSeparator(WidgetT *parent) {
WidgetT *w = widgetAlloc(parent, WidgetSeparatorE);
if (w) {
w->as.separator.vertical = false;
}
return w;
}
// ============================================================
// wgtVSeparator
// ============================================================
WidgetT *wgtVSeparator(WidgetT *parent) {
WidgetT *w = widgetAlloc(parent, WidgetSeparatorE);
if (w) {
w->as.separator.vertical = true;
}
return w;
}
// ============================================================
// widgetSeparatorPaint
// ============================================================
void widgetSeparatorPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops,
const BitmapFontT *font, const ColorSchemeT *colors) {
(void)font;
if (w->as.separator.vertical) {
int32_t cx = w->x + w->w / 2;
drawVLine(d, ops, cx, w->y, w->h, colors->windowShadow);
drawVLine(d, ops, cx + 1, w->y, w->h, colors->windowHighlight);
} else {
int32_t cy = w->y + w->h / 2;
drawHLine(d, ops, w->x, cy, w->w, colors->windowShadow);
drawHLine(d, ops, w->x, cy + 1, w->w, colors->windowHighlight);
}
}

201
dvx/widgets/widgetSlider.c Normal file
View file

@ -0,0 +1,201 @@
// widgetSlider.c — Slider (trackbar) widget
#include "widgetInternal.h"
// ============================================================
// wgtSlider
// ============================================================
WidgetT *wgtSlider(WidgetT *parent, int32_t minVal, int32_t maxVal) {
WidgetT *w = widgetAlloc(parent, WidgetSliderE);
if (w) {
w->as.slider.value = minVal;
w->as.slider.minValue = minVal;
w->as.slider.maxValue = maxVal;
w->as.slider.vertical = false;
w->weight = 100;
}
return w;
}
// ============================================================
// wgtSliderGetValue
// ============================================================
int32_t wgtSliderGetValue(const WidgetT *w) {
if (!w || w->type != WidgetSliderE) {
return 0;
}
return w->as.slider.value;
}
// ============================================================
// wgtSliderSetValue
// ============================================================
void wgtSliderSetValue(WidgetT *w, int32_t value) {
if (!w || w->type != WidgetSliderE) {
return;
}
if (value < w->as.slider.minValue) {
value = w->as.slider.minValue;
}
if (value > w->as.slider.maxValue) {
value = w->as.slider.maxValue;
}
w->as.slider.value = value;
}
// ============================================================
// widgetSliderCalcMinSize
// ============================================================
void widgetSliderCalcMinSize(WidgetT *w, const BitmapFontT *font) {
(void)font;
if (w->as.slider.vertical) {
w->calcMinW = SLIDER_THUMB_W + 4;
w->calcMinH = SLIDER_THUMB_W * 5;
} else {
w->calcMinW = SLIDER_THUMB_W * 5;
w->calcMinH = SLIDER_THUMB_W + 4;
}
}
// ============================================================
// widgetSliderOnMouse
// ============================================================
void widgetSliderOnMouse(WidgetT *hit, int32_t vx, int32_t vy) {
int32_t range = hit->as.slider.maxValue - hit->as.slider.minValue;
if (range <= 0) {
return;
}
int32_t thumbRange;
int32_t thumbPos;
int32_t mousePos;
if (hit->as.slider.vertical) {
thumbRange = hit->h - SLIDER_THUMB_W;
thumbPos = ((hit->as.slider.value - hit->as.slider.minValue) * thumbRange) / range;
mousePos = vy - hit->y;
if (mousePos >= thumbPos && mousePos < thumbPos + SLIDER_THUMB_W) {
// Click on thumb — start drag
sDragSlider = hit;
sDragOffset = mousePos - thumbPos;
} else {
// Click on track — jump to position
int32_t newVal = hit->as.slider.minValue + ((mousePos - SLIDER_THUMB_W / 2) * range) / thumbRange;
if (newVal < hit->as.slider.minValue) { newVal = hit->as.slider.minValue; }
if (newVal > hit->as.slider.maxValue) { newVal = hit->as.slider.maxValue; }
hit->as.slider.value = newVal;
if (hit->onChange) {
hit->onChange(hit);
}
}
} else {
thumbRange = hit->w - SLIDER_THUMB_W;
thumbPos = ((hit->as.slider.value - hit->as.slider.minValue) * thumbRange) / range;
mousePos = vx - hit->x;
if (mousePos >= thumbPos && mousePos < thumbPos + SLIDER_THUMB_W) {
// Click on thumb — start drag
sDragSlider = hit;
sDragOffset = mousePos - thumbPos;
} else {
// Click on track — jump to position
int32_t newVal = hit->as.slider.minValue + ((mousePos - SLIDER_THUMB_W / 2) * range) / thumbRange;
if (newVal < hit->as.slider.minValue) { newVal = hit->as.slider.minValue; }
if (newVal > hit->as.slider.maxValue) { newVal = hit->as.slider.maxValue; }
hit->as.slider.value = newVal;
if (hit->onChange) {
hit->onChange(hit);
}
}
}
}
// ============================================================
// widgetSliderPaint
// ============================================================
void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops,
const BitmapFontT *font, const ColorSchemeT *colors) {
(void)font;
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
int32_t range = w->as.slider.maxValue - w->as.slider.minValue;
if (range <= 0) {
range = 1;
}
if (w->as.slider.vertical) {
// Track groove
int32_t trackX = w->x + (w->w - SLIDER_TRACK_H) / 2;
BevelStyleT groove;
groove.highlight = colors->windowShadow;
groove.shadow = colors->windowHighlight;
groove.face = colors->scrollbarTrough;
groove.width = 1;
drawBevel(d, ops, trackX, w->y, SLIDER_TRACK_H, w->h, &groove);
// Thumb
int32_t thumbRange = w->h - SLIDER_THUMB_W;
int32_t thumbY = w->y + ((w->as.slider.value - w->as.slider.minValue) * thumbRange) / range;
BevelStyleT thumb;
thumb.highlight = colors->windowHighlight;
thumb.shadow = colors->windowShadow;
thumb.face = colors->buttonFace;
thumb.width = 2;
drawBevel(d, ops, w->x, thumbY, w->w, SLIDER_THUMB_W, &thumb);
// Center tick on thumb
drawHLine(d, ops, w->x + 3, thumbY + SLIDER_THUMB_W / 2, w->w - 6, fg);
} else {
// Track groove
int32_t trackY = w->y + (w->h - SLIDER_TRACK_H) / 2;
BevelStyleT groove;
groove.highlight = colors->windowShadow;
groove.shadow = colors->windowHighlight;
groove.face = colors->scrollbarTrough;
groove.width = 1;
drawBevel(d, ops, w->x, trackY, w->w, SLIDER_TRACK_H, &groove);
// Thumb
int32_t thumbRange = w->w - SLIDER_THUMB_W;
int32_t thumbX = w->x + ((w->as.slider.value - w->as.slider.minValue) * thumbRange) / range;
BevelStyleT thumb;
thumb.highlight = colors->windowHighlight;
thumb.shadow = colors->windowShadow;
thumb.face = colors->buttonFace;
thumb.width = 2;
drawBevel(d, ops, thumbX, w->y, SLIDER_THUMB_W, w->h, &thumb);
// Center tick on thumb
drawVLine(d, ops, thumbX + SLIDER_THUMB_W / 2, w->y + 3, w->h - 6, fg);
}
}

View file

@ -0,0 +1,29 @@
// widgetSpacer.c — Spacer widget (invisible stretching element)
#include "widgetInternal.h"
// ============================================================
// wgtSpacer
// ============================================================
WidgetT *wgtSpacer(WidgetT *parent) {
WidgetT *w = widgetAlloc(parent, WidgetSpacerE);
if (w) {
w->weight = 100; // spacers stretch by default
}
return w;
}
// ============================================================
// widgetSpacerCalcMinSize
// ============================================================
void widgetSpacerCalcMinSize(WidgetT *w, const BitmapFontT *font) {
(void)font;
w->calcMinW = 0;
w->calcMinH = 0;
}

View file

@ -0,0 +1,43 @@
// widgetStatusBar.c — StatusBar widget
#include "widgetInternal.h"
// ============================================================
// wgtStatusBar
// ============================================================
WidgetT *wgtStatusBar(WidgetT *parent) {
WidgetT *w = widgetAlloc(parent, WidgetStatusBarE);
if (w) {
w->padding = wgtPixels(2);
w->spacing = wgtPixels(2);
}
return w;
}
// ============================================================
// widgetStatusBarPaint
// ============================================================
void widgetStatusBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops,
const BitmapFontT *font, const ColorSchemeT *colors) {
(void)font;
// Draw sunken border around each child
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (!c->visible) {
continue;
}
BevelStyleT bevel;
bevel.highlight = colors->windowShadow;
bevel.shadow = colors->windowHighlight;
bevel.face = 0;
bevel.width = 1;
drawBevel(d, ops, c->x - 1, c->y - 1, c->w + 2, c->h + 2, &bevel);
}
}

View file

@ -0,0 +1,254 @@
// widgetTabControl.c — TabControl and TabPage widgets
#include "widgetInternal.h"
// ============================================================
// wgtTabControl
// ============================================================
WidgetT *wgtTabControl(WidgetT *parent) {
WidgetT *w = widgetAlloc(parent, WidgetTabControlE);
if (w) {
w->as.tabControl.activeTab = 0;
w->weight = 100;
}
return w;
}
// ============================================================
// wgtTabControlGetActive
// ============================================================
int32_t wgtTabControlGetActive(const WidgetT *w) {
if (!w || w->type != WidgetTabControlE) {
return 0;
}
return w->as.tabControl.activeTab;
}
// ============================================================
// wgtTabControlSetActive
// ============================================================
void wgtTabControlSetActive(WidgetT *w, int32_t idx) {
if (!w || w->type != WidgetTabControlE) {
return;
}
w->as.tabControl.activeTab = idx;
}
// ============================================================
// wgtTabPage
// ============================================================
WidgetT *wgtTabPage(WidgetT *parent, const char *title) {
WidgetT *w = widgetAlloc(parent, WidgetTabPageE);
if (w) {
w->as.tabPage.title = title;
}
return w;
}
// ============================================================
// widgetTabControlCalcMinSize
// ============================================================
void widgetTabControlCalcMinSize(WidgetT *w, const BitmapFontT *font) {
int32_t tabH = font->charHeight + TAB_PAD_V * 2;
int32_t maxPageW = 0;
int32_t maxPageH = 0;
int32_t tabHeaderW = 0;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (c->type != WidgetTabPageE) {
continue;
}
widgetCalcMinSizeTree(c, font);
maxPageW = DVX_MAX(maxPageW, c->calcMinW);
maxPageH = DVX_MAX(maxPageH, c->calcMinH);
int32_t labelW = (int32_t)strlen(c->as.tabPage.title) * font->charWidth + TAB_PAD_H * 2;
tabHeaderW += labelW;
}
w->calcMinW = DVX_MAX(maxPageW + TAB_BORDER * 2, tabHeaderW);
w->calcMinH = tabH + maxPageH + TAB_BORDER * 2;
}
// ============================================================
// widgetTabControlLayout
// ============================================================
void widgetTabControlLayout(WidgetT *w, const BitmapFontT *font) {
int32_t tabH = font->charHeight + TAB_PAD_V * 2;
int32_t contentX = w->x + TAB_BORDER;
int32_t contentY = w->y + tabH + TAB_BORDER;
int32_t contentW = w->w - TAB_BORDER * 2;
int32_t contentH = w->h - tabH - TAB_BORDER * 2;
if (contentW < 0) { contentW = 0; }
if (contentH < 0) { contentH = 0; }
int32_t idx = 0;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (c->type != WidgetTabPageE) {
continue;
}
c->x = contentX;
c->y = contentY;
c->w = contentW;
c->h = contentH;
// Only layout the active page
if (idx == w->as.tabControl.activeTab) {
widgetLayoutChildren(c, font);
}
idx++;
}
}
// ============================================================
// widgetTabControlOnMouse
// ============================================================
void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
AppContextT *ctx = (AppContextT *)root->userData;
const BitmapFontT *font = &ctx->font;
int32_t tabH = font->charHeight + TAB_PAD_V * 2;
// Only handle clicks in the tab header area
if (vy >= hit->y && vy < hit->y + tabH) {
int32_t tabX = hit->x + 2;
int32_t tabIdx = 0;
for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) {
if (c->type != WidgetTabPageE) {
continue;
}
int32_t tw = (int32_t)strlen(c->as.tabPage.title) * font->charWidth + TAB_PAD_H * 2;
if (vx >= tabX && vx < tabX + tw) {
if (tabIdx != hit->as.tabControl.activeTab) {
hit->as.tabControl.activeTab = tabIdx;
if (hit->onChange) {
hit->onChange(hit);
}
}
break;
}
tabX += tw;
tabIdx++;
}
}
}
// ============================================================
// widgetTabControlPaint
// ============================================================
void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops,
const BitmapFontT *font, const ColorSchemeT *colors) {
int32_t tabH = font->charHeight + TAB_PAD_V * 2;
// Content panel
BevelStyleT panelBevel;
panelBevel.highlight = colors->windowHighlight;
panelBevel.shadow = colors->windowShadow;
panelBevel.face = colors->contentBg;
panelBevel.width = 2;
drawBevel(d, ops, w->x, w->y + tabH, w->w, w->h - tabH, &panelBevel);
// Tab headers
int32_t tabX = w->x + 2;
int32_t tabIdx = 0;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (c->type != WidgetTabPageE) {
continue;
}
int32_t tw = (int32_t)strlen(c->as.tabPage.title) * font->charWidth + TAB_PAD_H * 2;
bool isActive = (tabIdx == w->as.tabControl.activeTab);
int32_t ty = isActive ? w->y : w->y + 2;
int32_t th = isActive ? tabH + 2 : tabH;
uint32_t tabFace = isActive ? colors->contentBg : colors->windowFace;
// Fill tab background
rectFill(d, ops, tabX + 2, ty + 2, tw - 4, th - 2, tabFace);
// Top edge
drawHLine(d, ops, tabX + 2, ty, tw - 4, colors->windowHighlight);
drawHLine(d, ops, tabX + 2, ty + 1, tw - 4, colors->windowHighlight);
// Left edge
drawVLine(d, ops, tabX, ty + 2, th - 2, colors->windowHighlight);
drawVLine(d, ops, tabX + 1, ty + 2, th - 2, colors->windowHighlight);
// Right edge
drawVLine(d, ops, tabX + tw - 1, ty + 2, th - 2, colors->windowShadow);
drawVLine(d, ops, tabX + tw - 2, ty + 2, th - 2, colors->windowShadow);
if (isActive) {
// Erase panel top border under active tab
rectFill(d, ops, tabX + 2, w->y + tabH, tw - 4, 2, colors->contentBg);
} else {
// Bottom edge for inactive tab
drawHLine(d, ops, tabX, ty + th - 1, tw, colors->windowShadow);
drawHLine(d, ops, tabX + 1, ty + th - 2, tw - 2, colors->windowShadow);
}
// Tab label
int32_t labelY = ty + TAB_PAD_V;
if (!isActive) {
labelY++;
}
drawText(d, ops, font, tabX + TAB_PAD_H, labelY,
c->as.tabPage.title, colors->contentFg, tabFace, true);
tabX += tw;
tabIdx++;
}
// Paint only active tab page's children
tabIdx = 0;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (c->type != WidgetTabPageE) {
continue;
}
if (tabIdx == w->as.tabControl.activeTab) {
for (WidgetT *gc = c->firstChild; gc; gc = gc->nextSibling) {
widgetPaintOne(gc, d, ops, font, colors);
}
break;
}
tabIdx++;
}
}

View file

@ -0,0 +1,130 @@
// widgetTextInput.c — TextInput and TextArea widgets
#include "widgetInternal.h"
// ============================================================
// wgtTextArea
// ============================================================
WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen) {
WidgetT *w = widgetAlloc(parent, WidgetTextAreaE);
if (w) {
int32_t bufSize = maxLen > 0 ? maxLen + 1 : 256;
w->as.textArea.buf = (char *)malloc(bufSize);
w->as.textArea.bufSize = bufSize;
if (w->as.textArea.buf) {
w->as.textArea.buf[0] = '\0';
}
}
return w;
}
// ============================================================
// wgtTextInput
// ============================================================
WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen) {
WidgetT *w = widgetAlloc(parent, WidgetTextInputE);
if (w) {
int32_t bufSize = maxLen > 0 ? maxLen + 1 : 256;
w->as.textInput.buf = (char *)malloc(bufSize);
w->as.textInput.bufSize = bufSize;
if (w->as.textInput.buf) {
w->as.textInput.buf[0] = '\0';
}
w->weight = 100; // text inputs stretch by default
}
return w;
}
// ============================================================
// widgetTextInputCalcMinSize
// ============================================================
void widgetTextInputCalcMinSize(WidgetT *w, const BitmapFontT *font) {
w->calcMinW = font->charWidth * 8 + TEXT_INPUT_PAD * 2;
w->calcMinH = font->charHeight + TEXT_INPUT_PAD * 2;
}
// ============================================================
// widgetTextInputOnMouse
// ============================================================
void widgetTextInputOnMouse(WidgetT *hit, WidgetT *root, int32_t vx) {
hit->focused = true;
// Place cursor at click position
AppContextT *ctx = (AppContextT *)root->userData;
const BitmapFontT *font = &ctx->font;
int32_t relX = vx - hit->x - TEXT_INPUT_PAD;
int32_t charPos = relX / font->charWidth + hit->as.textInput.scrollOff;
if (charPos < 0) {
charPos = 0;
}
if (charPos > hit->as.textInput.len) {
charPos = hit->as.textInput.len;
}
hit->as.textInput.cursorPos = charPos;
}
// ============================================================
// widgetTextInputPaint
// ============================================================
void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops,
const BitmapFontT *font, const ColorSchemeT *colors) {
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
// Sunken border
BevelStyleT bevel;
bevel.highlight = colors->windowShadow;
bevel.shadow = colors->windowHighlight;
bevel.face = bg;
bevel.width = 2;
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
// Draw text
if (w->as.textInput.buf) {
int32_t textX = w->x + TEXT_INPUT_PAD;
int32_t textY = w->y + (w->h - font->charHeight) / 2;
int32_t maxChars = (w->w - TEXT_INPUT_PAD * 2) / font->charWidth;
int32_t off = w->as.textInput.scrollOff;
int32_t len = w->as.textInput.len - off;
if (len > maxChars) {
len = maxChars;
}
for (int32_t i = 0; i < len; i++) {
drawChar(d, ops, font, textX + i * font->charWidth, textY,
w->as.textInput.buf[off + i],
fg, bg, true);
}
// Draw cursor
if (w->focused) {
int32_t cursorX = textX + (w->as.textInput.cursorPos - off) * font->charWidth;
if (cursorX >= w->x + TEXT_INPUT_PAD &&
cursorX < w->x + w->w - TEXT_INPUT_PAD) {
drawVLine(d, ops, cursorX, textY, font->charHeight, fg);
}
}
}
}

View file

@ -0,0 +1,37 @@
// widgetToolbar.c — Toolbar widget
#include "widgetInternal.h"
// ============================================================
// wgtToolbar
// ============================================================
WidgetT *wgtToolbar(WidgetT *parent) {
WidgetT *w = widgetAlloc(parent, WidgetToolbarE);
if (w) {
w->padding = wgtPixels(2);
w->spacing = wgtPixels(2);
}
return w;
}
// ============================================================
// widgetToolbarPaint
// ============================================================
void widgetToolbarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops,
const BitmapFontT *font, const ColorSchemeT *colors) {
(void)font;
// Draw raised background and bottom separator
BevelStyleT bevel;
bevel.highlight = colors->windowHighlight;
bevel.shadow = colors->windowShadow;
bevel.face = colors->windowFace;
bevel.width = 1;
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
}

View file

@ -0,0 +1,364 @@
// widgetTreeView.c — TreeView and TreeItem widgets
#include "widgetInternal.h"
// ============================================================
// Prototypes
// ============================================================
static int32_t calcTreeItemsHeight(WidgetT *parent, const BitmapFontT *font);
static int32_t calcTreeItemsMaxWidth(WidgetT *parent, const BitmapFontT *font, int32_t depth);
static void layoutTreeItems(WidgetT *parent, const BitmapFontT *font, int32_t x, int32_t *y, int32_t width, int32_t depth);
static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t baseX, int32_t *itemY, int32_t depth, int32_t clipTop, int32_t clipBottom);
static WidgetT *treeItemAtY(WidgetT *parent, int32_t targetY, int32_t *curY, const BitmapFontT *font);
// ============================================================
// calcTreeItemsHeight
// ============================================================
static int32_t calcTreeItemsHeight(WidgetT *parent, const BitmapFontT *font) {
int32_t totalH = 0;
for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) {
if (c->type != WidgetTreeItemE || !c->visible) {
continue;
}
totalH += font->charHeight;
if (c->as.treeItem.expanded && c->firstChild) {
totalH += calcTreeItemsHeight(c, font);
}
}
return totalH;
}
// ============================================================
// calcTreeItemsMaxWidth
// ============================================================
static int32_t calcTreeItemsMaxWidth(WidgetT *parent, const BitmapFontT *font, int32_t depth) {
int32_t maxW = 0;
for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) {
if (c->type != WidgetTreeItemE || !c->visible) {
continue;
}
int32_t indent = depth * TREE_INDENT + TREE_EXPAND_SIZE + TREE_ICON_GAP;
int32_t textW = (int32_t)strlen(c->as.treeItem.text) * font->charWidth;
int32_t itemW = indent + textW;
if (itemW > maxW) {
maxW = itemW;
}
if (c->as.treeItem.expanded && c->firstChild) {
int32_t childW = calcTreeItemsMaxWidth(c, font, depth + 1);
if (childW > maxW) {
maxW = childW;
}
}
}
return maxW;
}
// ============================================================
// layoutTreeItems
// ============================================================
static void layoutTreeItems(WidgetT *parent, const BitmapFontT *font,
int32_t x, int32_t *y, int32_t width, int32_t depth) {
for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) {
if (c->type != WidgetTreeItemE || !c->visible) {
continue;
}
c->x = x;
c->y = *y;
c->w = width;
c->h = font->charHeight;
*y += font->charHeight;
if (c->as.treeItem.expanded && c->firstChild) {
layoutTreeItems(c, font, x, y, width, depth + 1);
}
}
}
// ============================================================
// paintTreeItems
// ============================================================
static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops,
const BitmapFontT *font, const ColorSchemeT *colors,
int32_t baseX, int32_t *itemY, int32_t depth,
int32_t clipTop, int32_t clipBottom) {
for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) {
if (c->type != WidgetTreeItemE || !c->visible) {
continue;
}
int32_t ix = baseX + depth * TREE_INDENT;
int32_t iy = *itemY;
*itemY += font->charHeight;
// Skip items outside visible area
if (iy + font->charHeight <= clipTop || iy >= clipBottom) {
if (c->as.treeItem.expanded && c->firstChild) {
paintTreeItems(c, d, ops, font, colors, baseX, itemY, depth + 1, clipTop, clipBottom);
}
continue;
}
// Draw expand/collapse icon if item has children
bool hasChildren = false;
for (WidgetT *gc = c->firstChild; gc; gc = gc->nextSibling) {
if (gc->type == WidgetTreeItemE) {
hasChildren = true;
break;
}
}
int32_t textX = ix;
if (hasChildren) {
int32_t iconX = ix;
int32_t iconY = iy + (font->charHeight - TREE_EXPAND_SIZE) / 2;
// Draw box
rectFill(d, ops, iconX + 1, iconY + 1, TREE_EXPAND_SIZE - 2, TREE_EXPAND_SIZE - 2, colors->contentBg);
drawHLine(d, ops, iconX, iconY, TREE_EXPAND_SIZE, colors->windowShadow);
drawHLine(d, ops, iconX, iconY + TREE_EXPAND_SIZE - 1, TREE_EXPAND_SIZE, colors->windowShadow);
drawVLine(d, ops, iconX, iconY, TREE_EXPAND_SIZE, colors->windowShadow);
drawVLine(d, ops, iconX + TREE_EXPAND_SIZE - 1, iconY, TREE_EXPAND_SIZE, colors->windowShadow);
// Draw + or -
int32_t mid = TREE_EXPAND_SIZE / 2;
drawHLine(d, ops, iconX + 2, iconY + mid, TREE_EXPAND_SIZE - 4, colors->contentFg);
if (!c->as.treeItem.expanded) {
drawVLine(d, ops, iconX + mid, iconY + 2, TREE_EXPAND_SIZE - 4, colors->contentFg);
}
textX += TREE_EXPAND_SIZE + TREE_ICON_GAP;
} else {
textX += TREE_EXPAND_SIZE + TREE_ICON_GAP;
}
// Draw text
uint32_t fg = c->fgColor ? c->fgColor : colors->contentFg;
uint32_t bg = c->bgColor ? c->bgColor : colors->contentBg;
drawText(d, ops, font, textX, iy, c->as.treeItem.text, fg, bg, false);
// Recurse into expanded children
if (c->as.treeItem.expanded && c->firstChild) {
paintTreeItems(c, d, ops, font, colors, baseX, itemY, depth + 1, clipTop, clipBottom);
}
}
}
// ============================================================
// treeItemAtY
// ============================================================
//
// Find the tree item at a given Y coordinate.
static WidgetT *treeItemAtY(WidgetT *parent, int32_t targetY, int32_t *curY,
const BitmapFontT *font) {
for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) {
if (c->type != WidgetTreeItemE || !c->visible) {
continue;
}
if (targetY >= *curY && targetY < *curY + font->charHeight) {
return c;
}
*curY += font->charHeight;
if (c->as.treeItem.expanded && c->firstChild) {
WidgetT *found = treeItemAtY(c, targetY, curY, font);
if (found) {
return found;
}
}
}
return NULL;
}
// ============================================================
// wgtTreeItem
// ============================================================
WidgetT *wgtTreeItem(WidgetT *parent, const char *text) {
WidgetT *w = widgetAlloc(parent, WidgetTreeItemE);
if (w) {
w->as.treeItem.text = text;
w->as.treeItem.expanded = false;
}
return w;
}
// ============================================================
// wgtTreeItemIsExpanded
// ============================================================
bool wgtTreeItemIsExpanded(const WidgetT *w) {
if (!w || w->type != WidgetTreeItemE) {
return false;
}
return w->as.treeItem.expanded;
}
// ============================================================
// wgtTreeItemSetExpanded
// ============================================================
void wgtTreeItemSetExpanded(WidgetT *w, bool expanded) {
if (!w || w->type != WidgetTreeItemE) {
return;
}
w->as.treeItem.expanded = expanded;
}
// ============================================================
// wgtTreeView
// ============================================================
WidgetT *wgtTreeView(WidgetT *parent) {
WidgetT *w = widgetAlloc(parent, WidgetTreeViewE);
if (w) {
w->weight = 100;
}
return w;
}
// ============================================================
// widgetTreeViewCalcMinSize
// ============================================================
void widgetTreeViewCalcMinSize(WidgetT *w, const BitmapFontT *font) {
int32_t totalH = calcTreeItemsHeight(w, font);
int32_t maxW = calcTreeItemsMaxWidth(w, font, 0);
w->calcMinW = maxW + TREE_BORDER * 2;
w->calcMinH = totalH + TREE_BORDER * 2;
}
// ============================================================
// widgetTreeViewLayout
// ============================================================
void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font) {
int32_t innerX = w->x + TREE_BORDER;
int32_t innerY = w->y + TREE_BORDER;
int32_t innerW = w->w - TREE_BORDER * 2;
layoutTreeItems(w, font, innerX, &innerY, innerW, 0);
}
// ============================================================
// widgetTreeViewOnMouse
// ============================================================
void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
AppContextT *ctx = (AppContextT *)root->userData;
const BitmapFontT *font = &ctx->font;
int32_t curY = hit->y + TREE_BORDER;
WidgetT *item = treeItemAtY(hit, vy, &curY, font);
if (!item) {
return;
}
// Check if click is on expand/collapse icon
bool hasChildren = false;
for (WidgetT *gc = item->firstChild; gc; gc = gc->nextSibling) {
if (gc->type == WidgetTreeItemE) {
hasChildren = true;
break;
}
}
if (hasChildren) {
// Calculate indent depth
int32_t depth = 0;
WidgetT *p = item->parent;
while (p && p->type == WidgetTreeItemE) {
depth++;
p = p->parent;
}
int32_t iconX = hit->x + TREE_BORDER + depth * TREE_INDENT;
if (vx >= iconX && vx < iconX + TREE_EXPAND_SIZE) {
item->as.treeItem.expanded = !item->as.treeItem.expanded;
if (item->onChange) {
item->onChange(item);
}
} else {
if (item->onClick) {
item->onClick(item);
}
}
} else {
if (item->onClick) {
item->onClick(item);
}
}
}
// ============================================================
// widgetTreeViewPaint
// ============================================================
void widgetTreeViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops,
const BitmapFontT *font, const ColorSchemeT *colors) {
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
// Sunken border
BevelStyleT bevel;
bevel.highlight = colors->windowShadow;
bevel.shadow = colors->windowHighlight;
bevel.face = bg;
bevel.width = 2;
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
// Paint tree items
int32_t itemY = w->y + TREE_BORDER;
paintTreeItems(w, d, ops, font, colors,
w->x + TREE_BORDER, &itemY, 0,
w->y + TREE_BORDER, w->y + w->h - TREE_BORDER);
}