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}