gpui: Add support for slash pattern fills (`///`) (#23576)

Nate Butler and Antonio Scandurra created

TODO:
- [x] Add BackgroundTag::PatternSlash
- [x] Support metal slash pattern fills
- [x] Support blade slash pattern fills
---

Adds support for a new background type in gpui, `pattern_slash`.

Usage:

```rust
div().size(px(56.0)).bg(pattern_slash(gpui::red()))
```
This will create a 56px square with a red slash pattern fill.

You can run the pattern example with `cargo run -p gpui --example
pattern`:

![CleanShot 2025-01-23 at 16 22
09@2x](https://github.com/user-attachments/assets/39d9f8c8-816c-4d3b-bc75-fcc122747e17)

---

After talking with @as-cii at length about how we want to support
patterns in gpui, we decided for now we'll simply add a new
BackgroundTag specific to this pattern.

It isn't the best long term plan however – we'll likely want to
introduce the concept of a `Fill` at some point so we can have
`Fill::Solid`, `Fill::Gradient(LinearGradient)`, etc in the future.

The pattern is designed to seamlessly tile vertically for elements of
the same height. For example, for use in editor line backgrounds:

![CleanShot 2025-01-23 at 16 27
41@2x](https://github.com/user-attachments/assets/d51b94bc-cfc2-4aff-89e3-289a04ea8841)

---


Release Notes:

(do we do gpui release notes?)
- Adds support for slash pattern fills in `gpui`.

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>

Change summary

crates/gpui/Cargo.toml                      |  25 +++--
crates/gpui/examples/pattern.rs             | 103 ++++++++++++++++++++++
crates/gpui/src/color.rs                    |  11 ++
crates/gpui/src/platform/blade/shaders.wgsl |  19 ++++
crates/gpui/src/platform/mac/shaders.metal  |  44 +++++++--
crates/gpui/src/style.rs                    |   1 
6 files changed, 181 insertions(+), 22 deletions(-)

Detailed changes

crates/gpui/Cargo.toml 🔗

@@ -212,7 +212,6 @@ flume = "0.11"
 rand.workspace = true
 windows.workspace = true
 windows-core = "0.58"
-
 [[example]]
 name = "hello_world"
 path = "examples/hello_world.rs"
@@ -222,16 +221,20 @@ name = "image"
 path = "examples/image/image.rs"
 
 [[example]]
-name = "set_menus"
-path = "examples/set_menus.rs"
+name = "input"
+path = "examples/input.rs"
 
 [[example]]
-name = "window_shadow"
-path = "examples/window_shadow.rs"
+name = "opacity"
+path = "examples/opacity.rs"
 
 [[example]]
-name = "input"
-path = "examples/input.rs"
+name = "pattern"
+path = "examples/pattern.rs"
+
+[[example]]
+name = "set_menus"
+path = "examples/set_menus.rs"
 
 [[example]]
 name = "shadow"
@@ -245,10 +248,10 @@ path = "examples/svg/svg.rs"
 name = "text_wrapper"
 path = "examples/text_wrapper.rs"
 
-[[example]]
-name = "opacity"
-path = "examples/opacity.rs"
-
 [[example]]
 name = "uniform_list"
 path = "examples/uniform_list.rs"
+
+[[example]]
+name = "window_shadow"
+path = "examples/window_shadow.rs"

crates/gpui/examples/pattern.rs 🔗

@@ -0,0 +1,103 @@
+use gpui::{
+    div, linear_color_stop, linear_gradient, pattern_slash, prelude::*, px, rgb, size, App,
+    AppContext, Bounds, ViewContext, WindowBounds, WindowOptions,
+};
+
+struct PatternExample;
+
+impl Render for PatternExample {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
+        div()
+            .flex()
+            .flex_col()
+            .gap_3()
+            .bg(rgb(0xffffff))
+            .size(px(600.0))
+            .justify_center()
+            .items_center()
+            .shadow_lg()
+            .text_xl()
+            .text_color(rgb(0x000000))
+            .child("Pattern Example")
+            .child(
+                div()
+                    .flex()
+                    .flex_col()
+                    .border_1()
+                    .border_color(gpui::blue())
+                    .child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash(gpui::red())))
+                    .child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash(gpui::red())))
+                    .child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash(gpui::red())))
+                    .child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash(gpui::red()))),
+            )
+            .child(
+                div()
+                    .flex()
+                    .flex_col()
+                    .border_1()
+                    .border_color(gpui::blue())
+                    .bg(gpui::green().opacity(0.16))
+                    .child("Elements the same height should align")
+                    .child(
+                        div()
+                            .w(px(256.0))
+                            .h(px(56.0))
+                            .bg(pattern_slash(gpui::red())),
+                    )
+                    .child(
+                        div()
+                            .w(px(256.0))
+                            .h(px(56.0))
+                            .bg(pattern_slash(gpui::green())),
+                    )
+                    .child(
+                        div()
+                            .w(px(256.0))
+                            .h(px(56.0))
+                            .bg(pattern_slash(gpui::blue())),
+                    )
+                    .child(
+                        div()
+                            .w(px(256.0))
+                            .h(px(26.0))
+                            .bg(pattern_slash(gpui::yellow())),
+                    ),
+            )
+            .child(
+                div()
+                    .border_1()
+                    .border_color(gpui::blue())
+                    .w(px(240.0))
+                    .h(px(40.0))
+                    .bg(gpui::red()),
+            )
+            .child(
+                div()
+                    .border_1()
+                    .border_color(gpui::blue())
+                    .w(px(240.0))
+                    .h(px(40.0))
+                    .bg(linear_gradient(
+                        45.,
+                        linear_color_stop(gpui::red(), 0.),
+                        linear_color_stop(gpui::blue(), 1.),
+                    )),
+            )
+    }
+}
+
+fn main() {
+    App::new().run(|cx: &mut AppContext| {
+        let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx);
+        cx.open_window(
+            WindowOptions {
+                window_bounds: Some(WindowBounds::Windowed(bounds)),
+                ..Default::default()
+            },
+            |cx| cx.new_view(|_cx| PatternExample),
+        )
+        .unwrap();
+
+        cx.activate(true);
+    });
+}

