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