Restored bitmap buttons in demo toolbar. Added image load/save APIs. Removed duplicate image handling code.

This commit is contained in:
Scott Duensing 2026-03-18 00:31:10 -05:00
parent fddd97ad91
commit fd695afac8
8 changed files with 205 additions and 163 deletions

View file

@ -583,18 +583,39 @@ static void setupControlsWindow(void) {
WidgetT *tb = wgtToolbar(page5tb); WidgetT *tb = wgtToolbar(page5tb);
WidgetT *btnNew = wgtButton(tb, "&New"); char iconPath[272];
snprintf(iconPath, sizeof(iconPath), "%s/new.bmp", sDxeCtx->appDir);
WidgetT *btnNew = wgtImageButtonFromFile(tb, iconPath);
if (!btnNew) {
btnNew = wgtButton(tb, "&New");
}
btnNew->onClick = onToolbarClick; btnNew->onClick = onToolbarClick;
WidgetT *btnOpen = wgtButton(tb, "&Open");
snprintf(iconPath, sizeof(iconPath), "%s/open.bmp", sDxeCtx->appDir);
WidgetT *btnOpen = wgtImageButtonFromFile(tb, iconPath);
if (!btnOpen) {
btnOpen = wgtButton(tb, "&Open");
}
btnOpen->onClick = onToolbarClick; btnOpen->onClick = onToolbarClick;
WidgetT *btnSave = wgtButton(tb, "&Save");
snprintf(iconPath, sizeof(iconPath), "%s/save.bmp", sDxeCtx->appDir);
WidgetT *btnSave = wgtImageButtonFromFile(tb, iconPath);
if (!btnSave) {
btnSave = wgtButton(tb, "&Save");
}
btnSave->onClick = onToolbarClick; btnSave->onClick = onToolbarClick;
wgtVSeparator(tb); wgtVSeparator(tb);
WidgetT *btnHelp = wgtButton(tb, "&Help"); WidgetT *btnHelp = wgtButton(tb, "&Help");
btnHelp->onClick = onToolbarClick; btnHelp->onClick = onToolbarClick;
wgtLabel(page5tb, "Toolbar with text buttons and VSeparator."); wgtLabel(page5tb, "Toolbar with image buttons, text fallback, and VSeparator.");
// --- Tab 6: Media (Image from file) --- // --- Tab 6: Media (Image from file) ---
WidgetT *page6m = wgtTabPage(tabs, "&Media"); WidgetT *page6m = wgtTabPage(tabs, "&Media");

View file

@ -40,6 +40,7 @@
#include <ctype.h> #include <ctype.h>
#include <time.h> #include <time.h>
#include "thirdparty/stb_image.h"
#include "thirdparty/stb_image_write.h" #include "thirdparty/stb_image_write.h"
// Double-click timing uses CLOCKS_PER_SEC so it's portable between DJGPP // Double-click timing uses CLOCKS_PER_SEC so it's portable between DJGPP
@ -1447,6 +1448,15 @@ void dvxFreeAccelTable(AccelTableT *table) {
} }
// ============================================================
// dvxFreeImage
// ============================================================
void dvxFreeImage(uint8_t *data) {
free(data);
}
// ============================================================ // ============================================================
// dvxGetBlitOps // dvxGetBlitOps
// ============================================================ // ============================================================
@ -1549,6 +1559,75 @@ int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_
} }
// ============================================================
// dvxLoadImage
// ============================================================
//
// Public image loading API. Loads any image file supported by stb_image
// (BMP, PNG, JPEG, GIF, etc.) and converts the RGB pixels to the
// display's native pixel format for direct use with rectCopy, wgtImage,
// wgtImageButton, or any other pixel-data consumer. The caller owns the
// returned buffer and must free it with dvxFreeImage().
uint8_t *dvxLoadImage(const AppContextT *ctx, const char *path, int32_t *outW, int32_t *outH, int32_t *outPitch) {
if (!ctx || !path) {
return NULL;
}
const DisplayT *d = &ctx->display;
int imgW;
int imgH;
int channels;
uint8_t *rgb = stbi_load(path, &imgW, &imgH, &channels, 3);
if (!rgb) {
return NULL;
}
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);
if (outW) {
*outW = imgW;
}
if (outH) {
*outH = imgH;
}
if (outPitch) {
*outPitch = pitch;
}
return buf;
}
// ============================================================ // ============================================================
// dvxInvalidateRect // dvxInvalidateRect
// ============================================================ // ============================================================
@ -1692,6 +1771,35 @@ bool dvxUpdate(AppContextT *ctx) {
} }
// ============================================================
// dvxSaveImage
// ============================================================
//
// Save native-format pixel data to a PNG file. Converts from the
// display's native pixel format to RGB, then encodes as PNG via
// stb_image_write. This is the general-purpose image save function;
// dvxScreenshot and dvxWindowScreenshot are convenience wrappers
// around it for common use cases.
int32_t dvxSaveImage(const AppContextT *ctx, const uint8_t *data, int32_t w, int32_t h, int32_t pitch, const char *path) {
if (!ctx || !data || !path || w <= 0 || h <= 0) {
return -1;
}
uint8_t *rgb = bufferToRgb(&ctx->display, data, w, h, pitch);
if (!rgb) {
return -1;
}
int32_t result = stbi_write_png(path, w, h, 3, rgb, w * 3) ? 0 : -1;
free(rgb);
return result;
}
// ============================================================ // ============================================================
// dvxScreenshot // dvxScreenshot
// ============================================================ // ============================================================

View file

@ -199,6 +199,20 @@ void dvxTileWindowsH(AppContextT *ctx);
// Vertical tile: stacked, full width, equal height. // Vertical tile: stacked, full width, equal height.
void dvxTileWindowsV(AppContextT *ctx); void dvxTileWindowsV(AppContextT *ctx);
// Load an image file (BMP, PNG, JPEG, GIF) and convert to the display's
// native pixel format. Returns the pixel buffer on success (caller must
// free with dvxFreeImage), or NULL on failure. Output params receive the
// image dimensions and row pitch in bytes.
uint8_t *dvxLoadImage(const AppContextT *ctx, const char *path, int32_t *outW, int32_t *outH, int32_t *outPitch);
// Free a pixel buffer returned by dvxLoadImage.
void dvxFreeImage(uint8_t *data);
// Save native-format pixel data to a PNG file. The pixel data must be
// in the display's native format (as returned by dvxLoadImage or
// captured from a content buffer). Returns 0 on success, -1 on failure.
int32_t dvxSaveImage(const AppContextT *ctx, const uint8_t *data, int32_t w, int32_t h, int32_t pitch, const char *path);
// Copy text to the process-wide clipboard buffer. The clipboard is a // Copy text to the process-wide clipboard buffer. The clipboard is a
// simple static buffer (not inter-process) — adequate for copy/paste // simple static buffer (not inter-process) — adequate for copy/paste
// within the DVX environment on single-tasking DOS. // within the DVX environment on single-tasking DOS.

View file

@ -774,6 +774,10 @@ int32_t wgtSplitterGetPos(const WidgetT *w);
// Takes ownership of the data buffer (freed on destroy). // Takes ownership of the data buffer (freed on destroy).
WidgetT *wgtImageButton(WidgetT *parent, uint8_t *data, int32_t w, int32_t h, int32_t pitch); WidgetT *wgtImageButton(WidgetT *parent, uint8_t *data, int32_t w, int32_t h, int32_t pitch);
// Load an image button from a file (BMP, PNG, JPEG, GIF).
// Returns NULL on load failure; falls through gracefully.
WidgetT *wgtImageButtonFromFile(WidgetT *parent, const char *path);
// Replace the image data. Takes ownership of the new buffer. // Replace the image data. Takes ownership of the new buffer.
void wgtImageButtonSetData(WidgetT *w, uint8_t *data, int32_t imgW, int32_t imgH, int32_t pitch); void wgtImageButtonSetData(WidgetT *w, uint8_t *data, int32_t imgW, int32_t imgH, int32_t pitch);

View file

