1use std::{
9 cell::{Cell, RefCell},
10 sync::Arc,
11};
12
13use gpui_kit::component::list::{List, ListDelegate, ListEvent, ListItem, ListState};
14use gpui_kit::component::{ActiveTheme as _, IndexPath, Sizable as _, Size as ComponentSize};
15use gpui_kit::{
16 AnyElement, App, AppContext as _, Context, Entity, IntoElement, ParentElement as _,
17 SharedString, Styled as _, Subscription, Task, Window, div, px,
18};
19use rust_i18n::t;
20
21use crate::{
22 actions::CommitEdit,
23 model::{CellValue, GridRow, OwnedValue, display_text},
24 state::edit::{
25 CellEditor, EditError, EditInput, EditorCell, EditorField, EditorOpen, EditorStart,
26 },
27};
28
29#[derive(Clone, Debug)]
31pub struct SelectOption {
32 value: OwnedValue,
33 label: SharedString,
34}
35
36impl SelectOption {
37 pub fn new(value: OwnedValue, label: impl Into<SharedString>) -> Self {
39 Self {
40 value,
41 label: label.into(),
42 }
43 }
44
45 pub fn value(&self) -> &OwnedValue {
47 &self.value
48 }
49
50 pub fn label(&self) -> &SharedString {
52 &self.label
53 }
54}
55
56#[derive(Clone, Debug)]
65pub struct SelectEditor {
66 items: Arc<[SelectOption]>,
68 has_empty: bool,
69}
70
71impl SelectEditor {
72 pub fn new(options: impl IntoIterator<Item = SelectOption>) -> Self {
74 Self {
75 items: options.into_iter().collect(),
76 has_empty: false,
77 }
78 }
79
80 pub fn with_empty(self, label: impl Into<SharedString>) -> Self {
82 let empty = SelectOption::new(OwnedValue::Empty, label);
83 let options = if self.has_empty {
84 &self.items[1..]
85 } else {
86 &self.items[..]
87 };
88 Self {
89 items: std::iter::once(empty)
90 .chain(options.iter().cloned())
91 .collect(),
92 has_empty: true,
93 }
94 }
95
96 pub fn options(&self) -> &[SelectOption] {
98 if self.has_empty {
99 &self.items[1..]
100 } else {
101 &self.items
102 }
103 }
104
105 pub fn empty_label(&self) -> Option<&SharedString> {
107 self.has_empty.then(|| self.items[0].label())
108 }
109
110 fn picked(&self, value: CellValue<'_>, input: &EditInput) -> Option<Option<usize>> {
114 match input {
115 EditInput::Clear => self.has_empty.then_some(Some(0)),
116 _ => {
117 let value = OwnedValue::from_cell(value);
118 Some(item_of(&self.items, &value))
119 }
120 }
121 }
122}
123
124fn item_of(items: &[SelectOption], value: &OwnedValue) -> Option<usize> {
126 items
127 .iter()
128 .position(|item| item.value.cmp_total(value).is_eq())
129}
130
131impl<R: GridRow> CellEditor<R> for SelectEditor {
132 fn open(&self, start: &EditorStart<'_, R>, window: &mut Window, cx: &mut App) -> EditorOpen {
133 let Some(picked) = self.picked(start.value(), start.input()) else {
134 return EditorOpen::Refuse;
135 };
136 let options = OptionList {
137 matches: (0..self.items.len()).collect(),
138 items: self.items.clone(),
139 };
140 let list = cx.new(|cx| ListState::new(options, window, cx).searchable(true));
141 let query = match start.input() {
146 EditInput::Typing(text) => Some(text.clone()),
147 _ => None,
148 };
149 list.update(cx, |list, cx| match start.input() {
150 EditInput::Typing(text) => {
151 list.delegate_mut().filter(text);
152 let first = (!list.delegate().matches.is_empty()).then(|| IndexPath::new(0));
153 list.set_selected_index(first, window, cx);
154 }
155 _ => {
156 let row = picked.map(IndexPath::new);
157 list.set_selected_index(row, window, cx);
158 if let Some(row) = row {
159 list.scroll_to_item(row, gpui_kit::ScrollStrategy::Center, window, cx);
160 }
161 }
162 });
163 let confirm = window.subscribe(&list, cx, |_, event: &ListEvent, window, cx| {
166 if let ListEvent::Confirm(_) = event {
167 window.dispatch_action(Box::new(CommitEdit), cx);
168 }
169 });
170 EditorOpen::Field(Box::new(SelectField {
171 list,
172 start: RefCell::new(OwnedValue::from_cell(start.value())),
173 initial: Cell::new(picked),
174 query: RefCell::new(query),
175 drawn: Cell::new(false),
176 _confirm: confirm,
177 }))
178 }
179}
180
181struct OptionList {
183 items: Arc<[SelectOption]>,
184 matches: Vec<usize>,
186}
187
188impl OptionList {
189 fn filter(&mut self, query: &str) {
192 let query = query.to_lowercase();
193 self.matches = (0..self.items.len())
194 .filter(|ix| query.is_empty() || self.items[*ix].label.to_lowercase().contains(&query))
195 .collect();
196 }
197}
198
199impl ListDelegate for OptionList {
200 type Item = ListItem;
201
202 fn perform_search(
203 &mut self,
204 query: &str,
205 _: &mut Window,
206 _: &mut Context<ListState<Self>>,
207 ) -> Task<()> {
208 self.filter(query);
209 Task::ready(())
210 }
211
212 fn items_count(&self, _: usize, _: &App) -> usize {
213 self.matches.len()
214 }
215
216 fn render_item(
217 &mut self,
218 ix: IndexPath,
219 _: &mut Window,
220 _: &mut Context<ListState<Self>>,
221 ) -> Option<ListItem> {
222 let item = &self.items[*self.matches.get(ix.row)?];
223 Some(
224 ListItem::new(ix.row)
225 .accessibility_label(item.label.clone())
226 .child(item.label.clone()),
227 )
228 }
229
230 fn render_empty(
231 &mut self,
232 _: &mut Window,
233 cx: &mut Context<ListState<Self>>,
234 ) -> impl IntoElement {
235 div()
236 .px(px(8.))
237 .py(px(6.))
238 .text_sm()
239 .text_color(cx.theme().muted_foreground)
240 .child(SharedString::from(t!("live_grid.edit.no_matches")))
241 }
242
243 fn set_selected_index(
244 &mut self,
245 _: Option<IndexPath>,
246 _: &mut Window,
247 _: &mut Context<ListState<Self>>,
248 ) {
249 }
250}
251
252struct SelectField {
255 list: Entity<ListState<OptionList>>,
256 start: RefCell<OwnedValue>,
258 initial: Cell<Option<usize>>,
260 query: RefCell<Option<SharedString>>,
263 drawn: Cell<bool>,
265 _confirm: Subscription,
266}
267
268impl SelectField {
269 fn picked(&self, cx: &App) -> Option<usize> {
271 let list = self.list.read(cx);
272 let row = list.selected_index()?.row;
273 list.delegate().matches.get(row).copied()
274 }
275
276 fn picked_item(&self, cx: &App) -> Option<SelectOption> {
278 let ix = self.picked(cx)?;
279 Some(self.list.read(cx).delegate().items[ix].clone())
280 }
281}
282
283impl EditorField for SelectField {
284 fn render(&self, cell: &EditorCell<'_>, window: &mut Window, cx: &mut App) -> AnyElement {
285 if self.query.borrow().is_some() {
286 if self.drawn.replace(true) {
287 let query = self.query.take().unwrap_or_default();
290 self.list
291 .update(cx, |list, cx| list.set_query(&query, window, cx));
292 } else {
293 self.list.update(cx, |_, cx| cx.notify());
295 }
296 }
297 let text = self.picked_item(cx).map_or_else(
298 || {
299 let mut text = String::new();
300 display_text(self.start.borrow().as_cell(), &mut text).to_owned()
301 },
302 |item| item.label.to_string(),
303 );
304 let size = popup_size(cell.size());
305 let width = cell.bounds().size.width.max(px(160.));
306 div()
307 .flex_1()
308 .min_w_0()
309 .px(px(4.))
310 .truncate()
311 .child(text)
312 .child(
313 cell.popup(
314 div().w(width).child(
315 List::new(&self.list)
316 .search_placeholder(SharedString::from(t!("live_grid.edit.search")))
317 .with_size(size)
318 .max_h(px(240.))
319 .p(px(4.)),
320 ),
321 cx,
322 ),
323 )
324 .into_any_element()
325 }
326
327 fn focus(&self, window: &mut Window, cx: &mut App) {
328 self.list.update(cx, |list, cx| list.focus(window, cx));
329 }
330
331 fn value(&self, cx: &App) -> Result<OwnedValue, EditError> {
332 Ok(self
333 .picked_item(cx)
334 .map_or_else(|| self.start.borrow().clone(), |item| item.value))
335 }
336
337 fn set_value(&self, value: &OwnedValue, window: &mut Window, cx: &mut App) {
338 self.list.update(cx, |list, cx| {
339 let row = list
340 .delegate()
341 .matches
342 .iter()
343 .position(|ix| list.delegate().items[*ix].value.cmp_total(value).is_eq());
344 list.set_selected_index(row.map(IndexPath::new), window, cx);
345 cx.notify();
346 });
347 *self.start.borrow_mut() = value.clone();
349 self.initial.set(self.picked(cx));
350 }
351
352 fn is_dirty(&self, cx: &App) -> bool {
353 self.picked(cx) != self.initial.get()
354 }
355}
356
357fn popup_size(size: ComponentSize) -> ComponentSize {
360 match size {
361 ComponentSize::XSmall => ComponentSize::Small,
362 size => size,
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369
370 fn sides() -> SelectEditor {
371 SelectEditor::new([
372 SelectOption::new(OwnedValue::Text("Buy".into()), "Buy"),
373 SelectOption::new(OwnedValue::Text("Sell".into()), "Sell"),
374 ])
375 }
376
377 #[test]
378 fn test_select_picked() {
379 let editor = sides();
380 assert_eq!(editor.options().len(), 2);
381 assert_eq!(editor.empty_label(), None);
382 assert_eq!(
383 editor.picked(CellValue::Text("Sell"), &EditInput::Command),
384 Some(Some(1))
385 );
386 assert_eq!(
387 editor.picked(CellValue::Text("Hold"), &EditInput::Command),
388 Some(None)
389 );
390 assert_eq!(
392 editor.picked(CellValue::Text("Sell"), &EditInput::Clear),
393 None
394 );
395
396 let editor = sides().with_empty("None").with_empty("—");
397 assert_eq!(editor.options().len(), 2, "one empty item");
398 assert_eq!(editor.empty_label().map(AsRef::as_ref), Some("—"));
399 assert_eq!(
400 editor.picked(CellValue::Text("Sell"), &EditInput::Clear),
401 Some(Some(0))
402 );
403 assert_eq!(
404 editor.picked(CellValue::Empty, &EditInput::Command),
405 Some(Some(0))
406 );
407 assert_eq!(
408 editor.picked(CellValue::Text("Buy"), &EditInput::Press { click_count: 2 }),
409 Some(Some(1))
410 );
411 }
412}