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 |
|
||||
| `dvxCursor.h` | Mouse cursor bitmask data (5 shapes) |
|
||||
| `dvxPalette.h` | Default VGA palette for 8-bit mode |
|
||||
| `dvxIcon.c` | stb_image implementation unit (BMP/TGA/PNG) |
|
||||
| `dvxIcon.c` | stb_image implementation unit (BMP/PNG/JPEG/GIF) |
|
||||
| `thirdparty/stb_image.h` | Third-party single-header image loader |
|
||||
|
||||
## Quick start
|
||||
|
|
@ -379,7 +379,17 @@ WidgetT *wgtHBox(WidgetT *parent); // horizontal stack
|
|||
WidgetT *wgtFrame(WidgetT *parent, const char *title); // titled border
|
||||
```
|
||||
Containers hold child widgets and control layout direction. `wgtFrame`
|
||||
draws a beveled border with a title label and lays out children vertically.
|
||||
draws a styled border with a title label and lays out children vertically.
|
||||
|
||||
Frame styles (set `w->as.frame.style` after creation):
|
||||
|
||||
| Style | Description |
|
||||
|-------|-------------|
|
||||
| `FrameInE` | Beveled inward / sunken (default) |
|
||||
| `FrameOutE` | Beveled outward / raised |
|
||||
| `FrameFlatE` | Solid color line; set `w->as.frame.color` (0 = windowShadow) |
|
||||
|
||||
The title text is vertically centered on the top border line.
|
||||
|
||||
Container properties (set directly on the returned `WidgetT *`):
|
||||
|
||||
67
dvx/Makefile
67
dvx/Makefile
|
|
@ -6,25 +6,58 @@ CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586
|
|||
LDFLAGS = -lm
|
||||
|
||||
OBJDIR = ../obj
|
||||
WOBJDIR = ../obj/widgets
|
||||
BINDIR = ../bin
|
||||
|
||||
SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxIcon.c dvxWidget.c dvxApp.c demo.c
|
||||
SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxIcon.c dvxImageWrite.c dvxApp.c demo.c
|
||||
|
||||
WSRCS = widgets/widgetCore.c \
|
||||
widgets/widgetLayout.c \
|
||||
widgets/widgetEvent.c \
|
||||
widgets/widgetOps.c \
|
||||
widgets/widgetBox.c \
|
||||
widgets/widgetButton.c \
|
||||
widgets/widgetCheckbox.c \
|
||||
widgets/widgetComboBox.c \
|
||||
widgets/widgetDropdown.c \
|
||||
widgets/widgetCanvas.c \
|
||||
widgets/widgetImage.c \
|
||||
widgets/widgetLabel.c \
|
||||
widgets/widgetListBox.c \
|
||||
widgets/widgetProgressBar.c \
|
||||
widgets/widgetRadio.c \
|
||||
widgets/widgetSeparator.c \
|
||||
widgets/widgetSlider.c \
|
||||
widgets/widgetSpacer.c \
|
||||
widgets/widgetStatusBar.c \
|
||||
widgets/widgetTabControl.c \
|
||||
widgets/widgetTextInput.c \
|
||||
widgets/widgetToolbar.c \
|
||||
widgets/widgetTreeView.c
|
||||
|
||||
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
|
||||
WOBJS = $(patsubst widgets/%.c,$(WOBJDIR)/%.o,$(WSRCS))
|
||||
TARGET = $(BINDIR)/demo.exe
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) | $(BINDIR)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS)
|
||||
$(TARGET): $(OBJS) $(WOBJS) | $(BINDIR)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS) $(WOBJS)
|
||||
|
||||
$(OBJDIR)/%.o: %.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(WOBJDIR)/%.o: widgets/%.c | $(WOBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR):
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
$(WOBJDIR):
|
||||
mkdir -p $(WOBJDIR)
|
||||
|
||||
$(BINDIR):
|
||||
mkdir -p $(BINDIR)
|
||||
|
||||
|
|
@ -34,9 +67,35 @@ $(OBJDIR)/dvxDraw.o: dvxDraw.c dvxDraw.h dvxTypes.h
|
|||
$(OBJDIR)/dvxComp.o: dvxComp.c dvxComp.h dvxTypes.h
|
||||
$(OBJDIR)/dvxWm.o: dvxWm.c dvxWm.h dvxTypes.h dvxDraw.h dvxComp.h dvxVideo.h thirdparty/stb_image.h
|
||||
$(OBJDIR)/dvxIcon.o: dvxIcon.c thirdparty/stb_image.h
|
||||
$(OBJDIR)/dvxWidget.o: dvxWidget.c dvxWidget.h dvxTypes.h dvxApp.h dvxDraw.h dvxWm.h dvxVideo.h
|
||||
$(OBJDIR)/dvxImageWrite.o: dvxImageWrite.c thirdparty/stb_image_write.h
|
||||
$(OBJDIR)/dvxApp.o: dvxApp.c dvxApp.h dvxTypes.h dvxVideo.h dvxDraw.h dvxComp.h dvxWm.h dvxFont.h dvxCursor.h
|
||||
$(OBJDIR)/demo.o: demo.c dvxApp.h dvxWidget.h
|
||||
|
||||
# Widget file dependencies
|
||||
WIDGET_DEPS = widgets/widgetInternal.h dvxWidget.h dvxTypes.h dvxApp.h dvxDraw.h dvxWm.h dvxVideo.h
|
||||
$(WOBJDIR)/widgetCore.o: widgets/widgetCore.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetLayout.o: widgets/widgetLayout.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetEvent.o: widgets/widgetEvent.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetOps.o: widgets/widgetOps.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetBox.o: widgets/widgetBox.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetCanvas.o: widgets/widgetCanvas.c $(WIDGET_DEPS) thirdparty/stb_image.h thirdparty/stb_image_write.h
|
||||
$(WOBJDIR)/widgetButton.o: widgets/widgetButton.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetCheckbox.o: widgets/widgetCheckbox.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetComboBox.o: widgets/widgetComboBox.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetDropdown.o: widgets/widgetDropdown.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetImage.o: widgets/widgetImage.c $(WIDGET_DEPS) thirdparty/stb_image.h
|
||||
$(WOBJDIR)/widgetLabel.o: widgets/widgetLabel.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetListBox.o: widgets/widgetListBox.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetProgressBar.o: widgets/widgetProgressBar.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetRadio.o: widgets/widgetRadio.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetSeparator.o: widgets/widgetSeparator.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetSlider.o: widgets/widgetSlider.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetSpacer.o: widgets/widgetSpacer.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetStatusBar.o: widgets/widgetStatusBar.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetTabControl.o: widgets/widgetTabControl.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetTextInput.o: widgets/widgetTextInput.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetToolbar.o: widgets/widgetToolbar.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetTreeView.o: widgets/widgetTreeView.c $(WIDGET_DEPS)
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(BINDIR)
|
||||
|
|
|
|||
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 onPaintPattern(WindowT *win, RectT *dirtyArea);
|
||||
static void onPaintText(WindowT *win, RectT *dirtyArea);
|
||||
static void setupWidgetDemo2(AppContextT *ctx);
|
||||
static void setupWindows(AppContextT *ctx);
|
||||
|
||||
|
||||
|
|
@ -249,6 +250,92 @@ static void onPaintText(WindowT *win, RectT *dirtyArea) {
|
|||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// setupWidgetDemo2
|
||||
// ============================================================
|
||||
|
||||
static const char *colorItems[] = {"Red", "Green", "Blue", "Yellow", "Cyan", "Magenta"};
|
||||
static const char *sizeItems[] = {"Small", "Medium", "Large", "Extra Large"};
|
||||
|
||||
static void setupWidgetDemo2(AppContextT *ctx) {
|
||||
WindowT *win = dvxCreateWindow(ctx, "Advanced Widgets", 380, 50, 320, 400, true);
|
||||
|
||||
if (!win) {
|
||||
return;
|
||||
}
|
||||
|
||||
win->userData = ctx;
|
||||
win->onClose = onCloseCb;
|
||||
|
||||
WidgetT *root = wgtInitWindow(ctx, win);
|
||||
|
||||
// TabControl at top
|
||||
WidgetT *tabs = wgtTabControl(root);
|
||||
|
||||
// --- Tab 1: Controls ---
|
||||
WidgetT *page1 = wgtTabPage(tabs, "Controls");
|
||||
|
||||
WidgetT *ddRow = wgtHBox(page1);
|
||||
wgtLabel(ddRow, "Color:");
|
||||
WidgetT *dd = wgtDropdown(ddRow);
|
||||
wgtDropdownSetItems(dd, colorItems, 6);
|
||||
wgtDropdownSetSelected(dd, 0);
|
||||
|
||||
WidgetT *cbRow = wgtHBox(page1);
|
||||
wgtLabel(cbRow, "Size:");
|
||||
WidgetT *cb = wgtComboBox(cbRow, 32);
|
||||
wgtComboBoxSetItems(cb, sizeItems, 4);
|
||||
wgtComboBoxSetSelected(cb, 1);
|
||||
|
||||
wgtHSeparator(page1);
|
||||
|
||||
wgtLabel(page1, "Progress:");
|
||||
WidgetT *pb = wgtProgressBar(page1);
|
||||
wgtProgressBarSetValue(pb, 65);
|
||||
|
||||
wgtLabel(page1, "Volume:");
|
||||
wgtSlider(page1, 0, 100);
|
||||
|
||||
// --- Tab 2: Tree ---
|
||||
WidgetT *page2 = wgtTabPage(tabs, "Tree");
|
||||
|
||||
WidgetT *tree = wgtTreeView(page2);
|
||||
WidgetT *docs = wgtTreeItem(tree, "Documents");
|
||||
wgtTreeItemSetExpanded(docs, true);
|
||||
wgtTreeItem(docs, "README.md");
|
||||
wgtTreeItem(docs, "DESIGN.md");
|
||||
WidgetT *src = wgtTreeItem(docs, "src");
|
||||
wgtTreeItemSetExpanded(src, true);
|
||||
wgtTreeItem(src, "main.c");
|
||||
wgtTreeItem(src, "utils.c");
|
||||
wgtTreeItem(src, "render.c");
|
||||
WidgetT *images = wgtTreeItem(tree, "Images");
|
||||
wgtTreeItem(images, "logo.png");
|
||||
wgtTreeItem(images, "icon.bmp");
|
||||
WidgetT *config = wgtTreeItem(tree, "Config");
|
||||
wgtTreeItem(config, "settings.ini");
|
||||
wgtTreeItem(config, "palette.dat");
|
||||
|
||||
// --- Tab 3: Toolbar ---
|
||||
WidgetT *page3 = wgtTabPage(tabs, "Toolbar");
|
||||
|
||||
WidgetT *tb = wgtToolbar(page3);
|
||||
wgtButton(tb, "New");
|
||||
wgtButton(tb, "Open");
|
||||
wgtButton(tb, "Save");
|
||||
|
||||
wgtLabel(page3, "Toolbar with buttons above.");
|
||||
|
||||
// Status bar at bottom (outside tabs)
|
||||
WidgetT *sb = wgtStatusBar(root);
|
||||
WidgetT *sbLabel = wgtLabel(sb, "Ready");
|
||||
sbLabel->weight = 100;
|
||||
wgtLabel(sb, "Line 1, Col 1");
|
||||
|
||||
wgtInvalidate(root);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// setupWindows
|
||||
// ============================================================
|
||||
|
|
@ -386,6 +473,7 @@ int main(void) {
|
|||
}
|
||||
|
||||
setupWindows(&ctx);
|
||||
setupWidgetDemo2(&ctx);
|
||||
|
||||
dvxRun(&ctx);
|
||||
|
||||
|
|
|
|||
|
|
@ -703,12 +703,12 @@ static void initColorScheme(AppContextT *ctx) {
|
|||
ctx->colors.desktop = packColor(d, 0, 128, 128); // GEOS teal desktop
|
||||
ctx->colors.windowFace = packColor(d, 192, 192, 192); // standard Motif grey
|
||||
ctx->colors.windowHighlight = packColor(d, 255, 255, 255);
|
||||
ctx->colors.windowShadow = packColor(d, 80, 80, 80);
|
||||
ctx->colors.windowShadow = packColor(d, 128, 128, 128);
|
||||
ctx->colors.activeTitleBg = packColor(d, 48, 48, 48); // GEOS dark charcoal
|
||||
ctx->colors.activeTitleFg = packColor(d, 255, 255, 255);
|
||||
ctx->colors.inactiveTitleBg = packColor(d, 160, 160, 160); // lighter grey
|
||||
ctx->colors.inactiveTitleFg = packColor(d, 64, 64, 64);
|
||||
ctx->colors.contentBg = packColor(d, 255, 255, 255);
|
||||
ctx->colors.contentBg = packColor(d, 192, 192, 192);
|
||||
ctx->colors.contentFg = packColor(d, 0, 0, 0);
|
||||
ctx->colors.menuBg = packColor(d, 192, 192, 192);
|
||||
ctx->colors.menuFg = packColor(d, 0, 0, 0);
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@
|
|||
#pragma GCC diagnostic ignored "-Wunused-function"
|
||||
|
||||
#define STBI_ONLY_BMP
|
||||
#define STBI_ONLY_TGA
|
||||
#define STBI_ONLY_PNG
|
||||
#define STBI_ONLY_JPEG
|
||||
#define STBI_ONLY_GIF
|
||||
#define STBI_NO_SIMD
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#include "thirdparty/stb_image.h"
|
||||
|
|
|
|||
10
dvx/dvxImageWrite.c
Normal file
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
193
dvx/dvxWidget.h
193
dvx/dvxWidget.h
|
|
@ -50,7 +50,19 @@ typedef enum {
|
|||
WidgetListBoxE,
|
||||
WidgetSpacerE,
|
||||
WidgetSeparatorE,
|
||||
WidgetFrameE
|
||||
WidgetFrameE,
|
||||
WidgetDropdownE,
|
||||
WidgetComboBoxE,
|
||||
WidgetProgressBarE,
|
||||
WidgetSliderE,
|
||||
WidgetTabControlE,
|
||||
WidgetTabPageE,
|
||||
WidgetStatusBarE,
|
||||
WidgetToolbarE,
|
||||
WidgetTreeViewE,
|
||||
WidgetTreeItemE,
|
||||
WidgetImageE,
|
||||
WidgetCanvasE
|
||||
} WidgetTypeE;
|
||||
|
||||
// ============================================================
|
||||
|
|
@ -67,6 +79,16 @@ typedef enum {
|
|||
AlignEndE
|
||||
} WidgetAlignE;
|
||||
|
||||
// ============================================================
|
||||
// Frame style enum
|
||||
// ============================================================
|
||||
|
||||
typedef enum {
|
||||
FrameInE, // beveled inward (sunken) — default
|
||||
FrameOutE, // beveled outward (raised)
|
||||
FrameFlatE // solid color line
|
||||
} FrameStyleE;
|
||||
|
||||
// ============================================================
|
||||
// Widget structure
|
||||
// ============================================================
|
||||
|
|
@ -108,6 +130,10 @@ typedef struct WidgetT {
|
|||
int32_t spacing; // tagged size for spacing between children (0 = default)
|
||||
int32_t padding; // tagged size for internal padding (0 = default)
|
||||
|
||||
// Colors (0 = use color scheme defaults)
|
||||
uint32_t fgColor;
|
||||
uint32_t bgColor;
|
||||
|
||||
// State
|
||||
bool visible;
|
||||
bool enabled;
|
||||
|
|
@ -173,7 +199,80 @@ typedef struct WidgetT {
|
|||
|
||||
struct {
|
||||
const char *title;
|
||||
FrameStyleE style; // FrameInE (default), FrameOutE, FrameFlatE
|
||||
uint32_t color; // border color for FrameFlatE (0 = use windowShadow)
|
||||
} frame;
|
||||
|
||||
struct {
|
||||
const char **items;
|
||||
int32_t itemCount;
|
||||
int32_t selectedIdx;
|
||||
bool open;
|
||||
int32_t hoverIdx;
|
||||
int32_t scrollPos;
|
||||
} dropdown;
|
||||
|
||||
struct {
|
||||
char *buf;
|
||||
int32_t bufSize;
|
||||
int32_t len;
|
||||
int32_t cursorPos;
|
||||
int32_t scrollOff;
|
||||
const char **items;
|
||||
int32_t itemCount;
|
||||
int32_t selectedIdx;
|
||||
bool open;
|
||||
int32_t hoverIdx;
|
||||
int32_t listScrollPos;
|
||||
} comboBox;
|
||||
|
||||
struct {
|
||||
int32_t value;
|
||||
int32_t maxValue;
|
||||
} progressBar;
|
||||
|
||||
struct {
|
||||
int32_t value;
|
||||
int32_t minValue;
|
||||
int32_t maxValue;
|
||||
bool vertical;
|
||||
} slider;
|
||||
|
||||
struct {
|
||||
int32_t activeTab;
|
||||
} tabControl;
|
||||
|
||||
struct {
|
||||
const char *title;
|
||||
} tabPage;
|
||||
|
||||
struct {
|
||||
int32_t scrollPos;
|
||||
} treeView;
|
||||
|
||||
struct {
|
||||
const char *text;
|
||||
bool expanded;
|
||||
} treeItem;
|
||||
|
||||
struct {
|
||||
uint8_t *data; // pixel buffer in display format
|
||||
int32_t imgW;
|
||||
int32_t imgH;
|
||||
int32_t imgPitch;
|
||||
bool pressed;
|
||||
} image;
|
||||
|
||||
struct {
|
||||
uint8_t *data; // pixel buffer in display format
|
||||
int32_t canvasW;
|
||||
int32_t canvasH;
|
||||
int32_t canvasPitch;
|
||||
uint32_t penColor;
|
||||
int32_t penSize;
|
||||
int32_t lastX;
|
||||
int32_t lastY;
|
||||
} canvas;
|
||||
} as;
|
||||
} WidgetT;
|
||||
|
||||
|
|
@ -224,6 +323,98 @@ WidgetT *wgtVSeparator(WidgetT *parent);
|
|||
WidgetT *wgtListBox(WidgetT *parent);
|
||||
WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen);
|
||||
|
||||
// ============================================================
|
||||
// Dropdown and ComboBox
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtDropdown(WidgetT *parent);
|
||||
void wgtDropdownSetItems(WidgetT *w, const char **items, int32_t count);
|
||||
int32_t wgtDropdownGetSelected(const WidgetT *w);
|
||||
void wgtDropdownSetSelected(WidgetT *w, int32_t idx);
|
||||
|
||||
WidgetT *wgtComboBox(WidgetT *parent, int32_t maxLen);
|
||||
void wgtComboBoxSetItems(WidgetT *w, const char **items, int32_t count);
|
||||
int32_t wgtComboBoxGetSelected(const WidgetT *w);
|
||||
void wgtComboBoxSetSelected(WidgetT *w, int32_t idx);
|
||||
|
||||
// ============================================================
|
||||
// ProgressBar
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtProgressBar(WidgetT *parent);
|
||||
void wgtProgressBarSetValue(WidgetT *w, int32_t value);
|
||||
int32_t wgtProgressBarGetValue(const WidgetT *w);
|
||||
|
||||
// ============================================================
|
||||
// Slider (TrackBar)
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtSlider(WidgetT *parent, int32_t minVal, int32_t maxVal);
|
||||
void wgtSliderSetValue(WidgetT *w, int32_t value);
|
||||
int32_t wgtSliderGetValue(const WidgetT *w);
|
||||
|
||||
// ============================================================
|
||||
// TabControl
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtTabControl(WidgetT *parent);
|
||||
WidgetT *wgtTabPage(WidgetT *parent, const char *title);
|
||||
void wgtTabControlSetActive(WidgetT *w, int32_t idx);
|
||||
int32_t wgtTabControlGetActive(const WidgetT *w);
|
||||
|
||||
// ============================================================
|
||||
// StatusBar and Toolbar
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtStatusBar(WidgetT *parent);
|
||||
WidgetT *wgtToolbar(WidgetT *parent);
|
||||
|
||||
// ============================================================
|
||||
// TreeView
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtTreeView(WidgetT *parent);
|
||||
WidgetT *wgtTreeItem(WidgetT *parent, const char *text);
|
||||
void wgtTreeItemSetExpanded(WidgetT *w, bool expanded);
|
||||
bool wgtTreeItemIsExpanded(const WidgetT *w);
|
||||
|
||||
// ============================================================
|
||||
// Image
|
||||
// ============================================================
|
||||
|
||||
// Create an image widget from raw pixel data (display format).
|
||||
// Takes ownership of the data buffer (freed on destroy).
|
||||
WidgetT *wgtImage(WidgetT *parent, uint8_t *data, int32_t w, int32_t h, int32_t pitch);
|
||||
|
||||
// Load an image widget from a file (BMP, PNG, JPEG, GIF).
|
||||
// Returns NULL on load failure.
|
||||
WidgetT *wgtImageFromFile(WidgetT *parent, const char *path);
|
||||
|
||||
// Replace the image data. Takes ownership of the new buffer.
|
||||
void wgtImageSetData(WidgetT *w, uint8_t *data, int32_t imgW, int32_t imgH, int32_t pitch);
|
||||
|
||||
// ============================================================
|
||||
// Canvas
|
||||
// ============================================================
|
||||
|
||||
// Create a drawable canvas widget with the given dimensions.
|
||||
WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h);
|
||||
|
||||
// Clear the canvas to the specified color.
|
||||
void wgtCanvasClear(WidgetT *w, uint32_t color);
|
||||
|
||||
// Set the pen color (in display pixel format).
|
||||
void wgtCanvasSetPenColor(WidgetT *w, uint32_t color);
|
||||
|
||||
// Set the pen size in pixels (diameter).
|
||||
void wgtCanvasSetPenSize(WidgetT *w, int32_t size);
|
||||
|
||||
// Save the canvas to a PNG file. Returns 0 on success, -1 on failure.
|
||||
int32_t wgtCanvasSave(WidgetT *w, const char *path);
|
||||
|
||||
// Load a PNG file onto the canvas. Returns 0 on success, -1 on failure.
|
||||
int32_t wgtCanvasLoad(WidgetT *w, const char *path);
|
||||
|
||||
// ============================================================
|
||||
// Operations
|
||||
// ============================================================
|
||||
|
|
|
|||
1724
dvx/thirdparty/stb_image_write.h
vendored
Normal file
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