Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make font hinting user-configurable. #78830

Merged
merged 10 commits into from
Jan 5, 2025
13 changes: 6 additions & 7 deletions data/fontdata.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
{
"//1": "If more than one font is specified for a typeface the list is treated as a fallback order.",
"//2": "unifont will always be used as a 'last resort' fallback even if not listed here.",
"//1": "See docs/FONT_OPTIONS.md for how to configure your fonts.",
"typeface": [
"data/font/Terminus.ttf",
{ "path": "data/font/Terminus.ttf", "hinting": "Bitmap" },
"data/font/unifont.ttf"
],
"gui_typeface": [
"data/font/Roboto-Medium.ttf",
"data/font/Terminus.ttf",
{ "path": "data/font/Roboto-Medium.ttf", "hinting": "Auto" },
{ "path": "data/font/Terminus.ttf", "hinting": "Bitmap" },
"data/font/unifont.ttf"
],
"map_typeface": [
"data/font/Terminus.ttf",
{ "path": "data/font/Terminus.ttf", "hinting": "Bitmap" },
"data/font/unifont.ttf"
],
"overmap_typeface": [
"data/font/Terminus.ttf",
{ "path": "data/font/Terminus.ttf", "hinting": "Bitmap" },
"data/font/unifont.ttf"
]
}
52 changes: 52 additions & 0 deletions doc/FONT_OPTIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Configuring Fonts

Fonts can be configured through changing the config/fonts.json file. This file is created with
default options on game load, so if it doesn't exist try loading the game.

The default options (available in data/fontdata.json) might look like the following:
```json
{
"typeface": [
{ "path": "data/font/Terminus.ttf", "hinting": "Bitmap" },
"data/font/unifont.ttf"
],
"gui_typeface": [
{ "path": "data/font/Roboto-Medium.ttf", "hinting": "Auto" },
{ "path": "data/font/Terminus.ttf", "hinting": "Bitmap" },
"data/font/unifont.ttf"
],
"map_typeface": [
{ "path": "data/font/Terminus.ttf", "hinting": "Bitmap" },
"data/font/unifont.ttf"
],
"overmap_typeface": [
{ "path": "data/font/Terminus.ttf", "hinting": "Bitmap" },
"data/font/unifont.ttf"
]
}
```

There are four different font categories: `typeface`, `gui_typeface`, `map_typeface`, and `overmap_typeface`,
which are used for the old-style game interface, ImGui menus, the game display and the overmap, respectively.
If more than one font is specified for a typeface the list is treated as a fallback order. Unifont will always
be used as a 'last resort' fallback even if not listed here.

Fonts can be provided as a list, as seen above, or as single entries, e.g.:
```json
"typeface": { "path": "data/font/Terminus.ttf", "hinting": "Bitmap" },
"map_typeface": "unifont.ttf"
```
Each entry may be a string or an object. If a string is provided it is interpreted as the path to the typeface file.
If an object is provided, then it must have a path attribute, which provides the path to the font. It may also have
a 'hinting' attribute, which determines the font hinting mode. If it's not specified then 'Default' is used.
Hinting may be one of: Auto, NoAuto, Default, Light, or Bitmap. Antialiasing may also be provided and is default set
to true, but can be disabled by setting to off.

A full object may look like the following:
```json
"gui_typeface": {
"path": "data/font/Roboto-Medium.ttf",
"hinting": "Auto",
"antialiasing": true,
}
```
36 changes: 20 additions & 16 deletions src/cata_imgui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -365,27 +365,31 @@ static void AddGlyphRangesMisc( UNUSED ImFontGlyphRangesBuilder *b )
b->AddRanges( &superscripts[0] );
}

