use gpui::prelude::*;
use gpui::*;
use crate::theme::{get_theme, Theme};
use super::text_input::{TextInput, TextInputEvent};
use super::focus_navigation::{repeatable_add_button, repeatable_remove_button};
actions!(ccf_repeatable_text_input, [ActivateButton]);
pub fn register_keybindings(cx: &mut App) {
cx.bind_keys([
KeyBinding::new("enter", ActivateButton, Some("CcfRepeatableButton")),
KeyBinding::new("space", ActivateButton, Some("CcfRepeatableButton")),
]);
}
#[derive(Debug, Clone)]
pub enum RepeatableTextInputEvent {
Change(Vec<String>),
EntryAdded(usize),
EntryRemoved(usize),
}
pub struct RepeatableTextInput {
placeholder: Option<SharedString>,
initial_values: Vec<String>,
entries: Vec<Entity<TextInput>>,
remove_focus_handles: Vec<FocusHandle>,
add_focus_handle: FocusHandle,
initialized: bool,
min_entries: usize,
custom_theme: Option<Theme>,
enabled: bool,
action_just_handled: bool,
}
impl RepeatableTextInput {
pub fn new(cx: &mut Context<Self>) -> Self {
Self {
placeholder: None,
initial_values: Vec::new(),
entries: Vec::new(),
remove_focus_handles: Vec::new(),
add_focus_handle: cx.focus_handle().tab_stop(true),
initialized: false,
min_entries: 1,
custom_theme: None,
enabled: true,
action_just_handled: false,
}
}
#[must_use]
pub fn with_values(mut self, values: Vec<String>) -> Self {
self.initial_values = values;
self
}
#[must_use]
pub fn placeholder(mut self, text: impl Into<SharedString>) -> Self {
self.placeholder = Some(text.into());
self
}
#[must_use]
pub fn min_entries(mut self, min: usize) -> Self {
self.min_entries = min.max(1); self
}
#[must_use]
pub fn theme(mut self, theme: Theme) -> Self {
self.custom_theme = Some(theme);
self
}
#[must_use]
pub fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn set_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
if self.enabled != enabled {
self.enabled = enabled;
for entry in &self.entries {
entry.update(cx, |e, cx| e.set_enabled(enabled, cx));
}
cx.notify();
}
}
pub fn values(&self, cx: &App) -> Vec<String> {
self.entries
.iter()
.map(|entry| entry.read(cx).content().to_string())
.filter(|s| !s.is_empty())
.collect()
}
fn create_entry(&self, value: Option<&str>, cx: &mut Context<Self>) -> Entity<TextInput> {
let placeholder = self.placeholder.clone();
let enabled = self.enabled;
let entry = cx.new(|cx| {
let mut state = TextInput::new(cx).with_enabled(enabled);
if let Some(ph) = placeholder {
state = state.placeholder(ph);
}
state
});
if let Some(v) = value.filter(|s| !s.is_empty()) {
entry.update(cx, |state, cx| {
state.set_value(v, cx);
});
}
cx.subscribe(&entry, |this, _input, event: &TextInputEvent, cx| {
if matches!(event, TextInputEvent::Change) {
let values = this.values(cx);
cx.emit(RepeatableTextInputEvent::Change(values));
}
}).detach();
entry
}
fn initialize_entries(&mut self, cx: &mut Context<Self>) {
if self.initialized {
return;
}
self.initialized = true;
let values = std::mem::take(&mut self.initial_values);
let values = if values.is_empty() {
vec![String::new(); self.min_entries]
} else if values.len() < self.min_entries {
let mut v = values;
v.resize(self.min_entries, String::new());
v
} else {
values
};
for value in values {
let entry = self.create_entry(Some(&value), cx);
self.entries.push(entry);
self.remove_focus_handles.push(cx.focus_handle().tab_stop(true));
}
}
fn add_entry(&mut self, cx: &mut Context<Self>) {
let index = self.entries.len();
let entry = self.create_entry(None, cx);
self.entries.push(entry);
self.remove_focus_handles.push(cx.focus_handle().tab_stop(true));
cx.emit(RepeatableTextInputEvent::EntryAdded(index));
cx.emit(RepeatableTextInputEvent::Change(self.values(cx)));
cx.notify();
}
fn remove_entry(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
if self.entries.len() > self.min_entries && index < self.entries.len() {
let had_focus = self.remove_focus_handles[index].is_focused(window);
self.entries.remove(index);
self.remove_focus_handles.remove(index);
if had_focus {
if self.entries.len() <= self.min_entries {
self.entries[0].read(cx).focus_handle().focus(window);
} else if index > 0 {
self.remove_focus_handles[index - 1].focus(window);
} else {
self.remove_focus_handles[0].focus(window);
}
}
cx.emit(RepeatableTextInputEvent::EntryRemoved(index));
cx.emit(RepeatableTextInputEvent::Change(self.values(cx)));
cx.notify();
}
}
fn get_theme(&self, cx: &App) -> Theme {
self.custom_theme.unwrap_or_else(|| get_theme(cx))
}
}
impl EventEmitter<RepeatableTextInputEvent> for RepeatableTextInput {}
impl Render for RepeatableTextInput {
fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
self.initialize_entries(cx);
let theme = self.get_theme(cx);
let entries_count = self.entries.len();
let can_remove = entries_count > self.min_entries;
let add_focused = self.add_focus_handle.is_focused(window);
let enabled = self.enabled;
let entry_data: Vec<_> = self.entries.iter()
.zip(self.remove_focus_handles.iter())
.enumerate()
.map(|(index, (entry, focus_handle))| {
let is_focused = focus_handle.is_focused(window);
(index, entry.clone(), focus_handle.clone(), is_focused)
})
.collect();
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.flex()
.flex_col()
.gap_2()
.children(entry_data.into_iter().map(|(index, entry, focus_handle, is_focused)| {
let remove_button = repeatable_remove_button(
format!("repeatable_remove_{}", index),
&focus_handle,
&theme,
enabled,
is_focused,
move |this: &mut Self, window, cx| {
this.action_just_handled = true;
this.remove_entry(index, window, cx);
},
move |this: &mut Self, window, cx| {
if this.action_just_handled {
this.action_just_handled = false;
return;
}
this.remove_entry(index, window, cx);
},
cx,
);
div()
.flex()
.flex_row()
.items_center()
.gap_2()
.child(
div()
.flex_1()
.child(entry)
)
.when(can_remove, |d| d.child(remove_button))
}))
)
.child(
div()
.flex()
.flex_row()
.child(
repeatable_add_button(
"repeatable_add_button",
&self.add_focus_handle,
&theme,
enabled,
add_focused,
|this: &mut Self, _window, cx| {
this.action_just_handled = true;
this.add_entry(cx);
},
|this: &mut Self, _window, cx| {
if this.action_just_handled {
this.action_just_handled = false;
return;
}
this.add_entry(cx);
},
cx,
)
)
)
}
}