use std::{cell::RefCell, collections::HashMap, rc::Rc}; use wgui::{ assets::AssetPath, globals::WguiGlobals, i18n::Translation, layout::{Layout, WidgetID}, parser::{Fetchable, ParseDocumentParams, ParserState}, task::Tasks, widget::{ ConstructEssentials, label::{WidgetLabel, WidgetLabelParams}, }, }; use crate::{ frontend::{FrontendTask, FrontendTasks}, util::{ cached_fetcher::CoverArt, popup_manager::{MountPopupParams, PopupHandle}, steam_utils::{self, AppID, AppManifest, SteamUtils}, various::AsyncExecutor, }, views::{self, game_cover, game_launcher}, }; #[derive(Clone)] enum Task { AppManifestClicked(steam_utils::AppManifest), SetCoverArt(AppID, Rc), CloseLauncher, Refresh, } pub struct Params<'a> { pub globals: WguiGlobals, pub executor: AsyncExecutor, pub frontend_tasks: FrontendTasks, pub layout: &'a mut Layout, pub parent_id: WidgetID, } pub struct Cell { view_cover: game_cover::View, manifest: AppManifest, } struct State { view_launcher: Option<(PopupHandle, views::game_launcher::View)>, } pub struct View { #[allow(dead_code)] parser_state: ParserState, tasks: Tasks, frontend_tasks: FrontendTasks, globals: WguiGlobals, id_list_parent: WidgetID, steam_utils: steam_utils::SteamUtils, cells: HashMap, game_cover_view_common: game_cover::ViewCommon, executor: AsyncExecutor, state: Rc>, } impl View { pub fn new(params: Params) -> anyhow::Result { let doc_params = &ParseDocumentParams { globals: params.globals.clone(), path: AssetPath::BuiltIn("gui/view/game_list.xml"), extra: Default::default(), }; let parser_state = wgui::parser::parse_from_assets(doc_params, params.layout, params.parent_id)?; let list_parent = parser_state.fetch_widget(¶ms.layout.state, "list_parent")?; let tasks = Tasks::new(); let steam_utils = SteamUtils::new()?; tasks.push(Task::Refresh); Ok(Self { parser_state, tasks, frontend_tasks: params.frontend_tasks, globals: params.globals.clone(), id_list_parent: list_parent.id, steam_utils, cells: HashMap::new(), game_cover_view_common: game_cover::ViewCommon::new(params.globals.clone()), state: Rc::new(RefCell::new(State { view_launcher: None })), executor: params.executor, }) } pub fn update(&mut self, layout: &mut Layout, executor: &AsyncExecutor) -> anyhow::Result<()> { loop { let tasks = self.tasks.drain(); if tasks.is_empty() { break; } for task in tasks { match task { Task::Refresh => self.refresh(layout, executor)?, Task::AppManifestClicked(manifest) => self.action_app_manifest_clicked(manifest)?, Task::SetCoverArt(app_id, cover_art) => self.set_cover_art(layout, app_id, cover_art), Task::CloseLauncher => self.state.borrow_mut().view_launcher = None, } } } let mut state = self.state.borrow_mut(); if let Some((_, view)) = &mut state.view_launcher { view.update(layout)?; } Ok(()) } } pub struct Games { manifests: Vec, } fn fill_game_list( ess: &mut ConstructEssentials, executor: &AsyncExecutor, cells: &mut HashMap, games: &Games, tasks: &Tasks, ) -> anyhow::Result<()> { for manifest in &games.manifests { let on_loaded = { let app_id = manifest.app_id.clone(); let tasks = tasks.clone(); Box::new(move |cover_art: CoverArt| { tasks.push(Task::SetCoverArt(app_id, Rc::from(cover_art))); }) }; let view_cover = game_cover::View::new(game_cover::Params { ess, executor, manifest, on_loaded, scale: 1.0, })?; view_cover.button.on_click({ let tasks = tasks.clone(); let manifest = manifest.clone(); Box::new(move |_, _| { tasks.push(Task::AppManifestClicked(manifest.clone())); Ok(()) }) }); cells.insert( manifest.app_id.clone(), Cell { view_cover, manifest: manifest.clone(), }, ); } Ok(()) } impl View { fn game_list(&self) -> anyhow::Result { let manifests = self .steam_utils .list_installed_games(steam_utils::GameSortMethod::PlayDateDesc)?; Ok(Games { manifests }) } fn refresh(&mut self, layout: &mut Layout, executor: &AsyncExecutor) -> anyhow::Result<()> { layout.remove_children(self.id_list_parent); self.cells.clear(); let mut text: Option = None; match self.game_list() { Ok(list) => { if list.manifests.is_empty() { text = Some(Translation::from_translation_key("GAME_LIST.NO_GAMES_FOUND")) } else { fill_game_list( &mut ConstructEssentials { layout, parent: self.id_list_parent, }, executor, &mut self.cells, &list, &self.tasks, )? } } Err(e) => text = Some(Translation::from_raw_text(&format!("Error: {:?}", e))), } if let Some(text) = text.take() { layout.add_child( self.id_list_parent, WidgetLabel::create( &mut self.globals.get(), WidgetLabelParams { content: text, ..Default::default() }, ), Default::default(), )?; } Ok(()) } fn set_cover_art(&mut self, layout: &mut Layout, app_id: AppID, cover_art: Rc) { let Some(cell) = &mut self.cells.get_mut(&app_id) else { return; }; if let Err(e) = cell .view_cover .set_cover_art(&mut self.game_cover_view_common, layout, &cover_art) { log::error!("{:?}", e); }; } fn action_app_manifest_clicked(&mut self, manifest: steam_utils::AppManifest) -> anyhow::Result<()> { self.frontend_tasks.push(FrontendTask::MountPopup(MountPopupParams { title: Translation::from_raw_text(&manifest.name), on_content: { let state = self.state.clone(); let tasks = self.tasks.clone(); let executor = self.executor.clone(); let globals = self.globals.clone(); let frontend_tasks = self.frontend_tasks.clone(); Rc::new(move |data| { let on_launched = { let tasks = tasks.clone(); Box::new(move || tasks.push(Task::CloseLauncher)) }; let view = game_launcher::View::new(game_launcher::Params { manifest: manifest.clone(), executor: executor.clone(), globals: &globals, layout: data.layout, parent_id: data.id_content, frontend_tasks: &frontend_tasks, on_launched, })?; state.borrow_mut().view_launcher = Some((data.handle, view)); Ok(()) }) }, })); Ok(()) } }