Game launcher (wip), wgui refactor

[skip ci]
This commit is contained in:
Aleksander
2025-12-26 23:22:17 +01:00
parent e0c51492b8
commit d70b51184c
16 changed files with 379 additions and 93 deletions

View File

@@ -0,0 +1,158 @@
use crate::{
frontend::{FrontendTask, FrontendTasks},
util::{
cached_fetcher::{self},
steam_utils::{AppID, AppManifest},
various::AsyncExecutor,
},
};
use wgui::{
assets::AssetPath,
components::button::ComponentButton,
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState},
task::Tasks,
widget::label::WidgetLabel,
};
#[derive(Clone)]
enum Task {
FillAppDetails(cached_fetcher::AppDetailsJSONData),
Launch,
}
pub struct Params<'a> {
pub globals: &'a WguiGlobals,
pub executor: AsyncExecutor,
pub manifest: AppManifest,
pub layout: &'a mut Layout,
pub parent_id: WidgetID,
pub frontend_tasks: &'a FrontendTasks,
pub on_launched: Box<dyn Fn()>,
}
pub struct View {
#[allow(dead_code)]
state: ParserState,
tasks: Tasks<Task>,
on_launched: Box<dyn Fn()>,
frontend_tasks: FrontendTasks,
#[allow(dead_code)]
id_cover_art_parent: WidgetID,
#[allow(dead_code)]
executor: AsyncExecutor,
#[allow(dead_code)]
globals: WguiGlobals,
#[allow(dead_code)]
manifest: AppManifest,
}
impl View {
async fn fetch_details(executor: AsyncExecutor, tasks: Tasks<Task>, app_id: AppID) {
let Some(details) = cached_fetcher::get_app_details_json(executor, app_id).await else {
return;
};
tasks.push(Task::FillAppDetails(details));
}
pub fn new(params: Params) -> anyhow::Result<Self> {
let doc_params = &ParseDocumentParams {
globals: params.globals.clone(),
path: AssetPath::BuiltIn("gui/view/game_launcher.xml"),
extra: Default::default(),
};
let state = wgui::parser::parse_from_assets(doc_params, params.layout, params.parent_id)?;
let mut label_title = state.fetch_widget_as::<WidgetLabel>(&params.layout.state, "label_title")?;
label_title.set_text_simple(
&mut params.globals.get(),
Translation::from_raw_text(&params.manifest.name),
);
let tasks = Tasks::new();
// fetch details from the web
let fut = View::fetch_details(params.executor.clone(), tasks.clone(), params.manifest.app_id.clone());
params.executor.spawn(fut).detach();
let id_cover_art_parent = state.get_widget_id("cover_art_parent")?;
let btn_launch = state.fetch_component_as::<ComponentButton>("btn_launch")?;
tasks.handle_button(&btn_launch, Task::Launch);
Ok(Self {
state,
tasks,
on_launched: params.on_launched,
id_cover_art_parent,
frontend_tasks: params.frontend_tasks.clone(),
executor: params.executor.clone(),
globals: params.globals.clone(),
manifest: params.manifest,
})
}
pub fn update(&mut self, layout: &mut Layout) -> anyhow::Result<()> {
loop {
let tasks = self.tasks.drain();
if tasks.is_empty() {
break;
}
for task in tasks {
match task {
Task::FillAppDetails(details) => self.action_fill_app_details(layout, details)?,
Task::Launch => self.action_launch(),
}
}
}
Ok(())
}
fn action_fill_app_details(
&mut self,
layout: &mut Layout,
mut details: cached_fetcher::AppDetailsJSONData,
) -> anyhow::Result<()> {
let mut c = layout.start_common();
{
let label_author = self.state.fetch_widget(&c.layout.state, "label_author")?.widget;
let label_description = self.state.fetch_widget(&c.layout.state, "label_description")?.widget;
if let Some(developer) = details.developers.pop() {
label_author
.cast::<WidgetLabel>()?
.set_text(&mut c.common(), Translation::from_raw_text_string(developer));
}
let desc = if let Some(desc) = &details.short_description {
Some(desc)
} else if let Some(desc) = &details.detailed_description {
Some(desc)
} else {
None
};
if let Some(desc) = desc {
label_description
.cast::<WidgetLabel>()?
.set_text(&mut c.common(), Translation::from_raw_text(desc));
}
}
c.finish()?;
Ok(())
}
fn action_launch(&mut self) {
self
.frontend_tasks
.push(FrontendTask::PushToast(Translation::from_raw_text("Game launch TODO")));
(*self.on_launched)();
}
}

View File

