Game launcher (fully functional)

This commit is contained in:
Aleksander
2025-12-28 16:38:24 +01:00
parent 686a6f3ba5
commit d664b1b9e2
12 changed files with 435 additions and 343 deletions

View File

@@ -1,15 +1,15 @@
<layout> <layout>
<elements> <elements>
<div flex_direction="row" gap="16"> <div flex_direction="row" gap="16" align_items="center">
<div id="cover_art_parent" /> <div id="cover_art_parent" />
<div flex_direction="column" gap="16"> <div flex_direction="column" gap="16">
<label id="label_title" weight="bold" size="32" /> <label id="label_title" weight="bold" size="32" />
<div flex_direction="row" gap="8"> <div flex_direction="row" gap="8">
<label text="by" /> <label text="by" />
<label weight="bold" id="label_author" /> <label weight="bold" id="label_author" text="Unknown" />
</div> </div>
<label id="label_description" wrap="1" /> <label id="label_description" wrap="1" text="No description available" />
<Button id="btn_launch" align_self="baseline" color="#44ce22FF" padding_top="4" padding_bottom="4" round="8" padding_right="12" min_height="40"> <Button id="btn_launch" align_self="baseline" color="#44ce22FF" padding_top="4" padding_bottom="4" round="8" padding_right="12" min_width="200" min_height="40">
<sprite src_builtin="dashboard/play.svg" width="32" height="32" /> <sprite src_builtin="dashboard/play.svg" width="32" height="32" />
<label text="Launch" weight="bold" size="17" shadow="#00000099" /> <label text="Launch" weight="bold" size="17" shadow="#00000099" />
</Button> </Button>

View File

@@ -68,5 +68,6 @@
"GAME_LIST": { "GAME_LIST": {
"NO_GAMES_FOUND": "Keine Spiele gefunden" "NO_GAMES_FOUND": "Keine Spiele gefunden"
}, },
"TERMINATE_PROCESS": "Prozess beenden" "TERMINATE_PROCESS": "Prozess beenden",
"GAME_LAUNCHED": "Spiel gestartet"
} }

View File

@@ -39,7 +39,8 @@
"VOLUME": "Volume" "VOLUME": "Volume"
}, },
"CLOSE_WINDOW": "Close window", "CLOSE_WINDOW": "Close window",
"FAILED_TO_LAUNCH_APPLICATION": "Failed to launcha application:", "FAILED_TO_LAUNCH_APPLICATION": "Failed to launch a application:",
"GAME_LAUNCHED": "Game launched",
"GAME_LIST": { "GAME_LIST": {
"NO_GAMES_FOUND": "No games found" "NO_GAMES_FOUND": "No games found"
}, },

View File

@@ -68,5 +68,6 @@
"GAME_LIST": { "GAME_LIST": {
"NO_GAMES_FOUND": "No se encontraron juegos" "NO_GAMES_FOUND": "No se encontraron juegos"
}, },
"TERMINATE_PROCESS": "Finalizar proceso" "TERMINATE_PROCESS": "Finalizar proceso",
"GAME_LAUNCHED": "Juego lanzado"
} }

View File

@@ -68,5 +68,6 @@
"GAME_LIST": { "GAME_LIST": {
"NO_GAMES_FOUND": "ゲームが見つかりませんでした" "NO_GAMES_FOUND": "ゲームが見つかりませんでした"
}, },
"TERMINATE_PROCESS": "プロセスを終了する" "TERMINATE_PROCESS": "プロセスを終了する",
"GAME_LAUNCHED": "ゲームが起動しました"
} }

View File

@@ -68,5 +68,6 @@
"GAME_LIST": { "GAME_LIST": {
"NO_GAMES_FOUND": "Nie znaleziono gier" "NO_GAMES_FOUND": "Nie znaleziono gier"
}, },
"TERMINATE_PROCESS": "Zakończ proces" "TERMINATE_PROCESS": "Zakończ proces",
"GAME_LAUNCHED": "Gra uruchomiona"
} }

View File

