DVX_GUI/dvx/widgets/widgetCanvas.c

726 lines
21 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"
#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^2 <= r^2 - dy^2
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^2 <= r^2 - dy^2
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);
}