From 8dd51b48ca290988f87deba61e6b1e42110804f4 Mon Sep 17 00:00:00 2001 From: rootiest Date: Mon, 3 Aug 2026 19:16:02 -0400 Subject: [PATCH] feat: add picker UI (live-filtered thumbnail grid) --- src/ui/mod.rs | 2 + src/ui/picker.rs | 105 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 src/ui/mod.rs create mode 100644 src/ui/picker.rs diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..39451c9 --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,2 @@ +pub mod editor; +pub mod picker; diff --git a/src/ui/picker.rs b/src/ui/picker.rs new file mode 100644 index 0000000..945360d --- /dev/null +++ b/src/ui/picker.rs @@ -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>> = Rc::new(RefCell::new(Vec::new())); + + { + let current_matches = Rc::clone(¤t_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(¤t_matches); + move |query: &str| { + while let Some(child) = flow_box.first_child() { + flow_box.remove(&child); + } + + let matches: Vec = 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() +}