From 6d947b7746d77155f25d9fe357aaf67298344770 Mon Sep 17 00:00:00 2001
From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Date: Mon, 29 Dec 2025 11:01:19 -0300
Subject: [PATCH] ui: Add a `CopyButton` component (#45821)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
There were several places adding a copy icon button, so thought of
encapsulating the logic to copy a given string into the clipboard (and
other small details like swapping the icon and tooltip if copied) into a
component, making it easier to introduce this sort of functionality in
the future, with fewer lines of code.
All it takes (for the simplest case) is:
```rs
CopyButton::new(your_message)
```
Release Notes:
- N/A
---
crates/acp_tools/src/acp_tools.rs | 53 ++----
crates/agent_ui/src/acp/thread_view.rs | 23 +--
crates/collab_ui/src/collab_panel.rs | 17 +-
crates/editor/src/hover_popover.rs | 26 +--
crates/git_ui/src/blame_ui.rs | 16 +-
crates/git_ui/src/commit_tooltip.rs | 20 +--
crates/markdown/src/markdown.rs | 25 +--
.../markdown_preview/src/markdown_renderer.rs | 26 +--
crates/repl/src/outputs.rs | 134 ++++++---------
crates/ui/src/components/button.rs | 2 +
.../ui/src/components/button/copy_button.rs | 162 ++++++++++++++++++
crates/workspace/src/notifications.rs | 20 +--
12 files changed, 273 insertions(+), 251 deletions(-)
create mode 100644 crates/ui/src/components/button/copy_button.rs
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)