diff --git a/Cargo.lock b/Cargo.lock
index bad71bf0..dcfac810 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1423,6 +1423,7 @@ dependencies = [
"smol",
"smol-hyper",
"strum",
+ "uuid",
"wayvr-ipc",
"wgui",
"wlx-common",
diff --git a/Cargo.toml b/Cargo.toml
index 5488eca7..1550b9d5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,3 +1,42 @@
+[workspace]
+resolver = "3"
+members = [
+ "dash-frontend",
+ "scripts/prost_build",
+ "uidev",
+ "wayvr",
+ "wayvr-ipc",
+ "wayvrctl",
+ "wgui",
+ "wlx-capture",
+ "wlx-common",
+]
+
+[workspace.dependencies]
+anyhow = "1.0.100"
+clap = { version = "4.5.53", features = ["derive"] }
+glam = { version = "0.30.9", features = ["mint", "serde"] }
+idmap = "0.2.2"
+idmap-derive = "0.2.22"
+log = "0.4.29"
+regex = "1.12.2"
+rust-embed = "8.9.0"
+serde = { version = "1", features = ["derive"] }
+serde_json = "1.0.145"
+slotmap = "1.1.1"
+smol = "2.0.2"
+strum = { version = "0.27.2", features = ["derive"] }
+uuid = { version = "1.19.0", features = ["fast-rng", "v4", "serde"] }
+vulkano = { version = "0.35.2", default-features = false, features = [
+ "macros",
+] }
+vulkano-shaders = "0.35.0"
+wayland-client = { version = "0.31.11" }
+xdg = "3.0.0"
+
+[patch.crates-io]
+vulkano = { git = "https://github.com/galister/vulkano.git", rev = "cf7f92867928a56ce16b376037c1120f2b167678" }
+
[profile.dev]
opt-level = 1
debug = true
@@ -19,41 +58,3 @@ incremental = true
[profile.release-with-debug]
inherits = "release"
debug = true
-
-[workspace]
-members = [
- "uidev",
- "wgui",
- "wlx-common",
- "wayvr",
- "wlx-capture",
- "dash-frontend",
- "wayvr-ipc",
- "wayvrctl",
- "scripts/prost_build",
-]
-resolver = "3"
-
-[patch.crates-io]
-vulkano = { git = "https://github.com/galister/vulkano.git", rev = "cf7f92867928a56ce16b376037c1120f2b167678" }
-
-[workspace.dependencies]
-anyhow = "1.0.100"
-smol = "2.0.2"
-glam = { version = "0.30.9", features = ["mint", "serde"] }
-clap = { version = "4.5.53", features = ["derive"] }
-xdg = "3.0.0"
-idmap = "0.2.2"
-idmap-derive = "0.2.22"
-log = "0.4.29"
-regex = "1.12.2"
-rust-embed = "8.9.0"
-serde = { version = "1", features = ["derive"] }
-serde_json = "1.0.145"
-slotmap = "1.1.1"
-strum = { version = "0.27.2", features = ["derive"] }
-vulkano = { version = "0.35.2", default-features = false, features = [
- "macros",
-] }
-vulkano-shaders = "0.35.0"
-wayland-client = { version = "0.31.11" }
diff --git a/dash-frontend/Cargo.toml b/dash-frontend/Cargo.toml
index 06f1b010..0f2a839f 100644
--- a/dash-frontend/Cargo.toml
+++ b/dash-frontend/Cargo.toml
@@ -8,26 +8,25 @@ authors = ["galister", "oo8dev"]
repository = "https://github.com/wlx-team/wayvr"
[dependencies]
-wayvr-ipc = { path = "../wayvr-ipc", default-features = false }
-wgui = { path = "../wgui/" }
-wlx-common = { path = "../wlx-common" }
-
anyhow.workspace = true
+async-native-tls = "0.5.0"
+chrono = "0.4.42"
glam = { workspace = true, features = ["mint", "serde"] }
+http-body-util = "0.1.3"
+hyper = { version = "1.8.1", features = ["client", "http1", "http2"] }
+keyvalues-parser = { git = "https://github.com/CosmicHorrorDev/vdf-rs.git", rev = "fc6dcbea9eb13cacb98dea40063f6f56cde6e145" }
log.workspace = true
-xdg.workspace = true
rust-embed.workspace = true
serde = { workspace = true, features = ["rc"] }
serde_json.workspace = true
-strum.workspace = true
-
-chrono = "0.4.42"
-keyvalues-parser = { git = "https://github.com/CosmicHorrorDev/vdf-rs.git", rev = "fc6dcbea9eb13cacb98dea40063f6f56cde6e145" }
smol = { workspace = true }
-hyper = { version = "1.8.1", features = ["client", "http1", "http2"] }
-http-body-util = "0.1.3"
-async-native-tls = "0.5.0"
smol-hyper = "0.1.1"
+strum.workspace = true
+uuid.workspace = true
+wayvr-ipc = { path = "../wayvr-ipc", default-features = false }
+wgui = { path = "../wgui/" }
+wlx-common = { path = "../wlx-common" }
+xdg.workspace = true
[features]
default = ["monado"]
diff --git a/dash-frontend/assets/dashboard/check.svg b/dash-frontend/assets/dashboard/check.svg
new file mode 100644
index 00000000..d4ba7af0
--- /dev/null
+++ b/dash-frontend/assets/dashboard/check.svg
@@ -0,0 +1 @@
+
diff --git a/dash-frontend/assets/dashboard/download.svg b/dash-frontend/assets/dashboard/download.svg
new file mode 100644
index 00000000..f446969b
--- /dev/null
+++ b/dash-frontend/assets/dashboard/download.svg
@@ -0,0 +1 @@
+
diff --git a/dash-frontend/assets/dashboard/error.svg b/dash-frontend/assets/dashboard/error.svg
new file mode 100644
index 00000000..fa2fe4b7
--- /dev/null
+++ b/dash-frontend/assets/dashboard/error.svg
@@ -0,0 +1 @@
+
diff --git a/dash-frontend/assets/dashboard/loading.svg b/dash-frontend/assets/dashboard/loading.svg
new file mode 100644
index 00000000..1c800c12
--- /dev/null
+++ b/dash-frontend/assets/dashboard/loading.svg
@@ -0,0 +1 @@
+
diff --git a/dash-frontend/assets/dashboard/trash.svg b/dash-frontend/assets/dashboard/trash.svg
new file mode 100644
index 00000000..914fd402
--- /dev/null
+++ b/dash-frontend/assets/dashboard/trash.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/dash-frontend/assets/gui/t_loading.xml b/dash-frontend/assets/gui/t_loading.xml
new file mode 100644
index 00000000..ca8c5786
--- /dev/null
+++ b/dash-frontend/assets/gui/t_loading.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dash-frontend/assets/gui/tab/settings.xml b/dash-frontend/assets/gui/tab/settings.xml
index 9ca21283..61583d75 100644
--- a/dash-frontend/assets/gui/tab/settings.xml
+++ b/dash-frontend/assets/gui/tab/settings.xml
@@ -48,6 +48,7 @@
+
@@ -59,4 +60,4 @@
-
\ No newline at end of file
+
diff --git a/dash-frontend/assets/gui/view/dialog_box.xml b/dash-frontend/assets/gui/view/dialog_box.xml
new file mode 100644
index 00000000..0c3f87cf
--- /dev/null
+++ b/dash-frontend/assets/gui/view/dialog_box.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/dash-frontend/assets/gui/view/download_file.xml b/dash-frontend/assets/gui/view/download_file.xml
new file mode 100644
index 00000000..63813dd7
--- /dev/null
+++ b/dash-frontend/assets/gui/view/download_file.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dash-frontend/assets/gui/view/remote_skymap_downloader.xml b/dash-frontend/assets/gui/view/remote_skymap_downloader.xml
new file mode 100644
index 00000000..12dfbadc
--- /dev/null
+++ b/dash-frontend/assets/gui/view/remote_skymap_downloader.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dash-frontend/assets/gui/view/remote_skymap_list.xml b/dash-frontend/assets/gui/view/remote_skymap_list.xml
new file mode 100644
index 00000000..e2f3603a
--- /dev/null
+++ b/dash-frontend/assets/gui/view/remote_skymap_list.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/dash-frontend/assets/gui/view/skymap_list.xml b/dash-frontend/assets/gui/view/skymap_list.xml
new file mode 100644
index 00000000..b32b0ec1
--- /dev/null
+++ b/dash-frontend/assets/gui/view/skymap_list.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/dash-frontend/assets/gui/view/skymap_list_cell.xml b/dash-frontend/assets/gui/view/skymap_list_cell.xml
new file mode 100644
index 00000000..93f32e8e
--- /dev/null
+++ b/dash-frontend/assets/gui/view/skymap_list_cell.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dash-frontend/assets/lang/en.json b/dash-frontend/assets/lang/en.json
index 9d702da8..30436cc1 100644
--- a/dash-frontend/assets/lang/en.json
+++ b/dash-frontend/assets/lang/en.json
@@ -38,6 +38,8 @@
"BLOCK_GAME_INPUT_IGNORE_WATCH_HELP": "Do not block input when watch is hovered",
"BLOCK_POSES_ON_KBD_INTERACTION": "Block poses when interacting with keyboard",
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Blocks the game from receiving poses when the keyboard is hovered and 'Block game input' is enabled",
+ "BROWSE_ONLINE_CATALOG": "Browse online catalog...",
+ "BROWSE_SKYMAPS": "Browse skymaps",
"CAPTURE_METHOD": "Wayland screen capture",
"CAPTURE_METHOD_HELP": "Try changing this if you are\nexperiencing black or glitchy screens",
"COLOR_KEYING": "Color keying",
@@ -70,6 +72,7 @@
"LONG_PRESS_DURATION": "Long press duration",
"LOOK_AND_FEEL": "Look & Feel",
"MISC": "Miscellaneous",
+ "NO_SKYMAPS_FOUND": "No skymaps found",
"NOTIFICATIONS_ENABLED": "Enable notifications",
"NOTIFICATIONS_SOUND_ENABLED": "Notification sounds",
"OPAQUE_BACKGROUND": "Opaque background",
@@ -96,7 +99,10 @@
"SCREEN_RENDER_DOWN": "Render screen at lower resolution",
"SCREEN_RENDER_DOWN_HELP": "Helps with aliasing on high-res screens",
"SCROLL_SPEED": "Scroll speed",
+ "SELECT_VARIANT": "Select variant",
"SETS_ON_WATCH": "Sets on watch",
+ "SKYBOX": "Skybox",
+ "SKYMAP_ALREADY_DOWNLOADED": "This skymap is already downloaded. Select desired action.",
"SPACE_DRAG_MULTIPLIER": "Space drag multiplier",
"SPACE_DRAG_UNLOCKED": "Allow space drag on all axes",
"SPACE_ROTATE_UNLOCKED": "Allow space rotate on all axes",
@@ -134,8 +140,12 @@
"VOLUME": "Volume"
},
"CLOSE_WINDOW": "Close window",
+ "CREATION_DATE": "Creation date",
"DEBUG_INFO": "Debug info",
"DISPLAY_BRIGHTNESS": "Display brightness",
+ "DOWNLOADER": "Downloader",
+ "DOWNLOAD_AGAIN": "Download again",
+ "DOWNLOADING_FILE": "Downloading file...",
"FAILED_TO_LAUNCH_APPLICATION": "Failed to launch a application:",
"GAME_LAUNCHED": "Game launched",
"GAME_LIST": {
@@ -149,7 +159,9 @@
"HELLO_USER": "Hello, {USER}!",
"HIDE": "Hide",
"HOME_SCREEN": "Home",
+ "MODIFICATION_DATE": "Modification date",
"MONADO_RUNTIME": "Monado runtime",
+ "LOADING": "Loading...",
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "Resolution"
},
@@ -160,8 +172,11 @@
"PROCESS_LIST": "Process list",
"REFRESH": "Refresh",
"REMOVE": "Remove",
+ "RELOAD_FROM_DISK": "Reload from disk",
"SETTINGS": "Settings",
"SHOW": "Show",
+ "TARGET_PATH": "Target path",
"TERMINATE_PROCESS": "Terminate process",
+ "VERSION": "Version",
"WIDTH": "Width"
}
diff --git a/dash-frontend/src/frontend.rs b/dash-frontend/src/frontend.rs
index 26a1970c..192cbc06 100644
--- a/dash-frontend/src/frontend.rs
+++ b/dash-frontend/src/frontend.rs
@@ -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, PopupManager, PopupManagerParams},
toast_manager::ToastManager,
},
views,
@@ -101,7 +101,7 @@ pub enum FrontendTask {
SetTab(TabType),
RefreshClock,
RefreshBackground,
- MountPopup(MountPopupParams),
+ MountPopupOnce(MountPopupOnceParams),
RefreshPopupManager,
ShowAudioSettings,
UpdateAudioSettingsView,
@@ -279,10 +279,8 @@ impl Frontend {
}
fn update_time(&mut self, data: &mut T) -> anyhow::Result<()> {
- let mut c = self.layout.start_common();
- let mut common = c.common();
-
{
+ let mut common = self.layout.common();
let mut label = common
.state
.widgets
@@ -303,27 +301,20 @@ impl Frontend {
label.set_text(&mut common, Translation::from_raw_text(&text));
}
- c.finish()?;
Ok(())
}
- fn mount_popup(&mut self, params: MountPopupParams, data: &mut T) -> anyhow::Result<()> {
+ 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(
- self.globals.clone(),
- &mut self.layout,
- self.tasks.clone(),
- params,
- config,
- )?;
+ self
+ .popup_manager
+ .mount_popup_once(&self.globals, &mut self.layout, &self.tasks, params, config)?;
Ok(())
}
fn refresh_popup_manager(&mut self) -> anyhow::Result<()> {
- let mut c = self.layout.start_common();
- self.popup_manager.refresh(c.common().alterables);
- c.finish()?;
+ self.popup_manager.refresh(&mut self.layout.alterables);
Ok(())
}
@@ -356,7 +347,7 @@ impl Frontend {
FrontendTask::SetTab(tab_type) => self.set_tab(params.data, tab_type)?,
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()?,
@@ -369,8 +360,7 @@ impl Frontend {
}
fn set_tab_title(&mut self, translation: &str, icon: &str) -> anyhow::Result<()> {
- let mut c = self.layout.start_common();
- let mut common = c.common();
+ let mut common = self.layout.common();
{
let mut label = common
@@ -386,12 +376,11 @@ impl Frontend {
.widgets
.cast_as::(self.widgets.id_sprite_titlebar_icon)?;
sprite.set_content(
- &mut common,
+ common.alterables,
Some(CustomGlyphData::from_assets(&self.globals, AssetPath::BuiltIn(icon))?),
);
}
- c.finish()?;
Ok(())
}
diff --git a/dash-frontend/src/tab/apps.rs b/dash-frontend/src/tab/apps.rs
index 7e17ff39..cf093f84 100644
--- a/dash-frontend/src/tab/apps.rs
+++ b/dash-frontend/src/tab/apps.rs
@@ -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,17 @@ 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},
};
-enum Task {
- CloseLauncher,
-}
+#[derive(Clone)]
+enum Task {}
struct State {
- view_launcher: Option<(PopupHandle, views::app_launcher::View)>,
+ view_launcher: PopupHolder,
}
pub struct TabApps {
@@ -47,21 +45,17 @@ impl Tab for TabApps {
}
fn update(&mut self, frontend: &mut Frontend, _time_ms: u32, data: &mut T) -> anyhow::Result<()> {
- let mut state = self.state.borrow_mut();
+ let state = self.state.borrow_mut();
for task in self.tasks.drain() {
- match task {
- Task::CloseLauncher => state.view_launcher = None,
- }
+ match task {}
}
- self
- .app_list
- .tick(frontend, &self.state, &self.tasks, &mut self.parser_state)?;
+ self.app_list.tick(frontend, &self.state, &mut self.parser_state)?;
- if let Some((_, launcher)) = &mut state.view_launcher {
- launcher.update(&mut frontend.interface, data)?;
- }
+ state
+ .view_launcher
+ .with_view_res(|view| view.update(&mut frontend.interface, data))?;
Ok(())
}
}
@@ -79,40 +73,14 @@ fn on_app_click(
globals: WguiGlobals,
entry: DesktopEntry,
state: Rc>,
- tasks: Tasks,
) -> 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
- 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(())
- })
- },
- }));
+ views::app_launcher::mount_popup(
+ frontend_tasks.clone(),
+ globals.clone(),
+ entry.clone(),
+ state.borrow_mut().view_launcher.clone(),
+ );
Ok(())
})
}
@@ -129,7 +97,9 @@ impl TabApps {
pub fn new(frontend: &mut Frontend, parent_id: WidgetID, data: &mut T) -> anyhow::Result {
let globals = frontend.layout.state.globals.clone();
let tasks = Tasks::new();
- let state = Rc::new(RefCell::new(State { view_launcher: None }));
+ let state = Rc::new(RefCell::new(State {
+ view_launcher: Default::default(),
+ }));
let parser_state = wgui::parser::parse_from_assets(&doc_params(globals.clone()), &mut frontend.layout, parent_id)?;
let app_list_parent = parser_state.fetch_widget(&frontend.layout.state, "app_list_parent")?;
@@ -334,7 +304,6 @@ impl AppList {
&mut self,
frontend: &mut Frontend,
state: &Rc>,
- tasks: &Tasks,
parser_state: &mut ParserState,
) -> anyhow::Result<()> {
// load 4 entries for a single frame at most
@@ -348,7 +317,6 @@ impl AppList {
globals.clone(),
entry.clone(),
state.clone(),
- tasks.clone(),
));
} else {
break;
diff --git a/dash-frontend/src/tab/games.rs b/dash-frontend/src/tab/games.rs
index fcdc3cad..02082ef7 100644
--- a/dash-frontend/src/tab/games.rs
+++ b/dash-frontend/src/tab/games.rs
@@ -10,7 +10,7 @@ use crate::{
frontend::Frontend,
tab::{Tab, TabType},
util::steam_utils::SteamUtils,
- views::{game_list, running_games_list},
+ views::{ViewTrait, ViewUpdateParams, game_list, running_games_list},
};
pub struct TabGames {
@@ -19,7 +19,6 @@ pub struct TabGames {
view_game_list: game_list::View,
view_running_games_list: running_games_list::View,
- steam_utils: SteamUtils,
marker: PhantomData,
}
@@ -29,9 +28,11 @@ impl Tab for TabGames {
}
fn update(&mut self, frontend: &mut Frontend, time_ms: u32, _data: &mut T) -> anyhow::Result<()> {
- self
- .view_game_list
- .update(&mut frontend.layout, &mut self.steam_utils, &frontend.executor)?;
+ self.view_game_list.update(&mut ViewUpdateParams {
+ layout: &mut frontend.layout,
+ executor: &mut frontend.executor,
+ })?;
+
self.view_running_games_list.update(&mut frontend.layout, time_ms)?;
Ok(())
}
@@ -54,16 +55,17 @@ impl TabGames {
let game_list_parent = state.get_widget_id("game_list_parent")?;
let id_running_games_list_parent = state.get_widget_id("running_games_list_parent")?;
+ let mut steam_utils = SteamUtils::new()?;
+
let view_game_list = game_list::View::new(game_list::Params {
executor: frontend.executor.clone(),
frontend_tasks: frontend.tasks.clone(),
globals: globals.clone(),
layout: &mut frontend.layout,
parent_id: game_list_parent,
+ steam_utils: &steam_utils,
})?;
- let mut steam_utils = SteamUtils::new()?;
-
let view_running_games_list = running_games_list::View::new(running_games_list::Params {
globals: globals.clone(),
layout: &mut frontend.layout,
@@ -77,7 +79,6 @@ impl TabGames {
view_game_list,
view_running_games_list,
marker: PhantomData,
- steam_utils,
})
}
}
diff --git a/dash-frontend/src/tab/home.rs b/dash-frontend/src/tab/home.rs
index 57e967cd..bb541ee1 100644
--- a/dash-frontend/src/tab/home.rs
+++ b/dash-frontend/src/tab/home.rs
@@ -59,9 +59,12 @@ impl TabHome {
parent_id,
)?;
- let mut c = frontend.layout.start_common();
- let widget_label = state.fetch_widget(&c.layout.state, "label_hello")?.widget;
- configure_label_hello(&mut c.common(), widget_label, frontend.interface.general_config(data));
+ let widget_label = state.fetch_widget(&frontend.layout.state, "label_hello")?.widget;
+ configure_label_hello(
+ &mut frontend.layout.common(),
+ widget_label,
+ frontend.interface.general_config(data),
+ );
let btn_apps = state.fetch_component_as::("btn_apps")?;
let btn_games = state.fetch_component_as::("btn_games")?;
diff --git a/dash-frontend/src/tab/monado.rs b/dash-frontend/src/tab/monado.rs
index 8efe58de..4a9d1b95 100644
--- a/dash-frontend/src/tab/monado.rs
+++ b/dash-frontend/src/tab/monado.rs
@@ -18,7 +18,7 @@ use wgui::{
};
use wlx_common::{
config::GeneralConfig,
- dash_interface::{self, MonadoDumpSessionFrame},
+ dash_interface::{self, ConfigChangeKind, MonadoDumpSessionFrame},
};
use crate::{
@@ -175,7 +175,7 @@ impl Tab for TabMonado {
Task::GeneralSettingsChromaUpdate => {
if let Subtab::GeneralSettings(tab) = &mut self.subtab {
tab.chroma_update(frontend.interface.general_config(data));
- frontend.interface.config_changed(data);
+ frontend.interface.config_changed(data, ConfigChangeKind::EnvironmentBlend);
}
}
Task::SetBrightness(brightness) => self.set_brightness(frontend, data, brightness),
@@ -281,9 +281,7 @@ impl SubtabGeneralSettings {
// get brightness
let slider_brightness = state.fetch_component_as::("slider_brightness")?;
if let Some(brightness) = frontend.interface.monado_brightness_get(data) {
- let mut c = frontend.layout.start_common();
- slider_brightness.set_value(&mut c.common(), brightness * 100.0);
- c.finish()?;
+ slider_brightness.set_value(&mut frontend.layout.common(), brightness * 100.0);
slider_brightness.on_value_changed({
let tasks = tasks.clone();
@@ -304,8 +302,7 @@ impl SubtabGeneralSettings {
let slider_keying_value_range = state.fetch_component_as::("slider_keying_value_range")?;
{
- let mut lc = frontend.layout.start_common();
- let mut common = lc.common();
+ let mut common = frontend.layout.common();
// set initial values
let (rgb, range_h, range_s, range_v) = config.chroma_key_params.get_rgb_and_hsv_ranges();
@@ -340,8 +337,6 @@ impl SubtabGeneralSettings {
slider_keying_saturation_range.on_value_changed(get_slider_callback(&tasks));
slider_keying_value_range.on_value_changed(get_slider_callback(&tasks));
cs_keying.on_changed(get_color_selector_callback(&tasks));
-
- lc.finish()?;
}
Ok(Self {
diff --git a/dash-frontend/src/tab/settings/mod.rs b/dash-frontend/src/tab/settings/mod.rs
index bcd79abf..fe539b59 100644
--- a/dash-frontend/src/tab/settings/mod.rs
+++ b/dash-frontend/src/tab/settings/mod.rs
@@ -5,7 +5,6 @@ use wgui::{
assets::AssetPath,
components::tabs::ComponentTabs,
drawing,
- event::{CallbackDataCommon, EventAlterables},
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
@@ -20,11 +19,12 @@ use wgui::{
},
windowing::context_menu::{self, Blueprint, ContextMenu, TickResult},
};
-use wlx_common::{config::GeneralConfig, config_io::ConfigRoot, dash_interface::RecenterMode};
+use wlx_common::{config::GeneralConfig, config_io::ConfigRoot, dash_interface::{ConfigChangeKind, RecenterMode}};
use crate::{
- frontend::{Frontend, FrontendTask},
+ frontend::{Frontend, FrontendTask, FrontendTasks},
tab::{Tab, TabType, settings::macros::MacroParams},
+ views::ViewUpdateParams,
};
mod macros;
@@ -33,6 +33,7 @@ mod tab_controls;
mod tab_features;
mod tab_look_and_feel;
mod tab_misc;
+mod tab_skybox;
mod tab_troubleshooting;
#[derive(Clone)]
@@ -43,6 +44,7 @@ enum TabNameEnum {
Misc,
AutostartApps,
Troubleshooting,
+ Skybox,
}
impl TabNameEnum {
@@ -54,6 +56,7 @@ impl TabNameEnum {
"misc" => Some(TabNameEnum::Misc),
"autostart_apps" => Some(TabNameEnum::AutostartApps),
"troubleshooting" => Some(TabNameEnum::Troubleshooting),
+ "skybox" => Some(TabNameEnum::Skybox),
_ => None,
}
}
@@ -75,14 +78,29 @@ enum Task {
SetTab(TabNameEnum),
}
+struct SettingsMountParams<'a> {
+ mp: &'a mut MacroParams<'a>,
+ frontend_tasks: &'a FrontendTasks,
+ id_parent: WidgetID,
+}
+
+trait SettingsTab {
+ fn update(&mut self, _par: &mut ViewUpdateParams) -> anyhow::Result<()> {
+ Ok(())
+ }
+}
+
pub struct TabSettings {
pub state: ParserState,
app_button_ids: Vec>,
context_menu: ContextMenu,
+ current_tab: Option>,
+
tasks: Tasks,
marker: PhantomData,
+ frontend_tasks: FrontendTasks,
}
impl Tab for TabSettings {
@@ -91,6 +109,13 @@ impl Tab for TabSettings {
}
fn update(&mut self, frontend: &mut Frontend, _time_ms: u32, data: &mut T) -> anyhow::Result<()> {
+ if let Some(tab) = &mut self.current_tab {
+ tab.update(&mut ViewUpdateParams {
+ layout: &mut frontend.layout,
+ executor: &frontend.executor,
+ })?;
+ }
+
let mut changed = false;
for task in self.tasks.drain() {
match task {
@@ -179,15 +204,10 @@ impl Tab for TabSettings {
let mut s = name.splitn(5, ';');
(s.next(), s.next(), s.next(), s.next(), s.next())
} {
+ let mut common = frontend.layout.common();
let mut label = self
.state
- .fetch_widget_as::(&frontend.layout.state, &format!("{id}_value"))?;
-
- let mut alterables = EventAlterables::default();
- let mut common = CallbackDataCommon {
- alterables: &mut alterables,
- state: &frontend.layout.state,
- };
+ .fetch_widget_as::(&common.state, &format!("{id}_value"))?;
let translation = Translation {
text: text.into(),
@@ -204,7 +224,7 @@ impl Tab for TabSettings {
// Notify overlays of the change
if changed {
- frontend.interface.config_changed(data);
+ frontend.interface.config_changed(data, ConfigChangeKind::OverlayConfig);
}
Ok(())
@@ -500,6 +520,7 @@ impl TabSettings {
let root = self.state.get_widget_id("settings_root")?;
frontend.layout.remove_children(root);
let globals = frontend.layout.state.globals.clone();
+ self.current_tab = None;
let mut mp = MacroParams {
layout: &mut frontend.layout,
@@ -510,24 +531,36 @@ impl TabSettings {
idx: 9001,
};
+ let settings_mount_params = SettingsMountParams {
+ mp: &mut mp,
+ id_parent: root,
+ frontend_tasks: &self.frontend_tasks,
+ };
+
match name {
TabNameEnum::LookAndFeel => {
- tab_look_and_feel::mount(&mut mp, root)?;
+ self.current_tab = Some(Box::new(tab_look_and_feel::State::mount(settings_mount_params)?));
}
TabNameEnum::Features => {
- tab_features::mount(&mut mp, root)?;
+ self.current_tab = Some(Box::new(tab_features::State::mount(settings_mount_params)?));
}
TabNameEnum::Controls => {
- tab_controls::mount(&mut mp, root)?;
+ self.current_tab = Some(Box::new(tab_controls::State::mount(settings_mount_params)?));
}
TabNameEnum::Misc => {
- tab_misc::mount(&mut mp, root)?;
+ self.current_tab = Some(Box::new(tab_misc::State::mount(settings_mount_params)?));
}
TabNameEnum::AutostartApps => {
- tab_autostart_apps::mount(&mut mp, root, &mut self.app_button_ids)?;
+ self.current_tab = Some(Box::new(tab_autostart_apps::State::mount(
+ settings_mount_params,
+ &mut self.app_button_ids,
+ )?));
}
TabNameEnum::Troubleshooting => {
- tab_troubleshooting::mount(&mut mp, root)?;
+ self.current_tab = Some(Box::new(tab_troubleshooting::State::mount(settings_mount_params)?));
+ }
+ TabNameEnum::Skybox => {
+ self.current_tab = Some(Box::new(tab_skybox::State::mount(settings_mount_params)?));
}
}
@@ -562,6 +595,8 @@ impl TabSettings {
state: parser_state,
marker: PhantomData,
context_menu: ContextMenu::default(),
+ current_tab: None,
+ frontend_tasks: frontend.tasks.clone(),
})
}
}
diff --git a/dash-frontend/src/tab/settings/tab_autostart_apps.rs b/dash-frontend/src/tab/settings/tab_autostart_apps.rs
index 6483fc94..76986f88 100644
--- a/dash-frontend/src/tab/settings/tab_autostart_apps.rs
+++ b/dash-frontend/src/tab/settings/tab_autostart_apps.rs
@@ -1,19 +1,33 @@
use std::rc::Rc;
-use crate::tab::settings::macros::{MacroParams, options_autostart_app, options_category};
-use wgui::layout::WidgetID;
+use crate::tab::settings::{
+ SettingsMountParams, SettingsTab,
+ macros::{options_autostart_app, options_category},
+};
-pub fn mount(mp: &mut MacroParams, parent: WidgetID, app_button_ids: &mut Vec>) -> anyhow::Result<()> {
- *app_button_ids = Vec::new();
+pub struct State {}
- if !mp.config.autostart_apps.is_empty() {
- let c = options_category(mp, parent, "APP_SETTINGS.AUTOSTART_APPS", "dashboard/apps.svg")?;
+impl SettingsTab for State {}
- // todo: prevent clone
- let autostart_apps = mp.config.autostart_apps.clone();
- for app in autostart_apps {
- options_autostart_app(mp, c, &app.name, app_button_ids)?;
+impl State {
+ pub fn mount(par: SettingsMountParams, app_button_ids: &mut Vec>) -> anyhow::Result {
+ *app_button_ids = Vec::new();
+
+ if !par.mp.config.autostart_apps.is_empty() {
+ let c = options_category(
+ par.mp,
+ par.id_parent,
+ "APP_SETTINGS.AUTOSTART_APPS",
+ "dashboard/apps.svg",
+ )?;
+
+ // todo: prevent clone
+ let autostart_apps = par.mp.config.autostart_apps.clone();
+ for app in autostart_apps {
+ options_autostart_app(par.mp, c, &app.name, app_button_ids)?;
+ }
}
+
+ Ok(State {})
}
- Ok(())
}
diff --git a/dash-frontend/src/tab/settings/tab_controls.rs b/dash-frontend/src/tab/settings/tab_controls.rs
index 27b1f10b..266df0c9 100644
--- a/dash-frontend/src/tab/settings/tab_controls.rs
+++ b/dash-frontend/src/tab/settings/tab_controls.rs
@@ -1,23 +1,33 @@
use crate::tab::settings::{
- SettingType,
- macros::{MacroParams, options_category, options_checkbox, options_dropdown, options_slider_f32, options_slider_i32},
+ SettingType, SettingsMountParams, SettingsTab,
+ macros::{options_category, options_checkbox, options_dropdown, options_slider_f32, options_slider_i32},
};
-use wgui::layout::WidgetID;
-pub fn mount(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> {
- let c = options_category(mp, parent, "APP_SETTINGS.CONTROLS", "dashboard/controller.svg")?;
- options_dropdown::(mp, c, &SettingType::KeyboardMiddleClick)?;
- options_dropdown::(mp, c, &SettingType::HandsfreePointer)?;
- options_checkbox(mp, c, SettingType::FocusFollowsMouseMode)?;
- options_checkbox(mp, c, SettingType::LeftHandedMouse)?;
- options_checkbox(mp, c, SettingType::AllowSliding)?;
- options_checkbox(mp, c, SettingType::InvertScrollDirectionX)?;
- options_checkbox(mp, c, SettingType::InvertScrollDirectionY)?;
- options_slider_f32(mp, c, SettingType::ScrollSpeed, 0.1, 5.0, 0.1)?;
- options_slider_f32(mp, c, SettingType::LongPressDuration, 0.1, 2.0, 0.1)?;
- options_slider_f32(mp, c, SettingType::PointerLerpFactor, 0.1, 1.0, 0.1)?;
- options_slider_f32(mp, c, SettingType::XrClickSensitivity, 0.1, 1.0, 0.1)?;
- options_slider_f32(mp, c, SettingType::XrClickSensitivityRelease, 0.1, 1.0, 0.1)?;
- options_slider_i32(mp, c, SettingType::ClickFreezeTimeMs, 0, 500, 50)?;
- Ok(())
+pub struct State {}
+
+impl SettingsTab for State {}
+
+impl State {
+ pub fn mount(par: SettingsMountParams) -> anyhow::Result {
+ let c = options_category(
+ par.mp,
+ par.id_parent,
+ "APP_SETTINGS.CONTROLS",
+ "dashboard/controller.svg",
+ )?;
+ options_dropdown::(par.mp, c, &SettingType::KeyboardMiddleClick)?;
+ options_dropdown::(par.mp, c, &SettingType::HandsfreePointer)?;
+ options_checkbox(par.mp, c, SettingType::FocusFollowsMouseMode)?;
+ options_checkbox(par.mp, c, SettingType::LeftHandedMouse)?;
+ options_checkbox(par.mp, c, SettingType::AllowSliding)?;
+ options_checkbox(par.mp, c, SettingType::InvertScrollDirectionX)?;
+ options_checkbox(par.mp, c, SettingType::InvertScrollDirectionY)?;
+ options_slider_f32(par.mp, c, SettingType::ScrollSpeed, 0.1, 5.0, 0.1)?;
+ options_slider_f32(par.mp, c, SettingType::LongPressDuration, 0.1, 2.0, 0.1)?;
+ options_slider_f32(par.mp, c, SettingType::PointerLerpFactor, 0.1, 1.0, 0.1)?;
+ options_slider_f32(par.mp, c, SettingType::XrClickSensitivity, 0.1, 1.0, 0.1)?;
+ options_slider_f32(par.mp, c, SettingType::XrClickSensitivityRelease, 0.1, 1.0, 0.1)?;
+ options_slider_i32(par.mp, c, SettingType::ClickFreezeTimeMs, 0, 500, 50)?;
+ Ok(State {})
+ }
}
diff --git a/dash-frontend/src/tab/settings/tab_features.rs b/dash-frontend/src/tab/settings/tab_features.rs
index eb0e9865..2269069e 100644
--- a/dash-frontend/src/tab/settings/tab_features.rs
+++ b/dash-frontend/src/tab/settings/tab_features.rs
@@ -1,19 +1,24 @@
use crate::tab::settings::{
- SettingType,
- macros::{MacroParams, options_category, options_checkbox, options_slider_f32},
+ macros::{options_category, options_checkbox, options_slider_f32},
+ SettingType, SettingsMountParams, SettingsTab,
};
-use wgui::layout::WidgetID;
-pub fn mount(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> {
- let c = options_category(mp, parent, "APP_SETTINGS.FEATURES", "dashboard/options.svg")?;
- options_checkbox(mp, c, SettingType::NotificationsEnabled)?;
- options_checkbox(mp, c, SettingType::NotificationsSoundEnabled)?;
- options_checkbox(mp, c, SettingType::KeyboardSoundEnabled)?;
- options_checkbox(mp, c, SettingType::SpaceDragUnlocked)?;
- options_checkbox(mp, c, SettingType::SpaceRotateUnlocked)?;
- options_slider_f32(mp, c, SettingType::SpaceDragMultiplier, -10.0, 10.0, 0.5)?;
- options_checkbox(mp, c, SettingType::BlockGameInput)?;
- options_checkbox(mp, c, SettingType::BlockGameInputIgnoreWatch)?;
- options_checkbox(mp, c, SettingType::BlockPosesOnKbdInteraction)?;
- Ok(())
+pub struct State {}
+
+impl SettingsTab for State {}
+
+impl State {
+ pub fn mount(par: SettingsMountParams) -> anyhow::Result {
+ let c = options_category(par.mp, par.id_parent, "APP_SETTINGS.FEATURES", "dashboard/options.svg")?;
+ options_checkbox(par.mp, c, SettingType::NotificationsEnabled)?;
+ options_checkbox(par.mp, c, SettingType::NotificationsSoundEnabled)?;
+ options_checkbox(par.mp, c, SettingType::KeyboardSoundEnabled)?;
+ options_checkbox(par.mp, c, SettingType::SpaceDragUnlocked)?;
+ options_checkbox(par.mp, c, SettingType::SpaceRotateUnlocked)?;
+ options_slider_f32(par.mp, c, SettingType::SpaceDragMultiplier, -10.0, 10.0, 0.5)?;
+ options_checkbox(par.mp, c, SettingType::BlockGameInput)?;
+ options_checkbox(par.mp, c, SettingType::BlockGameInputIgnoreWatch)?;
+ options_checkbox(par.mp, c, SettingType::BlockPosesOnKbdInteraction)?;
+ Ok(State {})
+ }
}
diff --git a/dash-frontend/src/tab/settings/tab_look_and_feel.rs b/dash-frontend/src/tab/settings/tab_look_and_feel.rs
index 6fe40420..f2e3ac66 100644
--- a/dash-frontend/src/tab/settings/tab_look_and_feel.rs
+++ b/dash-frontend/src/tab/settings/tab_look_and_feel.rs
@@ -1,22 +1,30 @@
use crate::tab::settings::{
- SettingType,
- macros::{MacroParams, options_category, options_checkbox, options_dropdown, options_slider_f32},
+ SettingType, SettingsMountParams, SettingsTab,
+ macros::{options_category, options_checkbox, options_dropdown, options_slider_f32},
};
-use wgui::layout::WidgetID;
-pub fn mount(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> {
- let c = options_category(mp, parent, "APP_SETTINGS.LOOK_AND_FEEL", "dashboard/palette.svg")?;
- options_dropdown::(mp, c, &SettingType::Language)?;
- options_checkbox(mp, c, SettingType::OpaqueBackground)?;
- options_checkbox(mp, c, SettingType::HideUsername)?;
- options_checkbox(mp, c, SettingType::HideGrabHelp)?;
- options_slider_f32(mp, c, SettingType::UiAnimationSpeed, 0.5, 5.0, 0.1)?; // min, max, step
- options_slider_f32(mp, c, SettingType::UiGradientIntensity, 0.0, 1.0, 0.05)?; // min, max, step
- options_slider_f32(mp, c, SettingType::UiRoundMultiplier, 0.5, 5.0, 0.1)?;
- options_checkbox(mp, c, SettingType::SetsOnWatch)?;
- options_checkbox(mp, c, SettingType::UseSkybox)?;
- options_slider_f32(mp, c, SettingType::GridOpacity, 0.0, 1.0, 0.05)?; // min, max, step
- options_checkbox(mp, c, SettingType::UsePassthrough)?;
- options_checkbox(mp, c, SettingType::Clock12h)?;
- Ok(())
+pub struct State {}
+
+impl SettingsTab for State {}
+
+impl State {
+ pub fn mount(par: SettingsMountParams) -> anyhow::Result {
+ let c = options_category(
+ par.mp,
+ par.id_parent,
+ "APP_SETTINGS.LOOK_AND_FEEL",
+ "dashboard/palette.svg",
+ )?;
+ options_dropdown::(par.mp, c, &SettingType::Language)?;
+ options_checkbox(par.mp, c, SettingType::HideUsername)?;
+ options_checkbox(par.mp, c, SettingType::HideGrabHelp)?;
+ options_slider_f32(par.mp, c, SettingType::UiAnimationSpeed, 0.5, 5.0, 0.1)?; // min, max, step
+ options_slider_f32(par.mp, c, SettingType::UiGradientIntensity, 0.0, 1.0, 0.05)?; // min, max, step
+ options_slider_f32(par.mp, c, SettingType::UiRoundMultiplier, 0.5, 5.0, 0.1)?;
+ options_checkbox(par.mp, c, SettingType::SetsOnWatch)?;
+ options_slider_f32(par.mp, c, SettingType::GridOpacity, 0.0, 1.0, 0.05)?; // min, max, step
+ options_checkbox(par.mp, c, SettingType::UsePassthrough)?;
+ options_checkbox(par.mp, c, SettingType::Clock12h)?;
+ Ok(State {})
+ }
}
diff --git a/dash-frontend/src/tab/settings/tab_misc.rs b/dash-frontend/src/tab/settings/tab_misc.rs
index 9fa6f429..29b57522 100644
--- a/dash-frontend/src/tab/settings/tab_misc.rs
+++ b/dash-frontend/src/tab/settings/tab_misc.rs
@@ -1,15 +1,20 @@
use crate::tab::settings::{
- SettingType,
- macros::{MacroParams, options_category, options_checkbox, options_dropdown},
+ SettingType, SettingsMountParams, SettingsTab,
+ macros::{options_category, options_checkbox, options_dropdown},
};
-use wgui::layout::WidgetID;
-pub fn mount(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> {
- let c = options_category(mp, parent, "APP_SETTINGS.MISC", "dashboard/blocks.svg")?;
- options_dropdown::(mp, c, &SettingType::CaptureMethod)?;
- options_checkbox(mp, c, SettingType::XwaylandByDefault)?;
- options_checkbox(mp, c, SettingType::UprightScreenFix)?;
- options_checkbox(mp, c, SettingType::DoubleCursorFix)?;
- options_checkbox(mp, c, SettingType::ScreenRenderDown)?;
- Ok(())
+pub struct State {}
+
+impl SettingsTab for State {}
+
+impl State {
+ pub fn mount(par: SettingsMountParams) -> anyhow::Result {
+ let c = options_category(par.mp, par.id_parent, "APP_SETTINGS.MISC", "dashboard/blocks.svg")?;
+ options_dropdown::(par.mp, c, &SettingType::CaptureMethod)?;
+ options_checkbox(par.mp, c, SettingType::XwaylandByDefault)?;
+ options_checkbox(par.mp, c, SettingType::UprightScreenFix)?;
+ options_checkbox(par.mp, c, SettingType::DoubleCursorFix)?;
+ options_checkbox(par.mp, c, SettingType::ScreenRenderDown)?;
+ Ok(State {})
+ }
}
diff --git a/dash-frontend/src/tab/settings/tab_skybox.rs b/dash-frontend/src/tab/settings/tab_skybox.rs
new file mode 100644
index 00000000..f516a2d0
--- /dev/null
+++ b/dash-frontend/src/tab/settings/tab_skybox.rs
@@ -0,0 +1,69 @@
+use wgui::{assets::AssetPath, i18n::Translation, layout::Layout, task::Tasks};
+
+use crate::{
+ frontend::FrontendTasks,
+ tab::settings::{
+ SettingType, SettingsMountParams, SettingsTab,
+ macros::{options_category, options_checkbox},
+ },
+ util::{popup_manager::PopupHolder, wgui_simple},
+ views::{self, ViewUpdateParams, skymap_list},
+};
+
+#[derive(Clone)]
+enum Task {
+ ShowSkymapList,
+}
+
+pub struct State {
+ popup_skymap_list: PopupHolder,
+ tasks: Tasks,
+ frontend_tasks: FrontendTasks,
+}
+
+impl SettingsTab for State {
+ fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
+ self.popup_skymap_list.update(par)?;
+
+ for task in self.tasks.drain() {
+ match task {
+ Task::ShowSkymapList => self.show_skymap_list(par.layout),
+ }
+ }
+
+ Ok(())
+ }
+}
+
+impl State {
+ pub fn mount(par: SettingsMountParams) -> anyhow::Result {
+ let id_category = options_category(par.mp, par.id_parent, "APP_SETTINGS.SKYBOX", "dashboard/globe.svg")?;
+ options_checkbox(par.mp, id_category, SettingType::UseSkybox)?;
+ options_checkbox(par.mp, id_category, SettingType::OpaqueBackground)?;
+
+ let tasks = Tasks::::new();
+
+ // "Browse skymaps" button
+ wgui_simple::create_button(wgui_simple::CreateButtonParams {
+ id_parent: id_category,
+ layout: par.mp.layout,
+ content: Translation::from_translation_key("APP_SETTINGS.BROWSE_SKYMAPS"),
+ icon_builtin: AssetPath::BuiltIn("dashboard/globe.svg"),
+ on_click: tasks.get_button_click_callback(Task::ShowSkymapList),
+ })?;
+
+ Ok(Self {
+ popup_skymap_list: Default::default(),
+ frontend_tasks: par.frontend_tasks.clone(),
+ tasks,
+ })
+ }
+
+ fn show_skymap_list(&mut self, layout: &mut Layout) {
+ views::skymap_list::mount_popup(
+ self.frontend_tasks.clone(),
+ layout.state.globals.clone(),
+ self.popup_skymap_list.clone(),
+ );
+ }
+}
diff --git a/dash-frontend/src/tab/settings/tab_troubleshooting.rs b/dash-frontend/src/tab/settings/tab_troubleshooting.rs
index 9e16fd4b..8635efe5 100644
--- a/dash-frontend/src/tab/settings/tab_troubleshooting.rs
+++ b/dash-frontend/src/tab/settings/tab_troubleshooting.rs
@@ -1,45 +1,55 @@
use crate::tab::settings::{
- Task,
- macros::{MacroParams, options_category, options_danger_button},
+ SettingsMountParams, SettingsTab, Task,
+ macros::{options_category, options_danger_button},
};
-use wgui::layout::WidgetID;
-pub fn mount(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> {
- let c = options_category(mp, parent, "APP_SETTINGS.TROUBLESHOOTING", "dashboard/cpu.svg")?;
- options_danger_button(
- mp,
- c,
- "APP_SETTINGS.RESET_PLAYSPACE",
- "dashboard/recenter.svg",
- Task::ResetPlayspace,
- )?;
- options_danger_button(
- mp,
- c,
- "APP_SETTINGS.CLEAR_PIPEWIRE_TOKENS",
- "dashboard/display.svg",
- Task::ClearPipewireTokens,
- )?;
- options_danger_button(
- mp,
- c,
- "APP_SETTINGS.CLEAR_SAVED_STATE",
- "dashboard/binary.svg",
- Task::ClearSavedState,
- )?;
- options_danger_button(
- mp,
- c,
- "APP_SETTINGS.DELETE_ALL_CONFIGS",
- "dashboard/circle.svg",
- Task::DeleteAllConfigs,
- )?;
- options_danger_button(
- mp,
- c,
- "APP_SETTINGS.RESTART_SOFTWARE",
- "dashboard/refresh.svg",
- Task::RestartSoftware,
- )?;
- Ok(())
+pub struct State {}
+
+impl SettingsTab for State {}
+
+impl State {
+ pub fn mount(par: SettingsMountParams) -> anyhow::Result {
+ let c = options_category(
+ par.mp,
+ par.id_parent,
+ "APP_SETTINGS.TROUBLESHOOTING",
+ "dashboard/cpu.svg",
+ )?;
+ options_danger_button(
+ par.mp,
+ c,
+ "APP_SETTINGS.RESET_PLAYSPACE",
+ "dashboard/recenter.svg",
+ Task::ResetPlayspace,
+ )?;
+ options_danger_button(
+ par.mp,
+ c,
+ "APP_SETTINGS.CLEAR_PIPEWIRE_TOKENS",
+ "dashboard/display.svg",
+ Task::ClearPipewireTokens,
+ )?;
+ options_danger_button(
+ par.mp,
+ c,
+ "APP_SETTINGS.CLEAR_SAVED_STATE",
+ "dashboard/binary.svg",
+ Task::ClearSavedState,
+ )?;
+ options_danger_button(
+ par.mp,
+ c,
+ "APP_SETTINGS.DELETE_ALL_CONFIGS",
+ "dashboard/circle.svg",
+ Task::DeleteAllConfigs,
+ )?;
+ options_danger_button(
+ par.mp,
+ c,
+ "APP_SETTINGS.RESTART_SOFTWARE",
+ "dashboard/refresh.svg",
+ Task::RestartSoftware,
+ )?;
+ Ok(State {})
+ }
}
diff --git a/dash-frontend/src/util/cached_fetcher.rs b/dash-frontend/src/util/cached_fetcher.rs
index 5c7f84bc..2671a748 100644
--- a/dash-frontend/src/util/cached_fetcher.rs
+++ b/dash-frontend/src/util/cached_fetcher.rs
@@ -1,9 +1,8 @@
+use crate::util::{networking::http_client, steam_utils::AppID};
use anyhow::Context;
use serde::Deserialize;
use wlx_common::{async_executor::AsyncExecutor, cache_dir};
-use crate::util::{http_client, steam_utils::AppID};
-
pub struct CoverArt {
// can be empty in case if data couldn't be fetched (use a fallback image then)
pub compressed_image_data: Vec,
@@ -24,7 +23,7 @@ pub async fn request_image(executor: AsyncExecutor, app_id: AppID) -> anyhow::Re
app_id
);
- match http_client::get(&executor, &url).await {
+ match http_client::get_simple(&executor, &url).await {
Ok(response) => {
log::info!("Success");
cache_dir::set_data(&cache_file_path, &response.data).await?;
@@ -69,7 +68,7 @@ async fn get_app_details_json_internal(
// 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 response = http_client::get_simple(&executor, &url).await?;
let res_utf8 = String::from_utf8(response.data)?;
let root = serde_json::from_str::(&res_utf8)?;
let body = root.get(&app_id).context("invalid body")?;
diff --git a/dash-frontend/src/util/mod.rs b/dash-frontend/src/util/mod.rs
index e5290607..521889d0 100644
--- a/dash-frontend/src/util/mod.rs
+++ b/dash-frontend/src/util/mod.rs
@@ -1,5 +1,5 @@
pub mod cached_fetcher;
-pub mod http_client;
+pub mod networking;
pub mod pactl_wrapper;
pub mod popup_manager;
pub mod steam_utils;
diff --git a/dash-frontend/src/util/http_client.rs b/dash-frontend/src/util/networking/http_client.rs
similarity index 70%
rename from dash-frontend/src/util/http_client.rs
rename to dash-frontend/src/util/networking/http_client.rs
index 875d509e..812620c3 100644
--- a/dash-frontend/src/util/http_client.rs
+++ b/dash-frontend/src/util/networking/http_client.rs
@@ -18,10 +18,33 @@ pub struct HttpClientResponse {
pub data: Vec,
}
-pub async fn get(executor: &AsyncExecutor, url: &str) -> anyhow::Result {
- log::info!("fetching URL \"{}\"", url);
+impl HttpClientResponse {
+ pub fn as_json(self) -> anyhow::Result
+ where
+ T: for<'a> serde::Deserialize<'a>,
+ {
+ let utf8 = str::from_utf8(&self.data)?;
+ Ok(serde_json::from_str::(utf8)?)
+ }
+}
- let url: hyper::Uri = url.try_into()?;
+pub struct ProgressFuncData {
+ pub bytes_downloaded: u64,
+ pub file_size: u64,
+}
+
+pub type ProgressFunc = Box;
+
+pub struct GetParams<'a> {
+ pub executor: &'a AsyncExecutor,
+ pub url: &'a str,
+ pub on_progress: Option,
+}
+
+pub async fn get(params: GetParams<'_>) -> anyhow::Result {
+ log::info!("fetching URL \"{}\"", params.url);
+
+ let url: hyper::Uri = params.url.try_into()?;
let req = Request::builder()
.header(
hyper::header::HOST,
@@ -30,23 +53,56 @@ pub async fn get(executor: &AsyncExecutor, url: &str) -> anyhow::Result anyhow::Result {
+ get(GetParams {
+ executor,
+ url,
+ on_progress: None,
+ })
+ .await
}
async fn fetch(
diff --git a/dash-frontend/src/util/networking/image_fetch.rs b/dash-frontend/src/util/networking/image_fetch.rs
new file mode 100644
index 00000000..75d7c892
--- /dev/null
+++ b/dash-frontend/src/util/networking/image_fetch.rs
@@ -0,0 +1,16 @@
+use std::rc::Rc;
+
+use wgui::{globals::WguiGlobals, renderer_vk::text::custom_glyph::CustomGlyphData};
+use wlx_common::async_executor::AsyncExecutor;
+
+use crate::util::networking::http_client;
+
+pub async fn fetch_to_glyph_data(
+ globals: &WguiGlobals,
+ executor: &AsyncExecutor,
+ url: &str,
+) -> anyhow::Result<(CustomGlyphData, Rc>)> {
+ let res = http_client::get_simple(executor, url).await?;
+ let glyph_data = CustomGlyphData::from_bytes_raster(globals, url, &res.data)?;
+ Ok((glyph_data, Rc::new(res.data)))
+}
diff --git a/dash-frontend/src/util/networking/mod.rs b/dash-frontend/src/util/networking/mod.rs
new file mode 100644
index 00000000..b6c625a8
--- /dev/null
+++ b/dash-frontend/src/util/networking/mod.rs
@@ -0,0 +1,6 @@
+pub mod http_client;
+pub mod image_fetch;
+pub mod skymap_catalog;
+
+// pub const WAYVR_ROOT_URL: &'static str = "https://wayvr.org";
+pub const WAYVR_SKYMAPS_ROOT: &'static str = "https://wayvr.org/skymaps";
diff --git a/dash-frontend/src/util/networking/skymap_catalog.rs b/dash-frontend/src/util/networking/skymap_catalog.rs
new file mode 100644
index 00000000..db1feee6
--- /dev/null
+++ b/dash-frontend/src/util/networking/skymap_catalog.rs
@@ -0,0 +1,200 @@
+#![allow(dead_code)]
+use std::path::PathBuf;
+
+// TODO: Remove later
+use serde::{Deserialize, Serialize};
+use wlx_common::{async_executor::AsyncExecutor, config_io};
+
+use crate::util::networking::{self, WAYVR_SKYMAPS_ROOT, http_client};
+
+pub type SkymapUuid = uuid::Uuid;
+
+#[derive(Copy, Clone, Serialize, Deserialize, Debug)]
+pub enum SkymapResolution {
+ Res2k,
+ Res4k,
+ Res8k,
+}
+
+impl SkymapResolution {
+ pub const fn get_display_str(&self) -> &'static str {
+ match self {
+ SkymapResolution::Res2k => "2K (2 MiB VRAM)",
+ SkymapResolution::Res4k => "4K (8 MiB VRAM)",
+ SkymapResolution::Res8k => "8K (33 MiB VRAM)",
+ }
+ }
+
+ pub const fn get_display_str_simple(&self) -> &'static str {
+ match self {
+ SkymapResolution::Res2k => "2K",
+ SkymapResolution::Res4k => "4K",
+ SkymapResolution::Res8k => "8K",
+ }
+ }
+
+ pub fn from_display_str_simple(text: &str) -> Option {
+ match text {
+ "2K" => Some(SkymapResolution::Res2k),
+ "4K" => Some(SkymapResolution::Res4k),
+ "8K" => Some(SkymapResolution::Res8k),
+ _ => None,
+ }
+ }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct SkymapCatalogEntryFiles {
+ pub size_8k: Option, // "my_skymap_8k.dds"
+ pub size_4k: Option, // "my_skymap_4k.dds"
+ pub size_2k: String, // we should have *at least* this
+ pub preview: String,
+}
+
+impl SkymapCatalogEntryFiles {
+ pub fn get_url_preview(&self) -> String {
+ format!("{}/files/{}", WAYVR_SKYMAPS_ROOT, self.preview)
+ }
+
+ pub fn get_filename_from_res(&self, res: SkymapResolution) -> Option {
+ match res {
+ SkymapResolution::Res2k => Some(&self.size_2k),
+ SkymapResolution::Res4k => self.size_4k.as_ref(),
+ SkymapResolution::Res8k => self.size_8k.as_ref(),
+ }
+ .map(|raw_filename| {
+ // sanitize filename, do not allow "../" just in case
+ PathBuf::from(raw_filename)
+ .file_name()
+ .map(|s| String::from(s.to_string_lossy()))
+ })?
+ }
+
+ // example result: "https://wayvr.org/skymaps/files/my_skymap_8k.dds"
+ pub fn get_url_from_res(&self, res: SkymapResolution) -> Option {
+ let Some(filename) = self.get_filename_from_res(res) else {
+ return None;
+ };
+
+ Some(format!("{}/files/{}", WAYVR_SKYMAPS_ROOT, filename))
+ }
+
+ pub fn get_preview_path(&self) -> PathBuf {
+ config_io::get_skymaps_root().join(&self.preview)
+ }
+
+ pub fn save_preview_to_file(&self, data: &[u8]) -> anyhow::Result<()> {
+ std::fs::write(self.get_preview_path(), data)?;
+ Ok(())
+ }
+
+ pub fn remove_preview_file(&self) {
+ let _dont_care = std::fs::remove_file(self.get_preview_path());
+ }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct SkymapCatalogEntry {
+ pub uuid: SkymapUuid,
+ pub created_at: String,
+ pub modified_at: String,
+ pub version: u32,
+ pub name: String,
+ pub description: String,
+ pub author: String,
+ pub files: SkymapCatalogEntryFiles,
+}
+
+impl SkymapCatalogEntry {
+ pub fn get_destination_path(&self, resolution: SkymapResolution) -> Option {
+ let Some(filename) = self.files.get_filename_from_res(resolution) else {
+ return None;
+ };
+
+ Some(config_io::get_skymaps_root().join(filename))
+ }
+
+ pub fn get_destination_metadata_path(&self) -> PathBuf {
+ config_io::get_skymaps_root().join(format!("{}.json", self.uuid))
+ }
+
+ pub fn is_downloaded(&self, resolution: SkymapResolution) -> anyhow::Result {
+ let Some(full_path) = self.get_destination_path(resolution) else {
+ return Ok(false);
+ };
+
+ Ok(std::fs::exists(full_path)?)
+ }
+
+ pub fn has_any_downloaded(&self) -> bool {
+ self.is_downloaded(SkymapResolution::Res2k).unwrap_or(false)
+ || self.is_downloaded(SkymapResolution::Res4k).unwrap_or(false)
+ || self.is_downloaded(SkymapResolution::Res8k).unwrap_or(false)
+ }
+
+ pub fn remove_file(&self, resolution: SkymapResolution) {
+ let Some(full_path) = self.get_destination_path(resolution) else {
+ return;
+ };
+
+ let _dont_care = std::fs::remove_file(full_path);
+ }
+
+ pub fn save_metadata(&self) -> anyhow::Result<()> {
+ let json = serde_json::to_string_pretty(self)?;
+ std::fs::write(self.get_destination_metadata_path(), json)?;
+ Ok(())
+ }
+
+ pub fn remove_metadata(&self) {
+ let _dont_care = std::fs::remove_file(self.get_destination_metadata_path());
+ }
+}
+
+#[derive(Clone, Debug, Deserialize)]
+pub struct SkymapCatalog {
+ pub version: u32,
+ pub r#type: String,
+ pub entries: Vec,
+}
+
+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 {
+ log::info!("Fetching skymap list");
+
+ let res = http_client::get_simple(executor, &format!("{}/catalog.json", networking::WAYVR_SKYMAPS_ROOT)).await?;
+ let catalog = res.as_json::()?;
+ catalog.validate()?;
+
+ Ok(catalog)
+}
+
+pub fn get_entries_from_disk() -> anyhow::Result> {
+ let mut entries = Vec::::new();
+
+ let skymaps_root = config_io::get_skymaps_root();
+
+ for uuid in config_io::get_skymaps_uuids().unwrap_or_default() {
+ let metadata_path = skymaps_root.join(format!("{}.json", uuid));
+ let Ok(data) = std::fs::read_to_string(metadata_path) else {
+ continue;
+ };
+
+ let entry = serde_json::from_str::(&data)?;
+ entries.push(entry);
+ }
+
+ Ok(entries)
+}
diff --git a/dash-frontend/src/util/popup_manager.rs b/dash-frontend/src/util/popup_manager.rs
index ff46f515..fa7b7cfd 100644
--- a/dash-frontend/src/util/popup_manager.rs
+++ b/dash-frontend/src/util/popup_manager.rs
@@ -16,7 +16,10 @@ use wgui::{
};
use wlx_common::config::GeneralConfig;
-use crate::frontend::{FrontendTask, FrontendTasks};
+use crate::{
+ frontend::{FrontendTask, FrontendTasks},
+ views::{ViewTrait, ViewUpdateParams},
+};
pub struct PopupManagerParams {
pub parent_id: WidgetID,
@@ -34,15 +37,140 @@ pub struct MountedPopup {
frontend_tasks: FrontendTasks,
}
+#[derive(Default)]
struct MountedPopupState {
mounted_popup: Option,
+ closed_callback: Option,
}
-#[derive(Clone)]
+#[derive(Default, Clone)]
pub struct PopupHandle {
state: Rc>,
}
+struct PopupHolderState {
+ popup_handle: PopupHandle,
+ view: Option,
+ on_view_close: Option>,
+}
+
+// we can't use #[derive(Default)] due to the fact that ViewType can't be Default.
+impl Default for PopupHolderState {
+ fn default() -> Self {
+ Self {
+ popup_handle: Default::default(),
+ view: None,
+ on_view_close: None,
+ }
+ }
+}
+
+pub struct PopupHolder {
+ state: Rc>>,
+}
+
+impl Default for PopupHolder {
+ fn default() -> Self {
+ Self {
+ state: Rc::new(RefCell::new(PopupHolderState::default())),
+ }
+ }
+}
+
+impl PopupHolderState {
+ fn close(&mut self) {
+ if self.view.is_some() {
+ self.view = None;
+ if let Some(on_close) = self.on_view_close.take() {
+ on_close();
+ }
+ }
+ self.popup_handle.close();
+ }
+}
+
+impl Drop for PopupHolderState {
+ fn drop(&mut self) {
+ self.close();
+ }
+}
+
+// we can't derive(Clone) due to the fact that ViewType is non-cloneable
+impl Clone for PopupHolder {
+ fn clone(&self) -> Self {
+ Self {
+ state: self.state.clone(),
+ }
+ }
+}
+
+impl PopupHolder {
+ pub fn update(&self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
+ let mut state = self.state.borrow_mut();
+ let Some(view) = &mut state.view else {
+ return Ok(());
+ };
+
+ view.update(par)
+ }
+
+ pub fn set_view(&self, handle: PopupHandle, view: ViewType, on_view_close: Option>) {
+ let mut state = self.state.borrow_mut();
+ state.view = Some(view);
+ state.popup_handle = handle;
+ state.on_view_close = on_view_close;
+ }
+
+ // Get underlying ViewType object in a closure and return its value
+ // example usage:
+ //
+ // ```rs
+ // holder.with_view(|view| {
+ // view.foo();
+ // })
+ // ```
+ //
+ pub fn with_view(&self, f: F) -> Option
+ where
+ F: FnOnce(&mut ViewType) -> R,
+ {
+ let mut state = self.state.borrow_mut();
+ if let Some(view) = state.view.as_mut() {
+ Some(f(view))
+ } else {
+ None
+ }
+ }
+
+ // Same as with_view, but the closure expects a simple anyhow::Result<()> type
+ pub fn with_view_res(&self, f: F) -> anyhow::Result<()>
+ where
+ F: FnOnce(&mut ViewType) -> anyhow::Result<()>,
+ {
+ if let Some(res) = self.with_view(f) {
+ return res;
+ }
+ Ok(())
+ }
+
+ pub fn get_close_callback(&self, layout: &Layout) -> Box
+ where
+ ViewType: 'static,
+ {
+ let layout_tasks = layout.tasks.clone();
+ let weak_state = Rc::downgrade(&self.state);
+ Box::new(move || {
+ // we can't borrow State here yet, dispatch it.
+ layout_tasks.push(LayoutTask::Dispatch(Box::new(move |_common| {
+ if let Some(state) = weak_state.upgrade() {
+ state.borrow_mut().close();
+ }
+ Ok(())
+ })));
+ })
+ }
+}
+
impl PopupHandle {
pub fn close(&self) {
self.state.borrow_mut().mounted_popup = None; // Drop will be called
@@ -61,10 +189,26 @@ pub struct PopupContentFuncData<'a> {
pub id_content: WidgetID,
}
+type PopupClosedCallback = Box;
+
+// 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 MountPopupParams {
- pub title: Translation,
- pub on_content: Rc anyhow::Result<()>>,
+pub struct MountPopupOnceParams {
+ title: Translation,
+ on_content: Rc anyhow::Result>>>>,
+}
+
+impl MountPopupOnceParams {
+ pub fn new(
+ title: Translation,
+ on_content: Box anyhow::Result>,
+ ) -> Self {
+ Self {
+ title,
+ on_content: Rc::new(RefCell::new(Some(on_content))),
+ }
+ }
}
impl Drop for MountedPopup {
@@ -78,10 +222,16 @@ impl State {
fn refresh_stack(&mut self, alterables: &mut EventAlterables) {
// show only the topmost popup
self.popup_stack.retain(|weak| {
- let Some(popup) = weak.upgrade() else {
- return false;
+ let retain = {
+ let Some(popup) = weak.upgrade() else {
+ return false;
+ };
+ popup.borrow_mut().mounted_popup.is_some()
};
- popup.borrow_mut().mounted_popup.is_some()
+ if !retain {
+ log::debug!("removing popup from popup_stack");
+ }
+ retain
});
for (idx, popup) in self.popup_stack.iter().enumerate() {
@@ -116,16 +266,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 +285,7 @@ impl PopupManager {
{
let mut label_title = state.fetch_widget_as::(&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::("but_back")?;
@@ -152,6 +299,7 @@ impl PopupManager {
let mounted_popup_state = MountedPopupState {
mounted_popup: Some(mounted_popup),
+ closed_callback: None,
};
let popup_handle = PopupHandle {
@@ -159,28 +307,57 @@ impl PopupManager {
};
let mut state = self.state.borrow_mut();
+ log::debug!("pushing popup to popup_stack");
state.popup_stack.push(Rc::downgrade(&popup_handle.state));
but_back.on_click({
let popup_handle = Rc::downgrade(&popup_handle.state);
Rc::new(move |_common, _evt| {
if let Some(popup_handle) = popup_handle.upgrade() {
- popup_handle.borrow_mut().mounted_popup = None; // will call Drop
+ if let Some(closed_callback) = {
+ let mut state = popup_handle.borrow_mut();
+ state.mounted_popup = None; // will call Drop
+ state.closed_callback.take()
+ } {
+ log::debug!("closed_callback called");
+ closed_callback();
+ }
}
Ok(())
})
});
frontend_tasks.push(FrontendTask::RefreshPopupManager);
+ Ok((popup_handle, id_content))
+ }
+
+ /// Mount a new popup on top of the existing popup stack.
+ /// 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, ¶ms.title)?;
// mount user-set popup content
- (*params.on_content)(PopupContentFuncData {
+ let closed_callback = on_content_func(PopupContentFuncData {
layout,
handle: popup_handle.clone(),
id_content,
config,
})?;
+ popup_handle.state.borrow_mut().closed_callback = Some(closed_callback);
+
Ok(())
}
}
diff --git a/dash-frontend/src/util/steam_utils.rs b/dash-frontend/src/util/steam_utils.rs
index a2c6e778..3c7faa3a 100644
--- a/dash-frontend/src/util/steam_utils.rs
+++ b/dash-frontend/src/util/steam_utils.rs
@@ -2,6 +2,7 @@ use keyvalues_parser::{Obj, Vdf};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
+#[derive(Clone)]
pub struct SteamUtils {
steam_root: PathBuf,
}
diff --git a/dash-frontend/src/util/wgui_simple.rs b/dash-frontend/src/util/wgui_simple.rs
index 44dad5ee..f09b69ab 100644
--- a/dash-frontend/src/util/wgui_simple.rs
+++ b/dash-frontend/src/util/wgui_simple.rs
@@ -1,12 +1,51 @@
+use glam::{Mat4, Vec2};
use wgui::{
+ animation::{Animation, AnimationEasing},
+ assets::AssetPath,
+ components::{self, button::ButtonClickCallback},
+ drawing,
i18n::Translation,
- layout::{Layout, WidgetID},
- renderer_vk::text::TextStyle,
- widget::label::{WidgetLabel, WidgetLabelParams},
+ layout::{Layout, LayoutTask, WidgetID},
+ parser::{Fetchable, ParseDocumentParams},
+ renderer_vk::{
+ text::{FontWeight, TextStyle, custom_glyph::CustomGlyphData},
+ util::centered_matrix,
+ },
+ taffy::{self, prelude::length},
+ widget::{
+ ConstructEssentials,
+ label::{WidgetLabel, WidgetLabelParams},
+ sprite::{WidgetSprite, WidgetSpriteParams},
+ },
};
-#[allow(dead_code)]
-pub fn create_label(layout: &mut Layout, parent: WidgetID, content: Translation) -> anyhow::Result<()> {
+pub struct CreateButtonParams<'a> {
+ pub id_parent: WidgetID,
+ pub layout: &'a mut Layout,
+ pub content: Translation,
+ pub icon_builtin: AssetPath<'a>,
+ pub on_click: ButtonClickCallback,
+}
+
+pub fn create_button(par: CreateButtonParams) -> anyhow::Result<()> {
+ let (_, button) = components::button::construct(
+ &mut ConstructEssentials {
+ layout: par.layout,
+ parent: par.id_parent,
+ },
+ components::button::Params {
+ text: Some(par.content),
+ sprite_src: Some(par.icon_builtin),
+ ..Default::default()
+ },
+ )?;
+
+ button.on_click(par.on_click);
+
+ Ok(())
+}
+
+pub fn create_label(layout: &mut Layout, id_parent: WidgetID, content: Translation) -> anyhow::Result<()> {
let label = WidgetLabel::create(
&mut layout.state,
WidgetLabelParams {
@@ -18,7 +57,102 @@ pub fn create_label(layout: &mut Layout, parent: WidgetID, content: Translation)
},
);
+ layout.add_child(id_parent, label, Default::default())?;
+
+ 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 fn create_icon(layout: &mut Layout, id_parent: WidgetID, size: Vec2, path: AssetPath) -> anyhow::Result {
+ let widget_sprite = WidgetSprite::create(WidgetSpriteParams {
+ color: None,
+ glyph_data: Some(CustomGlyphData::from_assets(&layout.state.globals, path)?),
+ });
+
+ let size = taffy::Size {
+ width: length(size.x),
+ height: length(size.y),
+ };
+
+ let (widget, _) = layout.add_child(
+ id_parent,
+ widget_sprite,
+ taffy::Style {
+ min_size: size.clone(),
+ max_size: size.clone(),
+ size: size.clone(),
+ ..Default::default()
+ },
+ )?;
+
+ Ok(widget.id)
+}
+
+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 {
+ 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)
+}
diff --git a/dash-frontend/src/views/app_launcher.rs b/dash-frontend/src/views/app_launcher.rs
index 76c5b524..af9c398b 100644
--- a/dash-frontend/src/views/app_launcher.rs
+++ b/dash-frontend/src/views/app_launcher.rs
@@ -14,7 +14,11 @@ 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},
+ views::{ViewTrait, ViewUpdateParams},
+};
#[derive(Clone, Copy, Eq, PartialEq, EnumString, VariantNames, AsRefStr)]
enum PosMode {
@@ -66,7 +70,7 @@ struct LaunchParams<'a, T> {
interface: &'a mut BoxDashInterface,
auto_start: bool,
data: &'a mut T,
- on_launched: &'a dyn Fn(),
+ on_launched: Option>,
}
pub struct View {
@@ -91,7 +95,7 @@ pub struct View {
auto_start: bool,
- on_launched: Box,
+ on_launched: Option>,
}
pub struct Params<'a> {
@@ -101,7 +105,13 @@ pub struct Params<'a> {
pub parent_id: WidgetID,
pub config: &'a GeneralConfig,
pub frontend_tasks: &'a FrontendTasks,
- pub on_launched: Box,
+ pub on_launched: Box,
+}
+
+impl ViewTrait for View {
+ fn update(&mut self, _par: &mut ViewUpdateParams) -> anyhow::Result<()> {
+ Ok(())
+ }
}
impl View {
@@ -280,7 +290,7 @@ impl View {
entry: params.entry,
frontend_tasks: params.frontend_tasks.clone(),
globals: params.globals.clone(),
- on_launched: params.on_launched,
+ on_launched: Some(params.on_launched),
})
}
@@ -316,7 +326,7 @@ impl View {
auto_start: self.auto_start,
interface,
data,
- on_launched: &self.on_launched,
+ on_launched: self.on_launched.take(),
});
}
@@ -334,7 +344,7 @@ impl View {
))));
}
- fn launch(params: LaunchParams) -> anyhow::Result<()> {
+ fn launch(mut params: LaunchParams) -> anyhow::Result<()> {
let mut env = Vec::::new();
if params.compositor_mode == CompositorMode::Native {
@@ -390,7 +400,9 @@ impl View {
params.frontend_tasks.push(FrontendTask::PlaySound(SoundType::Launch));
- (*params.on_launched)();
+ if let Some(on_launched) = params.on_launched.take() {
+ on_launched();
+ }
// we're done!
Ok(())
@@ -420,3 +432,26 @@ impl View {
[width as u32, height as u32]
}
}
+
+pub fn mount_popup(frontend_tasks: FrontendTasks, globals: WguiGlobals, entry: DesktopEntry, popup: PopupHolder) {
+ frontend_tasks
+ .clone()
+ .push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
+ Translation::from_raw_text(&entry.app_name),
+ Box::new(move |data| {
+ let on_launched = popup.get_close_callback(data.layout);
+ 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,
+ })?;
+
+ popup.set_view(data.handle, view, None);
+ Ok(popup.get_close_callback(data.layout))
+ }),
+ )));
+}
diff --git a/dash-frontend/src/views/audio_settings.rs b/dash-frontend/src/views/audio_settings.rs
index 8284ea50..d3900bf3 100644
--- a/dash-frontend/src/views/audio_settings.rs
+++ b/dash-frontend/src/views/audio_settings.rs
@@ -653,9 +653,6 @@ impl View {
}
fn update_button_highlights(&self, layout: &mut Layout) -> anyhow::Result<()> {
- let mut c = layout.start_common();
- let mut common = c.common();
-
let num: u8 = match &self.mode {
CurrentMode::Sinks => 0,
CurrentMode::Sources => 1,
@@ -663,20 +660,21 @@ impl View {
CurrentMode::CardProfileSelector(_) => 255,
};
+ let mut com = layout.common();
+
let mut perform = |btn_num: u8, btn: &Rc| {
let color = if num == btn_num {
- common.state.theme.accent_color
+ com.state.theme.accent_color
} else {
- common.state.theme.button_color
+ com.state.theme.button_color
};
- btn.set_color(&mut common, color);
+ btn.set_color(&mut com, color);
};
perform(0, &self.btn_sinks);
perform(1, &self.btn_sources);
perform(2, &self.btn_cards);
- c.finish()?;
Ok(())
}
@@ -801,9 +799,7 @@ impl View {
par,
)?;
- let mut c = params.layout.start_common();
- let mut common = c.common();
-
+ let mut common = params.layout.common();
let checkbox = data.fetch_component_as::("checkbox")?;
let btn_mute = data.fetch_component_as::("btn_mute")?;
let slider = data.fetch_component_as::("slider")?;
@@ -838,8 +834,6 @@ impl View {
})
});
- c.finish()?;
-
Ok(())
}
diff --git a/dash-frontend/src/views/dialog_box.rs b/dash-frontend/src/views/dialog_box.rs
new file mode 100644
index 00000000..eba712af
--- /dev/null
+++ b/dash-frontend/src/views/dialog_box.rs
@@ -0,0 +1,127 @@
+use std::{collections::HashMap, rc::Rc};
+
+use crate::{
+ frontend::{FrontendTask, FrontendTasks},
+ util::popup_manager::{MountPopupOnceParams, PopupHolder},
+ views::{ViewTrait, ViewUpdateParams},
+};
+use wgui::{
+ assets::AssetPath,
+ components::button::ComponentButton,
+ globals::WguiGlobals,
+ i18n::Translation,
+ layout::{Layout, WidgetID},
+ parser::{Fetchable, ParseDocumentParams, ParserState},
+ task::Tasks,
+ widget::label::WidgetLabel,
+};
+
+pub struct ButtonEntry {
+ pub content: Translation, // button text
+ pub icon: &'static str, // sprite_src_builtin
+ pub action: &'static str, // action name (will be passed into on_action_click)
+}
+
+pub struct Params {
+ pub globals: WguiGlobals,
+ pub entries: Vec,
+ pub message: Translation,
+ pub on_action_click: Box,
+}
+
+#[derive(Clone)]
+enum Task {
+ ActionClicked(&'static str),
+}
+
+pub struct View {
+ tasks: Tasks,
+
+ #[allow(dead_code)]
+ parser_state: ParserState,
+
+ on_action_click: Option>,
+ on_close_request: Option>,
+}
+
+fn doc_params(globals: &WguiGlobals) -> ParseDocumentParams<'_> {
+ ParseDocumentParams {
+ globals: globals.clone(),
+ path: AssetPath::BuiltIn("gui/view/dialog_box.xml"),
+ extra: Default::default(),
+ }
+}
+
+impl ViewTrait for View {
+ fn update(&mut self, _par: &mut ViewUpdateParams) -> anyhow::Result<()> {
+ for task in self.tasks.drain() {
+ match task {
+ Task::ActionClicked(action) => {
+ if let Some(func) = self.on_action_click.take() {
+ func(action);
+ }
+
+ if let Some(on_close) = self.on_close_request.take() {
+ on_close();
+ }
+ }
+ }
+ }
+ Ok(())
+ }
+}
+
+impl View {
+ pub fn new(
+ layout: &mut Layout,
+ id_parent: WidgetID,
+ on_close_request: Box,
+ par: Params,
+ ) -> anyhow::Result {
+ let tasks = Tasks::::new();
+
+ let mut parser_state = wgui::parser::parse_from_assets(&doc_params(&par.globals), layout, id_parent)?;
+ let id_buttons = parser_state.get_widget_id("buttons")?;
+
+ {
+ let label_message = parser_state.fetch_widget(&layout.state, "label_message")?.widget;
+ label_message
+ .cast::()?
+ .set_text(&mut layout.common(), par.message);
+ }
+
+ for entry in par.entries {
+ let mut t_par = HashMap::, Rc>::new();
+ t_par.insert(Rc::from("icon"), Rc::from(entry.icon));
+
+ let data =
+ parser_state.realize_template(&doc_params(&par.globals), "DialogBoxButton", layout, id_buttons, t_par)?;
+
+ let button = data.fetch_component_as::("btn")?;
+ button.set_text(&mut layout.common(), entry.content.clone());
+ button.on_click(tasks.get_button_click_callback(Task::ActionClicked(entry.action)));
+ }
+
+ Ok(Self {
+ tasks,
+ parser_state,
+ on_action_click: Some(par.on_action_click),
+ on_close_request: Some(on_close_request),
+ })
+ }
+}
+
+pub fn mount_popup(popup: PopupHolder, frontend_tasks: FrontendTasks, params: Params) {
+ frontend_tasks
+ .clone()
+ .push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
+ Translation::from_raw_text("Info"),
+ Box::new(move |data| {
+ let on_close_request = popup.get_close_callback(data.layout);
+ let view = View::new(data.layout, data.id_content, on_close_request, params)?;
+
+ popup.set_view(data.handle, view, None);
+ Ok(popup.get_close_callback(data.layout))
+ }),
+ )));
+}
diff --git a/dash-frontend/src/views/download_file.rs b/dash-frontend/src/views/download_file.rs
new file mode 100644
index 00000000..30b299ff
--- /dev/null
+++ b/dash-frontend/src/views/download_file.rs
@@ -0,0 +1,265 @@
+use crate::{
+ frontend::{FrontendTask, FrontendTasks},
+ util::{
+ networking::http_client::{self, ProgressFuncData},
+ popup_manager::{MountPopupOnceParams, PopupHolder},
+ wgui_simple,
+ },
+ views::{ViewTrait, ViewUpdateParams},
+};
+use glam::Vec2;
+use std::path::PathBuf;
+use wgui::{
+ assets::AssetPath,
+ components::button::ComponentButton,
+ globals::WguiGlobals,
+ i18n::Translation,
+ layout::{Layout, WidgetID},
+ parser::{Fetchable, ParseDocumentParams, ParserState},
+ task::Tasks,
+ widget::label::WidgetLabel,
+};
+use wlx_common::async_executor::AsyncExecutor;
+
+pub struct Params {
+ pub globals: WguiGlobals,
+ pub executor: AsyncExecutor,
+ pub target_path: PathBuf,
+ pub url: String,
+ pub on_downloaded: Box,
+}
+
+#[derive(Clone)]
+enum Task {
+ StartDownload(/*url*/ String, /*target path*/ PathBuf),
+ SetStatusText(String),
+ ShowIconSuccess,
+ ShowIconError,
+ Close,
+}
+
+pub struct View {
+ globals: WguiGlobals,
+ tasks: Tasks,
+ executor: AsyncExecutor,
+
+ #[allow(dead_code)]
+ parser_state: ParserState,
+
+ id_label_status: WidgetID,
+ id_loading_parent: WidgetID,
+ id_content: WidgetID,
+ on_close_request: Option>,
+ on_downloaded: Option>,
+}
+
+fn doc_params(globals: &WguiGlobals) -> ParseDocumentParams<'_> {
+ ParseDocumentParams {
+ globals: globals.clone(),
+ path: AssetPath::BuiltIn("gui/view/download_file.xml"),
+ extra: Default::default(),
+ }
+}
+
+impl ViewTrait for View {
+ fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
+ for task in self.tasks.drain() {
+ match task {
+ Task::StartDownload(url, path) => {
+ if let Some(on_downloaded) = self.on_downloaded.take() {
+ self
+ .executor
+ .spawn(View::download(
+ self.tasks.clone(),
+ self.executor.clone(),
+ url,
+ path,
+ on_downloaded,
+ ))
+ .detach();
+ }
+ }
+ Task::SetStatusText(text) => {
+ let widgets = &mut par.layout.state.widgets;
+ widgets
+ .fetch(self.id_label_status)?
+ .cast::()?
+ .set_text(&mut par.layout.common(), Translation::from_raw_text_string(text));
+ }
+ Task::ShowIconSuccess => {
+ par.layout.remove_children(self.id_loading_parent);
+ wgui_simple::create_icon(
+ par.layout,
+ self.id_loading_parent,
+ Vec2::splat(32.0),
+ AssetPath::BuiltIn("dashboard/check.svg"),
+ )?;
+
+ // "Close window" button
+ self
+ .parser_state
+ .realize_template(
+ &doc_params(&self.globals),
+ "btn_close",
+ par.layout,
+ self.id_content,
+ Default::default(),
+ )?
+ .fetch_component_as::("btn")?
+ .on_click(self.tasks.get_button_click_callback(Task::Close));
+ }
+ Task::ShowIconError => {
+ par.layout.remove_children(self.id_loading_parent);
+ wgui_simple::create_icon(
+ par.layout,
+ self.id_loading_parent,
+ Vec2::splat(32.0),
+ AssetPath::BuiltIn("dashboard/error.svg"),
+ )?;
+ }
+ Task::Close => {
+ if let Some(on_close) = self.on_close_request.take() {
+ on_close();
+ }
+ }
+ }
+ }
+ Ok(())
+ }
+}
+
+fn handle_async_result(error_reason: &'static str, tasks: &Tasks, result: anyhow::Result) -> Option
+where
+ E: std::fmt::Debug,
+{
+ match result {
+ Ok(res) => Some(res),
+ Err(e) => {
+ tasks.push(Task::ShowIconError);
+ tasks.push(Task::SetStatusText(format!("{}: {:?}", error_reason, e)));
+ None
+ }
+ }
+}
+
+impl View {
+ pub fn new(
+ layout: &mut Layout,
+ id_parent: WidgetID,
+ on_close_request: Box,
+ par: Params,
+ ) -> anyhow::Result {
+ let tasks = Tasks::::new();
+
+ let parser_state = wgui::parser::parse_from_assets(&doc_params(&par.globals), layout, id_parent)?;
+ let id_label_status = parser_state.get_widget_id("label_status")?;
+ let id_content = parser_state.get_widget_id("content")?;
+ let id_loading_parent = parser_state.get_widget_id("loading_parent")?;
+
+ wgui_simple::create_loading(wgui_simple::CreateLoadingParams {
+ parent_id: id_loading_parent,
+ layout: layout,
+ with_text: false,
+ })?;
+
+ let str_target_path = par.globals.i18n().translate("TARGET_PATH");
+
+ {
+ let label_target_path = parser_state.fetch_widget(&layout.state, "label_target_path")?.widget;
+ label_target_path.cast::()?.set_text(
+ &mut layout.common(),
+ Translation::from_raw_text_string(format!("{}: {}", str_target_path, par.target_path.display())),
+ );
+ }
+
+ tasks.push(Task::StartDownload(par.url, par.target_path));
+
+ Ok(Self {
+ tasks,
+ globals: par.globals.clone(),
+ executor: par.executor.clone(),
+ parser_state,
+ id_label_status,
+ id_loading_parent,
+ id_content,
+ on_close_request: Some(on_close_request),
+ on_downloaded: Some(par.on_downloaded),
+ })
+ }
+
+ async fn download(
+ tasks: Tasks,
+ executor: AsyncExecutor,
+ url: String,
+ target_path: PathBuf,
+ on_downloaded: Box,
+ ) -> Option<()> {
+ tasks.push(Task::SetStatusText(String::from("Connecting to the server...")));
+
+ // start downloading from the server with progress reporting
+ let res = handle_async_result(
+ "Download failed",
+ &tasks,
+ http_client::get(http_client::GetParams {
+ executor: &executor,
+ url: &url,
+ on_progress: Some(Box::new({
+ let tasks = tasks.clone();
+ move |data: ProgressFuncData| {
+ tasks.push(Task::SetStatusText(format!(
+ "{}/{} KiB ({}%)",
+ data.bytes_downloaded / 1024,
+ data.file_size / 1024,
+ (data.bytes_downloaded as f32 / data.file_size as f32 * 100.0).round()
+ )))
+ }
+ })),
+ })
+ .await,
+ )?;
+
+ tasks.push(Task::SetStatusText(String::from("Writing to file...")));
+
+ // create skymaps directory if it doesn't exist yet
+ if let Some(parent) = target_path.parent() {
+ handle_async_result(
+ "Directory creation failed",
+ &tasks,
+ smol::fs::create_dir_all(parent).await,
+ )?;
+ }
+
+ handle_async_result(
+ "File write failed",
+ &tasks,
+ smol::fs::write(target_path, res.data).await,
+ )?;
+
+ tasks.push(Task::SetStatusText(String::from("Download finished")));
+ tasks.push(Task::ShowIconSuccess);
+
+ on_downloaded();
+
+ None
+ }
+}
+
+pub fn mount_popup(
+ popup: PopupHolder,
+ frontend_tasks: FrontendTasks,
+ on_view_close: Box,
+ params: Params,
+) {
+ frontend_tasks
+ .clone()
+ .push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
+ Translation::from_translation_key("DOWNLOADER"),
+ Box::new(move |data| {
+ let on_close_request = popup.get_close_callback(data.layout);
+ let view = View::new(data.layout, data.id_content, on_close_request, params)?;
+
+ popup.set_view(data.handle, view, Some(on_view_close));
+ Ok(popup.get_close_callback(data.layout))
+ }),
+ )));
+}
diff --git a/dash-frontend/src/views/game_cover.rs b/dash-frontend/src/views/game_cover.rs
index 6adc4110..5258b041 100644
--- a/dash-frontend/src/views/game_cover.rs
+++ b/dash-frontend/src/views/game_cover.rs
@@ -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,
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,
})
}
}
diff --git a/dash-frontend/src/views/game_launcher.rs b/dash-frontend/src/views/game_launcher.rs
index 03d26d77..17722bc1 100644
--- a/dash-frontend/src/views/game_launcher.rs
+++ b/dash-frontend/src/views/game_launcher.rs
@@ -4,9 +4,10 @@ use crate::{
frontend::{FrontendTask, FrontendTasks, SoundType},
util::{
cached_fetcher::{self, CoverArt},
+ popup_manager::{MountPopupOnceParams, PopupHolder},
steam_utils::{self, AppID, AppManifest},
},
- views::game_cover,
+ views::{ViewTrait, ViewUpdateParams, game_cover},
};
use wgui::{
assets::AssetPath,
@@ -34,13 +35,14 @@ pub struct Params<'a> {
pub layout: &'a mut Layout,
pub parent_id: WidgetID,
pub frontend_tasks: &'a FrontendTasks,
- pub on_launched: Box,
+ pub on_launched: Box,
}
+
pub struct View {
#[allow(dead_code)]
state: ParserState,
tasks: Tasks,
- on_launched: Box,
+ on_launched: Option>,
frontend_tasks: FrontendTasks,
game_cover_view_common: game_cover::ViewCommon,
@@ -48,6 +50,30 @@ pub struct View {
app_id: AppID,
}
+impl ViewTrait for View {
+ fn update(&mut self, par: &mut ViewUpdateParams) -> 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(&mut par.layout, details)?,
+ Task::Launch => self.action_launch(),
+ Task::SetCoverArt(cover_art) => {
+ let _ = self
+ .view_cover
+ .set_cover_art(&mut self.game_cover_view_common, &mut par.layout, &cover_art);
+ }
+ }
+ }
+ }
+
+ Ok(())
+ }
+}
+
impl View {
async fn fetch_details(executor: AsyncExecutor, tasks: Tasks, app_id: AppID) {
let Some(details) = cached_fetcher::get_app_details_json(executor, app_id).await else {
@@ -104,7 +130,7 @@ impl View {
Ok(Self {
state,
tasks,
- on_launched: params.on_launched,
+ on_launched: Some(params.on_launched),
frontend_tasks: params.frontend_tasks.clone(),
game_cover_view_common: game_cover::ViewCommon::new(params.globals.clone()),
view_cover,
@@ -112,43 +138,20 @@ impl View {
})
}
- 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(),
- Task::SetCoverArt(cover_art) => {
- let _ = self
- .view_cover
- .set_cover_art(&mut self.game_cover_view_common, layout, &cover_art);
- }
- }
- }
- }
-
- 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;
+ let mut c = layout.common();
+ let label_author = self.state.fetch_widget(&c.state, "label_author")?.widget;
+ let label_description = self.state.fetch_widget(&c.state, "label_description")?.widget;
if let Some(developer) = details.developers.pop() {
label_author
.cast::()?
- .set_text(&mut c.common(), Translation::from_raw_text_string(developer));
+ .set_text(&mut c, Translation::from_raw_text_string(developer));
}
let desc = if let Some(desc) = &details.short_description {
@@ -162,11 +165,10 @@ impl View {
if let Some(desc) = desc {
label_description
.cast::()?
- .set_text(&mut c.common(), Translation::from_raw_text(desc));
+ .set_text(&mut c, Translation::from_raw_text(desc));
}
}
- c.finish()?;
Ok(())
}
@@ -190,6 +192,37 @@ impl View {
}
}
- (*self.on_launched)();
+ if let Some(on_launched) = self.on_launched.take() {
+ on_launched();
+ }
}
}
+
+pub fn mount_popup(
+ frontend_tasks: FrontendTasks,
+ executor: AsyncExecutor,
+ globals: WguiGlobals,
+ manifest: AppManifest,
+ popup: PopupHolder,
+) {
+ frontend_tasks
+ .clone()
+ .push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
+ Translation::from_raw_text(&manifest.name),
+ Box::new(move |data| {
+ let on_launched = popup.get_close_callback(data.layout);
+ 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,
+ })?;
+
+ popup.set_view(data.handle, view, None);
+ Ok(popup.get_close_callback(data.layout))
+ }),
+ )));
+}
diff --git a/dash-frontend/src/views/game_list.rs b/dash-frontend/src/views/game_list.rs
index 5de3cf64..ab7e6448 100644
--- a/dash-frontend/src/views/game_list.rs
+++ b/dash-frontend/src/views/game_list.rs
@@ -1,4 +1,4 @@
-use std::{cell::RefCell, collections::HashMap, rc::Rc};
+use std::{collections::HashMap, rc::Rc};
use wgui::{
assets::AssetPath,
@@ -16,20 +16,19 @@ 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, ViewTrait, ViewUpdateParams, game_cover},
};
#[derive(Clone)]
enum Task {
AppManifestClicked(steam_utils::AppManifest),
SetCoverArt(AppID, Rc),
- CloseLauncher,
LoadManifests,
FillPage(u32),
PrevPage,
@@ -42,6 +41,7 @@ pub struct Params<'a> {
pub frontend_tasks: FrontendTasks,
pub layout: &'a mut Layout,
pub parent_id: WidgetID,
+ pub steam_utils: &'a SteamUtils,
}
const MAX_GAMES_PER_PAGE: u32 = 30;
@@ -50,10 +50,6 @@ pub struct GameCoverCell {
view_cover: game_cover::View,
}
-struct State {
- view_launcher: Option<(PopupHandle, views::game_launcher::View)>,
-}
-
pub struct View {
#[allow(dead_code)]
parser_state: ParserState,
@@ -63,12 +59,37 @@ pub struct View {
id_list_parent: WidgetID,
game_cover_view_common: game_cover::ViewCommon,
executor: AsyncExecutor,
- state: Rc>,
mounted_game_covers: HashMap,
all_manifests: Vec,
cur_page: u32,
page_count: u32,
id_label_page: WidgetID,
+ view_launcher: PopupHolder,
+ steam_utils: SteamUtils,
+}
+
+impl ViewTrait for View {
+ fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
+ loop {
+ let tasks = self.tasks.drain();
+ if tasks.is_empty() {
+ break;
+ }
+ for task in tasks {
+ match task {
+ Task::LoadManifests => self.load_manifests(),
+ Task::FillPage(page_idx) => self.fill_page(&mut par.layout, &mut par.executor, page_idx)?,
+ Task::AppManifestClicked(manifest) => self.action_app_manifest_clicked(manifest)?,
+ Task::SetCoverArt(app_id, cover_art) => self.set_cover_art(&mut par.layout, app_id, cover_art),
+ Task::PrevPage => self.page_prev(),
+ Task::NextPage => self.page_next(),
+ }
+ }
+ }
+
+ self.view_launcher.update(par)?;
+ Ok(())
+ }
}
impl View {
@@ -105,46 +126,15 @@ impl View {
id_list_parent: list_parent.id,
mounted_game_covers: HashMap::new(),
game_cover_view_common: game_cover::ViewCommon::new(params.globals.clone()),
- state: Rc::new(RefCell::new(State { view_launcher: None })),
executor: params.executor,
all_manifests: Vec::new(),
cur_page: 0,
page_count: 0,
id_label_page,
+ view_launcher: Default::default(),
+ steam_utils: params.steam_utils.clone(),
})
}
-
- pub fn update(
- &mut self,
- layout: &mut Layout,
- steam_utils: &mut SteamUtils,
- executor: &AsyncExecutor,
- ) -> anyhow::Result<()> {
- loop {
- let tasks = self.tasks.drain();
- if tasks.is_empty() {
- break;
- }
- for task in tasks {
- match task {
- Task::LoadManifests => self.load_manifests(steam_utils),
- Task::FillPage(page_idx) => self.fill_page(layout, executor, page_idx)?,
- Task::AppManifestClicked(manifest) => self.action_app_manifest_clicked(manifest)?,
- Task::SetCoverArt(app_id, cover_art) => self.set_cover_art(layout, app_id, cover_art),
- Task::CloseLauncher => self.state.borrow_mut().view_launcher = None,
- Task::PrevPage => self.page_prev(),
- Task::NextPage => self.page_next(),
- }
- }
- }
-
- let mut state = self.state.borrow_mut();
- if let Some((_, view)) = &mut state.view_launcher {
- view.update(layout)?;
- }
-
- Ok(())
- }
}
fn fill_game_list(
@@ -187,8 +177,11 @@ fn fill_game_list(
}
impl View {
- fn load_manifests(&mut self, steam_utils: &mut SteamUtils) {
- match steam_utils.list_installed_games(steam_utils::GameSortMethod::PlayDateDesc) {
+ fn load_manifests(&mut self) {
+ match self
+ .steam_utils
+ .list_installed_games(steam_utils::GameSortMethod::PlayDateDesc)
+ {
Ok(manifests) => {
self.page_count = (manifests.len() as u32 + MAX_GAMES_PER_PAGE) / MAX_GAMES_PER_PAGE;
self.all_manifests = manifests;
@@ -233,16 +226,14 @@ impl View {
}
// set page text
- let mut c = layout.start_common();
{
- let mut common = c.common();
- let mut widget = common.state.widgets.cast_as::(self.id_label_page)?;
+ let mut c = layout.common();
+ let mut widget = c.state.widgets.cast_as::(self.id_label_page)?;
widget.set_text(
- &mut common,
+ &mut c,
Translation::from_raw_text_string(format!("{}/{}", self.cur_page + 1, self.page_count)),
);
}
- c.finish()?;
fill_game_list(
&mut ConstructEssentials {
@@ -283,36 +274,13 @@ 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: {
- 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(())
- })
- },
- }));
+ views::game_launcher::mount_popup(
+ self.frontend_tasks.clone(),
+ self.executor.clone(),
+ self.globals.clone(),
+ manifest,
+ self.view_launcher.clone(),
+ );
Ok(())
}
diff --git a/dash-frontend/src/views/mod.rs b/dash-frontend/src/views/mod.rs
index d2cd9369..91cdee11 100644
--- a/dash-frontend/src/views/mod.rs
+++ b/dash-frontend/src/views/mod.rs
@@ -1,6 +1,23 @@
+use wlx_common::async_executor::AsyncExecutor;
+
pub mod app_launcher;
pub mod audio_settings;
+pub mod dialog_box;
+pub mod download_file;
pub mod game_cover;
pub mod game_launcher;
pub mod game_list;
+pub mod remote_skymap_downloader;
+pub mod remote_skymap_list;
pub mod running_games_list;
+pub mod skymap_list;
+pub mod skymap_list_cell;
+
+pub struct ViewUpdateParams<'a> {
+ pub layout: &'a mut wgui::layout::Layout,
+ pub executor: &'a AsyncExecutor,
+}
+
+pub trait ViewTrait {
+ fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()>;
+}
diff --git a/dash-frontend/src/views/remote_skymap_downloader.rs b/dash-frontend/src/views/remote_skymap_downloader.rs
new file mode 100644
index 00000000..84e03350
--- /dev/null
+++ b/dash-frontend/src/views/remote_skymap_downloader.rs
@@ -0,0 +1,372 @@
+use std::{collections::HashMap, rc::Rc};
+
+use crate::{
+ frontend::{FrontendTask, FrontendTasks},
+ util::{
+ networking::{self, skymap_catalog::SkymapResolution},
+ popup_manager::{MountPopupOnceParams, PopupHolder},
+ },
+ views::{self, ViewTrait, ViewUpdateParams},
+};
+use wgui::{
+ assets::AssetPath,
+ components::button::ComponentButton,
+ drawing::Color,
+ globals::WguiGlobals,
+ i18n::Translation,
+ layout::{Layout, WidgetID},
+ parser::{Fetchable, ParseDocumentParams, ParserState},
+ renderer_vk::text::custom_glyph::CustomGlyphData,
+ task::Tasks,
+ widget::{image::WidgetImage, label::WidgetLabel},
+};
+use wlx_common::{async_executor::AsyncExecutor, config_io};
+
+pub struct Params<'a> {
+ pub globals: &'a WguiGlobals,
+ pub layout: &'a mut Layout,
+ pub executor: &'a AsyncExecutor,
+ pub frontend_tasks: FrontendTasks,
+ pub parent_id: WidgetID,
+ pub entry: networking::skymap_catalog::SkymapCatalogEntry,
+ pub preview_image: CustomGlyphData,
+ pub preview_image_compressed: Rc>,
+ pub on_updated_library: Rc,
+}
+
+#[derive(Clone)]
+enum Task {
+ Refresh,
+ ResolutionClicked(SkymapResolution),
+ DownloadFinished,
+ RunDownload(SkymapResolution),
+ RemoveFile(SkymapResolution),
+}
+
+pub struct View {
+ entry: networking::skymap_catalog::SkymapCatalogEntry,
+ frontend_tasks: FrontendTasks,
+ globals: WguiGlobals,
+ tasks: Tasks,
+ executor: AsyncExecutor,
+
+ id_resolution_buttons: WidgetID,
+
+ #[allow(dead_code)]
+ parser_state: ParserState,
+
+ popup_download: PopupHolder,
+ popup_dialog_box: PopupHolder,
+
+ preview_image_compressed: Rc>,
+ on_updated_library: Rc,
+}
+
+fn mount_resolution_button(
+ layout: &mut Layout,
+ parser_state: &mut ParserState,
+ doc_params: &ParseDocumentParams,
+ parent_id: WidgetID,
+ res: SkymapResolution,
+ tasks: &Tasks,
+ already_downloaded: bool,
+) -> anyhow::Result<()> {
+ let mut t = HashMap::, Rc>::new();
+ t.insert(Rc::from("text"), Rc::from(res.get_display_str()));
+ t.insert(
+ Rc::from("sprite"),
+ Rc::from(match already_downloaded {
+ true => "dashboard/check.svg",
+ false => "dashboard/download.svg",
+ }),
+ );
+ let data = parser_state.realize_template(doc_params, "ResolutionButton", layout, parent_id, t)?;
+ let button = data.fetch_component_as::("button")?;
+
+ if already_downloaded {
+ button.set_color(&mut layout.common(), Color::new(0.0, 0.4, 0.0, 1.0)); // green
+ }
+ tasks.handle_button(&button, Task::ResolutionClicked(res));
+ Ok(())
+}
+
+impl ViewTrait for View {
+ fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
+ for task in self.tasks.drain() {
+ match task {
+ Task::ResolutionClicked(resolution) => {
+ self.resolution_clicked(resolution)?;
+ }
+ Task::Refresh => {
+ self.refresh(par.layout)?;
+ }
+ Task::DownloadFinished => {
+ self.download_finished()?;
+ }
+ Task::RunDownload(resolution) => {
+ self.run_download(resolution)?;
+ }
+ Task::RemoveFile(resolution) => {
+ self.remove_file(resolution)?;
+ }
+ }
+ }
+
+ self.popup_download.update(par)?;
+ self.popup_dialog_box.update(par)?;
+ Ok(())
+ }
+}
+
+fn doc_params(globals: &WguiGlobals) -> ParseDocumentParams<'_> {
+ ParseDocumentParams {
+ globals: globals.clone(),
+ path: AssetPath::BuiltIn("gui/view/remote_skymap_downloader.xml"),
+ extra: Default::default(),
+ }
+}
+
+impl View {
+ pub fn new(par: Params) -> anyhow::Result {
+ let tasks = Tasks::::new();
+
+ let parser_state = wgui::parser::parse_from_assets(&doc_params(&par.globals), par.layout, par.parent_id)?;
+ let id_resolution_buttons = parser_state.get_widget_id("resolution_buttons")?;
+
+ let str_version = par.globals.i18n().translate("VERSION");
+ let str_creation_date = par.globals.i18n().translate("CREATION_DATE");
+ let str_modification_date = par.globals.i18n().translate("MODIFICATION_DATE");
+
+ let image = parser_state.fetch_widget(&par.layout.state, "image")?.widget;
+ let mut image = image.cast::()?;
+ image.set_content(&mut par.layout.alterables, Some(par.preview_image));
+
+ // Set author label
+ parser_state
+ .fetch_widget_as::(&par.layout.state, "label_author")?
+ .set_text_simple(
+ &mut par.globals.get(),
+ Translation::from_raw_text_string(format!("by {}", par.entry.author)),
+ );
+
+ // Set description label
+ parser_state
+ .fetch_widget_as::(&par.layout.state, "label_description")?
+ .set_text_simple(
+ &mut par.globals.get(),
+ Translation::from_raw_text(&par.entry.description),
+ );
+
+ // Set version label
+ parser_state
+ .fetch_widget_as::(&par.layout.state, "label_version")?
+ .set_text_simple(
+ &mut par.globals.get(),
+ Translation::from_raw_text_string(format!("{}: {}", str_version, par.entry.version)),
+ );
+
+ // Set creation date label
+ parser_state
+ .fetch_widget_as::(&par.layout.state, "label_creation_date")?
+ .set_text_simple(
+ &mut par.globals.get(),
+ Translation::from_raw_text_string(format!("{}: {}", str_creation_date, par.entry.created_at)),
+ );
+
+ // Set modification date label
+ parser_state
+ .fetch_widget_as::(&par.layout.state, "label_modification_date")?
+ .set_text_simple(
+ &mut par.globals.get(),
+ Translation::from_raw_text_string(format!("{}: {}", str_modification_date, par.entry.created_at)),
+ );
+
+ tasks.push(Task::Refresh);
+
+ Ok(Self {
+ tasks,
+ globals: par.globals.clone(),
+ executor: par.executor.clone(),
+ entry: par.entry,
+ parser_state,
+ frontend_tasks: par.frontend_tasks,
+ popup_download: Default::default(),
+ popup_dialog_box: Default::default(),
+ id_resolution_buttons,
+ preview_image_compressed: par.preview_image_compressed,
+ on_updated_library: par.on_updated_library,
+ })
+ }
+
+ fn refresh(&mut self, layout: &mut Layout) -> anyhow::Result<()> {
+ layout.remove_children(self.id_resolution_buttons);
+
+ let files = &self.entry.files;
+ let mut mount_res = |res: SkymapResolution| -> anyhow::Result<()> {
+ mount_resolution_button(
+ layout,
+ &mut self.parser_state,
+ &doc_params(&self.globals),
+ self.id_resolution_buttons,
+ res,
+ &self.tasks,
+ self.entry.is_downloaded(res)?,
+ )
+ };
+
+ mount_res(SkymapResolution::Res2k)?;
+ if files.size_4k.is_some() {
+ mount_res(SkymapResolution::Res4k)?;
+ }
+ if files.size_8k.is_some() {
+ mount_res(SkymapResolution::Res8k)?;
+ }
+ Ok(())
+ }
+
+ fn resolution_clicked(&mut self, resolution: SkymapResolution) -> anyhow::Result<()> {
+ let is_downloaded = self.entry.is_downloaded(resolution).unwrap_or(false);
+ if !is_downloaded {
+ self.tasks.push(Task::RunDownload(resolution));
+ } else {
+ self.show_dialog_box_action(resolution)?;
+ }
+ Ok(())
+ }
+
+ fn show_dialog_box_action(&mut self, resolution: SkymapResolution) -> anyhow::Result<()> {
+ const ACTION_REMOVE: &'static str = "remove";
+ const ACTION_DOWNLOAD_AGAIN: &'static str = "download_again";
+
+ let tasks = self.tasks.clone();
+
+ views::dialog_box::mount_popup(
+ self.popup_dialog_box.clone(),
+ self.frontend_tasks.clone(),
+ views::dialog_box::Params {
+ globals: self.globals.clone(),
+ message: Translation::from_translation_key("APP_SETTINGS.SKYMAP_ALREADY_DOWNLOADED"),
+ entries: vec![
+ views::dialog_box::ButtonEntry {
+ content: Translation::from_translation_key("REMOVE"),
+ icon: "dashboard/trash.svg",
+ action: ACTION_REMOVE,
+ },
+ views::dialog_box::ButtonEntry {
+ content: Translation::from_translation_key("DOWNLOAD_AGAIN"),
+ icon: "dashboard/download.svg",
+ action: ACTION_DOWNLOAD_AGAIN,
+ },
+ ],
+ on_action_click: Box::new(move |action| match action {
+ ACTION_REMOVE => {
+ tasks.push(Task::RemoveFile(resolution));
+ tasks.push(Task::Refresh);
+ }
+ ACTION_DOWNLOAD_AGAIN => {
+ tasks.push(Task::RunDownload(resolution));
+ tasks.push(Task::Refresh);
+ }
+ _ => unreachable!(),
+ }),
+ },
+ );
+
+ Ok(())
+ }
+
+ fn download_finished(&mut self) -> anyhow::Result<()> {
+ self.entry.save_metadata()?;
+ let mut uuids = config_io::get_skymaps_uuids().unwrap_or_default();
+ let uuid_str = self.entry.uuid.to_string();
+ if !uuids.contains(&uuid_str) {
+ uuids.push(uuid_str);
+ }
+ config_io::set_skymaps_uuids(&uuids)?;
+
+ // Save preview image
+ self.entry.files.save_preview_to_file(&self.preview_image_compressed)?;
+
+ (*self.on_updated_library)();
+
+ Ok(())
+ }
+
+ fn run_download(&mut self, resolution: SkymapResolution) -> anyhow::Result<()> {
+ let Some(url) = self.entry.files.get_url_from_res(resolution) else {
+ return Ok(());
+ };
+
+ let Some(target_path) = self.entry.get_destination_path(resolution) else {
+ return Ok(());
+ };
+
+ views::download_file::mount_popup(
+ self.popup_download.clone(),
+ self.frontend_tasks.clone(),
+ self.tasks.make_callback_box(Task::Refresh),
+ views::download_file::Params {
+ globals: self.globals.clone(),
+ executor: self.executor.clone(),
+ target_path,
+ url,
+ on_downloaded: self.tasks.make_callback_box(Task::DownloadFinished),
+ },
+ );
+ Ok(())
+ }
+
+ fn remove_file(&mut self, resolution: SkymapResolution) -> anyhow::Result<()> {
+ self.entry.remove_file(resolution);
+
+ if !self.entry.has_any_downloaded() {
+ // all skymaps of this uuid are removed, clean-up files
+ self.entry.remove_metadata();
+
+ // remove uuid of this entry from downloaded skymaps uuid and save the file again
+ let mut uuids = config_io::get_skymaps_uuids().unwrap_or_default();
+ uuids.retain(|uuid| *uuid != self.entry.uuid.to_string());
+ config_io::set_skymaps_uuids(&uuids)?;
+
+ // remove "_preview.dds" files from the disk too
+ self.entry.files.remove_preview_file();
+ }
+
+ (*self.on_updated_library)();
+
+ Ok(())
+ }
+}
+
+pub fn mount_popup(
+ frontend_tasks: FrontendTasks,
+ executor: AsyncExecutor,
+ globals: WguiGlobals,
+ entry: networking::skymap_catalog::SkymapCatalogEntry,
+ preview_image: CustomGlyphData,
+ preview_image_compressed: Rc>,
+ on_updated_library: Rc,
+ popup: PopupHolder,
+) {
+ frontend_tasks
+ .clone()
+ .push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
+ Translation::from_raw_text(&entry.name),
+ Box::new(move |data| {
+ let view = View::new(Params {
+ globals: &globals,
+ layout: data.layout,
+ executor: &executor,
+ parent_id: data.id_content,
+ entry,
+ preview_image,
+ frontend_tasks: frontend_tasks.clone(),
+ preview_image_compressed,
+ on_updated_library,
+ })?;
+
+ popup.set_view(data.handle, view, None);
+ Ok(popup.get_close_callback(data.layout))
+ }),
+ )));
+}
diff --git a/dash-frontend/src/views/remote_skymap_list.rs b/dash-frontend/src/views/remote_skymap_list.rs
new file mode 100644
index 00000000..813cc7f3
--- /dev/null
+++ b/dash-frontend/src/views/remote_skymap_list.rs
@@ -0,0 +1,314 @@
+use std::rc::Rc;
+
+use uuid::Uuid;
+use wgui::{
+ assets::AssetPath,
+ globals::WguiGlobals,
+ i18n::Translation,
+ layout::{Layout, WidgetID},
+ parser::{Fetchable, ParseDocumentParams},
+ renderer_vk::text::custom_glyph::CustomGlyphData,
+ task::Tasks,
+};
+use wlx_common::async_executor::AsyncExecutor;
+
+use crate::{
+ frontend::{FrontendTask, FrontendTasks},
+ util::{
+ networking::{
+ self,
+ skymap_catalog::{SkymapCatalog, SkymapCatalogEntry, SkymapUuid},
+ },
+ popup_manager::{MountPopupOnceParams, PopupHolder},
+ wgui_simple,
+ },
+ views::{self, ViewTrait, ViewUpdateParams},
+};
+
+pub struct Params<'a> {
+ pub globals: &'a WguiGlobals,
+ pub layout: &'a mut Layout,
+ pub executor: &'a AsyncExecutor,
+ pub parent_id: WidgetID,
+ pub frontend_tasks: FrontendTasks,
+ pub on_updated_library: Rc,
+}
+
+#[derive(Clone)]
+enum Task {
+ SetSkymapCatalog(Rc>),
+ SetSkymapPreview(
+ (
+ SkymapUuid,
+ Option<(
+ CustomGlyphData, /* ready-to-use preview image data */
+ Rc>, /* compressed preview image data (should weigh about 10-15 KiB) */
+ )>,
+ ),
+ ),
+ ShowRemoteSkymapDownloader(SkymapUuid),
+ RefreshCells,
+}
+
+struct MountedCell {
+ skymap_uuid: SkymapUuid,
+ view: views::skymap_list_cell::View,
+ preview_image_compressed: Option>>,
+}
+
+pub struct View {
+ id_parent: WidgetID,
+ id_loading: WidgetID,
+ globals: WguiGlobals,
+ tasks: Tasks,
+ mounted_cells: Vec,
+ executor: AsyncExecutor,
+ frontend_tasks: FrontendTasks,
+ catalog: Option,
+ popup_remote_skymap_downloader: PopupHolder,
+ on_updated_library: Rc,
+}
+
+fn get_entry_by_uuid(catalog: &SkymapCatalog, skymap_uuid: Uuid) -> Option<&SkymapCatalogEntry> {
+ let Some(entry) = catalog.entries.iter().find(|entry| entry.uuid == skymap_uuid) else {
+ return None;
+ };
+
+ Some(entry)
+}
+
+impl ViewTrait for View {
+ fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
+ self.popup_remote_skymap_downloader.update(par)?;
+
+ for task in self.tasks.drain() {
+ match task {
+ Task::SetSkymapCatalog(skymap_catalog) => {
+ par.layout.remove_widget(self.id_loading);
+ match &*skymap_catalog {
+ Ok(skymap_catalog) => {
+ self.mount_catalog(par.layout, skymap_catalog)?;
+ }
+ Err(e) => wgui_simple::create_label_error(
+ par.layout,
+ self.id_parent,
+ format!("Failed to fetch skymap catalog: {:?}", e),
+ )?,
+ }
+ }
+ Task::SetSkymapPreview((skymap_uuid, opt_preview_image)) => {
+ if let Some(cell) = &mut self
+ .mounted_cells
+ .iter_mut()
+ .find(|cell| cell.skymap_uuid == skymap_uuid)
+ {
+ if let Some((preview_image, preview_image_compressed)) = opt_preview_image {
+ cell.view.set_image(par.layout, Some(preview_image))?;
+ cell.preview_image_compressed = Some(preview_image_compressed);
+ } else {
+ cell.view.set_image(par.layout, None)?;
+ }
+ }
+ }
+ Task::ShowRemoteSkymapDownloader(skymap_uuid) => {
+ if let Some((preview_image, preview_image_compressed)) = self.get_image_preview(skymap_uuid) {
+ self.show_remote_skymap_downloader(skymap_uuid, preview_image, preview_image_compressed)?;
+ } else {
+ log::error!("preview image not present, ignoring request");
+ }
+ }
+ Task::RefreshCells => {
+ self.refresh_cells(par.layout)?;
+ }
+ }
+ }
+ Ok(())
+ }
+}
+
+impl View {
+ async fn skymap_catalog_request_wrapper(tasks: Tasks, executor: AsyncExecutor) {
+ let res = networking::skymap_catalog::request_catalog(&executor).await;
+ tasks.push(Task::SetSkymapCatalog(Rc::new(res)))
+ }
+
+ pub fn new(par: Params) -> anyhow::Result {
+ let id_loading = wgui_simple::create_loading(wgui_simple::CreateLoadingParams {
+ layout: par.layout,
+ parent_id: par.parent_id,
+ with_text: true,
+ })?;
+ let tasks = Tasks::::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_loading,
+ tasks,
+ globals: par.globals.clone(),
+ mounted_cells: Vec::new(),
+ executor: par.executor.clone(),
+ frontend_tasks: par.frontend_tasks,
+ catalog: None,
+ popup_remote_skymap_downloader: Default::default(),
+ on_updated_library: par.on_updated_library,
+ })
+ }
+
+ async fn request_skymap_preview(
+ globals: WguiGlobals,
+ executor: AsyncExecutor,
+ entry: SkymapCatalogEntry,
+ tasks: Tasks,
+ ) {
+ tasks.push(Task::SetSkymapPreview((
+ entry.uuid,
+ networking::image_fetch::fetch_to_glyph_data(&globals, &executor, &entry.files.get_url_preview())
+ .await
+ .ok(),
+ )));
+ }
+
+ fn refresh_cells(&mut self, layout: &mut Layout) -> anyhow::Result<()> {
+ let Some(catalog) = &self.catalog else {
+ debug_assert!(false);
+ return Ok(());
+ };
+
+ for cell in &mut self.mounted_cells {
+ if let Some(entry) = get_entry_by_uuid(&catalog, cell.skymap_uuid) {
+ cell.view.refresh_resolution_pips(layout, entry)?;
+ }
+ }
+
+ Ok(())
+ }
+
+ 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/remote_skymap_list.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 task = View::request_skymap_preview(
+ self.globals.clone(),
+ self.executor.clone(),
+ entry.clone(),
+ self.tasks.clone(),
+ );
+
+ let skymap_uuid = entry.uuid.clone();
+
+ self.mounted_cells.push(MountedCell {
+ preview_image_compressed: None,
+ skymap_uuid: entry.uuid.clone(),
+ view: views::skymap_list_cell::View::new(views::skymap_list_cell::Params {
+ id_parent: id_list,
+ layout,
+ entry: entry.clone(),
+ on_click: self
+ .tasks
+ .get_button_click_callback(Task::ShowRemoteSkymapDownloader(skymap_uuid)),
+ })?,
+ });
+
+ self.executor.spawn(task).detach();
+ }
+
+ self.catalog = Some(catalog.clone());
+ Ok(())
+ }
+
+ fn show_remote_skymap_downloader(
+ &mut self,
+ uuid: SkymapUuid,
+ preview_image: CustomGlyphData,
+ preview_image_compressed: Rc>,
+ ) -> anyhow::Result<()> {
+ let Some(catalog) = &self.catalog else {
+ debug_assert!(false); // impossible
+ return Ok(());
+ };
+
+ let Some(entry) = get_entry_by_uuid(&catalog, uuid) else {
+ return Ok(());
+ };
+
+ // call our task before calling underlying on_updated_library callback
+ let on_updated_library = Rc::new({
+ let func = self.on_updated_library.clone();
+ let tasks = self.tasks.clone();
+ move || {
+ tasks.push(Task::RefreshCells);
+ (*func)();
+ }
+ });
+
+ views::remote_skymap_downloader::mount_popup(
+ self.frontend_tasks.clone(),
+ self.executor.clone(),
+ self.globals.clone(),
+ entry.clone(),
+ preview_image,
+ preview_image_compressed,
+ on_updated_library,
+ self.popup_remote_skymap_downloader.clone(),
+ );
+
+ Ok(())
+ }
+
+ fn get_image_preview(
+ &self,
+ skymap_uuid: SkymapUuid,
+ ) -> Option<(CustomGlyphData, Rc