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