Checkpoint: underlines

Antonio Scandurra created

Change summary

crates/gpui3/build.rs                           |   6 
crates/gpui3/src/platform/mac/metal_renderer.rs | 141 +++++++++++++++---
crates/gpui3/src/platform/mac/shaders.metal     |  55 ++++++
crates/gpui3/src/scene.rs                       | 131 ++++++++++++++---
crates/gpui3/src/style.rs                       |   2 
crates/gpui3/src/style_helpers.rs               |  90 ++++++++++++
crates/gpui3/src/text_system/line.rs            |  78 +++++-----
crates/gpui3/src/window.rs                      |  45 +++++
crates/storybook2/src/workspace.rs              |   8 
9 files changed, 448 insertions(+), 108 deletions(-)

Detailed changes

crates/gpui3/build.rs 🔗

@@ -50,10 +50,12 @@ fn generate_shader_bindings() -> PathBuf {
         "ScaledContentMask".into(),
         "Uniforms".into(),
         "AtlasTile".into(),
-        "QuadInputIndex".into(),
-        "Quad".into(),
         "ShadowInputIndex".into(),
         "Shadow".into(),
+        "QuadInputIndex".into(),
+        "Underline".into(),
+        "UnderlineInputIndex".into(),
+        "Quad".into(),
         "SpriteInputIndex".into(),
         "MonochromeSprite".into(),
         "PolychromeSprite".into(),

crates/gpui3/src/platform/mac/metal_renderer.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
     point, size, AtlasTextureId, DevicePixels, MetalAtlas, MonochromeSprite, PolychromeSprite,
-    Quad, Scene, Shadow, Size,
+    PrimitiveBatch, Quad, Scene, Shadow, Size, Underline,
 };
 use cocoa::{
     base::{NO, YES},
@@ -17,8 +17,9 @@ const INSTANCE_BUFFER_SIZE: usize = 8192 * 1024; // This is an arbitrary decisio
 pub struct MetalRenderer {
     layer: metal::MetalLayer,
     command_queue: CommandQueue,
-    quads_pipeline_state: metal::RenderPipelineState,
     shadows_pipeline_state: metal::RenderPipelineState,
+    quads_pipeline_state: metal::RenderPipelineState,
+    underlines_pipeline_state: metal::RenderPipelineState,
     monochrome_sprites_pipeline_state: metal::RenderPipelineState,
     polychrome_sprites_pipeline_state: metal::RenderPipelineState,
     unit_vertices: metal::Buffer,
@@ -83,6 +84,14 @@ impl MetalRenderer {
             MTLResourceOptions::StorageModeManaged,
         );
 
+        let shadows_pipeline_state = build_pipeline_state(
+            &device,
+            &library,
+            "shadows",
+            "shadow_vertex",
+            "shadow_fragment",
+            PIXEL_FORMAT,
+        );
         let quads_pipeline_state = build_pipeline_state(
             &device,
             &library,
@@ -91,12 +100,12 @@ impl MetalRenderer {
             "quad_fragment",
             PIXEL_FORMAT,
         );
-        let shadows_pipeline_state = build_pipeline_state(
+        let underlines_pipeline_state = build_pipeline_state(
             &device,
             &library,
-            "shadows",
-            "shadow_vertex",
-            "shadow_fragment",
+            "underlines",
+            "underline_vertex",
+            "underline_fragment",
             PIXEL_FORMAT,
         );
         let monochrome_sprites_pipeline_state = build_pipeline_state(
@@ -122,8 +131,9 @@ impl MetalRenderer {
         Self {
             layer,
             command_queue,
-            quads_pipeline_state,
             shadows_pipeline_state,
+            quads_pipeline_state,
+            underlines_pipeline_state,
             monochrome_sprites_pipeline_state,
             polychrome_sprites_pipeline_state,
             unit_vertices,
@@ -184,10 +194,7 @@ impl MetalRenderer {
         let mut instance_offset = 0;
         for batch in scene.batches() {
             match batch {
-                crate::PrimitiveBatch::Quads(quads) => {
-                    self.draw_quads(quads, &mut instance_offset, viewport_size, command_encoder);
-                }
-                crate::PrimitiveBatch::Shadows(shadows) => {
+                PrimitiveBatch::Shadows(shadows) => {
                     self.draw_shadows(
                         shadows,
                         &mut instance_offset,
@@ -195,7 +202,18 @@ impl MetalRenderer {
                         command_encoder,
                     );
                 }
-                crate::PrimitiveBatch::MonochromeSprites {
+                PrimitiveBatch::Quads(quads) => {
+                    self.draw_quads(quads, &mut instance_offset, viewport_size, command_encoder);
+                }
+                PrimitiveBatch::Underlines(underlines) => {
+                    self.draw_underlines(
+                        underlines,
+                        &mut instance_offset,
+                        viewport_size,
+                        command_encoder,
+                    );
+                }
+                PrimitiveBatch::MonochromeSprites {
                     texture_id,
                     sprites,
                 } => {
@@ -207,7 +225,7 @@ impl MetalRenderer {
                         command_encoder,
                     );
                 }
-                crate::PrimitiveBatch::PolychromeSprites {
+                PrimitiveBatch::PolychromeSprites {
                     texture_id,
                     sprites,
                 } => {
@@ -234,6 +252,66 @@ impl MetalRenderer {
         drawable.present();
     }
 
+    fn draw_shadows(
+        &mut self,
+        shadows: &[Shadow],
+        offset: &mut usize,
+        viewport_size: Size<DevicePixels>,
+        command_encoder: &metal::RenderCommandEncoderRef,
+    ) {
+        if shadows.is_empty() {
+            return;
+        }
+        align_offset(offset);
+
+        command_encoder.set_render_pipeline_state(&self.shadows_pipeline_state);
+        command_encoder.set_vertex_buffer(
+            ShadowInputIndex::Vertices as u64,
+            Some(&self.unit_vertices),
+            0,
+        );
+        command_encoder.set_vertex_buffer(
+            ShadowInputIndex::Shadows as u64,
+            Some(&self.instances),
+            *offset as u64,
+        );
+        command_encoder.set_fragment_buffer(
+            ShadowInputIndex::Shadows as u64,
+            Some(&self.instances),
+            *offset as u64,
+        );
+
+        command_encoder.set_vertex_bytes(
+            ShadowInputIndex::ViewportSize as u64,
+            mem::size_of_val(&viewport_size) as u64,
+            &viewport_size as *const Size<DevicePixels> as *const _,
+        );
+
+        let shadow_bytes_len = mem::size_of::<Shadow>() * shadows.len();
+        let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
+        unsafe {
+            ptr::copy_nonoverlapping(
+                shadows.as_ptr() as *const u8,
+                buffer_contents,
+                shadow_bytes_len,
+            );
+        }
+
+        let next_offset = *offset + shadow_bytes_len;
+        assert!(
+            next_offset <= INSTANCE_BUFFER_SIZE,
+            "instance buffer exhausted"
+        );
+
+        command_encoder.draw_primitives_instanced(
+            metal::MTLPrimitiveType::Triangle,
+            0,
+            6,
+            shadows.len() as u64,
+        );
+        *offset = next_offset;
+    }
+
     fn draw_quads(
         &mut self,
         quads: &[Quad],
@@ -290,52 +368,52 @@ impl MetalRenderer {
         *offset = next_offset;
     }
 
-    fn draw_shadows(
+    fn draw_underlines(
         &mut self,
-        shadows: &[Shadow],
+        underlines: &[Underline],
         offset: &mut usize,
         viewport_size: Size<DevicePixels>,
         command_encoder: &metal::RenderCommandEncoderRef,
     ) {
-        if shadows.is_empty() {
+        if underlines.is_empty() {
             return;
         }
         align_offset(offset);
 
-        command_encoder.set_render_pipeline_state(&self.shadows_pipeline_state);
+        command_encoder.set_render_pipeline_state(&self.underlines_pipeline_state);
         command_encoder.set_vertex_buffer(
-            ShadowInputIndex::Vertices as u64,
+            UnderlineInputIndex::Vertices as u64,
             Some(&self.unit_vertices),
             0,
         );
         command_encoder.set_vertex_buffer(
-            ShadowInputIndex::Shadows as u64,
+            UnderlineInputIndex::Underlines as u64,
             Some(&self.instances),
             *offset as u64,
         );
         command_encoder.set_fragment_buffer(
-            ShadowInputIndex::Shadows as u64,
+            UnderlineInputIndex::Underlines as u64,
             Some(&self.instances),
             *offset as u64,
         );
 
         command_encoder.set_vertex_bytes(
-            ShadowInputIndex::ViewportSize as u64,
+            UnderlineInputIndex::ViewportSize as u64,
             mem::size_of_val(&viewport_size) as u64,
             &viewport_size as *const Size<DevicePixels> as *const _,
         );
 
-        let shadow_bytes_len = mem::size_of::<Shadow>() * shadows.len();
+        let quad_bytes_len = mem::size_of::<Underline>() * underlines.len();
         let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
         unsafe {
             ptr::copy_nonoverlapping(
-                shadows.as_ptr() as *const u8,
+                underlines.as_ptr() as *const u8,
                 buffer_contents,
-                shadow_bytes_len,
+                quad_bytes_len,
             );
         }
 
-        let next_offset = *offset + shadow_bytes_len;
+        let next_offset = *offset + quad_bytes_len;
         assert!(
             next_offset <= INSTANCE_BUFFER_SIZE,
             "instance buffer exhausted"
@@ -345,7 +423,7 @@ impl MetalRenderer {
             metal::MTLPrimitiveType::Triangle,
             0,
             6,
-            shadows.len() as u64,
+            underlines.len() as u64,
         );
         *offset = next_offset;
     }
@@ -533,6 +611,13 @@ fn align_offset(offset: &mut usize) {
     *offset = ((*offset + 255) / 256) * 256;
 }
 
+#[repr(C)]
+enum ShadowInputIndex {
+    Vertices = 0,
+    Shadows = 1,
+    ViewportSize = 2,
+}
+
 #[repr(C)]
 enum QuadInputIndex {
     Vertices = 0,
@@ -541,9 +626,9 @@ enum QuadInputIndex {
 }
 
 #[repr(C)]
-enum ShadowInputIndex {
+enum UnderlineInputIndex {
     Vertices = 0,
-    Shadows = 1,
+    Underlines = 1,
     ViewportSize = 2,
 }
 

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

@@ -193,6 +193,53 @@ fragment float4 shadow_fragment(ShadowVertexOutput input [[stage_in]],
   return input.color * float4(1., 1., 1., alpha);
 }
 
+struct UnderlineVertexOutput {
+  float4 position [[position]];
+  float4 color [[flat]];
+  uint underline_id [[flat]];
+};
+
+vertex UnderlineVertexOutput underline_vertex(
+    uint unit_vertex_id [[vertex_id]], uint underline_id [[instance_id]],
+    constant float2 *unit_vertices [[buffer(UnderlineInputIndex_Vertices)]],
+    constant Underline *underlines [[buffer(UnderlineInputIndex_Underlines)]],
+    constant Size_DevicePixels *viewport_size
+    [[buffer(ShadowInputIndex_ViewportSize)]]) {
+  float2 unit_vertex = unit_vertices[unit_vertex_id];
+  Underline underline = underlines[underline_id];
+  float4 device_position =
+      to_device_position(unit_vertex, underline.bounds,
+                         underline.content_mask.bounds, viewport_size);
+  float4 color = hsla_to_rgba(underline.color);
+  return UnderlineVertexOutput{device_position, color, underline_id};
+}
+
+fragment float4 underline_fragment(UnderlineVertexOutput input [[stage_in]],
+                                   constant Underline *underlines
+                                   [[buffer(UnderlineInputIndex_Underlines)]]) {
+  Underline underline = underlines[input.underline_id];
+  if (underline.wavy) {
+    float half_thickness = underline.thickness * 0.5;
+    float2 origin =
+        float2(underline.bounds.origin.x, underline.bounds.origin.y);
+    float2 st = ((input.position.xy - origin) / underline.bounds.size.height) -
+                float2(0., 0.5);
+    float frequency = (M_PI_F * (3. * underline.thickness)) / 8.;
+    float amplitude = 1. / (2. * underline.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 * underline.bounds.size.height;
+    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;
+  }
+}
+
 struct MonochromeSpriteVertexOutput {
   float4 position [[position]];
   float2 tile_position;
@@ -211,8 +258,8 @@ vertex MonochromeSpriteVertexOutput monochrome_sprite_vertex(
 
   float2 unit_vertex = unit_vertices[unit_vertex_id];
   MonochromeSprite sprite = sprites[sprite_id];
-  // Don't apply content mask at the vertex level because we don't have time to
-  // make sampling from the texture match the mask.
+  // Don't apply content mask at the vertex level because we don't have time
+  // to make sampling from the texture match the mask.
   float4 device_position = to_device_position(unit_vertex, sprite.bounds,
                                               sprite.bounds, viewport_size);
   float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size);
@@ -254,8 +301,8 @@ vertex PolychromeSpriteVertexOutput polychrome_sprite_vertex(
 
   float2 unit_vertex = unit_vertices[unit_vertex_id];
   PolychromeSprite sprite = sprites[sprite_id];
-  // Don't apply content mask at the vertex level because we don't have time to
-  // make sampling from the texture match the mask.
+  // Don't apply content mask at the vertex level because we don't have time
+  // to make sampling from the texture match the mask.
   float4 device_position = to_device_position(unit_vertex, sprite.bounds,
                                               sprite.bounds, viewport_size);
   float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size);

crates/gpui3/src/scene.rs 🔗

@@ -17,8 +17,9 @@ pub type DrawOrder = u32;
 pub struct Scene {
     pub(crate) scale_factor: f32,
     pub(crate) layers: BTreeMap<StackingOrder, LayerId>,
-    pub quads: Vec<Quad>,
     pub shadows: Vec<Shadow>,
+    pub quads: Vec<Quad>,
+    pub underlines: Vec<Underline>,
     pub monochrome_sprites: Vec<MonochromeSprite>,
     pub polychrome_sprites: Vec<PolychromeSprite>,
 }
@@ -28,8 +29,9 @@ impl Scene {
         Scene {
             scale_factor,
             layers: BTreeMap::new(),
-            quads: Vec::new(),
             shadows: Vec::new(),
+            quads: Vec::new(),
+            underlines: Vec::new(),
             monochrome_sprites: Vec::new(),
             polychrome_sprites: Vec::new(),
         }
@@ -39,8 +41,9 @@ impl Scene {
         Scene {
             scale_factor: self.scale_factor,
             layers: mem::take(&mut self.layers),
-            quads: mem::take(&mut self.quads),
             shadows: mem::take(&mut self.shadows),
+            quads: mem::take(&mut self.quads),
+            underlines: mem::take(&mut self.underlines),
             monochrome_sprites: mem::take(&mut self.monochrome_sprites),
             polychrome_sprites: mem::take(&mut self.polychrome_sprites),
         }
@@ -51,13 +54,17 @@ impl Scene {
         let layer_id = *self.layers.entry(layer_id).or_insert(next_id);
         let primitive = primitive.into();
         match primitive {
+            Primitive::Shadow(mut shadow) => {
+                shadow.order = layer_id;
+                self.shadows.push(shadow);
+            }
             Primitive::Quad(mut quad) => {
                 quad.order = layer_id;
                 self.quads.push(quad);
             }
-            Primitive::Shadow(mut shadow) => {
-                shadow.order = layer_id;
-                self.shadows.push(shadow);
+            Primitive::Underline(mut underline) => {
+                underline.order = layer_id;
+                self.underlines.push(underline);
             }
             Primitive::MonochromeSprite(mut sprite) => {
                 sprite.order = layer_id;
@@ -78,15 +85,26 @@ impl Scene {
         }
 
         // Add all primitives to the BSP splitter to determine draw order
+        // todo!("reuse the same splitter")
         let mut splitter = BspSplitter::new();
+
+        for (ix, shadow) in self.shadows.iter().enumerate() {
+            let z = layer_z_values[shadow.order as LayerId as usize];
+            splitter.add(shadow.bounds.to_bsp_polygon(z, (PrimitiveKind::Shadow, ix)));
+        }
+
         for (ix, quad) in self.quads.iter().enumerate() {
             let z = layer_z_values[quad.order as LayerId as usize];
             splitter.add(quad.bounds.to_bsp_polygon(z, (PrimitiveKind::Quad, ix)));
         }
 
-        for (ix, shadow) in self.shadows.iter().enumerate() {
-            let z = layer_z_values[shadow.order as LayerId as usize];
-            splitter.add(shadow.bounds.to_bsp_polygon(z, (PrimitiveKind::Shadow, ix)));
+        for (ix, underline) in self.underlines.iter().enumerate() {
+            let z = layer_z_values[underline.order as LayerId as usize];
+            splitter.add(
+                underline
+                    .bounds
+                    .to_bsp_polygon(z, (PrimitiveKind::Underline, ix)),
+            );
         }
 
         for (ix, monochrome_sprite) in self.monochrome_sprites.iter().enumerate() {
@@ -111,8 +129,11 @@ impl Scene {
         // We need primitives to be repr(C), hence the weird reuse of the order field for two different types.
         for (draw_order, polygon) in splitter.sort(Vector3D::new(0., 0., 1.)).iter().enumerate() {
             match polygon.anchor {
-                (PrimitiveKind::Quad, ix) => self.quads[ix].order = draw_order as DrawOrder,
                 (PrimitiveKind::Shadow, ix) => self.shadows[ix].order = draw_order as DrawOrder,
+                (PrimitiveKind::Quad, ix) => self.quads[ix].order = draw_order as DrawOrder,
+                (PrimitiveKind::Underline, ix) => {
+                    self.underlines[ix].order = draw_order as DrawOrder
+                }
                 (PrimitiveKind::MonochromeSprite, ix) => {
                     self.monochrome_sprites[ix].order = draw_order as DrawOrder
                 }
@@ -123,18 +144,22 @@ impl Scene {
         }
 
         // Sort the primitives
-        self.quads.sort_unstable();
         self.shadows.sort_unstable();
+        self.quads.sort_unstable();
+        self.underlines.sort_unstable();
         self.monochrome_sprites.sort_unstable();
         self.polychrome_sprites.sort_unstable();
 
         BatchIterator {
-            quads: &self.quads,
-            quads_start: 0,
-            quads_iter: self.quads.iter().peekable(),
             shadows: &self.shadows,
             shadows_start: 0,
             shadows_iter: self.shadows.iter().peekable(),
+            quads: &self.quads,
+            quads_start: 0,
+            quads_iter: self.quads.iter().peekable(),
+            underlines: &self.underlines,
+            underlines_start: 0,
+            underlines_iter: self.underlines.iter().peekable(),
             monochrome_sprites: &self.monochrome_sprites,
             monochrome_sprites_start: 0,
             monochrome_sprites_iter: self.monochrome_sprites.iter().peekable(),
@@ -152,6 +177,9 @@ struct BatchIterator<'a> {
     shadows: &'a [Shadow],
     shadows_start: usize,
     shadows_iter: Peekable<slice::Iter<'a, Shadow>>,
+    underlines: &'a [Underline],
+    underlines_start: usize,
+    underlines_iter: Peekable<slice::Iter<'a, Underline>>,
     monochrome_sprites: &'a [MonochromeSprite],
     monochrome_sprites_start: usize,
     monochrome_sprites_iter: Peekable<slice::Iter<'a, MonochromeSprite>>,
@@ -165,11 +193,15 @@ impl<'a> Iterator for BatchIterator<'a> {
 
     fn next(&mut self) -> Option<Self::Item> {
         let mut orders_and_kinds = [
-            (self.quads_iter.peek().map(|q| q.order), PrimitiveKind::Quad),
             (
                 self.shadows_iter.peek().map(|s| s.order),
                 PrimitiveKind::Shadow,
             ),
+            (self.quads_iter.peek().map(|q| q.order), PrimitiveKind::Quad),
+            (
+                self.underlines_iter.peek().map(|u| u.order),
+                PrimitiveKind::Underline,
+            ),
             (
                 self.monochrome_sprites_iter.peek().map(|s| s.order),
                 PrimitiveKind::MonochromeSprite,
@@ -190,6 +222,21 @@ impl<'a> Iterator for BatchIterator<'a> {
         };
 
         match batch_kind {
+            PrimitiveKind::Shadow => {
+                let shadows_start = self.shadows_start;
+                let mut shadows_end = shadows_start;
+                while self
+                    .shadows_iter
+                    .next_if(|shadow| shadow.order <= max_order)
+                    .is_some()
+                {
+                    shadows_end += 1;
+                }
+                self.shadows_start = shadows_end;
+                Some(PrimitiveBatch::Shadows(
+                    &self.shadows[shadows_start..shadows_end],
+                ))
+            }
             PrimitiveKind::Quad => {
                 let quads_start = self.quads_start;
                 let mut quads_end = quads_start;
@@ -203,19 +250,19 @@ impl<'a> Iterator for BatchIterator<'a> {
                 self.quads_start = quads_end;
                 Some(PrimitiveBatch::Quads(&self.quads[quads_start..quads_end]))
             }
-            PrimitiveKind::Shadow => {
-                let shadows_start = self.shadows_start;
-                let mut shadows_end = shadows_start;
+            PrimitiveKind::Underline => {
+                let underlines_start = self.underlines_start;
+                let mut underlines_end = underlines_start;
                 while self
-                    .shadows_iter
-                    .next_if(|shadow| shadow.order <= max_order)
+                    .underlines_iter
+                    .next_if(|underline| underline.order <= max_order)
                     .is_some()
                 {
-                    shadows_end += 1;
+                    underlines_end += 1;
                 }
-                self.shadows_start = shadows_end;
-                Some(PrimitiveBatch::Shadows(
-                    &self.shadows[shadows_start..shadows_end],
+                self.underlines_start = underlines_end;
+                Some(PrimitiveBatch::Underlines(
+                    &self.underlines[underlines_start..underlines_end],
                 ))
             }
             PrimitiveKind::MonochromeSprite => {
@@ -265,22 +312,25 @@ pub enum PrimitiveKind {
     Shadow,
     #[default]
     Quad,
+    Underline,
     MonochromeSprite,
     PolychromeSprite,
 }
 
 #[derive(Clone, Debug)]
 pub enum Primitive {
-    Quad(Quad),
     Shadow(Shadow),
+    Quad(Quad),
+    Underline(Underline),
     MonochromeSprite(MonochromeSprite),
     PolychromeSprite(PolychromeSprite),
 }
 
 #[derive(Debug)]
 pub(crate) enum PrimitiveBatch<'a> {
-    Quads(&'a [Quad]),
     Shadows(&'a [Shadow]),
+    Quads(&'a [Quad]),
+    Underlines(&'a [Underline]),
     MonochromeSprites {
         texture_id: AtlasTextureId,
         sprites: &'a [MonochromeSprite],
@@ -321,6 +371,35 @@ impl From<Quad> for Primitive {
     }
 }
 
+#[derive(Debug, Clone, Eq, PartialEq)]
+#[repr(C)]
+pub struct Underline {
+    pub order: u32,
+    pub bounds: Bounds<ScaledPixels>,
+    pub content_mask: ScaledContentMask,
+    pub thickness: ScaledPixels,
+    pub color: Hsla,
+    pub wavy: bool,
+}
+
+impl Ord for Underline {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.order.cmp(&other.order)
+    }
+}
+
+impl PartialOrd for Underline {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl From<Underline> for Primitive {
+    fn from(underline: Underline) -> Self {
+        Primitive::Underline(underline)
+    }
+}
+
 #[derive(Debug, Clone, Eq, PartialEq)]
 #[repr(C)]
 pub struct Shadow {

crates/gpui3/src/style.rs 🔗

@@ -344,7 +344,7 @@ impl Default for Style {
 pub struct UnderlineStyle {
     pub thickness: Pixels,
     pub color: Option<Hsla>,
-    pub squiggly: bool,
+    pub wavy: bool,
 }
 
 #[derive(Clone, Debug)]

crates/gpui3/src/style_helpers.rs 🔗

@@ -416,6 +416,96 @@ pub trait StyleHelpers: Styled<Style = Style> {
         self
     }
 
+    fn text_decoration_none(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.text_style()
+            .get_or_insert_with(Default::default)
+            .underline = None;
+        self
+    }
+
+    fn text_decoration_color(mut self, color: impl Into<Hsla>) -> Self
+    where
+        Self: Sized,
+    {
+        let style = self.text_style().get_or_insert_with(Default::default);
+        let underline = style.underline.get_or_insert_with(Default::default);
+        underline.color = Some(color.into());
+        self
+    }
+
+    fn text_decoration_solid(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        let style = self.text_style().get_or_insert_with(Default::default);
+        let underline = style.underline.get_or_insert_with(Default::default);
+        underline.wavy = false;
+        self
+    }
+
+    fn text_decoration_wavy(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        let style = self.text_style().get_or_insert_with(Default::default);
+        let underline = style.underline.get_or_insert_with(Default::default);
+        underline.wavy = true;
+        self
+    }
+
+    fn text_decoration_0(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        let style = self.text_style().get_or_insert_with(Default::default);
+        let underline = style.underline.get_or_insert_with(Default::default);
+        underline.thickness = px(0.);
+        self
+    }
+
+    fn text_decoration_1(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        let style = self.text_style().get_or_insert_with(Default::default);
+        let underline = style.underline.get_or_insert_with(Default::default);
+        underline.thickness = px(1.);
+        self
+    }
+
+    fn text_decoration_2(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        let style = self.text_style().get_or_insert_with(Default::default);
+        let underline = style.underline.get_or_insert_with(Default::default);
+        underline.thickness = px(2.);
+        self
+    }
+
+    fn text_decoration_4(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        let style = self.text_style().get_or_insert_with(Default::default);
+        let underline = style.underline.get_or_insert_with(Default::default);
+        underline.thickness = px(4.);
+        self
+    }
+
+    fn text_decoration_8(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        let style = self.text_style().get_or_insert_with(Default::default);
+        let underline = style.underline.get_or_insert_with(Default::default);
+        underline.thickness = px(8.);
+        self
+    }
+
     fn font(mut self, family_name: impl Into<SharedString>) -> Self
     where
         Self: Sized,

crates/gpui3/src/text_system/line.rs 🔗

@@ -134,9 +134,11 @@ impl Line {
                                     origin.y + baseline_offset.y + (self.layout.descent * 0.618),
                                 ),
                                 UnderlineStyle {
-                                    color: style_run.underline.color,
+                                    color: Some(
+                                        style_run.underline.color.unwrap_or(style_run.color),
+                                    ),
                                     thickness: style_run.underline.thickness,
-                                    squiggly: style_run.underline.squiggly,
+                                    wavy: style_run.underline.wavy,
                                 },
                             ));
                         }
@@ -153,8 +155,12 @@ impl Line {
                     continue;
                 }
 
-                if let Some((_underline_origin, _underline_style)) = finished_underline {
-                    todo!()
+                if let Some((underline_origin, underline_style)) = finished_underline {
+                    cx.paint_underline(
+                        underline_origin,
+                        glyph_origin.x - underline_origin.x,
+                        &underline_style,
+                    )?;
                 }
 
                 if glyph.is_emoji {
@@ -171,15 +177,13 @@ impl Line {
             }
         }
 
-        if let Some((_underline_start, _underline_style)) = underline.take() {
-            let _line_end_x = origin.x + self.layout.width;
-            // cx.scene().push_underline(Underline {
-            //     origin: underline_start,
-            //     width: line_end_x - underline_start.x,
-            //     color: underline_style.color,
-            //     thickness: underline_style.thickness.into(),
-            //     squiggly: underline_style.squiggly,
-            // });
+        if let Some((underline_start, underline_style)) = underline.take() {
+            let line_end_x = origin.x + self.layout.width;
+            cx.paint_underline(
+                underline_start,
+                line_end_x - underline_start.x,
+                &underline_style,
+            )?;
         }
 
         Ok(())
@@ -188,7 +192,7 @@ impl Line {
     pub fn paint_wrapped(
         &self,
         origin: Point<Pixels>,
-        _visible_bounds: Bounds<Pixels>,
+        _visible_bounds: Bounds<Pixels>, // todo!("use clipping")
         line_height: Pixels,
         boundaries: &[ShapedBoundary],
         cx: &mut WindowContext,
@@ -213,14 +217,12 @@ impl Line {
                     .map_or(false, |b| b.run_ix == run_ix && b.glyph_ix == glyph_ix)
                 {
                     boundaries.next();
-                    if let Some((_underline_origin, _underline_style)) = underline.take() {
-                        // cx.scene().push_underline(Underline {
-                        //     origin: underline_origin,
-                        //     width: glyph_origin.x - underline_origin.x,
-                        //     thickness: underline_style.thickness.into(),
-                        //     color: underline_style.color.unwrap(),
-                        //     squiggly: underline_style.squiggly,
-                        // });
+                    if let Some((underline_origin, underline_style)) = underline.take() {
+                        cx.paint_underline(
+                            underline_origin,
+                            glyph_origin.x - underline_origin.x,
+                            &underline_style,
+                        )?;
                     }
 
                     glyph_origin = point(origin.x, glyph_origin.y + line_height);
@@ -249,7 +251,7 @@ impl Line {
                                         style_run.underline.color.unwrap_or(style_run.color),
                                     ),
                                     thickness: style_run.underline.thickness,
-                                    squiggly: style_run.underline.squiggly,
+                                    wavy: style_run.underline.wavy,
                                 },
                             ));
                         }
@@ -260,14 +262,12 @@ impl Line {
                     }
                 }
 
-                if let Some((_underline_origin, _underline_style)) = finished_underline {
-                    // cx.scene().push_underline(Underline {
-                    //     origin: underline_origin,
-                    //     width: glyph_origin.x - underline_origin.x,
-                    //     thickness: underline_style.thickness.into(),
-                    //     color: underline_style.color.unwrap(),
-                    //     squiggly: underline_style.squiggly,
-                    // });
+                if let Some((underline_origin, underline_style)) = finished_underline {
+                    cx.paint_underline(
+                        underline_origin,
+                        glyph_origin.x - underline_origin.x,
+                        &underline_style,
+                    )?;
                 }
 
                 let text_system = cx.text_system();
@@ -298,15 +298,13 @@ impl Line {
             }
         }
 
-        if let Some((_underline_origin, _underline_style)) = underline.take() {
-            // let line_end_x = glyph_origin.x + self.layout.width - prev_position;
-            // cx.scene().push_underline(Underline {
-            //     origin: underline_origin,
-            //     width: line_end_x - underline_origin.x,
-            //     thickness: underline_style.thickness.into(),
-            //     color: underline_style.color,
-            //     squiggly: underline_style.squiggly,
-            // });
+        if let Some((underline_origin, underline_style)) = underline.take() {
+            let line_end_x = glyph_origin.x + self.layout.width - prev_position;
+            cx.paint_underline(
+                underline_origin,
+                line_end_x - underline_origin.x,
+                &underline_style,
+            )?;
         }
 
         Ok(())

crates/gpui3/src/window.rs 🔗

@@ -1,10 +1,11 @@
 use crate::{
-    image_cache::RenderImageParams, px, AnyView, AppContext, AsyncWindowContext, AvailableSpace,
-    BorrowAppContext, Bounds, Context, Corners, DevicePixels, DisplayId, Effect, Element, EntityId,
-    FontId, GlyphId, Handle, Hsla, ImageData, IsZero, LayoutId, MainThread, MainThreadOnly,
-    MonochromeSprite, Pixels, PlatformAtlas, PlatformWindow, Point, PolychromeSprite, Reference,
-    RenderGlyphParams, RenderSvgParams, ScaledPixels, Scene, SharedString, Size, StackingOrder,
-    Style, TaffyLayoutEngine, Task, WeakHandle, WindowOptions, SUBPIXEL_VARIANTS,
+    image_cache::RenderImageParams, px, size, AnyView, AppContext, AsyncWindowContext,
+    AvailableSpace, BorrowAppContext, Bounds, Context, Corners, DevicePixels, DisplayId, Effect,
+    Element, EntityId, FontId, GlyphId, Handle, Hsla, ImageData, IsZero, LayoutId, MainThread,
+    MainThreadOnly, MonochromeSprite, Pixels, PlatformAtlas, PlatformWindow, Point,
+    PolychromeSprite, Reference, RenderGlyphParams, RenderSvgParams, ScaledPixels, Scene,
+    SharedString, Size, StackingOrder, Style, TaffyLayoutEngine, Task, Underline, UnderlineStyle,
+    WeakHandle, WindowOptions, SUBPIXEL_VARIANTS,
 };
 use anyhow::Result;
 use smallvec::SmallVec;
@@ -259,6 +260,38 @@ impl<'a, 'w> WindowContext<'a, 'w> {
         self.window.current_stacking_order.clone()
     }
 
+    pub fn paint_underline(
+        &mut self,
+        origin: Point<Pixels>,
+        width: Pixels,
+        style: &UnderlineStyle,
+    ) -> Result<()> {
+        let scale_factor = self.scale_factor();
+        let height = if style.wavy {
+            style.thickness * 3.
+        } else {
+            style.thickness
+        };
+        let bounds = Bounds {
+            origin,
+            size: size(width, height),
+        };
+        let content_mask = self.content_mask();
+        let layer_id = self.current_stacking_order();
+        self.window.scene.insert(
+            layer_id,
+            Underline {
+                order: 0,
+                bounds: bounds.scale(scale_factor),
+                content_mask: content_mask.scale(scale_factor),
+                thickness: style.thickness.scale(scale_factor),
+                color: style.color.unwrap_or_default(),
+                wavy: style.wavy,
+            },
+        );
+        Ok(())
+    }
+
     pub fn paint_glyph(
         &mut self,
         origin: Point<Pixels>,

crates/storybook2/src/workspace.rs 🔗

@@ -160,7 +160,13 @@ impl Titlebar {
                             // .fill(theme.lowest.base.hovered.background)
                             // .active()
                             // .fill(theme.lowest.base.pressed.background)
-                            .child(div().text_sm().child("branch")),
+                            .child(
+                                div()
+                                    .text_sm()
+                                    .text_decoration_1()
+                                    .text_decoration_wavy()
+                                    .child("branch"),
+                            ),
                     ),
             )
     }