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