gpui: Do not render ligatures between different styled text runs (#43080)

Kirill Bulatov and Lukas Wirth created

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:

<img width="301" height="86" alt="image"
src="https://github.com/user-attachments/assets/e710391a-b8ad-4343-8344-c86fc5cb86b6"
/>

and


https://github.com/user-attachments/assets/ae77ba64-ca50-4b5d-9ee4-a7d46fcaeb34


After:
<img width="1254" height="302" alt="image"
src="https://github.com/user-attachments/assets/7fd5dba5-d798-4153-acf2-e38a1cb712ae"
/>


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 <lukas@zed.dev>

Change summary

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(-)

Detailed changes

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.

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();