feat: redesign installer on MTK toolkit with component selection
Rewrite the Installer app on the Montauk Toolkit (Canvas + gui/mtk widgets, theme, hover states, scrollbar) replacing the hand-rolled px_* renderer and bespoke TrueType usage. Add a new "Software" step: an expandable checkbox tree for optional components (Montauk SDK with gcc/g++, binutils, tcc, lua sub-items; Games; Office; Internet apps; Printing; experimental httpd and SDR). Unchecked components' paths are excluded from the install copy, with a longest-path ownership rule so e.g. sdk/tcc installs even when the rest of the SDK is deselected. The update flow refreshes only the components the target already has. Add tri-state checkbox and disclosure-arrow widgets to the MTK toolkit (anti-aliased, supersampled). Merge tcc and lua into 0:/sdk (0:/sdk/tcc, 0:/sdk/lua) and drop 0:/lib: update tcc config.h, lua luaconf.h, both Makefiles, the devkit clean scope, man pages and montaukos.org notices. Make printing optional: init skips printd when 0:/os/printd.elf is absent. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
@@ -942,6 +942,217 @@ inline void draw_radio(Canvas& c,
|
||||
label, theme.text);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Checkboxes and disclosure arrows
|
||||
// ============================================================================
|
||||
|
||||
enum CheckState : uint8_t {
|
||||
CHECK_OFF = 0,
|
||||
CHECK_ON,
|
||||
CHECK_MIXED,
|
||||
};
|
||||
|
||||
inline CheckState check_state(bool checked) {
|
||||
return checked ? CHECK_ON : CHECK_OFF;
|
||||
}
|
||||
|
||||
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,
|
||||
double ax, double ay, double bx, double by) {
|
||||
double dx = bx - ax, dy = by - ay;
|
||||
double len2 = dx * dx + dy * dy;
|
||||
double t = len2 > 0 ? ((px - ax) * dx + (py - ay) * dy) / len2 : 0.0;
|
||||
if (t < 0) t = 0; else if (t > 1) t = 1;
|
||||
double ex = px - (ax + t * dx), ey = py - (ay + t * dy);
|
||||
return ex * ex + ey * ey;
|
||||
}
|
||||
|
||||
// Anti-aliased two-stroke checkmark via 4x4 supersampling of a fixed-width
|
||||
// polyline.
|
||||
inline void draw_check_mark(Canvas& c, const Rect& box, Color color) {
|
||||
double ax = box.x + box.w * 0.24, ay = box.y + box.h * 0.50;
|
||||
double bx = box.x + box.w * 0.42, by = box.y + box.h * 0.68;
|
||||
double cx = box.x + box.w * 0.74, cy = box.y + box.h * 0.26;
|
||||
double hw = box.w * 0.085 + 0.35; // half stroke width
|
||||
double hw2 = hw * hw;
|
||||
const int S = 4;
|
||||
for (int py = box.y - 1; py <= box.y + box.h + 1; py++) {
|
||||
if (py < 0 || py >= c.h) continue;
|
||||
for (int px = box.x - 1; px <= box.x + box.w + 1; px++) {
|
||||
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 = px + (sx + 0.5) / S;
|
||||
double fy = py + (sy + 0.5) / S;
|
||||
double d1 = checkbox_seg_dist2(fx, fy, ax, ay, bx, by);
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void draw_checkbox(Canvas& c,
|
||||
const Rect& option,
|
||||
const char* label,
|
||||
CheckState state,
|
||||
const Theme& theme,
|
||||
bool enabled = true,
|
||||
bool hovered = false) {
|
||||
Rect box = checkbox_indicator_rect(option);
|
||||
double radius = box.w * 0.22;
|
||||
|
||||
Color fill, border, mark;
|
||||
if (!enabled) {
|
||||
fill = theme.disabled_bg;
|
||||
border = theme.disabled_bg;
|
||||
mark = theme.disabled_fg;
|
||||
} else if (state == CHECK_OFF) {
|
||||
fill = colors::WHITE;
|
||||
border = hovered ? theme.accent : theme.border;
|
||||
mark = theme.accent_fg;
|
||||
} else {
|
||||
fill = hovered ? theme.accent_hover : theme.accent;
|
||||
border = fill;
|
||||
mark = theme.accent_fg;
|
||||
}
|
||||
|
||||
if (state == CHECK_OFF) {
|
||||
// Colored ring with a white interior.
|
||||
checkbox_fill_round_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);
|
||||
} else {
|
||||
checkbox_fill_round_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);
|
||||
}
|
||||
|
||||
if (label && label[0]) {
|
||||
int fh = system_font_height();
|
||||
c.text(box.x + box.w + theme.gap_sm,
|
||||
option.y + (option.h - fh) / 2,
|
||||
label, enabled ? theme.text : theme.text_muted);
|
||||
}
|
||||
}
|
||||
|
||||
inline Rect disclosure_rect(const Rect& option, int size = 16) {
|
||||
return {option.x, option.y + (option.h - size) / 2, size, size};
|
||||
}
|
||||
|
||||
inline void draw_disclosure(Canvas& c,
|
||||
const Rect& box,
|
||||
bool expanded,
|
||||
const Theme& theme,
|
||||
bool hovered = false) {
|
||||
Color color = hovered ? theme.text : theme.text_subtle;
|
||||
double cx = box.x + box.w / 2.0;
|
||||
double cy = box.y + box.h / 2.0;
|
||||
double s = box.w * 0.26;
|
||||
if (s < 3) s = 3;
|
||||
|
||||
// Triangle vertices (equilateral-ish), rotated per state.
|
||||
double vx[3], vy[3];
|
||||
if (expanded) { // pointing down
|
||||
vx[0] = cx - s; vy[0] = cy - s * 0.6;
|
||||
vx[1] = cx + s; vy[1] = cy - s * 0.6;
|
||||
vx[2] = cx; vy[2] = cy + s * 0.75;
|
||||
} else { // pointing right
|
||||
vx[0] = cx - s * 0.6; vy[0] = cy - s;
|
||||
vx[1] = cx - s * 0.6; vy[1] = cy + s;
|
||||
vx[2] = cx + s * 0.75; vy[2] = cy;
|
||||
}
|
||||
|
||||
// Anti-aliased fill via 4x4 supersampling of the triangle's half-plane test.
|
||||
auto edge = [](double ax, double ay, double bx, double by, double px, double py) {
|
||||
return (px - ax) * (by - ay) - (py - ay) * (bx - ax);
|
||||
};
|
||||
const int S = 4;
|
||||
for (int py = box.y; py < box.y + box.h; py++) {
|
||||
if (py < 0 || py >= c.h) continue;
|
||||
for (int px = box.x; px < box.x + box.w; px++) {
|
||||
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 = px + (sx + 0.5) / S;
|
||||
double fy = py + (sy + 0.5) / S;
|
||||
double e0 = edge(vx[0], vy[0], vx[1], vy[1], fx, fy);
|
||||
double e1 = edge(vx[1], vy[1], vx[2], vy[2], fx, fy);
|
||||
double e2 = edge(vx[2], vy[2], vx[0], vy[0], fx, fy);
|
||||
bool inside = (e0 >= 0 && e1 >= 0 && e2 >= 0) ||
|
||||
(e0 <= 0 && e1 <= 0 && e2 <= 0);
|
||||
if (inside) hits++;
|
||||
}
|
||||
if (hits) checkbox_blend_px(c, px, py, color, hits * 255 / (S * S));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline Rect swatch_rect(int row_x, int row_y, int index, int size = 24, int gap = 6) {
|
||||
return {row_x + index * (size + gap), row_y, size, size};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user