Detailed changes
@@ -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,
)));
@@ -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()),
@@ -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)]
@@ -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()
}
}
@@ -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(())
+ }
+}
@@ -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;
@@ -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,
@@ -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());
@@ -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,
@@ -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;
+}
@@ -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,
}
}
@@ -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(())
- }
-}
@@ -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));
@@ -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));
@@ -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();
@@ -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);
@@ -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);