Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [4.1.1] - 2026-07-03

### Added

- **`InputFreshPressGate`** — shared helper for ignoring in-progress presses when input handlers arm (on subscribe or view switch).
- **`ViewDismissalInputHandler.requireFreshPress`** — (default: `true`) requires Cancel to be released after the current view changes before dismissing, preventing the same press from closing a newly focused view (e.g. overlay → parent screen).

### Changed

- **`InputActionButtonBase`** — fresh-press filtering now delegates to `InputFreshPressGate` instead of inline state.

## [4.1.0] - 2026-07-02

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ _The Sentinal Debug Window (**Window > Sentinal > Debug**) displaying active vie
- **Controller-first focus**: Automatically selects the right button when views open, remembers the last selected control, and resolves overlapping views by priority and recency.
- **Stack-based back behavior**: `ViewDismissalInputHandler` wires cancel/back input to the top non-root view, so `Esc`, `B`, or `Circle` behaves consistently across screens.
- **Layer-safe menus**: `ViewGroupMask` or Root views keeps gameplay HUDs, popups, pause menus, overlays, and tab panels from closing or hiding each other accidentally.
- **Input System integration**: Gate action maps per focused view, target primary/all/specific players, and bind input actions directly to buttons and tabs.
- **Input System integration**: Gate action maps per focused view, target primary/all/specific players, bind actions to buttons and tabs, with fresh-press gating to prevent input bleeding on cancel and input buttons (on by default).
- **No manager prefab required**: Views self-register with the static router, and Sentinal resets its static state for Fast Enter Play Mode.

## Quick Start
Expand Down
21 changes: 5 additions & 16 deletions Runtime/Input/InputSystem/Components/Base/InputActionButtonBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public abstract class InputActionButtonBase : ViewInputActionHandler
protected Button button;
protected InputAction inputAction;
protected EventSystem eventSystem;
protected bool wasPressedOnSubscribe;
protected InputFreshPressGate freshPressGate;

protected override void Awake()
{
Expand Down Expand Up @@ -75,7 +75,7 @@ protected override void Subscribe()
return;
}

wasPressedOnSubscribe = requireFreshPress && inputAction.IsPressed();
freshPressGate.Arm(inputAction, requireFreshPress);

SubscribeToInputAction();
isSubscribed = true;
Expand All @@ -89,27 +89,16 @@ protected override void Unsubscribe()
inputAction = null;
}

wasPressedOnSubscribe = false;
freshPressGate.Disarm();
isSubscribed = false;
}

/// <summary>
/// Validates whether the current input action event is valid based on fresh press rules.
/// Returns true if the event should be processed, or false if it should be ignored.
/// </summary>
protected bool ValidateFreshPress(bool isCancelEvent = false)
{
if (!requireFreshPress || !wasPressedOnSubscribe)
return true;

if (inputAction == null)
return false;

if (!inputAction.IsPressed() || isCancelEvent)
wasPressedOnSubscribe = false;

return false;
}
protected bool ValidateFreshPress(bool isCancelEvent = false) =>
freshPressGate.ShouldProcess(inputAction, isCancelEvent, requireFreshPress);

/// <summary>
/// Called when subscribing to the input action. Override to subscribe to specific action events.
Expand Down
39 changes: 39 additions & 0 deletions Runtime/Input/InputSystem/InputFreshPressGate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem;

namespace Sentinal.InputSystem
{
/// <summary>
/// Ignores input events that are part of a press already in progress when armed
/// (e.g. when a view opens or when subscribe runs).
/// </summary>
public struct InputFreshPressGate
{
private bool wasPressedOnArm;

public void Arm(InputAction action, bool requireFreshPress)
{
wasPressedOnArm = requireFreshPress && action != null && action.IsPressed();
}

public void Disarm() => wasPressedOnArm = false;

/// <summary>
/// Returns true when the event should be processed.
/// </summary>
public bool ShouldProcess(InputAction action, bool isCancelEvent, bool requireFreshPress)
{
if (!requireFreshPress || !wasPressedOnArm)
return true;

if (action == null)
return false;

if (!action.IsPressed() || isCancelEvent)
wasPressedOnArm = false;

return false;
}
}
}
#endif
2 changes: 2 additions & 0 deletions Runtime/Input/InputSystem/InputFreshPressGate.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 37 additions & 2 deletions Runtime/Input/InputSystem/ViewDismissalInputHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ public class ViewDismissalInputHandler : MonoBehaviour, IPlayerInputHandler
[Tooltip("Fire on action release (canceled) instead of action press (performed).")]
private bool cancelActionOnRelease;

