/* * wifi.cpp * Wi-Fi panel entry: the network list popup, the passphrase dialog window, * and the automatic scan and saved-network join that run at startup. * * Everything here goes through the non-blocking Wi-Fi syscalls * (wifi_scan_start / wifi_results / wifi_connect_async). The blocking pair * takes seconds, and the compositor cannot stand still for that long. * * Copyright (c) 2026 Daniel Hammer */ #include "desktop_internal.hpp" #include #include using namespace gui; // ============================================================================ // Geometry // ============================================================================ static constexpr int WIFI_POPUP_W = 264; static constexpr int WIFI_ROW_H = 30; static constexpr int WIFI_VISIBLE = 6; // network rows on screen at once static constexpr int WIFI_BTN_H = 26; static int wifi_popup_x(const DesktopState* ds) { int x = ds->wifi_icon_rect.x + ds->wifi_icon_rect.w - WIFI_POPUP_W; return x < 4 ? 4 : x; } static int wifi_visible_rows(const DesktopState* ds) { int rows = ds->wifi_network_count; if (rows > WIFI_VISIBLE) rows = WIFI_VISIBLE; if (rows < 1) rows = 1; // the "no networks" line return rows; } static int wifi_popup_h(const DesktopState* ds) { int fh = system_font_height(); int header = fh + 10; // title + status int detail = ds->wifi_info.connected ? (fh + 6) * 3 + 8 : 0; int list = wifi_visible_rows(ds) * WIFI_ROW_H; int footer = WIFI_BTN_H + 14 + fh + 6; return header + 10 + detail + list + 10 + footer; } static Rect wifi_popup_rect(const DesktopState* ds) { return {wifi_popup_x(ds), PANEL_HEIGHT + 2, WIFI_POPUP_W, wifi_popup_h(ds)}; } // y of the first network row static int wifi_list_y(const DesktopState* ds) { int fh = system_font_height(); int y = PANEL_HEIGHT + 2 + 10 + fh + 10; if (ds->wifi_info.connected) y += (fh + 6) * 3 + 8; return y; } static Rect wifi_scan_button(const DesktopState* ds) { Rect popup = wifi_popup_rect(ds); int fh = system_font_height(); int y = popup.y + popup.h - 10 - fh - 6 - WIFI_BTN_H; return {popup.x + 12, y, 76, WIFI_BTN_H}; } static Rect wifi_action_button(const DesktopState* ds) { Rect scan = wifi_scan_button(ds); Rect popup = wifi_popup_rect(ds); int w = 96; return {popup.x + popup.w - 12 - w, scan.y, w, WIFI_BTN_H}; } // ============================================================================ // State helpers // ============================================================================ // The saved-network list lives here rather than on the stack: it is several // kilobytes, and the compositor's stack is 32 KiB shared with the TrueType // rasteriser. One copy also saves re-parsing the file on every click. static montauk::wifi::SavedList g_saved; static void wifi_reload_saved() { montauk::wifi::saved_load(&g_saved); } static const char* wifi_saved_psk(const char* ssid) { const montauk::wifi::SavedNetwork* entry = montauk::wifi::saved_find(&g_saved, ssid); return (entry && entry->psk[0]) ? entry->psk : nullptr; } static void wifi_set_status(DesktopState* ds, const char* msg) { montauk::strncpy(ds->wifi_status, msg ? msg : "", sizeof(ds->wifi_status)); ds->wifi_status_time = montauk::get_milliseconds(); } // Sorted strongest-first so the list reads the way a user expects. static void wifi_sort_results(DesktopState* ds) { for (int i = 1; i < ds->wifi_network_count; i++) { montauk::abi::WifiNetwork key = ds->wifi_networks[i]; int j = i - 1; while (j >= 0 && ds->wifi_networks[j].rssi < key.rssi) { ds->wifi_networks[j + 1] = ds->wifi_networks[j]; j--; } ds->wifi_networks[j + 1] = key; } } static void wifi_refresh_results(DesktopState* ds) { montauk::abi::WifiNetwork raw[DesktopState::MAX_WIFI_NETWORKS]; int n = montauk::wifi_results(raw, DesktopState::MAX_WIFI_NETWORKS); if (n < 0) n = 0; // One row per network rather than one per access point: a house with a // repeater answers on several BSSIDs under the same name, and the strongest // is the one worth joining. Hidden networks have no name to join by, so // they are left out of the list entirely. ds->wifi_network_count = 0; for (int i = 0; i < n; i++) { if (!raw[i].ssid[0]) continue; int existing = -1; for (int k = 0; k < ds->wifi_network_count; k++) { if (montauk::streq(ds->wifi_networks[k].ssid, raw[i].ssid)) { existing = k; break; } } if (existing >= 0) { if (raw[i].rssi > ds->wifi_networks[existing].rssi) ds->wifi_networks[existing] = raw[i]; continue; } ds->wifi_networks[ds->wifi_network_count++] = raw[i]; } wifi_sort_results(ds); if (ds->wifi_scroll > ds->wifi_network_count - 1) ds->wifi_scroll = 0; } static void wifi_start_scan(DesktopState* ds) { int rc = montauk::wifi_scan_start(6000); if (rc < 0) { wifi_set_status(ds, "The adapter is not ready yet"); return; } ds->wifi_scanning = true; // No status line here: the button reads "Scanning" and the list says so // too, and three copies of the same word is not progress reporting. ds->wifi_status[0] = '\0'; } static void wifi_begin_join(DesktopState* ds, const char* ssid, const char* psk) { int rc = montauk::wifi_connect_async(ssid, psk); if (rc < 0) { wifi_set_status(ds, montauk::wifi::error_message(rc)); ds->wifi_joining = false; return; } ds->wifi_joining = true; ds->wifi_dhcp_pending = true; montauk::strncpy(ds->wifi_joining_ssid, ssid, sizeof(ds->wifi_joining_ssid)); char msg[96]; snprintf(msg, sizeof(msg), "Connecting to %s...", ssid); wifi_set_status(ds, msg); } // Defined below: the passphrase dialog is a real desktop window, created the // same way the reboot and shutdown dialogs are. static void wifi_open_password_dialog(DesktopState* ds, const montauk::abi::WifiNetwork& net); // Join the network under the cursor: straight away when it is open or its // passphrase is already saved, otherwise ask for the key. static void wifi_select_network(DesktopState* ds, int index) { if (index < 0 || index >= ds->wifi_network_count) return; const montauk::abi::WifiNetwork& net = ds->wifi_networks[index]; if (!montauk::wifi::needs_key(net.security)) { wifi_begin_join(ds, net.ssid, ""); return; } const char* saved = wifi_saved_psk(net.ssid); if (saved) { wifi_begin_join(ds, net.ssid, saved); return; } wifi_open_password_dialog(ds, net); } // ============================================================================ // Startup and polling // ============================================================================ void desktop_wifi_init(DesktopState* ds) { ds->wifi_popup_open = false; ds->wifi_icon_rect = {0, 0, 0, 0}; ds->wifi_network_count = 0; ds->wifi_scroll = 0; ds->wifi_scanning = false; ds->wifi_boot_scan_started = false; ds->wifi_autoconnect_done = false; ds->wifi_joining = false; ds->wifi_dhcp_pending = false; ds->wifi_dhcp_waiting = false; ds->wifi_joining_ssid[0] = '\0'; ds->wifi_status[0] = '\0'; ds->wifi_status_time = 0; ds->wifi_scan_generation = 0; ds->wifi_last_poll = 0; wifi_reload_saved(); montauk::memset(&ds->wifi_info, 0, sizeof(ds->wifi_info)); ds->wifi_present = montauk::wifi_info(&ds->wifi_info) == 0 && ds->wifi_info.present; } // Try the strongest saved network that is actually in range. static void wifi_try_autoconnect(DesktopState* ds) { if (!g_saved.autoconnect || g_saved.count == 0) { ds->wifi_autoconnect_done = true; return; } for (int i = 0; i < ds->wifi_network_count; i++) { // strongest first const montauk::abi::WifiNetwork& net = ds->wifi_networks[i]; const montauk::wifi::SavedNetwork* entry = montauk::wifi::saved_find(&g_saved, net.ssid); if (!entry) continue; wifi_begin_join(ds, entry->ssid, entry->psk); break; } ds->wifi_autoconnect_done = true; } // Called from the panel refresh. Returns true when something on screen moved. bool desktop_wifi_poll(DesktopState* ds, uint64_t now) { // The Network app writes the same file, so pick up its edits whenever the // popup is opened rather than trusting the copy read at startup. static bool popup_was_open = false; if (ds->wifi_popup_open && !popup_was_open) wifi_reload_saved(); popup_was_open = ds->wifi_popup_open; // The adapter finishes its firmware load well after login, so keep looking // for it until it shows up rather than deciding once at startup. if (!ds->wifi_present) { if (now - ds->wifi_last_poll < 3000) return false; ds->wifi_last_poll = now; montauk::abi::WifiInfo info; if (montauk::wifi_info(&info) != 0 || !info.present) return false; ds->wifi_info = info; ds->wifi_present = true; return true; // the icon appears now } // Poll faster while something is in flight so progress text keeps up. bool busy = ds->wifi_scanning || ds->wifi_joining || ds->wifi_popup_open; if (now - ds->wifi_last_poll < (busy ? 400u : 3000u)) return false; ds->wifi_last_poll = now; montauk::abi::WifiInfo info; if (montauk::wifi_info(&info) != 0) return false; bool changed = info.connected != ds->wifi_info.connected || info.connState != ds->wifi_info.connState || info.scanning != ds->wifi_info.scanning || info.scanGeneration != ds->wifi_info.scanGeneration || info.state != ds->wifi_info.state; ds->wifi_info = info; // The first scan runs by itself once the firmware is up, so the list is // already populated the first time the user opens the popup. if (!ds->wifi_boot_scan_started && info.state == montauk::abi::WIFI_STATE_RUNNING) { ds->wifi_boot_scan_started = true; wifi_start_scan(ds); return true; } if (info.scanGeneration != ds->wifi_scan_generation) { ds->wifi_scan_generation = info.scanGeneration; ds->wifi_scanning = false; wifi_refresh_results(ds); changed = true; char msg[64]; if (ds->wifi_network_count > 0) { snprintf(msg, sizeof(msg), "Found %d network%s", ds->wifi_network_count, ds->wifi_network_count == 1 ? "" : "s"); } else { snprintf(msg, sizeof(msg), "No networks found"); } wifi_set_status(ds, msg); // Rejoin whatever was saved as soon as the first sweep lands. if (!ds->wifi_autoconnect_done && !info.connected && !ds->wifi_joining) { wifi_reload_saved(); wifi_try_autoconnect(ds); } } else if (ds->wifi_scanning && !info.scanning) { ds->wifi_scanning = false; changed = true; } if (ds->wifi_joining) { if (info.connected) { ds->wifi_joining = false; char msg[96]; snprintf(msg, sizeof(msg), "Connected to %s", info.ssid); wifi_set_status(ds, msg); changed = true; } else if (info.lastError != 0 && !info.joining) { ds->wifi_joining = false; ds->wifi_dhcp_pending = false; ds->wifi_dhcp_waiting = false; wifi_set_status(ds, montauk::wifi::error_message(info.lastError)); changed = true; } } // A wireless link with no address of its own needs a lease; the wired // interface, if there is one, keeps whatever it already had. if (ds->wifi_dhcp_pending && info.connected && ds->cached_net_cfg.ipAddress == 0) { ds->wifi_dhcp_pending = false; ds->wifi_dhcp_waiting = true; montauk::spawn("0:/os/dhcp.elf"); wifi_set_status(ds, "Requesting an address..."); changed = true; } else if (ds->wifi_dhcp_pending && info.connected) { ds->wifi_dhcp_pending = false; } // "Requesting an address..." is only true until the lease lands. Leaving it // up afterwards contradicts the address row further up the popup, so it is // replaced the moment there is an address (or the link goes away). if (ds->wifi_dhcp_waiting && (ds->cached_net_cfg.ipAddress != 0 || !info.connected)) { bool leased = ds->cached_net_cfg.ipAddress != 0; ds->wifi_dhcp_waiting = false; if (leased) { char msg[96]; snprintf(msg, sizeof(msg), "Connected to %s", info.ssid); wifi_set_status(ds, msg); } else { ds->wifi_status[0] = '\0'; } changed = true; } return changed; } // ============================================================================ // Drawing // ============================================================================ static void draw_signal_bars(Framebuffer& fb, int x, int y, int8_t rssi, Color on, Color off) { int bars = montauk::wifi::signal_bars(rssi); for (int i = 0; i < 4; i++) { int h = 3 + i * 3; fb.fill_rect(x + i * 4, y + 12 - h, 3, h, i < bars ? on : off); } } // A small padlock, so encrypted networks read at a glance. static void draw_lock(Framebuffer& fb, int x, int y, Color c) { fb.fill_rect(x + 1, y + 5, 8, 6, c); fb.fill_rect(x + 2, y + 2, 1, 3, c); fb.fill_rect(x + 7, y + 2, 1, 3, c); fb.fill_rect(x + 3, y + 1, 4, 1, c); } static void draw_kv_row(Framebuffer& fb, int lx, int vx, int y, const char* label, const char* value, Color dim, Color text) { draw_text(fb, lx, y, label, dim); draw_text(fb, vx, y, value, text); } void desktop_draw_wifi_popup(DesktopState* ds) { Framebuffer& fb = ds->fb; Rect popup = wifi_popup_rect(ds); int fh = system_font_height(); Color dim = Color::from_rgb(0x66, 0x66, 0x66); Color faint = Color::from_rgb(0xCC, 0xCC, 0xCC); draw_shadow(fb, popup.x, popup.y, popup.w, popup.h, 4, colors::SHADOW); fill_rounded_rect(fb, popup.x, popup.y, popup.w, popup.h, 8, colors::MENU_BG); draw_rect(fb, popup.x, popup.y, popup.w, popup.h, colors::BORDER); int lx = popup.x + 14; int ty = popup.y + 10; // Header: "Wi-Fi" + connection state draw_text(fb, lx, ty, "Wi-Fi", colors::TEXT_COLOR); const char* status_str; if (ds->wifi_info.connected) status_str = "Connected"; else if (ds->wifi_joining) status_str = "Connecting"; else if (ds->wifi_info.state == montauk::abi::WIFI_STATE_RFKILL) status_str = "Radio off"; else status_str = "Not connected"; Color dot_color = ds->wifi_info.connected ? Color::from_rgb(0x4C, 0xAF, 0x50) : (ds->wifi_joining ? Color::from_rgb(0xD0, 0x9A, 0x2E) : Color::from_rgb(0xCC, 0x33, 0x33)); int sw = text_width(status_str); int status_x = popup.x + popup.w - 14 - sw; fill_circle(fb, status_x - 9, ty + fh / 2, 4, dot_color); draw_text(fb, status_x, ty, status_str, dim); ty += fh + 10; // What this interface has, when it has it. The address belongs to the // wireless interface only while that is the one carrying traffic. if (ds->wifi_info.connected) { int vx = popup.x + 76; char value[40]; montauk::strncpy(value, ds->wifi_info.ssid[0] ? ds->wifi_info.ssid : "-", sizeof(value)); draw_kv_row(fb, lx, vx, ty, "Network", value, dim, colors::TEXT_COLOR); ty += fh + 6; bool wireless_active = false; for (int i = 0; i < ds->netif_count; i++) { if (ds->netifs[i].kind == montauk::abi::NETIF_KIND_WIRELESS && ds->netifs[i].active) wireless_active = true; } if (wireless_active && ds->cached_net_cfg.ipAddress != 0) format_ip(value, ds->cached_net_cfg.ipAddress); else montauk::strcpy(value, "No address"); draw_kv_row(fb, lx, vx, ty, "IP", value, dim, colors::TEXT_COLOR); ty += fh + 6; snprintf(value, sizeof(value), "Channel %d", (int)ds->wifi_info.channel); draw_kv_row(fb, lx, vx, ty, "Radio", value, dim, colors::TEXT_COLOR); ty += fh + 6 + 8; } // Network list int list_y = wifi_list_y(ds); int mx = ds->mouse.x, my = ds->mouse.y; if (ds->wifi_network_count == 0) { const char* empty = ds->wifi_scanning ? "Looking for networks..." : "No networks found"; draw_text(fb, lx, list_y + (WIFI_ROW_H - fh) / 2, empty, dim); } int rows = wifi_visible_rows(ds); for (int r = 0; r < rows && (ds->wifi_scroll + r) < ds->wifi_network_count; r++) { int idx = ds->wifi_scroll + r; const montauk::abi::WifiNetwork& net = ds->wifi_networks[idx]; Rect row = {popup.x + 6, list_y + r * WIFI_ROW_H, popup.w - 12, WIFI_ROW_H}; bool current = ds->wifi_info.connected && montauk::streq(net.ssid, ds->wifi_info.ssid); if (row.contains(mx, my)) fill_rounded_rect(fb, row.x, row.y, row.w, row.h, 4, gui::mtk::accent_hover_tint(ds->settings.accent_color)); else if (current) fill_rounded_rect(fb, row.x, row.y, row.w, row.h, 4, Color::from_rgb(0xEC, 0xF2, 0xFB)); draw_signal_bars(fb, row.x + 8, row.y + (WIFI_ROW_H - 12) / 2, net.rssi, current ? ds->settings.accent_color : Color::from_rgb(0x55, 0x55, 0x55), faint); int text_x = row.x + 32; int text_y = row.y + (WIFI_ROW_H - fh) / 2; int text_max = row.w - 32 - 26; char label[40]; montauk::strncpy(label, net.ssid[0] ? net.ssid : "(hidden)", sizeof(label)); while (label[0] && text_width(label) > text_max) { int n = montauk::slen(label); label[n - 1] = '\0'; } draw_text(fb, text_x, text_y, label, colors::TEXT_COLOR); if (montauk::wifi::needs_key(net.security)) draw_lock(fb, row.x + row.w - 20, row.y + (WIFI_ROW_H - 12) / 2, dim); } // A slim scrollbar rather than a line of text: the list scrolls with the // wheel, and there is no room under it for a hint. if (ds->wifi_network_count > WIFI_VISIBLE) { int track_x = popup.x + popup.w - 8; int track_y = list_y + 2; int track_h = rows * WIFI_ROW_H - 4; fb.fill_rect(track_x, track_y, 3, track_h, faint); int thumb_h = track_h * WIFI_VISIBLE / ds->wifi_network_count; if (thumb_h < 12) thumb_h = 12; int span = ds->wifi_network_count - WIFI_VISIBLE; int thumb_y = track_y + (span > 0 ? (track_h - thumb_h) * ds->wifi_scroll / span : 0); fb.fill_rect(track_x, thumb_y, 3, thumb_h, Color::from_rgb(0x99, 0x99, 0x99)); } // Footer: scan / disconnect and the last status line Rect scan = wifi_scan_button(ds); Rect action = wifi_action_button(ds); auto draw_btn = [&](const Rect& r, const char* label, bool primary) { Color bg = primary ? ds->settings.accent_color : Color::from_rgb(0xE0, 0xE0, 0xE0); if (r.contains(mx, my)) bg = primary ? gui::mtk::darken(ds->settings.accent_color, 32) : Color::from_rgb(0xD0, 0xD0, 0xD0); fill_rounded_rect(fb, r.x, r.y, r.w, r.h, 6, bg); int tw = text_width(label); draw_text(fb, r.x + (r.w - tw) / 2, r.y + (r.h - fh) / 2, label, primary ? colors::WHITE : colors::TEXT_COLOR); }; draw_btn(scan, ds->wifi_scanning ? "Scanning" : "Scan", false); draw_btn(action, ds->wifi_info.connected ? "Disconnect" : "Rescan", ds->wifi_info.connected); // A result worth reading now, not one left over from ten minutes ago. bool status_fresh = ds->wifi_status[0] && montauk::get_milliseconds() - ds->wifi_status_time < 20000; const char* line = status_fresh ? ds->wifi_status : ""; if (ds->wifi_joining && ds->wifi_info.connState != montauk::abi::WIFI_CONN_IDLE) line = montauk::wifi::conn_state_name(ds->wifi_info.connState); if (line && line[0]) { char fitted[64]; montauk::strncpy(fitted, line, sizeof(fitted)); while (fitted[0] && text_width(fitted) > popup.w - 28) { int n = montauk::slen(fitted); fitted[n - 1] = '\0'; } draw_text(fb, lx, scan.y + scan.h + 8, fitted, dim); } } // ============================================================================ // Input // ============================================================================ // Returns true when the click was consumed. bool desktop_wifi_handle_mouse(DesktopState* ds, int mx, int my, bool left_pressed, int scroll) { if (!ds->wifi_popup_open) return false; Rect popup = wifi_popup_rect(ds); if (scroll != 0 && popup.contains(mx, my)) { int max_scroll = ds->wifi_network_count - WIFI_VISIBLE; if (max_scroll < 0) max_scroll = 0; ds->wifi_scroll -= scroll; if (ds->wifi_scroll < 0) ds->wifi_scroll = 0; if (ds->wifi_scroll > max_scroll) ds->wifi_scroll = max_scroll; return true; } if (!left_pressed) return false; if (!popup.contains(mx, my)) { if (!ds->wifi_icon_rect.contains(mx, my)) ds->wifi_popup_open = false; return false; } if (wifi_scan_button(ds).contains(mx, my)) { if (!ds->wifi_scanning) wifi_start_scan(ds); return true; } if (wifi_action_button(ds).contains(mx, my)) { if (ds->wifi_info.connected) { montauk::wifi_disconnect(); ds->wifi_joining = false; ds->wifi_dhcp_pending = false; ds->wifi_dhcp_waiting = false; wifi_set_status(ds, "Disconnected"); } else if (!ds->wifi_scanning) { wifi_start_scan(ds); } return true; } int list_y = wifi_list_y(ds); if (my >= list_y && my < list_y + wifi_visible_rows(ds) * WIFI_ROW_H) { int row = (my - list_y) / WIFI_ROW_H; wifi_select_network(ds, ds->wifi_scroll + row); return true; } return true; // a click inside the popup never falls through } // ============================================================================ // Passphrase dialog // // A real desktop window, created and driven exactly like the reboot and // shutdown dialogs in dialogs.cpp: desktop_create_window() plus the four // callbacks. It gets a title bar, dragging, a close button and a place in the // window list for free, and the compositor draws it in the window layer rather // than in the panel overlay. // ============================================================================ static constexpr int PWD_W = 400; static constexpr int PWD_PAD = 20; static constexpr int PWD_FIELD_H = 32; static constexpr int PWD_BTN_W = 96; static constexpr int PWD_BTN_H = 32; // The vertical rhythm, in one place, so the window height computed at open // time and the rects drawn into it cannot drift apart. static constexpr int PWD_TOP = 18; // content top to the heading static constexpr int PWD_HEAD_GAP = 6; // heading to the network line static constexpr int PWD_FIELD_GAP = 22; // network line to the field label static constexpr int PWD_CHECK_GAP = 20; // field to the first checkbox static constexpr int PWD_ROW_GAP = 6; // between the checkboxes static constexpr int PWD_BTN_GAP = 30; // last checkbox to the buttons struct WifiPasswordDialog { DesktopState* ds; char ssid[DesktopState::WIFI_SSID_CAP]; char password[DesktopState::WIFI_PSK_CAP]; uint8_t security; bool reveal; bool remember; mtk::TextInputState input; }; static mtk::Theme wifi_dialog_theme(const DesktopState* ds) { return mtk::make_theme(ds->settings.accent_color); } // Content-relative layout. Everything above the buttons is measured down from // the top and everything below them up from the bottom, so the two meet in the // middle wherever the window is sized. static int pwd_row_h() { return system_font_height() + 10; // a checkbox row } static int pwd_label_y() { int fh = system_font_height(); return PWD_TOP + fh + PWD_HEAD_GAP + fh + PWD_FIELD_GAP; } static Rect pwd_field_rect(int cw, const mtk::Theme& theme) { return mtk::labeled_text_input_rect(PWD_PAD, pwd_label_y(), cw - PWD_PAD * 2, theme, PWD_FIELD_H); } static Rect pwd_show_rect(int cw, const mtk::Theme& theme) { Rect field = pwd_field_rect(cw, theme); return {PWD_PAD, field.y + field.h + PWD_CHECK_GAP, 190, pwd_row_h()}; } static Rect pwd_remember_rect(int cw, const mtk::Theme& theme) { Rect show = pwd_show_rect(cw, theme); return {show.x, show.y + show.h + PWD_ROW_GAP, 240, show.h}; } // The height the content needs for all of that plus the button row. static int pwd_content_h(const mtk::Theme& theme) { Rect remember = pwd_remember_rect(PWD_W, theme); return remember.y + remember.h + PWD_BTN_GAP + PWD_BTN_H + PWD_PAD; } static Rect pwd_join_rect(const Canvas& c) { return {c.w - PWD_PAD - PWD_BTN_W, c.h - PWD_PAD - PWD_BTN_H, PWD_BTN_W, PWD_BTN_H}; } static Rect pwd_cancel_rect(const Canvas& c) { Rect join = pwd_join_rect(c); return {join.x - 12 - PWD_BTN_W, join.y, PWD_BTN_W, PWD_BTN_H}; } static void wifi_dialog_close(WifiPasswordDialog* pd) { if (!pd) return; for (int i = 0; i < pd->ds->window_count; i++) { if (pd->ds->windows[i].app_data == pd) { desktop_close_window(pd->ds, i); return; } } } static void wifi_dialog_submit(WifiPasswordDialog* pd) { if (!pd->password[0]) return; // Join either way; a passphrase that cannot be written down still works // for this session. bool save_failed = false; if (pd->remember) { montauk::wifi::saved_set(&g_saved, pd->ssid, pd->password); save_failed = montauk::wifi::saved_store(&g_saved) != 0; } wifi_begin_join(pd->ds, pd->ssid, pd->password); if (save_failed) wifi_set_status(pd->ds, "Connected, but the password could not be saved"); wifi_dialog_close(pd); } static void wifi_dialog_on_draw(Window* win, Framebuffer& fb) { (void)fb; auto* pd = (WifiPasswordDialog*)win->app_data; if (!pd) return; Canvas c(win); mtk::Theme theme = wifi_dialog_theme(pd->ds); c.fill(theme.window_bg); int fh = system_font_height(); int mx = pd->ds->mouse.x - win->content_rect().x; int my = pd->ds->mouse.y - win->content_rect().y; char line[96]; snprintf(line, sizeof(line), "Enter the password for %s", pd->ssid); while (line[0] && text_width(line) > c.w - PWD_PAD * 2) { int n = montauk::slen(line); line[n - 1] = '\0'; } c.text(PWD_PAD, PWD_TOP, line, theme.text); char sub[64]; snprintf(sub, sizeof(sub), "%s network", montauk::wifi::security_name(pd->security)); c.text(PWD_PAD, PWD_TOP + fh + PWD_HEAD_GAP, sub, theme.text_subtle); Rect field = pwd_field_rect(c.w, theme); mtk::draw_labeled_text_field(c, PWD_PAD, field.y - fh - theme.gap_xs, c.w - PWD_PAD * 2, "Password", pd->password, pd->input.cursor, true, !pd->reveal, theme, PWD_FIELD_H, pd->input.selection_anchor); Rect show = pwd_show_rect(c.w, theme); mtk::draw_checkbox(c, show, "Show password", mtk::check_state(pd->reveal), theme, true, show.contains(mx, my)); Rect remember = pwd_remember_rect(c.w, theme); mtk::draw_checkbox(c, remember, "Remember this network", mtk::check_state(pd->remember), theme, true, remember.contains(mx, my)); Rect cancel = pwd_cancel_rect(c); Rect join = pwd_join_rect(c); mtk::draw_button(c, cancel, "Cancel", mtk::BUTTON_SECONDARY, mtk::widget_state(false, cancel.contains(mx, my), true), theme); mtk::draw_button(c, join, "Join", mtk::BUTTON_PRIMARY, mtk::widget_state(false, join.contains(mx, my), pd->password[0] != '\0'), theme); } static void wifi_dialog_on_mouse(Window* win, MouseEvent& ev) { auto* pd = (WifiPasswordDialog*)win->app_data; if (!pd) return; Canvas c(win); mtk::Theme theme = wifi_dialog_theme(pd->ds); Rect cr = win->content_rect(); int mx = ev.x - cr.x; int my = ev.y - cr.y; // The field owns the pointer while a drag or its context menu is running. Rect field = pwd_field_rect(c.w, theme); if (pd->input.context.open || pd->input.dragging || field.contains(mx, my)) { mtk::text_input_handle_mouse(pd->input, field, pd->password, (int)sizeof(pd->password), mx, my, ev.buttons, ev.prev_buttons, c.w, c.h, true, !pd->reveal, nullptr); return; } if (!ev.left_pressed()) return; if (pwd_show_rect(c.w, theme).contains(mx, my)) { pd->reveal = !pd->reveal; return; } if (pwd_remember_rect(c.w, theme).contains(mx, my)) { pd->remember = !pd->remember; return; } if (pwd_cancel_rect(c).contains(mx, my)) { wifi_dialog_close(pd); return; } if (pwd_join_rect(c).contains(mx, my)) { wifi_dialog_submit(pd); return; } } static void wifi_dialog_on_key(Window* win, const montauk::abi::KeyEvent& key) { auto* pd = (WifiPasswordDialog*)win->app_data; if (!pd || !key.pressed) return; if (key.scancode == 0x01) { // Escape wifi_dialog_close(pd); return; } if (key.ascii == '\n' || key.ascii == '\r') { wifi_dialog_submit(pd); return; } mtk::text_input_key(pd->input, pd->password, (int)sizeof(pd->password), key, nullptr); } static void wifi_dialog_on_close(Window* win) { if (!win->app_data) return; // Do not leave the passphrase behind in freed memory. auto* pd = (WifiPasswordDialog*)win->app_data; montauk::memset(pd, 0, sizeof(*pd)); montauk::mfree(win->app_data); win->app_data = nullptr; } static void wifi_open_password_dialog(DesktopState* ds, const montauk::abi::WifiNetwork& net) { // One dialog at a time: asking twice for the same key helps nobody. for (int i = 0; i < ds->window_count; i++) { if (ds->windows[i].on_close == wifi_dialog_on_close && ds->windows[i].state != WIN_CLOSED) { desktop_raise_window(ds, i); return; } } char title[MAX_TITLE_LEN]; snprintf(title, sizeof(title), "Join %s", net.ssid); // Height comes from the layout above rather than a constant: at the shipped // UI size a fixed 250px window put the checkboxes under the buttons. int pwd_h = pwd_content_h(wifi_dialog_theme(ds)) + TITLEBAR_HEIGHT + BORDER_WIDTH; int wx = (ds->screen_w - PWD_W) / 2; int wy = (ds->screen_h - pwd_h) / 2; if (wy < PANEL_HEIGHT + 8) wy = PANEL_HEIGHT + 8; int idx = desktop_create_window(ds, title, wx, wy, PWD_W, pwd_h); if (idx < 0) return; auto* pd = (WifiPasswordDialog*)montauk::malloc(sizeof(WifiPasswordDialog)); if (!pd) { desktop_close_window(ds, idx); return; } montauk::memset(pd, 0, sizeof(*pd)); pd->ds = ds; montauk::strncpy(pd->ssid, net.ssid, sizeof(pd->ssid)); pd->security = net.security; pd->remember = true; mtk::text_input_reset(pd->input, 0); Window* win = &ds->windows[idx]; win->app_data = pd; win->on_draw = wifi_dialog_on_draw; win->on_mouse = wifi_dialog_on_mouse; win->on_key = wifi_dialog_on_key; win->on_close = wifi_dialog_on_close; // The popup has done its job; the dialog is where the interaction is now. ds->wifi_popup_open = false; }