@@ -137,7 +137,7 @@ pub fn stop(app_id: AppID, force_kill: bool) -> anyhow::Result<()> {
Ok(()) Ok(())
} }
pub fn launch(app_id: AppID) -> anyhow::Result<()> { pub fn launch(app_id: &AppID) -> anyhow::Result<()> {
log::info!("Launching Steam game with AppID {}", app_id); log::info!("Launching Steam game with AppID {}", app_id);
call_steam(&format!("steam://rungameid/{}", app_id))?; call_steam(&format!("steam://rungameid/{}", app_id))?;
Ok(()) Ok(())

View File

@@ -0,0 +1,317 @@
use std::rc::Rc;
use wgui::{
assets::AssetPath,
components::{
self,
button::ComponentButton,
tooltip::{TooltipInfo, TooltipSide},
},
drawing::{self, GradientMode},
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID, WidgetPair},
renderer_vk::text::{FontWeight, HorizontalAlign, TextShadow, TextStyle, custom_glyph::CustomGlyphData},
taffy::{
self, AlignItems, AlignSelf, JustifyContent, JustifySelf,
prelude::{auto, length, percent},
},
widget::{
ConstructEssentials,
div::WidgetDiv,
image::{WidgetImage, WidgetImageParams},
label::{WidgetLabel, WidgetLabelParams},
rectangle,
util::WLength,
},
};
use crate::util::{
cached_fetcher::{self, CoverArt},
steam_utils::{self, AppID},
various::AsyncExecutor,
};
pub struct ViewCommon {
img_placeholder: Option<CustomGlyphData>,
globals: WguiGlobals,
}
pub struct Params<'a, 'b> {
pub ess: &'a mut ConstructEssentials<'b>,
pub executor: &'a AsyncExecutor,
pub manifest: &'a steam_utils::AppManifest,
pub scale: f32,
pub on_loaded: Box<dyn FnOnce(CoverArt)>,
}
pub struct View {
pub button: Rc<ComponentButton>,
pair: WidgetPair,
id_image_parent: WidgetID,
app_name: String,
app_id: AppID,
}
const BORDER_COLOR_DEFAULT: drawing::Color = drawing::Color::new(0.0, 0.0, 0.0, 0.35);
const BORDER_COLOR_HOVERED: drawing::Color = drawing::Color::new(1.0, 1.0, 1.0, 1.0);
const GAME_COVER_SIZE_X: f32 = 140.0;
const GAME_COVER_SIZE_Y: f32 = 210.0;
impl View {
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);
return;
}
};
on_loaded(cover_art)
}
fn mount_image(&self, layout: &mut Layout, glyph: &CustomGlyphData) -> anyhow::Result<()> {
let image = WidgetImage::create(WidgetImageParams {
round: WLength::Units(10.0),
glyph_data: Some(glyph.clone()),
..Default::default()
});
let (a, _) = layout.add_child(
self.id_image_parent,
image,
taffy::Style {
size: taffy::Size {
width: percent(1.0),
height: percent(1.0),
},
..Default::default()
},
)?;
a.widget.state().flags.new_pass = true;
Ok(())
}
fn mount_placeholder_text(
&self,
globals: &WguiGlobals,
layout: &mut Layout,
parent: WidgetID,
text: &str,
) -> anyhow::Result<()> {
let label = WidgetLabel::create(
&mut globals.get(),
WidgetLabelParams {
content: Translation::from_raw_text(text),
style: TextStyle {
weight: Some(FontWeight::Bold),
wrap: true,
size: Some(16.0),
align: Some(HorizontalAlign::Center),
shadow: Some(TextShadow {
color: drawing::Color::new(0.0, 0.0, 0.0, 1.0),
x: 2.0,
y: 2.0,
}),
..Default::default()
},
},
);
layout.add_child(
parent,
label,
taffy::Style {
position: taffy::Position::Absolute,
align_self: Some(AlignSelf::Baseline),
justify_self: Some(JustifySelf::Center),
margin: taffy::Rect {
top: length(32.0),
bottom: auto(),
left: auto(),
right: auto(),
},
..Default::default()
},
)?;
Ok(())
}
pub fn set_cover_art(
&mut self,
view_common: &mut ViewCommon,
layout: &mut Layout,
cover_art: &CoverArt,
) -> anyhow::Result<()> {
if cover_art.compressed_image_data.is_empty() {
// mount placeholder
let img = view_common.get_placeholder_image()?.clone();
self.mount_image(layout, &img)?;
self.mount_placeholder_text(&view_common.globals, layout, self.id_image_parent, &self.app_name)?;
} else {
// mount image
let path = format!("app:{:?}", self.app_id);
let glyph =
match CustomGlyphData::from_bytes_raster(&view_common.globals, &path, &cover_art.compressed_image_data) {
Ok(c) => c,
Err(e) => {
log::warn!("failed to decode cover art image: {:?}", e);
return Ok(());
}
};
self.mount_image(layout, &glyph)?;
}
Ok(())
}
pub fn new(params: Params) -> anyhow::Result<Self> {
let (widget_button, button) = components::button::construct(
params.ess,
components::button::Params {
color: Some(drawing::Color::new(1.0, 1.0, 1.0, 0.0)),
border_color: Some(BORDER_COLOR_DEFAULT),
hover_border_color: Some(BORDER_COLOR_HOVERED),
round: WLength::Units(12.0),
border: 2.0,
tooltip: Some(TooltipInfo {
side: TooltipSide::Bottom,
text: Translation::from_raw_text(&params.manifest.name),
}),
style: taffy::Style {
position: taffy::Position::Relative,
align_items: Some(taffy::AlignItems::Center),
justify_content: Some(taffy::JustifyContent::Center),
size: taffy::Size {
width: length(GAME_COVER_SIZE_X * params.scale),
height: length(GAME_COVER_SIZE_Y * params.scale),
},
..Default::default()
},
..Default::default()
},
)?;
let (image_parent, _) = params.ess.layout.add_child(
widget_button.id,
WidgetDiv::create(),
taffy::Style {
position: taffy::Position::Absolute,
size: taffy::Size {
width: percent(1.0),
height: percent(1.0),
},
padding: taffy::Rect::length(2.0),
align_items: Some(AlignItems::Center),
justify_content: Some(JustifyContent::Center),
..Default::default()
},
)?;
let rect_gradient = |color: drawing::Color, color2: drawing::Color| {
rectangle::WidgetRectangle::create(rectangle::WidgetRectangleParams {
color,
color2,
round: WLength::Units(12.0),
gradient: GradientMode::Vertical,
..Default::default()
})
};
let rect_gradient_style = |align_self: taffy::AlignSelf, height: f32| taffy::Style {
position: taffy::Position::Absolute,
align_self: Some(align_self),
size: taffy::Size {
width: percent(1.0),
height: percent(height),
},
..Default::default()
};
// top shine
let (top_shine, _) = params.ess.layout.add_child(
widget_button.id,
rect_gradient(
drawing::Color::new(1.0, 1.0, 1.0, 0.2),
drawing::Color::new(1.0, 1.0, 1.0, 0.02),
),
rect_gradient_style(taffy::AlignSelf::Baseline, 0.05),
)?;
// not optimal, this forces us to create a new pass for every created cover art just to overlay various rectangles at the top of the image cover art
top_shine.widget.state().flags.new_pass = true;
// top white gradient
params.ess.layout.add_child(
widget_button.id,
rect_gradient(
drawing::Color::new(1.0, 1.0, 1.0, 0.15),
drawing::Color::new(1.0, 1.0, 1.0, 0.0),
),
rect_gradient_style(taffy::AlignSelf::Baseline, 0.5),
)?;
// bottom black gradient
params.ess.layout.add_child(
widget_button.id,
rect_gradient(
drawing::Color::new(0.0, 0.0, 0.0, 0.0),
drawing::Color::new(0.0, 0.0, 0.0, 0.25),
),
rect_gradient_style(taffy::AlignSelf::End, 0.5),
)?;
// bottom shadow
params.ess.layout.add_child(
widget_button.id,
rect_gradient(
drawing::Color::new(0.0, 0.0, 0.0, 0.1),
drawing::Color::new(0.0, 0.0, 0.0, 0.9),
),
rect_gradient_style(taffy::AlignSelf::End, 0.05),
)?;
// request cover image data from the internet or disk cache
params
.executor
.spawn(View::request_cover_image(
params.executor.clone(),
params.manifest.clone(),
Box::new(params.on_loaded),
))
.detach();
Ok(View {
pair: widget_button,
button,
id_image_parent: image_parent.id,
app_name: params.manifest.name.clone(),
app_id: params.manifest.app_id.clone(),
})
}
}
impl ViewCommon {
pub fn new(globals: WguiGlobals) -> Self {
Self {
globals,
img_placeholder: None,
}
}
fn get_placeholder_image(&mut self) -> anyhow::Result<&CustomGlyphData> {
if self.img_placeholder.is_none() {
let c = CustomGlyphData::from_assets(&self.globals, AssetPath::BuiltIn("dashboard/placeholder_cover.png"))?;
self.img_placeholder = Some(c);
}
Ok(self.img_placeholder.as_ref().unwrap()) // safe
}
}

