diff --git a/programs/src/wordprocessor/document.cpp b/programs/src/wordprocessor/document.cpp index 3d79867..3ebdb0e 100644 --- a/programs/src/wordprocessor/document.cpp +++ b/programs/src/wordprocessor/document.cpp @@ -542,6 +542,8 @@ void wp_init_empty_document(WordProcessorState* wp) { wp->line_spacing_dropdown_open = false; wp->divider_flyout_open = false; wp->special_char_flyout_open = false; + wp->page_view_mode = false; + wp->pending_page_view_toggle = false; wp->undo_count = 0; wp->undo_pos = 0; @@ -1685,6 +1687,8 @@ static bool wp_deserialize_document(WordProcessorState* wp, const uint8_t* buf, 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; } diff --git a/programs/src/wordprocessor/input.cpp b/programs/src/wordprocessor/input.cpp index c345fe3..099a38a 100644 --- a/programs/src/wordprocessor/input.cpp +++ b/programs/src/wordprocessor/input.cpp @@ -159,8 +159,13 @@ static void wp_insert_divider_choice(WordProcessorState* wp, uint8_t divider_typ wp_history_checkpoint(wp); } -static int wp_hit_test_text(WordProcessorState* wp, int local_x, int local_y, int edit_y) { +static int wp_hit_test_text(WordProcessorState* wp, int local_x, int local_y, int edit_y, + int page_off_x, int page_off_y) { int click_y = local_y - edit_y + wp->scrollbar.scroll_offset; + // In page view mode, subtract the page y offset + if (wp->page_view_mode) { + click_y = local_y - page_off_y + wp->scrollbar.scroll_offset; + } int target_line = wp->wrap_line_count - 1; for (int i = 0; i < wp->wrap_line_count; i++) { @@ -175,7 +180,12 @@ static int wp_hit_test_text(WordProcessorState* wp, int local_x, int local_y, in if (wl->divider_type != PARA_DIVIDER_NONE) return wp_wrap_line_start(wp, target_line); - int click_x = local_x - wl->x; + // In page view mode, subtract the page x offset from click x + int click_x = local_x; + if (wp->page_view_mode) { + click_x = local_x - page_off_x; + } + click_x -= wl->x; int ri = wl->run_idx; int ro = wl->run_offset; int chars_left = wl->char_count; @@ -226,6 +236,25 @@ void wp_handle_mouse(const Montauk::WinEvent& ev) { int edit_y = WP_TOOLBAR_H + (wp->show_pathbar ? WP_PATHBAR_H : 0); int text_area_h = g_win_h - edit_y - WP_STATUS_H; + // Handle pending page view toggle first (from toolbar button click) + if (wp->pending_page_view_toggle) { + wp->page_view_mode = !wp->page_view_mode; + wp->pending_page_view_toggle = false; + wp->wrap_dirty = true; + } + + // Compute page offsets for hit testing (same logic as wp_render) + int page_off_x = 0; + int page_off_y = edit_y; + int recompute_w = g_win_w; + if (wp->page_view_mode) { + page_off_x = (g_win_w - WP_A4_PAGE_W) / 2; + if (page_off_x < 0) page_off_x = 0; + page_off_y = edit_y + WP_PAGE_TOP_MARGIN; + // Use A4 page width for wrap computation (to get proper margins) + recompute_w = WP_A4_PAGE_W; + } + wp->scrollbar.handle_mouse(local_x, local_y, ev.mouse.buttons, ev.mouse.prev_buttons, ev.mouse.scroll); if (wp->font_dropdown_open && wp_left_pressed(ev.mouse.buttons, ev.mouse.prev_buttons)) { @@ -413,6 +442,11 @@ void wp_handle_mouse(const Montauk::WinEvent& ev) { wp->special_char_flyout_open = open; return; } + if (local_x >= WP_BTN_PAGEVIEW_X && local_x < WP_BTN_PAGEVIEW_X + 24 && local_y >= 6 && local_y < 30) { + wp_close_dropdowns(wp); + wp->pending_page_view_toggle = true; + return; + } return; } @@ -428,13 +462,13 @@ void wp_handle_mouse(const Montauk::WinEvent& ev) { return; } - wp_recompute_wrap(wp, g_win_w, WP_SCREEN_DPI); + wp_recompute_wrap(wp, recompute_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) { wp_close_dropdowns(wp); - int abs = wp_hit_test_text(wp, local_x, local_y, edit_y); + int abs = wp_hit_test_text(wp, local_x, local_y, edit_y, page_off_x, page_off_y); wp_pos_to_run(wp, abs, &wp->cursor_run, &wp->cursor_offset); wp->sel_anchor = abs; wp->sel_end = abs; @@ -444,7 +478,7 @@ void wp_handle_mouse(const Montauk::WinEvent& ev) { } if (wp->mouse_selecting && wp_left_held(ev.mouse.buttons) && local_y >= edit_y - 20) { - int abs = wp_hit_test_text(wp, local_x, local_y, edit_y); + int abs = wp_hit_test_text(wp, local_x, local_y, edit_y, page_off_x, page_off_y); wp->sel_end = abs; wp_pos_to_run(wp, abs, &wp->cursor_run, &wp->cursor_offset); wp->has_selection = (wp->sel_anchor != wp->sel_end); diff --git a/programs/src/wordprocessor/render.cpp b/programs/src/wordprocessor/render.cpp index 491e39d..683609e 100644 --- a/programs/src/wordprocessor/render.cpp +++ b/programs/src/wordprocessor/render.cpp @@ -152,15 +152,15 @@ static const char* wp_divider_label(uint8_t divider_type) { } } -static void wp_draw_list_marker(Canvas& c, WordProcessorState* wp, WrapLine* wl, int py) { +static void wp_draw_list_marker(Canvas& c, WordProcessorState* wp, WrapLine* wl, int py, int page_off_x) { 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->divider_type != PARA_DIVIDER_NONE || para->list_type == PARA_LIST_NONE) return; - 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; + int marker_x = page_off_x + WP_MARGIN + wp_scale_layout_units(para->left_indent + para->first_line_indent, WP_SCREEN_DPI); + if (marker_x < page_off_x + 4) marker_x = page_off_x + 4; if (para->list_type == PARA_LIST_BULLET) { int bullet_x = marker_x + wp_scale_layout_units(7, WP_SCREEN_DPI); @@ -293,6 +293,22 @@ void wp_render() { draw_text(c, font ? font : g_ui_font, WP_BTN_SECTION_X + 6, 8, section, colors::TEXT_COLOR, fonts::UI_SIZE); } + c.vline(WP_BTN_PAGEVIEW_X - 4, 4, 28, colors::BORDER); + + Color pageview_bg = wp->page_view_mode ? btn_active : btn_bg; + c.fill_rounded_rect(WP_BTN_PAGEVIEW_X, 6, 24, 24, 3, pageview_bg); + { + // Draw a small page icon: rectangle with lines + int px = WP_BTN_PAGEVIEW_X + 4; + int py = 9; + // Page outline + c.rect(px, py, 14, 18, colors::TEXT_COLOR); + // Lines inside page + c.hline(px + 2, py + 4, 10, colors::TEXT_COLOR); + c.hline(px + 2, py + 8, 10, colors::TEXT_COLOR); + c.hline(px + 2, py + 12, 6, colors::TEXT_COLOR); + } + c.hline(0, WP_TOOLBAR_H - 1, c.w, colors::BORDER); int edit_y = WP_TOOLBAR_H; @@ -330,12 +346,40 @@ void wp_render() { } int text_area_h = c.h - edit_y - WP_STATUS_H; - wp_recompute_wrap(wp, c.w, WP_SCREEN_DPI); + + // Handle pending page view toggle + if (wp->pending_page_view_toggle) { + wp->page_view_mode = !wp->page_view_mode; + wp->pending_page_view_toggle = false; + wp->wrap_dirty = true; + } + + // Page view mode: compute A4 page position and use A4 content width + int page_x = 0; + int page_y = edit_y; + int page_content_w = c.w; + bool in_page_view = wp->page_view_mode; + if (in_page_view) { + // Center A4 page horizontally in the window + page_x = (c.w - WP_A4_PAGE_W) / 2; + if (page_x < 0) page_x = 0; + // Page top with margin from edit area top + page_y = edit_y + WP_PAGE_TOP_MARGIN; + // Use full A4 page width for wrap computation to get proper A4 margins + // (WP_MARGIN is used for left margin, right margin comes from WP_A4_PAGE_W) + page_content_w = WP_A4_PAGE_W; + } + + wp_recompute_wrap(wp, page_content_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; wp->scrollbar.view_height = text_area_h; + // In page view mode, keep scrollbar at right edge but use text_area_h for proper thumb sizing + // The scrollbar position doesn't change - it stays at c.w - WP_SCROLLBAR_W + // But the content scrolls within the A4 page view + { int ms = wp->scrollbar.max_scroll(); if (wp->scrollbar.scroll_offset > ms) wp->scrollbar.scroll_offset = ms; @@ -350,21 +394,53 @@ void wp_render() { if (wp->has_selection) wp_sel_range(wp, &sel_s, &sel_e); Color sel_bg = Color::from_rgb(0xB0, 0xD0, 0xF0); + // In page view mode, draw the A4 page with shadow before text rendering + if (in_page_view) { + // Shadow strips around the page (4 sides + corners, drawn as solid dark rectangles) + int so = WP_PAGE_SHADOW_OFFSET; + Color shadow_col = Color::from_rgb(0x50, 0x50, 0x50); + // Top shadow (above page, full width) + c.fill_rect(page_x - so, page_y - so, WP_A4_PAGE_W + so * 2, so, shadow_col); + // Bottom shadow + c.fill_rect(page_x - so, page_y + WP_A4_PAGE_H, WP_A4_PAGE_W + so * 2, so, shadow_col); + // Left shadow (vertical strip, excluding corners already covered) + c.fill_rect(page_x - so, page_y, so, WP_A4_PAGE_H, shadow_col); + // Right shadow (vertical strip, excluding corners) + c.fill_rect(page_x + WP_A4_PAGE_W, page_y, so, WP_A4_PAGE_H, shadow_col); + // Corner pieces (top-left, top-right, bottom-left, bottom-right) + c.fill_rect(page_x - so, page_y - so, so, so, shadow_col); + c.fill_rect(page_x + WP_A4_PAGE_W, page_y - so, so, so, shadow_col); + c.fill_rect(page_x - so, page_y + WP_A4_PAGE_H, so, so, shadow_col); + c.fill_rect(page_x + WP_A4_PAGE_W, page_y + WP_A4_PAGE_H, so, so, shadow_col); + // White page background + c.fill_rect(page_x, page_y, WP_A4_PAGE_W, WP_A4_PAGE_H, colors::WHITE); + } + for (int li = 0; li < wp->wrap_line_count; li++) { WrapLine* wl = &wp->wrap_lines[li]; int py = edit_y + wl->y - scroll_y; + // In page view mode, offset y by page_y + if (in_page_view) { + py = page_y + wl->y - scroll_y; + } if (py + wl->height <= edit_y) continue; if (py >= edit_y + text_area_h) break; - wp_draw_list_marker(c, wp, wl, py); + // In page view mode, offset x by page_x for all drawing + int draw_x = wl->x; + if (in_page_view) { + draw_x = page_x + wl->x; + } + + wp_draw_list_marker(c, wp, wl, py, in_page_view ? page_x : 0); 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); + wp_draw_divider_sample(c, draw_x, py, wl->width, wl->height, wl->divider_type, colors::TEXT_COLOR); int chars_left = wl->char_count; int ri = wl->run_idx; int ro = wl->run_offset; - int x = wl->x; + int x = draw_x; int line_abs_start = wp_wrap_line_start(wp, li); int char_idx = 0; @@ -395,22 +471,56 @@ void wp_render() { if (wp->has_selection && abs_ch >= sel_s && abs_ch < sel_e) { int sel_w = char_adv > 0 ? char_adv : 6; - c.fill_rect(x, py, sel_w, wl->height, sel_bg); + // Clip selection to page bounds in page view mode + int sel_x = x; + int sel_y = py; + int sel_h = wl->height; + if (in_page_view) { + sel_x = gui_max(x, page_x); + int right = gui_min(x + sel_w, page_x + WP_A4_PAGE_W); + sel_w = right - sel_x; + sel_y = gui_max(py, page_y); + int bot = gui_min(py + sel_h, page_y + WP_A4_PAGE_H); + sel_h = bot - sel_y; + if (sel_w <= 0 || sel_h <= 0) sel_w = 0; + } + if (sel_w > 0) + c.fill_rect(sel_x, sel_y, sel_w, sel_h, sel_bg); } if (abs_ch == cursor_abs) { int cur_h = wl->height; - if (py >= edit_y && py + cur_h <= edit_y + text_area_h) - c.fill_rect(x, py, 2, cur_h, colors::ACCENT); + int cur_x = x; + int cur_y = py; + int cur_w = 2; + if (in_page_view) { + // Clip cursor to page bounds + if (cur_x < page_x) { cur_w = cur_x + cur_w - page_x; cur_x = page_x; } + if (cur_x + cur_w > page_x + WP_A4_PAGE_W) cur_w = page_x + WP_A4_PAGE_W - cur_x; + cur_y = gui_max(cur_y, page_y); + int bot = gui_min(cur_y + cur_h, page_y + WP_A4_PAGE_H); + cur_h = bot - cur_y; + if (cur_h <= 0 || cur_w <= 0) cur_x = -1; // hide + } + if (cur_x >= 0 && py >= edit_y && py + cur_h <= edit_y + text_area_h) + c.fill_rect(cur_x, cur_y, cur_w, cur_h, colors::ACCENT); } if (ch != '\n' && (ch >= 32 || ch < 0)) { - if (x < c.w - WP_SCROLLBAR_W - WP_MARGIN) { + // In page view mode, clip to page bounds + bool can_draw = (x < c.w - WP_SCROLLBAR_W - WP_MARGIN); + if (in_page_view) { + can_draw = (x + char_adv > page_x && x < page_x + WP_A4_PAGE_W && + py + wl->height > page_y && py < page_y + WP_A4_PAGE_H); + } + if (can_draw) { Color text_col = (wp->has_selection && abs_ch >= sel_s && abs_ch < sel_e) ? Color::from_rgb(0x10, 0x10, 0x10) : colors::TEXT_COLOR; int adv = font->draw_char_to_buffer( c.pixels, c.w, c.h, x, baseline, (unsigned char)ch, text_col, gc); x += adv; + } else { + x += char_adv; } } @@ -427,8 +537,18 @@ void wp_render() { if (line_abs_start + char_idx == cursor_abs && li == wp->wrap_line_count - 1) { int cur_h = wl->height; - if (py >= edit_y && py + cur_h <= edit_y + text_area_h) - c.fill_rect(x, py, 2, cur_h, colors::ACCENT); + int cur_x = x; + int cur_y = py; + if (in_page_view) { + if (cur_x < page_x) cur_x = page_x; + if (cur_x > page_x + WP_A4_PAGE_W) cur_x = -1; + cur_y = gui_max(cur_y, page_y); + int bot = gui_min(cur_y + cur_h, page_y + WP_A4_PAGE_H); + cur_h = bot - cur_y; + if (cur_h <= 0) cur_x = -1; + } + if (cur_x >= 0 && py >= edit_y && py + cur_h <= edit_y + text_area_h) + c.fill_rect(cur_x, cur_y, 2, cur_h, colors::ACCENT); } } diff --git a/programs/src/wordprocessor/wordprocessor.hpp b/programs/src/wordprocessor/wordprocessor.hpp index 778dce0..6c54c8c 100644 --- a/programs/src/wordprocessor/wordprocessor.hpp +++ b/programs/src/wordprocessor/wordprocessor.hpp @@ -28,6 +28,17 @@ 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; @@ -64,6 +75,7 @@ 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; @@ -358,6 +370,8 @@ struct WordProcessorState { 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;