feat: split word processor app out into standalone Window Server app
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 spreadsheet pdfviewer disks devexplorer installer volume music bluetooth desktop login shell rpgdemo paint tcc screenshot texteditor
|
||||
CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music bluetooth desktop login shell rpgdemo paint tcc screenshot texteditor
|
||||
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
|
||||
|
||||
# Build targets: system programs go to bin/os/, apps go to bin/apps/<name>/.
|
||||
@@ -81,9 +81,9 @@ CA_CERTS := $(BINDIR)/etc/ca-certificates.crt
|
||||
# Common shared assets (wallpapers, etc.)
|
||||
COMMONKEEP := $(BINDIR)/common/.keep
|
||||
|
||||
.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer installer volume music bluetooth login desktop shell rpgdemo paint tcc screenshot texteditor icons fonts bearssl libc tls libjpeg libjpegwrite install-apps
|
||||
.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music bluetooth login desktop shell rpgdemo paint tcc screenshot texteditor icons fonts bearssl libc tls libjpeg libjpegwrite install-apps
|
||||
|
||||
all: bearssl libc libjpeg libjpegwrite tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer installer volume music bluetooth doom rpgdemo paint tcc screenshot texteditor login desktop shell icons fonts install-apps $(MANDST) $(WWWDST) $(CA_CERTS) $(COMMONKEEP)
|
||||
all: bearssl libc libjpeg libjpegwrite tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music bluetooth doom rpgdemo paint tcc screenshot texteditor login desktop shell icons fonts install-apps $(MANDST) $(WWWDST) $(CA_CERTS) $(COMMONKEEP)
|
||||
|
||||
# Build BearSSL static library (cross-compiled for freestanding x86_64).
|
||||
BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc)
|
||||
@@ -140,6 +140,10 @@ fontpreview:
|
||||
spreadsheet: libc
|
||||
$(MAKE) -C src/spreadsheet
|
||||
|
||||
# Build word processor standalone GUI client (depends on libc).
|
||||
wordprocessor: libc
|
||||
$(MAKE) -C src/wordprocessor
|
||||
|
||||
# Build PDF viewer standalone GUI client (depends on libc).
|
||||
pdfviewer: libc
|
||||
$(MAKE) -C src/pdfviewer
|
||||
@@ -218,7 +222,7 @@ tcc: libc
|
||||
cp include/libc/sys/*.h $(BINDIR)/lib/tcc/include/sys/
|
||||
|
||||
# Install app bundles (manifests, icons, data files) into bin/apps/<name>/.
|
||||
install-apps: doom rpgdemo paint spreadsheet weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer volume music bluetooth screenshot texteditor
|
||||
install-apps: doom rpgdemo paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer volume music bluetooth screenshot texteditor
|
||||
../scripts/install_apps.sh
|
||||
|
||||
# Copy man pages into bin/man/ so mkramdisk.sh picks them up.
|
||||
@@ -264,6 +268,7 @@ clean:
|
||||
$(MAKE) -C src/imageviewer clean
|
||||
$(MAKE) -C src/fontpreview clean
|
||||
$(MAKE) -C src/spreadsheet clean
|
||||
$(MAKE) -C src/wordprocessor clean
|
||||
$(MAKE) -C src/pdfviewer clean
|
||||
$(MAKE) -C src/disks clean
|
||||
$(MAKE) -C src/devexplorer clean
|
||||
|
||||
@@ -155,6 +155,10 @@ static bool is_spreadsheet_file(const char* name) {
|
||||
return str_ends_with(name, ".mss");
|
||||
}
|
||||
|
||||
static bool is_wordprocessor_file(const char* name) {
|
||||
return str_ends_with(name, ".mwp");
|
||||
}
|
||||
|
||||
static bool is_video_file(const char* name) {
|
||||
return str_ends_with(name, ".mp4") || str_ends_with(name, ".m4v");
|
||||
}
|
||||
@@ -1070,6 +1074,8 @@ static void filemanager_open_entry(FileManagerState* fm, int idx) {
|
||||
montauk::spawn("0:/apps/pdfviewer/pdfviewer.elf", fullpath);
|
||||
} else if (is_spreadsheet_file(fm->entry_names[idx])) {
|
||||
montauk::spawn("0:/apps/spreadsheet/spreadsheet.elf", fullpath);
|
||||
} else if (is_wordprocessor_file(fm->entry_names[idx])) {
|
||||
montauk::spawn("0:/apps/wordprocessor/wordprocessor.elf", fullpath);
|
||||
} else if (is_video_file(fm->entry_names[idx])) {
|
||||
montauk::spawn("0:/apps/video/video.elf", fullpath);
|
||||
} else if (is_audio_file(fm->entry_names[idx])) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,74 @@
|
||||
# Makefile for wordprocessor (standalone Window Server app) on MontaukOS
|
||||
# Copyright (c) 2026 Daniel Hammer
|
||||
|
||||
MAKEFLAGS += -rR
|
||||
.SUFFIXES:
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
|
||||
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
else
|
||||
CXX := g++
|
||||
endif
|
||||
|
||||
PROG_INC := ../../include
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
LIBDIR := ../../lib
|
||||
|
||||
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
|
||||
|
||||
LDFLAGS := \
|
||||
-nostdlib \
|
||||
-static \
|
||||
-Wl,--build-id=none \
|
||||
-Wl,--gc-sections \
|
||||
-Wl,-m,elf_x86_64 \
|
||||
-z max-page-size=0x1000 \
|
||||
-T $(LINK_LD)
|
||||
|
||||
SRCS := main.cpp document.cpp render.cpp input.cpp stb_truetype_impl.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
|
||||
TARGET := $(BINDIR)/apps/wordprocessor/wordprocessor.elf
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile
|
||||
mkdir -p $(BINDIR)/apps/wordprocessor
|
||||
$(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,872 @@
|
||||
/*
|
||||
* document.cpp
|
||||
* Document model, wrap layout, font loading, and file I/O
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "wordprocessor.hpp"
|
||||
|
||||
static void wp_init_run(StyledRun* r, uint8_t font_id, uint8_t size, uint8_t flags) {
|
||||
r->cap = 64;
|
||||
r->text = (char*)montauk::malloc(r->cap);
|
||||
r->text[0] = '\0';
|
||||
r->len = 0;
|
||||
r->font_id = font_id;
|
||||
r->size = size;
|
||||
r->flags = flags;
|
||||
}
|
||||
|
||||
static void wp_free_run(StyledRun* r) {
|
||||
if (r->text) montauk::mfree(r->text);
|
||||
r->text = nullptr;
|
||||
r->len = 0;
|
||||
r->cap = 0;
|
||||
}
|
||||
|
||||
static void wp_ensure_run_cap(StyledRun* r, int needed) {
|
||||
if (r->len + needed <= r->cap) return;
|
||||
int new_cap = r->cap * 2;
|
||||
if (new_cap < r->len + needed) new_cap = r->len + needed;
|
||||
r->text = (char*)montauk::realloc(r->text, new_cap);
|
||||
r->cap = new_cap;
|
||||
}
|
||||
|
||||
static bool wp_same_style(const StyledRun* a, uint8_t font_id, uint8_t size, uint8_t flags) {
|
||||
return a->font_id == font_id && a->size == size && a->flags == flags;
|
||||
}
|
||||
|
||||
static void wp_font_at(WordProcessorState* wp, int abs_pos,
|
||||
TrueTypeFont** out_font, GlyphCache** out_gc) {
|
||||
int r, o;
|
||||
wp_pos_to_run(wp, abs_pos, &r, &o);
|
||||
if (r >= wp->run_count) r = wp->run_count - 1;
|
||||
StyledRun* run = &wp->runs[r];
|
||||
*out_font = wp_get_font(run->font_id, run->flags);
|
||||
*out_gc = (*out_font && (*out_font)->valid) ? (*out_font)->get_cache(run->size) : nullptr;
|
||||
}
|
||||
|
||||
static void wp_free_runs(WordProcessorState* wp) {
|
||||
for (int i = 0; i < wp->run_count; i++)
|
||||
wp_free_run(&wp->runs[i]);
|
||||
wp->run_count = 0;
|
||||
wp->total_text_len = 0;
|
||||
}
|
||||
|
||||
static void wp_merge_adjacent(WordProcessorState* wp) {
|
||||
int i = 0;
|
||||
while (i < wp->run_count - 1) {
|
||||
StyledRun* a = &wp->runs[i];
|
||||
StyledRun* b = &wp->runs[i + 1];
|
||||
if (a->font_id == b->font_id && a->size == b->size && a->flags == b->flags) {
|
||||
int old_a_len = a->len;
|
||||
wp_ensure_run_cap(a, b->len);
|
||||
montauk::memcpy(a->text + a->len, b->text, b->len);
|
||||
a->len += b->len;
|
||||
|
||||
if (wp->cursor_run == i + 1) {
|
||||
wp->cursor_run = i;
|
||||
wp->cursor_offset = old_a_len + wp->cursor_offset;
|
||||
} else if (wp->cursor_run > i + 1) {
|
||||
wp->cursor_run--;
|
||||
}
|
||||
|
||||
wp_free_run(b);
|
||||
for (int j = i + 1; j < wp->run_count - 1; j++)
|
||||
wp->runs[j] = wp->runs[j + 1];
|
||||
wp->run_count--;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
i = 0;
|
||||
while (i < wp->run_count && wp->run_count > 1) {
|
||||
if (wp->runs[i].len == 0) {
|
||||
if (wp->cursor_run == i) {
|
||||
if (i < wp->run_count - 1) {
|
||||
wp->cursor_run = i;
|
||||
wp->cursor_offset = 0;
|
||||
} else {
|
||||
wp->cursor_run = i - 1;
|
||||
wp->cursor_offset = wp->runs[i - 1].len;
|
||||
}
|
||||
} else if (wp->cursor_run > i) {
|
||||
wp->cursor_run--;
|
||||
}
|
||||
wp_free_run(&wp->runs[i]);
|
||||
for (int j = i; j < wp->run_count - 1; j++)
|
||||
wp->runs[j] = wp->runs[j + 1];
|
||||
wp->run_count--;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int wp_split_at(WordProcessorState* wp, int abs_pos) {
|
||||
int r, o;
|
||||
wp_pos_to_run(wp, abs_pos, &r, &o);
|
||||
if (o == 0) return r;
|
||||
if (o >= wp->runs[r].len) return r + 1;
|
||||
if (wp->run_count >= WP_MAX_RUNS) return r;
|
||||
|
||||
StyledRun* src = &wp->runs[r];
|
||||
int after_len = src->len - o;
|
||||
|
||||
StyledRun after;
|
||||
wp_init_run(&after, src->font_id, src->size, src->flags);
|
||||
wp_ensure_run_cap(&after, after_len);
|
||||
montauk::memcpy(after.text, src->text + o, after_len);
|
||||
after.len = after_len;
|
||||
|
||||
src->len = o;
|
||||
|
||||
for (int i = wp->run_count - 1; i > r; i--)
|
||||
wp->runs[i + 1] = wp->runs[i];
|
||||
wp->runs[r + 1] = after;
|
||||
wp->run_count++;
|
||||
|
||||
if (wp->cursor_run == r && wp->cursor_offset > o) {
|
||||
wp->cursor_run = r + 1;
|
||||
wp->cursor_offset -= o;
|
||||
} else if (wp->cursor_run > r) {
|
||||
wp->cursor_run++;
|
||||
}
|
||||
|
||||
return r + 1;
|
||||
}
|
||||
|
||||
void wp_load_fonts() {
|
||||
if (g_wp_fonts.loaded) return;
|
||||
|
||||
auto load = [](const char* path) -> TrueTypeFont* {
|
||||
TrueTypeFont* f = (TrueTypeFont*)montauk::malloc(sizeof(TrueTypeFont));
|
||||
montauk::memset(f, 0, sizeof(TrueTypeFont));
|
||||
if (!f->init(path)) {
|
||||
montauk::mfree(f);
|
||||
return nullptr;
|
||||
}
|
||||
return f;
|
||||
};
|
||||
|
||||
g_wp_fonts.fonts[FONT_ROBOTO][0] = fonts::system_font ? fonts::system_font : load("0:/fonts/Roboto-Medium.ttf");
|
||||
g_wp_fonts.fonts[FONT_ROBOTO][1] = fonts::system_bold ? fonts::system_bold : load("0:/fonts/Roboto-Bold.ttf");
|
||||
g_wp_fonts.fonts[FONT_ROBOTO][2] = load("0:/fonts/Roboto-Italic.ttf");
|
||||
g_wp_fonts.fonts[FONT_ROBOTO][3] = load("0:/fonts/Roboto-BoldItalic.ttf");
|
||||
|
||||
g_wp_fonts.fonts[FONT_NOTOSERIF][0] = load("0:/fonts/NotoSerif-Regular.ttf");
|
||||
g_wp_fonts.fonts[FONT_NOTOSERIF][1] = load("0:/fonts/NotoSerif-SemiBold.ttf");
|
||||
g_wp_fonts.fonts[FONT_NOTOSERIF][2] = load("0:/fonts/NotoSerif-Italic.ttf");
|
||||
g_wp_fonts.fonts[FONT_NOTOSERIF][3] = load("0:/fonts/NotoSerif-BoldItalic.ttf");
|
||||
|
||||
g_wp_fonts.fonts[FONT_C059][0] = load("0:/fonts/C059-Roman.ttf");
|
||||
g_wp_fonts.fonts[FONT_C059][1] = load("0:/fonts/C059-Bold.ttf");
|
||||
g_wp_fonts.fonts[FONT_C059][2] = load("0:/fonts/C059-Italic.ttf");
|
||||
g_wp_fonts.fonts[FONT_C059][3] = load("0:/fonts/C059-Bold.ttf");
|
||||
|
||||
g_ui_font = g_wp_fonts.fonts[FONT_ROBOTO][0];
|
||||
g_ui_bold = g_wp_fonts.fonts[FONT_ROBOTO][1] ? g_wp_fonts.fonts[FONT_ROBOTO][1] : g_ui_font;
|
||||
g_wp_fonts.loaded = true;
|
||||
}
|
||||
|
||||
TrueTypeFont* wp_get_font(int font_id, uint8_t flags) {
|
||||
if (font_id < 0 || font_id >= FONT_COUNT) font_id = 0;
|
||||
|
||||
int variant = 0;
|
||||
if ((flags & STYLE_BOLD) && (flags & STYLE_ITALIC)) variant = 3;
|
||||
else if (flags & STYLE_BOLD) variant = 1;
|
||||
else if (flags & STYLE_ITALIC) variant = 2;
|
||||
|
||||
TrueTypeFont* f = g_wp_fonts.fonts[font_id][variant];
|
||||
if (f && f->valid) return f;
|
||||
|
||||
f = g_wp_fonts.fonts[font_id][0];
|
||||
if (f && f->valid) return f;
|
||||
|
||||
return g_ui_font ? g_ui_font : fonts::system_font;
|
||||
}
|
||||
|
||||
void wp_init_empty_document(WordProcessorState* wp) {
|
||||
wp_init_run(&wp->runs[0], FONT_ROBOTO, WP_DEFAULT_SIZE, 0);
|
||||
wp->run_count = 1;
|
||||
wp->total_text_len = 0;
|
||||
|
||||
wp->cursor_run = 0;
|
||||
wp->cursor_offset = 0;
|
||||
|
||||
wp->sel_anchor = 0;
|
||||
wp->sel_end = 0;
|
||||
wp->has_selection = false;
|
||||
wp->mouse_selecting = false;
|
||||
|
||||
wp->cur_font_id = FONT_ROBOTO;
|
||||
wp->cur_size = WP_DEFAULT_SIZE;
|
||||
wp->cur_flags = 0;
|
||||
|
||||
wp->scrollbar.init(0, 0, WP_SCROLLBAR_W, 100);
|
||||
wp->content_height = 0;
|
||||
|
||||
wp->wrap_lines = nullptr;
|
||||
wp->wrap_line_count = 0;
|
||||
wp->wrap_dirty = true;
|
||||
wp->last_wrap_width = 0;
|
||||
|
||||
wp->modified = false;
|
||||
wp->filepath[0] = '\0';
|
||||
wp->filename[0] = '\0';
|
||||
|
||||
wp->show_pathbar = false;
|
||||
wp->pathbar_save_mode = false;
|
||||
wp->pathbar_text[0] = '\0';
|
||||
wp->pathbar_cursor = 0;
|
||||
wp->pathbar_len = 0;
|
||||
|
||||
wp->font_dropdown_open = false;
|
||||
wp->size_dropdown_open = false;
|
||||
}
|
||||
|
||||
void wp_free_document(WordProcessorState* wp) {
|
||||
wp_free_runs(wp);
|
||||
if (wp->wrap_lines) {
|
||||
montauk::mfree(wp->wrap_lines);
|
||||
wp->wrap_lines = nullptr;
|
||||
}
|
||||
wp->wrap_line_count = 0;
|
||||
}
|
||||
|
||||
int wp_abs_pos(WordProcessorState* wp, int run, int offset) {
|
||||
int pos = 0;
|
||||
for (int i = 0; i < run && i < wp->run_count; i++)
|
||||
pos += wp->runs[i].len;
|
||||
pos += offset;
|
||||
return pos;
|
||||
}
|
||||
|
||||
void wp_pos_to_run(WordProcessorState* wp, int abs_pos, int* out_run, int* out_offset) {
|
||||
if (abs_pos <= 0) {
|
||||
*out_run = 0;
|
||||
*out_offset = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
int pos = 0;
|
||||
for (int i = 0; i < wp->run_count; i++) {
|
||||
if (pos + wp->runs[i].len >= abs_pos || i == wp->run_count - 1) {
|
||||
*out_run = i;
|
||||
*out_offset = abs_pos - pos;
|
||||
if (*out_offset > wp->runs[i].len) *out_offset = wp->runs[i].len;
|
||||
return;
|
||||
}
|
||||
pos += wp->runs[i].len;
|
||||
}
|
||||
|
||||
*out_run = 0;
|
||||
*out_offset = 0;
|
||||
}
|
||||
|
||||
char wp_char_at(WordProcessorState* wp, int abs_pos) {
|
||||
int r, o;
|
||||
wp_pos_to_run(wp, abs_pos, &r, &o);
|
||||
if (r < wp->run_count && o < wp->runs[r].len)
|
||||
return wp->runs[r].text[o];
|
||||
return '\0';
|
||||
}
|
||||
|
||||
void wp_insert_char(WordProcessorState* wp, char c) {
|
||||
if (wp->total_text_len >= WP_MAX_TEXT - 1) return;
|
||||
|
||||
StyledRun* cur = &wp->runs[wp->cursor_run];
|
||||
if (wp_same_style(cur, wp->cur_font_id, wp->cur_size, wp->cur_flags)) {
|
||||
wp_ensure_run_cap(cur, 1);
|
||||
for (int i = cur->len; i > wp->cursor_offset; i--)
|
||||
cur->text[i] = cur->text[i - 1];
|
||||
cur->text[wp->cursor_offset] = c;
|
||||
cur->len++;
|
||||
wp->cursor_offset++;
|
||||
wp->total_text_len++;
|
||||
wp->modified = true;
|
||||
wp->wrap_dirty = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (wp->cursor_offset == 0) {
|
||||
if (wp->run_count >= WP_MAX_RUNS) return;
|
||||
for (int i = wp->run_count; i > wp->cursor_run; i--)
|
||||
wp->runs[i] = wp->runs[i - 1];
|
||||
wp->run_count++;
|
||||
StyledRun* nr = &wp->runs[wp->cursor_run];
|
||||
wp_init_run(nr, wp->cur_font_id, wp->cur_size, wp->cur_flags);
|
||||
wp_ensure_run_cap(nr, 1);
|
||||
nr->text[0] = c;
|
||||
nr->len = 1;
|
||||
wp->cursor_offset = 1;
|
||||
wp->total_text_len++;
|
||||
} else if (wp->cursor_offset == cur->len) {
|
||||
if (wp->run_count >= WP_MAX_RUNS) return;
|
||||
int insert_idx = wp->cursor_run + 1;
|
||||
for (int i = wp->run_count; i > insert_idx; i--)
|
||||
wp->runs[i] = wp->runs[i - 1];
|
||||
wp->run_count++;
|
||||
StyledRun* nr = &wp->runs[insert_idx];
|
||||
wp_init_run(nr, wp->cur_font_id, wp->cur_size, wp->cur_flags);
|
||||
wp_ensure_run_cap(nr, 1);
|
||||
nr->text[0] = c;
|
||||
nr->len = 1;
|
||||
wp->cursor_run = insert_idx;
|
||||
wp->cursor_offset = 1;
|
||||
wp->total_text_len++;
|
||||
} else {
|
||||
if (wp->run_count + 2 > WP_MAX_RUNS) return;
|
||||
int split_at = wp->cursor_offset;
|
||||
int after_len = cur->len - split_at;
|
||||
|
||||
StyledRun after;
|
||||
wp_init_run(&after, cur->font_id, cur->size, cur->flags);
|
||||
wp_ensure_run_cap(&after, after_len);
|
||||
montauk::memcpy(after.text, cur->text + split_at, after_len);
|
||||
after.len = after_len;
|
||||
|
||||
cur->len = split_at;
|
||||
|
||||
int new_idx = wp->cursor_run + 1;
|
||||
for (int i = wp->run_count - 1; i >= new_idx; i--)
|
||||
wp->runs[i + 2] = wp->runs[i];
|
||||
|
||||
StyledRun* nr = &wp->runs[new_idx];
|
||||
wp_init_run(nr, wp->cur_font_id, wp->cur_size, wp->cur_flags);
|
||||
wp_ensure_run_cap(nr, 1);
|
||||
nr->text[0] = c;
|
||||
nr->len = 1;
|
||||
|
||||
wp->runs[new_idx + 1] = after;
|
||||
wp->run_count += 2;
|
||||
wp->cursor_run = new_idx;
|
||||
wp->cursor_offset = 1;
|
||||
wp->total_text_len++;
|
||||
}
|
||||
|
||||
wp->modified = true;
|
||||
wp->wrap_dirty = true;
|
||||
}
|
||||
|
||||
void wp_backspace(WordProcessorState* wp) {
|
||||
if (wp->cursor_run == 0 && wp->cursor_offset == 0) return;
|
||||
|
||||
if (wp->cursor_offset > 0) {
|
||||
StyledRun* r = &wp->runs[wp->cursor_run];
|
||||
for (int i = wp->cursor_offset - 1; i < r->len - 1; i++)
|
||||
r->text[i] = r->text[i + 1];
|
||||
r->len--;
|
||||
wp->cursor_offset--;
|
||||
} else {
|
||||
wp->cursor_run--;
|
||||
StyledRun* r = &wp->runs[wp->cursor_run];
|
||||
if (r->len > 0) r->len--;
|
||||
wp->cursor_offset = r->len;
|
||||
}
|
||||
|
||||
wp->total_text_len--;
|
||||
wp->modified = true;
|
||||
wp->wrap_dirty = true;
|
||||
wp_merge_adjacent(wp);
|
||||
}
|
||||
|
||||
void wp_delete_char(WordProcessorState* wp) {
|
||||
int abs = wp_abs_pos(wp, wp->cursor_run, wp->cursor_offset);
|
||||
if (abs >= wp->total_text_len) return;
|
||||
|
||||
StyledRun* r = &wp->runs[wp->cursor_run];
|
||||
if (wp->cursor_offset < r->len) {
|
||||
for (int i = wp->cursor_offset; i < r->len - 1; i++)
|
||||
r->text[i] = r->text[i + 1];
|
||||
r->len--;
|
||||
} else if (wp->cursor_run + 1 < wp->run_count) {
|
||||
StyledRun* next = &wp->runs[wp->cursor_run + 1];
|
||||
if (next->len > 0) {
|
||||
for (int i = 0; i < next->len - 1; i++)
|
||||
next->text[i] = next->text[i + 1];
|
||||
next->len--;
|
||||
}
|
||||
}
|
||||
|
||||
wp->total_text_len--;
|
||||
wp->modified = true;
|
||||
wp->wrap_dirty = true;
|
||||
wp_merge_adjacent(wp);
|
||||
}
|
||||
|
||||
void wp_cursor_left(WordProcessorState* wp) {
|
||||
if (wp->cursor_offset > 0) {
|
||||
wp->cursor_offset--;
|
||||
} else if (wp->cursor_run > 0) {
|
||||
wp->cursor_run--;
|
||||
wp->cursor_offset = wp->runs[wp->cursor_run].len;
|
||||
}
|
||||
}
|
||||
|
||||
void wp_cursor_right(WordProcessorState* wp) {
|
||||
if (wp->cursor_offset < wp->runs[wp->cursor_run].len) {
|
||||
wp->cursor_offset++;
|
||||
} else if (wp->cursor_run + 1 < wp->run_count) {
|
||||
wp->cursor_run++;
|
||||
wp->cursor_offset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void wp_clear_selection(WordProcessorState* wp) {
|
||||
wp->has_selection = false;
|
||||
wp->sel_anchor = 0;
|
||||
wp->sel_end = 0;
|
||||
}
|
||||
|
||||
void wp_sel_range(WordProcessorState* wp, int* out_start, int* out_end) {
|
||||
if (wp->sel_anchor < wp->sel_end) {
|
||||
*out_start = wp->sel_anchor;
|
||||
*out_end = wp->sel_end;
|
||||
} else {
|
||||
*out_start = wp->sel_end;
|
||||
*out_end = wp->sel_anchor;
|
||||
}
|
||||
}
|
||||
|
||||
void wp_start_selection(WordProcessorState* wp) {
|
||||
if (!wp->has_selection) {
|
||||
wp->sel_anchor = wp_abs_pos(wp, wp->cursor_run, wp->cursor_offset);
|
||||
wp->sel_end = wp->sel_anchor;
|
||||
wp->has_selection = true;
|
||||
}
|
||||
}
|
||||
|
||||
void wp_update_selection_to_cursor(WordProcessorState* wp) {
|
||||
wp->sel_end = wp_abs_pos(wp, wp->cursor_run, wp->cursor_offset);
|
||||
if (wp->sel_anchor == wp->sel_end) wp->has_selection = false;
|
||||
}
|
||||
|
||||
void wp_apply_style_to_selection(WordProcessorState* wp, int mode, int value) {
|
||||
if (!wp->has_selection) return;
|
||||
|
||||
int sel_s, sel_e;
|
||||
wp_sel_range(wp, &sel_s, &sel_e);
|
||||
if (sel_s >= sel_e) return;
|
||||
|
||||
int end_ri = wp_split_at(wp, sel_e);
|
||||
int start_ri = wp_split_at(wp, sel_s);
|
||||
|
||||
{
|
||||
int pos = 0;
|
||||
end_ri = wp->run_count;
|
||||
for (int i = 0; i < wp->run_count; i++) {
|
||||
if (pos >= sel_e) {
|
||||
end_ri = i;
|
||||
break;
|
||||
}
|
||||
pos += wp->runs[i].len;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = start_ri; i < end_ri && i < wp->run_count; i++) {
|
||||
StyledRun* r = &wp->runs[i];
|
||||
switch (mode) {
|
||||
case 0: r->font_id = (uint8_t)value; break;
|
||||
case 1: r->size = (uint8_t)value; break;
|
||||
case 2: r->flags ^= STYLE_BOLD; break;
|
||||
case 3: r->flags ^= STYLE_ITALIC; break;
|
||||
}
|
||||
}
|
||||
|
||||
wp->wrap_dirty = true;
|
||||
wp->modified = true;
|
||||
wp_merge_adjacent(wp);
|
||||
}
|
||||
|
||||
void wp_delete_selection(WordProcessorState* wp) {
|
||||
if (!wp->has_selection) return;
|
||||
|
||||
int sel_s, sel_e;
|
||||
wp_sel_range(wp, &sel_s, &sel_e);
|
||||
int count = sel_e - sel_s;
|
||||
if (count <= 0) {
|
||||
wp_clear_selection(wp);
|
||||
return;
|
||||
}
|
||||
|
||||
wp_pos_to_run(wp, sel_s, &wp->cursor_run, &wp->cursor_offset);
|
||||
for (int i = 0; i < count; i++)
|
||||
wp_delete_char(wp);
|
||||
|
||||
wp_clear_selection(wp);
|
||||
}
|
||||
|
||||
void wp_recompute_wrap(WordProcessorState* wp, int content_w) {
|
||||
int wrap_width = content_w - WP_MARGIN * 2 - WP_SCROLLBAR_W;
|
||||
if (wrap_width < 50) wrap_width = 50;
|
||||
|
||||
if (wrap_width != wp->last_wrap_width) {
|
||||
wp->wrap_dirty = true;
|
||||
wp->last_wrap_width = wrap_width;
|
||||
}
|
||||
|
||||
if (!wp->wrap_dirty && wp->wrap_lines) return;
|
||||
|
||||
if (!wp->wrap_lines)
|
||||
wp->wrap_lines = (WrapLine*)montauk::malloc(WP_MAX_WRAP_LINES * sizeof(WrapLine));
|
||||
|
||||
wp->wrap_line_count = 0;
|
||||
int total = wp->total_text_len;
|
||||
int y = WP_MARGIN;
|
||||
int pos = 0;
|
||||
|
||||
while (pos < total && wp->wrap_line_count < WP_MAX_WRAP_LINES) {
|
||||
WrapLine* line = &wp->wrap_lines[wp->wrap_line_count];
|
||||
wp_pos_to_run(wp, pos, &line->run_idx, &line->run_offset);
|
||||
line->char_count = 0;
|
||||
line->y = y;
|
||||
|
||||
int x = 0;
|
||||
int last_space = -1;
|
||||
int line_start = pos;
|
||||
int max_ascent = 0;
|
||||
int max_height = 0;
|
||||
|
||||
int scan = pos;
|
||||
while (scan < total) {
|
||||
char ch = wp_char_at(wp, scan);
|
||||
|
||||
TrueTypeFont* font;
|
||||
GlyphCache* gc;
|
||||
wp_font_at(wp, scan, &font, &gc);
|
||||
if (gc) {
|
||||
if (gc->ascent > max_ascent) max_ascent = gc->ascent;
|
||||
if (gc->line_height > max_height) max_height = gc->line_height;
|
||||
}
|
||||
|
||||
if (ch == '\n') {
|
||||
scan++;
|
||||
break;
|
||||
}
|
||||
|
||||
int cw = 8;
|
||||
if (font && gc) {
|
||||
CachedGlyph* g = font->get_glyph(gc, (unsigned char)ch);
|
||||
cw = g ? g->advance : 8;
|
||||
}
|
||||
|
||||
if (x + cw > wrap_width && scan > line_start) {
|
||||
if (last_space >= line_start) scan = last_space + 1;
|
||||
break;
|
||||
}
|
||||
|
||||
if (ch == ' ') last_space = scan;
|
||||
x += cw;
|
||||
scan++;
|
||||
}
|
||||
|
||||
line->char_count = scan - line_start;
|
||||
|
||||
if (max_height == 0) {
|
||||
TrueTypeFont* df = wp_get_font(wp->cur_font_id, 0);
|
||||
if (df && df->valid) {
|
||||
GlyphCache* dgc = df->get_cache(wp->cur_size);
|
||||
max_height = dgc->line_height;
|
||||
max_ascent = dgc->ascent;
|
||||
} else {
|
||||
max_height = WP_DEFAULT_SIZE;
|
||||
max_ascent = WP_DEFAULT_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
max_ascent = 0;
|
||||
max_height = 0;
|
||||
{
|
||||
int ri, ro;
|
||||
wp_pos_to_run(wp, line_start, &ri, &ro);
|
||||
int left = line->char_count;
|
||||
while (left > 0 && ri < wp->run_count) {
|
||||
StyledRun* r = &wp->runs[ri];
|
||||
TrueTypeFont* font = wp_get_font(r->font_id, r->flags);
|
||||
if (font && font->valid) {
|
||||
GlyphCache* gc = font->get_cache(r->size);
|
||||
if (gc->ascent > max_ascent) max_ascent = gc->ascent;
|
||||
if (gc->line_height > max_height) max_height = gc->line_height;
|
||||
}
|
||||
int avail = r->len - ro;
|
||||
int consume = avail < left ? avail : left;
|
||||
left -= consume;
|
||||
ro += consume;
|
||||
if (ro >= r->len) {
|
||||
ri++;
|
||||
ro = 0;
|
||||
}
|
||||
}
|
||||
if (max_height == 0) {
|
||||
TrueTypeFont* df = wp_get_font(wp->cur_font_id, 0);
|
||||
if (df && df->valid) {
|
||||
GlyphCache* dgc = df->get_cache(wp->cur_size);
|
||||
max_height = dgc->line_height;
|
||||
max_ascent = dgc->ascent;
|
||||
} else {
|
||||
max_height = WP_DEFAULT_SIZE;
|
||||
max_ascent = WP_DEFAULT_SIZE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
line->height = max_height;
|
||||
line->baseline = max_ascent;
|
||||
wp->wrap_line_count++;
|
||||
|
||||
y += max_height;
|
||||
pos = line_start + line->char_count;
|
||||
|
||||
if (line->char_count == 0 && pos < total)
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (total > 0 && wp_char_at(wp, total - 1) == '\n' && wp->wrap_line_count < WP_MAX_WRAP_LINES) {
|
||||
WrapLine* line = &wp->wrap_lines[wp->wrap_line_count];
|
||||
wp_pos_to_run(wp, total, &line->run_idx, &line->run_offset);
|
||||
line->char_count = 0;
|
||||
line->y = y;
|
||||
|
||||
TrueTypeFont* df = wp_get_font(wp->cur_font_id, wp->cur_flags);
|
||||
if (df && df->valid) {
|
||||
GlyphCache* dgc = df->get_cache(wp->cur_size);
|
||||
line->height = dgc->line_height;
|
||||
line->baseline = dgc->ascent;
|
||||
} else {
|
||||
line->height = WP_DEFAULT_SIZE;
|
||||
line->baseline = WP_DEFAULT_SIZE;
|
||||
}
|
||||
|
||||
wp->wrap_line_count++;
|
||||
y += line->height;
|
||||
}
|
||||
|
||||
if (wp->wrap_line_count == 0) {
|
||||
WrapLine* line = &wp->wrap_lines[0];
|
||||
line->run_idx = 0;
|
||||
line->run_offset = 0;
|
||||
line->char_count = 0;
|
||||
line->y = WP_MARGIN;
|
||||
|
||||
TrueTypeFont* df = wp_get_font(wp->cur_font_id, 0);
|
||||
if (df && df->valid) {
|
||||
GlyphCache* dgc = df->get_cache(wp->cur_size);
|
||||
line->height = dgc->line_height;
|
||||
line->baseline = dgc->ascent;
|
||||
} else {
|
||||
line->height = WP_DEFAULT_SIZE;
|
||||
line->baseline = WP_DEFAULT_SIZE;
|
||||
}
|
||||
|
||||
wp->wrap_line_count = 1;
|
||||
y = WP_MARGIN + line->height;
|
||||
}
|
||||
|
||||
wp->content_height = y + WP_MARGIN;
|
||||
wp->wrap_dirty = false;
|
||||
}
|
||||
|
||||
int wp_find_wrap_line(WordProcessorState* wp, int abs_pos) {
|
||||
int pos = 0;
|
||||
for (int i = 0; i < wp->wrap_line_count; i++) {
|
||||
int next_pos = pos + wp->wrap_lines[i].char_count;
|
||||
if (abs_pos < next_pos || i == wp->wrap_line_count - 1)
|
||||
return i;
|
||||
pos = next_pos;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int wp_wrap_line_start(WordProcessorState* wp, int line_idx) {
|
||||
int pos = 0;
|
||||
for (int i = 0; i < line_idx && i < wp->wrap_line_count; i++)
|
||||
pos += wp->wrap_lines[i].char_count;
|
||||
return pos;
|
||||
}
|
||||
|
||||
void wp_cursor_up(WordProcessorState* wp) {
|
||||
int abs = wp_abs_pos(wp, wp->cursor_run, wp->cursor_offset);
|
||||
int line = wp_find_wrap_line(wp, abs);
|
||||
if (line <= 0) return;
|
||||
|
||||
int col = abs - wp_wrap_line_start(wp, line);
|
||||
int prev_start = wp_wrap_line_start(wp, line - 1);
|
||||
int prev_len = wp->wrap_lines[line - 1].char_count;
|
||||
int new_col = col < prev_len ? col : prev_len;
|
||||
if (new_col > 0 && new_col == prev_len) {
|
||||
char ch = wp_char_at(wp, prev_start + prev_len - 1);
|
||||
if (ch == '\n') new_col = prev_len - 1;
|
||||
}
|
||||
wp_pos_to_run(wp, prev_start + new_col, &wp->cursor_run, &wp->cursor_offset);
|
||||
}
|
||||
|
||||
void wp_cursor_down(WordProcessorState* wp) {
|
||||
int abs = wp_abs_pos(wp, wp->cursor_run, wp->cursor_offset);
|
||||
int line = wp_find_wrap_line(wp, abs);
|
||||
if (line >= wp->wrap_line_count - 1) return;
|
||||
|
||||
int col = abs - wp_wrap_line_start(wp, line);
|
||||
int next_start = wp_wrap_line_start(wp, line + 1);
|
||||
int next_len = wp->wrap_lines[line + 1].char_count;
|
||||
int new_col = col < next_len ? col : next_len;
|
||||
if (new_col > 0 && new_col == next_len && line + 1 < wp->wrap_line_count - 1) {
|
||||
char ch = wp_char_at(wp, next_start + next_len - 1);
|
||||
if (ch == '\n') new_col = next_len - 1;
|
||||
}
|
||||
wp_pos_to_run(wp, next_start + new_col, &wp->cursor_run, &wp->cursor_offset);
|
||||
}
|
||||
|
||||
void wp_ensure_cursor_visible(WordProcessorState* wp, int view_h) {
|
||||
int abs = wp_abs_pos(wp, wp->cursor_run, wp->cursor_offset);
|
||||
int line = wp_find_wrap_line(wp, abs);
|
||||
if (line < 0 || line >= wp->wrap_line_count) return;
|
||||
|
||||
int cy = wp->wrap_lines[line].y;
|
||||
int ch = wp->wrap_lines[line].height;
|
||||
|
||||
if (cy < wp->scrollbar.scroll_offset) {
|
||||
wp->scrollbar.scroll_offset = cy - WP_MARGIN;
|
||||
if (wp->scrollbar.scroll_offset < 0) wp->scrollbar.scroll_offset = 0;
|
||||
}
|
||||
if (cy + ch > wp->scrollbar.scroll_offset + view_h) {
|
||||
wp->scrollbar.scroll_offset = cy + ch - view_h + WP_MARGIN;
|
||||
}
|
||||
|
||||
int ms = wp->scrollbar.max_scroll();
|
||||
if (wp->scrollbar.scroll_offset > ms) wp->scrollbar.scroll_offset = ms;
|
||||
if (wp->scrollbar.scroll_offset < 0) wp->scrollbar.scroll_offset = 0;
|
||||
}
|
||||
|
||||
void wp_set_filepath(WordProcessorState* wp, const char* path) {
|
||||
montauk::strncpy(wp->filepath, path, 255);
|
||||
int last_slash = -1;
|
||||
for (int i = 0; path[i]; i++)
|
||||
if (path[i] == '/') last_slash = i;
|
||||
if (last_slash >= 0)
|
||||
montauk::strncpy(wp->filename, path + last_slash + 1, 63);
|
||||
else
|
||||
montauk::strncpy(wp->filename, path, 63);
|
||||
}
|
||||
|
||||
void wp_open_save_pathbar(WordProcessorState* wp) {
|
||||
wp->show_pathbar = true;
|
||||
wp->pathbar_save_mode = true;
|
||||
montauk::strncpy(wp->pathbar_text, wp->filepath, 255);
|
||||
wp->pathbar_len = montauk::slen(wp->pathbar_text);
|
||||
wp->pathbar_cursor = wp->pathbar_len;
|
||||
}
|
||||
|
||||
void wp_save_file(WordProcessorState* wp) {
|
||||
if (wp->filepath[0] == '\0') {
|
||||
wp_open_save_pathbar(wp);
|
||||
return;
|
||||
}
|
||||
|
||||
int size = 4 + 2 + 1 + 1 + 2;
|
||||
for (int i = 0; i < wp->run_count; i++)
|
||||
size += 2 + 1 + 1 + 1 + 1 + wp->runs[i].len;
|
||||
|
||||
uint8_t* buf = (uint8_t*)montauk::malloc(size);
|
||||
int off = 0;
|
||||
|
||||
buf[off++] = 'M'; buf[off++] = 'W'; buf[off++] = 'P'; buf[off++] = '1';
|
||||
buf[off++] = 1; buf[off++] = 0;
|
||||
buf[off++] = wp->cur_font_id;
|
||||
buf[off++] = wp->cur_size;
|
||||
buf[off++] = (uint8_t)(wp->run_count & 0xFF);
|
||||
buf[off++] = (uint8_t)((wp->run_count >> 8) & 0xFF);
|
||||
|
||||
for (int i = 0; i < wp->run_count; i++) {
|
||||
StyledRun* r = &wp->runs[i];
|
||||
buf[off++] = (uint8_t)(r->len & 0xFF);
|
||||
buf[off++] = (uint8_t)((r->len >> 8) & 0xFF);
|
||||
buf[off++] = r->font_id;
|
||||
buf[off++] = r->size;
|
||||
buf[off++] = r->flags;
|
||||
buf[off++] = 0;
|
||||
montauk::memcpy(buf + off, r->text, r->len);
|
||||
off += r->len;
|
||||
}
|
||||
|
||||
int fd = montauk::fcreate(wp->filepath);
|
||||
if (fd >= 0) {
|
||||
montauk::fwrite(fd, buf, 0, off);
|
||||
montauk::close(fd);
|
||||
wp->modified = false;
|
||||
}
|
||||
|
||||
montauk::mfree(buf);
|
||||
}
|
||||
|
||||
void wp_load_file(WordProcessorState* wp, const char* path) {
|
||||
int fd = montauk::open(path);
|
||||
if (fd < 0) return;
|
||||
|
||||
uint64_t fsize = montauk::getsize(fd);
|
||||
if (fsize < 10 || fsize > WP_MAX_TEXT * 2) {
|
||||
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] != 'W' || buf[2] != 'P' || buf[3] != '1') {
|
||||
montauk::mfree(buf);
|
||||
return;
|
||||
}
|
||||
|
||||
wp_free_runs(wp);
|
||||
|
||||
int off = 4;
|
||||
off += 2;
|
||||
wp->cur_font_id = buf[off++];
|
||||
wp->cur_size = buf[off++];
|
||||
|
||||
int run_count = buf[off] | (buf[off + 1] << 8);
|
||||
off += 2;
|
||||
|
||||
for (int i = 0; i < run_count && off < (int)fsize && i < WP_MAX_RUNS; i++) {
|
||||
if (off + 6 > (int)fsize) break;
|
||||
int text_len = buf[off] | (buf[off + 1] << 8);
|
||||
off += 2;
|
||||
uint8_t font_id = buf[off++];
|
||||
uint8_t size = buf[off++];
|
||||
uint8_t flags = buf[off++];
|
||||
off++;
|
||||
|
||||
if (off + text_len > (int)fsize) break;
|
||||
|
||||
StyledRun* r = &wp->runs[wp->run_count];
|
||||
wp_init_run(r, font_id, size, flags);
|
||||
wp_ensure_run_cap(r, text_len);
|
||||
montauk::memcpy(r->text, buf + off, text_len);
|
||||
r->len = text_len;
|
||||
off += text_len;
|
||||
|
||||
wp->total_text_len += text_len;
|
||||
wp->run_count++;
|
||||
}
|
||||
|
||||
if (wp->run_count == 0) {
|
||||
wp_init_run(&wp->runs[0], wp->cur_font_id, wp->cur_size, 0);
|
||||
wp->run_count = 1;
|
||||
}
|
||||
|
||||
wp->cursor_run = 0;
|
||||
wp->cursor_offset = 0;
|
||||
wp->scrollbar.scroll_offset = 0;
|
||||
wp->modified = false;
|
||||
wp->wrap_dirty = true;
|
||||
wp->sel_anchor = 0;
|
||||
wp->sel_end = 0;
|
||||
wp->has_selection = false;
|
||||
wp->mouse_selecting = false;
|
||||
wp->font_dropdown_open = false;
|
||||
wp->size_dropdown_open = false;
|
||||
|
||||
wp_set_filepath(wp, path);
|
||||
montauk::mfree(buf);
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
/*
|
||||
* input.cpp
|
||||
* Input handling
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "wordprocessor.hpp"
|
||||
|
||||
static void wp_open_pathbar_for_open(WordProcessorState* wp) {
|
||||
wp->show_pathbar = !wp->show_pathbar;
|
||||
wp->pathbar_save_mode = false;
|
||||
if (wp->show_pathbar) {
|
||||
montauk::strncpy(wp->pathbar_text, wp->filepath, 255);
|
||||
wp->pathbar_len = montauk::slen(wp->pathbar_text);
|
||||
wp->pathbar_cursor = wp->pathbar_len;
|
||||
}
|
||||
}
|
||||
|
||||
static void wp_commit_pathbar(WordProcessorState* wp) {
|
||||
if (!wp->pathbar_text[0]) return;
|
||||
if (wp->pathbar_save_mode) {
|
||||
wp_set_filepath(wp, wp->pathbar_text);
|
||||
wp_save_file(wp);
|
||||
} else {
|
||||
wp_load_file(wp, wp->pathbar_text);
|
||||
}
|
||||
wp->show_pathbar = false;
|
||||
}
|
||||
|
||||
static int wp_hit_test_text(WordProcessorState* wp, int local_x, int local_y, int edit_y) {
|
||||
int click_y = local_y - edit_y + wp->scrollbar.scroll_offset;
|
||||
int click_x = local_x - WP_MARGIN;
|
||||
|
||||
int target_line = wp->wrap_line_count - 1;
|
||||
for (int i = 0; i < wp->wrap_line_count; i++) {
|
||||
if (click_y >= wp->wrap_lines[i].y &&
|
||||
click_y < wp->wrap_lines[i].y + wp->wrap_lines[i].height) {
|
||||
target_line = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
WrapLine* wl = &wp->wrap_lines[target_line];
|
||||
int ri = wl->run_idx;
|
||||
int ro = wl->run_offset;
|
||||
int chars_left = wl->char_count;
|
||||
int x = 0;
|
||||
int best_abs = wp_wrap_line_start(wp, target_line);
|
||||
|
||||
while (chars_left > 0 && ri < wp->run_count) {
|
||||
StyledRun* r = &wp->runs[ri];
|
||||
TrueTypeFont* font = wp_get_font(r->font_id, r->flags);
|
||||
if (!font || !font->valid) {
|
||||
ri++;
|
||||
ro = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
GlyphCache* gc = font->get_cache(r->size);
|
||||
int avail = r->len - ro;
|
||||
int to_check = avail < chars_left ? avail : chars_left;
|
||||
|
||||
for (int ci = 0; ci < to_check; ci++) {
|
||||
char ch = r->text[ro + ci];
|
||||
CachedGlyph* g = (ch >= 32 || ch < 0) && ch != '\n'
|
||||
? font->get_glyph(gc, (unsigned char)ch) : nullptr;
|
||||
int char_w = g ? g->advance : 0;
|
||||
|
||||
if (x + char_w / 2 > click_x) return best_abs;
|
||||
x += char_w;
|
||||
best_abs++;
|
||||
}
|
||||
|
||||
chars_left -= to_check;
|
||||
ro += to_check;
|
||||
if (ro >= r->len) {
|
||||
ri++;
|
||||
ro = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return best_abs;
|
||||
}
|
||||
|
||||
void wp_handle_mouse(const Montauk::WinEvent& ev) {
|
||||
WordProcessorState* wp = &g_wp;
|
||||
int local_x = ev.mouse.x;
|
||||
int local_y = ev.mouse.y;
|
||||
int edit_y = WP_TOOLBAR_H + (wp->show_pathbar ? WP_PATHBAR_H : 0);
|
||||
int text_area_h = g_win_h - edit_y - WP_STATUS_H;
|
||||
|
||||
wp->scrollbar.handle_mouse(local_x, local_y, ev.mouse.buttons, ev.mouse.prev_buttons, ev.mouse.scroll);
|
||||
|
||||
if (wp->font_dropdown_open && wp_left_pressed(ev.mouse.buttons, ev.mouse.prev_buttons)) {
|
||||
int dx = 128;
|
||||
int dy = WP_TOOLBAR_H;
|
||||
int dh = FONT_COUNT * 26 + 4;
|
||||
if (local_x >= dx && local_x < dx + 110 && local_y >= dy && local_y < dy + dh) {
|
||||
int idx = (local_y - dy - 2) / 26;
|
||||
if (idx >= 0 && idx < FONT_COUNT) {
|
||||
if (wp->has_selection) wp_apply_style_to_selection(wp, 0, idx);
|
||||
wp->cur_font_id = (uint8_t)idx;
|
||||
}
|
||||
}
|
||||
wp->font_dropdown_open = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (wp->size_dropdown_open && wp_left_pressed(ev.mouse.buttons, ev.mouse.prev_buttons)) {
|
||||
int dx = 224;
|
||||
int dy = WP_TOOLBAR_H;
|
||||
int dh = WP_SIZE_OPTION_COUNT * 26 + 4;
|
||||
if (local_x >= dx && local_x < dx + 56 && local_y >= dy && local_y < dy + dh) {
|
||||
int idx = (local_y - dy - 2) / 26;
|
||||
if (idx >= 0 && idx < WP_SIZE_OPTION_COUNT) {
|
||||
if (wp->has_selection) wp_apply_style_to_selection(wp, 1, WP_SIZE_OPTIONS[idx]);
|
||||
wp->cur_size = (uint8_t)WP_SIZE_OPTIONS[idx];
|
||||
wp->wrap_dirty = true;
|
||||
}
|
||||
}
|
||||
wp->size_dropdown_open = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (wp_left_pressed(ev.mouse.buttons, ev.mouse.prev_buttons) && local_y < WP_TOOLBAR_H) {
|
||||
if (local_x >= 4 && local_x < 28 && local_y >= 6 && local_y < 30) {
|
||||
wp_open_pathbar_for_open(wp);
|
||||
return;
|
||||
}
|
||||
if (local_x >= 32 && local_x < 56 && local_y >= 6 && local_y < 30) {
|
||||
wp_save_file(wp);
|
||||
return;
|
||||
}
|
||||
if (local_x >= 66 && local_x < 90 && local_y >= 6 && local_y < 30) {
|
||||
if (wp->has_selection) wp_apply_style_to_selection(wp, 2, 0);
|
||||
wp->cur_flags ^= STYLE_BOLD;
|
||||
return;
|
||||
}
|
||||
if (local_x >= 94 && local_x < 118 && local_y >= 6 && local_y < 30) {
|
||||
if (wp->has_selection) wp_apply_style_to_selection(wp, 3, 0);
|
||||
wp->cur_flags ^= STYLE_ITALIC;
|
||||
return;
|
||||
}
|
||||
if (local_x >= 128 && local_x < 218 && local_y >= 6 && local_y < 30) {
|
||||
wp->font_dropdown_open = !wp->font_dropdown_open;
|
||||
wp->size_dropdown_open = false;
|
||||
return;
|
||||
}
|
||||
if (local_x >= 224 && local_x < 268 && local_y >= 6 && local_y < 30) {
|
||||
wp->size_dropdown_open = !wp->size_dropdown_open;
|
||||
wp->font_dropdown_open = false;
|
||||
return;
|
||||
}
|
||||
if (local_x >= 278 && local_x < 302 && local_y >= 6 && local_y < 30) {
|
||||
wp_insert_char(wp, (char)0xA7);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (wp->show_pathbar && local_y >= WP_TOOLBAR_H && local_y < WP_TOOLBAR_H + WP_PATHBAR_H) {
|
||||
if (wp_left_pressed(ev.mouse.buttons, ev.mouse.prev_buttons)) {
|
||||
int btn_w = 56;
|
||||
int inp_w = g_win_w - 8 - btn_w - 12;
|
||||
int btn_x = 8 + inp_w + 6;
|
||||
if (local_x >= btn_x && local_x < btn_x + btn_w) {
|
||||
wp_commit_pathbar(wp);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
wp_recompute_wrap(wp, g_win_w);
|
||||
|
||||
if (wp_left_pressed(ev.mouse.buttons, ev.mouse.prev_buttons) &&
|
||||
local_y >= edit_y && local_y < edit_y + text_area_h) {
|
||||
wp->font_dropdown_open = false;
|
||||
wp->size_dropdown_open = false;
|
||||
|
||||
int abs = wp_hit_test_text(wp, local_x, local_y, edit_y);
|
||||
wp_pos_to_run(wp, abs, &wp->cursor_run, &wp->cursor_offset);
|
||||
wp->sel_anchor = abs;
|
||||
wp->sel_end = abs;
|
||||
wp->has_selection = false;
|
||||
wp->mouse_selecting = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (wp->mouse_selecting && wp_left_held(ev.mouse.buttons) && local_y >= edit_y - 20) {
|
||||
int abs = wp_hit_test_text(wp, local_x, local_y, edit_y);
|
||||
wp->sel_end = abs;
|
||||
wp_pos_to_run(wp, abs, &wp->cursor_run, &wp->cursor_offset);
|
||||
wp->has_selection = (wp->sel_anchor != wp->sel_end);
|
||||
return;
|
||||
}
|
||||
|
||||
if (wp->mouse_selecting && wp_left_released(ev.mouse.buttons, ev.mouse.prev_buttons)) {
|
||||
wp->mouse_selecting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.mouse.scroll != 0 && local_y >= edit_y && local_y < edit_y + text_area_h) {
|
||||
wp->scrollbar.scroll_offset -= ev.mouse.scroll * 40;
|
||||
int ms = wp->scrollbar.max_scroll();
|
||||
if (wp->scrollbar.scroll_offset < 0) wp->scrollbar.scroll_offset = 0;
|
||||
if (wp->scrollbar.scroll_offset > ms) wp->scrollbar.scroll_offset = ms;
|
||||
}
|
||||
}
|
||||
|
||||
void wp_handle_key(const Montauk::KeyEvent& key) {
|
||||
WordProcessorState* wp = &g_wp;
|
||||
if (!key.pressed) return;
|
||||
|
||||
if (wp->show_pathbar) {
|
||||
if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
wp_commit_pathbar(wp);
|
||||
return;
|
||||
}
|
||||
if (key.scancode == 0x01) {
|
||||
wp->show_pathbar = false;
|
||||
return;
|
||||
}
|
||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
if (wp->pathbar_cursor > 0) {
|
||||
for (int i = wp->pathbar_cursor - 1; i < wp->pathbar_len - 1; i++)
|
||||
wp->pathbar_text[i] = wp->pathbar_text[i + 1];
|
||||
wp->pathbar_len--;
|
||||
wp->pathbar_cursor--;
|
||||
wp->pathbar_text[wp->pathbar_len] = '\0';
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (key.scancode == 0x4B) {
|
||||
if (wp->pathbar_cursor > 0) wp->pathbar_cursor--;
|
||||
return;
|
||||
}
|
||||
if (key.scancode == 0x4D) {
|
||||
if (wp->pathbar_cursor < wp->pathbar_len) wp->pathbar_cursor++;
|
||||
return;
|
||||
}
|
||||
if (key.ascii >= 32 && key.ascii < 127 && wp->pathbar_len < 254) {
|
||||
for (int i = wp->pathbar_len; i > wp->pathbar_cursor; i--)
|
||||
wp->pathbar_text[i] = wp->pathbar_text[i - 1];
|
||||
wp->pathbar_text[wp->pathbar_cursor] = key.ascii;
|
||||
wp->pathbar_cursor++;
|
||||
wp->pathbar_len++;
|
||||
wp->pathbar_text[wp->pathbar_len] = '\0';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
wp->font_dropdown_open = false;
|
||||
wp->size_dropdown_open = false;
|
||||
wp_recompute_wrap(wp, g_win_w);
|
||||
|
||||
if (key.ctrl && (key.ascii == 's' || key.ascii == 'S')) {
|
||||
wp_save_file(wp);
|
||||
return;
|
||||
}
|
||||
if (key.ctrl && (key.ascii == 'o' || key.ascii == 'O')) {
|
||||
wp_open_pathbar_for_open(wp);
|
||||
return;
|
||||
}
|
||||
if (key.ctrl && (key.ascii == 'b' || key.ascii == 'B')) {
|
||||
if (wp->has_selection) wp_apply_style_to_selection(wp, 2, 0);
|
||||
wp->cur_flags ^= STYLE_BOLD;
|
||||
return;
|
||||
}
|
||||
if (key.ctrl && (key.ascii == 'i' || key.ascii == 'I')) {
|
||||
if (wp->has_selection) wp_apply_style_to_selection(wp, 3, 0);
|
||||
wp->cur_flags ^= STYLE_ITALIC;
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.scancode == 0x48) {
|
||||
if (key.shift) wp_start_selection(wp);
|
||||
else wp_clear_selection(wp);
|
||||
wp_cursor_up(wp);
|
||||
if (key.shift) wp_update_selection_to_cursor(wp);
|
||||
return;
|
||||
}
|
||||
if (key.scancode == 0x50) {
|
||||
if (key.shift) wp_start_selection(wp);
|
||||
else wp_clear_selection(wp);
|
||||
wp_cursor_down(wp);
|
||||
if (key.shift) wp_update_selection_to_cursor(wp);
|
||||
return;
|
||||
}
|
||||
if (key.scancode == 0x4B) {
|
||||
if (key.shift) wp_start_selection(wp);
|
||||
else if (wp->has_selection) {
|
||||
int s, e;
|
||||
wp_sel_range(wp, &s, &e);
|
||||
wp_pos_to_run(wp, s, &wp->cursor_run, &wp->cursor_offset);
|
||||
wp_clear_selection(wp);
|
||||
return;
|
||||
}
|
||||
wp_cursor_left(wp);
|
||||
if (key.shift) wp_update_selection_to_cursor(wp);
|
||||
return;
|
||||
}
|
||||
if (key.scancode == 0x4D) {
|
||||
if (key.shift) wp_start_selection(wp);
|
||||
else if (wp->has_selection) {
|
||||
int s, e;
|
||||
wp_sel_range(wp, &s, &e);
|
||||
wp_pos_to_run(wp, e, &wp->cursor_run, &wp->cursor_offset);
|
||||
wp_clear_selection(wp);
|
||||
return;
|
||||
}
|
||||
wp_cursor_right(wp);
|
||||
if (key.shift) wp_update_selection_to_cursor(wp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.scancode == 0x47) {
|
||||
if (key.shift) wp_start_selection(wp);
|
||||
else wp_clear_selection(wp);
|
||||
int abs = wp_abs_pos(wp, wp->cursor_run, wp->cursor_offset);
|
||||
int line = wp_find_wrap_line(wp, abs);
|
||||
int start = wp_wrap_line_start(wp, line);
|
||||
wp_pos_to_run(wp, start, &wp->cursor_run, &wp->cursor_offset);
|
||||
if (key.shift) wp_update_selection_to_cursor(wp);
|
||||
return;
|
||||
}
|
||||
if (key.scancode == 0x4F) {
|
||||
if (key.shift) wp_start_selection(wp);
|
||||
else wp_clear_selection(wp);
|
||||
int abs = wp_abs_pos(wp, wp->cursor_run, wp->cursor_offset);
|
||||
int line = wp_find_wrap_line(wp, abs);
|
||||
int start = wp_wrap_line_start(wp, line);
|
||||
int end = start + wp->wrap_lines[line].char_count;
|
||||
if (end > start) {
|
||||
char ch = wp_char_at(wp, end - 1);
|
||||
if (ch == '\n') end--;
|
||||
}
|
||||
wp_pos_to_run(wp, end, &wp->cursor_run, &wp->cursor_offset);
|
||||
if (key.shift) wp_update_selection_to_cursor(wp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.ctrl && (key.ascii == 'a' || key.ascii == 'A')) {
|
||||
wp->sel_anchor = 0;
|
||||
wp->sel_end = wp->total_text_len;
|
||||
wp->has_selection = (wp->total_text_len > 0);
|
||||
wp_pos_to_run(wp, wp->total_text_len, &wp->cursor_run, &wp->cursor_offset);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.scancode == 0x53) {
|
||||
if (wp->has_selection) wp_delete_selection(wp);
|
||||
else wp_delete_char(wp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
if (wp->has_selection) wp_delete_selection(wp);
|
||||
else wp_backspace(wp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
if (wp->has_selection) wp_delete_selection(wp);
|
||||
wp_insert_char(wp, '\n');
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.ascii == '\t') {
|
||||
if (wp->has_selection) wp_delete_selection(wp);
|
||||
for (int i = 0; i < 4; i++) wp_insert_char(wp, ' ');
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.ascii >= 32 && key.ascii < 127) {
|
||||
if (wp->has_selection) wp_delete_selection(wp);
|
||||
wp_insert_char(wp, key.ascii);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* main.cpp
|
||||
* MontaukOS Word Processor
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "wordprocessor.hpp"
|
||||
|
||||
int g_win_w = INIT_W;
|
||||
int g_win_h = INIT_H;
|
||||
WsWindow g_win;
|
||||
WordProcessorState g_wp = {};
|
||||
WPFontTable g_wp_fonts = { {{nullptr}}, false };
|
||||
SvgIcon g_icon_folder = {};
|
||||
SvgIcon g_icon_save = {};
|
||||
TrueTypeFont* g_ui_font = nullptr;
|
||||
TrueTypeFont* g_ui_bold = nullptr;
|
||||
|
||||
void wp_load_icons() {
|
||||
Color def_color = colors::ICON_COLOR;
|
||||
if (!g_icon_folder.pixels)
|
||||
g_icon_folder = svg_load("0:/icons/folder.svg", 16, 16, def_color);
|
||||
if (!g_icon_save.pixels)
|
||||
g_icon_save = svg_load("0:/icons/document-save-symbolic.svg", 16, 16, def_color);
|
||||
}
|
||||
|
||||
void wp_cleanup_state() {
|
||||
wp_free_document(&g_wp);
|
||||
if (g_icon_folder.pixels) svg_free(g_icon_folder);
|
||||
if (g_icon_save.pixels) svg_free(g_icon_save);
|
||||
}
|
||||
|
||||
extern "C" void _start() {
|
||||
if (!fonts::init())
|
||||
montauk::exit(1);
|
||||
|
||||
wp_load_fonts();
|
||||
wp_load_icons();
|
||||
wp_init_empty_document(&g_wp);
|
||||
|
||||
char args[512] = {};
|
||||
int arglen = montauk::getargs(args, sizeof(args));
|
||||
if (arglen > 0 && args[0]) {
|
||||
wp_load_file(&g_wp, args);
|
||||
}
|
||||
|
||||
char title[64] = "Word Processor";
|
||||
if (g_wp.filename[0]) {
|
||||
snprintf(title, sizeof(title), "%s - Word Processor", g_wp.filename);
|
||||
}
|
||||
|
||||
if (!g_win.create(title, INIT_W, INIT_H))
|
||||
montauk::exit(1);
|
||||
|
||||
g_win_w = g_win.width;
|
||||
g_win_h = g_win.height;
|
||||
|
||||
wp_render();
|
||||
g_win.present();
|
||||
|
||||
while (true) {
|
||||
Montauk::WinEvent ev;
|
||||
int r = g_win.poll(&ev);
|
||||
|
||||
if (r < 0) break;
|
||||
|
||||
if (r == 0) {
|
||||
montauk::sleep_ms(16);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ev.type == 3) break;
|
||||
|
||||
if (ev.type == 2 || ev.type == 4) {
|
||||
g_win_w = g_win.width;
|
||||
g_win_h = g_win.height;
|
||||
wp_render();
|
||||
g_win.present();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ev.type == 0 && ev.key.pressed) {
|
||||
wp_handle_key(ev.key);
|
||||
wp_render();
|
||||
g_win.present();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ev.type == 1) {
|
||||
wp_handle_mouse(ev);
|
||||
wp_render();
|
||||
g_win.present();
|
||||
}
|
||||
}
|
||||
|
||||
wp_cleanup_state();
|
||||
g_win.destroy();
|
||||
montauk::exit(0);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[app]
|
||||
name = "Word Processor"
|
||||
binary = "wordprocessor.elf"
|
||||
icon = "wordprocessor.svg"
|
||||
|
||||
[menu]
|
||||
category = "Applications"
|
||||
visible = false
|
||||
@@ -0,0 +1,290 @@
|
||||
/*
|
||||
* render.cpp
|
||||
* Rendering for the standalone MontaukOS Word Processor
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "wordprocessor.hpp"
|
||||
|
||||
static int wp_ui_line_height() {
|
||||
if (g_ui_font && g_ui_font->valid)
|
||||
return g_ui_font->get_line_height(fonts::UI_SIZE);
|
||||
return fonts::UI_SIZE;
|
||||
}
|
||||
|
||||
static void wp_draw_ui_text(Canvas& c, int x, int y, const char* text, Color color) {
|
||||
draw_text(c, g_ui_font, x, y, text, color, fonts::UI_SIZE);
|
||||
}
|
||||
|
||||
static void wp_draw_ui_button(Canvas& c, int x, int y, int w, int h,
|
||||
const char* label, Color bg, Color fg, int radius = 3) {
|
||||
c.fill_rounded_rect(x, y, w, h, radius, bg);
|
||||
int tw = text_width(g_ui_font, label, fonts::UI_SIZE);
|
||||
int th = wp_ui_line_height();
|
||||
draw_text(c, g_ui_font, x + (w - tw) / 2, y + (h - th) / 2, label, fg, fonts::UI_SIZE);
|
||||
}
|
||||
|
||||
void wp_render() {
|
||||
if (!g_win.pixels) return;
|
||||
|
||||
WordProcessorState* wp = &g_wp;
|
||||
Canvas c = g_win.canvas();
|
||||
c.fill(colors::WINDOW_BG);
|
||||
|
||||
int sfh = wp_ui_line_height();
|
||||
Color toolbar_bg = Color::from_rgb(0xF5, 0xF5, 0xF5);
|
||||
Color btn_bg = Color::from_rgb(0xE8, 0xE8, 0xE8);
|
||||
Color btn_active = Color::from_rgb(0xC0, 0xD0, 0xE8);
|
||||
|
||||
c.fill_rect(0, 0, c.w, WP_TOOLBAR_H, toolbar_bg);
|
||||
|
||||
c.fill_rounded_rect(4, 6, 24, 24, 3, btn_bg);
|
||||
if (g_icon_folder.pixels)
|
||||
c.icon(8, 10, g_icon_folder);
|
||||
|
||||
c.fill_rounded_rect(32, 6, 24, 24, 3, btn_bg);
|
||||
if (g_icon_save.pixels)
|
||||
c.icon(36, 10, g_icon_save);
|
||||
|
||||
c.vline(60, 4, 28, colors::BORDER);
|
||||
|
||||
Color bold_bg = (wp->cur_flags & STYLE_BOLD) ? btn_active : btn_bg;
|
||||
c.fill_rounded_rect(66, 6, 24, 24, 3, bold_bg);
|
||||
draw_text(c, g_ui_bold ? g_ui_bold : g_ui_font, 73, 8, "B", colors::TEXT_COLOR, fonts::UI_SIZE);
|
||||
|
||||
Color italic_bg = (wp->cur_flags & STYLE_ITALIC) ? btn_active : btn_bg;
|
||||
c.fill_rounded_rect(94, 6, 24, 24, 3, italic_bg);
|
||||
{
|
||||
TrueTypeFont* italic_font = wp_get_font(FONT_ROBOTO, STYLE_ITALIC);
|
||||
draw_text(c, italic_font ? italic_font : g_ui_font, 103, 8, "I", colors::TEXT_COLOR, fonts::UI_SIZE);
|
||||
}
|
||||
|
||||
c.vline(122, 4, 28, colors::BORDER);
|
||||
|
||||
c.fill_rounded_rect(128, 6, 90, 24, 3, btn_bg);
|
||||
wp_draw_ui_text(c, 134, (WP_TOOLBAR_H - sfh) / 2,
|
||||
WP_FONT_NAMES[wp->cur_font_id < FONT_COUNT ? wp->cur_font_id : 0],
|
||||
colors::TEXT_COLOR);
|
||||
|
||||
c.fill_rounded_rect(224, 6, 44, 24, 3, btn_bg);
|
||||
{
|
||||
char sz[8];
|
||||
snprintf(sz, sizeof(sz), "%d", (int)wp->cur_size);
|
||||
wp_draw_ui_text(c, 232, (WP_TOOLBAR_H - sfh) / 2, sz, colors::TEXT_COLOR);
|
||||
}
|
||||
|
||||
c.vline(272, 4, 28, colors::BORDER);
|
||||
|
||||
c.fill_rounded_rect(278, 6, 24, 24, 3, btn_bg);
|
||||
{
|
||||
char section[2] = { (char)0xA7, '\0' };
|
||||
TrueTypeFont* font = wp_get_font(FONT_ROBOTO, 0);
|
||||
draw_text(c, font ? font : g_ui_font, 284, 8, section, colors::TEXT_COLOR, fonts::UI_SIZE);
|
||||
}
|
||||
|
||||
{
|
||||
char label[128];
|
||||
if (wp->filename[0])
|
||||
snprintf(label, sizeof(label), "%s%s", wp->filename, wp->modified ? " *" : "");
|
||||
else
|
||||
snprintf(label, sizeof(label), "Untitled%s", wp->modified ? " *" : "");
|
||||
wp_draw_ui_text(c, 312, (WP_TOOLBAR_H - sfh) / 2, label, Color::from_rgb(0x88, 0x88, 0x88));
|
||||
}
|
||||
|
||||
c.hline(0, WP_TOOLBAR_H - 1, c.w, colors::BORDER);
|
||||
|
||||
int edit_y = WP_TOOLBAR_H;
|
||||
if (wp->show_pathbar) {
|
||||
int pb_y = WP_TOOLBAR_H;
|
||||
c.fill_rect(0, pb_y, c.w, WP_PATHBAR_H, Color::from_rgb(0xF0, 0xF0, 0xF0));
|
||||
|
||||
int inp_x = 8;
|
||||
int inp_y = pb_y + 4;
|
||||
int btn_w = 56;
|
||||
int inp_w = c.w - inp_x - btn_w - 12;
|
||||
int inp_h = 24;
|
||||
|
||||
c.fill_rect(inp_x, inp_y, inp_w, inp_h, colors::WHITE);
|
||||
c.rect(inp_x, inp_y, inp_w, inp_h, colors::ACCENT);
|
||||
|
||||
int text_y = inp_y + (inp_h - sfh) / 2;
|
||||
wp_draw_ui_text(c, inp_x + 4, text_y, wp->pathbar_text, colors::TEXT_COLOR);
|
||||
|
||||
char prefix[256];
|
||||
int plen = wp->pathbar_cursor;
|
||||
if (plen > 255) plen = 255;
|
||||
for (int i = 0; i < plen; i++) prefix[i] = wp->pathbar_text[i];
|
||||
prefix[plen] = '\0';
|
||||
int cx = inp_x + 4 + text_width(g_ui_font, prefix, fonts::UI_SIZE);
|
||||
c.fill_rect(cx, inp_y + 3, 2, inp_h - 6, colors::ACCENT);
|
||||
|
||||
int btn_x = inp_x + inp_w + 6;
|
||||
wp_draw_ui_button(c, btn_x, inp_y, btn_w, inp_h,
|
||||
wp->pathbar_save_mode ? "Save" : "Open",
|
||||
colors::ACCENT, colors::WHITE);
|
||||
|
||||
c.hline(0, pb_y + WP_PATHBAR_H - 1, c.w, colors::BORDER);
|
||||
edit_y = WP_TOOLBAR_H + WP_PATHBAR_H;
|
||||
}
|
||||
|
||||
int text_area_h = c.h - edit_y - WP_STATUS_H;
|
||||
wp_recompute_wrap(wp, c.w);
|
||||
|
||||
wp->scrollbar.bounds = {c.w - WP_SCROLLBAR_W, edit_y, WP_SCROLLBAR_W, text_area_h};
|
||||
wp->scrollbar.content_height = wp->content_height;
|
||||
wp->scrollbar.view_height = text_area_h;
|
||||
|
||||
{
|
||||
int ms = wp->scrollbar.max_scroll();
|
||||
if (wp->scrollbar.scroll_offset > ms) wp->scrollbar.scroll_offset = ms;
|
||||
if (wp->scrollbar.scroll_offset < 0) wp->scrollbar.scroll_offset = 0;
|
||||
}
|
||||
|
||||
wp_ensure_cursor_visible(wp, text_area_h);
|
||||
|
||||
int scroll_y = wp->scrollbar.scroll_offset;
|
||||
int cursor_abs = wp_abs_pos(wp, wp->cursor_run, wp->cursor_offset);
|
||||
int sel_s = 0;
|
||||
int sel_e = 0;
|
||||
if (wp->has_selection) wp_sel_range(wp, &sel_s, &sel_e);
|
||||
Color sel_bg = Color::from_rgb(0xB0, 0xD0, 0xF0);
|
||||
|
||||
for (int li = 0; li < wp->wrap_line_count; li++) {
|
||||
WrapLine* wl = &wp->wrap_lines[li];
|
||||
int py = edit_y + wl->y - scroll_y;
|
||||
|
||||
if (py + wl->height <= edit_y) continue;
|
||||
if (py >= edit_y + text_area_h) break;
|
||||
|
||||
int chars_left = wl->char_count;
|
||||
int ri = wl->run_idx;
|
||||
int ro = wl->run_offset;
|
||||
int x = WP_MARGIN;
|
||||
int line_abs_start = wp_wrap_line_start(wp, li);
|
||||
int char_idx = 0;
|
||||
|
||||
while (chars_left > 0 && ri < wp->run_count) {
|
||||
StyledRun* r = &wp->runs[ri];
|
||||
TrueTypeFont* font = wp_get_font(r->font_id, r->flags);
|
||||
if (!font || !font->valid) {
|
||||
ri++;
|
||||
ro = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
GlyphCache* gc = font->get_cache(r->size);
|
||||
int baseline = py + wl->baseline;
|
||||
|
||||
int avail = r->len - ro;
|
||||
int to_draw = avail < chars_left ? avail : chars_left;
|
||||
|
||||
for (int ci = 0; ci < to_draw; ci++) {
|
||||
char ch = r->text[ro + ci];
|
||||
int abs_ch = line_abs_start + char_idx;
|
||||
|
||||
int char_adv = 0;
|
||||
if (ch != '\n' && (ch >= 32 || ch < 0)) {
|
||||
CachedGlyph* g = font->get_glyph(gc, (unsigned char)ch);
|
||||
char_adv = g ? g->advance : 8;
|
||||
}
|
||||
|
||||
if (wp->has_selection && abs_ch >= sel_s && abs_ch < sel_e) {
|
||||
int sel_w = char_adv > 0 ? char_adv : 6;
|
||||
c.fill_rect(x, py, sel_w, wl->height, sel_bg);
|
||||
}
|
||||
|
||||
if (abs_ch == cursor_abs) {
|
||||
int cur_h = wl->height;
|
||||
if (py >= edit_y && py + cur_h <= edit_y + text_area_h)
|
||||
c.fill_rect(x, py, 2, cur_h, colors::ACCENT);
|
||||
}
|
||||
|
||||
if (ch != '\n' && (ch >= 32 || ch < 0)) {
|
||||
if (x < c.w - WP_SCROLLBAR_W - WP_MARGIN) {
|
||||
Color text_col = (wp->has_selection && abs_ch >= sel_s && abs_ch < sel_e)
|
||||
? Color::from_rgb(0x10, 0x10, 0x10) : colors::TEXT_COLOR;
|
||||
int adv = font->draw_char_to_buffer(
|
||||
c.pixels, c.w, c.h, x, baseline, (unsigned char)ch, text_col, gc);
|
||||
x += adv;
|
||||
}
|
||||
}
|
||||
|
||||
char_idx++;
|
||||
}
|
||||
|
||||
chars_left -= to_draw;
|
||||
ro += to_draw;
|
||||
if (ro >= r->len) {
|
||||
ri++;
|
||||
ro = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (line_abs_start + char_idx == cursor_abs && li == wp->wrap_line_count - 1) {
|
||||
int cur_h = wl->height;
|
||||
if (py >= edit_y && py + cur_h <= edit_y + text_area_h)
|
||||
c.fill_rect(x, py, 2, cur_h, colors::ACCENT);
|
||||
}
|
||||
}
|
||||
|
||||
if (wp->scrollbar.content_height > wp->scrollbar.view_height) {
|
||||
Color sb_fg = (wp->scrollbar.hovered || wp->scrollbar.dragging)
|
||||
? wp->scrollbar.hover_fg : wp->scrollbar.fg;
|
||||
int sbx = wp->scrollbar.bounds.x;
|
||||
int sby = wp->scrollbar.bounds.y;
|
||||
int sbw = wp->scrollbar.bounds.w;
|
||||
int sbh = wp->scrollbar.bounds.h;
|
||||
c.fill_rect(sbx, sby, sbw, sbh, colors::SCROLLBAR_BG);
|
||||
int th = wp->scrollbar.thumb_height();
|
||||
int tty = wp->scrollbar.thumb_y();
|
||||
c.fill_rect(sbx + 1, tty, sbw - 2, th, sb_fg);
|
||||
}
|
||||
|
||||
int status_y = c.h - WP_STATUS_H;
|
||||
c.fill_rect(0, status_y, c.w, WP_STATUS_H, Color::from_rgb(0x2B, 0x3E, 0x50));
|
||||
int status_text_y = status_y + (WP_STATUS_H - sfh) / 2;
|
||||
|
||||
char status_left[128];
|
||||
snprintf(status_left, sizeof(status_left), " %s %dpt %s%s",
|
||||
WP_FONT_NAMES[wp->cur_font_id < FONT_COUNT ? wp->cur_font_id : 0], (int)wp->cur_size,
|
||||
(wp->cur_flags & STYLE_BOLD) ? "Bold " : "",
|
||||
(wp->cur_flags & STYLE_ITALIC) ? "Italic" : "");
|
||||
wp_draw_ui_text(c, 4, status_text_y, status_left, colors::PANEL_TEXT);
|
||||
|
||||
char status_right[32];
|
||||
snprintf(status_right, sizeof(status_right), "%d chars ", wp->total_text_len);
|
||||
int sr_w = text_width(g_ui_font, status_right, fonts::UI_SIZE);
|
||||
wp_draw_ui_text(c, c.w - sr_w - 4, status_text_y, status_right, colors::PANEL_TEXT);
|
||||
|
||||
if (wp->font_dropdown_open) {
|
||||
int dx = 128;
|
||||
int dy = WP_TOOLBAR_H;
|
||||
int dw = 110;
|
||||
int dh = FONT_COUNT * 26 + 4;
|
||||
c.fill_rect(dx, dy, dw, dh, colors::MENU_BG);
|
||||
c.rect(dx, dy, dw, dh, colors::BORDER);
|
||||
for (int i = 0; i < FONT_COUNT; i++) {
|
||||
int iy = dy + 2 + i * 26;
|
||||
if (i == wp->cur_font_id)
|
||||
c.fill_rect(dx + 2, iy, dw - 4, 24, colors::MENU_HOVER);
|
||||
wp_draw_ui_text(c, dx + 8, iy + (24 - sfh) / 2, WP_FONT_NAMES[i], colors::TEXT_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
if (wp->size_dropdown_open) {
|
||||
int dx = 224;
|
||||
int dy = WP_TOOLBAR_H;
|
||||
int dw = 56;
|
||||
int dh = WP_SIZE_OPTION_COUNT * 26 + 4;
|
||||
c.fill_rect(dx, dy, dw, dh, colors::MENU_BG);
|
||||
c.rect(dx, dy, dw, dh, colors::BORDER);
|
||||
for (int i = 0; i < WP_SIZE_OPTION_COUNT; i++) {
|
||||
int iy = dy + 2 + i * 26;
|
||||
if (WP_SIZE_OPTIONS[i] == wp->cur_size)
|
||||
c.fill_rect(dx + 2, iy, dw - 4, 24, colors::MENU_HOVER);
|
||||
char sz[8];
|
||||
snprintf(sz, sizeof(sz), "%d", WP_SIZE_OPTIONS[i]);
|
||||
wp_draw_ui_text(c, dx + 8, iy + (24 - sfh) / 2, sz, colors::TEXT_COLOR);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* stb_truetype_impl.cpp
|
||||
* Single compilation unit for stb_truetype in MontaukOS freestanding environment
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/string.h>
|
||||
#include <gui/stb_math.h>
|
||||
|
||||
#define STBTT_ifloor(x) ((int) stb_floor(x))
|
||||
#define STBTT_iceil(x) ((int) stb_ceil(x))
|
||||
#define STBTT_sqrt(x) stb_sqrt(x)
|
||||
#define STBTT_pow(x,y) stb_pow(x,y)
|
||||
#define STBTT_fmod(x,y) stb_fmod(x,y)
|
||||
#define STBTT_cos(x) stb_cos(x)
|
||||
#define STBTT_acos(x) stb_acos(x)
|
||||
#define STBTT_fabs(x) stb_fabs(x)
|
||||
|
||||
#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x))
|
||||
#define STBTT_free(x,u) ((void)(u), montauk::mfree(x))
|
||||
|
||||
#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n)
|
||||
#define STBTT_memset(d,v,n) montauk::memset(d,v,n)
|
||||
|
||||
#define STBTT_strlen(x) montauk::slen(x)
|
||||
|
||||
#define STBTT_assert(x) ((void)(x))
|
||||
|
||||
#define STB_TRUETYPE_IMPLEMENTATION
|
||||
#include <gui/stb_truetype.h>
|
||||
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
* wordprocessor.hpp
|
||||
* Shared declarations for the Word Processor 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/standalone.hpp>
|
||||
#include <gui/truetype.hpp>
|
||||
#include <gui/svg.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
}
|
||||
|
||||
using namespace gui;
|
||||
|
||||
static constexpr int INIT_W = 640;
|
||||
static constexpr int INIT_H = 480;
|
||||
static constexpr int WP_TOOLBAR_H = 36;
|
||||
static constexpr int WP_PATHBAR_H = 32;
|
||||
static constexpr int WP_STATUS_H = 24;
|
||||
static constexpr int WP_SCROLLBAR_W = 12;
|
||||
static constexpr int WP_MARGIN = 16;
|
||||
static constexpr int WP_MAX_RUNS = 1024;
|
||||
static constexpr int WP_MAX_TEXT = 262144;
|
||||
static constexpr int WP_MAX_WRAP_LINES = 4096;
|
||||
static constexpr int WP_DEFAULT_SIZE = 18;
|
||||
|
||||
static constexpr int FONT_ROBOTO = 0;
|
||||
static constexpr int FONT_NOTOSERIF = 1;
|
||||
static constexpr int FONT_C059 = 2;
|
||||
static constexpr int FONT_COUNT = 3;
|
||||
|
||||
static constexpr uint8_t STYLE_BOLD = 0x01;
|
||||
static constexpr uint8_t STYLE_ITALIC = 0x02;
|
||||
|
||||
inline constexpr const char* WP_FONT_NAMES[FONT_COUNT] = {
|
||||
"Roboto",
|
||||
"NotoSerif",
|
||||
"C059",
|
||||
};
|
||||
|
||||
inline constexpr int WP_SIZE_OPTIONS[] = { 12, 14, 16, 18, 20, 24, 28, 36 };
|
||||
static constexpr int WP_SIZE_OPTION_COUNT = 8;
|
||||
|
||||
struct WPFontTable {
|
||||
TrueTypeFont* fonts[FONT_COUNT][4];
|
||||
bool loaded;
|
||||
};
|
||||
|
||||
struct StyledRun {
|
||||
char* text;
|
||||
int len;
|
||||
int cap;
|
||||
uint8_t font_id;
|
||||
uint8_t size;
|
||||
uint8_t flags;
|
||||
};
|
||||
|
||||
struct WrapLine {
|
||||
int run_idx;
|
||||
int run_offset;
|
||||
int char_count;
|
||||
int y;
|
||||
int height;
|
||||
int baseline;
|
||||
};
|
||||
|
||||
inline bool wp_left_held(uint8_t buttons) {
|
||||
return (buttons & 0x01) != 0;
|
||||
}
|
||||
|
||||
inline bool wp_left_pressed(uint8_t buttons, uint8_t prev_buttons) {
|
||||
return (buttons & 0x01) && !(prev_buttons & 0x01);
|
||||
}
|
||||
|
||||
inline bool wp_left_released(uint8_t buttons, uint8_t prev_buttons) {
|
||||
return !(buttons & 0x01) && (prev_buttons & 0x01);
|
||||
}
|
||||
|
||||
struct WpScrollbar {
|
||||
Rect bounds;
|
||||
int content_height;
|
||||
int view_height;
|
||||
int scroll_offset;
|
||||
bool dragging;
|
||||
int drag_start_y;
|
||||
int drag_start_offset;
|
||||
Color bg;
|
||||
Color fg;
|
||||
Color hover_fg;
|
||||
bool hovered;
|
||||
|
||||
void init(int x, int y, int w, int h) {
|
||||
bounds = {x, y, w, h};
|
||||
content_height = 0;
|
||||
view_height = h;
|
||||
scroll_offset = 0;
|
||||
dragging = false;
|
||||
drag_start_y = 0;
|
||||
drag_start_offset = 0;
|
||||
bg = colors::SCROLLBAR_BG;
|
||||
fg = colors::SCROLLBAR_FG;
|
||||
hover_fg = Color::from_rgb(0xA0, 0xA0, 0xA0);
|
||||
hovered = false;
|
||||
}
|
||||
|
||||
int thumb_height() const {
|
||||
if (content_height <= view_height) return bounds.h;
|
||||
int th = (view_height * bounds.h) / content_height;
|
||||
return th < 20 ? 20 : th;
|
||||
}
|
||||
|
||||
int thumb_y() const {
|
||||
if (content_height <= view_height) return bounds.y;
|
||||
int range = bounds.h - thumb_height();
|
||||
int max_scroll = content_height - view_height;
|
||||
if (max_scroll <= 0) return bounds.y;
|
||||
return bounds.y + (scroll_offset * range) / max_scroll;
|
||||
}
|
||||
|
||||
int max_scroll() const {
|
||||
int ms = content_height - view_height;
|
||||
return ms > 0 ? ms : 0;
|
||||
}
|
||||
|
||||
void handle_mouse(int x, int y, uint8_t buttons, uint8_t prev_buttons, int scroll) {
|
||||
if (content_height <= view_height) return;
|
||||
|
||||
Rect thumb_rect = {bounds.x, thumb_y(), bounds.w, thumb_height()};
|
||||
hovered = thumb_rect.contains(x, y);
|
||||
|
||||
if (hovered && wp_left_pressed(buttons, prev_buttons)) {
|
||||
dragging = true;
|
||||
drag_start_y = y;
|
||||
drag_start_offset = scroll_offset;
|
||||
}
|
||||
|
||||
if (dragging && wp_left_held(buttons)) {
|
||||
int dy = y - drag_start_y;
|
||||
int range = bounds.h - thumb_height();
|
||||
if (range > 0) {
|
||||
int ms = max_scroll();
|
||||
scroll_offset = drag_start_offset + (dy * ms) / range;
|
||||
if (scroll_offset < 0) scroll_offset = 0;
|
||||
if (scroll_offset > ms) scroll_offset = ms;
|
||||
}
|
||||
}
|
||||
|
||||
if (!wp_left_held(buttons)) {
|
||||
dragging = false;
|
||||
}
|
||||
|
||||
if (bounds.contains(x, y) && scroll != 0) {
|
||||
scroll_offset += scroll * 20;
|
||||
int ms = max_scroll();
|
||||
if (scroll_offset < 0) scroll_offset = 0;
|
||||
if (scroll_offset > ms) scroll_offset = ms;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct WordProcessorState {
|
||||
StyledRun runs[WP_MAX_RUNS];
|
||||
int run_count;
|
||||
int total_text_len;
|
||||
|
||||
int cursor_run;
|
||||
int cursor_offset;
|
||||
|
||||
int sel_anchor;
|
||||
int sel_end;
|
||||
bool has_selection;
|
||||
bool mouse_selecting;
|
||||
|
||||
uint8_t cur_font_id;
|
||||
uint8_t cur_size;
|
||||
uint8_t cur_flags;
|
||||
|
||||
WpScrollbar scrollbar;
|
||||
int content_height;
|
||||
|
||||
WrapLine* wrap_lines;
|
||||
int wrap_line_count;
|
||||
bool wrap_dirty;
|
||||
int last_wrap_width;
|
||||
|
||||
bool modified;
|
||||
char filepath[256];
|
||||
char filename[64];
|
||||
|
||||
bool show_pathbar;
|
||||
bool pathbar_save_mode;
|
||||
char pathbar_text[256];
|
||||
int pathbar_cursor;
|
||||
int pathbar_len;
|
||||
|
||||
bool font_dropdown_open;
|
||||
bool size_dropdown_open;
|
||||
};
|
||||
|
||||
extern int g_win_w;
|
||||
extern int g_win_h;
|
||||
extern WsWindow g_win;
|
||||
extern WordProcessorState g_wp;
|
||||
extern WPFontTable g_wp_fonts;
|
||||
extern SvgIcon g_icon_folder;
|
||||
extern SvgIcon g_icon_save;
|
||||
extern TrueTypeFont* g_ui_font;
|
||||
extern TrueTypeFont* g_ui_bold;
|
||||
|
||||
void wp_load_fonts();
|
||||
void wp_load_icons();
|
||||
void wp_init_empty_document(WordProcessorState* wp);
|
||||
void wp_free_document(WordProcessorState* wp);
|
||||
void wp_cleanup_state();
|
||||
|
||||
TrueTypeFont* wp_get_font(int font_id, uint8_t flags);
|
||||
|
||||
int wp_abs_pos(WordProcessorState* wp, int run, int offset);
|
||||
void wp_pos_to_run(WordProcessorState* wp, int abs_pos, int* out_run, int* out_offset);
|
||||
char wp_char_at(WordProcessorState* wp, int abs_pos);
|
||||
|
||||
void wp_insert_char(WordProcessorState* wp, char c);
|
||||
void wp_delete_char(WordProcessorState* wp);
|
||||
void wp_backspace(WordProcessorState* wp);
|
||||
|
||||
void wp_cursor_left(WordProcessorState* wp);
|
||||
void wp_cursor_right(WordProcessorState* wp);
|
||||
void wp_cursor_up(WordProcessorState* wp);
|
||||
void wp_cursor_down(WordProcessorState* wp);
|
||||
|
||||
void wp_clear_selection(WordProcessorState* wp);
|
||||
void wp_sel_range(WordProcessorState* wp, int* out_start, int* out_end);
|
||||
void wp_start_selection(WordProcessorState* wp);
|
||||
void wp_update_selection_to_cursor(WordProcessorState* wp);
|
||||
void wp_apply_style_to_selection(WordProcessorState* wp, int mode, int value);
|
||||
void wp_delete_selection(WordProcessorState* wp);
|
||||
|
||||
void wp_recompute_wrap(WordProcessorState* wp, int content_w);
|
||||
int wp_find_wrap_line(WordProcessorState* wp, int abs_pos);
|
||||
int wp_wrap_line_start(WordProcessorState* wp, int line_idx);
|
||||
void wp_ensure_cursor_visible(WordProcessorState* wp, int view_h);
|
||||
|
||||
void wp_set_filepath(WordProcessorState* wp, const char* path);
|
||||
void wp_open_save_pathbar(WordProcessorState* wp);
|
||||
void wp_save_file(WordProcessorState* wp);
|
||||
void wp_load_file(WordProcessorState* wp, const char* path);
|
||||
|
||||
void wp_render();
|
||||
void wp_handle_mouse(const Montauk::WinEvent& ev);
|
||||
void wp_handle_key(const Montauk::KeyEvent& key);
|
||||
Reference in New Issue
Block a user