I’m making a GTK4 application that heavily relies on ListViews. The elements inside the listview could reach billions of entries. I have used gtk::ListStore, but it can’t handle more than 10,000 entries, if it does at all. I have tried many times to make a multi-columns listview using gtk::gio::liststore since supposedly it can handle unlimited amount of entries, but never succeeded.
The listview doesn’t have to have multiple columns (I think gtk::gio::ListStore doesn’t support multi-columns anyway) if I could show an entire struct within each row. I have checked ShortWave (A gtk4 radio applicaiton that uses listbox I believe) if a listbox would handle the task I would like to know
I have been trying to find a viable solution for about 4 months
This is what I’m currently using, a struct that would handle any listview in the app:
use gtk::prelude::*;
use gtk4 as gtk;
use std::sync::Arc;
/// Trait each row-type must implement to provide column schema and fill logic.
pub trait ListRow {
/// The sequence of `glib::Type` for each column in the ListStore.
fn column_types() -> &'static [gtk::glib::Type];
/// Called for each item to insert its values into the store.
fn fill_row(store: >k::ListStore, item: &Self);
}
/// A reusable GTK4 ListView component, parameterized on `T: ListRow`.
#[derive(Clone)]
pub struct GenericListView<T: ListRow> {
pub container: gtk::Box, // Vertical box holding everything
pub tree_view: gtk::TreeView,
pub scrolled: gtk::ScrolledWindow,
pub search_entry: gtk::SearchEntry,
pub search_bar: gtk::SearchBar,
pub list_store: gtk::ListStore,
filter_model: gtk::TreeModelFilter,
sort_model: gtk::TreeModelSort,
row_mapper: Arc<dyn Fn(>k::ListStore, &T)>,
_marker: std::marker::PhantomData<T>,
}
impl<T: ListRow + 'static> GenericListView<T> {
/// Create the basic widgets and empty ListStore with the correct column types.
pub fn new() -> Self {
// 1) Create tree view & scroll
let tree_view = gtk::TreeView::builder().headers_visible(true).build();
let scrolled = gtk::ScrolledWindow::builder().child(&tree_view).hexpand(true).vexpand(true).build();
// 2) Create filter/search
let search_entry = gtk::SearchEntry::new();
let search_bar = gtk::SearchBar::builder().halign(gtk::Align::End).valign(gtk::Align::End).show_close_button(true).child(&search_entry).build();
// 3) Pack them into a vertical container (so search_bar overlays)
let container = gtk::Box::new(gtk::Orientation::Vertical, 0);
let overlay = gtk::Overlay::builder().child(&scrolled).hexpand(true).vexpand(true).build();
overlay.add_overlay(&search_bar);
container.append(&overlay);
// 4) Create the ListStore with T::column_types()
let list_store = gtk::ListStore::new(T::column_types());
// 5) Wrap in filter + sort
let filter_model = gtk::TreeModelFilter::new(&list_store, None);
let sort_model = gtk::TreeModelSort::with_model(&filter_model);
tree_view.set_model(Some(&sort_model));
// 6) Filtering function
{
let search_entry = search_entry.downgrade();
filter_model.set_visible_func(move |model, iter| {
let Some(search_entry) = search_entry.upgrade() else { return false };
let text = search_entry.text();
if text.is_empty() {
return true;
}
for i in 0..T::column_types().len() as i32 {
if let Ok(val) = model.get_value(iter, i).get::<String>() {
if val.to_lowercase().contains(&text.to_lowercase()) {
return true;
}
}
}
false
});
}
tree_view.set_search_entry(Some(&search_entry));
{
let filter_model = filter_model.downgrade();
search_entry.connect_search_changed(move |_| {
if let Some(filter_model) = filter_model.upgrade() {
filter_model.refilter();
}
});
}
let key_controller = gtk::EventControllerKey::new();
let search_bar_c = search_bar.clone();
key_controller.connect_key_pressed(move |_, keyval, _keycode, state| {
// Check for Ctrl+Shift modifiers :contentReference[oaicite:0]{index=0}
let ctrl_shift = gtk::gdk::ModifierType::CONTROL_MASK | gtk::gdk::ModifierType::SHIFT_MASK;
if state.contains(ctrl_shift) && keyval == gtk::gdk::Key::F {
// Show the search bar and stop further propagation :contentReference[oaicite:1]{index=1}
search_bar_c.set_search_mode(true);
return gtk::glib::Propagation::Stop;
}
// Otherwise let other handlers run :contentReference[oaicite:2]{index=2}
gtk::glib::Propagation::Proceed
});
tree_view.add_controller(key_controller);
// 7) Default row_mapper is a no-op; user must set it before populating
fn noop_row_mapper<T: ListRow>(_: >k::ListStore, _: &T) {}
let row_mapper = Arc::new(noop_row_mapper::<T>);
GenericListView {
container,
tree_view,
scrolled,
search_entry,
search_bar,
list_store,
filter_model,
sort_model,
row_mapper,
_marker: std::marker::PhantomData,
}
}
/// Add a text column bound to the given model index.
pub fn add_text_column(&mut self, title: &str, model_idx: i32, max_width: Option<i32>, alignment: gtk::pango::Alignment) -> &mut Self {
let renderer = gtk::CellRendererText::new();
// renderer.set_xalign(1.0);
// Explicitly use the CellRendererTextExt version
gtk::prelude::CellRendererTextExt::set_alignment(&renderer, alignment);
let column = gtk::TreeViewColumn::builder().title(title).resizable(true).clickable(true).sort_column_id(model_idx).min_width(10).build();
if let Some(w) = max_width {
column.set_max_width(w);
column.set_expand(true);
}
column.pack_start(&renderer, true);
column.add_attribute(&renderer, "text", model_idx);
self.tree_view.append_column(&column);
self
}
pub fn add_icon_column(&mut self, title: &str, index: i32, width: Option<i32>) -> &mut Self {
let column = gtk::TreeViewColumn::new();
column.set_title(title);
let cell = gtk::CellRendererPixbuf::new();
column.pack_start(&cell, false);
column.add_attribute(&cell, "gicon", index); // or "pixbuf" if using Pixbuf
if let Some(w) = width {
column.set_fixed_width(w);
}
self.tree_view.append_column(&column);
self
}
/// Enable sorting by clicking headers (default descending on first column).
pub fn enable_sorting(&mut self, default_col: u32, default_order: gtk::SortType) -> &mut Self {
self.sort_model.set_sort_column_id(gtk::SortColumn::Index(default_col), default_order);
self
}
/// Provide the function that maps `&T` → store-rows.
/// Must be called before `set_items`.
pub fn set_row_mapper<F>(&mut self, f: F) -> &mut Self
where
F: Fn(>k::ListStore, &T) + 'static,
{
self.row_mapper = Arc::new(f);
self
}
/// Given a slice of `T`, clear+populate the store.
pub fn set_items(&self, items: &[T]) {
self.list_store.clear();
for item in items {
(self.row_mapper)(&self.list_store, item);
}
}
pub fn get_selected(&self) -> Vec<gtk::TreeIter> {
let selection = self.tree_view.selection();
let (paths, _) = selection.selected_rows();
let mut selected_iters = Vec::new();
for path in paths {
if let Some(sort_iter) = self.sort_model.iter(&path) {
let filter_iter = self.sort_model.convert_iter_to_child_iter(&sort_iter);
let list_store_iter = self.filter_model.convert_iter_to_child_iter(&filter_iter);
selected_iters.push(list_store_iter);
}
}
selected_iters
}
}