1use std::ops::Range;
2use std::time::Duration;
3
4use crate::actions::{Cancel, Confirm, SelectDown, SelectUp};
5use crate::input::InputState;
6use crate::list::cache::{MeasuredEntrySize, RowEntry, RowsCache};
7use crate::{
8 input::{Input, InputEvent},
9 scroll::{Scrollbar, ScrollbarState},
10 v_flex, ActiveTheme, IconName, Size,
11};
12use crate::{list::ListDelegate, v_virtual_list, VirtualListScrollHandle};
13use crate::{Icon, IndexPath, Selectable, Sizable as _, StyledExt};
14use gpui::{
15 div, prelude::FluentBuilder, AppContext, Entity, FocusHandle, Focusable, InteractiveElement,
16 IntoElement, KeyBinding, Length, MouseButton, ParentElement, Render, Styled, Task, Window,
17};
18use gpui::{
19 px, size, App, AvailableSpace, ClickEvent, Context, Edges, EventEmitter, ListSizingBehavior,
20 Pixels, ScrollStrategy, SharedString, StatefulInteractiveElement, Subscription,
21};
22use rust_i18n::t;
23use smol::Timer;
24
25pub(crate) fn init(cx: &mut App) {
26 let context: Option<&str> = Some("List");
27 cx.bind_keys([
28 KeyBinding::new("escape", Cancel, context),
29 KeyBinding::new("enter", Confirm { secondary: false }, context),
30 KeyBinding::new("secondary-enter", Confirm { secondary: true }, context),
31 KeyBinding::new("up", SelectUp, context),
32 KeyBinding::new("down", SelectDown, context),
33 ]);
34}
35
36#[derive(Clone)]
37pub enum ListEvent {
38 Select(IndexPath),
40 Confirm(IndexPath),
42 Cancel,
44}
45
46pub struct List<D: ListDelegate> {
47 focus_handle: FocusHandle,
48 delegate: D,
49 max_height: Option<Length>,
50 paddings: Edges<Pixels>,
51 query_input: Option<Entity<InputState>>,
52 last_query: Option<String>,
53 selectable: bool,
54 querying: bool,
55 scrollbar_visible: bool,
56 scroll_handle: VirtualListScrollHandle,
57 scroll_state: ScrollbarState,
58 pub(crate) size: Size,
59 rows_cache: RowsCache,
60 selected_index: Option<IndexPath>,
61 item_to_measure_index: IndexPath,
62 deferred_scroll_to_index: Option<(IndexPath, ScrollStrategy)>,
63 mouse_right_clicked_index: Option<IndexPath>,
64 reset_on_cancel: bool,
65 _search_task: Task<()>,
66 _load_more_task: Task<()>,
67 _query_input_subscription: Subscription,
68}
69
70impl<D> List<D>
71where
72 D: ListDelegate,
73{
74 pub fn new(delegate: D, window: &mut Window, cx: &mut Context<Self>) -> Self {
75 let query_input =
76 cx.new(|cx| InputState::new(window, cx).placeholder(t!("List.search_placeholder")));
77
78 let _query_input_subscription =
79 cx.subscribe_in(&query_input, window, Self::on_query_input_event);
80
81 Self {
82 focus_handle: cx.focus_handle(),
83 delegate,
84 rows_cache: RowsCache::default(),
85 query_input: Some(query_input),
86 last_query: None,
87 selected_index: None,
88 item_to_measure_index: IndexPath::default(),
89 deferred_scroll_to_index: None,
90 mouse_right_clicked_index: None,
91 scroll_handle: VirtualListScrollHandle::new(),
92 scroll_state: ScrollbarState::default(),
93 max_height: None,
94 scrollbar_visible: true,
95 selectable: true,
96 querying: false,
97 size: Size::default(),
98 reset_on_cancel: true,
99 paddings: Edges::default(),
100 _search_task: Task::ready(()),
101 _load_more_task: Task::ready(()),
102 _query_input_subscription,
103 }
104 }
105
106 pub fn set_size(&mut self, size: Size, _: &mut Window, _: &mut Context<Self>) {
108 self.size = size;
109 }
110
111 pub fn max_h(mut self, height: impl Into<Length>) -> Self {
112 self.max_height = Some(height.into());
113 self
114 }
115
116 pub fn scrollbar_visible(mut self, visible: bool) -> Self {
118 self.scrollbar_visible = visible;
119 self
120 }
121
122 pub fn no_query(mut self) -> Self {
123 self.query_input = None;
124 self
125 }
126
127 pub fn selectable(mut self, selectable: bool) -> Self {
129 self.selectable = selectable;
130 self
131 }
132
133 pub fn set_query_input(
134 &mut self,
135 query_input: Entity<InputState>,
136 window: &mut Window,
137 cx: &mut Context<Self>,
138 ) {
139 self._query_input_subscription =
140 cx.subscribe_in(&query_input, window, Self::on_query_input_event);
141 self.query_input = Some(query_input);
142 }
143
144 pub fn query_input(&self) -> Option<&Entity<InputState>> {
146 self.query_input.as_ref()
147 }
148
149 pub fn delegate(&self) -> &D {
150 &self.delegate
151 }
152
153 pub fn delegate_mut(&mut self) -> &mut D {
154 &mut self.delegate
155 }
156
157 pub fn focus(&mut self, window: &mut Window, cx: &mut App) {
158 self.focus_handle(cx).focus(window);
159 }
160
161 pub(crate) fn _set_selected_index(
164 &mut self,
165 ix: Option<IndexPath>,
166 window: &mut Window,
167 cx: &mut Context<Self>,
168 ) {
169 self.selected_index = ix;
170 self.delegate.set_selected_index(ix, window, cx);
171 self.scroll_to_selected_item(window, cx);
172 }
173
174 pub fn set_selected_index(
177 &mut self,
178 ix: Option<IndexPath>,
179 window: &mut Window,
180 cx: &mut Context<Self>,
181 ) {
182 self.selected_index = ix;
183 self.delegate.set_selected_index(ix, window, cx);
184 }
185
186 pub fn selected_index(&self) -> Option<IndexPath> {
187 self.selected_index
188 }
189
190 pub fn set_item_to_measure_index(
192 &mut self,
193 ix: IndexPath,
194 _: &mut Window,
195 cx: &mut Context<Self>,
196 ) {
197 self.item_to_measure_index = ix;
198 cx.notify();
199 }
200
201 fn render_scrollbar(&self, _: &mut Window, _: &mut Context<Self>) -> Option<impl IntoElement> {
202 if !self.scrollbar_visible {
203 return None;
204 }
205
206 Some(Scrollbar::uniform_scroll(
207 &self.scroll_state,
208 &self.scroll_handle,
209 ))
210 }
211
212 pub fn scroll_to_item(
214 &mut self,
215 ix: IndexPath,
216 strategy: ScrollStrategy,
217 _: &mut Window,
218 cx: &mut Context<Self>,
219 ) {
220 if ix.section == 0 && ix.row == 0 {
221 let mut offset = self.scroll_handle.base_handle().offset();
223 offset.y = px(0.);
224 self.scroll_handle.base_handle().set_offset(offset);
225 cx.notify();
226 return;
227 }
228 self.deferred_scroll_to_index = Some((ix, strategy));
229 cx.notify();
230 }
231
232 pub fn scroll_handle(&self) -> &VirtualListScrollHandle {
234 &self.scroll_handle
235 }
236
237 pub fn scroll_to_selected_item(&mut self, _: &mut Window, cx: &mut Context<Self>) {
238 if let Some(ix) = self.selected_index {
239 self.deferred_scroll_to_index = Some((ix, ScrollStrategy::Top));
240 cx.notify();
241 }
242 }
243
244 pub fn paddings(mut self, paddings: Edges<Pixels>) -> Self {
246 self.paddings = paddings;
247 self
248 }
249
250 fn on_query_input_event(
251 &mut self,
252 state: &Entity<InputState>,
253 event: &InputEvent,
254 window: &mut Window,
255 cx: &mut Context<Self>,
256 ) {
257 match event {
258 InputEvent::Change => {
259 let text = state.read(cx).value();
260 let text = text.trim().to_string();
261 if Some(&text) == self.last_query.as_ref() {
262 return;
263 }
264
265 self.set_querying(true, window, cx);
266 let search = self.delegate.perform_search(&text, window, cx);
267
268 if self.rows_cache.len() > 0 {
269 self._set_selected_index(Some(IndexPath::default()), window, cx);
270 } else {
271 self._set_selected_index(None, window, cx);
272 }
273
274 self._search_task = cx.spawn_in(window, async move |this, window| {
275 search.await;
276
277 _ = this.update_in(window, |this, _, _| {
278 this.scroll_handle.scroll_to_item(0, ScrollStrategy::Top);
279 this.last_query = Some(text);
280 });
281
282 Timer::after(Duration::from_millis(100)).await;
284 _ = this.update_in(window, |this, window, cx| {
285 this.set_querying(false, window, cx);
286 });
287 });
288 }
289 InputEvent::PressEnter { secondary } => self.on_action_confirm(
290 &Confirm {
291 secondary: *secondary,
292 },
293 window,
294 cx,
295 ),
296 _ => {}
297 }
298 }
299
300 fn set_querying(&mut self, querying: bool, window: &mut Window, cx: &mut Context<Self>) {
301 self.querying = querying;
302 if let Some(input) = &self.query_input {
303 input.update(cx, |input, cx| input.set_loading(querying, window, cx))
304 }
305 cx.notify();
306 }
307
308 fn load_more_if_need(
311 &mut self,
312 entities_count: usize,
313 visible_end: usize,
314 window: &mut Window,
315 cx: &mut Context<Self>,
316 ) {
317 let threshold = self.delegate.load_more_threshold();
320 if visible_end >= entities_count.saturating_sub(threshold) {
323 if !self.delegate.is_eof(cx) {
324 return;
325 }
326
327 self._load_more_task = cx.spawn_in(window, async move |view, cx| {
328 _ = view.update_in(cx, |view, window, cx| {
329 view.delegate.load_more(window, cx);
330 });
331 });
332 }
333 }
334
335 pub(crate) fn reset_on_cancel(mut self, reset: bool) -> Self {
336 self.reset_on_cancel = reset;
337 self
338 }
339
340 fn on_action_cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
341 cx.propagate();
342 if self.reset_on_cancel {
343 self._set_selected_index(None, window, cx);
344 }
345
346 self.delegate.cancel(window, cx);
347 cx.emit(ListEvent::Cancel);
348 cx.notify();
349 }
350
351 fn on_action_confirm(
352 &mut self,
353 confirm: &Confirm,
354 window: &mut Window,
355 cx: &mut Context<Self>,
356 ) {
357 if self.rows_cache.len() == 0 {
358 return;
359 }
360
361 let Some(ix) = self.selected_index else {
362 return;
363 };
364
365 self.delegate
366 .set_selected_index(self.selected_index, window, cx);
367 self.delegate.confirm(confirm.secondary, window, cx);
368 cx.emit(ListEvent::Confirm(ix));
369 cx.notify();
370 }
371
372 fn select_item(&mut self, ix: IndexPath, window: &mut Window, cx: &mut Context<Self>) {
373 self.selected_index = Some(ix);
374 self.delegate.set_selected_index(Some(ix), window, cx);
375 self.scroll_to_selected_item(window, cx);
376 cx.emit(ListEvent::Select(ix));
377 cx.notify();
378 }
379
380 pub(crate) fn on_action_select_prev(
381 &mut self,
382 _: &SelectUp,
383 window: &mut Window,
384 cx: &mut Context<Self>,
385 ) {
386 if self.rows_cache.len() == 0 {
387 return;
388 }
389
390 let prev_ix = self
391 .rows_cache
392 .prev(self.selected_index.unwrap_or(IndexPath::default()));
393 self.select_item(prev_ix, window, cx);
394 }
395
396 pub(crate) fn on_action_select_next(
397 &mut self,
398 _: &SelectDown,
399 window: &mut Window,
400 cx: &mut Context<Self>,
401 ) {
402 if self.rows_cache.len() == 0 {
403 return;
404 }
405
406 let next_ix = self
407 .rows_cache
408 .next(self.selected_index.unwrap_or_default());
409 self.select_item(next_ix, window, cx);
410 }
411
412 fn render_list_item(
413 &self,
414 ix: IndexPath,
415 window: &mut Window,
416 cx: &mut Context<Self>,
417 ) -> impl IntoElement {
418 let selected = self.selected_index.map(|s| s.eq_row(ix)).unwrap_or(false);
419 let mouse_right_clicked = self
420 .mouse_right_clicked_index
421 .map(|s| s.eq_row(ix))
422 .unwrap_or(false);
423 let id = SharedString::from(format!("list-item-{}", ix));
424
425 div()
426 .id(id)
427 .w_full()
428 .relative()
429 .children(self.delegate.render_item(ix, window, cx).map(|item| {
430 item.selected(selected)
431 .secondary_selected(mouse_right_clicked)
432 }))
433 .when(self.selectable, |this| {
434 this.on_click(cx.listener(move |this, e: &ClickEvent, window, cx| {
435 this.mouse_right_clicked_index = None;
436 this.selected_index = Some(ix);
437 this.on_action_confirm(
438 &Confirm {
439 secondary: e.modifiers().secondary(),
440 },
441 window,
442 cx,
443 );
444 }))
445 .on_mouse_down(
446 MouseButton::Right,
447 cx.listener(move |this, _, _, cx| {
448 this.mouse_right_clicked_index = Some(ix);
449 cx.notify();
450 }),
451 )
452 })
453 }
454
455 fn render_items(
456 &mut self,
457 items_count: usize,
458 entities_count: usize,
459 window: &mut Window,
460 cx: &mut Context<Self>,
461 ) -> impl IntoElement {
462 let measured_size = self.rows_cache.measured_size();
463
464 v_flex()
465 .flex_grow()
466 .relative()
467 .h_full()
468 .min_w(measured_size.item_size.width)
469 .when_some(self.max_height, |this, h| this.max_h(h))
470 .overflow_hidden()
471 .when(items_count == 0, |this| {
472 this.child(self.delegate().render_empty(window, cx))
473 })
474 .when(items_count > 0, {
475 let rows_cache = self.rows_cache.clone();
476 |this| {
477 this.child(
478 v_virtual_list(
479 cx.entity().clone(),
480 "virtual-list",
481 rows_cache.entries_sizes.clone(),
482 move |list, visible_range: Range<usize>, window, cx| {
483 list.load_more_if_need(
484 entities_count,
485 visible_range.end,
486 window,
487 cx,
488 );
489
490 visible_range
495 .map(|ix| {
496 let Some(entry) = rows_cache.get(ix) else {
497 return div();
498 };
499
500 div().children(match entry {
501 RowEntry::Entry(index) => Some(
502 list.render_list_item(index, window, cx)
503 .into_any_element(),
504 ),
505 RowEntry::SectionHeader(section_ix) => list
506 .delegate()
507 .render_section_header(section_ix, window, cx)
508 .map(|r| r.into_any_element()),
509 RowEntry::SectionFooter(section_ix) => list
510 .delegate()
511 .render_section_footer(section_ix, window, cx)
512 .map(|r| r.into_any_element()),
513 })
514 })
515 .collect::<Vec<_>>()
516 },
517 )
518 .paddings(self.paddings)
519 .when(self.max_height.is_some(), |this| {
520 this.with_sizing_behavior(ListSizingBehavior::Infer)
521 })
522 .track_scroll(&self.scroll_handle)
523 .into_any_element(),
524 )
525 }
526 })
527 .children(self.render_scrollbar(window, cx))
528 }
529
530 fn prepare_items_if_needed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
531 let sections_count = self.delegate.sections_count(cx);
532
533 let mut measured_size = MeasuredEntrySize::default();
534
535 let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
537 measured_size.item_size = self
538 .render_list_item(self.item_to_measure_index, window, cx)
539 .into_any_element()
540 .layout_as_root(available_space, window, cx);
541
542 if let Some(mut el) = self
543 .delegate
544 .render_section_header(0, window, cx)
545 .map(|r| r.into_any_element())
546 {
547 measured_size.section_header_size = el.layout_as_root(available_space, window, cx);
548 }
549 if let Some(mut el) = self
550 .delegate
551 .render_section_footer(0, window, cx)
552 .map(|r| r.into_any_element())
553 {
554 measured_size.section_footer_size = el.layout_as_root(available_space, window, cx);
555 }
556
557 self.rows_cache
558 .prepare_if_needed(sections_count, measured_size, cx, |section_ix, cx| {
559 self.delegate.items_count(section_ix, cx)
560 });
561 }
562}
563
564impl<D> Focusable for List<D>
565where
566 D: ListDelegate,
567{
568 fn focus_handle(&self, cx: &App) -> FocusHandle {
569 if let Some(query_input) = &self.query_input {
570 query_input.focus_handle(cx)
571 } else {
572 self.focus_handle.clone()
573 }
574 }
575}
576impl<D> EventEmitter<ListEvent> for List<D> where D: ListDelegate {}
577impl<D> Render for List<D>
578where
579 D: ListDelegate,
580{
581 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
582 self.prepare_items_if_needed(window, cx);
583
584 if let Some((ix, strategy)) = self.deferred_scroll_to_index.take() {
586 if let Some(item_ix) = self.rows_cache.position_of(&ix) {
587 self.scroll_handle.scroll_to_item(item_ix, strategy);
588 }
589 }
590
591 let items_count = self.rows_cache.items_count();
592 let entities_count = self.rows_cache.len();
593 let loading = self.delegate.loading(cx);
594
595 let initial_view = if let Some(input) = &self.query_input {
596 if input.read(cx).value().is_empty() {
597 self.delegate().render_initial(window, cx)
598 } else {
599 None
600 }
601 } else {
602 None
603 };
604
605 v_flex()
606 .key_context("List")
607 .id("list")
608 .track_focus(&self.focus_handle)
609 .size_full()
610 .relative()
611 .overflow_hidden()
612 .when_some(self.query_input.clone(), |this, input| {
613 this.child(
614 div()
615 .map(|this| match self.size {
616 Size::Small => this.px_1p5(),
617 _ => this.px_2(),
618 })
619 .border_b_1()
620 .border_color(cx.theme().border)
621 .child(
622 Input::new(&input)
623 .with_size(self.size)
624 .prefix(
625 Icon::new(IconName::Search)
626 .text_color(cx.theme().muted_foreground),
627 )
628 .cleanable()
629 .p_0()
630 .appearance(false),
631 ),
632 )
633 })
634 .when(loading, |this| {
635 this.child(self.delegate().render_loading(window, cx))
636 })
637 .when(!loading, |this| {
638 this.on_action(cx.listener(Self::on_action_cancel))
639 .on_action(cx.listener(Self::on_action_confirm))
640 .on_action(cx.listener(Self::on_action_select_next))
641 .on_action(cx.listener(Self::on_action_select_prev))
642 .map(|this| {
643 if let Some(view) = initial_view {
644 this.child(view)
645 } else {
646 this.child(self.render_items(items_count, entities_count, window, cx))
647 }
648 })
649 .when(self.mouse_right_clicked_index.is_some(), |this| {
651 this.on_mouse_down_out(cx.listener(|this, _, _, cx| {
652 this.mouse_right_clicked_index = None;
653 cx.notify();
654 }))
655 })
656 })
657 }
658}