/* * wordprocessor.hpp * Shared declarations for the Word Processor app * Copyright (c) 2026 Daniel Hammer */ #pragma once #include #include #include #include #include #include #include extern "C" { #include #include } 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);