Detailed changes
@@ -2187,8 +2187,7 @@ dependencies = [
[[package]]
name = "blade-graphics"
version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4deb8f595ce7f00dee3543ebf6fd9a20ea86fc421ab79600dac30876250bdae"
+source = "git+https://github.com/kvark/blade?rev=e3cf011ca18a6dfd907d1dedd93e85e21f005fe3#e3cf011ca18a6dfd907d1dedd93e85e21f005fe3"
dependencies = [
"ash",
"ash-window",
@@ -2222,8 +2221,7 @@ dependencies = [
[[package]]
name = "blade-macros"
version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27142319e2f4c264581067eaccb9f80acccdde60d8b4bf57cc50cd3152f109ca"
+source = "git+https://github.com/kvark/blade?rev=e3cf011ca18a6dfd907d1dedd93e85e21f005fe3#e3cf011ca18a6dfd907d1dedd93e85e21f005fe3"
dependencies = [
"proc-macro2",
"quote",
@@ -2233,8 +2231,7 @@ dependencies = [
[[package]]
name = "blade-util"
version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a6be3a82c001ba7a17b6f8e413ede5d1004e6047213f8efaf0ffc15b5c4904c"
+source = "git+https://github.com/kvark/blade?rev=e3cf011ca18a6dfd907d1dedd93e85e21f005fe3#e3cf011ca18a6dfd907d1dedd93e85e21f005fe3"
dependencies = [
"blade-graphics",
"bytemuck",
@@ -7392,6 +7389,7 @@ dependencies = [
"stacksafe",
"strum 0.27.2",
"sum_tree",
+ "swash",
"taffy",
"thiserror 2.0.17",
"unicode-segmentation",
@@ -473,9 +473,9 @@ backtrace = "0.3"
base64 = "0.22"
bincode = "1.2.1"
bitflags = "2.6.0"
-blade-graphics = { version = "0.7.0" }
-blade-macros = { version = "0.3.0" }
-blade-util = { version = "0.3.0" }
+blade-graphics = { git = "https://github.com/kvark/blade", rev = "e3cf011ca18a6dfd907d1dedd93e85e21f005fe3" }
+blade-macros = { git = "https://github.com/kvark/blade", rev = "e3cf011ca18a6dfd907d1dedd93e85e21f005fe3" }
+blade-util = { git = "https://github.com/kvark/blade", rev = "e3cf011ca18a6dfd907d1dedd93e85e21f005fe3" }
brotli = "8.0.2"
bytes = "1.0"
cargo_metadata = "0.19"
@@ -169,6 +169,15 @@
// 2. Always quit the application
// "on_last_window_closed": "quit_app",
"on_last_window_closed": "platform_default",
+ // The text rendering mode to use.
+ // May take 3 values:
+ // 1. Use platform default behavior:
+ // "text_rendering_mode": "platform_default"
+ // 2. Use subpixel (ClearType-style) text rendering:
+ // "text_rendering_mode": "subpixel"
+ // 3. Use grayscale text rendering:
+ // "text_rendering_mode": "grayscale"
+ "text_rendering_mode": "platform_default",
// Whether to show padding for zoomed panels.
// When enabled, zoomed center panels (e.g. code editor) will have padding all around,
// while zoomed bottom/left/right panels will have padding to the top/right/left (respectively).
@@ -183,6 +183,7 @@ blade-macros = { workspace = true, optional = true }
blade-util = { workspace = true, optional = true }
bytemuck = { version = "1", optional = true }
cosmic-text = { version = "0.14.0", optional = true }
+swash = { version = "0.2.6" }
# WARNING: If you change this, you must also publish a new version of zed-font-kit to crates.io
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "110523127440aefb11ce0cf280ae7c5071337ec5", package = "zed-font-kit", version = "0.14.1-zed", features = [
"source-fontconfig-dlopen",
@@ -297,6 +297,7 @@ mod windows {
"path_sprite",
"underline",
"monochrome_sprite",
+ "subpixel_sprite",
"polychrome_sprite",
];
@@ -1,6 +1,6 @@
use std::{
any::{TypeId, type_name},
- cell::{BorrowMutError, Ref, RefCell, RefMut},
+ cell::{BorrowMutError, Cell, Ref, RefCell, RefMut},
marker::PhantomData,
mem,
ops::{Deref, DerefMut},
@@ -43,8 +43,8 @@ use crate::{
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, Priority,
PromptBuilder, PromptButton, PromptHandle, PromptLevel, Render, RenderImage,
RenderablePromptHandle, Reservation, ScreenCaptureSource, SharedString, SubscriberSet,
- Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance, WindowHandle, WindowId,
- WindowInvalidator,
+ Subscription, SvgRenderer, Task, TextRenderingMode, TextSystem, Window, WindowAppearance,
+ WindowHandle, WindowId, WindowInvalidator,
colors::{Colors, GlobalColors},
current_platform, hash, init_app_menus,
};
@@ -637,6 +637,7 @@ pub struct App {
pub(crate) inspector_element_registry: InspectorElementRegistry,
#[cfg(any(test, feature = "test-support", debug_assertions))]
pub(crate) name: Option<&'static str>,
+ pub(crate) text_rendering_mode: Rc<Cell<TextRenderingMode>>,
quit_mode: QuitMode,
quitting: bool,
}
@@ -666,6 +667,7 @@ impl App {
liveness: std::sync::Arc::new(()),
platform: platform.clone(),
text_system,
+ text_rendering_mode: Rc::new(Cell::new(TextRenderingMode::default())),
mode: GpuiMode::Production,
actions: Rc::new(ActionRegistry::default()),
flushing_effects: false,
@@ -1088,6 +1090,16 @@ impl App {
self.platform.read_from_clipboard()
}
+ /// Sets the text rendering mode for the application.
+ pub fn set_text_rendering_mode(&mut self, mode: TextRenderingMode) {
+ self.text_rendering_mode.set(mode);
+ }
+
+ /// Returns the current text rendering mode for the application.
+ pub fn text_rendering_mode(&self) -> TextRenderingMode {
+ self.text_rendering_mode.get()
+ }
+
/// Writes data to the platform clipboard.
pub fn write_to_clipboard(&self, item: ClipboardItem) {
self.platform.write_to_clipboard(item)
@@ -499,6 +499,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn activate(&self);
fn is_active(&self) -> bool;
fn is_hovered(&self) -> bool;
+ fn background_appearance(&self) -> WindowBackgroundAppearance;
fn set_title(&mut self, title: &str);
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
fn minimize(&self);
@@ -518,6 +519,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn draw(&self, scene: &Scene);
fn completed_frame(&self) {}
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
+ fn is_subpixel_rendering_supported(&self) -> bool;
// macOS specific methods
fn get_title(&self) -> String {
@@ -654,6 +656,8 @@ pub(crate) trait PlatformTextSystem: Send + Sync {
raster_bounds: Bounds<DevicePixels>,
) -> Result<(Size<DevicePixels>, Vec<u8>)>;
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
+ fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
+ -> TextRenderingMode;
}
pub(crate) struct NoopTextSystem;
@@ -774,6 +778,14 @@ impl PlatformTextSystem for NoopTextSystem {
len: text.len(),
}
}
+
+ fn recommended_rendering_mode(
+ &self,
+ _font_id: FontId,
+ _font_size: Pixels,
+ ) -> TextRenderingMode {
+ TextRenderingMode::Grayscale
+ }
}
// Adapted from https://github.com/microsoft/terminal/blob/1283c0f5b99a2961673249fa77c6b986efb5086c/src/renderer/atlas/dwrite.cpp
@@ -831,6 +843,8 @@ impl AtlasKey {
AtlasKey::Glyph(params) => {
if params.is_emoji {
AtlasTextureKind::Polychrome
+ } else if params.subpixel_rendering {
+ AtlasTextureKind::Subpixel
} else {
AtlasTextureKind::Monochrome
}
@@ -932,6 +946,7 @@ pub(crate) struct AtlasTextureId {
pub(crate) enum AtlasTextureKind {
Monochrome = 0,
Polychrome = 1,
+ Subpixel = 2,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
@@ -1443,6 +1458,18 @@ pub enum WindowBackgroundAppearance {
MicaAltBackdrop,
}
+/// The text rendering mode to use for drawing glyphs.
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
+pub enum TextRenderingMode {
+ /// Use the platform's default text rendering mode.
+ #[default]
+ PlatformDefault,
+ /// Use subpixel (ClearType-style) text rendering.
+ Subpixel,
+ /// Use grayscale text rendering.
+ Grayscale,
+}
+
/// The options that can be configured for a file dialog prompt
#[derive(Clone, Debug)]
pub struct PathPromptOptions {
@@ -162,6 +162,10 @@ impl BladeAtlasState {
format = gpu::TextureFormat::R8Unorm;
usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE;
}
+ AtlasTextureKind::Subpixel => {
+ format = gpu::TextureFormat::Bgra8Unorm;
+ usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE;
+ }
AtlasTextureKind::Polychrome => {
format = gpu::TextureFormat::Bgra8Unorm;
usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE;
@@ -263,6 +267,7 @@ impl BladeAtlasState {
#[derive(Default)]
struct BladeAtlasStorage {
monochrome_textures: AtlasTextureList<BladeAtlasTexture>,
+ subpixel_textures: AtlasTextureList<BladeAtlasTexture>,
polychrome_textures: AtlasTextureList<BladeAtlasTexture>,
}
@@ -271,6 +276,7 @@ impl ops::Index<AtlasTextureKind> for BladeAtlasStorage {
fn index(&self, kind: AtlasTextureKind) -> &Self::Output {
match kind {
crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
+ crate::AtlasTextureKind::Subpixel => &self.subpixel_textures,
crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
}
}
@@ -280,6 +286,7 @@ impl ops::IndexMut<AtlasTextureKind> for BladeAtlasStorage {
fn index_mut(&mut self, kind: AtlasTextureKind) -> &mut Self::Output {
match kind {
crate::AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
+ crate::AtlasTextureKind::Subpixel => &mut self.subpixel_textures,
crate::AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
}
}
@@ -290,6 +297,7 @@ impl ops::Index<AtlasTextureId> for BladeAtlasStorage {
fn index(&self, id: AtlasTextureId) -> &Self::Output {
let textures = match id.kind {
crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
+ crate::AtlasTextureKind::Subpixel => &self.subpixel_textures,
crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
};
textures[id.index as usize].as_ref().unwrap()
@@ -301,6 +309,9 @@ impl BladeAtlasStorage {
for mut texture in self.monochrome_textures.drain().flatten() {
texture.destroy(gpu);
}
+ for mut texture in self.subpixel_textures.drain().flatten() {
+ texture.destroy(gpu);
+ }
for mut texture in self.polychrome_textures.drain().flatten() {
texture.destroy(gpu);
}
@@ -34,6 +34,11 @@ impl BladeContext {
);
Ok(Self { gpu })
}
+
+ #[allow(dead_code)]
+ pub fn supports_dual_source_blending(&self) -> bool {
+ self.gpu.capabilities().dual_source_blending
+ }
}
fn parse_pci_id(id: &str) -> anyhow::Result<u32> {
@@ -95,6 +95,16 @@ struct ShaderMonoSpritesData {
b_mono_sprites: gpu::BufferPiece,
}
+#[derive(blade_macros::ShaderData)]
+struct ShaderSubpixelSpritesData {
+ globals: GlobalParams,
+ gamma_ratios: [f32; 4],
+ subpixel_enhanced_contrast: f32,
+ t_sprite: gpu::TextureView,
+ s_sprite: gpu::Sampler,
+ b_subpixel_sprites: gpu::BufferPiece,
+}
+
#[derive(blade_macros::ShaderData)]
struct ShaderPolySpritesData {
globals: GlobalParams,
@@ -134,6 +144,7 @@ struct BladePipelines {
paths: gpu::RenderPipeline,
underlines: gpu::RenderPipeline,
mono_sprites: gpu::RenderPipeline,
+ subpixel_sprites: gpu::RenderPipeline,
poly_sprites: gpu::RenderPipeline,
surfaces: gpu::RenderPipeline,
}
@@ -277,6 +288,31 @@ impl BladePipelines {
color_targets,
multisample_state: gpu::MultisampleState::default(),
}),
+ subpixel_sprites: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
+ name: "subpixel-sprites",
+ data_layouts: &[&ShaderSubpixelSpritesData::layout()],
+ vertex: shader.at("vs_subpixel_sprite"),
+ vertex_fetches: &[],
+ primitive: gpu::PrimitiveState {
+ topology: gpu::PrimitiveTopology::TriangleStrip,
+ ..Default::default()
+ },
+ depth_stencil: None,
+ fragment: Some(shader.at("fs_subpixel_sprite")),
+ color_targets: &[gpu::ColorTargetState {
+ format: surface_info.format,
+ blend: Some(gpu::BlendState {
+ color: gpu::BlendComponent {
+ src_factor: gpu::BlendFactor::Src1,
+ dst_factor: gpu::BlendFactor::OneMinusSrc1,
+ operation: gpu::BlendOperation::Add,
+ },
+ alpha: gpu::BlendComponent::OVER,
+ }),
+ write_mask: gpu::ColorWrites::COLOR,
+ }],
+ multisample_state: gpu::MultisampleState::default(),
+ }),
poly_sprites: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
name: "poly-sprites",
data_layouts: &[&ShaderPolySpritesData::layout()],
@@ -315,6 +351,7 @@ impl BladePipelines {
gpu.destroy_render_pipeline(&mut self.paths);
gpu.destroy_render_pipeline(&mut self.underlines);
gpu.destroy_render_pipeline(&mut self.mono_sprites);
+ gpu.destroy_render_pipeline(&mut self.subpixel_sprites);
gpu.destroy_render_pipeline(&mut self.poly_sprites);
gpu.destroy_render_pipeline(&mut self.surfaces);
}
@@ -672,7 +709,11 @@ impl BladeRenderer {
gpu::RenderTargetSet {
colors: &[gpu::RenderTarget {
view: frame.texture_view(),
- init_op: gpu::InitOp::Clear(gpu::TextureColor::TransparentBlack),
+ init_op: gpu::InitOp::Clear(if self.surface_config.transparent {
+ gpu::TextureColor::TransparentBlack
+ } else {
+ gpu::TextureColor::White
+ }),
finish_op: gpu::FinishOp::Store,
}],
depth_stencil: None,
@@ -818,6 +859,29 @@ impl BladeRenderer {
);
encoder.draw(0, 4, 0, sprites.len() as u32);
}
+ PrimitiveBatch::SubpixelSprites {
+ texture_id,
+ sprites,
+ } => {
+ let tex_info = self.atlas.get_texture_info(texture_id);
+ let instance_buf =
+ unsafe { self.instance_belt.alloc_typed(sprites, &self.gpu) };
+ let mut encoder = pass.with(&self.pipelines.subpixel_sprites);
+ encoder.bind(
+ 0,
+ &ShaderSubpixelSpritesData {
+ globals,
+ gamma_ratios: self.rendering_parameters.gamma_ratios,
+ subpixel_enhanced_contrast: self
+ .rendering_parameters
+ .subpixel_enhanced_contrast,
+ t_sprite: tex_info.raw_view,
+ s_sprite: self.atlas_sampler,
+ b_subpixel_sprites: instance_buf,
+ },
+ );
+ encoder.draw(0, 4, 0, sprites.len() as u32);
+ }
PrimitiveBatch::Surfaces(surfaces) => {
let mut _encoder = pass.with(&self.pipelines.surfaces);
@@ -1016,6 +1080,10 @@ struct RenderingParameters {
// Allowed range: [0.0, ..), other values are clipped
// Default: 1.0
grayscale_enhanced_contrast: f32,
+ // Env var: ZED_FONTS_SUBPIXEL_ENHANCED_CONTRAST
+ // Allowed range: [0.0, ..), other values are clipped
+ // Default: 0.5
+ subpixel_enhanced_contrast: f32,
}
impl RenderingParameters {
@@ -1042,11 +1110,17 @@ impl RenderingParameters {
.and_then(|v| v.parse().ok())
.unwrap_or(1.0_f32)
.max(0.0);
+ let subpixel_enhanced_contrast = env::var("ZED_FONTS_SUBPIXEL_ENHANCED_CONTRAST")
+ .ok()
+ .and_then(|v| v.parse().ok())
+ .unwrap_or(0.5_f32)
+ .max(0.0);
Self {
path_sample_count,
gamma_ratios,
grayscale_enhanced_contrast,
+ subpixel_enhanced_contrast,
}
}
}
@@ -1,3 +1,4 @@
+enable dual_source_blending;
/* Functions useful for debugging:
// A heat map color for debugging (blue -> cyan -> green -> yellow -> red).
@@ -46,12 +47,22 @@ fn enhance_contrast(alpha: f32, k: f32) -> f32 {
return alpha * (k + 1.0) / (alpha * k + 1.0);
}
+fn enhance_contrast3(alpha: vec3<f32>, k: f32) -> vec3<f32> {
+ return alpha * (k + 1.0) / (alpha * k + 1.0);
+}
+
fn apply_alpha_correction(a: f32, b: f32, g: vec4<f32>) -> f32 {
let brightness_adjustment = g.x * b + g.y;
let correction = brightness_adjustment * a + (g.z * b + g.w);
return a + a * (1.0 - a) * correction;
}
+fn apply_alpha_correction3(a: vec3<f32>, b: vec3<f32>, g: vec4<f32>) -> vec3<f32> {
+ let brightness_adjustment = g.x * b + g.y;
+ let correction = brightness_adjustment * a + (g.z * b + g.w);
+ return a + a * (1.0 - a) * correction;
+}
+
fn apply_contrast_and_gamma_correction(sample: f32, color: vec3<f32>, enhanced_contrast_factor: f32, gamma_ratios: vec4<f32>) -> f32 {
let enhanced_contrast = light_on_dark_contrast(enhanced_contrast_factor, color);
let brightness = color_brightness(color);
@@ -60,6 +71,13 @@ fn apply_contrast_and_gamma_correction(sample: f32, color: vec3<f32>, enhanced_c
return apply_alpha_correction(contrasted, brightness, gamma_ratios);
}
+fn apply_contrast_and_gamma_correction3(sample: vec3<f32>, color: vec3<f32>, enhanced_contrast_factor: f32, gamma_ratios: vec4<f32>) -> vec3<f32> {
+ let enhanced_contrast = light_on_dark_contrast(enhanced_contrast_factor, color);
+
+ let contrasted = enhance_contrast3(sample, enhanced_contrast);
+ return apply_alpha_correction3(contrasted, color, gamma_ratios);
+}
+
struct GlobalParams {
viewport_size: vec2<f32>,
premultiplied_alpha: u32,
@@ -69,6 +87,7 @@ struct GlobalParams {
var<uniform> globals: GlobalParams;
var<uniform> gamma_ratios: vec4<f32>;
var<uniform> grayscale_enhanced_contrast: f32;
+var<uniform> subpixel_enhanced_contrast: f32;
var t_sprite: texture_2d<f32>;
var s_sprite: sampler;
@@ -1190,7 +1209,6 @@ fn fs_mono_sprite(input: MonoSpriteVarying) -> @location(0) vec4<f32> {
return vec4<f32>(0.0);
}
- // convert to srgb space as the rest of the code (output swapchain) expects that
return blend_color(input.color, alpha_corrected);
}
@@ -1297,3 +1315,57 @@ fn fs_surface(input: SurfaceVarying) -> @location(0) vec4<f32> {
return ycbcr_to_RGB * y_cb_cr;
}
+
+// --- subpixel sprites --- //
+
+struct SubpixelSprite {
+ order: u32,
+ pad: u32,
+ bounds: Bounds,
+ content_mask: Bounds,
+ color: Hsla,
+ tile: AtlasTile,
+ transformation: TransformationMatrix,
+}
+var<storage, read> b_subpixel_sprites: array<SubpixelSprite>;
+
+struct SubpixelSpriteOutput {
+ @builtin(position) position: vec4<f32>,
+ @location(0) tile_position: vec2<f32>,
+ @location(1) @interpolate(flat) color: vec4<f32>,
+ @location(3) clip_distances: vec4<f32>,
+}
+
+struct SubpixelSpriteFragmentOutput {
+ @location(0) @blend_src(0) foreground: vec4<f32>,
+ @location(0) @blend_src(1) alpha: vec4<f32>,
+}
+
+@vertex
+fn vs_subpixel_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> SubpixelSpriteOutput {
+ let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
+ let sprite = b_subpixel_sprites[instance_id];
+
+ var out = SubpixelSpriteOutput();
+ out.position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation);
+ out.tile_position = to_tile_position(unit_vertex, sprite.tile);
+ out.color = hsla_to_rgba(sprite.color);
+ out.clip_distances = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation);
+ return out;
+}
+
+@fragment
+fn fs_subpixel_sprite(input: SubpixelSpriteOutput) -> SubpixelSpriteFragmentOutput {
+ let sample = textureSample(t_sprite, s_sprite, input.tile_position).rgb;
+ let alpha_corrected = apply_contrast_and_gamma_correction3(sample, input.color.rgb, subpixel_enhanced_contrast, gamma_ratios);
+
+ // Alpha clip after using the derivatives.
+ if (any(input.clip_distances < vec4<f32>(0.0))) {
+ return SubpixelSpriteFragmentOutput(vec4<f32>(0.0), vec4<f32>(0.0));
+ }
+
+ var out = SubpixelSpriteFragmentOutput();
+ out.foreground = vec4<f32>(input.color.rgb, 1.0);
+ out.alpha = vec4<f32>(input.color.a * alpha_corrected, 1.0);
+ return out;
+}
@@ -1,13 +1,14 @@
use crate::{
Bounds, DevicePixels, Font, FontFeatures, FontId, FontMetrics, FontRun, FontStyle, FontWeight,
GlyphId, LineLayout, Pixels, PlatformTextSystem, Point, RenderGlyphParams, SUBPIXEL_VARIANTS_X,
- SUBPIXEL_VARIANTS_Y, ShapedGlyph, ShapedRun, SharedString, Size, point, size,
+ SUBPIXEL_VARIANTS_Y, ShapedGlyph, ShapedRun, SharedString, Size, TextRenderingMode, point,
+ size,
};
use anyhow::{Context as _, Ok, Result};
use collections::HashMap;
use cosmic_text::{
- Attrs, AttrsList, CacheKey, Family, Font as CosmicTextFont, FontFeatures as CosmicFontFeatures,
- FontSystem, ShapeBuffer, ShapeLine, SwashCache,
+ Attrs, AttrsList, Family, Font as CosmicTextFont, FontFeatures as CosmicFontFeatures,
+ FontSystem, ShapeBuffer, ShapeLine,
};
use itertools::Itertools;
@@ -18,6 +19,10 @@ use pathfinder_geometry::{
};
use smallvec::SmallVec;
use std::{borrow::Cow, sync::Arc};
+use swash::{
+ scale::{Render, ScaleContext, Source, StrikeWith},
+ zeno::{Format, Transform, Vector},
+};
pub(crate) struct CosmicTextSystem(RwLock<CosmicTextSystemState>);
@@ -34,9 +39,9 @@ impl FontKey {
}
struct CosmicTextSystemState {
- swash_cache: SwashCache,
font_system: FontSystem,
scratch: ShapeBuffer,
+ swash_scale_context: ScaleContext,
/// Contains all already loaded fonts, including all faces. Indexed by `FontId`.
loaded_fonts: Vec<LoadedFont>,
/// Caches the `FontId`s associated with a specific family to avoid iterating the font database
@@ -57,8 +62,8 @@ impl CosmicTextSystem {
Self(RwLock::new(CosmicTextSystemState {
font_system,
- swash_cache: SwashCache::new(),
scratch: ShapeBuffer::default(),
+ swash_scale_context: ScaleContext::new(),
loaded_fonts: Vec::new(),
font_ids_by_family_cache: HashMap::default(),
}))
@@ -183,6 +188,15 @@ impl PlatformTextSystem for CosmicTextSystem {
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout {
self.0.write().layout_line(text, font_size, runs)
}
+
+ fn recommended_rendering_mode(
+ &self,
+ _font_id: FontId,
+ _font_size: Pixels,
+ ) -> TextRenderingMode {
+ // Ideally, we'd use fontconfig to read the user preference.
+ TextRenderingMode::Subpixel
+ }
}
impl CosmicTextSystemState {
@@ -273,26 +287,7 @@ impl CosmicTextSystemState {
}
fn raster_bounds(&mut self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
- let font = &self.loaded_fonts[params.font_id.0].font;
- let subpixel_shift = point(
- params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor,
- params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor,
- );
- let image = self
- .swash_cache
- .get_image(
- &mut self.font_system,
- CacheKey::new(
- font.id(),
- params.glyph_id.0 as u16,
- (params.font_size * params.scale_factor).into(),
- (subpixel_shift.x, subpixel_shift.y.trunc()),
- cosmic_text::CacheKeyFlags::empty(),
- )
- .0,
- )
- .clone()
- .with_context(|| format!("no image for {params:?} in font {font:?}"))?;
+ let image = self.render_glyph_image(params)?;
Ok(Bounds {
origin: point(image.placement.left.into(), (-image.placement.top).into()),
size: size(image.placement.width.into(), image.placement.height.into()),
@@ -307,38 +302,75 @@ impl CosmicTextSystemState {
) -> 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 {
- let bitmap_size = glyph_bounds.size;
- let font = &self.loaded_fonts[params.font_id.0].font;
- let subpixel_shift = point(
- params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor,
- params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor,
- );
- let mut image = self
- .swash_cache
- .get_image(
- &mut self.font_system,
- CacheKey::new(
- font.id(),
- params.glyph_id.0 as u16,
- (params.font_size * params.scale_factor).into(),
- (subpixel_shift.x, subpixel_shift.y.trunc()),
- cosmic_text::CacheKeyFlags::empty(),
- )
- .0,
- )
- .clone()
- .with_context(|| format!("no image for {params:?} in font {font:?}"))?;
-
- if params.is_emoji {
+ }
+
+ let mut image = self.render_glyph_image(params)?;
+ let bitmap_size = glyph_bounds.size;
+ match image.content {
+ swash::scale::image::Content::Color | swash::scale::image::Content::SubpixelMask => {
// Convert from RGBA to BGRA.
for pixel in image.data.chunks_exact_mut(4) {
pixel.swap(0, 2);
}
+ Ok((bitmap_size, image.data))
}
+ swash::scale::image::Content::Mask => Ok((bitmap_size, image.data)),
+ }
+ }
- Ok((bitmap_size, image.data))
+ fn render_glyph_image(
+ &mut self,
+ params: &RenderGlyphParams,
+ ) -> Result<swash::scale::image::Image> {
+ let loaded_font = &self.loaded_fonts[params.font_id.0];
+ let font_ref = loaded_font.font.as_swash();
+ let pixel_size = params.font_size.0;
+
+ let subpixel_offset = Vector::new(
+ params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor,
+ params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor,
+ );
+
+ let mut scaler = self
+ .swash_scale_context
+ .builder(font_ref)
+ .size(pixel_size)
+ .hint(true)
+ .build();
+
+ let sources: &[Source] = if params.is_emoji {
+ &[
+ Source::ColorOutline(0),
+ Source::ColorBitmap(StrikeWith::BestFit),
+ Source::Outline,
+ ]
+ } else {
+ &[Source::Outline]
+ };
+
+ let mut renderer = Render::new(sources);
+ renderer.transform(Some(Transform {
+ xx: params.scale_factor,
+ xy: 0.0,
+ yx: 0.0,
+ yy: params.scale_factor,
+ x: 0.0,
+ y: 0.0,
+ }));
+
+ if params.subpixel_rendering {
+ // There seems to be a bug in Swash where the B and R values are swapped.
+ renderer
+ .format(Format::subpixel_bgra())
+ .offset(subpixel_offset);
+ } else {
+ renderer.format(Format::Alpha).offset(subpixel_offset);
}
+
+ let glyph_id: u16 = params.glyph_id.0.try_into()?;
+ renderer
+ .render(&mut scaler, glyph_id)
+ .with_context(|| format!("unable to render glyph via swash for {params:?}"))
}
/// This is used when cosmic_text has chosen a fallback font instead of using the requested
@@ -204,7 +204,7 @@ pub struct Output {
pub(crate) struct WaylandClientState {
serial_tracker: SerialTracker,
globals: Globals,
- gpu_context: BladeContext,
+ pub gpu_context: BladeContext,
wl_seat: wl_seat::WlSeat, // TODO: Multi seat support
wl_pointer: Option<wl_pointer::WlPointer>,
wl_keyboard: Option<wl_keyboard::WlKeyboard>,
@@ -247,7 +247,7 @@ pub(crate) struct WaylandClientState {
cursor: Cursor,
pending_activation: Option<PendingActivation>,
event_loop: Option<EventLoop<'static, WaylandClientStatePtr>>,
- common: LinuxCommon,
+ pub common: LinuxCommon,
}
pub struct DragState {
@@ -1220,6 +1220,16 @@ impl PlatformWindow for WaylandWindow {
update_window(state);
}
+ fn background_appearance(&self) -> WindowBackgroundAppearance {
+ self.borrow().background_appearance
+ }
+
+ fn is_subpixel_rendering_supported(&self) -> bool {
+ let client = self.borrow().client.get_client();
+ let state = client.borrow();
+ state.gpu_context.supports_dual_source_blending()
+ }
+
fn minimize(&self) {
if let Some(toplevel) = self.borrow().surface_state.toplevel() {
toplevel.set_minimized();
@@ -177,7 +177,7 @@ pub struct X11ClientState {
pub(crate) last_location: Point<Pixels>,
pub(crate) current_count: usize,
- gpu_context: BladeContext,
+ pub(crate) gpu_context: BladeContext,
pub(crate) scale_factor: f32,
@@ -1459,6 +1459,24 @@ impl PlatformWindow for X11Window {
state.renderer.update_transparency(transparent);
}
+ fn background_appearance(&self) -> WindowBackgroundAppearance {
+ self.0.state.borrow().background_appearance
+ }
+
+ fn is_subpixel_rendering_supported(&self) -> bool {
+ self.0
+ .state
+ .borrow()
+ .client
+ .0
+ .upgrade()
+ .map(|ref_cell| {
+ let state = ref_cell.borrow();
+ state.gpu_context.supports_dual_source_blending()
+ })
+ .unwrap_or_default()
+ }
+
fn minimize(&self) {
let state = self.0.state.borrow();
const WINDOW_ICONIC_STATE: u32 = 3;
@@ -66,6 +66,7 @@ impl PlatformAtlas for MetalAtlas {
let textures = match id.kind {
AtlasTextureKind::Monochrome => &mut lock.monochrome_textures,
AtlasTextureKind::Polychrome => &mut lock.polychrome_textures,
+ AtlasTextureKind::Subpixel => unreachable!(),
};
let Some(texture_slot) = textures
@@ -99,6 +100,7 @@ impl MetalAtlasState {
let textures = match texture_kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
+ AtlasTextureKind::Subpixel => unreachable!(),
};
if let Some(tile) = textures
@@ -143,6 +145,7 @@ impl MetalAtlasState {
pixel_format = metal::MTLPixelFormat::BGRA8Unorm;
usage = metal::MTLTextureUsage::ShaderRead;
}
+ AtlasTextureKind::Subpixel => unreachable!(),
}
texture_descriptor.set_pixel_format(pixel_format);
texture_descriptor.set_usage(usage);
@@ -151,6 +154,7 @@ impl MetalAtlasState {
let texture_list = match kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
+ AtlasTextureKind::Subpixel => unreachable!(),
};
let index = texture_list.free_list.pop();
@@ -181,6 +185,7 @@ impl MetalAtlasState {
let textures = match id.kind {
crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
+ crate::AtlasTextureKind::Subpixel => unreachable!(),
};
textures[id.index as usize].as_ref().unwrap()
}
@@ -630,6 +630,7 @@ impl MetalRenderer {
viewport_size,
command_encoder,
),
+ PrimitiveBatch::SubpixelSprites { .. } => unreachable!(),
};
if !ok {
command_encoder.end_encoding();
@@ -2,7 +2,7 @@ use crate::{
Bounds, DevicePixels, Font, FontFallbacks, FontFeatures, FontId, FontMetrics, FontRun,
FontStyle, FontWeight, GlyphId, LineLayout, Pixels, PlatformTextSystem, Point,
RenderGlyphParams, Result, SUBPIXEL_VARIANTS_X, ShapedGlyph, ShapedRun, SharedString, Size,
- point, px, size, swap_rgba_pa_to_bgra,
+ TextRenderingMode, point, px, size, swap_rgba_pa_to_bgra,
};
use anyhow::anyhow;
use cocoa::appkit::CGFloat;
@@ -204,6 +204,14 @@ impl PlatformTextSystem for MacTextSystem {
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
+ }
}
impl MacTextSystemState {
@@ -402,6 +402,7 @@ struct MacWindowState {
native_window: id,
native_view: NonNull<Object>,
blurred_view: Option<id>,
+ background_appearance: WindowBackgroundAppearance,
display_link: Option<DisplayLink>,
renderer: renderer::Renderer,
request_frame_callback: Option<Box<dyn FnMut(RequestFrameOptions)>>,
@@ -706,6 +707,7 @@ impl MacWindow {
native_window,
native_view: NonNull::new_unchecked(native_view),
blurred_view: None,
+ background_appearance: WindowBackgroundAppearance::Opaque,
display_link: None,
renderer: renderer::new_renderer(
renderer_context,
@@ -1304,6 +1306,7 @@ impl PlatformWindow for MacWindow {
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
let mut this = self.0.as_ref().lock();
+ this.background_appearance = background_appearance;
let opaque = background_appearance == WindowBackgroundAppearance::Opaque;
this.renderer.update_transparency(!opaque);
@@ -1358,6 +1361,14 @@ impl PlatformWindow for MacWindow {
}
}
+ fn background_appearance(&self) -> WindowBackgroundAppearance {
+ self.0.as_ref().lock().background_appearance
+ }
+
+ fn is_subpixel_rendering_supported(&self) -> bool {
+ false
+ }
+
fn set_edited(&mut self, edited: bool) {
unsafe {
let window = self.0.lock().native_window;
@@ -199,6 +199,14 @@ impl PlatformWindow for TestWindow {
false
}
+ fn background_appearance(&self) -> WindowBackgroundAppearance {
+ WindowBackgroundAppearance::Opaque
+ }
+
+ fn is_subpixel_rendering_supported(&self) -> bool {
+ false
+ }
+
fn set_title(&mut self, title: &str) {
self.0.lock().title = Some(title.to_owned());
}
@@ -17,12 +17,22 @@ float enhance_contrast(float alpha, float k) {
return alpha * (k + 1.0f) / (alpha * k + 1.0f);
}
+float3 enhance_contrast3(float3 alpha, float k) {
+ return alpha * (k + 1.0f) / (alpha * k + 1.0f);
+}
+
float apply_alpha_correction(float a, float b, float4 g) {
float brightness_adjustment = g.x * b + g.y;
float correction = brightness_adjustment * a + (g.z * b + g.w);
return a + a * (1.0f - a) * correction;
}
+float3 apply_alpha_correction3(float3 a, float3 b, float4 g) {
+ float3 brightness_adjustment = g.x * b + g.y;
+ float3 correction = brightness_adjustment * a + (g.z * b + g.w);
+ return a + a * (1.0f - a) * correction;
+}
+
float apply_contrast_and_gamma_correction(float sample, float3 color, float enhanced_contrast_factor, float4 gamma_ratios) {
float enhanced_contrast = light_on_dark_contrast(enhanced_contrast_factor, color);
float brightness = color_brightness(color);
@@ -30,3 +40,10 @@ float apply_contrast_and_gamma_correction(float sample, float3 color, float enha
float contrasted = enhance_contrast(sample, enhanced_contrast);
return apply_alpha_correction(contrasted, brightness, gamma_ratios);
}
+
+float3 apply_contrast_and_gamma_correction3(float3 sample, float3 color, float enhanced_contrast_factor, float4 gamma_ratios) {
+ float enhanced_contrast = light_on_dark_contrast(enhanced_contrast_factor, color);
+
+ float3 contrasted = enhance_contrast3(sample, enhanced_contrast);
+ return apply_alpha_correction3(contrasted, color, gamma_ratios);
+}
@@ -1,4 +1,8 @@
-use std::{borrow::Cow, sync::Arc};
+use std::{
+ borrow::Cow,
+ ffi::{c_uint, c_void},
+ sync::Arc,
+};
use ::util::ResultExt;
use anyhow::{Context, Result};
@@ -60,6 +64,7 @@ struct DirectWriteState {
fonts: Vec<FontInfo>,
font_selections: HashMap<Font, FontId>,
font_id_by_identifier: HashMap<FontIdentifier, FontId>,
+ system_subpixel_rendering: bool,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
@@ -199,6 +204,7 @@ impl DirectWriteTextSystem {
.CreateFontCollectionFromFontSet(&custom_font_set)?
};
let system_ui_font_name = get_system_ui_font_name();
+ let system_subpixel_rendering = get_system_subpixel_rendering();
Ok(Self(RwLock::new(DirectWriteState {
components,
@@ -208,6 +214,7 @@ impl DirectWriteTextSystem {
fonts: Vec::new(),
font_selections: HashMap::default(),
font_id_by_identifier: HashMap::default(),
+ system_subpixel_rendering,
})))
}
@@ -280,6 +287,18 @@ impl PlatformTextSystem for DirectWriteTextSystem {
..Default::default()
})
}
+
+ fn recommended_rendering_mode(
+ &self,
+ _font_id: FontId,
+ _font_size: Pixels,
+ ) -> TextRenderingMode {
+ if self.0.read().system_subpixel_rendering {
+ TextRenderingMode::Subpixel
+ } else {
+ TextRenderingMode::Grayscale
+ }
+ }
}
impl DirectWriteState {
@@ -759,6 +778,12 @@ impl DirectWriteState {
m => m,
};
+ let antialias_mode = if params.subpixel_rendering {
+ DWRITE_TEXT_ANTIALIAS_MODE_CLEARTYPE
+ } else {
+ DWRITE_TEXT_ANTIALIAS_MODE_GRAYSCALE
+ };
+
let glyph_analysis = unsafe {
self.components.factory.CreateGlyphRunAnalysis(
&glyph_run,
@@ -766,7 +791,7 @@ impl DirectWriteState {
rendering_mode,
DWRITE_MEASURING_MODE_NATURAL,
grid_fit_mode,
- DWRITE_TEXT_ANTIALIAS_MODE_GRAYSCALE,
+ antialias_mode,
baseline_origin_x,
baseline_origin_y,
)
@@ -777,7 +802,13 @@ impl DirectWriteState {
fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
let glyph_analysis = self.create_glyph_run_analysis(params)?;
- let bounds = unsafe { glyph_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_ALIASED_1x1)? };
+ let texture_type = if params.subpixel_rendering {
+ DWRITE_TEXTURE_CLEARTYPE_3x1
+ } else {
+ DWRITE_TEXTURE_ALIASED_1x1
+ };
+
+ let bounds = unsafe { glyph_analysis.GetAlphaTextureBounds(texture_type)? };
if bounds.right < bounds.left {
Ok(Bounds {
@@ -839,23 +870,64 @@ impl DirectWriteState {
params: &RenderGlyphParams,
glyph_bounds: Bounds<DevicePixels>,
) -> Result<Vec<u8>> {
- let mut bitmap_data =
- vec![0u8; glyph_bounds.size.width.0 as usize * glyph_bounds.size.height.0 as usize];
+ if !params.subpixel_rendering {
+ let mut bitmap_data =
+ vec![0u8; glyph_bounds.size.width.0 as usize * glyph_bounds.size.height.0 as usize];
+
+ let glyph_analysis = self.create_glyph_run_analysis(params)?;
+ unsafe {
+ glyph_analysis.CreateAlphaTexture(
+ DWRITE_TEXTURE_ALIASED_1x1,
+ &RECT {
+ left: glyph_bounds.origin.x.0,
+ top: glyph_bounds.origin.y.0,
+ right: glyph_bounds.size.width.0 + glyph_bounds.origin.x.0,
+ bottom: glyph_bounds.size.height.0 + glyph_bounds.origin.y.0,
+ },
+ &mut bitmap_data,
+ )?;
+ }
+
+ return Ok(bitmap_data);
+ }
+
+ let width = glyph_bounds.size.width.0 as usize;
+ let height = glyph_bounds.size.height.0 as usize;
+ let pixel_count = width * height;
+
+ let mut bitmap_data = vec![0u8; pixel_count * 4];
let glyph_analysis = self.create_glyph_run_analysis(params)?;
unsafe {
glyph_analysis.CreateAlphaTexture(
- DWRITE_TEXTURE_ALIASED_1x1,
+ DWRITE_TEXTURE_CLEARTYPE_3x1,
&RECT {
left: glyph_bounds.origin.x.0,
top: glyph_bounds.origin.y.0,
right: glyph_bounds.size.width.0 + glyph_bounds.origin.x.0,
bottom: glyph_bounds.size.height.0 + glyph_bounds.origin.y.0,
},
- &mut bitmap_data,
+ &mut bitmap_data[..pixel_count * 3],
)?;
}
+ // The output buffer expects RGBA data, so pad the alpha channel with zeros.
+ for pixel_ix in (0..pixel_count).rev() {
+ let src = pixel_ix * 3;
+ let dst = pixel_ix * 4;
+ (
+ bitmap_data[dst],
+ bitmap_data[dst + 1],
+ bitmap_data[dst + 2],
+ bitmap_data[dst + 3],
+ ) = (
+ bitmap_data[src],
+ bitmap_data[src + 1],
+ bitmap_data[src + 2],
+ 0,
+ );
+ }
+
Ok(bitmap_data)
}
@@ -1076,6 +1148,7 @@ impl DirectWriteState {
let crate::FontInfo {
gamma_ratios,
grayscale_enhanced_contrast,
+ ..
} = DirectXRenderer::get_font_info();
for layer in glyph_layers {
@@ -1820,6 +1893,23 @@ fn get_name(string: IDWriteLocalizedStrings, locale: &str) -> Result<String> {
Ok(String::from_utf16_lossy(&name_vec[..name_length]))
}
+fn get_system_subpixel_rendering() -> bool {
+ let mut value = c_uint::default();
+ let result = unsafe {
+ SystemParametersInfoW(
+ SPI_GETFONTSMOOTHINGTYPE,
+ 0,
+ Some((&mut value) as *mut c_uint as *mut c_void),
+ SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS::default(),
+ )
+ };
+ if result.log_err().is_some() {
+ value == FE_FONTSMOOTHINGCLEARTYPE
+ } else {
+ true
+ }
+}
+
fn get_system_ui_font_name() -> SharedString {
unsafe {
let mut info: LOGFONTW = std::mem::zeroed();
@@ -21,6 +21,7 @@ struct DirectXAtlasState {
device_context: ID3D11DeviceContext,
monochrome_textures: AtlasTextureList<DirectXAtlasTexture>,
polychrome_textures: AtlasTextureList<DirectXAtlasTexture>,
+ subpixel_textures: AtlasTextureList<DirectXAtlasTexture>,
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
}
@@ -40,6 +41,7 @@ impl DirectXAtlas {
device_context: device_context.clone(),
monochrome_textures: Default::default(),
polychrome_textures: Default::default(),
+ subpixel_textures: Default::default(),
tiles_by_key: Default::default(),
}))
}
@@ -63,6 +65,7 @@ impl DirectXAtlas {
lock.device_context = device_context.clone();
lock.monochrome_textures = AtlasTextureList::default();
lock.polychrome_textures = AtlasTextureList::default();
+ lock.subpixel_textures = AtlasTextureList::default();
lock.tiles_by_key.clear();
}
}
@@ -102,6 +105,7 @@ impl PlatformAtlas for DirectXAtlas {
let textures = match id.kind {
AtlasTextureKind::Monochrome => &mut lock.monochrome_textures,
AtlasTextureKind::Polychrome => &mut lock.polychrome_textures,
+ AtlasTextureKind::Subpixel => &mut lock.subpixel_textures,
};
let Some(texture_slot) = textures.textures.get_mut(id.index as usize) else {
@@ -130,6 +134,7 @@ impl DirectXAtlasState {
let textures = match texture_kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
+ AtlasTextureKind::Subpixel => &mut self.subpixel_textures,
};
if let Some(tile) = textures
@@ -175,6 +180,11 @@ impl DirectXAtlasState {
bind_flag = D3D11_BIND_SHADER_RESOURCE;
bytes_per_pixel = 4;
}
+ AtlasTextureKind::Subpixel => {
+ pixel_format = DXGI_FORMAT_R8G8B8A8_UNORM;
+ bind_flag = D3D11_BIND_SHADER_RESOURCE;
+ bytes_per_pixel = 4;
+ }
}
let texture_desc = D3D11_TEXTURE2D_DESC {
Width: size.width.0 as u32,
@@ -204,6 +214,7 @@ impl DirectXAtlasState {
let texture_list = match kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
+ AtlasTextureKind::Subpixel => &mut self.subpixel_textures,
};
let index = texture_list.free_list.pop();
let view = unsafe {
@@ -241,6 +252,9 @@ impl DirectXAtlasState {
crate::AtlasTextureKind::Polychrome => &self.polychrome_textures[id.index as usize]
.as_ref()
.unwrap(),
+ crate::AtlasTextureKind::Subpixel => {
+ &self.subpixel_textures[id.index as usize].as_ref().unwrap()
+ }
}
}
}
@@ -34,6 +34,7 @@ const PATH_MULTISAMPLE_COUNT: u32 = 4;
pub(crate) struct FontInfo {
pub gamma_ratios: [f32; 4],
pub grayscale_enhanced_contrast: f32,
+ pub subpixel_enhanced_contrast: f32,
}
pub(crate) struct DirectXRenderer {
@@ -89,6 +90,7 @@ struct DirectXRenderPipelines {
path_sprite_pipeline: PipelineState<PathSprite>,
underline_pipeline: PipelineState<Underline>,
mono_sprites: PipelineState<MonochromeSprite>,
+ subpixel_sprites: PipelineState<SubpixelSprite>,
poly_sprites: PipelineState<PolychromeSprite>,
}
@@ -181,7 +183,7 @@ impl DirectXRenderer {
self.atlas.clone()
}
- fn pre_draw(&self) -> Result<()> {
+ fn pre_draw(&self, clear_color: &[f32; 4]) -> Result<()> {
let resources = self.resources.as_ref().expect("resources missing");
let device_context = &self
.devices
@@ -195,7 +197,7 @@ impl DirectXRenderer {
gamma_ratios: self.font_info.gamma_ratios,
viewport_size: [resources.viewport.Width, resources.viewport.Height],
grayscale_enhanced_contrast: self.font_info.grayscale_enhanced_contrast,
- _pad: 0,
+ subpixel_enhanced_contrast: self.font_info.subpixel_enhanced_contrast,
}],
)?;
unsafe {
@@ -204,7 +206,7 @@ impl DirectXRenderer {
.render_target_view
.as_ref()
.context("missing render target view")?,
- &[0.0; 4],
+ clear_color,
);
device_context
.OMSetRenderTargets(Some(slice::from_ref(&resources.render_target_view)), None);
@@ -299,13 +301,20 @@ impl DirectXRenderer {
Ok(())
}
- pub(crate) fn draw(&mut self, scene: &Scene) -> Result<()> {
+ pub(crate) fn draw(
+ &mut self,
+ scene: &Scene,
+ background_appearance: WindowBackgroundAppearance,
+ ) -> Result<()> {
if self.skip_draws {
// skip drawing this frame, we just recovered from a device lost event
// and so likely do not have the textures anymore that are required for drawing
return Ok(());
}
- self.pre_draw()?;
+ self.pre_draw(&match background_appearance {
+ WindowBackgroundAppearance::Opaque => [1.0f32; 4],
+ _ => [0.0f32; 4],
+ })?;
for batch in scene.batches() {
match batch {
PrimitiveBatch::Shadows(shadows) => self.draw_shadows(shadows),
@@ -319,6 +328,10 @@ impl DirectXRenderer {
texture_id,
sprites,
} => self.draw_monochrome_sprites(texture_id, sprites),
+ PrimitiveBatch::SubpixelSprites {
+ texture_id,
+ sprites,
+ } => self.draw_subpixel_sprites(texture_id, sprites),
PrimitiveBatch::PolychromeSprites {
texture_id,
sprites,
@@ -327,12 +340,13 @@ impl DirectXRenderer {
}
.context(format!(
"scene too large:\
- {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces",
+ {} paths, {} shadows, {} quads, {} underlines, {} mono, {} subpixel, {} poly, {} surfaces",
scene.paths.len(),
scene.shadows.len(),
scene.quads.len(),
scene.underlines.len(),
scene.monochrome_sprites.len(),
+ scene.subpixel_sprites.len(),
scene.polychrome_sprites.len(),
scene.surfaces.len(),
))?;
@@ -578,6 +592,7 @@ impl DirectXRenderer {
}
let devices = self.devices.as_ref().context("devices missing")?;
let resources = self.resources.as_ref().context("resources missing")?;
+
self.pipelines.mono_sprites.update_buffer(
&devices.device,
&devices.device_context,
@@ -594,6 +609,33 @@ impl DirectXRenderer {
)
}
+ fn draw_subpixel_sprites(
+ &mut self,
+ texture_id: AtlasTextureId,
+ sprites: &[SubpixelSprite],
+ ) -> Result<()> {
+ if sprites.is_empty() {
+ return Ok(());
+ }
+ let devices = self.devices.as_ref().context("devices missing")?;
+ let resources = self.resources.as_ref().context("resources missing")?;
+
+ self.pipelines.subpixel_sprites.update_buffer(
+ &devices.device,
+ &devices.device_context,
+ &sprites,
+ )?;
+ let texture_view = self.atlas.get_texture_view(texture_id);
+ self.pipelines.subpixel_sprites.draw_with_texture(
+ &devices.device_context,
+ &texture_view,
+ slice::from_ref(&resources.viewport),
+ slice::from_ref(&self.globals.global_params_buffer),
+ slice::from_ref(&self.globals.sampler),
+ sprites.len() as u32,
+ )
+ }
+
fn draw_polychrome_sprites(
&mut self,
texture_id: AtlasTextureId,
@@ -667,6 +709,7 @@ impl DirectXRenderer {
FontInfo {
gamma_ratios: get_gamma_correction_ratios(render_params.GetGamma()),
grayscale_enhanced_contrast: render_params.GetGrayscaleEnhancedContrast(),
+ subpixel_enhanced_contrast: render_params.GetEnhancedContrast(),
}
})
}
@@ -789,6 +832,13 @@ impl DirectXRenderPipelines {
512,
create_blend_state(device)?,
)?;
+ let subpixel_sprites = PipelineState::new(
+ device,
+ "subpixel_sprite_pipeline",
+ ShaderModule::SubpixelSprite,
+ 512,
+ create_blend_state_for_subpixel_rendering(device)?,
+ )?;
let poly_sprites = PipelineState::new(
device,
"polychrome_sprite_pipeline",
@@ -804,6 +854,7 @@ impl DirectXRenderPipelines {
path_sprite_pipeline,
underline_pipeline,
mono_sprites,
+ subpixel_sprites,
poly_sprites,
})
}
@@ -878,7 +929,7 @@ struct GlobalParams {
gamma_ratios: [f32; 4],
viewport_size: [f32; 2],
grayscale_enhanced_contrast: f32,
- _pad: u32,
+ subpixel_enhanced_contrast: f32,
}
struct PipelineState<T> {
@@ -1235,8 +1286,6 @@ fn set_rasterizer_state(device: &ID3D11Device, device_context: &ID3D11DeviceCont
// https://learn.microsoft.com/en-us/windows/win32/api/d3d11/ns-d3d11-d3d11_blend_desc
#[inline]
fn create_blend_state(device: &ID3D11Device) -> Result<ID3D11BlendState> {
- // If the feature level is set to greater than D3D_FEATURE_LEVEL_9_3, the display
- // device performs the blend in linear space, which is ideal.
let mut desc = D3D11_BLEND_DESC::default();
desc.RenderTarget[0].BlendEnable = true.into();
desc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
@@ -1253,6 +1302,27 @@ fn create_blend_state(device: &ID3D11Device) -> Result<ID3D11BlendState> {
}
}
+#[inline]
+fn create_blend_state_for_subpixel_rendering(device: &ID3D11Device) -> Result<ID3D11BlendState> {
+ let mut desc = D3D11_BLEND_DESC::default();
+ desc.RenderTarget[0].BlendEnable = true.into();
+ desc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
+ desc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;
+ desc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC1_COLOR;
+ desc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC1_COLOR;
+ // It does not make sense to draw transparent subpixel-rendered text, since it cannot be meaningfully alpha-blended onto anything else.
+ desc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
+ desc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ZERO;
+ desc.RenderTarget[0].RenderTargetWriteMask =
+ D3D11_COLOR_WRITE_ENABLE_ALL.0 as u8 & !D3D11_COLOR_WRITE_ENABLE_ALPHA.0 as u8;
+
+ unsafe {
+ let mut state = None;
+ device.CreateBlendState(&desc, Some(&mut state))?;
+ Ok(state.unwrap())
+ }
+}
+
#[inline]
fn create_blend_state_for_path_rasterization(device: &ID3D11Device) -> Result<ID3D11BlendState> {
// If the feature level is set to greater than D3D_FEATURE_LEVEL_9_3, the display
@@ -1410,6 +1480,7 @@ pub(crate) mod shader_resources {
PathRasterization,
PathSprite,
MonochromeSprite,
+ SubpixelSprite,
PolychromeSprite,
EmojiRasterization,
}
@@ -1477,6 +1548,10 @@ pub(crate) mod shader_resources {
ShaderTarget::Vertex => MONOCHROME_SPRITE_VERTEX_BYTES,
ShaderTarget::Fragment => MONOCHROME_SPRITE_FRAGMENT_BYTES,
},
+ ShaderModule::SubpixelSprite => match target {
+ ShaderTarget::Vertex => SUBPIXEL_SPRITE_VERTEX_BYTES,
+ ShaderTarget::Fragment => SUBPIXEL_SPRITE_FRAGMENT_BYTES,
+ },
ShaderModule::PolychromeSprite => match target {
ShaderTarget::Vertex => POLYCHROME_SPRITE_VERTEX_BYTES,
ShaderTarget::Fragment => POLYCHROME_SPRITE_FRAGMENT_BYTES,
@@ -1561,7 +1636,7 @@ pub(crate) mod shader_resources {
#[cfg(debug_assertions)]
impl ShaderModule {
- pub fn as_str(&self) -> &str {
+ pub fn as_str(self) -> &'static str {
match self {
ShaderModule::Quad => "quad",
ShaderModule::Shadow => "shadow",
@@ -1569,6 +1644,7 @@ pub(crate) mod shader_resources {
ShaderModule::PathRasterization => "path_rasterization",
ShaderModule::PathSprite => "path_sprite",
ShaderModule::MonochromeSprite => "monochrome_sprite",
+ ShaderModule::SubpixelSprite => "subpixel_sprite",
ShaderModule::PolychromeSprite => "polychrome_sprite",
ShaderModule::EmojiRasterization => "emoji_rasterization",
}
@@ -4,12 +4,17 @@ cbuffer GlobalParams: register(b0) {
float4 gamma_ratios;
float2 global_viewport_size;
float grayscale_enhanced_contrast;
- uint _pad;
+ float subpixel_enhanced_contrast;
};
Texture2D<float4> t_sprite: register(t0);
SamplerState s_sprite: register(s0);
+struct SubpixelSpriteFragmentOutput {
+ float4 foreground : SV_Target0;
+ float4 alpha : SV_Target1;
+};
+
struct Bounds {
float2 origin;
float2 size;
@@ -1119,6 +1124,20 @@ float4 monochrome_sprite_fragment(MonochromeSpriteFragmentInput input): SV_Targe
return float4(input.color.rgb, input.color.a * alpha_corrected);
}
+MonochromeSpriteVertexOutput subpixel_sprite_vertex(uint vertex_id: SV_VertexID, uint sprite_id: SV_InstanceID) {
+ return monochrome_sprite_vertex(vertex_id, sprite_id);
+}
+
+SubpixelSpriteFragmentOutput subpixel_sprite_fragment(MonochromeSpriteFragmentInput input) {
+ float3 sample = t_sprite.Sample(s_sprite, input.tile_position).rgb;
+ float3 alpha_corrected = apply_contrast_and_gamma_correction3(sample, input.color.rgb, subpixel_enhanced_contrast, gamma_ratios);
+
+ SubpixelSpriteFragmentOutput output;
+ output.foreground = float4(input.color.rgb, 1.0f);
+ output.alpha = float4(input.color.a * alpha_corrected, 1.0f);
+ return output;
+}
+
/*
**
** Polychrome sprites
@@ -7,8 +7,8 @@ use ::util::ResultExt;
use windows::Win32::UI::{
Shell::{ABM_GETSTATE, ABM_GETTASKBARPOS, ABS_AUTOHIDE, APPBARDATA, SHAppBarMessage},
WindowsAndMessaging::{
- SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES, SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS,
- SystemParametersInfoW,
+ SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES, SPI_SETWORKAREA,
+ SYSTEM_PARAMETERS_INFO_ACTION, SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, SystemParametersInfoW,
},
};
@@ -39,18 +39,16 @@ impl WindowsSystemSettings {
settings
}
- fn init(&self, display: WindowsDisplay) {
+ fn init(&mut self, display: WindowsDisplay) {
self.mouse_wheel_settings.update();
self.auto_hide_taskbar_position
.set(AutoHideTaskbarPosition::new(display).log_err().flatten());
}
pub(crate) fn update(&self, display: WindowsDisplay, wparam: usize) {
- match wparam {
- // SPI_SETWORKAREA
- 47 => self.update_taskbar_position(display),
- // SPI_GETWHEELSCROLLLINES, SPI_GETWHEELSCROLLCHARS
- 104 | 108 => self.update_mouse_wheel_settings(),
+ match SYSTEM_PARAMETERS_INFO_ACTION(wparam as u32) {
+ SPI_SETWORKAREA => self.update_taskbar_position(display),
+ SPI_GETWHEELSCROLLLINES | SPI_GETWHEELSCROLLCHARS => self.update_mouse_wheel_settings(),
_ => {}
}
}
@@ -45,6 +45,7 @@ pub struct WindowsWindowState {
pub fullscreen_restore_bounds: Cell<Bounds<Pixels>>,
pub border_offset: WindowBorderOffset,
pub appearance: Cell<WindowAppearance>,
+ pub background_appearance: Cell<WindowBackgroundAppearance>,
pub scale_factor: Cell<f32>,
pub restore_from_minimized: Cell<Option<Box<dyn FnMut(RequestFrameOptions)>>>,
@@ -135,6 +136,7 @@ impl WindowsWindowState {
fullscreen_restore_bounds: Cell::new(fullscreen_restore_bounds),
border_offset,
appearance: Cell::new(appearance),
+ background_appearance: Cell::new(WindowBackgroundAppearance::Opaque),
scale_factor: Cell::new(scale_factor),
restore_from_minimized: Cell::new(restore_from_minimized),
min_size,
@@ -788,6 +790,14 @@ impl PlatformWindow for WindowsWindow {
self.state.hovered.get()
}
+ fn background_appearance(&self) -> WindowBackgroundAppearance {
+ self.state.background_appearance.get()
+ }
+
+ fn is_subpixel_rendering_supported(&self) -> bool {
+ true
+ }
+
fn set_title(&mut self, title: &str) {
unsafe { SetWindowTextW(self.0.hwnd, &HSTRING::from(title)) }
.inspect_err(|e| log::error!("Set title failed: {e}"))
@@ -795,6 +805,7 @@ impl PlatformWindow for WindowsWindow {
}
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
+ self.state.background_appearance.set(background_appearance);
let hwnd = self.0.hwnd;
// using Dwm APIs for Mica and MicaAlt backdrops.
@@ -905,7 +916,11 @@ impl PlatformWindow for WindowsWindow {
}
fn draw(&self, scene: &Scene) {
- self.state.renderer.borrow_mut().draw(scene).log_err();
+ self.state
+ .renderer
+ .borrow_mut()
+ .draw(scene, self.state.background_appearance.get())
+ .log_err();
}
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
@@ -30,6 +30,7 @@ pub(crate) struct Scene {
pub(crate) paths: Vec<Path<ScaledPixels>>,
pub(crate) underlines: Vec<Underline>,
pub(crate) monochrome_sprites: Vec<MonochromeSprite>,
+ pub(crate) subpixel_sprites: Vec<SubpixelSprite>,
pub(crate) polychrome_sprites: Vec<PolychromeSprite>,
pub(crate) surfaces: Vec<PaintSurface>,
}
@@ -44,6 +45,7 @@ impl Scene {
self.quads.clear();
self.underlines.clear();
self.monochrome_sprites.clear();
+ self.subpixel_sprites.clear();
self.polychrome_sprites.clear();
self.surfaces.clear();
}
@@ -101,6 +103,10 @@ impl Scene {
sprite.order = order;
self.monochrome_sprites.push(sprite.clone());
}
+ Primitive::SubpixelSprite(sprite) => {
+ sprite.order = order;
+ self.subpixel_sprites.push(sprite.clone());
+ }
Primitive::PolychromeSprite(sprite) => {
sprite.order = order;
self.polychrome_sprites.push(sprite.clone());
@@ -131,6 +137,8 @@ impl Scene {
self.underlines.sort_by_key(|underline| underline.order);
self.monochrome_sprites
.sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id));
+ self.subpixel_sprites
+ .sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id));
self.polychrome_sprites
.sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id));
self.surfaces.sort_by_key(|surface| surface.order);
@@ -160,6 +168,9 @@ impl Scene {
monochrome_sprites: &self.monochrome_sprites,
monochrome_sprites_start: 0,
monochrome_sprites_iter: self.monochrome_sprites.iter().peekable(),
+ subpixel_sprites: &self.subpixel_sprites,
+ subpixel_sprites_start: 0,
+ subpixel_sprites_iter: self.subpixel_sprites.iter().peekable(),
polychrome_sprites: &self.polychrome_sprites,
polychrome_sprites_start: 0,
polychrome_sprites_iter: self.polychrome_sprites.iter().peekable(),
@@ -185,6 +196,7 @@ pub(crate) enum PrimitiveKind {
Path,
Underline,
MonochromeSprite,
+ SubpixelSprite,
PolychromeSprite,
Surface,
}
@@ -202,6 +214,7 @@ pub(crate) enum Primitive {
Path(Path<ScaledPixels>),
Underline(Underline),
MonochromeSprite(MonochromeSprite),
+ SubpixelSprite(SubpixelSprite),
PolychromeSprite(PolychromeSprite),
Surface(PaintSurface),
}
@@ -214,6 +227,7 @@ impl Primitive {
Primitive::Path(path) => &path.bounds,
Primitive::Underline(underline) => &underline.bounds,
Primitive::MonochromeSprite(sprite) => &sprite.bounds,
+ Primitive::SubpixelSprite(sprite) => &sprite.bounds,
Primitive::PolychromeSprite(sprite) => &sprite.bounds,
Primitive::Surface(surface) => &surface.bounds,
}
@@ -226,6 +240,7 @@ impl Primitive {
Primitive::Path(path) => &path.content_mask,
Primitive::Underline(underline) => &underline.content_mask,
Primitive::MonochromeSprite(sprite) => &sprite.content_mask,
+ Primitive::SubpixelSprite(sprite) => &sprite.content_mask,
Primitive::PolychromeSprite(sprite) => &sprite.content_mask,
Primitive::Surface(surface) => &surface.content_mask,
}
@@ -255,6 +270,9 @@ struct BatchIterator<'a> {
monochrome_sprites: &'a [MonochromeSprite],
monochrome_sprites_start: usize,
monochrome_sprites_iter: Peekable<slice::Iter<'a, MonochromeSprite>>,
+ subpixel_sprites: &'a [SubpixelSprite],
+ subpixel_sprites_start: usize,
+ subpixel_sprites_iter: Peekable<slice::Iter<'a, SubpixelSprite>>,
polychrome_sprites: &'a [PolychromeSprite],
polychrome_sprites_start: usize,
polychrome_sprites_iter: Peekable<slice::Iter<'a, PolychromeSprite>>,
@@ -282,6 +300,10 @@ impl<'a> Iterator for BatchIterator<'a> {
self.monochrome_sprites_iter.peek().map(|s| s.order),
PrimitiveKind::MonochromeSprite,
),
+ (
+ self.subpixel_sprites_iter.peek().map(|s| s.order),
+ PrimitiveKind::SubpixelSprite,
+ ),
(
self.polychrome_sprites_iter.peek().map(|s| s.order),
PrimitiveKind::PolychromeSprite,
@@ -383,6 +405,27 @@ impl<'a> Iterator for BatchIterator<'a> {
sprites: &self.monochrome_sprites[sprites_start..sprites_end],
})
}
+ PrimitiveKind::SubpixelSprite => {
+ let texture_id = self.subpixel_sprites_iter.peek().unwrap().tile.texture_id;
+ let sprites_start = self.subpixel_sprites_start;
+ let mut sprites_end = sprites_start + 1;
+ self.subpixel_sprites_iter.next();
+ while self
+ .subpixel_sprites_iter
+ .next_if(|sprite| {
+ (sprite.order, batch_kind) < max_order_and_kind
+ && sprite.tile.texture_id == texture_id
+ })
+ .is_some()
+ {
+ sprites_end += 1;
+ }
+ self.subpixel_sprites_start = sprites_end;
+ Some(PrimitiveBatch::SubpixelSprites {
+ texture_id,
+ sprites: &self.subpixel_sprites[sprites_start..sprites_end],
+ })
+ }
PrimitiveKind::PolychromeSprite => {
let texture_id = self.polychrome_sprites_iter.peek().unwrap().tile.texture_id;
let sprites_start = self.polychrome_sprites_start;
@@ -441,6 +484,11 @@ pub(crate) enum PrimitiveBatch<'a> {
texture_id: AtlasTextureId,
sprites: &'a [MonochromeSprite],
},
+ #[cfg_attr(target_os = "macos", allow(dead_code))]
+ SubpixelSprites {
+ texture_id: AtlasTextureId,
+ sprites: &'a [SubpixelSprite],
+ },
PolychromeSprites {
texture_id: AtlasTextureId,
sprites: &'a [PolychromeSprite],
@@ -634,6 +682,24 @@ impl From<MonochromeSprite> for Primitive {
}
}
+#[derive(Clone, Debug)]
+#[repr(C)]
+pub(crate) struct SubpixelSprite {
+ pub order: DrawOrder,
+ pub pad: u32, // align to 8 bytes
+ pub bounds: Bounds<ScaledPixels>,
+ pub content_mask: ContentMask<ScaledPixels>,
+ pub color: Hsla,
+ pub tile: AtlasTile,
+ pub transformation: TransformationMatrix,
+}
+
+impl From<SubpixelSprite> for Primitive {
+ fn from(sprite: SubpixelSprite) -> Self {
+ Primitive::SubpixelSprite(sprite)
+ }
+}
+
#[derive(Clone, Debug)]
#[repr(C)]
pub(crate) struct PolychromeSprite {
@@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize};
use crate::{
Bounds, DevicePixels, Hsla, Pixels, PlatformTextSystem, Point, Result, SharedString, Size,
- StrikethroughStyle, UnderlineStyle, px,
+ StrikethroughStyle, TextRenderingMode, UnderlineStyle, px,
};
use anyhow::{Context as _, anyhow};
use collections::FxHashMap;
@@ -326,6 +326,17 @@ impl TextSystem {
self.platform_text_system
.rasterize_glyph(params, raster_bounds)
}
+
+ /// 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(
+ &self,
+ font_id: FontId,
+ font_size: Pixels,
+ ) -> TextRenderingMode {
+ self.platform_text_system
+ .recommended_rendering_mode(font_id, font_size)
+ }
}
/// The GPUI text layout subsystem.
@@ -775,6 +786,7 @@ pub(crate) struct RenderGlyphParams {
pub(crate) subpixel_variant: Point<u8>,
pub(crate) scale_factor: f32,
pub(crate) is_emoji: bool,
+ pub(crate) subpixel_rendering: bool,
}
impl Eq for RenderGlyphParams {}
@@ -787,6 +799,7 @@ impl Hash for RenderGlyphParams {
self.subpixel_variant.hash(state);
self.scale_factor.to_bits().hash(state);
self.is_emoji.hash(state);
+ self.subpixel_rendering.hash(state);
}
}
@@ -12,12 +12,12 @@ use crate::{
PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, Priority, PromptButton,
PromptLevel, Quad, Render, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams,
Replay, ResizeEdge, SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS_X, SUBPIXEL_VARIANTS_Y,
- ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style, SubscriberSet,
- Subscription, SystemWindowTab, SystemWindowTabController, TabStopMap, TaffyLayoutEngine, Task,
- TextStyle, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle,
- WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations,
- WindowOptions, WindowParams, WindowTextSystem, point, prelude::*, px, rems, size,
- transparent_black,
+ ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style, SubpixelSprite,
+ SubscriberSet, Subscription, SystemWindowTab, SystemWindowTabController, TabStopMap,
+ TaffyLayoutEngine, Task, TextRenderingMode, TextStyle, TextStyleRefinement,
+ TransformationMatrix, Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance,
+ WindowBounds, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem,
+ point, prelude::*, px, rems, size, transparent_black,
};
use anyhow::{Context as _, Result, anyhow};
use collections::{FxHashMap, FxHashSet};
@@ -838,6 +838,7 @@ pub struct Window {
display_id: Option<DisplayId>,
sprite_atlas: Arc<dyn PlatformAtlas>,
text_system: Arc<WindowTextSystem>,
+ text_rendering_mode: Rc<Cell<TextRenderingMode>>,
rem_size: Pixels,
/// The stack of override values for the window's rem size.
///
@@ -1312,6 +1313,7 @@ impl Window {
display_id,
sprite_atlas,
text_system,
+ text_rendering_mode: cx.text_rendering_mode.clone(),
rem_size: px(16.),
rem_size_override_stack: SmallVec::new(),
viewport_size: content_size,
@@ -3130,6 +3132,7 @@ impl Window {
x: (glyph_origin.x.0.fract() * SUBPIXEL_VARIANTS_X as f32).floor() as u8,
y: (glyph_origin.y.0.fract() * SUBPIXEL_VARIANTS_Y as f32).floor() as u8,
};
+ let subpixel_rendering = self.should_use_subpixel_rendering(font_id, font_size);
let params = RenderGlyphParams {
font_id,
glyph_id,
@@ -3137,6 +3140,7 @@ impl Window {
subpixel_variant,
scale_factor,
is_emoji: false,
+ subpixel_rendering,
};
let raster_bounds = self.text_system().raster_bounds(¶ms)?;
@@ -3153,19 +3157,51 @@ impl Window {
size: tile.bounds.size.map(Into::into),
};
let content_mask = self.content_mask().scale(scale_factor);
- self.next_frame.scene.insert_primitive(MonochromeSprite {
- order: 0,
- pad: 0,
- bounds,
- content_mask,
- color: color.opacity(element_opacity),
- tile,
- transformation: TransformationMatrix::unit(),
- });
+
+ if subpixel_rendering {
+ self.next_frame.scene.insert_primitive(SubpixelSprite {
+ order: 0,
+ pad: 0,
+ bounds,
+ content_mask,
+ color: color.opacity(element_opacity),
+ tile,
+ transformation: TransformationMatrix::unit(),
+ });
+ } else {
+ self.next_frame.scene.insert_primitive(MonochromeSprite {
+ order: 0,
+ pad: 0,
+ bounds,
+ content_mask,
+ color: color.opacity(element_opacity),
+ tile,
+ transformation: TransformationMatrix::unit(),
+ });
+ }
}
Ok(())
}
+ fn should_use_subpixel_rendering(&self, font_id: FontId, font_size: Pixels) -> bool {
+ if self.platform_window.background_appearance() != WindowBackgroundAppearance::Opaque {
+ return false;
+ }
+
+ if !self.platform_window.is_subpixel_rendering_supported() {
+ return false;
+ }
+
+ let mode = match self.text_rendering_mode.get() {
+ TextRenderingMode::PlatformDefault => self
+ .text_system()
+ .recommended_rendering_mode(font_id, font_size),
+ mode => mode,
+ };
+
+ mode == TextRenderingMode::Subpixel
+ }
+
/// Paints an emoji glyph into the scene for the next frame at the current z-index.
///
/// The y component of the origin is the baseline of the glyph.
@@ -3193,6 +3229,7 @@ impl Window {
subpixel_variant: Default::default(),
scale_factor,
is_emoji: true,
+ subpixel_rendering: false,
};
let raster_bounds = self.text_system().raster_bounds(¶ms)?;
@@ -15,6 +15,10 @@ use crate::{
pub struct WorkspaceSettingsContent {
/// Active pane styling settings.
pub active_pane_modifiers: Option<ActivePaneModifiers>,
+ /// The text rendering mode to use.
+ ///
+ /// Default: platform_default
+ pub text_rendering_mode: Option<TextRenderingMode>,
/// Layout mode for the bottom dock
///
/// Default: contained
@@ -545,6 +549,31 @@ pub enum OnLastWindowClosed {
QuitApp,
}
+#[derive(
+ Copy,
+ Clone,
+ Default,
+ Serialize,
+ Deserialize,
+ JsonSchema,
+ MergeFrom,
+ PartialEq,
+ Eq,
+ Debug,
+ strum::VariantArray,
+ strum::VariantNames,
+)]
+#[serde(rename_all = "snake_case")]
+pub enum TextRenderingMode {
+ /// Use platform default behavior.
+ #[default]
+ PlatformDefault,
+ /// Use subpixel (ClearType-style) text rendering.
+ Subpixel,
+ /// Use grayscale text rendering.
+ Grayscale,
+}
+
impl OnLastWindowClosed {
pub fn is_quit_app(&self) -> bool {
match self {
@@ -817,6 +817,7 @@ impl VsCodeSettings {
fn workspace_settings_content(&self) -> WorkspaceSettingsContent {
WorkspaceSettingsContent {
active_pane_modifiers: self.active_pane_modifiers(),
+ text_rendering_mode: None,
autosave: self.read_enum("files.autoSave", |s| match s {
"off" => Some(AutosaveSetting::Off),
"afterDelay" => Some(AutosaveSetting::AfterDelay {
@@ -890,6 +890,22 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
metadata: None,
files: USER,
}),
+ SettingsPageItem::SectionHeader("Text Rendering"),
+ SettingsPageItem::SettingItem(SettingItem {
+ title: "Text Rendering Mode",
+ description: "The text rendering mode to use.",
+ field: Box::new(SettingField {
+ json_path: Some("text_rendering_mode"),
+ pick: |settings_content| {
+ settings_content.workspace.text_rendering_mode.as_ref()
+ },
+ write: |settings_content, value| {
+ settings_content.workspace.text_rendering_mode = value;
+ },
+ }),
+ metadata: None,
+ files: USER,
+ }),
SettingsPageItem::SectionHeader("Cursor"),
SettingsPageItem::SettingItem(SettingItem {
title: "Multi Cursor Modifier",
@@ -436,6 +436,7 @@ fn init_renderers(cx: &mut App) {
.add_basic_renderer::<settings::BottomDockLayout>(render_dropdown)
.add_basic_renderer::<settings::OnLastWindowClosed>(render_dropdown)
.add_basic_renderer::<settings::CloseWindowWhenNoItems>(render_dropdown)
+ .add_basic_renderer::<settings::TextRenderingMode>(render_dropdown)
.add_basic_renderer::<settings::FontFamilyName>(render_font_picker)
.add_basic_renderer::<settings::BaseKeymapContent>(render_dropdown)
.add_basic_renderer::<settings::MultiCursorModifier>(render_dropdown)
@@ -27,6 +27,7 @@ pub struct WorkspaceSettings {
pub max_tabs: Option<NonZeroUsize>,
pub when_closing_with_no_tabs: settings::CloseWindowWhenNoItems,
pub on_last_window_closed: settings::OnLastWindowClosed,
+ pub text_rendering_mode: settings::TextRenderingMode,
pub resize_all_panels_in_dock: Vec<DockPosition>,
pub close_on_file_delete: bool,
pub use_system_window_tabs: bool,
@@ -96,6 +97,7 @@ impl Settings for WorkspaceSettings {
max_tabs: workspace.max_tabs,
when_closing_with_no_tabs: workspace.when_closing_with_no_tabs.unwrap(),
on_last_window_closed: workspace.on_last_window_closed.unwrap(),
+ text_rendering_mode: workspace.text_rendering_mode.unwrap(),
resize_all_panels_in_dock: workspace
.resize_all_panels_in_dock
.clone()
@@ -678,6 +678,18 @@ fn main() {
.ok();
}
+ cx.set_text_rendering_mode(
+ match WorkspaceSettings::get_global(cx).text_rendering_mode {
+ settings::TextRenderingMode::PlatformDefault => {
+ gpui::TextRenderingMode::PlatformDefault
+ }
+ settings::TextRenderingMode::Subpixel => gpui::TextRenderingMode::Subpixel,
+ settings::TextRenderingMode::Grayscale => {
+ gpui::TextRenderingMode::Grayscale
+ }
+ },
+ );
+
let new_host = &client::ClientSettings::get_global(cx).server_url;
if &http.base_url() != new_host {
http.set_base_url(new_host);