1876 lines
59 KiB
C++
1876 lines
59 KiB
C++
/*
|
|
* document.cpp
|
|
* Document model, wrap layout, font loading, and file I/O
|
|
* Copyright (c) 2026 Daniel Hammer
|
|
*/
|
|
|
|
#include "wordprocessor.hpp"
|
|
#include <gui/dialogs.hpp>
|
|
|
|
static inline int wp_min_int(int a, int b) {
|
|
return a < b ? a : b;
|
|
}
|
|
|
|
static bool wp_valid_divider_type(uint8_t divider_type) {
|
|
return divider_type >= PARA_DIVIDER_SINGLE && divider_type <= PARA_DIVIDER_THICK_THIN;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
void wp_init_paragraph_style(ParagraphStyle* para) {
|
|
para->align = PARA_ALIGN_LEFT;
|
|
para->list_type = PARA_LIST_NONE;
|
|
para->line_spacing = 100;
|
|
para->divider_type = PARA_DIVIDER_NONE;
|
|
para->left_indent = 0;
|
|
para->first_line_indent = 0;
|
|
para->space_before = 0;
|
|
para->space_after = 0;
|
|
}
|
|
|
|
static ParagraphStyle wp_make_divider_style(const ParagraphStyle* src, uint8_t divider_type) {
|
|
ParagraphStyle style = *src;
|
|
style.align = PARA_ALIGN_LEFT;
|
|
style.list_type = PARA_LIST_NONE;
|
|
style.divider_type = divider_type;
|
|
style.left_indent = 0;
|
|
style.first_line_indent = 0;
|
|
style.space_before = 0;
|
|
style.space_after = 0;
|
|
return style;
|
|
}
|
|
|
|
static void wp_paragraph_bounds(WordProcessorState* wp, int para_idx,
|
|
int* out_start, int* out_end, bool* out_has_newline) {
|
|
if (out_start) *out_start = 0;
|
|
if (out_end) *out_end = 0;
|
|
if (out_has_newline) *out_has_newline = false;
|
|
|
|
if (para_idx < 0) para_idx = 0;
|
|
if (para_idx >= wp->paragraph_count) para_idx = wp->paragraph_count - 1;
|
|
if (para_idx < 0) return;
|
|
|
|
int start = 0;
|
|
int cur_para = 0;
|
|
while (cur_para < para_idx && start < wp->total_text_len) {
|
|
while (start < wp->total_text_len && wp_char_at(wp, start) != '\n')
|
|
start++;
|
|
if (start < wp->total_text_len && wp_char_at(wp, start) == '\n')
|
|
start++;
|
|
cur_para++;
|
|
}
|
|
|
|
int end = start;
|
|
bool has_newline = false;
|
|
while (end < wp->total_text_len) {
|
|
if (wp_char_at(wp, end) == '\n') {
|
|
has_newline = true;
|
|
break;
|
|
}
|
|
end++;
|
|
}
|
|
|
|
if (out_start) *out_start = start;
|
|
if (out_end) *out_end = end;
|
|
if (out_has_newline) *out_has_newline = has_newline;
|
|
}
|
|
|
|
static void wp_font_at(WordProcessorState* wp, int abs_pos, int layout_dpi,
|
|
TrueTypeFont** out_font, GlyphCache** out_gc) {
|
|
if (wp->run_count <= 0) {
|
|
*out_font = wp_get_font(wp->cur_font_id, wp->cur_flags);
|
|
*out_gc = (*out_font && (*out_font)->valid)
|
|
? (*out_font)->get_cache(wp_points_to_pixels(wp->cur_size, layout_dpi))
|
|
: nullptr;
|
|
return;
|
|
}
|
|
|
|
int r, o;
|
|
wp_pos_to_run(wp, abs_pos, &r, &o);
|
|
if (r >= wp->run_count) r = wp->run_count - 1;
|
|
if (r < 0) r = 0;
|
|
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(wp_points_to_pixels(run->size, layout_dpi))
|
|
: 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_free_snapshot(UndoSnapshot* snap) {
|
|
if (snap->data) montauk::mfree(snap->data);
|
|
montauk::memset(snap, 0, sizeof(*snap));
|
|
}
|
|
|
|
static void wp_clear_history(WordProcessorState* wp) {
|
|
for (int i = 0; i < wp->undo_count; i++)
|
|
wp_free_snapshot(&wp->undo[i]);
|
|
wp->undo_count = 0;
|
|
wp->undo_pos = 0;
|
|
}
|
|
|
|
static void wp_insert_paragraph_style(WordProcessorState* wp, int at, const ParagraphStyle* style) {
|
|
if (wp->paragraph_count >= WP_MAX_PARAGRAPHS) return;
|
|
if (at < 0) at = 0;
|
|
if (at > wp->paragraph_count) at = wp->paragraph_count;
|
|
for (int i = wp->paragraph_count; i > at; i--)
|
|
wp->paragraphs[i] = wp->paragraphs[i - 1];
|
|
wp->paragraphs[at] = *style;
|
|
wp->paragraph_count++;
|
|
}
|
|
|
|
static void wp_remove_paragraph_style(WordProcessorState* wp, int at) {
|
|
if (wp->paragraph_count <= 1) {
|
|
wp->paragraph_count = 1;
|
|
wp_init_paragraph_style(&wp->paragraphs[0]);
|
|
return;
|
|
}
|
|
if (at < 0 || at >= wp->paragraph_count) return;
|
|
for (int i = at; i < wp->paragraph_count - 1; i++)
|
|
wp->paragraphs[i] = wp->paragraphs[i + 1];
|
|
wp->paragraph_count--;
|
|
}
|
|
|
|
static int wp_count_paragraphs_in_text(WordProcessorState* wp) {
|
|
int count = 1;
|
|
for (int i = 0; i < wp->total_text_len; i++) {
|
|
if (wp_char_at(wp, i) == '\n') {
|
|
count++;
|
|
if (count > WP_MAX_PARAGRAPHS) return WP_MAX_PARAGRAPHS + 1;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
static void wp_sync_paragraph_count(WordProcessorState* wp) {
|
|
int needed = wp_count_paragraphs_in_text(wp);
|
|
if (needed <= 0) needed = 1;
|
|
if (needed > WP_MAX_PARAGRAPHS) needed = WP_MAX_PARAGRAPHS;
|
|
|
|
if (wp->paragraph_count <= 0) {
|
|
wp->paragraph_count = 1;
|
|
wp_init_paragraph_style(&wp->paragraphs[0]);
|
|
}
|
|
|
|
if (wp->paragraph_count < needed) {
|
|
ParagraphStyle style = wp->paragraphs[wp->paragraph_count - 1];
|
|
while (wp->paragraph_count < needed)
|
|
wp_insert_paragraph_style(wp, wp->paragraph_count, &style);
|
|
} else if (wp->paragraph_count > needed) {
|
|
wp->paragraph_count = needed;
|
|
}
|
|
}
|
|
|
|
static int wp_count_newline_chars(const char* text, int len) {
|
|
if (len < 0) return -1;
|
|
if (len > 0 && text == nullptr) return -1;
|
|
|
|
int count = 0;
|
|
for (int i = 0; i < len; i++) {
|
|
if (text[i] == '\0') return -1;
|
|
if (text[i] == '\n') count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
static int wp_count_newlines_in_range(WordProcessorState* wp, int start, int end) {
|
|
int count = 0;
|
|
for (int i = start; i < end; i++) {
|
|
if (wp_char_at(wp, i) == '\n')
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
static void wp_on_insert_newline(WordProcessorState* wp, int abs_pos) {
|
|
if (wp->paragraph_count >= WP_MAX_PARAGRAPHS) return;
|
|
int para = wp_find_paragraph_at(wp, abs_pos);
|
|
if (para < 0) para = 0;
|
|
if (para >= wp->paragraph_count) para = wp->paragraph_count - 1;
|
|
ParagraphStyle style = wp->paragraphs[para];
|
|
style.divider_type = PARA_DIVIDER_NONE;
|
|
wp_insert_paragraph_style(wp, para + 1, &style);
|
|
}
|
|
|
|
static void wp_on_delete_newline(WordProcessorState* wp, int abs_pos) {
|
|
int para = wp_find_paragraph_at(wp, abs_pos);
|
|
if (para + 1 < wp->paragraph_count)
|
|
wp_remove_paragraph_style(wp, para + 1);
|
|
}
|
|
|
|
static void wp_default_line_metrics(WordProcessorState* wp, int abs_pos, int layout_dpi,
|
|
int* out_height, int* out_ascent) {
|
|
TrueTypeFont* font = nullptr;
|
|
GlyphCache* gc = nullptr;
|
|
if (wp->total_text_len > 0) {
|
|
int pos = abs_pos;
|
|
if (pos >= wp->total_text_len) pos = wp->total_text_len - 1;
|
|
if (pos < 0) pos = 0;
|
|
wp_font_at(wp, pos, layout_dpi, &font, &gc);
|
|
} else {
|
|
font = wp_get_font(wp->cur_font_id, wp->cur_flags);
|
|
gc = (font && font->valid) ? font->get_cache(wp_points_to_pixels(wp->cur_size, layout_dpi)) : nullptr;
|
|
}
|
|
|
|
if (gc) {
|
|
*out_height = gc->line_height;
|
|
*out_ascent = gc->ascent;
|
|
} else {
|
|
*out_height = wp_points_to_pixels(WP_DEFAULT_SIZE, layout_dpi);
|
|
*out_ascent = *out_height;
|
|
}
|
|
}
|
|
|
|
static int wp_char_advance_at(WordProcessorState* wp, int abs_pos, int layout_dpi, char ch) {
|
|
if (ch == '\n') return 0;
|
|
if (!(ch >= 32 || ch < 0)) return 0;
|
|
|
|
TrueTypeFont* font = nullptr;
|
|
GlyphCache* gc = nullptr;
|
|
wp_font_at(wp, abs_pos, layout_dpi, &font, &gc);
|
|
if (!font || !gc) return wp_points_to_pixels(6, layout_dpi);
|
|
|
|
CachedGlyph* g = font->get_glyph(gc, (unsigned char)ch);
|
|
return g ? g->advance : wp_points_to_pixels(6, layout_dpi);
|
|
}
|
|
|
|
static void wp_measure_line_range(WordProcessorState* wp, int start, int count, int layout_dpi,
|
|
int* out_width, int* out_height, int* out_ascent) {
|
|
int width = 0;
|
|
int height = 0;
|
|
int ascent = 0;
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
char ch = wp_char_at(wp, start + i);
|
|
if (ch != '\n')
|
|
width += wp_char_advance_at(wp, start + i, layout_dpi, ch);
|
|
|
|
TrueTypeFont* font = nullptr;
|
|
GlyphCache* gc = nullptr;
|
|
wp_font_at(wp, start + i, layout_dpi, &font, &gc);
|
|
if (gc) {
|
|
if (gc->line_height > height) height = gc->line_height;
|
|
if (gc->ascent > ascent) ascent = gc->ascent;
|
|
}
|
|
}
|
|
|
|
if (height == 0)
|
|
wp_default_line_metrics(wp, start, layout_dpi, &height, &ascent);
|
|
|
|
*out_width = width;
|
|
*out_height = height;
|
|
*out_ascent = ascent;
|
|
}
|
|
|
|
static int wp_line_spacing_advance(int height, int spacing_percent) {
|
|
int advance = (height * spacing_percent + 99) / 100;
|
|
return advance < height ? height : advance;
|
|
}
|
|
|
|
static int wp_divider_height(uint8_t divider_type, int layout_dpi) {
|
|
int base = 14;
|
|
if (divider_type == PARA_DIVIDER_DOUBLE) base = 18;
|
|
else if (divider_type == PARA_DIVIDER_HEAVY) base = 16;
|
|
else if (divider_type == PARA_DIVIDER_THIN_THICK || divider_type == PARA_DIVIDER_THICK_THIN) base = 18;
|
|
int scaled = wp_scale_layout_units(base, layout_dpi);
|
|
return scaled > 0 ? scaled : 1;
|
|
}
|
|
|
|
static int wp_effective_text_indent(const ParagraphStyle* para, bool first_line, int layout_dpi) {
|
|
int indent = para->left_indent;
|
|
if (indent < 0) indent = 0;
|
|
if (first_line && para->list_type == PARA_LIST_NONE)
|
|
indent += para->first_line_indent;
|
|
if (indent < 0) indent = 0;
|
|
return wp_scale_layout_units(indent, layout_dpi);
|
|
}
|
|
|
|
static void wp_prepare_paragraph_for_text_insert(WordProcessorState* wp, int abs_pos, char c) {
|
|
if (c == '\n' || wp->paragraph_count <= 0) return;
|
|
|
|
int para = wp_find_paragraph_at(wp, abs_pos);
|
|
if (para < 0 || para >= wp->paragraph_count) return;
|
|
|
|
ParagraphStyle* style = &wp->paragraphs[para];
|
|
if (style->divider_type == PARA_DIVIDER_NONE) return;
|
|
|
|
int para_start = 0;
|
|
int para_end = 0;
|
|
wp_paragraph_bounds(wp, para, ¶_start, ¶_end, nullptr);
|
|
if (para_start != para_end) return;
|
|
|
|
style->divider_type = PARA_DIVIDER_NONE;
|
|
wp->modified = true;
|
|
wp->wrap_dirty = true;
|
|
}
|
|
|
|
static bool wp_ensure_wrap_capacity(WordProcessorState* wp, int needed) {
|
|
if (needed <= wp->wrap_line_cap && wp->wrap_lines)
|
|
return true;
|
|
|
|
int new_cap = wp->wrap_line_cap > 0 ? wp->wrap_line_cap : 128;
|
|
while (new_cap < needed && new_cap < WP_MAX_WRAP_LINES)
|
|
new_cap *= 2;
|
|
if (new_cap > WP_MAX_WRAP_LINES) new_cap = WP_MAX_WRAP_LINES;
|
|
if (new_cap < needed) return false;
|
|
|
|
void* mem = nullptr;
|
|
if (wp->wrap_lines)
|
|
mem = montauk::realloc(wp->wrap_lines, new_cap * (int)sizeof(WrapLine));
|
|
else
|
|
mem = montauk::malloc(new_cap * (int)sizeof(WrapLine));
|
|
if (!mem) return false;
|
|
|
|
wp->wrap_lines = (WrapLine*)mem;
|
|
wp->wrap_line_cap = new_cap;
|
|
return true;
|
|
}
|
|
|
|
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_line_cap = 0;
|
|
wp->wrap_dirty = true;
|
|
wp->last_wrap_width = 0;
|
|
wp->last_wrap_dpi = 0;
|
|
|
|
wp->paragraph_count = 1;
|
|
wp_init_paragraph_style(&wp->paragraphs[0]);
|
|
|
|
wp->modified = false;
|
|
wp->filepath[0] = '\0';
|
|
wp->filename[0] = '\0';
|
|
wp->preferred_printer_uri[0] = '\0';
|
|
wp->preferred_printer_name[0] = '\0';
|
|
wp->preferred_print_copies = 1;
|
|
wp->status_msg[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;
|
|
wp->line_spacing_dropdown_open = false;
|
|
wp->divider_flyout_open = false;
|
|
wp->special_char_flyout_open = false;
|
|
mtk::context_menu_reset(wp->context_menu);
|
|
wp->page_view_mode = false;
|
|
wp->pending_page_view_toggle = false;
|
|
|
|
wp->undo_count = 0;
|
|
wp->undo_pos = 0;
|
|
montauk::memset(wp->undo, 0, sizeof(wp->undo));
|
|
|
|
wp_history_reset(wp);
|
|
}
|
|
|
|
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;
|
|
wp->wrap_line_cap = 0;
|
|
wp_clear_history(wp);
|
|
}
|
|
|
|
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';
|
|
}
|
|
|
|
int wp_find_paragraph_at(WordProcessorState* wp, int abs_pos) {
|
|
if (wp->paragraph_count <= 1 || abs_pos <= 0) return 0;
|
|
if (abs_pos > wp->total_text_len) abs_pos = wp->total_text_len;
|
|
|
|
int para = 0;
|
|
for (int i = 0; i < abs_pos && i < wp->total_text_len; i++) {
|
|
if (wp_char_at(wp, i) == '\n') {
|
|
para++;
|
|
if (para >= wp->paragraph_count - 1)
|
|
return wp->paragraph_count - 1;
|
|
}
|
|
}
|
|
return para;
|
|
}
|
|
|
|
void wp_selected_paragraph_range(WordProcessorState* wp, int* out_start_para, int* out_end_para) {
|
|
if (!out_start_para || !out_end_para) return;
|
|
|
|
if (wp->has_selection) {
|
|
int sel_s, sel_e;
|
|
wp_sel_range(wp, &sel_s, &sel_e);
|
|
if (sel_s != sel_e) {
|
|
*out_start_para = wp_find_paragraph_at(wp, sel_s);
|
|
int end_abs = sel_e > 0 ? sel_e - 1 : 0;
|
|
if (end_abs < sel_s) end_abs = sel_s;
|
|
*out_end_para = wp_find_paragraph_at(wp, end_abs);
|
|
return;
|
|
}
|
|
}
|
|
|
|
int abs = wp_abs_pos(wp, wp->cursor_run, wp->cursor_offset);
|
|
*out_start_para = wp_find_paragraph_at(wp, abs);
|
|
*out_end_para = *out_start_para;
|
|
}
|
|
|
|
void wp_insert_char(WordProcessorState* wp, char c) {
|
|
if (wp->total_text_len >= WP_MAX_TEXT - 1) return;
|
|
if (c == '\n' && wp->paragraph_count >= WP_MAX_PARAGRAPHS) return;
|
|
|
|
int insert_abs = wp_abs_pos(wp, wp->cursor_run, wp->cursor_offset);
|
|
wp_prepare_paragraph_for_text_insert(wp, insert_abs, c);
|
|
|
|
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;
|
|
if (c == '\n') wp_on_insert_newline(wp, insert_abs);
|
|
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;
|
|
if (c == '\n') wp_on_insert_newline(wp, insert_abs);
|
|
}
|
|
|
|
void wp_backspace(WordProcessorState* wp) {
|
|
if (wp->cursor_run == 0 && wp->cursor_offset == 0) return;
|
|
|
|
int removed_abs = wp_abs_pos(wp, wp->cursor_run, wp->cursor_offset) - 1;
|
|
char removed = '\0';
|
|
if (removed_abs >= 0)
|
|
removed = wp_char_at(wp, removed_abs);
|
|
|
|
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;
|
|
if (removed == '\n') wp_on_delete_newline(wp, removed_abs);
|
|
wp_merge_adjacent(wp);
|
|
wp_sync_paragraph_count(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;
|
|
|
|
char removed = wp_char_at(wp, abs);
|
|
|
|
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;
|
|
if (removed == '\n') wp_on_delete_newline(wp, abs);
|
|
wp_merge_adjacent(wp);
|
|
wp_sync_paragraph_count(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_apply_alignment(WordProcessorState* wp, uint8_t align) {
|
|
if (align > PARA_ALIGN_RIGHT) align = PARA_ALIGN_LEFT;
|
|
|
|
int para_s, para_e;
|
|
wp_selected_paragraph_range(wp, ¶_s, ¶_e);
|
|
bool changed = false;
|
|
for (int i = para_s; i <= para_e && i < wp->paragraph_count; i++) {
|
|
if (wp->paragraphs[i].align != align) {
|
|
wp->paragraphs[i].align = align;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (changed) {
|
|
wp->modified = true;
|
|
wp->wrap_dirty = true;
|
|
}
|
|
}
|
|
|
|
void wp_adjust_paragraph_indent(WordProcessorState* wp, int delta) {
|
|
int para_s, para_e;
|
|
wp_selected_paragraph_range(wp, ¶_s, ¶_e);
|
|
bool changed = false;
|
|
for (int i = para_s; i <= para_e && i < wp->paragraph_count; i++) {
|
|
int v = wp->paragraphs[i].left_indent + delta;
|
|
int16_t nv = (int16_t)gui_clamp(v, 0, 240);
|
|
if (wp->paragraphs[i].left_indent != nv) {
|
|
wp->paragraphs[i].left_indent = nv;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (changed) {
|
|
wp->modified = true;
|
|
wp->wrap_dirty = true;
|
|
}
|
|
}
|
|
|
|
void wp_adjust_paragraph_first_line_indent(WordProcessorState* wp, int delta) {
|
|
int para_s, para_e;
|
|
wp_selected_paragraph_range(wp, ¶_s, ¶_e);
|
|
bool changed = false;
|
|
for (int i = para_s; i <= para_e && i < wp->paragraph_count; i++) {
|
|
int v = wp->paragraphs[i].first_line_indent + delta;
|
|
int16_t nv = (int16_t)gui_clamp(v, -120, 120);
|
|
if (wp->paragraphs[i].first_line_indent != nv) {
|
|
wp->paragraphs[i].first_line_indent = nv;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (changed) {
|
|
wp->modified = true;
|
|
wp->wrap_dirty = true;
|
|
}
|
|
}
|
|
|
|
void wp_adjust_paragraph_spacing_before(WordProcessorState* wp, int delta) {
|
|
int para_s, para_e;
|
|
wp_selected_paragraph_range(wp, ¶_s, ¶_e);
|
|
bool changed = false;
|
|
for (int i = para_s; i <= para_e && i < wp->paragraph_count; i++) {
|
|
int v = wp->paragraphs[i].space_before + delta;
|
|
int16_t nv = (int16_t)gui_clamp(v, 0, 120);
|
|
if (wp->paragraphs[i].space_before != nv) {
|
|
wp->paragraphs[i].space_before = nv;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (changed) {
|
|
wp->modified = true;
|
|
wp->wrap_dirty = true;
|
|
}
|
|
}
|
|
|
|
void wp_adjust_paragraph_spacing_after(WordProcessorState* wp, int delta) {
|
|
int para_s, para_e;
|
|
wp_selected_paragraph_range(wp, ¶_s, ¶_e);
|
|
bool changed = false;
|
|
for (int i = para_s; i <= para_e && i < wp->paragraph_count; i++) {
|
|
int v = wp->paragraphs[i].space_after + delta;
|
|
int16_t nv = (int16_t)gui_clamp(v, 0, 120);
|
|
if (wp->paragraphs[i].space_after != nv) {
|
|
wp->paragraphs[i].space_after = nv;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (changed) {
|
|
wp->modified = true;
|
|
wp->wrap_dirty = true;
|
|
}
|
|
}
|
|
|
|
void wp_set_line_spacing(WordProcessorState* wp, int value) {
|
|
int selected = WP_LINE_SPACING_OPTIONS[0];
|
|
for (int i = 0; i < WP_LINE_SPACING_OPTION_COUNT; i++) {
|
|
if (WP_LINE_SPACING_OPTIONS[i] == value) {
|
|
selected = value;
|
|
break;
|
|
}
|
|
}
|
|
|
|
int para_s, para_e;
|
|
wp_selected_paragraph_range(wp, ¶_s, ¶_e);
|
|
bool changed = false;
|
|
for (int i = para_s; i <= para_e && i < wp->paragraph_count; i++) {
|
|
if (wp->paragraphs[i].line_spacing != selected) {
|
|
wp->paragraphs[i].line_spacing = (uint8_t)selected;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (changed) {
|
|
wp->modified = true;
|
|
wp->wrap_dirty = true;
|
|
}
|
|
}
|
|
|
|
void wp_cycle_line_spacing(WordProcessorState* wp) {
|
|
int para_s, para_e;
|
|
wp_selected_paragraph_range(wp, ¶_s, ¶_e);
|
|
int current = wp->paragraphs[para_s].line_spacing;
|
|
int next = WP_LINE_SPACING_OPTIONS[0];
|
|
|
|
for (int i = 0; i < WP_LINE_SPACING_OPTION_COUNT; i++) {
|
|
if (WP_LINE_SPACING_OPTIONS[i] == current) {
|
|
next = WP_LINE_SPACING_OPTIONS[(i + 1) % WP_LINE_SPACING_OPTION_COUNT];
|
|
break;
|
|
}
|
|
}
|
|
|
|
wp_set_line_spacing(wp, next);
|
|
}
|
|
|
|
void wp_toggle_list(WordProcessorState* wp, uint8_t list_type) {
|
|
if (list_type != PARA_LIST_BULLET && list_type != PARA_LIST_NUMBER)
|
|
return;
|
|
|
|
int para_s, para_e;
|
|
wp_selected_paragraph_range(wp, ¶_s, ¶_e);
|
|
|
|
bool turn_off = true;
|
|
bool has_target = false;
|
|
for (int i = para_s; i <= para_e && i < wp->paragraph_count; i++) {
|
|
if (wp->paragraphs[i].divider_type != PARA_DIVIDER_NONE)
|
|
continue;
|
|
has_target = true;
|
|
if (wp->paragraphs[i].list_type != list_type) {
|
|
turn_off = false;
|
|
break;
|
|
}
|
|
}
|
|
if (!has_target) return;
|
|
|
|
bool changed = false;
|
|
for (int i = para_s; i <= para_e && i < wp->paragraph_count; i++) {
|
|
ParagraphStyle* para = &wp->paragraphs[i];
|
|
if (para->divider_type != PARA_DIVIDER_NONE)
|
|
continue;
|
|
if (turn_off) {
|
|
if (para->list_type != PARA_LIST_NONE) {
|
|
para->list_type = PARA_LIST_NONE;
|
|
changed = true;
|
|
}
|
|
if (para->left_indent == WP_LIST_LEFT && para->first_line_indent == WP_LIST_HANGING) {
|
|
para->left_indent = 0;
|
|
para->first_line_indent = 0;
|
|
changed = true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (para->list_type == PARA_LIST_NONE &&
|
|
para->left_indent == 0 && para->first_line_indent == 0) {
|
|
para->left_indent = WP_LIST_LEFT;
|
|
para->first_line_indent = WP_LIST_HANGING;
|
|
changed = true;
|
|
}
|
|
if (para->list_type != list_type) {
|
|
para->list_type = list_type;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (changed) {
|
|
wp->modified = true;
|
|
wp->wrap_dirty = true;
|
|
}
|
|
}
|
|
|
|
void wp_insert_divider(WordProcessorState* wp, uint8_t divider_type) {
|
|
if (!wp_valid_divider_type(divider_type)) return;
|
|
|
|
if (wp->has_selection)
|
|
wp_delete_selection(wp);
|
|
|
|
int abs = wp_abs_pos(wp, wp->cursor_run, wp->cursor_offset);
|
|
int para = wp_find_paragraph_at(wp, abs);
|
|
if (para < 0) para = 0;
|
|
if (para >= wp->paragraph_count) para = wp->paragraph_count - 1;
|
|
|
|
int para_start = 0;
|
|
int para_end = 0;
|
|
bool has_newline = false;
|
|
wp_paragraph_bounds(wp, para, ¶_start, ¶_end, &has_newline);
|
|
|
|
int needed_newlines = 0;
|
|
if (para_start == para_end) {
|
|
needed_newlines = has_newline ? 0 : 1;
|
|
} else if (abs == para_start) {
|
|
needed_newlines = 1;
|
|
} else if (abs == para_end) {
|
|
needed_newlines = has_newline ? 1 : 2;
|
|
} else {
|
|
needed_newlines = 2;
|
|
}
|
|
|
|
if (wp->paragraph_count + needed_newlines > WP_MAX_PARAGRAPHS)
|
|
return;
|
|
if (wp->total_text_len + needed_newlines > WP_MAX_TEXT - 1)
|
|
return;
|
|
|
|
ParagraphStyle divider_style = wp_make_divider_style(&wp->paragraphs[para], divider_type);
|
|
|
|
if (para_start == para_end) {
|
|
wp->paragraphs[para] = divider_style;
|
|
wp->modified = true;
|
|
wp->wrap_dirty = true;
|
|
|
|
if (!has_newline) {
|
|
wp_insert_char(wp, '\n');
|
|
} else {
|
|
wp_pos_to_run(wp, para_end + 1, &wp->cursor_run, &wp->cursor_offset);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (abs == para_start) {
|
|
wp_insert_char(wp, '\n');
|
|
wp->paragraphs[para] = divider_style;
|
|
wp_pos_to_run(wp, para_start + 1, &wp->cursor_run, &wp->cursor_offset);
|
|
} else if (abs == para_end) {
|
|
wp_insert_char(wp, '\n');
|
|
if (!has_newline)
|
|
wp_insert_char(wp, '\n');
|
|
wp->paragraphs[para + 1] = divider_style;
|
|
wp_pos_to_run(wp, abs + 2, &wp->cursor_run, &wp->cursor_offset);
|
|
} else {
|
|
wp_insert_char(wp, '\n');
|
|
wp_insert_char(wp, '\n');
|
|
wp->paragraphs[para + 1] = divider_style;
|
|
wp_pos_to_run(wp, abs + 2, &wp->cursor_run, &wp->cursor_offset);
|
|
}
|
|
|
|
wp->modified = true;
|
|
wp->wrap_dirty = true;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
char* wp_selection_plain_text_alloc(WordProcessorState* wp, int* out_len) {
|
|
if (out_len) *out_len = 0;
|
|
if (wp == nullptr || !wp->has_selection) return nullptr;
|
|
|
|
int sel_s, sel_e;
|
|
wp_sel_range(wp, &sel_s, &sel_e);
|
|
int len = sel_e - sel_s;
|
|
if (len <= 0) return nullptr;
|
|
|
|
char* text = (char*)montauk::malloc(len + 1);
|
|
if (text == nullptr) return nullptr;
|
|
|
|
for (int i = 0; i < len; i++)
|
|
text[i] = wp_char_at(wp, sel_s + i);
|
|
text[len] = '\0';
|
|
|
|
if (out_len) *out_len = len;
|
|
return text;
|
|
}
|
|
|
|
bool wp_insert_text(WordProcessorState* wp, const char* text, int len) {
|
|
int newline_count = wp_count_newline_chars(text, len);
|
|
if (wp == nullptr || newline_count < 0) return false;
|
|
if (len == 0) return true;
|
|
if (wp->cursor_run < 0 || wp->cursor_run >= wp->run_count) return false;
|
|
if (wp->total_text_len + len > WP_MAX_TEXT - 1) return false;
|
|
if (wp->paragraph_count + newline_count > WP_MAX_PARAGRAPHS) return false;
|
|
|
|
StyledRun* cur = &wp->runs[wp->cursor_run];
|
|
int extra_runs = 0;
|
|
if (!wp_same_style(cur, wp->cur_font_id, wp->cur_size, wp->cur_flags)) {
|
|
extra_runs = (wp->cursor_offset == 0 || wp->cursor_offset == cur->len) ? 1 : 2;
|
|
}
|
|
if (wp->run_count + extra_runs > WP_MAX_RUNS) return false;
|
|
|
|
for (int i = 0; i < len; i++)
|
|
wp_insert_char(wp, text[i]);
|
|
return true;
|
|
}
|
|
|
|
void wp_recompute_wrap(WordProcessorState* wp, int content_w, int layout_dpi) {
|
|
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 || layout_dpi != wp->last_wrap_dpi) {
|
|
wp->wrap_dirty = true;
|
|
wp->last_wrap_width = wrap_width;
|
|
wp->last_wrap_dpi = layout_dpi;
|
|
}
|
|
|
|
if (!wp->wrap_dirty && wp->wrap_lines) return;
|
|
|
|
wp->wrap_line_count = 0;
|
|
if (!wp_ensure_wrap_capacity(wp, 1)) {
|
|
wp->content_height = WP_MARGIN * 2;
|
|
return;
|
|
}
|
|
int y = WP_MARGIN;
|
|
int total = wp->total_text_len;
|
|
int pos = 0;
|
|
int para_idx = 0;
|
|
int list_number = 1;
|
|
|
|
wp_sync_paragraph_count(wp);
|
|
|
|
while (para_idx < wp->paragraph_count && wp->wrap_line_count < WP_MAX_WRAP_LINES) {
|
|
ParagraphStyle* para = &wp->paragraphs[para_idx];
|
|
int para_start = pos;
|
|
int para_content_end = pos;
|
|
bool has_newline = false;
|
|
|
|
while (para_content_end < total) {
|
|
char ch = wp_char_at(wp, para_content_end);
|
|
if (ch == '\n') {
|
|
has_newline = true;
|
|
break;
|
|
}
|
|
para_content_end++;
|
|
}
|
|
|
|
bool is_divider = (para->divider_type != PARA_DIVIDER_NONE && para_start == para_content_end);
|
|
y += wp_scale_layout_units(para->space_before, layout_dpi);
|
|
|
|
bool first_line = true;
|
|
int line_start = para_start;
|
|
while (line_start < para_content_end) {
|
|
if (wp->wrap_line_count >= WP_MAX_WRAP_LINES) break;
|
|
if (!wp_ensure_wrap_capacity(wp, wp->wrap_line_count + 1)) break;
|
|
|
|
int text_indent = wp_effective_text_indent(para, first_line, layout_dpi);
|
|
int avail_width = wrap_width - text_indent;
|
|
if (avail_width < 40) avail_width = 40;
|
|
|
|
int scan = line_start;
|
|
int width = 0;
|
|
int last_space = -1;
|
|
int width_at_last_space = 0;
|
|
|
|
while (scan < para_content_end) {
|
|
char ch = wp_char_at(wp, scan);
|
|
int cw = wp_char_advance_at(wp, scan, layout_dpi, ch);
|
|
if (width + cw > avail_width && scan > line_start) {
|
|
if (last_space >= line_start) {
|
|
scan = last_space + 1;
|
|
width = width_at_last_space;
|
|
}
|
|
break;
|
|
}
|
|
|
|
width += cw;
|
|
if (ch == ' ') {
|
|
last_space = scan;
|
|
width_at_last_space = width;
|
|
}
|
|
scan++;
|
|
}
|
|
|
|
if (scan == line_start && scan < para_content_end) {
|
|
width += wp_char_advance_at(wp, scan, layout_dpi, wp_char_at(wp, scan));
|
|
scan++;
|
|
}
|
|
|
|
int char_count = scan - line_start;
|
|
bool last_visual_line = (scan >= para_content_end);
|
|
if (last_visual_line && has_newline) char_count++;
|
|
|
|
int line_width = 0;
|
|
int line_height = 0;
|
|
int line_ascent = 0;
|
|
wp_measure_line_range(wp, line_start, char_count, layout_dpi,
|
|
&line_width, &line_height, &line_ascent);
|
|
|
|
WrapLine* line = &wp->wrap_lines[wp->wrap_line_count];
|
|
wp_pos_to_run(wp, line_start, &line->run_idx, &line->run_offset);
|
|
line->char_count = char_count;
|
|
line->y = y;
|
|
line->height = line_height;
|
|
line->baseline = line_ascent;
|
|
line->width = line_width;
|
|
line->paragraph_idx = para_idx;
|
|
line->list_number = (first_line && para->list_type == PARA_LIST_NUMBER) ? list_number : 0;
|
|
line->divider_type = PARA_DIVIDER_NONE;
|
|
line->first_in_paragraph = first_line;
|
|
|
|
int extra = wrap_width - text_indent - line_width;
|
|
if (extra < 0) extra = 0;
|
|
int x = WP_MARGIN + text_indent;
|
|
if (para->align == PARA_ALIGN_CENTER) x += extra / 2;
|
|
else if (para->align == PARA_ALIGN_RIGHT) x += extra;
|
|
line->x = x;
|
|
|
|
wp->wrap_line_count++;
|
|
|
|
line_start = scan;
|
|
first_line = false;
|
|
if (!last_visual_line)
|
|
y += wp_line_spacing_advance(line_height, para->line_spacing);
|
|
else
|
|
y += line_height;
|
|
}
|
|
|
|
if (para_start == para_content_end && wp->wrap_line_count < WP_MAX_WRAP_LINES) {
|
|
if (!wp_ensure_wrap_capacity(wp, wp->wrap_line_count + 1)) break;
|
|
WrapLine* line = &wp->wrap_lines[wp->wrap_line_count];
|
|
wp_pos_to_run(wp, para_start, &line->run_idx, &line->run_offset);
|
|
line->char_count = has_newline ? 1 : 0;
|
|
line->y = y;
|
|
line->paragraph_idx = para_idx;
|
|
line->list_number = (is_divider || para->list_type != PARA_LIST_NUMBER) ? 0 : list_number;
|
|
line->divider_type = is_divider ? para->divider_type : (uint8_t)PARA_DIVIDER_NONE;
|
|
line->first_in_paragraph = true;
|
|
|
|
int text_indent = wp_effective_text_indent(para, true, layout_dpi);
|
|
if (is_divider) {
|
|
line->height = wp_divider_height(para->divider_type, layout_dpi);
|
|
line->baseline = line->height / 2;
|
|
line->x = WP_MARGIN + text_indent;
|
|
line->width = wrap_width - text_indent;
|
|
if (line->width < 24) line->width = 24;
|
|
} else {
|
|
wp_default_line_metrics(wp, para_start, layout_dpi, &line->height, &line->baseline);
|
|
line->width = 0;
|
|
|
|
int extra = wrap_width - text_indent;
|
|
if (extra < 0) extra = 0;
|
|
int x = WP_MARGIN + text_indent;
|
|
if (para->align == PARA_ALIGN_CENTER) x += extra / 2;
|
|
else if (para->align == PARA_ALIGN_RIGHT) x += extra;
|
|
line->x = x;
|
|
}
|
|
|
|
wp->wrap_line_count++;
|
|
y = line->y + line->height;
|
|
}
|
|
|
|
y += wp_scale_layout_units(para->space_after, layout_dpi);
|
|
|
|
pos = para_content_end;
|
|
if (has_newline) pos++;
|
|
if (para->list_type == PARA_LIST_NUMBER) list_number++;
|
|
else list_number = 1;
|
|
para_idx++;
|
|
}
|
|
|
|
if (wp->wrap_line_count == 0) {
|
|
if (!wp_ensure_wrap_capacity(wp, 1)) {
|
|
wp->content_height = WP_MARGIN * 2;
|
|
return;
|
|
}
|
|
WrapLine* line = &wp->wrap_lines[0];
|
|
line->run_idx = 0;
|
|
line->run_offset = 0;
|
|
line->char_count = 0;
|
|
line->y = WP_MARGIN;
|
|
wp_default_line_metrics(wp, 0, layout_dpi, &line->height, &line->baseline);
|
|
line->x = WP_MARGIN;
|
|
line->width = 0;
|
|
line->paragraph_idx = 0;
|
|
line->list_number = 0;
|
|
line->divider_type = PARA_DIVIDER_NONE;
|
|
line->first_in_paragraph = true;
|
|
|
|
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_start_pathbar(WordProcessorState* wp, bool save_mode, const char* initial_text) {
|
|
if (!initial_text) initial_text = "";
|
|
wp->show_pathbar = true;
|
|
wp->pathbar_save_mode = save_mode;
|
|
montauk::strncpy(wp->pathbar_text, initial_text, 255);
|
|
wp->pathbar_text[255] = '\0';
|
|
wp->pathbar_len = montauk::slen(wp->pathbar_text);
|
|
wp->pathbar_cursor = wp->pathbar_len;
|
|
}
|
|
|
|
void wp_open_save_pathbar(WordProcessorState* wp) {
|
|
char path[256] = {};
|
|
const char* initial_path = wp->filepath[0] ? wp->filepath : "";
|
|
const char* suggested_name = wp->filename[0] ? wp->filename : "document.mwp";
|
|
char msg[160] = {};
|
|
if (dialogs::save_file("Save Document", initial_path, suggested_name, path, sizeof(path), msg, sizeof(msg))) {
|
|
wp_set_filepath(wp, path);
|
|
wp_save_file(wp);
|
|
} else if (msg[0]) {
|
|
wp_start_pathbar(wp, true, initial_path[0] ? initial_path : suggested_name);
|
|
}
|
|
}
|
|
|
|
static int wp_serialized_size(WordProcessorState* wp) {
|
|
int size = 4 + 2 + 1 + 1 + 1 + 2 + 2;
|
|
for (int i = 0; i < wp->run_count; i++)
|
|
size += 2 + 1 + 1 + 1 + 1 + wp->runs[i].len;
|
|
size += wp->paragraph_count * 12;
|
|
return size;
|
|
}
|
|
|
|
static void wp_write_u16(uint8_t* buf, int* off, int value) {
|
|
buf[(*off)++] = (uint8_t)(value & 0xFF);
|
|
buf[(*off)++] = (uint8_t)((value >> 8) & 0xFF);
|
|
}
|
|
|
|
static int wp_read_u16(const uint8_t* buf, int* off) {
|
|
int value = buf[*off] | (buf[*off + 1] << 8);
|
|
*off += 2;
|
|
return value;
|
|
}
|
|
|
|
static bool wp_serialize_document(WordProcessorState* wp, uint8_t** out_buf, int* out_size) {
|
|
int size = wp_serialized_size(wp);
|
|
uint8_t* buf = (uint8_t*)montauk::malloc(size);
|
|
if (!buf) return false;
|
|
|
|
int off = 0;
|
|
buf[off++] = 'M';
|
|
buf[off++] = 'W';
|
|
buf[off++] = 'P';
|
|
buf[off++] = '1';
|
|
buf[off++] = 2;
|
|
buf[off++] = 0;
|
|
buf[off++] = wp->cur_font_id;
|
|
buf[off++] = wp->cur_size;
|
|
buf[off++] = wp->cur_flags;
|
|
wp_write_u16(buf, &off, wp->run_count);
|
|
wp_write_u16(buf, &off, wp->paragraph_count);
|
|
|
|
for (int i = 0; i < wp->run_count; i++) {
|
|
StyledRun* r = &wp->runs[i];
|
|
wp_write_u16(buf, &off, r->len);
|
|
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;
|
|
}
|
|
|
|
for (int i = 0; i < wp->paragraph_count; i++) {
|
|
ParagraphStyle* para = &wp->paragraphs[i];
|
|
buf[off++] = para->align;
|
|
buf[off++] = para->list_type;
|
|
buf[off++] = para->line_spacing;
|
|
buf[off++] = para->divider_type;
|
|
wp_write_u16(buf, &off, (uint16_t)para->left_indent);
|
|
wp_write_u16(buf, &off, (uint16_t)para->first_line_indent);
|
|
wp_write_u16(buf, &off, (uint16_t)para->space_before);
|
|
wp_write_u16(buf, &off, (uint16_t)para->space_after);
|
|
}
|
|
|
|
*out_buf = buf;
|
|
*out_size = off;
|
|
return true;
|
|
}
|
|
|
|
static int wp_count_newlines_in_buf(const uint8_t* text, int len) {
|
|
int count = 1;
|
|
for (int i = 0; i < len; i++) {
|
|
if (text[i] == '\n') {
|
|
count++;
|
|
if (count > WP_MAX_PARAGRAPHS) return WP_MAX_PARAGRAPHS + 1;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
static bool wp_deserialize_document(WordProcessorState* wp, const uint8_t* buf, int size) {
|
|
if (size < 10) return false;
|
|
if (buf[0] != 'M' || buf[1] != 'W' || buf[2] != 'P' || buf[3] != '1')
|
|
return false;
|
|
|
|
uint8_t major = buf[4];
|
|
if (major != 1 && major != 2) return false;
|
|
|
|
if (major == 1) {
|
|
if (size < 10) return false;
|
|
} else {
|
|
if (size < 13) return false;
|
|
}
|
|
|
|
int header_off = 6;
|
|
uint8_t cur_font = buf[header_off++];
|
|
uint8_t cur_size = buf[header_off++];
|
|
uint8_t cur_flags = 0;
|
|
if (major >= 2) cur_flags = buf[header_off++];
|
|
|
|
if (header_off + 2 > size) return false;
|
|
int run_count = buf[header_off] | (buf[header_off + 1] << 8);
|
|
header_off += 2;
|
|
int stored_para_count = 0;
|
|
if (major >= 2) {
|
|
if (header_off + 2 > size) return false;
|
|
stored_para_count = buf[header_off] | (buf[header_off + 1] << 8);
|
|
header_off += 2;
|
|
}
|
|
|
|
if (run_count < 0 || run_count > WP_MAX_RUNS) return false;
|
|
|
|
int off = header_off;
|
|
int actual_para_count = 1;
|
|
for (int i = 0; i < run_count; i++) {
|
|
if (off + 6 > size) return false;
|
|
int text_len = wp_read_u16(buf, &off);
|
|
off += 4;
|
|
if (text_len < 0 || off + text_len > size) return false;
|
|
int para_count = wp_count_newlines_in_buf(buf + off, text_len);
|
|
if (para_count > WP_MAX_PARAGRAPHS) return false;
|
|
actual_para_count += para_count - 1;
|
|
if (actual_para_count > WP_MAX_PARAGRAPHS) return false;
|
|
off += text_len;
|
|
}
|
|
|
|
if (major >= 2) {
|
|
if (stored_para_count <= 0) stored_para_count = actual_para_count;
|
|
if (stored_para_count > WP_MAX_PARAGRAPHS) return false;
|
|
if (off + stored_para_count * 12 > size) return false;
|
|
}
|
|
|
|
wp_free_runs(wp);
|
|
wp->paragraph_count = 1;
|
|
wp_init_paragraph_style(&wp->paragraphs[0]);
|
|
wp->total_text_len = 0;
|
|
|
|
off = header_off;
|
|
for (int i = 0; i < run_count; i++) {
|
|
int text_len = wp_read_u16(buf, &off);
|
|
uint8_t font_id = buf[off++];
|
|
uint8_t run_size = buf[off++];
|
|
uint8_t flags = buf[off++];
|
|
off++;
|
|
|
|
StyledRun* r = &wp->runs[wp->run_count];
|
|
wp_init_run(r, font_id, run_size, flags);
|
|
wp_ensure_run_cap(r, text_len);
|
|
montauk::memcpy(r->text, buf + off, text_len);
|
|
r->len = text_len;
|
|
wp->run_count++;
|
|
wp->total_text_len += text_len;
|
|
off += text_len;
|
|
}
|
|
|
|
if (wp->run_count == 0) {
|
|
wp_init_run(&wp->runs[0], cur_font, cur_size, cur_flags);
|
|
wp->run_count = 1;
|
|
}
|
|
|
|
wp->cur_font_id = cur_font;
|
|
wp->cur_size = cur_size;
|
|
wp->cur_flags = cur_flags;
|
|
|
|
wp->paragraph_count = actual_para_count > 0 ? actual_para_count : 1;
|
|
for (int i = 0; i < wp->paragraph_count; i++)
|
|
wp_init_paragraph_style(&wp->paragraphs[i]);
|
|
|
|
if (major >= 2) {
|
|
int limit = wp_min_int(stored_para_count, wp->paragraph_count);
|
|
for (int i = 0; i < limit; i++) {
|
|
ParagraphStyle* para = &wp->paragraphs[i];
|
|
para->align = buf[off++];
|
|
para->list_type = buf[off++];
|
|
para->line_spacing = buf[off++];
|
|
para->divider_type = buf[off++];
|
|
para->left_indent = (int16_t)wp_read_u16(buf, &off);
|
|
para->first_line_indent = (int16_t)wp_read_u16(buf, &off);
|
|
para->space_before = (int16_t)wp_read_u16(buf, &off);
|
|
para->space_after = (int16_t)wp_read_u16(buf, &off);
|
|
|
|
if (para->align > PARA_ALIGN_RIGHT) para->align = PARA_ALIGN_LEFT;
|
|
if (para->list_type > PARA_LIST_NUMBER) para->list_type = PARA_LIST_NONE;
|
|
if (!wp_valid_divider_type(para->divider_type)) para->divider_type = PARA_DIVIDER_NONE;
|
|
|
|
bool known_spacing = false;
|
|
for (int j = 0; j < WP_LINE_SPACING_OPTION_COUNT; j++) {
|
|
if (para->line_spacing == WP_LINE_SPACING_OPTIONS[j]) {
|
|
known_spacing = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!known_spacing) para->line_spacing = 100;
|
|
}
|
|
|
|
for (int i = limit; i < stored_para_count; i++)
|
|
off += 12;
|
|
}
|
|
|
|
wp_sync_paragraph_count(wp);
|
|
wp_merge_adjacent(wp);
|
|
wp->cursor_run = 0;
|
|
wp->cursor_offset = 0;
|
|
wp->scrollbar.scroll_offset = 0;
|
|
wp->modified = false;
|
|
wp->wrap_dirty = true;
|
|
wp->last_wrap_width = 0;
|
|
wp->last_wrap_dpi = 0;
|
|
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->line_spacing_dropdown_open = false;
|
|
wp->divider_flyout_open = false;
|
|
wp->special_char_flyout_open = false;
|
|
wp->show_pathbar = false;
|
|
wp->page_view_mode = false;
|
|
wp->pending_page_view_toggle = false;
|
|
return true;
|
|
}
|
|
|
|
static bool wp_capture_snapshot(WordProcessorState* wp, UndoSnapshot* snap) {
|
|
montauk::memset(snap, 0, sizeof(*snap));
|
|
if (!wp_serialize_document(wp, &snap->data, &snap->size))
|
|
return false;
|
|
|
|
snap->cursor_abs = wp_abs_pos(wp, wp->cursor_run, wp->cursor_offset);
|
|
snap->sel_anchor = wp->sel_anchor;
|
|
snap->sel_end = wp->sel_end;
|
|
snap->has_selection = wp->has_selection;
|
|
snap->modified = wp->modified;
|
|
return true;
|
|
}
|
|
|
|
static bool wp_snapshots_match(const UndoSnapshot* a, const UndoSnapshot* b) {
|
|
if (a->size != b->size) return false;
|
|
if (a->cursor_abs != b->cursor_abs) return false;
|
|
if (a->sel_anchor != b->sel_anchor || a->sel_end != b->sel_end) return false;
|
|
if (a->has_selection != b->has_selection) return false;
|
|
if (a->modified != b->modified) return false;
|
|
if (a->size == 0) return true;
|
|
return memcmp(a->data, b->data, a->size) == 0;
|
|
}
|
|
|
|
static bool wp_restore_snapshot(WordProcessorState* wp, const UndoSnapshot* snap) {
|
|
if (!wp_deserialize_document(wp, snap->data, snap->size))
|
|
return false;
|
|
|
|
int cursor_abs = gui_clamp(snap->cursor_abs, 0, wp->total_text_len);
|
|
wp_pos_to_run(wp, cursor_abs, &wp->cursor_run, &wp->cursor_offset);
|
|
wp->sel_anchor = gui_clamp(snap->sel_anchor, 0, wp->total_text_len);
|
|
wp->sel_end = gui_clamp(snap->sel_end, 0, wp->total_text_len);
|
|
wp->has_selection = snap->has_selection && (wp->sel_anchor != wp->sel_end);
|
|
wp->modified = snap->modified;
|
|
wp->scrollbar.scroll_offset = 0;
|
|
return true;
|
|
}
|
|
|
|
bool wp_replace_selection_with_text(WordProcessorState* wp, const char* text, int len) {
|
|
int newline_count = wp_count_newline_chars(text, len);
|
|
if (wp == nullptr || newline_count < 0) return false;
|
|
|
|
int sel_len = 0;
|
|
int sel_newlines = 0;
|
|
if (wp->has_selection) {
|
|
int sel_s, sel_e;
|
|
wp_sel_range(wp, &sel_s, &sel_e);
|
|
sel_len = sel_e - sel_s;
|
|
sel_newlines = wp_count_newlines_in_range(wp, sel_s, sel_e);
|
|
}
|
|
|
|
if (wp->total_text_len - sel_len + len > WP_MAX_TEXT - 1)
|
|
return false;
|
|
if (wp->paragraph_count - sel_newlines + newline_count > WP_MAX_PARAGRAPHS)
|
|
return false;
|
|
|
|
UndoSnapshot snap = {};
|
|
if (!wp_capture_snapshot(wp, &snap))
|
|
return false;
|
|
|
|
if (wp->has_selection)
|
|
wp_delete_selection(wp);
|
|
|
|
bool ok = wp_insert_text(wp, text, len);
|
|
if (!ok)
|
|
wp_restore_snapshot(wp, &snap);
|
|
wp_free_snapshot(&snap);
|
|
return ok;
|
|
}
|
|
|
|
void wp_save_file(WordProcessorState* wp) {
|
|
if (wp->filepath[0] == '\0') {
|
|
wp_open_save_pathbar(wp);
|
|
return;
|
|
}
|
|
|
|
uint8_t* buf = nullptr;
|
|
int off = 0;
|
|
if (!wp_serialize_document(wp, &buf, &off))
|
|
return;
|
|
|
|
int fd = montauk::fcreate(wp->filepath);
|
|
if (fd >= 0) {
|
|
montauk::fwrite(fd, buf, 0, off);
|
|
montauk::close(fd);
|
|
wp_history_mark_saved(wp);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
bool ok = wp_deserialize_document(wp, buf, (int)fsize);
|
|
montauk::mfree(buf);
|
|
if (!ok) return;
|
|
|
|
wp_set_filepath(wp, path);
|
|
wp_history_reset(wp);
|
|
}
|
|
|
|
void wp_history_reset(WordProcessorState* wp) {
|
|
wp_clear_history(wp);
|
|
|
|
UndoSnapshot snap = {};
|
|
if (!wp_capture_snapshot(wp, &snap))
|
|
return;
|
|
|
|
wp->undo[0] = snap;
|
|
wp->undo_count = 1;
|
|
wp->undo_pos = 0;
|
|
}
|
|
|
|
void wp_history_checkpoint(WordProcessorState* wp) {
|
|
UndoSnapshot snap = {};
|
|
if (!wp_capture_snapshot(wp, &snap))
|
|
return;
|
|
|
|
if (wp->undo_count > 0 && wp_snapshots_match(&snap, &wp->undo[wp->undo_pos])) {
|
|
wp_free_snapshot(&snap);
|
|
return;
|
|
}
|
|
|
|
for (int i = wp->undo_pos + 1; i < wp->undo_count; i++)
|
|
wp_free_snapshot(&wp->undo[i]);
|
|
wp->undo_count = wp->undo_pos + 1;
|
|
|
|
if (wp->undo_count >= WP_UNDO_MAX) {
|
|
wp_free_snapshot(&wp->undo[0]);
|
|
for (int i = 1; i < wp->undo_count; i++)
|
|
wp->undo[i - 1] = wp->undo[i];
|
|
montauk::memset(&wp->undo[wp->undo_count - 1], 0, sizeof(UndoSnapshot));
|
|
wp->undo_count--;
|
|
if (wp->undo_pos > 0) wp->undo_pos--;
|
|
}
|
|
|
|
wp->undo[wp->undo_count++] = snap;
|
|
wp->undo_pos = wp->undo_count - 1;
|
|
}
|
|
|
|
void wp_history_mark_saved(WordProcessorState* wp) {
|
|
wp->modified = false;
|
|
if (wp->undo_count <= 0) {
|
|
wp_history_reset(wp);
|
|
return;
|
|
}
|
|
wp->undo[wp->undo_pos].modified = false;
|
|
}
|
|
|
|
bool wp_undo(WordProcessorState* wp) {
|
|
if (wp->undo_count <= 0 || wp->undo_pos <= 0)
|
|
return false;
|
|
|
|
wp->undo_pos--;
|
|
return wp_restore_snapshot(wp, &wp->undo[wp->undo_pos]);
|
|
}
|
|
|
|
bool wp_redo(WordProcessorState* wp) {
|
|
if (wp->undo_count <= 0 || wp->undo_pos >= wp->undo_count - 1)
|
|
return false;
|
|
|
|
wp->undo_pos++;
|
|
return wp_restore_snapshot(wp, &wp->undo[wp->undo_pos]);
|
|
}
|