/* * main.cpp * MontaukOS Device Explorer — entry point, event loop, state * Copyright (c) 2026 Daniel Hammer */ #include "devexplorer.h" #include // ============================================================================ // Global state definitions // ============================================================================ int g_win_w = INIT_W; int g_win_h = INIT_H; DevExplorerState g_state; TrueTypeFont* g_font = nullptr; mtk::Theme g_theme; static void refresh_devices(DevExplorerState* de) { if (de == nullptr) return; int kernel_count = montauk::devlist(de->devs, MAX_DEVS); if (kernel_count < 0) kernel_count = 0; if (kernel_count > MAX_DEVS) kernel_count = MAX_DEVS; int printer_count = append_printer_devices( de->devs + kernel_count, MAX_TOTAL_DEVS - kernel_count); if (printer_count < 0) printer_count = 0; de->dev_count = kernel_count + printer_count; de->last_poll_ms = montauk::get_milliseconds(); } // ============================================================================ // Display row building // ============================================================================ int build_display_rows(DevExplorerState* de, DisplayRow* rows) { int count = 0; for (int cat = 0; cat < NUM_CATEGORIES; cat++) { int cat_count = 0; for (int d = 0; d < de->dev_count; d++) if (de->devs[d].category == cat) cat_count++; if (cat_count == 0) continue; rows[count].type = ROW_CATEGORY; rows[count].category = cat; rows[count].dev_index = -1; count++; if (!de->collapsed[cat]) { for (int d = 0; d < de->dev_count; d++) { if (de->devs[d].category == cat) { rows[count].type = ROW_DEVICE; rows[count].category = cat; rows[count].dev_index = d; count++; } } } } return count; } // ============================================================================ // Scroll metrics // ============================================================================ // Computes pixel-space scroll geometry and clamps de->scroll_y so the list can // never scroll past its content. Shared by render.cpp and the input handlers. ScrollMetrics compute_scroll_metrics(DevExplorerState* de, const DisplayRow* rows, int row_count) { ScrollMetrics m; m.list_y = TOOLBAR_H; m.list_h = g_win_h - m.list_y; if (m.list_h < 0) m.list_h = 0; int total_h = 0; for (int i = 0; i < row_count; i++) total_h += (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H; m.total_h = total_h; int max_scroll_px = total_h - m.list_h; if (max_scroll_px < 0) max_scroll_px = 0; m.max_scroll_px = max_scroll_px; // scroll_y is a pixel offset. Clamping to max_scroll_px (rather than a row // boundary) lets the final scroll position sit the last row flush against // the bottom edge, so a trailing category like Printers is never cut off. if (de->scroll_y < 0) de->scroll_y = 0; if (de->scroll_y > max_scroll_px) de->scroll_y = max_scroll_px; m.scroll_px = de->scroll_y; return m; } // Stores a target pixel offset (from the dragged thumb) directly. scroll_y is // pixel-space; compute_scroll_metrics clamps it to the content bounds. void set_scroll_from_px(DevExplorerState* de, const DisplayRow* rows, int row_count, int target_px) { (void)rows; (void)row_count; de->scroll_y = target_px < 0 ? 0 : target_px; } // ============================================================================ // Toolbar hit testing // ============================================================================ static bool handle_toolbar_click(int mx, int my) { if (my >= TOOLBAR_H) return false; if (refresh_button_rect().contains(mx, my)) { g_state.last_poll_ms = 0; // force refresh return true; } return false; } // ============================================================================ // List click handling // ============================================================================ static bool handle_list_click(int mx, int my) { auto& de = g_state; int list_y = TOOLBAR_H; if (my < list_y) return false; DisplayRow rows[MAX_DISPLAY_ROWS]; int row_count = build_display_rows(&de, rows); // Locate the first visible row for the current pixel scroll offset, mirroring // render_list() so click targets line up with what is drawn. The first row // may be partially scrolled above the viewport (negative top offset). int scroll_px = de.scroll_y; int first_visible = 0, row_top = 0; { int acc = 0; for (int i = 0; i < row_count; i++) { int rh = (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H; if (acc + rh > scroll_px) { first_visible = i; row_top = acc; break; } acc += rh; first_visible = i + 1; row_top = acc; } } uint64_t now = montauk::get_milliseconds(); int cur_y = list_y - (scroll_px - row_top); for (int i = first_visible; i < row_count; i++) { int row_h = (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H; if (my >= cur_y && my < cur_y + row_h) { if (rows[i].type == ROW_CATEGORY) { int cat = rows[i].category; de.collapsed[cat] = !de.collapsed[cat]; de.selected_row = -1; de.last_click_row = -1; } else { int di = rows[i].dev_index; bool is_double = (de.last_click_row == i) && (now - de.last_click_ms < 400); if (is_double && de.devs[di].category == CAT_STORAGE) { int port = (int)de.devs[di]._pad[0]; open_disk_detail(port, de.devs[di].name); de.last_click_row = -1; } else { de.selected_row = i; de.last_click_row = i; de.last_click_ms = now; } } return true; } cur_y += row_h; if (cur_y >= g_win_h) break; } de.selected_row = -1; de.last_click_row = -1; return true; } // ============================================================================ // Mouse handling (wheel, draggable scrollbar, clicks) // ============================================================================ static bool handle_main_mouse(const montauk::abi::WinEvent& ev) { auto& de = g_state; int mx = ev.mouse.x; int my = ev.mouse.y; bool changed = false; bool left_down = (ev.mouse.buttons & 1) != 0; bool just_clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); bool just_released = !(ev.mouse.buttons & 1) && (ev.mouse.prev_buttons & 1); de.mouse_x = mx; de.mouse_y = my; DisplayRow rows[MAX_DISPLAY_ROWS]; int row_count = build_display_rows(&de, rows); ScrollMetrics m = compute_scroll_metrics(&de, rows, row_count); Rect viewport = { 0, m.list_y, g_win_w, m.list_h }; Rect track = mtk::scrollbar_track_rect(viewport); Rect thumb = mtk::scrollbar_thumb_rect(track, m.total_h, m.list_h, m.scroll_px); // Mouse wheel scrolls by roughly one row per notch (scroll_y is pixels). if (ev.mouse.scroll != 0) { de.scroll_y += ev.mouse.scroll * ITEM_H; if (de.scroll_y < 0) de.scroll_y = 0; changed = true; } // Release ends any active drag. if (just_released && de.sb_dragging) { de.sb_dragging = false; changed = true; } // Active drag follows the pointer even if it leaves the track horizontally. if (de.sb_dragging && left_down && !track.empty() && !thumb.empty()) { int new_top = my - de.sb_drag_offset; int target_px = mtk::scrollbar_offset_from_thumb_top( track, m.total_h, m.list_h, thumb.h, new_top); set_scroll_from_px(&de, rows, row_count, target_px); changed = true; } // Hover highlight on the thumb; only redraw when it changes. The track is // only interactive when the list actually scrolls (thumb is present). bool scrollable = !track.empty() && !thumb.empty(); bool hover_now = scrollable && track.contains(mx, my); if (hover_now != de.sb_hover) { de.sb_hover = hover_now; changed = true; } // Hover highlight on the Refresh button. bool btn_hover_now = refresh_button_rect().contains(mx, my); if (btn_hover_now != de.btn_hover) { de.btn_hover = btn_hover_now; changed = true; } if (just_clicked) { if (scrollable && thumb.contains(mx, my)) { // Grab the thumb. de.sb_dragging = true; de.sb_drag_offset = my - thumb.y; changed = true; } else if (scrollable && track.contains(mx, my)) { // Click on the track: center the thumb under the pointer and drag. de.sb_dragging = true; de.sb_drag_offset = thumb.h / 2; int new_top = my - de.sb_drag_offset; int target_px = mtk::scrollbar_offset_from_thumb_top( track, m.total_h, m.list_h, thumb.h, new_top); set_scroll_from_px(&de, rows, row_count, target_px); changed = true; } else if (handle_toolbar_click(mx, my) || handle_list_click(mx, my)) { changed = true; } } return changed; } // ============================================================================ // Key handling // ============================================================================ static bool handle_key(const montauk::abi::KeyEvent& key) { if (!key.pressed) return false; auto& de = g_state; DisplayRow rows[MAX_DISPLAY_ROWS]; int row_count = build_display_rows(&de, rows); if (row_count == 0) return true; if (key.scancode == 0x48) { // Up if (de.selected_row <= 0) de.selected_row = 0; else de.selected_row--; // Scroll up just enough to bring the selected row's top into view. int sel_top = 0; for (int i = 0; i < de.selected_row && i < row_count; i++) sel_top += (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H; if (sel_top < de.scroll_y) de.scroll_y = sel_top; } else if (key.scancode == 0x50) { // Down if (de.selected_row < row_count - 1) de.selected_row++; // Scroll down just enough to bring the selected row's bottom into view. int sel_top = 0; for (int i = 0; i < de.selected_row && i < row_count; i++) sel_top += (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H; int sel_h = (rows[de.selected_row].type == ROW_CATEGORY) ? CAT_H : ITEM_H; int list_h = g_win_h - TOOLBAR_H; if (sel_top + sel_h > de.scroll_y + list_h) de.scroll_y = sel_top + sel_h - list_h; } else if (key.scancode == 0x4B) { // Left — collapse if (de.selected_row >= 0 && de.selected_row < row_count) { int cat = rows[de.selected_row].category; if (!de.collapsed[cat]) { de.collapsed[cat] = true; if (rows[de.selected_row].type == ROW_DEVICE) { for (int i = de.selected_row - 1; i >= 0; i--) { if (rows[i].type == ROW_CATEGORY && rows[i].category == cat) { de.selected_row = i; break; } } int new_count = build_display_rows(&de, rows); if (de.selected_row >= new_count) de.selected_row = new_count - 1; } } } } else if (key.scancode == 0x4D) { // Right — expand if (de.selected_row >= 0 && de.selected_row < row_count) { int cat = rows[de.selected_row].category; de.collapsed[cat] = false; } } else if (key.scancode == 0x1C) { // Enter — toggle category if (de.selected_row >= 0 && de.selected_row < row_count) { if (rows[de.selected_row].type == ROW_CATEGORY) { int cat = rows[de.selected_row].category; de.collapsed[cat] = !de.collapsed[cat]; } } } else if (key.scancode == 0x01) { // Escape return false; } return true; } // ============================================================================ // Entry point // ============================================================================ extern "C" void _start() { montauk::memset(&g_state, 0, sizeof(g_state)); g_state.selected_row = -1; g_state.last_click_row = -1; for (int i = 0; i < NUM_CATEGORIES; i++) g_state.collapsed[i] = false; // Load fonts. fonts::init() loads the shared system font (Roboto-Medium at // UI_SIZE 18) which both the device list (via g_font) and the MTK widgets // (button, scrollbar) render with, so point g_font at it to avoid a second // copy of the same face. gui::fonts::init(); g_font = gui::fonts::system_font; // Theme for MTK widgets, built once from the user's accent color. g_theme = mtk::make_theme(); // Initial device poll refresh_devices(&g_state); WsWindow win; if (!win.create("Devices", INIT_W, INIT_H)) montauk::exit(1); uint32_t* pixels = win.pixels; render(pixels); win.present(); while (true) { montauk::abi::WinEvent ev; bool redraw_main = false; bool redraw_detail = false; // Poll for device refresh uint64_t now = montauk::get_milliseconds(); if (now - g_state.last_poll_ms >= POLL_MS) { refresh_devices(&g_state); redraw_main = true; } // Poll disk detail window if (g_state.detail.open) { int dr = montauk::win_poll(g_state.detail.win_id, &ev); if (dr > 0) { if (ev.type == 3) { // close close_disk_detail(); redraw_main = true; } else if (ev.type == 1) { bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); if (handle_detail_mouse(ev.mouse.x, ev.mouse.y, clicked)) redraw_detail = true; } else if (ev.type == 2) { // resize g_state.detail.win_w = ev.resize.w; g_state.detail.win_h = ev.resize.h; g_state.detail.pixels = (uint32_t*)(uintptr_t)montauk::win_resize( g_state.detail.win_id, ev.resize.w, ev.resize.h); redraw_detail = true; } } } // Poll main window int r = win.poll(&ev); if (r < 0) break; if (r == 0) { if (redraw_main) { render(pixels); win.present(); } if (redraw_detail) { render_disk_detail(); montauk::win_present(g_state.detail.win_id); } if (!redraw_main && !redraw_detail) montauk::sleep_ms(16); continue; } if (ev.type == 3) break; // close // Resize if (ev.type == 2) { g_win_w = win.width; g_win_h = win.height; pixels = win.pixels; redraw_main = true; } // Keyboard if (ev.type == 0 && ev.key.pressed) { if (!handle_key(ev.key) && ev.key.scancode == 0x01) break; redraw_main = true; } // Mouse if (ev.type == 1) { if (handle_main_mouse(ev)) redraw_main = true; } if (redraw_main) { render(pixels); win.present(); } if (redraw_detail) { render_disk_detail(); montauk::win_present(g_state.detail.win_id); } } close_disk_detail(); win.destroy(); montauk::exit(0); }