static void load_font( ImGuiIO &io, const std::vector<std::string> &typefaces,

// Load the first font that exists in typefaces, falling back to unifont
// if none of them exist.
// - typefaces is a list of paths.
static void load_font( ImGuiIO &io, const std::vector<font_config> &typefaces,
const ImWchar *ranges )
{
std::vector<std::string> io_typefaces{ typefaces };
std::vector<font_config> io_typefaces{ typefaces };
ensure_unifont_loaded( io_typefaces );

auto it = std::find_if( io_typefaces.begin(),
io_typefaces.end(),
[]( const std::string & io_typeface ) {
return file_exist( io_typeface );
} );
std::string existing_typeface = *it;
ImFontConfig config = ImFontConfig();
#ifdef IMGUI_ENABLE_FREETYPE
if( existing_typeface.find( "Terminus.ttf" ) != std::string::npos ||
existing_typeface.find( "unifont.ttf" ) != std::string::npos ) {
config.FontBuilderFlags = ImGuiFreeTypeBuilderFlags_ForceAutoHint;
auto it = std::begin( io_typefaces );
for( ; it != std::end( io_typefaces ); ++it ) {
if( !file_exist( it->path ) ) {
debugmsg( "Font file '%s' does not exist.", it->path );
}
break;
}
#endif
if( it == std::end( io_typefaces ) ) {
debugmsg( "No fonts were found in the fontdata file." );
}

ImFontConfig config = ImFontConfig();
config.FontBuilderFlags = it->imgui_config();

io.Fonts->AddFontFromFileTTF( existing_typeface.c_str(), fontheight, &config, ranges );
io.Fonts->AddFontFromFileTTF( it->path.c_str(), fontheight, &config, ranges );
}

static void check_font( const ImFont *font )
Expand All @@ -405,7 +409,7 @@ static void check_font( const ImFont *font )
void cataimgui::client::load_fonts( UNUSED const Font_Ptr &gui_font,
const Font_Ptr &mono_font,
const std::array<SDL_Color, color_loader<SDL_Color>::COLOR_NAMES_COUNT> &windowsPalette,
const std::vector<std::string> &gui_typefaces, const std::vector<std::string> &mono_typefaces )
const std::vector<font_config> &gui_typefaces, const std::vector<font_config> &mono_typefaces )
{
ImGuiIO &io = ImGui::GetIO();
if( ImGui::GetIO().FontDefault == nullptr ) {
Expand Down
4 changes: 3 additions & 1 deletion src/cata_imgui.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#include <vector>
#include <unordered_map>

#include "font_loader.h"

class nc_color;
struct input_event;
using ImGuiInputTextFlags = int;
Expand Down Expand Up @@ -74,7 +76,7 @@ class client
const GeometryRenderer_Ptr &sdl_geometry );
void load_fonts( const std::unique_ptr<Font> &gui_font, const std::unique_ptr<Font> &mono_font,
const std::array<SDL_Color, color_loader<SDL_Color>::COLOR_NAMES_COUNT> &windowsPalette,
const std::vector<std::string> &gui_typeface, const std::vector<std::string> &mono_typeface );
const std::vector<font_config> &gui_typeface, const std::vector<font_config> &mono_typeface );
#endif
~client();

Expand Down
197 changes: 170 additions & 27 deletions src/font_loader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@
#include "json_loader.h"

// Ensure that unifont is always loaded as a fallback font to prevent users from shooting themselves in the foot
void ensure_unifont_loaded( std::vector<font_config> &font_list )
{
const std::string unifont = PATH_INFO::fontdir() + "unifont.ttf";
if( std::find_if( font_list.begin(), font_list.end(), [&]( const font_config & conf ) {
return conf.path == unifont;
} ) == font_list.end() ) {
font_list.emplace_back( unifont );
}
}

void ensure_unifont_loaded( std::vector<std::string> &font_list )
{
const std::string unifont = PATH_INFO::fontdir() + "unifont.ttf";
Expand All @@ -13,54 +23,184 @@ void ensure_unifont_loaded( std::vector<std::string> &font_list )
}
}