View File

@@ -1,10 +1,13 @@
use std::rc::Rc;
use crate::{ use crate::{
frontend::{FrontendTask, FrontendTasks}, frontend::{FrontendTask, FrontendTasks},
util::{ util::{
cached_fetcher::{self}, cached_fetcher::{self, CoverArt},
steam_utils::{AppID, AppManifest}, steam_utils::{self, AppID, AppManifest},
various::AsyncExecutor, various::AsyncExecutor,
}, },
views::game_cover,
}; };
use wgui::{ use wgui::{
assets::AssetPath, assets::AssetPath,
@@ -14,12 +17,13 @@ use wgui::{
layout::{Layout, WidgetID}, layout::{Layout, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState}, parser::{Fetchable, ParseDocumentParams, ParserState},
task::Tasks, task::Tasks,
widget::label::WidgetLabel, widget::{ConstructEssentials, label::WidgetLabel},
}; };
#[derive(Clone)] #[derive(Clone)]
enum Task { enum Task {
FillAppDetails(cached_fetcher::AppDetailsJSONData), FillAppDetails(cached_fetcher::AppDetailsJSONData),
SetCoverArt(Rc<CoverArt>),
Launch, Launch,
} }
@@ -39,14 +43,9 @@ pub struct View {
on_launched: Box<dyn Fn()>, on_launched: Box<dyn Fn()>,
frontend_tasks: FrontendTasks, frontend_tasks: FrontendTasks,
#[allow(dead_code)] game_cover_view_common: game_cover::ViewCommon,
id_cover_art_parent: WidgetID, view_cover: game_cover::View,
#[allow(dead_code)] app_id: AppID,
executor: AsyncExecutor,
#[allow(dead_code)]
globals: WguiGlobals,
#[allow(dead_code)]
manifest: AppManifest,
} }
impl View { impl View {
@@ -67,11 +66,13 @@ impl View {
let state = wgui::parser::parse_from_assets(doc_params, params.layout, params.parent_id)?; 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")?; let mut label_title = state.fetch_widget_as::<WidgetLabel>(&params.layout.state, "label_title")?;
label_title.set_text_simple( label_title.set_text_simple(
&mut params.globals.get(), &mut params.globals.get(),
Translation::from_raw_text(&params.manifest.name), Translation::from_raw_text(&params.manifest.name),
); );
}
let tasks = Tasks::new(); let tasks = Tasks::new();
@@ -84,15 +85,30 @@ impl View {
tasks.handle_button(&btn_launch, Task::Launch); tasks.handle_button(&btn_launch, Task::Launch);
let view_cover = game_cover::View::new(game_cover::Params {
ess: &mut ConstructEssentials {
layout: params.layout,
parent: id_cover_art_parent,
},
executor: &params.executor,
manifest: &params.manifest,
on_loaded: {
let tasks = tasks.clone();
Box::new(move |cover_art| {
tasks.push(Task::SetCoverArt(Rc::new(cover_art)));
})
},
scale: 1.5,
})?;
Ok(Self { Ok(Self {
state, state,
tasks, tasks,
on_launched: params.on_launched, on_launched: params.on_launched,
id_cover_art_parent,
frontend_tasks: params.frontend_tasks.clone(), frontend_tasks: params.frontend_tasks.clone(),
executor: params.executor.clone(), game_cover_view_common: game_cover::ViewCommon::new(params.globals.clone()),
globals: params.globals.clone(), view_cover,
manifest: params.manifest, app_id: params.manifest.app_id.clone(),
}) })
} }
@@ -106,6 +122,11 @@ impl View {
match task { match task {
Task::FillAppDetails(details) => self.action_fill_app_details(layout, details)?, Task::FillAppDetails(details) => self.action_fill_app_details(layout, details)?,
Task::Launch => self.action_launch(), Task::Launch => self.action_launch(),
Task::SetCoverArt(cover_art) => {
let _ = self
.view_cover
.set_cover_art(&mut self.game_cover_view_common, layout, &cover_art);
}
} }
} }
} }
@@ -150,9 +171,24 @@ impl View {
} }
fn action_launch(&mut self) { fn action_launch(&mut self) {
match steam_utils::launch(&self.app_id) {
Ok(_) => {
self self
.frontend_tasks .frontend_tasks
.push(FrontendTask::PushToast(Translation::from_raw_text("Game launch TODO"))); .push(FrontendTask::PushToast(Translation::from_translation_key(
"GAME_LAUNCHED",
)));
}
Err(e) => {
self
.frontend_tasks
.push(FrontendTask::PushToast(Translation::from_raw_text_string(format!(
"Failed to launch: {:?}",
e
))));
}
}
(*self.on_launched)(); (*self.on_launched)();
} }
} }

