HTTP client, game cover art fetcher, game list image display, use smol::LocalExecutor for async runtime
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use std::{collections::HashMap, rc::Rc};
|
||||
|
||||
use wayvr_ipc::packet_server::{self, WvrWindowHandle};
|
||||
use wgui::{
|
||||
assets::AssetPath,
|
||||
components::{
|
||||
@@ -13,32 +12,36 @@ use wgui::{
|
||||
i18n::Translation,
|
||||
layout::{Layout, WidgetID, WidgetPair},
|
||||
parser::{Fetchable, ParseDocumentParams, ParserState},
|
||||
renderer_vk::text::custom_glyph::{CustomGlyphContent, CustomGlyphData},
|
||||
taffy::{
|
||||
self,
|
||||
prelude::{length, percent},
|
||||
},
|
||||
widget::{
|
||||
ConstructEssentials,
|
||||
div::WidgetDiv,
|
||||
image::{WidgetImage, WidgetImageParams},
|
||||
label::{WidgetLabel, WidgetLabelParams},
|
||||
rectangle,
|
||||
util::WLength,
|
||||
},
|
||||
};
|
||||
use wlx_common::dash_interface::BoxDashInterface;
|
||||
|
||||
use crate::{
|
||||
frontend::{FrontendTask, FrontendTasks},
|
||||
task::Tasks,
|
||||
util::{
|
||||
cover_art_fetcher::{self, CoverArt},
|
||||
popup_manager::MountPopupParams,
|
||||
steam_utils::{self, SteamUtils},
|
||||
steam_utils::{self, AppID, SteamUtils},
|
||||
various::AsyncExecutor,
|
||||
},
|
||||
views::window_options,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Task {
|
||||
AppManifestClicked(steam_utils::AppManifest),
|
||||
SetCoverArt((AppID, Rc<CoverArt>)),
|
||||
Refresh,
|
||||
}
|
||||
|
||||
@@ -49,6 +52,10 @@ pub struct Params<'a> {
|
||||
pub parent_id: WidgetID,
|
||||
}
|
||||
|
||||
struct Cell {
|
||||
image_parent: WidgetID,
|
||||
}
|
||||
|
||||
pub struct View {
|
||||
#[allow(dead_code)]
|
||||
pub parser_state: ParserState,
|
||||
@@ -57,6 +64,8 @@ pub struct View {
|
||||
globals: WguiGlobals,
|
||||
id_list_parent: WidgetID,
|
||||
steam_utils: steam_utils::SteamUtils,
|
||||
|
||||
cells: HashMap<AppID, Cell>,
|
||||
}
|
||||
|
||||
impl View {
|
||||
@@ -83,10 +92,11 @@ impl View {
|
||||
globals: params.globals.clone(),
|
||||
id_list_parent: list_parent.id,
|
||||
steam_utils,
|
||||
cells: HashMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update(&mut self, layout: &mut Layout, interface: &mut BoxDashInterface) -> anyhow::Result<()> {
|
||||
pub fn update(&mut self, layout: &mut Layout, executor: &AsyncExecutor) -> anyhow::Result<()> {
|
||||
loop {
|
||||
let tasks = self.tasks.drain();
|
||||
if tasks.is_empty() {
|
||||
@@ -94,8 +104,9 @@ impl View {
|
||||
}
|
||||
for task in tasks {
|
||||
match task {
|
||||
Task::Refresh => self.refresh(layout, interface)?,
|
||||
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)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,11 +125,25 @@ const BORDER_COLOR_HOVERED: drawing::Color = drawing::Color::new(1.0, 1.0, 1.0,
|
||||
const GAME_COVER_SIZE_X: f32 = 140.0;
|
||||
const GAME_COVER_SIZE_Y: f32 = 210.0;
|
||||
|
||||
pub fn construct_game_cover(
|
||||
async fn request_cover_image(executor: AsyncExecutor, manifest: steam_utils::AppManifest, tasks: Tasks<Task>) {
|
||||
let cover_art = match cover_art_fetcher::request_image(executor, manifest.app_id.clone()).await {
|
||||
Ok(cover_art) => cover_art,
|
||||
Err(e) => {
|
||||
log::error!("request_cover_image failed: {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
tasks.push(Task::SetCoverArt((manifest.app_id, Rc::from(cover_art))));
|
||||
}
|
||||
|
||||
fn construct_game_cover(
|
||||
ess: &mut ConstructEssentials,
|
||||
globals: &WguiGlobals,
|
||||
executor: &AsyncExecutor,
|
||||
tasks: &Tasks<Task>,
|
||||
_globals: &WguiGlobals,
|
||||
manifest: &steam_utils::AppManifest,
|
||||
) -> anyhow::Result<(WidgetPair, Rc<ComponentButton>)> {
|
||||
) -> anyhow::Result<(WidgetPair, Rc<ComponentButton>, Cell)> {
|
||||
let (widget_button, button) = components::button::construct(
|
||||
ess,
|
||||
components::button::Params {
|
||||
@@ -145,6 +170,20 @@ pub fn construct_game_cover(
|
||||
},
|
||||
)?;
|
||||
|
||||
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),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
let rect_gradient = |color: drawing::Color, color2: drawing::Color| {
|
||||
rectangle::WidgetRectangle::create(rectangle::WidgetRectangleParams {
|
||||
color,
|
||||
@@ -166,7 +205,7 @@ pub fn construct_game_cover(
|
||||
};
|
||||
|
||||
// top shine
|
||||
ess.layout.add_child(
|
||||
let (top_shine, _) = ess.layout.add_child(
|
||||
widget_button.id,
|
||||
rect_gradient(
|
||||
drawing::Color::new(1.0, 1.0, 1.0, 0.25),
|
||||
@@ -175,6 +214,9 @@ pub fn construct_game_cover(
|
||||
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,
|
||||
@@ -190,7 +232,7 @@ pub fn construct_game_cover(
|
||||
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.2),
|
||||
drawing::Color::new(0.0, 0.0, 0.0, 0.25),
|
||||
),
|
||||
rect_gradient_style(taffy::AlignSelf::End, 0.5),
|
||||
)?;
|
||||
@@ -200,23 +242,35 @@ pub fn construct_game_cover(
|
||||
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.2),
|
||||
drawing::Color::new(0.0, 0.0, 0.0, 0.5),
|
||||
),
|
||||
rect_gradient_style(taffy::AlignSelf::End, 0.05),
|
||||
rect_gradient_style(taffy::AlignSelf::End, 0.1),
|
||||
)?;
|
||||
|
||||
Ok((widget_button, button))
|
||||
// request cover image data from the internet or disk cache
|
||||
executor
|
||||
.spawn(request_cover_image(executor.clone(), manifest.clone(), tasks.clone()))
|
||||
.detach();
|
||||
|
||||
Ok((
|
||||
widget_button,
|
||||
button,
|
||||
Cell {
|
||||
image_parent: image_parent.id,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn fill_game_list(
|
||||
globals: &WguiGlobals,
|
||||
ess: &mut ConstructEssentials,
|
||||
interface: &mut BoxDashInterface,
|
||||
executor: &AsyncExecutor,
|
||||
cells: &mut HashMap<AppID, Cell>,
|
||||
games: &Games,
|
||||
tasks: &Tasks<Task>,
|
||||
) -> anyhow::Result<()> {
|
||||
for manifest in &games.manifests {
|
||||
let (_, button) = construct_game_cover(ess, globals, manifest)?;
|
||||
let (_, button, cell) = construct_game_cover(ess, executor, tasks, globals, manifest)?;
|
||||
|
||||
button.on_click({
|
||||
let tasks = tasks.clone();
|
||||
@@ -226,6 +280,8 @@ fn fill_game_list(
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
|
||||
cells.insert(manifest.app_id.clone(), cell);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -240,8 +296,9 @@ impl View {
|
||||
Ok(Games { manifests })
|
||||
}
|
||||
|
||||
fn refresh(&mut self, layout: &mut Layout, interface: &mut BoxDashInterface) -> anyhow::Result<()> {
|
||||
fn refresh(&mut self, layout: &mut Layout, executor: &AsyncExecutor) -> anyhow::Result<()> {
|
||||
layout.remove_children(self.id_list_parent);
|
||||
self.cells.clear();
|
||||
|
||||
let mut text: Option<Translation> = None;
|
||||
match self.game_list() {
|
||||
@@ -255,7 +312,8 @@ impl View {
|
||||
layout,
|
||||
parent: self.id_list_parent,
|
||||
},
|
||||
interface,
|
||||
executor,
|
||||
&mut self.cells,
|
||||
&list,
|
||||
&self.tasks,
|
||||
)?
|
||||
@@ -285,11 +343,7 @@ impl View {
|
||||
self.frontend_tasks.push(FrontendTask::MountPopup(MountPopupParams {
|
||||
title: Translation::from_raw_text(&manifest.name),
|
||||
on_content: {
|
||||
let frontend_tasks = self.frontend_tasks.clone();
|
||||
let globals = self.globals.clone();
|
||||
let tasks = self.tasks.clone();
|
||||
|
||||
Rc::new(move |data| {
|
||||
Rc::new(move |_data| {
|
||||
// todo
|
||||
Ok(())
|
||||
})
|
||||
@@ -298,4 +352,53 @@ impl View {
|
||||
|
||||
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() {
|
||||
return Ok(()); // do nothing
|
||||
}
|
||||
|
||||
let Some(cell) = self.cells.get(app_id) else {
|
||||
debug_assert!(false); // this shouldn't happen
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let glyph_content = match CustomGlyphContent::from_bin_raster(&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(());
|
||||
}
|
||||
};
|
||||
|
||||
let image = WidgetImage::create(WidgetImageParams {
|
||||
round: WLength::Units(12.0),
|
||||
glyph_data: Some(CustomGlyphData::new(glyph_content)),
|
||||
..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(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user