// The MIT License (MIT) // // Copyright (C) 2026 Scott Duensing // // 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. #define DVX_WIDGET_IMPL // widgetImageButton.c -- Image button widget (button with image instead of text) // // Combines a Button's beveled border and press behavior with an Image's // bitmap rendering. The image is centered within the button bounds and // shifts by 1px on press, just like text in a regular button. // // Uses the same two-phase press model as Button: mouse press registers as // sDragWidget, keyboard press (Space/Enter) stores in sKeyPressedBtn, // and the event dispatcher handles release/cancel. The onClick callback // fires on release, not press. The drag/press state machine itself lives // in widgetOps.c (widgetPressableOn*) and drives w->pressed on the WidgetT. // // The widget takes ownership of the image data buffer -- if widget creation // fails, the data is freed to prevent leaks. // // The 4px added to min size (widgetImageButtonCalcMinSize) accounts for // the 2px bevel on each side -- no extra padding is added beyond that, // keeping image buttons compact for toolbar use. #include "dvxWgtP.h" // 2px bevel on every side -- min size and draw inset both derive from this. #define IMAGEBUTTON_BEVEL_W 2 static int32_t sTypeId = -1; typedef struct { uint8_t *pixelData; uint8_t *grayData; // lazily generated grayscale cache (NULL until needed) int32_t imgW; int32_t imgH; int32_t imgPitch; char picturePath[DVX_MAX_PATH]; } ImageButtonDataT; // ============================================================ // Prototypes // ============================================================ WidgetT *wgtImageButton(WidgetT *parent, uint8_t *pixelData, int32_t w, int32_t h, int32_t pitch); WidgetT *wgtImageButtonFromFile(WidgetT *parent, const char *path); static int32_t wgtImageButtonGetHeight(const WidgetT *w); static const char *wgtImageButtonGetPicture(const WidgetT *w); static int32_t wgtImageButtonGetWidth(const WidgetT *w); static void wgtImageButtonLoadFile(WidgetT *w, const char *path); void wgtImageButtonSetData(WidgetT *w, uint8_t *pixelData, int32_t imgW, int32_t imgH, int32_t pitch); void wgtRegister(void); void widgetImageButtonCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetImageButtonDestroy(WidgetT *w); void widgetImageButtonPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); WidgetT *wgtImageButton(WidgetT *parent, uint8_t *pixelData, int32_t w, int32_t h, int32_t pitch) { if (!parent || !pixelData || w <= 0 || h <= 0) { return NULL; } WidgetT *wgt = widgetAlloc(parent, sTypeId); if (!wgt) { free(pixelData); return NULL; } ImageButtonDataT *d = calloc(1, sizeof(ImageButtonDataT)); if (!d) { free(pixelData); widgetAllocRollback(wgt); return NULL; } d->pixelData = pixelData; d->imgW = w; d->imgH = h; d->imgPitch = pitch; wgt->data = d; return wgt; } WidgetT *wgtImageButtonFromFile(WidgetT *parent, const char *path) { if (!parent || !path) { return NULL; } AppContextT *ctx = wgtGetContext(parent); if (!ctx) { return NULL; } int32_t imgW; int32_t imgH; int32_t pitch; uint8_t *buf = dvxLoadImage(ctx, path, &imgW, &imgH, &pitch); if (!buf) { return NULL; } return wgtImageButton(parent, buf, imgW, imgH, pitch); } static int32_t wgtImageButtonGetHeight(const WidgetT *w) { VALIDATE_WIDGET(w, sTypeId, 0); ImageButtonDataT *d = (ImageButtonDataT *)w->data; return d->imgH; } static const char *wgtImageButtonGetPicture(const WidgetT *w) { VALIDATE_WIDGET(w, sTypeId, ""); ImageButtonDataT *d = (ImageButtonDataT *)w->data; return d->picturePath; } static int32_t wgtImageButtonGetWidth(const WidgetT *w) { VALIDATE_WIDGET(w, sTypeId, 0); ImageButtonDataT *d = (ImageButtonDataT *)w->data; return d->imgW; } static void wgtImageButtonLoadFile(WidgetT *w, const char *path) { VALIDATE_WIDGET_VOID(w, sTypeId); if (!path) { return; } AppContextT *ctx = wgtGetContext(w); if (!ctx) { return; } int32_t imgW; int32_t imgH; int32_t pitch; uint8_t *buf = dvxLoadImage(ctx, path, &imgW, &imgH, &pitch); if (!buf) { return; } ImageButtonDataT *d = (ImageButtonDataT *)w->data; snprintf(d->picturePath, sizeof(d->picturePath), "%s", path); wgtImageButtonSetData(w, buf, imgW, imgH, pitch); } void wgtImageButtonSetData(WidgetT *w, uint8_t *pixelData, int32_t imgW, int32_t imgH, int32_t pitch) { VALIDATE_WIDGET_VOID(w, sTypeId); ImageButtonDataT *d = (ImageButtonDataT *)w->data; free(d->pixelData); d->pixelData = pixelData; d->imgW = imgW; d->imgH = imgH; d->imgPitch = pitch; wgtInvalidate(w); } void widgetImageButtonCalcMinSize(WidgetT *w, const BitmapFontT *font) { (void)font; ImageButtonDataT *d = (ImageButtonDataT *)w->data; // Bevel border only, no extra padding w->calcMinW = d->imgW + 2 * IMAGEBUTTON_BEVEL_W; w->calcMinH = d->imgH + 2 * IMAGEBUTTON_BEVEL_W; } void widgetImageButtonDestroy(WidgetT *w) { ImageButtonDataT *d = (ImageButtonDataT *)w->data; if (d) { free(d->pixelData); free(d->grayData); free(d); } } void widgetImageButtonPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { (void)font; ImageButtonDataT *d = (ImageButtonDataT *)w->data; uint32_t bgFace = w->bgColor ? w->bgColor : colors->buttonFace; bool pressed = w->pressed && w->enabled; drawPressableBevel(disp, ops, w->x, w->y, w->w, w->h, pressed, bgFace, colors); if (d->pixelData) { int32_t imgX = w->x + (w->w - d->imgW) / 2; int32_t imgY = w->y + (w->h - d->imgH) / 2; if (pressed) { imgX++; imgY++; } if (w->enabled) { rectCopy(disp, ops, imgX, imgY, d->pixelData, d->imgPitch, 0, 0, d->imgW, d->imgH); } else { // Lazy-generate grayscale cache on first disabled paint if (!d->grayData) { int32_t bufSize = d->imgPitch * d->imgH; d->grayData = (uint8_t *)malloc(bufSize); if (d->grayData) { // Use a temp display to blit grayscale into the cache DisplayT tmp = *disp; tmp.backBuf = d->grayData; tmp.width = d->imgW; tmp.height = d->imgH; tmp.pitch = d->imgPitch; tmp.clipX = 0; tmp.clipY = 0; tmp.clipW = d->imgW; tmp.clipH = d->imgH; rectCopyGrayscale(&tmp, ops, 0, 0, d->pixelData, d->imgPitch, 0, 0, d->imgW, d->imgH); } } if (d->grayData) { rectCopy(disp, ops, imgX, imgY, d->grayData, d->imgPitch, 0, 0, d->imgW, d->imgH); } else { // Fallback if malloc failed rectCopy(disp, ops, imgX, imgY, d->pixelData, d->imgPitch, 0, 0, d->imgW, d->imgH); } } } if (w == sFocusedWidget) { uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; int32_t off = pressed ? 1 : 0; drawFocusRect(disp, ops, w->x + 3 + off, w->y + 3 + off, w->w - 6, w->h - 6, fg); } } // ============================================================ // DXE registration // ============================================================ static const WidgetClassT sClassImageButton = { .version = WGT_CLASS_VERSION, .flags = WCLASS_FOCUSABLE | WCLASS_PRESS_RELEASE, .handlers = { [WGT_METHOD_PAINT] = (void *)widgetImageButtonPaint, [WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetImageButtonCalcMinSize, [WGT_METHOD_ON_MOUSE] = (void *)widgetPressableOnMouse, [WGT_METHOD_ON_KEY] = (void *)widgetPressableOnKey, [WGT_METHOD_ON_ACCEL_ACTIVATE] = (void *)widgetPressableOnAccelActivate, [WGT_METHOD_DESTROY] = (void *)widgetImageButtonDestroy, [WGT_METHOD_ON_DRAG_UPDATE] = (void *)widgetPressableOnDragUpdate, [WGT_METHOD_ON_DRAG_END] = (void *)widgetPressableOnDragEnd, } }; static const struct { WidgetT *(*create)(WidgetT *parent, uint8_t *pixelData, int32_t w, int32_t h, int32_t pitch); WidgetT *(*fromFile)(WidgetT *parent, const char *path); void (*setData)(WidgetT *w, uint8_t *pixelData, int32_t imgW, int32_t imgH, int32_t pitch); void (*loadFile)(WidgetT *w, const char *path); } sApi = { .create = wgtImageButton, .fromFile = wgtImageButtonFromFile, .setData = wgtImageButtonSetData, .loadFile = wgtImageButtonLoadFile }; static const WgtPropDescT sImgBtnProps[] = { { "Picture", WGT_IFACE_STRING, (void *)wgtImageButtonGetPicture, (void *)wgtImageButtonLoadFile, NULL }, { "ImageWidth", WGT_IFACE_INT, (void *)wgtImageButtonGetWidth, NULL, NULL }, { "ImageHeight", WGT_IFACE_INT, (void *)wgtImageButtonGetHeight, NULL, NULL } }; static const WgtIfaceT sIface = { .basName = "ImageButton", .props = sImgBtnProps, .propCount = 3, .methods = NULL, .methodCount = 0, .events = NULL, .eventCount = 0, .createSig = WGT_CREATE_PARENT_DATA, .defaultEvent = "Click" }; void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassImageButton); wgtRegisterApi("imagebutton", &sApi); wgtRegisterIface("imagebutton", &sIface); }