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> /* preview_image_compressed */)> { + if let Some(cell) = &self.mounted_cells.iter().find(|mc| mc.skymap_uuid == skymap_uuid) { + let Some(image) = cell.view.get_image() else { + return None; + }; + + let Some(preview_image_compressed) = cell.preview_image_compressed.clone() else { + return None; + }; + return Some((image, preview_image_compressed)); + } + None + } +} + +pub fn mount_popup( + frontend_tasks: FrontendTasks, + executor: AsyncExecutor, + globals: WguiGlobals, + on_updated_library: Rc, + popup: PopupHolder, +) { + frontend_tasks + .clone() + .push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new( + Translation::from_translation_key("APP_SETTINGS.BROWSE_ONLINE_CATALOG"), + Box::new(move |data| { + let view = View::new(Params { + globals: &globals, + layout: data.layout, + executor: &executor, + parent_id: data.id_content, + frontend_tasks, + on_updated_library, + })?; + + popup.set_view(data.handle, view, None); + Ok(popup.get_close_callback(data.layout)) + }), + ))); +} diff --git a/dash-frontend/src/views/skymap_list.rs b/dash-frontend/src/views/skymap_list.rs new file mode 100644 index 00000000..66a58635 --- /dev/null +++ b/dash-frontend/src/views/skymap_list.rs @@ -0,0 +1,253 @@ +use anyhow::Context; +use wgui::{ + assets::AssetPath, + components::button::ComponentButton, + globals::WguiGlobals, + i18n::Translation, + layout::{Layout, WidgetID}, + parser::{Fetchable, ParseDocumentParams, ParserState}, + renderer_vk::text::custom_glyph::CustomGlyphData, + task::Tasks, +}; +use wlx_common::{async_executor::AsyncExecutor, config_io}; + +use crate::{ + frontend::{FrontendTask, FrontendTasks}, + util::{ + networking::skymap_catalog::{self, SkymapCatalogEntry, SkymapResolution}, + popup_manager::{MountPopupOnceParams, PopupHolder}, + wgui_simple, + }, + views::{self, ViewTrait, ViewUpdateParams}, +}; + +#[derive(Clone)] +enum Task { + DownloadSkymaps, + Refresh, + ShowSkymapResolutionSelector(SkymapCatalogEntry), + SetSkymap(SkymapCatalogEntry, SkymapResolution), +} + +pub struct Params<'a> { + pub globals: WguiGlobals, + pub layout: &'a mut Layout, + pub parent_id: WidgetID, + pub frontend_tasks: &'a FrontendTasks, +} + +struct Cell { + #[allow(dead_code)] + view: views::skymap_list_cell::View, +} + +pub struct View { + #[allow(dead_code)] + parser_state: ParserState, + tasks: Tasks, + list_parent: WidgetID, + frontend_tasks: FrontendTasks, + globals: WguiGlobals, + popup_remote_skymap_list: PopupHolder, + popup_dialog_box: PopupHolder, + cells: Vec, +} + +impl ViewTrait for View { + fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> { + self.popup_remote_skymap_list.update(par)?; + self.popup_dialog_box.update(par)?; + + loop { + let tasks = self.tasks.drain(); + if tasks.is_empty() { + break; + } + for task in tasks { + match task { + Task::DownloadSkymaps => { + self.download_skymaps(&par.executor)?; + } + Task::Refresh => { + self.refresh(&mut par.layout)?; + } + Task::ShowSkymapResolutionSelector(entry) => { + self.show_skymap_resolution_selector(entry); + } + Task::SetSkymap(entry, resolution) => { + self.set_skymap(entry, resolution)?; + } + } + } + } + + Ok(()) + } +} + +impl View { + pub fn new(params: Params) -> anyhow::Result { + let doc_params = &ParseDocumentParams { + globals: params.globals.clone(), + path: AssetPath::BuiltIn("gui/view/skymap_list.xml"), + extra: Default::default(), + }; + + let parser_state = wgui::parser::parse_from_assets(doc_params, params.layout, params.parent_id)?; + let list_parent = parser_state.fetch_widget(¶ms.layout.state, "list_parent")?.id; + let tasks = Tasks::new(); + + tasks.push(Task::Refresh); + + tasks.handle_button( + &parser_state.fetch_component_as::("btn_download_skymaps")?, + Task::DownloadSkymaps, + ); + + tasks.handle_button( + &parser_state.fetch_component_as::("btn_refresh")?, + Task::Refresh, + ); + + Ok(Self { + parser_state, + tasks, + list_parent, + frontend_tasks: params.frontend_tasks.clone(), + globals: params.globals.clone(), + popup_remote_skymap_list: Default::default(), + popup_dialog_box: Default::default(), + cells: Vec::new(), + }) + } + + fn download_skymaps(&mut self, executor: &AsyncExecutor) -> anyhow::Result<()> { + views::remote_skymap_list::mount_popup( + self.frontend_tasks.clone(), + executor.clone(), + self.globals.clone(), + self.tasks.make_callback_rc(Task::Refresh), /* on_updated_library */ + self.popup_remote_skymap_list.clone(), + ); + Ok(()) + } + + fn set_skymap(&mut self, entry: SkymapCatalogEntry, resolution: SkymapResolution) -> anyhow::Result<()> { + let skymap_file_path = entry + .get_destination_path(resolution) + .context("Skymap not found" /* you shouldn't really see this, like ever. */)?; + + log::error!( + "not implemented (skymap path to be loaded: {} with resolution {:?})", + skymap_file_path.to_string_lossy(), + resolution + ); + + Ok(()) + } + + fn show_skymap_resolution_selector(&mut self, entry: SkymapCatalogEntry) { + let tasks = self.tasks.clone(); + + let mut entries = Vec::::new(); + + let mut add_entry_resolution = |resolution: SkymapResolution| { + if entry.is_downloaded(resolution).unwrap_or(false) { + entries.push(views::dialog_box::ButtonEntry { + content: Translation::from_raw_text(resolution.get_display_str()), + icon: "dashboard/globe.svg", + action: resolution.get_display_str_simple(), + }); + } + }; + + add_entry_resolution(SkymapResolution::Res2k); + add_entry_resolution(SkymapResolution::Res4k); + add_entry_resolution(SkymapResolution::Res8k); + + if entries.is_empty() { + // if the skymap is not downloaded, how did we get there?! + debug_assert!(false); + return; + } + + views::dialog_box::mount_popup( + self.popup_dialog_box.clone(), + self.frontend_tasks.clone(), + views::dialog_box::Params { + globals: self.globals.clone(), + entries, + message: Translation::from_translation_key("APP_SETTINGS.SELECT_VARIANT"), + on_action_click: Box::new(move |action| { + let resolution = SkymapResolution::from_display_str_simple(action).unwrap(); // safety: never fails. + tasks.push(Task::SetSkymap(entry, resolution)) + }), + }, + ); + } + + fn refresh(&mut self, layout: &mut Layout) -> anyhow::Result<()> { + let entries = match skymap_catalog::get_entries_from_disk() { + Ok(entries) => entries, + Err(e) => { + log::error!("failed to get skymap entries: {}", e); + Default::default() + } + }; + + layout.remove_children(self.list_parent); + self.cells.clear(); + + if entries.is_empty() { + wgui_simple::create_label( + layout, + self.list_parent, + Translation::from_translation_key("APP_SETTINGS.NO_SKYMAPS_FOUND"), + )?; + return Ok(()); + } + + let skymaps_root = config_io::get_skymaps_root(); + + for entry in &entries { + let mut view = views::skymap_list_cell::View::new(views::skymap_list_cell::Params { + id_parent: self.list_parent, + layout, + entry: entry.clone(), + on_click: self + .tasks + .get_button_click_callback(Task::ShowSkymapResolutionSelector(entry.clone())), + })?; + + // load preview image + if let Ok(data) = std::fs::read(skymaps_root.join(&entry.files.preview)) { + if let Ok(glyph_data) = CustomGlyphData::from_bytes_raster(&self.globals, &entry.files.preview, &data) { + view.set_image(layout, Some(glyph_data))?; + } + } + + self.cells.push(Cell { view }); + } + + Ok(()) + } +} + +pub fn mount_popup(frontend_tasks: FrontendTasks, globals: WguiGlobals, popup: PopupHolder) { + frontend_tasks + .clone() + .push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new( + Translation::from_translation_key("APP_SETTINGS.BROWSE_SKYMAPS"), + Box::new(move |data| { + let view = View::new(Params { + globals: globals.clone(), + layout: data.layout, + parent_id: data.id_content, + frontend_tasks: &frontend_tasks, + })?; + + popup.set_view(data.handle, view, None); + Ok(popup.get_close_callback(data.layout)) + }), + ))); +} diff --git a/dash-frontend/src/views/skymap_list_cell.rs b/dash-frontend/src/views/skymap_list_cell.rs new file mode 100644 index 00000000..26bfcce3 --- /dev/null +++ b/dash-frontend/src/views/skymap_list_cell.rs @@ -0,0 +1,157 @@ +use std::{collections::HashMap, rc::Rc}; + +use wgui::{ + assets::AssetPath, + components::button::{ButtonClickCallback, ComponentButton}, + event::EventAlterables, + globals::WguiGlobals, + i18n::Translation, + layout::{Layout, WidgetID}, + parser::{Fetchable, ParseDocumentParams, ParserState}, + renderer_vk::text::custom_glyph::CustomGlyphData, + widget::{image::WidgetImage, label::WidgetLabel}, +}; + +use crate::util::{ + networking::{ + self, + skymap_catalog::{SkymapCatalogEntry, SkymapResolution}, + }, + wgui_simple, +}; + +pub struct Params<'a> { + pub id_parent: WidgetID, + pub layout: &'a mut Layout, + pub entry: networking::skymap_catalog::SkymapCatalogEntry, + pub on_click: ButtonClickCallback, +} + +pub struct View { + #[allow(dead_code)] + parser_state: ParserState, + id_loading: WidgetID, + id_image_preview: WidgetID, + id_resolution_pips: WidgetID, + image: Option, +} + +fn doc_params(globals: &'_ WguiGlobals) -> ParseDocumentParams<'_> { + ParseDocumentParams { + globals: globals.clone(), + path: AssetPath::BuiltIn("gui/view/skymap_list_cell.xml"), + extra: Default::default(), + } +} + +fn populate_res_pips( + layout: &mut Layout, + id_parent: WidgetID, + parser_state: &mut ParserState, + entry: &SkymapCatalogEntry, +) -> anyhow::Result<()> { + let globals = layout.state.globals.clone(); + layout.remove_children(id_parent); + + let mut populate_res_pip = |res: SkymapResolution| -> anyhow::Result<()> { + let mut tpar = HashMap::, Rc>::new(); + let downloaded = entry.is_downloaded(res).unwrap_or(false); + tpar.insert( + Rc::from("color"), + if downloaded { + Rc::from("#11aa40") + } else { + Rc::from("#444444") + }, + ); + tpar.insert(Rc::from("text"), res.get_display_str_simple().into()); + parser_state.realize_template(&doc_params(&globals), "ResolutionPip", layout, id_parent, tpar)?; + + Ok(()) + }; + + populate_res_pip(SkymapResolution::Res2k)?; + if entry.files.size_4k.is_some() { + populate_res_pip(SkymapResolution::Res4k)?; + } + if entry.files.size_8k.is_some() { + populate_res_pip(SkymapResolution::Res8k)?; + } + + Ok(()) +} + +impl View { + pub fn new(par: Params) -> anyhow::Result { + let globals = par.layout.state.globals.clone(); + + let mut parser_state = wgui::parser::parse_from_assets(&doc_params(&globals), par.layout, par.id_parent)?; + + let data = parser_state.realize_template( + &doc_params(&globals), + "Cell", + par.layout, + par.id_parent, + Default::default(), + )?; + + let id_image_preview = data.get_widget_id("image_preview")?; + let id_resolution_pips = data.get_widget_id("resolution_pips")?; + + data + .fetch_component_as::("button")? + .on_click(par.on_click); + + { + let mut label_title = data.fetch_widget_as::(&par.layout.state, "label_title")?; + let mut label_author = data.fetch_widget_as::(&par.layout.state, "label_author")?; + + label_title.set_text_simple( + &mut globals.get(), + Translation::from_raw_text_string(par.entry.name.clone()), + ); + label_author.set_text_simple( + &mut globals.get(), + Translation::from_raw_text_string(format!("by {}", par.entry.author)), + ); + } + + let id_loading = wgui_simple::create_loading(wgui_simple::CreateLoadingParams { + layout: par.layout, + parent_id: id_image_preview, + with_text: false, + })?; + + // Populate resolution pips + populate_res_pips(par.layout, id_resolution_pips, &mut parser_state, &par.entry)?; + + Ok(Self { + parser_state, + id_loading, + id_image_preview, + image: None, + id_resolution_pips, + }) + } + + pub fn refresh_resolution_pips(&mut self, layout: &mut Layout, entry: &SkymapCatalogEntry) -> anyhow::Result<()> { + populate_res_pips(layout, self.id_resolution_pips, &mut self.parser_state, &entry)?; + Ok(()) + } + + pub fn set_image(&mut self, layout: &mut Layout, content: Option) -> anyhow::Result<()> { + layout.remove_widget(self.id_loading); + let mut alt = EventAlterables::default(); + { + let mut image_preview = layout.state.widgets.cast_as::(self.id_image_preview)?; + image_preview.set_content(&mut alt, content.clone()); + } + layout.process_alterables(alt)?; + self.image = content; + Ok(()) + } + + pub fn get_image(&self) -> Option { + return self.image.clone(); + } +} diff --git a/uidev/src/testbed/testbed_generic.rs b/uidev/src/testbed/testbed_generic.rs index 802863f4..d1d078da 100644 --- a/uidev/src/testbed/testbed_generic.rs +++ b/uidev/src/testbed/testbed_generic.rs @@ -46,7 +46,6 @@ pub struct TestbedGeneric { pub parser_state: ParserState, tasks: Tasks, - globals: WguiGlobals, data: Rc>, } @@ -183,7 +182,6 @@ impl TestbedGeneric { layout, parser_state, tasks: Default::default(), - globals: globals.clone(), data: Rc::new(RefCell::new(Data { popup_window: WguiWindow::default(), context_menu: context_menu::ContextMenu::default(), diff --git a/wayvr/Cargo.toml b/wayvr/Cargo.toml index fa78a7b8..aedac498 100644 --- a/wayvr/Cargo.toml +++ b/wayvr/Cargo.toml @@ -83,7 +83,7 @@ sysinfo = { version = "0.37" } thiserror = "2.0" tracing = "0.1.43" tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } -uuid = { version = "1.19.0", features = ["fast-rng", "v4"] } +uuid = { workspace = true } wayland-client = { workspace = true } winit = { version = "0.30.12", optional = true } xcb = { version = "1.6.0", features = [ diff --git a/wayvr/src/backend/openxr/helpers.rs b/wayvr/src/backend/openxr/helpers.rs index 56ed80ca..5f85a094 100644 --- a/wayvr/src/backend/openxr/helpers.rs +++ b/wayvr/src/backend/openxr/helpers.rs @@ -212,9 +212,9 @@ pub(super) fn posef_to_transform(pose: &xr::Posef) -> Affine3A { Affine3A::from_rotation_translation(rotation, translation) } -pub(super) fn reconfigure_chroma_key(app: &AppState) { +pub(super) fn try_apply_chroma_key(app: &AppState) -> bool { let Some(monado) = app.monado_state.as_ref() else { - return; + return false; }; let params = &app.session.config.chroma_key_params; @@ -226,6 +226,10 @@ pub(super) fn reconfigure_chroma_key(app: &AppState) { params.curve, params.despill, ) { - log::warn!("Could not set Chroma Key: {e:?}") + log::warn!("Could not set Chroma Key: {e:?}"); + return false; } + + // if all values were non-0, assume we're in chroma key mode + params.hsv_max[0] * params.hsv_max[1] * params.hsv_max[2] * params.curve > 0.001 } diff --git a/wayvr/src/backend/openxr/mod.rs b/wayvr/src/backend/openxr/mod.rs index acdb2086..8e3787ed 100644 --- a/wayvr/src/backend/openxr/mod.rs +++ b/wayvr/src/backend/openxr/mod.rs @@ -17,7 +17,7 @@ use crate::{ backend::{ BackendError, XrBackend, input::interact, - openxr::{helpers::reconfigure_chroma_key, lines::LinePool, overlay::OpenXrOverlayData}, + openxr::{helpers::try_apply_chroma_key, lines::LinePool, overlay::OpenXrOverlayData}, task::{OpenXrTask, OverlayTask, TaskType}, }, config::{save_settings, save_state}, @@ -100,8 +100,6 @@ pub fn openxr_run( .ok() }); - reconfigure_chroma_key(&app); - let mut blocker = app .monado_state .as_ref() @@ -488,7 +486,7 @@ pub fn openxr_run( } } TaskType::OpenXR(task) => { - if matches!(task, OpenXrTask::SettingsChanged) { + if matches!(task, OpenXrTask::EnvironmentChanged) { reconfigure_environment_blend( &app, &xr_state, @@ -497,7 +495,6 @@ pub fn openxr_run( &mut environment_blend_mode, main_session_visible, ); - reconfigure_chroma_key(&app); } } #[cfg(feature = "openvr")] @@ -538,6 +535,9 @@ pub(super) enum CompositionLayer<'a> { Equirect2(xr::CompositionLayerEquirect2KHR<'a, xr::Vulkan>), } +// applies chroma key settings. +// if chroma key or passthrough is enabled, use alpha env blend +// if alpha blend is not used and skybox is enabled, use skybox fn reconfigure_environment_blend( app: &AppState, xr_state: &XrState, @@ -546,9 +546,11 @@ fn reconfigure_environment_blend( environment_blend_mode: &mut xr::EnvironmentBlendMode, main_session_visible: bool, ) { + let has_chroma_key = try_apply_chroma_key(app); + *environment_blend_mode = { if modes.contains(&xr::EnvironmentBlendMode::ALPHA_BLEND) - && app.session.config.use_passthrough + && (app.session.config.use_passthrough || has_chroma_key) { xr::EnvironmentBlendMode::ALPHA_BLEND } else { @@ -560,13 +562,14 @@ fn reconfigure_environment_blend( && app.session.config.use_skybox && !main_session_visible; - if want_skybox == skybox.is_some() { - return; - } - if want_skybox { - log::debug!("Allocating skybox."); - *skybox = create_skybox(xr_state, app); + if let Some(curr_skybox) = skybox.as_ref() { + if curr_skybox.needs_recreate(app) { + *skybox = None; + log::debug!("Allocating skybox."); + *skybox = create_skybox(xr_state, app); + } + } } else { log::debug!("Destroying skybox."); *skybox = None; diff --git a/wayvr/src/backend/openxr/monado_state.rs b/wayvr/src/backend/openxr/monado_state.rs index 418e5673..ff27fc00 100644 --- a/wayvr/src/backend/openxr/monado_state.rs +++ b/wayvr/src/backend/openxr/monado_state.rs @@ -19,6 +19,8 @@ impl MonadoState { Ok(res) } + #[allow(clippy::missing_const_for_fn)] + #[allow(clippy::unused_self)] pub fn update(&mut self) { #[cfg(feature = "feat-monado-metrics")] if let Some(metrics) = &mut self.metrics { @@ -30,6 +32,10 @@ impl MonadoState { } } + #[allow(clippy::missing_const_for_fn)] + #[allow(clippy::unused_self)] + #[allow(clippy::unnecessary_wraps)] + #[cfg(feature = "feat-monado-metrics")] pub fn set_metrics_enabled(&mut self, enabled: bool) -> anyhow::Result<()> { #[cfg(feature = "feat-monado-metrics")] { diff --git a/wayvr/src/backend/openxr/skybox.rs b/wayvr/src/backend/openxr/skybox.rs index 06b2e970..1ca0c072 100644 --- a/wayvr/src/backend/openxr/skybox.rs +++ b/wayvr/src/backend/openxr/skybox.rs @@ -29,6 +29,7 @@ pub(super) struct Skybox { grid: Option, grid_pose: xr::Posef, grid_color_scale_bias_khr: Option>, + current_skybox: Arc, } impl Skybox { @@ -66,6 +67,12 @@ impl Skybox { } } + let current_skybox = if maybe_image.is_some() { + app.session.config.skybox_texture.clone() + } else { + "".into() + }; + if maybe_image.is_none() { let p = include_bytes!("../../res/table_mountain_2.dds"); maybe_image = Some(command_buffer.upload_image_dds(p.as_slice())?); @@ -99,9 +106,14 @@ impl Skybox { grid: None, grid_pose: translation_rotation_to_posef(Vec3A::ZERO, Quat::from_rotation_x(PI * -0.5)), grid_color_scale_bias_khr, + current_skybox, }) } + pub(super) fn needs_recreate(&self, app: &AppState) -> bool { + *self.current_skybox != *app.session.config.skybox_texture + } + fn prepare_sky<'a>( &'a mut self, xr: &'a XrState, diff --git a/wayvr/src/backend/task.rs b/wayvr/src/backend/task.rs index 002881e5..2bc86886 100644 --- a/wayvr/src/backend/task.rs +++ b/wayvr/src/backend/task.rs @@ -56,7 +56,7 @@ pub enum OpenVrTask { #[cfg(feature = "openxr")] pub enum OpenXrTask { - SettingsChanged, + EnvironmentChanged, } pub enum PlayspaceTask { diff --git a/wayvr/src/gui/panel/mod.rs b/wayvr/src/gui/panel/mod.rs index 85b89615..c0f6d8ac 100644 --- a/wayvr/src/gui/panel/mod.rs +++ b/wayvr/src/gui/panel/mod.rs @@ -1,10 +1,20 @@ -use std::{cell::RefCell, rc::Rc}; - +use super::timer::GuiTimer; +use crate::{ + app_misc, + backend::input::{Haptics, HoverResult, PointerHit, PointerMode}, + backend::task::ModifyPanelCommand, + state::AppState, + subsystem::hid::WheelDelta, + windowing::backend::{ + FrameMeta, OverlayBackend, OverlayEventData, RenderResources, ShouldRender, ui_transform, + }, +}; use anyhow::Context; use button::setup_custom_button; use glam::{Affine2, Vec2, vec2}; use idmap::IdMap; use label::setup_custom_label; +use std::{cell::RefCell, rc::Rc}; use wgui::{ assets::AssetPath, components::{ @@ -12,9 +22,9 @@ use wgui::{ slider::ComponentSlider, }, event::{ - CallbackDataCommon, Event as WguiEvent, EventAlterables, EventCallback, EventListenerID, - EventListenerKind, InternalStateChangeEvent, MouseButtonEvent, MouseButtonIndex, - MouseLeaveEvent, MouseMotionEvent, MouseWheelEvent, + Event as WguiEvent, EventCallback, EventListenerID, EventListenerKind, + InternalStateChangeEvent, MouseButtonEvent, MouseButtonIndex, MouseLeaveEvent, + MouseMotionEvent, MouseWheelEvent, }, gfx::cmd::WGfxClearMode, i18n::Translation, @@ -33,19 +43,6 @@ use wgui::{ use wlx_common::overlays::{BackendAttrib, BackendAttribValue}; use wlx_common::timestep::Timestep; -use crate::{ - app_misc, - backend::input::{Haptics, HoverResult, PointerHit, PointerMode}, - backend::task::ModifyPanelCommand, - state::AppState, - subsystem::hid::WheelDelta, - windowing::backend::{ - FrameMeta, OverlayBackend, OverlayEventData, RenderResources, ShouldRender, ui_transform, - }, -}; - -use super::timer::GuiTimer; - pub mod button; pub mod device_list; mod label; @@ -493,17 +490,13 @@ pub fn apply_custom_command( element: &str, command: &ModifyPanelCommand, ) -> anyhow::Result<()> { - let mut alterables = EventAlterables::default(); - let mut com = CallbackDataCommon { - alterables: &mut alterables, - state: &panel.layout.state, - }; + let mut com = panel.layout.common(); match command { ModifyPanelCommand::SetText(text) => { if let Ok(mut label) = panel .parser_state - .fetch_widget_as::(&panel.layout.state, element) + .fetch_widget_as::(&com.state, element) { label.set_text(&mut com, Translation::from_raw_text(text)); } else if let Ok(button) = panel @@ -516,10 +509,7 @@ pub fn apply_custom_command( } } ModifyPanelCommand::SetImage(path) => { - if let Ok(pair) = panel - .parser_state - .fetch_widget(&panel.layout.state, element) - { + if let Ok(pair) = panel.parser_state.fetch_widget(&com.state, element) { let data = CustomGlyphData::from_assets( &app.wgui_globals, wgui::assets::AssetPath::File(path), @@ -527,9 +517,9 @@ pub fn apply_custom_command( .context("Could not load content from supplied path.")?; if let Some(mut sprite) = pair.widget.get_as::() { - sprite.set_content(&mut com, Some(data)); + sprite.set_content(com.alterables, Some(data)); } else if let Some(mut image) = pair.widget.get_as::() { - image.set_content(&mut com, Some(data)); + image.set_content(com.alterables, Some(data)); } else { anyhow::bail!("No or with such id."); } @@ -541,10 +531,7 @@ pub fn apply_custom_command( let color = parse_color_hex(color) .context("Invalid color format, must be a html hex color!")?; - if let Ok(pair) = panel - .parser_state - .fetch_widget(&panel.layout.state, element) - { + if let Ok(pair) = panel.parser_state.fetch_widget(&com.state, element) { if let Some(mut rect) = pair.widget.get_as::() { rect.set_color(&mut com, color); } else if let Some(mut label) = pair.widget.get_as::() { @@ -610,6 +597,5 @@ pub fn apply_custom_command( } } - panel.layout.process_alterables(alterables)?; Ok(()) } diff --git a/wayvr/src/gui/panel/overlay_list.rs b/wayvr/src/gui/panel/overlay_list.rs index 64dc4e8c..90d9044f 100644 --- a/wayvr/src/gui/panel/overlay_list.rs +++ b/wayvr/src/gui/panel/overlay_list.rs @@ -1,15 +1,12 @@ -use std::{collections::HashMap, rc::Rc}; - +use crate::windowing::{OverlayID, backend::OverlayEventData, window::OverlayCategory}; use slotmap::{Key, SecondaryMap}; +use std::{collections::HashMap, rc::Rc}; use wgui::{ components::button::ComponentButton, - event::{CallbackDataCommon, EventAlterables}, layout::Layout, parser::{Fetchable, ParseDocumentParams, ParserState}, }; -use crate::windowing::{OverlayID, backend::OverlayEventData, window::OverlayCategory}; - #[derive(Default)] /// Helper for managing a list of overlays /// Populates `id="panels_root"` with ``, ``, `` templates @@ -25,7 +22,6 @@ impl OverlayList { layout: &mut Layout, parser_state: &mut ParserState, event_data: &OverlayEventData, - alterables: &mut EventAlterables, doc_params: &ParseDocumentParams, ) -> anyhow::Result { let mut elements_changed = false; @@ -98,11 +94,7 @@ impl OverlayList { }; if meta.visible { - let mut com = CallbackDataCommon { - alterables, - state: &layout.state, - }; - overlay_button.set_sticky_state(&mut com, true); + overlay_button.set_sticky_state(&mut layout.common(), true); } self.overlay_buttons.insert(meta.id, overlay_button); continue; @@ -121,31 +113,23 @@ impl OverlayList { let overlay_button = parser_state .fetch_component_as::(&format!("overlay_{i}"))?; if meta.visible { - let mut com = CallbackDataCommon { - alterables, - state: &layout.state, - }; - overlay_button.set_sticky_state(&mut com, true); + overlay_button.set_sticky_state(&mut layout.common(), true); } self.overlay_buttons.insert(meta.id, overlay_button); } elements_changed = true; } OverlayEventData::VisibleOverlaysChanged(overlays) => { - let mut com = CallbackDataCommon { - alterables, - state: &layout.state, - }; let mut overlay_buttons = self.overlay_buttons.clone(); for visible in overlays.as_ref() { if let Some(btn) = overlay_buttons.remove(*visible) { - btn.set_sticky_state(&mut com, true); + btn.set_sticky_state(&mut layout.common(), true); } } for btn in overlay_buttons.values() { - btn.set_sticky_state(&mut com, false); + btn.set_sticky_state(&mut layout.common(), false); } } _ => {} diff --git a/wayvr/src/gui/panel/set_list.rs b/wayvr/src/gui/panel/set_list.rs index 30767c7a..83922efd 100644 --- a/wayvr/src/gui/panel/set_list.rs +++ b/wayvr/src/gui/panel/set_list.rs @@ -2,7 +2,6 @@ use std::{collections::HashMap, rc::Rc}; use wgui::{ components::button::ComponentButton, - event::{CallbackDataCommon, EventAlterables}, layout::Layout, parser::{Fetchable, ParseDocumentParams, ParserState}, }; @@ -23,25 +22,20 @@ impl SetList { layout: &mut Layout, parser_state: &mut ParserState, event_data: &OverlayEventData, - alterables: &mut EventAlterables, doc_params: &ParseDocumentParams, ) -> anyhow::Result { let mut elements_changed = false; match event_data { OverlayEventData::ActiveSetChanged(current_set) => { - let mut com = CallbackDataCommon { - alterables, - state: &layout.state, - }; if let Some(old_set) = self.current_set.take() && let Some(old_set) = self.set_buttons.get_mut(old_set) { - old_set.set_sticky_state(&mut com, false); + old_set.set_sticky_state(&mut layout.common(), false); } if let Some(new_set) = current_set && let Some(new_set) = self.set_buttons.get_mut(*new_set) { - new_set.set_sticky_state(&mut com, true); + new_set.set_sticky_state(&mut layout.common(), true); } self.current_set = *current_set; } @@ -59,11 +53,7 @@ impl SetList { let set_button = parser_state.fetch_component_as::(&format!("set_{i}"))?; if self.current_set == Some(i) { - let mut com = CallbackDataCommon { - alterables, - state: &layout.state, - }; - set_button.set_sticky_state(&mut com, true); + set_button.set_sticky_state(&mut layout.common(), true); } self.set_buttons.push(set_button); } diff --git a/wayvr/src/overlays/dashboard.rs b/wayvr/src/overlays/dashboard.rs index 8a635bee..10b05964 100644 --- a/wayvr/src/overlays/dashboard.rs +++ b/wayvr/src/overlays/dashboard.rs @@ -16,7 +16,7 @@ use wgui::{ widget::EventResult, }; use wlx_common::{ - dash_interface::{self, DashInterface, RecenterMode}, + dash_interface::{self, ConfigChangeKind, DashInterface, RecenterMode}, locale::WayVRLangProvider, overlays::{BackendAttrib, BackendAttribValue}, }; @@ -448,16 +448,22 @@ impl DashInterface for DashInterfaceLive { &mut data.session.config } - fn config_changed(&mut self, data: &mut AppState) { + fn config_changed(&mut self, data: &mut AppState, kind: ConfigChangeKind) { data.session.config_dirty = true; - #[cfg(feature = "openxr")] - { - use crate::backend::task::OpenXrTask; - data.tasks - .enqueue(TaskType::OpenXR(OpenXrTask::SettingsChanged)); + + match kind { + ConfigChangeKind::OverlayConfig => data + .tasks + .enqueue(TaskType::Overlay(OverlayTask::SettingsChanged)), + ConfigChangeKind::EnvironmentBlend => { + #[cfg(feature = "openxr")] + { + use crate::backend::task::OpenXrTask; + data.tasks + .enqueue(TaskType::OpenXR(OpenXrTask::EnvironmentChanged)); + } + } } - data.tasks - .enqueue(TaskType::Overlay(OverlayTask::SettingsChanged)); } fn restart(&mut self, _data: &mut AppState) { diff --git a/wayvr/src/overlays/edit/mod.rs b/wayvr/src/overlays/edit/mod.rs index 66b19a40..aa0c5169 100644 --- a/wayvr/src/overlays/edit/mod.rs +++ b/wayvr/src/overlays/edit/mod.rs @@ -10,7 +10,7 @@ use glam::vec2; use slotmap::Key; use wgui::{ components::{button::ComponentButton, checkbox::ComponentCheckbox, slider::ComponentSlider}, - event::{CallbackDataCommon, EventAlterables, EventCallback}, + event::EventCallback, i18n::Translation, parser::Fetchable, widget::EventResult, @@ -466,98 +466,82 @@ fn reset_panel( *panel.state.id.borrow_mut() = id; let state = owc.active_state.as_mut().unwrap(); - let mut alterables = EventAlterables::default(); - let mut common = CallbackDataCommon { - alterables: &mut alterables, - state: &panel.layout.state, - }; + let mut com = panel.layout.common(); let c = panel .parser_state .fetch_component_as::("top_grab")?; - c.set_sticky_state(&mut common, !state.grabbable); + c.set_sticky_state(&mut com, !state.grabbable); let c = panel .parser_state .fetch_component_as::("lerp_slider")?; - c.set_value(&mut common, state.positioning.get_lerp().unwrap_or(1.0)); + c.set_value(&mut com, state.positioning.get_lerp().unwrap_or(1.0)); let c = panel .parser_state .fetch_component_as::("alpha_slider")?; - c.set_value(&mut common, state.alpha); + c.set_value(&mut com, state.alpha); let c = panel .parser_state .fetch_component_as::("curve_slider")?; - c.set_value(&mut common, state.curvature.unwrap_or(0.0)); + c.set_value(&mut com, state.curvature.unwrap_or(0.0)); let c = panel .parser_state .fetch_component_as::("additive_box")?; - c.set_checked(&mut common, state.additive); + c.set_checked(&mut com, state.additive); let c = panel .parser_state .fetch_component_as::("align_box")?; - c.set_checked(&mut common, state.positioning.get_align().unwrap_or(false)); + c.set_checked(&mut com, state.positioning.get_align().unwrap_or(false)); let c = panel .parser_state .fetch_component_as::("global_box")?; - c.set_checked(&mut common, owc.global); + c.set_checked(&mut com, owc.global); let c = panel .parser_state .fetch_component_as::("angle_fade_box")?; - c.set_checked(&mut common, state.angle_fade); + c.set_checked(&mut com, state.angle_fade); let c = panel .parser_state .fetch_component_as::("block_input_box")?; - c.set_checked(&mut common, state.block_input); + c.set_checked(&mut com, state.block_input); - panel - .state - .pos - .reset(&mut common, &state.positioning.into()); - panel.state.lock.reset(&mut common, state.interactable); - panel.state.tabs.reset(&mut common); + panel.state.pos.reset(&mut com, &state.positioning.into()); + panel.state.lock.reset(&mut com, state.interactable); + panel.state.tabs.reset(&mut com); if let Some(stereo) = attrib_value!( owc.backend.get_attrib(BackendAttrib::Stereo), BackendAttribValue::Stereo ) { - panel - .state - .tabs - .set_tab_visible(&mut common, "stereo", true); - panel.state.stereo.reset(&mut common, &stereo); + panel.state.tabs.set_tab_visible(&mut com, "stereo", true); + panel.state.stereo.reset(&mut com, &stereo); // Set the checkbox label based on stereo mode let translation = get_stereo_full_frame_translation(stereo); let c = panel .parser_state .fetch_component_as::("stereo_full_frame_box")?; - c.set_text(&mut common, Translation::from_translation_key(translation)); + c.set_text(&mut com, Translation::from_translation_key(translation)); } else { - panel - .state - .tabs - .set_tab_visible(&mut common, "stereo", false); + panel.state.tabs.set_tab_visible(&mut com, "stereo", false); } if let Some(mouse) = attrib_value!( owc.backend.get_attrib(BackendAttrib::MouseTransform), BackendAttribValue::MouseTransform ) { - panel.state.tabs.set_tab_visible(&mut common, "mouse", true); - panel.state.mouse.reset(&mut common, &mouse); + panel.state.tabs.set_tab_visible(&mut com, "mouse", true); + panel.state.mouse.reset(&mut com, &mouse); } else { - panel - .state - .tabs - .set_tab_visible(&mut common, "mouse", false); + panel.state.tabs.set_tab_visible(&mut com, "mouse", false); } if let Some(full_frame) = attrib_value!( @@ -567,7 +551,7 @@ fn reset_panel( let c = panel .parser_state .fetch_component_as::("stereo_full_frame_box")?; - c.set_checked(&mut common, full_frame); + c.set_checked(&mut com, full_frame); } if let Some(adjust_mouse) = attrib_value!( @@ -577,11 +561,9 @@ fn reset_panel( let c = panel .parser_state .fetch_component_as::("stereo_adjust_mouse_box")?; - c.set_checked(&mut common, adjust_mouse); + c.set_checked(&mut com, adjust_mouse); } - panel.layout.process_alterables(alterables)?; - Ok(()) } diff --git a/wayvr/src/overlays/edit/sprite_tab.rs b/wayvr/src/overlays/edit/sprite_tab.rs index 4468ec3e..afc97136 100644 --- a/wayvr/src/overlays/edit/sprite_tab.rs +++ b/wayvr/src/overlays/edit/sprite_tab.rs @@ -105,7 +105,7 @@ where .widgets .get_as::(self.top_sprite_id) { - sprite.set_content(common, Some(new.sprite.clone())); + sprite.set_content(common.alterables, Some(new.sprite.clone())); } } diff --git a/wayvr/src/overlays/keyboard/builder.rs b/wayvr/src/overlays/keyboard/builder.rs index b96b0368..ced68625 100644 --- a/wayvr/src/overlays/keyboard/builder.rs +++ b/wayvr/src/overlays/keyboard/builder.rs @@ -17,7 +17,7 @@ use wgui::{ animation::{Animation, AnimationEasing}, assets::AssetPath, drawing::{self, Color}, - event::{self, CallbackMetadata, EventAlterables, EventListenerKind}, + event::{self, CallbackMetadata, EventListenerKind}, layout::LayoutUpdateParams, log::LogErr, parser::{Fetchable, ParseDocumentParams}, @@ -265,13 +265,10 @@ pub(super) fn create_keyboard_panel( panel.on_notify = Some(Box::new({ let name = "kbd"; move |panel, app, event_data| { - let mut alterables = EventAlterables::default(); - let mut elems_changed = panel.state.overlay_list.on_notify( &mut panel.layout, &mut panel.parser_state, &event_data, - &mut alterables, &doc_params, )?; @@ -279,7 +276,6 @@ pub(super) fn create_keyboard_panel( &mut panel.layout, &mut panel.parser_state, &event_data, - &mut alterables, &doc_params, )?; @@ -322,7 +318,6 @@ pub(super) fn create_keyboard_panel( panel.process_custom_elems(app); } - panel.layout.process_alterables(alterables)?; Ok(()) } })); diff --git a/wayvr/src/overlays/watch.rs b/wayvr/src/overlays/watch.rs index f4f19582..596d2a4d 100644 --- a/wayvr/src/overlays/watch.rs +++ b/wayvr/src/overlays/watch.rs @@ -4,7 +4,7 @@ use glam::{Affine3A, Quat, Vec3, vec3}; use wgui::{ assets::AssetPath, components::button::ComponentButton, - event::{CallbackDataCommon, EventAlterables, StyleSetRequest}, + event::StyleSetRequest, parser::{Fetchable, ParseDocumentParams}, taffy, }; @@ -48,9 +48,7 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result { let mut panel = GuiPanel::new_from_template(app, watch_xml, state, NewGuiPanelParams::default())?; - let mut alterables = EventAlterables::default(); - sets_or_overlays(&panel, app, &mut alterables); - panel.layout.process_alterables(alterables)?; + sets_or_overlays(&mut panel, app); let doc_params = ParseDocumentParams { globals: panel.layout.state.globals.clone(), @@ -61,13 +59,10 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result { panel.on_notify = Some(Box::new({ let name = WATCH_NAME; move |panel, app, event_data| { - let mut alterables = EventAlterables::default(); - let mut elems_changed = panel.state.overlay_list.on_notify( &mut panel.layout, &mut panel.parser_state, &event_data, - &mut alterables, &doc_params, )?; @@ -75,7 +70,6 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result { &mut panel.layout, &mut panel.parser_state, &event_data, - &mut alterables, &doc_params, )?; @@ -93,16 +87,12 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result { .parser_state .fetch_component_as::("btn_edit_mode") { - let mut com = CallbackDataCommon { - alterables: &mut alterables, - state: &panel.layout.state, - }; - btn_edit_mode.set_sticky_state(&mut com, edit_mode); + btn_edit_mode.set_sticky_state(&mut panel.layout.common(), edit_mode); } } OverlayEventData::SettingsChanged => { panel.layout.mark_redraw(); - sets_or_overlays(panel, app, &mut alterables); + sets_or_overlays(panel, app); if app.session.config.clock_12h != panel.state.clock_12h { panel.state.clock_12h = app.session.config.clock_12h; @@ -135,7 +125,6 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result { panel.process_custom_elems(app); } - panel.layout.process_alterables(alterables)?; Ok(()) } })); @@ -173,11 +162,7 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result { }) } -fn sets_or_overlays( - panel: &GuiPanel, - app: &mut AppState, - alterables: &mut EventAlterables, -) { +fn sets_or_overlays(panel: &mut GuiPanel, app: &mut AppState) { let display = if app.session.config.sets_on_watch { [taffy::Display::None, taffy::Display::Flex] } else { @@ -196,6 +181,9 @@ fn sets_or_overlays( ]; for i in 0..2 { - alterables.set_style(widget[i], StyleSetRequest::Display(display[i])); + panel + .layout + .alterables + .set_style(widget[i], StyleSetRequest::Display(display[i])); } } diff --git a/wayvr/src/state.rs b/wayvr/src/state.rs index 76d48422..a2546111 100644 --- a/wayvr/src/state.rs +++ b/wayvr/src/state.rs @@ -9,7 +9,6 @@ use wgui::{ drawing, font_config::WguiFontConfig, gfx::WGfx, globals::WguiGlobals, parser::parse_color_hex, renderer_vk::context::SharedContext as WSharedContext, }; -use wlx_common::async_executor::AsyncExecutor; use wlx_common::locale::WayVRLangProvider; use wlx_common::{ audio, @@ -38,7 +37,6 @@ use crate::{ pub struct AppState { pub session: AppSession, pub tasks: TaskContainer, - pub executor: AsyncExecutor, pub gfx: Arc, pub gfx_extras: WGfxExtras, @@ -157,12 +155,9 @@ impl AppState { let lang_provider = WayVRLangProvider::from_config(&session.config); - let executor = Rc::new(smol::LocalExecutor::new()); - Ok(Self { session, tasks, - executor, gfx, gfx_extras, hid_provider, diff --git a/wayvr/src/subsystem/monado_metrics/metrics_fd.rs b/wayvr/src/subsystem/monado_metrics/metrics_fd.rs index 983a39c9..4e349ca1 100644 --- a/wayvr/src/subsystem/monado_metrics/metrics_fd.rs +++ b/wayvr/src/subsystem/monado_metrics/metrics_fd.rs @@ -8,6 +8,7 @@ use crate::subsystem::monado_metrics::proto; pub struct MonadoMetricsFd { stream_reader: UnixStream, + #[allow(dead_code)] stream_writer: UnixStream, records: VecDeque, diff --git a/wgui/src/animation.rs b/wgui/src/animation.rs index c89d29cb..2269e716 100644 --- a/wgui/src/animation.rs +++ b/wgui/src/animation.rs @@ -133,16 +133,16 @@ impl Animations { anim.pos_prev = anim.pos; anim.pos = pos; - anim.call(state, alterables, 1.0); if anim.last_tick { + anim.call(state, alterables, 1.0); alterables.needs_redraw = true; + } else { + anim.ticks_remaining -= 1; } - - anim.ticks_remaining -= 1; } - self.running_animations.retain(|anim| anim.ticks_remaining > 0); + self.running_animations.retain(|anim| !anim.last_tick); } pub fn process(&mut self, state: &LayoutState, alterables: &mut EventAlterables, alpha: f32) { diff --git a/wgui/src/components/bar_graph.rs b/wgui/src/components/bar_graph.rs index 37026997..6e3df392 100644 --- a/wgui/src/components/bar_graph.rs +++ b/wgui/src/components/bar_graph.rs @@ -70,11 +70,7 @@ impl ComponentTrait for ComponentBarGraph { fn refresh(&self, data: &mut RefreshData) { let state = self.state.borrow(); - - // FIXME: refactor this after merging feat-skybox-catalog branch - let mut lc = data.layout.start_common(); - self.update_limits_text(&state, &mut lc.common()); - let _ = lc.finish(); + self.update_limits_text(&state, &mut data.layout.common()); } } diff --git a/wgui/src/components/button.rs b/wgui/src/components/button.rs index e498d736..ab2bfda8 100644 --- a/wgui/src/components/button.rs +++ b/wgui/src/components/button.rs @@ -123,21 +123,16 @@ impl ComponentTrait for ComponentButton { fn refresh(&self, data: &mut RefreshData) { let mut state = self.state.borrow_mut(); - // FIXME: refactor this after merging feat-skybox-catalog branch - let mut lc = data.layout.start_common(); - if state.active_tooltip.is_some() { - let common = lc.common(); - if let Some(node_id) = common.state.nodes.get(self.base.get_id()) { - if !widget::is_node_visible(&common.state.tree, *node_id) { + let l_state = &data.layout.state; + if let Some(node_id) = l_state.nodes.get(self.base.get_id()) { + if !widget::is_node_visible(&l_state.tree, *node_id) { state.active_tooltip = None; // destroy the tooltip, this button is now hidden } } else { debug_assert!(false); } } - - let _ = lc.finish(); } } diff --git a/wgui/src/components/color_selector.rs b/wgui/src/components/color_selector.rs index bbda2afe..65d6a4e7 100644 --- a/wgui/src/components/color_selector.rs +++ b/wgui/src/components/color_selector.rs @@ -7,10 +7,9 @@ use crate::{ }, drawing::{self}, event::CallbackDataCommon, - globals::WguiGlobals, i18n::Translation, layout::{Layout, WidgetID, WidgetPair}, - parser::{self, Fetchable, ParseDocumentParams, ParserState}, + parser::{self, Fetchable, ParseDocumentParams}, widget::{ConstructEssentials, rectangle::WidgetRectangle, util::WLength}, windowing::window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra}, }; @@ -91,18 +90,12 @@ impl ComponentTrait for ComponentColorSelector { } } - // FIXME: refactor this after merging feat-skybox-catalog branch - let mut lc = data.layout.start_common(); - let mut common = lc.common(); - self.data.button.set_text( - &mut common, + &mut data.layout.common(), Translation::from_raw_text_string(format!("{}", state.color.to_hex_rgb())), ); - self.data.button.set_color(&mut common, state.color); - - let _ = lc.finish(); + self.data.button.set_color(&mut data.layout.common(), state.color); } } @@ -169,12 +162,10 @@ impl ComponentColorSelector { let slider_b = parser_state.fetch_component_as::("slider_b")?; { - let mut lc = layout.start_common(); - let common = &mut lc.common(); - - slider_r.set_value(common, state.color.r * 255.0); - slider_g.set_value(common, state.color.g * 255.0); - slider_b.set_value(common, state.color.b * 255.0); + let mut common = layout.common(); + slider_r.set_value(&mut common, state.color.r * 255.0); + slider_g.set_value(&mut common, state.color.g * 255.0); + slider_b.set_value(&mut common, state.color.b * 255.0); } slider_r.on_value_changed(self.gen_slider_callback(ColorIndex::Red)); diff --git a/wgui/src/components/editbox.rs b/wgui/src/components/editbox.rs index 0b9032bf..c28650e6 100644 --- a/wgui/src/components/editbox.rs +++ b/wgui/src/components/editbox.rs @@ -147,15 +147,9 @@ impl ComponentTrait for ComponentEditBox { } fn refresh(&self, data: &mut RefreshData) { - // FIXME: refactor this after merging feat-skybox-catalog branch - let mut lc = data.layout.start_common(); - let mut common = lc.common(); - let mut state = self.state.borrow_mut(); - let res = refresh_all(&mut common, &self.data, &mut state); + let res = refresh_all(&mut data.layout.common(), &self.data, &mut state); debug_assert!(res.is_some()); - - let _ = lc.finish(); } fn on_focus_change(&self, data: &mut FocusChangeData) { diff --git a/wgui/src/components/slider.rs b/wgui/src/components/slider.rs index 36ae9421..0814f329 100644 --- a/wgui/src/components/slider.rs +++ b/wgui/src/components/slider.rs @@ -111,13 +111,10 @@ pub struct ComponentSlider { impl ComponentTrait for ComponentSlider { fn refresh(&self, data: &mut RefreshData) { - // FIXME: refactor this after merging feat-skybox-catalog branch - let mut lc = data.layout.start_common(); - let mut common = lc.common(); + let mut common = data.layout.common(); let mut state = self.state.borrow_mut(); let value = state.values.value; state.set_value(&mut common, &self.data, value); - let _ = lc.finish(); } fn base(&self) -> &ComponentBase { diff --git a/wgui/src/event.rs b/wgui/src/event.rs index e3d93dac..cba47205 100644 --- a/wgui/src/event.rs +++ b/wgui/src/event.rs @@ -129,6 +129,11 @@ impl EventAlterables { self.dirty_widgets.push(widget_id); } + pub fn mark_dirty_and_redraw(&mut self, widget_id: WidgetID) { + self.mark_dirty(widget_id); + self.mark_redraw(); + } + pub fn mark_tick(&mut self, widget_id: WidgetID) { self.widgets_to_tick.insert(widget_id); } @@ -174,8 +179,7 @@ impl CallbackDataCommon<'_> { // helper functions pub fn mark_widget_dirty(&mut self, id: WidgetID) { - self.alterables.mark_dirty(id); - self.alterables.mark_redraw(); + self.alterables.mark_dirty_and_redraw(id); } pub fn globals(&self) -> RefMut<'_, globals::Globals> { diff --git a/wgui/src/layout.rs b/wgui/src/layout.rs index b3a15aed..0d195e32 100644 --- a/wgui/src/layout.rs +++ b/wgui/src/layout.rs @@ -86,6 +86,11 @@ impl WidgetMap { self.0.get(handle) } + // same as get(), but with error message + pub fn fetch(&self, handle: WidgetID) -> anyhow::Result { + self.get(handle).cloned().context("Failed to fetch widget") + } + pub fn insert(&mut self, obj: Widget) -> WidgetID { self .0 @@ -163,6 +168,9 @@ pub struct Layout { sounds_to_play_once: Vec, focused_component: Option, + // Global EventAlterables queue, always processed in update() call at the end + pub alterables: EventAlterables, + pub widgets_to_tick: Vec, // *Main root* @@ -218,31 +226,11 @@ fn add_child_internal( )) } -pub struct LayoutCommon<'a> { - alterables: EventAlterables, - pub layout: &'a mut Layout, -} - -impl LayoutCommon<'_> { - pub const fn common(&mut self) -> CallbackDataCommon<'_> { +impl Layout { + pub fn common(&mut self) -> CallbackDataCommon<'_> { CallbackDataCommon { alterables: &mut self.alterables, - state: &self.layout.state, - } - } - - pub fn finish(self) -> anyhow::Result<()> { - self.layout.process_alterables(self.alterables)?; - Ok(()) - } -} - -impl Layout { - // helper function - pub fn start_common(&mut self) -> LayoutCommon<'_> { - LayoutCommon { - alterables: EventAlterables::default(), - layout: self, + state: &self.state, } } @@ -338,13 +326,8 @@ impl Layout { self.needs_redraw = true; } - fn process_pending_components(&mut self, alterables: &mut EventAlterables) { + fn process_pending_components(&mut self) { for comp in std::mem::take(&mut self.components_to_refresh_once) { - let mut common = CallbackDataCommon { - state: &self.state, - alterables, - }; - comp.0.refresh(&mut RefreshData { layout: self }); } } @@ -596,6 +579,7 @@ impl Layout { tasks: LayoutTasks::new(), sounds_to_play_once: Vec::new(), focused_component: None, + alterables: Default::default(), }) } @@ -683,10 +667,12 @@ impl Layout { } pub fn update(&mut self, params: &mut LayoutUpdateParams) -> anyhow::Result { - let mut alterables = EventAlterables::default(); + // get all queued alterables and process them + let alterables = std::mem::take(&mut self.alterables); + self .animations - .process(&self.state, &mut alterables, params.timestep_alpha); + .process(&self.state, &mut self.alterables, params.timestep_alpha); self.process_alterables(alterables)?; self.try_recompute_layout(params.size)?; @@ -698,7 +684,7 @@ impl Layout { pub fn tick(&mut self) -> anyhow::Result<()> { let mut alterables = EventAlterables::default(); self.animations.tick(&self.state, &mut alterables); - self.process_pending_components(&mut alterables); + self.process_pending_components(); self.process_pending_widget_ticks(&mut alterables); self.process_alterables(alterables)?; Ok(()) @@ -720,9 +706,7 @@ impl Layout { } } LayoutTask::Dispatch(func) => { - let mut c = self.start_common(); - func(&mut c.common())?; - c.finish()?; + func(&mut self.common())?; } LayoutTask::SetWidgetStyle(widget_id, style_request) => { self.set_style_request(widget_id, &style_request); @@ -740,29 +724,26 @@ impl Layout { } pub fn set_focus(&mut self, to_focus: Option<&Component>) -> anyhow::Result<()> { - let mut c = self.start_common(); - - if let Some(focused) = &c.layout.focused_component + if let Some(focused) = &self.focused_component && let Some(focused) = focused.upgrade() { // Unfocus focused.on_focus_change(&mut FocusChangeData { - common: &mut c.common(), + common: &mut self.common(), focused: false, }); - c.layout.focused_component = None; + self.focused_component = None; } if let Some(to_focus) = to_focus { to_focus.0.on_focus_change(&mut FocusChangeData { - common: &mut c.common(), + common: &mut self.common(), focused: true, }); - c.layout.focused_component = Some(to_focus.weak()); + self.focused_component = Some(to_focus.weak()); } - c.finish()?; Ok(()) } diff --git a/wgui/src/task.rs b/wgui/src/task.rs index e3437cf7..e34be1ad 100644 --- a/wgui/src/task.rs +++ b/wgui/src/task.rs @@ -1,4 +1,4 @@ -use crate::components::button::ComponentButton; +use crate::components::button::{ButtonClickCallback, ComponentButton}; use std::{cell::RefCell, collections::VecDeque, rc::Rc}; pub struct Tasks(Rc>>); @@ -30,21 +30,31 @@ impl Default for Tasks { } } +// copyable tasks only! impl Tasks { - pub fn handle_button(&self, button: &Rc, task: TaskType) { - button.on_click({ - let this = self.clone(); - Rc::new(move |_, _| { - this.push(task.clone()); - Ok(()) - }) - }); + pub fn get_button_click_callback(&self, task: TaskType) -> ButtonClickCallback { + let this = self.clone(); + Rc::new(move |_, _| { + this.push(task.clone()); + Ok(()) + }) } - pub fn make_callback(&self, task: TaskType) -> Rc { + pub fn handle_button(&self, button: &Rc, task: TaskType) { + button.on_click(self.get_button_click_callback(task)); + } + + pub fn make_callback_rc(&self, task: TaskType) -> Rc { let this = self.clone(); Rc::new(move || { this.push(task.clone()); }) } + + pub fn make_callback_box(&self, task: TaskType) -> Box { + let this = self.clone(); + Box::new(move || { + this.push(task.clone()); + }) + } } diff --git a/wgui/src/widget/image.rs b/wgui/src/widget/image.rs index 0b27aa43..0a63ebac 100644 --- a/wgui/src/widget/image.rs +++ b/wgui/src/widget/image.rs @@ -4,7 +4,7 @@ use slotmap::Key; use crate::{ drawing::{self, ImagePrimitive, PrimitiveExtent}, - event::CallbackDataCommon, + event::EventAlterables, globals::Globals, layout::WidgetID, renderer_vk::text::custom_glyph::CustomGlyphData, @@ -44,13 +44,13 @@ impl WidgetImage { ) } - pub fn set_content(&mut self, common: &mut CallbackDataCommon, content: Option) { + pub fn set_content(&mut self, alterables: &mut EventAlterables, content: Option) { if self.params.glyph_data == content { return; } self.params.glyph_data = content; - common.mark_widget_dirty(self.id); + alterables.mark_dirty_and_redraw(self.id); } pub fn get_content(&self) -> Option { diff --git a/wgui/src/widget/sprite.rs b/wgui/src/widget/sprite.rs index e8063913..f9d3366f 100644 --- a/wgui/src/widget/sprite.rs +++ b/wgui/src/widget/sprite.rs @@ -5,7 +5,7 @@ use slotmap::Key; use crate::{ drawing::{self, PrimitiveExtent}, - event::CallbackDataCommon, + event::{CallbackDataCommon, EventAlterables}, globals::Globals, layout::WidgetID, renderer_vk::text::{ @@ -49,13 +49,13 @@ impl WidgetSprite { self.params.color } - pub fn set_content(&mut self, common: &mut CallbackDataCommon, content: Option) { + pub fn set_content(&mut self, alterables: &mut EventAlterables, content: Option) { if self.params.glyph_data == content { return; } self.params.glyph_data = content; - common.mark_widget_dirty(self.id); + alterables.mark_dirty_and_redraw(self.id); } pub fn get_content(&self) -> Option { diff --git a/wgui/src/windowing/window.rs b/wgui/src/windowing/window.rs index 82779273..328ef746 100644 --- a/wgui/src/windowing/window.rs +++ b/wgui/src/windowing/window.rs @@ -9,7 +9,6 @@ use crate::{ components::button::ComponentButton, drawing, event::{EventListenerKind, StyleSetRequest}, - globals::WguiGlobals, i18n::Translation, layout::{Layout, LayoutTask, LayoutTasks, WidgetPair}, parser::{self, Fetchable, ParserState}, @@ -271,21 +270,20 @@ impl WguiWindow { content.id }; - let mut c = params.layout.start_common(); if let Some(width) = params.extra.fixed_width { - c.common() + params + .layout .alterables .set_style(content_id, StyleSetRequest::Width(length(width))); } if let Some(height) = params.extra.fixed_height { - c.common() + params + .layout .alterables .set_style(content_id, StyleSetRequest::Height(length(height))); } - c.finish()?; - Ok(()) } diff --git a/wlx-common/src/config_io.rs b/wlx-common/src/config_io.rs index 961e4b1a..7976a5de 100644 --- a/wlx-common/src/config_io.rs +++ b/wlx-common/src/config_io.rs @@ -21,6 +21,23 @@ pub fn get_config_root() -> PathBuf { CONFIG_ROOT_PATH.clone() } +pub fn get_skymaps_root() -> PathBuf { + get_config_root().join("skymaps") +} + +pub fn get_skymaps_uuids() -> anyhow::Result> { + let data = std::fs::read_to_string(get_skymaps_root().join("skymaps.txt"))?; + Ok(data.lines().filter(|line| !line.is_empty()).map(String::from).collect()) +} + +pub fn set_skymaps_uuids(uuids: &[String]) -> anyhow::Result<()> { + let skymaps_root = get_skymaps_root(); + let _ = std::fs::create_dir_all(&skymaps_root); + let data = String::from_iter(uuids.iter().map(|uuid| format!("{}\n", uuid))); + std::fs::write(skymaps_root.join("skymaps.txt"), data)?; + Ok(()) +} + impl ConfigRoot { pub fn get_conf_d_path(&self) -> PathBuf { get_config_root().join(match self { diff --git a/wlx-common/src/dash_interface.rs b/wlx-common/src/dash_interface.rs index 7f80d557..bf9544ae 100644 --- a/wlx-common/src/dash_interface.rs +++ b/wlx-common/src/dash_interface.rs @@ -65,9 +65,15 @@ pub trait DashInterface { fn recenter_playspace(&mut self, data: &mut T, mode: RecenterMode) -> anyhow::Result<()>; fn desktop_finder<'a>(&'a mut self, data: &'a mut T) -> &'a mut DesktopFinder; fn general_config<'a>(&'a mut self, data: &'a mut T) -> &'a mut GeneralConfig; - fn config_changed(&mut self, data: &mut T); + fn config_changed(&mut self, data: &mut T, kind: ConfigChangeKind); fn restart(&mut self, data: &mut T); fn toggle_dashboard(&mut self, data: &mut T); } +#[derive(Clone, Copy)] +pub enum ConfigChangeKind { + OverlayConfig, + EnvironmentBlend, +} + pub type BoxDashInterface = Box>; diff --git a/wlx-common/src/dash_interface_emulated.rs b/wlx-common/src/dash_interface_emulated.rs index 3526de94..d2479e86 100644 --- a/wlx-common/src/dash_interface_emulated.rs +++ b/wlx-common/src/dash_interface_emulated.rs @@ -5,7 +5,7 @@ use wayvr_ipc::{ use crate::{ config::GeneralConfig, - dash_interface::{self, DashInterface, RecenterMode}, + dash_interface::{self, ConfigChangeKind, DashInterface, RecenterMode}, desktop_finder::DesktopFinder, gen_id, }; @@ -230,7 +230,7 @@ impl DashInterface<()> for DashInterfaceEmulated { &mut self.general_config } - fn config_changed(&mut self, _: &mut ()) {} + fn config_changed(&mut self, _: &mut (), _: ConfigChangeKind) {} fn restart(&mut self, _data: &mut ()) {}