1use std::ops::Range;
2use std::time::Duration;
3
4use crate::actions::{Cancel, Confirm, SelectNext, SelectPrev};
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, Context, Edges, EventEmitter, ListSizingBehavior,
22 MouseDownEvent, Pixels, ScrollStrategy, 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", SelectPrev, context),
34 KeyBinding::new("down", SelectNext, 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 _: &SelectPrev,
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 _: &SelectNext,
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
426 div()
427 .id("list-item")
428 .w_full()
429 .relative()
430 .children(self.delegate.render_item(ix, window, cx).map(|item| {
431 item.selected(selected)
432 .secondary_selected(mouse_right_clicked)
433 }))
434 .when(self.selectable, |this| {
435 this.on_mouse_down(
436 MouseButton::Left,
437 cx.listener(move |this, ev: &MouseDownEvent, window, cx| {
438 this.mouse_right_clicked_index = None;
439 this.selected_index = Some(ix);
440 this.on_action_confirm(
441 &Confirm {
442 secondary: ev.modifiers.secondary(),
443 },
444 window,
445 cx,
446 );
447 }),
448 )
449 .on_mouse_down(
450 MouseButton::Right,
451 cx.listener(move |this, _, _, cx| {
452 this.mouse_right_clicked_index = Some(ix);
453 cx.notify();
454 }),
455 )
456 })
457 }
458
459 fn render_items(
460 &mut self,
461 items_count: usize,
462 entities_count: usize,
463 window: &mut Window,
464 cx: &mut Context<Self>,
465 ) -> impl IntoElement {
466 let measured_size = self.rows_cache.measured_size();
467
468 v_flex()
469 .flex_grow()
470 .relative()
471 .h_full()
472 .min_w(measured_size.item_size.width)
473 .when_some(self.max_height, |this, h| this.max_h(h))
474 .overflow_hidden()
475 .when(items_count == 0, |this| {
476 this.child(self.delegate().render_empty(window, cx))
477 })
478 .when(items_count > 0, {
479 let rows_cache = self.rows_cache.clone();
480 |this| {
481 this.child(
482 v_virtual_list(
483 cx.entity().clone(),
484 "virtual-list",
485 rows_cache.entries_sizes.clone(),
486 move |list, visible_range: Range<usize>, window, cx| {
487 list.load_more_if_need(
488 entities_count,
489 visible_range.end,
490 window,
491 cx,
492 );
493
494 visible_range
499 .map(|ix| {
500 let Some(entry) = rows_cache.get(ix) else {
501 return div();
502 };
503
504 div().children(match entry {
505 RowEntry::Entry(index) => Some(
506 list.render_list_item(index, window, cx)
507 .into_any_element(),
508 ),
509 RowEntry::SectionHeader(section_ix) => list
510 .delegate()
511 .render_section_header(section_ix, window, cx)
512 .map(|r| r.into_any_element()),
513 RowEntry::SectionFooter(section_ix) => list
514 .delegate()
515 .render_section_footer(section_ix, window, cx)
516 .map(|r| r.into_any_element()),
517 })
518 })
519 .collect::<Vec<_>>()
520 },
521 )
522 .paddings(self.paddings)
523 .when(self.max_height.is_some(), |this| {
524 this.with_sizing_behavior(ListSizingBehavior::Infer)
525 })
526 .track_scroll(&self.scroll_handle)
527 .into_any_element(),
528 )
529 }
530 })
531 .children(self.render_scrollbar(window, cx))
532 }
533
534 fn prepare_items_if_needed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
535 let sections_count = self.delegate.sections_count(cx);
536
537 let mut measured_size = MeasuredEntrySize::default();
538
539 let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
541 measured_size.item_size = self
542 .render_list_item(self.item_to_measure_index, window, cx)
543 .into_any_element()
544 .layout_as_root(available_space, window, cx);
545
546 if let Some(mut el) = self
547 .delegate
548 .render_section_header(0, window, cx)
549 .map(|r| r.into_any_element())
550 {
551 measured_size.section_header_size = el.layout_as_root(available_space, window, cx);
552 }
553 if let Some(mut el) = self
554 .delegate
555 .render_section_footer(0, window, cx)
556 .map(|r| r.into_any_element())
557 {
558 measured_size.section_footer_size = el.layout_as_root(available_space, window, cx);
559 }
560
561 self.rows_cache
562 .prepare_if_needed(sections_count, measured_size, cx, |section_ix, cx| {
563 self.delegate.items_count(section_ix, cx)
564 });
565 }
566}
567
568impl<D> Focusable for List<D>
569where
570 D: ListDelegate,
571{
572 fn focus_handle(&self, cx: &App) -> FocusHandle {
573 if let Some(query_input) = &self.query_input {
574 query_input.focus_handle(cx)
575 } else {
576 self.focus_handle.clone()
577 }
578 }
579}
580impl<D> EventEmitter<ListEvent> for List<D> where D: ListDelegate {}
581impl<D> Render for List<D>
582where
583 D: ListDelegate,
584{
585 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
586 self.prepare_items_if_needed(window, cx);
587
588 if let Some((ix, strategy)) = self.deferred_scroll_to_index.take() {
590 if let Some(item_ix) = self.rows_cache.position_of(&ix) {
591 self.scroll_handle.scroll_to_item(item_ix, strategy);
592 }
593 }
594
595 let items_count = self.rows_cache.items_count();
596 let entities_count = self.rows_cache.len();
597 let loading = self.delegate.loading(cx);
598
599 let initial_view = if let Some(input) = &self.query_input {
600 if input.read(cx).value().is_empty() {
601 self.delegate().render_initial(window, cx)
602 } else {
603 None
604 }
605 } else {
606 None
607 };
608
609 v_flex()
610 .key_context("List")
611 .id("list")
612 .track_focus(&self.focus_handle)
613 .size_full()
614 .relative()
615 .overflow_hidden()
616 .when_some(self.query_input.clone(), |this, input| {
617 this.child(
618 div()
619 .map(|this| match self.size {
620 Size::Small => this.px_1p5(),
621 _ => this.px_2(),
622 })
623 .border_b_1()
624 .border_color(cx.theme().border)
625 .child(
626 TextInput::new(&input)
627 .with_size(self.size)
628 .prefix(
629 Icon::new(IconName::Search)
630 .text_color(cx.theme().muted_foreground),
631 )
632 .cleanable()
633 .p_0()
634 .appearance(false),
635 ),
636 )
637 })
638 .when(loading, |this| {
639 this.child(self.delegate().render_loading(window, cx))
640 })
641 .when(!loading, |this| {
642 this.on_action(cx.listener(Self::on_action_cancel))
643 .on_action(cx.listener(Self::on_action_confirm))
644 .on_action(cx.listener(Self::on_action_select_next))
645 .on_action(cx.listener(Self::on_action_select_prev))
646 .map(|this| {
647 if let Some(view) = initial_view {
648 this.child(view)
649 } else {
650 this.child(self.render_items(items_count, entities_count, window, cx))
651 }
652 })
653 .when(self.mouse_right_clicked_index.is_some(), |this| {
655 this.on_mouse_down_out(cx.listener(|this, _, _, cx| {
656 this.mouse_right_clicked_index = None;
657 cx.notify();
658 }))
659 })
660 })
661 }
662}