dash-frontend: games: pagination

This commit is contained in:
Aleksander
2026-01-27 19:49:11 +01:00
parent 19083cfd83
commit d97bbfc796
20 changed files with 224 additions and 130 deletions

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE -->
<path fill="currentColor" d="m10 18l-6-6l6-6l1.4 1.45L7.85 11H20v2H7.85l3.55 3.55z" />
</svg>

After

Width:  |  Height:  |  Size: 294 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE -->
<path fill="currentColor" d="m14 18l-1.4-1.45L16.15 13H4v-2h12.15L12.6 7.45L14 6l6 6z" />
</svg>

After

Width:  |  Height:  |  Size: 297 B

View File

@@ -66,7 +66,7 @@
<!-- Title bar --> <!-- Title bar -->
<div width="100%" align_items="center" justify_content="center"> <div width="100%" align_items="center" justify_content="center">
<rectangle min_width="300" height="100%" consume_mouse_events="1" round="100%" color="~color_top_panel" align_items="center" justify_content="center" gap="8"> <rectangle min_width="300" height="100%" consume_mouse_events="1" round="100%" color="~color_top_panel" align_items="center" justify_content="center" gap="8">
<sprite id="sprite_titlebar_icon" width="28" height="28" /> <sprite id="sprite_titlebar_icon" width="24" height="24" />
<label id="label_titlebar_title" weight="bold" size="16" text="Title" /> <label id="label_titlebar_title" weight="bold" size="16" text="Title" />
</rectangle> </rectangle>
</div> </div>
@@ -98,10 +98,7 @@
flex_direction="column" flex_direction="column"
overflow_x="scroll" overflow_x="scroll"
overflow_y="scroll" overflow_y="scroll"
padding_top="8" padding="16"
padding_bottom="8"
padding_left="16"
padding_right="16"
gap="8" gap="8"
width="100%" width="100%"
min_height="100%" min_height="100%"

View File

@@ -1,8 +1,6 @@
<layout> <layout>
<include src="../t_group_box.xml" />
<elements> <elements>
<rectangle macro="group_box" id="running_games_list_parent" align_self="center" /> <div id="running_games_list_parent" align_self="center" />
<div id="game_list_parent" align_items="center" /> <div id="game_list_parent" align_items="center" flex_direction="column" gap="8" overflow_y="scroll" />
</elements> </elements>
</layout> </layout>

View File

@@ -1,7 +1,12 @@
<layout> <layout>
<elements> <elements>
<div flex_direction="column"> <div flex_direction="column" padding_top="8" padding_bottom="8">
<div id="list_parent" gap="8" flex_direction="row" flex_wrap="wrap" justify_content="center" /> <div id="list_parent" gap="8" flex_direction="row" flex_wrap="wrap" justify_content="center" />
</div> </div>
<div align_items="center" justify_content="center" gap="16">
<Button id="btn_prev" sprite_src_builtin="dashboard/arrow_left.svg" width="32" height="32" />
<label id="label_page" text="Page X" weight="bold" />
<Button id="btn_next" sprite_src_builtin="dashboard/arrow_right.svg" width="32" height="32" />
</div>
</elements> </elements>
</layout> </layout>

View File

