feat: Word Processor discovers fonts automatically (via 0:/fonts), UI fix
This commit is contained in:
@@ -12,4 +12,4 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#define MONTAUK_BUILD_NUMBER 52
|
#define MONTAUK_BUILD_NUMBER 54
|
||||||
|
|||||||
@@ -443,8 +443,193 @@ static int wp_split_at(WordProcessorState* wp, int abs_pos) {
|
|||||||
return r + 1;
|
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() {
|
void wp_load_fonts() {
|
||||||
if (g_wp_fonts.loaded) return;
|
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* {
|
auto load = [](const char* path) -> TrueTypeFont* {
|
||||||
TrueTypeFont* f = (TrueTypeFont*)montauk::malloc(sizeof(TrueTypeFont));
|
TrueTypeFont* f = (TrueTypeFont*)montauk::malloc(sizeof(TrueTypeFont));
|
||||||
@@ -456,45 +641,98 @@ void wp_load_fonts() {
|
|||||||
return f;
|
return f;
|
||||||
};
|
};
|
||||||
|
|
||||||
g_wp_fonts.fonts[FONT_ROBOTO][0] = fonts::system_font ? fonts::system_font : load("0:/fonts/Roboto-Medium.ttf");
|
for (int i = 0; i < g_wp_fonts.count; i++) {
|
||||||
g_wp_fonts.fonts[FONT_ROBOTO][1] = fonts::system_bold ? fonts::system_bold : load("0:/fonts/Roboto-Bold.ttf");
|
WpFontFamily* fam = &g_wp_fonts.families[i];
|
||||||
g_wp_fonts.fonts[FONT_ROBOTO][2] = load("0:/fonts/Roboto-Italic.ttf");
|
bool is_roboto = wp_ci_equal(fam->name, "Roboto");
|
||||||
g_wp_fonts.fonts[FONT_ROBOTO][3] = load("0:/fonts/Roboto-BoldItalic.ttf");
|
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");
|
// Defensive fallback: if 0:/fonts yielded nothing usable, expose the
|
||||||
g_wp_fonts.fonts[FONT_NOTOSERIF][1] = load("0:/fonts/NotoSerif-SemiBold.ttf");
|
// shared system font so the editor still renders.
|
||||||
g_wp_fonts.fonts[FONT_NOTOSERIF][2] = load("0:/fonts/NotoSerif-Italic.ttf");
|
if (g_wp_fonts.count == 0 && fonts::system_font) {
|
||||||
g_wp_fonts.fonts[FONT_NOTOSERIF][3] = load("0:/fonts/NotoSerif-BoldItalic.ttf");
|
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");
|
int def = wp_default_font_id();
|
||||||
g_wp_fonts.fonts[FONT_C059][1] = load("0:/fonts/C059-Bold.ttf");
|
g_ui_font = fonts::system_font;
|
||||||
g_wp_fonts.fonts[FONT_C059][2] = load("0:/fonts/C059-Italic.ttf");
|
if (!g_ui_font && def < g_wp_fonts.count) g_ui_font = g_wp_fonts.families[def].fonts[0];
|
||||||
g_wp_fonts.fonts[FONT_C059][3] = load("0:/fonts/C059-Bold.ttf");
|
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;
|
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) {
|
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;
|
int variant = 0;
|
||||||
if ((flags & STYLE_BOLD) && (flags & STYLE_ITALIC)) variant = 3;
|
if ((flags & STYLE_BOLD) && (flags & STYLE_ITALIC)) variant = 3;
|
||||||
else if (flags & STYLE_BOLD) variant = 1;
|
else if (flags & STYLE_BOLD) variant = 1;
|
||||||
else if (flags & STYLE_ITALIC) variant = 2;
|
else if (flags & STYLE_ITALIC) variant = 2;
|
||||||
|
|
||||||
TrueTypeFont* f = g_wp_fonts.fonts[font_id][variant];
|
int v = wp_resolve_variant(font_id, variant);
|
||||||
if (f && f->valid) return f;
|
if (v >= 0) return g_wp_fonts.families[font_id].fonts[v];
|
||||||
|
|
||||||
f = g_wp_fonts.fonts[font_id][0];
|
|
||||||
if (f && f->valid) return f;
|
|
||||||
|
|
||||||
return g_ui_font ? g_ui_font : fonts::system_font;
|
return g_ui_font ? g_ui_font : fonts::system_font;
|
||||||
}
|
}
|
||||||
|
|
||||||
void wp_init_empty_document(WordProcessorState* wp) {
|
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->run_count = 1;
|
||||||
wp->total_text_len = 0;
|
wp->total_text_len = 0;
|
||||||
|
|
||||||
@@ -506,7 +744,7 @@ void wp_init_empty_document(WordProcessorState* wp) {
|
|||||||
wp->has_selection = false;
|
wp->has_selection = false;
|
||||||
wp->mouse_selecting = 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_size = WP_DEFAULT_SIZE;
|
||||||
wp->cur_flags = 0;
|
wp->cur_flags = 0;
|
||||||
|
|
||||||
@@ -1477,6 +1715,10 @@ void wp_open_save_pathbar(WordProcessorState* wp) {
|
|||||||
|
|
||||||
static int wp_serialized_size(WordProcessorState* wp) {
|
static int wp_serialized_size(WordProcessorState* wp) {
|
||||||
int size = 4 + 2 + 1 + 1 + 1 + 2 + 2;
|
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++)
|
for (int i = 0; i < wp->run_count; i++)
|
||||||
size += 2 + 1 + 1 + 1 + 1 + wp->runs[i].len;
|
size += 2 + 1 + 1 + 1 + 1 + wp->runs[i].len;
|
||||||
size += wp->paragraph_count * 12;
|
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++] = 'W';
|
||||||
buf[off++] = 'P';
|
buf[off++] = 'P';
|
||||||
buf[off++] = '1';
|
buf[off++] = '1';
|
||||||
buf[off++] = 2;
|
buf[off++] = 3; // v3: font ids reference the embedded font-name table below
|
||||||
buf[off++] = 0;
|
buf[off++] = 0;
|
||||||
buf[off++] = wp->cur_font_id;
|
buf[off++] = wp->cur_font_id;
|
||||||
buf[off++] = wp->cur_size;
|
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->run_count);
|
||||||
wp_write_u16(buf, &off, wp->paragraph_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++) {
|
for (int i = 0; i < wp->run_count; i++) {
|
||||||
StyledRun* r = &wp->runs[i];
|
StyledRun* r = &wp->runs[i];
|
||||||
wp_write_u16(buf, &off, r->len);
|
wp_write_u16(buf, &off, r->len);
|
||||||
@@ -1557,7 +1812,7 @@ static bool wp_deserialize_document(WordProcessorState* wp, const uint8_t* buf,
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
uint8_t major = buf[4];
|
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 (major == 1) {
|
||||||
if (size < 10) return false;
|
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;
|
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 off = header_off;
|
||||||
int actual_para_count = 1;
|
int actual_para_count = 1;
|
||||||
for (int i = 0; i < run_count; i++) {
|
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;
|
off = header_off;
|
||||||
for (int i = 0; i < run_count; i++) {
|
for (int i = 0; i < run_count; i++) {
|
||||||
int text_len = wp_read_u16(buf, &off);
|
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 run_size = buf[off++];
|
||||||
uint8_t flags = buf[off++];
|
uint8_t flags = buf[off++];
|
||||||
off++;
|
off++;
|
||||||
@@ -1626,6 +1914,8 @@ static bool wp_deserialize_document(WordProcessorState* wp, const uint8_t* buf,
|
|||||||
off += text_len;
|
off += text_len;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cur_font = map_font(cur_font);
|
||||||
|
|
||||||
if (wp->run_count == 0) {
|
if (wp->run_count == 0) {
|
||||||
wp_init_run(&wp->runs[0], cur_font, cur_size, cur_flags);
|
wp_init_run(&wp->runs[0], cur_font, cur_size, cur_flags);
|
||||||
wp->run_count = 1;
|
wp->run_count = 1;
|
||||||
|
|||||||
@@ -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)) {
|
if (wp->font_dropdown_open && wp_left_pressed(ev.mouse.buttons, ev.mouse.prev_buttons)) {
|
||||||
int dx = WP_FONT_DD_X;
|
int dx = WP_FONT_DD_X;
|
||||||
int dy = WP_TOOLBAR_H;
|
int dy = WP_TOOLBAR_H;
|
||||||
int dh = FONT_COUNT * 26 + 4;
|
int dh = wp_font_count() * 26 + 4;
|
||||||
if (local_x >= dx && local_x < dx + 110 && local_y >= dy && local_y < dy + dh) {
|
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;
|
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);
|
if (wp->has_selection) wp_apply_style_to_selection(wp, 0, idx);
|
||||||
wp->cur_font_id = (uint8_t)idx;
|
wp->cur_font_id = (uint8_t)idx;
|
||||||
if (wp->has_selection) wp_history_checkpoint(wp);
|
if (wp->has_selection) wp_history_checkpoint(wp);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ int g_win_w = INIT_W;
|
|||||||
int g_win_h = INIT_H;
|
int g_win_h = INIT_H;
|
||||||
WsWindow g_win;
|
WsWindow g_win;
|
||||||
WordProcessorState g_wp = {};
|
WordProcessorState g_wp = {};
|
||||||
WPFontTable g_wp_fonts = { {{nullptr}}, false };
|
WPFontTable g_wp_fonts = {};
|
||||||
SvgIcon g_icon_folder = {};
|
SvgIcon g_icon_folder = {};
|
||||||
SvgIcon g_icon_save = {};
|
SvgIcon g_icon_save = {};
|
||||||
SvgIcon g_icon_print = {};
|
SvgIcon g_icon_print = {};
|
||||||
|
|||||||
@@ -671,61 +671,28 @@ static int wp_pdf_style_index(uint8_t flags) {
|
|||||||
return idx;
|
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* wp_pdf_font_path(uint8_t font_id, uint8_t style_index) {
|
||||||
static const char* PATHS[FONT_COUNT][4] = {
|
if (font_id >= g_wp_fonts.count || style_index > 3) return nullptr;
|
||||||
{
|
int v = wp_resolve_variant(font_id, style_index);
|
||||||
"0:/fonts/Roboto-Medium.ttf",
|
if (v < 0) return nullptr;
|
||||||
"0:/fonts/Roboto-Bold.ttf",
|
const char* p = g_wp_fonts.families[font_id].paths[v];
|
||||||
"0:/fonts/Roboto-Italic.ttf",
|
return p[0] ? p : nullptr;
|
||||||
"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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static const char* wp_pdf_base_font_name(uint8_t font_id, uint8_t style_index) {
|
static const char* wp_pdf_base_font_name(uint8_t font_id, uint8_t style_index) {
|
||||||
static const char* NAMES[FONT_COUNT][4] = {
|
if (font_id >= g_wp_fonts.count || style_index > 3) return nullptr;
|
||||||
{
|
int v = wp_resolve_variant(font_id, style_index);
|
||||||
"Roboto-Medium",
|
if (v < 0) return nullptr;
|
||||||
"Roboto-Bold",
|
const char* n = g_wp_fonts.families[font_id].basenames[v];
|
||||||
"Roboto-Italic",
|
return n[0] ? n : nullptr;
|
||||||
"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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static int wp_pdf_descriptor_flags(uint8_t font_id, uint8_t style_index) {
|
static int wp_pdf_descriptor_flags(uint8_t font_id, uint8_t style_index) {
|
||||||
int flags = 32; // Nonsymbolic
|
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
|
flags |= 2; // Serif
|
||||||
if (style_index == 2 || style_index == 3)
|
if (style_index == 2 || style_index == 3)
|
||||||
flags |= 64; // Italic
|
flags |= 64; // Italic
|
||||||
@@ -885,17 +852,18 @@ static bool wp_pdf_collect_fonts(WordProcessorState* wp,
|
|||||||
if (out_count) *out_count = 0;
|
if (out_count) *out_count = 0;
|
||||||
if (!wp || !defs) return false;
|
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++) {
|
for (int i = 0; i < wp->run_count; i++) {
|
||||||
StyledRun* run = &wp->runs[i];
|
StyledRun* run = &wp->runs[i];
|
||||||
if (run->len <= 0) continue;
|
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);
|
int style_index = wp_pdf_style_index(run->flags);
|
||||||
used[font_id][style_index] = true;
|
used[font_id][style_index] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
int count = 0;
|
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++) {
|
for (int style_index = 0; style_index < 4; style_index++) {
|
||||||
if (!used[font_id][style_index]) continue;
|
if (!used[font_id][style_index]) continue;
|
||||||
if (!wp_pdf_prepare_font_def(&defs[count],
|
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 font_id,
|
||||||
uint8_t flags) {
|
uint8_t flags) {
|
||||||
int want_style = wp_pdf_style_index(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++) {
|
for (int i = 0; i < count; i++) {
|
||||||
if (defs[i].font_id == want_font && defs[i].style_index == want_style)
|
if (defs[i].font_id == want_font && defs[i].style_index == want_style)
|
||||||
return &defs[i];
|
return &defs[i];
|
||||||
@@ -1235,7 +1203,10 @@ bool wp_export_pdf_document(WordProcessorState* wp, const char* path, char* out_
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool success = false;
|
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;
|
int font_count = 0;
|
||||||
WpPdfPageStream* page_streams = nullptr;
|
WpPdfPageStream* page_streams = nullptr;
|
||||||
int* page_obj_nums = 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 next_obj = 0;
|
||||||
int total_objects = 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))) {
|
if (!wp_pdf_collect_fonts(wp, fonts, &font_count, err, sizeof(err))) {
|
||||||
wp_status_copy(out_status, out_status_len, err);
|
wp_status_copy(out_status, out_status_len, err);
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
@@ -1473,6 +1451,7 @@ cleanup:
|
|||||||
if (line_pages) montauk::mfree(line_pages);
|
if (line_pages) montauk::mfree(line_pages);
|
||||||
if (line_y) montauk::mfree(line_y);
|
if (line_y) montauk::mfree(line_y);
|
||||||
wp_pdf_free_fonts(fonts, font_count);
|
wp_pdf_free_fonts(fonts, font_count);
|
||||||
|
if (fonts) montauk::mfree(fonts);
|
||||||
wp_pdf_buffer_free(&pdf);
|
wp_pdf_buffer_free(&pdf);
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
|
|||||||
@@ -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) {
|
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, PARA_DIVIDER_NONE, 0, 0, 0, 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;
|
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);
|
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);
|
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);
|
c.fill_rounded_rect(WP_FONT_DD_X, 6, WP_FONT_DD_W, 24, 3, btn_bg);
|
||||||
wp_draw_ui_text(c, WP_FONT_DD_X + 6, (WP_TOOLBAR_H - sfh) / 2,
|
{
|
||||||
WP_FONT_NAMES[wp->cur_font_id < FONT_COUNT ? wp->cur_font_id : 0],
|
char font_fit[WP_FONT_NAME_MAX + 4];
|
||||||
colors::TEXT_COLOR);
|
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,
|
||||||
|
font_fit, colors::TEXT_COLOR);
|
||||||
|
}
|
||||||
|
|
||||||
c.fill_rounded_rect(WP_SIZE_DD_X, 6, WP_SIZE_DD_W, 24, 3, btn_bg);
|
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);
|
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(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);
|
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) {
|
if (cur_para->divider_type != PARA_DIVIDER_NONE) {
|
||||||
snprintf(status_left, sizeof(status_left), " %s%s | %s %dpt | Divider: %s | %d%%",
|
snprintf(status_left, sizeof(status_left), " %s%s | %s %dpt | Divider: %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_name(wp->cur_font_id), (int)wp->cur_size,
|
||||||
wp_divider_label(cur_para->divider_type), (int)cur_para->line_spacing);
|
wp_divider_label(cur_para->divider_type), (int)cur_para->line_spacing);
|
||||||
} else {
|
} else {
|
||||||
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_name(wp->cur_font_id), (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);
|
||||||
}
|
}
|
||||||
@@ -599,15 +641,17 @@ void wp_render() {
|
|||||||
if (wp->font_dropdown_open) {
|
if (wp->font_dropdown_open) {
|
||||||
int dx = WP_FONT_DD_X;
|
int dx = WP_FONT_DD_X;
|
||||||
int dy = WP_TOOLBAR_H;
|
int dy = WP_TOOLBAR_H;
|
||||||
int dw = 110;
|
int dw = wp_font_dropdown_width();
|
||||||
int dh = FONT_COUNT * 26 + 4;
|
int dh = wp_font_count() * 26 + 4;
|
||||||
c.fill_rect(dx, dy, dw, dh, colors::MENU_BG);
|
c.fill_rect(dx, dy, dw, dh, colors::MENU_BG);
|
||||||
c.rect(dx, dy, dw, dh, colors::BORDER);
|
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;
|
int iy = dy + 2 + i * 26;
|
||||||
if (i == wp->cur_font_id)
|
if (i == wp->cur_font_id)
|
||||||
c.fill_rect(dx + 2, iy, dw - 4, 24, colors::MENU_HOVER);
|
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 dy = WP_TOOLBAR_H;
|
||||||
int dw = WP_SPECIAL_CHAR_FLYOUT_W;
|
int dw = WP_SPECIAL_CHAR_FLYOUT_W;
|
||||||
int dh = WP_SPECIAL_CHAR_OPTION_COUNT * WP_SPECIAL_CHAR_ROW_H + 4;
|
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.fill_rect(dx, dy, dw, dh, colors::MENU_BG);
|
||||||
c.rect(dx, dy, dw, dh, colors::BORDER);
|
c.rect(dx, dy, dw, dh, colors::BORDER);
|
||||||
for (int i = 0; i < WP_SPECIAL_CHAR_OPTION_COUNT; i++) {
|
for (int i = 0; i < WP_SPECIAL_CHAR_OPTION_COUNT; i++) {
|
||||||
|
|||||||
@@ -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_ROW_H = 24;
|
||||||
static constexpr int WP_SPECIAL_CHAR_FLYOUT_W = 144;
|
static constexpr int WP_SPECIAL_CHAR_FLYOUT_W = 144;
|
||||||
|
|
||||||
static constexpr int FONT_ROBOTO = 0;
|
// Fonts are discovered at runtime by scanning 0:/fonts (see wp_load_fonts).
|
||||||
static constexpr int FONT_NOTOSERIF = 1;
|
// A font id is an index into the discovered family table (g_wp_fonts). The
|
||||||
static constexpr int FONT_C059 = 2;
|
// table is bounded by WP_MAX_FONTS; ids beyond the table fall back to default.
|
||||||
static constexpr int FONT_COUNT = 3;
|
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_BOLD = 0x01;
|
||||||
static constexpr uint8_t STYLE_ITALIC = 0x02;
|
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 };
|
inline constexpr int WP_SIZE_OPTIONS[] = { 12, 14, 16, 18, 20, 24, 28, 36 };
|
||||||
static constexpr int WP_SIZE_OPTION_COUNT = 8;
|
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 };
|
||||||
@@ -178,9 +175,22 @@ inline constexpr WpDividerOption WP_DIVIDER_OPTIONS[] = {
|
|||||||
};
|
};
|
||||||
static constexpr int WP_DIVIDER_OPTION_COUNT = 7;
|
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 {
|
struct WPFontTable {
|
||||||
TrueTypeFont* fonts[FONT_COUNT][4];
|
WpFontFamily families[WP_MAX_FONTS];
|
||||||
bool loaded;
|
int count;
|
||||||
|
bool loaded;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct StyledRun {
|
struct StyledRun {
|
||||||
@@ -422,6 +432,12 @@ void wp_free_document(WordProcessorState* wp);
|
|||||||
void wp_cleanup_state();
|
void wp_cleanup_state();
|
||||||
|
|
||||||
TrueTypeFont* wp_get_font(int font_id, uint8_t flags);
|
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);
|
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);
|
void wp_pos_to_run(WordProcessorState* wp, int abs_pos, int* out_run, int* out_offset);
|
||||||
|
|||||||
Reference in New Issue
Block a user