diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 6cd8e9cd8d4..ac36f05c425 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -31,10 +31,10 @@ pub(crate) type PlatformScreenCaptureFrame = core_video::image_buffer::CVImageBu use crate::{ Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds, DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun, - ForegroundExecutor, GlyphId, GpuSpecs, ImageSource, Keymap, LineLayout, Pixels, PlatformInput, - Point, Priority, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Scene, - ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer, SystemWindowTab, Task, - ThreadTaskTimings, Window, WindowControlArea, hash, point, px, size, + ForegroundExecutor, GlyphId, GpuSpecs, Hsla, ImageSource, Keymap, LineLayout, Pixels, + PlatformInput, Point, Priority, RenderGlyphParams, RenderImage, RenderImageParams, + RenderSvgParams, Scene, ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer, + SystemWindowTab, Task, ThreadTaskTimings, Window, WindowControlArea, hash, point, px, size, }; use anyhow::Result; #[cfg(any(target_os = "linux", target_os = "freebsd"))] @@ -783,6 +783,10 @@ pub trait PlatformTextSystem: Send + Sync { /// Returns the recommended text rendering mode for the given font and size. fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels) -> TextRenderingMode; + /// Returns the dilation level to use for a glyph painted in the given color. + fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 { + 0 + } } #[expect(missing_docs)] diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index c7b11ecaa4e..bebe180b262 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -348,6 +348,11 @@ impl TextSystem { .rasterize_glyph(params, raster_bounds) } + /// Returns the dilation level to use for a glyph painted in the given color. + pub(crate) fn glyph_dilation_for_color(&self, color: Hsla) -> u8 { + self.platform_text_system.glyph_dilation_for_color(color) + } + /// Returns the text rendering mode recommended by the platform for the given font and size. /// The return value will never be [`TextRenderingMode::PlatformDefault`]. pub(crate) fn recommended_rendering_mode( @@ -1007,6 +1012,7 @@ pub struct RenderGlyphParams { pub scale_factor: f32, pub is_emoji: bool, pub subpixel_rendering: bool, + pub dilation: u8, } impl Eq for RenderGlyphParams {} @@ -1020,6 +1026,7 @@ impl Hash for RenderGlyphParams { self.scale_factor.to_bits().hash(state); self.is_emoji.hash(state); self.subpixel_rendering.hash(state); + self.dilation.hash(state); } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 4cea82bde7a..99374fd042e 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -3591,6 +3591,7 @@ impl Window { ); let integer_origin = quantized_origin.map(|c| ScaledPixels(c.trunc())); let subpixel_rendering = self.should_use_subpixel_rendering(font_id, font_size); + let dilation = self.text_system().glyph_dilation_for_color(color); let params = RenderGlyphParams { font_id, glyph_id, @@ -3599,6 +3600,7 @@ impl Window { scale_factor, is_emoji: false, subpixel_rendering, + dilation, }; let raster_bounds = self.text_system().raster_bounds(¶ms)?; @@ -3688,6 +3690,7 @@ impl Window { scale_factor, is_emoji: true, subpixel_rendering: false, + dilation: 0, }; let raster_bounds = self.text_system().raster_bounds(¶ms)?; diff --git a/crates/gpui_macos/src/text_system.rs b/crates/gpui_macos/src/text_system.rs index d4ffd2514e3..80145f706ee 100644 --- a/crates/gpui_macos/src/text_system.rs +++ b/crates/gpui_macos/src/text_system.rs @@ -35,9 +35,9 @@ use font_kit::{ }; use gpui::{ Bounds, DevicePixels, Font, FontFallbacks, FontFeatures, FontId, FontMetrics, FontRun, - FontStyle, FontWeight, GlyphId, LineLayout, Pixels, PlatformTextSystem, RenderGlyphParams, - Result, SUBPIXEL_VARIANTS_X, ShapedGlyph, ShapedRun, SharedString, Size, TextRenderingMode, - point, px, size, swap_rgba_pa_to_bgra, + 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::{ @@ -46,7 +46,7 @@ use pathfinder_geometry::{ vector::Vector2F, }; use smallvec::SmallVec; -use std::{borrow::Cow, char, convert::TryFrom, sync::Arc}; +use std::{borrow::Cow, char, convert::TryFrom, sync::Arc, sync::OnceLock}; use crate::open_type::apply_features_and_fallbacks; @@ -214,6 +214,39 @@ impl PlatformTextSystem for MacTextSystem { ) -> 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 = 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 number = unsafe { CFNumber::wrap_under_create_rule(value_ref as _) }; + // Only an explicit value of `0` means that font smoothing is disabled. + number.to_i64() != Some(0) + }) } impl MacTextSystemState { @@ -361,7 +394,7 @@ impl MacTextSystemState { fn raster_bounds(&self, params: &RenderGlyphParams) -> Result> { let font = &self.fonts[params.font_id.0]; let scale = Transform2F::from_scale(params.scale_factor); - let mut bounds: Bounds = bounds_from_rect_i(font.raster_bounds( + let bounds: Bounds = bounds_from_rect_i(font.raster_bounds( params.glyph_id.0, params.font_size.into(), scale, @@ -369,14 +402,8 @@ impl MacTextSystemState { font_kit::canvas::RasterizationOptions::GrayscaleAa, )?); - // Add 3% of font size as padding, clamped between 1 and 5 pixels - // to avoid clipping of anti-aliased edges. - let pad = - ((params.font_size.as_f32() * 0.03 * params.scale_factor).ceil() as i32).clamp(1, 5); - bounds.origin.x -= DevicePixels(pad); - bounds.size.width += DevicePixels(pad); - - Ok(bounds) + // Expand the bounds by 1 pixel on each side to give CG room for anti-aliasing. + Ok(bounds.dilate(DevicePixels(1))) } fn rasterize_glyph( @@ -438,13 +465,20 @@ impl MacTextSystemState { .subpixel_variant .map(|v| v as f32 / SUBPIXEL_VARIANTS_X as f32); cx.set_text_drawing_mode(CGTextDrawingMode::CGTextFill); - cx.set_gray_fill_color(0.0, 1.0); 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); + + if params.dilation > 0 { + let luminance = params.dilation as f64 * 0.25; + cx.set_should_smooth_fonts(true); + cx.set_gray_fill_color(luminance, 1.0); + } else { + cx.set_gray_fill_color(0.0, 1.0); + } self.fonts[params.font_id.0] .native_font() .clone_with_font_size(f32::from(params.font_size) as CGFloat)