@@ -78,12 +78,16 @@
"SCREENCOPY_HELP": "Langsam, keine Bildschirmfreigabe-Popups.\nFunktioniert mit: Hyprland, Niri, River, Sway", "SCREENCOPY_HELP": "Langsam, keine Bildschirmfreigabe-Popups.\nFunktioniert mit: Hyprland, Niri, River, Sway",
"NONE": "Keine", "NONE": "Keine",
"HMD_PINCH": "HMD + Kneifen", "HMD_PINCH": "HMD + Kneifen",
"EYE_PINCH": "Auge + Kneifen" "EYE_PINCH": "Auge + Kneifen",
"EYE_ONLY": "Nur Auge",
"HMD_ONLY": "Nur HMD"
}, },
"AUTOSTART_APPS": "Anwendungen, die beim Start ausgeführt werden sollen", "AUTOSTART_APPS": "Anwendungen, die beim Start ausgeführt werden sollen",
"HANDSFREE_POINTER": "Freihändige Modus", "HANDSFREE_POINTER": "Freihändige Modus",
"HANDSFREE_POINTER_HELP": "Eingabe, die bei Bewegung\nder Controller verwendet wird, wenn diese nicht verfügbar sind.\nLinkes Kneifen greift, rechtes klickt.", "HANDSFREE_POINTER_HELP": "Eingabe, die bei Bewegung\nder Controller verwendet wird, wenn diese nicht verfügbar sind.\nLinkes Kneifen greift, rechtes klickt.",
"UI_GRADIENT_INTENSITY": "UI-Verlaufsintensität" "UI_GRADIENT_INTENSITY": "UI-Verlaufsintensität",
"RESET_PLAYSPACE": "Spielbereich zurücksetzen",
"RESET_PLAYSPACE_HELP": "Den Abstand des Spielbereichs zurücksetzen."
}, },
"HELLO": "Hallo!", "HELLO": "Hallo!",
"AUDIO": { "AUDIO": {
@@ -116,8 +120,7 @@
"CLOSE_WINDOW": "Fenster schließen", "CLOSE_WINDOW": "Fenster schließen",
"GAME_LIST": { "GAME_LIST": {
"NO_GAMES_FOUND": "Keine Spiele gefunden", "NO_GAMES_FOUND": "Keine Spiele gefunden",
"RUNNING_GAMES_LIST": "Liste der laufenden Spiele", "RUNNING_GAMES_LIST": "Liste der laufenden Spiele"
"NO_RUNNING_GAME_FOUND": "Kein laufendes Spiel gefunden"
}, },
"TERMINATE_PROCESS": "Prozess beenden", "TERMINATE_PROCESS": "Prozess beenden",
"GAME_LAUNCHED": "Spiel gestartet", "GAME_LAUNCHED": "Spiel gestartet",

View File

@@ -132,8 +132,7 @@
"GAME_LAUNCHED": "Game launched", "GAME_LAUNCHED": "Game launched",
"GAME_LIST": { "GAME_LIST": {
"NO_GAMES_FOUND": "No games found", "NO_GAMES_FOUND": "No games found",
"RUNNING_GAMES_LIST": "List of running games", "RUNNING_GAMES_LIST": "List of running games"
"NO_RUNNING_GAME_FOUND": "No running game found"
}, },
"GAMES": "Games", "GAMES": "Games",
"GENERAL_SETTINGS": "General settings", "GENERAL_SETTINGS": "General settings",

View File

@@ -78,12 +78,16 @@
"SCREENCOPY_HELP": "Lento, sin ventanas emergentes de uso compartido de pantalla.\nFunciona en: Hyprland, Niri, River, Sway", "SCREENCOPY_HELP": "Lento, sin ventanas emergentes de uso compartido de pantalla.\nFunciona en: Hyprland, Niri, River, Sway",
"NONE": "Ninguno", "NONE": "Ninguno",
"HMD_PINCH": "HMD + pellizco", "HMD_PINCH": "HMD + pellizco",
"EYE_PINCH": "Ojo + pellizco" "EYE_PINCH": "Ojo + pellizco",
"EYE_ONLY": "Solo ojo",
"HMD_ONLY": "Solo HMD"
}, },
"AUTOSTART_APPS": "Aplicaciones a ejecutar al inicio", "AUTOSTART_APPS": "Aplicaciones a ejecutar al inicio",
"HANDSFREE_POINTER": "Modo manos libres", "HANDSFREE_POINTER": "Modo manos libres",
"HANDSFREE_POINTER_HELP": "Entrada a utilizar cuando no\nestén disponibles los mandos de movimiento.\nPellizco con la izquierda para agarrar, con la derecha para hacer clic.", "HANDSFREE_POINTER_HELP": "Entrada a utilizar cuando no\nestén disponibles los mandos de movimiento.\nPellizco con la izquierda para agarrar, con la derecha para hacer clic.",
"UI_GRADIENT_INTENSITY": "Intensidad del degradado de la IU" "UI_GRADIENT_INTENSITY": "Intensidad del degradado de la IU",
"RESET_PLAYSPACE": "Restablecer espacio de juego",
"RESET_PLAYSPACE_HELP": "Borrar el desplazamiento del espacio de juego."
}, },
"HELLO": "¡Hola!", "HELLO": "¡Hola!",
"AUDIO": { "AUDIO": {
@@ -116,8 +120,7 @@
"CLOSE_WINDOW": "Cerrar ventana", "CLOSE_WINDOW": "Cerrar ventana",
"GAME_LIST": { "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", "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", "TERMINATE_PROCESS": "Finalizar proceso",
"GAME_LAUNCHED": "Juego lanzado", "GAME_LAUNCHED": "Juego lanzado",

View File

@@ -75,7 +75,9 @@
"SCREENCOPY_HELP": "Lento, nessuna finestra pop-up per la condivisione dello schermo.\nFunziona su: Hyprland, Niri, River, Sway", "SCREENCOPY_HELP": "Lento, nessuna finestra pop-up per la condivisione dello schermo.\nFunziona su: Hyprland, Niri, River, Sway",
"NONE": "Nessuno", "NONE": "Nessuno",
"HMD_PINCH": "HMD + pizzico", "HMD_PINCH": "HMD + pizzico",
"EYE_PINCH": "Occhio + pizzico" "EYE_PINCH": "Occhio + pizzico",
"EYE_ONLY": "Solo occhio",
"HMD_ONLY": "Solo HMD"
}, },
"POINTER_LERP_FACTOR": "Smussamento puntatore", "POINTER_LERP_FACTOR": "Smussamento puntatore",
"RESTART_SOFTWARE": "Riavvia il software", "RESTART_SOFTWARE": "Riavvia il software",
@@ -103,7 +105,9 @@
"AUTOSTART_APPS": "App da avviare all'avvio", "AUTOSTART_APPS": "App da avviare all'avvio",
"HANDSFREE_POINTER": "Modalità a mani libere", "HANDSFREE_POINTER": "Modalità a mani libere",
"HANDSFREE_POINTER_HELP": "Input da usare quando i\ncontroller di movimento non sono disponibili.\nPizzico sinistro per afferrare, destro per cliccare.", "HANDSFREE_POINTER_HELP": "Input da usare quando i\ncontroller di movimento non sono disponibili.\nPizzico sinistro per afferrare, destro per cliccare.",
"UI_GRADIENT_INTENSITY": "Intensità gradiente dell'interfaccia utente" "UI_GRADIENT_INTENSITY": "Intensità gradiente dell'interfaccia utente",
"RESET_PLAYSPACE": "Ripristina playspace",
"RESET_PLAYSPACE_HELP": "Cancella l'offset dello spazio di gioco."
}, },
"APPLICATION_LAUNCHER": "Lanciatore applicazioni", "APPLICATION_LAUNCHER": "Lanciatore applicazioni",
"APPLICATION_STARTED": "Applicazione avviata", "APPLICATION_STARTED": "Applicazione avviata",
@@ -128,8 +132,7 @@
"GAME_LAUNCHED": "Gioco lanciato", "GAME_LAUNCHED": "Gioco lanciato",
"GAME_LIST": { "GAME_LIST": {
"NO_GAMES_FOUND": "Nessun gioco trovato", "NO_GAMES_FOUND": "Nessun gioco trovato",
"RUNNING_GAMES_LIST": "Lista dei giochi in esecuzione", "RUNNING_GAMES_LIST": "Lista dei giochi in esecuzione"
"NO_RUNNING_GAME_FOUND": "Nessun gioco in esecuzione trovato"
}, },
"GAMES": "Giochi", "GAMES": "Giochi",
"GENERAL_SETTINGS": "Impostazioni generali", "GENERAL_SETTINGS": "Impostazioni generali",

