diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg
new file mode 100644
index 00000000..f3b629c6
--- /dev/null
+++ b/assets/icons/copy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/crates/app/src/story_workspace.rs b/crates/app/src/story_workspace.rs
index 3f2d428b..557b200d 100644
--- a/crates/app/src/story_workspace.rs
+++ b/crates/app/src/story_workspace.rs
@@ -177,7 +177,7 @@ impl StoryWorkspace {
StoryContainer::add_pane(
"Scrollable",
- "A scrollable area with scroll bar",
+ "A scrollable area with scroll bar.",
ScrollableStory::view(cx).into(),
workspace.clone(),
cx,
diff --git a/crates/story/src/text_story.rs b/crates/story/src/text_story.rs
index fc80a1a4..1f01ae64 100644
--- a/crates/story/src/text_story.rs
+++ b/crates/story/src/text_story.rs
@@ -6,6 +6,7 @@ use gpui::{
use ui::{
button::{Button, ButtonStyle},
checkbox::Checkbox,
+ clipboard::Clipboard,
h_flex,
label::Label,
link::Link,
@@ -213,5 +214,25 @@ impl Render for TextStory {
)
),
)
+ .child(
+ section("Clipboard", cx).child(
+ h_flex()
+ .w_full()
+ .gap_4()
+ .items_start()
+ .child(
+ Clipboard::new("clipboard1")
+ .content(|_| Label::new("Click icon to copy"))
+ .value("Copied!")
+ .on_copied(|value, _| println!("Copied value: {}", value)),
+ )
+ .child(
+ Clipboard::new("clipboard2")
+ .content(|_| Link::new("link1").href("https://github.com").child("GitHub"))
+ .value("https://github.com")
+ .on_copied(|value, _| println!("Copied value: {}", value)),
+ )
+ ),
+ )
}
}
diff --git a/crates/ui/src/clipboard.rs b/crates/ui/src/clipboard.rs
new file mode 100644
index 00000000..a2e278f2
--- /dev/null
+++ b/crates/ui/src/clipboard.rs
@@ -0,0 +1,70 @@
+use gpui::{
+ prelude::FluentBuilder, AnyElement, ClipboardItem, ElementId, IntoElement, ParentElement,
+ RenderOnce, SharedString, Styled, WindowContext,
+};
+
+use crate::{button::Button, h_flex, Clickable, IconName};
+
+#[derive(IntoElement)]
+pub struct Clipboard {
+ id: ElementId,
+ value: SharedString,
+ content_builder: Option AnyElement>>,
+ copied_callback: Option>,
+}
+
+impl Clipboard {
+ pub fn new(id: impl Into) -> Self {
+ Self {
+ id: id.into(),
+ value: "".into(),
+ content_builder: None,
+ copied_callback: None,
+ }
+ }
+
+ pub fn value(mut self, value: impl Into) -> Self {
+ self.value = value.into();
+ self
+ }
+
+ pub fn content(mut self, element_builder: F) -> Self
+ where
+ E: IntoElement,
+ F: Fn(&mut WindowContext) -> E + 'static,
+ {
+ self.content_builder = Some(Box::new(move |cx| element_builder(cx).into_any_element()));
+ self
+ }
+
+ pub fn on_copied(mut self, handler: F) -> Self
+ where
+ F: Fn(SharedString, &mut WindowContext) + 'static,
+ {
+ self.copied_callback = Some(Box::new(handler));
+ self
+ }
+}
+
+impl RenderOnce for Clipboard {
+ fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+ h_flex()
+ .gap_0p5()
+ .when_some(self.content_builder, |this, builder| {
+ this.child(builder(cx))
+ })
+ .child(
+ Button::new(self.id, cx)
+ .icon(IconName::Copy)
+ .ghost()
+ .on_click(move |_, cx| {
+ cx.stop_propagation();
+ cx.write_to_clipboard(ClipboardItem::new(self.value.to_string()));
+
+ if let Some(copied) = &self.copied_callback {
+ copied(self.value.clone(), cx);
+ }
+ }),
+ )
+ }
+}
diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs
index 645b6699..1968f1d1 100644
--- a/crates/ui/src/icon.rs
+++ b/crates/ui/src/icon.rs
@@ -45,6 +45,7 @@ pub enum IconName {
ThumbsDown,
ThumbsUp,
Menu,
+ Copy,
}
impl IconName {
@@ -89,6 +90,7 @@ impl IconName {
IconName::ThumbsDown => "icons/thumbs-down.svg",
IconName::ThumbsUp => "icons/thumbs-up.svg",
IconName::Menu => "icons/menu.svg",
+ IconName::Copy => "icons/copy.svg",
}
.into()
}
diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs
index 01c38597..25d7549e 100644
--- a/crates/ui/src/lib.rs
+++ b/crates/ui/src/lib.rs
@@ -12,6 +12,7 @@ mod svg_img;
pub mod button;
pub mod checkbox;
+pub mod clipboard;
pub mod divider;
pub mod dropdown;
pub mod indicator;