Feature/fallback fonts (#15306)

Mikayla Maki and Junkui Zhang created

Supersedes https://github.com/zed-industries/zed/pull/12090

fixes #5180
fixes #5055

See original PR for an example of the feature at work.

This PR changes the settings interface to be backwards compatible, and
adds the `ui_font_fallbacks`, `buffer_font_fallbacks`, and
`terminal.font_fallbacks` settings.

Release Notes:

- Added support for font fallbacks via three new settings:
`ui_font_fallbacks`, `buffer_font_fallbacks`, and
`terminal.font_fallbacks`.(#5180, #5055).

---------

Co-authored-by: Junkui Zhang <364772080@qq.com>

Change summary

Cargo.lock                                        |   1 
assets/settings/default.json                      |  12 +
crates/assistant/src/context.rs                   |  19 +-
crates/assistant/src/inline_assistant.rs          |  22 +-
crates/assistant/src/prompt_library.rs            |  43 ++--
crates/assistant/src/terminal_inline_assistant.rs |  22 +-
crates/collab_ui/src/chat_panel/message_editor.rs |   1 
crates/collab_ui/src/collab_panel.rs              |   1 
crates/completion/src/completion.rs               |   8 
crates/editor/src/editor.rs                       |   2 
crates/editor/src/test.rs                         |   1 
crates/extensions_ui/src/extensions_ui.rs         |   1 
crates/gpui/Cargo.toml                            |   1 
crates/gpui/src/platform/mac/open_type.rs         | 110 ++++++++++---
crates/gpui/src/platform/mac/text_system.rs       |  18 +
crates/gpui/src/platform/windows/direct_write.rs  | 133 ++++++++++++++--
crates/gpui/src/shared_string.rs                  |   1 
crates/gpui/src/style.rs                          |  12 +
crates/gpui/src/styled.rs                         |   2 
crates/gpui/src/text_system.rs                    |   9 
crates/gpui/src/text_system/font_fallbacks.rs     |  21 ++
crates/language/src/outline.rs                    |   1 
crates/language_model/src/provider/anthropic.rs   |   1 
crates/language_model/src/provider/open_ai.rs     |   1 
crates/search/src/buffer_search.rs                |   1 
crates/search/src/project_search.rs               |   1 
crates/settings/src/settings_file.rs              |   2 
crates/terminal/src/terminal_settings.rs          |  59 +++++-
crates/terminal_view/src/terminal_element.rs      |  17 +
crates/theme/src/settings.rs                      |  57 ++++++-
30 files changed, 444 insertions(+), 136 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4866,6 +4866,7 @@ dependencies = [
  "cocoa",
  "collections",
  "core-foundation",
+ "core-foundation-sys 0.8.6",
  "core-graphics",
  "core-text",
  "cosmic-text",

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.

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()

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),

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()
             });

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),

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,

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,

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<usize>> {
+    ) -> Option<BoxFuture<'static, Result<usize>>> {
         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
         }
     }
 

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()),

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(),
     };

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),

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"

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<CFString> =
+                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<CTFontDescriptor> =
+                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;
 }

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<MacTextSystemState>);
 struct FontKey {
     font_family: SharedString,
     font_features: FontFeatures,
+    font_fallbacks: Option<FontFallbacks>,
 }
 
 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<SmallVec<[FontId; 4]>> {
         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<FontStyle> 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::{

crates/gpui/src/platform/windows/direct_write.rs 🔗

@@ -30,6 +30,7 @@ struct FontInfo {
     font_family: String,
     font_face: IDWriteFontFace3,
     features: IDWriteTypography,
+    fallbacks: Option<IDWriteFontFallback>,
     is_system_font: bool,
     is_emoji: bool,
 }
@@ -287,6 +288,63 @@ impl DirectWriteState {
         Ok(())
     }
 
+    fn generate_font_fallbacks(
+        &self,
+        fallbacks: Option<&FontFallbacks>,
+    ) -> Result<Option<IDWriteFontFallback>> {
+        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<FontId> {
         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<FontId> {
         // 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))

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;

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<FontFallbacks>,
+
     /// 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,
             },

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
     }

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<FontFallbacks>,
+
     /// The font weight.
     pub weight: FontWeight,
 
@@ -697,6 +701,7 @@ pub fn font(family: impl Into<SharedString>) -> Font {
         features: FontFeatures::default(),
         weight: FontWeight::default(),
         style: FontStyle::default(),
+        fallbacks: None,
     }
 }
 

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<Vec<String>>);
+
+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<String>) -> Self {
+        FontFallbacks(Arc::new(fonts))
+    }
+}

crates/language/src/outline.rs 🔗

@@ -162,6 +162,7 @@ pub fn render_item<T>(
         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.),

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,

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,

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),

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),

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,

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<Pixels>,
-    pub font_family: Option<String>,
-    pub line_height: TerminalLineHeight,
+    pub font_family: Option<SharedString>,
+    pub font_fallbacks: Option<FontFallbacks>,
     pub font_features: Option<FontFeatures>,
     pub font_weight: Option<FontWeight>,
+    pub line_height: TerminalLineHeight,
     pub env: HashMap<String, String>,
     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<String>,
+
+    /// 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<Vec<String>>,
+
     /// 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::<Self::FileContent>();
-        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
     }

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(),

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<String>,
+    /// The font fallbacks to use for rendering in the UI.
+    #[serde(default)]
+    pub ui_font_fallbacks: Option<Vec<String>>,
     /// The OpenType features to enable for text in the UI.
     #[serde(default)]
     pub ui_font_features: Option<FontFeatures>,
@@ -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<String>,
+    /// The font fallbacks to use for rendering in text buffers.
+    #[serde(default)]
+    pub buffer_font_fallbacks: Option<Vec<String>>,
     /// The default font size for rendering in text buffers.
     #[serde(default)]
     pub buffer_font_size: Option<f32>,
@@ -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::<Vec<_>>();
+        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