diff --git a/Cargo.lock b/Cargo.lock index 325e456b57..54405379ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4866,6 +4866,7 @@ dependencies = [ "cocoa", "collections", "core-foundation", + "core-foundation-sys 0.8.6", "core-graphics", "core-text", "cosmic-text", diff --git a/assets/settings/default.json b/assets/settings/default.json index 517f43baf7..529b91b7cd 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -26,6 +26,9 @@ }, // The name of a font to use for rendering text in the editor "buffer_font_family": "Zed Plex Mono", + // Set the buffer text's font fallbacks, this will be merged with + // the platform's default fallbacks. + "buffer_font_fallbacks": [], // The OpenType features to enable for text in the editor. "buffer_font_features": { // Disable ligatures: @@ -47,8 +50,11 @@ // }, "buffer_line_height": "comfortable", // The name of a font to use for rendering text in the UI - // (On macOS) You can set this to ".SystemUIFont" to use the system font + // You can set this to ".SystemUIFont" to use the system font "ui_font_family": "Zed Plex Sans", + // Set the UI's font fallbacks, this will be merged with the platform's + // default font fallbacks. + "ui_font_fallbacks": [], // The OpenType features to enable for text in the UI "ui_font_features": { // Disable ligatures: @@ -675,6 +681,10 @@ // Set the terminal's font family. If this option is not included, // the terminal will default to matching the buffer's font family. // "font_family": "Zed Plex Mono", + // Set the terminal's font fallbacks. If this option is not included, + // the terminal will default to matching the buffer's font fallbacks. + // This will be merged with the platform's default font fallbacks + // "font_fallbacks": ["FiraCode Nerd Fonts"], // Sets the maximum number of lines in the terminal's scrollback buffer. // Default: 10_000, maximum: 100_000 (all bigger values set will be treated as 100_000), 0 disables the scrolling. // Existing terminals will not pick up this change until they are recreated. diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 1b53686e06..445eec8fe5 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -1123,16 +1123,17 @@ impl Context { .timer(Duration::from_millis(200)) .await; - let token_count = cx - .update(|cx| { - LanguageModelCompletionProvider::read_global(cx).count_tokens(request, cx) - })? - .await?; + if let Some(token_count) = cx.update(|cx| { + LanguageModelCompletionProvider::read_global(cx).count_tokens(request, cx) + })? { + let token_count = token_count.await?; + + this.update(&mut cx, |this, cx| { + this.token_count = Some(token_count); + cx.notify() + })?; + } - this.update(&mut cx, |this, cx| { - this.token_count = Some(token_count); - cx.notify() - })?; anyhow::Ok(()) } .log_err() diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index 307d2efe2c..88a8382a97 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -1635,15 +1635,18 @@ impl PromptEditor { })? .await?; - let token_count = cx - .update(|cx| { - LanguageModelCompletionProvider::read_global(cx).count_tokens(request, cx) - })? - .await?; - this.update(&mut cx, |this, cx| { - this.token_count = Some(token_count); - cx.notify(); - }) + if let Some(token_count) = cx.update(|cx| { + LanguageModelCompletionProvider::read_global(cx).count_tokens(request, cx) + })? { + let token_count = token_count.await?; + + this.update(&mut cx, |this, cx| { + this.token_count = Some(token_count); + cx.notify(); + }) + } else { + Ok(()) + } }) } @@ -1832,6 +1835,7 @@ impl PromptEditor { }, font_family: settings.ui_font.family.clone(), font_features: settings.ui_font.features.clone(), + font_fallbacks: settings.ui_font.fallbacks.clone(), font_size: rems(0.875).into(), font_weight: settings.ui_font.weight, line_height: relative(1.3), diff --git a/crates/assistant/src/prompt_library.rs b/crates/assistant/src/prompt_library.rs index c85aef9314..0fbac05ef5 100644 --- a/crates/assistant/src/prompt_library.rs +++ b/crates/assistant/src/prompt_library.rs @@ -734,26 +734,29 @@ impl PromptLibrary { const DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1); cx.background_executor().timer(DEBOUNCE_TIMEOUT).await; - let token_count = cx - .update(|cx| { - LanguageModelCompletionProvider::read_global(cx).count_tokens( - LanguageModelRequest { - messages: vec![LanguageModelRequestMessage { - role: Role::System, - content: body.to_string(), - }], - stop: Vec::new(), - temperature: 1., - }, - cx, - ) - })? - .await?; - this.update(&mut cx, |this, cx| { - let prompt_editor = this.prompt_editors.get_mut(&prompt_id).unwrap(); - prompt_editor.token_count = Some(token_count); - cx.notify(); - }) + if let Some(token_count) = cx.update(|cx| { + LanguageModelCompletionProvider::read_global(cx).count_tokens( + LanguageModelRequest { + messages: vec![LanguageModelRequestMessage { + role: Role::System, + content: body.to_string(), + }], + stop: Vec::new(), + temperature: 1., + }, + cx, + ) + })? { + let token_count = token_count.await?; + + this.update(&mut cx, |this, cx| { + let prompt_editor = this.prompt_editors.get_mut(&prompt_id).unwrap(); + prompt_editor.token_count = Some(token_count); + cx.notify(); + }) + } else { + Ok(()) + } } .log_err() }); diff --git a/crates/assistant/src/terminal_inline_assistant.rs b/crates/assistant/src/terminal_inline_assistant.rs index cbd33f38a5..d154e2f09c 100644 --- a/crates/assistant/src/terminal_inline_assistant.rs +++ b/crates/assistant/src/terminal_inline_assistant.rs @@ -707,15 +707,18 @@ impl PromptEditor { inline_assistant.request_for_inline_assist(assist_id, cx) })??; - let token_count = cx - .update(|cx| { - LanguageModelCompletionProvider::read_global(cx).count_tokens(request, cx) - })? - .await?; - this.update(&mut cx, |this, cx| { - this.token_count = Some(token_count); - cx.notify(); - }) + if let Some(token_count) = cx.update(|cx| { + LanguageModelCompletionProvider::read_global(cx).count_tokens(request, cx) + })? { + let token_count = token_count.await?; + + this.update(&mut cx, |this, cx| { + this.token_count = Some(token_count); + cx.notify(); + }) + } else { + Ok(()) + } }) } @@ -906,6 +909,7 @@ impl PromptEditor { }, font_family: settings.ui_font.family.clone(), font_features: settings.ui_font.features.clone(), + font_fallbacks: settings.ui_font.fallbacks.clone(), font_size: rems(0.875).into(), font_weight: settings.ui_font.weight, line_height: relative(1.3), diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index fb558e2dda..35502a3b14 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/crates/collab_ui/src/chat_panel/message_editor.rs @@ -533,6 +533,7 @@ impl Render for MessageEditor { }, font_family: settings.ui_font.family.clone(), font_features: settings.ui_font.features.clone(), + font_fallbacks: settings.ui_font.fallbacks.clone(), font_size: TextSize::Small.rems(cx).into(), font_weight: settings.ui_font.weight, font_style: FontStyle::Normal, diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 9a91e403bf..d5cf56654d 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2190,6 +2190,7 @@ impl CollabPanel { }, font_family: settings.ui_font.family.clone(), font_features: settings.ui_font.features.clone(), + font_fallbacks: settings.ui_font.fallbacks.clone(), font_size: rems(0.875).into(), font_weight: settings.ui_font.weight, font_style: FontStyle::Normal, diff --git a/crates/completion/src/completion.rs b/crates/completion/src/completion.rs index c817b79ff3..e2fe9b27c6 100644 --- a/crates/completion/src/completion.rs +++ b/crates/completion/src/completion.rs @@ -1,5 +1,5 @@ use anyhow::{anyhow, Result}; -use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt}; +use futures::{future::BoxFuture, stream::BoxStream, StreamExt}; use gpui::{AppContext, Global, Model, ModelContext, Task}; use language_model::{ LanguageModel, LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, @@ -143,11 +143,11 @@ impl LanguageModelCompletionProvider { &self, request: LanguageModelRequest, cx: &AppContext, - ) -> BoxFuture<'static, Result> { + ) -> Option>> { if let Some(model) = self.active_model() { - model.count_tokens(request, cx) + Some(model.count_tokens(request, cx)) } else { - std::future::ready(Err(anyhow!("No active model set"))).boxed() + None } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8e5c75dcb4..db2eec8a1e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -12430,6 +12430,7 @@ impl Render for Editor { color: cx.theme().colors().editor_foreground, font_family: settings.ui_font.family.clone(), font_features: settings.ui_font.features.clone(), + font_fallbacks: settings.ui_font.fallbacks.clone(), font_size: rems(0.875).into(), font_weight: settings.ui_font.weight, line_height: relative(settings.buffer_line_height.value()), @@ -12439,6 +12440,7 @@ impl Render for Editor { color: cx.theme().colors().editor_foreground, font_family: settings.buffer_font.family.clone(), font_features: settings.buffer_font.features.clone(), + font_fallbacks: settings.buffer_font.fallbacks.clone(), font_size: settings.buffer_font_size(cx).into(), font_weight: settings.buffer_font.weight, line_height: relative(settings.buffer_line_height.value()), diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index aeee22cabd..fcbd3bd423 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -27,6 +27,7 @@ pub fn marked_display_snapshot( let font = Font { family: "Zed Plex Mono".into(), features: FontFeatures::default(), + fallbacks: None, weight: FontWeight::default(), style: FontStyle::default(), }; diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 53f519736e..19b10bafc9 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -816,6 +816,7 @@ impl ExtensionsPage { }, font_family: settings.ui_font.family.clone(), font_features: settings.ui_font.features.clone(), + font_fallbacks: settings.ui_font.fallbacks.clone(), font_size: rems(0.875).into(), font_weight: settings.ui_font.weight, line_height: relative(1.3), diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 080bc8b3c4..cc8ec90d67 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -93,6 +93,7 @@ cbindgen = { version = "0.26.0", default-features = false } block = "0.1" cocoa.workspace = true core-foundation.workspace = true +core-foundation-sys = "0.8" core-graphics = "0.23" core-text = "20.1" foreign-types = "0.5" diff --git a/crates/gpui/src/platform/mac/open_type.rs b/crates/gpui/src/platform/mac/open_type.rs index b7c49cdfcd..87e2543521 100644 --- a/crates/gpui/src/platform/mac/open_type.rs +++ b/crates/gpui/src/platform/mac/open_type.rs @@ -1,10 +1,12 @@ #![allow(unused, non_upper_case_globals)] -use crate::FontFeatures; +use crate::{FontFallbacks, FontFeatures}; use cocoa::appkit::CGFloat; use core_foundation::{ array::{ - kCFTypeArrayCallBacks, CFArray, CFArrayAppendValue, CFArrayCreateMutable, CFMutableArrayRef, + kCFTypeArrayCallBacks, CFArray, CFArrayAppendArray, CFArrayAppendValue, + CFArrayCreateMutable, CFArrayGetCount, CFArrayGetValueAtIndex, CFArrayRef, + CFMutableArrayRef, }, base::{kCFAllocatorDefault, CFRelease, TCFType}, dictionary::{ @@ -13,21 +15,88 @@ use core_foundation::{ number::CFNumber, string::{CFString, CFStringRef}, }; +use core_foundation_sys::locale::CFLocaleCopyPreferredLanguages; use core_graphics::{display::CFDictionary, geometry::CGAffineTransform}; use core_text::{ - font::{CTFont, CTFontRef}, + font::{cascade_list_for_languages, CTFont, CTFontRef}, font_descriptor::{ - kCTFontFeatureSettingsAttribute, CTFontDescriptor, CTFontDescriptorCopyAttributes, - CTFontDescriptorCreateCopyWithFeature, CTFontDescriptorCreateWithAttributes, + kCTFontCascadeListAttribute, kCTFontFeatureSettingsAttribute, CTFontDescriptor, + CTFontDescriptorCopyAttributes, CTFontDescriptorCreateCopyWithFeature, + CTFontDescriptorCreateWithAttributes, CTFontDescriptorCreateWithNameAndSize, CTFontDescriptorRef, }, }; -use font_kit::font::Font; +use font_kit::font::Font as FontKitFont; use std::ptr; -pub fn apply_features(font: &mut Font, features: &FontFeatures) { +pub fn apply_features_and_fallbacks( + font: &mut FontKitFont, + features: &FontFeatures, + fallbacks: Option<&FontFallbacks>, +) -> anyhow::Result<()> { + unsafe { + let fallback_array = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks); + + if let Some(fallbacks) = fallbacks { + for user_fallback in fallbacks.fallback_list() { + let name = CFString::from(user_fallback.as_str()); + let fallback_desc = + CTFontDescriptorCreateWithNameAndSize(name.as_concrete_TypeRef(), 0.0); + CFArrayAppendValue(fallback_array, fallback_desc as _); + CFRelease(fallback_desc as _); + } + } + + { + let preferred_languages: CFArray = + CFArray::wrap_under_create_rule(CFLocaleCopyPreferredLanguages()); + + let default_fallbacks = CTFontCopyDefaultCascadeListForLanguages( + font.native_font().as_concrete_TypeRef(), + preferred_languages.as_concrete_TypeRef(), + ); + let default_fallbacks: CFArray = + CFArray::wrap_under_create_rule(default_fallbacks); + + default_fallbacks + .iter() + .filter(|desc| desc.font_path().is_some()) + .map(|desc| { + CFArrayAppendValue(fallback_array, desc.as_concrete_TypeRef() as _); + }); + } + + let feature_array = generate_feature_array(features); + let keys = [kCTFontFeatureSettingsAttribute, kCTFontCascadeListAttribute]; + let values = [feature_array, fallback_array]; + let attrs = CFDictionaryCreate( + kCFAllocatorDefault, + keys.as_ptr() as _, + values.as_ptr() as _, + 2, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks, + ); + CFRelease(feature_array as *const _ as _); + CFRelease(fallback_array as *const _ as _); + let new_descriptor = CTFontDescriptorCreateWithAttributes(attrs); + CFRelease(attrs as _); + let new_descriptor = CTFontDescriptor::wrap_under_create_rule(new_descriptor); + let new_font = CTFontCreateCopyWithAttributes( + font.native_font().as_concrete_TypeRef(), + 0.0, + std::ptr::null(), + new_descriptor.as_concrete_TypeRef(), + ); + let new_font = CTFont::wrap_under_create_rule(new_font); + *font = font_kit::font::Font::from_native_font(&new_font); + + Ok(()) + } +} + +fn generate_feature_array(features: &FontFeatures) -> CFMutableArrayRef { unsafe { - let native_font = font.native_font(); let mut feature_array = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks); for (tag, value) in features.tag_value_list() { @@ -48,26 +117,7 @@ pub fn apply_features(font: &mut Font, features: &FontFeatures) { CFArrayAppendValue(feature_array, dict as _); CFRelease(dict as _); } - let attrs = CFDictionaryCreate( - kCFAllocatorDefault, - &kCTFontFeatureSettingsAttribute as *const _ as _, - &feature_array as *const _ as _, - 1, - &kCFTypeDictionaryKeyCallBacks, - &kCFTypeDictionaryValueCallBacks, - ); - CFRelease(feature_array as *const _ as _); - let new_descriptor = CTFontDescriptorCreateWithAttributes(attrs); - CFRelease(attrs as _); - let new_descriptor = CTFontDescriptor::wrap_under_create_rule(new_descriptor); - let new_font = CTFontCreateCopyWithAttributes( - font.native_font().as_concrete_TypeRef(), - 0.0, - ptr::null(), - new_descriptor.as_concrete_TypeRef(), - ); - let new_font = CTFont::wrap_under_create_rule(new_font); - *font = Font::from_native_font(&new_font); + feature_array } } @@ -82,4 +132,8 @@ extern "C" { matrix: *const CGAffineTransform, attributes: CTFontDescriptorRef, ) -> CTFontRef; + fn CTFontCopyDefaultCascadeListForLanguages( + font: CTFontRef, + languagePrefList: CFArrayRef, + ) -> CFArrayRef; } diff --git a/crates/gpui/src/platform/mac/text_system.rs b/crates/gpui/src/platform/mac/text_system.rs index 8806d1bd18..2967e2f56e 100644 --- a/crates/gpui/src/platform/mac/text_system.rs +++ b/crates/gpui/src/platform/mac/text_system.rs @@ -1,6 +1,6 @@ use crate::{ - point, px, size, Bounds, DevicePixels, Font, FontFeatures, FontId, FontMetrics, FontRun, - FontStyle, FontWeight, GlyphId, LineLayout, Pixels, PlatformTextSystem, Point, + point, px, size, Bounds, DevicePixels, Font, FontFallbacks, FontFeatures, FontId, FontMetrics, + FontRun, FontStyle, FontWeight, GlyphId, LineLayout, Pixels, PlatformTextSystem, Point, RenderGlyphParams, Result, ShapedGlyph, ShapedRun, SharedString, Size, SUBPIXEL_VARIANTS, }; use anyhow::anyhow; @@ -43,7 +43,7 @@ use pathfinder_geometry::{ use smallvec::SmallVec; use std::{borrow::Cow, char, cmp, convert::TryFrom, sync::Arc}; -use super::open_type; +use super::open_type::apply_features_and_fallbacks; #[allow(non_upper_case_globals)] const kCGImageAlphaOnly: u32 = 7; @@ -54,6 +54,7 @@ pub(crate) struct MacTextSystem(RwLock); struct FontKey { font_family: SharedString, font_features: FontFeatures, + font_fallbacks: Option, } struct MacTextSystemState { @@ -123,11 +124,13 @@ impl PlatformTextSystem for MacTextSystem { let font_key = FontKey { font_family: font.family.clone(), font_features: font.features.clone(), + font_fallbacks: font.fallbacks.clone(), }; let candidates = if let Some(font_ids) = lock.font_ids_by_font_key.get(&font_key) { font_ids.as_slice() } else { - let font_ids = lock.load_family(&font.family, &font.features)?; + let font_ids = + lock.load_family(&font.family, &font.features, font.fallbacks.as_ref())?; lock.font_ids_by_font_key.insert(font_key.clone(), font_ids); lock.font_ids_by_font_key[&font_key].as_ref() }; @@ -212,6 +215,7 @@ impl MacTextSystemState { &mut self, name: &str, features: &FontFeatures, + fallbacks: Option<&FontFallbacks>, ) -> Result> { let name = if name == ".SystemUIFont" { ".AppleSystemUIFont" @@ -227,8 +231,7 @@ impl MacTextSystemState { for font in family.fonts() { let mut font = font.load()?; - open_type::apply_features(&mut font, features); - + apply_features_and_fallbacks(&mut font, features, fallbacks)?; // This block contains a precautionary fix to guard against loading fonts // that might cause panics due to `.unwrap()`s up the chain. { @@ -457,6 +460,7 @@ impl MacTextSystemState { CFRange::init(utf16_start as isize, (utf16_end - utf16_start) as isize); let font: &FontKitFont = &self.fonts[run.font_id.0]; + unsafe { string.set_attribute( cf_range, @@ -634,7 +638,7 @@ impl From for FontkitStyle { } } -// Some fonts may have no attributest despite `core_text` requiring them (and panicking). +// Some fonts may have no attributes despite `core_text` requiring them (and panicking). // This is the same version as `core_text` has without `expect` calls. mod lenient_font_attributes { use core_foundation::{ diff --git a/crates/gpui/src/platform/windows/direct_write.rs b/crates/gpui/src/platform/windows/direct_write.rs index 1e5e3f2e43..aad8e09322 100644 --- a/crates/gpui/src/platform/windows/direct_write.rs +++ b/crates/gpui/src/platform/windows/direct_write.rs @@ -30,6 +30,7 @@ struct FontInfo { font_family: String, font_face: IDWriteFontFace3, features: IDWriteTypography, + fallbacks: Option, is_system_font: bool, is_emoji: bool, } @@ -287,6 +288,63 @@ impl DirectWriteState { Ok(()) } + fn generate_font_fallbacks( + &self, + fallbacks: Option<&FontFallbacks>, + ) -> Result> { + if fallbacks.is_some_and(|fallbacks| fallbacks.fallback_list().is_empty()) { + return Ok(None); + } + unsafe { + let builder = self.components.factory.CreateFontFallbackBuilder()?; + let font_set = &self.system_font_collection.GetFontSet()?; + if let Some(fallbacks) = fallbacks { + for family_name in fallbacks.fallback_list() { + let Some(fonts) = font_set + .GetMatchingFonts( + &HSTRING::from(family_name), + DWRITE_FONT_WEIGHT_NORMAL, + DWRITE_FONT_STRETCH_NORMAL, + DWRITE_FONT_STYLE_NORMAL, + ) + .log_err() + else { + continue; + }; + if fonts.GetFontCount() == 0 { + log::error!("No mathcing font find for {}", family_name); + continue; + } + let font = fonts.GetFontFaceReference(0)?.CreateFontFace()?; + let mut count = 0; + font.GetUnicodeRanges(None, &mut count).ok(); + if count == 0 { + continue; + } + let mut unicode_ranges = vec![DWRITE_UNICODE_RANGE::default(); count as usize]; + let Some(_) = font + .GetUnicodeRanges(Some(&mut unicode_ranges), &mut count) + .log_err() + else { + continue; + }; + let target_family_name = HSTRING::from(family_name); + builder.AddMapping( + &unicode_ranges, + &[target_family_name.as_ptr()], + None, + None, + None, + 1.0, + )?; + } + } + let system_fallbacks = self.components.factory.GetSystemFontFallback()?; + builder.AddMappings(&system_fallbacks)?; + Ok(Some(builder.CreateFontFallback()?)) + } + } + unsafe fn generate_font_features( &self, font_features: &FontFeatures, @@ -302,6 +360,7 @@ impl DirectWriteState { font_weight: FontWeight, font_style: FontStyle, font_features: &FontFeatures, + font_fallbacks: Option<&FontFallbacks>, is_system_font: bool, ) -> Option { let collection = if is_system_font { @@ -334,11 +393,16 @@ impl DirectWriteState { else { continue; }; + let fallbacks = self + .generate_font_fallbacks(font_fallbacks) + .log_err() + .unwrap_or_default(); let font_info = FontInfo { font_family: family_name.to_owned(), font_face, - is_system_font, features: direct_write_features, + fallbacks, + is_system_font, is_emoji, }; let font_id = FontId(self.fonts.len()); @@ -371,6 +435,7 @@ impl DirectWriteState { target_font.weight, target_font.style, &target_font.features, + target_font.fallbacks.as_ref(), ) .unwrap() } else { @@ -379,6 +444,7 @@ impl DirectWriteState { target_font.weight, target_font.style, &target_font.features, + target_font.fallbacks.as_ref(), ) .unwrap_or_else(|| { let family = self.system_ui_font_name.clone(); @@ -388,6 +454,7 @@ impl DirectWriteState { target_font.weight, target_font.style, &target_font.features, + target_font.fallbacks.as_ref(), true, ) .unwrap() @@ -402,16 +469,38 @@ impl DirectWriteState { weight: FontWeight, style: FontStyle, features: &FontFeatures, + fallbacks: Option<&FontFallbacks>, ) -> Option { // try to find target font in custom font collection first - self.get_font_id_from_font_collection(family_name, weight, style, features, false) - .or_else(|| { - self.get_font_id_from_font_collection(family_name, weight, style, features, true) - }) - .or_else(|| { - self.update_system_font_collection(); - self.get_font_id_from_font_collection(family_name, weight, style, features, true) - }) + self.get_font_id_from_font_collection( + family_name, + weight, + style, + features, + fallbacks, + false, + ) + .or_else(|| { + self.get_font_id_from_font_collection( + family_name, + weight, + style, + features, + fallbacks, + true, + ) + }) + .or_else(|| { + self.update_system_font_collection(); + self.get_font_id_from_font_collection( + family_name, + weight, + style, + features, + fallbacks, + true, + ) + }) } fn layout_line( @@ -440,15 +529,22 @@ impl DirectWriteState { } else { &self.custom_font_collection }; - let format = self.components.factory.CreateTextFormat( - &HSTRING::from(&font_info.font_family), - collection, - font_info.font_face.GetWeight(), - font_info.font_face.GetStyle(), - DWRITE_FONT_STRETCH_NORMAL, - font_size.0, - &HSTRING::from(&self.components.locale), - )?; + let format: IDWriteTextFormat1 = self + .components + .factory + .CreateTextFormat( + &HSTRING::from(&font_info.font_family), + collection, + font_info.font_face.GetWeight(), + font_info.font_face.GetStyle(), + DWRITE_FONT_STRETCH_NORMAL, + font_size.0, + &HSTRING::from(&self.components.locale), + )? + .cast()?; + if let Some(ref fallbacks) = font_info.fallbacks { + format.SetFontFallback(fallbacks)?; + } let layout = self.components.factory.CreateTextLayout( &text_wide, @@ -1183,6 +1279,7 @@ fn get_font_identifier_and_font_struct( features: FontFeatures::default(), weight: weight.into(), style: style.into(), + fallbacks: None, }; let is_emoji = unsafe { font_face.IsColorFont().as_bool() }; Some((identifier, font_struct, is_emoji)) diff --git a/crates/gpui/src/shared_string.rs b/crates/gpui/src/shared_string.rs index efdf32388d..a4ed36ec21 100644 --- a/crates/gpui/src/shared_string.rs +++ b/crates/gpui/src/shared_string.rs @@ -1,4 +1,5 @@ use derive_more::{Deref, DerefMut}; + use serde::{Deserialize, Serialize}; use std::{borrow::Borrow, sync::Arc}; use util::arc_cow::ArcCow; diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 8f1ebc8a61..0d23a96206 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -6,9 +6,9 @@ use std::{ use crate::{ black, phi, point, quad, rems, AbsoluteLength, Bounds, ContentMask, Corners, CornersRefinement, - CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font, FontFeatures, FontStyle, FontWeight, - Hsla, Length, Pixels, Point, PointRefinement, Rgba, SharedString, Size, SizeRefinement, Styled, - TextRun, WindowContext, + CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font, FontFallbacks, FontFeatures, + FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba, SharedString, Size, + SizeRefinement, Styled, TextRun, WindowContext, }; use collections::HashSet; use refineable::Refineable; @@ -180,6 +180,9 @@ pub struct TextStyle { /// The font features to use pub font_features: FontFeatures, + /// The fallback fonts to use + pub font_fallbacks: Option, + /// The font size to use, in pixels or rems. pub font_size: AbsoluteLength, @@ -218,6 +221,7 @@ impl Default for TextStyle { "Helvetica".into() }, font_features: FontFeatures::default(), + font_fallbacks: None, font_size: rems(1.).into(), line_height: phi(), font_weight: FontWeight::default(), @@ -269,6 +273,7 @@ impl TextStyle { Font { family: self.font_family.clone(), features: self.font_features.clone(), + fallbacks: self.font_fallbacks.clone(), weight: self.font_weight, style: self.font_style, } @@ -286,6 +291,7 @@ impl TextStyle { font: Font { family: self.font_family.clone(), features: Default::default(), + fallbacks: self.font_fallbacks.clone(), weight: self.font_weight, style: self.font_style, }, diff --git a/crates/gpui/src/styled.rs b/crates/gpui/src/styled.rs index ff8c86a1d6..cc54909a8c 100644 --- a/crates/gpui/src/styled.rs +++ b/crates/gpui/src/styled.rs @@ -506,6 +506,7 @@ pub trait Styled: Sized { let Font { family, features, + fallbacks, weight, style, } = font; @@ -515,6 +516,7 @@ pub trait Styled: Sized { text_style.font_features = Some(features); text_style.font_weight = Some(weight); text_style.font_style = Some(style); + text_style.font_fallbacks = fallbacks; self } diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index a6ff8a8d16..67f8ac783b 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -1,8 +1,10 @@ +mod font_fallbacks; mod font_features; mod line; mod line_layout; mod line_wrapper; +pub use font_fallbacks::*; pub use font_features::*; pub use line::*; pub use line_layout::*; @@ -62,8 +64,7 @@ impl TextSystem { wrapper_pool: Mutex::default(), font_runs_pool: Mutex::default(), fallback_font_stack: smallvec![ - // TODO: This is currently Zed-specific. - // We should allow GPUI users to provide their own fallback font stack. + // TODO: Remove this when Linux have implemented setting fallbacks. font("Zed Plex Mono"), font("Helvetica"), font("Segoe UI"), // Windows @@ -683,6 +684,9 @@ pub struct Font { /// The font features to use. pub features: FontFeatures, + /// The fallbacks fonts to use. + pub fallbacks: Option, + /// The font weight. pub weight: FontWeight, @@ -697,6 +701,7 @@ pub fn font(family: impl Into) -> Font { features: FontFeatures::default(), weight: FontWeight::default(), style: FontStyle::default(), + fallbacks: None, } } diff --git a/crates/gpui/src/text_system/font_fallbacks.rs b/crates/gpui/src/text_system/font_fallbacks.rs new file mode 100644 index 0000000000..9433f9c9db --- /dev/null +++ b/crates/gpui/src/text_system/font_fallbacks.rs @@ -0,0 +1,21 @@ +use std::sync::Arc; + +use schemars::JsonSchema; +use serde_derive::{Deserialize, Serialize}; + +/// The fallback fonts that can be configured for a given font. +/// Fallback fonts family names are stored here. +#[derive(Default, Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize, JsonSchema)] +pub struct FontFallbacks(pub Arc>); + +impl FontFallbacks { + /// Get the fallback fonts family names + pub fn fallback_list(&self) -> &[String] { + &self.0.as_slice() + } + + /// Create a font fallback from a list of strings + pub fn from_fonts(fonts: Vec) -> Self { + FontFallbacks(Arc::new(fonts)) + } +} diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index 31e1b9abc6..af17a0efb4 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -162,6 +162,7 @@ pub fn render_item( color: cx.theme().colors().text, font_family: settings.buffer_font.family.clone(), font_features: settings.buffer_font.features.clone(), + font_fallbacks: settings.buffer_font.fallbacks.clone(), font_size: settings.buffer_font_size(cx).into(), font_weight: settings.buffer_font.weight, line_height: relative(1.), diff --git a/crates/language_model/src/provider/anthropic.rs b/crates/language_model/src/provider/anthropic.rs index 7743ec6128..145d0ec8cf 100644 --- a/crates/language_model/src/provider/anthropic.rs +++ b/crates/language_model/src/provider/anthropic.rs @@ -406,6 +406,7 @@ impl AuthenticationPrompt { color: cx.theme().colors().text, font_family: settings.ui_font.family.clone(), font_features: settings.ui_font.features.clone(), + font_fallbacks: settings.ui_font.fallbacks.clone(), font_size: rems(0.875).into(), font_weight: settings.ui_font.weight, font_style: FontStyle::Normal, diff --git a/crates/language_model/src/provider/open_ai.rs b/crates/language_model/src/provider/open_ai.rs index bc31ccafed..c81a435946 100644 --- a/crates/language_model/src/provider/open_ai.rs +++ b/crates/language_model/src/provider/open_ai.rs @@ -341,6 +341,7 @@ impl AuthenticationPrompt { color: cx.theme().colors().text, font_family: settings.ui_font.family.clone(), font_features: settings.ui_font.features.clone(), + font_fallbacks: settings.ui_font.fallbacks.clone(), font_size: rems(0.875).into(), font_weight: settings.ui_font.weight, font_style: FontStyle::Normal, diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 50a50171bf..cff5aa7fc8 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -114,6 +114,7 @@ impl BufferSearchBar { }, font_family: settings.buffer_font.family.clone(), font_features: settings.buffer_font.features.clone(), + font_fallbacks: settings.buffer_font.fallbacks.clone(), font_size: rems(0.875).into(), font_weight: settings.buffer_font.weight, line_height: relative(1.3), diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 5f2aa2233c..96be71b7d9 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1338,6 +1338,7 @@ impl ProjectSearchBar { }, font_family: settings.buffer_font.family.clone(), font_features: settings.buffer_font.features.clone(), + font_fallbacks: settings.buffer_font.fallbacks.clone(), font_size: rems(0.875).into(), font_weight: settings.buffer_font.weight, line_height: relative(1.3), diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index 59adabd7ff..dd0ab6e4a6 100644 --- a/crates/settings/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -18,9 +18,11 @@ pub fn test_settings() -> String { "ui_font_family": "Courier", "ui_font_features": {}, "ui_font_size": 14, + "ui_font_fallback": [], "buffer_font_family": "Courier", "buffer_font_features": {}, "buffer_font_size": 14, + "buffer_font_fallback": [], "theme": EMPTY_THEME_NAME, }), &mut value, diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index 7e31183b7b..4fbe2cdc86 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -1,8 +1,10 @@ use collections::HashMap; -use gpui::{px, AbsoluteLength, AppContext, FontFeatures, FontWeight, Pixels}; +use gpui::{ + px, AbsoluteLength, AppContext, FontFallbacks, FontFeatures, FontWeight, Pixels, SharedString, +}; use schemars::{ gen::SchemaGenerator, - schema::{InstanceType, RootSchema, Schema, SchemaObject}, + schema::{ArrayValidation, InstanceType, RootSchema, Schema, SchemaObject}, JsonSchema, }; use serde_derive::{Deserialize, Serialize}; @@ -24,15 +26,16 @@ pub struct Toolbar { pub title: bool, } -#[derive(Deserialize)] +#[derive(Debug, Deserialize)] pub struct TerminalSettings { pub shell: Shell, pub working_directory: WorkingDirectory, pub font_size: Option, - pub font_family: Option, - pub line_height: TerminalLineHeight, + pub font_family: Option, + pub font_fallbacks: Option, pub font_features: Option, pub font_weight: Option, + pub line_height: TerminalLineHeight, pub env: HashMap, pub blinking: TerminalBlink, pub alternate_scroll: AlternateScroll, @@ -111,6 +114,13 @@ pub struct TerminalSettingsContent { /// If this option is not included, /// the terminal will default to matching the buffer's font family. pub font_family: Option, + + /// Sets the terminal's font fallbacks. + /// + /// If this option is not included, + /// the terminal will default to matching the buffer's font fallbacks. + pub font_fallbacks: Option>, + /// Sets the terminal's line height. /// /// Default: comfortable @@ -192,30 +202,51 @@ impl settings::Settings for TerminalSettings { _: &AppContext, ) -> RootSchema { let mut root_schema = generator.root_schema_for::(); - let available_fonts = params + let available_fonts: Vec<_> = params .font_names .iter() .cloned() .map(Value::String) .collect(); - let fonts_schema = SchemaObject { + + let font_family_schema = SchemaObject { instance_type: Some(InstanceType::String.into()), enum_values: Some(available_fonts), ..Default::default() }; - root_schema - .definitions - .extend([("FontFamilies".into(), fonts_schema.into())]); + + let font_fallback_schema = SchemaObject { + instance_type: Some(InstanceType::Array.into()), + array: Some(Box::new(ArrayValidation { + items: Some(schemars::schema::SingleOrVec::Single(Box::new( + font_family_schema.clone().into(), + ))), + unique_items: Some(true), + ..Default::default() + })), + ..Default::default() + }; + + root_schema.definitions.extend([ + ("FontFamilies".into(), font_family_schema.into()), + ("FontFallbacks".into(), font_fallback_schema.into()), + ]); root_schema .schema .object .as_mut() .unwrap() .properties - .extend([( - "font_family".to_owned(), - Schema::new_ref("#/definitions/FontFamilies".into()), - )]); + .extend([ + ( + "font_family".to_owned(), + Schema::new_ref("#/definitions/FontFamilies".into()), + ), + ( + "font_fallbacks".to_owned(), + Schema::new_ref("#/definitions/FontFallbacks".into()), + ), + ]); root_schema } diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index b48f603203..9d94d9d129 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -614,16 +614,24 @@ impl Element for TerminalElement { let buffer_font_size = settings.buffer_font_size(cx); let terminal_settings = TerminalSettings::get_global(cx); + let font_family = terminal_settings .font_family .as_ref() - .map(|string| string.clone().into()) - .unwrap_or(settings.buffer_font.family); + .unwrap_or(&settings.buffer_font.family) + .clone(); + + let font_fallbacks = terminal_settings + .font_fallbacks + .as_ref() + .or(settings.buffer_font.fallbacks.as_ref()) + .map(|fallbacks| fallbacks.clone()); let font_features = terminal_settings .font_features - .clone() - .unwrap_or(settings.buffer_font.features.clone()); + .as_ref() + .unwrap_or(&settings.buffer_font.features) + .clone(); let font_weight = terminal_settings.font_weight.unwrap_or_default(); @@ -653,6 +661,7 @@ impl Element for TerminalElement { font_family, font_features, font_weight, + font_fallbacks, font_size: font_size.into(), font_style: FontStyle::Normal, line_height: line_height.into(), diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index 7c2a9619c3..e99549477e 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -3,10 +3,11 @@ use crate::{Appearance, SyntaxTheme, Theme, ThemeRegistry, ThemeStyleContent}; use anyhow::Result; use derive_more::{Deref, DerefMut}; use gpui::{ - px, AppContext, Font, FontFeatures, FontStyle, FontWeight, Global, Pixels, Subscription, - ViewContext, WindowContext, + px, AppContext, Font, FontFallbacks, FontFeatures, FontStyle, FontWeight, Global, Pixels, + Subscription, ViewContext, WindowContext, }; use refineable::Refineable; +use schemars::schema::ArrayValidation; use schemars::{ gen::SchemaGenerator, schema::{InstanceType, Schema, SchemaObject}, @@ -244,6 +245,9 @@ pub struct ThemeSettingsContent { /// The name of a font to use for rendering in the UI. #[serde(default)] pub ui_font_family: Option, + /// The font fallbacks to use for rendering in the UI. + #[serde(default)] + pub ui_font_fallbacks: Option>, /// The OpenType features to enable for text in the UI. #[serde(default)] pub ui_font_features: Option, @@ -253,6 +257,9 @@ pub struct ThemeSettingsContent { /// The name of a font to use for rendering in text buffers. #[serde(default)] pub buffer_font_family: Option, + /// The font fallbacks to use for rendering in text buffers. + #[serde(default)] + pub buffer_font_fallbacks: Option>, /// The default font size for rendering in text buffers. #[serde(default)] pub buffer_font_size: Option, @@ -510,14 +517,22 @@ impl settings::Settings for ThemeSettings { let mut this = Self { ui_font_size: defaults.ui_font_size.unwrap().into(), ui_font: Font { - family: defaults.ui_font_family.clone().unwrap().into(), + family: defaults.ui_font_family.as_ref().unwrap().clone().into(), features: defaults.ui_font_features.clone().unwrap(), + fallbacks: defaults + .ui_font_fallbacks + .as_ref() + .map(|fallbacks| FontFallbacks::from_fonts(fallbacks.clone())), weight: defaults.ui_font_weight.map(FontWeight).unwrap(), style: Default::default(), }, buffer_font: Font { - family: defaults.buffer_font_family.clone().unwrap().into(), + family: defaults.buffer_font_family.as_ref().unwrap().clone().into(), features: defaults.buffer_font_features.clone().unwrap(), + fallbacks: defaults + .buffer_font_fallbacks + .as_ref() + .map(|fallbacks| FontFallbacks::from_fonts(fallbacks.clone())), weight: defaults.buffer_font_weight.map(FontWeight).unwrap(), style: FontStyle::default(), }, @@ -543,7 +558,9 @@ impl settings::Settings for ThemeSettings { if let Some(value) = value.buffer_font_features.clone() { this.buffer_font.features = value; } - + if let Some(value) = value.buffer_font_fallbacks.clone() { + this.buffer_font.fallbacks = Some(FontFallbacks::from_fonts(value)); + } if let Some(value) = value.buffer_font_weight { this.buffer_font.weight = FontWeight(value); } @@ -554,6 +571,9 @@ impl settings::Settings for ThemeSettings { if let Some(value) = value.ui_font_features.clone() { this.ui_font.features = value; } + if let Some(value) = value.ui_font_fallbacks.clone() { + this.ui_font.fallbacks = Some(FontFallbacks::from_fonts(value)); + } if let Some(value) = value.ui_font_weight { this.ui_font.weight = FontWeight(value); } @@ -605,15 +625,28 @@ impl settings::Settings for ThemeSettings { .iter() .cloned() .map(Value::String) - .collect(); - let fonts_schema = SchemaObject { + .collect::>(); + let font_family_schema = SchemaObject { instance_type: Some(InstanceType::String.into()), enum_values: Some(available_fonts), ..Default::default() }; + let font_fallback_schema = SchemaObject { + instance_type: Some(InstanceType::Array.into()), + array: Some(Box::new(ArrayValidation { + items: Some(schemars::schema::SingleOrVec::Single(Box::new( + font_family_schema.clone().into(), + ))), + unique_items: Some(true), + ..Default::default() + })), + ..Default::default() + }; + root_schema.definitions.extend([ ("ThemeName".into(), theme_name_schema.into()), - ("FontFamilies".into(), fonts_schema.into()), + ("FontFamilies".into(), font_family_schema.into()), + ("FontFallbacks".into(), font_fallback_schema.into()), ]); root_schema @@ -627,10 +660,18 @@ impl settings::Settings for ThemeSettings { "buffer_font_family".to_owned(), Schema::new_ref("#/definitions/FontFamilies".into()), ), + ( + "buffer_font_fallbacks".to_owned(), + Schema::new_ref("#/definitions/FontFallbacks".into()), + ), ( "ui_font_family".to_owned(), Schema::new_ref("#/definitions/FontFamilies".into()), ), + ( + "ui_font_fallbacks".to_owned(), + Schema::new_ref("#/definitions/FontFallbacks".into()), + ), ]); root_schema