670 lines
28 KiB
C++
670 lines
28 KiB
C++
/*
|
|
* render.cpp
|
|
* Rendering for the standalone MontaukOS Word Processor
|
|
* Copyright (c) 2026 Daniel Hammer
|
|
*/
|
|
|
|
#include "wordprocessor.hpp"
|
|
|
|
static int wp_ui_line_height() {
|
|
if (g_ui_font && g_ui_font->valid)
|
|
return g_ui_font->get_line_height(fonts::UI_SIZE);
|
|
return fonts::UI_SIZE;
|
|
}
|
|
|
|
static void wp_draw_ui_text(Canvas& c, int x, int y, const char* text, Color color) {
|
|
draw_text(c, g_ui_font, x, y, text, color, fonts::UI_SIZE);
|
|
}
|
|
|
|
static void wp_draw_ui_button(Canvas& c, int x, int y, int w, int h,
|
|
const char* label, Color bg, Color fg, int radius = 3) {
|
|
c.fill_rounded_rect(x, y, w, h, radius, bg);
|
|
int tw = text_width(g_ui_font, label, fonts::UI_SIZE);
|
|
int th = wp_ui_line_height();
|
|
draw_text(c, g_ui_font, x + (w - tw) / 2, y + (h - th) / 2, label, fg, fonts::UI_SIZE);
|
|
}
|
|
|
|
static void wp_draw_ui_icon_button(Canvas& c, int x, int y, int w, int h,
|
|
const SvgIcon& icon, const char* fallback,
|
|
Color bg, Color fg, int radius = 3) {
|
|
c.fill_rounded_rect(x, y, w, h, radius, bg);
|
|
if (icon.pixels) {
|
|
c.icon(x + (w - icon.width) / 2, y + (h - icon.height) / 2, icon);
|
|
return;
|
|
}
|
|
wp_draw_ui_button(c, x, y, w, h, fallback, bg, fg, radius);
|
|
}
|
|
|
|
static int wp_special_char_flyout_x() {
|
|
int dx = WP_BTN_SECTION_X + 24 - WP_SPECIAL_CHAR_FLYOUT_W;
|
|
if (dx + WP_SPECIAL_CHAR_FLYOUT_W > g_win_w) dx = g_win_w - WP_SPECIAL_CHAR_FLYOUT_W;
|
|
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';
|
|
return;
|
|
}
|
|
|
|
montauk::strncpy(out, src, out_len - 1);
|
|
out[out_len - 1] = '\0';
|
|
if (text_width(g_ui_font, out, fonts::UI_SIZE) <= max_w) return;
|
|
|
|
int slen = montauk::slen(src);
|
|
for (int keep = slen; keep > 1; keep--) {
|
|
char candidate[256];
|
|
candidate[0] = '.';
|
|
candidate[1] = '.';
|
|
int pos = 2;
|
|
const char* tail = src + (slen - keep);
|
|
while (*tail && pos < (int)sizeof(candidate) - 1)
|
|
candidate[pos++] = *tail++;
|
|
candidate[pos] = '\0';
|
|
if (text_width(g_ui_font, candidate, fonts::UI_SIZE) <= max_w) {
|
|
montauk::strncpy(out, candidate, out_len - 1);
|
|
out[out_len - 1] = '\0';
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
static ParagraphStyle* wp_current_paragraph_style(WordProcessorState* wp) {
|
|
if (wp->paragraph_count <= 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));
|
|
if (para < 0) para = 0;
|
|
if (para >= wp->paragraph_count) para = wp->paragraph_count - 1;
|
|
return &wp->paragraphs[para];
|
|
}
|
|
|
|
static const char* wp_align_label(uint8_t align) {
|
|
switch (align) {
|
|
case PARA_ALIGN_CENTER: return "Center";
|
|
case PARA_ALIGN_RIGHT: return "Right";
|
|
default: return "Left";
|
|
}
|
|
}
|
|
|
|
static const char* wp_list_label(uint8_t list_type) {
|
|
switch (list_type) {
|
|
case PARA_LIST_BULLET: return "Bullets";
|
|
case PARA_LIST_NUMBER: return "Numbers";
|
|
default: return "Plain";
|
|
}
|
|
}
|
|
|
|
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, 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 = 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);
|
|
StyledRun* run = (wl->run_idx >= 0 && wl->run_idx < wp->run_count) ? &wp->runs[wl->run_idx] : nullptr;
|
|
int run_size = run ? run->size : WP_DEFAULT_SIZE;
|
|
int bullet_size = wp_screen_font_pixels(run_size);
|
|
TrueTypeFont* font = run ? wp_get_font(run->font_id, 0) : g_ui_font;
|
|
if (!font) font = g_ui_font;
|
|
int th = text_height(font, bullet_size);
|
|
int top_y = py + (wl->height - th) / 2;
|
|
char bullet[2] = { '\x95', '\0' };
|
|
draw_text(c, font, bullet_x, top_y, bullet, colors::TEXT_COLOR, bullet_size);
|
|
return;
|
|
}
|
|
|
|
char label[16];
|
|
snprintf(label, sizeof(label), "%d.", wl->list_number > 0 ? wl->list_number : 1);
|
|
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 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() {
|
|
if (!g_win.pixels) return;
|
|
|
|
WordProcessorState* wp = &g_wp;
|
|
Canvas c = g_win.canvas();
|
|
c.fill(colors::WINDOW_BG);
|
|
|
|
int sfh = wp_ui_line_height();
|
|
int cursor_abs = wp_abs_pos(wp, wp->cursor_run, wp->cursor_offset);
|
|
ParagraphStyle* cur_para = wp_current_paragraph_style(wp);
|
|
Color toolbar_bg = Color::from_rgb(0xF5, 0xF5, 0xF5);
|
|
Color btn_bg = Color::from_rgb(0xE8, 0xE8, 0xE8);
|
|
Color btn_active = Color::from_rgb(0xC0, 0xD0, 0xE8);
|
|
|
|
c.fill_rect(0, 0, c.w, WP_TOOLBAR_H, toolbar_bg);
|
|
|
|
c.fill_rounded_rect(WP_BTN_OPEN_X, 6, 24, 24, 3, btn_bg);
|
|
if (g_icon_folder.pixels)
|
|
c.icon(WP_BTN_OPEN_X + 4, 10, g_icon_folder);
|
|
|
|
c.fill_rounded_rect(WP_BTN_SAVE_X, 6, 24, 24, 3, btn_bg);
|
|
if (g_icon_save.pixels)
|
|
c.icon(WP_BTN_SAVE_X + 4, 10, g_icon_save);
|
|
|
|
c.fill_rounded_rect(WP_BTN_PRINT_X, 6, 24, 24, 3, btn_bg);
|
|
if (g_icon_print.pixels)
|
|
c.icon(WP_BTN_PRINT_X + 4, 10, g_icon_print);
|
|
|
|
wp_draw_ui_icon_button(c, WP_BTN_EXPORT_X, 6, 24, 24, g_icon_export_pdf, "P",
|
|
btn_bg, colors::TEXT_COLOR);
|
|
|
|
c.vline(WP_BTN_EXPORT_X + 28, 4, 28, colors::BORDER);
|
|
|
|
wp_draw_ui_icon_button(c, WP_BTN_UNDO_X, 6, 24, 24, g_icon_undo, "U", btn_bg, colors::TEXT_COLOR);
|
|
wp_draw_ui_icon_button(c, WP_BTN_REDO_X, 6, 24, 24, g_icon_redo, "R", btn_bg, colors::TEXT_COLOR);
|
|
|
|
c.vline(WP_BTN_REDO_X + 28, 4, 28, colors::BORDER);
|
|
|
|
Color bold_bg = (wp->cur_flags & STYLE_BOLD) ? btn_active : btn_bg;
|
|
c.fill_rounded_rect(WP_BTN_BOLD_X, 6, 24, 24, 3, bold_bg);
|
|
draw_text(c, g_ui_bold ? g_ui_bold : g_ui_font, WP_BTN_BOLD_X + 7, 8, "B", colors::TEXT_COLOR, fonts::UI_SIZE);
|
|
|
|
Color italic_bg = (wp->cur_flags & STYLE_ITALIC) ? btn_active : btn_bg;
|
|
c.fill_rounded_rect(WP_BTN_ITALIC_X, 6, 24, 24, 3, italic_bg);
|
|
{
|
|
TrueTypeFont* italic_font = wp_get_font(FONT_ROBOTO, STYLE_ITALIC);
|
|
draw_text(c, italic_font ? italic_font : g_ui_font, WP_BTN_ITALIC_X + 9, 8, "I", colors::TEXT_COLOR, fonts::UI_SIZE);
|
|
}
|
|
|
|
c.fill_rounded_rect(WP_FONT_DD_X, 6, WP_FONT_DD_W, 24, 3, btn_bg);
|
|
wp_draw_ui_text(c, WP_FONT_DD_X + 6, (WP_TOOLBAR_H - sfh) / 2,
|
|
WP_FONT_NAMES[wp->cur_font_id < FONT_COUNT ? wp->cur_font_id : 0],
|
|
colors::TEXT_COLOR);
|
|
|
|
c.fill_rounded_rect(WP_SIZE_DD_X, 6, WP_SIZE_DD_W, 24, 3, btn_bg);
|
|
{
|
|
char sz[8];
|
|
snprintf(sz, sizeof(sz), "%d", (int)wp->cur_size);
|
|
wp_draw_ui_text(c, WP_SIZE_DD_X + 8, (WP_TOOLBAR_H - sfh) / 2, sz, colors::TEXT_COLOR);
|
|
}
|
|
|
|
c.vline(WP_SIZE_DD_X + WP_SIZE_DD_W + 6, 4, 28, colors::BORDER);
|
|
|
|
wp_draw_ui_icon_button(c, WP_BTN_ALIGN_L_X, 6, 24, 24, g_icon_align_left, "L",
|
|
cur_para->align == PARA_ALIGN_LEFT ? btn_active : btn_bg, colors::TEXT_COLOR);
|
|
wp_draw_ui_icon_button(c, WP_BTN_ALIGN_C_X, 6, 24, 24, g_icon_align_center, "C",
|
|
cur_para->align == PARA_ALIGN_CENTER ? btn_active : btn_bg, colors::TEXT_COLOR);
|
|
wp_draw_ui_icon_button(c, WP_BTN_ALIGN_R_X, 6, 24, 24, g_icon_align_right, "R",
|
|
cur_para->align == PARA_ALIGN_RIGHT ? btn_active : btn_bg, colors::TEXT_COLOR);
|
|
c.vline(WP_BTN_ALIGN_R_X + 28, 4, 28, colors::BORDER);
|
|
wp_draw_ui_icon_button(c, WP_BTN_BULLET_X, 6, 24, 24, g_icon_list_bullet, "*",
|
|
cur_para->list_type == PARA_LIST_BULLET ? btn_active : btn_bg, colors::TEXT_COLOR);
|
|
wp_draw_ui_icon_button(c, WP_BTN_NUMBER_X, 6, 24, 24, g_icon_list_number, "1.",
|
|
cur_para->list_type == PARA_LIST_NUMBER ? btn_active : btn_bg, colors::TEXT_COLOR);
|
|
c.vline(WP_BTN_NUMBER_X + 28, 4, 28, colors::BORDER);
|
|
wp_draw_ui_icon_button(c, WP_BTN_OUTDENT_X, 6, 24, 24, g_icon_indent_less, "<", btn_bg, colors::TEXT_COLOR);
|
|
wp_draw_ui_icon_button(c, WP_BTN_INDENT_X, 6, 24, 24, g_icon_indent_more, ">", btn_bg, colors::TEXT_COLOR);
|
|
c.vline(WP_BTN_INDENT_X + 28, 4, 28, colors::BORDER);
|
|
|
|
{
|
|
char spacing[12];
|
|
snprintf(spacing, sizeof(spacing), "%d%%", (int)cur_para->line_spacing);
|
|
c.fill_rounded_rect(WP_LINE_DD_X, 6, WP_LINE_DD_W, 24, 3, btn_bg);
|
|
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;
|
|
c.fill_rounded_rect(WP_BTN_SECTION_X, 6, 24, 24, 3, special_bg);
|
|
{
|
|
char section[2] = { (char)0xA7, '\0' };
|
|
TrueTypeFont* font = wp_get_font(FONT_ROBOTO, 0);
|
|
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);
|
|
wp_draw_ui_icon_button(c, WP_BTN_PAGEVIEW_X, 6, 24, 24, g_icon_page_view, "\xA7",
|
|
pageview_bg, colors::TEXT_COLOR);
|
|
|
|
c.hline(0, WP_TOOLBAR_H - 1, c.w, colors::BORDER);
|
|
|
|
int edit_y = WP_TOOLBAR_H;
|
|
if (wp->show_pathbar) {
|
|
int pb_y = WP_TOOLBAR_H;
|
|
c.fill_rect(0, pb_y, c.w, WP_PATHBAR_H, Color::from_rgb(0xF0, 0xF0, 0xF0));
|
|
|
|
int inp_x = 8;
|
|
int inp_y = pb_y + 4;
|
|
int btn_w = 56;
|
|
int inp_w = c.w - inp_x - btn_w - 12;
|
|
int inp_h = 24;
|
|
|
|
c.fill_rect(inp_x, inp_y, inp_w, inp_h, colors::WHITE);
|
|
c.rect(inp_x, inp_y, inp_w, inp_h, colors::ACCENT);
|
|
|
|
int text_y = inp_y + (inp_h - sfh) / 2;
|
|
wp_draw_ui_text(c, inp_x + 4, text_y, wp->pathbar_text, colors::TEXT_COLOR);
|
|
|
|
char prefix[256];
|
|
int plen = wp->pathbar_cursor;
|
|
if (plen > 255) plen = 255;
|
|
for (int i = 0; i < plen; i++) prefix[i] = wp->pathbar_text[i];
|
|
prefix[plen] = '\0';
|
|
int cx = inp_x + 4 + text_width(g_ui_font, prefix, fonts::UI_SIZE);
|
|
c.fill_rect(cx, inp_y + 3, 2, inp_h - 6, colors::ACCENT);
|
|
|
|
int btn_x = inp_x + inp_w + 6;
|
|
wp_draw_ui_button(c, btn_x, inp_y, btn_w, inp_h,
|
|
wp->pathbar_save_mode ? "Save" : "Open",
|
|
colors::ACCENT, colors::WHITE);
|
|
|
|
c.hline(0, pb_y + WP_PATHBAR_H - 1, c.w, colors::BORDER);
|
|
edit_y = WP_TOOLBAR_H + WP_PATHBAR_H;
|
|
}
|
|
|
|
int text_area_h = c.h - edit_y - WP_STATUS_H;
|
|
|
|
// 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;
|
|
if (wp->scrollbar.scroll_offset < 0) wp->scrollbar.scroll_offset = 0;
|
|
}
|
|
|
|
wp_ensure_cursor_visible(wp, text_area_h);
|
|
|
|
int scroll_y = wp->scrollbar.scroll_offset;
|
|
int sel_s = 0;
|
|
int sel_e = 0;
|
|
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) {
|
|
// Gray background behind page (same style as PDF viewer)
|
|
Color page_bg = Color::from_rgb(0x80, 0x80, 0x80);
|
|
c.fill_rect(0, edit_y, c.w, text_area_h, page_bg);
|
|
|
|
// Simple offset shadow (same style as PDF viewer)
|
|
Color shadow_col = Color::from_rgb(0x60, 0x60, 0x60);
|
|
c.fill_rect(page_x + 3, page_y + 3, WP_A4_PAGE_W, WP_A4_PAGE_H, 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;
|
|
|
|
// 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, 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 = draw_x;
|
|
int line_abs_start = wp_wrap_line_start(wp, li);
|
|
int char_idx = 0;
|
|
|
|
while (chars_left > 0 && ri < wp->run_count) {
|
|
StyledRun* r = &wp->runs[ri];
|
|
TrueTypeFont* font = wp_get_font(r->font_id, r->flags);
|
|
if (!font || !font->valid) {
|
|
ri++;
|
|
ro = 0;
|
|
continue;
|
|
}
|
|
|
|
GlyphCache* gc = font->get_cache(wp_screen_font_pixels(r->size));
|
|
int baseline = py + wl->baseline;
|
|
|
|
int avail = r->len - ro;
|
|
int to_draw = avail < chars_left ? avail : chars_left;
|
|
|
|
for (int ci = 0; ci < to_draw; ci++) {
|
|
char ch = r->text[ro + ci];
|
|
int abs_ch = line_abs_start + char_idx;
|
|
|
|
int char_adv = 0;
|
|
if (ch != '\n' && (ch >= 32 || ch < 0)) {
|
|
CachedGlyph* g = font->get_glyph(gc, (unsigned char)ch);
|
|
char_adv = g ? g->advance : 8;
|
|
}
|
|
|
|
if (wp->has_selection && abs_ch >= sel_s && abs_ch < sel_e) {
|
|
int sel_w = char_adv > 0 ? char_adv : 6;
|
|
// 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;
|
|
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)) {
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
char_idx++;
|
|
}
|
|
|
|
chars_left -= to_draw;
|
|
ro += to_draw;
|
|
if (ro >= r->len) {
|
|
ri++;
|
|
ro = 0;
|
|
}
|
|
}
|
|
|
|
if (line_abs_start + char_idx == cursor_abs && li == wp->wrap_line_count - 1) {
|
|
int cur_h = wl->height;
|
|
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);
|
|
}
|
|
}
|
|
|
|
if (wp->scrollbar.content_height > wp->scrollbar.view_height) {
|
|
Color sb_fg = (wp->scrollbar.hovered || wp->scrollbar.dragging)
|
|
? wp->scrollbar.hover_fg : wp->scrollbar.fg;
|
|
int sbx = wp->scrollbar.bounds.x;
|
|
int sby = wp->scrollbar.bounds.y;
|
|
int sbw = wp->scrollbar.bounds.w;
|
|
int sbh = wp->scrollbar.bounds.h;
|
|
c.fill_rect(sbx, sby, sbw, sbh, colors::SCROLLBAR_BG);
|
|
int th = wp->scrollbar.thumb_height();
|
|
int tty = wp->scrollbar.thumb_y();
|
|
c.fill_rect(sbx + 1, tty, sbw - 2, th, sb_fg);
|
|
}
|
|
|
|
int status_y = c.h - WP_STATUS_H;
|
|
c.fill_rect(0, status_y, c.w, WP_STATUS_H, Color::from_rgb(0x2B, 0x3E, 0x50));
|
|
int status_text_y = status_y + (WP_STATUS_H - sfh) / 2;
|
|
|
|
char status_left[320];
|
|
const char* name = wp->filename[0] ? wp->filename : "Untitled";
|
|
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);
|
|
int sr_w = text_width(g_ui_font, status_right, fonts::UI_SIZE);
|
|
char status_combined[320];
|
|
if (wp->status_msg[0]) snprintf(status_combined, sizeof(status_combined), "%s | %s", status_left, wp->status_msg);
|
|
else montauk::strncpy(status_combined, status_left, (int)sizeof(status_combined) - 1);
|
|
status_combined[sizeof(status_combined) - 1] = '\0';
|
|
|
|
char status_fit[320];
|
|
wp_fit_ui_text(status_combined, status_fit, sizeof(status_fit), c.w - sr_w - 12);
|
|
wp_draw_ui_text(c, 4, status_text_y, status_fit, colors::PANEL_TEXT);
|
|
wp_draw_ui_text(c, c.w - sr_w - 4, status_text_y, status_right, colors::PANEL_TEXT);
|
|
|
|
if (wp->font_dropdown_open) {
|
|
int dx = WP_FONT_DD_X;
|
|
int dy = WP_TOOLBAR_H;
|
|
int dw = 110;
|
|
int dh = FONT_COUNT * 26 + 4;
|
|
c.fill_rect(dx, dy, dw, dh, colors::MENU_BG);
|
|
c.rect(dx, dy, dw, dh, colors::BORDER);
|
|
for (int i = 0; i < FONT_COUNT; i++) {
|
|
int iy = dy + 2 + i * 26;
|
|
if (i == wp->cur_font_id)
|
|
c.fill_rect(dx + 2, iy, dw - 4, 24, colors::MENU_HOVER);
|
|
wp_draw_ui_text(c, dx + 8, iy + (24 - sfh) / 2, WP_FONT_NAMES[i], colors::TEXT_COLOR);
|
|
}
|
|
}
|
|
|
|
if (wp->size_dropdown_open) {
|
|
int dx = WP_SIZE_DD_X;
|
|
int dy = WP_TOOLBAR_H;
|
|
int dw = 56;
|
|
int dh = WP_SIZE_OPTION_COUNT * 26 + 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_SIZE_OPTION_COUNT; i++) {
|
|
int iy = dy + 2 + i * 26;
|
|
if (WP_SIZE_OPTIONS[i] == wp->cur_size)
|
|
c.fill_rect(dx + 2, iy, dw - 4, 24, colors::MENU_HOVER);
|
|
char sz[8];
|
|
snprintf(sz, sizeof(sz), "%d", WP_SIZE_OPTIONS[i]);
|
|
wp_draw_ui_text(c, dx + 8, iy + (24 - sfh) / 2, sz, colors::TEXT_COLOR);
|
|
}
|
|
}
|
|
|
|
if (wp->line_spacing_dropdown_open) {
|
|
int dx = WP_LINE_DD_X;
|
|
int dy = WP_TOOLBAR_H;
|
|
int dw = 64;
|
|
int dh = WP_LINE_SPACING_OPTION_COUNT * 26 + 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_LINE_SPACING_OPTION_COUNT; i++) {
|
|
int iy = dy + 2 + i * 26;
|
|
if (WP_LINE_SPACING_OPTIONS[i] == cur_para->line_spacing)
|
|
c.fill_rect(dx + 2, iy, dw - 4, 24, colors::MENU_HOVER);
|
|
char spacing[12];
|
|
snprintf(spacing, sizeof(spacing), "%d%%", WP_LINE_SPACING_OPTIONS[i]);
|
|
wp_draw_ui_text(c, dx + 8, iy + (24 - sfh) / 2, spacing, colors::TEXT_COLOR);
|
|
}
|
|
}
|
|
|
|
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;
|
|
int dw = WP_SPECIAL_CHAR_FLYOUT_W;
|
|
int dh = WP_SPECIAL_CHAR_OPTION_COUNT * WP_SPECIAL_CHAR_ROW_H + 4;
|
|
TrueTypeFont* glyph_font = wp_get_font(FONT_ROBOTO, 0);
|
|
c.fill_rect(dx, dy, dw, dh, colors::MENU_BG);
|
|
c.rect(dx, dy, dw, dh, colors::BORDER);
|
|
for (int i = 0; i < WP_SPECIAL_CHAR_OPTION_COUNT; i++) {
|
|
int iy = dy + 2 + i * WP_SPECIAL_CHAR_ROW_H;
|
|
char glyph[2] = { (char)WP_SPECIAL_CHAR_OPTIONS[i].code, '\0' };
|
|
c.fill_rect(dx + 2, iy, dw - 4, WP_SPECIAL_CHAR_ROW_H - 2, colors::MENU_BG);
|
|
draw_text(c, glyph_font ? glyph_font : g_ui_font,
|
|
dx + 8, iy + (WP_SPECIAL_CHAR_ROW_H - sfh) / 2,
|
|
glyph, colors::TEXT_COLOR, fonts::UI_SIZE);
|
|
wp_draw_ui_text(c, dx + 32, iy + (WP_SPECIAL_CHAR_ROW_H - sfh) / 2,
|
|
WP_SPECIAL_CHAR_OPTIONS[i].label, colors::TEXT_COLOR);
|
|
}
|
|
}
|
|
}
|