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> <layout>
<elements> <elements>
<div flex_direction="column"> <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>
<div flex_direction="row" gap="4"> <div flex_direction="row" gap="4">
<Button id="btn_refresh" tooltip="REFRESH" width="32" height="32" sprite_src_builtin="dashboard/refresh.svg" /> <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", "HIDE": "Hide",
"HOME_SCREEN": "Home", "HOME_SCREEN": "Home",
"MONADO_RUNTIME": "Monado runtime", "MONADO_RUNTIME": "Monado runtime",
"LOADING": "Loading...",
"POPUP_ADD_DISPLAY": { "POPUP_ADD_DISPLAY": {
"RESOLUTION": "Resolution" "RESOLUTION": "Resolution"
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -43,6 +43,8 @@ pub struct PopupHandle {
state: Rc<RefCell<MountedPopupState>>, state: Rc<RefCell<MountedPopupState>>,
} }
pub type PopupHolder<ViewType> = (PopupHandle, ViewType);
impl PopupHandle { impl PopupHandle {
pub fn close(&self) { pub fn close(&self) {
self.state.borrow_mut().mounted_popup = None; // Drop will be called 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<()>>, 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 { impl Drop for MountedPopup {
fn drop(&mut self) { fn drop(&mut self) {
self.layout_tasks.push(LayoutTask::RemoveWidget(self.id_root)); self.layout_tasks.push(LayoutTask::RemoveWidget(self.id_root));
@ -116,16 +135,13 @@ impl PopupManager {
state.refresh_stack(alterables); state.refresh_stack(alterables);
} }
/// Mount a new popup on top of the existing popup stack. fn mount_popup_prepare(
/// Only the topmost popup is visible. &self,
pub fn mount_popup( globals: &WguiGlobals,
&mut self,
globals: WguiGlobals,
layout: &mut Layout, layout: &mut Layout,
frontend_tasks: FrontendTasks, frontend_tasks: &FrontendTasks,
params: MountPopupParams, popup_title: &Translation,
config: &GeneralConfig, ) -> anyhow::Result<(PopupHandle, WidgetID /* content widget ID */)> {
) -> anyhow::Result<()> {
let doc_params = &ParseDocumentParams { let doc_params = &ParseDocumentParams {
globals: globals.clone(), globals: globals.clone(),
path: AssetPath::BuiltIn("gui/view/popup_window.xml"), 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")?; 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")?; let but_back = state.fetch_component_as::<ComponentButton>("but_back")?;
@ -172,6 +188,48 @@ impl PopupManager {
}); });
frontend_tasks.push(FrontendTask::RefreshPopupManager); 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 // mount user-set popup content
(*params.on_content)(PopupContentFuncData { (*params.on_content)(PopupContentFuncData {

View File

@ -1,11 +1,18 @@
use glam::Mat4;
use wgui::{ use wgui::{
animation::{Animation, AnimationEasing},
assets::AssetPath,
drawing,
i18n::Translation, i18n::Translation,
layout::{Layout, WidgetID}, layout::{Layout, LayoutTask, WidgetID},
renderer_vk::text::TextStyle, parser::{Fetchable, ParseDocumentParams},
renderer_vk::{
text::{FontWeight, TextStyle},
util::centered_matrix,
},
widget::label::{WidgetLabel, WidgetLabelParams}, widget::label::{WidgetLabel, WidgetLabelParams},
}; };
#[allow(dead_code)]
pub fn create_label(layout: &mut Layout, parent: WidgetID, content: Translation) -> anyhow::Result<()> { pub fn create_label(layout: &mut Layout, parent: WidgetID, content: Translation) -> anyhow::Result<()> {
let label = WidgetLabel::create( let label = WidgetLabel::create(
&mut layout.state, &mut layout.state,
@ -22,3 +29,73 @@ pub fn create_label(layout: &mut Layout, parent: WidgetID, content: Translation)
Ok(()) 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 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)] #[derive(Clone, Copy, Eq, PartialEq, EnumString, VariantNames, AsRefStr)]
enum PosMode { enum PosMode {
@ -420,3 +423,31 @@ impl View {
[width as u32, height as u32] [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::{ use crate::util::{
cached_fetcher::{self, CoverArt}, cached_fetcher::{self, CoverArt},
steam_utils::{self, AppID}, steam_utils::{self, AppID},
wgui_simple,
}; };
pub struct ViewCommon { pub struct ViewCommon {
@ -48,6 +49,7 @@ pub struct Params<'a, 'b> {
pub struct View { pub struct View {
pub button: Rc<ComponentButton>, pub button: Rc<ComponentButton>,
id_image_parent: WidgetID, id_image_parent: WidgetID,
id_loading: WidgetID,
app_name: String, app_name: String,
app_id: AppID, app_id: AppID,
} }
@ -143,6 +145,8 @@ impl View {
layout: &mut Layout, layout: &mut Layout,
cover_art: &CoverArt, cover_art: &CoverArt,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
layout.remove_widget(self.id_loading);
if cover_art.compressed_image_data.is_empty() { if cover_art.compressed_image_data.is_empty() {
// mount placeholder // mount placeholder
let img = view_common.get_placeholder_image()?.clone(); let img = view_common.get_placeholder_image()?.clone();
@ -271,6 +275,12 @@ impl View {
rect_gradient_style(taffy::AlignSelf::End, 0.05), 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 // request cover image data from the internet or disk cache
params params
.executor .executor
@ -286,6 +296,7 @@ impl View {
id_image_parent: image_parent.id, id_image_parent: image_parent.id,
app_name: params.manifest.name.clone(), app_name: params.manifest.name.clone(),
app_id: params.manifest.app_id.clone(), app_id: params.manifest.app_id.clone(),
id_loading,
}) })
} }
} }

View File

@ -4,6 +4,7 @@ use crate::{
frontend::{FrontendTask, FrontendTasks, SoundType}, frontend::{FrontendTask, FrontendTasks, SoundType},
util::{ util::{
cached_fetcher::{self, CoverArt}, cached_fetcher::{self, CoverArt},
popup_manager::{MountPopupOnceParams, PopupHolder},
steam_utils::{self, AppID, AppManifest}, steam_utils::{self, AppID, AppManifest},
}, },
views::game_cover, views::game_cover,
@ -193,3 +194,32 @@ impl View {
(*self.on_launched)(); (*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 wlx_common::async_executor::AsyncExecutor;
use crate::{ use crate::{
frontend::{FrontendTask, FrontendTasks}, frontend::FrontendTasks,
util::{ util::{
cached_fetcher::CoverArt, cached_fetcher::CoverArt,
popup_manager::{MountPopupParams, PopupHandle}, popup_manager::PopupHolder,
steam_utils::{self, AppID, SteamUtils}, steam_utils::{self, AppID, SteamUtils},
}, },
views::{self, game_cover, game_launcher}, views::{self, game_cover},
}; };
#[derive(Clone)] #[derive(Clone)]
@ -51,7 +51,7 @@ pub struct GameCoverCell {
} }
struct State { struct State {
view_launcher: Option<(PopupHandle, views::game_launcher::View)>, view_launcher: Option<PopupHolder<views::game_launcher::View>>,
} }
pub struct View { pub struct View {
@ -283,36 +283,17 @@ impl View {
} }
fn action_app_manifest_clicked(&mut self, manifest: steam_utils::AppManifest) -> anyhow::Result<()> { fn action_app_manifest_clicked(&mut self, manifest: steam_utils::AppManifest) -> anyhow::Result<()> {
self.frontend_tasks.push(FrontendTask::MountPopup(MountPopupParams { views::game_launcher::mount_popup(
title: Translation::from_raw_text(&manifest.name), self.frontend_tasks.clone(),
on_content: { self.executor.clone(),
self.globals.clone(),
manifest,
self.tasks.make_callback_box(Task::CloseLauncher),
Box::new({
let state = self.state.clone(); let state = self.state.clone();
let tasks = self.tasks.clone(); move |popup| state.borrow_mut().view_launcher = Some(popup)
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(())
})
},
}));
Ok(()) Ok(())
} }

View File

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

View File

@ -1,3 +1,5 @@
use std::{cell::RefCell, rc::Rc};
use wgui::{ use wgui::{
assets::AssetPath, assets::AssetPath,
components::button::ComponentButton, components::button::ComponentButton,
@ -9,12 +11,16 @@ use wgui::{
}; };
use wlx_common::{async_executor::AsyncExecutor, config_io}; 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)] #[derive(Clone)]
enum Task { enum Task {
DownloadSkymaps, DownloadSkymaps,
SetSkymapCatalog(Result<networking::skymap_catalog::SkymapCatalog, String>), ClosePopupDownloadSkymaps,
Refresh, Refresh,
} }
@ -22,6 +28,11 @@ pub struct Params<'a> {
pub globals: WguiGlobals, pub globals: WguiGlobals,
pub layout: &'a mut Layout, pub layout: &'a mut Layout,
pub parent_id: WidgetID, pub parent_id: WidgetID,
pub frontend_tasks: &'a FrontendTasks,
}
pub struct State {
popup_download_skymaps: Option<PopupHolder<views::download_skymaps::View>>,
} }
pub struct View { pub struct View {
@ -29,6 +40,9 @@ pub struct View {
parser_state: ParserState, parser_state: ParserState,
tasks: Tasks<Task>, tasks: Tasks<Task>,
list_parent: WidgetID, list_parent: WidgetID,
frontend_tasks: FrontendTasks,
globals: WguiGlobals,
state: Rc<RefCell<State>>,
} }
impl View { impl View {
@ -55,14 +69,28 @@ impl View {
Task::Refresh, Task::Refresh,
); );
let state = Rc::new(RefCell::new(State {
popup_download_skymaps: None,
}));
Ok(Self { Ok(Self {
parser_state, parser_state,
tasks, tasks,
list_parent, 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<()> { 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 { loop {
let tasks = self.tasks.drain(); let tasks = self.tasks.drain();
if tasks.is_empty() { if tasks.is_empty() {
@ -76,8 +104,8 @@ impl View {
Task::Refresh => { Task::Refresh => {
self.refresh(layout)?; self.refresh(layout)?;
} }
Task::SetSkymapCatalog(skymap_catalog) => { Task::ClosePopupDownloadSkymaps => {
log::info!("{:?}", skymap_catalog); (*self.state.borrow_mut()).popup_download_skymaps = None;
} }
} }
} }
@ -86,14 +114,19 @@ impl View {
Ok(()) 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<()> { fn download_skymaps(&mut self, executor: &AsyncExecutor) -> anyhow::Result<()> {
let fut = View::skymap_catalog_request_wrapper(self.tasks.clone(), executor.clone()); views::download_skymaps::mount_popup(
executor.spawn(fut).detach(); 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(()) 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_prev = anim.pos;
anim.pos = pos; anim.pos = pos;
anim.call(state, alterables, 1.0);
if anim.last_tick { if anim.last_tick {
anim.call(state, alterables, 1.0);
alterables.needs_redraw = true; 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) { 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(); let this = self.clone();
Rc::new(move || { Rc::new(move || {
this.push(task.clone()); 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());
})
}
} }