Skip to main content

ui/
hover_card.rs

1//! [`HoverCard`] — the richer sibling of [`crate::tooltip::Tooltip`]: a card
2//! that opens on hover and can itself be hovered, for a preview the pointer
3//! can travel into (a profile, a link target, a definition).
4//!
5//! It is mounted through `hoverable_tooltip`, not `tooltip` — that one word is
6//! the whole difference between a tooltip and a hover card, and it means there
7//! is no open/close state machine to write here: gpui already owns the delay
8//! (500ms by default, `.tooltip_show_delay(..)` to change it) and keeps the
9//! card alive while the pointer is inside it.
10//!
11//! ```ignore
12//! use ui::hover_card::HoverCard;
13//!
14//! div()
15//!     .id("clearloop")
16//!     .hoverable_tooltip(|window, cx| {
17//!         HoverCard::summary("clearloop", "Builds desktop software in Rust.", window, cx)
18//!     })
19//!     .child("@clearloop")
20//! ```
21
22use gpui::{AnyView, App, Context, IntoElement, SharedString, Window, div, prelude::*, px};
23
24use crate::widgets::Content;
25use theme::{TextStyle, Theme, Typeset};
26
27use crate::{popover, surface::Surfaced as _};
28
29pub struct HoverCard {
30    title: SharedString,
31    body: SharedString,
32    /// Initials for the leading avatar, when the subject is a person.
33    initials: Option<SharedString>,
34    /// The quiet line under the body — a role, a path, a timestamp.
35    meta: Option<SharedString>,
36}
37
38impl HoverCard {
39    /// A plain card: heading plus a line or two of prose.
40    pub fn summary(
41        title: impl Into<SharedString>,
42        body: impl Into<SharedString>,
43        _window: &mut Window,
44        cx: &mut App,
45    ) -> AnyView {
46        let (title, body) = (title.into(), body.into());
47        cx.new(|_| Self {
48            title,
49            body,
50            initials: None,
51            meta: None,
52        })
53        .into()
54    }
55
56    /// A hover card for a person: avatar initials beside the name, and a meta
57    /// line under the body.
58    pub fn person(
59        initials: impl Into<SharedString>,
60        name: impl Into<SharedString>,
61        body: impl Into<SharedString>,
62        meta: impl Into<SharedString>,
63        _window: &mut Window,
64        cx: &mut App,
65    ) -> AnyView {
66        let (initials, name, body, meta) = (initials.into(), name.into(), body.into(), meta.into());
67        cx.new(|_| Self {
68            title: name,
69            body,
70            initials: Some(initials),
71            meta: Some(meta),
72        })
73        .into()
74    }
75}
76
77impl Render for HoverCard {
78    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
79        let theme = Theme::of(cx).clone();
80        let heading = div()
81            .flex()
82            .flex_row()
83            .items_center()
84            .gap(px(10.0))
85            .when_some(self.initials.clone(), |row, initials| {
86                row.child(theme.avatar(initials))
87            })
88            .child(
89                div()
90                    .text_style(TextStyle::Headline)
91                    .text_color(theme.text)
92                    .child(self.title.clone()),
93            );
94
95        // Wider and airier than a tooltip: this holds prose, not a label.
96        popover::popover_card(&theme)
97            .w(px(280.0))
98            .p(px(12.0))
99            .flex()
100            .flex_col()
101            .gap(px(Theme::SPACE))
102            .child(heading)
103            .child(
104                div()
105                    .text_style(TextStyle::Callout)
106                    .line_height(px(18.0))
107                    .text_color(theme.text_muted)
108                    .child(self.body.clone()),
109            )
110            .when_some(self.meta.clone(), |card, meta| {
111                card.child(
112                    div()
113                        .text_style(TextStyle::Subheadline)
114                        .text_color(theme.text_faint)
115                        .child(meta),
116                )
117            })
118            .surface(&theme, theme.popover_surface)
119    }
120}