diff --git a/Cargo.toml b/Cargo.toml index d9734134e4..84c950fdf1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,8 @@ unicode-linebreak = "0.1.4" unicode-script = "0.5.5" unicode-segmentation = "1.10.0" rangemap = "1.2.0" +hashbrown = { version = "0.14.0", default-features = false } +rustc-hash = { version = "1.1.0", default-features = false } [dependencies.unicode-bidi] version = "0.3.8" diff --git a/src/font/system.rs b/src/font/system.rs new file mode 100644 index 0000000000..1152cd24c1 --- /dev/null +++ b/src/font/system.rs @@ -0,0 +1,207 @@ +use crate::{Attrs, AttrsOwned, Font}; +use alloc::string::String; +use alloc::sync::Arc; +use alloc::vec::Vec; +use core::fmt; +use core::hash::BuildHasherDefault; +use core::ops::{Deref, DerefMut}; + +type HashMap = hashbrown::HashMap>; + +// re-export fontdb and rustybuzz +pub use fontdb; +pub use rustybuzz; + +/// Access to the system fonts. +pub struct FontSystem { + /// The locale of the system. + locale: String, + + /// The underlying font database. + db: fontdb::Database, + + /// Cache for loaded fonts from the database. + font_cache: HashMap>>, + + /// Cache for font matches. + font_matches_cache: HashMap>>, +} + +impl fmt::Debug for FontSystem { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("FontSystem") + .field("locale", &self.locale) + .field("db", &self.db) + .finish() + } +} + +impl FontSystem { + /// Create a new [`FontSystem`], that allows access to any installed system fonts + /// + /// # Timing + /// + /// This function takes some time to run. On the release build, it can take up to a second, + /// while debug builds can take up to ten times longer. For this reason, it should only be + /// called once, and the resulting [`FontSystem`] should be shared. + pub fn new() -> Self { + Self::new_with_fonts(core::iter::empty()) + } + + /// Create a new [`FontSystem`] with a pre-specified set of fonts. + pub fn new_with_fonts(fonts: impl IntoIterator) -> Self { + let locale = Self::get_locale(); + log::debug!("Locale: {}", locale); + + let mut db = fontdb::Database::new(); + Self::load_fonts(&mut db, fonts.into_iter()); + + //TODO: configurable default fonts + db.set_monospace_family("Fira Mono"); + db.set_sans_serif_family("Fira Sans"); + db.set_serif_family("DejaVu Serif"); + + Self::new_with_locale_and_db(locale, db) + } + + /// Create a new [`FontSystem`] with a pre-specified locale and font database. + pub fn new_with_locale_and_db(locale: String, db: fontdb::Database) -> Self { + Self { + locale, + db, + font_cache: HashMap::default(), + font_matches_cache: HashMap::default(), + } + } + + /// Get the locale. + pub fn locale(&self) -> &str { + &self.locale + } + + /// Get the database. + pub fn db(&self) -> &fontdb::Database { + &self.db + } + + /// Get a mutable reference to the database. + pub fn db_mut(&mut self) -> &mut fontdb::Database { + self.font_matches_cache.clear(); + &mut self.db + } + + /// Consume this [`FontSystem`] and return the locale and database. + pub fn into_locale_and_db(self) -> (String, fontdb::Database) { + (self.locale, self.db) + } + + /// Get a font by its ID. + pub fn get_font(&mut self, id: fontdb::ID) -> Option> { + self.font_cache + .entry(id) + .or_insert_with(|| { + #[cfg(feature = "std")] + unsafe { + self.db.make_shared_face_data(id); + } + let face = self.db.face(id)?; + match Font::new(face) { + Some(font) => Some(Arc::new(font)), + None => { + log::warn!("failed to load font '{}'", face.post_script_name); + None + } + } + }) + .clone() + } + + pub fn get_font_matches(&mut self, attrs: Attrs<'_>) -> Arc> { + self.font_matches_cache + //TODO: do not create AttrsOwned unless entry does not already exist + .entry(AttrsOwned::new(attrs)) + .or_insert_with(|| { + #[cfg(all(feature = "std", not(target_arch = "wasm32")))] + let now = std::time::Instant::now(); + + let ids = self + .db + .faces() + .filter(|face| attrs.matches(face)) + .map(|face| face.id) + .collect::>(); + + #[cfg(all(feature = "std", not(target_arch = "wasm32")))] + { + let elapsed = now.elapsed(); + log::debug!("font matches for {:?} in {:?}", attrs, elapsed); + } + + Arc::new(ids) + }) + .clone() + } + + #[cfg(feature = "std")] + fn get_locale() -> String { + sys_locale::get_locale().unwrap_or_else(|| { + log::warn!("failed to get system locale, falling back to en-US"); + String::from("en-US") + }) + } + + #[cfg(not(feature = "std"))] + fn get_locale() -> String { + String::from("en-US") + } + + #[cfg(feature = "std")] + fn load_fonts(db: &mut fontdb::Database, fonts: impl Iterator) { + #[cfg(not(target_arch = "wasm32"))] + let now = std::time::Instant::now(); + + #[cfg(target_os = "redox")] + db.load_fonts_dir("/ui/fonts"); + + db.load_system_fonts(); + + for source in fonts { + db.load_font_source(source); + } + + #[cfg(not(target_arch = "wasm32"))] + log::info!( + "Parsed {} font faces in {}ms.", + db.len(), + now.elapsed().as_millis() + ); + } + + #[cfg(not(feature = "std"))] + fn load_fonts(db: &mut fontdb::Database, fonts: impl Iterator) { + for source in fonts { + db.load_font_source(source); + } + } +} + +/// A value borrowed together with an [`FontSystem`] +#[derive(Debug)] +pub struct BorrowedWithFontSystem<'a, T> { + pub(crate) inner: &'a mut T, + pub(crate) font_system: &'a mut FontSystem, +} + +impl<'a, T> Deref for BorrowedWithFontSystem<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.inner + } +} + +impl<'a, T> DerefMut for BorrowedWithFontSystem<'a, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.inner + } +} diff --git a/src/font/system/mod.rs b/src/font/system/mod.rs deleted file mode 100644 index 666df05fa6..0000000000 --- a/src/font/system/mod.rs +++ /dev/null @@ -1,36 +0,0 @@ -use core::ops::{Deref, DerefMut}; - -#[cfg(not(feature = "std"))] -pub use self::no_std::*; -#[cfg(not(feature = "std"))] -mod no_std; - -#[cfg(feature = "std")] -pub use self::std::*; -#[cfg(feature = "std")] -mod std; - -// re-export fontdb and rustybuzz -pub use fontdb; -pub use rustybuzz; - -/// A value borrowed together with an [`FontSystem`] -#[derive(Debug)] -pub struct BorrowedWithFontSystem<'a, T> { - pub(crate) inner: &'a mut T, - pub(crate) font_system: &'a mut FontSystem, -} - -impl<'a, T> Deref for BorrowedWithFontSystem<'a, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - self.inner - } -} - -impl<'a, T> DerefMut for BorrowedWithFontSystem<'a, T> { - fn deref_mut(&mut self) -> &mut Self::Target { - self.inner - } -} diff --git a/src/font/system/no_std.rs b/src/font/system/no_std.rs deleted file mode 100644 index af0f00032d..0000000000 --- a/src/font/system/no_std.rs +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use alloc::{ - string::{String, ToString}, - sync::Arc, - vec::Vec, -}; - -use crate::{Attrs, Font}; - -/// Access system fonts -#[derive(Debug)] -pub struct FontSystem { - locale: String, - db: fontdb::Database, -} - -impl FontSystem { - pub fn new() -> Self { - let locale = "en-US".to_string(); - - let mut db = fontdb::Database::new(); - { - db.set_monospace_family("Fira Mono"); - db.set_sans_serif_family("Fira Sans"); - db.set_serif_family("DejaVu Serif"); - } - - Self { locale, db } - } - - pub fn new_with_locale_and_db(locale: String, db: fontdb::Database) -> Self { - Self { locale, db } - } - - pub fn locale(&self) -> &str { - &self.locale - } - - pub fn db(&self) -> &fontdb::Database { - &self.db - } - - pub fn db_mut(&mut self) -> &mut fontdb::Database { - &mut self.db - } - - pub fn get_font(&self, id: fontdb::ID) -> Option> { - get_font(&self.db, id) - } - - pub fn get_font_matches(&mut self, attrs: Attrs) -> Arc> { - let ids = self - .db - .faces() - .filter(|face| attrs.matches(face)) - .map(|face| face.id) - .collect::>(); - - Arc::new(ids) - } -} - -fn get_font(db: &fontdb::Database, id: fontdb::ID) -> Option> { - let face = db.face(id)?; - match Font::new(face) { - Some(font) => Some(Arc::new(font)), - None => { - log::warn!("failed to load font '{}'", face.post_script_name); - None - } - } -} diff --git a/src/font/system/std.rs b/src/font/system/std.rs deleted file mode 100644 index 4a355c0179..0000000000 --- a/src/font/system/std.rs +++ /dev/null @@ -1,144 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use std::{collections::HashMap, sync::Arc}; - -use crate::{Attrs, AttrsOwned, Font}; - -/// Access system fonts -#[derive(Debug)] -pub struct FontSystem { - locale: String, - db: fontdb::Database, - font_cache: HashMap>>, - font_matches_cache: HashMap>>, -} - -impl FontSystem { - /// Create a new [`FontSystem`], that allows access to any installed system fonts - /// - /// # Timing - /// - /// This function takes some time to run. On the release build, it can take up to a second, - /// while debug builds can take up to ten times longer. For this reason, it should only be - /// called once, and the resulting [`FontSystem`] should be shared. - pub fn new() -> Self { - Self::new_with_fonts(std::iter::empty()) - } - - pub fn new_with_fonts(fonts: impl Iterator) -> Self { - let locale = sys_locale::get_locale().unwrap_or_else(|| { - log::warn!("failed to get system locale, falling back to en-US"); - String::from("en-US") - }); - log::debug!("Locale: {}", locale); - - let mut db = fontdb::Database::new(); - { - #[cfg(not(target_arch = "wasm32"))] - let now = std::time::Instant::now(); - - #[cfg(target_os = "redox")] - db.load_fonts_dir("/ui/fonts"); - - db.load_system_fonts(); - - for source in fonts { - db.load_font_source(source); - } - - //TODO: configurable default fonts - db.set_monospace_family("Fira Mono"); - db.set_sans_serif_family("Fira Sans"); - db.set_serif_family("DejaVu Serif"); - - #[cfg(not(target_arch = "wasm32"))] - log::info!( - "Parsed {} font faces in {}ms.", - db.len(), - now.elapsed().as_millis() - ); - } - - Self::new_with_locale_and_db(locale, db) - } - - /// Create a new [`FontSystem`], manually specifying the current locale and font database. - pub fn new_with_locale_and_db(locale: String, db: fontdb::Database) -> Self { - Self { - locale, - db, - font_cache: HashMap::new(), - font_matches_cache: HashMap::new(), - } - } - - pub fn locale(&self) -> &str { - &self.locale - } - - pub fn db(&self) -> &fontdb::Database { - &self.db - } - - pub fn db_mut(&mut self) -> &mut fontdb::Database { - self.font_matches_cache.clear(); - &mut self.db - } - - pub fn into_locale_and_db(self) -> (String, fontdb::Database) { - (self.locale, self.db) - } - - pub fn get_font(&mut self, id: fontdb::ID) -> Option> { - get_font(&mut self.font_cache, &mut self.db, id) - } - - pub fn get_font_matches(&mut self, attrs: Attrs) -> Arc> { - self.font_matches_cache - //TODO: do not create AttrsOwned unless entry does not already exist - .entry(AttrsOwned::new(attrs)) - .or_insert_with(|| { - #[cfg(not(target_arch = "wasm32"))] - let now = std::time::Instant::now(); - - let ids = self - .db - .faces() - .filter(|face| attrs.matches(face)) - .map(|face| face.id) - .collect::>(); - - #[cfg(not(target_arch = "wasm32"))] - { - let elapsed = now.elapsed(); - log::debug!("font matches for {:?} in {:?}", attrs, elapsed); - } - - Arc::new(ids) - }) - .clone() - } -} - -fn get_font( - font_cache: &mut HashMap>>, - db: &mut fontdb::Database, - id: fontdb::ID, -) -> Option> { - font_cache - .entry(id) - .or_insert_with(|| { - unsafe { - db.make_shared_face_data(id); - } - let face = db.face(id)?; - match Font::new(face) { - Some(font) => Some(Arc::new(font)), - None => { - log::warn!("failed to load font '{}'", face.post_script_name); - None - } - } - }) - .clone() -}