1use std::path::PathBuf;
8use std::rc::Rc;
9
10use gpui::prelude::*;
11use gpui::{
12 div, px, App, ElementId, ExternalPaths, IntoElement, PathPromptOptions, SharedString, Window,
13};
14
15use super::accept::{filter_paths, normalize_ext};
16use crate::devtools::Probed;
17use crate::icon::{Icon, IconName};
18use crate::theme::{theme, Size};
19
20type FilesHandler = Rc<dyn Fn(Vec<PathBuf>, &mut App) + 'static>;
21
22#[derive(IntoElement)]
24pub struct Dropzone {
25 id: ElementId,
26 label: SharedString,
27 hint: Option<SharedString>,
28 icon: IconName,
29 accept: Vec<String>,
30 multiple: bool,
31 clickable: bool,
32 height: f32,
33 on_files: Option<FilesHandler>,
34}
35
36impl Dropzone {
37 pub fn new(id: impl Into<ElementId>) -> Self {
38 Dropzone {
39 id: id.into(),
40 label: SharedString::new_static("Drop files here"),
41 hint: None,
42 icon: IconName::CloudUpload,
43 accept: Vec::new(),
44 multiple: true,
45 clickable: true,
46 height: 140.0,
47 on_files: None,
48 }
49 }
50
51 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
52 self.label = label.into();
53 self
54 }
55
56 pub fn hint(mut self, hint: impl Into<SharedString>) -> Self {
58 self.hint = Some(hint.into());
59 self
60 }
61
62 pub fn icon(mut self, icon: IconName) -> Self {
63 self.icon = icon;
64 self
65 }
66
67 pub fn accept<I, S>(mut self, entries: I) -> Self
70 where
71 I: IntoIterator<Item = S>,
72 S: AsRef<str>,
73 {
74 self.accept = entries
75 .into_iter()
76 .map(|e| normalize_ext(e.as_ref()))
77 .collect();
78 self
79 }
80
81 pub fn single(mut self) -> Self {
83 self.multiple = false;
84 self
85 }
86
87 pub fn no_click(mut self) -> Self {
89 self.clickable = false;
90 self
91 }
92
93 pub fn height(mut self, height: f32) -> Self {
94 self.height = height;
95 self
96 }
97
98 pub fn on_files(mut self, handler: impl Fn(Vec<PathBuf>, &mut App) + 'static) -> Self {
100 self.on_files = Some(Rc::new(handler));
101 self
102 }
103}
104
105impl RenderOnce for Dropzone {
106 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
107 let t = theme(cx);
108 let radius = t.radius(t.default_radius);
109 let surface = t.surface().hsla();
110 let border = t.border().hsla();
111 let text_color = t.text().hsla();
112 let dimmed = t.dimmed().hsla();
113 let accent = t.primary();
114 let accent_border = accent.hsla();
115 let accent_tint = accent.alpha(0.08);
116 let font = t.font_size(Size::Sm);
117
118 let accept = self.accept;
119 let multiple = self.multiple;
120 let handler = self.on_files;
121
122 let deliver = {
123 let accept = accept.clone();
124 let handler = handler.clone();
125 move |mut paths: Vec<PathBuf>, cx: &mut App| {
126 paths = filter_paths(paths, &accept);
127 if !multiple {
128 paths.truncate(1);
129 }
130 if let (Some(handler), false) = (&handler, paths.is_empty()) {
131 handler(paths, cx);
132 }
133 }
134 };
135
136 let mut zone = div()
137 .id(self.id)
138 .flex()
139 .flex_col()
140 .items_center()
141 .justify_center()
142 .gap(px(6.0))
143 .w_full()
144 .h(px(self.height))
145 .rounded(px(radius))
146 .border_1()
147 .border_dashed()
148 .border_color(border)
149 .bg(surface)
150 .drag_over::<ExternalPaths>(move |style, _paths, _window, _cx| {
151 style.border_color(accent_border).bg(accent_tint)
152 })
153 .on_drop({
154 let deliver = deliver.clone();
155 move |dropped: &ExternalPaths, _window, cx| {
156 deliver(dropped.paths().to_vec(), cx);
157 }
158 })
159 .child(
160 div()
161 .text_color(dimmed)
162 .child(Icon::new(self.icon).size(Size::Lg)),
163 )
164 .child(
165 div()
166 .text_size(px(font))
167 .text_color(text_color)
168 .child(self.label),
169 );
170
171 if let Some(hint) = self.hint {
172 zone = zone.child(
173 div()
174 .text_size(px(font - 2.0))
175 .text_color(dimmed)
176 .child(hint),
177 );
178 }
179
180 if self.clickable {
181 zone = zone.cursor_pointer().on_click(move |_ev, _window, cx| {
182 let receiver = cx.prompt_for_paths(PathPromptOptions {
183 files: true,
184 directories: false,
185 multiple,
186 prompt: None,
187 });
188 let deliver = deliver.clone();
189 cx.spawn(async move |cx| {
190 if let Ok(Ok(Some(paths))) = receiver.await {
191 cx.update(|cx| deliver(paths, cx)).ok();
192 }
193 })
194 .detach();
195 });
196 }
197
198 zone.probe("Dropzone")
199 }
200}