469 lines
13 KiB
C
469 lines
13 KiB
C
// widgetCanvas.c — Drawable canvas widget (freehand draw, PNG save/load)
|
|
|
|
#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);
|
|
|
|
|
|
// ============================================================
|
|
// 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.canvasPitch / w->as.canvas.canvasW;
|
|
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;
|
|
|
|
if (bpp == 1) {
|
|
*dst = (uint8_t)color;
|
|
} else if (bpp == 2) {
|
|
*(uint16_t *)dst = (uint16_t)color;
|
|
} else {
|
|
*(uint32_t *)dst = color;
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Filled circle via bounding box + radius 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;
|
|
}
|
|
|
|
for (int32_t dx = -rad; dx <= rad; dx++) {
|
|
int32_t px = cx + dx;
|
|
|
|
if (px < 0 || px >= cw) {
|
|
continue;
|
|
}
|
|
|
|
if (dx * dx + dy * dy <= r2) {
|
|
uint8_t *dst = data + py * pitch + px * bpp;
|
|
|
|
if (bpp == 1) {
|
|
*dst = (uint8_t)color;
|
|
} else if (bpp == 2) {
|
|
*(uint16_t *)dst = (uint16_t)color;
|
|
} else {
|
|
*(uint32_t *)dst = color;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// canvasDrawLine
|
|
// ============================================================
|
|
//
|
|
// Bresenham line from (x0,y0) to (x1,y1), placing dots along the path.
|
|
|
|
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.
|
|
|
|
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
|
|
// ============================================================
|
|
|
|
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
|
|
uint32_t white = packColor(d, 255, 255, 255);
|
|
|
|
for (int32_t y = 0; y < h; y++) {
|
|
for (int32_t x = 0; x < w; x++) {
|
|
uint8_t *dst = data + y * pitch + x * bpp;
|
|
|
|
if (bpp == 1) {
|
|
*dst = (uint8_t)white;
|
|
} else if (bpp == 2) {
|
|
*(uint16_t *)dst = (uint16_t)white;
|
|
} else {
|
|
*(uint32_t *)dst = white;
|
|
}
|
|
}
|
|
}
|
|
|
|
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.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;
|
|
}
|
|
|
|
int32_t bpp = w->as.canvas.canvasPitch / w->as.canvas.canvasW;
|
|
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++) {
|
|
for (int32_t x = 0; x < cw; x++) {
|
|
uint8_t *dst = w->as.canvas.data + 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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtCanvasLoad
|
|
// ============================================================
|
|
|
|
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;
|
|
|
|
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);
|
|
|
|
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;
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtCanvasSave
|
|
// ============================================================
|
|
|
|
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;
|
|
|
|
if (bpp == 1) {
|
|
pixel = *src;
|
|
} else if (bpp == 2) {
|
|
pixel = *(const uint16_t *)src;
|
|
} else {
|
|
pixel = *(const uint32_t *)src;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetCanvasCalcMinSize
|
|
// ============================================================
|
|
|
|
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
|
|
// ============================================================
|
|
|
|
void widgetCanvasOnMouse(WidgetT *hit, int32_t vx, int32_t vy) {
|
|
// Convert widget coords to canvas coords
|
|
int32_t cx = vx - hit->x - CANVAS_BORDER;
|
|
int32_t cy = vy - hit->y - CANVAS_BORDER;
|
|
|
|
if (sDrawingCanvas == hit) {
|
|
// Continuation of a drag stroke — draw line from last to current
|
|
if (hit->as.canvas.lastX >= 0) {
|
|
canvasDrawLine(hit, hit->as.canvas.lastX, hit->as.canvas.lastY, cx, cy);
|
|
} else {
|
|
canvasDrawDot(hit, cx, cy);
|
|
}
|
|
} else {
|
|
// First click — start drawing, place a dot
|
|
sDrawingCanvas = hit;
|
|
canvasDrawDot(hit, cx, cy);
|
|
}
|
|
|
|
hit->as.canvas.lastX = cx;
|
|
hit->as.canvas.lastY = cy;
|
|
|
|
wgtInvalidate(hit);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetCanvasPaint
|
|
// ============================================================
|
|
|
|
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);
|
|
}
|