feat: add A4 page option to Word Processor
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user