From ff8ee4350484771cf28f3e27c4344e362d19a00c Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 9 Jul 2026 05:35:53 +0900 Subject: [PATCH 1/3] Characterize mouse UI coordinate conversion Expose the final coordinate conversion seam and pin the top-left Y flip and round-trip behavior against the current Game view height before moving the shared implementation. --- .../Editor/MouseUiCoordinateConverterTests.cs | 45 +++++++++++++++++++ .../MouseUiCoordinateConverterTests.cs.meta | 11 +++++ .../SimulateMouseUi/SimulateMouseUiUseCase.cs | 4 +- 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 Assets/Tests/Editor/MouseUiCoordinateConverterTests.cs create mode 100644 Assets/Tests/Editor/MouseUiCoordinateConverterTests.cs.meta diff --git a/Assets/Tests/Editor/MouseUiCoordinateConverterTests.cs b/Assets/Tests/Editor/MouseUiCoordinateConverterTests.cs new file mode 100644 index 0000000000..ae52a6aa5d --- /dev/null +++ b/Assets/Tests/Editor/MouseUiCoordinateConverterTests.cs @@ -0,0 +1,45 @@ +using NUnit.Framework; +using UnityEditor; +using UnityEngine; + +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor +{ + /// + /// Characterizes coordinate conversion shared by mouse UI action executors. + /// + [TestFixture] + public sealed class MouseUiCoordinateConverterTests + { + /// + /// Verifies top-left input coordinates are flipped against the current Game view height. + /// + [Test] + public void InputToScreen_WithTopLeftCoordinates_FlipsYAgainstCurrentGameViewHeight() + { + Vector2 inputPosition = new(12.5f, 37.25f); + float gameViewHeight = Handles.GetMainGameViewSize().y; + + Vector2 screenPosition = SimulateMouseUiUseCase.InputToScreen(inputPosition); + + Assert.That( + screenPosition, + Is.EqualTo(new Vector2(inputPosition.x, gameViewHeight - inputPosition.y))); + } + + /// + /// Verifies converting input coordinates to screen space and back preserves the position. + /// + [Test] + public void ScreenToInput_AfterInputToScreen_RestoresPosition() + { + Vector2 inputPosition = new(42.5f, 64.25f); + + Vector2 screenPosition = SimulateMouseUiUseCase.InputToScreen(inputPosition); + Vector2 restoredPosition = SimulateMouseUiUseCase.ScreenToInput(screenPosition); + + Assert.That(restoredPosition, Is.EqualTo(inputPosition)); + } + } +} diff --git a/Assets/Tests/Editor/MouseUiCoordinateConverterTests.cs.meta b/Assets/Tests/Editor/MouseUiCoordinateConverterTests.cs.meta new file mode 100644 index 0000000000..13acb98485 --- /dev/null +++ b/Assets/Tests/Editor/MouseUiCoordinateConverterTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 87ef2e81957a4e62a25589f09ed27b6b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs index cc65111861..a74047e767 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs @@ -114,13 +114,13 @@ private async Task ExecuteMouseAction( // Input coordinates use top-left origin; Unity Screen space uses bottom-left origin. // Handles.GetMainGameViewSize() returns the Game view's target resolution (e.g. 1920x1080), // which matches the Canvas layout space — unlike Screen.height which returns the window pixel size. - private static Vector2 InputToScreen(Vector2 inputPos) + internal static Vector2 InputToScreen(Vector2 inputPos) { float targetHeight = Handles.GetMainGameViewSize().y; return new Vector2(inputPos.x, targetHeight - inputPos.y); } - private static Vector2 ScreenToInput(Vector2 screenPos) + internal static Vector2 ScreenToInput(Vector2 screenPos) { float targetHeight = Handles.GetMainGameViewSize().y; return new Vector2(screenPos.x, targetHeight - screenPos.y); From fdce5052465c31d3e4fd2c46ab2e829351cfa2b0 Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 9 Jul 2026 05:37:27 +0900 Subject: [PATCH 2/3] Extract shared mouse UI coordinate conversion Move top-left and Unity screen-space conversion into a focused helper so press and drag executors can share the same Game view coordinate contract without depending on the use case. --- .../Editor/MouseUiCoordinateConverterTests.cs | 6 +-- .../MouseUiCoordinateConverter.cs | 27 ++++++++++++++ .../MouseUiCoordinateConverter.cs.meta | 11 ++++++ .../SimulateMouseUi/SimulateMouseUiUseCase.cs | 37 ++++++------------- 4 files changed, 52 insertions(+), 29 deletions(-) create mode 100644 Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiCoordinateConverter.cs create mode 100644 Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiCoordinateConverter.cs.meta diff --git a/Assets/Tests/Editor/MouseUiCoordinateConverterTests.cs b/Assets/Tests/Editor/MouseUiCoordinateConverterTests.cs index ae52a6aa5d..6969c1abb9 100644 --- a/Assets/Tests/Editor/MouseUiCoordinateConverterTests.cs +++ b/Assets/Tests/Editor/MouseUiCoordinateConverterTests.cs @@ -21,7 +21,7 @@ public void InputToScreen_WithTopLeftCoordinates_FlipsYAgainstCurrentGameViewHei Vector2 inputPosition = new(12.5f, 37.25f); float gameViewHeight = Handles.GetMainGameViewSize().y; - Vector2 screenPosition = SimulateMouseUiUseCase.InputToScreen(inputPosition); + Vector2 screenPosition = MouseUiCoordinateConverter.InputToScreen(inputPosition); Assert.That( screenPosition, @@ -36,8 +36,8 @@ public void ScreenToInput_AfterInputToScreen_RestoresPosition() { Vector2 inputPosition = new(42.5f, 64.25f); - Vector2 screenPosition = SimulateMouseUiUseCase.InputToScreen(inputPosition); - Vector2 restoredPosition = SimulateMouseUiUseCase.ScreenToInput(screenPosition); + Vector2 screenPosition = MouseUiCoordinateConverter.InputToScreen(inputPosition); + Vector2 restoredPosition = MouseUiCoordinateConverter.ScreenToInput(screenPosition); Assert.That(restoredPosition, Is.EqualTo(inputPosition)); } diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiCoordinateConverter.cs b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiCoordinateConverter.cs new file mode 100644 index 0000000000..ea8fe05ebf --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiCoordinateConverter.cs @@ -0,0 +1,27 @@ +#nullable enable +using UnityEditor; +using UnityEngine; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// Converts between top-left mouse input coordinates and Unity screen coordinates. + /// + internal static class MouseUiCoordinateConverter + { + // Input coordinates use top-left origin; Unity Screen space uses bottom-left origin. + // Handles.GetMainGameViewSize() returns the Game view's target resolution (e.g. 1920x1080), + // which matches the Canvas layout space — unlike Screen.height which returns the window pixel size. + internal static Vector2 InputToScreen(Vector2 inputPos) + { + float targetHeight = Handles.GetMainGameViewSize().y; + return new Vector2(inputPos.x, targetHeight - inputPos.y); + } + + internal static Vector2 ScreenToInput(Vector2 screenPos) + { + float targetHeight = Handles.GetMainGameViewSize().y; + return new Vector2(screenPos.x, targetHeight - screenPos.y); + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiCoordinateConverter.cs.meta b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiCoordinateConverter.cs.meta new file mode 100644 index 0000000000..44713341fe --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiCoordinateConverter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: acada63040774a569f5945fc464b30f1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs index a74047e767..21cae78699 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs @@ -111,26 +111,11 @@ private async Task ExecuteMouseAction( } } - // Input coordinates use top-left origin; Unity Screen space uses bottom-left origin. - // Handles.GetMainGameViewSize() returns the Game view's target resolution (e.g. 1920x1080), - // which matches the Canvas layout space — unlike Screen.height which returns the window pixel size. - internal static Vector2 InputToScreen(Vector2 inputPos) - { - float targetHeight = Handles.GetMainGameViewSize().y; - return new Vector2(inputPos.x, targetHeight - inputPos.y); - } - - internal static Vector2 ScreenToInput(Vector2 screenPos) - { - float targetHeight = Handles.GetMainGameViewSize().y; - return new Vector2(screenPos.x, targetHeight - screenPos.y); - } - private async Task ExecuteClick( MouseUiSimulationCommand parameters, EventSystem eventSystem, CancellationToken ct) { Vector2 inputPos = new(parameters.X, parameters.Y); - Vector2 screenPos = InputToScreen(inputPos); + Vector2 screenPos = MouseUiCoordinateConverter.InputToScreen(inputPos); PointerEventData pointerData = MouseUiPointerTargetResolver.CreatePointerPressData(eventSystem, screenPos, parameters.Button); ResolvedPointerTargets resolvedTargets = MouseUiPointerTargetResolver.ResolvePressablePointerTargets(parameters, eventSystem, inputPos, screenPos, pointerData, MouseAction.Click); @@ -193,7 +178,7 @@ private async Task ExecuteLongPress( } Vector2 inputPos = new(parameters.X, parameters.Y); - Vector2 screenPos = InputToScreen(inputPos); + Vector2 screenPos = MouseUiCoordinateConverter.InputToScreen(inputPos); PointerEventData pointerData = MouseUiPointerTargetResolver.CreatePointerPressData(eventSystem, screenPos, parameters.Button); ResolvedPointerTargets resolvedTargets = MouseUiPointerTargetResolver.ResolvePressablePointerTargets(parameters, eventSystem, inputPos, screenPos, pointerData, MouseAction.LongPress); @@ -354,8 +339,8 @@ private async Task ExecuteDragOneShot( { Vector2 inputStart = new(parameters.FromX, parameters.FromY); Vector2 inputEnd = new(parameters.X, parameters.Y); - Vector2 screenStart = InputToScreen(inputStart); - Vector2 screenEnd = InputToScreen(inputEnd); + Vector2 screenStart = MouseUiCoordinateConverter.InputToScreen(inputStart); + Vector2 screenEnd = MouseUiCoordinateConverter.InputToScreen(inputEnd); RaycastResult? hit = parameters.BypassRaycast ? null : UiRaycastHelper.RaycastUI(screenStart, eventSystem); RaycastResult startRaycast = new(); GameObject? rawTarget = null; @@ -547,7 +532,7 @@ private async Task InterpolateDragPosition( pointerData.delta = endPos - previousPosition; ExecuteEvents.Execute(target, pointerData, ExecuteEvents.dragHandler); - SimulateMouseUiOverlayState.UpdatePosition(ScreenToInput(endPos)); + SimulateMouseUiOverlayState.UpdatePosition(MouseUiCoordinateConverter.ScreenToInput(endPos)); return true; } @@ -573,7 +558,7 @@ private async Task InterpolateDragPosition( ExecuteEvents.Execute(target, pointerData, ExecuteEvents.dragHandler); - SimulateMouseUiOverlayState.UpdatePosition(ScreenToInput(currentPosition)); + SimulateMouseUiOverlayState.UpdatePosition(MouseUiCoordinateConverter.ScreenToInput(currentPosition)); } while (t < 1.0f); @@ -596,7 +581,7 @@ private async Task ExecuteDragStart( } Vector2 inputPos = new(parameters.X, parameters.Y); - Vector2 screenPos = InputToScreen(inputPos); + Vector2 screenPos = MouseUiCoordinateConverter.InputToScreen(inputPos); RaycastResult? hit = parameters.BypassRaycast ? null : UiRaycastHelper.RaycastUI(screenPos, eventSystem); RaycastResult startRaycast = new(); GameObject? rawTarget = null; @@ -730,14 +715,14 @@ private async Task ExecuteDragMove( } Vector2 inputEnd = new(parameters.X, parameters.Y); - Vector2 screenEnd = InputToScreen(inputEnd); + Vector2 screenEnd = MouseUiCoordinateConverter.InputToScreen(inputEnd); PointerEventData pointerData = MouseDragState.PointerData!; GameObject target = MouseDragState.Target!; string targetName = target.name; SimulateMouseUiOverlayState.Update( MouseAction.DragMove, - ScreenToInput(pointerData.position), + MouseUiCoordinateConverter.ScreenToInput(pointerData.position), SimulateMouseUiOverlayState.DragStartPosition, targetName, Handles.GetMainGameViewSize()); @@ -790,7 +775,7 @@ private async Task ExecuteDragEnd( } Vector2 inputEnd = new(parameters.X, parameters.Y); - Vector2 screenEnd = InputToScreen(inputEnd); + Vector2 screenEnd = MouseUiCoordinateConverter.InputToScreen(inputEnd); PointerEventData pointerData = MouseDragState.PointerData!; GameObject target = MouseDragState.Target!; string targetName = target.name; @@ -808,7 +793,7 @@ private async Task ExecuteDragEnd( SimulateMouseUiOverlayState.Update( MouseAction.DragEnd, - ScreenToInput(pointerData.position), + MouseUiCoordinateConverter.ScreenToInput(pointerData.position), SimulateMouseUiOverlayState.DragStartPosition, targetName, Handles.GetMainGameViewSize()); From 1238636f0dc0f3dd6e2de4730d8746a0f0870237 Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 9 Jul 2026 05:42:43 +0900 Subject: [PATCH 3/3] Extract mouse UI press action execution Move click and long-press event orchestration into a stateless executor, passing the shared cleanup scheduler explicitly so the helper remains independent of the use case. Retarget the timeout-state source guard to the new owner. --- .../Editor/StaticFacadeStateGuardTests.cs | 5 +- .../MouseUiPressActionExecutor.cs | 218 ++++++++++++++++++ .../MouseUiPressActionExecutor.cs.meta | 11 + .../SimulateMouseUi/SimulateMouseUiUseCase.cs | 206 +---------------- 4 files changed, 242 insertions(+), 198 deletions(-) create mode 100644 Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiPressActionExecutor.cs create mode 100644 Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiPressActionExecutor.cs.meta diff --git a/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs b/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs index 433f950558..4526b8987e 100644 --- a/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs +++ b/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs @@ -328,10 +328,11 @@ public void ExecuteDynamicCodeUseCase_WhenAwaitingWorkflow_DoesNotCaptureEditorS } [Test] - public void SimulateMouseUiUseCase_WhenReturningAfterTimeout_UsesCapturedUnityObjectState() + public void MouseUiPressActionExecutor_WhenReturningAfterTimeout_UsesCapturedUnityObjectState() { // Tests that timeout result branches use plain captured bools instead of UnityEngine.Object null checks. - string source = ReadSourceFile("Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs"); + string source = ReadSourceFile( + "Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiPressActionExecutor.cs"); Assert.That(source, Does.Contain("bool hitTarget = resolvedTargets.Target != null;")); Assert.That(source, Does.Contain("bool shouldReleasePointer = resolvedTargets.RawTarget != null && resolvedTargets.Target != null;")); diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiPressActionExecutor.cs b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiPressActionExecutor.cs new file mode 100644 index 0000000000..5cb3b556c3 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiPressActionExecutor.cs @@ -0,0 +1,218 @@ +#nullable enable +using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; +using UnityEngine.EventSystems; + +using io.github.hatayama.UnityCliLoop.Runtime; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// Executes click and long-press mouse UI actions. + /// + internal static class MouseUiPressActionExecutor + { + internal static async Task ExecuteClick( + MouseUiSimulationCommand parameters, + EventSystem eventSystem, + MouseUiMainThreadCleanupScheduler cleanupScheduler, + CancellationToken ct) + { + Vector2 inputPos = new(parameters.X, parameters.Y); + Vector2 screenPos = MouseUiCoordinateConverter.InputToScreen(inputPos); + PointerEventData pointerData = MouseUiPointerTargetResolver.CreatePointerPressData(eventSystem, screenPos, parameters.Button); + ResolvedPointerTargets resolvedTargets = + MouseUiPointerTargetResolver.ResolvePressablePointerTargets(parameters, eventSystem, inputPos, screenPos, pointerData, MouseAction.Click); + if (resolvedTargets.FailureResponse != null) + { + return resolvedTargets.FailureResponse; + } + + if (parameters.BypassRaycast && resolvedTargets.Target == null) + { + return new SimulateMouseUiResponse + { + Success = false, + Message = $"TargetPath '{parameters.TargetPath}' has no pointer click or pointer down handler.", + Action = MouseAction.Click.ToString(), + PositionX = inputPos.x, + PositionY = inputPos.y + }; + } + + string? targetName = resolvedTargets.Target?.name; + bool hitTarget = resolvedTargets.Target != null; + SimulateMouseUiOverlayState.Update( + MouseAction.Click, inputPos, null, + targetName, Handles.GetMainGameViewSize()); + + bool expandCompleted = await MouseUiOverlayAnimator.PlayExpandAnimation(ct).ConfigureAwait(false); + if (!expandCompleted) + { + cleanupScheduler.QueueOverlayClear(); + return MouseUiSimulationResponseFactory.CreateFrameTimeoutResult(MouseAction.Click, inputPos, null, targetName); + } + await MainThreadSwitcher.SwitchToMainThread(ct); + + // Fire click events after expand animation so the user sees where the click lands + ExecutePointerClickEvents(resolvedTargets, pointerData); + + bool dissipateCompleted = await MouseUiOverlayAnimator.PlayDissipateAnimation(ct).ConfigureAwait(false); + if (!dissipateCompleted) + { + cleanupScheduler.QueueOverlayClear(); + return MouseUiSimulationResponseFactory.CreateClickResult(parameters, inputPos, targetName, hitTarget); + } + await MainThreadSwitcher.SwitchToMainThread(ct); + + return MouseUiSimulationResponseFactory.CreateClickResult(parameters, inputPos, targetName, hitTarget); + } + + internal static async Task ExecuteLongPress( + MouseUiSimulationCommand parameters, + EventSystem eventSystem, + MouseUiMainThreadCleanupScheduler cleanupScheduler, + CancellationToken ct) + { + if (parameters.Duration <= 0f || float.IsNaN(parameters.Duration) || float.IsInfinity(parameters.Duration)) + { + return new SimulateMouseUiResponse + { + Success = false, + Message = $"Duration must be positive, got: {parameters.Duration}", + Action = MouseAction.LongPress.ToString() + }; + } + + Vector2 inputPos = new(parameters.X, parameters.Y); + Vector2 screenPos = MouseUiCoordinateConverter.InputToScreen(inputPos); + PointerEventData pointerData = MouseUiPointerTargetResolver.CreatePointerPressData(eventSystem, screenPos, parameters.Button); + ResolvedPointerTargets resolvedTargets = + MouseUiPointerTargetResolver.ResolvePressablePointerTargets(parameters, eventSystem, inputPos, screenPos, pointerData, MouseAction.LongPress); + if (resolvedTargets.FailureResponse != null) + { + return resolvedTargets.FailureResponse; + } + + if (parameters.BypassRaycast && resolvedTargets.Target == null) + { + return new SimulateMouseUiResponse + { + Success = false, + Message = $"TargetPath '{parameters.TargetPath}' has no pointer down or pointer click handler.", + Action = MouseAction.LongPress.ToString(), + PositionX = inputPos.x, + PositionY = inputPos.y + }; + } + + string? targetName = resolvedTargets.Target?.name; + bool hitTarget = resolvedTargets.Target != null; + bool shouldReleasePointer = resolvedTargets.RawTarget != null && resolvedTargets.Target != null; + SimulateMouseUiOverlayState.Update( + MouseAction.LongPress, inputPos, null, + targetName, Handles.GetMainGameViewSize()); + + bool expandCompleted = await MouseUiOverlayAnimator.PlayExpandAnimation(ct).ConfigureAwait(false); + if (!expandCompleted) + { + cleanupScheduler.QueueOverlayClear(); + return MouseUiSimulationResponseFactory.CreateFrameTimeoutResult(MouseAction.LongPress, inputPos, null, targetName); + } + await MainThreadSwitcher.SwitchToMainThread(ct); + + ExecuteLongPressPointerDown(resolvedTargets, pointerData); + + try + { + // Hold for Duration seconds, updating elapsed time each frame for overlay display + float startTime = Time.realtimeSinceStartup; + float elapsed = 0f; + while (elapsed < parameters.Duration) + { + SimulateMouseUiOverlayState.UpdateLongPressElapsed(elapsed); + bool frameReady = await MouseUiEditorFrameWaiter.WaitForEditorFrameAndSwitchToMainThreadAsync(ct).ConfigureAwait(false); + if (!frameReady) + { + cleanupScheduler.QueueOverlayClear(); + return MouseUiSimulationResponseFactory.CreateFrameTimeoutResult(MouseAction.LongPress, inputPos, null, targetName); + } + await MainThreadSwitcher.SwitchToMainThread(ct); + elapsed = Time.realtimeSinceStartup - startTime; + } + SimulateMouseUiOverlayState.UpdateLongPressElapsed(parameters.Duration); + } + finally + { + // Ensure pointerUp fires even if the hold loop is cancelled + if (shouldReleasePointer) + { + cleanupScheduler.ExecuteCleanupOnMainThread( + () => ExecuteEvents.Execute(resolvedTargets.Target!, pointerData, ExecuteEvents.pointerUpHandler)); + } + } + + bool dissipateCompleted = await MouseUiOverlayAnimator.PlayDissipateAnimation(ct).ConfigureAwait(false); + if (!dissipateCompleted) + { + cleanupScheduler.QueueOverlayClear(); + return MouseUiSimulationResponseFactory.CreateLongPressResult(parameters, inputPos, targetName, hitTarget); + } + await MainThreadSwitcher.SwitchToMainThread(ct); + + return MouseUiSimulationResponseFactory.CreateLongPressResult(parameters, inputPos, targetName, hitTarget); + } + + private static void ExecutePointerClickEvents( + ResolvedPointerTargets resolvedTargets, + PointerEventData pointerData) + { + if (resolvedTargets.RawTarget == null) + { + return; + } + + if (resolvedTargets.PressTarget != null) + { + ExecuteEvents.ExecuteHierarchy( + resolvedTargets.RawTarget, + pointerData, + ExecuteEvents.pointerDownHandler); + } + + if (resolvedTargets.Target != null) + { + ExecuteEvents.Execute( + resolvedTargets.Target, + pointerData, + ExecuteEvents.pointerUpHandler); + } + + if (resolvedTargets.ClickTarget != null) + { + ExecuteEvents.Execute( + resolvedTargets.ClickTarget, + pointerData, + ExecuteEvents.pointerClickHandler); + } + } + + private static void ExecuteLongPressPointerDown( + ResolvedPointerTargets resolvedTargets, + PointerEventData pointerData) + { + if (resolvedTargets.RawTarget == null || resolvedTargets.Target == null) + { + return; + } + + ExecuteEvents.ExecuteHierarchy( + resolvedTargets.RawTarget, + pointerData, + ExecuteEvents.pointerDownHandler); + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiPressActionExecutor.cs.meta b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiPressActionExecutor.cs.meta new file mode 100644 index 0000000000..dac5c0e384 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiPressActionExecutor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8b0a30f1d03b457c870578e67fd99f1b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs index 21cae78699..4f30a03383 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs @@ -85,7 +85,11 @@ private async Task ExecuteMouseAction( switch (parameters.Action) { case MouseAction.Click: - return await ExecuteClick(parameters, eventSystem, ct).ConfigureAwait(false); + return await MouseUiPressActionExecutor.ExecuteClick( + parameters, + eventSystem, + _mainThreadCleanupScheduler, + ct).ConfigureAwait(false); case MouseAction.Drag: return await ExecuteDragOneShot(parameters, eventSystem, ct).ConfigureAwait(false); @@ -100,7 +104,11 @@ private async Task ExecuteMouseAction( return await ExecuteDragEnd(parameters, ct).ConfigureAwait(false); case MouseAction.LongPress: - return await ExecuteLongPress(parameters, eventSystem, ct).ConfigureAwait(false); + return await MouseUiPressActionExecutor.ExecuteLongPress( + parameters, + eventSystem, + _mainThreadCleanupScheduler, + ct).ConfigureAwait(false); default: // Unreachable when TryFromSchema succeeds; kept as a defensive Success=false response @@ -111,200 +119,6 @@ private async Task ExecuteMouseAction( } } - private async Task ExecuteClick( - MouseUiSimulationCommand parameters, EventSystem eventSystem, CancellationToken ct) - { - Vector2 inputPos = new(parameters.X, parameters.Y); - Vector2 screenPos = MouseUiCoordinateConverter.InputToScreen(inputPos); - PointerEventData pointerData = MouseUiPointerTargetResolver.CreatePointerPressData(eventSystem, screenPos, parameters.Button); - ResolvedPointerTargets resolvedTargets = - MouseUiPointerTargetResolver.ResolvePressablePointerTargets(parameters, eventSystem, inputPos, screenPos, pointerData, MouseAction.Click); - if (resolvedTargets.FailureResponse != null) - { - return resolvedTargets.FailureResponse; - } - - if (parameters.BypassRaycast && resolvedTargets.Target == null) - { - return new SimulateMouseUiResponse - { - Success = false, - Message = $"TargetPath '{parameters.TargetPath}' has no pointer click or pointer down handler.", - Action = MouseAction.Click.ToString(), - PositionX = inputPos.x, - PositionY = inputPos.y - }; - } - - string? targetName = resolvedTargets.Target?.name; - bool hitTarget = resolvedTargets.Target != null; - SimulateMouseUiOverlayState.Update( - MouseAction.Click, inputPos, null, - targetName, Handles.GetMainGameViewSize()); - - bool expandCompleted = await MouseUiOverlayAnimator.PlayExpandAnimation(ct).ConfigureAwait(false); - if (!expandCompleted) - { - _mainThreadCleanupScheduler.QueueOverlayClear(); - return MouseUiSimulationResponseFactory.CreateFrameTimeoutResult(MouseAction.Click, inputPos, null, targetName); - } - await MainThreadSwitcher.SwitchToMainThread(ct); - - // Fire click events after expand animation so the user sees where the click lands - ExecutePointerClickEvents(resolvedTargets, pointerData); - - bool dissipateCompleted = await MouseUiOverlayAnimator.PlayDissipateAnimation(ct).ConfigureAwait(false); - if (!dissipateCompleted) - { - _mainThreadCleanupScheduler.QueueOverlayClear(); - return MouseUiSimulationResponseFactory.CreateClickResult(parameters, inputPos, targetName, hitTarget); - } - await MainThreadSwitcher.SwitchToMainThread(ct); - - return MouseUiSimulationResponseFactory.CreateClickResult(parameters, inputPos, targetName, hitTarget); - } - - private async Task ExecuteLongPress( - MouseUiSimulationCommand parameters, EventSystem eventSystem, CancellationToken ct) - { - if (parameters.Duration <= 0f || float.IsNaN(parameters.Duration) || float.IsInfinity(parameters.Duration)) - { - return new SimulateMouseUiResponse - { - Success = false, - Message = $"Duration must be positive, got: {parameters.Duration}", - Action = MouseAction.LongPress.ToString() - }; - } - - Vector2 inputPos = new(parameters.X, parameters.Y); - Vector2 screenPos = MouseUiCoordinateConverter.InputToScreen(inputPos); - PointerEventData pointerData = MouseUiPointerTargetResolver.CreatePointerPressData(eventSystem, screenPos, parameters.Button); - ResolvedPointerTargets resolvedTargets = - MouseUiPointerTargetResolver.ResolvePressablePointerTargets(parameters, eventSystem, inputPos, screenPos, pointerData, MouseAction.LongPress); - if (resolvedTargets.FailureResponse != null) - { - return resolvedTargets.FailureResponse; - } - - if (parameters.BypassRaycast && resolvedTargets.Target == null) - { - return new SimulateMouseUiResponse - { - Success = false, - Message = $"TargetPath '{parameters.TargetPath}' has no pointer down or pointer click handler.", - Action = MouseAction.LongPress.ToString(), - PositionX = inputPos.x, - PositionY = inputPos.y - }; - } - - string? targetName = resolvedTargets.Target?.name; - bool hitTarget = resolvedTargets.Target != null; - bool shouldReleasePointer = resolvedTargets.RawTarget != null && resolvedTargets.Target != null; - SimulateMouseUiOverlayState.Update( - MouseAction.LongPress, inputPos, null, - targetName, Handles.GetMainGameViewSize()); - - bool expandCompleted = await MouseUiOverlayAnimator.PlayExpandAnimation(ct).ConfigureAwait(false); - if (!expandCompleted) - { - _mainThreadCleanupScheduler.QueueOverlayClear(); - return MouseUiSimulationResponseFactory.CreateFrameTimeoutResult(MouseAction.LongPress, inputPos, null, targetName); - } - await MainThreadSwitcher.SwitchToMainThread(ct); - - ExecuteLongPressPointerDown(resolvedTargets, pointerData); - - try - { - // Hold for Duration seconds, updating elapsed time each frame for overlay display - float startTime = Time.realtimeSinceStartup; - float elapsed = 0f; - while (elapsed < parameters.Duration) - { - SimulateMouseUiOverlayState.UpdateLongPressElapsed(elapsed); - bool frameReady = await MouseUiEditorFrameWaiter.WaitForEditorFrameAndSwitchToMainThreadAsync(ct).ConfigureAwait(false); - if (!frameReady) - { - _mainThreadCleanupScheduler.QueueOverlayClear(); - return MouseUiSimulationResponseFactory.CreateFrameTimeoutResult(MouseAction.LongPress, inputPos, null, targetName); - } - await MainThreadSwitcher.SwitchToMainThread(ct); - elapsed = Time.realtimeSinceStartup - startTime; - } - SimulateMouseUiOverlayState.UpdateLongPressElapsed(parameters.Duration); - } - finally - { - // Ensure pointerUp fires even if the hold loop is cancelled - if (shouldReleasePointer) - { - _mainThreadCleanupScheduler.ExecuteCleanupOnMainThread( - () => ExecuteEvents.Execute(resolvedTargets.Target!, pointerData, ExecuteEvents.pointerUpHandler)); - } - } - - bool dissipateCompleted = await MouseUiOverlayAnimator.PlayDissipateAnimation(ct).ConfigureAwait(false); - if (!dissipateCompleted) - { - _mainThreadCleanupScheduler.QueueOverlayClear(); - return MouseUiSimulationResponseFactory.CreateLongPressResult(parameters, inputPos, targetName, hitTarget); - } - await MainThreadSwitcher.SwitchToMainThread(ct); - - return MouseUiSimulationResponseFactory.CreateLongPressResult(parameters, inputPos, targetName, hitTarget); - } - - private static void ExecutePointerClickEvents( - ResolvedPointerTargets resolvedTargets, - PointerEventData pointerData) - { - if (resolvedTargets.RawTarget == null) - { - return; - } - - if (resolvedTargets.PressTarget != null) - { - ExecuteEvents.ExecuteHierarchy( - resolvedTargets.RawTarget, - pointerData, - ExecuteEvents.pointerDownHandler); - } - - if (resolvedTargets.Target != null) - { - ExecuteEvents.Execute( - resolvedTargets.Target, - pointerData, - ExecuteEvents.pointerUpHandler); - } - - if (resolvedTargets.ClickTarget != null) - { - ExecuteEvents.Execute( - resolvedTargets.ClickTarget, - pointerData, - ExecuteEvents.pointerClickHandler); - } - } - - private static void ExecuteLongPressPointerDown( - ResolvedPointerTargets resolvedTargets, - PointerEventData pointerData) - { - if (resolvedTargets.RawTarget == null || resolvedTargets.Target == null) - { - return; - } - - ExecuteEvents.ExecuteHierarchy( - resolvedTargets.RawTarget, - pointerData, - ExecuteEvents.pointerDownHandler); - } - private PointerEventData InitiateDrag( EventSystem eventSystem, Vector2 screenPos,