From ee667e917b3073a39f682d6ff8104b9c7511a193 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Thu, 2 Apr 2026 16:41:34 +0200 Subject: [PATCH] feat: dividers in word processor, login screen improvements --- programs/include/gui/truetype.hpp | 44 +++- programs/src/login/login_render.cpp | 25 +- programs/src/login/login_wallpaper.cpp | 21 +- programs/src/login/main.cpp | 4 +- programs/src/wordprocessor/document.cpp | 263 ++++++++++++++++--- programs/src/wordprocessor/input.cpp | 59 ++++- programs/src/wordprocessor/print_export.cpp | 59 ++++- programs/src/wordprocessor/render.cpp | 124 ++++++++- programs/src/wordprocessor/wordprocessor.hpp | 67 ++++- 9 files changed, 574 insertions(+), 92 deletions(-) diff --git a/programs/include/gui/truetype.hpp b/programs/include/gui/truetype.hpp index 15df10f..f39eb85 100644 --- a/programs/include/gui/truetype.hpp +++ b/programs/include/gui/truetype.hpp @@ -41,6 +41,41 @@ namespace gui { +inline int decode_single_byte_codepoint(int codepoint) { + if (codepoint < 0 || codepoint > 255) return codepoint; + + switch (codepoint) { + case 0x80: return 0x20AC; // Euro Sign + case 0x82: return 0x201A; // Single Low-9 Quotation Mark + case 0x83: return 0x0192; // Latin Small Letter F With Hook + case 0x84: return 0x201E; // Double Low-9 Quotation Mark + case 0x85: return 0x2026; // Horizontal Ellipsis + case 0x86: return 0x2020; // Dagger + case 0x87: return 0x2021; // Double Dagger + case 0x88: return 0x02C6; // Modifier Letter Circumflex Accent + case 0x89: return 0x2030; // Per Mille Sign + case 0x8A: return 0x0160; // Latin Capital Letter S With Caron + case 0x8B: return 0x2039; // Single Left-Pointing Angle Quotation Mark + case 0x8C: return 0x0152; // Latin Capital Ligature OE + case 0x8E: return 0x017D; // Latin Capital Letter Z With Caron + case 0x91: return 0x2018; // Left Single Quotation Mark + case 0x92: return 0x2019; // Right Single Quotation Mark + case 0x93: return 0x201C; // Left Double Quotation Mark + case 0x94: return 0x201D; // Right Double Quotation Mark + case 0x95: return 0x2022; // Bullet + case 0x96: return 0x2013; // En Dash + case 0x97: return 0x2014; // Em Dash + case 0x98: return 0x02DC; // Small Tilde + case 0x99: return 0x2122; // Trade Mark Sign + case 0x9A: return 0x0161; // Latin Small Letter S With Caron + case 0x9B: return 0x203A; // Single Right-Pointing Angle Quotation Mark + case 0x9C: return 0x0153; // Latin Small Ligature OE + case 0x9E: return 0x017E; // Latin Small Letter Z With Caron + case 0x9F: return 0x0178; // Latin Capital Letter Y With Diaeresis + default: return codepoint; + } +} + struct CachedGlyph { uint8_t* bitmap; int width, height; @@ -50,7 +85,7 @@ struct CachedGlyph { }; struct GlyphCache { - CachedGlyph glyphs[256]; // Latin-1 Supplement (U+0080..U+00FF) included + CachedGlyph glyphs[256]; // Single-byte text with Windows-1252 punctuation mapping int pixel_size; float scale; int ascent, descent, line_gap; @@ -155,12 +190,13 @@ struct TrueTypeFont { if (g->loaded) return g; g->loaded = true; + int font_codepoint = decode_single_byte_codepoint(codepoint); int advance, lsb; - stbtt_GetCodepointHMetrics(&info, codepoint, &advance, &lsb); + stbtt_GetCodepointHMetrics(&info, font_codepoint, &advance, &lsb); g->advance = (int)(advance * gc->scale); int x0, y0, x1, y1; - stbtt_GetCodepointBitmapBox(&info, codepoint, gc->scale, gc->scale, + stbtt_GetCodepointBitmapBox(&info, font_codepoint, gc->scale, gc->scale, &x0, &y0, &x1, &y1); g->width = x1 - x0; g->height = y1 - y0; @@ -172,7 +208,7 @@ struct TrueTypeFont { if (!g->bitmap) return g; stbtt_MakeCodepointBitmap(&info, g->bitmap, g->width, g->height, - g->width, gc->scale, gc->scale, codepoint); + g->width, gc->scale, gc->scale, font_codepoint); } return g; diff --git a/programs/src/login/login_render.cpp b/programs/src/login/login_render.cpp index 621b95d..097b7f6 100644 --- a/programs/src/login/login_render.cpp +++ b/programs/src/login/login_render.cpp @@ -10,6 +10,23 @@ using namespace gui; namespace { +void draw_login_background(LoginState* ls) { + Framebuffer& fb = ls->fb; + if (!ls->has_wallpaper) { + fb.clear(BG_COLOR); + return; + } + + uint32_t* dst = fb.buffer(); + int pitch = fb.pitch(); + int row_bytes = ls->screen_w * (int)sizeof(uint32_t); + for (int y = 0; y < ls->screen_h; y++) { + uint32_t* dst_row = (uint32_t*)((uint8_t*)dst + y * pitch); + const uint32_t* src_row = ls->bg_wallpaper + y * ls->bg_wallpaper_w; + montauk::memcpy(dst_row, src_row, row_bytes); + } +} + Rect offset_rect(Rect rect, int dx, int dy) { rect.x += dx; rect.y += dy; @@ -295,13 +312,7 @@ void draw_login_screen(LoginState* ls) { LoginLayout lo = layout_login_screen(ls); int sfh = system_font_height(); - if (ls->has_wallpaper) { - fb.blit(0, 0, ls->bg_wallpaper_w, ls->bg_wallpaper_h, ls->bg_wallpaper); - fb.fill_rect_alpha(0, 0, ls->screen_w, ls->screen_h, - Color::from_rgba(0, 0, 0, 0x38)); - } else { - fb.clear(BG_COLOR); - } + draw_login_background(ls); draw_shadow(fb, lo.card.x, lo.card.y, lo.card.w, lo.card.h, 4, colors::SHADOW); fb.fill_rect(lo.card.x, lo.card.y, lo.card.w, lo.card.h, CARD_BG); diff --git a/programs/src/login/login_wallpaper.cpp b/programs/src/login/login_wallpaper.cpp index f13d8ed..5d697d5 100644 --- a/programs/src/login/login_wallpaper.cpp +++ b/programs/src/login/login_wallpaper.cpp @@ -6,6 +6,18 @@ #include "login.hpp" +namespace { + +constexpr uint32_t kLoginOverlayAlpha = 0x38; +constexpr uint32_t kLoginOverlayInvAlpha = 255 - kLoginOverlayAlpha; + +static uint8_t dim_component(uint8_t value) { + uint32_t scaled = kLoginOverlayInvAlpha * value; + return (uint8_t)((scaled + 1 + (scaled >> 8)) >> 8); +} + +} // namespace + bool load_login_wallpaper(LoginState* ls) { auto doc = montauk::config::load("desktop"); const char* wp = doc.get_string("wallpaper.path", ""); @@ -69,10 +81,13 @@ bool load_login_wallpaper(LoginState* ls) { if (sx < 0) sx = 0; if (sx >= img_w) sx = img_w - 1; int si = (sy * img_w + sx) * 3; + uint8_t r = dim_component(rgb[si]); + uint8_t g = dim_component(rgb[si + 1]); + uint8_t b = dim_component(rgb[si + 2]); scaled[y * dst_w + x] = 0xFF000000u - | ((uint32_t)rgb[si] << 16) - | ((uint32_t)rgb[si + 1] << 8) - | (uint32_t)rgb[si + 2]; + | ((uint32_t)r << 16) + | ((uint32_t)g << 8) + | (uint32_t)b; } } diff --git a/programs/src/login/main.cpp b/programs/src/login/main.cpp index 8b5f3ed..72d31fd 100644 --- a/programs/src/login/main.cpp +++ b/programs/src/login/main.cpp @@ -70,6 +70,7 @@ extern "C" void _start() { for (;;) { bool mouse_changed = false; bool key_changed = false; + bool fast_mouse_path = false; int prev_mouse_x = ls->mouse.x; int prev_mouse_y = ls->mouse.y; @@ -80,6 +81,7 @@ extern "C" void _start() { || ls->mouse.y != prev_mouse_y || ls->mouse.buttons != prev_mouse_buttons || ls->mouse.scrollDelta != 0; + fast_mouse_path = mouse_changed || ls->mouse.buttons != 0; while (montauk::is_key_available()) { Montauk::KeyEvent key; @@ -93,7 +95,7 @@ extern "C" void _start() { if (first_frame || mouse_changed || key_changed) { draw_login_screen(ls); first_frame = false; - montauk::sleep_ms(4); + montauk::sleep_ms(fast_mouse_path ? 1 : 4); } else { montauk::sleep_ms(16); } diff --git a/programs/src/wordprocessor/document.cpp b/programs/src/wordprocessor/document.cpp index ca6bd53..aeb5735 100644 --- a/programs/src/wordprocessor/document.cpp +++ b/programs/src/wordprocessor/document.cpp @@ -11,6 +11,10 @@ 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); @@ -44,18 +48,67 @@ void wp_init_paragraph_style(ParagraphStyle* para) { para->align = PARA_ALIGN_LEFT; para->list_type = PARA_LIST_NONE; para->line_spacing = 100; - para->_pad = 0; + para->divider_type = PARA_DIVIDER_NONE; para->left_indent = 0; para->first_line_indent = 0; para->space_before = 0; para->space_after = 0; } -static void wp_font_at(WordProcessorState* wp, int abs_pos, +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->cur_size) : nullptr; + *out_gc = (*out_font && (*out_font)->valid) + ? (*out_font)->get_cache(wp_points_to_pixels(wp->cur_size, layout_dpi)) + : nullptr; return; } @@ -65,7 +118,9 @@ static void wp_font_at(WordProcessorState* wp, int abs_pos, 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(run->size) : nullptr; + *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) { @@ -145,6 +200,7 @@ static void wp_on_insert_newline(WordProcessorState* wp, int 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); } @@ -154,42 +210,43 @@ static void wp_on_delete_newline(WordProcessorState* wp, int abs_pos) { wp_remove_paragraph_style(wp, para + 1); } -static void wp_default_line_metrics(WordProcessorState* wp, int abs_pos, int* out_height, int* out_ascent) { +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, &font, &gc); + 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->cur_size) : nullptr; + 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_DEFAULT_SIZE; - *out_ascent = WP_DEFAULT_SIZE; + *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, char ch) { +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, &font, &gc); - if (!font || !gc) return 8; + 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 : 8; + return g ? g->advance : wp_points_to_pixels(6, layout_dpi); } -static void wp_measure_line_range(WordProcessorState* wp, int start, int count, +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; @@ -198,11 +255,11 @@ static void wp_measure_line_range(WordProcessorState* wp, int start, int count, 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, ch); + width += wp_char_advance_at(wp, start + i, layout_dpi, ch); TrueTypeFont* font = nullptr; GlyphCache* gc = nullptr; - wp_font_at(wp, start + i, &font, &gc); + 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; @@ -210,7 +267,7 @@ static void wp_measure_line_range(WordProcessorState* wp, int start, int count, } if (height == 0) - wp_default_line_metrics(wp, start, &height, &ascent); + wp_default_line_metrics(wp, start, layout_dpi, &height, &ascent); *out_width = width; *out_height = height; @@ -222,13 +279,41 @@ static int wp_line_spacing_advance(int height, int spacing_percent) { return advance < height ? height : advance; } -static int wp_effective_text_indent(const ParagraphStyle* para, bool first_line) { +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 indent; + 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) { @@ -412,6 +497,7 @@ void wp_init_empty_document(WordProcessorState* wp) { 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]); @@ -433,6 +519,7 @@ void wp_init_empty_document(WordProcessorState* wp) { 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->undo_count = 0; @@ -531,6 +618,7 @@ void wp_insert_char(WordProcessorState* wp, char c) { 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)) { @@ -893,16 +981,23 @@ void wp_toggle_list(WordProcessorState* wp, uint8_t list_type) { 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; @@ -934,6 +1029,74 @@ void wp_toggle_list(WordProcessorState* wp, uint8_t list_type) { } } +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; @@ -952,13 +1115,14 @@ void wp_delete_selection(WordProcessorState* wp) { wp_clear_selection(wp); } -void wp_recompute_wrap(WordProcessorState* wp, int content_w) { +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) { + 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; @@ -991,7 +1155,8 @@ void wp_recompute_wrap(WordProcessorState* wp, int content_w) { para_content_end++; } - y += para->space_before; + 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; @@ -999,7 +1164,7 @@ void wp_recompute_wrap(WordProcessorState* wp, int content_w) { 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); + 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; @@ -1010,7 +1175,7 @@ void wp_recompute_wrap(WordProcessorState* wp, int content_w) { while (scan < para_content_end) { char ch = wp_char_at(wp, scan); - int cw = wp_char_advance_at(wp, scan, ch); + 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; @@ -1028,7 +1193,7 @@ void wp_recompute_wrap(WordProcessorState* wp, int content_w) { } if (scan == line_start && scan < para_content_end) { - width += wp_char_advance_at(wp, scan, wp_char_at(wp, scan)); + width += wp_char_advance_at(wp, scan, layout_dpi, wp_char_at(wp, scan)); scan++; } @@ -1039,7 +1204,8 @@ void wp_recompute_wrap(WordProcessorState* wp, int content_w) { int line_width = 0; int line_height = 0; int line_ascent = 0; - wp_measure_line_range(wp, line_start, char_count, &line_width, &line_height, &line_ascent); + 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); @@ -1050,6 +1216,7 @@ void wp_recompute_wrap(WordProcessorState* wp, int content_w) { 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; @@ -1075,25 +1242,35 @@ void wp_recompute_wrap(WordProcessorState* wp, int content_w) { wp_pos_to_run(wp, para_start, &line->run_idx, &line->run_offset); line->char_count = has_newline ? 1 : 0; line->y = y; - wp_default_line_metrics(wp, para_start, &line->height, &line->baseline); - line->width = 0; line->paragraph_idx = para_idx; - line->list_number = (para->list_type == PARA_LIST_NUMBER) ? list_number : 0; + 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); - 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; + 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 += para->space_after; + y += wp_scale_layout_units(para->space_after, layout_dpi); pos = para_content_end; if (has_newline) pos++; @@ -1112,11 +1289,12 @@ void wp_recompute_wrap(WordProcessorState* wp, int content_w) { line->run_offset = 0; line->char_count = 0; line->y = WP_MARGIN; - wp_default_line_metrics(wp, 0, &line->height, &line->baseline); + 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; @@ -1285,7 +1463,7 @@ static bool wp_serialize_document(WordProcessorState* wp, uint8_t** out_buf, int buf[off++] = para->align; buf[off++] = para->list_type; buf[off++] = para->line_spacing; - buf[off++] = 0; + 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); @@ -1403,7 +1581,7 @@ static bool wp_deserialize_document(WordProcessorState* wp, const uint8_t* buf, para->align = buf[off++]; para->list_type = buf[off++]; para->line_spacing = buf[off++]; - 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); @@ -1411,6 +1589,7 @@ static bool wp_deserialize_document(WordProcessorState* wp, const uint8_t* buf, 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++) { @@ -1433,6 +1612,8 @@ static bool wp_deserialize_document(WordProcessorState* wp, const uint8_t* buf, 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; @@ -1440,6 +1621,8 @@ static bool wp_deserialize_document(WordProcessorState* wp, const uint8_t* buf, 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; return true; } diff --git a/programs/src/wordprocessor/input.cpp b/programs/src/wordprocessor/input.cpp index 19c0178..c30ba94 100644 --- a/programs/src/wordprocessor/input.cpp +++ b/programs/src/wordprocessor/input.cpp @@ -46,10 +46,17 @@ static int wp_special_char_flyout_x() { return dx < 0 ? 0 : dx; } +static int wp_divider_flyout_x() { + int dx = WP_BTN_DIVIDER_X + 24 - WP_DIVIDER_FLYOUT_W; + if (dx + WP_DIVIDER_FLYOUT_W > g_win_w) dx = g_win_w - WP_DIVIDER_FLYOUT_W; + return dx < 0 ? 0 : dx; +} + static void wp_close_dropdowns(WordProcessorState* wp) { 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; } @@ -59,6 +66,11 @@ static void wp_insert_special_char(WordProcessorState* wp, uint8_t ch) { wp_history_checkpoint(wp); } +static void wp_insert_divider_choice(WordProcessorState* wp, uint8_t divider_type) { + wp_insert_divider(wp, divider_type); + wp_history_checkpoint(wp); +} + 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; @@ -72,6 +84,9 @@ static int wp_hit_test_text(WordProcessorState* wp, int local_x, int local_y, in } WrapLine* wl = &wp->wrap_lines[target_line]; + if (wl->divider_type != PARA_DIVIDER_NONE) + return wp_wrap_line_start(wp, target_line); + int click_x = local_x - wl->x; int ri = wl->run_idx; int ro = wl->run_offset; @@ -90,7 +105,7 @@ static int wp_hit_test_text(WordProcessorState* wp, int local_x, int local_y, in continue; } - GlyphCache* gc = font->get_cache(r->size); + GlyphCache* gc = font->get_cache(wp_screen_font_pixels(r->size)); int avail = r->len - ro; int to_check = avail < chars_left ? avail : chars_left; @@ -173,6 +188,19 @@ void wp_handle_mouse(const Montauk::WinEvent& ev) { return; } + if (wp->divider_flyout_open && wp_left_pressed(ev.mouse.buttons, ev.mouse.prev_buttons)) { + int dx = wp_divider_flyout_x(); + int dy = WP_TOOLBAR_H; + int dh = WP_DIVIDER_OPTION_COUNT * WP_DIVIDER_ROW_H + 4; + if (local_x >= dx && local_x < dx + WP_DIVIDER_FLYOUT_W && local_y >= dy && local_y < dy + dh) { + int idx = (local_y - dy - 2) / WP_DIVIDER_ROW_H; + if (idx >= 0 && idx < WP_DIVIDER_OPTION_COUNT) + wp_insert_divider_choice(wp, WP_DIVIDER_OPTIONS[idx].type); + } + wp->divider_flyout_open = false; + return; + } + if (wp->special_char_flyout_open && wp_left_pressed(ev.mouse.buttons, ev.mouse.prev_buttons)) { int dx = wp_special_char_flyout_x(); int dy = WP_TOOLBAR_H; @@ -222,15 +250,15 @@ void wp_handle_mouse(const Montauk::WinEvent& ev) { return; } if (local_x >= WP_FONT_DD_X && local_x < WP_FONT_DD_X + WP_FONT_DD_W && local_y >= 6 && local_y < 30) { - wp->font_dropdown_open = !wp->font_dropdown_open; - wp->size_dropdown_open = false; - wp->line_spacing_dropdown_open = false; + bool open = !wp->font_dropdown_open; + wp_close_dropdowns(wp); + wp->font_dropdown_open = open; return; } if (local_x >= WP_SIZE_DD_X && local_x < WP_SIZE_DD_X + WP_SIZE_DD_W && local_y >= 6 && local_y < 30) { - wp->size_dropdown_open = !wp->size_dropdown_open; - wp->font_dropdown_open = false; - wp->line_spacing_dropdown_open = false; + bool open = !wp->size_dropdown_open; + wp_close_dropdowns(wp); + wp->size_dropdown_open = open; return; } if (local_x >= WP_BTN_ALIGN_L_X && local_x < WP_BTN_ALIGN_L_X + 24 && local_y >= 6 && local_y < 30) { @@ -276,10 +304,15 @@ void wp_handle_mouse(const Montauk::WinEvent& ev) { return; } if (local_x >= WP_LINE_DD_X && local_x < WP_LINE_DD_X + WP_LINE_DD_W && local_y >= 6 && local_y < 30) { - wp->line_spacing_dropdown_open = !wp->line_spacing_dropdown_open; - wp->font_dropdown_open = false; - wp->size_dropdown_open = false; - wp->special_char_flyout_open = false; + bool open = !wp->line_spacing_dropdown_open; + wp_close_dropdowns(wp); + wp->line_spacing_dropdown_open = open; + return; + } + if (local_x >= WP_BTN_DIVIDER_X && local_x < WP_BTN_DIVIDER_X + 24 && local_y >= 6 && local_y < 30) { + bool open = !wp->divider_flyout_open; + wp_close_dropdowns(wp); + wp->divider_flyout_open = open; return; } if (local_x >= WP_BTN_SECTION_X && local_x < WP_BTN_SECTION_X + 24 && local_y >= 6 && local_y < 30) { @@ -303,7 +336,7 @@ void wp_handle_mouse(const Montauk::WinEvent& ev) { return; } - wp_recompute_wrap(wp, g_win_w); + wp_recompute_wrap(wp, g_win_w, WP_SCREEN_DPI); if (wp_left_pressed(ev.mouse.buttons, ev.mouse.prev_buttons) && local_y >= edit_y && local_y < edit_y + text_area_h) { @@ -382,7 +415,7 @@ void wp_handle_key(const Montauk::KeyEvent& key) { } wp_close_dropdowns(wp); - wp_recompute_wrap(wp, g_win_w); + wp_recompute_wrap(wp, g_win_w, WP_SCREEN_DPI); if (key.ctrl && (key.ascii == 's' || key.ascii == 'S')) { wp_save_file(wp); diff --git a/programs/src/wordprocessor/print_export.cpp b/programs/src/wordprocessor/print_export.cpp index 6c4046b..996fd55 100644 --- a/programs/src/wordprocessor/print_export.cpp +++ b/programs/src/wordprocessor/print_export.cpp @@ -64,6 +64,43 @@ static int wp_print_layout_width() { return (WP_PRINT_PAGE_W - WP_PRINT_MARGIN_X * 2) + WP_MARGIN * 2 + WP_SCROLLBAR_W; } +static void wp_draw_print_divider(Canvas& c, int x, int y, int w, int h, + uint8_t divider_type, Color color) { + if (w <= 0 || h <= 0 || divider_type == PARA_DIVIDER_NONE) return; + + int cy = y + h / 2; + switch (divider_type) { + case PARA_DIVIDER_SINGLE: + c.fill_rect(x, cy, w, 1, color); + break; + case PARA_DIVIDER_DOUBLE: + c.fill_rect(x, cy - 2, w, 1, color); + c.fill_rect(x, cy + 2, w, 1, color); + break; + case PARA_DIVIDER_DOTTED: + for (int dx = x; dx < x + w; dx += 5) + c.fill_rect(dx, cy, 2, 1, color); + break; + case PARA_DIVIDER_DASHED: + for (int dx = x; dx < x + w; dx += 11) { + int dash_w = (x + w - dx) < 7 ? (x + w - dx) : 7; + if (dash_w > 0) c.fill_rect(dx, cy, dash_w, 1, color); + } + break; + case PARA_DIVIDER_HEAVY: + c.fill_rect(x, cy - 1, w, 3, color); + break; + case PARA_DIVIDER_THIN_THICK: + c.fill_rect(x, cy - 3, w, 1, color); + c.fill_rect(x, cy, w, 3, color); + break; + case PARA_DIVIDER_THICK_THIN: + c.fill_rect(x, cy - 3, w, 3, color); + c.fill_rect(x, cy + 2, w, 1, color); + break; + } +} + static void wp_make_base_print_name(WordProcessorState* wp, char* out, int out_len) { const char* src = wp->filename[0] ? wp->filename : "document"; int last_dot = -1; @@ -109,7 +146,7 @@ static bool wp_build_print_page_map(WordProcessorState* wp, if (out_line_y) *out_line_y = nullptr; if (out_page_count) *out_page_count = 0; - wp_recompute_wrap(wp, wp_print_layout_width()); + wp_recompute_wrap(wp, wp_print_layout_width(), WP_PRINT_DPI); if (wp->wrap_line_count <= 0) { wp_status_copy(err, err_len, "failed to paginate document"); return false; @@ -164,14 +201,17 @@ static void wp_draw_print_list_marker(Canvas& c, WordProcessorState* wp, WrapLin return; ParagraphStyle* para = &wp->paragraphs[wl->paragraph_idx]; - if (para->list_type == PARA_LIST_NONE) return; + if (para->divider_type != PARA_DIVIDER_NONE || para->list_type == PARA_LIST_NONE) return; - int marker_x = WP_PRINT_MARGIN_X + para->left_indent + para->first_line_indent; + int marker_x = WP_PRINT_MARGIN_X + wp_scale_layout_units(para->left_indent + para->first_line_indent, WP_PRINT_DPI); int min_x = WP_PRINT_MARGIN_X - WP_MARGIN + 4; if (marker_x < min_x) marker_x = min_x; if (para->list_type == PARA_LIST_BULLET) { - fill_circle(c, marker_x + 7, py + wl->height / 2, 3, colors::BLACK); + int bullet_x = marker_x + wp_scale_layout_units(7, WP_PRINT_DPI); + int bullet_r = wp_scale_layout_units(3, WP_PRINT_DPI); + if (bullet_r < 2) bullet_r = 2; + fill_circle(c, bullet_x, py + wl->height / 2, bullet_r, colors::BLACK); return; } @@ -180,8 +220,9 @@ static void wp_draw_print_list_marker(Canvas& c, WordProcessorState* wp, WrapLin TrueTypeFont* font = wp_get_font(run->font_id, run->flags); char label[16]; snprintf(label, sizeof(label), "%d.", wl->list_number > 0 ? wl->list_number : 1); - int top_y = py + (wl->height - run->size) / 2; - draw_text(c, font ? font : g_ui_font, marker_x, top_y, label, colors::BLACK, run->size); + int font_px = wp_print_font_pixels(run->size); + int top_y = py + (wl->height - font_px) / 2; + draw_text(c, font ? font : g_ui_font, marker_x, top_y, label, colors::BLACK, font_px); } static bool wp_encode_print_page(WordProcessorState* wp, @@ -210,6 +251,10 @@ static bool wp_encode_print_page(WordProcessorState* wp, WrapLine* wl = &wp->wrap_lines[li]; int py = line_y[li]; wp_draw_print_list_marker(c, wp, wl, py); + if (wl->divider_type != PARA_DIVIDER_NONE) { + int divider_x = WP_PRINT_MARGIN_X + (wl->x - WP_MARGIN); + wp_draw_print_divider(c, divider_x, py, wl->width, wl->height, wl->divider_type, colors::BLACK); + } int chars_left = wl->char_count; int ri = wl->run_idx; @@ -228,7 +273,7 @@ static bool wp_encode_print_page(WordProcessorState* wp, continue; } - GlyphCache* gc = font->get_cache(r->size); + GlyphCache* gc = font->get_cache(wp_print_font_pixels(r->size)); int baseline = py + wl->baseline; int avail = r->len - ro; diff --git a/programs/src/wordprocessor/render.cpp b/programs/src/wordprocessor/render.cpp index d46bb3f..ff42ffd 100644 --- a/programs/src/wordprocessor/render.cpp +++ b/programs/src/wordprocessor/render.cpp @@ -41,6 +41,49 @@ static int wp_special_char_flyout_x() { return dx < 0 ? 0 : dx; } +static int wp_divider_flyout_x() { + int dx = WP_BTN_DIVIDER_X + 24 - WP_DIVIDER_FLYOUT_W; + if (dx + WP_DIVIDER_FLYOUT_W > g_win_w) dx = g_win_w - WP_DIVIDER_FLYOUT_W; + return dx < 0 ? 0 : dx; +} + +static void wp_draw_divider_sample(Canvas& c, int x, int y, int w, int h, + uint8_t divider_type, Color color) { + if (w <= 0 || h <= 0 || divider_type == PARA_DIVIDER_NONE) return; + + int cy = y + h / 2; + switch (divider_type) { + case PARA_DIVIDER_SINGLE: + c.fill_rect(x, cy, w, 1, color); + break; + case PARA_DIVIDER_DOUBLE: + c.fill_rect(x, cy - 2, w, 1, color); + c.fill_rect(x, cy + 2, w, 1, color); + break; + case PARA_DIVIDER_DOTTED: + for (int dx = x; dx < x + w; dx += 5) + c.fill_rect(dx, cy, 2, 1, color); + break; + case PARA_DIVIDER_DASHED: + for (int dx = x; dx < x + w; dx += 11) { + int dash_w = (x + w - dx) < 7 ? (x + w - dx) : 7; + if (dash_w > 0) c.fill_rect(dx, cy, dash_w, 1, color); + } + break; + case PARA_DIVIDER_HEAVY: + c.fill_rect(x, cy - 1, w, 3, color); + break; + case PARA_DIVIDER_THIN_THICK: + c.fill_rect(x, cy - 3, w, 1, color); + c.fill_rect(x, cy, w, 3, color); + break; + case PARA_DIVIDER_THICK_THIN: + c.fill_rect(x, cy - 3, w, 3, color); + c.fill_rect(x, cy + 2, w, 1, color); + break; + } +} + static void wp_fit_ui_text(const char* src, char* out, int out_len, int max_w) { if (!src || !src[0]) { if (out_len > 0) out[0] = '\0'; @@ -71,7 +114,7 @@ static void wp_fit_ui_text(const char* src, char* out, int out_len, int max_w) { static ParagraphStyle* wp_current_paragraph_style(WordProcessorState* wp) { if (wp->paragraph_count <= 0) { - static ParagraphStyle fallback = { PARA_ALIGN_LEFT, PARA_LIST_NONE, 100, 0, 0, 0, 0, 0 }; + static ParagraphStyle fallback = { PARA_ALIGN_LEFT, PARA_LIST_NONE, 100, PARA_DIVIDER_NONE, 0, 0, 0, 0 }; return &fallback; } int para = wp_find_paragraph_at(wp, wp_abs_pos(wp, wp->cursor_run, wp->cursor_offset)); @@ -96,18 +139,34 @@ static const char* wp_list_label(uint8_t list_type) { } } +static const char* wp_divider_label(uint8_t divider_type) { + switch (divider_type) { + case PARA_DIVIDER_SINGLE: return "Single"; + case PARA_DIVIDER_DOUBLE: return "Double"; + case PARA_DIVIDER_DOTTED: return "Dotted"; + case PARA_DIVIDER_DASHED: return "Dashed"; + case PARA_DIVIDER_HEAVY: return "Heavy"; + case PARA_DIVIDER_THIN_THICK: return "Thin/Thick"; + case PARA_DIVIDER_THICK_THIN: return "Thick/Thin"; + default: return "None"; + } +} + static void wp_draw_list_marker(Canvas& c, WordProcessorState* wp, WrapLine* wl, int py) { if (!wl->first_in_paragraph || wl->paragraph_idx < 0 || wl->paragraph_idx >= wp->paragraph_count) return; ParagraphStyle* para = &wp->paragraphs[wl->paragraph_idx]; - if (para->list_type == PARA_LIST_NONE) return; + if (para->divider_type != PARA_DIVIDER_NONE || para->list_type == PARA_LIST_NONE) return; - int marker_x = WP_MARGIN + para->left_indent + para->first_line_indent; + int marker_x = WP_MARGIN + wp_scale_layout_units(para->left_indent + para->first_line_indent, WP_SCREEN_DPI); if (marker_x < 4) marker_x = 4; if (para->list_type == PARA_LIST_BULLET) { - fill_circle(c, marker_x + 7, py + wl->height / 2, 3, colors::TEXT_COLOR); + int bullet_x = marker_x + wp_scale_layout_units(7, WP_SCREEN_DPI); + int bullet_r = wp_scale_layout_units(3, WP_SCREEN_DPI); + if (bullet_r < 2) bullet_r = 2; + fill_circle(c, bullet_x, py + wl->height / 2, bullet_r, colors::TEXT_COLOR); return; } @@ -116,8 +175,9 @@ static void wp_draw_list_marker(Canvas& c, WordProcessorState* wp, WrapLine* wl, if (wp->run_count <= 0) return; StyledRun* run = &wp->runs[wl->run_idx < wp->run_count ? wl->run_idx : 0]; TrueTypeFont* font = wp_get_font(run->font_id, run->flags); - int top_y = py + (wl->height - run->size) / 2; - draw_text(c, font ? font : g_ui_font, marker_x, top_y, label, colors::TEXT_COLOR, run->size); + int font_px = wp_screen_font_pixels(run->size); + int top_y = py + (wl->height - font_px) / 2; + draw_text(c, font ? font : g_ui_font, marker_x, top_y, label, colors::TEXT_COLOR, font_px); } void wp_render() { @@ -203,6 +263,17 @@ void wp_render() { wp_draw_ui_text(c, WP_LINE_DD_X + 8, (WP_TOOLBAR_H - sfh) / 2, spacing, colors::TEXT_COLOR); } + c.vline(WP_BTN_DIVIDER_X - 4, 4, 28, colors::BORDER); + + Color divider_bg = (wp->divider_flyout_open || cur_para->divider_type != PARA_DIVIDER_NONE) ? btn_active : btn_bg; + c.fill_rounded_rect(WP_BTN_DIVIDER_X, 6, 24, 24, 3, divider_bg); + uint8_t divider_preview = cur_para->divider_type != PARA_DIVIDER_NONE + ? cur_para->divider_type + : (uint8_t)PARA_DIVIDER_SINGLE; + wp_draw_divider_sample(c, WP_BTN_DIVIDER_X + 4, 6, 16, 24, + divider_preview, + colors::TEXT_COLOR); + c.vline(WP_BTN_SECTION_X - 4, 4, 28, colors::BORDER); Color special_bg = wp->special_char_flyout_open ? btn_active : btn_bg; @@ -250,7 +321,7 @@ void wp_render() { } int text_area_h = c.h - edit_y - WP_STATUS_H; - wp_recompute_wrap(wp, c.w); + wp_recompute_wrap(wp, c.w, WP_SCREEN_DPI); wp->scrollbar.bounds = {c.w - WP_SCROLLBAR_W, edit_y, WP_SCROLLBAR_W, text_area_h}; wp->scrollbar.content_height = wp->content_height; @@ -278,6 +349,8 @@ void wp_render() { if (py >= edit_y + text_area_h) break; wp_draw_list_marker(c, wp, wl, py); + if (wl->divider_type != PARA_DIVIDER_NONE) + wp_draw_divider_sample(c, wl->x, py, wl->width, wl->height, wl->divider_type, colors::TEXT_COLOR); int chars_left = wl->char_count; int ri = wl->run_idx; @@ -295,7 +368,7 @@ void wp_render() { continue; } - GlyphCache* gc = font->get_cache(r->size); + GlyphCache* gc = font->get_cache(wp_screen_font_pixels(r->size)); int baseline = py + wl->baseline; int avail = r->len - ro; @@ -369,11 +442,18 @@ void wp_render() { char status_left[320]; const char* name = wp->filename[0] ? wp->filename : "Untitled"; - snprintf(status_left, sizeof(status_left), " %s%s | %s %dpt | %s | %s | %d%%", - name, wp->modified ? " *" : "", - WP_FONT_NAMES[wp->cur_font_id < FONT_COUNT ? wp->cur_font_id : 0], (int)wp->cur_size, - wp_align_label(cur_para->align), wp_list_label(cur_para->list_type), - (int)cur_para->line_spacing); + if (cur_para->divider_type != PARA_DIVIDER_NONE) { + snprintf(status_left, sizeof(status_left), " %s%s | %s %dpt | Divider: %s | %d%%", + name, wp->modified ? " *" : "", + WP_FONT_NAMES[wp->cur_font_id < FONT_COUNT ? wp->cur_font_id : 0], (int)wp->cur_size, + wp_divider_label(cur_para->divider_type), (int)cur_para->line_spacing); + } else { + snprintf(status_left, sizeof(status_left), " %s%s | %s %dpt | %s | %s | %d%%", + name, wp->modified ? " *" : "", + WP_FONT_NAMES[wp->cur_font_id < FONT_COUNT ? wp->cur_font_id : 0], (int)wp->cur_size, + wp_align_label(cur_para->align), wp_list_label(cur_para->list_type), + (int)cur_para->line_spacing); + } char status_right[32]; snprintf(status_right, sizeof(status_right), "%d chars ", wp->total_text_len); @@ -437,6 +517,24 @@ void wp_render() { } } + if (wp->divider_flyout_open) { + int dx = wp_divider_flyout_x(); + int dy = WP_TOOLBAR_H; + int dw = WP_DIVIDER_FLYOUT_W; + int dh = WP_DIVIDER_OPTION_COUNT * WP_DIVIDER_ROW_H + 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_DIVIDER_OPTION_COUNT; i++) { + int iy = dy + 2 + i * WP_DIVIDER_ROW_H; + if (WP_DIVIDER_OPTIONS[i].type == cur_para->divider_type) + c.fill_rect(dx + 2, iy, dw - 4, WP_DIVIDER_ROW_H - 2, colors::MENU_HOVER); + wp_draw_divider_sample(c, dx + 10, iy, 56, WP_DIVIDER_ROW_H, + WP_DIVIDER_OPTIONS[i].type, colors::TEXT_COLOR); + wp_draw_ui_text(c, dx + 78, iy + (WP_DIVIDER_ROW_H - sfh) / 2, + WP_DIVIDER_OPTIONS[i].label, colors::TEXT_COLOR); + } + } + if (wp->special_char_flyout_open) { int dx = wp_special_char_flyout_x(); int dy = WP_TOOLBAR_H; diff --git a/programs/src/wordprocessor/wordprocessor.hpp b/programs/src/wordprocessor/wordprocessor.hpp index 004d20c..4292e9b 100644 --- a/programs/src/wordprocessor/wordprocessor.hpp +++ b/programs/src/wordprocessor/wordprocessor.hpp @@ -34,6 +34,8 @@ 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; @@ -59,7 +61,10 @@ static constexpr int WP_BTN_OUTDENT_X = 496; static constexpr int WP_BTN_INDENT_X = 524; static constexpr int WP_LINE_DD_X = 558; static constexpr int WP_LINE_DD_W = 48; -static constexpr int WP_BTN_SECTION_X = 614; +static constexpr int WP_BTN_DIVIDER_X = 614; +static constexpr int WP_BTN_SECTION_X = 646; +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; @@ -82,20 +87,48 @@ 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 = 6; +static constexpr int WP_SPECIAL_CHAR_OPTION_COUNT = 8; enum ParagraphAlign : uint8_t { PARA_ALIGN_LEFT = 0, @@ -109,6 +142,28 @@ enum ParagraphListType : uint8_t { 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; @@ -134,6 +189,7 @@ struct WrapLine { int width; int paragraph_idx; int list_number; + uint8_t divider_type; bool first_in_paragraph; }; @@ -141,7 +197,7 @@ struct ParagraphStyle { uint8_t align; uint8_t list_type; uint8_t line_spacing; - uint8_t _pad; + uint8_t divider_type; int16_t left_indent; int16_t first_line_indent; int16_t space_before; @@ -277,6 +333,7 @@ struct WordProcessorState { int wrap_line_cap; bool wrap_dirty; int last_wrap_width; + int last_wrap_dpi; ParagraphStyle paragraphs[WP_MAX_PARAGRAPHS]; int paragraph_count; @@ -298,6 +355,7 @@ struct WordProcessorState { bool font_dropdown_open; bool size_dropdown_open; bool line_spacing_dropdown_open; + bool divider_flyout_open; bool special_char_flyout_open; UndoSnapshot undo[WP_UNDO_MAX]; @@ -362,9 +420,10 @@ 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); -void wp_recompute_wrap(WordProcessorState* wp, int content_w); +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);