dash-frontend: list skymaps from the server (wip), loading spinner, wgui: animation bugfix

This commit is contained in:
Aleksander 2026-04-06 13:22:59 +02:00 committed by galister
parent a3460fbe70
commit f2dcdcc6c3
23 changed files with 594 additions and 110 deletions

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE --><path fill="currentColor" d="M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8"/></svg>

After

Width:  |  Height:  |  Size: 286 B

View File

@ -0,0 +1,16 @@
<layout>
<template name="LoadingWithText">
<div id="root" width="100%" height="100%" align_items="center" justify_content="center">
<div flex_direction="row" gap="8" align_items="center" >
<sprite id="sprite_loading" src_builtin="dashboard/loading.svg" width="32" height="32"/>
<label translation="LOADING" weight="bold"/>
</div>
</div>
</template>
<template name="LoadingWithoutText">
<div id="root" width="100%" height="100%" align_items="center" justify_content="center">
<sprite id="sprite_loading" src_builtin="dashboard/loading.svg" width="32" height="32"/>
</div>
</template>
</layout>

View File

@ -0,0 +1,7 @@
<layout>
<elements>
<div id="list" gap="8" flex_wrap="wrap">
<!-- filled-in at runtime -->
</div>
</elements>
</layout>

View File

@ -1,7 +1,7 @@
<layout>
<elements>
<div flex_direction="column">
<div id="list_parent" gap="8" flex_direction="row" flex_wrap="wrap" justify_content="center" />
<div id="list_parent" gap="8" flex_direction="row" flex_wrap="wrap" overflow_y="scroll"/>
</div>
<div flex_direction="row" gap="4">
<Button id="btn_refresh" tooltip="REFRESH" width="32" height="32" sprite_src_builtin="dashboard/refresh.svg" />

View File

@ -0,0 +1,26 @@
<layout>
<!--
ids:
"image_preview"
"label_title"
"label_desc"
-->
<template name="Cell">
<rectangle
border="2"
color="~color_bg_translucent"
border_color="~color_accent"
padding="8"
round="8"
flex_direction="column"
gap="4"
width="256"
align_items="center"
align_self="start">
<image id="image_preview" width="100%" height="128"/>
<label id="label_title" wrap="1" weight="bold"/>
<label id="label_author" wrap="1"/>
</rectangle>
</template>
</layout>

View File

@ -155,6 +155,7 @@
"HIDE": "Hide",
"HOME_SCREEN": "Home",
"MONADO_RUNTIME": "Monado runtime",
"LOADING": "Loading...",
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "Resolution"
},

View File

