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

@@ -1,8 +1,10 @@
<layout>
<include src="t_tab_title.xml" />
<include src="../t_group_box.xml" />
<elements>
<TabTitle translation="GAMES" icon="dashboard/games.svg" />
<rectangle macro="group_box" id="running_games_list_parent" align_self="center" />
<div id="game_list_parent" align_items="center" />
</elements>
</layout>

View File

@@ -31,7 +31,7 @@
<label translation="DISPLAY_BRIGHTNESS" />
<Slider id="slider_brightness" width="300" height="24" min_value="0" max_value="140" />
<label translation="LIST_OF_PROCESSES" />
<label translation="PROCESS_LIST" />
<div id="list_parent" flex_direction="column" gap="8">
<!-- filled at runtime -->
</div>

View File

@@ -1,7 +1,7 @@
<layout>
<include src="../t_group_box.xml" />
<elements>
<div id="list_parent" gap="8" flex_direction="row" flex_wrap="wrap" justify_content="center" />
<div flex_direction="column">
<div id="list_parent" gap="8" flex_direction="row" flex_wrap="wrap" justify_content="center" />
</div>
</elements>
</layout>

View File

@@ -0,0 +1,20 @@
<layout>
<include src="../t_group_box.xml" />
<template name="RunningGameCell">
<rectangle macro="group_box" flex_direction="row">
<Button id="btn_stop" sprite_src_builtin="dashboard/remove_circle.svg" tooltip="PROCESS.STOP" />
<Button id="btn_kill" sprite_src_builtin="dashboard/knife.svg" tooltip="PROCESS.FORCE_KILL" />
<label id="label_name" weight="bold" />
</rectangle>
</template>
<elements>
<div align_items="center" gap="8">
<Button id="btn_refresh" tooltip="REFRESH" width="32" height="32" sprite_src_builtin="dashboard/refresh.svg" />
<sprite src_builtin="dashboard/cpu.svg" width="24" height="24" />
<label translation="GAME_LIST.RUNNING_GAMES_LIST" />
</div>
<div id="list_parent" gap="8" />
</elements>
</layout>

View File

@@ -111,12 +111,13 @@
"HIDE": "Verbergen",
"REMOVE": "Entfernen",
"SHOW": "Anzeigen",
"PROCESS_LIST": {},
"FAILED_TO_LAUNCH_APPLICATION": "Fehler beim Starten der Anwendung:",
"APPLICATION_STARTED": "Anwendung gestartet",
"CLOSE_WINDOW": "Fenster schließen",
"GAME_LIST": {
"NO_GAMES_FOUND": "Keine Spiele gefunden"
"NO_GAMES_FOUND": "Keine Spiele gefunden",
"RUNNING_GAMES_LIST": "Liste der laufenden Spiele",
"NO_RUNNING_GAME_FOUND": "Kein laufendes Spiel gefunden"
},
"TERMINATE_PROCESS": "Prozess beenden",
"GAME_LAUNCHED": "Spiel gestartet",
@@ -146,5 +147,11 @@
"AUTOSTART": "Automatisch beim Start ausführen",
"LAUNCH": "Starten"
},
"DISPLAY_BRIGHTNESS": "Bildschirmhelligkeit"
"DISPLAY_BRIGHTNESS": "Bildschirmhelligkeit",
"PROCESS_LIST": "Prozessliste",
"REFRESH": "Aktualisieren",
"PROCESS": {
"STOP": "Stopp",
"FORCE_KILL": "Erzwinge Beenden"
}
}

View File

