Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drag and drop doesn't work using Headless #17331

Open
odemaru opened this issue Oct 23, 2024 · 1 comment
Open

Drag and drop doesn't work using Headless #17331

odemaru opened this issue Oct 23, 2024 · 1 comment

Comments

@odemaru
Copy link

odemaru commented Oct 23, 2024

Describe the bug

You can't emulate drag and drop using Headless

To Reproduce

public static void DragAndDrop(this Window window, Point dragStartPosition, Point dropPosition)
{
window.MouseDown(dragStartPosition, MouseButton.Left);
window.MouseMove(dropPosition);
window.MouseUp(dropPosition, MouseButton.Left);
}

Expected behavior

No response

Avalonia version

11.1.3

OS

Windows

Additional context

No response

@odemaru odemaru added the bug label Oct 23, 2024
@maxkatz6 maxkatz6 added help-wanted A contribution from the community would be most welcome. area-headless and removed help-wanted A contribution from the community would be most welcome. labels Oct 23, 2024
@jl0pd
Copy link

jl0pd commented Feb 27, 2025

Drag&drop itself is working in headless with one important problem: it doesn't understand teleporting cursor. Between MouseDown and MouseUp should be 2 MouseMoves, first 1 pixel away, second on desired destination.

example of tests
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Headless;
using Avalonia.Headless.XUnit;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Reactive;
using Xunit;

namespace HeadlessTest;

public class UnitTest1 : IAsyncLifetime
{
    [AvaloniaFact]
    public async Task InWindowDragDrop()
    {
        var win = new Window()
        {
            Width = 640,
            Height = 480,
        };

        var left = new Ellipse
        {
            Fill = Brushes.Red,
        };

        var dropComplete = new TaskCompletionSource<object?>();

        bool isPressed = false;
        left.PointerPressed += (sender, e) => { isPressed = true; };
        left.PointerReleased += (sender, e) => { isPressed = false; };
        left.PointerMoved += async (sender, e) =>
        {
            if (isPressed)
            {
                await DragDrop.DoDragDrop(e, new DataObject { }, DragDropEffects.Link);
                isPressed = false;
            }
        };

        var right = new Rectangle
        {
            Fill = Brushes.Green,
            [DragDrop.AllowDropProperty] = true,
        };

        right.AddHandler(DragDrop.DropEvent, (sender, e) =>
        {
            right.Fill = Brushes.Blue;
            dropComplete.SetResult(null);
        });
        right.AddHandler(DragDrop.DragEnterEvent, (sender, e) =>
        {
            right.Fill = Brushes.Yellow;
        });
        right.AddHandler(DragDrop.DragLeaveEvent, (sender, e) =>
        {
            right.Fill = Brushes.Green;
        });

        win.Content = new UniformGrid
        {
            Columns = 2,
            Children = { left, right }
        };

        win.Show();

        DragMouse(win, new Point(160, 240), new Point(480, 240));

        await dropComplete.Task.WaitAsync2(TimeSpan.FromSeconds(1));
    }

    [AvaloniaFact]
    public async Task BetweenWindowsDragDrop()
    {
        var left = new Ellipse
        {
            Fill = Brushes.Red,
        };

        var leftWin = new Window()
        {
            Width = 640,
            Height = 480,
            Content = left,
            Position = new PixelPoint(0, 0),
        };

        var dropComplete = new TaskCompletionSource<object?>();

        bool isPressed = false;
        left.PointerPressed += (sender, e) => { isPressed = true; };
        left.PointerReleased += (sender, e) => { isPressed = false; };
        left.PointerMoved += async (sender, e) =>
        {
            if (isPressed)
            {
                await DragDrop.DoDragDrop(e, new DataObject { }, DragDropEffects.Link);
                isPressed = false;
            }
        };
        var right = new Rectangle
        {
            Fill = Brushes.Green,
            [DragDrop.AllowDropProperty] = true,
        };
        var rightWin = new Window
        {
            Width = 640,
            Height = 480,
            Content = right,
            Position = new PixelPoint(640, 0),
        };

        right.AddHandler(DragDrop.DropEvent, (sender, e) =>
        {
            right.Fill = Brushes.Blue;
            dropComplete.SetResult(null);
        });
        right.AddHandler(DragDrop.DragEnterEvent, (sender, e) =>
        {
            right.Fill = Brushes.Yellow;
        });
        right.AddHandler(DragDrop.DragLeaveEvent, (sender, e) =>
        {
            right.Fill = Brushes.Green;
        });

        leftWin.Show();
        rightWin.Show();

        DragMouseAbsolute(new Point(320, 240), new Point(960, 240));

        await dropComplete.Task.WaitAsync2(TimeSpan.FromSeconds(1));
    }

