Skip to content

Commit fc84b1d

Browse files
committed
Add font fallback functionality
1 parent b955aa8 commit fc84b1d

File tree

18 files changed

+349
-126
lines changed

18 files changed

+349
-126
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
target/
33

44
# Korangar
5+
korangar/archive/data/font/*
56
korangar/client/
67
korangar/data.grf
78
korangar/rdata.grf

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ ron = "0.8"
4646
serde = "1.0"
4747
spin_sleep = "1.3"
4848
syn = "2.0"
49+
sys-locale = "0.3"
4950
tokio = { version = "1.42", default-features = false }
5051
walkdir = "2.5"
5152
wgpu = "23.0"

korangar/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ rayon = { workspace = true }
3636
ron = { workspace = true }
3737
serde = { workspace = true }
3838
spin_sleep = { workspace = true }
39+
sys-locale = { workspace = true }
3940
walkdir = { workspace = true }
4041
wgpu = { workspace = true }
4142
winit = { workspace = true }
-2.69 KB
Binary file not shown.
-1.92 MB
Loading

korangar/archive/data/font/README.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,20 @@ included in the distributed font file. Currently, we use "Noto Sans", which incl
55
and Greek alphabets. If you need to support a different alphabet, then you need to use a different font (for example
66
"Noto Sans Japanese"). Since Korangar uses a pre-assembled font map, that includes all glyphs of a font file in
77
a multichannel signed distance field (MSDF) representation, you need to create a such a font map and also a font map
8-
description file in the CSV format:
8+
description file in the CSV format. We support having fallback fonts, so if for example the primary fonts doesn't have
9+
a specific glyph, the fallback font is tried. Multiple fallback fonts are supported. The final height of all font maps
10+
combined must not exceed 8192 pixel.
911

1012
1. Use [msdfgen](https://github.com/Chlumsky/msdfgen) to create the font map image and the description file in the
11-
CSV format.
13+
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
14+
minimal size, so that all glyphs are included and no space is wasted. This is needed to properly merge multiple fonts
15+
into one font map.
1216
```sh
13-
msdf-atlas-gen -allglyphs -pxrange 6 -size 32 -yorigin top -type mtsdf -format png -font NotoSans.ttf -csv NotoSans.csv -imageout NotoSans.png
17+
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
1418
```
1519
2. Copy the original font file and also the generated PNG and CSV file into the `archive/data/font` folder.
16-
3. Optionally compress the CSV file with gzip:
20+
3. Compress the CSV file with gzip:
1721
```sh
1822
gzip NotoSans.csv
1923
```
20-
4. Update the `FONT_FILE_PATH`, `FONT_MAP_DESCRIPTION_FILE_PATH`, `FONT_MAP_FILE_PATH` and `FONT_FAMILY_NAME` in the
21-
`korangar/src/loaders/font/mod.rs` file.
24+
4. Update the DEFAULT_FONTS list inside the `korangar/src/interface/application.rs` file.

korangar/src/interface/application.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::marker::ConstParamTy;
2+
use std::sync::Arc;
23

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

26+
const DEFAULT_FONTS: &[&str] = &["NotoSans", "NotoSansKR"];
27+
2528
impl korangar_interface::application::ColorTrait for Color {
2629
fn is_transparent(&self) -> bool {
2730
const TRANSPARENCY_THRESHOLD: f32 = 0.999;
@@ -177,6 +180,7 @@ impl<const KIND: InternalThemeKind> PrototypeElement<InterfaceSettings> for Them
177180

178181
#[derive(Serialize, Deserialize)]
179182
struct InterfaceSettingsStorage {
183+
fonts: Vec<String>,
180184
menu_theme: String,
181185
main_theme: String,
182186
game_theme: String,
@@ -185,12 +189,14 @@ struct InterfaceSettingsStorage {
185189

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

193198
Self {
199+
fonts,
194200
main_theme,
195201
menu_theme,
196202
game_theme,
@@ -231,6 +237,7 @@ impl InterfaceSettingsStorage {
231237

232238
#[derive(PrototypeElement)]
233239
pub struct InterfaceSettings {
240+
fonts: Vec<String>,
234241
#[name("Main theme")]
235242
pub main_theme: ThemeSelector<{ InternalThemeKind::Main }>,
236243
#[name("Menu theme")]
@@ -245,19 +252,21 @@ pub struct InterfaceSettings {
245252
impl InterfaceSettings {
246253
pub fn load_or_default() -> Self {
247254
let InterfaceSettingsStorage {
255+
fonts,
248256
menu_theme,
249257
main_theme,
250258
game_theme,
251259
scaling,
252260
} = InterfaceSettingsStorage::load_or_default();
253261

254262
let themes = Themes::new(
255-
InterfaceTheme::new::<super::theme::DefaultMenu>(&menu_theme),
256-
InterfaceTheme::new::<super::theme::DefaultMain>(&main_theme),
263+
InterfaceTheme::new::<DefaultMenu>(&menu_theme),
264+
InterfaceTheme::new::<DefaultMain>(&main_theme),
257265
GameTheme::new(&menu_theme),
258266
);
259267

260268
Self {
269+
fonts,
261270
main_theme: ThemeSelector(main_theme),
262271
menu_theme: ThemeSelector(menu_theme),
263272
game_theme: ThemeSelector(game_theme),
@@ -278,6 +287,10 @@ impl InterfaceSettings {
278287
pub fn get_game_theme(&self) -> &GameTheme {
279288
&self.themes.game
280289
}
290+
291+
pub fn get_fonts(&self) -> &[String] {
292+
&self.fonts
293+
}
281294
}
282295

283296
impl InterfaceSettings {
@@ -317,7 +330,7 @@ impl Application for InterfaceSettings {
317330
type CustomEvent = UserEvent;
318331
type DropResource = PartialMove;
319332
type DropResult = Move;
320-
type FontLoader = std::rc::Rc<std::cell::RefCell<FontLoader>>;
333+
type FontLoader = Arc<FontLoader>;
321334
type FontSize = FontSize;
322335
type MouseInputMode = MouseInputMode;
323336
type PartialSize = PartialScreenSize;
@@ -343,6 +356,7 @@ impl Application for InterfaceSettings {
343356
impl Drop for InterfaceSettings {
344357
fn drop(&mut self) {
345358
InterfaceSettingsStorage {
359+
fonts: self.fonts.to_owned(),
346360
menu_theme: self.menu_theme.get_file().to_owned(),
347361
main_theme: self.main_theme.get_file().to_owned(),
348362
game_theme: self.game_theme.get_file().to_owned(),

korangar/src/interface/elements/miscellanious/chat/builder.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
use std::cell::RefCell;
2-
use std::rc::Rc;
1+
use std::sync::Arc;
32

43
use korangar_interface::builder::Unset;
54
use korangar_interface::state::PlainRemote;
@@ -33,12 +32,12 @@ impl<Font> ChatBuilder<Unset, Font> {
3332
}
3433

3534
impl<Messages> ChatBuilder<Messages, Unset> {
36-
pub fn with_font_loader(self, font_loader: Rc<RefCell<FontLoader>>) -> ChatBuilder<Messages, Rc<RefCell<FontLoader>>> {
35+
pub fn with_font_loader(self, font_loader: Arc<FontLoader>) -> ChatBuilder<Messages, Arc<FontLoader>> {
3736
ChatBuilder { font_loader, ..self }
3837
}
3938
}
4039

41-
impl ChatBuilder<PlainRemote<Vec<ChatMessage>>, Rc<RefCell<FontLoader>>> {
40+
impl ChatBuilder<PlainRemote<Vec<ChatMessage>>, Arc<FontLoader>> {
4241
/// Take the builder and turn it into a [`Chat`].
4342
///
4443
/// NOTE: This method is only available if

korangar/src/interface/elements/miscellanious/chat/mod.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
mod builder;
22

3-
use std::cell::RefCell;
4-
use std::rc::Rc;
3+
use std::sync::Arc;
54

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

2322
pub struct Chat {
2423
messages: PlainRemote<Vec<ChatMessage>>,
25-
font_loader: Rc<RefCell<FontLoader>>,
24+
font_loader: Arc<FontLoader>,
2625
state: ElementState<InterfaceSettings>,
2726
}
2827

@@ -55,7 +54,6 @@ impl Element<InterfaceSettings> for Chat {
5554
for message in self.messages.get().iter() {
5655
height += self
5756
.font_loader
58-
.borrow_mut()
5957
.get_text_dimensions(
6058
&message.text,
6159
theme.chat.font_size.get().scaled(application.get_scaling()),

korangar/src/interface/windows/generic/chat.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
use std::cell::RefCell;
2-
use std::rc::Rc;
1+
use std::sync::Arc;
32

43
use derive_new::new;
54
use korangar_interface::elements::{ButtonBuilder, ElementWrap, InputFieldBuilder, ScrollView};
@@ -26,7 +25,7 @@ pub struct ChatMessage {
2625
#[derive(new)]
2726
pub struct ChatWindow {
2827
messages: PlainRemote<Vec<ChatMessage>>,
29-
font_loader: Rc<RefCell<FontLoader>>,
28+
font_loader: Arc<FontLoader>,
3029
}
3130

3231
impl ChatWindow {
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
use std::io::{Cursor, Read};
2+
use std::sync::Arc;
3+
4+
use cosmic_text::fontdb::{Source, ID};
5+
use cosmic_text::FontSystem;
6+
use flate2::bufread::GzDecoder;
7+
use hashbrown::HashMap;
8+
use image::{ImageFormat, ImageReader, RgbaImage};
9+
#[cfg(feature = "debug")]
10+
use korangar_debug::logging::{print_debug, Colorize, Timer};
11+
use korangar_util::FileLoader;
12+
13+
use crate::loaders::font::font_map_descriptor::parse_glyphs;
14+
use crate::loaders::font::GlyphCoordinate;
15+
use crate::loaders::GameFileLoader;
16+
17+
const FONT_FOLDER_PATH: &str = "data\\font";
18+
19+
pub(crate) struct FontFile {
20+
pub(crate) ids: Vec<ID>,
21+
pub(crate) font_map: RgbaImage,
22+
pub(crate) glyphs: Arc<HashMap<u16, GlyphCoordinate>>,
23+
}
24+
25+
impl FontFile {
26+
pub(crate) fn new(name: &str, game_file_loader: &GameFileLoader, font_system: &mut FontSystem) -> Option<Self> {
27+
#[cfg(feature = "debug")]
28+
let timer = Timer::new_dynamic(format!("load font: {}", name.magenta()));
29+
30+
let font_base_path = format!("{}\\{}", FONT_FOLDER_PATH, name);
31+
let ttf_file_path = format!("{}.ttf", font_base_path);
32+
let map_file_path = format!("{}.png", font_base_path);
33+
let map_description_file_path = format!("{}.csv.gz", font_base_path);
34+
35+
let Ok(font_data) = game_file_loader.get(&ttf_file_path) else {
36+
#[cfg(feature = "debug")]
37+
print_debug!("[{}] failed to load font file '{}'", "error".red(), ttf_file_path.magenta());
38+
return None;
39+
};
40+
41+
let ids = font_system.db_mut().load_font_source(Source::Binary(Arc::new(font_data)));
42+
43+
let Ok(font_map_data) = game_file_loader.get(&map_file_path) else {
44+
#[cfg(feature = "debug")]
45+
print_debug!("[{}] failed to load font map file '{}'", "error".red(), map_file_path.magenta());
46+
return None;
47+
};
48+
49+
let font_map_reader = ImageReader::with_format(Cursor::new(font_map_data), ImageFormat::Png);
50+
51+
let Ok(font_map_decoder) = font_map_reader.decode() else {
52+
#[cfg(feature = "debug")]
53+
print_debug!("[{}] failed to decode font map '{}'", "error".red(), map_file_path.magenta());
54+
return None;
55+
};
56+
57+
let font_map_rgba_image = font_map_decoder.into_rgba8();
58+
let font_map_width = font_map_rgba_image.width();
59+
let font_map_height = font_map_rgba_image.height();
60+
61+
let Ok(mut font_description_data) = game_file_loader.get(&map_description_file_path) else {
62+
#[cfg(feature = "debug")]
63+
print_debug!(
64+
"[{}] failed to load font map description file '{}'",
65+
"error".red(),
66+
map_description_file_path.magenta()
67+
);
68+
return None;
69+
};
70+
71+
let mut decoder = GzDecoder::new(&font_description_data[..]);
72+
let mut data = Vec::with_capacity(font_description_data.len() * 2);
73+
74+
if let Err(_err) = decoder.read_to_end(&mut data) {
75+
#[cfg(feature = "debug")]
76+
print_debug!(
77+
"[{}] failed to decompress font map description file '{}': {:?}",
78+
"error".red(),
79+
map_description_file_path.magenta(),
80+
_err
81+
);
82+
return None;
83+
}
84+
85+
font_description_data = data;
86+
87+
let Ok(font_description_content) = String::from_utf8(font_description_data) else {
88+
#[cfg(feature = "debug")]
89+
print_debug!(
90+
"[{}] invalid UTF-8 text data found in font map description file '{}'",
91+
"error".red(),
92+
map_description_file_path.magenta()
93+
);
94+
return None;
95+
};
96+
97+
let glyphs = parse_glyphs(font_description_content, font_map_width, font_map_height);
98+
99+
#[cfg(feature = "debug")]
100+
timer.stop();
101+
102+
Some(Self {
103+
ids: Vec::from_iter(ids),
104+
font_map: font_map_rgba_image,
105+
glyphs: Arc::new(glyphs),
106+
})
107+
}
108+
}

korangar/src/loaders/font/font_map_descriptor.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,7 @@ struct Bounds {
2525
top: f64,
2626
}
2727

28-
pub(crate) fn parse_glyph_cache(
29-
font_description_content: String,
30-
font_map_width: u32,
31-
font_map_height: u32,
32-
) -> HashMap<u16, GlyphCoordinate> {
28+
pub(crate) fn parse_glyphs(font_description_content: String, font_map_width: u32, font_map_height: u32) -> HashMap<u16, GlyphCoordinate> {
3329
let font_map_width = font_map_width as f32;
3430
let font_map_height = font_map_height as f32;
3531

0 commit comments

Comments
 (0)