feat(mtk): anti-alias rounded-rect frames (buttons, fields, panels)
Route draw_rounded_frame through a new fill_round_rect_aa that fills the straight interior solid and 4x4-supersamples only the four corner arcs, so button/text-field/panel/list-frame corners are smooth instead of pixelated, at negligible cost. Consolidate the checkbox's private AA fill/blend helpers onto the shared aa_blend_px / fill_round_rect_aa primitives. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user