WIP: Start on squiggly underlines

Antonio Scandurra created

Change summary

crates/gpui/src/platform/mac/renderer.rs           | 79 +++++++++++++++
crates/gpui/src/platform/mac/shaders/shaders.h     | 15 +++
crates/gpui/src/platform/mac/shaders/shaders.metal | 46 +++++++++
crates/gpui/src/text_layout.rs                     |  4 
4 files changed, 141 insertions(+), 3 deletions(-)

Detailed changes

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

@@ -26,6 +26,7 @@ pub struct Renderer {
     sprite_pipeline_state: metal::RenderPipelineState,
     image_pipeline_state: metal::RenderPipelineState,
     path_atlas_pipeline_state: metal::RenderPipelineState,
+    underline_pipeline_state: metal::RenderPipelineState,
     unit_vertices: metal::Buffer,
     instances: metal::Buffer,
 }
@@ -109,6 +110,14 @@ impl Renderer {
             "path_atlas_fragment",
             MTLPixelFormat::R16Float,
         );
+        let underline_pipeline_state = build_pipeline_state(
+            &device,
+            &library,
+            "underline",
+            "underline_vertex",
+            "underline_fragment",
+            pixel_format,
+        );
         Self {
             sprite_cache,
             image_cache,
@@ -118,6 +127,7 @@ impl Renderer {
             sprite_pipeline_state,
             image_pipeline_state,
             path_atlas_pipeline_state,
+            underline_pipeline_state,
             unit_vertices,
             instances,
         }
@@ -339,7 +349,7 @@ impl Renderer {
                 drawable_size,
                 command_encoder,
             );
-            self.render_quads(
+            self.render_underlines(
                 layer.underlines(),
                 scale_factor,
                 offset,
@@ -821,6 +831,73 @@ impl Renderer {
         );
         *offset = next_offset;
     }
+
+    fn render_underlines(
+        &mut self,
+        underlines: &[Quad],
+        scale_factor: f32,
+        offset: &mut usize,
+        drawable_size: Vector2F,
+        command_encoder: &metal::RenderCommandEncoderRef,
+    ) {
+        if underlines.is_empty() {
+            return;
+        }
+        align_offset(offset);
+        let next_offset = *offset + underlines.len() * mem::size_of::<shaders::GPUIUnderline>();
+        assert!(
+            next_offset <= INSTANCE_BUFFER_SIZE,
+            "instance buffer exhausted"
+        );
+
+        command_encoder.set_render_pipeline_state(&self.underline_pipeline_state);
+        command_encoder.set_vertex_buffer(
+            shaders::GPUIUnderlineInputIndex_GPUIUnderlineInputIndexVertices as u64,
+            Some(&self.unit_vertices),
+            0,
+        );
+        command_encoder.set_vertex_buffer(
+            shaders::GPUIUnderlineInputIndex_GPUIUnderlineInputIndexUnderlines as u64,
+            Some(&self.instances),
+            *offset as u64,
+        );
+        command_encoder.set_vertex_bytes(
+            shaders::GPUIUnderlineInputIndex_GPUIUnderlineInputIndexUniforms as u64,
+            mem::size_of::<shaders::GPUIUniforms>() as u64,
+            [shaders::GPUIUniforms {
+                viewport_size: drawable_size.to_float2(),
+            }]
+            .as_ptr() as *const c_void,
+        );
+
+        let buffer_contents = unsafe {
+            (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(),
+            };
+            unsafe {
+                *(buffer_contents.offset(ix as isize)) = shader_quad;
+            }
+        }
+
+        command_encoder.draw_primitives_instanced(
+            metal::MTLPrimitiveType::Triangle,
+            0,
+            6,
+            underlines.len() as u64,
+        );
+        *offset = next_offset;
+    }
 }
 
 fn build_path_atlas_texture_descriptor() -> metal::TextureDescriptor {

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

@@ -104,3 +104,18 @@ typedef struct
     vector_uchar4 border_color;
     float corner_radius;
 } GPUIImage;
+
+typedef enum
+{
+    GPUIUnderlineInputIndexVertices = 0,
+    GPUIUnderlineInputIndexUnderlines = 1,
+    GPUIUnderlineInputIndexUniforms = 2,
+} GPUIUnderlineInputIndex;
+
+typedef struct
+{
+    vector_float2 origin;
+    vector_float2 size;
+    float thickness;
+    vector_uchar4 color;
+} GPUIUnderline;

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

@@ -304,3 +304,49 @@ fragment float4 path_atlas_fragment(
     float alpha = saturate(0.5 - distance);
     return float4(alpha, 0., 0., 1.);
 }
+
+struct UnderlineFragmentInput {
+    float4 position [[position]];
+    float2 origin;
+    float2 size;
+    float thickness;
+    float4 color;
+};
+
+vertex UnderlineFragmentInput underline_vertex(
+    uint unit_vertex_id [[vertex_id]],
+    uint underline_id [[instance_id]],
+    constant float2 *unit_vertices [[buffer(GPUIUnderlineInputIndexVertices)]],
+    constant GPUIUnderline *underlines [[buffer(GPUIUnderlineInputIndexUnderlines)]],
+    constant GPUIUniforms *uniforms [[buffer(GPUIUnderlineInputIndexUniforms)]]
+) {
+    float2 unit_vertex = unit_vertices[unit_vertex_id];
+    GPUIUnderline underline = underlines[underline_id];
+    float2 position = unit_vertex * underline.size + underline.origin;
+    float4 device_position = to_device_position(position, uniforms->viewport_size);
+
+    return UnderlineFragmentInput {
+        device_position,
+        underline.origin,
+        underline.size,
+        underline.thickness,
+        coloru_to_colorf(underline.color),
+    };
+}
+
+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);
+}

crates/gpui/src/text_layout.rs 🔗

@@ -290,7 +290,7 @@ impl Line {
 
                 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., 1.)),
+                        bounds: RectF::from_points(underline_origin, glyph_origin + vec2f(0., 3.)),
                         background: Some(underline_color),
                         border: Default::default(),
                         corner_radius: 0.,
@@ -311,7 +311,7 @@ impl Line {
             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., 1.)),
+                bounds: RectF::from_points(underline_start, line_end + vec2f(0., 3.)),
                 background: Some(underline_color),
                 border: Default::default(),
                 corner_radius: 0.,