/* * main.cpp * MontaukOS Calculator - standalone Window Server app * Redesigned to match MontaukOS application conventions * Copyright (c) 2026 Daniel Hammer */ #include #include #include #include #include #include #include #include #include extern "C" { #include } using namespace gui; 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 }; enum CalcButtonKind : uint8_t { BTN_DIGIT = 0, BTN_UTILITY = 1, BTN_OPERATOR = 2, BTN_EQUALS = 3, }; enum CalcAction : uint8_t { ACT_CLEAR = 0, ACT_NEGATE, ACT_PERCENT, ACT_DIVIDE, ACT_7, ACT_8, ACT_9, ACT_MULTIPLY, ACT_4, ACT_5, ACT_6, ACT_SUBTRACT, ACT_1, ACT_2, ACT_3, ACT_ADD, ACT_0, ACT_DECIMAL, ACT_EQUALS, }; struct CalcButtonDef { CalcAction action; const char* label; int row; int col; int col_span; CalcButtonKind kind; char op; }; struct CalcButtonLayout { Rect rect; const CalcButtonDef* def; }; struct CalcLayout { Rect display_card; CalcButtonLayout buttons[19]; int button_count; }; static constexpr int INIT_W = 260; static constexpr int INIT_H = 298; static constexpr int OUTER_PAD = 8; static constexpr int GRID_GAP = 4; static constexpr int DISPLAY_H = 78; static constexpr int CARD_RADIUS = 8; static constexpr int CALC_BTN_RADIUS = 6; static constexpr int LABEL_SIZE = 16; static constexpr int VALUE_SIZE = 28; static constexpr Color APP_BG = Color::from_rgb(0xFA, 0xFA, 0xFA); static constexpr Color GRID_COLOR = Color::from_rgb(0xD0, 0xD0, 0xD0); static constexpr Color HEADER_TEXT = Color::from_rgb(0x55, 0x55, 0x55); static constexpr Color CELL_TEXT = Color::from_rgb(0x22, 0x22, 0x22); static constexpr Color SURFACE_BG = Color::from_rgb(0xFF, 0xFF, 0xFF); static constexpr Color SELECT_FILL = Color::from_rgb(0xD8, 0xE8, 0xFD); static constexpr Color TB_BTN_BG = Color::from_rgb(0xE8, 0xE8, 0xE8); static constexpr Color TB_BTN_ACTIVE = Color::from_rgb(0xC0, 0xD0, 0xE8); static CalcState g_calc = {}; static int g_hovered_button = -1; static int g_pressed_button = -1; static Color g_accent = colors::ACCENT; static Color g_accent_soft = SELECT_FILL; static Color g_accent_mid = TB_BTN_ACTIVE; static Color g_accent_dark = colors::ACCENT; static const CalcButtonDef g_button_defs[] = { { ACT_CLEAR, "C", 0, 0, 1, BTN_UTILITY, 0 }, { ACT_NEGATE, "+/-", 0, 1, 1, BTN_UTILITY, 0 }, { ACT_PERCENT, "%", 0, 2, 1, BTN_UTILITY, 0 }, { ACT_DIVIDE, "/", 0, 3, 1, BTN_OPERATOR, '/' }, { ACT_7, "7", 1, 0, 1, BTN_DIGIT, 0 }, { ACT_8, "8", 1, 1, 1, BTN_DIGIT, 0 }, { ACT_9, "9", 1, 2, 1, BTN_DIGIT, 0 }, { ACT_MULTIPLY, "*", 1, 3, 1, BTN_OPERATOR, '*' }, { ACT_4, "4", 2, 0, 1, BTN_DIGIT, 0 }, { ACT_5, "5", 2, 1, 1, BTN_DIGIT, 0 }, { ACT_6, "6", 2, 2, 1, BTN_DIGIT, 0 }, { ACT_SUBTRACT, "-", 2, 3, 1, BTN_OPERATOR, '-' }, { ACT_1, "1", 3, 0, 1, BTN_DIGIT, 0 }, { ACT_2, "2", 3, 1, 1, BTN_DIGIT, 0 }, { ACT_3, "3", 3, 2, 1, BTN_DIGIT, 0 }, { ACT_ADD, "+", 3, 3, 1, BTN_OPERATOR, '+' }, { ACT_0, "0", 4, 0, 2, BTN_DIGIT, 0 }, { ACT_DECIMAL, ".", 4, 2, 1, BTN_DIGIT, 0 }, { ACT_EQUALS, "=", 4, 3, 1, BTN_EQUALS, 0 }, }; static constexpr int BUTTON_COUNT = sizeof(g_button_defs) / sizeof(g_button_defs[0]); static Color shade(Color c, int delta) { return Color::from_rgb( (uint8_t)gui_clamp((int)c.r + delta, 0, 255), (uint8_t)gui_clamp((int)c.g + delta, 0, 255), (uint8_t)gui_clamp((int)c.b + delta, 0, 255)); } static Color mix(Color a, Color b, int t) { if (t < 0) t = 0; if (t > 255) t = 255; int inv = 255 - t; return Color::from_rgb( (uint8_t)((a.r * inv + b.r * t + 127) / 255), (uint8_t)((a.g * inv + b.g * t + 127) / 255), (uint8_t)((a.b * inv + b.b * t + 127) / 255)); } static void calc_set_accent(Color accent) { g_accent = accent; g_accent_soft = mix(accent, colors::WHITE, 212); g_accent_mid = mix(accent, colors::WHITE, 170); g_accent_dark = shade(accent, -28); } static void calc_load_accent() { calc_set_accent(mtk::load_system_accent()); } 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) { 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, sizeof(cs->display_str), "-%s.%02d", int_buf, (int)frac); } else { snprintf(cs->display_str, sizeof(cs->display_str), "%s.%02d", int_buf, (int)frac); } 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 { 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, sizeof(cs->display_str), "-%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'; } } } static void calc_format_value(char* out, int out_sz, int64_t value) { CalcState tmp = {}; tmp.display_val = value; calc_format_display(&tmp); montauk::strncpy(out, tmp.display_str, out_sz); } 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 += 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) { 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); } static void calc_expression_text(char* out, int out_sz) { if (g_calc.pending_op) { char acc_buf[32]; calc_format_value(acc_buf, sizeof(acc_buf), g_calc.accumulator); snprintf(out, out_sz, "%s %c", acc_buf, g_calc.pending_op); } else if (g_calc.start_new) { snprintf(out, out_sz, "Result"); } else if (g_calc.has_decimal) { snprintf(out, out_sz, "Entering decimal"); } else { out[0] = '\0'; } } static void calc_build_layout(int w, int h, CalcLayout* layout) { if (!layout) return; layout->display_card = {OUTER_PAD, OUTER_PAD, w - OUTER_PAD * 2, DISPLAY_H}; layout->button_count = BUTTON_COUNT; int grid_y = layout->display_card.y + layout->display_card.h + OUTER_PAD; int grid_h = h - grid_y - OUTER_PAD; if (grid_h < 5) grid_h = 5; int grid_w = w - OUTER_PAD * 2; int btn_w = (grid_w - GRID_GAP * 3) / 4; int btn_h = (grid_h - GRID_GAP * 4) / 5; if (btn_w < 8) btn_w = 8; if (btn_h < 8) btn_h = 8; for (int i = 0; i < BUTTON_COUNT; i++) { const CalcButtonDef* def = &g_button_defs[i]; int x = OUTER_PAD + def->col * (btn_w + GRID_GAP); int y = grid_y + def->row * (btn_h + GRID_GAP); int bw = btn_w * def->col_span + GRID_GAP * (def->col_span - 1); layout->buttons[i].rect = {x, y, bw, btn_h}; layout->buttons[i].def = def; } } static int calc_hit_button(const CalcLayout& layout, int x, int y) { for (int i = 0; i < layout.button_count; i++) { if (layout.buttons[i].rect.contains(x, y)) return i; } return -1; } static void draw_surface(Canvas& c, const Rect& r, int radius, Color border, Color fill) { c.fill_rounded_rect(r.x, r.y, r.w, r.h, radius, border); if (r.w > 2 && r.h > 2) { c.fill_rounded_rect( r.x + 1, r.y + 1, r.w - 2, r.h - 2, radius > 0 ? radius - 1 : 0, fill); } } static bool calc_button_active(const CalcButtonDef& def) { return def.kind == BTN_OPERATOR && def.op != 0 && g_calc.pending_op == def.op; } static void calc_dispatch(CalcAction action) { switch (action) { case ACT_CLEAR: calc_press_clear(&g_calc); break; case ACT_NEGATE: calc_press_negate(&g_calc); break; case ACT_PERCENT: calc_press_percent(&g_calc); break; case ACT_DIVIDE: calc_press_operator(&g_calc, '/'); break; case ACT_7: calc_input_digit(&g_calc, 7); break; case ACT_8: calc_input_digit(&g_calc, 8); break; case ACT_9: calc_input_digit(&g_calc, 9); break; case ACT_MULTIPLY: calc_press_operator(&g_calc, '*'); break; case ACT_4: calc_input_digit(&g_calc, 4); break; case ACT_5: calc_input_digit(&g_calc, 5); break; case ACT_6: calc_input_digit(&g_calc, 6); break; case ACT_SUBTRACT: calc_press_operator(&g_calc, '-'); break; case ACT_1: calc_input_digit(&g_calc, 1); break; case ACT_2: calc_input_digit(&g_calc, 2); break; case ACT_3: calc_input_digit(&g_calc, 3); break; case ACT_ADD: calc_press_operator(&g_calc, '+'); break; case ACT_0: calc_input_digit(&g_calc, 0); break; case ACT_DECIMAL: calc_press_decimal(&g_calc); break; case ACT_EQUALS: calc_press_equals(&g_calc); break; } } static void render(Canvas& c) { CalcLayout layout; calc_build_layout(c.w, c.h, &layout); c.fill(APP_BG); char chip_text[24]; if (g_calc.pending_op) { snprintf(chip_text, sizeof(chip_text), "Op: %c", g_calc.pending_op); } else { snprintf(chip_text, sizeof(chip_text), "Ready"); } int chip_w = text_width(fonts::system_font, chip_text, LABEL_SIZE) + 18; Rect chip = { layout.display_card.x + layout.display_card.w - chip_w - 10, layout.display_card.y + 10, chip_w, 24 }; c.fill_rounded_rect(chip.x, chip.y, chip.w, chip.h, 4, g_calc.pending_op ? g_accent_mid : g_accent_soft); draw_text(c, fonts::system_font, chip.x + 9, chip.y + 4, chip_text, HEADER_TEXT, LABEL_SIZE); draw_surface(c, layout.display_card, CARD_RADIUS, GRID_COLOR, SURFACE_BG); char expr[48]; calc_expression_text(expr, sizeof(expr)); draw_text(c, fonts::system_font, layout.display_card.x + 12, layout.display_card.y + 10, expr, HEADER_TEXT, LABEL_SIZE); int value_w; int value_h; if (fonts::system_font && fonts::system_font->valid) { value_w = fonts::system_font->measure_text(g_calc.display_str, VALUE_SIZE); value_h = fonts::system_font->get_line_height(VALUE_SIZE); } else { int text_len = montauk::slen(g_calc.display_str); value_w = text_len * FONT_WIDTH * 2; value_h = FONT_HEIGHT * 2; } int value_x = layout.display_card.x + layout.display_card.w - value_w - 12; int value_y = layout.display_card.y + 26 + (layout.display_card.h - 26 - value_h) / 2; if (value_x < layout.display_card.x + 10) value_x = layout.display_card.x + 10; draw_text(c, fonts::system_font, value_x, value_y, g_calc.display_str, CELL_TEXT, VALUE_SIZE); for (int i = 0; i < layout.button_count; i++) { const CalcButtonDef& def = *layout.buttons[i].def; const Rect& rect = layout.buttons[i].rect; bool active = calc_button_active(def); bool hovered = i == g_hovered_button; bool pressed = i == g_pressed_button; Color fill = g_accent_soft; Color text = CELL_TEXT; if (def.kind == BTN_UTILITY) { fill = g_accent_mid; } else if (def.kind == BTN_OPERATOR) { fill = active ? g_accent_dark : g_accent_mid; text = active ? colors::WHITE : CELL_TEXT; } else if (def.kind == BTN_EQUALS) { fill = g_accent; text = colors::WHITE; } if (hovered && !pressed) { fill = shade(fill, 8); } if (pressed) { fill = shade(fill, -12); } c.fill_rounded_rect(rect.x, rect.y, rect.w, rect.h, CALC_BTN_RADIUS, fill); TrueTypeFont* button_font = ((def.kind == BTN_OPERATOR || def.kind == BTN_EQUALS || def.kind == BTN_UTILITY) && fonts::system_bold && fonts::system_bold->valid) ? fonts::system_bold : fonts::system_font; int tw = text_width(button_font, def.label, LABEL_SIZE); int th = text_height(button_font, LABEL_SIZE); int lx = rect.x + (rect.w - tw) / 2; int ly = rect.y + (rect.h - th) / 2; draw_text(c, button_font, lx, ly, def.label, text, LABEL_SIZE); } } static bool handle_mouse(const Montauk::WinEvent& ev, int win_w, int win_h) { CalcLayout layout; calc_build_layout(win_w, win_h, &layout); bool redraw = false; int hovered = calc_hit_button(layout, ev.mouse.x, ev.mouse.y); if (hovered != g_hovered_button) { g_hovered_button = hovered; redraw = true; } bool left_pressed = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); bool left_released = !(ev.mouse.buttons & 1) && (ev.mouse.prev_buttons & 1); if (left_pressed && hovered >= 0) { g_pressed_button = hovered; calc_dispatch(layout.buttons[hovered].def->action); redraw = true; } if (left_released && g_pressed_button != -1) { g_pressed_button = -1; redraw = true; } return redraw; } static bool handle_key(const Montauk::KeyEvent& key) { if (!key.pressed) return false; if (key.ascii >= '0' && key.ascii <= '9') { calc_input_digit(&g_calc, key.ascii - '0'); } else if (key.ascii == '+') { calc_press_operator(&g_calc, '+'); } else if (key.ascii == '-') { calc_press_operator(&g_calc, '-'); } else if (key.ascii == '*') { calc_press_operator(&g_calc, '*'); } else if (key.ascii == '/') { calc_press_operator(&g_calc, '/'); } else if (key.ascii == '=' || key.ascii == '\n' || key.ascii == '\r') { calc_press_equals(&g_calc); } else if (key.ascii == '.') { calc_press_decimal(&g_calc); } else if (key.ascii == 'c' || key.ascii == 'C' || key.scancode == 0x0E) { calc_press_clear(&g_calc); } else if (key.ascii == '%') { calc_press_percent(&g_calc); } else { return false; } return true; } extern "C" void _start() { fonts::init(); calc_load_accent(); calc_press_clear(&g_calc); WsWindow win; if (!win.create("Calculator", INIT_W, INIT_H)) montauk::exit(1); { Canvas canvas = win.canvas(); render(canvas); } win.present(); while (win.id >= 0 && !win.closed) { Montauk::WinEvent ev; int r = win.poll(&ev); if (r < 0) break; if (r == 0) { montauk::sleep_ms(16); continue; } bool redraw = false; if (ev.type == 3) break; if (ev.type == 2 || ev.type == 4) { g_hovered_button = -1; g_pressed_button = -1; redraw = true; } else if (ev.type == 1) { redraw = handle_mouse(ev, win.width, win.height); } else if (ev.type == 0) { redraw = handle_key(ev.key); } if (redraw) { Canvas canvas = win.canvas(); render(canvas); win.present(); } } win.destroy(); montauk::exit(0); }