libsdl-org / SDL

Simple Directmedia Layer
https://libsdl.org
zlib License
10.17k stars 1.86k forks source link

Embed a SDL window by NativeControlHost control in AvaloniaUI, mouse cursor don't restore to default on Linux x11. #10527

Closed sandsc closed 1 month ago

sandsc commented 3 months ago

Create a two sdl window wrapped by NativeControlHost splitted by a AvaloniaUI's GridSplitter control, Cursor is changed to resize when mouse hover on splitter, then move cursor back to NativeControlHost, it remains resize shape. but it restored to default(arrow) cursor on Windows platform.

Since avaloniaui doesn't handle any events for nativecontrol, I think it's most likely a bug in the SDL.

sandsc commented 3 months ago

I found a solution to fix this issue may give some help to you: First set a hit test function to embedded window control.

    SDL_SetWindowHitTest(m_hWnd, SDL_X11_HitTest, nullptr);

Since it's a embedded control, SDL_X11_HitTest always returns SDL_HITTEST_NORMAL

then modify code in SDL_x11mouse.c:

void X11_SetHitTestCursor(SDL_HitTestResult rc)
{
   X11_ShowCursor(sys_cursors[rc]);
    /*if (rc == SDL_HITTEST_NORMAL || rc == SDL_HITTEST_DRAGGABLE) {
        SDL_SetCursor(NULL);
    } else {
        X11_ShowCursor(sys_cursors[rc]);
    }*/
}

Now whenever cursor hover over embedded control, it restores to default!

slouken commented 1 month ago

Can you please provide a minimal test case so we can reproduce that here? Modifying testnative to reproduce this would be ideal.

Thanks!

sandsc commented 1 month ago

here is the minimal test case, include prebuilt SDL libraries in both windows and Linux platform, which built from latest sdl code.

src.zip

slouken commented 1 month ago

Thanks for the repro case!

This is the right way to handle this:

diff -ru src.orig/MainWindow.axaml.cs src/MainWindow.axaml.cs
--- src.orig/MainWindow.axaml.cs    2024-10-09 11:43:58.000000000 -0700
+++ src/MainWindow.axaml.cs 2024-10-08 22:28:26.568585336 -0700
@@ -7,9 +7,11 @@
 {
     private readonly NativeWindow _leftWindow;
     private readonly NativeWindow _rightWindow;
+    private readonly nint _cursor;
     public MainWindow()
     {
         InitializeComponent();
+        _cursor = SDL.SDL_CreateSystemCursor(SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_DEFAULT);
         _leftWindow = new NativeWindow();
         _rightWindow = new NativeWindow();
         Grid.SetColumn(_leftWindow, 0);
@@ -18,9 +20,22 @@
         this.main.Children.Add(_rightWindow);
     }

+    private void PollEvents()
+    {
+        SDL.SDL_Event e = new SDL.SDL_Event();
+        while (SDL.SDL_PollEvent(ref e) == 1)
+        {
+            //Console.WriteLine($"Got event: type {e.type}");
+            if (e.type == SDL.SDL_EVENT_WINDOW_MOUSE_ENTER) {
+                SDL.SDL_SetCursor(_cursor);
+            }
+        }
+    }
+
     public void Tick()
     {
+        PollEvents();
         _leftWindow.Tick();
         _rightWindow.Tick();
     }
-}
\ No newline at end of file
+}
diff -ru src.orig/NativeWindow.cs src/NativeWindow.cs
--- src.orig/NativeWindow.cs    2024-10-09 11:06:58.000000000 -0700
+++ src/NativeWindow.cs 2024-10-08 22:29:53.649806502 -0700
@@ -16,12 +16,9 @@
     internal class NativeWindow : NativeControlHost
     {
         private IntPtr _hwnd;
+        private uint _windowID;
         private IntPtr _renderer;

-        private const string libX11 = "libX11.so.6";
-        [DllImport(libX11)]
-        public static extern IntPtr XSelectInput(IntPtr display, IntPtr window, IntPtr mask);
-
         protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
         {
             IPlatformHandle platformHandle = base.CreateNativeControlCore(parent);
@@ -41,6 +38,7 @@
                 SDL.SDL_SetNumberProperty(pid, "SDL.window.create.x11.window", platformHandle.Handle.ToInt64());
             }
             _hwnd = SDL.SDL_CreateWindowWithProperties(pid);
+            _windowID = SDL.SDL_GetWindowID(_hwnd);
             SDL.SDL_DestroyProperties(pid);

             _renderer = SDL.SDL_CreateRenderer(_hwnd, "opengl", SDL.SDL_RendererFlags.SDL_RENDERER_PRESENTVSYNC);
@@ -73,15 +71,6 @@
             base.Render(context);
         }

