feat: Word Processor discovers fonts automatically (via 0:/fonts), UI fix

This commit is contained in:
2026-06-06 10:12:11 +02:00
parent df204cc15a
commit c27516b8fd
7 changed files with 435 additions and 106 deletions
+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once
#define MONTAUK_BUILD_NUMBER 52
#define MONTAUK_BUILD_NUMBER 54
+315 -25
View File
@@ -443,8 +443,193 @@ static int wp_split_at(WordProcessorState* wp, int abs_pos) {
return r + 1;
}
// ============================================================================
// Font discovery
//
// Fonts live in 0:/fonts as "<Family>-<Style>.ttf" (e.g. Roboto-BoldItalic.ttf).
// We scan that directory at startup, group files by family, and slot each file
// into one of four style variants. Families are sorted deterministically so a
// given set of installed fonts yields stable font ids across runs. Documents
// embed family names (see wp_serialize_document) so ids survive font changes.
// ============================================================================
static char wp_ascii_lower(char c) {
return (c >= 'A' && c <= 'Z') ? (char)(c + ('a' - 'A')) : c;
}
static bool wp_ci_equal(const char* a, const char* b) {
while (*a && *b) {
if (wp_ascii_lower(*a) != wp_ascii_lower(*b)) return false;
a++; b++;
}
return *a == *b;
}
static int wp_ci_strcmp(const char* a, const char* b) {
int i = 0;
while (a[i] && b[i]) {
char ca = wp_ascii_lower(a[i]);
char cb = wp_ascii_lower(b[i]);
if (ca != cb) return (int)(unsigned char)ca - (int)(unsigned char)cb;
i++;
}
return (int)(unsigned char)wp_ascii_lower(a[i]) - (int)(unsigned char)wp_ascii_lower(b[i]);
}
static bool wp_ci_contains(const char* hay, const char* needle) {
for (int i = 0; hay[i]; i++) {
int j = 0;
while (needle[j] && hay[i + j] &&
wp_ascii_lower(hay[i + j]) == wp_ascii_lower(needle[j]))
j++;
if (!needle[j]) return true;
}
return false;
}
static bool wp_str_ends_with_ci(const char* s, const char* suffix) {
int sl = montauk::slen(s);
int xl = montauk::slen(suffix);
if (xl > sl) return false;
for (int i = 0; i < xl; i++)
if (wp_ascii_lower(s[sl - xl + i]) != wp_ascii_lower(suffix[i])) return false;
return true;
}
// Classify the style token (text after the last '-' in a filename stem).
// Returns true and sets *variant (0=reg, 1=bold, 2=italic, 3=bold-italic) when
// the token names a weight/slope; returns false when it looks like part of the
// family name (e.g. "Mono", "Condensed"), so the caller keeps it in the name.
static bool wp_classify_style_token(const char* tok, int* variant) {
bool bold = wp_ci_contains(tok, "bold") || wp_ci_contains(tok, "black") ||
wp_ci_contains(tok, "heavy");
bool italic = wp_ci_contains(tok, "italic") || wp_ci_contains(tok, "oblique");
bool weight = wp_ci_equal(tok, "regular") || wp_ci_equal(tok, "normal") ||
wp_ci_equal(tok, "roman") || wp_ci_equal(tok, "book") ||
wp_ci_equal(tok, "light") || wp_ci_equal(tok, "thin") ||
wp_ci_equal(tok, "medium") || wp_ci_equal(tok, "retina");
if (!bold && !italic && !weight) return false;
*variant = (bold ? 1 : 0) | (italic ? 2 : 0);
return true;
}
static bool wp_family_is_serif(const char* name) {
return wp_ci_contains(name, "serif") || wp_ci_equal(name, "c059");
}
// Find family `name` in the table, creating it if absent. Returns -1 if full.
static int wp_intern_family(const char* name) {
for (int i = 0; i < g_wp_fonts.count; i++)
if (wp_ci_equal(g_wp_fonts.families[i].name, name))
return i;
if (g_wp_fonts.count >= WP_MAX_FONTS) return -1;
int idx = g_wp_fonts.count++;
WpFontFamily* fam = &g_wp_fonts.families[idx];
montauk::memset(fam, 0, sizeof(*fam));
montauk::strncpy(fam->name, name, WP_FONT_NAME_MAX);
fam->serif = wp_family_is_serif(fam->name);
return idx;
}
// Slot a single "<Family>-<Style>.ttf" filename into the family table.
static void wp_register_font_file(const char* filename) {
if (!wp_str_ends_with_ci(filename, ".ttf")) return;
char stem[WP_FONT_BASENAME_MAX];
int flen = montauk::slen(filename) - 4; // strip ".ttf"
if (flen <= 0 || flen >= WP_FONT_BASENAME_MAX) return;
montauk::memcpy(stem, filename, flen);
stem[flen] = '\0';
char family[WP_FONT_NAME_MAX];
int variant = 0;
int dash = -1;
for (int i = 0; stem[i]; i++) if (stem[i] == '-') dash = i;
if (dash > 0 && wp_classify_style_token(stem + dash + 1, &variant)) {
int nlen = dash < WP_FONT_NAME_MAX ? dash : WP_FONT_NAME_MAX - 1;
montauk::memcpy(family, stem, nlen);
family[nlen] = '\0';
} else {
variant = 0;
montauk::strncpy(family, stem, WP_FONT_NAME_MAX);
}
int fi = wp_intern_family(family);
if (fi < 0) return;
WpFontFamily* fam = &g_wp_fonts.families[fi];
if (fam->paths[variant][0]) return; // first file wins for this slot
montauk::strncpy(fam->paths[variant], "0:/fonts/", WP_FONT_PATH_MAX);
int plen = montauk::slen(fam->paths[variant]);
montauk::strncpy(fam->paths[variant] + plen, filename, WP_FONT_PATH_MAX - plen);
montauk::strncpy(fam->basenames[variant], stem, WP_FONT_BASENAME_MAX);
}
static int wp_family_priority(const char* name) {
if (wp_ci_equal(name, "Roboto")) return 0;
if (wp_ci_equal(name, "NotoSerif")) return 1;
if (wp_ci_equal(name, "C059")) return 2;
return 100;
}
static bool wp_family_less(const WpFontFamily* a, const WpFontFamily* b) {
int pa = wp_family_priority(a->name);
int pb = wp_family_priority(b->name);
if (pa != pb) return pa < pb;
return wp_ci_strcmp(a->name, b->name) < 0;
}
// Canonical built-ins first (Roboto, NotoSerif, C059), then the rest
// alphabetically. Deterministic so ids are stable for a fixed font set.
static void wp_sort_font_families() {
for (int i = 0; i < g_wp_fonts.count; i++) {
int best = i;
for (int j = i + 1; j < g_wp_fonts.count; j++)
if (wp_family_less(&g_wp_fonts.families[j], &g_wp_fonts.families[best]))
best = j;
if (best != i) {
WpFontFamily tmp = g_wp_fonts.families[i];
g_wp_fonts.families[i] = g_wp_fonts.families[best];
g_wp_fonts.families[best] = tmp;
}
}
}
void wp_load_fonts() {
if (g_wp_fonts.loaded) return;
g_wp_fonts.count = 0;
const char* names[256];
int count = montauk::readdir("0:/fonts", names, 256);
// Sort entries so per-slot conflicts resolve deterministically. With
// first-wins below, "Roboto-Medium" sorts before "Roboto-Regular" and
// takes the regular slot, matching the shared system font (loaded from
// Roboto-Medium.ttf) used on screen and embedded in exported PDFs.
for (int i = 1; i < count; i++) {
const char* key = names[i];
int j = i - 1;
while (j >= 0 && wp_ci_strcmp(names[j], key) > 0) {
names[j + 1] = names[j];
j--;
}
names[j + 1] = key;
}
for (int i = 0; i < count; i++) {
const char* raw = names[i];
if (!raw) continue;
int rl = montauk::slen(raw);
if (rl == 0 || raw[rl - 1] == '/') continue; // skip directories
// readdir may return a full internal path (ramdisk) or a bare name;
// take everything after the last '/'.
const char* base = raw;
for (int k = 0; raw[k]; k++) if (raw[k] == '/') base = raw + k + 1;
wp_register_font_file(base);
}
wp_sort_font_families();
auto load = [](const char* path) -> TrueTypeFont* {
TrueTypeFont* f = (TrueTypeFont*)montauk::malloc(sizeof(TrueTypeFont));
@@ -456,45 +641,98 @@ void wp_load_fonts() {
return f;
};
g_wp_fonts.fonts[FONT_ROBOTO][0] = fonts::system_font ? fonts::system_font : load("0:/fonts/Roboto-Medium.ttf");
g_wp_fonts.fonts[FONT_ROBOTO][1] = fonts::system_bold ? fonts::system_bold : load("0:/fonts/Roboto-Bold.ttf");
g_wp_fonts.fonts[FONT_ROBOTO][2] = load("0:/fonts/Roboto-Italic.ttf");
g_wp_fonts.fonts[FONT_ROBOTO][3] = load("0:/fonts/Roboto-BoldItalic.ttf");
for (int i = 0; i < g_wp_fonts.count; i++) {
WpFontFamily* fam = &g_wp_fonts.families[i];
bool is_roboto = wp_ci_equal(fam->name, "Roboto");
for (int v = 0; v < 4; v++) {
if (!fam->paths[v][0]) { fam->fonts[v] = nullptr; continue; }
// Reuse the already-loaded shared system faces for Roboto reg/bold.
if (is_roboto && v == 0 && fonts::system_font) { fam->fonts[v] = fonts::system_font; continue; }
if (is_roboto && v == 1 && fonts::system_bold) { fam->fonts[v] = fonts::system_bold; continue; }
fam->fonts[v] = load(fam->paths[v]);
}
}
g_wp_fonts.fonts[FONT_NOTOSERIF][0] = load("0:/fonts/NotoSerif-Regular.ttf");
g_wp_fonts.fonts[FONT_NOTOSERIF][1] = load("0:/fonts/NotoSerif-SemiBold.ttf");
g_wp_fonts.fonts[FONT_NOTOSERIF][2] = load("0:/fonts/NotoSerif-Italic.ttf");
g_wp_fonts.fonts[FONT_NOTOSERIF][3] = load("0:/fonts/NotoSerif-BoldItalic.ttf");
// Defensive fallback: if 0:/fonts yielded nothing usable, expose the
// shared system font so the editor still renders.
if (g_wp_fonts.count == 0 && fonts::system_font) {
WpFontFamily* fam = &g_wp_fonts.families[0];
montauk::memset(fam, 0, sizeof(*fam));
montauk::strncpy(fam->name, "Roboto", WP_FONT_NAME_MAX);
fam->fonts[0] = fonts::system_font;
fam->fonts[1] = fonts::system_bold;
g_wp_fonts.count = 1;
}
g_wp_fonts.fonts[FONT_C059][0] = load("0:/fonts/C059-Roman.ttf");
g_wp_fonts.fonts[FONT_C059][1] = load("0:/fonts/C059-Bold.ttf");
g_wp_fonts.fonts[FONT_C059][2] = load("0:/fonts/C059-Italic.ttf");
g_wp_fonts.fonts[FONT_C059][3] = load("0:/fonts/C059-Bold.ttf");
int def = wp_default_font_id();
g_ui_font = fonts::system_font;
if (!g_ui_font && def < g_wp_fonts.count) g_ui_font = g_wp_fonts.families[def].fonts[0];
g_ui_bold = fonts::system_bold;
if (!g_ui_bold && def < g_wp_fonts.count) g_ui_bold = g_wp_fonts.families[def].fonts[1];
if (!g_ui_bold) g_ui_bold = g_ui_font;
g_ui_font = g_wp_fonts.fonts[FONT_ROBOTO][0];
g_ui_bold = g_wp_fonts.fonts[FONT_ROBOTO][1] ? g_wp_fonts.fonts[FONT_ROBOTO][1] : g_ui_font;
g_wp_fonts.loaded = true;
}
int wp_font_count() { return g_wp_fonts.count; }
const char* wp_font_name(int font_id) {
if (font_id < 0 || font_id >= g_wp_fonts.count) return "";
return g_wp_fonts.families[font_id].name;
}
int wp_find_font_by_name(const char* name) {
if (!name || !name[0]) return -1;
for (int i = 0; i < g_wp_fonts.count; i++)
if (wp_ci_equal(g_wp_fonts.families[i].name, name)) return i;
return -1;
}
int wp_default_font_id() {
int r = wp_find_font_by_name("Roboto");
return r >= 0 ? r : 0;
}
// Resolve a requested style variant to the closest face a family actually has,
// preferring the nearest style: bold-italic -> bold -> italic -> regular, and
// bold/italic -> regular. Returns -1 if the family has no usable face.
int wp_resolve_variant(int font_id, int variant) {
if (font_id < 0 || font_id >= g_wp_fonts.count) return -1;
if (variant < 0 || variant > 3) variant = 0;
static const int kFallback[4][4] = {
{ 0, -1, -1, -1 }, // regular
{ 1, 0, -1, -1 }, // bold -> regular
{ 2, 0, -1, -1 }, // italic -> regular
{ 3, 1, 2, 0 }, // bold-italic -> bold -> italic -> regular
};
WpFontFamily* fam = &g_wp_fonts.families[font_id];
for (int i = 0; i < 4; i++) {
int v = kFallback[variant][i];
if (v < 0) break;
if (fam->fonts[v] && fam->fonts[v]->valid) return v;
}
return -1;
}
TrueTypeFont* wp_get_font(int font_id, uint8_t flags) {
if (font_id < 0 || font_id >= FONT_COUNT) font_id = 0;
if (font_id < 0 || font_id >= g_wp_fonts.count) font_id = wp_default_font_id();
if (font_id < 0 || font_id >= g_wp_fonts.count)
return g_ui_font ? g_ui_font : fonts::system_font;
int variant = 0;
if ((flags & STYLE_BOLD) && (flags & STYLE_ITALIC)) variant = 3;
else if (flags & STYLE_BOLD) variant = 1;
else if (flags & STYLE_ITALIC) variant = 2;
TrueTypeFont* f = g_wp_fonts.fonts[font_id][variant];
if (f && f->valid) return f;
f = g_wp_fonts.fonts[font_id][0];
if (f && f->valid) return f;
int v = wp_resolve_variant(font_id, variant);
if (v >= 0) return g_wp_fonts.families[font_id].fonts[v];
return g_ui_font ? g_ui_font : fonts::system_font;
}
void wp_init_empty_document(WordProcessorState* wp) {
wp_init_run(&wp->runs[0], FONT_ROBOTO, WP_DEFAULT_SIZE, 0);
uint8_t default_font = (uint8_t)wp_default_font_id();
wp_init_run(&wp->runs[0], default_font, WP_DEFAULT_SIZE, 0);
wp->run_count = 1;
wp->total_text_len = 0;
@@ -506,7 +744,7 @@ void wp_init_empty_document(WordProcessorState* wp) {
wp->has_selection = false;
wp->mouse_selecting = false;
wp->cur_font_id = FONT_ROBOTO;
wp->cur_font_id = default_font;
wp->cur_size = WP_DEFAULT_SIZE;
wp->cur_flags = 0;
@@ -1477,6 +1715,10 @@ void wp_open_save_pathbar(WordProcessorState* wp) {
static int wp_serialized_size(WordProcessorState* wp) {
int size = 4 + 2 + 1 + 1 + 1 + 2 + 2;
// Embedded font-name table: u8 count, then per family u8 len + name bytes.
size += 1;
for (int i = 0; i < g_wp_fonts.count; i++)
size += 1 + montauk::slen(g_wp_fonts.families[i].name);
for (int i = 0; i < wp->run_count; i++)
size += 2 + 1 + 1 + 1 + 1 + wp->runs[i].len;
size += wp->paragraph_count * 12;
@@ -1504,7 +1746,7 @@ static bool wp_serialize_document(WordProcessorState* wp, uint8_t** out_buf, int
buf[off++] = 'W';
buf[off++] = 'P';
buf[off++] = '1';
buf[off++] = 2;
buf[off++] = 3; // v3: font ids reference the embedded font-name table below
buf[off++] = 0;
buf[off++] = wp->cur_font_id;
buf[off++] = wp->cur_size;
@@ -1512,6 +1754,19 @@ static bool wp_serialize_document(WordProcessorState* wp, uint8_t** out_buf, int
wp_write_u16(buf, &off, wp->run_count);
wp_write_u16(buf, &off, wp->paragraph_count);
// Font-name table: maps each runtime font id (used by cur_font_id and run
// font_id below) to a family name, so ids stay correct if the installed
// font set changes between save and load.
buf[off++] = (uint8_t)g_wp_fonts.count;
for (int i = 0; i < g_wp_fonts.count; i++) {
const char* name = g_wp_fonts.families[i].name;
int nl = montauk::slen(name);
if (nl > WP_FONT_NAME_MAX - 1) nl = WP_FONT_NAME_MAX - 1;
buf[off++] = (uint8_t)nl;
montauk::memcpy(buf + off, name, nl);
off += nl;
}
for (int i = 0; i < wp->run_count; i++) {
StyledRun* r = &wp->runs[i];
wp_write_u16(buf, &off, r->len);
@@ -1557,7 +1812,7 @@ static bool wp_deserialize_document(WordProcessorState* wp, const uint8_t* buf,
return false;
uint8_t major = buf[4];
if (major != 1 && major != 2) return false;
if (major != 1 && major != 2 && major != 3) return false;
if (major == 1) {
if (size < 10) return false;
@@ -1583,6 +1838,39 @@ static bool wp_deserialize_document(WordProcessorState* wp, const uint8_t* buf,
if (run_count < 0 || run_count > WP_MAX_RUNS) return false;
// Resolve stored font ids to current runtime ids via the embedded font-name
// table (v3); legacy documents used fixed ids 0=Roboto, 1=NotoSerif, 2=C059.
char emb_names[WP_MAX_FONTS][WP_FONT_NAME_MAX];
int emb_count = 0;
if (major >= 3) {
if (header_off + 1 > size) return false;
emb_count = buf[header_off++];
if (emb_count > WP_MAX_FONTS) return false;
for (int i = 0; i < emb_count; i++) {
if (header_off + 1 > size) return false;
int nl = buf[header_off++];
if (nl < 0 || nl >= WP_FONT_NAME_MAX || header_off + nl > size) return false;
montauk::memcpy(emb_names[i], buf + header_off, nl);
emb_names[i][nl] = '\0';
header_off += nl;
}
} else {
static const char* kLegacyNames[3] = { "Roboto", "NotoSerif", "C059" };
emb_count = 3;
for (int i = 0; i < 3; i++)
montauk::strncpy(emb_names[i], kLegacyNames[i], WP_FONT_NAME_MAX);
}
uint8_t font_remap[WP_MAX_FONTS];
uint8_t fallback_font = (uint8_t)wp_default_font_id();
for (int i = 0; i < emb_count; i++) {
int r = wp_find_font_by_name(emb_names[i]);
font_remap[i] = (uint8_t)(r >= 0 ? r : fallback_font);
}
auto map_font = [&](uint8_t stored) -> uint8_t {
return stored < emb_count ? font_remap[stored] : fallback_font;
};
int off = header_off;
int actual_para_count = 1;
for (int i = 0; i < run_count; i++) {
@@ -1611,7 +1899,7 @@ static bool wp_deserialize_document(WordProcessorState* wp, const uint8_t* buf,
off = header_off;
for (int i = 0; i < run_count; i++) {
int text_len = wp_read_u16(buf, &off);
uint8_t font_id = buf[off++];
uint8_t font_id = map_font(buf[off++]);
uint8_t run_size = buf[off++];
uint8_t flags = buf[off++];
off++;
@@ -1626,6 +1914,8 @@ static bool wp_deserialize_document(WordProcessorState* wp, const uint8_t* buf,
off += text_len;
}
cur_font = map_font(cur_font);
if (wp->run_count == 0) {
wp_init_run(&wp->runs[0], cur_font, cur_size, cur_flags);
wp->run_count = 1;
+3 -3
View File
@@ -333,10 +333,10 @@ void wp_handle_mouse(const Montauk::WinEvent& ev) {
if (wp->font_dropdown_open && wp_left_pressed(ev.mouse.buttons, ev.mouse.prev_buttons)) {
int dx = WP_FONT_DD_X;
int dy = WP_TOOLBAR_H;
int dh = FONT_COUNT * 26 + 4;
if (local_x >= dx && local_x < dx + 110 && local_y >= dy && local_y < dy + dh) {
int dh = wp_font_count() * 26 + 4;
if (local_x >= dx && local_x < dx + wp_font_dropdown_width() && local_y >= dy && local_y < dy + dh) {
int idx = (local_y - dy - 2) / 26;
if (idx >= 0 && idx < FONT_COUNT) {
if (idx >= 0 && idx < wp_font_count()) {
if (wp->has_selection) wp_apply_style_to_selection(wp, 0, idx);
wp->cur_font_id = (uint8_t)idx;
if (wp->has_selection) wp_history_checkpoint(wp);
+1 -1
View File
@@ -10,7 +10,7 @@ int g_win_w = INIT_W;
int g_win_h = INIT_H;
WsWindow g_win;
WordProcessorState g_wp = {};
WPFontTable g_wp_fonts = { {{nullptr}}, false };
WPFontTable g_wp_fonts = {};
SvgIcon g_icon_folder = {};
SvgIcon g_icon_save = {};
SvgIcon g_icon_print = {};
+31 -52
View File
@@ -671,61 +671,28 @@ static int wp_pdf_style_index(uint8_t flags) {
return idx;
}
// Path and base-font lookups fall back to the regular variant when the
// requested style is missing, mirroring wp_get_font so a document with, say,
// bold-italic text in a regular-only family still embeds (in the regular face).
static const char* wp_pdf_font_path(uint8_t font_id, uint8_t style_index) {
static const char* PATHS[FONT_COUNT][4] = {
{
"0:/fonts/Roboto-Medium.ttf",
"0:/fonts/Roboto-Bold.ttf",
"0:/fonts/Roboto-Italic.ttf",
"0:/fonts/Roboto-BoldItalic.ttf",
},
{
"0:/fonts/NotoSerif-Regular.ttf",
"0:/fonts/NotoSerif-SemiBold.ttf",
"0:/fonts/NotoSerif-Italic.ttf",
"0:/fonts/NotoSerif-BoldItalic.ttf",
},
{
"0:/fonts/C059-Roman.ttf",
"0:/fonts/C059-Bold.ttf",
"0:/fonts/C059-Italic.ttf",
"0:/fonts/C059-Bold.ttf",
},
};
if (font_id >= FONT_COUNT || style_index > 3) return nullptr;
return PATHS[font_id][style_index];
if (font_id >= g_wp_fonts.count || style_index > 3) return nullptr;
int v = wp_resolve_variant(font_id, style_index);
if (v < 0) return nullptr;
const char* p = g_wp_fonts.families[font_id].paths[v];
return p[0] ? p : nullptr;
}
static const char* wp_pdf_base_font_name(uint8_t font_id, uint8_t style_index) {
static const char* NAMES[FONT_COUNT][4] = {
{
"Roboto-Medium",
"Roboto-Bold",
"Roboto-Italic",
"Roboto-BoldItalic",
},
{
"NotoSerif-Regular",
"NotoSerif-SemiBold",
"NotoSerif-Italic",
"NotoSerif-BoldItalic",
},
{
"C059-Roman",
"C059-Bold",
"C059-Italic",
"C059-Bold",
},
};
if (font_id >= FONT_COUNT || style_index > 3) return nullptr;
return NAMES[font_id][style_index];
if (font_id >= g_wp_fonts.count || style_index > 3) return nullptr;
int v = wp_resolve_variant(font_id, style_index);
if (v < 0) return nullptr;
const char* n = g_wp_fonts.families[font_id].basenames[v];
return n[0] ? n : nullptr;
}
static int wp_pdf_descriptor_flags(uint8_t font_id, uint8_t style_index) {
int flags = 32; // Nonsymbolic
if (font_id == FONT_NOTOSERIF || font_id == FONT_C059)
if (font_id < g_wp_fonts.count && g_wp_fonts.families[font_id].serif)
flags |= 2; // Serif
if (style_index == 2 || style_index == 3)
flags |= 64; // Italic
@@ -885,17 +852,18 @@ static bool wp_pdf_collect_fonts(WordProcessorState* wp,
if (out_count) *out_count = 0;
if (!wp || !defs) return false;
bool used[FONT_COUNT][4] = {};
bool used[WP_MAX_FONTS][4] = {};
for (int i = 0; i < wp->run_count; i++) {
StyledRun* run = &wp->runs[i];
if (run->len <= 0) continue;
int font_id = run->font_id < FONT_COUNT ? run->font_id : FONT_ROBOTO;
int font_id = run->font_id < wp_font_count() ? run->font_id : wp_default_font_id();
if (font_id < 0 || font_id >= WP_MAX_FONTS) font_id = 0;
int style_index = wp_pdf_style_index(run->flags);
used[font_id][style_index] = true;
}
int count = 0;
for (int font_id = 0; font_id < FONT_COUNT; font_id++) {
for (int font_id = 0; font_id < wp_font_count(); font_id++) {
for (int style_index = 0; style_index < 4; style_index++) {
if (!used[font_id][style_index]) continue;
if (!wp_pdf_prepare_font_def(&defs[count],
@@ -919,7 +887,7 @@ static const WpPdfFontDef* wp_pdf_find_font_def(const WpPdfFontDef* defs,
uint8_t font_id,
uint8_t flags) {
int want_style = wp_pdf_style_index(flags);
int want_font = font_id < FONT_COUNT ? font_id : FONT_ROBOTO;
int want_font = font_id < wp_font_count() ? font_id : wp_default_font_id();
for (int i = 0; i < count; i++) {
if (defs[i].font_id == want_font && defs[i].style_index == want_style)
return &defs[i];
@@ -1235,7 +1203,10 @@ bool wp_export_pdf_document(WordProcessorState* wp, const char* path, char* out_
}
bool success = false;
WpPdfFontDef fonts[FONT_COUNT * 4] = {};
// Sized for every discovered family in all four styles; heap-allocated to
// keep the deep PDF call chain off an already-tight user stack.
WpPdfFontDef* fonts = nullptr;
int font_cap = wp_font_count() > 0 ? wp_font_count() * 4 : 4;
int font_count = 0;
WpPdfPageStream* page_streams = nullptr;
int* page_obj_nums = nullptr;
@@ -1245,6 +1216,13 @@ bool wp_export_pdf_document(WordProcessorState* wp, const char* path, char* out_
int next_obj = 0;
int total_objects = 0;
fonts = (WpPdfFontDef*)montauk::malloc((uint64_t)font_cap * sizeof(WpPdfFontDef));
if (!fonts) {
wp_status_copy(out_status, out_status_len, "Out of memory while exporting PDF");
goto cleanup;
}
montauk::memset(fonts, 0, (uint64_t)font_cap * sizeof(WpPdfFontDef));
if (!wp_pdf_collect_fonts(wp, fonts, &font_count, err, sizeof(err))) {
wp_status_copy(out_status, out_status_len, err);
goto cleanup;
@@ -1473,6 +1451,7 @@ cleanup:
if (line_pages) montauk::mfree(line_pages);
if (line_y) montauk::mfree(line_y);
wp_pdf_free_fonts(fonts, font_count);
if (fonts) montauk::mfree(fonts);
wp_pdf_buffer_free(&pdf);
return success;
+55 -11
View File
@@ -128,6 +128,44 @@ static void wp_fit_ui_text(const char* src, char* out, int out_len, int max_w) {
}
}
// Truncate from the end with a trailing ".." so `src` fits in max_w pixels.
// Used for labels (e.g. font names) where the start carries the meaning.
static void wp_fit_ui_text_end(const char* src, char* out, int out_len, int max_w) {
if (out_len <= 0) return;
if (!src) { 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;
for (int keep = montauk::slen(src); keep > 0; keep--) {
char candidate[256];
if (keep > (int)sizeof(candidate) - 3) continue;
montauk::memcpy(candidate, src, keep);
candidate[keep] = '.';
candidate[keep + 1] = '.';
candidate[keep + 2] = '\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;
}
}
out[0] = '\0';
}
// Width of the font popup, sized to the longest discovered name (clamped) so
// names display in full in the list even when the toolbar button is narrow.
int wp_font_dropdown_width() {
int w = 110;
for (int i = 0; i < wp_font_count(); i++) {
int tw = text_width(g_ui_font, wp_font_name(i), fonts::UI_SIZE) + 24;
if (tw > w) w = tw;
}
if (w > 260) w = 260;
return w;
}
static ParagraphStyle* wp_current_paragraph_style(WordProcessorState* wp) {
if (wp->paragraph_count <= 0) {
static ParagraphStyle fallback = { PARA_ALIGN_LEFT, PARA_LIST_NONE, 100, PARA_DIVIDER_NONE, 0, 0, 0, 0 };
@@ -247,14 +285,18 @@ void wp_render() {
Color italic_bg = (wp->cur_flags & STYLE_ITALIC) ? btn_active : btn_bg;
c.fill_rounded_rect(WP_BTN_ITALIC_X, 6, 24, 24, 3, italic_bg);
{
TrueTypeFont* italic_font = wp_get_font(FONT_ROBOTO, STYLE_ITALIC);
TrueTypeFont* italic_font = wp_get_font(wp_default_font_id(), STYLE_ITALIC);
draw_text(c, italic_font ? italic_font : g_ui_font, WP_BTN_ITALIC_X + 9, 8, "I", colors::TEXT_COLOR, fonts::UI_SIZE);
}
c.fill_rounded_rect(WP_FONT_DD_X, 6, WP_FONT_DD_W, 24, 3, btn_bg);
{
char font_fit[WP_FONT_NAME_MAX + 4];
wp_fit_ui_text_end(wp_font_name(wp->cur_font_id), font_fit, sizeof(font_fit),
WP_FONT_DD_W - 12);
wp_draw_ui_text(c, WP_FONT_DD_X + 6, (WP_TOOLBAR_H - sfh) / 2,
WP_FONT_NAMES[wp->cur_font_id < FONT_COUNT ? wp->cur_font_id : 0],
colors::TEXT_COLOR);
font_fit, colors::TEXT_COLOR);
}
c.fill_rounded_rect(WP_SIZE_DD_X, 6, WP_SIZE_DD_W, 24, 3, btn_bg);
{
@@ -305,7 +347,7 @@ void wp_render() {
c.fill_rounded_rect(WP_BTN_SECTION_X, 6, 24, 24, 3, special_bg);
{
char section[2] = { (char)0xA7, '\0' };
TrueTypeFont* font = wp_get_font(FONT_ROBOTO, 0);
TrueTypeFont* font = wp_get_font(wp_default_font_id(), 0);
draw_text(c, font ? font : g_ui_font, WP_BTN_SECTION_X + 6, 8, section, colors::TEXT_COLOR, fonts::UI_SIZE);
}
@@ -573,12 +615,12 @@ void wp_render() {
if (cur_para->divider_type != PARA_DIVIDER_NONE) {
snprintf(status_left, sizeof(status_left), " %s%s | %s %dpt | Divider: %s | %d%%",
name, wp->modified ? " *" : "",
WP_FONT_NAMES[wp->cur_font_id < FONT_COUNT ? wp->cur_font_id : 0], (int)wp->cur_size,
wp_font_name(wp->cur_font_id), (int)wp->cur_size,
wp_divider_label(cur_para->divider_type), (int)cur_para->line_spacing);
} else {
snprintf(status_left, sizeof(status_left), " %s%s | %s %dpt | %s | %s | %d%%",
name, wp->modified ? " *" : "",
WP_FONT_NAMES[wp->cur_font_id < FONT_COUNT ? wp->cur_font_id : 0], (int)wp->cur_size,
wp_font_name(wp->cur_font_id), (int)wp->cur_size,
wp_align_label(cur_para->align), wp_list_label(cur_para->list_type),
(int)cur_para->line_spacing);
}
@@ -599,15 +641,17 @@ void wp_render() {
if (wp->font_dropdown_open) {
int dx = WP_FONT_DD_X;
int dy = WP_TOOLBAR_H;
int dw = 110;
int dh = FONT_COUNT * 26 + 4;
int dw = wp_font_dropdown_width();
int dh = wp_font_count() * 26 + 4;
c.fill_rect(dx, dy, dw, dh, colors::MENU_BG);
c.rect(dx, dy, dw, dh, colors::BORDER);
for (int i = 0; i < FONT_COUNT; i++) {
for (int i = 0; i < wp_font_count(); i++) {
int iy = dy + 2 + i * 26;
if (i == wp->cur_font_id)
c.fill_rect(dx + 2, iy, dw - 4, 24, colors::MENU_HOVER);
wp_draw_ui_text(c, dx + 8, iy + (24 - sfh) / 2, WP_FONT_NAMES[i], colors::TEXT_COLOR);
char item_fit[WP_FONT_NAME_MAX + 4];
wp_fit_ui_text_end(wp_font_name(i), item_fit, sizeof(item_fit), dw - 16);
wp_draw_ui_text(c, dx + 8, iy + (24 - sfh) / 2, item_fit, colors::TEXT_COLOR);
}
}
@@ -668,7 +712,7 @@ void wp_render() {
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);
TrueTypeFont* glyph_font = wp_get_font(wp_default_font_id(), 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++) {
+27 -11
View File
@@ -82,20 +82,17 @@ static constexpr int WP_DIVIDER_FLYOUT_W = 216;
static constexpr int WP_SPECIAL_CHAR_ROW_H = 24;
static constexpr int WP_SPECIAL_CHAR_FLYOUT_W = 144;
static constexpr int FONT_ROBOTO = 0;
static constexpr int FONT_NOTOSERIF = 1;
static constexpr int FONT_C059 = 2;
static constexpr int FONT_COUNT = 3;
// Fonts are discovered at runtime by scanning 0:/fonts (see wp_load_fonts).
// A font id is an index into the discovered family table (g_wp_fonts). The
// table is bounded by WP_MAX_FONTS; ids beyond the table fall back to default.
static constexpr int WP_MAX_FONTS = 24; // max discovered families
static constexpr int WP_FONT_NAME_MAX = 32; // family/display name
static constexpr int WP_FONT_PATH_MAX = 96; // "0:/fonts/<file>.ttf"
static constexpr int WP_FONT_BASENAME_MAX = 48; // filename stem (PDF base font)
static constexpr uint8_t STYLE_BOLD = 0x01;
static constexpr uint8_t STYLE_ITALIC = 0x02;
inline constexpr const char* WP_FONT_NAMES[FONT_COUNT] = {
"Roboto",
"NotoSerif",
"C059",
};
inline constexpr int WP_SIZE_OPTIONS[] = { 12, 14, 16, 18, 20, 24, 28, 36 };
static constexpr int WP_SIZE_OPTION_COUNT = 8;
inline constexpr int WP_LINE_SPACING_OPTIONS[] = { 100, 125, 150, 200 };
@@ -178,8 +175,21 @@ inline constexpr WpDividerOption WP_DIVIDER_OPTIONS[] = {
};
static constexpr int WP_DIVIDER_OPTION_COUNT = 7;
// One font family with up to four style variants, indexed:
// [0] regular [1] bold [2] italic [3] bold-italic
// A missing variant has an empty path/basename and a null font; callers fall
// back to the regular face via wp_get_font().
struct WpFontFamily {
char name[WP_FONT_NAME_MAX]; // e.g. "Roboto"
char paths[4][WP_FONT_PATH_MAX]; // vfs path per variant ("" if absent)
char basenames[4][WP_FONT_BASENAME_MAX]; // filename stem per variant (PDF base font)
TrueTypeFont* fonts[4];
bool serif; // PDF font descriptor hint
};
struct WPFontTable {
TrueTypeFont* fonts[FONT_COUNT][4];
WpFontFamily families[WP_MAX_FONTS];
int count;
bool loaded;
};
@@ -422,6 +432,12 @@ void wp_free_document(WordProcessorState* wp);
void wp_cleanup_state();
TrueTypeFont* wp_get_font(int font_id, uint8_t flags);
int wp_font_count(); // number of discovered families
const char* wp_font_name(int font_id); // family display name ("" if out of range)
int wp_default_font_id(); // index of "Roboto", else 0
int wp_find_font_by_name(const char* name); // -1 if not found
int wp_resolve_variant(int font_id, int variant); // closest available face, -1 if none
int wp_font_dropdown_width(); // popup width fitted to the longest font name
int wp_abs_pos(WordProcessorState* wp, int run, int offset);
void wp_pos_to_run(WordProcessorState* wp, int abs_pos, int* out_run, int* out_offset);