448 lines
16 KiB
C++
448 lines
16 KiB
C++
/*
|
|
* main.cpp
|
|
* MontaukOS Device Explorer — entry point, event loop, state
|
|
* Copyright (c) 2026 Daniel Hammer
|
|
*/
|
|
|
|
#include "devexplorer.h"
|
|
#include <gui/standalone.hpp>
|
|
|
|
// ============================================================================
|
|
// 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);
|
|
}
|