From dad07989ea99f17776cd308f33d1e0d75b8d5ea5 Mon Sep 17 00:00:00 2001
From: towneh <25694892+towneh@users.noreply.github.com>
Date: Mon, 1 Jun 2026 01:33:26 +0100
Subject: [PATCH] feat: gate hamburger menu behind a 1s hold gesture
VR controllers expose very few buttons, so the same button has to serve
more than one purpose. The hamburger menu was bound to the left-hand
secondary button on release, which made a quick tap unavailable to
anything else and prone to accidental firing.
Introduce a reusable HoldGesture struct and require the secondary button
to be held for 1s to toggle the menu. This protects the menu from
misfire and, more importantly, frees the button's quick tap: the VR
camera fly mode can now claim a tap on the same button without the menu
also reacting. A future quick menu on the other controller can adopt the
same pattern by adding its own ActionId + HoldGesture field and binding
it to that hand.
The enum/method name is kept as ToggleHamburgerOnSecondaryRelease so
existing saved bindings (serialised by action name) keep resolving.
---
.../Devices/Base/BasisActionDriver.cs | 61 ++++++++++++++++++-
1 file changed, 58 insertions(+), 3 deletions(-)
diff --git a/Basis/Packages/com.basis.framework/Device Management/Devices/Base/BasisActionDriver.cs b/Basis/Packages/com.basis.framework/Device Management/Devices/Base/BasisActionDriver.cs
index 0847070709..5565210ab7 100644
--- a/Basis/Packages/com.basis.framework/Device Management/Devices/Base/BasisActionDriver.cs
+++ b/Basis/Packages/com.basis.framework/Device Management/Devices/Base/BasisActionDriver.cs
@@ -394,21 +394,76 @@ public static void TickMovementSpeed(ref BasisInputState current, ref BasisInput
controller.UpdateMovementSpeed(true);
}
+ /// Continuous hold (seconds) on the secondary button required to toggle the hamburger menu.
+ public const float HamburgerHoldSeconds = 1f;
+
+ // VR controllers have very few buttons, so the same button often serves more than one purpose.
+ // A hold gate lets a button's quick tap stay free for another consumer while the hold drives this
+ // action — here the secondary button's tap is left for the VR fly camera launch/recall. To add
+ // another hold-activated action (e.g. a future quick menu on the other hand): add a new ActionId,
+ // give it its OWN HoldGesture field (the action delegates are static, so a shared field would race
+ // across hands), then Bind it to the desired role.
+ private static HoldGesture s_hamburgerHold;
+
///
- /// Toggles the hamburger menu on secondary button release.
+ /// Toggles the hamburger menu once the secondary button has been held for
+ /// . A quick tap does nothing, which leaves the tap free for other
+ /// consumers of the same button (e.g. the VR fly camera launch/recall).
///
/// Current input snapshot.
/// Previous input snapshot.
+ // Enum/method name kept as "…OnSecondaryRelease" so saved bindings (BasisActionBindingsV1.json,
+ // which serialises actions by name) keep resolving; the trigger is now a hold, not a release.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ToggleHamburgerOnSecondaryRelease(ref BasisInputState current, ref BasisInputState last)
{
- if (current.SecondaryButtonGetState == false && last.SecondaryButtonGetState)
+ if (s_hamburgerHold.Tick(current.SecondaryButtonGetState, HamburgerHoldSeconds))
{
-
Basis.BasisUI.BasisMainMenu.Toggle();
}
}
+ ///
+ /// Tracks a press-and-hold on a single button. returns true on the one frame the
+ /// button has been held continuously for the given duration; releasing early cancels without firing
+ /// and re-arms on the next press. One instance per logical button — do not share across hands.
+ ///
+ public struct HoldGesture
+ {
+ private double pressStartTime;
+ private bool isPressing;
+ private bool fired;
+
+ /// Advances the gesture; returns true once per press when the hold threshold is met.
+ /// Whether the button is held this frame.
+ /// Continuous hold duration required to fire.
+ public bool Tick(bool buttonDown, float holdSeconds)
+ {
+ if (!buttonDown)
+ {
+ isPressing = false;
+ fired = false;
+ return false;
+ }
+
+ if (!isPressing)
+ {
+ isPressing = true;
+ fired = false;
+ pressStartTime = Time.unscaledTimeAsDouble;
+ return false;
+ }
+
+ if (!fired && Time.unscaledTimeAsDouble - pressStartTime >= holdSeconds)
+ {
+ fired = true;
+ return true;
+ }
+
+ return false;
+ }
+ }
+
///
/// Toggles the microphone pause state on primary button release when not hovering UI.
///