From 8d91f640598d475a2ebd66e3b73a9c5d900fa7ad Mon Sep 17 00:00:00 2001 From: Darren Kelly Date: Wed, 10 Jun 2026 15:13:27 +0200 Subject: [PATCH 1/7] Add test and fix for the touchscreen losing focus and dead input issue. --- .../Plugins/InputForUIFocusRegressionTests.cs | 150 ++++++++++++++++++ .../InputForUIFocusRegressionTests.cs.meta | 11 ++ .../Plugins/InputForUI/InputSystemProvider.cs | 15 ++ 3 files changed, 176 insertions(+) create mode 100644 Assets/Tests/InputSystem/Plugins/InputForUIFocusRegressionTests.cs create mode 100644 Assets/Tests/InputSystem/Plugins/InputForUIFocusRegressionTests.cs.meta diff --git a/Assets/Tests/InputSystem/Plugins/InputForUIFocusRegressionTests.cs b/Assets/Tests/InputSystem/Plugins/InputForUIFocusRegressionTests.cs new file mode 100644 index 0000000000..73a30d94a2 --- /dev/null +++ b/Assets/Tests/InputSystem/Plugins/InputForUIFocusRegressionTests.cs @@ -0,0 +1,150 @@ +#if UNITY_2023_2_OR_NEWER // UnityEngine.InputForUI Module unavailable in earlier releases +using System.Collections.Generic; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.InputForUI; +using UnityEngine.InputSystem; +using UnityEngine.InputSystem.LowLevel; +using UnityEngine.InputSystem.Plugins.InputForUI; +using Event = UnityEngine.InputForUI.Event; +using EventProvider = UnityEngine.InputForUI.EventProvider; + +// Regression tests for UUM-139662: +// "UI Elements cannot be interacted with in a build when using a touchscreen monitor after using Alt+Tab" +// +// Root cause: InputSystemProvider.OnFocusChanged() does not reset pointer state (m_TouchState, m_MouseState, +// m_PenState, m_SeenTouchEvents, m_SeenPenEvents, m_ResetSeenEventsOnUpdate, m_Events) on focus loss. +// After an Alt+Tab cycle, stale values corrupt event processing for subsequent touch and mouse input. +// +// Fix: Reset all pointer state in OnFocusChanged(false), matching how InputManagerProvider behaves +// (it polls fresh state every frame so stale state is never possible there). +[Category("InputForUI")] +public class InputForUIFocusRegressionTests : InputTestFixture +{ + readonly List m_RecordedEvents = new List(); + InputSystemProvider m_Provider; + + [SetUp] + public override void Setup() + { + base.Setup(); + m_Provider = new InputSystemProvider(); + EventProvider.SetMockProvider(m_Provider); + EventProvider.Subscribe(OnEvent); + } + + [TearDown] + public override void TearDown() + { + EventProvider.Unsubscribe(OnEvent); + EventProvider.ClearMockProvider(); + m_RecordedEvents.Clear(); + base.TearDown(); + } + + bool OnEvent(in Event ev) + { + m_RecordedEvents.Add(ev); + return true; + } + + static void FlushEvents() + { + // Process pending input events first, then let the provider dispatch them. + // Mirrors the Update() helper in InputForUITests. + EventProvider.NotifyUpdate(); + InputSystem.Update(); + } + + // ─── Stale ClickCount ───────────────────────────────────────────────────── + // + // Scenario: user taps the touchscreen, Alt+Tabs away and back, then taps again. + // BackgroundBehavior resets the device on focus loss, but the InputSystemProvider's + // internal m_TouchState is never cleared — OnFocusChanged(false) only delegates to + // m_InputEventPartialProvider and touches nothing else. + // + // PointerState.Reset() clears ButtonsState and LastPosition but intentionally + // preserves ClickCount for within-session double-tap detection. After an Alt+Tab + // that ClickCount is stale: the tap before focus loss left it at 1, so the first + // tap after focus regain calls OnButtonChange(false→true) starting from 1, producing + // clickCount=2 in the ButtonPressed event. UIToolkit uses clickCount to distinguish + // single/double/triple taps — a wrong value causes the element to receive the wrong + // interaction type and ignore the input. + // + // Expected (after fix): OnFocusChanged(false) assigns m_TouchState = default, + // zeroing every field including ClickCount. The first post-regain tap starts from 0 + // and correctly produces clickCount == 1. + [Test] + [Description("Verifies that m_TouchState is reset on focus loss so that the first " + + "touch press after an Alt+Tab cycle generates a clean ButtonPressed event " + + "rather than one with stale (stuck-pressed) button state.")] + public void AfterAltTab_FirstTouchPress_GeneratesCleanButtonPressedEvent() + { + InputSystem.settings.backgroundBehavior = + InputSettings.BackgroundBehavior.ResetAndDisableNonBackgroundDevices; + + InputSystem.AddDevice(); + + // ── Phase 1: complete touch tap (press + release) ──────────────────────── + // Use a full tap so the device is in a clean state (no active touch) before the + // focus cycle. The only accumulated state we care about is ClickCount, which + // PointerState.Reset() does NOT clear — that is the stale value the fix must address. + BeginTouch(1, new Vector2(100f, 100f)); + EndTouch(1, new Vector2(100f, 100f)); + FlushEvents(); + + // Confirm the tap was recorded so we know the provider is wired up. + Assert.That(m_RecordedEvents.Count >= 1 && + m_RecordedEvents[0] is + { + type: Event.Type.PointerEvent, + asPointerEvent: { type: PointerEvent.Type.ButtonPressed, eventSource: EventSource.Touch } + }, "Pre-condition failed: expected initial ButtonPressed from Touch"); + + m_RecordedEvents.Clear(); + + // ── Phase 2: Alt+Tab cycle ──────────────────────────────────────────────── + // ScheduleFocusChangedEvent triggers the InputManager's BackgroundBehavior + // (device disable/reset/re-enable) but does NOT call IEventProviderImpl.OnFocusChanged — + // that is called by the UnityEngine.InputForUI EventProvider module in response to + // Application.focusChanged, which cannot be triggered from a unit test. We call it + // directly on the provider here to reflect what actually happens at runtime. + m_Provider.OnFocusChanged(false); + + ScheduleFocusChangedEvent(applicationHasFocus: false); + currentTime += 0.5f; + ScheduleFocusChangedEvent(applicationHasFocus: true); + currentTime += 0.5f; + + InputSystem.Update(InputUpdateType.Editor); + InputSystem.Update(InputUpdateType.Dynamic); + EventProvider.NotifyUpdate(); + + m_Provider.OnFocusChanged(true); + + m_RecordedEvents.Clear(); + + // ── Phase 3: new touch at a different position after focus regain ───────── + var freshTouchPosition = new Vector2(200f, 200f); + BeginTouch(1, freshTouchPosition); + EndTouch(1, freshTouchPosition); + FlushEvents(); + + // Expect exactly one ButtonPressed event followed by one ButtonReleased event, + // both with EventSource.Touch and clickCount == 1 (a fresh single tap). + var pressEvents = m_RecordedEvents.FindAll(e => + e.type == Event.Type.PointerEvent && + e.asPointerEvent.type == PointerEvent.Type.ButtonPressed && + e.asPointerEvent.eventSource == EventSource.Touch); + + Assert.AreEqual(1, pressEvents.Count, + "Expected exactly one Touch ButtonPressed event after focus regain. " + + "If stale ButtonsState shows the button as already pressed, OnButtonChange(true,true) " + + "produces no transition and UIToolkit will not recognise this as a new press."); + + Assert.AreEqual(1, pressEvents[0].asPointerEvent.clickCount, + "clickCount should be 1 for a fresh single tap after focus regain. " + + "A stale clickCount indicates m_TouchState was not reset on focus loss."); + } +} +#endif diff --git a/Assets/Tests/InputSystem/Plugins/InputForUIFocusRegressionTests.cs.meta b/Assets/Tests/InputSystem/Plugins/InputForUIFocusRegressionTests.cs.meta new file mode 100644 index 0000000000..2abb601fa0 --- /dev/null +++ b/Assets/Tests/InputSystem/Plugins/InputForUIFocusRegressionTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2446c5ae5b9d47fa99e3282c3234c49e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/InputForUI/InputSystemProvider.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/InputForUI/InputSystemProvider.cs index cbc8f53901..13847bb862 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/InputForUI/InputSystemProvider.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/InputForUI/InputSystemProvider.cs @@ -281,6 +281,21 @@ static int SortEvents(Event a, Event b) public void OnFocusChanged(bool focus) { m_InputEventPartialProvider.OnFocusChanged(focus); + + if (!focus) + { + // Replace state structs with default instances rather than calling Reset(), + // because Reset() preserves ClickCount (used for double-tap tracking within a + // session) but on focus loss we want a completely clean slate — stale ClickCount + // causes the first tap after Alt+Tab to be misidentified as a double/triple tap. + m_MouseState = default; + m_TouchState = default; + m_PenState = default; + m_SeenTouchEvents = false; + m_SeenPenEvents = false; + m_ResetSeenEventsOnUpdate = false; + m_Events.Clear(); + } } public bool RequestCurrentState(Event.Type type) From 645f4aee266c1ff198442934d0aae64a64421854 Mon Sep 17 00:00:00 2001 From: Darren Kelly Date: Wed, 10 Jun 2026 15:39:44 +0200 Subject: [PATCH 2/7] Fix suggested issue from from U-pr. --- .../Plugins/InputForUI/InputSystemProvider.cs | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/InputForUI/InputSystemProvider.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/InputForUI/InputSystemProvider.cs index 13847bb862..d91ca1cb0a 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/InputForUI/InputSystemProvider.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/InputForUI/InputSystemProvider.cs @@ -284,13 +284,32 @@ public void OnFocusChanged(bool focus) if (!focus) { - // Replace state structs with default instances rather than calling Reset(), - // because Reset() preserves ClickCount (used for double-tap tracking within a - // session) but on focus loss we want a completely clean slate — stale ClickCount - // causes the first tap after Alt+Tab to be misidentified as a double/triple tap. + // Preserve the last known cursor/pen positions before resetting. + // Mouse and pen positions are needed so that a click-to-refocus + // (where the pointer hasn't moved and OnPointerPerformed won't fire) + // still dispatches to the correct screen location via OnClickPerformed. + // Touch positions are NOT preserved — every new touch always arrives + // with a fresh position from OnPointerPerformed. + var mouseLastValid = m_MouseState.LastPositionValid; + var mouseLastPos = m_MouseState.LastPosition; + var mouseLastDisplay = m_MouseState.LastDisplayIndex; + + var penLastValid = m_PenState.LastPositionValid; + var penLastPos = m_PenState.LastPosition; + var penLastDisplay = m_PenState.LastDisplayIndex; + + // Use default assignment rather than Reset() because Reset() intentionally + // preserves ClickCount for within-session double-tap tracking. + // After a focus loss that ClickCount is stale and must be zeroed. m_MouseState = default; m_TouchState = default; m_PenState = default; + + if (mouseLastValid) + m_MouseState.OnMove(m_CurrentTime, mouseLastPos, mouseLastDisplay); + if (penLastValid) + m_PenState.OnMove(m_CurrentTime, penLastPos, penLastDisplay); + m_SeenTouchEvents = false; m_SeenPenEvents = false; m_ResetSeenEventsOnUpdate = false; From 2ed08a53838fc31c676ba051cc9acad0ef48e28a Mon Sep 17 00:00:00 2001 From: Darren Kelly Date: Wed, 10 Jun 2026 15:41:16 +0200 Subject: [PATCH 3/7] Add suggestion from u-pr. --- .../Runtime/Plugins/InputForUI/InputSystemProvider.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/InputForUI/InputSystemProvider.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/InputForUI/InputSystemProvider.cs index d91ca1cb0a..7ffc9fabf5 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/InputForUI/InputSystemProvider.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/InputForUI/InputSystemProvider.cs @@ -313,6 +313,7 @@ public void OnFocusChanged(bool focus) m_SeenTouchEvents = false; m_SeenPenEvents = false; m_ResetSeenEventsOnUpdate = false; + m_RepeatHelper.Reset(); m_Events.Clear(); } } From 52b1216d281681abf4bf62af2c9da0c230e70e78 Mon Sep 17 00:00:00 2001 From: Darren Kelly Date: Thu, 11 Jun 2026 14:40:39 +0200 Subject: [PATCH 4/7] Fix formatting. --- .../Plugins/InputForUIFocusRegressionTests.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Assets/Tests/InputSystem/Plugins/InputForUIFocusRegressionTests.cs b/Assets/Tests/InputSystem/Plugins/InputForUIFocusRegressionTests.cs index 73a30d94a2..94018e2f98 100644 --- a/Assets/Tests/InputSystem/Plugins/InputForUIFocusRegressionTests.cs +++ b/Assets/Tests/InputSystem/Plugins/InputForUIFocusRegressionTests.cs @@ -76,8 +76,8 @@ static void FlushEvents() // and correctly produces clickCount == 1. [Test] [Description("Verifies that m_TouchState is reset on focus loss so that the first " + - "touch press after an Alt+Tab cycle generates a clean ButtonPressed event " + - "rather than one with stale (stuck-pressed) button state.")] + "touch press after an Alt+Tab cycle generates a clean ButtonPressed event " + + "rather than one with stale (stuck-pressed) button state.")] public void AfterAltTab_FirstTouchPress_GeneratesCleanButtonPressedEvent() { InputSystem.settings.backgroundBehavior = @@ -95,11 +95,11 @@ public void AfterAltTab_FirstTouchPress_GeneratesCleanButtonPressedEvent() // Confirm the tap was recorded so we know the provider is wired up. Assert.That(m_RecordedEvents.Count >= 1 && - m_RecordedEvents[0] is - { - type: Event.Type.PointerEvent, - asPointerEvent: { type: PointerEvent.Type.ButtonPressed, eventSource: EventSource.Touch } - }, "Pre-condition failed: expected initial ButtonPressed from Touch"); + m_RecordedEvents[0] is + { + type: Event.Type.PointerEvent, + asPointerEvent: { type: PointerEvent.Type.ButtonPressed, eventSource: EventSource.Touch } + }, "Pre-condition failed: expected initial ButtonPressed from Touch"); m_RecordedEvents.Clear(); From 431fe27e4b07bb9a33d6306c8348280a43932032 Mon Sep 17 00:00:00 2001 From: Darren Kelly Date: Thu, 11 Jun 2026 16:24:15 +0200 Subject: [PATCH 5/7] Fix failing CI. --- .../InputSystem/Plugins/InputForUIFocusRegressionTests.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Assets/Tests/InputSystem/Plugins/InputForUIFocusRegressionTests.cs b/Assets/Tests/InputSystem/Plugins/InputForUIFocusRegressionTests.cs index 94018e2f98..eb9ae6992a 100644 --- a/Assets/Tests/InputSystem/Plugins/InputForUIFocusRegressionTests.cs +++ b/Assets/Tests/InputSystem/Plugins/InputForUIFocusRegressionTests.cs @@ -116,7 +116,11 @@ m_RecordedEvents[0] is ScheduleFocusChangedEvent(applicationHasFocus: true); currentTime += 0.5f; +#if UNITY_EDITOR + // InputUpdateType.Editor is only valid inside the editor; in a player build + // the focus events are processed through the Dynamic update path. InputSystem.Update(InputUpdateType.Editor); +#endif InputSystem.Update(InputUpdateType.Dynamic); EventProvider.NotifyUpdate(); From bb3a9afb675c6297209b140c8cabbd78521766e6 Mon Sep 17 00:00:00 2001 From: Darren Kelly Date: Fri, 12 Jun 2026 13:18:32 +0200 Subject: [PATCH 6/7] Add code review suggestion from Morgan. --- .../Runtime/Plugins/InputForUI/InputSystemProvider.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/InputForUI/InputSystemProvider.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/InputForUI/InputSystemProvider.cs index 7ffc9fabf5..d976dd5049 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/InputForUI/InputSystemProvider.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/InputForUI/InputSystemProvider.cs @@ -310,8 +310,7 @@ public void OnFocusChanged(bool focus) if (penLastValid) m_PenState.OnMove(m_CurrentTime, penLastPos, penLastDisplay); - m_SeenTouchEvents = false; - m_SeenPenEvents = false; + ResetSeenEvents(); m_ResetSeenEventsOnUpdate = false; m_RepeatHelper.Reset(); m_Events.Clear(); From 212060eb32cf57126abf833d8833fea0f07bfeee Mon Sep 17 00:00:00 2001 From: Darren Kelly Date: Fri, 12 Jun 2026 13:42:08 +0200 Subject: [PATCH 7/7] remove u-pr suggestion --- .../Runtime/Plugins/InputForUI/InputSystemProvider.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/InputForUI/InputSystemProvider.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/InputForUI/InputSystemProvider.cs index d976dd5049..2f26e07af2 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/InputForUI/InputSystemProvider.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/InputForUI/InputSystemProvider.cs @@ -312,7 +312,6 @@ public void OnFocusChanged(bool focus) ResetSeenEvents(); m_ResetSeenEventsOnUpdate = false; - m_RepeatHelper.Reset(); m_Events.Clear(); } }