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::Theme;
26
27use crate::popover;
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_size(px(13.0))
91                    .font_weight(gpui::FontWeight::MEDIUM)
92                    .text_color(theme.text)
93                    .child(self.title.clone()),
94            );
95
96        // Wider and airier than a tooltip: this holds prose, not a label.
97        popover::popover_card(&theme)
98            .w(px(280.0))
99            .p(px(12.0))
100            .flex()
101            .flex_col()
102            .gap(px(8.0))
103            .child(heading)
104            .child(
105                div()
106                    .text_size(px(12.5))
107                    .line_height(px(18.0))
108                    .text_color(theme.text_muted)
109                    .child(self.body.clone()),
110            )
111            .when_some(self.meta.clone(), |card, meta| {
112                card.child(
113                    div()
114                        .text_size(px(11.5))
115                        .text_color(theme.text_faint)
116                        .child(meta),
117                )
118            })
119    }
120}