@ -28,7 +28,7 @@ use crate::{
assets,
tab::{Tab, TabType, apps::TabApps, games::TabGames, home::TabHome, monado::TabMonado, settings::TabSettings},
util::{
popup_manager::{MountPopupParams, PopupManager, PopupManagerParams},
popup_manager::{MountPopupOnceParams, MountPopupParams, PopupManager, PopupManagerParams},
toast_manager::ToastManager,
},
views,
@ -102,6 +102,7 @@ pub enum FrontendTask {
RefreshClock,
RefreshBackground,
MountPopup(MountPopupParams),
MountPopupOnce(MountPopupOnceParams),
RefreshPopupManager,
ShowAudioSettings,
UpdateAudioSettingsView,
@ -310,13 +311,18 @@ impl<T: 'static> Frontend<T> {
fn mount_popup(&mut self, params: MountPopupParams, data: &mut T) -> anyhow::Result<()> {
let config = self.interface.general_config(data);
self.popup_manager.mount_popup(
self.globals.clone(),
&mut self.layout,
self.tasks.clone(),
params,
config,
)?;
self
.popup_manager
.mount_popup(&self.globals, &mut self.layout, &self.tasks, params, config)?;
Ok(())
}
fn mount_popup_once(&mut self, params: MountPopupOnceParams, data: &mut T) -> anyhow::Result<()> {
let config = self.interface.general_config(data);
self
.popup_manager
.mount_popup_once(&self.globals, &mut self.layout, &self.tasks, params, config)?;
Ok(())
}
@ -357,6 +363,7 @@ impl<T: 'static> Frontend<T> {
FrontendTask::RefreshClock => self.update_time(params.data)?,
FrontendTask::RefreshBackground => self.update_background(params.data)?,
FrontendTask::MountPopup(popup_params) => self.mount_popup(popup_params, params.data)?,
FrontendTask::MountPopupOnce(popup_params) => self.mount_popup_once(popup_params, params.data)?,
FrontendTask::RefreshPopupManager => self.refresh_popup_manager()?,
FrontendTask::ShowAudioSettings => self.action_show_audio_settings()?,
FrontendTask::UpdateAudioSettingsView => self.action_update_audio_settings()?,

View File

@ -9,7 +9,6 @@ use wgui::{
assets::AssetPath,
components::button::{ButtonClickCallback, ComponentButton},
globals::WguiGlobals,
i18n::Translation,
layout::{WidgetID, WidgetPair},
parser::{Fetchable, ParseDocumentParams, ParserState},
task::Tasks,
@ -17,18 +16,19 @@ use wgui::{
use wlx_common::desktop_finder::DesktopEntry;
use crate::{
frontend::{Frontend, FrontendTask, FrontendTasks},
frontend::{Frontend, FrontendTasks},
tab::{Tab, TabType},
util::popup_manager::{MountPopupParams, PopupHandle},
views::{self, app_launcher},
util::popup_manager::PopupHolder,
views::{self},
};
#[derive(Clone)]
enum Task {
CloseLauncher,
}
struct State {
view_launcher: Option<(PopupHandle, views::app_launcher::View)>,
view_launcher: Option<PopupHolder<views::app_launcher::View>>,
}
pub struct TabApps<T> {
@ -82,37 +82,16 @@ fn on_app_click(
tasks: Tasks<Task>,
) -> ButtonClickCallback {
Rc::new(move |_common, _evt| {
frontend_tasks.push(FrontendTask::MountPopup(MountPopupParams {
title: Translation::from_raw_text(&entry.app_name),
on_content: {
// this is awful
views::app_launcher::mount_popup(
frontend_tasks.clone(),
globals.clone(),
entry.clone(),
tasks.make_callback_box(Task::CloseLauncher),
Box::new({
let state = state.clone();
let entry = entry.clone();
let globals = globals.clone();
let frontend_tasks = frontend_tasks.clone();
let tasks = tasks.clone();
Rc::new(move |data| {
let on_launched = {
let tasks = tasks.clone();
Box::new(move || tasks.push(Task::CloseLauncher))
};
let view = app_launcher::View::new(app_launcher::Params {
entry: entry.clone(),
globals: &globals,
layout: data.layout,
parent_id: data.id_content,
frontend_tasks: &frontend_tasks,
config: data.config,
on_launched,
})?;
state.borrow_mut().view_launcher = Some((data.handle, view));
Ok(())
})
},
}));
move |popup| state.borrow_mut().view_launcher = Some(popup)
}),
);
Ok(())
})
}

View File

@ -25,7 +25,7 @@ use wlx_common::{
};
use crate::{
frontend::{Frontend, FrontendTask},
frontend::{Frontend, FrontendTask, FrontendTasks},
tab::{Tab, TabType, settings::macros::MacroParams},
};
@ -83,6 +83,7 @@ enum Task {
struct SettingsMountParams<'a> {
mp: &'a mut MacroParams<'a>,
globals: &'a WguiGlobals,
frontend_tasks: &'a FrontendTasks,
parent_id: WidgetID,
}
@ -107,6 +108,7 @@ pub struct TabSettings<T> {
tasks: Tasks<Task>,
marker: PhantomData<T>,
frontend_tasks: FrontendTasks,
}
impl<T> Tab<T> for TabSettings<T> {
@ -550,6 +552,7 @@ impl<T> TabSettings<T> {
mp: &mut mp,
globals: &globals,
parent_id: root,
frontend_tasks: &self.frontend_tasks,
};
match name {
@ -611,6 +614,7 @@ impl<T> TabSettings<T> {
marker: PhantomData,
context_menu: ContextMenu::default(),
current_tab: None,
frontend_tasks: frontend.tasks.clone(),
})
}
}

View File

@ -27,6 +27,7 @@ impl State {
globals: par.globals.clone(),
layout: par.mp.layout,
parent_id: c,
frontend_tasks: par.frontend_tasks,
})?;
Ok(Self { skymap_list })

View File

@ -4,6 +4,8 @@ use wlx_common::async_executor::AsyncExecutor;
use crate::util::{http_client, networking};
pub type SkymapUuid = String;
#[derive(Clone, Debug, Deserialize)]
pub struct SkymapCatalogEntryFiles {
pub size_16k: Option<String>, // "my_skymap_16k.png"
@ -15,10 +17,10 @@ pub struct SkymapCatalogEntryFiles {
#[derive(Clone, Debug, Deserialize)]
pub struct SkymapCatalogEntry {
pub uuid: String,
pub uuid: SkymapUuid,
pub created_at: String,
pub modified_at: String,
pub entry_version: u32,
pub version: u32,
pub name: String,
pub description: String,
pub author: String,
@ -32,11 +34,25 @@ pub struct SkymapCatalog {
pub entries: Vec<SkymapCatalogEntry>,
}
impl SkymapCatalog {
fn validate(&self) -> anyhow::Result<()> {
if self.version != 1 {
anyhow::bail!("Unsupported version");
}
if self.r#type != "wayvr_skymaps" {
anyhow::bail!("Unsupported type");
}
Ok(())
}
}
pub async fn request_catalog(executor: &AsyncExecutor) -> anyhow::Result<SkymapCatalog> {
log::info!("Fetching skymap list");
let res = http_client::get(executor, &format!("{}/catalog.json", networking::WAYVR_SKYMAPS_ROOT)).await?;
let catalog = res.as_json::<SkymapCatalog>()?;
catalog.validate()?;
Ok(catalog)
}

View File

@ -43,6 +43,8 @@ pub struct PopupHandle {
state: Rc<RefCell<MountedPopupState>>,
}
pub type PopupHolder<ViewType> = (PopupHandle, ViewType);
impl PopupHandle {
pub fn close(&self) {
self.state.borrow_mut().mounted_popup = None; // Drop will be called
@ -67,6 +69,23 @@ pub struct MountPopupParams {
pub on_content: Rc<dyn Fn(PopupContentFuncData) -> anyhow::Result<()>>,
}
// we need to implement Clone here, but the underlying function can be called only once.
// on_content will be cleared after the first call
#[derive(Clone)]
pub struct MountPopupOnceParams {
title: Translation,
on_content: Rc<RefCell<Option<Box<dyn FnOnce(PopupContentFuncData) -> anyhow::Result<()>>>>>,
}
impl MountPopupOnceParams {
pub fn new(title: Translation, on_content: Box<dyn FnOnce(PopupContentFuncData) -> anyhow::Result<()>>) -> Self {
Self {
title,
on_content: Rc::new(RefCell::new(Some(on_content))),
}
}
}
impl Drop for MountedPopup {
fn drop(&mut self) {
self.layout_tasks.push(LayoutTask::RemoveWidget(self.id_root));
@ -116,16 +135,13 @@ impl PopupManager {
state.refresh_stack(alterables);
}
/// Mount a new popup on top of the existing popup stack.
/// Only the topmost popup is visible.
pub fn mount_popup(
&mut self,
globals: WguiGlobals,
fn mount_popup_prepare(
&self,
globals: &WguiGlobals,
layout: &mut Layout,
frontend_tasks: FrontendTasks,
params: MountPopupParams,
config: &GeneralConfig,
) -> anyhow::Result<()> {
frontend_tasks: &FrontendTasks,
popup_title: &Translation,
) -> anyhow::Result<(PopupHandle, WidgetID /* content widget ID */)> {
let doc_params = &ParseDocumentParams {
globals: globals.clone(),
path: AssetPath::BuiltIn("gui/view/popup_window.xml"),
@ -138,7 +154,7 @@ impl PopupManager {
{
let mut label_title = state.fetch_widget_as::<WidgetLabel>(&layout.state, "popup_title")?;
label_title.set_text_simple(&mut globals.get(), params.title);
label_title.set_text_simple(&mut globals.get(), popup_title.clone());
}
let but_back = state.fetch_component_as::<ComponentButton>("but_back")?;
@ -172,6 +188,48 @@ impl PopupManager {
});
frontend_tasks.push(FrontendTask::RefreshPopupManager);
Ok((popup_handle, id_content))
}
/// Mount a new popup on top of the existing popup stack (non-cloneable version).
/// Only the topmost popup is visible.
pub fn mount_popup_once(
&mut self,
globals: &WguiGlobals,
layout: &mut Layout,
frontend_tasks: &FrontendTasks,
params: MountPopupOnceParams,
config: &GeneralConfig,
) -> anyhow::Result<()> {
let mut func = params.on_content.borrow_mut();
let Some(on_content_func) = func.take() else {
anyhow::bail!("mount_popup_once called more than once");
};
let (popup_handle, id_content) = self.mount_popup_prepare(globals, layout, frontend_tasks, &params.title)?;
// mount user-set popup content
on_content_func(PopupContentFuncData {
layout,
handle: popup_handle.clone(),
id_content,
config,
})?;
Ok(())
}
/// Mount a new popup on top of the existing popup stack.
/// Only the topmost popup is visible.
pub fn mount_popup(
&mut self,
globals: &WguiGlobals,
layout: &mut Layout,
frontend_tasks: &FrontendTasks,
params: MountPopupParams,
config: &GeneralConfig,
) -> anyhow::Result<()> {
let (popup_handle, id_content) = self.mount_popup_prepare(globals, layout, frontend_tasks, &params.title)?;
// mount user-set popup content
(*params.on_content)(PopupContentFuncData {

View File

@ -1,11 +1,18 @@
use glam::Mat4;
use wgui::{
animation::{Animation, AnimationEasing},
assets::AssetPath,
drawing,
i18n::Translation,
layout::{Layout, WidgetID},
renderer_vk::text::TextStyle,
layout::{Layout, LayoutTask, WidgetID},
parser::{Fetchable, ParseDocumentParams},
renderer_vk::{
text::{FontWeight, TextStyle},
util::centered_matrix,
},
widget::label::{WidgetLabel, WidgetLabelParams},
};
#[allow(dead_code)]
pub fn create_label(layout: &mut Layout, parent: WidgetID, content: Translation) -> anyhow::Result<()> {
let label = WidgetLabel::create(
&mut layout.state,
@ -22,3 +29,73 @@ pub fn create_label(layout: &mut Layout, parent: WidgetID, content: Translation)
Ok(())
}
pub fn create_label_error(layout: &mut Layout, parent: WidgetID, content: String) -> anyhow::Result<()> {
let label = WidgetLabel::create(
&mut layout.state,
WidgetLabelParams {
content: Translation::from_raw_text_string(content),
style: TextStyle {
wrap: true,
color: Some(drawing::Color::new(1.0, 0.5, 0.0, 1.0)),
weight: Some(FontWeight::Bold),
..Default::default()
},
},
);
layout.add_child(parent, label, Default::default())?;
Ok(())
}
pub struct CreateLoadingParams<'a> {
pub layout: &'a mut Layout,
pub parent_id: WidgetID,
pub with_text: bool,
}
pub fn create_loading(par: CreateLoadingParams) -> anyhow::Result<WidgetID> {
let doc_params = ParseDocumentParams {
globals: par.layout.state.globals.clone(),
path: AssetPath::BuiltIn("gui/t_loading.xml"),
extra: Default::default(),
};
let mut parser_state = wgui::parser::parse_from_assets(&doc_params, par.layout, par.parent_id)?;
let data = parser_state.realize_template(
&doc_params,
if par.with_text {
"LoadingWithText"
} else {
"LoadingWithoutText"
},
par.layout,
par.parent_id,
Default::default(),
)?;
let id_root = data.get_widget_id("root")?;
let id_sprite_loading = data.get_widget_id("sprite_loading")?;
par.layout.animations.add(Animation::new(
id_sprite_loading,
60 * 30, /* spin it for 30 seconds at most */
AnimationEasing::Linear,
Box::new(move |common, data| {
// spin it
data.data.transform = centered_matrix(data.widget_boundary.size, &Mat4::from_rotation_z(data.pos * 400.0));
if data.pos == 1.0 {
// remove the spinner, do not waste energy
common
.alterables
.tasks
.push(LayoutTask::RemoveWidget(id_sprite_loading));
}
common.alterables.mark_redraw();
}),
));
Ok(id_root)
}

View File

@ -14,7 +14,10 @@ use wgui::{
};
use wlx_common::{config::GeneralConfig, dash_interface::BoxDashInterface, desktop_finder::DesktopEntry};
use crate::frontend::{FrontendTask, FrontendTasks, SoundType};
use crate::{
frontend::{FrontendTask, FrontendTasks, SoundType},
util::popup_manager::{MountPopupOnceParams, PopupHolder},
};
#[derive(Clone, Copy, Eq, PartialEq, EnumString, VariantNames, AsRefStr)]
enum PosMode {
@ -420,3 +423,31 @@ impl View {
[width as u32, height as u32]
}
}
pub fn mount_popup(
frontend_tasks: FrontendTasks,
globals: WguiGlobals,
entry: DesktopEntry,
on_close_request: Box<dyn Fn()>,
set_holder: Box<dyn FnOnce(PopupHolder<View>)>,
) {
frontend_tasks
.clone()
.push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
Translation::from_raw_text(&entry.app_name),
Box::new(move |data| {
let view = View::new(Params {
entry: entry.clone(),
globals: &globals,
layout: data.layout,
parent_id: data.id_content,
frontend_tasks: &frontend_tasks,
config: data.config,
on_launched: on_close_request,
})?;
set_holder((data.handle, view));
Ok(())
}),
)));
}

View File

@ -0,0 +1,140 @@
use wgui::{
assets::AssetPath,
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
parser::{Fetchable, ParseDocumentParams},
task::Tasks,
};
use wlx_common::async_executor::AsyncExecutor;
use crate::{
frontend::{FrontendTask, FrontendTasks},
util::{
networking::{self},
popup_manager::{MountPopupOnceParams, PopupHolder},
wgui_simple,
},
views,
};
pub struct Params<'a> {
pub globals: &'a WguiGlobals,
pub layout: &'a mut Layout,
pub executor: &'a AsyncExecutor,
pub parent_id: WidgetID,
pub on_close_request: Box<dyn Fn()>,
}
enum Task {
SetSkymapCatalog(anyhow::Result<networking::skymap_catalog::SkymapCatalog>),
}
pub struct View {
id_parent: WidgetID,
id_list: WidgetID,
id_loading: WidgetID,
globals: WguiGlobals,
tasks: Tasks<Task>,
mounted_cells: Vec<views::skymap_list_cell::View>,
}
impl View {
async fn skymap_catalog_request_wrapper(tasks: Tasks<Task>, executor: AsyncExecutor) {
let res = networking::skymap_catalog::request_catalog(&executor).await;
tasks.push(Task::SetSkymapCatalog(res))
}
pub fn new(par: Params) -> anyhow::Result<Self> {
let id_loading = wgui_simple::create_loading(wgui_simple::CreateLoadingParams {
layout: par.layout,
parent_id: par.parent_id,
with_text: true,
})?;
let tasks = Tasks::<Task>::new();
let fut = View::skymap_catalog_request_wrapper(tasks.clone(), par.executor.clone());
par.executor.spawn(fut).detach();
Ok(Self {
id_parent: par.parent_id,
id_list: WidgetID::default(),
id_loading,
tasks,
globals: par.globals.clone(),
mounted_cells: Vec::new(),
})
}
fn mount_catalog(
&mut self,
layout: &mut Layout,
catalog: networking::skymap_catalog::SkymapCatalog,
) -> anyhow::Result<()> {
let doc_params = &ParseDocumentParams {
globals: self.globals.clone(),
path: AssetPath::BuiltIn("gui/view/download_skymaps.xml"),
extra: Default::default(),
};
let parser_state = wgui::parser::parse_from_assets(&doc_params, layout, self.id_parent)?;
let id_list = parser_state.fetch_widget(&layout.state, "list")?.id;
for entry in catalog.entries {
let view_cell = views::skymap_list_cell::View::new(views::skymap_list_cell::Params {
id_parent: id_list,
layout,
entry,
})?;
self.mounted_cells.push(view_cell);
}
Ok(())
}
pub fn update(&mut self, layout: &mut Layout) -> anyhow::Result<()> {
for task in self.tasks.drain() {
match task {
Task::SetSkymapCatalog(skymap_catalog) => {
layout.remove_widget(self.id_loading);
match skymap_catalog {
Ok(skymap_catalog) => {
self.mount_catalog(layout, skymap_catalog)?;
}
Err(e) => wgui_simple::create_label_error(
layout,
self.id_parent,
format!("Failed to fetch skymap catalog: {:?}", e),
)?,
}
}
}
}
Ok(())
}
}
pub fn mount_popup(
frontend_tasks: FrontendTasks,
executor: AsyncExecutor,
globals: WguiGlobals,
on_close_request: Box<dyn Fn()>,
set_holder: Box<dyn FnOnce(PopupHolder<View>)>,
) {
frontend_tasks
.clone()
.push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
Translation::from_translation_key("APP_SETTINGS.DOWNLOAD_SKYMAPS"),
Box::new(move |data| {
let view = View::new(Params {
globals: &globals,
layout: data.layout,
executor: &executor,
parent_id: data.id_content,
on_close_request,
})?;
set_holder((data.handle, view));
Ok(())
}),
)));
}

View File

@ -30,6 +30,7 @@ use wlx_common::async_executor::AsyncExecutor;
use crate::util::{
cached_fetcher::{self, CoverArt},
steam_utils::{self, AppID},
wgui_simple,
};
pub struct ViewCommon {
@ -48,6 +49,7 @@ pub struct Params<'a, 'b> {
pub struct View {
pub button: Rc<ComponentButton>,
id_image_parent: WidgetID,
id_loading: WidgetID,
app_name: String,
app_id: AppID,
}
@ -143,6 +145,8 @@ impl View {
layout: &mut Layout,
cover_art: &CoverArt,
) -> anyhow::Result<()> {
layout.remove_widget(self.id_loading);
if cover_art.compressed_image_data.is_empty() {
// mount placeholder
let img = view_common.get_placeholder_image()?.clone();
@ -271,6 +275,12 @@ impl View {
rect_gradient_style(taffy::AlignSelf::End, 0.05),
)?;
let id_loading = wgui_simple::create_loading(wgui_simple::CreateLoadingParams {
layout: params.ess.layout,
parent_id: image_parent.id,
with_text: false,
})?;
// request cover image data from the internet or disk cache
params
.executor
@ -286,6 +296,7 @@ impl View {
id_image_parent: image_parent.id,
app_name: params.manifest.name.clone(),
app_id: params.manifest.app_id.clone(),
id_loading,
})
}
}

View File

@ -4,6 +4,7 @@ use crate::{
frontend::{FrontendTask, FrontendTasks, SoundType},
util::{
cached_fetcher::{self, CoverArt},
popup_manager::{MountPopupOnceParams, PopupHolder},
steam_utils::{self, AppID, AppManifest},
},
views::game_cover,
@ -193,3 +194,32 @@ impl View {
(*self.on_launched)();
}
}
pub fn mount_popup(
frontend_tasks: FrontendTasks,
executor: AsyncExecutor,
globals: WguiGlobals,
manifest: AppManifest,
on_close_request: Box<dyn Fn()>,
set_holder: Box<dyn FnOnce(PopupHolder<View>)>,
) {
frontend_tasks
.clone()
.push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
Translation::from_raw_text(&manifest.name),
Box::new(move |data| {
let view = View::new(Params {
manifest: manifest.clone(),
executor: executor.clone(),
globals: &globals,
layout: data.layout,
parent_id: data.id_content,
frontend_tasks: &frontend_tasks,
on_launched: on_close_request,
})?;
set_holder((data.handle, view));
Ok(())
}),
)));
}

View File

@ -16,13 +16,13 @@ use wgui::{
use wlx_common::async_executor::AsyncExecutor;
use crate::{
frontend::{FrontendTask, FrontendTasks},
frontend::FrontendTasks,
util::{
cached_fetcher::CoverArt,
popup_manager::{MountPopupParams, PopupHandle},
popup_manager::PopupHolder,
steam_utils::{self, AppID, SteamUtils},
},
views::{self, game_cover, game_launcher},
views::{self, game_cover},
};
#[derive(Clone)]
@ -51,7 +51,7 @@ pub struct GameCoverCell {
}
struct State {
view_launcher: Option<(PopupHandle, views::game_launcher::View)>,
view_launcher: Option<PopupHolder<views::game_launcher::View>>,
}
pub struct View {
@ -283,36 +283,17 @@ impl View {
}
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),
on_content: {
views::game_launcher::mount_popup(
self.frontend_tasks.clone(),
self.executor.clone(),
self.globals.clone(),
manifest,
self.tasks.make_callback_box(Task::CloseLauncher),
Box::new({
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(())
})
},
}));
move |popup| state.borrow_mut().view_launcher = Some(popup)
}),
);
Ok(())
}

