GTK4 efficient ListView

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: &gtk::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(&gtk::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>(_: &gtk::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(&gtk::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
  }
}

Look at the listview demos in gtk4-demo. There are examples with models containing between 100k and millions of items.

2 Likes

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.