diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6475d84..407601c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -23,7 +23,11 @@ cargo:test: - echo 'deb http://deb.debian.org/debian experimental main' > /etc/apt/sources.list.d/experimental.list - apt-get update - apt-get -t experimental install libgtk-4-dev libadwaita-1-dev libgtksourceview-5-dev libssl-dev -y - - apt-get install rust-all cargo meson ninja-build git desktop-file-utils gettext libjxl-dev file libusb-dev libusb-1.0-0-dev -y + - apt-get install meson ninja-build git desktop-file-utils gettext libjxl-dev file libusb-dev libusb-1.0-0-dev curl -y + - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o /tmp/rustup.sh + - chmod +x /tmp/rustup.sh + - /tmp/rustup.sh -y + - source "$HOME/.cargo/env" - rustc --version && cargo --version # Print version info for debugging - meson setup build -Dprefix="$PWD/build/localprefix" -Dprofile=development - ninja -C build @@ -38,7 +42,11 @@ appimage: - echo 'deb http://deb.debian.org/debian experimental main' > /etc/apt/sources.list.d/experimental.list - apt-get update - apt-get -t experimental install libgtk-4-dev libadwaita-1-dev libgtksourceview-5-dev libssl-dev -y - - apt-get install rust-all cargo meson ninja-build git desktop-file-utils gettext libjxl-dev file libusb-dev libusb-1.0-0-dev -y + - apt-get install meson ninja-build git desktop-file-utils gettext libjxl-dev file libusb-dev libusb-1.0-0-dev curl -y + - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o /tmp/rustup.sh + - chmod +x /tmp/rustup.sh + - /tmp/rustup.sh -y + - source "$HOME/.cargo/env" - bash ./dist/appimage/build_appimage.sh artifacts: paths: diff --git a/Cargo.lock b/Cargo.lock index a79a34f..61af5f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -70,6 +70,29 @@ version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +[[package]] +name = "bindgen" +version = "0.68.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" +dependencies = [ + "bitflags 2.4.0", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.33", + "which", +] + [[package]] name = "bit-set" version = "0.2.0" @@ -150,6 +173,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-expr" version = "0.15.5" @@ -166,6 +198,35 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clang-sys" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -182,6 +243,35 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "dlopen2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bc2c7ed06fd72a8513ded8d0d2f6fd2655a85d6885c48cae8625d80faf28c03" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.33", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + [[package]] name = "encoding_rs" version = "0.8.33" @@ -199,7 +289,9 @@ dependencies = [ "gettext-rs", "git2", "gtk4", + "lazy_static", "libadwaita", + "libmonado-rs", "libusb", "nix", "phf", @@ -257,6 +349,12 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "flagset" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a7e408202050813e6f1d9addadcaafef3dca7530c7ddfb005d4081cce6779" + [[package]] name = "flume" version = "0.11.0" @@ -585,6 +683,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "gobject-sys" version = "0.18.0" @@ -747,6 +851,15 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys", +] + [[package]] name = "http" version = "0.2.9" @@ -884,6 +997,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libadwaita" version = "0.5.2" @@ -936,6 +1055,29 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libmonado-rs" +version = "0.1.0" +source = "git+https://github.com/technobaboo/libmonado-rs#3b3f098cb131843ee90f078e26362fcefe02b822" +dependencies = [ + "bindgen", + "cmake", + "convert_case", + "dlopen2", + "flagset", + "semver", +] + [[package]] name = "libssh2-sys" version = "0.3.0" @@ -1057,6 +1199,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -1117,6 +1265,16 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -1240,6 +1398,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "percent-encoding" version = "2.3.0" @@ -1311,6 +1475,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "prettyplease" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +dependencies = [ + "proc-macro2", + "syn 2.0.33", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -1514,6 +1688,12 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.0" @@ -1638,6 +1818,12 @@ dependencies = [ "serde", ] +[[package]] +name = "shlex" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" + [[package]] name = "siphasher" version = "0.3.11" @@ -1977,6 +2163,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + [[package]] name = "url" version = "2.4.1" @@ -2107,6 +2299,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index ef47679..066524d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,9 +14,11 @@ git2 = "0.17.2" gtk4 = { version = "0.7.2", features = [ "v4_10", ] } +lazy_static = "1.4.0" libadwaita = { version = "0.5.2", features = [ "v1_3" ] } +libmonado-rs = { git = "https://github.com/technobaboo/libmonado-rs", version = "0.1.0" } libusb = "0.3.0" nix = { version = "0.26.4", features = [ "fs" diff --git a/README.md b/README.md index cabdb33..b87fb1f 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,12 @@ cd envision ./dist/appimage/build_appimage.sh ``` +# Feature flags + +|Env var|Values|Default| +|---|---|---| +|`ENVISION_FF_USE_LIBMONADO`|`1`: enabled; `0`: disabled|`0`| + # Common issues ## NOSUID with systemd-homed diff --git a/src/main.rs b/src/main.rs index 2a03a02..38852bc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,6 +30,7 @@ pub mod func_runner; pub mod gpu_profile; pub mod log_level; pub mod log_parser; +pub mod monado_utils; pub mod paths; pub mod profile; pub mod profiles; diff --git a/src/monado_utils.rs b/src/monado_utils.rs new file mode 100644 index 0000000..d8c2643 --- /dev/null +++ b/src/monado_utils.rs @@ -0,0 +1,11 @@ +use crate::profile::Profile; + +pub fn get_devs(prof: &Profile) { + if let Ok(monado) = libmonado_rs::Monado::create(prof.libmonado_so().unwrap()) { + if let Ok(devs) = monado.devices() { + for dev in devs { + println!(">>> {}", dev.name); + } + } + } +} diff --git a/src/profile.rs b/src/profile.rs index 01a0681..ea299f3 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -350,6 +350,23 @@ impl Profile { pub fn can_start(&self) -> bool { Path::new(&self.xrservice_binary()).is_file() } + + pub fn libmonado_so(&self) -> Option { + let mut res = format!("{}/lib/libmonado.so", self.prefix); + if Path::new(&res).is_file() { + return Some(res); + } + res = format!("{}/lib64/libmonado.so", self.prefix); + if Path::new(&res).is_file() { + return Some(res); + } + + None + } + + pub fn has_libmonado(&self) -> bool { + self.libmonado_so().is_some() + } } #[cfg(test)] diff --git a/src/ui/app.rs b/src/ui/app.rs index 26c3916..381d6c4 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -3,6 +3,7 @@ use super::alert::alert; use super::build_window::{BuildStatus, BuildWindow}; use super::debug_view::{DebugView, DebugViewMsg}; use super::fbt_config_editor::{FbtConfigEditor, FbtConfigEditorInit, FbtConfigEditorMsg}; +use super::feature_flags::FF_LIBMONADO_DEVICE_ENUMERATION_ENABLED; use super::job_worker::internal_worker::JobWorkerOut; use super::job_worker::job::WorkerJob; use super::job_worker::JobWorker; @@ -43,7 +44,7 @@ use crate::ui::build_window::{BuildWindowMsg, BuildWindowOutMsg}; use crate::ui::debug_view::DebugViewInit; use crate::ui::libsurvive_setup_window::LibsurviveSetupMsg; use crate::ui::main_view::{MainView, MainViewInit, MainViewOutMsg}; -use crate::xr_devices::XRDevices; +use crate::xr_devices::XRDevice; use crate::{stateless_action, withclones}; use gtk::prelude::*; use relm4::actions::{AccelsPlus, ActionGroupName, RelmAction, RelmActionGroup}; @@ -91,9 +92,11 @@ pub struct App { #[tracker::do_not_track] profiles: Vec, #[tracker::do_not_track] - xr_devices: XRDevices, + xr_devices: Vec, #[tracker::do_not_track] fbt_config_editor: Option>, + #[tracker::do_not_track] + libmonado: Option, } #[derive(Debug)] @@ -162,7 +165,7 @@ impl App { return; }; self.debug_view.sender().emit(DebugViewMsg::ClearLog); - self.xr_devices = XRDevices::default(); + self.xr_devices = vec![]; if prof.can_start() { remove_file(&get_ipc_file_path(&prof.xrservice_type)) .is_err() @@ -218,11 +221,12 @@ impl App { if let Some(worker) = self.xrservice_worker.as_ref() { worker.stop(); } + self.libmonado = None; self.restore_openxr_openvr_files(); self.main_view .sender() .emit(MainViewMsg::XRServiceActiveChanged(false, None)); - self.xr_devices = XRDevices::default(); + self.xr_devices = vec![]; } pub fn profiles_list(config: &Config) -> Vec { @@ -309,26 +313,49 @@ impl SimpleComponent for App { } Msg::ClockTicking => { self.main_view.sender().emit(MainViewMsg::ClockTicking); + if *FF_LIBMONADO_DEVICE_ENUMERATION_ENABLED { + if let Some(w) = self.xrservice_worker.as_ref() { + if { + let state = w.state.lock().unwrap(); + state.exit_status.is_none() && !state.stop_requested + } { + if let Some(monado) = self.libmonado.as_ref() { + self.xr_devices = XRDevice::merge( + &self.xr_devices, + &XRDevice::from_libmonado(monado), + ); + self.main_view + .sender() + .emit(MainViewMsg::UpdateDevices(self.xr_devices.clone())); + } else { + if let Some(so) = self.get_selected_profile().libmonado_so() { + self.libmonado = libmonado_rs::Monado::create(so).ok(); + if self.libmonado.is_some() { + sender.input(Msg::ClockTicking); + } + } + } + } + } + } } Msg::ParseLog(rows) => { for row in rows { match MonadoLog::new_from_str(row.as_str()) { None => {} Some(parsed) => { - if parsed.func == "p_create_system" { - match XRDevices::from_log_message(parsed.message.as_str()) { - None => {} - Some(devices) => { - self.xr_devices.merge(devices.clone()); - self.main_view.sender().emit(MainViewMsg::UpdateDevices( - Some(self.xr_devices.clone()), - )); - break; - } - }; - } else { - self.xr_devices - .search_log_for_generic_trackers(parsed.message.as_str()); + if !*FF_LIBMONADO_DEVICE_ENUMERATION_ENABLED + && parsed.func == "p_create_system" + { + let n_devs = XRDevice::from_log_message(parsed.message.as_str()); + self.xr_devices = XRDevice::merge(&self.xr_devices, &n_devs); + self.main_view + .sender() + .emit(MainViewMsg::UpdateDevices(self.xr_devices.clone())); + } else if let Some(tracker) = + XRDevice::generic_tracker_from_log_row(parsed.message.as_str()) + { + self.xr_devices.push(tracker); } } }; @@ -364,7 +391,7 @@ impl SimpleComponent for App { self.start_xrservice(sender); } None => { - worker.stop(); + self.shutdown_xrservice(); self.restart_xrservice = true; } } @@ -643,9 +670,10 @@ impl SimpleComponent for App { profiles, xrservice_worker: None, build_worker: None, - xr_devices: XRDevices::default(), + xr_devices: vec![], fbt_config_editor: None, restart_xrservice: false, + libmonado: None, }; let widgets = view_output!(); diff --git a/src/ui/devices_box.rs b/src/ui/devices_box.rs index 5ab2491..7a33e3e 100644 --- a/src/ui/devices_box.rs +++ b/src/ui/devices_box.rs @@ -1,48 +1,43 @@ +use super::{ + alert::alert, + factories::device_row_factory::{DeviceRowModel, DeviceRowModelInit, DeviceRowState}, +}; use crate::{ file_builders::monado_config_v0::dump_generic_trackers, - xr_devices::{XRDevice, XRDevices}, + xr_devices::{XRDevice, XRDeviceType}, }; use adw::prelude::*; -use relm4::prelude::*; - -use super::alert::alert; +use relm4::{factory::FactoryVecDeque, prelude::*, Sender}; #[tracker::track] pub struct DevicesBox { - devices: Option, + devices: Vec, + + #[tracker::do_not_track] + device_rows: FactoryVecDeque, } #[derive(Debug)] pub enum DevicesBoxMsg { - UpdateDevices(Option), + UpdateDevices(Vec), DumpGenericTrackers, } impl DevicesBox { - fn get_dev(&self, key: XRDevice) -> Option { - match &self.devices { - None => None, - Some(devs) => match key { - XRDevice::Head => devs.head.clone(), - XRDevice::Left => devs.left.clone(), - XRDevice::Right => devs.right.clone(), - XRDevice::Gamepad => devs.gamepad.clone(), - XRDevice::Eyes => devs.eyes.clone(), - XRDevice::HandTrackingLeft => devs.hand_tracking_left.clone(), - XRDevice::HandTrackingRight => devs.hand_tracking_right.clone(), - XRDevice::GenericTracker => { - if devs.generic_trackers.is_empty() { - return None; - } else { - return Some(devs.generic_trackers.join(", ")); - } - } - }, - } - } + fn create_save_trackers_btn(sender: Sender) -> gtk::Button { + let btn = gtk::Button::builder() + .halign(gtk::Align::Center) + .valign(gtk::Align::Center) + .icon_name("document-save-symbolic") + .tooltip_text("Save current trackers") + .css_classes(["circular", "flat"]) + .build(); - fn get_dev_or_none(&self, key: XRDevice) -> String { - self.get_dev(key).unwrap_or("None".into()) + btn.connect_clicked(move |_| { + sender.emit(DevicesBoxMsg::DumpGenericTrackers); + }); + + btn } } @@ -60,147 +55,129 @@ impl SimpleComponent for DevicesBox { set_spacing: 12, set_margin_top: 12, #[track = "model.changed(Self::devices())"] - set_visible: model.devices.is_some(), - gtk::ListBox { - add_css_class: "boxed-list", - set_selection_mode: gtk::SelectionMode::None, - set_margin_all: 12, - // Head - adw::ActionRow { - #[track = "model.changed(Self::devices())"] - set_icon_name: Some(match model.get_dev(XRDevice::Head) { - Some(name) => match name.as_str() { - "Simulated HMD" => "dialog-warning-symbolic", - _ => "emblem-ok-symbolic", - }, - None => "dialog-question-symbolic", - }), - #[track = "model.changed(Self::devices())"] - set_class_active: ("error", model.get_dev(XRDevice::Head).is_none()), - #[track = "model.changed(Self::devices())"] - set_class_active: ("warning", model.get_dev_or_none(XRDevice::Head) == "Simulated HMD"), - set_title: "Head", - #[track = "model.changed(Self::devices())"] - set_subtitle: match model.get_dev_or_none(XRDevice::Head).as_str() { - "Simulated HMD" => "No HMD detected (Simulated HMD)", - s => s, - }, - // TODO: status icon with popover - }, - // Left - adw::ActionRow { - #[track = "model.changed(Self::devices())"] - set_icon_name: Some(match model.get_dev(XRDevice::Left) { - Some(_) => "emblem-ok-symbolic", - None => "dialog-question-symbolic", - }), - #[track = "model.changed(Self::devices())"] - set_class_active: ("error", model.get_dev(XRDevice::Left).is_none()), - set_title: "Left", - #[track = "model.changed(Self::devices())"] - set_subtitle: model.get_dev_or_none(XRDevice::Left).as_str(), - // TODO: status icon with popover - }, - // Right - adw::ActionRow { - #[track = "model.changed(Self::devices())"] - set_icon_name: Some(match model.get_dev(XRDevice::Right) { - Some(_) => "emblem-ok-symbolic", - None => "dialog-question-symbolic", - }), - #[track = "model.changed(Self::devices())"] - set_class_active: ("error", model.get_dev(XRDevice::Right).is_none()), - set_title: "Right", - #[track = "model.changed(Self::devices())"] - set_subtitle: model.get_dev_or_none(XRDevice::Right).as_str(), - // TODO: status icon with popover - }, - // Gamepad - adw::ActionRow { - #[track = "model.changed(Self::devices())"] - set_visible: model.get_dev(XRDevice::Gamepad).is_some(), - set_icon_name: Some("emblem-ok-symbolic"), - set_title: "Gamepad", - #[track = "model.changed(Self::devices())"] - set_subtitle: model.get_dev_or_none(XRDevice::Gamepad).as_str(), - }, - // Eyes - adw::ActionRow { - #[track = "model.changed(Self::devices())"] - set_visible: model.get_dev(XRDevice::Eyes).is_some(), - set_icon_name: Some("emblem-ok-symbolic"), - set_title: "Eye Tracking", - #[track = "model.changed(Self::devices())"] - set_subtitle: model.get_dev_or_none(XRDevice::Eyes).as_str(), - }, - // Hand Tracking Left - adw::ActionRow { - #[track = "model.changed(Self::devices())"] - set_visible: model.get_dev(XRDevice::HandTrackingLeft).is_some(), - set_icon_name: Some("emblem-ok-symbolic"), - set_title: "Hand Tracking Left", - #[track = "model.changed(Self::devices())"] - set_subtitle: model.get_dev_or_none(XRDevice::HandTrackingLeft).as_str(), - }, - // Hand Tracking Right - adw::ActionRow { - #[track = "model.changed(Self::devices())"] - set_visible: model.get_dev(XRDevice::HandTrackingRight).is_some(), - set_icon_name: Some("emblem-ok-symbolic"), - set_title: "Hand Tracking Right", - #[track = "model.changed(Self::devices())"] - set_subtitle: model.get_dev_or_none(XRDevice::HandTrackingRight).as_str(), - }, - // Generic Trackers - adw::ActionRow { - #[track = "model.changed(Self::devices())"] - set_visible: model.get_dev(XRDevice::GenericTracker).is_some(), - set_icon_name: Some("emblem-ok-symbolic"), - set_title: "Generic Trackers", - #[track = "model.changed(Self::devices())"] - set_subtitle: model.get_dev_or_none(XRDevice::GenericTracker).as_str(), - add_suffix: save_trackers_btn = >k::Button { - set_halign: gtk::Align::Center, - set_valign: gtk::Align::Center, - set_icon_name: "document-save-symbolic", - set_tooltip_text: Some("Save current trackers"), - set_css_classes: &["circular", "flat"], - connect_clicked => move |_| { - sender.input(Self::Input::DumpGenericTrackers); - } - }, - }, - } + set_visible: !model.devices.is_empty(), + + append: &devices_listbox, } } - fn update(&mut self, message: Self::Input, _sender: ComponentSender) { + fn update(&mut self, message: Self::Input, sender: ComponentSender) { self.reset(); match message { Self::Input::UpdateDevices(devs) => { self.set_devices(devs); + let mut guard = self.device_rows.guard(); + guard.clear(); + if !self.devices.is_empty() { + let mut has_head = false; + let mut has_left = false; + let mut has_right = false; + let mut models: Vec = vec![]; + let mut generic: Vec<&XRDevice> = vec![]; + for dev in &self.devices { + match dev.dev_type { + XRDeviceType::Head => { + has_head = true; + let mut init = DeviceRowModelInit::from_xr_device(&dev); + if dev.name == "Simulated HMD" { + init.state = Some(DeviceRowState::Warning); + init.subtitle = Some("No HMD detected (Simulated HMD)".into()); + } + models.push(init); + } + XRDeviceType::Left => { + has_left = true; + models.push(DeviceRowModelInit::from_xr_device(&dev)); + } + XRDeviceType::Right => { + has_right = true; + models.push(DeviceRowModelInit::from_xr_device(&dev)); + } + XRDeviceType::GenericTracker => { + generic.push(dev); + } + _ => { + models.push(DeviceRowModelInit::from_xr_device(&dev)); + } + }; + } + if !generic.is_empty() { + models.push(DeviceRowModelInit { + title: Some(XRDeviceType::GenericTracker.to_string()), + subtitle: Some( + generic + .iter() + .map(|d| d.id.as_str()) + .collect::>() + .join(", "), + ), + suffix: Some( + Self::create_save_trackers_btn(sender.input_sender().clone()) + .upcast::(), + ), + ..Default::default() + }); + } + if !has_right { + models.push(DeviceRowModelInit::new_missing(XRDeviceType::Right)); + } + if !has_left { + models.push(DeviceRowModelInit::new_missing(XRDeviceType::Left)); + } + if !has_head { + models.push(DeviceRowModelInit::new_missing(XRDeviceType::Head)); + } + + models.sort_by(|m1, m2| { + let dt1 = XRDeviceType::from_display_str( + m1.title.as_ref().unwrap_or(&String::new()), + ); + let dt2 = XRDeviceType::from_display_str( + m2.title.as_ref().unwrap_or(&String::new()), + ); + dt1.cmp(&dt2) + }); + + for model in models { + guard.push_back(model); + } + } } Self::Input::DumpGenericTrackers => { - if let Some(devs) = self.devices.as_ref() { - let added = dump_generic_trackers(&devs.generic_trackers); - let multi_title = format!("Added {} new trackers", added); - let (title, msg) = match added { - 0 => ( - "No new trackers found", - "All the currently connected trackers are already present in your configuration" + let added = dump_generic_trackers( + &self + .devices + .iter() + .filter(|d| d.dev_type == XRDeviceType::GenericTracker) + .map(|d| d.id.clone()) + .collect::>(), + ); + let multi_title = format!("Added {} new trackers", added); + let (title, msg) = match added { + 0 => ( + "No new trackers found", + concat!( + "All the currently connected trackers ", + "are already present in your configuration" ), - 1 => ( - "Added 1 new tracker", - "Edit your configuration to make sure that all the trackers have the appropriate roles assigned" + ), + 1 => ( + "Added 1 new tracker", + concat!( + "Edit your configuration to make sure that ", + "all the trackers have the appropriate roles assigned" ), - _ => ( - multi_title.as_str(), - "Edit your configuration to make sure that all the trackers have the appropriate roles assigned" + ), + _ => ( + multi_title.as_str(), + concat!( + "Edit your configuration to make sure that ", + "all the trackers have the appropriate roles assigned" ), - }; - alert(title, Some(msg), None); - } + ), + }; + alert(title, Some(msg), None); } } } @@ -210,9 +187,19 @@ impl SimpleComponent for DevicesBox { root: &Self::Root, sender: ComponentSender, ) -> ComponentParts { + let devices_listbox = gtk::ListBox::builder() + .css_classes(["boxed-list"]) + .selection_mode(gtk::SelectionMode::None) + .margin_start(12) + .margin_end(12) + .margin_top(12) + .margin_bottom(12) + .build(); + let model = Self { tracker: 0, - devices: None, + devices: vec![], + device_rows: FactoryVecDeque::new(devices_listbox.clone(), sender.input_sender()), }; let widgets = view_output!(); diff --git a/src/ui/factories/device_row_factory.rs b/src/ui/factories/device_row_factory.rs new file mode 100644 index 0000000..5187bea --- /dev/null +++ b/src/ui/factories/device_row_factory.rs @@ -0,0 +1,124 @@ +use adw::prelude::*; +use relm4::prelude::*; + +use crate::{ + ui::devices_box::DevicesBoxMsg, + xr_devices::{XRDevice, XRDeviceType}, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DeviceRowState { + Ok, + Error, + Warning, +} + +impl Default for DeviceRowState { + fn default() -> Self { + Self::Ok + } +} + +impl DeviceRowState { + pub fn icon(&self) -> &str { + match &self { + Self::Ok => "emblem-ok-symbolic", + Self::Error => "dialog-question-symbolic", + Self::Warning => "dialog-warning-symbolic", + } + } + + pub fn class_name(&self) -> Option<&str> { + match &self { + Self::Ok => None, + Self::Error => Some("error"), + Self::Warning => Some("warning"), + } + } +} + +#[derive(Debug)] +pub struct DeviceRowModel { + title: String, + subtitle: String, + state: DeviceRowState, + suffix: Option, +} + +#[derive(Debug, Default)] +pub struct DeviceRowModelInit { + pub title: Option, + pub subtitle: Option, + pub state: Option, + pub suffix: Option, +} + +impl DeviceRowModelInit { + pub fn from_xr_device(d: &XRDevice) -> Self { + Self { + title: Some(d.dev_type.to_string()), + subtitle: Some(d.name.clone()), + ..Default::default() + } + } + + pub fn new_missing(t: XRDeviceType) -> Self { + DeviceRowModelInit { + title: Some(t.to_string()), + subtitle: Some("None".into()), + state: Some(DeviceRowState::Error), + ..Default::default() + } + } +} + +#[relm4::factory(pub)] +impl FactoryComponent for DeviceRowModel { + type Init = DeviceRowModelInit; + type Input = (); + type Output = (); + type CommandOutput = (); + type Widgets = DeviceRowModelWidgets; + type ParentInput = DevicesBoxMsg; + type ParentWidget = gtk::ListBox; + + view! { + root = adw::ActionRow { + // TODO: replace with flat button that spawns popover + add_prefix: icon = >k::Image { + set_icon_name: Some(self.state.icon()), + }, + set_title: self.title.as_str(), + set_subtitle: self.subtitle.as_str(), + } + } + + fn init_widgets( + &mut self, + _index: &Self::Index, + root: &Self::Root, + _returned_widget: &::ReturnedWidget, + _sender: FactorySender, + ) -> Self::Widgets { + let widgets = view_output!(); + + if let Some(suffix) = self.suffix.as_ref() { + widgets.root.add_suffix(suffix); + } + if let Some(cls) = self.state.class_name() { + widgets.root.add_css_class(cls); + widgets.icon.add_css_class(cls); + } + + widgets + } + + fn init_model(init: Self::Init, _index: &Self::Index, _sender: FactorySender) -> Self { + Self { + title: init.title.unwrap_or_default(), + subtitle: init.subtitle.unwrap_or_default(), + state: init.state.unwrap_or_default(), + suffix: init.suffix, + } + } +} diff --git a/src/ui/factories/mod.rs b/src/ui/factories/mod.rs index 41fe8c9..e311478 100644 --- a/src/ui/factories/mod.rs +++ b/src/ui/factories/mod.rs @@ -1,2 +1,3 @@ +pub mod device_row_factory; pub mod env_var_row_factory; pub mod tracker_role_group_factory; diff --git a/src/ui/feature_flags.rs b/src/ui/feature_flags.rs new file mode 100644 index 0000000..3697538 --- /dev/null +++ b/src/ui/feature_flags.rs @@ -0,0 +1,11 @@ +use lazy_static::lazy_static; +use std::env; + +fn get_ff_libmonado_device_enumeration_enabled() -> bool { + env::var("ENVISION_FF_USE_LIBMONADO").unwrap_or_default() == "1" +} + +lazy_static! { + pub static ref FF_LIBMONADO_DEVICE_ENUMERATION_ENABLED: bool = + get_ff_libmonado_device_enumeration_enabled(); +} diff --git a/src/ui/main_view.rs b/src/ui/main_view.rs index 7254cf8..fa317c6 100644 --- a/src/ui/main_view.rs +++ b/src/ui/main_view.rs @@ -18,7 +18,7 @@ use crate::ui::app::{ }; use crate::ui::profile_editor::ProfileEditorInit; use crate::ui::util::{limit_dropdown_width, warning_heading}; -use crate::xr_devices::XRDevices; +use crate::xr_devices::XRDevice; use gtk::prelude::*; use relm4::adw::traits::MessageDialogExt; use relm4::adw::ResponseAppearance; @@ -66,7 +66,7 @@ pub enum MainViewMsg { DeleteProfile, DuplicateProfile, SaveProfile(Profile), - UpdateDevices(Option), + UpdateDevices(Vec), } #[derive(Debug)] @@ -462,7 +462,7 @@ impl SimpleComponent for MainView { Self::Input::XRServiceActiveChanged(active, profile) => { self.set_xrservice_active(active); if !active { - sender.input(Self::Input::UpdateDevices(None)); + sender.input(Self::Input::UpdateDevices(vec![])); } self.steam_launch_options_box .sender() diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 973ca96..d5f1d84 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -6,6 +6,7 @@ pub mod debug_view; pub mod devices_box; pub mod factories; pub mod fbt_config_editor; +pub mod feature_flags; pub mod install_wivrn_box; pub mod job_worker; pub mod libsurvive_setup_window; diff --git a/src/xr_devices.rs b/src/xr_devices.rs index 9a7b4f6..0aeda43 100644 --- a/src/xr_devices.rs +++ b/src/xr_devices.rs @@ -1,5 +1,9 @@ -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum XRDevice { +use std::{fmt::Display, slice::Iter}; + +const GENERIC_TRACKER_PREFIX: &str = "Found generic tracker device: "; + +#[derive(Debug, Clone, PartialEq, Eq, Copy)] +pub enum XRDeviceType { Head, Left, Right, @@ -10,25 +14,162 @@ pub enum XRDevice { GenericTracker, } -#[derive(Debug, Default, Clone, PartialEq, Eq)] -pub struct XRDevices { - pub head: Option, - pub left: Option, - pub right: Option, - pub gamepad: Option, - pub eyes: Option, - pub hand_tracking_left: Option, - pub hand_tracking_right: Option, - pub generic_trackers: Vec, +impl Display for XRDeviceType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Head => "Head", + Self::Left => "Left", + Self::Right => "Right", + Self::Gamepad => "Gamepad", + Self::Eyes => "Eye Tracking", + Self::HandTrackingLeft => "Hand tracking left", + Self::HandTrackingRight => "Hand tracking right", + Self::GenericTracker => "Generic tracker", + }) + } } -const GENERIC_TRACKER_PREFIX: &str = "Found generic tracker device: "; +impl PartialOrd for XRDeviceType { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.as_number().cmp(&other.as_number())) + } +} -impl XRDevices { - pub fn from_log_message(s: &str) -> Option { +impl Ord for XRDeviceType { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.partial_cmp(other).unwrap() + } +} + +impl XRDeviceType { + pub fn iter() -> Iter<'static, Self> { + [ + Self::Head, + Self::Left, + Self::Right, + Self::Gamepad, + Self::Eyes, + Self::HandTrackingLeft, + Self::HandTrackingRight, + Self::GenericTracker, + ] + .iter() + } + + pub fn to_monado_str(&self) -> &str { + match self { + Self::Head => "head", + Self::Left => "left", + Self::Right => "right", + Self::Gamepad => "gamepad", + Self::Eyes => "eyes", + Self::HandTrackingLeft => "hand_tracking.left", + Self::HandTrackingRight => "hand_tracking.right", + Self::GenericTracker => "generic_tracker", + } + } + + pub fn as_number(&self) -> u32 { + match self { + Self::Head => 0, + Self::Left => 1, + Self::Right => 2, + Self::Gamepad => 3, + Self::Eyes => 4, + Self::HandTrackingLeft => 5, + Self::HandTrackingRight => 6, + Self::GenericTracker => 7, + } + } + + pub fn from_monado_str(s: &str) -> Option { + match s { + "head" => Some(Self::Head), + "left" => Some(Self::Left), + "right" => Some(Self::Right), + "gamepad" => Some(Self::Gamepad), + "eyes" => Some(Self::Eyes), + "hand_tracking.left" => Some(Self::HandTrackingLeft), + "hand_tracking.right" => Some(Self::HandTrackingRight), + _ => None, + } + } + + pub fn from_display_str(s: &str) -> Self { + match s { + "Head" => Self::Head, + "Left" => Self::Left, + "Right" => Self::Right, + "Gamepad" => Self::Gamepad, + "Eye Tracking" => Self::Eyes, + "Hand tracking left" => Self::HandTrackingLeft, + "Hand tracking right" => Self::HandTrackingRight, + "Generic tracker" => Self::GenericTracker, + _ => Self::GenericTracker, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct XRDevice { + pub dev_type: XRDeviceType, + pub name: String, + pub id: String, + pub battery: f32, // battery percentage, from 0 to 1 maybe + // still need to implement it in monado & gui +} + +impl Default for XRDevice { + fn default() -> Self { + Self { + dev_type: XRDeviceType::GenericTracker, + name: String::default(), + id: String::default(), + battery: f32::default(), + } + } +} + +impl XRDevice { + pub fn generic_tracker_from_log_row(s: &str) -> Option { + if s.starts_with(GENERIC_TRACKER_PREFIX) { + let n_tracker = s.trim_start_matches(GENERIC_TRACKER_PREFIX); + return Some(Self { + dev_type: XRDeviceType::GenericTracker, + id: n_tracker.into(), + ..Default::default() + }); + } + None + } + + pub fn from_libmonado(monado: &libmonado_rs::Monado) -> Vec { + let mut res = vec![]; + [ + XRDeviceType::Head, + XRDeviceType::Left, + XRDeviceType::Right, + XRDeviceType::Gamepad, + XRDeviceType::Eyes, + ] + .iter() + .for_each(|xrd| { + if let Ok(dev) = monado.device_from_role(xrd.to_monado_str()) { + res.push(Self { + name: dev.name, + id: dev.id.to_string(), + dev_type: xrd.clone(), + ..Default::default() + }) + } + }); + res + } + + pub fn from_log_message(s: &str) -> Vec { + let mut res = vec![]; let rows = s.split('\n'); let mut in_section = false; - let mut devs = Self::default(); for row in rows { if !in_section && row.starts_with("\tIn roles:") { in_section = true; @@ -40,64 +181,58 @@ impl XRDevices { } match row.trim().split(": ").collect::>()[..] { [_, ""] => {} - ["head", val] => devs.head = Some(val.to_string()), - ["left", val] => devs.left = Some(val.to_string()), - ["right", val] => devs.right = Some(val.to_string()), - ["gamepad", val] => devs.gamepad = Some(val.to_string()), - ["eyes", val] => devs.eyes = Some(val.to_string()), - ["hand_tracking.left", val] => devs.hand_tracking_left = Some(val.to_string()), - ["hand_tracking.right", val] => { - devs.hand_tracking_right = Some(val.to_string()) + [dev_type_s, name] => { + if let Some(xrdt) = XRDeviceType::from_monado_str(dev_type_s) { + res.push(Self { + dev_type: xrdt, + name: name.to_string(), + ..Default::default() + }); + } } _ => {} } } } - if in_section { - return Some(devs); - } - None + res } - pub fn merge(&mut self, new: Self) { - if new.head.is_some() { - self.head = new.head; + pub fn merge(old: &[Self], new: &[Self]) -> Vec { + if old.is_empty() { + return Vec::from(new); } - if new.left.is_some() { - self.left = new.left; - } - if new.right.is_some() { - self.right = new.right; - } - if new.gamepad.is_some() { - self.gamepad = new.gamepad; - } - if new.eyes.is_some() { - self.eyes = new.eyes; - } - if new.hand_tracking_left.is_some() { - self.hand_tracking_left = new.hand_tracking_left; - } - if new.hand_tracking_right.is_some() { - self.hand_tracking_right = new.hand_tracking_right; - } - if !new.generic_trackers.is_empty() { - self.generic_trackers.extend( - new.generic_trackers - .iter() - .filter(|t| !self.generic_trackers.contains(t)) - .cloned() - .collect::>(), - ); - } - } - - pub fn search_log_for_generic_trackers(&mut self, s: &str) { - if s.starts_with(GENERIC_TRACKER_PREFIX) { - let n_tracker = s.trim_start_matches(GENERIC_TRACKER_PREFIX); - if !self.generic_trackers.contains(&n_tracker.to_string()) { - self.generic_trackers.push(n_tracker.into()); + let new_dev_types = new + .iter() + .filter_map(|d| { + if d.dev_type == XRDeviceType::GenericTracker { + return None; + } + Some(d.dev_type) + }) + .collect::>(); + let mut res = old + .iter() + .filter(|d| !new_dev_types.contains(&d.dev_type)) + .map(Self::clone) + .collect::>(); + let old_tracker_ids = old + .iter() + .filter_map(|d| { + if d.dev_type == XRDeviceType::GenericTracker { + return Some(d.id.clone()); + } + None + }) + .collect::>(); + for n_dev in new { + if n_dev.dev_type == XRDeviceType::GenericTracker { + if !old_tracker_ids.contains(&n_dev.id) { + res.push(n_dev.clone()); + } + } else { + res.push(n_dev.clone()); } } + res } }