ui: Improve performance in the CopyButton component (#47292)

This PR improves the performance of the `CopyButton` component by
tracking the copied state locally through a `CopyButtonState` struct
instead of making an OS call every time the component re-renders. Also
pushing a slight improvement here by resetting the state after two
seconds so as to make the check mark go away after you clicked to copy.

Release Notes:

- N/A
This commit is contained in:
Danilo Leal 2026-01-21 13:40:42 -03:00 committed by GitHub
parent 099650fdfb
commit e1e7676b5a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 89 additions and 44 deletions

View file

@ -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)
})

View file

@ -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<String>) -> 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<Self>) -> impl IntoElement {

View file

@ -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()

View file

@ -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::<EditorSettings>()

View file

@ -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"),
),
),

View file

@ -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"),
),
),
),
)

View file

@ -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();

View file

@ -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");

View file

@ -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(

View file

@ -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<Instant>,
}
impl CopyButtonState {
fn new(_window: &mut Window, _cx: &mut Context<Self>) -> 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<SharedString>) -> Self {
pub fn new(id: impl Into<ElementId>, message: impl Into<SharedString>) -> 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 custom_on_click = self.custom_on_click;
let visible_on_hover = self.visible_on_hover;
let id = format!("copy-button-{}", message_clone);
let state: Entity<CopyButtonState> =
window.use_keyed_state(id.clone(), cx, CopyButtonState::new);
let is_copied = state.read(cx).is_copied();
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 {
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<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![
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())
}
}

View file

@ -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)