@ -22,8 +22,6 @@
// widget-space coordinates to canvas-space by subtracting the border offset. // widget-space coordinates to canvas-space by subtracting the border offset.
#include "widgetInternal.h" #include "widgetInternal.h"
#include "../thirdparty/stb_image.h"
#include "../thirdparty/stb_image_write.h"
#define CANVAS_BORDER 2 #define CANVAS_BORDER 2
@ -34,7 +32,6 @@
static void canvasDrawDot(WidgetT *w, int32_t cx, int32_t cy); static void canvasDrawDot(WidgetT *w, int32_t cx, int32_t cy);
static void canvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1); static void canvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1);
static void canvasUnpackColor(const DisplayT *d, uint32_t pixel, uint8_t *r, uint8_t *g, uint8_t *b);
// ============================================================ // ============================================================
@ -197,37 +194,6 @@ static void canvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32
} }
// ============================================================
// canvasUnpackColor
// ============================================================
//
// Reverse of packColor — extract RGB from a display-format pixel.
// Only used during PNG save (wgtCanvasSave) to convert the canvas's
// native-format pixels back to RGB for stb_image_write. The bit-shift
// approach works for 15/16/24/32-bit modes; 8-bit paletted mode uses
// a direct palette table lookup instead.
static void canvasUnpackColor(const DisplayT *d, uint32_t pixel, uint8_t *r, uint8_t *g, uint8_t *b) {
if (d->format.bitsPerPixel == 8) {
// 8-bit paletted — look up the palette entry
int32_t idx = pixel & 0xFF;
*r = d->palette[idx * 3 + 0];
*g = d->palette[idx * 3 + 1];
*b = d->palette[idx * 3 + 2];
return;
}
uint32_t rv = (pixel >> d->format.redShift) & ((1u << d->format.redBits) - 1);
uint32_t gv = (pixel >> d->format.greenShift) & ((1u << d->format.greenBits) - 1);
uint32_t bv = (pixel >> d->format.blueShift) & ((1u << d->format.blueBits) - 1);
// Scale back up to 8 bits
*r = (uint8_t)(rv << (8 - d->format.redBits));
*g = (uint8_t)(gv << (8 - d->format.greenBits));
*b = (uint8_t)(bv << (8 - d->format.blueBits));
}
// ============================================================ // ============================================================
// wgtCanvas // wgtCanvas
// ============================================================ // ============================================================
@ -559,67 +525,35 @@ uint32_t wgtCanvasGetPixel(const WidgetT *w, int32_t x, int32_t y) {
// ============================================================ // ============================================================
// Load an image file into the canvas, replacing the current content. // Load an image file into the canvas, replacing the current content.
// Uses stb_image for decoding (supports BMP, PNG, JPEG, GIF, etc.). // Delegates to dvxLoadImage for format decoding and pixel conversion.
// The loaded RGB pixels are converted to the display's native pixel format // The old buffer is freed and replaced with the new one — canvas
// during load so that subsequent repaints are just a memcpy. The old buffer // dimensions change to match the loaded image.
// is freed and replaced with the new one — canvas dimensions change to match
// the loaded image.
int32_t wgtCanvasLoad(WidgetT *w, const char *path) { int32_t wgtCanvasLoad(WidgetT *w, const char *path) {
if (!w || w->type != WidgetCanvasE || !path) { if (!w || w->type != WidgetCanvasE || !path) {
return -1; return -1;
} }
// Find the AppContextT to get display format AppContextT *ctx = wgtGetContext(w);
WidgetT *root = w;
while (root->parent) {
root = root->parent;
}
AppContextT *ctx = (AppContextT *)root->userData;
if (!ctx) { if (!ctx) {
return -1; return -1;
} }
const DisplayT *d = &ctx->display; int32_t imgW;
int32_t imgH;
int imgW; int32_t pitch;
int imgH; uint8_t *data = dvxLoadImage(ctx, path, &imgW, &imgH, &pitch);
int channels;
uint8_t *rgb = stbi_load(path, &imgW, &imgH, &channels, 3);
if (!rgb) {
return -1;
}
int32_t bpp = d->format.bytesPerPixel;
int32_t pitch = imgW * bpp;
uint8_t *data = (uint8_t *)malloc(pitch * imgH);
if (!data) { if (!data) {
stbi_image_free(rgb);
return -1; return -1;
} }
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 = data + y * pitch + x * bpp;
canvasPutPixel(dst, color, bpp);
}
}
stbi_image_free(rgb);
free(w->as.canvas.data); free(w->as.canvas.data);
w->as.canvas.data = data; w->as.canvas.data = data;
w->as.canvas.canvasW = imgW; w->as.canvas.canvasW = imgW;
w->as.canvas.canvasH = imgH; w->as.canvas.canvasH = imgH;
w->as.canvas.canvasPitch = pitch; w->as.canvas.canvasPitch = pitch;
w->as.canvas.canvasBpp = bpp; w->as.canvas.canvasBpp = ctx->display.format.bytesPerPixel;
return 0; return 0;
} }
@ -629,54 +563,20 @@ int32_t wgtCanvasLoad(WidgetT *w, const char *path) {
// wgtCanvasSave // wgtCanvasSave
// ============================================================ // ============================================================
// Save the canvas content to a PNG file. Since the canvas stores pixels in // Save the canvas content to a PNG file. Delegates to dvxSaveImage
// the display's native format (which varies per video mode), we must convert // which handles native-to-RGB conversion and PNG encoding.
// back to RGB before writing. This is the inverse of the load conversion.
int32_t wgtCanvasSave(WidgetT *w, const char *path) { int32_t wgtCanvasSave(WidgetT *w, const char *path) {
if (!w || w->type != WidgetCanvasE || !path || !w->as.canvas.data) { if (!w || w->type != WidgetCanvasE || !path || !w->as.canvas.data) {
return -1; return -1;
} }
// Find the AppContextT to get display format AppContextT *ctx = wgtGetContext(w);
WidgetT *root = w;
while (root->parent) {
root = root->parent;
}
AppContextT *ctx = (AppContextT *)root->userData;
if (!ctx) { if (!ctx) {
return -1; return -1;
} }
const DisplayT *d = &ctx->display; return dvxSaveImage(ctx, w->as.canvas.data, w->as.canvas.canvasW, w->as.canvas.canvasH, w->as.canvas.canvasPitch, path);
int32_t cw = w->as.canvas.canvasW;
int32_t ch = w->as.canvas.canvasH;
int32_t bpp = d->format.bytesPerPixel;
int32_t pitch = w->as.canvas.canvasPitch;
// Convert display format back to RGB
uint8_t *rgb = (uint8_t *)malloc(cw * ch * 3);
if (!rgb) {
return -1;
}
for (int32_t y = 0; y < ch; y++) {
for (int32_t x = 0; x < cw; x++) {
const uint8_t *src = w->as.canvas.data + y * pitch + x * bpp;
uint32_t pixel = canvasGetPixel(src, bpp);
uint8_t *dst = rgb + (y * cw + x) * 3;
canvasUnpackColor(d, pixel, &dst[0], &dst[1], &dst[2]);
}
}
int32_t result = stbi_write_png(path, cw, ch, 3, rgb, cw * 3);
free(rgb);
return result ? 0 : -1;
} }

View file

@ -17,7 +17,6 @@
// centering if the widget is larger than the image. // centering if the widget is larger than the image.
#include "widgetInternal.h" #include "widgetInternal.h"
#include "../thirdparty/stb_image.h"
// ============================================================ // ============================================================
@ -47,67 +46,28 @@ WidgetT *wgtImage(WidgetT *parent, uint8_t *data, int32_t w, int32_t h, int32_t
// ============================================================ // ============================================================
// //
// Load an image from a file (BMP, PNG, JPEG, GIF), convert to // Load an image from a file (BMP, PNG, JPEG, GIF), convert to
// display pixel format, and create an image widget. // display pixel format, and create an image widget. Delegates to
// dvxLoadImage for format decoding and pixel conversion.
// 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) { WidgetT *wgtImageFromFile(WidgetT *parent, const char *path) {
if (!parent || !path) { if (!parent || !path) {
return NULL; return NULL;
} }
// Find the AppContextT to get display format
AppContextT *ctx = wgtGetContext(parent); AppContextT *ctx = wgtGetContext(parent);
if (!ctx) { if (!ctx) {
return NULL; return NULL;
} }
const DisplayT *d = &ctx->display; int32_t imgW;
int32_t imgH;
// Load image via stb_image (force RGB) int32_t pitch;
int imgW; uint8_t *buf = dvxLoadImage(ctx, path, &imgW, &imgH, &pitch);
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) { if (!buf) {
stbi_image_free(rgb);
return NULL; 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); return wgtImage(parent, buf, imgW, imgH, pitch);
} }