    private static void DragMouse(Window win, Point start, Point end)
    {
        win.MouseDown(start, MouseButton.Left, RawInputModifiers.LeftMouseButton);
        win.MouseMove(start + GetDirection(start, end), RawInputModifiers.LeftMouseButton);
        win.MouseMove(end, RawInputModifiers.LeftMouseButton);
        win.MouseUp(end, MouseButton.Left, RawInputModifiers.LeftMouseButton);
    }

    private static void DragMouseAbsolute(Point start, Point end)
    {
        var startWin = WindowLocator.FindWindow(start) ?? throw new Exception("Cannot find window under 'start'");
        var endWin = WindowLocator.FindWindow(end) ?? throw new Exception("Cannot find window under 'end'");

        var direction = GetDirection(start, end);

        var localStart = start - startWin.Position.ToPoint(1);
        var localEnd = end - endWin.Position.ToPoint(1);

        startWin.MouseDown(localStart, MouseButton.Left, RawInputModifiers.LeftMouseButton);
        startWin.MouseMove(localStart + direction, RawInputModifiers.LeftMouseButton);

        endWin.MouseMove(localEnd - direction, RawInputModifiers.LeftMouseButton);
        endWin.MouseUp(localEnd, MouseButton.Left, RawInputModifiers.LeftMouseButton);
    }

    private static Point GetDirection(Point start, Point end)
    {
        var x = Math.Sign(end.X - start.X);
        var y = Math.Sign(end.Y - start.Y);

        return new Point(x, y);
    }

    public Task InitializeAsync()
    {
        // force WindowLocator to subscribe to events and start monitoring windows
        RuntimeHelpers.RunClassConstructor(typeof(WindowLocator).TypeHandle);

        return Task.CompletedTask;
    }

    public Task DisposeAsync()
    {
        return Task.CompletedTask;
    }
}

internal static class TaskExtensions
{
    // Task.WaitAsync is not available on netstandard2.0
    public static async Task<T> WaitAsync2<T>(this Task<T> task, TimeSpan timeout)
    {
        var completedTask = await Task.WhenAny(task, Task.Delay(timeout));
        if (completedTask is Task<T> t)
        {
            return await t;
        }
        else
        {
            throw new TimeoutException();
        }
    }
}

internal static class WindowLocator
{
    private static readonly HashSet<Window> s_windows = [];

    static WindowLocator()
    {
        Window.WindowOpenedEvent.Raised.Subscribe(e =>
        {
            s_windows.Add((Window)e.Item1);
        });

        Window.WindowClosedEvent.Raised.Subscribe(e =>
        {
            s_windows.Remove((Window)e.Item1);
        });
    }

    public static IEnumerable<Window> GetWindows()
    {
        return s_windows;
    }

    public static Window? FindWindow(Point point)
    {
        foreach (var win in s_windows)
        {
            var bounds = new Rect(win.Position.ToPoint(1), win.Bounds.Size);
            if (bounds.Contains(point))
            {
                return win;
            }
        }

        return null;
    }
}

Attached code contains 2 most important methods:

  1. DragMouse(Windows win, Point start, Point end) - moves pointer within single window
  2. DragMouseAbsolute(Point start, Point end) - moves pointer using display coordinates, allows dragging between non-overlapping windows

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants