feat: new calculator UI, video app icon, Super + Space search window
This commit is contained in:
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
#include "../desktop/font_data.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 <cstdint>
|
||||
#include <montauk/config.h>
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <gui/gui.hpp>
|
||||
#include <gui/canvas.hpp>
|
||||
#include <gui/font.hpp>
|
||||
#include <gui/standalone.hpp>
|
||||
#include <gui/truetype.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include <stdio.h>
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
[app]
|
||||
name = "Calculator"
|
||||
binary = "calculator.elf"
|
||||
icon = "accessories-calculator.svg"
|
||||
|
||||
[menu]
|
||||
category = "Applications"
|
||||
visible = true
|
||||
|
||||
[launch]
|
||||
pass_home_dir = false
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* stb_truetype_impl.cpp
|
||||
* Single compilation unit for stb_truetype in MontaukOS freestanding environment
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/string.h>
|
||||
#include <gui/stb_math.h>
|
||||
|
||||
#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 <gui/stb_truetype.h>
|
||||
@@ -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)))
|
||||
|
||||
@@ -1,404 +0,0 @@
|
||||
/*
|
||||
* app_calculator.cpp
|
||||
* MontaukOS Desktop - Calculator application
|
||||
* Integer-only 4-function calculator (values scaled by 100 for 2 decimal places)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "apps_common.hpp"
|
||||
|
||||
// ============================================================================
|
||||
// Calculator state
|
||||
// ============================================================================
|
||||
|
||||
struct CalcState {
|
||||
int64_t display_val; // current display value * 100
|
||||
int64_t accumulator; // stored accumulator * 100
|
||||
char pending_op; // '+', '-', '*', '/', or 0
|
||||
bool start_new; // next digit starts a new number
|
||||
bool has_decimal; // decimal point pressed
|
||||
int decimal_digits; // number of decimal digits entered
|
||||
char display_str[32]; // formatted display string
|
||||
};
|
||||
|
||||
static constexpr int CALC_DISPLAY_H = 56;
|
||||
static constexpr int CALC_BTN_W = 52;
|
||||
static constexpr int CALC_BTN_H = 40;
|
||||
static constexpr int CALC_BTN_PAD = 4;
|
||||
|
||||
// ============================================================================
|
||||
// Display formatting
|
||||
// ============================================================================
|
||||
|
||||
static void calc_format_display(CalcState* cs) {
|
||||
int64_t val = cs->display_val;
|
||||
bool neg = val < 0;
|
||||
if (neg) val = -val;
|
||||
|
||||
int64_t integer = val / 100;
|
||||
int64_t frac = val % 100;
|
||||
|
||||
if (cs->has_decimal || frac != 0) {
|
||||
// Show decimal
|
||||
char int_buf[20];
|
||||
int pos = 0;
|
||||
|
||||
if (integer == 0) {
|
||||
int_buf[pos++] = '0';
|
||||
} else {
|
||||
char tmp[20]; int ti = 0;
|
||||
int64_t v = integer;
|
||||
while (v > 0) { tmp[ti++] = '0' + (int)(v % 10); v /= 10; }
|
||||
while (ti > 0) int_buf[pos++] = tmp[--ti];
|
||||
}
|
||||
int_buf[pos] = '\0';
|
||||
|
||||
if (neg) {
|
||||
snprintf(cs->display_str, 32, "-%s.%02d", int_buf, (int)frac);
|
||||
} else {
|
||||
snprintf(cs->display_str, 32, "%s.%02d", int_buf, (int)frac);
|
||||
}
|
||||
|
||||
// Trim trailing zeros after decimal point (unless user is entering decimals)
|
||||
if (!cs->has_decimal) {
|
||||
int len = montauk::slen(cs->display_str);
|
||||
while (len > 1 && cs->display_str[len - 1] == '0') {
|
||||
cs->display_str[--len] = '\0';
|
||||
}
|
||||
if (len > 0 && cs->display_str[len - 1] == '.') {
|
||||
cs->display_str[--len] = '\0';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Integer display
|
||||
if (neg) {
|
||||
char int_buf[20];
|
||||
int pos = 0;
|
||||
int64_t v = integer;
|
||||
if (v == 0) {
|
||||
int_buf[pos++] = '0';
|
||||
} else {
|
||||
char tmp[20]; int ti = 0;
|
||||
while (v > 0) { tmp[ti++] = '0' + (int)(v % 10); v /= 10; }
|
||||
while (ti > 0) int_buf[pos++] = tmp[--ti];
|
||||
}
|
||||
int_buf[pos] = '\0';
|
||||
snprintf(cs->display_str, 32, "-%s", int_buf);
|
||||
} else {
|
||||
int pos = 0;
|
||||
int64_t v = integer;
|
||||
if (v == 0) {
|
||||
cs->display_str[pos++] = '0';
|
||||
} else {
|
||||
char tmp[20]; int ti = 0;
|
||||
while (v > 0) { tmp[ti++] = '0' + (int)(v % 10); v /= 10; }
|
||||
while (ti > 0) cs->display_str[pos++] = tmp[--ti];
|
||||
}
|
||||
cs->display_str[pos] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Calculator operations
|
||||
// ============================================================================
|
||||
|
||||
static void calc_apply_op(CalcState* cs) {
|
||||
if (cs->pending_op == 0) {
|
||||
cs->accumulator = cs->display_val;
|
||||
return;
|
||||
}
|
||||
switch (cs->pending_op) {
|
||||
case '+': cs->accumulator += cs->display_val; break;
|
||||
case '-': cs->accumulator -= cs->display_val; break;
|
||||
case '*': cs->accumulator = (cs->accumulator * cs->display_val) / 100; break;
|
||||
case '/':
|
||||
if (cs->display_val != 0)
|
||||
cs->accumulator = (cs->accumulator * 100) / cs->display_val;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void calc_input_digit(CalcState* cs, int digit) {
|
||||
if (cs->start_new) {
|
||||
cs->display_val = 0;
|
||||
cs->start_new = false;
|
||||
cs->has_decimal = false;
|
||||
cs->decimal_digits = 0;
|
||||
}
|
||||
|
||||
if (cs->has_decimal) {
|
||||
if (cs->decimal_digits < 2) {
|
||||
cs->decimal_digits++;
|
||||
bool neg = cs->display_val < 0;
|
||||
int64_t abs_val = neg ? -cs->display_val : cs->display_val;
|
||||
if (cs->decimal_digits == 1) {
|
||||
abs_val = (abs_val / 100) * 100 + digit * 10;
|
||||
} else {
|
||||
abs_val = abs_val + digit;
|
||||
}
|
||||
cs->display_val = neg ? -abs_val : abs_val;
|
||||
}
|
||||
} else {
|
||||
bool neg = cs->display_val < 0;
|
||||
int64_t abs_val = neg ? -cs->display_val : cs->display_val;
|
||||
int64_t integer = abs_val / 100;
|
||||
if (integer < 999999999) {
|
||||
integer = integer * 10 + digit;
|
||||
cs->display_val = integer * 100;
|
||||
if (neg) cs->display_val = -cs->display_val;
|
||||
}
|
||||
}
|
||||
calc_format_display(cs);
|
||||
}
|
||||
|
||||
static void calc_press_operator(CalcState* cs, char op) {
|
||||
if (!cs->start_new) {
|
||||
calc_apply_op(cs);
|
||||
cs->display_val = cs->accumulator;
|
||||
}
|
||||
cs->pending_op = op;
|
||||
cs->start_new = true;
|
||||
cs->has_decimal = false;
|
||||
cs->decimal_digits = 0;
|
||||
calc_format_display(cs);
|
||||
}
|
||||
|
||||
static void calc_press_equals(CalcState* cs) {
|
||||
calc_apply_op(cs);
|
||||
cs->display_val = cs->accumulator;
|
||||
cs->pending_op = 0;
|
||||
cs->start_new = true;
|
||||
cs->has_decimal = false;
|
||||
cs->decimal_digits = 0;
|
||||
calc_format_display(cs);
|
||||
}
|
||||
|
||||
static void calc_press_clear(CalcState* cs) {
|
||||
cs->display_val = 0;
|
||||
cs->accumulator = 0;
|
||||
cs->pending_op = 0;
|
||||
cs->start_new = false;
|
||||
cs->has_decimal = false;
|
||||
cs->decimal_digits = 0;
|
||||
calc_format_display(cs);
|
||||
}
|
||||
|
||||
static void calc_press_negate(CalcState* cs) {
|
||||
cs->display_val = -cs->display_val;
|
||||
calc_format_display(cs);
|
||||
}
|
||||
|
||||
static void calc_press_percent(CalcState* cs) {
|
||||
// Divide by 100: display_val is already scaled by 100, so dividing by 100 means /100
|
||||
cs->display_val = cs->display_val / 100;
|
||||
calc_format_display(cs);
|
||||
}
|
||||
|
||||
static void calc_press_decimal(CalcState* cs) {
|
||||
if (cs->start_new) {
|
||||
cs->display_val = 0;
|
||||
cs->start_new = false;
|
||||
cs->decimal_digits = 0;
|
||||
}
|
||||
cs->has_decimal = true;
|
||||
calc_format_display(cs);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Drawing
|
||||
// ============================================================================
|
||||
|
||||
// Button layout: labels[row][col]
|
||||
static const char* calc_labels[5][4] = {
|
||||
{ "C", "+/-", "%", "/" },
|
||||
{ "7", "8", "9", "*" },
|
||||
{ "4", "5", "6", "-" },
|
||||
{ "1", "2", "3", "+" },
|
||||
{ "0", "0", ".", "=" },
|
||||
};
|
||||
|
||||
static void calculator_on_draw(Window* win, Framebuffer& fb) {
|
||||
CalcState* cs = (CalcState*)win->app_data;
|
||||
if (!cs) return;
|
||||
|
||||
Canvas c(win);
|
||||
|
||||
// Background
|
||||
c.fill(Color::from_rgb(0xF0, 0xF0, 0xF0));
|
||||
|
||||
// Display area
|
||||
c.fill_rect(0, 0, c.w, CALC_DISPLAY_H, Color::from_rgb(0x2D, 0x2D, 0x2D));
|
||||
|
||||
// Display text (right-aligned, 2x scale)
|
||||
int text_w;
|
||||
int large_h;
|
||||
if (fonts::system_font && fonts::system_font->valid) {
|
||||
text_w = fonts::system_font->measure_text(cs->display_str, fonts::LARGE_SIZE);
|
||||
large_h = fonts::system_font->get_line_height(fonts::LARGE_SIZE);
|
||||
} else {
|
||||
int text_len = montauk::slen(cs->display_str);
|
||||
text_w = text_len * FONT_WIDTH * 2;
|
||||
large_h = FONT_HEIGHT * 2;
|
||||
}
|
||||
int tx = c.w - text_w - 12;
|
||||
int ty = (CALC_DISPLAY_H - large_h) / 2;
|
||||
if (tx < 4) tx = 4;
|
||||
c.text_2x(tx, ty, cs->display_str, colors::WHITE);
|
||||
|
||||
// Button grid
|
||||
int grid_y = CALC_DISPLAY_H + CALC_BTN_PAD;
|
||||
|
||||
for (int row = 0; row < 5; row++) {
|
||||
for (int col = 0; col < 4; col++) {
|
||||
// Skip second column of "0" button (it spans 2 cols)
|
||||
if (row == 4 && col == 1) continue;
|
||||
|
||||
int bx = CALC_BTN_PAD + col * (CALC_BTN_W + CALC_BTN_PAD);
|
||||
int by = grid_y + row * (CALC_BTN_H + CALC_BTN_PAD);
|
||||
int bw = CALC_BTN_W;
|
||||
|
||||
// "0" button spans 2 columns
|
||||
if (row == 4 && col == 0) {
|
||||
bw = CALC_BTN_W * 2 + CALC_BTN_PAD;
|
||||
}
|
||||
|
||||
// Button color
|
||||
Color btn_color;
|
||||
if (col == 3) {
|
||||
btn_color = colors::ACCENT;
|
||||
} else if (row == 0) {
|
||||
btn_color = Color::from_rgb(0xD0, 0xD0, 0xD0);
|
||||
} else {
|
||||
btn_color = Color::from_rgb(0xE8, 0xE8, 0xE8);
|
||||
}
|
||||
|
||||
// Draw button
|
||||
c.fill_rect(bx, by, bw, CALC_BTN_H, btn_color);
|
||||
|
||||
// Button text
|
||||
const char* label = calc_labels[row][col];
|
||||
Color label_color = (col == 3) ? colors::WHITE : colors::TEXT_COLOR;
|
||||
int label_w = text_width(label);
|
||||
int lx = bx + (bw - label_w) / 2;
|
||||
int ly = by + (CALC_BTN_H - system_font_height()) / 2;
|
||||
c.text(lx, ly, label, label_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Mouse handling
|
||||
// ============================================================================
|
||||
|
||||
static void calculator_on_mouse(Window* win, MouseEvent& ev) {
|
||||
CalcState* cs = (CalcState*)win->app_data;
|
||||
if (!cs) return;
|
||||
|
||||
if (!ev.left_pressed()) return;
|
||||
|
||||
Rect cr = win->content_rect();
|
||||
int local_x = ev.x - cr.x;
|
||||
int local_y = ev.y - cr.y;
|
||||
|
||||
// Check if click is in button grid
|
||||
int grid_y = CALC_DISPLAY_H + CALC_BTN_PAD;
|
||||
if (local_y < grid_y) return;
|
||||
|
||||
int row = (local_y - grid_y) / (CALC_BTN_H + CALC_BTN_PAD);
|
||||
int col = (local_x - CALC_BTN_PAD) / (CALC_BTN_W + CALC_BTN_PAD);
|
||||
|
||||
if (row < 0 || row > 4 || col < 0 || col > 3) return;
|
||||
|
||||
// Handle "0" button spanning 2 columns
|
||||
if (row == 4 && col <= 1) col = 0;
|
||||
|
||||
// Dispatch button press
|
||||
if (row == 0) {
|
||||
switch (col) {
|
||||
case 0: calc_press_clear(cs); break;
|
||||
case 1: calc_press_negate(cs); break;
|
||||
case 2: calc_press_percent(cs); break;
|
||||
case 3: calc_press_operator(cs, '/'); break;
|
||||
}
|
||||
} else if (row >= 1 && row <= 3) {
|
||||
if (col == 3) {
|
||||
char ops[] = {'*', '-', '+'};
|
||||
calc_press_operator(cs, ops[row - 1]);
|
||||
} else {
|
||||
int digits[3][3] = {{7,8,9},{4,5,6},{1,2,3}};
|
||||
calc_input_digit(cs, digits[row - 1][col]);
|
||||
}
|
||||
} else if (row == 4) {
|
||||
switch (col) {
|
||||
case 0: calc_input_digit(cs, 0); break;
|
||||
case 2: calc_press_decimal(cs); break;
|
||||
case 3: calc_press_equals(cs); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Keyboard handling
|
||||
// ============================================================================
|
||||
|
||||
static void calculator_on_key(Window* win, const Montauk::KeyEvent& key) {
|
||||
CalcState* cs = (CalcState*)win->app_data;
|
||||
if (!cs || !key.pressed) return;
|
||||
|
||||
if (key.ascii >= '0' && key.ascii <= '9') {
|
||||
calc_input_digit(cs, key.ascii - '0');
|
||||
} else if (key.ascii == '+') {
|
||||
calc_press_operator(cs, '+');
|
||||
} else if (key.ascii == '-') {
|
||||
calc_press_operator(cs, '-');
|
||||
} else if (key.ascii == '*') {
|
||||
calc_press_operator(cs, '*');
|
||||
} else if (key.ascii == '/') {
|
||||
calc_press_operator(cs, '/');
|
||||
} else if (key.ascii == '=' || key.ascii == '\n' || key.ascii == '\r') {
|
||||
calc_press_equals(cs);
|
||||
} else if (key.ascii == '.') {
|
||||
calc_press_decimal(cs);
|
||||
} else if (key.ascii == 'c' || key.ascii == 'C' || key.scancode == 0x0E) {
|
||||
calc_press_clear(cs);
|
||||
} else if (key.ascii == '%') {
|
||||
calc_press_percent(cs);
|
||||
}
|
||||
}
|
||||
|
||||
static void calculator_on_close(Window* win) {
|
||||
if (win->app_data) {
|
||||
montauk::mfree(win->app_data);
|
||||
win->app_data = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Calculator launcher
|
||||
// ============================================================================
|
||||
|
||||
void open_calculator(DesktopState* ds) {
|
||||
int calc_w = CALC_BTN_PAD + 4 * (CALC_BTN_W + CALC_BTN_PAD);
|
||||
int calc_h = CALC_DISPLAY_H + CALC_BTN_PAD + 5 * (CALC_BTN_H + CALC_BTN_PAD);
|
||||
|
||||
int idx = desktop_create_window(ds, "Calculator", 350, 150, calc_w, calc_h + TITLEBAR_HEIGHT + BORDER_WIDTH);
|
||||
if (idx < 0) return;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
CalcState* cs = (CalcState*)montauk::malloc(sizeof(CalcState));
|
||||
montauk::memset(cs, 0, sizeof(CalcState));
|
||||
cs->display_val = 0;
|
||||
cs->accumulator = 0;
|
||||
cs->pending_op = 0;
|
||||
cs->start_new = false;
|
||||
cs->has_decimal = false;
|
||||
cs->decimal_digits = 0;
|
||||
calc_format_display(cs);
|
||||
|
||||
win->app_data = cs;
|
||||
win->on_draw = calculator_on_draw;
|
||||
win->on_mouse = calculator_on_mouse;
|
||||
win->on_key = calculator_on_key;
|
||||
win->on_close = calculator_on_close;
|
||||
}
|
||||
@@ -17,6 +17,11 @@ void open_terminal(DesktopState* ds) {
|
||||
spawn_app("0:/apps/terminal/terminal.elf", home);
|
||||
}
|
||||
|
||||
void open_calculator(DesktopState* ds) {
|
||||
(void)ds;
|
||||
spawn_app("0:/apps/calculator/calculator.elf");
|
||||
}
|
||||
|
||||
void open_texteditor(DesktopState* ds) {
|
||||
(void)ds;
|
||||
spawn_app("0:/apps/texteditor/texteditor.elf");
|
||||
|
||||
@@ -1903,7 +1903,32 @@ static void filemanager_on_close(Window* win) {
|
||||
// File Manager launcher
|
||||
// ============================================================================
|
||||
|
||||
void open_filemanager(DesktopState* ds) {
|
||||
void ensure_filemanager_icons_loaded(DesktopState* ds) {
|
||||
if (!ds || ds->icon_drive.pixels) return;
|
||||
|
||||
Color defColor = colors::ICON_COLOR;
|
||||
ds->icon_drive = svg_load("0:/icons/drive-harddisk.svg", 16, 16, defColor);
|
||||
ds->icon_drive_lg = svg_load("0:/icons/drive-harddisk.svg", 48, 48, defColor);
|
||||
ds->icon_home_folder = svg_load("0:/icons/folder-blue-home.svg", 16, 16, defColor);
|
||||
ds->icon_home_folder_lg = svg_load("0:/icons/folder-blue-home.svg", 48, 48, defColor);
|
||||
ds->icon_apps = svg_load("0:/icons/folder-blue-development.svg", 16, 16, defColor);
|
||||
ds->icon_apps_lg = svg_load("0:/icons/folder-blue-development.svg", 48, 48, defColor);
|
||||
|
||||
for (int sf = 0; sf < SF_COUNT; sf++) {
|
||||
char icon_path[128];
|
||||
snprintf(icon_path, 128, "0:/icons/%s", sf_icons[sf]);
|
||||
ds->icon_special_folder[sf] = svg_load(icon_path, 16, 16, defColor);
|
||||
ds->icon_special_folder_lg[sf] = svg_load(icon_path, 48, 48, defColor);
|
||||
}
|
||||
ds->icon_delete = svg_load("0:/icons/trash-empty.svg", 16, 16, defColor);
|
||||
ds->icon_copy = svg_load("0:/icons/edit-copy.svg", 16, 16, defColor);
|
||||
ds->icon_cut = svg_load("0:/icons/edit-cut.svg", 16, 16, defColor);
|
||||
ds->icon_paste = svg_load("0:/icons/edit-paste.svg", 16, 16, defColor);
|
||||
ds->icon_rename = svg_load("0:/icons/edit-rename.svg", 16, 16, defColor);
|
||||
ds->icon_folder_new = svg_load("0:/icons/folder-new.svg", 16, 16, defColor);
|
||||
}
|
||||
|
||||
static void open_filemanager_internal(DesktopState* ds, const char* initial_path) {
|
||||
int idx = desktop_create_window(ds, "Files", 150, 120, 560, 420);
|
||||
if (idx < 0) return;
|
||||
|
||||
@@ -1919,32 +1944,20 @@ void open_filemanager(DesktopState* ds) {
|
||||
|
||||
fm->scrollbar.init(0, 0, FM_SCROLLBAR_W, 100);
|
||||
|
||||
// Lazy-load file manager icons on first open
|
||||
if (ds && !ds->icon_drive.pixels) {
|
||||
Color defColor = colors::ICON_COLOR;
|
||||
ds->icon_drive = svg_load("0:/icons/drive-harddisk.svg", 16, 16, defColor);
|
||||
ds->icon_drive_lg = svg_load("0:/icons/drive-harddisk.svg", 48, 48, defColor);
|
||||
ds->icon_home_folder = svg_load("0:/icons/folder-blue-home.svg", 16, 16, defColor);
|
||||
ds->icon_home_folder_lg = svg_load("0:/icons/folder-blue-home.svg", 48, 48, defColor);
|
||||
ds->icon_apps = svg_load("0:/icons/folder-blue-development.svg", 16, 16, defColor);
|
||||
ds->icon_apps_lg = svg_load("0:/icons/folder-blue-development.svg", 48, 48, defColor);
|
||||
ensure_filemanager_icons_loaded(ds);
|
||||
|
||||
// Special user folder icons
|
||||
for (int sf = 0; sf < SF_COUNT; sf++) {
|
||||
char icon_path[128];
|
||||
snprintf(icon_path, 128, "0:/icons/%s", sf_icons[sf]);
|
||||
ds->icon_special_folder[sf] = svg_load(icon_path, 16, 16, defColor);
|
||||
ds->icon_special_folder_lg[sf] = svg_load(icon_path, 48, 48, defColor);
|
||||
if (initial_path && initial_path[0] != '\0') {
|
||||
int probe_fd = montauk::open(initial_path);
|
||||
if (probe_fd >= 0) {
|
||||
montauk::close(probe_fd);
|
||||
montauk::strncpy(fm->current_path, initial_path, sizeof(fm->current_path));
|
||||
filemanager_read_dir(fm);
|
||||
} else {
|
||||
filemanager_read_drives(fm);
|
||||
}
|
||||
ds->icon_delete = svg_load("0:/icons/trash-empty.svg", 16, 16, defColor);
|
||||
ds->icon_copy = svg_load("0:/icons/edit-copy.svg", 16, 16, defColor);
|
||||
ds->icon_cut = svg_load("0:/icons/edit-cut.svg", 16, 16, defColor);
|
||||
ds->icon_paste = svg_load("0:/icons/edit-paste.svg", 16, 16, defColor);
|
||||
ds->icon_rename = svg_load("0:/icons/edit-rename.svg", 16, 16, defColor);
|
||||
ds->icon_folder_new = svg_load("0:/icons/folder-new.svg", 16, 16, defColor);
|
||||
} else {
|
||||
filemanager_read_drives(fm);
|
||||
}
|
||||
|
||||
filemanager_read_drives(fm);
|
||||
filemanager_push_history(fm);
|
||||
|
||||
win->app_data = fm;
|
||||
@@ -1953,3 +1966,11 @@ void open_filemanager(DesktopState* ds) {
|
||||
win->on_key = filemanager_on_key;
|
||||
win->on_close = filemanager_on_close;
|
||||
}
|
||||
|
||||
void open_filemanager(DesktopState* ds) {
|
||||
open_filemanager_internal(ds, nullptr);
|
||||
}
|
||||
|
||||
void open_filemanager_path(DesktopState* ds, const char* path) {
|
||||
open_filemanager_internal(ds, path);
|
||||
}
|
||||
|
||||
@@ -161,6 +161,8 @@ inline void format_size(char* buf, int size) {
|
||||
|
||||
void open_terminal(DesktopState* ds);
|
||||
void open_filemanager(DesktopState* ds);
|
||||
void open_filemanager_path(DesktopState* ds, const char* path);
|
||||
void ensure_filemanager_icons_loaded(DesktopState* ds);
|
||||
void open_sysinfo(DesktopState* ds);
|
||||
void open_calculator(DesktopState* ds);
|
||||
void open_texteditor(DesktopState* ds);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[app]
|
||||
name = "Videos"
|
||||
binary = "video.elf"
|
||||
icon = "multimedia-video-player.svg"
|
||||
icon = "video.svg"
|
||||
|
||||
[menu]
|
||||
category = "Applications"
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg width="512" height="512" version="1.1" viewBox="0 0 384 384" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="44" y="72" width="296" height="184" rx="20" ry="20" fill="#1f2933"/>
|
||||
<rect x="56" y="84" width="272" height="160" rx="14" ry="14" fill="#0b1117"/>
|
||||
<path d="m165 128 78 36-78 36z" fill="#d94f32"/>
|
||||
<rect x="154" y="268" width="76" height="18" rx="8" ry="8" fill="#2e3945"/>
|
||||
<rect x="126" y="292" width="132" height="16" rx="8" ry="8" fill="#44515f"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 477 B |
Reference in New Issue
Block a user