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::list::ListDelegate;
8use crate::{
9 input::{InputEvent, TextInput},
10 scroll::{Scrollbar, ScrollbarState},
11 v_flex, ActiveTheme, IconName, Size,
12};
13use crate::{
14 v_virtual_list, Icon, IndexPath, Selectable, Sizable as _, StyledExt, VirtualListScrollHandle,
15};
16use gpui::{
17 div, prelude::FluentBuilder, AppContext, Entity, FocusHandle, Focusable, InteractiveElement,
18 IntoElement, KeyBinding, Length, MouseButton, ParentElement, Render, Styled, Task, Window,
19};
20use gpui::{
21 px, size, App, AvailableSpace, ClickEvent, Context, Edges, EventEmitter, ListSizingBehavior,
22 Pixels, ScrollStrategy, SharedString, StatefulInteractiveElement, Subscription,
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
48pub struct List<D: ListDelegate> {
49 focus_handle: FocusHandle,
50 delegate: D,
51 max_height: Option<Length>,
52 paddings: Edges<Pixels>,
53 query_input: Option<Entity<InputState>>,
54 last_query: Option<String>,
55 selectable: bool,
56 querying: bool,
57 scrollbar_visible: bool,
58 scroll_handle: VirtualListScrollHandle,
59 scroll_state: ScrollbarState,
60 pub(crate) size: Size,
61 rows_cache: RowsCache,
62 selected_index: Option<IndexPath>,
63 item_to_measure_index: IndexPath,
64 deferred_scroll_to_index: Option<(IndexPath, ScrollStrategy)>,
65 mouse_right_clicked_index: Option<IndexPath>,
66 reset_on_cancel: bool,
67 _search_task: Task<()>,
68 _load_more_task: Task<()>,
69 _query_input_subscription: Subscription,
70}
71
72impl<D> List<D>
73where
74 D: ListDelegate,
75{
76 pub fn new(delegate: D, window: &mut Window, cx: &mut Context<Self>) -> Self {
77 let query_input =
78 cx.new(|cx| InputState::new(window, cx).placeholder(t!("List.search_placeholder")));
79
80 let _query_input_subscription =
81 cx.subscribe_in(&query_input, window, Self::on_query_input_event);
82
83 Self {
84 focus_handle: cx.focus_handle(),
85 delegate,
86 rows_cache: RowsCache::default(),
87 query_input: Some(query_input),
88 last_query: None,
89 selected_index: None,
90 item_to_measure_index: IndexPath::default(),
91 deferred_scroll_to_index: None,
92 mouse_right_clicked_index: None,
93 scroll_handle: VirtualListScrollHandle::new(),
94 scroll_state: ScrollbarState::default(),
95 max_height: None,
96 scrollbar_visible: true,
97 selectable: true,
98 querying: false,
99 size: Size::default(),
100 reset_on_cancel: true,
101 paddings: Edges::default(),
102 _search_task: Task::ready(()),
103 _load_more_task: Task::ready(()),
104 _query_input_subscription,
105 }
106 }
107
108 pub fn set_size(&mut self, size: Size, _: &mut Window, _: &mut Context<Self>) {
110 self.size = size;
111 }
112
113 pub fn max_h(mut self, height: impl Into<Length>) -> Self {
114 self.max_height = Some(height.into());
115 self
116 }
117
118 pub fn scrollbar_visible(mut self, visible: bool) -> Self {
120 self.scrollbar_visible = visible;
121 self
122 }
123
124 pub fn no_query(mut self) -> Self {
125 self.query_input = None;
126 self
127 }
128
129 pub fn selectable(mut self, selectable: bool) -> Self {
131 self.selectable = selectable;
132 self
133 }
134
135 pub fn set_query_input(
136 &mut self,
137 query_input: Entity<InputState>,
138 window: &mut Window,
139 cx: &mut Context<Self>,
140 ) {
141 self._query_input_subscription =
142 cx.subscribe_in(&query_input, window, Self::on_query_input_event);
143 self.query_input = Some(query_input);
144 }
145
146 pub fn query_input(&self) -> Option<&Entity<InputState>> {
148 self.query_input.as_ref()
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) {
160 self.focus_handle(cx).focus(window);
161 }
162
163 pub(crate) fn _set_selected_index(
166 &mut self,
167 ix: Option<IndexPath>,
168 window: &mut Window,
169 cx: &mut Context<Self>,
170 ) {
171 self.selected_index = ix;
172 self.delegate.set_selected_index(ix, window, cx);
173 self.scroll_to_selected_item(window, cx);
174 }
175
176 pub fn set_selected_index(
179 &mut self,
180 ix: Option<IndexPath>,
181 window: &mut Window,
182 cx: &mut Context<Self>,
183 ) {
184 self.selected_index = ix;
185 self.delegate.set_selected_index(ix, window, cx);
186 }
187
188 pub fn selected_index(&self) -> Option<IndexPath> {
189 self.selected_index
190 }
191
192 pub fn set_item_to_measure_index(
194 &mut self,
195 ix: IndexPath,
196 _: &mut Window,
197 cx: &mut Context<Self>,
198 ) {
199 self.item_to_measure_index = ix;
200 cx.notify();
201 }
202
203 fn render_scrollbar(&self, _: &mut Window, _: &mut Context<Self>) -> Option<impl IntoElement> {
204 if !self.scrollbar_visible {
205 return None;
206 }
207
208 Some(Scrollbar::uniform_scroll(
209 &self.scroll_state,
210 &self.scroll_handle,
211 ))
212 }
213
214 pub fn scroll_to_item(
216 &mut self,
217 ix: IndexPath,
218 strategy: ScrollStrategy,
219 _: &mut Window,
220 cx: &mut Context<Self>,
221 ) {
222 if ix.section == 0 && ix.row == 0 {
223 let mut offset = self.scroll_handle.base_handle().offset();
225 offset.y = px(0.);
226 self.scroll_handle.base_handle().set_offset(offset);
227 cx.notify();
228 return;
229 }
230 self.deferred_scroll_to_index = Some((ix, strategy));
231 cx.notify();
232 }
233
234 pub fn scroll_handle(&self) -> &VirtualListScrollHandle {
236 &self.scroll_handle
237 }
238
239 pub fn scroll_to_selected_item(&mut self, _: &mut Window, cx: &mut Context<Self>) {
240 if let Some(ix) = self.selected_index {
241 self.deferred_scroll_to_index = Some((ix, ScrollStrategy::Top));
242 cx.notify();
243 }
244 }
245
246 pub fn paddings(mut self, paddings: Edges<Pixels>) -> Self {
248 self.paddings = paddings;
249 self
250 }
251
252 fn on_query_input_event(
253 &mut self,
254 state: &Entity<InputState>,
255 event: &InputEvent,
256 window: &mut Window,
257 cx: &mut Context<Self>,
258 ) {
259 match event {
260 InputEvent::Change => {
261 let text = state.read(cx).value();
262 let text = text.trim().to_string();
263 if Some(&text) == self.last_query.as_ref() {
264 return;
265 }
266
267 self.set_querying(true, window, cx);
268 let search = self.delegate.perform_search(&text, window, cx);
269
270 if self.rows_cache.len() > 0 {
271 self._set_selected_index(Some(IndexPath::default()), window, cx);
272 } else {
273 self._set_selected_index(None, window, cx);
274 }
275
276 self._search_task = cx.spawn_in(window, async move |this, window| {
277 search.await;
278
279 _ = this.update_in(window, |this, _, _| {
280 this.scroll_handle.scroll_to_item(0, ScrollStrategy::Top);
281 this.last_query = Some(text);
282 });
283
284 Timer::after(Duration::from_millis(100)).await;
286 _ = this.update_in(window, |this, window, cx| {
287 this.set_querying(false, window, cx);
288 });
289 });
290 }
291 InputEvent::PressEnter { secondary } => self.on_action_confirm(
292 &Confirm {
293 secondary: *secondary,
294 },
295 window,
296 cx,
297 ),
298 _ => {}
299 }
300 }
301
302 fn set_querying(&mut self, querying: bool, window: &mut Window, cx: &mut Context<Self>) {
303 self.querying = querying;
304 if let Some(input) = &self.query_input {
305 input.update(cx, |input, cx| input.set_loading(querying, window, cx))
306 }
307 cx.notify();
308 }
309
310 fn load_more_if_need(
313 &mut self,
314 entities_count: usize,
315 visible_end: usize,
316 window: &mut Window,
317 cx: &mut Context<Self>,
318 ) {
319 let threshold = self.delegate.load_more_threshold();
322 if visible_end >= entities_count.saturating_sub(threshold) {
325 if !self.delegate.is_eof(cx) {
326 return;
327 }
328
329 self._load_more_task = cx.spawn_in(window, async move |view, cx| {
330 _ = view.update_in(cx, |view, window, cx| {
331 view.delegate.load_more(window, cx);
332 });
333 });
334 }
335 }
336
337 pub(crate) fn reset_on_cancel(mut self, reset: bool) -> Self {
338 self.reset_on_cancel = reset;
339 self
340 }
341
342 fn on_action_cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
343 cx.propagate();
344 if self.reset_on_cancel {
345 self._set_selected_index(None, window, cx);
346 }
347
348 self.delegate.cancel(window, cx);
349 cx.emit(ListEvent::Cancel);
350 cx.notify();
351 }
352
353 fn on_action_confirm(
354 &mut self,
355 confirm: &Confirm,
356 window: &mut Window,
357 cx: &mut Context<Self>,
358 ) {
359 if self.rows_cache.len() == 0 {
360 return;
361 }
362
363 let Some(ix) = self.selected_index else {
364 return;
365 };
366
367 self.delegate
368 .set_selected_index(self.selected_index, window, cx);
369 self.delegate.confirm(confirm.secondary, window, cx);
370 cx.emit(ListEvent::Confirm(ix));
371 cx.notify();
372 }
373
374 fn select_item(&mut self, ix: IndexPath, window: &mut Window, cx: &mut Context<Self>) {
375 self.selected_index = Some(ix);
376 self.delegate.set_selected_index(Some(ix), window, cx);
377 self.scroll_to_selected_item(window, cx);
378 cx.emit(ListEvent::Select(ix));
379 cx.notify();
380 }
381
382 pub(crate) fn on_action_select_prev(
383 &mut self,
384 _: &SelectUp,
385 window: &mut Window,
386 cx: &mut Context<Self>,
387 ) {
388 if self.rows_cache.len() == 0 {
389 return;
390 }
391
392 let prev_ix = self
393 .rows_cache
394 .prev(self.selected_index.unwrap_or(IndexPath::default()));
395 self.select_item(prev_ix, window, cx);
396 }
397
398 pub(crate) fn on_action_select_next(
399 &mut self,
400 _: &SelectDown,
401 window: &mut Window,
402 cx: &mut Context<Self>,
403 ) {
404 if self.rows_cache.len() == 0 {
405 return;
406 }
407
408 let next_ix = self
409 .rows_cache
410 .next(self.selected_index.unwrap_or_default());
411 self.select_item(next_ix, window, cx);
412 }
413
414 fn render_list_item(
415 &self,
416 ix: IndexPath,
417 window: &mut Window,
418 cx: &mut Context<Self>,
419 ) -> impl IntoElement {
420 let selected = self.selected_index.map(|s| s.eq_row(ix)).unwrap_or(false);
421 let mouse_right_clicked = self
422 .mouse_right_clicked_index
423 .map(|s| s.eq_row(ix))
424 .unwrap_or(false);
425 let id = SharedString::from(format!("list-item-{}", ix));
426
427 div()
428 .id(id)
429 .w_full()
430 .relative()
431 .children(self.delegate.render_item(ix, window, cx).map(|item| {
432 item.selected(selected)
433 .secondary_selected(mouse_right_clicked)
434 }))
435 .when(self.selectable, |this| {
436 this.on_click(cx.listener(move |this, e: &ClickEvent, window, cx| {
437 this.mouse_right_clicked_index = None;
438 this.selected_index = Some(ix);
439 this.on_action_confirm(
440 &Confirm {
441 secondary: e.modifiers().secondary(),
442 },
443 window,
444 cx,
445 );
446 }))
447 .on_mouse_down(
448 MouseButton::Right,
449 cx.listener(move |this, _, _, cx| {
450 this.mouse_right_clicked_index = Some(ix);
451 cx.notify();
452 }),
453 )
454 })
455 }
456
457 fn render_items(
458 &mut self,
459 items_count: usize,
460 entities_count: usize,
461 window: &mut Window,
462 cx: &mut Context<Self>,
463 ) -> impl IntoElement {
464 let measured_size = self.rows_cache.measured_size();
465
466 v_flex()
467 .flex_grow()
468 .relative()
469 .h_full()
470 .min_w(measured_size.item_size.width)
471 .when_some(self.max_height, |this, h| this.max_h(h))
472 .overflow_hidden()
473 .when(items_count == 0, |this| {
474 this.child(self.delegate().render_empty(window, cx))
475 })
476 .when(items_count > 0, {
477 let rows_cache = self.rows_cache.clone();
478 |this| {
479 this.child(
480 v_virtual_list(
481 cx.entity().clone(),
482 "virtual-list",
483 rows_cache.entries_sizes.clone(),
484 move |list, visible_range: Range<usize>, window, cx| {
485 list.load_more_if_need(
486 entities_count,
487 visible_range.end,
488 window,
489 cx,
490 );
491
492 visible_range
497 .map(|ix| {
498 let Some(entry) = rows_cache.get(ix) else {
499 return div();
500 };
501
502 div().children(match entry {
503 RowEntry::Entry(index) => Some(
504 list.render_list_item(index, window, cx)
505 .into_any_element(),
506 ),
507 RowEntry::SectionHeader(section_ix) => list
508 .delegate()
509 .render_section_header(section_ix, window, cx)
510 .map(|r| r.into_any_element()),
511 RowEntry::SectionFooter(section_ix) => list
512 .delegate()
513 .render_section_footer(section_ix, window, cx)
514 .map(|r| r.into_any_element()),
515 })
516 })
517 .collect::<Vec<_>>()
518 },
519 )
520 .paddings(self.paddings)
521 .when(self.max_height.is_some(), |this| {
522 this.with_sizing_behavior(ListSizingBehavior::Infer)
523 })
524 .track_scroll(&self.scroll_handle)
525 .into_any_element(),
526 )
527 }
528 })
529 .children(self.render_scrollbar(window, cx))
530 }
531
532 fn prepare_items_if_needed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
533 let sections_count = self.delegate.sections_count(cx);
534
535 let mut measured_size = MeasuredEntrySize::default();
536
537 let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
539 measured_size.item_size = self
540 .render_list_item(self.item_to_measure_index, window, cx)
541 .into_any_element()
542 .layout_as_root(available_space, window, cx);
543
544 if let Some(mut el) = self
545 .delegate
546 .render_section_header(0, window, cx)
547 .map(|r| r.into_any_element())
548 {
549 measured_size.section_header_size = el.layout_as_root(available_space, window, cx);
550 }
551 if let Some(mut el) = self
552 .delegate
553 .render_section_footer(0, window, cx)
554 .map(|r| r.into_any_element())
555 {
556 measured_size.section_footer_size = el.layout_as_root(available_space, window, cx);
557 }
558
559 self.rows_cache
560 .prepare_if_needed(sections_count, measured_size, cx, |section_ix, cx| {
561 self.delegate.items_count(section_ix, cx)
562 });
563 }
564}
565
566impl<D> Focusable for List<D>
567where
568 D: ListDelegate,
569{
570 fn focus_handle(&self, cx: &App) -> FocusHandle {
571 if let Some(query_input) = &self.query_input {
572 query_input.focus_handle(cx)
573 } else {
574 self.focus_handle.clone()
575 }
576 }
577}
578impl<D> EventEmitter<ListEvent> for List<D> where D: ListDelegate {}
579impl<D> Render for List<D>
580where
581 D: ListDelegate,
582{
583 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
584 self.prepare_items_if_needed(window, cx);
585
586 if let Some((ix, strategy)) = self.deferred_scroll_to_index.take() {
588 if let Some(item_ix) = self.rows_cache.position_of(&ix) {
589 self.scroll_handle.scroll_to_item(item_ix, strategy);
590 }
591 }
592
593 let items_count = self.rows_cache.items_count();
594 let entities_count = self.rows_cache.len();
595 let loading = self.delegate.loading(cx);
596
597 let initial_view = if let Some(input) = &self.query_input {
598 if input.read(cx).value().is_empty() {
599 self.delegate().render_initial(window, cx)
600 } else {
601 None
602 }
603 } else {
604 None
605 };
606
607 v_flex()
608 .key_context("List")
609 .id("list")
610 .track_focus(&self.focus_handle)
611 .size_full()
612 .relative()
613 .overflow_hidden()
614 .when_some(self.query_input.clone(), |this, input| {
615 this.child(
616 div()
617 .map(|this| match self.size {
618 Size::Small => this.px_1p5(),
619 _ => this.px_2(),
620 })
621 .border_b_1()
622 .border_color(cx.theme().border)
623 .child(
624 TextInput::new(&input)
625 .with_size(self.size)
626 .prefix(
627 Icon::new(IconName::Search)
628 .text_color(cx.theme().muted_foreground),
629 )
630 .cleanable()
631 .p_0()
632 .appearance(false),
633 ),
634 )
635 })
636 .when(loading, |this| {
637 this.child(self.delegate().render_loading(window, cx))
638 })
639 .when(!loading, |this| {
640 this.on_action(cx.listener(Self::on_action_cancel))
641 .on_action(cx.listener(Self::on_action_confirm))
642 .on_action(cx.listener(Self::on_action_select_next))
643 .on_action(cx.listener(Self::on_action_select_prev))
644 .map(|this| {
645 if let Some(view) = initial_view {
646 this.child(view)
647 } else {
648 this.child(self.render_items(items_count, entities_count, window, cx))
649 }
650 })
651 .when(self.mouse_right_clicked_index.is_some(), |this| {
653 this.on_mouse_down_out(cx.listener(|this, _, _, cx| {
654 this.mouse_right_clicked_index = None;
655 cx.notify();
656 }))
657 })
658 })
659 }
660}