Skip to content

Commit

Permalink
Add font fallback functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
hasenbanck committed Jan 7, 2025
1 parent 4c2191e commit a88bbb9
Show file tree
Hide file tree
Showing 18 changed files with 349 additions and 126 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
target/

# Korangar
korangar/archive/data/font/*
korangar/client/
korangar/data.grf
korangar/rdata.grf
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ ron = "0.8"
serde = "1.0"
spin_sleep = "1.3"
syn = "2.0"
sys-locale = "0.3"
tokio = { version = "1.42", default-features = false }
walkdir = "2.5"
wgpu = "23.0"
Expand Down
1 change: 1 addition & 0 deletions korangar/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ rayon = { workspace = true }
ron = { workspace = true }
serde = { workspace = true }
spin_sleep = { workspace = true }
sys-locale = { workspace = true }
walkdir = { workspace = true }
wgpu = { workspace = true }
winit = { workspace = true }
Expand Down
Binary file modified korangar/archive/data/font/NotoSans.csv.gz
Binary file not shown.
Binary file modified korangar/archive/data/font/NotoSans.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 9 additions & 6 deletions korangar/archive/data/font/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ included in the distributed font file. Currently, we use "Noto Sans", which incl
and Greek alphabets. If you need to support a different alphabet, then you need to use a different font (for example
"Noto Sans Japanese"). Since Korangar uses a pre-assembled font map, that includes all glyphs of a font file in
a multichannel signed distance field (MSDF) representation, you need to create a such a font map and also a font map
description file in the CSV format:
description file in the CSV format. We support having fallback fonts, so if for example the primary fonts doesn't have
a specific glyph, the fallback font is tried. Multiple fallback fonts are supported. The final height of all font maps
combined must not exceed 8192 pixel.

1. Use [msdfgen](https://github.com/Chlumsky/msdfgen) to create the font map image and the description file in the
CSV format.
CSV format. The image width must be 8192 pixel wide and the height should be chosen to be a multiple of 4 and have a
minimal size, so that all glyphs are included and no space is wasted. This is needed to properly merge multiple fonts
into one font map.
```sh
msdf-atlas-gen -allglyphs -pxrange 6 -size 32 -yorigin top -type mtsdf -format png -font NotoSans.ttf -csv NotoSans.csv -imageout NotoSans.png
msdf-atlas-gen -allglyphs -pxrange 6 -size 32 -yorigin top -dimensions 8192 4096 -type msdf -format png -font NotoSans.ttf -csv NotoSans.csv -imageout NotoSans.png
```
2. Copy the original font file and also the generated PNG and CSV file into the `archive/data/font` folder.
3. Optionally compress the CSV file with gzip:
3. Compress the CSV file with gzip:
```sh
gzip NotoSans.csv
```
4. Update the `FONT_FILE_PATH`, `FONT_MAP_DESCRIPTION_FILE_PATH`, `FONT_MAP_FILE_PATH` and `FONT_FAMILY_NAME` in the
`korangar/src/loaders/font/mod.rs` file.
4. Update the DEFAULT_FONTS list inside the `korangar/src/interface/application.rs` file.
20 changes: 17 additions & 3 deletions korangar/src/interface/application.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::marker::ConstParamTy;
use std::sync::Arc;

#[cfg(feature = "debug")]
use korangar_debug::logging::{print_debug, Colorize};
Expand All @@ -22,6 +23,8 @@ use crate::input::{MouseInputMode, UserEvent};
use crate::loaders::{FontLoader, FontSize, Scaling};
use crate::renderer::InterfaceRenderer;

const DEFAULT_FONTS: &[&str] = &["NotoSans", "NotoSansKR"];

impl korangar_interface::application::ColorTrait for Color {
fn is_transparent(&self) -> bool {
const TRANSPARENCY_THRESHOLD: f32 = 0.999;
Expand Down Expand Up @@ -177,6 +180,7 @@ impl<const KIND: InternalThemeKind> PrototypeElement<InterfaceSettings> for Them

#[derive(Serialize, Deserialize)]
struct InterfaceSettingsStorage {
fonts: Vec<String>,
menu_theme: String,
main_theme: String,
game_theme: String,
Expand All @@ -185,12 +189,14 @@ struct InterfaceSettingsStorage {

impl Default for InterfaceSettingsStorage {
fn default() -> Self {
let fonts = Vec::from_iter(DEFAULT_FONTS.iter().map(|font| font.to_string()));
let main_theme = "client/themes/main.ron".to_string();
let menu_theme = "client/themes/menu.ron".to_string();
let game_theme = "client/themes/game.ron".to_string();
let scaling = Scaling::new(1.0);

Self {
fonts,
main_theme,
menu_theme,
game_theme,
Expand Down Expand Up @@ -231,6 +237,7 @@ impl InterfaceSettingsStorage {

#[derive(PrototypeElement)]
pub struct InterfaceSettings {
fonts: Vec<String>,
#[name("Main theme")]
pub main_theme: ThemeSelector<{ InternalThemeKind::Main }>,
#[name("Menu theme")]
Expand All @@ -245,19 +252,21 @@ pub struct InterfaceSettings {
impl InterfaceSettings {
pub fn load_or_default() -> Self {
let InterfaceSettingsStorage {
fonts,
menu_theme,
main_theme,
game_theme,
scaling,
} = InterfaceSettingsStorage::load_or_default();

let themes = Themes::new(
InterfaceTheme::new::<super::theme::DefaultMenu>(&menu_theme),
InterfaceTheme::new::<super::theme::DefaultMain>(&main_theme),
InterfaceTheme::new::<DefaultMenu>(&menu_theme),
InterfaceTheme::new::<DefaultMain>(&main_theme),
GameTheme::new(&menu_theme),
);

Self {
fonts,
main_theme: ThemeSelector(main_theme),
menu_theme: ThemeSelector(menu_theme),
game_theme: ThemeSelector(game_theme),
Expand All @@ -278,6 +287,10 @@ impl InterfaceSettings {
pub fn get_game_theme(&self) -> &GameTheme {
&self.themes.game
}

pub fn get_fonts(&self) -> &[String] {
&self.fonts
}
}

impl InterfaceSettings {
Expand Down Expand Up @@ -317,7 +330,7 @@ impl Application for InterfaceSettings {
type CustomEvent = UserEvent;
type DropResource = PartialMove;
type DropResult = Move;
type FontLoader = std::rc::Rc<std::cell::RefCell<FontLoader>>;
type FontLoader = Arc<FontLoader>;
type FontSize = FontSize;
type MouseInputMode = MouseInputMode;
type PartialSize = PartialScreenSize;
Expand All @@ -343,6 +356,7 @@ impl Application for InterfaceSettings {
impl Drop for InterfaceSettings {
fn drop(&mut self) {
InterfaceSettingsStorage {
fonts: self.fonts.to_owned(),
menu_theme: self.menu_theme.get_file().to_owned(),
main_theme: self.main_theme.get_file().to_owned(),
game_theme: self.game_theme.get_file().to_owned(),
Expand Down
7 changes: 3 additions & 4 deletions korangar/src/interface/elements/miscellanious/chat/builder.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;

use korangar_interface::builder::Unset;
use korangar_interface::state::PlainRemote;
Expand Down Expand Up @@ -33,12 +32,12 @@ impl<Font> ChatBuilder<Unset, Font> {
}

impl<Messages> ChatBuilder<Messages, Unset> {
pub fn with_font_loader(self, font_loader: Rc<RefCell<FontLoader>>) -> ChatBuilder<Messages, Rc<RefCell<FontLoader>>> {
pub fn with_font_loader(self, font_loader: Arc<FontLoader>) -> ChatBuilder<Messages, Arc<FontLoader>> {
ChatBuilder { font_loader, ..self }
}
}

impl ChatBuilder<PlainRemote<Vec<ChatMessage>>, Rc<RefCell<FontLoader>>> {
impl ChatBuilder<PlainRemote<Vec<ChatMessage>>, Arc<FontLoader>> {
/// Take the builder and turn it into a [`Chat`].
///
/// NOTE: This method is only available if
Expand Down
6 changes: 2 additions & 4 deletions korangar/src/interface/elements/miscellanious/chat/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
mod builder;

use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;

use korangar_interface::application::{Application, FontSizeTraitExt};
use korangar_interface::elements::{Element, ElementState};
Expand All @@ -22,7 +21,7 @@ use crate::renderer::InterfaceRenderer;

pub struct Chat {
messages: PlainRemote<Vec<ChatMessage>>,
font_loader: Rc<RefCell<FontLoader>>,
font_loader: Arc<FontLoader>,
state: ElementState<InterfaceSettings>,
}

Expand Down Expand Up @@ -55,7 +54,6 @@ impl Element<InterfaceSettings> for Chat {
for message in self.messages.get().iter() {
height += self
.font_loader
.borrow_mut()
.get_text_dimensions(
&message.text,
theme.chat.font_size.get().scaled(application.get_scaling()),
Expand Down
5 changes: 2 additions & 3 deletions korangar/src/interface/windows/generic/chat.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;

use derive_new::new;
use korangar_interface::elements::{ButtonBuilder, ElementWrap, InputFieldBuilder, ScrollView};
Expand All @@ -26,7 +25,7 @@ pub struct ChatMessage {
#[derive(new)]
pub struct ChatWindow {
messages: PlainRemote<Vec<ChatMessage>>,
font_loader: Rc<RefCell<FontLoader>>,
font_loader: Arc<FontLoader>,
}

impl ChatWindow {
Expand Down
108 changes: 108 additions & 0 deletions korangar/src/loaders/font/font_file.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
use std::io::{Cursor, Read};
use std::sync::Arc;

use cosmic_text::fontdb::{Source, ID};
use cosmic_text::FontSystem;
use flate2::bufread::GzDecoder;
use hashbrown::HashMap;
use image::{ImageFormat, ImageReader, RgbaImage};
#[cfg(feature = "debug")]
use korangar_debug::logging::{print_debug, Colorize, Timer};
use korangar_util::FileLoader;

use crate::loaders::font::font_map_descriptor::parse_glyphs;
use crate::loaders::font::GlyphCoordinate;
use crate::loaders::GameFileLoader;

const FONT_FOLDER_PATH: &str = "data\\font";

pub(crate) struct FontFile {
pub(crate) ids: Vec<ID>,
pub(crate) font_map: RgbaImage,
pub(crate) glyphs: Arc<HashMap<u16, GlyphCoordinate>>,
}

impl FontFile {
pub(crate) fn new(name: &str, game_file_loader: &GameFileLoader, font_system: &mut FontSystem) -> Option<Self> {
#[cfg(feature = "debug")]
let timer = Timer::new_dynamic(format!("load font: {}", name.magenta()));

let font_base_path = format!("{}\\{}", FONT_FOLDER_PATH, name);
let ttf_file_path = format!("{}.ttf", font_base_path);
let map_file_path = format!("{}.png", font_base_path);
let map_description_file_path = format!("{}.csv.gz", font_base_path);

let Ok(font_data) = game_file_loader.get(&ttf_file_path) else {
#[cfg(feature = "debug")]
print_debug!("[{}] failed to load font file '{}'", "error".red(), ttf_file_path.magenta());
return None;
};

let ids = font_system.db_mut().load_font_source(Source::Binary(Arc::new(font_data)));

let Ok(font_map_data) = game_file_loader.get(&map_file_path) else {
#[cfg(feature = "debug")]
print_debug!("[{}] failed to load font map file '{}'", "error".red(), map_file_path.magenta());
return None;
};

let font_map_reader = ImageReader::with_format(Cursor::new(font_map_data), ImageFormat::Png);

let Ok(font_map_decoder) = font_map_reader.decode() else {
#[cfg(feature = "debug")]
print_debug!("[{}] failed to decode font map '{}'", "error".red(), map_file_path.magenta());
return None;
};

let font_map_rgba_image = font_map_decoder.into_rgba8();
let font_map_width = font_map_rgba_image.width();
let font_map_height = font_map_rgba_image.height();

let Ok(mut font_description_data) = game_file_loader.get(&map_description_file_path) else {
#[cfg(feature = "debug")]
print_debug!(
"[{}] failed to load font map description file '{}'",
"error".red(),
map_description_file_path.magenta()
);
return None;
};

let mut decoder = GzDecoder::new(&font_description_data[..]);
let mut data = Vec::with_capacity(font_description_data.len() * 2);

if let Err(_err) = decoder.read_to_end(&mut data) {
#[cfg(feature = "debug")]
print_debug!(
"[{}] failed to decompress font map description file '{}': {:?}",
"error".red(),
map_description_file_path.magenta(),
_err
);
return None;
}

font_description_data = data;

let Ok(font_description_content) = String::from_utf8(font_description_data) else {
#[cfg(feature = "debug")]
print_debug!(
"[{}] invalid UTF-8 text data found in font map description file '{}'",
"error".red(),
map_description_file_path.magenta()
);
return None;
};

let glyphs = parse_glyphs(font_description_content, font_map_width, font_map_height);

#[cfg(feature = "debug")]
timer.stop();

Some(Self {
ids: Vec::from_iter(ids),
font_map: font_map_rgba_image,
glyphs: Arc::new(glyphs),
})
}
}
6 changes: 1 addition & 5 deletions korangar/src/loaders/font/font_map_descriptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,7 @@ struct Bounds {
top: f64,
}

pub(crate) fn parse_glyph_cache(
font_description_content: String,
font_map_width: u32,
font_map_height: u32,
) -> HashMap<u16, GlyphCoordinate> {
pub(crate) fn parse_glyphs(font_description_content: String, font_map_width: u32, font_map_height: u32) -> HashMap<u16, GlyphCoordinate> {
let font_map_width = font_map_width as f32;
let font_map_height = font_map_height as f32;

Expand Down
Loading

0 comments on commit a88bbb9

Please sign in to comment.