@@ -31,7 +31,7 @@
"APP_SETTINGS": {
"ALLOW_SLIDING": "Stick interaction during grab",
"ANIMATION_SPEED": "UI Animation speed",
"UI_GRADIENT_INTENSITY": "UI Gradient intensity",
"AUTOSTART_APPS": "Apps to run on startup",
"BLOCK_GAME_INPUT": "Block game input",
"BLOCK_GAME_INPUT_HELP": "Blocks all input when an overlay is hovered",
"BLOCK_GAME_INPUT_IGNORE_WATCH": "Ignore watch when blocking input",
@@ -52,6 +52,8 @@
"DOUBLE_CURSOR_FIX_HELP": "Enable this if you see 2 cursors",
"FEATURES": "Features",
"FOCUS_FOLLOWS_MOUSE_MODE": "Mouse move on trigger touch",
"HANDSFREE_POINTER": "Handsfree mode",
"HANDSFREE_POINTER_HELP": "Input to use when motion\ncontrollers are unavailable.\nLeft pinch is grab, right is click.",
"HIDE_GRAB_HELP": "Hide grab help",
"HIDE_USERNAME": "Hide username",
"INVERT_SCROLL_DIRECTION_X": "Invert horizontal scroll direction",
@@ -68,11 +70,11 @@
"NOTIFICATIONS_SOUND_ENABLED": "Notification sounds",
"OPAQUE_BACKGROUND": "Opaque background",
"OPTION": {
"NONE": "None",
"HMD_PINCH": "HMD + pinch",
"EYE_PINCH": "Eye + pinch",
"AUTO": "Automatic",
"AUTO_HELP": "ScreenCopy GPU if supported,\notherwise PipeWire GPU.",
"EYE_PINCH": "Eye + pinch",
"HMD_PINCH": "HMD + pinch",
"NONE": "None",
"PIPEWIRE_HELP": "Fast GPU capture,\nstandard on all desktops.",
"PW_FALLBACK_HELP": "Slow method with high CPU usage.\nTry in case PipeWire GPU doesn't work",
"SCREENCOPY_GPU_HELP": "Fast, no screen share popups.\nWorks on: Hyprland, Niri, River, Sway",
@@ -90,6 +92,7 @@
"SPACE_DRAG_UNLOCKED": "Allow space drag on all axes",
"SPACE_ROTATE_UNLOCKED": "Allow space rotate on all axes",
"TROUBLESHOOTING": "Troubleshooting",
"UI_GRADIENT_INTENSITY": "UI Gradient intensity",
"UPRIGHT_SCREEN_FIX": "Upright screen fix",
"UPRIGHT_SCREEN_FIX_HELP": "Fixes upright screens on some desktops",
"USE_PASSTHROUGH": "Enable passthrough",
@@ -100,10 +103,7 @@
"XR_CLICK_SENSITIVITY_HELP": "Analog trigger sensitivity",
"XR_CLICK_SENSITIVITY_RELEASE": "XR release sensitivity",
"XR_CLICK_SENSITIVITY_RELEASE_HELP": "Must be lower than click",
"XWAYLAND_BY_DEFAULT": "Run apps in Compatibility mode by default",
"HANDSFREE_POINTER": "Handsfree mode",
"HANDSFREE_POINTER_HELP": "Input to use when motion\ncontrollers are unavailable.\nLeft pinch is grab, right is click.",
"AUTOSTART_APPS": "Apps to run on startup"
"XWAYLAND_BY_DEFAULT": "Run apps in Compatibility mode by default"
},
"APPLICATION_LAUNCHER": "Application launcher",
"APPLICATION_STARTED": "Application started",
@@ -127,7 +127,9 @@
"FAILED_TO_LAUNCH_APPLICATION": "Failed to launch a application:",
"GAME_LAUNCHED": "Game launched",
"GAME_LIST": {
"NO_GAMES_FOUND": "No games found"
"NO_GAMES_FOUND": "No games found",
"RUNNING_GAMES_LIST": "List of running games",
"NO_RUNNING_GAME_FOUND": "No running game found"
},
"GAMES": "Games",
"GENERAL_SETTINGS": "General settings",
@@ -140,6 +142,12 @@
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "Resolution"
},
"PROCESS": {
"STOP": "Stop",
"FORCE_KILL": "Force-kill"
},
"PROCESS_LIST": "Process list",
"REFRESH": "Refresh",
"REMOVE": "Remove",
"SETTINGS": "Settings",
"SHOW": "Show",

View File

@@ -111,12 +111,13 @@
"HIDE": "Ocultar",
"REMOVE": "Eliminar",
"SHOW": "Mostrar",
"PROCESS_LIST": {},
"FAILED_TO_LAUNCH_APPLICATION": "No se pudo iniciar la aplicación:",
"APPLICATION_STARTED": "Aplicación iniciada",
"CLOSE_WINDOW": "Cerrar ventana",
"GAME_LIST": {
"NO_GAMES_FOUND": "No se encontraron juegos"
"NO_GAMES_FOUND": "No se encontraron juegos",
"RUNNING_GAMES_LIST": "Lista de juegos en ejecución",
"NO_RUNNING_GAME_FOUND": "No se encontró ningún juego en ejecución"
},
"TERMINATE_PROCESS": "Finalizar proceso",
"GAME_LAUNCHED": "Juego lanzado",
@@ -146,5 +147,11 @@
"AUTOSTART": "Ejecutar automáticamente al inicio",
"LAUNCH": "Iniciar"
},
"DISPLAY_BRIGHTNESS": "Brillo de la pantalla"
"DISPLAY_BRIGHTNESS": "Brillo de la pantalla",
"PROCESS_LIST": "Lista de procesos",
"REFRESH": "Actualizar",
"PROCESS": {
"STOP": "Detener",
"FORCE_KILL": "Forzar cierre"
}
}

View File

