Fix 5 deferred performance/UX issues from audit
M8: Pre-compile regex once before rename preview loop instead of recompiling per file. Adds apply_simple_compiled() to RenameConfig. M9: Cache font data in watermark module using OnceLock (default font) and Mutex<HashMap> (named fonts) to avoid repeated filesystem walks during preview updates. M12: Add 150ms debounce to watermark opacity, rotation, margin, and scale sliders to avoid spawning preview threads on every pixel of slider movement. M13: Add 150ms debounce to compress per-format quality sliders (JPEG, PNG, WebP, AVIF) for the same reason. M14: Move thumbnail loading to background threads instead of blocking the GTK main loop. Each thumbnail is decoded via image crate in a spawned thread and delivered to the main thread via channel polling.
This commit is contained in:
@@ -732,20 +732,50 @@ fn build_loaded_state(state: &AppState) -> gtk::Box {
|
||||
};
|
||||
unsafe { thumb_stack.set_data("bind-gen", bind_gen); }
|
||||
|
||||
// Load thumbnail asynchronously
|
||||
// Load thumbnail in background thread to avoid blocking the UI
|
||||
let thumb_stack_c = thumb_stack.clone();
|
||||
let picture_c = picture.clone();
|
||||
let path_c = path.clone();
|
||||
glib::idle_add_local_once(move || {
|
||||
let (tx, rx) = std::sync::mpsc::channel::<Option<Vec<u8>>>();
|
||||
std::thread::spawn(move || {
|
||||
let result = (|| -> Option<Vec<u8>> {
|
||||
let img = image::open(&path_c).ok()?;
|
||||
let thumb = img.resize(
|
||||
(THUMB_SIZE * 2) as u32,
|
||||
(THUMB_SIZE * 2) as u32,
|
||||
image::imageops::FilterType::Triangle,
|
||||
);
|
||||
let mut buf = Vec::new();
|
||||
thumb.write_to(
|
||||
&mut std::io::Cursor::new(&mut buf),
|
||||
image::ImageFormat::Png,
|
||||
).ok()?;
|
||||
Some(buf)
|
||||
})();
|
||||
let _ = tx.send(result);
|
||||
});
|
||||
glib::timeout_add_local(std::time::Duration::from_millis(50), move || {
|
||||
let current: u32 = unsafe {
|
||||
thumb_stack_c.data::<u32>("bind-gen")
|
||||
.map(|p| *p.as_ref())
|
||||
.unwrap_or(0)
|
||||
};
|
||||
if current != bind_gen {
|
||||
return; // Item was recycled; skip stale load
|
||||
return glib::ControlFlow::Break;
|
||||
}
|
||||
match rx.try_recv() {
|
||||
Ok(Some(bytes)) => {
|
||||
let gbytes = glib::Bytes::from(&bytes);
|
||||
if let Ok(texture) = gtk::gdk::Texture::from_bytes(&gbytes) {
|
||||
picture_c.set_paintable(Some(&texture));
|
||||
thumb_stack_c.set_visible_child_name("picture");
|
||||
}
|
||||
glib::ControlFlow::Break
|
||||
}
|
||||
Ok(None) => glib::ControlFlow::Break, // decode failed, leave placeholder
|
||||
Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue,
|
||||
Err(_) => glib::ControlFlow::Break,
|
||||
}
|
||||
load_thumbnail(&path_c, &picture_c, &thumb_stack_c);
|
||||
});
|
||||
|
||||
// Set checkbox state
|
||||
@@ -935,22 +965,6 @@ fn find_check_button(widget: >k::Widget) -> Option<gtk::CheckButton> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Load a thumbnail for the given path into the Picture widget
|
||||
fn load_thumbnail(path: &std::path::Path, picture: >k::Picture, stack: >k::Stack) {
|
||||
// Use GdkPixbuf to load at reduced size for speed
|
||||
match gtk::gdk_pixbuf::Pixbuf::from_file_at_scale(path, THUMB_SIZE * 2, THUMB_SIZE * 2, true) {
|
||||
Ok(pixbuf) => {
|
||||
#[allow(deprecated)]
|
||||
let texture = gtk::gdk::Texture::for_pixbuf(&pixbuf);
|
||||
picture.set_paintable(Some(&texture));
|
||||
stack.set_visible_child_name("picture");
|
||||
}
|
||||
Err(_) => {
|
||||
// Leave placeholder visible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Download an image from a URL to a temporary file
|
||||
fn download_image_url(url: &str) -> Option<std::path::PathBuf> {
|
||||
let temp_dir = std::env::temp_dir().join("pixstrip-downloads");
|
||||
|
||||
Reference in New Issue
Block a user