ccf_gpui_widgets/widgets/
repeatable_text_input.rs1use gpui::prelude::*;
34use gpui::*;
35use crate::theme::{get_theme, Theme};
36use super::text_input::{TextInput, TextInputEvent};
37use super::focus_navigation::{repeatable_add_button, repeatable_remove_button};
38
39actions!(ccf_repeatable_text_input, [ActivateButton]);
41
42pub fn register_keybindings(cx: &mut App) {
49 cx.bind_keys([
50 KeyBinding::new("enter", ActivateButton, Some("CcfRepeatableButton")),
51 KeyBinding::new("space", ActivateButton, Some("CcfRepeatableButton")),
52 ]);
53}
54
55#[derive(Debug, Clone)]
57pub enum RepeatableTextInputEvent {
58 Change(Vec<String>),
60 EntryAdded(usize),
62 EntryRemoved(usize),
64}
65
66pub struct RepeatableTextInput {
70 placeholder: Option<SharedString>,
71 initial_values: Vec<String>,
73 entries: Vec<Entity<TextInput>>,
75 remove_focus_handles: Vec<FocusHandle>,
77 add_focus_handle: FocusHandle,
79 initialized: bool,
81 min_entries: usize,
83 custom_theme: Option<Theme>,
84 enabled: bool,
86 action_just_handled: bool,
89}
90
91impl RepeatableTextInput {
92 pub fn new(cx: &mut Context<Self>) -> Self {
94 Self {
95 placeholder: None,
96 initial_values: Vec::new(),
97 entries: Vec::new(),
98 remove_focus_handles: Vec::new(),
99 add_focus_handle: cx.focus_handle().tab_stop(true),
100 initialized: false,
101 min_entries: 1,
102 custom_theme: None,
103 enabled: true,
104 action_just_handled: false,
105 }
106 }
107
108 #[must_use]
110 pub fn with_values(mut self, values: Vec<String>) -> Self {
111 self.initial_values = values;
112 self
113 }
114
115 #[must_use]
117 pub fn placeholder(mut self, text: impl Into<SharedString>) -> Self {
118 self.placeholder = Some(text.into());
119 self
120 }
121
122 #[must_use]
126 pub fn min_entries(mut self, min: usize) -> Self {
127 self.min_entries = min.max(1); self
129 }
130
131 #[must_use]
133 pub fn theme(mut self, theme: Theme) -> Self {
134 self.custom_theme = Some(theme);
135 self
136 }
137
138 #[must_use]
140 pub fn with_enabled(mut self, enabled: bool) -> Self {
141 self.enabled = enabled;
142 self
143 }
144
145 pub fn is_enabled(&self) -> bool {
147 self.enabled
148 }
149
150 pub fn set_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
152 if self.enabled != enabled {
153 self.enabled = enabled;
154 for entry in &self.entries {
156 entry.update(cx, |e, cx| e.set_enabled(enabled, cx));
157 }
158 cx.notify();
159 }
160 }
161
162 pub fn values(&self, cx: &App) -> Vec<String> {
164 self.entries
165 .iter()
166 .map(|entry| entry.read(cx).content().to_string())
167 .filter(|s| !s.is_empty())
168 .collect()
169 }
170
171 fn create_entry(&self, value: Option<&str>, cx: &mut Context<Self>) -> Entity<TextInput> {
173 let placeholder = self.placeholder.clone();
174 let enabled = self.enabled;
175 let entry = cx.new(|cx| {
176 let mut state = TextInput::new(cx).with_enabled(enabled);
177 if let Some(ph) = placeholder {
178 state = state.placeholder(ph);
179 }
180 state
181 });
182
183 if let Some(v) = value.filter(|s| !s.is_empty()) {
184 entry.update(cx, |state, cx| {
185 state.set_value(v, cx);
186 });
187 }
188
189 cx.subscribe(&entry, |this, _input, event: &TextInputEvent, cx| {
191 if matches!(event, TextInputEvent::Change) {
192 let values = this.values(cx);
194 cx.emit(RepeatableTextInputEvent::Change(values));
195 }
196 }).detach();
197
198 entry
199 }
200
201 fn initialize_entries(&mut self, cx: &mut Context<Self>) {
203 if self.initialized {
204 return;
205 }
206 self.initialized = true;
207
208 let values = std::mem::take(&mut self.initial_values);
209 let values = if values.is_empty() {
211 vec![String::new(); self.min_entries]
212 } else if values.len() < self.min_entries {
213 let mut v = values;
214 v.resize(self.min_entries, String::new());
215 v
216 } else {
217 values
218 };
219
220 for value in values {
221 let entry = self.create_entry(Some(&value), cx);
222 self.entries.push(entry);
223 self.remove_focus_handles.push(cx.focus_handle().tab_stop(true));
225 }
226 }
227
228 fn add_entry(&mut self, cx: &mut Context<Self>) {
229 let index = self.entries.len();
230 let entry = self.create_entry(None, cx);
231 self.entries.push(entry);
232 self.remove_focus_handles.push(cx.focus_handle().tab_stop(true));
234 cx.emit(RepeatableTextInputEvent::EntryAdded(index));
235 cx.emit(RepeatableTextInputEvent::Change(self.values(cx)));
236 cx.notify();
237 }
238
239 fn remove_entry(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
240 if self.entries.len() > self.min_entries && index < self.entries.len() {
241 let had_focus = self.remove_focus_handles[index].is_focused(window);
243
244 self.entries.remove(index);
245 self.remove_focus_handles.remove(index);
246
247 if had_focus {
249 if self.entries.len() <= self.min_entries {
250 self.entries[0].read(cx).focus_handle().focus(window);
252 } else if index > 0 {
253 self.remove_focus_handles[index - 1].focus(window);
255 } else {
256 self.remove_focus_handles[0].focus(window);
258 }
259 }
260
261 cx.emit(RepeatableTextInputEvent::EntryRemoved(index));
262 cx.emit(RepeatableTextInputEvent::Change(self.values(cx)));
263 cx.notify();
264 }
265 }
266
267 fn get_theme(&self, cx: &App) -> Theme {
268 self.custom_theme.unwrap_or_else(|| get_theme(cx))
269 }
270}
271
272impl EventEmitter<RepeatableTextInputEvent> for RepeatableTextInput {}
273
274impl Render for RepeatableTextInput {
275 fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
276 self.initialize_entries(cx);
278
279 let theme = self.get_theme(cx);
280 let entries_count = self.entries.len();
281 let can_remove = entries_count > self.min_entries;
282 let add_focused = self.add_focus_handle.is_focused(window);
283 let enabled = self.enabled;
284
285 let entry_data: Vec<_> = self.entries.iter()
287 .zip(self.remove_focus_handles.iter())
288 .enumerate()
289 .map(|(index, (entry, focus_handle))| {
290 let is_focused = focus_handle.is_focused(window);
291 (index, entry.clone(), focus_handle.clone(), is_focused)
292 })
293 .collect();
294
295 div()
296 .flex()
297 .flex_col()
298 .gap_2()
299 .child(
300 div()
302 .flex()
303 .flex_col()
304 .gap_2()
305 .children(entry_data.into_iter().map(|(index, entry, focus_handle, is_focused)| {
306 let remove_button = repeatable_remove_button(
307 format!("repeatable_remove_{}", index),
308 &focus_handle,
309 &theme,
310 enabled,
311 is_focused,
312 move |this: &mut Self, window, cx| {
314 this.action_just_handled = true;
315 this.remove_entry(index, window, cx);
316 },
317 move |this: &mut Self, window, cx| {
319 if this.action_just_handled {
320 this.action_just_handled = false;
321 return;
322 }
323 this.remove_entry(index, window, cx);
324 },
325 cx,
326 );
327
328 div()
329 .flex()
330 .flex_row()
331 .items_center()
332 .gap_2()
333 .child(
334 div()
336 .flex_1()
337 .child(entry)
338 )
339 .when(can_remove, |d| d.child(remove_button))
340 }))
341 )
342 .child(
343 div()
345 .flex()
346 .flex_row()
347 .child(
348 repeatable_add_button(
349 "repeatable_add_button",
350 &self.add_focus_handle,
351 &theme,
352 enabled,
353 add_focused,
354 |this: &mut Self, _window, cx| {
356 this.action_just_handled = true;
357 this.add_entry(cx);
358 },
359 |this: &mut Self, _window, cx| {
361 if this.action_just_handled {
362 this.action_just_handled = false;
363 return;
364 }
365 this.add_entry(cx);
366 },
367 cx,
368 )
369 )
370 )
371 }
372}