mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
ui: Add a CopyButton component (#45821)
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) ``` <img width="600" height="714" alt="Screenshot 2025-12-29 at 10 50@2x" src="https://github.com/user-attachments/assets/e6949863-a056-4855-82d8-e4ffb5d62c90" /> Release Notes: - N/A
This commit is contained in:
parent
db221ca72d
commit
6d947b7746
12 changed files with 272 additions and 250 deletions
|
|
@ -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<Entity<AcpTools>>,
|
||||
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));
|
||||
let message = acp_tools
|
||||
.read(cx)
|
||||
.serialize_observed_messages()
|
||||
.unwrap_or_default();
|
||||
|
||||
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();
|
||||
}
|
||||
}))
|
||||
CopyButton::new(message)
|
||||
.tooltip_label("Copy All Messages")
|
||||
.disabled(!has_messages)
|
||||
})
|
||||
.child(
|
||||
IconButton::new("clear_messages", IconName::Trash)
|
||||
|
|
|
|||
|
|
@ -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<String>) -> 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<Self>) -> impl IntoElement {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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")),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<Markdown>,
|
||||
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());
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
|
|||
162
crates/ui/src/components/button/copy_button.rs
Normal file
162
crates/ui/src/components/button/copy_button.rs
Normal file
|
|
@ -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<SharedString>,
|
||||
custom_on_click: Option<Box<dyn Fn(&mut Window, &mut App) + 'static>>,
|
||||
}
|
||||
|
||||
impl CopyButton {
|
||||
pub fn new(message: impl Into<SharedString>) -> 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<SharedString>) -> Self {
|
||||
self.tooltip_label = tooltip_label.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn visible_on_hover(mut self, visible_on_hover: impl Into<SharedString>) -> 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<AnyElement> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue