gpui: Fix macOS font render clipped bug again (#47001)

Jason Lee created

Continue #45957 #46906 to fix font render issue again.

Release Notes:

- Fixed macOS font render clipped bug when use `.SystemUIFont`.

| .SystemUIFont | .ZedMono |
| --- | --- |
| <img width="1034" height="978" alt="image"
src="https://github.com/user-attachments/assets/96b815a1-9484-4a38-8391-a4af3db51a36"
/> | <img width="1034" height="978" alt="image"
src="https://github.com/user-attachments/assets/1aaece42-9acd-47b0-a0a0-3cb425f40301"
/> |

```bash
cargo run -p gpui --example text
```

Test in Zed with `.ZedMono` font:

<img width="815" height="844" alt="image"
src="https://github.com/user-attachments/assets/1e44d186-cee5-4f54-910a-4c4602ca010e"
/>

With `.ZedSans`:

<img width="815" height="844" alt="image"
src="https://github.com/user-attachments/assets/1e502f3e-794c-4082-9faf-5a920adc1214"
/>

`Monaco`:

<img width="815" height="844" alt="image"
src="https://github.com/user-attachments/assets/906a3fd2-715a-4f53-b6fe-4614f4c6edab"
/>

`Menlo`:

<img width="815" height="844" alt="image"
src="https://github.com/user-attachments/assets/a8f8c302-2083-477c-ae72-f6e5c7f91d00"
/>

Change summary

crates/gpui/Cargo.toml               |  2 
crates/gpui/examples/text.rs         | 64 +++++++++++++++++++++++++++++
crates/gpui_macos/src/text_system.rs | 13 +++++
3 files changed, 74 insertions(+), 5 deletions(-)

Detailed changes

crates/gpui/Cargo.toml 🔗

@@ -144,7 +144,7 @@ windows = { version = "0.61", features = ["Win32_Foundation"] }
 backtrace.workspace = true
 collections = { workspace = true, features = ["test-support"] }
 env_logger.workspace = true
-gpui_platform.workspace = true
+gpui_platform = { workspace = true, features = ["font-kit"] }
 lyon = { version = "1.0", features = ["extra"] }
 rand.workspace = true
 scheduler = { workspace = true, features = ["test-support"] }

crates/gpui/examples/text.rs 🔗

@@ -1,6 +1,7 @@
 #![cfg_attr(target_family = "wasm", no_main)]
 
 use std::{
+    borrow::Cow,
     ops::{Deref, DerefMut},
     sync::Arc,
 };
@@ -204,7 +205,7 @@ impl RenderOnce for CharacterGrid {
             "❮", "<=", "!=", "==", "--", "++", "=>", "->", "🏀", "🎊", "😍", "❤️", "👍", "👎",
         ];
 
-        let columns = 11;
+        let columns = 20;
         let rows = characters.len().div_ceil(columns);
 
         let grid_rows = (0..rows).map(|row_idx| {
@@ -238,6 +239,7 @@ impl RenderOnce for CharacterGrid {
 
 struct TextExample {
     next_id: usize,
+    font_family: SharedString,
 }
 
 impl TextExample {
@@ -245,8 +247,33 @@ impl TextExample {
         self.next_id += 1;
         self.next_id
     }
+
+    fn button(
+        text: &str,
+        cx: &mut Context<Self>,
+        on_click: impl Fn(&mut Self, &mut Context<Self>) + 'static,
+    ) -> impl IntoElement {
+        div()
+            .id(text.to_string())
+            .flex_none()
+            .child(text.to_string())
+            .bg(gpui::black())
+            .text_color(gpui::white())
+            .active(|this| this.opacity(0.8))
+            .px_3()
+            .py_1()
+            .on_click(cx.listener(move |this, _, _, cx| on_click(this, cx)))
+    }
 }
 
+const FONT_FAMILIES: [&str; 5] = [
+    ".ZedMono",
+    ".SystemUIFont",
+    "Menlo",
+    "Monaco",
+    "Courier New",
+];
+
 impl Render for TextExample {
     fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let tcx = cx.text_context();
@@ -265,7 +292,26 @@ impl Render for TextExample {
         let step_up_6 = step_up_5 * type_scale;
 
         div()
+            .font_family(self.font_family.clone())
             .size_full()
+            .child(
+                div()
+                    .bg(gpui::white())
+                    .border_b_1()
+                    .border_color(gpui::black())
+                    .p_3()
+                    .flex()
+                    .child(Self::button(&self.font_family, cx, |this, cx| {
+                        let new_family = FONT_FAMILIES
+                            .iter()
+                            .position(|f| *f == this.font_family.as_str())
+                            .map(|idx| FONT_FAMILIES[(idx + 1) % FONT_FAMILIES.len()])
+                            .unwrap_or(FONT_FAMILIES[0]);
+
+                        this.font_family = SharedString::new(new_family);
+                        cx.notify();
+                    })),
+            )
             .child(
                 div()
                     .id("text-example")
@@ -307,6 +353,15 @@ fn run_example() {
             items: vec![],
         }]);
 
+        let fonts = [include_bytes!(
+            "../../../assets/fonts/lilex/Lilex-Regular.ttf"
+        )]
+        .iter()
+        .map(|b| Cow::Borrowed(&b[..]))
+        .collect();
+
+        _ = cx.text_system().add_fonts(fonts);
+
         cx.init_colors();
         cx.set_global(GlobalTextContext(Arc::new(TextContext::default())));
 
@@ -323,7 +378,12 @@ fn run_example() {
                     ))),
                     ..Default::default()
                 },
-                |_window, cx| cx.new(|_cx| TextExample { next_id: 0 }),
+                |_window, cx| {
+                    cx.new(|_cx| TextExample {
+                        next_id: 0,
+                        font_family: ".ZedMono".into(),
+                    })
+                },
             )
             .unwrap();
 

crates/gpui_macos/src/text_system.rs 🔗

@@ -361,13 +361,22 @@ 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);
-        Ok(bounds_from_rect_i(font.raster_bounds(
+        let mut bounds: Bounds<DevicePixels> = bounds_from_rect_i(font.raster_bounds(
             params.glyph_id.0,
             params.font_size.into(),
             scale,
             HintingOptions::None,
             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)
     }
 
     fn rasterize_glyph(