//! The clipboard button: copies a value and confirms with a brief check mark. use std::rc::Rc; use std::time::Duration; use gpui::prelude::FluentBuilder as _; use gpui::{ AnyView, App, ClipboardItem, ElementId, Entity, IntoElement, RenderOnce, SharedString, Task, Window, }; use gpui_native_icons::IconRole; use gpui_native_theme::Size; use crate::button::{Button, ButtonVariant}; use crate::common::Sizable; /// How long the check mark shows before reverting to the copy icon. pub const COPIED_DURATION: Duration = Duration::from_secs(1); /// The tooltip view builder gpui's `.tooltip(...)` expects. type TooltipBuilder = Rc AnyView>; /// Runs after the value lands on the clipboard. type CopiedHandler = Box; /// A quiet icon button that copies a value to the system clipboard. #[derive(IntoElement)] pub struct ClipboardButton { id: ElementId, value: SharedString, size: Size, disabled: bool, a11y_label: Option, tooltip: Option, on_copied: Option, } impl ClipboardButton { /// Creates a copy button for this value. pub fn new(id: I, value: V) -> Self where I: Into, V: Into, { Self { id: id.into(), value: value.into(), size: Size::default(), disabled: false, a11y_label: None, tooltip: None, on_copied: None, } } /// Shows a tooltip view next to the button after it is hovered. pub fn tooltip(mut self, tooltip: F) -> Self where F: Fn(&mut Window, &mut App) -> AnyView + 'static, { self.tooltip = Some(Rc::new(tooltip)); self } /// Renders the button inert and dimmed. pub fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self } /// Overrides the accessibility label, which defaults to `Copy`. pub fn accessibility_label(mut self, label: L) -> Self where L: Into, { self.a11y_label = Some(label.into()); self } /// Runs after the value lands on the clipboard. pub fn on_copied(mut self, handler: F) -> Self where F: Fn(&mut App) + 'static, { self.on_copied = Some(Box::new(handler)); self } } impl Sizable for ClipboardButton { fn apply_size(&mut self, size: Size) { self.size = size; } } /// The transient copied marker keyed per button. struct CopiedState { generation: u64, tasks: Vec>, } impl RenderOnce for ClipboardButton { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let copied: Entity = window.use_keyed_state((self.id.clone(), "clipboard-copied"), cx, |_, _| { CopiedState { generation: 0, tasks: Vec::new(), } }); let is_copied = copied.read(cx).generation != 0; let selector = if is_copied { "clipboard-copied" } else { "clipboard" }; Button::new((self.id.clone(), "clipboard-button")) .icon(if is_copied { IconRole::Check } else { IconRole::Copy }) .variant(ButtonVariant::Ghost) .with_size(self.size) .disabled(self.disabled) .debug_selector(move || selector.to_string()) .accessibility_label(self.a11y_label.unwrap_or_else(|| "Copy".into())) .when_some(self.tooltip, |button, tooltip| { button.tooltip(move |window, cx| tooltip(window, cx)) }) .on_click({ let value = self.value; let copied = copied.clone(); let on_copied = self.on_copied; move |_, window, cx| { cx.write_to_clipboard(ClipboardItem::new_string(value.to_string())); let generation = copied.update(cx, |state, cx| { state.generation += 1; cx.notify(); state.generation }); let weak = copied.downgrade(); let task = window.spawn(cx, async move |cx| { cx.background_executor().timer(COPIED_DURATION).await; cx.update(|_, cx| { weak.update(cx, |state, cx| { if state.generation == generation { state.generation = 0; cx.notify(); } }) .ok(); }) .ok(); }); copied.update(cx, |state, _| { state.tasks.push(task); }); // Invoke the callback after copying if let Some(on_copied) = &on_copied { on_copied(cx); } } }) } }