ui: Add `GradientFade` component (#51113)

Danilo Leal created

Just adding this here as an utility component given we were doing
similar things on the sidebar, thread item, and list item. It'd be
probably useful, in the near future, to give this more methods so it's
more flexible.

Release Notes:

- N/A

Change summary

crates/sidebar/src/sidebar.rs              | 42 +++--------
crates/ui/src/components.rs                |  2 
crates/ui/src/components/ai/thread_item.rs | 30 ++-----
crates/ui/src/components/gradient_fade.rs  | 88 ++++++++++++++++++++++++
crates/ui/src/components/list/list_item.rs | 41 ++---------
5 files changed, 118 insertions(+), 85 deletions(-)

Detailed changes

crates/sidebar/src/sidebar.rs 🔗

@@ -7,8 +7,8 @@ use editor::{Editor, EditorElement, EditorStyle};
 use feature_flags::{AgentV2FeatureFlag, FeatureFlagViewExt as _};
 use gpui::{
     AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, FontStyle, ListState,
-    Pixels, Render, SharedString, TextStyle, WeakEntity, Window, actions, linear_color_stop,
-    linear_gradient, list, prelude::*, px, relative, rems,
+    Pixels, Render, SharedString, TextStyle, WeakEntity, Window, actions, list, prelude::*, px,
+    relative, rems,
 };
 use menu::{Cancel, Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
 use project::Event as ProjectEvent;
@@ -18,8 +18,8 @@ use std::mem;
 use theme::{ActiveTheme, ThemeSettings};
 use ui::utils::TRAFFIC_LIGHT_PADDING;
 use ui::{
-    AgentThreadStatus, HighlightedLabel, IconButtonShape, KeyBinding, ListItem, Tab, ThreadItem,
-    Tooltip, WithScrollbar, prelude::*,
+    AgentThreadStatus, GradientFade, HighlightedLabel, IconButtonShape, KeyBinding, ListItem, Tab,
+    ThreadItem, Tooltip, WithScrollbar, prelude::*,
 };
 use util::path_list::PathList;
 use workspace::{
@@ -803,33 +803,13 @@ impl Sidebar {
         };
 
         let color = cx.theme().colors();
-        let base_bg = color.panel_background;
-        let gradient_overlay = div()
-            .id("gradient_overlay")
-            .absolute()
-            .top_0()
-            .right_0()
-            .w_12()
-            .h_full()
-            .bg(linear_gradient(
-                90.,
-                linear_color_stop(base_bg, 0.6),
-                linear_color_stop(base_bg.opacity(0.0), 0.),
-            ))
-            .group_hover(group_name.clone(), |s| {
-                s.bg(linear_gradient(
-                    90.,
-                    linear_color_stop(color.element_hover, 0.6),
-                    linear_color_stop(color.element_hover.opacity(0.0), 0.),
-                ))
-            })
-            .group_active(group_name.clone(), |s| {
-                s.bg(linear_gradient(
-                    90.,
-                    linear_color_stop(color.element_active, 0.6),
-                    linear_color_stop(color.element_active.opacity(0.0), 0.),
-                ))
-            });
+        let gradient_overlay = GradientFade::new(
+            color.panel_background,
+            color.element_hover,
+            color.element_active,
+        )
+        .width(px(48.0))
+        .group_name(group_name.clone());
 
         ListItem::new(id)
             .group_name(group_name)

crates/ui/src/components.rs 🔗

@@ -12,6 +12,7 @@ mod disclosure;
 mod divider;
 mod dropdown_menu;
 mod facepile;
+mod gradient_fade;
 mod group;
 mod icon;
 mod image;
@@ -54,6 +55,7 @@ pub use disclosure::*;
 pub use divider::*;
 pub use dropdown_menu::*;
 pub use facepile::*;
+pub use gradient_fade::*;
 pub use group::*;
 pub use icon::*;
 pub use image::*;

crates/ui/src/components/ai/thread_item.rs 🔗

@@ -1,9 +1,9 @@
 use crate::{
-    DecoratedIcon, DiffStat, HighlightedLabel, IconDecoration, IconDecorationKind, SpinnerLabel,
-    prelude::*,
+    DecoratedIcon, DiffStat, GradientFade, HighlightedLabel, IconDecoration, IconDecorationKind,
+    SpinnerLabel, prelude::*,
 };
 
-use gpui::{AnyView, ClickEvent, Hsla, SharedString, linear_color_stop, linear_gradient};
+use gpui::{AnyView, ClickEvent, Hsla, SharedString};
 
 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
 pub enum AgentThreadStatus {
@@ -220,24 +220,12 @@ impl RenderOnce for ThreadItem {
             color.panel_background
         };
 
-        let gradient_overlay = div()
-            .absolute()
-            .top_0()
-            .right(px(-10.0))
-            .w_8()
-            .h_full()
-            .bg(linear_gradient(
-                90.,
-                linear_color_stop(base_bg, 0.8),
-                linear_color_stop(base_bg.opacity(0.0), 0.),
-            ))
-            .group_hover("thread-item", |s| {
-                s.bg(linear_gradient(
-                    90.,
-                    linear_color_stop(color.element_hover, 0.8),
-                    linear_color_stop(color.element_hover.opacity(0.0), 0.),
-                ))
-            });
+        let gradient_overlay =
+            GradientFade::new(base_bg, color.element_hover, color.element_active)
+                .width(px(32.0))
+                .right(px(-10.0))
+                .gradient_stop(0.8)
+                .group_name("thread-item");
 
         v_flex()
             .id(self.id.clone())

crates/ui/src/components/gradient_fade.rs 🔗

@@ -0,0 +1,88 @@
+use gpui::{Hsla, Pixels, SharedString, linear_color_stop, linear_gradient, px};
+
+use crate::prelude::*;
+
+/// A gradient overlay that fades from a solid color to transparent.
+#[derive(IntoElement)]
+pub struct GradientFade {
+    base_bg: Hsla,
+    hover_bg: Hsla,
+    active_bg: Hsla,
+    width: Pixels,
+    right: Pixels,
+    gradient_stop: f32,
+    group_name: Option<SharedString>,
+}
+
+impl GradientFade {
+    pub fn new(base_bg: Hsla, hover_bg: Hsla, active_bg: Hsla) -> Self {
+        Self {
+            base_bg,
+            hover_bg,
+            active_bg,
+            width: px(48.0),
+            right: px(0.0),
+            gradient_stop: 0.6,
+            group_name: None,
+        }
+    }
+
+    pub fn width(mut self, width: Pixels) -> Self {
+        self.width = width;
+        self
+    }
+
+    pub fn right(mut self, right: Pixels) -> Self {
+        self.right = right;
+        self
+    }
+
+    pub fn gradient_stop(mut self, stop: f32) -> Self {
+        self.gradient_stop = stop;
+        self
+    }
+
+    pub fn group_name(mut self, name: impl Into<SharedString>) -> Self {
+        self.group_name = Some(name.into());
+        self
+    }
+}
+
+impl RenderOnce for GradientFade {
+    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
+        let stop = self.gradient_stop;
+        let hover_bg = self.hover_bg;
+        let active_bg = self.active_bg;
+
+        div()
+            .id("gradient_fade")
+            .absolute()
+            .top_0()
+            .right(self.right)
+            .w(self.width)
+            .h_full()
+            .bg(linear_gradient(
+                90.,
+                linear_color_stop(self.base_bg, stop),
+                linear_color_stop(self.base_bg.opacity(0.0), 0.),
+            ))
+            .when_some(self.group_name.clone(), |element, group_name| {
+                element.group_hover(group_name, move |s| {
+                    s.bg(linear_gradient(
+                        90.,
+                        linear_color_stop(hover_bg, stop),
+                        linear_color_stop(hover_bg.opacity(0.0), 0.),
+                    ))
+                })
+            })
+            .when_some(self.group_name, |element, group_name| {
+                element.group_active(group_name, move |s| {
+                    s.bg(linear_gradient(
+                        90.,
+                        linear_color_stop(active_bg, stop),
+                        linear_color_stop(active_bg.opacity(0.0), 0.),
+                    ))
+                })
+            })
+    }
+}

crates/ui/src/components/list/list_item.rs 🔗

@@ -1,13 +1,10 @@
 use std::sync::Arc;
 
 use component::{Component, ComponentScope, example_group_with_title, single_example};
-use gpui::{
-    AnyElement, AnyView, ClickEvent, MouseButton, MouseDownEvent, Pixels, linear_color_stop,
-    linear_gradient, px,
-};
+use gpui::{AnyElement, AnyView, ClickEvent, MouseButton, MouseDownEvent, Pixels, px};
 use smallvec::SmallVec;
 
-use crate::{Disclosure, prelude::*};
+use crate::{Disclosure, GradientFade, prelude::*};
 
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
 pub enum ListItemSpacing {
@@ -220,34 +217,12 @@ impl RenderOnce for ListItem {
             color.panel_background
         };
 
-        let end_hover_gradient_overlay = div()
-            .id("gradient_overlay")
-            .absolute()
-            .top_0()
-            .right_0()
-            .w_24()
-            .h_full()
-            .bg(linear_gradient(
-                90.,
-                linear_color_stop(base_bg, 0.6),
-                linear_color_stop(base_bg.opacity(0.0), 0.),
-            ))
-            .when_some(self.group_name.clone(), |s, group_name| {
-                s.group_hover(group_name.clone(), |s| {
-                    s.bg(linear_gradient(
-                        90.,
-                        linear_color_stop(color.element_hover, 0.6),
-                        linear_color_stop(color.element_hover.opacity(0.0), 0.),
-                    ))
-                })
-                .group_active(group_name, |s| {
-                    s.bg(linear_gradient(
-                        90.,
-                        linear_color_stop(color.element_active, 0.6),
-                        linear_color_stop(color.element_active.opacity(0.0), 0.),
-                    ))
-                })
-            });
+        let end_hover_gradient_overlay =
+            GradientFade::new(base_bg, color.element_hover, color.element_active)
+                .width(px(96.0))
+                .when_some(self.group_name.clone(), |fade, group| {
+                    fade.group_name(group)
+                });
 
         h_flex()
             .id(self.id)