From e890ef940c0a9cd1ad824c9401ff3e5d55079afb Mon Sep 17 00:00:00 2001 From: d-b-c-e Date: Mon, 2 Feb 2026 12:10:31 -0600 Subject: [PATCH] Fix UI blocking in Images tab (#461) The Images tab would freeze when selecting shortcuts or correcting grid IDs because block_on() was called on the main UI thread. Changes: - Convert handle_shortcut_selected() to spawn async SteamGridDB search in the background instead of blocking the UI thread - Convert handle_correct_grid_request() to spawn async name search in the background instead of blocking - Add poll_grid_id_search() and poll_name_search() to receive async results via watch channels - Add grid_id_search and name_search fields to ImageSelectState - Fix Windows file:// URL paths by normalizing backslashes to forward slashes (file:/// prefix for absolute paths) - Remove blocking thread::sleep(100ms) after image selection, replace with request_repaint() Co-authored-by: Claude Opus 4.5 --- src/ui/images/image_select_state.rs | 11 +++++ src/ui/images/pages/change_grid_db_id.rs | 44 +++++++++++------- src/ui/images/pages/select_image_type.rs | 4 +- .../images/pages/shortcut_images_overview.rs | 46 +++++++++++++------ src/ui/images/ui_image_download.rs | 36 ++++++++++++++- 5 files changed, 107 insertions(+), 34 deletions(-) diff --git a/src/ui/images/image_select_state.rs b/src/ui/images/image_select_state.rs index aedbb3f..71e70c5 100644 --- a/src/ui/images/image_select_state.rs +++ b/src/ui/images/image_select_state.rs @@ -7,6 +7,9 @@ use super::{ gamemode::GameMode, possible_image::PossibleImage, gametype::GameT use tokio::sync::watch::{self, Receiver}; +/// Result type for grid ID search operations +pub type GridIdSearchResult = Result, String>; + pub struct ImageSelectState { pub selected_shortcut: Option, pub grid_id: Option, @@ -21,6 +24,12 @@ pub struct ImageSelectState { pub steam_games: Option>, pub possible_names: Option>, + + /// Receiver for async grid ID search results + pub grid_id_search: Receiver>, + + /// Receiver for async name search results (for "correct grid ID" feature) + pub name_search: Receiver>>, } @@ -39,6 +48,8 @@ impl Default for ImageSelectState { possible_names: None, image_options: watch::channel(FetchStatus::NeedsFetched).1, steam_games: None, + grid_id_search: watch::channel(FetchStatus::NeedsFetched).1, + name_search: watch::channel(FetchStatus::NeedsFetched).1, } } } diff --git a/src/ui/images/pages/change_grid_db_id.rs b/src/ui/images/pages/change_grid_db_id.rs index 47d589b..b7b18d9 100644 --- a/src/ui/images/pages/change_grid_db_id.rs +++ b/src/ui/images/pages/change_grid_db_id.rs @@ -1,8 +1,10 @@ +use tokio::sync::watch; + use crate::{ steamgriddb::CachedSearch, ui::{ images::{image_select_state::ImageSelectState, useraction::UserAction}, - MyEguiApp, + FetchStatus, MyEguiApp, }, }; @@ -51,20 +53,28 @@ pub fn handle_grid_change(app: &mut MyEguiApp, grid_id: usize) { } -pub fn handle_correct_grid_request(app:&mut MyEguiApp) { - let app_name = app - .image_selected_state - .selected_shortcut - .as_ref() - .map(|s| s.name()) - .unwrap_or_default(); - let auth_key = app - .settings - .steamgrid_db - .auth_key - .clone() - .unwrap_or_default(); - let client = steamgriddb_api::Client::new(auth_key); - let search_results = app.rt.block_on(client.search(app_name)); - app.image_selected_state.possible_names = search_results.ok(); +pub fn handle_correct_grid_request(app: &mut MyEguiApp) { + let app_name = app + .image_selected_state + .selected_shortcut + .as_ref() + .map(|s| s.name().to_string()) + .unwrap_or_default(); + + if let Some(auth_key) = app.settings.steamgrid_db.auth_key.clone() { + // Create channel to communicate results + let (tx, rx) = watch::channel(FetchStatus::Fetching); + app.image_selected_state.name_search = rx; + + // Clear any existing possible_names so we show loading state + app.image_selected_state.possible_names = None; + + // Spawn the search in the background instead of blocking the UI + app.rt.spawn(async move { + let client = steamgriddb_api::Client::new(auth_key); + let search_results = client.search(&app_name).await; + let results = search_results.unwrap_or_default(); + let _ = tx.send(FetchStatus::Fetched(results)); + }); } +} diff --git a/src/ui/images/pages/select_image_type.rs b/src/ui/images/pages/select_image_type.rs index 1fabdc9..efb926d 100644 --- a/src/ui/images/pages/select_image_type.rs +++ b/src/ui/images/pages/select_image_type.rs @@ -83,7 +83,9 @@ fn render_thumbnail( ) -> bool { let (_path, key) = shortcut.key(image_type, Path::new(&user_path)); let text = format!("Pick {} image", image_type.name()); - let image = egui::Image::new(format!("file://{}", key)).max_width(MAX_WIDTH).shrink_to_fit(); + // Convert Windows backslashes to forward slashes for file:// URL + let key_normalized = key.replace('\\', "/"); + let image = egui::Image::new(format!("file:///{}", key_normalized)).max_width(MAX_WIDTH).shrink_to_fit(); let calced = image.calc_size(egui::Vec2 { x: MAX_WIDTH, y: f32::INFINITY }, image.size()); let button = ImageButton::new(image); ui.add_sized(calced,button).on_hover_text(text).clicked() diff --git a/src/ui/images/pages/shortcut_images_overview.rs b/src/ui/images/pages/shortcut_images_overview.rs index 97db186..a7e84a5 100644 --- a/src/ui/images/pages/shortcut_images_overview.rs +++ b/src/ui/images/pages/shortcut_images_overview.rs @@ -2,16 +2,17 @@ use std::path::Path; use egui::ImageButton; use steam_shortcuts_util::shortcut::ShortcutOwned; +use tokio::sync::watch; use crate::{ steam::SteamUsersInfo, steamgriddb::{CachedSearch, ImageType}, ui::{ images::{ - gametype::GameType, hasimagekey::HasImageKey, + gametype::GameType, hasimagekey::HasImageKey, useraction::UserAction, }, - MyEguiApp, + FetchStatus, MyEguiApp, }, }; @@ -66,7 +67,9 @@ fn render_image( &ImageType::Grid, Path::new(&user_info.steam_user_data_folder), ); - let image = egui::Image::new(format!("file://{}", key)).max_width(column_width).shrink_to_fit(); + // Convert Windows backslashes to forward slashes for file:// URL + let key_normalized = key.replace('\\', "/"); + let image = egui::Image::new(format!("file:///{}", key_normalized)).max_width(column_width).shrink_to_fit(); let calced = image.calc_size(egui::Vec2 { x: column_width, y: f32::INFINITY }, image.size()); let button = ImageButton::new(image); @@ -77,17 +80,32 @@ fn render_image( } None } -pub fn handle_shortcut_selected(app: &mut MyEguiApp, shortcut: GameType ) { - let state = &mut app.image_selected_state; - //We must have a user to get to this action; - if let Some(auth_key) = &app.settings.steamgrid_db.auth_key { +pub fn handle_shortcut_selected(app: &mut MyEguiApp, shortcut: GameType) { + // Set the selected shortcut immediately so UI can show it + app.image_selected_state.selected_shortcut = Some(shortcut.clone()); + app.image_selected_state.grid_id = None; + + // We must have a user to get to this action + if let Some(auth_key) = app.settings.steamgrid_db.auth_key.clone() { + // Create channel to communicate results + let (tx, rx) = watch::channel(FetchStatus::Fetching); + app.image_selected_state.grid_id_search = rx; + + let app_id = shortcut.app_id(); + let app_name = shortcut.name().to_string(); + + // Spawn the search in the background instead of blocking the UI + app.rt.spawn(async move { let client = steamgriddb_api::Client::new(auth_key); let search = CachedSearch::new(&client); - state.grid_id = app - .rt - .block_on(search.search(shortcut.app_id(), shortcut.name())) - .ok() - .flatten(); - } - state.selected_shortcut = Some(shortcut); + let result = search.search(app_id, &app_name).await; + + let search_result = match result { + Ok(grid_id) => Ok(grid_id), + Err(e) => Err(e.to_string()), + }; + + let _ = tx.send(FetchStatus::Fetched(search_result)); + }); + } } diff --git a/src/ui/images/ui_image_download.rs b/src/ui/images/ui_image_download.rs index 7ee7d60..ff962c9 100644 --- a/src/ui/images/ui_image_download.rs +++ b/src/ui/images/ui_image_download.rs @@ -11,7 +11,7 @@ use super::{ useraction::UserAction, }; -use std::{ path::Path, thread, time::Duration}; +use std::path::Path; use crate::{ config::get_thumbnails_folder, @@ -131,9 +131,39 @@ impl MyEguiApp { } } + /// Poll for async grid_id search results from background task + fn poll_grid_id_search(&mut self) { + if let FetchStatus::Fetched(result) = &*self.image_selected_state.grid_id_search.borrow() { + match result { + Ok(Some(grid_id)) => { + if self.image_selected_state.grid_id.is_none() { + self.image_selected_state.grid_id = Some(*grid_id); + } + } + Ok(None) => {} + Err(_) => {} + } + } + } + + /// Poll for async name search results from background task + fn poll_name_search(&mut self) { + if let FetchStatus::Fetched(results) = &*self.image_selected_state.name_search.borrow() { + if self.image_selected_state.possible_names.is_none() { + self.image_selected_state.possible_names = Some(results.clone()); + } + } + } + pub fn render_ui_images(&mut self, ui: &mut egui::Ui) { self.ensure_steam_users_loaded(); + // Poll for async grid_id search results + self.poll_grid_id_search(); + + // Poll for async name search results + self.poll_name_search(); + if let Some(error_message) = &self.image_selected_state.settings_error { ui.label(error_message); return; @@ -147,6 +177,7 @@ impl MyEguiApp { ui.reset_style(); action = self.render_ui_image_action(ui); }); + match action { UserAction::UserSelected(user) => { self.handle_user_selected(user); @@ -159,7 +190,8 @@ impl MyEguiApp { } UserAction::ImageSelected(image) => { handle_image_selected(self, image); - thread::sleep(Duration::from_millis(100)); + // Request repaint to refresh images after download starts + ui.ctx().request_repaint(); ui.ctx().forget_all_images(); } UserAction::BackButton => {