-
Notifications
You must be signed in to change notification settings - Fork 9
Guide Recipes VirtualList Search
MeowLynxSea edited this page Jan 24, 2026
·
3 revisions
This recipe shows the common “search box + virtualized results” pattern.
Goal:
- A search input with submit + clear
- A result list that stays smooth for large datasets
- Stable row identity and correct spacing under virtualization
-
SearchInputfor query entry -
VirtualListwidget for scrolling + virtualization -
VirtualRowfor row identity and row-local namespace -
ListItemfor standardized row layout
use gpui::{AnyElement, ListAlignment, SharedString, div, px};
use yororen_ui::{
component::{label, list_item, search_input, virtual_row},
widget::{virtual_list, virtual_list_state, VirtualListController},
};
struct SearchView {
list_state: gpui::ListState,
controller: VirtualListController,
items: Vec<String>,
}
impl SearchView {
fn new(cx: &mut gpui::App) -> Self {
let list_state = virtual_list_state(0, ListAlignment::Top, px(256.));
let controller = VirtualListController::new(list_state.clone());
Self { list_state, controller, items: Vec::new() }
}
fn render(&mut self, window: &mut gpui::Window, cx: &mut gpui::Context<Self>) -> impl gpui::IntoElement {
let query = window.use_keyed_state("recipe:search:query", cx, |_, _| {
SharedString::new_static("")
});
let query_value = query.read(cx).to_string().to_lowercase();
let filtered = if query_value.is_empty() {
self.items.clone()
} else {
self.items
.iter()
.filter(|s| s.to_lowercase().contains(&query_value))
.cloned()
.collect::<Vec<_>>()
};
let last_len = window.use_keyed_state("recipe:search:last-len", cx, |_, _| 0usize);
if *last_len.read(cx) != filtered.len() {
self.controller.reset(filtered.len());
last_len.update(cx, |state, _| *state = filtered.len());
}
let controller = self.controller.clone();
let filtered_for_rows = filtered.clone();
let items_for_count = self.items.clone();
let query_for_change = query.clone();
div()
.flex()
.flex_col()
.gap_3()
.child(
search_input()
.id(("demo", "search"))
.placeholder("Search…")
.on_change(move |value, window, cx| {
query_for_change.update(cx, |state, _| *state = value);
let q = query_for_change.read(cx).to_string().to_lowercase();
let next_len = if q.is_empty() {
items_for_count.len()
} else {
items_for_count
.iter()
.filter(|s| s.to_lowercase().contains(&q))
.count()
};
controller.reset(next_len);
window.refresh();
}),
)
.child(
virtual_list(self.list_state.clone(), move |ix, _window, _cx| -> AnyElement {
let text = filtered_for_rows.get(ix).cloned().unwrap_or_default();
virtual_row(("result", ix))
.gap_below(px(6.))
.child(list_item().content(label(text).strong(true)))
.into_any_element()
}),
)
}
}- Prefer stable keys from your data model (id/uuid/path) instead of
(\"result\", ix)when rows can reorder. - When your filtered list length changes, call
controller.reset(...)orsplice(...).
Design-VirtualizationDesign-Keyed-State
Yororen UI v0.3.0 · repository · Apache-2.0 · This wiki documents Yororen UI v0.3.0.
This wiki documents Yororen UI v0.3.0 — the headless-core, swappable-renderer build.