feat: add word processor, spreadsheet, kernel and userspace memory improvements

This commit is contained in:
2026-03-05 19:18:11 +01:00
parent fcb6f8e247
commit ead7d296f7
28 changed files with 4146 additions and 56 deletions
+20 -7
View File
@@ -97,6 +97,19 @@ namespace WinServer {
} }
} }
// Unmap pixel pages from the owner's address space so that
// FreeUserHalf() won't double-free them when the process exits.
{
auto* ownerProc = Sched::GetProcessByPid(slot.ownerPid);
if (ownerProc) {
for (int p = 0; p < slot.pixelNumPages; p++) {
Memory::VMM::Paging::UnmapUserIn(
ownerProc->pml4Phys,
slot.ownerVa + (uint64_t)p * 0x1000);
}
}
}
// Free physical pixel pages // Free physical pixel pages
for (int i = 0; i < slot.pixelNumPages; i++) { for (int i = 0; i < slot.pixelNumPages; i++) {
if (slot.pixelPhysPages[i] != 0) { if (slot.pixelPhysPages[i] != 0) {
@@ -198,9 +211,11 @@ namespace WinServer {
int numPages = (int)((bufSize + 0xFFF) / 0x1000); int numPages = (int)((bufSize + 0xFFF) / 0x1000);
if (numPages > MaxPixelPages) return -1; if (numPages > MaxPixelPages) return -1;
// Free old pixel pages before allocating new ones // Unmap old pixel pages from owner's address space, then free them
int oldNumPages = slot.pixelNumPages; int oldNumPages = slot.pixelNumPages;
for (int i = 0; i < oldNumPages; i++) { for (int i = 0; i < oldNumPages; i++) {
Memory::VMM::Paging::UnmapUserIn(
ownerPml4, slot.ownerVa + (uint64_t)i * 0x1000);
if (slot.pixelPhysPages[i] != 0) { if (slot.pixelPhysPages[i] != 0) {
Memory::g_pfa->Free((void*)Memory::HHDM(slot.pixelPhysPages[i])); Memory::g_pfa->Free((void*)Memory::HHDM(slot.pixelPhysPages[i]));
slot.pixelPhysPages[i] = 0; slot.pixelPhysPages[i] = 0;
@@ -271,12 +286,10 @@ namespace WinServer {
} }
} }
// Free physical pixel pages // Do NOT free physical pixel pages here — they are still mapped
for (int p = 0; p < g_slots[i].pixelNumPages; p++) { // in the owner's page tables and FreeUserHalf() (called right
if (g_slots[i].pixelPhysPages[p] != 0) { // after CleanupProcess) will free them. Freeing here would
Memory::g_pfa->Free((void*)Memory::HHDM(g_slots[i].pixelPhysPages[p])); // cause a double-free, creating a cycle in the PFA free list.
}
}
g_slots[i].used = false; g_slots[i].used = false;
} }
+74 -10
View File
@@ -108,20 +108,84 @@ namespace Memory {
return nullptr; return nullptr;
} }
void PageFrameAllocator::Free(void* ptr) { // Core free implementation: sorted-insert with coalescing.
Lock.Acquire(); // The free list is kept sorted by address so adjacent blocks can be merged,
auto prev_next = head.next; // preventing fragmentation from accumulating over time.
head.next = (Page*)ptr; void PageFrameAllocator::FreeRange(void* ptr, size_t size) {
if (ptr == nullptr || size == 0) return;
Lock.Acquire();
uint64_t addr = (uint64_t)ptr;
// Walk to find the sorted insertion point: prev < addr < current
Page* prev = &head;
Page* current = head.next;
while (current != nullptr && (uint64_t)current < addr) {
// Double-free check: addr falls within an existing free block
if (addr < (uint64_t)current + current->size) {
Kt::KernelLogStream(Kt::WARNING, "PFA")
<< "Double-free detected at " << addr << ", ignoring";
Lock.Release();
return;
}
prev = current;
current = current->next;
}
// Double-free check: exact match with next block
if (current != nullptr && (uint64_t)current == addr) {
Kt::KernelLogStream(Kt::WARNING, "PFA")
<< "Double-free detected at " << addr << ", ignoring";
Lock.Release();
return;
}
// Try to coalesce with previous block (if prev ends where new block starts)
bool merged_prev = false;
if (prev != &head) {
uint64_t prev_end = (uint64_t)prev + prev->size;
if (prev_end == addr) {
prev->size += size;
merged_prev = true;
}
}
// Try to coalesce with next block (if new block ends where next starts)
if (current != nullptr && addr + size == (uint64_t)current) {
if (merged_prev) {
// Three-way merge: prev absorbs new block and current
prev->size += current->size;
prev->next = current->next;
} else {
// Forward merge: new block absorbs current
Page* new_page = (Page*)addr;
new_page->size = size + current->size;
new_page->next = current->next;
prev->next = new_page;
}
} else if (!merged_prev) {
// No merging possible: insert new block between prev and current
Page* new_page = (Page*)addr;
new_page->size = size;
new_page->next = current;
prev->next = new_page;
}
head.next->next = prev_next;
head.next->size = 0x1000;
Lock.Release(); Lock.Release();
} }
void PageFrameAllocator::Free(void* ptr) {
FreeRange(ptr, 0x1000);
}
void PageFrameAllocator::Free(void* ptr, int n) { void PageFrameAllocator::Free(void* ptr, int n) {
for (int i = 0; i < n; i++) { if (ptr == nullptr || n <= 0) return;
Free((void*)((uint64_t)ptr + 0x1000 * i)); // Free the entire contiguous range as a single block.
} // This is more efficient than freeing one page at a time and
// guarantees the range stays coalesced.
FreeRange(ptr, (size_t)n * 0x1000);
} }
void PageFrameAllocator::GetStats(Montauk::MemStats* out) { void PageFrameAllocator::GetStats(Montauk::MemStats* out) {
@@ -140,4 +204,4 @@ namespace Memory {
out->usedBytes = g_section.size > freeBytes ? g_section.size - freeBytes : 0; out->usedBytes = g_section.size > freeBytes ? g_section.size - freeBytes : 0;
out->pageSize = 0x1000; out->pageSize = 0x1000;
} }
}; };
+2
View File
@@ -20,6 +20,8 @@ namespace Memory {
Page head{}; Page head{};
kcp::Spinlock Lock{}; kcp::Spinlock Lock{};
LargestSection g_section; LargestSection g_section;
void FreeRange(void* ptr, std::size_t size);
public: public:
PageFrameAllocator(LargestSection section); PageFrameAllocator(LargestSection section);
+8 -3
View File
@@ -59,7 +59,7 @@ BINDIR := bin
PROGRAMS := $(notdir $(wildcard src/*)) PROGRAMS := $(notdir $(wildcard src/*))
# Programs with custom Makefiles (built separately). # 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)) SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
# Build targets: system programs go to bin/os/, games are handled separately. # 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. # Home directory placeholder.
HOMEKEEP := $(BINDIR)/home/.keep 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). # 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) 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: fontpreview:
$(MAKE) -C src/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). # Build desktop via its own Makefile (depends on libc and libjpeg for wallpaper).
desktop: libc libjpeg desktop: libc libjpeg
$(MAKE) -C src/desktop $(MAKE) -C src/desktop
@@ -195,3 +199,4 @@ clean:
$(MAKE) -C src/weather clean $(MAKE) -C src/weather clean
$(MAKE) -C src/imageviewer clean $(MAKE) -C src/imageviewer clean
$(MAKE) -C src/fontpreview clean $(MAKE) -C src/fontpreview clean
$(MAKE) -C src/spreadsheet clean
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -87,6 +87,7 @@ struct DesktopState {
SvgIcon icon_procmgr; SvgIcon icon_procmgr;
SvgIcon icon_mandelbrot; SvgIcon icon_mandelbrot;
SvgIcon icon_devexplorer; SvgIcon icon_devexplorer;
SvgIcon icon_spreadsheet;
bool ctx_menu_open; bool ctx_menu_open;
int ctx_menu_x, ctx_menu_y; int ctx_menu_x, ctx_menu_y;
+16 -13
View File
@@ -321,7 +321,7 @@ struct SvgEdgeList {
int capacity; int capacity;
void init(int cap) { void init(int cap) {
edges = (SvgEdge*)montauk::alloc(cap * sizeof(SvgEdge)); edges = (SvgEdge*)montauk::malloc(cap * sizeof(SvgEdge));
count = 0; count = 0;
capacity = cap; 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 // Temporary array for x-intersections on each scanline
// Allocate enough for all edges (each edge can intersect at most once per scanline) // Allocate enough for all edges (each edge can intersect at most once per scanline)
int maxIsect = el.count + 16; 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) { for (int y = 0; y < h; ++y) {
// Scanline center in fixed-point // 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; if (el.count == 0) return;
int maxIsect = el.count + 16; 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 fr = (fill >> 16) & 0xFF;
uint32_t fg = (fill >> 8) & 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; SvgIcon icon;
icon.width = target_w; icon.width = target_w;
icon.height = target_h; 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 // Clear to transparent
svg_memset(icon.pixels, 0, target_w * target_h * sizeof(uint32_t)); 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; SvgEdgeList el;
el.init(SVG_MAX_EDGES); 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 // Scan for <path, <circle, <rect elements — rasterize each individually
// Skip elements inside <defs> blocks (they define reusable items, not rendered directly) // Skip elements inside <defs> blocks (they define reusable items, not rendered directly)
const char* p = svg_data; 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); int alpha = svg_get_element_opacity(elem_start, elem_len);
// Extract and rasterize path // 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); int d_len = svg_get_attr(elem_start, elem_len, " d", d_buf, SVG_MAX_PATH_LEN);
if (d_len > 0) { if (d_len > 0) {
el.clear(); el.clear();
@@ -1359,7 +1361,8 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
++p; ++p;
} }
montauk::free(el.edges); montauk::mfree(d_buf);
montauk::mfree(el.edges);
return icon; 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}; 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::read(fd, (uint8_t*)buf, 0, size);
montauk::close(fd); montauk::close(fd);
buf[size] = '\0'; 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; int hi_h = target_h * SS;
SvgIcon hi = svg_render(buf, (int)size, hi_w, hi_h, fill_color); 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}; if (!hi.pixels) return {nullptr, 0, 0};
// Allocate final icon at target resolution // 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; for (int i = 0; i < target_w * target_h; i++) out[i] = 0;
// Downsample: average each SSxSS block using premultiplied alpha // 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}; 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 // Free icon pixel data
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
inline void svg_free(SvgIcon& icon) { inline void svg_free(SvgIcon& icon) {
if (icon.pixels) montauk::free(icon.pixels); if (icon.pixels) montauk::mfree(icon.pixels);
icon.pixels = nullptr; icon.pixels = nullptr;
icon.width = 0; icon.width = 0;
icon.height = 0; icon.height = 0;
+15 -2
View File
@@ -130,6 +130,13 @@ static inline void terminal_init_cells(TerminalState* t, int cols, int rows, int
int screen_cells = rows * cols; int screen_cells = rows * cols;
t->cells = (TermCell*)montauk::alloc(total_cells * sizeof(TermCell)); t->cells = (TermCell*)montauk::alloc(total_cells * sizeof(TermCell));
t->alt_cells = (TermCell*)montauk::alloc(screen_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++) { for (int i = 0; i < total_cells; i++) {
t->cells[i] = {' ', colors::TERM_FG, colors::TERM_BG}; 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->cursor_visible = true;
t->child_pid = montauk::spawn_redir("0:/os/shell.elf"); 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) { 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) { 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; t->dirty = false;
int cell_w = mono_cell_width(); 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; int new_total = new_capacity * new_cols;
TermCell* new_cells = (TermCell*)montauk::alloc(new_total * sizeof(TermCell)); TermCell* new_cells = (TermCell*)montauk::alloc(new_total * sizeof(TermCell));
TermCell* new_alt = (TermCell*)montauk::alloc(new_rows * new_cols * 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 // Clear new buffers
for (int i = 0; i < new_total; i++) for (int i = 0; i < new_total; i++)
+4 -3
View File
@@ -24,9 +24,10 @@ namespace heap_detail {
FreeNode* next; FreeNode* next;
}; };
// Per-process heap state (single TU per program, so static is fine) // Per-process heap state — must be `inline` (not `static`) so that all
static FreeNode g_head{0, nullptr}; // translation units in a multi-TU program share a single heap.
static bool g_initialized = false; inline FreeNode g_head{0, nullptr};
inline bool g_initialized = false;
static inline Header* get_header(void* block) { static inline Header* get_header(void* block) {
return (Header*)((uint8_t*)block - sizeof(Header)); return (Header*)((uint8_t*)block - sizeof(Header));
+1 -1
View File
@@ -63,7 +63,7 @@ LDFLAGS := \
# ---- C++ source files ---- # ---- 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)) OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
# ---- Target ---- # ---- Target ----
+12
View File
@@ -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");
}
+2
View File
@@ -110,6 +110,8 @@ static void term_add_tab(TermTabState* tts, int cols, int rows) {
static void terminal_on_draw(Window* win, Framebuffer& fb) { static void terminal_on_draw(Window* win, Framebuffer& fb) {
TermTabState* tts = (TermTabState*)win->app_data; TermTabState* tts = (TermTabState*)win->app_data;
if (!tts || tts->tab_count == 0) return; 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(); Rect cr = win->content_rect();
int term_h = cr.h - TERM_TAB_BAR_H; int term_h = cr.h - TERM_TAB_BAR_H;
File diff suppressed because it is too large Load Diff
+2
View File
@@ -174,5 +174,7 @@ void open_devexplorer(DesktopState* ds);
void open_settings(DesktopState* ds); void open_settings(DesktopState* ds);
void open_doom(DesktopState* ds); void open_doom(DesktopState* ds);
void open_reboot_dialog(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 open_shutdown_dialog(DesktopState* ds);
void desktop_poll_external_windows(DesktopState* ds); void desktop_poll_external_windows(DesktopState* ds);
+89 -17
View File
@@ -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_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_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_devexplorer = svg_load("0:/icons/hardware.svg", 20, 20, defColor);
ds->icon_spreadsheet = svg_load("0:/icons/spreadsheet.svg", 20, 20, defColor);
// Settings defaults // Settings defaults
ds->settings.bg_gradient = true; 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_W = 220;
static constexpr int MENU_ITEM_H = 36; 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; static constexpr int MENU_DIV_H = 10;
struct MenuRow { struct MenuRow {
@@ -437,30 +438,53 @@ struct MenuRow {
int app_id; // -1 for category headers / dividers 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] = { static const MenuRow menu_rows[MENU_ROW_COUNT] = {
{ true, "Applications", -1 }, { true, "Applications", -1 }, // cat 0
{ false, "Terminal", 0 }, { false, "Terminal", 0 },
{ false, "Files", 1 }, { false, "Files", 1 },
{ false, "Text Editor", 4 }, { false, "Text Editor", 4 },
{ false, "Word Processor", 15 },
{ false, "Spreadsheet", 16 },
{ false, "Calculator", 3 }, { false, "Calculator", 3 },
{ true, "Internet", -1 }, { true, "Internet", -1 }, // cat 1
{ false, "Wikipedia", 9 }, { false, "Wikipedia", 9 },
{ false, "Weather", 13 }, { false, "Weather", 13 },
{ true, "System", -1 }, { true, "System", -1 }, // cat 2
{ false, "System Info", 2 }, { false, "System Info", 2 },
{ false, "Kernel Log", 5 }, { false, "Kernel Log", 5 },
{ false, "Processes", 6 }, { false, "Processes", 6 },
{ false, "Devices", 8 }, { false, "Devices", 8 },
{ true, "Games", -1 }, { true, "Games", -1 }, // cat 3
{ false, "Mandelbrot", 7 }, { false, "Mandelbrot", 7 },
{ false, "DOOM", 10 }, { false, "DOOM", 10 },
{ true, "", -1 }, // divider { true, "", -1 }, // divider (always visible)
{ false, "Settings", 11 }, { false, "Settings", 11 },
{ false, "Reboot", 12 }, { false, "Reboot", 12 },
{ false, "Shutdown", 14 }, { 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) { static int menu_row_height(const MenuRow& row) {
if (!row.is_category) return MENU_ITEM_H; if (!row.is_category) return MENU_ITEM_H;
return row.label[0] ? MENU_CAT_H : MENU_DIV_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() { static int menu_total_height() {
int h = 10; // top + bottom padding int h = 10; // top + bottom padding
for (int i = 0; i < MENU_ROW_COUNT; i++) 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; 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); draw_rect(fb, menu_x, menu_y, MENU_W, menu_h, colors::BORDER);
// Icon lookup by app_id // Icon lookup by app_id
SvgIcon* icons[15] = { SvgIcon* icons[17] = {
&ds->icon_terminal, // 0 &ds->icon_terminal, // 0
&ds->icon_filemanager, // 1 &ds->icon_filemanager, // 1
&ds->icon_sysinfo, // 2 &ds->icon_sysinfo, // 2
@@ -504,29 +529,64 @@ static void desktop_draw_app_menu(DesktopState* ds) {
&ds->icon_reboot, // 12 &ds->icon_reboot, // 12
&ds->icon_weather, // 13 &ds->icon_weather, // 13
&ds->icon_shutdown, // 14 &ds->icon_shutdown, // 14
&ds->icon_texteditor, // 15
&ds->icon_spreadsheet, // 16
}; };
int mx = ds->mouse.x; int mx = ds->mouse.x;
int my = ds->mouse.y; int my = ds->mouse.y;
int iy = menu_y + 5; int iy = menu_y + 5;
int cur_cat = -1;
for (int i = 0; i < MENU_ROW_COUNT; i++) { for (int i = 0; i < MENU_ROW_COUNT; i++) {
const MenuRow& row = menu_rows[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); int row_h = menu_row_height(row);
if (row.is_category) { if (row.is_category) {
// Separator line above (except first category) // Separator line above (except first category)
if (i > 0) { if (i > 0) {
for (int sx = menu_x + 8; sx < menu_x + MENU_W - 8; sx++) 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]) { 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; int ty = iy + (row_h - system_font_height()) / 2;
if (i > 0) ty += 2; draw_text(fb, tx, ty, row.label, Color::from_rgb(0x66, 0x66, 0x66));
draw_text(fb, tx, ty, row.label, Color::from_rgb(0x88, 0x88, 0x88));
} }
} else { } else {
Rect item_rect = {menu_x + 4, iy, MENU_W - 8, row_h}; 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 // Icon
int icon_x = item_rect.x + 8; int icon_x = item_rect.x + 8;
int icon_y = item_rect.y + (row_h - 20) / 2; 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]; SvgIcon* icon = icons[row.app_id];
if (icon && icon->pixels) { if (icon && icon->pixels) {
fb.blit_alpha(icon_x, icon_y, icon->width, icon->height, 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}; Rect menu_rect = {menu_x, menu_y, MENU_W, menu_h};
if (menu_rect.contains(mx, my)) { 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 iy = menu_y + 5;
int cur_cat = -1;
for (int i = 0; i < MENU_ROW_COUNT; i++) { for (int i = 0; i < MENU_ROW_COUNT; i++) {
const MenuRow& row = menu_rows[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); int row_h = menu_row_height(row);
if (my >= iy && my < iy + row_h) { 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) { switch (row.app_id) {
case 0: open_terminal(ds); break; case 0: open_terminal(ds); break;
case 1: open_filemanager(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 12: open_reboot_dialog(ds); break;
case 13: open_weather(ds); break; case 13: open_weather(ds); break;
case 14: open_shutdown_dialog(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; ds->app_menu_open = false;
} }
@@ -1509,6 +1577,10 @@ void gui::desktop_handle_keyboard(DesktopState* ds, const Montauk::KeyEvent& key
open_texteditor(ds); open_texteditor(ds);
return; return;
} }
if (key.ascii == 'w' || key.ascii == 'W') {
open_wordprocessor(ds);
return;
}
if (key.ascii == 'k' || key.ascii == 'K') { if (key.ascii == 'k' || key.ascii == 'K') {
open_klog(ds); open_klog(ds);
return; return;
+86
View File
@@ -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)
+245
View File
@@ -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();
}
+125
View File
@@ -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);
}
+308
View File
@@ -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'; }
}
+194
View File
@@ -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';
}
}
}
+538
View File
@@ -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);
}
+342
View File
@@ -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);
}
}
}
+240
View File
@@ -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>
+7
View File
@@ -16,10 +16,17 @@ FONTS=(
"programs/gui/fonts/Roboto/static/Roboto-Regular.ttf" "programs/gui/fonts/Roboto/static/Roboto-Regular.ttf"
"programs/gui/fonts/Roboto/static/Roboto-Bold.ttf" "programs/gui/fonts/Roboto/static/Roboto-Bold.ttf"
"programs/gui/fonts/Roboto/static/Roboto-Medium.ttf" "programs/gui/fonts/Roboto/static/Roboto-Medium.ttf"
"programs/gui/fonts/Roboto/static/Roboto-Italic.ttf"
"programs/gui/fonts/Roboto/static/Roboto-BoldItalic.ttf"
"programs/gui/fonts/JetBrains_Mono/static/JetBrainsMono-Regular.ttf" "programs/gui/fonts/JetBrains_Mono/static/JetBrainsMono-Regular.ttf"
"programs/gui/fonts/JetBrains_Mono/static/JetBrainsMono-Bold.ttf" "programs/gui/fonts/JetBrains_Mono/static/JetBrainsMono-Bold.ttf"
"programs/gui/fonts/Noto_Serif/static/NotoSerif-Regular.ttf" "programs/gui/fonts/Noto_Serif/static/NotoSerif-Regular.ttf"
"programs/gui/fonts/Noto_Serif/static/NotoSerif-SemiBold.ttf" "programs/gui/fonts/Noto_Serif/static/NotoSerif-SemiBold.ttf"
"programs/gui/fonts/Noto_Serif/static/NotoSerif-Italic.ttf"
"programs/gui/fonts/Noto_Serif/static/NotoSerif-BoldItalic.ttf"
"programs/gui/fonts/C059/C059-Roman.ttf"
"programs/gui/fonts/C059/C059-Bold.ttf"
"programs/gui/fonts/C059/C059-Italic.ttf"
) )
copied=0 copied=0
+1
View File
@@ -84,6 +84,7 @@ ICONS=(
"panel/weather-none-available.svg" "panel/weather-none-available.svg"
"apps/symbolic/utilities-system-monitor-symbolic.svg" # system monitor "apps/symbolic/utilities-system-monitor-symbolic.svg" # system monitor
"apps/scalable/utilities-system-monitor.svg" # system monitor "apps/scalable/utilities-system-monitor.svg" # system monitor
"mimetypes/scalable/spreadsheet.svg"
) )
copied=0 copied=0