Skip to content

List

A powerful List component that provides a virtualized, searchable list interface with support for sections, headers, footers, selection states, and infinite scrolling. The component is built on a delegate pattern that allows for flexible data management and custom item rendering.

Import

rust
use gpui_component::list::{List, ListDelegate, ListItem, ListEvent, ListSeparatorItem};
use gpui_component::IndexPath;

Usage

Basic List

To create a list, you need to implement the ListDelegate trait for your data:

rust
struct MyListDelegate {
    items: Vec<String>,
    selected_index: Option<IndexPath>,
}

impl ListDelegate for MyListDelegate {
    type Item = ListItem;

    fn items_count(&self, _section: usize, _cx: &App) -> usize {
        self.items.len()
    }

    fn render_item(
        &self,
        ix: IndexPath,
        _window: &mut Window,
        _cx: &mut Context<List<Self>>,
    ) -> Option<Self::Item> {
        self.items.get(ix.row).map(|item| {
            ListItem::new(ix)
                .child(Label::new(item.clone()))
                .selected(Some(ix) == self.selected_index)
        })
    }

    fn set_selected_index(
        &mut self,
        ix: Option<IndexPath>,
        _window: &mut Window,
        cx: &mut Context<List<Self>>,
    ) {
        self.selected_index = ix;
        cx.notify();
    }
}

// Create the list
let delegate = MyListDelegate {
    items: vec!["Item 1".into(), "Item 2".into(), "Item 3".into()],
    selected_index: None,
};

let list = cx.new(|cx| List::new(delegate, window, cx));

List with Sections

rust
impl ListDelegate for MyListDelegate {
    type Item = ListItem;

    fn sections_count(&self, _cx: &App) -> usize {
        3 // Number of sections
    }

    fn items_count(&self, section: usize, _cx: &App) -> usize {
        match section {
            0 => 5,
            1 => 3,
            2 => 7,
            _ => 0,
        }
    }

    fn render_section_header(
        &self,
        section: usize,
        _window: &mut Window,
        cx: &mut Context<List<Self>>,
    ) -> Option<impl IntoElement> {
        let title = match section {
            0 => "Section 1",
            1 => "Section 2",
            2 => "Section 3",
            _ => return None,
        };

        Some(
            h_flex()
                .px_2()
                .py_1()
                .gap_2()
                .text_sm()
                .text_color(cx.theme().muted_foreground)
                .child(Icon::new(IconName::Folder))
                .child(title)
        )
    }

    fn render_section_footer(
        &self,
        section: usize,
        _window: &mut Window,
        cx: &mut Context<List<Self>>,
    ) -> Option<impl IntoElement> {
        Some(
            div()
                .px_2()
                .py_1()
                .text_xs()
                .text_color(cx.theme().muted_foreground)
                .child(format!("End of section {}", section + 1))
        )
    }
}

List Items with Icons and Actions

rust
fn render_item(
    &self,
    ix: IndexPath,
    _window: &mut Window,
    cx: &mut Context<List<Self>>,
) -> Option<Self::Item> {
    self.items.get(ix.row).map(|item| {
        ListItem::new(ix)
            .child(
                h_flex()
                    .items_center()
                    .gap_2()
                    .child(Icon::new(IconName::File))
                    .child(Label::new(item.title.clone()))
            )
            .suffix(|_, _| {
                Button::new("action")
                    .ghost()
                    .small()
                    .icon(IconName::MoreHorizontal)
            })
            .selected(Some(ix) == self.selected_index)
            .on_click(cx.listener(move |this, _, window, cx| {
                this.delegate_mut().select_item(ix, window, cx);
            }))
    })
}

The list automatically includes a search input by default. Implement perform_search to handle queries:

rust
impl ListDelegate for MyListDelegate {
    fn perform_search(
        &mut self,
        query: &str,
        _window: &mut Window,
        _cx: &mut Context<List<Self>>,
    ) -> Task<()> {
        // Filter items based on query
        self.filtered_items = self.all_items
            .iter()
            .filter(|item| item.to_lowercase().contains(&query.to_lowercase()))
            .cloned()
            .collect();

        Task::ready(())
    }
}

// Create list without search input
let list = cx.new(|cx| List::new(delegate, window, cx).no_query());

List with Loading State

