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