@@ -127,7 +127,9 @@
"FAILED_TO_LAUNCH_APPLICATION": "Impossibile avviare l'applicazione:",
"GAME_LAUNCHED": "Gioco lanciato",
"GAME_LIST": {
"NO_GAMES_FOUND": "Nessun gioco trovato"
"NO_GAMES_FOUND": "Nessun gioco trovato",
"RUNNING_GAMES_LIST": "Lista dei giochi in esecuzione",
"NO_RUNNING_GAME_FOUND": "Nessun gioco in esecuzione trovato"
},
"GAMES": "Giochi",
"GENERAL_SETTINGS": "Impostazioni generali",
@@ -140,10 +142,15 @@
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "Risoluzione"
},
"PROCESS_LIST": {},
"REMOVE": "Rimuovi",
"SETTINGS": "Impostazioni",
"SHOW": "Mostra",
"TERMINATE_PROCESS": "Termina processo",
"WIDTH": "Larghezza"
"WIDTH": "Larghezza",
"PROCESS_LIST": "Elenco processi",
"REFRESH": "Aggiorna",
"PROCESS": {
"STOP": "Interrompi",
"FORCE_KILL": "Uccidi forzatamente"
}
}

View File

@@ -111,12 +111,13 @@
"HIDE": "隠す",
"REMOVE": "削除",
"SHOW": "表示",
"PROCESS_LIST": {},
"FAILED_TO_LAUNCH_APPLICATION": "アプリケーションの起動に失敗しました:",
"APPLICATION_STARTED": "アプリケーションが起動しました",
"CLOSE_WINDOW": "ウィンドウを閉じる",
"GAME_LIST": {
"NO_GAMES_FOUND": "ゲームが見つかりませんでした"
"NO_GAMES_FOUND": "ゲームが見つかりませんでした",
"RUNNING_GAMES_LIST": "実行中のゲーム一覧",
"NO_RUNNING_GAME_FOUND": "実行中のゲームが見つかりません"
},
"TERMINATE_PROCESS": "プロセスを終了する",
"GAME_LAUNCHED": "ゲームが起動しました",
@@ -146,5 +147,11 @@
"AUTOSTART": "起動時に自動実行",
"LAUNCH": "起動"
},
"DISPLAY_BRIGHTNESS": "ディスプレイの明るさ"
"DISPLAY_BRIGHTNESS": "ディスプレイの明るさ",
"PROCESS_LIST": "プロセスリスト",
"REFRESH": "更新",
"PROCESS": {
"STOP": "停止",
"FORCE_KILL": "強制終了"
}
}

View File

@@ -111,12 +111,13 @@
"SETTINGS": "Ustawienia",
"SHOW": "Pokaż",
"WIDTH": "Szerokość",
"PROCESS_LIST": {},
"FAILED_TO_LAUNCH_APPLICATION": "Nie udało się uruchomić aplikacji:",
"APPLICATION_STARTED": "Aplikacja uruchomiona",
"CLOSE_WINDOW": "Zamknij okno",
"GAME_LIST": {
"NO_GAMES_FOUND": "Nie znaleziono gier"
"NO_GAMES_FOUND": "Nie znaleziono gier",
"RUNNING_GAMES_LIST": "Lista uruchomionych gier",
"NO_RUNNING_GAME_FOUND": "Nie znaleziono uruchomionej gry"
},
"TERMINATE_PROCESS": "Zakończ proces",
"GAME_LAUNCHED": "Gra uruchomiona",
@@ -146,5 +147,11 @@
"AUTOSTART": "Uruchom automatycznie przy starcie",
"LAUNCH": "Uruchom"
},
"DISPLAY_BRIGHTNESS": "Jasność wyświetlacza"
"DISPLAY_BRIGHTNESS": "Jasność wyświetlacza",
"PROCESS_LIST": "Lista procesów",
"REFRESH": "Odśwież",
"PROCESS": {
"STOP": "Zatrzymaj",
"FORCE_KILL": "Wymuś zakończenie"
}
}

View File

@@ -127,7 +127,9 @@
"FAILED_TO_LAUNCH_APPLICATION": "启动应用失败:",
"GAME_LAUNCHED": "游戏已启动",
"GAME_LIST": {
"NO_GAMES_FOUND": "未找到游戏"
"NO_GAMES_FOUND": "未找到游戏",
"RUNNING_GAMES_LIST": "正在运行的游戏列表",
"NO_RUNNING_GAME_FOUND": "未找到正在运行的游戏"
},
"GAMES": "游戏",
"GENERAL_SETTINGS": "通用设置",
@@ -140,10 +142,15 @@
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "分辨率"
},
"PROCESS_LIST": {},
"REMOVE": "移除",
"SETTINGS": "设置",
"SHOW": "显示",
"TERMINATE_PROCESS": "终止进程",
"WIDTH": "宽度"
"WIDTH": "宽度",
"PROCESS_LIST": "进程列表",
"REFRESH": "刷新",
"PROCESS": {
"STOP": "停止",
"FORCE_KILL": "强制关闭"
}
}

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
))));
}
}
}