From 6de0dcf47f39a2b75a8425b3189ef355306337e8 Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 9 Jul 2026 06:13:34 +0900 Subject: [PATCH 1/2] Extract shared mouse UI drag event mechanics Move drag initiation, interpolation, and finalization into a stateless helper so one-shot and incremental drag orchestration share the same pointer event lifecycle without depending on the use case. --- .../MouseUiDragEventExecutor.cs | 143 ++++++++++++++++++ .../MouseUiDragEventExecutor.cs.meta | 11 ++ .../SimulateMouseUi/SimulateMouseUiUseCase.cs | 139 +---------------- 3 files changed, 162 insertions(+), 131 deletions(-) create mode 100644 Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiDragEventExecutor.cs create mode 100644 Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiDragEventExecutor.cs.meta diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiDragEventExecutor.cs b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiDragEventExecutor.cs new file mode 100644 index 0000000000..a318732dfd --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiDragEventExecutor.cs @@ -0,0 +1,143 @@ +#nullable enable +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; +using UnityEngine.EventSystems; + +using io.github.hatayama.UnityCliLoop.Runtime; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// Executes pointer event mechanics shared by one-shot and incremental UI drags. + /// + internal static class MouseUiDragEventExecutor + { + internal static PointerEventData InitiateDrag( + EventSystem eventSystem, + Vector2 screenPos, + RaycastResult raycastResult, + GameObject dragTarget, + PointerEventData.InputButton inputButton) + { + PointerEventData pointerData = new(eventSystem) + { + position = screenPos, + pressPosition = screenPos, + button = inputButton, + pointerCurrentRaycast = raycastResult, + pointerPressRaycast = raycastResult, + pointerDrag = dragTarget, + rawPointerPress = raycastResult.gameObject + }; + + // Slider.OnPointerDown initializes m_Offset for handle positioning + GameObject? pressTarget = ExecuteEvents.ExecuteHierarchy( + raycastResult.gameObject, pointerData, ExecuteEvents.pointerDownHandler); + pointerData.pointerPress = pressTarget; + + // ScrollRect.OnInitializePotentialDrag clears inertia, Slider sets useDragThreshold=false + ExecuteEvents.Execute(dragTarget, pointerData, ExecuteEvents.initializePotentialDrag); + + return pointerData; + } + + // Lifecycle must match StandaloneInputModule: raycast → pointerUp → drop → endDrag + internal static void FinalizeDrag( + PointerEventData pointerData, + GameObject target, + GameObject? explicitDropTarget) + { + if (explicitDropTarget != null) + { + pointerData.pointerCurrentRaycast = MouseUiPointerTargetResolver.CreateDirectRaycastResult(explicitDropTarget); + } + else + { + UpdatePointerRaycast(pointerData); + } + + if (pointerData.pointerPress != null) + { + ExecuteEvents.Execute(pointerData.pointerPress, pointerData, ExecuteEvents.pointerUpHandler); + } + + // Standard IDropHandler dispatch so Unity drop targets respond without manual workarounds + GameObject? dropTarget = pointerData.pointerCurrentRaycast.gameObject; + if (dropTarget != null) + { + ExecuteEvents.ExecuteHierarchy(dropTarget, pointerData, ExecuteEvents.dropHandler); + } + + pointerData.dragging = false; + ExecuteEvents.Execute(target, pointerData, ExecuteEvents.endDragHandler); + } + + private static void UpdatePointerRaycast(PointerEventData pointerData) + { + RaycastResult? hit = UiRaycastHelper.RaycastUI(pointerData.position, EventSystem.current); + pointerData.pointerCurrentRaycast = hit ?? new RaycastResult(); + } + + internal static async Task InterpolateDragPosition( + PointerEventData pointerData, + GameObject target, + Vector2 endPos, + float dragSpeed, + CancellationToken ct) + { + Debug.Assert(dragSpeed >= 0f, "dragSpeed must be non-negative"); + + Vector2 startPos = pointerData.position; + float distance = Vector2.Distance(startPos, endPos); + float duration = dragSpeed > 0f ? distance / dragSpeed : 0f; + + if (duration <= 0f) + { + bool frameReady = await MouseUiEditorFrameWaiter.WaitForEditorFrameAndSwitchToMainThreadAsync(ct).ConfigureAwait(false); + if (!frameReady) + { + return false; + } + await MainThreadSwitcher.SwitchToMainThread(ct); + + Vector2 previousPosition = pointerData.position; + pointerData.position = endPos; + pointerData.delta = endPos - previousPosition; + ExecuteEvents.Execute(target, pointerData, ExecuteEvents.dragHandler); + + SimulateMouseUiOverlayState.UpdatePosition(MouseUiCoordinateConverter.ScreenToInput(endPos)); + return true; + } + + float startTime = Time.realtimeSinceStartup; + float t; + + do + { + bool frameReady = await MouseUiEditorFrameWaiter.WaitForEditorFrameAndSwitchToMainThreadAsync(ct).ConfigureAwait(false); + if (!frameReady) + { + return false; + } + await MainThreadSwitcher.SwitchToMainThread(ct); + + float elapsed = Time.realtimeSinceStartup - startTime; + t = Mathf.Clamp01(elapsed / duration); + Vector2 previousPosition = pointerData.position; + Vector2 currentPosition = Vector2.Lerp(startPos, endPos, t); + + pointerData.position = currentPosition; + pointerData.delta = currentPosition - previousPosition; + + ExecuteEvents.Execute(target, pointerData, ExecuteEvents.dragHandler); + + SimulateMouseUiOverlayState.UpdatePosition(MouseUiCoordinateConverter.ScreenToInput(currentPosition)); + } + while (t < 1.0f); + + return true; + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiDragEventExecutor.cs.meta b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiDragEventExecutor.cs.meta new file mode 100644 index 0000000000..abb02a6a7a --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiDragEventExecutor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6528ed09150a4ced91b1dffea8ada186 +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 4f30a03383..50f095aec7 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs @@ -119,35 +119,6 @@ private async Task ExecuteMouseAction( } } - private PointerEventData InitiateDrag( - EventSystem eventSystem, - Vector2 screenPos, - RaycastResult raycastResult, - GameObject dragTarget, - PointerEventData.InputButton inputButton) - { - PointerEventData pointerData = new(eventSystem) - { - position = screenPos, - pressPosition = screenPos, - button = inputButton, - pointerCurrentRaycast = raycastResult, - pointerPressRaycast = raycastResult, - pointerDrag = dragTarget, - rawPointerPress = raycastResult.gameObject - }; - - // Slider.OnPointerDown initializes m_Offset for handle positioning - GameObject? pressTarget = ExecuteEvents.ExecuteHierarchy( - raycastResult.gameObject, pointerData, ExecuteEvents.pointerDownHandler); - pointerData.pointerPress = pressTarget; - - // ScrollRect.OnInitializePotentialDrag clears inertia, Slider sets useDragThreshold=false - ExecuteEvents.Execute(dragTarget, pointerData, ExecuteEvents.initializePotentialDrag); - - return pointerData; - } - private async Task ExecuteDragOneShot( MouseUiSimulationCommand parameters, EventSystem eventSystem, CancellationToken ct) { @@ -231,7 +202,7 @@ private async Task ExecuteDragOneShot( } // uGUI drag controls (ScrollRect, Slider) only respond to left-button drags - PointerEventData pointerData = InitiateDrag(eventSystem, screenStart, startRaycast, target, PointerEventData.InputButton.Left); + PointerEventData pointerData = MouseUiDragEventExecutor.InitiateDrag(eventSystem, screenStart, startRaycast, target, PointerEventData.InputButton.Left); ExecuteEvents.Execute(target, pointerData, ExecuteEvents.beginDragHandler); pointerData.dragging = true; @@ -249,7 +220,7 @@ private async Task ExecuteDragOneShot( } await MainThreadSwitcher.SwitchToMainThread(ct); - bool dragCompleted = await InterpolateDragPosition(pointerData, target, screenEnd, parameters.DragSpeed, ct) + bool dragCompleted = await MouseUiDragEventExecutor.InterpolateDragPosition(pointerData, target, screenEnd, parameters.DragSpeed, ct) .ConfigureAwait(false); if (!dragCompleted) { @@ -268,7 +239,7 @@ private async Task ExecuteDragOneShot( } finally { - _mainThreadCleanupScheduler.ExecuteCleanupOnMainThread(() => FinalizeDrag(pointerData, target, explicitDropTarget)); + _mainThreadCleanupScheduler.ExecuteCleanupOnMainThread(() => MouseUiDragEventExecutor.FinalizeDrag(pointerData, target, explicitDropTarget)); } SimulateMouseUiOverlayState.Update( @@ -285,100 +256,6 @@ private async Task ExecuteDragOneShot( return MouseUiSimulationResponseFactory.CreateDragResult(parameters, inputStart, inputEnd, targetName); } - // Lifecycle must match StandaloneInputModule: raycast → pointerUp → drop → endDrag - private void FinalizeDrag(PointerEventData pointerData, GameObject target, GameObject? explicitDropTarget) - { - if (explicitDropTarget != null) - { - pointerData.pointerCurrentRaycast = MouseUiPointerTargetResolver.CreateDirectRaycastResult(explicitDropTarget); - } - else - { - UpdatePointerRaycast(pointerData); - } - - if (pointerData.pointerPress != null) - { - ExecuteEvents.Execute(pointerData.pointerPress, pointerData, ExecuteEvents.pointerUpHandler); - } - - // Standard IDropHandler dispatch so Unity drop targets respond without manual workarounds - GameObject? dropTarget = pointerData.pointerCurrentRaycast.gameObject; - if (dropTarget != null) - { - ExecuteEvents.ExecuteHierarchy(dropTarget, pointerData, ExecuteEvents.dropHandler); - } - - pointerData.dragging = false; - ExecuteEvents.Execute(target, pointerData, ExecuteEvents.endDragHandler); - } - - private void UpdatePointerRaycast(PointerEventData pointerData) - { - RaycastResult? hit = UiRaycastHelper.RaycastUI(pointerData.position, EventSystem.current); - pointerData.pointerCurrentRaycast = hit ?? new RaycastResult(); - } - - private async Task InterpolateDragPosition( - PointerEventData pointerData, - GameObject target, - Vector2 endPos, - float dragSpeed, - CancellationToken ct) - { - Debug.Assert(dragSpeed >= 0f, "dragSpeed must be non-negative"); - - Vector2 startPos = pointerData.position; - float distance = Vector2.Distance(startPos, endPos); - float duration = dragSpeed > 0f ? distance / dragSpeed : 0f; - - if (duration <= 0f) - { - bool frameReady = await MouseUiEditorFrameWaiter.WaitForEditorFrameAndSwitchToMainThreadAsync(ct).ConfigureAwait(false); - if (!frameReady) - { - return false; - } - await MainThreadSwitcher.SwitchToMainThread(ct); - - Vector2 previousPosition = pointerData.position; - pointerData.position = endPos; - pointerData.delta = endPos - previousPosition; - ExecuteEvents.Execute(target, pointerData, ExecuteEvents.dragHandler); - - SimulateMouseUiOverlayState.UpdatePosition(MouseUiCoordinateConverter.ScreenToInput(endPos)); - return true; - } - - float startTime = Time.realtimeSinceStartup; - float t; - - do - { - bool frameReady = await MouseUiEditorFrameWaiter.WaitForEditorFrameAndSwitchToMainThreadAsync(ct).ConfigureAwait(false); - if (!frameReady) - { - return false; - } - await MainThreadSwitcher.SwitchToMainThread(ct); - - float elapsed = Time.realtimeSinceStartup - startTime; - t = Mathf.Clamp01(elapsed / duration); - Vector2 previousPosition = pointerData.position; - Vector2 currentPosition = Vector2.Lerp(startPos, endPos, t); - - pointerData.position = currentPosition; - pointerData.delta = currentPosition - previousPosition; - - ExecuteEvents.Execute(target, pointerData, ExecuteEvents.dragHandler); - - SimulateMouseUiOverlayState.UpdatePosition(MouseUiCoordinateConverter.ScreenToInput(currentPosition)); - } - while (t < 1.0f); - - return true; - } - private async Task ExecuteDragStart( MouseUiSimulationCommand parameters, EventSystem eventSystem, CancellationToken ct) { @@ -457,7 +334,7 @@ private async Task ExecuteDragStart( }; } - PointerEventData pointerData = InitiateDrag(eventSystem, screenPos, startRaycast, target, PointerEventData.InputButton.Left); + PointerEventData pointerData = MouseUiDragEventExecutor.InitiateDrag(eventSystem, screenPos, startRaycast, target, PointerEventData.InputButton.Left); ExecuteEvents.Execute(target, pointerData, ExecuteEvents.beginDragHandler); pointerData.dragging = true; @@ -487,7 +364,7 @@ private async Task ExecuteDragStart( { _mainThreadCleanupScheduler.ExecuteCleanupOnMainThread(() => { - FinalizeDrag(pointerData, target, null); + MouseUiDragEventExecutor.FinalizeDrag(pointerData, target, null); MouseDragState.Clear(); }); } @@ -541,7 +418,7 @@ private async Task ExecuteDragMove( targetName, Handles.GetMainGameViewSize()); // Cancellation leaves drag state intact so the user can continue with DragMove/DragEnd - bool dragCompleted = await InterpolateDragPosition( + bool dragCompleted = await MouseUiDragEventExecutor.InterpolateDragPosition( pointerData, target, screenEnd, parameters.DragSpeed, ct).ConfigureAwait(false); if (!dragCompleted) @@ -613,7 +490,7 @@ private async Task ExecuteDragEnd( try { - bool dragCompleted = await InterpolateDragPosition( + bool dragCompleted = await MouseUiDragEventExecutor.InterpolateDragPosition( pointerData, target, screenEnd, parameters.DragSpeed, ct).ConfigureAwait(false); if (!dragCompleted) @@ -635,7 +512,7 @@ private async Task ExecuteDragEnd( { _mainThreadCleanupScheduler.ExecuteCleanupOnMainThread(() => { - FinalizeDrag(pointerData, target, explicitDropTarget); + MouseUiDragEventExecutor.FinalizeDrag(pointerData, target, explicitDropTarget); MouseDragState.Clear(); }); } From fda8c3bf939dcb1fe8ba070b069fb4aac88ba8a7 Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 9 Jul 2026 06:17:02 +0900 Subject: [PATCH 2/2] Extract one-shot mouse UI drag execution Move complete drag orchestration into a stateless executor, passing the shared cleanup scheduler explicitly while preserving pointer event timing, captured cleanup state, and timeout behavior. --- .../MouseUiOneShotDragExecutor.cs | 158 ++++++++++++++++++ .../MouseUiOneShotDragExecutor.cs.meta | 11 ++ .../SimulateMouseUi/SimulateMouseUiUseCase.cs | 143 +--------------- 3 files changed, 174 insertions(+), 138 deletions(-) create mode 100644 Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiOneShotDragExecutor.cs create mode 100644 Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiOneShotDragExecutor.cs.meta diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiOneShotDragExecutor.cs b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiOneShotDragExecutor.cs new file mode 100644 index 0000000000..4b5b25e43a --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiOneShotDragExecutor.cs @@ -0,0 +1,158 @@ +#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 a complete mouse UI drag in one request. + /// + internal static class MouseUiOneShotDragExecutor + { + internal static async Task ExecuteDragOneShot( + MouseUiSimulationCommand parameters, + EventSystem eventSystem, + MouseUiMainThreadCleanupScheduler cleanupScheduler, + CancellationToken ct) + { + Vector2 inputStart = new(parameters.FromX, parameters.FromY); + Vector2 inputEnd = new(parameters.X, parameters.Y); + 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; + + if (parameters.BypassRaycast) + { + if (!MouseUiPointerTargetResolver.TryResolveGameObjectPath( + parameters.TargetPath, + "TargetPath", + MouseAction.Drag, + inputStart, + out rawTarget, + out SimulateMouseUiResponse? failureResponse)) + { + return failureResponse!; + } + + startRaycast = MouseUiPointerTargetResolver.CreateDirectRaycastResult(rawTarget!); + } + else if (hit != null) + { + rawTarget = hit.Value.gameObject; + startRaycast = hit.Value; + } + + GameObject? explicitDropTarget = null; + if (!MouseUiPointerTargetResolver.TryResolveDropTargetPath( + parameters, + MouseAction.Drag, + inputEnd, + out explicitDropTarget, + out SimulateMouseUiResponse? dropFailureResponse)) + { + return dropFailureResponse!; + } + + // Execute dispatches only to the exact target; resolve the actual drag handler up the hierarchy + GameObject? target = rawTarget != null + ? ExecuteEvents.GetEventHandler(rawTarget) + : null; + + if (target == null) + { + SimulateMouseUiOverlayState.Update( + MouseAction.Drag, inputStart, null, null, Handles.GetMainGameViewSize()); + bool expandCompleted = await MouseUiOverlayAnimator.PlayExpandAnimation(ct).ConfigureAwait(false); + if (!expandCompleted) + { + cleanupScheduler.QueueOverlayClear(); + return MouseUiSimulationResponseFactory.CreateFrameTimeoutResult(MouseAction.Drag, inputStart, inputEnd, null); + } + await MainThreadSwitcher.SwitchToMainThread(ct); + + bool dissipateCompleted = await MouseUiOverlayAnimator.PlayDissipateAnimation(ct).ConfigureAwait(false); + if (!dissipateCompleted) + { + cleanupScheduler.QueueOverlayClear(); + return MouseUiSimulationResponseFactory.CreateFrameTimeoutResult(MouseAction.Drag, inputStart, inputEnd, null); + } + await MainThreadSwitcher.SwitchToMainThread(ct); + + return new SimulateMouseUiResponse + { + Success = false, + Message = parameters.BypassRaycast + ? $"TargetPath '{parameters.TargetPath}' has no drag handler." + : $"No draggable UI element at ({inputStart.x:F1}, {inputStart.y:F1}). Use find-game-objects or screenshot to verify positions.", + Action = MouseAction.Drag.ToString(), + PositionX = inputStart.x, + PositionY = inputStart.y, + EndPositionX = inputEnd.x, + EndPositionY = inputEnd.y + }; + } + + // uGUI drag controls (ScrollRect, Slider) only respond to left-button drags + PointerEventData pointerData = MouseUiDragEventExecutor.InitiateDrag(eventSystem, screenStart, startRaycast, target, PointerEventData.InputButton.Left); + ExecuteEvents.Execute(target, pointerData, ExecuteEvents.beginDragHandler); + pointerData.dragging = true; + + string targetName = target.name; + SimulateMouseUiOverlayState.Update( + MouseAction.Drag, inputStart, inputStart, targetName, Handles.GetMainGameViewSize()); + + try + { + bool expandCompleted = await MouseUiOverlayAnimator.PlayExpandAnimation(ct).ConfigureAwait(false); + if (!expandCompleted) + { + cleanupScheduler.QueueOverlayClear(); + return MouseUiSimulationResponseFactory.CreateFrameTimeoutResult(MouseAction.Drag, inputStart, inputEnd, targetName); + } + await MainThreadSwitcher.SwitchToMainThread(ct); + + bool dragCompleted = await MouseUiDragEventExecutor.InterpolateDragPosition(pointerData, target, screenEnd, parameters.DragSpeed, ct) + .ConfigureAwait(false); + if (!dragCompleted) + { + cleanupScheduler.QueueOverlayClear(); + return MouseUiSimulationResponseFactory.CreateFrameTimeoutResult(MouseAction.Drag, inputStart, inputEnd, targetName); + } + await MainThreadSwitcher.SwitchToMainThread(ct); + + bool frameReady = await MouseUiEditorFrameWaiter.WaitForEditorFrameAndSwitchToMainThreadAsync(ct).ConfigureAwait(false); + if (!frameReady) + { + cleanupScheduler.QueueOverlayClear(); + return MouseUiSimulationResponseFactory.CreateFrameTimeoutResult(MouseAction.Drag, inputStart, inputEnd, targetName); + } + await MainThreadSwitcher.SwitchToMainThread(ct); + } + finally + { + cleanupScheduler.ExecuteCleanupOnMainThread(() => MouseUiDragEventExecutor.FinalizeDrag(pointerData, target, explicitDropTarget)); + } + + SimulateMouseUiOverlayState.Update( + MouseAction.Drag, inputEnd, inputStart, targetName, Handles.GetMainGameViewSize()); + + bool completedDissipate = await MouseUiOverlayAnimator.PlayDissipateAnimation(ct).ConfigureAwait(false); + if (!completedDissipate) + { + cleanupScheduler.QueueOverlayClear(); + return MouseUiSimulationResponseFactory.CreateDragResult(parameters, inputStart, inputEnd, targetName); + } + await MainThreadSwitcher.SwitchToMainThread(ct); + + return MouseUiSimulationResponseFactory.CreateDragResult(parameters, inputStart, inputEnd, targetName); + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiOneShotDragExecutor.cs.meta b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiOneShotDragExecutor.cs.meta new file mode 100644 index 0000000000..fbc4f5ef41 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiOneShotDragExecutor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0eff98014e504554a60bbee519a91190 +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 50f095aec7..8bf22ca3aa 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs @@ -92,7 +92,11 @@ private async Task ExecuteMouseAction( ct).ConfigureAwait(false); case MouseAction.Drag: - return await ExecuteDragOneShot(parameters, eventSystem, ct).ConfigureAwait(false); + return await MouseUiOneShotDragExecutor.ExecuteDragOneShot( + parameters, + eventSystem, + _mainThreadCleanupScheduler, + ct).ConfigureAwait(false); case MouseAction.DragStart: return await ExecuteDragStart(parameters, eventSystem, ct).ConfigureAwait(false); @@ -119,143 +123,6 @@ private async Task ExecuteMouseAction( } } - private async Task ExecuteDragOneShot( - MouseUiSimulationCommand parameters, EventSystem eventSystem, CancellationToken ct) - { - Vector2 inputStart = new(parameters.FromX, parameters.FromY); - Vector2 inputEnd = new(parameters.X, parameters.Y); - 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; - - if (parameters.BypassRaycast) - { - if (!MouseUiPointerTargetResolver.TryResolveGameObjectPath( - parameters.TargetPath, - "TargetPath", - MouseAction.Drag, - inputStart, - out rawTarget, - out SimulateMouseUiResponse? failureResponse)) - { - return failureResponse!; - } - - startRaycast = MouseUiPointerTargetResolver.CreateDirectRaycastResult(rawTarget!); - } - else if (hit != null) - { - rawTarget = hit.Value.gameObject; - startRaycast = hit.Value; - } - - GameObject? explicitDropTarget = null; - if (!MouseUiPointerTargetResolver.TryResolveDropTargetPath( - parameters, - MouseAction.Drag, - inputEnd, - out explicitDropTarget, - out SimulateMouseUiResponse? dropFailureResponse)) - { - return dropFailureResponse!; - } - - // Execute dispatches only to the exact target; resolve the actual drag handler up the hierarchy - GameObject? target = rawTarget != null - ? ExecuteEvents.GetEventHandler(rawTarget) - : null; - - if (target == null) - { - SimulateMouseUiOverlayState.Update( - MouseAction.Drag, inputStart, null, null, Handles.GetMainGameViewSize()); - bool expandCompleted = await MouseUiOverlayAnimator.PlayExpandAnimation(ct).ConfigureAwait(false); - if (!expandCompleted) - { - _mainThreadCleanupScheduler.QueueOverlayClear(); - return MouseUiSimulationResponseFactory.CreateFrameTimeoutResult(MouseAction.Drag, inputStart, inputEnd, null); - } - await MainThreadSwitcher.SwitchToMainThread(ct); - - bool dissipateCompleted = await MouseUiOverlayAnimator.PlayDissipateAnimation(ct).ConfigureAwait(false); - if (!dissipateCompleted) - { - _mainThreadCleanupScheduler.QueueOverlayClear(); - return MouseUiSimulationResponseFactory.CreateFrameTimeoutResult(MouseAction.Drag, inputStart, inputEnd, null); - } - await MainThreadSwitcher.SwitchToMainThread(ct); - - return new SimulateMouseUiResponse - { - Success = false, - Message = parameters.BypassRaycast - ? $"TargetPath '{parameters.TargetPath}' has no drag handler." - : $"No draggable UI element at ({inputStart.x:F1}, {inputStart.y:F1}). Use find-game-objects or screenshot to verify positions.", - Action = MouseAction.Drag.ToString(), - PositionX = inputStart.x, - PositionY = inputStart.y, - EndPositionX = inputEnd.x, - EndPositionY = inputEnd.y - }; - } - - // uGUI drag controls (ScrollRect, Slider) only respond to left-button drags - PointerEventData pointerData = MouseUiDragEventExecutor.InitiateDrag(eventSystem, screenStart, startRaycast, target, PointerEventData.InputButton.Left); - ExecuteEvents.Execute(target, pointerData, ExecuteEvents.beginDragHandler); - pointerData.dragging = true; - - string targetName = target.name; - SimulateMouseUiOverlayState.Update( - MouseAction.Drag, inputStart, inputStart, targetName, Handles.GetMainGameViewSize()); - - try - { - bool expandCompleted = await MouseUiOverlayAnimator.PlayExpandAnimation(ct).ConfigureAwait(false); - if (!expandCompleted) - { - _mainThreadCleanupScheduler.QueueOverlayClear(); - return MouseUiSimulationResponseFactory.CreateFrameTimeoutResult(MouseAction.Drag, inputStart, inputEnd, targetName); - } - await MainThreadSwitcher.SwitchToMainThread(ct); - - bool dragCompleted = await MouseUiDragEventExecutor.InterpolateDragPosition(pointerData, target, screenEnd, parameters.DragSpeed, ct) - .ConfigureAwait(false); - if (!dragCompleted) - { - _mainThreadCleanupScheduler.QueueOverlayClear(); - return MouseUiSimulationResponseFactory.CreateFrameTimeoutResult(MouseAction.Drag, inputStart, inputEnd, targetName); - } - await MainThreadSwitcher.SwitchToMainThread(ct); - - bool frameReady = await MouseUiEditorFrameWaiter.WaitForEditorFrameAndSwitchToMainThreadAsync(ct).ConfigureAwait(false); - if (!frameReady) - { - _mainThreadCleanupScheduler.QueueOverlayClear(); - return MouseUiSimulationResponseFactory.CreateFrameTimeoutResult(MouseAction.Drag, inputStart, inputEnd, targetName); - } - await MainThreadSwitcher.SwitchToMainThread(ct); - } - finally - { - _mainThreadCleanupScheduler.ExecuteCleanupOnMainThread(() => MouseUiDragEventExecutor.FinalizeDrag(pointerData, target, explicitDropTarget)); - } - - SimulateMouseUiOverlayState.Update( - MouseAction.Drag, inputEnd, inputStart, targetName, Handles.GetMainGameViewSize()); - - bool completedDissipate = await MouseUiOverlayAnimator.PlayDissipateAnimation(ct).ConfigureAwait(false); - if (!completedDissipate) - { - _mainThreadCleanupScheduler.QueueOverlayClear(); - return MouseUiSimulationResponseFactory.CreateDragResult(parameters, inputStart, inputEnd, targetName); - } - await MainThreadSwitcher.SwitchToMainThread(ct); - - return MouseUiSimulationResponseFactory.CreateDragResult(parameters, inputStart, inputEnd, targetName); - } - private async Task ExecuteDragStart( MouseUiSimulationCommand parameters, EventSystem eventSystem, CancellationToken ct) {