diff --git a/wayvr/src/backend/input.rs b/wayvr/src/backend/input.rs index 3d92e6b..27a01b4 100644 --- a/wayvr/src/backend/input.rs +++ b/wayvr/src/backend/input.rs @@ -91,29 +91,36 @@ impl InputState { if hand.now.click { hand.last_click = Instant::now(); - + if !hand.before.click { hand.click_press_start = Some(Instant::now()); - hand.interaction.long_press_click_sent = false; } } else { hand.click_press_start = None; - hand.interaction.long_press_click_sent = false; } + // Check if click has been held long enough to trigger right-click mode let long_press_threshold = Duration::from_secs_f32(session.config.long_press_duration); let is_long_press = hand .click_press_start .is_some_and(|start| start.elapsed() >= long_press_threshold); - // Prevent the mode from changing during a click (except for long press) - if !hand.before.click || is_long_press { + // Allow mode changes when: + // 1. Click just started (!hand.before.click) + // 2. A modifier became active during the click + // 3. Long press threshold reached (auto right-click fallback) + let modifier_just_activated = + (!hand.before.click_modifier_right && hand.now.click_modifier_right) || + (!hand.before.click_modifier_middle && hand.now.click_modifier_middle); + + if !hand.before.click || modifier_just_activated || (is_long_press && hand.before.click) { if hand.now.click_modifier_right { hand.interaction.mode = PointerMode::Right; - } else if is_long_press { - hand.interaction.mode = PointerMode::Right; } else if hand.now.click_modifier_middle { hand.interaction.mode = PointerMode::Middle; + } else if is_long_press { + // Auto right-click on long press if no modifier is active + hand.interaction.mode = PointerMode::Right; } else if !hand.before.click { let hmd_up = self.hmd.transform_vector3a(Vec3A::Y); let dot = hmd_up.dot(hand.pose.transform_vector3a(Vec3A::X)) @@ -541,40 +548,57 @@ where handle_scroll(&hit, hovered, app); - // click / release - normal behavior + // click / release let pointer = &mut app.input_state.pointers[hit.pointer]; - // Check if we should send a long-press click immediately - let long_press_threshold = Duration::from_secs_f32(app.session.config.long_press_duration); - let is_long_press = pointer - .click_press_start - .is_some_and(|start| start.elapsed() >= long_press_threshold); + // Check if mode changed during an active click + let mode_changed = pointer.now.click && pointer.before.click + && pointer.interaction.click_mode != pointer.interaction.mode; - if is_long_press && pointer.now.click && !pointer.interaction.long_press_click_sent { - // Immediately send right-click (press + release) - pointer.interaction.long_press_click_sent = true; - let mut hit_right = hit; - hit_right.mode = PointerMode::Right; - hovered.config.backend.on_pointer(app, &hit_right, true); - hovered.config.backend.on_pointer(app, &hit_right, false); - } else if pointer.now.click && !pointer.before.click { + if pointer.now.click && !pointer.before.click { + // Click just started pointer.interaction.clicked_id = Some(hit.overlay); + pointer.interaction.click_mode = pointer.interaction.mode; + // Ensure hit has the correct mode + hit.mode = pointer.interaction.mode; update_focus( &mut app.hid_provider.keyboard_focus, hovered.config.keyboard_focus, ); hovered.config.backend.on_pointer(app, &hit, true); - } else if !pointer.now.click && pointer.before.click { - // Only send release if we haven't already sent a long press click - if !pointer.interaction.long_press_click_sent { - if let Some(clicked_id) = pointer.interaction.clicked_id.take() { - if let Some(clicked) = overlays.mut_by_id(clicked_id) { - clicked.config.backend.on_pointer(app, &hit, false); - } - } else { - hovered.config.backend.on_pointer(app, &hit, false); + } else if mode_changed { + // Mode changed during click - send release with old mode, then press with new mode + let mut old_hit = hit; + old_hit.mode = pointer.interaction.click_mode; + + // Send release with old mode + if let Some(clicked_id) = pointer.interaction.clicked_id { + if let Some(clicked) = overlays.mut_by_id(clicked_id) { + clicked.config.backend.on_pointer(app, &old_hit, false); } } + + // Update to new mode and send press + let pointer = &mut app.input_state.pointers[hit.pointer]; + pointer.interaction.click_mode = pointer.interaction.mode; + pointer.interaction.clicked_id = Some(hit.overlay); + + // Ensure hit has the new mode + hit.mode = pointer.interaction.mode; + + if let Some(hovered) = overlays.mut_by_id(hit.overlay) { + // Send press with new mode + hovered.config.backend.on_pointer(app, &hit, true); + } + } else if !pointer.now.click && pointer.before.click { + // Click released + if let Some(clicked_id) = pointer.interaction.clicked_id.take() { + if let Some(clicked) = overlays.mut_by_id(clicked_id) { + clicked.config.backend.on_pointer(app, &hit, false); + } + } else { + hovered.config.backend.on_pointer(app, &hit, false); + } } (Some((hit, raw_hit)), haptics.or(pending_haptics)) @@ -605,7 +629,6 @@ fn handle_no_hit( // send release event to overlay that was originally clicked if !pointer.now.click && pointer.before.click - && !pointer.interaction.long_press_click_sent && let Some(clicked_id) = pointer.interaction.clicked_id.take() { let hit = PointerHit { diff --git a/wayvr/src/backend/openxr/input.rs b/wayvr/src/backend/openxr/input.rs index eb690eb..1055045 100644 --- a/wayvr/src/backend/openxr/input.rs +++ b/wayvr/src/backend/openxr/input.rs @@ -43,6 +43,76 @@ pub struct MultiClickHandler { held_inactive: bool, } +pub struct LongPressHandler { + name: String, + action_f32: xr::Action, + action_bool: xr::Action, + press_start: Option, + activated: bool, +} + +impl LongPressHandler { + fn new(action_set: &xr::ActionSet, action_name: &str, side: &str) -> anyhow::Result { + let name = format!("{side}_long-{action_name}"); + let name_f32 = format!("{}_value", &name); + + let action_bool = action_set.create_action::(&name, &name, &[])?; + let action_f32 = action_set.create_action::(&name_f32, &name_f32, &[])?; + + Ok(Self { + name, + action_f32, + action_bool, + press_start: None, + activated: false, + }) + } + + fn check( + &mut self, + session: &xr::Session, + threshold: f32, + long_press_duration: f32, + ) -> anyhow::Result { + let res = self.action_bool.state(session, xr::Path::NULL)?; + let mut state = res.is_active && res.current_state; + + if !state { + let res = self.action_f32.state(session, xr::Path::NULL)?; + state = res.is_active && res.current_state >= threshold - 0.001; + } + + if !state { + // Button released, reset state + self.press_start = None; + self.activated = false; + return Ok(false); + } + + // Button is pressed + if self.press_start.is_none() { + self.press_start = Some(Instant::now()); + self.activated = false; + } + + // Check if we've held long enough + if let Some(start) = self.press_start { + let elapsed = start.elapsed().as_secs_f32(); + if elapsed >= long_press_duration && !self.activated { + log::trace!("{}: long press activated", self.name); + self.activated = true; + return Ok(true); + } + } + + Ok(self.activated) + } + + fn is_pending(&self) -> bool { + self.press_start.is_some() && !self.activated + } +} + impl MultiClickHandler { fn new(action_set: &xr::ActionSet, action_name: &str, side: &str) -> anyhow::Result { let name = format!("{side}_{COUNT}-{action_name}"); @@ -112,19 +182,19 @@ impl MultiClickHandler { pub struct CustomClickAction { single: MultiClickHandler<0>, double: MultiClickHandler<1>, - triple: MultiClickHandler<2>, + long_press: LongPressHandler, } impl CustomClickAction { pub fn new(action_set: &xr::ActionSet, name: &str, side: &str) -> anyhow::Result { let single = MultiClickHandler::new(action_set, name, side)?; let double = MultiClickHandler::new(action_set, name, side)?; - let triple = MultiClickHandler::new(action_set, name, side)?; + let long_press = LongPressHandler::new(action_set, name, side)?; Ok(Self { single, double, - triple, + long_press, }) } pub fn state( @@ -141,7 +211,7 @@ impl CustomClickAction { Ok(self.single.check(&state.session, threshold)? || self.double.check(&state.session, threshold)? - || self.triple.check(&state.session, threshold)?) + || self.long_press.check(&state.session, threshold, session.config.long_press_duration)?) } } @@ -435,7 +505,14 @@ impl OpenXrPointer { xr: &XrState, session: &AppSession, ) -> anyhow::Result<()> { - pointer.now.click = self.source.click.state(pointer.before.click, xr, session)?; + let long_press_pending = self.source.modifier_right.long_press.is_pending() + || self.source.modifier_middle.long_press.is_pending(); + + pointer.now.click = if long_press_pending { + false + } else { + self.source.click.state(pointer.before.click, xr, session)? + }; pointer.now.grab = self.source.grab.state(pointer.before.grab, xr, session)?; @@ -574,9 +651,9 @@ macro_rules! add_custom { for s in iter { if let Some(p) = to_paths(Some(s.as_str()), $instance) { if is_bool(s) { - if action.triple_click.unwrap_or(false) { + if action.long_press.unwrap_or(false) { $bindings.push(xr::Binding::new( - &$hands[i].$field.triple.action_bool, + &$hands[i].$field.long_press.action_bool, p, )); } else if action.double_click.unwrap_or(false) { @@ -591,9 +668,9 @@ macro_rules! add_custom { )); } } else { - if action.triple_click.unwrap_or(false) { + if action.long_press.unwrap_or(false) { $bindings.push(xr::Binding::new( - &$hands[i].$field.triple.action_f32, + &$hands[i].$field.long_press.action_f32, p, )); } else if action.double_click.unwrap_or(false) { @@ -739,6 +816,7 @@ struct OpenXrActionConfAction { threshold: Option<[f32; 2]>, double_click: Option, triple_click: Option, + long_press: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/wayvr/src/backend/openxr/openxr_actions.json5 b/wayvr/src/backend/openxr/openxr_actions.json5 index 30574e0..bbd1892 100644 --- a/wayvr/src/backend/openxr/openxr_actions.json5 +++ b/wayvr/src/backend/openxr/openxr_actions.json5 @@ -32,6 +32,16 @@ // // -- pose, haptic -- // do not mess with these, unless you know what you're doing +// +// Binding modifiers (optional): +// - double_click: true - require double-clicking the binding +// - long_press: true - activate after holding the binding for long_press_duration (config) +// +// Example usage: +// click_modifier_right: { +// long_press: true, +// left: "/user/hand/left/input/trigger/value" +// } [ // Eye+hand interaction