Game launcher (fully functional)
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
<layout>
|
||||
<elements>
|
||||
<div flex_direction="row" gap="16">
|
||||
<div flex_direction="row" gap="16" align_items="center">
|
||||
<div id="cover_art_parent" />
|
||||
<div flex_direction="column" gap="16">
|
||||
<label id="label_title" weight="bold" size="32" />
|
||||
<div flex_direction="row" gap="8">
|
||||
<label text="by" />
|
||||
<label weight="bold" id="label_author" />
|
||||
<label weight="bold" id="label_author" text="Unknown" />
|
||||
</div>
|
||||
<label id="label_description" wrap="1" />
|
||||
<Button id="btn_launch" align_self="baseline" color="#44ce22FF" padding_top="4" padding_bottom="4" round="8" padding_right="12" min_height="40">
|
||||
<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_width="200" min_height="40">
|
||||
<sprite src_builtin="dashboard/play.svg" width="32" height="32" />
|
||||
<label text="Launch" weight="bold" size="17" shadow="#00000099" />
|
||||
</Button>
|
||||
|
||||
@@ -68,5 +68,6 @@
|
||||
"GAME_LIST": {
|
||||
"NO_GAMES_FOUND": "Keine Spiele gefunden"
|
||||
},
|
||||
"TERMINATE_PROCESS": "Prozess beenden"
|
||||
"TERMINATE_PROCESS": "Prozess beenden",
|
||||
"GAME_LAUNCHED": "Spiel gestartet"
|
||||
}
|
||||
@@ -39,7 +39,8 @@
|
||||
"VOLUME": "Volume"
|
||||
},
|
||||
"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": {
|
||||
"NO_GAMES_FOUND": "No games found"
|
||||
},
|
||||
|
||||
@@ -68,5 +68,6 @@
|
||||
"GAME_LIST": {
|
||||
"NO_GAMES_FOUND": "No se encontraron juegos"
|
||||
},
|
||||
"TERMINATE_PROCESS": "Finalizar proceso"
|
||||
"TERMINATE_PROCESS": "Finalizar proceso",
|
||||
"GAME_LAUNCHED": "Juego lanzado"
|
||||
}
|
||||
@@ -68,5 +68,6 @@
|
||||
"GAME_LIST": {
|
||||
"NO_GAMES_FOUND": "ゲームが見つかりませんでした"
|
||||
},
|
||||
"TERMINATE_PROCESS": "プロセスを終了する"
|
||||
"TERMINATE_PROCESS": "プロセスを終了する",
|
||||
"GAME_LAUNCHED": "ゲームが起動しました"
|
||||
}
|
||||
@@ -68,5 +68,6 @@
|
||||
"GAME_LIST": {
|
||||
"NO_GAMES_FOUND": "Nie znaleziono gier"
|
||||
},
|
||||
"TERMINATE_PROCESS": "Zakończ proces"
|
||||
"TERMINATE_PROCESS": "Zakończ proces",
|
||||
"GAME_LAUNCHED": "Gra uruchomiona"
|
||||
}
|
||||
@@ -137,7 +137,7 @@ pub fn stop(app_id: AppID, force_kill: bool) -> anyhow::Result<()> {
|
||||
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);
|
||||
call_steam(&format!("steam://rungameid/{}", app_id))?;
|
||||
Ok(())
|
||||
|
||||
317
dash-frontend/src/views/game_cover.rs
Normal file
317
dash-frontend/src/views/game_cover.rs
Normal 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(¶ms.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
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
frontend::{FrontendTask, FrontendTasks},
|
||||
util::{
|
||||
cached_fetcher::{self},
|
||||
steam_utils::{AppID, AppManifest},
|
||||
cached_fetcher::{self, CoverArt},
|
||||
steam_utils::{self, AppID, AppManifest},
|
||||
various::AsyncExecutor,
|
||||
},
|
||||
views::game_cover,
|
||||
};
|
||||
use wgui::{
|
||||
assets::AssetPath,
|
||||
@@ -14,12 +17,13 @@ use wgui::{
|
||||
layout::{Layout, WidgetID},
|
||||
parser::{Fetchable, ParseDocumentParams, ParserState},
|
||||
task::Tasks,
|
||||
widget::label::WidgetLabel,
|
||||
widget::{ConstructEssentials, label::WidgetLabel},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Task {
|
||||
FillAppDetails(cached_fetcher::AppDetailsJSONData),
|
||||
SetCoverArt(Rc<CoverArt>),
|
||||
Launch,
|
||||
}
|
||||
|
||||
@@ -39,14 +43,9 @@ pub struct View {
|
||||
on_launched: Box<dyn Fn()>,
|
||||
frontend_tasks: FrontendTasks,
|
||||
|
||||
#[allow(dead_code)]
|
||||
id_cover_art_parent: WidgetID,
|
||||
#[allow(dead_code)]
|
||||
executor: AsyncExecutor,
|
||||
#[allow(dead_code)]
|
||||
globals: WguiGlobals,
|
||||
#[allow(dead_code)]
|
||||
manifest: AppManifest,
|
||||
game_cover_view_common: game_cover::ViewCommon,
|
||||
view_cover: game_cover::View,
|
||||
app_id: AppID,
|
||||
}
|
||||
|
||||
impl View {
|
||||
@@ -67,11 +66,13 @@ impl View {
|
||||
|
||||
let state = wgui::parser::parse_from_assets(doc_params, params.layout, params.parent_id)?;
|
||||
|
||||
{
|
||||
let mut label_title = state.fetch_widget_as::<WidgetLabel>(¶ms.layout.state, "label_title")?;
|
||||
label_title.set_text_simple(
|
||||
&mut params.globals.get(),
|
||||
Translation::from_raw_text(¶ms.manifest.name),
|
||||
);
|
||||
}
|
||||
|
||||
let tasks = Tasks::new();
|
||||
|
||||
@@ -84,15 +85,30 @@ impl View {
|
||||
|
||||
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: ¶ms.executor,
|
||||
manifest: ¶ms.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 {
|
||||
state,
|
||||
tasks,
|
||||
on_launched: params.on_launched,
|
||||
id_cover_art_parent,
|
||||
frontend_tasks: params.frontend_tasks.clone(),
|
||||
executor: params.executor.clone(),
|
||||
globals: params.globals.clone(),
|
||||
manifest: params.manifest,
|
||||
game_cover_view_common: game_cover::ViewCommon::new(params.globals.clone()),
|
||||
view_cover,
|
||||
app_id: params.manifest.app_id.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -106,6 +122,11 @@ impl View {
|
||||
match task {
|
||||
Task::FillAppDetails(details) => self.action_fill_app_details(layout, details)?,
|
||||
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) {
|
||||
match steam_utils::launch(&self.app_id) {
|
||||
Ok(_) => {
|
||||
self
|
||||
.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)();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,50 +2,32 @@ use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||
|
||||
use wgui::{
|
||||
assets::AssetPath,
|
||||
components::{
|
||||
self,
|
||||
button::ComponentButton,
|
||||
tooltip::{TooltipInfo, TooltipSide},
|
||||
},
|
||||
drawing::{self, GradientMode},
|
||||
globals::WguiGlobals,
|
||||
i18n::Translation,
|
||||
layout::{Layout, WidgetID, WidgetPair},
|
||||
layout::{Layout, WidgetID},
|
||||
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,
|
||||
widget::{
|
||||
ConstructEssentials,
|
||||
div::WidgetDiv,
|
||||
image::{WidgetImage, WidgetImageParams},
|
||||
label::{WidgetLabel, WidgetLabelParams},
|
||||
rectangle,
|
||||
util::WLength,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
frontend::{FrontendTask, FrontendTasks},
|
||||
util::{
|
||||
cached_fetcher::{self, CoverArt},
|
||||
cached_fetcher::CoverArt,
|
||||
popup_manager::{MountPopupParams, PopupHandle},
|
||||
steam_utils::{self, AppID, AppManifest, SteamUtils},
|
||||
various::AsyncExecutor,
|
||||
},
|
||||
views::{self, game_launcher},
|
||||
views::{self, game_cover, game_launcher},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Task {
|
||||
AppManifestClicked(steam_utils::AppManifest),
|
||||
SetCoverArt((AppID, Rc<CoverArt>)),
|
||||
SetCoverArt(AppID, Rc<CoverArt>),
|
||||
CloseLauncher,
|
||||
Refresh,
|
||||
}
|
||||
@@ -59,7 +41,7 @@ pub struct Params<'a> {
|
||||
}
|
||||
|
||||
pub struct Cell {
|
||||
image_parent: WidgetID,
|
||||
view_cover: game_cover::View,
|
||||
manifest: AppManifest,
|
||||
}
|
||||
|
||||
@@ -76,7 +58,7 @@ pub struct View {
|
||||
id_list_parent: WidgetID,
|
||||
steam_utils: steam_utils::SteamUtils,
|
||||
cells: HashMap<AppID, Cell>,
|
||||
img_placeholder: Option<CustomGlyphData>,
|
||||
game_cover_view_common: game_cover::ViewCommon,
|
||||
executor: AsyncExecutor,
|
||||
state: Rc<RefCell<State>>,
|
||||
}
|
||||
@@ -106,7 +88,7 @@ impl View {
|
||||
id_list_parent: list_parent.id,
|
||||
steam_utils,
|
||||
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 })),
|
||||
executor: params.executor,
|
||||
})
|
||||
@@ -122,7 +104,7 @@ impl View {
|
||||
match task {
|
||||
Task::Refresh => self.refresh(layout, executor)?,
|
||||
Task::AppManifestClicked(manifest) => self.action_app_manifest_clicked(manifest)?,
|
||||
Task::SetCoverArt((app_id, cover_art)) => self.action_set_cover_art(layout, &app_id, cover_art)?,
|
||||
Task::SetCoverArt(app_id, cover_art) => self.set_cover_art(layout, app_id, cover_art),
|
||||
Task::CloseLauncher => self.state.borrow_mut().view_launcher = None,
|
||||
}
|
||||
}
|
||||
@@ -141,157 +123,7 @@ pub struct Games {
|
||||
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(
|
||||
globals: &WguiGlobals,
|
||||
ess: &mut ConstructEssentials,
|
||||
executor: &AsyncExecutor,
|
||||
cells: &mut HashMap<AppID, Cell>,
|
||||
@@ -302,14 +134,20 @@ fn fill_game_list(
|
||||
let on_loaded = {
|
||||
let app_id = manifest.app_id.clone();
|
||||
let tasks = tasks.clone();
|
||||
move |cover_art: CoverArt| {
|
||||
tasks.push(Task::SetCoverArt((app_id, Rc::from(cover_art))));
|
||||
}
|
||||
Box::new(move |cover_art: CoverArt| {
|
||||
tasks.push(Task::SetCoverArt(app_id, Rc::from(cover_art)));
|
||||
})
|
||||
};
|
||||
|
||||
let (_, button, cell) = construct_game_cover(ess, executor, globals, manifest, Box::new(on_loaded))?;
|
||||
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 manifest = manifest.clone();
|
||||
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(())
|
||||
@@ -344,7 +188,6 @@ impl View {
|
||||
text = Some(Translation::from_translation_key("GAME_LIST.NO_GAMES_FOUND"))
|
||||
} else {
|
||||
fill_game_list(
|
||||
&self.globals,
|
||||
&mut ConstructEssentials {
|
||||
layout,
|
||||
parent: self.id_list_parent,
|
||||
@@ -376,6 +219,19 @@ impl View {
|
||||
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<()> {
|
||||
self.frontend_tasks.push(FrontendTask::MountPopup(MountPopupParams {
|
||||
title: Translation::from_raw_text(&manifest.name),
|
||||
@@ -410,126 +266,4 @@ impl View {
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod app_launcher;
|
||||
pub mod audio_settings;
|
||||
pub mod game_cover;
|
||||
pub mod game_launcher;
|
||||
pub mod game_list;
|
||||
pub mod process_list;
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
use crate::{
|
||||
animation::{Animation, AnimationEasing},
|
||||
assets::AssetPath,
|
||||
components::{self, tooltip::ComponentTooltip, Component, ComponentBase, ComponentTrait, RefreshData},
|
||||
components::{self, Component, ComponentBase, ComponentTrait, RefreshData, tooltip::ComponentTooltip},
|
||||
drawing::{self, Boundary, Color},
|
||||
event::{CallbackDataCommon, EventListenerCollection, EventListenerID, EventListenerKind},
|
||||
i18n::Translation,
|
||||
layout::{LayoutTask, WidgetID, WidgetPair},
|
||||
renderer_vk::{
|
||||
text::{custom_glyph::CustomGlyphData, FontWeight, TextStyle},
|
||||
text::{FontWeight, TextStyle, custom_glyph::CustomGlyphData},
|
||||
util::centered_matrix,
|
||||
},
|
||||
widget::{
|
||||
self,
|
||||
self, ConstructEssentials, EventResult, WidgetData,
|
||||
label::{WidgetLabel, WidgetLabelParams},
|
||||
rectangle::{WidgetRectangle, WidgetRectangleParams},
|
||||
sprite::{WidgetSprite, WidgetSpriteParams},
|
||||
util::WLength,
|
||||
ConstructEssentials, EventResult, WidgetData,
|
||||
},
|
||||
};
|
||||
use glam::{Mat4, Vec3};
|
||||
@@ -25,7 +24,7 @@ use std::{
|
||||
rc::Rc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use taffy::{prelude::length, AlignItems, JustifyContent};
|
||||
use taffy::{AlignItems, JustifyContent, prelude::length};
|
||||
|
||||
pub struct Params<'a> {
|
||||
pub text: Option<Translation>, // if unset, label will not be populated
|
||||
|
||||
Reference in New Issue
Block a user