summaryrefslogtreecommitdiff
path: root/fonts.ha
diff options
context:
space:
mode:
authorLassi Pulkkinen <lassi@pulk.fi>2024-10-31 03:11:21 +0200
committerLassi Pulkkinen <lassi@pulk.fi>2024-10-31 03:51:35 +0200
commitae44478b30d890fe0fb04022f44d474dcdcc3f9d (patch)
tree5f462459ae4b47d22114eed717d1382d08cf4dfe /fonts.ha
Initial commit (import old repo)HEADmain
Diffstat (limited to 'fonts.ha')
-rw-r--r--fonts.ha491
1 files changed, 491 insertions, 0 deletions
diff --git a/fonts.ha b/fonts.ha
new file mode 100644
index 0000000..faa05f8
--- /dev/null
+++ b/fonts.ha
@@ -0,0 +1,491 @@
+use dejson;
+use encoding::json;
+use gl::*;
+use glm;
+use math;
+use sort;
+use strings;
+use trace;
+
+type Font = struct {
+ Object,
+ glyphs: []Glyph,
+};
+
+type Glyph = struct {
+ char: rune,
+ scale: f32,
+ ascent: f32,
+ advance: f32,
+ width: u8,
+ height: u8,
+ x: u16,
+ y: u16,
+ gl_texture: uint,
+ tex_width: u32,
+ tex_height: u32,
+};
+
+let FONTS = OBJREG_EMPTY;
+
+fn fonts_find(ident: str) nullable *Font =
+ objreg_find(&FONTS, ident): nullable *Font;
+
+type FontsIter = struct {
+ inner: ObjectRegistryIterator,
+};
+
+fn fonts_iter() FontsIter =
+ FontsIter { inner = objreg_iter(&FONTS) };
+
+fn fonts_next(it: *FontsIter) nullable *Font =
+ objreg_next(&it.inner): nullable *Font;
+
+fn load_fonts(assets: *Pack) void = {
+ const tr = &trace::root;
+ trace::info(tr, "loading fonts...");
+
+ const results = resource_search(assets, "font", ".json");
+ for (let i = 0z; i < len(results); i += 1) {
+ const (ident, ext) = results[i];
+ const tr = trace::ctx(tr, "load font {}", ident);
+
+ const font = alloc(Font {
+ name = strings::dup(ident),
+ glyphs = [],
+ });
+ objreg_register(&FONTS, font);
+
+ const json = resource_load_json(
+ assets, "font", ident, ".json", &tr);
+ const json = match (json) {
+ case let json: json::value =>
+ yield json;
+ case trace::failed =>
+ continue;
+ };
+ defer json::finish(json);
+
+ const deser = dejson::newdeser(&json);
+ const decl = deser_font_decl(&deser);
+ const decl = match (decl) {
+ case let decl: FontDecl =>
+ yield decl;
+ case let err: dejson::error =>
+ defer free(err);
+ trace::error(&tr, "deser: {}", err): void;
+ continue;
+ };
+
+ font_load(font, &decl, &tr);
+ };
+};
+
+fn font_load(font: *Font, decl: *FontDecl, tr: *trace::tracer) void = {
+ for (let i = 0z; i < len(decl.providers); i += 1) {
+ match (decl.providers[i]) {
+ case let provider: FontDeclProviderBitmap =>
+ font_load_bitmap(font, &provider, tr): void;
+ case FontDeclProviderLegacyUnicode =>
+ void;
+ case let provider: FontDeclProviderSpace =>
+ font_load_space(font, &provider);
+ };
+ };
+
+ sort::sort(font.glyphs, size(Glyph), &glyph_cmpfunc);
+
+ let dedup: []Glyph = [];
+ for (let i = 0z; i < len(font.glyphs); i += 1) {
+ if (i == len(font.glyphs) - 1 || font.glyphs[i].char
+ != font.glyphs[i + 1].char) {
+ append(dedup, font.glyphs[i]);
+ };
+ };
+ free(font.glyphs);
+ font.glyphs = dedup;
+};
+
+fn font_load_bitmap(
+ font: *Font,
+ provider: *FontDeclProviderBitmap,
+ tr: *trace::tracer,
+) (void | trace::failed) = {
+ const tr = trace::ctx(tr, "bitmap {}", provider.file);
+
+ // TODO: arghhhhh
+ const ident = strings::trimsuffix(provider.file, ".png");
+ const tex = match (textures_find(ident)) {
+ case let tex: *Texture =>
+ yield tex;
+ case null =>
+ return trace::error(&tr, "Unknown texture");
+ };
+ const gl_texture = texture_upload(tex, &tr);
+ if (tex.width % provider.ncolumns != 0
+ || tex.height & provider.nrows != 0) {
+ return trace::error(&tr,
+ "Texture dimensions {},{} not divisible by glyph grid dimensions {},{}",
+ tex.width, tex.height,
+ provider.ncolumns, provider.nrows);
+ };
+
+ const glyph_width = tex.width / provider.ncolumns;
+ if (glyph_width: u8 != glyph_width) {
+ return trace::error(&tr, "Glyph width {} exceeds limit of 255",
+ glyph_width);
+ };
+ const glyph_width = glyph_width: u8;
+ const glyph_height = tex.height / provider.nrows;
+ if (glyph_height: u8 != glyph_height) {
+ return trace::error(&tr, "Glyph height {} exceeds limit of 255",
+ glyph_height);
+ };
+ const glyph_height = glyph_height: u8;
+ const scale = provider.height: f32 / glyph_height: f32;
+ const tex_po2width = 1 << math::bit_size_u32(tex.width - 1): u32;
+ const tex_po2height = 1 << math::bit_size_u32(tex.height - 1): u32;
+
+ for (let y = 0u16; y < provider.nrows; y += 1) {
+ for (let x = 0u16; x < provider.ncolumns; x += 1) {
+ const ch = provider.chars[x + y * provider.ncolumns];
+ if (ch == '\0' || ch == ' ') {
+ continue;
+ };
+ const visual_width = 8i32; // TODO: fake value for now ._.
+ const advance = visual_width: f32 * scale;
+ // It's how Mojang does it *shrug*.
+ const advance = math::truncf64(advance + 0.5): f32 + 1.0;
+ append(font.glyphs, Glyph {
+ char = ch,
+ scale = scale,
+ ascent = provider.ascent: f32,
+ advance = advance,
+ width = glyph_width,
+ height = glyph_height,
+ x = x * glyph_width,
+ y = y * glyph_height,
+ gl_texture = gl_texture,
+ tex_width = tex_po2width,
+ tex_height = tex_po2height,
+ });
+ };
+ };
+};
+
+fn font_load_space(font: *Font, provider: *FontDeclProviderSpace) void = {
+ for (let i = 0z; i < len(provider.advances); i += 1) {
+ const (ch, advance) = provider.advances[i];
+ append(font.glyphs, Glyph {
+ char = ch,
+ scale = 0.0,
+ ascent = 0.0,
+ advance = advance,
+ width = 0,
+ height = 0,
+ x = 0,
+ y = 0,
+ gl_texture = 0,
+ tex_width = 0,
+ tex_height = 0,
+ });
+ };
+};
+
+fn font_find_glyph(font: *Font, ch: rune) nullable *Glyph = {
+ const key = Glyph {
+ char = ch,
+ ...
+ };
+ match (sort::search(font.glyphs, size(Glyph), &key, &glyph_cmpfunc)) {
+ case let i: size =>
+ return &font.glyphs[i];
+ case void =>
+ return null;
+ };
+};
+
+fn glyph_cmpfunc(a: const *opaque, b: const *opaque) int = {
+ let a = a: *Glyph;
+ let b = b: *Glyph;
+ return if (a.char: u32 < b.char: u32) -1
+ else if (a.char: u32 > b.char: u32) 1
+ else 0;
+};
+
+def TEXT_BASELINE_OFFSET = 7.0f32;
+
+type TextMetrics = struct {
+ width: f32,
+ ymin: f32,
+ ymax: f32,
+};
+
+fn font_measure(font: *Font, text: str) TextMetrics = {
+ let res = TextMetrics { ... };
+ let x = 0f32;
+
+ let it = strings::iter(text);
+ for (true) match (strings::next(&it)) {
+ case let ch: rune =>
+ const glyph = match (font_find_glyph(font, ch)) {
+ case let glyph: *Glyph =>
+ yield glyph;
+ case null =>
+ continue;
+ };
+
+ if (x + glyph.width: f32 * glyph.scale > res.width) {
+ res.width = x + glyph.width: f32 * glyph.scale;
+ };
+ if (-glyph.ascent < res.ymin) {
+ res.ymin = -glyph.ascent;
+ };
+ if (glyph.height: f32 * glyph.scale
+ - glyph.ascent > res.ymax) {
+ res.ymax = glyph.height: f32 * glyph.scale
+ - glyph.ascent;
+ };
+
+ x += glyph.advance;
+ case done => break;
+ };
+
+ res.ymin += TEXT_BASELINE_OFFSET;
+ res.ymax += TEXT_BASELINE_OFFSET;
+
+ return res;
+};
+
+fn render_text_shadow(text: str, font: *Font, trans: *glm::m4, color: [4]u8)
+ void = {
+ let shadow_trans = glm::translation_make(&[1.0f32, 1.0, 0.0]);
+ shadow_trans = glm::m4_mul(trans, &shadow_trans);
+ render_text(text, font, &shadow_trans,
+ [color[0] >> 2, color[1] >> 2, color[2] >> 2, color[3]]);
+ render_text(text, font, trans, color);
+};
+
+fn render_text(text: str, font: *Font, trans: *glm::m4, color: [4]u8) void = {
+ // TODO: being very latin-centric here...
+
+ glEnable(GL_BLEND);
+ glBlendEquation(GL_FUNC_ADD);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ let x = 0f32;
+
+ let it = strings::iter(text);
+ for (true) match (strings::next(&it)) {
+ case let ch: rune =>
+ const glyph = match (font_find_glyph(font, ch)) {
+ case let glyph: *Glyph =>
+ yield glyph;
+ case null =>
+ continue;
+ };
+
+ if (glyph.gl_texture != 0) {
+ render_rect_textured(trans,
+ x, TEXT_BASELINE_OFFSET - glyph.ascent,
+ glyph.width: f32 * glyph.scale,
+ glyph.height: f32 * glyph.scale,
+ color,
+ glyph.x: f32, glyph.y: f32,
+ glyph.width: f32, glyph.height: f32,
+ glyph.gl_texture,
+ glyph.tex_width, glyph.tex_height);
+ };
+
+ x += glyph.advance;
+ case done => break;
+ };
+};
+
+type FontDecl = struct {
+ providers: []FontDeclProvider,
+};
+
+type FontDeclProvider = (
+ FontDeclProviderBitmap |
+ FontDeclProviderLegacyUnicode |
+ FontDeclProviderSpace);
+
+// TODO: it might be necessary to work with grapheme clusters instead of code
+// points here...
+
+type FontDeclProviderBitmap = struct {
+ file: str,
+ height: i32,
+ ascent: i32,
+ chars: []rune, // len = ncolumns * nrows
+ ncolumns: u32,
+ nrows: u32,
+};
+
+type FontDeclProviderLegacyUnicode = void;
+
+type FontDeclProviderSpace = struct {
+ advances: [](rune, f32),
+};
+
+fn font_decl_provider_finish(provider: *FontDeclProvider) void = {
+ match (*provider) {
+ case let provider: FontDeclProviderBitmap =>
+ free(provider.file);
+ free(provider.chars);
+ case FontDeclProviderLegacyUnicode =>
+ void;
+ case let provider: FontDeclProviderSpace =>
+ free(provider.advances);
+ };
+};
+
+fn deser_font_decl(de: *dejson::deser) (FontDecl | dejson::error) = {
+ wassert_fields(de, "providers")?;
+ let success = false;
+
+ const de_providers = dejson::field(de, "providers")?;
+ const nproviders = dejson::length(&de_providers)?;
+ let providers: []FontDeclProvider = alloc([], nproviders);
+ defer if (!success) free(providers);
+ defer if (!success) for (let i = 0z; i < len(providers); i += 1) {
+ font_decl_provider_finish(&providers[i]);
+ };
+ for (let i = 0z; i < nproviders; i += 1) {
+ const de_provider = dejson::index(&de_providers, i)?;
+ const de_typ = dejson::field(&de_provider, "type")?;
+ const typ = dejson::string(&de_typ)?;
+ append(providers, switch (typ) {
+ case "bitmap" =>
+ yield deser_font_decl_provider_bitmap(&de_provider)?;
+ case "legacy_unicode" =>
+ yield deser_font_decl_provider_legacy_unicode(
+ &de_provider)?;
+ case "space" =>
+ yield deser_font_decl_provider_space(&de_provider)?;
+ case =>
+ const typename = dejson::strfield(typ);
+ defer free(typename);
+ return dejson::fail(&de_typ, "Unknown type {}", typename);
+ });
+ };
+
+ success = true;
+ return FontDecl {
+ providers = providers,
+ };
+};
+
+fn deser_font_decl_provider_bitmap(de: *dejson::deser)
+ (FontDeclProviderBitmap | dejson::error) = {
+ wassert_fields(de, "type", "file", "height", "ascent", "chars")?;
+ let success = false;
+
+ const de_file = dejson::field(de, "file")?;
+ const file = strings::dup(dejson::string(&de_file)?);
+ defer if (!success) free(file);
+
+ const de_height = dejson::optfield(de, "height")?;
+ const height = match (de_height) {
+ case let de_height: dejson::deser =>
+ yield dejson::number_i32(&de_height)?;
+ case void =>
+ yield 8i32;
+ };
+
+ const de_ascent = dejson::optfield(de, "ascent")?;
+ const ascent = match (de_ascent) {
+ case let de_ascent: dejson::deser =>
+ yield dejson::number_i32(&de_ascent)?;
+ case void =>
+ yield 0i32;
+ };
+
+ let chars: []rune = [];
+ defer if (!success) free(chars);
+ const de_chars = dejson::field(de, "chars")?;
+ const nrows = dejson::length(&de_chars)?;
+ if (nrows: u32 != nrows) {
+ return dejson::fail(&de_chars, "Too many rows");
+ };
+ const nrows = nrows: u32;
+ let ncolumns = 0z;
+ for (let i = 0z; i < nrows; i += 1) {
+ const de_row = dejson::index(&de_chars, i)?;
+ const row = dejson::string(&de_row)?;
+
+ let ncolumns_ = 0z;
+ let it = strings::iter(row);
+ for (true) match (strings::next(&it)) {
+ case let ch: rune =>
+ append(chars, ch);
+ ncolumns_ += 1;
+ case done => break;
+ };
+
+ if (i != 0 && ncolumns_ != ncolumns) {
+ return dejson::fail(&de_row,
+ "Row length {} is inconsistent with previous rows (was {})",
+ ncolumns_, ncolumns);
+ };
+ ncolumns = ncolumns_;
+ };
+ if (len(chars) == 0) {
+ return dejson::fail(&de_chars,
+ "Bitmap provider declares no characters");
+ };
+ if (ncolumns: u32 != ncolumns) {
+ return dejson::fail(&de_chars, "Too many rows");
+ };
+ let ncolumns = ncolumns: u32;
+
+ success = true;
+ return FontDeclProviderBitmap {
+ file = file,
+ height = height,
+ ascent = ascent,
+ chars = chars,
+ ncolumns = ncolumns,
+ nrows = nrows,
+ };
+};
+
+fn deser_font_decl_provider_legacy_unicode(de: *dejson::deser)
+ (FontDeclProviderLegacyUnicode | dejson::error) = {
+ // TODO: probably just remove this when updating to 1.20.
+ return FontDeclProviderLegacyUnicode;
+};
+
+fn deser_font_decl_provider_space(de: *dejson::deser)
+ (FontDeclProviderSpace | dejson::error) = {
+ wassert_fields(de, "type", "advances")?;
+ let success = false;
+
+ const de_advances = dejson::field(de, "advances")?;
+ let advances: [](rune, f32) = alloc([], dejson::count(&de_advances)?);
+ defer if (!success) free(advances);
+ const it = dejson::iter(&de_advances)?;
+ for (true) match (dejson::next(&it)) {
+ case let entry: (str, dejson::deser) =>
+ let it = strings::iter(entry.0);
+ const ch = match (strings::next(&it)) {
+ case let ch: rune =>
+ yield ch;
+ case done =>
+ return dejson::fail(&entry.1,
+ "Empty field name not allowed");
+ };
+ if (strings::next(&it) is rune) {
+ return dejson::fail(&entry.1,
+ "Field name must be a single code point");
+ };
+ append(advances, (ch, dejson::number(&entry.1)?: f32));
+ case void => break;
+ };
+
+ success = true;
+ return FontDeclProviderSpace {
+ advances = advances,
+ };
+};