Font_File :: struct { font_name: string; font_data: string; } // The fonts are compiled into the executable. The paths are relative to the working directory of // first.jai; assets/fonts holds a few more to swap in. #run,host reads the file on the build // machine even when cross-compiling, and sees nothing declared outside its own block. OpenSans_Bold :: Font_File.{"OpenSans-Bold", #run,host -> string { #import "File"; return read_entire_file("assets/fonts/OpenSans-Bold.ttf"); }}; OpenSans_Regular :: Font_File.{"OpenSans-Regular", #run,host -> string { #import "File"; return read_entire_file("assets/fonts/OpenSans-Regular.ttf"); }}; RobotoMono_Regular :: Font_File.{"RobotoMono-Regular", #run,host -> string { #import "File"; return read_entire_file("assets/fonts/RobotoMono-Regular.ttf"); }}; default_global_font_size :: 12.0; default_header_font_size :: 0.5 * default_global_font_size + default_global_font_size; font_data_default: *ImGui.ImFont; alternate_font: *ImGui.ImFont; header_font: *ImGui.ImFont; push_imgui_font :: (font: *ImGui.ImFont, scale: float32) #expand { style := ImGui.GetStyle(); ImGui.PushFont(font, style.FontSizeBase * scale); `defer ImGui.PopFont(); } app_imgui_load_fonts :: () { io := ImGui.GetIO(); cjk_fonts_load(); { font_config: ImGui.ImFontConfig; font_config.Constructor(*font_config); // *sigh* // By default, ImGui takes ownership of the font data and attempts to free it, resulting in a crash, // so we have to explicitly tell it to not do this. font_config.FontDataOwnedByAtlas = false; memcpy(font_config.Name.data, RobotoMono_Regular.font_name.data, RobotoMono_Regular.font_name.count); font_data_default = ImGui.IO.Fonts.AddFontFromMemoryTTF(io.Fonts, RobotoMono_Regular.font_data.data, xx RobotoMono_Regular.font_data.count, default_global_font_size, *font_config); merge_cjk_fallbacks(font_data_default); } { // Header font for menu font_config: ImGui.ImFontConfig; font_config.Constructor(*font_config); font_config.FontDataOwnedByAtlas = false; memcpy(font_config.Name.data, OpenSans_Bold.font_name.data, OpenSans_Bold.font_name.count); header_font = ImGui.IO.Fonts.AddFontFromMemoryTTF(io.Fonts, OpenSans_Bold.font_data.data, xx OpenSans_Bold.font_data.count, default_header_font_size, *font_config); merge_cjk_fallbacks(header_font); } // Add another font: { font_config: ImGui.ImFontConfig; font_config.Constructor(*font_config); font_config.FontDataOwnedByAtlas = false; memcpy(font_config.Name.data, OpenSans_Regular.font_name.data, OpenSans_Regular.font_name.count); alternate_font = ImGui.IO.Fonts.AddFontFromMemoryTTF(io.Fonts, OpenSans_Regular.font_data.data, xx OpenSans_Regular.font_data.count, default_global_font_size, *font_config); merge_cjk_fallbacks(alternate_font); } assert(font_data_default != null); // Set global default font: io.FontDefault = font_data_default; } Cjk_Script :: enum u8 { SIMPLIFIED; TRADITIONAL; JAPANESE; KOREAN; } Cjk_Font :: struct { script: Cjk_Script; path: string; name: string; // for the log and ImFontConfig.Name face: u32; // index within a .ttc data: string; // once loaded: mapped or read, kept for the life of the atlas } cjk_fonts: [..] Cjk_Font; // in fallback order cjk_candidate :: (list: *[..] Cjk_Font, script: Cjk_Script, path: string, name: string, face: u32 = 0) { array_add(list, .{script = script, path = path, name = name, face = face}); } // The scripts share their Han characters and the first merged font that has a glyph draws it, so // the user's language picks which comes first. cjk_fallback_order :: (language: string) -> [4] Cjk_Script { subtags := split(to_lower_copy(replace(language, "_", "-",, temp),, temp), "-",, temp); primary := ifx subtags.count then subtags[0] else ""; if primary == "ja" return Cjk_Script.[.JAPANESE, .SIMPLIFIED, .TRADITIONAL, .KOREAN]; if primary == "ko" return Cjk_Script.[.KOREAN, .SIMPLIFIED, .TRADITIONAL, .JAPANESE]; if primary == "zh" { for subtags if it == "hant" || it == "tw" || it == "hk" || it == "mo" return Cjk_Script.[.TRADITIONAL, .SIMPLIFIED, .JAPANESE, .KOREAN]; } return Cjk_Script.[.SIMPLIFIED, .TRADITIONAL, .JAPANESE, .KOREAN]; } // One font per script, the first candidate found. A file several scripts share is loaded once. cjk_fonts_load :: () { push_allocator(context.default_allocator); language := platform_preferred_language(); candidates := platform_cjk_font_candidates(); for script: cjk_fallback_order(language) { for candidate: candidates { if candidate.script != script || !file_exists(candidate.path) continue; font := candidate; for cjk_fonts if it.path == font.path font.data = it.data; if !font.data { ok: bool; font.data, ok = platform_map_file_read_only(font.path); if !ok { font.data, ok = read_entire_file(font.path); if !ok continue; log("[fonts] Could not map %; read it into memory instead.", font.path); } } font.path = copy_string(font.path); font.name = copy_string(font.name); array_add(*cjk_fonts, font); break; } } names: [..] string; names.allocator = temp; for cjk_fonts array_add(*names, it.name); log("[fonts] CJK fallback for %: %.", ifx language then language else "an unknown language", ifx names.count then join(..names, ", ",, temp) else "none found"); } // Since ImGui 1.92 a merged font is only asked for glyphs the ones before it lack, as they are drawn. merge_cjk_fallbacks :: (dst: *ImGui.ImFont) { io := ImGui.GetIO(); for cjk_fonts { font_config: ImGui.ImFontConfig; font_config.Constructor(*font_config); font_config.FontDataOwnedByAtlas = false; font_config.MergeMode = true; font_config.DstFont = dst; font_config.FontNo = it.face; memcpy(font_config.Name.data, it.name.data, min(it.name.count, font_config.Name.count - 1)); ImGui.IO.Fonts.AddFontFromMemoryTTF(io.Fonts, it.data.data, xx it.data.count, 0, *font_config); } } DEFAULT_FONT_SCALE_INCREMENT :: 0.05; FONT_SCALE_MIN :: 0.5; FONT_SCALE_MAX :: 4.0; imgui_decrease_font_scale :: (scale_increment:=DEFAULT_FONT_SCALE_INCREMENT) { style := ImGui.GetStyle(); style.FontScaleMain = clamp(style.FontScaleMain - scale_increment, FONT_SCALE_MIN, FONT_SCALE_MAX); } imgui_increase_font_scale :: (scale_increment:=DEFAULT_FONT_SCALE_INCREMENT) { style := ImGui.GetStyle(); style.FontScaleMain = clamp(style.FontScaleMain + scale_increment, FONT_SCALE_MIN, FONT_SCALE_MAX); }