1use std::time::{Duration, Instant};
2
3use gpui::{
4 AnyElement, App, ClipboardItem, Context, ElementId, Entity, IntoElement, ParentElement,
5 RenderOnce, Styled, Window,
6};
7
8use crate::{Tooltip, prelude::*};
9
10const COPIED_STATE_DURATION: Duration = Duration::from_secs(2);
11
12struct CopyButtonState {
13 copied_at: Option<Instant>,
14}
15
16impl CopyButtonState {
17 fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self {
18 Self { copied_at: None }
19 }
20
21 fn is_copied(&self) -> bool {
22 self.copied_at
23 .map(|t| t.elapsed() < COPIED_STATE_DURATION)
24 .unwrap_or(false)
25 }
26
27 fn mark_copied(&mut self) {
28 self.copied_at = Some(Instant::now());
29 }
30}
31
32#[derive(IntoElement, RegisterComponent)]
33pub struct CopyButton {
34 id: ElementId,
35 message: SharedString,
36 icon_size: IconSize,
37 disabled: bool,
38 tooltip_label: SharedString,
39 visible_on_hover: Option<SharedString>,
40 custom_on_click: Option<Box<dyn Fn(&mut Window, &mut App) + 'static>>,
41}
42
43impl CopyButton {
44 pub fn new(id: impl Into<ElementId>, message: impl Into<SharedString>) -> Self {
45 Self {
46 id: id.into(),
47 message: message.into(),
48 icon_size: IconSize::Small,
49 disabled: false,
50 tooltip_label: "Copy".into(),
51 visible_on_hover: None,
52 custom_on_click: None,
53 }
54 }
55
56 pub fn icon_size(mut self, icon_size: IconSize) -> Self {
57 self.icon_size = icon_size;
58 self
59 }
60
61 pub fn disabled(mut self, disabled: bool) -> Self {
62 self.disabled = disabled;
63 self
64 }
65
66 pub fn tooltip_label(mut self, tooltip_label: impl Into<SharedString>) -> Self {
67 self.tooltip_label = tooltip_label.into();
68 self
69 }
70
71 pub fn visible_on_hover(mut self, visible_on_hover: impl Into<SharedString>) -> Self {
72 self.visible_on_hover = Some(visible_on_hover.into());
73 self
74 }
75
76 pub fn custom_on_click(
77 mut self,
78 custom_on_click: impl Fn(&mut Window, &mut App) + 'static,
79 ) -> Self {
80 self.custom_on_click = Some(Box::new(custom_on_click));
81 self
82 }
83}
84
85impl RenderOnce for CopyButton {
86 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
87 let id = self.id.clone();
88 let message = self.message;
89 let custom_on_click = self.custom_on_click;
90 let visible_on_hover = self.visible_on_hover;
91
92 let state: Entity<CopyButtonState> =
93 window.use_keyed_state(id.clone(), cx, CopyButtonState::new);
94 let is_copied = state.read(cx).is_copied();
95
96 let (icon, color, tooltip) = if is_copied {
97 (IconName::Check, Color::Success, "Copied!".into())
98 } else {
99 (IconName::Copy, Color::Muted, self.tooltip_label)
100 };
101
102 let button = IconButton::new(id, icon)
103 .icon_color(color)
104 .icon_size(self.icon_size)
105 .disabled(self.disabled)
106 .tooltip(Tooltip::text(tooltip))
107 .on_click(move |_, window, cx| {
108 state.update(cx, |state, _cx| {
109 state.mark_copied();
110 });
111
112 if let Some(custom_on_click) = custom_on_click.as_ref() {
113 (custom_on_click)(window, cx);
114 } else {
115 cx.stop_propagation();
116 cx.write_to_clipboard(ClipboardItem::new_string(message.to_string()));
117 }
118
119 let state_id = state.entity_id();
120 cx.spawn(async move |cx| {
121 cx.background_executor().timer(COPIED_STATE_DURATION).await;
122 cx.update(|cx| {
123 cx.notify(state_id);
124 })
125 })
126 .detach();
127 });
128
129 if let Some(visible_on_hover) = visible_on_hover {
130 button.visible_on_hover(visible_on_hover)
131 } else {
132 button
133 }
134 }
135}
136
137impl Component for CopyButton {
138 fn scope() -> ComponentScope {
139 ComponentScope::Input
140 }
141
142 fn description() -> Option<&'static str> {
143 Some("An icon button that encapsulates the logic to copy a string into the clipboard.")
144 }
145
146 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
147 let label_text = "Here's an example label";
148
149 let examples = vec![
150 single_example(
151 "Default",
152 h_flex()
153 .gap_1()
154 .child(Label::new(label_text).size(LabelSize::Small))
155 .child(CopyButton::new("preview-default", label_text))
156 .into_any_element(),
157 ),
158 single_example(
159 "Multiple Icon Sizes",
160 h_flex()
161 .gap_1()
162 .child(Label::new(label_text).size(LabelSize::Small))
163 .child(
164 CopyButton::new("preview-xsmall", label_text).icon_size(IconSize::XSmall),
165 )
166 .child(
167 CopyButton::new("preview-medium", label_text).icon_size(IconSize::Medium),
168 )
169 .child(
170 CopyButton::new("preview-xlarge", label_text).icon_size(IconSize::XLarge),
171 )
172 .into_any_element(),
173 ),
174 single_example(
175 "Custom Tooltip Label",
176 h_flex()
177 .gap_1()
178 .child(Label::new(label_text).size(LabelSize::Small))
179 .child(
180 CopyButton::new("preview-tooltip", label_text)
181 .tooltip_label("Custom tooltip label"),
182 )
183 .into_any_element(),
184 ),
185 single_example(
186 "Visible On Hover",
187 h_flex()
188 .group("container")
189 .gap_1()
190 .child(Label::new(label_text).size(LabelSize::Small))
191 .child(
192 CopyButton::new("preview-hover", label_text).visible_on_hover("container"),
193 )
194 .into_any_element(),
195 ),
196 ];
197
198 Some(example_group(examples).vertical().into_any_element())
199 }
200}