Compare commits
1 Commits
main
...
long_press
| Author | SHA1 | Date | |
|---|---|---|---|
| 27dcc7ded2 |
@@ -91,29 +91,36 @@ impl InputState {
|
|||||||
|
|
||||||
if hand.now.click {
|
if hand.now.click {
|
||||||
hand.last_click = Instant::now();
|
hand.last_click = Instant::now();
|
||||||
|
|
||||||
if !hand.before.click {
|
if !hand.before.click {
|
||||||
hand.click_press_start = Some(Instant::now());
|
hand.click_press_start = Some(Instant::now());
|
||||||
hand.interaction.long_press_click_sent = false;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
hand.click_press_start = None;
|
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 long_press_threshold = Duration::from_secs_f32(session.config.long_press_duration);
|
||||||
let is_long_press = hand
|
let is_long_press = hand
|
||||||
.click_press_start
|
.click_press_start
|
||||||
.is_some_and(|start| start.elapsed() >= long_press_threshold);
|
.is_some_and(|start| start.elapsed() >= long_press_threshold);
|
||||||
|
|
||||||
// Prevent the mode from changing during a click (except for long press)
|
// Allow mode changes when:
|
||||||
if !hand.before.click || is_long_press {
|
// 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 {
|
if hand.now.click_modifier_right {
|
||||||
hand.interaction.mode = PointerMode::Right;
|
hand.interaction.mode = PointerMode::Right;
|
||||||
} else if is_long_press {
|
|
||||||
hand.interaction.mode = PointerMode::Right;
|
|
||||||
} else if hand.now.click_modifier_middle {
|
} else if hand.now.click_modifier_middle {
|
||||||
hand.interaction.mode = PointerMode::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 {
|
} else if !hand.before.click {
|
||||||
let hmd_up = self.hmd.transform_vector3a(Vec3A::Y);
|
let hmd_up = self.hmd.transform_vector3a(Vec3A::Y);
|
||||||
let dot = hmd_up.dot(hand.pose.transform_vector3a(Vec3A::X))
|
let dot = hmd_up.dot(hand.pose.transform_vector3a(Vec3A::X))
|
||||||
@@ -541,40 +548,57 @@ where
|
|||||||
|
|
||||||
handle_scroll(&hit, hovered, app);
|
handle_scroll(&hit, hovered, app);
|
||||||
|
|
||||||
// click / release - normal behavior
|
// click / release
|
||||||
let pointer = &mut app.input_state.pointers[hit.pointer];
|
let pointer = &mut app.input_state.pointers[hit.pointer];
|
||||||
|
|
||||||
// Check if we should send a long-press click immediately
|
// Check if mode changed during an active click
|
||||||
let long_press_threshold = Duration::from_secs_f32(app.session.config.long_press_duration);
|
let mode_changed = pointer.now.click && pointer.before.click
|
||||||
let is_long_press = pointer
|
&& pointer.interaction.click_mode != pointer.interaction.mode;
|
||||||
.click_press_start
|
|
||||||
.is_some_and(|start| start.elapsed() >= long_press_threshold);
|
|
||||||
|
|
||||||
if is_long_press && pointer.now.click && !pointer.interaction.long_press_click_sent {
|
if pointer.now.click && !pointer.before.click {
|
||||||
// Immediately send right-click (press + release)
|
// Click just started
|
||||||
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 {
|
|
||||||
pointer.interaction.clicked_id = Some(hit.overlay);
|
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(
|
update_focus(
|
||||||
&mut app.hid_provider.keyboard_focus,
|
&mut app.hid_provider.keyboard_focus,
|
||||||
hovered.config.keyboard_focus,
|
hovered.config.keyboard_focus,
|
||||||
);
|
);
|
||||||
hovered.config.backend.on_pointer(app, &hit, true);
|
hovered.config.backend.on_pointer(app, &hit, true);
|
||||||
} else if !pointer.now.click && pointer.before.click {
|
} else if mode_changed {
|
||||||
// Only send release if we haven't already sent a long press click
|
// Mode changed during click - send release with old mode, then press with new mode
|
||||||
if !pointer.interaction.long_press_click_sent {
|
let mut old_hit = hit;
|
||||||
if let Some(clicked_id) = pointer.interaction.clicked_id.take() {
|
old_hit.mode = pointer.interaction.click_mode;
|
||||||
if let Some(clicked) = overlays.mut_by_id(clicked_id) {
|
|
||||||
clicked.config.backend.on_pointer(app, &hit, false);
|
// Send release with old mode
|
||||||
}
|
if let Some(clicked_id) = pointer.interaction.clicked_id {
|
||||||
} else {
|
if let Some(clicked) = overlays.mut_by_id(clicked_id) {
|
||||||
hovered.config.backend.on_pointer(app, &hit, false);
|
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))
|
(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
|
// send release event to overlay that was originally clicked
|
||||||
if !pointer.now.click
|
if !pointer.now.click
|
||||||
&& pointer.before.click
|
&& pointer.before.click
|
||||||
&& !pointer.interaction.long_press_click_sent
|
|
||||||
&& let Some(clicked_id) = pointer.interaction.clicked_id.take()
|
&& let Some(clicked_id) = pointer.interaction.clicked_id.take()
|
||||||
{
|
{
|
||||||
let hit = PointerHit {
|
let hit = PointerHit {
|
||||||
|
|||||||
@@ -43,6 +43,76 @@ pub struct MultiClickHandler<const COUNT: usize> {
|
|||||||
held_inactive: bool,
|
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> {
|
impl<const COUNT: usize> MultiClickHandler<COUNT> {
|
||||||
fn new(action_set: &xr::ActionSet, action_name: &str, side: &str) -> anyhow::Result<Self> {
|
fn new(action_set: &xr::ActionSet, action_name: &str, side: &str) -> anyhow::Result<Self> {
|
||||||
let name = format!("{side}_{COUNT}-{action_name}");
|
let name = format!("{side}_{COUNT}-{action_name}");
|
||||||
@@ -112,19 +182,19 @@ impl<const COUNT: usize> MultiClickHandler<COUNT> {
|
|||||||
pub struct CustomClickAction {
|
pub struct CustomClickAction {
|
||||||
single: MultiClickHandler<0>,
|
single: MultiClickHandler<0>,
|
||||||
double: MultiClickHandler<1>,
|
double: MultiClickHandler<1>,
|
||||||
triple: MultiClickHandler<2>,
|
long_press: LongPressHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CustomClickAction {
|
impl CustomClickAction {
|
||||||
pub fn new(action_set: &xr::ActionSet, name: &str, side: &str) -> anyhow::Result<Self> {
|
pub fn new(action_set: &xr::ActionSet, name: &str, side: &str) -> anyhow::Result<Self> {
|
||||||
let single = MultiClickHandler::new(action_set, name, side)?;
|
let single = MultiClickHandler::new(action_set, name, side)?;
|
||||||
let double = 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 {
|
Ok(Self {
|
||||||
single,
|
single,
|
||||||
double,
|
double,
|
||||||
triple,
|
long_press,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
pub fn state(
|
pub fn state(
|
||||||
@@ -141,7 +211,7 @@ impl CustomClickAction {
|
|||||||
|
|
||||||
Ok(self.single.check(&state.session, threshold)?
|
Ok(self.single.check(&state.session, threshold)?
|
||||||
|| self.double.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,
|
xr: &XrState,
|
||||||
session: &AppSession,
|
session: &AppSession,
|
||||||
) -> anyhow::Result<()> {
|
) -> 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)?;
|
pointer.now.grab = self.source.grab.state(pointer.before.grab, xr, session)?;
|
||||||
|
|
||||||
@@ -574,9 +651,9 @@ macro_rules! add_custom {
|
|||||||
for s in iter {
|
for s in iter {
|
||||||
if let Some(p) = to_paths(Some(s.as_str()), $instance) {
|
if let Some(p) = to_paths(Some(s.as_str()), $instance) {
|
||||||
if is_bool(s) {
|
if is_bool(s) {
|
||||||
if action.triple_click.unwrap_or(false) {
|
if action.long_press.unwrap_or(false) {
|
||||||
$bindings.push(xr::Binding::new(
|
$bindings.push(xr::Binding::new(
|
||||||
&$hands[i].$field.triple.action_bool,
|
&$hands[i].$field.long_press.action_bool,
|
||||||
p,
|
p,
|
||||||
));
|
));
|
||||||
} else if action.double_click.unwrap_or(false) {
|
} else if action.double_click.unwrap_or(false) {
|
||||||
@@ -591,9 +668,9 @@ macro_rules! add_custom {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if action.triple_click.unwrap_or(false) {
|
if action.long_press.unwrap_or(false) {
|
||||||
$bindings.push(xr::Binding::new(
|
$bindings.push(xr::Binding::new(
|
||||||
&$hands[i].$field.triple.action_f32,
|
&$hands[i].$field.long_press.action_f32,
|
||||||
p,
|
p,
|
||||||
));
|
));
|
||||||
} else if action.double_click.unwrap_or(false) {
|
} else if action.double_click.unwrap_or(false) {
|
||||||
@@ -739,6 +816,7 @@ struct OpenXrActionConfAction {
|
|||||||
threshold: Option<[f32; 2]>,
|
threshold: Option<[f32; 2]>,
|
||||||
double_click: Option<bool>,
|
double_click: Option<bool>,
|
||||||
triple_click: Option<bool>,
|
triple_click: Option<bool>,
|
||||||
|
long_press: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -32,6 +32,16 @@
|
|||||||
//
|
//
|
||||||
// -- pose, haptic --
|
// -- pose, haptic --
|
||||||
// do not mess with these, unless you know what you're doing
|
// 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
|
// Eye+hand interaction
|
||||||
|
|||||||
Reference in New Issue
Block a user