Skip to main content

gpui_component/
clipboard.rs

1use std::{rc::Rc, time::Duration};
2
3use gpui::{
4    App, ClipboardItem, ElementId, IntoElement, RenderOnce, SharedString, Window,
5    prelude::FluentBuilder,
6};
7use rust_i18n::t;
8
9use crate::{
10    IconName, Sizable, Size,
11    button::{Button, ButtonVariants as _},
12};
13
14/// An element that provides clipboard copy functionality.
15#[derive(IntoElement)]
16pub struct Clipboard {
17    id: ElementId,
18    value: SharedString,
19    value_fn: Option<Rc<dyn Fn(&mut Window, &mut App) -> SharedString>>,
20    on_copied: Option<Rc<dyn Fn(SharedString, &mut Window, &mut App)>>,
21    tooltip_text: Option<SharedString>,
22    tooltip_show_delay: Option<Duration>,
23    accessibility_label: Option<SharedString>,
24    size: Size,
25}
26
27impl Sizable for Clipboard {
28    fn with_size(mut self, size: impl Into<Size>) -> Self {
29        self.size = size.into();
30        self
31    }
32}
33
34impl Clipboard {
35    /// Create a new Clipboard element with the given ID.
36    pub fn new(id: impl Into<ElementId>) -> Self {
37        Self {
38            id: id.into(),
39            value: SharedString::default(),
40            value_fn: None,
41            on_copied: None,
42            tooltip_text: None,
43            tooltip_show_delay: None,
44            accessibility_label: None,
45            size: Size::XSmall,
46        }
47    }
48
49    /// Set tooltip text for the clipboard button.
50    pub fn tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
51        self.tooltip_text = Some(tooltip.into());
52        self
53    }
54
55    /// Overrides how long the pointer must rest before the tooltip shows.
56    ///
57    /// Defaults to [`gpui_base::TooltipDefaults::show_delay`].
58    pub fn tooltip_show_delay(mut self, delay: Duration) -> Self {
59        self.tooltip_show_delay = Some(delay);
60        self
61    }
62
63    /// Set the name a screen reader announces. Defaults to the localized "Copy".
64    ///
65    /// The clipboard button shows only an icon, so it has no visible label to
66    /// read, and its tooltip is a hint rather than a name.
67    pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
68        self.accessibility_label = Some(label.into());
69        self
70    }
71
72    /// Set the value for copying to the clipboard. Default is an empty string.
73    pub fn value(mut self, value: impl Into<SharedString>) -> Self {
74        self.value = value.into();
75        self
76    }
77
78    /// Set the value of the clipboard to the result of the given function. Default is None.
79    ///
80    /// When used this, the copy value will use the result of the function.
81    pub fn value_fn(
82        mut self,
83        value: impl Fn(&mut Window, &mut App) -> SharedString + 'static,
84    ) -> Self {
85        self.value_fn = Some(Rc::new(value));
86        self
87    }
88
89    /// Set a callback to be invoked when the content is copied to the clipboard.
90    pub fn on_copied<F>(mut self, handler: F) -> Self
91    where
92        F: Fn(SharedString, &mut Window, &mut App) + 'static,
93    {
94        self.on_copied = Some(Rc::new(handler));
95        self
96    }
97}
98
99impl RenderOnce for Clipboard {
100    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
101        let state = window.use_keyed_state(self.id.clone(), cx, |_, _| ClipboardState::default());
102
103        let value = self.value.clone();
104        let clipboard_id = self.id.clone();
105        let copied = state.read(cx).copied;
106        let value_fn = self.value_fn.clone();
107
108        Button::new(clipboard_id)
109            .icon(if copied {
110                IconName::Check
111            } else {
112                IconName::Copy
113            })
114            .ghost()
115            .with_size(self.size)
116            .when_some(self.tooltip_text, |this, text| this.tooltip(text))
117            .when_some(self.tooltip_show_delay, |this, delay| {
118                this.tooltip_show_delay(delay)
119            })
120            .accessibility_label(
121                self.accessibility_label
122                    .unwrap_or_else(|| t!("Copy").into()),
123            )
124            .when(!copied, |this| {
125                this.on_click({
126                    let state = state.clone();
127                    let on_copied = self.on_copied.clone();
128                    move |_, window, cx| {
129                        cx.stop_propagation();
130                        let value = value_fn
131                            .as_ref()
132                            .map(|f| f(window, cx))
133                            .unwrap_or_else(|| value.clone());
134                        cx.write_to_clipboard(ClipboardItem::new_string(value.to_string()));
135                        state.update(cx, |state, cx| {
136                            state.copied = true;
137                            cx.notify();
138                        });
139
140                        let state = state.clone();
141                        cx.spawn(async move |cx| {
142                            cx.background_executor().timer(Duration::from_secs(2)).await;
143                            _ = state.update(cx, |state, cx| {
144                                state.copied = false;
145                                cx.notify();
146                            });
147                        })
148                        .detach();
149
150                        if let Some(on_copied) = &on_copied {
151                            on_copied(value.clone(), window, cx);
152                        }
153                    }
154                })
155            })
156    }
157}
158
159#[doc(hidden)]
160#[derive(Default)]
161struct ClipboardState {
162    copied: bool,
163}