468 lines
14 KiB
C++
468 lines
14 KiB
C++
/*
|
|
* 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 = 900;
|
|
static constexpr int INIT_H = 600;
|
|
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;
|
|
// A4 page dimensions at 96 DPI screen resolution (210mm x 297mm = 8.27" x 11.69")
|
|
static constexpr int WP_A4_PAGE_W = 794; // 8.27" * 96 DPI
|
|
static constexpr int WP_A4_PAGE_H = 1122; // 11.69" * 96 DPI
|
|
static constexpr int WP_A4_MARGIN_X = 96; // 1 inch = 96px margins (same as print)
|
|
static constexpr int WP_A4_MARGIN_Y = 96;
|
|
// Content width inside A4 page (794 - 96*2 - 16*2 = 570... but we use WP_A4 margins internally)
|
|
// A4 text area = WP_A4_PAGE_W - WP_A4_MARGIN_X * 2 = 794 - 192 = 602
|
|
static constexpr int WP_A4_CONTENT_W = 602;
|
|
// Vertical page offset from top of edit area (page top at edit_y + WP_PAGE_TOP_MARGIN)
|
|
static constexpr int WP_PAGE_TOP_MARGIN = 20;
|
|
static constexpr int WP_PAGE_SHADOW_OFFSET = 6;
|
|
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_MAX_PARAGRAPHS = 4096;
|
|
static constexpr int WP_DEFAULT_SIZE = 18;
|
|
static constexpr int WP_UNDO_MAX = 24;
|
|
static constexpr int WP_SCREEN_DPI = 96;
|
|
static constexpr int WP_PRINT_DPI = 150;
|
|
static constexpr int WP_PARA_STEP = 12;
|
|
static constexpr int WP_SPACE_STEP = 6;
|
|
static constexpr int WP_LIST_LEFT = 28;
|
|
static constexpr int WP_LIST_HANGING = -18;
|
|
static constexpr int WP_LIST_MARKER_W = 20;
|
|
static constexpr int WP_BTN_OPEN_X = 4;
|
|
static constexpr int WP_BTN_SAVE_X = 32;
|
|
static constexpr int WP_BTN_PRINT_X = 60;
|
|
static constexpr int WP_BTN_EXPORT_X = 88;
|
|
static constexpr int WP_BTN_UNDO_X = 122;
|
|
static constexpr int WP_BTN_REDO_X = 150;
|
|
static constexpr int WP_BTN_BOLD_X = 184;
|
|
static constexpr int WP_BTN_ITALIC_X = 212;
|
|
static constexpr int WP_FONT_DD_X = 244;
|
|
static constexpr int WP_FONT_DD_W = 74;
|
|
static constexpr int WP_SIZE_DD_X = 324;
|
|
static constexpr int WP_SIZE_DD_W = 36;
|
|
static constexpr int WP_BTN_ALIGN_L_X = 372;
|
|
static constexpr int WP_BTN_ALIGN_C_X = 400;
|
|
static constexpr int WP_BTN_ALIGN_R_X = 428;
|
|
static constexpr int WP_BTN_BULLET_X = 460;
|
|
static constexpr int WP_BTN_NUMBER_X = 488;
|
|
static constexpr int WP_BTN_OUTDENT_X = 524;
|
|
static constexpr int WP_BTN_INDENT_X = 552;
|
|
static constexpr int WP_LINE_DD_X = 586;
|
|
static constexpr int WP_LINE_DD_W = 48;
|
|
static constexpr int WP_BTN_DIVIDER_X = 642;
|
|
static constexpr int WP_BTN_SECTION_X = 674;
|
|
static constexpr int WP_BTN_PAGEVIEW_X = 712;
|
|
static constexpr int WP_DIVIDER_ROW_H = 24;
|
|
static constexpr int WP_DIVIDER_FLYOUT_W = 216;
|
|
static constexpr int WP_SPECIAL_CHAR_ROW_H = 24;
|
|
static constexpr int WP_SPECIAL_CHAR_FLYOUT_W = 144;
|
|
|
|
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;
|
|
inline constexpr int WP_LINE_SPACING_OPTIONS[] = { 100, 125, 150, 200 };
|
|
static constexpr int WP_LINE_SPACING_OPTION_COUNT = 4;
|
|
|
|
inline int wp_points_to_pixels(int points, int dpi) {
|
|
if (points <= 0) points = 1;
|
|
if (dpi <= 0) dpi = WP_SCREEN_DPI;
|
|
int px = (points * dpi + 36) / 72;
|
|
return px > 0 ? px : 1;
|
|
}
|
|
|
|
inline int wp_screen_font_pixels(int points) {
|
|
return wp_points_to_pixels(points, WP_SCREEN_DPI);
|
|
}
|
|
|
|
inline int wp_print_font_pixels(int points) {
|
|
return wp_points_to_pixels(points, WP_PRINT_DPI);
|
|
}
|
|
|
|
inline int wp_scale_layout_units(int units, int dpi) {
|
|
if (dpi <= 0) dpi = WP_SCREEN_DPI;
|
|
int scaled = (units * dpi + (units >= 0 ? WP_SCREEN_DPI / 2 : -WP_SCREEN_DPI / 2)) / WP_SCREEN_DPI;
|
|
return scaled;
|
|
}
|
|
|
|
struct WpSpecialCharOption {
|
|
uint8_t code;
|
|
const char* label;
|
|
};
|
|
|
|
struct WpDividerOption {
|
|
uint8_t type;
|
|
const char* label;
|
|
};
|
|
|
|
inline constexpr WpSpecialCharOption WP_SPECIAL_CHAR_OPTIONS[] = {
|
|
{ 0xA7, "Section" },
|
|
{ 0xB6, "Pilcrow" },
|
|
{ 0x96, "En Dash" },
|
|
{ 0x97, "Em Dash" },
|
|
{ 0xA9, "Copyright" },
|
|
{ 0xAE, "Registered" },
|
|
{ 0xB0, "Degree" },
|
|
{ 0xB1, "Plus/Minus" },
|
|
};
|
|
static constexpr int WP_SPECIAL_CHAR_OPTION_COUNT = 8;
|
|
|
|
enum ParagraphAlign : uint8_t {
|
|
PARA_ALIGN_LEFT = 0,
|
|
PARA_ALIGN_CENTER = 1,
|
|
PARA_ALIGN_RIGHT = 2,
|
|
};
|
|
|
|
enum ParagraphListType : uint8_t {
|
|
PARA_LIST_NONE = 0,
|
|
PARA_LIST_BULLET = 1,
|
|
PARA_LIST_NUMBER = 2,
|
|
};
|
|
|
|
enum ParagraphDividerType : uint8_t {
|
|
PARA_DIVIDER_NONE = 0,
|
|
PARA_DIVIDER_SINGLE = 1,
|
|
PARA_DIVIDER_DOUBLE = 2,
|
|
PARA_DIVIDER_DOTTED = 3,
|
|
PARA_DIVIDER_DASHED = 4,
|
|
PARA_DIVIDER_HEAVY = 5,
|
|
PARA_DIVIDER_THIN_THICK = 6,
|
|
PARA_DIVIDER_THICK_THIN = 7,
|
|
};
|
|
|
|
inline constexpr WpDividerOption WP_DIVIDER_OPTIONS[] = {
|
|
{ PARA_DIVIDER_SINGLE, "Single Line" },
|
|
{ PARA_DIVIDER_DOUBLE, "Double Line" },
|
|
{ PARA_DIVIDER_DOTTED, "Dotted Line" },
|
|
{ PARA_DIVIDER_DASHED, "Dashed Line" },
|
|
{ PARA_DIVIDER_HEAVY, "Heavy Line" },
|
|
{ PARA_DIVIDER_THIN_THICK, "Thin/Thick Rule" },
|
|
{ PARA_DIVIDER_THICK_THIN, "Thick/Thin Rule" },
|
|
};
|
|
static constexpr int WP_DIVIDER_OPTION_COUNT = 7;
|
|
|
|
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;
|
|
int x;
|
|
int width;
|
|
int paragraph_idx;
|
|
int list_number;
|
|
uint8_t divider_type;
|
|
bool first_in_paragraph;
|
|
};
|
|
|
|
struct ParagraphStyle {
|
|
uint8_t align;
|
|
uint8_t list_type;
|
|
uint8_t line_spacing;
|
|
uint8_t divider_type;
|
|
int16_t left_indent;
|
|
int16_t first_line_indent;
|
|
int16_t space_before;
|
|
int16_t space_after;
|
|
};
|
|
|
|
struct UndoSnapshot {
|
|
uint8_t* data;
|
|
int size;
|
|
int cursor_abs;
|
|
int sel_anchor;
|
|
int sel_end;
|
|
bool has_selection;
|
|
bool modified;
|
|
};
|
|
|
|
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;
|
|
int wrap_line_cap;
|
|
bool wrap_dirty;
|
|
int last_wrap_width;
|
|
int last_wrap_dpi;
|
|
|
|
ParagraphStyle paragraphs[WP_MAX_PARAGRAPHS];
|
|
int paragraph_count;
|
|
|
|
bool modified;
|
|
char filepath[256];
|
|
char filename[64];
|
|
char preferred_printer_uri[256];
|
|
char preferred_printer_name[128];
|
|
uint32_t preferred_print_copies;
|
|
char status_msg[160];
|
|
|
|
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;
|
|
bool line_spacing_dropdown_open;
|
|
bool divider_flyout_open;
|
|
bool special_char_flyout_open;
|
|
bool page_view_mode;
|
|
bool pending_page_view_toggle;
|
|
|
|
UndoSnapshot undo[WP_UNDO_MAX];
|
|
int undo_count;
|
|
int undo_pos;
|
|
};
|
|
|
|
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 SvgIcon g_icon_print;
|
|
extern SvgIcon g_icon_export_pdf;
|
|
extern SvgIcon g_icon_undo;
|
|
extern SvgIcon g_icon_redo;
|
|
extern SvgIcon g_icon_align_left;
|
|
extern SvgIcon g_icon_align_center;
|
|
extern SvgIcon g_icon_align_right;
|
|
extern SvgIcon g_icon_list_bullet;
|
|
extern SvgIcon g_icon_list_number;
|
|
extern SvgIcon g_icon_indent_less;
|
|
extern SvgIcon g_icon_indent_more;
|
|
extern TrueTypeFont* g_ui_font;
|
|
extern TrueTypeFont* g_ui_bold;
|
|
|
|
void wp_load_fonts();
|
|
void wp_load_icons();
|
|
void wp_init_paragraph_style(ParagraphStyle* para);
|
|
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);
|
|
int wp_find_paragraph_at(WordProcessorState* wp, int abs_pos);
|
|
void wp_selected_paragraph_range(WordProcessorState* wp, int* out_start_para, int* out_end_para);
|
|
|
|
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_apply_alignment(WordProcessorState* wp, uint8_t align);
|
|
void wp_adjust_paragraph_indent(WordProcessorState* wp, int delta);
|
|
void wp_adjust_paragraph_first_line_indent(WordProcessorState* wp, int delta);
|
|
void wp_adjust_paragraph_spacing_before(WordProcessorState* wp, int delta);
|
|
void wp_adjust_paragraph_spacing_after(WordProcessorState* wp, int delta);
|
|
void wp_cycle_line_spacing(WordProcessorState* wp);
|
|
void wp_set_line_spacing(WordProcessorState* wp, int value);
|
|
void wp_toggle_list(WordProcessorState* wp, uint8_t list_type);
|
|
void wp_insert_divider(WordProcessorState* wp, uint8_t divider_type);
|
|
void wp_delete_selection(WordProcessorState* wp);
|
|
char* wp_selection_plain_text_alloc(WordProcessorState* wp, int* out_len);
|
|
bool wp_insert_text(WordProcessorState* wp, const char* text, int len);
|
|
bool wp_replace_selection_with_text(WordProcessorState* wp, const char* text, int len);
|
|
|
|
void wp_recompute_wrap(WordProcessorState* wp, int content_w, int layout_dpi);
|
|
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_start_pathbar(WordProcessorState* wp, bool save_mode, const char* initial_text);
|
|
void wp_open_save_pathbar(WordProcessorState* wp);
|
|
void wp_save_file(WordProcessorState* wp);
|
|
void wp_load_file(WordProcessorState* wp, const char* path);
|
|
|
|
void wp_history_reset(WordProcessorState* wp);
|
|
void wp_history_checkpoint(WordProcessorState* wp);
|
|
void wp_history_mark_saved(WordProcessorState* wp);
|
|
bool wp_undo(WordProcessorState* wp);
|
|
bool wp_redo(WordProcessorState* wp);
|
|
|
|
bool wp_print_document(WordProcessorState* wp, char* out_status, int out_status_len);
|
|
bool wp_export_pdf_document(WordProcessorState* wp, const char* path, char* out_status, int out_status_len);
|
|
|
|
void wp_render();
|
|
void wp_handle_mouse(const Montauk::WinEvent& ev);
|
|
void wp_handle_key(const Montauk::KeyEvent& key);
|