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 <noreply@anthropic.com>
This commit is contained in:
d-b-c-e
2026-02-02 19:10:31 +01:00
committed by GitHub
co-authored by Claude Opus 4.5
parent 80695c2b95
commit e890ef940c
5 changed files with 107 additions and 34 deletions
+11
View File
@@ -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<Option<usize>, String>;
pub struct ImageSelectState {
pub selected_shortcut: Option<GameType>,
pub grid_id: Option<usize>,
@@ -21,6 +24,12 @@ pub struct ImageSelectState {
pub steam_games: Option<Vec<crate::steam::SteamGameInfo>>,
pub possible_names: Option<Vec<steamgriddb_api::search::SearchResult>>,
/// Receiver for async grid ID search results
pub grid_id_search: Receiver<FetchStatus<GridIdSearchResult>>,
/// Receiver for async name search results (for "correct grid ID" feature)
pub name_search: Receiver<FetchStatus<Vec<steamgriddb_api::search::SearchResult>>>,
}
@@ -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,
}
}
}
+21 -11
View File
@@ -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) {
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()
.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 = app.rt.block_on(client.search(app_name));
app.image_selected_state.possible_names = search_results.ok();
let search_results = client.search(&app_name).await;
let results = search_results.unwrap_or_default();
let _ = tx.send(FetchStatus::Fetched(results));
});
}
}
+3 -1
View File
@@ -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()
+30 -12
View File
@@ -2,6 +2,7 @@ use std::path::Path;
use egui::ImageButton;
use steam_shortcuts_util::shortcut::ShortcutOwned;
use tokio::sync::watch;
use crate::{
steam::SteamUsersInfo,
@@ -11,7 +12,7 @@ use crate::{
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();
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));
});
}
state.selected_shortcut = Some(shortcut);
}
+34 -2
View File
@@ -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 => {