More widgets!
This commit is contained in:
parent
67900cbd7f
commit
76a955a9e7
33 changed files with 6610 additions and 1649 deletions
|
|
@ -42,7 +42,7 @@ Supporting files:
|
||||||
| `dvxFont.h` | Built-in 8x14 bitmap font glyph data |
|
| `dvxFont.h` | Built-in 8x14 bitmap font glyph data |
|
||||||
| `dvxCursor.h` | Mouse cursor bitmask data (5 shapes) |
|
| `dvxCursor.h` | Mouse cursor bitmask data (5 shapes) |
|
||||||
| `dvxPalette.h` | Default VGA palette for 8-bit mode |
|
| `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 |
|
| `thirdparty/stb_image.h` | Third-party single-header image loader |
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
@ -379,7 +379,17 @@ WidgetT *wgtHBox(WidgetT *parent); // horizontal stack
|
||||||
WidgetT *wgtFrame(WidgetT *parent, const char *title); // titled border
|
WidgetT *wgtFrame(WidgetT *parent, const char *title); // titled border
|
||||||
```
|
```
|
||||||
Containers hold child widgets and control layout direction. `wgtFrame`
|
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 *`):
|
Container properties (set directly on the returned `WidgetT *`):
|
||||||
|
|
||||||
75
dvx/Makefile
75
dvx/Makefile
|
|
@ -5,26 +5,59 @@ CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
|
||||||
CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586
|
CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586
|
||||||
LDFLAGS = -lm
|
LDFLAGS = -lm
|
||||||
|
|
||||||
OBJDIR = ../obj
|
OBJDIR = ../obj
|
||||||
BINDIR = ../bin
|
WOBJDIR = ../obj/widgets
|
||||||
|
BINDIR = ../bin
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxIcon.c dvxWidget.c dvxApp.c demo.c
|
|
||||||
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
|
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
|
||||||
|
WOBJS = $(patsubst widgets/%.c,$(WOBJDIR)/%.o,$(WSRCS))
|
||||||
TARGET = $(BINDIR)/demo.exe
|
TARGET = $(BINDIR)/demo.exe
|
||||||
|
|
||||||
.PHONY: all clean
|
.PHONY: all clean
|
||||||
|
|
||||||
all: $(TARGET)
|
all: $(TARGET)
|
||||||
|
|
||||||
$(TARGET): $(OBJS) | $(BINDIR)
|
$(TARGET): $(OBJS) $(WOBJS) | $(BINDIR)
|
||||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS)
|
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS) $(WOBJS)
|
||||||
|
|
||||||
$(OBJDIR)/%.o: %.c | $(OBJDIR)
|
$(OBJDIR)/%.o: %.c | $(OBJDIR)
|
||||||
$(CC) $(CFLAGS) -c -o $@ $<
|
$(CC) $(CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
|
$(WOBJDIR)/%.o: widgets/%.c | $(WOBJDIR)
|
||||||
|
$(CC) $(CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
$(OBJDIR):
|
$(OBJDIR):
|
||||||
mkdir -p $(OBJDIR)
|
mkdir -p $(OBJDIR)
|
||||||
|
|
||||||
|
$(WOBJDIR):
|
||||||
|
mkdir -p $(WOBJDIR)
|
||||||
|
|
||||||
$(BINDIR):
|
$(BINDIR):
|
||||||
mkdir -p $(BINDIR)
|
mkdir -p $(BINDIR)
|
||||||
|
|
||||||
|
|
@ -33,10 +66,36 @@ $(OBJDIR)/dvxVideo.o: dvxVideo.c dvxVideo.h dvxTypes.h dvxPalette.h
|
||||||
$(OBJDIR)/dvxDraw.o: dvxDraw.c dvxDraw.h dvxTypes.h
|
$(OBJDIR)/dvxDraw.o: dvxDraw.c dvxDraw.h dvxTypes.h
|
||||||
$(OBJDIR)/dvxComp.o: dvxComp.c dvxComp.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)/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)/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)/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
|
$(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:
|
clean:
|
||||||
rm -rf $(OBJDIR) $(BINDIR)
|
rm -rf $(OBJDIR) $(BINDIR)
|
||||||
|
|
|
||||||
88
dvx/demo.c
88
dvx/demo.c
|
|
@ -26,6 +26,7 @@ static void onOkClick(WidgetT *w);
|
||||||
static void onPaintColor(WindowT *win, RectT *dirtyArea);
|
static void onPaintColor(WindowT *win, RectT *dirtyArea);
|
||||||
static void onPaintPattern(WindowT *win, RectT *dirtyArea);
|
static void onPaintPattern(WindowT *win, RectT *dirtyArea);
|
||||||
static void onPaintText(WindowT *win, RectT *dirtyArea);
|
static void onPaintText(WindowT *win, RectT *dirtyArea);
|
||||||
|
static void setupWidgetDemo2(AppContextT *ctx);
|
||||||
static void setupWindows(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
|
// setupWindows
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -386,6 +473,7 @@ int main(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
setupWindows(&ctx);
|
setupWindows(&ctx);
|
||||||
|
setupWidgetDemo2(&ctx);
|
||||||
|
|
||||||
dvxRun(&ctx);
|
dvxRun(&ctx);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -703,12 +703,12 @@ static void initColorScheme(AppContextT *ctx) {
|
||||||
ctx->colors.desktop = packColor(d, 0, 128, 128); // GEOS teal desktop
|
ctx->colors.desktop = packColor(d, 0, 128, 128); // GEOS teal desktop
|
||||||
ctx->colors.windowFace = packColor(d, 192, 192, 192); // standard Motif grey
|
ctx->colors.windowFace = packColor(d, 192, 192, 192); // standard Motif grey
|
||||||
ctx->colors.windowHighlight = packColor(d, 255, 255, 255);
|
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.activeTitleBg = packColor(d, 48, 48, 48); // GEOS dark charcoal
|
||||||
ctx->colors.activeTitleFg = packColor(d, 255, 255, 255);
|
ctx->colors.activeTitleFg = packColor(d, 255, 255, 255);
|
||||||
ctx->colors.inactiveTitleBg = packColor(d, 160, 160, 160); // lighter grey
|
ctx->colors.inactiveTitleBg = packColor(d, 160, 160, 160); // lighter grey
|
||||||
ctx->colors.inactiveTitleFg = packColor(d, 64, 64, 64);
|
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.contentFg = packColor(d, 0, 0, 0);
|
||||||
ctx->colors.menuBg = packColor(d, 192, 192, 192);
|
ctx->colors.menuBg = packColor(d, 192, 192, 192);
|
||||||
ctx->colors.menuFg = packColor(d, 0, 0, 0);
|
ctx->colors.menuFg = packColor(d, 0, 0, 0);
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@
|
||||||
#pragma GCC diagnostic ignored "-Wunused-function"
|
#pragma GCC diagnostic ignored "-Wunused-function"
|
||||||
|
|
||||||
#define STBI_ONLY_BMP
|
#define STBI_ONLY_BMP
|
||||||
#define STBI_ONLY_TGA
|
|
||||||
#define STBI_ONLY_PNG
|
#define STBI_ONLY_PNG
|
||||||
|
#define STBI_ONLY_JPEG
|
||||||
|
#define STBI_ONLY_GIF
|
||||||
#define STBI_NO_SIMD
|
#define STBI_NO_SIMD
|
||||||
#define STB_IMAGE_IMPLEMENTATION
|
#define STB_IMAGE_IMPLEMENTATION
|
||||||
#include "thirdparty/stb_image.h"
|
#include "thirdparty/stb_image.h"
|
||||||
|
|
|
||||||
10
dvx/dvxImageWrite.c
Normal file
10
dvx/dvxImageWrite.c
Normal 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
|
||||||
1634
dvx/dvxWidget.c
1634
dvx/dvxWidget.c
File diff suppressed because it is too large
Load diff
195
dvx/dvxWidget.h
195
dvx/dvxWidget.h
|
|
@ -50,7 +50,19 @@ typedef enum {
|
||||||
WidgetListBoxE,
|
WidgetListBoxE,
|
||||||
WidgetSpacerE,
|
WidgetSpacerE,
|
||||||
WidgetSeparatorE,
|
WidgetSeparatorE,
|
||||||
WidgetFrameE
|
WidgetFrameE,
|
||||||
|
WidgetDropdownE,
|
||||||
|
WidgetComboBoxE,
|
||||||
|
WidgetProgressBarE,
|
||||||
|
WidgetSliderE,
|
||||||
|
WidgetTabControlE,
|
||||||
|
WidgetTabPageE,
|
||||||
|
WidgetStatusBarE,
|
||||||
|
WidgetToolbarE,
|
||||||
|
WidgetTreeViewE,
|
||||||
|
WidgetTreeItemE,
|
||||||
|
WidgetImageE,
|
||||||
|
WidgetCanvasE
|
||||||
} WidgetTypeE;
|
} WidgetTypeE;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -67,6 +79,16 @@ typedef enum {
|
||||||
AlignEndE
|
AlignEndE
|
||||||
} WidgetAlignE;
|
} WidgetAlignE;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Frame style enum
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
FrameInE, // beveled inward (sunken) — default
|
||||||
|
FrameOutE, // beveled outward (raised)
|
||||||
|
FrameFlatE // solid color line
|
||||||
|
} FrameStyleE;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Widget structure
|
// Widget structure
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -108,6 +130,10 @@ typedef struct WidgetT {
|
||||||
int32_t spacing; // tagged size for spacing between children (0 = default)
|
int32_t spacing; // tagged size for spacing between children (0 = default)
|
||||||
int32_t padding; // tagged size for internal padding (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
|
// State
|
||||||
bool visible;
|
bool visible;
|
||||||
bool enabled;
|
bool enabled;
|
||||||
|
|
@ -172,8 +198,81 @@ typedef struct WidgetT {
|
||||||
} separator;
|
} separator;
|
||||||
|
|
||||||
struct {
|
struct {
|
||||||
const char *title;
|
const char *title;
|
||||||
|
FrameStyleE style; // FrameInE (default), FrameOutE, FrameFlatE
|
||||||
|
uint32_t color; // border color for FrameFlatE (0 = use windowShadow)
|
||||||
} frame;
|
} 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;
|
} as;
|
||||||
} WidgetT;
|
} WidgetT;
|
||||||
|
|
||||||
|
|
@ -224,6 +323,98 @@ WidgetT *wgtVSeparator(WidgetT *parent);
|
||||||
WidgetT *wgtListBox(WidgetT *parent);
|
WidgetT *wgtListBox(WidgetT *parent);
|
||||||
WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen);
|
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
|
// Operations
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
1724
dvx/thirdparty/stb_image_write.h
vendored
Normal file
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
97
dvx/widgets/widgetBox.c
Normal 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);
|
||||||
|
}
|
||||||
77
dvx/widgets/widgetButton.c
Normal file
77
dvx/widgets/widgetButton.c
Normal 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
469
dvx/widgets/widgetCanvas.c
Normal 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);
|
||||||
|
}
|
||||||
81
dvx/widgets/widgetCheckbox.c
Normal file
81
dvx/widgets/widgetCheckbox.c
Normal 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);
|
||||||
|
}
|
||||||
248
dvx/widgets/widgetComboBox.c
Normal file
248
dvx/widgets/widgetComboBox.c
Normal 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
258
dvx/widgets/widgetCore.c
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
188
dvx/widgets/widgetDropdown.c
Normal file
188
dvx/widgets/widgetDropdown.c
Normal 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
570
dvx/widgets/widgetEvent.c
Normal 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
170
dvx/widgets/widgetImage.c
Normal 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);
|
||||||
|
}
|
||||||
158
dvx/widgets/widgetInternal.h
Normal file
158
dvx/widgets/widgetInternal.h
Normal 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
42
dvx/widgets/widgetLabel.c
Normal 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
389
dvx/widgets/widgetLayout.c
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
62
dvx/widgets/widgetListBox.c
Normal file
62
dvx/widgets/widgetListBox.c
Normal 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
388
dvx/widgets/widgetOps.c
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
102
dvx/widgets/widgetProgressBar.c
Normal file
102
dvx/widgets/widgetProgressBar.c
Normal 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
102
dvx/widgets/widgetRadio.c
Normal 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);
|
||||||
|
}
|
||||||
53
dvx/widgets/widgetSeparator.c
Normal file
53
dvx/widgets/widgetSeparator.c
Normal 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
201
dvx/widgets/widgetSlider.c
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
dvx/widgets/widgetSpacer.c
Normal file
29
dvx/widgets/widgetSpacer.c
Normal 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;
|
||||||
|
}
|
||||||
43
dvx/widgets/widgetStatusBar.c
Normal file
43
dvx/widgets/widgetStatusBar.c
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
254
dvx/widgets/widgetTabControl.c
Normal file
254
dvx/widgets/widgetTabControl.c
Normal 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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
130
dvx/widgets/widgetTextInput.c
Normal file
130
dvx/widgets/widgetTextInput.c
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
dvx/widgets/widgetToolbar.c
Normal file
37
dvx/widgets/widgetToolbar.c
Normal 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);
|
||||||
|
}
|
||||||
364
dvx/widgets/widgetTreeView.c
Normal file
364
dvx/widgets/widgetTreeView.c
Normal 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);
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue