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 Select(IndexPath),
41 Confirm(IndexPath),
43 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
67pub 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 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 pub fn selectable(mut self, selectable: bool) -> Self {
137 self.selectable = selectable;
138 self
139 }
140
141 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 pub fn focus(&mut self, window: &mut Window, cx: &mut App) {
157 self.focus_handle(cx).focus(window, cx);
158 }
159
160 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 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 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 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 pub fn right_clicked_index(&self) -> Option<IndexPath> {
211 self.mouse_right_clicked_index
212 }
213
214 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 self.start_search(query.trim().to_string(), window, cx);
223 }
224
225 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 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 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 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 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 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 let threshold = self.delegate.load_more_threshold();
337 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 let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
435 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 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 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 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 .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#[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 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 pub fn scrollbar_visible(mut self, visible: bool) -> Self {
742 self.options.scrollbar_visible = visible;
743 self
744 }
745
746 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 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 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}