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
+12
View File
@@ -16,6 +16,7 @@ namespace WinServer {
static int g_uiScale = 1; static int g_uiScale = 1;
static kcp::Mutex wsLock; static kcp::Mutex wsLock;
static constexpr int MaxRetiredSnapshots = MaxWindows * 4; static constexpr int MaxRetiredSnapshots = MaxWindows * 4;
static constexpr uint32_t InitialWindowPixel = 0xFFFFFFFFu;
struct RetiredSnapshot { struct RetiredSnapshot {
bool used; bool used;
@@ -34,6 +35,11 @@ namespace WinServer {
memset(&slot, 0, sizeof(WindowSlot)); memset(&slot, 0, sizeof(WindowSlot));
} }
static void FillSurfaceLocked(Ipc::Surface* surface, int width, int height, uint32_t pixel) {
if (surface == nullptr || width <= 0 || height <= 0) return;
Ipc::CopySurfacePreserve(surface, width, height, surface, width, height, pixel);
}
static void RetireSnapshotLocked(int windowId, Ipc::Surface* surface) { static void RetireSnapshotLocked(int windowId, Ipc::Surface* surface) {
if (surface == nullptr) return; if (surface == nullptr) return;
@@ -127,6 +133,12 @@ namespace WinServer {
Ipc::RetainSurface(snapshot); Ipc::RetainSurface(snapshot);
Ipc::RetainMailbox(mailbox, true, true); Ipc::RetainMailbox(mailbox, true, true);
// New app windows become visible to the compositor before the app has
// necessarily rendered its first frame. Seed both buffers with the
// normal white document background so we don't flash an all-zero frame.
FillSurfaceLocked(live, w, h, InitialWindowPixel);
FillSurfaceLocked(snapshot, w, h, InitialWindowPixel);
uint64_t userVa = 0; uint64_t userVa = 0;
if (Ipc::MapSurfaceForPid(live, ownerPid, ownerPml4, heapNext, userVa) != 0) { if (Ipc::MapSurfaceForPid(live, ownerPid, ownerPml4, heapNext, userVa) != 0) {
Ipc::ReleaseMailbox(mailbox, true, true); Ipc::ReleaseMailbox(mailbox, true, true);
+51
View File
@@ -27,6 +27,11 @@ enum FileDialogMode : uint8_t {
FILE_DIALOG_SAVE = 2, FILE_DIALOG_SAVE = 2,
}; };
enum PrintDialogMode : uint8_t {
PRINT_DIALOG_SETUP = 1,
PRINT_DIALOG_SUBMIT = 2,
};
struct Request { struct Request {
uint8_t kind; uint8_t kind;
uint8_t mode; uint8_t mode;
@@ -35,6 +40,8 @@ struct Request {
char suggested_name[128]; char suggested_name[128];
char source_path[256]; char source_path[256];
char job_name[128]; char job_name[128];
char printer_uri[256];
uint32_t copies;
char result_path[256]; char result_path[256];
}; };
@@ -42,6 +49,9 @@ struct Result {
char status[16]; char status[16];
char path[256]; char path[256];
char job_id[64]; char job_id[64];
char printer_uri[256];
char printer_name[128];
uint32_t copies;
char message[160]; char message[160];
}; };
@@ -147,6 +157,8 @@ inline bool write_request_file(const char* path, const Request* req) {
"suggested_name=%s\n" "suggested_name=%s\n"
"source_path=%s\n" "source_path=%s\n"
"job_name=%s\n" "job_name=%s\n"
"printer_uri=%s\n"
"copies=%u\n"
"result_path=%s\n", "result_path=%s\n",
(unsigned)req->kind, (unsigned)req->kind,
(unsigned)req->mode, (unsigned)req->mode,
@@ -155,6 +167,8 @@ inline bool write_request_file(const char* path, const Request* req) {
req->suggested_name, req->suggested_name,
req->source_path, req->source_path,
req->job_name, req->job_name,
req->printer_uri,
(unsigned)req->copies,
req->result_path); req->result_path);
if (n <= 0 || n >= (int)sizeof(text)) return false; if (n <= 0 || n >= (int)sizeof(text)) return false;
return write_text_file(path, text); return write_text_file(path, text);
@@ -174,6 +188,8 @@ inline bool read_request_file(const char* path, Request* req) {
line_value(text, "suggested_name", req->suggested_name, sizeof(req->suggested_name)); line_value(text, "suggested_name", req->suggested_name, sizeof(req->suggested_name));
line_value(text, "source_path", req->source_path, sizeof(req->source_path)); line_value(text, "source_path", req->source_path, sizeof(req->source_path));
line_value(text, "job_name", req->job_name, sizeof(req->job_name)); line_value(text, "job_name", req->job_name, sizeof(req->job_name));
line_value(text, "printer_uri", req->printer_uri, sizeof(req->printer_uri));
if (line_value(text, "copies", value, sizeof(value))) req->copies = parse_u32(value);
line_value(text, "result_path", req->result_path, sizeof(req->result_path)); line_value(text, "result_path", req->result_path, sizeof(req->result_path));
return req->kind != 0 && req->result_path[0] != '\0'; return req->kind != 0 && req->result_path[0] != '\0';
} }
@@ -185,10 +201,16 @@ inline bool write_result_file(const char* path, const Result* res) {
"status=%s\n" "status=%s\n"
"path=%s\n" "path=%s\n"
"job_id=%s\n" "job_id=%s\n"
"printer_uri=%s\n"
"printer_name=%s\n"
"copies=%u\n"
"message=%s\n", "message=%s\n",
res->status, res->status,
res->path, res->path,
res->job_id, res->job_id,
res->printer_uri,
res->printer_name,
(unsigned)res->copies,
res->message); res->message);
if (n <= 0 || n >= (int)sizeof(text)) return false; if (n <= 0 || n >= (int)sizeof(text)) return false;
return write_text_file(path, text); return write_text_file(path, text);
@@ -200,9 +222,13 @@ inline bool read_result_file(const char* path, Result* res) {
if (!read_text_file(path, text, sizeof(text))) return false; if (!read_text_file(path, text, sizeof(text))) return false;
reset_result(res); reset_result(res);
char value[256];
line_value(text, "status", res->status, sizeof(res->status)); line_value(text, "status", res->status, sizeof(res->status));
line_value(text, "path", res->path, sizeof(res->path)); line_value(text, "path", res->path, sizeof(res->path));
line_value(text, "job_id", res->job_id, sizeof(res->job_id)); line_value(text, "job_id", res->job_id, sizeof(res->job_id));
line_value(text, "printer_uri", res->printer_uri, sizeof(res->printer_uri));
line_value(text, "printer_name", res->printer_name, sizeof(res->printer_name));
if (line_value(text, "copies", value, sizeof(value))) res->copies = parse_u32(value);
line_value(text, "message", res->message, sizeof(res->message)); line_value(text, "message", res->message, sizeof(res->message));
return res->status[0] != '\0'; return res->status[0] != '\0';
} }
@@ -311,6 +337,8 @@ inline bool print_file(const char* title,
char* out_message = nullptr, int out_message_len = 0) { char* out_message = nullptr, int out_message_len = 0) {
Request req = {}; Request req = {};
req.kind = REQUEST_KIND_PRINT; req.kind = REQUEST_KIND_PRINT;
req.mode = PRINT_DIALOG_SUBMIT;
req.copies = 1;
safe_copy(req.title, sizeof(req.title), title ? title : "Print"); safe_copy(req.title, sizeof(req.title), title ? title : "Print");
safe_copy(req.source_path, sizeof(req.source_path), source_path); safe_copy(req.source_path, sizeof(req.source_path), source_path);
safe_copy(req.job_name, sizeof(req.job_name), job_name); safe_copy(req.job_name, sizeof(req.job_name), job_name);
@@ -321,4 +349,27 @@ inline bool print_file(const char* title,
return ok; return ok;
} }
inline bool configure_print(const char* title,
const char* initial_printer_uri,
const char* job_name,
char* out_printer_uri, int out_printer_uri_len,
char* out_printer_name, int out_printer_name_len,
uint32_t* out_copies = nullptr,
char* out_message = nullptr, int out_message_len = 0) {
Request req = {};
req.kind = REQUEST_KIND_PRINT;
req.mode = PRINT_DIALOG_SETUP;
req.copies = out_copies && *out_copies > 0 ? *out_copies : 1;
safe_copy(req.title, sizeof(req.title), title ? title : "Print");
safe_copy(req.job_name, sizeof(req.job_name), job_name ? job_name : "");
safe_copy(req.printer_uri, sizeof(req.printer_uri), initial_printer_uri);
Result res = {};
bool ok = run_request(&req, &res, out_message, out_message_len);
if (ok && out_printer_uri && out_printer_uri_len > 0) safe_copy(out_printer_uri, out_printer_uri_len, res.printer_uri);
if (ok && out_printer_name && out_printer_name_len > 0) safe_copy(out_printer_name, out_printer_name_len, res.printer_name);
if (ok && out_copies) *out_copies = res.copies > 0 ? res.copies : 1;
return ok;
}
} // namespace gui::dialogs } // namespace gui::dialogs
+253 -11
View File
@@ -30,7 +30,9 @@ static constexpr const char* SPOOL_DONE_DIR = "0:/spool/print/done";
static constexpr const char* SPOOL_FAILED_DIR = "0:/spool/print/failed"; static constexpr const char* SPOOL_FAILED_DIR = "0:/spool/print/failed";
static constexpr const char* SPOOL_DOCS_DIR = "0:/spool/print/docs"; static constexpr const char* SPOOL_DOCS_DIR = "0:/spool/print/docs";
static constexpr const char* SPOOL_TMP_DIR = "0:/spool/print/tmp"; static constexpr const char* SPOOL_TMP_DIR = "0:/spool/print/tmp";
static constexpr const char* SPOOL_PRINTERS_DIR = "0:/spool/print/printers";
static constexpr const char* SPOOL_DAEMON_PID_PATH = "0:/spool/print/daemon.pid"; static constexpr const char* SPOOL_DAEMON_PID_PATH = "0:/spool/print/daemon.pid";
static constexpr const char* SPOOL_DAEMON_STATE_PATH = "0:/spool/print/daemon.state";
static constexpr const char* SPOOL_PROBE_STATE_PATH = "0:/spool/print/printer-probe.state"; static constexpr const char* SPOOL_PROBE_STATE_PATH = "0:/spool/print/printer-probe.state";
static constexpr const char* PRINTD_BINARY = "0:/os/printd.elf"; static constexpr const char* PRINTD_BINARY = "0:/os/printd.elf";
static constexpr int MAX_PATH_LEN = 256; static constexpr int MAX_PATH_LEN = 256;
@@ -58,6 +60,7 @@ struct JobMeta {
char debug_info[MAX_DEBUG_LEN]; char debug_info[MAX_DEBUG_LEN];
int remote_job_id; int remote_job_id;
int size_bytes; int size_bytes;
int copies;
}; };
struct IppUri { struct IppUri {
@@ -98,6 +101,15 @@ struct ProbeState {
IppCapabilities caps; IppCapabilities caps;
}; };
struct KnownPrinter {
char uri[MAX_PATH_LEN];
char display_name[128];
char last_seen[32];
char status_message[128];
bool ok;
bool is_default;
};
struct IppPrintResult { struct IppPrintResult {
bool ok; bool ok;
bool use_tls; bool use_tls;
@@ -134,6 +146,10 @@ inline void zero_probe_state(ProbeState* state) {
if (state) memset(state, 0, sizeof(*state)); if (state) memset(state, 0, sizeof(*state));
} }
inline void zero_known_printer(KnownPrinter* printer) {
if (printer) memset(printer, 0, sizeof(*printer));
}
inline void safe_copy(char* dst, int dst_len, const char* src) { inline void safe_copy(char* dst, int dst_len, const char* src) {
if (dst == nullptr || dst_len <= 0) return; if (dst == nullptr || dst_len <= 0) return;
if (src == nullptr) { if (src == nullptr) {
@@ -207,7 +223,8 @@ inline bool ensure_spool_dirs() {
&& ensure_dir(SPOOL_DONE_DIR) && ensure_dir(SPOOL_DONE_DIR)
&& ensure_dir(SPOOL_FAILED_DIR) && ensure_dir(SPOOL_FAILED_DIR)
&& ensure_dir(SPOOL_DOCS_DIR) && ensure_dir(SPOOL_DOCS_DIR)
&& ensure_dir(SPOOL_TMP_DIR); && ensure_dir(SPOOL_TMP_DIR)
&& ensure_dir(SPOOL_PRINTERS_DIR);
} }
inline bool path_join(char* out, int out_len, const char* a, const char* b) { inline bool path_join(char* out, int out_len, const char* a, const char* b) {
@@ -370,9 +387,136 @@ inline bool read_default_printer_uri(char* out, int out_len) {
return read_text_file_line(DEFAULT_PRINTER_PATH, out, out_len); return read_text_file_line(DEFAULT_PRINTER_PATH, out, out_len);
} }
inline uint32_t hash_uri_key(const char* text) {
uint32_t hash = 2166136261u;
if (!text) return hash;
for (const unsigned char* p = (const unsigned char*)text; *p; ++p) {
hash ^= (uint32_t)*p;
hash *= 16777619u;
}
return hash;
}
inline void make_known_printer_path(const char* uri, char* out, int out_len) {
unsigned hash = (unsigned)hash_uri_key(uri);
char leaf[32];
snprintf(leaf, sizeof(leaf), "%08x.printer", hash);
path_join(out, out_len, SPOOL_PRINTERS_DIR, leaf);
}
inline bool save_known_printer_atomic(const KnownPrinter* printer) {
if (printer == nullptr || printer->uri[0] == '\0') return false;
char path[MAX_PATH_LEN];
make_known_printer_path(printer->uri, path, sizeof(path));
char text[768];
int n = snprintf(text, sizeof(text),
"uri=%s\n"
"display_name=%s\n"
"last_seen=%s\n"
"status_message=%s\n"
"ok=%d\n",
printer->uri,
printer->display_name,
printer->last_seen,
printer->status_message,
printer->ok ? 1 : 0);
if (n <= 0 || n >= (int)sizeof(text)) return false;
return write_text_file_atomic(path, text);
}
inline bool load_known_printer_from_path(const char* path, KnownPrinter* printer) {
if (path == nullptr || printer == nullptr) return false;
zero_known_printer(printer);
FILE* f = fopen(path, "rb");
if (!f) return false;
char line[384];
while (fgets(line, sizeof(line), f) != nullptr) {
trim_line(line);
char* eq = strchr(line, '=');
if (!eq) continue;
*eq++ = '\0';
if (strcmp(line, "uri") == 0) safe_copy(printer->uri, sizeof(printer->uri), eq);
else if (strcmp(line, "display_name") == 0) safe_copy(printer->display_name, sizeof(printer->display_name), eq);
else if (strcmp(line, "last_seen") == 0) safe_copy(printer->last_seen, sizeof(printer->last_seen), eq);
else if (strcmp(line, "status_message") == 0) safe_copy(printer->status_message, sizeof(printer->status_message), eq);
else if (strcmp(line, "ok") == 0) printer->ok = atoi(eq) != 0;
}
fclose(f);
return printer->uri[0] != '\0';
}
inline bool remember_printer_uri(const char* uri,
const char* display_name,
bool ok,
const char* status_message,
const char* last_seen) {
if (uri == nullptr || *uri == '\0') return false;
ensure_spool_dirs();
KnownPrinter printer = {};
safe_copy(printer.uri, sizeof(printer.uri), uri);
safe_copy(printer.display_name, sizeof(printer.display_name), display_name ? display_name : "");
safe_copy(printer.last_seen, sizeof(printer.last_seen), last_seen ? last_seen : "");
safe_copy(printer.status_message, sizeof(printer.status_message), status_message ? status_message : "");
printer.ok = ok;
return save_known_printer_atomic(&printer);
}
inline int list_known_printers(KnownPrinter* out, int max) {
if (out == nullptr || max <= 0) return 0;
ensure_spool_dirs();
int count = 0;
DIR* d = opendir(SPOOL_PRINTERS_DIR);
if (d) {
struct dirent* ent = nullptr;
while ((ent = readdir(d)) != nullptr && count < max) {
if (ent->d_name[0] == '.') continue;
char path[MAX_PATH_LEN];
path_join(path, sizeof(path), SPOOL_PRINTERS_DIR, ent->d_name);
if (load_known_printer_from_path(path, &out[count]))
count++;
}
closedir(d);
}
char default_uri[MAX_PATH_LEN] = {};
bool have_default = read_default_printer_uri(default_uri, sizeof(default_uri));
for (int i = 0; i < count; i++) {
out[i].is_default = have_default && strcmp(out[i].uri, default_uri) == 0;
}
for (int i = 1; i < count; i++) {
KnownPrinter tmp = out[i];
int j = i - 1;
while (j >= 0) {
bool move = false;
if (tmp.is_default && !out[j].is_default) move = true;
else if (tmp.is_default == out[j].is_default) {
const char* ta = tmp.display_name[0] ? tmp.display_name : tmp.uri;
const char* tb = out[j].display_name[0] ? out[j].display_name : out[j].uri;
if (strcmp(ta, tb) < 0) move = true;
}
if (!move) break;
out[j + 1] = out[j];
j--;
}
out[j + 1] = tmp;
}
return count;
}
inline bool write_default_printer_uri(const char* uri) { inline bool write_default_printer_uri(const char* uri) {
ensure_spool_dirs(); ensure_spool_dirs();
return write_text_file_atomic(DEFAULT_PRINTER_PATH, uri); bool ok = write_text_file_atomic(DEFAULT_PRINTER_PATH, uri);
if (ok && uri && *uri) remember_printer_uri(uri, nullptr, false, "", "");
return ok;
} }
inline bool parse_args(ArgList* out) { inline bool parse_args(ArgList* out) {
@@ -623,14 +767,14 @@ inline bool infer_doc_format(const char* path, char* out, int out_len) {
} }
inline void generate_job_id(char* out, int out_len) { inline void generate_job_id(char* out, int out_len) {
static uint32_t seq = 0;
uint64_t now = montauk::get_milliseconds(); uint64_t now = montauk::get_milliseconds();
uint32_t rnd = 0; uint32_t cur = ++seq;
if (montauk::getrandom(&rnd, sizeof(rnd)) < 0) { unsigned pid = (unsigned)(montauk::getpid() & 0xFFFF);
rnd = (uint32_t)(now ^ (uint64_t)montauk::getpid()); snprintf(out, (size_t)out_len, "%llu-%04x-%04x",
}
snprintf(out, (size_t)out_len, "%llu-%04x",
(unsigned long long)now, (unsigned long long)now,
(unsigned)(rnd & 0xFFFFu)); pid,
(unsigned)(cur & 0xFFFFu));
} }
inline bool save_job_to_path_atomic(const char* path, const JobMeta* job) { inline bool save_job_to_path_atomic(const char* path, const JobMeta* job) {
@@ -668,6 +812,7 @@ inline bool save_job_to_path_atomic(const char* path, const JobMeta* job) {
fprintf(f, "debug_info=%s\n", copy.debug_info); fprintf(f, "debug_info=%s\n", copy.debug_info);
fprintf(f, "remote_job_id=%d\n", copy.remote_job_id); fprintf(f, "remote_job_id=%d\n", copy.remote_job_id);
fprintf(f, "size_bytes=%d\n", copy.size_bytes); fprintf(f, "size_bytes=%d\n", copy.size_bytes);
fprintf(f, "copies=%d\n", copy.copies);
fclose(f); fclose(f);
if (rename(tmp, path) != 0) { if (rename(tmp, path) != 0) {
@@ -706,8 +851,10 @@ inline bool load_job_from_path(const char* path, JobMeta* job) {
else if (strcmp(line, "debug_info") == 0) safe_copy(job->debug_info, sizeof(job->debug_info), eq); else if (strcmp(line, "debug_info") == 0) safe_copy(job->debug_info, sizeof(job->debug_info), eq);
else if (strcmp(line, "remote_job_id") == 0) job->remote_job_id = atoi(eq); else if (strcmp(line, "remote_job_id") == 0) job->remote_job_id = atoi(eq);
else if (strcmp(line, "size_bytes") == 0) job->size_bytes = atoi(eq); else if (strcmp(line, "size_bytes") == 0) job->size_bytes = atoi(eq);
else if (strcmp(line, "copies") == 0) job->copies = atoi(eq);
} }
fclose(f); fclose(f);
if (job->copies <= 0) job->copies = 1;
return job->id[0] != '\0'; return job->id[0] != '\0';
} }
@@ -806,7 +953,14 @@ inline bool load_probe_state(const char* path, ProbeState* state) {
inline bool save_printer_probe_state(const ProbeState* state) { inline bool save_printer_probe_state(const ProbeState* state) {
ensure_spool_dirs(); ensure_spool_dirs();
return save_probe_state_atomic(SPOOL_PROBE_STATE_PATH, state); bool ok = save_probe_state_atomic(SPOOL_PROBE_STATE_PATH, state);
if (ok && state && state->printer_uri[0]) {
const char* name = state->caps.printer_name[0] ? state->caps.printer_name : nullptr;
const char* msg = state->message[0] ? state->message
: (state->caps.status_message[0] ? state->caps.status_message : "");
remember_printer_uri(state->printer_uri, name, state->ok, msg, state->probed_at);
}
return ok;
} }
inline bool load_printer_probe_state(ProbeState* state) { inline bool load_printer_probe_state(ProbeState* state) {
@@ -853,6 +1007,17 @@ inline bool daemon_is_running(int* out_pid) {
uint32_t sig = montauk::wait_handle(handle, Montauk::IPC_SIGNAL_EXITED, 0); uint32_t sig = montauk::wait_handle(handle, Montauk::IPC_SIGNAL_EXITED, 0);
montauk::close(handle); montauk::close(handle);
if (sig & Montauk::IPC_SIGNAL_EXITED) return false; if (sig & Montauk::IPC_SIGNAL_EXITED) return false;
Montauk::ProcInfo procs[128];
int proc_count = montauk::proclist(procs, (int)(sizeof(procs) / sizeof(procs[0])));
bool matched = false;
for (int i = 0; i < proc_count; i++) {
if (procs[i].pid != pid) continue;
if (strcmp(procs[i].name, PRINTD_BINARY) == 0) matched = true;
break;
}
if (!matched) return false;
if (out_pid) *out_pid = pid; if (out_pid) *out_pid = pid;
return true; return true;
} }
@@ -1709,6 +1874,7 @@ inline bool probe_printer_uri(const char* printer_uri, ProbeState* out, char* er
inline bool ipp_print_buffer(const char* printer_uri, inline bool ipp_print_buffer(const char* printer_uri,
const char* job_name, const char* job_name,
const char* user_name, const char* user_name,
int copies,
const char* doc_format, const char* doc_format,
const uint8_t* data, int data_len, const uint8_t* data, int data_len,
IppPrintResult* out, IppPrintResult* out,
@@ -1738,6 +1904,7 @@ inline bool ipp_print_buffer(const char* printer_uri,
const char* use_job_name = (job_name && *job_name) ? job_name : "MontaukOS Print Job"; const char* use_job_name = (job_name && *job_name) ? job_name : "MontaukOS Print Job";
const char* use_user = (user_name && *user_name) ? user_name : "montauk"; const char* use_user = (user_name && *user_name) ? user_name : "montauk";
const char* use_format = (doc_format && *doc_format) ? doc_format : "application/octet-stream"; const char* use_format = (doc_format && *doc_format) ? doc_format : "application/octet-stream";
int use_copies = copies > 0 ? copies : 1;
if (out) { if (out) {
out->request_id = (int)request_id; out->request_id = (int)request_id;
safe_copy(out->document_format, sizeof(out->document_format), use_format); safe_copy(out->document_format, sizeof(out->document_format), use_format);
@@ -1752,6 +1919,7 @@ inline bool ipp_print_buffer(const char* printer_uri,
&& bb_put_attr_str(&req, 0x45, "printer-uri", uri.normalized) && bb_put_attr_str(&req, 0x45, "printer-uri", uri.normalized)
&& bb_put_attr_str(&req, 0x42, "requesting-user-name", use_user) && bb_put_attr_str(&req, 0x42, "requesting-user-name", use_user)
&& bb_put_attr_str(&req, 0x42, "job-name", use_job_name) && bb_put_attr_str(&req, 0x42, "job-name", use_job_name)
&& bb_put_attr_int(&req, 0x21, "copies", use_copies)
&& bb_put_attr_str(&req, 0x49, "document-format", use_format) && bb_put_attr_str(&req, 0x49, "document-format", use_format)
&& bb_put8(&req, 0x03) && bb_put8(&req, 0x03)
&& bb_put_bytes(&req, data, data_len); && bb_put_bytes(&req, data, data_len);
@@ -1802,7 +1970,8 @@ inline bool submit_document_job(const char* source_path,
const char* printer_uri, const char* printer_uri,
const char* job_name_override, const char* job_name_override,
char* out_job_id, int out_job_id_len, char* out_job_id, int out_job_id_len,
char* err, int err_len) { char* err, int err_len,
int copies = 1) {
ensure_spool_dirs(); ensure_spool_dirs();
char resolved_uri[MAX_PATH_LEN]; char resolved_uri[MAX_PATH_LEN];
@@ -1843,6 +2012,77 @@ inline bool submit_document_job(const char* source_path,
uri.normalized, job.doc_format, job.source_name); uri.normalized, job.doc_format, job.source_name);
job.remote_job_id = 0; job.remote_job_id = 0;
job.size_bytes = size_bytes; job.size_bytes = size_bytes;
job.copies = copies > 0 ? copies : 1;
char job_path[MAX_PATH_LEN];
make_job_file_path(SPOOL_QUEUE_DIR, job.id, job_path, sizeof(job_path));
if (!save_job_to_path_atomic(job_path, &job)) {
remove(doc_path);
safe_copy(err, err_len, "failed to create queued job");
return false;
}
if (!ensure_daemon_running(err, err_len)) return false;
safe_copy(out_job_id, out_job_id_len, job.id);
return true;
}
inline bool submit_document_buffer_job(const uint8_t* data, int data_len,
const char* source_name,
const char* doc_format,
const char* printer_uri,
const char* job_name_override,
int copies = 1,
char* out_job_id = nullptr, int out_job_id_len = 0,
char* err = nullptr, int err_len = 0) {
ensure_spool_dirs();
if (data == nullptr || data_len < 0) {
safe_copy(err, err_len, "missing print document");
return false;
}
char resolved_uri[MAX_PATH_LEN];
if (printer_uri && *printer_uri) safe_copy(resolved_uri, sizeof(resolved_uri), printer_uri);
else if (!read_default_printer_uri(resolved_uri, sizeof(resolved_uri))) {
safe_copy(err, err_len, "no default printer is configured");
return false;
}
IppUri uri = {};
if (!parse_ipp_uri(resolved_uri, &uri, err, err_len)) return false;
char job_id[48];
generate_job_id(job_id, sizeof(job_id));
char doc_path[MAX_PATH_LEN];
make_doc_file_path(job_id, doc_path, sizeof(doc_path));
if (!write_file_atomic(doc_path, data, data_len, err, err_len)) return false;
JobMeta job = {};
safe_copy(job.id, sizeof(job.id), job_id);
safe_copy(job.state, sizeof(job.state), "queued");
safe_copy(job.source_kind, sizeof(job.source_kind), "document");
safe_copy(job.job_name, sizeof(job.job_name),
(job_name_override && *job_name_override) ? job_name_override
: (source_name && *source_name ? source_name : "document"));
safe_copy(job.user_name, sizeof(job.user_name), "montauk");
safe_copy(job.printer_uri, sizeof(job.printer_uri), uri.normalized);
safe_copy(job.doc_path, sizeof(job.doc_path), doc_path);
safe_copy(job.doc_format, sizeof(job.doc_format),
(doc_format && *doc_format) ? doc_format : "application/octet-stream");
safe_copy(job.source_name, sizeof(job.source_name),
(source_name && *source_name) ? source_name : "document");
now_string(job.created_at, sizeof(job.created_at));
safe_copy(job.updated_at, sizeof(job.updated_at), job.created_at);
safe_copy(job.status_message, sizeof(job.status_message), "Queued");
snprintf(job.debug_info, sizeof(job.debug_info),
"printer=%s format=%s source=%s",
uri.normalized, job.doc_format, job.source_name);
job.remote_job_id = 0;
job.size_bytes = data_len;
job.copies = copies > 0 ? copies : 1;
char job_path[MAX_PATH_LEN]; char job_path[MAX_PATH_LEN];
make_job_file_path(SPOOL_QUEUE_DIR, job.id, job_path, sizeof(job_path)); make_job_file_path(SPOOL_QUEUE_DIR, job.id, job_path, sizeof(job_path));
@@ -1860,7 +2100,8 @@ inline bool submit_document_job(const char* source_path,
inline bool submit_test_page_job(const char* printer_uri, inline bool submit_test_page_job(const char* printer_uri,
char* out_job_id, int out_job_id_len, char* out_job_id, int out_job_id_len,
char* err, int err_len) { char* err, int err_len,
int copies = 1) {
ensure_spool_dirs(); ensure_spool_dirs();
char resolved_uri[MAX_PATH_LEN]; char resolved_uri[MAX_PATH_LEN];
@@ -1889,6 +2130,7 @@ inline bool submit_test_page_job(const char* printer_uri,
snprintf(job.debug_info, sizeof(job.debug_info), snprintf(job.debug_info, sizeof(job.debug_info),
"printer=%s format=auto source=%s", "printer=%s format=auto source=%s",
uri.normalized, job.source_name); uri.normalized, job.source_name);
job.copies = copies > 0 ? copies : 1;
char job_path[MAX_PATH_LEN]; char job_path[MAX_PATH_LEN];
make_job_file_path(SPOOL_QUEUE_DIR, job.id, job_path, sizeof(job_path)); make_job_file_path(SPOOL_QUEUE_DIR, job.id, job_path, sizeof(job_path));
+329 -53
View File
@@ -28,8 +28,8 @@ namespace {
constexpr int FILE_INIT_W = 720; constexpr int FILE_INIT_W = 720;
constexpr int FILE_INIT_H = 520; constexpr int FILE_INIT_H = 520;
constexpr int PRINT_INIT_W = 480; constexpr int PRINT_INIT_W = 640;
constexpr int PRINT_INIT_H = 260; constexpr int PRINT_INIT_H = 400;
constexpr int TOOLBAR_H = 32; constexpr int TOOLBAR_H = 32;
constexpr int PATHBAR_H = 32; constexpr int PATHBAR_H = 32;
@@ -46,6 +46,7 @@ constexpr int GRID_PAD = 4;
constexpr int MAX_ENTRIES = 64; constexpr int MAX_ENTRIES = 64;
constexpr int MAX_HISTORY = 16; constexpr int MAX_HISTORY = 16;
constexpr int MAX_DRIVES = 16; constexpr int MAX_DRIVES = 16;
constexpr int MAX_DISCOVERED_PRINTERS = 8;
constexpr int BUTTON_H = 30; constexpr int BUTTON_H = 30;
constexpr int BUTTON_W = 88; constexpr int BUTTON_W = 88;
@@ -137,10 +138,24 @@ struct PrintDialogState {
SvgIcon printer_icon; SvgIcon printer_icon;
char source_path[256]; char source_path[256];
char job_name[128]; char job_name[128];
char printer_uri[256]; struct PrinterRow {
char printer_name[128]; char uri[256];
char status[160]; char name[128];
bool can_print; 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_x;
int mouse_y; int mouse_y;
}; };
@@ -173,7 +188,12 @@ struct FileDialogLayout {
}; };
struct PrintDialogLayout { 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 cancel_btn;
Rect printers_btn; Rect printers_btn;
Rect refresh_btn; Rect refresh_btn;
@@ -377,7 +397,13 @@ FileDialogLayout file_layout(const FileDialogState* st) {
PrintDialogLayout print_layout() { PrintDialogLayout print_layout() {
PrintDialogLayout lo = {}; 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; 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.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}; 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; 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.status, sizeof(g_app.result.status), status);
safe_copy(g_app.result.path, sizeof(g_app.result.path), path); 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.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); safe_copy(g_app.result.message, sizeof(g_app.result.message), message);
dlg::write_result_file(g_app.request.result_path, &g_app.result); dlg::write_result_file(g_app.request.result_path, &g_app.result);
g_app.running = false; 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); 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) { int count_dir_entries(const char* path) {
st->printer_uri[0] = '\0'; DIR* d = opendir(path);
st->printer_name[0] = '\0'; if (!d) return 0;
st->status[0] = '\0';
st->can_print = false;
if (!print::read_default_printer_uri(st->printer_uri, sizeof(st->printer_uri))) { int count = 0;
safe_copy(st->status, sizeof(st->status), "No default printer configured"); 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; return;
} }
if (!st->daemon_running) {
print::ProbeState probe = {}; safe_copy(st->note, sizeof(st->note), "The print spooler is offline. Saved printers are still available.");
if (print::load_printer_probe_state(&probe) && montauk::streq(probe.printer_uri, st->printer_uri)) { return;
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->submit_mode) {
if (!st->printer_name[0]) safe_copy(st->printer_name, sizeof(st->printer_name), "Default printer"); safe_copy(st->note, sizeof(st->note), "The selected document will be handed to the userspace print spooler.");
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;
} else { } 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) { void init_print_dialog(const dlg::Request& req) {
PrintDialogState* st = &g_app.print; PrintDialogState* st = &g_app.print;
montauk::memset(st, 0, sizeof(*st)); 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); 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->source_path, sizeof(st->source_path), req.source_path);
safe_copy(st->job_name, sizeof(st->job_name), req.job_name); 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); print_refresh(st);
} }
@@ -1124,44 +1270,132 @@ void draw_print_dialog() {
Canvas c = g_app.win.canvas(); Canvas c = g_app.win.canvas();
c.fill(colors::WINDOW_BG); c.fill(colors::WINDOW_BG);
PrintDialogLayout lo = print_layout(); 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.fill_rounded_rect(lo.printers_rect.x, lo.printers_rect.y, lo.printers_rect.w, lo.printers_rect.h, 6, colors::WHITE);
c.hline(0, 53, c.w, colors::BORDER); c.rect(lo.printers_rect.x, lo.printers_rect.y, lo.printers_rect.w, lo.printers_rect.h, colors::BORDER);
if (st->printer_icon.pixels) c.icon(18, 15, st->printer_icon); c.text(lo.printers_rect.x + 12, lo.printers_rect.y + 8, "Printers", Color::from_rgb(0x77, 0x77, 0x77));
c.text(54, 16, st->printer_name[0] ? st->printer_name : "Print", colors::TEXT_COLOR);
char source_line[256]; if (st->printer_count <= 0) {
fit_text_end(path_basename(st->source_path), source_line, sizeof(source_line), c.w - 36); c.text(lo.printers_rect.x + 12, lo.printers_rect.y + 38, "No printers discovered", colors::TEXT_COLOR);
c.text(18, 78, "Document", Color::from_rgb(0x77, 0x77, 0x77)); c.text(lo.printers_rect.x + 12, lo.printers_rect.y + 58, "Open Printers to configure one.", Color::from_rgb(0x77, 0x77, 0x77));
c.text(18, 100, source_line, colors::TEXT_COLOR); } 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 display[128];
char printer_line[256]; if (st->printers[i].is_default && st->printers[i].name[0])
fit_text_end(st->printer_uri[0] ? st->printer_uri : "Not configured", printer_line, sizeof(printer_line), c.w - 36); snprintf(display, sizeof(display), "%s (Default)", st->printers[i].name);
c.text(18, 158, printer_line, colors::TEXT_COLOR); 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]; char title[112];
fit_text_end(st->status, status_line, sizeof(status_line), c.w - 36); fit_text_end(display, title, sizeof(title), row.w - 68);
c.text(18, 190, status_line, st->can_print ? Color::from_rgb(0x66, 0x66, 0x66) : Color::from_rgb(0xC0, 0x33, 0x33)); 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.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.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.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) { 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 job_id[64] = {};
char err[160] = {}; char err[160] = {};
const char* job_name = st->job_name[0] ? st->job_name : path_basename(st->source_path); 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))) { if (!print::submit_document_job(st->source_path, printer.uri, job_name,
safe_copy(st->status, sizeof(st->status), err[0] ? err : "Failed to queue print job"); 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; return;
} }
char msg[160]; char msg[160];
snprintf(msg, sizeof(msg), "Queued as %s", job_id); 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) { 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.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.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.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]) { if (lo.copy_minus_btn.contains(me.x, me.y) && st->copies > 1) {
print_submit(st); 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(); dialog_cancel();
return; return;
} }
if ((key.ascii == '\n' || key.ascii == '\r') && g_app.print.can_print && g_app.print.printer_uri[0]) { if ((key.ascii == '\n' || key.ascii == '\r') && print_can_confirm(&g_app.print)) {
print_submit(&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')) { if (key.ctrl && (key.ascii == 'r' || key.ascii == 'R')) {
print_refresh(&g_app.print); 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.printer_uri[0]) printf(" printer: %s\n", job.printer_uri);
if (job.doc_format[0]) printf(" format: %s\n", job.doc_format); 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.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.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); if (job.status_message[0]) printf(" status: %s\n", job.status_message);
+118 -30
View File
@@ -15,8 +15,30 @@ extern "C" {
using namespace print; using namespace print;
static constexpr uint64_t PROBE_REFRESH_OK_MS = 60000; static constexpr bool ENABLE_BACKGROUND_PROBE = false;
static constexpr uint64_t PROBE_REFRESH_FAIL_MS = 15000;
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, ...) { static void log_msg(const char* fmt, ...) {
va_list ap; va_list ap;
@@ -68,6 +90,41 @@ static bool write_pid_file() {
return write_text_file_atomic(SPOOL_DAEMON_PID_PATH, pid); 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, static void auto_probe_configured_printer(char* last_uri, int last_uri_len,
uint64_t* last_probe_ms, bool* last_probe_ok, uint64_t* last_probe_ms, bool* last_probe_ok,
bool force = false) { 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; 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)
if (!force && !uri_changed && *last_probe_ms != 0 && now - *last_probe_ms < refresh_ms)
return; return;
ProbeState state = {}; ProbeState state = {};
@@ -136,42 +192,59 @@ static void requeue_stale_active_jobs() {
set_job_state(&job, "queued", "Re-queued after daemon restart"); set_job_state(&job, "queued", "Re-queued after daemon restart");
save_job_to_path_atomic(src, &job); 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); 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); 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; struct dirent* ent = nullptr;
while ((ent = readdir(dir)) != nullptr) { while ((ent = readdir(dir)) != nullptr) {
if (ent->d_name[0] == '.') continue; if (ent->d_name[0] == '.') continue;
if (best_name[0] == '\0' || strcmp(ent->d_name, best_name) < 0)
safe_copy(best_name, sizeof(best_name), ent->d_name);
}
closedir(dir);
if (best_name[0] == '\0') return ClaimResult::None;
char queued[MAX_PATH_LEN]; char queued[MAX_PATH_LEN];
char active[MAX_PATH_LEN]; char active[MAX_PATH_LEN];
path_join(queued, sizeof(queued), SPOOL_QUEUE_DIR, ent->d_name); path_join(queued, sizeof(queued), SPOOL_QUEUE_DIR, best_name);
path_join(active, sizeof(active), SPOOL_ACTIVE_DIR, ent->d_name); path_join(active, sizeof(active), SPOOL_ACTIVE_DIR, best_name);
if (rename(queued, active) == 0) { 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); safe_copy(out_active_path, out_active_path_len, active);
claimed = true; return ClaimResult::Claimed;
break;
}
}
closedir(dir);
return claimed;
} }
static void finalize_job(const char* active_path, const char* target_dir, JobMeta* job) { static void finalize_job(const char* active_path, const char* target_dir, JobMeta* job) {
save_job_to_path_atomic(active_path, job); save_job_to_path_atomic(active_path, job);
char target[MAX_PATH_LEN]; char target[MAX_PATH_LEN];
make_job_file_path(target_dir, job->id, target, sizeof(target)); 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); 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]; char detail[MAX_DEBUG_LEN];
snprintf(detail, sizeof(detail), "submitting test page as %s (%d bytes)", snprintf(detail, sizeof(detail), "submitting test page as %s (%d bytes, %d %s)",
attempts[i].mime, len); attempts[i].mime, len,
job->copies > 0 ? job->copies : 1,
(job->copies > 1) ? "copies" : "copy");
persist_job_debug(active_path, job, detail); persist_job_debug(active_path, job, detail);
bool ok = ipp_print_buffer(job->printer_uri, job->job_name, job->user_name, 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); free(data);
if (ok) return true; 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; if (!read_file_bytes(job->doc_path, &data, &len, err, err_len)) return false;
char detail[MAX_DEBUG_LEN]; char detail[MAX_DEBUG_LEN];
snprintf(detail, sizeof(detail), "submitting %s document (%d bytes)", snprintf(detail, sizeof(detail), "submitting %s document (%d bytes, %d %s)",
job->doc_format[0] ? job->doc_format : "application/octet-stream", len); 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); persist_job_debug(active_path, job, detail);
bool ok = ipp_print_buffer(job->printer_uri, job->job_name, job->user_name, 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); free(data);
return ok; return ok;
} }
@@ -259,11 +336,13 @@ static void process_job(const char* active_path) {
JobMeta job = {}; JobMeta job = {};
if (!load_job_from_path(active_path, &job)) { if (!load_job_from_path(active_path, &job)) {
log_msg("dropping unreadable job file: %s", active_path); log_msg("dropping unreadable job file: %s", active_path);
write_daemon_state("unreadable-job", active_path, nullptr);
remove(active_path); remove(active_path);
return; return;
} }
log_msg("processing job %s (%s)", job.id, job.job_name); 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_state(&job, "printing", "Sending to printer");
set_job_debug(&job, "dispatching queued job"); set_job_debug(&job, "dispatching queued job");
save_job_to_path_atomic(active_path, &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); set_job_debug_from_result(&job, &result, nullptr);
finalize_job(active_path, SPOOL_DONE_DIR, &job); finalize_job(active_path, SPOOL_DONE_DIR, &job);
log_msg("job %s completed (printer job id %d)", job.id, result.job_id); log_msg("job %s completed (printer job id %d)", job.id, result.job_id);
write_daemon_state("completed", job.id, job.status_message);
} else { } else {
set_job_state(&job, "failed", err[0] ? err : "Print failed"); set_job_state(&job, "failed", err[0] ? err : "Print failed");
set_job_debug_from_result(&job, &result, err[0] ? err : nullptr); set_job_debug_from_result(&job, &result, err[0] ? err : nullptr);
finalize_job(active_path, SPOOL_FAILED_DIR, &job); finalize_job(active_path, SPOOL_FAILED_DIR, &job);
log_msg("job %s failed: %s", job.id, job.status_message); 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); 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); 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] = {}; char last_probed_uri[MAX_PATH_LEN] = {};
uint64_t last_probe_ms = 0; uint64_t last_probe_ms = 0;
bool last_probe_ok = false; 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"); log_msg("ready");
write_daemon_state("ready", nullptr, nullptr);
for (;;) { for (;;) {
auto_probe_configured_printer(last_probed_uri, sizeof(last_probed_uri),
&last_probe_ms, &last_probe_ok);
char active_path[MAX_PATH_LEN]; 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); process_job(active_path);
continue; 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); montauk::sleep_ms(1000);
} }
} }
+5 -3
View File
@@ -41,6 +41,7 @@ CXXFLAGS := \
-mcmodel=small \ -mcmodel=small \
-I $(PROG_INC) \ -I $(PROG_INC) \
-isystem $(PROG_INC)/libc \ -isystem $(PROG_INC)/libc \
-I ../../lib/bearssl/inc \
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \ -isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include -isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
@@ -53,8 +54,9 @@ LDFLAGS := \
-z max-page-size=0x1000 \ -z max-page-size=0x1000 \
-T $(LINK_LD) -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)) 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 TARGET := $(BINDIR)/apps/wordprocessor/wordprocessor.elf
@@ -62,9 +64,9 @@ TARGET := $(BINDIR)/apps/wordprocessor/wordprocessor.elf
all: $(TARGET) all: $(TARGET)
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(TARGET): $(OBJS) $(LINK_LD) Makefile $(LIBS)
mkdir -p $(BINDIR)/apps/wordprocessor mkdir -p $(BINDIR)/apps/wordprocessor
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@ $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
$(OBJDIR)/%.o: %.cpp Makefile $(OBJDIR)/%.o: %.cpp Makefile
mkdir -p $(OBJDIR) mkdir -p $(OBJDIR)
+5
View File
@@ -419,6 +419,10 @@ void wp_init_empty_document(WordProcessorState* wp) {
wp->modified = false; wp->modified = false;
wp->filepath[0] = '\0'; wp->filepath[0] = '\0';
wp->filename[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->show_pathbar = false;
wp->pathbar_save_mode = false; wp->pathbar_save_mode = false;
@@ -429,6 +433,7 @@ void wp_init_empty_document(WordProcessorState* wp) {
wp->font_dropdown_open = false; wp->font_dropdown_open = false;
wp->size_dropdown_open = false; wp->size_dropdown_open = false;
wp->line_spacing_dropdown_open = false; wp->line_spacing_dropdown_open = false;
wp->special_char_flyout_open = false;
wp->undo_count = 0; wp->undo_count = 0;
wp->undo_pos = 0; wp->undo_pos = 0;
+49 -2
View File
@@ -7,6 +7,12 @@
#include "wordprocessor.hpp" #include "wordprocessor.hpp"
#include <gui/dialogs.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) { static void wp_open_pathbar_for_open(WordProcessorState* wp) {
char path[256] = {}; char path[256] = {};
char msg[160] = {}; 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) { static void wp_commit_pathbar(WordProcessorState* wp) {
if (!wp->pathbar_text[0]) return; if (!wp->pathbar_text[0]) return;
if (wp->pathbar_save_mode) { if (wp->pathbar_save_mode) {
@@ -28,10 +40,23 @@ static void wp_commit_pathbar(WordProcessorState* wp) {
wp->show_pathbar = false; 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) { static void wp_close_dropdowns(WordProcessorState* wp) {
wp->font_dropdown_open = false; wp->font_dropdown_open = false;
wp->size_dropdown_open = false; wp->size_dropdown_open = false;
wp->line_spacing_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) { 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; 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 (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) { 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); wp_open_pathbar_for_open(wp);
@@ -157,6 +195,10 @@ void wp_handle_mouse(const Montauk::WinEvent& ev) {
wp_save_file(wp); wp_save_file(wp);
return; 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) { 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_close_dropdowns(wp);
wp_undo(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->line_spacing_dropdown_open = !wp->line_spacing_dropdown_open;
wp->font_dropdown_open = false; wp->font_dropdown_open = false;
wp->size_dropdown_open = false; wp->size_dropdown_open = false;
wp->special_char_flyout_open = false;
return; return;
} }
if (local_x >= WP_BTN_SECTION_X && local_x < WP_BTN_SECTION_X + 24 && 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) {
bool open = !wp->special_char_flyout_open;
wp_close_dropdowns(wp); wp_close_dropdowns(wp);
wp_insert_char(wp, (char)0xA7); wp->special_char_flyout_open = open;
wp_history_checkpoint(wp);
return; return;
} }
return; return;
@@ -349,6 +392,10 @@ void wp_handle_key(const Montauk::KeyEvent& key) {
wp_open_pathbar_for_open(wp); wp_open_pathbar_for_open(wp);
return; return;
} }
if (key.ctrl && (key.ascii == 'p' || key.ascii == 'P')) {
wp_open_print_dialog(wp);
return;
}
if (key.ctrl && !key.alt && if (key.ctrl && !key.alt &&
(key.ascii == 'z' || key.ascii == 'Z')) { (key.ascii == 'z' || key.ascii == 'Z')) {
+4
View File
@@ -13,6 +13,7 @@ WordProcessorState g_wp = {};
WPFontTable g_wp_fonts = { {{nullptr}}, false }; WPFontTable g_wp_fonts = { {{nullptr}}, false };
SvgIcon g_icon_folder = {}; SvgIcon g_icon_folder = {};
SvgIcon g_icon_save = {}; SvgIcon g_icon_save = {};
SvgIcon g_icon_print = {};
SvgIcon g_icon_undo = {}; SvgIcon g_icon_undo = {};
SvgIcon g_icon_redo = {}; SvgIcon g_icon_redo = {};
SvgIcon g_icon_align_left = {}; 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); g_icon_folder = svg_load("0:/icons/folder.svg", 16, 16, def_color);
if (!g_icon_save.pixels) if (!g_icon_save.pixels)
g_icon_save = svg_load("0:/icons/document-save-symbolic.svg", 16, 16, def_color); 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) if (!g_icon_undo.pixels)
g_icon_undo = svg_load("0:/icons/edit-undo-symbolic.svg", 16, 16, def_color); g_icon_undo = svg_load("0:/icons/edit-undo-symbolic.svg", 16, 16, def_color);
if (!g_icon_redo.pixels) if (!g_icon_redo.pixels)
@@ -55,6 +58,7 @@ void wp_cleanup_state() {
wp_free_document(&g_wp); wp_free_document(&g_wp);
if (g_icon_folder.pixels) svg_free(g_icon_folder); if (g_icon_folder.pixels) svg_free(g_icon_folder);
if (g_icon_save.pixels) svg_free(g_icon_save); 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_undo.pixels) svg_free(g_icon_undo);
if (g_icon_redo.pixels) svg_free(g_icon_redo); 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_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); 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) { static ParagraphStyle* wp_current_paragraph_style(WordProcessorState* wp) {
if (wp->paragraph_count <= 0) { if (wp->paragraph_count <= 0) {
static ParagraphStyle fallback = { PARA_ALIGN_LEFT, PARA_LIST_NONE, 100, 0, 0, 0, 0, 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) if (g_icon_save.pixels)
c.icon(WP_BTN_SAVE_X + 4, 10, g_icon_save); 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_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); 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.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' }; char section[2] = { (char)0xA7, '\0' };
TrueTypeFont* font = wp_get_font(FONT_ROBOTO, 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)); 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; 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"; const char* name = wp->filename[0] ? wp->filename : "Untitled";
snprintf(status_left, sizeof(status_left), " %s%s | %s %dpt | %s | %s | %d%%", snprintf(status_left, sizeof(status_left), " %s%s | %s %dpt | %s | %s | %d%%",
name, wp->modified ? " *" : "", name, wp->modified ? " *" : "",
WP_FONT_NAMES[wp->cur_font_id < FONT_COUNT ? wp->cur_font_id : 0], (int)wp->cur_size, 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), wp_align_label(cur_para->align), wp_list_label(cur_para->list_type),
(int)cur_para->line_spacing); (int)cur_para->line_spacing);
wp_draw_ui_text(c, 4, status_text_y, status_left, colors::PANEL_TEXT);
char status_right[32]; char status_right[32];
snprintf(status_right, sizeof(status_right), "%d chars ", wp->total_text_len); 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); 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); wp_draw_ui_text(c, c.w - sr_w - 4, status_text_y, status_right, colors::PANEL_TEXT);
if (wp->font_dropdown_open) { 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); 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; using namespace gui;
static constexpr int INIT_W = 640; static constexpr int INIT_W = 900;
static constexpr int INIT_H = 480; static constexpr int INIT_H = 600;
static constexpr int WP_TOOLBAR_H = 36; static constexpr int WP_TOOLBAR_H = 36;
static constexpr int WP_PATHBAR_H = 32; static constexpr int WP_PATHBAR_H = 32;
static constexpr int WP_STATUS_H = 24; 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_LIST_MARKER_W = 20;
static constexpr int WP_BTN_OPEN_X = 4; static constexpr int WP_BTN_OPEN_X = 4;
static constexpr int WP_BTN_SAVE_X = 32; static constexpr int WP_BTN_SAVE_X = 32;
static constexpr int WP_BTN_UNDO_X = 66; static constexpr int WP_BTN_PRINT_X = 60;
static constexpr int WP_BTN_REDO_X = 94; static constexpr int WP_BTN_UNDO_X = 94;
static constexpr int WP_BTN_BOLD_X = 128; static constexpr int WP_BTN_REDO_X = 122;
static constexpr int WP_BTN_ITALIC_X = 156; static constexpr int WP_BTN_BOLD_X = 156;
static constexpr int WP_FONT_DD_X = 188; static constexpr int WP_BTN_ITALIC_X = 184;
static constexpr int WP_FONT_DD_W = 84; static constexpr int WP_FONT_DD_X = 216;
static constexpr int WP_SIZE_DD_X = 278; static constexpr int WP_FONT_DD_W = 74;
static constexpr int WP_SIZE_DD_W = 40; static constexpr int WP_SIZE_DD_X = 296;
static constexpr int WP_BTN_ALIGN_L_X = 330; static constexpr int WP_SIZE_DD_W = 36;
static constexpr int WP_BTN_ALIGN_C_X = 358; static constexpr int WP_BTN_ALIGN_L_X = 344;
static constexpr int WP_BTN_ALIGN_R_X = 386; static constexpr int WP_BTN_ALIGN_C_X = 372;
static constexpr int WP_BTN_BULLET_X = 418; static constexpr int WP_BTN_ALIGN_R_X = 400;
static constexpr int WP_BTN_NUMBER_X = 446; static constexpr int WP_BTN_BULLET_X = 432;
static constexpr int WP_BTN_OUTDENT_X = 482; static constexpr int WP_BTN_NUMBER_X = 460;
static constexpr int WP_BTN_INDENT_X = 510; static constexpr int WP_BTN_OUTDENT_X = 496;
static constexpr int WP_LINE_DD_X = 544; static constexpr int WP_BTN_INDENT_X = 524;
static constexpr int WP_LINE_DD_W = 56; static constexpr int WP_LINE_DD_X = 558;
static constexpr int WP_BTN_SECTION_X = 608; 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_ROBOTO = 0;
static constexpr int FONT_NOTOSERIF = 1; 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 }; inline constexpr int WP_LINE_SPACING_OPTIONS[] = { 100, 125, 150, 200 };
static constexpr int WP_LINE_SPACING_OPTION_COUNT = 4; 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 { enum ParagraphAlign : uint8_t {
PARA_ALIGN_LEFT = 0, PARA_ALIGN_LEFT = 0,
PARA_ALIGN_CENTER = 1, PARA_ALIGN_CENTER = 1,
@@ -266,6 +284,10 @@ struct WordProcessorState {
bool modified; bool modified;
char filepath[256]; char filepath[256];
char filename[64]; 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 show_pathbar;
bool pathbar_save_mode; bool pathbar_save_mode;
@@ -276,6 +298,7 @@ struct WordProcessorState {
bool font_dropdown_open; bool font_dropdown_open;
bool size_dropdown_open; bool size_dropdown_open;
bool line_spacing_dropdown_open; bool line_spacing_dropdown_open;
bool special_char_flyout_open;
UndoSnapshot undo[WP_UNDO_MAX]; UndoSnapshot undo[WP_UNDO_MAX];
int undo_count; int undo_count;
@@ -289,6 +312,7 @@ extern WordProcessorState g_wp;
extern WPFontTable g_wp_fonts; extern WPFontTable g_wp_fonts;
extern SvgIcon g_icon_folder; extern SvgIcon g_icon_folder;
extern SvgIcon g_icon_save; extern SvgIcon g_icon_save;
extern SvgIcon g_icon_print;
extern SvgIcon g_icon_undo; extern SvgIcon g_icon_undo;
extern SvgIcon g_icon_redo; extern SvgIcon g_icon_redo;
extern SvgIcon g_icon_align_left; extern SvgIcon g_icon_align_left;
@@ -357,6 +381,8 @@ void wp_history_mark_saved(WordProcessorState* wp);
bool wp_undo(WordProcessorState* wp); bool wp_undo(WordProcessorState* wp);
bool wp_redo(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_render();
void wp_handle_mouse(const Montauk::WinEvent& ev); void wp_handle_mouse(const Montauk::WinEvent& ev);
void wp_handle_key(const Montauk::KeyEvent& key); void wp_handle_key(const Montauk::KeyEvent& key);