feat: new calculator UI, video app icon, Super + Space search window
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user