void font_loader::load_throws( const cata_path &path )
unsigned int font_config::imgui_config() const
{
try {
JsonValue json = json_loader::from_path( path );
JsonObject config = json.get_object();
if( config.has_string( "typeface" ) ) {
typeface.emplace_back( config.get_string( "typeface" ) );
} else {
config.read( "typeface", typeface );
}
if( config.has_string( "gui_typeface" ) ) {
gui_typeface.emplace_back( config.get_string( "gui_typeface" ) );
unsigned int ret = 0;
if( !antialiasing ) {
ret |= ImGuiFreeTypeBuilderFlags_Monochrome;
ret |= ImGuiFreeTypeBuilderFlags_MonoHinting;
}
if( hinting != std::nullopt ) {
ret |= *hinting;
}
return ret;
}

std::optional<ImGuiFreeTypeBuilderFlags> hint_to_fonthint( const std::string_view hinting )
{
if( hinting == "Auto" ) {
return ImGuiFreeTypeBuilderFlags_ForceAutoHint;
}
if( hinting == "NoAuto" ) {
return ImGuiFreeTypeBuilderFlags_NoAutoHint;
}
if( hinting == "Light" ) {
return ImGuiFreeTypeBuilderFlags_LightHinting;
}
if( hinting == "Bitmap" ) {
return ImGuiFreeTypeBuilderFlags_Bitmap;
}
if( hinting == "Default" ) {
return std::nullopt;
GuardianDll marked this conversation as resolved.
Show resolved Hide resolved
}
debugmsg( "'%s' is an invalid font hinting value.", hinting );
return std::nullopt;
}

void font_config::deserialize( const JsonObject &jo )
{
bool has_path = jo.read( "path", path, true );
if( !( has_path ) ) {
jo.throw_error( "Dummy error - force config read to return false." );
}
// Manually read hinting.
// Specifying enum traits for would allow all options, and we want only
// some to be available to the user.
if( jo.has_string( "hinting" ) ) {
hinting = hint_to_fonthint( jo.get_string( "hinting" ) );
}
jo.read( "antialiasing", antialiasing, false );
}

static void load_font_from_config( const JsonObject &config, const std::string &key,
std::vector<font_config> &typefaces )
{

if( config.has_string( key ) ) {
std::string path = config.get_string( key );
// Migrate old font config files. Remove after 0.I
if( path.find( "Terminus.ttf" ) != std::string::npos ) {
typefaces.emplace_back( path, ImGuiFreeTypeBuilderFlags_Bitmap );
} else if( path.find( "Roboto-Medium.ttf" ) != std::string::npos ) {
typefaces.emplace_back( path, ImGuiFreeTypeBuilderFlags_ForceAutoHint );
} else {
config.read( "gui_typeface", gui_typeface );
typefaces.emplace_back( path );
}
if( config.has_string( "map_typeface" ) ) {
map_typeface.emplace_back( config.get_string( "map_typeface" ) );
} else if( config.has_object( key ) ) {
// TODO: This should be doable without having to instantiate a fake path.
CLIDragon marked this conversation as resolved.
Show resolved Hide resolved
font_config conf = font_config( "dummy" );
if( !config.read( key, conf, false ) ) {
debugmsg( "Key '%s' should contain a path entry.", key );
} else {
config.read( "map_typeface", map_typeface );
typefaces.push_back( conf );
}
if( config.has_string( "overmap_typeface" ) ) {
overmap_typeface.emplace_back( config.get_string( "overmap_typeface" ) );
} else {
config.read( "overmap_typeface", overmap_typeface );
} else if( config.has_array( key ) ) {
JsonArray array = config.get_array( key );
for( JsonValue value : array ) {
if( value.test_string() ) {
std::string path = value.get_string();
// Migrate old font config files. Remove after 0.I
if( path.find( "Terminus.ttf" ) != std::string::npos ) {
typefaces.emplace_back( path, ImGuiFreeTypeBuilderFlags_Bitmap );
} else if( path.find( "Roboto-Medium.ttf" ) != std::string::npos ) {
typefaces.emplace_back( path, ImGuiFreeTypeBuilderFlags_ForceAutoHint );
} else {
typefaces.emplace_back( path );
}
} else if( value.test_object() ) {
font_config conf = font_config( "dummy" );
if( !value.read( conf, false ) ) {
debugmsg( "Key '%s' has an invalid array entry in the font config file.", key );
} else {
typefaces.push_back( conf );
}
} else {
// Value is neither a string nor an object.
debugmsg( "Invalid font entry for key '%s'. Font array entries should be objects or strings.",
key );
}
}
} else {
debugmsg( "Font specifiers must be an array, object, or string." );
}

ensure_unifont_loaded( typefaces );
}

ensure_unifont_loaded( typeface );
ensure_unifont_loaded( gui_typeface );
ensure_unifont_loaded( map_typeface );
ensure_unifont_loaded( overmap_typeface );

void font_loader::load_throws( const cata_path &path )
{
try {
JsonValue json = json_loader::from_path( path );
JsonObject config = json.get_object();
config.allow_omitted_members(); // We do actually visit every member, but over several functions.
load_font_from_config( config, "typeface", typeface );
load_font_from_config( config, "gui_typeface", gui_typeface );
load_font_from_config( config, "map_typeface", map_typeface );
load_font_from_config( config, "overmap_typeface", overmap_typeface );
} catch( const std::exception &err ) {
throw std::runtime_error( std::string( "loading font settings from " ) + path.generic_u8string() +
" failed: " +
err.what() );
}
}

// Convenience function for font_loader::save. Assumes that this is a member of
// an object.
void write_font_config( JsonOut &json, const std::vector<font_config> &typefaces )
{
json.start_array();
for( const font_config &config : typefaces ) {
json.start_object();
json.member( "path", config.path );

if( config.hinting == std::nullopt ) {
json.member( "hinting", "Default" );
} else {
switch( *config.hinting ) {
case ImGuiFreeTypeBuilderFlags_ForceAutoHint:
json.member( "hinting", "Auto" );
break;
case ImGuiFreeTypeBuilderFlags_Bitmap:
json.member( "hinting", "Bitmap" );
break;
case ImGuiFreeTypeBuilderFlags_LightHinting:
json.member( "hinting", "Light" );
break;
case ImGuiFreeTypeBuilderFlags_NoHinting:
json.member( "hinting", "NoAuto" );
break;
default:
// This should never be reached.
json.member( "hinting", "Default" );
break;
GuardianDll marked this conversation as resolved.
Show resolved Hide resolved
}
}

if( !config.antialiasing ) {
json.member( "antialiasing", false );
}

json.end_object();
}
json.end_array();
}

void font_loader::save( const cata_path &path ) const
{
try {
write_to_file( path, [&]( std::ostream & stream ) {
JsonOut json( stream, true ); // pretty-print
json.start_object();
json.member( "typeface", typeface );
json.member( "gui_typeface", typeface );
json.member( "map_typeface", map_typeface );
json.member( "overmap_typeface", overmap_typeface );
json.member( "//", "See docs/FONT_OPTIONS.md for an explanation of this file." );
json.member( "typeface" );
write_font_config( json, typeface );
json.member( "gui_typeface" );
write_font_config( json, gui_typeface );
json.member( "map_typeface" );
write_font_config( json, map_typeface );
json.member( "overmap_typeface" );
write_font_config( json, overmap_typeface );
json.end_object();
stream << "\n";
} );
Expand All @@ -74,6 +214,9 @@ void font_loader::load()
const cata_path fontdata = PATH_INFO::fontdata();
if( file_exist( fontdata ) ) {
load_throws( fontdata );
// Migrate old font files to the new format.
// Remove after 0.I.
save( fontdata );
} else {
const cata_path legacy_fontdata = PATH_INFO::legacy_fontdata();
load_throws( legacy_fontdata );
Expand Down
Loading
Loading