-
Notifications
You must be signed in to change notification settings - Fork 9
Guide Recipes VirtualList Search
MeowLynxSea edited this page Jun 18, 2026
·
3 revisions
The "search box + virtualized results" pattern using search_input + virtual_list.
-
search_input— text input with a search icon and a built-in × clear button.search_input(id)takes 1 arg; renders via.render(cx, window).on_clearfires after the renderer clears the input on Escape. -
virtual_list+VirtualListController— wrapsgpui::ListState. Use.on_visible_range_changefor infinite loading. -
list_item— the row content.
use std::ops::Range;
use yororen_ui::headless::label::label;
use yororen_ui::headless::list_item::list_item;
use yororen_ui::headless::search_input::search_input;
use yororen_ui::headless::virtual_list::{VirtualListController, virtual_list};
pub struct SearchView {
pub all_items: Vec<String>,
pub visible: Vec<usize>,
pub controller: VirtualListController,
}
impl SearchView {
pub fn new(_cx: &mut gpui::App) -> Self {
Self {
all_items: Vec::new(),
visible: Vec::new(),
controller: VirtualListController::with_default(0),
}
}
pub fn set_items(&mut self, items: Vec<String>) {
self.visible = (0..items.len()).collect();
self.all_items = items;
self.controller.reset(self.visible.len());
}
pub fn apply_query(&mut self, q: &str) {
self.visible = if q.is_empty() {
(0..self.all_items.len()).collect()
} else {
let needle = q.to_lowercase();
self.all_items.iter().enumerate()
.filter(|(_, s)| s.to_lowercase().contains(&needle))
.map(|(i, _)| i)
.collect()
};
self.controller.reset(self.visible.len());
}
}
impl Render for SearchView {
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let entity = cx.entity();
let entity_for_change = entity.clone();
let controller_for_change = self.controller.clone();
div().size_full().flex().flex_col().gap_3().p_4()
.child(
search_input("search-box")
.placeholder("Search…")
.on_change(move |value: &str, _w, cx| {
entity_for_change.update(cx, |s, _| s.apply_query(value));
let visible_len = entity_for_change.read(cx).visible.len();
controller_for_change.reset(visible_len);
})
.render(cx, _w),
)
.child({
let entity_for_rows = entity.clone();
virtual_list("search-results", &self.controller, cx)
.row(move |ix, _w, cx| {
let source_ix = entity_for_rows.read(cx).visible.get(ix).copied().unwrap_or(0);
let text = entity_for_rows.read(cx).all_items.get(source_ix).cloned().unwrap_or_default();
let row_id = format!("search-row-{source_ix}");
list_item(row_id, text.clone(), cx)
.render(cx)
.child(label(format!("search-row-label-{source_ix}"), text, cx).render(cx))
.into_any_element()
})
.render(cx)
})
}
}-
Stable keys from the source data. Key by
source_ixbecausevisiblerebuilds on every query. If your data can reorder, key by the data's primary id. -
controller.reset(n)for full replacement. When the query changes the visible set rebuilds and the count changes. -
controller.append(n)for tail-only growth. Preserves scroll position. The right call for "infinite loading". -
controller.splice(old, new)for partial updates. If a row's height changes (disclosure opens) or you edit one row in place. -
on_visible_range_changefires when the visible range changes (scroll, resize, item-count change).FnMut + 'static. -
uniform_virtual_listis the fixed-height variant — same shape but measures only the first row. Noon_visible_range_changesupport. -
search_inputclears on Escape automatically. The renderer resets the value and fireson_clear; your hook is for extra work (logging, re-fetching).
- SearchInput — full API.
- VirtualList — full API.
- Virtualization.
- Recipe: empty state — pair with the empty-results 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.