DVX_GUI/dvx/widgets/widgetCanvas.c

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