Pick image component (#295)

* Extract render_possible_image into function

* Seperate render_image into reusable functions

* Seperate game image into component

* Use image component in shortcut overview

* Reuse image pick component in type select

* Reuse component across select_image_type

* Use image type to scale image buttons

Use image type to scale

* Remove old functions
This commit is contained in:
Philip Kristoffersen
2022-12-30 09:22:33 +01:00
committed by GitHub
parent ef72451289
commit 90c0185c8f
8 changed files with 251 additions and 217 deletions
+11
View File
@@ -22,6 +22,17 @@ impl ImageType {
&ALL_TYPES &ALL_TYPES
} }
pub fn ratio(&self) -> f32{
match self {
ImageType::Hero => 0.3,
ImageType::Grid => 1.6,
ImageType::WideGrid => 0.5,
ImageType::Logo => 0.2,
ImageType::BigPicture => 0.5,
ImageType::Icon => 1.0,
}
}
pub fn name(&self) -> &str { pub fn name(&self) -> &str {
match self { match self {
ImageType::Hero => "Hero", ImageType::Hero => "Hero",
+147
View File
@@ -0,0 +1,147 @@
use std::path::Path;
use egui::{Button, ImageButton};
use futures::executor::block_on;
use tokio::runtime::Runtime;
use crate::steamgriddb::{ImageType, ToDownload};
use crate::ui::images::{clamp_to_width, ImageHandles, TextureDownloadState};
use crate::ui::ui_images::load_image_from_path;
pub fn render_image_from_path(
ui: &mut egui::Ui,
image_handles: &ImageHandles,
path: &Path,
max_width: f32,
text: &str,
) -> bool {
render_possible_image(ui, image_handles, path, max_width, text, &ImageType::Grid, None, None)
}
pub fn render_image_from_path_image_type(
ui: &mut egui::Ui,
image_handles: &ImageHandles,
path: &Path,
max_width: f32,
text: &str,
image_type: &ImageType,
) -> bool {
render_possible_image(ui, image_handles, path, max_width, text, image_type, None, None)
}
pub fn render_image_from_path_or_url(
ui: &mut egui::Ui,
image_handles: &ImageHandles,
path: &Path,
max_width: f32,
text: &str,
image_type: &ImageType,
rt: &Runtime,
url: &str,
) -> bool {
render_possible_image(
ui,
image_handles,
path,
max_width,
text,
image_type,
Some(rt),
Some(url),
)
}
fn render_possible_image(
ui: &mut egui::Ui,
image_handles: &ImageHandles,
path: &Path,
max_width: f32,
text: &str,
image_type: &ImageType,
rt: Option<&Runtime>,
url: Option<&str>,
) -> bool {
let image_key = path.to_string_lossy().to_string();
match image_handles.get_mut(&image_key) {
Some(mut state) => {
match state.value() {
TextureDownloadState::Downloading => {
ui.ctx().request_repaint();
//nothing to do,just wait
ui.spinner();
}
TextureDownloadState::Downloaded => {
//Need to load
let image_data = load_image_from_path(&path);
match image_data {
Ok(image_data) => {
let handle = ui.ctx().load_texture(
&image_key,
image_data,
egui::TextureOptions::LINEAR,
);
*state.value_mut() = TextureDownloadState::Loaded(handle);
ui.spinner();
}
Err(_) => *state.value_mut() = TextureDownloadState::Failed,
}
ui.ctx().request_repaint();
}
TextureDownloadState::Loaded(texture_handle) => {
//need to show
let mut size = texture_handle.size_vec2();
clamp_to_width(&mut size, max_width);
let image_button = ImageButton::new(texture_handle, size);
if ui
.add_sized(size, image_button)
.on_hover_text(text)
.clicked()
{
return true;
}
}
TextureDownloadState::Failed => {
let button =
ui.add_sized([max_width, max_width * image_type.ratio()], Button::new(text).wrap(true));
if button.clicked() {
return true;
}
}
}
}
None => {
if !path.exists() && url.is_none() {
image_handles.insert(image_key, TextureDownloadState::Failed);
} else {
//We need to start a download
//Redownload if file is too small
if !path.exists()
|| std::fs::metadata(path).map(|m| m.len()).unwrap_or_default() < 2
{
image_handles.insert(image_key.clone(), TextureDownloadState::Downloading);
let to_download = ToDownload {
path: path.to_path_buf(),
url: url.unwrap().to_string(),
app_name: "Thumbnail".to_string(),
image_type: *image_type,
};
let image_handles = image_handles.clone();
let image_key = image_key.clone();
if let Some(rt) = rt {
rt.spawn_blocking(move || {
block_on(crate::steamgriddb::download_to_download(&to_download))
.unwrap();
image_handles.insert(image_key, TextureDownloadState::Downloaded);
});
}
} else {
image_handles.insert(image_key.clone(), TextureDownloadState::Downloaded);
}
}
}
}
false
}
+4
View File
@@ -1,3 +1,7 @@
mod steam_user_select; mod steam_user_select;
mod game_image_button;
pub use steam_user_select::render_user_select; pub use steam_user_select::render_user_select;
pub use game_image_button::render_image_from_path;
pub use game_image_button::render_image_from_path_or_url;
pub use game_image_button::render_image_from_path_image_type;
+4 -1
View File
@@ -8,6 +8,9 @@ use crate::{steam::SteamUsersInfo, steamgriddb::ImageType, ui::FetcStatus};
use super::{ gamemode::GameMode, possible_image::PossibleImage, texturestate::TextureDownloadState, gametype::GameType}; use super::{ gamemode::GameMode, possible_image::PossibleImage, texturestate::TextureDownloadState, gametype::GameType};
use tokio::sync::watch::{self, Receiver}; use tokio::sync::watch::{self, Receiver};
pub type ImageHandles = std::sync::Arc<DashMap<String, TextureDownloadState>>;
pub struct ImageSelectState { pub struct ImageSelectState {
pub selected_shortcut: Option<GameType>, pub selected_shortcut: Option<GameType>,
pub grid_id: Option<usize>, pub grid_id: Option<usize>,
@@ -20,7 +23,7 @@ pub struct ImageSelectState {
pub image_type_selected: Option<ImageType>, pub image_type_selected: Option<ImageType>,
pub image_options: Receiver<FetcStatus<Vec<PossibleImage>>>, pub image_options: Receiver<FetcStatus<Vec<PossibleImage>>>,
pub steam_games: Option<Vec<crate::steam::SteamGameInfo>>, pub steam_games: Option<Vec<crate::steam::SteamGameInfo>>,
pub image_handles: std::sync::Arc<DashMap<String, TextureDownloadState>>, pub image_handles: ImageHandles,
pub possible_names: Option<Vec<steamgriddb_api::search::SearchResult>>, pub possible_names: Option<Vec<steamgriddb_api::search::SearchResult>>,
} }
+3
View File
@@ -12,3 +12,6 @@ mod constants;
mod pages; mod pages;
pub use image_select_state::ImageSelectState; pub use image_select_state::ImageSelectState;
pub use image_select_state::ImageHandles;
pub use texturestate::TextureDownloadState;
pub use image_resize::clamp_to_width;
+31 -101
View File
@@ -1,19 +1,20 @@
use std::path::Path; use std::path::Path;
use egui::{Grid, ImageButton}; use egui::{Grid, };
use futures::executor::block_on; use futures::executor::block_on;
use tokio::sync::watch; use tokio::{ sync::watch};
use crate::{ use crate::{
steamgriddb::{get_image_extension, ImageType, ToDownload}, steamgriddb::{get_image_extension, ImageType, ToDownload},
ui::{ ui::{
images::{ images::{
constants::MAX_WIDTH, image_resize::clamp_to_width, constants::MAX_WIDTH,
image_select_state::ImageSelectState, possible_image::PossibleImage, hasimagekey::HasImageKey,
texturestate::TextureDownloadState, useraction::UserAction, hasimagekey::HasImageKey, image_select_state::{ ImageSelectState},
possible_image::PossibleImage,
useraction::UserAction,
}, },
ui_images::load_image_from_path, FetcStatus, MyEguiApp, components::render_image_from_path_or_url,
FetcStatus, MyEguiApp,
}, },
}; };
@@ -59,88 +60,18 @@ pub fn render_page_pick_image(
.spacing([column_padding, column_padding]) .spacing([column_padding, column_padding])
.show(ui, |ui| { .show(ui, |ui| {
for image in images { for image in images {
let image_key = let path = image.thumbnail_path.as_path();
image.thumbnail_path.as_path().to_string_lossy().to_string(); if render_image_from_path_or_url(
ui,
match state.image_handles.get_mut(&image_key) { &state.image_handles,
Some(mut state) => { &path,
match state.value() { column_width,
TextureDownloadState::Downloading => { &image.full_url,
ui.ctx().request_repaint(); image_type,
//nothing to do,just wait &app.rt,
ui.spinner(); &image.thumbnail_url,
} ) {
TextureDownloadState::Downloaded => { return Some(image.clone());
//Need to load
let image_data =
load_image_from_path(&image.thumbnail_path);
match image_data {
Ok(image_data) => {
let handle = ui.ctx().load_texture(
&image_key,
image_data,
egui::TextureOptions::LINEAR,
);
*state.value_mut() =
TextureDownloadState::Loaded(handle);
ui.spinner();
}
Err(_) => {
*state.value_mut() = TextureDownloadState::Failed
}
}
ui.ctx().request_repaint();
}
TextureDownloadState::Loaded(texture_handle) => {
//need to show
let mut size = texture_handle.size_vec2();
clamp_to_width(&mut size, column_width);
let image_button = ImageButton::new(texture_handle, size);
if ui.add_sized(size, image_button).clicked() {
return Some(UserAction::ImageSelected(image.clone()));
}
}
TextureDownloadState::Failed => {
ui.label("Failed to load image");
}
}
}
None => {
//We need to start a download
let image_handles = &app.image_selected_state.image_handles;
let path = &image.thumbnail_path;
//Redownload if file is too small
if !path.exists()
|| std::fs::metadata(path).map(|m| m.len()).unwrap_or_default()
< 2
{
image_handles.insert(
image_key.clone(),
TextureDownloadState::Downloading,
);
let to_download = ToDownload {
path: path.clone(),
url: image.thumbnail_url.clone(),
app_name: "Thumbnail".to_string(),
image_type: *image_type,
};
let image_handles = image_handles.clone();
let image_key = image_key.clone();
app.rt.spawn_blocking(move || {
block_on(crate::steamgriddb::download_to_download(
&to_download,
))
.unwrap();
image_handles
.insert(image_key, TextureDownloadState::Downloaded);
});
} else {
image_handles.insert(
image_key.clone(),
TextureDownloadState::Downloaded,
);
}
}
} }
column += 1; column += 1;
if column >= columns { if column >= columns {
@@ -148,13 +79,12 @@ pub fn render_page_pick_image(
ui.end_row(); ui.end_row();
} }
} }
None None
}) })
.inner; .inner;
if x.is_some() { if let Some(x) = x {
return x; return Some(UserAction::ImageSelected(x));
} }
} }
_ => { _ => {
ui.horizontal(|ui| { ui.horizontal(|ui| {
@@ -188,10 +118,10 @@ pub fn handle_image_selected(app: &mut MyEguiApp, image: PossibleImage) {
let data_folder = Path::new(&user.steam_user_data_folder); let data_folder = Path::new(&user.steam_user_data_folder);
//Keep deleting images of this type untill we don't find any more //Keep deleting images of this type untill we don't find any more
let mut path = get_shortcut_image_path(app,data_folder); let mut path = get_shortcut_image_path(app, data_folder);
while Path::new(&path).exists() { while Path::new(&path).exists() {
let _ = std::fs::remove_file(&path); let _ = std::fs::remove_file(&path);
path = get_shortcut_image_path(app,data_folder); path = get_shortcut_image_path(app, data_folder);
} }
//Put the loaded thumbnail into the image handler map, we can use that for preview //Put the loaded thumbnail into the image handler map, we can use that for preview
@@ -229,7 +159,7 @@ pub fn handle_image_selected(app: &mut MyEguiApp, image: PossibleImage) {
} }
} }
fn get_shortcut_image_path(app:&MyEguiApp, data_folder: &Path) -> String { fn get_shortcut_image_path(app: &MyEguiApp, data_folder: &Path) -> String {
app.image_selected_state app.image_selected_state
.selected_shortcut .selected_shortcut
.as_ref() .as_ref()
@@ -242,10 +172,10 @@ fn get_shortcut_image_path(app:&MyEguiApp, data_folder: &Path) -> String {
} }
fn clear_loaded_images(app: &mut MyEguiApp) { fn clear_loaded_images(app: &mut MyEguiApp) {
if let FetcStatus::Fetched(options) = &*app.image_selected_state.image_options.borrow() { if let FetcStatus::Fetched(options) = &*app.image_selected_state.image_options.borrow() {
for option in options { for option in options {
let key = option.thumbnail_path.to_string_lossy().to_string(); let key = option.thumbnail_path.to_string_lossy().to_string();
app.image_selected_state.image_handles.remove(&key); app.image_selected_state.image_handles.remove(&key);
}
} }
} }
}
+46 -90
View File
@@ -1,78 +1,61 @@
use std::path::Path; use std::path::Path;
use egui::ImageButton; use crate::ui::images::{
gametype::GameType, hasimagekey::HasImageKey, image_select_state::ImageSelectState,
use crate::{ui::images::{image_select_state::ImageSelectState, useraction::UserAction, gametype::GameType, texturestate::TextureDownloadState, hasimagekey::HasImageKey, image_resize::clamp_to_width}, steamgriddb::ImageType}; useraction::UserAction, ImageHandles,
};
use crate::{steamgriddb::ImageType, ui::components::render_image_from_path_image_type};
const MAX_WIDTH: f32 = 300.; const MAX_WIDTH: f32 = 300.;
pub fn render_page_shortcut_select_image_type(
pub fn render_page_shortcut_select_image_type(ui: &mut egui::Ui, state: &ImageSelectState) -> Option<UserAction> { ui: &mut egui::Ui,
state: &ImageSelectState,
) -> Option<UserAction> {
let shortcut = state.selected_shortcut.as_ref().unwrap(); let shortcut = state.selected_shortcut.as_ref().unwrap();
let user_path = &state.steam_user.as_ref().unwrap().steam_user_data_folder; let user_path = &state.steam_user.as_ref().unwrap().steam_user_data_folder;
let thumbnail = |ui: &mut egui::Ui, image_type: &ImageType| {
if render_thumbnail(ui, &state.image_handles, shortcut, image_type, user_path) {
Some(UserAction::ImageTypeSelected(*image_type))
} else {
None
}
};
let x = if ui.available_width() > MAX_WIDTH * 3. { let x = if ui.available_width() > MAX_WIDTH * 3. {
ui.horizontal(|ui| { ui.horizontal(|ui| {
let x = ui let x = ui.vertical(|ui| thumbnail(ui, &ImageType::Grid)).inner;
.vertical(|ui| {
let texture =
texture_from_iamge_type(shortcut, &ImageType::Grid, user_path, state);
ui.label(ImageType::Grid.name());
if render_thumbnail(ui, texture).clicked() {
return Some(UserAction::ImageTypeSelected(ImageType::Grid));
}
None
})
.inner;
if x.is_some() { if x.is_some() {
return x; return x;
} }
let x = ui let x = ui
.vertical(|ui| { .vertical(|ui| {
let texture = let types = &[ImageType::Hero, ImageType::WideGrid, ImageType::Logo];
texture_from_iamge_type(shortcut, &ImageType::Hero, user_path, state); types
ui.label(ImageType::Hero.name()); .iter()
if render_thumbnail(ui, texture).clicked() { .flat_map(|image_type| thumbnail(ui, image_type))
return Some(UserAction::ImageTypeSelected(ImageType::Hero)); .next()
}
let texture =
texture_from_iamge_type(shortcut, &ImageType::WideGrid, user_path, state);
ui.label(ImageType::WideGrid.name());
if render_thumbnail(ui, texture).clicked() {
return Some(UserAction::ImageTypeSelected(ImageType::WideGrid));
}
let texture =
texture_from_iamge_type(shortcut, &ImageType::Logo, user_path, state);
ui.label(ImageType::Logo.name());
if render_thumbnail(ui, texture).clicked() {
return Some(UserAction::ImageTypeSelected(ImageType::Logo));
}
None
}) })
.inner; .inner;
if x.is_some() { if x.is_some() {
return x; return x;
} }
ui.vertical(|ui| { ui.vertical(|ui| {
let texture = texture_from_iamge_type(shortcut, &ImageType::Icon, user_path, state); let types = &[ImageType::Icon, ImageType::BigPicture];
ui.label(ImageType::Icon.name()); types
if render_thumbnail(ui, texture).clicked() { .iter()
return Some(UserAction::ImageTypeSelected(ImageType::Icon)); .flat_map(|image_type| thumbnail(ui, image_type))
} .next()
let texture =
texture_from_iamge_type(shortcut, &ImageType::BigPicture, user_path, state);
ui.label(ImageType::BigPicture.name());
if render_thumbnail(ui, texture).clicked() {
return Some(UserAction::ImageTypeSelected(ImageType::BigPicture));
}
None
}) })
.inner .inner
}) })
.inner .inner
} else { } else {
render_image_types_as_list(shortcut, user_path, state, ui) let types = ImageType::all();
types
.iter()
.flat_map(|image_type| thumbnail(ui, image_type))
.next()
}; };
if ui if ui
@@ -84,48 +67,21 @@ pub fn render_page_shortcut_select_image_type(ui: &mut egui::Ui, state: &ImageSe
x x
} }
fn render_image_types_as_list( fn render_thumbnail(
shortcut: &GameType,
user_path: &String,
state: &ImageSelectState,
ui: &mut egui::Ui, ui: &mut egui::Ui,
) -> Option<UserAction> { image_handles: &ImageHandles,
let types = ImageType::all();
for image_type in types {
let texture = texture_from_iamge_type(shortcut, image_type, user_path, state);
let response = ui
.vertical(|ui| {
ui.label(image_type.name());
render_thumbnail(ui, texture)
})
.inner;
if response.clicked() {
return Some(UserAction::ImageTypeSelected(*image_type));
}
}
None
}
fn texture_from_iamge_type(
shortcut: &GameType, shortcut: &GameType,
image_type: &ImageType, image_type: &ImageType,
user_path: &String, user_path: &String,
state: &ImageSelectState, ) -> bool {
) -> Option<egui::TextureHandle> { let (path, _key) = shortcut.key(image_type, Path::new(&user_path));
let (_path, key) = shortcut.key(image_type, Path::new(&user_path)); let text = format!("Pick {} image", image_type.name());
state.image_handles.get(&key).and_then(|k| match k.value() { render_image_from_path_image_type(
TextureDownloadState::Loaded(texture) => Some(texture.clone()), ui,
_ => None, image_handles,
}) path.as_path(),
} MAX_WIDTH,
&text,
fn render_thumbnail(ui: &mut egui::Ui, image: Option<egui::TextureHandle>) -> egui::Response { image_type,
if let Some(texture) = image { )
let mut size = texture.size_vec2();
clamp_to_width(&mut size, MAX_WIDTH);
let image_button = ImageButton::new(&texture, size);
ui.add(image_button)
} else {
ui.button("Pick an image")
}
} }
@@ -1,6 +1,5 @@
use std::path::Path; use std::path::Path;
use egui::{Button, ImageButton};
use steam_shortcuts_util::shortcut::ShortcutOwned; use steam_shortcuts_util::shortcut::ShortcutOwned;
use crate::{ use crate::{
@@ -8,10 +7,10 @@ use crate::{
steamgriddb::{ImageType, CachedSearch}, steamgriddb::{ImageType, CachedSearch},
ui::{ ui::{
images::{ images::{
gametype::GameType, hasimagekey::HasImageKey, image_resize::clamp_to_width, gametype::GameType, hasimagekey::HasImageKey,
texturestate::TextureDownloadState, useraction::UserAction, texturestate::TextureDownloadState, useraction::UserAction,
}, },
MyEguiApp, ui_images::load_image_from_path, MyEguiApp, ui_images::load_image_from_path, components::render_image_from_path,
}, },
}; };
@@ -65,27 +64,8 @@ fn render_image(
&ImageType::Grid, &ImageType::Grid,
Path::new(&user_info.steam_user_data_folder), Path::new(&user_info.steam_user_data_folder),
); );
let mut clicked = false;
let texture = app.image_selected_state.image_handles.get(&key);
if let Some(texture) = texture {
if let TextureDownloadState::Loaded(texture) = &texture.value() {
let mut size = texture.size_vec2();
clamp_to_width(&mut size, column_width);
let image_button = ImageButton::new(texture, size);
clicked = ui
.add(image_button)
.on_hover_text(&shortcut.app_name)
.clicked();
}
} else {
let button = ui.add_sized(
[column_width, column_width * 1.6],
Button::new(&shortcut.app_name).wrap(true),
);
clicked = clicked || button.clicked();
}
let clicked= render_image_from_path(ui, &app.image_selected_state.image_handles, Path::new(&key), column_width, &shortcut.app_name);
if clicked { if clicked {
return Some(Some(UserAction::ShortcutSelected(GameType::Shortcut( return Some(Some(UserAction::ShortcutSelected(GameType::Shortcut(
shortcut.clone(), shortcut.clone(),