From 280988e1f765c565c78cd69c132d2acc732bac47 Mon Sep 17 00:00:00 2001 From: Gabriele Musco Date: Fri, 16 Jun 2023 23:31:56 +0200 Subject: [PATCH] feat: initial ui for importing steamvr calibration to libsurvive --- Cargo.lock | 1 + Cargo.toml | 3 + src/ui/app.rs | 32 ++- src/ui/libsurvive_setup_window.rs | 350 ++++++++++++++++++++++++++++++ src/ui/main_view.rs | 3 +- src/ui/mod.rs | 1 + 6 files changed, 387 insertions(+), 3 deletions(-) create mode 100644 src/ui/libsurvive_setup_window.rs diff --git a/Cargo.lock b/Cargo.lock index ed53f4e..88cb51c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1119,6 +1119,7 @@ name = "rex2" version = "0.1.0" dependencies = [ "expect-dialog", + "gtk4", "nix", "relm4", "relm4-components", diff --git a/Cargo.toml b/Cargo.toml index a192119..935c53e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,9 @@ edition = "2021" [dependencies] expect-dialog = "1.0.0" +gtk4 = { version = "0.6.6", features = [ + "v4_10" +] } nix = "0.26.2" relm4 = { version = "0.6.0", features = [ "libadwaita" diff --git a/src/ui/app.rs b/src/ui/app.rs index dba8bb6..b6739af 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -1,6 +1,7 @@ use super::about_dialog::AboutDialog; use super::build_window::BuildWindow; use super::debug_view::{DebugView, DebugViewMsg}; +use super::libsurvive_setup_window::LibsurviveSetupWindow; use super::main_view::MainViewMsg; use crate::builders::build_libsurvive::get_build_libsurvive_runner; use crate::builders::build_monado::get_build_monado_runner; @@ -22,12 +23,13 @@ use crate::runner::{Runner, RunnerStatus}; use crate::runner_pipeline::RunnerPipeline; use crate::ui::build_window::BuildWindowMsg; use crate::ui::debug_view::DebugViewInit; +use crate::ui::libsurvive_setup_window::LibsurviveSetupMsg; use crate::ui::main_view::{MainView, MainViewInit, MainViewOutMsg}; use expect_dialog::ExpectDialog; use gtk::prelude::*; use relm4::actions::{ActionGroupName, RelmAction, RelmActionGroup}; -use relm4::adw::ResponseAppearance; use relm4::adw::traits::MessageDialogExt; +use relm4::adw::ResponseAppearance; use relm4::gtk::glib; use relm4::{new_action_group, new_stateful_action, new_stateless_action, prelude::*}; use relm4::{ComponentParts, ComponentSender, SimpleComponent}; @@ -49,6 +51,8 @@ pub struct App { dependencies_dialog: adw::MessageDialog, #[tracker::do_not_track] setcap_confirm_dialog: adw::MessageDialog, + #[tracker::do_not_track] + libsurvive_setup_window: Controller, #[tracker::do_not_track] config: Config, @@ -71,6 +75,7 @@ pub enum Msg { ProfileSelected(String), SetMonadoRuntime(bool), RunSetCap, + OpenLibsurviveSetup, } impl App { @@ -264,7 +269,10 @@ impl SimpleComponent for App { pipeline.start(); self.build_window .sender() - .emit(BuildWindowMsg::UpdateTitle(format!("Building Profile {}", profile.name))); + .emit(BuildWindowMsg::UpdateTitle(format!( + "Building Profile {}", + profile.name + ))); self.build_window .sender() .emit(BuildWindowMsg::UpdateCanClose(false)); @@ -288,6 +296,13 @@ impl SimpleComponent for App { set_current_openvrpaths_to_steam(); } } + Msg::OpenLibsurviveSetup => { + self.libsurvive_setup_window + .sender() + .send(LibsurviveSetupMsg::Present( + self.get_selected_profile().clone(), + )); + } } } @@ -357,6 +372,10 @@ impl SimpleComponent for App { .transient_for(root) .launch(()) .detach(), + libsurvive_setup_window: LibsurviveSetupWindow::builder() + .transient_for(root) + .launch(()) + .detach(), dependencies_dialog, setcap_confirm_dialog, enable_debug_view: config.debug_view_enabled, @@ -385,6 +404,13 @@ impl SimpleComponent for App { }) }; + let libsurvive_setup_action = { + let lss_sender = sender.clone(); + RelmAction::::new_stateless(move |_| { + lss_sender.input(Msg::OpenLibsurviveSetup); + }) + }; + let debug_view_toggle_action: RelmAction = { let debugtoggle_sender = sender.clone(); RelmAction::::new_stateful( @@ -400,6 +426,7 @@ impl SimpleComponent for App { actions.add_action(about_action); actions.add_action(buildprofile_action); actions.add_action(debug_view_toggle_action); + actions.add_action(libsurvive_setup_action); root.insert_action_group(AppActionGroup::NAME, Some(&actions.into_action_group())); @@ -423,4 +450,5 @@ impl SimpleComponent for App { new_action_group!(pub AppActionGroup, "win"); new_stateless_action!(pub AboutAction, AppActionGroup, "about"); new_stateless_action!(pub BuildProfileAction, AppActionGroup, "buildprofile"); +new_stateless_action!(pub LibsurviveSetupAction, AppActionGroup, "libsurvivesetup"); new_stateful_action!(pub DebugViewToggleAction, AppActionGroup, "debugviewtoggle", (), bool); diff --git a/src/ui/libsurvive_setup_window.rs b/src/ui/libsurvive_setup_window.rs new file mode 100644 index 0000000..c02d569 --- /dev/null +++ b/src/ui/libsurvive_setup_window.rs @@ -0,0 +1,350 @@ +use crate::{profile::Profile, runner::Runner}; +use gtk::prelude::*; +use relm4::prelude::*; +use std::path::Path; + +const NO_FILE_MSG: &str = "(No file selected)"; + +#[tracker::track] +pub struct LibsurviveSetupWindow { + steam_lighthouse_path: String, + + #[tracker::do_not_track] + win: Option, + #[tracker::do_not_track] + progressbar: Option, + #[tracker::do_not_track] + carousel: Option, + #[tracker::do_not_track] + loading_page: Option, + #[tracker::do_not_track] + filefilter_listmodel: gtk::gio::ListStore, + + #[tracker::do_not_track] + profile: Option, + #[tracker::do_not_track] + calibration_runner: Option, +} + +#[derive(Debug)] +pub enum LibsurviveSetupMsg { + Present(Profile), + CalibrateLibsurvive, + SetSteamLighthousePath(Option), + ChooseFileDialog, +} + +#[relm4::component(pub)] +impl SimpleComponent for LibsurviveSetupWindow { + type Init = (); + type Input = LibsurviveSetupMsg; + type Output = (); + + view! { + #[name(win)] + adw::Window { + set_modal: true, + set_default_size: (520, 400), + set_hide_on_close: true, + connect_close_request[sender, carousel, page1, progressbar] => move |_| { + carousel.scroll_to(&page1, true); + progressbar.set_fraction(0.0); + sender.input(LibsurviveSetupMsg::SetSteamLighthousePath(None)); + gtk::Inhibit(false) + }, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_hexpand: true, + set_vexpand: true, + set_spacing: 12, + set_margin_all: 12, + gtk::WindowHandle { + adw::HeaderBar { + add_css_class: "flat", + #[wrap(Some)] + set_title_widget: title_label = >k::Label { + set_label: "Setup Lighthouses", + add_css_class: "title", + }, + } + }, + #[name(carousel)] + adw::Carousel { + set_allow_long_swipes: false, + set_allow_scroll_wheel: false, + set_allow_mouse_drag: false, + set_hexpand: true, + set_vexpand: true, + #[name(page1)] + adw::StatusPage { + set_hexpand: true, + set_vexpand: true, + set_title: "Let's Get Started", + set_description: Some(concat!( + "This procedure will guide you through importing ", + "your SteamVR lighthouse calibration into Libsurvive.", + )), + gtk::Button { + set_hexpand: true, + set_halign: gtk::Align::Center, + add_css_class: "pill", + add_css_class: "suggested-action", + set_label: "Start Setup", + connect_clicked[carousel, page2] => move |_| { + carousel.scroll_to( + &page2, true + ); + }, + } + }, + #[name(page2)] + adw::StatusPage { + set_hexpand: true, + set_vexpand: true, + set_title: "Train Your SteamVR Calibration", + set_description: Some(concat!( + "If you've already played extensively with SteamVR ", + "(either on Linux or on Windows), chances are you ", + "already have an adequate calibration profile; ", + "then you can skip this step.\n\n", + "If not, you can plug in your HMD, start SteamVR and ", + "slowly move your headset around your play space for ", + "a while. This will generate the necessary calibration ", + "data that will be imported by Libsurvive.", + )), + gtk::Button { + set_hexpand: true, + set_halign: gtk::Align::Center, + add_css_class: "pill", + add_css_class: "suggested-action", + set_label: "Next", + connect_clicked[carousel, page3] => move |_| { + carousel.scroll_to( + &page3, true + ); + }, + } + }, + #[name(page3)] + adw::StatusPage { + set_hexpand: true, + set_vexpand: true, + set_title: "Import Calibration into Libsurvive", + set_description: Some(concat!( + "Plug in your HMD and place it on the floor, ", + "preferably in the middle of your play space, and ", + "in direct line of sight of all of your lighthouses.\n\n", + "Then select the SteamVR calibration file from your disk.", + "\n\nFinally, you can press the \"Import Calibration\" button.", + )), + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_spacing: 12, + adw::Bin { + set_hexpand: true, + add_css_class: "card", + set_halign: gtk::Align::Center, + gtk::Grid { + set_column_spacing: 6, + set_row_spacing: 6, + set_margin_all: 12, + set_hexpand: true, + set_halign: gtk::Align::Center, + attach: ( + >k::Label::builder() + .use_markup(true) + .label("Linux") + .xalign(1.0) + .build(), + 0, 0, 1, 1 + ), + attach: ( + >k::Label::builder() + .use_markup(true) + .label("~/.steam/steam/config/lighthouse/lighthousedb.json") + .wrap(true) + .wrap_mode(gtk::pango::WrapMode::Char) + .selectable(true) + .xalign(0.0) + .build(), + 1, 0, 1, 1 + ), + attach: ( + >k::Label::builder() + .use_markup(true) + .label("Windows") + .xalign(1.0) + .build(), + 0, 1, 1, 1 + ), + attach: ( + >k::Label::builder() + .use_markup(true) + .label( + "\"C:\\Program Files (x86)\\steam\\config\\lighthouse\\lighthouse.json\"" + ) + .wrap(true) + .wrap_mode(gtk::pango::WrapMode::Char) + .selectable(true) + .xalign(0.0) + .build(), + 1, 1, 1, 1 + ), + }, + }, + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_spacing: 12, + set_hexpand: true, + gtk::Button { + set_label: "Select File", + connect_clicked[sender] => move |_| { + sender.input(LibsurviveSetupMsg::ChooseFileDialog); + } + }, + gtk::Label { + #[track = "model.changed(LibsurviveSetupWindow::steam_lighthouse_path())"] + set_label: model.steam_lighthouse_path.as_str(), + #[track = "model.changed(LibsurviveSetupWindow::steam_lighthouse_path())"] + set_tooltip_text: Some(model.steam_lighthouse_path.as_str()), + set_ellipsize: gtk::pango::EllipsizeMode::Start, + set_hexpand: true, + set_xalign: 1.0, + } + }, + gtk::Button { + set_hexpand: true, + set_halign: gtk::Align::Center, + add_css_class: "pill", + add_css_class: "suggested-action", + set_label: "Import Calibration", + #[track = "model.changed(LibsurviveSetupWindow::steam_lighthouse_path())"] + set_sensitive: model.steam_lighthouse_path != NO_FILE_MSG, + connect_clicked[sender] => move |_| { + sender.input( + LibsurviveSetupMsg::CalibrateLibsurvive + ); + }, + } + } + }, + #[name(loading_page)] + adw::StatusPage { + set_hexpand: true, + set_vexpand: true, + set_title: "Importing Calibration...\nDo Not Touch your Headset!", + set_description: Some("Please stand by"), + #[name(progressbar)] + gtk::ProgressBar { + set_margin_top: 12, + set_margin_bottom: 12, + set_margin_start: 24, + set_margin_end: 24, + set_fraction: 0.0, + set_hexpand: true, + } + } + }, + adw::CarouselIndicatorDots { + set_carousel: Some(&carousel), + } + } + } + } + + fn update(&mut self, message: Self::Input, sender: ComponentSender) { + self.reset(); + + match message { + LibsurviveSetupMsg::Present(prof) => { + self.profile = Some(prof); + self.win.as_ref().unwrap().present(); + } + LibsurviveSetupMsg::CalibrateLibsurvive => { + if self.steam_lighthouse_path == NO_FILE_MSG { + return; + } + let lh_path = Path::new(&self.steam_lighthouse_path); + if lh_path.is_file() { + self.calibration_runner = Some(Runner::new( + None, + format!( + "{pfx}/bin/survive-cli", + pfx = self.profile.as_ref().unwrap().prefix + ), + vec![ + "--steamvr-calibration".into(), + lh_path.to_str().unwrap().to_string(), + ], + )); + self.carousel + .as_ref() + .unwrap() + .scroll_to(self.loading_page.as_ref().unwrap(), true); + } + } + LibsurviveSetupMsg::SetSteamLighthousePath(n_path) => { + self.set_steam_lighthouse_path(match n_path { + None => NO_FILE_MSG.into(), + Some(p) => p, + }); + } + LibsurviveSetupMsg::ChooseFileDialog => { + let chooser = gtk::FileDialog::builder() + .modal(true) + .title("Select SteamVR Calibration") + .filters(&self.filefilter_listmodel) + .build(); + let fd_sender = sender.clone(); + chooser.open( + Some(&self.win.as_ref().unwrap().clone()), + gtk::gio::Cancellable::NONE, + move |res| match res { + Ok(file) => { + let path = file.path(); + if path.is_some() { + fd_sender.input(LibsurviveSetupMsg::SetSteamLighthousePath(Some( + path.unwrap().to_str().unwrap().to_string(), + ))) + } + } + _ => {} + }, + ); + } + } + } + + fn init( + init: Self::Init, + root: &Self::Root, + sender: relm4::ComponentSender, + ) -> relm4::ComponentParts { + let json_filter = gtk::FileFilter::new(); + json_filter.add_mime_type("application/json"); + let mut model = LibsurviveSetupWindow { + win: None, + progressbar: None, + carousel: None, + loading_page: None, + steam_lighthouse_path: NO_FILE_MSG.into(), + filefilter_listmodel: gtk4::gio::ListStore::builder() + .item_type(gtk::FileFilter::static_type()) + .build(), + profile: None, + calibration_runner: None, + tracker: 0, + }; + + model.filefilter_listmodel.append(&json_filter); + + let widgets = view_output!(); + + model.win = Some(widgets.win.clone()); + model.progressbar = Some(widgets.progressbar.clone()); + model.carousel = Some(widgets.carousel.clone()); + model.loading_page = Some(widgets.loading_page.clone()); + + ComponentParts { model, widgets } + } +} diff --git a/src/ui/main_view.rs b/src/ui/main_view.rs index 0f6b7e4..91b84ab 100644 --- a/src/ui/main_view.rs +++ b/src/ui/main_view.rs @@ -1,7 +1,7 @@ use crate::config::Config; use crate::constants::APP_NAME; use crate::file_builders::active_runtime_json::{self, get_current_active_runtime}; -use crate::ui::app::{AboutAction, BuildProfileAction, DebugViewToggleAction}; +use crate::ui::app::{AboutAction, BuildProfileAction, DebugViewToggleAction, LibsurviveSetupAction}; use expect_dialog::ExpectDialog; use gtk::prelude::*; use relm4::prelude::*; @@ -51,6 +51,7 @@ impl SimpleComponent for MainView { // value inside action is ignored "_Debug View" => DebugViewToggleAction, "_Build Profile" => BuildProfileAction, + "_Calibrate Lighthouses" => LibsurviveSetupAction, }, section! { "_About" => AboutAction, diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 174fed0..1f5832c 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -3,3 +3,4 @@ pub mod main_view; pub mod about_dialog; pub mod debug_view; pub mod build_window; +pub mod libsurvive_setup_window;