View File

@@ -78,12 +78,16 @@
"SCREENCOPY_HELP": "遅延あり、画面共有ポップアップなし。\n動作する環境: Hyprland, Niri, River, Sway", "SCREENCOPY_HELP": "遅延あり、画面共有ポップアップなし。\n動作する環境: Hyprland, Niri, River, Sway",
"NONE": "なし", "NONE": "なし",
"HMD_PINCH": "HMD + ピンチ", "HMD_PINCH": "HMD + ピンチ",
"EYE_PINCH": "つまんで目を合わせる" "EYE_PINCH": "つまんで目を合わせる",
"EYE_ONLY": "視野のみ",
"HMD_ONLY": "HMDのみ"
}, },
"AUTOSTART_APPS": "起動時に実行するアプリ", "AUTOSTART_APPS": "起動時に実行するアプリ",
"HANDSFREE_POINTER": "ハンズフリーモード", "HANDSFREE_POINTER": "ハンズフリーモード",
"HANDSFREE_POINTER_HELP": "モーションコントローラーが利用できない場合の入力方法。\n左手のピンチは掴み、右手のピンチはクリックです。", "HANDSFREE_POINTER_HELP": "モーションコントローラーが利用できない場合の入力方法。\n左手のピンチは掴み、右手のピンチはクリックです。",
"UI_GRADIENT_INTENSITY": "UIグラデーションの強さ" "UI_GRADIENT_INTENSITY": "UIグラデーションの強さ",
"RESET_PLAYSPACE": "プレイエリアをリセット",
"RESET_PLAYSPACE_HELP": "プレイエリアのオフセットをクリアします。"
}, },
"HELLO": "こんにちは!", "HELLO": "こんにちは!",
"AUDIO": { "AUDIO": {
@@ -116,8 +120,7 @@
"CLOSE_WINDOW": "ウィンドウを閉じる", "CLOSE_WINDOW": "ウィンドウを閉じる",
"GAME_LIST": { "GAME_LIST": {
"NO_GAMES_FOUND": "ゲームが見つかりませんでした", "NO_GAMES_FOUND": "ゲームが見つかりませんでした",
"RUNNING_GAMES_LIST": "実行中のゲーム一覧", "RUNNING_GAMES_LIST": "実行中のゲーム一覧"
"NO_RUNNING_GAME_FOUND": "実行中のゲームが見つかりません"
}, },
"TERMINATE_PROCESS": "プロセスを終了する", "TERMINATE_PROCESS": "プロセスを終了する",
"GAME_LAUNCHED": "ゲームが起動しました", "GAME_LAUNCHED": "ゲームが起動しました",

View File

@@ -73,12 +73,16 @@
"SCREENCOPY_HELP": "Wolne, bez wyskakujących okienek udostępniania ekranu.\nDziała na: Hyprland, Niri, River, Sway", "SCREENCOPY_HELP": "Wolne, bez wyskakujących okienek udostępniania ekranu.\nDziała na: Hyprland, Niri, River, Sway",
"NONE": "Brak", "NONE": "Brak",
"HMD_PINCH": "HMD + szczyknięcie", "HMD_PINCH": "HMD + szczyknięcie",
"EYE_PINCH": "Ściśnięcie palcami + oko" "EYE_PINCH": "Ściśnięcie palcami + oko",
"EYE_ONLY": "Tylko oko",
"HMD_ONLY": "Tylko HMD"
}, },
"AUTOSTART_APPS": "Aplikacje do uruchomienia przy starcie", "AUTOSTART_APPS": "Aplikacje do uruchomienia przy starcie",
"HANDSFREE_POINTER": "Tryb bez użycia rąk", "HANDSFREE_POINTER": "Tryb bez użycia rąk",
"HANDSFREE_POINTER_HELP": "Wejście do użycia, gdy kontrolery ruchu\nsą niedostępne. Lewy szczyptak to chwyt,\nprawy to kliknięcie.", "HANDSFREE_POINTER_HELP": "Wejście do użycia, gdy kontrolery ruchu\nsą niedostępne. Lewy szczyptak to chwyt,\nprawy to kliknięcie.",
"UI_GRADIENT_INTENSITY": "Intensywność gradientu UI" "UI_GRADIENT_INTENSITY": "Intensywność gradientu UI",
"RESET_PLAYSPACE": "Zresetuj przestrzeń gry",
"RESET_PLAYSPACE_HELP": "Wyczyść przesunięcie przestrzeni gry."
}, },
"APPLICATION_LAUNCHER": "Uruchamiacz aplikacji", "APPLICATION_LAUNCHER": "Uruchamiacz aplikacji",
"APPLICATIONS": "Aplikacje", "APPLICATIONS": "Aplikacje",
@@ -116,8 +120,7 @@
"CLOSE_WINDOW": "Zamknij okno", "CLOSE_WINDOW": "Zamknij okno",
"GAME_LIST": { "GAME_LIST": {
"NO_GAMES_FOUND": "Nie znaleziono gier", "NO_GAMES_FOUND": "Nie znaleziono gier",
"RUNNING_GAMES_LIST": "Lista uruchomionych gier", "RUNNING_GAMES_LIST": "Lista uruchomionych gier"
"NO_RUNNING_GAME_FOUND": "Nie znaleziono uruchomionej gry"
}, },
"TERMINATE_PROCESS": "Zakończ proces", "TERMINATE_PROCESS": "Zakończ proces",
"GAME_LAUNCHED": "Gra uruchomiona", "GAME_LAUNCHED": "Gra uruchomiona",

