diff --git a/src/paths.rs b/src/paths.rs index 1f62891..bb34a64 100644 --- a/src/paths.rs +++ b/src/paths.rs @@ -125,3 +125,10 @@ pub fn get_ipc_file_path(xrservice_type: &XRServiceType) -> String { } ) } + +pub fn get_steamvr_bin_dir_path() -> String { + format!( + "{data}/Steam/steamapps/common/SteamVR/bin/linux64", + data = get_xdg_data_dir() + ) +} diff --git a/src/ui/main_view.rs b/src/ui/main_view.rs index ce1ac54..c793c98 100644 --- a/src/ui/main_view.rs +++ b/src/ui/main_view.rs @@ -3,6 +3,7 @@ use super::devices_box::{DevicesBox, DevicesBoxMsg}; use super::install_wivrn_box::{InstallWivrnBox, InstallWivrnBoxInit, InstallWivrnBoxMsg}; use super::profile_editor::{ProfileEditor, ProfileEditorMsg, ProfileEditorOutMsg}; use super::steam_launch_options_box::{SteamLaunchOptionsBox, SteamLaunchOptionsBoxMsg}; +use super::steamvr_calibration_box::SteamVrCalibrationBox; use crate::config::Config; use crate::constants::APP_NAME; use crate::file_utils::mount_has_nosuid; @@ -17,6 +18,7 @@ use crate::ui::app::{ BuildProfileDebugAction, ConfigFbtAction, DebugViewToggleAction, }; use crate::ui::profile_editor::ProfileEditorInit; +use crate::ui::steamvr_calibration_box::SteamVrCalibrationBoxMsg; use crate::ui::util::{limit_dropdown_width, warning_heading}; use crate::xr_devices::XRDevice; use gtk::prelude::*; @@ -47,6 +49,8 @@ pub struct MainView { #[tracker::do_not_track] profile_editor: Option>, #[tracker::do_not_track] + steamvr_calibration_box: Controller, + #[tracker::do_not_track] root_win: gtk::Window, } @@ -334,6 +338,7 @@ impl SimpleComponent for MainView { }, model.steam_launch_options_box.widget(), model.install_wivrn_box.widget(), + model.steamvr_calibration_box.widget(), gtk::Box { set_orientation: gtk::Orientation::Vertical, set_hexpand: true, @@ -368,7 +373,7 @@ impl SimpleComponent for MainView { set_label: "Calibrate", set_halign: gtk::Align::Start, connect_clicked[sender] => move |_| { - sender.output(Self::Output::OpenLibsurviveSetup).expect("Sender outut failed"); + sender.output(Self::Output::OpenLibsurviveSetup).expect("Sender output failed"); } }, }, @@ -465,6 +470,9 @@ impl SimpleComponent for MainView { } Self::Input::XRServiceActiveChanged(active, profile) => { self.set_xrservice_active(active); + self.steamvr_calibration_box + .sender() + .emit(SteamVrCalibrationBoxMsg::XRServiceActiveChanged(active)); if !active { sender.input(Self::Input::UpdateDevices(vec![])); } @@ -492,6 +500,11 @@ impl SimpleComponent for MainView { } Self::Input::UpdateSelectedProfile(prof) => { self.set_selected_profile(prof.clone()); + self.steamvr_calibration_box + .sender() + .emit(SteamVrCalibrationBoxMsg::SetVisible( + prof.lighthouse_driver == LighthouseDriver::SteamVR, + )); self.install_wivrn_box .sender() .emit(InstallWivrnBoxMsg::UpdateSelectedProfile(prof.clone())); @@ -630,6 +643,13 @@ impl SimpleComponent for MainView { }); } + let steamvr_calibration_box = SteamVrCalibrationBox::builder().launch(()).detach(); + steamvr_calibration_box + .sender() + .emit(SteamVrCalibrationBoxMsg::SetVisible( + init.selected_profile.lighthouse_driver == LighthouseDriver::SteamVR, + )); + let mut model = Self { xrservice_active: false, enable_debug_view: init.config.debug_view_enabled, @@ -647,6 +667,7 @@ impl SimpleComponent for MainView { profile_not_editable_dialog, profile_delete_confirm_dialog, root_win: init.root_win.clone(), + steamvr_calibration_box, profile_editor: None, tracker: 0, }; diff --git a/src/ui/mod.rs b/src/ui/mod.rs index d5f1d84..0fb674e 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -15,5 +15,6 @@ pub mod main_view; pub mod preference_rows; pub mod profile_editor; pub mod steam_launch_options_box; +mod steamvr_calibration_box; pub mod util; pub mod wivrn_conf_editor; diff --git a/src/ui/steamvr_calibration_box.rs b/src/ui/steamvr_calibration_box.rs new file mode 100644 index 0000000..58c8988 --- /dev/null +++ b/src/ui/steamvr_calibration_box.rs @@ -0,0 +1,210 @@ +use std::{ + collections::{HashMap, VecDeque}, + path::Path, + thread::sleep, + time::Duration, +}; + +use crate::paths::get_steamvr_bin_dir_path; + +use super::job_worker::{ + internal_worker::JobWorkerOut, + job::{FuncWorkerOut, WorkerJob}, + JobWorker, +}; +use relm4::{ + gtk::{self, prelude::*}, + ComponentParts, ComponentSender, RelmWidgetExt, SimpleComponent, +}; + +#[tracker::track] +pub struct SteamVrCalibrationBox { + calibration_running: bool, + calibration_result: Option, + calibration_success: bool, + visible: bool, + xrservice_active: bool, + #[tracker::do_not_track] + server_worker: Option, + #[tracker::do_not_track] + calibration_worker: Option, +} + +#[derive(Debug)] +pub enum SteamVrCalibrationBoxMsg { + SetVisible(bool), + RunCalibration, + OnServerWorkerExit(i32), + OnCalWorkerExit(i32), + XRServiceActiveChanged(bool), + NoOp, +} + +#[relm4::component(pub)] +impl SimpleComponent for SteamVrCalibrationBox { + type Init = (); + type Input = SteamVrCalibrationBoxMsg; + type Output = (); + + view! { + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_hexpand: true, + set_vexpand: false, + set_spacing: 12, + add_css_class: "card", + add_css_class: "padded", + #[track = "model.changed(Self::visible())"] + set_visible: model.visible, + gtk::Label { + add_css_class: "heading", + set_hexpand: true, + set_xalign: 0.0, + set_label: "SteamVR Calibration", + set_wrap: true, + set_wrap_mode: gtk::pango::WrapMode::Word, + }, + gtk::Label { + add_css_class: "dim-label", + set_hexpand: true, + set_label: concat!( + "Run a quick SteamVR calibration.\n\n", + "\u{2022} Plug in your HMD and place it on the floor, ", + "approximately in the middle of your play area\n", + "\u{2022} Make sure your controllers and other VR devices ", + "are turned off\n", + "\u{2022} Click the Calibrate button and wait for the ", + "process to finish\n\n", + "Note that the orientation of your HMD during this process ", + "will dictate the forward direction of your play area.", + ), + set_xalign: 0.0, + set_wrap: true, + set_wrap_mode: gtk::pango::WrapMode::Word, + }, + gtk::Label { + add_css_class: "error", + add_css_class: "success", + set_hexpand: true, + #[track = "model.changed(Self::calibration_result())"] + set_visible: model.calibration_result.is_some(), + #[track = "model.changed(Self::calibration_result())"] + set_label: model.calibration_result.as_ref().unwrap_or(&String::new()), + #[track = "model.changed(Self::calibration_success())"] + set_class_active: ("error", !model.calibration_success), + #[track = "model.changed(Self::calibration_success())"] + set_class_active: ("success", model.calibration_success), + set_xalign: 0.0, + set_wrap: true, + set_wrap_mode: gtk::pango::WrapMode::Word, + }, + gtk::Button { + add_css_class: "suggested-action", + set_label: "Calibrate", + set_halign: gtk::Align::Start, + #[track = "model.changed(Self::calibration_running()) || model.changed(Self::xrservice_active())"] + set_sensitive: !model.xrservice_active && !model.calibration_running, + connect_clicked[sender] => move |_| { + sender.input(Self::Input::RunCalibration); + } + }, + }, + } + + fn update(&mut self, message: Self::Input, sender: ComponentSender) { + self.reset(); + + match message { + Self::Input::SetVisible(state) => { + self.set_visible(state); + } + Self::Input::XRServiceActiveChanged(active) => { + self.set_xrservice_active(active); + } + Self::Input::RunCalibration => { + self.set_calibration_result(None); + let steamvr_bin_dir = get_steamvr_bin_dir_path(); + if !Path::new(&steamvr_bin_dir).is_dir() { + self.set_calibration_success(false); + self.set_calibration_result(Some("SteamVR not found".into())); + return; + } + let mut env: HashMap = HashMap::new(); + env.insert("LD_LIBRARY_PATH".into(), steamvr_bin_dir.clone()); + let vrcmd = format!("{steamvr_bin_dir}/vrcmd"); + let mut server_worker = { + let mut jobs: VecDeque = VecDeque::new(); + jobs.push_back(WorkerJob::new_cmd( + Some(env.clone()), + vrcmd.clone(), + Some(vec!["--pollposes".into()]), + )); + JobWorker::new(jobs, sender.input_sender(), |msg| match msg { + JobWorkerOut::Log(_) => Self::Input::NoOp, + JobWorkerOut::Exit(code) => Self::Input::OnServerWorkerExit(code), + }) + }; + let mut cal_worker = { + let mut jobs: VecDeque = VecDeque::new(); + jobs.push_back(WorkerJob::new_func(Box::new(move || { + sleep(Duration::from_secs(2)); + FuncWorkerOut { + success: true, + out: vec![], + } + }))); + jobs.push_back(WorkerJob::new_cmd( + Some(env), + vrcmd, + Some(vec!["--resetroomsetup".into()]), + )); + JobWorker::new(jobs, sender.input_sender(), |msg| match msg { + JobWorkerOut::Log(_) => Self::Input::NoOp, + JobWorkerOut::Exit(code) => Self::Input::OnCalWorkerExit(code), + }) + }; + + server_worker.start(); + cal_worker.start(); + self.server_worker = Some(server_worker); + self.calibration_worker = Some(cal_worker); + } + Self::Input::OnServerWorkerExit(_) => { + self.calibration_running = false; + } + Self::Input::OnCalWorkerExit(code) => { + self.calibration_success = code == 0; + self.calibration_result = if code == 0 { + Some("Calibration completed".into()) + } else { + Some(format!("Calibration failed with code {code}")) + }; + if let Some(sw) = self.server_worker.as_ref() { + sw.stop(); + } + } + Self::Input::NoOp => {} + } + } + + fn init( + init: Self::Init, + root: &Self::Root, + sender: ComponentSender, + ) -> ComponentParts { + let model = Self { + tracker: 0, + visible: false, + calibration_result: None, + calibration_running: false, + calibration_success: false, + xrservice_active: false, + server_worker: None, + calibration_worker: None, + }; + + let widgets = view_output!(); + + ComponentParts { model, widgets } + } +}