From 1d8b22d9922c345d6d2e8348132f7150c7aa5aad Mon Sep 17 00:00:00 2001 From: Aleksander Date: Sat, 4 Apr 2026 14:47:01 +0200 Subject: [PATCH 01/36] dash-frontend: "Skybox" tab --- dash-frontend/assets/gui/tab/settings.xml | 3 ++- dash-frontend/assets/lang/en.json | 1 + dash-frontend/src/tab/settings/mod.rs | 6 ++++++ dash-frontend/src/tab/settings/tab_look_and_feel.rs | 2 -- dash-frontend/src/tab/settings/tab_skybox.rs | 12 ++++++++++++ wayvr/src/backend/openxr/monado_state.rs | 6 ++++++ wayvr/src/state.rs | 5 ----- wayvr/src/subsystem/monado_metrics/metrics_fd.rs | 1 + 8 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 dash-frontend/src/tab/settings/tab_skybox.rs 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/lang/en.json b/dash-frontend/assets/lang/en.json index 7d5e2e05..81fe3df7 100644 --- a/dash-frontend/assets/lang/en.json +++ b/dash-frontend/assets/lang/en.json @@ -99,6 +99,7 @@ "SCREEN_RENDER_DOWN_HELP": "Helps with aliasing on high-res screens", "SCROLL_SPEED": "Scroll speed", "SETS_ON_WATCH": "Sets on watch", + "SKYBOX": "Skybox", "SPACE_DRAG_MULTIPLIER": "Space drag multiplier", "SPACE_DRAG_UNLOCKED": "Allow space drag on all axes", "SPACE_ROTATE_UNLOCKED": "Allow space rotate on all axes", diff --git a/dash-frontend/src/tab/settings/mod.rs b/dash-frontend/src/tab/settings/mod.rs index 55eb02a9..0374fd95 100644 --- a/dash-frontend/src/tab/settings/mod.rs +++ b/dash-frontend/src/tab/settings/mod.rs @@ -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, } } @@ -533,6 +536,9 @@ impl TabSettings { TabNameEnum::Troubleshooting => { tab_troubleshooting::mount(&mut mp, root)?; } + TabNameEnum::Skybox => { + tab_skybox::mount(&mut mp, root)?; + } } Ok(()) 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..ad2d1a5a 100644 --- a/dash-frontend/src/tab/settings/tab_look_and_feel.rs +++ b/dash-frontend/src/tab/settings/tab_look_and_feel.rs @@ -7,14 +7,12 @@ 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)?; 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..41d05831 --- /dev/null +++ b/dash-frontend/src/tab/settings/tab_skybox.rs @@ -0,0 +1,12 @@ +use crate::tab::settings::{ + SettingType, + macros::{MacroParams, options_category, options_checkbox}, +}; +use wgui::layout::WidgetID; + +pub fn mount(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> { + let c = options_category(mp, parent, "APP_SETTINGS.SKYBOX", "dashboard/globe.svg")?; + options_checkbox(mp, c, SettingType::UseSkybox)?; + options_checkbox(mp, c, SettingType::OpaqueBackground)?; + Ok(()) +} 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/state.rs b/wayvr/src/state.rs index 9e8d7da2..051427c3 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, From 7f822b6787c1b2d313f1989def092aca12dafad4 Mon Sep 17 00:00:00 2001 From: Aleksander Date: Sat, 4 Apr 2026 21:50:35 +0200 Subject: [PATCH 02/36] dash-frontend: Skymap catalog fetch --- dash-frontend/assets/dashboard/download.svg | 1 + dash-frontend/assets/gui/view/skymap_list.xml | 11 ++ dash-frontend/assets/lang/en.json | 2 + dash-frontend/src/tab/settings/mod.rs | 38 +++++- dash-frontend/src/tab/settings/tab_skybox.rs | 40 ++++-- dash-frontend/src/util/http_client.rs | 10 ++ dash-frontend/src/util/mod.rs | 1 + dash-frontend/src/util/networking/mod.rs | 4 + .../src/util/networking/skymap_catalog.rs | 42 +++++++ dash-frontend/src/views/mod.rs | 1 + dash-frontend/src/views/skymap_list.rs | 117 ++++++++++++++++++ wlx-common/src/config_io.rs | 17 +++ 12 files changed, 273 insertions(+), 11 deletions(-) create mode 100644 dash-frontend/assets/dashboard/download.svg create mode 100644 dash-frontend/assets/gui/view/skymap_list.xml create mode 100644 dash-frontend/src/util/networking/mod.rs create mode 100644 dash-frontend/src/util/networking/skymap_catalog.rs create mode 100644 dash-frontend/src/views/skymap_list.rs 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/gui/view/skymap_list.xml b/dash-frontend/assets/gui/view/skymap_list.xml new file mode 100644 index 00000000..00465088 --- /dev/null +++ b/dash-frontend/assets/gui/view/skymap_list.xml @@ -0,0 +1,11 @@ + + +
+
+
+
+
+ + diff --git a/dash-frontend/assets/lang/en.json b/dash-frontend/assets/lang/en.json index 81fe3df7..122d8206 100644 --- a/dash-frontend/assets/lang/en.json +++ b/dash-frontend/assets/lang/en.json @@ -53,6 +53,7 @@ "DELETE_ALL_CONFIGS_HELP": "Remove all configuration files from conf.d", "DOUBLE_CURSOR_FIX": "Double cursor fix", "DOUBLE_CURSOR_FIX_HELP": "Enable this if you see 2 cursors", + "DOWNLOAD_SKYMAPS": "Download skymaps", "FEATURES": "Features", "FOCUS_FOLLOWS_MOUSE_MODE": "Mouse move on trigger touch", "HANDSFREE_POINTER": "Handsfree mode", @@ -72,6 +73,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", diff --git a/dash-frontend/src/tab/settings/mod.rs b/dash-frontend/src/tab/settings/mod.rs index 0374fd95..1367f193 100644 --- a/dash-frontend/src/tab/settings/mod.rs +++ b/dash-frontend/src/tab/settings/mod.rs @@ -20,7 +20,9 @@ use wgui::{ }, windowing::context_menu::{self, Blueprint, ContextMenu, TickResult}, }; -use wlx_common::{config::GeneralConfig, config_io::ConfigRoot, dash_interface::RecenterMode}; +use wlx_common::{ + async_executor::AsyncExecutor, config::GeneralConfig, config_io::ConfigRoot, dash_interface::RecenterMode, +}; use crate::{ frontend::{Frontend, FrontendTask}, @@ -78,12 +80,31 @@ enum Task { SetTab(TabNameEnum), } +struct SettingsMountParams<'a> { + mp: &'a mut MacroParams<'a>, + globals: &'a WguiGlobals, + parent_id: WidgetID, +} + +struct SettingsUpdateParams<'a> { + layout: &'a mut Layout, + executor: &'a AsyncExecutor, +} + +trait SettingsTab { + fn update(&mut self, _par: SettingsUpdateParams) -> anyhow::Result<()> { + Ok(()) + } +} + pub struct TabSettings { pub state: ParserState, app_button_ids: Vec>, context_menu: ContextMenu, + current_tab: Option>, + tasks: Tasks, marker: PhantomData, } @@ -94,6 +115,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(SettingsUpdateParams { + layout: &mut frontend.layout, + executor: &frontend.executor, + })?; + } + let mut changed = false; for task in self.tasks.drain() { match task { @@ -507,6 +535,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, @@ -537,7 +566,11 @@ impl TabSettings { tab_troubleshooting::mount(&mut mp, root)?; } TabNameEnum::Skybox => { - tab_skybox::mount(&mut mp, root)?; + self.current_tab = Some(Box::new(tab_skybox::State::mount(SettingsMountParams { + mp: &mut mp, + globals: &globals, + parent_id: root, + })?)); } } @@ -572,6 +605,7 @@ impl TabSettings { state: parser_state, marker: PhantomData, context_menu: ContextMenu::default(), + current_tab: None, }) } } diff --git a/dash-frontend/src/tab/settings/tab_skybox.rs b/dash-frontend/src/tab/settings/tab_skybox.rs index 41d05831..3738f1aa 100644 --- a/dash-frontend/src/tab/settings/tab_skybox.rs +++ b/dash-frontend/src/tab/settings/tab_skybox.rs @@ -1,12 +1,34 @@ -use crate::tab::settings::{ - SettingType, - macros::{MacroParams, options_category, options_checkbox}, +use crate::{ + tab::settings::{ + SettingType, SettingsMountParams, SettingsTab, + macros::{options_category, options_checkbox}, + }, + views::skymap_list, }; -use wgui::layout::WidgetID; -pub fn mount(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> { - let c = options_category(mp, parent, "APP_SETTINGS.SKYBOX", "dashboard/globe.svg")?; - options_checkbox(mp, c, SettingType::UseSkybox)?; - options_checkbox(mp, c, SettingType::OpaqueBackground)?; - Ok(()) +pub struct State { + skymap_list: skymap_list::View, +} + +impl SettingsTab for State { + fn update(&mut self, par: super::SettingsUpdateParams) -> anyhow::Result<()> { + self.skymap_list.update(par.layout, par.executor)?; + Ok(()) + } +} + +impl State { + pub fn mount(par: SettingsMountParams) -> anyhow::Result { + let c = options_category(par.mp, par.parent_id, "APP_SETTINGS.SKYBOX", "dashboard/globe.svg")?; + options_checkbox(par.mp, c, SettingType::UseSkybox)?; + options_checkbox(par.mp, c, SettingType::OpaqueBackground)?; + + let skymap_list = skymap_list::View::new(skymap_list::Params { + globals: par.globals.clone(), + layout: par.mp.layout, + parent_id: c, + })?; + + Ok(Self { skymap_list }) + } } diff --git a/dash-frontend/src/util/http_client.rs b/dash-frontend/src/util/http_client.rs index 875d509e..69a018f2 100644 --- a/dash-frontend/src/util/http_client.rs +++ b/dash-frontend/src/util/http_client.rs @@ -18,6 +18,16 @@ pub struct HttpClientResponse { pub data: Vec, } +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)?) + } +} + pub async fn get(executor: &AsyncExecutor, url: &str) -> anyhow::Result { log::info!("fetching URL \"{}\"", url); diff --git a/dash-frontend/src/util/mod.rs b/dash-frontend/src/util/mod.rs index e5290607..1bd01c98 100644 --- a/dash-frontend/src/util/mod.rs +++ b/dash-frontend/src/util/mod.rs @@ -1,5 +1,6 @@ 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/networking/mod.rs b/dash-frontend/src/util/networking/mod.rs new file mode 100644 index 00000000..5f6c078e --- /dev/null +++ b/dash-frontend/src/util/networking/mod.rs @@ -0,0 +1,4 @@ +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..498c12f9 --- /dev/null +++ b/dash-frontend/src/util/networking/skymap_catalog.rs @@ -0,0 +1,42 @@ +#![allow(dead_code)] // TODO: Remove later +use serde::Deserialize; +use wlx_common::async_executor::AsyncExecutor; + +use crate::util::{http_client, networking}; + +#[derive(Clone, Debug, Deserialize)] +pub struct SkymapCatalogEntryFiles { + pub size_16k: Option, // "my_skymap_16k.png" + pub size_8k: Option, // "my_skymap_8k.png" + pub size_4k: Option, // "my_skymap_4k.png" + pub size_2k: String, // we should have *at least* this + pub preview: String, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct SkymapCatalogEntry { + pub uuid: String, + pub created_at: String, + pub modified_at: String, + pub entry_version: u32, + pub name: String, + pub description: String, + pub author: String, + pub files: SkymapCatalogEntryFiles, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct SkymapCatalog { + pub version: u32, + pub r#type: String, + pub entries: Vec, +} + +pub async fn request_catalog(executor: &AsyncExecutor) -> anyhow::Result { + log::info!("Fetching skymap list"); + + let res = http_client::get(executor, &format!("{}/catalog.json", networking::WAYVR_SKYMAPS_ROOT)).await?; + let catalog = res.as_json::()?; + + Ok(catalog) +} diff --git a/dash-frontend/src/views/mod.rs b/dash-frontend/src/views/mod.rs index d2cd9369..2f832208 100644 --- a/dash-frontend/src/views/mod.rs +++ b/dash-frontend/src/views/mod.rs @@ -4,3 +4,4 @@ pub mod game_cover; pub mod game_launcher; pub mod game_list; pub mod running_games_list; +pub mod skymap_list; diff --git a/dash-frontend/src/views/skymap_list.rs b/dash-frontend/src/views/skymap_list.rs new file mode 100644 index 00000000..9a968f75 --- /dev/null +++ b/dash-frontend/src/views/skymap_list.rs @@ -0,0 +1,117 @@ +use wgui::{ + assets::AssetPath, + components::button::ComponentButton, + globals::WguiGlobals, + i18n::Translation, + layout::{Layout, WidgetID}, + parser::{Fetchable, ParseDocumentParams, ParserState}, + task::Tasks, +}; +use wlx_common::{async_executor::AsyncExecutor, config_io}; + +use crate::util::{networking, wgui_simple}; + +#[derive(Clone)] +enum Task { + DownloadSkymaps, + SetSkymapCatalog(Result), + Refresh, +} + +pub struct Params<'a> { + pub globals: WguiGlobals, + pub layout: &'a mut Layout, + pub parent_id: WidgetID, +} + +pub struct View { + #[allow(dead_code)] + parser_state: ParserState, + tasks: Tasks, + list_parent: WidgetID, +} + +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, + }) + } + + pub fn update(&mut self, layout: &mut Layout, executor: &AsyncExecutor) -> anyhow::Result<()> { + loop { + let tasks = self.tasks.drain(); + if tasks.is_empty() { + break; + } + for task in tasks { + match task { + Task::DownloadSkymaps => { + self.download_skymaps(executor)?; + } + Task::Refresh => { + self.refresh(layout)?; + } + Task::SetSkymapCatalog(skymap_catalog) => { + log::info!("{:?}", skymap_catalog); + } + } + } + } + + Ok(()) + } + + async fn skymap_catalog_request_wrapper(tasks: Tasks, executor: AsyncExecutor) { + let res = networking::skymap_catalog::request_catalog(&executor).await; + tasks.push(Task::SetSkymapCatalog(res.map_err(|e| format!("{}", e)))); + } + + fn download_skymaps(&mut self, executor: &AsyncExecutor) -> anyhow::Result<()> { + let fut = View::skymap_catalog_request_wrapper(self.tasks.clone(), executor.clone()); + executor.spawn(fut).detach(); + Ok(()) + } + + fn refresh(&mut self, layout: &mut Layout) -> anyhow::Result<()> { + let skymaps_uuids = config_io::get_skymaps_uuids().unwrap_or_default(); + log::info!("skymap uuids {:?}", skymaps_uuids); + + layout.remove_children(self.list_parent); + + if skymaps_uuids.is_empty() { + wgui_simple::create_label( + layout, + self.list_parent, + Translation::from_translation_key("APP_SETTINGS.NO_SKYMAPS_FOUND"), + )?; + return Ok(()); + } + + 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 { From a3460fbe7055f1ef2b3f927fee44283018c62a6c Mon Sep 17 00:00:00 2001 From: Aleksander Date: Sat, 4 Apr 2026 22:08:44 +0200 Subject: [PATCH 03/36] dash-frontend: use SettingsTab trait for each settings tab --- dash-frontend/src/tab/settings/mod.rs | 27 +++--- .../src/tab/settings/tab_autostart_apps.rs | 35 ++++--- .../src/tab/settings/tab_controls.rs | 48 ++++++---- .../src/tab/settings/tab_features.rs | 37 ++++---- .../src/tab/settings/tab_look_and_feel.rs | 42 +++++---- dash-frontend/src/tab/settings/tab_misc.rs | 27 +++--- .../src/tab/settings/tab_troubleshooting.rs | 92 ++++++++++--------- 7 files changed, 183 insertions(+), 125 deletions(-) diff --git a/dash-frontend/src/tab/settings/mod.rs b/dash-frontend/src/tab/settings/mod.rs index 1367f193..3194e11e 100644 --- a/dash-frontend/src/tab/settings/mod.rs +++ b/dash-frontend/src/tab/settings/mod.rs @@ -546,31 +546,36 @@ impl TabSettings { idx: 9001, }; + let settings_mount_params = SettingsMountParams { + mp: &mut mp, + globals: &globals, + parent_id: root, + }; + 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(SettingsMountParams { - mp: &mut mp, - globals: &globals, - parent_id: root, - })?)); + self.current_tab = Some(Box::new(tab_skybox::State::mount(settings_mount_params)?)); } } diff --git a/dash-frontend/src/tab/settings/tab_autostart_apps.rs b/dash-frontend/src/tab/settings/tab_autostart_apps.rs index 6483fc94..79496d4e 100644 --- a/dash-frontend/src/tab/settings/tab_autostart_apps.rs +++ b/dash-frontend/src/tab/settings/tab_autostart_apps.rs @@ -1,19 +1,32 @@ 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.parent_id, + "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..a8ba00f3 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.parent_id, + "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 264a40fd..a01abdc4 100644 --- a/dash-frontend/src/tab/settings/tab_features.rs +++ b/dash-frontend/src/tab/settings/tab_features.rs @@ -1,20 +1,25 @@ use crate::tab::settings::{ - SettingType, - macros::{MacroParams, options_category, options_checkbox, options_slider_f32}, + SettingType, SettingsMountParams, SettingsTab, + macros::{options_category, options_checkbox, 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.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::KeyboardSwipeToTypeEnabled)?; - 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.parent_id, "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::KeyboardSwipeToTypeEnabled)?; + 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 ad2d1a5a..55927880 100644 --- a/dash-frontend/src/tab/settings/tab_look_and_feel.rs +++ b/dash-frontend/src/tab/settings/tab_look_and_feel.rs @@ -1,20 +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::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_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.parent_id, + "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..36b3e1b9 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.parent_id, "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_troubleshooting.rs b/dash-frontend/src/tab/settings/tab_troubleshooting.rs index 9e16fd4b..83a822c3 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.parent_id, + "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 {}) + } } From f2dcdcc6c398e1d64f0e611fbed2d368763edd5e Mon Sep 17 00:00:00 2001 From: Aleksander Date: Mon, 6 Apr 2026 13:22:59 +0200 Subject: [PATCH 04/36] dash-frontend: list skymaps from the server (wip), loading spinner, wgui: animation bugfix --- dash-frontend/assets/dashboard/loading.svg | 1 + dash-frontend/assets/gui/t_loading.xml | 16 ++ .../assets/gui/view/download_skymaps.xml | 7 + dash-frontend/assets/gui/view/skymap_list.xml | 2 +- .../assets/gui/view/skymap_list_cell.xml | 26 ++++ dash-frontend/assets/lang/en.json | 1 + dash-frontend/src/frontend.rs | 23 ++- dash-frontend/src/tab/apps.rs | 49 ++---- dash-frontend/src/tab/settings/mod.rs | 6 +- dash-frontend/src/tab/settings/tab_skybox.rs | 1 + .../src/util/networking/skymap_catalog.rs | 20 ++- dash-frontend/src/util/popup_manager.rs | 78 ++++++++-- dash-frontend/src/util/wgui_simple.rs | 83 ++++++++++- dash-frontend/src/views/app_launcher.rs | 33 ++++- dash-frontend/src/views/download_skymaps.rs | 140 ++++++++++++++++++ dash-frontend/src/views/game_cover.rs | 11 ++ dash-frontend/src/views/game_launcher.rs | 30 ++++ dash-frontend/src/views/game_list.rs | 47 ++---- dash-frontend/src/views/mod.rs | 2 + dash-frontend/src/views/skymap_list.rs | 55 +++++-- dash-frontend/src/views/skymap_list_cell.rs | 56 +++++++ wgui/src/animation.rs | 8 +- wgui/src/task.rs | 9 +- 23 files changed, 594 insertions(+), 110 deletions(-) create mode 100644 dash-frontend/assets/dashboard/loading.svg create mode 100644 dash-frontend/assets/gui/t_loading.xml create mode 100644 dash-frontend/assets/gui/view/download_skymaps.xml create mode 100644 dash-frontend/assets/gui/view/skymap_list_cell.xml create mode 100644 dash-frontend/src/views/download_skymaps.rs create mode 100644 dash-frontend/src/views/skymap_list_cell.rs 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/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/view/download_skymaps.xml b/dash-frontend/assets/gui/view/download_skymaps.xml new file mode 100644 index 00000000..39c2711e --- /dev/null +++ b/dash-frontend/assets/gui/view/download_skymaps.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 index 00465088..c273f753 100644 --- a/dash-frontend/assets/gui/view/skymap_list.xml +++ b/dash-frontend/assets/gui/view/skymap_list.xml @@ -1,7 +1,7 @@
-
+
- - - @@ -243,10 +231,7 @@
-
- - -
+
diff --git a/wayvr/src/backend/wayvr/client.rs b/wayvr/src/backend/wayvr/client.rs index 73a2f944..862fcc5d 100644 --- a/wayvr/src/backend/wayvr/client.rs +++ b/wayvr/src/backend/wayvr/client.rs @@ -1,8 +1,6 @@ use std::{io::Read, os::unix::net::UnixStream, path::PathBuf, sync::Arc}; use anyhow::Context; -use smithay::input::Seat; -use smithay::wayland::selection::data_device::set_data_device_selection; use smithay::{ backend::input::Keycode, input::{keyboard::KeyboardHandle, pointer::PointerHandle}, @@ -11,13 +9,13 @@ use smithay::{ }; use xkbcommon::xkb; +use crate::backend::wayvr::{ExternalProcessRequest, WayVRTask}; + use super::{ ProcessWayVREnv, comp::{self, ClientState}, process, }; -use crate::backend::wayvr::comp::Application; -use crate::backend::wayvr::{ExternalProcessRequest, WayVRTask}; pub struct WayVRClient { pub client: wayland_server::Client, @@ -28,7 +26,6 @@ pub struct WayVRCompositor { pub state: comp::Application, pub seat_keyboard: KeyboardHandle, pub seat_pointer: PointerHandle, - pub seat: Seat, pub serial_counter: SerialCounter, pub wayland_env: super::WaylandEnv, @@ -71,7 +68,6 @@ impl WayVRCompositor { display: wayland_server::Display, seat_keyboard: KeyboardHandle, seat_pointer: PointerHandle, - seat: Seat, ) -> anyhow::Result { let (wayland_env, listener) = create_wayland_listener()?; @@ -85,7 +81,6 @@ impl WayVRCompositor { serial_counter: SerialCounter::new(), clients: Vec::new(), toplevel_surf_count: 0, - seat, }) } @@ -205,17 +200,6 @@ impl WayVRCompositor { ); } - pub fn set_clipboard_text(&mut self, text: String) { - set_data_device_selection::( - &self.state.display_handle, - &self.seat, - vec![ - "text/plain;charset=utf-8".to_string(), - "text/plain".to_string(), - ], - text.as_bytes().into(), - ); - } pub fn set_keymap(&mut self, keymap: &xkb::Keymap) -> anyhow::Result<()> { // Smithay only accepts keymaps in a string form due to thread safety concerns self.seat_keyboard diff --git a/wayvr/src/backend/wayvr/mod.rs b/wayvr/src/backend/wayvr/mod.rs index 74674949..5d83f02b 100644 --- a/wayvr/src/backend/wayvr/mod.rs +++ b/wayvr/src/backend/wayvr/mod.rs @@ -241,13 +241,7 @@ impl WvrServerState { }; Ok(Self { - manager: client::WayVRCompositor::new( - state, - display, - seat_keyboard, - seat_pointer, - seat, - )?, + manager: client::WayVRCompositor::new(state, display, seat_keyboard, seat_pointer)?, processes: ProcessVec::new(), wm: window::WindowManager::new(), ticks: 0, @@ -614,9 +608,6 @@ impl WvrServerState { pub fn send_key(&mut self, virtual_key: u32, down: bool) { self.manager.send_key(virtual_key, down); } - pub fn set_clipboard_text(&mut self, text: String) { - self.manager.set_clipboard_text(text); - } pub fn set_keymap(&mut self, keymap: &xkb::Keymap) -> anyhow::Result<()> { self.manager.set_keymap(keymap) diff --git a/wayvr/src/config.rs b/wayvr/src/config.rs index 676dda24..9c89a629 100644 --- a/wayvr/src/config.rs +++ b/wayvr/src/config.rs @@ -140,7 +140,6 @@ pub struct AutoSettings { pub context_menu_hold_and_release: bool, pub capture_method: CaptureMethod, pub keyboard_middle_click_mode: AltModifier, - pub keyboard_swipe_to_type_enabled: bool, pub autostart_apps: Vec, pub handsfree_pointer: HandsfreePointer, pub language: Option, @@ -193,7 +192,6 @@ pub fn save_settings(config: &GeneralConfig) -> anyhow::Result<()> { context_menu_hold_and_release: config.context_menu_hold_and_release, capture_method: config.capture_method, keyboard_middle_click_mode: config.keyboard_middle_click_mode, - keyboard_swipe_to_type_enabled: config.keyboard_swipe_to_type_enabled, autostart_apps: config.autostart_apps.clone(), handsfree_pointer: config.handsfree_pointer, language: config.language, diff --git a/wayvr/src/main.rs b/wayvr/src/main.rs index 7a49f02e..6649b633 100644 --- a/wayvr/src/main.rs +++ b/wayvr/src/main.rs @@ -285,7 +285,6 @@ fn logging_init(args: &mut Args) { .from_env_lossy() .add_directive("symphonia_core::probe=warn".parse().unwrap()) .add_directive("symphonia_bundle_mp3=warn".parse().unwrap()) - .add_directive("ort=warn".parse().unwrap()) .add_directive("zbus=warn".parse().unwrap()) .add_directive("usvg=error".parse().unwrap()) .add_directive("resvg=error".parse().unwrap()) diff --git a/wayvr/src/overlays/keyboard/builder.rs b/wayvr/src/overlays/keyboard/builder.rs index ec72e09c..b96b0368 100644 --- a/wayvr/src/overlays/keyboard/builder.rs +++ b/wayvr/src/overlays/keyboard/builder.rs @@ -1,8 +1,5 @@ -use super::{ - KeyButtonData, KeyState, KeyboardState, handle_mouse_motion, handle_press, handle_release, - init_swipe_type_manager, - layout::{self, KeyCapType}, -}; +use std::{collections::HashMap, rc::Rc, time::Duration}; + use crate::{ app_misc, gui::{ @@ -14,13 +11,8 @@ use crate::{ subsystem::hid::XkbKeymap, windowing::backend::OverlayEventData, }; -use anyhow::{Context, bail}; +use anyhow::Context; use glam::{FloatExt, Mat4, Vec2, vec2, vec3}; -use slotmap::Key; -use std::{collections::HashMap, rc::Rc, time::Duration}; -use wgui::event::StyleSetRequest; -use wgui::layout::LayoutTask; -use wgui::taffy::Display; use wgui::{ animation::{Animation, AnimationEasing}, assets::AssetPath, @@ -34,6 +26,11 @@ use wgui::{ widget::{EventResult, div::WidgetDiv, rectangle::WidgetRectangle}, }; +use super::{ + KeyButtonData, KeyState, KeyboardState, handle_press, handle_release, + layout::{self, KeyCapType}, +}; + const PIXELS_PER_UNIT: f32 = 60.; fn new_doc_params(panel: &mut GuiPanel) -> ParseDocumentParams<'static> { @@ -44,134 +41,6 @@ fn new_doc_params(panel: &mut GuiPanel) -> ParseDocumentParams<'s } } -pub(super) fn update_swipe_prediction_bar( - panel: &mut GuiPanel, - app: &mut AppState, -) -> anyhow::Result { - let mut elements_changed = false; - - let (accent_color, anim_mult) = { - let theme = &app.wgui_theme; - (theme.accent_color, theme.animation_mult) - }; - - if let Some(recv) = panel.state.swipe_candidate_receiver.as_mut() - && let Ok(candidates) = recv.try_recv() - { - let predictions_root = panel - .parser_state - .get_widget_id("swipe_predictions_root") - .unwrap_or_default(); - - if predictions_root.is_null() { - return Ok(elements_changed); - } - let doc_params = new_doc_params(panel); - - panel.layout.remove_children(predictions_root); - - let Some(new_suggestions) = candidates else { - return Ok(elements_changed); - }; - - let mut iter = new_suggestions.iter(); - let Some(best_prediction) = iter.next() else { - bail!("not enough swipe predictions"); - }; - if let Some(manager) = panel.state.swipe_typing_manager.as_mut() { - manager.select_word(best_prediction, app, panel.state.modifiers); - } - for (i, prediction) in iter.enumerate() { - let mut params = HashMap::new(); - let id: Rc = Rc::from(format!("Prediction-{i}")); - params.insert("id".into(), id.clone()); - params.insert("text".into(), prediction.clone().into()); - - panel.parser_state.instantiate_template( - &doc_params, - "KeyPrediction", - &mut panel.layout, - predictions_root, - params, - )?; - - if let Ok(widget_id) = panel.parser_state.get_widget_id(&id) { - let key_state = { - let rect = panel - .layout - .state - .widgets - .get_as::(widget_id) - .unwrap(); // want panic - - Rc::new(KeyState { - // fake button state just so we get key state for anims - button_state: KeyButtonData::Modifier { - modifier: 0, - sticky: core::cell::Cell::new(false), - }, - color: rect.params.color, - color2: rect.params.color2, - base_border_color: rect.params.border_color, - cur_border_color: rect.params.border_color.into(), - border: rect.params.border, - drawn_state: false.into(), - }) - }; - panel.add_event_listener( - widget_id, - EventListenerKind::MousePress, - Box::new({ - let k = key_state.clone(); - let pred = prediction.clone(); - move |common, data, app, state| { - if let Some(manager) = state.swipe_typing_manager.as_mut() { - manager.select_alternate_prediction(&pred, app, state.modifiers); - on_press_anim(k.clone(), common, data) - } - Ok(EventResult::Pass) - } - }), - ); - panel.add_event_listener( - widget_id, - EventListenerKind::MouseEnter, - Box::new({ - let k = key_state.clone(); - move |common, data, _app, _state| { - on_enter_anim(k.clone(), common, data, accent_color, anim_mult, 0.0); - Ok(EventResult::Pass) - } - }), - ); - panel.add_event_listener( - widget_id, - EventListenerKind::MouseLeave, - Box::new({ - let k = key_state.clone(); - move |common, data, _app, _state| { - on_leave_anim(k.clone(), common, data, accent_color, anim_mult, 0.0); - Ok(EventResult::Pass) - } - }), - ); - panel.add_event_listener( - widget_id, - EventListenerKind::MouseRelease, - Box::new({ - let k = key_state.clone(); - move |common, data, _app, _state| { - on_release_anim(k.clone(), common, data); - Ok(EventResult::Pass) - } - }), - ); - } - } - elements_changed = true; - } - Ok(elements_changed) -} #[allow(clippy::too_many_lines, clippy::significant_drop_tightening)] pub(super) fn create_keyboard_panel( app: &mut AppState, @@ -244,7 +113,7 @@ pub(super) fn create_keyboard_panel( params.insert(Rc::from("width"), Rc::from(key_width.to_string())); params.insert(Rc::from("height"), Rc::from(key_height.to_string())); - let mut label = key.label.clone().into_iter(); + let mut label = key.label.into_iter(); label .next() .and_then(|s| params.insert("text".into(), s.into())); @@ -300,9 +169,6 @@ pub(super) fn create_keyboard_panel( }) }; - let key_cap_type: Rc = Rc::from(key.cap_type); - let key_label: Rc> = Rc::from(key.label); - let width_mul = 1. / my_size_f32; panel.add_event_listener( @@ -320,7 +186,6 @@ pub(super) fn create_keyboard_panel( anim_mult, width_mul, ); - Ok(EventResult::Pass) } }), @@ -340,7 +205,6 @@ pub(super) fn create_keyboard_panel( anim_mult, width_mul, ); - Ok(EventResult::Pass) } }), @@ -350,69 +214,26 @@ pub(super) fn create_keyboard_panel( EventListenerKind::MousePress, Box::new({ let k = key_state.clone(); - let k_label = key_label.clone(); - let k_cap_type = key_cap_type.clone(); move |common, data, app, state| { let CallbackMetadata::MouseButton(button) = data.metadata else { panic!("CallbackMetadata should contain MouseButton!"); }; - let within_key_pos = data - .metadata - .get_mouse_pos_normalized(&common.alterables.transform_stack); - handle_press( - app, - &k, - &k_label, - &k_cap_type, - &within_key_pos, - state, - button, - button.device, - ); + handle_press(app, &k, state, button); on_press_anim(k.clone(), common, data); Ok(EventResult::Pass) } }), ); - panel.add_event_listener( - widget_id, - EventListenerKind::MouseMotion, - Box::new({ - let k = key_state.clone(); - let k_label = key_label.clone(); - let k_cap_type = key_cap_type.clone(); - move |common, data, _app, state| { - let within_key_pos = data - .metadata - .get_mouse_pos_normalized(&common.alterables.transform_stack); - let CallbackMetadata::MousePosition(position) = data.metadata else { - panic!("CallbackMetadata should contain MousePosition!"); - }; - - handle_mouse_motion( - &k, - &k_label, - &k_cap_type, - state, - &within_key_pos, - position.device, - ); - Ok(EventResult::Pass) - } - }), - ); panel.add_event_listener( widget_id, EventListenerKind::MouseRelease, Box::new({ let k = key_state.clone(); - let k_cap_type = key_cap_type.clone(); move |common, data, app, state| { - if handle_release(app, &k, &k_cap_type, state) { + if handle_release(app, &k, state) { on_release_anim(k.clone(), common, data); } - Ok(EventResult::Pass) } }), @@ -486,41 +307,6 @@ pub(super) fn create_keyboard_panel( elems_changed = true; } } - if !app.session.config.keyboard_swipe_to_type_enabled { - panel.state.swipe_typing_manager = None; - panel.state.swipe_candidate_receiver = None; - - let predictions_root = panel - .parser_state - .get_widget_id("swipe_predictions_root") - .unwrap_or_default(); - - if !predictions_root.is_null() { - panel.layout.remove_children(predictions_root); - - panel.layout.tasks.push(LayoutTask::SetWidgetStyle( - predictions_root, - StyleSetRequest::Display(Display::None), - )); - } - } - if app.session.config.keyboard_swipe_to_type_enabled - && panel.state.swipe_typing_manager.is_none() - { - init_swipe_type_manager(&mut panel.state); - - let predictions_root = panel - .parser_state - .get_widget_id("swipe_predictions_root") - .unwrap_or_default(); - - if !predictions_root.is_null() { - panel.layout.tasks.push(LayoutTask::SetWidgetStyle( - predictions_root, - StyleSetRequest::Display(Display::Flex), - )); - } - } } OverlayEventData::CustomCommand { element, command } => { diff --git a/wayvr/src/overlays/keyboard/layout.rs b/wayvr/src/overlays/keyboard/layout.rs index 87cfa8c1..2af0590f 100644 --- a/wayvr/src/overlays/keyboard/layout.rs +++ b/wayvr/src/overlays/keyboard/layout.rs @@ -1,13 +1,14 @@ use std::{collections::HashMap, str::FromStr, sync::LazyLock}; +use regex::Regex; +use serde::{Deserialize, Serialize}; + use crate::{ config::{ConfigType, load_known_yaml}, subsystem::hid::{ KEYS_TO_MODS, KeyType, META, NUM_LOCK, SHIFT, VirtualKey, XkbKeymap, get_key_type, }, }; -use regex::Regex; -use serde::{Deserialize, Serialize}; use super::KeyButtonData; @@ -229,7 +230,7 @@ pub(super) struct KeyData { pub(super) cap_type: KeyCapType, } -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug)] pub enum KeyCapType { /// Label an SVG Special, diff --git a/wayvr/src/overlays/keyboard/mod.rs b/wayvr/src/overlays/keyboard/mod.rs index dc432395..966f5baa 100644 --- a/wayvr/src/overlays/keyboard/mod.rs +++ b/wayvr/src/overlays/keyboard/mod.rs @@ -1,6 +1,10 @@ -use crate::overlays::keyboard::builder::update_swipe_prediction_bar; -use crate::overlays::keyboard::layout::KeyCapType; -use crate::overlays::keyboard::swipe_type::SwipeTypingManager; +use std::{ + cell::Cell, + collections::HashMap, + process::{Child, Command}, + sync::atomic::Ordering, +}; + use crate::{ KEYMAP_CHANGE, backend::{ @@ -23,20 +27,9 @@ use crate::{ }, }; use anyhow::Context; -use glam::{Affine3A, Quat, Vec2, Vec3, vec3}; +use glam::{Affine3A, Quat, Vec3, vec3}; use regex::Regex; -use slotmap::{Key, SlotMap, new_key_type}; -use std::sync::mpsc::Receiver; -use std::{ - cell::Cell, - collections::HashMap, - process::{Child, Command}, - sync::atomic::Ordering, -}; -use wgui::event::StyleSetRequest; -use wgui::layout::LayoutTask; -use wgui::parser::Fetchable; -use wgui::taffy::Display; +use slotmap::{SlotMap, new_key_type}; use wgui::{ drawing, event::{InternalStateChangeEvent, MouseButtonEvent, MouseButtonIndex}, @@ -49,7 +42,6 @@ use wlx_common::{ pub mod builder; mod layout; -mod swipe_type; pub const KEYBOARD_NAME: &str = "kbd"; const AUTO_RELEASE_MODS: [KeyModifier; 5] = [SHIFT, CTRL, ALT, SUPER, META]; @@ -64,8 +56,6 @@ pub fn create_keyboard(app: &mut AppState, wayland: bool) -> anyhow::Result anyhow::Result { - state.swipe_typing_manager = Some(engine); - state.swipe_candidate_receiver = Some(receiver); - } - Err(e) => { - log::error!("Error occurred while trying to load swipe engine: {}", e); - } - }; -} -pub(self) fn hide_swipe_type_manager(panel: &mut GuiPanel) { - let predictions_root = panel - .parser_state - .get_widget_id("swipe_predictions_root") - .unwrap_or_default(); - if !predictions_root.is_null() { - panel.layout.tasks.push(LayoutTask::SetWidgetStyle( - predictions_root, - StyleSetRequest::Display(Display::None), - )); - } -} + const fn alt_modifier_to_key(m: AltModifier) -> KeyModifier { match m { AltModifier::Shift => SHIFT, @@ -182,18 +150,8 @@ impl KeyboardBackend { keymap: Option<&XkbKeymap>, app: &mut AppState, ) -> anyhow::Result { - let mut state = self.default_state.take(); - - if app.session.config.keyboard_swipe_to_type_enabled { - init_swipe_type_manager(&mut state); - } - - log::info!("swipe engine created"); - let mut panel = create_keyboard_panel(app, keymap, state, &self.wlx_layout)?; - - if !app.session.config.keyboard_swipe_to_type_enabled { - hide_swipe_type_manager(&mut panel); - } + let panel = + create_keyboard_panel(app, keymap, self.default_state.take(), &self.wlx_layout)?; let id = self.layout_panels.insert(panel); if let Some(layout_name) = keymap.and_then(|k| k.get_name()) { @@ -218,38 +176,30 @@ impl KeyboardBackend { if self.active_layout.eq(new_key) { return Ok(false); } - self.internal_switch_keymap(*new_key, app); + self.internal_switch_keymap(*new_key); } else { let new_key = self.add_new_keymap(Some(keymap), app)?; - self.internal_switch_keymap(new_key, app); + self.internal_switch_keymap(new_key); } app.tasks .enqueue(TaskType::Overlay(OverlayTask::KeyboardChanged)); Ok(true) } - fn internal_switch_keymap(&mut self, new_key: KeyboardPanelKey, app: &AppState) { - let mut state_from = self + fn internal_switch_keymap(&mut self, new_key: KeyboardPanelKey) { + let state_from = self .layout_panels .get_mut(self.active_layout) .unwrap() .state .take(); - if app.session.config.keyboard_swipe_to_type_enabled { - init_swipe_type_manager(&mut state_from); - } - self.active_layout = new_key; self.layout_panels .get_mut(self.active_layout) .unwrap() .state = state_from; - - if !app.session.config.keyboard_swipe_to_type_enabled { - hide_swipe_type_manager(self.layout_panels.get_mut(self.active_layout).unwrap()) - } } fn get_effective_keymap(&mut self) -> anyhow::Result { @@ -283,13 +233,6 @@ impl KeyboardBackend { } } - fn update_swipe_prediction_bar(&mut self, app: &mut AppState) -> anyhow::Result<()> { - if update_swipe_prediction_bar(self.panel(), app)? { - self.panel().process_custom_elems(app); - } - Ok(()) - } - fn auto_switch_keymap(&mut self, app: &mut AppState) -> anyhow::Result { let keymap = self.get_effective_keymap()?; app.hid_provider @@ -323,7 +266,6 @@ impl OverlayBackend for KeyboardBackend { }); } } - self.update_swipe_prediction_bar(app)?; self.panel().should_render(app) } fn render(&mut self, app: &mut AppState, rdr: &mut RenderResources) -> anyhow::Result<()> { @@ -385,8 +327,6 @@ struct KeyboardState { overlay_list: OverlayList, set_list: SetList, clock_12h: bool, - swipe_typing_manager: Option, - swipe_candidate_receiver: Option>>>, } macro_rules! take_and_leave_default { @@ -406,8 +346,6 @@ impl KeyboardState { overlay_list: OverlayList::default(), set_list: SetList::default(), clock_12h: self.clock_12h, - swipe_typing_manager: None, - swipe_candidate_receiver: None, } } } @@ -448,75 +386,26 @@ enum KeyButtonData { }, } -fn handle_mouse_motion( - key: &KeyState, - key_label: &Vec, - key_cap_type: &KeyCapType, - keyboard: &mut KeyboardState, - within_key_pos: &Option, - device: usize, -) { - if let Some(swipe_manager) = keyboard.swipe_typing_manager.as_mut() - && matches!(*key_cap_type, KeyCapType::Letter | KeyCapType::LetterAltGr) - { - if !swipe_manager.is_current_swipe_empty() { - match &key.button_state { - KeyButtonData::Key { vk, pressed } => { - if let Some(pos) = within_key_pos { - // check because mouse motion can trigger despite hover being false - if pos.x >= 0.0 && pos.x <= 1.0 && pos.y >= 0.0 && pos.y <= 1.0 { - if let Some(label) = key_label.first() { - swipe_manager.add_swipe( - pos, - label.chars().next().unwrap_or_default(), - device, - ); - } - } - } - } - _ => {} - } - } - } -} fn handle_press( app: &mut AppState, key: &KeyState, - key_label: &Vec, - key_cap_type: &KeyCapType, - within_key_pos: &Option, keyboard: &mut KeyboardState, button: MouseButtonEvent, - device: usize, ) { match &key.button_state { KeyButtonData::Key { vk, pressed } => { - if let Some(swipe_manager) = keyboard.swipe_typing_manager.as_mut() - && matches!(*key_cap_type, KeyCapType::Letter | KeyCapType::LetterAltGr) - { - if let Some(pos) = within_key_pos { - if let Some(label) = key_label.first() { - swipe_manager.add_swipe( - pos, - label.chars().next().unwrap_or_default(), - device, - ); - } - } - } else { - keyboard.modifiers |= match button.index { - MouseButtonIndex::Right => SHIFT, - MouseButtonIndex::Middle => keyboard.alt_modifier, - _ => 0, - }; - app.hid_provider - .set_modifiers_routed(app.wvr_server.as_mut(), keyboard.modifiers); - app.hid_provider - .send_key_routed(app.wvr_server.as_mut(), *vk, true); - pressed.set(true); - play_key_click(app); - } + keyboard.modifiers |= match button.index { + MouseButtonIndex::Right => SHIFT, + MouseButtonIndex::Middle => keyboard.alt_modifier, + _ => 0, + }; + + app.hid_provider + .set_modifiers_routed(app.wvr_server.as_mut(), keyboard.modifiers); + app.hid_provider + .send_key_routed(app.wvr_server.as_mut(), *vk, true); + pressed.set(true); + play_key_click(app); } KeyButtonData::Modifier { modifier, sticky } => { sticky.set(keyboard.modifiers & *modifier == 0); @@ -546,51 +435,20 @@ fn handle_press( } } -fn handle_release( - app: &mut AppState, - key: &KeyState, - k_cap_type: &KeyCapType, - keyboard: &mut KeyboardState, -) -> bool { +fn handle_release(app: &mut AppState, key: &KeyState, keyboard: &mut KeyboardState) -> bool { match &key.button_state { KeyButtonData::Key { vk, pressed } => { - if let Some(swipe_manager) = keyboard.swipe_typing_manager.as_mut() - && matches!(*k_cap_type, KeyCapType::Letter | KeyCapType::LetterAltGr) - { - if swipe_manager.did_swipe_leave_first_key() { - match swipe_manager.predict() { - Ok(()) => {} - Err(e) => { - log::error!("{}", e) - } - } - } else { - // pointer must have been released on the same key it was pressed on - swipe_manager.reset(); // drop swipe tracking that was started on press + pressed.set(false); - app.hid_provider - .send_key_routed(app.wvr_server.as_mut(), *vk, true); - pressed.set(true); - app.hid_provider - .send_key_routed(app.wvr_server.as_mut(), *vk, false); - play_key_click(app); + for m in &AUTO_RELEASE_MODS { + if keyboard.modifiers & *m != 0 { + keyboard.modifiers &= !*m; } - } else { - if let Some(swipe_manager) = keyboard.swipe_typing_manager.as_mut() { - swipe_manager.reset(); - } - pressed.set(false); - - for m in &AUTO_RELEASE_MODS { - if keyboard.modifiers & *m != 0 { - keyboard.modifiers &= !*m; - } - } - app.hid_provider - .send_key_routed(app.wvr_server.as_mut(), *vk, false); - app.hid_provider - .set_modifiers_routed(app.wvr_server.as_mut(), keyboard.modifiers); } + app.hid_provider + .send_key_routed(app.wvr_server.as_mut(), *vk, false); + app.hid_provider + .set_modifiers_routed(app.wvr_server.as_mut(), keyboard.modifiers); true } KeyButtonData::Modifier { modifier, sticky } => { diff --git a/wayvr/src/overlays/keyboard/swipe_type.rs b/wayvr/src/overlays/keyboard/swipe_type.rs deleted file mode 100644 index d4e98e75..00000000 --- a/wayvr/src/overlays/keyboard/swipe_type.rs +++ /dev/null @@ -1,250 +0,0 @@ -use crate::state::AppState; -use crate::subsystem::hid::{CTRL, KeyModifier, VirtualKey}; -use crate::subsystem::input::KeyboardFocus; -use anyhow::bail; -use arboard::Clipboard; -use glam::Vec2; -use std::mem; -use std::sync::mpsc::{Receiver, Sender, SyncSender, channel, sync_channel}; -use std::thread::{self, JoinHandle}; -use std::time::Instant; -use super_swipe_type::SwipePoint; -use super_swipe_type::keyboard_manager::QwertyKeyboardGrid; -use super_swipe_type::swipe_orchestrator::SwipeOrchestrator; - -const PREDICTION_SUGGESTION_COUNT: usize = 5; - -enum PredictionTask { - Predict { - swipe: Vec, - last_word: Option, - }, - Shutdown, -} - -pub struct SwipeTypingManager { - keyboard_gird: QwertyKeyboardGrid, - current_swipe: Vec, - swipe_candidate_sender: SyncSender>>, - prediction_task_sender: Sender, - worker_thread: Option>, - swipe_start_time: Option, - clipboard: Clipboard, - swipe_left_first_key: bool, - first_swipe_char: char, - current_swipe_device: Option, - last_swiped_word: Option, -} - -impl SwipeTypingManager { - pub fn select_alternate_prediction( - &mut self, - word: &String, - app: &mut AppState, - original_keyboard_mods: KeyModifier, - ) { - Self::undo(app, original_keyboard_mods); - self.select_word(word, app, original_keyboard_mods); - } - - pub fn select_word( - &mut self, - word: &String, - app: &mut AppState, - original_keyboard_mods: KeyModifier, - ) { - self.last_swiped_word = Some(word.clone()); - let text_to_paste = format!("{word} "); - - match app.hid_provider.keyboard_focus { - KeyboardFocus::PhysicalScreen => { - if let Ok(_) = self.clipboard.set_text(text_to_paste) { - Self::paste(app, original_keyboard_mods); - } - } - KeyboardFocus::WayVR => { - if let Some(wvr_server) = app.wvr_server.as_mut() { - wvr_server.set_clipboard_text(text_to_paste); - Self::paste(app, original_keyboard_mods); - } - } - } - } - - fn undo(app: &mut AppState, original_keyboard_mods: KeyModifier) { - app.hid_provider - .set_modifiers_routed(app.wvr_server.as_mut(), CTRL); - app.hid_provider - .send_key_routed(app.wvr_server.as_mut(), VirtualKey::Z, true); - app.hid_provider - .send_key_routed(app.wvr_server.as_mut(), VirtualKey::Z, false); - app.hid_provider - .set_modifiers_routed(app.wvr_server.as_mut(), original_keyboard_mods); - } - - fn paste(app: &mut AppState, original_keyboard_mods: KeyModifier) { - app.hid_provider - .set_modifiers_routed(app.wvr_server.as_mut(), CTRL); - app.hid_provider - .send_key_routed(app.wvr_server.as_mut(), VirtualKey::V, true); - app.hid_provider - .send_key_routed(app.wvr_server.as_mut(), VirtualKey::V, false); - app.hid_provider - .set_modifiers_routed(app.wvr_server.as_mut(), original_keyboard_mods); - } - pub fn new() -> anyhow::Result<(SwipeTypingManager, Receiver>>)> { - let (candidate_sender, candidate_receiver) = sync_channel(1); - let (task_sender, task_receiver) = channel::(); - - // Spawn persistent worker thread - let worker_candidate_sender = candidate_sender.clone(); - let worker_thread = thread::spawn(move || { - let mut swipe_engine = match SwipeOrchestrator::new() { - Ok(engine) => engine, - Err(e) => { - log::error!("Failed to initialize SwipeOrchestrator: {}", e); - return; - } - }; - - while let Ok(task) = task_receiver.recv() { - match task { - PredictionTask::Predict { swipe, last_word } => { - match swipe_engine.predict(swipe, &last_word) { - Ok(candidates) => { - let words: Vec = candidates - .into_iter() - .take(PREDICTION_SUGGESTION_COUNT) - .map(|c| c.word) - .collect(); - - let _ = worker_candidate_sender.send(Some(words)); - } - Err(e) => { - log::error!("Prediction failed: {}", e); - } - } - } - PredictionTask::Shutdown => break, - } - } - }); - - Ok(( - Self { - keyboard_gird: QwertyKeyboardGrid::new(), - current_swipe: Vec::new(), - swipe_candidate_sender: candidate_sender, - prediction_task_sender: task_sender, - worker_thread: Some(worker_thread), - swipe_start_time: None, - clipboard: Clipboard::new()?, - swipe_left_first_key: false, - first_swipe_char: char::default(), - current_swipe_device: None, - last_swiped_word: None, - }, - candidate_receiver, - )) - } - - pub fn predict(&mut self) -> anyhow::Result<()> { - if self.is_current_swipe_empty() { - bail!("nothing to predict"); - } - - let current_swipe = mem::take(&mut self.current_swipe); - let last_word = self.last_swiped_word.clone(); - self.reset_swipe(); - - self.prediction_task_sender.send(PredictionTask::Predict { - swipe: current_swipe, - last_word, - })?; - - Ok(()) - } - - pub fn reset(&mut self) { - self.reset_swipe(); - let _ = self.swipe_candidate_sender.send(None); - self.last_swiped_word = None; - } - - fn reset_swipe(&mut self) { - self.swipe_start_time = None; - self.current_swipe = Vec::new(); - self.first_swipe_char = char::default(); - self.swipe_left_first_key = false; - self.current_swipe_device = None; - } - - fn start_swipe(&mut self, key_label: char, device: usize) -> Instant { - let now = Instant::now(); - self.swipe_start_time = Some(now); - self.first_swipe_char = key_label.to_ascii_lowercase(); - self.current_swipe_device = Some(device); - now - } - - pub fn did_swipe_leave_first_key(&self) -> bool { - self.swipe_left_first_key - } - - pub fn is_current_swipe_empty(&self) -> bool { - self.current_swipe.is_empty() - } - - pub fn add_swipe(&mut self, within_key_pos_normalized: &Vec2, key_label: char, device: usize) { - if let Some(pos) = self - .keyboard_gird - .key_positions - .get(&key_label.to_ascii_lowercase()) - { - if let Some(current_device) = self.current_swipe_device { - if current_device != device { - return; - } - } - - if self.first_swipe_char != char::default() - && self.first_swipe_char != key_label.to_ascii_lowercase() - { - self.swipe_left_first_key = true; - } - - let key_pos = Vec2 { - x: pos.x as f32, - y: pos.y as f32, - }; - - let start_time = match self.swipe_start_time { - Some(time) => time, - None => self.start_swipe(key_label, device), - }; - - let within_key_pos_from_center = Vec2 { - x: within_key_pos_normalized.x - 0.5, - y: within_key_pos_normalized.y - 0.5, - }; - let key_dimensions = Vec2 { - x: QwertyKeyboardGrid::get_key_width() as f32, - y: QwertyKeyboardGrid::get_key_height() as f32, - }; - - let point = within_key_pos_from_center * key_dimensions + key_pos; - let duration = Instant::now().duration_since(start_time).mul_f32(0.8); // multiply by .8 because library is trained on mobile swipes which happen on a smaller keyboard and are faster - self.current_swipe - .push(SwipePoint::new(point.x.into(), point.y.into(), duration)) - } - } -} - -impl Drop for SwipeTypingManager { - fn drop(&mut self) { - let _ = self.prediction_task_sender.send(PredictionTask::Shutdown); - if let Some(handle) = self.worker_thread.take() { - let _ = handle.join(); - } - } -} diff --git a/wayvr/src/subsystem/hid/mod.rs b/wayvr/src/subsystem/hid/mod.rs index b48f8ee7..ed446d12 100644 --- a/wayvr/src/subsystem/hid/mod.rs +++ b/wayvr/src/subsystem/hid/mod.rs @@ -253,7 +253,6 @@ impl HidProvider for UInputProvider { log::error!("send_key: {res}"); } } - fn set_desktop_extent(&mut self, extent: Vec2) { self.desktop_extent = extent; } @@ -337,7 +336,7 @@ pub const META: KeyModifier = 0x80; #[allow(non_camel_case_types)] #[repr(u16)] -#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Copy, IntegerId, EnumString, EnumIter, Hash)] +#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Copy, IntegerId, EnumString, EnumIter)] pub enum VirtualKey { Escape = 9, N1, // number row diff --git a/wgui/src/event.rs b/wgui/src/event.rs index 1d4c3d5e..e3d93dac 100644 --- a/wgui/src/event.rs +++ b/wgui/src/event.rs @@ -215,10 +215,6 @@ impl CallbackMetadata { let mouse_pos_abs = self.get_mouse_pos_absolute()?; Some(mouse_pos_abs - transform_stack.get().abs_pos) } - pub fn get_mouse_pos_normalized(&self, transform_stack: &TransformStack) -> Option { - let pos_relative = self.get_mouse_pos_relative(transform_stack)?; - Some(pos_relative / transform_stack.parent().raw_dim) - } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/wlx-common/src/config.rs b/wlx-common/src/config.rs index 9ce45c5f..e673bda9 100644 --- a/wlx-common/src/config.rs +++ b/wlx-common/src/config.rs @@ -381,9 +381,6 @@ pub struct GeneralConfig { #[serde(default = "def_one")] pub grid_opacity: f32, - #[serde(default = "def_false")] - pub keyboard_swipe_to_type_enabled: bool, - #[serde(default)] pub chroma_key_params: ChromaKeyParams, } From 9986b71b26bd61535b7260978f68aa48f7e5ef06 Mon Sep 17 00:00:00 2001 From: galister <22305755+galister@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:09:26 +0900 Subject: [PATCH 22/36] skybox selector --- .../gui/tab/monado_tab_general_settings.xml | 2 +- dash-frontend/src/tab/games.rs | 12 +++++- dash-frontend/src/tab/monado.rs | 4 +- dash-frontend/src/tab/settings/mod.rs | 37 ++++++++++++++----- .../src/tab/settings/tab_features.rs | 2 +- dash-frontend/src/views/mod.rs | 4 +- dash-frontend/src/views/skymap_list.rs | 23 ++++++++---- wayvr/src/backend/openxr/helpers.rs | 4 +- wayvr/src/backend/openxr/mod.rs | 3 ++ 9 files changed, 66 insertions(+), 25 deletions(-) diff --git a/dash-frontend/assets/gui/tab/monado_tab_general_settings.xml b/dash-frontend/assets/gui/tab/monado_tab_general_settings.xml index 3d918b82..c99cbf21 100644 --- a/dash-frontend/assets/gui/tab/monado_tab_general_settings.xml +++ b/dash-frontend/assets/gui/tab/monado_tab_general_settings.xml @@ -29,7 +29,7 @@
-
diff --git a/dash-frontend/src/tab/games.rs b/dash-frontend/src/tab/games.rs index 02082ef7..514b7c3e 100644 --- a/dash-frontend/src/tab/games.rs +++ b/dash-frontend/src/tab/games.rs @@ -27,12 +27,20 @@ impl Tab for TabGames { TabType::Games } - fn update(&mut self, frontend: &mut Frontend, time_ms: u32, _data: &mut T) -> anyhow::Result<()> { + fn update(&mut self, frontend: &mut Frontend, time_ms: u32, data: &mut T) -> anyhow::Result<()> { + let mut config_change_kind = None; + self.view_game_list.update(&mut ViewUpdateParams { layout: &mut frontend.layout, - executor: &mut frontend.executor, + executor: &frontend.executor, + general_config: frontend.interface.general_config(data), + config_change_kind: &mut config_change_kind, })?; + if let Some(kind) = config_change_kind { + frontend.interface.config_changed(data, kind); + } + self.view_running_games_list.update(&mut frontend.layout, time_ms)?; Ok(()) } diff --git a/dash-frontend/src/tab/monado.rs b/dash-frontend/src/tab/monado.rs index 4a9d1b95..ade22f89 100644 --- a/dash-frontend/src/tab/monado.rs +++ b/dash-frontend/src/tab/monado.rs @@ -175,7 +175,9 @@ 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, ConfigChangeKind::EnvironmentBlend); + frontend + .interface + .config_changed(data, ConfigChangeKind::EnvironmentBlend); } } Task::SetBrightness(brightness) => self.set_brightness(frontend, data, brightness), diff --git a/dash-frontend/src/tab/settings/mod.rs b/dash-frontend/src/tab/settings/mod.rs index fe539b59..f6e09be8 100644 --- a/dash-frontend/src/tab/settings/mod.rs +++ b/dash-frontend/src/tab/settings/mod.rs @@ -19,7 +19,11 @@ use wgui::{ }, windowing::context_menu::{self, Blueprint, ContextMenu, TickResult}, }; -use wlx_common::{config::GeneralConfig, config_io::ConfigRoot, dash_interface::{ConfigChangeKind, RecenterMode}}; +use wlx_common::{ + config::GeneralConfig, + config_io::ConfigRoot, + dash_interface::{ConfigChangeKind, RecenterMode}, +}; use crate::{ frontend::{Frontend, FrontendTask, FrontendTasks}, @@ -110,13 +114,21 @@ 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 { + let mut config_change_kind = None; + tab.update(&mut ViewUpdateParams { layout: &mut frontend.layout, executor: &frontend.executor, + general_config: frontend.interface.general_config(data), + config_change_kind: &mut config_change_kind, })?; + + if let Some(kind) = config_change_kind { + frontend.interface.config_changed(data, kind); + } } - let mut changed = false; + let mut changed = None; for task in self.tasks.drain() { match task { Task::SetTab(tab) => { @@ -129,7 +141,7 @@ impl Tab for TabSettings { } let config = frontend.interface.general_config(data); *setting.mut_bool(config) = n; - changed = true; + changed = Some(setting.change_kind()); } Task::UpdateFloat(setting, n) => { self.tasks.push(Task::SettingUpdated(setting)); @@ -138,7 +150,7 @@ impl Tab for TabSettings { } let config = frontend.interface.general_config(data); *setting.mut_f32(config) = n; - changed = true; + changed = Some(setting.change_kind()); } Task::UpdateInt(setting, n) => { self.tasks.push(Task::SettingUpdated(setting)); @@ -147,7 +159,7 @@ impl Tab for TabSettings { } let config = frontend.interface.general_config(data); *setting.mut_i32(config) = n; - changed = true; + changed = Some(setting.change_kind()); } Task::ClearPipewireTokens => { let _ = std::fs::remove_file(ConfigRoot::Generic.get_conf_d_path().join("pw_tokens.yaml")) @@ -186,7 +198,7 @@ impl Tab for TabSettings { let config = frontend.interface.general_config(data); config.autostart_apps.remove(idx); frontend.layout.remove_widget(widget); - changed = true; + changed = Some(ConfigChangeKind::OverlayConfig); } } Task::SettingUpdated(setting) => match setting { @@ -219,12 +231,12 @@ impl Tab for TabSettings { let setting = SettingType::from_str(setting).expect("Invalid Enum string"); let config = frontend.interface.general_config(data); setting.set_enum(config, value); - changed = true; + changed = Some(ConfigChangeKind::OverlayConfig); } // Notify overlays of the change - if changed { - frontend.interface.config_changed(data, ConfigChangeKind::OverlayConfig); + if let Some(changed) = changed { + frontend.interface.config_changed(data, changed); } Ok(()) @@ -277,6 +289,13 @@ enum SettingType { } impl SettingType { + pub fn change_kind(self) -> ConfigChangeKind { + match self { + Self::UseSkybox | Self::UsePassthrough => ConfigChangeKind::EnvironmentBlend, + _ => ConfigChangeKind::OverlayConfig, + } + } + pub fn mut_bool(self, config: &mut GeneralConfig) -> &mut bool { match self { Self::InvertScrollDirectionX => &mut config.invert_scroll_direction_x, diff --git a/dash-frontend/src/tab/settings/tab_features.rs b/dash-frontend/src/tab/settings/tab_features.rs index 2269069e..992bf06c 100644 --- a/dash-frontend/src/tab/settings/tab_features.rs +++ b/dash-frontend/src/tab/settings/tab_features.rs @@ -1,6 +1,6 @@ use crate::tab::settings::{ - macros::{options_category, options_checkbox, options_slider_f32}, SettingType, SettingsMountParams, SettingsTab, + macros::{options_category, options_checkbox, options_slider_f32}, }; pub struct State {} diff --git a/dash-frontend/src/views/mod.rs b/dash-frontend/src/views/mod.rs index 91cdee11..918c67fc 100644 --- a/dash-frontend/src/views/mod.rs +++ b/dash-frontend/src/views/mod.rs @@ -1,4 +1,4 @@ -use wlx_common::async_executor::AsyncExecutor; +use wlx_common::{async_executor::AsyncExecutor, config::GeneralConfig, dash_interface::ConfigChangeKind}; pub mod app_launcher; pub mod audio_settings; @@ -16,6 +16,8 @@ pub mod skymap_list_cell; pub struct ViewUpdateParams<'a> { pub layout: &'a mut wgui::layout::Layout, pub executor: &'a AsyncExecutor, + pub general_config: &'a mut GeneralConfig, + pub config_change_kind: &'a mut Option, } pub trait ViewTrait { diff --git a/dash-frontend/src/views/skymap_list.rs b/dash-frontend/src/views/skymap_list.rs index 66a58635..5bedf5ea 100644 --- a/dash-frontend/src/views/skymap_list.rs +++ b/dash-frontend/src/views/skymap_list.rs @@ -9,7 +9,7 @@ use wgui::{ renderer_vk::text::custom_glyph::CustomGlyphData, task::Tasks, }; -use wlx_common::{async_executor::AsyncExecutor, config_io}; +use wlx_common::{async_executor::AsyncExecutor, config::GeneralConfig, config_io, dash_interface::ConfigChangeKind}; use crate::{ frontend::{FrontendTask, FrontendTasks}, @@ -75,7 +75,7 @@ impl ViewTrait for View { self.show_skymap_resolution_selector(entry); } Task::SetSkymap(entry, resolution) => { - self.set_skymap(entry, resolution)?; + self.set_skymap(entry, resolution, par.general_config, par.config_change_kind)?; } } } @@ -132,16 +132,23 @@ impl View { Ok(()) } - fn set_skymap(&mut self, entry: SkymapCatalogEntry, resolution: SkymapResolution) -> anyhow::Result<()> { + fn set_skymap( + &mut self, + entry: SkymapCatalogEntry, + resolution: SkymapResolution, + config: &mut GeneralConfig, + change_kind: &mut Option, + ) -> 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 - ); + config.skybox_texture = config_io::get_skymaps_root() + .join(skymap_file_path) + .to_str() + .context("Skymap filename not valid UTF-8")? + .into(); + *change_kind = Some(ConfigChangeKind::EnvironmentBlend); Ok(()) } diff --git a/wayvr/src/backend/openxr/helpers.rs b/wayvr/src/backend/openxr/helpers.rs index 5f85a094..0f9b352f 100644 --- a/wayvr/src/backend/openxr/helpers.rs +++ b/wayvr/src/backend/openxr/helpers.rs @@ -214,6 +214,7 @@ pub(super) fn posef_to_transform(pose: &xr::Posef) -> Affine3A { pub(super) fn try_apply_chroma_key(app: &AppState) -> bool { let Some(monado) = app.monado_state.as_ref() else { + log::warn!("Could not set Chroma Key: no monado_state"); return false; }; @@ -230,6 +231,5 @@ pub(super) fn try_apply_chroma_key(app: &AppState) -> bool { 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 + params.curve > 0.001 } diff --git a/wayvr/src/backend/openxr/mod.rs b/wayvr/src/backend/openxr/mod.rs index 8e3787ed..f6ec7a44 100644 --- a/wayvr/src/backend/openxr/mod.rs +++ b/wayvr/src/backend/openxr/mod.rs @@ -569,6 +569,9 @@ fn reconfigure_environment_blend( log::debug!("Allocating skybox."); *skybox = create_skybox(xr_state, app); } + } else { + log::debug!("Allocating skybox."); + *skybox = create_skybox(xr_state, app); } } else { log::debug!("Destroying skybox."); From 07bd841bc669a7010b2f8945dbfb3d1ef5466c02 Mon Sep 17 00:00:00 2001 From: Aleksander Date: Wed, 22 Apr 2026 19:03:47 +0200 Subject: [PATCH 23/36] dash-frontend: skymap browser ui improvements, wayvr: fix context menus not working --- .../gui/view/remote_skymap_downloader.xml | 20 +++++++++---------- .../src/tab/settings/tab_look_and_feel.rs | 2 +- dash-frontend/src/tab/settings/tab_skybox.rs | 2 +- wgui/src/windowing/context_menu.rs | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/dash-frontend/assets/gui/view/remote_skymap_downloader.xml b/dash-frontend/assets/gui/view/remote_skymap_downloader.xml index 12dfbadc..c6e386ae 100644 --- a/dash-frontend/assets/gui/view/remote_skymap_downloader.xml +++ b/dash-frontend/assets/gui/view/remote_skymap_downloader.xml @@ -16,20 +16,20 @@
-
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 f2e3ac66..601eb840 100644 --- a/dash-frontend/src/tab/settings/tab_look_and_feel.rs +++ b/dash-frontend/src/tab/settings/tab_look_and_feel.rs @@ -17,13 +17,13 @@ impl State { )?; options_dropdown::(par.mp, c, &SettingType::Language)?; options_checkbox(par.mp, c, SettingType::HideUsername)?; + options_checkbox(par.mp, c, SettingType::OpaqueBackground)?; 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_skybox.rs b/dash-frontend/src/tab/settings/tab_skybox.rs index f516a2d0..dfe4b3e6 100644 --- a/dash-frontend/src/tab/settings/tab_skybox.rs +++ b/dash-frontend/src/tab/settings/tab_skybox.rs @@ -39,7 +39,7 @@ 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)?; + options_checkbox(par.mp, id_category, SettingType::UsePassthrough)?; let tasks = Tasks::::new(); diff --git a/wgui/src/windowing/context_menu.rs b/wgui/src/windowing/context_menu.rs index e9139015..ef0c0bf9 100644 --- a/wgui/src/windowing/context_menu.rs +++ b/wgui/src/windowing/context_menu.rs @@ -8,7 +8,7 @@ use crate::{ globals::WguiGlobals, i18n::Translation, layout::Layout, - parser::{self, Fetchable, ParserState}, + parser::{self, Fetchable, ParserData, ParserState}, task::Tasks, windowing::window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra}, }; @@ -117,7 +117,7 @@ impl ContextMenu { par.insert(Rc::from("tooltip_str"), tooltip.generate(&mut globals.i18n())); } - let mut data_cell = inner_parser.realize_template(&doc_params, "Cell", layout, id_buttons, par)?; + let mut data_cell = inner_parser.parse_template_only(&doc_params, "Cell", layout, id_buttons, par)?; let button = data_cell.fetch_component_as::("button")?; let button_id = button.base().get_id(); From f3732c7087eafb0ce94f6167c3330217c3d5f189 Mon Sep 17 00:00:00 2001 From: Aleksander Date: Wed, 22 Apr 2026 20:22:45 +0200 Subject: [PATCH 24/36] wayvr: improve grid shader, dash-frontend: make pop-up title non-scrollable --- dash-frontend/assets/gui/dashboard.xml | 3 +- .../assets/gui/view/popup_window.xml | 15 ++++--- dash-frontend/src/tab/settings/mod.rs | 1 - wayvr/src/shaders/grid.frag | 43 +++++++++++++------ wgui/src/windowing/context_menu.rs | 2 +- 5 files changed, 44 insertions(+), 20 deletions(-) diff --git a/dash-frontend/assets/gui/dashboard.xml b/dash-frontend/assets/gui/dashboard.xml index 7d5a9a4c..8f2b05f4 100644 --- a/dash-frontend/assets/gui/dashboard.xml +++ b/dash-frontend/assets/gui/dashboard.xml @@ -86,7 +86,8 @@ round="8" flex_grow="1" width="100%" - overflow_y="scroll" + overflow="scroll" + position="relative" > - - + position="relative" + > +
+ +
-
\ No newline at end of file + diff --git a/dash-frontend/src/tab/settings/mod.rs b/dash-frontend/src/tab/settings/mod.rs index f6e09be8..5477078e 100644 --- a/dash-frontend/src/tab/settings/mod.rs +++ b/dash-frontend/src/tab/settings/mod.rs @@ -475,7 +475,6 @@ impl SettingType { | Self::UiGradientIntensity | Self::UprightScreenFix | Self::DoubleCursorFix - | Self::ScreenRenderDown | Self::Language | Self::CaptureMethod ) diff --git a/wayvr/src/shaders/grid.frag b/wayvr/src/shaders/grid.frag index 467aec41..d9016ae1 100644 --- a/wayvr/src/shaders/grid.frag +++ b/wayvr/src/shaders/grid.frag @@ -1,18 +1,37 @@ #version 310 es precision highp float; -layout (location = 0) in vec2 in_uv; -layout (location = 0) out vec4 out_color; +layout(location = 0) in vec2 in_uv; +layout(location = 0) out vec4 out_color; -void main() -{ - float fade = max(1.0 - 2.0 * length(in_uv.xy + vec2(-0.5, -0.5)), 0.0); - float grid; +const float circle_smoothness = 0.0025; +const float circle_thickness = 0.01; +const float circle_opacity = 0.3; +const float circle_size = 0.1; - if (fract(in_uv.x / 0.0005) < 0.01 || fract(in_uv.y / 0.0005) < 0.01) { - grid = 1.0; - } else { - grid = 0.0; - } - out_color = vec4(1.0, 1.0, 1.0, grid * fade); +float calc_grid(vec2 coord, float m) { + vec2 grid = fract(coord * m); + return (step(m, grid.x) * step(m, grid.y)); +} + +float calc_circle(float dist, float size) { + float c1 = size; + float c2 = size - circle_thickness; + + return smoothstep(c1, c1 - circle_smoothness, dist) * + smoothstep(c2 - circle_smoothness, c2, dist); +} + +void main() { + float dist = length(in_uv.xy + vec2(-0.5, -0.5)); + float fade = max(1.0 - 2.0 * dist, 0.0); + + float mask = 1.0 - calc_grid(in_uv.xy * 1000.0, 0.02); + + mask = max(mask, (calc_circle(dist, circle_size) + + calc_circle(dist, circle_size * 2.0) + + calc_circle(dist, circle_size * 3.0)) * + circle_opacity); + + out_color = vec4(1.0, 1.0, 1.0, mask * fade); } diff --git a/wgui/src/windowing/context_menu.rs b/wgui/src/windowing/context_menu.rs index ef0c0bf9..16e62d01 100644 --- a/wgui/src/windowing/context_menu.rs +++ b/wgui/src/windowing/context_menu.rs @@ -8,7 +8,7 @@ use crate::{ globals::WguiGlobals, i18n::Translation, layout::Layout, - parser::{self, Fetchable, ParserData, ParserState}, + parser::{self, Fetchable, ParserState}, task::Tasks, windowing::window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra}, }; From 6cddb43b0fcb9f5fce32f21c8b682f4af673f15c Mon Sep 17 00:00:00 2001 From: galister <22305755+galister@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:21:25 +0900 Subject: [PATCH 25/36] default skybox is shader skybox --- wayvr/src/backend/openxr/skybox.rs | 74 +++++++++++++++++++----------- wayvr/src/graphics/mod.rs | 5 +- wayvr/src/shaders/mod.rs | 7 +++ 3 files changed, 59 insertions(+), 27 deletions(-) diff --git a/wayvr/src/backend/openxr/skybox.rs b/wayvr/src/backend/openxr/skybox.rs index 1ca0c072..2d0215e2 100644 --- a/wayvr/src/backend/openxr/skybox.rs +++ b/wayvr/src/backend/openxr/skybox.rs @@ -24,7 +24,7 @@ use super::{ }; pub(super) struct Skybox { - view: Arc, + view: Option>, sky: Option, grid: Option, grid_pose: xr::Posef, @@ -73,14 +73,12 @@ impl Skybox { "".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())?); - } - - command_buffer.build_and_execute_now()?; - - let view = ImageView::new_default(maybe_image.unwrap())?; // safe unwrap + let view = if let Some(image) = maybe_image { + command_buffer.build_and_execute_now()?; + Some(ImageView::new_default(image)?) + } else { + None + }; let grid_color_scale_bias_khr = xr .instance @@ -125,7 +123,12 @@ impl Skybox { } let opts = SwapchainOpts::new().immutable(); - let extent = self.view.extent_u32arr(); + let extent = self + .view + .as_ref() + .map(|v| v.extent_u32arr()) + .unwrap_or([4096, 4096]); + let mut swapchain = create_swapchain(xr, app.gfx.clone(), extent, 1, opts)?; let tgt = swapchain .acquire_wait_image()? @@ -133,23 +136,42 @@ impl Skybox { .into_iter() .next() .unwrap(); - let pipeline = app.gfx.create_pipeline( - app.gfx_extras.shaders.get("vert_quad").unwrap(), // want panic - app.gfx_extras.shaders.get("frag_srgb").unwrap(), // want panic - WPipelineCreateInfo::new(app.gfx.surface_format), - )?; - let set0 = pipeline.uniform_sampler(0, self.view.clone(), app.gfx.texture_filter)?; - let set1 = pipeline.uniform_buffer_upload(1, vec![1f32])?; - let pass = pipeline.create_pass( - tgt.extent_f32(), - [0.0, 0.0], - app.gfx_extras.quad_verts.clone(), - 0..4, - 0..1, - vec![set0, set1], - &Default::default(), - )?; + let pass = if let Some(view) = self.view.as_ref() { + let pipeline = app.gfx.create_pipeline( + app.gfx_extras.shaders.get("vert_quad").unwrap(), // want panic + app.gfx_extras.shaders.get("frag_srgb").unwrap(), // want panic + WPipelineCreateInfo::new(app.gfx.surface_format), + )?; + + let set0 = pipeline.uniform_sampler(0, view.clone(), app.gfx.texture_filter)?; + let set1 = pipeline.uniform_buffer_upload(1, vec![1f32])?; + pipeline.create_pass( + tgt.extent_f32(), + [0.0, 0.0], + app.gfx_extras.quad_verts.clone(), + 0..4, + 0..1, + vec![set0, set1], + &Default::default(), + )? + } else { + let pipeline = app.gfx.create_pipeline( + app.gfx_extras.shaders.get("vert_quad").unwrap(), // want panic + app.gfx_extras.shaders.get("frag_sky").unwrap(), // want panic + WPipelineCreateInfo::new(app.gfx.surface_format), + )?; + + pipeline.create_pass( + tgt.extent_f32(), + [0.0, 0.0], + app.gfx_extras.quad_verts.clone(), + 0..4, + 0..1, + vec![], + &Default::default(), + )? + }; let mut cmd_buffer = app .gfx diff --git a/wayvr/src/graphics/mod.rs b/wayvr/src/graphics/mod.rs index 52ffa913..5c1573ba 100644 --- a/wayvr/src/graphics/mod.rs +++ b/wayvr/src/graphics/mod.rs @@ -21,7 +21,7 @@ use wgui::gfx::WGfx; use vulkano::instance::InstanceCreateFlags; use wlx_capture::DrmFormat; -use crate::shaders::{frag_color, frag_grid, frag_screen, frag_srgb, vert_quad}; +use crate::shaders::{frag_color, frag_grid, frag_screen, frag_sky, frag_srgb, vert_quad}; #[cfg(feature = "openxr")] use {ash::vk, std::os::raw::c_void}; @@ -74,6 +74,9 @@ impl WGfxExtras { let shader = frag_srgb::load(gfx.device.clone())?; shaders.insert("frag_srgb", shader); + let shader = frag_sky::load(gfx.device.clone())?; + shaders.insert("frag_sky", shader); + let shader = frag_grid::load(gfx.device.clone())?; shaders.insert("frag_grid", shader); diff --git a/wayvr/src/shaders/mod.rs b/wayvr/src/shaders/mod.rs index 86e9f5c4..d7c986a3 100644 --- a/wayvr/src/shaders/mod.rs +++ b/wayvr/src/shaders/mod.rs @@ -32,3 +32,10 @@ pub mod frag_srgb { path: "src/shaders/srgb.frag", } } + +pub mod frag_sky { + vulkano_shaders::shader! { + ty: "fragment", + path: "src/shaders/sky.frag", + } +} From 90c45675050e6cf2cce18e9c7e1610a372b5da2c Mon Sep 17 00:00:00 2001 From: galister <22305755+galister@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:25:35 +0900 Subject: [PATCH 26/36] shuffle some options around --- dash-frontend/src/tab/settings/tab_look_and_feel.rs | 3 +-- dash-frontend/src/tab/settings/tab_skybox.rs | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) 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 601eb840..c632e667 100644 --- a/dash-frontend/src/tab/settings/tab_look_and_feel.rs +++ b/dash-frontend/src/tab/settings/tab_look_and_feel.rs @@ -16,14 +16,13 @@ impl State { "dashboard/palette.svg", )?; options_dropdown::(par.mp, c, &SettingType::Language)?; - options_checkbox(par.mp, c, SettingType::HideUsername)?; options_checkbox(par.mp, c, SettingType::OpaqueBackground)?; + 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::Clock12h)?; Ok(State {}) } diff --git a/dash-frontend/src/tab/settings/tab_skybox.rs b/dash-frontend/src/tab/settings/tab_skybox.rs index dfe4b3e6..66c2edf8 100644 --- a/dash-frontend/src/tab/settings/tab_skybox.rs +++ b/dash-frontend/src/tab/settings/tab_skybox.rs @@ -4,7 +4,7 @@ use crate::{ frontend::FrontendTasks, tab::settings::{ SettingType, SettingsMountParams, SettingsTab, - macros::{options_category, options_checkbox}, + macros::{options_category, options_checkbox, options_slider_f32}, }, util::{popup_manager::PopupHolder, wgui_simple}, views::{self, ViewUpdateParams, skymap_list}, @@ -40,6 +40,7 @@ impl State { 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::UsePassthrough)?; + options_slider_f32(par.mp, id_category, SettingType::GridOpacity, 0.0, 1.0, 0.05)?; let tasks = Tasks::::new(); From bf7276b2dd7da7332ebb4fbb7b2f9bcb17373b2b Mon Sep 17 00:00:00 2001 From: galister <22305755+galister@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:36:25 +0900 Subject: [PATCH 27/36] skybox downloader: apply button for downloaded skymaps --- dash-frontend/assets/lang/en.json | 1 + .../src/views/remote_skymap_downloader.rs | 27 ++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/dash-frontend/assets/lang/en.json b/dash-frontend/assets/lang/en.json index 30436cc1..7b1b1626 100644 --- a/dash-frontend/assets/lang/en.json +++ b/dash-frontend/assets/lang/en.json @@ -172,6 +172,7 @@ "PROCESS_LIST": "Process list", "REFRESH": "Refresh", "REMOVE": "Remove", + "APPLY": "Apply", "RELOAD_FROM_DISK": "Reload from disk", "SETTINGS": "Settings", "SHOW": "Show", diff --git a/dash-frontend/src/views/remote_skymap_downloader.rs b/dash-frontend/src/views/remote_skymap_downloader.rs index 84e03350..3b78a384 100644 --- a/dash-frontend/src/views/remote_skymap_downloader.rs +++ b/dash-frontend/src/views/remote_skymap_downloader.rs @@ -8,6 +8,7 @@ use crate::{ }, views::{self, ViewTrait, ViewUpdateParams}, }; +use anyhow::Context; use wgui::{ assets::AssetPath, components::button::ComponentButton, @@ -20,7 +21,7 @@ use wgui::{ task::Tasks, widget::{image::WidgetImage, label::WidgetLabel}, }; -use wlx_common::{async_executor::AsyncExecutor, config_io}; +use wlx_common::{async_executor::AsyncExecutor, config_io, dash_interface::ConfigChangeKind}; pub struct Params<'a> { pub globals: &'a WguiGlobals, @@ -41,6 +42,7 @@ enum Task { DownloadFinished, RunDownload(SkymapResolution), RemoveFile(SkymapResolution), + SetSkymap(SkymapResolution), } pub struct View { @@ -109,6 +111,19 @@ impl ViewTrait for View { Task::RemoveFile(resolution) => { self.remove_file(resolution)?; } + Task::SetSkymap(resolution) => { + let skymap_file_path = self + .entry + .get_destination_path(resolution) + .context("Skymap not found" /* you shouldn't really see this, like ever. */)?; + + par.general_config.skybox_texture = config_io::get_skymaps_root() + .join(skymap_file_path) + .to_str() + .context("Skymap filename not valid UTF-8")? + .into(); + *par.config_change_kind = Some(ConfigChangeKind::EnvironmentBlend); + } } } @@ -237,6 +252,7 @@ impl View { 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"; + const ACTION_APPLY: &'static str = "apply"; let tasks = self.tasks.clone(); @@ -257,6 +273,11 @@ impl View { icon: "dashboard/download.svg", action: ACTION_DOWNLOAD_AGAIN, }, + views::dialog_box::ButtonEntry { + content: Translation::from_translation_key("APPLY"), + icon: "dashboard/check.svg", + action: ACTION_APPLY, + }, ], on_action_click: Box::new(move |action| match action { ACTION_REMOVE => { @@ -267,6 +288,10 @@ impl View { tasks.push(Task::RunDownload(resolution)); tasks.push(Task::Refresh); } + ACTION_APPLY => { + tasks.push(Task::SetSkymap(resolution)); + tasks.push(Task::Refresh); + } _ => unreachable!(), }), }, From dd3108d42350d59cd61c6177f48d8bd473260a6b Mon Sep 17 00:00:00 2001 From: galister <22305755+galister@users.noreply.github.com> Date: Thu, 23 Apr 2026 19:10:43 +0900 Subject: [PATCH 28/36] new built-in sky shader --- .../dashboard/builtin-skybox-preview.jpg | Bin 0 -> 9994 bytes .../src/views/remote_skymap_downloader.rs | 3 +- dash-frontend/src/views/remote_skymap_list.rs | 2 +- dash-frontend/src/views/skymap_list.rs | 29 +- dash-frontend/src/views/skymap_list_cell.rs | 19 +- wayvr/src/shaders/sky.frag | 368 ++++++++++++++++++ 6 files changed, 404 insertions(+), 17 deletions(-) create mode 100644 dash-frontend/assets/dashboard/builtin-skybox-preview.jpg create mode 100644 wayvr/src/shaders/sky.frag diff --git a/dash-frontend/assets/dashboard/builtin-skybox-preview.jpg b/dash-frontend/assets/dashboard/builtin-skybox-preview.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7ca3e583b599b1227a967ea8b8c42a02fb873c43 GIT binary patch literal 9994 zcmb7pcR*816K{Y3p@$Bk_ZE7Q4xt2T(tDB6MO2zJQ7NI9&_l0+G^KeF5S88nQbYko zq>G5s5f$W}aJ~1t@BR0d!`X9YXJ%(-XMa0;az1yy1cDkM^^qU~puh>>Akg`HkT!?} zOiV%yCLtjvAtfb&kVDDI$;ikVsA(vn%nU3n%nVFSFb)AO7#klu6B9R_n@>RAxfRvP!970Y{PEIcjV}c3)f0y%K5G{lNL+h>{4<1SNukKm>dIn9ANbGJP_AEtVP( z$Oznh!B0S`*{|(SMoUI3szsZ?lGlGh?U#uJxlqmJM8a}g^RFRYPjCipGH;FHWVEzs zePL~YN1Krf1j^${j|2&`LOei3AfS5>P``psR*Qr=_t%mtna&Wo=T~qxnzN7)a(&u`=>(oPIxebF7(dE;J^W{3d$WUY!fj7$|R{RrEtC^^jn-pjfC>2Z)7&=0z8 zI)yF7nW7TG7GhN%Bv`wL6*!ssI$TlftXNU&8~#_mDYqO_+)6shJHC{$k9JNzwp89z zY}(dZR8@gG>nQI!n{NrMzsL)iHDj-uNjjj;kdzK83w(Qmi@lMwB3~N-%gCE%Ek2M{ zGjHXadoA`stVqGPLuvZ+l~2W9$o`FD&5ilaYjNWZL*kh!fp7TWm-@t8>h5_`r}v7Y zh~0$K(nqKU0#~Q>$DHIHXQ#ZRK9h>yW(-Ix3n-sU9Bk|$Uzz<7>qkA=CI8N9(5a@8 z^R9l;$ZZeVvAfoR52wQ>l)q+5;~Y|2C)Z#7MPl>7p5vQvh)kkA<7{<@cWyq?!qh`< z*2C3Wf7&|HGvITWUDbwXG*-My__=xAktQ}fb^zY2Bfta|9JN~3>T1|;7Yn8(?2eM> zcSw@4$ZdK17t>!g?e@v}2<7bz0wK$KN11U+yQ)uA)=PXfKajWV0{XXJSXNdQJTfa# zi6LuyBrDQqsztE)M9s%>Ld)I*1r?;tq;vHbBGW-9f0}66VoxZx{{em7Ol|NuC)Bxm zHc7vE^?LJaMx7mdkoX4ctZ|@r6{VE@gNL7Kxs%d$GI^`(_@nsd8og^{7?+nXqq*~Ss=fG(Q_(^__DDmg`I8acGbOHlL@Y7?6};} zl!RlnHsUtUsedTw<~Sx~Ft%vf+2U-m)xNG?ndb00UivV9Oc^z%?7QCAXxUy>;^4SR zul-6QOX^w}|>WKm*0D2;4n(~UDNp>E=I9H>~Y5( z8v22EuE-)9-jzoos;f7` z^OGw*+$$n;44yjLez`3}fU-4;G}fgzvyunpgN?0|4a~xd1X^*RqM&?_He!z`4K-iChO_(c{*A^6<Vl zp{l4=1GjlxSg~Er7|dwRTeeciJGGN?_%-{koF3f9F+gChx#k00q76rx71V4jk7G~G z9C0tOk_LRUu2ugwsFTTMazE~2PC;&vx7}2QQiX5m{G&B0PtzH9Du!SU;&azk=9=A&5SAT5VggWS!TswWSUKM;H8TKnc+3T(42F5LcP=cUE! zQka5xi{`VR-x&kNsGAl_!;;%L_&=C)!6kqXSJ1 z;-VB*D0#3k!ccKQC!yB80-k^_B6MfH%a41j)Gy{|mDoD`RiPME=nhs~SaQDL#5_x%KsH>OwKltV| zTJR>)^6C&8@D3-^`H8w~>F8w8x-an$1Ks)X09qd02cTUD3g9<@t%M~1f4$+3yz2~|j z|HBF>mOKzGjUhB}uLDZk(CgHDc&eB6Z8M+-1n^Bz%q2jnbi>am5%1IveIJ2ldTxM^e$Z#1vx<3-+X|SPl8AM744N%X(w^Jvrdhzjkk7 zS!{;lkHlAk_Byw|)VFagu1b8H3keCnlhV5J%8A)4IM~3vH0^$(fqC}Ri7Cv`hn(E& z>#sPzhs?j1nDBpgOE93t;&@%7_2sGC-tXgc&|YJV{4$oZ-D60?E&x~Jxc@0frrL6} z!$x!+qPK&eTcG0_D=TN?d#S;1+uq!;exs}&Kbgun<-{;$?06LL24}vke8w7euFpzz1moJAT6VAz&(=Rm`eWO13m)GBzQe?=2o0Esho2mKv7wZlZqGWo|E|99 zkm+ecgveY@9KS)JDT3^hOhO=hA7aPxe~{6|kUr0o$r zyjr!><1Eap@3GzC*iW0bJ6>(L_v?LL97SywqU&7u@1G2wgTx*KcK&_gtu+S%#f!}Y zBj`6&Gmo(xs=M>Fvu|Pfqm2{ul(JZ5NgB%?&%0vk`K3k^4{_?0?dubw9;_!J=b)5( z8o;Fg&aU!k`HXdJ{xpxV$W^npJu(WfRJA_T1gLrGTuyEf?y>E`I$q9#vle3&2UHQt*Rfb-uK`Ryp6P-!=VCp$1Me26e@GW} z{0%3Gb$qt|w02x*{SUkKzl#U|kaD4=p#>*^UlSLz18*5I^gWZ^$B*12^FsNBRy@0> z!v7`x&oZ=gZ<~0nAqMEMw!?SxaC;OGPQ9?pBNI@Q`#-yGlx9Etfj7`W%v>-x4LJ%<+NSXf>S^_#kdMG!Klo^qLZF%u>fsfOQ~FeTH0@ zHv_XGB8H7_u{M}s5w+AF-nt);AVT>E5MroF9YLpM}FQFxvGN+N|Cd#z4 z!)ssmxZfn!(;LBzNn#Kc#&p+)iv(5b3?=|QI=ofxDTV;Az2Qihq`=y|;5^@K)=k-O z_mt$_)CXe_>X~O0o0~z(p{&=odnWx9cQwk-Xg2R_v#bRuv8U+?tYK{ULNZVEPey-w zc2P;MO?qcpBRj-+SGhPwt|_?R9R4V{dQ+e&vt7TQvlm!E6z#D%N<1_$;#X4*P>WjD znpXfXw1l6W0FqPHsm8xvB_se75dXhkC6dxKGv(!zM*QtnLcCW`3884obTm&8UNuCE znW7V@Df0Pv2a;*(E=a)bwj0Yx$r#|w3m>CW+*kp>N~gH7;FzMPM)6!uwu9G1BWwF%Vj24Ku%!op;kw)p7%(Wt=yY>PuR4FB zjKbQDEdTa}o0r(iK7hUGG_K`Jk_;;#0@iU@31J3u@*Z0;VwcAP(()@NRm6fCiJX2n z@-7u7zuZ0t>GBRYWQ!2HT-a|%!3#|Bh@FT4IcyZRG)v6gzz}^`SniDB70rTpeygl+ zVd~_K{(0gu|9#W42e75rBnfh`7O^54MTPZ)Da9dA{HO+{_kl3O*D3j2H1W7&;0Y5a z{hJSPuQ(LQ4ZuXCU?55OQwRuoq-8P?{^d|wSxqfd7j9muD86MnGrvDs0*MCU&X*;M zfBw>o&*sTKe)w`F{_+WI&-t6SXbxT%k+ z{6aXE4c%PxDx=Bcj{0NW`mHLHz=BebVC{ygS;g=og>1R<2Ze6c{e`Y^*=+;j&4!n6 z8l@*SN^GOoT4du8 zP5&)&Xjx@A{ESDbmSH=e>Z3c;hp2fnqUyEx0o4n&Perde7`(HNz2^Fy{7`hV*4Oy0 za-#COSU^q(vI=54ott?hQDKlt^lA)MUf|8)D-&vYEOaJ1lYQCwUs zyqxTZ+I=3mwxPbNI`~|#6bsSv1=H$+p$M?*9W`de>U?NUoHPB$b`X9ULi5H@5F-kg zDcdL3@&J?GC+R=G5^ivXge|Ycw|sB4!h>;m@Len> z9BGS+Z|_DhMz#^?dNUFG22fC;xSLedk9Bwy;&6Sa<(A{$st)j`zG-g^!OBnnn(|3+ zS2?gX;bPChForlfyXqbKV_pxDIY}N|>X3s)S;{-1fuD(PF%kd_=25j=xo5o1GtV}S z^pfwWI!K{~mMj+2&?rum+p@C*@tL>j6;WSf=xDVUyx zaVYG1_oOqaG{=J%7V5cTeeDHegI+XFZ4RTV!9&_{)GOm(MQDEN9Q1~ zv(fTbbxuM4uxqKZGmj=x8O#j&=lyhrONJmJuBvWzbnT7LVXvgXBxy8%!P+Gu`(73C zu)CvSUk8&^RYN4tKr=L*6J_J~TD{udHB^kk5w$;ME{}wXD{;qJ@hJ0b-o434Ov7hhAI6T+Q0jBUoZaUzO&(1^(uLdCujO)mqpG&(v3d} z72j*UT|bI6Rl%%)%B~r3k37a zykW?wva7sFf(!USxri40%@^2-fqdSB!SvPc@({IW;mCK{Ezg+!5c>RriJsc+>vv!@ zB!vvd{tfEYca3~UU_JS)W?59Z4hhETXhCv%vi@H2-4GPDUDli4)?<_BCLc<}PGL94 zWA8Pt{nE(V{B3-XVKNNH1;3QCpqJPpZ*1ZCMt(fda#nuak;CZ`kF>N&^7AXa#qlNa z9MWY&?%X`-KadF>+9olw&XZ;XPI;H!y00xqoO(^hPgiVQy><>FRJhB)^_{i3GN!EZ zBv`@U%fEGkb5t0gpNxtTbJ|KKJW9Vf^uSwy^9@>Gq?jBMC^9r#n|T!_n_OAU9JsB= zPbr+;A*Uqhp=%U*UASd%71gXOah%<5cG}0SChPr^;pJYb&$8L?Pap1+ZgTbfoZY>* zb}Suz7K=N-K;9bO))QmIVC~DngkrW%7Sf6%jm(Z=Xq6 zkKE&Iy0548kB*Saz5iuo_@_R{{?q=H{;vK5+A&WWMX}Q1kC#yXJ^is?e#adievBl+ zy^C_CwY^RSmRtr!I)6IzYyvvX=Ln>j-c*}7!=|C)IER^dh$EYrdfu`vo`l7BftVt^ zpYwdX3h&aUlsdlGyvZHiZ7UQRLqo8nmM$KN+5r)iLj2-5SJ$x^k0t!@44bTBeX3wi zp0AwI^D2w|L_O@nuGj#A|Cnk<+C~U)1{432{AS;WmR9`xQq}usgu?m%5&Fk6tQhAIipIg{7q7q-NT7&yK8NAb>QKY-aHbrL}Ni_f_DO#uY8 zB({m{fGzHIuF}q?`xD>WvvIYaf0ibb8FG5?Gd(VlYhjNLG{KPD{ENU3fo2bO z;Cqeui}BhK#Os-0wfQRsP!yuV#;r(HqEs~SEO~$8y}ka!qtPL``mB0o8uq(OynIES z%dhma1$G4ByB2=Gg->I8EZd}HFq5@AaP>9~edXkXT<}`<8 z@`xn6`wkO)B0(Z4iYv9M zWiQN^wA0F`J*W4e&+kv9BjmnOC_B#)U5=>>;z%Rkal7mFd?tM_M0Lu>vFhtbFUR!T zQ!xWXN)+GuUN{VLYX#sIs3FgUSX@sPBPF(r{mm!MZJy%@{G%RJm(oY58fh6)E>GP+ zGro|h`a;vqFhH}{e)q$~F}q3RX7S-#OsX&EPMQxrS7`$G^`0_sAzdj}dPJ@mm{I2^ z5xYwVeO(uUQ{t_3qlrK&qdK$&Hfb4&S8H81aK z{_m&&z4h?|7E3yX@(z{}pU6f_7`dFJ?K;~HJ2ha7 zZI+&ipHWKM9GNfWTeGpqBCeT_E0}#(MK7l(ysS>W`9h`Q=i|-CDwRv$;;ah3#m!oz zuPLQz7kyHC$U&9762GjbY*rvo!0X*c}$hUKaS9Sd?B|9lh| zeK@`>^ZYmIkLn6wD+A;0h&hbD*wtL3?XqP4x4->)>Lv5Uf2QNZM+9)fAp(B1`6msh zg#r;_1|)6%n&rF87pWTlhy%Th{_V*9Hu`n`5@`B%M0NyZTy{(P*a16nWB3wSH)m6^_7Poir}ZzC|};B8-LlLUsNCi z&&Gc$V`Z(q+AqOycc^(=e?dGL=*U)cLA|mM*5-aJ;>!v(oqZYrX)p z%P=ZO%u{e_)~?MT&tD19;5)73L0E(JLM83pBdSy7-=09uXKXA=93Cy7g6hsJ|rAYqsGaCV|8444rKRCvd0mk?4W%F{p#}`7`zTmw^9^Yo&xS1jL-jxil9gOTAz9w3itD@HFy8KMI1`2T zrh1HzYlGCui8STkGTxXTd|nUb=Wd!pkS)!L=i)pvd%V}(wMWlEe0r0195GET_V8Ro z;^rB)Ne_}kju;v)Wg5=GK2NHbafFcW43>lj6r)Q68zP$?spUgm?%iZ7&SWT1F+wAB zN+qio-QyBW-83>kNe$Z-v;5L;69jPs2?P;m-sxVUw{CQ$t$BBk*$y zqNWx*j*2JmRP@4ahkqs3T2~Yo0rNKgaE~BYqre78x*|j!{#Wn z^a3(rac_b0C+}`BM)z8~SVPK>#aZVU!4>BG5V8uhVKjdNts8CGbc{W1SuR>wz8pp+ zELTnr1t<^9s4%<%gXm1@Kxw6b? z>u58cCPy+B!OM9~`sqm3rXs52Ls#>zKB4NBV5c#KPYPEnG|E-GXD1D0*LntV($QK7 zX=pLo)=kENKEItyZ1MhGHmem`jC{QPmj$44S+M2HaBvfX(n#I$t+-Q*Y7XPeCi|-a ztIpRx&PfX2q2if8p?$Sc%r0SlYhle3J9n$i>(wu+7JY%z>rTSPx4k?1q15$h$ig+~(+SHDBZr(|zPvT8`JP{l z8K0RYi@1W|Uq;anQTL1~kGMz$>xI$ZN;gfsL{v0C4cCo4UcnTwQRd5`3xkyklO9cf z6WTs}dX47N_pY{htntS=@2BK)F^i$ml%0>*OBd`tcck9zeLLWtz3PPi8sWcm4)WMe z*%!OMFJv8*=r~h)TUKH-B$4gPzVsUoRBT?~*v%tWvkuY3UHZg+dqMgH?xJG-nPR<9 z=b$X+rd@s>B3tgL3JCXxpcb;q^r&{Xt?X!IcQ2H_tH_u8s<51HnKw1blTC; zZJx?UInTUSG_z(jb4zebu%@IUJhRsC=8=F|GhZlo3IzyFMn(qF5{lxwfeen1!XpzO z9faFE8q#Yg@u0nizvC93Ro>rcdzqLi>zR+cN!*m3(vf#im|w|L9hcAxPGGqrSoRLRX%G`mU#?77 zR$R6i)5$=@nHj;W!i9T5KQ!C%0?3ZZv4^IyfUx-gl zu>v=vuYNpZP+6!jkI%FEWoysW^_N>mLB1+;>}DM}RSQ|M+a)0#eN1=2h>$cRpOM!Y zdpDoHw=J}`FHdPa@>JvpU9b@66~`*k?m|{VdGD-=IV;acu@8zivlDB>t*}Lq{rdiN zuMf8lO~3u5T7-4=2pYeCr~mL{z=M$EL_6L2<>m;V*X#UO3Ol)4*ZF8fBDhQiT=fEw zE4vY&Rb-KOnma_7oHiD0C}ci_73II8#0< z7x;ocpkpW^voV&D5VoP|9_)rK_7z%NZ~3~(lWn!pm74$5>V!R6=IOD(hLig#{R26Q zdCi#@4dh0NoJM1L<9_Olt8)7uz@!e67K zzh5(Zwf8NYX_-lYmKwyM%$-q`ea$ADmBg4_)MkBmyccS&ul&Xr7n|Kf-ywh zi&rXrQtK3IMbpatZs&cWZrY-5-%4u^!%~cOU8xMmCR*Mw*NrqXN-uUFi%Q8K8`L7W zuI;H*>UUj!D@#^r{?L=`C!!$aYO|+JS>=^ljNqBYeC84Tq36&h(((C7(i;vc+6wv+ zRyz6RTAe_YxVCi)dNNmG;)(YqSv}0qX44)G!Sc7BXNx%(Jnh}C)J|_>Z`y(1H;&c51yzoz1(i6bjZ;Sd z_D)B}IS79t1w^W^-A86OB$>+j7OO{c+=1WYOXSO`Yq<|t75Lo;P0z+}nov=3kzRL^ z?y@Cc^>24w4hq2#gtj|ztV$e&1ucr<29_Ni83$HQ`noz*cAZwcLejR`(2Ofxiz-9G z#H)Uf83&ggZ(gEaUNES9N$Nbiw7uD`GQQFkl62tT9)dNDX%BH(lwd|WScbTCUa9D6 zca*>mghg~ZajZ{&9vfS5;*#)qE#bN6EQX6S8uHtbz^3-L+pxv@10BQ(m@PQTe{d$| z^i1e-x|FyD$S>Svn*oIT*i&|GLohM9@{U4`13he2eOY-w4kWO8!4xrTV}h@X(FAdWEU2Z+^Y|5+ dmC#HP$pw-Io_XCbf?qK!CIoUR;{N&k{{c1KlL!C+ literal 0 HcmV?d00001 diff --git a/dash-frontend/src/views/remote_skymap_downloader.rs b/dash-frontend/src/views/remote_skymap_downloader.rs index 3b78a384..4a2aa537 100644 --- a/dash-frontend/src/views/remote_skymap_downloader.rs +++ b/dash-frontend/src/views/remote_skymap_downloader.rs @@ -117,8 +117,7 @@ impl ViewTrait for View { .get_destination_path(resolution) .context("Skymap not found" /* you shouldn't really see this, like ever. */)?; - par.general_config.skybox_texture = config_io::get_skymaps_root() - .join(skymap_file_path) + par.general_config.skybox_texture = skymap_file_path .to_str() .context("Skymap filename not valid UTF-8")? .into(); diff --git a/dash-frontend/src/views/remote_skymap_list.rs b/dash-frontend/src/views/remote_skymap_list.rs index 813cc7f3..76c171dc 100644 --- a/dash-frontend/src/views/remote_skymap_list.rs +++ b/dash-frontend/src/views/remote_skymap_list.rs @@ -215,7 +215,7 @@ impl View { view: views::skymap_list_cell::View::new(views::skymap_list_cell::Params { id_parent: id_list, layout, - entry: entry.clone(), + entry: Some(entry.clone()), on_click: self .tasks .get_button_click_callback(Task::ShowRemoteSkymapDownloader(skymap_uuid)), diff --git a/dash-frontend/src/views/skymap_list.rs b/dash-frontend/src/views/skymap_list.rs index 5bedf5ea..cf97aa2d 100644 --- a/dash-frontend/src/views/skymap_list.rs +++ b/dash-frontend/src/views/skymap_list.rs @@ -16,7 +16,6 @@ use crate::{ util::{ networking::skymap_catalog::{self, SkymapCatalogEntry, SkymapResolution}, popup_manager::{MountPopupOnceParams, PopupHolder}, - wgui_simple, }, views::{self, ViewTrait, ViewUpdateParams}, }; @@ -27,6 +26,7 @@ enum Task { Refresh, ShowSkymapResolutionSelector(SkymapCatalogEntry), SetSkymap(SkymapCatalogEntry, SkymapResolution), + SetSkymapBuiltIn, } pub struct Params<'a> { @@ -77,6 +77,10 @@ impl ViewTrait for View { Task::SetSkymap(entry, resolution) => { self.set_skymap(entry, resolution, par.general_config, par.config_change_kind)?; } + Task::SetSkymapBuiltIn => { + par.general_config.skybox_texture = "".into(); + *par.config_change_kind = Some(ConfigChangeKind::EnvironmentBlend); + } } } } @@ -205,22 +209,27 @@ impl View { 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(); + + let mut view = views::skymap_list_cell::View::new(views::skymap_list_cell::Params { + id_parent: self.list_parent, + layout, + entry: None, + on_click: self.tasks.get_button_click_callback(Task::SetSkymapBuiltIn), + })?; + // load preview image + let data = include_bytes!("../../assets/dashboard/builtin-skybox-preview.jpg"); + if let Ok(glyph_data) = CustomGlyphData::from_bytes_raster(&self.globals, "builtin-skybox-preview.jpg", data) { + view.set_image(layout, Some(glyph_data))?; } - let skymaps_root = config_io::get_skymaps_root(); + self.cells.push(Cell { view }); 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(), + entry: Some(entry.clone()), on_click: self .tasks .get_button_click_callback(Task::ShowSkymapResolutionSelector(entry.clone())), diff --git a/dash-frontend/src/views/skymap_list_cell.rs b/dash-frontend/src/views/skymap_list_cell.rs index 26bfcce3..f0267ce5 100644 --- a/dash-frontend/src/views/skymap_list_cell.rs +++ b/dash-frontend/src/views/skymap_list_cell.rs @@ -23,7 +23,7 @@ use crate::util::{ pub struct Params<'a> { pub id_parent: WidgetID, pub layout: &'a mut Layout, - pub entry: networking::skymap_catalog::SkymapCatalogEntry, + pub entry: Option, pub on_click: ButtonClickCallback, } @@ -108,11 +108,20 @@ impl View { label_title.set_text_simple( &mut globals.get(), - Translation::from_raw_text_string(par.entry.name.clone()), + Translation::from_raw_text_string( + par + .entry + .as_ref() + .map(|e| e.name.clone()) + .unwrap_or_else(|| "Built-in Sky Shader".into()), + ), ); label_author.set_text_simple( &mut globals.get(), - Translation::from_raw_text_string(format!("by {}", par.entry.author)), + Translation::from_raw_text_string(format!( + "by {}", + par.entry.as_ref().map(|e| e.author.as_str()).unwrap_or("WayVR Team") + )), ); } @@ -123,7 +132,9 @@ impl View { })?; // Populate resolution pips - populate_res_pips(par.layout, id_resolution_pips, &mut parser_state, &par.entry)?; + if let Some(entry) = par.entry.as_ref() { + populate_res_pips(par.layout, id_resolution_pips, &mut parser_state, entry)?; + } Ok(Self { parser_state, diff --git a/wayvr/src/shaders/sky.frag b/wayvr/src/shaders/sky.frag new file mode 100644 index 00000000..d26f8110 --- /dev/null +++ b/wayvr/src/shaders/sky.frag @@ -0,0 +1,368 @@ +#version 450 + +layout(location = 0) in vec2 vUV; +layout(location = 0) out vec4 outColor; + +float gauss(float x, float sigma) +{ + float t = x / sigma; + return exp(-t * t); +} + +float smoothBand(float x, float lo, float hi, float featherLo, float featherHi) +{ + float a = smoothstep(lo - featherLo, lo + featherLo, x); + float b = 1.0 - smoothstep(hi - featherHi, hi + featherHi, x); + return a * b; +} + +float thinLine(float x, float halfWidth) +{ + float d = abs(x); + float aa = max(fwidth(x) * 1.5, 1e-5); + + float core = 1.0 - smoothstep(0.0, halfWidth + aa, d); + float edge = 1.0 - smoothstep(halfWidth, halfWidth + aa, d); + + return core * edge; +} + +float hash11(float p) +{ + return fract(sin(p * 127.1 + 311.7) * 43758.5453123); +} + +float hash12(vec2 p) +{ + return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); +} + +float noise1Periodic(float x, float period) +{ + float i0 = floor(x); + float i1 = i0 + 1.0; + float f = fract(x); + float u = f * f * (3.0 - 2.0 * f); + + float a = hash11(mod(i0, period)); + float b = hash11(mod(i1, period)); + return mix(a, b, u); +} + +float fbm1Periodic(float x, float period) +{ + float sum = 0.0; + float amp = 0.5; + float norm = 0.0; + + for (int i = 0; i < 5; ++i) + { + sum += amp * noise1Periodic(x, period); + norm += amp; + x *= 2.0; + period *= 2.0; + amp *= 0.5; + } + + return sum / norm; +} + +float noise2TileX(vec2 p, float periodX) +{ + vec2 i = floor(p); + vec2 f = fract(p); + vec2 u = f * f * (3.0 - 2.0 * f); + + float x0 = mod(i.x, periodX); + float x1 = mod(i.x + 1.0, periodX); + float y0 = i.y; + float y1 = i.y + 1.0; + + float a = hash12(vec2(x0, y0)); + float b = hash12(vec2(x1, y0)); + float c = hash12(vec2(x0, y1)); + float d = hash12(vec2(x1, y1)); + + return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); +} + +float fbm2TileX(vec2 p, float periodX) +{ + float sum = 0.0; + float amp = 0.5; + float norm = 0.0; + + for (int i = 0; i < 4; ++i) + { + sum += amp * noise2TileX(p, periodX); + norm += amp; + p *= 2.0; + periodX *= 2.0; + amp *= 0.5; + } + + return sum / norm; +} + +vec3 tonemapACESApprox(vec3 x) +{ + const float a = 2.51; + const float b = 0.03; + const float c = 2.43; + const float d = 0.59; + const float e = 0.14; + return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0); +} + +float independentPath( + float u, + float baseLat, + float warpAmp0, float warpFreq0, float warpPhase0, + float warpAmp1, float warpFreq1, float warpPhase1, + float amp0, float freq0, float phase0, + float amp1, float freq1, float phase1, + float amp2, float freq2, float phase2, + out float uw) +{ + uw = fract(u + + warpAmp0 * (fbm1Periodic(u * warpFreq0 + warpPhase0, warpFreq0) - 0.5) + + warpAmp1 * (fbm1Periodic(u * warpFreq1 + warpPhase1, warpFreq1) - 0.5)); + + return baseLat + + amp0 * (fbm1Periodic(uw * freq0 + phase0, freq0) - 0.5) + + amp1 * (fbm1Periodic(uw * freq1 + phase1, freq1) - 0.5) + + amp2 * (fbm1Periodic(uw * freq2 + phase2, freq2) - 0.5); +} + +float independentPositive( + float u, + float baseValue, + float amp0, float freq0, float phase0, + float amp1, float freq1, float phase1) +{ + return baseValue + + amp0 * fbm1Periodic(u * freq0 + phase0, freq0) + + amp1 * fbm1Periodic(u * freq1 + phase1, freq1); +} + +void main() +{ + float u = fract(vUV.x); + float v = 0.5 - vUV.y; + + // background gradient + float skyT = smoothstep(-0.50, 0.50, v); + vec3 col = mix(vec3(0.00008, 0.00012, 0.00045), + vec3(0.0220, 0.0470, 0.0980), + skyT); + + col += gauss(v - 0.24, 0.30) * vec3(0.0045, 0.0100, 0.0220) * 0.28; + + // seamless flow field around 360 degrees + float flowA = fbm1Periodic(u * 7.0 + 1.3, 7.0); + float flowB = fbm1Periodic(u * 13.0 + 5.1, 13.0); + + float uu = fract(u + + 0.045 * (flowA - 0.5) + + 0.020 * (flowB - 0.5)); + + float sA = fbm1Periodic(uu * 5.0 + 2.7, 5.0); + float sB = fbm1Periodic(uu * 11.0 + 11.4, 11.0); + float sC = fbm1Periodic(uu * 23.0 + 4.2, 23.0); + float sD = fbm1Periodic(uu * 9.0 + 17.2, 9.0); + float sE = fbm1Periodic(uu * 19.0 + 8.3, 19.0); + + float center = -0.012 + + 0.100 * (sA - 0.5) + + 0.045 * (sB - 0.5) + + 0.018 * (sC - 0.5); + + float thickness = 0.072 + 0.040 * sD; + float skew = 0.014 * (sE - 0.5); + + float mainLo = center - 0.035 - skew; + float mainHi = center + thickness + 0.15 * skew; + + float veilU; + float veilCenter = independentPath( + u, 0.050, + 0.028, 7.0, 1.1, + -0.015, 13.0, 5.4, + 0.050, 3.0, 2.3, + 0.030, 11.0, 8.1, + 0.010, 29.0, 5.7, + veilU); + + float veilThickness = independentPositive( + veilU, 0.011, + 0.010, 9.0, 14.2, + 0.006, 21.0, 3.6); + + float veilLo = veilCenter - 0.004; + float veilHi = veilCenter + veilThickness; + + float auroraU; + float auroraCenter = independentPath( + u, 0.182, + 0.033, 5.0, 3.9, + 0.020, 17.0, 7.7, + 0.060, 4.0, 1.6, + 0.040, 9.0, 12.4, + 0.014, 27.0, 6.9, + auroraU); + + float auroraThickness = independentPositive( + auroraU, 0.034, + 0.014, 7.0, 4.1, + 0.008, 19.0, 15.2); + + float auroraLo = auroraCenter - 0.004; + float auroraHi = auroraCenter + auroraThickness; + + float mistU; + float mistCenter = independentPath( + u, 0.255, + 0.020, 6.0, 2.8, + -0.012, 15.0, 9.1, + 0.030, 4.0, 1.9, + 0.020, 10.0, 13.2, + 0.008, 26.0, 7.4, + mistU); + + float electric0U; + float electric0Center = independentPath( + u, 0.004, + 0.052, 9.0, 2.4, + -0.018, 1.0, 11.6, + 0.098, 3.0, 4.1, + 0.068, 1.0, 9.7, + 0.054, 2.0, 15.3, + electric0U); + + float electric1U; + float electric1Center = independentPath( + u, 0.002, + 0.046, 7.0, 8.3, + 0.020, 7.0, 3.1, + 0.080, 4.0, 6.6, + 0.071, 2.0, 12.2, + 0.064, 1.0, 5.8, + electric1U); + + float electric2U; + float electric2Center = independentPath( + u, 0.006, + 0.044, 3.0, 13.7, + -0.017, 4.0, 6.4, + 0.061, 5.0, 10.9, + 0.082, 6.0, 4.2, + 0.075, 5.0, 14.8, + electric2U); + + float mainTex = fbm2TileX(vec2(uu * 20.0 + 7.0, (v - center) * 8.0 + 13.0), 20.0); + float fineTex = fbm2TileX(vec2(uu * 36.0 + 17.0, (v - center) * 14.0 + 29.0), 36.0); + + float veilTex = fbm2TileX(vec2(veilU * 14.0 + 5.0, (v - veilCenter) * 9.0 + 41.0), 14.0); + float auroraTex = fbm2TileX(vec2(auroraU * 16.0 + 21.0, (v - auroraCenter) * 11.0 + 57.0), 16.0); + float auroraFine = fbm2TileX(vec2(auroraU * 28.0 + 3.0, (v - auroraCenter) * 18.0 + 71.0), 28.0); + + float bodyMod = 0.82 + 0.28 * mainTex + 0.18 * fineTex; + float veilMod = 0.80 + 0.30 * veilTex; + float auroraMod = 0.78 + 0.24 * auroraTex + 0.16 * auroraFine; + + // nasks + float aura = gauss(v - center, 0.14); + + float mainBody = smoothBand(v, mainLo, mainHi, 0.018, 0.024); + float mainMid = gauss(v - mix(mainLo, mainHi, 0.42), 0.55 * thickness); + float mainHiEdge = gauss(v - mainHi, 0.017); + + float veilBody = smoothBand(v, veilLo, veilHi, 0.014, 0.020); + float veilMid = gauss(v - mix(veilLo, veilHi, 0.52), 0.60 * veilThickness); + float veilLoEdge = gauss(v - veilLo, 0.012); + float veilHiEdge = gauss(v - veilHi, 0.018); + + float auroraBody = smoothBand(v, auroraLo, auroraHi, 0.016, 0.022); + float auroraMid = gauss(v - mix(auroraLo, auroraHi, 0.52), 0.62 * auroraThickness); + float auroraLoEdge = gauss(v - auroraLo, 0.013); + float auroraHiEdge = gauss(v - auroraHi, 0.020); + + float mist = gauss(v - mistCenter, 0.085); + + float veilLat = v - veilCenter; + float auroraLat = v - auroraCenter; + + float veilColorNoise = + 0.60 * fbm2TileX(vec2(veilU * 6.0 + 2.0, veilLat * 72.0 + 11.0), 6.0) + + 0.40 * fbm2TileX(vec2(veilU * 14.0 + 7.0, veilLat * 128.0 + 31.0), 14.0); + + float auroraColorU = fract( + auroraU + + 0.035 * (fbm1Periodic(auroraU * 9.0 + 2.7, 9.0) - 0.5) + + 0.018 * (fbm1Periodic(auroraU * 21.0 + 6.1, 21.0) - 0.5) + ); + + vec3 auroraColorNoise = clamp(vec3( + fbm1Periodic(auroraColorU * 5.0 + 1.3, 5.0), // R + fbm1Periodic(auroraColorU * 9.0 + 7.8, 9.0), // G + fbm1Periodic(auroraColorU * 17.0 + 12.4, 17.0) // B + ), 0.0, 1.0); + + veilColorNoise = clamp(veilColorNoise, 0.0, 1.0); + auroraColorNoise = clamp(auroraColorNoise, 0.0, 1.0); + + vec3 veilBaseColor = 0.2 * mix(vec3(0.070, 0.160, 0.460), + vec3(0.140, 0.620, 0.760), + 0.55 * veilColorNoise); + + vec3 veilPeakColor = 0.5 * mix(vec3(0.110, 0.240, 0.620), + vec3(0.180, 0.900, 0.950), + 0.65 * veilColorNoise); + + vec3 auroraBaseColor = 0.2 * vec3(0.100, 0.640, 0.720) * auroraColorNoise * auroraColorNoise; + vec3 auroraPeakColor = 0.5 * vec3(0.180, 0.980, 1.050) * auroraColorNoise * auroraColorNoise; + + float electric0 = thinLine(v - electric0Center, 0.0001); + float electric1 = thinLine(v - electric1Center, 0.00015); + float electric2 = thinLine(v - electric2Center, 0.0002); + + float electric0Glow = gauss(v - electric0Center, 0.0040); + float electric1Glow = gauss(v - electric1Center, 0.0048); + float electric2Glow = gauss(v - electric2Center, 0.0052); + + float e0Energy = 0.82 + 0.28 * fbm1Periodic(electric0U * 41.0 + 6.2, 41.0); + float e1Energy = 0.80 + 0.25 * fbm1Periodic(electric1U * 47.0 + 10.4, 47.0); + float e2Energy = 0.80 + 0.25 * fbm1Periodic(electric2U * 39.0 + 14.9, 39.0); + + // compose + col += aura * (0.85 + 0.25 * mainTex) * vec3(0.014, 0.032, 0.080) * 0.95; + + col += mainBody * bodyMod * vec3(0.070, 0.160, 0.420) * 0.5; + col += mainMid * bodyMod * vec3(0.120, 0.280, 0.740) * 0.4; + col += mainHiEdge * bodyMod * vec3(0.105, 0.240, 0.620) * 0.1; + + col += veilBody * veilMod * veilBaseColor * 1.05; + col += veilMid * veilMod * veilPeakColor * 0.90; + col += veilLoEdge * veilMod * mix(veilBaseColor, veilPeakColor, 0.55) * 0.80; + col += veilHiEdge * veilMod * vec3(0.100, 0.220, 0.580) * 0.20; + + col += auroraBody * auroraMod * auroraBaseColor * 0.95; + col += auroraMid * auroraMod * auroraPeakColor * 0.78; + col += auroraLoEdge * auroraMod * mix(auroraBaseColor, auroraPeakColor, 0.45) * 0.55; + col += auroraHiEdge * auroraMod * vec3(0.080, 0.180, 0.480) * 0.18; + + col += mist * (0.52 + 0.28 * auroraTex) * vec3(0.035, 0.082, 0.220) * 0.20; + + col += electric0Glow * vec3(0.70, 1.10, 1.90) * 0.55; + col += electric1Glow * mix(veilBaseColor, veilPeakColor, 0.60) * 1.20; + col += electric2Glow * mix(auroraBaseColor, auroraPeakColor, 0.65) * 1.35; + + col += electric0 * e0Energy * vec3(10.0, 10.3, 10.8); + col += electric1 * e1Energy * vec3( 6.2, 6.8, 7.4); + col += electric2 * e2Energy * vec3( 5.8, 6.5, 7.2); + + //TODO: skipped if rendering to a HDR swapchain + col = tonemapACESApprox(col); + + outColor = vec4(col, 1.0); +} From 9734955ebb60c9b3c231d8d9e64f01c6e845d77d Mon Sep 17 00:00:00 2001 From: galister <22305755+galister@users.noreply.github.com> Date: Thu, 23 Apr 2026 19:58:02 +0900 Subject: [PATCH 29/36] dont show skymap tab if not openxr --- dash-frontend/src/tab/settings/mod.rs | 13 ++++++++++++- wayvr/src/overlays/dashboard.rs | 7 +++++++ wgui/src/components/tabs.rs | 12 +++++++++++- wlx-common/src/dash_interface.rs | 7 +++++++ wlx-common/src/dash_interface_emulated.rs | 7 +++++++ 5 files changed, 44 insertions(+), 2 deletions(-) diff --git a/dash-frontend/src/tab/settings/mod.rs b/dash-frontend/src/tab/settings/mod.rs index 5477078e..32ec394a 100644 --- a/dash-frontend/src/tab/settings/mod.rs +++ b/dash-frontend/src/tab/settings/mod.rs @@ -5,6 +5,7 @@ use wgui::{ assets::AssetPath, components::tabs::ComponentTabs, drawing, + event::StyleSetRequest, globals::WguiGlobals, i18n::Translation, layout::{Layout, WidgetID}, @@ -585,7 +586,7 @@ impl TabSettings { Ok(()) } - pub fn new(frontend: &mut Frontend, parent_id: WidgetID, _data: &mut T) -> anyhow::Result { + pub fn new(frontend: &mut Frontend, parent_id: WidgetID, data: &mut T) -> anyhow::Result { let doc_params = ParseDocumentParams { globals: frontend.layout.state.globals.clone(), path: AssetPath::BuiltIn("gui/tab/settings.xml"), @@ -595,6 +596,16 @@ impl TabSettings { let parser_state = wgui::parser::parse_from_assets(&doc_params, &mut frontend.layout, parent_id)?; let tasks = Tasks::default(); let tabs = parser_state.fetch_component_as::("tabs")?; + + if !frontend.interface.get_feats(data).openxr { + let skybox_btn = tabs.get_tab_button("skybox").unwrap(); + frontend + .layout + .common() + .alterables + .set_style(skybox_btn.get_rect(), StyleSetRequest::Display(taffy::Display::None)); + } + tabs.on_select({ let tasks = tasks.clone(); Rc::new(move |_common, evt| { diff --git a/wayvr/src/overlays/dashboard.rs b/wayvr/src/overlays/dashboard.rs index 10b05964..a0b531d7 100644 --- a/wayvr/src/overlays/dashboard.rs +++ b/wayvr/src/overlays/dashboard.rs @@ -476,6 +476,13 @@ impl DashInterface for DashInterfaceLive { .enqueue(TaskType::Overlay(OverlayTask::ToggleDashboard)); } + fn get_feats(&mut self, data: &mut AppState) -> dash_interface::InterfaceFeats { + dash_interface::InterfaceFeats { + openxr: matches!(data.xr_backend, XrBackend::OpenXR), + monado: data.monado_state.is_some(), + } + } + #[cfg(feature = "openxr")] fn monado_client_list( &mut self, diff --git a/wgui/src/components/tabs.rs b/wgui/src/components/tabs.rs index 521fb842..293cc00a 100644 --- a/wgui/src/components/tabs.rs +++ b/wgui/src/components/tabs.rs @@ -9,7 +9,7 @@ use crate::{ layout::WidgetPair, widget::{ConstructEssentials, div::WidgetDiv}, }; -use std::{cell::RefCell, rc::Rc}; +use std::{cell::RefCell, fmt::Pointer, rc::Rc}; use taffy::{ AlignItems, prelude::{length, percent}, @@ -94,6 +94,16 @@ impl ComponentTabs { pub fn on_select(&self, callback: TabSelectCallback) { self.state.borrow_mut().on_select = Some(callback); } + + pub fn get_tab_button(&self, name: &str) -> Option> { + self + .state + .borrow_mut() + .mounted_entries + .iter() + .find(|e| name == &*e.name) + .map(|e| e.button.clone()) + } } pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Result<(WidgetPair, Rc)> { diff --git a/wlx-common/src/dash_interface.rs b/wlx-common/src/dash_interface.rs index bf9544ae..34990443 100644 --- a/wlx-common/src/dash_interface.rs +++ b/wlx-common/src/dash_interface.rs @@ -43,6 +43,12 @@ pub enum RecenterMode { Reset, } +#[derive(Clone, Copy)] +pub struct InterfaceFeats { + pub openxr: bool, + pub monado: bool, +} + pub trait DashInterface { fn window_list(&mut self, data: &mut T) -> anyhow::Result>; fn window_set_visible(&mut self, data: &mut T, handle: WvrWindowHandle, visible: bool) -> anyhow::Result<()>; @@ -68,6 +74,7 @@ pub trait DashInterface { 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); + fn get_feats(&mut self, data: &mut T) -> InterfaceFeats; } #[derive(Clone, Copy)] diff --git a/wlx-common/src/dash_interface_emulated.rs b/wlx-common/src/dash_interface_emulated.rs index d2479e86..919054dd 100644 --- a/wlx-common/src/dash_interface_emulated.rs +++ b/wlx-common/src/dash_interface_emulated.rs @@ -236,6 +236,13 @@ impl DashInterface<()> for DashInterfaceEmulated { fn toggle_dashboard(&mut self, _data: &mut ()) {} + fn get_feats(&mut self, _data: &mut ()) -> dash_interface::InterfaceFeats { + dash_interface::InterfaceFeats { + openxr: true, + monado: true, + } + } + fn monado_client_list( &mut self, _data: &mut (), From 6b81f07cc805c818791682f725f9f32f7bab7794 Mon Sep 17 00:00:00 2001 From: oo8dev Date: Sat, 2 May 2026 19:21:30 +0200 Subject: [PATCH 30/36] wgui: Swipe kinetic scrolling support (#504) * wgui: use DeviceBitmask instead of usize * wgui: Swipe logic works (wip) * wgui: Kinetic scrolling, rubberband effect --- .../src/util/networking/skymap_catalog.rs | 11 +- dash-frontend/src/util/wgui_simple.rs | 1 + uidev/src/main.rs | 10 +- wayvr/src/gui/panel/mod.rs | 18 +- wayvr/src/ipc/event_queue.rs | 2 - wayvr/src/overlays/dashboard.rs | 20 +- wgui/src/components/button.rs | 13 + wgui/src/components/checkbox.rs | 12 + wgui/src/components/slider.rs | 6 +- wgui/src/components/tabs.rs | 2 +- wgui/src/event.rs | 32 ++- wgui/src/layout.rs | 71 ++++-- wgui/src/widget/mod.rs | 230 ++++++++++++++---- 13 files changed, 326 insertions(+), 102 deletions(-) diff --git a/dash-frontend/src/util/networking/skymap_catalog.rs b/dash-frontend/src/util/networking/skymap_catalog.rs index db1feee6..d7d21ca2 100644 --- a/dash-frontend/src/util/networking/skymap_catalog.rs +++ b/dash-frontend/src/util/networking/skymap_catalog.rs @@ -1,12 +1,7 @@ -#![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}; - +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use wlx_common::{async_executor::AsyncExecutor, config_io}; pub type SkymapUuid = uuid::Uuid; #[derive(Copy, Clone, Serialize, Deserialize, Debug)] diff --git a/dash-frontend/src/util/wgui_simple.rs b/dash-frontend/src/util/wgui_simple.rs index f09b69ab..af4d393c 100644 --- a/dash-frontend/src/util/wgui_simple.rs +++ b/dash-frontend/src/util/wgui_simple.rs @@ -45,6 +45,7 @@ pub fn create_button(par: CreateButtonParams) -> anyhow::Result<()> { Ok(()) } +#[allow(dead_code)] pub fn create_label(layout: &mut Layout, id_parent: WidgetID, content: Translation) -> anyhow::Result<()> { let label = WidgetLabel::create( &mut layout.state, diff --git a/uidev/src/main.rs b/uidev/src/main.rs index a812d1ce..69e95fa2 100644 --- a/uidev/src/main.rs +++ b/uidev/src/main.rs @@ -143,7 +143,7 @@ fn main() -> Result<(), Box> { &wgui::event::Event::MouseWheel(MouseWheelEvent { delta: Vec2::new(x, y), pos: mouse / scale, - device: 0, + device: wgui::event::DeviceBitmask(0), }), &mut (), &mut (), @@ -157,7 +157,7 @@ fn main() -> Result<(), Box> { &wgui::event::Event::MouseWheel(MouseWheelEvent { delta: Vec2::new(pos.x as f32 / 5.0, pos.y as f32 / 5.0), pos: mouse / scale, - device: 0, + device: wgui::event::DeviceBitmask(0), }), &mut (), &mut (), @@ -177,7 +177,7 @@ fn main() -> Result<(), Box> { &wgui::event::Event::MouseDown(MouseButtonEvent { pos: mouse / scale, index: MouseButtonIndex::Left, - device: 0, + device: wgui::event::DeviceBitmask(0), }), &mut (), &mut (), @@ -190,7 +190,7 @@ fn main() -> Result<(), Box> { &wgui::event::Event::MouseUp(MouseButtonEvent { pos: mouse / scale, index: MouseButtonIndex::Left, - device: 0, + device: wgui::event::DeviceBitmask(0), }), &mut (), &mut (), @@ -209,7 +209,7 @@ fn main() -> Result<(), Box> { .push_event( &wgui::event::Event::MouseMotion(MouseMotionEvent { pos: mouse / scale, - device: 0, + device: wgui::event::DeviceBitmask(0), }), &mut (), &mut (), diff --git a/wayvr/src/gui/panel/mod.rs b/wayvr/src/gui/panel/mod.rs index c0f6d8ac..04d067a1 100644 --- a/wayvr/src/gui/panel/mod.rs +++ b/wayvr/src/gui/panel/mod.rs @@ -22,7 +22,7 @@ use wgui::{ slider::ComponentSlider, }, event::{ - Event as WguiEvent, EventCallback, EventListenerID, EventListenerKind, + DeviceBitmask, Event as WguiEvent, EventCallback, EventListenerID, EventListenerKind, InternalStateChangeEvent, MouseButtonEvent, MouseButtonIndex, MouseLeaveEvent, MouseMotionEvent, MouseWheelEvent, }, @@ -368,7 +368,7 @@ impl OverlayBackend for GuiPanel { let e = WguiEvent::MouseWheel(MouseWheelEvent { delta: vec2(delta.x, delta.y) / 8.0, pos: hit.uv * self.layout.content_size, - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }); self.push_event(app, &e); } @@ -376,7 +376,7 @@ impl OverlayBackend for GuiPanel { fn on_hover(&mut self, app: &mut AppState, hit: &PointerHit) -> HoverResult { let e = &WguiEvent::MouseMotion(MouseMotionEvent { pos: hit.uv * self.layout.content_size, - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }); self.has_focus[hit.pointer] = true; @@ -397,7 +397,9 @@ impl OverlayBackend for GuiPanel { } fn on_left(&mut self, app: &mut AppState, pointer: usize) { - let e = WguiEvent::MouseLeave(MouseLeaveEvent { device: pointer }); + let e = WguiEvent::MouseLeave(MouseLeaveEvent { + device: DeviceBitmask::from_usize(pointer), + }); self.has_focus[pointer] = false; self.push_event(app, &e); } @@ -414,13 +416,13 @@ impl OverlayBackend for GuiPanel { WguiEvent::MouseDown(MouseButtonEvent { pos: hit.uv * self.layout.content_size, index, - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }) } else { WguiEvent::MouseUp(MouseButtonEvent { pos: hit.uv * self.layout.content_size, index, - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }) }; self.push_event(app, &e); @@ -429,11 +431,11 @@ impl OverlayBackend for GuiPanel { if !pressed && !self.has_focus[hit.pointer] { let e = WguiEvent::MouseMotion(MouseMotionEvent { pos: vec2(-1., -1.), - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }); self.push_event(app, &e); let e = WguiEvent::MouseLeave(MouseLeaveEvent { - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }); self.push_event(app, &e); } diff --git a/wayvr/src/ipc/event_queue.rs b/wayvr/src/ipc/event_queue.rs index c150c4c4..13c9adb4 100644 --- a/wayvr/src/ipc/event_queue.rs +++ b/wayvr/src/ipc/event_queue.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] - use std::{cell::RefCell, collections::VecDeque, rc::Rc}; #[derive(Debug)] diff --git a/wayvr/src/overlays/dashboard.rs b/wayvr/src/overlays/dashboard.rs index a0b531d7..316f66ff 100644 --- a/wayvr/src/overlays/dashboard.rs +++ b/wayvr/src/overlays/dashboard.rs @@ -8,8 +8,8 @@ use wayvr_ipc::{ }; use wgui::{ event::{ - Event as WguiEvent, MouseButtonEvent, MouseButtonIndex, MouseLeaveEvent, MouseMotionEvent, - MouseWheelEvent, + DeviceBitmask, Event as WguiEvent, MouseButtonEvent, MouseButtonIndex, MouseLeaveEvent, + MouseMotionEvent, MouseWheelEvent, }, gfx::cmd::WGfxClearMode, renderer_vk::context::Context as WguiContext, @@ -205,7 +205,7 @@ impl OverlayBackend for DashFrontend { let e = WguiEvent::MouseWheel(MouseWheelEvent { delta: vec2(delta.x, delta.y) / 8.0, pos: hit.uv * self.inner.layout.content_size, - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }); self.push_event(&e); } @@ -213,7 +213,7 @@ impl OverlayBackend for DashFrontend { fn on_hover(&mut self, _app: &mut AppState, hit: &PointerHit) -> HoverResult { let e = &WguiEvent::MouseMotion(MouseMotionEvent { pos: hit.uv * self.inner.layout.content_size, - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }); self.has_focus[hit.pointer] = true; @@ -235,7 +235,9 @@ impl OverlayBackend for DashFrontend { } fn on_left(&mut self, _app: &mut AppState, pointer: usize) { - let e = WguiEvent::MouseLeave(MouseLeaveEvent { device: pointer }); + let e = WguiEvent::MouseLeave(MouseLeaveEvent { + device: DeviceBitmask::from_usize(pointer), + }); self.has_focus[pointer] = false; self.push_event(&e); } @@ -252,13 +254,13 @@ impl OverlayBackend for DashFrontend { WguiEvent::MouseDown(MouseButtonEvent { pos: hit.uv * self.inner.layout.content_size, index, - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }) } else { WguiEvent::MouseUp(MouseButtonEvent { pos: hit.uv * self.inner.layout.content_size, index, - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }) }; self.push_event(&e); @@ -267,11 +269,11 @@ impl OverlayBackend for DashFrontend { if !pressed && !self.has_focus[hit.pointer] { let e = WguiEvent::MouseMotion(MouseMotionEvent { pos: vec2(-1., -1.), - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }); self.push_event(&e); let e = WguiEvent::MouseLeave(MouseLeaveEvent { - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }); self.push_event(&e); } diff --git a/wgui/src/components/button.rs b/wgui/src/components/button.rs index ab2bfda8..e543f059 100644 --- a/wgui/src/components/button.rs +++ b/wgui/src/components/button.rs @@ -334,6 +334,18 @@ fn register_event_mouse_leave( ) } +fn register_event_mouse_cancel(state: Rc>, listeners: &mut EventListenerCollection) -> EventListenerID { + listeners.register( + EventListenerKind::MouseCancel, + Box::new(move |_common, _event_data, (), ()| { + let mut state = state.borrow_mut(); + state.down = false; + state.hovered = false; + Ok(EventResult::Pass) + }), + ) +} + fn register_event_mouse_press(state: Rc>, listeners: &mut EventListenerCollection) -> EventListenerID { listeners.register( EventListenerKind::MousePress, @@ -552,6 +564,7 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul let listeners = &mut root.widget.state().event_listeners; let anim_mult = ess.layout.state.theme.animation_mult; vec![ + register_event_mouse_cancel(state.clone(), listeners), register_event_mouse_enter(data.clone(), state.clone(), listeners, params.tooltip, anim_mult), register_event_mouse_leave(state.clone(), listeners, anim_mult), register_event_mouse_press(state.clone(), listeners), diff --git a/wgui/src/components/checkbox.rs b/wgui/src/components/checkbox.rs index 552e7ed6..4a49e775 100644 --- a/wgui/src/components/checkbox.rs +++ b/wgui/src/components/checkbox.rs @@ -240,6 +240,17 @@ fn register_event_mouse_leave( ) } +fn register_event_mouse_cancel(state: Rc>, listeners: &mut EventListenerCollection) -> EventListenerID { + listeners.register( + EventListenerKind::MouseCancel, + Box::new(move |_common, _event_data, (), ()| { + let mut state = state.borrow_mut(); + state.down = false; + Ok(EventResult::Pass) + }), + ) +} + fn register_event_mouse_press(state: Rc>, listeners: &mut EventListenerCollection) -> EventListenerID { listeners.register( EventListenerKind::MousePress, @@ -439,6 +450,7 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul vec![ register_event_mouse_enter(state.clone(), listeners, params.tooltip, anim_mult), register_event_mouse_leave(state.clone(), listeners, anim_mult), + register_event_mouse_cancel(state.clone(), listeners), register_event_mouse_press(state.clone(), listeners), register_event_mouse_release(data.clone(), state.clone(), listeners), ] diff --git a/wgui/src/components/slider.rs b/wgui/src/components/slider.rs index 0814f329..004d4b22 100644 --- a/wgui/src/components/slider.rs +++ b/wgui/src/components/slider.rs @@ -11,8 +11,8 @@ use crate::{ }, drawing::{self}, event::{ - self, CallbackDataCommon, CallbackMetadata, EventAlterables, EventListenerCollection, EventListenerKind, - StyleSetRequest, + self, CallbackDataCommon, CallbackMetadata, DeviceBitmask, EventAlterables, EventListenerCollection, + EventListenerKind, StyleSetRequest, }, i18n::Translation, layout::{WidgetID, WidgetPair}, @@ -77,7 +77,7 @@ pub struct Params { } struct State { - dragged_by: Option, + dragged_by: Option, hovered: bool, values: ValuesMinMax, on_value_changed: Option, diff --git a/wgui/src/components/tabs.rs b/wgui/src/components/tabs.rs index 293cc00a..de6acb66 100644 --- a/wgui/src/components/tabs.rs +++ b/wgui/src/components/tabs.rs @@ -9,7 +9,7 @@ use crate::{ layout::WidgetPair, widget::{ConstructEssentials, div::WidgetDiv}, }; -use std::{cell::RefCell, fmt::Pointer, rc::Rc}; +use std::{cell::RefCell, rc::Rc}; use taffy::{ AlignItems, prelude::{length, percent}, diff --git a/wgui/src/event.rs b/wgui/src/event.rs index cba47205..5e09650e 100644 --- a/wgui/src/event.rs +++ b/wgui/src/event.rs @@ -19,6 +19,17 @@ use crate::{ widget::{EventResult, WidgetData, WidgetObj}, }; +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +pub struct DeviceBitmask(pub u8); + +impl DeviceBitmask { + pub fn from_usize(mask: usize) -> Self { + // more than 8 input devices? + debug_assert!(mask & !0xff == 0); + Self(mask as u8) + } +} + #[derive(Debug, Clone, Copy)] pub enum MouseButtonIndex { Left, @@ -30,28 +41,28 @@ pub enum MouseButtonIndex { pub struct MouseButtonEvent { pub index: MouseButtonIndex, pub pos: Vec2, - pub device: usize, + pub device: DeviceBitmask, } #[derive(Debug, Clone, Copy)] pub struct MousePosition { pub pos: Vec2, - pub device: usize, + pub device: DeviceBitmask, } pub struct MouseLeaveEvent { - pub device: usize, + pub device: DeviceBitmask, } pub struct MouseMotionEvent { pub pos: Vec2, - pub device: usize, + pub device: DeviceBitmask, } pub struct MouseWheelEvent { pub pos: Vec2, /* mouse position */ pub delta: Vec2, /* wheel delta */ - pub device: usize, + pub device: DeviceBitmask, } #[derive(Clone)] @@ -70,6 +81,7 @@ pub enum Event { MouseMotion(MouseMotionEvent), MouseUp(MouseButtonEvent), MouseWheel(MouseWheelEvent), + MouseCancel, // Called if the user started scrolling by swiping above the button, to cancel all currently pressed buttons (prevent clicks) TextInput(TextInputEvent), } @@ -106,6 +118,7 @@ pub struct EventAlterables { pub dirty_widgets: Vec, pub components_to_refresh_once: Vec, pub style_set_requests: Vec<(WidgetID, StyleSetRequest)>, + pub global_events_to_emit: Vec, pub animations: Vec, pub widgets_to_tick: HashSet, // widgets which needs to be ticked in the next `Layout::update()` fn pub transform_stack: TransformStack, @@ -134,6 +147,10 @@ impl EventAlterables { self.mark_redraw(); } + pub fn emit_global_event(&mut self, event: Event) { + self.global_events_to_emit.push(event); + } + pub fn mark_tick(&mut self, widget_id: WidgetID) { self.widgets_to_tick.insert(widget_id); } @@ -224,9 +241,10 @@ impl CallbackMetadata { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum EventListenerKind { MousePress, - MouseRelease, - MouseEnter, MouseMotion, + MouseRelease, + MouseCancel, + MouseEnter, MouseLeave, TextInput, InternalStateChange, diff --git a/wgui/src/layout.rs b/wgui/src/layout.rs index 0d195e32..6bcda51f 100644 --- a/wgui/src/layout.rs +++ b/wgui/src/layout.rs @@ -11,7 +11,7 @@ use crate::{ drawing::{ self, ANSI_BOLD_CODE, ANSI_RESET_CODE, Boundary, PushScissorStackResult, push_scissor_stack, push_transform_stack, }, - event::{self, CallbackDataCommon, EventAlterables}, + event::{self, CallbackDataCommon, Event, EventAlterables}, globals::WguiGlobals, sound::WguiSoundType, task::Tasks, @@ -173,6 +173,8 @@ pub struct Layout { pub widgets_to_tick: Vec, + global_events_to_emit: Vec, + // *Main root* // contains content_root_widget and topmost widgets pub tree_root_widget: WidgetID, @@ -450,19 +452,25 @@ impl Layout { .as_ref() .is_none_or(PushScissorStackResult::should_display) { - // check children first - self.push_event_children(node_id, event, event_result, alterables, user_data)?; + let res_priority = widget.process_event_priority( + &mut self.get_event_params(l, node_id, style, alterables), + widget_id, + event, + )?; - if event_result.can_propagate() { - let mut params = EventParams { - state: &self.state, - layout: l, - alterables, - node_id, - style, - }; + if res_priority.can_propagate() { + // check children first + self.push_event_children(node_id, event, event_result, alterables, user_data)?; - widget.process_event(widget_id, node_id, event, event_result, user_data, &mut params)?; + if event_result.can_propagate() { + widget.process_event( + &mut self.get_event_params(l, node_id, style, alterables), + widget_id, + event, + event_result, + user_data, + )?; + } } } @@ -474,6 +482,22 @@ impl Layout { Ok(()) } + fn get_event_params<'a>( + &'a self, + l: &'a taffy::Layout, + node_id: taffy::NodeId, + style: &'a taffy::Style, + alterables: &'a mut EventAlterables, + ) -> EventParams<'a> { + EventParams { + node_id, + style, + state: &self.state, + alterables, + taffy_layout: l, + } + } + pub const fn check_toggle_needs_redraw(&mut self) -> bool { if self.needs_redraw { self.needs_redraw = false; @@ -508,6 +532,18 @@ impl Layout { &mut (user1, user2), )?; self.process_alterables(alterables)?; + + let mut alterables = EventAlterables::default(); + for event in std::mem::take(&mut self.global_events_to_emit) { + self.push_event_widget( + self.tree_root_node, + &event, + &mut event_result, + &mut alterables, + &mut (user1, user2), + )?; + } + Ok(event_result) } @@ -580,6 +616,7 @@ impl Layout { sounds_to_play_once: Vec::new(), focused_component: None, alterables: Default::default(), + global_events_to_emit: Vec::new(), }) } @@ -815,10 +852,12 @@ impl Layout { } } - if !alterables.widgets_to_tick.is_empty() { - for widget_id in &alterables.widgets_to_tick { - self.widgets_to_tick.push(*widget_id); - } + for widget_id in alterables.widgets_to_tick { + self.widgets_to_tick.push(widget_id); + } + + for event in alterables.global_events_to_emit { + self.global_events_to_emit.push(event); } for c in alterables.components_to_refresh_once { diff --git a/wgui/src/widget/mod.rs b/wgui/src/widget/mod.rs index 08f7aeda..961d916d 100644 --- a/wgui/src/widget/mod.rs +++ b/wgui/src/widget/mod.rs @@ -1,5 +1,5 @@ use anyhow::Context; -use glam::Vec2; +use glam::{FloatExt, Vec2}; use taffy::{NodeId, TaffyTree}; use super::drawing::RenderPrimitive; @@ -8,7 +8,8 @@ use crate::{ any::AnyTrait, drawing::{self, PrimitiveExtent}, event::{ - self, CallbackData, CallbackDataCommon, CallbackMetadata, Event, EventAlterables, EventListenerCollection, + self, CallbackData, CallbackDataCommon, CallbackMetadata, DeviceBitmask, Event, EventAlterables, + EventListenerCollection, EventListenerKind::{self, InternalStateChange, MouseLeave}, MouseWheelEvent, }, @@ -25,57 +26,66 @@ pub mod rectangle; pub mod sprite; pub mod util; +const SWIPE_START_THRESHOLD_UNITS: f32 = 16.0; +const KINETIC_VELOCITY_DAMPING: f32 = 0.92; +const KINETIC_VELOCITY_BRAKE: f32 = 0.85; +const RUBBERBANDING_DAMP: f32 = 0.8; + pub struct WidgetData { - hovered: usize, - pressed: usize, - pub scrolling_target: Vec2, // normalized, 0.0-1.0. Not used in case if overflow != scroll - pub scrolling_cur: Vec2, // normalized, used for smooth scrolling animation - pub scrolling_cur_prev: Vec2, // for motion interpolation while rendering between ticks + hovered: DeviceBitmask, + pressed: DeviceBitmask, + scrolling_target: Vec2, // normalized, 0.0-1.0. Not used in case if overflow != scroll + scrolling_cur: Vec2, // normalized, used for smooth scrolling animation + scrolling_cur_prev: Vec2, // for motion interpolation while rendering between ticks + scrolling_velocity: Vec2, + press_down_start_mouse_pos: Option, + swipe_running: bool, + swipe_scroll_start: Vec2, // normalized, 0.0-1.0 pub transform: glam::Mat4, pub cached_absolute_boundary: drawing::Boundary, // updated in Layout::push_event_widget } impl WidgetData { - pub const fn set_device_pressed(&mut self, device: usize, pressed: bool) -> bool { - let bit = 1 << device; + pub const fn set_device_pressed(&mut self, device: DeviceBitmask, pressed: bool) -> bool { + let bit = 1 << device.0; let state_changed; if pressed { - state_changed = self.pressed == 0; - self.pressed |= bit; + state_changed = self.pressed.0 == 0; + self.pressed.0 |= bit; } else { - state_changed = self.pressed == bit; - self.pressed &= !bit; + state_changed = self.pressed.0 == bit; + self.pressed.0 &= !bit; } state_changed } - pub const fn set_device_hovered(&mut self, device: usize, hovered: bool) -> bool { - let bit = 1 << device; + pub const fn set_device_hovered(&mut self, device: DeviceBitmask, hovered: bool) -> bool { + let bit = 1 << device.0; let state_changed; if hovered { - state_changed = self.hovered == 0; - self.hovered |= bit; + state_changed = self.hovered.0 == 0; + self.hovered.0 |= bit; } else { - state_changed = self.hovered == bit; - self.hovered &= !bit; + state_changed = self.hovered.0 == bit; + self.hovered.0 &= !bit; } state_changed } - pub const fn get_pressed(&self, device: usize) -> bool { - self.pressed & (1 << device) != 0 + pub const fn get_pressed(&self, device: DeviceBitmask) -> bool { + self.pressed.0 & (1 << device.0) != 0 } - pub const fn get_hovered(&self, device: usize) -> bool { - self.hovered & (1 << device) != 0 + pub const fn get_hovered(&self, device: DeviceBitmask) -> bool { + self.hovered.0 & (1 << device.0) != 0 } pub const fn is_pressed(&self) -> bool { - self.pressed != 0 + self.pressed.0 != 0 } pub const fn is_hovered(&self) -> bool { - self.hovered != 0 + self.hovered.0 != 0 } } @@ -116,13 +126,17 @@ impl WidgetState { fn new(flags: WidgetStateFlags, obj: Box) -> Self { Self { data: WidgetData { - hovered: 0, - pressed: 0, + hovered: DeviceBitmask(0), + pressed: DeviceBitmask(0), scrolling_target: Vec2::default(), scrolling_cur: Vec2::default(), scrolling_cur_prev: Vec2::default(), transform: glam::Mat4::IDENTITY, cached_absolute_boundary: drawing::Boundary::default(), + press_down_start_mouse_pos: None, + swipe_running: false, + swipe_scroll_start: Vec2::default(), + scrolling_velocity: Vec2::default(), }, obj, event_listeners: EventListenerCollection::default(), @@ -192,7 +206,7 @@ pub struct EventParams<'a> { pub style: &'a taffy::Style, pub state: &'a LayoutState, pub alterables: &'a mut EventAlterables, - pub layout: &'a taffy::Layout, + pub taffy_layout: &'a taffy::Layout, } #[derive(Clone, Copy, Eq, PartialEq, PartialOrd)] @@ -221,6 +235,22 @@ fn get_scroll_enabled(style: &taffy::Style) -> (bool, bool) { ) } +const OVERFLOW_START_THRESHOLD_UNITS: f32 = 3.0; // don't show scrollbars for nearly non-scrollable lists + +fn get_scroll_active_axis(style: &taffy::Style, taffy_layout: &taffy::Layout) -> (bool, bool) { + let (enabled_horiz, enabled_vert) = get_scroll_enabled(style); + if !enabled_horiz && !enabled_vert { + return (false, false); + } + + let overflow = Vec2::new(taffy_layout.scroll_width(), taffy_layout.scroll_height()); + + ( + overflow.x >= OVERFLOW_START_THRESHOLD_UNITS, + overflow.y >= OVERFLOW_START_THRESHOLD_UNITS, + ) +} + pub struct ScrollbarInfo { // total contents size of the currently scrolling widget content_size: Vec2, @@ -229,15 +259,13 @@ pub struct ScrollbarInfo { handle_size: Vec2, } -pub fn get_scrollbar_info(l: &taffy::Layout) -> Option { - let overflow_start_threshold_units = 3.0; // don't show scrollbars for nearly non-scrollable lists - - let overflow = Vec2::new(l.scroll_width(), l.scroll_height()); - if overflow.x < overflow_start_threshold_units && overflow.y < overflow_start_threshold_units { +pub fn get_scrollbar_info(taffy_layout: &taffy::Layout) -> Option { + let overflow = Vec2::new(taffy_layout.scroll_width(), taffy_layout.scroll_height()); + if overflow.x < OVERFLOW_START_THRESHOLD_UNITS && overflow.y < OVERFLOW_START_THRESHOLD_UNITS { return None; // not overflowing } - let content_size = Vec2::new(l.content_size.width, l.content_size.height); + let content_size = Vec2::new(taffy_layout.content_size.width, taffy_layout.content_size.height); let handle_size = 1.0 - (overflow / content_size); Some(ScrollbarInfo { @@ -246,6 +274,15 @@ pub fn get_scrollbar_info(l: &taffy::Layout) -> Option { }) } +impl ScrollbarInfo { + fn get_potential_scroll_axis_multiplier(&self, taffy_layout: &taffy::Layout) -> Vec2 { + Vec2::new( + self.content_size.x - taffy_layout.content_box_width(), + self.content_size.y - taffy_layout.content_box_height(), + ) + } +} + impl dyn WidgetObj { pub fn get_as(&self) -> Option<&T> { let any = self.as_any(); @@ -348,6 +385,46 @@ impl WidgetState { let scrolling_cur_prev = &mut self.data.scrolling_cur_prev; let scrolling_target = &mut self.data.scrolling_target; + let mut perform_next_tick = false; + + // check for boundaries + if !self.data.swipe_running { + // top bound + if scrolling_target.y < 0.0 { + self.data.scrolling_velocity.y *= KINETIC_VELOCITY_BRAKE; + scrolling_target.y *= RUBBERBANDING_DAMP; + perform_next_tick = true; + } + + // left bound + if scrolling_target.x < 0.0 { + self.data.scrolling_velocity.x *= KINETIC_VELOCITY_BRAKE; + scrolling_target.x *= RUBBERBANDING_DAMP; + perform_next_tick = true; + } + + // right bound + if scrolling_target.x > 1.0 { + self.data.scrolling_velocity.x *= KINETIC_VELOCITY_BRAKE; + scrolling_target.x = 1.0.lerp(scrolling_target.x, RUBBERBANDING_DAMP); + perform_next_tick = true; + } + + // bottom bound + if scrolling_target.y > 1.0 { + self.data.scrolling_velocity.y *= KINETIC_VELOCITY_BRAKE; + scrolling_target.y = 1.0.lerp(scrolling_target.y, RUBBERBANDING_DAMP); + perform_next_tick = true; + } + } + + // Perform kinetic velocity animation + if self.data.scrolling_velocity.length_squared() > 1e-9 { + *scrolling_target += self.data.scrolling_velocity; + self.data.scrolling_velocity *= KINETIC_VELOCITY_DAMPING; + perform_next_tick = true; + } + *scrolling_cur_prev = *scrolling_cur; if scrolling_cur != scrolling_target { @@ -355,8 +432,7 @@ impl WidgetState { *scrolling_cur = scrolling_cur.lerp(*scrolling_target, 0.2); // trigger tick request again - alterables.mark_tick(this_widget_id); - alterables.mark_redraw(); + perform_next_tick = true; let epsilon = 0.00001; if (scrolling_cur.x - scrolling_target.x).abs() < epsilon @@ -365,6 +441,11 @@ impl WidgetState { *scrolling_cur = *scrolling_target; } } + + if perform_next_tick { + alterables.mark_tick(this_widget_id); + alterables.mark_redraw(); + } } pub fn draw_scrollbars(&mut self, state: &mut DrawState, params: &DrawParams, info: &ScrollbarInfo) { @@ -432,13 +513,13 @@ impl WidgetState { return false; } - let l = params.layout; + let l = params.taffy_layout; let overflow = Vec2::new(l.scroll_width(), l.scroll_height()); if overflow.x == 0.0 && overflow.y == 0.0 { return false; // not overflowing } - let Some(info) = get_scrollbar_info(params.layout) else { + let Some(info) = get_scrollbar_info(l) else { return false; }; @@ -451,7 +532,7 @@ impl WidgetState { } let mult = (1.0 / (content_box_length - content_length)) * step_pixels; - let new_scroll = (*scrolling_target + wheel_delta * mult).clamp(0.0, 1.0); + let new_scroll = *scrolling_target + wheel_delta * mult; if *scrolling_target == new_scroll { return; } @@ -479,20 +560,81 @@ impl WidgetState { true } + // this is called before calling children of this widget + pub fn process_event_priority( + &mut self, + params: &mut EventParams, + widget_id: WidgetID, + event: &Event, + ) -> anyhow::Result { + match &event { + Event::MouseDown(e) => { + // firstly, check if this widget is scrollable at all + let (active_x, active_y) = get_scroll_active_axis(¶ms.style, ¶ms.taffy_layout); + if active_x || active_y { + self.data.press_down_start_mouse_pos = Some(e.pos); + self.data.swipe_scroll_start = self.data.scrolling_target; + } + } + Event::MouseUp(_e) => { + if self.data.swipe_running { + self.data.swipe_running = false; + let kinetic_force = (self.data.scrolling_cur - self.data.scrolling_cur_prev) * 20.0; + self.data.scrolling_velocity += kinetic_force; + params.alterables.mark_tick(widget_id); + params.alterables.mark_redraw(); + } + self.data.press_down_start_mouse_pos = None; + } + Event::MouseMotion(e) => { + if let Some(start_mouse_pos) = &self.data.press_down_start_mouse_pos { + let (active_x, active_y) = get_scroll_active_axis(¶ms.style, ¶ms.taffy_layout); + + if !self.data.swipe_running { + if (active_x && (e.pos.x - start_mouse_pos.x).abs() >= SWIPE_START_THRESHOLD_UNITS) + || (active_y && (e.pos.y - start_mouse_pos.y).abs() >= SWIPE_START_THRESHOLD_UNITS) + { + self.data.swipe_running = true; + params.alterables.emit_global_event(Event::MouseCancel); + } + } + + if self.data.swipe_running + && let Some(scrollbar_info) = get_scrollbar_info(params.taffy_layout) + { + let mouse_diff = e.pos - start_mouse_pos; + + let mult = scrollbar_info.get_potential_scroll_axis_multiplier(params.taffy_layout); + + let scroll_diff_x = if mult.x != 0.0 { -mouse_diff.x / mult.x } else { 0.0 }; + let scroll_diff_y = if mult.y != 0.0 { -mouse_diff.y / mult.y } else { 0.0 }; + + self.data.scrolling_target = self.data.swipe_scroll_start + Vec2::new(scroll_diff_x, scroll_diff_y); + params.alterables.mark_tick(self.obj.get_id()); + + return Ok(EventResult::Consumed); + } + } + } + _ => {} + } + + Ok(EventResult::Pass) + } + pub fn process_event<'a, 'b, U1: 'static, U2: 'static>( &mut self, + params: &'a mut EventParams<'a>, widget_id: WidgetID, - node_id: taffy::NodeId, event: &Event, event_result: &'a mut EventResult, user_data: &'a mut (&'b mut U1, &'b mut U2), - params: &'a mut EventParams<'a>, ) -> anyhow::Result<()> { let hovered = event.test_mouse_within_transform(params.alterables.transform_stack.get()); let mut invoke_data = InvokeData { widget_id, - node_id, + node_id: params.node_id, event_result, user_data, params, @@ -508,6 +650,9 @@ impl WidgetState { CallbackMetadata::TextInput(e.clone()), )?); } + Event::MouseCancel => { + res = Some(self.invoke_listeners(&mut invoke_data, EventListenerKind::MouseCancel, CallbackMetadata::None)?); + } Event::MouseDown(e) => { if hovered && self.data.set_device_pressed(e.device, true) { res = Some(self.invoke_listeners( @@ -528,7 +673,6 @@ impl WidgetState { } Event::MouseMotion(e) => { let hover_state_changed = self.data.set_device_hovered(e.device, hovered); - if hover_state_changed { if self.data.is_hovered() { res = From 10b14794ed90fc9f7aed4bc80312610883d08b04 Mon Sep 17 00:00:00 2001 From: galister <22305755+galister@users.noreply.github.com> Date: Sun, 3 May 2026 14:36:49 +0900 Subject: [PATCH 31/36] update to upstream pipewire-rs (#508) * update to upstream pipewire-rs * CI to use 24.04 without no pipewire ppa * fix build without openxr feature --- .github/workflows/build-all-features.yml | 2 +- .github/workflows/build-appimage.yml | 2 +- .github/workflows/build-default.yml | 2 +- .github/workflows/build-wayland-openvr.yml | 2 +- .github/workflows/build-wayland-openxr.yml | 2 +- .github/workflows/build-x11-openvr.yml | 2 +- .github/workflows/build-x11-openxr.yml | 2 +- .github/workflows/make-release.yml | 2 +- .../workflows/scripts/appimage_prepare_env.sh | 2 - Cargo.lock | 126 ++++++------------ wayvr/src/overlays/dashboard.rs | 3 + wlx-capture/Cargo.toml | 4 +- wlx-capture/src/pipewire.rs | 60 +++++---- 13 files changed, 86 insertions(+), 125 deletions(-) diff --git a/.github/workflows/build-all-features.yml b/.github/workflows/build-all-features.yml index ea748df3..81267f63 100644 --- a/.github/workflows/build-all-features.yml +++ b/.github/workflows/build-all-features.yml @@ -11,7 +11,7 @@ env: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 defaults: run: working-directory: ./wayvr diff --git a/.github/workflows/build-appimage.yml b/.github/workflows/build-appimage.yml index 9eb37b2c..4776d417 100644 --- a/.github/workflows/build-appimage.yml +++ b/.github/workflows/build-appimage.yml @@ -15,7 +15,7 @@ env: jobs: build_appimage: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 defaults: run: working-directory: ./wayvr diff --git a/.github/workflows/build-default.yml b/.github/workflows/build-default.yml index a20e5dfa..3ad4d140 100644 --- a/.github/workflows/build-default.yml +++ b/.github/workflows/build-default.yml @@ -11,7 +11,7 @@ env: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 defaults: run: working-directory: ./wayvr diff --git a/.github/workflows/build-wayland-openvr.yml b/.github/workflows/build-wayland-openvr.yml index 9b540dc5..dd3beb57 100644 --- a/.github/workflows/build-wayland-openvr.yml +++ b/.github/workflows/build-wayland-openvr.yml @@ -11,7 +11,7 @@ env: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 defaults: run: working-directory: ./wayvr diff --git a/.github/workflows/build-wayland-openxr.yml b/.github/workflows/build-wayland-openxr.yml index 42a5c33c..9ff5c9d4 100644 --- a/.github/workflows/build-wayland-openxr.yml +++ b/.github/workflows/build-wayland-openxr.yml @@ -11,7 +11,7 @@ env: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 defaults: run: working-directory: ./wayvr diff --git a/.github/workflows/build-x11-openvr.yml b/.github/workflows/build-x11-openvr.yml index cacd5e5c..2d54b81f 100644 --- a/.github/workflows/build-x11-openvr.yml +++ b/.github/workflows/build-x11-openvr.yml @@ -11,7 +11,7 @@ env: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 defaults: run: working-directory: ./wayvr diff --git a/.github/workflows/build-x11-openxr.yml b/.github/workflows/build-x11-openxr.yml index 7e045dee..697f0806 100644 --- a/.github/workflows/build-x11-openxr.yml +++ b/.github/workflows/build-x11-openxr.yml @@ -11,7 +11,7 @@ env: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 defaults: run: working-directory: ./wayvr diff --git a/.github/workflows/make-release.yml b/.github/workflows/make-release.yml index 3225e372..01286bc0 100644 --- a/.github/workflows/make-release.yml +++ b/.github/workflows/make-release.yml @@ -13,7 +13,7 @@ env: jobs: make_release: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 defaults: run: working-directory: ./wayvr diff --git a/.github/workflows/scripts/appimage_prepare_env.sh b/.github/workflows/scripts/appimage_prepare_env.sh index 7b7f14f3..857d81c7 100755 --- a/.github/workflows/scripts/appimage_prepare_env.sh +++ b/.github/workflows/scripts/appimage_prepare_env.sh @@ -1,7 +1,5 @@ #!/bin/sh -sudo add-apt-repository -syn universe -sudo add-apt-repository -syn ppa:pipewire-debian/pipewire-upstream || sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 25088A0359807596 sudo apt-get update sudo apt-get install -y fuse cmake pkg-config fontconfig libasound2-dev libxkbcommon-dev libxkbcommon-x11-0 libxkbcommon-x11-dev libopenxr-dev libfontconfig-dev libdbus-1-dev libpipewire-0.3-0 libpipewire-0.3-dev libspa-0.2-dev libx11-6 libxext6 libxrandr2 libx11-dev libxext-dev libxrandr-dev libopenvr-dev libopenvr-api1 libwayland-dev libegl-dev libxcb-glx0 libxcb-glx0-dev rustup update diff --git a/Cargo.lock b/Cargo.lock index dcfac810..0be28f52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,6 +143,16 @@ dependencies = [ "libc", ] +[[package]] +name = "annotate-snippets" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4" +dependencies = [ + "anstyle", + "unicode-width 0.2.2", +] + [[package]] name = "anstream" version = "0.6.21" @@ -677,6 +687,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ + "annotate-snippets", "bitflags 2.10.0", "cexpr", "clang-sys", @@ -880,9 +891,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.15.8" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" dependencies = [ "smallvec", "target-lexicon", @@ -1051,7 +1062,7 @@ dependencies = [ "serde-untagged", "serde_core", "serde_json", - "toml 0.9.10+spec-1.1.0", + "toml", "winnow", "yaml-rust2", ] @@ -1096,9 +1107,6 @@ name = "cookie-factory" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" -dependencies = [ - "futures", -] [[package]] name = "core-foundation" @@ -2881,24 +2889,23 @@ dependencies = [ [[package]] name = "libspa" -version = "0.8.0" -source = "git+https://gitlab.freedesktop.org/galister/pipewire-rs.git?rev=6ef2cd1af41e5e8844c63c37ea9a01f0758079b0#6ef2cd1af41e5e8844c63c37ea9a01f0758079b0" +version = "0.9.2" +source = "git+https://gitlab.freedesktop.org/pipewire/pipewire-rs.git?rev=670b0aabf04263ece5dc1e752e5d64aeae3056e8#670b0aabf04263ece5dc1e752e5d64aeae3056e8" dependencies = [ "bitflags 2.10.0", "cc", - "convert_case 0.6.0", "cookie-factory", "libc", "libspa-sys", - "nix 0.27.1", - "nom 7.1.3", + "nom 8.0.0", + "rustix 1.1.3", "system-deps", ] [[package]] name = "libspa-sys" -version = "0.8.0" -source = "git+https://gitlab.freedesktop.org/galister/pipewire-rs.git?rev=6ef2cd1af41e5e8844c63c37ea9a01f0758079b0#6ef2cd1af41e5e8844c63c37ea9a01f0758079b0" +version = "0.9.2" +source = "git+https://gitlab.freedesktop.org/pipewire/pipewire-rs.git?rev=670b0aabf04263ece5dc1e752e5d64aeae3056e8#670b0aabf04263ece5dc1e752e5d64aeae3056e8" dependencies = [ "bindgen", "cc", @@ -3167,17 +3174,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nix" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "libc", -] - [[package]] name = "nix" version = "0.29.0" @@ -3991,24 +3987,21 @@ dependencies = [ [[package]] name = "pipewire" -version = "0.8.0" -source = "git+https://gitlab.freedesktop.org/galister/pipewire-rs.git?rev=6ef2cd1af41e5e8844c63c37ea9a01f0758079b0#6ef2cd1af41e5e8844c63c37ea9a01f0758079b0" +version = "0.9.2" +source = "git+https://gitlab.freedesktop.org/pipewire/pipewire-rs.git?rev=670b0aabf04263ece5dc1e752e5d64aeae3056e8#670b0aabf04263ece5dc1e752e5d64aeae3056e8" dependencies = [ - "anyhow", "bitflags 2.10.0", "libc", "libspa", "libspa-sys", - "nix 0.27.1", - "once_cell", "pipewire-sys", - "thiserror 1.0.69", + "rustix 1.1.3", ] [[package]] name = "pipewire-sys" -version = "0.8.0" -source = "git+https://gitlab.freedesktop.org/galister/pipewire-rs.git?rev=6ef2cd1af41e5e8844c63c37ea9a01f0758079b0#6ef2cd1af41e5e8844c63c37ea9a01f0758079b0" +version = "0.9.2" +source = "git+https://gitlab.freedesktop.org/pipewire/pipewire-rs.git?rev=670b0aabf04263ece5dc1e752e5d64aeae3056e8#670b0aabf04263ece5dc1e752e5d64aeae3056e8" dependencies = [ "bindgen", "libspa-sys", @@ -4116,7 +4109,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.23.10+spec-1.0.0", + "toml_edit", ] [[package]] @@ -4946,15 +4939,6 @@ dependencies = [ "syn 2.0.113", ] -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - [[package]] name = "serde_spanned" version = "1.0.4" @@ -5454,14 +5438,14 @@ dependencies = [ [[package]] name = "system-deps" -version = "6.2.2" +version = "7.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f" dependencies = [ "cfg-expr", "heck 0.5.0", "pkg-config", - "toml 0.8.23", + "toml", "version-compare", ] @@ -5479,9 +5463,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.16" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" [[package]] name = "tempfile" @@ -5684,40 +5668,21 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_edit 0.22.27", -] - [[package]] name = "toml" version = "0.9.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" dependencies = [ + "indexmap 2.12.1", "serde_core", - "serde_spanned 1.0.4", - "toml_datetime 0.7.5+spec-1.1.0", + "serde_spanned", + "toml_datetime", "toml_parser", + "toml_writer", "winnow", ] -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" @@ -5727,19 +5692,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap 2.12.1", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "winnow", -] - [[package]] name = "toml_edit" version = "0.23.10+spec-1.0.0" @@ -5747,7 +5699,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap 2.12.1", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime", "toml_parser", "winnow", ] @@ -5761,6 +5713,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + [[package]] name = "tracing" version = "0.1.44" diff --git a/wayvr/src/overlays/dashboard.rs b/wayvr/src/overlays/dashboard.rs index 316f66ff..a3eaf6b0 100644 --- a/wayvr/src/overlays/dashboard.rs +++ b/wayvr/src/overlays/dashboard.rs @@ -481,7 +481,10 @@ impl DashInterface for DashInterfaceLive { fn get_feats(&mut self, data: &mut AppState) -> dash_interface::InterfaceFeats { dash_interface::InterfaceFeats { openxr: matches!(data.xr_backend, XrBackend::OpenXR), + #[cfg(feature = "openxr")] monado: data.monado_state.is_some(), + #[cfg(not(feature = "openxr"))] + monado: false, } } diff --git a/wlx-capture/Cargo.toml b/wlx-capture/Cargo.toml index 8ff3a9e0..5294556e 100644 --- a/wlx-capture/Cargo.toml +++ b/wlx-capture/Cargo.toml @@ -32,8 +32,8 @@ drm-fourcc = "2.2.0" idmap = { workspace = true } libc = "0.2.178" log = { workspace = true } -pipewire = { git = "https://gitlab.freedesktop.org/galister/pipewire-rs.git", rev = "6ef2cd1af41e5e8844c63c37ea9a01f0758079b0", features = [ - "v0_3_33", +pipewire = { git = "https://gitlab.freedesktop.org/pipewire/pipewire-rs.git", rev = "670b0aabf04263ece5dc1e752e5d64aeae3056e8", features = [ + "v0_3_33", "v0_3_65", ], optional = true } #pipewire = { version = "0.8.0", features = ["v0_3_33"], optional = true } rxscreen = { version = "0.1.7", features = [ diff --git a/wlx-capture/src/pipewire.rs b/wlx-capture/src/pipewire.rs index 3d5de1cf..f3e5cdd5 100644 --- a/wlx-capture/src/pipewire.rs +++ b/wlx-capture/src/pipewire.rs @@ -17,14 +17,17 @@ use drm_fourcc::DrmFormat; use drm_fourcc::DrmFourcc; use drm_fourcc::DrmModifier; use pipewire as pw; +use pipewire::context::ContextRc; +use pipewire::main_loop::MainLoopRc; +use pipewire::spa::buffer::meta::MetaVideoTransformValue; +use pipewire::spa::buffer::meta::{MetaCursor, MetaHeader, MetaHeaderFlags, MetaVideoTransform}; +use pipewire::stream::StreamRc; use pw::spa; +use pw::Error; use pw::properties::properties; use pw::stream::{Stream, StreamFlags}; -use pw::{Error, context::Context, main_loop::MainLoop}; use spa::buffer::DataType; -use spa::buffer::MetaData; -use spa::buffer::MetaType; use spa::param::ParamType; use spa::param::video::VideoFormat; use spa::param::video::VideoInfoRaw; @@ -278,12 +281,12 @@ where R: Any, { log::debug!("{}: pipewire main_loop start", &name); - let main_loop = MainLoop::new(None)?; - let context = Context::new(&main_loop)?; - let core = context.connect(None)?; + let main_loop = MainLoopRc::new(None)?; + let context = ContextRc::new(&main_loop, None)?; + let core = context.connect_rc(None)?; - let stream = Stream::new( - &core, + let stream = StreamRc::new( + core, &name, properties! { *pw::keys::MEDIA_TYPE => "Video", @@ -377,36 +380,35 @@ where } if let Some(mut buffer) = maybe_buffer { - if let MetaData::Header(header) = buffer.find_meta_data(MetaType::Header) - && header.flags & spa::sys::SPA_META_HEADER_FLAG_CORRUPTED != 0 + if let Some(header) = buffer.find_meta::() + && header.flags().contains(MetaHeaderFlags::CORRUPTED) { log::warn!("{}: PipeWire buffer is corrupt.", &name); return; } - if let MetaData::VideoTransform(transform) = - buffer.find_meta_data(MetaType::VideoTransform) - { - format.transform = match transform.transform { - spa::sys::SPA_META_TRANSFORMATION_None => Transform::Normal, - spa::sys::SPA_META_TRANSFORMATION_90 => Transform::Rotated90, - spa::sys::SPA_META_TRANSFORMATION_180 => Transform::Rotated180, - spa::sys::SPA_META_TRANSFORMATION_270 => Transform::Rotated270, - spa::sys::SPA_META_TRANSFORMATION_Flipped => Transform::Flipped, - spa::sys::SPA_META_TRANSFORMATION_Flipped90 => Transform::Flipped90, - spa::sys::SPA_META_TRANSFORMATION_Flipped180 => Transform::Flipped180, - spa::sys::SPA_META_TRANSFORMATION_Flipped270 => Transform::Flipped270, + + if let Some(transform) = buffer.find_meta::() { + format.transform = match transform.transform() { + MetaVideoTransformValue::NONE => Transform::Normal, + MetaVideoTransformValue::ROTATED90 => Transform::Rotated90, + MetaVideoTransformValue::ROTATED180 => Transform::Rotated180, + MetaVideoTransformValue::ROTATED270 => Transform::Rotated270, + MetaVideoTransformValue::FLIPPED => Transform::Flipped, + MetaVideoTransformValue::FLIPPED90 => Transform::Flipped90, + MetaVideoTransformValue::FLIPPED180 => Transform::Flipped180, + MetaVideoTransformValue::FLIPPED270 => Transform::Flipped270, _ => Transform::Undefined, }; log::debug!("{}: Transform: {:?}", &name, &format.transform); } - let mouse_meta = match buffer.find_meta_data(MetaType::Cursor) { - MetaData::Cursor(cursor) if cursor.id != 0 => Some(MouseMeta { - x: cursor.position.x as f32 / format.width as f32, - y: cursor.position.y as f32 / format.height as f32, - }), - _ => None, - }; + let mouse_meta = buffer + .find_meta::() + .filter(|c| c.is_valid()) + .map(|c| MouseMeta { + x: c.position().x as f32 / format.width as f32, + y: c.position().y as f32 / format.height as f32, + }); let datas = buffer.datas_mut(); if datas.is_empty() { From 45af52ddf94ad114bf4f5876c1bb7b20bd35b92b Mon Sep 17 00:00:00 2001 From: Aleksander Date: Sun, 3 May 2026 18:54:31 +0200 Subject: [PATCH 32/36] wgui: Range sliders, fix swipe scrolling <-> slider conflict --- dash-frontend/src/tab/monado.rs | 22 +- dash-frontend/src/views/audio_settings.rs | 2 +- uidev/assets/gui/various_widgets.xml | 5 + wayvr/src/gui/panel/mod.rs | 2 +- wayvr/src/overlays/edit/mod.rs | 6 +- wgui/src/components/color_selector.rs | 6 +- wgui/src/components/slider.rs | 406 ++++++++++++++++------ wgui/src/parser/component_slider.rs | 16 +- wgui/src/widget/mod.rs | 15 +- 9 files changed, 336 insertions(+), 144 deletions(-) diff --git a/dash-frontend/src/tab/monado.rs b/dash-frontend/src/tab/monado.rs index ade22f89..a9c5332e 100644 --- a/dash-frontend/src/tab/monado.rs +++ b/dash-frontend/src/tab/monado.rs @@ -283,7 +283,7 @@ impl SubtabGeneralSettings { // get brightness let slider_brightness = state.fetch_component_as::("slider_brightness")?; if let Some(brightness) = frontend.interface.monado_brightness_get(data) { - slider_brightness.set_value(&mut frontend.layout.common(), brightness * 100.0); + slider_brightness.set_value_primary(&mut frontend.layout.common(), brightness * 100.0); slider_brightness.on_value_changed({ let tasks = tasks.clone(); @@ -308,11 +308,11 @@ impl SubtabGeneralSettings { // set initial values let (rgb, range_h, range_s, range_v) = config.chroma_key_params.get_rgb_and_hsv_ranges(); - slider_keying_curve.set_value(&mut common, config.chroma_key_params.curve * SLIDER_MULTIPLIER); - slider_keying_despill.set_value(&mut common, config.chroma_key_params.despill * SLIDER_MULTIPLIER); - slider_keying_hue_range.set_value(&mut common, range_h * SLIDER_MULTIPLIER); - slider_keying_saturation_range.set_value(&mut common, range_s * SLIDER_MULTIPLIER); - slider_keying_value_range.set_value(&mut common, range_v * SLIDER_MULTIPLIER); + slider_keying_curve.set_value_primary(&mut common, config.chroma_key_params.curve * SLIDER_MULTIPLIER); + slider_keying_despill.set_value_primary(&mut common, config.chroma_key_params.despill * SLIDER_MULTIPLIER); + slider_keying_hue_range.set_value_primary(&mut common, range_h * SLIDER_MULTIPLIER); + slider_keying_saturation_range.set_value_primary(&mut common, range_s * SLIDER_MULTIPLIER); + slider_keying_value_range.set_value_primary(&mut common, range_v * SLIDER_MULTIPLIER); cs_keying.set_color(&mut common, rgb); // prepare callbacks @@ -353,11 +353,11 @@ impl SubtabGeneralSettings { } fn chroma_update(&mut self, config: &mut GeneralConfig) { - let val_curve = self.slider_keying_curve.get_value(); - let val_despill = self.slider_keying_despill.get_value(); - let val_range_h = self.slider_keying_hue_range.get_value(); - let val_range_s = self.slider_keying_saturation_range.get_value(); - let val_range_v = self.slider_keying_value_range.get_value(); + let val_curve = self.slider_keying_curve.get_value_primary(); + let val_despill = self.slider_keying_despill.get_value_primary(); + let val_range_h = self.slider_keying_hue_range.get_value_primary(); + let val_range_s = self.slider_keying_saturation_range.get_value_primary(); + let val_range_v = self.slider_keying_value_range.get_value_primary(); let val_rgb = self.cs_keying.get_color(); config.chroma_key_params.despill = val_despill / SLIDER_MULTIPLIER; diff --git a/dash-frontend/src/views/audio_settings.rs b/dash-frontend/src/views/audio_settings.rs index d3900bf3..9546d961 100644 --- a/dash-frontend/src/views/audio_settings.rs +++ b/dash-frontend/src/views/audio_settings.rs @@ -804,7 +804,7 @@ impl View { let btn_mute = data.fetch_component_as::("btn_mute")?; let slider = data.fetch_component_as::("slider")?; - slider.set_value(&mut common, params.control.on_volume_request()? / VOLUME_MULT); + slider.set_value_primary(&mut common, params.control.on_volume_request()? / VOLUME_MULT); checkbox.set_checked(&mut common, params.checked); diff --git a/uidev/assets/gui/various_widgets.xml b/uidev/assets/gui/various_widgets.xml index 163e7391..b2eea786 100644 --- a/uidev/assets/gui/various_widgets.xml +++ b/uidev/assets/gui/various_widgets.xml @@ -93,6 +93,11 @@ + + +
diff --git a/wayvr/src/gui/panel/mod.rs b/wayvr/src/gui/panel/mod.rs index 04d067a1..c135e58a 100644 --- a/wayvr/src/gui/panel/mod.rs +++ b/wayvr/src/gui/panel/mod.rs @@ -581,7 +581,7 @@ pub fn apply_custom_command( .fetch_component_as::(element) { let value_f32 = value_str.parse::().context("Not a valid number")?; - slider.set_value(&mut com, value_f32); + slider.set_value_primary(&mut com, value_f32); } else if let Ok(radio) = panel .parser_state .fetch_component_as::(element) diff --git a/wayvr/src/overlays/edit/mod.rs b/wayvr/src/overlays/edit/mod.rs index aa0c5169..d9f913b2 100644 --- a/wayvr/src/overlays/edit/mod.rs +++ b/wayvr/src/overlays/edit/mod.rs @@ -476,17 +476,17 @@ fn reset_panel( let c = panel .parser_state .fetch_component_as::("lerp_slider")?; - c.set_value(&mut com, state.positioning.get_lerp().unwrap_or(1.0)); + c.set_value_primary(&mut com, state.positioning.get_lerp().unwrap_or(1.0)); let c = panel .parser_state .fetch_component_as::("alpha_slider")?; - c.set_value(&mut com, state.alpha); + c.set_value_primary(&mut com, state.alpha); let c = panel .parser_state .fetch_component_as::("curve_slider")?; - c.set_value(&mut com, state.curvature.unwrap_or(0.0)); + c.set_value_primary(&mut com, state.curvature.unwrap_or(0.0)); let c = panel .parser_state diff --git a/wgui/src/components/color_selector.rs b/wgui/src/components/color_selector.rs index 65d6a4e7..16631c58 100644 --- a/wgui/src/components/color_selector.rs +++ b/wgui/src/components/color_selector.rs @@ -163,9 +163,9 @@ impl ComponentColorSelector { { 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.set_value_primary(&mut common, state.color.r * 255.0); + slider_g.set_value_primary(&mut common, state.color.g * 255.0); + slider_b.set_value_primary(&mut common, state.color.b * 255.0); } slider_r.on_value_changed(self.gen_slider_callback(ColorIndex::Red)); diff --git a/wgui/src/components/slider.rs b/wgui/src/components/slider.rs index 004d4b22..86af620d 100644 --- a/wgui/src/components/slider.rs +++ b/wgui/src/components/slider.rs @@ -29,31 +29,29 @@ use crate::{ }, }; +#[derive(Default, Clone)] +pub struct Value(pub f32); + #[derive(Default)] -pub struct ValuesMinMax { - pub value: f32, +pub struct Limits { pub min_value: f32, pub max_value: f32, pub step: f32, } -impl ValuesMinMax { - fn to_normalized(&self) -> f32 { - (self.value - self.min_value) / (self.max_value - self.min_value) +impl Value { + pub fn get(&self) -> f32 { + self.0 } - fn get_from_normalized(&self, normalized: f32) -> f32 { - normalized * (self.max_value - self.min_value) + self.min_value - } - - fn set_value(&mut self, new_value: f32) -> &mut Self { - let span = self.max_value - self.min_value; - let clamped = new_value.max(self.min_value).min(self.max_value); + pub fn set(&mut self, limits: &Limits, new_value: f32) { + let span = limits.max_value - limits.min_value; + let clamped = new_value.max(limits.min_value).min(limits.max_value); // get the step index from min - let mut k = ((clamped - self.min_value) / self.step).round(); + let mut k = ((clamped - limits.min_value) / limits.step).round(); - let k_max = (span / self.step).floor(); + let k_max = (span / limits.step).floor(); if k < 0.0 { k = 0.0; } @@ -61,25 +59,50 @@ impl ValuesMinMax { k = k_max; } - let snapped = self.min_value + k * self.step; - self.value = snapped.max(self.min_value).min(self.max_value); + let snapped = limits.min_value + k * limits.step; + self.0 = snapped.max(limits.min_value).min(limits.max_value); + } +} - self +impl Limits { + fn to_normalized(&self, value: f32) -> f32 { + (value - self.min_value) / (self.max_value - self.min_value) + } + + fn get_from_normalized(&self, normalized: f32) -> f32 { + normalized * (self.max_value - self.min_value) + self.min_value } } #[derive(Default)] pub struct Params { pub style: taffy::Style, - pub values: ValuesMinMax, + pub limits: Limits, + pub value1: Value, + pub value2: Option, // range slider support pub show_value: bool, pub tooltip: Option, } +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum ValueIndex { + Primary, + Secondary, /* for range sliders */ +} + +struct DraggedBy { + index: ValueIndex, + device: DeviceBitmask, +} + struct State { - dragged_by: Option, - hovered: bool, - values: ValuesMinMax, + dragged_by: Option, + hovered_body: bool, + hovered1: bool, + hovered2: bool, + value1: Value, + value2: Option, + limits: Limits, on_value_changed: Option, active_tooltip: Option>, } @@ -90,14 +113,20 @@ impl TooltipTrait for State { } } +struct SliderHandleData { + id_handle_rect: WidgetID, // Rectangle + id_text: Option, // Text + id_handle: WidgetID, +} + struct Data { body_node: taffy::NodeId, - slider_handle_rect_id: WidgetID, // Rectangle - slider_text_id: Option, // Text - slider_handle_id: WidgetID, + handle1: SliderHandleData, + handle2: Option, } pub struct SliderValueChangedEvent { + pub index: ValueIndex, pub value: f32, } @@ -113,8 +142,13 @@ impl ComponentTrait for ComponentSlider { fn refresh(&self, data: &mut RefreshData) { 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 value1 = state.value1.get(); + state.set_value(&mut common, &self.data, ValueIndex::Primary, value1); + + if let Some(value2) = state.value2.as_ref().map(|v| v.get()) { + state.set_value(&mut common, &self.data, ValueIndex::Secondary, value2); + } } fn base(&self) -> &ComponentBase { @@ -127,13 +161,25 @@ impl ComponentTrait for ComponentSlider { } impl ComponentSlider { - pub fn get_value(&self) -> f32 { - self.state.borrow().values.value + pub fn get_value_primary(&self) -> f32 { + self.get_value(ValueIndex::Primary).unwrap() /* safe */ } - pub fn set_value(&self, common: &mut CallbackDataCommon, value: f32) { + pub fn get_value(&self, index: ValueIndex) -> Option { + let state = self.state.borrow(); + match index { + ValueIndex::Primary => Some(state.value1.get()), + ValueIndex::Secondary => state.value2.as_ref().map(|v| v.get()), + } + } + + pub fn set_value(&self, common: &mut CallbackDataCommon, index: ValueIndex, new_value: f32) { let mut state = self.state.borrow_mut(); - state.set_value(common, &self.data, value); + state.set_value(common, &self.data, index, new_value); + } + + pub fn set_value_primary(&self, common: &mut CallbackDataCommon, new_value: f32) { + self.set_value(common, ValueIndex::Primary, new_value); } pub fn on_value_changed(&self, func: SliderValueChangedCallback) { @@ -153,14 +199,15 @@ fn get_width(slider_body_node: taffy::NodeId, tree: &taffy::tree::TaffyTree, ) -> bool { - /* returns false if nothing changed */ - let norm = values.to_normalized(); + /* returns false if nothing has changed */ + let norm = limits.to_normalized(value); // convert normalized value to taffy percentage margin in percent let width = get_width(body_node, tree); @@ -184,11 +231,22 @@ const HANDLE_WIDTH: f32 = 32.0; const HANDLE_HEIGHT: f32 = 24.0; impl State { + fn get_hovered_index(&self) -> Option { + if self.hovered1 { + Some(ValueIndex::Primary) + } else if self.hovered2 { + Some(ValueIndex::Secondary) + } else { + None + } + } + fn update_value_to_mouse( &mut self, event_data: &event::CallbackData<'_>, data: &Data, common: &mut CallbackDataCommon, + index: ValueIndex, ) { let mouse_pos = event_data .metadata @@ -200,10 +258,10 @@ impl State { get_width(data.body_node, &common.state.tree) - HANDLE_WIDTH, ); - let target_value = self.values.get_from_normalized(norm); + let target_value = self.limits.get_from_normalized(norm); let val = target_value; - self.set_value(common, data, val); + self.set_value(common, data, index, val); } fn update_text(common: &mut CallbackDataCommon, text: &mut WidgetLabel, value: f32) { @@ -217,23 +275,47 @@ impl State { text.set_text(common, Translation::from_raw_text(&pretty)); } - fn set_value(&mut self, common: &mut CallbackDataCommon, data: &Data, value: f32) { - let before = self.values.value; - self.values.set_value(value); + fn set_value(&mut self, common: &mut CallbackDataCommon, data: &Data, index: ValueIndex, new_value: f32) { + let val1 = self.value1.get(); - let changed = self.values.value != before; + let Some(value) = (match index { + ValueIndex::Primary => Some(&mut self.value1), + ValueIndex::Secondary => self.value2.as_mut(), + }) else { + return; + }; - let Some(slider_handle_node_id) = common.state.nodes.get(data.slider_handle_id) else { + // Slider handle widget + let Some(handle_data) = (match index { + ValueIndex::Primary => Some(&data.handle1), + ValueIndex::Secondary => data.handle2.as_ref(), + }) else { + unreachable!(); + }; + + let before = value.get(); + + if index == ValueIndex::Secondary { + value.set(&self.limits, new_value.max(val1)); + } else { + value.set(&self.limits, new_value); + } + + let has_changed = value.get() != before; + + let Some(slider_handle_node_id) = common.state.nodes.get(handle_data.id_handle) else { return; }; let Ok(style) = common.state.tree.style(*slider_handle_node_id) else { return; }; + if !conf_handle_style( common.alterables, - &self.values, - data.slider_handle_id, + &self.limits, + value.get(), + handle_data.id_handle, data.body_node, style, &common.state.tree, @@ -241,20 +323,21 @@ impl State { return; // nothing changed visually } - common.alterables.mark_dirty(data.slider_handle_id); + common.alterables.mark_dirty(handle_data.id_handle); common.alterables.mark_redraw(); - if let Some(slider_text_id) = data.slider_text_id - && let Some(mut label) = common.state.widgets.get_as::(slider_text_id) + if let Some(id_text) = handle_data.id_text + && let Some(mut label) = common.state.widgets.get_as::(id_text) { - Self::update_text(common, &mut label, self.values.value); + Self::update_text(common, &mut label, value.get()); } - if changed && let Some(on_value_changed) = &self.on_value_changed { + if has_changed && let Some(on_value_changed) = &self.on_value_changed { on_value_changed( common, SliderValueChangedEvent { - value: self.values.value, + index: index, + value: value.get(), }, ) } @@ -310,18 +393,16 @@ fn on_leave_anim(common: &mut event::CallbackDataCommon, handle_id: WidgetID, an } fn register_event_mouse_enter( - data: Rc, state: Rc>, listeners: &mut EventListenerCollection, tooltip_info: Option, - anim_mult: f32, ) -> event::EventListenerID { listeners.register( EventListenerKind::MouseEnter, Box::new(move |common, event_data, (), ()| { common.alterables.trigger_haptics(); - state.borrow_mut().hovered = true; - on_enter_anim(common, data.slider_handle_rect_id, anim_mult); + state.borrow_mut().hovered_body = true; + ComponentTooltip::register_hover_in(common, &tooltip_info, event_data.widget_id, state.clone()); Ok(EventResult::Pass) @@ -330,10 +411,8 @@ fn register_event_mouse_enter( } fn register_event_mouse_leave( - data: Rc, state: Rc>, listeners: &mut EventListenerCollection, - anim_mult: f32, ) -> event::EventListenerID { listeners.register( EventListenerKind::MouseLeave, @@ -342,36 +421,110 @@ fn register_event_mouse_leave( { let mut state = state.borrow_mut(); - state.hovered = false; + state.hovered_body = false; state.active_tooltip = None; } - on_leave_anim(common, data.slider_handle_rect_id, anim_mult); Ok(EventResult::Pass) }), ) } +fn get_handle_dist(common: &mut CallbackDataCommon, handle: &SliderHandleData, mouse_pos: Vec2) -> f32 { + let center = common.state.get_widget_boundary(handle.id_handle).center(); + Vec2::distance(center, mouse_pos) +} + +const MAX_HOVER_DIST: f32 = 64.0; + +fn update_handle_hovers( + common: &mut CallbackDataCommon, + data: &Data, + state: &mut State, + anim_mult: f32, + mouse_pos: Vec2, +) { + let hovered1_prev = state.hovered1; + let hovered2_prev = state.hovered2; + + if !state.hovered_body { + state.hovered1 = false; + state.hovered2 = false; + } else { + let dist1 = get_handle_dist(common, &data.handle1, mouse_pos); + + let dist2 = data + .handle2 + .as_ref() + .map(|h| get_handle_dist(common, h, mouse_pos)) + .unwrap_or(std::f32::MAX); + + state.hovered1 = dist1 <= MAX_HOVER_DIST; + state.hovered2 = dist2 <= MAX_HOVER_DIST; + + // both of them are hovered, hover the closest one + if state.hovered1 && state.hovered2 { + if dist1 < dist2 { + state.hovered2 = false; + } else { + state.hovered1 = false; + } + } + } + + // hover state changed, run animations + if state.hovered1 != hovered1_prev { + if state.hovered1 && !hovered1_prev { + on_enter_anim(common, data.handle1.id_handle_rect, anim_mult); + } else { + on_leave_anim(common, data.handle1.id_handle_rect, anim_mult); + } + } + + if state.hovered2 != hovered2_prev + && let Some(handle2) = data.handle2.as_ref() + { + if state.hovered2 && !hovered2_prev { + on_enter_anim(common, handle2.id_handle_rect, anim_mult); + } else { + on_leave_anim(common, handle2.id_handle_rect, anim_mult); + } + } +} + fn register_event_mouse_motion( data: Rc, state: Rc>, listeners: &mut EventListenerCollection, + anim_mult: f32, ) -> event::EventListenerID { listeners.register( EventListenerKind::MouseMotion, Box::new(move |common, event_data, (), ()| { let mut state = state.borrow_mut(); - let CallbackMetadata::MousePosition(pos) = event_data.metadata else { + let Some(pos_relative) = event_data + .metadata + .get_mouse_pos_relative(&common.alterables.transform_stack) + else { unreachable!(); }; - if state.dragged_by.is_some_and(|device| device == pos.device) { - state.update_value_to_mouse(event_data, &data, common); - Ok(EventResult::Consumed) - } else { - Ok(EventResult::Pass) + let CallbackMetadata::MousePosition(pos) = &event_data.metadata else { + unreachable!(); + }; + + update_handle_hovers(common, &data, &mut state, anim_mult, pos_relative); + + if let Some(dragged_by) = &state.dragged_by { + if dragged_by.device == pos.device { + let index = dragged_by.index; + state.update_value_to_mouse(event_data, &data, common, index); + return Ok(EventResult::Consumed); + } } + + Ok(EventResult::Pass) }), ) } @@ -392,13 +545,18 @@ fn register_event_mouse_press( unreachable!(); }; - if state.hovered { - state.dragged_by = Some(btn.device); - state.update_value_to_mouse(event_data, &data, common); - Ok(EventResult::Consumed) - } else { - Ok(EventResult::Pass) + if !state.hovered_body { + // this slider isn't hovered at all? + return Ok(EventResult::Pass); } + + let hovered_index = state.get_hovered_index().unwrap_or(ValueIndex::Primary); + state.dragged_by = Some(DraggedBy { + device: btn.device, + index: hovered_index, + }); + state.update_value_to_mouse(event_data, &data, common, hovered_index); + Ok(EventResult::Consumed) }), ) } @@ -423,36 +581,11 @@ fn register_event_mouse_release( ) } -pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Result<(WidgetPair, Rc)> { - let mut style = params.style; - style.position = taffy::Position::Relative; - style.min_size = style.size; - style.max_size = style.size; - - let (root, slider_body_node) = ess.layout.add_child(ess.parent, WidgetDiv::create(), style)?; - let body_id = root.id; - - let (_background_id, _) = ess.layout.add_child( - body_id, - WidgetRectangle::create(WidgetRectangleParams { - color: BODY_COLOR, - round: WLength::Percent(1.0), - border_color: BODY_BORDER_COLOR, - border: 2.0, - ..Default::default() - }), - taffy::Style { - size: taffy::Size { - width: percent(1.0), - height: percent(PAD_PERCENT), - }, - position: taffy::Position::Absolute, - align_self: Some(taffy::AlignItems::Center), - justify_self: Some(taffy::JustifySelf::Center), - ..Default::default() - }, - )?; - +fn mount_slider_handle( + ess: &mut ConstructEssentials, + body_id: WidgetID, + show_value: bool, +) -> anyhow::Result { let slider_handle_style = taffy::Style { size: taffy::Size { width: length(0.0), @@ -488,15 +621,7 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul }, )?; - let state = State { - dragged_by: None, - hovered: false, - values: params.values, - on_value_changed: None, - active_tooltip: None, - }; - - let slider_text: Option<(WidgetPair, taffy::NodeId)> = if params.show_value { + let slider_text: Option<(WidgetPair, taffy::NodeId)> = if show_value { let label = WidgetLabel::create( &mut ess.layout.state, WidgetLabelParams { @@ -514,11 +639,66 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul None }; + Ok(SliderHandleData { + id_handle_rect: slider_handle_rect.id, + id_text: slider_text.map(|s| s.0.id), + id_handle: slider_handle.id, + }) +} + +pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Result<(WidgetPair, Rc)> { + let mut style = params.style; + style.position = taffy::Position::Relative; + style.min_size = style.size; + style.max_size = style.size; + + let (root, slider_body_node) = ess.layout.add_child(ess.parent, WidgetDiv::create(), style)?; + let body_id = root.id; + + let (_background_id, _) = ess.layout.add_child( + body_id, + WidgetRectangle::create(WidgetRectangleParams { + color: BODY_COLOR, + round: WLength::Percent(1.0), + border_color: BODY_BORDER_COLOR, + border: 2.0, + ..Default::default() + }), + taffy::Style { + size: taffy::Size { + width: percent(1.0), + height: percent(PAD_PERCENT), + }, + position: taffy::Position::Absolute, + align_self: Some(taffy::AlignItems::Center), + justify_self: Some(taffy::JustifySelf::Center), + ..Default::default() + }, + )?; + + let slider_handle1 = mount_slider_handle(ess, body_id, params.show_value)?; + let slider_handle2 = if params.value2.is_some() { + Some(mount_slider_handle(ess, body_id, params.show_value)?) + } else { + None + }; + + let state = State { + dragged_by: None, + hovered_body: false, + hovered1: false, + hovered2: false, + value1: params.value1, + value2: params.value2, + limits: params.limits, + on_value_changed: None, + active_tooltip: None, + }; + let data = Rc::new(Data { - slider_handle_rect_id: slider_handle_rect.id, body_node: slider_body_node, - slider_handle_id: slider_handle.id, - slider_text_id: slider_text.map(|s| s.0.id), + handle1: slider_handle1, + handle2: slider_handle2, }); let state = Rc::new(RefCell::new(state)); @@ -529,9 +709,9 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul let listeners = &mut root.widget.state().event_listeners; let anim_mult = ess.layout.state.theme.animation_mult; vec![ - register_event_mouse_enter(data.clone(), state.clone(), listeners, params.tooltip, anim_mult), - register_event_mouse_leave(data.clone(), state.clone(), listeners, anim_mult), - register_event_mouse_motion(data.clone(), state.clone(), listeners), + register_event_mouse_enter(state.clone(), listeners, params.tooltip), + register_event_mouse_leave(state.clone(), listeners), + register_event_mouse_motion(data.clone(), state.clone(), listeners, anim_mult), register_event_mouse_press(data.clone(), state.clone(), listeners), register_event_mouse_release(state.clone(), listeners), ] diff --git a/wgui/src/parser/component_slider.rs b/wgui/src/parser/component_slider.rs index ef09e8a8..d38a738a 100644 --- a/wgui/src/parser/component_slider.rs +++ b/wgui/src/parser/component_slider.rs @@ -18,7 +18,8 @@ pub fn parse_component_slider( ) -> anyhow::Result { let mut min_value = 0.0; let mut max_value = 1.0; - let mut initial_value = 0.5; + let mut initial_value1 = 0.5; + let mut initial_value2: Option = None; let mut step = 1.0; let mut show_value = 1; let mut tooltip = TooltipAttribs::default(); @@ -35,7 +36,13 @@ pub fn parse_component_slider( ctx.parse_check_f32(tag_name, key, value, &mut max_value); } "value" => { - ctx.parse_check_f32(tag_name, key, value, &mut initial_value); + ctx.parse_check_f32(tag_name, key, value, &mut initial_value1); + } + "value2" => { + let mut val = 0.0; + if ctx.parse_check_f32(tag_name, key, value, &mut val) { + initial_value2 = Some(val); + } } "step" => { ctx.parse_check_f32(tag_name, key, value, &mut step); @@ -56,12 +63,13 @@ pub fn parse_component_slider( }, slider::Params { style, - values: slider::ValuesMinMax { + limits: slider::Limits { min_value, max_value, - value: initial_value, step, }, + value1: slider::Value(initial_value1), + value2: initial_value2.map(|v| slider::Value(v)), show_value: show_value != 0, tooltip: tooltip.get_info(), }, diff --git a/wgui/src/widget/mod.rs b/wgui/src/widget/mod.rs index 961d916d..f3e877eb 100644 --- a/wgui/src/widget/mod.rs +++ b/wgui/src/widget/mod.rs @@ -568,14 +568,6 @@ impl WidgetState { event: &Event, ) -> anyhow::Result { match &event { - Event::MouseDown(e) => { - // firstly, check if this widget is scrollable at all - let (active_x, active_y) = get_scroll_active_axis(¶ms.style, ¶ms.taffy_layout); - if active_x || active_y { - self.data.press_down_start_mouse_pos = Some(e.pos); - self.data.swipe_scroll_start = self.data.scrolling_target; - } - } Event::MouseUp(_e) => { if self.data.swipe_running { self.data.swipe_running = false; @@ -654,6 +646,13 @@ impl WidgetState { res = Some(self.invoke_listeners(&mut invoke_data, EventListenerKind::MouseCancel, CallbackMetadata::None)?); } Event::MouseDown(e) => { + // firstly, check if this widget is scrollable at all + let (active_x, active_y) = get_scroll_active_axis(&invoke_data.params.style, &invoke_data.params.taffy_layout); + if active_x || active_y { + self.data.press_down_start_mouse_pos = Some(e.pos); + self.data.swipe_scroll_start = self.data.scrolling_target; + } + if hovered && self.data.set_device_pressed(e.device, true) { res = Some(self.invoke_listeners( &mut invoke_data, From f30c650bbac3e5d8b3d3de60dc3d633bcd64a368 Mon Sep 17 00:00:00 2001 From: Aleksander Date: Sun, 3 May 2026 19:13:45 +0200 Subject: [PATCH 33/36] wgui: add `EventResult::ConsumedExclusive` for Slider components This prevents us from accidentally swipe-scrolling the content if the user tries to change a slider value. --- wgui/src/components/slider.rs | 2 +- wgui/src/layout.rs | 16 ++++++------ wgui/src/widget/mod.rs | 47 ++++++++++++++++++++++++----------- 3 files changed, 41 insertions(+), 24 deletions(-) diff --git a/wgui/src/components/slider.rs b/wgui/src/components/slider.rs index 86af620d..fa61b8bf 100644 --- a/wgui/src/components/slider.rs +++ b/wgui/src/components/slider.rs @@ -556,7 +556,7 @@ fn register_event_mouse_press( index: hovered_index, }); state.update_value_to_mouse(event_data, &data, common, hovered_index); - Ok(EventResult::Consumed) + Ok(EventResult::ConsumedExclusive) }), ) } diff --git a/wgui/src/layout.rs b/wgui/src/layout.rs index 6bcda51f..0ff8f588 100644 --- a/wgui/src/layout.rs +++ b/wgui/src/layout.rs @@ -462,15 +462,13 @@ impl Layout { // check children first self.push_event_children(node_id, event, event_result, alterables, user_data)?; - if event_result.can_propagate() { - widget.process_event( - &mut self.get_event_params(l, node_id, style, alterables), - widget_id, - event, - event_result, - user_data, - )?; - } + widget.process_event( + &mut self.get_event_params(l, node_id, style, alterables), + widget_id, + event, + event_result, + user_data, + )?; } } diff --git a/wgui/src/widget/mod.rs b/wgui/src/widget/mod.rs index f3e877eb..bcbc1dfb 100644 --- a/wgui/src/widget/mod.rs +++ b/wgui/src/widget/mod.rs @@ -211,15 +211,19 @@ pub struct EventParams<'a> { #[derive(Clone, Copy, Eq, PartialEq, PartialOrd)] pub enum EventResult { - NoHit, // event was pushed but has not found a listener (yet) - Pass, // widget acknowledged it and allows the event to propagate further - Consumed, // widget triggered an action, do not propagate further + NoHit, // event was pushed but has not found a listener (yet) + Pass, // widget acknowledged it and allows the event to propagate further + Consumed, // widget triggered an action, do not propagate further + ConsumedExclusive, // same as `Consumed`, but with force-disabled swipe-scroll event (used in sliders for example) } impl EventResult { #[must_use] - pub const fn can_propagate(self) -> bool { - !matches!(self, EventResult::Consumed) + pub const fn can_propagate(&self) -> bool { + match &self { + EventResult::Consumed | EventResult::ConsumedExclusive => false, + _ => true, + } } #[must_use] @@ -351,8 +355,6 @@ impl WidgetState { } pub fn get_scroll_shift_smooth(&self, info: &ScrollbarInfo, l: &taffy::Layout, timestep_alpha: f32) -> (Vec2, bool) { - //return (self.get_scroll_shift_raw(info, l), false); - let currently_animating = self.data.scrolling_cur != self.data.scrolling_cur_prev; let scrolling = self @@ -614,6 +616,20 @@ impl WidgetState { Ok(EventResult::Pass) } + fn process_swipe_event(&mut self, event: &Event, params: &EventParams) { + let Event::MouseDown(e) = &event else { + return; + }; + + // firstly, check if this widget is scrollable at all + let (active_x, active_y) = get_scroll_active_axis(¶ms.style, ¶ms.taffy_layout); + if active_x || active_y { + log::info!("swipe"); + self.data.press_down_start_mouse_pos = Some(e.pos); + self.data.swipe_scroll_start = self.data.scrolling_target; + } + } + pub fn process_event<'a, 'b, U1: 'static, U2: 'static>( &mut self, params: &'a mut EventParams<'a>, @@ -622,6 +638,16 @@ impl WidgetState { event_result: &'a mut EventResult, user_data: &'a mut (&'b mut U1, &'b mut U2), ) -> anyhow::Result<()> { + // check if we should swipe-scroll this widget + if *event_result != EventResult::ConsumedExclusive { + self.process_swipe_event(event, params); + } + + if !event_result.can_propagate() { + // we don't want to pass any other events + return Ok(()); + } + let hovered = event.test_mouse_within_transform(params.alterables.transform_stack.get()); let mut invoke_data = InvokeData { @@ -646,13 +672,6 @@ impl WidgetState { res = Some(self.invoke_listeners(&mut invoke_data, EventListenerKind::MouseCancel, CallbackMetadata::None)?); } Event::MouseDown(e) => { - // firstly, check if this widget is scrollable at all - let (active_x, active_y) = get_scroll_active_axis(&invoke_data.params.style, &invoke_data.params.taffy_layout); - if active_x || active_y { - self.data.press_down_start_mouse_pos = Some(e.pos); - self.data.swipe_scroll_start = self.data.scrolling_target; - } - if hovered && self.data.set_device_pressed(e.device, true) { res = Some(self.invoke_listeners( &mut invoke_data, From ec2e0501c105103eb36ec8290825083f5d557e6b Mon Sep 17 00:00:00 2001 From: Aleksander Date: Sun, 3 May 2026 19:35:49 +0200 Subject: [PATCH 34/36] wayvr: skip unsupported config files (Closes #510) --- wayvr/src/config.rs | 31 +++++++++++++++++++++++++++++-- wgui/src/widget/mod.rs | 1 - 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/wayvr/src/config.rs b/wayvr/src/config.rs index 9c89a629..237fca9d 100644 --- a/wayvr/src/config.rs +++ b/wayvr/src/config.rs @@ -1,7 +1,7 @@ use config::{Config, File}; use log::error; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use wayvr_ipc::packet_client::WvrProcessLaunchParams; use wlx_common::{ astr_containers::AStrMap, @@ -45,6 +45,32 @@ where panic!("No usable config found."); } +const SUPPORTED_EXTESIONS: [&'static str; 4] = ["yaml", "yml", "json", "json5"]; + +fn is_supported_config_file(path: &Path) -> bool { + if path.is_dir() { + return false; + } + match path.extension().and_then(|e| e.to_str()) { + Some(ext) => { + for sup in SUPPORTED_EXTESIONS { + if sup.eq_ignore_ascii_case(ext) { + return true; + } + } + + log::error!( + "Unsupported file extension: \".{}\" in {}. Ignoring file.", + ext, + path.to_string_lossy() + ); + debug_assert!(false); + false + } + _ => false, + } +} + pub fn load_config_with_conf_d( root_config_filename: &str, ctype: config_io::ConfigRoot, @@ -68,7 +94,8 @@ where if let Ok(paths_unsorted) = std::fs::read_dir(path_conf_d) { let mut paths: Vec<_> = paths_unsorted .filter_map(|r| match r { - Ok(entry) => Some(entry), + Ok(entry) if is_supported_config_file(&entry.path()) => Some(entry), + Ok(_other) => None, Err(e) => { error!("Failed to read conf.d directory: {e}"); None diff --git a/wgui/src/widget/mod.rs b/wgui/src/widget/mod.rs index bcbc1dfb..f82aea71 100644 --- a/wgui/src/widget/mod.rs +++ b/wgui/src/widget/mod.rs @@ -624,7 +624,6 @@ impl WidgetState { // firstly, check if this widget is scrollable at all let (active_x, active_y) = get_scroll_active_axis(¶ms.style, ¶ms.taffy_layout); if active_x || active_y { - log::info!("swipe"); self.data.press_down_start_mouse_pos = Some(e.pos); self.data.swipe_scroll_start = self.data.scrolling_target; } From cdd658d4ea8fc0309bbd658ab9fee714695c4282 Mon Sep 17 00:00:00 2001 From: oo8dev Date: Thu, 7 May 2026 22:06:54 +0200 Subject: [PATCH 35/36] wayvr: IPC: Fix dangling fds (#513) --- wayvr/src/ipc/ipc_server.rs | 91 +++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 38 deletions(-) diff --git a/wayvr/src/ipc/ipc_server.rs b/wayvr/src/ipc/ipc_server.rs index 4490ac21..8a33c87f 100644 --- a/wayvr/src/ipc/ipc_server.rs +++ b/wayvr/src/ipc/ipc_server.rs @@ -42,35 +42,40 @@ pub fn send_packet(conn: &mut local_socket::Stream, data: &[u8]) -> anyhow::Resu Ok(()) } -fn read_check(expected_size: u32, res: std::io::Result) -> bool { +fn read_check(expected_size: u32, res: std::io::Result) -> anyhow::Result { match res { Ok(count) => { if count == 0 { - return false; + anyhow::bail!("End of stream"); } if count as u32 == expected_size { - true // read succeeded + return Ok(true); // read succeeded } else { log::error!("count {count} is not {expected_size}"); - false + return Ok(false); } } - Err(_e) => { - //log::error!("failed to get packet size: {}", e); - false - } + Err(e) => match e.kind() { + std::io::ErrorKind::WouldBlock => { + // no incoming data (this socket is non-blocking), try again later + return Ok(false); + } + _ => { + anyhow::bail!("Connection error: {:?}", e); + } + }, } } type Payload = SmallVec<[u8; 64]>; -fn read_payload(conn: &mut local_socket::Stream, size: u32) -> Option { +fn read_payload(conn: &mut local_socket::Stream, size: u32) -> anyhow::Result> { let mut payload = Payload::new(); payload.resize(size as usize, 0); - if read_check(size, conn.read(&mut payload)) { - Some(payload) + if read_check(size, conn.read(&mut payload))? { + Ok(Some(payload)) } else { - None + Ok(None) } } @@ -433,36 +438,33 @@ impl Connection { Ok(()) } - fn process_check_payload(&mut self, params: &mut TickParams, payload: Payload) -> bool { - log::debug!("payload size {}", payload.len()); - + fn process_check_payload( + &mut self, + params: &mut TickParams, + payload: Payload, + ) -> anyhow::Result<()> { if let Err(e) = self.process_payload(params, payload) { - log::error!("Invalid payload from the client, closing connection: {e}"); // send also error message directly to the client before disconnecting self.kill(format!("{e}").as_str()); - false - } else { - true + anyhow::bail!("Invalid payload from the client, closing connection: {e}"); } + Ok(()) } - fn read_packet(&mut self, params: &mut TickParams) -> bool { + fn read_packet(&mut self, params: &mut TickParams) -> anyhow::Result { if let Some(payload_size) = self.next_packet { - let Some(payload) = read_payload(&mut self.conn, payload_size) else { - // still failed to read payload, try in next tick - return false; + let Some(payload) = read_payload(&mut self.conn, payload_size)? else { + // still failed to read payload, try in the next tick + return Ok(false); }; - if !self.process_check_payload(params, payload) { - return false; - } - + self.process_check_payload(params, payload)?; self.next_packet = None; } let mut buf_packet_header: [u8; 4] = [0; 4]; - if !read_check(4, self.conn.read(&mut buf_packet_header)) { - return false; + if !read_check(4, self.conn.read(&mut buf_packet_header))? { + return Ok(false); } let payload_size = u32::from_be_bytes(buf_packet_header[0..4].try_into().unwrap()); // 0-3 bytes (u32 size) @@ -475,30 +477,43 @@ impl Connection { "Client sent a packet header with the size over {size_limit} bytes, closing connection." ); self.kill("Too big packet received (over 128 KiB)"); - return false; + return Ok(false); } - let Some(payload) = read_payload(&mut self.conn, payload_size) else { + let Some(payload) = read_payload(&mut self.conn, payload_size)? else { // failed to read payload, try in next tick self.next_packet = Some(payload_size); - return false; + return Ok(false); }; - if !self.process_check_payload(params, payload) { - return false; - } + self.process_check_payload(params, payload)?; - true + Ok(true) } fn tick(&mut self, params: &mut TickParams) { - while self.read_packet(params) {} + loop { + match self.read_packet(params) { + Ok(loop_further) => { + if !loop_further { + break; + } + } + Err(e) => { + log::debug!("Disconnecting client: {:?}", e); + self.alive = false; + break; + } + } + } } } impl Drop for Connection { fn drop(&mut self) { - log::info!("Connection closed"); + if let Some(auth) = &self.auth { + log::info!("IPC: Client \"{}\" disconnected.", auth.client_name); + } } } From 2309f89701005b0b2faf03a27993e432af2bfe1a Mon Sep 17 00:00:00 2001 From: galister <22305755+galister@users.noreply.github.com> Date: Tue, 12 May 2026 17:11:15 +0900 Subject: [PATCH 36/36] update issue template --- .github/ISSUE_TEMPLATE/bug-report.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 582645a7..8eba3098 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -22,25 +22,28 @@ If this is a regression, please mention which version was working previously. **Kernel version**: **VR Runtime**: -- [ ] Monado/WiVRn -- [ ] SteamVR/ALVR +- [ ] Monado +- [ ] WiVRn +- [ ] ALVR +- [ ] SteamLink +- [ ] SteamVR (not SteamLink) **GPU models and driver versions**: ## Overlay Logs -