Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split font selector into resolver and fontdb::Database #87

Merged
merged 6 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading