Skip to content

Commit 099c2af

Browse files
committed
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.
1 parent 740c643 commit 099c2af

1 file changed

Lines changed: 58 additions & 3 deletions

File tree

Basis/Packages/com.basis.framework/Device Management/Devices/Base/BasisActionDriver.cs

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -394,21 +394,76 @@ public static void TickMovementSpeed(ref BasisInputState current, ref BasisInput
394394
controller.UpdateMovementSpeed(true);
395395
}
396396

397+
/// <summary>Continuous hold (seconds) on the secondary button required to toggle the hamburger menu.</summary>
398+
public const float HamburgerHoldSeconds = 1f;
399+
400+
// VR controllers have very few buttons, so the same button often serves more than one purpose.
401+
// A hold gate lets a button's quick tap stay free for another consumer while the hold drives this
402+
// action — here the secondary button's tap is left for the VR fly camera launch/recall. To add
403+
// another hold-activated action (e.g. a future quick menu on the other hand): add a new ActionId,
404+
// give it its OWN HoldGesture field (the action delegates are static, so a shared field would race
405+
// across hands), then Bind it to the desired role.
406+
private static HoldGesture s_hamburgerHold;
407+
397408
/// <summary>
398-
/// Toggles the hamburger menu on secondary button release.
409+
/// Toggles the hamburger menu once the secondary button has been held for
410+
/// <see cref="HamburgerHoldSeconds"/>. A quick tap does nothing, which leaves the tap free for other
411+
/// consumers of the same button (e.g. the VR fly camera launch/recall).
399412
/// </summary>
400413
/// <param name="current">Current input snapshot.</param>
401414
/// <param name="last">Previous input snapshot.</param>
415+
// Enum/method name kept as "…OnSecondaryRelease" so saved bindings (BasisActionBindingsV1.json,
416+
// which serialises actions by name) keep resolving; the trigger is now a hold, not a release.
402417
[MethodImpl(MethodImplOptions.AggressiveInlining)]
403418
public static void ToggleHamburgerOnSecondaryRelease(ref BasisInputState current, ref BasisInputState last)
404419
{
405-
if (current.SecondaryButtonGetState == false && last.SecondaryButtonGetState)
420+
if (s_hamburgerHold.Tick(current.SecondaryButtonGetState, HamburgerHoldSeconds))
406421
{
407-
408422
Basis.BasisUI.BasisMainMenu.Toggle();
409423
}
410424
}
411425

426+
/// <summary>
427+
/// Tracks a press-and-hold on a single button. <see cref="Tick"/> returns true on the one frame the
428+
/// button has been held continuously for the given duration; releasing early cancels without firing
429+
/// and re-arms on the next press. One instance per logical button — do not share across hands.
430+
/// </summary>
431+
public struct HoldGesture
432+
{
433+
private double pressStartTime;
434+
private bool isPressing;
435+
private bool fired;
436+
437+
/// <summary>Advances the gesture; returns true once per press when the hold threshold is met.</summary>
438+
/// <param name="buttonDown">Whether the button is held this frame.</param>
439+
/// <param name="holdSeconds">Continuous hold duration required to fire.</param>
440+
public bool Tick(bool buttonDown, float holdSeconds)
441+
{
442+
if (!buttonDown)
443+
{
444+
isPressing = false;
445+
fired = false;
446+
return false;
447+
}
448+
449+
if (!isPressing)
450+
{
451+
isPressing = true;
452+
fired = false;
453+
pressStartTime = Time.unscaledTimeAsDouble;
454+
return false;
455+
}
456+
457+
if (!fired && Time.unscaledTimeAsDouble - pressStartTime >= holdSeconds)
458+
{
459+
fired = true;
460+
return true;
461+
}
462+
463+
return false;
464+
}
465+
}
466+
412467
/// <summary>
413468
/// Toggles the microphone pause state on primary button release when not hovering UI.
414469
/// </summary>

0 commit comments

Comments
 (0)