Add hover behaviour to tabs

Conrad Irwin , Marshall , and Nathan created

Co-Authored-By: Marshall <marshall@zed.dev>
Co-Authored-By: Nathan <nathan@zed.dev>

Change summary

crates/gpui2/src/element.rs      | 13 +++++++++++++
crates/gpui2/src/elements/div.rs |  8 ++++++--
crates/gpui2/src/style.rs        | 11 +++++++++++
crates/gpui2/src/styled.rs       | 21 +++++++++++++++++++++
crates/ui2/src/elements/icon.rs  | 13 +++++++++++--
crates/workspace2/src/pane.rs    | 10 +++++++++-
6 files changed, 71 insertions(+), 5 deletions(-)

Detailed changes

crates/gpui2/src/element.rs 🔗

@@ -212,6 +212,19 @@ pub trait Component<V> {
     {
         self.map(|this| if condition { then(this) } else { this })
     }
+
+    fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
+    where
+        Self: Sized,
+    {
+        self.map(|this| {
+            if let Some(value) = option {
+                then(this, value)
+            } else {
+                this
+            }
+        })
+    }
 }
 
 impl<V> Component<V> for AnyElement<V> {

crates/gpui2/src/elements/div.rs 🔗

@@ -3,7 +3,7 @@ use crate::{
     ElementInteraction, FocusDisabled, FocusEnabled, FocusHandle, FocusListeners, Focusable,
     GlobalElementId, GroupBounds, InteractiveElementState, LayoutId, Overflow, ParentElement,
     Pixels, Point, SharedString, StatefulInteraction, StatefulInteractive, StatelessInteraction,
-    StatelessInteractive, Style, StyleRefinement, Styled, ViewContext,
+    StatelessInteractive, Style, StyleRefinement, Styled, ViewContext, Visibility,
 };
 use refineable::Refineable;
 use smallvec::SmallVec;
@@ -249,11 +249,15 @@ where
         cx: &mut ViewContext<V>,
     ) {
         self.with_element_id(cx, |this, _global_id, cx| {
+            let style = this.compute_style(bounds, element_state, cx);
+            if style.visibility == Visibility::Hidden {
+                return;
+            }
+
             if let Some(group) = this.group.clone() {
                 GroupBounds::push(group, bounds, cx);
             }
 
-            let style = this.compute_style(bounds, element_state, cx);
             let z_index = style.z_index.unwrap_or(0);
 
             let mut child_min = point(Pixels::MAX, Pixels::MAX);

crates/gpui2/src/style.rs 🔗

@@ -19,6 +19,9 @@ pub struct Style {
     /// What layout strategy should be used?
     pub display: Display,
 
+    /// Should the element be painted on screen?
+    pub visibility: Visibility,
+
     // Overflow properties
     /// How children overflowing their container should affect layout
     #[refineable]
@@ -107,6 +110,13 @@ impl Styled for StyleRefinement {
     }
 }
 
+#[derive(Default, Clone, Copy, Debug, Eq, PartialEq)]
+pub enum Visibility {
+    #[default]
+    Visible,
+    Hidden,
+}
+
 #[derive(Clone, Debug)]
 pub struct BoxShadow {
     pub color: Hsla,
@@ -297,6 +307,7 @@ impl Default for Style {
     fn default() -> Self {
         Style {
             display: Display::Block,
+            visibility: Visibility::Visible,
             overflow: Point {
                 x: Overflow::Visible,
                 y: Overflow::Visible,

crates/gpui2/src/styled.rs 🔗

@@ -1,6 +1,7 @@
 use crate::{
     self as gpui2, hsla, point, px, relative, rems, AlignItems, DefiniteLength, Display, Fill,
     FlexDirection, Hsla, JustifyContent, Length, Position, Rems, SharedString, StyleRefinement,
+    Visibility,
 };
 use crate::{BoxShadow, TextStyleRefinement};
 use smallvec::smallvec;
@@ -60,6 +61,26 @@ pub trait Styled {
         self
     }
 
+    /// Sets the visibility of the element to `visible`.
+    /// [Docs](https://tailwindcss.com/docs/visibility)
+    fn visible(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().visibility = Some(Visibility::Visible);
+        self
+    }
+
+    /// Sets the visibility of the element to `hidden`.
+    /// [Docs](https://tailwindcss.com/docs/visibility)
+    fn invisible(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().visibility = Some(Visibility::Hidden);
+        self
+    }
+
     /// Sets the flex direction of the element to `column`.
     /// [Docs](https://tailwindcss.com/docs/flex-direction#column)
     fn flex_col(mut self) -> Self

crates/ui2/src/elements/icon.rs 🔗

@@ -159,6 +159,7 @@ impl Icon {
 pub struct IconElement {
     icon: Icon,
     color: IconColor,
+    hover_color: Option<IconColor>,
     size: IconSize,
 }
 
@@ -167,6 +168,7 @@ impl IconElement {
         Self {
             icon,
             color: IconColor::default(),
+            hover_color: None,
             size: IconSize::default(),
         }
     }
@@ -176,13 +178,17 @@ impl IconElement {
         self
     }
 
+    pub fn hover_color(mut self, hover_color: impl Into<Option<IconColor>>) -> Self {
+        self.hover_color = hover_color.into();
+        self
+    }
+
     pub fn size(mut self, size: IconSize) -> Self {
         self.size = size;
         self
     }
 
     fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        let fill = self.color.color(cx);
         let svg_size = match self.size {
             IconSize::Small => rems(0.75),
             IconSize::Medium => rems(0.9375),
@@ -192,7 +198,10 @@ impl IconElement {
             .size(svg_size)
             .flex_none()
             .path(self.icon.path())
-            .text_color(fill)
+            .text_color(self.color.color(cx))
+            .when_some(self.hover_color, |this, hover_color| {
+                this.hover(|style| style.text_color(hover_color.color(cx)))
+            })
     }
 }
 

crates/workspace2/src/pane.rs 🔗

@@ -1361,9 +1361,16 @@ impl Pane {
         let label = item.tab_content(Some(detail), cx);
         let close_icon = || {
             let id = item.id();
+
             div()
                 .id(item.id())
-                .child(IconElement::new(Icon::Close).color(IconColor::Muted))
+                .invisible()
+                .group_hover("", |style| style.visible())
+                .child(
+                    IconElement::new(Icon::Close)
+                        .color(IconColor::Muted)
+                        .hover_color(IconColor::Accent),
+                )
                 .on_click(move |pane: &mut Self, _, cx| {
                     pane.close_item_by_id(id, SaveIntent::Close, cx)
                         .detach_and_log_err(cx);
@@ -1388,6 +1395,7 @@ impl Pane {
         let close_right = ItemSettings::get_global(cx).close_position.right();
 
         div()
+            .group("")
             .id(item.id())
             // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx))
             // .drag_over::<DraggedTab>(|d| d.bg(cx.theme().colors().element_drop_target))