diff --git a/programs/include/gui/mtk/widgets.hpp b/programs/include/gui/mtk/widgets.hpp index 2115404..0285367 100644 --- a/programs/include/gui/mtk/widgets.hpp +++ b/programs/include/gui/mtk/widgets.hpp @@ -678,6 +678,71 @@ inline bool same_color(Color a, Color b) { return a.r == b.r && a.g == b.g && a.b == b.b && a.a == b.a; } +// ============================================================================ +// Anti-aliased rounded-rect fill +// ============================================================================ + +// Coverage-based pixel blend; `cov` is 0..255 and the destination is treated +// as opaque. +inline void aa_blend_px(Canvas& c, int x, int y, Color color, int cov) { + if (x < 0 || x >= c.w || y < 0 || y >= c.h || cov <= 0) return; + if (cov > 255) cov = 255; + uint32_t dst = c.pixels[y * c.w + x]; + uint32_t dr = (dst >> 16) & 0xFF, dg = (dst >> 8) & 0xFF, db = dst & 0xFF; + uint32_t a = (uint32_t)cov, ia = 255 - a; + uint32_t nr = (color.r * a + dr * ia + 127) / 255; + uint32_t ng = (color.g * a + dg * ia + 127) / 255; + uint32_t nb = (color.b * a + db * ia + 127) / 255; + c.pixels[y * c.w + x] = 0xFF000000u | (nr << 16) | (ng << 8) | nb; +} + +// Rounded-rect fill with 4x4-supersampled corners. The straight interior is +// filled solid (fast); only the four corner arcs pay for anti-aliasing, so +// this stays cheap even for large frames. +inline void fill_round_rect_aa(Canvas& c, const Rect& box, int radius, Color color) { + if (box.w <= 0 || box.h <= 0) return; + int r = radius; + if (r < 0) r = 0; + if (r > box.w / 2) r = box.w / 2; + if (r > box.h / 2) r = box.h / 2; + + if (r == 0) { + c.fill_rect(box.x, box.y, box.w, box.h, color); + return; + } + + // Solid interior: center band + top/bottom edge bands between the corners. + c.fill_rect(box.x, box.y + r, box.w, box.h - 2 * r, color); + c.fill_rect(box.x + r, box.y, box.w - 2 * r, r, color); + c.fill_rect(box.x + r, box.y + box.h - r, box.w - 2 * r, r, color); + + const int S = 4; + double r2 = (double)r * r; + // (cell origin x, cell origin y, arc center x, arc center y) + int corners[4][4] = { + {box.x, box.y, box.x + r, box.y + r}, + {box.x + box.w - r, box.y, box.x + box.w - r, box.y + r}, + {box.x, box.y + box.h - r, box.x + r, box.y + box.h - r}, + {box.x + box.w - r, box.y + box.h - r, box.x + box.w - r, box.y + box.h - r}, + }; + for (auto& cn : corners) { + for (int yy = 0; yy < r; yy++) { + int py = cn[1] + yy; + for (int xx = 0; xx < r; xx++) { + int px = cn[0] + xx; + int hits = 0; + for (int sy = 0; sy < S; sy++) + for (int sx = 0; sx < S; sx++) { + double dx = (px + (sx + 0.5) / S) - cn[2]; + double dy = (py + (sy + 0.5) / S) - cn[3]; + if (dx * dx + dy * dy <= r2) hits++; + } + if (hits) aa_blend_px(c, px, py, color, hits * 255 / (S * S)); + } + } + } +} + inline void draw_rounded_frame(Canvas& c, const Rect& bounds, int radius, @@ -686,16 +751,16 @@ inline void draw_rounded_frame(Canvas& c, int border_w = 1) { if (bounds.empty()) return; if (border_w <= 0 || same_color(fill, border)) { - c.fill_rounded_rect(bounds.x, bounds.y, bounds.w, bounds.h, gui_max(radius, 0), fill); + fill_round_rect_aa(c, bounds, gui_max(radius, 0), fill); return; } - c.fill_rounded_rect(bounds.x, bounds.y, bounds.w, bounds.h, gui_max(radius, 0), border); + fill_round_rect_aa(c, bounds, gui_max(radius, 0), border); int inner_w = bounds.w - border_w * 2; int inner_h = bounds.h - border_w * 2; if (inner_w <= 0 || inner_h <= 0) return; - c.fill_rounded_rect(bounds.x + border_w, bounds.y + border_w, - inner_w, inner_h, gui_max(radius - border_w, 0), fill); + Rect inner = {bounds.x + border_w, bounds.y + border_w, inner_w, inner_h}; + fill_round_rect_aa(c, inner, gui_max(radius - border_w, 0), fill); } inline ButtonColors resolve_button_colors(ButtonVariant variant, @@ -960,58 +1025,6 @@ inline Rect checkbox_indicator_rect(const Rect& option, int size = 16) { return {option.x, option.y + (option.h - size) / 2, size, size}; } -// Coverage-based pixel blend used by the anti-aliased checkbox drawing. -// `cov` is 0..255; the destination is treated as opaque. -inline void checkbox_blend_px(Canvas& c, int x, int y, Color color, int cov) { - if (x < 0 || x >= c.w || y < 0 || y >= c.h || cov <= 0) return; - if (cov > 255) cov = 255; - uint32_t dst = c.pixels[y * c.w + x]; - uint32_t dr = (dst >> 16) & 0xFF, dg = (dst >> 8) & 0xFF, db = dst & 0xFF; - uint32_t a = (uint32_t)cov, ia = 255 - a; - uint32_t nr = (color.r * a + dr * ia + 127) / 255; - uint32_t ng = (color.g * a + dg * ia + 127) / 255; - uint32_t nb = (color.b * a + db * ia + 127) / 255; - c.pixels[y * c.w + x] = 0xFF000000u | (nr << 16) | (ng << 8) | nb; -} - -// Point (px, py) inside a rounded rect of size w x h with corner radius r, -// evaluated in the rect's local coordinate space. -inline bool round_rect_contains(double px, double py, double w, double h, double r) { - if (px < 0 || py < 0 || px > w || py > h) return false; - double rx = 0, ry = 0; - bool corner = false; - if (px < r && py < r) { rx = r - px; ry = r - py; corner = true; } - else if (px > w - r && py < r) { rx = px - (w - r); ry = r - py; corner = true; } - else if (px < r && py > h - r) { rx = r - px; ry = py - (h - r); corner = true; } - else if (px > w - r && py > h - r) { rx = px - (w - r); ry = py - (h - r); corner = true; } - if (!corner) return true; - return rx * rx + ry * ry <= r * r; -} - -// Anti-aliased rounded-rect fill via 4x4 supersampling. Kept local to the -// checkbox so the rest of the toolkit's crisp fills are unchanged. -inline void checkbox_fill_round_aa(Canvas& c, const Rect& box, double radius, Color color) { - if (box.w <= 0 || box.h <= 0) return; - if (radius < 0) radius = 0; - const int S = 4; - for (int row = -1; row <= box.h; row++) { - int py = box.y + row; - if (py < 0 || py >= c.h) continue; - for (int col = -1; col <= box.w; col++) { - int px = box.x + col; - if (px < 0 || px >= c.w) continue; - int hits = 0; - for (int sy = 0; sy < S; sy++) - for (int sx = 0; sx < S; sx++) { - double fx = col + (sx + 0.5) / S; - double fy = row + (sy + 0.5) / S; - if (round_rect_contains(fx, fy, box.w, box.h, radius)) hits++; - } - if (hits) checkbox_blend_px(c, px, py, color, hits * 255 / (S * S)); - } - } -} - // Squared distance from a point to a line segment (no sqrt; used for the // thickness test of the checkmark strokes). inline double checkbox_seg_dist2(double px, double py, @@ -1046,7 +1059,7 @@ inline void draw_check_mark(Canvas& c, const Rect& box, Color color) { double d2 = checkbox_seg_dist2(fx, fy, bx, by, cx, cy); if ((d1 < d2 ? d1 : d2) <= hw2) hits++; } - if (hits) checkbox_blend_px(c, px, py, color, hits * 255 / (S * S)); + if (hits) aa_blend_px(c, px, py, color, hits * 255 / (S * S)); } } } @@ -1059,7 +1072,7 @@ inline void draw_checkbox(Canvas& c, bool enabled = true, bool hovered = false) { Rect box = checkbox_indicator_rect(option); - double radius = box.w * 0.22; + int radius = gui_max(box.w / 5, 2); Color fill, border, mark; if (!enabled) { @@ -1078,18 +1091,18 @@ inline void draw_checkbox(Canvas& c, if (state == CHECK_OFF) { // Colored ring with a white interior. - checkbox_fill_round_aa(c, box, radius, border); + fill_round_rect_aa(c, box, radius, border); Rect inner = {box.x + 1, box.y + 1, box.w - 2, box.h - 2}; - checkbox_fill_round_aa(c, inner, radius - 1, fill); + fill_round_rect_aa(c, inner, gui_max(radius - 1, 1), fill); } else { - checkbox_fill_round_aa(c, box, radius, fill); + fill_round_rect_aa(c, box, radius, fill); } if (state == CHECK_ON) { draw_check_mark(c, box, mark); } else if (state == CHECK_MIXED) { Rect dash = {box.x + 4, box.y + box.h / 2 - 1, box.w - 8, 2}; - checkbox_fill_round_aa(c, dash, 1, mark); + fill_round_rect_aa(c, dash, 1, mark); } if (label && label[0]) { @@ -1148,7 +1161,7 @@ inline void draw_disclosure(Canvas& c, (e0 <= 0 && e1 <= 0 && e2 <= 0); if (inside) hits++; } - if (hits) checkbox_blend_px(c, px, py, color, hits * 255 / (S * S)); + if (hits) aa_blend_px(c, px, py, color, hits * 255 / (S * S)); } } }