use std::time::{Duration, Instant}; use glam::{bool, Affine3A, Quat, Vec3}; use openxr as xr; use serde::{Deserialize, Serialize}; use crate::{ backend::input::{Haptics, Pointer}, config_io, state::{AppSession, AppState}, }; use super::XrState; type XrSession = xr::Session; static DOUBLE_CLICK_TIME: Duration = Duration::from_millis(500); pub(super) struct OpenXrAction {} pub(super) struct OpenXrInputSource { action_set: xr::ActionSet, hands: [OpenXrHand; 2], } pub(super) struct OpenXrHand { source: OpenXrHandSource, space: xr::Space, } pub struct CustomClickAction { action_f32: xr::Action, action_bool: xr::Action, action_f32_double: xr::Action, action_bool_double: xr::Action, last_click: Option, held: bool, } impl CustomClickAction { pub fn new(action_set: &xr::ActionSet, name: &str, side: &str) -> anyhow::Result { let action_f32 = action_set.create_action::( &format!("{}_{}_value", side, name), &format!("{} hand {} value", side, name), &[], )?; let action_f32_double = action_set.create_action::( &format!("{}_{}_value_double", side, name), &format!("{} hand {} value double", side, name), &[], )?; let action_bool = action_set.create_action::( &format!("{}_{}", side, name), &format!("{} hand {}", side, name), &[], )?; let action_bool_double = action_set.create_action::( &format!("{}_{}_double", side, name), &format!("{} hand {} double", side, name), &[], )?; Ok(Self { action_f32, action_f32_double, action_bool, action_bool_double, last_click: None, held: false, }) } pub fn state( &mut self, before: bool, state: &XrState, session: &AppSession, ) -> anyhow::Result { let res = self.action_bool.state(&state.session, xr::Path::NULL)?; if res.is_active && res.current_state { return Ok(true); } let res = self .action_bool_double .state(&state.session, xr::Path::NULL)?; if res.is_active && self.check_double_click(res.current_state) { return Ok(true); } let threshold = if before { session.config.xr_click_sensitivity_release } else { session.config.xr_click_sensitivity }; let res = self.action_f32.state(&state.session, xr::Path::NULL)?; if res.is_active && res.current_state > threshold { return Ok(true); } let res = self.action_f32.state(&state.session, xr::Path::NULL)?; if res.is_active && self.check_double_click(res.current_state > threshold) { return Ok(true); } Ok(false) } // submit a click. returns true if it should count as a double click fn check_double_click(&mut self, state: bool) -> bool { if !state { self.held = false; return false; } if self.held { return false; } let now = Instant::now(); let double_click = match self.last_click { Some(last_click) => now - last_click < DOUBLE_CLICK_TIME, None => false, }; self.last_click = if double_click { None } else { Some(now) }; self.held = true; double_click } } pub(super) struct OpenXrHandSource { action_pose: xr::Action, action_click: CustomClickAction, action_grab: CustomClickAction, action_alt_click: CustomClickAction, action_show_hide: CustomClickAction, action_space_drag: CustomClickAction, action_modifier_right: CustomClickAction, action_modifier_middle: CustomClickAction, action_move_mouse: CustomClickAction, action_scroll: xr::Action, action_haptics: xr::Action, } impl OpenXrInputSource { pub fn new(xr: &XrState) -> anyhow::Result { let mut action_set = xr.session .instance() .create_action_set("wlx-overlay-s", "WlxOverlay-S Actions", 0)?; let left_source = OpenXrHandSource::new(&mut action_set, "left")?; let right_source = OpenXrHandSource::new(&mut action_set, "right")?; suggest_bindings(&xr.instance, &[&left_source, &right_source])?; xr.session.attach_action_sets(&[&action_set])?; Ok(Self { action_set, hands: [ OpenXrHand::new(xr, left_source)?, OpenXrHand::new(xr, right_source)?, ], }) } pub fn haptics(&self, xr: &XrState, hand: usize, haptics: &Haptics) { let action = &self.hands[hand].source.action_haptics; let duration_nanos = (haptics.duration as f64) * 1_000_000_000.0; let _ = action.apply_feedback( &xr.session, xr::Path::NULL, &xr::HapticVibration::new() .amplitude(haptics.intensity) .frequency(haptics.frequency) .duration(xr::Duration::from_nanos(duration_nanos as _)), ); } pub fn update(&mut self, xr: &XrState, state: &mut AppState) -> anyhow::Result<()> { xr.session.sync_actions(&[(&self.action_set).into()])?; for i in 0..2 { self.hands[i].update(&mut state.input_state.pointers[i], xr, &state.session)?; } Ok(()) } } impl OpenXrHand { pub(super) fn new(xr: &XrState, source: OpenXrHandSource) -> Result { let space = source.action_pose.create_space( xr.session.clone(), xr::Path::NULL, xr::Posef::IDENTITY, )?; Ok(Self { source, space }) } pub(super) fn update( &mut self, pointer: &mut Pointer, xr: &XrState, session: &AppSession, ) -> anyhow::Result<()> { let location = self.space.locate(&xr.stage, xr.predicted_display_time)?; if location .location_flags .contains(xr::SpaceLocationFlags::ORIENTATION_VALID) { let quat = unsafe { std::mem::transmute::<_, Quat>(location.pose.orientation) }; let pos = unsafe { std::mem::transmute::<_, Vec3>(location.pose.position) }; pointer.pose = Affine3A::from_rotation_translation(quat, pos); } pointer.now.click = self .source .action_click .state(pointer.before.click, xr, session)?; pointer.now.grab = self .source .action_grab .state(pointer.before.grab, xr, session)?; pointer.now.scroll = self .source .action_scroll .state(&xr.session, xr::Path::NULL)? .current_state; pointer.now.alt_click = self.source .action_alt_click .state(pointer.before.alt_click, xr, session)?; pointer.now.show_hide = self.source .action_show_hide .state(pointer.before.show_hide, xr, session)?; pointer.now.click_modifier_right = self.source.action_modifier_right.state( pointer.before.click_modifier_right, xr, session, )?; pointer.now.click_modifier_middle = self.source.action_modifier_middle.state( pointer.before.click_modifier_middle, xr, session, )?; pointer.now.move_mouse = self.source .action_move_mouse .state(pointer.before.move_mouse, xr, session)?; pointer.now.space_drag = self.source .action_space_drag .state(pointer.before.space_drag, xr, session)?; Ok(()) } } // supported action types: Haptic, Posef, Vector2f, f32, bool impl OpenXrHandSource { pub(super) fn new(action_set: &mut xr::ActionSet, side: &str) -> anyhow::Result { let action_pose = action_set.create_action::( &format!("{}_hand", side), &format!("{} hand pose", side), &[], )?; let action_scroll = action_set.create_action::( &format!("{}_scroll", side), &format!("{} hand scroll", side), &[], )?; let action_haptics = action_set.create_action::( &format!("{}_haptics", side), &format!("{} hand haptics", side), &[], )?; Ok(Self { action_pose, action_click: CustomClickAction::new(action_set, "click", side)?, action_grab: CustomClickAction::new(action_set, "grab", side)?, action_scroll, action_alt_click: CustomClickAction::new(action_set, "alt_click", side)?, action_show_hide: CustomClickAction::new(action_set, "show_hide", side)?, action_space_drag: CustomClickAction::new(action_set, "space_drag", side)?, action_modifier_right: CustomClickAction::new( action_set, "click_modifier_right", side, )?, action_modifier_middle: CustomClickAction::new( action_set, "click_modifier_middle", side, )?, action_move_mouse: CustomClickAction::new(action_set, "move_mouse", side)?, action_haptics, }) } } fn to_path(maybe_path_str: &Option, instance: &xr::Instance) -> Option { maybe_path_str .as_ref() .and_then(|s| match instance.string_to_path(s) { Ok(path) => Some(path), Err(_) => { log::warn!("Invalid binding path: {}", s); None } }) } fn is_bool(maybe_type_str: &Option) -> bool { maybe_type_str .as_ref() .unwrap() .split('/') .last() .map(|last| matches!(last, "click" | "touch")) .unwrap_or(false) } macro_rules! add_custom { ($action:expr, $left:expr, $right:expr, $bindings:expr, $instance:expr) => { if let Some(action) = $action.as_ref() { if let Some(p) = to_path(&action.left, $instance) { if is_bool(&action.left) { if action.double_click.unwrap_or(false) { $bindings.push(xr::Binding::new(&$left.action_bool_double, p)); } else { $bindings.push(xr::Binding::new(&$left.action_bool, p)); } } else { if action.double_click.unwrap_or(false) { $bindings.push(xr::Binding::new(&$left.action_f32_double, p)); } else { $bindings.push(xr::Binding::new(&$left.action_f32, p)); } } } if let Some(p) = to_path(&action.right, $instance) { if is_bool(&action.right) { if action.double_click.unwrap_or(false) { $bindings.push(xr::Binding::new(&$right.action_bool_double, p)); } else { $bindings.push(xr::Binding::new(&$right.action_bool, p)); } } else { if action.double_click.unwrap_or(false) { $bindings.push(xr::Binding::new(&$right.action_f32_double, p)); } else { $bindings.push(xr::Binding::new(&$right.action_f32, p)); } } } } }; } fn suggest_bindings(instance: &xr::Instance, hands: &[&OpenXrHandSource; 2]) -> anyhow::Result<()> { let profiles = load_action_profiles()?; for profile in profiles { let Ok(profile_path) = instance.string_to_path(&profile.profile) else { log::debug!("Profile not supported: {}", profile.profile); continue; }; let mut bindings: Vec = vec![]; if let Some(action) = profile.pose { if let Some(p) = to_path(&action.left, instance) { bindings.push(xr::Binding::new(&hands[0].action_pose, p)); } if let Some(p) = to_path(&action.right, instance) { bindings.push(xr::Binding::new(&hands[1].action_pose, p)); } } if let Some(action) = profile.haptics { if let Some(p) = to_path(&action.left, instance) { bindings.push(xr::Binding::new(&hands[0].action_haptics, p)); } if let Some(p) = to_path(&action.right, instance) { bindings.push(xr::Binding::new(&hands[1].action_haptics, p)); } } if let Some(action) = profile.scroll { if let Some(p) = to_path(&action.left, instance) { bindings.push(xr::Binding::new(&hands[0].action_scroll, p)); } if let Some(p) = to_path(&action.right, instance) { bindings.push(xr::Binding::new(&hands[1].action_scroll, p)); } } add_custom!( profile.click, hands[0].action_click, hands[1].action_click, bindings, instance ); add_custom!( profile.alt_click, &hands[0].action_alt_click, &hands[1].action_alt_click, bindings, instance ); add_custom!( profile.grab, &hands[0].action_grab, &hands[1].action_grab, bindings, instance ); add_custom!( profile.show_hide, &hands[0].action_show_hide, &hands[1].action_show_hide, bindings, instance ); add_custom!( profile.space_drag, &hands[0].action_space_drag, &hands[1].action_space_drag, bindings, instance ); add_custom!( profile.click_modifier_right, &hands[0].action_modifier_right, &hands[1].action_modifier_right, bindings, instance ); add_custom!( profile.click_modifier_middle, &hands[0].action_modifier_middle, &hands[1].action_modifier_middle, bindings, instance ); add_custom!( profile.move_mouse, &hands[0].action_move_mouse, &hands[1].action_move_mouse, bindings, instance ); if instance .suggest_interaction_profile_bindings(profile_path, &bindings) .is_err() { log::error!("Bad bindings for {}", &profile.profile[22..]); log::error!("Verify config: ~/.config/wlxoverlay/openxr_actions.json5"); } } Ok(()) } #[derive(Debug, Clone, Serialize, Deserialize)] struct OpenXrActionConfAction { left: Option, right: Option, threshold: Option<[f32; 2]>, double_click: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] struct OpenXrActionConfProfile { profile: String, pose: Option, click: Option, grab: Option, alt_click: Option, show_hide: Option, space_drag: Option, click_modifier_right: Option, click_modifier_middle: Option, move_mouse: Option, scroll: Option, haptics: Option, } const DEFAULT_PROFILES: &str = include_str!("openxr_actions.json5"); fn load_action_profiles() -> anyhow::Result> { let mut profiles: Vec = serde_json5::from_str(DEFAULT_PROFILES).unwrap(); // want panic let Some(conf) = config_io::load("openxr_actions.json5") else { return Ok(profiles); }; match serde_json5::from_str::>(&conf) { Ok(override_profiles) => { override_profiles.into_iter().for_each(|new| { if let Some(i) = profiles.iter().position(|old| old.profile == new.profile) { profiles[i] = new; } }); } Err(e) => { log::error!("Failed to load openxr_actions.json5: {}", e); } } Ok(profiles) }