feat: wi-fi - expand support and fix issues, add GUI components

This commit is contained in:
2026-08-07 10:36:53 +02:00
parent bbe1df62fd
commit 9eb21eb3e3
67 changed files with 4573 additions and 245 deletions
+39 -5
View File
@@ -161,24 +161,58 @@ struct Canvas {
// ---- Text ----
void text_bitmap(int x, int y, const char* str, Color c, int scale = 1) {
if (!str || !font_data || scale <= 0) return;
uint32_t pixel = c.to_pixel();
int cx = x;
for (int i = 0; str[i]; i++, cx += FONT_WIDTH * scale) {
const uint8_t* glyph =
&font_data[(unsigned char)str[i] * FONT_HEIGHT];
for (int row = 0; row < FONT_HEIGHT; row++) {
uint8_t bits = glyph[row];
if (!bits) continue;
for (int col = 0; col < FONT_WIDTH; col++) {
if (!(bits & (0x80 >> col))) continue;
int px = cx + col * scale;
int py = y + row * scale;
for (int sy = 0; sy < scale; sy++) {
int dy = py + sy;
if (dy < 0 || dy >= h) continue;
for (int sx = 0; sx < scale; sx++) {
int dx = px + sx;
if (dx >= 0 && dx < w) pixels[dy * w + dx] = pixel;
}
}
}
}
}
}
void text(int x, int y, const char* str, Color c) {
if (fonts::system_font && fonts::system_font->valid) {
fonts::system_font->draw_to_buffer(pixels, w, h, x, y, str, c, fonts::UI_SIZE);
if (fonts::system_font->draw_to_buffer(
pixels, w, h, x, y, str, c, fonts::UI_SIZE))
return;
}
text_bitmap(x, y, str, c);
}
void text_2x(int x, int y, const char* str, Color c) {
if (fonts::system_font && fonts::system_font->valid) {
fonts::system_font->draw_to_buffer(pixels, w, h, x, y, str, c, fonts::LARGE_SIZE);
if (fonts::system_font->draw_to_buffer(
pixels, w, h, x, y, str, c, fonts::LARGE_SIZE))
return;
}
text_bitmap(x, y, str, c, 2);
}
void text_mono(int x, int y, const char* str, Color c) {
if (fonts::mono && fonts::mono->valid) {
fonts::mono->draw_to_buffer(pixels, w, h, x, y, str, c, fonts::TERM_SIZE);
return;
if (fonts::mono->draw_to_buffer(
pixels, w, h, x, y, str, c, fonts::TERM_SIZE))
return;
}
text(x, y, str, c);
text_bitmap(x, y, str, c);
}
// ---- Icons ----
+47 -6
View File
@@ -15,9 +15,8 @@
namespace gui {
static constexpr int MAX_WINDOWS = 8;
static constexpr int MAX_WINDOWS = 32;
static constexpr int PANEL_HEIGHT = 32;
static constexpr int MAX_LAUNCHER_ITEMS = 64;
enum DesktopItemSection : uint8_t {
DESKTOP_ITEM_SECTION_HIDDEN = 0,
@@ -111,9 +110,10 @@ struct DesktopState {
bool launcher_open;
char launcher_query[64];
int launcher_query_len;
LauncherItem launcher_items[MAX_LAUNCHER_ITEMS];
LauncherItem* launcher_items; // dynamically grown
int launcher_item_count;
int launcher_results[MAX_LAUNCHER_ITEMS];
int launcher_item_capacity;
int* launcher_results; // indices into launcher_items, same capacity
int launcher_result_count;
int launcher_selected;
int launcher_scroll;
@@ -122,7 +122,6 @@ struct DesktopState {
SvgIcon icon_terminal;
SvgIcon icon_filemanager;
SvgIcon icon_sysinfo;
SvgIcon icon_appmenu;
SvgIcon icon_folder;
SvgIcon icon_file;
@@ -181,6 +180,48 @@ struct DesktopState {
uint64_t net_cfg_last_poll;
Rect net_icon_rect;
// Registered link-layer interfaces. The IP configuration is global to the
// stack, so the wired and wireless popups use `active` to decide which of
// them owns the address currently on screen.
static constexpr int MAX_NETIFS = 4;
montauk::abi::NetIfInfo netifs[MAX_NETIFS];
int netif_count;
bool eth_present;
// ---- Wi-Fi -------------------------------------------------------------
// The panel entry exists only when a supported adapter is present.
static constexpr int MAX_WIFI_NETWORKS = 32;
static constexpr int WIFI_SSID_CAP = 36;
static constexpr int WIFI_PSK_CAP = 72;
SvgIcon icon_wifi;
bool wifi_present;
bool wifi_popup_open;
Rect wifi_icon_rect;
montauk::abi::WifiInfo wifi_info;
montauk::abi::WifiNetwork wifi_networks[MAX_WIFI_NETWORKS];
int wifi_network_count;
uint64_t wifi_last_poll;
uint32_t wifi_scan_generation;
bool wifi_scanning;
int wifi_scroll; // first visible row of the list
bool wifi_boot_scan_started; // the automatic scan at startup
bool wifi_autoconnect_done; // saved-network join already tried
bool wifi_joining; // a join we started is in flight
bool wifi_dhcp_pending; // ask for a lease once the link is up
char wifi_joining_ssid[WIFI_SSID_CAP];
char wifi_status[96]; // last result line in the popup
uint64_t wifi_status_time;
// Passphrase prompt, shown when a selected network needs a key.
bool wifi_prompt_open;
char wifi_prompt_ssid[WIFI_SSID_CAP];
char wifi_prompt_password[WIFI_PSK_CAP];
int wifi_prompt_len;
bool wifi_prompt_reveal;
bool wifi_prompt_remember;
uint8_t wifi_prompt_security;
bool vol_popup_open;
Rect vol_icon_rect;
int vol_level; // 0-100
@@ -207,7 +248,7 @@ struct DesktopState {
// IDs of external windows we've sent a close event to but that haven't
// been destroyed yet by their owning process. Prevents the poll loop
// from re-creating them at the default position (visible flicker).
static constexpr int MAX_CLOSING = 8;
static constexpr int MAX_CLOSING = MAX_WINDOWS;
int closing_ext_ids[MAX_CLOSING];
int closing_ext_count;
+4 -2
View File
@@ -14,8 +14,10 @@ namespace gui {
static constexpr int FONT_WIDTH = 8;
static constexpr int FONT_HEIGHT = 16;
// Defined in font_data.cpp
extern const uint8_t font_data[256 * 16];
// Defined by programs that ship the built-in VGA fallback. Keep the symbol
// weak so Canvas remains usable by small apps that intentionally rely only on
// TrueType fonts, while login/desktop can recover when TrueType setup fails.
extern const uint8_t font_data[256 * 16] __attribute__((weak));
// Dynamic font height: TTF line height or 16 (bitmap fallback)
inline int system_font_height() {
+235 -4
View File
@@ -678,6 +678,78 @@ 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) {
// Push the arc center a half pixel away from this corner (toward the
// box interior). Pixel-center sampling otherwise sits half a pixel
// closer to the center than the legacy pixel-corner test did, which
// filled the arcs fuller and made small radii read as square. Left
// corners share x == box.x, top corners share y == box.y.
double cx = cn[2] + (cn[0] == box.x ? 0.5 : -0.5);
double cy = cn[3] + (cn[1] == box.y ? 0.5 : -0.5);
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) - cx;
double dy = (py + (sy + 0.5) / S) - cy;
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 +758,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,
@@ -942,6 +1014,165 @@ 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};
}
// 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) aa_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);
int radius = gui_max(box.w / 5, 2);
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.
fill_round_rect_aa(c, box, radius, border);
Rect inner = {box.x + 1, box.y + 1, box.w - 2, box.h - 2};
fill_round_rect_aa(c, inner, gui_max(radius - 1, 1), fill);
} else {
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};
fill_round_rect_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) aa_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};
}
+12 -2
View File
@@ -795,11 +795,21 @@ static inline void terminal_resize(TerminalState* t, int new_cols, int new_rows)
}
}
int new_scrollback = keep - new_rows;
// Anchor the new visible region to the cursor's line. Treating the bottom
// new_rows of kept content as the screen (the naive keep - new_rows) only
// works when the screen is full: with a partly filled screen -- e.g. a
// shell prompt a few lines down with blank rows below it -- it would push
// the real text up into scrollback and drop the view onto the blank region,
// so a zoom appears to scroll down and strands the cursor in empty space.
// Instead pin the cursor to the bottom row when there is enough history
// above it, otherwise keep the content anchored at the top.
int abs_cursor_y = t->scrollback_lines + t->cursor_y - discard;
if (abs_cursor_y < 0) abs_cursor_y = 0;
int new_scrollback = abs_cursor_y - (new_rows - 1);
if (new_scrollback < 0) new_scrollback = 0;
if (new_scrollback > t->max_scrollback) new_scrollback = t->max_scrollback;
// Adjust cursor
int abs_cursor_y = t->scrollback_lines + t->cursor_y - discard;
int new_cursor_y = abs_cursor_y - new_scrollback;
if (new_cursor_y < 0) new_cursor_y = 0;
if (new_cursor_y >= new_rows) new_cursor_y = new_rows - 1;
+15 -4
View File
@@ -121,10 +121,18 @@ struct TrueTypeFont {
return false;
}
montauk::read(fd, data, 0, size);
int read_result = montauk::read(fd, data, 0, size);
montauk::close(fd);
if (read_result < 0 || (uint64_t)read_result != size) {
montauk::free(data);
data = nullptr;
return false;
}
if (!stbtt_InitFont(&info, data, stbtt_GetFontOffsetForIndex(data, 0))) {
// stbtt_GetFontOffsetForIndex returns -1 for non-font data; passing
// that into stbtt_InitFont makes it dereference data + (uint32)-1.
int off = stbtt_GetFontOffsetForIndex(data, 0);
if (off < 0 || !stbtt_InitFont(&info, data, off)) {
montauk::free(data);
data = nullptr;
return false;
@@ -308,13 +316,14 @@ struct TrueTypeFont {
draw(fb, x, y, text, fg, pixel_size);
}
void draw_to_buffer(uint32_t* pixels, int buf_w, int buf_h,
bool draw_to_buffer(uint32_t* pixels, int buf_w, int buf_h,
int x, int y, const char* text,
Color color, int pixel_size) {
if (!valid) return;
if (!valid) return false;
GlyphCache* gc = get_cache(pixel_size);
int cx = x;
int baseline = y + gc->ascent;
bool drew_pixel = false;
for (int i = 0; text[i]; i++) {
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]);
@@ -331,6 +340,7 @@ struct TrueTypeFont {
if (dx < 0 || dx >= buf_w) continue;
uint8_t alpha = g->bitmap[row * g->width + col];
if (alpha == 0) continue;
drew_pixel = true;
if (alpha == 255) {
pixels[dy * buf_w + dx] =
@@ -353,6 +363,7 @@ struct TrueTypeFont {
}
cx += g->advance;
}
return drew_pixel;
}
// Draw text to buffer with clip rectangle (pixels outside clip_x..clip_x+clip_w are not drawn)