/* * panel.cpp * Panel bar, app menu, and network popup drawing * Copyright (c) 2026 Daniel Hammer */ #include "desktop_internal.hpp" #include void gui::desktop_draw_panel(DesktopState* ds) { Framebuffer& fb = ds->fb; int sw = ds->screen_w; // Panel gradient background (slightly lighter at top). // Saturating add so light panel colors don't wrap around to dark via uint8_t overflow. Color pc = ds->settings.panel_color; for (int y = 0; y < PANEL_HEIGHT; y++) { int t = y * 255 / PANEL_HEIGHT; int delta = 10 - t * 10 / 255; int r = (int)pc.r + delta; if (r > 255) r = 255; int g = (int)pc.g + delta; if (g > 255) g = 255; int b = (int)pc.b + delta; if (b > 255) b = 255; fb.fill_rect(0, y, sw, 1, Color::from_rgb((uint8_t)r, (uint8_t)g, (uint8_t)b)); } // Bottom highlight line (skip when wallpaper is set — the white bleeds through) if (!ds->settings.bg_image) { fb.fill_rect(0, PANEL_HEIGHT - 1, sw, 1, Color::from_rgba(0xFF, 0xFF, 0xFF, 0x10)); } // App menu button (left side) int btn_x = 4; int btn_y = 2; int btn_w = 28; int btn_h = 28; if (ds->icon_appmenu.pixels) { int ix = btn_x + (btn_w - ds->icon_appmenu.width) / 2; int iy = btn_y + (btn_h - ds->icon_appmenu.height) / 2; fb.blit_alpha(ix, iy, ds->icon_appmenu.width, ds->icon_appmenu.height, ds->icon_appmenu.pixels); } else { for (int gr = 0; gr < 3; gr++) { for (int gc = 0; gc < 3; gc++) { int dx = btn_x + 6 + gc * 6; int dy = btn_y + 6 + gr * 6; fb.fill_rect(dx, dy, 3, 3, colors::PANEL_TEXT); } } } // Window indicator pills (center area) int indicator_x = 40; for (int i = 0; i < ds->window_count; i++) { Window* win = &ds->windows[i]; if (win->state == WIN_CLOSED) continue; int tw = text_width(win->title); int pad = 12; int iw = tw + pad * 2; if (iw > 150) iw = 150; // Pre-blended indicator pill colors (opaque, blended against PANEL_BG) Color pill_bg = (i == ds->focused_window) ? colors::PANEL_INDICATOR_ACTIVE : colors::PANEL_INDICATOR_INACTIVE; fill_rounded_rect(fb, indicator_x, 4, iw, 24, 6, pill_bg); // Active window accent underline bar if (i == ds->focused_window) { fb.fill_rect(indicator_x + 4, 26, iw - 8, 2, ds->settings.accent_color); } // Truncate title if too long char short_title[20]; montauk::strncpy(short_title, win->title, 18); int tx = indicator_x + pad; int ty = 4 + (24 - system_font_height()) / 2; draw_text(fb, tx, ty, short_title, colors::PANEL_TEXT); indicator_x += iw + 4; } // Date + Clock (right side) montauk::abi::DateTime dt; montauk::gettime(&dt); char clock_str[12]; if (ds->settings.clock_24h) { snprintf(clock_str, sizeof(clock_str), "%02d:%02d", (int)dt.Hour, (int)dt.Minute); } else { int h12 = (int)dt.Hour % 12; if (h12 == 0) h12 = 12; const char* ampm = dt.Hour < 12 ? "AM" : "PM"; snprintf(clock_str, sizeof(clock_str), "%d:%02d %s", h12, (int)dt.Minute, ampm); } int clock_w = text_width(clock_str); int clock_x = sw - clock_w - 12; int clock_y = (PANEL_HEIGHT - system_font_height()) / 2; draw_text(fb, clock_x, clock_y, clock_str, colors::PANEL_TEXT); // Date before clock char date_str[12]; int month_idx = dt.Month > 0 && dt.Month <= 12 ? dt.Month - 1 : 0; snprintf(date_str, sizeof(date_str), "%s %d", month_names[month_idx], (int)dt.Day); int date_w = text_width(date_str); int date_x = clock_x - date_w - 10; draw_text(fb, date_x, clock_y, date_str, colors::PANEL_TEXT); // Volume icon (to the left of the date) int vol_icon_x = date_x - 16 - 12; int vol_icon_y = (PANEL_HEIGHT - 16) / 2; ds->vol_icon_rect = {vol_icon_x, vol_icon_y, 16, 16}; if (ds->icon_volume.pixels) { if (ds->vol_muted) { // Tint red-ish when muted uint32_t* src = ds->icon_volume.pixels; int npx = 16 * 16; uint32_t tinted[256]; for (int p = 0; p < npx; p++) { uint32_t px = src[p]; uint8_t a = (px >> 24) & 0xFF; tinted[p] = ((uint32_t)a << 24) | 0x00CC3333; } fb.blit_alpha(vol_icon_x, vol_icon_y, 16, 16, tinted); } else if (ds->vol_output == montauk::abi::AUDIO_OUTPUT_BLUETOOTH) { // The speaker glyph remains recognizable while the accent tint // communicates that audio is routed wirelessly. uint32_t* src = ds->icon_volume.pixels; uint32_t tinted[256]; uint32_t rgb = ds->settings.accent_color.to_pixel() & 0x00FFFFFF; for (int p = 0; p < 16 * 16; p++) { uint8_t a = (src[p] >> 24) & 0xFF; tinted[p] = ((uint32_t)a << 24) | rgb; } fb.blit_alpha(vol_icon_x, vol_icon_y, 16, 16, tinted); } else { fb.blit_alpha(vol_icon_x, vol_icon_y, ds->icon_volume.width, ds->icon_volume.height, ds->icon_volume.pixels); } } // Status icons fill in leftwards from the volume icon. Ethernet only // appears when a wired interface is registered, Wi-Fi only when a // supported adapter is present, so a machine without either shows neither. int icon_y = (PANEL_HEIGHT - 16) / 2; int next_icon_x = vol_icon_x - 16 - 10; if (ds->eth_present) { ds->net_icon_rect = {next_icon_x, icon_y, 16, 16}; next_icon_x -= 16 + 10; if (ds->icon_network.pixels) { // Tinted while the cable is doing nothing for us: no link, or a // link that is not the one carrying traffic. if (!desktop_eth_online(ds)) { uint32_t* src = ds->icon_network.pixels; uint32_t tinted[256]; for (int p = 0; p < 16 * 16; p++) { uint8_t a = (src[p] >> 24) & 0xFF; tinted[p] = ((uint32_t)a << 24) | 0x004444CC; } fb.blit_alpha(ds->net_icon_rect.x, icon_y, 16, 16, tinted); } else { fb.blit_alpha(ds->net_icon_rect.x, icon_y, ds->icon_network.width, ds->icon_network.height, ds->icon_network.pixels); } } } else { ds->net_icon_rect = {0, 0, 0, 0}; } // Wi-Fi stays white whatever the radio is doing: the popup carries the // state, and a colour-shifting icon next to the clock is just noise. if (ds->wifi_present) { ds->wifi_icon_rect = {next_icon_x, icon_y, 16, 16}; next_icon_x -= 16 + 10; if (ds->icon_wifi.pixels) { fb.blit_alpha(ds->wifi_icon_rect.x, icon_y, ds->icon_wifi.width, ds->icon_wifi.height, ds->icon_wifi.pixels); } } else { ds->wifi_icon_rect = {0, 0, 0, 0}; } } // ============================================================================ // App Menu // ============================================================================ void desktop_draw_app_menu(DesktopState* ds) { Framebuffer& fb = ds->fb; int menu_x = 4; int menu_y = PANEL_HEIGHT + 2; int menu_h = menu_total_height(); // Menu shadow draw_shadow(fb, menu_x, menu_y, MENU_W, menu_h, 4, colors::SHADOW); // Menu background with rounded corners fill_rounded_rect(fb, menu_x, menu_y, MENU_W, menu_h, 8, colors::MENU_BG); draw_rect(fb, menu_x, menu_y, MENU_W, menu_h, colors::BORDER); int mx = ds->mouse.x; int my = ds->mouse.y; int iy = menu_y + 5; int cur_cat = -1; for (int i = 0; i < menu_row_count; i++) { const MenuRow& row = menu_rows[i]; if (row.is_category) cur_cat++; if (!menu_row_visible(i)) continue; int row_h = menu_row_height(row); if (row.is_category) { // Separator line above (except first category) if (i > 0) { for (int sx = menu_x + 8; sx < menu_x + MENU_W - 8; sx++) fb.put_pixel(sx, iy + 1, colors::BORDER); } // Category header with hover + expand/collapse indicator if (row.label[0]) { Rect cat_rect = {menu_x + 4, iy, MENU_W - 8, row_h}; bool hovered = cat_rect.contains(mx, my); if (hovered) fill_rounded_rect(fb, cat_rect.x, cat_rect.y, cat_rect.w, cat_rect.h, 4, Color::from_rgb(0xE8, 0xE8, 0xE8)); // Arrow indicator: > for collapsed, v for expanded bool expanded = (cur_cat >= 0 && cur_cat < MENU_NUM_CATS) && menu_cat_expanded[cur_cat]; int ax = menu_x + 10; int ay = iy + row_h / 2; Color arrow_color = Color::from_rgb(0x88, 0x88, 0x88); if (expanded) { // Down arrow (v shape) for (int d = 0; d < 4; d++) { fb.put_pixel(ax + d, ay - 2 + d, arrow_color); fb.put_pixel(ax + d + 1, ay - 2 + d, arrow_color); fb.put_pixel(ax + 7 - d, ay - 2 + d, arrow_color); fb.put_pixel(ax + 6 - d, ay - 2 + d, arrow_color); } } else { // Right arrow (> shape) for (int d = 0; d < 4; d++) { fb.put_pixel(ax + d, ay - 3 + d, arrow_color); fb.put_pixel(ax + d, ay - 3 + d + 1, arrow_color); fb.put_pixel(ax + d, ay + 3 - d, arrow_color); fb.put_pixel(ax + d, ay + 3 - d - 1, arrow_color); } } int tx = menu_x + 24; int ty = iy + (row_h - system_font_height()) / 2; draw_text(fb, tx, ty, row.label, Color::from_rgb(0x66, 0x66, 0x66)); } } else { Rect item_rect = {menu_x + 4, iy, MENU_W - 8, row_h}; // Hover highlight if (item_rect.contains(mx, my)) { fill_rounded_rect(fb, item_rect.x, item_rect.y, item_rect.w, item_rect.h, 4, gui::mtk::accent_hover_tint(ds->settings.accent_color)); } // Icon int icon_x = item_rect.x + 8; int icon_y = item_rect.y + (row_h - 20) / 2; if (row.icon && row.icon->pixels) { fb.blit_alpha(icon_x, icon_y, row.icon->width, row.icon->height, row.icon->pixels); } // Label int tx = icon_x + 28; int ty = item_rect.y + (row_h - system_font_height()) / 2; draw_text(fb, tx, ty, row.label, colors::TEXT_COLOR); } iy += row_h; } } // ============================================================================ // Network Popup // ============================================================================ // The wired interface, if one is registered. const montauk::abi::NetIfInfo* desktop_eth_iface(const DesktopState* ds) { for (int i = 0; i < ds->netif_count; i++) { if (ds->netifs[i].kind == montauk::abi::NETIF_KIND_ETHERNET) return &ds->netifs[i]; } return nullptr; } // "Online" for the panel icon means the cable is both up and the interface the // stack is actually sending through, with an address to send from. bool desktop_eth_online(const DesktopState* ds) { const montauk::abi::NetIfInfo* eth = desktop_eth_iface(ds); return eth && eth->linkUp && eth->active && ds->cached_net_cfg.ipAddress != 0; } void desktop_draw_net_popup(DesktopState* ds) { Framebuffer& fb = ds->fb; montauk::abi::NetCfg& nc = ds->cached_net_cfg; const montauk::abi::NetIfInfo* eth = desktop_eth_iface(ds); bool link_up = eth && eth->linkUp; // The IP configuration is global to the stack, so it is only this // interface's address while this interface is the active one. bool owns_address = eth && eth->active && nc.ipAddress != 0; bool connected = link_up && owns_address; int popup_w = 220; int fh = system_font_height(); int row_h = fh + 8; int header_h = row_h + 8; // title + status row + padding int body_rows = 6; // Interface, IP, Subnet, Gateway, DNS, MAC int popup_h = header_h + row_h * body_rows + 12; int popup_x = ds->net_icon_rect.x + ds->net_icon_rect.w - popup_w; int popup_y = PANEL_HEIGHT + 2; if (popup_x < 4) popup_x = 4; 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: "Ethernet" + status dot draw_text(fb, lx, ty, "Ethernet", colors::TEXT_COLOR); // Status dot + label (right-aligned in header). A cable that is plugged // in but idle (Wi-Fi is carrying the traffic) is not the same as unplugged. Color dot_color = connected ? Color::from_rgb(0x4C, 0xAF, 0x50) // green : (link_up ? Color::from_rgb(0xD0, 0x9A, 0x2E) // amber : Color::from_rgb(0xCC, 0x33, 0x33)); // red const char* status_str = connected ? "Connected" : (link_up ? "Link up" : "Disconnected"); int sw = text_width(status_str); int dot_r = 4; int status_x = popup_x + popup_w - 14 - sw; int dot_x = status_x - dot_r * 2 - 5; int dot_cy = ty + fh / 2; for (int dy = -dot_r; dy <= dot_r; dy++) for (int dx = -dot_r; dx <= dot_r; dx++) if (dx * dx + dy * dy <= dot_r * dot_r) fb.put_pixel(dot_x + dot_r + dx, dot_cy + dy, dot_color); Color dim = Color::from_rgb(0x66, 0x66, 0x66); draw_text(fb, status_x, ty, status_str, dim); ty += row_h + 2; // Separator line for (int sx = popup_x + 10; sx < popup_x + popup_w - 10; sx++) fb.put_pixel(sx, ty, colors::BORDER); ty += 6; // Body rows: label (dim) + value (dark), two-column int val_x = popup_x + 76; // fixed column for values struct NetRow { const char* label; char value[24]; }; NetRow rows[6]; rows[0].label = "Interface"; montauk::strncpy(rows[0].value, eth ? eth->name : "None", sizeof(rows[0].value)); // Addresses are shown only when this interface owns them; otherwise the // wireless popup is where they belong. rows[1].label = "IP"; if (owns_address) format_ip(rows[1].value, nc.ipAddress); else montauk::strcpy(rows[1].value, link_up ? "Not in use" : "0.0.0.0"); rows[2].label = "Subnet"; if (owns_address) format_ip(rows[2].value, nc.subnetMask); else montauk::strcpy(rows[2].value, "-"); rows[3].label = "Gateway"; if (owns_address) format_ip(rows[3].value, nc.gateway); else montauk::strcpy(rows[3].value, "-"); rows[4].label = "DNS"; if (owns_address) format_ip(rows[4].value, nc.dnsServer); else montauk::strcpy(rows[4].value, "-"); rows[5].label = "MAC"; format_mac(rows[5].value, eth ? eth->mac : nc.macAddress); for (int i = 0; i < body_rows; i++) { draw_text(fb, lx, ty, rows[i].label, dim); draw_text(fb, val_x, ty, rows[i].value, colors::TEXT_COLOR); ty += row_h; } } // ============================================================================ // Volume Popup // ============================================================================ static constexpr int VOL_POPUP_W = 200; static constexpr int VOL_POPUP_H = 202; static constexpr int VOL_SLIDER_X = 16; static constexpr int VOL_SLIDER_W = VOL_POPUP_W - 32; static constexpr int VOL_SLIDER_H = 8; static constexpr int VOL_KNOB_R = 8; void desktop_draw_vol_popup(DesktopState* ds) { Framebuffer& fb = ds->fb; int popup_x = ds->vol_icon_rect.x + ds->vol_icon_rect.w - VOL_POPUP_W; int popup_y = PANEL_HEIGHT + 2; if (popup_x < 4) popup_x = 4; draw_shadow(fb, popup_x, popup_y, VOL_POPUP_W, VOL_POPUP_H, 4, colors::SHADOW); fill_rounded_rect(fb, popup_x, popup_y, VOL_POPUP_W, VOL_POPUP_H, 8, colors::MENU_BG); draw_rect(fb, popup_x, popup_y, VOL_POPUP_W, VOL_POPUP_H, colors::BORDER); int display_vol = ds->vol_muted ? 0 : ds->vol_level; // Volume percentage label char vol_str[8]; snprintf(vol_str, sizeof(vol_str), "%d%%", display_vol); int vw = text_width(vol_str); Color vol_color = ds->vol_muted ? Color::from_rgb(0xCC, 0x33, 0x33) : ds->settings.accent_color; draw_text(fb, popup_x + (VOL_POPUP_W - vw) / 2, popup_y + 12, vol_str, vol_color); // "Muted" sub-label if (ds->vol_muted) { const char* ml = "Muted"; int mw = text_width(ml); draw_text(fb, popup_x + (VOL_POPUP_W - mw) / 2, popup_y + 12 + system_font_height() + 2, ml, Color::from_rgb(0xCC, 0x33, 0x33)); } // Slider track int slider_abs_x = popup_x + VOL_SLIDER_X; int slider_abs_y = popup_y + 56; fill_rounded_rect(fb, slider_abs_x, slider_abs_y, VOL_SLIDER_W, VOL_SLIDER_H, 4, Color::from_rgb(0xDD, 0xDD, 0xDD)); // Filled portion int fill_w = (display_vol * VOL_SLIDER_W) / 100; if (fill_w > 0) fill_rounded_rect(fb, slider_abs_x, slider_abs_y, fill_w, VOL_SLIDER_H, 4, ds->settings.accent_color); // Knob int knob_cx = slider_abs_x + fill_w; int knob_cy = slider_abs_y + VOL_SLIDER_H / 2; // Draw filled circle for knob for (int dy = -VOL_KNOB_R; dy <= VOL_KNOB_R; dy++) { for (int dx = -VOL_KNOB_R; dx <= VOL_KNOB_R; dx++) { if (dx * dx + dy * dy <= VOL_KNOB_R * VOL_KNOB_R) { int px = knob_cx + dx; int py = knob_cy + dy; if (px >= 0 && px < ds->screen_w && py >= 0 && py < ds->screen_h) fb.put_pixel(px, py, ds->settings.accent_color); } } } // White center int inner_r = VOL_KNOB_R - 3; for (int dy = -inner_r; dy <= inner_r; dy++) { for (int dx = -inner_r; dx <= inner_r; dx++) { if (dx * dx + dy * dy <= inner_r * inner_r) { int px = knob_cx + dx; int py = knob_cy + dy; if (px >= 0 && px < ds->screen_w && py >= 0 && py < ds->screen_h) fb.put_pixel(px, py, Color::from_rgb(0xFF, 0xFF, 0xFF)); } } } // Buttons: [-] [+] [Mute] int btn_h = 24; int btn_y = popup_y + 78; int btn_rad = 6; int minus_w = 36; int plus_w = 36; int mute_w = 50; int gap = 8; int total_w = minus_w + plus_w + mute_w + gap * 2; int bx = popup_x + (VOL_POPUP_W - total_w) / 2; int mmx = ds->mouse.x; int mmy = ds->mouse.y; // [-] button Color minus_bg = Color::from_rgb(0xE0, 0xE0, 0xE0); Rect minus_r = {bx, btn_y, minus_w, btn_h}; if (minus_r.contains(mmx, mmy)) minus_bg = Color::from_rgb(0xD0, 0xD0, 0xD0); fill_rounded_rect(fb, bx, btn_y, minus_w, btn_h, btn_rad, minus_bg); int tw = text_width("-"); draw_text(fb, bx + (minus_w - tw) / 2, btn_y + (btn_h - system_font_height()) / 2, "-", colors::TEXT_COLOR); bx += minus_w + gap; // [+] button Color plus_bg = Color::from_rgb(0xE0, 0xE0, 0xE0); Rect plus_r = {bx, btn_y, plus_w, btn_h}; if (plus_r.contains(mmx, mmy)) plus_bg = Color::from_rgb(0xD0, 0xD0, 0xD0); fill_rounded_rect(fb, bx, btn_y, plus_w, btn_h, btn_rad, plus_bg); tw = text_width("+"); draw_text(fb, bx + (plus_w - tw) / 2, btn_y + (btn_h - system_font_height()) / 2, "+", colors::TEXT_COLOR); bx += plus_w + gap; // [Mute] button Color mute_bg = ds->vol_muted ? Color::from_rgb(0xCC, 0x33, 0x33) : Color::from_rgb(0xE0, 0xE0, 0xE0); Color mute_fg = ds->vol_muted ? Color::from_rgb(0xFF, 0xFF, 0xFF) : colors::TEXT_COLOR; Rect mute_r = {bx, btn_y, mute_w, btn_h}; if (mute_r.contains(mmx, mmy)) { if (ds->vol_muted) mute_bg = Color::from_rgb(0xAA, 0x22, 0x22); else mute_bg = Color::from_rgb(0xD0, 0xD0, 0xD0); } fill_rounded_rect(fb, bx, btn_y, mute_w, btn_h, btn_rad, mute_bg); tw = text_width("Mute"); draw_text(fb, bx + (mute_w - tw) / 2, btn_y + (btn_h - system_font_height()) / 2, "Mute", mute_fg); // Output is a persistent selection, so present it as a conventional radio // group rather than a second bank of action buttons. int output_label_y = popup_y + 116; draw_text(fb, popup_x + 16, output_label_y, "Output", Color::from_rgb(0x77, 0x77, 0x77)); auto draw_device_row = [&](const Rect& row, const char* label, bool selected, bool enabled) { bool hovered = enabled && row.contains(ds->mouse.x, ds->mouse.y); if (hovered) fill_rounded_rect(fb, row.x, row.y, row.w, row.h, 5, Color::from_rgb(0xF0, 0xF3, 0xF6)); int radio_size = 14; int rx = row.x + 4; int ry = row.y + (row.h - radio_size) / 2; fill_rounded_rect(fb, rx, ry, radio_size, radio_size, radio_size / 2, Color::from_rgb(0xAA, 0xAA, 0xAA)); fill_rounded_rect(fb, rx + 1, ry + 1, radio_size - 2, radio_size - 2, (radio_size - 2) / 2, colors::WHITE); if (selected) fill_rounded_rect(fb, rx + 4, ry + 4, radio_size - 8, radio_size - 8, (radio_size - 8) / 2, ds->settings.accent_color); Color fg = enabled ? colors::TEXT_COLOR : Color::from_rgb(0x99, 0x99, 0x99); draw_text(fb, rx + radio_size + 8, row.y + (row.h - system_font_height()) / 2, label, fg); }; Rect hda_r = {popup_x + 12, popup_y + 137, VOL_POPUP_W - 24, 25}; Rect bt_r = {hda_r.x, hda_r.y + hda_r.h, hda_r.w, hda_r.h}; bool bt_ready = ds->vol_bt_status >= 2; draw_device_row(hda_r, "Built-in Speakers", ds->vol_output == montauk::abi::AUDIO_OUTPUT_HDA, true); draw_device_row(bt_r, bt_ready ? "Bluetooth" : "Bluetooth (offline)", ds->vol_output == montauk::abi::AUDIO_OUTPUT_BLUETOOTH, bt_ready); }