gpui_component/list/
list.rs

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    /// Move to select item.
39    Select(IndexPath),
40    /// Click on item or pressed Enter.
41    Confirm(IndexPath),
42    /// Pressed ESC to deselect the item.
43    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    /// Set the size
107    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    /// Set the visibility of the scrollbar, default is true.
117    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    /// Sets whether the list is selectable, default is true.
128    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    /// Get the query input entity.
145    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    /// Set the selected index of the list,
162    /// this will also scroll to the selected item.
163    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    /// Set the selected index of the list,
175    /// this method will not scroll to the selected item.
176    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    /// Set a specific list item for measurement.
191    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    /// Scroll to the item at the given index.
213    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            // If the item is the first item, scroll to the top.
222            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    /// Get scroll handle
233    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    /// Set paddings for the list.
245    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                    // Always wait 100ms to avoid flicker
283                    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    /// Dispatch delegate's `load_more` method when the
309    /// visible range is near the end.
310    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        // FIXME: Here need void sections items count.
318
319        let threshold = self.delegate.load_more_threshold();
320        // Securely handle subtract logic to prevent attempt
321        // to subtract with overflow
322        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                                // NOTE: Here the v_virtual_list would not able to have gap_y,
491                                // because the section header, footer is always have rendered as a empty child item,
492                                // even the delegate give a None result.
493
494                                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        // Measure the item_height and section header/footer height.
536        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        // Scroll to the selected item if it is set.
585        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                    // Click out to cancel right clicked row
650                    .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}