feat: add clipboard button component
This commit is contained in:
@@ -29,6 +29,7 @@ full = [
|
|||||||
"avatar",
|
"avatar",
|
||||||
"badge",
|
"badge",
|
||||||
"input",
|
"input",
|
||||||
|
"clipboard",
|
||||||
"popover",
|
"popover",
|
||||||
"scroll_area",
|
"scroll_area",
|
||||||
"scrollbar",
|
"scrollbar",
|
||||||
@@ -58,6 +59,7 @@ progress = []
|
|||||||
avatar = []
|
avatar = []
|
||||||
badge = []
|
badge = []
|
||||||
input = ["button"]
|
input = ["button"]
|
||||||
|
clipboard = ["button"]
|
||||||
popover = []
|
popover = []
|
||||||
scroll_area = []
|
scroll_area = []
|
||||||
scrollbar = ["scroll_area"]
|
scrollbar = ["scroll_area"]
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
//! The clipboard button: copies a value and confirms with a brief check mark.
|
||||||
|
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use gpui::prelude::FluentBuilder as _;
|
||||||
|
use gpui::{
|
||||||
|
AnyView, App, ClipboardItem, ElementId, Entity, IntoElement, RenderOnce, SharedString, Window,
|
||||||
|
};
|
||||||
|
use gpui_native_icons::IconRole;
|
||||||
|
use gpui_native_theme::Size;
|
||||||
|
|
||||||
|
use crate::button::{Button, ButtonVariant};
|
||||||
|
use crate::common::Sizable;
|
||||||
|
|
||||||
|
/// How long the check mark shows before reverting to the copy icon.
|
||||||
|
pub const COPIED_DURATION: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
|
/// The tooltip view builder gpui's `.tooltip(...)` expects.
|
||||||
|
type TooltipBuilder = Rc<dyn Fn(&mut Window, &mut App) -> AnyView>;
|
||||||
|
|
||||||
|
/// Runs after the value lands on the clipboard.
|
||||||
|
type CopiedHandler = Box<dyn Fn(&mut App)>;
|
||||||
|
|
||||||
|
/// A quiet icon button that copies a value to the system clipboard.
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct ClipboardButton {
|
||||||
|
id: ElementId,
|
||||||
|
value: SharedString,
|
||||||
|
size: Size,
|
||||||
|
disabled: bool,
|
||||||
|
a11y_label: Option<SharedString>,
|
||||||
|
tooltip: Option<TooltipBuilder>,
|
||||||
|
on_copied: Option<CopiedHandler>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClipboardButton {
|
||||||
|
/// Creates a copy button for this value.
|
||||||
|
pub fn new<I, V>(id: I, value: V) -> Self
|
||||||
|
where
|
||||||
|
I: Into<ElementId>,
|
||||||
|
V: Into<SharedString>,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
id: id.into(),
|
||||||
|
value: value.into(),
|
||||||
|
size: Size::default(),
|
||||||
|
disabled: false,
|
||||||
|
a11y_label: None,
|
||||||
|
tooltip: None,
|
||||||
|
on_copied: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows a tooltip view next to the button after it is hovered.
|
||||||
|
pub fn tooltip<F>(mut self, tooltip: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||||
|
{
|
||||||
|
self.tooltip = Some(Rc::new(tooltip));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders the button inert and dimmed.
|
||||||
|
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||||
|
self.disabled = disabled;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Overrides the accessibility label, which defaults to `Copy`.
|
||||||
|
pub fn accessibility_label<L>(mut self, label: L) -> Self
|
||||||
|
where
|
||||||
|
L: Into<SharedString>,
|
||||||
|
{
|
||||||
|
self.a11y_label = Some(label.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs after the value lands on the clipboard.
|
||||||
|
pub fn on_copied<F>(mut self, handler: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(&mut App) + 'static,
|
||||||
|
{
|
||||||
|
self.on_copied = Some(Box::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sizable for ClipboardButton {
|
||||||
|
fn apply_size(&mut self, size: Size) {
|
||||||
|
self.size = size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for ClipboardButton {
|
||||||
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let copied: Entity<Option<u64>> =
|
||||||
|
window.use_keyed_state((self.id.clone(), "clipboard-copied"), cx, |_, _| None);
|
||||||
|
let is_copied = copied.read(cx).is_some();
|
||||||
|
|
||||||
|
let selector = if is_copied {
|
||||||
|
"clipboard-copied"
|
||||||
|
} else {
|
||||||
|
"clipboard"
|
||||||
|
};
|
||||||
|
|
||||||
|
Button::new((self.id.clone(), "clipboard-button"))
|
||||||
|
.icon(if is_copied {
|
||||||
|
IconRole::Check
|
||||||
|
} else {
|
||||||
|
IconRole::Copy
|
||||||
|
})
|
||||||
|
.variant(ButtonVariant::Ghost)
|
||||||
|
.with_size(self.size)
|
||||||
|
.disabled(self.disabled)
|
||||||
|
.debug_selector(move || selector.to_string())
|
||||||
|
.accessibility_label(self.a11y_label.unwrap_or_else(|| "Copy".into()))
|
||||||
|
.when_some(self.tooltip, |button, tooltip| {
|
||||||
|
button.tooltip(move |window, cx| tooltip(window, cx))
|
||||||
|
})
|
||||||
|
.on_click({
|
||||||
|
let value = self.value;
|
||||||
|
let copied = copied.clone();
|
||||||
|
let on_copied = self.on_copied;
|
||||||
|
move |_, window, cx| {
|
||||||
|
cx.write_to_clipboard(ClipboardItem::new_string(value.to_string()));
|
||||||
|
|
||||||
|
let generation = copied.update(cx, |state, cx| {
|
||||||
|
let generation = state.map_or(1, |generation| generation.wrapping_add(1));
|
||||||
|
*state = Some(generation);
|
||||||
|
cx.notify();
|
||||||
|
generation
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear the copied state after the duration expires
|
||||||
|
window
|
||||||
|
.spawn(cx, {
|
||||||
|
let copied = copied.clone();
|
||||||
|
async move |cx| {
|
||||||
|
cx.background_executor().timer(COPIED_DURATION).await;
|
||||||
|
cx.update(|_, cx| {
|
||||||
|
copied.update(cx, |state, cx| {
|
||||||
|
if *state == Some(generation) {
|
||||||
|
*state = None;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
// Invoke the callback after copying
|
||||||
|
if let Some(on_copied) = &on_copied {
|
||||||
|
on_copied(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,6 +45,9 @@ pub mod badge;
|
|||||||
#[cfg(feature = "input")]
|
#[cfg(feature = "input")]
|
||||||
pub mod input;
|
pub mod input;
|
||||||
|
|
||||||
|
#[cfg(feature = "clipboard")]
|
||||||
|
pub mod clipboard;
|
||||||
|
|
||||||
#[cfg(feature = "dialog")]
|
#[cfg(feature = "dialog")]
|
||||||
pub mod dialog;
|
pub mod dialog;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
AnyWindowHandle, AppContext as _, Context, IntoElement, Modifiers, ParentElement as _, Render,
|
||||||
|
Styled as _, TestAppContext, VisualTestContext, Window, div,
|
||||||
|
};
|
||||||
|
use gpui_native_core::clipboard::{COPIED_DURATION, ClipboardButton};
|
||||||
|
use gpui_native_theme::Theme;
|
||||||
|
|
||||||
|
const VALUE: &str = "https://example.com/repo.git";
|
||||||
|
|
||||||
|
struct Harness {
|
||||||
|
copies: Rc<RefCell<usize>>,
|
||||||
|
disabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Harness {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let copies = self.copies.clone();
|
||||||
|
div().size_full().flex().flex_col().child(
|
||||||
|
ClipboardButton::new("test-clipboard", VALUE)
|
||||||
|
.disabled(self.disabled)
|
||||||
|
.on_copied(move |_| *copies.borrow_mut() += 1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A maximized window whose button stretches across the top control row, so
|
||||||
|
/// `point(px(60.), px(14.))` always lands on it.
|
||||||
|
fn clipboard_window(
|
||||||
|
disabled: bool,
|
||||||
|
) -> (
|
||||||
|
TestAppContext,
|
||||||
|
VisualTestContext,
|
||||||
|
AnyWindowHandle,
|
||||||
|
Rc<RefCell<usize>>,
|
||||||
|
) {
|
||||||
|
let mut cx = TestAppContext::single();
|
||||||
|
cx.update(Theme::init);
|
||||||
|
let copies = Rc::new(RefCell::new(0));
|
||||||
|
let harness_copies = copies.clone();
|
||||||
|
let window = cx.add_window(move |_, _| Harness {
|
||||||
|
copies: harness_copies,
|
||||||
|
disabled,
|
||||||
|
});
|
||||||
|
let window = window.into();
|
||||||
|
cx.update_window(window, |_, window, cx| {
|
||||||
|
window.draw(cx).clear(cx);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let visual = VisualTestContext::from_window(window, &cx);
|
||||||
|
(cx, visual, window, copies)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(visual: &mut VisualTestContext, window: AnyWindowHandle) {
|
||||||
|
visual
|
||||||
|
.update_window(window, |_, window, cx| {
|
||||||
|
window.draw(cx).clear(cx);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clipboard_text(visual: &mut VisualTestContext) -> String {
|
||||||
|
visual.update(|_window, cx| {
|
||||||
|
cx.read_from_clipboard()
|
||||||
|
.and_then(|item| item.text())
|
||||||
|
.unwrap_or_default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn click(visual: &mut VisualTestContext) {
|
||||||
|
let selector = if visual.debug_bounds("clipboard").is_some() {
|
||||||
|
"clipboard"
|
||||||
|
} else {
|
||||||
|
"clipboard-copied"
|
||||||
|
};
|
||||||
|
let bounds = visual
|
||||||
|
.debug_bounds(selector)
|
||||||
|
.expect("clipboard button should be rendered");
|
||||||
|
visual.simulate_click(bounds.center(), Modifiers::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn click_copies_the_value_and_fires_on_copied() {
|
||||||
|
let (_cx, mut visual, _window, copies) = clipboard_window(false);
|
||||||
|
click(&mut visual);
|
||||||
|
assert_eq!(clipboard_text(&mut visual), VALUE);
|
||||||
|
assert_eq!(*copies.borrow(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn disabled_button_does_not_copy() {
|
||||||
|
let (_cx, mut visual, _window, copies) = clipboard_window(true);
|
||||||
|
click(&mut visual);
|
||||||
|
assert_eq!(clipboard_text(&mut visual), "");
|
||||||
|
assert_eq!(*copies.borrow(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn copied_confirmation_shows_then_reverts() {
|
||||||
|
let (cx, mut visual, window, _copies) = clipboard_window(false);
|
||||||
|
click(&mut visual);
|
||||||
|
draw(&mut visual, window);
|
||||||
|
assert!(
|
||||||
|
visual.debug_bounds("clipboard-copied").is_some(),
|
||||||
|
"the check mark state should render right after the click"
|
||||||
|
);
|
||||||
|
assert!(visual.debug_bounds("clipboard").is_none());
|
||||||
|
|
||||||
|
cx.executor().advance_clock(COPIED_DURATION);
|
||||||
|
visual.run_until_parked();
|
||||||
|
draw(&mut visual, window);
|
||||||
|
assert!(
|
||||||
|
visual.debug_bounds("clipboard").is_some(),
|
||||||
|
"the copy icon should return once the confirmation window passes"
|
||||||
|
);
|
||||||
|
assert!(visual.debug_bounds("clipboard-copied").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_second_click_within_the_window_keeps_the_confirmation_up() {
|
||||||
|
let (cx, mut visual, window, _copies) = clipboard_window(false);
|
||||||
|
click(&mut visual);
|
||||||
|
cx.executor().advance_clock(COPIED_DURATION / 2);
|
||||||
|
click(&mut visual);
|
||||||
|
cx.executor().advance_clock(COPIED_DURATION / 2);
|
||||||
|
visual.run_until_parked();
|
||||||
|
draw(&mut visual, window);
|
||||||
|
assert!(
|
||||||
|
visual.debug_bounds("clipboard-copied").is_some(),
|
||||||
|
"the first click's stale revert must not clear the second click's confirmation"
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.executor().advance_clock(COPIED_DURATION);
|
||||||
|
visual.run_until_parked();
|
||||||
|
draw(&mut visual, window);
|
||||||
|
assert!(visual.debug_bounds("clipboard").is_some());
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ pub enum IconRole {
|
|||||||
Folder,
|
Folder,
|
||||||
Warning,
|
Warning,
|
||||||
Info,
|
Info,
|
||||||
|
Copy,
|
||||||
Eye,
|
Eye,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
PanelLeft,
|
PanelLeft,
|
||||||
@@ -52,6 +53,7 @@ impl IconRole {
|
|||||||
Self::Folder,
|
Self::Folder,
|
||||||
Self::Warning,
|
Self::Warning,
|
||||||
Self::Info,
|
Self::Info,
|
||||||
|
Self::Copy,
|
||||||
Self::Eye,
|
Self::Eye,
|
||||||
Self::EyeOff,
|
Self::EyeOff,
|
||||||
Self::PanelLeft,
|
Self::PanelLeft,
|
||||||
@@ -77,6 +79,7 @@ impl IconRole {
|
|||||||
Self::Folder => "icons/folder.svg",
|
Self::Folder => "icons/folder.svg",
|
||||||
Self::Warning => "icons/triangle-alert.svg",
|
Self::Warning => "icons/triangle-alert.svg",
|
||||||
Self::Info => "icons/info.svg",
|
Self::Info => "icons/info.svg",
|
||||||
|
Self::Copy => "icons/copy.svg",
|
||||||
Self::Eye => "icons/eye.svg",
|
Self::Eye => "icons/eye.svg",
|
||||||
Self::EyeOff => "icons/eye-off.svg",
|
Self::EyeOff => "icons/eye-off.svg",
|
||||||
Self::PanelLeft => "icons/panel-left.svg",
|
Self::PanelLeft => "icons/panel-left.svg",
|
||||||
@@ -102,6 +105,7 @@ impl IconRole {
|
|||||||
Self::Folder => "folder",
|
Self::Folder => "folder",
|
||||||
Self::Warning => "warning",
|
Self::Warning => "warning",
|
||||||
Self::Info => "info",
|
Self::Info => "info",
|
||||||
|
Self::Copy => "copy",
|
||||||
Self::Eye => "eye",
|
Self::Eye => "eye",
|
||||||
Self::EyeOff => "eye-off",
|
Self::EyeOff => "eye-off",
|
||||||
Self::PanelLeft => "panel-left",
|
Self::PanelLeft => "panel-left",
|
||||||
@@ -160,6 +164,7 @@ impl IconRole {
|
|||||||
Self::Folder => &["folder-symbolic", "folder"],
|
Self::Folder => &["folder-symbolic", "folder"],
|
||||||
Self::Warning => &["dialog-warning-symbolic", "dialog-warning", "warning"],
|
Self::Warning => &["dialog-warning-symbolic", "dialog-warning", "warning"],
|
||||||
Self::Info => &["dialog-information-symbolic", "dialog-information", "info"],
|
Self::Info => &["dialog-information-symbolic", "dialog-information", "info"],
|
||||||
|
Self::Copy => &["edit-copy-symbolic", "edit-copy"],
|
||||||
Self::Eye => &["view-visible-symbolic", "view-visible", "visibility", "eye"],
|
Self::Eye => &["view-visible-symbolic", "view-visible", "visibility", "eye"],
|
||||||
Self::EyeOff => &[
|
Self::EyeOff => &[
|
||||||
"view-conceal-symbolic",
|
"view-conceal-symbolic",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ pub(crate) fn glyph(role: IconRole) -> Option<char> {
|
|||||||
IconRole::Folder => 0xE8B7, // Folder
|
IconRole::Folder => 0xE8B7, // Folder
|
||||||
IconRole::Warning => 0xE7BA, // Warning
|
IconRole::Warning => 0xE7BA, // Warning
|
||||||
IconRole::Info => 0xE946, // Info
|
IconRole::Info => 0xE946, // Info
|
||||||
|
IconRole::Copy => 0xE8C8, // Copy
|
||||||
IconRole::Eye => 0xE7B3, // RedEye
|
IconRole::Eye => 0xE7B3, // RedEye
|
||||||
IconRole::EyeOff => 0xED1A, // Hide
|
IconRole::EyeOff => 0xED1A, // Hide
|
||||||
IconRole::PanelLeft => 0xE90C, // DockLeft
|
IconRole::PanelLeft => 0xE90C, // DockLeft
|
||||||
|
|||||||
@@ -11,6 +11,26 @@ is the first downstream consumer; its screens drive the order.
|
|||||||
|
|
||||||
## Queue — to follow
|
## Queue — to follow
|
||||||
|
|
||||||
|
Gaps surfaced by the signed comparison; ordered by signed's usage.
|
||||||
|
|
||||||
|
- [ ] **Virtual list** — generic virtualized variable-height rows with a
|
||||||
|
scroll handle, promoted from the editor views' pattern over gpui's
|
||||||
|
`list`; signed's PR lists and commit diff drive it.
|
||||||
|
- [ ] **Editable code field** — syntax-highlighted editable multiline field,
|
||||||
|
the code view's highlighter over the input's editing model; signed's
|
||||||
|
repo file browser.
|
||||||
|
- [ ] **Separator** — standalone horizontal/vertical rule; menu-item
|
||||||
|
separators stay menu-scoped.
|
||||||
|
- [ ] **Resizable group** — stacked arbitrary panes with drag dividers
|
||||||
|
outside the dock; signed's commit-diff split.
|
||||||
|
- [ ] **Searchable list filter** — searchable collection behind combobox and
|
||||||
|
list option sources; the combobox's pluggable filter covers part of it
|
||||||
|
(signed's `SearchableVec`).
|
||||||
|
- [ ] **Dropdown menu** — trigger button + popover + menu composite, three
|
||||||
|
signed call sites; must stay a real component, not a wrapper.
|
||||||
|
- [ ] **Motion easing helpers** — cubic ease-out and lerp beside
|
||||||
|
`theme::Motion`; low priority, gpui's animation primitives cover most
|
||||||
|
cases.
|
||||||
- [ ] **Markdown view follow-ups** — streaming `append` with a coalesced
|
- [ ] **Markdown view follow-ups** — streaming `append` with a coalesced
|
||||||
background parse, text selection with copy, per-block a11y roles,
|
background parse, text selection with copy, per-block a11y roles,
|
||||||
GFM alerts + table column alignment, per-fence highlight reuse,
|
GFM alerts + table column alignment, per-fence highlight reuse,
|
||||||
@@ -59,6 +79,8 @@ is the first downstream consumer; its screens drive the order.
|
|||||||
- [x] Command palette — filtered actions; the matcher stays caller-owned
|
- [x] Command palette — filtered actions; the matcher stays caller-owned
|
||||||
- [x] Scrollbar — overlay thumbs for gpui's `list` and `uniform_list`,
|
- [x] Scrollbar — overlay thumbs for gpui's `list` and `uniform_list`,
|
||||||
platform autohide with a `visibility` override
|
platform autohide with a `visibility` override
|
||||||
|
- [x] Clipboard button — copies a value, confirms with a brief check mark
|
||||||
|
that reverts itself
|
||||||
|
|
||||||
### data (`crates/data`)
|
### data (`crates/data`)
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use gpui_native_core::avatar::Avatar;
|
|||||||
use gpui_native_core::badge::{Badge, BadgeVariant};
|
use gpui_native_core::badge::{Badge, BadgeVariant};
|
||||||
use gpui_native_core::button::{Button, ButtonVariant};
|
use gpui_native_core::button::{Button, ButtonVariant};
|
||||||
use gpui_native_core::checkbox::Checkbox;
|
use gpui_native_core::checkbox::Checkbox;
|
||||||
|
use gpui_native_core::clipboard::ClipboardButton;
|
||||||
use gpui_native_core::combobox::{Combobox, ComboboxOption, ComboboxState};
|
use gpui_native_core::combobox::{Combobox, ComboboxOption, ComboboxState};
|
||||||
use gpui_native_core::command_palette::{Command, CommandPalette, CommandPaletteState};
|
use gpui_native_core::command_palette::{Command, CommandPalette, CommandPaletteState};
|
||||||
use gpui_native_core::common::{Sizable as _, Size, h_flex, v_flex};
|
use gpui_native_core::common::{Sizable as _, Size, h_flex, v_flex};
|
||||||
@@ -72,6 +73,7 @@ const CATALOG: &[(&str, &[(&str, &str)])] = &[
|
|||||||
("segmented", "Segmented"),
|
("segmented", "Segmented"),
|
||||||
("select", "Select"),
|
("select", "Select"),
|
||||||
("link", "Link"),
|
("link", "Link"),
|
||||||
|
("clipboard", "Clipboard"),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -496,6 +498,12 @@ impl Gallery {
|
|||||||
}),
|
}),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
dock.register_panel(
|
||||||
|
PanelDescriptor::new("clipboard", "Clipboard", |_, cx| {
|
||||||
|
cx.new(|_| ClipboardDemo).into()
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
dock.register_panel(
|
dock.register_panel(
|
||||||
PanelDescriptor::new("badge", "Badge", |_, cx| cx.new(|_| BadgeDemo).into()),
|
PanelDescriptor::new("badge", "Badge", |_, cx| cx.new(|_| BadgeDemo).into()),
|
||||||
cx,
|
cx,
|
||||||
@@ -1624,6 +1632,41 @@ impl Render for LinkDemo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ClipboardDemo;
|
||||||
|
|
||||||
|
impl Render for ClipboardDemo {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let colors = cx.theme().colors;
|
||||||
|
|
||||||
|
ScrollArea::new("clipboard-demo")
|
||||||
|
.size_full()
|
||||||
|
.p_4()
|
||||||
|
.child(demo_section(
|
||||||
|
"sizes",
|
||||||
|
colors,
|
||||||
|
h_flex()
|
||||||
|
.flex_wrap()
|
||||||
|
.gap_3()
|
||||||
|
.child(ClipboardButton::new("clipboard-small", "gpui-native-kit").small())
|
||||||
|
.child(ClipboardButton::new("clipboard-medium", "gpui-native-kit"))
|
||||||
|
.child(ClipboardButton::new("clipboard-large", "gpui-native-kit").large()),
|
||||||
|
))
|
||||||
|
.child(demo_section(
|
||||||
|
"states",
|
||||||
|
colors,
|
||||||
|
h_flex()
|
||||||
|
.flex_wrap()
|
||||||
|
.gap_3()
|
||||||
|
.child(
|
||||||
|
ClipboardButton::new("clipboard-url", "https://example.com/repo.git")
|
||||||
|
.tooltip(Tooltip::text("Copy repository URL"))
|
||||||
|
.accessibility_label("Copy repository URL"),
|
||||||
|
)
|
||||||
|
.child(ClipboardButton::new("clipboard-disabled", "drift").disabled(true)),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct BadgeDemo;
|
struct BadgeDemo;
|
||||||
|
|
||||||
impl Render for BadgeDemo {
|
impl Render for BadgeDemo {
|
||||||
|
|||||||
Reference in New Issue
Block a user