From 9178e91cc09525acbeac7de3849885b1d61a50a3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 24 Mar 2021 16:51:28 +0100 Subject: [PATCH] Extract platform-dependant `FontSystem` Co-Authored-By: Nathan Sobo --- 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(-) create mode 100644 gpui/src/platform/mac/fonts.rs diff --git a/gpui/src/app.rs b/gpui/src/app.rs index eedf8e89c9bce4dc7bcbf184676b0d28807553d1..4da09387b89405057df00a5f6828802905fb9a71 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -250,8 +250,8 @@ impl App { self.0.borrow().finish_pending_tasks() } - pub fn fonts(&self) -> Arc { - self.0.borrow().fonts.clone() + pub fn font_cache(&self) -> Arc { + self.0.borrow().font_cache.clone() } pub fn platform(&self) -> Arc { @@ -295,7 +295,7 @@ type GlobalActionCallback = dyn FnMut(&dyn Any, &mut MutableAppContext); pub struct MutableAppContext { weak_self: Option>>, platform: Arc, - fonts: Arc, + font_cache: Arc, assets: Arc, ctx: AppContext, actions: HashMap>>>, @@ -325,10 +325,11 @@ impl MutableAppContext { platform: Arc, 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, ))); diff --git a/gpui/src/elements/label.rs b/gpui/src/elements/label.rs index 83ca3c7db9302515269b8d97979f2974fc9a1c76..423624054830aedc56547a6d765448c3d90edb55 100644 --- a/gpui/src/elements/label.rs +++ b/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()), diff --git a/gpui/src/fonts.rs b/gpui/src/fonts.rs index fe0f781c4e5af12ed3bcc3702b5af6f46b9584ce..fc86c741fc7d01746dddcbb425527ffd3be5e102 100644 --- a/gpui/src/fonts.rs +++ b/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); pub struct FontCacheState { - source: SystemSource, + fonts: Arc, families: Vec, - fonts: Vec>, - font_names: Vec>, font_selections: HashMap>, metrics: HashMap, - native_fonts: HashMap<(FontId, OrderedFloat), NativeFont>, - fonts_by_name: HashMap, FontId>, - emoji_font_id: Option, } unsafe impl Send for FontCache {} @@ -50,17 +33,12 @@ struct Family { } impl FontCache { - pub fn new() -> Self { + pub fn new(fonts: Arc) -> 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::>(); - 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 { - self.0.read().fonts[font_id.0].clone() - } - - pub fn font_name(&self, font_id: FontId) -> Arc { - self.0.read().font_names[font_id.0].clone() - } - pub fn metric(&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)> { - 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 { - 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)] diff --git a/gpui/src/platform/mac/app.rs b/gpui/src/platform/mac/app.rs index cb1f3cca91d835183e0be28abe7bbc1bc667d295..daf8335f6081a003a8c6672d3bdee253a754a9d3 100644 --- a/gpui/src/platform/mac/app.rs +++ b/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, + fonts: Arc, } 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, - font_cache: Arc, ) -> Result> { - Ok(Box::new(Window::open(options, executor, font_cache)?)) + Ok(Box::new(Window::open(options, executor, self.fonts())?)) + } + + fn fonts(&self) -> Arc { + self.fonts.clone() } } diff --git a/gpui/src/platform/mac/fonts.rs b/gpui/src/platform/mac/fonts.rs new file mode 100644 index 0000000000000000000000000000000000000000..59afa002c3533009369c99bc568ca3543b2c08d8 --- /dev/null +++ b/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); + +struct FontSystemState { + source: SystemSource, + fonts: Vec, +} + +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> { + self.0.write().load_family(name) + } + + fn select_font(&self, font_ids: &[FontId], properties: &Properties) -> anyhow::Result { + 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 { + self.0.read().typographic_bounds(font_id, glyph_id) + } + + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { + 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)> { + 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, FontId)], + ) -> Line { + self.0.read().layout_str(text, font_size, runs) + } +} + +impl FontSystemState { + fn load_family(&mut self, name: &str) -> anyhow::Result> { + 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 { + let candidates = font_ids + .iter() + .map(|font_id| self.fonts[font_id.0].properties()) + .collect::>(); + 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 { + Ok(self.fonts[font_id.0].typographic_bounds(glyph_id)?) + } + + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { + 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)> { + 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, 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::(); + let utf16_end = utf16_start + + utf16_lens + .by_ref() + .take(range.end - range.start) + .sum::(); + 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::() + .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![ + 0, 2, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 30, 31 + ] + ); + Ok(()) + } +} diff --git a/gpui/src/platform/mac/mod.rs b/gpui/src/platform/mac/mod.rs index 0ddd8f80c7600129c77b9edb3d398fb84e4bc477..1585974e8faddf5c0b4c1c3b4df94591fa30e696 100644 --- a/gpui/src/platform/mac/mod.rs +++ b/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; diff --git a/gpui/src/platform/mac/renderer.rs b/gpui/src/platform/mac/renderer.rs index c129dca65b99f9d18b57fb30707071edcb82181c..084171a1b42464b3b5032046e0838648725cb84c 100644 --- a/gpui/src/platform/mac/renderer.rs +++ b/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, + fonts: Arc, ) -> Result { 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, diff --git a/gpui/src/platform/mac/sprite_cache.rs b/gpui/src/platform/mac/sprite_cache.rs index 650e7429d658b211e21cb6f3696af500695bff59..891ecb42c0f98c3bff6ae9e6e270cfbd1306947a 100644 --- a/gpui/src/platform/mac/sprite_cache.rs +++ b/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, + fonts: Arc, atlasses: Vec, glyphs: HashMap>, } impl SpriteCache { - pub fn new(device: metal::Device, size: Vector2I, font_cache: Arc) -> Self { + pub fn new( + device: metal::Device, + size: Vector2I, + fonts: Arc, + ) -> 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 { - 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()); diff --git a/gpui/src/platform/mac/window.rs b/gpui/src/platform/mac/window.rs index 98e40aa6a3fdd5ff502b11a63b87d517644551e1..c5dde159f319148473f3b790163926670bec5176 100644 --- a/gpui/src/platform/mac/window.rs +++ b/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, - font_cache: Arc, + fonts: Arc, ) -> Result { 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, diff --git a/gpui/src/platform/mod.rs b/gpui/src/platform/mod.rs index a5e2ed3411d664eb3eb19ce02249b0ddbc872a53..666f654ac2cea174a9e65a51761a8abcff838683 100644 --- a/gpui/src/platform/mod.rs +++ b/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(self, callback: F) -> Self where; @@ -32,8 +38,8 @@ pub trait App { &self, options: WindowOptions, executor: Rc, - font_cache: Arc, ) -> Result>; + fn fonts(&self) -> Arc; } 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>; + fn select_font( + &self, + font_ids: &[FontId], + properties: &FontProperties, + ) -> anyhow::Result; + fn font_metrics(&self, font_id: FontId) -> FontMetrics; + fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result; + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option; + fn rasterize_glyph( + &self, + font_id: FontId, + font_size: f32, + glyph_id: GlyphId, + scale_factor: f32, + ) -> Option<(RectI, Vec)>; + fn layout_str(&self, text: &str, font_size: f32, runs: &[(Range, FontId)]) -> Line; +} diff --git a/gpui/src/presenter.rs b/gpui/src/presenter.rs index 717b4c8f313da7c82ed247b5db663f17aa041982..dc7db6041293056197edcf4dca4b1ef78417ac48 100644 --- a/gpui/src/presenter.rs +++ b/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, + fonts: Arc, asset_cache: Arc, 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, } } diff --git a/gpui/src/text_layout.rs b/gpui/src/text_layout.rs index 0a9717b388d2f3c52e7035a270364a4c0842f2a1..eda4e775e1737f98976858c5c6ac2f08330e8ea9 100644 --- a/gpui/src/text_layout.rs +++ b/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>>, curr_frame: RwLock>>, + fonts: Arc, } impl TextLayoutCache { - pub fn new() -> Self { + pub fn new(fonts: Arc) -> 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, FontId)], - font_cache: &'a FontCache, ) -> Arc { 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, 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, 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::(); - let utf16_end = utf16_start - + utf16_lens - .by_ref() - .take(range.end - range.start) - .sum::(); - 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::() - .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![ - 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(()) - } -} diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index bdd673693eeee617846b7f9812a67b52d3d9533c..899cda7ce3d8be3a963971f38a15b646aadba905 100644 --- a/zed/src/editor/buffer_view.rs +++ b/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)); diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index b0640a4efef2e750b9fd7024b5a34b0c6edd5932..d52c2c61ebe4dbcbb996be9efacb73b305da311b 100644 --- a/zed/src/file_finder.rs +++ b/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)); diff --git a/zed/src/main.rs b/zed/src/main.rs index a315f07bcbc3933bf6ed1b8059f2dbf9263882cb..ed52fb6163758aee7ec13a99f5e85d5c6e8f82d1 100644 --- a/zed/src/main.rs +++ b/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(); diff --git a/zed/src/workspace/mod.rs b/zed/src/workspace/mod.rs index 9dd038abb3765e294d6d81918ea2390ddd21e713..95bb71289e032eabd820d5276a9401fda197943d 100644 --- a/zed/src/workspace/mod.rs +++ b/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); diff --git a/zed/src/workspace/workspace_view.rs b/zed/src/workspace/workspace_view.rs index 12c73c14f7effec3ef6367f3e8c15523e9b63c48..a5b586091d5c10a749636e676b85b8ca40a03abf 100644 --- a/zed/src/workspace/workspace_view.rs +++ b/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);