wip: printing from word processor, word processor improvements

This commit is contained in:
2026-04-02 11:30:37 +02:00
parent 19090d3ea5
commit 516b53281e
13 changed files with 1379 additions and 126 deletions
+329 -53
View File
@@ -28,8 +28,8 @@ namespace {
constexpr int FILE_INIT_W = 720;
constexpr int FILE_INIT_H = 520;
constexpr int PRINT_INIT_W = 480;
constexpr int PRINT_INIT_H = 260;
constexpr int PRINT_INIT_W = 640;
constexpr int PRINT_INIT_H = 400;
constexpr int TOOLBAR_H = 32;
constexpr int PATHBAR_H = 32;
@@ -46,6 +46,7 @@ constexpr int GRID_PAD = 4;
constexpr int MAX_ENTRIES = 64;
constexpr int MAX_HISTORY = 16;
constexpr int MAX_DRIVES = 16;
constexpr int MAX_DISCOVERED_PRINTERS = 8;
constexpr int BUTTON_H = 30;
constexpr int BUTTON_W = 88;
@@ -137,10 +138,24 @@ struct PrintDialogState {
SvgIcon printer_icon;
char source_path[256];
char job_name[128];
char printer_uri[256];
char printer_name[128];
char status[160];
bool can_print;
struct PrinterRow {
char uri[256];
char name[128];
char status[128];
char seen_at[32];
bool ok;
bool is_default;
} printers[MAX_DISCOVERED_PRINTERS];
int printer_count;
int selected_printer;
uint32_t copies;
int queue_count;
int active_count;
bool daemon_running;
bool submit_mode;
bool source_ready;
char note[160];
char error[160];
int mouse_x;
int mouse_y;
};
@@ -173,7 +188,12 @@ struct FileDialogLayout {
};
struct PrintDialogLayout {
Rect content_rect;
Rect header_rect;
Rect printers_rect;
Rect detail_rect;
Rect copy_minus_btn;
Rect copy_value_rect;
Rect copy_plus_btn;
Rect cancel_btn;
Rect printers_btn;
Rect refresh_btn;
@@ -377,7 +397,13 @@ FileDialogLayout file_layout(const FileDialogState* st) {
PrintDialogLayout print_layout() {
PrintDialogLayout lo = {};
lo.content_rect = {0, 0, g_app.win.width, g_app.win.height};
lo.header_rect = {0, 0, 0, 0};
lo.printers_rect = {16, 16, 212, g_app.win.height - 94};
lo.detail_rect = {244, 16, g_app.win.width - 260, g_app.win.height - 94};
int copy_y = lo.detail_rect.y + 202;
lo.copy_minus_btn = {lo.detail_rect.x + 16, copy_y, 28, 28};
lo.copy_value_rect = {lo.copy_minus_btn.x + 36, copy_y, 104, 28};
lo.copy_plus_btn = {lo.copy_value_rect.x + lo.copy_value_rect.w + 8, copy_y, 28, 28};
int btn_y = g_app.win.height - BUTTON_H - 16;
lo.confirm_btn = {g_app.win.width - BUTTON_W - 16, btn_y, BUTTON_W, BUTTON_H};
lo.cancel_btn = {lo.confirm_btn.x - BUTTON_W - 8, btn_y, BUTTON_W, BUTTON_H};
@@ -386,10 +412,14 @@ PrintDialogLayout print_layout() {
return lo;
}
void dialog_finish(const char* status, const char* path, const char* job_id, const char* message) {
void dialog_finish(const char* status, const char* path, const char* job_id, const char* message,
const char* printer_uri = "", const char* printer_name = "", uint32_t copies = 0) {
safe_copy(g_app.result.status, sizeof(g_app.result.status), status);
safe_copy(g_app.result.path, sizeof(g_app.result.path), path);
safe_copy(g_app.result.job_id, sizeof(g_app.result.job_id), job_id);
safe_copy(g_app.result.printer_uri, sizeof(g_app.result.printer_uri), printer_uri);
safe_copy(g_app.result.printer_name, sizeof(g_app.result.printer_name), printer_name);
g_app.result.copies = copies;
safe_copy(g_app.result.message, sizeof(g_app.result.message), message);
dlg::write_result_file(g_app.request.result_path, &g_app.result);
g_app.running = false;
@@ -1080,35 +1110,149 @@ void draw_file_dialog() {
draw_action_button(c, lo.confirm_btn, confirm_label, confirm_enabled, lo.confirm_btn.contains(st->mouse_x, st->mouse_y), true);
}
void print_refresh(PrintDialogState* st) {
st->printer_uri[0] = '\0';
st->printer_name[0] = '\0';
st->status[0] = '\0';
st->can_print = false;
int count_dir_entries(const char* path) {
DIR* d = opendir(path);
if (!d) return 0;
if (!print::read_default_printer_uri(st->printer_uri, sizeof(st->printer_uri))) {
safe_copy(st->status, sizeof(st->status), "No default printer configured");
int count = 0;
struct dirent* ent = nullptr;
while ((ent = readdir(d)) != nullptr) {
if (ent->d_name[0] == '.') continue;
count++;
}
closedir(d);
return count;
}
void print_set_error(PrintDialogState* st, const char* msg) {
safe_copy(st->error, sizeof(st->error), msg);
}
void print_adjust_copies(PrintDialogState* st, int delta) {
if (!st) return;
int next = (int)st->copies + delta;
if (next < 1) next = 1;
if (next > 99) next = 99;
st->copies = (uint32_t)next;
}
void print_update_note(PrintDialogState* st) {
if (st->printer_count <= 0) {
safe_copy(st->note, sizeof(st->note), "No printers discovered from the userspace print spooler yet.");
return;
}
print::ProbeState probe = {};
if (print::load_printer_probe_state(&probe) && montauk::streq(probe.printer_uri, st->printer_uri)) {
if (probe.caps.printer_name[0]) safe_copy(st->printer_name, sizeof(st->printer_name), probe.caps.printer_name);
if (probe.message[0]) safe_copy(st->status, sizeof(st->status), probe.message);
if (!st->daemon_running) {
safe_copy(st->note, sizeof(st->note), "The print spooler is offline. Saved printers are still available.");
return;
}
if (!st->printer_name[0]) safe_copy(st->printer_name, sizeof(st->printer_name), "Default printer");
if (!st->status[0]) safe_copy(st->status, sizeof(st->status), "Uses the system print spooler");
int fd = montauk::open(st->source_path);
if (fd >= 0) {
montauk::close(fd);
st->can_print = true;
if (st->submit_mode) {
safe_copy(st->note, sizeof(st->note), "The selected document will be handed to the userspace print spooler.");
} else {
safe_copy(st->status, sizeof(st->status), "Source document is no longer available");
safe_copy(st->note, sizeof(st->note), "This app stores printer settings only for now. Document output wiring will be added later.");
}
}
int print_find_printer(const PrintDialogState* st, const char* uri) {
if (!st || !uri || !uri[0]) return -1;
for (int i = 0; i < st->printer_count; i++) {
if (montauk::streq(st->printers[i].uri, uri)) return i;
}
return -1;
}
void print_add_printer(PrintDialogState* st,
const char* uri,
const char* name,
const char* status,
const char* seen_at,
bool ok,
bool is_default) {
if (!st || !uri || !uri[0]) return;
int idx = print_find_printer(st, uri);
if (idx < 0) {
if (st->printer_count >= MAX_DISCOVERED_PRINTERS) return;
idx = st->printer_count++;
montauk::memset(&st->printers[idx], 0, sizeof(st->printers[idx]));
safe_copy(st->printers[idx].uri, sizeof(st->printers[idx].uri), uri);
}
if (name && name[0]) safe_copy(st->printers[idx].name, sizeof(st->printers[idx].name), name);
if (status && status[0]) safe_copy(st->printers[idx].status, sizeof(st->printers[idx].status), status);
if (seen_at && seen_at[0]) safe_copy(st->printers[idx].seen_at, sizeof(st->printers[idx].seen_at), seen_at);
st->printers[idx].ok = ok;
st->printers[idx].is_default = is_default;
}
Rect print_row_rect(const PrintDialogLayout& lo, int idx) {
return {lo.printers_rect.x + 8, lo.printers_rect.y + 28 + idx * 58, lo.printers_rect.w - 16, 52};
}
bool print_can_confirm(const PrintDialogState* st) {
if (!st || st->selected_printer < 0 || st->selected_printer >= st->printer_count) return false;
if (!st->submit_mode) return true;
return st->source_ready;
}
void print_refresh(PrintDialogState* st) {
if (!st) return;
char preferred_uri[256] = {};
if (st->selected_printer >= 0 && st->selected_printer < st->printer_count)
safe_copy(preferred_uri, sizeof(preferred_uri), st->printers[st->selected_printer].uri);
else if (g_app.request.printer_uri[0])
safe_copy(preferred_uri, sizeof(preferred_uri), g_app.request.printer_uri);
st->printer_count = 0;
st->selected_printer = -1;
st->error[0] = '\0';
st->queue_count = count_dir_entries(print::SPOOL_QUEUE_DIR);
st->active_count = count_dir_entries(print::SPOOL_ACTIVE_DIR);
int pid = -1;
st->daemon_running = print::daemon_is_running(&pid);
print::KnownPrinter known[MAX_DISCOVERED_PRINTERS];
int known_count = print::list_known_printers(known, MAX_DISCOVERED_PRINTERS);
for (int i = 0; i < known_count; i++) {
const char* status = known[i].status_message[0]
? known[i].status_message
: (known[i].ok ? "Ready for queued jobs" : "Saved printer");
print_add_printer(st, known[i].uri, known[i].display_name, status, known[i].last_seen, known[i].ok, known[i].is_default);
}
if (st->printer_count <= 0) {
char default_uri[256] = {};
bool have_default = print::read_default_printer_uri(default_uri, sizeof(default_uri));
print::ProbeState probe = {};
bool have_probe = print::load_printer_probe_state(&probe);
if (have_probe && probe.printer_uri[0]) {
const char* status = probe.message[0] ? probe.message
: (probe.caps.status_message[0] ? probe.caps.status_message
: (probe.ok ? "Ready for queued jobs" : "Saved printer"));
print_add_printer(st, probe.printer_uri, probe.caps.printer_name, status, probe.probed_at,
probe.ok, have_default && montauk::streq(default_uri, probe.printer_uri));
} else if (have_default) {
print_add_printer(st, default_uri, "Configured printer", "Saved by the print spooler", "", false, true);
}
}
if (preferred_uri[0]) st->selected_printer = print_find_printer(st, preferred_uri);
if (st->selected_printer < 0) {
for (int i = 0; i < st->printer_count; i++) {
if (st->printers[i].is_default) {
st->selected_printer = i;
break;
}
}
}
if (st->selected_printer < 0 && st->printer_count > 0) st->selected_printer = 0;
st->source_ready = !st->submit_mode || (st->source_path[0] && path_exists_via_open(st->source_path));
print_update_note(st);
}
void init_print_dialog(const dlg::Request& req) {
PrintDialogState* st = &g_app.print;
montauk::memset(st, 0, sizeof(*st));
@@ -1116,6 +1260,8 @@ void init_print_dialog(const dlg::Request& req) {
st->printer_icon = svg_load("0:/icons/printer-symbolic.svg", 24, 24, def);
safe_copy(st->source_path, sizeof(st->source_path), req.source_path);
safe_copy(st->job_name, sizeof(st->job_name), req.job_name);
st->submit_mode = req.mode == dlg::PRINT_DIALOG_SUBMIT;
st->copies = req.copies > 0 ? req.copies : 1;
print_refresh(st);
}
@@ -1124,44 +1270,132 @@ void draw_print_dialog() {
Canvas c = g_app.win.canvas();
c.fill(colors::WINDOW_BG);
PrintDialogLayout lo = print_layout();
const auto* selected = (st->selected_printer >= 0 && st->selected_printer < st->printer_count)
? &st->printers[st->selected_printer]
: nullptr;
c.fill_rect(0, 0, c.w, 54, Color::from_rgb(0xF7, 0xF7, 0xF7));
c.hline(0, 53, c.w, colors::BORDER);
if (st->printer_icon.pixels) c.icon(18, 15, st->printer_icon);
c.text(54, 16, st->printer_name[0] ? st->printer_name : "Print", colors::TEXT_COLOR);
c.fill_rounded_rect(lo.printers_rect.x, lo.printers_rect.y, lo.printers_rect.w, lo.printers_rect.h, 6, colors::WHITE);
c.rect(lo.printers_rect.x, lo.printers_rect.y, lo.printers_rect.w, lo.printers_rect.h, colors::BORDER);
c.text(lo.printers_rect.x + 12, lo.printers_rect.y + 8, "Printers", Color::from_rgb(0x77, 0x77, 0x77));
char source_line[256];
fit_text_end(path_basename(st->source_path), source_line, sizeof(source_line), c.w - 36);
c.text(18, 78, "Document", Color::from_rgb(0x77, 0x77, 0x77));
c.text(18, 100, source_line, colors::TEXT_COLOR);
if (st->printer_count <= 0) {
c.text(lo.printers_rect.x + 12, lo.printers_rect.y + 38, "No printers discovered", colors::TEXT_COLOR);
c.text(lo.printers_rect.x + 12, lo.printers_rect.y + 58, "Open Printers to configure one.", Color::from_rgb(0x77, 0x77, 0x77));
} else {
for (int i = 0; i < st->printer_count; i++) {
Rect row = print_row_rect(lo, i);
bool selected = i == st->selected_printer;
bool hovered = row.contains(st->mouse_x, st->mouse_y);
Color bg = selected ? colors::MENU_HOVER
: (hovered ? Color::from_rgb(0xF6, 0xF6, 0xF6) : colors::WHITE);
c.fill_rounded_rect(row.x, row.y, row.w, row.h, 6, bg);
c.rect(row.x, row.y, row.w, row.h, selected ? colors::ACCENT : Color::from_rgb(0xE4, 0xE4, 0xE4));
if (st->printer_icon.pixels) c.icon(row.x + 10, row.y + 14, st->printer_icon);
c.text(18, 136, "Printer", Color::from_rgb(0x77, 0x77, 0x77));
char printer_line[256];
fit_text_end(st->printer_uri[0] ? st->printer_uri : "Not configured", printer_line, sizeof(printer_line), c.w - 36);
c.text(18, 158, printer_line, colors::TEXT_COLOR);
char display[128];
if (st->printers[i].is_default && st->printers[i].name[0])
snprintf(display, sizeof(display), "%s (Default)", st->printers[i].name);
else if (st->printers[i].is_default)
snprintf(display, sizeof(display), "Default printer");
else
safe_copy(display, sizeof(display), st->printers[i].name[0] ? st->printers[i].name : st->printers[i].uri);
char status_line[160];
fit_text_end(st->status, status_line, sizeof(status_line), c.w - 36);
c.text(18, 190, status_line, st->can_print ? Color::from_rgb(0x66, 0x66, 0x66) : Color::from_rgb(0xC0, 0x33, 0x33));
char title[112];
fit_text_end(display, title, sizeof(title), row.w - 68);
c.text(row.x + 42, row.y + 9, title, colors::TEXT_COLOR);
char subtitle[256];
safe_copy(subtitle, sizeof(subtitle), st->printers[i].uri[0] ? st->printers[i].uri : "Saved printer");
fit_text_end(subtitle, title, sizeof(title), row.w - 68);
c.text(row.x + 42, row.y + 28, title, Color::from_rgb(0x77, 0x77, 0x77));
}
}
c.fill_rounded_rect(lo.detail_rect.x, lo.detail_rect.y, lo.detail_rect.w, lo.detail_rect.h, 6, colors::WHITE);
c.rect(lo.detail_rect.x, lo.detail_rect.y, lo.detail_rect.w, lo.detail_rect.h, colors::BORDER);
int dx = lo.detail_rect.x + 16;
int dy = lo.detail_rect.y + 18;
c.text(dx, dy, "Document", Color::from_rgb(0x77, 0x77, 0x77));
char line[256];
const char* job_label = st->job_name[0] ? st->job_name
: (st->source_path[0] ? path_basename(st->source_path) : "Untitled document");
fit_text_end(job_label, line, sizeof(line), lo.detail_rect.w - 32);
c.text(dx, dy + 20, line, colors::TEXT_COLOR);
dy += 68;
c.text(dx, dy, "Printer", Color::from_rgb(0x77, 0x77, 0x77));
if (selected) {
const char* title = selected->name[0] ? selected->name : selected->uri;
fit_text_end(title, line, sizeof(line), lo.detail_rect.w - 32);
c.text(dx, dy + 18, line, colors::TEXT_COLOR);
const char* subtitle = selected->uri[0] ? selected->uri : selected->status;
fit_text_end(subtitle, line, sizeof(line), lo.detail_rect.w - 32);
c.text(dx, dy + 36, line, Color::from_rgb(0x77, 0x77, 0x77));
} else {
c.text(dx, dy + 18, "No printer selected", Color::from_rgb(0x77, 0x77, 0x77));
}
dy += 86;
c.text(dx, dy, "Copies", Color::from_rgb(0x77, 0x77, 0x77));
draw_action_button(c, lo.copy_minus_btn, "-", st->copies > 1,
lo.copy_minus_btn.contains(st->mouse_x, st->mouse_y), false);
c.fill_rounded_rect(lo.copy_value_rect.x, lo.copy_value_rect.y, lo.copy_value_rect.w, lo.copy_value_rect.h, 4, Color::from_rgb(0xF6, 0xF6, 0xF6));
c.rect(lo.copy_value_rect.x, lo.copy_value_rect.y, lo.copy_value_rect.w, lo.copy_value_rect.h, colors::BORDER);
snprintf(line, sizeof(line), "%u %s", (unsigned)st->copies, st->copies == 1 ? "copy" : "copies");
c.text(lo.copy_value_rect.x + 10, lo.copy_value_rect.y + 7, line, colors::TEXT_COLOR);
draw_action_button(c, lo.copy_plus_btn, "+", st->copies < 99,
lo.copy_plus_btn.contains(st->mouse_x, st->mouse_y), false);
if (st->error[0]) {
char line[160];
fit_text_end(st->error, line, sizeof(line), lo.cancel_btn.x - 24);
c.text(16, lo.cancel_btn.y - 22, line, Color::from_rgb(0xC0, 0x33, 0x33));
}
draw_action_button(c, lo.refresh_btn, "Refresh", true, lo.refresh_btn.contains(st->mouse_x, st->mouse_y), false);
draw_action_button(c, lo.printers_btn, "Printers", true, lo.printers_btn.contains(st->mouse_x, st->mouse_y), false);
draw_action_button(c, lo.cancel_btn, "Cancel", true, lo.cancel_btn.contains(st->mouse_x, st->mouse_y), false);
draw_action_button(c, lo.confirm_btn, "Print", st->can_print && st->printer_uri[0], lo.confirm_btn.contains(st->mouse_x, st->mouse_y), true);
draw_action_button(c, lo.confirm_btn, "Print", print_can_confirm(st),
lo.confirm_btn.contains(st->mouse_x, st->mouse_y), true);
}
void print_finish_setup(PrintDialogState* st) {
if (!st || st->selected_printer < 0 || st->selected_printer >= st->printer_count) {
print_set_error(st, "Choose a printer to continue");
return;
}
const auto& printer = st->printers[st->selected_printer];
dialog_finish("ok", "", "", "Printer selected",
printer.uri, printer.name, st->copies);
}
void print_submit(PrintDialogState* st) {
if (!st || st->selected_printer < 0 || st->selected_printer >= st->printer_count) {
print_set_error(st, "Choose a printer to print");
return;
}
if (!st->source_ready) {
print_set_error(st, "Source document is unavailable");
return;
}
const auto& printer = st->printers[st->selected_printer];
char job_id[64] = {};
char err[160] = {};
const char* job_name = st->job_name[0] ? st->job_name : path_basename(st->source_path);
if (!print::submit_document_job(st->source_path, nullptr, job_name, job_id, sizeof(job_id), err, sizeof(err))) {
safe_copy(st->status, sizeof(st->status), err[0] ? err : "Failed to queue print job");
if (!print::submit_document_job(st->source_path, printer.uri, job_name,
job_id, sizeof(job_id), err, sizeof(err),
(int)(st->copies > 0 ? st->copies : 1))) {
print_set_error(st, err[0] ? err : "Failed to queue print job");
return;
}
char msg[160];
snprintf(msg, sizeof(msg), "Queued as %s", job_id);
dialog_finish("ok", "", job_id, msg);
dialog_finish("ok", "", job_id, msg, printer.uri, printer.name, st->copies);
}
void text_insert(char* buf, int* len, int* cursor, int max_len, char ch) {
@@ -1375,8 +1609,27 @@ void handle_print_mouse(const Montauk::WinEvent& ev) {
if (lo.cancel_btn.contains(me.x, me.y)) { dialog_cancel(); return; }
if (lo.refresh_btn.contains(me.x, me.y)) { print_refresh(st); return; }
if (lo.printers_btn.contains(me.x, me.y)) { montauk::spawn("0:/apps/printers/printers.elf"); return; }
if (lo.confirm_btn.contains(me.x, me.y) && st->can_print && st->printer_uri[0]) {
print_submit(st);
if (lo.copy_minus_btn.contains(me.x, me.y) && st->copies > 1) {
print_adjust_copies(st, -1);
st->error[0] = '\0';
return;
}
if (lo.copy_plus_btn.contains(me.x, me.y) && st->copies < 99) {
print_adjust_copies(st, +1);
st->error[0] = '\0';
return;
}
for (int i = 0; i < st->printer_count; i++) {
Rect row = print_row_rect(lo, i);
if (row.contains(me.x, me.y)) {
st->selected_printer = i;
st->error[0] = '\0';
return;
}
}
if (lo.confirm_btn.contains(me.x, me.y) && print_can_confirm(st)) {
if (st->submit_mode) print_submit(st);
else print_finish_setup(st);
}
}
@@ -1386,11 +1639,34 @@ void handle_print_key(const Montauk::KeyEvent& key) {
dialog_cancel();
return;
}
if ((key.ascii == '\n' || key.ascii == '\r') && g_app.print.can_print && g_app.print.printer_uri[0]) {
print_submit(&g_app.print);
if ((key.ascii == '\n' || key.ascii == '\r') && print_can_confirm(&g_app.print)) {
if (g_app.print.submit_mode) print_submit(&g_app.print);
else print_finish_setup(&g_app.print);
}
if (key.ctrl && (key.ascii == 'r' || key.ascii == 'R')) {
print_refresh(&g_app.print);
return;
}
if ((key.ascii == '+' || key.ascii == '=') && g_app.print.copies < 99) {
print_adjust_copies(&g_app.print, +1);
g_app.print.error[0] = '\0';
return;
}
if ((key.ascii == '-' || key.ascii == '_') && g_app.print.copies > 1) {
print_adjust_copies(&g_app.print, -1);
g_app.print.error[0] = '\0';
return;
}
if ((key.scancode == 0x48 || key.scancode == 0x4B) && g_app.print.printer_count > 0) {
if (g_app.print.selected_printer > 0) g_app.print.selected_printer--;
else g_app.print.selected_printer = 0;
g_app.print.error[0] = '\0';
return;
}
if ((key.scancode == 0x50 || key.scancode == 0x4D) && g_app.print.printer_count > 0) {
if (g_app.print.selected_printer < g_app.print.printer_count - 1) g_app.print.selected_printer++;
else g_app.print.selected_printer = g_app.print.printer_count - 1;
g_app.print.error[0] = '\0';
}
}
+1
View File
@@ -70,6 +70,7 @@ static void print_job_detail(const JobMeta& job, const char* state, bool verbose
if (job.printer_uri[0]) printf(" printer: %s\n", job.printer_uri);
if (job.doc_format[0]) printf(" format: %s\n", job.doc_format);
if (job.copies > 1) printf(" copies: %d\n", job.copies);
if (job.source_name[0]) printf(" source: %s\n", job.source_name);
if (job.remote_job_id > 0) printf(" printer job id: %d\n", job.remote_job_id);
if (job.status_message[0]) printf(" status: %s\n", job.status_message);
+121 -33
View File
@@ -15,8 +15,30 @@ extern "C" {
using namespace print;
static constexpr uint64_t PROBE_REFRESH_OK_MS = 60000;
static constexpr uint64_t PROBE_REFRESH_FAIL_MS = 15000;
static constexpr bool ENABLE_BACKGROUND_PROBE = false;
enum class ClaimResult {
None,
Claimed,
Failed,
};
static void write_daemon_state(const char* phase, const char* subject = nullptr,
const char* detail = nullptr) {
char now[32] = {};
now_string(now, sizeof(now));
char buf[512];
int n = snprintf(buf, sizeof(buf),
"time=%s\npid=%d\nphase=%s\nsubject=%s\ndetail=%s\n",
now,
montauk::getpid(),
phase ? phase : "",
subject ? subject : "",
detail ? detail : "");
if (n < 0) return;
write_text_file_atomic(SPOOL_DAEMON_STATE_PATH, buf);
}
static void log_msg(const char* fmt, ...) {
va_list ap;
@@ -68,6 +90,41 @@ static bool write_pid_file() {
return write_text_file_atomic(SPOOL_DAEMON_PID_PATH, pid);
}
static bool move_job_file(const char* src, const char* dst, char* err = nullptr, int err_len = 0) {
if (err && err_len > 0) err[0] = '\0';
if (src == nullptr || dst == nullptr || *src == '\0' || *dst == '\0') {
safe_copy(err, err_len, "invalid source or destination path");
return false;
}
if (rename(src, dst) == 0) return true;
uint8_t* data = nullptr;
int len = 0;
char io_err[128] = {};
if (!read_file_bytes(src, &data, &len, io_err, sizeof(io_err))) {
char detail[160];
snprintf(detail, sizeof(detail), "rename failed; could not read source (%s)", io_err);
safe_copy(err, err_len, detail);
return false;
}
bool ok = write_file_atomic(dst, data, len, io_err, sizeof(io_err));
free(data);
if (!ok) {
char detail[160];
snprintf(detail, sizeof(detail), "rename failed; could not write destination (%s)", io_err);
safe_copy(err, err_len, detail);
return false;
}
if (remove(src) != 0) {
remove(dst);
safe_copy(err, err_len, "rename failed; copied destination but could not delete source");
return false;
}
return true;
}
static void auto_probe_configured_printer(char* last_uri, int last_uri_len,
uint64_t* last_probe_ms, bool* last_probe_ok,
bool force = false) {
@@ -90,8 +147,7 @@ static void auto_probe_configured_printer(char* last_uri, int last_uri_len,
}
bool uri_changed = strcmp(last_uri, current_uri) != 0;
uint64_t refresh_ms = *last_probe_ok ? PROBE_REFRESH_OK_MS : PROBE_REFRESH_FAIL_MS;
if (!force && !uri_changed && *last_probe_ms != 0 && now - *last_probe_ms < refresh_ms)
if (!force && !uri_changed && *last_probe_ms != 0)
return;
ProbeState state = {};
@@ -136,42 +192,59 @@ static void requeue_stale_active_jobs() {
set_job_state(&job, "queued", "Re-queued after daemon restart");
save_job_to_path_atomic(src, &job);
}
rename(src, dst);
char move_err[160] = {};
if (!move_job_file(src, dst, move_err, sizeof(move_err))) {
log_msg("warning: failed to requeue stale job %s: %s",
ent->d_name, move_err[0] ? move_err : "move failed");
}
}
closedir(dir);
}
static bool claim_next_job(char* out_active_path, int out_active_path_len) {
static ClaimResult claim_next_job(char* out_active_path, int out_active_path_len) {
DIR* dir = opendir(SPOOL_QUEUE_DIR);
if (!dir) return false;
if (!dir) return ClaimResult::None;
bool claimed = false;
char best_name[64] = {};
struct dirent* ent = nullptr;
while ((ent = readdir(dir)) != nullptr) {
if (ent->d_name[0] == '.') continue;
char queued[MAX_PATH_LEN];
char active[MAX_PATH_LEN];
path_join(queued, sizeof(queued), SPOOL_QUEUE_DIR, ent->d_name);
path_join(active, sizeof(active), SPOOL_ACTIVE_DIR, ent->d_name);
if (rename(queued, active) == 0) {
safe_copy(out_active_path, out_active_path_len, active);
claimed = true;
break;
}
if (best_name[0] == '\0' || strcmp(ent->d_name, best_name) < 0)
safe_copy(best_name, sizeof(best_name), ent->d_name);
}
closedir(dir);
return claimed;
if (best_name[0] == '\0') return ClaimResult::None;
char queued[MAX_PATH_LEN];
char active[MAX_PATH_LEN];
path_join(queued, sizeof(queued), SPOOL_QUEUE_DIR, best_name);
path_join(active, sizeof(active), SPOOL_ACTIVE_DIR, best_name);
write_daemon_state("claiming", best_name, queued);
char move_err[160] = {};
if (!move_job_file(queued, active, move_err, sizeof(move_err))) {
char detail[256];
snprintf(detail, sizeof(detail), "%s -> %s (%s)",
queued, active,
move_err[0] ? move_err : "move failed");
write_daemon_state("claim-failed", best_name, detail);
log_msg("failed to claim job %s: %s", best_name, detail);
return ClaimResult::Failed;
}
write_daemon_state("claimed", best_name, active);
safe_copy(out_active_path, out_active_path_len, active);
return ClaimResult::Claimed;
}
static void finalize_job(const char* active_path, const char* target_dir, JobMeta* job) {
save_job_to_path_atomic(active_path, job);
char target[MAX_PATH_LEN];
make_job_file_path(target_dir, job->id, target, sizeof(target));
if (rename(active_path, target) != 0) {
char move_err[160] = {};
if (!move_job_file(active_path, target, move_err, sizeof(move_err))) {
log_msg("warning: failed to move job %s to %s", job->id, target_dir);
write_daemon_state("finalize-failed", job->id,
move_err[0] ? move_err : target_dir);
}
}
@@ -223,12 +296,14 @@ static bool try_print_test_page(const char* active_path, JobMeta* job,
}
char detail[MAX_DEBUG_LEN];
snprintf(detail, sizeof(detail), "submitting test page as %s (%d bytes)",
attempts[i].mime, len);
snprintf(detail, sizeof(detail), "submitting test page as %s (%d bytes, %d %s)",
attempts[i].mime, len,
job->copies > 0 ? job->copies : 1,
(job->copies > 1) ? "copies" : "copy");
persist_job_debug(active_path, job, detail);
bool ok = ipp_print_buffer(job->printer_uri, job->job_name, job->user_name,
attempts[i].mime, data, len, result, err, err_len);
job->copies, attempts[i].mime, data, len, result, err, err_len);
free(data);
if (ok) return true;
}
@@ -245,12 +320,14 @@ static bool try_print_document(const char* active_path, JobMeta* job,
if (!read_file_bytes(job->doc_path, &data, &len, err, err_len)) return false;
char detail[MAX_DEBUG_LEN];
snprintf(detail, sizeof(detail), "submitting %s document (%d bytes)",
job->doc_format[0] ? job->doc_format : "application/octet-stream", len);
snprintf(detail, sizeof(detail), "submitting %s document (%d bytes, %d %s)",
job->doc_format[0] ? job->doc_format : "application/octet-stream", len,
job->copies > 0 ? job->copies : 1,
(job->copies > 1) ? "copies" : "copy");
persist_job_debug(active_path, job, detail);
bool ok = ipp_print_buffer(job->printer_uri, job->job_name, job->user_name,
job->doc_format, data, len, result, err, err_len);
job->copies, job->doc_format, data, len, result, err, err_len);
free(data);
return ok;
}
@@ -259,11 +336,13 @@ static void process_job(const char* active_path) {
JobMeta job = {};
if (!load_job_from_path(active_path, &job)) {
log_msg("dropping unreadable job file: %s", active_path);
write_daemon_state("unreadable-job", active_path, nullptr);
remove(active_path);
return;
}
log_msg("processing job %s (%s)", job.id, job.job_name);
write_daemon_state("processing", job.id, job.job_name);
set_job_state(&job, "printing", "Sending to printer");
set_job_debug(&job, "dispatching queued job");
save_job_to_path_atomic(active_path, &job);
@@ -284,12 +363,14 @@ static void process_job(const char* active_path) {
set_job_debug_from_result(&job, &result, nullptr);
finalize_job(active_path, SPOOL_DONE_DIR, &job);
log_msg("job %s completed (printer job id %d)", job.id, result.job_id);
write_daemon_state("completed", job.id, job.status_message);
} else {
set_job_state(&job, "failed", err[0] ? err : "Print failed");
set_job_debug_from_result(&job, &result, err[0] ? err : nullptr);
finalize_job(active_path, SPOOL_FAILED_DIR, &job);
log_msg("job %s failed: %s", job.id, job.status_message);
if (job.debug_info[0]) log_msg("job %s debug: %s", job.id, job.debug_info);
write_daemon_state("failed", job.id, job.status_message);
}
if (job.doc_path[0] != '\0') remove(job.doc_path);
@@ -316,18 +397,25 @@ extern "C" void _start() {
char last_probed_uri[MAX_PATH_LEN] = {};
uint64_t last_probe_ms = 0;
bool last_probe_ok = false;
auto_probe_configured_printer(last_probed_uri, sizeof(last_probed_uri),
&last_probe_ms, &last_probe_ok, true);
log_msg("ready");
write_daemon_state("ready", nullptr, nullptr);
for (;;) {
auto_probe_configured_printer(last_probed_uri, sizeof(last_probed_uri),
&last_probe_ms, &last_probe_ok);
char active_path[MAX_PATH_LEN];
if (claim_next_job(active_path, sizeof(active_path))) {
ClaimResult claim = claim_next_job(active_path, sizeof(active_path));
if (claim == ClaimResult::Claimed) {
process_job(active_path);
continue;
}
if (claim == ClaimResult::Failed) {
montauk::sleep_ms(1000);
continue;
}
write_daemon_state("idle", nullptr, nullptr);
if (ENABLE_BACKGROUND_PROBE) {
auto_probe_configured_printer(last_probed_uri, sizeof(last_probed_uri),
&last_probe_ms, &last_probe_ok);
}
montauk::sleep_ms(1000);
}
}
+5 -3
View File
@@ -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)
+5
View File
@@ -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;
+49 -2
View File
@@ -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')) {
+4
View File
@@ -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);
+433
View File
@@ -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;
}
+70 -4
View File
@@ -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);
}
}
}
+46 -20
View File
@@ -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);