Skip to main content

gpui_component/list/
list.rs

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(
32            if cx.platform_info().is_mac() {
33                "cmd-enter"
34            } else {
35                "ctrl-enter"
36            },
37            Confirm { secondary: true },
38            context,
39        ),
40        KeyBinding::new("up", SelectUp, context),
41        KeyBinding::new("down", SelectDown, context),
42    ]);
43}
44
45#[derive(Clone)]
46pub enum ListEvent {
47    /// Move to select item.
48    Select(IndexPath),
49    /// Click on item or pressed Enter.
50    Confirm(IndexPath),
51    /// Pressed ESC to deselect the item.
52    Cancel,
53}
54
55struct ListOptions {
56    scroll_bounce: Option<bool>,
57    size: Size,
58    scrollbar_visible: bool,
59    search_placeholder: Option<SharedString>,
60    max_height: Option<Length>,
61    paddings: EdgesRefinement<DefiniteLength>,
62}
63
64impl Default for ListOptions {
65    fn default() -> Self {
66        Self {
67            scroll_bounce: None,
68            size: Size::default(),
69            scrollbar_visible: true,
70            max_height: None,
71            search_placeholder: None,
72            paddings: EdgesRefinement::default(),
73        }
74    }
75}
76
77/// The state for List.
78///
79/// List required all items has the same height.
80pub struct ListState<D: ListDelegate> {
81    pub(crate) focus_handle: FocusHandle,
82    pub(crate) query_input: Entity<InputState>,
83    options: ListOptions,
84    delegate: D,
85    last_query: Option<String>,
86    scroll_handle: VirtualListScrollHandle,
87    rows_cache: RowsCache,
88    selected_index: Option<IndexPath>,
89    item_to_measure_index: IndexPath,
90    deferred_scroll_to_index: Option<(IndexPath, ScrollStrategy)>,
91    mouse_right_clicked_index: Option<IndexPath>,
92    reset_on_cancel: bool,
93    searchable: bool,
94    selectable: bool,
95    _search_task: Task<()>,
96    _load_more_task: Task<()>,
97    _query_input_subscription: Subscription,
98}
99
100impl<D> ListState<D>
101where
102    D: ListDelegate,
103{
104    pub fn new(delegate: D, window: &mut Window, cx: &mut Context<Self>) -> Self {
105        let query_input =
106            cx.new(|cx| InputState::new(window, cx).placeholder(t!("List.search_placeholder")));
107
108        let _query_input_subscription =
109            cx.subscribe_in(&query_input, window, Self::on_query_input_event);
110
111        Self {
112            focus_handle: cx.focus_handle(),
113            options: ListOptions::default(),
114            delegate,
115            rows_cache: RowsCache::default(),
116            query_input,
117            last_query: None,
118            selected_index: None,
119            selectable: true,
120            searchable: false,
121            item_to_measure_index: IndexPath::default(),
122            deferred_scroll_to_index: None,
123            mouse_right_clicked_index: None,
124            scroll_handle: VirtualListScrollHandle::new(),
125            reset_on_cancel: true,
126            _search_task: Task::ready(()),
127            _load_more_task: Task::ready(()),
128            _query_input_subscription,
129        }
130    }
131
132    /// Sets whether the list is searchable, default is `false`.
133    ///
134    /// When `true`, there will be a search input at the top of the list.
135    pub fn searchable(mut self, searchable: bool) -> Self {
136        self.searchable = searchable;
137        self
138    }
139
140    pub fn set_searchable(&mut self, searchable: bool, cx: &mut Context<Self>) {
141        if self.searchable != searchable {
142            self.searchable = searchable;
143            cx.notify();
144        }
145    }
146
147    /// Sets whether the list is selectable, default is true.
148    pub fn selectable(mut self, selectable: bool) -> Self {
149        self.selectable = selectable;
150        self
151    }
152
153    /// Sets whether the list is selectable, default is true.
154    pub fn set_selectable(&mut self, selectable: bool, cx: &mut Context<Self>) {
155        self.selectable = selectable;
156        cx.notify();
157    }
158
159    pub fn delegate(&self) -> &D {
160        &self.delegate
161    }
162
163    pub fn delegate_mut(&mut self) -> &mut D {
164        &mut self.delegate
165    }
166
167    /// Focus the list, if the list is searchable, focus the search input.
168    pub fn focus(&mut self, window: &mut Window, cx: &mut App) {
169        self.focus_handle(cx).focus(window, cx);
170    }
171
172    /// Return true if either the list or the search input is focused.
173    pub(crate) fn is_focused(&self, window: &Window, cx: &App) -> bool {
174        self.focus_handle.is_focused(window) || self.query_input.focus_handle(cx).is_focused(window)
175    }
176
177    /// Set the selected index of the list,
178    /// this will also scroll to the selected item.
179    pub(crate) fn _set_selected_index(
180        &mut self,
181        ix: Option<IndexPath>,
182        window: &mut Window,
183        cx: &mut Context<Self>,
184    ) {
185        if !self.selectable {
186            return;
187        }
188
189        self.selected_index = ix;
190        self.delegate.set_selected_index(ix, window, cx);
191        self.scroll_to_selected_item(window, cx);
192    }
193
194    /// Set the selected index of the list,
195    /// this method will not scroll to the selected item.
196    pub fn set_selected_index(
197        &mut self,
198        ix: Option<IndexPath>,
199        window: &mut Window,
200        cx: &mut Context<Self>,
201    ) {
202        self.selected_index = ix;
203        self.delegate.set_selected_index(ix, window, cx);
204    }
205
206    pub fn selected_index(&self) -> Option<IndexPath> {
207        self.selected_index
208    }
209
210    /// Set the index of the item that has been right clicked.
211    pub fn set_right_clicked_index(
212        &mut self,
213        ix: Option<IndexPath>,
214        window: &mut Window,
215        cx: &mut Context<Self>,
216    ) {
217        self.mouse_right_clicked_index = ix;
218        self.delegate.set_right_clicked_index(ix, window, cx);
219    }
220
221    /// Returns the index of the item that has been right clicked.
222    pub fn right_clicked_index(&self) -> Option<IndexPath> {
223        self.mouse_right_clicked_index
224    }
225
226    /// Set the query text of the search input, this will trigger a search.
227    pub fn set_query(&mut self, query: &str, window: &mut Window, cx: &mut Context<Self>) {
228        let query = query.to_string();
229        self.query_input.update(cx, |input, cx| {
230            input.set_value(query.clone(), window, cx);
231        });
232
233        // `set_value` does not emit `InputEvent::Change`, so start the search here.
234        self.start_search(query.trim().to_string(), window, cx);
235    }
236
237    /// Set a specific list item for measurement.
238    ///
239    /// If the item is absent, measure the first item in the first non-empty section.
240    pub fn set_item_to_measure_index(
241        &mut self,
242        ix: IndexPath,
243        _: &mut Window,
244        cx: &mut Context<Self>,
245    ) {
246        self.item_to_measure_index = ix;
247        cx.notify();
248    }
249
250    /// Scroll to the item at the given index.
251    pub fn scroll_to_item(
252        &mut self,
253        ix: IndexPath,
254        strategy: ScrollStrategy,
255        _: &mut Window,
256        cx: &mut Context<Self>,
257    ) {
258        if ix.section == 0 && ix.row == 0 {
259            // If the item is the first item, scroll to the top.
260            let mut offset = self.scroll_handle.base_handle().offset();
261            offset.y = px(0.);
262            self.scroll_handle.base_handle().set_offset(offset);
263            cx.notify();
264            return;
265        }
266        self.deferred_scroll_to_index = Some((ix, strategy));
267        cx.notify();
268    }
269
270    /// Get scroll handle
271    pub fn scroll_handle(&self) -> &VirtualListScrollHandle {
272        &self.scroll_handle
273    }
274
275    pub fn scroll_to_selected_item(&mut self, _: &mut Window, cx: &mut Context<Self>) {
276        if let Some(ix) = self.selected_index {
277            self.deferred_scroll_to_index = Some((ix, ScrollStrategy::Top));
278            cx.notify();
279        }
280    }
281
282    fn on_query_input_event(
283        &mut self,
284        state: &Entity<InputState>,
285        event: &InputEvent,
286        window: &mut Window,
287        cx: &mut Context<Self>,
288    ) {
289        match event {
290            InputEvent::Change => {
291                let text = state.read(cx).value();
292                let text = text.trim().to_string();
293                if Some(&text) == self.last_query.as_ref() {
294                    return;
295                }
296
297                self.start_search(text, window, cx);
298            }
299            _ => {}
300        }
301    }
302
303    fn start_search(&mut self, query: String, window: &mut Window, cx: &mut Context<Self>) {
304        self.set_searching(true, window, cx);
305        let search = self.delegate.perform_search(&query, window, cx);
306
307        if self.rows_cache.len() > 0 {
308            self._set_selected_index(Some(IndexPath::default()), window, cx);
309        } else {
310            self._set_selected_index(None, window, cx);
311        }
312
313        self._search_task = cx.spawn_in(window, async move |this, window| {
314            search.await;
315
316            _ = this.update_in(window, |this, _, _| {
317                this.scroll_handle.scroll_to_item(0, ScrollStrategy::Top);
318                this.last_query = Some(query);
319            });
320
321            // Always wait 100ms to avoid flicker
322            window
323                .background_executor()
324                .timer(Duration::from_millis(100))
325                .await;
326            _ = this.update_in(window, |this, window, cx| {
327                this.set_searching(false, window, cx);
328            });
329        });
330    }
331
332    fn set_searching(&mut self, searching: bool, window: &mut Window, cx: &mut Context<Self>) {
333        self.query_input
334            .update(cx, |input, cx| input.set_loading(searching, window, cx));
335    }
336
337    /// Dispatch delegate's `load_more` method when the
338    /// visible range is near the end.
339    fn load_more_if_need(
340        &mut self,
341        entities_count: usize,
342        visible_end: usize,
343        window: &mut Window,
344        cx: &mut Context<Self>,
345    ) {
346        // FIXME: Here need void sections items count.
347
348        let threshold = self.delegate.load_more_threshold();
349        // Securely handle subtract logic to prevent attempt
350        // to subtract with overflow
351        if visible_end >= entities_count.saturating_sub(threshold) {
352            if !self.delegate.has_more(cx) {
353                return;
354            }
355
356            self._load_more_task = cx.spawn_in(window, async move |view, cx| {
357                _ = view.update_in(cx, |view, window, cx| {
358                    view.delegate.load_more(window, cx);
359                });
360            });
361        }
362    }
363
364    pub(crate) fn reset_on_cancel(mut self, reset: bool) -> Self {
365        self.reset_on_cancel = reset;
366        self
367    }
368
369    fn on_action_cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
370        cx.propagate();
371        if self.reset_on_cancel {
372            self._set_selected_index(None, window, cx);
373        }
374
375        self.delegate.cancel(window, cx);
376        cx.emit(ListEvent::Cancel);
377        cx.notify();
378    }
379
380    fn on_action_confirm(
381        &mut self,
382        confirm: &Confirm,
383        window: &mut Window,
384        cx: &mut Context<Self>,
385    ) {
386        if self.rows_cache.len() == 0 {
387            return;
388        }
389
390        let Some(ix) = self.selected_index else {
391            return;
392        };
393
394        self.delegate
395            .set_selected_index(self.selected_index, window, cx);
396        self.delegate.confirm(confirm.secondary, window, cx);
397        cx.emit(ListEvent::Confirm(ix));
398        cx.notify();
399    }
400
401    fn select_item(&mut self, ix: IndexPath, window: &mut Window, cx: &mut Context<Self>) {
402        if !self.selectable {
403            return;
404        }
405
406        self.selected_index = Some(ix);
407        self.delegate.set_selected_index(Some(ix), window, cx);
408        self.scroll_to_selected_item(window, cx);
409        cx.emit(ListEvent::Select(ix));
410        cx.notify();
411    }
412
413    pub(crate) fn on_action_select_prev(
414        &mut self,
415        _: &SelectUp,
416        window: &mut Window,
417        cx: &mut Context<Self>,
418    ) {
419        if self.rows_cache.len() == 0 {
420            return;
421        }
422
423        let prev_ix = self.rows_cache.prev(self.selected_index);
424        self.select_item(prev_ix, window, cx);
425    }
426
427    pub(crate) fn on_action_select_next(
428        &mut self,
429        _: &SelectDown,
430        window: &mut Window,
431        cx: &mut Context<Self>,
432    ) {
433        if self.rows_cache.len() == 0 {
434            return;
435        }
436
437        let next_ix = self.rows_cache.next(self.selected_index);
438        self.select_item(next_ix, window, cx);
439    }
440
441    fn prepare_items_if_needed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
442        let sections_count = self.delegate.sections_count(cx).max(1);
443        let mut measured_size = MeasuredEntrySize::default();
444
445        // Measure the item_height and section header/footer height.
446        let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
447        // Use the fallback for this measurement without overwriting the caller's configured index.
448        let requested = self.item_to_measure_index;
449        let item_to_measure = if requested.section < sections_count
450            && requested.row < self.delegate.items_count(requested.section, cx)
451        {
452            Some(requested)
453        } else {
454            (0..sections_count)
455                .find(|section| self.delegate.items_count(*section, cx) > 0)
456                .map(|section| IndexPath::default().section(section))
457        };
458        if let Some(index) = item_to_measure {
459            measured_size.item_size = self
460                .render_list_item(index, window, cx)
461                .into_any_element()
462                .layout_as_root(available_space, window, cx);
463        }
464
465        if let Some(mut el) = self
466            .delegate
467            .render_section_header(0, window, cx)
468            .map(|r| r.into_any_element())
469        {
470            measured_size.section_header_size = el.layout_as_root(available_space, window, cx);
471        }
472        if let Some(mut el) = self
473            .delegate
474            .render_section_footer(0, window, cx)
475            .map(|r| r.into_any_element())
476        {
477            measured_size.section_footer_size = el.layout_as_root(available_space, window, cx);
478        }
479
480        self.rows_cache
481            .prepare_if_needed(sections_count, measured_size, cx, |section_ix, cx| {
482                self.delegate.items_count(section_ix, cx)
483            });
484    }
485
486    fn render_list_item(
487        &mut self,
488        ix: IndexPath,
489        window: &mut Window,
490        cx: &mut Context<Self>,
491    ) -> impl IntoElement {
492        let selectable = self.selectable;
493        let selected = self.selected_index.map(|s| s.eq_row(ix)).unwrap_or(false);
494        let mouse_right_clicked = self
495            .mouse_right_clicked_index
496            .map(|s| s.eq_row(ix))
497            .unwrap_or(false);
498        let id = SharedString::from(format!("list-item-{}", ix));
499
500        let total_items = self.rows_cache.items_count();
501
502        div()
503            .id(id)
504            .role(Role::ListItem)
505            .aria_position_in_set(ix.row + 1)
506            .aria_size_of_set(total_items)
507            .aria_selected(selected)
508            .w_full()
509            .relative()
510            .overflow_hidden()
511            .children(self.delegate.render_item(ix, window, cx).map(|item| {
512                item.selected(selected)
513                    .secondary_selected(mouse_right_clicked)
514            }))
515            .when(selectable, |this| {
516                this.on_click(cx.listener(move |this, e: &ClickEvent, window, cx| {
517                    this.set_right_clicked_index(None, window, cx);
518                    this.selected_index = Some(ix);
519                    this.on_action_confirm(
520                        &Confirm {
521                            secondary: e.modifiers().secondary(cx),
522                        },
523                        window,
524                        cx,
525                    );
526                }))
527                .on_mouse_down(
528                    MouseButton::Right,
529                    cx.listener(move |this, _, window, cx| {
530                        this.set_right_clicked_index(Some(ix), window, cx);
531                        cx.notify();
532                    }),
533                )
534            })
535    }
536
537    fn render_items(
538        &mut self,
539        items_count: usize,
540        entities_count: usize,
541        window: &mut Window,
542        cx: &mut Context<Self>,
543    ) -> impl IntoElement {
544        let rows_cache = self.rows_cache.clone();
545        let scrollbar_visible = self.options.scrollbar_visible;
546        let scroll_handle = self.scroll_handle.clone();
547        let item_to_measure_index = rows_cache
548            .position_of(&self.item_to_measure_index)
549            .or_else(|| rows_cache.first_entry_position())
550            .unwrap_or(0);
551
552        v_flex()
553            .flex_grow_1()
554            .relative()
555            .size_full()
556            .when_some(self.options.max_height, |this, h| this.max_h(h))
557            .overflow_hidden()
558            .when(items_count == 0, |this| {
559                this.child(self.delegate.render_empty(window, cx))
560            })
561            .when(items_count > 0, {
562                |this| {
563                    this.child(
564                        v_virtual_list(
565                            cx.entity(),
566                            "virtual-list",
567                            rows_cache.entries_sizes.clone(),
568                            move |list, visible_range: Range<usize>, window, cx| {
569                                list.load_more_if_need(
570                                    entities_count,
571                                    visible_range.end,
572                                    window,
573                                    cx,
574                                );
575
576                                // NOTE: Here the v_virtual_list would not able to have gap_y,
577                                // because the section header, footer is always have rendered as a empty child item,
578                                // even the delegate give a None result.
579
580                                visible_range
581                                    .map(|ix| {
582                                        let Some(entry) = rows_cache.get(ix) else {
583                                            return div();
584                                        };
585
586                                        div().children(match entry {
587                                            RowEntry::Entry(index) => Some(
588                                                list.render_list_item(index, window, cx)
589                                                    .into_any_element(),
590                                            ),
591                                            RowEntry::SectionHeader(section_ix) => list
592                                                .delegate_mut()
593                                                .render_section_header(section_ix, window, cx)
594                                                .map(|r| r.into_any_element()),
595                                            RowEntry::SectionFooter(section_ix) => list
596                                                .delegate_mut()
597                                                .render_section_footer(section_ix, window, cx)
598                                                .map(|r| r.into_any_element()),
599                                        })
600                                    })
601                                    .collect::<Vec<_>>()
602                            },
603                        )
604                        .with_item_to_measure_index(item_to_measure_index)
605                        .paddings(self.options.paddings.clone())
606                        .when(self.options.max_height.is_some(), |this| {
607                            this.with_sizing_behavior(ListSizingBehavior::Infer)
608                        })
609                        .track_scroll(&scroll_handle)
610                        .map(|list| {
611                            gpui_base::ScrollBounce::new("list-scroll-bounce", &scroll_handle, list)
612                                .when_some(self.options.scroll_bounce, |bounce, on| {
613                                    bounce.enabled(on)
614                                })
615                        })
616                        .into_any_element(),
617                    )
618                }
619            })
620            .when(scrollbar_visible, |this| {
621                this.child(Scrollbar::vertical(&scroll_handle).viewport_from_layout())
622            })
623    }
624}
625
626impl<D> Focusable for ListState<D>
627where
628    D: ListDelegate,
629{
630    fn focus_handle(&self, cx: &App) -> FocusHandle {
631        if self.searchable {
632            self.query_input.focus_handle(cx)
633        } else {
634            self.focus_handle.clone()
635        }
636    }
637}
638impl<D> EventEmitter<ListEvent> for ListState<D> where D: ListDelegate {}
639impl<D> Render for ListState<D>
640where
641    D: ListDelegate,
642{
643    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
644        self.prepare_items_if_needed(window, cx);
645
646        // Scroll to the selected item if it is set.
647        if let Some((ix, strategy)) = self.deferred_scroll_to_index.take() {
648            if let Some(item_ix) = self.rows_cache.position_of(&ix) {
649                self.scroll_handle.scroll_to_item(item_ix, strategy);
650            }
651        }
652
653        let loading = self.delegate().loading(cx);
654        let query_input = if self.searchable {
655            // sync placeholder
656            if let Some(placeholder) = &self.options.search_placeholder {
657                self.query_input.update(cx, |input, cx| {
658                    input.set_placeholder(placeholder.clone(), window, cx);
659                });
660            }
661            Some(self.query_input.clone())
662        } else {
663            None
664        };
665
666        let loading_view = if loading {
667            Some(self.delegate.render_loading(window, cx).into_any_element())
668        } else {
669            None
670        };
671        let initial_view = if let Some(input) = &query_input {
672            if input.read(cx).value().is_empty() {
673                self.delegate.render_initial(window, cx)
674            } else {
675                None
676            }
677        } else {
678            None
679        };
680        let items_count = self.rows_cache.items_count();
681        let entities_count = self.rows_cache.len();
682        let mouse_right_clicked_index = self.mouse_right_clicked_index;
683
684        v_flex()
685            .key_context("List")
686            .id("list-state")
687            .track_focus(&self.focus_handle)
688            .role(Role::List)
689            .size_full()
690            .relative()
691            .overflow_hidden()
692            .when_some(query_input, |this, input| {
693                this.child(
694                    div()
695                        .map(|this| match self.options.size {
696                            Size::Small => this.px_1p5(),
697                            _ => this.px_2(),
698                        })
699                        .border_b_1()
700                        .border_color(cx.theme().border)
701                        .child(
702                            Input::new(&input)
703                                .with_size(self.options.size)
704                                .prefix(
705                                    Icon::new(IconName::Search)
706                                        .text_color(cx.theme().muted_foreground),
707                                )
708                                .cleanable(true)
709                                .p_0()
710                                .appearance(false),
711                        ),
712                )
713            })
714            .when(!loading, |this| {
715                this.on_action(cx.listener(Self::on_action_cancel))
716                    .on_action(cx.listener(Self::on_action_confirm))
717                    .on_action(cx.listener(Self::on_action_select_next))
718                    .on_action(cx.listener(Self::on_action_select_prev))
719                    .map(|this| {
720                        if let Some(view) = initial_view {
721                            this.child(view)
722                        } else {
723                            this.child(self.render_items(items_count, entities_count, window, cx))
724                        }
725                    })
726                    // Click out to cancel right clicked row
727                    .when(mouse_right_clicked_index.is_some(), |this| {
728                        this.on_mouse_down_out(cx.listener(|this, _, window, cx| {
729                            this.set_right_clicked_index(None, window, cx);
730                            cx.notify();
731                        }))
732                    })
733            })
734            .children(loading_view)
735    }
736}
737
738/// The List element.
739#[derive(IntoElement)]
740pub struct List<D: ListDelegate + 'static> {
741    state: Entity<ListState<D>>,
742    style: StyleRefinement,
743    options: ListOptions,
744}
745
746impl<D> List<D>
747where
748    D: ListDelegate + 'static,
749{
750    /// Create a new List element with the given ListState entity.
751    pub fn new(state: &Entity<ListState<D>>) -> Self {
752        Self {
753            state: state.clone(),
754            style: StyleRefinement::default(),
755            options: ListOptions::default(),
756        }
757    }
758
759    /// Set whether the scrollbar is visible, default is `true`.
760    pub fn scrollbar_visible(mut self, visible: bool) -> Self {
761        self.options.scrollbar_visible = visible;
762        self
763    }
764
765    /// Bounce at the vertical boundaries. On by default on iOS, Android and
766    /// macOS, off elsewhere; reduced motion disables displacement.
767    pub fn scroll_bounce(mut self, enabled: bool) -> Self {
768        self.options.scroll_bounce = Some(enabled);
769        self
770    }
771
772    /// Sets the placeholder text for the search input.
773    pub fn search_placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
774        self.options.search_placeholder = Some(placeholder.into());
775        self
776    }
777}
778
779impl<D> Styled for List<D>
780where
781    D: ListDelegate + 'static,
782{
783    fn style(&mut self) -> &mut StyleRefinement {
784        &mut self.style
785    }
786}
787
788impl<D> Sizable for List<D>
789where
790    D: ListDelegate + 'static,
791{
792    fn with_size(mut self, size: impl Into<Size>) -> Self {
793        self.options.size = size.into();
794        self
795    }
796}
797
798impl<D> RenderOnce for List<D>
799where
800    D: ListDelegate + 'static,
801{
802    fn render(mut self, _: &mut Window, cx: &mut App) -> impl IntoElement {
803        // Take paddings, max_height to options, and clear them from style,
804        // because they would be applied to the inner virtual list.
805        self.options.paddings = self.style.padding.clone();
806        self.options.max_height = self.style.max_size.height;
807        self.style.padding = EdgesRefinement::default();
808        self.style.max_size.height = None;
809
810        self.state.update(cx, |state, _| {
811            state.options = self.options;
812        });
813
814        div()
815            .id("list")
816            .size_full()
817            .refine_style(&self.style)
818            .child(self.state.clone())
819    }
820}
821
822#[cfg(test)]
823mod measurement_tests {
824    use super::*;
825    use crate::list::ListItem;
826    use gpui::{Element, TestAppContext};
827
828    struct Delegate {
829        counts: Vec<usize>,
830    }
831
832    impl ListDelegate for Delegate {
833        type Item = ListItem;
834        fn sections_count(&self, _: &App) -> usize {
835            self.counts.len()
836        }
837        fn items_count(&self, section: usize, _: &App) -> usize {
838            self.counts[section]
839        }
840        fn set_selected_index(
841            &mut self,
842            _: Option<IndexPath>,
843            _: &mut Window,
844            _: &mut Context<ListState<Self>>,
845        ) {
846        }
847        fn render_item(
848            &mut self,
849            index: IndexPath,
850            _: &mut Window,
851            _: &mut Context<ListState<Self>>,
852        ) -> Option<ListItem> {
853            (index.row < *self.counts.get(index.section)?)
854                .then(|| ListItem::new(index.row).h(px(if index.row == 0 { 36. } else { 48. })))
855        }
856    }
857
858    struct BounceList(Entity<ListState<Delegate>>);
859
860    impl Render for BounceList {
861        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
862            div()
863                .w(px(240.))
864                .h(px(200.))
865                .child(List::new(&self.0).scroll_bounce(true).size_full())
866        }
867    }
868
869    #[gpui::test]
870    fn scroll_bounce_list_accepts_a_reverse_wheel_during_return(cx: &mut TestAppContext) {
871        cx.update(crate::init);
872        let (view, cx) = cx.add_window_view(|window, cx| {
873            BounceList(cx.new(|cx| ListState::new(Delegate { counts: vec![40] }, window, cx)))
874        });
875        cx.update(|window, cx| window.draw(cx).clear(cx));
876        let state = view.read_with(cx, |view, _| view.0.clone());
877        let wheel = |cx: &mut gpui::VisualTestContext, dy| {
878            cx.simulate_event(gpui::ScrollEvent {
879                position: gpui::point(px(100.), px(100.)),
880                delta: gpui::ScrollDelta::Lines(gpui::point(0., dy)),
881                ..Default::default()
882            });
883        };
884        for (dy, phase) in [
885            (-10000., gpui::TouchPhase::Started),
886            (0., gpui::TouchPhase::Ended),
887        ] {
888            cx.simulate_event(gpui::ScrollEvent {
889                position: gpui::point(px(100.), px(100.)),
890                delta: gpui::ScrollDelta::Pixels(gpui::point(px(0.), px(dy))),
891                touch_phase: phase,
892                ..Default::default()
893            });
894        }
895        cx.update(|window, cx| window.draw(cx).clear(cx));
896        let end = state.read_with(cx, |state, _| state.scroll_handle.offset().y);
897        assert!(end < px(0.));
898        cx.executor()
899            .advance_clock(std::time::Duration::from_millis(200));
900        cx.update(|window, cx| window.draw(cx).clear(cx));
901        wheel(cx, 2.);
902        assert!(state.read_with(cx, |state, _| state.scroll_handle.offset().y) > end);
903    }
904
905    #[gpui::test]
906    fn measures_an_existing_row_when_the_requested_item_is_absent(cx: &mut TestAppContext) {
907        cx.update(crate::init);
908        let window = cx.add_empty_window();
909        window.draw(
910            gpui::point(px(0.), px(0.)),
911            size(px(300.), px(300.)),
912            |window, cx| {
913                let list = cx.new(|cx| ListState::new(Delegate { counts: vec![0, 2] }, window, cx));
914                list.update(cx, |list, cx| {
915                    for (requested, expected_height) in [
916                        (IndexPath::default(), 36.),
917                        (IndexPath::new(1).section(1), 48.),
918                        (IndexPath::new(99).section(1), 36.),
919                        (IndexPath::new(0).section(99), 36.),
920                    ] {
921                        list.set_item_to_measure_index(requested, window, cx);
922                        list.prepare_items_if_needed(window, cx);
923                        let position = list
924                            .rows_cache
925                            .position_of(&IndexPath::new(0).section(1))
926                            .unwrap();
927                        assert_eq!(
928                            list.rows_cache.entries_sizes[position].height,
929                            px(expected_height)
930                        );
931                        assert_eq!(list.item_to_measure_index, requested);
932                    }
933                    let requested = IndexPath::new(1).section(1);
934                    list.set_item_to_measure_index(requested, window, cx);
935                    // Filtering removes the requested row, then all rows, before restoring it.
936                    for (counts, expected_height) in [
937                        (vec![0, 2], Some(48.)),
938                        (vec![0, 1], Some(36.)),
939                        (vec![0, 0], None),
940                        (vec![0, 2], Some(48.)),
941                    ] {
942                        list.delegate.counts = counts;
943                        list.prepare_items_if_needed(window, cx);
944                        if let Some(height) = expected_height {
945                            let position = list
946                                .rows_cache
947                                .position_of(&IndexPath::new(0).section(1))
948                                .unwrap();
949                            assert_eq!(list.rows_cache.entries_sizes[position].height, px(height));
950                        } else {
951                            assert_eq!(list.rows_cache.items_count(), 0);
952                            assert!(list.rows_cache.entries_sizes.is_empty());
953                        }
954                        assert_eq!(list.item_to_measure_index, requested);
955                    }
956                });
957                div()
958            },
959        );
960    }
961
962    #[gpui::test]
963    fn list_state_has_list_role(cx: &mut TestAppContext) {
964        cx.update(crate::init);
965        let window = cx.add_empty_window();
966        window.draw(
967            gpui::point(px(0.), px(0.)),
968            size(px(300.), px(300.)),
969            |window, cx| {
970                let list = cx.new(|cx| ListState::new(Delegate { counts: vec![2] }, window, cx));
971                list.update(cx, |list, cx| {
972                    let element = list.render(window, cx).into_element();
973                    assert_eq!(element.a11y_role(), Some(Role::List));
974                });
975                div()
976            },
977        );
978    }
979}