added long_press as a option in binding system
Some checks failed
Build AppImage / build_appimage (push) Failing after 2m2s

This commit is contained in:
2026-02-15 18:15:54 +08:00
parent 182206afbe
commit 27dcc7ded2
3 changed files with 151 additions and 40 deletions

View File

@@ -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 {

View File

@@ -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)]

View File

@@ -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