diff --git a/.gitignore b/.gitignore index 4d252295..3278f95d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ target/ # Korangar +korangar/archive/data/font/* korangar/client/ korangar/data.grf korangar/rdata.grf diff --git a/Cargo.lock b/Cargo.lock index 00528bde..e9a1b583 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1623,6 +1623,7 @@ dependencies = [ "ron", "serde", "spin_sleep", + "sys-locale", "walkdir", "wgpu", "winit", diff --git a/Cargo.toml b/Cargo.toml index 6be8ae86..38e6b211 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/korangar/Cargo.toml b/korangar/Cargo.toml index 189b0e21..e200621b 100644 --- a/korangar/Cargo.toml +++ b/korangar/Cargo.toml @@ -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 } diff --git a/korangar/archive/data/font/NotoSans.csv.gz b/korangar/archive/data/font/NotoSans.csv.gz index ba5ab010..12cb6341 100644 Binary files a/korangar/archive/data/font/NotoSans.csv.gz and b/korangar/archive/data/font/NotoSans.csv.gz differ diff --git a/korangar/archive/data/font/NotoSans.png b/korangar/archive/data/font/NotoSans.png index 1c4a2cad..12ff99b0 100644 Binary files a/korangar/archive/data/font/NotoSans.png and b/korangar/archive/data/font/NotoSans.png differ diff --git a/korangar/archive/data/font/README.md b/korangar/archive/data/font/README.md index ae03e3ae..7b259dfe 100644 --- a/korangar/archive/data/font/README.md +++ b/korangar/archive/data/font/README.md @@ -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. diff --git a/korangar/src/interface/application.rs b/korangar/src/interface/application.rs index 097187ee..5e3b8423 100644 --- a/korangar/src/interface/application.rs +++ b/korangar/src/interface/application.rs @@ -1,4 +1,5 @@ use std::marker::ConstParamTy; +use std::sync::Arc; #[cfg(feature = "debug")] use korangar_debug::logging::{print_debug, Colorize}; @@ -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; @@ -177,6 +180,7 @@ impl PrototypeElement for Them #[derive(Serialize, Deserialize)] struct InterfaceSettingsStorage { + fonts: Vec, menu_theme: String, main_theme: String, game_theme: String, @@ -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, @@ -231,6 +237,7 @@ impl InterfaceSettingsStorage { #[derive(PrototypeElement)] pub struct InterfaceSettings { + fonts: Vec, #[name("Main theme")] pub main_theme: ThemeSelector<{ InternalThemeKind::Main }>, #[name("Menu theme")] @@ -245,6 +252,7 @@ pub struct InterfaceSettings { impl InterfaceSettings { pub fn load_or_default() -> Self { let InterfaceSettingsStorage { + fonts, menu_theme, main_theme, game_theme, @@ -252,12 +260,13 @@ impl InterfaceSettings { } = InterfaceSettingsStorage::load_or_default(); let themes = Themes::new( - InterfaceTheme::new::(&menu_theme), - InterfaceTheme::new::(&main_theme), + InterfaceTheme::new::(&menu_theme), + InterfaceTheme::new::(&main_theme), GameTheme::new(&menu_theme), ); Self { + fonts, main_theme: ThemeSelector(main_theme), menu_theme: ThemeSelector(menu_theme), game_theme: ThemeSelector(game_theme), @@ -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 { @@ -317,7 +330,7 @@ impl Application for InterfaceSettings { type CustomEvent = UserEvent; type DropResource = PartialMove; type DropResult = Move; - type FontLoader = std::rc::Rc>; + type FontLoader = Arc; type FontSize = FontSize; type MouseInputMode = MouseInputMode; type PartialSize = PartialScreenSize; @@ -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(), diff --git a/korangar/src/interface/elements/miscellanious/chat/builder.rs b/korangar/src/interface/elements/miscellanious/chat/builder.rs index 717d81c0..1115e293 100644 --- a/korangar/src/interface/elements/miscellanious/chat/builder.rs +++ b/korangar/src/interface/elements/miscellanious/chat/builder.rs @@ -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; @@ -33,12 +32,12 @@ impl ChatBuilder { } impl ChatBuilder { - pub fn with_font_loader(self, font_loader: Rc>) -> ChatBuilder>> { + pub fn with_font_loader(self, font_loader: Arc) -> ChatBuilder> { ChatBuilder { font_loader, ..self } } } -impl ChatBuilder>, Rc>> { +impl ChatBuilder>, Arc> { /// Take the builder and turn it into a [`Chat`]. /// /// NOTE: This method is only available if diff --git a/korangar/src/interface/elements/miscellanious/chat/mod.rs b/korangar/src/interface/elements/miscellanious/chat/mod.rs index 0e1cb8bd..22534b52 100644 --- a/korangar/src/interface/elements/miscellanious/chat/mod.rs +++ b/korangar/src/interface/elements/miscellanious/chat/mod.rs @@ -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}; @@ -22,7 +21,7 @@ use crate::renderer::InterfaceRenderer; pub struct Chat { messages: PlainRemote>, - font_loader: Rc>, + font_loader: Arc, state: ElementState, } @@ -55,7 +54,6 @@ impl Element 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()), diff --git a/korangar/src/interface/windows/generic/chat.rs b/korangar/src/interface/windows/generic/chat.rs index c2a530c7..d0290bb7 100644 --- a/korangar/src/interface/windows/generic/chat.rs +++ b/korangar/src/interface/windows/generic/chat.rs @@ -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}; @@ -26,7 +25,7 @@ pub struct ChatMessage { #[derive(new)] pub struct ChatWindow { messages: PlainRemote>, - font_loader: Rc>, + font_loader: Arc, } impl ChatWindow { diff --git a/korangar/src/loaders/font/font_file.rs b/korangar/src/loaders/font/font_file.rs new file mode 100644 index 00000000..52f4c563 --- /dev/null +++ b/korangar/src/loaders/font/font_file.rs @@ -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, + pub(crate) font_map: RgbaImage, + pub(crate) glyphs: Arc>, +} + +impl FontFile { + pub(crate) fn new(name: &str, game_file_loader: &GameFileLoader, font_system: &mut FontSystem) -> Option { + #[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), + }) + } +} diff --git a/korangar/src/loaders/font/font_map_descriptor.rs b/korangar/src/loaders/font/font_map_descriptor.rs index 4d2b0cee..41459336 100644 --- a/korangar/src/loaders/font/font_map_descriptor.rs +++ b/korangar/src/loaders/font/font_map_descriptor.rs @@ -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 { +pub(crate) fn parse_glyphs(font_description_content: String, font_map_width: u32, font_map_height: u32) -> HashMap { let font_map_width = font_map_width as f32; let font_map_height = font_map_height as f32; diff --git a/korangar/src/loaders/font/mod.rs b/korangar/src/loaders/font/mod.rs index 2a630548..caa544c0 100644 --- a/korangar/src/loaders/font/mod.rs +++ b/korangar/src/loaders/font/mod.rs @@ -1,30 +1,29 @@ mod color_span_iterator; +mod font_file; mod font_map_descriptor; -use std::io::{Cursor, Read}; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use cgmath::{Point2, Vector2}; -use cosmic_text::{Attrs, Buffer, Family, FontSystem, Metrics, Shaping}; -use flate2::bufread::GzDecoder; +use cosmic_text::fontdb::ID; +use cosmic_text::{fontdb, Attrs, Buffer, Family, FontSystem, Metrics, Shaping}; use hashbrown::HashMap; -use image::{ImageFormat, ImageReader}; +use image::{imageops, ImageBuffer, Rgba, RgbaImage}; +#[cfg(feature = "debug")] +use korangar_debug::logging::print_debug; +#[cfg(feature = "debug")] +use korangar_debug::logging::Colorize; use korangar_interface::application::FontSizeTrait; use korangar_interface::elements::ElementDisplay; -use korangar_util::{FileLoader, Rectangle}; +use korangar_util::Rectangle; use serde::{Deserialize, Serialize}; use self::color_span_iterator::ColorSpanIterator; -use self::font_map_descriptor::parse_glyph_cache; use super::{GameFileLoader, TextureLoader}; -use crate::graphics::{Color, Texture}; +use crate::graphics::{Color, Texture, MAX_TEXTURE_SIZE}; use crate::interface::application::InterfaceSettings; use crate::interface::layout::{ArrayType, ScreenSize}; - -const FONT_FILE_PATH: &str = "data\\font\\NotoSans.ttf"; -const FONT_MAP_DESCRIPTION_FILE_PATH: &str = "data\\font\\NotoSans.csv.gz"; -const FONT_MAP_FILE_PATH: &str = "data\\font\\NotoSans.png"; -const FONT_FAMILY_NAME: &str = "Noto Sans"; +use crate::loaders::font::font_file::FontFile; #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[serde(transparent)] @@ -104,11 +103,6 @@ impl korangar_interface::application::ScalingTrait for Scaling { } } -pub struct TextLayout { - pub glyphs: Vec, - pub size: Vector2, -} - pub struct GlyphInstruction { pub position: Rectangle, pub texture_coordinate: Rectangle, @@ -125,45 +119,130 @@ pub(crate) struct GlyphCoordinate { } pub struct FontLoader { - font_system: FontSystem, + font_system: Mutex, + primary_font_family: String, font_map: Arc, - glyph_cache: HashMap, + glyph_cache: HashMap>>, } impl FontLoader { - pub fn new(game_file_loader: &GameFileLoader, texture_loader: &TextureLoader) -> Self { - let font_data = game_file_loader.get(FONT_FILE_PATH).unwrap(); - let mut font_system = FontSystem::new(); - font_system.db_mut().load_font_data(font_data); - - let font_map_data = game_file_loader.get(FONT_MAP_FILE_PATH).unwrap(); - let font_map_reader = ImageReader::with_format(Cursor::new(font_map_data), ImageFormat::Png); - let font_map_decoder = font_map_reader.decode().unwrap(); - let font_map_rgba_image = font_map_decoder.into_rgba8(); - let font_map = texture_loader.create_msdf("font map", font_map_rgba_image); - let font_map_size = font_map.get_size(); - - let mut font_description_data = game_file_loader.get(FONT_MAP_DESCRIPTION_FILE_PATH).unwrap(); - - if FONT_MAP_DESCRIPTION_FILE_PATH.ends_with(".gz") { - let mut decoder = GzDecoder::new(&font_description_data[..]); - let mut data = Vec::with_capacity(font_description_data.len() * 2); - decoder.read_to_end(&mut data).unwrap(); - font_description_data = data; - } + pub fn new(fonts: &[String], game_file_loader: &GameFileLoader, texture_loader: &TextureLoader) -> Self { + assert_ne!(fonts.len(), 0, "no font defined"); + + let mut font_system = FontSystem::new_with_locale_and_db(Self::system_locale(), fontdb::Database::new()); + let mut glyph_cache = HashMap::new(); + + let fonts: Vec = fonts + .iter() + .filter_map(|font_name| FontFile::new(font_name, game_file_loader, &mut font_system)) + .collect(); - let font_description_content = String::from_utf8(font_description_data).unwrap(); - let glyph_cache = parse_glyph_cache(font_description_content, font_map_size.width, font_map_size.height); + let primary_font_family = Self::extract_primary_font_family(&font_system, &fonts); + let font_map_image_data = Self::merge_font_maps(&mut glyph_cache, &mut font_system, fonts); + + let font_map = texture_loader.create_msdf("font map", font_map_image_data); Self { - font_system, + font_system: Mutex::new(font_system), + primary_font_family, font_map, glyph_cache, } } - pub fn get_text_dimensions(&mut self, text: &str, font_size: FontSize, line_height_scale: f32, available_width: f32) -> ScreenSize { - let TextLayout { size, .. } = self.get_text_layout(text, Color::BLACK, font_size, line_height_scale, available_width); + fn system_locale() -> String { + sys_locale::get_locale().unwrap_or_else(|| { + #[cfg(feature = "debug")] + print_debug!("[{}] failed to get system locale, falling back to en-US", "warning".yellow()); + "en-US".to_string() + }) + } + + fn extract_primary_font_family(font_system: &FontSystem, fonts: &[FontFile]) -> String { + let primary_font_id = fonts + .first() + .and_then(|font| font.ids.first()) + .copied() + .expect("no primary font ID found"); + + font_system + .db() + .face(primary_font_id) + .and_then(|face| face.families.first().map(|(family, _)| family.clone())) + .expect("primary font has no family name") + } + + fn merge_font_maps( + glyph_cache: &mut HashMap>>, + font_system: &mut FontSystem, + mut fonts: Vec, + ) -> ImageBuffer, Vec> { + if fonts.len() == 1 { + let FontFile { ids, font_map, glyphs } = fonts.drain(..).take(1).next().unwrap(); + + for &id in &ids { + glyph_cache.insert(id, glyphs.clone()); + } + font_system.cache_fonts(ids); + + font_map + } else { + let overall_height: u32 = fonts.iter().map(|font| font.font_map.height()).sum(); + + assert!( + overall_height <= MAX_TEXTURE_SIZE, + "aggregated font map is higher than max texture size" + ); + assert_ne!(overall_height, 0, "aggregated font map height is zero"); + + let mut font_map_image_data = RgbaImage::new(MAX_TEXTURE_SIZE, overall_height); + let mut start_height = 0; + + for font in fonts { + let FontFile { ids, font_map, glyphs } = font; + + let font_map_height = font_map.height() as f32; + + let adjusted_glyphs: Arc> = Arc::new( + glyphs + .iter() + .map(|(&index, &coordinate)| { + let mut new_coordinate = coordinate; + + let y_offset = start_height as f32 / overall_height as f32; + let scale_factor = font_map_height / overall_height as f32; + + new_coordinate.texture_coordinate = Rectangle::new( + Point2::new( + coordinate.texture_coordinate.min.x, + coordinate.texture_coordinate.min.y * scale_factor + y_offset, + ), + Point2::new( + coordinate.texture_coordinate.max.x, + coordinate.texture_coordinate.max.y * scale_factor + y_offset, + ), + ); + + (index, new_coordinate) + }) + .collect(), + ); + + for &id in &ids { + glyph_cache.insert(id, adjusted_glyphs.clone()); + } + font_system.cache_fonts(ids); + + imageops::replace(&mut font_map_image_data, &font_map, 0, start_height); + start_height += font_map_height as i64; + } + + font_map_image_data + } + } + + pub fn get_text_dimensions(&self, text: &str, font_size: FontSize, line_height_scale: f32, available_width: f32) -> ScreenSize { + let size = self.layout_text(text, Color::BLACK, font_size, line_height_scale, available_width, None); ScreenSize { width: size.x, @@ -175,43 +254,58 @@ impl FontLoader { // But that would need us to re-evaluate on how we render test in general. // We also don't really use the "line_height_scale", which would provide // an easy way to handle "line height". - pub fn get_text_layout( - &mut self, + /// Writes the text layout for the given text into the `glyphs` buffer and + /// returns the size of the text in pixels. + /// + /// Does not clear the glyph buffer before writing into it. + pub fn layout_text( + &self, text: &str, default_color: Color, font_size: FontSize, line_height_scale: f32, available_width: f32, - ) -> TextLayout { - let mut glyphs = Vec::with_capacity(text.len()); + mut glyphs: Option<&mut Vec>, + ) -> Vector2 { let mut text_width = 0f32; let mut text_height = 0f32; let metrics = Metrics::relative(font_size.0, line_height_scale); - let mut buffer = Buffer::new(&mut self.font_system, metrics); - let attributes = Attrs::new().family(Family::Name(FONT_FAMILY_NAME)); + let attributes = Attrs::new().family(Family::Name(&self.primary_font_family)); + + // We try to hold the mutex lock as short as possible. + let buffer = { + let mut font_system = self.font_system.lock().unwrap(); + let mut buffer = Buffer::new(&mut font_system, metrics); + + buffer.set_size(&mut font_system, Some(available_width), None); + buffer.set_rich_text( + &mut font_system, + ColorSpanIterator::new(text, default_color, attributes), + attributes, + Shaping::Advanced, + ); - buffer.set_size(&mut self.font_system, Some(available_width), None); - buffer.set_rich_text( - &mut self.font_system, - ColorSpanIterator::new(text, default_color, attributes), - attributes, - Shaping::Advanced, - ); + buffer + }; for run in buffer.layout_runs() { text_width = text_width.max(run.line_w); text_height += run.line_height; + let Some(glyphs) = glyphs.as_mut() else { continue }; + for layout_glyph in run.glyphs.iter() { let physical_glyph = layout_glyph.physical((0.0, 0.0), 1.0); - let Some(glyph_coordinate) = self.glyph_cache.get(&layout_glyph.glyph_id).copied().map(|mut glyph| { - glyph.width *= font_size.0; - glyph.height *= font_size.0; - glyph.offset_left *= font_size.0; - glyph.offset_top *= font_size.0; - glyph + let Some(glyph_coordinate) = self.glyph_cache.get(&layout_glyph.font_id).and_then(|font| { + font.get(&layout_glyph.glyph_id).copied().map(|mut glyph| { + glyph.width *= font_size.0; + glyph.height *= font_size.0; + glyph.offset_left *= font_size.0; + glyph.offset_top *= font_size.0; + glyph + }) }) else { continue; }; @@ -232,10 +326,7 @@ impl FontLoader { } } - TextLayout { - glyphs, - size: Vector2::new(text_width, text_height), - } + Vector2::new(text_width, text_height) } /// The texture of the static font map. @@ -244,8 +335,8 @@ impl FontLoader { } } -impl korangar_interface::application::FontLoaderTrait for std::rc::Rc> { +impl korangar_interface::application::FontLoaderTrait for Arc { fn get_text_dimensions(&self, text: &str, font_size: FontSize, available_width: f32) -> ScreenSize { - self.borrow_mut().get_text_dimensions(text, font_size, 1.0, available_width) + FontLoader::get_text_dimensions(&self, text, font_size, 1.0, available_width) } } diff --git a/korangar/src/loaders/mod.rs b/korangar/src/loaders/mod.rs index 7c8c25c6..190b0781 100644 --- a/korangar/src/loaders/mod.rs +++ b/korangar/src/loaders/mod.rs @@ -16,7 +16,7 @@ mod texture; pub use self::action::*; pub use self::animation::*; pub use self::effect::EffectLoader; -pub use self::font::{FontLoader, FontSize, GlyphInstruction, Scaling, TextLayout}; +pub use self::font::{FontLoader, FontSize, GlyphInstruction, Scaling}; pub use self::gamefile::*; pub use self::map::{MapLoader, MAP_TILE_SIZE}; pub use self::model::*; diff --git a/korangar/src/main.rs b/korangar/src/main.rs index 8160f871..54e582c6 100644 --- a/korangar/src/main.rs +++ b/korangar/src/main.rs @@ -35,10 +35,8 @@ mod settings; mod system; mod world; -use std::cell::RefCell; use std::io::Cursor; use std::net::{SocketAddr, ToSocketAddrs}; -use std::rc::Rc; use std::sync::atomic::AtomicU64; use std::sync::Arc; @@ -163,7 +161,7 @@ struct Client { action_loader: Arc, async_loader: Arc, effect_loader: Arc, - font_loader: Rc>, + font_loader: Arc, sprite_loader: Arc, texture_loader: Arc, library: Library, @@ -278,6 +276,7 @@ impl Client { let picker_value = Arc::new(AtomicU64::new(0)); let input_system = InputSystem::new(picker_value.clone()); let graphics_settings = PlainTrackedState::new(GraphicsSettings::new()); + let application = InterfaceSettings::load_or_default(); let lighting_mode = graphics_settings.mapped(|settings| &settings.lighting_mode).new_remote(); let vsync = graphics_settings.mapped(|settings| &settings.vsync).new_remote(); @@ -373,7 +372,7 @@ impl Client { let model_loader = Arc::new(ModelLoader::new(game_file_loader.clone())); let texture_loader = Arc::new(TextureLoader::new(device.clone(), queue.clone(), game_file_loader.clone())); - let font_loader = Rc::new(RefCell::new(FontLoader::new(&game_file_loader, &texture_loader))); + let font_loader = Arc::new(FontLoader::new(application.get_fonts(), &game_file_loader, &texture_loader)); let map_loader = Arc::new(MapLoader::new( device.clone(), queue.clone(), @@ -463,7 +462,6 @@ impl Client { }); time_phase!("initialize interface", { - let application = InterfaceSettings::load_or_default(); let mut interface = Interface::new(INITIAL_SCREEN_SIZE); let mut focus_state = FocusState::default(); let mouse_cursor = MouseCursor::new(&sprite_loader, &action_loader); @@ -2309,7 +2307,6 @@ impl Client { let bottom_layer_instructions = self.bottom_interface_renderer.get_instructions(); let middle_layer_instructions = self.middle_interface_renderer.get_instructions(); let top_layer_instructions = self.top_interface_renderer.get_instructions(); - let font_loader = self.font_loader.borrow(); let render_instruction = RenderInstruction { clear_interface, @@ -2349,7 +2346,7 @@ impl Client { effects: self.effect_renderer.get_instructions(), water: water_instruction, map_picker_tile_vertex_buffer: Some(map.get_tile_picker_vertex_buffer()), - font_map_texture: Some(font_loader.get_font_map()), + font_map_texture: Some(self.font_loader.get_font_map()), #[cfg(feature = "debug")] render_settings: *self.render_settings.get(), #[cfg(feature = "debug")] diff --git a/korangar/src/renderer/game_interface.rs b/korangar/src/renderer/game_interface.rs index 9f8ac6ae..99b3a30e 100644 --- a/korangar/src/renderer/game_interface.rs +++ b/korangar/src/renderer/game_interface.rs @@ -1,5 +1,4 @@ use std::cell::{Ref, RefCell}; -use std::rc::Rc; use std::sync::Arc; #[cfg(feature = "debug")] @@ -9,7 +8,7 @@ use korangar_interface::application::FontSizeTraitExt; use crate::graphics::{Color, RectangleInstruction, Texture}; use crate::interface::layout::{ScreenClip, ScreenPosition, ScreenSize}; -use crate::loaders::{FontLoader, FontSize, GlyphInstruction, Scaling, TextLayout}; +use crate::loaders::{FontLoader, FontSize, GlyphInstruction, Scaling}; #[cfg(feature = "debug")] use crate::loaders::{ImageType, TextureLoader}; #[cfg(feature = "debug")] @@ -31,7 +30,8 @@ pub enum AlignHorizontal { /// the health bars). pub struct GameInterfaceRenderer { instructions: RefCell>, - font_loader: Rc>, + glyphs: RefCell>, + font_loader: Arc, window_size: ScreenSize, scaling: Scaling, #[cfg(feature = "debug")] @@ -97,10 +97,11 @@ impl GameInterfaceRenderer { pub fn new( window_size: ScreenSize, scaling: Scaling, - font_loader: Rc>, + font_loader: Arc, #[cfg(feature = "debug")] texture_loader: &TextureLoader, ) -> Self { let instructions = RefCell::new(Vec::new()); + let glyphs = RefCell::new(Vec::new()); #[cfg(feature = "debug")] let object_marker_texture = texture_loader.get_or_load("marker_object.png", ImageType::Sdf).unwrap(); @@ -117,6 +118,7 @@ impl GameInterfaceRenderer { Self { instructions, + glyphs, font_loader, window_size, scaling, @@ -138,7 +140,8 @@ impl GameInterfaceRenderer { pub fn from_renderer(other: &Self) -> Self { Self { instructions: RefCell::new(Vec::default()), - font_loader: Rc::clone(&other.font_loader), + glyphs: RefCell::new(Vec::default()), + font_loader: Arc::clone(&other.font_loader), window_size: other.window_size, scaling: other.scaling, #[cfg(feature = "debug")] @@ -182,7 +185,11 @@ impl GameInterfaceRenderer { ) { let font_size = font_size.scaled(self.scaling); - let TextLayout { glyphs, size } = self.font_loader.borrow_mut().get_text_layout(text, color, font_size, 1.0, f32::MAX); + let mut glyphs = self.glyphs.borrow_mut(); + + let size = self + .font_loader + .layout_text(text, color, font_size, 1.0, f32::MAX, Some(&mut glyphs)); let horizontal_offset = match align_horizontal { AlignHorizontal::Left => 0.0, @@ -191,7 +198,7 @@ impl GameInterfaceRenderer { let mut instructions = self.instructions.borrow_mut(); - glyphs.iter().for_each( + glyphs.drain(..).for_each( |GlyphInstruction { position, texture_coordinate, @@ -213,7 +220,7 @@ impl GameInterfaceRenderer { instructions.push(RectangleInstruction::Text { screen_position, screen_size, - color: *color, + color, texture_position, texture_size, }); diff --git a/korangar/src/renderer/interface.rs b/korangar/src/renderer/interface.rs index bb54a5ce..72728743 100644 --- a/korangar/src/renderer/interface.rs +++ b/korangar/src/renderer/interface.rs @@ -1,5 +1,4 @@ use std::cell::{Ref, RefCell}; -use std::rc::Rc; use std::sync::Arc; use cgmath::EuclideanSpace; @@ -8,13 +7,14 @@ use korangar_interface::application::Application; use crate::graphics::{Color, InterfaceRectangleInstruction, Texture}; use crate::interface::application::InterfaceSettings; use crate::interface::layout::{CornerRadius, ScreenClip, ScreenPosition, ScreenSize}; -use crate::loaders::{FontLoader, GlyphInstruction, ImageType, TextLayout, TextureLoader}; +use crate::loaders::{FontLoader, GlyphInstruction, ImageType, TextureLoader}; use crate::renderer::SpriteRenderer; /// Renders the interface provided by 'korangar_interface'. pub struct InterfaceRenderer { instructions: RefCell>, - font_loader: Rc>, + glyphs: RefCell>, + font_loader: Arc, filled_box_texture: Arc, unfilled_box_texture: Arc, expanded_arrow_texture: Arc, @@ -27,11 +27,12 @@ pub struct InterfaceRenderer { impl InterfaceRenderer { pub fn new( window_size: ScreenSize, - font_loader: Rc>, + font_loader: Arc, texture_loader: &TextureLoader, high_quality_interface: bool, ) -> Self { let instructions = RefCell::new(Vec::default()); + let glyphs = RefCell::new(Vec::default()); let filled_box_texture = texture_loader.get_or_load("filled_box.png", ImageType::Sdf).unwrap(); let unfilled_box_texture = texture_loader.get_or_load("unfilled_box.png", ImageType::Sdf).unwrap(); @@ -42,6 +43,7 @@ impl InterfaceRenderer { Self { instructions, + glyphs, font_loader, filled_box_texture, unfilled_box_texture, @@ -94,10 +96,7 @@ impl korangar_interface::application::InterfaceRenderer for I available_width *= 2.0; } - let mut size = self - .font_loader - .borrow_mut() - .get_text_dimensions(text, font_size, 1.0, available_width); + let mut size = self.font_loader.get_text_dimensions(text, font_size, 1.0, available_width); if self.high_quality_interface { size = size / 2.0; @@ -146,12 +145,20 @@ impl korangar_interface::application::InterfaceRenderer for I font_size = font_size * 2.0; } - let TextLayout { glyphs, mut size, .. } = - self.font_loader - .borrow_mut() - .get_text_layout(text, color, font_size, 1.0, screen_clip.right - text_position.left); + let mut glyphs = self.glyphs.borrow_mut(); - glyphs.iter().for_each( + let mut size = self.font_loader.layout_text( + text, + color, + font_size, + 1.0, + screen_clip.right - text_position.left, + Some(&mut glyphs), + ); + + let mut instructions = self.instructions.borrow_mut(); + + glyphs.drain(..).for_each( |GlyphInstruction { position, texture_coordinate, @@ -170,13 +177,13 @@ impl korangar_interface::application::InterfaceRenderer for I let texture_position = texture_coordinate.min.to_vec(); let texture_size = texture_coordinate.max - texture_coordinate.min; - self.instructions.borrow_mut().push(InterfaceRectangleInstruction::Text { + instructions.push(InterfaceRectangleInstruction::Text { screen_position, screen_size, screen_clip, texture_position, texture_size, - color: *color, + color, }); }, );