@@ -1,4 +1,4 @@
use std::{collections::HashMap, rc::Rc};
use std::{cell::RefCell, collections::HashMap, rc::Rc};
use wgui::{
assets::AssetPath,
@@ -34,43 +34,51 @@ use wgui::{
use crate::{
frontend::{FrontendTask, FrontendTasks},
util::{
cover_art_fetcher::{self, CoverArt},
popup_manager::MountPopupParams,
cached_fetcher::{self, CoverArt},
popup_manager::{MountPopupParams, PopupHandle},
steam_utils::{self, AppID, AppManifest, SteamUtils},
various::AsyncExecutor,
},
views::{self, game_launcher},
};
#[derive(Clone)]
enum Task {
AppManifestClicked(steam_utils::AppManifest),
SetCoverArt((AppID, Rc<CoverArt>)),
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,
}
struct Cell {
pub struct Cell {
image_parent: WidgetID,
manifest: AppManifest,
}
struct State {
view_launcher: Option<(PopupHandle, views::game_launcher::View)>,
}
pub struct View {
#[allow(dead_code)]
pub parser_state: ParserState,
parser_state: ParserState,
tasks: Tasks<Task>,
frontend_tasks: FrontendTasks,
globals: WguiGlobals,
id_list_parent: WidgetID,
steam_utils: steam_utils::SteamUtils,
cells: HashMap<AppID, Cell>,
img_placeholder: Option<CustomGlyphData>,
executor: AsyncExecutor,
state: Rc<RefCell<State>>,
}
impl View {
@@ -99,6 +107,8 @@ impl View {
steam_utils,
cells: HashMap::new(),
img_placeholder: None,
state: Rc::new(RefCell::new(State { view_launcher: None })),
executor: params.executor,
})
}
@@ -113,10 +123,16 @@ impl View {
Task::Refresh => self.refresh(layout, executor)?,
Task::AppManifestClicked(manifest) => self.action_app_manifest_clicked(manifest)?,
Task::SetCoverArt((app_id, cover_art)) => self.action_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(())
}
}
@@ -131,8 +147,12 @@ const BORDER_COLOR_HOVERED: drawing::Color = drawing::Color::new(1.0, 1.0, 1.0,
const GAME_COVER_SIZE_X: f32 = 140.0;
const GAME_COVER_SIZE_Y: f32 = 210.0;
async fn request_cover_image(executor: AsyncExecutor, manifest: steam_utils::AppManifest, tasks: Tasks<Task>) {
let cover_art = match cover_art_fetcher::request_image(executor, manifest.app_id.clone()).await {
async fn request_cover_image(
executor: AsyncExecutor,
manifest: steam_utils::AppManifest,
on_loaded: Box<dyn FnOnce(CoverArt)>,
) {
let cover_art = match cached_fetcher::request_image(executor, manifest.app_id.clone()).await {
Ok(cover_art) => cover_art,
Err(e) => {
log::error!("request_cover_image failed: {:?}", e);
@@ -140,15 +160,15 @@ async fn request_cover_image(executor: AsyncExecutor, manifest: steam_utils::App
}
};
tasks.push(Task::SetCoverArt((manifest.app_id, Rc::from(cover_art))));
on_loaded(cover_art)
}
fn construct_game_cover(
pub fn construct_game_cover(
ess: &mut ConstructEssentials,
executor: &AsyncExecutor,
tasks: &Tasks<Task>,
_globals: &WguiGlobals,
manifest: &steam_utils::AppManifest,
on_loaded: Box<dyn FnOnce(CoverArt)>,
) -> anyhow::Result<(WidgetPair, Rc<ComponentButton>, Cell)> {
let (widget_button, button) = components::button::construct(
ess,
@@ -257,7 +277,7 @@ fn construct_game_cover(
// request cover image data from the internet or disk cache
executor
.spawn(request_cover_image(executor.clone(), manifest.clone(), tasks.clone()))
.spawn(request_cover_image(executor.clone(), manifest.clone(), on_loaded))
.detach();
Ok((
@@ -279,7 +299,15 @@ fn fill_game_list(
tasks: &Tasks<Task>,
) -> anyhow::Result<()> {
for manifest in &games.manifests {
let (_, button, cell) = construct_game_cover(ess, executor, tasks, globals, manifest)?;
let on_loaded = {
let app_id = manifest.app_id.clone();
let tasks = tasks.clone();
move |cover_art: CoverArt| {
tasks.push(Task::SetCoverArt((app_id, Rc::from(cover_art))));
}
};
let (_, button, cell) = construct_game_cover(ess, executor, globals, manifest, Box::new(on_loaded))?;
button.on_click({
let tasks = tasks.clone();
@@ -352,8 +380,29 @@ impl View {
self.frontend_tasks.push(FrontendTask::MountPopup(MountPopupParams {
title: Translation::from_raw_text(&manifest.name),
on_content: {
Rc::new(move |_data| {
// todo
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(())
})
},

View File

@@ -1,5 +1,6 @@
pub mod app_launcher;
pub mod audio_settings;
pub mod game_launcher;
pub mod game_list;
pub mod process_list;
pub mod window_list;