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}