feat: split desktop implementation into multiple source files, reorganize apps
This commit is contained in:
@@ -0,0 +1,404 @@
|
||||
/*
|
||||
* app_calculator.cpp
|
||||
* MontaukOS Desktop - Calculator application
|
||||
* Integer-only 4-function calculator (values scaled by 100 for 2 decimal places)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "apps_common.hpp"
|
||||
|
||||
// ============================================================================
|
||||
// Calculator state
|
||||
// ============================================================================
|
||||
|
||||
struct CalcState {
|
||||
int64_t display_val; // current display value * 100
|
||||
int64_t accumulator; // stored accumulator * 100
|
||||
char pending_op; // '+', '-', '*', '/', or 0
|
||||
bool start_new; // next digit starts a new number
|
||||
bool has_decimal; // decimal point pressed
|
||||
int decimal_digits; // number of decimal digits entered
|
||||
char display_str[32]; // formatted display string
|
||||
};
|
||||
|
||||
static constexpr int CALC_DISPLAY_H = 56;
|
||||
static constexpr int CALC_BTN_W = 52;
|
||||
static constexpr int CALC_BTN_H = 40;
|
||||
static constexpr int CALC_BTN_PAD = 4;
|
||||
|
||||
// ============================================================================
|
||||
// Display formatting
|
||||
// ============================================================================
|
||||
|
||||
static void calc_format_display(CalcState* cs) {
|
||||
int64_t val = cs->display_val;
|
||||
bool neg = val < 0;
|
||||
if (neg) val = -val;
|
||||
|
||||
int64_t integer = val / 100;
|
||||
int64_t frac = val % 100;
|
||||
|
||||
if (cs->has_decimal || frac != 0) {
|
||||
// Show decimal
|
||||
char int_buf[20];
|
||||
int pos = 0;
|
||||
|
||||
if (integer == 0) {
|
||||
int_buf[pos++] = '0';
|
||||
} else {
|
||||
char tmp[20]; int ti = 0;
|
||||
int64_t v = integer;
|
||||
while (v > 0) { tmp[ti++] = '0' + (int)(v % 10); v /= 10; }
|
||||
while (ti > 0) int_buf[pos++] = tmp[--ti];
|
||||
}
|
||||
int_buf[pos] = '\0';
|
||||
|
||||
if (neg) {
|
||||
snprintf(cs->display_str, 32, "-%s.%02d", int_buf, (int)frac);
|
||||
} else {
|
||||
snprintf(cs->display_str, 32, "%s.%02d", int_buf, (int)frac);
|
||||
}
|
||||
|
||||
// Trim trailing zeros after decimal point (unless user is entering decimals)
|
||||
if (!cs->has_decimal) {
|
||||
int len = montauk::slen(cs->display_str);
|
||||
while (len > 1 && cs->display_str[len - 1] == '0') {
|
||||
cs->display_str[--len] = '\0';
|
||||
}
|
||||
if (len > 0 && cs->display_str[len - 1] == '.') {
|
||||
cs->display_str[--len] = '\0';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Integer display
|
||||
if (neg) {
|
||||
char int_buf[20];
|
||||
int pos = 0;
|
||||
int64_t v = integer;
|
||||
if (v == 0) {
|
||||
int_buf[pos++] = '0';
|
||||
} else {
|
||||
char tmp[20]; int ti = 0;
|
||||
while (v > 0) { tmp[ti++] = '0' + (int)(v % 10); v /= 10; }
|
||||
while (ti > 0) int_buf[pos++] = tmp[--ti];
|
||||
}
|
||||
int_buf[pos] = '\0';
|
||||
snprintf(cs->display_str, 32, "-%s", int_buf);
|
||||
} else {
|
||||
int pos = 0;
|
||||
int64_t v = integer;
|
||||
if (v == 0) {
|
||||
cs->display_str[pos++] = '0';
|
||||
} else {
|
||||
char tmp[20]; int ti = 0;
|
||||
while (v > 0) { tmp[ti++] = '0' + (int)(v % 10); v /= 10; }
|
||||
while (ti > 0) cs->display_str[pos++] = tmp[--ti];
|
||||
}
|
||||
cs->display_str[pos] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Calculator operations
|
||||
// ============================================================================
|
||||
|
||||
static void calc_apply_op(CalcState* cs) {
|
||||
if (cs->pending_op == 0) {
|
||||
cs->accumulator = cs->display_val;
|
||||
return;
|
||||
}
|
||||
switch (cs->pending_op) {
|
||||
case '+': cs->accumulator += cs->display_val; break;
|
||||
case '-': cs->accumulator -= cs->display_val; break;
|
||||
case '*': cs->accumulator = (cs->accumulator * cs->display_val) / 100; break;
|
||||
case '/':
|
||||
if (cs->display_val != 0)
|
||||
cs->accumulator = (cs->accumulator * 100) / cs->display_val;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void calc_input_digit(CalcState* cs, int digit) {
|
||||
if (cs->start_new) {
|
||||
cs->display_val = 0;
|
||||
cs->start_new = false;
|
||||
cs->has_decimal = false;
|
||||
cs->decimal_digits = 0;
|
||||
}
|
||||
|
||||
if (cs->has_decimal) {
|
||||
if (cs->decimal_digits < 2) {
|
||||
cs->decimal_digits++;
|
||||
bool neg = cs->display_val < 0;
|
||||
int64_t abs_val = neg ? -cs->display_val : cs->display_val;
|
||||
if (cs->decimal_digits == 1) {
|
||||
abs_val = (abs_val / 100) * 100 + digit * 10;
|
||||
} else {
|
||||
abs_val = abs_val + digit;
|
||||
}
|
||||
cs->display_val = neg ? -abs_val : abs_val;
|
||||
}
|
||||
} else {
|
||||
bool neg = cs->display_val < 0;
|
||||
int64_t abs_val = neg ? -cs->display_val : cs->display_val;
|
||||
int64_t integer = abs_val / 100;
|
||||
if (integer < 999999999) {
|
||||
integer = integer * 10 + digit;
|
||||
cs->display_val = integer * 100;
|
||||
if (neg) cs->display_val = -cs->display_val;
|
||||
}
|
||||
}
|
||||
calc_format_display(cs);
|
||||
}
|
||||
|
||||
static void calc_press_operator(CalcState* cs, char op) {
|
||||
if (!cs->start_new) {
|
||||
calc_apply_op(cs);
|
||||
cs->display_val = cs->accumulator;
|
||||
}
|
||||
cs->pending_op = op;
|
||||
cs->start_new = true;
|
||||
cs->has_decimal = false;
|
||||
cs->decimal_digits = 0;
|
||||
calc_format_display(cs);
|
||||
}
|
||||
|
||||
static void calc_press_equals(CalcState* cs) {
|
||||
calc_apply_op(cs);
|
||||
cs->display_val = cs->accumulator;
|
||||
cs->pending_op = 0;
|
||||
cs->start_new = true;
|
||||
cs->has_decimal = false;
|
||||
cs->decimal_digits = 0;
|
||||
calc_format_display(cs);
|
||||
}
|
||||
|
||||
static void calc_press_clear(CalcState* cs) {
|
||||
cs->display_val = 0;
|
||||
cs->accumulator = 0;
|
||||
cs->pending_op = 0;
|
||||
cs->start_new = false;
|
||||
cs->has_decimal = false;
|
||||
cs->decimal_digits = 0;
|
||||
calc_format_display(cs);
|
||||
}
|
||||
|
||||
static void calc_press_negate(CalcState* cs) {
|
||||
cs->display_val = -cs->display_val;
|
||||
calc_format_display(cs);
|
||||
}
|
||||
|
||||
static void calc_press_percent(CalcState* cs) {
|
||||
// Divide by 100: display_val is already scaled by 100, so dividing by 100 means /100
|
||||
cs->display_val = cs->display_val / 100;
|
||||
calc_format_display(cs);
|
||||
}
|
||||
|
||||
static void calc_press_decimal(CalcState* cs) {
|
||||
if (cs->start_new) {
|
||||
cs->display_val = 0;
|
||||
cs->start_new = false;
|
||||
cs->decimal_digits = 0;
|
||||
}
|
||||
cs->has_decimal = true;
|
||||
calc_format_display(cs);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Drawing
|
||||
// ============================================================================
|
||||
|
||||
// Button layout: labels[row][col]
|
||||
static const char* calc_labels[5][4] = {
|
||||
{ "C", "+/-", "%", "/" },
|
||||
{ "7", "8", "9", "*" },
|
||||
{ "4", "5", "6", "-" },
|
||||
{ "1", "2", "3", "+" },
|
||||
{ "0", "0", ".", "=" },
|
||||
};
|
||||
|
||||
static void calculator_on_draw(Window* win, Framebuffer& fb) {
|
||||
CalcState* cs = (CalcState*)win->app_data;
|
||||
if (!cs) return;
|
||||
|
||||
Canvas c(win);
|
||||
|
||||
// Background
|
||||
c.fill(Color::from_rgb(0xF0, 0xF0, 0xF0));
|
||||
|
||||
// Display area
|
||||
c.fill_rect(0, 0, c.w, CALC_DISPLAY_H, Color::from_rgb(0x2D, 0x2D, 0x2D));
|
||||
|
||||
// Display text (right-aligned, 2x scale)
|
||||
int text_w;
|
||||
int large_h;
|
||||
if (fonts::system_font && fonts::system_font->valid) {
|
||||
text_w = fonts::system_font->measure_text(cs->display_str, fonts::LARGE_SIZE);
|
||||
large_h = fonts::system_font->get_line_height(fonts::LARGE_SIZE);
|
||||
} else {
|
||||
int text_len = montauk::slen(cs->display_str);
|
||||
text_w = text_len * FONT_WIDTH * 2;
|
||||
large_h = FONT_HEIGHT * 2;
|
||||
}
|
||||
int tx = c.w - text_w - 12;
|
||||
int ty = (CALC_DISPLAY_H - large_h) / 2;
|
||||
if (tx < 4) tx = 4;
|
||||
c.text_2x(tx, ty, cs->display_str, colors::WHITE);
|
||||
|
||||
// Button grid
|
||||
int grid_y = CALC_DISPLAY_H + CALC_BTN_PAD;
|
||||
|
||||
for (int row = 0; row < 5; row++) {
|
||||
for (int col = 0; col < 4; col++) {
|
||||
// Skip second column of "0" button (it spans 2 cols)
|
||||
if (row == 4 && col == 1) continue;
|
||||
|
||||
int bx = CALC_BTN_PAD + col * (CALC_BTN_W + CALC_BTN_PAD);
|
||||
int by = grid_y + row * (CALC_BTN_H + CALC_BTN_PAD);
|
||||
int bw = CALC_BTN_W;
|
||||
|
||||
// "0" button spans 2 columns
|
||||
if (row == 4 && col == 0) {
|
||||
bw = CALC_BTN_W * 2 + CALC_BTN_PAD;
|
||||
}
|
||||
|
||||
// Button color
|
||||
Color btn_color;
|
||||
if (col == 3) {
|
||||
btn_color = colors::ACCENT;
|
||||
} else if (row == 0) {
|
||||
btn_color = Color::from_rgb(0xD0, 0xD0, 0xD0);
|
||||
} else {
|
||||
btn_color = Color::from_rgb(0xE8, 0xE8, 0xE8);
|
||||
}
|
||||
|
||||
// Draw button
|
||||
c.fill_rect(bx, by, bw, CALC_BTN_H, btn_color);
|
||||
|
||||
// Button text
|
||||
const char* label = calc_labels[row][col];
|
||||
Color label_color = (col == 3) ? colors::WHITE : colors::TEXT_COLOR;
|
||||
int label_w = text_width(label);
|
||||
int lx = bx + (bw - label_w) / 2;
|
||||
int ly = by + (CALC_BTN_H - system_font_height()) / 2;
|
||||
c.text(lx, ly, label, label_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Mouse handling
|
||||
// ============================================================================
|
||||
|
||||
static void calculator_on_mouse(Window* win, MouseEvent& ev) {
|
||||
CalcState* cs = (CalcState*)win->app_data;
|
||||
if (!cs) return;
|
||||
|
||||
if (!ev.left_pressed()) return;
|
||||
|
||||
Rect cr = win->content_rect();
|
||||
int local_x = ev.x - cr.x;
|
||||
int local_y = ev.y - cr.y;
|
||||
|
||||
// Check if click is in button grid
|
||||
int grid_y = CALC_DISPLAY_H + CALC_BTN_PAD;
|
||||
if (local_y < grid_y) return;
|
||||
|
||||
int row = (local_y - grid_y) / (CALC_BTN_H + CALC_BTN_PAD);
|
||||
int col = (local_x - CALC_BTN_PAD) / (CALC_BTN_W + CALC_BTN_PAD);
|
||||
|
||||
if (row < 0 || row > 4 || col < 0 || col > 3) return;
|
||||
|
||||
// Handle "0" button spanning 2 columns
|
||||
if (row == 4 && col <= 1) col = 0;
|
||||
|
||||
// Dispatch button press
|
||||
if (row == 0) {
|
||||
switch (col) {
|
||||
case 0: calc_press_clear(cs); break;
|
||||
case 1: calc_press_negate(cs); break;
|
||||
case 2: calc_press_percent(cs); break;
|
||||
case 3: calc_press_operator(cs, '/'); break;
|
||||
}
|
||||
} else if (row >= 1 && row <= 3) {
|
||||
if (col == 3) {
|
||||
char ops[] = {'*', '-', '+'};
|
||||
calc_press_operator(cs, ops[row - 1]);
|
||||
} else {
|
||||
int digits[3][3] = {{7,8,9},{4,5,6},{1,2,3}};
|
||||
calc_input_digit(cs, digits[row - 1][col]);
|
||||
}
|
||||
} else if (row == 4) {
|
||||
switch (col) {
|
||||
case 0: calc_input_digit(cs, 0); break;
|
||||
case 2: calc_press_decimal(cs); break;
|
||||
case 3: calc_press_equals(cs); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Keyboard handling
|
||||
// ============================================================================
|
||||
|
||||
static void calculator_on_key(Window* win, const Montauk::KeyEvent& key) {
|
||||
CalcState* cs = (CalcState*)win->app_data;
|
||||
if (!cs || !key.pressed) return;
|
||||
|
||||
if (key.ascii >= '0' && key.ascii <= '9') {
|
||||
calc_input_digit(cs, key.ascii - '0');
|
||||
} else if (key.ascii == '+') {
|
||||
calc_press_operator(cs, '+');
|
||||
} else if (key.ascii == '-') {
|
||||
calc_press_operator(cs, '-');
|
||||
} else if (key.ascii == '*') {
|
||||
calc_press_operator(cs, '*');
|
||||
} else if (key.ascii == '/') {
|
||||
calc_press_operator(cs, '/');
|
||||
} else if (key.ascii == '=' || key.ascii == '\n' || key.ascii == '\r') {
|
||||
calc_press_equals(cs);
|
||||
} else if (key.ascii == '.') {
|
||||
calc_press_decimal(cs);
|
||||
} else if (key.ascii == 'c' || key.ascii == 'C' || key.scancode == 0x0E) {
|
||||
calc_press_clear(cs);
|
||||
} else if (key.ascii == '%') {
|
||||
calc_press_percent(cs);
|
||||
}
|
||||
}
|
||||
|
||||
static void calculator_on_close(Window* win) {
|
||||
if (win->app_data) {
|
||||
montauk::mfree(win->app_data);
|
||||
win->app_data = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Calculator launcher
|
||||
// ============================================================================
|
||||
|
||||
void open_calculator(DesktopState* ds) {
|
||||
int calc_w = CALC_BTN_PAD + 4 * (CALC_BTN_W + CALC_BTN_PAD);
|
||||
int calc_h = CALC_DISPLAY_H + CALC_BTN_PAD + 5 * (CALC_BTN_H + CALC_BTN_PAD);
|
||||
|
||||
int idx = desktop_create_window(ds, "Calculator", 350, 150, calc_w, calc_h + TITLEBAR_HEIGHT + BORDER_WIDTH);
|
||||
if (idx < 0) return;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
CalcState* cs = (CalcState*)montauk::malloc(sizeof(CalcState));
|
||||
montauk::memset(cs, 0, sizeof(CalcState));
|
||||
cs->display_val = 0;
|
||||
cs->accumulator = 0;
|
||||
cs->pending_op = 0;
|
||||
cs->start_new = false;
|
||||
cs->has_decimal = false;
|
||||
cs->decimal_digits = 0;
|
||||
calc_format_display(cs);
|
||||
|
||||
win->app_data = cs;
|
||||
win->on_draw = calculator_on_draw;
|
||||
win->on_mouse = calculator_on_mouse;
|
||||
win->on_key = calculator_on_key;
|
||||
win->on_close = calculator_on_close;
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
/*
|
||||
* app_devexplorer.cpp
|
||||
* MontaukOS 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;
|
||||
Montauk::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 = montauk::get_milliseconds();
|
||||
if (now - de->last_poll_ms < DE_POLL_MS) return;
|
||||
de->last_poll_ms = now;
|
||||
|
||||
de->dev_count = montauk::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 Montauk::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) {
|
||||
montauk::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*)montauk::malloc(sizeof(DevExplorerState));
|
||||
montauk::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 = montauk::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;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* app_doom.cpp
|
||||
* MontaukOS Desktop - DOOM launcher (spawns standalone doom.elf)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "apps_common.hpp"
|
||||
|
||||
void open_doom(DesktopState* ds) {
|
||||
(void)ds;
|
||||
montauk::spawn("0:/games/doom.elf");
|
||||
}
|
||||
@@ -0,0 +1,745 @@
|
||||
/*
|
||||
* app_filemanager.cpp
|
||||
* MontaukOS Desktop - Enhanced File Manager application
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "apps_common.hpp"
|
||||
|
||||
// ============================================================================
|
||||
// File Manager state
|
||||
// ============================================================================
|
||||
|
||||
struct FileManagerState {
|
||||
char current_path[256];
|
||||
char history[16][256];
|
||||
int history_pos;
|
||||
int history_count;
|
||||
char entry_names[64][64];
|
||||
int entry_types[64]; // 0=file, 1=directory, 2=executable
|
||||
int entry_sizes[64];
|
||||
int entry_count;
|
||||
int selected;
|
||||
int scroll_offset;
|
||||
bool is_dir[64];
|
||||
int last_click_item;
|
||||
uint64_t last_click_time;
|
||||
Scrollbar scrollbar;
|
||||
DesktopState* desktop;
|
||||
bool grid_view;
|
||||
};
|
||||
|
||||
static constexpr int FM_TOOLBAR_H = 32;
|
||||
static constexpr int FM_PATHBAR_H = 24;
|
||||
static constexpr int FM_HEADER_H = 20;
|
||||
static constexpr int FM_ITEM_H = 24;
|
||||
static constexpr int FM_SCROLLBAR_W = 12;
|
||||
static constexpr int FM_GRID_CELL_W = 80;
|
||||
static constexpr int FM_GRID_CELL_H = 80;
|
||||
static constexpr int FM_GRID_ICON = 48;
|
||||
static constexpr int FM_GRID_PAD = 4;
|
||||
|
||||
// ============================================================================
|
||||
// File type detection
|
||||
// ============================================================================
|
||||
|
||||
static bool str_ends_with(const char* s, const char* suffix) {
|
||||
int slen = montauk::slen(s);
|
||||
int suflen = montauk::slen(suffix);
|
||||
if (suflen > slen) return false;
|
||||
for (int i = 0; i < suflen; i++) {
|
||||
char sc = s[slen - suflen + i];
|
||||
char ec = suffix[i];
|
||||
if (sc >= 'A' && sc <= 'Z') sc += 32;
|
||||
if (ec >= 'A' && ec <= 'Z') ec += 32;
|
||||
if (sc != ec) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_image_file(const char* name) {
|
||||
return str_ends_with(name, ".jpg") || str_ends_with(name, ".jpeg");
|
||||
}
|
||||
|
||||
static bool is_font_file(const char* name) {
|
||||
return str_ends_with(name, ".ttf");
|
||||
}
|
||||
|
||||
static bool is_pdf_file(const char* name) {
|
||||
return str_ends_with(name, ".pdf");
|
||||
}
|
||||
|
||||
static bool is_spreadsheet_file(const char* name) {
|
||||
return str_ends_with(name, ".mss");
|
||||
}
|
||||
|
||||
static int detect_file_type(const char* name, bool is_dir) {
|
||||
if (is_dir) return 1;
|
||||
if (str_ends_with(name, ".elf")) return 2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Directory reading with sorting and file sizes
|
||||
// ============================================================================
|
||||
|
||||
static void filemanager_read_dir(FileManagerState* fm) {
|
||||
const char* names[64];
|
||||
fm->entry_count = montauk::readdir(fm->current_path, names, 64);
|
||||
if (fm->entry_count < 0) fm->entry_count = 0;
|
||||
|
||||
// readdir returns full paths from the VFS (e.g. "man/fetch.1" instead
|
||||
// of just "fetch.1"). Compute the prefix to strip so we get basenames.
|
||||
const char* after_drive = fm->current_path;
|
||||
for (int k = 0; after_drive[k]; k++) {
|
||||
if (after_drive[k] == ':' && after_drive[k + 1] == '/') {
|
||||
after_drive += k + 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
char prefix[256] = {0};
|
||||
int prefix_len = 0;
|
||||
if (after_drive[0] != '\0') {
|
||||
montauk::strcpy(prefix, after_drive);
|
||||
prefix_len = montauk::slen(prefix);
|
||||
if (prefix_len > 0 && prefix[prefix_len - 1] != '/') {
|
||||
prefix[prefix_len++] = '/';
|
||||
prefix[prefix_len] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < fm->entry_count; i++) {
|
||||
const char* raw = names[i];
|
||||
// Strip directory prefix if it matches
|
||||
if (prefix_len > 0) {
|
||||
bool match = true;
|
||||
for (int k = 0; k < prefix_len; k++) {
|
||||
if (raw[k] != prefix[k]) { match = false; break; }
|
||||
}
|
||||
if (match) raw += prefix_len;
|
||||
}
|
||||
montauk::strncpy(fm->entry_names[i], raw, 63);
|
||||
int len = montauk::slen(fm->entry_names[i]);
|
||||
|
||||
// Detect directory
|
||||
if (len > 0 && fm->entry_names[i][len - 1] == '/') {
|
||||
fm->is_dir[i] = true;
|
||||
fm->entry_names[i][len - 1] = '\0';
|
||||
} else {
|
||||
bool has_dot = false;
|
||||
for (int j = 0; j < len; j++) {
|
||||
if (fm->entry_names[i][j] == '.') { has_dot = true; break; }
|
||||
}
|
||||
fm->is_dir[i] = !has_dot;
|
||||
}
|
||||
|
||||
fm->entry_types[i] = detect_file_type(fm->entry_names[i], fm->is_dir[i]);
|
||||
|
||||
// Get file size
|
||||
fm->entry_sizes[i] = 0;
|
||||
if (!fm->is_dir[i]) {
|
||||
char fullpath[512];
|
||||
montauk::strcpy(fullpath, fm->current_path);
|
||||
int plen = montauk::slen(fullpath);
|
||||
if (plen > 0 && fullpath[plen - 1] != '/') {
|
||||
str_append(fullpath, "/", 512);
|
||||
}
|
||||
str_append(fullpath, fm->entry_names[i], 512);
|
||||
int fd = montauk::open(fullpath);
|
||||
if (fd >= 0) {
|
||||
fm->entry_sizes[i] = (int)montauk::getsize(fd);
|
||||
montauk::close(fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort: directories first, then alphabetical (case-insensitive)
|
||||
for (int i = 1; i < fm->entry_count; i++) {
|
||||
char tmp_name[64];
|
||||
int tmp_type = fm->entry_types[i];
|
||||
int tmp_size = fm->entry_sizes[i];
|
||||
bool tmp_isdir = fm->is_dir[i];
|
||||
montauk::strcpy(tmp_name, fm->entry_names[i]);
|
||||
|
||||
int j = i - 1;
|
||||
while (j >= 0) {
|
||||
bool swap = false;
|
||||
if (tmp_isdir && !fm->is_dir[j]) {
|
||||
swap = true;
|
||||
} else if (tmp_isdir == fm->is_dir[j]) {
|
||||
if (str_compare_ci(tmp_name, fm->entry_names[j]) < 0) {
|
||||
swap = true;
|
||||
}
|
||||
}
|
||||
if (!swap) break;
|
||||
|
||||
montauk::strcpy(fm->entry_names[j + 1], fm->entry_names[j]);
|
||||
fm->entry_types[j + 1] = fm->entry_types[j];
|
||||
fm->entry_sizes[j + 1] = fm->entry_sizes[j];
|
||||
fm->is_dir[j + 1] = fm->is_dir[j];
|
||||
j--;
|
||||
}
|
||||
montauk::strcpy(fm->entry_names[j + 1], tmp_name);
|
||||
fm->entry_types[j + 1] = tmp_type;
|
||||
fm->entry_sizes[j + 1] = tmp_size;
|
||||
fm->is_dir[j + 1] = tmp_isdir;
|
||||
}
|
||||
|
||||
fm->selected = -1;
|
||||
fm->scroll_offset = 0;
|
||||
fm->scrollbar.scroll_offset = 0;
|
||||
fm->last_click_item = -1;
|
||||
fm->last_click_time = 0;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// History management
|
||||
// ============================================================================
|
||||
|
||||
static void filemanager_push_history(FileManagerState* fm) {
|
||||
// Don't push if same as current position
|
||||
if (fm->history_count > 0 && fm->history_pos >= 0) {
|
||||
if (montauk::streq(fm->history[fm->history_pos], fm->current_path)) return;
|
||||
}
|
||||
fm->history_pos++;
|
||||
if (fm->history_pos >= 16) fm->history_pos = 15;
|
||||
montauk::strcpy(fm->history[fm->history_pos], fm->current_path);
|
||||
fm->history_count = fm->history_pos + 1;
|
||||
}
|
||||
|
||||
static void filemanager_navigate(FileManagerState* fm, const char* name) {
|
||||
int path_len = montauk::slen(fm->current_path);
|
||||
if (path_len > 0 && fm->current_path[path_len - 1] != '/') {
|
||||
str_append(fm->current_path, "/", 256);
|
||||
}
|
||||
str_append(fm->current_path, name, 256);
|
||||
filemanager_push_history(fm);
|
||||
filemanager_read_dir(fm);
|
||||
}
|
||||
|
||||
static void filemanager_go_up(FileManagerState* fm) {
|
||||
int len = montauk::slen(fm->current_path);
|
||||
if (len <= 3) return; // "0:/" is root
|
||||
|
||||
if (len > 0 && fm->current_path[len - 1] == '/') {
|
||||
fm->current_path[len - 1] = '\0';
|
||||
len--;
|
||||
}
|
||||
|
||||
int last_slash = -1;
|
||||
for (int i = len - 1; i >= 0; i--) {
|
||||
if (fm->current_path[i] == '/') { last_slash = i; break; }
|
||||
}
|
||||
if (last_slash >= 0) {
|
||||
fm->current_path[last_slash + 1] = '\0';
|
||||
}
|
||||
filemanager_push_history(fm);
|
||||
filemanager_read_dir(fm);
|
||||
}
|
||||
|
||||
static void filemanager_go_back(FileManagerState* fm) {
|
||||
if (fm->history_pos <= 0) return;
|
||||
fm->history_pos--;
|
||||
montauk::strcpy(fm->current_path, fm->history[fm->history_pos]);
|
||||
filemanager_read_dir(fm);
|
||||
}
|
||||
|
||||
static void filemanager_go_forward(FileManagerState* fm) {
|
||||
if (fm->history_pos >= fm->history_count - 1) return;
|
||||
fm->history_pos++;
|
||||
montauk::strcpy(fm->current_path, fm->history[fm->history_pos]);
|
||||
filemanager_read_dir(fm);
|
||||
}
|
||||
|
||||
static void filemanager_go_home(FileManagerState* fm) {
|
||||
montauk::strcpy(fm->current_path, "0:/");
|
||||
filemanager_push_history(fm);
|
||||
filemanager_read_dir(fm);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Drawing
|
||||
// ============================================================================
|
||||
|
||||
static void filemanager_draw_header(Canvas& c, FileManagerState* fm,
|
||||
Color toolbar_color, Color btn_bg) {
|
||||
DesktopState* ds = fm->desktop;
|
||||
|
||||
// ---- Toolbar (32px) ----
|
||||
c.fill_rect(0, 0, c.w, FM_TOOLBAR_H, toolbar_color);
|
||||
|
||||
struct ToolBtn { int x; SvgIcon* icon; };
|
||||
ToolBtn btns[4] = {
|
||||
{ 4, ds ? &ds->icon_go_back : nullptr },
|
||||
{ 32, ds ? &ds->icon_go_forward : nullptr },
|
||||
{ 60, ds ? &ds->icon_go_up : nullptr },
|
||||
{ 88, ds ? &ds->icon_home : nullptr },
|
||||
};
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int bx = btns[i].x;
|
||||
int by = 4;
|
||||
c.fill_rect(bx, by, 24, 24, btn_bg);
|
||||
|
||||
if (btns[i].icon) {
|
||||
int ix = bx + (24 - btns[i].icon->width) / 2;
|
||||
int iy = by + (24 - btns[i].icon->height) / 2;
|
||||
c.icon(ix, iy, *btns[i].icon);
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle view button (5th toolbar button)
|
||||
{
|
||||
int bx = 120, by = 4;
|
||||
c.fill_rect(bx, by, 24, 24, btn_bg);
|
||||
if (fm->grid_view) {
|
||||
for (int r = 0; r < 2; r++)
|
||||
for (int cc = 0; cc < 2; cc++)
|
||||
c.fill_rect(bx + 5 + cc * 8, by + 5 + r * 8, 6, 6, colors::TEXT_COLOR);
|
||||
} else {
|
||||
for (int r = 0; r < 3; r++)
|
||||
c.fill_rect(bx + 5, by + 5 + r * 5, 14, 2, colors::TEXT_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
// Toolbar separator
|
||||
c.hline(0, FM_TOOLBAR_H - 1, c.w, colors::BORDER);
|
||||
|
||||
// ---- Path bar ----
|
||||
int pathbar_y = FM_TOOLBAR_H;
|
||||
c.fill_rect(0, pathbar_y, c.w, FM_PATHBAR_H, Color::from_rgb(0xF0, 0xF0, 0xF0));
|
||||
c.text(8, pathbar_y + 4, fm->current_path, colors::TEXT_COLOR);
|
||||
|
||||
// Path bar separator
|
||||
c.hline(0, pathbar_y + FM_PATHBAR_H - 1, c.w, colors::BORDER);
|
||||
}
|
||||
|
||||
static void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
||||
FileManagerState* fm = (FileManagerState*)win->app_data;
|
||||
if (!fm) return;
|
||||
|
||||
Canvas c(win);
|
||||
c.fill(colors::WINDOW_BG);
|
||||
|
||||
Color toolbar_color = Color::from_rgb(0xF5, 0xF5, 0xF5);
|
||||
Color btn_bg = Color::from_rgb(0xE8, 0xE8, 0xE8);
|
||||
Color dim = Color::from_rgb(0x88, 0x88, 0x88);
|
||||
DesktopState* ds = fm->desktop;
|
||||
|
||||
// Draw header (toolbar + path bar)
|
||||
filemanager_draw_header(c, fm, toolbar_color, btn_bg);
|
||||
|
||||
if (fm->grid_view) {
|
||||
// ---- Grid View ----
|
||||
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H;
|
||||
int list_h = c.h - list_y;
|
||||
int cols = (c.w - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
|
||||
if (cols < 1) cols = 1;
|
||||
int rows = (fm->entry_count + cols - 1) / cols;
|
||||
int content_h = rows * FM_GRID_CELL_H;
|
||||
|
||||
// Update scrollbar
|
||||
fm->scrollbar.bounds = {c.w - FM_SCROLLBAR_W, list_y, FM_SCROLLBAR_W, list_h};
|
||||
fm->scrollbar.content_height = content_h;
|
||||
fm->scrollbar.view_height = list_h;
|
||||
|
||||
for (int i = 0; i < fm->entry_count; i++) {
|
||||
int col = i % cols;
|
||||
int row = i / cols;
|
||||
int cell_x = col * FM_GRID_CELL_W;
|
||||
int cell_y = list_y + row * FM_GRID_CELL_H - fm->scrollbar.scroll_offset;
|
||||
|
||||
// Skip if entirely off-screen
|
||||
if (cell_y + FM_GRID_CELL_H <= list_y || cell_y >= c.h) continue;
|
||||
|
||||
// Selection highlight
|
||||
if (i == fm->selected) {
|
||||
int sy = gui_max(cell_y, list_y);
|
||||
int sh = gui_min(cell_y + FM_GRID_CELL_H, c.h) - sy;
|
||||
int sw = gui_min(FM_GRID_CELL_W, c.w - FM_SCROLLBAR_W - cell_x);
|
||||
if (sh > 0 && sw > 0)
|
||||
c.fill_rect(cell_x, sy, sw, sh, colors::MENU_HOVER);
|
||||
}
|
||||
|
||||
// Large icon centered horizontally
|
||||
int icon_x = cell_x + (FM_GRID_CELL_W - FM_GRID_ICON) / 2;
|
||||
int icon_y = cell_y + FM_GRID_PAD;
|
||||
if (ds && fm->entry_types[i] == 1 && ds->icon_folder_lg.pixels) {
|
||||
c.icon(icon_x, icon_y, ds->icon_folder_lg);
|
||||
} else if (ds && fm->entry_types[i] == 2 && ds->icon_exec_lg.pixels) {
|
||||
c.icon(icon_x, icon_y, ds->icon_exec_lg);
|
||||
} else if (ds && ds->icon_file_lg.pixels) {
|
||||
c.icon(icon_x, icon_y, ds->icon_file_lg);
|
||||
} else {
|
||||
Color icon_c = fm->is_dir[i]
|
||||
? Color::from_rgb(0xFF, 0xBD, 0x2E)
|
||||
: Color::from_rgb(0x90, 0x90, 0x90);
|
||||
int iy_clip = gui_max(icon_y, list_y);
|
||||
int ih_clip = FM_GRID_ICON - (iy_clip - icon_y);
|
||||
if (ih_clip > 0)
|
||||
c.fill_rect(icon_x, iy_clip, FM_GRID_ICON, ih_clip, icon_c);
|
||||
}
|
||||
|
||||
// Filename centered below icon, truncated if needed
|
||||
char label[16];
|
||||
int nlen = montauk::slen(fm->entry_names[i]);
|
||||
if (nlen > 9) {
|
||||
for (int k = 0; k < 9; k++) label[k] = fm->entry_names[i][k];
|
||||
label[9] = '.';
|
||||
label[10] = '.';
|
||||
label[11] = '\0';
|
||||
} else {
|
||||
montauk::strncpy(label, fm->entry_names[i], 15);
|
||||
}
|
||||
int tw = text_width(label);
|
||||
int tx = cell_x + (FM_GRID_CELL_W - tw) / 2;
|
||||
if (tx < cell_x) tx = cell_x;
|
||||
int ty = icon_y + FM_GRID_ICON + 2;
|
||||
if (ty >= list_y && ty + system_font_height() <= c.h)
|
||||
c.text(tx, ty, label, colors::TEXT_COLOR);
|
||||
}
|
||||
} else {
|
||||
// ---- List View ----
|
||||
|
||||
// ---- Column headers ----
|
||||
int header_y = FM_TOOLBAR_H + FM_PATHBAR_H;
|
||||
c.fill_rect(0, header_y, c.w, FM_HEADER_H, Color::from_rgb(0xF8, 0xF8, 0xF8));
|
||||
|
||||
int name_col_x = 8;
|
||||
int size_col_x = c.w - FM_SCROLLBAR_W - 120;
|
||||
int type_col_x = c.w - FM_SCROLLBAR_W - 60;
|
||||
|
||||
c.text(name_col_x, header_y + 2, "Name", dim);
|
||||
if (size_col_x > 100)
|
||||
c.text(size_col_x, header_y + 2, "Size", dim);
|
||||
if (type_col_x > 160)
|
||||
c.text(type_col_x, header_y + 2, "Type", dim);
|
||||
|
||||
// Header separator
|
||||
c.hline(0, header_y + FM_HEADER_H - 1, c.w, colors::BORDER);
|
||||
|
||||
// Column separator lines
|
||||
if (size_col_x > 100) {
|
||||
c.vline(size_col_x - 4, header_y, c.h - header_y, colors::BORDER);
|
||||
}
|
||||
|
||||
// ---- File entries ----
|
||||
int list_y = header_y + FM_HEADER_H;
|
||||
int list_h = c.h - list_y;
|
||||
int visible_items = list_h / FM_ITEM_H;
|
||||
int content_h = fm->entry_count * FM_ITEM_H;
|
||||
|
||||
// Update scrollbar
|
||||
fm->scrollbar.bounds = {c.w - FM_SCROLLBAR_W, list_y, FM_SCROLLBAR_W, list_h};
|
||||
fm->scrollbar.content_height = content_h;
|
||||
fm->scrollbar.view_height = list_h;
|
||||
|
||||
int scroll_items = fm->scrollbar.scroll_offset / FM_ITEM_H;
|
||||
|
||||
for (int i = scroll_items; i < fm->entry_count && (i - scroll_items) < visible_items + 1; i++) {
|
||||
int iy = list_y + (i - scroll_items) * FM_ITEM_H - (fm->scrollbar.scroll_offset % FM_ITEM_H);
|
||||
if (iy + FM_ITEM_H <= list_y || iy >= c.h) continue;
|
||||
|
||||
// Highlight selected
|
||||
if (i == fm->selected) {
|
||||
int sy = gui_max(iy, list_y);
|
||||
int sh = gui_min(iy + FM_ITEM_H, c.h) - sy;
|
||||
if (sh > 0)
|
||||
c.fill_rect(0, sy, c.w - FM_SCROLLBAR_W, sh, colors::MENU_HOVER);
|
||||
}
|
||||
|
||||
// Icon
|
||||
int ico_x = 8;
|
||||
int ico_y = iy + (FM_ITEM_H - 16) / 2;
|
||||
if (ds && fm->entry_types[i] == 1 && ds->icon_folder.pixels) {
|
||||
c.icon(ico_x, ico_y, ds->icon_folder);
|
||||
} else if (ds && fm->entry_types[i] == 2 && ds->icon_exec.pixels) {
|
||||
c.icon(ico_x, ico_y, ds->icon_exec);
|
||||
} else if (ds && ds->icon_file.pixels) {
|
||||
c.icon(ico_x, ico_y, ds->icon_file);
|
||||
} else {
|
||||
Color icon_c = fm->is_dir[i]
|
||||
? Color::from_rgb(0xFF, 0xBD, 0x2E)
|
||||
: Color::from_rgb(0x90, 0x90, 0x90);
|
||||
int iy_clip = gui_max(ico_y, list_y);
|
||||
int ih_clip = 16 - (iy_clip - ico_y);
|
||||
if (ih_clip > 0)
|
||||
c.fill_rect(ico_x, iy_clip, 16, ih_clip, icon_c);
|
||||
}
|
||||
|
||||
// Name
|
||||
int tx = 30;
|
||||
int fm_sfh = system_font_height();
|
||||
int ty = iy + (FM_ITEM_H - fm_sfh) / 2;
|
||||
if (ty >= list_y && ty + fm_sfh <= c.h)
|
||||
c.text(tx, ty, fm->entry_names[i], colors::TEXT_COLOR);
|
||||
|
||||
// Size
|
||||
if (size_col_x > 100 && !fm->is_dir[i] && ty >= list_y && ty + fm_sfh <= c.h) {
|
||||
char size_str[16];
|
||||
format_size(size_str, fm->entry_sizes[i]);
|
||||
c.text(size_col_x, ty, size_str, dim);
|
||||
}
|
||||
|
||||
// Type
|
||||
if (type_col_x > 160 && ty >= list_y && ty + fm_sfh <= c.h) {
|
||||
const char* type_str = "File";
|
||||
if (fm->entry_types[i] == 1) type_str = "Dir";
|
||||
else if (fm->entry_types[i] == 2) type_str = "Exec";
|
||||
c.text(type_col_x, ty, type_str, dim);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Scrollbar ----
|
||||
if (fm->scrollbar.content_height > fm->scrollbar.view_height) {
|
||||
Color sb_fg_color = (fm->scrollbar.hovered || fm->scrollbar.dragging)
|
||||
? fm->scrollbar.hover_fg : fm->scrollbar.fg;
|
||||
|
||||
int sbx = fm->scrollbar.bounds.x;
|
||||
int sby = fm->scrollbar.bounds.y;
|
||||
int sbw = fm->scrollbar.bounds.w;
|
||||
int sbh = fm->scrollbar.bounds.h;
|
||||
|
||||
c.fill_rect(sbx, sby, sbw, sbh, colors::SCROLLBAR_BG);
|
||||
|
||||
int th = fm->scrollbar.thumb_height();
|
||||
int tty = fm->scrollbar.thumb_y();
|
||||
c.fill_rect(sbx + 1, tty, sbw - 2, th, sb_fg_color);
|
||||
}
|
||||
|
||||
// Repaint header on top of content to clip scrolled icon overflow
|
||||
if (fm->scrollbar.scroll_offset > 0)
|
||||
filemanager_draw_header(c, fm, toolbar_color, btn_bg);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Mouse handling
|
||||
// ============================================================================
|
||||
|
||||
static void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
FileManagerState* fm = (FileManagerState*)win->app_data;
|
||||
if (!fm) return;
|
||||
|
||||
Rect cr = win->content_rect();
|
||||
int local_x = ev.x - cr.x;
|
||||
int local_y = ev.y - cr.y;
|
||||
int cw = cr.w;
|
||||
|
||||
// Scrollbar interaction
|
||||
MouseEvent local_ev = ev;
|
||||
local_ev.x = local_x;
|
||||
local_ev.y = local_y;
|
||||
fm->scrollbar.handle_mouse(local_ev);
|
||||
|
||||
if (ev.left_pressed()) {
|
||||
// Toolbar button clicks
|
||||
if (local_y < FM_TOOLBAR_H) {
|
||||
if (local_x >= 4 && local_x < 28) filemanager_go_back(fm);
|
||||
else if (local_x >= 32 && local_x < 56) filemanager_go_forward(fm);
|
||||
else if (local_x >= 60 && local_x < 84) filemanager_go_up(fm);
|
||||
else if (local_x >= 88 && local_x < 112) filemanager_go_home(fm);
|
||||
else if (local_x >= 120 && local_x < 144) {
|
||||
fm->grid_view = !fm->grid_view;
|
||||
fm->scrollbar.scroll_offset = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// File clicks (grid vs list)
|
||||
if (fm->grid_view) {
|
||||
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H;
|
||||
if (local_y >= list_y && local_x < cw - FM_SCROLLBAR_W) {
|
||||
int cols = (cw - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
|
||||
if (cols < 1) cols = 1;
|
||||
int col = local_x / FM_GRID_CELL_W;
|
||||
int row = (local_y - list_y + fm->scrollbar.scroll_offset) / FM_GRID_CELL_H;
|
||||
int clicked_idx = row * cols + col;
|
||||
|
||||
if (clicked_idx >= 0 && clicked_idx < fm->entry_count && col < cols) {
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
|
||||
if (fm->last_click_item == clicked_idx &&
|
||||
(now - fm->last_click_time) < 400) {
|
||||
if (fm->is_dir[clicked_idx]) {
|
||||
filemanager_navigate(fm, fm->entry_names[clicked_idx]);
|
||||
} else {
|
||||
char fullpath[512];
|
||||
montauk::strcpy(fullpath, fm->current_path);
|
||||
int plen = montauk::slen(fullpath);
|
||||
if (plen > 0 && fullpath[plen - 1] != '/') {
|
||||
str_append(fullpath, "/", 512);
|
||||
}
|
||||
str_append(fullpath, fm->entry_names[clicked_idx], 512);
|
||||
if (is_image_file(fm->entry_names[clicked_idx])) {
|
||||
montauk::spawn("0:/os/imageviewer.elf", fullpath);
|
||||
} else if (is_font_file(fm->entry_names[clicked_idx])) {
|
||||
montauk::spawn("0:/os/fontpreview.elf", fullpath);
|
||||
} else if (is_pdf_file(fm->entry_names[clicked_idx])) {
|
||||
montauk::spawn("0:/os/pdfviewer.elf", fullpath);
|
||||
} else if (is_spreadsheet_file(fm->entry_names[clicked_idx])) {
|
||||
montauk::spawn("0:/os/spreadsheet.elf", fullpath);
|
||||
} else if (fm->desktop) {
|
||||
open_texteditor_with_file(fm->desktop, fullpath);
|
||||
}
|
||||
}
|
||||
fm->last_click_item = -1;
|
||||
fm->last_click_time = 0;
|
||||
} else {
|
||||
fm->selected = clicked_idx;
|
||||
fm->last_click_item = clicked_idx;
|
||||
fm->last_click_time = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// List view clicks
|
||||
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H;
|
||||
if (local_y >= list_y && local_x < cw - FM_SCROLLBAR_W) {
|
||||
int rel_y = local_y - list_y + fm->scrollbar.scroll_offset;
|
||||
int clicked_idx = rel_y / FM_ITEM_H;
|
||||
|
||||
if (clicked_idx >= 0 && clicked_idx < fm->entry_count) {
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
|
||||
// Double-click detection
|
||||
if (fm->last_click_item == clicked_idx &&
|
||||
(now - fm->last_click_time) < 400) {
|
||||
if (fm->is_dir[clicked_idx]) {
|
||||
filemanager_navigate(fm, fm->entry_names[clicked_idx]);
|
||||
} else {
|
||||
// Open file in appropriate viewer
|
||||
char fullpath[512];
|
||||
montauk::strcpy(fullpath, fm->current_path);
|
||||
int plen = montauk::slen(fullpath);
|
||||
if (plen > 0 && fullpath[plen - 1] != '/') {
|
||||
str_append(fullpath, "/", 512);
|
||||
}
|
||||
str_append(fullpath, fm->entry_names[clicked_idx], 512);
|
||||
if (is_image_file(fm->entry_names[clicked_idx])) {
|
||||
montauk::spawn("0:/os/imageviewer.elf", fullpath);
|
||||
} else if (is_font_file(fm->entry_names[clicked_idx])) {
|
||||
montauk::spawn("0:/os/fontpreview.elf", fullpath);
|
||||
} else if (is_pdf_file(fm->entry_names[clicked_idx])) {
|
||||
montauk::spawn("0:/os/pdfviewer.elf", fullpath);
|
||||
} else if (is_spreadsheet_file(fm->entry_names[clicked_idx])) {
|
||||
montauk::spawn("0:/os/spreadsheet.elf", fullpath);
|
||||
} else if (fm->desktop) {
|
||||
open_texteditor_with_file(fm->desktop, fullpath);
|
||||
}
|
||||
}
|
||||
fm->last_click_item = -1;
|
||||
fm->last_click_time = 0;
|
||||
} else {
|
||||
fm->selected = clicked_idx;
|
||||
fm->last_click_item = clicked_idx;
|
||||
fm->last_click_time = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll handling
|
||||
if (ev.scroll != 0) {
|
||||
int list_y_start = fm->grid_view
|
||||
? FM_TOOLBAR_H + FM_PATHBAR_H
|
||||
: FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H;
|
||||
int scroll_step = fm->grid_view ? FM_GRID_CELL_H : FM_ITEM_H;
|
||||
if (local_y >= list_y_start) {
|
||||
fm->scrollbar.scroll_offset -= ev.scroll * scroll_step;
|
||||
int ms = fm->scrollbar.max_scroll();
|
||||
if (fm->scrollbar.scroll_offset < 0) fm->scrollbar.scroll_offset = 0;
|
||||
if (fm->scrollbar.scroll_offset > ms) fm->scrollbar.scroll_offset = ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Keyboard handling
|
||||
// ============================================================================
|
||||
|
||||
static void filemanager_on_key(Window* win, const Montauk::KeyEvent& key) {
|
||||
FileManagerState* fm = (FileManagerState*)win->app_data;
|
||||
if (!fm || !key.pressed) return;
|
||||
|
||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
filemanager_go_up(fm);
|
||||
} else if (key.scancode == 0x48) {
|
||||
// Up arrow
|
||||
if (fm->grid_view) {
|
||||
Rect cr = win->content_rect();
|
||||
int cols = (cr.w - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
|
||||
if (cols < 1) cols = 1;
|
||||
if (fm->selected >= cols) fm->selected -= cols;
|
||||
} else {
|
||||
if (fm->selected > 0) fm->selected--;
|
||||
}
|
||||
} else if (key.scancode == 0x50) {
|
||||
// Down arrow
|
||||
if (fm->grid_view) {
|
||||
Rect cr = win->content_rect();
|
||||
int cols = (cr.w - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
|
||||
if (cols < 1) cols = 1;
|
||||
if (fm->selected + cols < fm->entry_count) fm->selected += cols;
|
||||
} else {
|
||||
if (fm->selected < fm->entry_count - 1) fm->selected++;
|
||||
}
|
||||
} else if (key.scancode == 0x4B && !key.alt && fm->grid_view) {
|
||||
// Left arrow (grid view only)
|
||||
if (fm->selected > 0) fm->selected--;
|
||||
} else if (key.scancode == 0x4D && !key.alt && fm->grid_view) {
|
||||
// Right arrow (grid view only)
|
||||
if (fm->selected < fm->entry_count - 1) fm->selected++;
|
||||
} else if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
if (fm->selected >= 0 && fm->selected < fm->entry_count) {
|
||||
if (fm->is_dir[fm->selected]) {
|
||||
filemanager_navigate(fm, fm->entry_names[fm->selected]);
|
||||
}
|
||||
}
|
||||
} else if (key.alt && key.scancode == 0x4B) {
|
||||
// Alt+Left: go back
|
||||
filemanager_go_back(fm);
|
||||
} else if (key.alt && key.scancode == 0x4D) {
|
||||
// Alt+Right: go forward
|
||||
filemanager_go_forward(fm);
|
||||
}
|
||||
}
|
||||
|
||||
static void filemanager_on_close(Window* win) {
|
||||
if (win->app_data) {
|
||||
montauk::mfree(win->app_data);
|
||||
win->app_data = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// File Manager launcher
|
||||
// ============================================================================
|
||||
|
||||
void open_filemanager(DesktopState* ds) {
|
||||
int idx = desktop_create_window(ds, "Files", 150, 120, 560, 420);
|
||||
if (idx < 0) return;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
FileManagerState* fm = (FileManagerState*)montauk::malloc(sizeof(FileManagerState));
|
||||
montauk::memset(fm, 0, sizeof(FileManagerState));
|
||||
montauk::strcpy(fm->current_path, "0:/");
|
||||
fm->selected = -1;
|
||||
fm->last_click_item = -1;
|
||||
fm->history_pos = -1;
|
||||
fm->history_count = 0;
|
||||
fm->desktop = ds;
|
||||
fm->grid_view = true;
|
||||
|
||||
fm->scrollbar.init(0, 0, FM_SCROLLBAR_W, 100);
|
||||
|
||||
filemanager_push_history(fm);
|
||||
filemanager_read_dir(fm);
|
||||
|
||||
win->app_data = fm;
|
||||
win->on_draw = filemanager_on_draw;
|
||||
win->on_mouse = filemanager_on_mouse;
|
||||
win->on_key = filemanager_on_key;
|
||||
win->on_close = filemanager_on_close;
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* app_klog.cpp
|
||||
* MontaukOS Desktop - Kernel Log viewer (tails the kernel ring buffer)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "apps_common.hpp"
|
||||
|
||||
// ============================================================================
|
||||
// Kernel Log state
|
||||
// ============================================================================
|
||||
|
||||
static constexpr int KLOG_READ_SIZE = 65536; // matches kernel ring buffer size
|
||||
static constexpr int KLOG_POLL_MS = 250;
|
||||
|
||||
struct KlogState {
|
||||
TerminalState term;
|
||||
char* klog_buf;
|
||||
int last_len;
|
||||
char last_tail_byte;
|
||||
uint64_t last_poll_ms;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Re-feed log into the terminal (fills scrollback + visible screen)
|
||||
// ============================================================================
|
||||
|
||||
static void klog_refeed(KlogState* klog, int n) {
|
||||
// Find enough lines to fill scrollback + visible screen
|
||||
int lines_needed = klog->term.rows + klog->term.max_scrollback;
|
||||
int start = 0;
|
||||
int line_count = 0;
|
||||
for (int i = n - 1; i >= 0; i--) {
|
||||
if (klog->klog_buf[i] == '\n') {
|
||||
line_count++;
|
||||
if (line_count >= lines_needed) { start = i + 1; break; }
|
||||
}
|
||||
}
|
||||
|
||||
// Reset terminal state
|
||||
klog->term.scrollback_lines = 0;
|
||||
klog->term.view_offset = 0;
|
||||
klog->term.cursor_x = 0;
|
||||
klog->term.cursor_y = 0;
|
||||
klog->term.current_fg = colors::TERM_FG;
|
||||
klog->term.current_bg = colors::TERM_BG;
|
||||
|
||||
// Clear visible screen
|
||||
TermCell* screen = term_screen_row(&klog->term, 0);
|
||||
int total = klog->term.cols * klog->term.rows;
|
||||
for (int i = 0; i < total; i++)
|
||||
screen[i] = {' ', colors::TERM_FG, colors::TERM_BG};
|
||||
|
||||
terminal_feed(&klog->term, klog->klog_buf + start, n - start);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Callbacks
|
||||
// ============================================================================
|
||||
|
||||
static void klog_on_draw(Window* win, Framebuffer& fb) {
|
||||
KlogState* klog = (KlogState*)win->app_data;
|
||||
if (!klog) return;
|
||||
|
||||
Rect cr = win->content_rect();
|
||||
|
||||
// Handle resize
|
||||
int new_cols = cr.w / mono_cell_width();
|
||||
int new_rows = cr.h / mono_cell_height();
|
||||
if (new_cols != klog->term.cols || new_rows != klog->term.rows) {
|
||||
terminal_resize(&klog->term, new_cols, new_rows);
|
||||
// Refeed to fill the new grid with log content
|
||||
if (klog->last_len > 0) {
|
||||
klog_refeed(klog, klog->last_len);
|
||||
}
|
||||
}
|
||||
|
||||
bool was_dirty = klog->term.dirty;
|
||||
terminal_render(&klog->term, win->content, cr.w, cr.h);
|
||||
|
||||
// Draw scrollbar overlay when scrollback exists
|
||||
if (was_dirty && klog->term.scrollback_lines > 0) {
|
||||
int cell_h = mono_cell_height();
|
||||
int total_rows = klog->term.scrollback_lines + klog->term.rows;
|
||||
int total_h = total_rows * cell_h;
|
||||
int view_h = cr.h;
|
||||
if (total_h > view_h) {
|
||||
Canvas c(win->content, cr.w, cr.h);
|
||||
int sb_w = 4;
|
||||
int sb_x = cr.w - sb_w - 2;
|
||||
int thumb_h = (view_h * view_h) / total_h;
|
||||
if (thumb_h < 20) thumb_h = 20;
|
||||
int scroll_from_top = klog->term.scrollback_lines - klog->term.view_offset;
|
||||
int max_scroll = klog->term.scrollback_lines;
|
||||
int thumb_y = (max_scroll > 0)
|
||||
? (scroll_from_top * (view_h - thumb_h)) / max_scroll : 0;
|
||||
c.fill_rect(sb_x, 0, sb_w, view_h, Color::from_rgb(0x33, 0x33, 0x33));
|
||||
c.fill_rect(sb_x, thumb_y, sb_w, thumb_h, Color::from_rgb(0x88, 0x88, 0x88));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void klog_on_mouse(Window* win, MouseEvent& ev) {
|
||||
KlogState* klog = (KlogState*)win->app_data;
|
||||
if (!klog) return;
|
||||
|
||||
if (ev.scroll != 0) {
|
||||
klog->term.view_offset += (ev.scroll < 0) ? 3 : -3;
|
||||
if (klog->term.view_offset < 0) klog->term.view_offset = 0;
|
||||
if (klog->term.view_offset > klog->term.scrollback_lines)
|
||||
klog->term.view_offset = klog->term.scrollback_lines;
|
||||
klog->term.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
static void klog_on_key(Window* win, const Montauk::KeyEvent& key) {
|
||||
// Read-only viewer — no keyboard input
|
||||
}
|
||||
|
||||
static void klog_on_poll(Window* win) {
|
||||
KlogState* klog = (KlogState*)win->app_data;
|
||||
if (!klog) return;
|
||||
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
if (now - klog->last_poll_ms < KLOG_POLL_MS) return;
|
||||
klog->last_poll_ms = now;
|
||||
|
||||
int n = (int)montauk::read_klog(klog->klog_buf, KLOG_READ_SIZE);
|
||||
if (n <= 0 && klog->last_len <= 0) return;
|
||||
|
||||
if (n > klog->last_len) {
|
||||
// Buffer grew — feed only the new portion
|
||||
terminal_feed(&klog->term, klog->klog_buf + klog->last_len, n - klog->last_len);
|
||||
} else if (n == klog->last_len && n > 0) {
|
||||
// Same size — check if the tail changed (ring buffer wrapped)
|
||||
if (klog->klog_buf[n - 1] != klog->last_tail_byte) {
|
||||
klog_refeed(klog, n);
|
||||
} else {
|
||||
return; // nothing changed
|
||||
}
|
||||
} else if (n < klog->last_len) {
|
||||
// Unexpected shrink — full re-render
|
||||
klog_refeed(klog, n);
|
||||
}
|
||||
|
||||
klog->last_len = n;
|
||||
if (n > 0) klog->last_tail_byte = klog->klog_buf[n - 1];
|
||||
}
|
||||
|
||||
static void klog_on_close(Window* win) {
|
||||
KlogState* klog = (KlogState*)win->app_data;
|
||||
if (klog) {
|
||||
if (klog->term.cells) montauk::free(klog->term.cells);
|
||||
if (klog->term.alt_cells) montauk::free(klog->term.alt_cells);
|
||||
if (klog->klog_buf) montauk::mfree(klog->klog_buf);
|
||||
montauk::mfree(klog);
|
||||
win->app_data = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Kernel Log launcher
|
||||
// ============================================================================
|
||||
|
||||
void open_klog(DesktopState* ds) {
|
||||
int idx = desktop_create_window(ds, "Kernel Log", 160, 60, 720, 480);
|
||||
if (idx < 0) return;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
Rect cr = win->content_rect();
|
||||
int cols = cr.w / mono_cell_width();
|
||||
int rows = cr.h / mono_cell_height();
|
||||
|
||||
KlogState* klog = (KlogState*)montauk::malloc(sizeof(KlogState));
|
||||
montauk::memset(klog, 0, sizeof(KlogState));
|
||||
|
||||
// Initialize with scrollback enabled
|
||||
terminal_init_cells(&klog->term, cols, rows, TERM_MAX_SCROLLBACK);
|
||||
|
||||
// Allocate klog read buffer
|
||||
klog->klog_buf = (char*)montauk::malloc(KLOG_READ_SIZE);
|
||||
klog->last_len = 0;
|
||||
klog->last_tail_byte = 0;
|
||||
klog->last_poll_ms = 0;
|
||||
|
||||
// Do an initial read to show existing log content
|
||||
int n = (int)montauk::read_klog(klog->klog_buf, KLOG_READ_SIZE);
|
||||
if (n > 0) {
|
||||
klog_refeed(klog, n);
|
||||
klog->last_len = n;
|
||||
klog->last_tail_byte = klog->klog_buf[n - 1];
|
||||
}
|
||||
|
||||
win->app_data = klog;
|
||||
win->on_draw = klog_on_draw;
|
||||
win->on_mouse = klog_on_mouse;
|
||||
win->on_key = klog_on_key;
|
||||
win->on_close = klog_on_close;
|
||||
win->on_poll = klog_on_poll;
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
/*
|
||||
* app_mandelbrot.cpp
|
||||
* MontaukOS Desktop - Mandelbrot set visualizer
|
||||
* Supports zoom (scroll wheel), pan (drag), and reset (R key)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "apps_common.hpp"
|
||||
|
||||
// ============================================================================
|
||||
// Fixed-point Mandelbrot (avoids FPU/SSE dependency issues)
|
||||
// Uses 28.36 fixed-point: 36 fractional bits gives ~1e-10 precision
|
||||
// ============================================================================
|
||||
|
||||
using fp_t = int64_t;
|
||||
static constexpr int FP_SHIFT = 36;
|
||||
static constexpr fp_t FP_ONE = (fp_t)1 << FP_SHIFT;
|
||||
|
||||
static inline fp_t fp_from_int(int v) { return (fp_t)v << FP_SHIFT; }
|
||||
|
||||
static inline fp_t fp_mul(fp_t a, fp_t b) {
|
||||
// Split to avoid overflow: a * b >> SHIFT
|
||||
// Use 128-bit intermediate via compiler builtin
|
||||
__int128 r = (__int128)a * b;
|
||||
return (fp_t)(r >> FP_SHIFT);
|
||||
}
|
||||
|
||||
// fp_div for small numerators (avoids __divti3 by using 64-bit division)
|
||||
// Only valid when a is small enough that a << FP_SHIFT fits in int64_t
|
||||
static inline fp_t fp_div_small(int a, int b) {
|
||||
return ((fp_t)a << FP_SHIFT) / (fp_t)b;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// State
|
||||
// ============================================================================
|
||||
|
||||
static constexpr int MB_MAX_ITER = 256;
|
||||
static constexpr int MB_TOOLBAR_H = 32;
|
||||
|
||||
struct MandelbrotState {
|
||||
DesktopState* desktop;
|
||||
fp_t center_x, center_y; // center of view in fractal coords
|
||||
fp_t scale; // units per pixel (fixed-point)
|
||||
int max_iter;
|
||||
bool needs_render;
|
||||
// Drag state
|
||||
bool dragging;
|
||||
int drag_start_x, drag_start_y;
|
||||
fp_t drag_start_cx, drag_start_cy;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Color palette
|
||||
// ============================================================================
|
||||
|
||||
static uint32_t mandelbrot_color(int iter, int max_iter) {
|
||||
if (iter >= max_iter) return 0xFF000000; // black for inside set
|
||||
|
||||
// Smooth cycling palette using bit manipulation
|
||||
int t = (iter * 7) & 0xFF;
|
||||
int phase = (iter * 7) >> 8;
|
||||
|
||||
uint8_t r, g, b;
|
||||
switch (phase % 6) {
|
||||
case 0: r = 255; g = (uint8_t)t; b = 0; break;
|
||||
case 1: r = (uint8_t)(255 - t); g = 255; b = 0; break;
|
||||
case 2: r = 0; g = 255; b = (uint8_t)t; break;
|
||||
case 3: r = 0; g = (uint8_t)(255 - t); b = 255; break;
|
||||
case 4: r = (uint8_t)t; g = 0; b = 255; break;
|
||||
default: r = 255; g = 0; b = (uint8_t)(255 - t); break;
|
||||
}
|
||||
|
||||
return 0xFF000000 | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Render
|
||||
// ============================================================================
|
||||
|
||||
static void mandelbrot_render(MandelbrotState* mb, uint32_t* pixels, int w, int h) {
|
||||
int render_h = h - MB_TOOLBAR_H;
|
||||
if (render_h <= 0) return;
|
||||
|
||||
fp_t half_w = fp_mul(fp_from_int(w / 2), mb->scale);
|
||||
fp_t half_h = fp_mul(fp_from_int(render_h / 2), mb->scale);
|
||||
fp_t x_min = mb->center_x - half_w;
|
||||
fp_t y_min = mb->center_y - half_h;
|
||||
fp_t bailout = fp_from_int(4);
|
||||
|
||||
for (int py = 0; py < render_h; py++) {
|
||||
fp_t ci = y_min + fp_mul(fp_from_int(py), mb->scale);
|
||||
uint32_t* row = pixels + (py + MB_TOOLBAR_H) * w;
|
||||
|
||||
for (int px = 0; px < w; px++) {
|
||||
fp_t cr = x_min + fp_mul(fp_from_int(px), mb->scale);
|
||||
|
||||
fp_t zr = 0, zi = 0;
|
||||
int iter = 0;
|
||||
|
||||
while (iter < mb->max_iter) {
|
||||
fp_t zr2 = fp_mul(zr, zr);
|
||||
fp_t zi2 = fp_mul(zi, zi);
|
||||
if (zr2 + zi2 > bailout) break;
|
||||
fp_t new_zr = zr2 - zi2 + cr;
|
||||
zi = fp_mul(fp_from_int(2), fp_mul(zr, zi)) + ci;
|
||||
zr = new_zr;
|
||||
iter++;
|
||||
}
|
||||
|
||||
row[px] = mandelbrot_color(iter, mb->max_iter);
|
||||
}
|
||||
}
|
||||
|
||||
mb->needs_render = false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Callbacks
|
||||
// ============================================================================
|
||||
|
||||
static void mb_on_draw(Window* win, Framebuffer& fb) {
|
||||
MandelbrotState* mb = (MandelbrotState*)win->app_data;
|
||||
if (!mb) return;
|
||||
|
||||
Canvas c(win);
|
||||
|
||||
if (mb->needs_render) {
|
||||
mandelbrot_render(mb, win->content, c.w, c.h);
|
||||
}
|
||||
|
||||
// Draw toolbar over the render
|
||||
c.fill_rect(0, 0, c.w, MB_TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5));
|
||||
c.hline(0, MB_TOOLBAR_H - 1, c.w, colors::BORDER);
|
||||
|
||||
// Reset button
|
||||
c.button(8, 4, 60, 24, "Reset", colors::ACCENT, colors::WHITE, 4);
|
||||
|
||||
// Iter +/- buttons
|
||||
char iter_str[24];
|
||||
snprintf(iter_str, sizeof(iter_str), "Iter: %d", mb->max_iter);
|
||||
int fh = system_font_height();
|
||||
c.text(80, (MB_TOOLBAR_H - fh) / 2, iter_str, colors::TEXT_COLOR);
|
||||
|
||||
int iter_text_w = text_width(iter_str);
|
||||
int btn_x = 80 + iter_text_w + 8;
|
||||
c.button(btn_x, 4, 24, 24, "-", Color::from_rgb(0xAA, 0xAA, 0xAA), colors::WHITE, 4);
|
||||
c.button(btn_x + 28, 4, 24, 24, "+", Color::from_rgb(0xAA, 0xAA, 0xAA), colors::WHITE, 4);
|
||||
|
||||
// Hint
|
||||
const char* hint = "Scroll=zoom Drag=pan";
|
||||
int hw = text_width(hint);
|
||||
c.text(c.w - hw - 8, (MB_TOOLBAR_H - fh) / 2, hint, Color::from_rgb(0x99, 0x99, 0x99));
|
||||
}
|
||||
|
||||
static void mb_on_mouse(Window* win, MouseEvent& ev) {
|
||||
MandelbrotState* mb = (MandelbrotState*)win->app_data;
|
||||
if (!mb) return;
|
||||
|
||||
Rect cr = win->content_rect();
|
||||
int lx = ev.x - cr.x;
|
||||
int ly = ev.y - cr.y;
|
||||
|
||||
// Handle toolbar clicks
|
||||
if (ev.left_pressed() && ly < MB_TOOLBAR_H) {
|
||||
// Reset button
|
||||
Rect reset_r = {8, 4, 60, 24};
|
||||
if (reset_r.contains(lx, ly)) {
|
||||
mb->center_x = -fp_from_int(1) / 2; // -0.5
|
||||
mb->center_y = 0;
|
||||
mb->scale = fp_div_small(3, cr.w > 0 ? cr.w : 400);
|
||||
mb->max_iter = MB_MAX_ITER;
|
||||
mb->needs_render = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Iter buttons
|
||||
char iter_str[24];
|
||||
snprintf(iter_str, sizeof(iter_str), "Iter: %d", mb->max_iter);
|
||||
int iter_text_w = text_width(iter_str);
|
||||
int btn_x = 80 + iter_text_w + 8;
|
||||
|
||||
Rect minus_r = {btn_x, 4, 24, 24};
|
||||
Rect plus_r = {btn_x + 28, 4, 24, 24};
|
||||
|
||||
if (minus_r.contains(lx, ly)) {
|
||||
if (mb->max_iter > 32) { mb->max_iter /= 2; mb->needs_render = true; }
|
||||
return;
|
||||
}
|
||||
if (plus_r.contains(lx, ly)) {
|
||||
if (mb->max_iter < 4096) { mb->max_iter *= 2; mb->needs_render = true; }
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Drag panning
|
||||
if (ev.left_pressed() && ly >= MB_TOOLBAR_H) {
|
||||
mb->dragging = true;
|
||||
mb->drag_start_x = lx;
|
||||
mb->drag_start_y = ly;
|
||||
mb->drag_start_cx = mb->center_x;
|
||||
mb->drag_start_cy = mb->center_y;
|
||||
return;
|
||||
}
|
||||
|
||||
if (mb->dragging && ev.left_held()) {
|
||||
int dx = lx - mb->drag_start_x;
|
||||
int dy = ly - mb->drag_start_y;
|
||||
mb->center_x = mb->drag_start_cx - fp_mul(fp_from_int(dx), mb->scale);
|
||||
mb->center_y = mb->drag_start_cy - fp_mul(fp_from_int(dy), mb->scale);
|
||||
mb->needs_render = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (mb->dragging && !ev.left_held()) {
|
||||
mb->dragging = false;
|
||||
}
|
||||
|
||||
// Scroll zoom (centered on mouse position)
|
||||
if (ev.scroll != 0 && ly >= MB_TOOLBAR_H) {
|
||||
int render_h = cr.h - MB_TOOLBAR_H;
|
||||
// Mouse position in fractal coords before zoom
|
||||
fp_t mx_frac = mb->center_x + fp_mul(fp_from_int(lx - cr.w / 2), mb->scale);
|
||||
fp_t my_frac = mb->center_y + fp_mul(fp_from_int((ly - MB_TOOLBAR_H) - render_h / 2), mb->scale);
|
||||
|
||||
if (ev.scroll < 0) {
|
||||
// Zoom in
|
||||
mb->scale = fp_mul(mb->scale, fp_from_int(3) / 4); // * 0.75
|
||||
} else {
|
||||
// Zoom out
|
||||
mb->scale = fp_mul(mb->scale, fp_from_int(4) / 3); // * 1.33
|
||||
}
|
||||
|
||||
// Adjust center so mouse stays at same fractal point
|
||||
fp_t new_mx = mb->center_x + fp_mul(fp_from_int(lx - cr.w / 2), mb->scale);
|
||||
fp_t new_my = mb->center_y + fp_mul(fp_from_int((ly - MB_TOOLBAR_H) - render_h / 2), mb->scale);
|
||||
mb->center_x += mx_frac - new_mx;
|
||||
mb->center_y += my_frac - new_my;
|
||||
|
||||
mb->needs_render = true;
|
||||
}
|
||||
}
|
||||
|
||||
static void mb_on_key(Window* win, const Montauk::KeyEvent& key) {
|
||||
MandelbrotState* mb = (MandelbrotState*)win->app_data;
|
||||
if (!mb || !key.pressed) return;
|
||||
|
||||
if (key.ascii == 'r' || key.ascii == 'R') {
|
||||
Rect cr = win->content_rect();
|
||||
mb->center_x = -fp_from_int(1) / 2;
|
||||
mb->center_y = 0;
|
||||
mb->scale = fp_div_small(3, cr.w > 0 ? cr.w : 400);
|
||||
mb->max_iter = MB_MAX_ITER;
|
||||
mb->needs_render = true;
|
||||
} else if (key.ascii == '+' || key.ascii == '=') {
|
||||
if (mb->max_iter < 4096) { mb->max_iter *= 2; mb->needs_render = true; }
|
||||
} else if (key.ascii == '-') {
|
||||
if (mb->max_iter > 32) { mb->max_iter /= 2; mb->needs_render = true; }
|
||||
}
|
||||
}
|
||||
|
||||
static void mb_on_close(Window* win) {
|
||||
if (win->app_data) {
|
||||
montauk::mfree(win->app_data);
|
||||
win->app_data = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Mandelbrot launcher
|
||||
// ============================================================================
|
||||
|
||||
void open_mandelbrot(DesktopState* ds) {
|
||||
int idx = desktop_create_window(ds, "Mandelbrot", 120, 60, 500, 400);
|
||||
if (idx < 0) return;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
Rect cr = win->content_rect();
|
||||
|
||||
MandelbrotState* mb = (MandelbrotState*)montauk::malloc(sizeof(MandelbrotState));
|
||||
montauk::memset(mb, 0, sizeof(MandelbrotState));
|
||||
mb->desktop = ds;
|
||||
mb->center_x = -fp_from_int(1) / 2; // -0.5
|
||||
mb->center_y = 0;
|
||||
mb->scale = fp_div_small(3, cr.w > 0 ? cr.w : 400);
|
||||
mb->max_iter = MB_MAX_ITER;
|
||||
mb->needs_render = true;
|
||||
mb->dragging = false;
|
||||
|
||||
win->app_data = mb;
|
||||
win->on_draw = mb_on_draw;
|
||||
win->on_mouse = mb_on_mouse;
|
||||
win->on_key = mb_on_key;
|
||||
win->on_close = mb_on_close;
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
* app_procmgr.cpp
|
||||
* MontaukOS Desktop - Process Manager
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "apps_common.hpp"
|
||||
|
||||
// ============================================================================
|
||||
// Process Manager state
|
||||
// ============================================================================
|
||||
|
||||
static constexpr int PM_TOOLBAR_H = 36;
|
||||
static constexpr int PM_HEADER_H = 24;
|
||||
static constexpr int PM_ITEM_H = 28;
|
||||
static constexpr int PM_MAX_PROCS = 16;
|
||||
static constexpr int PM_POLL_MS = 1000;
|
||||
|
||||
struct ProcMgrState {
|
||||
DesktopState* desktop;
|
||||
Montauk::ProcInfo procs[PM_MAX_PROCS];
|
||||
int proc_count;
|
||||
int selected; // selected row index (-1 = none)
|
||||
uint64_t last_poll_ms;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Callbacks
|
||||
// ============================================================================
|
||||
|
||||
static void procmgr_on_poll(Window* win) {
|
||||
ProcMgrState* pm = (ProcMgrState*)win->app_data;
|
||||
if (!pm) return;
|
||||
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
if (now - pm->last_poll_ms < PM_POLL_MS) return;
|
||||
pm->last_poll_ms = now;
|
||||
|
||||
// Remember previously selected PID
|
||||
int prev_pid = -1;
|
||||
if (pm->selected >= 0 && pm->selected < pm->proc_count) {
|
||||
prev_pid = pm->procs[pm->selected].pid;
|
||||
}
|
||||
|
||||
pm->proc_count = montauk::proclist(pm->procs, PM_MAX_PROCS);
|
||||
|
||||
// Restore selection by matching PID
|
||||
pm->selected = -1;
|
||||
if (prev_pid >= 0) {
|
||||
for (int i = 0; i < pm->proc_count; i++) {
|
||||
if (pm->procs[i].pid == prev_pid) {
|
||||
pm->selected = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void procmgr_on_draw(Window* win, Framebuffer& fb) {
|
||||
ProcMgrState* pm = (ProcMgrState*)win->app_data;
|
||||
if (!pm) return;
|
||||
|
||||
Canvas c(win);
|
||||
c.fill(colors::WINDOW_BG);
|
||||
|
||||
int fh = system_font_height();
|
||||
|
||||
// --- Toolbar ---
|
||||
c.fill_rect(0, 0, c.w, PM_TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5));
|
||||
c.hline(0, PM_TOOLBAR_H - 1, c.w, colors::BORDER);
|
||||
|
||||
// "End Process" button
|
||||
int btn_w = 100;
|
||||
int btn_h = 26;
|
||||
int btn_x = 8;
|
||||
int btn_y = (PM_TOOLBAR_H - btn_h) / 2;
|
||||
Color btn_bg = (pm->selected >= 0 && pm->procs[pm->selected].pid != 0)
|
||||
? Color::from_rgb(0xCC, 0x33, 0x33)
|
||||
: Color::from_rgb(0xAA, 0xAA, 0xAA);
|
||||
c.button(btn_x, btn_y, btn_w, btn_h, "End Process", btn_bg, colors::WHITE, 4);
|
||||
|
||||
// Process count on right
|
||||
char count_str[24];
|
||||
snprintf(count_str, sizeof(count_str), "%d processes", pm->proc_count);
|
||||
int cw = text_width(count_str);
|
||||
c.text(c.w - cw - 12, (PM_TOOLBAR_H - fh) / 2, count_str, colors::TEXT_COLOR);
|
||||
|
||||
// --- Header row ---
|
||||
int header_y = PM_TOOLBAR_H;
|
||||
c.fill_rect(0, header_y, c.w, PM_HEADER_H, Color::from_rgb(0xF0, 0xF0, 0xF0));
|
||||
|
||||
int col_pid = 12;
|
||||
int col_name = 64;
|
||||
int col_mem = c.w - 120;
|
||||
int col_state = c.w - 52;
|
||||
|
||||
int ty = header_y + (PM_HEADER_H - fh) / 2;
|
||||
c.text(col_pid, ty, "PID", Color::from_rgb(0x66, 0x66, 0x66));
|
||||
c.text(col_name, ty, "Name", Color::from_rgb(0x66, 0x66, 0x66));
|
||||
c.text(col_mem, ty, "Mem", Color::from_rgb(0x66, 0x66, 0x66));
|
||||
c.text(col_state, ty, "St", Color::from_rgb(0x66, 0x66, 0x66));
|
||||
|
||||
c.hline(0, header_y + PM_HEADER_H - 1, c.w, colors::BORDER);
|
||||
|
||||
// --- Process rows ---
|
||||
int list_y = PM_TOOLBAR_H + PM_HEADER_H;
|
||||
for (int i = 0; i < pm->proc_count; i++) {
|
||||
int row_y = list_y + i * PM_ITEM_H;
|
||||
if (row_y + PM_ITEM_H > c.h) break;
|
||||
|
||||
// Selected row highlight
|
||||
if (i == pm->selected) {
|
||||
c.fill_rect(0, row_y, c.w, PM_ITEM_H, colors::MENU_HOVER);
|
||||
}
|
||||
|
||||
int ry = row_y + (PM_ITEM_H - fh) / 2;
|
||||
|
||||
// PID (right-aligned in PID column)
|
||||
char pid_str[8];
|
||||
snprintf(pid_str, sizeof(pid_str), "%d", (int)pm->procs[i].pid);
|
||||
int pid_w = text_width(pid_str);
|
||||
c.text(col_pid + 32 - pid_w, ry, pid_str, colors::TEXT_COLOR);
|
||||
|
||||
// Name (truncated to fit)
|
||||
c.text(col_name, ry, pm->procs[i].name, colors::TEXT_COLOR);
|
||||
|
||||
// Memory
|
||||
char mem_str[16];
|
||||
format_size(mem_str, (int)pm->procs[i].heapUsed);
|
||||
c.text(col_mem, ry, mem_str, colors::TEXT_COLOR);
|
||||
|
||||
// State
|
||||
const char* st_str;
|
||||
Color st_color;
|
||||
switch (pm->procs[i].state) {
|
||||
case 2: // Running
|
||||
st_str = "Run";
|
||||
st_color = Color::from_rgb(0x22, 0x88, 0x22);
|
||||
break;
|
||||
case 1: // Ready
|
||||
st_str = "Rdy";
|
||||
st_color = Color::from_rgb(0x33, 0x66, 0xCC);
|
||||
break;
|
||||
case 3: // Terminated
|
||||
st_str = "Term";
|
||||
st_color = Color::from_rgb(0xCC, 0x33, 0x33);
|
||||
break;
|
||||
default:
|
||||
st_str = "?";
|
||||
st_color = colors::TEXT_COLOR;
|
||||
break;
|
||||
}
|
||||
c.text(col_state, ry, st_str, st_color);
|
||||
}
|
||||
}
|
||||
|
||||
static void procmgr_on_mouse(Window* win, MouseEvent& ev) {
|
||||
ProcMgrState* pm = (ProcMgrState*)win->app_data;
|
||||
if (!pm) return;
|
||||
|
||||
Rect cr = win->content_rect();
|
||||
int lx = ev.x - cr.x;
|
||||
int ly = ev.y - cr.y;
|
||||
|
||||
if (ev.left_pressed()) {
|
||||
// Check "End Process" button click
|
||||
int btn_w = 100;
|
||||
int btn_h = 26;
|
||||
int btn_x = 8;
|
||||
int btn_y = (PM_TOOLBAR_H - btn_h) / 2;
|
||||
Rect btn_rect = {btn_x, btn_y, btn_w, btn_h};
|
||||
if (btn_rect.contains(lx, ly)) {
|
||||
if (pm->selected >= 0 && pm->selected < pm->proc_count
|
||||
&& pm->procs[pm->selected].pid != 0) {
|
||||
montauk::kill(pm->procs[pm->selected].pid);
|
||||
pm->last_poll_ms = 0; // force refresh
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check row click
|
||||
int list_y = PM_TOOLBAR_H + PM_HEADER_H;
|
||||
if (ly >= list_y) {
|
||||
int row = (ly - list_y) / PM_ITEM_H;
|
||||
if (row >= 0 && row < pm->proc_count) {
|
||||
pm->selected = row;
|
||||
} else {
|
||||
pm->selected = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void procmgr_on_key(Window* win, const Montauk::KeyEvent& key) {
|
||||
ProcMgrState* pm = (ProcMgrState*)win->app_data;
|
||||
if (!pm || !key.pressed) return;
|
||||
|
||||
if (key.scancode == 0x48) { // Up arrow
|
||||
if (pm->selected > 0) pm->selected--;
|
||||
else if (pm->proc_count > 0) pm->selected = 0;
|
||||
} else if (key.scancode == 0x50) { // Down arrow
|
||||
if (pm->selected < pm->proc_count - 1) pm->selected++;
|
||||
} else if (key.scancode == 0x53) { // Delete key
|
||||
if (pm->selected >= 0 && pm->selected < pm->proc_count
|
||||
&& pm->procs[pm->selected].pid != 0) {
|
||||
montauk::kill(pm->procs[pm->selected].pid);
|
||||
pm->last_poll_ms = 0; // force refresh
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void procmgr_on_close(Window* win) {
|
||||
if (win->app_data) {
|
||||
montauk::mfree(win->app_data);
|
||||
win->app_data = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Process Manager launcher
|
||||
// ============================================================================
|
||||
|
||||
void open_procmgr(DesktopState* ds) {
|
||||
int idx = desktop_create_window(ds, "Processes", 180, 80, 520, 400);
|
||||
if (idx < 0) return;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
|
||||
ProcMgrState* pm = (ProcMgrState*)montauk::malloc(sizeof(ProcMgrState));
|
||||
montauk::memset(pm, 0, sizeof(ProcMgrState));
|
||||
pm->desktop = ds;
|
||||
pm->selected = -1;
|
||||
pm->last_poll_ms = 0;
|
||||
|
||||
// Initial poll
|
||||
pm->proc_count = montauk::proclist(pm->procs, PM_MAX_PROCS);
|
||||
|
||||
win->app_data = pm;
|
||||
win->on_draw = procmgr_on_draw;
|
||||
win->on_mouse = procmgr_on_mouse;
|
||||
win->on_key = procmgr_on_key;
|
||||
win->on_close = procmgr_on_close;
|
||||
win->on_poll = procmgr_on_poll;
|
||||
}
|
||||
@@ -0,0 +1,608 @@
|
||||
/*
|
||||
* app_settings.cpp
|
||||
* MontaukOS Desktop - Settings application
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "apps_common.hpp"
|
||||
#include "../wallpaper.hpp"
|
||||
|
||||
// ============================================================================
|
||||
// Settings state
|
||||
// ============================================================================
|
||||
|
||||
struct SettingsState {
|
||||
DesktopState* desktop;
|
||||
int active_tab; // 0=Appearance, 1=Display, 2=About
|
||||
Montauk::SysInfo sys_info;
|
||||
uint64_t uptime_ms;
|
||||
WallpaperFileList wp_files;
|
||||
bool wp_scanned;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Color palette presets
|
||||
// ============================================================================
|
||||
|
||||
static constexpr int SWATCH_COUNT = 8;
|
||||
static constexpr int SWATCH_SIZE = 24;
|
||||
static constexpr int SWATCH_GAP = 6;
|
||||
|
||||
// Background colors (light tones)
|
||||
static const Color bg_palette[SWATCH_COUNT] = {
|
||||
Color::from_rgb(0xD0, 0xD8, 0xE8), // light blue-gray (default)
|
||||
Color::from_rgb(0xE8, 0xDD, 0xCB), // warm beige
|
||||
Color::from_rgb(0xC8, 0xE6, 0xD0), // mint green
|
||||
Color::from_rgb(0xD8, 0xD0, 0xE8), // lavender
|
||||
Color::from_rgb(0xB8, 0xBE, 0xC8), // slate
|
||||
Color::from_rgb(0xF0, 0xF0, 0xF0), // white
|
||||
Color::from_rgb(0xE8, 0xD0, 0xD8), // soft pink
|
||||
Color::from_rgb(0xE8, 0xE0, 0xC8), // light gold
|
||||
};
|
||||
|
||||
// Panel colors (dark tones)
|
||||
static const Color panel_palette[SWATCH_COUNT] = {
|
||||
Color::from_rgb(0x2B, 0x3E, 0x50), // dark blue-gray (default)
|
||||
Color::from_rgb(0x2D, 0x2D, 0x2D), // dark charcoal
|
||||
Color::from_rgb(0x1B, 0x2A, 0x4A), // navy
|
||||
Color::from_rgb(0x1A, 0x3A, 0x3A), // dark teal
|
||||
Color::from_rgb(0x1A, 0x3A, 0x1A), // dark green
|
||||
Color::from_rgb(0x30, 0x20, 0x40), // dark purple
|
||||
Color::from_rgb(0x40, 0x1A, 0x1A), // dark red
|
||||
Color::from_rgb(0x10, 0x10, 0x10), // black
|
||||
};
|
||||
|
||||
// Accent colors
|
||||
static const Color accent_palette[SWATCH_COUNT] = {
|
||||
Color::from_rgb(0x36, 0x7B, 0xF0), // blue (default)
|
||||
Color::from_rgb(0x00, 0x9B, 0x9B), // teal
|
||||
Color::from_rgb(0x2E, 0x9E, 0x3E), // green
|
||||
Color::from_rgb(0xE0, 0x8A, 0x20), // orange
|
||||
Color::from_rgb(0xD0, 0x3E, 0x3E), // red
|
||||
Color::from_rgb(0x7B, 0x3E, 0xB8), // purple
|
||||
Color::from_rgb(0xD0, 0x5C, 0x9E), // pink
|
||||
Color::from_rgb(0x44, 0x44, 0xCC), // indigo
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Layout constants
|
||||
// ============================================================================
|
||||
|
||||
static constexpr int TAB_BAR_H = 36;
|
||||
static constexpr int TAB_COUNT = 3;
|
||||
static const char* tab_labels[TAB_COUNT] = { "Appearance", "Display", "About" };
|
||||
|
||||
// ============================================================================
|
||||
// Helper: check if two colors match
|
||||
// ============================================================================
|
||||
|
||||
static bool color_eq(Color a, Color b) {
|
||||
return a.r == b.r && a.g == b.g && a.b == b.b;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper: find selected swatch index in a palette
|
||||
// ============================================================================
|
||||
|
||||
static int find_swatch(const Color* palette, Color current) {
|
||||
for (int i = 0; i < SWATCH_COUNT; i++) {
|
||||
if (color_eq(palette[i], current)) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Drawing
|
||||
// ============================================================================
|
||||
|
||||
static void draw_swatch_row(Canvas& c, int x, int y, const Color* palette,
|
||||
Color selected, Color accent) {
|
||||
int sel = find_swatch(palette, selected);
|
||||
for (int i = 0; i < SWATCH_COUNT; i++) {
|
||||
int sx = x + i * (SWATCH_SIZE + SWATCH_GAP);
|
||||
// Draw selection border
|
||||
if (i == sel) {
|
||||
c.fill_rounded_rect(sx - 2, y - 2, SWATCH_SIZE + 4, SWATCH_SIZE + 4, 4, accent);
|
||||
}
|
||||
c.fill_rounded_rect(sx, y, SWATCH_SIZE, SWATCH_SIZE, 3, palette[i]);
|
||||
// Thin border for light colors
|
||||
c.rect(sx, y, SWATCH_SIZE, SWATCH_SIZE, Color::from_rgb(0xCC, 0xCC, 0xCC));
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_radio(Canvas& c, int x, int y, bool selected, Color accent) {
|
||||
int r = 7;
|
||||
// Outer circle (simple square approximation with rounded rect)
|
||||
c.fill_rounded_rect(x, y, r * 2, r * 2, r, Color::from_rgb(0xCC, 0xCC, 0xCC));
|
||||
c.fill_rounded_rect(x + 1, y + 1, r * 2 - 2, r * 2 - 2, r - 1, colors::WHITE);
|
||||
if (selected) {
|
||||
c.fill_rounded_rect(x + 4, y + 4, r * 2 - 8, r * 2 - 8, r - 4, accent);
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_toggle_btn(Canvas& c, int x, int y, int bw, int bh,
|
||||
const char* label, bool active, Color accent) {
|
||||
Color bg = active ? accent : colors::WINDOW_BG;
|
||||
Color fg = active ? colors::WHITE : colors::TEXT_COLOR;
|
||||
c.fill_rounded_rect(x, y, bw, bh, 4, bg);
|
||||
if (!active) {
|
||||
c.rect(x, y, bw, bh, colors::BORDER);
|
||||
}
|
||||
int tw = text_width(label);
|
||||
int fh = system_font_height();
|
||||
c.text(x + (bw - tw) / 2, y + (bh - fh) / 2, label, fg);
|
||||
}
|
||||
|
||||
static constexpr int WP_ITEM_H = 24;
|
||||
|
||||
static void settings_draw_appearance(Canvas& c, SettingsState* st) {
|
||||
DesktopSettings& s = st->desktop->settings;
|
||||
Color accent = s.accent_color;
|
||||
Color dim = Color::from_rgb(0x88, 0x88, 0x88);
|
||||
int x = 16;
|
||||
int y = 12;
|
||||
int sfh = system_font_height();
|
||||
int line_h = sfh + 10;
|
||||
|
||||
// Section: Background
|
||||
c.text(x, y, "Background", colors::TEXT_COLOR);
|
||||
y += line_h;
|
||||
|
||||
// Radio buttons: Gradient / Solid Color / Image
|
||||
bool mode_grad = s.bg_gradient && !s.bg_image;
|
||||
bool mode_solid = !s.bg_gradient && !s.bg_image;
|
||||
bool mode_image = s.bg_image;
|
||||
|
||||
draw_radio(c, x, y, mode_grad, accent);
|
||||
c.text(x + 20, y + 2, "Gradient", colors::TEXT_COLOR);
|
||||
|
||||
draw_radio(c, x + 120, y, mode_solid, accent);
|
||||
c.text(x + 140, y + 2, "Solid", colors::TEXT_COLOR);
|
||||
|
||||
draw_radio(c, x + 220, y, mode_image, accent);
|
||||
c.text(x + 240, y + 2, "Image", colors::TEXT_COLOR);
|
||||
y += line_h + 4;
|
||||
|
||||
if (mode_image) {
|
||||
// Scan for images lazily
|
||||
if (!st->wp_scanned) {
|
||||
wallpaper_scan_dir("0:/home", &st->wp_files);
|
||||
st->wp_scanned = true;
|
||||
}
|
||||
|
||||
c.text(x, y, "Images in 0:/home/", dim);
|
||||
y += sfh + 6;
|
||||
|
||||
if (st->wp_files.count == 0) {
|
||||
c.text(x + 8, y, "No .jpg files found", dim);
|
||||
y += WP_ITEM_H;
|
||||
} else {
|
||||
for (int i = 0; i < st->wp_files.count && i < 8; i++) {
|
||||
// Build full path for comparison
|
||||
char fullpath[256];
|
||||
montauk::strcpy(fullpath, "0:/home/");
|
||||
str_append(fullpath, st->wp_files.names[i], 256);
|
||||
|
||||
bool selected = s.bg_image &&
|
||||
montauk::streq(s.bg_image_path, fullpath);
|
||||
|
||||
if (selected) {
|
||||
c.fill_rounded_rect(x, y, c.w - 2 * x, WP_ITEM_H, 3, accent);
|
||||
}
|
||||
|
||||
// Truncate long filenames
|
||||
char label[40];
|
||||
int nlen = montauk::slen(st->wp_files.names[i]);
|
||||
if (nlen > 35) {
|
||||
montauk::strncpy(label, st->wp_files.names[i], 32);
|
||||
label[32] = '.';
|
||||
label[33] = '.';
|
||||
label[34] = '.';
|
||||
label[35] = '\0';
|
||||
} else {
|
||||
montauk::strncpy(label, st->wp_files.names[i], 39);
|
||||
}
|
||||
|
||||
Color tc = selected ? colors::WHITE : colors::TEXT_COLOR;
|
||||
c.text(x + 8, y + (WP_ITEM_H - sfh) / 2, label, tc);
|
||||
y += WP_ITEM_H;
|
||||
}
|
||||
}
|
||||
y += 6;
|
||||
} else if (mode_grad) {
|
||||
// Top color swatches
|
||||
c.text(x, y + 4, "Top", dim);
|
||||
draw_swatch_row(c, x + 70, y, bg_palette, s.bg_grad_top, accent);
|
||||
y += SWATCH_SIZE + 14;
|
||||
|
||||
// Bottom color swatches
|
||||
c.text(x, y + 4, "Bottom", dim);
|
||||
draw_swatch_row(c, x + 70, y, bg_palette, s.bg_grad_bottom, accent);
|
||||
y += SWATCH_SIZE + 14;
|
||||
} else {
|
||||
// Solid color swatches
|
||||
c.text(x, y + 4, "Color", dim);
|
||||
draw_swatch_row(c, x + 70, y, bg_palette, s.bg_solid, accent);
|
||||
y += SWATCH_SIZE + 14;
|
||||
}
|
||||
|
||||
// Separator
|
||||
c.hline(x, y, c.w - 2 * x, colors::BORDER);
|
||||
y += 12;
|
||||
|
||||
// Panel color
|
||||
c.text(x, y + 4, "Panel Color", colors::TEXT_COLOR);
|
||||
draw_swatch_row(c, x + 110, y, panel_palette, s.panel_color, accent);
|
||||
y += SWATCH_SIZE + 14;
|
||||
|
||||
// Separator
|
||||
c.hline(x, y, c.w - 2 * x, colors::BORDER);
|
||||
y += 12;
|
||||
|
||||
// Accent color
|
||||
c.text(x, y + 4, "Accent Color", colors::TEXT_COLOR);
|
||||
draw_swatch_row(c, x + 110, y, accent_palette, s.accent_color, accent);
|
||||
}
|
||||
|
||||
static void apply_ui_scale(int scale) {
|
||||
switch (scale) {
|
||||
case 0: fonts::UI_SIZE=14; fonts::TITLE_SIZE=14; fonts::TERM_SIZE=14; fonts::LARGE_SIZE=22; break;
|
||||
case 2: fonts::UI_SIZE=22; fonts::TITLE_SIZE=22; fonts::TERM_SIZE=22; fonts::LARGE_SIZE=34; break;
|
||||
default: fonts::UI_SIZE=18; fonts::TITLE_SIZE=18; fonts::TERM_SIZE=18; fonts::LARGE_SIZE=28; break;
|
||||
}
|
||||
}
|
||||
|
||||
static void settings_draw_display(Canvas& c, SettingsState* st) {
|
||||
DesktopSettings& s = st->desktop->settings;
|
||||
Color accent = s.accent_color;
|
||||
int x = 16;
|
||||
int y = 20;
|
||||
int btn_w = 60;
|
||||
int btn_h = 28;
|
||||
|
||||
// Window Shadows
|
||||
c.text(x, y + 6, "Window Shadows", colors::TEXT_COLOR);
|
||||
int bx = x + 180;
|
||||
draw_toggle_btn(c, bx, y, btn_w, btn_h, "On", s.show_shadows, accent);
|
||||
draw_toggle_btn(c, bx + btn_w + 8, y, btn_w, btn_h, "Off", !s.show_shadows, accent);
|
||||
y += btn_h + 20;
|
||||
|
||||
// Separator
|
||||
c.hline(x, y, c.w - 2 * x, colors::BORDER);
|
||||
y += 16;
|
||||
|
||||
// Clock Format
|
||||
c.text(x, y + 6, "Clock Format", colors::TEXT_COLOR);
|
||||
bx = x + 180;
|
||||
draw_toggle_btn(c, bx, y, btn_w, btn_h, "24h", s.clock_24h, accent);
|
||||
draw_toggle_btn(c, bx + btn_w + 8, y, btn_w, btn_h, "12h", !s.clock_24h, accent);
|
||||
y += btn_h + 20;
|
||||
|
||||
// Separator
|
||||
c.hline(x, y, c.w - 2 * x, colors::BORDER);
|
||||
y += 16;
|
||||
|
||||
// UI Scale
|
||||
c.text(x, y + 6, "UI Scale", colors::TEXT_COLOR);
|
||||
bx = x + 180;
|
||||
int sbw = 68;
|
||||
draw_toggle_btn(c, bx, y, sbw, btn_h, "Small", s.ui_scale == 0, accent);
|
||||
draw_toggle_btn(c, bx + sbw + 8, y, sbw, btn_h, "Default", s.ui_scale == 1, accent);
|
||||
draw_toggle_btn(c, bx + (sbw + 8) * 2, y, sbw, btn_h, "Large", s.ui_scale == 2, accent);
|
||||
}
|
||||
|
||||
static void settings_draw_about(Canvas& c, SettingsState* st) {
|
||||
st->uptime_ms = montauk::get_milliseconds();
|
||||
|
||||
Color dim = Color::from_rgb(0x88, 0x88, 0x88);
|
||||
int x = 16;
|
||||
int y = 20;
|
||||
char line[128];
|
||||
int sfh = system_font_height();
|
||||
int line_h = sfh + 6;
|
||||
|
||||
// OS name in 2x size
|
||||
c.text_2x(x, y, st->sys_info.osName, st->desktop->settings.accent_color);
|
||||
int large_h = (fonts::system_font && fonts::system_font->valid)
|
||||
? fonts::system_font->get_line_height(fonts::LARGE_SIZE) : (FONT_HEIGHT * 2);
|
||||
y += large_h + 8;
|
||||
|
||||
snprintf(line, sizeof(line), "Version %s", st->sys_info.osVersion);
|
||||
c.text(x, y, line, colors::TEXT_COLOR);
|
||||
y += line_h + 8;
|
||||
|
||||
c.hline(x, y, c.w - 2 * x, colors::BORDER);
|
||||
y += 12;
|
||||
|
||||
snprintf(line, sizeof(line), "API version: %d", (int)st->sys_info.apiVersion);
|
||||
c.kv_line(x, &y, line, colors::TEXT_COLOR, line_h);
|
||||
|
||||
int up_sec = (int)(st->uptime_ms / 1000);
|
||||
int up_min = up_sec / 60;
|
||||
int up_hr = up_min / 60;
|
||||
snprintf(line, sizeof(line), "Uptime: %d:%02d:%02d", up_hr, up_min % 60, up_sec % 60);
|
||||
c.kv_line(x, &y, line, colors::TEXT_COLOR, line_h);
|
||||
|
||||
// snprintf(line, sizeof(line), "Build: %s %s", __DATE__, __TIME__);
|
||||
// c.text(x, y, line, colors::TEXT_COLOR);
|
||||
y += 8;
|
||||
|
||||
c.hline(x, y, c.w - 2 * x, colors::BORDER);
|
||||
y += 12;
|
||||
|
||||
c.text(x, y, "Copyright (c) 2026 Daniel Hammer", dim);
|
||||
}
|
||||
|
||||
static void settings_on_draw(Window* win, Framebuffer& fb) {
|
||||
SettingsState* st = (SettingsState*)win->app_data;
|
||||
if (!st) return;
|
||||
|
||||
Canvas c(win);
|
||||
c.fill(colors::WINDOW_BG);
|
||||
|
||||
Color accent = st->desktop->settings.accent_color;
|
||||
int sfh = system_font_height();
|
||||
|
||||
// Draw tab bar background
|
||||
c.fill_rect(0, 0, c.w, TAB_BAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5));
|
||||
c.hline(0, TAB_BAR_H - 1, c.w, colors::BORDER);
|
||||
|
||||
// Draw tabs
|
||||
int tab_w = c.w / TAB_COUNT;
|
||||
for (int i = 0; i < TAB_COUNT; i++) {
|
||||
int tx = i * tab_w;
|
||||
bool active = (i == st->active_tab);
|
||||
|
||||
if (active) {
|
||||
c.fill_rect(tx, 0, tab_w, TAB_BAR_H, colors::WINDOW_BG);
|
||||
// Active tab underline
|
||||
c.fill_rect(tx + 4, TAB_BAR_H - 3, tab_w - 8, 3, accent);
|
||||
}
|
||||
|
||||
int tw = text_width(tab_labels[i]);
|
||||
Color tc = active ? accent : Color::from_rgb(0x66, 0x66, 0x66);
|
||||
c.text(tx + (tab_w - tw) / 2, (TAB_BAR_H - sfh) / 2, tab_labels[i], tc);
|
||||
}
|
||||
|
||||
// Draw tab content below the tab bar
|
||||
// Create a sub-canvas offset by TAB_BAR_H
|
||||
Canvas content(win->content + TAB_BAR_H * win->content_w,
|
||||
win->content_w, win->content_h - TAB_BAR_H);
|
||||
|
||||
switch (st->active_tab) {
|
||||
case 0: settings_draw_appearance(content, st); break;
|
||||
case 1: settings_draw_display(content, st); break;
|
||||
case 2: settings_draw_about(content, st); break;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Mouse interaction
|
||||
// ============================================================================
|
||||
|
||||
static bool swatch_hit(int mx, int my, int row_x, int row_y, int* out_idx) {
|
||||
for (int i = 0; i < SWATCH_COUNT; i++) {
|
||||
int sx = row_x + i * (SWATCH_SIZE + SWATCH_GAP);
|
||||
if (mx >= sx && mx < sx + SWATCH_SIZE && my >= row_y && my < row_y + SWATCH_SIZE) {
|
||||
*out_idx = i;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void settings_on_mouse(Window* win, MouseEvent& ev) {
|
||||
SettingsState* st = (SettingsState*)win->app_data;
|
||||
if (!st) return;
|
||||
|
||||
if (!ev.left_pressed()) return;
|
||||
|
||||
Rect cr = win->content_rect();
|
||||
int mx = ev.x - cr.x;
|
||||
int my = ev.y - cr.y;
|
||||
|
||||
// Tab bar click
|
||||
if (my >= 0 && my < TAB_BAR_H) {
|
||||
int tab_w = win->content_w / TAB_COUNT;
|
||||
int tab = mx / tab_w;
|
||||
if (tab >= 0 && tab < TAB_COUNT) {
|
||||
st->active_tab = tab;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Content area (offset by TAB_BAR_H)
|
||||
int cy = my - TAB_BAR_H;
|
||||
DesktopSettings& s = st->desktop->settings;
|
||||
|
||||
if (st->active_tab == 0) {
|
||||
// Appearance tab
|
||||
int x = 16;
|
||||
int sfh = system_font_height();
|
||||
int line_h = sfh + 10;
|
||||
int y = 12;
|
||||
|
||||
bool mode_grad = s.bg_gradient && !s.bg_image;
|
||||
bool mode_image = s.bg_image;
|
||||
|
||||
// "Background" label
|
||||
y += line_h;
|
||||
|
||||
// Radio: Gradient
|
||||
if (mx >= x && mx < x + 100 && cy >= y && cy < y + 16) {
|
||||
s.bg_gradient = true;
|
||||
s.bg_image = false;
|
||||
return;
|
||||
}
|
||||
// Radio: Solid
|
||||
if (mx >= x + 120 && mx < x + 210 && cy >= y && cy < y + 16) {
|
||||
s.bg_gradient = false;
|
||||
s.bg_image = false;
|
||||
return;
|
||||
}
|
||||
// Radio: Image
|
||||
if (mx >= x + 220 && mx < x + 320 && cy >= y && cy < y + 16) {
|
||||
s.bg_image = true;
|
||||
s.bg_gradient = false;
|
||||
if (!st->wp_scanned) {
|
||||
wallpaper_scan_dir("0:/home", &st->wp_files);
|
||||
st->wp_scanned = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
y += line_h + 4;
|
||||
|
||||
int idx;
|
||||
if (mode_image) {
|
||||
// "Images in 0:/home/" label
|
||||
y += sfh + 6;
|
||||
|
||||
// File list clicks
|
||||
for (int i = 0; i < st->wp_files.count && i < 8; i++) {
|
||||
if (cy >= y && cy < y + WP_ITEM_H &&
|
||||
mx >= x && mx < win->content_w - x) {
|
||||
// Build full path and load wallpaper
|
||||
char fullpath[256];
|
||||
montauk::strcpy(fullpath, "0:/home/");
|
||||
str_append(fullpath, st->wp_files.names[i], 256);
|
||||
wallpaper_load(&s, fullpath,
|
||||
st->desktop->screen_w, st->desktop->screen_h);
|
||||
return;
|
||||
}
|
||||
y += WP_ITEM_H;
|
||||
}
|
||||
if (st->wp_files.count == 0) y += WP_ITEM_H;
|
||||
y += 6;
|
||||
} else if (mode_grad) {
|
||||
// Top swatches
|
||||
if (swatch_hit(mx, cy, x + 70, y, &idx)) {
|
||||
s.bg_grad_top = bg_palette[idx];
|
||||
return;
|
||||
}
|
||||
y += SWATCH_SIZE + 14;
|
||||
|
||||
// Bottom swatches
|
||||
if (swatch_hit(mx, cy, x + 70, y, &idx)) {
|
||||
s.bg_grad_bottom = bg_palette[idx];
|
||||
return;
|
||||
}
|
||||
y += SWATCH_SIZE + 14;
|
||||
} else {
|
||||
// Solid color swatches
|
||||
if (swatch_hit(mx, cy, x + 70, y, &idx)) {
|
||||
s.bg_solid = bg_palette[idx];
|
||||
return;
|
||||
}
|
||||
y += SWATCH_SIZE + 14;
|
||||
}
|
||||
|
||||
// Separator
|
||||
y += 12;
|
||||
|
||||
// Panel color swatches
|
||||
if (swatch_hit(mx, cy, x + 110, y, &idx)) {
|
||||
s.panel_color = panel_palette[idx];
|
||||
return;
|
||||
}
|
||||
y += SWATCH_SIZE + 14;
|
||||
|
||||
// Separator
|
||||
y += 12;
|
||||
|
||||
// Accent color swatches
|
||||
if (swatch_hit(mx, cy, x + 110, y, &idx)) {
|
||||
s.accent_color = accent_palette[idx];
|
||||
return;
|
||||
}
|
||||
} else if (st->active_tab == 1) {
|
||||
// Display tab
|
||||
int x = 16;
|
||||
int y = 20;
|
||||
int btn_w = 60;
|
||||
int btn_h = 28;
|
||||
int bx = x + 180;
|
||||
|
||||
// Window Shadows: On
|
||||
if (mx >= bx && mx < bx + btn_w && cy >= y && cy < y + btn_h) {
|
||||
s.show_shadows = true;
|
||||
return;
|
||||
}
|
||||
// Window Shadows: Off
|
||||
if (mx >= bx + btn_w + 8 && mx < bx + btn_w * 2 + 8 && cy >= y && cy < y + btn_h) {
|
||||
s.show_shadows = false;
|
||||
return;
|
||||
}
|
||||
y += btn_h + 20 + 16;
|
||||
|
||||
// Clock: 24h
|
||||
if (mx >= bx && mx < bx + btn_w && cy >= y && cy < y + btn_h) {
|
||||
s.clock_24h = true;
|
||||
return;
|
||||
}
|
||||
// Clock: 12h
|
||||
if (mx >= bx + btn_w + 8 && mx < bx + btn_w * 2 + 8 && cy >= y && cy < y + btn_h) {
|
||||
s.clock_24h = false;
|
||||
return;
|
||||
}
|
||||
y += btn_h + 20 + 16;
|
||||
|
||||
// UI Scale buttons
|
||||
int sbw = 68;
|
||||
// Small
|
||||
if (mx >= bx && mx < bx + sbw && cy >= y && cy < y + btn_h) {
|
||||
s.ui_scale = 0;
|
||||
apply_ui_scale(0);
|
||||
montauk::win_setscale(0);
|
||||
return;
|
||||
}
|
||||
// Default
|
||||
if (mx >= bx + sbw + 8 && mx < bx + sbw * 2 + 8 && cy >= y && cy < y + btn_h) {
|
||||
s.ui_scale = 1;
|
||||
apply_ui_scale(1);
|
||||
montauk::win_setscale(1);
|
||||
return;
|
||||
}
|
||||
// Large
|
||||
if (mx >= bx + (sbw + 8) * 2 && mx < bx + sbw * 3 + 16 && cy >= y && cy < y + btn_h) {
|
||||
s.ui_scale = 2;
|
||||
apply_ui_scale(2);
|
||||
montauk::win_setscale(2);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Cleanup
|
||||
// ============================================================================
|
||||
|
||||
static void settings_on_close(Window* win) {
|
||||
if (win->app_data) {
|
||||
montauk::mfree(win->app_data);
|
||||
win->app_data = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Settings launcher
|
||||
// ============================================================================
|
||||
|
||||
void open_settings(DesktopState* ds) {
|
||||
int idx = desktop_create_window(ds, "Settings", 200, 100, 480, 420);
|
||||
if (idx < 0) return;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
SettingsState* st = (SettingsState*)montauk::malloc(sizeof(SettingsState));
|
||||
montauk::memset(st, 0, sizeof(SettingsState));
|
||||
st->desktop = ds;
|
||||
st->active_tab = 0;
|
||||
montauk::get_info(&st->sys_info);
|
||||
st->uptime_ms = montauk::get_milliseconds();
|
||||
st->wp_scanned = false;
|
||||
st->wp_files.count = 0;
|
||||
|
||||
win->app_data = st;
|
||||
win->on_draw = settings_on_draw;
|
||||
win->on_mouse = settings_on_mouse;
|
||||
win->on_close = settings_on_close;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* app_spreadsheet.cpp
|
||||
* MontaukOS Desktop - Spreadsheet launcher (spawns standalone spreadsheet.elf)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "apps_common.hpp"
|
||||
|
||||
void open_spreadsheet(DesktopState* ds) {
|
||||
(void)ds;
|
||||
montauk::spawn("0:/os/spreadsheet.elf");
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* app_sysinfo.cpp
|
||||
* MontaukOS Desktop - System Info application
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "apps_common.hpp"
|
||||
|
||||
// ============================================================================
|
||||
// System Info state and callbacks
|
||||
// ============================================================================
|
||||
|
||||
struct SysInfoState {
|
||||
Montauk::SysInfo sys_info;
|
||||
Montauk::NetCfg net_cfg;
|
||||
uint64_t uptime_ms;
|
||||
};
|
||||
|
||||
static void sysinfo_on_draw(Window* win, Framebuffer& fb) {
|
||||
SysInfoState* si = (SysInfoState*)win->app_data;
|
||||
if (!si) return;
|
||||
|
||||
si->uptime_ms = montauk::get_milliseconds();
|
||||
|
||||
Canvas c(win);
|
||||
c.fill(colors::WINDOW_BG);
|
||||
|
||||
int y = 16;
|
||||
int x = 16;
|
||||
char line[128];
|
||||
|
||||
// Title
|
||||
c.text(x, y, "System Information", colors::ACCENT);
|
||||
y += system_font_height() + 12;
|
||||
|
||||
// Separator
|
||||
c.hline(x, y, c.w - 2 * x, colors::BORDER);
|
||||
y += 8;
|
||||
|
||||
// OS Name
|
||||
snprintf(line, sizeof(line), "OS: %s", si->sys_info.osName);
|
||||
c.kv_line(x, &y, line, colors::TEXT_COLOR);
|
||||
|
||||
// OS Version
|
||||
snprintf(line, sizeof(line), "Version: %s", si->sys_info.osVersion);
|
||||
c.kv_line(x, &y, line, colors::TEXT_COLOR);
|
||||
|
||||
// API Version
|
||||
snprintf(line, sizeof(line), "API: %d", (int)si->sys_info.apiVersion);
|
||||
c.kv_line(x, &y, line, colors::TEXT_COLOR);
|
||||
|
||||
// Max Processes
|
||||
snprintf(line, sizeof(line), "Max PIDs: %d", (int)si->sys_info.maxProcesses);
|
||||
c.text(x, y, line, colors::TEXT_COLOR);
|
||||
y += system_font_height() + 12;
|
||||
|
||||
// Uptime
|
||||
int up_sec = (int)(si->uptime_ms / 1000);
|
||||
int up_min = up_sec / 60;
|
||||
int up_hr = up_min / 60;
|
||||
snprintf(line, sizeof(line), "Uptime: %d:%02d:%02d", up_hr, up_min % 60, up_sec % 60);
|
||||
c.text(x, y, line, colors::TEXT_COLOR);
|
||||
y += system_font_height() + 12;
|
||||
|
||||
// Network section
|
||||
c.text(x, y, "Network", colors::ACCENT);
|
||||
y += system_font_height() + 8;
|
||||
|
||||
c.hline(x, y, c.w - 2 * x, colors::BORDER);
|
||||
y += 8;
|
||||
|
||||
// IP Address
|
||||
uint32_t ip = si->net_cfg.ipAddress;
|
||||
snprintf(line, sizeof(line), "IP: %d.%d.%d.%d",
|
||||
(int)(ip & 0xFF), (int)((ip >> 8) & 0xFF),
|
||||
(int)((ip >> 16) & 0xFF), (int)((ip >> 24) & 0xFF));
|
||||
c.kv_line(x, &y, line, colors::TEXT_COLOR);
|
||||
|
||||
// Subnet
|
||||
uint32_t mask = si->net_cfg.subnetMask;
|
||||
snprintf(line, sizeof(line), "Subnet: %d.%d.%d.%d",
|
||||
(int)(mask & 0xFF), (int)((mask >> 8) & 0xFF),
|
||||
(int)((mask >> 16) & 0xFF), (int)((mask >> 24) & 0xFF));
|
||||
c.kv_line(x, &y, line, colors::TEXT_COLOR);
|
||||
|
||||
// Gateway
|
||||
uint32_t gw = si->net_cfg.gateway;
|
||||
snprintf(line, sizeof(line), "Gateway: %d.%d.%d.%d",
|
||||
(int)(gw & 0xFF), (int)((gw >> 8) & 0xFF),
|
||||
(int)((gw >> 16) & 0xFF), (int)((gw >> 24) & 0xFF));
|
||||
c.kv_line(x, &y, line, colors::TEXT_COLOR);
|
||||
|
||||
// DNS
|
||||
uint32_t dns = si->net_cfg.dnsServer;
|
||||
snprintf(line, sizeof(line), "DNS: %d.%d.%d.%d",
|
||||
(int)(dns & 0xFF), (int)((dns >> 8) & 0xFF),
|
||||
(int)((dns >> 16) & 0xFF), (int)((dns >> 24) & 0xFF));
|
||||
c.kv_line(x, &y, line, colors::TEXT_COLOR);
|
||||
|
||||
// MAC Address
|
||||
snprintf(line, sizeof(line), "MAC: %02x:%02x:%02x:%02x:%02x:%02x",
|
||||
(unsigned)si->net_cfg.macAddress[0], (unsigned)si->net_cfg.macAddress[1],
|
||||
(unsigned)si->net_cfg.macAddress[2], (unsigned)si->net_cfg.macAddress[3],
|
||||
(unsigned)si->net_cfg.macAddress[4], (unsigned)si->net_cfg.macAddress[5]);
|
||||
c.text(x, y, line, colors::TEXT_COLOR);
|
||||
}
|
||||
|
||||
static void sysinfo_on_close(Window* win) {
|
||||
if (win->app_data) {
|
||||
montauk::mfree(win->app_data);
|
||||
win->app_data = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// System Info launcher
|
||||
// ============================================================================
|
||||
|
||||
void open_sysinfo(DesktopState* ds) {
|
||||
int idx = desktop_create_window(ds, "System Info", 300, 100, 400, 380);
|
||||
if (idx < 0) return;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
SysInfoState* si = (SysInfoState*)montauk::malloc(sizeof(SysInfoState));
|
||||
montauk::memset(si, 0, sizeof(SysInfoState));
|
||||
montauk::get_info(&si->sys_info);
|
||||
montauk::get_netcfg(&si->net_cfg);
|
||||
si->uptime_ms = montauk::get_milliseconds();
|
||||
|
||||
win->app_data = si;
|
||||
win->on_draw = sysinfo_on_draw;
|
||||
win->on_mouse = nullptr;
|
||||
win->on_key = nullptr;
|
||||
win->on_close = sysinfo_on_close;
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
/*
|
||||
* app_terminal.cpp
|
||||
* MontaukOS Desktop - Terminal application with tab support
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "apps_common.hpp"
|
||||
|
||||
// ============================================================================
|
||||
// Tab data model
|
||||
// ============================================================================
|
||||
|
||||
static constexpr int TERM_TAB_BAR_H = 34;
|
||||
static constexpr int TERM_MAX_TABS = 8;
|
||||
static constexpr int TERM_TAB_W = 130;
|
||||
static constexpr int TERM_TAB_GAP = 4;
|
||||
static constexpr int TERM_PLUS_W = 28;
|
||||
static constexpr int TERM_PLUS_PAD = 8;
|
||||
|
||||
// Compute the x-offset that centers all tabs within bar_w
|
||||
static int term_tabs_origin(int bar_w, int tab_count) {
|
||||
int total = tab_count * TERM_TAB_W + (tab_count - 1) * TERM_TAB_GAP;
|
||||
int x = (bar_w - total) / 2;
|
||||
if (x < 4) x = 4;
|
||||
return x;
|
||||
}
|
||||
|
||||
struct TermTabState {
|
||||
TerminalState* tabs[TERM_MAX_TABS];
|
||||
int tab_count;
|
||||
int active_tab;
|
||||
int prev_active_tab; // track tab switches to force redraw
|
||||
uint32_t* last_content; // detect content buffer reallocation
|
||||
DesktopState* desktop;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Tab helpers
|
||||
// ============================================================================
|
||||
|
||||
static void term_free_tab(TerminalState* ts) {
|
||||
if (!ts) return;
|
||||
if (ts->child_pid > 0) montauk::kill(ts->child_pid);
|
||||
if (ts->cells) montauk::free(ts->cells);
|
||||
if (ts->alt_cells) montauk::free(ts->alt_cells);
|
||||
montauk::mfree(ts);
|
||||
}
|
||||
|
||||
static TerminalState* term_create_tab(DesktopState* ds, int cols, int rows) {
|
||||
TerminalState* ts = (TerminalState*)montauk::malloc(sizeof(TerminalState));
|
||||
montauk::memset(ts, 0, sizeof(TerminalState));
|
||||
terminal_init(ts, cols, rows);
|
||||
ts->desktop = ds;
|
||||
return ts;
|
||||
}
|
||||
|
||||
static void term_close_tab(TermTabState* tts, Window* win, int idx) {
|
||||
if (idx < 0 || idx >= tts->tab_count) return;
|
||||
|
||||
term_free_tab(tts->tabs[idx]);
|
||||
|
||||
// Shift remaining tabs left
|
||||
for (int i = idx; i < tts->tab_count - 1; i++)
|
||||
tts->tabs[i] = tts->tabs[i + 1];
|
||||
tts->tab_count--;
|
||||
tts->tabs[tts->tab_count] = nullptr;
|
||||
|
||||
if (tts->tab_count == 0) {
|
||||
// Last tab closed — close the window
|
||||
DesktopState* ds = tts->desktop;
|
||||
if (ds) {
|
||||
for (int i = 0; i < ds->window_count; i++) {
|
||||
if (&ds->windows[i] == win) {
|
||||
desktop_close_window(ds, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Adjust active tab index
|
||||
if (idx == tts->active_tab) {
|
||||
// Closed the active tab: prefer left neighbor, fall back to right
|
||||
if (tts->active_tab > 0)
|
||||
tts->active_tab--;
|
||||
// else stays at 0 (right neighbor shifted here)
|
||||
} else if (tts->active_tab > idx) {
|
||||
tts->active_tab--;
|
||||
}
|
||||
if (tts->active_tab >= tts->tab_count)
|
||||
tts->active_tab = tts->tab_count - 1;
|
||||
|
||||
// Mark new active tab dirty so it redraws
|
||||
tts->tabs[tts->active_tab]->dirty = true;
|
||||
}
|
||||
|
||||
static void term_add_tab(TermTabState* tts, int cols, int rows) {
|
||||
if (tts->tab_count >= TERM_MAX_TABS) return;
|
||||
TerminalState* ts = term_create_tab(tts->desktop, cols, rows);
|
||||
tts->tabs[tts->tab_count] = ts;
|
||||
tts->active_tab = tts->tab_count;
|
||||
tts->tab_count++;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Terminal callbacks
|
||||
// ============================================================================
|
||||
|
||||
static void terminal_on_draw(Window* win, Framebuffer& fb) {
|
||||
TermTabState* tts = (TermTabState*)win->app_data;
|
||||
if (!tts || tts->tab_count == 0) return;
|
||||
if (tts->active_tab < 0 || tts->active_tab >= tts->tab_count)
|
||||
tts->active_tab = 0;
|
||||
|
||||
Rect cr = win->content_rect();
|
||||
int term_h = cr.h - TERM_TAB_BAR_H;
|
||||
if (term_h < 1) term_h = 1;
|
||||
|
||||
// Force full redraw when active tab changed
|
||||
if (tts->active_tab != tts->prev_active_tab) {
|
||||
tts->tabs[tts->active_tab]->dirty = true;
|
||||
tts->prev_active_tab = tts->active_tab;
|
||||
}
|
||||
|
||||
// Force full redraw when content buffer was reallocated (resize/maximize)
|
||||
if (win->content != tts->last_content) {
|
||||
tts->tabs[tts->active_tab]->dirty = true;
|
||||
tts->last_content = win->content;
|
||||
}
|
||||
|
||||
TerminalState* ts = tts->tabs[tts->active_tab];
|
||||
|
||||
// Check if window was resized and update terminal grid
|
||||
int new_cols = cr.w / mono_cell_width();
|
||||
int new_rows = term_h / mono_cell_height();
|
||||
if (new_cols < 1) new_cols = 1;
|
||||
if (new_rows < 1) new_rows = 1;
|
||||
if (new_cols != ts->cols || new_rows != ts->rows) {
|
||||
for (int i = 0; i < tts->tab_count; i++)
|
||||
terminal_resize(tts->tabs[i], new_cols, new_rows);
|
||||
}
|
||||
|
||||
// Only redraw when terminal content changed
|
||||
if (!ts->dirty) return;
|
||||
|
||||
// ---- Draw tab bar ----
|
||||
// Bar is darker than TERM_BG; active tab matches TERM_BG to merge with content
|
||||
Canvas c(win->content, cr.w, cr.h);
|
||||
Color bar_bg = Color::from_hex(0x1C1C1C);
|
||||
c.fill_rect(0, 0, cr.w, TERM_TAB_BAR_H, bar_bg);
|
||||
|
||||
int fh = system_font_height();
|
||||
int tab_x = term_tabs_origin(cr.w, tts->tab_count);
|
||||
|
||||
for (int i = 0; i < tts->tab_count; i++) {
|
||||
bool active = (i == tts->active_tab);
|
||||
char label[16];
|
||||
snprintf(label, sizeof(label), "Tab %d", i + 1);
|
||||
|
||||
if (active) {
|
||||
// Active tab: TERM_BG pill with squared-off bottom → merges into content
|
||||
int ty = 5;
|
||||
int th = TERM_TAB_BAR_H - ty;
|
||||
c.fill_rounded_rect(tab_x, ty, TERM_TAB_W, th, 6, colors::TERM_BG);
|
||||
c.fill_rect(tab_x, TERM_TAB_BAR_H - 6, TERM_TAB_W, 6, colors::TERM_BG);
|
||||
|
||||
int text_y = ty + (th - fh) / 2;
|
||||
c.text(tab_x + 12, text_y, label, Color::from_hex(0xE0E0E0));
|
||||
c.text(tab_x + TERM_TAB_W - 20, text_y, "x", Color::from_hex(0x707070));
|
||||
} else {
|
||||
// Inactive tab: subtle rounded pill, sits above the bottom edge
|
||||
int ty = 7;
|
||||
int th = 22;
|
||||
c.fill_rounded_rect(tab_x, ty, TERM_TAB_W, th, 5, Color::from_hex(0x262626));
|
||||
|
||||
int text_y = ty + (th - fh) / 2;
|
||||
c.text(tab_x + 12, text_y, label, Color::from_hex(0x6E6E6E));
|
||||
c.text(tab_x + TERM_TAB_W - 20, text_y, "x", Color::from_hex(0x444444));
|
||||
}
|
||||
|
||||
tab_x += TERM_TAB_W + TERM_TAB_GAP;
|
||||
}
|
||||
|
||||
// "+" button pinned to right edge
|
||||
if (tts->tab_count < TERM_MAX_TABS) {
|
||||
int plus_h = 22;
|
||||
int py = 7;
|
||||
int px = cr.w - TERM_PLUS_PAD - TERM_PLUS_W;
|
||||
c.fill_rounded_rect(px, py, TERM_PLUS_W, plus_h, 5, Color::from_hex(0x262626));
|
||||
int pw_text = text_width("+");
|
||||
c.text(px + (TERM_PLUS_W - pw_text) / 2, py + (plus_h - fh) / 2, "+",
|
||||
Color::from_hex(0x6E6E6E));
|
||||
}
|
||||
|
||||
// ---- Render active terminal into offset buffer ----
|
||||
uint32_t* term_pixels = win->content + TERM_TAB_BAR_H * cr.w;
|
||||
terminal_render(ts, term_pixels, cr.w, term_h);
|
||||
|
||||
// Draw scrollbar overlay when scrollback exists
|
||||
if (ts->scrollback_lines > 0 && !ts->alt_screen_active) {
|
||||
int cell_h = mono_cell_height();
|
||||
int total_rows = ts->scrollback_lines + ts->rows;
|
||||
int total_h = total_rows * cell_h;
|
||||
int view_h = term_h;
|
||||
if (total_h > view_h) {
|
||||
Canvas sc(term_pixels, cr.w, term_h);
|
||||
int sb_w = 4;
|
||||
int sb_x = cr.w - sb_w - 2;
|
||||
int thumb_h = (view_h * view_h) / total_h;
|
||||
if (thumb_h < 20) thumb_h = 20;
|
||||
int scroll_from_top = ts->scrollback_lines - ts->view_offset;
|
||||
int max_scroll = ts->scrollback_lines;
|
||||
int thumb_y = (max_scroll > 0)
|
||||
? (scroll_from_top * (view_h - thumb_h)) / max_scroll : 0;
|
||||
sc.fill_rect(sb_x, 0, sb_w, view_h, Color::from_rgb(0x33, 0x33, 0x33));
|
||||
sc.fill_rect(sb_x, thumb_y, sb_w, thumb_h, Color::from_rgb(0x88, 0x88, 0x88));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void terminal_on_mouse(Window* win, MouseEvent& ev) {
|
||||
TermTabState* tts = (TermTabState*)win->app_data;
|
||||
if (!tts || tts->tab_count == 0) return;
|
||||
|
||||
// Scroll wheel: forward to active tab regardless of mouse position
|
||||
TerminalState* ts = tts->tabs[tts->active_tab];
|
||||
if (ev.scroll != 0 && !ts->alt_screen_active) {
|
||||
ts->view_offset += (ev.scroll < 0) ? 3 : -3;
|
||||
if (ts->view_offset < 0) ts->view_offset = 0;
|
||||
if (ts->view_offset > ts->scrollback_lines)
|
||||
ts->view_offset = ts->scrollback_lines;
|
||||
ts->dirty = true;
|
||||
}
|
||||
|
||||
// Tab bar click handling
|
||||
if (!ev.left_pressed()) return;
|
||||
|
||||
Rect cr = win->content_rect();
|
||||
int lx = ev.x - cr.x;
|
||||
int ly = ev.y - cr.y;
|
||||
|
||||
if (ly >= TERM_TAB_BAR_H) return; // click in terminal area, ignore
|
||||
|
||||
int tab_x = term_tabs_origin(cr.w, tts->tab_count);
|
||||
for (int i = 0; i < tts->tab_count; i++) {
|
||||
if (lx >= tab_x && lx < tab_x + TERM_TAB_W) {
|
||||
// Check if click is on the close "x" (last 24px of tab)
|
||||
if (lx >= tab_x + TERM_TAB_W - 24) {
|
||||
term_close_tab(tts, win, i);
|
||||
} else if (i != tts->active_tab) {
|
||||
tts->active_tab = i;
|
||||
tts->tabs[i]->dirty = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
tab_x += TERM_TAB_W + TERM_TAB_GAP;
|
||||
}
|
||||
|
||||
// "+" button pinned to right edge
|
||||
int px = cr.w - TERM_PLUS_PAD - TERM_PLUS_W;
|
||||
if (tts->tab_count < TERM_MAX_TABS && lx >= px && lx < px + TERM_PLUS_W) {
|
||||
int term_h = cr.h - TERM_TAB_BAR_H;
|
||||
int cols = cr.w / mono_cell_width();
|
||||
int rows = term_h / mono_cell_height();
|
||||
if (cols < 1) cols = 1;
|
||||
if (rows < 1) rows = 1;
|
||||
term_add_tab(tts, cols, rows);
|
||||
}
|
||||
}
|
||||
|
||||
static void terminal_on_key(Window* win, const Montauk::KeyEvent& key) {
|
||||
TermTabState* tts = (TermTabState*)win->app_data;
|
||||
if (!tts || tts->tab_count == 0) return;
|
||||
|
||||
// Ctrl+T: new tab (scancode 0x14 = 'T')
|
||||
if (key.ctrl && key.pressed && key.scancode == 0x14) {
|
||||
if (tts->tab_count < TERM_MAX_TABS) {
|
||||
Rect cr = win->content_rect();
|
||||
int term_h = cr.h - TERM_TAB_BAR_H;
|
||||
int cols = cr.w / mono_cell_width();
|
||||
int rows = term_h / mono_cell_height();
|
||||
if (cols < 1) cols = 1;
|
||||
if (rows < 1) rows = 1;
|
||||
term_add_tab(tts, cols, rows);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+W: close current tab (scancode 0x11 = 'W')
|
||||
if (key.ctrl && key.pressed && key.scancode == 0x11) {
|
||||
term_close_tab(tts, win, tts->active_tab);
|
||||
return;
|
||||
}
|
||||
|
||||
// Forward all other keys to active tab
|
||||
TerminalState* ts = tts->tabs[tts->active_tab];
|
||||
terminal_handle_key(ts, key);
|
||||
}
|
||||
|
||||
static void terminal_on_close(Window* win) {
|
||||
TermTabState* tts = (TermTabState*)win->app_data;
|
||||
if (tts) {
|
||||
for (int i = 0; i < tts->tab_count; i++)
|
||||
term_free_tab(tts->tabs[i]);
|
||||
montauk::mfree(tts);
|
||||
win->app_data = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
static void terminal_on_poll(Window* win) {
|
||||
TermTabState* tts = (TermTabState*)win->app_data;
|
||||
if (!tts) return;
|
||||
|
||||
// Poll ALL tabs — background shells still produce output
|
||||
for (int i = tts->tab_count - 1; i >= 0; i--) {
|
||||
if (!terminal_poll(tts->tabs[i])) {
|
||||
// This tab's shell exited
|
||||
term_close_tab(tts, win, i);
|
||||
// term_close_tab may have closed the window (last tab).
|
||||
// desktop_close_window calls on_close which nulls app_data
|
||||
// before the window slot is recycled, so win is still valid here.
|
||||
if (!win->app_data) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Terminal launcher
|
||||
// ============================================================================
|
||||
|
||||
void open_terminal(DesktopState* ds) {
|
||||
int idx = desktop_create_window(ds, "Terminal", 200, 80, 648, 480);
|
||||
if (idx < 0) return;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
Rect cr = win->content_rect();
|
||||
int term_h = cr.h - TERM_TAB_BAR_H;
|
||||
int cols = cr.w / mono_cell_width();
|
||||
int rows = term_h / mono_cell_height();
|
||||
if (cols < 1) cols = 1;
|
||||
if (rows < 1) rows = 1;
|
||||
|
||||
TermTabState* tts = (TermTabState*)montauk::malloc(sizeof(TermTabState));
|
||||
montauk::memset(tts, 0, sizeof(TermTabState));
|
||||
tts->desktop = ds;
|
||||
tts->tabs[0] = term_create_tab(ds, cols, rows);
|
||||
tts->tab_count = 1;
|
||||
tts->active_tab = 0;
|
||||
tts->prev_active_tab = 0;
|
||||
tts->last_content = win->content;
|
||||
|
||||
win->app_data = tts;
|
||||
win->on_draw = terminal_on_draw;
|
||||
win->on_mouse = terminal_on_mouse;
|
||||
win->on_key = terminal_on_key;
|
||||
win->on_close = terminal_on_close;
|
||||
win->on_poll = terminal_on_poll;
|
||||
}
|
||||
@@ -0,0 +1,759 @@
|
||||
/*
|
||||
* app_texteditor.cpp
|
||||
* MontaukOS Desktop - Text Editor application
|
||||
* Single-buffer text editor with line numbers, cursor, scrolling, file I/O
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "apps_common.hpp"
|
||||
|
||||
// ============================================================================
|
||||
// Text Editor state
|
||||
// ============================================================================
|
||||
|
||||
static constexpr int TE_TOOLBAR_H = 36;
|
||||
static constexpr int TE_PATHBAR_H = 32;
|
||||
static constexpr int TE_STATUS_H = 24;
|
||||
static constexpr int TE_LINE_NUM_W = 48;
|
||||
static constexpr int TE_INIT_CAP = 4096;
|
||||
static constexpr int TE_MAX_CAP = 262144; // 256KB
|
||||
static constexpr int TE_MAX_LINES = 16384;
|
||||
static constexpr int TE_TAB_WIDTH = 4;
|
||||
|
||||
struct TextEditorState {
|
||||
char* buffer;
|
||||
int buf_len;
|
||||
int buf_cap;
|
||||
int* line_offsets;
|
||||
int line_count;
|
||||
int cursor_pos; // byte position in buffer
|
||||
int cursor_line;
|
||||
int cursor_col;
|
||||
int scroll_y; // first visible line
|
||||
int scroll_x; // horizontal scroll in pixels
|
||||
bool modified;
|
||||
char filepath[256];
|
||||
char filename[64];
|
||||
DesktopState* desktop;
|
||||
|
||||
bool show_pathbar;
|
||||
char pathbar_text[256];
|
||||
int pathbar_cursor;
|
||||
int pathbar_len;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Line index management
|
||||
// ============================================================================
|
||||
|
||||
static void te_recompute_lines(TextEditorState* te) {
|
||||
if (!te->line_offsets) {
|
||||
te->line_offsets = (int*)montauk::malloc(TE_MAX_LINES * sizeof(int));
|
||||
}
|
||||
|
||||
te->line_count = 0;
|
||||
te->line_offsets[te->line_count++] = 0;
|
||||
|
||||
for (int i = 0; i < te->buf_len; i++) {
|
||||
if (te->buffer[i] == '\n' && te->line_count < TE_MAX_LINES) {
|
||||
te->line_offsets[te->line_count++] = i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void te_update_cursor_pos(TextEditorState* te) {
|
||||
// Find which line the cursor is on
|
||||
te->cursor_line = 0;
|
||||
for (int i = te->line_count - 1; i >= 0; i--) {
|
||||
if (te->cursor_pos >= te->line_offsets[i]) {
|
||||
te->cursor_line = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
te->cursor_col = te->cursor_pos - te->line_offsets[te->cursor_line];
|
||||
}
|
||||
|
||||
static int te_line_length(TextEditorState* te, int line) {
|
||||
if (line < 0 || line >= te->line_count) return 0;
|
||||
int start = te->line_offsets[line];
|
||||
int end;
|
||||
if (line + 1 < te->line_count) {
|
||||
end = te->line_offsets[line + 1] - 1; // exclude newline
|
||||
} else {
|
||||
end = te->buf_len;
|
||||
}
|
||||
return end - start;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Buffer operations
|
||||
// ============================================================================
|
||||
|
||||
static void te_ensure_capacity(TextEditorState* te, int needed) {
|
||||
if (te->buf_len + needed <= te->buf_cap) return;
|
||||
int new_cap = te->buf_cap * 2;
|
||||
if (new_cap > TE_MAX_CAP) new_cap = TE_MAX_CAP;
|
||||
if (new_cap < te->buf_len + needed) new_cap = te->buf_len + needed;
|
||||
te->buffer = (char*)montauk::realloc(te->buffer, new_cap);
|
||||
te->buf_cap = new_cap;
|
||||
}
|
||||
|
||||
static void te_insert_char(TextEditorState* te, char c) {
|
||||
if (te->buf_len >= TE_MAX_CAP - 1) return;
|
||||
te_ensure_capacity(te, 1);
|
||||
|
||||
// Shift everything after cursor right
|
||||
for (int i = te->buf_len; i > te->cursor_pos; i--) {
|
||||
te->buffer[i] = te->buffer[i - 1];
|
||||
}
|
||||
te->buffer[te->cursor_pos] = c;
|
||||
te->buf_len++;
|
||||
te->cursor_pos++;
|
||||
te->modified = true;
|
||||
|
||||
te_recompute_lines(te);
|
||||
te_update_cursor_pos(te);
|
||||
}
|
||||
|
||||
static void te_insert_string(TextEditorState* te, const char* s, int len) {
|
||||
if (te->buf_len + len >= TE_MAX_CAP) return;
|
||||
te_ensure_capacity(te, len);
|
||||
|
||||
for (int i = te->buf_len - 1; i >= te->cursor_pos; i--) {
|
||||
te->buffer[i + len] = te->buffer[i];
|
||||
}
|
||||
for (int i = 0; i < len; i++) {
|
||||
te->buffer[te->cursor_pos + i] = s[i];
|
||||
}
|
||||
te->buf_len += len;
|
||||
te->cursor_pos += len;
|
||||
te->modified = true;
|
||||
|
||||
te_recompute_lines(te);
|
||||
te_update_cursor_pos(te);
|
||||
}
|
||||
|
||||
static void te_backspace(TextEditorState* te) {
|
||||
if (te->cursor_pos <= 0) return;
|
||||
te->cursor_pos--;
|
||||
for (int i = te->cursor_pos; i < te->buf_len - 1; i++) {
|
||||
te->buffer[i] = te->buffer[i + 1];
|
||||
}
|
||||
te->buf_len--;
|
||||
te->modified = true;
|
||||
|
||||
te_recompute_lines(te);
|
||||
te_update_cursor_pos(te);
|
||||
}
|
||||
|
||||
static void te_delete_char(TextEditorState* te) {
|
||||
if (te->cursor_pos >= te->buf_len) return;
|
||||
for (int i = te->cursor_pos; i < te->buf_len - 1; i++) {
|
||||
te->buffer[i] = te->buffer[i + 1];
|
||||
}
|
||||
te->buf_len--;
|
||||
te->modified = true;
|
||||
|
||||
te_recompute_lines(te);
|
||||
te_update_cursor_pos(te);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Cursor movement
|
||||
// ============================================================================
|
||||
|
||||
static void te_move_up(TextEditorState* te) {
|
||||
if (te->cursor_line <= 0) return;
|
||||
int target_col = te->cursor_col;
|
||||
int prev_line = te->cursor_line - 1;
|
||||
int prev_len = te_line_length(te, prev_line);
|
||||
if (target_col > prev_len) target_col = prev_len;
|
||||
te->cursor_pos = te->line_offsets[prev_line] + target_col;
|
||||
te_update_cursor_pos(te);
|
||||
}
|
||||
|
||||
static void te_move_down(TextEditorState* te) {
|
||||
if (te->cursor_line >= te->line_count - 1) return;
|
||||
int target_col = te->cursor_col;
|
||||
int next_line = te->cursor_line + 1;
|
||||
int next_len = te_line_length(te, next_line);
|
||||
if (target_col > next_len) target_col = next_len;
|
||||
te->cursor_pos = te->line_offsets[next_line] + target_col;
|
||||
te_update_cursor_pos(te);
|
||||
}
|
||||
|
||||
static void te_move_left(TextEditorState* te) {
|
||||
if (te->cursor_pos > 0) {
|
||||
te->cursor_pos--;
|
||||
te_update_cursor_pos(te);
|
||||
}
|
||||
}
|
||||
|
||||
static void te_move_right(TextEditorState* te) {
|
||||
if (te->cursor_pos < te->buf_len) {
|
||||
te->cursor_pos++;
|
||||
te_update_cursor_pos(te);
|
||||
}
|
||||
}
|
||||
|
||||
static void te_move_home(TextEditorState* te) {
|
||||
te->cursor_pos = te->line_offsets[te->cursor_line];
|
||||
te_update_cursor_pos(te);
|
||||
}
|
||||
|
||||
static void te_move_end(TextEditorState* te) {
|
||||
te->cursor_pos = te->line_offsets[te->cursor_line] + te_line_length(te, te->cursor_line);
|
||||
te_update_cursor_pos(te);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Scrolling
|
||||
// ============================================================================
|
||||
|
||||
static void te_ensure_cursor_visible(TextEditorState* te, int visible_lines, int content_w) {
|
||||
if (te->cursor_line < te->scroll_y) {
|
||||
te->scroll_y = te->cursor_line;
|
||||
}
|
||||
if (te->cursor_line >= te->scroll_y + visible_lines) {
|
||||
te->scroll_y = te->cursor_line - visible_lines + 1;
|
||||
}
|
||||
|
||||
// Horizontal scroll
|
||||
int cell_w = mono_cell_width();
|
||||
int cursor_px = te->cursor_col * cell_w;
|
||||
int view_w = content_w - TE_LINE_NUM_W;
|
||||
if (cursor_px - te->scroll_x > view_w - cell_w * 2) {
|
||||
te->scroll_x = cursor_px - view_w + cell_w * 4;
|
||||
}
|
||||
if (cursor_px < te->scroll_x) {
|
||||
te->scroll_x = cursor_px - cell_w * 2;
|
||||
if (te->scroll_x < 0) te->scroll_x = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// File I/O
|
||||
// ============================================================================
|
||||
|
||||
static void te_load_file(TextEditorState* te, const char* path) {
|
||||
int fd = montauk::open(path);
|
||||
if (fd < 0) return;
|
||||
|
||||
uint64_t size = montauk::getsize(fd);
|
||||
if (size > TE_MAX_CAP) size = TE_MAX_CAP;
|
||||
|
||||
if ((int)size >= te->buf_cap) {
|
||||
int new_cap = (int)size + 1024;
|
||||
if (new_cap > TE_MAX_CAP) new_cap = TE_MAX_CAP;
|
||||
te->buffer = (char*)montauk::realloc(te->buffer, new_cap);
|
||||
te->buf_cap = new_cap;
|
||||
}
|
||||
|
||||
montauk::read(fd, (uint8_t*)te->buffer, 0, size);
|
||||
montauk::close(fd);
|
||||
|
||||
te->buf_len = (int)size;
|
||||
te->cursor_pos = 0;
|
||||
te->scroll_y = 0;
|
||||
te->scroll_x = 0;
|
||||
te->modified = false;
|
||||
|
||||
montauk::strncpy(te->filepath, path, 255);
|
||||
|
||||
// Extract filename from path
|
||||
int last_slash = -1;
|
||||
for (int i = 0; path[i]; i++) {
|
||||
if (path[i] == '/') last_slash = i;
|
||||
}
|
||||
if (last_slash >= 0) {
|
||||
montauk::strncpy(te->filename, path + last_slash + 1, 63);
|
||||
} else {
|
||||
montauk::strncpy(te->filename, path, 63);
|
||||
}
|
||||
|
||||
te_recompute_lines(te);
|
||||
te_update_cursor_pos(te);
|
||||
}
|
||||
|
||||
static void te_save_file(TextEditorState* te) {
|
||||
if (te->filepath[0] == '\0') return;
|
||||
|
||||
int fd = montauk::fcreate(te->filepath);
|
||||
if (fd < 0) return;
|
||||
|
||||
montauk::fwrite(fd, (const uint8_t*)te->buffer, 0, te->buf_len);
|
||||
montauk::close(fd);
|
||||
|
||||
te->modified = false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Drawing
|
||||
// ============================================================================
|
||||
|
||||
static void texteditor_on_draw(Window* win, Framebuffer& fb) {
|
||||
TextEditorState* te = (TextEditorState*)win->app_data;
|
||||
if (!te) return;
|
||||
|
||||
Canvas c(win);
|
||||
c.fill(colors::WINDOW_BG);
|
||||
|
||||
int cell_w = mono_cell_width();
|
||||
int cell_h = mono_cell_height();
|
||||
|
||||
// ---- Toolbar (36px) ----
|
||||
c.fill_rect(0, 0, c.w, TE_TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5));
|
||||
|
||||
// Open button
|
||||
Color btn_bg = Color::from_rgb(0xE8, 0xE8, 0xE8);
|
||||
c.fill_rounded_rect(4, 6, 24, 24, 3, btn_bg);
|
||||
if (te->desktop && te->desktop->icon_folder.pixels)
|
||||
c.icon(8, 10, te->desktop->icon_folder);
|
||||
|
||||
// Save button
|
||||
c.fill_rounded_rect(32, 6, 24, 24, 3, btn_bg);
|
||||
if (te->desktop && te->desktop->icon_save.pixels)
|
||||
c.icon(36, 10, te->desktop->icon_save);
|
||||
|
||||
// Vertical separator
|
||||
c.vline(60, 4, 28, colors::BORDER);
|
||||
|
||||
// Filename + modified flag
|
||||
char toolbar_label[128];
|
||||
if (te->filename[0]) {
|
||||
snprintf(toolbar_label, 128, "%s%s", te->filename, te->modified ? " [modified]" : "");
|
||||
} else {
|
||||
snprintf(toolbar_label, 128, "Untitled%s", te->modified ? " [modified]" : "");
|
||||
}
|
||||
int sfh = system_font_height();
|
||||
c.text(68, (TE_TOOLBAR_H - sfh) / 2, toolbar_label, colors::TEXT_COLOR);
|
||||
|
||||
// Toolbar bottom separator
|
||||
c.hline(0, TE_TOOLBAR_H - 1, c.w, colors::BORDER);
|
||||
|
||||
// ---- Path bar (conditional, 32px) ----
|
||||
int editor_y_start = TE_TOOLBAR_H;
|
||||
|
||||
if (te->show_pathbar) {
|
||||
int pb_y = TE_TOOLBAR_H;
|
||||
c.fill_rect(0, pb_y, c.w, TE_PATHBAR_H, Color::from_rgb(0xF0, 0xF0, 0xF0));
|
||||
|
||||
// Text input box
|
||||
int inp_x = 8;
|
||||
int inp_y = pb_y + 4;
|
||||
int btn_w = 56;
|
||||
int inp_w = c.w - inp_x - btn_w - 12;
|
||||
int inp_h = 24;
|
||||
|
||||
c.fill_rect(inp_x, inp_y, inp_w, inp_h, colors::WHITE);
|
||||
c.rect(inp_x, inp_y, inp_w, inp_h, colors::ACCENT);
|
||||
|
||||
// Path text
|
||||
int text_y = inp_y + (inp_h - sfh) / 2;
|
||||
c.text(inp_x + 4, text_y, te->pathbar_text, colors::TEXT_COLOR);
|
||||
|
||||
// Blinking cursor in path input
|
||||
char prefix[256];
|
||||
int plen = te->pathbar_cursor;
|
||||
if (plen > 255) plen = 255;
|
||||
for (int i = 0; i < plen; i++) prefix[i] = te->pathbar_text[i];
|
||||
prefix[plen] = '\0';
|
||||
int cx = inp_x + 4 + text_width(prefix);
|
||||
c.fill_rect(cx, inp_y + 3, 2, inp_h - 6, colors::ACCENT);
|
||||
|
||||
// Open button
|
||||
int ob_x = inp_x + inp_w + 6;
|
||||
c.button(ob_x, inp_y, btn_w, inp_h, "Open", colors::ACCENT, colors::WHITE, 3);
|
||||
|
||||
// Path bar bottom separator
|
||||
c.hline(0, pb_y + TE_PATHBAR_H - 1, c.w, colors::BORDER);
|
||||
|
||||
editor_y_start = TE_TOOLBAR_H + TE_PATHBAR_H;
|
||||
}
|
||||
|
||||
// ---- Editor area ----
|
||||
int text_area_h = c.h - editor_y_start - TE_STATUS_H;
|
||||
int visible_lines = text_area_h / cell_h;
|
||||
if (visible_lines < 1) visible_lines = 1;
|
||||
|
||||
te_ensure_cursor_visible(te, visible_lines, c.w);
|
||||
|
||||
// Line number gutter background
|
||||
c.fill_rect(0, editor_y_start, TE_LINE_NUM_W, text_area_h, Color::from_rgb(0xF0, 0xF0, 0xF0));
|
||||
|
||||
// Gutter separator
|
||||
c.vline(TE_LINE_NUM_W, editor_y_start, text_area_h, colors::BORDER);
|
||||
|
||||
// Draw lines
|
||||
Color linenum_color = Color::from_rgb(0x99, 0x99, 0x99);
|
||||
Color cursor_line_color = Color::from_rgb(0xFF, 0xFD, 0xE8);
|
||||
Color text_color = colors::TEXT_COLOR;
|
||||
|
||||
int text_start_x = TE_LINE_NUM_W + 4;
|
||||
|
||||
for (int vis = 0; vis < visible_lines + 1; vis++) {
|
||||
int line = te->scroll_y + vis;
|
||||
if (line >= te->line_count) break;
|
||||
|
||||
int py = editor_y_start + vis * cell_h;
|
||||
if (py >= editor_y_start + text_area_h) break;
|
||||
|
||||
// Cursor line highlighting
|
||||
if (line == te->cursor_line) {
|
||||
int hl_h = gui_min(cell_h, editor_y_start + text_area_h - py);
|
||||
if (hl_h > 0)
|
||||
c.fill_rect(TE_LINE_NUM_W + 1, py, c.w - TE_LINE_NUM_W - 1, hl_h, cursor_line_color);
|
||||
}
|
||||
|
||||
// Line number
|
||||
char num_str[8];
|
||||
snprintf(num_str, 8, "%4d", line + 1);
|
||||
c.text_mono(4, py, num_str, linenum_color);
|
||||
|
||||
// Line text (per-character rendering with horizontal scroll clipping)
|
||||
int line_start = te->line_offsets[line];
|
||||
int line_len = te_line_length(te, line);
|
||||
|
||||
for (int ci = 0; ci < line_len; ci++) {
|
||||
int px = text_start_x + ci * cell_w - te->scroll_x;
|
||||
if (px + cell_w <= TE_LINE_NUM_W + 1) continue;
|
||||
if (px >= c.w) break;
|
||||
|
||||
char ch = te->buffer[line_start + ci];
|
||||
if (ch >= 32 || ch < 0) {
|
||||
if (fonts::mono && fonts::mono->valid) {
|
||||
GlyphCache* gc = fonts::mono->get_cache(fonts::TERM_SIZE);
|
||||
fonts::mono->draw_char_to_buffer(
|
||||
c.pixels, c.w, c.h, px, py + gc->ascent,
|
||||
ch, text_color, gc);
|
||||
} else {
|
||||
// bitmap fallback
|
||||
uint32_t text_px = text_color.to_pixel();
|
||||
const uint8_t* glyph = &font_data[(unsigned char)ch * FONT_HEIGHT];
|
||||
for (int fy = 0; fy < FONT_HEIGHT && py + fy < editor_y_start + text_area_h; fy++) {
|
||||
uint8_t bits = glyph[fy];
|
||||
for (int fx = 0; fx < FONT_WIDTH; fx++) {
|
||||
if (bits & (0x80 >> fx)) {
|
||||
int dx = px + fx;
|
||||
int dy = py + fy;
|
||||
if (dx > TE_LINE_NUM_W && dx < c.w && dy >= 0 && dy < c.h)
|
||||
c.pixels[dy * c.w + dx] = text_px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw cursor
|
||||
if (line == te->cursor_line) {
|
||||
int cx = text_start_x + te->cursor_col * cell_w - te->scroll_x;
|
||||
if (cx > TE_LINE_NUM_W && cx + 2 <= c.w) {
|
||||
int cur_h = gui_min(cell_h, editor_y_start + text_area_h - py);
|
||||
if (cur_h > 0)
|
||||
c.fill_rect(cx, py, 2, cur_h, colors::ACCENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Status bar ----
|
||||
int status_y = c.h - TE_STATUS_H;
|
||||
c.fill_rect(0, status_y, c.w, TE_STATUS_H, Color::from_rgb(0x2B, 0x3E, 0x50));
|
||||
|
||||
// Cursor position (right side)
|
||||
char status_right[32];
|
||||
snprintf(status_right, 32, "Ln %d, Col %d ", te->cursor_line + 1, te->cursor_col + 1);
|
||||
int sr_w = text_width(status_right);
|
||||
int status_text_y = status_y + (TE_STATUS_H - sfh) / 2;
|
||||
c.text(c.w - sr_w - 4, status_text_y, status_right, colors::PANEL_TEXT);
|
||||
|
||||
// Filename + modified flag (left side)
|
||||
char status_left[128];
|
||||
if (te->filename[0]) {
|
||||
snprintf(status_left, 128, " %s%s", te->filename, te->modified ? " [modified]" : "");
|
||||
} else {
|
||||
snprintf(status_left, 128, " Untitled%s", te->modified ? " [modified]" : "");
|
||||
}
|
||||
c.text(4, status_text_y, status_left, colors::PANEL_TEXT);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Mouse handling
|
||||
// ============================================================================
|
||||
|
||||
static void texteditor_on_mouse(Window* win, MouseEvent& ev) {
|
||||
TextEditorState* te = (TextEditorState*)win->app_data;
|
||||
if (!te) return;
|
||||
|
||||
Rect cr = win->content_rect();
|
||||
int local_x = ev.x - cr.x;
|
||||
int local_y = ev.y - cr.y;
|
||||
|
||||
int cell_w = mono_cell_width();
|
||||
int cell_h = mono_cell_height();
|
||||
|
||||
int editor_y_start = TE_TOOLBAR_H + (te->show_pathbar ? TE_PATHBAR_H : 0);
|
||||
int text_area_h = cr.h - editor_y_start - TE_STATUS_H;
|
||||
|
||||
// ---- Toolbar clicks ----
|
||||
if (ev.left_pressed() && local_y < TE_TOOLBAR_H) {
|
||||
// Open button
|
||||
if (local_x >= 4 && local_x < 28 && local_y >= 6 && local_y < 30) {
|
||||
te->show_pathbar = !te->show_pathbar;
|
||||
if (te->show_pathbar) {
|
||||
// Pre-fill with current filepath
|
||||
montauk::strncpy(te->pathbar_text, te->filepath, 255);
|
||||
te->pathbar_len = montauk::slen(te->pathbar_text);
|
||||
te->pathbar_cursor = te->pathbar_len;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Save button
|
||||
if (local_x >= 32 && local_x < 56 && local_y >= 6 && local_y < 30) {
|
||||
te_save_file(te);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Path bar clicks ----
|
||||
if (te->show_pathbar && local_y >= TE_TOOLBAR_H && local_y < TE_TOOLBAR_H + TE_PATHBAR_H) {
|
||||
if (ev.left_pressed()) {
|
||||
// Check "Open" button region
|
||||
int btn_w = 56;
|
||||
int inp_w = cr.w - 8 - btn_w - 12;
|
||||
int ob_x = 8 + inp_w + 6;
|
||||
if (local_x >= ob_x && local_x < ob_x + btn_w) {
|
||||
if (te->pathbar_text[0]) {
|
||||
te_load_file(te, te->pathbar_text);
|
||||
// Update window title
|
||||
char title[64];
|
||||
snprintf(title, 64, "%s - Editor", te->filename);
|
||||
montauk::strncpy(win->title, title, 63);
|
||||
te->show_pathbar = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Editor area clicks ----
|
||||
if (ev.left_pressed() && local_y >= editor_y_start && local_y < editor_y_start + text_area_h && local_x > TE_LINE_NUM_W) {
|
||||
int clicked_line = te->scroll_y + (local_y - editor_y_start) / cell_h;
|
||||
if (clicked_line >= te->line_count) clicked_line = te->line_count - 1;
|
||||
if (clicked_line < 0) clicked_line = 0;
|
||||
|
||||
int clicked_col = (local_x - TE_LINE_NUM_W - 4 + te->scroll_x + cell_w / 2) / cell_w;
|
||||
if (clicked_col < 0) clicked_col = 0;
|
||||
int line_len = te_line_length(te, clicked_line);
|
||||
if (clicked_col > line_len) clicked_col = line_len;
|
||||
|
||||
te->cursor_pos = te->line_offsets[clicked_line] + clicked_col;
|
||||
te_update_cursor_pos(te);
|
||||
}
|
||||
|
||||
// ---- Scroll ----
|
||||
if (ev.scroll != 0 && local_y >= editor_y_start && local_y < editor_y_start + text_area_h) {
|
||||
te->scroll_y -= ev.scroll * 3;
|
||||
if (te->scroll_y < 0) te->scroll_y = 0;
|
||||
int max_scroll = te->line_count - (text_area_h / cell_h) + 1;
|
||||
if (max_scroll < 0) max_scroll = 0;
|
||||
if (te->scroll_y > max_scroll) te->scroll_y = max_scroll;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Keyboard handling
|
||||
// ============================================================================
|
||||
|
||||
static void texteditor_on_key(Window* win, const Montauk::KeyEvent& key) {
|
||||
TextEditorState* te = (TextEditorState*)win->app_data;
|
||||
if (!te || !key.pressed) return;
|
||||
|
||||
// ---- Path bar input mode ----
|
||||
if (te->show_pathbar) {
|
||||
if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
if (te->pathbar_text[0]) {
|
||||
te_load_file(te, te->pathbar_text);
|
||||
char title[64];
|
||||
snprintf(title, 64, "%s - Editor", te->filename);
|
||||
montauk::strncpy(win->title, title, 63);
|
||||
te->show_pathbar = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (key.scancode == 0x01) { // Escape
|
||||
te->show_pathbar = false;
|
||||
return;
|
||||
}
|
||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
if (te->pathbar_cursor > 0) {
|
||||
for (int i = te->pathbar_cursor - 1; i < te->pathbar_len - 1; i++)
|
||||
te->pathbar_text[i] = te->pathbar_text[i + 1];
|
||||
te->pathbar_len--;
|
||||
te->pathbar_cursor--;
|
||||
te->pathbar_text[te->pathbar_len] = '\0';
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (key.scancode == 0x4B) { // Left
|
||||
if (te->pathbar_cursor > 0) te->pathbar_cursor--;
|
||||
return;
|
||||
}
|
||||
if (key.scancode == 0x4D) { // Right
|
||||
if (te->pathbar_cursor < te->pathbar_len) te->pathbar_cursor++;
|
||||
return;
|
||||
}
|
||||
if (key.ascii >= 32 && key.ascii < 127 && te->pathbar_len < 254) {
|
||||
for (int i = te->pathbar_len; i > te->pathbar_cursor; i--)
|
||||
te->pathbar_text[i] = te->pathbar_text[i - 1];
|
||||
te->pathbar_text[te->pathbar_cursor] = key.ascii;
|
||||
te->pathbar_cursor++;
|
||||
te->pathbar_len++;
|
||||
te->pathbar_text[te->pathbar_len] = '\0';
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Normal editor mode ----
|
||||
|
||||
// Ctrl+S: save
|
||||
if (key.ctrl && (key.ascii == 's' || key.ascii == 'S')) {
|
||||
te_save_file(te);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+O: open
|
||||
if (key.ctrl && (key.ascii == 'o' || key.ascii == 'O')) {
|
||||
te->show_pathbar = !te->show_pathbar;
|
||||
if (te->show_pathbar) {
|
||||
montauk::strncpy(te->pathbar_text, te->filepath, 255);
|
||||
te->pathbar_len = montauk::slen(te->pathbar_text);
|
||||
te->pathbar_cursor = te->pathbar_len;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Arrow keys
|
||||
if (key.scancode == 0x48) { te_move_up(te); return; }
|
||||
if (key.scancode == 0x50) { te_move_down(te); return; }
|
||||
if (key.scancode == 0x4B) { te_move_left(te); return; }
|
||||
if (key.scancode == 0x4D) { te_move_right(te); return; }
|
||||
|
||||
// Home
|
||||
if (key.scancode == 0x47) { te_move_home(te); return; }
|
||||
// End
|
||||
if (key.scancode == 0x4F) { te_move_end(te); return; }
|
||||
// Delete
|
||||
if (key.scancode == 0x53) { te_delete_char(te); return; }
|
||||
|
||||
// Backspace
|
||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
te_backspace(te);
|
||||
return;
|
||||
}
|
||||
|
||||
// Enter
|
||||
if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
te_insert_char(te, '\n');
|
||||
return;
|
||||
}
|
||||
|
||||
// Tab
|
||||
if (key.ascii == '\t') {
|
||||
for (int i = 0; i < TE_TAB_WIDTH; i++) {
|
||||
te_insert_char(te, ' ');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Printable characters
|
||||
if (key.ascii >= 32 && key.ascii < 127) {
|
||||
te_insert_char(te, key.ascii);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void texteditor_on_close(Window* win) {
|
||||
TextEditorState* te = (TextEditorState*)win->app_data;
|
||||
if (te) {
|
||||
if (te->buffer) montauk::mfree(te->buffer);
|
||||
if (te->line_offsets) montauk::mfree(te->line_offsets);
|
||||
montauk::mfree(te);
|
||||
win->app_data = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Text Editor launchers
|
||||
// ============================================================================
|
||||
|
||||
void open_texteditor(DesktopState* ds) {
|
||||
int idx = desktop_create_window(ds, "Text Editor", 180, 60, 600, 450);
|
||||
if (idx < 0) return;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
TextEditorState* te = (TextEditorState*)montauk::malloc(sizeof(TextEditorState));
|
||||
montauk::memset(te, 0, sizeof(TextEditorState));
|
||||
|
||||
te->buffer = (char*)montauk::malloc(TE_INIT_CAP);
|
||||
te->buf_cap = TE_INIT_CAP;
|
||||
te->buf_len = 0;
|
||||
te->modified = false;
|
||||
te->desktop = ds;
|
||||
te->show_pathbar = false;
|
||||
te->pathbar_text[0] = '\0';
|
||||
te->pathbar_cursor = 0;
|
||||
te->pathbar_len = 0;
|
||||
|
||||
te_recompute_lines(te);
|
||||
te_update_cursor_pos(te);
|
||||
|
||||
win->app_data = te;
|
||||
win->on_draw = texteditor_on_draw;
|
||||
win->on_mouse = texteditor_on_mouse;
|
||||
win->on_key = texteditor_on_key;
|
||||
win->on_close = texteditor_on_close;
|
||||
}
|
||||
|
||||
void open_texteditor_with_file(DesktopState* ds, const char* path) {
|
||||
// Extract filename for window title
|
||||
const char* name = path;
|
||||
for (int i = 0; path[i]; i++) {
|
||||
if (path[i] == '/') name = path + i + 1;
|
||||
}
|
||||
|
||||
char title[64];
|
||||
snprintf(title, 64, "%s - Editor", name);
|
||||
|
||||
int idx = desktop_create_window(ds, title, 180, 60, 600, 450);
|
||||
if (idx < 0) return;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
TextEditorState* te = (TextEditorState*)montauk::malloc(sizeof(TextEditorState));
|
||||
montauk::memset(te, 0, sizeof(TextEditorState));
|
||||
|
||||
te->buffer = (char*)montauk::malloc(TE_INIT_CAP);
|
||||
te->buf_cap = TE_INIT_CAP;
|
||||
te->buf_len = 0;
|
||||
te->modified = false;
|
||||
te->desktop = ds;
|
||||
te->show_pathbar = false;
|
||||
te->pathbar_text[0] = '\0';
|
||||
te->pathbar_cursor = 0;
|
||||
te->pathbar_len = 0;
|
||||
|
||||
// Initialize line index for empty document first (ensures line_offsets
|
||||
// is non-null even if te_load_file fails to open the file)
|
||||
te_recompute_lines(te);
|
||||
te_update_cursor_pos(te);
|
||||
|
||||
te_load_file(te, path);
|
||||
|
||||
win->app_data = te;
|
||||
win->on_draw = texteditor_on_draw;
|
||||
win->on_mouse = texteditor_on_mouse;
|
||||
win->on_key = texteditor_on_key;
|
||||
win->on_close = texteditor_on_close;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* app_weather.cpp
|
||||
* MontaukOS Desktop - Weather app launcher
|
||||
* Spawns weather.elf as a standalone Window Server process
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "apps_common.hpp"
|
||||
|
||||
void open_weather(DesktopState* ds) {
|
||||
(void)ds;
|
||||
montauk::spawn("0:/os/weather.elf");
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* app_wiki.cpp
|
||||
* MontaukOS Desktop - Wikipedia launcher
|
||||
* Spawns wikipedia.elf as a standalone Window Server process
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "apps_common.hpp"
|
||||
|
||||
void open_wiki(DesktopState* ds) {
|
||||
(void)ds;
|
||||
montauk::spawn("0:/os/wikipedia.elf");
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* apps_common.hpp
|
||||
* Shared inline utilities and forward declarations for desktop apps
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/heap.h>
|
||||
#include <gui/gui.hpp>
|
||||
#include <gui/framebuffer.hpp>
|
||||
#include <gui/font.hpp>
|
||||
#include <gui/draw.hpp>
|
||||
#include <gui/svg.hpp>
|
||||
#include <gui/widgets.hpp>
|
||||
#include <gui/window.hpp>
|
||||
#include <gui/canvas.hpp>
|
||||
#include <gui/terminal.hpp>
|
||||
#include <gui/desktop.hpp>
|
||||
|
||||
// Placement new for freestanding environment
|
||||
inline void* operator new(unsigned long, void* p) { return p; }
|
||||
|
||||
using namespace gui;
|
||||
|
||||
// ============================================================================
|
||||
// Minimal snprintf
|
||||
// ============================================================================
|
||||
|
||||
using va_list = __builtin_va_list;
|
||||
#define va_start __builtin_va_start
|
||||
#define va_end __builtin_va_end
|
||||
#define va_arg __builtin_va_arg
|
||||
|
||||
struct PfState { char* buf; int pos; int max; };
|
||||
|
||||
inline void pf_putc(PfState* st, char c) {
|
||||
if (st->pos < st->max) st->buf[st->pos] = c;
|
||||
st->pos++;
|
||||
}
|
||||
|
||||
inline void pf_putnum(PfState* st, unsigned long val, int base, int width, char pad, int neg) {
|
||||
char tmp[24]; int i = 0;
|
||||
const char* digits = "0123456789abcdef";
|
||||
if (val == 0) { tmp[i++] = '0'; }
|
||||
else { while (val > 0) { tmp[i++] = digits[val % base]; val /= base; } }
|
||||
int total = (neg ? 1 : 0) + i;
|
||||
if (neg && pad == '0') pf_putc(st, '-');
|
||||
for (int w = total; w < width; w++) pf_putc(st, pad);
|
||||
if (neg && pad != '0') pf_putc(st, '-');
|
||||
while (i > 0) pf_putc(st, tmp[--i]);
|
||||
}
|
||||
|
||||
inline int snprintf(char* buf, int size, const char* fmt, ...) {
|
||||
va_list ap; va_start(ap, fmt);
|
||||
PfState st; st.buf = buf; st.pos = 0; st.max = size > 0 ? size - 1 : 0;
|
||||
while (*fmt) {
|
||||
if (*fmt != '%') { pf_putc(&st, *fmt++); continue; }
|
||||
fmt++;
|
||||
char pad = ' ';
|
||||
if (*fmt == '0') { pad = '0'; fmt++; }
|
||||
int width = 0;
|
||||
while (*fmt >= '0' && *fmt <= '9') { width = width * 10 + (*fmt - '0'); fmt++; }
|
||||
if (*fmt == 'l') fmt++;
|
||||
switch (*fmt) {
|
||||
case 'd': case 'i': {
|
||||
long val = va_arg(ap, int);
|
||||
int neg = 0; unsigned long uval;
|
||||
if (val < 0) { neg = 1; uval = (unsigned long)(-val); } else uval = (unsigned long)val;
|
||||
pf_putnum(&st, uval, 10, width, pad, neg); break;
|
||||
}
|
||||
case 'u': { unsigned val = va_arg(ap, unsigned); pf_putnum(&st, val, 10, width, pad, 0); break; }
|
||||
case 'x': { unsigned val = va_arg(ap, unsigned); pf_putnum(&st, val, 16, width, pad, 0); break; }
|
||||
case 's': {
|
||||
const char* s = va_arg(ap, const char*); if (!s) s = "(null)";
|
||||
int slen = 0; while (s[slen]) slen++;
|
||||
for (int w = slen; w < width; w++) pf_putc(&st, ' ');
|
||||
for (int j = 0; j < slen; j++) pf_putc(&st, s[j]);
|
||||
break;
|
||||
}
|
||||
case 'c': { char c = (char)va_arg(ap, int); pf_putc(&st, c); break; }
|
||||
case '%': pf_putc(&st, '%'); break;
|
||||
default: pf_putc(&st, '%'); pf_putc(&st, *fmt); break;
|
||||
}
|
||||
if (*fmt) fmt++;
|
||||
}
|
||||
if (size > 0) { if (st.pos < size) st.buf[st.pos] = '\0'; else st.buf[size - 1] = '\0'; }
|
||||
va_end(ap); return st.pos;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// String helpers
|
||||
// ============================================================================
|
||||
|
||||
inline void str_append(char* dst, const char* src, int max) {
|
||||
int len = montauk::slen(dst);
|
||||
int i = 0;
|
||||
while (src[i] && len < max - 1) {
|
||||
dst[len++] = src[i++];
|
||||
}
|
||||
dst[len] = '\0';
|
||||
}
|
||||
|
||||
inline int str_compare_ci(const char* a, const char* b) {
|
||||
while (*a && *b) {
|
||||
char ca = (*a >= 'A' && *a <= 'Z') ? (*a + 32) : *a;
|
||||
char cb = (*b >= 'A' && *b <= 'Z') ? (*b + 32) : *b;
|
||||
if (ca != cb) return ca - cb;
|
||||
a++; b++;
|
||||
}
|
||||
return (unsigned char)*a - (unsigned char)*b;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Network formatting helpers
|
||||
// ============================================================================
|
||||
|
||||
inline void format_ip(char* buf, uint32_t ip) {
|
||||
snprintf(buf, 20, "%d.%d.%d.%d",
|
||||
(int)(ip & 0xFF), (int)((ip >> 8) & 0xFF),
|
||||
(int)((ip >> 16) & 0xFF), (int)((ip >> 24) & 0xFF));
|
||||
}
|
||||
|
||||
inline void format_mac(char* buf, const uint8_t* mac) {
|
||||
snprintf(buf, 20, "%02x:%02x:%02x:%02x:%02x:%02x",
|
||||
(unsigned)mac[0], (unsigned)mac[1], (unsigned)mac[2],
|
||||
(unsigned)mac[3], (unsigned)mac[4], (unsigned)mac[5]);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// File size formatting
|
||||
// ============================================================================
|
||||
|
||||
inline void format_size(char* buf, int size) {
|
||||
if (size < 1024) {
|
||||
snprintf(buf, 16, "%d B", size);
|
||||
} else if (size < 1024 * 1024) {
|
||||
int kb = size / 1024;
|
||||
int frac = ((size % 1024) * 10) / 1024;
|
||||
if (kb < 10) {
|
||||
snprintf(buf, 16, "%d.%d KB", kb, frac);
|
||||
} else {
|
||||
snprintf(buf, 16, "%d KB", kb);
|
||||
}
|
||||
} else {
|
||||
int mb = size / (1024 * 1024);
|
||||
int frac = ((size % (1024 * 1024)) * 10) / (1024 * 1024);
|
||||
if (mb < 10) {
|
||||
snprintf(buf, 16, "%d.%d MB", mb, frac);
|
||||
} else {
|
||||
snprintf(buf, 16, "%d MB", mb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Forward declarations for app launchers
|
||||
// ============================================================================
|
||||
|
||||
void open_terminal(DesktopState* ds);
|
||||
void open_filemanager(DesktopState* ds);
|
||||
void open_sysinfo(DesktopState* ds);
|
||||
void open_calculator(DesktopState* ds);
|
||||
void open_texteditor(DesktopState* ds);
|
||||
void open_texteditor_with_file(DesktopState* ds, const char* path);
|
||||
void open_klog(DesktopState* ds);
|
||||
void open_wiki(DesktopState* ds);
|
||||
void open_weather(DesktopState* ds);
|
||||
void open_procmgr(DesktopState* ds);
|
||||
void open_mandelbrot(DesktopState* ds);
|
||||
void open_devexplorer(DesktopState* ds);
|
||||
void open_settings(DesktopState* ds);
|
||||
void open_doom(DesktopState* ds);
|
||||
void open_reboot_dialog(DesktopState* ds);
|
||||
void open_wordprocessor(DesktopState* ds);
|
||||
void open_spreadsheet(DesktopState* ds);
|
||||
void open_shutdown_dialog(DesktopState* ds);
|
||||
void desktop_poll_external_windows(DesktopState* ds);
|
||||
Reference in New Issue
Block a user