mod font_fallbacks;
mod font_features;
mod line;
mod line_layout;
mod line_wrapper;
pub use font_fallbacks::*;
pub use font_features::*;
pub use line::*;
pub use line_layout::*;
pub use line_wrapper::*;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
Bounds, DevicePixels, Hsla, Pixels, PlatformTextSystem, Point, Result, SharedString, Size,
StrikethroughStyle, UnderlineStyle, px,
};
use anyhow::Context as _;
use collections::{FxHashMap, FxHashSet};
use core::fmt;
use derive_more::{Add, Deref, FromStr, Sub};
use itertools::Itertools;
use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
use smallvec::{SmallVec, smallvec};
use std::{
borrow::Cow,
cmp,
collections::VecDeque,
fmt::{Debug, Display, Formatter},
hash::{Hash, Hasher},
ops::{Deref, DerefMut, Range},
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
},
};
#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)]
#[repr(C)]
pub struct FontId(pub usize);
#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)]
pub struct FontFamilyId(pub usize);
pub const SUBPIXEL_VARIANTS_X: u8 = 4;
pub const SUBPIXEL_VARIANTS_Y: u8 = 1;
const MAX_REPORTED_MISSING_GLYPHS: usize = 1024;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum FallbackFontClass {
Proportional,
Monospace,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct MissingGlyph {
grapheme: SharedString,
font_class: FallbackFontClass,
}
impl MissingGlyph {
pub fn new(grapheme: SharedString, font_class: FallbackFontClass) -> Self {
Self {
grapheme,
font_class,
}
}
pub fn grapheme(&self) -> &str {
&self.grapheme
}
pub fn font_class(&self) -> FallbackFontClass {
self.font_class
}
}
pub trait MissingGlyphSink: Send + Sync {
fn report(&self, missing_glyphs: Vec<MissingGlyph>);
}
#[derive(Default)]
struct MissingGlyphState {
reported: FxHashSet<MissingGlyph>,
reported_order: VecDeque<MissingGlyph>,
generation: usize,
}
impl MissingGlyphState {
fn reset(&mut self, generation: usize) {
self.reported.clear();
self.reported_order.clear();
self.generation = generation;
}
}
struct QueuedMissingGlyph {
generation: usize,
missing_glyph: MissingGlyph,
}
struct MissingGlyphReporter {
generation: Arc<AtomicUsize>,
sender: async_channel::Sender<QueuedMissingGlyph>,
}
impl MissingGlyphSink for MissingGlyphReporter {
fn report(&self, missing_glyphs: Vec<MissingGlyph>) {
if self.sender.is_closed() {
return;
}
let generation = self.generation.load(Ordering::Acquire);
for missing_glyph in missing_glyphs.into_iter().unique() {
let queued = QueuedMissingGlyph {
generation,
missing_glyph,
};
if self.sender.try_send(queued).is_err() {
break;
}
}
}
}
impl MissingGlyphReporter {
fn reset(&self) {
self.generation.fetch_add(1, Ordering::AcqRel);
}
}
pub(crate) struct MissingGlyphReceiver {
state: MissingGlyphState,
generation: Arc<AtomicUsize>,
receiver: async_channel::Receiver<QueuedMissingGlyph>,
}
impl MissingGlyphReceiver {
pub(crate) async fn recv(
&mut self,
) -> std::result::Result<Vec<MissingGlyph>, async_channel::RecvError> {
loop {
let queued = self.receiver.recv().await?;
let mut missing_glyphs = Vec::new();
for queued in std::iter::once(queued)
.chain(std::iter::from_fn(|| self.receiver.try_recv().ok()))
.take(MAX_REPORTED_MISSING_GLYPHS)
{
let generation = self.generation.load(Ordering::Acquire);
if self.state.generation != generation {
self.state.reset(generation);
missing_glyphs.clear();
}
if queued.generation != generation
|| !self.state.reported.insert(queued.missing_glyph.clone())
{
continue;
}
self.state
.reported_order
.push_back(queued.missing_glyph.clone());
missing_glyphs.push(queued.missing_glyph);
if self.state.reported.len() > MAX_REPORTED_MISSING_GLYPHS
&& let Some(expired) = self.state.reported_order.pop_front()
{
self.state.reported.remove(&expired);
}
}
if !missing_glyphs.is_empty() {
return Ok(missing_glyphs);
}
let mut yielded = false;
std::future::poll_fn(|cx| {
if std::mem::replace(&mut yielded, true) {
std::task::Poll::Ready(())
} else {
cx.waker().wake_by_ref();
std::task::Poll::Pending
}
})
.await;
}
}
}
impl Drop for MissingGlyphReceiver {
fn drop(&mut self) {
self.receiver.close();
while self.receiver.try_recv().is_ok() {}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct TextSystemCacheStats {
pub lines: usize,
pub wrapped_lines: usize,
pub raster_bounds: usize,
}
pub struct TextSystem {
platform_text_system: Arc<dyn PlatformTextSystem>,
global_line_layout_cache: Arc<GlobalLineLayoutCache>,
font_ids_by_font: RwLock<FxHashMap<Font, CachedFont>>,
font_metrics: RwLock<FxHashMap<FontId, FontMetrics>>,
raster_bounds: RwLock<FxHashMap<RenderGlyphParams, Bounds<DevicePixels>>>,
raster_bounds_max_entries: Option<usize>,
wrapper_pool: Mutex<FxHashMap<FontIdWithSize, Vec<CharWidthCache>>>,
font_runs_pool: Mutex<Vec<Vec<FontRun>>>,
fallback_font_stack: SmallVec<[Font; 2]>,
font_generation: Arc<AtomicUsize>,
missing_glyph_reporter: Arc<MissingGlyphReporter>,
missing_glyph_receiver: Mutex<Option<MissingGlyphReceiver>>,
}
#[derive(Clone)]
enum CachedFont {
Direct(FontId),
Fallback(FontId),
Missing(Arc<anyhow::Error>),
}
impl TextSystem {
pub(crate) fn new(
platform_text_system: Arc<dyn PlatformTextSystem>,
text_budget: &crate::TextResourceBudget,
) -> Self {
let (sender, receiver) = async_channel::bounded(MAX_REPORTED_MISSING_GLYPHS);
let missing_glyph_generation = Arc::<AtomicUsize>::default();
let missing_glyph_reporter = Arc::new(MissingGlyphReporter {
generation: missing_glyph_generation.clone(),
sender,
});
platform_text_system.set_missing_glyph_sink(missing_glyph_reporter.clone());
TextSystem {
platform_text_system,
global_line_layout_cache: Arc::new(GlobalLineLayoutCache::new(
text_budget.line_layout_cache_max_entries,
text_budget.line_layout_cache_low_watermark,
)),
font_metrics: RwLock::default(),
raster_bounds: RwLock::default(),
raster_bounds_max_entries: text_budget.raster_bounds_cache_max_entries,
font_ids_by_font: RwLock::default(),
wrapper_pool: Mutex::default(),
font_runs_pool: Mutex::default(),
fallback_font_stack: smallvec![
font(".ZedMono"),
font(".ZedSans"),
font("Helvetica"),
font("Segoe UI"), font("Cantarell"), font("Ubuntu"), font("Noto Sans"), font("DejaVu Sans")
],
font_generation: Arc::default(),
missing_glyph_reporter,
missing_glyph_receiver: Mutex::new(Some(MissingGlyphReceiver {
state: MissingGlyphState::default(),
generation: missing_glyph_generation,
receiver,
})),
}
}
#[cfg(test)]
pub(crate) fn resource_budget_for_test(&self) -> (usize, usize, Option<usize>) {
let (max_entries, low_watermark) = self.global_line_layout_cache.budget_for_test();
(max_entries, low_watermark, self.raster_bounds_max_entries)
}
pub fn clear_layout_cache(&self) {
self.global_line_layout_cache.clear();
self.raster_bounds.write().clear();
self.platform_text_system.clear_glyph_raster_cache();
}
pub fn layout_cache_stats(&self) -> TextSystemCacheStats {
let (lines, wrapped_lines) = self.global_line_layout_cache.entry_counts();
TextSystemCacheStats {
lines,
wrapped_lines,
raster_bounds: self.raster_bounds.read().len(),
}
}
pub fn all_font_names(&self) -> Vec<String> {
let mut names = self.platform_text_system.all_font_names();
names.sort_unstable();
names.dedup();
names
}
pub fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
self.platform_text_system.add_fonts(fonts)?;
self.font_ids_by_font.write().clear();
self.missing_glyph_reporter.reset();
self.font_generation.fetch_add(1, Ordering::Release);
Ok(())
}
pub(crate) fn take_missing_glyph_receiver(&self) -> Option<MissingGlyphReceiver> {
self.missing_glyph_receiver
.try_lock()
.and_then(|mut receiver| receiver.take())
}
#[cfg(test)]
pub(crate) fn report_missing_glyphs_in_test(&self, missing_glyphs: Vec<MissingGlyph>) {
self.missing_glyph_reporter.report(missing_glyphs);
}
pub fn prewarm_fonts(&self, fonts: &[Font]) {
let mut font_ids = SmallVec::<[FontId; 8]>::new();
for font in fonts {
let font_id = self.resolve_font(font);
if !font_ids.contains(&font_id) {
font_ids.push(font_id);
}
}
self.platform_text_system.prewarm_fonts(&font_ids);
}
pub fn glyph_dilation_for_color(&self, color: Hsla) -> u8 {
self.platform_text_system.glyph_dilation_for_color(color)
}
pub fn get_font_for_id(&self, id: FontId) -> Option<Font> {
let lock = self.font_ids_by_font.read();
lock.iter()
.filter_map(|(font, cached)| match cached {
CachedFont::Direct(font_id) if *font_id == id => Some(font.clone()),
_ => None,
})
.next()
}
pub fn resolve_font(&self, font: &Font) -> FontId {
if let Some(CachedFont::Direct(font_id) | CachedFont::Fallback(font_id)) =
self.font_ids_by_font.read().get(font)
{
return *font_id;
}
self.resolve_font_uncached(font)
}
fn resolve_font_uncached(&self, font: &Font) -> FontId {
let mut font_error = None;
let font_id = {
let mut font_ids = self.font_ids_by_font.upgradable_read();
if let Some(CachedFont::Direct(font_id) | CachedFont::Fallback(font_id)) =
font_ids.get(font)
{
return *font_id;
}
let mut lookup_font = |candidate: &Font| {
let cached = if let Some(cached) = font_ids.get(candidate) {
cached.clone()
} else {
let cached = match self.platform_text_system.font_id(candidate) {
Ok(font_id) => CachedFont::Direct(font_id),
Err(error) => CachedFont::Missing(Arc::new(error)),
};
font_ids.with_upgraded(|font_ids| {
font_ids.insert(candidate.clone(), cached.clone());
});
cached
};
match cached {
CachedFont::Direct(font_id) => Some(font_id),
CachedFont::Fallback(_) => None,
CachedFont::Missing(error) => {
if candidate == font {
font_error = Some(error);
}
None
}
}
};
if let Some(font_id) = lookup_font(font) {
Some(font_id)
} else {
let font_id = self.fallback_font_stack.iter().find_map(lookup_font);
if let Some(font_id) = font_id {
font_ids.with_upgraded(|font_ids| {
font_ids.insert(font.clone(), CachedFont::Fallback(font_id));
});
}
font_id
}
};
if let Some(error) = font_error {
log::warn!("failed to load font {font:?}: {error:#}");
}
font_id.unwrap_or_else(|| {
panic!(
"failed to resolve font '{}' or any of the fallbacks: {}",
font.family,
self.fallback_font_stack
.iter()
.map(|fallback| &fallback.family)
.join(", ")
);
})
}
pub fn bounding_box(&self, font_id: FontId, font_size: Pixels) -> Bounds<Pixels> {
self.read_metrics(font_id, |metrics| metrics.bounding_box(font_size))
}
pub fn typographic_bounds(
&self,
font_id: FontId,
font_size: Pixels,
character: char,
) -> Result<Bounds<Pixels>> {
let glyph_id = self
.platform_text_system
.glyph_for_char(font_id, character)
.with_context(|| format!("glyph not found for character '{character}'"))?;
let bounds = self
.platform_text_system
.typographic_bounds(font_id, glyph_id)?;
Ok(self.read_metrics(font_id, |metrics| {
(bounds / metrics.units_per_em as f32 * font_size.0).map(px)
}))
}
pub fn advance(&self, font_id: FontId, font_size: Pixels, ch: char) -> Result<Size<Pixels>> {
let glyph_id = self
.platform_text_system
.glyph_for_char(font_id, ch)
.with_context(|| format!("glyph not found for character '{ch}'"))?;
let result = self.platform_text_system.advance(font_id, glyph_id)?
/ self.units_per_em(font_id) as f32;
Ok(result * font_size)
}
pub fn em_width(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
Ok(self.typographic_bounds(font_id, font_size, 'm')?.size.width)
}
pub fn em_advance(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
Ok(self.advance(font_id, font_size, 'm')?.width)
}
pub fn ch_width(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
Ok(self.typographic_bounds(font_id, font_size, '0')?.size.width)
}
pub fn ch_advance(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
Ok(self.advance(font_id, font_size, '0')?.width)
}
pub fn units_per_em(&self, font_id: FontId) -> u32 {
self.read_metrics(font_id, |metrics| metrics.units_per_em)
}
pub fn cap_height(&self, font_id: FontId, font_size: Pixels) -> Pixels {
self.read_metrics(font_id, |metrics| metrics.cap_height(font_size))
}
pub fn x_height(&self, font_id: FontId, font_size: Pixels) -> Pixels {
self.read_metrics(font_id, |metrics| metrics.x_height(font_size))
}
pub fn ascent(&self, font_id: FontId, font_size: Pixels) -> Pixels {
self.read_metrics(font_id, |metrics| metrics.ascent(font_size))
}
pub fn descent(&self, font_id: FontId, font_size: Pixels) -> Pixels {
self.read_metrics(font_id, |metrics| metrics.descent(font_size))
}
pub fn baseline_offset(
&self,
font_id: FontId,
font_size: Pixels,
line_height: Pixels,
) -> Pixels {
let ascent = self.ascent(font_id, font_size);
let descent = self.descent(font_id, font_size);
let padding_top = (line_height - ascent - descent) / 2.;
padding_top + ascent
}
fn read_metrics<T>(&self, font_id: FontId, read: impl FnOnce(&FontMetrics) -> T) -> T {
let lock = self.font_metrics.upgradable_read();
if let Some(metrics) = lock.get(&font_id) {
read(metrics)
} else {
let mut lock = RwLockUpgradableReadGuard::upgrade(lock);
let metrics = lock
.entry(font_id)
.or_insert_with(|| self.platform_text_system.font_metrics(font_id));
read(metrics)
}
}
pub fn line_wrapper(self: &Arc<Self>, font: Font, font_size: Pixels) -> LineWrapperHandle {
let lock = &mut self.wrapper_pool.lock();
let font_id = self.resolve_font(&font);
let char_widths = lock
.entry(FontIdWithSize { font_id, font_size })
.or_default()
.pop()
.unwrap_or_default();
let wrapper = LineWrapper::new(
font_id,
font_size,
self.platform_text_system.clone(),
char_widths,
);
LineWrapperHandle {
wrapper: Some(wrapper),
text_system: self.clone(),
}
}
pub(crate) fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
let raster_bounds = self.raster_bounds.upgradable_read();
if let Some(bounds) = raster_bounds.get(params) {
Ok(*bounds)
} else {
let mut raster_bounds = RwLockUpgradableReadGuard::upgrade(raster_bounds);
if let Some(max) = self.raster_bounds_max_entries {
if raster_bounds.len() >= max {
raster_bounds.clear();
self.platform_text_system.clear_glyph_raster_cache();
}
}
let bounds = self.platform_text_system.glyph_raster_bounds(params)?;
raster_bounds.insert(params.clone(), bounds);
Ok(bounds)
}
}
pub(crate) fn rasterize_glyph(
&self,
params: &RenderGlyphParams,
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
let raster_bounds = self.raster_bounds(params)?;
self.platform_text_system
.rasterize_glyph(params, raster_bounds)
}
}
#[derive(Deref)]
pub struct WindowTextSystem {
line_layout_cache: LineLayoutCache,
#[deref]
text_system: Arc<TextSystem>,
}
impl WindowTextSystem {
#[doc(hidden)]
pub fn new(text_system: Arc<TextSystem>) -> Self {
Self {
line_layout_cache: LineLayoutCache::new(
text_system.platform_text_system.clone(),
text_system.global_line_layout_cache.clone(),
text_system.font_generation.clone(),
),
text_system,
}
}
pub(crate) fn layout_index(&self) -> LineLayoutIndex {
self.line_layout_cache.layout_index()
}
pub(crate) fn reuse_layouts(&self, index: Range<LineLayoutIndex>) {
self.line_layout_cache.reuse_layouts(index)
}
pub(crate) fn truncate_layouts(&self, index: LineLayoutIndex) {
self.line_layout_cache.truncate_layouts(index)
}
pub fn shape_line(
&self,
text: SharedString,
font_size: Pixels,
runs: &[TextRun],
force_width: Option<Pixels>,
) -> ShapedLine {
self.shape_line_with_spacing(text, font_size, runs, force_width, None)
}
pub fn shape_line_with_spacing(
&self,
text: SharedString,
font_size: Pixels,
runs: &[TextRun],
force_width: Option<Pixels>,
letter_spacing: Option<Pixels>,
) -> ShapedLine {
debug_assert!(
text.find('\n').is_none(),
"text argument should not contain newlines"
);
let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
for run in runs {
push_decoration_run(&mut decoration_runs, run, run.len as u32);
}
let layout =
self.layout_line_with_spacing(&text, font_size, runs, force_width, letter_spacing);
ShapedLine {
layout,
text,
decoration_runs,
}
}
pub fn shape_text(
&self,
text: SharedString,
font_size: Pixels,
runs: &[TextRun],
wrap_width: Option<Pixels>,
line_clamp: Option<usize>,
) -> Result<SmallVec<[WrappedLine; 1]>> {
let mut runs = runs.iter().filter(|run| run.len > 0).cloned().peekable();
let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
let mut lines = SmallVec::new();
let mut line_start = 0;
let mut max_wrap_lines = line_clamp.unwrap_or(usize::MAX);
let mut wrapped_lines = 0;
let mut process_line = |line_text: SharedString| {
font_runs.clear();
let line_end = line_start + line_text.len();
let mut last_font: Option<FontId> = None;
let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
let mut run_start = line_start;
while run_start < line_end {
let Some(run) = runs.peek_mut() else {
break;
};
let run_len_within_line = cmp::min(line_end, run_start + run.len) - run_start;
let decoration_changed =
push_decoration_run(&mut decoration_runs, run, run_len_within_line as u32);
if let Some(font_run) = font_runs.last_mut()
&& Some(font_run.font_id) == last_font
&& !decoration_changed
{
font_run.len += run_len_within_line;
} else {
let font_id = self.resolve_font(&run.font);
last_font = Some(font_id);
font_runs.push(FontRun {
len: run_len_within_line,
font_id,
});
}
if run_len_within_line == run.len {
runs.next();
} else {
run.len -= run_len_within_line;
}
run_start += run_len_within_line;
}
let layout = self.line_layout_cache.layout_wrapped_line(
&line_text,
font_size,
&font_runs,
wrap_width,
Some(max_wrap_lines - wrapped_lines),
);
wrapped_lines += layout.wrap_boundaries.len();
lines.push(WrappedLine {
layout,
decoration_runs,
text: line_text,
});
line_start = line_end + 1;
if let Some(run) = runs.peek_mut() {
run.len -= 1;
if run.len == 0 {
runs.next();
}
}
};
let mut split_lines = text.split('\n');
let mut processed = false;
if let Some(first_line) = split_lines.next()
&& let Some(second_line) = split_lines.next()
{
processed = true;
process_line(first_line.to_string().into());
process_line(second_line.to_string().into());
for line_text in split_lines {
process_line(line_text.to_string().into());
}
}
if !processed {
process_line(text);
}
self.font_runs_pool.lock().push(font_runs);
Ok(lines)
}
pub(crate) fn finish_frame(&self) {
self.line_layout_cache.finish_frame()
}
pub fn layout_line(
&self,
text: &str,
font_size: Pixels,
runs: &[TextRun],
force_width: Option<Pixels>,
) -> Arc<LineLayout> {
self.layout_line_with_spacing(text, font_size, runs, force_width, None)
}
pub fn layout_line_with_spacing(
&self,
text: &str,
font_size: Pixels,
runs: &[TextRun],
force_width: Option<Pixels>,
letter_spacing: Option<Pixels>,
) -> Arc<LineLayout> {
let mut last_run = None::<&TextRun>;
let mut last_font: Option<FontId> = None;
let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
font_runs.clear();
for run in runs.iter() {
let decoration_changed = match last_run {
Some(last) if decorations_equal(last, run) => false,
_ => {
last_run = Some(run);
true
}
};
if let Some(font_run) = font_runs.last_mut()
&& Some(font_run.font_id) == last_font
&& !decoration_changed
{
font_run.len += run.len;
} else {
let font_id = self.resolve_font(&run.font);
last_font = Some(font_id);
font_runs.push(FontRun {
len: run.len,
font_id,
});
}
}
let layout = self.line_layout_cache.layout_line_with_spacing(
&SharedString::new(text),
font_size,
&font_runs,
force_width,
letter_spacing,
);
self.font_runs_pool.lock().push(font_runs);
layout
}
}
#[derive(Hash, Eq, PartialEq)]
struct FontIdWithSize {
font_id: FontId,
font_size: Pixels,
}
pub struct LineWrapperHandle {
wrapper: Option<LineWrapper>,
text_system: Arc<TextSystem>,
}
impl Drop for LineWrapperHandle {
fn drop(&mut self) {
let mut state = self.text_system.wrapper_pool.lock();
let wrapper = self.wrapper.take().unwrap();
state
.get_mut(&FontIdWithSize {
font_id: wrapper.font_id,
font_size: wrapper.font_size,
})
.unwrap()
.push(wrapper.into_char_widths());
}
}
impl Deref for LineWrapperHandle {
type Target = LineWrapper;
fn deref(&self) -> &Self::Target {
self.wrapper.as_ref().unwrap()
}
}
impl DerefMut for LineWrapperHandle {
fn deref_mut(&mut self) -> &mut Self::Target {
self.wrapper.as_mut().unwrap()
}
}
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Serialize, Deserialize, Add, Sub, FromStr)]
#[serde(transparent)]
pub struct FontWeight(pub f32);
impl Display for FontWeight {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<f32> for FontWeight {
fn from(weight: f32) -> Self {
FontWeight(weight)
}
}
impl Default for FontWeight {
#[inline]
fn default() -> FontWeight {
FontWeight::NORMAL
}
}
impl Hash for FontWeight {
fn hash<H: Hasher>(&self, state: &mut H) {
state.write_u32(u32::from_be_bytes(self.0.to_be_bytes()));
}
}
impl Eq for FontWeight {}
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 const ALL: [FontWeight; 9] = [
Self::THIN,
Self::EXTRA_LIGHT,
Self::LIGHT,
Self::NORMAL,
Self::MEDIUM,
Self::SEMIBOLD,
Self::BOLD,
Self::EXTRA_BOLD,
Self::BLACK,
];
}
impl schemars::JsonSchema for FontWeight {
fn schema_name() -> std::borrow::Cow<'static, str> {
"FontWeight".into()
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
use schemars::json_schema;
json_schema!({
"type": "number",
"minimum": Self::THIN,
"maximum": Self::BLACK,
"default": Self::default(),
"description": "Font weight value between 100 (thin) and 900 (black)"
})
}
}
#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize, JsonSchema)]
pub enum FontStyle {
#[default]
Normal,
Italic,
Oblique,
}
impl Display for FontStyle {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
Debug::fmt(self, f)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TextRun {
pub len: usize,
pub font: Font,
pub color: Hsla,
pub background_color: Option<Hsla>,
pub underline: Option<UnderlineStyle>,
pub strikethrough: Option<StrikethroughStyle>,
}
fn decorations_equal(a: &TextRun, b: &TextRun) -> bool {
a.color == b.color
&& a.underline == b.underline
&& a.strikethrough == b.strikethrough
&& a.background_color == b.background_color
}
fn push_decoration_run(
decoration_runs: &mut SmallVec<[DecorationRun; 32]>,
run: &TextRun,
len: u32,
) -> bool {
if let Some(last_run) = decoration_runs.last_mut()
&& last_run.color == run.color
&& last_run.underline == run.underline
&& last_run.strikethrough == run.strikethrough
&& last_run.background_color == run.background_color
{
last_run.len += len;
false
} else {
decoration_runs.push(DecorationRun {
len,
color: run.color,
background_color: run.background_color,
underline: run.underline,
strikethrough: run.strikethrough,
});
true
}
}
#[cfg(all(target_os = "macos", test))]
impl TextRun {
fn with_len(&self, len: usize) -> Self {
let mut this = self.clone();
this.len = len;
this
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
#[repr(C)]
pub struct GlyphId(pub u32);
#[derive(Clone, Debug, PartialEq)]
#[allow(missing_docs)]
pub struct RenderGlyphParams {
pub font_id: FontId,
pub glyph_id: GlyphId,
pub font_size: Pixels,
pub subpixel_variant: Point<u8>,
pub scale_factor: f32,
pub is_emoji: bool,
pub dilation: u8,
}
impl Eq for RenderGlyphParams {}
impl Hash for RenderGlyphParams {
fn hash<H: Hasher>(&self, state: &mut H) {
self.font_id.0.hash(state);
self.glyph_id.0.hash(state);
self.font_size.0.to_bits().hash(state);
self.subpixel_variant.hash(state);
self.scale_factor.to_bits().hash(state);
self.is_emoji.hash(state);
self.dilation.hash(state);
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct Font {
pub family: SharedString,
pub features: FontFeatures,
pub fallbacks: Option<FontFallbacks>,
pub weight: FontWeight,
pub style: FontStyle,
}
pub fn font(family: impl Into<SharedString>) -> Font {
Font {
family: family.into(),
features: FontFeatures::default(),
weight: FontWeight::default(),
style: FontStyle::default(),
fallbacks: None,
}
}
impl Font {
pub fn bold(mut self) -> Self {
self.weight = FontWeight::BOLD;
self
}
pub fn italic(mut self) -> Self {
self.style = FontStyle::Italic;
self
}
}
#[derive(Clone, Copy, Debug)]
pub struct FontMetrics {
pub units_per_em: u32,
pub ascent: f32,
pub descent: f32,
pub line_gap: f32,
pub underline_position: f32,
pub underline_thickness: f32,
pub cap_height: f32,
pub x_height: f32,
pub bounding_box: Bounds<f32>,
}
impl FontMetrics {
pub fn ascent(&self, font_size: Pixels) -> Pixels {
Pixels((self.ascent / self.units_per_em as f32) * font_size.0)
}
pub fn descent(&self, font_size: Pixels) -> Pixels {
Pixels((self.descent / self.units_per_em as f32) * font_size.0)
}
pub fn line_gap(&self, font_size: Pixels) -> Pixels {
Pixels((self.line_gap / self.units_per_em as f32) * font_size.0)
}
pub fn underline_position(&self, font_size: Pixels) -> Pixels {
Pixels((self.underline_position / self.units_per_em as f32) * font_size.0)
}
pub fn underline_thickness(&self, font_size: Pixels) -> Pixels {
Pixels((self.underline_thickness / self.units_per_em as f32) * font_size.0)
}
pub fn cap_height(&self, font_size: Pixels) -> Pixels {
Pixels((self.cap_height / self.units_per_em as f32) * font_size.0)
}
pub fn x_height(&self, font_size: Pixels) -> Pixels {
Pixels((self.x_height / self.units_per_em as f32) * font_size.0)
}
pub fn bounding_box(&self, font_size: Pixels) -> Bounds<Pixels> {
(self.bounding_box / self.units_per_em as f32 * font_size.0).map(px)
}
}
#[allow(unused)]
#[doc(hidden)]
pub fn font_name_with_fallbacks<'a>(name: &'a str, system: &'a str) -> &'a str {
match name {
".SystemUIFont" => system,
".ZedSans" | "Zed Plex Sans" => "IBM Plex Sans",
".ZedMono" | "Zed Plex Mono" => "Lilex",
_ => name,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{AppResourceProfile, NoopTextSystem, point, size};
use parking_lot::Mutex;
use std::collections::HashSet;
use std::sync::atomic::AtomicBool;
struct TrackingTextSystem {
noop: NoopTextSystem,
pending_glyph_images: Mutex<HashSet<RenderGlyphParams>>,
missing_glyph_sink_installed: AtomicBool,
}
impl TrackingTextSystem {
fn new() -> Self {
Self {
noop: NoopTextSystem::new(),
pending_glyph_images: Mutex::default(),
missing_glyph_sink_installed: AtomicBool::new(false),
}
}
fn pending_glyph_images_len(&self) -> usize {
self.pending_glyph_images.lock().len()
}
}
impl PlatformTextSystem for TrackingTextSystem {
fn set_missing_glyph_sink(&self, _sink: Arc<dyn MissingGlyphSink>) {
self.missing_glyph_sink_installed
.store(true, Ordering::SeqCst);
}
fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
self.noop.add_fonts(fonts)
}
fn all_font_names(&self) -> Vec<String> {
self.noop.all_font_names()
}
fn font_id(&self, descriptor: &Font) -> Result<FontId> {
self.noop.font_id(descriptor)
}
fn clear_glyph_raster_cache(&self) {
self.pending_glyph_images.lock().clear();
}
fn font_metrics(&self, font_id: FontId) -> FontMetrics {
self.noop.font_metrics(font_id)
}
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
self.noop.typographic_bounds(font_id, glyph_id)
}
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
self.noop.advance(font_id, glyph_id)
}
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
self.noop.glyph_for_char(font_id, ch)
}
fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
self.pending_glyph_images.lock().insert(params.clone());
Ok(Bounds {
origin: point(DevicePixels(0), DevicePixels(0)),
size: size(DevicePixels(1), DevicePixels(1)),
})
}
fn rasterize_glyph(
&self,
params: &RenderGlyphParams,
raster_bounds: Bounds<DevicePixels>,
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
self.pending_glyph_images.lock().remove(params);
Ok((raster_bounds.size, vec![0]))
}
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout {
self.noop.layout_line(text, font_size, runs)
}
}
fn glyph_params(glyph_id: u32) -> RenderGlyphParams {
RenderGlyphParams {
font_id: FontId(0),
glyph_id: GlyphId(glyph_id),
font_size: px(16.),
subpixel_variant: point(0, 0),
scale_factor: 1.0,
is_emoji: false,
dilation: 0,
}
}
struct RecordingTextSystem {
noop: NoopTextSystem,
layout_runs: Mutex<Vec<Vec<FontRun>>>,
}
impl PlatformTextSystem for RecordingTextSystem {
fn set_missing_glyph_sink(&self, sink: Arc<dyn MissingGlyphSink>) {
self.noop.set_missing_glyph_sink(sink)
}
fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
self.noop.add_fonts(fonts)
}
fn all_font_names(&self) -> Vec<String> {
self.noop.all_font_names()
}
fn font_id(&self, descriptor: &Font) -> Result<FontId> {
self.noop.font_id(descriptor)
}
fn clear_glyph_raster_cache(&self) {
self.noop.clear_glyph_raster_cache()
}
fn font_metrics(&self, font_id: FontId) -> FontMetrics {
self.noop.font_metrics(font_id)
}
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
self.noop.typographic_bounds(font_id, glyph_id)
}
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
self.noop.advance(font_id, glyph_id)
}
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
self.noop.glyph_for_char(font_id, ch)
}
fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
self.noop.glyph_raster_bounds(params)
}
fn rasterize_glyph(
&self,
params: &RenderGlyphParams,
raster_bounds: Bounds<DevicePixels>,
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
self.noop.rasterize_glyph(params, raster_bounds)
}
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout {
self.layout_runs.lock().push(runs.to_vec());
self.noop.layout_line(text, font_size, runs)
}
}
#[test]
fn background_color_runs_split_font_runs_in_both_shaping_paths() {
let platform = Arc::new(RecordingTextSystem {
noop: NoopTextSystem::new(),
layout_runs: Mutex::default(),
});
let runs = || {
let plain = TextRun {
len: 1,
font: font("Noop Font"),
color: Hsla::default(),
background_color: None,
underline: None,
strikethrough: None,
};
let mut highlighted = plain.clone();
highlighted.background_color = Some(Hsla::default());
[plain, highlighted]
};
let line_shaper = WindowTextSystem::new(Arc::new(TextSystem::new(
platform.clone(),
&AppResourceProfile::desktop().text,
)));
let shaped_line = line_shaper.shape_line("ab".into(), px(16.), &runs(), None);
assert_eq!(
shaped_line.decoration_runs.len(),
2,
"decoration runs must split on background color"
);
let text_shaper = WindowTextSystem::new(Arc::new(TextSystem::new(
platform.clone(),
&AppResourceProfile::desktop().text,
)));
let wrapped_lines = text_shaper
.shape_text("ab".into(), px(16.), &runs(), None, None)
.unwrap();
assert_eq!(
wrapped_lines[0].decoration_runs.len(),
2,
"decoration runs must split on background color"
);
let recorded = platform.layout_runs.lock();
assert_eq!(
recorded.len(),
2,
"each shaping path should reach the platform once"
);
assert_eq!(
recorded[0], recorded[1],
"shape_line and shape_text must agree on font run boundaries"
);
}
#[test]
fn all_font_names_lists_only_platform_families() {
let text_system = TextSystem::new(
Arc::new(NoopTextSystem::new()),
&AppResourceProfile::desktop().text,
);
assert!(text_system.all_font_names().is_empty());
}
#[test]
fn resolve_font_caches_fallbacks() {
let mut text_system = TextSystem::new(
Arc::new(NoopTextSystem::new()),
&AppResourceProfile::desktop().text,
);
let requested = font("Missing Font").bold().italic();
let fallback = font("DejaVu Sans");
{
let mut font_ids = text_system.font_ids_by_font.write();
let missing = CachedFont::Missing(Arc::new(anyhow::anyhow!("missing font")));
font_ids.insert(requested.clone(), missing.clone());
for candidate in &text_system.fallback_font_stack {
font_ids.insert(candidate.clone(), missing.clone());
}
font_ids.insert(fallback.clone(), CachedFont::Direct(FontId(42)));
}
assert_eq!(text_system.resolve_font(&requested), FontId(42));
assert_eq!(text_system.get_font_for_id(FontId(42)), Some(fallback));
text_system.fallback_font_stack.clear();
assert_eq!(text_system.resolve_font(&requested), FontId(42));
}
#[test]
fn text_system_installs_missing_glyph_sink_on_platform() {
let platform = Arc::new(TrackingTextSystem::new());
assert!(!platform.missing_glyph_sink_installed.load(Ordering::SeqCst));
let _text_system = TextSystem::new(platform.clone(), &AppResourceProfile::desktop().text);
assert!(platform.missing_glyph_sink_installed.load(Ordering::SeqCst));
}
#[test]
fn clearing_layout_cache_also_clears_platform_pending_glyph_images() {
let platform = Arc::new(TrackingTextSystem::new());
let text_system = TextSystem::new(platform.clone(), &AppResourceProfile::desktop().text);
text_system.raster_bounds(&glyph_params(1)).unwrap();
assert_eq!(platform.pending_glyph_images_len(), 1);
text_system.clear_layout_cache();
assert_eq!(platform.pending_glyph_images_len(), 0);
}
#[test]
fn evicting_raster_bounds_also_clears_platform_pending_glyph_images() {
let platform = Arc::new(TrackingTextSystem::new());
let mut budget = AppResourceProfile::desktop().text;
budget.raster_bounds_cache_max_entries = Some(1);
let text_system = TextSystem::new(platform.clone(), &budget);
text_system.raster_bounds(&glyph_params(1)).unwrap();
text_system.raster_bounds(&glyph_params(2)).unwrap();
assert_eq!(platform.pending_glyph_images_len(), 1);
}
#[test]
fn adding_fonts_invalidates_cached_line_layouts() {
let text_system = Arc::new(TextSystem::new(
Arc::new(NoopTextSystem::new()),
&AppResourceProfile::desktop().text,
));
let window_text_system = WindowTextSystem::new(text_system.clone());
let text: SharedString = "cached text".into();
let runs = [TextRun {
len: text.len(),
font: font("IBM Plex Sans"),
color: Default::default(),
background_color: None,
underline: None,
strikethrough: None,
}];
let first_layout = window_text_system.shape_line(text.clone(), px(14.0), &runs, None);
let cached_layout = window_text_system.shape_line(text.clone(), px(14.0), &runs, None);
assert!(std::ptr::eq::<LineLayout>(
&**first_layout,
&**cached_layout
));
text_system
.add_fonts(vec![Cow::Borrowed(b"unused")])
.unwrap();
let refreshed_layout = window_text_system.shape_line(text, px(14.0), &runs, None);
assert!(!std::ptr::eq::<LineLayout>(
&**first_layout,
&**refreshed_layout
));
}
#[test]
fn test_font_weight_from_str() {
let weight: FontWeight = "450".parse().unwrap();
assert_eq!(weight, FontWeight(450.0));
assert!("not-a-number".parse::<FontWeight>().is_err());
assert_eq!(weight + FontWeight(50.0), FontWeight(500.0));
assert_eq!(weight - FontWeight(50.0), FontWeight(400.0));
}
}
#[cfg(test)]
mod line_wrapper_pool_tests {
use super::*;
use crate::{AppResourceProfile, NoopTextSystem};
#[test]
fn pooled_line_wrappers_do_not_retain_text_system() {
let platform = Arc::new(NoopTextSystem::new());
let text_system = Arc::new(TextSystem::new(
platform.clone(),
&AppResourceProfile::desktop().text,
));
let weak_text_system = Arc::downgrade(&text_system);
let mut wrapper = text_system.line_wrapper(font(".ZedMono"), px(16.));
let fragments = [LineFragment::text("hello world")];
wrapper
.wrap_line(&fragments, px(40.), IndentAdjustment::default())
.for_each(drop);
drop(wrapper);
assert_eq!(Arc::strong_count(&platform), 2);
drop(text_system);
drop(platform);
assert!(weak_text_system.upgrade().is_none());
}
}
#[cfg(test)]
mod missing_glyph_tests {
use super::*;
use crate::{AppResourceProfile, NoopTextSystem};
use futures::FutureExt as _;
#[test]
fn bounds_retained_missing_glyphs() {
let (reporter, mut receiver) = missing_glyph_channel();
reporter.report(
(0..MAX_REPORTED_MISSING_GLYPHS)
.map(|index| {
MissingGlyph::new(index.to_string().into(), FallbackFontClass::Proportional)
})
.collect(),
);
assert!(receiver.recv().now_or_never().unwrap().is_ok());
let newest = MissingGlyph::new("newest".into(), FallbackFontClass::Monospace);
reporter.report(vec![newest.clone()]);
assert!(receiver.recv().now_or_never().unwrap().is_ok());
let state = &receiver.state;
assert_eq!(state.reported.len(), MAX_REPORTED_MISSING_GLYPHS);
assert_eq!(state.reported_order.len(), MAX_REPORTED_MISSING_GLYPHS);
assert!(state.reported.contains(&newest));
}
#[test]
fn dropping_receiver_closes_and_clears_reports() {
let (reporter, receiver) = missing_glyph_channel();
reporter.report(vec![missing_glyph("missing")]);
drop(receiver);
assert!(reporter.sender.is_closed());
assert!(reporter.sender.is_empty());
}
#[test]
fn text_system_delivers_reported_missing_glyphs() {
let text_system = TextSystem::new(
Arc::new(NoopTextSystem::new()),
&AppResourceProfile::desktop().text,
);
let mut receiver = text_system.take_missing_glyph_receiver().expect("receiver");
text_system.report_missing_glyphs_in_test(vec![missing_glyph("界")]);
let received = receiver.recv().now_or_never().unwrap().unwrap();
assert_eq!(received.len(), 1);
assert_eq!(received[0].grapheme(), "界");
assert!(text_system.take_missing_glyph_receiver().is_none());
}
fn missing_glyph_channel() -> (MissingGlyphReporter, MissingGlyphReceiver) {
let (sender, receiver) = async_channel::bounded(MAX_REPORTED_MISSING_GLYPHS);
let generation = Arc::<AtomicUsize>::default();
let reporter = MissingGlyphReporter {
generation: generation.clone(),
sender,
};
let receiver = MissingGlyphReceiver {
state: MissingGlyphState::default(),
generation,
receiver,
};
(reporter, receiver)
}
fn missing_glyph(grapheme: &'static str) -> MissingGlyph {
MissingGlyph::new(grapheme.into(), FallbackFontClass::Proportional)
}
}