Skip to main content

ui/components/button/
copy_button.rs

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}