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 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
69pub 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 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 pub fn selectable(mut self, selectable: bool) -> Self {
141 self.selectable = selectable;
142 self
143 }
144
145 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 pub fn focus(&mut self, window: &mut Window, cx: &mut App) {
161 self.focus_handle(cx).focus(window, cx);
162 }
163
164 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 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 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 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 pub fn right_clicked_index(&self) -> Option<IndexPath> {
215 self.mouse_right_clicked_index
216 }
217
218 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 self.start_search(query.trim().to_string(), window, cx);
227 }
228
229 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 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 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 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 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 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 let threshold = self.delegate.load_more_threshold();
341 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 let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
439 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 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 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 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 .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#[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 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 pub fn scrollbar_visible(mut self, visible: bool) -> Self {
753 self.options.scrollbar_visible = visible;
754 self
755 }
756
757 pub fn scroll_bounce(mut self, enabled: bool) -> Self {
760 self.options.scroll_bounce = Some(enabled);
761 self
762 }
763
764 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 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 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}