std1.0.0-beta-7 SafeC docs

Source gui/gui_canvas.sc

View in the repository

// SafeC Standard Library — GUI: the vector canvas' state, paths, shapes and
// images (see gui_canvas.h). Rasterization is in gui_canvas_raster.sc,
// stroking in gui_canvas_stroke.sc, text in gui_canvas_text.sc.
#pragma once
#include <std/stddef.h>
#include <std/gui/gui.h>
#include <std/gui/gui_canvas.h>
#include <std/gui/gui_path.h>
#include <std/gui/gui_png.h>
#include <std/gui/gui_text.h>
#include <std/mem.h>
#include <std/math.h>
#include <std/collections/vec.h>

namespace std {

// gui_canvas_raster.sc / gui_canvas_stroke.sc
void __gui_canvas_flatten(const &GuiPath p, struct GuiMatrix m, float tol, &Vec<float> pts, &Vec<int> cont);
void __gui_canvas_edges(&GuiCanvas c, &Vec<float> pts, &Vec<int> cont, struct GuiMatrix m, int transform);
void __gui_canvas_fill_edges(&GuiCanvas c, int rule, const &GuiPaint paint);
?&heap unsigned char __gui_canvas_mask_edges(&GuiCanvas c, int rule);
void __gui_canvas_fill_irect(&GuiCanvas c, int x0, int y0, int x1, int y1, const &GuiPaint paint);
void __gui_canvas_stroke_polys(&Vec<float> pts, &Vec<int> cont, float width, int cap, int lineJoin,
                               float miterLimit, float tol, &Vec<float> out, &Vec<int> outCont);
void __gui_canvas_dash(&Vec<float> pts, &Vec<int> cont, const &float dash, int n, float offset,
                       &Vec<float> out, &Vec<int> outCont);

#define GUI_FLATTEN_TOL 0.05f

// ── paints ────────────────────────────────────────────────────────────────

static struct GuiPaint __gui_paint_base(int kind) {
    struct GuiPaint p;
    safe_memset_obj(&p, 0);
    p.kind = kind;
    p.color = GuiColor::rgba((unsigned char)0, (unsigned char)0, (unsigned char)0, (unsigned char)255);
    p.nstops = 0;
    p.spread = GUI_SPREAD_PAD;
    p.image = null;
    p.matrix = GuiMatrix::identity();
    p.filter = GUI_FILTER_BILINEAR;
    p.opacity = 1.0f;
    return p;
}

struct GuiPaint GuiPaint::solid(struct GuiColor c) {
    struct GuiPaint p = __gui_paint_base(GUI_PAINT_SOLID);
    p.color = c;
    return p;
}

struct GuiPaint GuiPaint::linear(float x0, float y0, float x1, float y1) {
    struct GuiPaint p = __gui_paint_base(GUI_PAINT_LINEAR);
    p.x0 = x0; p.y0 = y0; p.x1 = x1; p.y1 = y1;
    return p;
}

struct GuiPaint GuiPaint::radial(float cx, float cy, float r) {
    return GuiPaint::radial2(cx, cy, 0.0f, cx, cy, r);
}

struct GuiPaint GuiPaint::radial2(float x0, float y0, float r0, float x1, float y1, float r1) {
    struct GuiPaint p = __gui_paint_base(GUI_PAINT_RADIAL);
    p.x0 = x0; p.y0 = y0; p.r0 = r0 < 0.0f ? 0.0f : r0;
    p.x1 = x1; p.y1 = y1; p.r1 = r1 < 0.0f ? 0.0f : r1;
    return p;
}

struct GuiPaint GuiPaint::sweep(float cx, float cy, float startAngle) {
    struct GuiPaint p = __gui_paint_base(GUI_PAINT_SWEEP);
    p.x0 = cx; p.y0 = cy; p.r0 = startAngle;
    return p;
}

struct GuiPaint GuiPaint::pattern(const &GuiImage img) {
    struct GuiPaint p = __gui_paint_base(GUI_PAINT_IMAGE);
    p.image = img;
    p.spread = GUI_SPREAD_REPEAT;
    return p;
}

void GuiPaint::add_stop(float offset, struct GuiColor c) {
    if (self.nstops >= GUI_GRADIENT_MAX_STOPS) { return; }
    if (offset < 0.0f) { offset = 0.0f; }
    if (offset > 1.0f) { offset = 1.0f; }
    self.stops[self.nstops].offset = offset;
    self.stops[self.nstops].color = c;
    self.nstops = self.nstops + 1;
}

void GuiPaint::set_transform(struct GuiMatrix m) { self.matrix = m; }
void GuiPaint::set_spread(int spread) { self.spread = spread; }
void GuiPaint::set_filter(int filter) { self.filter = filter; }
void GuiPaint::set_opacity(float opacity) {
    self.opacity = opacity < 0.0f ? 0.0f : (opacity > 1.0f ? 1.0f : opacity);
}

// ── construction / state ──────────────────────────────────────────────────

static void __gui_canvas_default_state(&GuiCanvas c) {
    c.st.m = GuiMatrix::identity();
    c.st.fill = GuiPaint::solid(GuiColor::rgb((unsigned char)0, (unsigned char)0, (unsigned char)0));
    c.st.stroke = GuiPaint::solid(GuiColor::rgb((unsigned char)0, (unsigned char)0, (unsigned char)0));
    c.st.lineWidth = 1.0f;
    c.st.miterLimit = 10.0f;
    c.st.lineCap = GUI_CAP_BUTT;
    c.st.lineJoin = GUI_JOIN_MITER;
    c.st.ndash = 0;
    c.st.dashOffset = 0.0f;
    c.st.alpha = 1.0f;
    c.st.blend = GUI_BLEND_SRC_OVER;
    c.st.fillRule = GUI_FILL_NONZERO;
    c.st.filter = GUI_FILTER_BILINEAR;
    c.st.antiAlias = 1;
    c.st.clipX0 = 0; c.st.clipY0 = 0;
    c.st.clipX1 = c.width; c.st.clipY1 = c.height;
    c.st.mask = null;
    c.st.ownsMask = 0;
    c.st.font = null;
    c.st.fontSize = 12.0f;
    c.st.textAlign = GUI_TEXT_LEFT;
    c.st.textBaseline = GUI_BASELINE_ALPHABETIC;
}

struct GuiCanvas GuiCanvas::for_pixels(&unsigned char px, int width, int height, int stride) {
    struct GuiCanvas c;
    c.px = px;
    c.width = width;
    c.height = height;
    c.stride = stride;
    c.saved = Vec::new();
    c.path = GuiPath::new();
    c.edges = Vec::new();
    c.acc = Vec::new();
    c.xs = Vec::new();
    c.sorted = Vec::new();
    c.active = Vec::new();
    c.fpts = Vec::new();
    c.fcont = Vec::new();
    c.dpts = Vec::new();
    c.dcont = Vec::new();
    c.spts = Vec::new();
    c.scont = Vec::new();
    c.row = Vec::new();
    c.lut = Vec::new();
    c.scratchPath = GuiPath::new();
    __gui_canvas_default_state(&c);
    return c;
}

struct GuiCanvas GuiCanvas::for_window(&GuiWindow win) {
    unsafe { return GuiCanvas::for_pixels((&unsigned char)win.pixels, win.width, win.height, win.width * 4); }
}

struct GuiCanvas GuiCanvas::for_image(&GuiImage img) {
    img.premultiply();
    unsafe { return GuiCanvas::for_pixels((&unsigned char)img.pixels, img.width, img.height, img.width * 4); }
}

static void __gui_canvas_drop_masks(&GuiCanvas c) {
    unsafe {
        if (c.st.ownsMask && !c.st.mask.is_null()) { dealloc(c.st.mask); }
        c.st.mask = null;
        c.st.ownsMask = 0;
        unsigned long i = 0UL;
        while (i < c.saved.len) {
            &GuiCanvasState s = c.saved.at(i);
            if (s.ownsMask && !s.mask.is_null()) { dealloc(s.mask); }
            s.mask = null;
            s.ownsMask = 0;
            i = i + 1UL;
        }
    }
}

void GuiCanvas::retarget_window(&GuiWindow win) {
    int resized = (win.width != self.width || win.height != self.height) ? 1 : 0;
    unsafe { self.px = (&unsigned char)win.pixels; }
    self.stride = win.width * 4;
    if (resized) {
        __gui_canvas_drop_masks(self);
        self.width = win.width;
        self.height = win.height;
        self.st.clipX0 = 0; self.st.clipY0 = 0;
        self.st.clipX1 = self.width; self.st.clipY1 = self.height;
        unsigned long i = 0UL;
        while (i < self.saved.len) {
            &GuiCanvasState s = self.saved.at(i);
            s.clipX0 = 0; s.clipY0 = 0; s.clipX1 = self.width; s.clipY1 = self.height;
            i = i + 1UL;
        }
    }
}

void GuiCanvas::reset() {
    __gui_canvas_drop_masks(self);
    self.saved.clear();
    self.path.clear();
    __gui_canvas_default_state(self);
}

void GuiCanvas::free() { self.drop(); }

void GuiCanvas::drop() {
    __gui_canvas_drop_masks(self);
    self.saved.free();
    self.path.free();
    self.edges.free();
    self.acc.free();
    self.xs.free();
    self.sorted.free();
    self.active.free();
    self.fpts.free();
    self.fcont.free();
    self.dpts.free();
    self.dcont.free();
    self.spts.free();
    self.scont.free();
    self.row.free();
    self.lut.free();
    self.scratchPath.free();
    self.px = null;
}

void GuiCanvas::save() {
    self.saved.push(self.st);
    self.st.ownsMask = 0;    // the saved state keeps owning it
}

void GuiCanvas::restore() {
    if (self.saved.len == 0UL) { return; }
    unsafe {
        if (self.st.ownsMask && !self.st.mask.is_null()) { dealloc(self.st.mask); }
    }
    ?struct GuiCanvasState top = self.saved.pop();
    match (top) {
        case some(s): { self.st = s; }
        case none: { }
    }
}

void GuiCanvas::translate(float tx, float ty) { self.st.m = self.st.m.translate(tx, ty); }
void GuiCanvas::scale(float sx, float sy) { self.st.m = self.st.m.scale(sx, sy); }
void GuiCanvas::rotate(float radians) { self.st.m = self.st.m.rotate(radians); }
void GuiCanvas::transform(struct GuiMatrix m) { self.st.m = self.st.m.mul(m); }
void GuiCanvas::set_transform(struct GuiMatrix m) { self.st.m = m; }
void GuiCanvas::reset_transform() { self.st.m = GuiMatrix::identity(); }
struct GuiMatrix GuiCanvas::get_transform() const { return self.st.m; }

void GuiCanvas::set_fill_color(struct GuiColor c) { self.st.fill = GuiPaint::solid(c); }
void GuiCanvas::set_fill_paint(const &GuiPaint p) { self.st.fill = *p; }
void GuiCanvas::set_stroke_color(struct GuiColor c) { self.st.stroke = GuiPaint::solid(c); }
void GuiCanvas::set_stroke_paint(const &GuiPaint p) { self.st.stroke = *p; }
void GuiCanvas::set_line_width(float w) { self.st.lineWidth = w < 0.0f ? 0.0f : w; }
void GuiCanvas::set_line_cap(int lineCap) { self.st.lineCap = lineCap; }
void GuiCanvas::set_line_join(int lineJoin) { self.st.lineJoin = lineJoin; }
void GuiCanvas::set_miter_limit(float limit) { self.st.miterLimit = limit < 1.0f ? 1.0f : limit; }

void GuiCanvas::set_dash_n_(const &float lengths, int n, float offset) {
    if (n <= 0) { self.st.ndash = 0; return; }
    if (n > GUI_DASH_MAX) { n = GUI_DASH_MAX; }
    float total = 0.0f;
    unsafe {
        int i = 0;
        while (i < n) {
            float v = lengths[i] < 0.0f ? 0.0f : lengths[i];
            self.st.dash[i] = v;
            total = total + v;
            i = i + 1;
        }
    }
    if (total <= 0.0f) { self.st.ndash = 0; return; }
    // An odd list repeats once more (SVG), when it fits.
    if ((n % 2) == 1) {
        if (n * 2 <= GUI_DASH_MAX) {
            int i = 0;
            while (i < n) { self.st.dash[n + i] = self.st.dash[i]; i = i + 1; }
            n = n * 2;
        } else {
            n = n - 1;
        }
    }
    self.st.ndash = n;
    self.st.dashOffset = offset;
}

void GuiCanvas::set_dash([]const float lengths_s, float offset) {
    int n = (int)lengths_s.len;
    float lengths_d_[1];
    const &float lengths = &lengths_d_[0];
    if (lengths_s.len > 0UL) { unsafe { lengths = (const &float)lengths_s.ptr; } }
    self.set_dash_n_(lengths, n, offset);
}

void GuiCanvas::set_global_alpha(float a) { self.st.alpha = a < 0.0f ? 0.0f : (a > 1.0f ? 1.0f : a); }
void GuiCanvas::set_blend_mode(int mode) {
    self.st.blend = (mode >= 0 && mode < GUI_BLEND_COUNT) ? mode : GUI_BLEND_SRC_OVER;
}
void GuiCanvas::set_fill_rule(int rule) { self.st.fillRule = rule; }
void GuiCanvas::set_image_filter(int filter) { self.st.filter = filter; }
void GuiCanvas::set_anti_alias(int on) { self.st.antiAlias = on ? 1 : 0; }

// ── clipping ──────────────────────────────────────────────────────────────

static int __gui_near_int(float v) {
    float r = floor_f(v + 0.5f);
    float d = v - r;
    return (d > -0.01f && d < 0.01f) ? 1 : 0;
}

static void __gui_clip_irect(&GuiCanvas c, int x0, int y0, int x1, int y1) {
    if (x0 > c.st.clipX0) { c.st.clipX0 = x0; }
    if (y0 > c.st.clipY0) { c.st.clipY0 = y0; }
    if (x1 < c.st.clipX1) { c.st.clipX1 = x1; }
    if (y1 < c.st.clipY1) { c.st.clipY1 = y1; }
    if (c.st.clipX1 < c.st.clipX0) { c.st.clipX1 = c.st.clipX0; }
    if (c.st.clipY1 < c.st.clipY0) { c.st.clipY1 = c.st.clipY0; }
}

void GuiCanvas::clip_path(const &GuiPath p) {
    __gui_canvas_flatten(p, self.st.m, GUI_FLATTEN_TOL, &self.fpts, &self.fcont);
    __gui_canvas_edges(self, &self.fpts, &self.fcont, GuiMatrix::identity(), 0);
    if (self.edges.len == 0UL) {
        // Nothing inside: an empty clip.
        __gui_clip_irect(self, 0, 0, 0, 0);
        return;
    }
    ?&heap unsigned char m = __gui_canvas_mask_edges(self, self.st.fillRule);
    unsafe {
        if (self.st.ownsMask && !self.st.mask.is_null()) { dealloc(self.st.mask); }
    }
    self.st.mask = m;
    self.st.ownsMask = 1;
    // Nothing outside the path's bounds survives: tighten the rectangle.
    struct GuiRect b = p.bounds();
    struct GuiRect db = self.st.m.apply_rect(b);
    __gui_clip_irect(self, (int)floor_f(db.x), (int)floor_f(db.y),
                     (int)ceil_f(db.x + db.w), (int)ceil_f(db.y + db.h));
}

void GuiCanvas::clip_rect(float x, float y, float w, float h) {
    if (self.st.m.is_axis_aligned()) {
        struct GuiRect r = self.st.m.apply_rect(GuiRect::new(x, y, w, h));
        if (__gui_near_int(r.x) && __gui_near_int(r.y) && __gui_near_int(r.x + r.w) && __gui_near_int(r.y + r.h)) {
            __gui_clip_irect(self, (int)floor_f(r.x + 0.5f), (int)floor_f(r.y + 0.5f),
                             (int)floor_f(r.x + r.w + 0.5f), (int)floor_f(r.y + r.h + 0.5f));
            return;
        }
    }
    self.scratchPath.clear();
    self.scratchPath.rect(x, y, w, h);
    self.clip_path(&self.scratchPath);
}

void GuiCanvas::clip() { self.clip_path(&self.path); }

void GuiCanvas::reset_clip() {
    unsafe {
        if (self.st.ownsMask && !self.st.mask.is_null()) { dealloc(self.st.mask); }
    }
    self.st.mask = null;
    self.st.ownsMask = 0;
    self.st.clipX0 = 0; self.st.clipY0 = 0;
    self.st.clipX1 = self.width; self.st.clipY1 = self.height;
}

struct GuiRect GuiCanvas::clip_bounds() const {
    return GuiRect::new((float)self.st.clipX0, (float)self.st.clipY0,
                        (float)(self.st.clipX1 - self.st.clipX0), (float)(self.st.clipY1 - self.st.clipY0));
}

// ── the current path ──────────────────────────────────────────────────────

void GuiCanvas::begin_path() { self.path.clear(); }
void GuiCanvas::move_to(float x, float y) { self.path.move_to(x, y); }
void GuiCanvas::line_to(float x, float y) { self.path.line_to(x, y); }
void GuiCanvas::quad_to(float cx, float cy, float x, float y) { self.path.quad_to(cx, cy, x, y); }
void GuiCanvas::cubic_to(float c1x, float c1y, float c2x, float c2y, float x, float y) {
    self.path.cubic_to(c1x, c1y, c2x, c2y, x, y);
}
void GuiCanvas::arc(float cx, float cy, float r, float a0, float a1, int ccw) { self.path.arc(cx, cy, r, a0, a1, ccw); }
void GuiCanvas::arc_to(float x1, float y1, float x2, float y2, float r) { self.path.arc_to(x1, y1, x2, y2, r); }
void GuiCanvas::rect(float x, float y, float w, float h) { self.path.rect(x, y, w, h); }
void GuiCanvas::rounded_rect(float x, float y, float w, float h, float r) { self.path.rounded_rect(x, y, w, h, r); }
void GuiCanvas::ellipse(float cx, float cy, float rx, float ry) { self.path.ellipse(cx, cy, rx, ry); }
void GuiCanvas::close_path() { self.path.close(); }
void GuiCanvas::fill() { self.fill_path(&self.path); }
void GuiCanvas::stroke() { self.stroke_path(&self.path); }

// ── filling and stroking ──────────────────────────────────────────────────

static void __gui_canvas_fill_with(&GuiCanvas c, const &GuiPath p, const &GuiPaint paint, int rule) {
    __gui_canvas_flatten(p, c.st.m, GUI_FLATTEN_TOL, &c.fpts, &c.fcont);
    __gui_canvas_edges(c, &c.fpts, &c.fcont, GuiMatrix::identity(), 0);
    __gui_canvas_fill_edges(c, rule, paint);
}

void GuiCanvas::fill_path(const &GuiPath p) {
    __gui_canvas_fill_with(self, p, &self.st.fill, self.st.fillRule);
}

// The stroke of 'p' as polygons in user space (in spts/scont).
static int __gui_canvas_stroke_outline(&GuiCanvas c, const &GuiPath p) {
    float s = c.st.m.scale_factor();
    if (s <= 0.0f) { return 0; }
    float tol = GUI_FLATTEN_TOL / s;
    __gui_canvas_flatten(p, GuiMatrix::identity(), tol, &c.fpts, &c.fcont);
    if (c.st.ndash > 0) {
        __gui_canvas_dash(&c.fpts, &c.fcont, &c.st.dash[0], c.st.ndash, c.st.dashOffset, &c.dpts, &c.dcont);
        __gui_canvas_stroke_polys(&c.dpts, &c.dcont, c.st.lineWidth, c.st.lineCap, c.st.lineJoin,
                                  c.st.miterLimit, tol, &c.spts, &c.scont);
    } else {
        __gui_canvas_stroke_polys(&c.fpts, &c.fcont, c.st.lineWidth, c.st.lineCap, c.st.lineJoin,
                                  c.st.miterLimit, tol, &c.spts, &c.scont);
    }
    return 1;
}

void GuiCanvas::stroke_path(const &GuiPath p) {
    if (self.st.lineWidth <= 0.0f) { return; }
    if (!__gui_canvas_stroke_outline(self, p)) { return; }
    __gui_canvas_edges(self, &self.spts, &self.scont, self.st.m, 1);
    __gui_canvas_fill_edges(self, GUI_FILL_NONZERO, &self.st.stroke);
}

void GuiCanvas::stroke_to_path(const &GuiPath p, &GuiPath out) {
    if (self.st.lineWidth <= 0.0f) { return; }
    if (!__gui_canvas_stroke_outline(self, p)) { return; }
    unsafe {
        const float* pts = (const float*)self.spts.data;
        const int* ct = (const int*)self.scont.data;
        unsigned long nc = self.scont.len / 3UL;
        unsigned long k = 0UL;
        while (k < nc) {
            int first = ct[k * 3UL];
            int count = ct[k * 3UL + 1UL];
            int i = 0;
            while (i < count) {
                float x = pts[(first + i) * 2];
                float y = pts[(first + i) * 2 + 1];
                if (i == 0) { out.move_to(x, y); } else { out.line_to(x, y); }
                i = i + 1;
            }
            out.close();
            k = k + 1UL;
        }
    }
}

// A device-space integer rectangle for (x,y,w,h) under a translation, if
// there is one (so fill_rect can skip the scan converter).
static int __gui_canvas_irect(&GuiCanvas c, float x, float y, float w, float h,
                              &int x0, &int y0, &int x1, &int y1) {
    if (!c.st.m.is_translation()) { return 0; }
    float ax = x + c.st.m.e;
    float ay = y + c.st.m.f;
    float bx = ax + w;
    float by = ay + h;
    if (!__gui_near_int(ax) || !__gui_near_int(ay) || !__gui_near_int(bx) || !__gui_near_int(by)) { return 0; }
    int ix0 = (int)floor_f(ax + 0.5f);
    int iy0 = (int)floor_f(ay + 0.5f);
    int ix1 = (int)floor_f(bx + 0.5f);
    int iy1 = (int)floor_f(by + 0.5f);
    *x0 = ix0 < ix1 ? ix0 : ix1;
    *x1 = ix0 < ix1 ? ix1 : ix0;
    *y0 = iy0 < iy1 ? iy0 : iy1;
    *y1 = iy0 < iy1 ? iy1 : iy0;
    return 1;
}

static void __gui_canvas_fill_rect_with(&GuiCanvas c, float x, float y, float w, float h, const &GuiPaint paint) {
    int x0 = 0; int y0 = 0; int x1 = 0; int y1 = 0;
    if (__gui_canvas_irect(c, x, y, w, h, &x0, &y0, &x1, &y1)) {
        __gui_canvas_fill_irect(c, x0, y0, x1, y1, paint);
        return;
    }
    c.scratchPath.clear();
    c.scratchPath.rect(x, y, w, h);
    __gui_canvas_fill_with(c, &c.scratchPath, paint, GUI_FILL_NONZERO);
}

void GuiCanvas::fill_rect(float x, float y, float w, float h) {
    __gui_canvas_fill_rect_with(self, x, y, w, h, &self.st.fill);
}

void GuiCanvas::stroke_rect(float x, float y, float w, float h) {
    self.scratchPath.clear();
    self.scratchPath.rect(x, y, w, h);
    self.stroke_path(&self.scratchPath);
}

void GuiCanvas::clear_rect(float x, float y, float w, float h) {
    int saveBlend = self.st.blend;
    float saveAlpha = self.st.alpha;
    self.st.blend = GUI_BLEND_CLEAR;
    self.st.alpha = 1.0f;
    struct GuiPaint p = GuiPaint::solid(GuiColor::rgba((unsigned char)0, (unsigned char)0, (unsigned char)0, (unsigned char)0));
    __gui_canvas_fill_rect_with(self, x, y, w, h, &p);
    self.st.blend = saveBlend;
    self.st.alpha = saveAlpha;
}

void GuiCanvas::clear(struct GuiColor c) {
    unsigned int a = (unsigned int)c.a;
    unsigned char b[4];
    b[0] = (unsigned char)(((unsigned int)c.r * a + 127U) / 255U);
    b[1] = (unsigned char)(((unsigned int)c.g * a + 127U) / 255U);
    b[2] = (unsigned char)(((unsigned int)c.b * a + 127U) / 255U);
    b[3] = (unsigned char)a;
    unsafe {
        unsigned int word = *(unsigned int*)&b[0];
        int y = 0;
        while (y < self.height) {
            unsigned int* row = (unsigned int*)((unsigned char*)self.px + (unsigned long)y * (unsigned long)self.stride);
            int x = 0;
            while (x < self.width) { row[x] = word; x = x + 1; }
            y = y + 1;
        }
    }
}

void GuiCanvas::fill_rounded_rect(float x, float y, float w, float h, float r) {
    if (r <= 0.0f) { self.fill_rect(x, y, w, h); return; }
    self.scratchPath.clear();
    self.scratchPath.rounded_rect(x, y, w, h, r);
    self.fill_path(&self.scratchPath);
}

void GuiCanvas::stroke_rounded_rect(float x, float y, float w, float h, float r) {
    self.scratchPath.clear();
    self.scratchPath.rounded_rect(x, y, w, h, r);
    self.stroke_path(&self.scratchPath);
}

void GuiCanvas::fill_circle(float cx, float cy, float r) {
    self.scratchPath.clear();
    self.scratchPath.circle(cx, cy, r);
    self.fill_path(&self.scratchPath);
}

void GuiCanvas::stroke_circle(float cx, float cy, float r) {
    self.scratchPath.clear();
    self.scratchPath.circle(cx, cy, r);
    self.stroke_path(&self.scratchPath);
}

void GuiCanvas::fill_ellipse(float cx, float cy, float rx, float ry) {
    self.scratchPath.clear();
    self.scratchPath.ellipse(cx, cy, rx, ry);
    self.fill_path(&self.scratchPath);
}

void GuiCanvas::draw_line(float x0, float y0, float x1, float y1) {
    self.scratchPath.clear();
    self.scratchPath.move_to(x0, y0);
    self.scratchPath.line_to(x1, y1);
    self.stroke_path(&self.scratchPath);
}

// ── images ────────────────────────────────────────────────────────────────

void GuiCanvas::draw_image_rect(const &GuiImage img, float sx, float sy, float sw, float sh,
                                float dx, float dy, float dw, float dh) {
    if (img.width < 1 || img.height < 1 || sw == 0.0f || sh == 0.0f || dw == 0.0f || dh == 0.0f) { return; }
    struct GuiPaint p = GuiPaint::pattern(img);
    p.spread = GUI_SPREAD_PAD;
    p.filter = self.st.filter;
    // image pixel (u, v) -> user space
    p.matrix = GuiMatrix::translation(dx, dy).scale(dw / sw, dh / sh).translate(-sx, -sy);
    // Unscaled at whole pixels: an exact copy (nearest sampling).
    if (self.st.m.is_translation() && dw == sw && dh == sh &&
        __gui_near_int(dx + self.st.m.e) && __gui_near_int(dy + self.st.m.f) &&
        __gui_near_int(sx) && __gui_near_int(sy)) {
        p.filter = GUI_FILTER_NEAREST;
    }
    // Only the source rectangle shows: clip to the destination rectangle.
    __gui_canvas_fill_rect_with(self, dx, dy, dw, dh, &p);
}

void GuiCanvas::draw_image(const &GuiImage img, float x, float y) {
    self.draw_image_rect(img, 0.0f, 0.0f, (float)img.width, (float)img.height,
                         x, y, (float)img.width, (float)img.height);
}

void GuiCanvas::draw_image_scaled(const &GuiImage img, float x, float y, float w, float h) {
    self.draw_image_rect(img, 0.0f, 0.0f, (float)img.width, (float)img.height, x, y, w, h);
}

// ── reading back ──────────────────────────────────────────────────────────

struct GuiColor GuiCanvas::get_pixel(int x, int y) const {
    if (x < 0 || y < 0 || x >= self.width || y >= self.height) {
        return GuiColor::rgba((unsigned char)0, (unsigned char)0, (unsigned char)0, (unsigned char)0);
    }
    unsafe {
        const unsigned char* p = (const unsigned char*)self.px + (unsigned long)y * (unsigned long)self.stride + (unsigned long)x * 4UL;
        unsigned int a = (unsigned int)p[3];
        if (a == 0U) { return GuiColor::rgba((unsigned char)0, (unsigned char)0, (unsigned char)0, (unsigned char)0); }
        if (a == 255U) { return GuiColor::rgba(p[0], p[1], p[2], p[3]); }
        unsigned int r = ((unsigned int)p[0] * 255U + a / 2U) / a;
        unsigned int g = ((unsigned int)p[1] * 255U + a / 2U) / a;
        unsigned int b = ((unsigned int)p[2] * 255U + a / 2U) / a;
        return GuiColor::rgba((unsigned char)(r > 255U ? 255U : r), (unsigned char)(g > 255U ? 255U : g),
                              (unsigned char)(b > 255U ? 255U : b), (unsigned char)a);
    }
}

} // namespace std