feat: TrueType (TTF) font rendering, many new desktop applications and DOOM support, among other improvements
This commit is contained in:
@@ -0,0 +1,475 @@
|
||||
/*
|
||||
* app_devexplorer.cpp
|
||||
* ZenithOS Desktop - Device Explorer (lists hardware detected by the kernel)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "apps_common.hpp"
|
||||
|
||||
// ============================================================================
|
||||
// Device Explorer state
|
||||
// ============================================================================
|
||||
|
||||
static constexpr int DE_TOOLBAR_H = 36;
|
||||
static constexpr int DE_CAT_H = 28;
|
||||
static constexpr int DE_ITEM_H = 24;
|
||||
static constexpr int DE_MAX_DEVS = 64;
|
||||
static constexpr int DE_POLL_MS = 2000;
|
||||
static constexpr int DE_INDENT = 28;
|
||||
|
||||
// Category names matching DevInfo.category values
|
||||
static const char* category_names[] = {
|
||||
"CPU", // 0
|
||||
"Interrupt", // 1
|
||||
"Timer", // 2
|
||||
"Input", // 3
|
||||
"USB", // 4
|
||||
"Network", // 5
|
||||
"Display", // 6
|
||||
"PCI", // 7
|
||||
};
|
||||
static constexpr int NUM_CATEGORIES = 8;
|
||||
|
||||
static Color category_colors[] = {
|
||||
Color::from_rgb(0x33, 0x66, 0xCC), // CPU - blue
|
||||
Color::from_rgb(0x88, 0x44, 0xAA), // Interrupt - purple
|
||||
Color::from_rgb(0x22, 0x88, 0x22), // Timer - green
|
||||
Color::from_rgb(0xCC, 0x88, 0x00), // Input - amber
|
||||
Color::from_rgb(0x00, 0x88, 0x88), // USB - teal
|
||||
Color::from_rgb(0xCC, 0x55, 0x22), // Network - orange
|
||||
Color::from_rgb(0x44, 0x66, 0xCC), // Display - indigo
|
||||
Color::from_rgb(0x66, 0x66, 0x66), // PCI - gray
|
||||
};
|
||||
|
||||
struct DevExplorerState {
|
||||
DesktopState* desktop;
|
||||
Zenith::DevInfo devs[DE_MAX_DEVS];
|
||||
int dev_count;
|
||||
bool collapsed[NUM_CATEGORIES]; // per-category collapse state
|
||||
int selected_row; // index into visible display rows (-1 = none)
|
||||
int scroll_y; // scroll offset in display rows
|
||||
uint64_t last_poll_ms;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Display row model
|
||||
// ============================================================================
|
||||
|
||||
enum RowType { ROW_CATEGORY, ROW_DEVICE };
|
||||
|
||||
struct DisplayRow {
|
||||
RowType type;
|
||||
int category; // category index (0..7)
|
||||
int dev_index; // index into devs[] (only valid for ROW_DEVICE)
|
||||
};
|
||||
|
||||
static constexpr int MAX_DISPLAY_ROWS = DE_MAX_DEVS + NUM_CATEGORIES;
|
||||
|
||||
static int build_display_rows(DevExplorerState* de, DisplayRow* rows) {
|
||||
int count = 0;
|
||||
for (int cat = 0; cat < NUM_CATEGORIES; cat++) {
|
||||
// Count devices in this category
|
||||
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;
|
||||
|
||||
// Emit category header
|
||||
rows[count].type = ROW_CATEGORY;
|
||||
rows[count].category = cat;
|
||||
rows[count].dev_index = -1;
|
||||
count++;
|
||||
|
||||
// Emit device rows if expanded
|
||||
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;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Triangle drawing helpers
|
||||
// ============================================================================
|
||||
|
||||
static void draw_triangle_right(Canvas& c, int x, int y, int size, Color col) {
|
||||
// Right-pointing filled triangle (▶)
|
||||
int half = size / 2;
|
||||
for (int row = 0; row < size; row++) {
|
||||
int dist = (row <= half) ? row : (size - 1 - row);
|
||||
for (int col_px = 0; col_px <= dist; col_px++) {
|
||||
c.put_pixel(x + col_px, y + row, col);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_triangle_down(Canvas& c, int x, int y, int size, Color col) {
|
||||
// Down-pointing filled triangle (▼)
|
||||
int half = size / 2;
|
||||
for (int row = 0; row < half + 1; row++) {
|
||||
int w = size - row * 2;
|
||||
for (int col_px = 0; col_px < w; col_px++) {
|
||||
c.put_pixel(x + row + col_px, y + row, col);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Callbacks
|
||||
// ============================================================================
|
||||
|
||||
static void devexplorer_on_poll(Window* win) {
|
||||
DevExplorerState* de = (DevExplorerState*)win->app_data;
|
||||
if (!de) return;
|
||||
|
||||
uint64_t now = zenith::get_milliseconds();
|
||||
if (now - de->last_poll_ms < DE_POLL_MS) return;
|
||||
de->last_poll_ms = now;
|
||||
|
||||
de->dev_count = zenith::devlist(de->devs, DE_MAX_DEVS);
|
||||
}
|
||||
|
||||
static void devexplorer_on_draw(Window* win, Framebuffer& fb) {
|
||||
DevExplorerState* de = (DevExplorerState*)win->app_data;
|
||||
if (!de) return;
|
||||
|
||||
Canvas c(win);
|
||||
c.fill(colors::WINDOW_BG);
|
||||
|
||||
int fh = system_font_height();
|
||||
|
||||
// --- Toolbar ---
|
||||
c.fill_rect(0, 0, c.w, DE_TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5));
|
||||
c.hline(0, DE_TOOLBAR_H - 1, c.w, colors::BORDER);
|
||||
|
||||
// "Refresh" button
|
||||
int btn_w = 80;
|
||||
int btn_h = 26;
|
||||
int btn_x = 8;
|
||||
int btn_y = (DE_TOOLBAR_H - btn_h) / 2;
|
||||
c.button(btn_x, btn_y, btn_w, btn_h, "Refresh",
|
||||
Color::from_rgb(0x33, 0x66, 0xCC), colors::WHITE, 4);
|
||||
|
||||
// Device count on right
|
||||
char count_str[24];
|
||||
snprintf(count_str, sizeof(count_str), "%d devices", de->dev_count);
|
||||
int cw = text_width(count_str);
|
||||
c.text(c.w - cw - 12, (DE_TOOLBAR_H - fh) / 2, count_str, colors::TEXT_COLOR);
|
||||
|
||||
// --- Build display rows ---
|
||||
DisplayRow rows[MAX_DISPLAY_ROWS];
|
||||
int row_count = build_display_rows(de, rows);
|
||||
|
||||
// --- Compute visible area ---
|
||||
int list_y = DE_TOOLBAR_H;
|
||||
int list_h = c.h - list_y;
|
||||
if (list_h < 1) return;
|
||||
|
||||
// Clamp scroll
|
||||
// Calculate total content height
|
||||
int total_h = 0;
|
||||
for (int i = 0; i < row_count; i++) {
|
||||
total_h += (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
|
||||
}
|
||||
int max_scroll_px = total_h - list_h;
|
||||
if (max_scroll_px < 0) max_scroll_px = 0;
|
||||
|
||||
// Convert scroll_y (in rows) to pixel offset for simplicity,
|
||||
// but keep the row-based model: scroll_y = row offset
|
||||
// We'll compute cumulative pixel offsets
|
||||
int scroll_px = 0;
|
||||
for (int i = 0; i < de->scroll_y && i < row_count; i++) {
|
||||
scroll_px += (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
|
||||
}
|
||||
if (scroll_px > max_scroll_px) {
|
||||
scroll_px = max_scroll_px;
|
||||
// Recalculate scroll_y to match
|
||||
de->scroll_y = 0;
|
||||
int acc = 0;
|
||||
for (int i = 0; i < row_count; i++) {
|
||||
int rh = (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
|
||||
if (acc + rh > max_scroll_px) break;
|
||||
acc += rh;
|
||||
de->scroll_y = i + 1;
|
||||
}
|
||||
}
|
||||
if (de->scroll_y < 0) de->scroll_y = 0;
|
||||
|
||||
// Clamp selected_row
|
||||
if (de->selected_row >= row_count) de->selected_row = row_count - 1;
|
||||
|
||||
// --- Draw rows ---
|
||||
int draw_y = list_y - scroll_px;
|
||||
// Advance to first visible area by recomputing from scroll offset
|
||||
draw_y = list_y;
|
||||
int first_visible = de->scroll_y;
|
||||
|
||||
// Compute starting y by accumulating heights of skipped rows
|
||||
// Actually, let's just iterate all rows and skip those above the viewport
|
||||
draw_y = list_y;
|
||||
for (int i = 0; i < first_visible && i < row_count; i++) {
|
||||
draw_y -= (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
|
||||
}
|
||||
|
||||
// Draw from first_visible onward
|
||||
int cur_y = list_y;
|
||||
for (int i = first_visible; i < row_count; i++) {
|
||||
int row_h = (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
|
||||
if (cur_y >= c.h) break; // Past bottom of window
|
||||
|
||||
if (rows[i].type == ROW_CATEGORY) {
|
||||
// Category header
|
||||
int cat = rows[i].category;
|
||||
c.fill_rect(0, cur_y, c.w, DE_CAT_H, Color::from_rgb(0xF0, 0xF0, 0xF0));
|
||||
|
||||
// Highlight if selected
|
||||
if (i == de->selected_row) {
|
||||
c.fill_rect(0, cur_y, c.w, DE_CAT_H, colors::MENU_HOVER);
|
||||
}
|
||||
|
||||
// Expand/collapse triangle
|
||||
int tri_x = 10;
|
||||
int tri_y = cur_y + (DE_CAT_H - 8) / 2;
|
||||
Color tri_color = Color::from_rgb(0x55, 0x55, 0x55);
|
||||
if (de->collapsed[cat]) {
|
||||
draw_triangle_right(c, tri_x, tri_y, 8, tri_color);
|
||||
} else {
|
||||
draw_triangle_down(c, tri_x, tri_y, 8, tri_color);
|
||||
}
|
||||
|
||||
// Colored dot
|
||||
int dot_x = 24;
|
||||
int dot_y = cur_y + (DE_CAT_H - 8) / 2;
|
||||
Color cat_col = (cat >= 0 && cat < NUM_CATEGORIES)
|
||||
? category_colors[cat] : colors::TEXT_COLOR;
|
||||
c.fill_rounded_rect(dot_x, dot_y, 8, 8, 4, cat_col);
|
||||
|
||||
// Category name
|
||||
const char* cat_name = (cat >= 0 && cat < NUM_CATEGORIES)
|
||||
? category_names[cat] : "?";
|
||||
int text_y = cur_y + (DE_CAT_H - fh) / 2;
|
||||
c.text(36, text_y, cat_name, Color::from_rgb(0x33, 0x33, 0x33));
|
||||
|
||||
// Device count in parentheses on right
|
||||
int cat_count = 0;
|
||||
for (int d = 0; d < de->dev_count; d++) {
|
||||
if (de->devs[d].category == cat) cat_count++;
|
||||
}
|
||||
char cnt_buf[16];
|
||||
snprintf(cnt_buf, sizeof(cnt_buf), "(%d)", cat_count);
|
||||
int name_w = text_width(cat_name);
|
||||
c.text(36 + name_w + 8, text_y, cnt_buf,
|
||||
Color::from_rgb(0x88, 0x88, 0x88));
|
||||
|
||||
// Bottom border
|
||||
c.hline(0, cur_y + DE_CAT_H - 1, c.w, Color::from_rgb(0xE0, 0xE0, 0xE0));
|
||||
} else {
|
||||
// Device item row
|
||||
int di = rows[i].dev_index;
|
||||
|
||||
// Selected row highlight
|
||||
if (i == de->selected_row) {
|
||||
c.fill_rect(0, cur_y, c.w, DE_ITEM_H, colors::MENU_HOVER);
|
||||
}
|
||||
|
||||
int text_y = cur_y + (DE_ITEM_H - fh) / 2;
|
||||
|
||||
// Device name (indented)
|
||||
c.text(DE_INDENT + 10, text_y, de->devs[di].name, colors::TEXT_COLOR);
|
||||
|
||||
// Detail string (right column, gray)
|
||||
int detail_x = c.w / 2 + 20;
|
||||
c.text(detail_x, text_y, de->devs[di].detail,
|
||||
Color::from_rgb(0x66, 0x66, 0x66));
|
||||
}
|
||||
|
||||
cur_y += row_h;
|
||||
}
|
||||
|
||||
// --- Scrollbar ---
|
||||
if (total_h > list_h) {
|
||||
int sb_x = c.w - 6;
|
||||
int thumb_h = (list_h * list_h) / total_h;
|
||||
if (thumb_h < 20) thumb_h = 20;
|
||||
int thumb_y = list_y + (scroll_px * (list_h - thumb_h)) / max_scroll_px;
|
||||
c.fill_rect(sb_x, list_y, 4, list_h, Color::from_rgb(0xE0, 0xE0, 0xE0));
|
||||
c.fill_rect(sb_x, thumb_y, 4, thumb_h, Color::from_rgb(0xAA, 0xAA, 0xAA));
|
||||
}
|
||||
}
|
||||
|
||||
static void devexplorer_on_mouse(Window* win, MouseEvent& ev) {
|
||||
DevExplorerState* de = (DevExplorerState*)win->app_data;
|
||||
if (!de) return;
|
||||
|
||||
Rect cr = win->content_rect();
|
||||
int lx = ev.x - cr.x;
|
||||
int ly = ev.y - cr.y;
|
||||
|
||||
// Scroll
|
||||
if (ev.scroll != 0) {
|
||||
de->scroll_y -= ev.scroll;
|
||||
if (de->scroll_y < 0) de->scroll_y = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.left_pressed()) {
|
||||
// Check "Refresh" button click
|
||||
int btn_w = 80;
|
||||
int btn_h = 26;
|
||||
int btn_x = 8;
|
||||
int btn_y = (DE_TOOLBAR_H - btn_h) / 2;
|
||||
Rect btn_rect = {btn_x, btn_y, btn_w, btn_h};
|
||||
if (btn_rect.contains(lx, ly)) {
|
||||
de->last_poll_ms = 0; // force refresh
|
||||
return;
|
||||
}
|
||||
|
||||
// Check row click in list area
|
||||
int list_y = DE_TOOLBAR_H;
|
||||
if (ly >= list_y) {
|
||||
// Build display rows to map click to row
|
||||
DisplayRow rows[MAX_DISPLAY_ROWS];
|
||||
int row_count = build_display_rows(de, rows);
|
||||
|
||||
// Walk rows from scroll offset, accumulating y
|
||||
int cur_y = list_y;
|
||||
for (int i = de->scroll_y; i < row_count; i++) {
|
||||
int row_h = (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
|
||||
if (ly >= cur_y && ly < cur_y + row_h) {
|
||||
// Hit this row
|
||||
if (rows[i].type == ROW_CATEGORY) {
|
||||
// Toggle collapse
|
||||
int cat = rows[i].category;
|
||||
de->collapsed[cat] = !de->collapsed[cat];
|
||||
de->selected_row = -1;
|
||||
} else {
|
||||
de->selected_row = i;
|
||||
}
|
||||
return;
|
||||
}
|
||||
cur_y += row_h;
|
||||
if (cur_y >= win->content_h) break;
|
||||
}
|
||||
// Clicked below all rows
|
||||
de->selected_row = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void devexplorer_on_key(Window* win, const Zenith::KeyEvent& key) {
|
||||
DevExplorerState* de = (DevExplorerState*)win->app_data;
|
||||
if (!de || !key.pressed) return;
|
||||
|
||||
DisplayRow rows[MAX_DISPLAY_ROWS];
|
||||
int row_count = build_display_rows(de, rows);
|
||||
if (row_count == 0) return;
|
||||
|
||||
if (key.scancode == 0x48) { // Up arrow
|
||||
if (de->selected_row <= 0) {
|
||||
de->selected_row = 0;
|
||||
} else {
|
||||
de->selected_row--;
|
||||
}
|
||||
// Scroll to keep selection visible
|
||||
if (de->selected_row < de->scroll_y) {
|
||||
de->scroll_y = de->selected_row;
|
||||
}
|
||||
} else if (key.scancode == 0x50) { // Down arrow
|
||||
if (de->selected_row < row_count - 1) {
|
||||
de->selected_row++;
|
||||
}
|
||||
// Scroll to keep selection visible — estimate visible rows
|
||||
int list_h = win->content_h - DE_TOOLBAR_H;
|
||||
int cur_h = 0;
|
||||
int last_visible = de->scroll_y;
|
||||
for (int i = de->scroll_y; i < row_count; i++) {
|
||||
int rh = (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
|
||||
if (cur_h + rh > list_h) break;
|
||||
cur_h += rh;
|
||||
last_visible = i;
|
||||
}
|
||||
if (de->selected_row > last_visible) {
|
||||
de->scroll_y += (de->selected_row - last_visible);
|
||||
}
|
||||
} else if (key.scancode == 0x4B) { // Left arrow — 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 we were on a device row, move selection to the category header
|
||||
if (rows[de->selected_row].type == ROW_DEVICE) {
|
||||
// Find the category header row for this category
|
||||
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;
|
||||
}
|
||||
}
|
||||
// After collapsing, rebuild and clamp
|
||||
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 arrow — 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 on category row
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void devexplorer_on_close(Window* win) {
|
||||
if (win->app_data) {
|
||||
zenith::mfree(win->app_data);
|
||||
win->app_data = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Device Explorer launcher
|
||||
// ============================================================================
|
||||
|
||||
void open_devexplorer(DesktopState* ds) {
|
||||
int idx = desktop_create_window(ds, "Devices", 140, 70, 640, 460);
|
||||
if (idx < 0) return;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
|
||||
DevExplorerState* de = (DevExplorerState*)zenith::malloc(sizeof(DevExplorerState));
|
||||
zenith::memset(de, 0, sizeof(DevExplorerState));
|
||||
de->desktop = ds;
|
||||
de->selected_row = -1;
|
||||
de->scroll_y = 0;
|
||||
de->last_poll_ms = 0;
|
||||
|
||||
// All categories start expanded
|
||||
for (int i = 0; i < NUM_CATEGORIES; i++)
|
||||
de->collapsed[i] = false;
|
||||
|
||||
// Initial poll
|
||||
de->dev_count = zenith::devlist(de->devs, DE_MAX_DEVS);
|
||||
|
||||
win->app_data = de;
|
||||
win->on_draw = devexplorer_on_draw;
|
||||
win->on_mouse = devexplorer_on_mouse;
|
||||
win->on_key = devexplorer_on_key;
|
||||
win->on_close = devexplorer_on_close;
|
||||
win->on_poll = devexplorer_on_poll;
|
||||
}
|
||||
Reference in New Issue
Block a user