feat: new word processor features

This commit is contained in:
2026-03-27 07:55:46 +01:00
parent d0ceefd8ef
commit 8f118c7a0f
7 changed files with 1402 additions and 257 deletions
+2
View File
@@ -169,6 +169,8 @@ struct TrueTypeFont {
if (g->width > 0 && g->height > 0) {
g->bitmap = (uint8_t*)montauk::malloc(g->width * g->height);
if (!g->bitmap)
return g;
stbtt_MakeCodepointBitmap(&info, g->bitmap, g->width, g->height,
g->width, gc->scale, gc->scale, codepoint);
}
File diff suppressed because it is too large Load Diff
+214 -22
View File
@@ -27,9 +27,14 @@ static void wp_commit_pathbar(WordProcessorState* wp) {
wp->show_pathbar = false;
}
static void wp_close_dropdowns(WordProcessorState* wp) {
wp->font_dropdown_open = false;
wp->size_dropdown_open = false;
wp->line_spacing_dropdown_open = false;
}
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;
int click_x = local_x - WP_MARGIN;
int target_line = wp->wrap_line_count - 1;
for (int i = 0; i < wp->wrap_line_count; i++) {
@@ -41,12 +46,15 @@ static int wp_hit_test_text(WordProcessorState* wp, int local_x, int local_y, in
}
WrapLine* wl = &wp->wrap_lines[target_line];
int click_x = local_x - wl->x;
int ri = wl->run_idx;
int ro = wl->run_offset;
int chars_left = wl->char_count;
int x = 0;
int best_abs = wp_wrap_line_start(wp, target_line);
if (click_x <= 0) return best_abs;
while (chars_left > 0 && ri < wp->run_count) {
StyledRun* r = &wp->runs[ri];
TrueTypeFont* font = wp_get_font(r->font_id, r->flags);
@@ -92,7 +100,7 @@ void wp_handle_mouse(const Montauk::WinEvent& ev) {
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)) {
int dx = 128;
int dx = WP_FONT_DD_X;
int dy = WP_TOOLBAR_H;
int dh = FONT_COUNT * 26 + 4;
if (local_x >= dx && local_x < dx + 110 && local_y >= dy && local_y < dy + dh) {
@@ -100,6 +108,7 @@ void wp_handle_mouse(const Montauk::WinEvent& ev) {
if (idx >= 0 && idx < FONT_COUNT) {
if (wp->has_selection) wp_apply_style_to_selection(wp, 0, idx);
wp->cur_font_id = (uint8_t)idx;
if (wp->has_selection) wp_history_checkpoint(wp);
}
}
wp->font_dropdown_open = false;
@@ -107,7 +116,7 @@ void wp_handle_mouse(const Montauk::WinEvent& ev) {
}
if (wp->size_dropdown_open && wp_left_pressed(ev.mouse.buttons, ev.mouse.prev_buttons)) {
int dx = 224;
int dx = WP_SIZE_DD_X;
int dy = WP_TOOLBAR_H;
int dh = WP_SIZE_OPTION_COUNT * 26 + 4;
if (local_x >= dx && local_x < dx + 56 && local_y >= dy && local_y < dy + dh) {
@@ -116,43 +125,123 @@ void wp_handle_mouse(const Montauk::WinEvent& ev) {
if (wp->has_selection) wp_apply_style_to_selection(wp, 1, WP_SIZE_OPTIONS[idx]);
wp->cur_size = (uint8_t)WP_SIZE_OPTIONS[idx];
wp->wrap_dirty = true;
if (wp->has_selection) wp_history_checkpoint(wp);
}
}
wp->size_dropdown_open = false;
return;
}
if (wp->line_spacing_dropdown_open && wp_left_pressed(ev.mouse.buttons, ev.mouse.prev_buttons)) {
int dx = WP_LINE_DD_X;
int dy = WP_TOOLBAR_H;
int dh = WP_LINE_SPACING_OPTION_COUNT * 26 + 4;
if (local_x >= dx && local_x < dx + 64 && local_y >= dy && local_y < dy + dh) {
int idx = (local_y - dy - 2) / 26;
if (idx >= 0 && idx < WP_LINE_SPACING_OPTION_COUNT) {
wp_set_line_spacing(wp, WP_LINE_SPACING_OPTIONS[idx]);
wp_history_checkpoint(wp);
}
}
wp->line_spacing_dropdown_open = false;
return;
}
if (wp_left_pressed(ev.mouse.buttons, ev.mouse.prev_buttons) && local_y < WP_TOOLBAR_H) {
if (local_x >= 4 && local_x < 28 && local_y >= 6 && local_y < 30) {
if (local_x >= WP_BTN_OPEN_X && local_x < WP_BTN_OPEN_X + 24 && local_y >= 6 && local_y < 30) {
wp_open_pathbar_for_open(wp);
return;
}
if (local_x >= 32 && local_x < 56 && local_y >= 6 && local_y < 30) {
if (local_x >= WP_BTN_SAVE_X && local_x < WP_BTN_SAVE_X + 24 && local_y >= 6 && local_y < 30) {
wp_save_file(wp);
return;
}
if (local_x >= 66 && local_x < 90 && local_y >= 6 && local_y < 30) {
if (local_x >= WP_BTN_UNDO_X && local_x < WP_BTN_UNDO_X + 24 && local_y >= 6 && local_y < 30) {
wp_close_dropdowns(wp);
wp_undo(wp);
return;
}
if (local_x >= WP_BTN_REDO_X && local_x < WP_BTN_REDO_X + 24 && local_y >= 6 && local_y < 30) {
wp_close_dropdowns(wp);
wp_redo(wp);
return;
}
if (local_x >= WP_BTN_BOLD_X && local_x < WP_BTN_BOLD_X + 24 && local_y >= 6 && local_y < 30) {
if (wp->has_selection) wp_apply_style_to_selection(wp, 2, 0);
wp->cur_flags ^= STYLE_BOLD;
if (wp->has_selection) wp_history_checkpoint(wp);
return;
}
if (local_x >= 94 && local_x < 118 && local_y >= 6 && local_y < 30) {
if (local_x >= WP_BTN_ITALIC_X && local_x < WP_BTN_ITALIC_X + 24 && local_y >= 6 && local_y < 30) {
if (wp->has_selection) wp_apply_style_to_selection(wp, 3, 0);
wp->cur_flags ^= STYLE_ITALIC;
if (wp->has_selection) wp_history_checkpoint(wp);
return;
}
if (local_x >= 128 && local_x < 218 && local_y >= 6 && local_y < 30) {
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;
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;
return;
}
if (local_x >= WP_BTN_ALIGN_L_X && local_x < WP_BTN_ALIGN_L_X + 24 && local_y >= 6 && local_y < 30) {
wp_close_dropdowns(wp);
wp_apply_alignment(wp, PARA_ALIGN_LEFT);
wp_history_checkpoint(wp);
return;
}
if (local_x >= WP_BTN_ALIGN_C_X && local_x < WP_BTN_ALIGN_C_X + 24 && local_y >= 6 && local_y < 30) {
wp_close_dropdowns(wp);
wp_apply_alignment(wp, PARA_ALIGN_CENTER);
wp_history_checkpoint(wp);
return;
}
if (local_x >= WP_BTN_ALIGN_R_X && local_x < WP_BTN_ALIGN_R_X + 24 && local_y >= 6 && local_y < 30) {
wp_close_dropdowns(wp);
wp_apply_alignment(wp, PARA_ALIGN_RIGHT);
wp_history_checkpoint(wp);
return;
}
if (local_x >= WP_BTN_BULLET_X && local_x < WP_BTN_BULLET_X + 24 && local_y >= 6 && local_y < 30) {
wp_close_dropdowns(wp);
wp_toggle_list(wp, PARA_LIST_BULLET);
wp_history_checkpoint(wp);
return;
}
if (local_x >= WP_BTN_NUMBER_X && local_x < WP_BTN_NUMBER_X + 24 && local_y >= 6 && local_y < 30) {
wp_close_dropdowns(wp);
wp_toggle_list(wp, PARA_LIST_NUMBER);
wp_history_checkpoint(wp);
return;
}
if (local_x >= WP_BTN_OUTDENT_X && local_x < WP_BTN_OUTDENT_X + 24 && local_y >= 6 && local_y < 30) {
wp_close_dropdowns(wp);
wp_adjust_paragraph_indent(wp, -WP_PARA_STEP);
wp_history_checkpoint(wp);
return;
}
if (local_x >= WP_BTN_INDENT_X && local_x < WP_BTN_INDENT_X + 24 && local_y >= 6 && local_y < 30) {
wp_close_dropdowns(wp);
wp_adjust_paragraph_indent(wp, WP_PARA_STEP);
wp_history_checkpoint(wp);
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;
return;
}
if (local_x >= 224 && local_x < 268 && local_y >= 6 && local_y < 30) {
wp->size_dropdown_open = !wp->size_dropdown_open;
wp->font_dropdown_open = false;
return;
}
if (local_x >= 278 && local_x < 302 && local_y >= 6 && local_y < 30) {
if (local_x >= WP_BTN_SECTION_X && local_x < WP_BTN_SECTION_X + 24 && local_y >= 6 && local_y < 30) {
wp_close_dropdowns(wp);
wp_insert_char(wp, (char)0xA7);
wp_history_checkpoint(wp);
return;
}
return;
@@ -174,8 +263,7 @@ void wp_handle_mouse(const Montauk::WinEvent& ev) {
if (wp_left_pressed(ev.mouse.buttons, ev.mouse.prev_buttons) &&
local_y >= edit_y && local_y < edit_y + text_area_h) {
wp->font_dropdown_open = false;
wp->size_dropdown_open = false;
wp_close_dropdowns(wp);
int abs = wp_hit_test_text(wp, local_x, local_y, edit_y);
wp_pos_to_run(wp, abs, &wp->cursor_run, &wp->cursor_offset);
@@ -249,8 +337,7 @@ void wp_handle_key(const Montauk::KeyEvent& key) {
return;
}
wp->font_dropdown_open = false;
wp->size_dropdown_open = false;
wp_close_dropdowns(wp);
wp_recompute_wrap(wp, g_win_w);
if (key.ctrl && (key.ascii == 's' || key.ascii == 'S')) {
@@ -261,14 +348,114 @@ void wp_handle_key(const Montauk::KeyEvent& key) {
wp_open_pathbar_for_open(wp);
return;
}
if (key.ctrl && (key.ascii == 'b' || key.ascii == 'B')) {
if (wp->has_selection) wp_apply_style_to_selection(wp, 2, 0);
wp->cur_flags ^= STYLE_BOLD;
if (key.ctrl && !key.alt &&
(key.ascii == 'z' || key.ascii == 'Z')) {
if (key.shift) wp_redo(wp);
else wp_undo(wp);
return;
}
if (key.ctrl && (key.ascii == 'i' || key.ascii == 'I')) {
if (key.ctrl && !key.alt && (key.ascii == 'y' || key.ascii == 'Y')) {
wp_redo(wp);
return;
}
if (key.ctrl && key.alt && (key.ascii == 'b' || key.ascii == 'B')) {
wp_toggle_list(wp, PARA_LIST_BULLET);
wp_history_checkpoint(wp);
return;
}
if (key.ctrl && key.alt && (key.ascii == 'n' || key.ascii == 'N')) {
wp_toggle_list(wp, PARA_LIST_NUMBER);
wp_history_checkpoint(wp);
return;
}
if (key.ctrl && !key.alt && (key.ascii == 'l' || key.ascii == 'L')) {
wp_apply_alignment(wp, PARA_ALIGN_LEFT);
wp_history_checkpoint(wp);
return;
}
if (key.ctrl && !key.alt && (key.ascii == 'e' || key.ascii == 'E')) {
wp_apply_alignment(wp, PARA_ALIGN_CENTER);
wp_history_checkpoint(wp);
return;
}
if (key.ctrl && !key.alt && (key.ascii == 'r' || key.ascii == 'R')) {
wp_apply_alignment(wp, PARA_ALIGN_RIGHT);
wp_history_checkpoint(wp);
return;
}
if (key.ctrl && !key.alt && (key.ascii == '1')) {
wp_set_line_spacing(wp, 100);
wp_history_checkpoint(wp);
return;
}
if (key.ctrl && !key.alt && (key.ascii == '2')) {
wp_set_line_spacing(wp, 125);
wp_history_checkpoint(wp);
return;
}
if (key.ctrl && !key.alt && (key.ascii == '3')) {
wp_set_line_spacing(wp, 150);
wp_history_checkpoint(wp);
return;
}
if (key.ctrl && !key.alt && (key.ascii == '4')) {
wp_set_line_spacing(wp, 200);
wp_history_checkpoint(wp);
return;
}
if (key.ctrl && !key.alt && (key.ascii == '[' || key.ascii == '{')) {
if (key.shift || key.ascii == '{')
wp_adjust_paragraph_first_line_indent(wp, -WP_PARA_STEP);
else
wp_adjust_paragraph_indent(wp, -WP_PARA_STEP);
wp_history_checkpoint(wp);
return;
}
if (key.ctrl && !key.alt && (key.ascii == ']' || key.ascii == '}')) {
if (key.shift || key.ascii == '}')
wp_adjust_paragraph_first_line_indent(wp, WP_PARA_STEP);
else
wp_adjust_paragraph_indent(wp, WP_PARA_STEP);
wp_history_checkpoint(wp);
return;
}
if (key.ctrl && key.alt && key.scancode == 0x48) {
wp_adjust_paragraph_spacing_before(wp, -WP_SPACE_STEP);
wp_history_checkpoint(wp);
return;
}
if (key.ctrl && key.alt && key.scancode == 0x50) {
wp_adjust_paragraph_spacing_before(wp, WP_SPACE_STEP);
wp_history_checkpoint(wp);
return;
}
if (key.ctrl && key.shift && key.scancode == 0x48) {
wp_adjust_paragraph_spacing_after(wp, -WP_SPACE_STEP);
wp_history_checkpoint(wp);
return;
}
if (key.ctrl && key.shift && key.scancode == 0x50) {
wp_adjust_paragraph_spacing_after(wp, WP_SPACE_STEP);
wp_history_checkpoint(wp);
return;
}
if (key.ctrl && !key.alt && (key.ascii == 'b' || key.ascii == 'B')) {
if (wp->has_selection) wp_apply_style_to_selection(wp, 2, 0);
wp->cur_flags ^= STYLE_BOLD;
if (wp->has_selection) wp_history_checkpoint(wp);
return;
}
if (key.ctrl && !key.alt && (key.ascii == 'i' || key.ascii == 'I')) {
if (wp->has_selection) wp_apply_style_to_selection(wp, 3, 0);
wp->cur_flags ^= STYLE_ITALIC;
if (wp->has_selection) wp_history_checkpoint(wp);
return;
}
@@ -350,29 +537,34 @@ void wp_handle_key(const Montauk::KeyEvent& key) {
if (key.scancode == 0x53) {
if (wp->has_selection) wp_delete_selection(wp);
else wp_delete_char(wp);
wp_history_checkpoint(wp);
return;
}
if (key.ascii == '\b' || key.scancode == 0x0E) {
if (wp->has_selection) wp_delete_selection(wp);
else wp_backspace(wp);
wp_history_checkpoint(wp);
return;
}
if (key.ascii == '\n' || key.ascii == '\r') {
if (wp->has_selection) wp_delete_selection(wp);
wp_insert_char(wp, '\n');
wp_history_checkpoint(wp);
return;
}
if (key.ascii == '\t') {
if (wp->has_selection) wp_delete_selection(wp);
for (int i = 0; i < 4; i++) wp_insert_char(wp, ' ');
wp_history_checkpoint(wp);
return;
}
if (key.ascii >= 32 && key.ascii < 127) {
if (wp->has_selection) wp_delete_selection(wp);
wp_insert_char(wp, key.ascii);
wp_history_checkpoint(wp);
}
}
+36
View File
@@ -13,6 +13,15 @@ WordProcessorState g_wp = {};
WPFontTable g_wp_fonts = { {{nullptr}}, false };
SvgIcon g_icon_folder = {};
SvgIcon g_icon_save = {};
SvgIcon g_icon_undo = {};
SvgIcon g_icon_redo = {};
SvgIcon g_icon_align_left = {};
SvgIcon g_icon_align_center = {};
SvgIcon g_icon_align_right = {};
SvgIcon g_icon_list_bullet = {};
SvgIcon g_icon_list_number = {};
SvgIcon g_icon_indent_less = {};
SvgIcon g_icon_indent_more = {};
TrueTypeFont* g_ui_font = nullptr;
TrueTypeFont* g_ui_bold = nullptr;
@@ -22,12 +31,39 @@ void wp_load_icons() {
g_icon_folder = svg_load("0:/icons/folder.svg", 16, 16, def_color);
if (!g_icon_save.pixels)
g_icon_save = svg_load("0:/icons/document-save-symbolic.svg", 16, 16, def_color);
if (!g_icon_undo.pixels)
g_icon_undo = svg_load("0:/icons/edit-undo-symbolic.svg", 16, 16, def_color);
if (!g_icon_redo.pixels)
g_icon_redo = svg_load("0:/icons/edit-redo-symbolic.svg", 16, 16, def_color);
if (!g_icon_align_left.pixels)
g_icon_align_left = svg_load("0:/icons/format-justify-left-symbolic.svg", 16, 16, def_color);
if (!g_icon_align_center.pixels)
g_icon_align_center = svg_load("0:/icons/format-justify-center-symbolic.svg", 16, 16, def_color);
if (!g_icon_align_right.pixels)
g_icon_align_right = svg_load("0:/icons/format-justify-right-symbolic.svg", 16, 16, def_color);
if (!g_icon_list_bullet.pixels)
g_icon_list_bullet = svg_load("0:/icons/view-list-bullet-symbolic.svg", 16, 16, def_color);
if (!g_icon_list_number.pixels)
g_icon_list_number = svg_load("0:/icons/view-list-ordered-symbolic.svg", 16, 16, def_color);
if (!g_icon_indent_less.pixels)
g_icon_indent_less = svg_load("0:/icons/format-indent-less-symbolic.svg", 16, 16, def_color);
if (!g_icon_indent_more.pixels)
g_icon_indent_more = svg_load("0:/icons/format-indent-more-symbolic.svg", 16, 16, def_color);
}
void wp_cleanup_state() {
wp_free_document(&g_wp);
if (g_icon_folder.pixels) svg_free(g_icon_folder);
if (g_icon_save.pixels) svg_free(g_icon_save);
if (g_icon_undo.pixels) svg_free(g_icon_undo);
if (g_icon_redo.pixels) svg_free(g_icon_redo);
if (g_icon_align_left.pixels) svg_free(g_icon_align_left);
if (g_icon_align_center.pixels) svg_free(g_icon_align_center);
if (g_icon_align_right.pixels) svg_free(g_icon_align_right);
if (g_icon_list_bullet.pixels) svg_free(g_icon_list_bullet);
if (g_icon_list_number.pixels) svg_free(g_icon_list_number);
if (g_icon_indent_less.pixels) svg_free(g_icon_indent_less);
if (g_icon_indent_more.pixels) svg_free(g_icon_indent_more);
}
extern "C" void _start() {
+138 -35
View File
@@ -24,6 +24,68 @@ static void wp_draw_ui_button(Canvas& c, int x, int y, int w, int h,
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;
@@ -32,63 +94,84 @@ void wp_render() {
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(4, 6, 24, 24, 3, btn_bg);
c.fill_rounded_rect(WP_BTN_OPEN_X, 6, 24, 24, 3, btn_bg);
if (g_icon_folder.pixels)
c.icon(8, 10, g_icon_folder);
c.icon(WP_BTN_OPEN_X + 4, 10, g_icon_folder);
c.fill_rounded_rect(32, 6, 24, 24, 3, btn_bg);
c.fill_rounded_rect(WP_BTN_SAVE_X, 6, 24, 24, 3, btn_bg);
if (g_icon_save.pixels)
c.icon(36, 10, g_icon_save);
c.icon(WP_BTN_SAVE_X + 4, 10, g_icon_save);
c.vline(60, 4, 28, colors::BORDER);
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(66, 6, 24, 24, 3, bold_bg);
draw_text(c, g_ui_bold ? g_ui_bold : g_ui_font, 73, 8, "B", colors::TEXT_COLOR, fonts::UI_SIZE);
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(94, 6, 24, 24, 3, italic_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, 103, 8, "I", colors::TEXT_COLOR, fonts::UI_SIZE);
draw_text(c, italic_font ? italic_font : g_ui_font, WP_BTN_ITALIC_X + 9, 8, "I", colors::TEXT_COLOR, fonts::UI_SIZE);
}
c.vline(122, 4, 28, colors::BORDER);
c.fill_rounded_rect(128, 6, 90, 24, 3, btn_bg);
wp_draw_ui_text(c, 134, (WP_TOOLBAR_H - sfh) / 2,
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(224, 6, 44, 24, 3, btn_bg);
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, 232, (WP_TOOLBAR_H - sfh) / 2, sz, colors::TEXT_COLOR);
wp_draw_ui_text(c, WP_SIZE_DD_X + 8, (WP_TOOLBAR_H - sfh) / 2, sz, colors::TEXT_COLOR);
}
c.vline(272, 4, 28, colors::BORDER);
c.vline(WP_SIZE_DD_X + WP_SIZE_DD_W + 6, 4, 28, colors::BORDER);
c.fill_rounded_rect(278, 6, 24, 24, 3, btn_bg);
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, 284, 8, section, colors::TEXT_COLOR, fonts::UI_SIZE);
}
{
char label[128];
if (wp->filename[0])
snprintf(label, sizeof(label), "%s%s", wp->filename, wp->modified ? " *" : "");
else
snprintf(label, sizeof(label), "Untitled%s", wp->modified ? " *" : "");
wp_draw_ui_text(c, 312, (WP_TOOLBAR_H - sfh) / 2, label, Color::from_rgb(0x88, 0x88, 0x88));
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);
@@ -143,7 +226,6 @@ void wp_render() {
wp_ensure_cursor_visible(wp, text_area_h);
int scroll_y = wp->scrollbar.scroll_offset;
int cursor_abs = wp_abs_pos(wp, wp->cursor_run, wp->cursor_offset);
int sel_s = 0;
int sel_e = 0;
if (wp->has_selection) wp_sel_range(wp, &sel_s, &sel_e);
@@ -156,10 +238,12 @@ void wp_render() {
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 = WP_MARGIN;
int x = wl->x;
int line_abs_start = wp_wrap_line_start(wp, li);
int char_idx = 0;
@@ -244,11 +328,13 @@ void wp_render() {
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[128];
snprintf(status_left, sizeof(status_left), " %s %dpt %s%s",
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->cur_flags & STYLE_BOLD) ? "Bold " : "",
(wp->cur_flags & STYLE_ITALIC) ? "Italic" : "");
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];
@@ -257,7 +343,7 @@ void wp_render() {
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 = 128;
int dx = WP_FONT_DD_X;
int dy = WP_TOOLBAR_H;
int dw = 110;
int dh = FONT_COUNT * 26 + 4;
@@ -272,7 +358,7 @@ void wp_render() {
}
if (wp->size_dropdown_open) {
int dx = 224;
int dx = WP_SIZE_DD_X;
int dy = WP_TOOLBAR_H;
int dw = 56;
int dh = WP_SIZE_OPTION_COUNT * 26 + 4;
@@ -287,4 +373,21 @@ void wp_render() {
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);
}
}
}
@@ -31,7 +31,34 @@ static constexpr int WP_MARGIN = 16;
static constexpr int WP_MAX_RUNS = 1024;
static constexpr int WP_MAX_TEXT = 262144;
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_PARA_STEP = 12;
static constexpr int WP_SPACE_STEP = 6;
static constexpr int WP_LIST_LEFT = 28;
static constexpr int WP_LIST_HANGING = -18;
static constexpr int WP_LIST_MARKER_W = 20;
static constexpr int WP_BTN_OPEN_X = 4;
static constexpr int WP_BTN_SAVE_X = 32;
static constexpr int WP_BTN_UNDO_X = 66;
static constexpr int WP_BTN_REDO_X = 94;
static constexpr int WP_BTN_BOLD_X = 128;
static constexpr int WP_BTN_ITALIC_X = 156;
static constexpr int WP_FONT_DD_X = 188;
static constexpr int WP_FONT_DD_W = 84;
static constexpr int WP_SIZE_DD_X = 278;
static constexpr int WP_SIZE_DD_W = 40;
static constexpr int WP_BTN_ALIGN_L_X = 330;
static constexpr int WP_BTN_ALIGN_C_X = 358;
static constexpr int WP_BTN_ALIGN_R_X = 386;
static constexpr int WP_BTN_BULLET_X = 418;
static constexpr int WP_BTN_NUMBER_X = 446;
static constexpr int WP_BTN_OUTDENT_X = 482;
static constexpr int WP_BTN_INDENT_X = 510;
static constexpr int WP_LINE_DD_X = 544;
static constexpr int WP_LINE_DD_W = 56;
static constexpr int WP_BTN_SECTION_X = 608;
static constexpr int FONT_ROBOTO = 0;
static constexpr int FONT_NOTOSERIF = 1;
@@ -49,6 +76,20 @@ inline constexpr const char* WP_FONT_NAMES[FONT_COUNT] = {
inline constexpr int WP_SIZE_OPTIONS[] = { 12, 14, 16, 18, 20, 24, 28, 36 };
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;
enum ParagraphAlign : uint8_t {
PARA_ALIGN_LEFT = 0,
PARA_ALIGN_CENTER = 1,
PARA_ALIGN_RIGHT = 2,
};
enum ParagraphListType : uint8_t {
PARA_LIST_NONE = 0,
PARA_LIST_BULLET = 1,
PARA_LIST_NUMBER = 2,
};
struct WPFontTable {
TrueTypeFont* fonts[FONT_COUNT][4];
@@ -71,6 +112,32 @@ struct WrapLine {
int y;
int height;
int baseline;
int x;
int width;
int paragraph_idx;
int list_number;
bool first_in_paragraph;
};
struct ParagraphStyle {
uint8_t align;
uint8_t list_type;
uint8_t line_spacing;
uint8_t _pad;
int16_t left_indent;
int16_t first_line_indent;
int16_t space_before;
int16_t space_after;
};
struct UndoSnapshot {
uint8_t* data;
int size;
int cursor_abs;
int sel_anchor;
int sel_end;
bool has_selection;
bool modified;
};
inline bool wp_left_held(uint8_t buttons) {
@@ -189,9 +256,13 @@ struct WordProcessorState {
WrapLine* wrap_lines;
int wrap_line_count;
int wrap_line_cap;
bool wrap_dirty;
int last_wrap_width;
ParagraphStyle paragraphs[WP_MAX_PARAGRAPHS];
int paragraph_count;
bool modified;
char filepath[256];
char filename[64];
@@ -204,6 +275,11 @@ struct WordProcessorState {
bool font_dropdown_open;
bool size_dropdown_open;
bool line_spacing_dropdown_open;
UndoSnapshot undo[WP_UNDO_MAX];
int undo_count;
int undo_pos;
};
extern int g_win_w;
@@ -213,11 +289,21 @@ extern WordProcessorState g_wp;
extern WPFontTable g_wp_fonts;
extern SvgIcon g_icon_folder;
extern SvgIcon g_icon_save;
extern SvgIcon g_icon_undo;
extern SvgIcon g_icon_redo;
extern SvgIcon g_icon_align_left;
extern SvgIcon g_icon_align_center;
extern SvgIcon g_icon_align_right;
extern SvgIcon g_icon_list_bullet;
extern SvgIcon g_icon_list_number;
extern SvgIcon g_icon_indent_less;
extern SvgIcon g_icon_indent_more;
extern TrueTypeFont* g_ui_font;
extern TrueTypeFont* g_ui_bold;
void wp_load_fonts();
void wp_load_icons();
void wp_init_paragraph_style(ParagraphStyle* para);
void wp_init_empty_document(WordProcessorState* wp);
void wp_free_document(WordProcessorState* wp);
void wp_cleanup_state();
@@ -227,6 +313,8 @@ TrueTypeFont* wp_get_font(int font_id, uint8_t flags);
int wp_abs_pos(WordProcessorState* wp, int run, int offset);
void wp_pos_to_run(WordProcessorState* wp, int abs_pos, int* out_run, int* out_offset);
char wp_char_at(WordProcessorState* wp, int abs_pos);
int wp_find_paragraph_at(WordProcessorState* wp, int abs_pos);
void wp_selected_paragraph_range(WordProcessorState* wp, int* out_start_para, int* out_end_para);
void wp_insert_char(WordProcessorState* wp, char c);
void wp_delete_char(WordProcessorState* wp);
@@ -242,6 +330,14 @@ void wp_sel_range(WordProcessorState* wp, int* out_start, int* out_end);
void wp_start_selection(WordProcessorState* wp);
void wp_update_selection_to_cursor(WordProcessorState* wp);
void wp_apply_style_to_selection(WordProcessorState* wp, int mode, int value);
void wp_apply_alignment(WordProcessorState* wp, uint8_t align);
void wp_adjust_paragraph_indent(WordProcessorState* wp, int delta);
void wp_adjust_paragraph_first_line_indent(WordProcessorState* wp, int delta);
void wp_adjust_paragraph_spacing_before(WordProcessorState* wp, int delta);
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_delete_selection(WordProcessorState* wp);
void wp_recompute_wrap(WordProcessorState* wp, int content_w);
@@ -254,6 +350,12 @@ void wp_open_save_pathbar(WordProcessorState* wp);
void wp_save_file(WordProcessorState* wp);
void wp_load_file(WordProcessorState* wp, const char* path);
void wp_history_reset(WordProcessorState* wp);
void wp_history_checkpoint(WordProcessorState* wp);
void wp_history_mark_saved(WordProcessorState* wp);
bool wp_undo(WordProcessorState* wp);
bool wp_redo(WordProcessorState* wp);
void wp_render();
void wp_handle_mouse(const Montauk::WinEvent& ev);
void wp_handle_key(const Montauk::KeyEvent& key);
+9
View File
@@ -24,6 +24,15 @@ ICONS=(
"actions/symbolic/go-next-symbolic.svg"
"actions/symbolic/go-home-symbolic.svg"
"actions/symbolic/document-save-symbolic.svg"
"actions/symbolic/edit-undo-symbolic.svg"
"actions/symbolic/edit-redo-symbolic.svg"
"actions/symbolic/format-justify-left-symbolic.svg"
"actions/symbolic/format-justify-center-symbolic.svg"
"actions/symbolic/format-justify-right-symbolic.svg"
"actions/symbolic/view-list-bullet-symbolic.svg"
"actions/symbolic/view-list-ordered-symbolic.svg"
"actions/symbolic/format-indent-less-symbolic.svg"
"actions/symbolic/format-indent-more-symbolic.svg"
"apps/symbolic/utilities-terminal-symbolic.svg"
"apps/symbolic/system-file-manager-symbolic.svg"
"apps/symbolic/preferences-desktop-apps-symbolic.svg"