1use instant::Duration;
2use std::ops::Range;
3
4use crate::actions::{Cancel, Confirm, SelectDown, SelectUp};
5use crate::input::InputState;
6use crate::list::cache::{MeasuredEntrySize, RowEntry, RowsCache};
7use crate::{
8 ActiveTheme, IconName, Size,
9 input::{Input, InputEvent},
10 scroll::Scrollbar,
11 v_flex,
12};
13use crate::{Icon, IndexPath, Selectable, Sizable, StyledExt};
14use crate::{VirtualListScrollHandle, list::ListDelegate, v_virtual_list};
15use gpui::{
16 App, AvailableSpace, ClickEvent, Context, DefiniteLength, EdgesRefinement, EventEmitter,
17 ListSizingBehavior, RenderOnce, Role, ScrollStrategy, SharedString, StatefulInteractiveElement,
18 StyleRefinement, Subscription, px, size,
19};
20use gpui::{
21 AppContext, Entity, FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding,
22 Length, MouseButton, ParentElement, Render, Styled, Task, Window, div, prelude::FluentBuilder,
23};
24use rust_i18n::t;
25
26pub(crate) fn init(cx: &mut App) {
27 let context: Option<&str> = Some("List");
28 cx.bind_keys([
29 KeyBinding::new("escape", Cancel, context),
30 KeyBinding::new("enter", Confirm { secondary: false }, context),
31 KeyBinding::new("secondary-enter", Confirm { secondary: true }, context),
32 KeyBinding::new("up", SelectUp, context),
33 KeyBinding::new("down", SelectDown, context),
34 ]);
35}
36
37#[derive(Clone)]
38pub enum ListEvent {
39 Select(IndexPath),
41 Confirm(IndexPath),
43 Cancel,
45}
46
47struct ListOptions {
48 size: Size,
49 scrollbar_visible: bool,
50 search_placeholder: Option<SharedString>,
51 max_height: Option<Length>,
52 paddings: EdgesRefinement<DefiniteLength>,
53}
54
55impl Default for ListOptions {
56 fn default() -> Self {
57 Self {
58 size: Size::default(),
59 scrollbar_visible: true,
60 max_height: None,
61 search_placeholder: None,
62 paddings: EdgesRefinement::default(),
63 }
64 }
65}
66
67pub struct ListState<D: ListDelegate> {
71 pub(crate) focus_handle: FocusHandle,
72 pub(crate) query_input: Entity<InputState>,
73 options: ListOptions,
74 delegate: D,
75 last_query: Option<String>,
76 scroll_handle: VirtualListScrollHandle,
77 rows_cache: RowsCache,
78 selected_index: Option<IndexPath>,
79 item_to_measure_index: IndexPath,
80 deferred_scroll_to_index: Option<(IndexPath, ScrollStrategy)>,
81 mouse_right_clicked_index: Option<IndexPath>,
82 reset_on_cancel: bool,
83 searchable: bool,
84 selectable: bool,
85 _search_task: Task<()>,
86 _load_more_task: Task<()>,
87 _query_input_subscription: Subscription,
88}
89
90impl<D> ListState<D>
91where
92 D: ListDelegate,
93{
94 pub fn new(delegate: D, window: &mut Window, cx: &mut Context<Self>) -> Self {
95 let query_input =
96 cx.new(|cx| InputState::new(window, cx).placeholder(t!("List.search_placeholder")));
97
98 let _query_input_subscription =
99 cx.subscribe_in(&query_input, window, Self::on_query_input_event);
100
101 Self {
102 focus_handle: cx.focus_handle(),
103 options: ListOptions::default(),
104 delegate,
105 rows_cache: RowsCache::default(),
106 query_input,
107 last_query: None,
108 selected_index: None,
109 selectable: true,
110 searchable: false,
111 item_to_measure_index: IndexPath::default(),
112 deferred_scroll_to_index: None,
113 mouse_right_clicked_index: None,
114 scroll_handle: VirtualListScrollHandle::new(),
115 reset_on_cancel: true,
116 _search_task: Task::ready(()),
117 _load_more_task: Task::ready(()),
118 _query_input_subscription,
119 }
120 }
121
122 pub fn searchable(mut self, searchable: bool) -> Self {
126 self.searchable = searchable;
127 self
128 }
129
130 pub fn set_searchable(&mut self, searchable: bool, cx: &mut Context<Self>) {
131 self.searchable = searchable;
132 cx.notify();
133 }
134
135 pub fn selectable(mut self, selectable: bool) -> Self {
137 self.selectable = selectable;
138 self
139 }
140
141 pub fn set_selectable(&mut self, selectable: bool, cx: &mut Context<Self>) {
143 self.selectable = selectable;
144 cx.notify();
145 }
146
147 pub fn delegate(&self) -> &D {
148 &self.delegate
149 }
150
151 pub fn delegate_mut(&mut self) -> &mut D {
152 &mut self.delegate
153 }
154
155 pub fn focus(&mut self, window: &mut Window, cx: &mut App) {
157 self.focus_handle(cx).focus(window, cx);
158 }
159
160 pub(crate) fn is_focused(&self, window: &Window, cx: &App) -> bool {
162 self.focus_handle.is_focused(window) || self.query_input.focus_handle(cx).is_focused(window)
163 }
164
165 pub(crate) fn _set_selected_index(
168 &mut self,
169 ix: Option<IndexPath>,
170 window: &mut Window,
171 cx: &mut Context<Self>,
172 ) {
173 if !self.selectable {
174 return;
175 }
176
177 self.selected_index = ix;
178 self.delegate.set_selected_index(ix, window, cx);
179 self.scroll_to_selected_item(window, cx);
180 }
181
182 pub fn set_selected_index(
185 &mut self,
186 ix: Option<IndexPath>,
187 window: &mut Window,
188 cx: &mut Context<Self>,
189 ) {
190 self.selected_index = ix;
191 self.delegate.set_selected_index(ix, window, cx);
192 }
193
194 pub fn selected_index(&self) -> Option<IndexPath> {
195 self.selected_index
196 }
197
198 pub fn set_right_clicked_index(
200 &mut self,
201 ix: Option<IndexPath>,
202 window: &mut Window,
203 cx: &mut Context<Self>,
204 ) {
205 self.mouse_right_clicked_index = ix;
206 self.delegate.set_right_clicked_index(ix, window, cx);
207 }
208
209 pub fn right_clicked_index(&self) -> Option<IndexPath> {
211 self.mouse_right_clicked_index
212 }
213
214 pub fn set_query(&mut self, query: &str, window: &mut Window, cx: &mut Context<Self>) {
216 let query = query.to_string();
217 self.query_input.update(cx, |input, cx| {
218 input.set_value(query.clone(), window, cx);
219 });
220
221 self.start_search(query.trim().to_string(), window, cx);
223 }
224
225 pub fn set_item_to_measure_index(
227 &mut self,
228 ix: IndexPath,
229 _: &mut Window,
230 cx: &mut Context<Self>,
231 ) {
232 self.item_to_measure_index = ix;
233 cx.notify();
234 }
235
236 pub fn scroll_to_item(
238 &mut self,
239 ix: IndexPath,
240 strategy: ScrollStrategy,
241 _: &mut Window,
242 cx: &mut Context<Self>,
243 ) {
244 if ix.section == 0 && ix.row == 0 {
245 let mut offset = self.scroll_handle.base_handle().offset();
247 offset.y = px(0.);
248 self.scroll_handle.base_handle().set_offset(offset);
249 cx.notify();
250 return;
251 }
252 self.deferred_scroll_to_index = Some((ix, strategy));
253 cx.notify();
254 }
255
256 pub fn scroll_handle(&self) -> &VirtualListScrollHandle {
258 &self.scroll_handle
259 }
260
261 pub fn scroll_to_selected_item(&mut self, _: &mut Window, cx: &mut Context<Self>) {
262 if let Some(ix) = self.selected_index {
263 self.deferred_scroll_to_index = Some((ix, ScrollStrategy::Top));
264 cx.notify();
265 }
266 }
267
268 fn on_query_input_event(
269 &mut self,
270 state: &Entity<InputState>,
271 event: &InputEvent,
272 window: &mut Window,
273 cx: &mut Context<Self>,
274 ) {
275 match event {
276 InputEvent::Change => {
277 let text = state.read(cx).value();
278 let text = text.trim().to_string();
279 if Some(&text) == self.last_query.as_ref() {
280 return;
281 }
282
283 self.start_search(text, window, cx);
284 }
285 _ => {}
286 }
287 }
288
289 fn start_search(&mut self, query: String, window: &mut Window, cx: &mut Context<Self>) {
290 self.set_searching(true, window, cx);
291 let search = self.delegate.perform_search(&query, window, cx);
292
293 if self.rows_cache.len() > 0 {
294 self._set_selected_index(Some(IndexPath::default()), window, cx);
295 } else {
296 self._set_selected_index(None, window, cx);
297 }
298
299 self._search_task = cx.spawn_in(window, async move |this, window| {
300 search.await;
301
302 _ = this.update_in(window, |this, _, _| {
303 this.scroll_handle.scroll_to_item(0, ScrollStrategy::Top);
304 this.last_query = Some(query);
305 });
306
307 window
309 .background_executor()
310 .timer(Duration::from_millis(100))
311 .await;
312 _ = this.update_in(window, |this, window, cx| {
313 this.set_searching(false, window, cx);
314 });
315 });
316 }
317
318 fn set_searching(&mut self, searching: bool, window: &mut Window, cx: &mut Context<Self>) {
319 self.query_input
320 .update(cx, |input, cx| input.set_loading(searching, window, cx));
321 }
322
323 fn load_more_if_need(
326 &mut self,
327 entities_count: usize,
328 visible_end: usize,
329 window: &mut Window,
330 cx: &mut Context<Self>,
331 ) {
332 let threshold = self.delegate.load_more_threshold();
335 if visible_end >= entities_count.saturating_sub(threshold) {
338 if !self.delegate.has_more(cx) {
339 return;
340 }
341
342 self._load_more_task = cx.spawn_in(window, async move |view, cx| {
343 _ = view.update_in(cx, |view, window, cx| {
344 view.delegate.load_more(window, cx);
345 });
346 });
347 }
348 }
349
350 pub(crate) fn reset_on_cancel(mut self, reset: bool) -> Self {
351 self.reset_on_cancel = reset;
352 self
353 }
354
355 fn on_action_cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
356 cx.propagate();
357 if self.reset_on_cancel {
358 self._set_selected_index(None, window, cx);
359 }
360
361 self.delegate.cancel(window, cx);
362 cx.emit(ListEvent::Cancel);
363 cx.notify();
364 }
365
366 fn on_action_confirm(
367 &mut self,
368 confirm: &Confirm,
369 window: &mut Window,
370 cx: &mut Context<Self>,
371 ) {
372 if self.rows_cache.len() == 0 {
373 return;
374 }
375
376 let Some(ix) = self.selected_index else {
377 return;
378 };
379
380 self.delegate
381 .set_selected_index(self.selected_index, window, cx);
382 self.delegate.confirm(confirm.secondary, window, cx);
383 cx.emit(ListEvent::Confirm(ix));
384 cx.notify();
385 }
386
387 fn select_item(&mut self, ix: IndexPath, window: &mut Window, cx: &mut Context<Self>) {
388 if !self.selectable {
389 return;
390 }
391
392 self.selected_index = Some(ix);
393 self.delegate.set_selected_index(Some(ix), window, cx);
394 self.scroll_to_selected_item(window, cx);
395 cx.emit(ListEvent::Select(ix));
396 cx.notify();
397 }
398
399 pub(crate) fn on_action_select_prev(
400 &mut self,
401 _: &SelectUp,
402 window: &mut Window,
403 cx: &mut Context<Self>,
404 ) {
405 if self.rows_cache.len() == 0 {
406 return;
407 }
408
409 let prev_ix = self.rows_cache.prev(self.selected_index);
410 self.select_item(prev_ix, window, cx);
411 }
412
413 pub(crate) fn on_action_select_next(
414 &mut self,
415 _: &SelectDown,
416 window: &mut Window,
417 cx: &mut Context<Self>,
418 ) {
419 if self.rows_cache.len() == 0 {
420 return;
421 }
422
423 let next_ix = self.rows_cache.next(self.selected_index);
424 self.select_item(next_ix, window, cx);
425 }
426
427 fn prepare_items_if_needed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
428 let sections_count = self.delegate.sections_count(cx).max(1);
429 let mut measured_size = MeasuredEntrySize::default();
430
431 let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
433 measured_size.item_size = self
434 .render_list_item(self.item_to_measure_index, window, cx)
435 .into_any_element()
436 .layout_as_root(available_space, window, cx);
437
438 if let Some(mut el) = self
439 .delegate
440 .render_section_header(0, window, cx)
441 .map(|r| r.into_any_element())
442 {
443 measured_size.section_header_size = el.layout_as_root(available_space, window, cx);
444 }
445 if let Some(mut el) = self
446 .delegate
447 .render_section_footer(0, window, cx)
448 .map(|r| r.into_any_element())
449 {
450 measured_size.section_footer_size = el.layout_as_root(available_space, window, cx);
451 }
452
453 self.rows_cache
454 .prepare_if_needed(sections_count, measured_size, cx, |section_ix, cx| {
455 self.delegate.items_count(section_ix, cx)
456 });
457 }
458
459 fn render_list_item(
460 &mut self,
461 ix: IndexPath,
462 window: &mut Window,
463 cx: &mut Context<Self>,
464 ) -> impl IntoElement {
465 let selectable = self.selectable;
466 let selected = self.selected_index.map(|s| s.eq_row(ix)).unwrap_or(false);
467 let mouse_right_clicked = self
468 .mouse_right_clicked_index
469 .map(|s| s.eq_row(ix))
470 .unwrap_or(false);
471 let id = SharedString::from(format!("list-item-{}", ix));
472
473 let total_items = self.rows_cache.items_count();
474
475 div()
476 .id(id)
477 .role(Role::ListItem)
478 .aria_position_in_set(ix.row + 1)
479 .aria_size_of_set(total_items)
480 .aria_selected(selected)
481 .w_full()
482 .relative()
483 .overflow_hidden()
484 .children(self.delegate.render_item(ix, window, cx).map(|item| {
485 item.selected(selected)
486 .secondary_selected(mouse_right_clicked)
487 }))
488 .when(selectable, |this| {
489 this.on_click(cx.listener(move |this, e: &ClickEvent, window, cx| {
490 this.set_right_clicked_index(None, window, cx);
491 this.selected_index = Some(ix);
492 this.on_action_confirm(
493 &Confirm {
494 secondary: e.modifiers().secondary(),
495 },
496 window,
497 cx,
498 );
499 }))
500 .on_mouse_down(
501 MouseButton::Right,
502 cx.listener(move |this, _, window, cx| {
503 this.set_right_clicked_index(Some(ix), window, cx);
504 cx.notify();
505 }),
506 )
507 })
508 }
509
510 fn render_items(
511 &mut self,
512 items_count: usize,
513 entities_count: usize,
514 window: &mut Window,
515 cx: &mut Context<Self>,
516 ) -> impl IntoElement {
517 let rows_cache = self.rows_cache.clone();
518 let scrollbar_visible = self.options.scrollbar_visible;
519 let scroll_handle = self.scroll_handle.clone();
520 let item_to_measure_index = rows_cache
521 .position_of(&self.item_to_measure_index)
522 .or_else(|| rows_cache.first_entry_position())
523 .unwrap_or(0);
524
525 v_flex()
526 .flex_grow_1()
527 .relative()
528 .size_full()
529 .when_some(self.options.max_height, |this, h| this.max_h(h))
530 .overflow_hidden()
531 .when(items_count == 0, |this| {
532 this.child(self.delegate.render_empty(window, cx))
533 })
534 .when(items_count > 0, {
535 |this| {
536 this.child(
537 v_virtual_list(
538 cx.entity(),
539 "virtual-list",
540 rows_cache.entries_sizes.clone(),
541 move |list, visible_range: Range<usize>, window, cx| {
542 list.load_more_if_need(
543 entities_count,
544 visible_range.end,
545 window,
546 cx,
547 );
548
549 visible_range
554 .map(|ix| {
555 let Some(entry) = rows_cache.get(ix) else {
556 return div();
557 };
558
559 div().children(match entry {
560 RowEntry::Entry(index) => Some(
561 list.render_list_item(index, window, cx)
562 .into_any_element(),
563 ),
564 RowEntry::SectionHeader(section_ix) => list
565 .delegate_mut()
566 .render_section_header(section_ix, window, cx)
567 .map(|r| r.into_any_element()),
568 RowEntry::SectionFooter(section_ix) => list
569 .delegate_mut()
570 .render_section_footer(section_ix, window, cx)
571 .map(|r| r.into_any_element()),
572 })
573 })
574 .collect::<Vec<_>>()
575 },
576 )
577 .with_item_to_measure_index(item_to_measure_index)
578 .paddings(self.options.paddings.clone())
579 .when(self.options.max_height.is_some(), |this| {
580 this.with_sizing_behavior(ListSizingBehavior::Infer)
581 })
582 .track_scroll(&scroll_handle)
583 .into_any_element(),
584 )
585 }
586 })
587 .when(scrollbar_visible, |this| {
588 this.child(Scrollbar::vertical(&scroll_handle))
589 })
590 }
591}
592
593impl<D> Focusable for ListState<D>
594where
595 D: ListDelegate,
596{
597 fn focus_handle(&self, cx: &App) -> FocusHandle {
598 if self.searchable {
599 self.query_input.focus_handle(cx)
600 } else {
601 self.focus_handle.clone()
602 }
603 }
604}
605impl<D> EventEmitter<ListEvent> for ListState<D> where D: ListDelegate {}
606impl<D> Render for ListState<D>
607where
608 D: ListDelegate,
609{
610 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
611 self.prepare_items_if_needed(window, cx);
612
613 if let Some((ix, strategy)) = self.deferred_scroll_to_index.take() {
615 if let Some(item_ix) = self.rows_cache.position_of(&ix) {
616 self.scroll_handle.scroll_to_item(item_ix, strategy);
617 }
618 }
619
620 let loading = self.delegate().loading(cx);
621 let query_input = if self.searchable {
622 if let Some(placeholder) = &self.options.search_placeholder {
624 self.query_input.update(cx, |input, cx| {
625 input.set_placeholder(placeholder.clone(), window, cx);
626 });
627 }
628 Some(self.query_input.clone())
629 } else {
630 None
631 };
632
633 let loading_view = if loading {
634 Some(self.delegate.render_loading(window, cx).into_any_element())
635 } else {
636 None
637 };
638 let initial_view = if let Some(input) = &query_input {
639 if input.read(cx).value().is_empty() {
640 self.delegate.render_initial(window, cx)
641 } else {
642 None
643 }
644 } else {
645 None
646 };
647 let items_count = self.rows_cache.items_count();
648 let entities_count = self.rows_cache.len();
649 let mouse_right_clicked_index = self.mouse_right_clicked_index;
650
651 v_flex()
652 .key_context("List")
653 .id("list-state")
654 .track_focus(&self.focus_handle)
655 .size_full()
656 .relative()
657 .overflow_hidden()
658 .when_some(query_input, |this, input| {
659 this.child(
660 div()
661 .map(|this| match self.options.size {
662 Size::Small => this.px_1p5(),
663 _ => this.px_2(),
664 })
665 .border_b_1()
666 .border_color(cx.theme().border)
667 .child(
668 Input::new(&input)
669 .with_size(self.options.size)
670 .prefix(
671 Icon::new(IconName::Search)
672 .text_color(cx.theme().muted_foreground),
673 )
674 .cleanable(true)
675 .p_0()
676 .appearance(false),
677 ),
678 )
679 })
680 .when(!loading, |this| {
681 this.on_action(cx.listener(Self::on_action_cancel))
682 .on_action(cx.listener(Self::on_action_confirm))
683 .on_action(cx.listener(Self::on_action_select_next))
684 .on_action(cx.listener(Self::on_action_select_prev))
685 .map(|this| {
686 if let Some(view) = initial_view {
687 this.child(view)
688 } else {
689 this.child(self.render_items(items_count, entities_count, window, cx))
690 }
691 })
692 .when(mouse_right_clicked_index.is_some(), |this| {
694 this.on_mouse_down_out(cx.listener(|this, _, window, cx| {
695 this.set_right_clicked_index(None, window, cx);
696 cx.notify();
697 }))
698 })
699 })
700 .children(loading_view)
701 }
702}
703
704#[derive(IntoElement)]
706pub struct List<D: ListDelegate + 'static> {
707 state: Entity<ListState<D>>,
708 style: StyleRefinement,
709 options: ListOptions,
710}
711
712impl<D> List<D>
713where
714 D: ListDelegate + 'static,
715{
716 pub fn new(state: &Entity<ListState<D>>) -> Self {
718 Self {
719 state: state.clone(),
720 style: StyleRefinement::default(),
721 options: ListOptions::default(),
722 }
723 }
724
725 pub fn scrollbar_visible(mut self, visible: bool) -> Self {
727 self.options.scrollbar_visible = visible;
728 self
729 }
730
731 pub fn search_placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
733 self.options.search_placeholder = Some(placeholder.into());
734 self
735 }
736}
737
738impl<D> Styled for List<D>
739where
740 D: ListDelegate + 'static,
741{
742 fn style(&mut self) -> &mut StyleRefinement {
743 &mut self.style
744 }
745}
746
747impl<D> Sizable for List<D>
748where
749 D: ListDelegate + 'static,
750{
751 fn with_size(mut self, size: impl Into<Size>) -> Self {
752 self.options.size = size.into();
753 self
754 }
755}
756
757impl<D> RenderOnce for List<D>
758where
759 D: ListDelegate + 'static,
760{
761 fn render(mut self, _: &mut Window, cx: &mut App) -> impl IntoElement {
762 self.options.paddings = self.style.padding.clone();
765 self.options.max_height = self.style.max_size.height;
766 self.style.padding = EdgesRefinement::default();
767 self.style.max_size.height = None;
768
769 self.state.update(cx, |state, _| {
770 state.options = self.options;
771 });
772
773 div()
774 .id("list")
775 .role(Role::List)
776 .size_full()
777 .refine_style(&self.style)
778 .child(self.state.clone())
779 }
780}