rust
impl ListDelegate for MyListDelegate {
    fn loading(&self, _cx: &App) -> bool {
        self.is_loading
    }

    fn render_loading(
        &self,
        _window: &mut Window,
        _cx: &mut Context<List<Self>>,
    ) -> impl IntoElement {
        // Custom loading view
        v_flex()
            .justify_center()
            .items_center()
            .py_4()
            .child(Skeleton::new().h_4().w_full())
            .child(Skeleton::new().h_4().w_3_4())
    }
}

Infinite Scrolling

rust
impl ListDelegate for MyListDelegate {
    fn is_eof(&self, _cx: &App) -> bool {
        !self.has_more_data
    }

    fn load_more_threshold(&self) -> usize {
        20 // Trigger when 20 items from bottom
    }

    fn load_more(&mut self, window: &mut Window, cx: &mut Context<List<Self>>) {
        if self.is_loading {
            return;
        }

        self.is_loading = true;
        cx.spawn_in(window, async move |view, window| {
            // Simulate API call
            Timer::after(Duration::from_secs(1)).await;

            view.update_in(window, |view, _, cx| {
                // Add more items
                view.delegate_mut().load_more_items();
                view.delegate_mut().is_loading = false;
                cx.notify();
            });
        }).detach();
    }
}

List Events

rust
// Subscribe to list events
let _subscription = cx.subscribe(&list, |_, _, event: &ListEvent, _| {
    match event {
        ListEvent::Select(ix) => {
            println!("Item selected at: {:?}", ix);
        }
        ListEvent::Confirm(ix) => {
            println!("Item confirmed at: {:?}", ix);
        }
        ListEvent::Cancel => {
            println!("Selection cancelled");
        }
    }
});

Different Item Styles

rust
// Basic item with hover effect
ListItem::new(ix)
    .child(Label::new("Basic Item"))
    .selected(is_selected)

// Item with check icon
ListItem::new(ix)
    .child(Label::new("Checkable Item"))
    .check_icon(IconName::Check)
    .confirmed(is_confirmed)

// Disabled item
ListItem::new(ix)
    .child(Label::new("Disabled Item"))
    .disabled(true)

// Separator item
ListSeparatorItem::new()
    .child(
        div()
            .h_px()
            .w_full()
            .bg(cx.theme().border)
    )

Custom Empty State

rust
impl ListDelegate for MyListDelegate {
    fn render_empty(&self, _window: &mut Window, cx: &mut Context<List<Self>>) -> impl IntoElement {
        v_flex()
            .size_full()
            .justify_center()
            .items_center()
            .gap_2()
            .child(Icon::new(IconName::Search).size_16().text_color(cx.theme().muted_foreground))
            .child(
                Label::new("No items found")
                    .text_color(cx.theme().muted_foreground)
            )
            .child(
                Label::new("Try adjusting your search terms")
                    .text_sm()
                    .text_color(cx.theme().muted_foreground.opacity(0.7))
            )
    }
}

Configuration Options

List Configuration

rust
List::new(delegate, window, cx)
    .max_h(px(400.))                    // Set maximum height
    .scrollbar_visible(false)           // Hide scrollbar
    .selectable(false)                  // Disable selection
    .no_query()                         // Remove search input
    .paddings(Edges::all(px(8.)))       // Set internal padding

Scrolling Control

rust
// Scroll to specific item
list.update(cx, |list, cx| {
    list.scroll_to_item(
        IndexPath::new(0).section(1),  // Row 0 of section 1
        ScrollStrategy::Center,
        window,
        cx,
    );
});

// Scroll to selected item
list.update(cx, |list, cx| {
    list.scroll_to_selected_item(window, cx);
});

// Set selected index without scrolling
list.update(cx, |list, cx| {
    list.set_selected_index(Some(IndexPath::new(5)), window, cx);
});

API Reference

List

MethodDescription
new(delegate, window, cx)Create a new list with delegate
max_h(height)Set maximum height
scrollbar_visible(bool)Show/hide scrollbar
no_query()Remove search input
selectable(bool)Enable/disable selection
paddings(edges)Set internal padding
set_selected_index(ix, window, cx)Set selected item
scroll_to_item(ix, strategy, window, cx)Scroll to specific item
scroll_to_selected_item(window, cx)Scroll to selected item

