wayvr/dash-frontend/src/views/game_cover.rs

318 lines
7.9 KiB
Rust

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