Display squiggly underlines underneath text with diagnostics

Antonio Scandurra and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

crates/editor/src/element.rs                       | 14 ++--
crates/gpui/src/fonts.rs                           | 41 ++++++++++++--
crates/gpui/src/platform/mac/renderer.rs           | 43 ++++++++-------
crates/gpui/src/platform/mac/shaders/shaders.h     |  1 
crates/gpui/src/platform/mac/shaders/shaders.metal | 30 ++++++----
crates/gpui/src/scene.rs                           | 17 ++++-
crates/gpui/src/text_layout.rs                     | 45 ++++++++-------
7 files changed, 119 insertions(+), 72 deletions(-)

Detailed changes

crates/editor/src/element.rs 🔗

@@ -8,7 +8,7 @@ use collections::{BTreeMap, HashMap};
 use gpui::{
     color::Color,
     elements::layout_highlighted_chunks,
-    fonts::HighlightStyle,
+    fonts::{HighlightStyle, Underline},
     geometry::{
         rect::RectF,
         vector::{vec2f, Vector2F},
@@ -540,12 +540,12 @@ impl EditorElement {
                 .chunks(rows.clone(), Some(&style.syntax))
                 .map(|chunk| {
                     let highlight = if let Some(severity) = chunk.diagnostic {
-                        let underline = Some(
-                            super::diagnostic_style(severity, true, style)
-                                .message
-                                .text
-                                .color,
-                        );
+                        let diagnostic_style = super::diagnostic_style(severity, true, style);
+                        let underline = Some(Underline {
+                            color: diagnostic_style.message.text.color,
+                            thickness: 1.0.into(),
+                            squiggly: true,
+                        });
                         if let Some(mut highlight) = chunk.highlight_style {
                             highlight.underline = underline;
                             Some(highlight)

crates/gpui/src/fonts.rs 🔗

@@ -10,6 +10,7 @@ pub use font_kit::{
     metrics::Metrics,
     properties::{Properties, Stretch, Style, Weight},
 };
+use ordered_float::OrderedFloat;
 use serde::{de, Deserialize};
 use serde_json::Value;
 use std::{cell::RefCell, sync::Arc};
@@ -27,14 +28,21 @@ pub struct TextStyle {
     pub font_id: FontId,
     pub font_size: f32,
     pub font_properties: Properties,
-    pub underline: Option<Color>,
+    pub underline: Option<Underline>,
 }
 
 #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
 pub struct HighlightStyle {
     pub color: Color,
     pub font_properties: Properties,
-    pub underline: Option<Color>,
+    pub underline: Option<Underline>,
+}
+
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
+pub struct Underline {
+    pub color: Color,
+    pub thickness: OrderedFloat<f32>,
+    pub squiggly: bool,
 }
 
 #[allow(non_camel_case_types)]
@@ -81,7 +89,14 @@ struct HighlightStyleJson {
 #[serde(untagged)]
 enum UnderlineStyleJson {
     Underlined(bool),
-    UnderlinedWithColor(Color),
+    UnderlinedWithProperties {
+        #[serde(default)]
+        color: Option<Color>,
+        #[serde(default)]
+        thickness: Option<f32>,
+        #[serde(default)]
+        squiggly: bool,
+    },
 }
 
 impl TextStyle {
@@ -89,7 +104,7 @@ impl TextStyle {
         font_family_name: impl Into<Arc<str>>,
         font_size: f32,
         font_properties: Properties,
-        underline: Option<Color>,
+        underline: Option<Underline>,
         color: Color,
         font_cache: &FontCache,
     ) -> anyhow::Result<Self> {
@@ -276,11 +291,23 @@ impl<'de> Deserialize<'de> for HighlightStyle {
     }
 }
 
-fn underline_from_json(json: UnderlineStyleJson, text_color: Color) -> Option<Color> {
+fn underline_from_json(json: UnderlineStyleJson, text_color: Color) -> Option<Underline> {
     match json {
         UnderlineStyleJson::Underlined(false) => None,
-        UnderlineStyleJson::Underlined(true) => Some(text_color),
-        UnderlineStyleJson::UnderlinedWithColor(color) => Some(color),
+        UnderlineStyleJson::Underlined(true) => Some(Underline {
+            color: text_color,
+            thickness: 1.0.into(),
+            squiggly: false,
+        }),
+        UnderlineStyleJson::UnderlinedWithProperties {
+            color,
+            thickness,
+            squiggly,
+        } => Some(Underline {
+            color: color.unwrap_or(text_color),
+            thickness: thickness.unwrap_or(1.).into(),
+            squiggly,
+        }),
     }
 }
 

crates/gpui/src/platform/mac/renderer.rs 🔗

@@ -6,7 +6,7 @@ use crate::{
         vector::{vec2f, vec2i, Vector2F},
     },
     platform,
-    scene::{Glyph, Icon, Image, Layer, Quad, Scene, Shadow},
+    scene::{Glyph, Icon, Image, Layer, Quad, Scene, Shadow, Underline},
 };
 use cocoa::foundation::NSUInteger;
 use metal::{MTLPixelFormat, MTLResourceOptions, NSRange};
@@ -334,23 +334,23 @@ impl Renderer {
                 drawable_size,
                 command_encoder,
             );
-            self.render_sprites(
-                layer.glyphs(),
-                layer.icons(),
+            self.render_underlines(
+                layer.underlines(),
                 scale_factor,
                 offset,
                 drawable_size,
                 command_encoder,
             );
-            self.render_images(
-                layer.images(),
+            self.render_sprites(
+                layer.glyphs(),
+                layer.icons(),
                 scale_factor,
                 offset,
                 drawable_size,
                 command_encoder,
             );
-            self.render_underlines(
-                layer.underlines(),
+            self.render_images(
+                layer.images(),
                 scale_factor,
                 offset,
                 drawable_size,
@@ -834,7 +834,7 @@ impl Renderer {
 
     fn render_underlines(
         &mut self,
-        underlines: &[Quad],
+        underlines: &[Underline],
         scale_factor: f32,
         offset: &mut usize,
         drawable_size: Vector2F,
@@ -874,19 +874,22 @@ impl Renderer {
             (self.instances.contents() as *mut u8).offset(*offset as isize)
                 as *mut shaders::GPUIUnderline
         };
-        for (ix, quad) in underlines.iter().enumerate() {
-            let bounds = quad.bounds * scale_factor;
-            let shader_quad = shaders::GPUIUnderline {
-                origin: bounds.origin().round().to_float2(),
-                size: bounds.size().round().to_float2(),
-                thickness: 1. * scale_factor,
-                color: quad
-                    .background
-                    .unwrap_or(Color::transparent_black())
-                    .to_uchar4(),
+        for (ix, underline) in underlines.iter().enumerate() {
+            let origin = underline.origin * scale_factor;
+            let mut height = underline.thickness;
+            if underline.squiggly {
+                height *= 3.;
+            }
+            let size = vec2f(underline.width, height) * scale_factor;
+            let shader_underline = shaders::GPUIUnderline {
+                origin: origin.round().to_float2(),
+                size: size.round().to_float2(),
+                thickness: underline.thickness * scale_factor,
+                color: underline.color.to_uchar4(),
+                squiggly: underline.squiggly as u8,
             };
             unsafe {
-                *(buffer_contents.offset(ix as isize)) = shader_quad;
+                *(buffer_contents.offset(ix as isize)) = shader_underline;
             }
         }
 

crates/gpui/src/platform/mac/shaders/shaders.metal 🔗

@@ -311,6 +311,7 @@ struct UnderlineFragmentInput {
     float2 size;
     float thickness;
     float4 color;
+    bool squiggly;
 };
 
 vertex UnderlineFragmentInput underline_vertex(
@@ -331,22 +332,27 @@ vertex UnderlineFragmentInput underline_vertex(
         underline.size,
         underline.thickness,
         coloru_to_colorf(underline.color),
+        underline.squiggly != 0,
     };
 }
 
 fragment float4 underline_fragment(
     UnderlineFragmentInput input [[stage_in]]
 ) {
-    float half_thickness = input.thickness * 0.5;
-    float2 st = ((input.position.xy - input.origin) / input.size.y) - float2(0., 0.5);
-    float frequency = M_PI_F * 0.75;
-    float amplitude = 0.3;
-    float sine = sin(st.x * frequency) * amplitude;
-    float dSine = cos(st.x * frequency) * amplitude * frequency;
-    float distance = (st.y - sine) / sqrt(1. + dSine * dSine);
-    float distance_in_pixels = distance * input.size.y;
-    float distance_from_top_border = distance_in_pixels - half_thickness;
-    float distance_from_bottom_border = distance_in_pixels + half_thickness;
-    float alpha = saturate(0.5 - max(-distance_from_bottom_border, distance_from_top_border));
-    return input.color * float4(1., 1., 1., alpha);
+    if (input.squiggly) {
+        float half_thickness = input.thickness * 0.5;
+        float2 st = ((input.position.xy - input.origin) / input.size.y) - float2(0., 0.5);
+        float frequency = (M_PI_F * (3. * input.thickness)) / 8.;
+        float amplitude = 1. / (2. * input.thickness);
+        float sine = sin(st.x * frequency) * amplitude;
+        float dSine = cos(st.x * frequency) * amplitude * frequency;
+        float distance = (st.y - sine) / sqrt(1. + dSine * dSine);
+        float distance_in_pixels = distance * input.size.y;
+        float distance_from_top_border = distance_in_pixels - half_thickness;
+        float distance_from_bottom_border = distance_in_pixels + half_thickness;
+        float alpha = saturate(0.5 - max(-distance_from_bottom_border, distance_from_top_border));
+        return input.color * float4(1., 1., 1., alpha);
+    } else {
+        return input.color;
+    }
 }

crates/gpui/src/scene.rs 🔗

@@ -25,7 +25,7 @@ struct StackingContext {
 pub struct Layer {
     clip_bounds: Option<RectF>,
     quads: Vec<Quad>,
-    underlines: Vec<Quad>,
+    underlines: Vec<Underline>,
     images: Vec<Image>,
     shadows: Vec<Shadow>,
     glyphs: Vec<Glyph>,
@@ -76,6 +76,15 @@ pub struct Border {
     pub left: bool,
 }
 
+#[derive(Clone, Copy, Default, Debug)]
+pub struct Underline {
+    pub origin: Vector2F,
+    pub width: f32,
+    pub thickness: f32,
+    pub color: Color,
+    pub squiggly: bool,
+}
+
 impl<'de> Deserialize<'de> for Border {
     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
     where
@@ -183,7 +192,7 @@ impl Scene {
         self.active_layer().push_image(image)
     }
 
-    pub fn push_underline(&mut self, underline: Quad) {
+    pub fn push_underline(&mut self, underline: Underline) {
         self.active_layer().push_underline(underline)
     }
 
@@ -277,11 +286,11 @@ impl Layer {
         self.quads.as_slice()
     }
 
-    fn push_underline(&mut self, underline: Quad) {
+    fn push_underline(&mut self, underline: Underline) {
         self.underlines.push(underline);
     }
 
-    pub fn underlines(&self) -> &[Quad] {
+    pub fn underlines(&self) -> &[Underline] {
         self.underlines.as_slice()
     }
 

crates/gpui/src/text_layout.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
     color::Color,
-    fonts::{FontId, GlyphId},
+    fonts::{FontId, GlyphId, Underline},
     geometry::{
         rect::RectF,
         vector::{vec2f, Vector2F},
@@ -28,7 +28,7 @@ pub struct TextLayoutCache {
 pub struct RunStyle {
     pub color: Color,
     pub font_id: FontId,
-    pub underline: Option<Color>,
+    pub underline: Option<Underline>,
 }
 
 impl TextLayoutCache {
@@ -167,7 +167,7 @@ impl<'a> Hash for CacheKeyRef<'a> {
 #[derive(Default, Debug)]
 pub struct Line {
     layout: Arc<LineLayout>,
-    style_runs: SmallVec<[(u32, Color, Option<Color>); 32]>,
+    style_runs: SmallVec<[(u32, Color, Option<Underline>); 32]>,
 }
 
 #[derive(Default, Debug)]
@@ -265,14 +265,14 @@ impl Line {
 
                 let mut finished_underline = None;
                 if glyph.index >= run_end {
-                    if let Some((run_len, run_color, run_underline_color)) = style_runs.next() {
-                        if let Some((_, underline_color)) = underline {
-                            if *run_underline_color != Some(underline_color) {
+                    if let Some((run_len, run_color, run_underline)) = style_runs.next() {
+                        if let Some((_, underline_style)) = underline {
+                            if *run_underline != Some(underline_style) {
                                 finished_underline = underline.take();
                             }
                         }
-                        if let Some(run_underline_color) = run_underline_color {
-                            underline.get_or_insert((glyph_origin, *run_underline_color));
+                        if let Some(run_underline) = run_underline {
+                            underline.get_or_insert((glyph_origin, *run_underline));
                         }
 
                         run_end += *run_len as usize;
@@ -288,12 +288,13 @@ impl Line {
                     continue;
                 }
 
-                if let Some((underline_origin, underline_color)) = finished_underline {
-                    cx.scene.push_underline(scene::Quad {
-                        bounds: RectF::from_points(underline_origin, glyph_origin + vec2f(0., 3.)),
-                        background: Some(underline_color),
-                        border: Default::default(),
-                        corner_radius: 0.,
+                if let Some((underline_origin, underline_style)) = finished_underline {
+                    cx.scene.push_underline(scene::Underline {
+                        origin: underline_origin,
+                        width: glyph_origin.x() - underline_origin.x(),
+                        thickness: underline_style.thickness.into(),
+                        color: underline_style.color,
+                        squiggly: underline_style.squiggly,
                     });
                 }
 
@@ -307,14 +308,14 @@ impl Line {
             }
         }
 
-        if let Some((underline_start, underline_color)) = underline.take() {
-            let line_end = origin + baseline_offset + vec2f(self.layout.width, 0.);
-
-            cx.scene.push_underline(scene::Quad {
-                bounds: RectF::from_points(underline_start, line_end + vec2f(0., 3.)),
-                background: Some(underline_color),
-                border: Default::default(),
-                corner_radius: 0.,
+        if let Some((underline_start, underline_style)) = underline.take() {
+            let line_end_x = origin.x() + self.layout.width;
+            cx.scene.push_underline(scene::Underline {
+                origin: underline_start,
+                width: line_end_x - underline_start.x(),
+                color: underline_style.color,
+                thickness: underline_style.thickness.into(),
+                squiggly: underline_style.squiggly,
             });
         }
     }