Add `.visible_on_hover` helper method (#3639)

Marshall Bowers created

This PR adds a `.visible_on_hover` helper method that can be used to
make an element only visible on hover.

I noticed we were repeating this similar stanza in a bunch of different
spots:

```rs
some_element
    .invisible()
    .group_hover("", |style| style.visible())
``` 

so it seemed like a nice thing to factor out into a reusable utility.

Release Notes:

- N/A

Change summary

crates/collab_ui2/src/chat_panel.rs           |  8 +---
crates/collab_ui2/src/collab_panel.rs         | 35 ++++++++------------
crates/editor2/src/editor.rs                  |  3 -
crates/ui2/src/components/list/list_header.rs |  3 -
crates/ui2/src/components/list/list_item.rs   |  6 +--
crates/ui2/src/components/tab.rs              |  3 -
crates/ui2/src/prelude.rs                     |  1 
crates/ui2/src/ui2.rs                         |  2 +
crates/ui2/src/visible_on_hover.rs            | 13 +++++++
9 files changed, 37 insertions(+), 37 deletions(-)

Detailed changes

crates/collab_ui2/src/chat_panel.rs 🔗

@@ -21,10 +21,7 @@ use settings::{Settings, SettingsStore};
 use std::sync::Arc;
 use theme::ActiveTheme as _;
 use time::{OffsetDateTime, UtcOffset};
-use ui::{
-    h_stack, prelude::WindowContext, v_stack, Avatar, Button, ButtonCommon as _, Clickable, Icon,
-    IconButton, Label, Tooltip,
-};
+use ui::{prelude::*, Avatar, Button, Icon, IconButton, Label, Tooltip};
 use util::{ResultExt, TryFutureExt};
 use workspace::{
     dock::{DockPosition, Panel, PanelEvent},
@@ -382,12 +379,11 @@ impl ChatPanel {
             .child(text.element("body".into(), cx))
             .child(
                 div()
-                    .invisible()
                     .absolute()
                     .top_1()
                     .right_2()
                     .w_8()
-                    .group_hover("", |this| this.visible())
+                    .visible_on_hover("")
                     .child(render_remove(message_id_to_remove, cx)),
             )
             .into_any()

crates/collab_ui2/src/collab_panel.rs 🔗

@@ -2290,8 +2290,7 @@ impl CollabPanel {
             Section::ActiveCall => channel_link.map(|channel_link| {
                 let channel_link_copy = channel_link.clone();
                 div()
-                    .invisible()
-                    .group_hover("section-header", |this| this.visible())
+                    .visible_on_hover("section-header")
                     .child(
                         IconButton::new("channel-link", Icon::Copy)
                             .icon_size(IconSize::Small)
@@ -2381,21 +2380,17 @@ impl CollabPanel {
                         })
                         .when(!calling, |el| {
                             el.child(
-                                div()
-                                    .id("remove_contact")
-                                    .invisible()
-                                    .group_hover("", |style| style.visible())
-                                    .child(
-                                        IconButton::new("remove_contact", Icon::Close)
-                                            .icon_color(Color::Muted)
-                                            .tooltip(|cx| Tooltip::text("Remove Contact", cx))
-                                            .on_click(cx.listener({
-                                                let github_login = github_login.clone();
-                                                move |this, _, cx| {
-                                                    this.remove_contact(user_id, &github_login, cx);
-                                                }
-                                            })),
-                                    ),
+                                div().visible_on_hover("").child(
+                                    IconButton::new("remove_contact", Icon::Close)
+                                        .icon_color(Color::Muted)
+                                        .tooltip(|cx| Tooltip::text("Remove Contact", cx))
+                                        .on_click(cx.listener({
+                                            let github_login = github_login.clone();
+                                            move |this, _, cx| {
+                                                this.remove_contact(user_id, &github_login, cx);
+                                            }
+                                        })),
+                                ),
                             )
                         }),
                 )
@@ -2626,8 +2621,7 @@ impl CollabPanel {
                             .child(
                                 div()
                                     .id("channel_chat")
-                                    .when(!has_messages_notification, |el| el.invisible())
-                                    .group_hover("", |style| style.visible())
+                                    .when(!has_messages_notification, |el| el.visible_on_hover(""))
                                     .child(
                                         IconButton::new("channel_chat", Icon::MessageBubbles)
                                             .icon_color(if has_messages_notification {
@@ -2644,8 +2638,7 @@ impl CollabPanel {
                             .child(
                                 div()
                                     .id("channel_notes")
-                                    .when(!has_notes_notification, |el| el.invisible())
-                                    .group_hover("", |style| style.visible())
+                                    .when(!has_notes_notification, |el| el.visible_on_hover(""))
                                     .child(
                                         IconButton::new("channel_notes", Icon::File)
                                             .icon_color(if has_notes_notification {

crates/editor2/src/editor.rs 🔗

@@ -9766,8 +9766,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
                         div()
                             .border()
                             .border_color(gpui::red())
-                            .invisible()
-                            .group_hover(group_id, |style| style.visible())
+                            .visible_on_hover(group_id)
                             .child(
                                 IconButton::new(copy_id.clone(), Icon::Copy)
                                     .icon_color(Color::Muted)

crates/ui2/src/components/list/list_header.rs 🔗

@@ -111,10 +111,9 @@ impl RenderOnce for ListHeader {
                 .when_some(self.end_hover_slot, |this, end_hover_slot| {
                     this.child(
                         div()
-                            .invisible()
-                            .group_hover("list_header", |this| this.visible())
                             .absolute()
                             .right_0()
+                            .visible_on_hover("list_header")
                             .child(end_hover_slot),
                     )
                 }),

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

@@ -198,8 +198,7 @@ impl RenderOnce for ListItem {
                             .flex()
                             .absolute()
                             .left(rems(-1.))
-                            .invisible()
-                            .group_hover("", |style| style.visible())
+                            .visible_on_hover("")
                             .child(Disclosure::new(is_open).on_toggle(self.on_toggle))
                     }))
                     .child(
@@ -226,8 +225,7 @@ impl RenderOnce for ListItem {
                                 .absolute()
                                 .right_2()
                                 .top_0()
-                                .invisible()
-                                .group_hover("list_item", |this| this.visible())
+                                .visible_on_hover("list_item")
                                 .child(end_hover_slot),
                         )
                     }),

crates/ui2/src/components/tab.rs 🔗

@@ -158,7 +158,6 @@ impl RenderOnce for Tab {
                     )
                     .child(
                         h_stack()
-                            .invisible()
                             .w_3()
                             .h_3()
                             .justify_center()
@@ -167,7 +166,7 @@ impl RenderOnce for Tab {
                                 TabCloseSide::Start => this.left_1(),
                                 TabCloseSide::End => this.right_1(),
                             })
-                            .group_hover("", |style| style.visible())
+                            .visible_on_hover("")
                             .children(self.end_slot),
                     )
                     .children(self.children),

crates/ui2/src/prelude.rs 🔗

@@ -9,6 +9,7 @@ pub use crate::clickable::*;
 pub use crate::disableable::*;
 pub use crate::fixed::*;
 pub use crate::selectable::*;
+pub use crate::visible_on_hover::*;
 pub use crate::{h_stack, v_stack};
 pub use crate::{Button, ButtonSize, ButtonStyle, IconButton};
 pub use crate::{ButtonCommon, Color, StyledExt};

crates/ui2/src/ui2.rs 🔗

@@ -21,6 +21,7 @@ mod selectable;
 mod styled_ext;
 mod styles;
 pub mod utils;
+mod visible_on_hover;
 
 pub use clickable::*;
 pub use components::*;
@@ -30,3 +31,4 @@ pub use prelude::*;
 pub use selectable::*;
 pub use styled_ext::*;
 pub use styles::*;
+pub use visible_on_hover::*;

crates/ui2/src/visible_on_hover.rs 🔗

@@ -0,0 +1,13 @@
+use gpui::{InteractiveElement, SharedString, Styled};
+
+pub trait VisibleOnHover: InteractiveElement + Styled + Sized {
+    /// Sets the element to only be visible when the specified group is hovered.
+    ///
+    /// Pass `""` as the `group_name` to use the global group.
+    fn visible_on_hover(self, group_name: impl Into<SharedString>) -> Self {
+        self.invisible()
+            .group_hover(group_name, |style| style.visible())
+    }
+}
+
+impl<E: InteractiveElement + Styled> VisibleOnHover for E {}