From 68c11e7391b294bcb53c97c35486b23610980165 Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 9 Jul 2026 09:48:05 +0900 Subject: [PATCH] Extract mouse input press action execution Move click and long-press sequencing, runtime coordinate conversion, and button mapping behind one stateless executor so the use case keeps only tool orchestration. Preserve the async source guard for the move. --- .../Editor/StaticFacadeStateGuardTests.cs | 1 + .../MouseInputPressActionExecutor.cs | 282 ++++++++++++++++++ .../MouseInputPressActionExecutor.cs.meta | 11 + .../SimulateMouseInputUseCase.cs | 266 +---------------- 4 files changed, 296 insertions(+), 264 deletions(-) create mode 100644 Packages/src/Editor/FirstPartyTools/SimulateMouseInput/MouseInputPressActionExecutor.cs create mode 100644 Packages/src/Editor/FirstPartyTools/SimulateMouseInput/MouseInputPressActionExecutor.cs.meta diff --git a/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs b/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs index 1056bb1290..2583ad8a61 100644 --- a/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs +++ b/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs @@ -63,6 +63,7 @@ public sealed class StaticFacadeStateGuardTests "Packages/src/Editor/Presentation/Setup/SetupWizardWindow.cs", "Packages/src/Editor/FirstPartyTools/SimulateMouseInput/SimulateMouseInputUseCase.cs", "Packages/src/Editor/FirstPartyTools/SimulateMouseInput/MouseInputMainThreadCleanup.cs", + "Packages/src/Editor/FirstPartyTools/SimulateMouseInput/MouseInputPressActionExecutor.cs", "Packages/src/Editor/FirstPartyTools/SimulateKeyboard/SimulateKeyboardUseCase.cs" }; diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/MouseInputPressActionExecutor.cs b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/MouseInputPressActionExecutor.cs new file mode 100644 index 0000000000..cc683e7618 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/MouseInputPressActionExecutor.cs @@ -0,0 +1,282 @@ +#if ULOOP_HAS_INPUT_SYSTEM +#nullable enable +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; +using UnityEngine.InputSystem; + +using RuntimeMouseButton = io.github.hatayama.UnityCliLoop.Runtime.MouseButton; +using io.github.hatayama.UnityCliLoop.Runtime; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// Executes click and long-press mouse input actions. + /// + internal static class MouseInputPressActionExecutor + { + // Input coordinates use top-left origin; Unity Screen space uses bottom-left origin. + // Uses Screen.height (runtime resolution) because Mouse.current.position is in + // runtime screen space, not the editor Game view target resolution. + private static Vector2 InputToScreen(Vector2 inputPos) + { + return new Vector2(inputPos.x, Screen.height - inputPos.y); + } + + internal static async Task ExecuteClick( + Mouse mouse, SimulateMouseInputSchema request, CancellationToken ct) + { + if (request.Duration < 0f || float.IsNaN(request.Duration) || float.IsInfinity(request.Duration)) + { + return new SimulateMouseInputResponse + { + Success = false, + Message = $"Duration must be non-negative, got: {request.Duration}", + Action = UnityCliLoopMouseInputAction.Click.ToString() + }; + } + + Vector2 inputPos = new(request.X, request.Y); + Vector2 screenPos = InputToScreen(inputPos); + RuntimeMouseButton button = ToRuntimeMouseButton(request.Button); + string buttonName = button.ToString(); + + // Set mouse position before clicking + InputSimulationWaitOutcome positionOutcome = await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate( + () => MouseInputState.SetPositionState(mouse, screenPos), ct).ConfigureAwait(false); + if (positionOutcome == InputSimulationWaitOutcome.TimedOut) + { + return MouseInputSimulationResponseFactory.TimedOutButtonResult( + UnityCliLoopMouseInputAction.Click, + buttonName, + inputPos); + } + + // Press button + await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct); + MouseInputState.SetButtonDown(button); + SimulateMouseInputOverlayState.SetButtonHeld(button, true); + bool pressWasApplied = false; + InputSimulationWaitOutcome waitOutcome = InputSimulationWaitOutcome.Completed; + + try + { + waitOutcome = await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate( + () => MouseInputState.SetButtonState(mouse, button, true), ct) + .ConfigureAwait(false); + if (waitOutcome == InputSimulationWaitOutcome.Completed) + { + pressWasApplied = true; + waitOutcome = await InputSystemUpdateHelper.WaitForPressLifetime(request.Duration, ct) + .ConfigureAwait(false); + } + } + finally + { + if (waitOutcome == InputSimulationWaitOutcome.TimedOut) + { + MouseInputMainThreadCleanup.ScheduleTimedOutButtonCleanup(mouse, button, pressWasApplied); + } + else if (pressWasApplied) + { + InputSimulationWaitOutcome releaseOutcome = + await MouseInputMainThreadCleanup.ReleaseButtonIfPossible( + mouse, + button, + CancellationToken.None).ConfigureAwait(false); + if (releaseOutcome == InputSimulationWaitOutcome.TimedOut) + { + waitOutcome = InputSimulationWaitOutcome.TimedOut; + MouseInputMainThreadCleanup.ScheduleTimedOutButtonCleanup(mouse, button, false); + } + else + { + await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(CancellationToken.None); + MouseInputState.SetButtonUp(button); + } + } + else + { + await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(CancellationToken.None); + MouseInputState.SetButtonUp(button); + } + + if (waitOutcome != InputSimulationWaitOutcome.TimedOut) + { + if (waitOutcome == InputSimulationWaitOutcome.Paused) + { + SimulateMouseInputOverlayState.Clear(); + } + else + { + SimulateMouseInputOverlayState.SetButtonHeld(button, false); + } + } + } + + if (waitOutcome == InputSimulationWaitOutcome.Paused) + { + return MouseInputSimulationResponseFactory.InterruptedButtonResult( + UnityCliLoopMouseInputAction.Click, + buttonName, + inputPos); + } + + if (waitOutcome == InputSimulationWaitOutcome.TimedOut) + { + return MouseInputSimulationResponseFactory.TimedOutButtonResult( + UnityCliLoopMouseInputAction.Click, + buttonName, + inputPos); + } + + string durationText = request.Duration > 0f ? $" for {InputSimulationDurationFormatter.FormatSeconds(request.Duration)}s" : ""; + return new SimulateMouseInputResponse + { + Success = true, + Message = $"Clicked {buttonName} at ({inputPos.x:F1}, {inputPos.y:F1}){durationText}", + Action = UnityCliLoopMouseInputAction.Click.ToString(), + Button = buttonName, + PositionX = inputPos.x, + PositionY = inputPos.y + }; + } + + internal static async Task ExecuteLongPress( + Mouse mouse, SimulateMouseInputSchema request, CancellationToken ct) + { + if (request.Duration <= 0f || float.IsNaN(request.Duration) || float.IsInfinity(request.Duration)) + { + return new SimulateMouseInputResponse + { + Success = false, + Message = $"Duration must be positive for LongPress, got: {request.Duration}", + Action = UnityCliLoopMouseInputAction.LongPress.ToString() + }; + } + + Vector2 inputPos = new(request.X, request.Y); + Vector2 screenPos = InputToScreen(inputPos); + RuntimeMouseButton button = ToRuntimeMouseButton(request.Button); + string buttonName = button.ToString(); + + // Set mouse position before pressing + InputSimulationWaitOutcome positionOutcome = await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate( + () => MouseInputState.SetPositionState(mouse, screenPos), ct).ConfigureAwait(false); + if (positionOutcome == InputSimulationWaitOutcome.TimedOut) + { + return MouseInputSimulationResponseFactory.TimedOutButtonResult( + UnityCliLoopMouseInputAction.LongPress, + buttonName, + inputPos); + } + + // Press button + await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct); + MouseInputState.SetButtonDown(button); + SimulateMouseInputOverlayState.SetButtonHeld(button, true); + bool pressWasApplied = false; + InputSimulationWaitOutcome waitOutcome = InputSimulationWaitOutcome.Completed; + + try + { + waitOutcome = await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate( + () => MouseInputState.SetButtonState(mouse, button, true), ct) + .ConfigureAwait(false); + + // Hold for at least the minimum observation frames so the press + // is visible to game code, then continue until duration elapses. + if (waitOutcome == InputSimulationWaitOutcome.Completed) + { + pressWasApplied = true; + waitOutcome = await InputSystemUpdateHelper.WaitForPressLifetime(request.Duration, ct) + .ConfigureAwait(false); + } + } + finally + { + if (waitOutcome == InputSimulationWaitOutcome.TimedOut) + { + MouseInputMainThreadCleanup.ScheduleTimedOutButtonCleanup(mouse, button, pressWasApplied); + } + else if (pressWasApplied) + { + InputSimulationWaitOutcome releaseOutcome = + await MouseInputMainThreadCleanup.ReleaseButtonIfPossible( + mouse, + button, + CancellationToken.None).ConfigureAwait(false); + if (releaseOutcome == InputSimulationWaitOutcome.TimedOut) + { + waitOutcome = InputSimulationWaitOutcome.TimedOut; + MouseInputMainThreadCleanup.ScheduleTimedOutButtonCleanup(mouse, button, false); + } + else + { + await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(CancellationToken.None); + MouseInputState.SetButtonUp(button); + } + } + else + { + await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(CancellationToken.None); + MouseInputState.SetButtonUp(button); + } + + if (waitOutcome != InputSimulationWaitOutcome.TimedOut) + { + if (waitOutcome == InputSimulationWaitOutcome.Paused) + { + SimulateMouseInputOverlayState.Clear(); + } + else + { + SimulateMouseInputOverlayState.SetButtonHeld(button, false); + } + } + } + + if (waitOutcome == InputSimulationWaitOutcome.Paused) + { + return MouseInputSimulationResponseFactory.InterruptedButtonResult( + UnityCliLoopMouseInputAction.LongPress, + buttonName, + inputPos); + } + + if (waitOutcome == InputSimulationWaitOutcome.TimedOut) + { + return MouseInputSimulationResponseFactory.TimedOutButtonResult( + UnityCliLoopMouseInputAction.LongPress, + buttonName, + inputPos); + } + + return new SimulateMouseInputResponse + { + Success = true, + Message = $"Long-pressed {buttonName} at ({inputPos.x:F1}, {inputPos.y:F1}) for {InputSimulationDurationFormatter.FormatSeconds(request.Duration)}s", + Action = UnityCliLoopMouseInputAction.LongPress.ToString(), + Button = buttonName, + PositionX = inputPos.x, + PositionY = inputPos.y + }; + } + + private static RuntimeMouseButton ToRuntimeMouseButton(UnityCliLoopMouseButton button) + { + switch (button) + { + case UnityCliLoopMouseButton.Right: + return RuntimeMouseButton.Right; + case UnityCliLoopMouseButton.Middle: + return RuntimeMouseButton.Middle; + default: + Debug.Assert(button == UnityCliLoopMouseButton.Left, $"Unexpected mouse button value: {button}"); + return RuntimeMouseButton.Left; + } + } + } +} +#endif diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/MouseInputPressActionExecutor.cs.meta b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/MouseInputPressActionExecutor.cs.meta new file mode 100644 index 0000000000..b5d52cc384 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/MouseInputPressActionExecutor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 63d49d1d13ed14d7da1f9a6d6085f329 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/SimulateMouseInputUseCase.cs b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/SimulateMouseInputUseCase.cs index 95c34b207f..fa79d12e32 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/SimulateMouseInputUseCase.cs +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/SimulateMouseInputUseCase.cs @@ -7,7 +7,6 @@ using UnityEngine.InputSystem; #endif -using RuntimeMouseButton = io.github.hatayama.UnityCliLoop.Runtime.MouseButton; using io.github.hatayama.UnityCliLoop.Runtime; using io.github.hatayama.UnityCliLoop.ToolContracts; @@ -108,11 +107,11 @@ public async Task ExecuteAsync( switch (parameters.Action) { case UnityCliLoopMouseInputAction.Click: - response = await ExecuteClick(mouse, parameters, ct); + response = await MouseInputPressActionExecutor.ExecuteClick(mouse, parameters, ct); break; case UnityCliLoopMouseInputAction.LongPress: - response = await ExecuteLongPress(mouse, parameters, ct); + response = await MouseInputPressActionExecutor.ExecuteLongPress(mouse, parameters, ct); break; case UnityCliLoopMouseInputAction.MoveDelta: @@ -148,254 +147,6 @@ private static void EnsureOverlayExists() OverlayCanvasFactory.EnsureExists(); } - // Input coordinates use top-left origin; Unity Screen space uses bottom-left origin. - // Uses Screen.height (runtime resolution) because Mouse.current.position is in - // runtime screen space, not the editor Game view target resolution. - private static Vector2 InputToScreen(Vector2 inputPos) - { - return new Vector2(inputPos.x, Screen.height - inputPos.y); - } - - private async Task ExecuteClick( - Mouse mouse, SimulateMouseInputSchema request, CancellationToken ct) - { - if (request.Duration < 0f || float.IsNaN(request.Duration) || float.IsInfinity(request.Duration)) - { - return new SimulateMouseInputResponse - { - Success = false, - Message = $"Duration must be non-negative, got: {request.Duration}", - Action = UnityCliLoopMouseInputAction.Click.ToString() - }; - } - - Vector2 inputPos = new(request.X, request.Y); - Vector2 screenPos = InputToScreen(inputPos); - RuntimeMouseButton button = ToRuntimeMouseButton(request.Button); - string buttonName = button.ToString(); - - // Set mouse position before clicking - InputSimulationWaitOutcome positionOutcome = await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate( - () => MouseInputState.SetPositionState(mouse, screenPos), ct).ConfigureAwait(false); - if (positionOutcome == InputSimulationWaitOutcome.TimedOut) - { - return MouseInputSimulationResponseFactory.TimedOutButtonResult( - UnityCliLoopMouseInputAction.Click, - buttonName, - inputPos); - } - - // Press button - await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct); - MouseInputState.SetButtonDown(button); - SimulateMouseInputOverlayState.SetButtonHeld(button, true); - bool pressWasApplied = false; - InputSimulationWaitOutcome waitOutcome = InputSimulationWaitOutcome.Completed; - - try - { - waitOutcome = await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate( - () => MouseInputState.SetButtonState(mouse, button, true), ct) - .ConfigureAwait(false); - if (waitOutcome == InputSimulationWaitOutcome.Completed) - { - pressWasApplied = true; - waitOutcome = await InputSystemUpdateHelper.WaitForPressLifetime(request.Duration, ct) - .ConfigureAwait(false); - } - } - finally - { - if (waitOutcome == InputSimulationWaitOutcome.TimedOut) - { - MouseInputMainThreadCleanup.ScheduleTimedOutButtonCleanup(mouse, button, pressWasApplied); - } - else if (pressWasApplied) - { - InputSimulationWaitOutcome releaseOutcome = - await MouseInputMainThreadCleanup.ReleaseButtonIfPossible( - mouse, - button, - CancellationToken.None).ConfigureAwait(false); - if (releaseOutcome == InputSimulationWaitOutcome.TimedOut) - { - waitOutcome = InputSimulationWaitOutcome.TimedOut; - MouseInputMainThreadCleanup.ScheduleTimedOutButtonCleanup(mouse, button, false); - } - else - { - await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(CancellationToken.None); - MouseInputState.SetButtonUp(button); - } - } - else - { - await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(CancellationToken.None); - MouseInputState.SetButtonUp(button); - } - - if (waitOutcome != InputSimulationWaitOutcome.TimedOut) - { - if (waitOutcome == InputSimulationWaitOutcome.Paused) - { - SimulateMouseInputOverlayState.Clear(); - } - else - { - SimulateMouseInputOverlayState.SetButtonHeld(button, false); - } - } - } - - if (waitOutcome == InputSimulationWaitOutcome.Paused) - { - return MouseInputSimulationResponseFactory.InterruptedButtonResult( - UnityCliLoopMouseInputAction.Click, - buttonName, - inputPos); - } - - if (waitOutcome == InputSimulationWaitOutcome.TimedOut) - { - return MouseInputSimulationResponseFactory.TimedOutButtonResult( - UnityCliLoopMouseInputAction.Click, - buttonName, - inputPos); - } - - string durationText = request.Duration > 0f ? $" for {InputSimulationDurationFormatter.FormatSeconds(request.Duration)}s" : ""; - return new SimulateMouseInputResponse - { - Success = true, - Message = $"Clicked {buttonName} at ({inputPos.x:F1}, {inputPos.y:F1}){durationText}", - Action = UnityCliLoopMouseInputAction.Click.ToString(), - Button = buttonName, - PositionX = inputPos.x, - PositionY = inputPos.y - }; - } - - private async Task ExecuteLongPress( - Mouse mouse, SimulateMouseInputSchema request, CancellationToken ct) - { - if (request.Duration <= 0f || float.IsNaN(request.Duration) || float.IsInfinity(request.Duration)) - { - return new SimulateMouseInputResponse - { - Success = false, - Message = $"Duration must be positive for LongPress, got: {request.Duration}", - Action = UnityCliLoopMouseInputAction.LongPress.ToString() - }; - } - - Vector2 inputPos = new(request.X, request.Y); - Vector2 screenPos = InputToScreen(inputPos); - RuntimeMouseButton button = ToRuntimeMouseButton(request.Button); - string buttonName = button.ToString(); - - // Set mouse position before pressing - InputSimulationWaitOutcome positionOutcome = await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate( - () => MouseInputState.SetPositionState(mouse, screenPos), ct).ConfigureAwait(false); - if (positionOutcome == InputSimulationWaitOutcome.TimedOut) - { - return MouseInputSimulationResponseFactory.TimedOutButtonResult( - UnityCliLoopMouseInputAction.LongPress, - buttonName, - inputPos); - } - - // Press button - await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct); - MouseInputState.SetButtonDown(button); - SimulateMouseInputOverlayState.SetButtonHeld(button, true); - bool pressWasApplied = false; - InputSimulationWaitOutcome waitOutcome = InputSimulationWaitOutcome.Completed; - - try - { - waitOutcome = await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate( - () => MouseInputState.SetButtonState(mouse, button, true), ct) - .ConfigureAwait(false); - - // Hold for at least the minimum observation frames so the press - // is visible to game code, then continue until duration elapses. - if (waitOutcome == InputSimulationWaitOutcome.Completed) - { - pressWasApplied = true; - waitOutcome = await InputSystemUpdateHelper.WaitForPressLifetime(request.Duration, ct) - .ConfigureAwait(false); - } - } - finally - { - if (waitOutcome == InputSimulationWaitOutcome.TimedOut) - { - MouseInputMainThreadCleanup.ScheduleTimedOutButtonCleanup(mouse, button, pressWasApplied); - } - else if (pressWasApplied) - { - InputSimulationWaitOutcome releaseOutcome = - await MouseInputMainThreadCleanup.ReleaseButtonIfPossible( - mouse, - button, - CancellationToken.None).ConfigureAwait(false); - if (releaseOutcome == InputSimulationWaitOutcome.TimedOut) - { - waitOutcome = InputSimulationWaitOutcome.TimedOut; - MouseInputMainThreadCleanup.ScheduleTimedOutButtonCleanup(mouse, button, false); - } - else - { - await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(CancellationToken.None); - MouseInputState.SetButtonUp(button); - } - } - else - { - await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(CancellationToken.None); - MouseInputState.SetButtonUp(button); - } - - if (waitOutcome != InputSimulationWaitOutcome.TimedOut) - { - if (waitOutcome == InputSimulationWaitOutcome.Paused) - { - SimulateMouseInputOverlayState.Clear(); - } - else - { - SimulateMouseInputOverlayState.SetButtonHeld(button, false); - } - } - } - - if (waitOutcome == InputSimulationWaitOutcome.Paused) - { - return MouseInputSimulationResponseFactory.InterruptedButtonResult( - UnityCliLoopMouseInputAction.LongPress, - buttonName, - inputPos); - } - - if (waitOutcome == InputSimulationWaitOutcome.TimedOut) - { - return MouseInputSimulationResponseFactory.TimedOutButtonResult( - UnityCliLoopMouseInputAction.LongPress, - buttonName, - inputPos); - } - - return new SimulateMouseInputResponse - { - Success = true, - Message = $"Long-pressed {buttonName} at ({inputPos.x:F1}, {inputPos.y:F1}) for {InputSimulationDurationFormatter.FormatSeconds(request.Duration)}s", - Action = UnityCliLoopMouseInputAction.LongPress.ToString(), - Button = buttonName, - PositionX = inputPos.x, - PositionY = inputPos.y - }; - } - private async Task ExecuteMoveDelta( Mouse mouse, SimulateMouseInputSchema request, CancellationToken ct) { @@ -562,19 +313,6 @@ private async Task ExecuteSmoothDelta( }; } - private static RuntimeMouseButton ToRuntimeMouseButton(UnityCliLoopMouseButton button) - { - switch (button) - { - case UnityCliLoopMouseButton.Right: - return RuntimeMouseButton.Right; - case UnityCliLoopMouseButton.Middle: - return RuntimeMouseButton.Middle; - default: - Debug.Assert(button == UnityCliLoopMouseButton.Left, $"Unexpected mouse button value: {button}"); - return RuntimeMouseButton.Left; - } - } #endif } }