1use std::ops::Range;
2use std::time::Duration;
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, 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;
25use smol::Timer;
26
27pub(crate) fn init(cx: &mut App) {
28 let context: Option<&str> = Some("List");
29 cx.bind_keys([
30 KeyBinding::new("escape", Cancel, context),
31 KeyBinding::new("enter", Confirm { secondary: false }, context),
32 KeyBinding::new("secondary-enter", Confirm { secondary: true }, context),
33 KeyBinding::new("up", SelectUp, context),
34 KeyBinding::new("down", SelectDown, context),
35 ]);
36}
37
38#[derive(Clone)]
39pub enum ListEvent {
40 Select(IndexPath),
42 Confirm(IndexPath),
44 Cancel,
46}
47
48struct ListOptions {
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 size: Size::default(),
60 scrollbar_visible: true,
61 max_height: None,
62 search_placeholder: None,
63 paddings: EdgesRefinement::default(),
64 }
65 }
66}
67
68pub struct ListState<D: ListDelegate> {
70 pub(crate) focus_handle: FocusHandle,
71 pub(crate) query_input: Entity<InputState>,
72 options: ListOptions,
73 delegate: D,
74 last_query: Option<String>,
75 scroll_handle: VirtualListScrollHandle,
76 rows_cache: RowsCache,
77 selected_index: Option<IndexPath>,
78 item_to_measure_index: IndexPath,
79 deferred_scroll_to_index: Option<(IndexPath, ScrollStrategy)>,
80 mouse_right_clicked_index: Option<IndexPath>,
81 reset_on_cancel: bool,
82 searchable: bool,
83 selectable: bool,
84 _search_task: Task<()>,
85 _load_more_task: Task<()>,
86 _query_input_subscription: Subscription,
87}
88
89impl<D> ListState<D>
90where
91 D: ListDelegate,
92{
93 pub fn new(delegate: D, window: &mut Window, cx: &mut Context<Self>) -> Self {
94 let query_input =
95 cx.new(|cx| InputState::new(window, cx).placeholder(t!("List.search_placeholder")));
96
97 let _query_input_subscription =
98 cx.subscribe_in(&query_input, window, Self::on_query_input_event);
99
100 Self {
101 focus_handle: cx.focus_handle(),
102 options: ListOptions::default(),
103 delegate,
104 rows_cache: RowsCache::default(),
105 query_input,
106 last_query: None,
107 selected_index: None,
108 selectable: true,
109 searchable: false,
110 item_to_measure_index: IndexPath::default(),
111 deferred_scroll_to_index: None,
112 mouse_right_clicked_index: None,
113 scroll_handle: VirtualListScrollHandle::new(),
114 reset_on_cancel: true,
115 _search_task: Task::ready(()),
116 _load_more_task: Task::ready(()),
117 _query_input_subscription,
118 }
119 }
120
121 pub fn searchable(mut self, searchable: bool) -> Self {
125 self.searchable = searchable;
126 self
127 }
128
129 pub fn set_searchable(&mut self, searchable: bool, cx: &mut Context<Self>) {
130 self.searchable = searchable;
131 cx.notify();
132 }
133
134 pub fn selectable(mut self, selectable: bool) -> Self {
136 self.selectable = selectable;
137 self
138 }
139
140 pub fn set_selectable(&mut self, selectable: bool, cx: &mut Context<Self>) {
142 self.selectable = selectable;
143 cx.notify();
144 }
145
146 pub fn delegate(&self) -> &D {
147 &self.delegate
148 }
149
150 pub fn delegate_mut(&mut self) -> &mut D {
151 &mut self.delegate
152 }
153
154 pub fn focus(&mut self, window: &mut Window, cx: &mut App) {
156 self.focus_handle(cx).focus(window);
157 }
158
159 pub(crate) fn is_focused(&self, window: &Window, cx: &App) -> bool {
161 self.focus_handle.is_focused(window) || self.query_input.focus_handle(cx).is_focused(window)
162 }
163
164 pub(crate) fn _set_selected_index(
167 &mut self,
168 ix: Option<IndexPath>,
169 window: &mut Window,
170 cx: &mut Context<Self>,
171 ) {
172 if !self.selectable {
173 return;
174 }
175
176 self.selected_index = ix;
177 self.delegate.set_selected_index(ix, window, cx);
178 self.scroll_to_selected_item(window, cx);
179 }
180
181 pub fn set_selected_index(
184 &mut self,
185 ix: Option<IndexPath>,
186 window: &mut Window,
187 cx: &mut Context<Self>,
188 ) {
189 self.selected_index = ix;
190 self.delegate.set_selected_index(ix, window, cx);
191 }
192
193 pub fn selected_index(&self) -> Option<IndexPath> {
194 self.selected_index
195 }
196
197 pub fn set_item_to_measure_index(
199 &mut self,
200 ix: IndexPath,
201 _: &mut Window,
202 cx: &mut Context<Self>,
203 ) {
204 self.item_to_measure_index = ix;
205 cx.notify();
206 }
207
208 pub fn scroll_to_item(
210 &mut self,
211 ix: IndexPath,
212 strategy: ScrollStrategy,
213 _: &mut Window,
214 cx: &mut Context<Self>,
215 ) {
216 if ix.section == 0 && ix.row == 0 {
217 let mut offset = self.scroll_handle.base_handle().offset();
219 offset.y = px(0.);
220 self.scroll_handle.base_handle().set_offset(offset);
221 cx.notify();
222 return;
223 }
224 self.deferred_scroll_to_index = Some((ix, strategy));
225 cx.notify();
226 }
227
228 pub fn scroll_handle(&self) -> &VirtualListScrollHandle {
230 &self.scroll_handle
231 }
232
233 pub fn scroll_to_selected_item(&mut self, _: &mut Window, cx: &mut Context<Self>) {
234 if let Some(ix) = self.selected_index {
235 self.deferred_scroll_to_index = Some((ix, ScrollStrategy::Top));
236 cx.notify();
237 }
238 }
239
240 fn on_query_input_event(
241 &mut self,
242 state: &Entity<InputState>,
243 event: &InputEvent,
244 window: &mut Window,
245 cx: &mut Context<Self>,
246 ) {
247 match event {
248 InputEvent::Change => {
249 let text = state.read(cx).value();
250 let text = text.trim().to_string();
251 if Some(&text) == self.last_query.as_ref() {
252 return;
253 }
254
255 self.set_searching(true, window, cx);
256 let search = self.delegate.perform_search(&text, window, cx);
257
258 if self.rows_cache.len() > 0 {
259 self._set_selected_index(Some(IndexPath::default()), window, cx);
260 } else {
261 self._set_selected_index(None, window, cx);
262 }
263
264 self._search_task = cx.spawn_in(window, async move |this, window| {
265 search.await;
266
267 _ = this.update_in(window, |this, _, _| {
268 this.scroll_handle.scroll_to_item(0, ScrollStrategy::Top);
269 this.last_query = Some(text);
270 });
271
272 Timer::after(Duration::from_millis(100)).await;
274 _ = this.update_in(window, |this, window, cx| {
275 this.set_searching(false, window, cx);
276 });
277 });
278 }
279 InputEvent::PressEnter { secondary } => self.on_action_confirm(
280 &Confirm {
281 secondary: *secondary,
282 },
283 window,
284 cx,
285 ),
286 _ => {}
287 }
288 }
289
290 fn set_searching(&mut self, searching: bool, window: &mut Window, cx: &mut Context<Self>) {
291 self.query_input
292 .update(cx, |input, cx| input.set_loading(searching, window, cx));
293 }
294
295 fn load_more_if_need(
298 &mut self,
299 entities_count: usize,
300 visible_end: usize,
301 window: &mut Window,
302 cx: &mut Context<Self>,
303 ) {
304 let threshold = self.delegate.load_more_threshold();
307 if visible_end >= entities_count.saturating_sub(threshold) {
310 if !self.delegate.is_eof(cx) {
311 return;
312 }
313
314 self._load_more_task = cx.spawn_in(window, async move |view, cx| {
315 _ = view.update_in(cx, |view, window, cx| {
316 view.delegate.load_more(window, cx);
317 });
318 });
319 }
320 }
321
322 pub(crate) fn reset_on_cancel(mut self, reset: bool) -> Self {
323 self.reset_on_cancel = reset;
324 self
325 }
326
327 fn on_action_cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
328 cx.propagate();
329 if self.reset_on_cancel {
330 self._set_selected_index(None, window, cx);
331 }
332
333 self.delegate.cancel(window, cx);
334 cx.emit(ListEvent::Cancel);
335 cx.notify();
336 }
337
338 fn on_action_confirm(
339 &mut self,
340 confirm: &Confirm,
341 window: &mut Window,
342 cx: &mut Context<Self>,
343 ) {
344 if self.rows_cache.len() == 0 {
345 return;
346 }
347
348 let Some(ix) = self.selected_index else {
349 return;
350 };
351
352 self.delegate
353 .set_selected_index(self.selected_index, window, cx);
354 self.delegate.confirm(confirm.secondary, window, cx);
355 cx.emit(ListEvent::Confirm(ix));
356 cx.notify();
357 }
358
359 fn select_item(&mut self, ix: IndexPath, window: &mut Window, cx: &mut Context<Self>) {
360 if !self.selectable {
361 return;
362 }
363
364 self.selected_index = Some(ix);
365 self.delegate.set_selected_index(Some(ix), window, cx);
366 self.scroll_to_selected_item(window, cx);
367 cx.emit(ListEvent::Select(ix));
368 cx.notify();
369 }
370
371 pub(crate) fn on_action_select_prev(
372 &mut self,
373 _: &SelectUp,
374 window: &mut Window,
375 cx: &mut Context<Self>,
376 ) {
377 if self.rows_cache.len() == 0 {
378 return;
379 }
380
381 let prev_ix = self.rows_cache.prev(self.selected_index);
382 self.select_item(prev_ix, window, cx);
383 }
384
385 pub(crate) fn on_action_select_next(
386 &mut self,
387 _: &SelectDown,
388 window: &mut Window,
389 cx: &mut Context<Self>,
390 ) {
391 if self.rows_cache.len() == 0 {
392 return;
393 }
394
395 let next_ix = self.rows_cache.next(self.selected_index);
396 self.select_item(next_ix, window, cx);
397 }
398
399 fn prepare_items_if_needed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
400 let sections_count = self.delegate.sections_count(cx);
401
402 let mut measured_size = MeasuredEntrySize::default();
403
404 let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
406 measured_size.item_size = self
407 .render_list_item(self.item_to_measure_index, window, cx)
408 .into_any_element()
409 .layout_as_root(available_space, window, cx);
410
411 if let Some(mut el) = self
412 .delegate
413 .render_section_header(0, window, cx)
414 .map(|r| r.into_any_element())
415 {
416 measured_size.section_header_size = el.layout_as_root(available_space, window, cx);
417 }
418 if let Some(mut el) = self
419 .delegate
420 .render_section_footer(0, window, cx)
421 .map(|r| r.into_any_element())
422 {
423 measured_size.section_footer_size = el.layout_as_root(available_space, window, cx);
424 }
425
426 self.rows_cache
427 .prepare_if_needed(sections_count, measured_size, cx, |section_ix, cx| {
428 self.delegate.items_count(section_ix, cx)
429 });
430 }
431
432 fn render_list_item(
433 &self,
434 ix: IndexPath,
435 window: &mut Window,
436 cx: &mut Context<Self>,
437 ) -> impl IntoElement {
438 let selectable = self.selectable;
439 let selected = self.selected_index.map(|s| s.eq_row(ix)).unwrap_or(false);
440 let mouse_right_clicked = self
441 .mouse_right_clicked_index
442 .map(|s| s.eq_row(ix))
443 .unwrap_or(false);
444 let id = SharedString::from(format!("list-item-{}", ix));
445
446 div()
447 .id(id)
448 .w_full()
449 .relative()
450 .children(self.delegate.render_item(ix, window, cx).map(|item| {
451 item.selected(selected)
452 .secondary_selected(mouse_right_clicked)
453 }))
454 .when(selectable, |this| {
455 this.on_click(cx.listener(move |this, e: &ClickEvent, window, cx| {
456 this.mouse_right_clicked_index = None;
457 this.selected_index = Some(ix);
458 this.on_action_confirm(
459 &Confirm {
460 secondary: e.modifiers().secondary(),
461 },
462 window,
463 cx,
464 );
465 }))
466 .on_mouse_down(
467 MouseButton::Right,
468 cx.listener(move |this, _, _, cx| {
469 this.mouse_right_clicked_index = Some(ix);
470 cx.notify();
471 }),
472 )
473 })
474 }
475
476 fn render_items(
477 &self,
478 items_count: usize,
479 entities_count: usize,
480 window: &mut Window,
481 cx: &mut Context<Self>,
482 ) -> impl IntoElement {
483 let rows_cache = self.rows_cache.clone();
484 let scrollbar_visible = self.options.scrollbar_visible;
485 let scroll_handle = self.scroll_handle.clone();
486 let measured_size = rows_cache.measured_size();
487
488 v_flex()
489 .flex_grow()
490 .relative()
491 .h_full()
492 .min_w(measured_size.item_size.width)
493 .when_some(self.options.max_height, |this, h| this.max_h(h))
494 .overflow_hidden()
495 .when(items_count == 0, |this| {
496 this.child(self.delegate.render_empty(window, cx))
497 })
498 .when(items_count > 0, {
499 |this| {
500 this.child(
501 v_virtual_list(
502 cx.entity(),
503 "virtual-list",
504 rows_cache.entries_sizes.clone(),
505 move |list, visible_range: Range<usize>, window, cx| {
506 list.load_more_if_need(
507 entities_count,
508 visible_range.end,
509 window,
510 cx,
511 );
512
513 visible_range
518 .map(|ix| {
519 let Some(entry) = rows_cache.get(ix) else {
520 return div();
521 };
522
523 div().children(match entry {
524 RowEntry::Entry(index) => Some(
525 list.render_list_item(index, window, cx)
526 .into_any_element(),
527 ),
528 RowEntry::SectionHeader(section_ix) => list
529 .delegate()
530 .render_section_header(section_ix, window, cx)
531 .map(|r| r.into_any_element()),
532 RowEntry::SectionFooter(section_ix) => list
533 .delegate()
534 .render_section_footer(section_ix, window, cx)
535 .map(|r| r.into_any_element()),
536 })
537 })
538 .collect::<Vec<_>>()
539 },
540 )
541 .paddings(self.options.paddings.clone())
542 .when(self.options.max_height.is_some(), |this| {
543 this.with_sizing_behavior(ListSizingBehavior::Infer)
544 })
545 .track_scroll(&scroll_handle)
546 .into_any_element(),
547 )
548 }
549 })
550 .when(scrollbar_visible, |this| {
551 this.child(Scrollbar::vertical(&scroll_handle))
552 })
553 }
554}
555
556impl<D> Focusable for ListState<D>
557where
558 D: ListDelegate,
559{
560 fn focus_handle(&self, cx: &App) -> FocusHandle {
561 if self.searchable {
562 self.query_input.focus_handle(cx)
563 } else {
564 self.focus_handle.clone()
565 }
566 }
567}
568impl<D> EventEmitter<ListEvent> for ListState<D> where D: ListDelegate {}
569impl<D> Render for ListState<D>
570where
571 D: ListDelegate,
572{
573 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
574 self.prepare_items_if_needed(window, cx);
575
576 if let Some((ix, strategy)) = self.deferred_scroll_to_index.take() {
578 if let Some(item_ix) = self.rows_cache.position_of(&ix) {
579 self.scroll_handle.scroll_to_item(item_ix, strategy);
580 }
581 }
582
583 let loading = self.delegate().loading(cx);
584 let query_input = if self.searchable {
585 if let Some(placeholder) = &self.options.search_placeholder {
587 self.query_input.update(cx, |input, cx| {
588 input.set_placeholder(placeholder.clone(), window, cx);
589 });
590 }
591 Some(self.query_input.clone())
592 } else {
593 None
594 };
595
596 let loading_view = if loading {
597 Some(self.delegate.render_loading(window, cx).into_any_element())
598 } else {
599 None
600 };
601 let initial_view = if let Some(input) = &query_input {
602 if input.read(cx).value().is_empty() {
603 self.delegate.render_initial(window, cx)
604 } else {
605 None
606 }
607 } else {
608 None
609 };
610 let items_count = self.rows_cache.items_count();
611 let entities_count = self.rows_cache.len();
612 let mouse_right_clicked_index = self.mouse_right_clicked_index;
613
614 v_flex()
615 .key_context("List")
616 .id("list-state")
617 .track_focus(&self.focus_handle)
618 .size_full()
619 .relative()
620 .overflow_hidden()
621 .when_some(query_input, |this, input| {
622 this.child(
623 div()
624 .map(|this| match self.options.size {
625 Size::Small => this.px_1p5(),
626 _ => this.px_2(),
627 })
628 .border_b_1()
629 .border_color(cx.theme().border)
630 .child(
631 Input::new(&input)
632 .with_size(self.options.size)
633 .prefix(
634 Icon::new(IconName::Search)
635 .text_color(cx.theme().muted_foreground),
636 )
637 .cleanable(true)
638 .p_0()
639 .appearance(false),
640 ),
641 )
642 })
643 .when(!loading, |this| {
644 this.on_action(cx.listener(Self::on_action_cancel))
645 .on_action(cx.listener(Self::on_action_confirm))
646 .on_action(cx.listener(Self::on_action_select_next))
647 .on_action(cx.listener(Self::on_action_select_prev))
648 .map(|this| {
649 if let Some(view) = initial_view {
650 this.child(view)
651 } else {
652 this.child(self.render_items(items_count, entities_count, window, cx))
653 }
654 })
655 .when(mouse_right_clicked_index.is_some(), |this| {
657 this.on_mouse_down_out(cx.listener(|this, _, _, cx| {
658 this.mouse_right_clicked_index = None;
659 cx.notify();
660 }))
661 })
662 })
663 .children(loading_view)
664 }
665}
666
667#[derive(IntoElement)]
669pub struct List<D: ListDelegate + 'static> {
670 state: Entity<ListState<D>>,
671 style: StyleRefinement,
672 options: ListOptions,
673}
674
675impl<D> List<D>
676where
677 D: ListDelegate + 'static,
678{
679 pub fn new(state: &Entity<ListState<D>>) -> Self {
681 Self {
682 state: state.clone(),
683 style: StyleRefinement::default(),
684 options: ListOptions::default(),
685 }
686 }
687
688 pub fn scrollbar_visible(mut self, visible: bool) -> Self {
690 self.options.scrollbar_visible = visible;
691 self
692 }
693
694 pub fn search_placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
696 self.options.search_placeholder = Some(placeholder.into());
697 self
698 }
699}
700
701impl<D> Styled for List<D>
702where
703 D: ListDelegate + 'static,
704{
705 fn style(&mut self) -> &mut StyleRefinement {
706 &mut self.style
707 }
708}
709
710impl<D> Sizable for List<D>
711where
712 D: ListDelegate + 'static,
713{
714 fn with_size(mut self, size: impl Into<Size>) -> Self {
715 self.options.size = size.into();
716 self
717 }
718}
719
720impl<D> RenderOnce for List<D>
721where
722 D: ListDelegate + 'static,
723{
724 fn render(mut self, _: &mut Window, cx: &mut App) -> impl IntoElement {
725 self.options.paddings = self.style.padding.clone();
728 self.options.max_height = self.style.max_size.height;
729 self.style.padding = EdgesRefinement::default();
730 self.style.max_size.height = None;
731
732 self.state.update(cx, |state, _| {
733 state.options = self.options;
734 });
735
736 div()
737 .id("list")
738 .size_full()
739 .refine_style(&self.style)
740 .child(self.state.clone())
741 }
742}