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) scroll_bounce: Option<bool>,
39 pub(crate) disabled: bool,
40 pub(crate) appearance: bool,
41 pub(crate) empty: Option<Box<dyn Fn(&mut Window, &App) -> AnyElement + 'static>>,
42
43 pub(crate) _subscriptions: Vec<Subscription>,
44}
45
46#[allow(private_bounds)]
47impl<D: SearchableListDelegate + 'static> SearchableListState<D>
48where
49 <D::Item as SearchableListItem>::Value: PartialEq + Clone,
50{
51 #[allow(clippy::too_many_arguments)]
57 pub fn new<P: 'static>(
58 delegate: D,
59 selected_indices: Vec<IndexPath>,
60 on_confirm: impl Fn(
61 Option<IndexPath>,
62 bool,
63 &mut Window,
64 &mut Context<ListState<SearchableListAdapter<D>>>,
65 ) + 'static,
66 on_cancel: impl Fn(
67 Option<IndexPath>,
68 &mut Window,
69 &mut Context<ListState<SearchableListAdapter<D>>>,
70 ) + 'static,
71 on_render_empty: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
72 on_blur: fn(&mut P, &mut Window, &mut Context<P>),
73 window: &mut Window,
74 cx: &mut Context<P>,
75 ) -> Self {
76 let focus_handle = cx.focus_handle();
77
78 let adapter = SearchableListAdapter::new(delegate, on_confirm, on_cancel, on_render_empty);
79 let list = cx.new(|cx| ListState::new(adapter, window, cx).reset_on_cancel(false));
80
81 let list_focus_handle = list.read(cx).focus_handle.clone();
82 let list_search_focus_handle = list.read(cx).query_input.focus_handle(cx);
83
84 let selection = {
85 let delegate = &list.read(cx).delegate().delegate;
86
87 selected_indices
88 .iter()
89 .copied()
90 .filter_map(|ix| delegate.item(ix).map(|i| (ix, i.clone())))
91 .collect::<Vec<_>>()
92 };
93
94 if let Some(cursor) = selected_indices.first().copied() {
95 list.update(cx, |l, cx| {
96 l.set_selected_index(Some(cursor), window, cx);
97 });
98 }
99
100 let initial_snapshot = selection.clone();
102 list.update(cx, |l, _| {
103 l.delegate_mut().update_selection_snapshot(initial_snapshot);
104 });
105
106 let _subscriptions = vec![
107 cx.on_blur(&list_focus_handle, window, on_blur),
108 cx.on_blur(&list_search_focus_handle, window, on_blur),
109 cx.on_blur(&focus_handle, window, on_blur),
110 ];
111
112 Self {
113 focus_handle,
114 list,
115 selection,
116 open: false,
117 deferred_context: None,
118 bounds: Bounds::default(),
119 size: Size::default(),
120 style: StyleRefinement::default(),
121 cleanable: false,
122 placeholder: None,
123 search_placeholder: None,
124 menu_width: Length::Auto,
125 menu_max_h: gpui::rems(20.).into(),
126 scroll_bounce: None,
127 disabled: false,
128 appearance: true,
129 empty: None,
130 _subscriptions,
131 }
132 }
133
134 pub fn selection(&self) -> &[(IndexPath, D::Item)] {
137 &self.selection
138 }
139
140 pub fn selected_values(&self) -> Vec<<D::Item as SearchableListItem>::Value> {
141 self.selection
142 .iter()
143 .map(|(_ix, i)| i.value().clone())
144 .collect()
145 }
146
147 pub fn is_open(&self) -> bool {
148 self.open
149 }
150
151 pub fn focus_handle(&self) -> &FocusHandle {
152 &self.focus_handle
153 }
154
155 pub(crate) fn clear_query(&mut self, window: &mut Window, cx: &mut App) {
169 self.list.update(cx, |list, cx| {
170 if !list.query_input.read(cx).value().is_empty() {
171 list.set_query("", window, cx);
172 }
173 });
174 }
175
176 pub(crate) fn add_by_item(&mut self, index: IndexPath, item: D::Item) {
180 if self.selection.iter().any(|(ix, _)| ix == &index) {
181 return;
182 }
183
184 self.selection.push((index, item));
185 }
186
187 pub(crate) fn remove_by_index(&mut self, index: &IndexPath) -> bool {
189 if let Some(pos) = self.selection.iter().position(|(ix, _)| ix == index) {
190 self.selection.remove(pos);
191
192 return true;
193 }
194
195 false
196 }
197
198 pub fn add_selected_index(&mut self, index: IndexPath, cx: &App) -> bool {
202 if self.selection.iter().any(|(ix, _)| ix == &index) {
203 return false;
204 }
205
206 let Some(item) = self.list.read(cx).delegate().delegate.item(index) else {
207 return false;
208 };
209
210 self.add_by_item(index, item.clone());
211
212 true
213 }
214
215 pub fn remove_selected_index(&mut self, index: IndexPath) -> bool {
217 self.remove_by_index(&index)
218 }
219
220 pub fn set_selected_indices(&mut self, indices: impl IntoIterator<Item = IndexPath>, cx: &App) {
222 let indices: Vec<IndexPath> = indices.into_iter().collect();
223
224 self.selection = indices
225 .into_iter()
226 .filter_map(|ix| {
227 self.list
228 .read(cx)
229 .delegate()
230 .delegate
231 .item(ix)
232 .map(|i| (ix, i.clone()))
233 })
234 .collect();
235 }
236
237 pub(crate) fn sync_snapshot<P: 'static>(&self, cx: &mut Context<P>) {
240 let snapshot = self.selection.clone();
241 self.list.update(cx, |l, _| {
242 l.delegate_mut().update_selection_snapshot(snapshot);
243 });
244 }
245}