From 23672987ff5af1c3946f369245a70631634cd114 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 28 Jan 2025 11:33:34 -0500 Subject: [PATCH] gpui: Add support for slash pattern fills (`///`) (#23576) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/gpui/Cargo.toml | 29 +++--- 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, 183 insertions(+), 24 deletions(-) create mode 100644 crates/gpui/examples/pattern.rs diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 6e904b7c97..760958b0e9 100644 --- a/crates/gpui/Cargo.toml +++ b/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" @@ -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" diff --git a/crates/gpui/examples/pattern.rs b/crates/gpui/examples/pattern.rs new file mode 100644 index 0000000000..6408c78b63 --- /dev/null +++ b/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) -> 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); + }); +} diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index 19182b088b..230eca3e6b 100644 --- a/crates/gpui/src/color.rs +++ b/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(), } } } diff --git a/crates/gpui/src/platform/blade/shaders.wgsl b/crates/gpui/src/platform/blade/shaders.wgsl index d497c40d7a..b41ffb26ef 100644 --- a/crates/gpui/src/platform/blade/shaders.wgsl +++ b/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) -> 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, 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( + 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; diff --git a/crates/gpui/src/platform/mac/shaders.metal b/crates/gpui/src/platform/mac/shaders.metal index 7ee5d63add..941c20c33d 100644 --- a/crates/gpui/src/platform/mac/shaders.metal +++ b/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; diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index df11f97f9d..39175b44c5 100644 --- a/crates/gpui/src/style.rs +++ b/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(), };