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