gpui_component/searchable_list/
state.rs1use gpui::{
2 AnyElement, App, AppContext as _, Bounds, Context, Entity, FocusHandle, Focusable as _, Length,
3 Pixels, StyleRefinement, Subscription, Window,
4};
5
6use gpui_base::DeferredPopover;
7
8use crate::{IndexPath, Size, list::ListState, searchable_list::adapter::SearchableListAdapter};
9
10use super::delegate::{SearchableListDelegate, SearchableListItem};
11
12pub struct SearchableListState<D: SearchableListDelegate + 'static>
18where
19 <D::Item as SearchableListItem>::Value: PartialEq + Clone,
20{
21 pub focus_handle: FocusHandle,
22 pub(crate) list: Entity<ListState<SearchableListAdapter<D>>>,
23 pub(crate) selection: Vec<(IndexPath, D::Item)>,
24 pub(crate) open: bool,
25 pub(crate) deferred_context: Option<DeferredPopover>,
28 pub(crate) bounds: Bounds<Pixels>,
29
30 pub(crate) size: Size,
32 pub(crate) style: StyleRefinement,
33 pub(crate) cleanable: bool,
34 pub(crate) placeholder: Option<gpui::SharedString>,
35 pub(crate) search_placeholder: Option<gpui::SharedString>,
36 pub(crate) menu_width: Length,
37 pub(crate) menu_max_h: Length,
38 pub(crate) disabled: bool,
39 pub(crate) appearance: bool,
40 pub(crate) empty: Option<Box<dyn Fn(&mut Window, &App) -> AnyElement + 'static>>,
41
42 pub(crate) _subscriptions: Vec<Subscription>,
43}
44
45#[allow(private_bounds)]
46impl<D: SearchableListDelegate + 'static> SearchableListState<D>
47where
48 <D::Item as SearchableListItem>::Value: PartialEq + Clone,
49{
50 #[allow(clippy::too_many_arguments)]
56 pub fn new<P: 'static>(
57 delegate: D,
58 selected_indices: Vec<IndexPath>,
59 on_confirm: impl Fn(
60 Option<IndexPath>,
61 bool,
62 &mut Window,
63 &mut Context<ListState<SearchableListAdapter<D>>>,
64 ) + 'static,
65 on_cancel: impl Fn(
66 Option<IndexPath>,
67 &mut Window,
68 &mut Context<ListState<SearchableListAdapter<D>>>,
69 ) + 'static,
70 on_render_empty: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
71 on_blur: fn(&mut P, &mut Window, &mut Context<P>),
72 window: &mut Window,
73 cx: &mut Context<P>,
74 ) -> Self {
75 let focus_handle = cx.focus_handle();
76
77 let adapter = SearchableListAdapter::new(delegate, on_confirm, on_cancel, on_render_empty);
78 let list = cx.new(|cx| ListState::new(adapter, window, cx).reset_on_cancel(false));
79
80 let list_focus_handle = list.read(cx).focus_handle.clone();
81 let list_search_focus_handle = list.read(cx).query_input.focus_handle(cx);
82
83 let selection = {
84 let delegate = &list.read(cx).delegate().delegate;
85
86 selected_indices
87 .iter()
88 .copied()
89 .filter_map(|ix| delegate.item(ix).map(|i| (ix, i.clone())))
90 .collect::<Vec<_>>()
91 };
92
93 if let Some(cursor) = selected_indices.first().copied() {
94 list.update(cx, |l, cx| {
95 l.set_selected_index(Some(cursor), window, cx);
96 });
97 }
98
99 let initial_snapshot = selection.clone();
101 list.update(cx, |l, _| {
102 l.delegate_mut().update_selection_snapshot(initial_snapshot);
103 });
104
105 let _subscriptions = vec![
106 cx.on_blur(&list_focus_handle, window, on_blur),
107 cx.on_blur(&list_search_focus_handle, window, on_blur),
108 cx.on_blur(&focus_handle, window, on_blur),
109 ];
110
111 Self {
112 focus_handle,
113 list,
114 selection,
115 open: false,
116 deferred_context: None,
117 bounds: Bounds::default(),
118 size: Size::default(),
119 style: StyleRefinement::default(),
120 cleanable: false,
121 placeholder: None,
122 search_placeholder: None,
123 menu_width: Length::Auto,
124 menu_max_h: gpui::rems(20.).into(),
125 disabled: false,
126 appearance: true,
127 empty: None,
128 _subscriptions,
129 }
130 }
131
132 pub fn selection(&self) -> &[(IndexPath, D::Item)] {
135 &self.selection
136 }
137
138 pub fn selected_values(&self) -> Vec<<D::Item as SearchableListItem>::Value> {
139 self.selection
140 .iter()
141 .map(|(_ix, i)| i.value().clone())
142 .collect()
143 }
144
145 pub fn is_open(&self) -> bool {
146 self.open
147 }
148
149 pub fn focus_handle(&self) -> &FocusHandle {
150 &self.focus_handle
151 }
152
153 pub(crate) fn clear_query(&mut self, window: &mut Window, cx: &mut App) {
167 self.list.update(cx, |list, cx| {
168 if !list.query_input.read(cx).value().is_empty() {
169 list.set_query("", window, cx);
170 }
171 });
172 }
173
174 pub(crate) fn add_by_item(&mut self, index: IndexPath, item: D::Item) {
178 if self.selection.iter().any(|(ix, _)| ix == &index) {
179 return;
180 }
181
182 self.selection.push((index, item));
183 }
184
185 pub(crate) fn remove_by_index(&mut self, index: &IndexPath) -> bool {
187 if let Some(pos) = self.selection.iter().position(|(ix, _)| ix == index) {
188 self.selection.remove(pos);
189
190 return true;
191 }
192
193 false
194 }
195
196 pub fn add_selected_index(&mut self, index: IndexPath, cx: &App) -> bool {
200 if self.selection.iter().any(|(ix, _)| ix == &index) {
201 return false;
202 }
203
204 let Some(item) = self.list.read(cx).delegate().delegate.item(index) else {
205 return false;
206 };
207
208 self.add_by_item(index, item.clone());
209
210 true
211 }
212
213 pub fn remove_selected_index(&mut self, index: IndexPath) -> bool {
215 self.remove_by_index(&index)
216 }
217
218 pub fn set_selected_indices(&mut self, indices: impl IntoIterator<Item = IndexPath>, cx: &App) {
220 let indices: Vec<IndexPath> = indices.into_iter().collect();
221
222 self.selection = indices
223 .into_iter()
224 .filter_map(|ix| {
225 self.list
226 .read(cx)
227 .delegate()
228 .delegate
229 .item(ix)
230 .map(|i| (ix, i.clone()))
231 })
232 .collect();
233 }
234
235 pub(crate) fn sync_snapshot<P: 'static>(&self, cx: &mut Context<P>) {
238 let snapshot = self.selection.clone();
239 self.list.update(cx, |l, _| {
240 l.delegate_mut().update_selection_snapshot(snapshot);
241 });
242 }
243}