feat: add word processor, spreadsheet, kernel and userspace memory improvements
This commit is contained in:
@@ -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 desktop
|
||||
CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet desktop
|
||||
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
|
||||
|
||||
# Build targets: system programs go to bin/os/, games are handled separately.
|
||||
@@ -84,9 +84,9 @@ CA_CERTS := $(BINDIR)/etc/ca-certificates.crt
|
||||
# Home directory placeholder.
|
||||
HOMEKEEP := $(BINDIR)/home/.keep
|
||||
|
||||
.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview desktop icons fonts bearssl libc tls libjpeg
|
||||
.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet desktop icons fonts bearssl libc tls libjpeg
|
||||
|
||||
all: bearssl libc libjpeg tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview doom desktop icons fonts $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP)
|
||||
all: bearssl libc libjpeg tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet doom desktop icons fonts $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP)
|
||||
|
||||
# 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)
|
||||
@@ -135,6 +135,10 @@ imageviewer: libc libjpeg
|
||||
fontpreview:
|
||||
$(MAKE) -C src/fontpreview
|
||||
|
||||
# Build spreadsheet standalone GUI client (depends on libc).
|
||||
spreadsheet: libc
|
||||
$(MAKE) -C src/spreadsheet
|
||||
|
||||
# Build desktop via its own Makefile (depends on libc and libjpeg for wallpaper).
|
||||
desktop: libc libjpeg
|
||||
$(MAKE) -C src/desktop
|
||||
@@ -195,3 +199,4 @@ clean:
|
||||
$(MAKE) -C src/weather clean
|
||||
$(MAKE) -C src/imageviewer clean
|
||||
$(MAKE) -C src/fontpreview clean
|
||||
$(MAKE) -C src/spreadsheet clean
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -87,6 +87,7 @@ struct DesktopState {
|
||||
SvgIcon icon_procmgr;
|
||||
SvgIcon icon_mandelbrot;
|
||||
SvgIcon icon_devexplorer;
|
||||
SvgIcon icon_spreadsheet;
|
||||
|
||||
bool ctx_menu_open;
|
||||
int ctx_menu_x, ctx_menu_y;
|
||||
|
||||
@@ -321,7 +321,7 @@ struct SvgEdgeList {
|
||||
int capacity;
|
||||
|
||||
void init(int cap) {
|
||||
edges = (SvgEdge*)montauk::alloc(cap * sizeof(SvgEdge));
|
||||
edges = (SvgEdge*)montauk::malloc(cap * sizeof(SvgEdge));
|
||||
count = 0;
|
||||
capacity = cap;
|
||||
}
|
||||
@@ -800,7 +800,7 @@ inline void svg_rasterize(const SvgEdgeList& el, uint32_t* pixels, int w, int h,
|
||||
// Temporary array for x-intersections on each scanline
|
||||
// Allocate enough for all edges (each edge can intersect at most once per scanline)
|
||||
int maxIsect = el.count + 16;
|
||||
fixed_t* isect = (fixed_t*)montauk::alloc(maxIsect * sizeof(fixed_t));
|
||||
fixed_t* isect = (fixed_t*)montauk::malloc(maxIsect * sizeof(fixed_t));
|
||||
|
||||
for (int y = 0; y < h; ++y) {
|
||||
// Scanline center in fixed-point
|
||||
@@ -858,7 +858,7 @@ inline void svg_rasterize(const SvgEdgeList& el, uint32_t* pixels, int w, int h,
|
||||
}
|
||||
}
|
||||
|
||||
montauk::free(isect);
|
||||
montauk::mfree(isect);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1018,7 +1018,7 @@ inline void svg_rasterize_blend(const SvgEdgeList& el, uint32_t* pixels, int w,
|
||||
if (el.count == 0) return;
|
||||
|
||||
int maxIsect = el.count + 16;
|
||||
fixed_t* isect = (fixed_t*)montauk::alloc(maxIsect * sizeof(fixed_t));
|
||||
fixed_t* isect = (fixed_t*)montauk::malloc(maxIsect * sizeof(fixed_t));
|
||||
|
||||
uint32_t fr = (fill >> 16) & 0xFF;
|
||||
uint32_t fg = (fill >> 8) & 0xFF;
|
||||
@@ -1081,7 +1081,7 @@ inline void svg_rasterize_blend(const SvgEdgeList& el, uint32_t* pixels, int w,
|
||||
}
|
||||
}
|
||||
|
||||
montauk::free(isect);
|
||||
montauk::mfree(isect);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1091,7 +1091,7 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
|
||||
SvgIcon icon;
|
||||
icon.width = target_w;
|
||||
icon.height = target_h;
|
||||
icon.pixels = (uint32_t*)montauk::alloc(target_w * target_h * sizeof(uint32_t));
|
||||
icon.pixels = (uint32_t*)montauk::malloc(target_w * target_h * sizeof(uint32_t));
|
||||
// Clear to transparent
|
||||
svg_memset(icon.pixels, 0, target_w * target_h * sizeof(uint32_t));
|
||||
|
||||
@@ -1204,6 +1204,9 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
|
||||
SvgEdgeList el;
|
||||
el.init(SVG_MAX_EDGES);
|
||||
|
||||
// Heap-allocated path data buffer (avoids 8 KiB on the stack)
|
||||
char* d_buf = (char*)montauk::malloc(SVG_MAX_PATH_LEN);
|
||||
|
||||
// Scan for <path, <circle, <rect elements — rasterize each individually
|
||||
// Skip elements inside <defs> blocks (they define reusable items, not rendered directly)
|
||||
const char* p = svg_data;
|
||||
@@ -1249,7 +1252,6 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
|
||||
int alpha = svg_get_element_opacity(elem_start, elem_len);
|
||||
|
||||
// Extract and rasterize path
|
||||
char d_buf[SVG_MAX_PATH_LEN];
|
||||
int d_len = svg_get_attr(elem_start, elem_len, " d", d_buf, SVG_MAX_PATH_LEN);
|
||||
if (d_len > 0) {
|
||||
el.clear();
|
||||
@@ -1359,7 +1361,8 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
|
||||
++p;
|
||||
}
|
||||
|
||||
montauk::free(el.edges);
|
||||
montauk::mfree(d_buf);
|
||||
montauk::mfree(el.edges);
|
||||
return icon;
|
||||
}
|
||||
|
||||
@@ -1378,7 +1381,7 @@ inline SvgIcon svg_load(const char* vfs_path, int target_w, int target_h, Color
|
||||
return {nullptr, 0, 0};
|
||||
}
|
||||
|
||||
char* buf = (char*)montauk::alloc(size + 1);
|
||||
char* buf = (char*)montauk::malloc(size + 1);
|
||||
montauk::read(fd, (uint8_t*)buf, 0, size);
|
||||
montauk::close(fd);
|
||||
buf[size] = '\0';
|
||||
@@ -1389,12 +1392,12 @@ inline SvgIcon svg_load(const char* vfs_path, int target_w, int target_h, Color
|
||||
int hi_h = target_h * SS;
|
||||
|
||||
SvgIcon hi = svg_render(buf, (int)size, hi_w, hi_h, fill_color);
|
||||
montauk::free(buf);
|
||||
montauk::mfree(buf);
|
||||
|
||||
if (!hi.pixels) return {nullptr, 0, 0};
|
||||
|
||||
// Allocate final icon at target resolution
|
||||
uint32_t* out = (uint32_t*)montauk::alloc(target_w * target_h * 4);
|
||||
uint32_t* out = (uint32_t*)montauk::malloc(target_w * target_h * 4);
|
||||
for (int i = 0; i < target_w * target_h; i++) out[i] = 0;
|
||||
|
||||
// Downsample: average each SSxSS block using premultiplied alpha
|
||||
@@ -1432,7 +1435,7 @@ inline SvgIcon svg_load(const char* vfs_path, int target_w, int target_h, Color
|
||||
}
|
||||
}
|
||||
|
||||
montauk::free(hi.pixels);
|
||||
montauk::mfree(hi.pixels);
|
||||
return {out, target_w, target_h};
|
||||
}
|
||||
|
||||
@@ -1440,7 +1443,7 @@ inline SvgIcon svg_load(const char* vfs_path, int target_w, int target_h, Color
|
||||
// Free icon pixel data
|
||||
// ---------------------------------------------------------------------------
|
||||
inline void svg_free(SvgIcon& icon) {
|
||||
if (icon.pixels) montauk::free(icon.pixels);
|
||||
if (icon.pixels) montauk::mfree(icon.pixels);
|
||||
icon.pixels = nullptr;
|
||||
icon.width = 0;
|
||||
icon.height = 0;
|
||||
|
||||
@@ -130,6 +130,13 @@ static inline void terminal_init_cells(TerminalState* t, int cols, int rows, int
|
||||
int screen_cells = rows * cols;
|
||||
t->cells = (TermCell*)montauk::alloc(total_cells * sizeof(TermCell));
|
||||
t->alt_cells = (TermCell*)montauk::alloc(screen_cells * sizeof(TermCell));
|
||||
if (!t->cells || !t->alt_cells) {
|
||||
// Allocation failed — leave terminal in a safe but unusable state
|
||||
if (t->cells) { montauk::free(t->cells); t->cells = nullptr; }
|
||||
if (t->alt_cells) { montauk::free(t->alt_cells); t->alt_cells = nullptr; }
|
||||
t->cols = 0; t->rows = 0;
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < total_cells; i++) {
|
||||
t->cells[i] = {' ', colors::TERM_FG, colors::TERM_BG};
|
||||
}
|
||||
@@ -143,7 +150,8 @@ static inline void terminal_init(TerminalState* t, int cols, int rows) {
|
||||
t->cursor_visible = true;
|
||||
|
||||
t->child_pid = montauk::spawn_redir("0:/os/shell.elf");
|
||||
montauk::childio_settermsz(t->child_pid, cols, rows);
|
||||
if (t->child_pid > 0)
|
||||
montauk::childio_settermsz(t->child_pid, cols, rows);
|
||||
}
|
||||
|
||||
static inline void terminal_put_char(TerminalState* t, char ch) {
|
||||
@@ -467,7 +475,7 @@ static inline void terminal_feed(TerminalState* t, const char* data, int len) {
|
||||
}
|
||||
|
||||
static inline void terminal_render(TerminalState* t, uint32_t* pixels, int pw, int ph) {
|
||||
if (!t->dirty) return;
|
||||
if (!t->dirty || !t->cells) return;
|
||||
t->dirty = false;
|
||||
|
||||
int cell_w = mono_cell_width();
|
||||
@@ -593,6 +601,11 @@ static inline void terminal_resize(TerminalState* t, int new_cols, int new_rows)
|
||||
int new_total = new_capacity * new_cols;
|
||||
TermCell* new_cells = (TermCell*)montauk::alloc(new_total * sizeof(TermCell));
|
||||
TermCell* new_alt = (TermCell*)montauk::alloc(new_rows * new_cols * sizeof(TermCell));
|
||||
if (!new_cells || !new_alt) {
|
||||
if (new_cells) montauk::free(new_cells);
|
||||
if (new_alt) montauk::free(new_alt);
|
||||
return; // keep existing buffers
|
||||
}
|
||||
|
||||
// Clear new buffers
|
||||
for (int i = 0; i < new_total; i++)
|
||||
|
||||
@@ -24,9 +24,10 @@ namespace heap_detail {
|
||||
FreeNode* next;
|
||||
};
|
||||
|
||||
// Per-process heap state (single TU per program, so static is fine)
|
||||
static FreeNode g_head{0, nullptr};
|
||||
static bool g_initialized = false;
|
||||
// Per-process heap state — must be `inline` (not `static`) so that all
|
||||
// translation units in a multi-TU program share a single heap.
|
||||
inline FreeNode g_head{0, nullptr};
|
||||
inline bool g_initialized = false;
|
||||
|
||||
static inline Header* get_header(void* block) {
|
||||
return (Header*)((uint8_t*)block - sizeof(Header));
|
||||
|
||||
@@ -63,7 +63,7 @@ LDFLAGS := \
|
||||
|
||||
# ---- C++ source files ----
|
||||
|
||||
SRCS := main.cpp font_data.cpp stb_truetype_impl.cpp app_terminal.cpp app_filemanager.cpp app_sysinfo.cpp app_calculator.cpp app_texteditor.cpp app_klog.cpp app_wiki.cpp app_weather.cpp app_procmgr.cpp app_mandelbrot.cpp app_devexplorer.cpp app_settings.cpp app_doom.cpp
|
||||
SRCS := main.cpp font_data.cpp stb_truetype_impl.cpp app_terminal.cpp app_filemanager.cpp app_sysinfo.cpp app_calculator.cpp app_texteditor.cpp app_wordprocessor.cpp app_spreadsheet.cpp app_klog.cpp app_wiki.cpp app_weather.cpp app_procmgr.cpp app_mandelbrot.cpp app_devexplorer.cpp app_settings.cpp app_doom.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
|
||||
# ---- Target ----
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* app_spreadsheet.cpp
|
||||
* MontaukOS Desktop - Spreadsheet launcher (spawns standalone spreadsheet.elf)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "apps_common.hpp"
|
||||
|
||||
void open_spreadsheet(DesktopState* ds) {
|
||||
(void)ds;
|
||||
montauk::spawn("0:/os/spreadsheet.elf");
|
||||
}
|
||||
@@ -110,6 +110,8 @@ static void term_add_tab(TermTabState* tts, int cols, int rows) {
|
||||
static void terminal_on_draw(Window* win, Framebuffer& fb) {
|
||||
TermTabState* tts = (TermTabState*)win->app_data;
|
||||
if (!tts || tts->tab_count == 0) return;
|
||||
if (tts->active_tab < 0 || tts->active_tab >= tts->tab_count)
|
||||
tts->active_tab = 0;
|
||||
|
||||
Rect cr = win->content_rect();
|
||||
int term_h = cr.h - TERM_TAB_BAR_H;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -174,5 +174,7 @@ void open_devexplorer(DesktopState* ds);
|
||||
void open_settings(DesktopState* ds);
|
||||
void open_doom(DesktopState* ds);
|
||||
void open_reboot_dialog(DesktopState* ds);
|
||||
void open_wordprocessor(DesktopState* ds);
|
||||
void open_spreadsheet(DesktopState* ds);
|
||||
void open_shutdown_dialog(DesktopState* ds);
|
||||
void desktop_poll_external_windows(DesktopState* ds);
|
||||
|
||||
@@ -69,6 +69,7 @@ void gui::desktop_init(DesktopState* ds) {
|
||||
ds->icon_procmgr = svg_load("0:/icons/system-monitor.svg", 20, 20, defColor);
|
||||
ds->icon_mandelbrot = svg_load("0:/icons/applications-science.svg", 20, 20, defColor);
|
||||
ds->icon_devexplorer = svg_load("0:/icons/hardware.svg", 20, 20, defColor);
|
||||
ds->icon_spreadsheet = svg_load("0:/icons/spreadsheet.svg", 20, 20, defColor);
|
||||
|
||||
// Settings defaults
|
||||
ds->settings.bg_gradient = true;
|
||||
@@ -428,7 +429,7 @@ void gui::desktop_draw_panel(DesktopState* ds) {
|
||||
|
||||
static constexpr int MENU_W = 220;
|
||||
static constexpr int MENU_ITEM_H = 36;
|
||||
static constexpr int MENU_CAT_H = 28;
|
||||
static constexpr int MENU_CAT_H = 32;
|
||||
static constexpr int MENU_DIV_H = 10;
|
||||
|
||||
struct MenuRow {
|
||||
@@ -437,30 +438,53 @@ struct MenuRow {
|
||||
int app_id; // -1 for category headers / dividers
|
||||
};
|
||||
|
||||
static constexpr int MENU_ROW_COUNT = 20;
|
||||
static constexpr int MENU_ROW_COUNT = 22;
|
||||
static const MenuRow menu_rows[MENU_ROW_COUNT] = {
|
||||
{ true, "Applications", -1 },
|
||||
{ true, "Applications", -1 }, // cat 0
|
||||
{ false, "Terminal", 0 },
|
||||
{ false, "Files", 1 },
|
||||
{ false, "Text Editor", 4 },
|
||||
{ false, "Word Processor", 15 },
|
||||
{ false, "Spreadsheet", 16 },
|
||||
{ false, "Calculator", 3 },
|
||||
{ true, "Internet", -1 },
|
||||
{ true, "Internet", -1 }, // cat 1
|
||||
{ false, "Wikipedia", 9 },
|
||||
{ false, "Weather", 13 },
|
||||
{ true, "System", -1 },
|
||||
{ true, "System", -1 }, // cat 2
|
||||
{ false, "System Info", 2 },
|
||||
{ false, "Kernel Log", 5 },
|
||||
{ false, "Processes", 6 },
|
||||
{ false, "Devices", 8 },
|
||||
{ true, "Games", -1 },
|
||||
{ true, "Games", -1 }, // cat 3
|
||||
{ false, "Mandelbrot", 7 },
|
||||
{ false, "DOOM", 10 },
|
||||
{ true, "", -1 }, // divider
|
||||
{ true, "", -1 }, // divider (always visible)
|
||||
{ false, "Settings", 11 },
|
||||
{ false, "Reboot", 12 },
|
||||
{ false, "Shutdown", 14 },
|
||||
};
|
||||
|
||||
// Collapsible category state (categories 0-3 are toggleable; divider category always expanded)
|
||||
static constexpr int MENU_NUM_CATS = 5;
|
||||
static bool menu_cat_expanded[MENU_NUM_CATS] = { false, false, false, false, true };
|
||||
|
||||
// Map a menu_rows index to its category index (0-4), or -1 for items before any category
|
||||
static int menu_get_cat(int row_idx) {
|
||||
int cat = -1;
|
||||
for (int i = 0; i <= row_idx; i++)
|
||||
if (menu_rows[i].is_category) cat++;
|
||||
return cat;
|
||||
}
|
||||
|
||||
// Check if a row should be visible given current expand/collapse state
|
||||
static bool menu_row_visible(int row_idx) {
|
||||
const MenuRow& row = menu_rows[row_idx];
|
||||
if (row.is_category) return true; // category headers always visible
|
||||
int cat = menu_get_cat(row_idx);
|
||||
if (cat < 0 || cat >= MENU_NUM_CATS) return true;
|
||||
return menu_cat_expanded[cat];
|
||||
}
|
||||
|
||||
static int menu_row_height(const MenuRow& row) {
|
||||
if (!row.is_category) return MENU_ITEM_H;
|
||||
return row.label[0] ? MENU_CAT_H : MENU_DIV_H;
|
||||
@@ -469,7 +493,8 @@ static int menu_row_height(const MenuRow& row) {
|
||||
static int menu_total_height() {
|
||||
int h = 10; // top + bottom padding
|
||||
for (int i = 0; i < MENU_ROW_COUNT; i++)
|
||||
h += menu_row_height(menu_rows[i]);
|
||||
if (menu_row_visible(i))
|
||||
h += menu_row_height(menu_rows[i]);
|
||||
return h;
|
||||
}
|
||||
|
||||
@@ -488,7 +513,7 @@ static void desktop_draw_app_menu(DesktopState* ds) {
|
||||
draw_rect(fb, menu_x, menu_y, MENU_W, menu_h, colors::BORDER);
|
||||
|
||||
// Icon lookup by app_id
|
||||
SvgIcon* icons[15] = {
|
||||
SvgIcon* icons[17] = {
|
||||
&ds->icon_terminal, // 0
|
||||
&ds->icon_filemanager, // 1
|
||||
&ds->icon_sysinfo, // 2
|
||||
@@ -504,29 +529,64 @@ static void desktop_draw_app_menu(DesktopState* ds) {
|
||||
&ds->icon_reboot, // 12
|
||||
&ds->icon_weather, // 13
|
||||
&ds->icon_shutdown, // 14
|
||||
&ds->icon_texteditor, // 15
|
||||
&ds->icon_spreadsheet, // 16
|
||||
};
|
||||
|
||||
int mx = ds->mouse.x;
|
||||
int my = ds->mouse.y;
|
||||
int iy = menu_y + 5;
|
||||
int cur_cat = -1;
|
||||
|
||||
for (int i = 0; i < MENU_ROW_COUNT; i++) {
|
||||
const MenuRow& row = menu_rows[i];
|
||||
|
||||
if (row.is_category) cur_cat++;
|
||||
if (!menu_row_visible(i)) continue;
|
||||
|
||||
int row_h = menu_row_height(row);
|
||||
|
||||
if (row.is_category) {
|
||||
// Separator line above (except first category)
|
||||
if (i > 0) {
|
||||
for (int sx = menu_x + 8; sx < menu_x + MENU_W - 8; sx++)
|
||||
fb.put_pixel(sx, iy + row_h / 2, colors::BORDER);
|
||||
fb.put_pixel(sx, iy + 1, colors::BORDER);
|
||||
}
|
||||
|
||||
// Category label (dimmed), skip for divider-only rows
|
||||
// Category header with hover + expand/collapse indicator
|
||||
if (row.label[0]) {
|
||||
int tx = menu_x + 12;
|
||||
Rect cat_rect = {menu_x + 4, iy, MENU_W - 8, row_h};
|
||||
bool hovered = cat_rect.contains(mx, my);
|
||||
if (hovered)
|
||||
fill_rounded_rect(fb, cat_rect.x, cat_rect.y, cat_rect.w, cat_rect.h, 4,
|
||||
Color::from_rgb(0xE8, 0xE8, 0xE8));
|
||||
|
||||
// Arrow indicator: > for collapsed, v for expanded
|
||||
bool expanded = (cur_cat >= 0 && cur_cat < MENU_NUM_CATS) && menu_cat_expanded[cur_cat];
|
||||
int ax = menu_x + 10;
|
||||
int ay = iy + row_h / 2;
|
||||
Color arrow_color = Color::from_rgb(0x88, 0x88, 0x88);
|
||||
if (expanded) {
|
||||
// Down arrow (v shape)
|
||||
for (int d = 0; d < 4; d++) {
|
||||
fb.put_pixel(ax + d, ay - 2 + d, arrow_color);
|
||||
fb.put_pixel(ax + d + 1, ay - 2 + d, arrow_color);
|
||||
fb.put_pixel(ax + 7 - d, ay - 2 + d, arrow_color);
|
||||
fb.put_pixel(ax + 6 - d, ay - 2 + d, arrow_color);
|
||||
}
|
||||
} else {
|
||||
// Right arrow (> shape)
|
||||
for (int d = 0; d < 4; d++) {
|
||||
fb.put_pixel(ax + d, ay - 3 + d, arrow_color);
|
||||
fb.put_pixel(ax + d, ay - 3 + d + 1, arrow_color);
|
||||
fb.put_pixel(ax + d, ay + 3 - d, arrow_color);
|
||||
fb.put_pixel(ax + d, ay + 3 - d - 1, arrow_color);
|
||||
}
|
||||
}
|
||||
|
||||
int tx = menu_x + 24;
|
||||
int ty = iy + (row_h - system_font_height()) / 2;
|
||||
if (i > 0) ty += 2;
|
||||
draw_text(fb, tx, ty, row.label, Color::from_rgb(0x88, 0x88, 0x88));
|
||||
draw_text(fb, tx, ty, row.label, Color::from_rgb(0x66, 0x66, 0x66));
|
||||
}
|
||||
} else {
|
||||
Rect item_rect = {menu_x + 4, iy, MENU_W - 8, row_h};
|
||||
@@ -539,7 +599,7 @@ static void desktop_draw_app_menu(DesktopState* ds) {
|
||||
// Icon
|
||||
int icon_x = item_rect.x + 8;
|
||||
int icon_y = item_rect.y + (row_h - 20) / 2;
|
||||
if (row.app_id >= 0 && row.app_id < 15) {
|
||||
if (row.app_id >= 0 && row.app_id < 17) {
|
||||
SvgIcon* icon = icons[row.app_id];
|
||||
if (icon && icon->pixels) {
|
||||
fb.blit_alpha(icon_x, icon_y, icon->width, icon->height, icon->pixels);
|
||||
@@ -1217,13 +1277,19 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
||||
Rect menu_rect = {menu_x, menu_y, MENU_W, menu_h};
|
||||
|
||||
if (menu_rect.contains(mx, my)) {
|
||||
// Walk rows to find which one was clicked
|
||||
// Walk visible rows to find which one was clicked
|
||||
int iy = menu_y + 5;
|
||||
int cur_cat = -1;
|
||||
for (int i = 0; i < MENU_ROW_COUNT; i++) {
|
||||
const MenuRow& row = menu_rows[i];
|
||||
if (row.is_category) cur_cat++;
|
||||
if (!menu_row_visible(i)) continue;
|
||||
int row_h = menu_row_height(row);
|
||||
if (my >= iy && my < iy + row_h) {
|
||||
if (!row.is_category) {
|
||||
if (row.is_category && row.label[0] && cur_cat >= 0 && cur_cat < MENU_NUM_CATS - 1) {
|
||||
// Toggle category expand/collapse
|
||||
menu_cat_expanded[cur_cat] = !menu_cat_expanded[cur_cat];
|
||||
} else if (!row.is_category) {
|
||||
switch (row.app_id) {
|
||||
case 0: open_terminal(ds); break;
|
||||
case 1: open_filemanager(ds); break;
|
||||
@@ -1240,6 +1306,8 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
||||
case 12: open_reboot_dialog(ds); break;
|
||||
case 13: open_weather(ds); break;
|
||||
case 14: open_shutdown_dialog(ds); break;
|
||||
case 15: open_wordprocessor(ds); break;
|
||||
case 16: open_spreadsheet(ds); break;
|
||||
}
|
||||
ds->app_menu_open = false;
|
||||
}
|
||||
@@ -1509,6 +1577,10 @@ void gui::desktop_handle_keyboard(DesktopState* ds, const Montauk::KeyEvent& key
|
||||
open_texteditor(ds);
|
||||
return;
|
||||
}
|
||||
if (key.ascii == 'w' || key.ascii == 'W') {
|
||||
open_wordprocessor(ds);
|
||||
return;
|
||||
}
|
||||
if (key.ascii == 'k' || key.ascii == 'K') {
|
||||
open_klog(ds);
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# Makefile for spreadsheet (standalone Window Server app) 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 helpers.cpp formula.cpp fileio.cpp edit.cpp render.cpp stb_truetype_impl.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
|
||||
# ---- Target ----
|
||||
|
||||
TARGET := $(BINDIR)/os/spreadsheet.elf
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile
|
||||
mkdir -p $(BINDIR)/os
|
||||
$(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,245 @@
|
||||
/*
|
||||
* edit.cpp
|
||||
* Editing, selection, clipboard, formatting, undo/redo, scroll
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "spreadsheet.h"
|
||||
|
||||
// ============================================================================
|
||||
// Undo/redo
|
||||
// ============================================================================
|
||||
|
||||
void undo_push() {
|
||||
for (int i = g_undo_pos; i < g_undo_count; i++) {
|
||||
if (g_undo[i]) { montauk::mfree(g_undo[i]); g_undo[i] = nullptr; }
|
||||
}
|
||||
if (g_undo[UNDO_MAX]) { montauk::mfree(g_undo[UNDO_MAX]); g_undo[UNDO_MAX] = nullptr; }
|
||||
g_undo_count = g_undo_pos;
|
||||
|
||||
if (g_undo_count >= UNDO_MAX) {
|
||||
if (g_undo[0]) montauk::mfree(g_undo[0]);
|
||||
for (int i = 0; i < UNDO_MAX - 1; i++) g_undo[i] = g_undo[i + 1];
|
||||
g_undo[UNDO_MAX - 1] = nullptr;
|
||||
g_undo_count = UNDO_MAX - 1;
|
||||
}
|
||||
|
||||
UndoEntry* e = (UndoEntry*)montauk::malloc(sizeof(UndoEntry));
|
||||
if (!e) return;
|
||||
for (int r = 0; r < MAX_ROWS; r++)
|
||||
for (int c = 0; c < MAX_COLS; c++) {
|
||||
str_cpy(e->cells[r][c].input, g_cells[r][c].input, CELL_TEXT_MAX);
|
||||
e->cells[r][c].align = g_cells[r][c].align;
|
||||
e->cells[r][c].fmt = g_cells[r][c].fmt;
|
||||
e->cells[r][c].bold = g_cells[r][c].bold;
|
||||
}
|
||||
g_undo[g_undo_count] = e;
|
||||
g_undo_count++;
|
||||
g_undo_pos = g_undo_count;
|
||||
}
|
||||
|
||||
static void undo_restore(UndoEntry* e) {
|
||||
for (int r = 0; r < MAX_ROWS; r++)
|
||||
for (int c = 0; c < MAX_COLS; c++) {
|
||||
str_cpy(g_cells[r][c].input, e->cells[r][c].input, CELL_TEXT_MAX);
|
||||
g_cells[r][c].align = e->cells[r][c].align;
|
||||
g_cells[r][c].fmt = e->cells[r][c].fmt;
|
||||
g_cells[r][c].bold = e->cells[r][c].bold;
|
||||
}
|
||||
eval_all_cells();
|
||||
}
|
||||
|
||||
void undo_do() {
|
||||
if (g_undo_pos <= 0) return;
|
||||
if (g_undo_pos == g_undo_count && !g_undo[g_undo_count]) {
|
||||
UndoEntry* e = (UndoEntry*)montauk::malloc(sizeof(UndoEntry));
|
||||
if (e) {
|
||||
for (int r = 0; r < MAX_ROWS; r++)
|
||||
for (int c = 0; c < MAX_COLS; c++) {
|
||||
str_cpy(e->cells[r][c].input, g_cells[r][c].input, CELL_TEXT_MAX);
|
||||
e->cells[r][c].align = g_cells[r][c].align;
|
||||
e->cells[r][c].fmt = g_cells[r][c].fmt;
|
||||
e->cells[r][c].bold = g_cells[r][c].bold;
|
||||
}
|
||||
g_undo[g_undo_count] = e;
|
||||
}
|
||||
}
|
||||
g_undo_pos--;
|
||||
if (g_undo[g_undo_pos]) undo_restore(g_undo[g_undo_pos]);
|
||||
}
|
||||
|
||||
void redo_do() {
|
||||
if (g_undo_pos >= g_undo_count) return;
|
||||
g_undo_pos++;
|
||||
UndoEntry* e = g_undo[g_undo_pos];
|
||||
if (e) undo_restore(e);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Editing
|
||||
// ============================================================================
|
||||
|
||||
void start_editing() {
|
||||
if (g_editing) return;
|
||||
g_editing = true;
|
||||
Cell* c = &g_cells[g_sel_row][g_sel_col];
|
||||
str_cpy(g_edit_buf, c->input, CELL_TEXT_MAX);
|
||||
g_edit_len = str_len(g_edit_buf);
|
||||
g_edit_cursor = g_edit_len;
|
||||
}
|
||||
|
||||
void commit_edit() {
|
||||
if (!g_editing) return;
|
||||
g_editing = false;
|
||||
undo_push();
|
||||
Cell* c = &g_cells[g_sel_row][g_sel_col];
|
||||
str_cpy(c->input, g_edit_buf, CELL_TEXT_MAX);
|
||||
g_modified = true;
|
||||
eval_all_cells();
|
||||
}
|
||||
|
||||
void cancel_edit() {
|
||||
g_editing = false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Selection helpers
|
||||
// ============================================================================
|
||||
|
||||
void sel_range(int* c0, int* r0, int* c1, int* r1) {
|
||||
if (!g_has_selection) {
|
||||
*c0 = *c1 = g_sel_col;
|
||||
*r0 = *r1 = g_sel_row;
|
||||
return;
|
||||
}
|
||||
*c0 = g_sel_col < g_anchor_col ? g_sel_col : g_anchor_col;
|
||||
*c1 = g_sel_col > g_anchor_col ? g_sel_col : g_anchor_col;
|
||||
*r0 = g_sel_row < g_anchor_row ? g_sel_row : g_anchor_row;
|
||||
*r1 = g_sel_row > g_anchor_row ? g_sel_row : g_anchor_row;
|
||||
}
|
||||
|
||||
void clear_selection() {
|
||||
g_has_selection = false;
|
||||
g_anchor_col = g_sel_col;
|
||||
g_anchor_row = g_sel_row;
|
||||
}
|
||||
|
||||
void copy_selection() {
|
||||
int c0, r0, c1, r1;
|
||||
sel_range(&c0, &r0, &c1, &r1);
|
||||
g_clip_count = 0;
|
||||
g_clip_cols = c1 - c0 + 1;
|
||||
g_clip_rows = r1 - r0 + 1;
|
||||
for (int r = r0; r <= r1 && g_clip_count < CLIP_MAX_CELLS; r++) {
|
||||
for (int c = c0; c <= c1 && g_clip_count < CLIP_MAX_CELLS; c++) {
|
||||
ClipCell* cc = &g_clipboard[g_clip_count];
|
||||
str_cpy(cc->text, g_cells[r][c].input, CELL_TEXT_MAX);
|
||||
cc->rel_col = c - c0;
|
||||
cc->rel_row = r - r0;
|
||||
cc->align = g_cells[r][c].align;
|
||||
cc->fmt = g_cells[r][c].fmt;
|
||||
cc->bold = g_cells[r][c].bold;
|
||||
g_clip_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void cut_selection() {
|
||||
copy_selection();
|
||||
undo_push();
|
||||
int c0, r0, c1, r1;
|
||||
sel_range(&c0, &r0, &c1, &r1);
|
||||
for (int r = r0; r <= r1; r++)
|
||||
for (int c = c0; c <= c1; c++)
|
||||
g_cells[r][c].input[0] = '\0';
|
||||
eval_all_cells();
|
||||
g_modified = true;
|
||||
}
|
||||
|
||||
void paste_at_cursor() {
|
||||
if (g_clip_count == 0) return;
|
||||
undo_push();
|
||||
for (int i = 0; i < g_clip_count; i++) {
|
||||
int tc = g_sel_col + g_clipboard[i].rel_col;
|
||||
int tr = g_sel_row + g_clipboard[i].rel_row;
|
||||
if (tc >= 0 && tc < MAX_COLS && tr >= 0 && tr < MAX_ROWS) {
|
||||
str_cpy(g_cells[tr][tc].input, g_clipboard[i].text, CELL_TEXT_MAX);
|
||||
g_cells[tr][tc].align = g_clipboard[i].align;
|
||||
g_cells[tr][tc].fmt = g_clipboard[i].fmt;
|
||||
g_cells[tr][tc].bold = g_clipboard[i].bold;
|
||||
}
|
||||
}
|
||||
eval_all_cells();
|
||||
g_modified = true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Formatting helpers (apply to selection)
|
||||
// ============================================================================
|
||||
|
||||
void apply_bold_toggle() {
|
||||
undo_push();
|
||||
int c0, r0, c1, r1;
|
||||
sel_range(&c0, &r0, &c1, &r1);
|
||||
bool new_bold = !g_cells[r0][c0].bold;
|
||||
for (int r = r0; r <= r1; r++)
|
||||
for (int c = c0; c <= c1; c++)
|
||||
g_cells[r][c].bold = new_bold;
|
||||
g_modified = true;
|
||||
}
|
||||
|
||||
void apply_align(CellAlign a) {
|
||||
undo_push();
|
||||
int c0, r0, c1, r1;
|
||||
sel_range(&c0, &r0, &c1, &r1);
|
||||
for (int r = r0; r <= r1; r++)
|
||||
for (int c = c0; c <= c1; c++)
|
||||
g_cells[r][c].align = a;
|
||||
g_modified = true;
|
||||
}
|
||||
|
||||
void apply_format(NumFormat f) {
|
||||
undo_push();
|
||||
int c0, r0, c1, r1;
|
||||
sel_range(&c0, &r0, &c1, &r1);
|
||||
for (int r = r0; r <= r1; r++)
|
||||
for (int c = c0; c <= c1; c++)
|
||||
g_cells[r][c].fmt = f;
|
||||
eval_all_cells();
|
||||
g_modified = true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Scroll clamping
|
||||
// ============================================================================
|
||||
|
||||
void clamp_scroll() {
|
||||
int pbh = g_pathbar_open ? PATHBAR_H : 0;
|
||||
int max_x = content_width() - (g_win_w - ROW_HEADER_W);
|
||||
int max_y = content_height() - (g_win_h - TOOLBAR_H - pbh - FORMULA_BAR_H - STATUS_BAR_H);
|
||||
if (max_x < 0) max_x = 0;
|
||||
if (max_y < 0) max_y = 0;
|
||||
if (g_scroll_x < 0) g_scroll_x = 0;
|
||||
if (g_scroll_x > max_x) g_scroll_x = max_x;
|
||||
if (g_scroll_y < 0) g_scroll_y = 0;
|
||||
if (g_scroll_y > max_y) g_scroll_y = max_y;
|
||||
}
|
||||
|
||||
void ensure_sel_visible() {
|
||||
int pbh = g_pathbar_open ? PATHBAR_H : 0;
|
||||
int area_w = g_win_w - ROW_HEADER_W;
|
||||
int area_h = g_win_h - TOOLBAR_H - pbh - FORMULA_BAR_H - COL_HEADER_H - STATUS_BAR_H;
|
||||
|
||||
int cx = col_x(g_sel_col) - ROW_HEADER_W;
|
||||
int cy = g_sel_row * ROW_H;
|
||||
|
||||
if (cx < g_scroll_x) g_scroll_x = cx;
|
||||
if (cx + g_col_widths[g_sel_col] > g_scroll_x + area_w)
|
||||
g_scroll_x = cx + g_col_widths[g_sel_col] - area_w;
|
||||
|
||||
if (cy < g_scroll_y) g_scroll_y = cy;
|
||||
if (cy + ROW_H > g_scroll_y + area_h)
|
||||
g_scroll_y = cy + ROW_H - area_h;
|
||||
|
||||
clamp_scroll();
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* fileio.cpp
|
||||
* File I/O - MSS (Montauk SpreadSheet) format
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "spreadsheet.h"
|
||||
|
||||
void save_file() {
|
||||
if (g_filepath[0] == '\0') return;
|
||||
|
||||
int count = 0;
|
||||
for (int r = 0; r < MAX_ROWS; r++)
|
||||
for (int c = 0; c < MAX_COLS; c++)
|
||||
if (g_cells[r][c].input[0]) count++;
|
||||
|
||||
int size = 4 + 2 + 2;
|
||||
for (int r = 0; r < MAX_ROWS; r++)
|
||||
for (int c = 0; c < MAX_COLS; c++)
|
||||
if (g_cells[r][c].input[0])
|
||||
size += 1 + 1 + 2 + 1 + str_len(g_cells[r][c].input);
|
||||
|
||||
uint8_t* buf = (uint8_t*)montauk::malloc(size);
|
||||
int off = 0;
|
||||
|
||||
buf[off++] = 'M'; buf[off++] = 'S'; buf[off++] = 'S'; buf[off++] = '2';
|
||||
buf[off++] = (uint8_t)(count & 0xFF);
|
||||
buf[off++] = (uint8_t)((count >> 8) & 0xFF);
|
||||
buf[off++] = MAX_COLS;
|
||||
buf[off++] = MAX_ROWS;
|
||||
|
||||
for (int r = 0; r < MAX_ROWS; r++) {
|
||||
for (int c = 0; c < MAX_COLS; c++) {
|
||||
if (!g_cells[r][c].input[0]) continue;
|
||||
buf[off++] = (uint8_t)c;
|
||||
buf[off++] = (uint8_t)r;
|
||||
int len = str_len(g_cells[r][c].input);
|
||||
buf[off++] = (uint8_t)(len & 0xFF);
|
||||
buf[off++] = (uint8_t)((len >> 8) & 0xFF);
|
||||
uint8_t flags = ((uint8_t)g_cells[r][c].align & 3)
|
||||
| (((uint8_t)g_cells[r][c].fmt & 3) << 2)
|
||||
| (g_cells[r][c].bold ? 0x10 : 0);
|
||||
buf[off++] = flags;
|
||||
montauk::memcpy(buf + off, g_cells[r][c].input, len);
|
||||
off += len;
|
||||
}
|
||||
}
|
||||
|
||||
int fd = montauk::fcreate(g_filepath);
|
||||
if (fd >= 0) {
|
||||
montauk::fwrite(fd, buf, 0, off);
|
||||
montauk::close(fd);
|
||||
g_modified = false;
|
||||
}
|
||||
|
||||
montauk::mfree(buf);
|
||||
}
|
||||
|
||||
void load_file(const char* path) {
|
||||
int fd = montauk::open(path);
|
||||
if (fd < 0) return;
|
||||
|
||||
uint64_t fsize = montauk::getsize(fd);
|
||||
if (fsize < 8 || fsize > 1024 * 1024) {
|
||||
montauk::close(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t* buf = (uint8_t*)montauk::malloc((int)fsize);
|
||||
montauk::read(fd, buf, 0, fsize);
|
||||
montauk::close(fd);
|
||||
|
||||
if (buf[0] != 'M' || buf[1] != 'S' || buf[2] != 'S' || (buf[3] != '1' && buf[3] != '2')) {
|
||||
montauk::mfree(buf);
|
||||
return;
|
||||
}
|
||||
bool v2 = (buf[3] == '2');
|
||||
|
||||
for (int r = 0; r < MAX_ROWS; r++)
|
||||
for (int c = 0; c < MAX_COLS; c++) {
|
||||
g_cells[r][c].input[0] = '\0';
|
||||
g_cells[r][c].display[0] = '\0';
|
||||
g_cells[r][c].value = 0;
|
||||
g_cells[r][c].type = CT_EMPTY;
|
||||
g_cells[r][c].align = ALIGN_AUTO;
|
||||
g_cells[r][c].fmt = FMT_AUTO;
|
||||
g_cells[r][c].bold = false;
|
||||
}
|
||||
|
||||
int off = 4;
|
||||
int count = buf[off] | (buf[off + 1] << 8);
|
||||
off += 2;
|
||||
off += 2; // skip cols/rows
|
||||
|
||||
for (int i = 0; i < count && off < (int)fsize; i++) {
|
||||
if (off + 4 > (int)fsize) break;
|
||||
int c = buf[off++];
|
||||
int r = buf[off++];
|
||||
int len = buf[off] | (buf[off + 1] << 8);
|
||||
off += 2;
|
||||
|
||||
uint8_t flags = 0;
|
||||
if (v2) {
|
||||
if (off >= (int)fsize) break;
|
||||
flags = buf[off++];
|
||||
}
|
||||
|
||||
if (off + len > (int)fsize) break;
|
||||
if (c < MAX_COLS && r < MAX_ROWS && len < CELL_TEXT_MAX) {
|
||||
montauk::memcpy(g_cells[r][c].input, buf + off, len);
|
||||
g_cells[r][c].input[len] = '\0';
|
||||
if (v2) {
|
||||
g_cells[r][c].align = (CellAlign)(flags & 3);
|
||||
g_cells[r][c].fmt = (NumFormat)((flags >> 2) & 3);
|
||||
g_cells[r][c].bold = (flags & 0x10) != 0;
|
||||
}
|
||||
}
|
||||
off += len;
|
||||
}
|
||||
|
||||
str_cpy(g_filepath, path, 256);
|
||||
g_modified = false;
|
||||
eval_all_cells();
|
||||
montauk::mfree(buf);
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
/*
|
||||
* formula.cpp
|
||||
* Formula parser, cell evaluation, and geometry helpers
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "spreadsheet.h"
|
||||
|
||||
// ============================================================================
|
||||
// Cell reference parsing
|
||||
// ============================================================================
|
||||
|
||||
static bool parse_cell_ref(const char* s, int* col, int* row, int* consumed) {
|
||||
int i = 0;
|
||||
char c = to_upper(s[i]);
|
||||
if (c < 'A' || c > 'Z') return false;
|
||||
*col = c - 'A';
|
||||
i++;
|
||||
|
||||
if (!is_digit(s[i])) return false;
|
||||
int r = 0;
|
||||
while (is_digit(s[i])) {
|
||||
r = r * 10 + (s[i] - '0');
|
||||
i++;
|
||||
}
|
||||
if (r < 1 || r > MAX_ROWS) return false;
|
||||
*row = r - 1;
|
||||
*consumed = i;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Formula evaluation (simple recursive-descent)
|
||||
// ============================================================================
|
||||
|
||||
static double eval_expr(const char* s, int* pos, bool* ok);
|
||||
|
||||
static double cell_value(int col, int row) {
|
||||
if (col < 0 || col >= MAX_COLS || row < 0 || row >= MAX_ROWS) return 0;
|
||||
return g_cells[row][col].value;
|
||||
}
|
||||
|
||||
static double eval_range_func(const char* func, const char* s, int* pos, bool* ok) {
|
||||
if (s[*pos] != '(') { *ok = false; return 0; }
|
||||
(*pos)++;
|
||||
|
||||
int c1, r1, c2, r2, consumed;
|
||||
if (!parse_cell_ref(s + *pos, &c1, &r1, &consumed)) { *ok = false; return 0; }
|
||||
*pos += consumed;
|
||||
|
||||
if (s[*pos] != ':') { *ok = false; return 0; }
|
||||
(*pos)++;
|
||||
|
||||
if (!parse_cell_ref(s + *pos, &c2, &r2, &consumed)) { *ok = false; return 0; }
|
||||
*pos += consumed;
|
||||
|
||||
if (s[*pos] != ')') { *ok = false; return 0; }
|
||||
(*pos)++;
|
||||
|
||||
if (c1 > c2) { int t = c1; c1 = c2; c2 = t; }
|
||||
if (r1 > r2) { int t = r1; r1 = r2; r2 = t; }
|
||||
|
||||
double sum = 0;
|
||||
int count = 0;
|
||||
for (int r = r1; r <= r2; r++) {
|
||||
for (int c = c1; c <= c2; c++) {
|
||||
sum += cell_value(c, r);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_sum = (func[0] == 'S' || func[0] == 's');
|
||||
bool is_avg = (func[0] == 'A' || func[0] == 'a');
|
||||
bool is_min = (func[0] == 'M' || func[0] == 'm') && (func[1] == 'I' || func[1] == 'i');
|
||||
bool is_max = (func[0] == 'M' || func[0] == 'm') && (func[1] == 'A' || func[1] == 'a');
|
||||
bool is_cnt = (func[0] == 'C' || func[0] == 'c');
|
||||
|
||||
if (is_sum) return sum;
|
||||
if (is_avg) return count > 0 ? sum / count : 0;
|
||||
if (is_cnt) return (double)count;
|
||||
if (is_min || is_max) {
|
||||
double result = cell_value(c1, r1);
|
||||
for (int r = r1; r <= r2; r++) {
|
||||
for (int c = c1; c <= c2; c++) {
|
||||
double v = cell_value(c, r);
|
||||
if (is_min && v < result) result = v;
|
||||
if (is_max && v > result) result = v;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
*ok = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void skip_spaces(const char* s, int* pos) {
|
||||
while (s[*pos] == ' ') (*pos)++;
|
||||
}
|
||||
|
||||
static double eval_primary(const char* s, int* pos, bool* ok) {
|
||||
skip_spaces(s, pos);
|
||||
|
||||
if (s[*pos] == '-') {
|
||||
(*pos)++;
|
||||
return -eval_primary(s, pos, ok);
|
||||
}
|
||||
|
||||
if (s[*pos] == '(') {
|
||||
(*pos)++;
|
||||
double v = eval_expr(s, pos, ok);
|
||||
skip_spaces(s, pos);
|
||||
if (s[*pos] == ')') (*pos)++;
|
||||
return v;
|
||||
}
|
||||
|
||||
if (is_alpha(s[*pos])) {
|
||||
char fname[8] = {};
|
||||
int fi = 0;
|
||||
int save_pos = *pos;
|
||||
while (is_alpha(s[*pos]) && fi < 7) {
|
||||
fname[fi++] = to_upper(s[*pos]);
|
||||
(*pos)++;
|
||||
}
|
||||
fname[fi] = '\0';
|
||||
|
||||
if (s[*pos] == '(' && (strcmp(fname, "SUM") == 0 ||
|
||||
strcmp(fname, "AVG") == 0 ||
|
||||
strcmp(fname, "MIN") == 0 ||
|
||||
strcmp(fname, "MAX") == 0 ||
|
||||
strcmp(fname, "COUNT") == 0)) {
|
||||
return eval_range_func(fname, s, pos, ok);
|
||||
}
|
||||
|
||||
*pos = save_pos;
|
||||
int col, row, consumed;
|
||||
if (parse_cell_ref(s + *pos, &col, &row, &consumed)) {
|
||||
*pos += consumed;
|
||||
return cell_value(col, row);
|
||||
}
|
||||
|
||||
*ok = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (is_digit(s[*pos]) || s[*pos] == '.') {
|
||||
double result = 0;
|
||||
while (is_digit(s[*pos])) {
|
||||
result = result * 10 + (s[*pos] - '0');
|
||||
(*pos)++;
|
||||
}
|
||||
if (s[*pos] == '.') {
|
||||
(*pos)++;
|
||||
double frac = 0.1;
|
||||
while (is_digit(s[*pos])) {
|
||||
result += (s[*pos] - '0') * frac;
|
||||
frac *= 0.1;
|
||||
(*pos)++;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
*ok = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static double eval_term(const char* s, int* pos, bool* ok) {
|
||||
double left = eval_primary(s, pos, ok);
|
||||
while (*ok) {
|
||||
skip_spaces(s, pos);
|
||||
char op = s[*pos];
|
||||
if (op != '*' && op != '/') break;
|
||||
(*pos)++;
|
||||
double right = eval_primary(s, pos, ok);
|
||||
if (op == '*') left *= right;
|
||||
else if (right != 0) left /= right;
|
||||
else { *ok = false; return 0; }
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
static double eval_expr(const char* s, int* pos, bool* ok) {
|
||||
double left = eval_term(s, pos, ok);
|
||||
while (*ok) {
|
||||
skip_spaces(s, pos);
|
||||
char op = s[*pos];
|
||||
if (op != '+' && op != '-') break;
|
||||
(*pos)++;
|
||||
double right = eval_term(s, pos, ok);
|
||||
if (op == '+') left += right;
|
||||
else left -= right;
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Cell evaluation
|
||||
// ============================================================================
|
||||
|
||||
void format_value(char* buf, int max, double val, NumFormat fmt) {
|
||||
switch (fmt) {
|
||||
case FMT_CURRENCY: {
|
||||
bool neg = val < 0;
|
||||
double av = neg ? -val : val;
|
||||
long long cents = (long long)(av * 100 + 0.5);
|
||||
long long dollars = cents / 100;
|
||||
int c = (int)(cents % 100);
|
||||
if (neg)
|
||||
snprintf(buf, max, "-$%lld.%02d", dollars, c);
|
||||
else
|
||||
snprintf(buf, max, "$%lld.%02d", dollars, c);
|
||||
break;
|
||||
}
|
||||
case FMT_PERCENT: {
|
||||
double av = val < 0 ? -val : val;
|
||||
long long ip = (long long)av;
|
||||
double frac = av - ip;
|
||||
if (frac < 0.005)
|
||||
snprintf(buf, max, "%s%lld%%", val < 0 ? "-" : "", ip);
|
||||
else
|
||||
snprintf(buf, max, "%s%lld.%02d%%", val < 0 ? "-" : "", ip, (int)(frac * 100 + 0.5));
|
||||
break;
|
||||
}
|
||||
case FMT_DECIMAL: {
|
||||
long long rounded = (long long)((val < 0 ? -val : val) * 100 + 0.5);
|
||||
long long ip = rounded / 100;
|
||||
int dp = (int)(rounded % 100);
|
||||
snprintf(buf, max, "%s%lld.%02d", val < 0 ? "-" : "", ip, dp);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
double_to_str(buf, max, val);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void eval_cell(int col, int row) {
|
||||
Cell* c = &g_cells[row][col];
|
||||
if (c->input[0] == '\0') {
|
||||
c->type = CT_EMPTY;
|
||||
c->display[0] = '\0';
|
||||
c->value = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (c->input[0] == '=') {
|
||||
int pos = 1;
|
||||
bool ok = true;
|
||||
double val = eval_expr(c->input, &pos, &ok);
|
||||
if (ok) {
|
||||
c->type = CT_FORMULA;
|
||||
c->value = val;
|
||||
format_value(c->display, CELL_TEXT_MAX, val, c->fmt);
|
||||
} else {
|
||||
c->type = CT_ERROR;
|
||||
c->value = 0;
|
||||
str_cpy(c->display, "#ERR", CELL_TEXT_MAX);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
bool ok;
|
||||
double val = str_to_double(c->input, &ok);
|
||||
if (ok) {
|
||||
c->type = CT_NUMBER;
|
||||
c->value = val;
|
||||
format_value(c->display, CELL_TEXT_MAX, val, c->fmt);
|
||||
} else {
|
||||
c->type = CT_TEXT;
|
||||
c->value = 0;
|
||||
str_cpy(c->display, c->input, CELL_TEXT_MAX);
|
||||
}
|
||||
}
|
||||
|
||||
void eval_all_cells() {
|
||||
for (int r = 0; r < MAX_ROWS; r++)
|
||||
for (int c = 0; c < MAX_COLS; c++)
|
||||
eval_cell(c, r);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Column/row geometry helpers
|
||||
// ============================================================================
|
||||
|
||||
int col_x(int col) {
|
||||
int x = ROW_HEADER_W;
|
||||
for (int i = 0; i < col; i++) x += g_col_widths[i];
|
||||
return x;
|
||||
}
|
||||
|
||||
int content_width() {
|
||||
int w = ROW_HEADER_W;
|
||||
for (int i = 0; i < MAX_COLS; i++) w += g_col_widths[i];
|
||||
return w;
|
||||
}
|
||||
|
||||
int content_height() {
|
||||
return COL_HEADER_H + MAX_ROWS * ROW_H;
|
||||
}
|
||||
|
||||
void cell_name(char* buf, int col, int row) {
|
||||
buf[0] = 'A' + col;
|
||||
int r = row + 1;
|
||||
if (r >= 100) { buf[1] = '0' + r / 100; buf[2] = '0' + (r / 10) % 10; buf[3] = '0' + r % 10; buf[4] = '\0'; }
|
||||
else if (r >= 10) { buf[1] = '0' + r / 10; buf[2] = '0' + r % 10; buf[3] = '\0'; }
|
||||
else { buf[1] = '0' + r; buf[2] = '\0'; }
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* helpers.cpp
|
||||
* Pixel drawing and string/number helpers
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "spreadsheet.h"
|
||||
|
||||
void px_fill(uint32_t* px, int bw, int bh,
|
||||
int x, int y, int w, int h, Color c) {
|
||||
uint32_t v = c.to_pixel();
|
||||
int x0 = x < 0 ? 0 : x, y0 = y < 0 ? 0 : y;
|
||||
int x1 = x + w > bw ? bw : x + w;
|
||||
int y1 = y + h > bh ? bh : y + h;
|
||||
for (int row = y0; row < y1; row++)
|
||||
for (int col = x0; col < x1; col++)
|
||||
px[row * bw + col] = v;
|
||||
}
|
||||
|
||||
void px_hline(uint32_t* px, int bw, int bh,
|
||||
int x, int y, int w, Color c) {
|
||||
if (y < 0 || y >= bh) return;
|
||||
uint32_t v = c.to_pixel();
|
||||
int x0 = x < 0 ? 0 : x;
|
||||
int x1 = x + w > bw ? bw : x + w;
|
||||
for (int col = x0; col < x1; col++)
|
||||
px[y * bw + col] = v;
|
||||
}
|
||||
|
||||
void px_vline(uint32_t* px, int bw, int bh,
|
||||
int x, int y, int h, Color c) {
|
||||
if (x < 0 || x >= bw) return;
|
||||
uint32_t v = c.to_pixel();
|
||||
int y0 = y < 0 ? 0 : y;
|
||||
int y1 = y + h > bh ? bh : y + h;
|
||||
for (int row = y0; row < y1; row++)
|
||||
px[row * bw + x] = v;
|
||||
}
|
||||
|
||||
void px_rect(uint32_t* px, int bw, int bh,
|
||||
int x, int y, int w, int h, Color c) {
|
||||
px_hline(px, bw, bh, x, y, w, c);
|
||||
px_hline(px, bw, bh, x, y + h - 1, w, c);
|
||||
px_vline(px, bw, bh, x, y, h, c);
|
||||
px_vline(px, bw, bh, x + w - 1, y, h, c);
|
||||
}
|
||||
|
||||
void px_fill_rounded(uint32_t* px, int bw, int bh,
|
||||
int x, int y, int w, int h, int r, Color c) {
|
||||
uint32_t v = c.to_pixel();
|
||||
for (int row = 0; row < h; row++) {
|
||||
int py = y + row;
|
||||
if (py < 0 || py >= bh) continue;
|
||||
int inset = 0;
|
||||
if (row < r) {
|
||||
int dy = r - 1 - row;
|
||||
if (r == 3) {
|
||||
if (dy >= 2) inset = 2;
|
||||
else if (dy >= 1) inset = 1;
|
||||
} else {
|
||||
for (int i = r; i > 0; i--) {
|
||||
int dx = r - i;
|
||||
if (dx * dx + dy * dy < r * r) { inset = i; break; }
|
||||
}
|
||||
}
|
||||
} else if (row >= h - r) {
|
||||
int dy = row - (h - r);
|
||||
if (r == 3) {
|
||||
if (dy >= 2) inset = 2;
|
||||
else if (dy >= 1) inset = 1;
|
||||
} else {
|
||||
for (int i = r; i > 0; i--) {
|
||||
int dx = r - i;
|
||||
if (dx * dx + dy * dy < r * r) { inset = i; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
int x0 = x + inset;
|
||||
int x1 = x + w - inset;
|
||||
if (x0 < 0) x0 = 0;
|
||||
if (x1 > bw) x1 = bw;
|
||||
for (int col = x0; col < x1; col++)
|
||||
px[py * bw + col] = v;
|
||||
}
|
||||
}
|
||||
|
||||
int str_len(const char* s) {
|
||||
int n = 0;
|
||||
while (s[n]) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
void str_cpy(char* dst, const char* src, int max) {
|
||||
int i = 0;
|
||||
while (src[i] && i < max - 1) { dst[i] = src[i]; i++; }
|
||||
dst[i] = '\0';
|
||||
}
|
||||
|
||||
bool is_digit(char c) { return c >= '0' && c <= '9'; }
|
||||
bool is_alpha(char c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); }
|
||||
char to_upper(char c) { return (c >= 'a' && c <= 'z') ? c - 32 : c; }
|
||||
|
||||
double str_to_double(const char* s, bool* ok) {
|
||||
*ok = false;
|
||||
if (!s || !s[0]) return 0;
|
||||
|
||||
int i = 0;
|
||||
bool neg = false;
|
||||
if (s[i] == '-') { neg = true; i++; }
|
||||
else if (s[i] == '+') i++;
|
||||
|
||||
double result = 0;
|
||||
bool has_digit = false;
|
||||
while (is_digit(s[i])) {
|
||||
result = result * 10 + (s[i] - '0');
|
||||
has_digit = true;
|
||||
i++;
|
||||
}
|
||||
|
||||
if (s[i] == '.') {
|
||||
i++;
|
||||
double frac = 0.1;
|
||||
while (is_digit(s[i])) {
|
||||
result += (s[i] - '0') * frac;
|
||||
frac *= 0.1;
|
||||
has_digit = true;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_digit) return 0;
|
||||
while (s[i] == ' ') i++;
|
||||
if (s[i] != '\0') return 0;
|
||||
|
||||
*ok = true;
|
||||
return neg ? -result : result;
|
||||
}
|
||||
|
||||
void double_to_str(char* buf, int max, double v) {
|
||||
bool neg = false;
|
||||
if (v < 0) { neg = true; v = -v; }
|
||||
|
||||
long long integer_part = (long long)v;
|
||||
double frac = v - (double)integer_part;
|
||||
|
||||
if (frac < 0.005) {
|
||||
int i = 0;
|
||||
if (neg && integer_part != 0) buf[i++] = '-';
|
||||
if (integer_part == 0) {
|
||||
buf[i++] = '0';
|
||||
} else {
|
||||
char tmp[32];
|
||||
int t = 0;
|
||||
long long n = integer_part;
|
||||
while (n > 0 && t < 30) { tmp[t++] = '0' + (int)(n % 10); n /= 10; }
|
||||
while (t > 0 && i < max - 1) buf[i++] = tmp[--t];
|
||||
}
|
||||
buf[i] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
long long rounded = (long long)(v * 100 + 0.5);
|
||||
long long int_part = rounded / 100;
|
||||
int dec_part = (int)(rounded % 100);
|
||||
|
||||
int i = 0;
|
||||
if (neg) buf[i++] = '-';
|
||||
if (int_part == 0) {
|
||||
buf[i++] = '0';
|
||||
} else {
|
||||
char tmp[32];
|
||||
int t = 0;
|
||||
long long n = int_part;
|
||||
while (n > 0 && t < 30) { tmp[t++] = '0' + (int)(n % 10); n /= 10; }
|
||||
while (t > 0 && i < max - 1) buf[i++] = tmp[--t];
|
||||
}
|
||||
if (i < max - 3) {
|
||||
buf[i++] = '.';
|
||||
buf[i++] = '0' + dec_part / 10;
|
||||
buf[i++] = '0' + dec_part % 10;
|
||||
}
|
||||
buf[i] = '\0';
|
||||
|
||||
int len = str_len(buf);
|
||||
if (len > 0) {
|
||||
bool has_dot = false;
|
||||
for (int j = 0; j < len; j++) if (buf[j] == '.') has_dot = true;
|
||||
if (has_dot) {
|
||||
while (len > 1 && buf[len - 1] == '0') len--;
|
||||
if (len > 1 && buf[len - 1] == '.') len--;
|
||||
buf[len] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,538 @@
|
||||
/*
|
||||
* main.cpp
|
||||
* Spreadsheet app — global state, hit testing, entry point
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "spreadsheet.h"
|
||||
|
||||
// ============================================================================
|
||||
// Global state definitions
|
||||
// ============================================================================
|
||||
|
||||
int g_win_w = INIT_W;
|
||||
int g_win_h = INIT_H;
|
||||
|
||||
Cell g_cells[MAX_ROWS][MAX_COLS];
|
||||
|
||||
int g_sel_col = 0;
|
||||
int g_sel_row = 0;
|
||||
|
||||
int g_scroll_x = 0;
|
||||
int g_scroll_y = 0;
|
||||
|
||||
int g_col_widths[MAX_COLS];
|
||||
|
||||
bool g_editing = false;
|
||||
char g_edit_buf[CELL_TEXT_MAX];
|
||||
int g_edit_len = 0;
|
||||
int g_edit_cursor = 0;
|
||||
|
||||
bool g_has_selection = false;
|
||||
int g_anchor_col = 0;
|
||||
int g_anchor_row = 0;
|
||||
|
||||
ClipCell g_clipboard[CLIP_MAX_CELLS];
|
||||
int g_clip_count = 0;
|
||||
int g_clip_cols = 0;
|
||||
int g_clip_rows = 0;
|
||||
|
||||
char g_filepath[256] = {};
|
||||
bool g_modified = false;
|
||||
|
||||
bool g_pathbar_open = false;
|
||||
bool g_pathbar_save = false;
|
||||
char g_pathbar_text[256] = {};
|
||||
int g_pathbar_len = 0;
|
||||
int g_pathbar_cursor = 0;
|
||||
|
||||
TrueTypeFont* g_font = nullptr;
|
||||
TrueTypeFont* g_font_bold = nullptr;
|
||||
|
||||
bool g_fmt_dropdown_open = false;
|
||||
|
||||
UndoEntry* g_undo[UNDO_MAX + 1];
|
||||
int g_undo_count = 0;
|
||||
int g_undo_pos = 0;
|
||||
|
||||
// ============================================================================
|
||||
// Hit testing
|
||||
// ============================================================================
|
||||
|
||||
bool hit_cell(int mx, int my, int* out_col, int* out_row) {
|
||||
int pbh = g_pathbar_open ? PATHBAR_H : 0;
|
||||
int grid_y = TOOLBAR_H + pbh + FORMULA_BAR_H + COL_HEADER_H;
|
||||
if (mx < ROW_HEADER_W || my < grid_y) return false;
|
||||
|
||||
int content_x = mx + g_scroll_x;
|
||||
int content_y = my - grid_y + g_scroll_y;
|
||||
|
||||
*out_row = content_y / ROW_H;
|
||||
if (*out_row < 0) *out_row = 0;
|
||||
if (*out_row >= MAX_ROWS) *out_row = MAX_ROWS - 1;
|
||||
|
||||
int x = ROW_HEADER_W;
|
||||
for (int c = 0; c < MAX_COLS; c++) {
|
||||
if (content_x >= x - ROW_HEADER_W && content_x < x - ROW_HEADER_W + g_col_widths[c]) {
|
||||
*out_col = c;
|
||||
return true;
|
||||
}
|
||||
x += g_col_widths[c];
|
||||
}
|
||||
|
||||
*out_col = MAX_COLS - 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool handle_toolbar_click(int mx, int my) {
|
||||
if (my >= TOOLBAR_H || my < TB_BTN_Y || my >= TB_BTN_Y + TB_BTN_SIZE) return false;
|
||||
|
||||
if (g_fmt_dropdown_open && !(mx >= TB_FMT_X0 && mx < TB_FMT_X1)) {
|
||||
g_fmt_dropdown_open = false;
|
||||
}
|
||||
|
||||
if (mx >= TB_OPEN_X0 && mx < TB_OPEN_X1) {
|
||||
if (g_editing) commit_edit();
|
||||
g_pathbar_open = true;
|
||||
g_pathbar_save = false;
|
||||
g_pathbar_text[0] = '\0';
|
||||
g_pathbar_len = 0;
|
||||
g_pathbar_cursor = 0;
|
||||
} else if (mx >= TB_SAVE_X0 && mx < TB_SAVE_X1) {
|
||||
if (g_editing) commit_edit();
|
||||
if (g_filepath[0]) {
|
||||
save_file();
|
||||
} else {
|
||||
g_pathbar_open = true;
|
||||
g_pathbar_save = true;
|
||||
str_cpy(g_pathbar_text, "0:/", 256);
|
||||
g_pathbar_len = str_len(g_pathbar_text);
|
||||
g_pathbar_cursor = g_pathbar_len;
|
||||
}
|
||||
} else if (mx >= TB_CUT_X0 && mx < TB_CUT_X1) {
|
||||
if (!g_editing) cut_selection();
|
||||
} else if (mx >= TB_COPY_X0 && mx < TB_COPY_X1) {
|
||||
if (!g_editing) copy_selection();
|
||||
} else if (mx >= TB_PASTE_X0 && mx < TB_PASTE_X1) {
|
||||
if (!g_editing) paste_at_cursor();
|
||||
} else if (mx >= TB_BOLD_X0 && mx < TB_BOLD_X1) {
|
||||
if (!g_editing) apply_bold_toggle();
|
||||
} else if (mx >= TB_AL_X0 && mx < TB_AL_X1) {
|
||||
if (!g_editing) apply_align(ALIGN_LEFT);
|
||||
} else if (mx >= TB_AC_X0 && mx < TB_AC_X1) {
|
||||
if (!g_editing) apply_align(ALIGN_CENTER);
|
||||
} else if (mx >= TB_AR_X0 && mx < TB_AR_X1) {
|
||||
if (!g_editing) apply_align(ALIGN_RIGHT);
|
||||
} else if (mx >= TB_FMT_X0 && mx < TB_FMT_X1) {
|
||||
g_fmt_dropdown_open = !g_fmt_dropdown_open;
|
||||
} else if (mx >= TB_UNDO_X0 && mx < TB_UNDO_X1) {
|
||||
undo_do();
|
||||
} else if (mx >= TB_REDO_X0 && mx < TB_REDO_X1) {
|
||||
redo_do();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool handle_fmt_dropdown_click(int mx, int my) {
|
||||
if (!g_fmt_dropdown_open) return false;
|
||||
int dx = TB_FMT_X0;
|
||||
int dy = TOOLBAR_H;
|
||||
int dw = 80;
|
||||
int item_h = 26;
|
||||
int item_count = 4;
|
||||
int dh = item_count * item_h + 4;
|
||||
|
||||
if (mx >= dx && mx < dx + dw && my >= dy && my < dy + dh) {
|
||||
int idx = (my - dy - 2) / item_h;
|
||||
if (idx >= 0 && idx < item_count) {
|
||||
apply_format((NumFormat)idx);
|
||||
}
|
||||
g_fmt_dropdown_open = false;
|
||||
return true;
|
||||
}
|
||||
g_fmt_dropdown_open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Entry point
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void _start() {
|
||||
// Initialize column widths
|
||||
for (int i = 0; i < MAX_COLS; i++)
|
||||
g_col_widths[i] = DEF_COL_W;
|
||||
|
||||
// Initialize cells
|
||||
for (int r = 0; r < MAX_ROWS; r++)
|
||||
for (int c = 0; c < MAX_COLS; c++) {
|
||||
g_cells[r][c].input[0] = '\0';
|
||||
g_cells[r][c].display[0] = '\0';
|
||||
g_cells[r][c].value = 0;
|
||||
g_cells[r][c].type = CT_EMPTY;
|
||||
g_cells[r][c].align = ALIGN_AUTO;
|
||||
g_cells[r][c].fmt = FMT_AUTO;
|
||||
g_cells[r][c].bold = false;
|
||||
}
|
||||
|
||||
// Initialize undo pointers
|
||||
for (int i = 0; i <= UNDO_MAX; i++) g_undo[i] = nullptr;
|
||||
g_undo_count = 0;
|
||||
g_undo_pos = 0;
|
||||
|
||||
// Load font
|
||||
auto load_font = [](const char* path) -> TrueTypeFont* {
|
||||
TrueTypeFont* f = (TrueTypeFont*)montauk::malloc(sizeof(TrueTypeFont));
|
||||
if (!f) return nullptr;
|
||||
montauk::memset(f, 0, sizeof(TrueTypeFont));
|
||||
if (!f->init(path)) { montauk::mfree(f); return nullptr; }
|
||||
return f;
|
||||
};
|
||||
g_font = load_font("0:/fonts/Roboto-Medium.ttf");
|
||||
g_font_bold = load_font("0:/fonts/Roboto-Bold.ttf");
|
||||
|
||||
// Check for file argument
|
||||
char args[512] = {};
|
||||
int arglen = montauk::getargs(args, sizeof(args));
|
||||
if (arglen > 0 && args[0]) {
|
||||
str_cpy(g_filepath, args, 256);
|
||||
load_file(g_filepath);
|
||||
}
|
||||
|
||||
// Build window title
|
||||
char title[64] = "Spreadsheet";
|
||||
if (g_filepath[0]) {
|
||||
const char* fname = g_filepath;
|
||||
for (int i = 0; g_filepath[i]; i++)
|
||||
if (g_filepath[i] == '/') fname = g_filepath + i + 1;
|
||||
snprintf(title, 64, "%s - Spreadsheet", fname);
|
||||
}
|
||||
|
||||
// Create window
|
||||
Montauk::WinCreateResult wres;
|
||||
if (montauk::win_create(title, INIT_W, INIT_H, &wres) < 0 || wres.id < 0)
|
||||
montauk::exit(1);
|
||||
|
||||
int win_id = wres.id;
|
||||
uint32_t* pixels = (uint32_t*)(uintptr_t)wres.pixelVa;
|
||||
|
||||
render(pixels);
|
||||
montauk::win_present(win_id);
|
||||
|
||||
while (true) {
|
||||
Montauk::WinEvent ev;
|
||||
int r = montauk::win_poll(win_id, &ev);
|
||||
|
||||
if (r < 0) break;
|
||||
|
||||
if (r == 0) {
|
||||
montauk::sleep_ms(16);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Close
|
||||
if (ev.type == 3) break;
|
||||
|
||||
// Resize
|
||||
if (ev.type == 2) {
|
||||
g_win_w = ev.resize.w;
|
||||
g_win_h = ev.resize.h;
|
||||
pixels = (uint32_t*)(uintptr_t)montauk::win_resize(win_id, g_win_w, g_win_h);
|
||||
clamp_scroll();
|
||||
render(pixels);
|
||||
montauk::win_present(win_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
bool redraw = false;
|
||||
|
||||
// Keyboard
|
||||
if (ev.type == 0 && ev.key.pressed) {
|
||||
auto& key = ev.key;
|
||||
|
||||
// Path bar input (intercepts all keys when open)
|
||||
if (g_pathbar_open) {
|
||||
if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
if (g_pathbar_text[0]) {
|
||||
if (g_pathbar_save) {
|
||||
str_cpy(g_filepath, g_pathbar_text, 256);
|
||||
save_file();
|
||||
} else {
|
||||
load_file(g_pathbar_text);
|
||||
}
|
||||
}
|
||||
g_pathbar_open = false;
|
||||
} else if (key.scancode == 0x01) {
|
||||
g_pathbar_open = false;
|
||||
} else if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
if (g_pathbar_cursor > 0) {
|
||||
for (int i = g_pathbar_cursor - 1; i < g_pathbar_len - 1; i++)
|
||||
g_pathbar_text[i] = g_pathbar_text[i + 1];
|
||||
g_pathbar_len--;
|
||||
g_pathbar_cursor--;
|
||||
g_pathbar_text[g_pathbar_len] = '\0';
|
||||
}
|
||||
} else if (key.scancode == 0x4B) {
|
||||
if (g_pathbar_cursor > 0) g_pathbar_cursor--;
|
||||
} else if (key.scancode == 0x4D) {
|
||||
if (g_pathbar_cursor < g_pathbar_len) g_pathbar_cursor++;
|
||||
} else if (key.ascii >= 32 && key.ascii < 127 && g_pathbar_len < 254) {
|
||||
for (int i = g_pathbar_len; i > g_pathbar_cursor; i--)
|
||||
g_pathbar_text[i] = g_pathbar_text[i - 1];
|
||||
g_pathbar_text[g_pathbar_cursor] = key.ascii;
|
||||
g_pathbar_cursor++;
|
||||
g_pathbar_len++;
|
||||
g_pathbar_text[g_pathbar_len] = '\0';
|
||||
}
|
||||
redraw = true;
|
||||
goto done_keys;
|
||||
}
|
||||
|
||||
// Escape: cancel edit or quit
|
||||
if (key.scancode == 0x01) {
|
||||
if (g_editing) { cancel_edit(); redraw = true; }
|
||||
else break;
|
||||
}
|
||||
// Enter: commit edit and move down
|
||||
else if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
if (g_editing) commit_edit();
|
||||
if (g_sel_row < MAX_ROWS - 1) g_sel_row++;
|
||||
ensure_sel_visible();
|
||||
redraw = true;
|
||||
}
|
||||
// Tab: commit and move right
|
||||
else if (key.ascii == '\t') {
|
||||
if (g_editing) commit_edit();
|
||||
if (key.shift) {
|
||||
if (g_sel_col > 0) g_sel_col--;
|
||||
} else {
|
||||
if (g_sel_col < MAX_COLS - 1) g_sel_col++;
|
||||
}
|
||||
ensure_sel_visible();
|
||||
redraw = true;
|
||||
}
|
||||
// Arrow keys (with Shift = extend selection)
|
||||
else if (key.scancode == 0x48 && !g_editing) { // Up
|
||||
if (key.shift) {
|
||||
if (!g_has_selection) { g_anchor_col = g_sel_col; g_anchor_row = g_sel_row; g_has_selection = true; }
|
||||
if (g_sel_row > 0) g_sel_row--;
|
||||
} else {
|
||||
if (g_sel_row > 0) g_sel_row--;
|
||||
clear_selection();
|
||||
}
|
||||
ensure_sel_visible();
|
||||
redraw = true;
|
||||
}
|
||||
else if (key.scancode == 0x50 && !g_editing) { // Down
|
||||
if (key.shift) {
|
||||
if (!g_has_selection) { g_anchor_col = g_sel_col; g_anchor_row = g_sel_row; g_has_selection = true; }
|
||||
if (g_sel_row < MAX_ROWS - 1) g_sel_row++;
|
||||
} else {
|
||||
if (g_sel_row < MAX_ROWS - 1) g_sel_row++;
|
||||
clear_selection();
|
||||
}
|
||||
ensure_sel_visible();
|
||||
redraw = true;
|
||||
}
|
||||
else if (key.scancode == 0x4B && !g_editing) { // Left
|
||||
if (key.shift) {
|
||||
if (!g_has_selection) { g_anchor_col = g_sel_col; g_anchor_row = g_sel_row; g_has_selection = true; }
|
||||
if (g_sel_col > 0) g_sel_col--;
|
||||
} else {
|
||||
if (g_sel_col > 0) g_sel_col--;
|
||||
clear_selection();
|
||||
}
|
||||
ensure_sel_visible();
|
||||
redraw = true;
|
||||
}
|
||||
else if (key.scancode == 0x4D && !g_editing) { // Right
|
||||
if (key.shift) {
|
||||
if (!g_has_selection) { g_anchor_col = g_sel_col; g_anchor_row = g_sel_row; g_has_selection = true; }
|
||||
if (g_sel_col < MAX_COLS - 1) g_sel_col++;
|
||||
} else {
|
||||
if (g_sel_col < MAX_COLS - 1) g_sel_col++;
|
||||
clear_selection();
|
||||
}
|
||||
ensure_sel_visible();
|
||||
redraw = true;
|
||||
}
|
||||
// Backspace
|
||||
else if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
if (g_editing) {
|
||||
if (g_edit_cursor > 0) {
|
||||
for (int i = g_edit_cursor - 1; i < g_edit_len - 1; i++)
|
||||
g_edit_buf[i] = g_edit_buf[i + 1];
|
||||
g_edit_len--;
|
||||
g_edit_cursor--;
|
||||
g_edit_buf[g_edit_len] = '\0';
|
||||
}
|
||||
redraw = true;
|
||||
} else {
|
||||
undo_push();
|
||||
int c0, r0, c1, r1;
|
||||
sel_range(&c0, &r0, &c1, &r1);
|
||||
for (int r = r0; r <= r1; r++)
|
||||
for (int c = c0; c <= c1; c++)
|
||||
g_cells[r][c].input[0] = '\0';
|
||||
eval_all_cells();
|
||||
g_modified = true;
|
||||
redraw = true;
|
||||
}
|
||||
}
|
||||
// Delete key
|
||||
else if (key.scancode == 0x53) {
|
||||
if (g_editing) {
|
||||
if (g_edit_cursor < g_edit_len) {
|
||||
for (int i = g_edit_cursor; i < g_edit_len - 1; i++)
|
||||
g_edit_buf[i] = g_edit_buf[i + 1];
|
||||
g_edit_len--;
|
||||
g_edit_buf[g_edit_len] = '\0';
|
||||
}
|
||||
redraw = true;
|
||||
} else {
|
||||
undo_push();
|
||||
int c0, r0, c1, r1;
|
||||
sel_range(&c0, &r0, &c1, &r1);
|
||||
for (int r = r0; r <= r1; r++)
|
||||
for (int c = c0; c <= c1; c++)
|
||||
g_cells[r][c].input[0] = '\0';
|
||||
eval_all_cells();
|
||||
g_modified = true;
|
||||
redraw = true;
|
||||
}
|
||||
}
|
||||
// Edit mode arrow keys (cursor movement within formula bar)
|
||||
else if (key.scancode == 0x4B && g_editing) {
|
||||
if (g_edit_cursor > 0) g_edit_cursor--;
|
||||
redraw = true;
|
||||
}
|
||||
else if (key.scancode == 0x4D && g_editing) {
|
||||
if (g_edit_cursor < g_edit_len) g_edit_cursor++;
|
||||
redraw = true;
|
||||
}
|
||||
// Ctrl+B: bold toggle
|
||||
else if (key.ctrl && (key.ascii == 'b' || key.ascii == 'B' || key.ascii == 2) && !g_editing) {
|
||||
apply_bold_toggle();
|
||||
redraw = true;
|
||||
}
|
||||
// Ctrl+Z: undo
|
||||
else if (key.ctrl && (key.ascii == 'z' || key.ascii == 'Z' || key.ascii == 26) && !g_editing) {
|
||||
undo_do();
|
||||
redraw = true;
|
||||
}
|
||||
// Ctrl+Y: redo
|
||||
else if (key.ctrl && (key.ascii == 'y' || key.ascii == 'Y' || key.ascii == 25) && !g_editing) {
|
||||
redo_do();
|
||||
redraw = true;
|
||||
}
|
||||
// Ctrl+C: copy
|
||||
else if (key.ctrl && (key.ascii == 'c' || key.ascii == 'C' || key.ascii == 3) && !g_editing) {
|
||||
copy_selection();
|
||||
redraw = true;
|
||||
}
|
||||
// Ctrl+X: cut
|
||||
else if (key.ctrl && (key.ascii == 'x' || key.ascii == 'X' || key.ascii == 24) && !g_editing) {
|
||||
cut_selection();
|
||||
redraw = true;
|
||||
}
|
||||
// Ctrl+V: paste
|
||||
else if (key.ctrl && (key.ascii == 'v' || key.ascii == 'V' || key.ascii == 22) && !g_editing) {
|
||||
paste_at_cursor();
|
||||
redraw = true;
|
||||
}
|
||||
// Ctrl+S: save (or Save As if no path)
|
||||
else if (key.ctrl && (key.ascii == 's' || key.ascii == 'S' || key.ascii == 19)) {
|
||||
if (g_editing) commit_edit();
|
||||
if (g_filepath[0]) {
|
||||
save_file();
|
||||
} else {
|
||||
g_pathbar_open = true;
|
||||
g_pathbar_save = true;
|
||||
str_cpy(g_pathbar_text, "0:/", 256);
|
||||
g_pathbar_len = str_len(g_pathbar_text);
|
||||
g_pathbar_cursor = g_pathbar_len;
|
||||
}
|
||||
redraw = true;
|
||||
}
|
||||
// Ctrl+O: open
|
||||
else if (key.ctrl && (key.ascii == 'o' || key.ascii == 'O' || key.ascii == 15) && !g_editing) {
|
||||
g_pathbar_open = true;
|
||||
g_pathbar_save = false;
|
||||
g_pathbar_text[0] = '\0';
|
||||
g_pathbar_len = 0;
|
||||
g_pathbar_cursor = 0;
|
||||
redraw = true;
|
||||
}
|
||||
// Printable character: start editing or insert
|
||||
else if (key.ascii >= 32 && key.ascii < 127 && !key.ctrl) {
|
||||
if (!g_editing) {
|
||||
clear_selection();
|
||||
g_editing = true;
|
||||
g_edit_buf[0] = key.ascii;
|
||||
g_edit_buf[1] = '\0';
|
||||
g_edit_len = 1;
|
||||
g_edit_cursor = 1;
|
||||
} else if (g_edit_len < CELL_TEXT_MAX - 1) {
|
||||
for (int i = g_edit_len; i > g_edit_cursor; i--)
|
||||
g_edit_buf[i] = g_edit_buf[i - 1];
|
||||
g_edit_buf[g_edit_cursor] = key.ascii;
|
||||
g_edit_len++;
|
||||
g_edit_cursor++;
|
||||
g_edit_buf[g_edit_len] = '\0';
|
||||
}
|
||||
redraw = true;
|
||||
}
|
||||
// F2: edit current cell content (append mode)
|
||||
else if (key.scancode == 0x3C) {
|
||||
if (!g_editing) start_editing();
|
||||
redraw = true;
|
||||
}
|
||||
}
|
||||
done_keys:
|
||||
|
||||
// Mouse
|
||||
if (ev.type == 1) {
|
||||
int mx = ev.mouse.x;
|
||||
int my = ev.mouse.y;
|
||||
uint8_t btns = ev.mouse.buttons;
|
||||
uint8_t prev = ev.mouse.prev_buttons;
|
||||
bool clicked = (btns & 1) && !(prev & 1);
|
||||
|
||||
if (clicked) {
|
||||
if (handle_fmt_dropdown_click(mx, my)) {
|
||||
redraw = true;
|
||||
}
|
||||
else if (handle_toolbar_click(mx, my)) {
|
||||
redraw = true;
|
||||
}
|
||||
else {
|
||||
int col, row;
|
||||
if (hit_cell(mx, my, &col, &row)) {
|
||||
if (g_editing) commit_edit();
|
||||
g_sel_col = col;
|
||||
g_sel_row = row;
|
||||
clear_selection();
|
||||
g_fmt_dropdown_open = false;
|
||||
redraw = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ev.mouse.scroll != 0) {
|
||||
g_scroll_y -= ev.mouse.scroll * SCROLL_STEP;
|
||||
clamp_scroll();
|
||||
redraw = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (redraw) {
|
||||
render(pixels);
|
||||
montauk::win_present(win_id);
|
||||
}
|
||||
}
|
||||
|
||||
montauk::win_destroy(win_id);
|
||||
montauk::exit(0);
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
/*
|
||||
* render.cpp
|
||||
* Rendering — toolbar, grid, status bar, overlays
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "spreadsheet.h"
|
||||
|
||||
void render(uint32_t* pixels) {
|
||||
px_fill(pixels, g_win_w, g_win_h, 0, 0, g_win_w, g_win_h, BG_COLOR);
|
||||
|
||||
// ---- Toolbar ----
|
||||
px_fill(pixels, g_win_w, g_win_h, 0, 0, g_win_w, TOOLBAR_H, TOOLBAR_BG);
|
||||
px_hline(pixels, g_win_w, g_win_h, 0, TOOLBAR_H - 1, g_win_w, GRID_COLOR);
|
||||
|
||||
Cell* cur_cell = &g_cells[g_sel_row][g_sel_col];
|
||||
|
||||
int bx = 4;
|
||||
auto tb_btn = [&](int w, bool active, const char* label) {
|
||||
Color bg = active ? TB_BTN_ACTIVE : TB_BTN_BG;
|
||||
px_fill_rounded(pixels, g_win_w, g_win_h, bx, TB_BTN_Y, w, TB_BTN_SIZE, TB_BTN_RAD, bg);
|
||||
if (g_font && label[0]) {
|
||||
TrueTypeFont* f = (active && g_font_bold) ? g_font_bold : g_font;
|
||||
int tw = f->measure_text(label, HEADER_FONT);
|
||||
f->draw_to_buffer(pixels, g_win_w, g_win_h,
|
||||
bx + (w - tw) / 2, TB_BTN_Y + (TB_BTN_SIZE - HEADER_FONT) / 2,
|
||||
label, HEADER_TEXT, HEADER_FONT);
|
||||
}
|
||||
bx += w + 4;
|
||||
};
|
||||
auto tb_sep = [&]() {
|
||||
px_vline(pixels, g_win_w, g_win_h, bx, 6, TOOLBAR_H - 12, TB_SEP_COLOR);
|
||||
bx += 8;
|
||||
};
|
||||
|
||||
// Open / Save
|
||||
tb_btn(36, false, "Open");
|
||||
tb_btn(36, false, "Save");
|
||||
tb_sep();
|
||||
|
||||
// Cut/Copy/Paste
|
||||
tb_btn(28, false, "Cut");
|
||||
tb_btn(36, false, "Copy");
|
||||
tb_btn(36, false, "Paste");
|
||||
tb_sep();
|
||||
|
||||
// Bold
|
||||
tb_btn(24, cur_cell->bold, "B");
|
||||
tb_sep();
|
||||
|
||||
// Alignment
|
||||
tb_btn(24, cur_cell->align == ALIGN_LEFT || (cur_cell->align == ALIGN_AUTO && cur_cell->type == CT_TEXT), "L");
|
||||
tb_btn(24, cur_cell->align == ALIGN_CENTER, "C");
|
||||
tb_btn(24, cur_cell->align == ALIGN_RIGHT || (cur_cell->align == ALIGN_AUTO && (cur_cell->type == CT_NUMBER || cur_cell->type == CT_FORMULA)), "R");
|
||||
tb_sep();
|
||||
|
||||
// Format dropdown
|
||||
{
|
||||
const char* fmt_labels[] = { "Auto", ".00", "$", "%" };
|
||||
int fi = (int)cur_cell->fmt;
|
||||
if (fi < 0 || fi > 3) fi = 0;
|
||||
char fmt_label[16];
|
||||
snprintf(fmt_label, 16, "%s v", fmt_labels[fi]);
|
||||
int fmt_w = 64;
|
||||
Color bg = g_fmt_dropdown_open ? TB_BTN_ACTIVE : TB_BTN_BG;
|
||||
px_fill_rounded(pixels, g_win_w, g_win_h, bx, TB_BTN_Y, fmt_w, TB_BTN_SIZE, TB_BTN_RAD, bg);
|
||||
if (g_font) {
|
||||
int tw = g_font->measure_text(fmt_label, HEADER_FONT);
|
||||
g_font->draw_to_buffer(pixels, g_win_w, g_win_h,
|
||||
bx + (fmt_w - tw) / 2, TB_BTN_Y + (TB_BTN_SIZE - HEADER_FONT) / 2,
|
||||
fmt_label, HEADER_TEXT, HEADER_FONT);
|
||||
}
|
||||
bx += fmt_w + 4;
|
||||
tb_sep();
|
||||
|
||||
// Undo/Redo
|
||||
tb_btn(36, false, "Undo");
|
||||
tb_btn(36, false, "Redo");
|
||||
}
|
||||
|
||||
// ---- Path bar (Open/Save As) ----
|
||||
int pathbar_h = g_pathbar_open ? PATHBAR_H : 0;
|
||||
if (g_pathbar_open) {
|
||||
int pby = TOOLBAR_H;
|
||||
px_fill(pixels, g_win_w, g_win_h, 0, pby, g_win_w, PATHBAR_H, HEADER_BG);
|
||||
px_hline(pixels, g_win_w, g_win_h, 0, pby + PATHBAR_H - 1, g_win_w, GRID_COLOR);
|
||||
|
||||
const char* label = g_pathbar_save ? "Save as:" : "Open:";
|
||||
if (g_font) {
|
||||
int lw = g_font->measure_text(label, HEADER_FONT);
|
||||
g_font->draw_to_buffer(pixels, g_win_w, g_win_h,
|
||||
8, pby + (PATHBAR_H - HEADER_FONT) / 2, label, HEADER_TEXT, HEADER_FONT);
|
||||
|
||||
int inp_x = 16 + lw;
|
||||
int inp_w = g_win_w - inp_x - 8;
|
||||
px_fill(pixels, g_win_w, g_win_h, inp_x, pby + 4, inp_w, PATHBAR_H - 8, BG_COLOR);
|
||||
px_rect(pixels, g_win_w, g_win_h, inp_x, pby + 4, inp_w, PATHBAR_H - 8, GRID_COLOR);
|
||||
|
||||
if (g_pathbar_text[0])
|
||||
g_font->draw_to_buffer(pixels, g_win_w, g_win_h,
|
||||
inp_x + 4, pby + (PATHBAR_H - FONT_SIZE) / 2,
|
||||
g_pathbar_text, CELL_TEXT, FONT_SIZE);
|
||||
|
||||
char prefix[256];
|
||||
int plen = g_pathbar_cursor < 255 ? g_pathbar_cursor : 255;
|
||||
for (int i = 0; i < plen; i++) prefix[i] = g_pathbar_text[i];
|
||||
prefix[plen] = '\0';
|
||||
int cx = inp_x + 4 + g_font->measure_text(prefix, FONT_SIZE);
|
||||
px_fill(pixels, g_win_w, g_win_h, cx, pby + 6, 2, PATHBAR_H - 12, SELECT_BORDER);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Formula bar ----
|
||||
int fbar_y = TOOLBAR_H + pathbar_h;
|
||||
px_fill(pixels, g_win_w, g_win_h, 0, fbar_y, g_win_w, FORMULA_BAR_H, HEADER_BG);
|
||||
px_hline(pixels, g_win_w, g_win_h, 0, fbar_y + FORMULA_BAR_H - 1, g_win_w, GRID_COLOR);
|
||||
|
||||
char name_buf[8];
|
||||
cell_name(name_buf, g_sel_col, g_sel_row);
|
||||
int fbar_ty = fbar_y + (FORMULA_BAR_H - HEADER_FONT) / 2;
|
||||
if (g_font)
|
||||
g_font->draw_to_buffer(pixels, g_win_w, g_win_h, 8, fbar_ty, name_buf, HEADER_TEXT, HEADER_FONT);
|
||||
|
||||
int sep_x = ROW_HEADER_W + 12;
|
||||
px_vline(pixels, g_win_w, g_win_h, sep_x, fbar_y + 4, FORMULA_BAR_H - 8, GRID_COLOR);
|
||||
|
||||
{
|
||||
int fx = sep_x + 10;
|
||||
const char* display;
|
||||
if (g_editing) {
|
||||
display = g_edit_buf;
|
||||
} else {
|
||||
display = cur_cell->input;
|
||||
}
|
||||
if (g_font && display[0])
|
||||
g_font->draw_to_buffer(pixels, g_win_w, g_win_h, fx, fbar_ty, display, CELL_TEXT, FONT_SIZE);
|
||||
|
||||
if (g_editing && g_font) {
|
||||
char prefix[CELL_TEXT_MAX];
|
||||
int plen = g_edit_cursor < CELL_TEXT_MAX - 1 ? g_edit_cursor : CELL_TEXT_MAX - 1;
|
||||
for (int i = 0; i < plen; i++) prefix[i] = g_edit_buf[i];
|
||||
prefix[plen] = '\0';
|
||||
int cx = fx + g_font->measure_text(prefix, FONT_SIZE);
|
||||
px_fill(pixels, g_win_w, g_win_h, cx, fbar_y + 6, 2, FORMULA_BAR_H - 12, SELECT_BORDER);
|
||||
}
|
||||
}
|
||||
|
||||
int area_y = TOOLBAR_H + pathbar_h + FORMULA_BAR_H;
|
||||
|
||||
// ---- Column headers ----
|
||||
px_fill(pixels, g_win_w, g_win_h, 0, area_y, g_win_w, COL_HEADER_H, HEADER_BG);
|
||||
px_hline(pixels, g_win_w, g_win_h, 0, area_y + COL_HEADER_H - 1, g_win_w, GRID_COLOR);
|
||||
|
||||
px_fill(pixels, g_win_w, g_win_h, 0, area_y, ROW_HEADER_W, COL_HEADER_H, HEADER_BG);
|
||||
px_vline(pixels, g_win_w, g_win_h, ROW_HEADER_W - 1, area_y, COL_HEADER_H, GRID_COLOR);
|
||||
|
||||
for (int c = 0; c < MAX_COLS; c++) {
|
||||
int x = col_x(c) - g_scroll_x;
|
||||
if (x + g_col_widths[c] <= ROW_HEADER_W) continue;
|
||||
if (x >= g_win_w) break;
|
||||
|
||||
{
|
||||
int sc0, sr0, sc1, sr1;
|
||||
sel_range(&sc0, &sr0, &sc1, &sr1);
|
||||
if (c >= sc0 && c <= sc1)
|
||||
px_fill(pixels, g_win_w, g_win_h, x, area_y, g_col_widths[c], COL_HEADER_H, SELECT_FILL);
|
||||
}
|
||||
|
||||
char label[2] = { (char)('A' + c), '\0' };
|
||||
if (g_font) {
|
||||
int tw = g_font->measure_text(label, HEADER_FONT);
|
||||
int lx = x + (g_col_widths[c] - tw) / 2;
|
||||
if (lx >= ROW_HEADER_W)
|
||||
g_font->draw_to_buffer(pixels, g_win_w, g_win_h, lx, area_y + (COL_HEADER_H - HEADER_FONT) / 2, label, HEADER_TEXT, HEADER_FONT);
|
||||
}
|
||||
|
||||
px_vline(pixels, g_win_w, g_win_h, x + g_col_widths[c] - 1, area_y, COL_HEADER_H, GRID_COLOR);
|
||||
}
|
||||
|
||||
// ---- Row headers + grid ----
|
||||
int grid_y = area_y + COL_HEADER_H;
|
||||
int grid_h = g_win_h - TOOLBAR_H - pathbar_h - FORMULA_BAR_H - COL_HEADER_H - STATUS_BAR_H;
|
||||
|
||||
for (int r = 0; r < MAX_ROWS; r++) {
|
||||
int y = grid_y + r * ROW_H - g_scroll_y;
|
||||
if (y + ROW_H <= grid_y) continue;
|
||||
if (y >= grid_y + grid_h) break;
|
||||
|
||||
// Row header
|
||||
{
|
||||
int sc0, sr0, sc1, sr1;
|
||||
sel_range(&sc0, &sr0, &sc1, &sr1);
|
||||
if (r >= sr0 && r <= sr1)
|
||||
px_fill(pixels, g_win_w, g_win_h, 0, y, ROW_HEADER_W, ROW_H, SELECT_FILL);
|
||||
else
|
||||
px_fill(pixels, g_win_w, g_win_h, 0, y, ROW_HEADER_W, ROW_H, HEADER_BG);
|
||||
}
|
||||
|
||||
char row_label[8];
|
||||
int rv = r + 1;
|
||||
if (rv >= 100) { row_label[0] = '0' + rv / 100; row_label[1] = '0' + (rv / 10) % 10; row_label[2] = '0' + rv % 10; row_label[3] = '\0'; }
|
||||
else if (rv >= 10) { row_label[0] = '0' + rv / 10; row_label[1] = '0' + rv % 10; row_label[2] = '\0'; }
|
||||
else { row_label[0] = '0' + rv; row_label[1] = '\0'; }
|
||||
|
||||
if (g_font) {
|
||||
int tw = g_font->measure_text(row_label, HEADER_FONT);
|
||||
int lx = (ROW_HEADER_W - tw) / 2;
|
||||
g_font->draw_to_buffer(pixels, g_win_w, g_win_h, lx, y + (ROW_H - HEADER_FONT) / 2, row_label, HEADER_TEXT, HEADER_FONT);
|
||||
}
|
||||
|
||||
px_vline(pixels, g_win_w, g_win_h, ROW_HEADER_W - 1, y, ROW_H, GRID_COLOR);
|
||||
|
||||
// Cells in this row
|
||||
for (int c = 0; c < MAX_COLS; c++) {
|
||||
int x = col_x(c) - g_scroll_x;
|
||||
if (x + g_col_widths[c] <= ROW_HEADER_W) continue;
|
||||
if (x >= g_win_w) break;
|
||||
|
||||
Cell* cell = &g_cells[r][c];
|
||||
|
||||
// Selection fill (drawn before text so text is visible)
|
||||
if (g_has_selection) {
|
||||
int sc0, sr0, sc1, sr1;
|
||||
sel_range(&sc0, &sr0, &sc1, &sr1);
|
||||
if (c >= sc0 && c <= sc1 && r >= sr0 && r <= sr1 &&
|
||||
!(c == g_sel_col && r == g_sel_row))
|
||||
px_fill(pixels, g_win_w, g_win_h, x + 1, y + 1, g_col_widths[c] - 2, ROW_H - 2, SELECT_FILL);
|
||||
}
|
||||
|
||||
// Cell text
|
||||
if (cell->display[0] && g_font) {
|
||||
Color col = CELL_TEXT;
|
||||
if (cell->type == CT_NUMBER || cell->type == CT_FORMULA) col = NUM_COLOR;
|
||||
if (cell->type == CT_ERROR) col = ERR_COLOR;
|
||||
|
||||
TrueTypeFont* cf = (cell->bold && g_font_bold) ? g_font_bold : g_font;
|
||||
|
||||
CellAlign eff_align = cell->align;
|
||||
if (eff_align == ALIGN_AUTO) {
|
||||
eff_align = (cell->type == CT_NUMBER || cell->type == CT_FORMULA) ? ALIGN_RIGHT : ALIGN_LEFT;
|
||||
}
|
||||
|
||||
int tw = cf->measure_text(cell->display, FONT_SIZE);
|
||||
int text_x;
|
||||
if (eff_align == ALIGN_RIGHT)
|
||||
text_x = x + g_col_widths[c] - tw - 6;
|
||||
else if (eff_align == ALIGN_CENTER)
|
||||
text_x = x + (g_col_widths[c] - tw) / 2;
|
||||
else
|
||||
text_x = x + 6;
|
||||
|
||||
if (text_x >= ROW_HEADER_W - tw && text_x < g_win_w)
|
||||
cf->draw_to_buffer(pixels, g_win_w, g_win_h, text_x, y + (ROW_H - FONT_SIZE) / 2, cell->display, col, FONT_SIZE);
|
||||
}
|
||||
|
||||
// Cell right border
|
||||
px_vline(pixels, g_win_w, g_win_h, x + g_col_widths[c] - 1, y, ROW_H, GRID_COLOR);
|
||||
}
|
||||
|
||||
// Row bottom border
|
||||
px_hline(pixels, g_win_w, g_win_h, ROW_HEADER_W, y + ROW_H - 1, g_win_w - ROW_HEADER_W, GRID_COLOR);
|
||||
}
|
||||
|
||||
// ---- Selection border ----
|
||||
{
|
||||
int c0, r0, c1, r1;
|
||||
sel_range(&c0, &r0, &c1, &r1);
|
||||
|
||||
int sx = col_x(c0) - g_scroll_x;
|
||||
int sy_sel = grid_y + r0 * ROW_H - g_scroll_y;
|
||||
int sw = 0;
|
||||
for (int c = c0; c <= c1; c++) sw += g_col_widths[c];
|
||||
int sh = (r1 - r0 + 1) * ROW_H;
|
||||
|
||||
px_rect(pixels, g_win_w, g_win_h, sx, sy_sel, sw, sh, SELECT_BORDER);
|
||||
px_rect(pixels, g_win_w, g_win_h, sx + 1, sy_sel + 1, sw - 2, sh - 2, SELECT_BORDER);
|
||||
}
|
||||
|
||||
// ---- Status bar ----
|
||||
int sy = g_win_h - STATUS_BAR_H;
|
||||
px_fill(pixels, g_win_w, g_win_h, 0, sy, g_win_w, STATUS_BAR_H, STATUS_BG);
|
||||
|
||||
if (g_font) {
|
||||
char status[128];
|
||||
if (g_filepath[0]) {
|
||||
const char* fname = g_filepath;
|
||||
for (int i = 0; g_filepath[i]; i++)
|
||||
if (g_filepath[i] == '/') fname = g_filepath + i + 1;
|
||||
snprintf(status, 128, " %s%s", fname, g_modified ? " *" : "");
|
||||
} else {
|
||||
snprintf(status, 128, " Untitled%s", g_modified ? " *" : "");
|
||||
}
|
||||
int sty = sy + (STATUS_BAR_H - HEADER_FONT) / 2;
|
||||
g_font->draw_to_buffer(pixels, g_win_w, g_win_h, 6, sty, status, STATUS_TEXT, HEADER_FONT);
|
||||
|
||||
char right[64];
|
||||
if (g_has_selection) {
|
||||
int c0, r0, c1, r1;
|
||||
sel_range(&c0, &r0, &c1, &r1);
|
||||
char n0[8], n1[8];
|
||||
cell_name(n0, c0, r0);
|
||||
cell_name(n1, c1, r1);
|
||||
int ncells = (c1 - c0 + 1) * (r1 - r0 + 1);
|
||||
snprintf(right, 64, "%s:%s (%d cells) ", n0, n1, ncells);
|
||||
} else {
|
||||
Cell* sel = &g_cells[g_sel_row][g_sel_col];
|
||||
char cname[8];
|
||||
cell_name(cname, g_sel_col, g_sel_row);
|
||||
if (sel->type == CT_FORMULA || sel->type == CT_NUMBER) {
|
||||
char vbuf[32];
|
||||
double_to_str(vbuf, 32, sel->value);
|
||||
snprintf(right, 64, "%s = %s ", cname, vbuf);
|
||||
} else {
|
||||
snprintf(right, 64, "%s ", cname);
|
||||
}
|
||||
}
|
||||
int rw = g_font->measure_text(right, HEADER_FONT);
|
||||
g_font->draw_to_buffer(pixels, g_win_w, g_win_h, g_win_w - rw - 6, sty, right, STATUS_TEXT, HEADER_FONT);
|
||||
}
|
||||
|
||||
// ---- Format dropdown overlay ----
|
||||
if (g_fmt_dropdown_open && g_font) {
|
||||
int dx = TB_FMT_X0;
|
||||
int dy = TOOLBAR_H;
|
||||
int dw = 80;
|
||||
const char* items[] = { "Auto", "Decimal", "Currency $", "Percent %" };
|
||||
int item_count = 4;
|
||||
int item_h = 26;
|
||||
int dh = item_count * item_h + 4;
|
||||
|
||||
px_fill(pixels, g_win_w, g_win_h, dx, dy, dw, dh, BG_COLOR);
|
||||
px_rect(pixels, g_win_w, g_win_h, dx, dy, dw, dh, GRID_COLOR);
|
||||
|
||||
for (int i = 0; i < item_count; i++) {
|
||||
int iy = dy + 2 + i * item_h;
|
||||
if ((int)cur_cell->fmt == i)
|
||||
px_fill(pixels, g_win_w, g_win_h, dx + 2, iy, dw - 4, item_h - 2, SELECT_FILL);
|
||||
g_font->draw_to_buffer(pixels, g_win_w, g_win_h,
|
||||
dx + 8, iy + (item_h - HEADER_FONT) / 2, items[i], CELL_TEXT, HEADER_FONT);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* spreadsheet.h
|
||||
* Shared header for the MontaukOS spreadsheet app
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/heap.h>
|
||||
#include <gui/gui.hpp>
|
||||
#include <gui/truetype.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
}
|
||||
|
||||
using namespace gui;
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
static constexpr int INIT_W = 900;
|
||||
static constexpr int INIT_H = 600;
|
||||
static constexpr int TOOLBAR_H = 36;
|
||||
static constexpr int FORMULA_BAR_H = 36;
|
||||
static constexpr int COL_HEADER_H = 28;
|
||||
static constexpr int ROW_HEADER_W = 48;
|
||||
static constexpr int STATUS_BAR_H = 24;
|
||||
static constexpr int SCROLL_STEP = 50;
|
||||
static constexpr int TB_BTN_SIZE = 24;
|
||||
static constexpr int TB_BTN_Y = 6;
|
||||
static constexpr int TB_BTN_RAD = 3;
|
||||
|
||||
static constexpr int MAX_COLS = 26; // A-Z
|
||||
static constexpr int MAX_ROWS = 100;
|
||||
static constexpr int DEF_COL_W = 100;
|
||||
static constexpr int ROW_H = 26;
|
||||
|
||||
static constexpr int CELL_TEXT_MAX = 128;
|
||||
static constexpr int FONT_SIZE = 18;
|
||||
static constexpr int HEADER_FONT = 16;
|
||||
|
||||
static constexpr Color BG_COLOR = Color::from_rgb(0xFF, 0xFF, 0xFF);
|
||||
static constexpr Color GRID_COLOR = Color::from_rgb(0xD0, 0xD0, 0xD0);
|
||||
static constexpr Color HEADER_BG = Color::from_rgb(0xF0, 0xF0, 0xF0);
|
||||
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 SELECT_BORDER = Color::from_rgb(0x36, 0x7B, 0xF0);
|
||||
static constexpr Color SELECT_FILL = Color::from_rgb(0xD8, 0xE8, 0xFD);
|
||||
static constexpr Color FORMULA_BG = Color::from_rgb(0xFF, 0xFF, 0xFF);
|
||||
static constexpr Color FORMULA_BORDER = Color::from_rgb(0xCC, 0xCC, 0xCC);
|
||||
static constexpr Color STATUS_BG = Color::from_rgb(0x2B, 0x3E, 0x50);
|
||||
static constexpr Color STATUS_TEXT = Color::from_rgb(0xFF, 0xFF, 0xFF);
|
||||
static constexpr Color NUM_COLOR = Color::from_rgb(0x10, 0x50, 0x90);
|
||||
static constexpr Color ERR_COLOR = Color::from_rgb(0xCC, 0x22, 0x22);
|
||||
static constexpr Color TOOLBAR_BG = Color::from_rgb(0xF5, 0xF5, 0xF5);
|
||||
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 constexpr Color TB_SEP_COLOR = Color::from_rgb(0xCC, 0xCC, 0xCC);
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
enum CellAlign : uint8_t { ALIGN_AUTO = 0, ALIGN_LEFT = 1, ALIGN_CENTER = 2, ALIGN_RIGHT = 3 };
|
||||
enum NumFormat : uint8_t { FMT_AUTO = 0, FMT_DECIMAL = 1, FMT_CURRENCY = 2, FMT_PERCENT = 3 };
|
||||
|
||||
enum CellType : uint8_t {
|
||||
CT_EMPTY = 0,
|
||||
CT_TEXT = 1,
|
||||
CT_NUMBER = 2,
|
||||
CT_FORMULA = 3,
|
||||
CT_ERROR = 4,
|
||||
};
|
||||
|
||||
struct Cell {
|
||||
char input[CELL_TEXT_MAX]; // raw user input
|
||||
char display[CELL_TEXT_MAX]; // computed display string
|
||||
double value; // numeric value (for formulas/numbers)
|
||||
CellType type;
|
||||
CellAlign align;
|
||||
NumFormat fmt;
|
||||
bool bold;
|
||||
};
|
||||
|
||||
static constexpr int CLIP_MAX_CELLS = 256;
|
||||
struct ClipCell {
|
||||
char text[CELL_TEXT_MAX];
|
||||
int rel_col;
|
||||
int rel_row;
|
||||
CellAlign align;
|
||||
NumFormat fmt;
|
||||
bool bold;
|
||||
};
|
||||
|
||||
static constexpr int UNDO_MAX = 6;
|
||||
struct UndoCellData {
|
||||
char input[CELL_TEXT_MAX];
|
||||
CellAlign align;
|
||||
NumFormat fmt;
|
||||
bool bold;
|
||||
};
|
||||
struct UndoEntry {
|
||||
UndoCellData cells[MAX_ROWS][MAX_COLS];
|
||||
};
|
||||
|
||||
static constexpr int PATHBAR_H = 32;
|
||||
|
||||
// ============================================================================
|
||||
// Toolbar button positions
|
||||
// ============================================================================
|
||||
|
||||
static constexpr int TB_OPEN_X0 = 4, TB_OPEN_X1 = 40;
|
||||
static constexpr int TB_SAVE_X0 = 44, TB_SAVE_X1 = 80;
|
||||
static constexpr int TB_CUT_X0 = 92, TB_CUT_X1 = 120;
|
||||
static constexpr int TB_COPY_X0 = 124, TB_COPY_X1 = 160;
|
||||
static constexpr int TB_PASTE_X0 = 164, TB_PASTE_X1 = 200;
|
||||
static constexpr int TB_BOLD_X0 = 212, TB_BOLD_X1 = 236;
|
||||
static constexpr int TB_AL_X0 = 248, TB_AL_X1 = 272;
|
||||
static constexpr int TB_AC_X0 = 276, TB_AC_X1 = 300;
|
||||
static constexpr int TB_AR_X0 = 304, TB_AR_X1 = 328;
|
||||
static constexpr int TB_FMT_X0 = 340, TB_FMT_X1 = 404;
|
||||
static constexpr int TB_UNDO_X0 = 416, TB_UNDO_X1 = 452;
|
||||
static constexpr int TB_REDO_X0 = 456, TB_REDO_X1 = 492;
|
||||
|
||||
// ============================================================================
|
||||
// Global state (extern — defined in main.cpp)
|
||||
// ============================================================================
|
||||
|
||||
extern int g_win_w, g_win_h;
|
||||
extern Cell g_cells[MAX_ROWS][MAX_COLS];
|
||||
extern int g_sel_col, g_sel_row;
|
||||
extern int g_scroll_x, g_scroll_y;
|
||||
extern int g_col_widths[MAX_COLS];
|
||||
|
||||
extern bool g_editing;
|
||||
extern char g_edit_buf[CELL_TEXT_MAX];
|
||||
extern int g_edit_len;
|
||||
extern int g_edit_cursor;
|
||||
|
||||
extern bool g_has_selection;
|
||||
extern int g_anchor_col, g_anchor_row;
|
||||
|
||||
extern ClipCell g_clipboard[CLIP_MAX_CELLS];
|
||||
extern int g_clip_count;
|
||||
extern int g_clip_cols, g_clip_rows;
|
||||
|
||||
extern char g_filepath[256];
|
||||
extern bool g_modified;
|
||||
|
||||
extern bool g_pathbar_open;
|
||||
extern bool g_pathbar_save;
|
||||
extern char g_pathbar_text[256];
|
||||
extern int g_pathbar_len;
|
||||
extern int g_pathbar_cursor;
|
||||
|
||||
extern TrueTypeFont* g_font;
|
||||
extern TrueTypeFont* g_font_bold;
|
||||
|
||||
extern bool g_fmt_dropdown_open;
|
||||
|
||||
extern UndoEntry* g_undo[UNDO_MAX + 1];
|
||||
extern int g_undo_count;
|
||||
extern int g_undo_pos;
|
||||
|
||||
// ============================================================================
|
||||
// Function declarations — helpers.cpp
|
||||
// ============================================================================
|
||||
|
||||
void px_fill(uint32_t* px, int bw, int bh, int x, int y, int w, int h, Color c);
|
||||
void px_hline(uint32_t* px, int bw, int bh, int x, int y, int w, Color c);
|
||||
void px_vline(uint32_t* px, int bw, int bh, int x, int y, int h, Color c);
|
||||
void px_rect(uint32_t* px, int bw, int bh, int x, int y, int w, int h, Color c);
|
||||
void px_fill_rounded(uint32_t* px, int bw, int bh, int x, int y, int w, int h, int r, Color c);
|
||||
|
||||
int str_len(const char* s);
|
||||
void str_cpy(char* dst, const char* src, int max);
|
||||
bool is_digit(char c);
|
||||
bool is_alpha(char c);
|
||||
char to_upper(char c);
|
||||
double str_to_double(const char* s, bool* ok);
|
||||
void double_to_str(char* buf, int max, double v);
|
||||
|
||||
// ============================================================================
|
||||
// Function declarations — formula.cpp
|
||||
// ============================================================================
|
||||
|
||||
void eval_cell(int col, int row);
|
||||
void eval_all_cells();
|
||||
int col_x(int col);
|
||||
int content_width();
|
||||
int content_height();
|
||||
void cell_name(char* buf, int col, int row);
|
||||
void format_value(char* buf, int max, double val, NumFormat fmt);
|
||||
|
||||
// ============================================================================
|
||||
// Function declarations — fileio.cpp
|
||||
// ============================================================================
|
||||
|
||||
void save_file();
|
||||
void load_file(const char* path);
|
||||
|
||||
// ============================================================================
|
||||
// Function declarations — edit.cpp
|
||||
// ============================================================================
|
||||
|
||||
void undo_push();
|
||||
void undo_do();
|
||||
void redo_do();
|
||||
void start_editing();
|
||||
void commit_edit();
|
||||
void cancel_edit();
|
||||
void sel_range(int* c0, int* r0, int* c1, int* r1);
|
||||
void clear_selection();
|
||||
void copy_selection();
|
||||
void cut_selection();
|
||||
void paste_at_cursor();
|
||||
void apply_bold_toggle();
|
||||
void apply_align(CellAlign a);
|
||||
void apply_format(NumFormat f);
|
||||
void clamp_scroll();
|
||||
void ensure_sel_visible();
|
||||
|
||||
// ============================================================================
|
||||
// Function declarations — render.cpp
|
||||
// ============================================================================
|
||||
|
||||
void render(uint32_t* pixels);
|
||||
|
||||
// ============================================================================
|
||||
// Function declarations — main.cpp
|
||||
// ============================================================================
|
||||
|
||||
bool hit_cell(int mx, int my, int* out_col, int* out_row);
|
||||
bool handle_toolbar_click(int mx, int my);
|
||||
bool handle_fmt_dropdown_click(int mx, int my);
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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>
|
||||
|
||||
// Override all stb_truetype dependencies before including the implementation
|
||||
|
||||
#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>
|
||||
Reference in New Issue
Block a user