crates/gpui/src/color.rs 🔗

@@ -553,6 +553,7 @@ impl<'de> Deserialize<'de> for Hsla {
 pub(crate) enum BackgroundTag {
     Solid = 0,
     LinearGradient = 1,
+    PatternSlash = 2,
 }
 
 /// A color space for color interpolation.
@@ -606,6 +607,15 @@ impl Default for Background {
     }
 }
 
+/// Creates a hash pattern background
+pub fn pattern_slash(color: Hsla) -> Background {
+    Background {
+        tag: BackgroundTag::PatternSlash,
+        solid: color,
+        ..Default::default()
+    }
+}
+
 /// Creates a LinearGradient background color.
 ///
 /// The gradient line's angle of direction. A value of `0.` is equivalent to to top; increasing values rotate clockwise from there.
@@ -683,6 +693,7 @@ impl Background {
         match self.tag {
             BackgroundTag::Solid => self.solid.is_transparent(),
             BackgroundTag::LinearGradient => self.colors.iter().all(|c| c.color.is_transparent()),
+            BackgroundTag::PatternSlash => self.solid.is_transparent(),
         }
     }
 }

crates/gpui/src/platform/blade/shaders.wgsl 🔗

@@ -45,6 +45,7 @@ struct LinearColorStop {
 struct Background {
     // 0u is Solid
     // 1u is LinearGradient
+    // 2u is PatternSlash
     tag: u32,
     // 0u is sRGB linear color
     // 1u is Oklab color
@@ -285,7 +286,7 @@ fn prepare_gradient_color(tag: u32, color_space: u32,
     solid: Hsla, colors: array<LinearColorStop, 2>) -> GradientColor {
     var result = GradientColor();
 
-    if (tag == 0u) {
+    if (tag == 0u || tag == 2u) {
         result.solid = hsla_to_rgba(solid);
     } else if (tag == 1u) {
         // The hsla_to_rgba is returns a linear sRGB color
@@ -357,6 +358,22 @@ fn gradient_color(background: Background, position: vec2<f32>, bounds: Bounds,
                 }
             }
         }
+        case 2u: {
+            let base_pattern_size = bounds.size.y / 5.0;
+            let width = base_pattern_size * 0.5;
+            let slash_spacing = 0.89;
+            let radians = M_PI_F / 4.0;
+            let rotation = mat2x2<f32>(
+                cos(radians), -sin(radians),
+                sin(radians), cos(radians)
+            );
+            let relative_position = position - bounds.origin;
+            let rotated_point = rotation * relative_position;
+            let pattern = (rotated_point.x / slash_spacing) % (base_pattern_size * 2.0);
+            let distance = min(pattern, base_pattern_size * 2.0 - pattern) - width;
+            background_color = sold_color;
+            background_color.a *= saturate(0.5 - distance);
+        }
     }
 
     return background_color;

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

@@ -26,7 +26,7 @@ float blur_along_x(float x, float y, float sigma, float corner,
                    float2 half_size);
 float4 over(float4 below, float4 above);
 float radians(float degrees);
-float4 gradient_color(Background background, float2 position, Bounds_ScaledPixels bounds,
+float4 fill_color(Background background, float2 position, Bounds_ScaledPixels bounds,
   float4 solid_color, float4 color0, float4 color1);
 
 struct GradientColor {
@@ -34,7 +34,7 @@ struct GradientColor {
   float4 color0;
   float4 color1;
 };
-GradientColor prepare_gradient_color(uint tag, uint color_space, Hsla solid, Hsla color0, Hsla color1);
+GradientColor prepare_fill_color(uint tag, uint color_space, Hsla solid, Hsla color0, Hsla color1);
 
 struct QuadVertexOutput {
   uint quad_id [[flat]];
@@ -71,7 +71,7 @@ vertex QuadVertexOutput quad_vertex(uint unit_vertex_id [[vertex_id]],
                                                  quad.content_mask.bounds);
   float4 border_color = hsla_to_rgba(quad.border_color);
 
-  GradientColor gradient = prepare_gradient_color(
+  GradientColor gradient = prepare_fill_color(
     quad.background.tag,
     quad.background.color_space,
     quad.background.solid,
@@ -96,7 +96,7 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
   float2 half_size = float2(quad.bounds.size.width, quad.bounds.size.height) / 2.;
   float2 center = float2(quad.bounds.origin.x, quad.bounds.origin.y) + half_size;
   float2 center_to_point = input.position.xy - center;
-  float4 color = gradient_color(quad.background, input.position.xy, quad.bounds,
+  float4 color = fill_color(quad.background, input.position.xy, quad.bounds,
     input.background_solid, input.background_color0, input.background_color1);
 
   // Fast path when the quad is not rounded and doesn't have any border.
@@ -491,7 +491,7 @@ vertex PathSpriteVertexOutput path_sprite_vertex(
       to_device_position(unit_vertex, sprite.bounds, viewport_size);
   float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size);
 
-  GradientColor gradient = prepare_gradient_color(
+  GradientColor gradient = prepare_fill_color(
     sprite.color.tag,
     sprite.color.color_space,
     sprite.color.solid,
@@ -520,7 +520,7 @@ fragment float4 path_sprite_fragment(
   float mask = 1. - abs(1. - fmod(sample.r, 2.));
   PathSprite sprite = sprites[input.sprite_id];
   Background background = sprite.color;
-  float4 color = gradient_color(background, input.position.xy, sprite.bounds,
+  float4 color = fill_color(background, input.position.xy, sprite.bounds,
     input.solid_color, input.color0, input.color1);
   color.a *= mask;
   return color;
@@ -794,10 +794,10 @@ float4 over(float4 below, float4 above) {
   return result;
 }
 
-GradientColor prepare_gradient_color(uint tag, uint color_space, Hsla solid,
+GradientColor prepare_fill_color(uint tag, uint color_space, Hsla solid,
                                      Hsla color0, Hsla color1) {
   GradientColor out;
-  if (tag == 0) {
+  if (tag == 0 || tag == 2) {
     out.solid = hsla_to_rgba(solid);
   } else if (tag == 1) {
     out.color0 = hsla_to_rgba(color0);
@@ -815,7 +815,13 @@ GradientColor prepare_gradient_color(uint tag, uint color_space, Hsla solid,
   return out;
 }
 
-float4 gradient_color(Background background,
+float2x2 rotate2d(float angle) {
+    float s = sin(angle);
+    float c = cos(angle);
+    return float2x2(c, -s, s, c);
+}
+
+float4 fill_color(Background background,
                       float2 position,
                       Bounds_ScaledPixels bounds,
                       float4 solid_color, float4 color0, float4 color1) {
@@ -842,7 +848,7 @@ float4 gradient_color(Background background,
       float2 center = float2(bounds.origin.x, bounds.origin.y) + half_size;
       float2 center_to_point = position - center;
       float t = dot(center_to_point, direction) / length(direction);
-      // Check the direct to determine the use x or y
+      // Check the direction to determine whether to use x or y
       if (abs(direction.x) > abs(direction.y)) {
           t = (t + half_size.x) / bounds.size.width;
       } else {
@@ -867,6 +873,24 @@ float4 gradient_color(Background background,
       }
       break;
     }
+    case 2: {
+        // This pattern is full of magic numbers to make it line up perfectly
+        // when vertically stacked. Make sure you know what you are doing
+        // if you change this!
+
+        float base_pattern_size = bounds.size.height / 5;
+        float width = base_pattern_size * 0.5;
+        float slash_spacing = .89;
+        float radians = M_PI_F / 4.0;
+        float2x2 rotation = rotate2d(radians);
+        float2 relative_position = position - float2(bounds.origin.x, bounds.origin.y);
+        float2 rotated_point = rotation * relative_position;
+        float pattern = fmod(rotated_point.x / slash_spacing, base_pattern_size * 2.0);
+        float distance = min(pattern, base_pattern_size * 2.0 - pattern) - width;
+        color = solid_color;
+        color.a *= saturate(0.5 - distance);
+        break;
+    }
   }
 
   return color;

crates/gpui/src/style.rs 🔗

@@ -582,6 +582,7 @@ impl Style {
                         .first()
                         .map(|stop| stop.color)
                         .unwrap_or_default(),
+                    BackgroundTag::PatternSlash => color.solid,
                 },
                 None => Hsla::default(),
             };