From 76a955a9e7c5e46bc91614f42dceb144c465034f Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Mon, 9 Mar 2026 22:50:06 -0500 Subject: [PATCH] More widgets! --- dvx/README.md => README.md | 14 +- dvx/Makefile | 75 +- dvx/demo.c | 88 ++ dvx/dvxApp.c | 4 +- dvx/dvxIcon.c | 3 +- dvx/dvxImageWrite.c | 10 + dvx/dvxWidget.c | 1634 ---------------------------- dvx/dvxWidget.h | 195 +++- dvx/thirdparty/stb_image_write.h | 1724 ++++++++++++++++++++++++++++++ dvx/widgets/widgetBox.c | 97 ++ dvx/widgets/widgetButton.c | 77 ++ dvx/widgets/widgetCanvas.c | 469 ++++++++ dvx/widgets/widgetCheckbox.c | 81 ++ dvx/widgets/widgetComboBox.c | 248 +++++ dvx/widgets/widgetCore.c | 258 +++++ dvx/widgets/widgetDropdown.c | 188 ++++ dvx/widgets/widgetEvent.c | 570 ++++++++++ dvx/widgets/widgetImage.c | 170 +++ dvx/widgets/widgetInternal.h | 158 +++ dvx/widgets/widgetLabel.c | 42 + dvx/widgets/widgetLayout.c | 389 +++++++ dvx/widgets/widgetListBox.c | 62 ++ dvx/widgets/widgetOps.c | 388 +++++++ dvx/widgets/widgetProgressBar.c | 102 ++ dvx/widgets/widgetRadio.c | 102 ++ dvx/widgets/widgetSeparator.c | 53 + dvx/widgets/widgetSlider.c | 201 ++++ dvx/widgets/widgetSpacer.c | 29 + dvx/widgets/widgetStatusBar.c | 43 + dvx/widgets/widgetTabControl.c | 254 +++++ dvx/widgets/widgetTextInput.c | 130 +++ dvx/widgets/widgetToolbar.c | 37 + dvx/widgets/widgetTreeView.c | 364 +++++++ 33 files changed, 6610 insertions(+), 1649 deletions(-) rename dvx/README.md => README.md (97%) create mode 100644 dvx/dvxImageWrite.c delete mode 100644 dvx/dvxWidget.c create mode 100644 dvx/thirdparty/stb_image_write.h create mode 100644 dvx/widgets/widgetBox.c create mode 100644 dvx/widgets/widgetButton.c create mode 100644 dvx/widgets/widgetCanvas.c create mode 100644 dvx/widgets/widgetCheckbox.c create mode 100644 dvx/widgets/widgetComboBox.c create mode 100644 dvx/widgets/widgetCore.c create mode 100644 dvx/widgets/widgetDropdown.c create mode 100644 dvx/widgets/widgetEvent.c create mode 100644 dvx/widgets/widgetImage.c create mode 100644 dvx/widgets/widgetInternal.h create mode 100644 dvx/widgets/widgetLabel.c create mode 100644 dvx/widgets/widgetLayout.c create mode 100644 dvx/widgets/widgetListBox.c create mode 100644 dvx/widgets/widgetOps.c create mode 100644 dvx/widgets/widgetProgressBar.c create mode 100644 dvx/widgets/widgetRadio.c create mode 100644 dvx/widgets/widgetSeparator.c create mode 100644 dvx/widgets/widgetSlider.c create mode 100644 dvx/widgets/widgetSpacer.c create mode 100644 dvx/widgets/widgetStatusBar.c create mode 100644 dvx/widgets/widgetTabControl.c create mode 100644 dvx/widgets/widgetTextInput.c create mode 100644 dvx/widgets/widgetToolbar.c create mode 100644 dvx/widgets/widgetTreeView.c diff --git a/dvx/README.md b/README.md similarity index 97% rename from dvx/README.md rename to README.md index 2a4a109..061465b 100644 --- a/dvx/README.md +++ b/README.md @@ -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 *`): diff --git a/dvx/Makefile b/dvx/Makefile index 8758aa7..19cfef3 100644 --- a/dvx/Makefile +++ b/dvx/Makefile @@ -5,26 +5,59 @@ CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 LDFLAGS = -lm -OBJDIR = ../obj -BINDIR = ../bin +OBJDIR = ../obj +WOBJDIR = ../obj/widgets +BINDIR = ../bin + +SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxIcon.c dvxImageWrite.c dvxApp.c demo.c + +WSRCS = widgets/widgetCore.c \ + widgets/widgetLayout.c \ + widgets/widgetEvent.c \ + widgets/widgetOps.c \ + widgets/widgetBox.c \ + widgets/widgetButton.c \ + widgets/widgetCheckbox.c \ + widgets/widgetComboBox.c \ + widgets/widgetDropdown.c \ + widgets/widgetCanvas.c \ + widgets/widgetImage.c \ + widgets/widgetLabel.c \ + widgets/widgetListBox.c \ + widgets/widgetProgressBar.c \ + widgets/widgetRadio.c \ + widgets/widgetSeparator.c \ + widgets/widgetSlider.c \ + widgets/widgetSpacer.c \ + widgets/widgetStatusBar.c \ + widgets/widgetTabControl.c \ + widgets/widgetTextInput.c \ + widgets/widgetToolbar.c \ + widgets/widgetTreeView.c -SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxIcon.c dvxWidget.c dvxApp.c demo.c OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) +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) @@ -33,10 +66,36 @@ $(OBJDIR)/dvxVideo.o: dvxVideo.c dvxVideo.h dvxTypes.h dvxPalette.h $(OBJDIR)/dvxDraw.o: dvxDraw.c dvxDraw.h dvxTypes.h $(OBJDIR)/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)/dvxApp.o: dvxApp.c dvxApp.h dvxTypes.h dvxVideo.h dvxDraw.h dvxComp.h dvxWm.h dvxFont.h dvxCursor.h +$(OBJDIR)/dvxIcon.o: dvxIcon.c thirdparty/stb_image.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) diff --git a/dvx/demo.c b/dvx/demo.c index 64bf3d3..3d8cf41 100644 --- a/dvx/demo.c +++ b/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); diff --git a/dvx/dvxApp.c b/dvx/dvxApp.c index 380ab75..3d9650e 100644 --- a/dvx/dvxApp.c +++ b/dvx/dvxApp.c @@ -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); diff --git a/dvx/dvxIcon.c b/dvx/dvxIcon.c index ecc3f5d..420dfbd 100644 --- a/dvx/dvxIcon.c +++ b/dvx/dvxIcon.c @@ -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" diff --git a/dvx/dvxImageWrite.c b/dvx/dvxImageWrite.c new file mode 100644 index 0000000..e6cfc0b --- /dev/null +++ b/dvx/dvxImageWrite.c @@ -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 diff --git a/dvx/dvxWidget.c b/dvx/dvxWidget.c deleted file mode 100644 index 86e6a1a..0000000 --- a/dvx/dvxWidget.c +++ /dev/null @@ -1,1634 +0,0 @@ -// dvxWidget.c — Widget system for DV/X GUI - -#include "dvxWidget.h" -#include "dvxApp.h" -#include "dvxDraw.h" -#include "dvxWm.h" -#include "dvxVideo.h" - -#include -#include -#include - -// ============================================================ -// 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_BORDER 2 -#define FRAME_TITLE_GAP 4 -#define TEXT_INPUT_PAD 3 - -// ============================================================ -// Prototypes -// ============================================================ - -static void addChild(WidgetT *parent, WidgetT *child); -static WidgetT *allocWidget(WidgetT *parent, WidgetTypeE type); -static void calcMinSizeBox(WidgetT *w, const BitmapFontT *font); -static void calcMinSizeLeaf(WidgetT *w, const BitmapFontT *font); -static void calcMinSizeTree(WidgetT *w, const BitmapFontT *font); -static int32_t countVisibleChildren(const WidgetT *w); -static void destroyChildren(WidgetT *w); -static WidgetT *hitTest(WidgetT *w, int32_t x, int32_t y); -static void layoutBox(WidgetT *w, const BitmapFontT *font); -static void layoutChildren(WidgetT *w, const BitmapFontT *font); -static void paintWidget(WidgetT *w, DisplayT *d, const BlitOpsT *ops, - const BitmapFontT *font, const ColorSchemeT *colors); -static void removeChild(WidgetT *parent, WidgetT *child); -static void widgetManageScrollbars(WindowT *win, AppContextT *ctx); -static void widgetOnKey(WindowT *win, int32_t key, int32_t mod); -static void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons); -static void widgetOnPaint(WindowT *win, RectT *dirtyArea); -static void widgetOnResize(WindowT *win, int32_t newW, int32_t newH); -static void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value); - - -// ============================================================ -// addChild -// ============================================================ - -static void addChild(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; - } -} - - -// ============================================================ -// allocWidget -// ============================================================ - -static WidgetT *allocWidget(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; - addChild(parent, w); - } - - return w; -} - - -// ============================================================ -// calcMinSizeBox -// ============================================================ - -static void calcMinSizeBox(WidgetT *w, const BitmapFontT *font) { - bool horiz = (w->type == WidgetHBoxE); // RadioGroupE and VBoxE are vertical - 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 adds title height and border - int32_t frameExtraTop = 0; - - if (w->type == WidgetFrameE) { - frameExtraTop = font->charHeight + FRAME_TITLE_GAP; - pad = DEFAULT_PADDING; - } - - for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { - if (!c->visible) { - continue; - } - - calcMinSizeTree(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) { - w->calcMinW += FRAME_BORDER * 2; - w->calcMinH += FRAME_BORDER * 2; - } -} - - -// ============================================================ -// calcMinSizeLeaf -// ============================================================ - -static void calcMinSizeLeaf(WidgetT *w, const BitmapFontT *font) { - switch (w->type) { - case WidgetLabelE: - w->calcMinW = (int32_t)strlen(w->as.label.text) * font->charWidth; - w->calcMinH = font->charHeight; - break; - - case WidgetButtonE: - w->calcMinW = (int32_t)strlen(w->as.button.text) * font->charWidth + BUTTON_PAD_H * 2; - w->calcMinH = font->charHeight + BUTTON_PAD_V * 2; - break; - - case WidgetCheckboxE: - 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); - break; - - case WidgetRadioE: - 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); - break; - - case WidgetTextInputE: - w->calcMinW = font->charWidth * 8 + TEXT_INPUT_PAD * 2; - w->calcMinH = font->charHeight + TEXT_INPUT_PAD * 2; - break; - - case WidgetSpacerE: - w->calcMinW = 0; - w->calcMinH = 0; - break; - - case WidgetSeparatorE: - if (w->as.separator.vertical) { - w->calcMinW = SEPARATOR_THICKNESS; - w->calcMinH = 0; - } else { - w->calcMinW = 0; - w->calcMinH = SEPARATOR_THICKNESS; - } - break; - - default: - w->calcMinW = 0; - w->calcMinH = 0; - break; - } -} - - -// ============================================================ -// calcMinSizeTree -// ============================================================ - -static void calcMinSizeTree(WidgetT *w, const BitmapFontT *font) { - if (w->type == WidgetVBoxE || w->type == WidgetHBoxE || w->type == WidgetFrameE || w->type == WidgetRadioGroupE) { - calcMinSizeBox(w, font); - } else { - calcMinSizeLeaf(w, font); - } - - // 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; - } - } -} - - -// ============================================================ -// countVisibleChildren -// ============================================================ - -static int32_t countVisibleChildren(const WidgetT *w) { - int32_t count = 0; - - for (const WidgetT *c = w->firstChild; c; c = c->nextSibling) { - if (c->visible) { - count++; - } - } - - return count; -} - - -// ============================================================ -// destroyChildren -// ============================================================ - -static void destroyChildren(WidgetT *w) { - WidgetT *child = w->firstChild; - - while (child) { - WidgetT *next = child->nextSibling; - destroyChildren(child); - - if (child->type == WidgetTextInputE) { - free(child->as.textInput.buf); - } else if (child->type == WidgetTextAreaE) { - free(child->as.textArea.buf); - } - - free(child); - child = next; - } - - w->firstChild = NULL; - w->lastChild = NULL; -} - - -// ============================================================ -// hitTest -// ============================================================ - -static WidgetT *hitTest(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 in reverse order (last child is on top) - // Walk to last visible child, then check backwards - // Since we use a singly-linked list, just check all and take the last match - WidgetT *hit = NULL; - - for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { - WidgetT *childHit = hitTest(c, x, y); - - if (childHit) { - hit = childHit; - } - } - - return hit ? hit : w; -} - - -// ============================================================ -// layoutBox -// ============================================================ - -static void layoutBox(WidgetT *w, const BitmapFontT *font) { - bool horiz = (w->type == WidgetHBoxE); - 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 frameBorder = 0; - - if (w->type == WidgetFrameE) { - frameExtraTop = font->charHeight + FRAME_TITLE_GAP; - frameBorder = FRAME_BORDER; - pad = DEFAULT_PADDING; - } - - int32_t innerX = w->x + pad + frameBorder; - int32_t innerY = w->y + pad + frameBorder + frameExtraTop; - int32_t innerW = w->w - pad * 2 - frameBorder * 2; - int32_t innerH = w->h - pad * 2 - frameBorder * 2 - frameExtraTop; - - if (innerW < 0) { innerW = 0; } - if (innerH < 0) { innerH = 0; } - - int32_t count = countVisibleChildren(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 - layoutChildren(c, font); - } -} - - -// ============================================================ -// layoutChildren -// ============================================================ - -static void layoutChildren(WidgetT *w, const BitmapFontT *font) { - if (w->type == WidgetVBoxE || w->type == WidgetHBoxE || w->type == WidgetFrameE || w->type == WidgetRadioGroupE) { - layoutBox(w, font); - } -} - - -// ============================================================ -// paintWidget -// ============================================================ - -static void paintWidget(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: { - // Draw beveled border - BevelStyleT bevel; - bevel.highlight = colors->windowHighlight; - bevel.shadow = colors->windowShadow; - bevel.face = 0; - bevel.width = FRAME_BORDER; - drawBevel(d, ops, w->x, w->y + font->charHeight / 2, - w->w, w->h - font->charHeight / 2, &bevel); - - // Draw title over the top border - 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 + FRAME_BORDER; - - rectFill(d, ops, titleX - 2, w->y, - titleW + 4, font->charHeight, colors->windowFace); - drawText(d, ops, font, titleX, w->y, - w->as.frame.title, colors->contentFg, colors->windowFace, true); - } - - break; - } - - case WidgetLabelE: - drawText(d, ops, font, w->x, w->y + (w->h - font->charHeight) / 2, - w->as.label.text, colors->contentFg, colors->contentBg, false); - break; - - case WidgetButtonE: { - BevelStyleT bevel; - bevel.highlight = w->as.button.pressed ? colors->windowShadow : colors->windowHighlight; - bevel.shadow = w->as.button.pressed ? colors->windowHighlight : colors->windowShadow; - bevel.face = colors->buttonFace; - 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 ? colors->contentFg : colors->windowShadow, - colors->buttonFace, true); - break; - } - - case WidgetCheckboxE: { - 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 = colors->contentBg; - 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, colors->contentFg); - drawHLine(d, ops, cx + cs - 1 - i, cy + i, 1, colors->contentFg); - } - } - - // Draw label - drawText(d, ops, font, - w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP, - w->y + (w->h - font->charHeight) / 2, - w->as.checkbox.text, colors->contentFg, colors->contentBg, false); - break; - } - - case WidgetRadioE: { - int32_t boxY = w->y + (w->h - CHECKBOX_BOX_SIZE) / 2; - - // Draw radio box (same as checkbox for now, could use circle later) - BevelStyleT bevel; - bevel.highlight = colors->windowShadow; - bevel.shadow = colors->windowHighlight; - bevel.face = colors->contentBg; - 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, colors->contentFg); - } - - drawText(d, ops, font, - w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP, - w->y + (w->h - font->charHeight) / 2, - w->as.radio.text, colors->contentFg, colors->contentBg, false); - break; - } - - case WidgetTextInputE: { - // Sunken border - BevelStyleT bevel; - bevel.highlight = colors->windowShadow; - bevel.shadow = colors->windowHighlight; - bevel.face = colors->contentBg; - 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], - colors->contentFg, colors->contentBg, 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, colors->contentFg); - } - } - } - - break; - } - - case WidgetSpacerE: - // Invisible — draws nothing - break; - - case WidgetSeparatorE: - 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); - } - break; - - default: - break; - } - - // Paint children - for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { - paintWidget(c, d, ops, font, colors); - } -} - - -// ============================================================ -// removeChild -// ============================================================ - -static void removeChild(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; - } -} - - -// ============================================================ -// 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. - -static void widgetManageScrollbars(WindowT *win, AppContextT *ctx) { - WidgetT *root = win->widgetRoot; - - if (!root) { - return; - } - - // Measure the tree without any layout pass - calcMinSizeTree(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 -// ============================================================ - -static void widgetOnKey(WindowT *win, int32_t key, int32_t mod) { - (void)mod; - WidgetT *root = win->widgetRoot; - - if (!root) { - return; - } - - // Find the focused widget - // For now, simple: find a focused text input and send keys to it - // TODO: proper focus chain / tab navigation - WidgetT *focus = NULL; - - // Simple linear scan for focused widget - WidgetT *stack[64]; - int32_t top = 0; - stack[top++] = root; - - while (top > 0) { - WidgetT *w = stack[--top]; - - if (w->focused && w->type == WidgetTextInputE) { - focus = w; - break; - } - - for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { - if (top < 64) { - stack[top++] = c; - } - } - } - - if (!focus || focus->type != WidgetTextInputE) { - return; - } - - // Handle key input for text input widget - if (key >= 32 && key < 127) { - // Printable character - if (focus->as.textInput.len < focus->as.textInput.bufSize - 1) { - int32_t pos = focus->as.textInput.cursorPos; - - memmove(focus->as.textInput.buf + pos + 1, - focus->as.textInput.buf + pos, - focus->as.textInput.len - pos + 1); - focus->as.textInput.buf[pos] = (char)key; - focus->as.textInput.len++; - focus->as.textInput.cursorPos++; - - if (focus->onChange) { - focus->onChange(focus); - } - } - } else if (key == 8) { - // Backspace - if (focus->as.textInput.cursorPos > 0) { - int32_t pos = focus->as.textInput.cursorPos; - - memmove(focus->as.textInput.buf + pos - 1, - focus->as.textInput.buf + pos, - focus->as.textInput.len - pos + 1); - focus->as.textInput.len--; - focus->as.textInput.cursorPos--; - - if (focus->onChange) { - focus->onChange(focus); - } - } - } else if (key == (0x4B | 0x100)) { - // Left arrow - if (focus->as.textInput.cursorPos > 0) { - focus->as.textInput.cursorPos--; - } - } else if (key == (0x4D | 0x100)) { - // Right arrow - if (focus->as.textInput.cursorPos < focus->as.textInput.len) { - focus->as.textInput.cursorPos++; - } - } else if (key == (0x47 | 0x100)) { - // Home - focus->as.textInput.cursorPos = 0; - } else if (key == (0x4F | 0x100)) { - // End - focus->as.textInput.cursorPos = focus->as.textInput.len; - } else if (key == (0x53 | 0x100)) { - // Delete - if (focus->as.textInput.cursorPos < focus->as.textInput.len) { - int32_t pos = focus->as.textInput.cursorPos; - - memmove(focus->as.textInput.buf + pos, - focus->as.textInput.buf + pos + 1, - focus->as.textInput.len - pos); - focus->as.textInput.len--; - - 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 visibleChars = (focus->w - TEXT_INPUT_PAD * 2) / font->charWidth; - - if (focus->as.textInput.cursorPos < focus->as.textInput.scrollOff) { - focus->as.textInput.scrollOff = focus->as.textInput.cursorPos; - } - - if (focus->as.textInput.cursorPos >= focus->as.textInput.scrollOff + visibleChars) { - focus->as.textInput.scrollOff = focus->as.textInput.cursorPos - visibleChars + 1; - } - - // Repaint the window - wgtInvalidate(focus); -} - - -// ============================================================ -// widgetOnMouse -// ============================================================ - -static void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { - WidgetT *root = win->widgetRoot; - - if (!root || !(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 = hitTest(root, vx, vy); - - if (!hit) { - return; - } - - // Clear focus from all text inputs, set focus on clicked text input - WidgetT *stack[64]; - int32_t top = 0; - stack[top++] = root; - - while (top > 0) { - WidgetT *w = stack[--top]; - w->focused = false; - - for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { - if (top < 64) { - stack[top++] = c; - } - } - } - - if (hit->type == WidgetTextInputE) { - hit->focused = true; - - // Place cursor at click position - AppContextT *ctx = (AppContextT *)root->userData; - const BitmapFontT *font = &ctx->font; - int32_t relX = x - 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; - } - - if (hit->type == WidgetButtonE && hit->enabled) { - hit->as.button.pressed = true; - wgtInvalidate(hit); - - // The button release will be handled by the next mouse event - // For now, just fire onClick on press - if (hit->onClick) { - hit->onClick(hit); - } - - hit->as.button.pressed = false; - } - - if (hit->type == WidgetCheckboxE && hit->enabled) { - hit->as.checkbox.checked = !hit->as.checkbox.checked; - - if (hit->onChange) { - hit->onChange(hit); - } - } - - if (hit->type == WidgetRadioE && hit->enabled && hit->parent && - hit->parent->type == WidgetRadioGroupE) { - hit->parent->as.radioGroup.selectedIdx = hit->as.radio.index; - - if (hit->parent->onChange) { - hit->parent->onChange(hit->parent); - } - } - - wgtInvalidate(root); -} - - -// ============================================================ -// widgetOnPaint -// ============================================================ - -static 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; - layoutChildren(root, &ctx->font); - - // Paint widget tree (clip rect limits drawing to visible area) - wgtPaint(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors); -} - - -// ============================================================ -// widgetOnResize -// ============================================================ - -static 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 -// ============================================================ - -static 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); - } -} - - -// ============================================================ -// wgtButton -// ============================================================ - -WidgetT *wgtButton(WidgetT *parent, const char *text) { - WidgetT *w = allocWidget(parent, WidgetButtonE); - - if (w) { - w->as.button.text = text; - w->as.button.pressed = false; - } - - return w; -} - - -// ============================================================ -// wgtCheckbox -// ============================================================ - -WidgetT *wgtCheckbox(WidgetT *parent, const char *text) { - WidgetT *w = allocWidget(parent, WidgetCheckboxE); - - if (w) { - w->as.checkbox.text = text; - w->as.checkbox.checked = false; - } - - return w; -} - - -// ============================================================ -// wgtDestroy -// ============================================================ - -void wgtDestroy(WidgetT *w) { - if (!w) { - return; - } - - if (w->parent) { - removeChild(w->parent, w); - } - - destroyChildren(w); - - if (w->type == WidgetTextInputE) { - free(w->as.textInput.buf); - } else if (w->type == WidgetTextAreaE) { - free(w->as.textArea.buf); - } - - // 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; -} - - -// ============================================================ -// wgtFrame -// ============================================================ - -WidgetT *wgtFrame(WidgetT *parent, const char *title) { - WidgetT *w = allocWidget(parent, WidgetFrameE); - - if (w) { - w->as.frame.title = title; - } - - return w; -} - - -// ============================================================ -// 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 : ""; - default: return ""; - } -} - - -// ============================================================ -// wgtHBox -// ============================================================ - -WidgetT *wgtHBox(WidgetT *parent) { - return allocWidget(parent, WidgetHBoxE); -} - - -// ============================================================ -// wgtHSeparator -// ============================================================ - -WidgetT *wgtHSeparator(WidgetT *parent) { - WidgetT *w = allocWidget(parent, WidgetSeparatorE); - - if (w) { - w->as.separator.vertical = false; - } - - return w; -} - - -// ============================================================ -// wgtInitWindow -// ============================================================ - -WidgetT *wgtInitWindow(AppContextT *ctx, WindowT *win) { - WidgetT *root = allocWidget(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; - - // Layout and paint are deferred until the caller adds widgets - // and calls wgtInvalidate(root) or until the first resize/paint event. - - 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); -} - - -// ============================================================ -// wgtLabel -// ============================================================ - -WidgetT *wgtLabel(WidgetT *parent, const char *text) { - WidgetT *w = allocWidget(parent, WidgetLabelE); - - if (w) { - w->as.label.text = text; - } - - return w; -} - - -// ============================================================ -// wgtLayout -// ============================================================ - -void wgtLayout(WidgetT *root, int32_t availW, int32_t availH, - const BitmapFontT *font) { - if (!root) { - return; - } - - // Measure pass - calcMinSizeTree(root, font); - - // Layout pass - root->x = 0; - root->y = 0; - root->w = availW; - root->h = availH; - - layoutChildren(root, font); -} - - -// ============================================================ -// wgtListBox -// ============================================================ - -WidgetT *wgtListBox(WidgetT *parent) { - WidgetT *w = allocWidget(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; -} - - -// ============================================================ -// wgtPaint -// ============================================================ - -void wgtPaint(WidgetT *root, DisplayT *d, const BlitOpsT *ops, - const BitmapFontT *font, const ColorSchemeT *colors) { - if (!root) { - return; - } - - paintWidget(root, d, ops, font, colors); -} - - -// ============================================================ -// wgtRadio -// ============================================================ - -WidgetT *wgtRadio(WidgetT *parent, const char *text) { - WidgetT *w = allocWidget(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 = allocWidget(parent, WidgetRadioGroupE); - - if (w) { - w->as.radioGroup.selectedIdx = 0; - } - - return w; -} - - -// ============================================================ -// 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; - } -} - - -// ============================================================ -// 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; - - default: - break; - } -} - - -// ============================================================ -// wgtSetVisible -// ============================================================ - -void wgtSetVisible(WidgetT *w, bool visible) { - if (w) { - w->visible = visible; - } -} - - -// ============================================================ -// wgtSpacer -// ============================================================ - -WidgetT *wgtSpacer(WidgetT *parent) { - WidgetT *w = allocWidget(parent, WidgetSpacerE); - - if (w) { - w->weight = 100; // spacers stretch by default - } - - return w; -} - - -// ============================================================ -// wgtTextArea -// ============================================================ - -WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen) { - WidgetT *w = allocWidget(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 = allocWidget(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; -} - - -// ============================================================ -// wgtVBox -// ============================================================ - -WidgetT *wgtVBox(WidgetT *parent) { - return allocWidget(parent, WidgetVBoxE); -} - - -// ============================================================ -// wgtVSeparator -// ============================================================ - -WidgetT *wgtVSeparator(WidgetT *parent) { - WidgetT *w = allocWidget(parent, WidgetSeparatorE); - - if (w) { - w->as.separator.vertical = true; - } - - return w; -} diff --git a/dvx/dvxWidget.h b/dvx/dvxWidget.h index 7a65b78..f1f7251 100644 --- a/dvx/dvxWidget.h +++ b/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; @@ -172,8 +198,81 @@ typedef struct WidgetT { } separator; struct { - const char *title; + 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 // ============================================================ diff --git a/dvx/thirdparty/stb_image_write.h b/dvx/thirdparty/stb_image_write.h new file mode 100644 index 0000000..e4b32ed --- /dev/null +++ b/dvx/thirdparty/stb_image_write.h @@ -0,0 +1,1724 @@ +/* stb_image_write - v1.16 - public domain - http://nothings.org/stb + writes out PNG/BMP/TGA/JPEG/HDR images to C stdio - Sean Barrett 2010-2015 + no warranty implied; use at your own risk + + Before #including, + + #define STB_IMAGE_WRITE_IMPLEMENTATION + + in the file that you want to have the implementation. + + Will probably not work correctly with strict-aliasing optimizations. + +ABOUT: + + This header file is a library for writing images to C stdio or a callback. + + The PNG output is not optimal; it is 20-50% larger than the file + written by a decent optimizing implementation; though providing a custom + zlib compress function (see STBIW_ZLIB_COMPRESS) can mitigate that. + This library is designed for source code compactness and simplicity, + not optimal image file size or run-time performance. + +BUILDING: + + You can #define STBIW_ASSERT(x) before the #include to avoid using assert.h. + You can #define STBIW_MALLOC(), STBIW_REALLOC(), and STBIW_FREE() to replace + malloc,realloc,free. + You can #define STBIW_MEMMOVE() to replace memmove() + You can #define STBIW_ZLIB_COMPRESS to use a custom zlib-style compress function + for PNG compression (instead of the builtin one), it must have the following signature: + unsigned char * my_compress(unsigned char *data, int data_len, int *out_len, int quality); + The returned data will be freed with STBIW_FREE() (free() by default), + so it must be heap allocated with STBIW_MALLOC() (malloc() by default), + +UNICODE: + + If compiling for Windows and you wish to use Unicode filenames, compile + with + #define STBIW_WINDOWS_UTF8 + and pass utf8-encoded filenames. Call stbiw_convert_wchar_to_utf8 to convert + Windows wchar_t filenames to utf8. + +USAGE: + + There are five functions, one for each image file format: + + int stbi_write_png(char const *filename, int w, int h, int comp, const void *data, int stride_in_bytes); + int stbi_write_bmp(char const *filename, int w, int h, int comp, const void *data); + int stbi_write_tga(char const *filename, int w, int h, int comp, const void *data); + int stbi_write_jpg(char const *filename, int w, int h, int comp, const void *data, int quality); + int stbi_write_hdr(char const *filename, int w, int h, int comp, const float *data); + + void stbi_flip_vertically_on_write(int flag); // flag is non-zero to flip data vertically + + There are also five equivalent functions that use an arbitrary write function. You are + expected to open/close your file-equivalent before and after calling these: + + int stbi_write_png_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data, int stride_in_bytes); + int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); + int stbi_write_tga_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); + int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const float *data); + int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int quality); + + where the callback is: + void stbi_write_func(void *context, void *data, int size); + + You can configure it with these global variables: + int stbi_write_tga_with_rle; // defaults to true; set to 0 to disable RLE + int stbi_write_png_compression_level; // defaults to 8; set to higher for more compression + int stbi_write_force_png_filter; // defaults to -1; set to 0..5 to force a filter mode + + + You can define STBI_WRITE_NO_STDIO to disable the file variant of these + functions, so the library will not use stdio.h at all. However, this will + also disable HDR writing, because it requires stdio for formatted output. + + Each function returns 0 on failure and non-0 on success. + + The functions create an image file defined by the parameters. The image + is a rectangle of pixels stored from left-to-right, top-to-bottom. + Each pixel contains 'comp' channels of data stored interleaved with 8-bits + per channel, in the following order: 1=Y, 2=YA, 3=RGB, 4=RGBA. (Y is + monochrome color.) The rectangle is 'w' pixels wide and 'h' pixels tall. + The *data pointer points to the first byte of the top-left-most pixel. + For PNG, "stride_in_bytes" is the distance in bytes from the first byte of + a row of pixels to the first byte of the next row of pixels. + + PNG creates output files with the same number of components as the input. + The BMP format expands Y to RGB in the file format and does not + output alpha. + + PNG supports writing rectangles of data even when the bytes storing rows of + data are not consecutive in memory (e.g. sub-rectangles of a larger image), + by supplying the stride between the beginning of adjacent rows. The other + formats do not. (Thus you cannot write a native-format BMP through the BMP + writer, both because it is in BGR order and because it may have padding + at the end of the line.) + + PNG allows you to set the deflate compression level by setting the global + variable 'stbi_write_png_compression_level' (it defaults to 8). + + HDR expects linear float data. Since the format is always 32-bit rgb(e) + data, alpha (if provided) is discarded, and for monochrome data it is + replicated across all three channels. + + TGA supports RLE or non-RLE compressed data. To use non-RLE-compressed + data, set the global variable 'stbi_write_tga_with_rle' to 0. + + JPEG does ignore alpha channels in input data; quality is between 1 and 100. + Higher quality looks better but results in a bigger image. + JPEG baseline (no JPEG progressive). + +CREDITS: + + + Sean Barrett - PNG/BMP/TGA + Baldur Karlsson - HDR + Jean-Sebastien Guay - TGA monochrome + Tim Kelsey - misc enhancements + Alan Hickman - TGA RLE + Emmanuel Julien - initial file IO callback implementation + Jon Olick - original jo_jpeg.cpp code + Daniel Gibson - integrate JPEG, allow external zlib + Aarni Koskela - allow choosing PNG filter + + bugfixes: + github:Chribba + Guillaume Chereau + github:jry2 + github:romigrou + Sergio Gonzalez + Jonas Karlsson + Filip Wasil + Thatcher Ulrich + github:poppolopoppo + Patrick Boettcher + github:xeekworx + Cap Petschulat + Simon Rodriguez + Ivan Tikhonov + github:ignotion + Adam Schackart + Andrew Kensler + +LICENSE + + See end of file for license information. + +*/ + +#ifndef INCLUDE_STB_IMAGE_WRITE_H +#define INCLUDE_STB_IMAGE_WRITE_H + +#include + +// if STB_IMAGE_WRITE_STATIC causes problems, try defining STBIWDEF to 'inline' or 'static inline' +#ifndef STBIWDEF +#ifdef STB_IMAGE_WRITE_STATIC +#define STBIWDEF static +#else +#ifdef __cplusplus +#define STBIWDEF extern "C" +#else +#define STBIWDEF extern +#endif +#endif +#endif + +#ifndef STB_IMAGE_WRITE_STATIC // C++ forbids static forward declarations +STBIWDEF int stbi_write_tga_with_rle; +STBIWDEF int stbi_write_png_compression_level; +STBIWDEF int stbi_write_force_png_filter; +#endif + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_png(char const *filename, int w, int h, int comp, const void *data, int stride_in_bytes); +STBIWDEF int stbi_write_bmp(char const *filename, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_tga(char const *filename, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_hdr(char const *filename, int w, int h, int comp, const float *data); +STBIWDEF int stbi_write_jpg(char const *filename, int x, int y, int comp, const void *data, int quality); + +#ifdef STBIW_WINDOWS_UTF8 +STBIWDEF int stbiw_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input); +#endif +#endif + +typedef void stbi_write_func(void *context, void *data, int size); + +STBIWDEF int stbi_write_png_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data, int stride_in_bytes); +STBIWDEF int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_tga_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const float *data); +STBIWDEF int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int quality); + +STBIWDEF void stbi_flip_vertically_on_write(int flip_boolean); + +#endif//INCLUDE_STB_IMAGE_WRITE_H + +#ifdef STB_IMAGE_WRITE_IMPLEMENTATION + +#ifdef _WIN32 + #ifndef _CRT_SECURE_NO_WARNINGS + #define _CRT_SECURE_NO_WARNINGS + #endif + #ifndef _CRT_NONSTDC_NO_DEPRECATE + #define _CRT_NONSTDC_NO_DEPRECATE + #endif +#endif + +#ifndef STBI_WRITE_NO_STDIO +#include +#endif // STBI_WRITE_NO_STDIO + +#include +#include +#include +#include + +#if defined(STBIW_MALLOC) && defined(STBIW_FREE) && (defined(STBIW_REALLOC) || defined(STBIW_REALLOC_SIZED)) +// ok +#elif !defined(STBIW_MALLOC) && !defined(STBIW_FREE) && !defined(STBIW_REALLOC) && !defined(STBIW_REALLOC_SIZED) +// ok +#else +#error "Must define all or none of STBIW_MALLOC, STBIW_FREE, and STBIW_REALLOC (or STBIW_REALLOC_SIZED)." +#endif + +#ifndef STBIW_MALLOC +#define STBIW_MALLOC(sz) malloc(sz) +#define STBIW_REALLOC(p,newsz) realloc(p,newsz) +#define STBIW_FREE(p) free(p) +#endif + +#ifndef STBIW_REALLOC_SIZED +#define STBIW_REALLOC_SIZED(p,oldsz,newsz) STBIW_REALLOC(p,newsz) +#endif + + +#ifndef STBIW_MEMMOVE +#define STBIW_MEMMOVE(a,b,sz) memmove(a,b,sz) +#endif + + +#ifndef STBIW_ASSERT +#include +#define STBIW_ASSERT(x) assert(x) +#endif + +#define STBIW_UCHAR(x) (unsigned char) ((x) & 0xff) + +#ifdef STB_IMAGE_WRITE_STATIC +static int stbi_write_png_compression_level = 8; +static int stbi_write_tga_with_rle = 1; +static int stbi_write_force_png_filter = -1; +#else +int stbi_write_png_compression_level = 8; +int stbi_write_tga_with_rle = 1; +int stbi_write_force_png_filter = -1; +#endif + +static int stbi__flip_vertically_on_write = 0; + +STBIWDEF void stbi_flip_vertically_on_write(int flag) +{ + stbi__flip_vertically_on_write = flag; +} + +typedef struct +{ + stbi_write_func *func; + void *context; + unsigned char buffer[64]; + int buf_used; +} stbi__write_context; + +// initialize a callback-based context +static void stbi__start_write_callbacks(stbi__write_context *s, stbi_write_func *c, void *context) +{ + s->func = c; + s->context = context; +} + +#ifndef STBI_WRITE_NO_STDIO + +static void stbi__stdio_write(void *context, void *data, int size) +{ + fwrite(data,1,size,(FILE*) context); +} + +#if defined(_WIN32) && defined(STBIW_WINDOWS_UTF8) +#ifdef __cplusplus +#define STBIW_EXTERN extern "C" +#else +#define STBIW_EXTERN extern +#endif +STBIW_EXTERN __declspec(dllimport) int __stdcall MultiByteToWideChar(unsigned int cp, unsigned long flags, const char *str, int cbmb, wchar_t *widestr, int cchwide); +STBIW_EXTERN __declspec(dllimport) int __stdcall WideCharToMultiByte(unsigned int cp, unsigned long flags, const wchar_t *widestr, int cchwide, char *str, int cbmb, const char *defchar, int *used_default); + +STBIWDEF int stbiw_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input) +{ + return WideCharToMultiByte(65001 /* UTF8 */, 0, input, -1, buffer, (int) bufferlen, NULL, NULL); +} +#endif + +static FILE *stbiw__fopen(char const *filename, char const *mode) +{ + FILE *f; +#if defined(_WIN32) && defined(STBIW_WINDOWS_UTF8) + wchar_t wMode[64]; + wchar_t wFilename[1024]; + if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, filename, -1, wFilename, sizeof(wFilename)/sizeof(*wFilename))) + return 0; + + if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, mode, -1, wMode, sizeof(wMode)/sizeof(*wMode))) + return 0; + +#if defined(_MSC_VER) && _MSC_VER >= 1400 + if (0 != _wfopen_s(&f, wFilename, wMode)) + f = 0; +#else + f = _wfopen(wFilename, wMode); +#endif + +#elif defined(_MSC_VER) && _MSC_VER >= 1400 + if (0 != fopen_s(&f, filename, mode)) + f=0; +#else + f = fopen(filename, mode); +#endif + return f; +} + +static int stbi__start_write_file(stbi__write_context *s, const char *filename) +{ + FILE *f = stbiw__fopen(filename, "wb"); + stbi__start_write_callbacks(s, stbi__stdio_write, (void *) f); + return f != NULL; +} + +static void stbi__end_write_file(stbi__write_context *s) +{ + fclose((FILE *)s->context); +} + +#endif // !STBI_WRITE_NO_STDIO + +typedef unsigned int stbiw_uint32; +typedef int stb_image_write_test[sizeof(stbiw_uint32)==4 ? 1 : -1]; + +static void stbiw__writefv(stbi__write_context *s, const char *fmt, va_list v) +{ + while (*fmt) { + switch (*fmt++) { + case ' ': break; + case '1': { unsigned char x = STBIW_UCHAR(va_arg(v, int)); + s->func(s->context,&x,1); + break; } + case '2': { int x = va_arg(v,int); + unsigned char b[2]; + b[0] = STBIW_UCHAR(x); + b[1] = STBIW_UCHAR(x>>8); + s->func(s->context,b,2); + break; } + case '4': { stbiw_uint32 x = va_arg(v,int); + unsigned char b[4]; + b[0]=STBIW_UCHAR(x); + b[1]=STBIW_UCHAR(x>>8); + b[2]=STBIW_UCHAR(x>>16); + b[3]=STBIW_UCHAR(x>>24); + s->func(s->context,b,4); + break; } + default: + STBIW_ASSERT(0); + return; + } + } +} + +static void stbiw__writef(stbi__write_context *s, const char *fmt, ...) +{ + va_list v; + va_start(v, fmt); + stbiw__writefv(s, fmt, v); + va_end(v); +} + +static void stbiw__write_flush(stbi__write_context *s) +{ + if (s->buf_used) { + s->func(s->context, &s->buffer, s->buf_used); + s->buf_used = 0; + } +} + +static void stbiw__putc(stbi__write_context *s, unsigned char c) +{ + s->func(s->context, &c, 1); +} + +static void stbiw__write1(stbi__write_context *s, unsigned char a) +{ + if ((size_t)s->buf_used + 1 > sizeof(s->buffer)) + stbiw__write_flush(s); + s->buffer[s->buf_used++] = a; +} + +static void stbiw__write3(stbi__write_context *s, unsigned char a, unsigned char b, unsigned char c) +{ + int n; + if ((size_t)s->buf_used + 3 > sizeof(s->buffer)) + stbiw__write_flush(s); + n = s->buf_used; + s->buf_used = n+3; + s->buffer[n+0] = a; + s->buffer[n+1] = b; + s->buffer[n+2] = c; +} + +static void stbiw__write_pixel(stbi__write_context *s, int rgb_dir, int comp, int write_alpha, int expand_mono, unsigned char *d) +{ + unsigned char bg[3] = { 255, 0, 255}, px[3]; + int k; + + if (write_alpha < 0) + stbiw__write1(s, d[comp - 1]); + + switch (comp) { + case 2: // 2 pixels = mono + alpha, alpha is written separately, so same as 1-channel case + case 1: + if (expand_mono) + stbiw__write3(s, d[0], d[0], d[0]); // monochrome bmp + else + stbiw__write1(s, d[0]); // monochrome TGA + break; + case 4: + if (!write_alpha) { + // composite against pink background + for (k = 0; k < 3; ++k) + px[k] = bg[k] + ((d[k] - bg[k]) * d[3]) / 255; + stbiw__write3(s, px[1 - rgb_dir], px[1], px[1 + rgb_dir]); + break; + } + /* FALLTHROUGH */ + case 3: + stbiw__write3(s, d[1 - rgb_dir], d[1], d[1 + rgb_dir]); + break; + } + if (write_alpha > 0) + stbiw__write1(s, d[comp - 1]); +} + +static void stbiw__write_pixels(stbi__write_context *s, int rgb_dir, int vdir, int x, int y, int comp, void *data, int write_alpha, int scanline_pad, int expand_mono) +{ + stbiw_uint32 zero = 0; + int i,j, j_end; + + if (y <= 0) + return; + + if (stbi__flip_vertically_on_write) + vdir *= -1; + + if (vdir < 0) { + j_end = -1; j = y-1; + } else { + j_end = y; j = 0; + } + + for (; j != j_end; j += vdir) { + for (i=0; i < x; ++i) { + unsigned char *d = (unsigned char *) data + (j*x+i)*comp; + stbiw__write_pixel(s, rgb_dir, comp, write_alpha, expand_mono, d); + } + stbiw__write_flush(s); + s->func(s->context, &zero, scanline_pad); + } +} + +static int stbiw__outfile(stbi__write_context *s, int rgb_dir, int vdir, int x, int y, int comp, int expand_mono, void *data, int alpha, int pad, const char *fmt, ...) +{ + if (y < 0 || x < 0) { + return 0; + } else { + va_list v; + va_start(v, fmt); + stbiw__writefv(s, fmt, v); + va_end(v); + stbiw__write_pixels(s,rgb_dir,vdir,x,y,comp,data,alpha,pad, expand_mono); + return 1; + } +} + +static int stbi_write_bmp_core(stbi__write_context *s, int x, int y, int comp, const void *data) +{ + if (comp != 4) { + // write RGB bitmap + int pad = (-x*3) & 3; + return stbiw__outfile(s,-1,-1,x,y,comp,1,(void *) data,0,pad, + "11 4 22 4" "4 44 22 444444", + 'B', 'M', 14+40+(x*3+pad)*y, 0,0, 14+40, // file header + 40, x,y, 1,24, 0,0,0,0,0,0); // bitmap header + } else { + // RGBA bitmaps need a v4 header + // use BI_BITFIELDS mode with 32bpp and alpha mask + // (straight BI_RGB with alpha mask doesn't work in most readers) + return stbiw__outfile(s,-1,-1,x,y,comp,1,(void *)data,1,0, + "11 4 22 4" "4 44 22 444444 4444 4 444 444 444 444", + 'B', 'M', 14+108+x*y*4, 0, 0, 14+108, // file header + 108, x,y, 1,32, 3,0,0,0,0,0, 0xff0000,0xff00,0xff,0xff000000u, 0, 0,0,0, 0,0,0, 0,0,0, 0,0,0); // bitmap V4 header + } +} + +STBIWDEF int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data) +{ + stbi__write_context s = { 0 }; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_bmp_core(&s, x, y, comp, data); +} + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_bmp(char const *filename, int x, int y, int comp, const void *data) +{ + stbi__write_context s = { 0 }; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_bmp_core(&s, x, y, comp, data); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif //!STBI_WRITE_NO_STDIO + +static int stbi_write_tga_core(stbi__write_context *s, int x, int y, int comp, void *data) +{ + int has_alpha = (comp == 2 || comp == 4); + int colorbytes = has_alpha ? comp-1 : comp; + int format = colorbytes < 2 ? 3 : 2; // 3 color channels (RGB/RGBA) = 2, 1 color channel (Y/YA) = 3 + + if (y < 0 || x < 0) + return 0; + + if (!stbi_write_tga_with_rle) { + return stbiw__outfile(s, -1, -1, x, y, comp, 0, (void *) data, has_alpha, 0, + "111 221 2222 11", 0, 0, format, 0, 0, 0, 0, 0, x, y, (colorbytes + has_alpha) * 8, has_alpha * 8); + } else { + int i,j,k; + int jend, jdir; + + stbiw__writef(s, "111 221 2222 11", 0,0,format+8, 0,0,0, 0,0,x,y, (colorbytes + has_alpha) * 8, has_alpha * 8); + + if (stbi__flip_vertically_on_write) { + j = 0; + jend = y; + jdir = 1; + } else { + j = y-1; + jend = -1; + jdir = -1; + } + for (; j != jend; j += jdir) { + unsigned char *row = (unsigned char *) data + j * x * comp; + int len; + + for (i = 0; i < x; i += len) { + unsigned char *begin = row + i * comp; + int diff = 1; + len = 1; + + if (i < x - 1) { + ++len; + diff = memcmp(begin, row + (i + 1) * comp, comp); + if (diff) { + const unsigned char *prev = begin; + for (k = i + 2; k < x && len < 128; ++k) { + if (memcmp(prev, row + k * comp, comp)) { + prev += comp; + ++len; + } else { + --len; + break; + } + } + } else { + for (k = i + 2; k < x && len < 128; ++k) { + if (!memcmp(begin, row + k * comp, comp)) { + ++len; + } else { + break; + } + } + } + } + + if (diff) { + unsigned char header = STBIW_UCHAR(len - 1); + stbiw__write1(s, header); + for (k = 0; k < len; ++k) { + stbiw__write_pixel(s, -1, comp, has_alpha, 0, begin + k * comp); + } + } else { + unsigned char header = STBIW_UCHAR(len - 129); + stbiw__write1(s, header); + stbiw__write_pixel(s, -1, comp, has_alpha, 0, begin); + } + } + } + stbiw__write_flush(s); + } + return 1; +} + +STBIWDEF int stbi_write_tga_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data) +{ + stbi__write_context s = { 0 }; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_tga_core(&s, x, y, comp, (void *) data); +} + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_tga(char const *filename, int x, int y, int comp, const void *data) +{ + stbi__write_context s = { 0 }; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_tga_core(&s, x, y, comp, (void *) data); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif + +// ************************************************************************************************* +// Radiance RGBE HDR writer +// by Baldur Karlsson + +#define stbiw__max(a, b) ((a) > (b) ? (a) : (b)) + +#ifndef STBI_WRITE_NO_STDIO + +static void stbiw__linear_to_rgbe(unsigned char *rgbe, float *linear) +{ + int exponent; + float maxcomp = stbiw__max(linear[0], stbiw__max(linear[1], linear[2])); + + if (maxcomp < 1e-32f) { + rgbe[0] = rgbe[1] = rgbe[2] = rgbe[3] = 0; + } else { + float normalize = (float) frexp(maxcomp, &exponent) * 256.0f/maxcomp; + + rgbe[0] = (unsigned char)(linear[0] * normalize); + rgbe[1] = (unsigned char)(linear[1] * normalize); + rgbe[2] = (unsigned char)(linear[2] * normalize); + rgbe[3] = (unsigned char)(exponent + 128); + } +} + +static void stbiw__write_run_data(stbi__write_context *s, int length, unsigned char databyte) +{ + unsigned char lengthbyte = STBIW_UCHAR(length+128); + STBIW_ASSERT(length+128 <= 255); + s->func(s->context, &lengthbyte, 1); + s->func(s->context, &databyte, 1); +} + +static void stbiw__write_dump_data(stbi__write_context *s, int length, unsigned char *data) +{ + unsigned char lengthbyte = STBIW_UCHAR(length); + STBIW_ASSERT(length <= 128); // inconsistent with spec but consistent with official code + s->func(s->context, &lengthbyte, 1); + s->func(s->context, data, length); +} + +static void stbiw__write_hdr_scanline(stbi__write_context *s, int width, int ncomp, unsigned char *scratch, float *scanline) +{ + unsigned char scanlineheader[4] = { 2, 2, 0, 0 }; + unsigned char rgbe[4]; + float linear[3]; + int x; + + scanlineheader[2] = (width&0xff00)>>8; + scanlineheader[3] = (width&0x00ff); + + /* skip RLE for images too small or large */ + if (width < 8 || width >= 32768) { + for (x=0; x < width; x++) { + switch (ncomp) { + case 4: /* fallthrough */ + case 3: linear[2] = scanline[x*ncomp + 2]; + linear[1] = scanline[x*ncomp + 1]; + linear[0] = scanline[x*ncomp + 0]; + break; + default: + linear[0] = linear[1] = linear[2] = scanline[x*ncomp + 0]; + break; + } + stbiw__linear_to_rgbe(rgbe, linear); + s->func(s->context, rgbe, 4); + } + } else { + int c,r; + /* encode into scratch buffer */ + for (x=0; x < width; x++) { + switch(ncomp) { + case 4: /* fallthrough */ + case 3: linear[2] = scanline[x*ncomp + 2]; + linear[1] = scanline[x*ncomp + 1]; + linear[0] = scanline[x*ncomp + 0]; + break; + default: + linear[0] = linear[1] = linear[2] = scanline[x*ncomp + 0]; + break; + } + stbiw__linear_to_rgbe(rgbe, linear); + scratch[x + width*0] = rgbe[0]; + scratch[x + width*1] = rgbe[1]; + scratch[x + width*2] = rgbe[2]; + scratch[x + width*3] = rgbe[3]; + } + + s->func(s->context, scanlineheader, 4); + + /* RLE each component separately */ + for (c=0; c < 4; c++) { + unsigned char *comp = &scratch[width*c]; + + x = 0; + while (x < width) { + // find first run + r = x; + while (r+2 < width) { + if (comp[r] == comp[r+1] && comp[r] == comp[r+2]) + break; + ++r; + } + if (r+2 >= width) + r = width; + // dump up to first run + while (x < r) { + int len = r-x; + if (len > 128) len = 128; + stbiw__write_dump_data(s, len, &comp[x]); + x += len; + } + // if there's a run, output it + if (r+2 < width) { // same test as what we break out of in search loop, so only true if we break'd + // find next byte after run + while (r < width && comp[r] == comp[x]) + ++r; + // output run up to r + while (x < r) { + int len = r-x; + if (len > 127) len = 127; + stbiw__write_run_data(s, len, comp[x]); + x += len; + } + } + } + } + } +} + +static int stbi_write_hdr_core(stbi__write_context *s, int x, int y, int comp, float *data) +{ + if (y <= 0 || x <= 0 || data == NULL) + return 0; + else { + // Each component is stored separately. Allocate scratch space for full output scanline. + unsigned char *scratch = (unsigned char *) STBIW_MALLOC(x*4); + int i, len; + char buffer[128]; + char header[] = "#?RADIANCE\n# Written by stb_image_write.h\nFORMAT=32-bit_rle_rgbe\n"; + s->func(s->context, header, sizeof(header)-1); + +#ifdef __STDC_LIB_EXT1__ + len = sprintf_s(buffer, sizeof(buffer), "EXPOSURE= 1.0000000000000\n\n-Y %d +X %d\n", y, x); +#else + len = sprintf(buffer, "EXPOSURE= 1.0000000000000\n\n-Y %d +X %d\n", y, x); +#endif + s->func(s->context, buffer, len); + + for(i=0; i < y; i++) + stbiw__write_hdr_scanline(s, x, comp, scratch, data + comp*x*(stbi__flip_vertically_on_write ? y-1-i : i)); + STBIW_FREE(scratch); + return 1; + } +} + +STBIWDEF int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const float *data) +{ + stbi__write_context s = { 0 }; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_hdr_core(&s, x, y, comp, (float *) data); +} + +STBIWDEF int stbi_write_hdr(char const *filename, int x, int y, int comp, const float *data) +{ + stbi__write_context s = { 0 }; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_hdr_core(&s, x, y, comp, (float *) data); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif // STBI_WRITE_NO_STDIO + + +////////////////////////////////////////////////////////////////////////////// +// +// PNG writer +// + +#ifndef STBIW_ZLIB_COMPRESS +// stretchy buffer; stbiw__sbpush() == vector<>::push_back() -- stbiw__sbcount() == vector<>::size() +#define stbiw__sbraw(a) ((int *) (void *) (a) - 2) +#define stbiw__sbm(a) stbiw__sbraw(a)[0] +#define stbiw__sbn(a) stbiw__sbraw(a)[1] + +#define stbiw__sbneedgrow(a,n) ((a)==0 || stbiw__sbn(a)+n >= stbiw__sbm(a)) +#define stbiw__sbmaybegrow(a,n) (stbiw__sbneedgrow(a,(n)) ? stbiw__sbgrow(a,n) : 0) +#define stbiw__sbgrow(a,n) stbiw__sbgrowf((void **) &(a), (n), sizeof(*(a))) + +#define stbiw__sbpush(a, v) (stbiw__sbmaybegrow(a,1), (a)[stbiw__sbn(a)++] = (v)) +#define stbiw__sbcount(a) ((a) ? stbiw__sbn(a) : 0) +#define stbiw__sbfree(a) ((a) ? STBIW_FREE(stbiw__sbraw(a)),0 : 0) + +static void *stbiw__sbgrowf(void **arr, int increment, int itemsize) +{ + int m = *arr ? 2*stbiw__sbm(*arr)+increment : increment+1; + void *p = STBIW_REALLOC_SIZED(*arr ? stbiw__sbraw(*arr) : 0, *arr ? (stbiw__sbm(*arr)*itemsize + sizeof(int)*2) : 0, itemsize * m + sizeof(int)*2); + STBIW_ASSERT(p); + if (p) { + if (!*arr) ((int *) p)[1] = 0; + *arr = (void *) ((int *) p + 2); + stbiw__sbm(*arr) = m; + } + return *arr; +} + +static unsigned char *stbiw__zlib_flushf(unsigned char *data, unsigned int *bitbuffer, int *bitcount) +{ + while (*bitcount >= 8) { + stbiw__sbpush(data, STBIW_UCHAR(*bitbuffer)); + *bitbuffer >>= 8; + *bitcount -= 8; + } + return data; +} + +static int stbiw__zlib_bitrev(int code, int codebits) +{ + int res=0; + while (codebits--) { + res = (res << 1) | (code & 1); + code >>= 1; + } + return res; +} + +static unsigned int stbiw__zlib_countm(unsigned char *a, unsigned char *b, int limit) +{ + int i; + for (i=0; i < limit && i < 258; ++i) + if (a[i] != b[i]) break; + return i; +} + +static unsigned int stbiw__zhash(unsigned char *data) +{ + stbiw_uint32 hash = data[0] + (data[1] << 8) + (data[2] << 16); + hash ^= hash << 3; + hash += hash >> 5; + hash ^= hash << 4; + hash += hash >> 17; + hash ^= hash << 25; + hash += hash >> 6; + return hash; +} + +#define stbiw__zlib_flush() (out = stbiw__zlib_flushf(out, &bitbuf, &bitcount)) +#define stbiw__zlib_add(code,codebits) \ + (bitbuf |= (code) << bitcount, bitcount += (codebits), stbiw__zlib_flush()) +#define stbiw__zlib_huffa(b,c) stbiw__zlib_add(stbiw__zlib_bitrev(b,c),c) +// default huffman tables +#define stbiw__zlib_huff1(n) stbiw__zlib_huffa(0x30 + (n), 8) +#define stbiw__zlib_huff2(n) stbiw__zlib_huffa(0x190 + (n)-144, 9) +#define stbiw__zlib_huff3(n) stbiw__zlib_huffa(0 + (n)-256,7) +#define stbiw__zlib_huff4(n) stbiw__zlib_huffa(0xc0 + (n)-280,8) +#define stbiw__zlib_huff(n) ((n) <= 143 ? stbiw__zlib_huff1(n) : (n) <= 255 ? stbiw__zlib_huff2(n) : (n) <= 279 ? stbiw__zlib_huff3(n) : stbiw__zlib_huff4(n)) +#define stbiw__zlib_huffb(n) ((n) <= 143 ? stbiw__zlib_huff1(n) : stbiw__zlib_huff2(n)) + +#define stbiw__ZHASH 16384 + +#endif // STBIW_ZLIB_COMPRESS + +STBIWDEF unsigned char * stbi_zlib_compress(unsigned char *data, int data_len, int *out_len, int quality) +{ +#ifdef STBIW_ZLIB_COMPRESS + // user provided a zlib compress implementation, use that + return STBIW_ZLIB_COMPRESS(data, data_len, out_len, quality); +#else // use builtin + static unsigned short lengthc[] = { 3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258, 259 }; + static unsigned char lengtheb[]= { 0,0,0,0,0,0,0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0 }; + static unsigned short distc[] = { 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577, 32768 }; + static unsigned char disteb[] = { 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13 }; + unsigned int bitbuf=0; + int i,j, bitcount=0; + unsigned char *out = NULL; + unsigned char ***hash_table = (unsigned char***) STBIW_MALLOC(stbiw__ZHASH * sizeof(unsigned char**)); + if (hash_table == NULL) + return NULL; + if (quality < 5) quality = 5; + + stbiw__sbpush(out, 0x78); // DEFLATE 32K window + stbiw__sbpush(out, 0x5e); // FLEVEL = 1 + stbiw__zlib_add(1,1); // BFINAL = 1 + stbiw__zlib_add(1,2); // BTYPE = 1 -- fixed huffman + + for (i=0; i < stbiw__ZHASH; ++i) + hash_table[i] = NULL; + + i=0; + while (i < data_len-3) { + // hash next 3 bytes of data to be compressed + int h = stbiw__zhash(data+i)&(stbiw__ZHASH-1), best=3; + unsigned char *bestloc = 0; + unsigned char **hlist = hash_table[h]; + int n = stbiw__sbcount(hlist); + for (j=0; j < n; ++j) { + if (hlist[j]-data > i-32768) { // if entry lies within window + int d = stbiw__zlib_countm(hlist[j], data+i, data_len-i); + if (d >= best) { best=d; bestloc=hlist[j]; } + } + } + // when hash table entry is too long, delete half the entries + if (hash_table[h] && stbiw__sbn(hash_table[h]) == 2*quality) { + STBIW_MEMMOVE(hash_table[h], hash_table[h]+quality, sizeof(hash_table[h][0])*quality); + stbiw__sbn(hash_table[h]) = quality; + } + stbiw__sbpush(hash_table[h],data+i); + + if (bestloc) { + // "lazy matching" - check match at *next* byte, and if it's better, do cur byte as literal + h = stbiw__zhash(data+i+1)&(stbiw__ZHASH-1); + hlist = hash_table[h]; + n = stbiw__sbcount(hlist); + for (j=0; j < n; ++j) { + if (hlist[j]-data > i-32767) { + int e = stbiw__zlib_countm(hlist[j], data+i+1, data_len-i-1); + if (e > best) { // if next match is better, bail on current match + bestloc = NULL; + break; + } + } + } + } + + if (bestloc) { + int d = (int) (data+i - bestloc); // distance back + STBIW_ASSERT(d <= 32767 && best <= 258); + for (j=0; best > lengthc[j+1]-1; ++j); + stbiw__zlib_huff(j+257); + if (lengtheb[j]) stbiw__zlib_add(best - lengthc[j], lengtheb[j]); + for (j=0; d > distc[j+1]-1; ++j); + stbiw__zlib_add(stbiw__zlib_bitrev(j,5),5); + if (disteb[j]) stbiw__zlib_add(d - distc[j], disteb[j]); + i += best; + } else { + stbiw__zlib_huffb(data[i]); + ++i; + } + } + // write out final bytes + for (;i < data_len; ++i) + stbiw__zlib_huffb(data[i]); + stbiw__zlib_huff(256); // end of block + // pad with 0 bits to byte boundary + while (bitcount) + stbiw__zlib_add(0,1); + + for (i=0; i < stbiw__ZHASH; ++i) + (void) stbiw__sbfree(hash_table[i]); + STBIW_FREE(hash_table); + + // store uncompressed instead if compression was worse + if (stbiw__sbn(out) > data_len + 2 + ((data_len+32766)/32767)*5) { + stbiw__sbn(out) = 2; // truncate to DEFLATE 32K window and FLEVEL = 1 + for (j = 0; j < data_len;) { + int blocklen = data_len - j; + if (blocklen > 32767) blocklen = 32767; + stbiw__sbpush(out, data_len - j == blocklen); // BFINAL = ?, BTYPE = 0 -- no compression + stbiw__sbpush(out, STBIW_UCHAR(blocklen)); // LEN + stbiw__sbpush(out, STBIW_UCHAR(blocklen >> 8)); + stbiw__sbpush(out, STBIW_UCHAR(~blocklen)); // NLEN + stbiw__sbpush(out, STBIW_UCHAR(~blocklen >> 8)); + memcpy(out+stbiw__sbn(out), data+j, blocklen); + stbiw__sbn(out) += blocklen; + j += blocklen; + } + } + + { + // compute adler32 on input + unsigned int s1=1, s2=0; + int blocklen = (int) (data_len % 5552); + j=0; + while (j < data_len) { + for (i=0; i < blocklen; ++i) { s1 += data[j+i]; s2 += s1; } + s1 %= 65521; s2 %= 65521; + j += blocklen; + blocklen = 5552; + } + stbiw__sbpush(out, STBIW_UCHAR(s2 >> 8)); + stbiw__sbpush(out, STBIW_UCHAR(s2)); + stbiw__sbpush(out, STBIW_UCHAR(s1 >> 8)); + stbiw__sbpush(out, STBIW_UCHAR(s1)); + } + *out_len = stbiw__sbn(out); + // make returned pointer freeable + STBIW_MEMMOVE(stbiw__sbraw(out), out, *out_len); + return (unsigned char *) stbiw__sbraw(out); +#endif // STBIW_ZLIB_COMPRESS +} + +static unsigned int stbiw__crc32(unsigned char *buffer, int len) +{ +#ifdef STBIW_CRC32 + return STBIW_CRC32(buffer, len); +#else + static unsigned int crc_table[256] = + { + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, + 0x0eDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, + 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, + 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, + 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, + 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, + 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, + 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, + 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, + 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, + 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, + 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, + 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, + 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, + 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, + 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, + 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, + 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, + 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, + 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, + 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, + 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, + 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, + 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, + 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, + 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, + 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, + 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, + 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, + 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, + 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, + 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D + }; + + unsigned int crc = ~0u; + int i; + for (i=0; i < len; ++i) + crc = (crc >> 8) ^ crc_table[buffer[i] ^ (crc & 0xff)]; + return ~crc; +#endif +} + +#define stbiw__wpng4(o,a,b,c,d) ((o)[0]=STBIW_UCHAR(a),(o)[1]=STBIW_UCHAR(b),(o)[2]=STBIW_UCHAR(c),(o)[3]=STBIW_UCHAR(d),(o)+=4) +#define stbiw__wp32(data,v) stbiw__wpng4(data, (v)>>24,(v)>>16,(v)>>8,(v)); +#define stbiw__wptag(data,s) stbiw__wpng4(data, s[0],s[1],s[2],s[3]) + +static void stbiw__wpcrc(unsigned char **data, int len) +{ + unsigned int crc = stbiw__crc32(*data - len - 4, len+4); + stbiw__wp32(*data, crc); +} + +static unsigned char stbiw__paeth(int a, int b, int c) +{ + int p = a + b - c, pa = abs(p-a), pb = abs(p-b), pc = abs(p-c); + if (pa <= pb && pa <= pc) return STBIW_UCHAR(a); + if (pb <= pc) return STBIW_UCHAR(b); + return STBIW_UCHAR(c); +} + +// @OPTIMIZE: provide an option that always forces left-predict or paeth predict +static void stbiw__encode_png_line(unsigned char *pixels, int stride_bytes, int width, int height, int y, int n, int filter_type, signed char *line_buffer) +{ + static int mapping[] = { 0,1,2,3,4 }; + static int firstmap[] = { 0,1,0,5,6 }; + int *mymap = (y != 0) ? mapping : firstmap; + int i; + int type = mymap[filter_type]; + unsigned char *z = pixels + stride_bytes * (stbi__flip_vertically_on_write ? height-1-y : y); + int signed_stride = stbi__flip_vertically_on_write ? -stride_bytes : stride_bytes; + + if (type==0) { + memcpy(line_buffer, z, width*n); + return; + } + + // first loop isn't optimized since it's just one pixel + for (i = 0; i < n; ++i) { + switch (type) { + case 1: line_buffer[i] = z[i]; break; + case 2: line_buffer[i] = z[i] - z[i-signed_stride]; break; + case 3: line_buffer[i] = z[i] - (z[i-signed_stride]>>1); break; + case 4: line_buffer[i] = (signed char) (z[i] - stbiw__paeth(0,z[i-signed_stride],0)); break; + case 5: line_buffer[i] = z[i]; break; + case 6: line_buffer[i] = z[i]; break; + } + } + switch (type) { + case 1: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - z[i-n]; break; + case 2: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - z[i-signed_stride]; break; + case 3: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - ((z[i-n] + z[i-signed_stride])>>1); break; + case 4: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - stbiw__paeth(z[i-n], z[i-signed_stride], z[i-signed_stride-n]); break; + case 5: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - (z[i-n]>>1); break; + case 6: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - stbiw__paeth(z[i-n], 0,0); break; + } +} + +STBIWDEF unsigned char *stbi_write_png_to_mem(const unsigned char *pixels, int stride_bytes, int x, int y, int n, int *out_len) +{ + int force_filter = stbi_write_force_png_filter; + int ctype[5] = { -1, 0, 4, 2, 6 }; + unsigned char sig[8] = { 137,80,78,71,13,10,26,10 }; + unsigned char *out,*o, *filt, *zlib; + signed char *line_buffer; + int j,zlen; + + if (stride_bytes == 0) + stride_bytes = x * n; + + if (force_filter >= 5) { + force_filter = -1; + } + + filt = (unsigned char *) STBIW_MALLOC((x*n+1) * y); if (!filt) return 0; + line_buffer = (signed char *) STBIW_MALLOC(x * n); if (!line_buffer) { STBIW_FREE(filt); return 0; } + for (j=0; j < y; ++j) { + int filter_type; + if (force_filter > -1) { + filter_type = force_filter; + stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, force_filter, line_buffer); + } else { // Estimate the best filter by running through all of them: + int best_filter = 0, best_filter_val = 0x7fffffff, est, i; + for (filter_type = 0; filter_type < 5; filter_type++) { + stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, filter_type, line_buffer); + + // Estimate the entropy of the line using this filter; the less, the better. + est = 0; + for (i = 0; i < x*n; ++i) { + est += abs((signed char) line_buffer[i]); + } + if (est < best_filter_val) { + best_filter_val = est; + best_filter = filter_type; + } + } + if (filter_type != best_filter) { // If the last iteration already got us the best filter, don't redo it + stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, best_filter, line_buffer); + filter_type = best_filter; + } + } + // when we get here, filter_type contains the filter type, and line_buffer contains the data + filt[j*(x*n+1)] = (unsigned char) filter_type; + STBIW_MEMMOVE(filt+j*(x*n+1)+1, line_buffer, x*n); + } + STBIW_FREE(line_buffer); + zlib = stbi_zlib_compress(filt, y*( x*n+1), &zlen, stbi_write_png_compression_level); + STBIW_FREE(filt); + if (!zlib) return 0; + + // each tag requires 12 bytes of overhead + out = (unsigned char *) STBIW_MALLOC(8 + 12+13 + 12+zlen + 12); + if (!out) return 0; + *out_len = 8 + 12+13 + 12+zlen + 12; + + o=out; + STBIW_MEMMOVE(o,sig,8); o+= 8; + stbiw__wp32(o, 13); // header length + stbiw__wptag(o, "IHDR"); + stbiw__wp32(o, x); + stbiw__wp32(o, y); + *o++ = 8; + *o++ = STBIW_UCHAR(ctype[n]); + *o++ = 0; + *o++ = 0; + *o++ = 0; + stbiw__wpcrc(&o,13); + + stbiw__wp32(o, zlen); + stbiw__wptag(o, "IDAT"); + STBIW_MEMMOVE(o, zlib, zlen); + o += zlen; + STBIW_FREE(zlib); + stbiw__wpcrc(&o, zlen); + + stbiw__wp32(o,0); + stbiw__wptag(o, "IEND"); + stbiw__wpcrc(&o,0); + + STBIW_ASSERT(o == out + *out_len); + + return out; +} + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_png(char const *filename, int x, int y, int comp, const void *data, int stride_bytes) +{ + FILE *f; + int len; + unsigned char *png = stbi_write_png_to_mem((const unsigned char *) data, stride_bytes, x, y, comp, &len); + if (png == NULL) return 0; + + f = stbiw__fopen(filename, "wb"); + if (!f) { STBIW_FREE(png); return 0; } + fwrite(png, 1, len, f); + fclose(f); + STBIW_FREE(png); + return 1; +} +#endif + +STBIWDEF int stbi_write_png_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int stride_bytes) +{ + int len; + unsigned char *png = stbi_write_png_to_mem((const unsigned char *) data, stride_bytes, x, y, comp, &len); + if (png == NULL) return 0; + func(context, png, len); + STBIW_FREE(png); + return 1; +} + + +/* *************************************************************************** + * + * JPEG writer + * + * This is based on Jon Olick's jo_jpeg.cpp: + * public domain Simple, Minimalistic JPEG writer - http://www.jonolick.com/code.html + */ + +static const unsigned char stbiw__jpg_ZigZag[] = { 0,1,5,6,14,15,27,28,2,4,7,13,16,26,29,42,3,8,12,17,25,30,41,43,9,11,18, + 24,31,40,44,53,10,19,23,32,39,45,52,54,20,22,33,38,46,51,55,60,21,34,37,47,50,56,59,61,35,36,48,49,57,58,62,63 }; + +static void stbiw__jpg_writeBits(stbi__write_context *s, int *bitBufP, int *bitCntP, const unsigned short *bs) { + int bitBuf = *bitBufP, bitCnt = *bitCntP; + bitCnt += bs[1]; + bitBuf |= bs[0] << (24 - bitCnt); + while(bitCnt >= 8) { + unsigned char c = (bitBuf >> 16) & 255; + stbiw__putc(s, c); + if(c == 255) { + stbiw__putc(s, 0); + } + bitBuf <<= 8; + bitCnt -= 8; + } + *bitBufP = bitBuf; + *bitCntP = bitCnt; +} + +static void stbiw__jpg_DCT(float *d0p, float *d1p, float *d2p, float *d3p, float *d4p, float *d5p, float *d6p, float *d7p) { + float d0 = *d0p, d1 = *d1p, d2 = *d2p, d3 = *d3p, d4 = *d4p, d5 = *d5p, d6 = *d6p, d7 = *d7p; + float z1, z2, z3, z4, z5, z11, z13; + + float tmp0 = d0 + d7; + float tmp7 = d0 - d7; + float tmp1 = d1 + d6; + float tmp6 = d1 - d6; + float tmp2 = d2 + d5; + float tmp5 = d2 - d5; + float tmp3 = d3 + d4; + float tmp4 = d3 - d4; + + // Even part + float tmp10 = tmp0 + tmp3; // phase 2 + float tmp13 = tmp0 - tmp3; + float tmp11 = tmp1 + tmp2; + float tmp12 = tmp1 - tmp2; + + d0 = tmp10 + tmp11; // phase 3 + d4 = tmp10 - tmp11; + + z1 = (tmp12 + tmp13) * 0.707106781f; // c4 + d2 = tmp13 + z1; // phase 5 + d6 = tmp13 - z1; + + // Odd part + tmp10 = tmp4 + tmp5; // phase 2 + tmp11 = tmp5 + tmp6; + tmp12 = tmp6 + tmp7; + + // The rotator is modified from fig 4-8 to avoid extra negations. + z5 = (tmp10 - tmp12) * 0.382683433f; // c6 + z2 = tmp10 * 0.541196100f + z5; // c2-c6 + z4 = tmp12 * 1.306562965f + z5; // c2+c6 + z3 = tmp11 * 0.707106781f; // c4 + + z11 = tmp7 + z3; // phase 5 + z13 = tmp7 - z3; + + *d5p = z13 + z2; // phase 6 + *d3p = z13 - z2; + *d1p = z11 + z4; + *d7p = z11 - z4; + + *d0p = d0; *d2p = d2; *d4p = d4; *d6p = d6; +} + +static void stbiw__jpg_calcBits(int val, unsigned short bits[2]) { + int tmp1 = val < 0 ? -val : val; + val = val < 0 ? val-1 : val; + bits[1] = 1; + while(tmp1 >>= 1) { + ++bits[1]; + } + bits[0] = val & ((1<0)&&(DU[end0pos]==0); --end0pos) { + } + // end0pos = first element in reverse order !=0 + if(end0pos == 0) { + stbiw__jpg_writeBits(s, bitBuf, bitCnt, EOB); + return DU[0]; + } + for(i = 1; i <= end0pos; ++i) { + int startpos = i; + int nrzeroes; + unsigned short bits[2]; + for (; DU[i]==0 && i<=end0pos; ++i) { + } + nrzeroes = i-startpos; + if ( nrzeroes >= 16 ) { + int lng = nrzeroes>>4; + int nrmarker; + for (nrmarker=1; nrmarker <= lng; ++nrmarker) + stbiw__jpg_writeBits(s, bitBuf, bitCnt, M16zeroes); + nrzeroes &= 15; + } + stbiw__jpg_calcBits(DU[i], bits); + stbiw__jpg_writeBits(s, bitBuf, bitCnt, HTAC[(nrzeroes<<4)+bits[1]]); + stbiw__jpg_writeBits(s, bitBuf, bitCnt, bits); + } + if(end0pos != 63) { + stbiw__jpg_writeBits(s, bitBuf, bitCnt, EOB); + } + return DU[0]; +} + +static int stbi_write_jpg_core(stbi__write_context *s, int width, int height, int comp, const void* data, int quality) { + // Constants that don't pollute global namespace + static const unsigned char std_dc_luminance_nrcodes[] = {0,0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0}; + static const unsigned char std_dc_luminance_values[] = {0,1,2,3,4,5,6,7,8,9,10,11}; + static const unsigned char std_ac_luminance_nrcodes[] = {0,0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,0x7d}; + static const unsigned char std_ac_luminance_values[] = { + 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12,0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07,0x22,0x71,0x14,0x32,0x81,0x91,0xa1,0x08, + 0x23,0x42,0xb1,0xc1,0x15,0x52,0xd1,0xf0,0x24,0x33,0x62,0x72,0x82,0x09,0x0a,0x16,0x17,0x18,0x19,0x1a,0x25,0x26,0x27,0x28, + 0x29,0x2a,0x34,0x35,0x36,0x37,0x38,0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58,0x59, + 0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x83,0x84,0x85,0x86,0x87,0x88,0x89, + 0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4,0xb5,0xb6, + 0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda,0xe1,0xe2, + 0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea,0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,0xfa + }; + static const unsigned char std_dc_chrominance_nrcodes[] = {0,0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0}; + static const unsigned char std_dc_chrominance_values[] = {0,1,2,3,4,5,6,7,8,9,10,11}; + static const unsigned char std_ac_chrominance_nrcodes[] = {0,0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,0x77}; + static const unsigned char std_ac_chrominance_values[] = { + 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21,0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71,0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91, + 0xa1,0xb1,0xc1,0x09,0x23,0x33,0x52,0xf0,0x15,0x62,0x72,0xd1,0x0a,0x16,0x24,0x34,0xe1,0x25,0xf1,0x17,0x18,0x19,0x1a,0x26, + 0x27,0x28,0x29,0x2a,0x35,0x36,0x37,0x38,0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58, + 0x59,0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x82,0x83,0x84,0x85,0x86,0x87, + 0x88,0x89,0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4, + 0xb5,0xb6,0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda, + 0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,0xfa + }; + // Huffman tables + static const unsigned short YDC_HT[256][2] = { {0,2},{2,3},{3,3},{4,3},{5,3},{6,3},{14,4},{30,5},{62,6},{126,7},{254,8},{510,9}}; + static const unsigned short UVDC_HT[256][2] = { {0,2},{1,2},{2,2},{6,3},{14,4},{30,5},{62,6},{126,7},{254,8},{510,9},{1022,10},{2046,11}}; + static const unsigned short YAC_HT[256][2] = { + {10,4},{0,2},{1,2},{4,3},{11,4},{26,5},{120,7},{248,8},{1014,10},{65410,16},{65411,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {12,4},{27,5},{121,7},{502,9},{2038,11},{65412,16},{65413,16},{65414,16},{65415,16},{65416,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {28,5},{249,8},{1015,10},{4084,12},{65417,16},{65418,16},{65419,16},{65420,16},{65421,16},{65422,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {58,6},{503,9},{4085,12},{65423,16},{65424,16},{65425,16},{65426,16},{65427,16},{65428,16},{65429,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {59,6},{1016,10},{65430,16},{65431,16},{65432,16},{65433,16},{65434,16},{65435,16},{65436,16},{65437,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {122,7},{2039,11},{65438,16},{65439,16},{65440,16},{65441,16},{65442,16},{65443,16},{65444,16},{65445,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {123,7},{4086,12},{65446,16},{65447,16},{65448,16},{65449,16},{65450,16},{65451,16},{65452,16},{65453,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {250,8},{4087,12},{65454,16},{65455,16},{65456,16},{65457,16},{65458,16},{65459,16},{65460,16},{65461,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {504,9},{32704,15},{65462,16},{65463,16},{65464,16},{65465,16},{65466,16},{65467,16},{65468,16},{65469,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {505,9},{65470,16},{65471,16},{65472,16},{65473,16},{65474,16},{65475,16},{65476,16},{65477,16},{65478,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {506,9},{65479,16},{65480,16},{65481,16},{65482,16},{65483,16},{65484,16},{65485,16},{65486,16},{65487,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {1017,10},{65488,16},{65489,16},{65490,16},{65491,16},{65492,16},{65493,16},{65494,16},{65495,16},{65496,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {1018,10},{65497,16},{65498,16},{65499,16},{65500,16},{65501,16},{65502,16},{65503,16},{65504,16},{65505,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {2040,11},{65506,16},{65507,16},{65508,16},{65509,16},{65510,16},{65511,16},{65512,16},{65513,16},{65514,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {65515,16},{65516,16},{65517,16},{65518,16},{65519,16},{65520,16},{65521,16},{65522,16},{65523,16},{65524,16},{0,0},{0,0},{0,0},{0,0},{0,0}, + {2041,11},{65525,16},{65526,16},{65527,16},{65528,16},{65529,16},{65530,16},{65531,16},{65532,16},{65533,16},{65534,16},{0,0},{0,0},{0,0},{0,0},{0,0} + }; + static const unsigned short UVAC_HT[256][2] = { + {0,2},{1,2},{4,3},{10,4},{24,5},{25,5},{56,6},{120,7},{500,9},{1014,10},{4084,12},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {11,4},{57,6},{246,8},{501,9},{2038,11},{4085,12},{65416,16},{65417,16},{65418,16},{65419,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {26,5},{247,8},{1015,10},{4086,12},{32706,15},{65420,16},{65421,16},{65422,16},{65423,16},{65424,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {27,5},{248,8},{1016,10},{4087,12},{65425,16},{65426,16},{65427,16},{65428,16},{65429,16},{65430,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {58,6},{502,9},{65431,16},{65432,16},{65433,16},{65434,16},{65435,16},{65436,16},{65437,16},{65438,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {59,6},{1017,10},{65439,16},{65440,16},{65441,16},{65442,16},{65443,16},{65444,16},{65445,16},{65446,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {121,7},{2039,11},{65447,16},{65448,16},{65449,16},{65450,16},{65451,16},{65452,16},{65453,16},{65454,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {122,7},{2040,11},{65455,16},{65456,16},{65457,16},{65458,16},{65459,16},{65460,16},{65461,16},{65462,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {249,8},{65463,16},{65464,16},{65465,16},{65466,16},{65467,16},{65468,16},{65469,16},{65470,16},{65471,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {503,9},{65472,16},{65473,16},{65474,16},{65475,16},{65476,16},{65477,16},{65478,16},{65479,16},{65480,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {504,9},{65481,16},{65482,16},{65483,16},{65484,16},{65485,16},{65486,16},{65487,16},{65488,16},{65489,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {505,9},{65490,16},{65491,16},{65492,16},{65493,16},{65494,16},{65495,16},{65496,16},{65497,16},{65498,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {506,9},{65499,16},{65500,16},{65501,16},{65502,16},{65503,16},{65504,16},{65505,16},{65506,16},{65507,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {2041,11},{65508,16},{65509,16},{65510,16},{65511,16},{65512,16},{65513,16},{65514,16},{65515,16},{65516,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {16352,14},{65517,16},{65518,16},{65519,16},{65520,16},{65521,16},{65522,16},{65523,16},{65524,16},{65525,16},{0,0},{0,0},{0,0},{0,0},{0,0}, + {1018,10},{32707,15},{65526,16},{65527,16},{65528,16},{65529,16},{65530,16},{65531,16},{65532,16},{65533,16},{65534,16},{0,0},{0,0},{0,0},{0,0},{0,0} + }; + static const int YQT[] = {16,11,10,16,24,40,51,61,12,12,14,19,26,58,60,55,14,13,16,24,40,57,69,56,14,17,22,29,51,87,80,62,18,22, + 37,56,68,109,103,77,24,35,55,64,81,104,113,92,49,64,78,87,103,121,120,101,72,92,95,98,112,100,103,99}; + static const int UVQT[] = {17,18,24,47,99,99,99,99,18,21,26,66,99,99,99,99,24,26,56,99,99,99,99,99,47,66,99,99,99,99,99,99, + 99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99}; + static const float aasf[] = { 1.0f * 2.828427125f, 1.387039845f * 2.828427125f, 1.306562965f * 2.828427125f, 1.175875602f * 2.828427125f, + 1.0f * 2.828427125f, 0.785694958f * 2.828427125f, 0.541196100f * 2.828427125f, 0.275899379f * 2.828427125f }; + + int row, col, i, k, subsample; + float fdtbl_Y[64], fdtbl_UV[64]; + unsigned char YTable[64], UVTable[64]; + + if(!data || !width || !height || comp > 4 || comp < 1) { + return 0; + } + + quality = quality ? quality : 90; + subsample = quality <= 90 ? 1 : 0; + quality = quality < 1 ? 1 : quality > 100 ? 100 : quality; + quality = quality < 50 ? 5000 / quality : 200 - quality * 2; + + for(i = 0; i < 64; ++i) { + int uvti, yti = (YQT[i]*quality+50)/100; + YTable[stbiw__jpg_ZigZag[i]] = (unsigned char) (yti < 1 ? 1 : yti > 255 ? 255 : yti); + uvti = (UVQT[i]*quality+50)/100; + UVTable[stbiw__jpg_ZigZag[i]] = (unsigned char) (uvti < 1 ? 1 : uvti > 255 ? 255 : uvti); + } + + for(row = 0, k = 0; row < 8; ++row) { + for(col = 0; col < 8; ++col, ++k) { + fdtbl_Y[k] = 1 / (YTable [stbiw__jpg_ZigZag[k]] * aasf[row] * aasf[col]); + fdtbl_UV[k] = 1 / (UVTable[stbiw__jpg_ZigZag[k]] * aasf[row] * aasf[col]); + } + } + + // Write Headers + { + static const unsigned char head0[] = { 0xFF,0xD8,0xFF,0xE0,0,0x10,'J','F','I','F',0,1,1,0,0,1,0,1,0,0,0xFF,0xDB,0,0x84,0 }; + static const unsigned char head2[] = { 0xFF,0xDA,0,0xC,3,1,0,2,0x11,3,0x11,0,0x3F,0 }; + const unsigned char head1[] = { 0xFF,0xC0,0,0x11,8,(unsigned char)(height>>8),STBIW_UCHAR(height),(unsigned char)(width>>8),STBIW_UCHAR(width), + 3,1,(unsigned char)(subsample?0x22:0x11),0,2,0x11,1,3,0x11,1,0xFF,0xC4,0x01,0xA2,0 }; + s->func(s->context, (void*)head0, sizeof(head0)); + s->func(s->context, (void*)YTable, sizeof(YTable)); + stbiw__putc(s, 1); + s->func(s->context, UVTable, sizeof(UVTable)); + s->func(s->context, (void*)head1, sizeof(head1)); + s->func(s->context, (void*)(std_dc_luminance_nrcodes+1), sizeof(std_dc_luminance_nrcodes)-1); + s->func(s->context, (void*)std_dc_luminance_values, sizeof(std_dc_luminance_values)); + stbiw__putc(s, 0x10); // HTYACinfo + s->func(s->context, (void*)(std_ac_luminance_nrcodes+1), sizeof(std_ac_luminance_nrcodes)-1); + s->func(s->context, (void*)std_ac_luminance_values, sizeof(std_ac_luminance_values)); + stbiw__putc(s, 1); // HTUDCinfo + s->func(s->context, (void*)(std_dc_chrominance_nrcodes+1), sizeof(std_dc_chrominance_nrcodes)-1); + s->func(s->context, (void*)std_dc_chrominance_values, sizeof(std_dc_chrominance_values)); + stbiw__putc(s, 0x11); // HTUACinfo + s->func(s->context, (void*)(std_ac_chrominance_nrcodes+1), sizeof(std_ac_chrominance_nrcodes)-1); + s->func(s->context, (void*)std_ac_chrominance_values, sizeof(std_ac_chrominance_values)); + s->func(s->context, (void*)head2, sizeof(head2)); + } + + // Encode 8x8 macroblocks + { + static const unsigned short fillBits[] = {0x7F, 7}; + int DCY=0, DCU=0, DCV=0; + int bitBuf=0, bitCnt=0; + // comp == 2 is grey+alpha (alpha is ignored) + int ofsG = comp > 2 ? 1 : 0, ofsB = comp > 2 ? 2 : 0; + const unsigned char *dataR = (const unsigned char *)data; + const unsigned char *dataG = dataR + ofsG; + const unsigned char *dataB = dataR + ofsB; + int x, y, pos; + if(subsample) { + for(y = 0; y < height; y += 16) { + for(x = 0; x < width; x += 16) { + float Y[256], U[256], V[256]; + for(row = y, pos = 0; row < y+16; ++row) { + // row >= height => use last input row + int clamped_row = (row < height) ? row : height - 1; + int base_p = (stbi__flip_vertically_on_write ? (height-1-clamped_row) : clamped_row)*width*comp; + for(col = x; col < x+16; ++col, ++pos) { + // if col >= width => use pixel from last input column + int p = base_p + ((col < width) ? col : (width-1))*comp; + float r = dataR[p], g = dataG[p], b = dataB[p]; + Y[pos]= +0.29900f*r + 0.58700f*g + 0.11400f*b - 128; + U[pos]= -0.16874f*r - 0.33126f*g + 0.50000f*b; + V[pos]= +0.50000f*r - 0.41869f*g - 0.08131f*b; + } + } + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+0, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+8, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+128, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+136, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT); + + // subsample U,V + { + float subU[64], subV[64]; + int yy, xx; + for(yy = 0, pos = 0; yy < 8; ++yy) { + for(xx = 0; xx < 8; ++xx, ++pos) { + int j = yy*32+xx*2; + subU[pos] = (U[j+0] + U[j+1] + U[j+16] + U[j+17]) * 0.25f; + subV[pos] = (V[j+0] + V[j+1] + V[j+16] + V[j+17]) * 0.25f; + } + } + DCU = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, subU, 8, fdtbl_UV, DCU, UVDC_HT, UVAC_HT); + DCV = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, subV, 8, fdtbl_UV, DCV, UVDC_HT, UVAC_HT); + } + } + } + } else { + for(y = 0; y < height; y += 8) { + for(x = 0; x < width; x += 8) { + float Y[64], U[64], V[64]; + for(row = y, pos = 0; row < y+8; ++row) { + // row >= height => use last input row + int clamped_row = (row < height) ? row : height - 1; + int base_p = (stbi__flip_vertically_on_write ? (height-1-clamped_row) : clamped_row)*width*comp; + for(col = x; col < x+8; ++col, ++pos) { + // if col >= width => use pixel from last input column + int p = base_p + ((col < width) ? col : (width-1))*comp; + float r = dataR[p], g = dataG[p], b = dataB[p]; + Y[pos]= +0.29900f*r + 0.58700f*g + 0.11400f*b - 128; + U[pos]= -0.16874f*r - 0.33126f*g + 0.50000f*b; + V[pos]= +0.50000f*r - 0.41869f*g - 0.08131f*b; + } + } + + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y, 8, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCU = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, U, 8, fdtbl_UV, DCU, UVDC_HT, UVAC_HT); + DCV = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, V, 8, fdtbl_UV, DCV, UVDC_HT, UVAC_HT); + } + } + } + + // Do the bit alignment of the EOI marker + stbiw__jpg_writeBits(s, &bitBuf, &bitCnt, fillBits); + } + + // EOI + stbiw__putc(s, 0xFF); + stbiw__putc(s, 0xD9); + + return 1; +} + +STBIWDEF int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int quality) +{ + stbi__write_context s = { 0 }; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_jpg_core(&s, x, y, comp, (void *) data, quality); +} + + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_jpg(char const *filename, int x, int y, int comp, const void *data, int quality) +{ + stbi__write_context s = { 0 }; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_jpg_core(&s, x, y, comp, data, quality); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif + +#endif // STB_IMAGE_WRITE_IMPLEMENTATION + +/* Revision history + 1.16 (2021-07-11) + make Deflate code emit uncompressed blocks when it would otherwise expand + support writing BMPs with alpha channel + 1.15 (2020-07-13) unknown + 1.14 (2020-02-02) updated JPEG writer to downsample chroma channels + 1.13 + 1.12 + 1.11 (2019-08-11) + + 1.10 (2019-02-07) + support utf8 filenames in Windows; fix warnings and platform ifdefs + 1.09 (2018-02-11) + fix typo in zlib quality API, improve STB_I_W_STATIC in C++ + 1.08 (2018-01-29) + add stbi__flip_vertically_on_write, external zlib, zlib quality, choose PNG filter + 1.07 (2017-07-24) + doc fix + 1.06 (2017-07-23) + writing JPEG (using Jon Olick's code) + 1.05 ??? + 1.04 (2017-03-03) + monochrome BMP expansion + 1.03 ??? + 1.02 (2016-04-02) + avoid allocating large structures on the stack + 1.01 (2016-01-16) + STBIW_REALLOC_SIZED: support allocators with no realloc support + avoid race-condition in crc initialization + minor compile issues + 1.00 (2015-09-14) + installable file IO function + 0.99 (2015-09-13) + warning fixes; TGA rle support + 0.98 (2015-04-08) + added STBIW_MALLOC, STBIW_ASSERT etc + 0.97 (2015-01-18) + fixed HDR asserts, rewrote HDR rle logic + 0.96 (2015-01-17) + add HDR output + fix monochrome BMP + 0.95 (2014-08-17) + add monochrome TGA output + 0.94 (2014-05-31) + rename private functions to avoid conflicts with stb_image.h + 0.93 (2014-05-27) + warning fixes + 0.92 (2010-08-01) + casts to unsigned char to fix warnings + 0.91 (2010-07-17) + first public release + 0.90 first internal release +*/ + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ diff --git a/dvx/widgets/widgetBox.c b/dvx/widgets/widgetBox.c new file mode 100644 index 0000000..10c51e1 --- /dev/null +++ b/dvx/widgets/widgetBox.c @@ -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); +} diff --git a/dvx/widgets/widgetButton.c b/dvx/widgets/widgetButton.c new file mode 100644 index 0000000..e85bf8c --- /dev/null +++ b/dvx/widgets/widgetButton.c @@ -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); +} diff --git a/dvx/widgets/widgetCanvas.c b/dvx/widgets/widgetCanvas.c new file mode 100644 index 0000000..18be424 --- /dev/null +++ b/dvx/widgets/widgetCanvas.c @@ -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); +} diff --git a/dvx/widgets/widgetCheckbox.c b/dvx/widgets/widgetCheckbox.c new file mode 100644 index 0000000..80babc9 --- /dev/null +++ b/dvx/widgets/widgetCheckbox.c @@ -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); +} diff --git a/dvx/widgets/widgetComboBox.c b/dvx/widgets/widgetComboBox.c new file mode 100644 index 0000000..df34acc --- /dev/null +++ b/dvx/widgets/widgetComboBox.c @@ -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); + } +} diff --git a/dvx/widgets/widgetCore.c b/dvx/widgets/widgetCore.c new file mode 100644 index 0000000..b89783a --- /dev/null +++ b/dvx/widgets/widgetCore.c @@ -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; + } +} diff --git a/dvx/widgets/widgetDropdown.c b/dvx/widgets/widgetDropdown.c new file mode 100644 index 0000000..b95dd72 --- /dev/null +++ b/dvx/widgets/widgetDropdown.c @@ -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); + } +} diff --git a/dvx/widgets/widgetEvent.c b/dvx/widgets/widgetEvent.c new file mode 100644 index 0000000..740e0c4 --- /dev/null +++ b/dvx/widgets/widgetEvent.c @@ -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); + } +} diff --git a/dvx/widgets/widgetImage.c b/dvx/widgets/widgetImage.c new file mode 100644 index 0000000..3f18fd1 --- /dev/null +++ b/dvx/widgets/widgetImage.c @@ -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); +} diff --git a/dvx/widgets/widgetInternal.h b/dvx/widgets/widgetInternal.h new file mode 100644 index 0000000..948e10c --- /dev/null +++ b/dvx/widgets/widgetInternal.h @@ -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 +#include +#include + +// ============================================================ +// 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 diff --git a/dvx/widgets/widgetLabel.c b/dvx/widgets/widgetLabel.c new file mode 100644 index 0000000..e1a7d80 --- /dev/null +++ b/dvx/widgets/widgetLabel.c @@ -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); +} diff --git a/dvx/widgets/widgetLayout.c b/dvx/widgets/widgetLayout.c new file mode 100644 index 0000000..661c2db --- /dev/null +++ b/dvx/widgets/widgetLayout.c @@ -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; + } +} diff --git a/dvx/widgets/widgetListBox.c b/dvx/widgets/widgetListBox.c new file mode 100644 index 0000000..fc95a77 --- /dev/null +++ b/dvx/widgets/widgetListBox.c @@ -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; +} diff --git a/dvx/widgets/widgetOps.c b/dvx/widgets/widgetOps.c new file mode 100644 index 0000000..e9d8227 --- /dev/null +++ b/dvx/widgets/widgetOps.c @@ -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; + } +} diff --git a/dvx/widgets/widgetProgressBar.c b/dvx/widgets/widgetProgressBar.c new file mode 100644 index 0000000..93c7912 --- /dev/null +++ b/dvx/widgets/widgetProgressBar.c @@ -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); + } +} diff --git a/dvx/widgets/widgetRadio.c b/dvx/widgets/widgetRadio.c new file mode 100644 index 0000000..20c8ec3 --- /dev/null +++ b/dvx/widgets/widgetRadio.c @@ -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); +} diff --git a/dvx/widgets/widgetSeparator.c b/dvx/widgets/widgetSeparator.c new file mode 100644 index 0000000..3442cce --- /dev/null +++ b/dvx/widgets/widgetSeparator.c @@ -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); + } +} diff --git a/dvx/widgets/widgetSlider.c b/dvx/widgets/widgetSlider.c new file mode 100644 index 0000000..f4ce494 --- /dev/null +++ b/dvx/widgets/widgetSlider.c @@ -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); + } +} diff --git a/dvx/widgets/widgetSpacer.c b/dvx/widgets/widgetSpacer.c new file mode 100644 index 0000000..a5f4fe2 --- /dev/null +++ b/dvx/widgets/widgetSpacer.c @@ -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; +} diff --git a/dvx/widgets/widgetStatusBar.c b/dvx/widgets/widgetStatusBar.c new file mode 100644 index 0000000..ad5e5c6 --- /dev/null +++ b/dvx/widgets/widgetStatusBar.c @@ -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); + } +} diff --git a/dvx/widgets/widgetTabControl.c b/dvx/widgets/widgetTabControl.c new file mode 100644 index 0000000..eab266f --- /dev/null +++ b/dvx/widgets/widgetTabControl.c @@ -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++; + } +} diff --git a/dvx/widgets/widgetTextInput.c b/dvx/widgets/widgetTextInput.c new file mode 100644 index 0000000..26ab6cf --- /dev/null +++ b/dvx/widgets/widgetTextInput.c @@ -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); + } + } + } +} diff --git a/dvx/widgets/widgetToolbar.c b/dvx/widgets/widgetToolbar.c new file mode 100644 index 0000000..54960b3 --- /dev/null +++ b/dvx/widgets/widgetToolbar.c @@ -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); +} diff --git a/dvx/widgets/widgetTreeView.c b/dvx/widgets/widgetTreeView.c new file mode 100644 index 0000000..986328f --- /dev/null +++ b/dvx/widgets/widgetTreeView.c @@ -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); +}