-        public void PollEvents()
-        {
-            SDL.SDL_Event sDL_Event = new SDL.SDL_Event();
-            while (SDL.SDL_PollEvent(ref sDL_Event) == 1)
-            {
-                //Console.WriteLine($"Got event: type {sDL_Event.type}");
-            }
-        }
-
         protected PixelSize GetPixelSize(IRenderRoot? visualRoot)
         {
             double scaling = visualRoot == null ? 1.0 : visualRoot.RenderScaling;
@@ -89,9 +78,12 @@
                 Math.Max(1, (int)(Bounds.Height * scaling)));
         }

+        public uint GetWindowID() {
+            return _windowID;
+        }
+
         public void Tick()
         {
-            PollEvents();
             if (_renderer == IntPtr.Zero)
                 return;

diff -ru src.orig/SDL.cs src/SDL.cs
--- src.orig/SDL.cs 2024-10-09 11:09:16.000000000 -0700
+++ src/SDL.cs  2024-10-08 22:25:44.834851917 -0700
@@ -37,6 +37,8 @@
         public const uint SDL_WINDOW_METAL = 0x20000000U; /**< window usable for Metal view */
         public const uint SDL_WINDOW_TRANSPARENT = 0x40000000U; /**< window with transparent buffer */
         public const uint SDL_WINDOW_NOT_FOCUSABLE = 0x80000000U; /**< window should not be focusable */
+        public const uint SDL_EVENT_WINDOW_MOUSE_ENTER = 0x20c;
+        public const uint SDL_EVENT_WINDOW_MOUSE_LEAVE = 0x20d;

         [Flags]
         public enum SDL_RendererFlags : uint
@@ -44,6 +46,11 @@
             SDL_RENDERER_PRESENTVSYNC = 0x00000004,
         }

+        public enum SDL_SystemCursor : int
+        {
+            SDL_SYSTEM_CURSOR_DEFAULT = 0,
+        }
+
         [StructLayout(LayoutKind.Sequential)]
         public struct SDL_Rect
         {
@@ -78,6 +85,18 @@
         public extern static void SDL_DestroyWindow(nint window);

         [DllImport(SDL3, CallingConvention = CallingConvention.Cdecl)]
+        public extern static uint SDL_GetWindowID(nint window);
+
+        [DllImport(SDL3, CallingConvention = CallingConvention.Cdecl)]
+        public static extern nint SDL_CreateSystemCursor(SDL_SystemCursor id);
+
+        [DllImport(SDL3, CallingConvention = CallingConvention.Cdecl)]
+        public static extern bool SDL_SetCursor(IntPtr cursor);
+
+        [DllImport(SDL3, CallingConvention = CallingConvention.Cdecl)]
+        public static extern void SDL_DestroyCursor(IntPtr cursor);
+
+        [DllImport(SDL3, CallingConvention = CallingConvention.Cdecl)]
         public extern static nint SDL_GetError();

         [DllImport(SDL3, CallingConvention = CallingConvention.Cdecl)]

The extra code to get the window ID isn't necessary, but if you want to check e.window.windowID and compare it against a specific window, that will be helpful.

sandsc commented 1 month ago

Thanks for the reply, I haven't tried it yet, so it looks like I need to manually restore the default cursor on the Linux platform? because on Windows platform, it will automatically revert back to the default cursor when mouse enter.

slouken commented 1 month ago

Yes, that’s correct. On Windows the OS queries for the current cursor every time the mouse moves. On Linux the application changes the cursor when it wants it to be different. Since the Avalonia framework is changing the cursor, you need to change it back yourself.