332 lines
9.3 KiB
C
332 lines
9.3 KiB
C
#define DVX_WIDGET_IMPL
|
|
// widgetImage.c -- Image widget (displays bitmap, responds to clicks)
|
|
//
|
|
// Displays a bitmap image, optionally responding to clicks. The image data
|
|
// must be in the display's native pixel format (pre-converted). Two creation
|
|
// paths are provided:
|
|
// - wgtImage: from raw pixel data already in display format (takes ownership)
|
|
// - wgtImageFromFile: loads from file via stb_image and converts to display
|
|
// format during load
|
|
//
|
|
// The widget supports a simple press effect (1px offset on click) and fires
|
|
// onClick immediately on mouse-down. Unlike Button which has press/release
|
|
// tracking, Image fires instantly -- this is intentional for image-based
|
|
// click targets where visual press feedback is less important than
|
|
// responsiveness.
|
|
//
|
|
// No border or bevel is drawn -- the image fills its widget bounds with
|
|
// centering if the widget is larger than the image.
|
|
|
|
#include "dvxWgtP.h"
|
|
|
|
static int32_t sTypeId = -1;
|
|
|
|
typedef struct {
|
|
uint8_t *pixelData;
|
|
uint8_t *grayData;
|
|
int32_t imgW;
|
|
int32_t imgH;
|
|
int32_t imgPitch;
|
|
bool pressed;
|
|
bool hasTransparency;
|
|
uint32_t keyColor;
|
|
} ImageDataT;
|
|
|
|
|
|
// ============================================================
|
|
// widgetImageDestroy
|
|
// ============================================================
|
|
|
|
void widgetImageDestroy(WidgetT *w) {
|
|
ImageDataT *d = (ImageDataT *)w->data;
|
|
|
|
if (d) {
|
|
free(d->pixelData);
|
|
free(d->grayData);
|
|
free(d);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetImageCalcMinSize
|
|
// ============================================================
|
|
|
|
void widgetImageCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
|
(void)font;
|
|
ImageDataT *d = (ImageDataT *)w->data;
|
|
w->calcMinW = d->imgW;
|
|
w->calcMinH = d->imgH;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetImageOnMouse
|
|
// ============================================================
|
|
|
|
// Mouse click: briefly sets pressed=true (for the 1px offset effect),
|
|
// invalidates to show the press, fires onClick, then immediately clears
|
|
// pressed. The press is purely visual -- there's no release tracking like
|
|
// Button has, since Image clicks are meant for instant actions (e.g.,
|
|
// clicking a logo or icon area).
|
|
void widgetImageOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
|
(void)root;
|
|
(void)vx;
|
|
(void)vy;
|
|
ImageDataT *d = (ImageDataT *)w->data;
|
|
d->pressed = true;
|
|
wgtInvalidatePaint(w);
|
|
|
|
if (w->onClick) {
|
|
w->onClick(w);
|
|
}
|
|
|
|
d->pressed = false;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetImagePaint
|
|
// ============================================================
|
|
|
|
void widgetImagePaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
|
(void)font;
|
|
(void)colors;
|
|
ImageDataT *d = (ImageDataT *)w->data;
|
|
|
|
if (!d->pixelData) {
|
|
return;
|
|
}
|
|
|
|
// Center the image within the widget bounds
|
|
int32_t imgW = d->imgW;
|
|
int32_t imgH = d->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 (d->pressed) {
|
|
dx++;
|
|
dy++;
|
|
}
|
|
|
|
if (w->enabled) {
|
|
if (d->hasTransparency) {
|
|
rectCopyTransparent(disp, ops, dx, dy,
|
|
d->pixelData, d->imgPitch,
|
|
0, 0, imgW, imgH, d->keyColor);
|
|
} else {
|
|
rectCopy(disp, ops, dx, dy,
|
|
d->pixelData, d->imgPitch,
|
|
0, 0, imgW, imgH);
|
|
}
|
|
} else {
|
|
if (!d->grayData) {
|
|
int32_t bufSize = d->imgPitch * d->imgH;
|
|
d->grayData = (uint8_t *)malloc(bufSize);
|
|
|
|
if (d->grayData) {
|
|
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, dx, dy,
|
|
d->grayData, d->imgPitch,
|
|
0, 0, imgW, imgH);
|
|
} else {
|
|
rectCopy(disp, ops, dx, dy,
|
|
d->pixelData, d->imgPitch,
|
|
0, 0, imgW, imgH);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Forward declaration
|
|
static void wgtImageLoadFile(WidgetT *w, const char *path);
|
|
|
|
// ============================================================
|
|
// DXE registration
|
|
// ============================================================
|
|
|
|
static const WidgetClassT sClassImage = {
|
|
.version = WGT_CLASS_VERSION,
|
|
.flags = 0,
|
|
.handlers = {
|
|
[WGT_METHOD_PAINT] = (void *)widgetImagePaint,
|
|
[WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetImageCalcMinSize,
|
|
[WGT_METHOD_ON_MOUSE] = (void *)widgetImageOnMouse,
|
|
[WGT_METHOD_DESTROY] = (void *)widgetImageDestroy,
|
|
[WGT_METHOD_SET_TEXT] = (void *)wgtImageLoadFile,
|
|
}
|
|
};
|
|
|
|
|
|
// ============================================================
|
|
// Widget creation functions
|
|
// ============================================================
|
|
|
|
|
|
WidgetT *wgtImage(WidgetT *parent, uint8_t *pixelData, int32_t w, int32_t h, int32_t pitch) {
|
|
WidgetT *wgt = widgetAlloc(parent, sTypeId);
|
|
|
|
if (wgt) {
|
|
ImageDataT *d = calloc(1, sizeof(ImageDataT));
|
|
d->pixelData = pixelData;
|
|
d->imgW = w;
|
|
d->imgH = h;
|
|
d->imgPitch = pitch;
|
|
d->pressed = false;
|
|
wgt->data = d;
|
|
}
|
|
|
|
return wgt;
|
|
}
|
|
|
|
|
|
WidgetT *wgtImageFromFile(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 wgtImage(parent, buf, imgW, imgH, pitch);
|
|
}
|
|
|
|
|
|
void wgtImageSetData(WidgetT *w, uint8_t *pixelData, int32_t imgW, int32_t imgH, int32_t pitch) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
ImageDataT *d = (ImageDataT *)w->data;
|
|
|
|
free(d->pixelData);
|
|
free(d->grayData);
|
|
d->pixelData = pixelData;
|
|
d->grayData = NULL;
|
|
d->imgW = imgW;
|
|
d->imgH = imgH;
|
|
d->imgPitch = pitch;
|
|
wgtInvalidate(w);
|
|
}
|
|
|
|
|
|
void wgtImageSetTransparent(WidgetT *w, bool hasTransparency, uint32_t keyColor) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
ImageDataT *d = (ImageDataT *)w->data;
|
|
d->hasTransparency = hasTransparency;
|
|
d->keyColor = keyColor;
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// BASIC-facing accessors
|
|
// ============================================================
|
|
|
|
static void wgtImageLoadFile(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;
|
|
}
|
|
|
|
wgtImageSetData(w, buf, imgW, imgH, pitch);
|
|
}
|
|
|
|
|
|
static int32_t wgtImageGetWidth(const WidgetT *w) {
|
|
VALIDATE_WIDGET(w, sTypeId, 0);
|
|
ImageDataT *d = (ImageDataT *)w->data;
|
|
return d->imgW;
|
|
}
|
|
|
|
|
|
static int32_t wgtImageGetHeight(const WidgetT *w) {
|
|
VALIDATE_WIDGET(w, sTypeId, 0);
|
|
ImageDataT *d = (ImageDataT *)w->data;
|
|
return d->imgH;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// DXE registration
|
|
// ============================================================
|
|
|
|
|
|
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);
|
|
void (*setTransparent)(WidgetT *w, bool hasTransparency, uint32_t keyColor);
|
|
} sApi = {
|
|
.create = wgtImage,
|
|
.fromFile = wgtImageFromFile,
|
|
.setData = wgtImageSetData,
|
|
.loadFile = wgtImageLoadFile,
|
|
.setTransparent = wgtImageSetTransparent
|
|
};
|
|
|
|
static const WgtPropDescT sProps[] = {
|
|
{ "Picture", WGT_IFACE_STRING, NULL, (void *)wgtImageLoadFile, NULL },
|
|
{ "ImageWidth", WGT_IFACE_INT, (void *)wgtImageGetWidth, NULL, NULL },
|
|
{ "ImageHeight", WGT_IFACE_INT, (void *)wgtImageGetHeight, NULL, NULL }
|
|
};
|
|
|
|
static const WgtIfaceT sIface = {
|
|
.basName = "Image",
|
|
.props = sProps,
|
|
.propCount = 3,
|
|
.methods = NULL,
|
|
.methodCount = 0,
|
|
.events = NULL,
|
|
.eventCount = 0,
|
|
.createSig = WGT_CREATE_PARENT_DATA,
|
|
.defaultEvent = "Click"
|
|
};
|
|
|
|
void wgtRegister(void) {
|
|
sTypeId = wgtRegisterClass(&sClassImage);
|
|
wgtRegisterApi("image", &sApi);
|
|
wgtRegisterIface("image", &sIface);
|
|
}
|