View File

@ -1,7 +1,9 @@
pub mod app_launcher;
pub mod audio_settings;
pub mod download_skymaps;
pub mod game_cover;
pub mod game_launcher;
pub mod game_list;
pub mod running_games_list;
pub mod skymap_list;
pub mod skymap_list_cell;

View File

@ -1,3 +1,5 @@
use std::{cell::RefCell, rc::Rc};
use wgui::{
assets::AssetPath,
components::button::ComponentButton,
@ -9,12 +11,16 @@ use wgui::{
};
use wlx_common::{async_executor::AsyncExecutor, config_io};
use crate::util::{networking, wgui_simple};
use crate::{
frontend::FrontendTasks,
util::{popup_manager::PopupHolder, wgui_simple},
views,
};
#[derive(Clone)]
enum Task {
DownloadSkymaps,
SetSkymapCatalog(Result<networking::skymap_catalog::SkymapCatalog, String>),
ClosePopupDownloadSkymaps,
Refresh,
}
@ -22,6 +28,11 @@ pub struct Params<'a> {
pub globals: WguiGlobals,
pub layout: &'a mut Layout,
pub parent_id: WidgetID,
pub frontend_tasks: &'a FrontendTasks,
}
pub struct State {
popup_download_skymaps: Option<PopupHolder<views::download_skymaps::View>>,
}
pub struct View {
@ -29,6 +40,9 @@ pub struct View {
parser_state: ParserState,
tasks: Tasks<Task>,
list_parent: WidgetID,
frontend_tasks: FrontendTasks,
globals: WguiGlobals,
state: Rc<RefCell<State>>,
}
impl View {
@ -55,14 +69,28 @@ impl View {
Task::Refresh,
);
let state = Rc::new(RefCell::new(State {
popup_download_skymaps: None,
}));
Ok(Self {
parser_state,
tasks,
list_parent,
frontend_tasks: params.frontend_tasks.clone(),
state,
globals: params.globals.clone(),
})
}
pub fn update(&mut self, layout: &mut Layout, executor: &AsyncExecutor) -> anyhow::Result<()> {
{
let mut state = self.state.borrow_mut();
if let Some(popup) = &mut state.popup_download_skymaps {
popup.1.update(layout)?;
}
}
loop {
let tasks = self.tasks.drain();
if tasks.is_empty() {
@ -76,8 +104,8 @@ impl View {
Task::Refresh => {
self.refresh(layout)?;
}
Task::SetSkymapCatalog(skymap_catalog) => {
log::info!("{:?}", skymap_catalog);
Task::ClosePopupDownloadSkymaps => {
(*self.state.borrow_mut()).popup_download_skymaps = None;
}
}
}
@ -86,14 +114,19 @@ impl View {
Ok(())
}
async fn skymap_catalog_request_wrapper(tasks: Tasks<Task>, executor: AsyncExecutor) {
let res = networking::skymap_catalog::request_catalog(&executor).await;
tasks.push(Task::SetSkymapCatalog(res.map_err(|e| format!("{}", e))));
}
fn download_skymaps(&mut self, executor: &AsyncExecutor) -> anyhow::Result<()> {
let fut = View::skymap_catalog_request_wrapper(self.tasks.clone(), executor.clone());
executor.spawn(fut).detach();
views::download_skymaps::mount_popup(
self.frontend_tasks.clone(),
executor.clone(),
self.globals.clone(),
self.tasks.make_callback_box(Task::ClosePopupDownloadSkymaps),
Box::new({
let state = self.state.clone();
move |popup| {
state.borrow_mut().popup_download_skymaps = Some(popup);
}
}),
);
Ok(())
}

View File

@ -0,0 +1,56 @@
use wgui::{
assets::AssetPath,
i18n::Translation,
layout::{Layout, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState},
widget::label::WidgetLabel,
};
use crate::util::{networking, wgui_simple};
pub struct Params<'a> {
pub id_parent: WidgetID,
pub layout: &'a mut Layout,
pub entry: networking::skymap_catalog::SkymapCatalogEntry,
}
pub struct View {
#[allow(dead_code)]
parser_state: ParserState,
}
impl View {
pub fn new(par: Params) -> anyhow::Result<Self> {
let globals = par.layout.state.globals.clone();
let doc_params = &ParseDocumentParams {
globals: globals.clone(),
path: AssetPath::BuiltIn("gui/view/skymap_list_cell.xml"),
extra: Default::default(),
};
let mut parser_state = wgui::parser::parse_from_assets(&doc_params, par.layout, par.id_parent)?;
let data = parser_state.realize_template(&doc_params, "Cell", par.layout, par.id_parent, Default::default())?;
let id_image_preview = data.get_widget_id("image_preview")?;
{
let mut label_title = data.fetch_widget_as::<WidgetLabel>(&par.layout.state, "label_title")?;
let mut label_author = data.fetch_widget_as::<WidgetLabel>(&par.layout.state, "label_author")?;
label_title.set_text_simple(&mut globals.get(), Translation::from_raw_text_string(par.entry.name));
label_author.set_text_simple(
&mut globals.get(),
Translation::from_raw_text_string(format!("by {}", par.entry.author)),
);
}
wgui_simple::create_loading(wgui_simple::CreateLoadingParams {
layout: par.layout,
parent_id: id_image_preview,
with_text: false,
})?;
Ok(Self { parser_state })
}
}

View File

@ -133,16 +133,16 @@ impl Animations {
anim.pos_prev = anim.pos;
anim.pos = pos;
anim.call(state, alterables, 1.0);
if anim.last_tick {
anim.call(state, alterables, 1.0);
alterables.needs_redraw = true;
} else {
anim.ticks_remaining -= 1;
}
anim.ticks_remaining -= 1;
}
self.running_animations.retain(|anim| anim.ticks_remaining > 0);
self.running_animations.retain(|anim| !anim.last_tick);
}
pub fn process(&mut self, state: &LayoutState, alterables: &mut EventAlterables, alpha: f32) {

View File

@ -41,10 +41,17 @@ impl<TaskType: Clone + 'static> Tasks<TaskType> {
});
}
pub fn make_callback(&self, task: TaskType) -> Rc<dyn Fn()> {
pub fn make_callback_rc(&self, task: TaskType) -> Rc<dyn Fn()> {
let this = self.clone();
Rc::new(move || {
this.push(task.clone());
})
}
pub fn make_callback_box(&self, task: TaskType) -> Box<dyn Fn()> {
let this = self.clone();
Box::new(move || {
this.push(task.clone());
})
}
}