From e5943975f9e382f84762c251da4f2afbf3680fb5 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 29 Jan 2025 12:18:34 -0500 Subject: [PATCH] gpui & ui: Use shader for dashed dividers (#23839) TODO: - [x] BackgroundOrientation - [x] PatternDash - [x] `pattern_horizontal_dash` & `pattern_vertical_dash` - [x] Metal dash shader - [x] Blade dash shader - [x] Update ui::Divider to use new pattern --- This PR introduces proper dashed dividers using the new `PatternDash` background shader. ![CleanShot 2025-01-29 at 09 33 06@2x](https://github.com/user-attachments/assets/2db5af58-1aa9-4ad7-aa52-b9046fbf8584) Before this we were using 128 elements to create a dashed divider, which is both expensive, and would not scale beyond a certain size. This allows us to simplify the divider element as well. Changes: - Adds `BackgroundOrientation` to `gpui::color::Background` to allow specifying a direction for a pattern - Adds the PatternDash pattern variant - Updates `ui::Divider`'s dashed variants to be more efficient Misc: - Documents the `ui::Divider` component - Treat `.metal` files as `C` in the Zed project until we get some metal syntax highlighting. Release Notes: - N/A --- .zed/settings.json | 1 + crates/gpui/examples/pattern.rs | 57 ++++++- crates/gpui/src/color.rs | 44 +++++ crates/gpui/src/platform/blade/shaders.wgsl | 16 ++ crates/gpui/src/platform/mac/shaders.metal | 25 ++- crates/gpui/src/style.rs | 1 + crates/ui/src/components/divider.rs | 170 +++++++++----------- crates/workspace/src/theme_preview.rs | 2 +- 8 files changed, 210 insertions(+), 106 deletions(-) diff --git a/.zed/settings.json b/.zed/settings.json index 41adfdbf59..68b9ef5d70 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -39,6 +39,7 @@ } }, "file_types": { + "C": ["metal"], "Dockerfile": ["Dockerfile*[!dockerignore]"], "Git Ignore": ["dockerignore"] }, diff --git a/crates/gpui/examples/pattern.rs b/crates/gpui/examples/pattern.rs index b872d6b6ad..f4c3cf9b67 100644 --- a/crates/gpui/examples/pattern.rs +++ b/crates/gpui/examples/pattern.rs @@ -1,6 +1,7 @@ use gpui::{ - div, linear_color_stop, linear_gradient, pattern_slash, prelude::*, px, rgb, size, App, - AppContext, Application, Bounds, Context, Window, WindowBounds, WindowOptions, + div, linear_color_stop, linear_gradient, pattern_horizontal_dash, pattern_slash, + pattern_vertical_dash, prelude::*, px, rgb, size, App, AppContext, Application, Bounds, + Context, Window, WindowBounds, WindowOptions, }; struct PatternExample; @@ -19,6 +20,58 @@ impl Render for PatternExample { .text_xl() .text_color(rgb(0x000000)) .child("Pattern Example") + .child( + div() + .flex() + .gap_4() + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .w(px(160.0)) + .h(px(1.0)) + .bg(pattern_horizontal_dash(gpui::red())), + ) + .child( + div() + .w(px(160.0)) + .h(px(4.0)) + .bg(pattern_horizontal_dash(gpui::red())), + ) + .child( + div() + .w(px(160.0)) + .h(px(8.0)) + .bg(pattern_horizontal_dash(gpui::red())), + ), + ) + .child( + div() + .flex() + .gap_1() + .child( + div() + .w(px(1.0)) + .h(px(160.0)) + .bg(pattern_vertical_dash(gpui::blue())), + ) + .child( + div() + .w(px(4.0)) + .h(px(160.0)) + .bg(pattern_vertical_dash(gpui::blue())), + ) + .child( + div() + .w(px(8.0)) + .h(px(160.0)) + .bg(pattern_vertical_dash(gpui::blue())), + ), + ), + ) .child( div() .flex() diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index 230eca3e6b..afade1bd02 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -548,12 +548,33 @@ impl<'de> Deserialize<'de> for Hsla { } } +/// The orientation of a background. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +#[repr(C)] +pub enum BackgroundOrientation { + /// The background is oriented horizontally. + #[default] + Horizontal = 0, + /// The background is oriented vertically. + Vertical = 1, +} + +impl Display for BackgroundOrientation { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + BackgroundOrientation::Horizontal => write!(f, "Horizontal"), + BackgroundOrientation::Vertical => write!(f, "Vertical"), + } + } +} + #[derive(Debug, Clone, Copy, PartialEq)] #[repr(C)] pub(crate) enum BackgroundTag { Solid = 0, LinearGradient = 1, PatternSlash = 2, + PatternDash = 3, } /// A color space for color interpolation. @@ -589,6 +610,7 @@ pub struct Background { pub(crate) solid: Hsla, pub(crate) angle: f32, pub(crate) colors: [LinearColorStop; 2], + pub(crate) orientation: BackgroundOrientation, /// Padding for alignment for repr(C) layout. pad: u32, } @@ -602,6 +624,7 @@ impl Default for Background { color_space: ColorSpace::default(), angle: 0.0, colors: [LinearColorStop::default(), LinearColorStop::default()], + orientation: BackgroundOrientation::default(), pad: 0, } } @@ -616,6 +639,26 @@ pub fn pattern_slash(color: Hsla) -> Background { } } +/// Creates a dash pattern background +pub fn pattern_horizontal_dash(color: Hsla) -> Background { + Background { + tag: BackgroundTag::PatternDash, + orientation: BackgroundOrientation::Horizontal, + solid: color, + ..Default::default() + } +} + +/// Creates a vertical dash pattern background +pub fn pattern_vertical_dash(color: Hsla) -> Background { + Background { + tag: BackgroundTag::PatternDash, + solid: color, + orientation: BackgroundOrientation::Vertical, + ..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. @@ -694,6 +737,7 @@ impl Background { BackgroundTag::Solid => self.solid.is_transparent(), BackgroundTag::LinearGradient => self.colors.iter().all(|c| c.color.is_transparent()), BackgroundTag::PatternSlash => self.solid.is_transparent(), + BackgroundTag::PatternDash => self.solid.is_transparent(), } } } diff --git a/crates/gpui/src/platform/blade/shaders.wgsl b/crates/gpui/src/platform/blade/shaders.wgsl index b41ffb26ef..32f386d231 100644 --- a/crates/gpui/src/platform/blade/shaders.wgsl +++ b/crates/gpui/src/platform/blade/shaders.wgsl @@ -359,6 +359,7 @@ fn gradient_color(background: Background, position: vec2, bounds: Bounds, } } case 2u: { + // Slash pattern let base_pattern_size = bounds.size.y / 5.0; let width = base_pattern_size * 0.5; let slash_spacing = 0.89; @@ -374,6 +375,21 @@ fn gradient_color(background: Background, position: vec2, bounds: Bounds, background_color = sold_color; background_color.a *= saturate(0.5 - distance); } + case 3u: { + // Dash pattern + let dash_width = 8.0; + let gap_width = 8.0; + let pattern_width = dash_width + gap_width; + let relative_position = position - bounds.origin; + + // Use a dot product to select x or y based on orientation + let orientation_vector = vec2(1.0 - f32(background.angle != 0.0), f32(background.angle != 0.0)); + let pattern_position = fmod(dot(relative_position, orientation_vector), pattern_width); + + let distance = pattern_position - dash_width; + background_color = sold_color; + background_color.a *= step(-distance, 0.0); + } } return background_color; diff --git a/crates/gpui/src/platform/mac/shaders.metal b/crates/gpui/src/platform/mac/shaders.metal index 941c20c33d..c2bdeb2d11 100644 --- a/crates/gpui/src/platform/mac/shaders.metal +++ b/crates/gpui/src/platform/mac/shaders.metal @@ -797,7 +797,7 @@ float4 over(float4 below, float4 above) { GradientColor prepare_fill_color(uint tag, uint color_space, Hsla solid, Hsla color0, Hsla color1) { GradientColor out; - if (tag == 0 || tag == 2) { + if (tag == 0 || tag == 2 || tag == 3) { out.solid = hsla_to_rgba(solid); } else if (tag == 1) { out.color0 = hsla_to_rgba(color0); @@ -874,13 +874,10 @@ float4 fill_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! - + // Slash pattern float base_pattern_size = bounds.size.height / 5; float width = base_pattern_size * 0.5; - float slash_spacing = .89; + float slash_spacing = .89; // exact number to make vertical elements line up float radians = M_PI_F / 4.0; float2x2 rotation = rotate2d(radians); float2 relative_position = position - float2(bounds.origin.x, bounds.origin.y); @@ -891,6 +888,22 @@ float4 fill_color(Background background, color.a *= saturate(0.5 - distance); break; } + case 3: { + // Dash pattern + float dash_width = 8.0; + float gap_width = 8.0; + float pattern_width = dash_width + gap_width; + float2 relative_position = position - float2(bounds.origin.x, bounds.origin.y); + + // Use a dot product to select x or y based on orientation + float2 orientation_vector = float2(1.0 - background.orientation, background.orientation); + float pattern_position = fmod(dot(relative_position, orientation_vector), pattern_width); + + float distance = pattern_position - dash_width; + color = solid_color; + color.a *= step(-distance, 0.0); + break; + } } return color; diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 39175b44c5..f9541b8ec7 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -583,6 +583,7 @@ impl Style { .map(|stop| stop.color) .unwrap_or_default(), BackgroundTag::PatternSlash => color.solid, + BackgroundTag::PatternDash => color.solid, }, None => Hsla::default(), }; diff --git a/crates/ui/src/components/divider.rs b/crates/ui/src/components/divider.rs index 2a0f87a610..f3c9c94134 100644 --- a/crates/ui/src/components/divider.rs +++ b/crates/ui/src/components/divider.rs @@ -1,14 +1,7 @@ -#![allow(missing_docs)] -use gpui::{Hsla, IntoElement}; +use gpui::{pattern_horizontal_dash, pattern_vertical_dash, Background, Hsla, IntoElement}; use crate::prelude::*; -#[derive(Clone, Copy, PartialEq)] -enum DividerStyle { - Solid, - Dashed, -} - #[derive(Clone, Copy, PartialEq)] enum DividerDirection { Horizontal, @@ -18,12 +11,15 @@ enum DividerDirection { /// The color of a [`Divider`]. #[derive(Default)] pub enum DividerColor { - Border, + /// The default border color. #[default] + Border, + /// Usually a de-emphasized border color. BorderVariant, } impl DividerColor { + /// Returns the divider's HSLA color. pub fn hsla(self, cx: &mut App) -> Hsla { match self { DividerColor::Border => cx.theme().colors().border, @@ -32,71 +28,29 @@ impl DividerColor { } } +/// A component that can be used to separate sections of content. +/// +/// Can be rendered horizontally or vertically. #[derive(IntoElement)] pub struct Divider { - style: DividerStyle, direction: DividerDirection, color: DividerColor, inset: bool, + is_dashed: bool, } impl RenderOnce for Divider { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - match self.style { - DividerStyle::Solid => self.render_solid(cx).into_any_element(), - DividerStyle::Dashed => self.render_dashed(cx).into_any_element(), - } - } -} + let color = self.color.hsla(cx); + let background = if self.is_dashed { + match self.direction { + DividerDirection::Horizontal => pattern_horizontal_dash(color), + DividerDirection::Vertical => pattern_vertical_dash(color), + } + } else { + Background::from(color) + }; -impl Divider { - pub fn horizontal() -> Self { - Self { - style: DividerStyle::Solid, - direction: DividerDirection::Horizontal, - color: DividerColor::default(), - inset: false, - } - } - - pub fn vertical() -> Self { - Self { - style: DividerStyle::Solid, - direction: DividerDirection::Vertical, - color: DividerColor::default(), - inset: false, - } - } - - pub fn horizontal_dashed() -> Self { - Self { - style: DividerStyle::Dashed, - direction: DividerDirection::Horizontal, - color: DividerColor::default(), - inset: false, - } - } - - pub fn vertical_dashed() -> Self { - Self { - style: DividerStyle::Dashed, - direction: DividerDirection::Vertical, - color: DividerColor::default(), - inset: false, - } - } - - pub fn inset(mut self) -> Self { - self.inset = true; - self - } - - pub fn color(mut self, color: DividerColor) -> Self { - self.color = color; - self - } - - pub fn render_solid(self, cx: &mut App) -> impl IntoElement { div() .map(|this| match self.direction { DividerDirection::Horizontal => { @@ -106,38 +60,60 @@ impl Divider { this.w_px().h_full().when(self.inset, |this| this.my_1p5()) } }) - .bg(self.color.hsla(cx)) - } - - // TODO: Use canvas or a shader here - // This obviously is a short term approach - pub fn render_dashed(self, cx: &mut App) -> impl IntoElement { - let segment_count = 128; - let segment_count_f = segment_count as f32; - let segment_min_w = 6.; - let base = match self.direction { - DividerDirection::Horizontal => h_flex(), - DividerDirection::Vertical => v_flex(), - }; - let (w, h) = match self.direction { - DividerDirection::Horizontal => (px(segment_min_w), px(1.)), - DividerDirection::Vertical => (px(1.), px(segment_min_w)), - }; - let color = self.color.hsla(cx); - let total_min_w = segment_min_w * segment_count_f * 2.; // * 2 because of the gap - - base.min_w(px(total_min_w)) - .map(|this| { - if self.direction == DividerDirection::Horizontal { - this.w_full().h_px() - } else { - this.w_px().h_full() - } - }) - .gap(px(segment_min_w)) - .overflow_hidden() - .children( - (0..segment_count).map(|_| div().flex_grow().flex_shrink_0().w(w).h(h).bg(color)), - ) + .bg(background) + } +} + +impl Divider { + /// Creates a solid horizontal divider. + pub fn horizontal() -> Self { + Self { + direction: DividerDirection::Horizontal, + color: DividerColor::default(), + inset: false, + is_dashed: false, + } + } + + /// Creates a solid vertical divider. + pub fn vertical() -> Self { + Self { + direction: DividerDirection::Vertical, + color: DividerColor::default(), + inset: false, + is_dashed: false, + } + } + + /// Creates a dashed horizontal divider. + pub fn horizontal_dashed() -> Self { + Self { + direction: DividerDirection::Horizontal, + color: DividerColor::default(), + inset: false, + is_dashed: true, + } + } + + /// Creates a dashed vertical divider. + pub fn vertical_dashed() -> Self { + Self { + direction: DividerDirection::Vertical, + color: DividerColor::default(), + inset: false, + is_dashed: true, + } + } + + /// Pads the divider with a margin. + pub fn inset(mut self) -> Self { + self.inset = true; + self + } + + /// Sets the color of the divider. + pub fn color(mut self, color: DividerColor) -> Self { + self.color = color; + self } } diff --git a/crates/workspace/src/theme_preview.rs b/crates/workspace/src/theme_preview.rs index 5062446fe5..a4df48f958 100644 --- a/crates/workspace/src/theme_preview.rs +++ b/crates/workspace/src/theme_preview.rs @@ -5,7 +5,7 @@ use theme::all_theme_colors; use ui::{ element_cell, prelude::*, string_cell, utils::calculate_contrast_ratio, AudioStatus, Availability, Avatar, AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike, - Checkbox, CheckboxWithLabel, ContentGroup, DecoratedIcon, ElevationIndex, Facepile, + Checkbox, CheckboxWithLabel, ContentGroup, DecoratedIcon, Divider, ElevationIndex, Facepile, IconDecoration, Indicator, Switch, Table, TintColor, Tooltip, };