diff --git a/Cargo.toml b/Cargo.toml index 6663428..a63e195 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,3 +63,6 @@ type_complexity = "allow" unit_arg = "allow" needless_lifetimes = "allow" neg_cmp_op_on_partial_ord = "allow" + +[patch.crates-io.fontique] +path = "../../text/parley/fontique" diff --git a/src/display/text_runs.rs b/src/display/text_runs.rs index 19c718c..5bc1ea7 100644 --- a/src/display/text_runs.rs +++ b/src/display/text_runs.rs @@ -12,6 +12,7 @@ use crate::conv::{to_u32, to_usize}; use crate::fonts::{self, FontSelector, NoFontMatch}; use crate::format::FormattableText; use crate::{script_to_fontique, shaper, Direction}; +use fontique::UnicodeRange; use swash::text::cluster::Boundary; use swash::text::LineBreak as LB; use unicode_bidi::{BidiClass, BidiInfo, LTR_LEVEL, RTL_LEVEL}; @@ -168,10 +169,13 @@ impl TextDisplay { } } + let script = script_to_fontique(props.script()); if input.script == UNKNOWN_SCRIPT && props.script().is_real() { - input.script = script_to_fontique(props.script()); + input.script = script; } + let unicode_range = UnicodeRange::find(c as u32); + let opt_last_face = if matches!( classes[pos], BidiClass::L | BidiClass::R | BidiClass::AL | BidiClass::EN | BidiClass::AN @@ -180,12 +184,11 @@ impl TextDisplay { } else { face_id }; - let font_id = fonts.select_font(&font, input.script)?; + let font_id = fonts.select_font(&font, script, unicode_range)?; let new_face_id = fonts .face_for_char(font_id, opt_last_face, c) - .expect("invalid FontId") - .or(face_id); - let font_break = face_id.is_some() && new_face_id != face_id; + .expect("invalid FontId"); + let font_break = face_id.is_some() && Some(new_face_id) != face_id; if hard_break || control_break || bidi_break || font_break { // TODO: sometimes this results in empty runs immediately @@ -201,7 +204,7 @@ impl TextDisplay { _ => RunSpecial::NoBreak, }; - let face = face_id.expect("have a set face_id"); + let face = face_id.unwrap_or(new_face_id); self.runs .push(shaper::shape(input, range, face, breaks, special)); @@ -218,10 +221,15 @@ impl TextDisplay { last_is_control = is_control; last_is_htab = is_htab; - face_id = new_face_id; + face_id = Some(new_face_id); input.dpem = dpem; } + let Some(face) = face_id else { + // Text is empty + return Ok(()); + }; + debug_assert!(analyzer.next().is_none()); let hard_break = last_props .map(|props| match props.line_break() { @@ -241,29 +249,16 @@ impl TextDisplay { _ => RunSpecial::None, }; - let font_id = fonts.select_font(&font, input.script)?; - if let Some(id) = face_id { - if !fonts.contains_face(font_id, id).expect("invalid FontId") { - face_id = None; - } - } - let face_id = - face_id.unwrap_or_else(|| fonts.first_face_for(font_id).expect("invalid FontId")); self.runs - .push(shaper::shape(input, range, face_id, breaks, special)); + .push(shaper::shape(input, range, face, breaks, special)); // Following a hard break we have an implied empty line. if hard_break { let range = (text.len()..text.len()).into(); input.level = default_para_level.unwrap_or(LTR_LEVEL); breaks = Default::default(); - self.runs.push(shaper::shape( - input, - range, - face_id, - breaks, - RunSpecial::None, - )); + self.runs + .push(shaper::shape(input, range, face, breaks, RunSpecial::None)); } /* @@ -271,8 +266,8 @@ impl TextDisplay { for run in &self.runs { let slice = &text[run.range]; print!( - "\t{:?}, text[{}..{}]: '{}', ", - run.level, run.range.start, run.range.end, slice + "\t{:?}, text[{}..{}]: '{}', {:?}, ", + run.level, run.range.start, run.range.end, slice, run.script ); match run.special { RunSpecial::None => (), diff --git a/src/fonts/face.rs b/src/fonts/face.rs index 8b00c1a..b055456 100644 --- a/src/fonts/face.rs +++ b/src/fonts/face.rs @@ -67,7 +67,7 @@ pub struct ScaledFaceRef<'a>(&'a Face<'a>, DPU); impl<'a> ScaledFaceRef<'a> { /// Unscaled face #[inline] - pub fn face(&self) -> FaceRef { + pub fn face(&self) -> FaceRef<'_> { FaceRef(self.0) } diff --git a/src/fonts/library.rs b/src/fonts/library.rs index c0ddbb7..fd9e266 100644 --- a/src/fonts/library.rs +++ b/src/fonts/library.rs @@ -9,7 +9,7 @@ use super::{FaceRef, FontSelector, Resolver}; use crate::conv::{to_u32, to_usize}; -use fontique::{Blob, QueryStatus, Script, Synthesis}; +use fontique::{Blob, QueryStatus, Script, Synthesis, UnicodeRange}; use std::collections::hash_map::{Entry, HashMap}; use std::sync::{LazyLock, Mutex, MutexGuard, RwLock}; use thiserror::Error; @@ -222,7 +222,7 @@ pub struct FontLibrary { /// Font management impl FontLibrary { /// Get a reference to the font resolver - pub fn resolver(&self) -> MutexGuard { + pub fn resolver(&self) -> MutexGuard<'_, Resolver> { self.resolver.lock().unwrap() } @@ -244,7 +244,7 @@ impl FontLibrary { /// /// This is a wrapper around [`FontLibrary::first_face_for`] and [`FontLibrary::get_face`]. #[inline] - pub fn get_first_face(&self, font_id: FontId) -> Result { + pub fn get_first_face(&self, font_id: FontId) -> Result, InvalidFontId> { let face_id = self.first_face_for(font_id)?; Ok(self.get_face(face_id)) } @@ -268,13 +268,15 @@ impl FontLibrary { /// /// Otherwise, return the first face of `font_id` which covers `c`. /// - /// Otherwise (if no face covers `c`) return `None`. + /// Otherwise (if no face covers `c`), return `last_face_id` (if some) or + /// else the first face listed for `font_id`. The idea here is to ensure + /// that shaping can continue without causing unnecessary font breaks. pub fn face_for_char( &self, font_id: FontId, last_face_id: Option, c: char, - ) -> Result, InvalidFontId> { + ) -> Result { // TODO: `face.glyph_index` is a bit slow to use like this where several // faces may return no result before we find a match. Caching results // in a HashMap helps. Perhaps better would be to (somehow) determine @@ -294,14 +296,16 @@ impl FontLibrary { let face = &faces.faces[face_id.get()]; // TODO(opt): should we cache this lookup? if face.face.glyph_index(c).is_some() { - return Ok(Some(face_id)); + return Ok(face_id); } } } - Ok(match font.2.entry(c) { + // Check the cache for c + let result = match font.2.entry(c) { Entry::Occupied(entry) => *entry.get(), Entry::Vacant(entry) => { + // Not cached: look for the first suitable face let mut id: Option = None; for face_id in font.1.iter() { let face = &faces.faces[face_id.get()]; @@ -310,10 +314,15 @@ impl FontLibrary { break; } } + entry.insert(id); id } - }) + }; + + Ok(result + .or(last_face_id) + .unwrap_or_else(|| *font.1.first().unwrap())) } /// Select a font @@ -324,6 +333,7 @@ impl FontLibrary { &self, selector: &FontSelector, script: Script, + range: Option, ) -> Result { let sel_hash = { use std::collections::hash_map::DefaultHasher; @@ -332,6 +342,7 @@ impl FontLibrary { let mut s = DefaultHasher::new(); selector.hash(&mut s); script.hash(&mut s); + range.hash(&mut s); s.finish() }; @@ -348,7 +359,7 @@ impl FontLibrary { let mut resolver = self.resolver.lock().unwrap(); let mut face_list = self.faces.write().unwrap(); - selector.select(&mut resolver, script, |qf| { + selector.select(&mut resolver, script, range, |qf| { if log::log_enabled!(log::Level::Debug) { families.push(qf.family); } diff --git a/src/fonts/resolver.rs b/src/fonts/resolver.rs index 5dce012..0c89f30 100644 --- a/src/fonts/resolver.rs +++ b/src/fonts/resolver.rs @@ -10,7 +10,7 @@ use super::{FontStyle, FontWeight, FontWidth}; use fontique::{ Attributes, Collection, FamilyId, GenericFamily, QueryFamily, QueryFont, QueryStatus, Script, - SourceCache, + SourceCache, UnicodeRange, }; use log::debug; #[cfg(feature = "serde")] @@ -195,21 +195,26 @@ impl FontSelector { /// Resolve font faces for each matching font /// /// All font faces matching steps 1-4 will be returned through the `add_face` closure. - pub(crate) fn select(&self, resolver: &mut Resolver, script: Script, add_face: F) - where + pub(crate) fn select( + &self, + resolver: &mut Resolver, + script: Script, + range: Option, + add_face: F, + ) where F: FnMut(&QueryFont) -> QueryStatus, { let mut query = resolver.collection.query(&mut resolver.cache); if let Some(gf) = self.family.as_generic() { debug!( - "select: Script::{:?}, GenericFamily::{:?}, {:?}, {:?}, {:?}", + "select: Script::{:?}, {range:?}, GenericFamily::{:?}, {:?}, {:?}, {:?}", &script, gf, &self.weight, &self.width, &self.style ); query.set_families([gf]); } else if let Some(set) = resolver.families.get(&self.family) { debug!( - "select: Script::{:?}, {:?}, {:?}, {:?}, {:?}", + "select: Script::{:?}, {range:?}, {:?}, {:?}, {:?}, {:?}", &script, set, &self.weight, &self.width, &self.style ); @@ -224,6 +229,8 @@ impl FontSelector { query.set_fallbacks(script); + query.set_range(range); + query.matches_with(add_face); } }