[SerializeField]
[Tooltip(
"Requires Cancel to be released before dismissing after the current view changes, preventing the same press from closing a newly focused view."
)]
private bool requireFreshPress = true;

[Header("Grouping")]
[SerializeField]
[Tooltip("Optional group mask to filter which views can be dismissed by this handler. Defaults to Everything.")]
Expand All @@ -55,6 +61,7 @@ public ViewGroupMask GroupMask
private bool isSubscribed;
private bool closeRequestedThisFrame;
private ViewSelector pendingCloseView;
private InputFreshPressGate cancelFreshPressGate;

private void Awake()
{
Expand All @@ -69,6 +76,8 @@ private void OnEnable()
if (inputSource == PlayerInputSource.SentinalPlayerRole)
SentinalPlayer.OnPlayerChanged += OnPlayerRoleChanged;

SentinalViewRouter.OnSwitch += OnViewSwitch;

if (playerInput != null && !isSubscribed)
SubscribeToInputActions();
}
Expand All @@ -77,9 +86,19 @@ private void OnDisable()
{
if (inputSource == PlayerInputSource.SentinalPlayerRole)
SentinalPlayer.OnPlayerChanged -= OnPlayerRoleChanged;

SentinalViewRouter.OnSwitch -= OnViewSwitch;
UnsubscribeFromInputActions();
}

private void OnViewSwitch(ViewSelector previousView, ViewSelector nextView)
{
if (!requireFreshPress || cancelInputAction == null)
return;

cancelFreshPressGate.Arm(cancelInputAction, requireFreshPress);
}

private void OnPlayerRoleChanged(int key, PlayerInput newPlayer)
{
if (key != playerKey)
Expand Down Expand Up @@ -144,6 +163,8 @@ private void SubscribeToInputActions()
cancelInputAction = cancelAction.FindAction(playerInput);
if (cancelInputAction != null)
{
cancelFreshPressGate.Arm(cancelInputAction, requireFreshPress);

if (cancelActionOnRelease)
cancelInputAction.canceled += OnCancelCanceled;
else
Expand Down Expand Up @@ -176,6 +197,8 @@ private void UnsubscribeFromInputActions()
cancelInputAction = null;
}

cancelFreshPressGate.Disarm();

if (focusInputAction != null)
{
focusInputAction.performed -= OnFocusPerformed;
Expand All @@ -185,9 +208,21 @@ private void UnsubscribeFromInputActions()
isSubscribed = false;
}

private void OnCancelPerformed(InputAction.CallbackContext context) => RequestCloseCurrentView();
private void OnCancelPerformed(InputAction.CallbackContext context)
{
if (!cancelFreshPressGate.ShouldProcess(cancelInputAction, isCancelEvent: false, requireFreshPress))
return;

RequestCloseCurrentView();
}

private void OnCancelCanceled(InputAction.CallbackContext context) => RequestCloseCurrentView();
private void OnCancelCanceled(InputAction.CallbackContext context)
{
if (!cancelFreshPressGate.ShouldProcess(cancelInputAction, isCancelEvent: true, requireFreshPress))
return;

RequestCloseCurrentView();
}

private void RequestCloseCurrentView()
{
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "com.tirt.sentinal",
"version": "4.1.0",
"version": "4.1.1",
"displayName": "Sentinal",
"description": "A package that provides a set of tools to manage menu/view navigation and auto-selection in Unity.",
"unity": "2021.3",
Expand Down