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