View file

@ -44,6 +44,37 @@ WidgetT *wgtImageButton(WidgetT *parent, uint8_t *data, int32_t w, int32_t h, in
} }
// ============================================================
// wgtImageButtonFromFile
// ============================================================
//
// Load an image from disk and create an ImageButton widget.
// Delegates to dvxLoadImage for format decoding and pixel conversion.
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);
}
// ============================================================ // ============================================================
// wgtImageButtonSetData // wgtImageButtonSetData
// ============================================================ // ============================================================

View file

@ -159,6 +159,9 @@ DXE_EXPORT_TABLE(shellExportTable)
DXE_EXPORT(dvxGetDisplay) DXE_EXPORT(dvxGetDisplay)
DXE_EXPORT(dvxGetBlitOps) DXE_EXPORT(dvxGetBlitOps)
DXE_EXPORT(dvxSetWindowIcon) DXE_EXPORT(dvxSetWindowIcon)
DXE_EXPORT(dvxLoadImage)
DXE_EXPORT(dvxFreeImage)
DXE_EXPORT(dvxSaveImage)
DXE_EXPORT(dvxScreenshot) DXE_EXPORT(dvxScreenshot)
DXE_EXPORT(dvxWindowScreenshot) DXE_EXPORT(dvxWindowScreenshot)
DXE_EXPORT(dvxCreateAccelTable) DXE_EXPORT(dvxCreateAccelTable)
@ -311,6 +314,7 @@ DXE_EXPORT_TABLE(shellExportTable)
// dvxWidget.h — image / image button // dvxWidget.h — image / image button
DXE_EXPORT(wgtImageButton) DXE_EXPORT(wgtImageButton)
DXE_EXPORT(wgtImageButtonFromFile)
DXE_EXPORT(wgtImageButtonSetData) DXE_EXPORT(wgtImageButtonSetData)
DXE_EXPORT(wgtImage) DXE_EXPORT(wgtImage)
DXE_EXPORT(wgtImageFromFile) DXE_EXPORT(wgtImageFromFile)