View File

@@ -75,7 +75,9 @@
"SCREENCOPY_HELP": "慢速,无屏幕共享弹窗。\n支持Hyprland, Niri, River, Sway", "SCREENCOPY_HELP": "慢速,无屏幕共享弹窗。\n支持Hyprland, Niri, River, Sway",
"NONE": "无", "NONE": "无",
"HMD_PINCH": "HMD + 捏合", "HMD_PINCH": "HMD + 捏合",
"EYE_PINCH": "眼睛 + 捏合" "EYE_PINCH": "眼睛 + 捏合",
"EYE_ONLY": "仅眼球",
"HMD_ONLY": "仅限头显"
}, },
"POINTER_LERP_FACTOR": "指针平滑", "POINTER_LERP_FACTOR": "指针平滑",
"RESTART_SOFTWARE": "重启软件", "RESTART_SOFTWARE": "重启软件",
@@ -103,7 +105,9 @@
"AUTOSTART_APPS": "开机启动应用", "AUTOSTART_APPS": "开机启动应用",
"HANDSFREE_POINTER": "免提模式", "HANDSFREE_POINTER": "免提模式",
"HANDSFREE_POINTER_HELP": "当运动控制器不可用时使用的输入。\n左手捏合为抓取右手为点击。", "HANDSFREE_POINTER_HELP": "当运动控制器不可用时使用的输入。\n左手捏合为抓取右手为点击。",
"UI_GRADIENT_INTENSITY": "UI 渐变强度" "UI_GRADIENT_INTENSITY": "UI 渐变强度",
"RESET_PLAYSPACE": "重置游戏空间",
"RESET_PLAYSPACE_HELP": "清除舞台空间偏移。"
}, },
"APPLICATION_LAUNCHER": "应用启动器", "APPLICATION_LAUNCHER": "应用启动器",
"APPLICATION_STARTED": "应用已启动", "APPLICATION_STARTED": "应用已启动",
@@ -128,8 +132,7 @@
"GAME_LAUNCHED": "游戏已启动", "GAME_LAUNCHED": "游戏已启动",
"GAME_LIST": { "GAME_LIST": {
"NO_GAMES_FOUND": "未找到游戏", "NO_GAMES_FOUND": "未找到游戏",
"RUNNING_GAMES_LIST": "正在运行的游戏列表", "RUNNING_GAMES_LIST": "正在运行的游戏列表"
"NO_RUNNING_GAME_FOUND": "未找到正在运行的游戏"
}, },
"GAMES": "游戏", "GAMES": "游戏",
"GENERAL_SETTINGS": "通用设置", "GENERAL_SETTINGS": "通用设置",

