feat: new calculator UI, video app icon, Super + Space search window

This commit is contained in:
2026-03-28 23:31:43 +01:00
parent 608625709c
commit cbead256c3
22 changed files with 1584 additions and 467 deletions
@@ -1,404 +0,0 @@
/*
* 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;
}
@@ -17,6 +17,11 @@ void open_terminal(DesktopState* ds) {
spawn_app("0:/apps/terminal/terminal.elf", home);
}
void open_calculator(DesktopState* ds) {
(void)ds;
spawn_app("0:/apps/calculator/calculator.elf");
}
void open_texteditor(DesktopState* ds) {
(void)ds;
spawn_app("0:/apps/texteditor/texteditor.elf");
+45 -24
View File
@@ -1903,7 +1903,32 @@ static void filemanager_on_close(Window* win) {
// File Manager launcher
// ============================================================================
void open_filemanager(DesktopState* ds) {
void ensure_filemanager_icons_loaded(DesktopState* ds) {
if (!ds || ds->icon_drive.pixels) return;
Color defColor = colors::ICON_COLOR;
ds->icon_drive = svg_load("0:/icons/drive-harddisk.svg", 16, 16, defColor);
ds->icon_drive_lg = svg_load("0:/icons/drive-harddisk.svg", 48, 48, defColor);
ds->icon_home_folder = svg_load("0:/icons/folder-blue-home.svg", 16, 16, defColor);
ds->icon_home_folder_lg = svg_load("0:/icons/folder-blue-home.svg", 48, 48, defColor);
ds->icon_apps = svg_load("0:/icons/folder-blue-development.svg", 16, 16, defColor);
ds->icon_apps_lg = svg_load("0:/icons/folder-blue-development.svg", 48, 48, defColor);
for (int sf = 0; sf < SF_COUNT; sf++) {
char icon_path[128];
snprintf(icon_path, 128, "0:/icons/%s", sf_icons[sf]);
ds->icon_special_folder[sf] = svg_load(icon_path, 16, 16, defColor);
ds->icon_special_folder_lg[sf] = svg_load(icon_path, 48, 48, defColor);
}
ds->icon_delete = svg_load("0:/icons/trash-empty.svg", 16, 16, defColor);
ds->icon_copy = svg_load("0:/icons/edit-copy.svg", 16, 16, defColor);
ds->icon_cut = svg_load("0:/icons/edit-cut.svg", 16, 16, defColor);
ds->icon_paste = svg_load("0:/icons/edit-paste.svg", 16, 16, defColor);
ds->icon_rename = svg_load("0:/icons/edit-rename.svg", 16, 16, defColor);
ds->icon_folder_new = svg_load("0:/icons/folder-new.svg", 16, 16, defColor);
}
static void open_filemanager_internal(DesktopState* ds, const char* initial_path) {
int idx = desktop_create_window(ds, "Files", 150, 120, 560, 420);
if (idx < 0) return;
@@ -1919,32 +1944,20 @@ void open_filemanager(DesktopState* ds) {
fm->scrollbar.init(0, 0, FM_SCROLLBAR_W, 100);
// Lazy-load file manager icons on first open
if (ds && !ds->icon_drive.pixels) {
Color defColor = colors::ICON_COLOR;
ds->icon_drive = svg_load("0:/icons/drive-harddisk.svg", 16, 16, defColor);
ds->icon_drive_lg = svg_load("0:/icons/drive-harddisk.svg", 48, 48, defColor);
ds->icon_home_folder = svg_load("0:/icons/folder-blue-home.svg", 16, 16, defColor);
ds->icon_home_folder_lg = svg_load("0:/icons/folder-blue-home.svg", 48, 48, defColor);
ds->icon_apps = svg_load("0:/icons/folder-blue-development.svg", 16, 16, defColor);
ds->icon_apps_lg = svg_load("0:/icons/folder-blue-development.svg", 48, 48, defColor);
ensure_filemanager_icons_loaded(ds);
// Special user folder icons
for (int sf = 0; sf < SF_COUNT; sf++) {
char icon_path[128];
snprintf(icon_path, 128, "0:/icons/%s", sf_icons[sf]);
ds->icon_special_folder[sf] = svg_load(icon_path, 16, 16, defColor);
ds->icon_special_folder_lg[sf] = svg_load(icon_path, 48, 48, defColor);
if (initial_path && initial_path[0] != '\0') {
int probe_fd = montauk::open(initial_path);
if (probe_fd >= 0) {
montauk::close(probe_fd);
montauk::strncpy(fm->current_path, initial_path, sizeof(fm->current_path));
filemanager_read_dir(fm);
} else {
filemanager_read_drives(fm);
}
ds->icon_delete = svg_load("0:/icons/trash-empty.svg", 16, 16, defColor);
ds->icon_copy = svg_load("0:/icons/edit-copy.svg", 16, 16, defColor);
ds->icon_cut = svg_load("0:/icons/edit-cut.svg", 16, 16, defColor);
ds->icon_paste = svg_load("0:/icons/edit-paste.svg", 16, 16, defColor);
ds->icon_rename = svg_load("0:/icons/edit-rename.svg", 16, 16, defColor);
ds->icon_folder_new = svg_load("0:/icons/folder-new.svg", 16, 16, defColor);
} else {
filemanager_read_drives(fm);
}
filemanager_read_drives(fm);
filemanager_push_history(fm);
win->app_data = fm;
@@ -1953,3 +1966,11 @@ void open_filemanager(DesktopState* ds) {
win->on_key = filemanager_on_key;
win->on_close = filemanager_on_close;
}
void open_filemanager(DesktopState* ds) {
open_filemanager_internal(ds, nullptr);
}
void open_filemanager_path(DesktopState* ds, const char* path) {
open_filemanager_internal(ds, path);
}
@@ -161,6 +161,8 @@ inline void format_size(char* buf, int size) {
void open_terminal(DesktopState* ds);
void open_filemanager(DesktopState* ds);
void open_filemanager_path(DesktopState* ds, const char* path);
void ensure_filemanager_icons_loaded(DesktopState* ds);
void open_sysinfo(DesktopState* ds);
void open_calculator(DesktopState* ds);
void open_texteditor(DesktopState* ds);