DVX_GUI/dvx/widgets/widgetImage.c

202 lines
6.1 KiB
C

// 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 "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.
// Load an image from disk and create an Image widget. Uses stb_image for
// decoding (any format it supports: PNG, BMP, JPEG, GIF, etc.). The RGB
// pixels are converted to the display's native pixel format during load
// using packColor, then the raw RGB data is freed. The per-pixel bpp switch
// is duplicated here rather than using canvasPutPixel because this function
// is in a different compilation unit and inlining across units isn't guaranteed
// on DJGPP.
WidgetT *wgtImageFromFile(WidgetT *parent, const char *path) {
if (!parent || !path) {
return NULL;
}
// Find the AppContextT to get display format
AppContextT *ctx = wgtGetContext(parent);
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) {
VALIDATE_WIDGET_VOID(w, WidgetImageE);
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;
wgtInvalidate(w);
}
// ============================================================
// widgetImageDestroy
// ============================================================
void widgetImageDestroy(WidgetT *w) {
free(w->as.image.data);
}
// ============================================================
// widgetImageCalcMinSize
// ============================================================
void widgetImageCalcMinSize(WidgetT *w, const BitmapFontT *font) {
(void)font;
w->calcMinW = w->as.image.imgW;
w->calcMinH = w->as.image.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;
w->as.image.pressed = true;
wgtInvalidatePaint(w);
if (w->onClick) {
w->onClick(w);
}
w->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);
}