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 input::{Input, InputEvent},
9 scroll::{Scrollbar, ScrollbarState},
10 v_flex, ActiveTheme, IconName, Size,
11};
12use crate::{list::ListDelegate, v_virtual_list, VirtualListScrollHandle};
13use crate::{Icon, IndexPath, Selectable, Sizable, StyledExt};
14use gpui::{
15 div, prelude::FluentBuilder, AppContext, Entity, FocusHandle, Focusable, InteractiveElement,
16 IntoElement, KeyBinding, Length, MouseButton, ParentElement, Render, Styled, Task, Window,
17};
18use gpui::{
19 px, size, App, AvailableSpace, ClickEvent, Context, DefiniteLength, EdgesRefinement,
20 EventEmitter, ListSizingBehavior, RenderOnce, ScrollStrategy, SharedString,
21 StatefulInteractiveElement, StyleRefinement, Subscription,
22};
23use rust_i18n::t;
24use smol::Timer;
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> {
69 pub(crate) focus_handle: FocusHandle,
70 pub(crate) query_input: Entity<InputState>,
71 options: ListOptions,
72 delegate: D,
73 last_query: Option<String>,
74 scroll_handle: VirtualListScrollHandle,
75 scroll_state: ScrollbarState,
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 scroll_state: ScrollbarState::default(),
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);
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_item_to_measure_index(
200 &mut self,
201 ix: IndexPath,
202 _: &mut Window,
203 cx: &mut Context<Self>,
204 ) {
205 self.item_to_measure_index = ix;
206 cx.notify();
207 }
208
209 pub fn scroll_to_item(
211 &mut self,
212 ix: IndexPath,
213 strategy: ScrollStrategy,
214 _: &mut Window,
215 cx: &mut Context<Self>,
216 ) {
217 if ix.section == 0 && ix.row == 0 {
218 let mut offset = self.scroll_handle.base_handle().offset();
220 offset.y = px(0.);
221 self.scroll_handle.base_handle().set_offset(offset);
222 cx.notify();
223 return;
224 }
225 self.deferred_scroll_to_index = Some((ix, strategy));
226 cx.notify();
227 }
228
229 pub fn scroll_handle(&self) -> &VirtualListScrollHandle {
231 &self.scroll_handle
232 }
233
234 pub fn scroll_to_selected_item(&mut self, _: &mut Window, cx: &mut Context<Self>) {
235 if let Some(ix) = self.selected_index {
236 self.deferred_scroll_to_index = Some((ix, ScrollStrategy::Top));
237 cx.notify();
238 }
239 }
240
241 fn on_query_input_event(
242 &mut self,
243 state: &Entity<InputState>,
244 event: &InputEvent,
245 window: &mut Window,
246 cx: &mut Context<Self>,
247 ) {
248 match event {
249 InputEvent::Change => {
250 let text = state.read(cx).value();
251 let text = text.trim().to_string();
252 if Some(&text) == self.last_query.as_ref() {
253 return;
254 }
255
256 self.set_searching(true, window, cx);
257 let search = self.delegate.perform_search(&text, window, cx);
258
259 if self.rows_cache.len() > 0 {
260 self._set_selected_index(Some(IndexPath::default()), window, cx);
261 } else {
262 self._set_selected_index(None, window, cx);
263 }
264
265 self._search_task = cx.spawn_in(window, async move |this, window| {
266 search.await;
267
268 _ = this.update_in(window, |this, _, _| {
269 this.scroll_handle.scroll_to_item(0, ScrollStrategy::Top);
270 this.last_query = Some(text);
271 });
272
273 Timer::after(Duration::from_millis(100)).await;
275 _ = this.update_in(window, |this, window, cx| {
276 this.set_searching(false, window, cx);
277 });
278 });
279 }
280 InputEvent::PressEnter { secondary } => self.on_action_confirm(
281 &Confirm {
282 secondary: *secondary,
283 },
284 window,
285 cx,
286 ),
287 _ => {}
288 }
289 }
290
291 fn set_searching(&mut self, searching: bool, window: &mut Window, cx: &mut Context<Self>) {
292 self.query_input
293 .update(cx, |input, cx| input.set_loading(searching, window, cx));
294 }
295
296 fn load_more_if_need(
299 &mut self,
300 entities_count: usize,
301 visible_end: usize,
302 window: &mut Window,
303 cx: &mut Context<Self>,
304 ) {
305 let threshold = self.delegate.load_more_threshold();
308 if visible_end >= entities_count.saturating_sub(threshold) {
311 if !self.delegate.is_eof(cx) {
312 return;
313 }
314
315 self._load_more_task = cx.spawn_in(window, async move |view, cx| {
316 _ = view.update_in(cx, |view, window, cx| {
317 view.delegate.load_more(window, cx);
318 });
319 });
320 }
321 }
322
323 pub(crate) fn reset_on_cancel(mut self, reset: bool) -> Self {
324 self.reset_on_cancel = reset;
325 self
326 }
327
328 fn on_action_cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
329 cx.propagate();
330 if self.reset_on_cancel {
331 self._set_selected_index(None, window, cx);
332 }
333
334 self.delegate.cancel(window, cx);
335 cx.emit(ListEvent::Cancel);
336 cx.notify();
337 }
338
339 fn on_action_confirm(
340 &mut self,
341 confirm: &Confirm,
342 window: &mut Window,
343 cx: &mut Context<Self>,
344 ) {
345 if self.rows_cache.len() == 0 {
346 return;
347 }
348
349 let Some(ix) = self.selected_index else {
350 return;
351 };
352
353 self.delegate
354 .set_selected_index(self.selected_index, window, cx);
355 self.delegate.confirm(confirm.secondary, window, cx);
356 cx.emit(ListEvent::Confirm(ix));
357 cx.notify();
358 }
359
360 fn select_item(&mut self, ix: IndexPath, window: &mut Window, cx: &mut Context<Self>) {
361 if !self.selectable {
362 return;
363 }
364
365 self.selected_index = Some(ix);
366 self.delegate.set_selected_index(Some(ix), window, cx);
367 self.scroll_to_selected_item(window, cx);
368 cx.emit(ListEvent::Select(ix));
369 cx.notify();
370 }
371
372 pub(crate) fn on_action_select_prev(
373 &mut self,
374 _: &SelectUp,
375 window: &mut Window,
376 cx: &mut Context<Self>,
377 ) {
378 if self.rows_cache.len() == 0 {
379 return;
380 }
381
382 let prev_ix = self.rows_cache.prev(self.selected_index);
383 self.select_item(prev_ix, window, cx);
384 }
385
386 pub(crate) fn on_action_select_next(
387 &mut self,
388 _: &SelectDown,
389 window: &mut Window,
390 cx: &mut Context<Self>,
391 ) {
392 if self.rows_cache.len() == 0 {
393 return;
394 }
395
396 let next_ix = self.rows_cache.next(self.selected_index);
397 self.select_item(next_ix, window, cx);
398 }
399
400 fn prepare_items_if_needed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
401 let sections_count = self.delegate.sections_count(cx);
402
403 let mut measured_size = MeasuredEntrySize::default();
404
405 let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
407 measured_size.item_size = self
408 .render_list_item(self.item_to_measure_index, window, cx)
409 .into_any_element()
410 .layout_as_root(available_space, window, cx);
411
412 if let Some(mut el) = self
413 .delegate
414 .render_section_header(0, window, cx)
415 .map(|r| r.into_any_element())
416 {
417 measured_size.section_header_size = el.layout_as_root(available_space, window, cx);
418 }
419 if let Some(mut el) = self
420 .delegate
421 .render_section_footer(0, window, cx)
422 .map(|r| r.into_any_element())
423 {
424 measured_size.section_footer_size = el.layout_as_root(available_space, window, cx);
425 }
426
427 self.rows_cache
428 .prepare_if_needed(sections_count, measured_size, cx, |section_ix, cx| {
429 self.delegate.items_count(section_ix, cx)
430 });
431 }
432
433 fn render_list_item(
434 &self,
435 ix: IndexPath,
436 window: &mut Window,
437 cx: &mut Context<Self>,
438 ) -> impl IntoElement {
439 let selectable = self.selectable;
440 let selected = self.selected_index.map(|s| s.eq_row(ix)).unwrap_or(false);
441 let mouse_right_clicked = self
442 .mouse_right_clicked_index
443 .map(|s| s.eq_row(ix))
444 .unwrap_or(false);
445 let id = SharedString::from(format!("list-item-{}", ix));
446
447 div()
448 .id(id)
449 .w_full()
450 .relative()
451 .children(self.delegate.render_item(ix, window, cx).map(|item| {
452 item.selected(selected)
453 .secondary_selected(mouse_right_clicked)
454 }))
455 .when(selectable, |this| {
456 this.on_click(cx.listener(move |this, e: &ClickEvent, window, cx| {
457 this.mouse_right_clicked_index = None;
458 this.selected_index = Some(ix);
459 this.on_action_confirm(
460 &Confirm {
461 secondary: e.modifiers().secondary(),
462 },
463 window,
464 cx,
465 );
466 }))
467 .on_mouse_down(
468 MouseButton::Right,
469 cx.listener(move |this, _, _, cx| {
470 this.mouse_right_clicked_index = Some(ix);
471 cx.notify();
472 }),
473 )
474 })
475 }
476
477 fn render_items(
478 &self,
479 items_count: usize,
480 entities_count: usize,
481 window: &mut Window,
482 cx: &mut Context<Self>,
483 ) -> impl IntoElement {
484 let rows_cache = self.rows_cache.clone();
485 let scrollbar_visible = self.options.scrollbar_visible;
486 let scroll_handle = self.scroll_handle.clone();
487 let scroll_state = self.scroll_state.clone();
488 let measured_size = rows_cache.measured_size();
489
490 v_flex()
491 .flex_grow()
492 .relative()
493 .h_full()
494 .min_w(measured_size.item_size.width)
495 .when_some(self.options.max_height, |this, h| this.max_h(h))
496 .overflow_hidden()
497 .when(items_count == 0, |this| {
498 this.child(self.delegate.render_empty(window, cx))
499 })
500 .when(items_count > 0, {
501 |this| {
502 this.child(
503 v_virtual_list(
504 cx.entity(),
505 "virtual-list",
506 rows_cache.entries_sizes.clone(),
507 move |list, visible_range: Range<usize>, window, cx| {
508 list.load_more_if_need(
509 entities_count,
510 visible_range.end,
511 window,
512 cx,
513 );
514
515 visible_range
520 .map(|ix| {
521 let Some(entry) = rows_cache.get(ix) else {
522 return div();
523 };
524
525 div().children(match entry {
526 RowEntry::Entry(index) => Some(
527 list.render_list_item(index, window, cx)
528 .into_any_element(),
529 ),
530 RowEntry::SectionHeader(section_ix) => list
531 .delegate()
532 .render_section_header(section_ix, window, cx)
533 .map(|r| r.into_any_element()),
534 RowEntry::SectionFooter(section_ix) => list
535 .delegate()
536 .render_section_footer(section_ix, window, cx)
537 .map(|r| r.into_any_element()),
538 })
539 })
540 .collect::<Vec<_>>()
541 },
542 )
543 .paddings(self.options.paddings.clone())
544 .when(self.options.max_height.is_some(), |this| {
545 this.with_sizing_behavior(ListSizingBehavior::Infer)
546 })
547 .track_scroll(&scroll_handle)
548 .into_any_element(),
549 )
550 }
551 })
552 .when(scrollbar_visible, |this| {
553 this.child(Scrollbar::uniform_scroll(&scroll_state, &scroll_handle))
554 })
555 }
556}
557
558impl<D> Focusable for ListState<D>
559where
560 D: ListDelegate,
561{
562 fn focus_handle(&self, cx: &App) -> FocusHandle {
563 if self.searchable {
564 self.query_input.focus_handle(cx)
565 } else {
566 self.focus_handle.clone()
567 }
568 }
569}
570impl<D> EventEmitter<ListEvent> for ListState<D> where D: ListDelegate {}
571impl<D> Render for ListState<D>
572where
573 D: ListDelegate,
574{
575 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
576 self.prepare_items_if_needed(window, cx);
577
578 if let Some((ix, strategy)) = self.deferred_scroll_to_index.take() {
580 if let Some(item_ix) = self.rows_cache.position_of(&ix) {
581 self.scroll_handle.scroll_to_item(item_ix, strategy);
582 }
583 }
584
585 let loading = self.delegate().loading(cx);
586 let query_input = if self.searchable {
587 if let Some(placeholder) = &self.options.search_placeholder {
589 self.query_input.update(cx, |input, cx| {
590 input.set_placeholder(placeholder.clone(), window, cx);
591 });
592 }
593 Some(self.query_input.clone())
594 } else {
595 None
596 };
597
598 let loading_view = if loading {
599 Some(self.delegate.render_loading(window, cx).into_any_element())
600 } else {
601 None
602 };
603 let initial_view = if let Some(input) = &query_input {
604 if input.read(cx).value().is_empty() {
605 self.delegate.render_initial(window, cx)
606 } else {
607 None
608 }
609 } else {
610 None
611 };
612 let items_count = self.rows_cache.items_count();
613 let entities_count = self.rows_cache.len();
614 let mouse_right_clicked_index = self.mouse_right_clicked_index;
615
616 v_flex()
617 .key_context("List")
618 .id("list-state")
619 .track_focus(&self.focus_handle)
620 .size_full()
621 .relative()
622 .overflow_hidden()
623 .when_some(query_input, |this, input| {
624 this.child(
625 div()
626 .map(|this| match self.options.size {
627 Size::Small => this.px_1p5(),
628 _ => this.px_2(),
629 })
630 .border_b_1()
631 .border_color(cx.theme().border)
632 .child(
633 Input::new(&input)
634 .with_size(self.options.size)
635 .prefix(
636 Icon::new(IconName::Search)
637 .text_color(cx.theme().muted_foreground),
638 )
639 .cleanable(true)
640 .p_0()
641 .appearance(false),
642 ),
643 )
644 })
645 .when(!loading, |this| {
646 this.on_action(cx.listener(Self::on_action_cancel))
647 .on_action(cx.listener(Self::on_action_confirm))
648 .on_action(cx.listener(Self::on_action_select_next))
649 .on_action(cx.listener(Self::on_action_select_prev))
650 .map(|this| {
651 if let Some(view) = initial_view {
652 this.child(view)
653 } else {
654 this.child(self.render_items(items_count, entities_count, window, cx))
655 }
656 })
657 .when(mouse_right_clicked_index.is_some(), |this| {
659 this.on_mouse_down_out(cx.listener(|this, _, _, cx| {
660 this.mouse_right_clicked_index = None;
661 cx.notify();
662 }))
663 })
664 })
665 .children(loading_view)
666 }
667}
668
669#[derive(IntoElement)]
671pub struct List<D: ListDelegate + 'static> {
672 state: Entity<ListState<D>>,
673 style: StyleRefinement,
674 options: ListOptions,
675}
676
677impl<D> List<D>
678where
679 D: ListDelegate + 'static,
680{
681 pub fn new(state: &Entity<ListState<D>>) -> Self {
683 Self {
684 state: state.clone(),
685 style: StyleRefinement::default(),
686 options: ListOptions::default(),
687 }
688 }
689
690 pub fn scrollbar_visible(mut self, visible: bool) -> Self {
692 self.options.scrollbar_visible = visible;
693 self
694 }
695
696 pub fn search_placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
698 self.options.search_placeholder = Some(placeholder.into());
699 self
700 }
701}
702
703impl<D> Styled for List<D>
704where
705 D: ListDelegate + 'static,
706{
707 fn style(&mut self) -> &mut StyleRefinement {
708 &mut self.style
709 }
710}
711
712impl<D> Sizable for List<D>
713where
714 D: ListDelegate + 'static,
715{
716 fn with_size(mut self, size: impl Into<Size>) -> Self {
717 self.options.size = size.into();
718 self
719 }
720}
721
722impl<D> RenderOnce for List<D>
723where
724 D: ListDelegate + 'static,
725{
726 fn render(mut self, _: &mut Window, cx: &mut App) -> impl IntoElement {
727 self.options.paddings = self.style.padding.clone();
730 self.options.max_height = self.style.max_size.height;
731 self.style.padding = EdgesRefinement::default();
732 self.style.max_size.height = None;
733
734 self.state.update(cx, |state, _| {
735 state.options = self.options;
736 });
737
738 div()
739 .id("list")
740 .size_full()
741 .refine_style(&self.style)
742 .child(self.state.clone())
743 }
744}