VirtualList
VirtualList is a high-performance component designed for efficiently rendering large datasets by only rendering visible items. Unlike uniform lists, VirtualList supports variable item sizes, making it perfect for complex layouts like tables with different row heights or dynamic content.
#Import
use gpui_kit::component::{
v_virtual_list, h_virtual_list, VirtualListScrollHandle,
scroll::{Scrollbar, ScrollbarState, ScrollbarAxis},
};
use std::rc::Rc;
use gpui_kit::{px, size, ScrollStrategy, Size, Pixels};// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { VirtualRow, VirtualList } from "virtual-list.slint";use iced::widget::{button, column, container, mouse_area, row, svg, text};
use iced::{Alignment, Border, Color, Element, Length, Shadow, Task, Theme};#Usage
#Basic Vertical Virtual List
use std::rc::Rc;
use gpui_kit::{px, size, Size, Pixels};
pub struct ListViewExample {
items: Vec<String>,
item_sizes: Rc<Vec<Size<Pixels>>>,
scroll_handle: VirtualListScrollHandle,
}
impl ListViewExample {
fn new(cx: &mut Context<Self>) -> Self {
let items = (0..5000).map(|i| format!("Item {}", i)).collect::<Vec<_>>();
let item_sizes = Rc::new(items.iter().map(|_| size(px(200.), px(30.))).collect());
Self {
items,
item_sizes,
scroll_handle: VirtualListScrollHandle::new(),
}
}
}
impl Render for ListViewExample {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_virtual_list(
cx.entity().clone(),
"my-list",
self.item_sizes.clone(),
|view, visible_range, _, cx| {
visible_range
.map(|ix| {
div()
.h(px(30.))
.w_full()
.bg(cx.theme().secondary)
.child(format!("Item {}", ix))
})
.collect()
},
)
.track_scroll(&self.scroll_handle)
}
}VirtualList {
height: 240px;
items: [
{ label: "Row 1" },
{ label: "Row 2" },
{ label: "Row 3" },
{ label: "Row 4" },
{ label: "Row 5" },
{ label: "Row 6" },
{ label: "Row 7" },
{ label: "Row 8" },
{ label: "Row 9" },
{ label: "Row 10" },
{ label: "Row 11" },
{ label: "Row 12" },
{ label: "Row 13" },
{ label: "Row 14" },
{ label: "Row 15" },
{ label: "Row 16" },
{ label: "Row 17" },
{ label: "Row 18" },
{ label: "Row 19" },
{ label: "Row 20" },
{ label: "Row 21" },
{ label: "Row 22" },
{ label: "Row 23" },
{ label: "Row 24" },
{ label: "Row 25" },
{ label: "Row 26" },
{ label: "Row 27" },
{ label: "Row 28" },
{ label: "Row 29" },
{ label: "Row 30" },
{ label: "Row 31" },
{ label: "Row 32" },
{ label: "Row 33" },
{ label: "Row 34" },
{ label: "Row 35" },
{ label: "Row 36" },
{ label: "Row 37" },
{ label: "Row 38" },
{ label: "Row 39" },
{ label: "Row 40" },
];
selected: 2;
}// 5000 rows, every one a real index the render function can be asked for, and
// about a dozen of them built. The list owns its 240px height so it knows how
// many rows fill it, the app holds the offset it reports, and the window is
// the rows that offset covers. The line above the list is `VirtualList::range`
// read back, which is the same range the render function was called for.
let basic = VirtualList::new(BASIC_ROWS, |index, selected| {
striped_row(
index,
format!("Row {}", index + 1),
selected,
Message::SelectBasic(index),
)
})
.row_height(ROW_HEIGHT)
.height(BASIC_HEIGHT)
.offset(app.basic_offset)
.selected(app.basic_selected)
.on_scroll(|viewport| Message::BasicViewport(viewport.absolute_offset().y));
let basic_range = basic.range();
let basic_status = range_line(format!(
"rows {}–{} of {} built ({})",
basic_range.start + 1,
basic_range.end,
BASIC_ROWS,
basic_range.end - basic_range.start
));
let basic_section = column![basic_status, basic]
.spacing(theme::SPACING_SM)
.width(Length::Fill);#Horizontal Virtual List
h_virtual_list(
cx.entity().clone(),
"horizontal-list",
item_sizes.clone(),
|view, visible_range, _, cx| {
visible_range
.map(|ix| {
div()
.w(px(120.)) // Width is used for horizontal lists
.h_full()
.bg(cx.theme().accent)
.child(format!("Card {}", ix))
})
.collect()
},
)
.track_scroll(&scroll_handle)SlintNo Slint version of this example yet.
#Variable Item Sizes
GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// The Slint example's third list: one taller row, a 44px milestone band,
// rather than a size per item. This port has the same single-row-height
// component the Slint file has, so this is what that heading draws; the GPUI
// original's per-item sizes are named in the component's module docs instead
// of a uniform band that pretends to be one.
let tall = VirtualList::new(MILESTONES.len(), |index, selected| {
striped_row(
index,
MILESTONES[index].to_string(),
selected,
Message::SelectTall(index),
)
})
.row_height(TALL_ROW_HEIGHT)
.height(TALL_HEIGHT)
.offset(app.tall_offset)
.selected(app.tall_selected)
.on_scroll(|viewport| Message::TallViewport(viewport.absolute_offset().y));VirtualList excels at handling items with different sizes:
let item_sizes = Rc::new(
(0..1000)
.map(|i| {
// Different heights based on index
let height = if i % 5 == 0 {
px(60.) // Header items are taller
} else if i % 3 == 0 {
px(45.) // Some items are medium
} else {
px(30.) // Regular items
};
size(px(300.), height)
})
.collect::<Vec<_>>()
);
v_virtual_list(
cx.entity().clone(),
"variable-list",
item_sizes.clone(),
|view, visible_range, _, cx| {
visible_range
.map(|ix| {
let content = if ix % 5 == 0 {
format!("Header {}", ix / 5)
} else {
format!("Item {}", ix)
};
let bg_color = if ix % 5 == 0 {
cx.theme().accent
} else {
cx.theme().secondary
};
div()
.w_full()
.h(item_sizes[ix].height)
.bg(bg_color)
.flex()
.items_center()
.px_4()
.child(content)
})
.collect()
},
)VirtualList {
height: 200px;
row-height: 44px;
items: [
{ label: "Milestone: first commit" },
{ label: "Milestone: gallery online" },
{ label: "Milestone: dock ships" },
{ label: "Milestone: menu alignment" },
{ label: "Milestone: editor chrome" },
{ label: "Milestone: 76 pages" },
];
selected: 0;
}#Table-like Layout with Multiple Columns
VirtualList can render complex layouts like tables:
v_virtual_list(
cx.entity().clone(),
"table-list",
item_sizes.clone(),
|view, visible_range, _, cx| {
visible_range
.map(|row_ix| {
h_flex()
.w_full()
.h(px(40.))
.border_b_1()
.border_color(cx.theme().border)
.children(
// Multiple columns per row
(0..5).map(|col_ix| {
div()
.flex_1()
.h_full()
.px_3()
.flex()
.items_center()
.child(format!("R{}C{}", row_ix, col_ix))
})
)
})
.collect()
},
)SlintNo Slint version of this example yet.
#File Explorer with Virtual Scrolling
pub struct FileExplorer {
files: Vec<FileEntry>,
item_sizes: Rc<Vec<Size<Pixels>>>,
scroll_handle: VirtualListScrollHandle,
selected_index: Option<usize>,
}
impl FileExplorer {
fn calculate_item_heights(&mut self) {
let sizes = self.files.iter().map(|file| {
// Different heights for different file types
let height = match file.file_type {
FileType::Directory => px(40.),
FileType::Image => px(60.), // Larger for thumbnails
FileType::Document => px(35.),
_ => px(30.),
};
size(px(400.), height)
}).collect();
self.item_sizes = Rc::new(sizes);
}
}
impl Render for FileExplorer {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_virtual_list(
cx.entity().clone(),
"file-list",
self.item_sizes.clone(),
|view, visible_range, _, cx| {
visible_range
.map(|ix| {
let file = &view.files[ix];
let is_selected = view.selected_index == Some(ix);
div()
.w_full()
.h(view.item_sizes[ix].height)
.px_3()
.py_1()
.flex()
.items_center()
.gap_2()
.bg(if is_selected {
cx.theme().accent
} else {
Color::transparent()
})
.hover(|style| style.bg(cx.theme().secondary_hover))
.child(file_icon(&file.file_type))
.child(file.name.clone())
.child(
div()
.flex_1()
.text_right()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(format_file_size(file.size))
)
.on_click(cx.listener(move |view, _, _, cx| {
view.selected_index = Some(ix);
cx.notify();
}))
})
.collect()
},
)
.track_scroll(&self.scroll_handle)
}
}SlintNo Slint version of this example yet.
#Chat Messages with Auto-scroll
pub struct ChatWindow {
messages: Vec<ChatMessage>,
scroll_handle: VirtualListScrollHandle,
auto_scroll: bool,
}
impl ChatWindow {
fn add_message(&mut self, message: ChatMessage, cx: &mut Context<Self>) {
self.messages.push(message);
// Recalculate item sizes
self.update_item_sizes();
if self.auto_scroll {
// Scroll to bottom for new messages
self.scroll_handle.scroll_to_bottom();
}
cx.notify();
}
fn update_item_sizes(&mut self) {
let sizes = self.messages.iter().map(|msg| {
// Calculate height based on message content
let lines = msg.content.lines().count().max(1);
let height = px(40. + (lines.saturating_sub(1)) as f32 * 16.);
size(px(350.), height)
}).collect();
self.item_sizes = Rc::new(sizes);
}
}
impl Render for ChatWindow {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.size_full()
.child(
v_virtual_list(
cx.entity().clone(),
"chat-messages",
self.item_sizes.clone(),
|view, visible_range, _, cx| {
visible_range
.map(|ix| {
let msg = &view.messages[ix];
div()
.w_full()
.px_4()
.py_2()
.child(
v_flex()
.gap_1()
.child(
h_flex()
.justify_between()
.child(
div()
.text_sm()
.font_weight(FontWeight::SEMIBOLD)
.child(msg.author.clone())
)
.child(
div()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(format_timestamp(msg.timestamp))
)
)
.child(
div()
.text_sm()
.child(msg.content.clone())
)
)
})
.collect()
},
)
.track_scroll(&self.scroll_handle)
.flex_1()
)
.child(
// Chat input at bottom
div()
.w_full()
.h(px(60.))
.border_t_1()
.border_color(cx.theme().border)
.child("Chat input here...")
)
}
}SlintNo Slint version of this example yet.
#Data Grid with Fixed Headers
pub struct DataGrid {
headers: Vec<String>,
data: Vec<Vec<String>>,
column_widths: Vec<Pixels>,
scroll_handle: VirtualListScrollHandle,
}
impl Render for DataGrid {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.size_full()
.child(
// Fixed header
h_flex()
.w_full()
.h(px(40.))
.bg(cx.theme().secondary)
.border_b_1()
.border_color(cx.theme().border)
.children(
self.headers.iter().zip(&self.column_widths).map(|(header, &width)| {
div()
.w(width)
.h_full()
.px_3()
.flex()
.items_center()
.font_weight(FontWeight::SEMIBOLD)
.child(header.clone())
})
)
)
.child(
// Virtual list for data rows
v_virtual_list(
cx.entity().clone(),
"data-rows",
Rc::new(vec![size(px(800.), px(32.)); self.data.len()]),
|view, visible_range, _, cx| {
visible_range
.map(|row_ix| {
h_flex()
.w_full()
.h(px(32.))
.border_b_1()
.border_color(cx.theme().border.opacity(0.5))
.children(
view.data[row_ix].iter().zip(&view.column_widths).map(|(cell, &width)| {
div()
.w(width)
.h_full()
.px_3()
.flex()
.items_center()
.child(cell.clone())
})
)
})
.collect()
},
)
.track_scroll(&self.scroll_handle)
.flex_1()
)
}
}SlintNo Slint version of this example yet.
#Scroll Handling
#Basic Scroll Control
pub struct ScrollableList {
scroll_handle: VirtualListScrollHandle,
scroll_state: ScrollbarState,
}
impl Render for ScrollableList {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.relative()
.size_full()
.child(
v_virtual_list(/* ... */)
.track_scroll(&self.scroll_handle)
.p_4()
.border_1()
.border_color(cx.theme().border)
)
.child(
// Add scrollbars
div()
.absolute()
.top_0()
.left_0()
.right_0()
.bottom_0()
.child(
Scrollbar::both(&self.scroll_state, &self.scroll_handle)
.axis(ScrollbarAxis::Vertical)
)
)
}
}VerticalLayout {
spacing: 10px;
alignment: start;
HorizontalLayout {
spacing: 8px;
alignment: start;
Button {
text: "Top";
icon: "arrow-up";
clicked => {
scroll.scroll-to(0px);
}
}
Button {
text: "Row 50";
clicked => {
scroll.scroll-to-row(49, false);
}
}
Button {
text: "Center 25";
clicked => {
scroll.scroll-to-row(24, true);
}
}
Button {
text: "Bottom";
icon: "arrow-down";
clicked => {
scroll.scroll-to-row(59, false);
}
}
}
Text {
text: "Visible: \{scroll.first-visible}..\{scroll.last-visible} of \{scroll.items.length}";
color: Theme.muted-foreground;
font-size: Theme.font-size-xs;
}
scroll := VirtualList {
height: 260px;
items <=> root.scroll-items;
selected: 4;
}
}// The Slint example's control strip. Each button asks iced to scroll the named
// region — `scroll-to(0)`, `scroll-to-row(49)`, `scroll-to-row(24, center)`,
// `scroll-to-row(59)` — and the line under it reports the range the region
// came back with, the Slint `first-visible`..`last-visible`.
let scroll = VirtualList::new(SCROLL_ROWS, |index, selected| {
striped_row(
index,
format!("Row {}", index + 1),
selected,
Message::SelectScroll(index),
)
})
.row_height(ROW_HEIGHT)
.height(SCROLL_HEIGHT)
.offset(app.scroll_offset)
.selected(app.scroll_selected)
.id(SCROLL_ID)
.on_scroll(|viewport| Message::ScrollViewport(viewport.absolute_offset().y));
let scroll_range = scroll.range();
let scroll_controls = row![
Button::new("Top")
.icon(arrow(ARROW_UP))
.on_press(Message::ScrollTo(0.0)),
Button::new("Row 50").on_press(Message::ScrollTo(virtual_list::row_offset(
49,
SCROLL_ROWS,
ROW_HEIGHT,
SCROLL_HEIGHT,
))),
Button::new("Center 25").on_press(Message::ScrollTo(virtual_list::centered_row_offset(
24,
SCROLL_ROWS,
ROW_HEIGHT,
SCROLL_HEIGHT,
))),
Button::new("Bottom")
.icon(arrow(ARROW_DOWN))
.on_press(Message::ScrollTo(virtual_list::row_offset(
59,
SCROLL_ROWS,
ROW_HEIGHT,
SCROLL_HEIGHT,
))),
]
.spacing(theme::SPACING_SM);
let scroll_status = range_line(format!(
"visible: {}..{} of {}",
scroll_range.start, scroll_range.end, SCROLL_ROWS
));
let scroll_section = column![scroll_controls, scroll_status, scroll]
.spacing(theme::SPACING_SM)
.width(Length::Fill);#Programmatic Scrolling
impl ScrollableList {
// Scroll to specific item
fn scroll_to_item(&self, index: usize) {
self.scroll_handle.scroll_to_item(index, ScrollStrategy::Top);
}
// Center item in view
fn center_item(&self, index: usize) {
self.scroll_handle.scroll_to_item(index, ScrollStrategy::Center);
}
// Scroll to bottom
fn scroll_to_bottom(&self) {
self.scroll_handle.scroll_to_bottom();
}
// Get current scroll position
fn get_scroll_offset(&self) -> Point<Pixels> {
self.scroll_handle.offset()
}
// Set scroll position manually
fn set_scroll_position(&self, offset: Point<Pixels>) {
self.scroll_handle.set_offset(offset);
}
}SlintNo Slint version of this example yet.
#Both Axis Scrolling
For content that scrolls in both directions:
v_virtual_list(
cx.entity().clone(),
"both-axis",
item_sizes.clone(),
|view, visible_range, _, cx| {
visible_range
.map(|ix| {
// Wide content that requires horizontal scrolling
h_flex()
.gap_2()
.children((0..20).map(|col| {
div()
.min_w(px(100.))
.h(px(30.))
.bg(cx.theme().secondary)
.child(format!("R{}C{}", ix, col))
}))
})
.collect()
},
)
.track_scroll(&scroll_handle)
.child(
Scrollbar::both(&scroll_state, &scroll_handle)
.axis(ScrollbarAxis::Both)
)SlintNo Slint version of this example yet.
#Performance Optimization
#Efficient Item Rendering
Only visible items are rendered, making VirtualList highly performant:
// The render function is only called for visible items
v_virtual_list(
cx.entity().clone(),
"efficient-list",
item_sizes.clone(),
|view, visible_range, _, cx| {
// visible_range contains only the items currently visible
// This typically contains 10-20 items, not all 10,000
println!("Rendering {} items out of {}",
visible_range.len(),
view.total_items);
visible_range
.map(|ix| {
// Complex rendering logic here
// Only executed for visible items
expensive_item_renderer(ix, cx)
})
.collect()
},
)SlintNo Slint version of this example yet.
#Memory Management
VirtualList automatically manages memory by:
- Only rendering visible items
- Reusing rendered elements when scrolling
- Calculating precise visible ranges
// Large dataset - only visible items use memory
let large_dataset = (0..1_000_000).map(|i| format!("Item {}", i)).collect();
// Memory usage remains constant regardless of dataset size
v_virtual_list(/* render only visible items */)SlintNo Slint version of this example yet.
#Variable Heights with Caching
For dynamic content with calculated heights:
struct DynamicItem {
content: String,
calculated_height: Option<Pixels>,
}
impl MyView {
fn calculate_item_size(&mut self, ix: usize) -> Size<Pixels> {
if let Some(height) = self.items[ix].calculated_height {
return size(px(300.), height);
}
// Calculate height based on content
let content_lines = self.items[ix].content.lines().count();
let height = px(20. + content_lines as f32 * 16.);
// Cache the calculated height
self.items[ix].calculated_height = Some(height);
size(px(300.), height)
}
}SlintNo Slint version of this example yet.
#Best Practices
- Item Sizing: Pre-calculate item sizes when possible for best performance
- Memory Management: Use VirtualList for any list with >50 items
- Scroll Performance: Avoid heavy computations in render functions
- State Management: Keep item state separate from rendering logic
- Error Handling: Handle edge cases like empty lists gracefully
- Testing: Test with various data sizes and scroll positions
#Performance Tips
- Pre-calculate Sizes: Calculate item sizes upfront rather than during render
- Minimize Re-renders: Use stable item keys and avoid recreating render functions
- Batch Updates: Group multiple data changes together
- Efficient Rendering: Keep item render functions lightweight
- Memory Monitoring: Monitor memory usage with very large datasets