DVX_GUI/widgets/image/widgetImage.c

420 lines
12 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;
char picturePath[DVX_MAX_PATH];
bool stretch; // true = scale to fit widget bounds
} 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;
if (d->stretch) {
// Stretch mode: don't force the widget to the image size.
// Let the layout engine determine the size; the paint method
// will scale the image to fit.
w->calcMinW = 0;
w->calcMinH = 0;
} else {
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 || d->imgW <= 0 || d->imgH <= 0) {
return;
}
int32_t imgW = d->imgW;
int32_t imgH = d->imgH;
int32_t wgtW = w->w;
int32_t wgtH = w->h;
// Scale to fit (if stretch enabled) or use native size
int32_t fitW;
int32_t fitH;
if (d->stretch && (imgW > wgtW || imgH > wgtH)) {
fitW = wgtW;
fitH = (imgH * wgtW) / imgW;
if (fitH > wgtH) {
fitH = wgtH;
fitW = (imgW * wgtH) / imgH;
}
if (fitW <= 0) { fitW = 1; }
if (fitH <= 0) { fitH = 1; }
} else {
fitW = imgW;
fitH = imgH;
}
// Center in widget bounds
int32_t dx = w->x + (wgtW - fitW) / 2;
int32_t dy = w->y + (wgtH - fitH) / 2;
if (d->pressed) {
dx++;
dy++;
}
// If image fits at 1:1, blit directly (no scaling needed)
if (fitW == imgW && fitH == imgH) {
uint8_t *src = w->enabled ? d->pixelData : d->grayData;
if (!src) {
src = d->pixelData;
}
if (d->hasTransparency && w->enabled) {
rectCopyTransparent(disp, ops, dx, dy,
src, d->imgPitch,
0, 0, imgW, imgH, d->keyColor);
} else {
rectCopy(disp, ops, dx, dy,
src, d->imgPitch,
0, 0, imgW, imgH);
}
return;
}
// Nearest-neighbor scale: sample source pixels directly into the
// display backbuffer. This avoids allocating a scaled copy.
int32_t bpp = disp->format.bitsPerPixel;
for (int32_t y = 0; y < fitH; y++) {
int32_t screenY = dy + y;
if (screenY < disp->clipY || screenY >= disp->clipY + disp->clipH) {
continue;
}
int32_t srcY = (y * imgH) / fitH;
if (srcY >= imgH) {
srcY = imgH - 1;
}
uint8_t *srcRow = d->pixelData + srcY * d->imgPitch;
for (int32_t x = 0; x < fitW; x++) {
int32_t screenX = dx + x;
if (screenX < disp->clipX || screenX >= disp->clipX + disp->clipW) {
continue;
}
int32_t srcX = (x * imgW) / fitW;
if (srcX >= imgW) {
srcX = imgW - 1;
}
// Copy one pixel from source to display
if (bpp == 16 || bpp == 15) {
uint16_t px = ((uint16_t *)srcRow)[srcX];
((uint16_t *)(disp->backBuf + screenY * disp->pitch))[screenX] = px;
} else if (bpp == 32) {
uint32_t px = ((uint32_t *)srcRow)[srcX];
((uint32_t *)(disp->backBuf + screenY * disp->pitch))[screenX] = px;
} else if (bpp == 8) {
uint8_t px = srcRow[srcX];
(disp->backBuf + screenY * disp->pitch)[screenX] = px;
}
}
}
}
// 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;
}
dvxSetBusy(ctx, true);
int32_t imgW;
int32_t imgH;
int32_t pitch;
uint8_t *buf = dvxLoadImage(ctx, path, &imgW, &imgH, &pitch);
dvxSetBusy(ctx, false);
if (!buf) {
return;
}
ImageDataT *d = (ImageDataT *)w->data;
snprintf(d->picturePath, sizeof(d->picturePath), "%s", path);
wgtImageSetData(w, buf, imgW, imgH, pitch);
}
static bool wgtImageGetStretch(const WidgetT *w) {
VALIDATE_WIDGET(w, sTypeId, false);
ImageDataT *d = (ImageDataT *)w->data;
return d->stretch;
}
static void wgtImageSetStretch(WidgetT *w, bool stretch) {
VALIDATE_WIDGET_VOID(w, sTypeId);
ImageDataT *d = (ImageDataT *)w->data;
d->stretch = stretch;
wgtInvalidatePaint(w);
}
static const char *wgtImageGetPicture(const WidgetT *w) {
VALIDATE_WIDGET(w, sTypeId, "");
ImageDataT *d = (ImageDataT *)w->data;
return d->picturePath;
}
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[] = {
{ "ImageHeight", WGT_IFACE_INT, (void *)wgtImageGetHeight, NULL, NULL },
{ "ImageWidth", WGT_IFACE_INT, (void *)wgtImageGetWidth, NULL, NULL },
{ "Picture", WGT_IFACE_STRING, (void *)wgtImageGetPicture, (void *)wgtImageLoadFile, NULL },
{ "Stretch", WGT_IFACE_BOOL, (void *)wgtImageGetStretch, (void *)wgtImageSetStretch, NULL }
};
static const WgtIfaceT sIface = {
.basName = "Image",
.props = sProps,
.propCount = 4,
.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);
}