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:
2026-07-17 20:52:19 +02:00
co-authored by Claude Opus 4.8
parent 8e6b619b02
commit 0377be4524
18 changed files with 1647 additions and 813 deletions
+211
View File
@@ -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};
}