ListDelegate

MethodDescription
sections_count(cx)Number of sections (default: 1)
items_count(section, cx)Number of items in section
render_item(ix, window, cx)Render list item
render_section_header(section, window, cx)Render section header
render_section_footer(section, window, cx)Render section footer
render_empty(window, cx)Render empty state
render_initial(window, cx)Render initial state
render_loading(window, cx)Render loading state
perform_search(query, window, cx)Handle search query
set_selected_index(ix, window, cx)Update selection
confirm(secondary, window, cx)Handle item confirmation
cancel(window, cx)Handle selection cancel
loading(cx)Return loading state
is_eof(cx)Return if more data available
load_more_threshold()Threshold for triggering load more
load_more(window, cx)Load more data

ListItem

MethodDescription
new(id)Create new list item
selected(bool)Set selected state
confirmed(bool)Set confirmed state (shows check)
disabled(bool)Set disabled state
check_icon(icon)Set check icon
suffix(fn)Add suffix element
on_click(fn)Click handler
on_mouse_enter(fn)Mouse enter handler

ListEvent

VariantDescription
Select(IndexPath)Item was selected
Confirm(IndexPath)Item was confirmed (clicked/Enter)
CancelSelection was cancelled (Escape)

IndexPath

MethodDescription
new(row)Create new index path
section(section)Set section index
rowRow index
sectionSection index

Examples

File Browser List

rust
struct FileBrowserDelegate {
    files: Vec<FileInfo>,
    selected: Option<IndexPath>,
}

#[derive(Clone)]
struct FileInfo {
    name: String,
    is_directory: bool,
    size: Option<u64>,
}

impl ListDelegate for FileBrowserDelegate {
    type Item = ListItem;

    fn render_item(&self, ix: IndexPath, window: &mut Window, cx: &mut Context<List<Self>>) -> Option<Self::Item> {
        self.files.get(ix.row).map(|file| {
            let icon = if file.is_directory {
                IconName::Folder
            } else {
                IconName::File
            };

            ListItem::new(ix)
                .child(
                    h_flex()
                        .items_center()
                        .justify_between()
                        .w_full()
                        .child(
                            h_flex()
                                .items_center()
                                .gap_2()
                                .child(Icon::new(icon))
                                .child(Label::new(file.name.clone()))
                        )
                        .when_some(file.size, |this, size| {
                            this.child(
                                Label::new(format_size(size))
                                    .text_sm()
                                    .text_color(cx.theme().muted_foreground)
                            )
                        })
                )
                .selected(Some(ix) == self.selected)
        })
    }
}

Contact List with Sections

rust
struct ContactListDelegate {
    contacts_by_letter: BTreeMap<char, Vec<Contact>>,
    selected: Option<IndexPath>,
}

impl ListDelegate for ContactListDelegate {
    type Item = ListItem;

    fn sections_count(&self, _cx: &App) -> usize {
        self.contacts_by_letter.len()
    }

    fn render_section_header(&self, section: usize, _window: &mut Window, cx: &mut Context<List<Self>>) -> Option<impl IntoElement> {
        let letter = self.contacts_by_letter.keys().nth(section)?;

        Some(
            div()
                .px_3()
                .py_2()
                .bg(cx.theme().background)
                .border_b_1()
                .border_color(cx.theme().border)
                .child(
                    Label::new(letter.to_string())
                        .text_lg()
                        .text_color(cx.theme().accent_foreground)
                        .font_weight(FontWeight::BOLD)
                )
        )
    }
}

Accessibility

  • Keyboard Navigation: Use arrow keys to navigate items, Enter to confirm, Escape to cancel
  • Focus Management: Proper focus indication and management
  • Screen Reader Support: Semantic markup and ARIA attributes
  • Search: Built-in search functionality with keyboard shortcuts
  • Selection States: Clear visual and programmatic indication of selection
  • Loading States: Accessible loading indicators and announcements

Performance

  • Virtualization: Only renders visible items for large datasets
  • Efficient Updates: Optimized re-rendering with proper change detection
  • Memory Management: Automatic cleanup of off-screen items
  • Smooth Scrolling: Hardware-accelerated scrolling with momentum
  • Lazy Loading: Built-in support for infinite scrolling and pagination