mirror of
https://github.com/zed-industries/zed.git
synced 2026-05-27 08:34:11 +00:00
Implement luminance-based glyph dilation for macOS (#54886)
Apple's text rendering stack dilates glyph outlines for text rendered with a light foreground color. Zed doesn't consider this nuance today; we populate our atlas using glyphs rendered with a dark foreground color. This means that, particularly in dark themes, text in Zed looks thin and blurry, and doesn't match the look of native macOS applications. This pull request replicates the native behavior of Core Graphics. Some reverse-engineering revealed that CG computes the foreground color luminance using the Rec. 709 formula ($Y=0.2126R + 0.7152B + 0.0722G$) and quantizes it into five levels (0, 0.25, 0.5, 0.75, and 1). Each level uses a different dilation factor. With this patch, we calculate this same luminance bucket and supply it as the foreground color during rasterization. The correct dilation will be applied, and we'll store this glyph in the atlas keyed by this luminance bucket. So, we'll generate and use up to 5 different bitmaps for each glyph based on its foreground color. I've confirmed that text rendered by Zed now exactly matches native applications like Safari, TextEdit, etc. Release Notes: - Improved text rendering clarity on macOS, particularly in dark themes. --------- Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com>
This commit is contained in:
parent
3014170d7e
commit
a38fc8c8de
4 changed files with 66 additions and 18 deletions
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)?;
|
||||
|
|
|
|||
|
|
@ -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<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 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<Bounds<DevicePixels>> {
|
||||
let font = &self.fonts[params.font_id.0];
|
||||
let scale = Transform2F::from_scale(params.scale_factor);
|
||||
let mut bounds: Bounds<DevicePixels> = bounds_from_rect_i(font.raster_bounds(
|
||||
let bounds: Bounds<DevicePixels> = 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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue