wip: printing from word processor, word processor improvements
This commit is contained in:
@@ -41,6 +41,7 @@ CXXFLAGS := \
|
||||
-mcmodel=small \
|
||||
-I $(PROG_INC) \
|
||||
-isystem $(PROG_INC)/libc \
|
||||
-I ../../lib/bearssl/inc \
|
||||
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
|
||||
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
|
||||
|
||||
@@ -53,8 +54,9 @@ LDFLAGS := \
|
||||
-z max-page-size=0x1000 \
|
||||
-T $(LINK_LD)
|
||||
|
||||
SRCS := main.cpp document.cpp render.cpp input.cpp stb_truetype_impl.cpp
|
||||
SRCS := main.cpp document.cpp render.cpp input.cpp print_export.cpp stb_truetype_impl.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
LIBS := $(LIBDIR)/tls/libtls.a $(LIBDIR)/bearssl/libbearssl.a $(LIBDIR)/libjpegwrite/libjpegwrite.a $(LIBDIR)/libc/liblibc.a
|
||||
|
||||
TARGET := $(BINDIR)/apps/wordprocessor/wordprocessor.elf
|
||||
|
||||
@@ -62,9 +64,9 @@ TARGET := $(BINDIR)/apps/wordprocessor/wordprocessor.elf
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LIBS)
|
||||
mkdir -p $(BINDIR)/apps/wordprocessor
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
|
||||
|
||||
$(OBJDIR)/%.o: %.cpp Makefile
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
@@ -419,6 +419,10 @@ void wp_init_empty_document(WordProcessorState* wp) {
|
||||
wp->modified = false;
|
||||
wp->filepath[0] = '\0';
|
||||
wp->filename[0] = '\0';
|
||||
wp->preferred_printer_uri[0] = '\0';
|
||||
wp->preferred_printer_name[0] = '\0';
|
||||
wp->preferred_print_copies = 1;
|
||||
wp->status_msg[0] = '\0';
|
||||
|
||||
wp->show_pathbar = false;
|
||||
wp->pathbar_save_mode = false;
|
||||
@@ -429,6 +433,7 @@ void wp_init_empty_document(WordProcessorState* wp) {
|
||||
wp->font_dropdown_open = false;
|
||||
wp->size_dropdown_open = false;
|
||||
wp->line_spacing_dropdown_open = false;
|
||||
wp->special_char_flyout_open = false;
|
||||
|
||||
wp->undo_count = 0;
|
||||
wp->undo_pos = 0;
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
#include "wordprocessor.hpp"
|
||||
#include <gui/dialogs.hpp>
|
||||
|
||||
static void wp_set_status(WordProcessorState* wp, const char* msg) {
|
||||
if (!msg) msg = "";
|
||||
montauk::strncpy(wp->status_msg, msg, (int)sizeof(wp->status_msg) - 1);
|
||||
wp->status_msg[sizeof(wp->status_msg) - 1] = '\0';
|
||||
}
|
||||
|
||||
static void wp_open_pathbar_for_open(WordProcessorState* wp) {
|
||||
char path[256] = {};
|
||||
char msg[160] = {};
|
||||
@@ -17,6 +23,12 @@ static void wp_open_pathbar_for_open(WordProcessorState* wp) {
|
||||
}
|
||||
}
|
||||
|
||||
static void wp_open_print_dialog(WordProcessorState* wp) {
|
||||
char msg[160] = {};
|
||||
if (wp_print_document(wp, msg, sizeof(msg)) || msg[0])
|
||||
wp_set_status(wp, msg);
|
||||
}
|
||||
|
||||
static void wp_commit_pathbar(WordProcessorState* wp) {
|
||||
if (!wp->pathbar_text[0]) return;
|
||||
if (wp->pathbar_save_mode) {
|
||||
@@ -28,10 +40,23 @@ static void wp_commit_pathbar(WordProcessorState* wp) {
|
||||
wp->show_pathbar = false;
|
||||
}
|
||||
|
||||
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 void wp_close_dropdowns(WordProcessorState* wp) {
|
||||
wp->font_dropdown_open = false;
|
||||
wp->size_dropdown_open = false;
|
||||
wp->line_spacing_dropdown_open = false;
|
||||
wp->special_char_flyout_open = false;
|
||||
}
|
||||
|
||||
static void wp_insert_special_char(WordProcessorState* wp, uint8_t ch) {
|
||||
if (wp->has_selection) wp_delete_selection(wp);
|
||||
wp_insert_char(wp, (char)ch);
|
||||
wp_history_checkpoint(wp);
|
||||
}
|
||||
|
||||
static int wp_hit_test_text(WordProcessorState* wp, int local_x, int local_y, int edit_y) {
|
||||
@@ -148,6 +173,19 @@ void wp_handle_mouse(const Montauk::WinEvent& ev) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (wp->special_char_flyout_open && wp_left_pressed(ev.mouse.buttons, ev.mouse.prev_buttons)) {
|
||||
int dx = wp_special_char_flyout_x();
|
||||
int dy = WP_TOOLBAR_H;
|
||||
int dh = WP_SPECIAL_CHAR_OPTION_COUNT * WP_SPECIAL_CHAR_ROW_H + 4;
|
||||
if (local_x >= dx && local_x < dx + WP_SPECIAL_CHAR_FLYOUT_W && local_y >= dy && local_y < dy + dh) {
|
||||
int idx = (local_y - dy - 2) / WP_SPECIAL_CHAR_ROW_H;
|
||||
if (idx >= 0 && idx < WP_SPECIAL_CHAR_OPTION_COUNT)
|
||||
wp_insert_special_char(wp, WP_SPECIAL_CHAR_OPTIONS[idx].code);
|
||||
}
|
||||
wp->special_char_flyout_open = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (wp_left_pressed(ev.mouse.buttons, ev.mouse.prev_buttons) && local_y < WP_TOOLBAR_H) {
|
||||
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);
|
||||
@@ -157,6 +195,10 @@ void wp_handle_mouse(const Montauk::WinEvent& ev) {
|
||||
wp_save_file(wp);
|
||||
return;
|
||||
}
|
||||
if (local_x >= WP_BTN_PRINT_X && local_x < WP_BTN_PRINT_X + 24 && local_y >= 6 && local_y < 30) {
|
||||
wp_open_print_dialog(wp);
|
||||
return;
|
||||
}
|
||||
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);
|
||||
@@ -237,12 +279,13 @@ void wp_handle_mouse(const Montauk::WinEvent& ev) {
|
||||
wp->line_spacing_dropdown_open = !wp->line_spacing_dropdown_open;
|
||||
wp->font_dropdown_open = false;
|
||||
wp->size_dropdown_open = false;
|
||||
wp->special_char_flyout_open = false;
|
||||
return;
|
||||
}
|
||||
if (local_x >= WP_BTN_SECTION_X && local_x < WP_BTN_SECTION_X + 24 && local_y >= 6 && local_y < 30) {
|
||||
bool open = !wp->special_char_flyout_open;
|
||||
wp_close_dropdowns(wp);
|
||||
wp_insert_char(wp, (char)0xA7);
|
||||
wp_history_checkpoint(wp);
|
||||
wp->special_char_flyout_open = open;
|
||||
return;
|
||||
}
|
||||
return;
|
||||
@@ -349,6 +392,10 @@ void wp_handle_key(const Montauk::KeyEvent& key) {
|
||||
wp_open_pathbar_for_open(wp);
|
||||
return;
|
||||
}
|
||||
if (key.ctrl && (key.ascii == 'p' || key.ascii == 'P')) {
|
||||
wp_open_print_dialog(wp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.ctrl && !key.alt &&
|
||||
(key.ascii == 'z' || key.ascii == 'Z')) {
|
||||
|
||||
@@ -13,6 +13,7 @@ WordProcessorState g_wp = {};
|
||||
WPFontTable g_wp_fonts = { {{nullptr}}, false };
|
||||
SvgIcon g_icon_folder = {};
|
||||
SvgIcon g_icon_save = {};
|
||||
SvgIcon g_icon_print = {};
|
||||
SvgIcon g_icon_undo = {};
|
||||
SvgIcon g_icon_redo = {};
|
||||
SvgIcon g_icon_align_left = {};
|
||||
@@ -31,6 +32,8 @@ 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_print.pixels)
|
||||
g_icon_print = svg_load("0:/icons/printer-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)
|
||||
@@ -55,6 +58,7 @@ 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_print.pixels) svg_free(g_icon_print);
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
/*
|
||||
* print_export.cpp
|
||||
* Word Processor print export and job submission
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "wordprocessor.hpp"
|
||||
#include <gui/dialogs.hpp>
|
||||
#include <print/print.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define STBI_WRITE_NO_STDIO
|
||||
#include <gui/stb_image_write.h>
|
||||
}
|
||||
|
||||
namespace dialogs = gui::dialogs;
|
||||
|
||||
static constexpr int WP_PRINT_PAGE_W = 1275; // 8.5in at 150 DPI
|
||||
static constexpr int WP_PRINT_PAGE_H = 1650; // 11in at 150 DPI
|
||||
static constexpr int WP_PRINT_MARGIN_X = 96;
|
||||
static constexpr int WP_PRINT_MARGIN_Y = 96;
|
||||
static constexpr int WP_PRINT_JPEG_QUALITY = 92;
|
||||
|
||||
struct WpJpegBuffer {
|
||||
uint8_t* data;
|
||||
int size;
|
||||
int capacity;
|
||||
bool failed;
|
||||
};
|
||||
|
||||
static void wp_status_copy(char* out, int out_len, const char* text) {
|
||||
if (!out || out_len <= 0) return;
|
||||
if (!text) text = "";
|
||||
montauk::strncpy(out, text, out_len - 1);
|
||||
out[out_len - 1] = '\0';
|
||||
}
|
||||
|
||||
static void wp_jpeg_write_callback(void* context, void* data, int size) {
|
||||
WpJpegBuffer* buf = (WpJpegBuffer*)context;
|
||||
if (!buf || !data || size <= 0 || buf->failed) return;
|
||||
|
||||
int needed = buf->size + size;
|
||||
if (needed > buf->capacity) {
|
||||
int new_cap = buf->capacity > 0 ? buf->capacity : 256 * 1024;
|
||||
while (new_cap < needed)
|
||||
new_cap *= 2;
|
||||
uint8_t* grown = (uint8_t*)montauk::realloc(buf->data, new_cap);
|
||||
if (!grown) {
|
||||
buf->failed = true;
|
||||
return;
|
||||
}
|
||||
buf->data = grown;
|
||||
buf->capacity = new_cap;
|
||||
}
|
||||
|
||||
montauk::memcpy(buf->data + buf->size, data, size);
|
||||
buf->size += size;
|
||||
}
|
||||
|
||||
static int wp_print_layout_width() {
|
||||
return (WP_PRINT_PAGE_W - WP_PRINT_MARGIN_X * 2) + WP_MARGIN * 2 + WP_SCROLLBAR_W;
|
||||
}
|
||||
|
||||
static void wp_make_base_print_name(WordProcessorState* wp, char* out, int out_len) {
|
||||
const char* src = wp->filename[0] ? wp->filename : "document";
|
||||
int last_dot = -1;
|
||||
for (int i = 0; src[i]; i++) {
|
||||
if (src[i] == '.') last_dot = i;
|
||||
}
|
||||
|
||||
if (last_dot <= 0) {
|
||||
wp_status_copy(out, out_len, src);
|
||||
return;
|
||||
}
|
||||
|
||||
int copy_len = last_dot;
|
||||
if (copy_len >= out_len) copy_len = out_len - 1;
|
||||
montauk::memcpy(out, src, (uint64_t)copy_len);
|
||||
out[copy_len] = '\0';
|
||||
}
|
||||
|
||||
static bool wp_printer_accepts_jpeg(const char* printer_uri, char* err, int err_len) {
|
||||
print::ProbeState probe = {};
|
||||
char probe_err[160] = {};
|
||||
if (!print::probe_printer_uri(printer_uri, &probe, probe_err, sizeof(probe_err)))
|
||||
return true;
|
||||
if (probe.caps.supports_jpeg)
|
||||
return true;
|
||||
|
||||
if (probe.caps.supported_formats[0]) {
|
||||
snprintf(err, (size_t)err_len,
|
||||
"Selected printer does not advertise JPEG support (supports: %s)",
|
||||
probe.caps.supported_formats);
|
||||
} else {
|
||||
wp_status_copy(err, err_len, "Selected printer does not advertise JPEG support");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool wp_build_print_page_map(WordProcessorState* wp,
|
||||
int** out_line_pages,
|
||||
int** out_line_y,
|
||||
int* out_page_count,
|
||||
char* err, int err_len) {
|
||||
if (out_line_pages) *out_line_pages = nullptr;
|
||||
if (out_line_y) *out_line_y = nullptr;
|
||||
if (out_page_count) *out_page_count = 0;
|
||||
|
||||
wp_recompute_wrap(wp, wp_print_layout_width());
|
||||
if (wp->wrap_line_count <= 0) {
|
||||
wp_status_copy(err, err_len, "failed to paginate document");
|
||||
return false;
|
||||
}
|
||||
|
||||
int* line_pages = (int*)montauk::malloc((uint64_t)wp->wrap_line_count * sizeof(int));
|
||||
int* line_y = (int*)montauk::malloc((uint64_t)wp->wrap_line_count * sizeof(int));
|
||||
if (!line_pages || !line_y) {
|
||||
if (line_pages) montauk::mfree(line_pages);
|
||||
if (line_y) montauk::mfree(line_y);
|
||||
wp_status_copy(err, err_len, "out of memory while preparing print pages");
|
||||
return false;
|
||||
}
|
||||
|
||||
int base_y = wp->wrap_lines[0].y;
|
||||
int printable_bottom = WP_PRINT_PAGE_H - WP_PRINT_MARGIN_Y;
|
||||
int current_page = 0;
|
||||
int current_y = WP_PRINT_MARGIN_Y;
|
||||
int prev_rel_bottom = 0;
|
||||
|
||||
for (int i = 0; i < wp->wrap_line_count; i++) {
|
||||
WrapLine* wl = &wp->wrap_lines[i];
|
||||
int rel_y = wl->y - base_y;
|
||||
if (rel_y < 0) rel_y = 0;
|
||||
|
||||
int gap_before = (i == 0) ? 0 : (rel_y - prev_rel_bottom);
|
||||
if (gap_before < 0) gap_before = 0;
|
||||
|
||||
int line_top = current_y + gap_before;
|
||||
if (line_top + wl->height > printable_bottom && current_y > WP_PRINT_MARGIN_Y) {
|
||||
current_page++;
|
||||
current_y = WP_PRINT_MARGIN_Y;
|
||||
line_top = current_y;
|
||||
}
|
||||
|
||||
line_pages[i] = current_page;
|
||||
line_y[i] = line_top;
|
||||
current_y = line_top + wl->height;
|
||||
prev_rel_bottom = rel_y + wl->height;
|
||||
}
|
||||
|
||||
if (out_line_pages) *out_line_pages = line_pages;
|
||||
else montauk::mfree(line_pages);
|
||||
if (out_line_y) *out_line_y = line_y;
|
||||
else montauk::mfree(line_y);
|
||||
if (out_page_count) *out_page_count = current_page + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void wp_draw_print_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_PRINT_MARGIN_X + para->left_indent + para->first_line_indent;
|
||||
int min_x = WP_PRINT_MARGIN_X - WP_MARGIN + 4;
|
||||
if (marker_x < min_x) marker_x = min_x;
|
||||
|
||||
if (para->list_type == PARA_LIST_BULLET) {
|
||||
fill_circle(c, marker_x + 7, py + wl->height / 2, 3, colors::BLACK);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
char label[16];
|
||||
snprintf(label, sizeof(label), "%d.", wl->list_number > 0 ? wl->list_number : 1);
|
||||
int top_y = py + (wl->height - run->size) / 2;
|
||||
draw_text(c, font ? font : g_ui_font, marker_x, top_y, label, colors::BLACK, run->size);
|
||||
}
|
||||
|
||||
static bool wp_encode_print_page(WordProcessorState* wp,
|
||||
const int* line_pages,
|
||||
const int* line_y,
|
||||
int page_index,
|
||||
uint8_t** out_data,
|
||||
int* out_len,
|
||||
char* err, int err_len) {
|
||||
if (out_data) *out_data = nullptr;
|
||||
if (out_len) *out_len = 0;
|
||||
|
||||
uint64_t pixel_bytes = (uint64_t)WP_PRINT_PAGE_W * WP_PRINT_PAGE_H * sizeof(uint32_t);
|
||||
uint32_t* pixels = (uint32_t*)montauk::malloc(pixel_bytes);
|
||||
if (!pixels) {
|
||||
wp_status_copy(err, err_len, "out of memory while rendering print page");
|
||||
return false;
|
||||
}
|
||||
|
||||
Canvas c(pixels, WP_PRINT_PAGE_W, WP_PRINT_PAGE_H);
|
||||
c.fill(colors::WHITE);
|
||||
|
||||
for (int li = 0; li < wp->wrap_line_count; li++) {
|
||||
if (line_pages[li] != page_index) continue;
|
||||
|
||||
WrapLine* wl = &wp->wrap_lines[li];
|
||||
int py = line_y[li];
|
||||
wp_draw_print_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_PRINT_MARGIN_X + (wl->x - WP_MARGIN);
|
||||
|
||||
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];
|
||||
(void)line_abs_start;
|
||||
(void)char_idx;
|
||||
|
||||
if (ch != '\n' && (ch >= 32 || ch < 0)) {
|
||||
int adv = font->draw_char_to_buffer(
|
||||
c.pixels, c.w, c.h, x, baseline, (unsigned char)ch, colors::BLACK, gc);
|
||||
x += adv;
|
||||
}
|
||||
char_idx++;
|
||||
}
|
||||
|
||||
chars_left -= to_draw;
|
||||
ro += to_draw;
|
||||
if (ro >= r->len) {
|
||||
ri++;
|
||||
ro = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t rgb_bytes = (uint64_t)WP_PRINT_PAGE_W * WP_PRINT_PAGE_H * 3;
|
||||
uint8_t* rgb = (uint8_t*)montauk::malloc(rgb_bytes);
|
||||
if (!rgb) {
|
||||
montauk::mfree(pixels);
|
||||
wp_status_copy(err, err_len, "out of memory while encoding print page");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int y = 0; y < WP_PRINT_PAGE_H; y++) {
|
||||
uint8_t* dst = rgb + (uint64_t)y * WP_PRINT_PAGE_W * 3;
|
||||
for (int x = 0; x < WP_PRINT_PAGE_W; x++) {
|
||||
uint32_t px = pixels[(uint64_t)y * WP_PRINT_PAGE_W + x];
|
||||
dst[x * 3 + 0] = (uint8_t)((px >> 16) & 0xFF);
|
||||
dst[x * 3 + 1] = (uint8_t)((px >> 8) & 0xFF);
|
||||
dst[x * 3 + 2] = (uint8_t)(px & 0xFF);
|
||||
}
|
||||
}
|
||||
montauk::mfree(pixels);
|
||||
|
||||
WpJpegBuffer jpeg = {};
|
||||
jpeg.capacity = 256 * 1024;
|
||||
jpeg.data = (uint8_t*)montauk::malloc(jpeg.capacity);
|
||||
if (!jpeg.data) {
|
||||
montauk::mfree(rgb);
|
||||
wp_status_copy(err, err_len, "out of memory for JPEG output");
|
||||
return false;
|
||||
}
|
||||
|
||||
int ok = stbi_write_jpg_to_func(wp_jpeg_write_callback, &jpeg,
|
||||
WP_PRINT_PAGE_W, WP_PRINT_PAGE_H, 3, rgb,
|
||||
WP_PRINT_JPEG_QUALITY);
|
||||
montauk::mfree(rgb);
|
||||
|
||||
if (!ok || jpeg.failed || jpeg.size <= 0) {
|
||||
montauk::mfree(jpeg.data);
|
||||
wp_status_copy(err, err_len, "JPEG encoding failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (out_data) *out_data = jpeg.data;
|
||||
else montauk::mfree(jpeg.data);
|
||||
if (out_len) *out_len = jpeg.size;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool wp_print_document(WordProcessorState* wp, char* out_status, int out_status_len) {
|
||||
if (out_status && out_status_len > 0) out_status[0] = '\0';
|
||||
if (!wp) {
|
||||
wp_status_copy(out_status, out_status_len, "No document loaded");
|
||||
return false;
|
||||
}
|
||||
|
||||
char printer_uri[256] = {};
|
||||
char printer_name[128] = {};
|
||||
char msg[160] = {};
|
||||
montauk::strncpy(printer_uri, wp->preferred_printer_uri, (int)sizeof(printer_uri) - 1);
|
||||
printer_uri[sizeof(printer_uri) - 1] = '\0';
|
||||
montauk::strncpy(printer_name, wp->preferred_printer_name, (int)sizeof(printer_name) - 1);
|
||||
printer_name[sizeof(printer_name) - 1] = '\0';
|
||||
uint32_t copies = wp->preferred_print_copies > 0 ? wp->preferred_print_copies : 1;
|
||||
|
||||
const char* job_name = wp->filename[0] ? wp->filename : "Untitled document";
|
||||
if (!dialogs::configure_print("Print Document",
|
||||
printer_uri, job_name,
|
||||
printer_uri, sizeof(printer_uri),
|
||||
printer_name, sizeof(printer_name),
|
||||
&copies, msg, sizeof(msg))) {
|
||||
if (msg[0]) wp_status_copy(out_status, out_status_len, msg);
|
||||
return false;
|
||||
}
|
||||
|
||||
montauk::strncpy(wp->preferred_printer_uri, printer_uri, (int)sizeof(wp->preferred_printer_uri) - 1);
|
||||
wp->preferred_printer_uri[sizeof(wp->preferred_printer_uri) - 1] = '\0';
|
||||
montauk::strncpy(wp->preferred_printer_name, printer_name, (int)sizeof(wp->preferred_printer_name) - 1);
|
||||
wp->preferred_printer_name[sizeof(wp->preferred_printer_name) - 1] = '\0';
|
||||
wp->preferred_print_copies = copies > 0 ? copies : 1;
|
||||
|
||||
char err[192] = {};
|
||||
if (!wp_printer_accepts_jpeg(printer_uri, err, sizeof(err))) {
|
||||
wp_status_copy(out_status, out_status_len, err);
|
||||
return false;
|
||||
}
|
||||
|
||||
int* line_pages = nullptr;
|
||||
int* line_y = nullptr;
|
||||
int page_count = 0;
|
||||
if (!wp_build_print_page_map(wp, &line_pages, &line_y, &page_count, err, sizeof(err))) {
|
||||
wp_status_copy(out_status, out_status_len, err);
|
||||
return false;
|
||||
}
|
||||
|
||||
char base_name[96] = {};
|
||||
wp_make_base_print_name(wp, base_name, sizeof(base_name));
|
||||
if (!base_name[0]) wp_status_copy(base_name, sizeof(base_name), "document");
|
||||
|
||||
int submitted_pages = 0;
|
||||
char first_job_id[64] = {};
|
||||
|
||||
for (int page = 0; page < page_count; page++) {
|
||||
uint8_t* jpeg = nullptr;
|
||||
int jpeg_len = 0;
|
||||
if (!wp_encode_print_page(wp, line_pages, line_y, page, &jpeg, &jpeg_len, err, sizeof(err))) {
|
||||
break;
|
||||
}
|
||||
|
||||
char page_job_name[128];
|
||||
char source_name[128];
|
||||
if (page_count > 1) {
|
||||
snprintf(page_job_name, sizeof(page_job_name), "%s (Page %d/%d)", job_name, page + 1, page_count);
|
||||
snprintf(source_name, sizeof(source_name), "%s-page-%03d.jpg", base_name, page + 1);
|
||||
} else {
|
||||
wp_status_copy(page_job_name, sizeof(page_job_name), job_name);
|
||||
snprintf(source_name, sizeof(source_name), "%s.jpg", base_name);
|
||||
}
|
||||
|
||||
char job_id[64] = {};
|
||||
bool ok = print::submit_document_buffer_job(
|
||||
jpeg, jpeg_len,
|
||||
source_name,
|
||||
"image/jpeg",
|
||||
printer_uri,
|
||||
page_job_name,
|
||||
(int)(copies > 0 ? copies : 1),
|
||||
job_id, sizeof(job_id),
|
||||
err, sizeof(err));
|
||||
montauk::mfree(jpeg);
|
||||
if (!ok) break;
|
||||
|
||||
if (submitted_pages == 0)
|
||||
wp_status_copy(first_job_id, sizeof(first_job_id), job_id);
|
||||
submitted_pages++;
|
||||
}
|
||||
|
||||
montauk::mfree(line_pages);
|
||||
montauk::mfree(line_y);
|
||||
|
||||
if (submitted_pages != page_count) {
|
||||
if (submitted_pages > 0) {
|
||||
char partial[192];
|
||||
snprintf(partial, sizeof(partial),
|
||||
"Queued %d of %d pages before failing: %s",
|
||||
submitted_pages, page_count,
|
||||
err[0] ? err : "unknown error");
|
||||
wp_status_copy(out_status, out_status_len, partial);
|
||||
} else {
|
||||
wp_status_copy(out_status, out_status_len, err[0] ? err : "Failed to queue print job");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
char summary[192];
|
||||
const char* label = printer_name[0] ? printer_name : printer_uri;
|
||||
if (page_count == 1) {
|
||||
if (copies > 1) {
|
||||
snprintf(summary, sizeof(summary),
|
||||
"Queued 1 page to %s (%u copies), job %s",
|
||||
label, (unsigned)copies, first_job_id);
|
||||
} else {
|
||||
snprintf(summary, sizeof(summary),
|
||||
"Queued print job %s to %s",
|
||||
first_job_id, label);
|
||||
}
|
||||
} else if (copies > 1) {
|
||||
snprintf(summary, sizeof(summary),
|
||||
"Queued %d pages to %s (%u copies each)",
|
||||
page_count, label, (unsigned)copies);
|
||||
} else {
|
||||
snprintf(summary, sizeof(summary),
|
||||
"Queued %d pages to %s",
|
||||
page_count, label);
|
||||
}
|
||||
|
||||
wp_status_copy(out_status, out_status_len, summary);
|
||||
return true;
|
||||
}
|
||||
@@ -35,6 +35,40 @@ static void wp_draw_ui_icon_button(Canvas& c, int x, int y, int w, int h,
|
||||
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 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, 0, 0, 0, 0, 0 };
|
||||
@@ -110,7 +144,11 @@ void wp_render() {
|
||||
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);
|
||||
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);
|
||||
|
||||
c.vline(WP_BTN_PRINT_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);
|
||||
@@ -167,7 +205,8 @@ void wp_render() {
|
||||
|
||||
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);
|
||||
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);
|
||||
@@ -328,18 +367,25 @@ 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[192];
|
||||
char status_left[320];
|
||||
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);
|
||||
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) {
|
||||
@@ -390,4 +436,24 @@ void wp_render() {
|
||||
wp_draw_ui_text(c, dx + 8, iy + (24 - sfh) / 2, spacing, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ extern "C" {
|
||||
|
||||
using namespace gui;
|
||||
|
||||
static constexpr int INIT_W = 640;
|
||||
static constexpr int INIT_H = 480;
|
||||
static constexpr int INIT_W = 900;
|
||||
static constexpr int INIT_H = 600;
|
||||
static constexpr int WP_TOOLBAR_H = 36;
|
||||
static constexpr int WP_PATHBAR_H = 32;
|
||||
static constexpr int WP_STATUS_H = 24;
|
||||
@@ -41,24 +41,27 @@ 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 WP_BTN_PRINT_X = 60;
|
||||
static constexpr int WP_BTN_UNDO_X = 94;
|
||||
static constexpr int WP_BTN_REDO_X = 122;
|
||||
static constexpr int WP_BTN_BOLD_X = 156;
|
||||
static constexpr int WP_BTN_ITALIC_X = 184;
|
||||
static constexpr int WP_FONT_DD_X = 216;
|
||||
static constexpr int WP_FONT_DD_W = 74;
|
||||
static constexpr int WP_SIZE_DD_X = 296;
|
||||
static constexpr int WP_SIZE_DD_W = 36;
|
||||
static constexpr int WP_BTN_ALIGN_L_X = 344;
|
||||
static constexpr int WP_BTN_ALIGN_C_X = 372;
|
||||
static constexpr int WP_BTN_ALIGN_R_X = 400;
|
||||
static constexpr int WP_BTN_BULLET_X = 432;
|
||||
static constexpr int WP_BTN_NUMBER_X = 460;
|
||||
static constexpr int WP_BTN_OUTDENT_X = 496;
|
||||
static constexpr int WP_BTN_INDENT_X = 524;
|
||||
static constexpr int WP_LINE_DD_X = 558;
|
||||
static constexpr int WP_LINE_DD_W = 48;
|
||||
static constexpr int WP_BTN_SECTION_X = 614;
|
||||
static constexpr int WP_SPECIAL_CHAR_ROW_H = 24;
|
||||
static constexpr int WP_SPECIAL_CHAR_FLYOUT_W = 144;
|
||||
|
||||
static constexpr int FONT_ROBOTO = 0;
|
||||
static constexpr int FONT_NOTOSERIF = 1;
|
||||
@@ -79,6 +82,21 @@ 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;
|
||||
|
||||
struct WpSpecialCharOption {
|
||||
uint8_t code;
|
||||
const char* label;
|
||||
};
|
||||
|
||||
inline constexpr WpSpecialCharOption WP_SPECIAL_CHAR_OPTIONS[] = {
|
||||
{ 0xA7, "Section" },
|
||||
{ 0xB6, "Pilcrow" },
|
||||
{ 0xA9, "Copyright" },
|
||||
{ 0xAE, "Registered" },
|
||||
{ 0xB0, "Degree" },
|
||||
{ 0xB1, "Plus/Minus" },
|
||||
};
|
||||
static constexpr int WP_SPECIAL_CHAR_OPTION_COUNT = 6;
|
||||
|
||||
enum ParagraphAlign : uint8_t {
|
||||
PARA_ALIGN_LEFT = 0,
|
||||
PARA_ALIGN_CENTER = 1,
|
||||
@@ -266,6 +284,10 @@ struct WordProcessorState {
|
||||
bool modified;
|
||||
char filepath[256];
|
||||
char filename[64];
|
||||
char preferred_printer_uri[256];
|
||||
char preferred_printer_name[128];
|
||||
uint32_t preferred_print_copies;
|
||||
char status_msg[160];
|
||||
|
||||
bool show_pathbar;
|
||||
bool pathbar_save_mode;
|
||||
@@ -276,6 +298,7 @@ struct WordProcessorState {
|
||||
bool font_dropdown_open;
|
||||
bool size_dropdown_open;
|
||||
bool line_spacing_dropdown_open;
|
||||
bool special_char_flyout_open;
|
||||
|
||||
UndoSnapshot undo[WP_UNDO_MAX];
|
||||
int undo_count;
|
||||
@@ -289,6 +312,7 @@ extern WordProcessorState g_wp;
|
||||
extern WPFontTable g_wp_fonts;
|
||||
extern SvgIcon g_icon_folder;
|
||||
extern SvgIcon g_icon_save;
|
||||
extern SvgIcon g_icon_print;
|
||||
extern SvgIcon g_icon_undo;
|
||||
extern SvgIcon g_icon_redo;
|
||||
extern SvgIcon g_icon_align_left;
|
||||
@@ -357,6 +381,8 @@ void wp_history_mark_saved(WordProcessorState* wp);
|
||||
bool wp_undo(WordProcessorState* wp);
|
||||
bool wp_redo(WordProcessorState* wp);
|
||||
|
||||
bool wp_print_document(WordProcessorState* wp, char* out_status, int out_status_len);
|
||||
|
||||
void wp_render();
|
||||
void wp_handle_mouse(const Montauk::WinEvent& ev);
|
||||
void wp_handle_key(const Montauk::KeyEvent& key);
|
||||
|
||||
Reference in New Issue
Block a user