Extract platform-dependant `FontSystem`

Antonio Scandurra and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

gpui/src/app.rs                       |  13 
gpui/src/elements/label.rs            |   9 
gpui/src/fonts.rs                     | 198 ++--------------
gpui/src/platform/mac/app.rs          |  13 
gpui/src/platform/mac/fonts.rs        | 338 +++++++++++++++++++++++++++++
gpui/src/platform/mac/mod.rs          |   2 
gpui/src/platform/mac/renderer.rs     |   7 
gpui/src/platform/mac/sprite_cache.rs |  19 
gpui/src/platform/mac/window.rs       |   6 
gpui/src/platform/mod.rs              |  34 ++
gpui/src/presenter.rs                 |   5 
gpui/src/text_layout.rs               | 177 --------------
zed/src/editor/buffer_view.rs         |  21 -
zed/src/file_finder.rs                |   2 
zed/src/main.rs                       |   2 
zed/src/workspace/mod.rs              |   2 
zed/src/workspace/workspace_view.rs   |   4 
17 files changed, 453 insertions(+), 399 deletions(-)

Detailed changes

gpui/src/app.rs πŸ”—

@@ -250,8 +250,8 @@ impl App {
         self.0.borrow().finish_pending_tasks()
     }
 
-    pub fn fonts(&self) -> Arc<FontCache> {
-        self.0.borrow().fonts.clone()
+    pub fn font_cache(&self) -> Arc<FontCache> {
+        self.0.borrow().font_cache.clone()
     }
 
     pub fn platform(&self) -> Arc<dyn platform::App> {
@@ -295,7 +295,7 @@ type GlobalActionCallback = dyn FnMut(&dyn Any, &mut MutableAppContext);
 pub struct MutableAppContext {
     weak_self: Option<rc::Weak<RefCell<Self>>>,
     platform: Arc<dyn platform::App>,
-    fonts: Arc<FontCache>,
+    font_cache: Arc<FontCache>,
     assets: Arc<AssetCache>,
     ctx: AppContext,
     actions: HashMap<TypeId, HashMap<String, Vec<Box<ActionCallback>>>>,
@@ -325,10 +325,11 @@ impl MutableAppContext {
         platform: Arc<dyn platform::App>,
         asset_source: impl AssetSource,
     ) -> Self {
+        let fonts = platform.fonts();
         Self {
             weak_self: None,
             platform,
-            fonts: Arc::new(FontCache::new()),
+            font_cache: Arc::new(FontCache::new(fonts)),
             assets: Arc::new(AssetCache::new(asset_source)),
             ctx: AppContext {
                 models: HashMap::new(),
@@ -620,13 +621,13 @@ impl MutableAppContext {
                 title: "Zed".into(),
             },
             self.foreground.clone(),
-            self.fonts.clone(),
         ) {
             Err(e) => log::error!("error opening window: {}", e),
             Ok(mut window) => {
                 let presenter = Rc::new(RefCell::new(Presenter::new(
                     window_id,
-                    self.fonts.clone(),
+                    self.font_cache.clone(),
+                    self.platform.fonts(),
                     self.assets.clone(),
                     self,
                 )));

gpui/src/elements/label.rs πŸ”—

@@ -109,12 +109,9 @@ impl Element for Label {
             colors = vec![(0..text_len, ColorU::black())];
         }
 
-        let line = ctx.text_layout_cache.layout_str(
-            self.text.as_str(),
-            self.font_size,
-            styles.as_slice(),
-            ctx.font_cache,
-        );
+        let line =
+            ctx.text_layout_cache
+                .layout_str(self.text.as_str(), self.font_size, styles.as_slice());
 
         let size = vec2f(
             line.width.max(constraint.min.x()).min(constraint.max.x()),

gpui/src/fonts.rs πŸ”—

@@ -1,45 +1,28 @@
-use crate::geometry::{
-    rect::RectI,
-    transform2d::Transform2F,
-    vector::{vec2f, Vector2F},
+use crate::{
+    geometry::vector::{vec2f, Vector2F},
+    platform,
 };
 use anyhow::{anyhow, Result};
-use cocoa::appkit::{CGFloat, CGPoint};
-use core_graphics::{
-    base::CGGlyph, color_space::CGColorSpace, context::CGContext, geometry::CGAffineTransform,
-};
+use font_kit::metrics::Metrics;
 pub use font_kit::properties::{Properties, Weight};
-use font_kit::{
-    canvas::RasterizationOptions, font::Font, hinting::HintingOptions,
-    loaders::core_text::NativeFont, metrics::Metrics, source::SystemSource,
-};
-use ordered_float::OrderedFloat;
 use parking_lot::{RwLock, RwLockUpgradableReadGuard};
 use std::{collections::HashMap, sync::Arc};
 
-#[allow(non_upper_case_globals)]
-const kCGImageAlphaOnly: u32 = 7;
-
 pub type GlyphId = u32;
 
 #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
 pub struct FamilyId(usize);
 
 #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
-pub struct FontId(usize);
+pub struct FontId(pub usize);
 
 pub struct FontCache(RwLock<FontCacheState>);
 
 pub struct FontCacheState {
-    source: SystemSource,
+    fonts: Arc<dyn platform::FontSystem>,
     families: Vec<Family>,
-    fonts: Vec<Arc<Font>>,
-    font_names: Vec<Arc<String>>,
     font_selections: HashMap<FamilyId, HashMap<Properties, FontId>>,
     metrics: HashMap<FontId, Metrics>,
-    native_fonts: HashMap<(FontId, OrderedFloat<f32>), NativeFont>,
-    fonts_by_name: HashMap<Arc<String>, FontId>,
-    emoji_font_id: Option<FontId>,
 }
 
 unsafe impl Send for FontCache {}
@@ -50,17 +33,12 @@ struct Family {
 }
 
 impl FontCache {
-    pub fn new() -> Self {
+    pub fn new(fonts: Arc<dyn platform::FontSystem>) -> Self {
         Self(RwLock::new(FontCacheState {
-            source: SystemSource::new(),
+            fonts,
             families: Vec::new(),
-            fonts: Vec::new(),
-            font_names: Vec::new(),
             font_selections: HashMap::new(),
             metrics: HashMap::new(),
-            native_fonts: HashMap::new(),
-            fonts_by_name: HashMap::new(),
-            emoji_font_id: None,
         }))
     }
 
@@ -74,19 +52,16 @@ impl FontCache {
 
             let mut state = RwLockUpgradableReadGuard::upgrade(state);
 
-            if let Ok(handle) = state.source.select_family_by_name(name) {
-                if handle.is_empty() {
+            if let Ok(font_ids) = state.fonts.load_family(name) {
+                if font_ids.is_empty() {
                     continue;
                 }
 
                 let family_id = FamilyId(state.families.len());
-                let mut font_ids = Vec::new();
-                for font in handle.fonts() {
-                    let font = font.load()?;
-                    if font.glyph_for_char('m').is_none() {
+                for font_id in &font_ids {
+                    if state.fonts.glyph_for_char(*font_id, 'm').is_none() {
                         return Err(anyhow!("font must contain a glyph for the 'm' character"));
                     }
-                    font_ids.push(push_font(&mut state, font));
                 }
 
                 state.families.push(Family {
@@ -117,13 +92,10 @@ impl FontCache {
         } else {
             let mut inner = RwLockUpgradableReadGuard::upgrade(inner);
             let family = &inner.families[family_id.0];
-            let candidates = family
-                .font_ids
-                .iter()
-                .map(|font_id| inner.fonts[font_id.0].properties())
-                .collect::<Vec<_>>();
-            let idx = font_kit::matching::find_best_match(&candidates, properties)?;
-            let font_id = family.font_ids[idx];
+            let font_id = inner
+                .fonts
+                .select_font(&family.font_ids, properties)
+                .unwrap_or(family.font_ids[0]);
 
             inner
                 .font_selections
@@ -134,14 +106,6 @@ impl FontCache {
         }
     }
 
-    pub fn font(&self, font_id: FontId) -> Arc<Font> {
-        self.0.read().fonts[font_id.0].clone()
-    }
-
-    pub fn font_name(&self, font_id: FontId) -> Arc<String> {
-        self.0.read().font_names[font_id.0].clone()
-    }
-
     pub fn metric<F, T>(&self, font_id: FontId, f: F) -> T
     where
         F: FnOnce(&Metrics) -> T,
@@ -151,7 +115,7 @@ impl FontCache {
         if let Some(metrics) = state.metrics.get(&font_id) {
             f(metrics)
         } else {
-            let metrics = state.fonts[font_id.0].metrics();
+            let metrics = state.fonts.font_metrics(font_id);
             let metric = f(&metrics);
             let mut state = RwLockUpgradableReadGuard::upgrade(state);
             state.metrics.insert(font_id, metrics);
@@ -159,13 +123,6 @@ impl FontCache {
         }
     }
 
-    pub fn is_emoji(&self, font_id: FontId) -> bool {
-        self.0
-            .read()
-            .emoji_font_id
-            .map_or(false, |emoji_font_id| emoji_font_id == font_id)
-    }
-
     pub fn bounding_box(&self, font_id: FontId, font_size: f32) -> Vector2F {
         let bounding_box = self.metric(font_id, |m| m.bounding_box);
         let width = self.scale_metric(bounding_box.width(), font_id, font_size);
@@ -173,6 +130,13 @@ impl FontCache {
         vec2f(width, height)
     }
 
+    pub fn em_width(&self, font_id: FontId, font_size: f32) -> f32 {
+        let state = self.0.read();
+        let glyph_id = state.fonts.glyph_for_char(font_id, 'm').unwrap();
+        let bounds = state.fonts.typographic_bounds(font_id, glyph_id).unwrap();
+        self.scale_metric(bounds.width(), font_id, font_size)
+    }
+
     pub fn line_height(&self, font_id: FontId, font_size: f32) -> f32 {
         let bounding_box = self.metric(font_id, |m| m.bounding_box);
         self.scale_metric(bounding_box.height(), font_id, font_size)
@@ -190,123 +154,9 @@ impl FontCache {
         self.scale_metric(self.metric(font_id, |m| m.descent), font_id, font_size)
     }
 
-    pub fn render_glyph(
-        &self,
-        font_id: FontId,
-        font_size: f32,
-        glyph_id: GlyphId,
-        scale_factor: f32,
-    ) -> Option<(RectI, Vec<u8>)> {
-        let font = self.font(font_id);
-        let scale = Transform2F::from_scale(scale_factor);
-        let bounds = font
-            .raster_bounds(
-                glyph_id,
-                font_size,
-                scale,
-                HintingOptions::None,
-                RasterizationOptions::GrayscaleAa,
-            )
-            .ok()?;
-
-        if bounds.width() == 0 || bounds.height() == 0 {
-            None
-        } else {
-            let mut pixels = vec![0; bounds.width() as usize * bounds.height() as usize];
-            let ctx = CGContext::create_bitmap_context(
-                Some(pixels.as_mut_ptr() as *mut _),
-                bounds.width() as usize,
-                bounds.height() as usize,
-                8,
-                bounds.width() as usize,
-                &CGColorSpace::create_device_gray(),
-                kCGImageAlphaOnly,
-            );
-
-            // Move the origin to bottom left and account for scaling, this
-            // makes drawing text consistent with the font-kit's raster_bounds.
-            ctx.translate(0.0, bounds.height() as CGFloat);
-            let transform = scale.translate(-bounds.origin().to_f32());
-            ctx.set_text_matrix(&CGAffineTransform {
-                a: transform.matrix.m11() as CGFloat,
-                b: -transform.matrix.m21() as CGFloat,
-                c: -transform.matrix.m12() as CGFloat,
-                d: transform.matrix.m22() as CGFloat,
-                tx: transform.vector.x() as CGFloat,
-                ty: -transform.vector.y() as CGFloat,
-            });
-
-            ctx.set_font(&font.native_font().copy_to_CGFont());
-            ctx.set_font_size(font_size as CGFloat);
-            ctx.show_glyphs_at_positions(&[glyph_id as CGGlyph], &[CGPoint::new(0.0, 0.0)]);
-
-            Some((bounds, pixels))
-        }
-    }
-
-    fn emoji_font_id(&self) -> Result<FontId> {
-        let state = self.0.upgradable_read();
-
-        if let Some(font_id) = state.emoji_font_id {
-            Ok(font_id)
-        } else {
-            let handle = state.source.select_family_by_name("Apple Color Emoji")?;
-            let font = handle
-                .fonts()
-                .first()
-                .ok_or(anyhow!("no fonts in Apple Color Emoji font family"))?
-                .load()?;
-            let mut state = RwLockUpgradableReadGuard::upgrade(state);
-            let font_id = push_font(&mut state, font);
-            state.emoji_font_id = Some(font_id);
-            Ok(font_id)
-        }
-    }
-
     pub fn scale_metric(&self, metric: f32, font_id: FontId, font_size: f32) -> f32 {
         metric * font_size / self.metric(font_id, |m| m.units_per_em as f32)
     }
-
-    pub fn native_font(&self, font_id: FontId, size: f32) -> NativeFont {
-        let native_key = (font_id, OrderedFloat(size));
-
-        let state = self.0.upgradable_read();
-        if let Some(native_font) = state.native_fonts.get(&native_key).cloned() {
-            native_font
-        } else {
-            let native_font = state.fonts[font_id.0]
-                .native_font()
-                .clone_with_font_size(size as f64);
-            RwLockUpgradableReadGuard::upgrade(state)
-                .native_fonts
-                .insert(native_key, native_font.clone());
-            native_font
-        }
-    }
-
-    pub fn font_id_for_native_font(&self, native_font: NativeFont) -> FontId {
-        let postscript_name = native_font.postscript_name();
-        let state = self.0.upgradable_read();
-        if let Some(font_id) = state.fonts_by_name.get(&postscript_name) {
-            *font_id
-        } else {
-            push_font(&mut RwLockUpgradableReadGuard::upgrade(state), unsafe {
-                Font::from_native_font(native_font.clone())
-            })
-        }
-    }
-}
-
-fn push_font(state: &mut FontCacheState, font: Font) -> FontId {
-    let font_id = FontId(state.fonts.len());
-    let name = Arc::new(font.postscript_name().unwrap());
-    if *name == "AppleColorEmoji" {
-        state.emoji_font_id = Some(font_id);
-    }
-    state.fonts.push(Arc::new(font));
-    state.font_names.push(name.clone());
-    state.fonts_by_name.insert(name, font_id);
-    font_id
 }
 
 // #[cfg(test)]

gpui/src/platform/mac/app.rs πŸ”—

@@ -1,5 +1,5 @@
-use super::{BoolExt as _, Dispatcher, Window};
-use crate::{executor, platform, FontCache};
+use super::{BoolExt as _, Dispatcher, FontSystem, Window};
+use crate::{executor, platform};
 use anyhow::Result;
 use cocoa::base::id;
 use objc::{class, msg_send, sel, sel_impl};
@@ -7,12 +7,14 @@ use std::{rc::Rc, sync::Arc};
 
 pub struct App {
     dispatcher: Arc<Dispatcher>,
+    fonts: Arc<FontSystem>,
 }
 
 impl App {
     pub fn new() -> Self {
         Self {
             dispatcher: Arc::new(Dispatcher),
+            fonts: Arc::new(FontSystem::new()),
         }
     }
 }
@@ -33,8 +35,11 @@ impl platform::App for App {
         &self,
         options: platform::WindowOptions,
         executor: Rc<executor::Foreground>,
-        font_cache: Arc<FontCache>,
     ) -> Result<Box<dyn platform::Window>> {
-        Ok(Box::new(Window::open(options, executor, font_cache)?))
+        Ok(Box::new(Window::open(options, executor, self.fonts())?))
+    }
+
+    fn fonts(&self) -> Arc<dyn platform::FontSystem> {
+        self.fonts.clone()
     }
 }

gpui/src/platform/mac/fonts.rs πŸ”—

@@ -0,0 +1,338 @@
+use crate::{
+    fonts::{FontId, GlyphId},
+    geometry::{
+        rect::{RectF, RectI},
+        transform2d::Transform2F,
+        vector::vec2f,
+    },
+    platform,
+    text_layout::{Glyph, Line, Run},
+};
+use cocoa::appkit::{CGFloat, CGPoint};
+use core_foundation::{
+    attributed_string::CFMutableAttributedString,
+    base::{CFRange, TCFType},
+    number::CFNumber,
+    string::CFString,
+};
+use core_graphics::{
+    base::CGGlyph, color_space::CGColorSpace, context::CGContext, geometry::CGAffineTransform,
+};
+use core_text::{
+    font_descriptor::kCTFontSizeAttribute, line::CTLine, string_attributes::kCTFontAttributeName,
+};
+use font_kit::{
+    canvas::RasterizationOptions, hinting::HintingOptions, metrics::Metrics,
+    properties::Properties, source::SystemSource,
+};
+use parking_lot::RwLock;
+use std::{char, convert::TryFrom};
+
+#[allow(non_upper_case_globals)]
+const kCGImageAlphaOnly: u32 = 7;
+
+pub struct FontSystem(RwLock<FontSystemState>);
+
+struct FontSystemState {
+    source: SystemSource,
+    fonts: Vec<font_kit::font::Font>,
+}
+
+impl FontSystem {
+    pub fn new() -> Self {
+        Self(RwLock::new(FontSystemState {
+            source: SystemSource::new(),
+            fonts: Vec::new(),
+        }))
+    }
+}
+
+impl platform::FontSystem for FontSystem {
+    fn load_family(&self, name: &str) -> anyhow::Result<Vec<FontId>> {
+        self.0.write().load_family(name)
+    }
+
+    fn select_font(&self, font_ids: &[FontId], properties: &Properties) -> anyhow::Result<FontId> {
+        self.0.read().select_font(font_ids, properties)
+    }
+
+    fn font_metrics(&self, font_id: FontId) -> Metrics {
+        self.0.read().font_metrics(font_id)
+    }
+
+    fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result<RectF> {
+        self.0.read().typographic_bounds(font_id, glyph_id)
+    }
+
+    fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
+        self.0.read().glyph_for_char(font_id, ch)
+    }
+
+    fn rasterize_glyph(
+        &self,
+        font_id: FontId,
+        font_size: f32,
+        glyph_id: GlyphId,
+        scale_factor: f32,
+    ) -> Option<(RectI, Vec<u8>)> {
+        self.0
+            .read()
+            .rasterize_glyph(font_id, font_size, glyph_id, scale_factor)
+    }
+
+    fn layout_str(
+        &self,
+        text: &str,
+        font_size: f32,
+        runs: &[(std::ops::Range<usize>, FontId)],
+    ) -> Line {
+        self.0.read().layout_str(text, font_size, runs)
+    }
+}
+
+impl FontSystemState {
+    fn load_family(&mut self, name: &str) -> anyhow::Result<Vec<FontId>> {
+        let mut font_ids = Vec::new();
+        for font in self.source.select_family_by_name(name)?.fonts() {
+            let font = font.load()?;
+            font_ids.push(FontId(self.fonts.len()));
+            self.fonts.push(font);
+        }
+        Ok(font_ids)
+    }
+
+    fn select_font(&self, font_ids: &[FontId], properties: &Properties) -> anyhow::Result<FontId> {
+        let candidates = font_ids
+            .iter()
+            .map(|font_id| self.fonts[font_id.0].properties())
+            .collect::<Vec<_>>();
+        let idx = font_kit::matching::find_best_match(&candidates, properties)?;
+        Ok(font_ids[idx])
+    }
+
+    fn font_metrics(&self, font_id: FontId) -> Metrics {
+        self.fonts[font_id.0].metrics()
+    }
+
+    fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result<RectF> {
+        Ok(self.fonts[font_id.0].typographic_bounds(glyph_id)?)
+    }
+
+    fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
+        self.fonts[font_id.0].glyph_for_char(ch)
+    }
+
+    fn rasterize_glyph(
+        &self,
+        font_id: FontId,
+        font_size: f32,
+        glyph_id: GlyphId,
+        scale_factor: f32,
+    ) -> Option<(RectI, Vec<u8>)> {
+        let font = &self.fonts[font_id.0];
+        let scale = Transform2F::from_scale(scale_factor);
+        let bounds = font
+            .raster_bounds(
+                glyph_id,
+                font_size,
+                scale,
+                HintingOptions::None,
+                RasterizationOptions::GrayscaleAa,
+            )
+            .ok()?;
+
+        if bounds.width() == 0 || bounds.height() == 0 {
+            None
+        } else {
+            let mut pixels = vec![0; bounds.width() as usize * bounds.height() as usize];
+            let ctx = CGContext::create_bitmap_context(
+                Some(pixels.as_mut_ptr() as *mut _),
+                bounds.width() as usize,
+                bounds.height() as usize,
+                8,
+                bounds.width() as usize,
+                &CGColorSpace::create_device_gray(),
+                kCGImageAlphaOnly,
+            );
+
+            // Move the origin to bottom left and account for scaling, this
+            // makes drawing text consistent with the font-kit's raster_bounds.
+            ctx.translate(0.0, bounds.height() as CGFloat);
+            let transform = scale.translate(-bounds.origin().to_f32());
+            ctx.set_text_matrix(&CGAffineTransform {
+                a: transform.matrix.m11() as CGFloat,
+                b: -transform.matrix.m21() as CGFloat,
+                c: -transform.matrix.m12() as CGFloat,
+                d: transform.matrix.m22() as CGFloat,
+                tx: transform.vector.x() as CGFloat,
+                ty: -transform.vector.y() as CGFloat,
+            });
+
+            ctx.set_font(&font.native_font().copy_to_CGFont());
+            ctx.set_font_size(font_size as CGFloat);
+            ctx.show_glyphs_at_positions(&[glyph_id as CGGlyph], &[CGPoint::new(0.0, 0.0)]);
+
+            Some((bounds, pixels))
+        }
+    }
+
+    fn layout_str(
+        &self,
+        text: &str,
+        font_size: f32,
+        runs: &[(std::ops::Range<usize>, FontId)],
+    ) -> Line {
+        let font_id_attr_name = CFString::from_static_string("zed_font_id");
+
+        let mut string = CFMutableAttributedString::new();
+        string.replace_str(&CFString::new(text), CFRange::init(0, 0));
+
+        let mut utf16_lens = text.chars().map(|c| c.len_utf16());
+        let mut prev_char_ix = 0;
+        let mut prev_utf16_ix = 0;
+
+        for (range, font_id) in runs {
+            let utf16_start = prev_utf16_ix
+                + utf16_lens
+                    .by_ref()
+                    .take(range.start - prev_char_ix)
+                    .sum::<usize>();
+            let utf16_end = utf16_start
+                + utf16_lens
+                    .by_ref()
+                    .take(range.end - range.start)
+                    .sum::<usize>();
+            prev_char_ix = range.end;
+            prev_utf16_ix = utf16_end;
+
+            let cf_range = CFRange::init(utf16_start as isize, (utf16_end - utf16_start) as isize);
+            let font = &self.fonts[font_id.0];
+            unsafe {
+                string.set_attribute(
+                    cf_range,
+                    kCTFontAttributeName,
+                    &font.native_font().clone_with_font_size(font_size as f64),
+                );
+                string.set_attribute(
+                    cf_range,
+                    font_id_attr_name.as_concrete_TypeRef(),
+                    &CFNumber::from(font_id.0 as i64),
+                );
+            }
+        }
+
+        let line = CTLine::new_with_attributed_string(string.as_concrete_TypeRef());
+
+        let width = line.get_typographic_bounds().width as f32;
+
+        let mut utf16_chars = text.encode_utf16();
+        let mut char_ix = 0;
+        let mut prev_utf16_ix = 0;
+
+        let mut runs = Vec::new();
+        for run in line.glyph_runs().into_iter() {
+            let font_id = FontId(
+                run.attributes()
+                    .unwrap()
+                    .get(&font_id_attr_name)
+                    .downcast::<CFNumber>()
+                    .unwrap()
+                    .to_i64()
+                    .unwrap() as usize,
+            );
+
+            let mut glyphs = Vec::new();
+            for ((glyph_id, position), utf16_ix) in run
+                .glyphs()
+                .iter()
+                .zip(run.positions().iter())
+                .zip(run.string_indices().iter())
+            {
+                let utf16_ix = usize::try_from(*utf16_ix).unwrap();
+                char_ix +=
+                    char::decode_utf16(utf16_chars.by_ref().take(utf16_ix - prev_utf16_ix)).count();
+                prev_utf16_ix = utf16_ix;
+
+                glyphs.push(Glyph {
+                    id: *glyph_id as GlyphId,
+                    position: vec2f(position.x as f32, position.y as f32),
+                    index: char_ix,
+                });
+            }
+
+            runs.push(Run { font_id, glyphs })
+        }
+
+        Line {
+            width,
+            runs,
+            font_size,
+            len: char_ix + 1,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use font_kit::properties::{Style, Weight};
+    use platform::FontSystem as _;
+
+    #[test]
+    fn test_layout_str() -> anyhow::Result<()> {
+        let fonts = FontSystem::new();
+        let menlo = fonts.load_family("Menlo")?;
+        let menlo_regular = fonts.select_font(&menlo, &Properties::new())?;
+        let menlo_italic = fonts.select_font(&menlo, &Properties::new().style(Style::Italic))?;
+        let menlo_bold = fonts.select_font(&menlo, &Properties::new().weight(Weight::BOLD))?;
+
+        let line = fonts.layout_str(
+            "hello world",
+            16.0,
+            &[
+                (0..2, menlo_bold),
+                (2..6, menlo_italic),
+                (6..11, menlo_regular),
+            ],
+        );
+        assert_eq!(line.runs.len(), 3);
+        assert_eq!(line.runs[0].font_id, menlo_bold);
+        assert_eq!(line.runs[0].glyphs.len(), 2);
+        assert_eq!(line.runs[1].font_id, menlo_italic);
+        assert_eq!(line.runs[1].glyphs.len(), 4);
+        assert_eq!(line.runs[2].font_id, menlo_regular);
+        assert_eq!(line.runs[2].glyphs.len(), 5);
+        Ok(())
+    }
+
+    #[test]
+    fn test_char_indices() -> anyhow::Result<()> {
+        let fonts = FontSystem::new();
+        let zapfino = fonts.load_family("Zapfino")?;
+        let zapfino_regular = fonts.select_font(&zapfino, &Properties::new())?;
+        let menlo = fonts.load_family("Menlo")?;
+        let menlo_regular = fonts.select_font(&menlo, &Properties::new())?;
+
+        let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈";
+        let line = fonts.layout_str(
+            text,
+            16.0,
+            &[
+                (0..9, zapfino_regular),
+                (9..22, menlo_regular),
+                (22..text.encode_utf16().count(), zapfino_regular),
+            ],
+        );
+        assert_eq!(
+            line.runs
+                .iter()
+                .flat_map(|r| r.glyphs.iter())
+                .map(|g| g.index)
+                .collect::<Vec<_>>(),
+            vec![
+                0, 2, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 30, 31
+            ]
+        );
+        Ok(())
+    }
+}

gpui/src/platform/mac/mod.rs πŸ”—

@@ -1,6 +1,7 @@
 mod app;
 mod dispatcher;
 mod event;
+mod fonts;
 mod geometry;
 mod renderer;
 mod runner;
@@ -11,6 +12,7 @@ use crate::platform;
 pub use app::App;
 use cocoa::base::{BOOL, NO, YES};
 pub use dispatcher::Dispatcher;
+pub use fonts::FontSystem;
 pub use runner::Runner;
 use window::Window;
 

gpui/src/platform/mac/renderer.rs πŸ”—

@@ -2,8 +2,9 @@ use super::{sprite_cache::SpriteCache, window::RenderContext};
 use crate::{
     color::ColorU,
     geometry::vector::{vec2i, Vector2I},
+    platform,
     scene::Layer,
-    FontCache, Scene,
+    Scene,
 };
 use anyhow::{anyhow, Result};
 use metal::{MTLResourceOptions, NSRange};
@@ -27,7 +28,7 @@ impl Renderer {
     pub fn new(
         device: metal::Device,
         pixel_format: metal::MTLPixelFormat,
-        font_cache: Arc<FontCache>,
+        fonts: Arc<dyn platform::FontSystem>,
     ) -> Result<Self> {
         let library = device
             .new_library_with_data(SHADERS_METALLIB)
@@ -53,7 +54,7 @@ impl Renderer {
 
         let atlas_size: Vector2I = vec2i(1024, 768);
         Ok(Self {
-            sprite_cache: SpriteCache::new(device.clone(), atlas_size, font_cache),
+            sprite_cache: SpriteCache::new(device.clone(), atlas_size, fonts),
             quad_pipeline_state: build_pipeline_state(
                 &device,
                 &library,

gpui/src/platform/mac/sprite_cache.rs πŸ”—

@@ -1,16 +1,15 @@
-use std::{collections::HashMap, sync::Arc};
-
 use crate::{
     fonts::{FontId, GlyphId},
     geometry::{
         rect::RectI,
         vector::{vec2i, Vector2I},
     },
-    FontCache,
+    platform,
 };
 use etagere::BucketedAtlasAllocator;
 use metal::{MTLPixelFormat, TextureDescriptor};
 use ordered_float::OrderedFloat;
+use std::{collections::HashMap, sync::Arc};
 
 #[derive(Hash, Eq, PartialEq)]
 struct GlyphDescriptor {
@@ -30,18 +29,22 @@ pub struct GlyphSprite {
 pub struct SpriteCache {
     device: metal::Device,
     atlas_size: Vector2I,
-    font_cache: Arc<FontCache>,
+    fonts: Arc<dyn platform::FontSystem>,
     atlasses: Vec<Atlas>,
     glyphs: HashMap<GlyphDescriptor, Option<GlyphSprite>>,
 }
 
 impl SpriteCache {
-    pub fn new(device: metal::Device, size: Vector2I, font_cache: Arc<FontCache>) -> Self {
+    pub fn new(
+        device: metal::Device,
+        size: Vector2I,
+        fonts: Arc<dyn platform::FontSystem>,
+    ) -> Self {
         let atlasses = vec![Atlas::new(&device, size)];
         Self {
             device,
             atlas_size: size,
-            font_cache,
+            fonts,
             atlasses,
             glyphs: Default::default(),
         }
@@ -58,7 +61,7 @@ impl SpriteCache {
         glyph_id: GlyphId,
         scale_factor: f32,
     ) -> Option<GlyphSprite> {
-        let font_cache = &self.font_cache;
+        let fonts = &self.fonts;
         let atlasses = &mut self.atlasses;
         let atlas_size = self.atlas_size;
         let device = &self.device;
@@ -70,7 +73,7 @@ impl SpriteCache {
             })
             .or_insert_with(|| {
                 let (glyph_bounds, mask) =
-                    font_cache.render_glyph(font_id, font_size, glyph_id, scale_factor)?;
+                    fonts.rasterize_glyph(font_id, font_size, glyph_id, scale_factor)?;
                 assert!(glyph_bounds.width() < atlas_size.x());
                 assert!(glyph_bounds.height() < atlas_size.y());
 

gpui/src/platform/mac/window.rs πŸ”—

@@ -2,7 +2,7 @@ use crate::{
     executor,
     geometry::vector::Vector2F,
     platform::{self, Event, WindowContext},
-    FontCache, Scene,
+    Scene,
 };
 use anyhow::{anyhow, Result};
 use cocoa::{
@@ -141,7 +141,7 @@ impl Window {
     pub fn open(
         options: platform::WindowOptions,
         executor: Rc<executor::Foreground>,
-        font_cache: Arc<FontCache>,
+        fonts: Arc<dyn platform::FontSystem>,
     ) -> Result<Self> {
         const PIXEL_FORMAT: metal::MTLPixelFormat = metal::MTLPixelFormat::BGRA8Unorm;
 
@@ -194,7 +194,7 @@ impl Window {
                 synthetic_drag_counter: 0,
                 executor,
                 scene_to_render: Default::default(),
-                renderer: Renderer::new(device.clone(), PIXEL_FORMAT, font_cache)?,
+                renderer: Renderer::new(device.clone(), PIXEL_FORMAT, fonts)?,
                 command_queue: device.new_command_queue(),
                 device,
                 layer,

gpui/src/platform/mod.rs πŸ”—

@@ -8,13 +8,19 @@ pub mod current {
 
 use crate::{
     executor,
-    geometry::{rect::RectF, vector::Vector2F},
-    FontCache, Scene,
+    fonts::{FontId, GlyphId},
+    geometry::{
+        rect::{RectF, RectI},
+        vector::Vector2F,
+    },
+    text_layout::Line,
+    Scene,
 };
 use anyhow::Result;
 use async_task::Runnable;
 pub use event::Event;
-use std::{path::PathBuf, rc::Rc, sync::Arc};
+use font_kit::{metrics::Metrics as FontMetrics, properties::Properties as FontProperties};
+use std::{ops::Range, path::PathBuf, rc::Rc, sync::Arc};
 
 pub trait Runner {
     fn on_finish_launching<F: 'static + FnOnce()>(self, callback: F) -> Self where;
@@ -32,8 +38,8 @@ pub trait App {
         &self,
         options: WindowOptions,
         executor: Rc<executor::Foreground>,
-        font_cache: Arc<FontCache>,
     ) -> Result<Box<dyn Window>>;
+    fn fonts(&self) -> Arc<dyn FontSystem>;
 }
 
 pub trait Dispatcher: Send + Sync {
@@ -56,3 +62,23 @@ pub struct WindowOptions<'a> {
     pub bounds: RectF,
     pub title: Option<&'a str>,
 }
+
+pub trait FontSystem: Send + Sync {
+    fn load_family(&self, name: &str) -> anyhow::Result<Vec<FontId>>;
+    fn select_font(
+        &self,
+        font_ids: &[FontId],
+        properties: &FontProperties,
+    ) -> anyhow::Result<FontId>;
+    fn font_metrics(&self, font_id: FontId) -> FontMetrics;
+    fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result<RectF>;
+    fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
+    fn rasterize_glyph(
+        &self,
+        font_id: FontId,
+        font_size: f32,
+        glyph_id: GlyphId,
+        scale_factor: f32,
+    ) -> Option<(RectI, Vec<u8>)>;
+    fn layout_str(&self, text: &str, font_size: f32, runs: &[(Range<usize>, FontId)]) -> Line;
+}

gpui/src/presenter.rs πŸ”—

@@ -2,7 +2,7 @@ use crate::{
     app::{AppContext, MutableAppContext, WindowInvalidation},
     elements::Element,
     fonts::FontCache,
-    platform::Event,
+    platform::{self, Event},
     text_layout::TextLayoutCache,
     AssetCache, ElementBox, Scene,
 };
@@ -22,6 +22,7 @@ impl Presenter {
     pub fn new(
         window_id: usize,
         font_cache: Arc<FontCache>,
+        fonts: Arc<dyn platform::FontSystem>,
         asset_cache: Arc<AssetCache>,
         app: &MutableAppContext,
     ) -> Self {
@@ -30,7 +31,7 @@ impl Presenter {
             rendered_views: app.render_views(window_id).unwrap(),
             parents: HashMap::new(),
             font_cache,
-            text_layout_cache: TextLayoutCache::new(),
+            text_layout_cache: TextLayoutCache::new(fonts),
             asset_cache,
         }
     }

gpui/src/text_layout.rs πŸ”—

@@ -2,7 +2,7 @@ use crate::{
     color::ColorU,
     fonts::{FontCache, FontId, GlyphId},
     geometry::rect::RectF,
-    scene, PaintContext,
+    platform, scene, PaintContext,
 };
 use core_foundation::{
     attributed_string::CFMutableAttributedString,
@@ -27,13 +27,15 @@ use std::{
 pub struct TextLayoutCache {
     prev_frame: Mutex<HashMap<CacheKeyValue, Arc<Line>>>,
     curr_frame: RwLock<HashMap<CacheKeyValue, Arc<Line>>>,
+    fonts: Arc<dyn platform::FontSystem>,
 }
 
 impl TextLayoutCache {
-    pub fn new() -> Self {
+    pub fn new(fonts: Arc<dyn platform::FontSystem>) -> Self {
         Self {
             prev_frame: Mutex::new(HashMap::new()),
             curr_frame: RwLock::new(HashMap::new()),
+            fonts,
         }
     }
 
@@ -49,7 +51,6 @@ impl TextLayoutCache {
         text: &'a str,
         font_size: f32,
         runs: &'a [(Range<usize>, FontId)],
-        font_cache: &'a FontCache,
     ) -> Arc<Line> {
         let key = &CacheKeyRef {
             text,
@@ -66,7 +67,7 @@ impl TextLayoutCache {
             curr_frame.insert(key, line.clone());
             line.clone()
         } else {
-            let line = Arc::new(layout_str(text, font_size, runs, font_cache));
+            let line = Arc::new(self.fonts.layout_str(text, font_size, runs));
             let key = CacheKeyValue {
                 text: text.into(),
                 font_size: OrderedFloat(font_size),
@@ -138,12 +139,12 @@ impl<'a> CacheKey for CacheKeyRef<'a> {
     }
 }
 
-#[derive(Default)]
+#[derive(Default, Debug)]
 pub struct Line {
     pub width: f32,
     pub runs: Vec<Run>,
     pub len: usize,
-    font_size: f32,
+    pub font_size: f32,
 }
 
 #[derive(Debug)]
@@ -192,21 +193,7 @@ impl Line {
 
         for run in &self.runs {
             let bounding_box = ctx.font_cache.bounding_box(run.font_id, self.font_size);
-            let ascent = ctx.font_cache.scale_metric(
-                ctx.font_cache.metric(run.font_id, |m| m.ascent),
-                run.font_id,
-                self.font_size,
-            );
-            let descent = ctx.font_cache.scale_metric(
-                ctx.font_cache.metric(run.font_id, |m| m.descent),
-                run.font_id,
-                self.font_size,
-            );
-
             let max_glyph_width = bounding_box.x();
-            let font = ctx.font_cache.font(run.font_id);
-            let font_name = ctx.font_cache.font_name(run.font_id);
-            let is_emoji = ctx.font_cache.is_emoji(run.font_id);
             for glyph in &run.glyphs {
                 let glyph_origin = bounds.origin() + glyph.position;
                 if glyph_origin.x() + max_glyph_width < bounds.origin().x() {
@@ -236,153 +223,3 @@ impl Line {
         }
     }
 }
-
-pub fn layout_str(
-    text: &str,
-    font_size: f32,
-    runs: &[(Range<usize>, FontId)],
-    font_cache: &FontCache,
-) -> Line {
-    let mut string = CFMutableAttributedString::new();
-    string.replace_str(&CFString::new(text), CFRange::init(0, 0));
-
-    let mut utf16_lens = text.chars().map(|c| c.len_utf16());
-    let mut prev_char_ix = 0;
-    let mut prev_utf16_ix = 0;
-
-    for (range, font_id) in runs {
-        let utf16_start = prev_utf16_ix
-            + utf16_lens
-                .by_ref()
-                .take(range.start - prev_char_ix)
-                .sum::<usize>();
-        let utf16_end = utf16_start
-            + utf16_lens
-                .by_ref()
-                .take(range.end - range.start)
-                .sum::<usize>();
-        prev_char_ix = range.end;
-        prev_utf16_ix = utf16_end;
-
-        let cf_range = CFRange::init(utf16_start as isize, (utf16_end - utf16_start) as isize);
-        let native_font = font_cache.native_font(*font_id, font_size);
-        unsafe {
-            string.set_attribute(cf_range, kCTFontAttributeName, &native_font);
-        }
-    }
-
-    let line = CTLine::new_with_attributed_string(string.as_concrete_TypeRef());
-
-    let width = line.get_typographic_bounds().width as f32;
-
-    let mut utf16_chars = text.encode_utf16();
-    let mut char_ix = 0;
-    let mut prev_utf16_ix = 0;
-
-    let mut runs = Vec::new();
-    for run in line.glyph_runs().into_iter() {
-        let font_id = font_cache.font_id_for_native_font(unsafe {
-            run.attributes()
-                .unwrap()
-                .get(kCTFontAttributeName)
-                .downcast::<CTFont>()
-                .unwrap()
-        });
-
-        let mut glyphs = Vec::new();
-        for ((glyph_id, position), utf16_ix) in run
-            .glyphs()
-            .iter()
-            .zip(run.positions().iter())
-            .zip(run.string_indices().iter())
-        {
-            let utf16_ix = usize::try_from(*utf16_ix).unwrap();
-            char_ix +=
-                char::decode_utf16(utf16_chars.by_ref().take(utf16_ix - prev_utf16_ix)).count();
-            prev_utf16_ix = utf16_ix;
-
-            glyphs.push(Glyph {
-                id: *glyph_id as GlyphId,
-                position: vec2f(position.x as f32, position.y as f32),
-                index: char_ix,
-            });
-        }
-
-        runs.push(Run { font_id, glyphs })
-    }
-
-    Line {
-        width,
-        runs,
-        font_size,
-        len: char_ix + 1,
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use anyhow::Result;
-    use font_kit::properties::{
-        Properties as FontProperties, Style as FontStyle, Weight as FontWeight,
-    };
-
-    #[test]
-    fn test_layout_str() -> Result<()> {
-        let mut font_cache = FontCache::new();
-        let menlo = font_cache.load_family(&["Menlo"])?;
-        let menlo_regular = font_cache.select_font(menlo, &FontProperties::new())?;
-        let menlo_italic =
-            font_cache.select_font(menlo, &FontProperties::new().style(FontStyle::Italic))?;
-        let menlo_bold =
-            font_cache.select_font(menlo, &FontProperties::new().weight(FontWeight::BOLD))?;
-
-        let line = layout_str(
-            "hello world πŸ˜ƒ",
-            16.0,
-            &[
-                (0..2, menlo_bold),
-                (2..6, menlo_italic),
-                (6..13, menlo_regular),
-            ],
-            &mut font_cache,
-        );
-
-        assert!(font_cache.is_emoji(line.runs.last().unwrap().font_id));
-
-        Ok(())
-    }
-
-    #[test]
-    fn test_char_indices() -> Result<()> {
-        let mut font_cache = FontCache::new();
-        let zapfino = font_cache.load_family(&["Zapfino"])?;
-        let zapfino_regular = font_cache.select_font(zapfino, &FontProperties::new())?;
-        let menlo = font_cache.load_family(&["Menlo"])?;
-        let menlo_regular = font_cache.select_font(menlo, &FontProperties::new())?;
-
-        let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈";
-        let line = layout_str(
-            text,
-            16.0,
-            &[
-                (0..9, zapfino_regular),
-                (11..22, menlo_regular),
-                (22..text.encode_utf16().count(), zapfino_regular),
-            ],
-            &mut font_cache,
-        );
-        assert_eq!(
-            line.runs
-                .iter()
-                .flat_map(|r| r.glyphs.iter())
-                .map(|g| g.index)
-                .collect::<Vec<_>>(),
-            vec![
-                0, 2, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
-                31, 32
-            ]
-        );
-        Ok(())
-    }
-}

zed/src/editor/buffer_view.rs πŸ”—

@@ -887,10 +887,7 @@ impl BufferView {
     pub fn em_width(&self, font_cache: &FontCache) -> f32 {
         let settings = smol::block_on(self.settings.read());
         let font_id = font_cache.default_font(settings.buffer_font_family);
-        let font = font_cache.font(font_id);
-        let glyph_id = font.glyph_for_char('m').unwrap();
-        let bounds = font.typographic_bounds(glyph_id).unwrap();
-        font_cache.scale_metric(bounds.width(), font_id, settings.buffer_font_size)
+        font_cache.em_width(font_id, settings.buffer_font_size)
     }
 
     // TODO: Can we make this not return a result?
@@ -914,7 +911,6 @@ impl BufferView {
                 "1".repeat(digit_count).as_str(),
                 font_size,
                 &[(0..digit_count, font_id)],
-                font_cache,
             )
             .width)
     }
@@ -959,7 +955,6 @@ impl BufferView {
                             &line_number,
                             font_size,
                             &[(0..line_number.len(), font_id)],
-                            font_cache,
                         );
                     }
                     Ok(())
@@ -1017,7 +1012,6 @@ impl BufferView {
                                 &line,
                                 font_size,
                                 &[(0..line_len, font_id)],
-                                font_cache,
                             );
                             line.clear();
                             line_len = 0;
@@ -1054,7 +1048,6 @@ impl BufferView {
             &line,
             settings.buffer_font_size,
             &[(0..self.line_len(row, app)? as usize, font_id)],
-            font_cache,
         ))
     }
 
@@ -1273,7 +1266,7 @@ mod tests {
     fn test_selection_with_mouse() {
         App::test((), |mut app| async move {
             let buffer = app.add_model(|_| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n"));
-            let settings = settings::channel(&FontCache::new()).unwrap().1;
+            let settings = settings::channel(&app.font_cache()).unwrap().1;
             let (_, buffer_view) =
                 app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
 
@@ -1372,10 +1365,10 @@ mod tests {
     fn test_layout_line_numbers() -> Result<()> {
         use gpui::{fonts::FontCache, text_layout::TextLayoutCache};
 
-        let font_cache = FontCache::new();
-        let layout_cache = TextLayoutCache::new();
-
         App::test((), |mut app| async move {
+            let font_cache = FontCache::new(app.platform().fonts());
+            let layout_cache = TextLayoutCache::new(app.platform().fonts());
+
             let buffer = app.add_model(|_| Buffer::new(0, sample_text(6, 6)));
 
             let settings = settings::channel(&font_cache).unwrap().1;
@@ -1416,7 +1409,7 @@ mod tests {
                     .unindent(),
                 )
             });
-            let settings = settings::channel(&FontCache::new()).unwrap().1;
+            let settings = settings::channel(&app.font_cache()).unwrap().1;
             let (_, view) =
                 app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
 
@@ -1488,7 +1481,7 @@ mod tests {
     fn test_move_cursor() -> Result<()> {
         App::test((), |mut app| async move {
             let buffer = app.add_model(|_| Buffer::new(0, sample_text(6, 6)));
-            let settings = settings::channel(&FontCache::new()).unwrap().1;
+            let settings = settings::channel(&app.font_cache()).unwrap().1;
             let (_, view) =
                 app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
 

zed/src/file_finder.rs πŸ”—

@@ -409,7 +409,7 @@ mod tests {
             super::init(&mut app);
             editor::init(&mut app);
 
-            let settings = settings::channel(&app.fonts()).unwrap().1;
+            let settings = settings::channel(&app.font_cache()).unwrap().1;
             let workspace = app.add_model(|ctx| Workspace::new(vec![tmp_dir.path().into()], ctx));
             let (window_id, workspace_view) =
                 app.add_window(|ctx| WorkspaceView::new(workspace.clone(), settings, ctx));

zed/src/main.rs πŸ”—

@@ -12,7 +12,7 @@ fn main() {
     init_logger();
 
     let app = gpui::App::new(assets::Assets).unwrap();
-    let (_, settings_rx) = settings::channel(&app.fonts()).unwrap();
+    let (_, settings_rx) = settings::channel(&app.font_cache()).unwrap();
 
     {
         let mut app = app.clone();

zed/src/workspace/mod.rs πŸ”—

@@ -59,7 +59,7 @@ mod tests {
     #[test]
     fn test_open_paths_action() {
         App::test((), |mut app| async move {
-            let settings = settings::channel(&FontCache::new()).unwrap().1;
+            let settings = settings::channel(&app.font_cache()).unwrap().1;
 
             init(&mut app);
 

zed/src/workspace/workspace_view.rs πŸ”—

@@ -339,7 +339,7 @@ mod tests {
                 },
             }));
 
-            let settings = settings::channel(&FontCache::new()).unwrap().1;
+            let settings = settings::channel(&app.font_cache()).unwrap().1;
             let workspace = app.add_model(|ctx| Workspace::new(vec![dir.path().into()], ctx));
             app.finish_pending_tasks().await; // Open and populate worktree.
             let entries = workspace.file_entries(&app);
@@ -409,7 +409,7 @@ mod tests {
                 },
             }));
 
-            let settings = settings::channel(&FontCache::new()).unwrap().1;
+            let settings = settings::channel(&app.font_cache()).unwrap().1;
             let workspace = app.add_model(|ctx| Workspace::new(vec![dir.path().into()], ctx));
             app.finish_pending_tasks().await; // Open and populate worktree.
             let entries = workspace.file_entries(&app);