View File

@@ -2,50 +2,32 @@ use std::{cell::RefCell, collections::HashMap, rc::Rc};
use wgui::{ use wgui::{
assets::AssetPath, assets::AssetPath,
components::{
self,
button::ComponentButton,
tooltip::{TooltipInfo, TooltipSide},
},
drawing::{self, GradientMode},
globals::WguiGlobals, globals::WguiGlobals,
i18n::Translation, i18n::Translation,
layout::{Layout, WidgetID, WidgetPair}, layout::{Layout, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState}, parser::{Fetchable, ParseDocumentParams, ParserState},
renderer_vk::text::{
FontWeight, HorizontalAlign, TextShadow, TextStyle,
custom_glyph::{ CustomGlyphData},
},
taffy::{
self, AlignItems, AlignSelf, JustifyContent, JustifySelf,
prelude::{auto, length, percent},
},
task::Tasks, task::Tasks,
widget::{ widget::{
ConstructEssentials, ConstructEssentials,
div::WidgetDiv,
image::{WidgetImage, WidgetImageParams},
label::{WidgetLabel, WidgetLabelParams}, label::{WidgetLabel, WidgetLabelParams},
rectangle,
util::WLength,
}, },
}; };
use crate::{ use crate::{
frontend::{FrontendTask, FrontendTasks}, frontend::{FrontendTask, FrontendTasks},
util::{ util::{
cached_fetcher::{self, CoverArt}, cached_fetcher::CoverArt,
popup_manager::{MountPopupParams, PopupHandle}, popup_manager::{MountPopupParams, PopupHandle},
steam_utils::{self, AppID, AppManifest, SteamUtils}, steam_utils::{self, AppID, AppManifest, SteamUtils},
various::AsyncExecutor, various::AsyncExecutor,
}, },
views::{self, game_launcher}, views::{self, game_cover, game_launcher},
}; };
#[derive(Clone)] #[derive(Clone)]
enum Task { enum Task {
AppManifestClicked(steam_utils::AppManifest), AppManifestClicked(steam_utils::AppManifest),
SetCoverArt((AppID, Rc<CoverArt>)), SetCoverArt(AppID, Rc<CoverArt>),
CloseLauncher, CloseLauncher,
Refresh, Refresh,
} }
@@ -59,7 +41,7 @@ pub struct Params<'a> {
} }
pub struct Cell { pub struct Cell {
image_parent: WidgetID, view_cover: game_cover::View,
manifest: AppManifest, manifest: AppManifest,
} }
@@ -76,7 +58,7 @@ pub struct View {
id_list_parent: WidgetID, id_list_parent: WidgetID,
steam_utils: steam_utils::SteamUtils, steam_utils: steam_utils::SteamUtils,
cells: HashMap<AppID, Cell>, cells: HashMap<AppID, Cell>,
img_placeholder: Option<CustomGlyphData>, game_cover_view_common: game_cover::ViewCommon,
executor: AsyncExecutor, executor: AsyncExecutor,
state: Rc<RefCell<State>>, state: Rc<RefCell<State>>,
} }
@@ -106,7 +88,7 @@ impl View {
id_list_parent: list_parent.id, id_list_parent: list_parent.id,
steam_utils, steam_utils,
cells: HashMap::new(), cells: HashMap::new(),
img_placeholder: None, 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,
}) })
@@ -122,7 +104,7 @@ impl View {
match task { match task {
Task::Refresh => self.refresh(layout, executor)?, Task::Refresh => self.refresh(layout, executor)?,
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.action_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,
} }
} }
@@ -141,157 +123,7 @@ pub struct Games {
manifests: Vec<steam_utils::AppManifest>, manifests: Vec<steam_utils::AppManifest>,
} }
const BORDER_COLOR_DEFAULT: drawing::Color = drawing::Color::new(0.0, 0.0, 0.0, 0.35);
const BORDER_COLOR_HOVERED: drawing::Color = drawing::Color::new(1.0, 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,
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);
return;
}
};
on_loaded(cover_art)
}
pub fn construct_game_cover(
ess: &mut ConstructEssentials,
executor: &AsyncExecutor,
_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,
components::button::Params {
color: Some(drawing::Color::new(1.0, 1.0, 1.0, 0.0)),
border_color: Some(BORDER_COLOR_DEFAULT),
hover_border_color: Some(BORDER_COLOR_HOVERED),
round: WLength::Units(12.0),
border: 2.0,
tooltip: Some(TooltipInfo {
side: TooltipSide::Bottom,
text: Translation::from_raw_text(&manifest.name),
}),
style: taffy::Style {
position: taffy::Position::Relative,
align_items: Some(taffy::AlignItems::Center),
justify_content: Some(taffy::JustifyContent::Center),
size: taffy::Size {
width: length(GAME_COVER_SIZE_X),
height: length(GAME_COVER_SIZE_Y),
},
..Default::default()
},
..Default::default()
},
)?;
let (image_parent, _) = ess.layout.add_child(
widget_button.id,
WidgetDiv::create(),
taffy::Style {
position: taffy::Position::Absolute,
size: taffy::Size {
width: percent(1.0),
height: percent(1.0),
},
padding: taffy::Rect::length(2.0),
align_items: Some(AlignItems::Center),
justify_content: Some(JustifyContent::Center),
..Default::default()
},
)?;
let rect_gradient = |color: drawing::Color, color2: drawing::Color| {
rectangle::WidgetRectangle::create(rectangle::WidgetRectangleParams {
color,
color2,
round: WLength::Units(12.0),
gradient: GradientMode::Vertical,
..Default::default()
})
};
let rect_gradient_style = |align_self: taffy::AlignSelf, height: f32| taffy::Style {
position: taffy::Position::Absolute,
align_self: Some(align_self),
size: taffy::Size {
width: percent(1.0),
height: percent(height),
},
..Default::default()
};
// top shine
let (top_shine, _) = ess.layout.add_child(
widget_button.id,
rect_gradient(
drawing::Color::new(1.0, 1.0, 1.0, 0.2),
drawing::Color::new(1.0, 1.0, 1.0, 0.02),
),
rect_gradient_style(taffy::AlignSelf::Baseline, 0.05),
)?;
// not optimal, this forces us to create a new pass for every created cover art just to overlay various rectangles at the top of the image cover art
top_shine.widget.state().flags.new_pass = true;
// top white gradient
ess.layout.add_child(
widget_button.id,
rect_gradient(
drawing::Color::new(1.0, 1.0, 1.0, 0.15),
drawing::Color::new(1.0, 1.0, 1.0, 0.0),
),
rect_gradient_style(taffy::AlignSelf::Baseline, 0.5),
)?;
// bottom black gradient
ess.layout.add_child(
widget_button.id,
rect_gradient(
drawing::Color::new(0.0, 0.0, 0.0, 0.0),
drawing::Color::new(0.0, 0.0, 0.0, 0.25),
),
rect_gradient_style(taffy::AlignSelf::End, 0.5),
)?;
// bottom shadow
ess.layout.add_child(
widget_button.id,
rect_gradient(
drawing::Color::new(0.0, 0.0, 0.0, 0.1),
drawing::Color::new(0.0, 0.0, 0.0, 0.9),
),
rect_gradient_style(taffy::AlignSelf::End, 0.05),
)?;
// request cover image data from the internet or disk cache
executor
.spawn(request_cover_image(executor.clone(), manifest.clone(), on_loaded))
.detach();
Ok((
widget_button,
button,
Cell {
image_parent: image_parent.id,
manifest: manifest.clone(),
},
))
}
fn fill_game_list( fn fill_game_list(
globals: &WguiGlobals,
ess: &mut ConstructEssentials, ess: &mut ConstructEssentials,
executor: &AsyncExecutor, executor: &AsyncExecutor,
cells: &mut HashMap<AppID, Cell>, cells: &mut HashMap<AppID, Cell>,
@@ -302,14 +134,20 @@ fn fill_game_list(
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();
move |cover_art: CoverArt| { Box::new(move |cover_art: CoverArt| {
tasks.push(Task::SetCoverArt((app_id, Rc::from(cover_art)))); tasks.push(Task::SetCoverArt(app_id, Rc::from(cover_art)));
} })
}; };
let (_, button, cell) = construct_game_cover(ess, executor, globals, manifest, Box::new(on_loaded))?; let view_cover = game_cover::View::new(game_cover::Params {
ess,
executor,
manifest,
on_loaded,
scale: 1.0,
})?;
button.on_click({ view_cover.button.on_click({
let tasks = tasks.clone(); let tasks = tasks.clone();
let manifest = manifest.clone(); let manifest = manifest.clone();
Box::new(move |_, _| { Box::new(move |_, _| {
@@ -318,7 +156,13 @@ fn fill_game_list(
}) })
}); });
cells.insert(manifest.app_id.clone(), cell); cells.insert(
manifest.app_id.clone(),
Cell {
view_cover,
manifest: manifest.clone(),
},
);
} }
Ok(()) Ok(())
@@ -344,7 +188,6 @@ impl View {
text = Some(Translation::from_translation_key("GAME_LIST.NO_GAMES_FOUND")) text = Some(Translation::from_translation_key("GAME_LIST.NO_GAMES_FOUND"))
} else { } else {
fill_game_list( fill_game_list(
&self.globals,
&mut ConstructEssentials { &mut ConstructEssentials {
layout, layout,
parent: self.id_list_parent, parent: self.id_list_parent,
@@ -376,6 +219,19 @@ impl View {
Ok(()) Ok(())
} }
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 {
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<()> { fn action_app_manifest_clicked(&mut self, manifest: steam_utils::AppManifest) -> anyhow::Result<()> {
self.frontend_tasks.push(FrontendTask::MountPopup(MountPopupParams { self.frontend_tasks.push(FrontendTask::MountPopup(MountPopupParams {
title: Translation::from_raw_text(&manifest.name), title: Translation::from_raw_text(&manifest.name),
@@ -410,126 +266,4 @@ impl View {
Ok(()) Ok(())
} }
fn get_placeholder_image(&mut self) -> anyhow::Result<&CustomGlyphData> {
if self.img_placeholder.is_none() {
let c = CustomGlyphData::from_assets(
&self.globals,
AssetPath::BuiltIn("dashboard/placeholder_cover.png"),
)?;
self.img_placeholder = Some(c);
}
Ok(self.img_placeholder.as_ref().unwrap()) // safe
}
fn mount_image(layout: &mut Layout, cell: &Cell, glyph: &CustomGlyphData) -> anyhow::Result<()> {
let image = WidgetImage::create(WidgetImageParams {
round: WLength::Units(10.0),
glyph_data: Some(glyph.clone()),
..Default::default()
});
let (a, _) = layout.add_child(
cell.image_parent,
image,
taffy::Style {
size: taffy::Size {
width: percent(1.0),
height: percent(1.0),
},
..Default::default()
},
)?;
a.widget.state().flags.new_pass = true;
Ok(())
}
fn mount_placeholder_text(
globals: &WguiGlobals,
layout: &mut Layout,
parent: WidgetID,
text: &str,
) -> anyhow::Result<()> {
let label = WidgetLabel::create(
&mut globals.get(),
WidgetLabelParams {
content: Translation::from_raw_text(text),
style: TextStyle {
weight: Some(FontWeight::Bold),
wrap: true,
size: Some(16.0),
align: Some(HorizontalAlign::Center),
shadow: Some(TextShadow {
color: drawing::Color::new(0.0, 0.0, 0.0, 1.0),
x: 2.0,
y: 2.0,
}),
..Default::default()
},
},
);
layout.add_child(
parent,
label,
taffy::Style {
position: taffy::Position::Absolute,
align_self: Some(AlignSelf::Baseline),
justify_self: Some(JustifySelf::Center),
margin: taffy::Rect {
top: length(32.0),
bottom: auto(),
left: auto(),
right: auto(),
},
..Default::default()
},
)?;
Ok(())
}
fn action_set_cover_art(
&mut self,
layout: &mut Layout,
app_id: &AppID,
cover_art: Rc<CoverArt>,
) -> anyhow::Result<()> {
if cover_art.compressed_image_data.is_empty() {
// mount placeholder
let img = self.get_placeholder_image()?.clone();
let Some(cell) = self.cells.get(app_id) else {
debug_assert!(false); // this shouldn't happen
return Ok(());
};
View::mount_image(layout, cell, &img)?;
View::mount_placeholder_text(&self.globals, layout, cell.image_parent, &cell.manifest.name)?;
} else {
// mount image
let Some(cell) = self.cells.get(app_id) else {
debug_assert!(false); // this shouldn't happen
return Ok(());
};
let path = format!("app:{app_id:?}");
let glyph = match CustomGlyphData::from_bytes_raster(&self.globals, &path ,&cover_art.compressed_image_data) {
Ok(c) => c,
Err(e) => {
log::warn!(
"failed to decode cover art image for AppID {} ({:?}), skipping",
app_id,
e
);
return Ok(());
}
};
View::mount_image(layout, cell, &glyph)?;
}
Ok(())
}
} }

View File

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

View File

@@ -1,22 +1,21 @@
use crate::{ use crate::{
animation::{Animation, AnimationEasing}, animation::{Animation, AnimationEasing},
assets::AssetPath, assets::AssetPath,
components::{self, tooltip::ComponentTooltip, Component, ComponentBase, ComponentTrait, RefreshData}, components::{self, Component, ComponentBase, ComponentTrait, RefreshData, tooltip::ComponentTooltip},
drawing::{self, Boundary, Color}, drawing::{self, Boundary, Color},
event::{CallbackDataCommon, EventListenerCollection, EventListenerID, EventListenerKind}, event::{CallbackDataCommon, EventListenerCollection, EventListenerID, EventListenerKind},
i18n::Translation, i18n::Translation,
layout::{LayoutTask, WidgetID, WidgetPair}, layout::{LayoutTask, WidgetID, WidgetPair},
renderer_vk::{ renderer_vk::{
text::{custom_glyph::CustomGlyphData, FontWeight, TextStyle}, text::{FontWeight, TextStyle, custom_glyph::CustomGlyphData},
util::centered_matrix, util::centered_matrix,
}, },
widget::{ widget::{
self, self, ConstructEssentials, EventResult, WidgetData,
label::{WidgetLabel, WidgetLabelParams}, label::{WidgetLabel, WidgetLabelParams},
rectangle::{WidgetRectangle, WidgetRectangleParams}, rectangle::{WidgetRectangle, WidgetRectangleParams},
sprite::{WidgetSprite, WidgetSpriteParams}, sprite::{WidgetSprite, WidgetSpriteParams},
util::WLength, util::WLength,
ConstructEssentials, EventResult, WidgetData,
}, },
}; };
use glam::{Mat4, Vec3}; use glam::{Mat4, Vec3};
@@ -25,7 +24,7 @@ use std::{
rc::Rc, rc::Rc,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use taffy::{prelude::length, AlignItems, JustifyContent}; use taffy::{AlignItems, JustifyContent, prelude::length};
pub struct Params<'a> { pub struct Params<'a> {
pub text: Option<Translation>, // if unset, label will not be populated pub text: Option<Translation>, // if unset, label will not be populated