#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); }