334 lines
11 KiB
C
334 lines
11 KiB
C
// 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);
|
|
}
|