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