diff --git a/kernel/src/Drivers/USB/HidKeyboard.cpp b/kernel/src/Drivers/USB/HidKeyboard.cpp index db77371..78d38be 100644 --- a/kernel/src/Drivers/USB/HidKeyboard.cpp +++ b/kernel/src/Drivers/USB/HidKeyboard.cpp @@ -160,9 +160,11 @@ namespace Drivers::USB::HidKeyboard { constexpr uint8_t SC_LEFT_CTRL = 0x1D; constexpr uint8_t SC_LEFT_SHIFT = 0x2A; constexpr uint8_t SC_LEFT_ALT = 0x38; + constexpr uint8_t SC_LEFT_GUI = 0x5B; constexpr uint8_t SC_RIGHT_CTRL = 0x1D; // Extended in PS/2, but we use same base constexpr uint8_t SC_RIGHT_SHIFT = 0x36; constexpr uint8_t SC_RIGHT_ALT = 0x38; // Extended in PS/2 + constexpr uint8_t SC_RIGHT_GUI = 0x5C; // ------------------------------------------------------------------------- // State @@ -290,6 +292,9 @@ namespace Drivers::USB::HidKeyboard { if (modChanged & MOD_LEFT_ALT) { InjectModifierKey(SC_LEFT_ALT, (modifiers & MOD_LEFT_ALT) != 0, modifiers); } + if (modChanged & MOD_LEFT_GUI) { + InjectModifierKey(SC_LEFT_GUI, (modifiers & MOD_LEFT_GUI) != 0, modifiers); + } if (modChanged & MOD_RIGHT_CTRL) { InjectModifierKey(SC_RIGHT_CTRL, (modifiers & MOD_RIGHT_CTRL) != 0, modifiers); } @@ -299,6 +304,9 @@ namespace Drivers::USB::HidKeyboard { if (modChanged & MOD_RIGHT_ALT) { InjectModifierKey(SC_RIGHT_ALT, (modifiers & MOD_RIGHT_ALT) != 0, modifiers); } + if (modChanged & MOD_RIGHT_GUI) { + InjectModifierKey(SC_RIGHT_GUI, (modifiers & MOD_RIGHT_GUI) != 0, modifiers); + } // ----------------------------------------------------------------- // Detect newly pressed keys (in current but not in previous) diff --git a/programs/GNUmakefile b/programs/GNUmakefile index 5d0bcc5..9cb32fa 100644 --- a/programs/GNUmakefile +++ b/programs/GNUmakefile @@ -59,7 +59,7 @@ BINDIR := bin PROGRAMS := $(notdir $(wildcard src/*)) # Programs with custom Makefiles (built separately). -CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr desktop login shell rpgdemo paint tcc lua screenshot texteditor mandelbrot +CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator desktop login shell rpgdemo paint tcc lua screenshot texteditor mandelbrot SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS)) # Build targets: system programs go to bin/os/, apps go to bin/apps//. @@ -81,9 +81,9 @@ CA_CERTS := $(BINDIR)/etc/ca-certificates.crt # Common shared assets (wallpapers, etc.) COMMONKEEP := $(BINDIR)/common/.keep -.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr login desktop shell rpgdemo paint tcc lua screenshot texteditor mandelbrot icons fonts bearssl libc tls libjpeg libjpegwrite install-apps +.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator login desktop shell rpgdemo paint tcc lua screenshot texteditor mandelbrot icons fonts bearssl libc tls libjpeg libjpegwrite install-apps -all: bearssl libc libjpeg libjpegwrite tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr doom rpgdemo paint tcc lua screenshot texteditor mandelbrot login desktop shell icons fonts install-apps $(MANDST) $(WWWDST) $(CA_CERTS) $(COMMONKEEP) +all: bearssl libc libjpeg libjpegwrite tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator doom rpgdemo paint tcc lua screenshot texteditor mandelbrot login desktop shell icons fonts install-apps $(MANDST) $(WWWDST) $(CA_CERTS) $(COMMONKEEP) # Build BearSSL static library (cross-compiled for freestanding x86_64). BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc) @@ -188,6 +188,10 @@ klog: libc procmgr: libc $(MAKE) -C src/procmgr +# Build calculator standalone GUI tool (depends on libc). +calculator: libc + $(MAKE) -C src/calculator + # Build login screen via its own Makefile (depends on bearssl and libc). login: bearssl libc $(MAKE) -C src/login @@ -246,7 +250,7 @@ lua: libc $(MAKE) -C src/lua # Install app bundles (manifests, icons, data files) into bin/apps//. -install-apps: doom rpgdemo paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr screenshot texteditor mandelbrot +install-apps: doom rpgdemo paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator screenshot texteditor mandelbrot ../scripts/install_apps.sh # Copy man pages into bin/man/ so mkramdisk.sh picks them up. @@ -304,6 +308,7 @@ clean: $(MAKE) -C src/terminal clean $(MAKE) -C src/klog clean $(MAKE) -C src/procmgr clean + $(MAKE) -C src/calculator clean $(MAKE) -C src/paint clean $(MAKE) -C src/rpgdemo clean $(MAKE) -C src/screenshot clean diff --git a/programs/include/gui/desktop.hpp b/programs/include/gui/desktop.hpp index 28ad46a..e194b2f 100644 --- a/programs/include/gui/desktop.hpp +++ b/programs/include/gui/desktop.hpp @@ -18,6 +18,7 @@ namespace gui { static constexpr int MAX_WINDOWS = 8; static constexpr int PANEL_HEIGHT = 32; static constexpr int MAX_EXTERNAL_APPS = 32; +static constexpr int MAX_LAUNCHER_ITEMS = 64; // External app discovered from 0:/apps/ manifest struct ExternalApp { @@ -29,6 +30,24 @@ struct ExternalApp { bool launch_with_home; }; +enum LauncherItemKind : uint8_t { + LAUNCHER_ITEM_EXTERNAL_APP = 0, + LAUNCHER_ITEM_BUILTIN_FILES, + LAUNCHER_ITEM_COMMAND_REBOOT, + LAUNCHER_ITEM_COMMAND_SHUTDOWN, + LAUNCHER_ITEM_SPECIAL_FOLDER, +}; + +struct LauncherItem { + char name[64]; + char category[48]; + char path[128]; + char search_text[192]; + SvgIcon* icon; + LauncherItemKind kind; + int ref_index; +}; + struct DesktopSettings { // Background bool bg_gradient; // true = gradient, false = solid @@ -72,6 +91,17 @@ struct DesktopState { uint8_t prev_buttons; bool app_menu_open; + bool launcher_open; + char launcher_query[64]; + int launcher_query_len; + LauncherItem launcher_items[MAX_LAUNCHER_ITEMS]; + int launcher_item_count; + int launcher_results[MAX_LAUNCHER_ITEMS]; + int launcher_result_count; + int launcher_selected; + int launcher_scroll; + bool super_left_down; + bool super_right_down; SvgIcon icon_terminal; SvgIcon icon_filemanager; @@ -80,7 +110,6 @@ struct DesktopState { SvgIcon icon_folder; SvgIcon icon_file; SvgIcon icon_network; - SvgIcon icon_calculator; SvgIcon icon_go_up; SvgIcon icon_go_back; SvgIcon icon_go_forward; diff --git a/programs/src/calculator/Makefile b/programs/src/calculator/Makefile new file mode 100644 index 0000000..ac9cae5 --- /dev/null +++ b/programs/src/calculator/Makefile @@ -0,0 +1,86 @@ +# Makefile for calculator (standalone Calculator) on MontaukOS +# Copyright (c) 2026 Daniel Hammer + +MAKEFLAGS += -rR +.SUFFIXES: + +# ---- Toolchain ---- + +TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf- +ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),) + CXX := $(TOOLCHAIN_PREFIX)g++ +else + CXX := g++ +endif + +# ---- Paths ---- + +PROG_INC := ../../include +LINK_LD := ../../link.ld +BINDIR := ../../bin +OBJDIR := obj +LIBDIR := ../../lib + +# ---- Compiler flags ---- + +CXXFLAGS := \ + -std=gnu++20 \ + -g -O2 -pipe \ + -Wall \ + -Wextra \ + -Wno-unused-parameter \ + -Wno-unused-function \ + -nostdinc \ + -ffreestanding \ + -fno-stack-protector \ + -fno-stack-check \ + -fno-PIC \ + -fno-rtti \ + -fno-exceptions \ + -ffunction-sections \ + -fdata-sections \ + -m64 \ + -march=x86-64 \ + -msse \ + -msse2 \ + -mno-red-zone \ + -mcmodel=small \ + -I $(PROG_INC) \ + -isystem $(PROG_INC)/libc \ + -isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \ + -isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include + +# ---- Linker flags ---- + +LDFLAGS := \ + -nostdlib \ + -static \ + -Wl,--build-id=none \ + -Wl,--gc-sections \ + -Wl,-m,elf_x86_64 \ + -z max-page-size=0x1000 \ + -T $(LINK_LD) + +# ---- Source files ---- + +SRCS := main.cpp stb_truetype_impl.cpp font_data.cpp +OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) + +# ---- Target ---- + +TARGET := $(BINDIR)/apps/calculator/calculator.elf + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(OBJS) $(LINK_LD) Makefile + mkdir -p $(BINDIR)/apps/calculator + $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@ + +$(OBJDIR)/%.o: %.cpp Makefile + mkdir -p $(OBJDIR) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +clean: + rm -rf $(OBJDIR) $(TARGET) diff --git a/programs/src/calculator/font_data.cpp b/programs/src/calculator/font_data.cpp new file mode 100644 index 0000000..a591f69 --- /dev/null +++ b/programs/src/calculator/font_data.cpp @@ -0,0 +1 @@ +#include "../desktop/font_data.cpp" diff --git a/programs/src/calculator/main.cpp b/programs/src/calculator/main.cpp new file mode 100644 index 0000000..88e17ec --- /dev/null +++ b/programs/src/calculator/main.cpp @@ -0,0 +1,662 @@ +/* + * 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(colors::ACCENT); + + char user[64]; + montauk::memset(user, 0, sizeof(user)); + if (montauk::getuser(user, sizeof(user)) > 0 && user[0]) { + auto doc = montauk::config::load_user(user, "desktop"); + int64_t accent = doc.get_int("appearance.accent_color", -1); + if (accent >= 0) { + calc_set_accent(Color::from_rgb( + (uint8_t)((accent >> 16) & 0xFF), + (uint8_t)((accent >> 8) & 0xFF), + (uint8_t)(accent & 0xFF))); + doc.destroy(); + return; + } + doc.destroy(); + } + + auto doc = montauk::config::load("desktop"); + int64_t accent = doc.get_int("appearance.accent_color", -1); + if (accent >= 0) { + calc_set_accent(Color::from_rgb( + (uint8_t)((accent >> 16) & 0xFF), + (uint8_t)((accent >> 8) & 0xFF), + (uint8_t)(accent & 0xFF))); + } + doc.destroy(); +} + +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); +} diff --git a/programs/src/calculator/manifest.toml b/programs/src/calculator/manifest.toml new file mode 100644 index 0000000..6d34aad --- /dev/null +++ b/programs/src/calculator/manifest.toml @@ -0,0 +1,11 @@ +[app] +name = "Calculator" +binary = "calculator.elf" +icon = "accessories-calculator.svg" + +[menu] +category = "Applications" +visible = true + +[launch] +pass_home_dir = false diff --git a/programs/src/calculator/stb_truetype_impl.cpp b/programs/src/calculator/stb_truetype_impl.cpp new file mode 100644 index 0000000..dd91785 --- /dev/null +++ b/programs/src/calculator/stb_truetype_impl.cpp @@ -0,0 +1,33 @@ +/* + * stb_truetype_impl.cpp + * Single compilation unit for stb_truetype in MontaukOS freestanding environment + * Copyright (c) 2026 Daniel Hammer + */ + +#include +#include +#include +#include +#include + +#define STBTT_ifloor(x) ((int) stb_floor(x)) +#define STBTT_iceil(x) ((int) stb_ceil(x)) +#define STBTT_sqrt(x) stb_sqrt(x) +#define STBTT_pow(x,y) stb_pow(x,y) +#define STBTT_fmod(x,y) stb_fmod(x,y) +#define STBTT_cos(x) stb_cos(x) +#define STBTT_acos(x) stb_acos(x) +#define STBTT_fabs(x) stb_fabs(x) + +#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x)) +#define STBTT_free(x,u) ((void)(u), montauk::mfree(x)) + +#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n) +#define STBTT_memset(d,v,n) montauk::memset(d,v,n) + +#define STBTT_strlen(x) montauk::slen(x) + +#define STBTT_assert(x) ((void)(x)) + +#define STB_TRUETYPE_IMPLEMENTATION +#include diff --git a/programs/src/desktop/Makefile b/programs/src/desktop/Makefile index 99be105..671f170 100644 --- a/programs/src/desktop/Makefile +++ b/programs/src/desktop/Makefile @@ -67,7 +67,7 @@ LDFLAGS := \ # ---- C++ source files ---- -CORE_SRCS := main.cpp window.cpp panel.cpp compose.cpp input.cpp dialogs.cpp font_data.cpp stb_truetype_impl.cpp +CORE_SRCS := main.cpp window.cpp panel.cpp compose.cpp input.cpp dialogs.cpp launcher.cpp font_data.cpp stb_truetype_impl.cpp APP_SRCS := $(wildcard apps/*.cpp) SRCS := $(CORE_SRCS) $(APP_SRCS) OBJS := $(addprefix $(OBJDIR)/,$(notdir $(SRCS:.cpp=.o))) diff --git a/programs/src/desktop/apps/app_calculator.cpp b/programs/src/desktop/apps/app_calculator.cpp deleted file mode 100644 index a9fa59b..0000000 --- a/programs/src/desktop/apps/app_calculator.cpp +++ /dev/null @@ -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; -} diff --git a/programs/src/desktop/apps/app_external_launchers.cpp b/programs/src/desktop/apps/app_external_launchers.cpp index a2f4a6d..8c0689c 100644 --- a/programs/src/desktop/apps/app_external_launchers.cpp +++ b/programs/src/desktop/apps/app_external_launchers.cpp @@ -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"); diff --git a/programs/src/desktop/apps/app_filemanager.cpp b/programs/src/desktop/apps/app_filemanager.cpp index 837678c..e78f2f8 100644 --- a/programs/src/desktop/apps/app_filemanager.cpp +++ b/programs/src/desktop/apps/app_filemanager.cpp @@ -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); +} diff --git a/programs/src/desktop/apps/apps_common.hpp b/programs/src/desktop/apps/apps_common.hpp index 73b1223..31822dc 100644 --- a/programs/src/desktop/apps/apps_common.hpp +++ b/programs/src/desktop/apps/apps_common.hpp @@ -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); diff --git a/programs/src/desktop/compose.cpp b/programs/src/desktop/compose.cpp index b868a1b..37094d0 100644 --- a/programs/src/desktop/compose.cpp +++ b/programs/src/desktop/compose.cpp @@ -270,6 +270,10 @@ void gui::desktop_compose(DesktopState* ds) { } } + if (ds->launcher_open) { + desktop_draw_launcher(ds); + } + // Draw snap preview overlay while dragging to screen edge for (int i = 0; i < ds->window_count; i++) { Window* win = &ds->windows[i]; @@ -288,31 +292,33 @@ void gui::desktop_compose(DesktopState* ds) { // Determine cursor style based on resize hover or active resize CursorStyle cur_style = CURSOR_ARROW; - for (int i = ds->window_count - 1; i >= 0; i--) { - Window* win = &ds->windows[i]; - if (win->resizing) { - cur_style = cursor_for_edge(win->resize_edge); - break; - } - if (win->state == WIN_MINIMIZED || win->state == WIN_CLOSED || win->state == WIN_MAXIMIZED) - continue; - if (win->frame.contains(ds->mouse.x, ds->mouse.y)) { - ResizeEdge edge = hit_test_resize_edge(win->frame, ds->mouse.x, ds->mouse.y); - if (edge != RESIZE_NONE) { - cur_style = cursor_for_edge(edge); + if (!ds->launcher_open) { + for (int i = ds->window_count - 1; i >= 0; i--) { + Window* win = &ds->windows[i]; + if (win->resizing) { + cur_style = cursor_for_edge(win->resize_edge); + break; + } + if (win->state == WIN_MINIMIZED || win->state == WIN_CLOSED || win->state == WIN_MAXIMIZED) + continue; + if (win->frame.contains(ds->mouse.x, ds->mouse.y)) { + ResizeEdge edge = hit_test_resize_edge(win->frame, ds->mouse.x, ds->mouse.y); + if (edge != RESIZE_NONE) { + cur_style = cursor_for_edge(edge); + } + break; } - break; } - } - // Check if focused external window requests a cursor style - if (cur_style == CURSOR_ARROW && ds->focused_window >= 0) { - Window* fwin = &ds->windows[ds->focused_window]; - if (fwin->external && fwin->ext_cursor > 0) { - Rect cr = fwin->content_rect(); - if (cr.contains(ds->mouse.x, ds->mouse.y)) { - if (fwin->ext_cursor == 1) cur_style = CURSOR_RESIZE_H; - else if (fwin->ext_cursor == 2) cur_style = CURSOR_RESIZE_V; + // Check if focused external window requests a cursor style + if (cur_style == CURSOR_ARROW && ds->focused_window >= 0) { + Window* fwin = &ds->windows[ds->focused_window]; + if (fwin->external && fwin->ext_cursor > 0) { + Rect cr = fwin->content_rect(); + if (cr.contains(ds->mouse.x, ds->mouse.y)) { + if (fwin->ext_cursor == 1) cur_style = CURSOR_RESIZE_H; + else if (fwin->ext_cursor == 2) cur_style = CURSOR_RESIZE_V; + } } } } diff --git a/programs/src/desktop/desktop_internal.hpp b/programs/src/desktop/desktop_internal.hpp index f8740ea..555fc4e 100644 --- a/programs/src/desktop/desktop_internal.hpp +++ b/programs/src/desktop/desktop_internal.hpp @@ -127,9 +127,18 @@ void desktop_draw_lock_screen(gui::DesktopState* ds); // main.cpp void desktop_scan_apps(gui::DesktopState* ds); void desktop_build_menu(gui::DesktopState* ds); +void desktop_open_launcher(gui::DesktopState* ds); +void desktop_close_launcher(gui::DesktopState* ds); // Month names (shared by panel clock) inline const char* month_names[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; + +// launcher.cpp +void desktop_init_launcher(gui::DesktopState* ds); +bool desktop_handle_launcher_keyboard(gui::DesktopState* ds, const Montauk::KeyEvent& key); +void desktop_handle_launcher_mouse(gui::DesktopState* ds); +void desktop_draw_launcher(gui::DesktopState* ds); +uint64_t desktop_launcher_blink_token(const gui::DesktopState* ds, uint64_t now); diff --git a/programs/src/desktop/input.cpp b/programs/src/desktop/input.cpp index 3b4160d..7c3adc8 100644 --- a/programs/src/desktop/input.cpp +++ b/programs/src/desktop/input.cpp @@ -18,6 +18,7 @@ static void lock_screen(DesktopState* ds) { ds->lock_error[0] = '\0'; ds->lock_show_error = false; ds->app_menu_open = false; + desktop_close_launcher(ds); ds->ctx_menu_open = false; ds->net_popup_open = false; ds->vol_popup_open = false; @@ -132,6 +133,11 @@ void gui::desktop_handle_mouse(DesktopState* ds) { return; } + if (ds->launcher_open) { + desktop_handle_launcher_mouse(ds); + return; + } + int mx = ds->mouse.x; int my = ds->mouse.y; uint8_t buttons = ds->mouse.buttons; @@ -349,7 +355,6 @@ void gui::desktop_handle_mouse(DesktopState* ds) { switch (row.app_id) { case 1: open_filemanager(ds); break; case 2: open_sysinfo(ds); break; - case 3: open_calculator(ds); break; case 11: open_settings(ds); break; case 12: open_reboot_dialog(ds); break; case 14: open_shutdown_dialog(ds); break; @@ -730,6 +735,10 @@ void gui::desktop_handle_mouse(DesktopState* ds) { } void gui::desktop_handle_keyboard(DesktopState* ds, const Montauk::KeyEvent& key) { + if (desktop_handle_launcher_keyboard(ds, key)) { + return; + } + if (ds->screen_locked) { handle_lock_keyboard(ds, key); return; diff --git a/programs/src/desktop/launcher.cpp b/programs/src/desktop/launcher.cpp new file mode 100644 index 0000000..e6f9e42 --- /dev/null +++ b/programs/src/desktop/launcher.cpp @@ -0,0 +1,618 @@ +/* + * launcher.cpp + * Spotlight-style launcher indexing, input, and rendering + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "desktop_internal.hpp" + +static constexpr int LAUNCHER_W = 560; +static constexpr int LAUNCHER_SEARCH_H = 52; +static constexpr int LAUNCHER_ROW_H = 52; +static constexpr int LAUNCHER_EMPTY_H = 84; +static constexpr int LAUNCHER_VISIBLE_ROWS = 7; +static constexpr int LAUNCHER_TEXT_PAD = 18; +static constexpr int LAUNCHER_CURSOR_BLINK_MS = 700; +static constexpr int LAUNCHER_SPECIAL_FOLDER_COUNT = 6; + +static constexpr Color LAUNCHER_BORDER = Color::from_rgb(0xD4, 0xDB, 0xE4); +static constexpr Color LAUNCHER_FIELD_BG = Color::from_rgb(0xFF, 0xFF, 0xFF); +static constexpr Color LAUNCHER_SELECTED_BG = Color::from_rgb(0xDA, 0xE7, 0xFD); +static constexpr Color LAUNCHER_HOVER_BG = Color::from_rgb(0xEC, 0xF3, 0xFE); +static constexpr Color LAUNCHER_MUTED = Color::from_rgb(0x6F, 0x7B, 0x89); +static constexpr Color LAUNCHER_PLACEHOLDER = Color::from_rgb(0x98, 0xA1, 0xAD); + +static const char* launcher_special_folder_names[LAUNCHER_SPECIAL_FOLDER_COUNT] = { + "Documents", "Desktop", "Music", "Videos", "Pictures", "Downloads" +}; + +static char fold_ascii(char c) { + return (c >= 'A' && c <= 'Z') ? (char)(c - 'A' + 'a') : c; +} + +static bool launcher_super_down(const DesktopState* ds) { + return ds->super_left_down || ds->super_right_down; +} + +static bool launcher_show_results(const DesktopState* ds) { + return ds->launcher_query_len > 0; +} + +static int launcher_search_y(const DesktopState* ds) { + int avail_h = ds->screen_h - PANEL_HEIGHT; + int y = PANEL_HEIGHT + ((avail_h - LAUNCHER_SEARCH_H) * 2) / 10; + if (y < PANEL_HEIGHT + 8) y = PANEL_HEIGHT + 8; + if (y + LAUNCHER_SEARCH_H > ds->screen_h - 8) y = ds->screen_h - LAUNCHER_SEARCH_H - 8; + return y; +} + +static int launcher_visible_rows(const DesktopState* ds) { + if (ds->launcher_result_count <= 0) return 0; + return gui_min(ds->launcher_result_count, LAUNCHER_VISIBLE_ROWS); +} + +static int launcher_scroll_max(const DesktopState* ds) { + int extra = ds->launcher_result_count - LAUNCHER_VISIBLE_ROWS; + return extra > 0 ? extra : 0; +} + +static Rect launcher_bounds(const DesktopState* ds) { + int w = gui_min(LAUNCHER_W, ds->screen_w - 16); + if (w < 260) w = ds->screen_w; + int result_h = launcher_show_results(ds) + ? (ds->launcher_result_count > 0 + ? launcher_visible_rows(ds) * LAUNCHER_ROW_H + : LAUNCHER_EMPTY_H) + : 0; + int h = LAUNCHER_SEARCH_H + result_h; + int x = (ds->screen_w - w) / 2; + int y = launcher_search_y(ds); + return {x, y, w, h}; +} + +static Rect launcher_search_rect(const DesktopState* ds) { + Rect bounds = launcher_bounds(ds); + return {bounds.x, launcher_search_y(ds), bounds.w, LAUNCHER_SEARCH_H}; +} + +static Rect launcher_results_rect(const DesktopState* ds) { + Rect search = launcher_search_rect(ds); + Rect bounds = launcher_bounds(ds); + int y = search.y + search.h; + return { + bounds.x, + y, + bounds.w, + bounds.y + bounds.h - y + }; +} + +static int launcher_visible_result_count(const DesktopState* ds) { + if (!launcher_show_results(ds)) return 0; + int remaining = ds->launcher_result_count - ds->launcher_scroll; + if (remaining <= 0) return 0; + return gui_min(remaining, LAUNCHER_VISIBLE_ROWS); +} + +static Rect launcher_row_rect(const DesktopState* ds, int visible_idx) { + Rect list = launcher_results_rect(ds); + return {list.x, list.y + visible_idx * LAUNCHER_ROW_H, list.w, LAUNCHER_ROW_H}; +} + +static bool launcher_contains_folded(const char* haystack, const char* needle) { + if (!needle[0]) return true; + + int nlen = montauk::slen(needle); + if (nlen == 0) return true; + + for (int i = 0; haystack[i]; i++) { + int j = 0; + while (j < nlen && haystack[i + j] && + fold_ascii(haystack[i + j]) == fold_ascii(needle[j])) { + j++; + } + if (j == nlen) return true; + } + + return false; +} + +static void launcher_append_search_term(char* dst, int dst_max, const char* term) { + if (!term || term[0] == '\0') return; + if (dst[0] != '\0') str_append(dst, " ", dst_max); + str_append(dst, term, dst_max); +} + +static void launcher_build_search_text(char* dst, int dst_max, + const char* name, const char* category, + const char* path, const char* keywords) { + dst[0] = '\0'; + launcher_append_search_term(dst, dst_max, name); + launcher_append_search_term(dst, dst_max, category); + launcher_append_search_term(dst, dst_max, path); + launcher_append_search_term(dst, dst_max, keywords); +} + +static bool launcher_item_matches(const LauncherItem* item, const char* query) { + if (!query[0]) return false; + return launcher_contains_folded(item->search_text, query); +} + +static int launcher_compare_items(const LauncherItem* a, const LauncherItem* b) { + for (int i = 0; a->name[i] || b->name[i]; i++) { + char ca = fold_ascii(a->name[i]); + char cb = fold_ascii(b->name[i]); + if (ca != cb) return (int)((unsigned char)ca) - (int)((unsigned char)cb); + } + + for (int i = 0; a->category[i] || b->category[i]; i++) { + char ca = fold_ascii(a->category[i]); + char cb = fold_ascii(b->category[i]); + if (ca != cb) return (int)((unsigned char)ca) - (int)((unsigned char)cb); + } + + return 0; +} + +static void launcher_add_item(DesktopState* ds, LauncherItemKind kind, + const char* name, const char* category, + SvgIcon* icon, int ref_index, + const char* path = nullptr, + const char* keywords = nullptr) { + if (ds->launcher_item_count >= MAX_LAUNCHER_ITEMS) return; + + LauncherItem* item = &ds->launcher_items[ds->launcher_item_count++]; + montauk::memset(item, 0, sizeof(LauncherItem)); + montauk::strncpy(item->name, name ? name : "", sizeof(item->name)); + montauk::strncpy(item->category, category ? category : "", sizeof(item->category)); + if (path) montauk::strncpy(item->path, path, sizeof(item->path)); + launcher_build_search_text(item->search_text, sizeof(item->search_text), + item->name, item->category, item->path, keywords); + item->icon = icon ? icon : &ds->icon_exec; + item->kind = kind; + item->ref_index = ref_index; +} + +static void desktop_launch_external_app(DesktopState* ds, const ExternalApp* app) { + if (!app) return; + + if (app->launch_with_home) { + montauk::spawn(app->binary_path, ds->home_dir); + } else { + montauk::spawn(app->binary_path); + } +} + +static void desktop_build_launcher_items(DesktopState* ds) { + ds->launcher_item_count = 0; + + ensure_filemanager_icons_loaded(ds); + + launcher_add_item(ds, LAUNCHER_ITEM_BUILTIN_FILES, + "Files", "Built-in App", + &ds->icon_filemanager, -1, nullptr, + "file manager browser"); + + for (int i = 0; i < ds->external_app_count; i++) { + ExternalApp* app = &ds->external_apps[i]; + SvgIcon* icon = app->icon.pixels ? &app->icon : &ds->icon_exec; + launcher_add_item(ds, LAUNCHER_ITEM_EXTERNAL_APP, + app->name, app->category, + icon, i, nullptr, nullptr); + } + + launcher_add_item(ds, LAUNCHER_ITEM_COMMAND_REBOOT, + "Reboot", "Command", + &ds->icon_reboot, -1, nullptr, + "restart reboot power"); + launcher_add_item(ds, LAUNCHER_ITEM_COMMAND_SHUTDOWN, + "Shutdown", "Command", + &ds->icon_shutdown, -1, nullptr, + "power off halt shutdown"); + + if (ds->home_dir[0] != '\0') { + for (int sf = 0; sf < LAUNCHER_SPECIAL_FOLDER_COUNT; sf++) { + char folder_path[128]; + montauk::strcpy(folder_path, ds->home_dir); + int plen = montauk::slen(folder_path); + if (plen > 0 && folder_path[plen - 1] != '/') + str_append(folder_path, "/", sizeof(folder_path)); + str_append(folder_path, launcher_special_folder_names[sf], sizeof(folder_path)); + + int probe_fd = montauk::open(folder_path); + if (probe_fd < 0) continue; + montauk::close(probe_fd); + + SvgIcon* icon = ds->icon_special_folder[sf].pixels + ? &ds->icon_special_folder[sf] + : &ds->icon_folder; + launcher_add_item(ds, LAUNCHER_ITEM_SPECIAL_FOLDER, + launcher_special_folder_names[sf], "Folder", + icon, sf, folder_path, "user folder"); + } + } +} + +static void desktop_update_launcher_results(DesktopState* ds) { + int selected_item = -1; + if (ds->launcher_selected >= 0 && ds->launcher_selected < ds->launcher_result_count) { + selected_item = ds->launcher_results[ds->launcher_selected]; + } + + ds->launcher_result_count = 0; + if (ds->launcher_query[0] == '\0') { + ds->launcher_selected = -1; + ds->launcher_scroll = 0; + return; + } + + for (int i = 0; i < ds->launcher_item_count; i++) { + if (!launcher_item_matches(&ds->launcher_items[i], ds->launcher_query)) continue; + ds->launcher_results[ds->launcher_result_count++] = i; + } + + for (int i = 1; i < ds->launcher_result_count; i++) { + int item_idx = ds->launcher_results[i]; + int j = i - 1; + while (j >= 0 && + launcher_compare_items(&ds->launcher_items[item_idx], + &ds->launcher_items[ds->launcher_results[j]]) < 0) { + ds->launcher_results[j + 1] = ds->launcher_results[j]; + j--; + } + ds->launcher_results[j + 1] = item_idx; + } + + if (ds->launcher_result_count <= 0) { + ds->launcher_selected = -1; + ds->launcher_scroll = 0; + return; + } + + int selected = 0; + if (selected_item >= 0) { + for (int i = 0; i < ds->launcher_result_count; i++) { + if (ds->launcher_results[i] == selected_item) { + selected = i; + break; + } + } + } + + ds->launcher_selected = gui_clamp(selected, 0, ds->launcher_result_count - 1); + int max_scroll = launcher_scroll_max(ds); + if (ds->launcher_scroll > max_scroll) ds->launcher_scroll = max_scroll; + if (ds->launcher_selected < ds->launcher_scroll) { + ds->launcher_scroll = ds->launcher_selected; + } + if (ds->launcher_selected >= ds->launcher_scroll + LAUNCHER_VISIBLE_ROWS) { + ds->launcher_scroll = ds->launcher_selected - LAUNCHER_VISIBLE_ROWS + 1; + } +} + +static void desktop_move_launcher_selection(DesktopState* ds, int delta) { + if (ds->launcher_result_count <= 0) return; + + if (ds->launcher_selected < 0) { + ds->launcher_selected = 0; + } else { + ds->launcher_selected = gui_clamp(ds->launcher_selected + delta, 0, ds->launcher_result_count - 1); + } + + if (ds->launcher_selected < ds->launcher_scroll) { + ds->launcher_scroll = ds->launcher_selected; + } else if (ds->launcher_selected >= ds->launcher_scroll + LAUNCHER_VISIBLE_ROWS) { + ds->launcher_scroll = ds->launcher_selected - LAUNCHER_VISIBLE_ROWS + 1; + } +} + +static void launcher_activate_selected(DesktopState* ds) { + if (ds->launcher_selected < 0 || ds->launcher_selected >= ds->launcher_result_count) return; + + int item_idx = ds->launcher_results[ds->launcher_selected]; + if (item_idx < 0 || item_idx >= ds->launcher_item_count) return; + + LauncherItem* item = &ds->launcher_items[item_idx]; + switch (item->kind) { + case LAUNCHER_ITEM_EXTERNAL_APP: + if (item->ref_index >= 0 && item->ref_index < ds->external_app_count) { + desktop_launch_external_app(ds, &ds->external_apps[item->ref_index]); + } + break; + case LAUNCHER_ITEM_BUILTIN_FILES: + open_filemanager(ds); + break; + case LAUNCHER_ITEM_COMMAND_REBOOT: + desktop_close_launcher(ds); + montauk::reset(); + return; + case LAUNCHER_ITEM_COMMAND_SHUTDOWN: + desktop_close_launcher(ds); + montauk::shutdown(); + return; + case LAUNCHER_ITEM_SPECIAL_FOLDER: + if (item->path[0] != '\0') { + open_filemanager_path(ds, item->path); + } + break; + } + + desktop_close_launcher(ds); +} + +static void launcher_update_super_state(DesktopState* ds, const Montauk::KeyEvent& key) { + uint8_t sc = key.scancode & 0x7F; + if (sc == 0x5B) { + ds->super_left_down = key.pressed; + } else if (sc == 0x5C) { + ds->super_right_down = key.pressed; + } +} + +static const char* fit_trailing_text(const char* text, int max_w) { + const char* visible = text; + while (*visible && text_width(visible) > max_w) visible++; + return visible; +} + +static void draw_rounded_panel(Framebuffer& fb, int x, int y, int w, int h, + int radius, Color border, Color bg) { + fill_rounded_rect(fb, x, y, w, h, radius, border); + if (w > 2 && h > 2) { + int inner_radius = radius > 0 ? radius - 1 : 0; + fill_rounded_rect(fb, x + 1, y + 1, w - 2, h - 2, inner_radius, bg); + } +} + +void desktop_init_launcher(DesktopState* ds) { + ds->launcher_open = false; + ds->launcher_query[0] = '\0'; + ds->launcher_query_len = 0; + ds->launcher_item_count = 0; + ds->launcher_result_count = 0; + ds->launcher_selected = -1; + ds->launcher_scroll = 0; + ds->super_left_down = false; + ds->super_right_down = false; +} + +void desktop_open_launcher(DesktopState* ds) { + desktop_build_launcher_items(ds); + ds->launcher_open = true; + ds->launcher_query[0] = '\0'; + ds->launcher_query_len = 0; + ds->launcher_selected = -1; + ds->launcher_scroll = 0; + ds->app_menu_open = false; + ds->ctx_menu_open = false; + ds->net_popup_open = false; + ds->vol_popup_open = false; + ds->vol_dragging = false; + desktop_update_launcher_results(ds); +} + +void desktop_close_launcher(DesktopState* ds) { + ds->launcher_open = false; + ds->launcher_scroll = 0; +} + +bool desktop_handle_launcher_keyboard(DesktopState* ds, const Montauk::KeyEvent& key) { + launcher_update_super_state(ds, key); + uint8_t sc = key.scancode & 0x7F; + + if (sc == 0x5B || sc == 0x5C) { + return true; + } + + if (ds->screen_locked) { + return false; + } + + if (key.pressed && sc == 0x39 && launcher_super_down(ds)) { + if (ds->launcher_open) { + desktop_close_launcher(ds); + } else { + desktop_open_launcher(ds); + } + return true; + } + + if (!ds->launcher_open) { + return false; + } + + if (!key.pressed) return true; + + if (key.ascii == '\n' || key.ascii == '\r' || sc == 0x1C) { + launcher_activate_selected(ds); + return true; + } + + if (sc == 0x01) { + desktop_close_launcher(ds); + return true; + } + + if (key.ascii == '\b' || sc == 0x0E) { + if (ds->launcher_query_len > 0) { + ds->launcher_query_len--; + ds->launcher_query[ds->launcher_query_len] = '\0'; + desktop_update_launcher_results(ds); + } + return true; + } + + if (sc == 0x48) { + desktop_move_launcher_selection(ds, -1); + return true; + } + if (sc == 0x50) { + desktop_move_launcher_selection(ds, 1); + return true; + } + if (sc == 0x49) { + desktop_move_launcher_selection(ds, -LAUNCHER_VISIBLE_ROWS); + return true; + } + if (sc == 0x51) { + desktop_move_launcher_selection(ds, LAUNCHER_VISIBLE_ROWS); + return true; + } + if (sc == 0x47) { + if (ds->launcher_result_count > 0) { + ds->launcher_selected = 0; + ds->launcher_scroll = 0; + } + return true; + } + if (sc == 0x4F) { + if (ds->launcher_result_count > 0) { + ds->launcher_selected = ds->launcher_result_count - 1; + ds->launcher_scroll = launcher_scroll_max(ds); + } + return true; + } + + if (key.ascii >= 0x20 && key.ascii < 0x7F && ds->launcher_query_len < 63) { + ds->launcher_query[ds->launcher_query_len++] = key.ascii; + ds->launcher_query[ds->launcher_query_len] = '\0'; + desktop_update_launcher_results(ds); + } + return true; +} + +void desktop_handle_launcher_mouse(DesktopState* ds) { + int mx = ds->mouse.x; + int my = ds->mouse.y; + uint8_t buttons = ds->mouse.buttons; + uint8_t prev = ds->prev_buttons; + bool left_pressed = (buttons & 0x01) && !(prev & 0x01); + bool right_pressed = (buttons & 0x02) && !(prev & 0x02); + + Rect bounds = launcher_bounds(ds); + if (right_pressed) { + desktop_close_launcher(ds); + return; + } + + if (!bounds.contains(mx, my)) { + if (left_pressed) { + desktop_close_launcher(ds); + } + return; + } + + int visible = launcher_visible_result_count(ds); + for (int i = 0; i < visible; i++) { + Rect row = launcher_row_rect(ds, i); + if (!row.contains(mx, my)) continue; + + ds->launcher_selected = ds->launcher_scroll + i; + if (left_pressed) { + launcher_activate_selected(ds); + } + return; + } +} + +void desktop_draw_launcher(DesktopState* ds) { + Framebuffer& fb = ds->fb; + int sfh = system_font_height(); + bool show_results = launcher_show_results(ds); + bool cursor_on = ((montauk::get_milliseconds() / LAUNCHER_CURSOR_BLINK_MS) & 1) == 0; + + Rect bounds = launcher_bounds(ds); + Rect search = launcher_search_rect(ds); + Rect list = launcher_results_rect(ds); + + draw_rounded_panel(fb, bounds.x, bounds.y, bounds.w, bounds.h, 16, LAUNCHER_BORDER, LAUNCHER_FIELD_BG); + + int count_w = 0; + int count_x = search.x + search.w - LAUNCHER_TEXT_PAD; + if (show_results) { + char count_str[32]; + snprintf(count_str, sizeof(count_str), "%d result%s", + ds->launcher_result_count, + ds->launcher_result_count == 1 ? "" : "s"); + count_w = text_width(count_str); + count_x -= count_w; + draw_text(fb, count_x, search.y + (search.h - sfh) / 2, count_str, LAUNCHER_MUTED); + } + + int query_x = search.x + LAUNCHER_TEXT_PAD; + int query_y = search.y + (search.h - sfh) / 2; + int query_max_w = search.w - LAUNCHER_TEXT_PAD * 2 - (show_results ? (count_w + 12) : 0); + if (query_max_w < 0) query_max_w = 0; + if (ds->launcher_query_len > 0) { + const char* visible = fit_trailing_text(ds->launcher_query, query_max_w); + draw_text(fb, query_x, query_y, visible, colors::TEXT_COLOR); + if (cursor_on) { + int cx = query_x + text_width(visible); + fb.fill_rect(cx + 1, query_y, 2, sfh, ds->settings.accent_color); + } + } else { + if (cursor_on) { + fb.fill_rect(query_x + 1, query_y, 2, sfh, ds->settings.accent_color); + } + draw_text(fb, query_x + 8, query_y, "Search apps, folders, and commands", LAUNCHER_PLACEHOLDER); + } + + if (show_results) { + fb.fill_rect(bounds.x + 2, search.y + search.h - 2, bounds.w - 4, 2, ds->settings.accent_color); + } + + if (!show_results) { + return; + } + + if (ds->launcher_result_count <= 0) { + const char* empty = "No matching results"; + const char* hint = "Try a different name, folder, or command"; + int empty_w = text_width(empty); + int hint_w = text_width(hint); + int cy = list.y + (list.h - sfh * 2 - 8) / 2; + draw_text(fb, list.x + (list.w - empty_w) / 2, cy, empty, colors::TEXT_COLOR); + draw_text(fb, list.x + (list.w - hint_w) / 2, cy + sfh + 8, hint, LAUNCHER_MUTED); + return; + } + + int visible = launcher_visible_result_count(ds); + for (int i = 0; i < visible; i++) { + int result_idx = ds->launcher_scroll + i; + if (result_idx < 0 || result_idx >= ds->launcher_result_count) continue; + + Rect row = launcher_row_rect(ds, i); + int item_idx = ds->launcher_results[result_idx]; + if (item_idx < 0 || item_idx >= ds->launcher_item_count) continue; + + LauncherItem* item = &ds->launcher_items[item_idx]; + bool selected = (result_idx == ds->launcher_selected); + bool hovered = row.contains(ds->mouse.x, ds->mouse.y); + if (selected) { + fill_rounded_rect(fb, row.x, row.y, row.w, row.h, 8, LAUNCHER_SELECTED_BG); + } else if (hovered) { + fill_rounded_rect(fb, row.x, row.y, row.w, row.h, 8, LAUNCHER_HOVER_BG); + } + + if (i > 0) { + fb.fill_rect(row.x + 8, row.y, row.w - 16, 1, Color::from_rgb(0xE3, 0xE8, 0xEF)); + } + + SvgIcon* icon = item->icon ? item->icon : &ds->icon_exec; + if (icon && icon->pixels) { + int ix = row.x + 12; + int iy = row.y + (row.h - icon->height) / 2; + fb.blit_alpha(ix, iy, icon->width, icon->height, icon->pixels); + } + + int text_x = row.x + 46; + int name_y = row.y + 9; + int cat_y = row.y + 27; + draw_text(fb, text_x, name_y, item->name, colors::TEXT_COLOR); + draw_text(fb, text_x, cat_y, item->category, LAUNCHER_MUTED); + } +} + +uint64_t desktop_launcher_blink_token(const DesktopState* ds, uint64_t now) { + return ds->launcher_open ? (now / LAUNCHER_CURSOR_BLINK_MS) : ~0ull; +} diff --git a/programs/src/desktop/main.cpp b/programs/src/desktop/main.cpp index 60709d3..fb8a5c1 100644 --- a/programs/src/desktop/main.cpp +++ b/programs/src/desktop/main.cpp @@ -117,7 +117,6 @@ struct EmbeddedAppDef { static const EmbeddedAppDef embedded_apps[] = { { "Files", 1, 0 }, - { "Calculator", 3, 0 }, { "System Info", 2, 2 }, }; @@ -128,7 +127,6 @@ static SvgIcon* icon_for_embedded(DesktopState* ds, int app_id) { switch (app_id) { case 1: return &ds->icon_filemanager; case 2: return &ds->icon_sysinfo; - case 3: return &ds->icon_calculator; default: return nullptr; } } @@ -187,6 +185,7 @@ void gui::desktop_init(DesktopState* ds) { ds->focused_window = -1; ds->prev_buttons = 0; ds->app_menu_open = false; + desktop_init_launcher(ds); montauk::memset(&ds->mouse, 0, sizeof(Montauk::MouseState)); montauk::set_mouse_bounds(ds->screen_w - 1, ds->screen_h - 1); @@ -200,7 +199,6 @@ void gui::desktop_init(DesktopState* ds) { ds->icon_folder = svg_load("0:/icons/folder.svg", 16, 16, defColor); ds->icon_file = svg_load("0:/icons/text-x-generic.svg", 16, 16, defColor); ds->icon_network = svg_load("0:/icons/network-wired-symbolic.svg", 16, 16, colors::PANEL_TEXT); - ds->icon_calculator = svg_load("0:/icons/accessories-calculator.svg", 20, 20, defColor); ds->icon_go_up = svg_load("0:/icons/go-up-symbolic.svg", 16, 16, defColor); ds->icon_go_back = svg_load("0:/icons/go-previous-symbolic.svg", 16, 16, defColor); ds->icon_go_forward = svg_load("0:/icons/go-next-symbolic.svg", 16, 16, defColor); @@ -538,6 +536,7 @@ bool desktop_poll_external_windows(DesktopState* ds) { void gui::desktop_run(DesktopState* ds) { uint64_t lastClockToken = 0; + uint64_t lastLauncherBlinkToken = ~0ull; bool firstFrame = true; for (;;) { @@ -594,6 +593,12 @@ void gui::desktop_run(DesktopState* ds) { uint64_t now = montauk::get_milliseconds(); sceneChanged |= desktop_panel_refresh_due(ds, now); + uint64_t launcherBlinkToken = desktop_launcher_blink_token(ds, now); + if (launcherBlinkToken != lastLauncherBlinkToken) { + lastLauncherBlinkToken = launcherBlinkToken; + sceneChanged = true; + } + uint64_t clockToken = desktop_clock_token(); if (clockToken != lastClockToken) { lastClockToken = clockToken; diff --git a/programs/src/video/manifest.toml b/programs/src/video/manifest.toml index d941820..3a5cabb 100644 --- a/programs/src/video/manifest.toml +++ b/programs/src/video/manifest.toml @@ -1,7 +1,7 @@ [app] name = "Videos" binary = "video.elf" -icon = "multimedia-video-player.svg" +icon = "video.svg" [menu] category = "Applications" diff --git a/programs/src/video/video.svg b/programs/src/video/video.svg new file mode 100644 index 0000000..b7dabf3 --- /dev/null +++ b/programs/src/video/video.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/scripts/install_apps.sh b/scripts/install_apps.sh index a1a22ad..57223b9 100755 --- a/scripts/install_apps.sh +++ b/scripts/install_apps.sh @@ -28,11 +28,12 @@ APPS=( "installer|mimetypes/scalable/text-x-install.svg" "volume|apps/scalable/pavucontrol.svg" "music|apps/scalable/audio-player.svg" - "video|apps/scalable/multimedia-video-player.svg" + "video|apps/scalable/video-player.svg" "bluetooth|apps/scalable/bluetooth.svg" "terminal|apps/scalable/utilities-terminal.svg" "klog|apps/scalable/utilities-terminal.svg" "procmgr|apps/scalable/system-monitor.svg" + "calculator|apps/scalable/accessories-calculator.svg" "rpgdemo|apps/scalable/utilities-terminal.svg" "paint|apps/scalable/kolourpaint.svg" "screenshot|apps/scalable/gnome-screenshot.svg" @@ -55,14 +56,17 @@ for entry in "${APPS[@]}"; do echo "install_apps: warning: $manifest not found" >&2 fi - # Copy icon + # Copy icon. Prefer a local app icon file when one exists; otherwise fall back + # to the shared theme asset declared in the app table above. icon_src="$ICON_SRC/$icon_rel" - # Extract target icon name from manifest icon_name=$(grep '^icon' "$manifest" 2>/dev/null | sed 's/.*= *"\(.*\)"/\1/' || echo "") - if [ -n "$icon_name" ] && [ -f "$icon_src" ]; then + local_icon="$SRC/$name/$icon_name" + if [ -n "$icon_name" ] && [ -f "$local_icon" ]; then + cp "$local_icon" "$app_dir/$icon_name" + elif [ -n "$icon_name" ] && [ -f "$icon_src" ]; then cp "$icon_src" "$app_dir/$icon_name" elif [ -n "$icon_name" ]; then - echo "install_apps: warning: icon $icon_src not found for $name" >&2 + echo "install_apps: warning: icon $local_icon or $icon_src not found for $name" >&2 fi installed=$((installed + 1)) diff --git a/template/docs/gui-apps.md b/template/docs/gui-apps.md index a717ace..d7f7081 100644 --- a/template/docs/gui-apps.md +++ b/template/docs/gui-apps.md @@ -29,7 +29,7 @@ MontaukOS supports two kinds of GUI applications: | **Window** | Own process, shared-memory pixel buffer | Embedded in desktop compositor | | **Event loop** | `win_poll()` syscall | Callback-driven (`on_draw`, `on_mouse`, `on_key`) | | **Drawing** | Direct pixel buffer writes | `Canvas` abstraction | -| **Examples** | Spreadsheet, Music, Wikipedia | Calculator, File Manager, Terminal | +| **Examples** | Spreadsheet, Music, Wikipedia | File Manager, System Info, Settings | **Choose standalone** when you need a separate process (e.g., networking, heavy computation, isolation). **Choose desktop-integrated** for lightweight tools that benefit from tight compositor integration.