// 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" #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); // ============================================================ // 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; } } } // ============================================================ // 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. // Delegates to dvxLoadImage for format decoding and pixel conversion. // 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; } AppContextT *ctx = wgtGetContext(w); if (!ctx) { return -1; } int32_t imgW; int32_t imgH; int32_t pitch; uint8_t *data = dvxLoadImage(ctx, path, &imgW, &imgH, &pitch); if (!data) { return -1; } 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 = ctx->display.format.bytesPerPixel; return 0; } // ============================================================ // wgtCanvasSave // ============================================================ // Save the canvas content to a PNG file. Delegates to dvxSaveImage // which handles native-to-RGB conversion and PNG encoding. int32_t wgtCanvasSave(WidgetT *w, const char *path) { if (!w || w->type != WidgetCanvasE || !path || !w->as.canvas.data) { return -1; } AppContextT *ctx = wgtGetContext(w); if (!ctx) { return -1; } return dvxSaveImage(ctx, w->as.canvas.data, w->as.canvas.canvasW, w->as.canvas.canvasH, w->as.canvas.canvasPitch, path); } // ============================================================ // 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); }