/* * 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 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 }; 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 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; int marker_x = WP_MARGIN + para->left_indent + para->first_line_indent; 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); 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 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); } 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.vline(WP_BTN_SAVE_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_SECTION_X - 4, 4, 28, colors::BORDER); c.fill_rounded_rect(WP_BTN_SECTION_X, 6, 24, 24, 3, btn_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.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; wp_recompute_wrap(wp, c.w); 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; { 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); for (int li = 0; li < wp->wrap_line_count; li++) { WrapLine* wl = &wp->wrap_lines[li]; int py = edit_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); int chars_left = wl->char_count; int ri = wl->run_idx; int ro = wl->run_offset; int x = wl->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(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; c.fill_rect(x, py, sel_w, wl->height, 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); } if (ch != '\n' && (ch >= 32 || ch < 0)) { if (x < c.w - WP_SCROLLBAR_W - WP_MARGIN) { 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; } } 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; if (py >= edit_y && py + cur_h <= edit_y + text_area_h) c.fill_rect(x, py, 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[192]; 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); wp_draw_ui_text(c, 4, status_text_y, status_left, colors::PANEL_TEXT); 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); 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); } } }