feat: add picker UI (live-filtered thumbnail grid)
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod editor;
|
||||||
|
pub mod picker;
|
||||||
@@ -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(¤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<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()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user