diff --git a/crates/acp_tools/src/acp_tools.rs b/crates/acp_tools/src/acp_tools.rs index b0d30367da0634dc82f8db96fc099e268aa4790e..524170ccdc3df6a5477df24dcbd89509405b0d58 100644 --- a/crates/acp_tools/src/acp_tools.rs +++ b/crates/acp_tools/src/acp_tools.rs @@ -4,22 +4,20 @@ use std::{ fmt::Display, rc::{Rc, Weak}, sync::Arc, - time::Duration, }; use agent_client_protocol as acp; use collections::HashMap; use gpui::{ - App, ClipboardItem, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment, - ListState, StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list, - prelude::*, + App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment, ListState, + StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list, prelude::*, }; use language::LanguageRegistry; use markdown::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownStyle}; use project::Project; use settings::Settings; use theme::ThemeSettings; -use ui::{Tooltip, WithScrollbar, prelude::*}; +use ui::{CopyButton, Tooltip, WithScrollbar, prelude::*}; use util::ResultExt as _; use workspace::{ Item, ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, @@ -544,15 +542,11 @@ impl Render for AcpTools { pub struct AcpToolsToolbarItemView { acp_tools: Option>, - just_copied: bool, } impl AcpToolsToolbarItemView { pub fn new() -> Self { - Self { - acp_tools: None, - just_copied: false, - } + Self { acp_tools: None } } } @@ -572,37 +566,14 @@ impl Render for AcpToolsToolbarItemView { h_flex() .gap_2() .child({ - let acp_tools = acp_tools.clone(); - IconButton::new( - "copy_all_messages", - if self.just_copied { - IconName::Check - } else { - IconName::Copy - }, - ) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text(if self.just_copied { - "Copied!" - } else { - "Copy All Messages" - })) - .disabled(!has_messages) - .on_click(cx.listener(move |this, _, _window, cx| { - if let Some(content) = acp_tools.read(cx).serialize_observed_messages() { - cx.write_to_clipboard(ClipboardItem::new_string(content)); - - this.just_copied = true; - cx.spawn(async move |this, cx| { - cx.background_executor().timer(Duration::from_secs(2)).await; - this.update(cx, |this, cx| { - this.just_copied = false; - cx.notify(); - }) - }) - .detach(); - } - })) + let message = acp_tools + .read(cx) + .serialize_observed_messages() + .unwrap_or_default(); + + CopyButton::new(message) + .tooltip_label("Copy All Messages") + .disabled(!has_messages) }) .child( IconButton::new("clear_messages", IconName::Trash) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 3859f5921bf2a99410ac883a649721ce2b6f9989..3f34250733cb9588542cb2fbbadce6376617c3a7 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -24,11 +24,11 @@ use file_icons::FileIcons; use fs::Fs; use futures::FutureExt as _; use gpui::{ - Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem, - CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, - ListOffset, ListState, PlatformDisplay, SharedString, StyleRefinement, Subscription, Task, - TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, Window, WindowHandle, div, - ease_in_out, linear_color_stop, linear_gradient, list, point, pulsating_between, + Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, CursorStyle, + EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, + ListState, PlatformDisplay, SharedString, StyleRefinement, Subscription, Task, TextStyle, + TextStyleRefinement, UnderlineStyle, WeakEntity, Window, WindowHandle, div, ease_in_out, + linear_color_stop, linear_gradient, list, point, pulsating_between, }; use language::Buffer; @@ -47,9 +47,9 @@ use terminal_view::terminal_panel::TerminalPanel; use text::Anchor; use theme::{AgentFontSize, ThemeSettings}; use ui::{ - Callout, CommonAnimationExt, ContextMenu, ContextMenuEntry, Disclosure, Divider, DividerColor, - ElevationIndex, KeyBinding, PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip, WithScrollbar, - prelude::*, right_click_menu, + Callout, CommonAnimationExt, ContextMenu, ContextMenuEntry, CopyButton, Disclosure, Divider, + DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip, + WithScrollbar, prelude::*, right_click_menu, }; use util::{ResultExt, size::format_file_size, time::duration_alt_display}; use workspace::{CollaboratorId, NewTerminal, Workspace}; @@ -5921,12 +5921,7 @@ impl AcpThreadView { fn create_copy_button(&self, message: impl Into) -> impl IntoElement { let message = message.into(); - IconButton::new("copy", IconName::Copy) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Copy Error Message")) - .on_click(move |_, _, cx| { - cx.write_to_clipboard(ClipboardItem::new_string(message.clone())) - }) + CopyButton::new(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 0ae4ff270bd672ca028d638484b9a23f5981de1a..9fddac64c06c2110e30484199998e0ae5dd513d5 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -31,9 +31,9 @@ use smallvec::SmallVec; use std::{mem, sync::Arc}; use theme::{ActiveTheme, ThemeSettings}; use ui::{ - Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu, Facepile, HighlightedLabel, - Icon, IconButton, IconName, IconSize, Indicator, Label, ListHeader, ListItem, Tab, Tooltip, - prelude::*, tooltip_container, + Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu, CopyButton, Facepile, + HighlightedLabel, Icon, IconButton, IconName, IconSize, Indicator, Label, ListHeader, ListItem, + Tab, Tooltip, prelude::*, tooltip_container, }; use util::{ResultExt, TryFutureExt, maybe}; use workspace::{ @@ -2527,16 +2527,9 @@ impl CollabPanel { let button = match section { Section::ActiveCall => channel_link.map(|channel_link| { - let channel_link_copy = channel_link; - IconButton::new("channel-link", IconName::Copy) - .icon_size(IconSize::Small) - .size(ButtonSize::None) + CopyButton::new(channel_link) .visible_on_hover("section-header") - .on_click(move |_, _, cx| { - let item = ClipboardItem::new_string(channel_link_copy.clone()); - cx.write_to_clipboard(item) - }) - .tooltip(Tooltip::text("Copy channel link")) + .tooltip_label("Copy Channel Link") .into_any_element() }), Section::Contacts => Some( diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 784b27d327dba7c64690fd6f87599d9566ab2ab6..db3b68ec6fe1be7c6a4c080366cd130c2d83fb96 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -8,8 +8,8 @@ use crate::{ }; use anyhow::Context as _; use gpui::{ - AnyElement, AsyncWindowContext, ClipboardItem, Context, Entity, Focusable as _, FontWeight, - Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, ScrollHandle, Size, + AnyElement, AsyncWindowContext, Context, Entity, Focusable as _, FontWeight, Hsla, + InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, ScrollHandle, Size, StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Task, TextStyleRefinement, Window, div, px, }; @@ -24,7 +24,7 @@ use std::{borrow::Cow, cell::RefCell}; use std::{ops::Range, sync::Arc, time::Duration}; use std::{path::PathBuf, rc::Rc}; use theme::ThemeSettings; -use ui::{Scrollbars, Tooltip, WithScrollbar, prelude::*, theme_is_transparent}; +use ui::{CopyButton, Scrollbars, WithScrollbar, prelude::*, theme_is_transparent}; use url::Url; use util::TryFutureExt; use workspace::{OpenOptions, OpenVisible, Workspace}; @@ -1026,25 +1026,7 @@ impl DiagnosticPopover { ) .child({ let message = self.local_diagnostic.diagnostic.message.clone(); - let copied = cx - .read_from_clipboard() - .map(|item| item.text().as_ref() == Some(&message)) - .unwrap_or(false); - let (icon, color) = if copied { - (IconName::Check, Color::Success) - } else { - (IconName::Copy, Color::Muted) - }; - - IconButton::new("copy-diagnostic", icon) - .icon_color(color) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Copy Diagnostic")) - .on_click(move |_, _, cx| { - cx.write_to_clipboard(ClipboardItem::new_string( - message.clone(), - )); - }) + CopyButton::new(message).tooltip_label("Copy Diagnostic") }), ) .custom_scrollbars( diff --git a/crates/git_ui/src/blame_ui.rs b/crates/git_ui/src/blame_ui.rs index d4d8750a18ee6efbd90a38722043450c6ec61358..66ebcebfe36f6bef201278e5409ead5c91d5e6bc 100644 --- a/crates/git_ui/src/blame_ui.rs +++ b/crates/git_ui/src/blame_ui.rs @@ -13,7 +13,7 @@ use project::{git_store::Repository, project_settings::ProjectSettings}; use settings::Settings as _; use theme::ThemeSettings; use time::OffsetDateTime; -use ui::{ContextMenu, Divider, prelude::*, tooltip_container}; +use ui::{ContextMenu, CopyButton, Divider, prelude::*, tooltip_container}; use workspace::Workspace; const GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED: usize = 20; @@ -335,18 +335,10 @@ impl BlameRenderer for GitBlameRenderer { cx.stop_propagation(); }), ) + .child(Divider::vertical()) .child( - IconButton::new("copy-sha-button", IconName::Copy) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .on_click(move |_, _, cx| { - cx.stop_propagation(); - cx.write_to_clipboard( - ClipboardItem::new_string( - sha.to_string(), - ), - ) - }), + CopyButton::new(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 d18770a704ff31d6dffd705baf44defaaf6d8d4a..264961c029e5c2b2817be1f9d4f2d7394fa25a31 100644 --- a/crates/git_ui/src/commit_tooltip.rs +++ b/crates/git_ui/src/commit_tooltip.rs @@ -5,7 +5,7 @@ use git::blame::BlameEntry; use git::repository::CommitSummary; use git::{GitRemote, commit::ParsedCommitMessage}; use gpui::{ - App, Asset, ClipboardItem, Element, Entity, MouseButton, ParentElement, Render, ScrollHandle, + App, Asset, Element, Entity, MouseButton, ParentElement, Render, ScrollHandle, StatefulInteractiveElement, WeakEntity, prelude::*, }; use markdown::{Markdown, MarkdownElement}; @@ -14,7 +14,7 @@ use settings::Settings; use std::hash::Hash; use theme::ThemeSettings; use time::{OffsetDateTime, UtcOffset}; -use ui::{Avatar, Divider, IconButtonShape, prelude::*, tooltip_container}; +use ui::{Avatar, CopyButton, Divider, prelude::*, tooltip_container}; use workspace::Workspace; #[derive(Clone, Debug)] @@ -315,8 +315,8 @@ impl Render for CommitTooltip { cx.open_url(pr.url.as_str()) }), ) + .child(Divider::vertical()) }) - .child(Divider::vertical()) .child( Button::new( "commit-sha-button", @@ -342,18 +342,8 @@ impl Render for CommitTooltip { }, ), ) - .child( - IconButton::new("copy-sha-button", IconName::Copy) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .on_click(move |_, _, cx| { - cx.stop_propagation(); - cx.write_to_clipboard( - ClipboardItem::new_string(full_sha.clone()), - ) - }), - ), + .child(Divider::vertical()) + .child(CopyButton::new(full_sha).tooltip_label("Copy SHA")), ), ), ) diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 2e18055a19c81189adb9a967c3dfe0d1ff55e8ff..19cd920021ce3aab0ba033df8925085c001484f4 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -8,6 +8,7 @@ use language::LanguageName; use log::Level; pub use path_range::{LineCol, PathWithRange}; use ui::Checkbox; +use ui::CopyButton; use std::borrow::Cow; use std::iter; @@ -32,7 +33,7 @@ use parser::{MarkdownEvent, MarkdownTag, MarkdownTagEnd, parse_links_only, parse use pulldown_cmark::Alignment; use sum_tree::TreeMap; use theme::SyntaxTheme; -use ui::{ScrollAxes, Scrollbars, Tooltip, WithScrollbar, prelude::*}; +use ui::{ScrollAxes, Scrollbars, WithScrollbar, prelude::*}; use util::ResultExt; use crate::parser::CodeBlockKind; @@ -1202,7 +1203,6 @@ impl Element for MarkdownElement { range.end, code, self.markdown.clone(), - cx, ); el.child( h_flex() @@ -1233,7 +1233,6 @@ impl Element for MarkdownElement { range.end, code, self.markdown.clone(), - cx, ); el.child( h_flex() @@ -1449,26 +1448,12 @@ fn render_copy_code_block_button( id: usize, code: String, markdown: Entity, - cx: &App, ) -> impl IntoElement { let id = ElementId::named_usize("copy-markdown-code", id); - let was_copied = markdown.read(cx).copied_code_blocks.contains(&id); - IconButton::new( - id.clone(), - if was_copied { - IconName::Check - } else { - IconName::Copy - }, - ) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .style(ButtonStyle::Filled) - .shape(ui::IconButtonShape::Square) - .tooltip(Tooltip::text("Copy")) - .on_click({ + + CopyButton::new(code.clone()).custom_on_click({ let markdown = markdown; - move |_event, _window, cx| { + move |_window, cx| { let id = id.clone(); markdown.update(cx, |this, cx| { this.copied_code_blocks.insert(id.clone()); diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index d4c810245c0fcf874160957cff1b029c4c4c1702..15dda91740f17bdd63c07b39045fe815caf1697c 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -6,10 +6,10 @@ use crate::markdown_elements::{ }; use fs::normalize_path; use gpui::{ - AbsoluteLength, AnyElement, App, AppContext as _, ClipboardItem, Context, Div, Element, - ElementId, Entity, HighlightStyle, Hsla, ImageSource, InteractiveText, IntoElement, Keystroke, - Modifiers, ParentElement, Render, Resource, SharedString, Styled, StyledText, TextStyle, - WeakEntity, Window, div, img, px, rems, + AbsoluteLength, AnyElement, App, AppContext as _, Context, Div, Element, ElementId, Entity, + HighlightStyle, Hsla, ImageSource, InteractiveText, IntoElement, Keystroke, Modifiers, + ParentElement, Render, Resource, SharedString, Styled, StyledText, TextStyle, WeakEntity, + Window, div, img, px, rems, }; use settings::Settings; use std::{ @@ -18,12 +18,7 @@ use std::{ vec, }; use theme::{ActiveTheme, SyntaxTheme, ThemeSettings}; -use ui::{ - ButtonCommon, Clickable, Color, FluentBuilder, IconButton, IconName, IconSize, - InteractiveElement, Label, LabelCommon, LabelSize, LinkPreview, Pixels, Rems, - StatefulInteractiveElement, StyledExt, StyledImage, ToggleState, Tooltip, VisibleOnHover, - h_flex, tooltip_container, v_flex, -}; +use ui::{CopyButton, LinkPreview, ToggleState, prelude::*, tooltip_container}; use workspace::{OpenOptions, OpenVisible, Workspace}; pub struct CheckboxClickedEvent { @@ -626,15 +621,8 @@ fn render_markdown_code_block( StyledText::new(parsed.contents.clone()) }; - let copy_block_button = IconButton::new("copy-code", IconName::Copy) - .icon_size(IconSize::Small) - .on_click({ - let contents = parsed.contents.clone(); - move |_, _window, cx| { - cx.write_to_clipboard(ClipboardItem::new_string(contents.to_string())); - } - }) - .tooltip(Tooltip::text("Copy code block")) + let copy_block_button = CopyButton::new(parsed.contents.clone()) + .tooltip_label("Copy Codeblock") .visible_on_hover("markdown-block"); let font = gpui::Font { diff --git a/crates/repl/src/outputs.rs b/crates/repl/src/outputs.rs index b99562393a2bbaad051f47bf58cf6c77ea5fb27b..bb4913e612627f0369f70fb77f2928ec69eef045 100644 --- a/crates/repl/src/outputs.rs +++ b/crates/repl/src/outputs.rs @@ -37,10 +37,7 @@ use editor::{Editor, MultiBuffer}; use gpui::{AnyElement, ClipboardItem, Entity, Render, WeakEntity}; use language::Buffer; use runtimelib::{ExecutionState, JupyterMessageContent, MimeBundle, MimeType}; -use ui::{ - ButtonStyle, CommonAnimationExt, Context, IconButton, IconName, IntoElement, Styled, Tooltip, - Window, div, h_flex, prelude::*, v_flex, -}; +use ui::{CommonAnimationExt, CopyButton, IconButton, Tooltip, prelude::*}; mod image; use image::ImageView; @@ -236,89 +233,62 @@ impl Output { Self::Image { content, .. } => { Self::render_output_controls(content.clone(), workspace, window, cx) } - Self::ErrorOutput(err) => { - // Add buttons for the traceback section - Some( - h_flex() - .pl_1() - .child( - IconButton::new( - ElementId::Name("copy-full-error-traceback".into()), - IconName::Copy, - ) - .style(ButtonStyle::Transparent) - .tooltip(Tooltip::text("Copy Full Error")) - .on_click({ - let ename = err.ename.clone(); - let evalue = err.evalue.clone(); - let traceback = err.traceback.clone(); - move |_, _window, cx| { + Self::ErrorOutput(err) => Some( + h_flex() + .pl_1() + .child({ + let ename = err.ename.clone(); + let evalue = err.evalue.clone(); + let traceback = err.traceback.clone(); + 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") + }) + .child( + IconButton::new( + ElementId::Name("open-full-error-in-buffer-traceback".into()), + IconName::FileTextOutlined, + ) + .style(ButtonStyle::Transparent) + .tooltip(Tooltip::text("Open Full Error in Buffer")) + .on_click({ + let ename = err.ename.clone(); + let evalue = err.evalue.clone(); + let traceback = err.traceback.clone(); + move |_, window, cx| { + if let Some(workspace) = workspace.upgrade() { let traceback_text = traceback.read(cx).full_text(); let full_error = format!("{}: {}\n{}", ename, evalue, traceback_text); - let clipboard_content = - ClipboardItem::new_string(full_error); - cx.write_to_clipboard(clipboard_content); - } - }), - ) - .child( - IconButton::new( - ElementId::Name("open-full-error-in-buffer-traceback".into()), - IconName::FileTextOutlined, - ) - .style(ButtonStyle::Transparent) - .tooltip(Tooltip::text("Open Full Error in Buffer")) - .on_click({ - let ename = err.ename.clone(); - let evalue = err.evalue.clone(); - let traceback = err.traceback.clone(); - move |_, window, cx| { - if let Some(workspace) = workspace.upgrade() { - let traceback_text = traceback.read(cx).full_text(); - let full_error = format!( - "{}: {}\n{}", - ename, evalue, traceback_text - ); - let buffer = cx.new(|cx| { - let mut buffer = Buffer::local(full_error, cx) - .with_language( - language::PLAIN_TEXT.clone(), - cx, - ); - buffer.set_capability( - language::Capability::ReadOnly, - cx, - ); - buffer - }); - let editor = Box::new(cx.new(|cx| { - let multibuffer = cx.new(|cx| { - let mut multi_buffer = - MultiBuffer::singleton(buffer.clone(), cx); - multi_buffer - .set_title("Full Error".to_string(), cx); - multi_buffer - }); - Editor::for_multibuffer( - multibuffer, - None, - window, - cx, - ) - })); - workspace.update(cx, |workspace, cx| { - workspace.add_item_to_active_pane( - editor, None, true, window, cx, - ); + let buffer = cx.new(|cx| { + let mut buffer = Buffer::local(full_error, cx) + .with_language(language::PLAIN_TEXT.clone(), cx); + buffer + .set_capability(language::Capability::ReadOnly, cx); + buffer + }); + let editor = Box::new(cx.new(|cx| { + let multibuffer = cx.new(|cx| { + let mut multi_buffer = + MultiBuffer::singleton(buffer.clone(), cx); + multi_buffer + .set_title("Full Error".to_string(), cx); + multi_buffer }); - } + Editor::for_multibuffer(multibuffer, None, window, cx) + })); + workspace.update(cx, |workspace, cx| { + workspace.add_item_to_active_pane( + editor, None, true, window, cx, + ); + }); } - }), - ) - .into_any_element(), - ) - } + } + }), + ) + .into_any_element(), + ), Self::Message(_) => None, Self::Table { content, .. } => { Self::render_output_controls(content.clone(), workspace, window, cx) diff --git a/crates/ui/src/components/button.rs b/crates/ui/src/components/button.rs index d56a9c09d3b57ba607b6837b16af31d240e58663..17c216ec7b000bd9b563b3e00d4ee9979ca5287f 100644 --- a/crates/ui/src/components/button.rs +++ b/crates/ui/src/components/button.rs @@ -2,6 +2,7 @@ mod button; mod button_icon; mod button_like; mod button_link; +mod copy_button; mod icon_button; mod split_button; mod toggle_button; @@ -9,6 +10,7 @@ mod toggle_button; pub use button::*; pub use button_like::*; pub use button_link::*; +pub use copy_button::*; pub use icon_button::*; pub use split_button::*; pub use toggle_button::*; diff --git a/crates/ui/src/components/button/copy_button.rs b/crates/ui/src/components/button/copy_button.rs new file mode 100644 index 0000000000000000000000000000000000000000..75253ba4e2153b1e94e0593fbff9f5af31f066e0 --- /dev/null +++ b/crates/ui/src/components/button/copy_button.rs @@ -0,0 +1,162 @@ +use gpui::{ + AnyElement, App, ClipboardItem, IntoElement, ParentElement, RenderOnce, Styled, Window, +}; + +use crate::{Tooltip, prelude::*}; + +#[derive(IntoElement, RegisterComponent)] +pub struct CopyButton { + message: SharedString, + icon_size: IconSize, + disabled: bool, + tooltip_label: SharedString, + visible_on_hover: Option, + custom_on_click: Option>, +} + +impl CopyButton { + pub fn new(message: impl Into) -> Self { + Self { + message: message.into(), + icon_size: IconSize::Small, + disabled: false, + tooltip_label: "Copy".into(), + visible_on_hover: None, + custom_on_click: None, + } + } + + pub fn icon_size(mut self, icon_size: IconSize) -> Self { + self.icon_size = icon_size; + self + } + + pub fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } + + pub fn tooltip_label(mut self, tooltip_label: impl Into) -> Self { + self.tooltip_label = tooltip_label.into(); + self + } + + pub fn visible_on_hover(mut self, visible_on_hover: impl Into) -> Self { + self.visible_on_hover = Some(visible_on_hover.into()); + self + } + + pub fn custom_on_click( + mut self, + custom_on_click: impl Fn(&mut Window, &mut App) + 'static, + ) -> Self { + self.custom_on_click = Some(Box::new(custom_on_click)); + self + } +} + +impl RenderOnce for CopyButton { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let message = self.message; + let message_clone = message.clone(); + + let id = format!("copy-button-{}", message_clone); + + let copied = cx + .read_from_clipboard() + .map(|item| item.text().as_ref() == Some(&message_clone.into())) + .unwrap_or(false); + + let (icon, color, tooltip) = if 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| { + 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())); + } + }); + + if let Some(visible_on_hover) = visible_on_hover { + button.visible_on_hover(visible_on_hover) + } else { + button + } + } +} + +impl Component for CopyButton { + fn scope() -> ComponentScope { + ComponentScope::Input + } + + fn description() -> Option<&'static str> { + Some("An icon button that encapsulates the logic to copy a string into the clipboard.") + } + + 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![ + single_example( + "Default", + h_flex() + .gap_1() + .child(Label::new(label_text).size(LabelSize::Small)) + .child(copy_b()) + .into_any_element(), + ), + single_example( + "Multiple Icon Sizes", + 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)) + .into_any_element(), + ), + single_example( + "Custom Tooltip Label", + h_flex() + .gap_1() + .child(Label::new(label_text).size(LabelSize::Small)) + .child(copy_b().tooltip_label("Custom tooltip label")) + .into_any_element(), + ), + single_example( + "Visible On Hover", + h_flex() + .group("container") + .gap_1() + .child(Label::new(label_text).size(LabelSize::Small)) + .child(copy_b().visible_on_hover("container")) + .into_any_element(), + ), + ]; + + Some(example_group(example).vertical().into_any_element()) + } +} diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 3b126d329e7fafefa4043661c5039f1e17b09b54..14119030f794b095ab419c9a68567fd600ae0420 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -1,9 +1,9 @@ use crate::{SuppressNotification, Toast, Workspace}; use anyhow::Context as _; use gpui::{ - AnyView, App, AppContext as _, AsyncWindowContext, ClickEvent, ClipboardItem, Context, - DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, PromptLevel, Render, ScrollHandle, - Task, TextStyleRefinement, UnderlineStyle, svg, + AnyView, App, AppContext as _, AsyncWindowContext, ClickEvent, Context, DismissEvent, Entity, + EventEmitter, FocusHandle, Focusable, PromptLevel, Render, ScrollHandle, Task, + TextStyleRefinement, UnderlineStyle, svg, }; use markdown::{Markdown, MarkdownElement, MarkdownStyle}; use parking_lot::Mutex; @@ -13,7 +13,7 @@ use theme::ThemeSettings; use std::ops::Deref; use std::sync::{Arc, LazyLock}; use std::{any::TypeId, time::Duration}; -use ui::{Tooltip, prelude::*}; +use ui::{CopyButton, Tooltip, prelude::*}; use util::ResultExt; #[derive(Default)] @@ -308,16 +308,8 @@ impl Render for LanguageServerPrompt { h_flex() .gap_1() .child( - IconButton::new("copy", IconName::Copy) - .on_click({ - let message = request.message.clone(); - move |_, _, cx| { - cx.write_to_clipboard( - ClipboardItem::new_string(message.clone()), - ) - } - }) - .tooltip(Tooltip::text("Copy Description")), + CopyButton::new(request.message.clone()) + .tooltip_label("Copy Description"), ) .child( IconButton::new(close_id, close_icon)