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, }; };