From 97b429953eb616d98032e7a74e2c98451210b183 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 19 Nov 2025 19:37:22 +0200 Subject: [PATCH] gpui: Do not render ligatures between different styled text runs (#43080) An attempt to re-land https://github.com/zed-industries/zed/pull/41043 Part of https://github.com/zed-industries/zed/issues/5259 (as `>>>` forms a ligature that we need to break into differently colored tokens) Before: image and https://github.com/user-attachments/assets/ae77ba64-ca50-4b5d-9ee4-a7d46fcaeb34 After: image When certain combination of characters forms a ligature, it takes the color of the first character. Even though the runs are split already by color and other properties, the underlying font system merges the runs together. Attempts to modify color and other, unrelated to font size, parameters, did not help on macOS, hence a somewhat odd approach was taken: runs get interleaved font sizes: normal and "normal + a tiny bit more". This is the only option that helped splitting the ligatures, and seems to render fine. Release Notes: - Fixed ligatures forming between different text kinds --------- Co-authored-by: Lukas Wirth --- crates/gpui/src/platform/mac/text_system.rs | 10 +++++++++- crates/gpui/src/platform/windows/direct_write.rs | 11 ++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/crates/gpui/src/platform/mac/text_system.rs b/crates/gpui/src/platform/mac/text_system.rs index 92135a2c96e5cb4c3587f7f01225be5b1fcd8b43..3faf4e6491e6588bdb1341d5a8845171562fa8a0 100644 --- a/crates/gpui/src/platform/mac/text_system.rs +++ b/crates/gpui/src/platform/mac/text_system.rs @@ -435,6 +435,7 @@ impl MacTextSystemState { { let mut text = text; + let mut break_ligature = true; for run in font_runs { let text_run; (text_run, text) = text.split_at(run.len); @@ -444,7 +445,8 @@ impl MacTextSystemState { string.replace_str(&CFString::new(text_run), CFRange::init(utf16_start, 0)); let utf16_end = string.char_len(); - let cf_range = CFRange::init(utf16_start, utf16_end - utf16_start); + let length = utf16_end - utf16_start; + let cf_range = CFRange::init(utf16_start, length); let font = &self.fonts[run.font_id.0]; let font_metrics = font.metrics(); @@ -452,6 +454,11 @@ impl MacTextSystemState { max_ascent = max_ascent.max(font_metrics.ascent * font_scale); max_descent = max_descent.max(-font_metrics.descent * font_scale); + let font_size = if break_ligature { + px(font_size.0.next_up()) + } else { + font_size + }; unsafe { string.set_attribute( cf_range, @@ -459,6 +466,7 @@ impl MacTextSystemState { &font.native_font().clone_with_font_size(font_size.into()), ); } + break_ligature = !break_ligature; } } // Retrieve the glyphs from the shaped line, converting UTF16 offsets to UTF8 offsets. diff --git a/crates/gpui/src/platform/windows/direct_write.rs b/crates/gpui/src/platform/windows/direct_write.rs index cb22948898fd496d6820e29088a9be7c5c502341..84539633c9e9c2ba2204d8ccaa94bd4156f8ea89 100644 --- a/crates/gpui/src/platform/windows/direct_write.rs +++ b/crates/gpui/src/platform/windows/direct_write.rs @@ -608,6 +608,7 @@ impl DirectWriteState { let mut first_run = true; let mut ascent = Pixels::default(); let mut descent = Pixels::default(); + let mut break_ligatures = false; for run in font_runs { if first_run { first_run = false; @@ -616,6 +617,7 @@ impl DirectWriteState { text_layout.GetLineMetrics(Some(&mut metrics), &mut line_count as _)?; ascent = px(metrics[0].baseline); descent = px(metrics[0].height - metrics[0].baseline); + break_ligatures = !break_ligatures; continue; } let font_info = &self.fonts[run.font_id.0]; @@ -636,10 +638,17 @@ impl DirectWriteState { text_layout.SetFontCollection(collection, text_range)?; text_layout .SetFontFamilyName(&HSTRING::from(&font_info.font_family), text_range)?; - text_layout.SetFontSize(font_size.0, text_range)?; + let font_size = if break_ligatures { + font_size.0.next_up() + } else { + font_size.0 + }; + text_layout.SetFontSize(font_size, text_range)?; text_layout.SetFontStyle(font_info.font_face.GetStyle(), text_range)?; text_layout.SetFontWeight(font_info.font_face.GetWeight(), text_range)?; text_layout.SetTypography(&font_info.features, text_range)?; + + break_ligatures = !break_ligatures; } let mut runs = Vec::new();