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

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>
This commit is contained in:
Nate Butler 2025-01-28 11:33:34 -05:00 committed by GitHub
parent 070890d361
commit 23672987ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 183 additions and 24 deletions

View file

@ -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"
@ -221,18 +220,22 @@ path = "examples/hello_world.rs"
name = "image"
path = "examples/image/image.rs"
[[example]]
name = "set_menus"
path = "examples/set_menus.rs"
[[example]]
name = "window_shadow"
path = "examples/window_shadow.rs"
[[example]]
name = "input"
path = "examples/input.rs"
[[example]]
name = "opacity"
path = "examples/opacity.rs"
[[example]]
name = "pattern"
path = "examples/pattern.rs"
[[example]]
name = "set_menus"
path = "examples/set_menus.rs"
[[example]]
name = "shadow"
path = "examples/shadow.rs"
@ -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"

View file

@ -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);
});
}

View file

@ -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(),
}
}
}

View file

@ -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;

View file

@ -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;

View file

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