feat: add picker UI (live-filtered thumbnail grid)

This commit is contained in:
2026-08-03 19:16:02 -04:00
parent 06d1204aac
commit 8dd51b48ca
2 changed files with 107 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
pub mod editor;
pub mod picker;
+105
View File
@@ -0,0 +1,105 @@
use crate::icon_index::{filter_icons, list_svg_icons};
use gtk::glib;
use gtk::prelude::*;
use gtk::{Box as GtkBox, FlowBox, Label, Orientation, Picture, ScrolledWindow, SearchEntry};
use std::cell::RefCell;
use std::path::PathBuf;
use std::rc::Rc;
const RESULT_LIMIT: usize = 300;
pub fn build_picker_page(
breeze_root: PathBuf,
on_select: impl Fn(PathBuf) + 'static,
) -> gtk::Widget {
let root = GtkBox::new(Orientation::Vertical, 8);
root.set_margin_top(8);
root.set_margin_bottom(8);
root.set_margin_start(8);
root.set_margin_end(8);
let search = SearchEntry::new();
search.set_placeholder_text(Some("Search icons…"));
root.append(&search);
let flow_box = FlowBox::new();
flow_box.set_valign(gtk::Align::Start);
flow_box.set_max_children_per_line(10);
flow_box.set_selection_mode(gtk::SelectionMode::Single);
let scroller = ScrolledWindow::new();
scroller.set_vexpand(true);
scroller.set_child(Some(&flow_box));
root.append(&scroller);
let all_icons = Rc::new(list_svg_icons(&breeze_root));
// Paths for whatever's currently shown in flow_box, in the same order
// as its children — FlowBoxChild::index() looks up into this. Simpler
// and unsafe-free compared to attaching data to each child widget.
let current_matches: Rc<RefCell<Vec<PathBuf>>> = Rc::new(RefCell::new(Vec::new()));
{
let current_matches = Rc::clone(&current_matches);
flow_box.connect_child_activated(move |_, child| {
let index = child.index();
if index < 0 {
return;
}
if let Some(path) = current_matches.borrow().get(index as usize) {
on_select(path.clone());
}
});
}
let populate = {
let flow_box = flow_box.clone();
let all_icons = Rc::clone(&all_icons);
let current_matches = Rc::clone(&current_matches);
move |query: &str| {
while let Some(child) = flow_box.first_child() {
flow_box.remove(&child);
}
let matches: Vec<PathBuf> = filter_icons(&all_icons, query, RESULT_LIMIT)
.into_iter()
.cloned()
.collect();
for path in &matches {
let item_box = GtkBox::new(Orientation::Vertical, 4);
item_box.set_size_request(96, 96);
let picture = Picture::new();
if let Ok(texture) = gtk::gdk::Texture::from_filename(path) {
picture.set_paintable(Some(&texture));
}
picture.set_size_request(64, 64);
item_box.append(&picture);
let name = path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
let label = Label::new(Some(&name));
label.set_ellipsize(gtk::pango::EllipsizeMode::Middle);
label.set_max_width_chars(14);
item_box.append(&label);
flow_box.append(&item_box);
}
*current_matches.borrow_mut() = matches;
}
};
populate("");
{
let populate = populate.clone();
search.connect_search_changed(move |entry| {
populate(&entry.text());
});
}
root.upcast()
}