Running games list (Closes #398)

This commit is contained in:
Aleksander
2026-01-17 20:07:37 +01:00
committed by galister
parent 7b3a2a1e48
commit 03a1f449b5
24 changed files with 366 additions and 61 deletions

View File

@@ -14,7 +14,11 @@ use wgui::{
widget::{label::WidgetLabel, rectangle::WidgetRectangle},
windowing::window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra, WguiWindowPlacement},
};
use wlx_common::{audio, dash_interface::BoxDashInterface, timestep::Timestep};
use wlx_common::{
audio,
dash_interface::BoxDashInterface,
timestep::{self, Timestep},
};
use crate::{
assets,
@@ -218,8 +222,10 @@ impl<T: 'static> Frontend<T> {
self.process_task(&mut params, task)?;
}
let time_ms = timestep::get_micros() / 1000;
if let Some(mut tab) = self.current_tab.take() {
tab.update(self, params.data)?;
tab.update(self, time_ms as u32, params.data)?;
self.current_tab = Some(tab);
}

View File

@@ -46,7 +46,7 @@ impl<T> Tab<T> for TabApps<T> {
TabType::Apps
}
fn update(&mut self, frontend: &mut Frontend<T>, data: &mut T) -> anyhow::Result<()> {
fn update(&mut self, frontend: &mut Frontend<T>, _time_ms: u32, data: &mut T) -> anyhow::Result<()> {
let mut state = self.state.borrow_mut();
for task in self.tasks.drain() {

View File

@@ -9,7 +9,8 @@ use wgui::{
use crate::{
frontend::Frontend,
tab::{Tab, TabType},
views::game_list,
util::steam_utils::SteamUtils,
views::{game_list, running_games_list},
};
pub struct TabGames<T> {
@@ -17,6 +18,8 @@ pub struct TabGames<T> {
pub state: ParserState,
view_game_list: game_list::View,
view_running_games_list: running_games_list::View,
steam_utils: SteamUtils,
marker: PhantomData<T>,
}
@@ -25,17 +28,22 @@ impl<T> Tab<T> for TabGames<T> {
TabType::Games
}
fn update(&mut self, frontend: &mut Frontend<T>, _data: &mut T) -> anyhow::Result<()> {
self.view_game_list.update(&mut frontend.layout, &frontend.executor)?;
fn update(&mut self, frontend: &mut Frontend<T>, time_ms: u32, _data: &mut T) -> anyhow::Result<()> {
self
.view_game_list
.update(&mut frontend.layout, &mut self.steam_utils, &frontend.executor)?;
self.view_running_games_list.update(&mut frontend.layout, time_ms)?;
Ok(())
}
}
impl<T> TabGames<T> {
pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID) -> anyhow::Result<Self> {
let globals = frontend.layout.state.globals.clone();
let state = wgui::parser::parse_from_assets(
&ParseDocumentParams {
globals: frontend.layout.state.globals.clone(),
globals: globals.clone(),
path: AssetPath::BuiltIn("gui/tab/games.xml"),
extra: Default::default(),
},
@@ -44,19 +52,32 @@ impl<T> TabGames<T> {
)?;
let game_list_parent = state.get_widget_id("game_list_parent")?;
let id_running_games_list_parent = state.get_widget_id("running_games_list_parent")?;
let view_game_list = game_list::View::new(game_list::Params {
executor: frontend.executor.clone(),
frontend_tasks: frontend.tasks.clone(),
globals: frontend.layout.state.globals.clone(),
globals: globals.clone(),
layout: &mut frontend.layout,
parent_id: game_list_parent,
})?;
let mut steam_utils = SteamUtils::new()?;
let view_running_games_list = running_games_list::View::new(running_games_list::Params {
globals: globals.clone(),
layout: &mut frontend.layout,
parent_id: id_running_games_list_parent,
steam_utils: &mut steam_utils,
frontend_tasks: frontend.tasks.clone(),
})?;
Ok(Self {
state,
view_game_list,
view_running_games_list,
marker: PhantomData,
steam_utils,
})
}
}

View File

@@ -19,7 +19,7 @@ pub trait Tab<T> {
#[allow(dead_code)]
fn get_type(&self) -> TabType;
fn update(&mut self, _: &mut Frontend<T>, _: &mut T) -> anyhow::Result<()> {
fn update(&mut self, _frontend: &mut Frontend<T>, _time_ms: u32, _user_data: &mut T) -> anyhow::Result<()> {
Ok(())
}
}

View File

@@ -42,7 +42,7 @@ impl<T> Tab<T> for TabMonado<T> {
TabType::Games
}
fn update(&mut self, frontend: &mut Frontend<T>, data: &mut T) -> anyhow::Result<()> {
fn update(&mut self, frontend: &mut Frontend<T>, _time_ms: u32, data: &mut T) -> anyhow::Result<()> {
for task in self.tasks.drain() {
match task {
Task::Refresh => self.refresh(frontend, data)?,

View File

@@ -80,7 +80,7 @@ impl<T> Tab<T> for TabSettings<T> {
TabType::Settings
}
fn update(&mut self, frontend: &mut Frontend<T>, data: &mut T) -> anyhow::Result<()> {
fn update(&mut self, frontend: &mut Frontend<T>, _time_ms: u32, data: &mut T) -> anyhow::Result<()> {
let mut changed = false;
for task in self.tasks.drain() {
match task {

View File

@@ -5,3 +5,4 @@ pub mod popup_manager;
pub mod steam_utils;
pub mod toast_manager;
pub mod various;
pub mod wgui_simple;

View File

@@ -35,6 +35,7 @@ pub struct AppManifest {
// TODO @oo8dev: game sort methods
#[allow(dead_code)]
pub enum GameSortMethod {
None,
NameAsc,
NameDesc,
PlayDateDesc,
@@ -127,9 +128,6 @@ pub fn launch(app_id: &AppID) -> anyhow::Result<()> {
Ok(())
}
// TODO @oo8dev: running games list (#398)
/*
pub fn stop(app_id: AppID, force_kill: bool) -> anyhow::Result<()> {
log::info!("Stopping Steam game with AppID {}", app_id);
@@ -220,7 +218,7 @@ pub fn list_running_games() -> anyhow::Result<Vec<RunningGame>> {
}
Ok(res)
} */
}
fn call_steam(arg: &str) -> anyhow::Result<()> {
match std::process::Command::new("xdg-open").arg(arg).spawn() {
@@ -286,6 +284,7 @@ impl SteamUtils {
.collect();
match sort_method {
GameSortMethod::None => {}
GameSortMethod::NameAsc => {
games.sort_by(|a, b| a.name.cmp(&b.name));
}

View File

@@ -0,0 +1,23 @@
use wgui::{
i18n::Translation,
layout::{Layout, WidgetID},
renderer_vk::text::TextStyle,
widget::label::{WidgetLabel, WidgetLabelParams},
};
pub fn create_label(layout: &mut Layout, parent: WidgetID, content: Translation) -> anyhow::Result<()> {
let label = WidgetLabel::create(
&mut layout.state.globals.get(),
WidgetLabelParams {
content,
style: TextStyle {
wrap: true,
..Default::default()
},
},
);
layout.add_child(parent, label, Default::default())?;
Ok(())
}

View File

@@ -180,7 +180,7 @@ impl View {
let tasks = tasks.clone();
Box::new(move |_, ev| {
if let Some(mode) = ev.value.and_then(|v| {
CompositorMode::from_str(&*v)
CompositorMode::from_str(&v)
.inspect_err(|_| {
log::error!(
"Invalid value for compositor: '{v}'. Valid values are: {:?}",
@@ -199,7 +199,7 @@ impl View {
let tasks = tasks.clone();
Box::new(move |_, ev| {
if let Some(mode) = ev.value.and_then(|v| {
ResMode::from_str(&*v)
ResMode::from_str(&v)
.inspect_err(|_| {
log::error!(
"Invalid value for resolution: '{v}'. Valid values are: {:?}",
@@ -237,7 +237,7 @@ impl View {
let tasks = tasks.clone();
Box::new(move |_, ev| {
if let Some(mode) = ev.value.and_then(|v| {
OrientationMode::from_str(&*v)
OrientationMode::from_str(&v)
.inspect_err(|_| {
log::error!(
"Invalid value for orientation: '{v}'. Valid values are: {:?}",

View File

@@ -55,7 +55,6 @@ pub struct View {
frontend_tasks: FrontendTasks,
globals: WguiGlobals,
id_list_parent: WidgetID,
steam_utils: steam_utils::SteamUtils,
cells: HashMap<AppID, Cell>,
game_cover_view_common: game_cover::ViewCommon,
executor: AsyncExecutor,
@@ -75,8 +74,6 @@ impl View {
let tasks = Tasks::new();
let steam_utils = SteamUtils::new()?;
tasks.push(Task::Refresh);
Ok(Self {
@@ -85,7 +82,6 @@ impl View {
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 })),
@@ -93,7 +89,12 @@ impl View {
})
}
pub fn update(&mut self, layout: &mut Layout, executor: &AsyncExecutor) -> anyhow::Result<()> {
pub fn update(
&mut self,
layout: &mut Layout,
steam_utils: &mut SteamUtils,
executor: &AsyncExecutor,
) -> anyhow::Result<()> {
loop {
let tasks = self.tasks.drain();
if tasks.is_empty() {
@@ -101,7 +102,7 @@ impl View {
}
for task in tasks {
match task {
Task::Refresh => self.refresh(layout, executor)?,
Task::Refresh => self.refresh(layout, steam_utils, 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,
@@ -162,20 +163,23 @@ fn fill_game_list(
}
impl View {
fn game_list(&self) -> anyhow::Result<Games> {
let manifests = self
.steam_utils
.list_installed_games(steam_utils::GameSortMethod::PlayDateDesc)?;
fn game_list(&self, steam_utils: &mut SteamUtils) -> anyhow::Result<Games> {
let manifests = steam_utils.list_installed_games(steam_utils::GameSortMethod::PlayDateDesc)?;
Ok(Games { manifests })
}
fn refresh(&mut self, layout: &mut Layout, executor: &AsyncExecutor) -> anyhow::Result<()> {
fn refresh(
&mut self,
layout: &mut Layout,
steam_utils: &mut SteamUtils,
executor: &AsyncExecutor,
) -> anyhow::Result<()> {
layout.remove_children(self.id_list_parent);
self.cells.clear();
let mut text: Option<Translation> = None;
match self.game_list() {
match self.game_list(steam_utils) {
Ok(list) => {
if list.manifests.is_empty() {
text = Some(Translation::from_translation_key("GAME_LIST.NO_GAMES_FOUND"))

View File

@@ -3,3 +3,4 @@ pub mod audio_settings;
pub mod game_cover;
pub mod game_launcher;
pub mod game_list;
pub mod running_games_list;

View File

@@ -0,0 +1,178 @@
use wgui::{
assets::AssetPath,
components::button::ComponentButton,
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState},
task::Tasks,
widget::label::WidgetLabel,
};
use crate::{
frontend::{FrontendTask, FrontendTasks},
util::{
steam_utils::{self, AppID, AppManifest, GameSortMethod, SteamUtils},
wgui_simple,
},
};
#[derive(Clone)]
enum Task {
Refresh,
StopGame(AppID, bool /* kill */),
}
pub struct Params<'a> {
pub globals: WguiGlobals,
pub layout: &'a mut Layout,
pub parent_id: WidgetID,
pub steam_utils: &'a mut SteamUtils,
pub frontend_tasks: FrontendTasks,
}
pub struct View {
#[allow(dead_code)]
state: ParserState,
tasks: Tasks<Task>,
last_update_ms: u32,
id_list_parent: WidgetID,
installed_games: Vec<AppManifest>,
frontend_tasks: FrontendTasks,
}
fn doc_params(globals: WguiGlobals) -> ParseDocumentParams<'static> {
ParseDocumentParams {
globals,
path: AssetPath::BuiltIn("gui/view/running_games_list.xml"),
extra: Default::default(),
}
}
impl View {
pub fn new(params: Params) -> anyhow::Result<Self> {
let state = wgui::parser::parse_from_assets(&doc_params(params.globals.clone()), params.layout, params.parent_id)?;
let btn_refresh = state.fetch_component_as::<ComponentButton>("btn_refresh")?;
let id_list_parent = state.get_widget_id("list_parent")?;
let installed_games = params
.steam_utils
.list_installed_games(GameSortMethod::None)
.unwrap_or(Vec::new());
let tasks = Tasks::<Task>::new();
tasks.handle_button(&btn_refresh, Task::Refresh);
tasks.push(Task::Refresh);
Ok(Self {
state,
tasks,
last_update_ms: 0,
id_list_parent,
installed_games,
frontend_tasks: params.frontend_tasks,
})
}
pub fn update(&mut self, layout: &mut Layout, time_ms: u32) -> anyhow::Result<()> {
loop {
let tasks = self.tasks.drain();
if tasks.is_empty() {
break;
}
for task in tasks {
match task {
Task::Refresh => self.refresh(layout)?,
Task::StopGame(app_id, kill) => self.stop_game(app_id, kill),
}
}
}
if self.last_update_ms + 5000 < time_ms {
self.last_update_ms = time_ms;
self.tasks.push(Task::Refresh);
}
Ok(())
}
fn extract_name_from_appid<'a>(app_id: &AppID, manifests: &[AppManifest]) -> String {
for manifest in manifests {
if manifest.app_id == *app_id {
return manifest.name.clone();
}
}
format!("Unknown AppID {}", app_id)
}
fn fill_list(&mut self, layout: &mut Layout, games: Vec<steam_utils::RunningGame>) -> anyhow::Result<()> {
if games.is_empty() {
wgui_simple::create_label(
layout,
self.id_list_parent,
Translation::from_translation_key("GAME_LIST.NO_RUNNING_GAME_FOUND"),
)?;
return Ok(());
}
for game in games {
let game_name = View::extract_name_from_appid(&game.app_id, &self.installed_games);
let t = self.state.parse_template(
&doc_params(layout.state.globals.clone()),
"RunningGameCell",
layout,
self.id_list_parent,
Default::default(),
)?;
let mut label_name = t.fetch_widget_as::<WidgetLabel>(&layout.state, "label_name")?;
self.tasks.handle_button(
&t.fetch_component_as::<ComponentButton>("btn_stop")?,
Task::StopGame(game.app_id.clone(), false),
);
self.tasks.handle_button(
&t.fetch_component_as::<ComponentButton>("btn_kill")?,
Task::StopGame(game.app_id, true),
);
label_name.set_text_simple(
&mut layout.state.globals.get(),
Translation::from_raw_text_string(game_name),
);
}
Ok(())
}
fn refresh(&mut self, layout: &mut Layout) -> anyhow::Result<()> {
log::debug!("refreshing running games list");
layout.remove_children(self.id_list_parent);
match steam_utils::list_running_games() {
Ok(games) => self.fill_list(layout, games)?,
Err(e) => {
log::error!("failed to list games: {}", e);
}
}
Ok(())
}
fn stop_game(&mut self, app_id: AppID, kill: bool) {
if let Err(e) = steam_utils::stop(app_id, kill) {
self
.frontend_tasks
.push(FrontendTask::PushToast(Translation::from_raw_text_string(format!(
"Error: {}",
e
))));
}
}
}