Skip to content

Commit

Permalink
Merge pull request #87 from kas-gui/work2
Browse files Browse the repository at this point in the history
Split font selector into resolver and fontdb::Database
  • Loading branch information
dhardy authored Sep 3, 2024
2 parents d88f73c + 13406e5 commit af1ac6d
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 216 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
toolchain: [stable]
include:
- os: ubuntu-latest
toolchain: "1.71.1"
toolchain: "1.80.0"
- os: ubuntu-latest
toolchain: beta

Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

Update dependencies:

- Rust (MSRV): 1.71.1 (#86)
- Rust (MSRV): 1.80.0 (#87)
- `fontdb`: 0.21.0 (#86)

## [0.6.0] — 2022-12-13
Expand Down
3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ keywords = ["text", "bidi", "shaping"]
categories = ["text-processing"]
repository = "https://github.com/kas-gui/kas-text"
exclude = ["design"]
rust-version = "1.71.1"
rust-version = "1.80.0"

[package.metadata.docs.rs]
# To build locally:
Expand Down Expand Up @@ -45,7 +45,6 @@ easy-cast = "0.5.0"
bitflags = "2.4.2"
fontdb = "0.21.0"
ttf-parser = "0.24.1"
lazy_static = "1.4.0"
smallvec = "1.6.1"
xi-unicode = "0.3.0"
unicode-bidi = "0.3.4"
Expand Down
147 changes: 95 additions & 52 deletions src/fonts/library.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,21 @@
#![allow(clippy::len_without_is_empty)]

use super::{selector::Database, FaceRef, FontSelector};
use super::{FaceRef, FontSelector, Resolver};
use crate::conv::{to_u32, to_usize};
use fontdb::Database;
use log::warn;
use std::collections::hash_map::{Entry, HashMap};
use std::path::{Path, PathBuf};
use std::sync::{RwLock, RwLockReadGuard};
use std::sync::{Arc, LazyLock, OnceLock, RwLock, RwLockReadGuard};
use thiserror::Error;
pub(crate) use ttf_parser::Face;

/// Font loading errors
#[derive(Error, Debug)]
enum FontError {
#[error("font DB not yet initialized")]
NotReady,
#[error("no matching font found")]
NotFound,
#[error("font load error")]
Expand Down Expand Up @@ -166,9 +170,9 @@ impl FontList {
/// This is the type of the global singleton accessible via the [`library()`]
/// function. Thread-safety is handled via internal locks.
pub struct FontLibrary {
db: RwLock<Database>,
resolver: RwLock<Resolver>,
// Font files loaded into memory. Safety: we assume that existing entries
// are never modified or removed (though the Vec is allowed to reallocate).
// are never modified or removed.
// Note: using std::pin::Pin does not help since u8 impls Unpin.
data: RwLock<HashMap<PathBuf, Box<[u8]>>>,
// Font faces defined over the above data (see safety note).
Expand All @@ -179,21 +183,68 @@ pub struct FontLibrary {

/// Font management
impl FontLibrary {
/// Get a reference to the font database
pub fn read_db(&self) -> RwLockReadGuard<Database> {
self.db.read().unwrap()
/// Adjust the font resolver
///
/// This method may only be called before [`FontLibrary::init`].
/// If called afterwards this will just return `None`.
pub fn adjust_resolver<F: FnOnce(&mut Resolver) -> T, T>(&self, f: F) -> Option<T> {
if DB.get().is_some() {
warn!("unable to update resolver after kas_text::fonts::library().init()");
return None;
}

Some(f(&mut self.resolver.write().unwrap()))
}

/// Initialize
///
/// This method constructs the [`fontdb::Database`], loads fonts
/// and resolves the default font (i.e. `FontId(0)`).
///
/// If a custom font loader is provided, this should load all desired fonts
/// (optionally including system fonts).
/// Otherwise, only system fonts will be loaded.
///
/// This *must* be called before any other font selection method, and before
/// querying any font-derived properties (such as text dimensions).
/// It is safe to call multiple times.
#[inline]
pub fn init(&self) -> Result<(), Box<dyn std::error::Error>> {
self.init_custom(|db| db.load_system_fonts())
}

/// Get mutable access to the font database
/// Initialize with custom fonts
///
/// This can be used to adjust font selection. Note that any changes only
/// affect *new* font selections, thus it is recommended only to adjust the
/// database before *any* fonts have been selected. No existing [`FaceId`]
/// or [`FontId`] will be affected by this; additionally any
/// [`FontSelector`] which has already been selected will continue to
/// resolve the existing [`FontId`] via the cache.
pub fn update_db<F: FnOnce(&mut Database) -> T, T>(&self, f: F) -> T {
f(&mut self.db.write().unwrap())
/// This method is an alternative to [`FontLibrary::init`], allowing custom
/// font loading.
///
/// The loader method must load all required fonts. It is called only if
/// initialization is not yet complete.
pub fn init_custom(
&self,
loader: impl FnOnce(&mut Database),
) -> Result<(), Box<dyn std::error::Error>> {
if DB.get().is_some() {
return Ok(());
}

let mut db = Arc::new(Database::new());
let dbm = Arc::make_mut(&mut db);
loader(dbm);

self.resolver.write().unwrap().init(dbm);

if let Ok(()) = DB.set(db) {
let id = self.select_font(&FontSelector::default())?;
debug_assert!(id == FontId::default());
}

Ok(())
}

/// Get a reference to the font resolver
pub fn resolver(&self) -> RwLockReadGuard<Resolver> {
self.resolver.read().unwrap()
}

/// Get the first face for a font
Expand Down Expand Up @@ -300,25 +351,6 @@ impl FontLibrary {
}
}

/// Select the default font
///
/// If the font database has not yet been initialized, it is initialized.
///
/// If `FontId(0)` has not been defined yet, this sets the default font.
///
/// This *must* be called (at least once) before any other font selection
/// method, and before querying any font-derived properties (such as text
/// dimensions).
#[inline]
pub fn select_default(&self) -> Result<(), Box<dyn std::error::Error>> {
self.db.write().unwrap().init();
if self.fonts.read().unwrap().fonts.is_empty() {
let id = self.select_font(&FontSelector::default())?;
debug_assert!(id == FontId::default());
}
Ok(())
}

/// Select a font
///
/// This method uses internal caching to enable fast look-ups of existing
Expand All @@ -337,7 +369,12 @@ impl FontLibrary {
drop(fonts);

let mut faces = Vec::new();
selector.select(&self.db.read().unwrap(), |source, index| {
let resolver = self.resolver.read().unwrap();
let Some(db) = DB.get() else {
return Err(Box::new(FontError::NotReady));
};

selector.select(&resolver, db, |source, index| {
Ok(faces.push(match source {
fontdb::Source::File(path) => self.load_path(path, index),
_ => unimplemented!("loading from source {:?}", source),
Expand Down Expand Up @@ -525,28 +562,34 @@ pub(crate) unsafe fn extend_lifetime<'b, T: ?Sized>(r: &'b T) -> &'static T {
std::mem::transmute::<&'b T, &'static T>(r)
}

// internals
impl FontLibrary {
// Private because: safety depends on instance(s) never being destructed.
fn new() -> Self {
FontLibrary {
db: RwLock::new(Database::new()),
data: Default::default(),
faces: Default::default(),
fonts: Default::default(),
}
}
}

lazy_static::lazy_static! {
static ref LIBRARY: FontLibrary = FontLibrary::new();
}
static LIBRARY: LazyLock<FontLibrary> = LazyLock::new(|| FontLibrary {
resolver: RwLock::new(Resolver::new()),
data: Default::default(),
faces: Default::default(),
fonts: Default::default(),
});

/// Access the [`FontLibrary`] singleton
pub fn library() -> &'static FontLibrary {
&LIBRARY
}

static DB: OnceLock<Arc<Database>> = OnceLock::new();

/// Access the font database
///
/// Returns `None` when called before [`FontLibrary::init`].
pub fn db() -> Option<&'static Database> {
DB.get().map(|arc| &**arc)
}

/// Get owning access the font database
///
/// Returns `None` when called before [`FontLibrary::init`].
pub fn clone_db() -> Option<Arc<Database>> {
DB.get().cloned()
}

fn calculate_hash<T: std::hash::Hash>(t: &T) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
Expand Down
14 changes: 4 additions & 10 deletions src/fonts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,12 @@
//! To make this work, the user of this library *must* load the default font
//! before all other fonts and before any operation requiring font metrics:
//! ```
//! if let Err(e) = kas_text::fonts::library().select_default() {
//! if let Err(e) = kas_text::fonts::library().init() {
//! panic!("Error loading font: {}", e);
//! }
//! // from now on, kas_text::fonts::FontId::default() identifies the default font
//! ```
//!
//! (It is not technically necessary to lead the first font with
//! [`FontLibrary::select_default`]; whichever font is loaded first has number 0.
//! If doing this, `select_default` must not be called at all.
//! It is harmless to attempt to load any font multiple times, whether with
//! `select_default` or another method.)
//!
//! ### `FaceId` vs `FontId`
//!
//! Why do both [`FaceId`] and [`FontId`] exist? Font fallbacks. A [`FontId`]
Expand Down Expand Up @@ -76,11 +70,11 @@ use std::sync::atomic::{AtomicBool, Ordering};
mod face;
mod families;
mod library;
mod selector;
mod resolver;

pub use face::{FaceRef, ScaledFaceRef};
pub use library::{library, FaceData, FaceId, FontId, FontLibrary, InvalidFontId};
pub use selector::*;
pub use library::{clone_db, db, library, FaceData, FaceId, FontId, FontLibrary, InvalidFontId};
pub use resolver::*;

impl From<GlyphId> for ttf_parser::GlyphId {
fn from(id: GlyphId) -> Self {
Expand Down
Loading

0 comments on commit af1ac6d

Please sign in to comment.