diff options
Diffstat (limited to 'fonts.ha')
-rw-r--r-- | fonts.ha | 491 |
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, + }; +}; |