View File

@@ -1,6 +1,5 @@
use std::{path::PathBuf, rc::Rc}; use std::{path::PathBuf, rc::Rc};
use anyhow::Context;
use chrono::Timelike; use chrono::Timelike;
use glam::Vec2; use glam::Vec2;
use wgui::{ use wgui::{

View File

@@ -2,6 +2,7 @@ use std::{cell::RefCell, collections::HashMap, rc::Rc};
use wgui::{ use wgui::{
assets::AssetPath, assets::AssetPath,
components::button::ComponentButton,
globals::WguiGlobals, globals::WguiGlobals,
i18n::Translation, i18n::Translation,
layout::{Layout, WidgetID}, layout::{Layout, WidgetID},
@@ -29,7 +30,10 @@ enum Task {
AppManifestClicked(steam_utils::AppManifest), AppManifestClicked(steam_utils::AppManifest),
SetCoverArt(AppID, Rc<CoverArt>), SetCoverArt(AppID, Rc<CoverArt>),
CloseLauncher, CloseLauncher,
Refresh, LoadManifests,
FillPage(u32),
PrevPage,
NextPage,
} }
pub struct Params<'a> { pub struct Params<'a> {
@@ -40,7 +44,9 @@ pub struct Params<'a> {
pub parent_id: WidgetID, pub parent_id: WidgetID,
} }
pub struct Cell { const MAX_GAMES_PER_PAGE: u32 = 30;
pub struct GameCoverCell {
view_cover: game_cover::View, view_cover: game_cover::View,
} }
@@ -55,10 +61,14 @@ pub struct View {
frontend_tasks: FrontendTasks, frontend_tasks: FrontendTasks,
globals: WguiGlobals, globals: WguiGlobals,
id_list_parent: WidgetID, id_list_parent: WidgetID,
cells: HashMap<AppID, Cell>,
game_cover_view_common: game_cover::ViewCommon, game_cover_view_common: game_cover::ViewCommon,
executor: AsyncExecutor, executor: AsyncExecutor,
state: Rc<RefCell<State>>, state: Rc<RefCell<State>>,
mounted_game_covers: HashMap<AppID, GameCoverCell>,
all_manifests: Vec<steam_utils::AppManifest>,
cur_page: u32,
page_count: u32,
id_label_page: WidgetID,
} }
impl View { impl View {
@@ -71,10 +81,21 @@ impl View {
let parser_state = wgui::parser::parse_from_assets(doc_params, params.layout, params.parent_id)?; let parser_state = wgui::parser::parse_from_assets(doc_params, params.layout, params.parent_id)?;
let list_parent = parser_state.fetch_widget(&params.layout.state, "list_parent")?; let list_parent = parser_state.fetch_widget(&params.layout.state, "list_parent")?;
let id_label_page = parser_state.get_widget_id("label_page")?;
let tasks = Tasks::new(); let tasks = Tasks::new();
tasks.push(Task::Refresh); tasks.handle_button(
&parser_state.fetch_component_as::<ComponentButton>("btn_prev")?,
Task::PrevPage,
);
tasks.handle_button(
&parser_state.fetch_component_as::<ComponentButton>("btn_next")?,
Task::NextPage,
);
tasks.push(Task::LoadManifests);
Ok(Self { Ok(Self {
parser_state, parser_state,
@@ -82,10 +103,14 @@ impl View {
frontend_tasks: params.frontend_tasks, frontend_tasks: params.frontend_tasks,
globals: params.globals.clone(), globals: params.globals.clone(),
id_list_parent: list_parent.id, id_list_parent: list_parent.id,
cells: HashMap::new(), mounted_game_covers: HashMap::new(),
game_cover_view_common: game_cover::ViewCommon::new(params.globals.clone()), game_cover_view_common: game_cover::ViewCommon::new(params.globals.clone()),
state: Rc::new(RefCell::new(State { view_launcher: None })), state: Rc::new(RefCell::new(State { view_launcher: None })),
executor: params.executor, executor: params.executor,
all_manifests: Vec::new(),
cur_page: 0,
page_count: 0,
id_label_page,
}) })
} }
@@ -102,10 +127,13 @@ impl View {
} }
for task in tasks { for task in tasks {
match task { match task {
Task::Refresh => self.refresh(layout, steam_utils, executor)?, Task::LoadManifests => self.load_manifests(steam_utils),
Task::FillPage(page_idx) => self.fill_page(layout, executor, page_idx)?,
Task::AppManifestClicked(manifest) => self.action_app_manifest_clicked(manifest)?, 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::SetCoverArt(app_id, cover_art) => self.set_cover_art(layout, app_id, cover_art),
Task::CloseLauncher => self.state.borrow_mut().view_launcher = None, Task::CloseLauncher => self.state.borrow_mut().view_launcher = None,
Task::PrevPage => self.page_prev(),
Task::NextPage => self.page_next(),
} }
} }
} }
@@ -119,18 +147,14 @@ impl View {
} }
} }
pub struct Games {
manifests: Vec<steam_utils::AppManifest>,
}
fn fill_game_list( fn fill_game_list(
ess: &mut ConstructEssentials, ess: &mut ConstructEssentials,
executor: &AsyncExecutor, executor: &AsyncExecutor,
cells: &mut HashMap<AppID, Cell>, mounted_game_covers: &mut HashMap<AppID, GameCoverCell>,
games: &Games, manifests: &[steam_utils::AppManifest],
tasks: &Tasks<Task>, tasks: &Tasks<Task>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
for manifest in &games.manifests { for manifest in manifests {
let on_loaded = { let on_loaded = {
let app_id = manifest.app_id.clone(); let app_id = manifest.app_id.clone();
let tasks = tasks.clone(); let tasks = tasks.clone();
@@ -156,49 +180,81 @@ fn fill_game_list(
}) })
}); });
cells.insert(manifest.app_id.clone(), Cell { view_cover }); mounted_game_covers.insert(manifest.app_id.clone(), GameCoverCell { view_cover });
} }
Ok(()) Ok(())
} }
impl View { impl View {
fn game_list(&self, steam_utils: &mut SteamUtils) -> anyhow::Result<Games> { fn load_manifests(&mut self, steam_utils: &mut SteamUtils) {
let manifests = steam_utils.list_installed_games(steam_utils::GameSortMethod::PlayDateDesc)?; match steam_utils.list_installed_games(steam_utils::GameSortMethod::PlayDateDesc) {
Ok(manifests) => {
Ok(Games { manifests }) self.page_count = (manifests.len() as u32 + MAX_GAMES_PER_PAGE) / MAX_GAMES_PER_PAGE;
self.all_manifests = manifests;
self.tasks.push(Task::FillPage(0));
}
Err(e) => {
log::error!("Failed to list installed games: {e:?}");
}
}
} }
fn refresh( fn page_prev(&mut self) {
&mut self, if self.cur_page == 0 {
layout: &mut Layout, return;
steam_utils: &mut SteamUtils, }
executor: &AsyncExecutor,
) -> anyhow::Result<()> { self.cur_page -= 1;
self.tasks.push(Task::FillPage(self.cur_page));
}
fn page_next(&mut self) {
if self.cur_page >= self.page_count - 1 {
return;
}
self.cur_page += 1;
self.tasks.push(Task::FillPage(self.cur_page));
}
fn fill_page(&mut self, layout: &mut Layout, executor: &AsyncExecutor, page_idx: u32) -> anyhow::Result<()> {
layout.remove_children(self.id_list_parent); layout.remove_children(self.id_list_parent);
self.cells.clear(); self.mounted_game_covers.clear();
let idx_from = (page_idx * MAX_GAMES_PER_PAGE).min(self.all_manifests.len() as u32);
let idx_to = ((page_idx + 1) * MAX_GAMES_PER_PAGE).min(self.all_manifests.len() as u32);
let page_manifests = &self.all_manifests[idx_from as usize..idx_to as usize];
let mut text: Option<Translation> = None; let mut text: Option<Translation> = None;
match self.game_list(steam_utils) {
Ok(list) => { if page_manifests.is_empty() {
if list.manifests.is_empty() { text = Some(Translation::from_translation_key("GAME_LIST.NO_GAMES_FOUND"))
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))),
} }
// set page text
let mut c = layout.start_common();
{
let mut common = c.common();
let mut widget = common.state.widgets.cast_as::<WidgetLabel>(self.id_label_page)?;
widget.set_text(
&mut common,
Translation::from_raw_text_string(format!("{}/{}", self.cur_page + 1, self.page_count)),
);
}
c.finish()?;
fill_game_list(
&mut ConstructEssentials {
layout,
parent: self.id_list_parent,
},
executor,
&mut self.mounted_game_covers,
page_manifests,
&self.tasks,
)?;
if let Some(text) = text.take() { if let Some(text) = text.take() {
layout.add_child( layout.add_child(
self.id_list_parent, self.id_list_parent,
@@ -217,11 +273,11 @@ impl View {
} }
fn set_cover_art(&mut self, layout: &mut Layout, app_id: AppID, cover_art: Rc<CoverArt>) { fn set_cover_art(&mut self, layout: &mut Layout, app_id: AppID, cover_art: Rc<CoverArt>) {
let Some(cell) = &mut self.cells.get_mut(&app_id) else { let Some(cover) = &mut self.mounted_game_covers.get_mut(&app_id) else {
return; return;
}; };
if let Err(e) = cell if let Err(e) = cover
.view_cover .view_cover
.set_cover_art(&mut self.game_cover_view_common, layout, &cover_art) .set_cover_art(&mut self.game_cover_view_common, layout, &cover_art)
{ {

View File

@@ -1,20 +1,19 @@
use wgui::{ use wgui::{
assets::AssetPath, assets::AssetPath,
components::button::ComponentButton, components::button::ComponentButton,
event::StyleSetRequest,
globals::WguiGlobals, globals::WguiGlobals,
i18n::Translation, i18n::Translation,
layout::{Layout, WidgetID}, layout::{Layout, LayoutTask, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState}, parser::{Fetchable, ParseDocumentParams, ParserState},
taffy::Display,
task::Tasks, task::Tasks,
widget::label::WidgetLabel, widget::label::WidgetLabel,
}; };
use crate::{ use crate::{
frontend::{FrontendTask, FrontendTasks}, frontend::{FrontendTask, FrontendTasks},
util::{ util::steam_utils::{self, AppID, AppManifest, GameSortMethod, SteamUtils},
steam_utils::{self, AppID, AppManifest, GameSortMethod, SteamUtils},
wgui_simple,
},
}; };
#[derive(Clone)] #[derive(Clone)]
@@ -39,6 +38,7 @@ pub struct View {
id_list_parent: WidgetID, id_list_parent: WidgetID,
installed_games: Vec<AppManifest>, installed_games: Vec<AppManifest>,
frontend_tasks: FrontendTasks, frontend_tasks: FrontendTasks,
parent_id: WidgetID,
} }
fn doc_params(globals: WguiGlobals) -> ParseDocumentParams<'static> { fn doc_params(globals: WguiGlobals) -> ParseDocumentParams<'static> {
@@ -58,7 +58,7 @@ impl View {
let installed_games = params let installed_games = params
.steam_utils .steam_utils
.list_installed_games(GameSortMethod::None) .list_installed_games(GameSortMethod::None)
.unwrap_or(Vec::new()); .unwrap_or_default();
let tasks = Tasks::<Task>::new(); let tasks = Tasks::<Task>::new();
@@ -72,6 +72,7 @@ impl View {
id_list_parent, id_list_parent,
installed_games, installed_games,
frontend_tasks: params.frontend_tasks, frontend_tasks: params.frontend_tasks,
parent_id: params.parent_id,
}) })
} }
@@ -98,7 +99,7 @@ impl View {
Ok(()) Ok(())
} }
fn extract_name_from_appid<'a>(app_id: &AppID, manifests: &[AppManifest]) -> String { fn extract_name_from_appid(app_id: &AppID, manifests: &[AppManifest]) -> String {
for manifest in manifests { for manifest in manifests {
if manifest.app_id == *app_id { if manifest.app_id == *app_id {
return manifest.name.clone(); return manifest.name.clone();
@@ -110,14 +111,19 @@ impl View {
fn fill_list(&mut self, layout: &mut Layout, games: Vec<steam_utils::RunningGame>) -> anyhow::Result<()> { fn fill_list(&mut self, layout: &mut Layout, games: Vec<steam_utils::RunningGame>) -> anyhow::Result<()> {
if games.is_empty() { if games.is_empty() {
wgui_simple::create_label( // hide self
layout, layout.tasks.push(LayoutTask::SetWidgetStyle(
self.id_list_parent, self.parent_id,
Translation::from_translation_key("GAME_LIST.NO_RUNNING_GAME_FOUND"), StyleSetRequest::Display(Display::None),
)?; ));
return Ok(()); return Ok(());
} }
layout.tasks.push(LayoutTask::SetWidgetStyle(
self.parent_id,
StyleSetRequest::Display(Display::DEFAULT),
));
for game in games { for game in games {
let game_name = View::extract_name_from_appid(&game.app_id, &self.installed_games); let game_name = View::extract_name_from_appid(&game.app_id, &self.installed_games);

View File

@@ -1,5 +1,6 @@
{ {
"lockfileVersion": 1, "lockfileVersion": 1,
"configVersion": 0,
"workspaces": { "workspaces": {
"": { "": {
"name": "llm_translator", "name": "llm_translator",

View File

@@ -103,7 +103,8 @@ pub(super) fn setup_custom_label<S: 'static>(
layout layout
.state .state
.widgets .widgets
.cast_as::<WidgetLabel>(attribs.widget_id)? .cast_as::<WidgetLabel>(attribs.widget_id)
.unwrap()
.set_text_simple(&mut globals, Translation::from_raw_text(pretty_tz)); .set_text_simple(&mut globals, Translation::from_raw_text(pretty_tz));
// does not need to be dynamic // does not need to be dynamic

View File

@@ -195,7 +195,7 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul
color: TOOLTIP_COLOR, color: TOOLTIP_COLOR,
border_color: TOOLTIP_BORDER_COLOR, border_color: TOOLTIP_BORDER_COLOR,
border: 2.0, border: 2.0,
round: WLength::Percent(1.0), round: WLength::Units(24.0),
..Default::default() ..Default::default()
}), }),
taffy::Style { taffy::Style {

View File

@@ -138,6 +138,7 @@ pub type ModifyLayoutStateFunc = Box<dyn FnOnce(ModifyLayoutStateData) -> anyhow
pub enum LayoutTask { pub enum LayoutTask {
RemoveWidget(WidgetID), RemoveWidget(WidgetID),
SetWidgetStyle(WidgetID, event::StyleSetRequest),
ModifyLayoutState(ModifyLayoutStateFunc), ModifyLayoutState(ModifyLayoutStateFunc),
PlaySound(WguiSoundType), PlaySound(WguiSoundType),
Dispatch(Box<dyn FnOnce(&mut CallbackDataCommon) -> anyhow::Result<()>>), Dispatch(Box<dyn FnOnce(&mut CallbackDataCommon) -> anyhow::Result<()>>),
@@ -706,12 +707,51 @@ impl Layout {
func(&mut c.common())?; func(&mut c.common())?;
c.finish()?; c.finish()?;
} }
LayoutTask::SetWidgetStyle(widget_id, style_request) => {
self.set_style_request(widget_id, style_request);
}
} }
} }
Ok(()) Ok(())
} }
fn set_style_request(&mut self, widget_id: WidgetID, style_request: event::StyleSetRequest) {
let Some(node_id) = self.state.nodes.get(widget_id) else {
return;
};
// taffy requires us to copy this whole 536-byte style struct.
// we can't get `&mut Style` directly from taffy unfortunately
let mut cur_style = self.state.tree.style(*node_id).unwrap().clone() /* always safe */;
match style_request {
event::StyleSetRequest::Display(display) => {
// refresh the component in case if visibility/display mode has changed
if cur_style.display != display
&& let Some(component) = self.registered_components_to_refresh.get(node_id)
{
self.components_to_refresh_once.insert(component.clone());
}
cur_style.display = display;
}
event::StyleSetRequest::Margin(margin) => {
cur_style.margin = margin;
}
event::StyleSetRequest::Width(val) => {
cur_style.size.width = val;
}
event::StyleSetRequest::Height(val) => {
cur_style.size.height = val;
}
}
if let Err(e) = self.state.tree.set_style(*node_id, cur_style) {
log::error!("failed to set style for taffy widget ID {node_id:?}: {e:?}");
}
}
pub fn process_alterables(&mut self, alterables: EventAlterables) -> anyhow::Result<()> { pub fn process_alterables(&mut self, alterables: EventAlterables) -> anyhow::Result<()> {
for task in alterables.tasks { for task in alterables.tasks {
self.tasks.push(task); self.tasks.push(task);
@@ -747,39 +787,7 @@ impl Layout {
} }
for (widget_id, style_request) in alterables.style_set_requests { for (widget_id, style_request) in alterables.style_set_requests {
let Some(node_id) = self.state.nodes.get(widget_id) else { self.set_style_request(widget_id, style_request);
continue;
};
// taffy requires us to copy this whole 536-byte style struct.
// we can't get `&mut Style` directly from taffy unfortunately
let mut cur_style = self.state.tree.style(*node_id).unwrap().clone() /* always safe */;
match style_request {
event::StyleSetRequest::Display(display) => {
// refresh the component in case if visibility/display mode has changed
if cur_style.display != display
&& let Some(component) = self.registered_components_to_refresh.get(node_id)
{
self.components_to_refresh_once.insert(component.clone());
}
cur_style.display = display;
}
event::StyleSetRequest::Margin(margin) => {
cur_style.margin = margin;
}
event::StyleSetRequest::Width(val) => {
cur_style.size.width = val;
}
event::StyleSetRequest::Height(val) => {
cur_style.size.height = val;
}
}
if let Err(e) = self.state.tree.set_style(*node_id, cur_style) {
log::error!("failed to set style for taffy widget ID {node_id:?}: {e:?}");
}
} }
Ok(()) Ok(())

View File

@@ -47,7 +47,7 @@ impl WidgetObj for WidgetRectangle {
let boundary = drawing::Boundary::construct_relative(state.transform_stack); let boundary = drawing::Boundary::construct_relative(state.transform_stack);
let round_units = match self.params.round { let round_units = match self.params.round {
WLength::Units(units) => units as u8, WLength::Units(units) => (f32::min(boundary.size.x, boundary.size.y) as u8 / 2).min(units as u8),
WLength::Percent(percent) => (f32::min(boundary.size.x, boundary.size.y) * percent / 2.0) as u8, WLength::Percent(percent) => (f32::min(boundary.size.x, boundary.size.y) * percent / 2.0) as u8,
}; };