826 lines
24 KiB
C
826 lines
24 KiB
C
// widgetCanvas.c — Drawable canvas widget (freehand draw, PNG save/load)
|
|
//
|
|
// The canvas widget provides a pixel buffer in the display's native pixel
|
|
// format that applications can draw into directly. It stores pixels in
|
|
// display format (not always RGB) to avoid per-pixel conversion on every
|
|
// repaint — the paint function just does a straight rectCopy blit from
|
|
// the canvas buffer to the display. This is critical on a 486 where
|
|
// per-pixel format conversion during repaint would be prohibitively slow.
|
|
//
|
|
// The tradeoff is that load/save operations must convert between RGB and
|
|
// the display format, but those are one-time costs at I/O time rather
|
|
// than per-frame costs.
|
|
//
|
|
// Drawing operations (dot, line, rect, circle) operate directly on the
|
|
// canvas buffer using canvasPutPixel for pixel-level writes. The dot
|
|
// primitive draws a filled circle using the pen size, and line uses
|
|
// Bresenham's algorithm placing dots along the path. This gives smooth
|
|
// freehand drawing with variable pen widths.
|
|
//
|
|
// Canvas coordinates are independent of widget position — (0,0) is the
|
|
// top-left of the canvas content, not the widget. Mouse events translate
|
|
// widget-space coordinates to canvas-space by subtracting the border offset.
|
|
|
|
#include "widgetInternal.h"
|
|
#include "../thirdparty/stb_image.h"
|
|
#include "../thirdparty/stb_image_write.h"
|
|
|
|
#define CANVAS_BORDER 2
|
|
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
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 canvasUnpackColor(const DisplayT *d, uint32_t pixel, uint8_t *r, uint8_t *g, uint8_t *b);
|
|
|
|
|
|
// ============================================================
|
|
// canvasGetPixel / canvasPutPixel
|
|
// ============================================================
|
|
//
|
|
// Read/write a single pixel at the given address, respecting the display's
|
|
// bytes-per-pixel depth (1=8-bit palette, 2=16-bit hicolor, 4=32-bit truecolor).
|
|
// These are inline because they're called per-pixel in tight loops (circle fill,
|
|
// line draw) — the function call overhead would dominate on a 486. The bpp
|
|
// branch is predictable since it doesn't change within a single draw operation.
|
|
|
|
static inline uint32_t canvasGetPixel(const uint8_t *src, int32_t bpp) {
|
|
if (bpp == 1) {
|
|
return *src;
|
|
} else if (bpp == 2) {
|
|
return *(const uint16_t *)src;
|
|
} else {
|
|
return *(const uint32_t *)src;
|
|
}
|
|
}
|
|
|
|
|
|
static inline void canvasPutPixel(uint8_t *dst, uint32_t color, int32_t bpp) {
|
|
if (bpp == 1) {
|
|
*dst = (uint8_t)color;
|
|
} else if (bpp == 2) {
|
|
*(uint16_t *)dst = (uint16_t)color;
|
|
} else {
|
|
*(uint32_t *)dst = color;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// canvasDrawDot
|
|
// ============================================================
|
|
//
|
|
// Draw a filled circle of diameter penSize at (cx, cy) in canvas coords.
|
|
|
|
static void canvasDrawDot(WidgetT *w, int32_t cx, int32_t cy) {
|
|
int32_t bpp = w->as.canvas.canvasBpp;
|
|
int32_t pitch = w->as.canvas.canvasPitch;
|
|
uint8_t *data = w->as.canvas.data;
|
|
int32_t cw = w->as.canvas.canvasW;
|
|
int32_t ch = w->as.canvas.canvasH;
|
|
uint32_t color = w->as.canvas.penColor;
|
|
int32_t rad = w->as.canvas.penSize / 2;
|
|
|
|
if (rad < 1) {
|
|
// Single pixel
|
|
if (cx >= 0 && cx < cw && cy >= 0 && cy < ch) {
|
|
uint8_t *dst = data + cy * pitch + cx * bpp;
|
|
|
|
canvasPutPixel(dst, color, bpp);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Filled circle via per-row horizontal span. For each row (dy offset from
|
|
// center), compute the horizontal extent using the circle equation
|
|
// dx^2 + dy^2 <= r^2. The horizontal half-span is floor(sqrt(r^2 - dy^2)).
|
|
// This approach is faster than checking each pixel individually because
|
|
// the inner loop just fills a horizontal run — no per-pixel distance check.
|
|
int32_t r2 = rad * rad;
|
|
|
|
for (int32_t dy = -rad; dy <= rad; dy++) {
|
|
int32_t py = cy + dy;
|
|
|
|
if (py < 0 || py >= ch) {
|
|
continue;
|
|
}
|
|
|
|
// Compute horizontal half-span: dx² <= r² - dy²
|
|
int32_t dy2 = dy * dy;
|
|
int32_t rem = r2 - dy2;
|
|
int32_t hspan = 0;
|
|
|
|
// Integer sqrt via Newton's method (Babylonian method). 8 iterations
|
|
// is more than enough to converge for any radius that fits in int32_t.
|
|
// Using integer sqrt avoids pulling in the FPU which may not be
|
|
// present on 486SX systems, and avoids the float-to-int conversion
|
|
// overhead even on systems with an FPU.
|
|
if (rem > 0) {
|
|
hspan = rad;
|
|
|
|
for (int32_t i = 0; i < 8; i++) {
|
|
hspan = (hspan + rem / hspan) / 2;
|
|
}
|
|
|
|
if (hspan * hspan > rem) {
|
|
hspan--;
|
|
}
|
|
}
|
|
|
|
int32_t x0 = cx - hspan;
|
|
int32_t x1 = cx + hspan;
|
|
|
|
if (x0 < 0) {
|
|
x0 = 0;
|
|
}
|
|
|
|
if (x1 >= cw) {
|
|
x1 = cw - 1;
|
|
}
|
|
|
|
if (x0 <= x1) {
|
|
uint8_t *dst = data + py * pitch + x0 * bpp;
|
|
|
|
for (int32_t px = x0; px <= x1; px++) {
|
|
canvasPutPixel(dst, color, bpp);
|
|
dst += bpp;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// canvasDrawLine
|
|
// ============================================================
|
|
//
|
|
// Bresenham line from (x0,y0) to (x1,y1), placing dots along the path.
|
|
// Each point on the line gets a full pen dot (canvasDrawDot), which means
|
|
// lines with large pen sizes are smooth rather than aliased. Bresenham was
|
|
// chosen over DDA because it's pure integer arithmetic — no floating point
|
|
// needed, which matters on 486SX (no FPU).
|
|
|
|
static void canvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1) {
|
|
int32_t dx = x1 - x0;
|
|
int32_t dy = y1 - y0;
|
|
int32_t sx = (dx >= 0) ? 1 : -1;
|
|
int32_t sy = (dy >= 0) ? 1 : -1;
|
|
|
|
if (dx < 0) { dx = -dx; }
|
|
if (dy < 0) { dy = -dy; }
|
|
|
|
int32_t err = dx - dy;
|
|
|
|
for (;;) {
|
|
canvasDrawDot(w, x0, y0);
|
|
|
|
if (x0 == x1 && y0 == y1) {
|
|
break;
|
|
}
|
|
|
|
int32_t e2 = 2 * err;
|
|
|
|
if (e2 > -dy) {
|
|
err -= dy;
|
|
x0 += sx;
|
|
}
|
|
|
|
if (e2 < dx) {
|
|
err += dx;
|
|
y0 += sy;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// 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
|
|
// ============================================================
|
|
|
|
// Create a canvas widget with the specified dimensions. The canvas buffer
|
|
// is allocated in the display's native pixel format by walking up the widget
|
|
// tree to find the AppContextT (which holds the display format info). This
|
|
// tree-walk pattern is necessary because the widget doesn't have direct access
|
|
// to the display — only the root widget's userData points to the AppContextT.
|
|
// The buffer is initialized to white using spanFill for performance.
|
|
WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h) {
|
|
if (!parent || w <= 0 || h <= 0) {
|
|
return NULL;
|
|
}
|
|
|
|
// Find the AppContextT to get display format
|
|
WidgetT *root = parent;
|
|
|
|
while (root->parent) {
|
|
root = root->parent;
|
|
}
|
|
|
|
AppContextT *ctx = (AppContextT *)root->userData;
|
|
|
|
if (!ctx) {
|
|
return NULL;
|
|
}
|
|
|
|
const DisplayT *d = &ctx->display;
|
|
int32_t bpp = d->format.bytesPerPixel;
|
|
int32_t pitch = w * bpp;
|
|
|
|
uint8_t *data = (uint8_t *)malloc(pitch * h);
|
|
|
|
if (!data) {
|
|
return NULL;
|
|
}
|
|
|
|
// Fill with white using span fill for performance
|
|
uint32_t white = packColor(d, 255, 255, 255);
|
|
BlitOpsT canvasOps;
|
|
drawInit(&canvasOps, d);
|
|
|
|
for (int32_t y = 0; y < h; y++) {
|
|
canvasOps.spanFill(data + y * pitch, white, w);
|
|
}
|
|
|
|
WidgetT *wgt = widgetAlloc(parent, WidgetCanvasE);
|
|
|
|
if (wgt) {
|
|
wgt->as.canvas.data = data;
|
|
wgt->as.canvas.canvasW = w;
|
|
wgt->as.canvas.canvasH = h;
|
|
wgt->as.canvas.canvasPitch = pitch;
|
|
wgt->as.canvas.canvasBpp = bpp;
|
|
wgt->as.canvas.penColor = packColor(d, 0, 0, 0);
|
|
wgt->as.canvas.penSize = 2;
|
|
wgt->as.canvas.lastX = -1;
|
|
wgt->as.canvas.lastY = -1;
|
|
} else {
|
|
free(data);
|
|
}
|
|
|
|
return wgt;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtCanvasClear
|
|
// ============================================================
|
|
|
|
void wgtCanvasClear(WidgetT *w, uint32_t color) {
|
|
if (!w || w->type != WidgetCanvasE || !w->as.canvas.data) {
|
|
return;
|
|
}
|
|
|
|
// Find BlitOps for span fill
|
|
WidgetT *root = w;
|
|
while (root->parent) {
|
|
root = root->parent;
|
|
}
|
|
AppContextT *ctx = (AppContextT *)root->userData;
|
|
if (!ctx) {
|
|
return;
|
|
}
|
|
|
|
int32_t pitch = w->as.canvas.canvasPitch;
|
|
int32_t cw = w->as.canvas.canvasW;
|
|
int32_t ch = w->as.canvas.canvasH;
|
|
|
|
for (int32_t y = 0; y < ch; y++) {
|
|
ctx->blitOps.spanFill(w->as.canvas.data + y * pitch, color, cw);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtCanvasDrawLine
|
|
// ============================================================
|
|
//
|
|
// Draw a line using the current pen color and pen size.
|
|
|
|
void wgtCanvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1) {
|
|
if (!w || w->type != WidgetCanvasE || !w->as.canvas.data) {
|
|
return;
|
|
}
|
|
|
|
canvasDrawLine(w, x0, y0, x1, y1);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtCanvasDrawRect
|
|
// ============================================================
|
|
//
|
|
// Draw a 1px outlined rectangle using the current pen color.
|
|
|
|
void wgtCanvasDrawRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height) {
|
|
if (!w || w->type != WidgetCanvasE || !w->as.canvas.data) {
|
|
return;
|
|
}
|
|
|
|
if (width <= 0 || height <= 0) {
|
|
return;
|
|
}
|
|
|
|
int32_t bpp = w->as.canvas.canvasBpp;
|
|
int32_t pitch = w->as.canvas.canvasPitch;
|
|
uint8_t *data = w->as.canvas.data;
|
|
int32_t cw = w->as.canvas.canvasW;
|
|
int32_t ch = w->as.canvas.canvasH;
|
|
uint32_t color = w->as.canvas.penColor;
|
|
|
|
// Top and bottom edges
|
|
for (int32_t px = x; px < x + width; px++) {
|
|
if (px < 0 || px >= cw) {
|
|
continue;
|
|
}
|
|
|
|
if (y >= 0 && y < ch) {
|
|
uint8_t *dst = data + y * pitch + px * bpp;
|
|
|
|
canvasPutPixel(dst, color, bpp);
|
|
}
|
|
|
|
int32_t by = y + height - 1;
|
|
|
|
if (by >= 0 && by < ch && by != y) {
|
|
uint8_t *dst = data + by * pitch + px * bpp;
|
|
|
|
canvasPutPixel(dst, color, bpp);
|
|
}
|
|
}
|
|
|
|
// Left and right edges (excluding corners already drawn)
|
|
for (int32_t py = y + 1; py < y + height - 1; py++) {
|
|
if (py < 0 || py >= ch) {
|
|
continue;
|
|
}
|
|
|
|
if (x >= 0 && x < cw) {
|
|
uint8_t *dst = data + py * pitch + x * bpp;
|
|
|
|
canvasPutPixel(dst, color, bpp);
|
|
}
|
|
|
|
int32_t rx = x + width - 1;
|
|
|
|
if (rx >= 0 && rx < cw && rx != x) {
|
|
uint8_t *dst = data + py * pitch + rx * bpp;
|
|
|
|
canvasPutPixel(dst, color, bpp);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtCanvasFillCircle
|
|
// ============================================================
|
|
//
|
|
// Draw a filled circle using the current pen color.
|
|
|
|
void wgtCanvasFillCircle(WidgetT *w, int32_t cx, int32_t cy, int32_t radius) {
|
|
if (!w || w->type != WidgetCanvasE || !w->as.canvas.data) {
|
|
return;
|
|
}
|
|
|
|
if (radius <= 0) {
|
|
return;
|
|
}
|
|
|
|
int32_t bpp = w->as.canvas.canvasBpp;
|
|
int32_t pitch = w->as.canvas.canvasPitch;
|
|
uint8_t *data = w->as.canvas.data;
|
|
int32_t cw = w->as.canvas.canvasW;
|
|
int32_t ch = w->as.canvas.canvasH;
|
|
uint32_t color = w->as.canvas.penColor;
|
|
int32_t r2 = radius * radius;
|
|
|
|
for (int32_t dy = -radius; dy <= radius; dy++) {
|
|
int32_t py = cy + dy;
|
|
|
|
if (py < 0 || py >= ch) {
|
|
continue;
|
|
}
|
|
|
|
// Compute horizontal half-span: dx² <= r² - dy²
|
|
int32_t dy2 = dy * dy;
|
|
int32_t rem = r2 - dy2;
|
|
int32_t hspan = 0;
|
|
|
|
// Integer sqrt via Newton's method
|
|
if (rem > 0) {
|
|
hspan = radius;
|
|
|
|
for (int32_t i = 0; i < 8; i++) {
|
|
hspan = (hspan + rem / hspan) / 2;
|
|
}
|
|
|
|
if (hspan * hspan > rem) {
|
|
hspan--;
|
|
}
|
|
}
|
|
|
|
int32_t x0 = cx - hspan;
|
|
int32_t x1 = cx + hspan;
|
|
|
|
if (x0 < 0) {
|
|
x0 = 0;
|
|
}
|
|
|
|
if (x1 >= cw) {
|
|
x1 = cw - 1;
|
|
}
|
|
|
|
if (x0 <= x1) {
|
|
uint8_t *dst = data + py * pitch + x0 * bpp;
|
|
|
|
for (int32_t px = x0; px <= x1; px++) {
|
|
canvasPutPixel(dst, color, bpp);
|
|
dst += bpp;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtCanvasFillRect
|
|
// ============================================================
|
|
//
|
|
// Draw a filled rectangle using the current pen color.
|
|
|
|
void wgtCanvasFillRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height) {
|
|
if (!w || w->type != WidgetCanvasE || !w->as.canvas.data) {
|
|
return;
|
|
}
|
|
|
|
if (width <= 0 || height <= 0) {
|
|
return;
|
|
}
|
|
|
|
int32_t bpp = w->as.canvas.canvasBpp;
|
|
int32_t pitch = w->as.canvas.canvasPitch;
|
|
uint8_t *data = w->as.canvas.data;
|
|
int32_t cw = w->as.canvas.canvasW;
|
|
int32_t ch = w->as.canvas.canvasH;
|
|
uint32_t color = w->as.canvas.penColor;
|
|
|
|
// Clip to canvas bounds
|
|
int32_t x0 = x < 0 ? 0 : x;
|
|
int32_t y0 = y < 0 ? 0 : y;
|
|
int32_t x1 = x + width > cw ? cw : x + width;
|
|
int32_t y1 = y + height > ch ? ch : y + height;
|
|
int32_t fillW = x1 - x0;
|
|
|
|
if (fillW <= 0) {
|
|
return;
|
|
}
|
|
|
|
// Find BlitOps for span fill
|
|
WidgetT *root = w;
|
|
while (root->parent) {
|
|
root = root->parent;
|
|
}
|
|
AppContextT *ctx = (AppContextT *)root->userData;
|
|
|
|
// Use the optimized spanFill (which uses rep stosl on x86) when available,
|
|
// falling back to per-pixel writes if the AppContextT can't be reached.
|
|
// The spanFill path is ~4x faster for 32-bit modes because it writes
|
|
// 4 bytes per iteration instead of going through the bpp switch.
|
|
if (ctx) {
|
|
for (int32_t py = y0; py < y1; py++) {
|
|
ctx->blitOps.spanFill(data + py * pitch + x0 * bpp, color, fillW);
|
|
}
|
|
} else {
|
|
for (int32_t py = y0; py < y1; py++) {
|
|
for (int32_t px = x0; px < x1; px++) {
|
|
canvasPutPixel(data + py * pitch + px * bpp, color, bpp);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtCanvasGetPixel
|
|
// ============================================================
|
|
|
|
uint32_t wgtCanvasGetPixel(const WidgetT *w, int32_t x, int32_t y) {
|
|
if (!w || w->type != WidgetCanvasE || !w->as.canvas.data) {
|
|
return 0;
|
|
}
|
|
|
|
if (x < 0 || x >= w->as.canvas.canvasW || y < 0 || y >= w->as.canvas.canvasH) {
|
|
return 0;
|
|
}
|
|
|
|
int32_t bpp = w->as.canvas.canvasBpp;
|
|
const uint8_t *src = w->as.canvas.data + y * w->as.canvas.canvasPitch + x * bpp;
|
|
|
|
return canvasGetPixel(src, bpp);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtCanvasLoad
|
|
// ============================================================
|
|
|
|
// Load an image file into the canvas, replacing the current content.
|
|
// Uses stb_image for decoding (supports BMP, PNG, JPEG, GIF, etc.).
|
|
// The loaded RGB pixels are converted to the display's native pixel format
|
|
// during load so that subsequent repaints are just a memcpy. The old buffer
|
|
// is freed and replaced with the new one — canvas dimensions change to match
|
|
// the loaded image.
|
|
int32_t wgtCanvasLoad(WidgetT *w, const char *path) {
|
|
if (!w || w->type != WidgetCanvasE || !path) {
|
|
return -1;
|
|
}
|
|
|
|
// Find the AppContextT to get display format
|
|
WidgetT *root = w;
|
|
|
|
while (root->parent) {
|
|
root = root->parent;
|
|
}
|
|
|
|
AppContextT *ctx = (AppContextT *)root->userData;
|
|
|
|
if (!ctx) {
|
|
return -1;
|
|
}
|
|
|
|
const DisplayT *d = &ctx->display;
|
|
|
|
int imgW;
|
|
int imgH;
|
|
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) {
|
|
stbi_image_free(rgb);
|
|
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);
|
|
w->as.canvas.data = data;
|
|
w->as.canvas.canvasW = imgW;
|
|
w->as.canvas.canvasH = imgH;
|
|
w->as.canvas.canvasPitch = pitch;
|
|
w->as.canvas.canvasBpp = bpp;
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtCanvasSave
|
|
// ============================================================
|
|
|
|
// Save the canvas content to a PNG file. Since the canvas stores pixels in
|
|
// the display's native format (which varies per video mode), we must convert
|
|
// back to RGB before writing. This is the inverse of the load conversion.
|
|
int32_t wgtCanvasSave(WidgetT *w, const char *path) {
|
|
if (!w || w->type != WidgetCanvasE || !path || !w->as.canvas.data) {
|
|
return -1;
|
|
}
|
|
|
|
// Find the AppContextT to get display format
|
|
WidgetT *root = w;
|
|
|
|
while (root->parent) {
|
|
root = root->parent;
|
|
}
|
|
|
|
AppContextT *ctx = (AppContextT *)root->userData;
|
|
|
|
if (!ctx) {
|
|
return -1;
|
|
}
|
|
|
|
const DisplayT *d = &ctx->display;
|
|
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;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtCanvasSetMouseCallback
|
|
// ============================================================
|
|
|
|
void wgtCanvasSetMouseCallback(WidgetT *w, void (*cb)(WidgetT *w, int32_t cx, int32_t cy, bool drag)) {
|
|
if (w && w->type == WidgetCanvasE) {
|
|
w->as.canvas.onMouse = cb;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtCanvasSetPenColor
|
|
// ============================================================
|
|
|
|
void wgtCanvasSetPenColor(WidgetT *w, uint32_t color) {
|
|
if (w && w->type == WidgetCanvasE) {
|
|
w->as.canvas.penColor = color;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtCanvasSetPenSize
|
|
// ============================================================
|
|
|
|
void wgtCanvasSetPenSize(WidgetT *w, int32_t size) {
|
|
if (w && w->type == WidgetCanvasE && size > 0) {
|
|
w->as.canvas.penSize = size;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtCanvasSetPixel
|
|
// ============================================================
|
|
|
|
void wgtCanvasSetPixel(WidgetT *w, int32_t x, int32_t y, uint32_t color) {
|
|
if (!w || w->type != WidgetCanvasE || !w->as.canvas.data) {
|
|
return;
|
|
}
|
|
|
|
if (x < 0 || x >= w->as.canvas.canvasW || y < 0 || y >= w->as.canvas.canvasH) {
|
|
return;
|
|
}
|
|
|
|
int32_t bpp = w->as.canvas.canvasBpp;
|
|
uint8_t *dst = w->as.canvas.data + y * w->as.canvas.canvasPitch + x * bpp;
|
|
|
|
canvasPutPixel(dst, color, bpp);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetCanvasDestroy
|
|
// ============================================================
|
|
|
|
void widgetCanvasDestroy(WidgetT *w) {
|
|
free(w->as.canvas.data);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetCanvasCalcMinSize
|
|
// ============================================================
|
|
|
|
// The canvas requests exactly its pixel dimensions plus the sunken bevel
|
|
// border. The font parameter is unused since the canvas has no text content.
|
|
// The canvas is not designed to scale — it reports its exact size as the
|
|
// minimum, and the layout engine should respect that.
|
|
void widgetCanvasCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
|
(void)font;
|
|
w->calcMinW = w->as.canvas.canvasW + CANVAS_BORDER * 2;
|
|
w->calcMinH = w->as.canvas.canvasH + CANVAS_BORDER * 2;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetCanvasOnMouse
|
|
// ============================================================
|
|
|
|
// Mouse handler: translates widget-space coordinates to canvas-space (subtracting
|
|
// border offset) and invokes the application's mouse callback. The sDrawingCanvas
|
|
// global tracks whether this is a drag (mouse was already down on this canvas)
|
|
// vs a new click, so the callback can distinguish between starting a new stroke
|
|
// and continuing one.
|
|
void widgetCanvasOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
|
(void)root;
|
|
|
|
if (!hit->as.canvas.onMouse) {
|
|
return;
|
|
}
|
|
|
|
// Convert widget coords to canvas coords
|
|
int32_t cx = vx - hit->x - CANVAS_BORDER;
|
|
int32_t cy = vy - hit->y - CANVAS_BORDER;
|
|
|
|
bool drag = (sDrawingCanvas == hit);
|
|
|
|
if (!drag) {
|
|
sDrawingCanvas = hit;
|
|
}
|
|
|
|
hit->as.canvas.lastX = cx;
|
|
hit->as.canvas.lastY = cy;
|
|
|
|
hit->as.canvas.onMouse(hit, cx, cy, drag);
|
|
wgtInvalidatePaint(hit);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetCanvasPaint
|
|
// ============================================================
|
|
|
|
// Paint: draws a sunken bevel border then blits the canvas buffer. Because
|
|
// the canvas stores pixels in the display's native format, rectCopy is a
|
|
// straight memcpy per scanline — no per-pixel conversion needed. This makes
|
|
// repaint essentially free relative to the display bandwidth.
|
|
void widgetCanvasPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
|
(void)font;
|
|
|
|
if (!w->as.canvas.data) {
|
|
return;
|
|
}
|
|
|
|
// Draw a sunken bevel border around the canvas
|
|
BevelStyleT sunken;
|
|
sunken.highlight = colors->windowShadow;
|
|
sunken.shadow = colors->windowHighlight;
|
|
sunken.face = 0;
|
|
sunken.width = CANVAS_BORDER;
|
|
drawBevel(d, ops, w->x, w->y, w->w, w->h, &sunken);
|
|
|
|
// Blit the canvas data inside the border
|
|
int32_t imgW = w->as.canvas.canvasW;
|
|
int32_t imgH = w->as.canvas.canvasH;
|
|
int32_t dx = w->x + CANVAS_BORDER;
|
|
int32_t dy = w->y + CANVAS_BORDER;
|
|
|
|
rectCopy(d, ops, dx, dy,
|
|
w->as.canvas.data, w->as.canvas.canvasPitch,
|
|
0, 0, imgW, imgH);
|
|
}
|