#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
#![doc(html_logo_url = "https://zng-ui.github.io/res/zng-logo.png")]
#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
#![expect(clippy::type_complexity)]
#![warn(unused_extern_crates)]
#![warn(missing_docs)]
#![cfg_attr(not(ipc), allow(unused))]
use font_features::RFontVariations;
use hashbrown::{HashMap, HashSet};
use skrifa::MetadataProvider;
use std::{borrow::Cow, fmt, io, ops, path::PathBuf, slice::SliceIndex, sync::Arc};
#[cfg(not(any(target_arch = "wasm32", target_os = "android")))]
use zng_task::channel::WeakIpcBytes;
#[macro_use]
extern crate bitflags;
pub mod font_features;
mod query_util;
mod emoji_util;
pub use emoji_util::*;
mod ligature_util;
use ligature_util::*;
mod unicode_bidi_util;
mod segmenting;
pub use segmenting::*;
mod shaping;
pub use shaping::*;
use zng_clone_move::{async_clmv, clmv};
mod hyphenation;
pub use self::hyphenation::*;
mod unit;
pub use unit::*;
use pastey::paste;
use zng_app::{
event::{event, event_args},
render::FontSynthesis,
update::UPDATES,
view_process::{
VIEW_PROCESS_INITED_EVENT, ViewRenderer,
raw_events::{RAW_FONT_AA_CHANGED_EVENT, RAW_FONT_CHANGED_EVENT},
},
};
use zng_app_context::app_local;
use zng_ext_l10n::{Lang, LangMap, lang};
use zng_layout::unit::{
ByteUnits as _, EQ_GRANULARITY, EQ_GRANULARITY_100, Factor, FactorPercent, Px, PxRect, TimeUnits as _, about_eq, about_eq_hash,
about_eq_ord, euclid,
};
use zng_task::parking_lot::{Mutex, RwLock};
use zng_task::{self as task, channel::IpcBytes};
use zng_txt::{ToTxt, Txt};
use zng_var::{IntoVar, ResponseVar, Var, animation::Transitionable, const_var, impl_from_and_into_var, response_done_var, response_var};
use zng_view_api::{config::FontAntiAliasing, font::IpcFontBytes};
#[derive(Clone)]
pub struct FontName {
txt: Txt,
is_ascii: bool,
}
impl fmt::Debug for FontName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
f.debug_struct("FontName")
.field("txt", &self.txt)
.field("is_ascii", &self.is_ascii)
.finish()
} else {
write!(f, "{:?}", self.txt)
}
}
}
impl PartialEq for FontName {
fn eq(&self, other: &Self) -> bool {
self.unicase() == other.unicase()
}
}
impl Eq for FontName {}
impl PartialEq<str> for FontName {
fn eq(&self, other: &str) -> bool {
self.unicase() == unicase::UniCase::<&str>::from(other)
}
}
impl std::hash::Hash for FontName {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
std::hash::Hash::hash(&self.unicase(), state)
}
}
impl Ord for FontName {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
if self == other {
return std::cmp::Ordering::Equal;
}
self.txt.cmp(&other.txt)
}
}
impl PartialOrd for FontName {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl FontName {
fn unicase(&self) -> unicase::UniCase<&str> {
if self.is_ascii {
unicase::UniCase::ascii(self)
} else {
unicase::UniCase::unicode(self)
}
}
pub const fn from_static(name: &'static str) -> Self {
FontName {
txt: Txt::from_static(name),
is_ascii: {
let name_bytes = name.as_bytes();
let mut i = name_bytes.len();
let mut is_ascii = true;
while i > 0 {
i -= 1;
if !name_bytes[i].is_ascii() {
is_ascii = false;
break;
}
}
is_ascii
},
}
}
pub fn new(name: impl Into<Txt>) -> Self {
let txt = name.into();
FontName {
is_ascii: txt.is_ascii(),
txt,
}
}
pub fn serif() -> Self {
Self::new("serif")
}
pub fn sans_serif() -> Self {
Self::new("sans-serif")
}
pub fn monospace() -> Self {
Self::new("monospace")
}
pub fn cursive() -> Self {
Self::new("cursive")
}
pub fn fantasy() -> Self {
Self::new("fantasy")
}
pub fn system_ui() -> Self {
Self::new("system-ui")
}
pub fn name(&self) -> &str {
&self.txt
}
pub fn into_text(self) -> Txt {
self.txt
}
}
impl_from_and_into_var! {
fn from(s: &'static str) -> FontName {
FontName::new(s)
}
fn from(s: String) -> FontName {
FontName::new(s)
}
fn from(s: Cow<'static, str>) -> FontName {
FontName::new(s)
}
fn from(f: FontName) -> Txt {
f.into_text()
}
fn from(s: Txt) -> FontName {
FontName::new(s)
}
}
impl fmt::Display for FontName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
impl std::ops::Deref for FontName {
type Target = str;
fn deref(&self) -> &Self::Target {
self.txt.deref()
}
}
impl AsRef<str> for FontName {
fn as_ref(&self) -> &str {
self.txt.as_ref()
}
}
impl serde::Serialize for FontName {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.txt.serialize(serializer)
}
}
impl<'de> serde::Deserialize<'de> for FontName {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Txt::deserialize(deserializer).map(FontName::new)
}
}
#[derive(Eq, PartialEq, Hash, Clone, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct FontNames(pub Vec<FontName>);
impl FontNames {
pub fn empty() -> Self {
FontNames(vec![])
}
pub fn push(&mut self, font_name: impl Into<FontName>) {
self.0.push(font_name.into())
}
}
impl Default for FontNames {
fn default() -> Self {
FontName::system_ui().into()
}
}
impl fmt::Debug for FontNames {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
f.debug_tuple("FontNames").field(&self.0).finish()
} else if self.0.is_empty() {
write!(f, "[]")
} else if self.0.len() == 1 {
write!(f, "{:?}", self.0[0])
} else {
write!(f, "[{:?}, ", self.0[0])?;
for name in &self.0[1..] {
write!(f, "{name:?}, ")?;
}
write!(f, "]")
}
}
}
impl fmt::Display for FontNames {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut iter = self.0.iter();
if let Some(name) = iter.next() {
write!(f, "{name}")?;
for name in iter {
write!(f, ", {name}")?;
}
}
Ok(())
}
}
impl_from_and_into_var! {
fn from(font_name: &'static str) -> FontNames {
FontNames(vec![FontName::new(font_name)])
}
fn from(font_name: String) -> FontNames {
FontNames(vec![FontName::new(font_name)])
}
fn from(font_name: Txt) -> FontNames {
FontNames(vec![FontName::new(font_name)])
}
fn from(font_names: Vec<FontName>) -> FontNames {
FontNames(font_names)
}
fn from(font_names: Vec<&'static str>) -> FontNames {
FontNames(font_names.into_iter().map(FontName::new).collect())
}
fn from(font_names: Vec<String>) -> FontNames {
FontNames(font_names.into_iter().map(FontName::new).collect())
}
fn from(font_name: FontName) -> FontNames {
FontNames(vec![font_name])
}
}
impl ops::Deref for FontNames {
type Target = Vec<FontName>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl ops::DerefMut for FontNames {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl std::iter::Extend<FontName> for FontNames {
fn extend<T: IntoIterator<Item = FontName>>(&mut self, iter: T) {
self.0.extend(iter)
}
}
impl IntoIterator for FontNames {
type Item = FontName;
type IntoIter = std::vec::IntoIter<FontName>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<const N: usize> From<[FontName; N]> for FontNames {
fn from(font_names: [FontName; N]) -> Self {
FontNames(font_names.into())
}
}
impl<const N: usize> IntoVar<FontNames> for [FontName; N] {
fn into_var(self) -> Var<FontNames> {
const_var(self.into())
}
}
impl<const N: usize> From<[&'static str; N]> for FontNames {
fn from(font_names: [&'static str; N]) -> Self {
FontNames(font_names.into_iter().map(FontName::new).collect())
}
}
impl<const N: usize> IntoVar<FontNames> for [&'static str; N] {
fn into_var(self) -> Var<FontNames> {
const_var(self.into())
}
}
impl<const N: usize> From<[String; N]> for FontNames {
fn from(font_names: [String; N]) -> Self {
FontNames(font_names.into_iter().map(FontName::new).collect())
}
}
impl<const N: usize> IntoVar<FontNames> for [String; N] {
fn into_var(self) -> Var<FontNames> {
const_var(self.into())
}
}
impl<const N: usize> From<[Txt; N]> for FontNames {
fn from(font_names: [Txt; N]) -> Self {
FontNames(font_names.into_iter().map(FontName::new).collect())
}
}
impl<const N: usize> IntoVar<FontNames> for [Txt; N] {
fn into_var(self) -> Var<FontNames> {
const_var(self.into())
}
}
event! {
pub static FONT_CHANGED_EVENT: FontChangedArgs;
}
event_args! {
pub struct FontChangedArgs {
pub change: FontChange,
..
fn is_in_target(&self, id: WidgetId) -> bool {
true
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum FontChange {
SystemFonts,
CustomFonts,
Refresh,
GenericFont(FontName, Lang),
Fallback(Lang),
}
app_local! {
static FONTS_SV: FontsService = FontsService::new();
}
struct FontsService {
loader: FontFaceLoader,
}
impl FontsService {
fn new() -> Self {
let s = FontsService {
loader: FontFaceLoader::new(),
};
RAW_FONT_CHANGED_EVENT
.hook(|args| {
FONT_CHANGED_EVENT.notify(FontChangedArgs::new(
args.timestamp,
args.propagation.clone(),
FontChange::SystemFonts,
));
true
})
.perm();
FONT_CHANGED_EVENT
.hook(|_| {
let mut s = FONTS_SV.write();
s.loader.on_refresh();
true
})
.perm();
VIEW_PROCESS_INITED_EVENT
.hook(|args| {
if args.is_respawn {
FONTS_SV.write().loader.on_view_process_respawn();
}
true
})
.perm();
s
}
}
pub struct FONTS;
impl FONTS {
pub fn refresh(&self) {
FONT_CHANGED_EVENT.notify(FontChangedArgs::now(FontChange::Refresh));
}
pub fn prune(&self) {
UPDATES.once_update("FONTS.prune", move || {
FONTS_SV.write().loader.on_prune();
});
}
pub fn generics(&self) -> &'static GenericFonts {
&GenericFonts {}
}
pub fn register(&self, custom_font: CustomFont) -> ResponseVar<Result<FontFace, FontLoadingError>> {
let resp = task::respond(FontFace::load_custom(custom_font));
resp.hook(|args| {
if let Some(done) = args.value().done() {
if let Ok(face) = done {
let mut fonts = FONTS_SV.write();
let family = fonts.loader.custom_fonts.entry(face.0.family_name.clone()).or_default();
let existing = family
.iter()
.position(|f| f.0.weight == face.0.weight && f.0.style == face.0.style && f.0.stretch == face.0.stretch);
if let Some(i) = existing {
family[i] = face.clone();
} else {
family.push(face.clone());
}
FONT_CHANGED_EVENT.notify(FontChangedArgs::now(FontChange::CustomFonts));
}
false
} else {
true
}
})
.perm();
resp
}
pub fn unregister(&self, custom_family: FontName) -> ResponseVar<bool> {
let (responder, response) = response_var();
UPDATES.once_update("FONTS.unregister", move || {
let mut fonts = FONTS_SV.write();
let r = if let Some(removed) = fonts.loader.custom_fonts.remove(&custom_family) {
for removed in removed {
removed.on_refresh();
}
true
} else {
false
};
responder.respond(r);
if r {
FONT_CHANGED_EVENT.notify(FontChangedArgs::now(FontChange::CustomFonts));
}
});
response
}
pub fn list(
&self,
families: &[FontName],
style: FontStyle,
weight: FontWeight,
stretch: FontStretch,
lang: &Lang,
) -> ResponseVar<FontFaceList> {
if let Some(cached) = FONTS_SV.read().loader.try_list(families, style, weight, stretch, lang) {
tracing::trace!("font list ({families:?} {style:?} {weight:?} {stretch:?} {lang:?}) found cached");
return cached;
}
tracing::trace!("font list ({families:?} {style:?} {weight:?} {stretch:?} {lang:?}) not cached, load");
FONTS_SV.write().loader.load_list(families, style, weight, stretch, lang)
}
pub fn find(
&self,
family: &FontName,
style: FontStyle,
weight: FontWeight,
stretch: FontStretch,
lang: &Lang,
) -> ResponseVar<Option<FontFace>> {
let resolved = GenericFonts {}.resolve(family, lang);
let family = resolved.as_ref().unwrap_or(family);
if let Some(cached) = FONTS_SV.read().loader.try_resolved(family, style, weight, stretch) {
return cached;
}
FONTS_SV.write().loader.load_resolved(family, style, weight, stretch)
}
pub fn normal(&self, family: &FontName, lang: &Lang) -> ResponseVar<Option<FontFace>> {
self.find(family, FontStyle::Normal, FontWeight::NORMAL, FontStretch::NORMAL, lang)
}
pub fn italic(&self, family: &FontName, lang: &Lang) -> ResponseVar<Option<FontFace>> {
self.find(family, FontStyle::Italic, FontWeight::NORMAL, FontStretch::NORMAL, lang)
}
pub fn bold(&self, family: &FontName, lang: &Lang) -> ResponseVar<Option<FontFace>> {
self.find(family, FontStyle::Normal, FontWeight::BOLD, FontStretch::NORMAL, lang)
}
pub fn custom_fonts(&self) -> Vec<FontName> {
FONTS_SV.read().loader.custom_fonts.keys().cloned().collect()
}
pub fn system_fonts(&self) -> ResponseVar<Vec<FontName>> {
query_util::system_all()
}
pub fn system_font_aa(&self) -> Var<FontAntiAliasing> {
RAW_FONT_AA_CHANGED_EVENT.var_map(|a| Some(a.aa), || FontAntiAliasing::Default)
}
}
#[derive(PartialEq, Eq, Hash)]
struct FontInstanceKey(Px, Box<[(skrifa::Tag, i32)]>);
impl FontInstanceKey {
pub(crate) fn new(size: Px, variations: &[harfrust::Variation]) -> Self {
let variations_key: Vec<_> = variations.iter().map(|p| (p.tag, (p.value * 1000.0) as i32)).collect();
FontInstanceKey(size, variations_key.into_boxed_slice())
}
}
#[derive(Clone)]
pub struct FontFace(Arc<LoadedFontFace>);
struct LoadedFontFace {
data: FontBytes,
face_index: u32,
display_name: FontName,
family_name: FontName,
postscript_name: Option<Txt>,
style: FontStyle,
weight: FontWeight,
stretch: FontStretch,
lig_carets: LigatureCaretList,
flags: FontFaceFlags,
m: Mutex<FontFaceMut>,
}
bitflags! {
#[derive(Debug, Clone, Copy)]
struct FontFaceFlags: u8 {
const IS_MONOSPACE = 0b0000_0001;
const HAS_LIGATURES = 0b0000_0010;
const HAS_RASTER_IMAGES = 0b0000_0100;
const HAS_SVG_IMAGES = 0b0000_1000;
}
}
struct FontFaceMut {
instances: HashMap<FontInstanceKey, Font>,
render_ids: Vec<RenderFontFace>,
unregistered: bool,
}
impl fmt::Debug for FontFace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let m = self.0.m.lock();
f.debug_struct("FontFace")
.field("display_name", &self.0.display_name)
.field("family_name", &self.0.family_name)
.field("postscript_name", &self.0.postscript_name)
.field("flags", &self.0.flags)
.field("style", &self.0.style)
.field("weight", &self.0.weight)
.field("stretch", &self.0.stretch)
.field("instances.len()", &m.instances.len())
.field("render_keys.len()", &m.render_ids.len())
.field("unregistered", &m.unregistered)
.finish_non_exhaustive()
}
}
impl PartialEq for FontFace {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}
impl Eq for FontFace {}
impl FontFace {
pub fn empty() -> Self {
FontFace(Arc::new(LoadedFontFace {
data: FontBytes::from_static(&[]),
face_index: 0,
display_name: FontName::from("<empty>"),
family_name: FontName::from("<empty>"),
postscript_name: None,
flags: FontFaceFlags::IS_MONOSPACE,
style: FontStyle::Normal,
weight: FontWeight::NORMAL,
stretch: FontStretch::NORMAL,
lig_carets: LigatureCaretList::empty(),
m: Mutex::new(FontFaceMut {
instances: HashMap::default(),
render_ids: vec![],
unregistered: false,
}),
}))
}
pub fn is_empty(&self) -> bool {
self.0.data.is_empty()
}
async fn load_custom(custom_font: CustomFont) -> Result<Self, FontLoadingError> {
let bytes;
let mut face_index;
match custom_font.source {
FontSource::File(path, index) => {
bytes = task::wait(|| FontBytes::from_file(path)).await?;
face_index = index;
}
FontSource::Memory(arc, index) => {
bytes = arc;
face_index = index;
}
FontSource::Alias(other_font) => {
let result = FONTS_SV
.write()
.loader
.load_resolved(&other_font, custom_font.style, custom_font.weight, custom_font.stretch);
return match result.wait_rsp().await {
Some(other_font) => Ok(FontFace(Arc::new(LoadedFontFace {
data: other_font.0.data.clone(),
face_index: other_font.0.face_index,
display_name: custom_font.name.clone(),
family_name: custom_font.name,
postscript_name: None,
style: other_font.0.style,
weight: other_font.0.weight,
stretch: other_font.0.stretch,
m: Mutex::new(FontFaceMut {
instances: Default::default(),
render_ids: Default::default(),
unregistered: Default::default(),
}),
lig_carets: other_font.0.lig_carets.clone(),
flags: other_font.0.flags,
}))),
None => Err(FontLoadingError::NoSuchFontInCollection),
};
}
}
let ttf_face = match skrifa::FontRef::from_index(&bytes, face_index) {
Ok(f) => f,
Err(e) => {
match e {
read_fonts::ReadError::InvalidCollectionIndex(_) if face_index != 0 => face_index = 0,
e => return Err(FontLoadingError::Parse(e)),
}
match skrifa::FontRef::from_index(&bytes, face_index) {
Ok(f) => f,
Err(_) => return Err(FontLoadingError::Parse(e)),
}
}
};
use read_fonts::TableProvider as _;
let has_ligatures = ttf_face.gsub().is_ok();
let lig_carets = if has_ligatures {
LigatureCaretList::empty()
} else {
LigatureCaretList::load(&ttf_face)?
};
let mut flags = FontFaceFlags::empty();
flags.set(
FontFaceFlags::IS_MONOSPACE,
ttf_face.post().map(|p| p.is_fixed_pitch() != 0).unwrap_or(false),
);
flags.set(FontFaceFlags::HAS_LIGATURES, has_ligatures);
flags.set(
FontFaceFlags::HAS_RASTER_IMAGES,
ttf_face.sbix().is_ok() || ttf_face.ebdt().is_ok() || ttf_face.cbdt().is_ok(),
);
flags.set(FontFaceFlags::HAS_SVG_IMAGES, ttf_face.svg().is_ok());
Ok(FontFace(Arc::new(LoadedFontFace {
face_index,
display_name: custom_font.name.clone(),
family_name: custom_font.name,
postscript_name: None,
style: custom_font.style,
weight: custom_font.weight,
stretch: custom_font.stretch,
lig_carets,
m: Mutex::new(FontFaceMut {
instances: Default::default(),
render_ids: Default::default(),
unregistered: Default::default(),
}),
data: bytes,
flags,
})))
}
fn load(bytes: FontBytes, mut face_index: u32) -> Result<Self, FontLoadingError> {
let _span = tracing::trace_span!("FontFace::load").entered();
let ttf_face = match skrifa::FontRef::from_index(&bytes, face_index) {
Ok(f) => f,
Err(e) => {
match e {
read_fonts::ReadError::InvalidCollectionIndex(_) if face_index != 0 => face_index = 0,
e => return Err(FontLoadingError::Parse(e)),
}
match skrifa::FontRef::from_index(&bytes, face_index) {
Ok(f) => f,
Err(_) => return Err(FontLoadingError::Parse(e)),
}
}
};
use read_fonts::TableProvider as _;
let has_ligatures = ttf_face.gsub().is_ok();
let lig_carets = if has_ligatures {
LigatureCaretList::empty()
} else {
LigatureCaretList::load(&ttf_face)?
};
let mut display_name = None;
let mut family_name = None;
let mut postscript_name = None;
let mut any_name = None::<Txt>;
if let Ok(name) = ttf_face.name() {
for record in name.name_record() {
let n = match record.string(name.string_data()) {
Ok(n) => n.to_txt(),
Err(_) => continue,
};
match record.name_id() {
read_fonts::tables::name::NameId::FULL_NAME => display_name = Some(n),
read_fonts::tables::name::NameId::FAMILY_NAME => family_name = Some(n),
read_fonts::tables::name::NameId::POSTSCRIPT_NAME => postscript_name = Some(n),
_ => {
if let Some(t) = &mut any_name {
if t.len() < n.len() {
*t = n;
}
} else {
any_name = Some(n)
}
}
}
}
}
let display_name = FontName::new(
display_name
.clone()
.or_else(|| family_name.clone())
.or_else(|| postscript_name.clone())
.or_else(|| any_name.clone())
.unwrap_or_default(),
);
let family_name = family_name.map(FontName::from).unwrap_or_else(|| display_name.clone());
let postscript_name = postscript_name;
let mut flags = FontFaceFlags::empty();
flags.set(
FontFaceFlags::IS_MONOSPACE,
ttf_face.post().map(|p| p.is_fixed_pitch() != 0).unwrap_or(false),
);
flags.set(FontFaceFlags::HAS_LIGATURES, has_ligatures);
flags.set(
FontFaceFlags::HAS_RASTER_IMAGES,
ttf_face.sbix().is_ok() || ttf_face.ebdt().is_ok() || ttf_face.cbdt().is_ok(),
);
flags.set(FontFaceFlags::HAS_SVG_IMAGES, ttf_face.svg().is_ok());
let attr = ttf_face.attributes();
Ok(FontFace(Arc::new(LoadedFontFace {
face_index,
family_name,
display_name,
postscript_name,
style: attr.style.into(),
weight: attr.weight.into(),
stretch: attr.stretch.into(),
lig_carets,
m: Mutex::new(FontFaceMut {
instances: Default::default(),
render_ids: Default::default(),
unregistered: Default::default(),
}),
data: bytes,
flags,
})))
}
fn on_refresh(&self) {
let mut m = self.0.m.lock();
m.instances.clear();
m.unregistered = true;
}
fn render_face(&self, renderer: &ViewRenderer) -> zng_view_api::font::FontFaceId {
let mut m = self.0.m.lock();
for r in m.render_ids.iter() {
if &r.renderer == renderer {
return r.face_id;
}
}
let data = match self.0.data.to_ipc() {
Ok(d) => d,
Err(e) => {
tracing::error!("cannot allocate ipc font data, {e}");
return zng_view_api::font::FontFaceId::INVALID;
}
};
let key = match renderer.add_font_face(data, self.0.face_index) {
Ok(k) => k,
Err(_) => {
tracing::debug!("respawned calling `add_font`, will return dummy font key");
return zng_view_api::font::FontFaceId::INVALID;
}
};
m.render_ids.push(RenderFontFace::new(renderer, key));
key
}
pub(crate) fn raw(&self) -> Option<harfrust::FontRef<'_>> {
if self.is_empty() {
None
} else {
Some(harfrust::FontRef::from_index(&self.0.data, self.0.face_index).unwrap())
}
}
pub fn bytes(&self) -> &FontBytes {
&self.0.data
}
pub fn index(&self) -> u32 {
self.0.face_index
}
pub fn display_name(&self) -> &FontName {
&self.0.display_name
}
pub fn family_name(&self) -> &FontName {
&self.0.family_name
}
pub fn postscript_name(&self) -> Option<&str> {
self.0.postscript_name.as_deref()
}
pub fn style(&self) -> FontStyle {
self.0.style
}
pub fn weight(&self) -> FontWeight {
self.0.weight
}
pub fn stretch(&self) -> FontStretch {
self.0.stretch
}
pub fn is_monospace(&self) -> bool {
self.0.flags.contains(FontFaceFlags::IS_MONOSPACE)
}
pub fn sized(&self, font_size: Px, variations: RFontVariations) -> Font {
let key = FontInstanceKey::new(font_size, &variations);
let mut m = self.0.m.lock();
if !m.unregistered {
m.instances
.entry(key)
.or_insert_with(|| Font::new(self.clone(), font_size, variations))
.clone()
} else {
tracing::debug!(target: "font_loading", "creating font from unregistered `{}`, will not cache", self.0.display_name);
Font::new(self.clone(), font_size, variations)
}
}
pub fn synthesis_for(&self, style: FontStyle, weight: FontWeight) -> FontSynthesis {
let mut synth = FontSynthesis::DISABLED;
if style != FontStyle::Normal && self.style() == FontStyle::Normal {
synth |= FontSynthesis::OBLIQUE;
}
if weight > self.weight() {
synth |= FontSynthesis::BOLD;
}
synth
}
pub fn is_cached(&self) -> bool {
!self.0.m.lock().unregistered
}
pub fn color_palettes(&self) -> ColorPalettes<'_> {
match self.raw() {
Some(ttf) => ColorPalettes::new(ttf),
None => ColorPalettes::empty(),
}
}
pub fn color_glyphs(&self) -> ColorGlyphs<'_> {
match self.raw() {
Some(ttf) => ColorGlyphs::new(ttf),
None => ColorGlyphs::empty(),
}
}
pub fn has_ligatures(&self) -> bool {
self.0.flags.contains(FontFaceFlags::HAS_LIGATURES)
}
pub fn has_ligature_caret_offsets(&self) -> bool {
!self.0.lig_carets.is_empty()
}
pub fn has_raster_images(&self) -> bool {
self.0.flags.contains(FontFaceFlags::HAS_RASTER_IMAGES)
}
pub fn has_svg_images(&self) -> bool {
self.0.flags.contains(FontFaceFlags::HAS_SVG_IMAGES)
}
}
#[derive(Clone)]
pub struct Font(Arc<LoadedFont>);
struct LoadedFont {
face: FontFace,
size: Px,
variations: RFontVariations,
metrics: FontMetrics,
render_keys: Mutex<Vec<RenderFont>>,
small_word_cache: RwLock<HashMap<WordCacheKey<[u8; Font::SMALL_WORD_LEN]>, ShapedSegmentData>>,
word_cache: RwLock<HashMap<WordCacheKey<String>, ShapedSegmentData>>,
shaper_cache: Option<harfrust::ShaperData>,
}
impl fmt::Debug for Font {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Font")
.field("face", &self.0.face)
.field("size", &self.0.size)
.field("metrics", &self.0.metrics)
.field("render_keys.len()", &self.0.render_keys.lock().len())
.field("small_word_cache.len()", &self.0.small_word_cache.read().len())
.field("word_cache.len()", &self.0.word_cache.read().len())
.finish()
}
}
impl PartialEq for Font {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}
impl Eq for Font {}
impl Font {
const SMALL_WORD_LEN: usize = 8;
fn to_small_word(s: &str) -> Option<[u8; Self::SMALL_WORD_LEN]> {
if s.len() <= Self::SMALL_WORD_LEN {
let mut a = [b'\0'; Self::SMALL_WORD_LEN];
a[..s.len()].copy_from_slice(s.as_bytes());
Some(a)
} else {
None
}
}
fn new(face: FontFace, size: Px, variations: RFontVariations) -> Self {
let (metrics, shaper_cache) = match face.raw() {
Some(f) => (FontMetrics::new(&f, size), Some(harfrust::ShaperData::new(&f))),
None => (FontMetrics::empty(), None),
};
Font(Arc::new(LoadedFont {
metrics,
face,
size,
variations,
render_keys: Mutex::new(vec![]),
small_word_cache: RwLock::default(),
word_cache: RwLock::default(),
shaper_cache,
}))
}
fn render_font(&self, renderer: &ViewRenderer, synthesis: FontSynthesis) -> zng_view_api::font::FontId {
let _span = tracing::trace_span!("Font::render_font").entered();
let mut render_keys = self.0.render_keys.lock();
for r in render_keys.iter() {
if &r.renderer == renderer && r.synthesis == synthesis {
return r.font_id;
}
}
let font_key = self.0.face.render_face(renderer);
let mut opt = zng_view_api::font::FontOptions::default();
opt.synthetic_oblique = synthesis.contains(FontSynthesis::OBLIQUE);
opt.synthetic_bold = synthesis.contains(FontSynthesis::BOLD);
let variations = self.0.variations.iter().map(|v| (v.tag.to_be_bytes(), v.value)).collect();
let key = match renderer.add_font(font_key, self.0.size, opt, variations) {
Ok(k) => k,
Err(_) => {
tracing::debug!("respawned calling `add_font_instance`, will return dummy font key");
return zng_view_api::font::FontId::INVALID;
}
};
render_keys.push(RenderFont::new(renderer, synthesis, key));
key
}
pub fn face(&self) -> &FontFace {
&self.0.face
}
pub fn size(&self) -> Px {
self.0.size
}
pub fn variations(&self) -> &RFontVariations {
&self.0.variations
}
pub fn metrics(&self) -> &FontMetrics {
&self.0.metrics
}
pub fn ligature_caret_offsets(
&self,
lig: zng_view_api::font::GlyphIndex,
) -> impl ExactSizeIterator<Item = f32> + DoubleEndedIterator + '_ {
self.0.face.0.lig_carets.carets(lig).iter().map(move |&o| match o {
ligature_util::LigatureCaret::Coordinate(o) => {
let size_scale = 1.0 / self.0.metrics.units_per_em as f32 * self.0.size.0 as f32;
o as f32 * size_scale
}
ligature_util::LigatureCaret::GlyphContourPoint(i) => {
if let Some(f) = self.face().raw() {
struct Search {
i: u16,
s: u16,
x: f32,
}
impl Search {
fn check(&mut self, x: f32) {
self.s = self.s.saturating_add(1);
if self.s == self.i {
self.x = x;
}
}
}
impl skrifa::outline::OutlinePen for Search {
fn move_to(&mut self, x: f32, _y: f32) {
self.check(x);
}
fn line_to(&mut self, x: f32, _y: f32) {
self.check(x);
}
fn quad_to(&mut self, _x1: f32, _y1: f32, x: f32, _y: f32) {
self.check(x)
}
fn curve_to(&mut self, _x1: f32, _y1: f32, _x2: f32, _y2: f32, x: f32, _y: f32) {
self.check(x);
}
fn close(&mut self) {}
}
let mut search = Search { i, s: 0, x: 0.0 };
if let Some(o) = f.outline_glyphs().get(skrifa::GlyphId::new(lig))
&& o.draw(
skrifa::outline::DrawSettings::unhinted(
skrifa::instance::Size::new(self.size().0 as f32),
skrifa::instance::LocationRef::default(),
),
&mut search,
)
.is_ok()
&& search.s >= search.i
{
return search.x;
}
}
0.0
}
})
}
}
impl zng_app::render::Font for Font {
fn is_empty_fallback(&self) -> bool {
self.face().is_empty()
}
fn renderer_id(&self, renderer: &ViewRenderer, synthesis: FontSynthesis) -> zng_view_api::font::FontId {
self.render_font(renderer, synthesis)
}
}
#[derive(Debug, Clone)]
pub struct FontFaceList {
fonts: Box<[FontFace]>,
requested_style: FontStyle,
requested_weight: FontWeight,
requested_stretch: FontStretch,
}
impl FontFaceList {
pub fn empty() -> Self {
Self {
fonts: Box::new([FontFace::empty()]),
requested_style: FontStyle::Normal,
requested_weight: FontWeight::NORMAL,
requested_stretch: FontStretch::NORMAL,
}
}
pub fn requested_style(&self) -> FontStyle {
self.requested_style
}
pub fn requested_weight(&self) -> FontWeight {
self.requested_weight
}
pub fn requested_stretch(&self) -> FontStretch {
self.requested_stretch
}
pub fn best(&self) -> &FontFace {
&self.fonts[0]
}
pub fn face_synthesis(&self, face_index: usize) -> FontSynthesis {
if let Some(face) = self.fonts.get(face_index) {
face.synthesis_for(self.requested_style, self.requested_weight)
} else {
FontSynthesis::DISABLED
}
}
pub fn iter(&self) -> std::slice::Iter<'_, FontFace> {
self.fonts.iter()
}
pub fn len(&self) -> usize {
self.fonts.len()
}
pub fn is_empty(&self) -> bool {
self.fonts[0].is_empty() && self.fonts.len() == 1
}
pub fn sized(&self, font_size: Px, variations: RFontVariations) -> FontList {
FontList {
fonts: self.fonts.iter().map(|f| f.sized(font_size, variations.clone())).collect(),
requested_style: self.requested_style,
requested_weight: self.requested_weight,
requested_stretch: self.requested_stretch,
}
}
}
impl PartialEq for FontFaceList {
fn eq(&self, other: &Self) -> bool {
self.requested_style == other.requested_style
&& self.requested_weight == other.requested_weight
&& self.requested_stretch == other.requested_stretch
&& self.fonts.len() == other.fonts.len()
&& self.fonts.iter().zip(other.fonts.iter()).all(|(a, b)| a == b)
}
}
impl Eq for FontFaceList {}
impl std::ops::Deref for FontFaceList {
type Target = [FontFace];
fn deref(&self) -> &Self::Target {
&self.fonts
}
}
impl<'a> std::iter::IntoIterator for &'a FontFaceList {
type Item = &'a FontFace;
type IntoIter = std::slice::Iter<'a, FontFace>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl std::ops::Index<usize> for FontFaceList {
type Output = FontFace;
fn index(&self, index: usize) -> &Self::Output {
&self.fonts[index]
}
}
#[derive(Debug, Clone)]
pub struct FontList {
fonts: Box<[Font]>,
requested_style: FontStyle,
requested_weight: FontWeight,
requested_stretch: FontStretch,
}
#[expect(clippy::len_without_is_empty)] impl FontList {
pub fn best(&self) -> &Font {
&self.fonts[0]
}
pub fn requested_size(&self) -> Px {
self.fonts[0].size()
}
pub fn requested_style(&self) -> FontStyle {
self.requested_style
}
pub fn requested_weight(&self) -> FontWeight {
self.requested_weight
}
pub fn requested_stretch(&self) -> FontStretch {
self.requested_stretch
}
pub fn face_synthesis(&self, font_index: usize) -> FontSynthesis {
if let Some(font) = self.fonts.get(font_index) {
font.0.face.synthesis_for(self.requested_style, self.requested_weight)
} else {
FontSynthesis::DISABLED
}
}
pub fn iter(&self) -> std::slice::Iter<'_, Font> {
self.fonts.iter()
}
pub fn len(&self) -> usize {
self.fonts.len()
}
pub fn is_sized_from(&self, faces: &FontFaceList) -> bool {
if self.len() != faces.len() {
return false;
}
for (font, face) in self.iter().zip(faces.iter()) {
if font.face() != face {
return false;
}
}
true
}
}
impl PartialEq for FontList {
fn eq(&self, other: &Self) -> bool {
self.requested_style == other.requested_style
&& self.requested_weight == other.requested_weight
&& self.requested_stretch == other.requested_stretch
&& self.fonts.len() == other.fonts.len()
&& self.fonts.iter().zip(other.fonts.iter()).all(|(a, b)| a == b)
}
}
impl Eq for FontList {}
impl std::ops::Deref for FontList {
type Target = [Font];
fn deref(&self) -> &Self::Target {
&self.fonts
}
}
impl<'a> std::iter::IntoIterator for &'a FontList {
type Item = &'a Font;
type IntoIter = std::slice::Iter<'a, Font>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl<I: SliceIndex<[Font]>> std::ops::Index<I> for FontList {
type Output = I::Output;
fn index(&self, index: I) -> &I::Output {
&self.fonts[index]
}
}
struct FontFaceLoader {
custom_fonts: HashMap<FontName, Vec<FontFace>>,
system_fonts_cache: HashMap<FontName, Vec<SystemFontFace>>,
list_cache: HashMap<Box<[FontName]>, Vec<FontFaceListQuery>>,
}
struct SystemFontFace {
properties: (FontStyle, FontWeight, FontStretch),
result: ResponseVar<Option<FontFace>>,
}
struct FontFaceListQuery {
properties: (FontStyle, FontWeight, FontStretch),
lang: Lang,
result: ResponseVar<FontFaceList>,
}
impl FontFaceLoader {
fn new() -> Self {
FontFaceLoader {
custom_fonts: HashMap::new(),
system_fonts_cache: HashMap::new(),
list_cache: HashMap::new(),
}
}
fn on_view_process_respawn(&mut self) {
let sys_fonts = self.system_fonts_cache.values().flatten().filter_map(|f| f.result.rsp().flatten());
for face in self.custom_fonts.values().flatten().cloned().chain(sys_fonts) {
let mut m = face.0.m.lock();
m.render_ids.clear();
for inst in m.instances.values() {
inst.0.render_keys.lock().clear();
}
}
}
fn on_refresh(&mut self) {
for (_, sys_family) in self.system_fonts_cache.drain() {
for sys_font in sys_family {
sys_font.result.with(|r| {
if let Some(Some(face)) = r.done() {
face.on_refresh();
}
});
}
}
}
fn on_prune(&mut self) {
self.system_fonts_cache.retain(|_, v| {
v.retain(|sff| {
if sff.result.strong_count() == 1 {
sff.result.with(|r| {
match r.done() {
Some(Some(face)) => Arc::strong_count(&face.0) > 1, Some(None) => false, None => true, }
})
} else {
true
}
});
!v.is_empty()
});
self.list_cache.clear();
}
fn try_list(
&self,
families: &[FontName],
style: FontStyle,
weight: FontWeight,
stretch: FontStretch,
lang: &Lang,
) -> Option<ResponseVar<FontFaceList>> {
if let Some(queries) = self.list_cache.get(families) {
for q in queries {
if q.properties == (style, weight, stretch) && &q.lang == lang {
return Some(q.result.clone());
}
}
}
None
}
fn load_list(
&mut self,
families: &[FontName],
style: FontStyle,
weight: FontWeight,
stretch: FontStretch,
lang: &Lang,
) -> ResponseVar<FontFaceList> {
if let Some(r) = self.try_list(families, style, weight, stretch, lang) {
return r;
}
let resolved = GenericFonts {}.resolve_list(families, lang);
let families = resolved.as_ref().map(|n| &***n).unwrap_or(families);
let mut list = Vec::with_capacity(families.len() + 1);
let mut pending = vec![];
{
let fallback = [GenericFonts {}.fallback(lang)];
let mut used = HashSet::with_capacity(families.len());
for name in families.iter().chain(&fallback) {
if !used.insert(name) {
continue;
}
let face = self.load_resolved(name, style, weight, stretch);
if face.is_done() {
if let Some(face) = face.rsp().unwrap() {
list.push(face);
}
} else {
pending.push((list.len(), face));
}
}
}
let r = if pending.is_empty() {
if list.is_empty() {
tracing::error!(target: "font_loading", "failed to load fallback font");
list.push(FontFace::empty());
}
response_done_var(FontFaceList {
fonts: list.into_boxed_slice(),
requested_style: style,
requested_weight: weight,
requested_stretch: stretch,
})
} else {
task::respond(async move {
for (i, pending) in pending.into_iter().rev() {
if let Some(rsp) = pending.wait_rsp().await {
list.insert(i, rsp);
}
}
if list.is_empty() {
tracing::error!(target: "font_loading", "failed to load fallback font");
list.push(FontFace::empty());
}
FontFaceList {
fonts: list.into_boxed_slice(),
requested_style: style,
requested_weight: weight,
requested_stretch: stretch,
}
})
};
self.list_cache
.entry(families.iter().cloned().collect())
.or_insert_with(|| Vec::with_capacity(1))
.push(FontFaceListQuery {
properties: (style, weight, stretch),
lang: lang.clone(),
result: r.clone(),
});
r
}
fn try_resolved(
&self,
font_name: &FontName,
style: FontStyle,
weight: FontWeight,
stretch: FontStretch,
) -> Option<ResponseVar<Option<FontFace>>> {
if let Some(custom_family) = self.custom_fonts.get(font_name) {
let custom = Self::match_custom(custom_family, style, weight, stretch);
return Some(response_done_var(Some(custom)));
}
if let Some(cached_sys_family) = self.system_fonts_cache.get(font_name) {
for sys_face in cached_sys_family.iter() {
if sys_face.properties == (style, weight, stretch) {
return Some(sys_face.result.clone());
}
}
}
None
}
fn load_resolved(
&mut self,
font_name: &FontName,
style: FontStyle,
weight: FontWeight,
stretch: FontStretch,
) -> ResponseVar<Option<FontFace>> {
if let Some(cached) = self.try_resolved(font_name, style, weight, stretch) {
return cached;
}
let load = task::wait(clmv!(font_name, || {
let (bytes, face_index) = match Self::get_system(&font_name, style, weight, stretch) {
Some(h) => h,
None => {
#[cfg(debug_assertions)]
static NOT_FOUND: Mutex<Option<HashSet<FontName>>> = Mutex::new(None);
#[cfg(debug_assertions)]
if NOT_FOUND.lock().get_or_insert_with(HashSet::default).insert(font_name.clone()) {
tracing::debug!(r#"font "{font_name}" not found"#);
}
return None;
}
};
match FontFace::load(bytes, face_index) {
Ok(f) => Some(f),
Err(FontLoadingError::UnknownFormat) => None,
Err(e) => {
tracing::error!(target: "font_loading", "failed to load system font, {e}\nquery: {:?}", (font_name, style, weight, stretch));
None
}
}
}));
let result = task::respond(async_clmv!(font_name, {
match task::with_deadline(load, 10.secs()).await {
Ok(r) => r,
Err(_) => {
tracing::error!(target: "font_loading", "timeout loading {font_name:?}");
None
}
}
}));
self.system_fonts_cache
.entry(font_name.clone())
.or_insert_with(|| Vec::with_capacity(1))
.push(SystemFontFace {
properties: (style, weight, stretch),
result: result.clone(),
});
result
}
fn get_system(font_name: &FontName, style: FontStyle, weight: FontWeight, stretch: FontStretch) -> Option<(FontBytes, u32)> {
let _span = tracing::trace_span!("FontFaceLoader::get_system").entered();
match query_util::best(font_name, style, weight, stretch) {
Ok(r) => r,
Err(e) => {
tracing::error!("cannot get `{font_name}` system font, {e}");
None
}
}
}
fn match_custom(faces: &[FontFace], style: FontStyle, weight: FontWeight, stretch: FontStretch) -> FontFace {
if faces.len() == 1 {
return faces[0].clone();
}
let mut set = Vec::with_capacity(faces.len());
let mut set_dist = 0.0f64;
let wrong_side = if stretch <= FontStretch::NORMAL {
|s| s > FontStretch::NORMAL
} else {
|s| s <= FontStretch::NORMAL
};
for face in faces {
let mut dist = (face.stretch().0 - stretch.0).abs() as f64;
if wrong_side(face.stretch()) {
dist += f32::MAX as f64 + 1.0;
}
if set.is_empty() {
set.push(face);
set_dist = dist;
} else if dist < set_dist {
set_dist = dist;
set.clear();
set.push(face);
} else if (dist - set_dist).abs() < 0.0001 {
set.push(face);
}
}
if set.len() == 1 {
return set[0].clone();
}
let style_pref = match style {
FontStyle::Normal => [FontStyle::Normal, FontStyle::Oblique, FontStyle::Italic],
FontStyle::Italic => [FontStyle::Italic, FontStyle::Oblique, FontStyle::Normal],
FontStyle::Oblique => [FontStyle::Oblique, FontStyle::Italic, FontStyle::Normal],
};
let mut best_style = style_pref.len();
for face in &set {
let i = style_pref.iter().position(|&s| s == face.style()).unwrap();
if i < best_style {
best_style = i;
}
}
set.retain(|f| f.style() == style_pref[best_style]);
if set.len() == 1 {
return set[0].clone();
}
let add_penalty = if weight.0 >= 400.0 && weight.0 <= 500.0 {
|face: &FontFace, weight: FontWeight, dist: &mut f64| {
if face.weight() < weight {
*dist += 100.0;
} else if face.weight().0 > 500.0 {
*dist += 600.0;
}
}
} else if weight.0 < 400.0 {
|face: &FontFace, weight: FontWeight, dist: &mut f64| {
if face.weight() > weight {
*dist += weight.0 as f64;
}
}
} else {
debug_assert!(weight.0 > 500.0);
|face: &FontFace, weight: FontWeight, dist: &mut f64| {
if face.weight() < weight {
*dist += f32::MAX as f64;
}
}
};
let mut best = set[0];
let mut best_dist = f64::MAX;
for face in &set {
let mut dist = (face.weight().0 - weight.0).abs() as f64;
add_penalty(face, weight, &mut dist);
if dist < best_dist {
best_dist = dist;
best = face;
}
}
best.clone()
}
}
struct RenderFontFace {
renderer: ViewRenderer,
face_id: zng_view_api::font::FontFaceId,
}
impl RenderFontFace {
fn new(renderer: &ViewRenderer, face_id: zng_view_api::font::FontFaceId) -> Self {
RenderFontFace {
renderer: renderer.clone(),
face_id,
}
}
}
impl Drop for RenderFontFace {
fn drop(&mut self) {
let _ = self.renderer.delete_font_face(self.face_id);
}
}
struct RenderFont {
renderer: ViewRenderer,
synthesis: FontSynthesis,
font_id: zng_view_api::font::FontId,
}
impl RenderFont {
fn new(renderer: &ViewRenderer, synthesis: FontSynthesis, font_id: zng_view_api::font::FontId) -> RenderFont {
RenderFont {
renderer: renderer.clone(),
synthesis,
font_id,
}
}
}
impl Drop for RenderFont {
fn drop(&mut self) {
let _ = self.renderer.delete_font(self.font_id);
}
}
app_local! {
static GENERIC_FONTS_SV: GenericFontsService = GenericFontsService::new();
}
struct GenericFontsService {
serif: LangMap<FontName>,
sans_serif: LangMap<FontName>,
monospace: LangMap<FontName>,
cursive: LangMap<FontName>,
fantasy: LangMap<FontName>,
fallback: LangMap<FontName>,
system_ui: LangMap<FontNames>,
}
impl GenericFontsService {
fn new() -> Self {
fn default(name: impl Into<FontName>) -> LangMap<FontName> {
let mut f = LangMap::with_capacity(1);
f.insert(lang!(und), name.into());
f
}
let serif = "serif";
let sans_serif = "sans-serif";
let monospace = "monospace";
let cursive = "cursive";
let fantasy = "fantasy";
let fallback = if cfg!(windows) {
"Segoe UI Symbol"
} else if cfg!(target_os = "linux") {
"Standard Symbols PS"
} else {
"sans-serif"
};
let mut system_ui = LangMap::with_capacity(5);
if cfg!(windows) {
system_ui.insert(
lang!("zh-Hans"),
["Segoe UI", "Microsoft YaHei", "Segoe Ui Emoji", "sans-serif"].into(),
);
system_ui.insert(
lang!("zh-Hant"),
["Segoe UI", "Microsoft Jhenghei", "Segoe Ui Emoji", "sans-serif"].into(),
);
system_ui.insert(
lang!("ja"),
["Segoe UI", "Yu Gothic UI", "Meiryo UI", "Segoe Ui Emoji", "sans-serif"].into(),
);
system_ui.insert(
lang!("ko"),
["Segoe UI", "Malgun Gothic", "Dotom", "Segoe Ui Emoji", "sans-serif"].into(),
);
for lang in [
lang!("hi"),
lang!("bn"),
lang!("te"),
lang!("as"),
lang!("gu"),
lang!("kn"),
lang!("mr"),
lang!("ne"),
lang!("or"),
lang!("pa"),
lang!("si"),
] {
system_ui.insert(lang, ["Segoe UI", "Nirmala UI", "Mangal", "Segoe Ui Emoji", "sans-serif"].into());
}
system_ui.insert(lang!("am"), ["Segoe UI", "Nyala", "Ebrima", "Segoe Ui Emoji", "sans-serif"].into());
system_ui.insert(
lang!("km"),
["Segoe UI", "Khmer UI", "Leelawadee UI", "Segoe Ui Emoji", "sans-serif"].into(),
);
system_ui.insert(
lang!("lo"),
["Segoe UI", "lao UI", "Leelawadee UI", "Segoe Ui Emoji", "sans-serif"].into(),
);
system_ui.insert(lang!("th"), ["Segoe UI", "Leelawadee UI", "Segoe Ui Emoji", "sans-serif"].into());
for lang in [lang!("ml"), lang!("ta")] {
system_ui.insert(lang, ["Segoe UI", "Nirmala UI", "Segoe Ui Emoji", "sans-serif"].into());
}
system_ui.insert(lang!("my"), ["Segoe UI", "Myanmar Text", "Segoe Ui Emoji", "sans-serif"].into());
system_ui.insert(lang!(und), ["Segoe UI", "Segoe Ui Emoji", "sans-serif"].into());
} else if cfg!(target_os = "macos") {
system_ui.insert(
lang!("zh-Hans"),
["system-ui", "PingFang SC", "Hiragino Sans GB", "Apple Color Emoji", "sans-serif"].into(),
);
system_ui.insert(
lang!("zh-Hant"),
["system-ui", "PingFang TC", "Apple Color Emoji", "sans-serif"].into(),
);
system_ui.insert(
lang!("ja"),
[
"system-ui",
"Hiragino Sans",
"Hiragino Kaku Gothic ProN",
"Apple Color Emoji",
"sans-serif",
]
.into(),
);
system_ui.insert(
lang!("ko"),
["system-ui", "Apple SD Gothic Neo", "NanumGothic", "Apple Color Emoji", "sans-serif"].into(),
);
for lang in [lang!("hi"), lang!("mr"), lang!("ne")] {
system_ui.insert(
lang,
[
"system-ui",
"Kohinoor Devanagari",
"Devanagari Sangam MN",
"Apple Color Emoji",
"sans-serif",
]
.into(),
);
}
for lang in [lang!("bn"), lang!("as")] {
system_ui.insert(
lang,
[
"system-ui",
"Kohinoor Bangla",
"Bangla Sangam MN",
"Apple Color Emoji",
"sans-serif",
]
.into(),
);
}
system_ui.insert(
lang!("te"),
[
"system-ui",
"Kohinoor Telugu",
"Telugu Sangam MN",
"Apple Color Emoji",
"sans-serif",
]
.into(),
);
system_ui.insert(
lang!("gu"),
[
"system-ui",
"Kohinoor Gujarati",
"Gujarati Sangam MN",
"Apple Color Emoji",
"sans-serif",
]
.into(),
);
system_ui.insert(
lang!("kn"),
["system-ui", "Kannada Sangam MN", "Apple Color Emoji", "sans-serif"].into(),
);
system_ui.insert(
lang!("or"),
["system-ui", "Oriya Sangam MN", "Apple Color Emoji", "sans-serif"].into(),
);
system_ui.insert(
lang!("pa"),
["system-ui", "Mukta Mahee", "Gurmukhi Sangam MN", "Apple Color Emoji", "sans-serif"].into(),
);
system_ui.insert(
lang!("si"),
["system-ui", "Sinhala Sangam MN", "Apple Color Emoji", "sans-serif"].into(),
);
system_ui.insert(lang!("am"), ["system-ui", "Kefa", "Apple Color Emoji", "sans-serif"].into());
system_ui.insert(
lang!("km"),
["system-ui", "Khmer Sangam MN", "Apple Color Emoji", "sans-serif"].into(),
);
system_ui.insert(
lang!("lo"),
["system-ui", "Lao Sangam MN", "Apple Color Emoji", "sans-serif"].into(),
);
system_ui.insert(
lang!("th"),
["system-ui", "Thonburi", "Ayuthaya", "Apple Color Emoji", "sans-serif"].into(),
);
system_ui.insert(
lang!("my"),
["system-ui", "Myanmar Sangam MN", "Apple Color Emoji", "sans-serif"].into(),
);
system_ui.insert(
lang!("ml"),
["system-ui", "Malayalam Sangam MN", "Apple Color Emoji", "sans-serif"].into(),
);
system_ui.insert(
lang!("ta"),
["system-ui", "Kohinoor Tamil", "Tamil Sangam MN", "Apple Color Emoji", "sans-serif"].into(),
);
for lang in [lang!("ar"), lang!("fa"), lang!("ps")] {
system_ui.insert(lang, ["system-ui", "Geeza Pro", "Apple Color Emoji", "sans-serif"].into());
}
system_ui.insert(lang!("he"), ["system-ui", "Arial Hebrew", "Apple Color Emoji", "sans-serif"].into());
system_ui.insert(lang!("hy"), ["system-ui", "Mshtakan", "Apple Color Emoji", "sans-serif"].into());
system_ui.insert(
lang!("ka"),
["system-ui", "Helvetica Neue", "Apple Color Emoji", "sans-serif"].into(),
);
system_ui.insert(
lang!("ur"),
["system-ui", "SF Arabic", "Geeza Pro", "Apple Color Emoji", "sans-serif"].into(),
);
system_ui.insert(
lang!(und),
["system-ui", "Neue Helvetica", "Lucida Grande", "Apple Color Emoji", "sans-serif"].into(),
);
} else if cfg!(target_os = "linux") {
system_ui.insert(
lang!("zh-Hans"),
[
"system-ui",
"Ubuntu",
"Noto Sans CJK SC",
"Source Han Sans SC",
"Noto Color Emoji",
"sans-serif",
]
.into(),
);
system_ui.insert(
lang!("zh-Hant"),
[
"system-ui",
"Ubuntu",
"Noto Sans CJK TC",
"Source Han Sans TC",
"Noto Color Emoji",
"sans-serif",
]
.into(),
);
system_ui.insert(
lang!("ja"),
[
"system-ui",
"Ubuntu",
"Noto Sans CJK JP",
"Source Han Sans JP",
"Noto Color Emoji",
"sans-serif",
]
.into(),
);
system_ui.insert(
lang!("ko"),
[
"system-ui",
"Ubuntu",
"Noto Sans CJK KR",
"Source Han Sans KR",
"UnDotum",
"Noto Color Emoji",
"sans-serif",
]
.into(),
);
for lang in [lang!("hi"), lang!("mr"), lang!("ne")] {
system_ui.insert(
lang,
[
"system-ui",
"Ubuntu",
"Noto Sans Devanagari",
"Lohit Devanagari",
"Noto Color Emoji",
"sans-serif",
]
.into(),
);
}
for lang in [lang!("bn"), lang!("as")] {
system_ui.insert(
lang,
[
"system-ui",
"Ubuntu",
"Noto Sans Bengali",
"Lohit Bengali",
"Noto Color Emoji",
"sans-serif",
]
.into(),
);
}
system_ui.insert(
lang!("te"),
[
"system-ui",
"Ubuntu",
"Noto Sans Telugu",
"Lohit Telugu",
"Noto Color Emoji",
"sans-serif",
]
.into(),
);
system_ui.insert(
lang!("gu"),
[
"system-ui",
"Ubuntu",
"Noto Sans Gujarati",
"Lohit Gujarati",
"Noto Color Emoji",
"sans-serif",
]
.into(),
);
system_ui.insert(
lang!("kn"),
[
"system-ui",
"Ubuntu",
"Noto Sans Kannada",
"Lohit Kannada",
"Noto Color Emoji",
"sans-serif",
]
.into(),
);
system_ui.insert(
lang!("or"),
[
"system-ui",
"Ubuntu",
"Noto Sans Oriya",
"Lohit Odia",
"Noto Color Emoji",
"sans-serif",
]
.into(),
);
system_ui.insert(
lang!("pa"),
[
"system-ui",
"Ubuntu",
"Noto Sans Gurmukhi",
"Lohit Gurmukhi",
"Noto Color Emoji",
"sans-serif",
]
.into(),
);
system_ui.insert(
lang!("si"),
[
"system-ui",
"Ubuntu",
"Noto Sans Sinhala",
"LKLUG",
"Noto Color Emoji",
"sans-serif",
]
.into(),
);
system_ui.insert(
lang!("am"),
[
"system-ui",
"Ubuntu",
"Noto Sans Ethiopic",
"Abyssinica SIL",
"Noto Color Emoji",
"sans-serif",
]
.into(),
);
system_ui.insert(
lang!("km"),
[
"system-ui",
"Ubuntu",
"Noto Sans Khmer",
"Hanuman",
"Noto Color Emoji",
"sans-serif",
]
.into(),
);
system_ui.insert(
lang!("lo"),
[
"system-ui",
"Ubuntu",
"Noto Sans Lao",
"Phetsarath OT",
"Noto Color Emoji",
"sans-serif",
]
.into(),
);
system_ui.insert(
lang!("th"),
[
"system-ui",
"Ubuntu",
"Noto Sans Thai",
"Kinnari",
"Garuda",
"Noto Color Emoji",
"sans-serif",
]
.into(),
);
system_ui.insert(
lang!("my"),
[
"system-ui",
"Ubuntu",
"Noto Sans Myanmar",
"Padauk",
"Noto Color Emoji",
"sans-serif",
]
.into(),
);
system_ui.insert(
lang!("ml"),
[
"system-ui",
"Ubuntu",
"Noto Sans Malayalam",
"Lohit Malayalam",
"Noto Color Emoji",
"sans-serif",
]
.into(),
);
system_ui.insert(
lang!("ta"),
[
"system-ui",
"Ubuntu",
"Noto Sans Tamil",
"Lohit Tamil",
"Noto Color Emoji",
"sans-serif",
]
.into(),
);
for lang in [lang!("ar"), lang!("fa"), lang!("ps"), lang!("ur")] {
system_ui.insert(lang, ["system-ui", "Noto Sans Arabic", "Noto Color Emoji", "sans-serif"].into());
}
system_ui.insert(
lang!("he"),
["system-ui", "Noto Sans Hebrew", "Noto Color Emoji", "sans-serif"].into(),
);
system_ui.insert(
lang!("hy"),
["system-ui", "Noto Sans Armenian", "Noto Color Emoji", "sans-serif"].into(),
);
system_ui.insert(lang!("ka"), ["system-ui", "DejaVu Sans", "Noto Color Emoji", "sans-serif"].into());
system_ui.insert(
lang!("ur"),
[
"system-ui",
"Noto Naskh Arabic",
"Noto Sans Arabic",
"Noto Color Emoji",
"sans-serif",
]
.into(),
);
system_ui.insert(
lang!(und),
["system-ui", "Ubuntu", "Droid Sans", "Noto Sans", "Noto Color Emoji", "sans-serif"].into(),
);
} else {
system_ui.insert(lang!(und), ["system-ui", "sans-serif"].into());
}
GenericFontsService {
serif: default(serif),
sans_serif: default(sans_serif),
monospace: default(monospace),
cursive: default(cursive),
fantasy: default(fantasy),
system_ui,
fallback: default(fallback),
}
}
}
#[non_exhaustive]
pub struct GenericFonts {}
macro_rules! impl_fallback_accessors {
($($name:ident=$name_str:tt),+ $(,)?) => {$($crate::paste! {
#[doc = "Gets the *"$name_str "* font for the given language."]
#[doc = "Note that the returned name can still be the generic `\""$name_str "\"`, this delegates the resolution to the operating system."]
pub fn $name(&self, lang: &Lang) -> FontName {
GENERIC_FONTS_SV.read().$name.get(lang).unwrap().clone()
}
#[doc = "Sets the *"$name_str "* font for the given language."]
pub fn [<set_ $name>]<F: Into<FontName>>(&self, lang: Lang, font_name: F) {
self.[<set_ $name _impl>](lang, font_name.into());
}
fn [<set_ $name _impl>](&self, lang: Lang, font_name: FontName) {
UPDATES.once_update("GenericFonts.set", move || {
GENERIC_FONTS_SV.write().$name.insert(lang.clone(), font_name);
FONT_CHANGED_EVENT.notify(FontChangedArgs::now(FontChange::GenericFont(FontName::$name(), lang)));
});
}
})+};
}
impl GenericFonts {
#[rustfmt::skip] impl_fallback_accessors! {
serif="serif", sans_serif="sans-serif", monospace="monospace", cursive="cursive", fantasy="fantasy"
}
pub fn system_ui(&self, lang: &Lang) -> FontNames {
GENERIC_FONTS_SV.read().system_ui.get(lang).unwrap().clone()
}
pub fn set_system_ui(&self, lang: Lang, font_names: impl Into<FontNames>) {
self.set_system_ui_impl(lang, font_names.into())
}
fn set_system_ui_impl(&self, lang: Lang, font_names: FontNames) {
UPDATES.once_update("GenericFonts.set_system_ui", move || {
GENERIC_FONTS_SV.write().system_ui.insert(lang.clone(), font_names);
FONT_CHANGED_EVENT.notify(FontChangedArgs::now(FontChange::GenericFont(FontName::system_ui(), lang)));
});
}
pub fn fallback(&self, lang: &Lang) -> FontName {
GENERIC_FONTS_SV.read().fallback.get(lang).unwrap().clone()
}
pub fn set_fallback<F: Into<FontName>>(&self, lang: Lang, font_name: F) {
self.set_fallback_impl(lang, font_name.into());
}
fn set_fallback_impl(&self, lang: Lang, font_name: FontName) {
UPDATES.once_update("GenericFonts.set", move || {
GENERIC_FONTS_SV.write().fallback.insert(lang.clone(), font_name);
FONT_CHANGED_EVENT.notify(FontChangedArgs::now(FontChange::Fallback(lang)));
});
}
pub fn resolve(&self, name: &FontName, lang: &Lang) -> Option<FontName> {
match &**name {
"serif" => Some(self.serif(lang)),
"sans-serif" => Some(self.sans_serif(lang)),
"monospace" => Some(self.monospace(lang)),
"cursive" => Some(self.cursive(lang)),
"fantasy" => Some(self.fantasy(lang)),
_ => None,
}
}
pub fn resolve_list(&self, names: &[FontName], lang: &Lang) -> Option<FontNames> {
if names
.iter()
.any(|n| ["system-ui", "serif", "sans-serif", "monospace", "cursive", "fantasy"].contains(&&**n))
{
let mut r = FontNames(Vec::with_capacity(names.len()));
for name in names {
match self.resolve(name, lang) {
Some(n) => r.push(n),
None => {
if name == "system-ui" {
r.extend(self.system_ui(lang));
} else {
r.push(name.clone())
}
}
}
}
Some(r)
} else {
None
}
}
}
#[cfg(not(any(target_arch = "wasm32", target_os = "android")))]
pub(crate) enum WeakFontBytes {
Ipc(WeakIpcBytes),
Arc(std::sync::Weak<Vec<u8>>),
Static(&'static [u8]),
Mmap(std::sync::Weak<SystemFontBytes>),
}
#[cfg(not(any(target_arch = "wasm32", target_os = "android")))]
impl WeakFontBytes {
pub(crate) fn upgrade(&self) -> Option<FontBytes> {
match self {
WeakFontBytes::Ipc(weak) => Some(FontBytes(FontBytesImpl::Ipc(weak.upgrade()?))),
WeakFontBytes::Arc(weak) => Some(FontBytes(FontBytesImpl::Arc(weak.upgrade()?))),
WeakFontBytes::Static(b) => Some(FontBytes(FontBytesImpl::Static(b))),
WeakFontBytes::Mmap(weak) => Some(FontBytes(FontBytesImpl::System(weak.upgrade()?))),
}
}
pub(crate) fn strong_count(&self) -> usize {
match self {
WeakFontBytes::Ipc(weak) => weak.strong_count(),
WeakFontBytes::Arc(weak) => weak.strong_count(),
WeakFontBytes::Static(_) => 1,
WeakFontBytes::Mmap(weak) => weak.strong_count(),
}
}
}
struct SystemFontBytes {
path: std::path::PathBuf,
mmap: IpcBytes,
}
#[derive(Clone)]
enum FontBytesImpl {
Ipc(IpcBytes),
Arc(Arc<Vec<u8>>),
Static(&'static [u8]),
System(Arc<SystemFontBytes>),
}
#[derive(Clone)]
pub struct FontBytes(FontBytesImpl);
impl FontBytes {
pub fn from_ipc(bytes: IpcBytes) -> Self {
Self(FontBytesImpl::Ipc(bytes))
}
pub fn from_vec(bytes: Vec<u8>) -> io::Result<Self> {
Ok(Self(FontBytesImpl::Ipc(IpcBytes::from_vec_blocking(bytes)?)))
}
pub fn from_static(bytes: &'static [u8]) -> Self {
Self(FontBytesImpl::Static(bytes))
}
pub fn from_arc(bytes: Arc<Vec<u8>>) -> Self {
Self(FontBytesImpl::Arc(bytes))
}
pub fn from_file(path: PathBuf) -> io::Result<Self> {
let path = dunce::canonicalize(path)?;
#[cfg(windows)]
{
use windows::Win32::{Foundation::MAX_PATH, System::SystemInformation::GetSystemWindowsDirectoryW};
let mut buffer = [0u16; MAX_PATH as usize];
let len = unsafe { GetSystemWindowsDirectoryW(Some(&mut buffer)) };
let fonts_dir = String::from_utf16_lossy(&buffer[..len as usize]);
if path.starts_with(fonts_dir) {
return unsafe { load_from_system(path) };
}
}
#[cfg(target_os = "macos")]
if path.starts_with("/System/Library/Fonts/") || path.starts_with("/Library/Fonts/") {
return unsafe { load_from_system(path) };
}
#[cfg(target_os = "android")]
if path.starts_with("/system/fonts/") || path.starts_with("/system/font/") || path.starts_with("/system/product/fonts/") {
return unsafe { load_from_system(path) };
}
#[cfg(unix)]
if path.starts_with("/usr/share/fonts/") {
return unsafe { load_from_system(path) };
}
#[cfg(ipc)]
unsafe fn load_from_system(path: PathBuf) -> io::Result<FontBytes> {
let mmap = unsafe { IpcBytes::open_memmap_blocking(path.clone(), None) }?;
Ok(FontBytes(FontBytesImpl::System(Arc::new(SystemFontBytes { path, mmap }))))
}
#[cfg(all(not(ipc), not(target_arch = "wasm32")))]
unsafe fn load_from_system(path: PathBuf) -> io::Result<FontBytes> {
let mmap = IpcBytes::from_path_blocking(&path)?;
Ok(FontBytes(FontBytesImpl::System(Arc::new(SystemFontBytes { path, mmap }))))
}
Ok(Self(FontBytesImpl::Ipc(IpcBytes::from_path_blocking(&path)?)))
}
#[cfg(ipc)]
pub unsafe fn from_file_mmap(path: PathBuf) -> std::io::Result<Self> {
let ipc = unsafe { IpcBytes::open_memmap_blocking(path, None) }?;
Ok(Self(FontBytesImpl::Ipc(ipc)))
}
#[cfg(ipc)]
pub fn mmap_path(&self) -> Option<&std::path::Path> {
if let FontBytesImpl::System(m) = &self.0 {
Some(&m.path)
} else {
None
}
}
pub fn to_ipc(&self) -> io::Result<IpcFontBytes> {
Ok(if let FontBytesImpl::System(m) = &self.0 {
IpcFontBytes::System(m.path.clone())
} else {
IpcFontBytes::Bytes(self.to_ipc_bytes()?)
})
}
pub fn to_ipc_bytes(&self) -> io::Result<IpcBytes> {
match &self.0 {
FontBytesImpl::Ipc(b) => Ok(b.clone()),
FontBytesImpl::Arc(b) => IpcBytes::from_slice_blocking(b),
FontBytesImpl::Static(b) => IpcBytes::from_slice_blocking(b),
FontBytesImpl::System(m) => IpcBytes::from_slice_blocking(&m.mmap[..]),
}
}
#[cfg(not(any(target_arch = "wasm32", target_os = "android")))]
pub(crate) fn downgrade(&self) -> WeakFontBytes {
match &self.0 {
FontBytesImpl::Ipc(ipc) => WeakFontBytes::Ipc(ipc.downgrade()),
FontBytesImpl::Arc(arc) => WeakFontBytes::Arc(Arc::downgrade(arc)),
FontBytesImpl::Static(b) => WeakFontBytes::Static(b),
FontBytesImpl::System(arc) => WeakFontBytes::Mmap(Arc::downgrade(arc)),
}
}
}
impl std::ops::Deref for FontBytes {
type Target = [u8];
fn deref(&self) -> &Self::Target {
match &self.0 {
FontBytesImpl::Ipc(b) => &b[..],
FontBytesImpl::Arc(b) => &b[..],
FontBytesImpl::Static(b) => b,
FontBytesImpl::System(m) => &m.mmap[..],
}
}
}
impl fmt::Debug for FontBytes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut b = f.debug_struct("FontBytes");
b.field(
".kind",
&match &self.0 {
FontBytesImpl::Ipc(_) => "IpcBytes",
FontBytesImpl::Arc(_) => "Arc",
FontBytesImpl::Static(_) => "Static",
FontBytesImpl::System(_) => "Mmap",
},
);
b.field(".len", &(self.len() as u64).bytes());
if let FontBytesImpl::System(m) = &self.0 {
b.field(".path", &m.path);
}
b.finish()
}
}
#[derive(Debug, Clone)]
enum FontSource {
File(PathBuf, u32),
Memory(FontBytes, u32),
Alias(FontName),
}
#[derive(Debug, Clone)]
pub struct CustomFont {
name: FontName,
source: FontSource,
stretch: FontStretch,
style: FontStyle,
weight: FontWeight,
}
impl CustomFont {
pub fn from_file<N: Into<FontName>, P: Into<PathBuf>>(name: N, path: P, font_index: u32) -> Self {
CustomFont {
name: name.into(),
source: FontSource::File(path.into(), font_index),
stretch: FontStretch::NORMAL,
style: FontStyle::Normal,
weight: FontWeight::NORMAL,
}
}
pub fn from_bytes<N: Into<FontName>>(name: N, data: FontBytes, font_index: u32) -> Self {
CustomFont {
name: name.into(),
source: FontSource::Memory(data, font_index),
stretch: FontStretch::NORMAL,
style: FontStyle::Normal,
weight: FontWeight::NORMAL,
}
}
pub fn from_other<N: Into<FontName>, O: Into<FontName>>(name: N, other_font: O) -> Self {
CustomFont {
name: name.into(),
source: FontSource::Alias(other_font.into()),
stretch: FontStretch::NORMAL,
style: FontStyle::Normal,
weight: FontWeight::NORMAL,
}
}
pub fn stretch(mut self, stretch: FontStretch) -> Self {
self.stretch = stretch;
self
}
pub fn style(mut self, style: FontStyle) -> Self {
self.style = style;
self
}
pub fn weight(mut self, weight: FontWeight) -> Self {
self.weight = weight;
self
}
}
#[derive(Clone, Copy, serde::Serialize, serde::Deserialize, Transitionable)]
#[serde(transparent)]
pub struct FontStretch(pub f32);
impl fmt::Debug for FontStretch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = self.name();
if name.is_empty() {
f.debug_tuple("FontStretch").field(&self.0).finish()
} else {
if f.alternate() {
write!(f, "FontStretch::")?;
}
write!(f, "{name}")
}
}
}
impl PartialOrd for FontStretch {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for FontStretch {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
about_eq_ord(self.0, other.0, EQ_GRANULARITY)
}
}
impl PartialEq for FontStretch {
fn eq(&self, other: &Self) -> bool {
about_eq(self.0, other.0, EQ_GRANULARITY)
}
}
impl Eq for FontStretch {}
impl std::hash::Hash for FontStretch {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
about_eq_hash(self.0, EQ_GRANULARITY, state)
}
}
impl Default for FontStretch {
fn default() -> FontStretch {
FontStretch::NORMAL
}
}
impl FontStretch {
pub const ULTRA_CONDENSED: FontStretch = FontStretch(0.5);
pub const EXTRA_CONDENSED: FontStretch = FontStretch(0.625);
pub const CONDENSED: FontStretch = FontStretch(0.75);
pub const SEMI_CONDENSED: FontStretch = FontStretch(0.875);
pub const NORMAL: FontStretch = FontStretch(1.0);
pub const SEMI_EXPANDED: FontStretch = FontStretch(1.125);
pub const EXPANDED: FontStretch = FontStretch(1.25);
pub const EXTRA_EXPANDED: FontStretch = FontStretch(1.5);
pub const ULTRA_EXPANDED: FontStretch = FontStretch(2.0);
pub fn name(self) -> &'static str {
macro_rules! name {
($($CONST:ident;)+) => {$(
if self == Self::$CONST {
return stringify!($CONST);
}
)+}
}
name! {
ULTRA_CONDENSED;
EXTRA_CONDENSED;
CONDENSED;
SEMI_CONDENSED;
NORMAL;
SEMI_EXPANDED;
EXPANDED;
EXTRA_EXPANDED;
ULTRA_EXPANDED;
}
""
}
}
impl_from_and_into_var! {
fn from(fct: Factor) -> FontStretch {
FontStretch(fct.0)
}
fn from(pct: FactorPercent) -> FontStretch {
FontStretch(pct.fct().0)
}
fn from(fct: f32) -> FontStretch {
FontStretch(fct)
}
}
impl From<skrifa::attribute::Stretch> for FontStretch {
fn from(value: skrifa::attribute::Stretch) -> Self {
FontStretch(value.ratio())
}
}
impl From<FontStretch> for skrifa::attribute::Stretch {
fn from(value: FontStretch) -> Self {
skrifa::attribute::Stretch::new(value.0)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
pub enum FontStyle {
#[default]
Normal,
Italic,
Oblique,
}
impl fmt::Debug for FontStyle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
write!(f, "FontStyle::")?;
}
match self {
Self::Normal => write!(f, "Normal"),
Self::Italic => write!(f, "Italic"),
Self::Oblique => write!(f, "Oblique"),
}
}
}
impl From<skrifa::attribute::Style> for FontStyle {
fn from(value: skrifa::attribute::Style) -> Self {
use skrifa::attribute::Style::*;
match value {
Normal => FontStyle::Normal,
Italic => FontStyle::Italic,
Oblique(_) => FontStyle::Oblique,
}
}
}
impl From<FontStyle> for skrifa::attribute::Style {
fn from(value: FontStyle) -> Self {
match value {
FontStyle::Normal => Self::Normal,
FontStyle::Italic => Self::Italic,
FontStyle::Oblique => Self::Oblique(None),
}
}
}
#[derive(Clone, Copy, Transitionable, serde::Serialize, serde::Deserialize)]
pub struct FontWeight(pub f32);
impl Default for FontWeight {
fn default() -> FontWeight {
FontWeight::NORMAL
}
}
impl fmt::Debug for FontWeight {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = self.name();
if name.is_empty() {
f.debug_tuple("FontWeight").field(&self.0).finish()
} else {
if f.alternate() {
write!(f, "FontWeight::")?;
}
write!(f, "{name}")
}
}
}
impl PartialOrd for FontWeight {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for FontWeight {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
about_eq_ord(self.0, other.0, EQ_GRANULARITY_100)
}
}
impl PartialEq for FontWeight {
fn eq(&self, other: &Self) -> bool {
about_eq(self.0, other.0, EQ_GRANULARITY_100)
}
}
impl Eq for FontWeight {}
impl std::hash::Hash for FontWeight {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
about_eq_hash(self.0, EQ_GRANULARITY_100, state)
}
}
impl FontWeight {
pub const THIN: FontWeight = FontWeight(100.0);
pub const EXTRA_LIGHT: FontWeight = FontWeight(200.0);
pub const LIGHT: FontWeight = FontWeight(300.0);
pub const NORMAL: FontWeight = FontWeight(400.0);
pub const MEDIUM: FontWeight = FontWeight(500.0);
pub const SEMIBOLD: FontWeight = FontWeight(600.0);
pub const BOLD: FontWeight = FontWeight(700.0);
pub const EXTRA_BOLD: FontWeight = FontWeight(800.0);
pub const BLACK: FontWeight = FontWeight(900.0);
pub fn name(self) -> &'static str {
macro_rules! name {
($($CONST:ident;)+) => {$(
if self == Self::$CONST {
return stringify!($CONST);
}
)+}
}
name! {
THIN;
EXTRA_LIGHT;
LIGHT;
NORMAL;
MEDIUM;
SEMIBOLD;
BOLD;
EXTRA_BOLD;
BLACK;
}
""
}
}
impl_from_and_into_var! {
fn from(weight: u32) -> FontWeight {
FontWeight(weight as f32)
}
fn from(weight: f32) -> FontWeight {
FontWeight(weight)
}
}
impl From<skrifa::attribute::Weight> for FontWeight {
fn from(value: skrifa::attribute::Weight) -> Self {
FontWeight(value.value())
}
}
impl From<FontWeight> for skrifa::attribute::Weight {
fn from(value: FontWeight) -> Self {
skrifa::attribute::Weight::new(value.0)
}
}
#[derive(Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum LineBreak {
Auto,
Loose,
Normal,
Strict,
Anywhere,
}
impl Default for LineBreak {
fn default() -> Self {
LineBreak::Auto
}
}
impl fmt::Debug for LineBreak {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
write!(f, "LineBreak::")?;
}
match self {
LineBreak::Auto => write!(f, "Auto"),
LineBreak::Loose => write!(f, "Loose"),
LineBreak::Normal => write!(f, "Normal"),
LineBreak::Strict => write!(f, "Strict"),
LineBreak::Anywhere => write!(f, "Anywhere"),
}
}
}
#[derive(Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum ParagraphBreak {
#[default]
None,
Line,
}
impl fmt::Debug for ParagraphBreak {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
write!(f, "ParagraphBreak::")?;
}
match self {
ParagraphBreak::None => write!(f, "None"),
ParagraphBreak::Line => write!(f, "Line"),
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Hyphens {
None,
Manual,
Auto,
}
impl Default for Hyphens {
fn default() -> Self {
Hyphens::Auto
}
}
impl fmt::Debug for Hyphens {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
write!(f, "Hyphens::")?;
}
match self {
Hyphens::None => write!(f, "None"),
Hyphens::Manual => write!(f, "Manual"),
Hyphens::Auto => write!(f, "Auto"),
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum WordBreak {
Normal,
BreakAll,
KeepAll,
}
impl Default for WordBreak {
fn default() -> Self {
WordBreak::Normal
}
}
impl fmt::Debug for WordBreak {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
write!(f, "WordBreak::")?;
}
match self {
WordBreak::Normal => write!(f, "Normal"),
WordBreak::BreakAll => write!(f, "BreakAll"),
WordBreak::KeepAll => write!(f, "KeepAll"),
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Justify {
Auto,
InterWord,
InterLetter,
}
impl Default for Justify {
fn default() -> Self {
Justify::Auto
}
}
impl Justify {
pub fn resolve(self, lang: &Lang) -> Self {
match self {
Self::Auto => match lang.language.as_str() {
"zh" | "ja" | "ko" => Self::InterLetter,
_ => Self::InterWord,
},
m => m,
}
}
}
impl fmt::Debug for Justify {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
write!(f, "Justify::")?;
}
match self {
Justify::Auto => write!(f, "Auto"),
Justify::InterWord => write!(f, "InterWord"),
Justify::InterLetter => write!(f, "InterLetter"),
}
}
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub struct FontMetrics {
pub ascent: Px,
pub descent: Px,
pub line_gap: Px,
pub underline_position: Px,
pub underline_thickness: Px,
pub cap_height: Px,
pub x_height: Px,
pub bounds: PxRect,
units_per_em: u16,
}
impl FontMetrics {
pub fn line_height(&self) -> Px {
self.ascent - self.descent + self.line_gap
}
fn new(f: &skrifa::FontRef, size: Px) -> Self {
let m = f.metrics(skrifa::instance::Size::new(size.0 as f32), skrifa::instance::LocationRef::default());
let u = m.underline.unwrap_or_default();
let b = m.bounds.unwrap_or_default();
let scale = size.0 as f32 / m.units_per_em as f32;
let line_gap = self::line_gap(f) as f32 * scale;
fn f32_to_px(v: f32) -> Px {
euclid::point2::<f32, ()>(v, v).cast().x
}
Self {
ascent: f32_to_px(m.ascent),
descent: f32_to_px(m.descent),
line_gap: f32_to_px(line_gap),
underline_position: f32_to_px(u.offset),
underline_thickness: f32_to_px(u.thickness),
cap_height: f32_to_px(m.cap_height.unwrap_or(0.0)),
x_height: f32_to_px(m.x_height.unwrap_or(0.0)),
bounds: euclid::rect(b.x_min, b.y_min, b.x_max - b.x_min, b.y_max - b.y_min).cast(),
units_per_em: m.units_per_em,
}
}
fn empty() -> Self {
Self {
ascent: Px(0),
descent: Px(0),
line_gap: Px(0),
underline_position: Px(0),
underline_thickness: Px(0),
cap_height: Px(0),
x_height: Px(0),
bounds: euclid::Rect::zero(),
units_per_em: 0,
}
}
}
fn line_gap(f: &skrifa::FontRef) -> i16 {
use read_fonts::TableProvider as _;
if let Ok(os2) = f.os2() {
let use_typographic_metrics = os2.version() >= 4
&& os2
.fs_selection()
.contains(read_fonts::tables::os2::SelectionFlags::USE_TYPO_METRICS);
if use_typographic_metrics {
return os2.s_typo_line_gap();
}
}
if let Ok(hrea) = f.hhea() {
if hrea.ascender().to_i16() == 0
&& hrea.descender().to_i16() == 0
&& let Ok(os2) = f.os2()
{
return if os2.s_typo_ascender() != 0 || os2.s_typo_descender() != 0 {
return os2.s_typo_line_gap();
} else {
0
};
}
return hrea.line_gap().to_i16();
}
0
}
#[derive(Clone)]
pub enum TextTransformFn {
None,
Uppercase,
Lowercase,
Custom(Arc<dyn Fn(&Txt) -> Cow<Txt> + Send + Sync>),
}
impl TextTransformFn {
pub fn transform<'t>(&self, text: &'t Txt) -> Cow<'t, Txt> {
match self {
TextTransformFn::None => Cow::Borrowed(text),
TextTransformFn::Uppercase => {
if text.chars().any(|c| !c.is_uppercase()) {
Cow::Owned(text.to_uppercase().into())
} else {
Cow::Borrowed(text)
}
}
TextTransformFn::Lowercase => {
if text.chars().any(|c| !c.is_lowercase()) {
Cow::Owned(text.to_lowercase().into())
} else {
Cow::Borrowed(text)
}
}
TextTransformFn::Custom(fn_) => fn_(text),
}
}
pub fn custom(fn_: impl Fn(&Txt) -> Cow<Txt> + Send + Sync + 'static) -> Self {
TextTransformFn::Custom(Arc::new(fn_))
}
}
impl fmt::Debug for TextTransformFn {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if f.alternate() {
write!(f, "TextTransformFn::")?;
}
match self {
TextTransformFn::None => write!(f, "None"),
TextTransformFn::Uppercase => write!(f, "Uppercase"),
TextTransformFn::Lowercase => write!(f, "Lowercase"),
TextTransformFn::Custom(_) => write!(f, "Custom"),
}
}
}
impl PartialEq for TextTransformFn {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Custom(l0), Self::Custom(r0)) => Arc::ptr_eq(l0, r0),
_ => core::mem::discriminant(self) == core::mem::discriminant(other),
}
}
}
#[derive(Default, Copy, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum WhiteSpace {
#[default]
Preserve,
Merge,
MergeParagraph,
MergeAll,
}
impl WhiteSpace {
pub fn transform(self, text: &Txt) -> Cow<'_, Txt> {
match self {
WhiteSpace::Preserve => Cow::Borrowed(text),
WhiteSpace::Merge => {
let mut prev_i = 0;
for line in text.split_inclusive('\n') {
let line_exclusive = line.trim_end_matches('\n').trim_end_matches('\r');
let line_trim = line_exclusive.trim();
let mut merge = line_trim.len() != line_exclusive.len() || line_trim.is_empty();
if !merge {
let mut prev_is_space = true; for c in line.chars() {
let is_space = c.is_whitespace();
if prev_is_space && is_space {
merge = true;
break;
}
prev_is_space = is_space;
}
}
if !merge {
prev_i += line.len();
continue;
}
let mut out = String::with_capacity(text.len() - 1);
out.push_str(&text[..prev_i]);
let mut chars = text[prev_i..].chars();
let mut prev_is_space = true;
let mut prev_is_break = true;
while let Some(c) = chars.next() {
if c == '\r'
&& let Some(nc) = chars.next()
{
if nc == '\n' {
if !prev_is_break && !out.is_empty() {
out.push('\n');
}
prev_is_break = true;
prev_is_space = true;
} else {
out.push(c);
out.push(nc);
prev_is_break = false;
prev_is_space = nc.is_whitespace();
}
} else if c == '\n' {
if !prev_is_break && !out.is_empty() {
out.push('\n');
}
prev_is_break = true;
prev_is_space = true;
} else if c.is_whitespace() {
if prev_is_space {
continue;
}
out.push(' ');
prev_is_space = true;
} else {
out.push(c);
prev_is_space = false;
prev_is_break = false;
}
}
if let Some((i, c)) = out.char_indices().rev().find(|(_, c)| !c.is_whitespace()) {
out.truncate(i + c.len_utf8());
}
return Cow::Owned(out.into());
}
Cow::Borrowed(text)
}
WhiteSpace::MergeParagraph => {
let mut merge = text.contains('\n') || text.chars().last().unwrap_or('\0').is_whitespace();
if !merge {
let mut prev_is_space = true;
for c in text.chars() {
let is_space = c.is_whitespace();
if prev_is_space && is_space {
merge = true;
break;
}
prev_is_space = is_space;
}
}
if merge {
let mut out = String::with_capacity(text.len());
let mut prev_is_break = false;
for line in text.lines() {
let line = line.trim();
let is_break = line.is_empty();
if !prev_is_break && is_break && !out.is_empty() {
out.push('\n');
}
if !prev_is_break && !is_break && !out.is_empty() {
out.push(' ');
}
prev_is_break = is_break;
let mut prev_is_space = false;
for c in line.chars() {
let is_space = c.is_whitespace();
if is_space {
if !prev_is_space {
out.push(' ');
}
} else {
out.push(c);
}
prev_is_space = is_space;
}
}
if let Some((i, c)) = out.char_indices().rev().find(|(_, c)| !c.is_whitespace()) {
out.truncate(i + c.len_utf8());
}
return Cow::Owned(out.into());
}
Cow::Borrowed(text)
}
WhiteSpace::MergeAll => {
let mut prev_i = 0;
let mut prev_is_space = true; for (i, c) in text.char_indices() {
let is_space = c.is_whitespace();
if prev_is_space && is_space || c == '\n' {
if !prev_is_space {
debug_assert_eq!(c, '\n');
prev_i += c.len_utf8();
prev_is_space = true;
}
let mut out = String::with_capacity(text.len() - 1);
out.push_str(&text[..prev_i]);
if !out.is_empty() {
out.push(' ');
}
for c in text[(i + c.len_utf8())..].chars() {
let is_space = c.is_whitespace();
if prev_is_space && is_space {
continue;
}
out.push(if is_space { ' ' } else { c });
prev_is_space = is_space;
}
if let Some((i, c)) = out.char_indices().rev().find(|(_, c)| !c.is_whitespace()) {
out.truncate(i + c.len_utf8());
}
return Cow::Owned(out.into());
}
prev_i = i;
prev_is_space = is_space;
}
let out = text.trim_end();
if out.len() != text.len() {
return Cow::Owned(Txt::from_str(out));
}
Cow::Borrowed(text)
}
}
}
}
impl fmt::Debug for WhiteSpace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
write!(f, "WhiteSpace::")?;
}
match self {
WhiteSpace::Preserve => write!(f, "Preserve"),
WhiteSpace::Merge => write!(f, "Merge"),
WhiteSpace::MergeAll => write!(f, "MergeAll"),
WhiteSpace::MergeParagraph => write!(f, "MergeParagraph"),
}
}
}
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
pub struct CaretIndex {
pub index: usize,
pub line: usize,
}
impl PartialEq for CaretIndex {
fn eq(&self, other: &Self) -> bool {
self.index == other.index
}
}
impl Eq for CaretIndex {}
impl CaretIndex {
pub const ZERO: CaretIndex = CaretIndex { index: 0, line: 0 };
}
impl PartialOrd for CaretIndex {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for CaretIndex {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.index.cmp(&other.index)
}
}
impl_from_and_into_var! {
fn from(index: usize) -> CaretIndex {
CaretIndex { index, line: 0 }
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum FontLoadingError {
UnknownFormat,
NoSuchFontInCollection,
Parse(read_fonts::ReadError),
NoFilesystem,
Io(Arc<std::io::Error>),
}
impl PartialEq for FontLoadingError {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Io(l0), Self::Io(r0)) => Arc::ptr_eq(l0, r0),
_ => core::mem::discriminant(self) == core::mem::discriminant(other),
}
}
}
impl From<std::io::Error> for FontLoadingError {
fn from(error: std::io::Error) -> FontLoadingError {
Self::Io(Arc::new(error))
}
}
impl fmt::Display for FontLoadingError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnknownFormat => write!(f, "unknown format"),
Self::NoSuchFontInCollection => write!(f, "no such font in the collection"),
Self::NoFilesystem => write!(f, "no filesystem present"),
Self::Parse(e) => fmt::Display::fmt(e, f),
Self::Io(e) => fmt::Display::fmt(e, f),
}
}
}
impl std::error::Error for FontLoadingError {
fn cause(&self) -> Option<&dyn std::error::Error> {
match self {
FontLoadingError::Parse(e) => Some(e),
FontLoadingError::Io(e) => Some(e),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use zng_app::APP;
use super::*;
#[test]
fn generic_fonts_default() {
let _app = APP.minimal().run_headless(false);
assert_eq!(FontName::sans_serif(), GenericFonts {}.sans_serif(&lang!(und)))
}
#[test]
fn generic_fonts_fallback() {
let _app = APP.minimal().run_headless(false);
assert_eq!(FontName::sans_serif(), GenericFonts {}.sans_serif(&lang!(en_US)));
assert_eq!(FontName::sans_serif(), GenericFonts {}.sans_serif(&lang!(es)));
}
#[test]
fn generic_fonts_get1() {
let mut app = APP.minimal().run_headless(false);
GenericFonts {}.set_sans_serif(lang!(en_US), "Test Value");
app.update(false).assert_wait();
assert_eq!(&GenericFonts {}.sans_serif(&lang!("en-US")), "Test Value");
assert_eq!(&GenericFonts {}.sans_serif(&lang!("en")), "Test Value");
}
#[test]
fn generic_fonts_get2() {
let mut app = APP.minimal().run_headless(false);
GenericFonts {}.set_sans_serif(lang!(en), "Test Value");
app.update(false).assert_wait();
assert_eq!(&GenericFonts {}.sans_serif(&lang!("en-US")), "Test Value");
assert_eq!(&GenericFonts {}.sans_serif(&lang!("en")), "Test Value");
}
#[test]
fn generic_fonts_get_best() {
let mut app = APP.minimal().run_headless(false);
GenericFonts {}.set_sans_serif(lang!(en), "Test Value");
GenericFonts {}.set_sans_serif(lang!(en_US), "Best");
app.update(false).assert_wait();
assert_eq!(&GenericFonts {}.sans_serif(&lang!("en-US")), "Best");
assert_eq!(&GenericFonts {}.sans_serif(&lang!("en")), "Test Value");
assert_eq!(&GenericFonts {}.sans_serif(&lang!("und")), "sans-serif");
}
#[test]
fn generic_fonts_get_no_lang_match() {
let mut app = APP.minimal().run_headless(false);
GenericFonts {}.set_sans_serif(lang!(es_US), "Test Value");
app.update(false).assert_wait();
assert_eq!(&GenericFonts {}.sans_serif(&lang!("en-US")), "sans-serif");
assert_eq!(&GenericFonts {}.sans_serif(&lang!("es")), "Test Value");
}
#[test]
fn white_space_merge() {
macro_rules! test {
($input:tt, $output:tt) => {
let input = Txt::from($input);
let output = WhiteSpace::Merge.transform(&input);
assert_eq!($output, output.as_str());
let input = input.replace('\n', "\r\n");
let output = WhiteSpace::Merge.transform(&Txt::from(input)).replace("\r\n", "\n");
assert_eq!($output, output.as_str());
};
}
test!("a b\n\nc", "a b\nc");
test!("a b\nc", "a b\nc");
test!(" a b\nc\n \n", "a b\nc");
test!(" \n a b\nc", "a b\nc");
test!("a\n \nb", "a\nb");
}
#[test]
fn white_space_merge_paragraph() {
macro_rules! test {
($input:tt, $output:tt) => {
let input = Txt::from($input);
let output = WhiteSpace::MergeParagraph.transform(&input);
assert_eq!($output, output.as_str());
let input = input.replace('\n', "\r\n");
let output = WhiteSpace::MergeParagraph.transform(&Txt::from(input)).replace("\r\n", "\n");
assert_eq!($output, output.as_str());
};
}
test!("a b\n\nc", "a b\nc");
test!("a b\nc", "a b c");
test!(" a b\nc\n \n", "a b c");
test!(" \n a b\nc", "a b c");
test!("a\n \nb", "a\nb");
}
#[test]
fn white_space_merge_all() {
macro_rules! test {
($input:tt, $output:tt) => {
let input = Txt::from($input);
let output = WhiteSpace::MergeAll.transform(&input);
assert_eq!($output, output.as_str());
};
}
test!("a b\n\nc", "a b c");
test!("a b\nc", "a b c");
test!(" a b\nc\n \n", "a b c");
test!(" \n a b\nc", "a b c");
test!("a\n \nb", "a b");
}
}