diff --git a/crates/acp_tools/src/acp_tools.rs b/crates/acp_tools/src/acp_tools.rs index 524170ccdc3df6a5477df24dcbd89509405b0d58..b5b0e078ae0e41f5c3527265009fac803757ff1a 100644 --- a/crates/acp_tools/src/acp_tools.rs +++ b/crates/acp_tools/src/acp_tools.rs @@ -571,7 +571,7 @@ impl Render for AcpToolsToolbarItemView { .serialize_observed_messages() .unwrap_or_default(); - CopyButton::new(message) + CopyButton::new("copy-all-messages", message) .tooltip_label("Copy All Messages") .disabled(!has_messages) }) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 7910059f4e300d27d9910a7b1a0827ab55dec616..13c5080136e0e1fefe0e407b5f4ac773a92f4b1a 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -4357,7 +4357,7 @@ impl AcpThreadView { })) .child( div().absolute().top_1().right_1().child( - CopyButton::new(command_source.to_string()) + CopyButton::new("copy-command", command_source.to_string()) .tooltip_label("Copy Command") .visible_on_hover(command_group), ), @@ -7794,7 +7794,7 @@ impl AcpThreadView { fn create_copy_button(&self, message: impl Into) -> impl IntoElement { let message = message.into(); - CopyButton::new(message).tooltip_label("Copy Error Message") + CopyButton::new("copy-error-message", message).tooltip_label("Copy Error Message") } fn dismiss_error_button(&self, cx: &mut Context) -> impl IntoElement { diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 834a3c4fe09cb6d89b750277ebcdfc15ec786361..663d64d56d3e9832a6a92c2916fa62d22afd23e6 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2541,7 +2541,7 @@ impl CollabPanel { let button = match section { Section::ActiveCall => channel_link.map(|channel_link| { - CopyButton::new(channel_link) + CopyButton::new("copy-channel-link", channel_link) .visible_on_hover("section-header") .tooltip_label("Copy Channel Link") .into_any_element() diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 6c9c17259b951a7ff216211171ab2618aabeede6..a40f668553652d3661ba0808911164aa0f408fe3 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1027,7 +1027,7 @@ impl DiagnosticPopover { ) .child(div().absolute().top_1().right_1().child({ let message = self.local_diagnostic.diagnostic.message.clone(); - CopyButton::new(message).tooltip_label("Copy Diagnostic") + CopyButton::new("copy-diagnostic", message).tooltip_label("Copy Diagnostic") })) .custom_scrollbars( Scrollbars::for_settings::() diff --git a/crates/git_ui/src/blame_ui.rs b/crates/git_ui/src/blame_ui.rs index 66ebcebfe36f6bef201278e5409ead5c91d5e6bc..e62a7c76677b1406bbfc00234f92ec7cb5a8ca5d 100644 --- a/crates/git_ui/src/blame_ui.rs +++ b/crates/git_ui/src/blame_ui.rs @@ -337,7 +337,7 @@ impl BlameRenderer for GitBlameRenderer { ) .child(Divider::vertical()) .child( - CopyButton::new(sha.to_string()) + CopyButton::new("copy-blame-sha", sha.to_string()) .tooltip_label("Copy SHA"), ), ), diff --git a/crates/git_ui/src/commit_tooltip.rs b/crates/git_ui/src/commit_tooltip.rs index 264961c029e5c2b2817be1f9d4f2d7394fa25a31..d79f98747c689c58ded35315a1cfe4eafb245579 100644 --- a/crates/git_ui/src/commit_tooltip.rs +++ b/crates/git_ui/src/commit_tooltip.rs @@ -343,7 +343,10 @@ impl Render for CommitTooltip { ), ) .child(Divider::vertical()) - .child(CopyButton::new(full_sha).tooltip_label("Copy SHA")), + .child( + CopyButton::new("copy-commit-sha", full_sha) + .tooltip_label("Copy SHA"), + ), ), ), ) diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 7bd78320a25ea2c9324b2c523dda5e11278dab51..df5b428ad6c5d644fd1ed57c5034b8c8221164c5 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -1451,7 +1451,7 @@ fn render_copy_code_block_button( ) -> impl IntoElement { let id = ElementId::named_usize("copy-markdown-code", id); - CopyButton::new(code.clone()).custom_on_click({ + CopyButton::new(id.clone(), code.clone()).custom_on_click({ let markdown = markdown; move |_window, cx| { let id = id.clone(); diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index 264e112baee364b7b31bde8b94cd05a5c586fcd5..9bff5276bc7a115512d6b2fdff8e615a0b2b61c4 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -623,7 +623,7 @@ fn render_markdown_code_block( StyledText::new(parsed.contents.clone()) }; - let copy_block_button = CopyButton::new(parsed.contents.clone()) + let copy_block_button = CopyButton::new("copy-codeblock", parsed.contents.clone()) .tooltip_label("Copy Codeblock") .visible_on_hover("markdown-block"); diff --git a/crates/repl/src/outputs.rs b/crates/repl/src/outputs.rs index 4673de1fa39533771e2ebc7425608259012bd9b8..ae2ba8539ad8d4ca1bca43244705360aac0f8b91 100644 --- a/crates/repl/src/outputs.rs +++ b/crates/repl/src/outputs.rs @@ -246,7 +246,8 @@ impl Output { let traceback_text = traceback.read(cx).full_text(); let full_error = format!("{}: {}\n{}", ename, evalue, traceback_text); - CopyButton::new(full_error).tooltip_label("Copy Full Error") + CopyButton::new("copy-full-error", full_error) + .tooltip_label("Copy Full Error") }) .child( IconButton::new( diff --git a/crates/ui/src/components/button/copy_button.rs b/crates/ui/src/components/button/copy_button.rs index 75253ba4e2153b1e94e0593fbff9f5af31f066e0..c31d0eabbd669a8a90d9276105e5289ae7f568e2 100644 --- a/crates/ui/src/components/button/copy_button.rs +++ b/crates/ui/src/components/button/copy_button.rs @@ -1,11 +1,37 @@ +use std::time::{Duration, Instant}; + use gpui::{ - AnyElement, App, ClipboardItem, IntoElement, ParentElement, RenderOnce, Styled, Window, + AnyElement, App, ClipboardItem, Context, ElementId, Entity, IntoElement, ParentElement, + RenderOnce, Styled, Window, }; use crate::{Tooltip, prelude::*}; +const COPIED_STATE_DURATION: Duration = Duration::from_secs(2); + +struct CopyButtonState { + copied_at: Option, +} + +impl CopyButtonState { + fn new(_window: &mut Window, _cx: &mut Context) -> Self { + Self { copied_at: None } + } + + fn is_copied(&self) -> bool { + self.copied_at + .map(|t| t.elapsed() < COPIED_STATE_DURATION) + .unwrap_or(false) + } + + fn mark_copied(&mut self) { + self.copied_at = Some(Instant::now()); + } +} + #[derive(IntoElement, RegisterComponent)] pub struct CopyButton { + id: ElementId, message: SharedString, icon_size: IconSize, disabled: bool, @@ -15,8 +41,9 @@ pub struct CopyButton { } impl CopyButton { - pub fn new(message: impl Into) -> Self { + pub fn new(id: impl Into, message: impl Into) -> Self { Self { + id: id.into(), message: message.into(), icon_size: IconSize::Small, disabled: false, @@ -56,38 +83,47 @@ impl CopyButton { } impl RenderOnce for CopyButton { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let id = self.id.clone(); let message = self.message; - let message_clone = message.clone(); - - let id = format!("copy-button-{}", message_clone); + let custom_on_click = self.custom_on_click; + let visible_on_hover = self.visible_on_hover; - let copied = cx - .read_from_clipboard() - .map(|item| item.text().as_ref() == Some(&message_clone.into())) - .unwrap_or(false); + let state: Entity = + window.use_keyed_state(id.clone(), cx, CopyButtonState::new); + let is_copied = state.read(cx).is_copied(); - let (icon, color, tooltip) = if copied { + let (icon, color, tooltip) = if is_copied { (IconName::Check, Color::Success, "Copied!".into()) } else { (IconName::Copy, Color::Muted, self.tooltip_label) }; - let custom_on_click = self.custom_on_click; - let visible_on_hover = self.visible_on_hover; - let button = IconButton::new(id, icon) .icon_color(color) .icon_size(self.icon_size) .disabled(self.disabled) .tooltip(Tooltip::text(tooltip)) .on_click(move |_, window, cx| { + state.update(cx, |state, _cx| { + state.mark_copied(); + }); + if let Some(custom_on_click) = custom_on_click.as_ref() { (custom_on_click)(window, cx); } else { cx.stop_propagation(); - cx.write_to_clipboard(ClipboardItem::new_string(message.clone().into())); + cx.write_to_clipboard(ClipboardItem::new_string(message.to_string())); } + + let state_id = state.entity_id(); + cx.spawn(async move |cx| { + cx.background_executor().timer(COPIED_STATE_DURATION).await; + cx.update(|cx| { + cx.notify(state_id); + }) + }) + .detach(); }); if let Some(visible_on_hover) = visible_on_hover { @@ -109,23 +145,14 @@ impl Component for CopyButton { fn preview(_window: &mut Window, _cx: &mut App) -> Option { let label_text = "Here's an example label"; - let mut counter: usize = 0; - - let mut copy_b = || { - counter += 1; - CopyButton::new(format!( - "Here's an example label (id for uniqueness: {} — ignore this)", - counter - )) - }; - let example = vec![ + let examples = vec![ single_example( "Default", h_flex() .gap_1() .child(Label::new(label_text).size(LabelSize::Small)) - .child(copy_b()) + .child(CopyButton::new("preview-default", label_text)) .into_any_element(), ), single_example( @@ -133,9 +160,15 @@ impl Component for CopyButton { h_flex() .gap_1() .child(Label::new(label_text).size(LabelSize::Small)) - .child(copy_b().icon_size(IconSize::XSmall)) - .child(copy_b().icon_size(IconSize::Medium)) - .child(copy_b().icon_size(IconSize::XLarge)) + .child( + CopyButton::new("preview-xsmall", label_text).icon_size(IconSize::XSmall), + ) + .child( + CopyButton::new("preview-medium", label_text).icon_size(IconSize::Medium), + ) + .child( + CopyButton::new("preview-xlarge", label_text).icon_size(IconSize::XLarge), + ) .into_any_element(), ), single_example( @@ -143,7 +176,10 @@ impl Component for CopyButton { h_flex() .gap_1() .child(Label::new(label_text).size(LabelSize::Small)) - .child(copy_b().tooltip_label("Custom tooltip label")) + .child( + CopyButton::new("preview-tooltip", label_text) + .tooltip_label("Custom tooltip label"), + ) .into_any_element(), ), single_example( @@ -152,11 +188,13 @@ impl Component for CopyButton { .group("container") .gap_1() .child(Label::new(label_text).size(LabelSize::Small)) - .child(copy_b().visible_on_hover("container")) + .child( + CopyButton::new("preview-hover", label_text).visible_on_hover("container"), + ) .into_any_element(), ), ]; - Some(example_group(example).vertical().into_any_element()) + Some(example_group(examples).vertical().into_any_element()) } } diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 1c973669d033c74f9d63c005ce65a7d1e8f86edd..03c7f624de381a3d4ca871eeba3f1d01577207af 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -308,8 +308,11 @@ impl Render for LanguageServerPrompt { h_flex() .gap_1() .child( - CopyButton::new(request.message.clone()) - .tooltip_label("Copy Description"), + CopyButton::new( + "copy-description", + request.message.clone(), + ) + .tooltip_label("Copy Description"), ) .child( IconButton::new(close_id, close_icon)