904 lines · 32.8 KB · Rust
RawDownload
use anyhow::anyhow;use cocoa::appkit::CGFloat;use collections::{HashMap, HashSet};use core_foundation::{    array::{CFArray, CFArrayRef},    attributed_string::CFMutableAttributedString,    base::{CFRange, CFType, TCFType},    number::CFNumber,    string::CFString,};use core_graphics::{    base::{CGGlyph, kCGImageAlphaPremultipliedLast},    color_space::CGColorSpace,    context::{CGContext, CGTextDrawingMode},    display::CGPoint,};use core_text::{    font::CTFont,    font_collection::CTFontCollectionRef,    font_descriptor::{        CTFontDescriptor, kCTFontSlantTrait, kCTFontSymbolicTrait, kCTFontWeightTrait,        kCTFontWidthTrait,    },    line::CTLine,    string_attributes::kCTFontAttributeName,};use font_kit::{    font::Font as FontKitFont,    handle::Handle,    hinting::HintingOptions,    metrics::Metrics,    properties::{Style as FontkitStyle, Weight as FontkitWeight},    source::SystemSource,    sources::mem::MemSource,};use gpui::{    Bounds, DevicePixels, Font, FontFallbacks, FontFeatures, FontId, FontMetrics, FontRun,    FontStyle, FontWeight, GlyphId, Hsla, LineLayout, Pixels, PlatformTextSystem,    RenderGlyphParams, Result, Rgba, SUBPIXEL_VARIANTS_X, ShapedGlyph, ShapedRun, SharedString,    Size, TextRenderingMode, point, px, size, swap_rgba_pa_to_bgra,};use parking_lot::{RwLock, RwLockUpgradableReadGuard};use pathfinder_geometry::{    rect::{RectF, RectI},    transform2d::Transform2F,    vector::Vector2F,};use smallvec::SmallVec;use std::{borrow::Cow, char, convert::TryFrom, sync::Arc, sync::OnceLock}; use crate::open_type::apply_features_and_fallbacks; #[allow(non_upper_case_globals)]const kCGImageAlphaOnly: u32 = 7; /// macOS text system using CoreText for font shaping.pub struct MacTextSystem(RwLock<MacTextSystemState>); #[derive(Clone, PartialEq, Eq, Hash)]struct FontKey {    font_family: SharedString,    font_features: FontFeatures,    font_fallbacks: Option<FontFallbacks>,} struct MacTextSystemState {    memory_source: MemSource,    system_source: SystemSource,    fonts: Vec<FontKitFont>,    font_selections: HashMap<Font, FontId>,    font_ids_by_postscript_name: HashMap<String, FontId>,    font_ids_by_font_key: HashMap<FontKey, SmallVec<[FontId; 4]>>,    postscript_names_by_font_id: HashMap<FontId, String>,} impl MacTextSystem {    /// Create a new MacTextSystem.    pub fn new() -> Self {        Self(RwLock::new(MacTextSystemState {            memory_source: MemSource::empty(),            system_source: SystemSource::new(),            fonts: Vec::new(),            font_selections: HashMap::default(),            font_ids_by_postscript_name: HashMap::default(),            font_ids_by_font_key: HashMap::default(),            postscript_names_by_font_id: HashMap::default(),        }))    }} impl Default for MacTextSystem {    fn default() -> Self {        Self::new()    }} impl PlatformTextSystem for MacTextSystem {    fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {        self.0.write().add_fonts(fonts)    }     fn all_font_names(&self) -> Vec<String> {        let mut names = Vec::new();        let collection = core_text::font_collection::create_for_all_families();        // NOTE: We intentionally avoid using `collection.get_descriptors()` here because        // it has a memory leak bug in core-text v21.0.0. The upstream code uses        // `wrap_under_get_rule` but `CTFontCollectionCreateMatchingFontDescriptors`        // follows the Create Rule (caller owns the result), so it should use        // `wrap_under_create_rule`. We call the function directly with correct memory management.        unsafe extern "C" {            fn CTFontCollectionCreateMatchingFontDescriptors(                collection: CTFontCollectionRef,            ) -> CFArrayRef;        }        let descriptors: Option<CFArray<CTFontDescriptor>> = unsafe {            let array_ref =                CTFontCollectionCreateMatchingFontDescriptors(collection.as_concrete_TypeRef());            if array_ref.is_null() {                None            } else {                Some(CFArray::wrap_under_create_rule(array_ref))            }        };        let Some(descriptors) = descriptors else {            return names;        };        for descriptor in descriptors.into_iter() {            names.extend(lenient_font_attributes::family_name(&descriptor));        }        if let Ok(fonts_in_memory) = self.0.read().memory_source.all_families() {            names.extend(fonts_in_memory);        }        names    }     fn font_id(&self, font: &Font) -> Result<FontId> {        let lock = self.0.upgradable_read();        if let Some(font_id) = lock.font_selections.get(font) {            Ok(*font_id)        } else {            let mut lock = RwLockUpgradableReadGuard::upgrade(lock);            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, 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()            };             let candidate_properties = candidates                .iter()                .map(|font_id| lock.fonts[font_id.0].properties())                .collect::<SmallVec<[_; 4]>>();             let ix = font_kit::matching::find_best_match(                &candidate_properties,                &font_kit::properties::Properties {                    style: fontkit_style(font.style),                    weight: fontkit_weight(font.weight),                    stretch: Default::default(),                },            )?;             let font_id = candidates[ix];            lock.font_selections.insert(font.clone(), font_id);            Ok(font_id)        }    }     fn font_metrics(&self, font_id: FontId) -> FontMetrics {        font_kit_metrics_to_metrics(self.0.read().fonts[font_id.0].metrics())    }     fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {        Ok(bounds_from_rect(            self.0.read().fonts[font_id.0].typographic_bounds(glyph_id.0)?,        ))    }     fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {        self.0.read().advance(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 glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {        self.0.read().raster_bounds(params)    }     fn rasterize_glyph(        &self,        glyph_id: &RenderGlyphParams,        raster_bounds: Bounds<DevicePixels>,    ) -> Result<(Size<DevicePixels>, Vec<u8>)> {        self.0.read().rasterize_glyph(glyph_id, raster_bounds)    }     fn layout_line(&self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {        self.0.write().layout_line(text, font_size, font_runs)    }     fn recommended_rendering_mode(        &self,        _font_id: FontId,        _font_size: Pixels,    ) -> TextRenderingMode {        TextRenderingMode::Grayscale    }     fn glyph_dilation_for_color(&self, color: Hsla) -> u8 {        // When font smoothing is enabled, CoreGraphics thickens glyph strokes by an amount that        // depends on the foreground color's luminance. We replicate the logic used by CoreGraphics        // to select between the different levels of dilation.        if !font_smoothing_allowed_by_user() {            return 0;        }        let rgba: Rgba = color.into();        let luminance = 0.2126 * rgba.r + 0.7152 * rgba.g + 0.0722 * rgba.b;        let level = ((4.0 * luminance) + 0.5).floor() as i32;        level.clamp(0, 4) as u8    }} fn font_smoothing_allowed_by_user() -> bool {    static ALLOWED: OnceLock<bool> = OnceLock::new();    *ALLOWED.get_or_init(|| {        use core_foundation_sys::preferences::{            CFPreferencesCopyAppValue, kCFPreferencesCurrentApplication,        };         let key = CFString::new("AppleFontSmoothing");        let value_ref = unsafe {            CFPreferencesCopyAppValue(key.as_concrete_TypeRef(), kCFPreferencesCurrentApplication)        };        if value_ref.is_null() {            return true;        }        let value = unsafe { CFType::wrap_under_create_rule(value_ref) };        let Some(number) = value.downcast_into::<CFNumber>() else {            return true;        };        // Only an explicit value of `0` means that font smoothing is disabled.        number.to_i64() != Some(0)    })} impl MacTextSystemState {    fn add_fonts(&mut self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {        let fonts = fonts            .into_iter()            .map(|bytes| match bytes {                Cow::Borrowed(embedded_font) => {                    let data_provider = unsafe {                        core_graphics::data_provider::CGDataProvider::from_slice(embedded_font)                    };                    let font = core_graphics::font::CGFont::from_data_provider(data_provider)                        .map_err(|()| anyhow!("Could not load an embedded font."))?;                    let font = font_kit::loaders::core_text::Font::from_core_graphics_font(font);                    Ok(Handle::from_native(&font))                }                Cow::Owned(bytes) => Ok(Handle::from_memory(Arc::new(bytes), 0)),            })            .collect::<Result<Vec<_>>>()?;        self.memory_source.add_fonts(fonts.into_iter())?;        Ok(())    }     fn load_family(        &mut self,        name: &str,        features: &FontFeatures,        fallbacks: Option<&FontFallbacks>,    ) -> Result<SmallVec<[FontId; 4]>> {        let name = gpui::font_name_with_fallbacks(name, ".AppleSystemUIFont");         let mut font_ids = SmallVec::new();        let mut postscript_names_seen = HashSet::default();        let family = self            .memory_source            .select_family_by_name(name)            .or_else(|_| self.system_source.select_family_by_name(name))?;        for font in family.fonts() {            let mut font = font.load()?;             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.            {                // We use the 'm' character for text measurements in various spots                // (e.g., the editor). However, at time of writing some of those usages                // will panic if the font has no 'm' glyph.                //                // Therefore, we check up front that the font has the necessary glyph.                let has_m_glyph = font.glyph_for_char('m').is_some();                 // HACK: The 'Segoe Fluent Icons' font does not have an 'm' glyph,                // but we need to be able to load it for rendering Windows icons in                // the Storybook (on macOS).                let is_segoe_fluent_icons = font.full_name() == "Segoe Fluent Icons";                 if !has_m_glyph && !is_segoe_fluent_icons {                    // I spent far too long trying to track down why a font missing the 'm'                    // character wasn't loading. This log statement will hopefully save                    // someone else from suffering the same fate.                    log::warn!(                        "font '{}' has no 'm' character and was not loaded",                        font.full_name()                    );                    continue;                }            }             // We've seen a number of panics in production caused by calling font.properties()            // which unwraps a downcast to CFNumber. This is an attempt to avoid the panic,            // and to try and identify the incalcitrant font.            let traits = font.native_font().all_traits();            if unsafe {                !(traits                    .get(kCTFontSymbolicTrait)                    .downcast::<CFNumber>()                    .is_some()                    && traits                        .get(kCTFontWidthTrait)                        .downcast::<CFNumber>()                        .is_some()                    && traits                        .get(kCTFontWeightTrait)                        .downcast::<CFNumber>()                        .is_some()                    && traits                        .get(kCTFontSlantTrait)                        .downcast::<CFNumber>()                        .is_some())            } {                log::error!(                    "Failed to read traits for font {:?} (PostScript name {:?})",                    font.full_name(),                    font.postscript_name(),                );                continue;            }             let Some(postscript_name) = font.postscript_name() else {                log::warn!(                    "font {:?} in family {:?} has no PostScript name; skipping",                    font.full_name(),                    name,                );                continue;            };            // Dedup is scoped to this single `load_family` call (issue #55472).            // The same family can be reloaded later under a different `FontKey`            // (different features/fallbacks); a global check against            // `font_ids_by_postscript_name` would skip every already-registered            // font and leave the second call's `font_ids` empty.            if !postscript_names_seen.insert(postscript_name.clone()) {                log::warn!(                    "skipping duplicate font {:?} with PostScript name {:?} \                     in family {:?}",                    font.full_name(),                    postscript_name,                    name,                );                continue;            }            let font_id = FontId(self.fonts.len());            font_ids.push(font_id);            self.font_ids_by_postscript_name                .insert(postscript_name.clone(), font_id);            self.postscript_names_by_font_id                .insert(font_id, postscript_name);            self.fonts.push(font);        }        Ok(font_ids)    }     fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {        Ok(size_from_vector2f(            self.fonts[font_id.0].advance(glyph_id.0)?,        ))    }     fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {        self.fonts[font_id.0].glyph_for_char(ch).map(GlyphId)    }     fn id_for_native_font(&mut self, requested_font: CTFont) -> FontId {        let postscript_name = requested_font.postscript_name();        if let Some(font_id) = self.font_ids_by_postscript_name.get(&postscript_name) {            *font_id        } else {            let font_id = FontId(self.fonts.len());            self.font_ids_by_postscript_name                .insert(postscript_name.clone(), font_id);            self.postscript_names_by_font_id                .insert(font_id, postscript_name);            self.fonts                .push(font_kit::font::Font::from_core_graphics_font(                    requested_font.copy_to_CGFont(),                ));            font_id        }    }     fn is_emoji(&self, font_id: FontId) -> bool {        self.postscript_names_by_font_id            .get(&font_id)            .is_some_and(|postscript_name| {                postscript_name == "AppleColorEmoji" || postscript_name == ".AppleColorEmojiUI"            })    }     fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {        let font = &self.fonts[params.font_id.0];        let scale = Transform2F::from_scale(params.scale_factor);        let bounds: Bounds<DevicePixels> = bounds_from_rect_i(font.raster_bounds(            params.glyph_id.0,            params.font_size.into(),            scale,            HintingOptions::None,            font_kit::canvas::RasterizationOptions::GrayscaleAa,        )?);         // Expand the bounds by 1 pixel on each side to give CG room for anti-aliasing.        Ok(bounds.dilate(DevicePixels(1)))    }     fn rasterize_glyph(        &self,        params: &RenderGlyphParams,        glyph_bounds: Bounds<DevicePixels>,    ) -> Result<(Size<DevicePixels>, Vec<u8>)> {        if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 {            anyhow::bail!("glyph bounds are empty");        } else {            // Add an extra pixel when the subpixel variant isn't zero to make room for anti-aliasing.            let mut bitmap_size = glyph_bounds.size;            if params.subpixel_variant.x > 0 {                bitmap_size.width += DevicePixels(1);            }            if params.subpixel_variant.y > 0 {                bitmap_size.height += DevicePixels(1);            }            let bitmap_size = bitmap_size;             let mut bytes;            let cx;            if params.is_emoji {                bytes = vec![0; bitmap_size.width.0 as usize * 4 * bitmap_size.height.0 as usize];                cx = CGContext::create_bitmap_context(                    Some(bytes.as_mut_ptr() as *mut _),                    bitmap_size.width.0 as usize,                    bitmap_size.height.0 as usize,                    8,                    bitmap_size.width.0 as usize * 4,                    &CGColorSpace::create_device_rgb(),                    kCGImageAlphaPremultipliedLast,                );            } else {                bytes = vec![0; bitmap_size.width.0 as usize * bitmap_size.height.0 as usize];                cx = CGContext::create_bitmap_context(                    Some(bytes.as_mut_ptr() as *mut _),                    bitmap_size.width.0 as usize,                    bitmap_size.height.0 as usize,                    8,                    bitmap_size.width.0 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.            cx.translate(                -glyph_bounds.origin.x.0 as CGFloat,                (glyph_bounds.origin.y.0 + glyph_bounds.size.height.0) as CGFloat,            );            cx.scale(                params.scale_factor as CGFloat,                params.scale_factor as CGFloat,            );             let subpixel_shift = params                .subpixel_variant                .map(|v| v as f32 / SUBPIXEL_VARIANTS_X as f32);            cx.set_text_drawing_mode(CGTextDrawingMode::CGTextFill);            cx.set_allows_antialiasing(true);            cx.set_should_antialias(true);            cx.set_allows_font_subpixel_positioning(true);            cx.set_should_subpixel_position_fonts(true);            cx.set_allows_font_subpixel_quantization(false);            cx.set_should_subpixel_quantize_fonts(false);

Lines 1–500 of 904.