Game launcher (wip), wgui refactor
[skip ci]
This commit is contained in:
19
dash-frontend/assets/gui/view/game_launcher.xml
Normal file
19
dash-frontend/assets/gui/view/game_launcher.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<layout>
|
||||
<elements>
|
||||
<div flex_direction="row" gap="16">
|
||||
<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" />
|
||||
</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">
|
||||
<sprite src_builtin="dashboard/play.svg" width="32" height="32" />
|
||||
<label text="Launch" weight="bold" size="17" shadow="#00000099" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</elements>
|
||||
</layout>
|
||||
@@ -14,7 +14,6 @@
|
||||
<rectangle
|
||||
position="relative"
|
||||
color="#000000"
|
||||
round="4"
|
||||
width="100%" height="48"
|
||||
>
|
||||
|
||||
|
||||
@@ -26,21 +26,16 @@ enum Task {
|
||||
}
|
||||
|
||||
struct State {
|
||||
launcher: Option<(PopupHandle, views::app_launcher::View)>,
|
||||
view_launcher: Option<(PopupHandle, views::app_launcher::View)>,
|
||||
}
|
||||
|
||||
pub struct TabApps {
|
||||
#[allow(dead_code)]
|
||||
pub parser_state: ParserState,
|
||||
parser_state: ParserState,
|
||||
|
||||
#[allow(dead_code)]
|
||||
state: Rc<RefCell<State>>,
|
||||
|
||||
#[allow(dead_code)]
|
||||
entries: Vec<DesktopEntry>,
|
||||
#[allow(dead_code)]
|
||||
app_list: AppList,
|
||||
|
||||
tasks: Tasks<Task>,
|
||||
}
|
||||
|
||||
@@ -54,11 +49,11 @@ impl Tab for TabApps {
|
||||
|
||||
for task in self.tasks.drain() {
|
||||
match task {
|
||||
Task::CloseLauncher => state.launcher = None,
|
||||
Task::CloseLauncher => state.view_launcher = None,
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((_, launcher)) = &mut state.launcher {
|
||||
if let Some((_, launcher)) = &mut state.view_launcher {
|
||||
launcher.update(&mut frontend.layout, &mut frontend.interface)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -105,7 +100,7 @@ fn on_app_click(
|
||||
on_launched,
|
||||
})?;
|
||||
|
||||
state.borrow_mut().launcher = Some((data.handle, view));
|
||||
state.borrow_mut().view_launcher = Some((data.handle, view));
|
||||
Ok(())
|
||||
})
|
||||
},
|
||||
@@ -129,7 +124,7 @@ impl TabApps {
|
||||
let globals = frontend.layout.state.globals.clone();
|
||||
|
||||
let tasks = Tasks::new();
|
||||
let state = Rc::new(RefCell::new(State { launcher: None }));
|
||||
let state = Rc::new(RefCell::new(State { view_launcher: None }));
|
||||
|
||||
let mut parser_state = wgui::parser::parse_from_assets(doc_params, &mut frontend.layout, parent_id)?;
|
||||
let app_list_parent = parser_state.fetch_widget(&frontend.layout.state, "app_list_parent")?;
|
||||
|
||||
@@ -23,10 +23,7 @@ impl Tab for TabGames {
|
||||
}
|
||||
|
||||
fn update(&mut self, frontend: &mut Frontend) -> anyhow::Result<()> {
|
||||
self
|
||||
.view_game_list
|
||||
.update(&mut frontend.layout, &mut frontend.executor)?;
|
||||
|
||||
self.view_game_list.update(&mut frontend.layout, &frontend.executor)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -46,6 +43,7 @@ impl TabGames {
|
||||
let game_list_parent = state.get_widget_id("game_list_parent")?;
|
||||
|
||||
let view_game_list = game_list::View::new(game_list::Params {
|
||||
executor: frontend.executor.clone(),
|
||||
frontend_tasks: frontend.tasks.clone(),
|
||||
globals: frontend.layout.state.globals.clone(),
|
||||
layout: &mut frontend.layout,
|
||||
|
||||
@@ -40,7 +40,7 @@ fn configure_label_hello(common: &mut CallbackDataCommon, label_hello: Widget, s
|
||||
common.i18n().translate("HELLO").to_string()
|
||||
};
|
||||
|
||||
let mut label_hello = label_hello.get_as_mut::<WidgetLabel>().unwrap();
|
||||
let mut label_hello = label_hello.get_as::<WidgetLabel>().unwrap();
|
||||
label_hello.set_text(common, Translation::from_raw_text(&translated));
|
||||
}
|
||||
|
||||
|
||||
103
dash-frontend/src/util/cached_fetcher.rs
Normal file
103
dash-frontend/src/util/cached_fetcher.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use anyhow::Context;
|
||||
use serde::Deserialize;
|
||||
use wlx_common::cache_dir;
|
||||
|
||||
use crate::util::{http_client, steam_utils::AppID, various::AsyncExecutor};
|
||||
|
||||
pub struct CoverArt {
|
||||
// can be empty in case if data couldn't be fetched (use a fallback image then)
|
||||
pub compressed_image_data: Vec<u8>,
|
||||
}
|
||||
|
||||
pub async fn request_image(executor: AsyncExecutor, app_id: AppID) -> anyhow::Result<CoverArt> {
|
||||
let cache_file_path = format!("cover_arts/{}.bin", app_id);
|
||||
|
||||
// check if file already exists in cache directory
|
||||
if let Some(data) = cache_dir::get_data(&cache_file_path).await {
|
||||
return Ok(CoverArt {
|
||||
compressed_image_data: data,
|
||||
});
|
||||
}
|
||||
|
||||
let url = format!(
|
||||
"https://shared.steamstatic.com/store_item_assets/steam/apps/{}/library_600x900.jpg",
|
||||
app_id
|
||||
);
|
||||
|
||||
match http_client::get(&executor, &url).await {
|
||||
Ok(response) => {
|
||||
log::info!("Success");
|
||||
cache_dir::set_data(&cache_file_path, &response.data).await?;
|
||||
Ok(CoverArt {
|
||||
compressed_image_data: response.data,
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
// fetch failed, write an empty file
|
||||
log::error!("CoverArtFetcher: failed fetch for AppID {}: {}", app_id, e);
|
||||
cache_dir::set_data(&cache_file_path, &[]).await?;
|
||||
Ok(CoverArt {
|
||||
compressed_image_data: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct AppDetailsJSONData {
|
||||
#[allow(dead_code)]
|
||||
pub r#type: String, // "game"
|
||||
#[allow(dead_code)]
|
||||
pub name: String, // "Half-Life 3"
|
||||
#[allow(dead_code)]
|
||||
pub is_free: Option<bool>, // "false"
|
||||
pub detailed_description: Option<String>, //
|
||||
pub short_description: Option<String>, //
|
||||
pub developers: Vec<String>, // ["Valve"]
|
||||
}
|
||||
|
||||
async fn get_app_details_json_internal(
|
||||
executor: AsyncExecutor,
|
||||
cache_file_path: &str,
|
||||
app_id: AppID,
|
||||
) -> anyhow::Result<AppDetailsJSONData> {
|
||||
// check if file already exists in cache directory
|
||||
if let Some(data) = cache_dir::get_data(cache_file_path).await {
|
||||
return Ok(serde_json::from_value(serde_json::from_slice(&data)?)?);
|
||||
}
|
||||
|
||||
// Fetch from Steam API
|
||||
log::info!("Fetching app detail ID {}", app_id);
|
||||
let url = format!("https://store.steampowered.com/api/appdetails?appids={}", app_id);
|
||||
let response = http_client::get(&executor, &url).await?;
|
||||
let res_utf8 = String::from_utf8(response.data)?;
|
||||
let root = serde_json::from_str::<serde_json::Value>(&res_utf8)?;
|
||||
let body = root.get(&app_id).context("invalid body")?;
|
||||
|
||||
if !body.get("success").is_some_and(|v| v.as_bool().unwrap_or(false)) {
|
||||
anyhow::bail!("Failed");
|
||||
}
|
||||
|
||||
let data = body.get("data").context("data null")?;
|
||||
|
||||
let data_bytes = serde_json::to_vec(&data)?;
|
||||
let app_details: AppDetailsJSONData = serde_json::from_value(data.clone())?;
|
||||
|
||||
// cache for future use
|
||||
cache_dir::set_data(cache_file_path, &data_bytes).await?;
|
||||
|
||||
Ok(app_details)
|
||||
}
|
||||
|
||||
pub async fn get_app_details_json(executor: AsyncExecutor, app_id: AppID) -> Option<AppDetailsJSONData> {
|
||||
let cache_file_path = format!("app_details/{}.json", app_id);
|
||||
|
||||
match get_app_details_json_internal(executor, &cache_file_path, app_id).await {
|
||||
Ok(r) => Some(r),
|
||||
Err(e) => {
|
||||
log::error!("Failed to get app details: {:?}", e);
|
||||
let _ = cache_dir::set_data(&cache_file_path, &[]).await; // write empty data
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
use wlx_common::cache_dir;
|
||||
|
||||
use crate::util::{http_client, steam_utils::AppID, various::AsyncExecutor};
|
||||
|
||||
pub struct CoverArt {
|
||||
// can be empty in case if data couldn't be fetched (use a fallback image then)
|
||||
pub compressed_image_data: Vec<u8>,
|
||||
}
|
||||
|
||||
pub async fn request_image(executor: AsyncExecutor, app_id: AppID) -> anyhow::Result<CoverArt> {
|
||||
let cache_file_path = format!("cover_arts/{}.bin", app_id);
|
||||
|
||||
// check if file already exists in cache directory
|
||||
if let Some(data) = cache_dir::get_data(&cache_file_path).await {
|
||||
return Ok(CoverArt {
|
||||
compressed_image_data: data,
|
||||
});
|
||||
}
|
||||
|
||||
let url = format!(
|
||||
"https://shared.steamstatic.com/store_item_assets/steam/apps/{}/library_600x900.jpg",
|
||||
app_id
|
||||
);
|
||||
|
||||
match http_client::get(&executor, &url).await {
|
||||
Ok(response) => {
|
||||
log::info!("Success");
|
||||
cache_dir::set_data(&cache_file_path, &response.data).await?;
|
||||
Ok(CoverArt {
|
||||
compressed_image_data: response.data,
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
// fetch failed, write an empty file
|
||||
log::error!("CoverArtFetcher: failed fetch for AppID {}: {}", app_id, e);
|
||||
cache_dir::set_data(&cache_file_path, &[]).await?;
|
||||
Ok(CoverArt {
|
||||
compressed_image_data: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
pub mod cover_art_fetcher;
|
||||
pub mod cached_fetcher;
|
||||
pub mod desktop_finder;
|
||||
pub mod http_client;
|
||||
pub mod pactl_wrapper;
|
||||
|
||||
158
dash-frontend/src/views/game_launcher.rs
Normal file
158
dash-frontend/src/views/game_launcher.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
use crate::{
|
||||
frontend::{FrontendTask, FrontendTasks},
|
||||
util::{
|
||||
cached_fetcher::{self},
|
||||
steam_utils::{AppID, AppManifest},
|
||||
various::AsyncExecutor,
|
||||
},
|
||||
};
|
||||
use wgui::{
|
||||
assets::AssetPath,
|
||||
components::button::ComponentButton,
|
||||
globals::WguiGlobals,
|
||||
i18n::Translation,
|
||||
layout::{Layout, WidgetID},
|
||||
parser::{Fetchable, ParseDocumentParams, ParserState},
|
||||
task::Tasks,
|
||||
widget::label::WidgetLabel,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Task {
|
||||
FillAppDetails(cached_fetcher::AppDetailsJSONData),
|
||||
Launch,
|
||||
}
|
||||
|
||||
pub struct Params<'a> {
|
||||
pub globals: &'a WguiGlobals,
|
||||
pub executor: AsyncExecutor,
|
||||
pub manifest: AppManifest,
|
||||
pub layout: &'a mut Layout,
|
||||
pub parent_id: WidgetID,
|
||||
pub frontend_tasks: &'a FrontendTasks,
|
||||
pub on_launched: Box<dyn Fn()>,
|
||||
}
|
||||
pub struct View {
|
||||
#[allow(dead_code)]
|
||||
state: ParserState,
|
||||
tasks: Tasks<Task>,
|
||||
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,
|
||||
}
|
||||
|
||||
impl View {
|
||||
async fn fetch_details(executor: AsyncExecutor, tasks: Tasks<Task>, app_id: AppID) {
|
||||
let Some(details) = cached_fetcher::get_app_details_json(executor, app_id).await else {
|
||||
return;
|
||||
};
|
||||
|
||||
tasks.push(Task::FillAppDetails(details));
|
||||
}
|
||||
|
||||
pub fn new(params: Params) -> anyhow::Result<Self> {
|
||||
let doc_params = &ParseDocumentParams {
|
||||
globals: params.globals.clone(),
|
||||
path: AssetPath::BuiltIn("gui/view/game_launcher.xml"),
|
||||
extra: Default::default(),
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
// fetch details from the web
|
||||
let fut = View::fetch_details(params.executor.clone(), tasks.clone(), params.manifest.app_id.clone());
|
||||
params.executor.spawn(fut).detach();
|
||||
|
||||
let id_cover_art_parent = state.get_widget_id("cover_art_parent")?;
|
||||
let btn_launch = state.fetch_component_as::<ComponentButton>("btn_launch")?;
|
||||
|
||||
tasks.handle_button(&btn_launch, Task::Launch);
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update(&mut self, layout: &mut Layout) -> anyhow::Result<()> {
|
||||
loop {
|
||||
let tasks = self.tasks.drain();
|
||||
if tasks.is_empty() {
|
||||
break;
|
||||
}
|
||||
for task in tasks {
|
||||
match task {
|
||||
Task::FillAppDetails(details) => self.action_fill_app_details(layout, details)?,
|
||||
Task::Launch => self.action_launch(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_fill_app_details(
|
||||
&mut self,
|
||||
layout: &mut Layout,
|
||||
mut details: cached_fetcher::AppDetailsJSONData,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut c = layout.start_common();
|
||||
|
||||
{
|
||||
let label_author = self.state.fetch_widget(&c.layout.state, "label_author")?.widget;
|
||||
let label_description = self.state.fetch_widget(&c.layout.state, "label_description")?.widget;
|
||||
|
||||
if let Some(developer) = details.developers.pop() {
|
||||
label_author
|
||||
.cast::<WidgetLabel>()?
|
||||
.set_text(&mut c.common(), Translation::from_raw_text_string(developer));
|
||||
}
|
||||
|
||||
let desc = if let Some(desc) = &details.short_description {
|
||||
Some(desc)
|
||||
} else if let Some(desc) = &details.detailed_description {
|
||||
Some(desc)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(desc) = desc {
|
||||
label_description
|
||||
.cast::<WidgetLabel>()?
|
||||
.set_text(&mut c.common(), Translation::from_raw_text(desc));
|
||||
}
|
||||
}
|
||||
|
||||
c.finish()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_launch(&mut self) {
|
||||
self
|
||||
.frontend_tasks
|
||||
.push(FrontendTask::PushToast(Translation::from_raw_text("Game launch TODO")));
|
||||
(*self.on_launched)();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{collections::HashMap, rc::Rc};
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||
|
||||
use wgui::{
|
||||
assets::AssetPath,
|
||||
@@ -34,43 +34,51 @@ use wgui::{
|
||||
use crate::{
|
||||
frontend::{FrontendTask, FrontendTasks},
|
||||
util::{
|
||||
cover_art_fetcher::{self, CoverArt},
|
||||
popup_manager::MountPopupParams,
|
||||
cached_fetcher::{self, CoverArt},
|
||||
popup_manager::{MountPopupParams, PopupHandle},
|
||||
steam_utils::{self, AppID, AppManifest, SteamUtils},
|
||||
various::AsyncExecutor,
|
||||
},
|
||||
views::{self, game_launcher},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Task {
|
||||
AppManifestClicked(steam_utils::AppManifest),
|
||||
SetCoverArt((AppID, Rc<CoverArt>)),
|
||||
CloseLauncher,
|
||||
Refresh,
|
||||
}
|
||||
|
||||
pub struct Params<'a> {
|
||||
pub globals: WguiGlobals,
|
||||
pub executor: AsyncExecutor,
|
||||
pub frontend_tasks: FrontendTasks,
|
||||
pub layout: &'a mut Layout,
|
||||
pub parent_id: WidgetID,
|
||||
}
|
||||
|
||||
struct Cell {
|
||||
pub struct Cell {
|
||||
image_parent: WidgetID,
|
||||
manifest: AppManifest,
|
||||
}
|
||||
|
||||
struct State {
|
||||
view_launcher: Option<(PopupHandle, views::game_launcher::View)>,
|
||||
}
|
||||
|
||||
pub struct View {
|
||||
#[allow(dead_code)]
|
||||
pub parser_state: ParserState,
|
||||
parser_state: ParserState,
|
||||
tasks: Tasks<Task>,
|
||||
frontend_tasks: FrontendTasks,
|
||||
globals: WguiGlobals,
|
||||
id_list_parent: WidgetID,
|
||||
steam_utils: steam_utils::SteamUtils,
|
||||
|
||||
cells: HashMap<AppID, Cell>,
|
||||
img_placeholder: Option<CustomGlyphData>,
|
||||
executor: AsyncExecutor,
|
||||
state: Rc<RefCell<State>>,
|
||||
}
|
||||
|
||||
impl View {
|
||||
@@ -99,6 +107,8 @@ impl View {
|
||||
steam_utils,
|
||||
cells: HashMap::new(),
|
||||
img_placeholder: None,
|
||||
state: Rc::new(RefCell::new(State { view_launcher: None })),
|
||||
executor: params.executor,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -113,10 +123,16 @@ impl View {
|
||||
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::CloseLauncher => self.state.borrow_mut().view_launcher = None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut state = self.state.borrow_mut();
|
||||
if let Some((_, view)) = &mut state.view_launcher {
|
||||
view.update(layout)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -131,8 +147,12 @@ 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;
|
||||
|
||||
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 {
|
||||
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);
|
||||
@@ -140,15 +160,15 @@ async fn request_cover_image(executor: AsyncExecutor, manifest: steam_utils::App
|
||||
}
|
||||
};
|
||||
|
||||
tasks.push(Task::SetCoverArt((manifest.app_id, Rc::from(cover_art))));
|
||||
on_loaded(cover_art)
|
||||
}
|
||||
|
||||
fn construct_game_cover(
|
||||
pub fn construct_game_cover(
|
||||
ess: &mut ConstructEssentials,
|
||||
executor: &AsyncExecutor,
|
||||
tasks: &Tasks<Task>,
|
||||
_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,
|
||||
@@ -257,7 +277,7 @@ fn construct_game_cover(
|
||||
|
||||
// request cover image data from the internet or disk cache
|
||||
executor
|
||||
.spawn(request_cover_image(executor.clone(), manifest.clone(), tasks.clone()))
|
||||
.spawn(request_cover_image(executor.clone(), manifest.clone(), on_loaded))
|
||||
.detach();
|
||||
|
||||
Ok((
|
||||
@@ -279,7 +299,15 @@ fn fill_game_list(
|
||||
tasks: &Tasks<Task>,
|
||||
) -> anyhow::Result<()> {
|
||||
for manifest in &games.manifests {
|
||||
let (_, button, cell) = construct_game_cover(ess, executor, tasks, globals, manifest)?;
|
||||
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))));
|
||||
}
|
||||
};
|
||||
|
||||
let (_, button, cell) = construct_game_cover(ess, executor, globals, manifest, Box::new(on_loaded))?;
|
||||
|
||||
button.on_click({
|
||||
let tasks = tasks.clone();
|
||||
@@ -352,8 +380,29 @@ impl View {
|
||||
self.frontend_tasks.push(FrontendTask::MountPopup(MountPopupParams {
|
||||
title: Translation::from_raw_text(&manifest.name),
|
||||
on_content: {
|
||||
Rc::new(move |_data| {
|
||||
// todo
|
||||
let state = self.state.clone();
|
||||
let tasks = self.tasks.clone();
|
||||
let executor = self.executor.clone();
|
||||
let globals = self.globals.clone();
|
||||
let frontend_tasks = self.frontend_tasks.clone();
|
||||
|
||||
Rc::new(move |data| {
|
||||
let on_launched = {
|
||||
let tasks = tasks.clone();
|
||||
Box::new(move || tasks.push(Task::CloseLauncher))
|
||||
};
|
||||
|
||||
let view = game_launcher::View::new(game_launcher::Params {
|
||||
manifest: manifest.clone(),
|
||||
executor: executor.clone(),
|
||||
globals: &globals,
|
||||
layout: data.layout,
|
||||
parent_id: data.id_content,
|
||||
frontend_tasks: &frontend_tasks,
|
||||
on_launched,
|
||||
})?;
|
||||
|
||||
state.borrow_mut().view_launcher = Some((data.handle, view));
|
||||
Ok(())
|
||||
})
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod app_launcher;
|
||||
pub mod audio_settings;
|
||||
pub mod game_launcher;
|
||||
pub mod game_list;
|
||||
pub mod process_list;
|
||||
pub mod window_list;
|
||||
|
||||
@@ -51,7 +51,7 @@ fn button_click_callback(
|
||||
) -> ButtonClickCallback {
|
||||
Box::new(move |common, _e| {
|
||||
label
|
||||
.get_as_mut::<WidgetLabel>()
|
||||
.get_as::<WidgetLabel>()
|
||||
.unwrap()
|
||||
.set_text(common, Translation::from_raw_text(text));
|
||||
|
||||
@@ -161,7 +161,7 @@ impl TestbedGeneric {
|
||||
let cb_first = state.fetch_component_as::<ComponentCheckbox>("cb_first")?;
|
||||
let label = label_cur_option.widget.clone();
|
||||
cb_first.on_toggle(Box::new(move |common, e| {
|
||||
let mut widget = label.get_as_mut::<WidgetLabel>().unwrap();
|
||||
let mut widget = label.get_as::<WidgetLabel>().unwrap();
|
||||
let text = format!("checkbox toggle: {}", e.checked);
|
||||
widget.set_text(common, Translation::from_raw_text(&text));
|
||||
Ok(())
|
||||
|
||||
@@ -340,6 +340,7 @@ fn register_event_mouse_press(state: Rc<RefCell<State>>, listeners: &mut EventLi
|
||||
if state.hovered {
|
||||
state.down = true;
|
||||
state.last_pressed = Instant::now();
|
||||
state.active_tooltip = None;
|
||||
Ok(EventResult::Consumed)
|
||||
} else {
|
||||
Ok(EventResult::Pass)
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::{
|
||||
widget::{self, EventParams, EventResult, WidgetObj, WidgetState, WidgetStateFlags, div::WidgetDiv},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use glam::{Vec2, vec2};
|
||||
use slotmap::{HopSlotMap, SecondaryMap, new_key_type};
|
||||
use taffy::{NodeId, TaffyTree, TraversePartialTree};
|
||||
@@ -31,10 +32,14 @@ impl Widget {
|
||||
Self(Rc::new(RefCell::new(widget_state)))
|
||||
}
|
||||
|
||||
pub fn get_as_mut<T: 'static>(&self) -> Option<RefMut<'_, T>> {
|
||||
pub fn get_as<T: 'static>(&self) -> Option<RefMut<'_, T>> {
|
||||
RefMut::filter_map(self.0.borrow_mut(), |w| w.obj.get_as_mut::<T>()).ok()
|
||||
}
|
||||
|
||||
pub fn cast<T: 'static>(&self) -> anyhow::Result<RefMut<'_, T>> {
|
||||
self.get_as().context("Widget cast failed")
|
||||
}
|
||||
|
||||
pub fn downgrade(&self) -> WeakWidget {
|
||||
WeakWidget(Rc::downgrade(&self.0))
|
||||
}
|
||||
@@ -65,7 +70,7 @@ impl WidgetMap {
|
||||
}
|
||||
|
||||
pub fn get_as<T: 'static>(&self, handle: WidgetID) -> Option<RefMut<'_, T>> {
|
||||
self.0.get(handle)?.get_as_mut::<T>()
|
||||
self.0.get(handle)?.get_as::<T>()
|
||||
}
|
||||
|
||||
pub fn get(&self, handle: WidgetID) -> Option<&Widget> {
|
||||
@@ -97,7 +102,7 @@ impl WidgetMap {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(mut casted) = widget.get_as_mut::<WIDGET>() {
|
||||
if let Some(mut casted) = widget.get_as::<WIDGET>() {
|
||||
func(&mut casted);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ mod widget_rectangle;
|
||||
mod widget_sprite;
|
||||
|
||||
use crate::{
|
||||
assets::{normalize_path, AssetPath, AssetPathOwned},
|
||||
assets::{AssetPath, AssetPathOwned, normalize_path},
|
||||
components::{Component, ComponentWeak},
|
||||
drawing::{self},
|
||||
globals::WguiGlobals,
|
||||
@@ -183,7 +183,7 @@ impl Fetchable for ParserData {
|
||||
.ok_or_else(|| anyhow::anyhow!("fetch_widget_as({id}): widget not found"))?;
|
||||
|
||||
let casted = widget
|
||||
.get_as_mut::<T>()
|
||||
.get_as::<T>()
|
||||
.ok_or_else(|| anyhow::anyhow!("fetch_widget_as({id}): failed to cast"))?;
|
||||
|
||||
Ok(casted)
|
||||
@@ -1002,7 +1002,7 @@ impl CustomAttribsInfo<'_> {
|
||||
}
|
||||
|
||||
pub fn get_widget_as<T: 'static>(&self) -> Option<RefMut<'_, T>> {
|
||||
self.widgets.get(self.widget_id)?.get_as_mut::<T>()
|
||||
self.widgets.get(self.widget_id)?.get_as::<T>()
|
||||
}
|
||||
|
||||
pub fn get_value(&self, attrib_name: &str) -> Option<Rc<str>> {
|
||||
|
||||
@@ -105,7 +105,7 @@ fn apply_custom_command(
|
||||
label.set_text(&mut com, Translation::from_raw_text(text));
|
||||
} else if let Ok(button) = panel
|
||||
.parser_state
|
||||
.fetch_component_as::<ComponentButton>(&element)
|
||||
.fetch_component_as::<ComponentButton>(element)
|
||||
{
|
||||
button.set_text(&mut com, Translation::from_raw_text(text));
|
||||
} else {
|
||||
@@ -118,15 +118,15 @@ fn apply_custom_command(
|
||||
.fetch_widget(&panel.layout.state, element)
|
||||
{
|
||||
let content = CustomGlyphContent::from_assets(
|
||||
&mut app.wgui_globals,
|
||||
wgui::assets::AssetPath::File(&path),
|
||||
&app.wgui_globals,
|
||||
wgui::assets::AssetPath::File(path),
|
||||
)
|
||||
.context("Could not load content from supplied path.")?;
|
||||
let data = CustomGlyphData::new(content);
|
||||
|
||||
if let Some(mut sprite) = pair.widget.get_as_mut::<WidgetSprite>() {
|
||||
if let Some(mut sprite) = pair.widget.get_as::<WidgetSprite>() {
|
||||
sprite.set_content(&mut com, Some(data));
|
||||
} else if let Some(mut image) = pair.widget.get_as_mut::<WidgetImage>() {
|
||||
} else if let Some(mut image) = pair.widget.get_as::<WidgetImage>() {
|
||||
image.set_content(&mut com, Some(data));
|
||||
} else {
|
||||
anyhow::bail!("No <sprite> or <image> with such id.");
|
||||
@@ -136,18 +136,18 @@ fn apply_custom_command(
|
||||
}
|
||||
}
|
||||
ModifyPanelCommand::SetColor(color) => {
|
||||
let color = parse_color_hex(&color)
|
||||
let color = parse_color_hex(color)
|
||||
.context("Invalid color format, must be a html hex color!")?;
|
||||
|
||||
if let Ok(pair) = panel
|
||||
.parser_state
|
||||
.fetch_widget(&panel.layout.state, element)
|
||||
{
|
||||
if let Some(mut rect) = pair.widget.get_as_mut::<WidgetRectangle>() {
|
||||
if let Some(mut rect) = pair.widget.get_as::<WidgetRectangle>() {
|
||||
rect.set_color(&mut com, color);
|
||||
} else if let Some(mut label) = pair.widget.get_as_mut::<WidgetLabel>() {
|
||||
} else if let Some(mut label) = pair.widget.get_as::<WidgetLabel>() {
|
||||
label.set_color(&mut com, color, true);
|
||||
} else if let Some(mut sprite) = pair.widget.get_as_mut::<WidgetSprite>() {
|
||||
} else if let Some(mut sprite) = pair.widget.get_as::<WidgetSprite>() {
|
||||
sprite.set_color(&mut com, color);
|
||||
} else {
|
||||
anyhow::bail!("No <rectangle> or <label> or <sprite> with such id.");
|
||||
@@ -159,7 +159,7 @@ fn apply_custom_command(
|
||||
ModifyPanelCommand::SetVisible(visible) => {
|
||||
let wid = panel
|
||||
.parser_state
|
||||
.get_widget_id(&element)
|
||||
.get_widget_id(element)
|
||||
.context("No widget with such id.")?;
|
||||
|
||||
let display = if *visible {
|
||||
|
||||
Reference in New Issue
Block a user