ccf_gpui_widgets/widgets/
repeatable_directory_picker.rs1use gpui::prelude::*;
38use gpui::*;
39use crate::theme::{get_theme, Theme};
40use super::directory_picker::{
41 DirectoryPicker, DirectoryPickerEvent,
42 DirectoryPickerValidation, ValidationDisplay,
43};
44use super::focus_navigation::{repeatable_add_button, repeatable_remove_button};
45
46#[derive(Debug, Clone)]
48pub enum RepeatableDirectoryPickerEvent {
49 Change(Vec<String>),
51 EntryAdded(usize),
53 EntryRemoved(usize),
55}
56
57pub struct RepeatableDirectoryPicker {
61 placeholder: Option<SharedString>,
63 browse_shortcut_enabled: bool,
64 validation_display: ValidationDisplay,
65
66 initial_values: Vec<String>,
68 entries: Vec<Entity<DirectoryPicker>>,
70 remove_focus_handles: Vec<FocusHandle>,
72 add_focus_handle: FocusHandle,
74 initialized: bool,
76 min_entries: usize,
78 custom_theme: Option<Theme>,
79 enabled: bool,
81 action_just_handled: bool,
84}
85
86impl RepeatableDirectoryPicker {
87 pub fn new(cx: &mut Context<Self>) -> Self {
89 Self {
90 placeholder: None,
91 browse_shortcut_enabled: true,
92 validation_display: ValidationDisplay::default(),
93 initial_values: Vec::new(),
94 entries: Vec::new(),
95 remove_focus_handles: Vec::new(),
96 add_focus_handle: cx.focus_handle().tab_stop(true),
97 initialized: false,
98 min_entries: 1,
99 custom_theme: None,
100 enabled: true,
101 action_just_handled: false,
102 }
103 }
104
105 #[must_use]
107 pub fn with_values(mut self, values: Vec<String>) -> Self {
108 self.initial_values = values;
109 self
110 }
111
112 #[must_use]
114 pub fn placeholder(mut self, text: impl Into<SharedString>) -> Self {
115 self.placeholder = Some(text.into());
116 self
117 }
118
119 #[must_use]
124 pub fn browse_shortcut(mut self, enabled: bool) -> Self {
125 self.browse_shortcut_enabled = enabled;
126 self
127 }
128
129 #[must_use]
134 pub fn validation_display(mut self, display: ValidationDisplay) -> Self {
135 self.validation_display = display;
136 self
137 }
138
139 #[must_use]
141 pub fn min_entries(mut self, min: usize) -> Self {
142 self.min_entries = min.max(1);
143 self
144 }
145
146 #[must_use]
148 pub fn theme(mut self, theme: Theme) -> Self {
149 self.custom_theme = Some(theme);
150 self
151 }
152
153 #[must_use]
155 pub fn with_enabled(mut self, enabled: bool) -> Self {
156 self.enabled = enabled;
157 self
158 }
159
160 pub fn is_enabled(&self) -> bool {
162 self.enabled
163 }
164
165 pub fn set_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
167 if self.enabled != enabled {
168 self.enabled = enabled;
169 for entry in &self.entries {
171 entry.update(cx, |e, cx| e.set_enabled(enabled, cx));
172 }
173 cx.notify();
174 }
175 }
176
177 pub fn values(&self, cx: &App) -> Vec<String> {
179 self.entries
180 .iter()
181 .map(|entry| entry.read(cx).value().to_string())
182 .filter(|s| !s.is_empty())
183 .collect()
184 }
185
186 pub fn entries(&self) -> &[Entity<DirectoryPicker>] {
188 &self.entries
189 }
190
191 pub fn validate_all(&self, cx: &App) -> Vec<DirectoryPickerValidation> {
193 self.entries
194 .iter()
195 .map(|entry| entry.read(cx).validate())
196 .collect()
197 }
198
199 pub fn is_all_valid(&self, cx: &App) -> bool {
201 self.entries
202 .iter()
203 .all(|entry| {
204 let picker = entry.read(cx);
205 picker.value().is_empty() || picker.is_valid()
206 })
207 }
208
209 fn create_entry(&self, value: Option<&str>, cx: &mut Context<Self>) -> Entity<DirectoryPicker> {
211 let placeholder = self.placeholder.clone();
212 let browse_shortcut_enabled = self.browse_shortcut_enabled;
213 let validation_display = self.validation_display.clone();
214 let theme = self.custom_theme;
215 let enabled = self.enabled;
216
217 let picker = cx.new(|cx| {
218 let mut p = DirectoryPicker::new(cx)
219 .browse_shortcut(browse_shortcut_enabled)
220 .validation_display(validation_display)
221 .with_enabled(enabled);
222
223 if let Some(ph) = placeholder {
224 p = p.placeholder(ph);
225 }
226 if let Some(th) = theme {
227 p = p.theme(th);
228 }
229 p
230 });
231
232 if let Some(v) = value.filter(|s| !s.is_empty()) {
233 picker.update(cx, |p, cx| {
234 p.set_value(v, cx);
235 });
236 }
237
238 cx.subscribe(&picker, |this, _picker, event: &DirectoryPickerEvent, cx| {
240 let DirectoryPickerEvent::Change(_) = event;
241 let values = this.values(cx);
242 cx.emit(RepeatableDirectoryPickerEvent::Change(values));
243 }).detach();
244
245 picker
246 }
247
248 fn initialize_entries(&mut self, cx: &mut Context<Self>) {
250 if self.initialized {
251 return;
252 }
253 self.initialized = true;
254
255 let values = std::mem::take(&mut self.initial_values);
256 let values = if values.is_empty() {
257 vec![String::new(); self.min_entries]
258 } else if values.len() < self.min_entries {
259 let mut v = values;
260 v.resize(self.min_entries, String::new());
261 v
262 } else {
263 values
264 };
265
266 for value in values {
267 let entry = self.create_entry(Some(&value), cx);
268 self.entries.push(entry);
269 self.remove_focus_handles.push(cx.focus_handle().tab_stop(true));
270 }
271 }
272
273 fn add_entry(&mut self, cx: &mut Context<Self>) {
274 let index = self.entries.len();
275 let entry = self.create_entry(None, cx);
276 self.entries.push(entry);
277 self.remove_focus_handles.push(cx.focus_handle().tab_stop(true));
278 cx.emit(RepeatableDirectoryPickerEvent::EntryAdded(index));
279 cx.emit(RepeatableDirectoryPickerEvent::Change(self.values(cx)));
280 cx.notify();
281 }
282
283 fn remove_entry(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
284 if self.entries.len() > self.min_entries && index < self.entries.len() {
285 let had_focus = self.remove_focus_handles[index].is_focused(window);
287
288 self.entries.remove(index);
289 self.remove_focus_handles.remove(index);
290
291 if had_focus {
293 if self.entries.len() <= self.min_entries {
294 self.entries[0].update(cx, |picker, cx| picker.focus(cx));
296 } else if index > 0 {
297 self.remove_focus_handles[index - 1].focus(window);
299 } else {
300 self.remove_focus_handles[0].focus(window);
302 }
303 }
304
305 cx.emit(RepeatableDirectoryPickerEvent::EntryRemoved(index));
306 cx.emit(RepeatableDirectoryPickerEvent::Change(self.values(cx)));
307 cx.notify();
308 }
309 }
310
311 fn get_theme(&self, cx: &App) -> Theme {
312 self.custom_theme.unwrap_or_else(|| get_theme(cx))
313 }
314}
315
316impl EventEmitter<RepeatableDirectoryPickerEvent> for RepeatableDirectoryPicker {}
317
318impl Render for RepeatableDirectoryPicker {
319 fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
320 self.initialize_entries(cx);
322
323 let theme = self.get_theme(cx);
324 let entries_count = self.entries.len();
325 let can_remove = entries_count > self.min_entries;
326 let add_focused = self.add_focus_handle.is_focused(window);
327 let enabled = self.enabled;
328
329 let entry_data: Vec<_> = self.entries.iter()
331 .zip(self.remove_focus_handles.iter())
332 .enumerate()
333 .map(|(index, (entry, focus_handle))| {
334 let is_focused = focus_handle.is_focused(window);
335 (index, entry.clone(), focus_handle.clone(), is_focused)
336 })
337 .collect();
338
339 div()
340 .flex()
341 .flex_col()
342 .gap_2()
343 .child(
344 div()
346 .flex()
347 .flex_col()
348 .gap_2()
349 .children(entry_data.into_iter().map(|(index, entry, focus_handle, is_focused)| {
350 let remove_button = repeatable_remove_button(
351 format!("dir_remove_{}", index),
352 &focus_handle,
353 &theme,
354 enabled,
355 is_focused,
356 move |this: &mut Self, window, cx| {
358 this.action_just_handled = true;
359 this.remove_entry(index, window, cx);
360 },
361 move |this: &mut Self, window, cx| {
363 if this.action_just_handled {
364 this.action_just_handled = false;
365 return;
366 }
367 this.remove_entry(index, window, cx);
368 },
369 cx,
370 );
371
372 div()
373 .flex()
374 .flex_row()
375 .items_center()
376 .gap_2()
377 .child(
378 div()
380 .flex_1()
381 .child(entry)
382 )
383 .when(can_remove, |d| d.child(remove_button))
384 }))
385 )
386 .child(
387 div()
389 .flex()
390 .flex_row()
391 .child(
392 repeatable_add_button(
393 "repeatable_dir_add_button",
394 &self.add_focus_handle,
395 &theme,
396 enabled,
397 add_focused,
398 |this: &mut Self, _window, cx| {
400 this.action_just_handled = true;
401 this.add_entry(cx);
402 },
403 |this: &mut Self, _window, cx| {
405 if this.action_just_handled {
406 this.action_just_handled = false;
407 return;
408 }
409 this.add_entry(cx);
410 },
411 cx,
412 )
413 )
414 )
415 }
416}