added long_press as a option in binding system
Some checks failed
Build AppImage / build_appimage (push) Failing after 2m2s
Some checks failed
Build AppImage / build_appimage (push) Failing after 2m2s
This commit is contained in:
@@ -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<O>(
|
||||
// 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 {
|
||||
|
||||
@@ -43,6 +43,76 @@ pub struct MultiClickHandler<const COUNT: usize> {
|
||||
held_inactive: bool,
|
||||
}
|
||||
|
||||
pub struct LongPressHandler {
|
||||
name: String,
|
||||
action_f32: xr::Action<f32>,
|
||||
action_bool: xr::Action<bool>,
|
||||
press_start: Option<Instant>,
|
||||
activated: bool,
|
||||
}
|
||||
|
||||
impl LongPressHandler {
|
||||
fn new(action_set: &xr::ActionSet, action_name: &str, side: &str) -> anyhow::Result<Self> {
|
||||
let name = format!("{side}_long-{action_name}");
|
||||
let name_f32 = format!("{}_value", &name);
|
||||
|
||||
let action_bool = action_set.create_action::<bool>(&name, &name, &[])?;
|
||||
let action_f32 = action_set.create_action::<f32>(&name_f32, &name_f32, &[])?;
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
action_f32,
|
||||
action_bool,
|
||||
press_start: None,
|
||||
activated: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn check<G>(
|
||||
&mut self,
|
||||
session: &xr::Session<G>,
|
||||
threshold: f32,
|
||||
long_press_duration: f32,
|
||||
) -> anyhow::Result<bool> {
|
||||
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<const COUNT: usize> MultiClickHandler<COUNT> {
|
||||
fn new(action_set: &xr::ActionSet, action_name: &str, side: &str) -> anyhow::Result<Self> {
|
||||
let name = format!("{side}_{COUNT}-{action_name}");
|
||||
@@ -112,19 +182,19 @@ impl<const COUNT: usize> MultiClickHandler<COUNT> {
|
||||
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<Self> {
|
||||
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<bool>,
|
||||
triple_click: Option<bool>,
|
||||
long_press: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user