mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
SDL3 (#5583)
* Start converting SDL2 backend to SDL3. Game starts, but a lot of stuff is broken. Oh well. * Fix text input SDL3 changed the API somewhat, for the better. Changes all over UI/Clyde/SDL3 layer. * Fix mouse buttons being broken * Remove records from SDL3 WSI The fact that this shaved 2-3% off Robust.Client.dll is mindboggling. Records are so bad. * Set Windows/X11 native window properties * Fix window resize events getting wrong size oops * Remove "using static" from SDL3 WSI Seriously seems to hurt IDE performance, oh well. * Apparently I never called CheckThreadApartment(). * Add STAThreadAttribute to sandbox Necessary for content start * Set window title on creation properly. * Load window icons * Fix GLFW NoTitleBar style handling Yeah this PR is supposed to be about SDL3, so what? * Implement more window creation settings in SDL3 Mostly the ones that need a lot of platform-specific stuff to work. * Make fullscreen work properly in SDL3. * File dialogs with SDL3 Removes need for swnfd. * Fix some TODOs * Fix WebView build
This commit is contained in:
committed by
GitHub
parent
e47ba0faea
commit
87a5745519
@@ -484,27 +484,27 @@ namespace Robust.Client.WebView.Cef
|
||||
public void FocusEntered()
|
||||
{
|
||||
if (_textInputActive)
|
||||
_clyde.TextInputStart();
|
||||
Owner.Root?.Window?.TextInputStart();
|
||||
}
|
||||
|
||||
public void FocusExited()
|
||||
{
|
||||
if (_textInputActive)
|
||||
_clyde.TextInputStop();
|
||||
Owner.Root?.Window?.TextInputStop();
|
||||
}
|
||||
|
||||
public void TextInputStart()
|
||||
{
|
||||
_textInputActive = true;
|
||||
if (Owner.HasKeyboardFocus())
|
||||
_clyde.TextInputStart();
|
||||
Owner.Root?.Window?.TextInputStart();
|
||||
}
|
||||
|
||||
public void TextInputStop()
|
||||
{
|
||||
_textInputActive = false;
|
||||
if (Owner.HasKeyboardFocus())
|
||||
_clyde.TextInputStop();
|
||||
Owner.Root?.Window?.TextInputStop();
|
||||
}
|
||||
|
||||
private sealed class LiveData
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace Robust.Client
|
||||
private Thread? _gameThread;
|
||||
private ISawmill _logger = default!;
|
||||
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
Start(args, new GameControllerOptions());
|
||||
|
||||
@@ -109,8 +109,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
case "glfw":
|
||||
winImpl = new GlfwWindowingImpl(this, _deps);
|
||||
break;
|
||||
case "sdl2":
|
||||
winImpl = new Sdl2WindowingImpl(this, _deps);
|
||||
case "sdl3":
|
||||
winImpl = new Sdl3WindowingImpl(this, _deps);
|
||||
break;
|
||||
default:
|
||||
_logManager.GetSawmill("clyde.win").Log(
|
||||
@@ -467,26 +467,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_windowing!.RunOnWindowThread(a);
|
||||
}
|
||||
|
||||
public void TextInputSetRect(UIBox2i rect)
|
||||
{
|
||||
DebugTools.AssertNotNull(_windowing);
|
||||
|
||||
_windowing!.TextInputSetRect(rect);
|
||||
}
|
||||
|
||||
public void TextInputStart()
|
||||
{
|
||||
DebugTools.AssertNotNull(_windowing);
|
||||
|
||||
_windowing!.TextInputStart();
|
||||
}
|
||||
|
||||
public void TextInputStop()
|
||||
{
|
||||
DebugTools.AssertNotNull(_windowing);
|
||||
|
||||
_windowing!.TextInputStop();
|
||||
}
|
||||
public IFileDialogManager? FileDialogImpl => _windowing as IFileDialogManager;
|
||||
|
||||
private abstract class WindowReg
|
||||
{
|
||||
@@ -590,6 +571,27 @@ namespace Robust.Client.Graphics.Clyde
|
||||
remove => Reg.Resized -= value;
|
||||
}
|
||||
|
||||
public void TextInputSetRect(UIBox2i rect, int cursor)
|
||||
{
|
||||
DebugTools.AssertNotNull(_clyde._windowing);
|
||||
|
||||
_clyde._windowing!.TextInputSetRect(Reg, rect, cursor);
|
||||
}
|
||||
|
||||
public void TextInputStart()
|
||||
{
|
||||
DebugTools.AssertNotNull(_clyde._windowing);
|
||||
|
||||
_clyde._windowing!.TextInputStart(Reg);
|
||||
}
|
||||
|
||||
public void TextInputStop()
|
||||
{
|
||||
DebugTools.AssertNotNull(_clyde._windowing);
|
||||
|
||||
_clyde._windowing!.TextInputStop(Reg);
|
||||
}
|
||||
|
||||
public nint? WindowsHWnd => _clyde._windowing!.WindowGetWin32Window(Reg);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ using JetBrains.Annotations;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.Map;
|
||||
@@ -284,6 +285,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
action();
|
||||
}
|
||||
|
||||
public IFileDialogManager? FileDialogImpl => null;
|
||||
|
||||
private sealed class DummyCursor : ICursor
|
||||
{
|
||||
public void Dispose()
|
||||
@@ -531,6 +534,21 @@ namespace Robust.Client.Graphics.Clyde
|
||||
public event Action<WindowDestroyedEventArgs>? Destroyed;
|
||||
public event Action<WindowResizedEventArgs>? Resized { add { } remove { } }
|
||||
|
||||
public void TextInputSetRect(UIBox2i rect, int cursor)
|
||||
{
|
||||
// Nop.
|
||||
}
|
||||
|
||||
public void TextInputStart()
|
||||
{
|
||||
// Nop.
|
||||
}
|
||||
|
||||
public void TextInputStop()
|
||||
{
|
||||
// Nop.
|
||||
}
|
||||
|
||||
public void MaximizeOnMonitor(IClydeMonitor monitor)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -96,7 +96,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private void ProcessEventChar(EventChar ev)
|
||||
{
|
||||
if (!_textInputActive)
|
||||
var windowReg = FindWindow(ev.Window);
|
||||
if (windowReg is not { TextInputActive: true })
|
||||
return;
|
||||
|
||||
_clyde.SendText(new TextEnteredEventArgs(new Rune(ev.CodePoint).ToString()));
|
||||
|
||||
@@ -468,6 +468,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
GLFW.WindowHint(WindowHintInt.AlphaBits, 8);
|
||||
GLFW.WindowHint(WindowHintInt.StencilBits, 8);
|
||||
|
||||
GLFW.WindowHint(WindowHintBool.Decorated, (parameters.Styles & OSWindowStyles.NoTitleBar) == 0);
|
||||
|
||||
var window = GLFW.CreateWindow(
|
||||
parameters.Width, parameters.Height,
|
||||
parameters.Title,
|
||||
@@ -485,23 +487,12 @@ namespace Robust.Client.Graphics.Clyde
|
||||
GLFW.MaximizeWindow(window);
|
||||
}
|
||||
|
||||
if ((parameters.Styles & OSWindowStyles.NoTitleBar) != 0)
|
||||
{
|
||||
GLFW.WindowHint(WindowHintBool.Decorated, false);
|
||||
}
|
||||
|
||||
if ((parameters.Styles & OSWindowStyles.NoTitleOptions) != 0)
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
var hWnd = (HWND) GLFW.GetWin32Window(window);
|
||||
DebugTools.Assert(hWnd != HWND.NULL);
|
||||
|
||||
Windows.SetWindowLongPtrW(
|
||||
hWnd,
|
||||
GWL.GWL_STYLE,
|
||||
// Cast to long here to work around a bug in rider with nint bitwise operators.
|
||||
(nint)((long)Windows.GetWindowLongPtrW(hWnd, GWL.GWL_STYLE) & ~WS.WS_SYSMENU));
|
||||
WsiShared.SetWindowStyleNoTitleOptionsWindows(hWnd);
|
||||
}
|
||||
else if (OperatingSystem.IsLinux())
|
||||
{
|
||||
@@ -509,23 +500,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
var x11Window = (X11Window)GLFW.GetX11Window(window);
|
||||
var x11Display = (Display*) GLFW.GetX11Display(window);
|
||||
DebugTools.Assert(x11Window != X11Window.NULL);
|
||||
// https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html#idm46181547486832
|
||||
var newPropValString = Marshal.StringToCoTaskMemUTF8("_NET_WM_WINDOW_TYPE_DIALOG");
|
||||
var newPropVal = Xlib.XInternAtom(x11Display, (sbyte*)newPropValString, Xlib.False);
|
||||
DebugTools.Assert(newPropVal != Atom.NULL);
|
||||
|
||||
var propNameString = Marshal.StringToCoTaskMemUTF8("_NET_WM_WINDOW_TYPE");
|
||||
#pragma warning disable CA1806
|
||||
// [display] [window] [property] [type] [format (8, 16,32)] [mode] [data] [element count]
|
||||
Xlib.XChangeProperty(x11Display, x11Window,
|
||||
Xlib.XInternAtom(x11Display, (sbyte*)propNameString, Xlib.False), // should never be null; part of spec
|
||||
Xlib.XA_ATOM, 32, Xlib.PropModeReplace,
|
||||
(byte*)&newPropVal, 1);
|
||||
#pragma warning restore CA1806
|
||||
|
||||
Marshal.FreeCoTaskMem(newPropValString);
|
||||
Marshal.FreeCoTaskMem(propNameString);
|
||||
WsiShared.SetWindowStyleNoTitleOptionsX11(x11Display, x11Window);
|
||||
}
|
||||
catch (EntryPointNotFoundException)
|
||||
{
|
||||
@@ -643,16 +618,16 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return reg;
|
||||
}
|
||||
|
||||
private WindowReg? FindWindow(nint window) => FindWindow((Window*) window);
|
||||
private GlfwWindowReg? FindWindow(nint window) => FindWindow((Window*) window);
|
||||
|
||||
private WindowReg? FindWindow(Window* window)
|
||||
private GlfwWindowReg? FindWindow(Window* window)
|
||||
{
|
||||
foreach (var windowReg in _clyde._windows)
|
||||
{
|
||||
var glfwReg = (GlfwWindowReg) windowReg;
|
||||
if (glfwReg.GlfwWindow == window)
|
||||
{
|
||||
return windowReg;
|
||||
return glfwReg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -739,23 +714,23 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return (void*) GLFW.GetProcAddress(procName);
|
||||
}
|
||||
|
||||
public void TextInputSetRect(UIBox2i rect)
|
||||
public void TextInputSetRect(WindowReg reg, UIBox2i rect, int cursor)
|
||||
{
|
||||
// Not supported on GLFW.
|
||||
}
|
||||
|
||||
public void TextInputStart()
|
||||
public void TextInputStart(WindowReg reg)
|
||||
{
|
||||
// Not properly supported on GLFW.
|
||||
|
||||
_textInputActive = true;
|
||||
((GlfwWindowReg)reg).TextInputActive = true;
|
||||
}
|
||||
|
||||
public void TextInputStop()
|
||||
public void TextInputStop(WindowReg reg)
|
||||
{
|
||||
// Not properly supported on GLFW.
|
||||
|
||||
_textInputActive = false;
|
||||
((GlfwWindowReg)reg).TextInputActive = false;
|
||||
}
|
||||
|
||||
private void CheckWindowDisposed(WindowReg reg)
|
||||
@@ -770,6 +745,10 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
// Kept around to avoid it being GCd.
|
||||
public CursorImpl? Cursor;
|
||||
|
||||
// While GLFW does not provide proper IME APIs, we can at least emulate SDL3's StartTextInput() system.
|
||||
// This will ensure some level of consistency between the backends.
|
||||
public bool TextInputActive;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private bool _glfwInitialized;
|
||||
private bool _win32Experience;
|
||||
|
||||
// While GLFW does not provide proper IME APIs, we can at least emulate SDL2's StartTextInput() system.
|
||||
// This will ensure some level of consistency between the backends.
|
||||
private bool _textInputActive;
|
||||
|
||||
public GlfwWindowingImpl(Clyde clyde, IDependencyCollection deps)
|
||||
{
|
||||
_clyde = clyde;
|
||||
|
||||
@@ -66,9 +66,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
void RunOnWindowThread(Action a);
|
||||
|
||||
// IME
|
||||
void TextInputSetRect(UIBox2i rect);
|
||||
void TextInputStart();
|
||||
void TextInputStop();
|
||||
void TextInputSetRect(WindowReg reg, UIBox2i rect, int cursor);
|
||||
void TextInputStart(WindowReg reg);
|
||||
void TextInputStop(WindowReg reg);
|
||||
string GetDescription();
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
25
Robust.Client/Graphics/Clyde/Windowing/SDL3-CS/LICENSE
Normal file
25
Robust.Client/Graphics/Clyde/Windowing/SDL3-CS/LICENSE
Normal file
@@ -0,0 +1,25 @@
|
||||
/* SDL3-CS - C# Bindings for SDL3
|
||||
*
|
||||
* Copyright (c) 2024 Colin Jackson
|
||||
*
|
||||
* This software is provided 'as-is', without any express or implied warranty.
|
||||
* In no event will the authors be held liable for any damages arising from
|
||||
* the use of this software.
|
||||
*
|
||||
* Permission is granted to anyone to use this software for any purpose,
|
||||
* including commercial applications, and to alter it and redistribute it
|
||||
* freely, subject to the following restrictions:
|
||||
*
|
||||
* 1. The origin of this software must not be misrepresented; you must not
|
||||
* claim that you wrote the original software. If you use this software in a
|
||||
* product, an acknowledgment in the product documentation would be
|
||||
* appreciated but is not required.
|
||||
*
|
||||
* 2. Altered source versions must be plainly marked as such, and must not be
|
||||
* misrepresented as being the original software.
|
||||
*
|
||||
* 3. This notice may not be removed or altered from any source distribution.
|
||||
*
|
||||
* Colin "cryy22" Jackson <c@cryy22.art>
|
||||
*
|
||||
*/
|
||||
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SDL3;
|
||||
|
||||
public static partial class SDL
|
||||
{
|
||||
// Extensions to SDL3-CS that aren't part of the main library.
|
||||
|
||||
[LibraryImport(nativeLibName)]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
public static unsafe partial void SDL_SetLogOutputFunction(delegate* unmanaged[Cdecl] <void*, int, SDL_LogPriority, byte*, void> callback, void* userdata);
|
||||
|
||||
[LibraryImport(nativeLibName)]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
public static unsafe partial SDLBool SDL_AddEventWatch(delegate* unmanaged[Cdecl] <void*, SDL_Event*, byte> filter, void* userdata);
|
||||
|
||||
[LibraryImport(nativeLibName)]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
public static unsafe partial void SDL_RemoveEventWatch(delegate* unmanaged[Cdecl] <void*, SDL_Event*, byte> filter, void* userdata);
|
||||
|
||||
[LibraryImport(nativeLibName, StringMarshalling = StringMarshalling.Utf8)]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
public static unsafe partial void SDL_ShowFileDialogWithProperties(int type, delegate* unmanaged[Cdecl]<void*, byte**, int, void> callback, void* userdata, uint properties);
|
||||
|
||||
public const byte SDL_BUTTON_LEFT = 1;
|
||||
public const byte SDL_BUTTON_MIDDLE = 2;
|
||||
public const byte SDL_BUTTON_RIGHT = 3;
|
||||
public const byte SDL_BUTTON_X1 = 4;
|
||||
public const byte SDL_BUTTON_X2 = 5;
|
||||
|
||||
public const int SDL_GL_CONTEXT_PROFILE_CORE = 0x0001;
|
||||
public const int SDL_GL_CONTEXT_PROFILE_COMPATIBILITY = 0x0002;
|
||||
public const int SDL_GL_CONTEXT_PROFILE_ES = 0x0004;
|
||||
|
||||
public const int SDL_GL_CONTEXT_DEBUG_FLAG = 0x0001;
|
||||
public const int SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG = 0x0002;
|
||||
public const int SDL_GL_CONTEXT_ROBUST_ACCESS_FLAG = 0x0004;
|
||||
public const int SDL_GL_CONTEXT_RESET_ISOLATION_FLAG = 0x0008;
|
||||
|
||||
public const int SDL_FILEDIALOG_OPENFILE = 0;
|
||||
public const int SDL_FILEDIALOG_SAVEFILE = 1;
|
||||
public const int SDL_FILEDIALOG_OPENFOLDER = 2;
|
||||
|
||||
public const string SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER = "SDL.filedialog.nfilters";
|
||||
public const string SDL_PROP_FILE_DIALOG_FILTERS_POINTER = "SDL.filedialog.filters";
|
||||
|
||||
public static int SDL_VERSIONNUM_MAJOR(int version) => version / 1000000;
|
||||
public static int SDL_VERSIONNUM_MINOR(int version) => version / 1000 % 1000;
|
||||
public static int SDL_VERSIONNUM_MICRO(int version) => version % 1000;
|
||||
}
|
||||
8043
Robust.Client/Graphics/Clyde/Windowing/SDL3-CS/SDL3.Core.cs
Normal file
8043
Robust.Client/Graphics/Clyde/Windowing/SDL3-CS/SDL3.Core.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,224 +0,0 @@
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Shared;
|
||||
using static SDL2.SDL;
|
||||
using static SDL2.SDL.SDL_Scancode;
|
||||
using Key = Robust.Client.Input.Keyboard.Key;
|
||||
using Button = Robust.Client.Input.Mouse.Button;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl2WindowingImpl
|
||||
{
|
||||
// Indices are values of SDL_Scancode
|
||||
private static readonly Key[] KeyMap;
|
||||
private static readonly FrozenDictionary<Key, SDL_Scancode> KeyMapReverse;
|
||||
private static readonly Button[] MouseButtonMap;
|
||||
|
||||
// TODO: to avoid having to ask the windowing thread, key names are cached.
|
||||
private readonly Dictionary<Key, string> _printableKeyNameMap = new();
|
||||
|
||||
private void ReloadKeyMap()
|
||||
{
|
||||
// This may be ran concurrently from the windowing thread.
|
||||
lock (_printableKeyNameMap)
|
||||
{
|
||||
_printableKeyNameMap.Clear();
|
||||
|
||||
// List of mappable keys from SDL2's source appears to be:
|
||||
// entries in SDL_default_keymap that aren't an SDLK_ enum reference.
|
||||
// (the actual logic is more nuanced, but it appears to match the above)
|
||||
// Comes out to these two ranges:
|
||||
|
||||
for (var k = SDL_SCANCODE_A; k <= SDL_SCANCODE_0; k++)
|
||||
{
|
||||
CacheKey(k);
|
||||
}
|
||||
|
||||
for (var k = SDL_SCANCODE_MINUS; k <= SDL_SCANCODE_SLASH; k++)
|
||||
{
|
||||
CacheKey(k);
|
||||
}
|
||||
|
||||
void CacheKey(SDL_Scancode scancode)
|
||||
{
|
||||
var rKey = ConvertSdl2Scancode(scancode);
|
||||
if (rKey == Key.Unknown)
|
||||
return;
|
||||
|
||||
var name = SDL_GetKeyName(SDL_GetKeyFromScancode(scancode));
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
_printableKeyNameMap.Add(rKey, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string? KeyGetName(Key key)
|
||||
{
|
||||
lock (_printableKeyNameMap)
|
||||
{
|
||||
if (_printableKeyNameMap.TryGetValue(key, out var name))
|
||||
return name;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
internal static Key ConvertSdl2Scancode(SDL_Scancode scancode)
|
||||
{
|
||||
return KeyMap[(int) scancode];
|
||||
}
|
||||
|
||||
public static Button ConvertSdl2Button(int button)
|
||||
{
|
||||
return MouseButtonMap[button];
|
||||
}
|
||||
|
||||
static Sdl2WindowingImpl()
|
||||
{
|
||||
MouseButtonMap = new Button[6];
|
||||
MouseButtonMap[SDL_BUTTON_LEFT] = Button.Left;
|
||||
MouseButtonMap[SDL_BUTTON_RIGHT] = Button.Right;
|
||||
MouseButtonMap[SDL_BUTTON_MIDDLE] = Button.Middle;
|
||||
MouseButtonMap[SDL_BUTTON_X1] = Button.Button4;
|
||||
MouseButtonMap[SDL_BUTTON_X2] = Button.Button5;
|
||||
|
||||
KeyMap = new Key[(int) SDL_NUM_SCANCODES];
|
||||
MapKey(SDL_SCANCODE_A, Key.A);
|
||||
MapKey(SDL_SCANCODE_B, Key.B);
|
||||
MapKey(SDL_SCANCODE_C, Key.C);
|
||||
MapKey(SDL_SCANCODE_D, Key.D);
|
||||
MapKey(SDL_SCANCODE_E, Key.E);
|
||||
MapKey(SDL_SCANCODE_F, Key.F);
|
||||
MapKey(SDL_SCANCODE_G, Key.G);
|
||||
MapKey(SDL_SCANCODE_H, Key.H);
|
||||
MapKey(SDL_SCANCODE_I, Key.I);
|
||||
MapKey(SDL_SCANCODE_J, Key.J);
|
||||
MapKey(SDL_SCANCODE_K, Key.K);
|
||||
MapKey(SDL_SCANCODE_L, Key.L);
|
||||
MapKey(SDL_SCANCODE_M, Key.M);
|
||||
MapKey(SDL_SCANCODE_N, Key.N);
|
||||
MapKey(SDL_SCANCODE_O, Key.O);
|
||||
MapKey(SDL_SCANCODE_P, Key.P);
|
||||
MapKey(SDL_SCANCODE_Q, Key.Q);
|
||||
MapKey(SDL_SCANCODE_R, Key.R);
|
||||
MapKey(SDL_SCANCODE_S, Key.S);
|
||||
MapKey(SDL_SCANCODE_T, Key.T);
|
||||
MapKey(SDL_SCANCODE_U, Key.U);
|
||||
MapKey(SDL_SCANCODE_V, Key.V);
|
||||
MapKey(SDL_SCANCODE_W, Key.W);
|
||||
MapKey(SDL_SCANCODE_X, Key.X);
|
||||
MapKey(SDL_SCANCODE_Y, Key.Y);
|
||||
MapKey(SDL_SCANCODE_Z, Key.Z);
|
||||
MapKey(SDL_SCANCODE_0, Key.Num0);
|
||||
MapKey(SDL_SCANCODE_1, Key.Num1);
|
||||
MapKey(SDL_SCANCODE_2, Key.Num2);
|
||||
MapKey(SDL_SCANCODE_3, Key.Num3);
|
||||
MapKey(SDL_SCANCODE_4, Key.Num4);
|
||||
MapKey(SDL_SCANCODE_5, Key.Num5);
|
||||
MapKey(SDL_SCANCODE_6, Key.Num6);
|
||||
MapKey(SDL_SCANCODE_7, Key.Num7);
|
||||
MapKey(SDL_SCANCODE_8, Key.Num8);
|
||||
MapKey(SDL_SCANCODE_9, Key.Num9);
|
||||
MapKey(SDL_SCANCODE_KP_0, Key.NumpadNum0);
|
||||
MapKey(SDL_SCANCODE_KP_1, Key.NumpadNum1);
|
||||
MapKey(SDL_SCANCODE_KP_2, Key.NumpadNum2);
|
||||
MapKey(SDL_SCANCODE_KP_3, Key.NumpadNum3);
|
||||
MapKey(SDL_SCANCODE_KP_4, Key.NumpadNum4);
|
||||
MapKey(SDL_SCANCODE_KP_5, Key.NumpadNum5);
|
||||
MapKey(SDL_SCANCODE_KP_6, Key.NumpadNum6);
|
||||
MapKey(SDL_SCANCODE_KP_7, Key.NumpadNum7);
|
||||
MapKey(SDL_SCANCODE_KP_8, Key.NumpadNum8);
|
||||
MapKey(SDL_SCANCODE_KP_9, Key.NumpadNum9);
|
||||
MapKey(SDL_SCANCODE_ESCAPE, Key.Escape);
|
||||
MapKey(SDL_SCANCODE_LCTRL, Key.Control);
|
||||
MapKey(SDL_SCANCODE_RCTRL, Key.Control);
|
||||
MapKey(SDL_SCANCODE_RSHIFT, Key.Shift);
|
||||
MapKey(SDL_SCANCODE_LSHIFT, Key.Shift);
|
||||
MapKey(SDL_SCANCODE_LALT, Key.Alt);
|
||||
MapKey(SDL_SCANCODE_RALT, Key.Alt);
|
||||
MapKey(SDL_SCANCODE_LGUI, Key.LSystem);
|
||||
MapKey(SDL_SCANCODE_RGUI, Key.RSystem);
|
||||
MapKey(SDL_SCANCODE_MENU, Key.Menu);
|
||||
MapKey(SDL_SCANCODE_LEFTBRACKET, Key.LBracket);
|
||||
MapKey(SDL_SCANCODE_RIGHTBRACKET, Key.RBracket);
|
||||
MapKey(SDL_SCANCODE_SEMICOLON, Key.SemiColon);
|
||||
MapKey(SDL_SCANCODE_COMMA, Key.Comma);
|
||||
MapKey(SDL_SCANCODE_PERIOD, Key.Period);
|
||||
MapKey(SDL_SCANCODE_APOSTROPHE, Key.Apostrophe);
|
||||
MapKey(SDL_SCANCODE_SLASH, Key.Slash);
|
||||
MapKey(SDL_SCANCODE_BACKSLASH, Key.BackSlash);
|
||||
MapKey(SDL_SCANCODE_GRAVE, Key.Tilde);
|
||||
MapKey(SDL_SCANCODE_EQUALS, Key.Equal);
|
||||
MapKey(SDL_SCANCODE_SPACE, Key.Space);
|
||||
MapKey(SDL_SCANCODE_RETURN, Key.Return);
|
||||
MapKey(SDL_SCANCODE_KP_ENTER, Key.NumpadEnter);
|
||||
MapKey(SDL_SCANCODE_BACKSPACE, Key.BackSpace);
|
||||
MapKey(SDL_SCANCODE_TAB, Key.Tab);
|
||||
MapKey(SDL_SCANCODE_PAGEUP, Key.PageUp);
|
||||
MapKey(SDL_SCANCODE_PAGEDOWN, Key.PageDown);
|
||||
MapKey(SDL_SCANCODE_END, Key.End);
|
||||
MapKey(SDL_SCANCODE_HOME, Key.Home);
|
||||
MapKey(SDL_SCANCODE_INSERT, Key.Insert);
|
||||
MapKey(SDL_SCANCODE_DELETE, Key.Delete);
|
||||
MapKey(SDL_SCANCODE_MINUS, Key.Minus);
|
||||
MapKey(SDL_SCANCODE_KP_PLUS, Key.NumpadAdd);
|
||||
MapKey(SDL_SCANCODE_KP_MINUS, Key.NumpadSubtract);
|
||||
MapKey(SDL_SCANCODE_KP_DIVIDE, Key.NumpadDivide);
|
||||
MapKey(SDL_SCANCODE_KP_MULTIPLY, Key.NumpadMultiply);
|
||||
MapKey(SDL_SCANCODE_KP_DECIMAL, Key.NumpadDecimal);
|
||||
MapKey(SDL_SCANCODE_LEFT, Key.Left);
|
||||
MapKey(SDL_SCANCODE_RIGHT, Key.Right);
|
||||
MapKey(SDL_SCANCODE_UP, Key.Up);
|
||||
MapKey(SDL_SCANCODE_DOWN, Key.Down);
|
||||
MapKey(SDL_SCANCODE_F1, Key.F1);
|
||||
MapKey(SDL_SCANCODE_F2, Key.F2);
|
||||
MapKey(SDL_SCANCODE_F3, Key.F3);
|
||||
MapKey(SDL_SCANCODE_F4, Key.F4);
|
||||
MapKey(SDL_SCANCODE_F5, Key.F5);
|
||||
MapKey(SDL_SCANCODE_F6, Key.F6);
|
||||
MapKey(SDL_SCANCODE_F7, Key.F7);
|
||||
MapKey(SDL_SCANCODE_F8, Key.F8);
|
||||
MapKey(SDL_SCANCODE_F9, Key.F9);
|
||||
MapKey(SDL_SCANCODE_F10, Key.F10);
|
||||
MapKey(SDL_SCANCODE_F11, Key.F11);
|
||||
MapKey(SDL_SCANCODE_F12, Key.F12);
|
||||
MapKey(SDL_SCANCODE_F13, Key.F13);
|
||||
MapKey(SDL_SCANCODE_F14, Key.F14);
|
||||
MapKey(SDL_SCANCODE_F15, Key.F15);
|
||||
MapKey(SDL_SCANCODE_F16, Key.F16);
|
||||
MapKey(SDL_SCANCODE_F17, Key.F17);
|
||||
MapKey(SDL_SCANCODE_F18, Key.F18);
|
||||
MapKey(SDL_SCANCODE_F19, Key.F19);
|
||||
MapKey(SDL_SCANCODE_F20, Key.F20);
|
||||
MapKey(SDL_SCANCODE_F21, Key.F21);
|
||||
MapKey(SDL_SCANCODE_F22, Key.F22);
|
||||
MapKey(SDL_SCANCODE_F23, Key.F23);
|
||||
MapKey(SDL_SCANCODE_F24, Key.F24);
|
||||
MapKey(SDL_SCANCODE_PAUSE, Key.Pause);
|
||||
|
||||
var keyMapReverse = new Dictionary<Key, SDL_Scancode>();
|
||||
|
||||
for (var code = 0; code < KeyMap.Length; code++)
|
||||
{
|
||||
var key = KeyMap[code];
|
||||
if (key != Key.Unknown)
|
||||
keyMapReverse[key] = (SDL_Scancode) code;
|
||||
}
|
||||
|
||||
KeyMapReverse = keyMapReverse.ToFrozenDictionary();
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static void MapKey(SDL_Scancode code, Key key)
|
||||
{
|
||||
KeyMap[(int)code] = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using SDL2;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl2WindowingImpl
|
||||
{
|
||||
// NOTE: SDL2 calls them "displays". GLFW calls them monitors. GLFW's is the one I'm going with.
|
||||
|
||||
// Can't use ClydeHandle because it's not thread safe to allocate.
|
||||
private int _nextMonitorId = 1;
|
||||
|
||||
private readonly Dictionary<int, WinThreadMonitorReg> _winThreadMonitors = new();
|
||||
private readonly Dictionary<int, Sdl2MonitorReg> _monitors = new();
|
||||
|
||||
private void InitMonitors()
|
||||
{
|
||||
var numDisplays = SDL.SDL_GetNumVideoDisplays();
|
||||
for (var i = 0; i < numDisplays; i++)
|
||||
{
|
||||
// SDL.SDL_GetDisplayDPI(i, out var ddpi, out var hdpi, out var vdpi);
|
||||
// _sawmill.Info($"[{i}] {ddpi} {hdpi} {vdpi}");
|
||||
WinThreadSetupMonitor(i);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void WinThreadSetupMonitor(int displayIdx)
|
||||
{
|
||||
var id = _nextMonitorId++;
|
||||
|
||||
var name = SDL.SDL_GetDisplayName(displayIdx);
|
||||
var modeCount = SDL.SDL_GetNumDisplayModes(displayIdx);
|
||||
SDL.SDL_GetCurrentDisplayMode(displayIdx, out var curMode);
|
||||
var modes = new VideoMode[modeCount];
|
||||
for (var i = 0; i < modes.Length; i++)
|
||||
{
|
||||
SDL.SDL_GetDisplayMode(displayIdx, i, out var mode);
|
||||
modes[i] = ConvertVideoMode(mode);
|
||||
}
|
||||
|
||||
_winThreadMonitors.Add(id, new WinThreadMonitorReg { Id = id, DisplayIdx = displayIdx });
|
||||
|
||||
SendEvent(new EventMonitorSetup(id, name, ConvertVideoMode(curMode), modes));
|
||||
|
||||
if (displayIdx == 0)
|
||||
_clyde._primaryMonitorId = id;
|
||||
}
|
||||
|
||||
private static VideoMode ConvertVideoMode(in SDL.SDL_DisplayMode mode)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Width = (ushort)mode.w,
|
||||
Height = (ushort)mode.h,
|
||||
RefreshRate = (ushort)mode.refresh_rate,
|
||||
// TODO: set bits count based on format (I'm lazy)
|
||||
RedBits = 8,
|
||||
GreenBits = 8,
|
||||
BlueBits = 8,
|
||||
};
|
||||
}
|
||||
|
||||
private void ProcessSetupMonitor(EventMonitorSetup ev)
|
||||
{
|
||||
var impl = new MonitorHandle(
|
||||
ev.Id,
|
||||
ev.Name,
|
||||
(ev.CurrentMode.Width, ev.CurrentMode.Height),
|
||||
ev.CurrentMode.RefreshRate,
|
||||
ev.AllModes);
|
||||
|
||||
_clyde._monitorHandles.Add(ev.Id, impl);
|
||||
_monitors[ev.Id] = new Sdl2MonitorReg
|
||||
{
|
||||
Id = ev.Id,
|
||||
Handle = impl
|
||||
};
|
||||
}
|
||||
|
||||
private void WinThreadDestroyMonitor(int displayIdx)
|
||||
{
|
||||
var monitorId = 0;
|
||||
|
||||
foreach (var (id, monitorReg) in _winThreadMonitors)
|
||||
{
|
||||
if (monitorReg.DisplayIdx == displayIdx)
|
||||
{
|
||||
monitorId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (monitorId == 0)
|
||||
return;
|
||||
|
||||
// So SDL2 doesn't have a very nice indexing system for monitors like GLFW does.
|
||||
// This means that, when a monitor is disconnected, all monitors *after* it get shifted down one slot.
|
||||
// Now, this happens *after* the event is fired, to make matters worse.
|
||||
// So we're basically trying to match unspecified SDL2 internals here. Great.
|
||||
|
||||
_winThreadMonitors.Remove(monitorId);
|
||||
|
||||
foreach (var (_, reg) in _winThreadMonitors)
|
||||
{
|
||||
if (reg.DisplayIdx > displayIdx)
|
||||
reg.DisplayIdx -= 1;
|
||||
}
|
||||
|
||||
SendEvent(new EventMonitorDestroy(monitorId));
|
||||
}
|
||||
|
||||
private void ProcessEventDestroyMonitor(EventMonitorDestroy ev)
|
||||
{
|
||||
_monitors.Remove(ev.Id);
|
||||
_clyde._monitorHandles.Remove(ev.Id);
|
||||
}
|
||||
|
||||
private sealed class Sdl2MonitorReg : MonitorReg
|
||||
{
|
||||
public int Id;
|
||||
}
|
||||
|
||||
private sealed class WinThreadMonitorReg
|
||||
{
|
||||
public int Id;
|
||||
public int DisplayIdx;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,316 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared.Maths;
|
||||
using TerraFX.Interop.Windows;
|
||||
using static SDL2.SDL;
|
||||
using static SDL2.SDL.SDL_EventType;
|
||||
using static SDL2.SDL.SDL_SYSWM_TYPE;
|
||||
using static SDL2.SDL.SDL_WindowEventID;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl2WindowingImpl
|
||||
{
|
||||
[UnmanagedCallersOnly(CallConvs = new []{typeof(CallConvCdecl)})]
|
||||
private static unsafe int EventWatch(void* userdata, SDL_Event* sdlevent)
|
||||
{
|
||||
var obj = (Sdl2WindowingImpl) GCHandle.FromIntPtr((IntPtr)userdata).Target!;
|
||||
ref readonly var ev = ref *sdlevent;
|
||||
|
||||
obj.ProcessSdl2Event(in ev);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void ProcessSdl2Event(in SDL_Event ev)
|
||||
{
|
||||
switch (ev.type)
|
||||
{
|
||||
case SDL_WINDOWEVENT:
|
||||
ProcessSdl2EventWindow(in ev.window);
|
||||
break;
|
||||
case SDL_KEYDOWN:
|
||||
case SDL_KEYUP:
|
||||
ProcessSdl2KeyEvent(in ev.key);
|
||||
break;
|
||||
case SDL_TEXTINPUT:
|
||||
ProcessSdl2EventTextInput(in ev.text);
|
||||
break;
|
||||
case SDL_TEXTEDITING:
|
||||
ProcessSdl2EventTextEditing(in ev.edit);
|
||||
break;
|
||||
case SDL_KEYMAPCHANGED:
|
||||
ProcessSdl2EventKeyMapChanged();
|
||||
break;
|
||||
case SDL_TEXTEDITING_EXT:
|
||||
ProcessSdl2EventTextEditingExt(in ev.editExt);
|
||||
break;
|
||||
case SDL_MOUSEMOTION:
|
||||
ProcessSdl2EventMouseMotion(in ev.motion);
|
||||
break;
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
case SDL_MOUSEBUTTONUP:
|
||||
ProcessSdl2EventMouseButton(in ev.button);
|
||||
break;
|
||||
case SDL_MOUSEWHEEL:
|
||||
ProcessSdl2EventMouseWheel(in ev.wheel);
|
||||
break;
|
||||
case SDL_DISPLAYEVENT:
|
||||
ProcessSdl2EventDisplay(in ev.display);
|
||||
break;
|
||||
case SDL_SYSWMEVENT:
|
||||
ProcessSdl2EventSysWM(in ev.syswm);
|
||||
break;
|
||||
case SDL_QUIT:
|
||||
ProcessSdl2EventQuit();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessSdl2EventQuit()
|
||||
{
|
||||
SendEvent(new EventQuit());
|
||||
}
|
||||
|
||||
private void ProcessSdl2EventDisplay(in SDL_DisplayEvent evDisplay)
|
||||
{
|
||||
switch (evDisplay.displayEvent)
|
||||
{
|
||||
case SDL_DisplayEventID.SDL_DISPLAYEVENT_CONNECTED:
|
||||
WinThreadSetupMonitor((int) evDisplay.display);
|
||||
break;
|
||||
case SDL_DisplayEventID.SDL_DISPLAYEVENT_DISCONNECTED:
|
||||
WinThreadDestroyMonitor((int) evDisplay.display);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessSdl2EventMouseWheel(in SDL_MouseWheelEvent ev)
|
||||
{
|
||||
SendEvent(new EventWheel(ev.windowID, ev.preciseX, ev.preciseY));
|
||||
}
|
||||
|
||||
private void ProcessSdl2EventMouseButton(in SDL_MouseButtonEvent ev)
|
||||
{
|
||||
SendEvent(new EventMouseButton(ev.windowID, ev.type, ev.button));
|
||||
}
|
||||
|
||||
private void ProcessSdl2EventMouseMotion(in SDL_MouseMotionEvent ev)
|
||||
{
|
||||
// _sawmill.Info($"{evMotion.x}, {evMotion.y}, {evMotion.xrel}, {evMotion.yrel}");
|
||||
SendEvent(new EventMouseMotion(ev.windowID, ev.x, ev.y, ev.xrel, ev.yrel));
|
||||
}
|
||||
|
||||
private unsafe void ProcessSdl2EventTextInput(in SDL_TextInputEvent ev)
|
||||
{
|
||||
fixed (byte* text = ev.text)
|
||||
{
|
||||
var str = Marshal.PtrToStringUTF8((IntPtr)text) ?? "";
|
||||
// _logManager.GetSawmill("ime").Debug($"Input: {str}");
|
||||
SendEvent(new EventText(ev.windowID, str));
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void ProcessSdl2EventTextEditing(in SDL_TextEditingEvent ev)
|
||||
{
|
||||
fixed (byte* text = ev.text)
|
||||
{
|
||||
SendTextEditing(ev.windowID, text, ev.start, ev.length);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void ProcessSdl2EventTextEditingExt(in SDL_TextEditingExtEvent ev)
|
||||
{
|
||||
SendTextEditing(ev.windowID, (byte*) ev.text, ev.start, ev.length);
|
||||
SDL_free(ev.text);
|
||||
}
|
||||
|
||||
private unsafe void SendTextEditing(uint window, byte* text, int start, int length)
|
||||
{
|
||||
var str = Marshal.PtrToStringUTF8((nint) text) ?? "";
|
||||
// _logManager.GetSawmill("ime").Debug($"Editing: '{str}', start: {start}, len: {length}");
|
||||
SendEvent(new EventTextEditing(window, str, start, length));
|
||||
}
|
||||
|
||||
private void ProcessSdl2EventKeyMapChanged()
|
||||
{
|
||||
ReloadKeyMap();
|
||||
SendEvent(new EventKeyMapChanged());
|
||||
}
|
||||
|
||||
private void ProcessSdl2KeyEvent(in SDL_KeyboardEvent ev)
|
||||
{
|
||||
SendEvent(new EventKey(
|
||||
ev.windowID,
|
||||
ev.keysym.scancode,
|
||||
ev.type,
|
||||
ev.repeat != 0,
|
||||
ev.keysym.mod));
|
||||
}
|
||||
|
||||
private void ProcessSdl2EventWindow(in SDL_WindowEvent ev)
|
||||
{
|
||||
var window = SDL_GetWindowFromID(ev.windowID);
|
||||
|
||||
switch (ev.windowEvent)
|
||||
{
|
||||
case SDL_WINDOWEVENT_SIZE_CHANGED:
|
||||
var width = ev.data1;
|
||||
var height = ev.data2;
|
||||
SDL_GetWindowSizeInPixels(window, out var fbW, out var fbH);
|
||||
var (xScale, yScale) = GetWindowScale(window);
|
||||
|
||||
_sawmill.Debug($"{width}x{height}, {fbW}x{fbH}, {xScale}x{yScale}");
|
||||
|
||||
SendEvent(new EventWindowSize(ev.windowID, width, height, fbW, fbH, xScale, yScale));
|
||||
break;
|
||||
|
||||
default:
|
||||
SendEvent(new EventWindow(ev.windowID, ev.windowEvent));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private unsafe void ProcessSdl2EventSysWM(in SDL_SysWMEvent ev)
|
||||
{
|
||||
ref readonly var sysWmMessage = ref *(SDL_SysWMmsg*)ev.msg;
|
||||
if (sysWmMessage.subsystem != SDL_SYSWM_WINDOWS)
|
||||
return;
|
||||
|
||||
ref readonly var winMessage = ref *(SDL_SysWMmsgWin32*)ev.msg;
|
||||
if (winMessage.msg is WM.WM_KEYDOWN or WM.WM_KEYUP)
|
||||
{
|
||||
TryWin32VirtualVKey(in winMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private void TryWin32VirtualVKey(in SDL_SysWMmsgWin32 msg)
|
||||
{
|
||||
// Workaround for https://github.com/ocornut/imgui/issues/2977
|
||||
// This is gonna bite me in the ass if SDL2 ever fixes this upstream, isn't it...
|
||||
// (I spent disproportionate amounts of effort on this).
|
||||
|
||||
// Code for V key.
|
||||
if ((int)msg.wParam is not (0x56 or VK.VK_CONTROL))
|
||||
return;
|
||||
|
||||
var scanCode = (msg.lParam >> 16) & 0xFF;
|
||||
if (scanCode != 0)
|
||||
return;
|
||||
|
||||
SendEvent(new EventWindowsFakeV(msg.hwnd, msg.msg, msg.wParam));
|
||||
}
|
||||
|
||||
private abstract record EventBase;
|
||||
|
||||
private record EventWindowCreate(
|
||||
Sdl2WindowCreateResult Result,
|
||||
TaskCompletionSource<Sdl2WindowCreateResult> Tcs
|
||||
) : EventBase;
|
||||
|
||||
private record EventKey(
|
||||
uint WindowId,
|
||||
SDL_Scancode Scancode,
|
||||
SDL_EventType Type,
|
||||
bool Repeat,
|
||||
SDL_Keymod Mods
|
||||
) : EventBase;
|
||||
|
||||
private record EventMouseMotion(
|
||||
uint WindowId,
|
||||
int X, int Y,
|
||||
int XRel, int YRel
|
||||
) : EventBase;
|
||||
|
||||
private record EventMouseButton(
|
||||
uint WindowId,
|
||||
SDL_EventType Type,
|
||||
byte Button
|
||||
) : EventBase;
|
||||
|
||||
private record EventText(
|
||||
uint WindowId,
|
||||
string Text
|
||||
) : EventBase;
|
||||
|
||||
private record EventTextEditing(
|
||||
uint WindowId,
|
||||
string Text,
|
||||
int Start,
|
||||
int Length
|
||||
) : EventBase;
|
||||
|
||||
private record EventWindowSize(
|
||||
uint WindowId,
|
||||
int Width,
|
||||
int Height,
|
||||
int FramebufferWidth,
|
||||
int FramebufferHeight,
|
||||
float XScale,
|
||||
float YScale
|
||||
) : EventBase;
|
||||
|
||||
private record EventWheel(
|
||||
uint WindowId,
|
||||
float XOffset,
|
||||
float YOffset
|
||||
) : EventBase;
|
||||
|
||||
// SDL_WindowEvents that don't have special handling like size.
|
||||
private record EventWindow(
|
||||
uint WindowId,
|
||||
SDL_WindowEventID EventId
|
||||
) : EventBase;
|
||||
|
||||
private record EventMonitorSetup
|
||||
(
|
||||
int Id,
|
||||
string Name,
|
||||
VideoMode CurrentMode,
|
||||
VideoMode[] AllModes
|
||||
) : EventBase;
|
||||
|
||||
private record EventMonitorDestroy
|
||||
(
|
||||
int Id
|
||||
) : EventBase;
|
||||
|
||||
private record EventWindowsFakeV(HWND Window,
|
||||
uint Message, WPARAM WParam) : EventBase;
|
||||
|
||||
private record EventKeyMapChanged : EventBase;
|
||||
private record EventQuit : EventBase;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
[SuppressMessage("ReSharper", "IdentifierTypo")]
|
||||
[SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Local")]
|
||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Local")]
|
||||
private struct SDL_SysWMmsg
|
||||
{
|
||||
public SDL_version version;
|
||||
public SDL_SYSWM_TYPE subsystem;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
[SuppressMessage("ReSharper", "IdentifierTypo")]
|
||||
[SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Local")]
|
||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Local")]
|
||||
private struct SDL_SysWMmsgWin32
|
||||
{
|
||||
public SDL_version version;
|
||||
public SDL_SYSWM_TYPE subsystem;
|
||||
public HWND hwnd;
|
||||
public uint msg;
|
||||
public WPARAM wParam;
|
||||
public LPARAM lParam;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,586 +0,0 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using static SDL2.SDL;
|
||||
using static SDL2.SDL.SDL_bool;
|
||||
using static SDL2.SDL.SDL_FlashOperation;
|
||||
using static SDL2.SDL.SDL_GLattr;
|
||||
using static SDL2.SDL.SDL_GLcontext;
|
||||
using static SDL2.SDL.SDL_GLprofile;
|
||||
using static SDL2.SDL.SDL_SYSWM_TYPE;
|
||||
using static SDL2.SDL.SDL_WindowFlags;
|
||||
using BOOL = TerraFX.Interop.Windows.BOOL;
|
||||
using HWND = TerraFX.Interop.Windows.HWND;
|
||||
using GWLP = TerraFX.Interop.Windows.GWLP;
|
||||
using Windows = TerraFX.Interop.Windows.Windows;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl2WindowingImpl
|
||||
{
|
||||
private int _nextWindowId = 1;
|
||||
|
||||
public (WindowReg?, string? error) WindowCreate(
|
||||
GLContextSpec? spec,
|
||||
WindowCreateParameters parameters,
|
||||
WindowReg? share,
|
||||
WindowReg? owner)
|
||||
{
|
||||
nint shareWindow = 0;
|
||||
nint shareContext = 0;
|
||||
if (share is Sdl2WindowReg shareReg)
|
||||
{
|
||||
shareWindow = shareReg.Sdl2Window;
|
||||
shareContext = shareReg.GlContext;
|
||||
}
|
||||
|
||||
nint ownerPtr = 0;
|
||||
if (owner is Sdl2WindowReg ownerReg)
|
||||
ownerPtr = ownerReg.Sdl2Window;
|
||||
|
||||
var task = SharedWindowCreate(spec, parameters, shareWindow, shareContext, ownerPtr);
|
||||
|
||||
// Block the main thread (to avoid stuff like texture uploads being problematic).
|
||||
WaitWindowCreate(task);
|
||||
|
||||
#pragma warning disable RA0004
|
||||
// Block above ensured task is done, this is safe.
|
||||
var (reg, error) = task.Result;
|
||||
#pragma warning restore RA0004
|
||||
if (reg != null)
|
||||
{
|
||||
reg.Owner = reg.Handle;
|
||||
}
|
||||
|
||||
return (reg, error);
|
||||
}
|
||||
|
||||
private void WaitWindowCreate(Task<Sdl2WindowCreateResult> windowTask)
|
||||
{
|
||||
while (!windowTask.IsCompleted)
|
||||
{
|
||||
// Keep processing events until the window task gives either an error or success.
|
||||
WaitEvents();
|
||||
ProcessEvents(single: true);
|
||||
}
|
||||
}
|
||||
|
||||
private Task<Sdl2WindowCreateResult> SharedWindowCreate(
|
||||
GLContextSpec? glSpec,
|
||||
WindowCreateParameters parameters,
|
||||
nint shareWindow,
|
||||
nint shareContext,
|
||||
nint owner)
|
||||
{
|
||||
//
|
||||
// IF YOU'RE WONDERING WHY THIS IS TASK-BASED:
|
||||
// I originally wanted this to be async so we could avoid blocking the main thread
|
||||
// while the OS takes its stupid 100~ms just to initialize a fucking GL context.
|
||||
// This doesn't *work* because
|
||||
// we have to release the GL context while the shared context is being created.
|
||||
// (at least on WGL, I didn't test other platforms and I don't care to.)
|
||||
// Not worth it to avoid a main thread blockage by allowing Clyde to temporarily release the GL context,
|
||||
// because rendering would be locked up *anyways*.
|
||||
//
|
||||
// Basically what I'm saying is that everything about OpenGL is a fucking mistake
|
||||
// and I should get on either Veldrid or Vulkan some time.
|
||||
// Probably Veldrid tbh.
|
||||
//
|
||||
|
||||
// Yes we ping-pong this TCS through the window thread and back, deal with it.
|
||||
var tcs = new TaskCompletionSource<Sdl2WindowCreateResult>();
|
||||
SendCmd(new CmdWinCreate(glSpec, parameters, shareWindow, shareContext, owner, tcs));
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private static void FinishWindowCreate(EventWindowCreate ev)
|
||||
{
|
||||
var (res, tcs) = ev;
|
||||
|
||||
tcs.TrySetResult(res);
|
||||
}
|
||||
|
||||
private void WinThreadWinCreate(CmdWinCreate cmd)
|
||||
{
|
||||
var (glSpec, parameters, shareWindow, shareContext, owner, tcs) = cmd;
|
||||
|
||||
var (window, context) = CreateSdl2WindowForRenderer(glSpec, parameters, shareWindow, shareContext, owner);
|
||||
|
||||
if (window == 0)
|
||||
{
|
||||
var err = SDL_GetError();
|
||||
|
||||
SendEvent(new EventWindowCreate(new Sdl2WindowCreateResult(null, err), tcs));
|
||||
return;
|
||||
}
|
||||
|
||||
// We can't invoke the TCS directly from the windowing thread because:
|
||||
// * it'd hit the synchronization context,
|
||||
// which would make (blocking) main window init more annoying.
|
||||
// * it'd not be synchronized to other incoming window events correctly which might be icky.
|
||||
// So we send the TCS back to the game thread
|
||||
// which processes events in the correct order and has better control of stuff during init.
|
||||
var reg = WinThreadSetupWindow(window, context);
|
||||
|
||||
SendEvent(new EventWindowCreate(new Sdl2WindowCreateResult(reg, null), tcs));
|
||||
}
|
||||
|
||||
private static void WinThreadWinDestroy(CmdWinDestroy cmd)
|
||||
{
|
||||
if (OperatingSystem.IsWindows() && cmd.HadOwner)
|
||||
{
|
||||
// On Windows, closing the child window causes the owner to be minimized, apparently.
|
||||
// Clear owner on close to avoid this.
|
||||
|
||||
SDL_SysWMinfo wmInfo = default;
|
||||
SDL_VERSION(out wmInfo.version);
|
||||
if (SDL_GetWindowWMInfo(cmd.Window, ref wmInfo) == SDL_TRUE && wmInfo.subsystem == SDL_SYSWM_WINDOWS)
|
||||
{
|
||||
var hWnd = (HWND)wmInfo.info.win.window;
|
||||
DebugTools.Assert(hWnd != HWND.NULL);
|
||||
|
||||
Windows.SetWindowLongPtrW(
|
||||
hWnd,
|
||||
GWLP.GWLP_HWNDPARENT,
|
||||
0);
|
||||
}
|
||||
}
|
||||
|
||||
SDL_DestroyWindow(cmd.Window);
|
||||
}
|
||||
|
||||
private (nint window, nint context) CreateSdl2WindowForRenderer(
|
||||
GLContextSpec? spec,
|
||||
WindowCreateParameters parameters,
|
||||
nint shareWindow,
|
||||
nint shareContext,
|
||||
nint ownerWindow)
|
||||
{
|
||||
var windowFlags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE;
|
||||
|
||||
if (spec is { } s)
|
||||
{
|
||||
windowFlags |= SDL_WINDOW_OPENGL;
|
||||
|
||||
SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
|
||||
SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
|
||||
SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
|
||||
SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);
|
||||
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
|
||||
SDL_GL_SetAttribute(SDL_GL_FRAMEBUFFER_SRGB_CAPABLE, s.Profile == GLContextProfile.Es ? 0 : 1);
|
||||
SDL_GLcontext ctxFlags = 0;
|
||||
#if DEBUG
|
||||
ctxFlags |= SDL_GL_CONTEXT_DEBUG_FLAG;
|
||||
#endif
|
||||
if (s.Profile == GLContextProfile.Core)
|
||||
ctxFlags |= SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG;
|
||||
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, (int)ctxFlags);
|
||||
|
||||
if (shareContext != 0)
|
||||
{
|
||||
SDL_GL_MakeCurrent(shareWindow, shareContext);
|
||||
SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 0);
|
||||
}
|
||||
|
||||
var profile = s.Profile switch
|
||||
{
|
||||
GLContextProfile.Compatibility => SDL_GL_CONTEXT_PROFILE_COMPATIBILITY,
|
||||
GLContextProfile.Core => SDL_GL_CONTEXT_PROFILE_CORE,
|
||||
GLContextProfile.Es => SDL_GL_CONTEXT_PROFILE_ES,
|
||||
_ => SDL_GL_CONTEXT_PROFILE_COMPATIBILITY,
|
||||
};
|
||||
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, profile);
|
||||
SDL_SetHint("SDL_OPENGL_ES_DRIVER", s.CreationApi == GLContextCreationApi.Egl ? "1" : "0");
|
||||
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, s.Major);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, s.Minor);
|
||||
|
||||
if (s.CreationApi == GLContextCreationApi.Egl)
|
||||
WsiShared.EnsureEglAvailable();
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
windowFlags |= SDL_WINDOW_ALLOW_HIGHDPI;
|
||||
}
|
||||
|
||||
if (parameters.Fullscreen)
|
||||
{
|
||||
windowFlags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
}
|
||||
|
||||
nint window = SDL_CreateWindow(
|
||||
"",
|
||||
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
|
||||
parameters.Width, parameters.Height,
|
||||
windowFlags);
|
||||
|
||||
if (window == 0)
|
||||
return default;
|
||||
|
||||
nint glContext = SDL_GL_CreateContext(window);
|
||||
if (glContext == 0)
|
||||
{
|
||||
SDL_DestroyWindow(window);
|
||||
return default;
|
||||
}
|
||||
|
||||
// TODO: Monitors, window maximize.
|
||||
// TODO: a bunch of win32 calls for funny window properties I still haven't ported to other platforms.
|
||||
|
||||
// Make sure window thread doesn't keep hold of the GL context.
|
||||
SDL_GL_MakeCurrent(IntPtr.Zero, IntPtr.Zero);
|
||||
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
SDL_SysWMinfo info = default;
|
||||
SDL_VERSION(out info.version);
|
||||
if (SDL_GetWindowWMInfo(window, ref info) == SDL_TRUE && info.subsystem == SDL_SYSWM_WINDOWS)
|
||||
WsiShared.WindowsSharedWindowCreate((HWND) info.info.win.window, _cfg);
|
||||
}
|
||||
|
||||
if (parameters.Visible)
|
||||
SDL_ShowWindow(window);
|
||||
|
||||
return (window, glContext);
|
||||
}
|
||||
|
||||
private unsafe Sdl2WindowReg WinThreadSetupWindow(nint window, nint context)
|
||||
{
|
||||
var reg = new Sdl2WindowReg
|
||||
{
|
||||
Sdl2Window = window,
|
||||
GlContext = context,
|
||||
WindowId = SDL_GetWindowID(window),
|
||||
Id = new WindowId(_nextWindowId++)
|
||||
};
|
||||
var handle = new WindowHandle(_clyde, reg);
|
||||
reg.Handle = handle;
|
||||
|
||||
SDL_VERSION(out reg.SysWMinfo.version);
|
||||
var res = SDL_GetWindowWMInfo(window, ref reg.SysWMinfo);
|
||||
if (res == SDL_FALSE)
|
||||
_sawmill.Error("Failed to get window WM info: {error}", SDL_GetError());
|
||||
|
||||
// LoadWindowIcon(window);
|
||||
|
||||
SDL_GetWindowSizeInPixels(window, out var fbW, out var fbH);
|
||||
reg.FramebufferSize = (fbW, fbH);
|
||||
|
||||
reg.WindowScale = GetWindowScale(window);
|
||||
|
||||
SDL_GetWindowSize(window, out var w, out var h);
|
||||
reg.PrevWindowSize = reg.WindowSize = (w, h);
|
||||
|
||||
SDL_GetWindowPosition(window, out var x, out var y);
|
||||
reg.PrevWindowPos = (x, y);
|
||||
|
||||
reg.PixelRatio = reg.FramebufferSize / (Vector2) reg.WindowSize;
|
||||
|
||||
return reg;
|
||||
}
|
||||
|
||||
public void WindowDestroy(WindowReg window)
|
||||
{
|
||||
var reg = (Sdl2WindowReg) window;
|
||||
SendCmd(new CmdWinDestroy(reg.Sdl2Window, window.Owner != null));
|
||||
}
|
||||
|
||||
public void UpdateMainWindowMode()
|
||||
{
|
||||
if (_clyde._mainWindow == null)
|
||||
return;
|
||||
|
||||
var win = (Sdl2WindowReg) _clyde._mainWindow;
|
||||
|
||||
SendCmd(new CmdWinWinSetMode(win.Sdl2Window, _clyde._windowMode));
|
||||
}
|
||||
|
||||
private static void WinThreadWinSetMode(CmdWinWinSetMode cmd)
|
||||
{
|
||||
var flags = cmd.Mode switch
|
||||
{
|
||||
WindowMode.Fullscreen => (uint) SDL_WINDOW_FULLSCREEN_DESKTOP,
|
||||
_ => 0u
|
||||
};
|
||||
|
||||
SDL_SetWindowFullscreen(cmd.Window, flags);
|
||||
}
|
||||
|
||||
public void WindowSetTitle(WindowReg window, string title)
|
||||
{
|
||||
SendCmd(new CmdWinSetTitle(WinPtr(window), title));
|
||||
}
|
||||
|
||||
private static void WinThreadWinSetTitle(CmdWinSetTitle cmd)
|
||||
{
|
||||
SDL_SetWindowTitle(cmd.Window, cmd.Title);
|
||||
}
|
||||
|
||||
public void WindowSetMonitor(WindowReg window, IClydeMonitor monitor)
|
||||
{
|
||||
// API isn't really used and kinda wack, don't feel like figuring it out for SDL2 yet.
|
||||
_sawmill.Warning("WindowSetMonitor not implemented on SDL2");
|
||||
}
|
||||
|
||||
public void WindowSetSize(WindowReg window, Vector2i size)
|
||||
{
|
||||
SendCmd(new CmdWinSetSize(WinPtr(window), size.X, size.Y));
|
||||
}
|
||||
|
||||
public void WindowSetVisible(WindowReg window, bool visible)
|
||||
{
|
||||
window.IsVisible = visible;
|
||||
SendCmd(new CmdWinSetVisible(WinPtr(window), visible));
|
||||
}
|
||||
|
||||
private static void WinThreadWinSetSize(CmdWinSetSize cmd)
|
||||
{
|
||||
SDL_SetWindowSize(cmd.Window, cmd.W, cmd.H);
|
||||
}
|
||||
|
||||
private static void WinThreadWinSetVisible(CmdWinSetVisible cmd)
|
||||
{
|
||||
if (cmd.Visible)
|
||||
SDL_ShowWindow(cmd.Window);
|
||||
else
|
||||
SDL_HideWindow(cmd.Window);
|
||||
}
|
||||
|
||||
public void WindowRequestAttention(WindowReg window)
|
||||
{
|
||||
SendCmd(new CmdWinRequestAttention(WinPtr(window)));
|
||||
}
|
||||
|
||||
private void WinThreadWinRequestAttention(CmdWinRequestAttention cmd)
|
||||
{
|
||||
var res = SDL_FlashWindow(cmd.Window, SDL_FLASH_UNTIL_FOCUSED);
|
||||
if (res < 0)
|
||||
_sawmill.Error("Failed to flash window: {error}", SDL_GetError());
|
||||
}
|
||||
|
||||
public unsafe void WindowSwapBuffers(WindowReg window)
|
||||
{
|
||||
var reg = (Sdl2WindowReg)window;
|
||||
var windowPtr = WinPtr(reg);
|
||||
|
||||
// On Windows, SwapBuffers does not correctly sync to the DWM compositor.
|
||||
// This means OpenGL vsync is effectively broken by default on Windows.
|
||||
// We manually sync via DwmFlush(). GLFW does this automatically, SDL2 does not.
|
||||
//
|
||||
// Windows DwmFlush logic partly taken from:
|
||||
// https://github.com/love2d/love/blob/5175b0d1b599ea4c7b929f6b4282dd379fa116b8/src/modules/window/sdl/Window.cpp#L1018
|
||||
// https://github.com/glfw/glfw/blob/d3ede7b6847b66cf30b067214b2b4b126d4c729b/src/wgl_context.c#L321-L340
|
||||
// See also: https://github.com/libsdl-org/SDL/issues/5797
|
||||
|
||||
var dwmFlush = false;
|
||||
var swapInterval = 0;
|
||||
|
||||
if (OperatingSystem.IsWindows() && !reg.Fullscreen && reg.SwapInterval > 0)
|
||||
{
|
||||
BOOL compositing;
|
||||
// 6.2 is Windows 8
|
||||
// https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_osversioninfoexw
|
||||
if (OperatingSystem.IsWindowsVersionAtLeast(6, 2)
|
||||
|| Windows.SUCCEEDED(Windows.DwmIsCompositionEnabled(&compositing)) && compositing)
|
||||
{
|
||||
var curCtx = SDL_GL_GetCurrentContext();
|
||||
var curWin = SDL_GL_GetCurrentWindow();
|
||||
|
||||
if (curCtx != reg.GlContext || curWin != reg.Sdl2Window)
|
||||
throw new InvalidOperationException("Window context must be current!");
|
||||
|
||||
SDL_GL_SetSwapInterval(0);
|
||||
dwmFlush = true;
|
||||
swapInterval = reg.SwapInterval;
|
||||
}
|
||||
}
|
||||
|
||||
SDL_GL_SwapWindow(windowPtr);
|
||||
|
||||
if (dwmFlush)
|
||||
{
|
||||
var i = swapInterval;
|
||||
while (i-- > 0)
|
||||
{
|
||||
Windows.DwmFlush();
|
||||
}
|
||||
|
||||
SDL_GL_SetSwapInterval(swapInterval);
|
||||
}
|
||||
}
|
||||
|
||||
public uint? WindowGetX11Id(WindowReg window)
|
||||
{
|
||||
CheckWindowDisposed(window);
|
||||
|
||||
var reg = (Sdl2WindowReg) window;
|
||||
|
||||
if (reg.SysWMinfo.subsystem != SDL_SYSWM_X11)
|
||||
return null;
|
||||
|
||||
return (uint?) reg.SysWMinfo.info.x11.window;
|
||||
}
|
||||
|
||||
public nint? WindowGetX11Display(WindowReg window)
|
||||
{
|
||||
CheckWindowDisposed(window);
|
||||
|
||||
var reg = (Sdl2WindowReg) window;
|
||||
|
||||
if (reg.SysWMinfo.subsystem != SDL_SYSWM_X11)
|
||||
return null;
|
||||
|
||||
return reg.SysWMinfo.info.x11.display;
|
||||
}
|
||||
|
||||
public nint? WindowGetWin32Window(WindowReg window)
|
||||
{
|
||||
CheckWindowDisposed(window);
|
||||
|
||||
var reg = (Sdl2WindowReg) window;
|
||||
|
||||
if (reg.SysWMinfo.subsystem != SDL_SYSWM_WINDOWS)
|
||||
return null;
|
||||
|
||||
return reg.SysWMinfo.info.win.window;
|
||||
}
|
||||
|
||||
public void RunOnWindowThread(Action a)
|
||||
{
|
||||
SendCmd(new CmdRunAction(a));
|
||||
}
|
||||
|
||||
public void TextInputSetRect(UIBox2i rect)
|
||||
{
|
||||
SendCmd(new CmdTextInputSetRect(new SDL_Rect
|
||||
{
|
||||
x = rect.Left,
|
||||
y = rect.Top,
|
||||
w = rect.Width,
|
||||
h = rect.Height
|
||||
}));
|
||||
}
|
||||
|
||||
private static void WinThreadSetTextInputRect(CmdTextInputSetRect cmdTextInput)
|
||||
{
|
||||
var rect = cmdTextInput.Rect;
|
||||
SDL_SetTextInputRect(ref rect);
|
||||
}
|
||||
|
||||
public void TextInputStart()
|
||||
{
|
||||
SendCmd(CmdTextInputStart.Instance);
|
||||
}
|
||||
|
||||
private static void WinThreadStartTextInput()
|
||||
{
|
||||
SDL_StartTextInput();
|
||||
}
|
||||
|
||||
public void TextInputStop()
|
||||
{
|
||||
SendCmd(CmdTextInputStop.Instance);
|
||||
}
|
||||
|
||||
private static void WinThreadStopTextInput()
|
||||
{
|
||||
SDL_StopTextInput();
|
||||
}
|
||||
|
||||
public void ClipboardSetText(WindowReg mainWindow, string text)
|
||||
{
|
||||
SendCmd(new CmdSetClipboard(text));
|
||||
}
|
||||
|
||||
private void WinThreadSetClipboard(CmdSetClipboard cmd)
|
||||
{
|
||||
var res = SDL_SetClipboardText(cmd.Text);
|
||||
if (res < 0)
|
||||
_sawmill.Error("Failed to set clipboard text: {error}", SDL_GetError());
|
||||
}
|
||||
|
||||
public Task<string> ClipboardGetText(WindowReg mainWindow)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<string>();
|
||||
SendCmd(new CmdGetClipboard(tcs));
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private static void WinThreadGetClipboard(CmdGetClipboard cmd)
|
||||
{
|
||||
cmd.Tcs.TrySetResult(SDL_GetClipboardText());
|
||||
}
|
||||
|
||||
private static Vector2 GetWindowScale(nint window)
|
||||
{
|
||||
// Get scale by diving size in pixels with size in points.
|
||||
SDL_GetWindowSizeInPixels(window, out var pixW, out var pixH);
|
||||
SDL_GetWindowSize(window, out var pointW, out var pointH);
|
||||
|
||||
// Avoiding degenerate cases, not sure if these can actually happen.
|
||||
if (pixW == 0 || pixH == 0 || pointW == 0 || pointH == 0)
|
||||
return new Vector2(1, 1);
|
||||
|
||||
var scaleH = pixW / (float) pointW;
|
||||
var scaleV = pixH / (float) pointH;
|
||||
|
||||
// Round to 5% increments to avoid rounding errors causing constantly different scales.
|
||||
scaleH = MathF.Round(scaleH * 20) / 20;
|
||||
scaleV = MathF.Round(scaleV * 20) / 20;
|
||||
|
||||
return new Vector2(scaleH, scaleV);
|
||||
}
|
||||
|
||||
private static void CheckWindowDisposed(WindowReg reg)
|
||||
{
|
||||
if (reg.IsDisposed)
|
||||
throw new ObjectDisposedException("Window disposed");
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static nint WinPtr(WindowReg reg) => ((Sdl2WindowReg)reg).Sdl2Window;
|
||||
|
||||
private WindowReg? FindWindow(uint windowId)
|
||||
{
|
||||
foreach (var windowReg in _clyde._windows)
|
||||
{
|
||||
var glfwReg = (Sdl2WindowReg) windowReg;
|
||||
if (glfwReg.WindowId == windowId)
|
||||
return windowReg;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private sealed class Sdl2WindowReg : WindowReg
|
||||
{
|
||||
public nint Sdl2Window;
|
||||
public uint WindowId;
|
||||
public nint GlContext;
|
||||
public SDL_SysWMinfo SysWMinfo;
|
||||
#pragma warning disable CS0649
|
||||
public bool Fullscreen;
|
||||
#pragma warning restore CS0649
|
||||
public int SwapInterval;
|
||||
|
||||
// Kept around to avoid it being GCd.
|
||||
public CursorImpl? Cursor;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using static SDL2.SDL;
|
||||
using static SDL2.SDL.SDL_LogCategory;
|
||||
using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl2WindowingImpl : IWindowingImpl
|
||||
{
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
private readonly Clyde _clyde;
|
||||
private GCHandle _selfGCHandle;
|
||||
|
||||
private readonly ISawmill _sawmill;
|
||||
private readonly ISawmill _sawmillSdl2;
|
||||
|
||||
public Sdl2WindowingImpl(Clyde clyde, IDependencyCollection deps)
|
||||
{
|
||||
_clyde = clyde;
|
||||
deps.InjectDependencies(this, true);
|
||||
|
||||
_sawmill = _logManager.GetSawmill("clyde.win");
|
||||
_sawmillSdl2 = _logManager.GetSawmill("clyde.win.sdl2");
|
||||
}
|
||||
|
||||
public bool Init()
|
||||
{
|
||||
InitChannels();
|
||||
|
||||
if (!InitSdl2())
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe bool InitSdl2()
|
||||
{
|
||||
_selfGCHandle = GCHandle.Alloc(this, GCHandleType.Normal);
|
||||
|
||||
SDL_LogSetAllPriority(SDL_LogPriority.SDL_LOG_PRIORITY_VERBOSE);
|
||||
SDL_LogSetOutputFunction(&LogOutputFunction, (void*) GCHandle.ToIntPtr(_selfGCHandle));
|
||||
|
||||
SDL_SetHint("SDL_WINDOWS_DPI_SCALING", "1");
|
||||
SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
|
||||
SDL_SetHint(SDL_HINT_IME_SUPPORT_EXTENDED_TEXT, "1");
|
||||
SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1");
|
||||
|
||||
var res = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS);
|
||||
if (res < 0)
|
||||
{
|
||||
_sawmill.Fatal("Failed to initialize SDL2: {error}", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_GetVersion(out var version);
|
||||
var videoDriver = SDL_GetCurrentVideoDriver();
|
||||
_sawmill.Debug(
|
||||
"SDL2 initialized, version: {major}.{minor}.{patch}, video driver: {videoDriver}", version.major, version.minor, version.patch, videoDriver);
|
||||
|
||||
_sdlEventWakeup = SDL_RegisterEvents(1);
|
||||
|
||||
SDL_EventState(SDL_EventType.SDL_SYSWMEVENT, SDL_ENABLE);
|
||||
|
||||
InitCursors();
|
||||
InitMonitors();
|
||||
ReloadKeyMap();
|
||||
|
||||
SDL_AddEventWatch(&EventWatch, (void*) GCHandle.ToIntPtr(_selfGCHandle));
|
||||
|
||||
// SDL defaults to having text input enabled, so we have to manually turn it off in init for consistency.
|
||||
// If we don't, text input will remain enabled *until* the user first leaves a LineEdit/TextEdit.
|
||||
SDL_StopTextInput();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public unsafe void Shutdown()
|
||||
{
|
||||
if (_selfGCHandle != default)
|
||||
{
|
||||
SDL_DelEventWatch(&EventWatch, (void*) GCHandle.ToIntPtr(_selfGCHandle));
|
||||
_selfGCHandle.Free();
|
||||
}
|
||||
|
||||
SDL_LogSetOutputFunction(null, null);
|
||||
|
||||
if (SDL_WasInit(0) != 0)
|
||||
{
|
||||
_sawmill.Debug("Terminating SDL2");
|
||||
SDL_Quit();
|
||||
}
|
||||
}
|
||||
|
||||
public void FlushDispose()
|
||||
{
|
||||
// Not currently used
|
||||
}
|
||||
|
||||
public void GLMakeContextCurrent(WindowReg? reg)
|
||||
{
|
||||
int res;
|
||||
if (reg is Sdl2WindowReg sdlReg)
|
||||
res = SDL_GL_MakeCurrent(sdlReg.Sdl2Window, sdlReg.GlContext);
|
||||
else
|
||||
res = SDL_GL_MakeCurrent(IntPtr.Zero, IntPtr.Zero);
|
||||
|
||||
if (res < 0)
|
||||
_sawmill.Error("SDL_GL_MakeCurrent failed: {error}", SDL_GetError());
|
||||
}
|
||||
|
||||
public void GLSwapInterval(WindowReg reg, int interval)
|
||||
{
|
||||
((Sdl2WindowReg)reg).SwapInterval = interval;
|
||||
SDL_GL_SetSwapInterval(interval);
|
||||
}
|
||||
|
||||
public unsafe void* GLGetProcAddress(string procName)
|
||||
{
|
||||
return (void*) SDL_GL_GetProcAddress(procName);
|
||||
}
|
||||
|
||||
public string GetDescription()
|
||||
{
|
||||
SDL_GetVersion(out var version);
|
||||
_sawmill.Debug(
|
||||
"SDL2 initialized, version: {major}.{minor}.{patch}", version.major, version.minor, version.patch);
|
||||
|
||||
var videoDriver = SDL_GetCurrentVideoDriver();
|
||||
|
||||
return $"SDL2 {version.major}.{version.minor}.{version.patch} ({videoDriver})";
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(CallConvs = new []{typeof(CallConvCdecl)})]
|
||||
private static unsafe void LogOutputFunction(
|
||||
void* userdata,
|
||||
int category,
|
||||
SDL_LogPriority priority,
|
||||
byte* message)
|
||||
{
|
||||
var obj = (Sdl2WindowingImpl) GCHandle.FromIntPtr((IntPtr)userdata).Target!;
|
||||
|
||||
var level = priority switch
|
||||
{
|
||||
SDL_LogPriority.SDL_LOG_PRIORITY_VERBOSE => LogLevel.Verbose,
|
||||
SDL_LogPriority.SDL_LOG_PRIORITY_DEBUG => LogLevel.Debug,
|
||||
SDL_LogPriority.SDL_LOG_PRIORITY_INFO => LogLevel.Info,
|
||||
SDL_LogPriority.SDL_LOG_PRIORITY_WARN => LogLevel.Warning,
|
||||
SDL_LogPriority.SDL_LOG_PRIORITY_ERROR => LogLevel.Error,
|
||||
SDL_LogPriority.SDL_LOG_PRIORITY_CRITICAL => LogLevel.Fatal,
|
||||
_ => LogLevel.Error
|
||||
};
|
||||
|
||||
var msg = Marshal.PtrToStringUTF8((IntPtr) message) ?? "";
|
||||
if (msg == "That operation is not supported")
|
||||
{
|
||||
obj._sawmillSdl2.Info(Environment.StackTrace);
|
||||
}
|
||||
|
||||
var categoryName = SdlLogCategoryName(category);
|
||||
obj._sawmillSdl2.Log(level, $"[{categoryName}] {msg}");
|
||||
}
|
||||
|
||||
private static string SdlLogCategoryName(int category)
|
||||
{
|
||||
return (SDL_LogCategory) category switch {
|
||||
// @formatter:off
|
||||
SDL_LOG_CATEGORY_APPLICATION => "application",
|
||||
SDL_LOG_CATEGORY_ERROR => "error",
|
||||
SDL_LOG_CATEGORY_ASSERT => "assert",
|
||||
SDL_LOG_CATEGORY_SYSTEM => "system",
|
||||
SDL_LOG_CATEGORY_AUDIO => "audio",
|
||||
SDL_LOG_CATEGORY_VIDEO => "video",
|
||||
SDL_LOG_CATEGORY_RENDER => "render",
|
||||
SDL_LOG_CATEGORY_INPUT => "input",
|
||||
SDL_LOG_CATEGORY_TEST => "test",
|
||||
_ => "unknown"
|
||||
// @formatter:on
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,16 +3,15 @@ using System.Collections.Generic;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using SDL3;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using static SDL2.SDL;
|
||||
using static SDL2.SDL.SDL_SystemCursor;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl2WindowingImpl : IWindowingImpl
|
||||
private sealed partial class Sdl3WindowingImpl
|
||||
{
|
||||
private readonly Dictionary<ClydeHandle, WinThreadCursorReg> _winThreadCursors = new();
|
||||
private readonly CursorImpl[] _standardCursors = new CursorImpl[(int)StandardCursorShape.CountCursors];
|
||||
@@ -28,31 +27,32 @@ internal partial class Clyde
|
||||
image.GetPixelSpan().CopyTo(cloneImg.GetPixelSpan());
|
||||
|
||||
var id = _clyde.AllocRid();
|
||||
SendCmd(new CmdCursorCreate(cloneImg, hotSpot, id));
|
||||
SendCmd(new CmdCursorCreate { Bytes = cloneImg, Hotspot = hotSpot, Cursor = id });
|
||||
|
||||
return new CursorImpl(this, id, false);
|
||||
}
|
||||
|
||||
private unsafe void WinThreadCursorCreate(CmdCursorCreate cmd)
|
||||
{
|
||||
var (img, (hotX, hotY), id) = cmd;
|
||||
using var img = cmd.Bytes;
|
||||
|
||||
fixed (Rgba32* pixPtr = img.GetPixelSpan())
|
||||
{
|
||||
var surface = SDL_CreateRGBSurfaceWithFormatFrom(
|
||||
var surface = SDL.SDL_CreateSurfaceFrom(
|
||||
img.Width,
|
||||
img.Height,
|
||||
SDL.SDL_PixelFormat.SDL_PIXELFORMAT_ABGR8888,
|
||||
(IntPtr)pixPtr,
|
||||
img.Width, img.Height, 0,
|
||||
sizeof(Rgba32) * img.Width,
|
||||
SDL_PIXELFORMAT_RGBA8888);
|
||||
sizeof(Rgba32) * img.Width);
|
||||
|
||||
var cursor = SDL_CreateColorCursor(surface, hotX, hotY);
|
||||
var cursor = SDL.SDL_CreateColorCursor(surface, cmd.Hotspot.X, cmd.Hotspot.Y);
|
||||
if (cursor == 0)
|
||||
throw new InvalidOperationException("SDL_CreateColorCursor failed");
|
||||
|
||||
_winThreadCursors.Add(id, new WinThreadCursorReg { Ptr = cursor });
|
||||
_winThreadCursors.Add(cmd.Cursor, new WinThreadCursorReg { Ptr = cursor });
|
||||
|
||||
SDL_FreeSurface(surface);
|
||||
SDL.SDL_DestroySurface(surface);
|
||||
}
|
||||
|
||||
img.Dispose();
|
||||
}
|
||||
|
||||
public void CursorSet(WindowReg window, ICursor? cursor)
|
||||
@@ -62,7 +62,7 @@ internal partial class Clyde
|
||||
// SDL_SetCursor(NULL) does redraw, not reset.
|
||||
cursor ??= CursorGetStandard(StandardCursorShape.Arrow);
|
||||
|
||||
var reg = (Sdl2WindowReg)window;
|
||||
var reg = (Sdl3WindowReg)window;
|
||||
|
||||
if (reg.Cursor == cursor)
|
||||
return;
|
||||
@@ -74,7 +74,7 @@ internal partial class Clyde
|
||||
throw new ObjectDisposedException(nameof(cursor));
|
||||
|
||||
reg.Cursor = impl;
|
||||
SendCmd(new CmdWinCursorSet(reg.Sdl2Window, impl.Id));
|
||||
SendCmd(new CmdWinCursorSet { Window = reg.Sdl3Window, Cursor = impl.Id });
|
||||
}
|
||||
|
||||
private void WinThreadWinCursorSet(CmdWinCursorSet cmd)
|
||||
@@ -83,22 +83,22 @@ internal partial class Clyde
|
||||
var ptr = _winThreadCursors[cmd.Cursor].Ptr;
|
||||
|
||||
// TODO: multi-window??
|
||||
SDL_SetCursor(ptr);
|
||||
SDL.SDL_SetCursor(ptr);
|
||||
}
|
||||
|
||||
private void InitCursors()
|
||||
{
|
||||
Add(StandardCursorShape.Arrow, SDL_SYSTEM_CURSOR_ARROW);
|
||||
Add(StandardCursorShape.IBeam, SDL_SYSTEM_CURSOR_IBEAM);
|
||||
Add(StandardCursorShape.Crosshair, SDL_SYSTEM_CURSOR_CROSSHAIR);
|
||||
Add(StandardCursorShape.Hand, SDL_SYSTEM_CURSOR_HAND);
|
||||
Add(StandardCursorShape.HResize, SDL_SYSTEM_CURSOR_SIZEWE);
|
||||
Add(StandardCursorShape.VResize, SDL_SYSTEM_CURSOR_SIZENS);
|
||||
Add(StandardCursorShape.Arrow, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_DEFAULT);
|
||||
Add(StandardCursorShape.IBeam, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_TEXT);
|
||||
Add(StandardCursorShape.Crosshair, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_CROSSHAIR);
|
||||
Add(StandardCursorShape.Hand, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_POINTER);
|
||||
Add(StandardCursorShape.HResize, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_EW_RESIZE);
|
||||
Add(StandardCursorShape.VResize, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_NS_RESIZE);
|
||||
|
||||
void Add(StandardCursorShape shape, SDL_SystemCursor sysCursor)
|
||||
void Add(StandardCursorShape shape, SDL.SDL_SystemCursor sysCursor)
|
||||
{
|
||||
var id = _clyde.AllocRid();
|
||||
var cursor = SDL_CreateSystemCursor(sysCursor);
|
||||
var cursor = SDL.SDL_CreateSystemCursor(sysCursor);
|
||||
|
||||
var impl = new CursorImpl(this, id, true);
|
||||
|
||||
@@ -107,13 +107,21 @@ internal partial class Clyde
|
||||
}
|
||||
}
|
||||
|
||||
private void WinThreadCursorDestroy(CmdCursorDestroy cmd)
|
||||
{
|
||||
if (!_winThreadCursors.TryGetValue(cmd.Cursor, out var cursor))
|
||||
return;
|
||||
|
||||
SDL.SDL_DestroyCursor(cursor.Ptr);
|
||||
}
|
||||
|
||||
private sealed class CursorImpl : ICursor
|
||||
{
|
||||
private readonly bool _standard;
|
||||
public Sdl2WindowingImpl Owner { get; }
|
||||
public Sdl3WindowingImpl Owner { get; }
|
||||
public ClydeHandle Id { get; private set; }
|
||||
|
||||
public CursorImpl(Sdl2WindowingImpl clyde, ClydeHandle id, bool standard)
|
||||
public CursorImpl(Sdl3WindowingImpl clyde, ClydeHandle id, bool standard)
|
||||
{
|
||||
_standard = standard;
|
||||
Owner = clyde;
|
||||
@@ -127,7 +135,7 @@ internal partial class Clyde
|
||||
|
||||
private void DisposeImpl()
|
||||
{
|
||||
Owner.SendCmd(new CmdCursorDestroy(Id));
|
||||
Owner.SendCmd(new CmdCursorDestroy { Cursor = Id });
|
||||
Id = default;
|
||||
}
|
||||
|
||||
@@ -147,9 +155,5 @@ internal partial class Clyde
|
||||
{
|
||||
public nint Ptr;
|
||||
}
|
||||
|
||||
private void WinThreadCursorDestroy(CmdCursorDestroy cmd)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,16 @@
|
||||
using System.Numerics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Shared.Map;
|
||||
using TerraFX.Interop.Windows;
|
||||
using static SDL2.SDL;
|
||||
using static SDL2.SDL.SDL_EventType;
|
||||
using static SDL2.SDL.SDL_Keymod;
|
||||
using static SDL2.SDL.SDL_WindowEventID;
|
||||
using SDL3;
|
||||
using Key = Robust.Client.Input.Keyboard.Key;
|
||||
using ET = SDL3.SDL.SDL_EventType;
|
||||
using SDL_Keymod = SDL3.SDL.SDL_Keymod;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl2WindowingImpl
|
||||
private sealed partial class Sdl3WindowingImpl
|
||||
{
|
||||
public void ProcessEvents(bool single = false)
|
||||
{
|
||||
@@ -47,15 +45,18 @@ internal partial class Clyde
|
||||
case EventWindowCreate wCreate:
|
||||
FinishWindowCreate(wCreate);
|
||||
break;
|
||||
case EventWindow ev:
|
||||
ProcessEventWindow(ev);
|
||||
case EventWindowMisc ev:
|
||||
ProcessEventWindowMisc(ev);
|
||||
break;
|
||||
case EventKey ev:
|
||||
ProcessEventKey(ev);
|
||||
break;
|
||||
case EventWindowSize ev:
|
||||
case EventWindowPixelSize ev:
|
||||
ProcessEventWindowSize(ev);
|
||||
break;
|
||||
case EventWindowContentScale ev:
|
||||
ProcessEventWindowContentScale(ev);
|
||||
break;
|
||||
case EventText ev:
|
||||
ProcessEventText(ev);
|
||||
break;
|
||||
@@ -74,9 +75,6 @@ internal partial class Clyde
|
||||
case EventMonitorSetup ev:
|
||||
ProcessSetupMonitor(ev);
|
||||
break;
|
||||
case EventWindowsFakeV ev:
|
||||
ProcessWindowsFakeV(ev);
|
||||
break;
|
||||
case EventKeyMapChanged:
|
||||
ProcessKeyMapChanged();
|
||||
break;
|
||||
@@ -84,7 +82,7 @@ internal partial class Clyde
|
||||
ProcessEventQuit();
|
||||
break;
|
||||
default:
|
||||
_sawmill.Error($"Unknown SDL2 event type: {evb.GetType().Name}");
|
||||
_sawmill.Error($"Unknown SDL3 event type: {evb.GetType().Name}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -96,7 +94,7 @@ internal partial class Clyde
|
||||
_clyde.SendCloseWindow(window, new WindowRequestClosedEventArgs(window.Handle));
|
||||
}
|
||||
|
||||
private void ProcessEventWindow(EventWindow ev)
|
||||
private void ProcessEventWindowMisc(EventWindowMisc ev)
|
||||
{
|
||||
var window = FindWindow(ev.WindowId);
|
||||
if (window == null)
|
||||
@@ -104,33 +102,38 @@ internal partial class Clyde
|
||||
|
||||
switch (ev.EventId)
|
||||
{
|
||||
case SDL_WINDOWEVENT_CLOSE:
|
||||
case ET.SDL_EVENT_WINDOW_CLOSE_REQUESTED:
|
||||
_clyde.SendCloseWindow(window, new WindowRequestClosedEventArgs(window.Handle));
|
||||
break;
|
||||
case SDL_WINDOWEVENT_ENTER:
|
||||
case ET.SDL_EVENT_WINDOW_MOUSE_ENTER:
|
||||
_clyde._currentHoveredWindow = window;
|
||||
_clyde.SendMouseEnterLeave(new MouseEnterLeaveEventArgs(window.Handle, true));
|
||||
break;
|
||||
case SDL_WINDOWEVENT_LEAVE:
|
||||
case ET.SDL_EVENT_WINDOW_MOUSE_LEAVE:
|
||||
if (_clyde._currentHoveredWindow == window)
|
||||
_clyde._currentHoveredWindow = null;
|
||||
|
||||
_clyde.SendMouseEnterLeave(new MouseEnterLeaveEventArgs(window.Handle, false));
|
||||
break;
|
||||
case SDL_WINDOWEVENT_MINIMIZED:
|
||||
case ET.SDL_EVENT_WINDOW_MINIMIZED:
|
||||
window.IsMinimized = true;
|
||||
break;
|
||||
case SDL_WINDOWEVENT_RESTORED:
|
||||
case ET.SDL_EVENT_WINDOW_RESTORED:
|
||||
window.IsMinimized = false;
|
||||
break;
|
||||
case SDL_WINDOWEVENT_FOCUS_GAINED:
|
||||
case ET.SDL_EVENT_WINDOW_FOCUS_GAINED:
|
||||
window.IsFocused = true;
|
||||
_clyde.SendWindowFocus(new WindowFocusedEventArgs(true, window.Handle));
|
||||
break;
|
||||
case SDL_WINDOWEVENT_FOCUS_LOST:
|
||||
case ET.SDL_EVENT_WINDOW_FOCUS_LOST:
|
||||
window.IsFocused = false;
|
||||
_clyde.SendWindowFocus(new WindowFocusedEventArgs(false, window.Handle));
|
||||
break;
|
||||
case ET.SDL_EVENT_WINDOW_MOVED:
|
||||
window.WindowPos = (ev.Data1, ev.Data2);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,10 +156,9 @@ internal partial class Clyde
|
||||
if (windowReg == null)
|
||||
return;
|
||||
|
||||
var mods = SDL_GetModState();
|
||||
var button = ConvertSdl2Button(ev.Button);
|
||||
var button = ConvertSdl3Button(ev.Button);
|
||||
var key = Mouse.MouseButtonToKey(button);
|
||||
EmitKeyEvent(key, ev.Type, false, mods, 0);
|
||||
EmitKeyEvent(key, ev.Type, false, ev.Mods, 0);
|
||||
}
|
||||
|
||||
private void ProcessEventMouseMotion(EventMouseMotion ev)
|
||||
@@ -166,8 +168,7 @@ internal partial class Clyde
|
||||
return;
|
||||
|
||||
var newPos = new Vector2(ev.X, ev.Y) * windowReg.PixelRatio;
|
||||
// SDL2 does give us delta positions, but I'm worried about rounding errors thanks to DPI stuff.
|
||||
var delta = newPos - windowReg.LastMousePos;
|
||||
var delta = new Vector2(ev.XRel, ev.YRel);
|
||||
windowReg.LastMousePos = newPos;
|
||||
|
||||
_clyde._currentHoveredWindow = windowReg;
|
||||
@@ -185,7 +186,7 @@ internal partial class Clyde
|
||||
_clyde.SendTextEditing(new TextEditingEventArgs(ev.Text, ev.Start, ev.Length));
|
||||
}
|
||||
|
||||
private void ProcessEventWindowSize(EventWindowSize ev)
|
||||
private void ProcessEventWindowSize(EventWindowPixelSize ev)
|
||||
{
|
||||
var window = ev.WindowId;
|
||||
var width = ev.Width;
|
||||
@@ -205,28 +206,31 @@ internal partial class Clyde
|
||||
return;
|
||||
|
||||
windowReg.PixelRatio = windowReg.FramebufferSize / (Vector2)windowReg.WindowSize;
|
||||
var newScale = new Vector2(ev.XScale, ev.YScale);
|
||||
|
||||
if (!windowReg.WindowScale.Equals(newScale))
|
||||
{
|
||||
windowReg.WindowScale = newScale;
|
||||
_clyde.SendWindowContentScaleChanged(new WindowContentScaleEventArgs(windowReg.Handle));
|
||||
}
|
||||
|
||||
_clyde.SendWindowResized(windowReg, oldSize);
|
||||
}
|
||||
|
||||
private void ProcessEventKey(EventKey ev)
|
||||
private void ProcessEventWindowContentScale(EventWindowContentScale ev)
|
||||
{
|
||||
EmitKeyEvent(ConvertSdl2Scancode(ev.Scancode), ev.Type, ev.Repeat, ev.Mods, ev.Scancode);
|
||||
var windowReg = FindWindow(ev.WindowId);
|
||||
if (windowReg == null)
|
||||
return;
|
||||
|
||||
windowReg.WindowScale = new Vector2(ev.Scale, ev.Scale);
|
||||
_clyde.SendWindowContentScaleChanged(new WindowContentScaleEventArgs(windowReg.Handle));
|
||||
}
|
||||
|
||||
private void EmitKeyEvent(Key key, SDL_EventType type, bool repeat, SDL_Keymod mods, SDL_Scancode scancode)
|
||||
private void ProcessEventKey(EventKey ev)
|
||||
{
|
||||
var shift = (mods & KMOD_SHIFT) != 0;
|
||||
var alt = (mods & KMOD_ALT) != 0;
|
||||
var control = (mods & KMOD_CTRL) != 0;
|
||||
var system = (mods & KMOD_GUI) != 0;
|
||||
EmitKeyEvent(ConvertSdl3Scancode(ev.Scancode), ev.Type, ev.Repeat, ev.Mods, ev.Scancode);
|
||||
}
|
||||
|
||||
private void EmitKeyEvent(Key key, ET type, bool repeat, SDL.SDL_Keymod mods, SDL.SDL_Scancode scancode)
|
||||
{
|
||||
var shift = (mods & SDL_Keymod.SDL_KMOD_SHIFT) != 0;
|
||||
var alt = (mods & SDL_Keymod.SDL_KMOD_ALT) != 0;
|
||||
var control = (mods & SDL_Keymod.SDL_KMOD_CTRL) != 0;
|
||||
var system = (mods & SDL_Keymod.SDL_KMOD_GUI) != 0;
|
||||
|
||||
var ev = new KeyEventArgs(
|
||||
key,
|
||||
@@ -236,36 +240,17 @@ internal partial class Clyde
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case SDL_KEYUP:
|
||||
case SDL_MOUSEBUTTONUP:
|
||||
case ET.SDL_EVENT_KEY_UP:
|
||||
case ET.SDL_EVENT_MOUSE_BUTTON_UP:
|
||||
_clyde.SendKeyUp(ev);
|
||||
break;
|
||||
case SDL_KEYDOWN:
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
case ET.SDL_EVENT_KEY_DOWN:
|
||||
case ET.SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||
_clyde.SendKeyDown(ev);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessWindowsFakeV(EventWindowsFakeV ev)
|
||||
{
|
||||
var type = (int)ev.Message switch
|
||||
{
|
||||
WM.WM_KEYUP => SDL_KEYUP,
|
||||
WM.WM_KEYDOWN => SDL_KEYDOWN,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
var key = (int)ev.WParam switch
|
||||
{
|
||||
0x56 /* V */ => Key.V,
|
||||
VK.VK_CONTROL => Key.Control,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
EmitKeyEvent(key, type, false, 0, 0);
|
||||
}
|
||||
|
||||
private void ProcessKeyMapChanged()
|
||||
{
|
||||
_clyde.SendInputModeChanged();
|
||||
145
Robust.Client/Graphics/Clyde/Windowing/Sdl3.FileDialog.cs
Normal file
145
Robust.Client/Graphics/Clyde/Windowing/Sdl3.FileDialog.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Client.UserInterface;
|
||||
using SDL3;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl3WindowingImpl : IFileDialogManager
|
||||
{
|
||||
public async Task<Stream?> OpenFile(FileDialogFilters? filters = null)
|
||||
{
|
||||
var fileName = await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_OPENFILE, filters);
|
||||
if (fileName == null)
|
||||
return null;
|
||||
|
||||
return File.OpenRead(fileName);
|
||||
}
|
||||
|
||||
public async Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null, bool truncate = true)
|
||||
{
|
||||
var fileName = await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_SAVEFILE, filters);
|
||||
if (fileName == null)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return (File.Open(fileName, truncate ? FileMode.Truncate : FileMode.Open), true);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
return (File.Open(fileName, FileMode.Create), false);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe Task<string?> ShowFileDialogOfType(int type, FileDialogFilters? filters)
|
||||
{
|
||||
var props = SDL.SDL_CreateProperties();
|
||||
|
||||
SDL.SDL_DialogFileFilter* filtersAlloc = null;
|
||||
if (filters != null)
|
||||
{
|
||||
filtersAlloc = (SDL.SDL_DialogFileFilter*)NativeMemory.Alloc(
|
||||
(UIntPtr)filters.Groups.Count,
|
||||
(UIntPtr)sizeof(SDL.SDL_DialogFileFilter));
|
||||
|
||||
SDL.SDL_SetNumberProperty(props, SDL.SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER, filters.Groups.Count);
|
||||
SDL.SDL_SetPointerProperty(props, SDL.SDL_PROP_FILE_DIALOG_FILTERS_POINTER, (nint)filtersAlloc);
|
||||
|
||||
// All these mallocs aren't gonna win any performance awards, but oh well.
|
||||
for (var i = 0; i < filters.Groups.Count; i++)
|
||||
{
|
||||
var (name, pattern) = ConvertFilterGroup(filters.Groups[i]);
|
||||
filtersAlloc[i].name = StringToNative(name);
|
||||
filtersAlloc[i].pattern = StringToNative(pattern);
|
||||
}
|
||||
}
|
||||
|
||||
var task = ShowFileDialogWithProperties(type, props);
|
||||
|
||||
SDL.SDL_DestroyProperties(props);
|
||||
|
||||
if (filtersAlloc != null)
|
||||
{
|
||||
for (var i = 0; i < filters!.Groups.Count; i++)
|
||||
{
|
||||
var filter = filtersAlloc[i];
|
||||
NativeMemory.Free(filter.name);
|
||||
NativeMemory.Free(filter.pattern);
|
||||
}
|
||||
}
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
private static unsafe byte* StringToNative(string str)
|
||||
{
|
||||
var byteCount = Encoding.UTF8.GetByteCount(str);
|
||||
|
||||
var mem = (byte*) NativeMemory.Alloc((nuint)(byteCount + 1));
|
||||
Encoding.UTF8.GetBytes(str, new Span<byte>(mem, byteCount));
|
||||
mem[byteCount] = 0; // null-terminate
|
||||
|
||||
return mem;
|
||||
}
|
||||
|
||||
private (string name, string pattern) ConvertFilterGroup(FileDialogFilters.Group group)
|
||||
{
|
||||
var name = string.Join(", ", group.Extensions.Select(e => $"*.{e}"));
|
||||
var pattern = string.Join(";", group.Extensions);
|
||||
return (name, pattern);
|
||||
}
|
||||
|
||||
private unsafe Task<string?> ShowFileDialogWithProperties(int type, uint properties)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<string?>();
|
||||
|
||||
var gcHandle = GCHandle.Alloc(new FileDialogState
|
||||
{
|
||||
Parent = this,
|
||||
Tcs = tcs
|
||||
});
|
||||
|
||||
SDL.SDL_ShowFileDialogWithProperties(
|
||||
type,
|
||||
&FileDialogCallback,
|
||||
(void*)GCHandle.ToIntPtr(gcHandle),
|
||||
properties);
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
|
||||
private static unsafe void FileDialogCallback(void* userdata, byte** filelist, int filter)
|
||||
{
|
||||
var stateHandle = GCHandle.FromIntPtr((IntPtr)userdata);
|
||||
var state = (FileDialogState)stateHandle.Target!;
|
||||
stateHandle.Free();
|
||||
|
||||
if (filelist == null)
|
||||
{
|
||||
// Error
|
||||
state.Parent._sawmill.Error("File dialog failed: {error}", SDL.SDL_GetError());
|
||||
state.Tcs.SetResult(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handles null (cancelled/none selected) transparently.
|
||||
var str = Marshal.PtrToStringUTF8((nint) filelist[0]);
|
||||
state.Tcs.SetResult(str);
|
||||
}
|
||||
|
||||
private sealed class FileDialogState
|
||||
{
|
||||
public required Sdl3WindowingImpl Parent;
|
||||
public required TaskCompletionSource<string?> Tcs;
|
||||
}
|
||||
}
|
||||
}
|
||||
225
Robust.Client/Graphics/Clyde/Windowing/Sdl3.Key.cs
Normal file
225
Robust.Client/Graphics/Clyde/Windowing/Sdl3.Key.cs
Normal file
@@ -0,0 +1,225 @@
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using SDL3;
|
||||
using Key = Robust.Client.Input.Keyboard.Key;
|
||||
using Button = Robust.Client.Input.Mouse.Button;
|
||||
using SC = SDL3.SDL.SDL_Scancode;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl3WindowingImpl
|
||||
{
|
||||
// Indices are values of SDL_Scancode
|
||||
private static readonly Key[] KeyMap;
|
||||
private static readonly FrozenDictionary<Key, SC> KeyMapReverse;
|
||||
private static readonly Button[] MouseButtonMap;
|
||||
|
||||
// TODO: to avoid having to ask the windowing thread, key names are cached.
|
||||
private readonly Dictionary<Key, string> _printableKeyNameMap = new();
|
||||
|
||||
private void ReloadKeyMap()
|
||||
{
|
||||
// This may be ran concurrently from the windowing thread.
|
||||
lock (_printableKeyNameMap)
|
||||
{
|
||||
_printableKeyNameMap.Clear();
|
||||
|
||||
// TODO: Validate this is correct in SDL3.
|
||||
|
||||
// List of mappable keys from SDL2's source appears to be:
|
||||
// entries in SDL_default_keymap that aren't an SDLK_ enum reference.
|
||||
// (the actual logic is more nuanced, but it appears to match the above)
|
||||
// Comes out to these two ranges:
|
||||
|
||||
for (var k = SC.SDL_SCANCODE_A; k <= SC.SDL_SCANCODE_0; k++)
|
||||
{
|
||||
CacheKey(k);
|
||||
}
|
||||
|
||||
for (var k = SC.SDL_SCANCODE_MINUS; k <= SC.SDL_SCANCODE_SLASH; k++)
|
||||
{
|
||||
CacheKey(k);
|
||||
}
|
||||
|
||||
void CacheKey(SC scancode)
|
||||
{
|
||||
var rKey = ConvertSdl3Scancode(scancode);
|
||||
if (rKey == Key.Unknown)
|
||||
return;
|
||||
|
||||
// TODO: SDL_GetKeyFromScancode correct?
|
||||
var name = SDL.SDL_GetKeyName(
|
||||
SDL.SDL_GetKeyFromScancode(scancode, SDL.SDL_Keymod.SDL_KMOD_NONE, false));
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
_printableKeyNameMap.Add(rKey, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string? KeyGetName(Key key)
|
||||
{
|
||||
lock (_printableKeyNameMap)
|
||||
{
|
||||
if (_printableKeyNameMap.TryGetValue(key, out var name))
|
||||
return name;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
internal static Key ConvertSdl3Scancode(SC scancode)
|
||||
{
|
||||
return KeyMap[(int) scancode];
|
||||
}
|
||||
|
||||
public static Button ConvertSdl3Button(int button)
|
||||
{
|
||||
return MouseButtonMap[button];
|
||||
}
|
||||
|
||||
static Sdl3WindowingImpl()
|
||||
{
|
||||
MouseButtonMap = new Button[6];
|
||||
MouseButtonMap[SDL.SDL_BUTTON_LEFT] = Button.Left;
|
||||
MouseButtonMap[SDL.SDL_BUTTON_RIGHT] = Button.Right;
|
||||
MouseButtonMap[SDL.SDL_BUTTON_MIDDLE] = Button.Middle;
|
||||
MouseButtonMap[SDL.SDL_BUTTON_X1] = Button.Button4;
|
||||
MouseButtonMap[SDL.SDL_BUTTON_X2] = Button.Button5;
|
||||
|
||||
KeyMap = new Key[(int) SC.SDL_SCANCODE_COUNT];
|
||||
MapKey(SC.SDL_SCANCODE_A, Key.A);
|
||||
MapKey(SC.SDL_SCANCODE_B, Key.B);
|
||||
MapKey(SC.SDL_SCANCODE_C, Key.C);
|
||||
MapKey(SC.SDL_SCANCODE_D, Key.D);
|
||||
MapKey(SC.SDL_SCANCODE_E, Key.E);
|
||||
MapKey(SC.SDL_SCANCODE_F, Key.F);
|
||||
MapKey(SC.SDL_SCANCODE_G, Key.G);
|
||||
MapKey(SC.SDL_SCANCODE_H, Key.H);
|
||||
MapKey(SC.SDL_SCANCODE_I, Key.I);
|
||||
MapKey(SC.SDL_SCANCODE_J, Key.J);
|
||||
MapKey(SC.SDL_SCANCODE_K, Key.K);
|
||||
MapKey(SC.SDL_SCANCODE_L, Key.L);
|
||||
MapKey(SC.SDL_SCANCODE_M, Key.M);
|
||||
MapKey(SC.SDL_SCANCODE_N, Key.N);
|
||||
MapKey(SC.SDL_SCANCODE_O, Key.O);
|
||||
MapKey(SC.SDL_SCANCODE_P, Key.P);
|
||||
MapKey(SC.SDL_SCANCODE_Q, Key.Q);
|
||||
MapKey(SC.SDL_SCANCODE_R, Key.R);
|
||||
MapKey(SC.SDL_SCANCODE_S, Key.S);
|
||||
MapKey(SC.SDL_SCANCODE_T, Key.T);
|
||||
MapKey(SC.SDL_SCANCODE_U, Key.U);
|
||||
MapKey(SC.SDL_SCANCODE_V, Key.V);
|
||||
MapKey(SC.SDL_SCANCODE_W, Key.W);
|
||||
MapKey(SC.SDL_SCANCODE_X, Key.X);
|
||||
MapKey(SC.SDL_SCANCODE_Y, Key.Y);
|
||||
MapKey(SC.SDL_SCANCODE_Z, Key.Z);
|
||||
MapKey(SC.SDL_SCANCODE_0, Key.Num0);
|
||||
MapKey(SC.SDL_SCANCODE_1, Key.Num1);
|
||||
MapKey(SC.SDL_SCANCODE_2, Key.Num2);
|
||||
MapKey(SC.SDL_SCANCODE_3, Key.Num3);
|
||||
MapKey(SC.SDL_SCANCODE_4, Key.Num4);
|
||||
MapKey(SC.SDL_SCANCODE_5, Key.Num5);
|
||||
MapKey(SC.SDL_SCANCODE_6, Key.Num6);
|
||||
MapKey(SC.SDL_SCANCODE_7, Key.Num7);
|
||||
MapKey(SC.SDL_SCANCODE_8, Key.Num8);
|
||||
MapKey(SC.SDL_SCANCODE_9, Key.Num9);
|
||||
MapKey(SC.SDL_SCANCODE_KP_0, Key.NumpadNum0);
|
||||
MapKey(SC.SDL_SCANCODE_KP_1, Key.NumpadNum1);
|
||||
MapKey(SC.SDL_SCANCODE_KP_2, Key.NumpadNum2);
|
||||
MapKey(SC.SDL_SCANCODE_KP_3, Key.NumpadNum3);
|
||||
MapKey(SC.SDL_SCANCODE_KP_4, Key.NumpadNum4);
|
||||
MapKey(SC.SDL_SCANCODE_KP_5, Key.NumpadNum5);
|
||||
MapKey(SC.SDL_SCANCODE_KP_6, Key.NumpadNum6);
|
||||
MapKey(SC.SDL_SCANCODE_KP_7, Key.NumpadNum7);
|
||||
MapKey(SC.SDL_SCANCODE_KP_8, Key.NumpadNum8);
|
||||
MapKey(SC.SDL_SCANCODE_KP_9, Key.NumpadNum9);
|
||||
MapKey(SC.SDL_SCANCODE_ESCAPE, Key.Escape);
|
||||
MapKey(SC.SDL_SCANCODE_LCTRL, Key.Control);
|
||||
MapKey(SC.SDL_SCANCODE_RCTRL, Key.Control);
|
||||
MapKey(SC.SDL_SCANCODE_RSHIFT, Key.Shift);
|
||||
MapKey(SC.SDL_SCANCODE_LSHIFT, Key.Shift);
|
||||
MapKey(SC.SDL_SCANCODE_LALT, Key.Alt);
|
||||
MapKey(SC.SDL_SCANCODE_RALT, Key.Alt);
|
||||
MapKey(SC.SDL_SCANCODE_LGUI, Key.LSystem);
|
||||
MapKey(SC.SDL_SCANCODE_RGUI, Key.RSystem);
|
||||
MapKey(SC.SDL_SCANCODE_MENU, Key.Menu);
|
||||
MapKey(SC.SDL_SCANCODE_LEFTBRACKET, Key.LBracket);
|
||||
MapKey(SC.SDL_SCANCODE_RIGHTBRACKET, Key.RBracket);
|
||||
MapKey(SC.SDL_SCANCODE_SEMICOLON, Key.SemiColon);
|
||||
MapKey(SC.SDL_SCANCODE_COMMA, Key.Comma);
|
||||
MapKey(SC.SDL_SCANCODE_PERIOD, Key.Period);
|
||||
MapKey(SC.SDL_SCANCODE_APOSTROPHE, Key.Apostrophe);
|
||||
MapKey(SC.SDL_SCANCODE_SLASH, Key.Slash);
|
||||
MapKey(SC.SDL_SCANCODE_BACKSLASH, Key.BackSlash);
|
||||
MapKey(SC.SDL_SCANCODE_GRAVE, Key.Tilde);
|
||||
MapKey(SC.SDL_SCANCODE_EQUALS, Key.Equal);
|
||||
MapKey(SC.SDL_SCANCODE_SPACE, Key.Space);
|
||||
MapKey(SC.SDL_SCANCODE_RETURN, Key.Return);
|
||||
MapKey(SC.SDL_SCANCODE_KP_ENTER, Key.NumpadEnter);
|
||||
MapKey(SC.SDL_SCANCODE_BACKSPACE, Key.BackSpace);
|
||||
MapKey(SC.SDL_SCANCODE_TAB, Key.Tab);
|
||||
MapKey(SC.SDL_SCANCODE_PAGEUP, Key.PageUp);
|
||||
MapKey(SC.SDL_SCANCODE_PAGEDOWN, Key.PageDown);
|
||||
MapKey(SC.SDL_SCANCODE_END, Key.End);
|
||||
MapKey(SC.SDL_SCANCODE_HOME, Key.Home);
|
||||
MapKey(SC.SDL_SCANCODE_INSERT, Key.Insert);
|
||||
MapKey(SC.SDL_SCANCODE_DELETE, Key.Delete);
|
||||
MapKey(SC.SDL_SCANCODE_MINUS, Key.Minus);
|
||||
MapKey(SC.SDL_SCANCODE_KP_PLUS, Key.NumpadAdd);
|
||||
MapKey(SC.SDL_SCANCODE_KP_MINUS, Key.NumpadSubtract);
|
||||
MapKey(SC.SDL_SCANCODE_KP_DIVIDE, Key.NumpadDivide);
|
||||
MapKey(SC.SDL_SCANCODE_KP_MULTIPLY, Key.NumpadMultiply);
|
||||
MapKey(SC.SDL_SCANCODE_KP_DECIMAL, Key.NumpadDecimal);
|
||||
MapKey(SC.SDL_SCANCODE_LEFT, Key.Left);
|
||||
MapKey(SC.SDL_SCANCODE_RIGHT, Key.Right);
|
||||
MapKey(SC.SDL_SCANCODE_UP, Key.Up);
|
||||
MapKey(SC.SDL_SCANCODE_DOWN, Key.Down);
|
||||
MapKey(SC.SDL_SCANCODE_F1, Key.F1);
|
||||
MapKey(SC.SDL_SCANCODE_F2, Key.F2);
|
||||
MapKey(SC.SDL_SCANCODE_F3, Key.F3);
|
||||
MapKey(SC.SDL_SCANCODE_F4, Key.F4);
|
||||
MapKey(SC.SDL_SCANCODE_F5, Key.F5);
|
||||
MapKey(SC.SDL_SCANCODE_F6, Key.F6);
|
||||
MapKey(SC.SDL_SCANCODE_F7, Key.F7);
|
||||
MapKey(SC.SDL_SCANCODE_F8, Key.F8);
|
||||
MapKey(SC.SDL_SCANCODE_F9, Key.F9);
|
||||
MapKey(SC.SDL_SCANCODE_F10, Key.F10);
|
||||
MapKey(SC.SDL_SCANCODE_F11, Key.F11);
|
||||
MapKey(SC.SDL_SCANCODE_F12, Key.F12);
|
||||
MapKey(SC.SDL_SCANCODE_F13, Key.F13);
|
||||
MapKey(SC.SDL_SCANCODE_F14, Key.F14);
|
||||
MapKey(SC.SDL_SCANCODE_F15, Key.F15);
|
||||
MapKey(SC.SDL_SCANCODE_F16, Key.F16);
|
||||
MapKey(SC.SDL_SCANCODE_F17, Key.F17);
|
||||
MapKey(SC.SDL_SCANCODE_F18, Key.F18);
|
||||
MapKey(SC.SDL_SCANCODE_F19, Key.F19);
|
||||
MapKey(SC.SDL_SCANCODE_F20, Key.F20);
|
||||
MapKey(SC.SDL_SCANCODE_F21, Key.F21);
|
||||
MapKey(SC.SDL_SCANCODE_F22, Key.F22);
|
||||
MapKey(SC.SDL_SCANCODE_F23, Key.F23);
|
||||
MapKey(SC.SDL_SCANCODE_F24, Key.F24);
|
||||
MapKey(SC.SDL_SCANCODE_PAUSE, Key.Pause);
|
||||
|
||||
var keyMapReverse = new Dictionary<Key, SC>();
|
||||
|
||||
for (var code = 0; code < KeyMap.Length; code++)
|
||||
{
|
||||
var key = KeyMap[code];
|
||||
if (key != Key.Unknown)
|
||||
keyMapReverse[key] = (SC) code;
|
||||
}
|
||||
|
||||
KeyMapReverse = keyMapReverse.ToFrozenDictionary();
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static void MapKey(SC code, Key key)
|
||||
{
|
||||
KeyMap[(int)code] = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
133
Robust.Client/Graphics/Clyde/Windowing/Sdl3.Monitor.cs
Normal file
133
Robust.Client/Graphics/Clyde/Windowing/Sdl3.Monitor.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using SDL3;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl3WindowingImpl
|
||||
{
|
||||
// NOTE: SDL3 calls them "displays". GLFW calls them monitors. GLFW's is the one I'm going with.
|
||||
|
||||
private int _nextMonitorId = 1;
|
||||
|
||||
private readonly Dictionary<int, WinThreadMonitorReg> _winThreadMonitors = new();
|
||||
private readonly Dictionary<int, Sdl3MonitorReg> _monitors = new();
|
||||
|
||||
private unsafe void InitMonitors()
|
||||
{
|
||||
var displayList = (uint*)SDL.SDL_GetDisplays(out var count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
WinThreadSetupMonitor(displayList[i]);
|
||||
}
|
||||
|
||||
SDL.SDL_free((nint)displayList);
|
||||
|
||||
// Needed so that monitor creation events get processed.
|
||||
ProcessEvents();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private unsafe void WinThreadSetupMonitor(uint displayId)
|
||||
{
|
||||
var id = _nextMonitorId++;
|
||||
|
||||
var name = SDL.SDL_GetDisplayName(displayId);
|
||||
var modePtr = (SDL.SDL_DisplayMode**)SDL.SDL_GetFullscreenDisplayModes(displayId, out var modeCount);
|
||||
var curMode = (SDL.SDL_DisplayMode*)SDL.SDL_GetCurrentDisplayMode(displayId);
|
||||
var modes = new VideoMode[modeCount];
|
||||
for (var i = 0; i < modes.Length; i++)
|
||||
{
|
||||
modes[i] = ConvertVideoMode(in *modePtr[i]);
|
||||
}
|
||||
|
||||
SDL.SDL_free((nint)modePtr);
|
||||
|
||||
_winThreadMonitors.Add(id, new WinThreadMonitorReg { DisplayId = displayId });
|
||||
|
||||
if (SDL.SDL_GetPrimaryDisplay() == displayId)
|
||||
_clyde._primaryMonitorId = id;
|
||||
|
||||
SendEvent(new EventMonitorSetup
|
||||
{
|
||||
Id = id,
|
||||
DisplayId = displayId,
|
||||
Name = name,
|
||||
AllModes = modes,
|
||||
CurrentMode = ConvertVideoMode(in *curMode),
|
||||
});
|
||||
}
|
||||
|
||||
private static VideoMode ConvertVideoMode(in SDL.SDL_DisplayMode mode)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Width = (ushort)mode.w,
|
||||
Height = (ushort)mode.h,
|
||||
RefreshRate = (ushort)mode.refresh_rate,
|
||||
// TODO: set bits count based on format (I'm lazy)
|
||||
RedBits = 8,
|
||||
GreenBits = 8,
|
||||
BlueBits = 8,
|
||||
};
|
||||
}
|
||||
|
||||
private void ProcessSetupMonitor(EventMonitorSetup ev)
|
||||
{
|
||||
var impl = new MonitorHandle(
|
||||
ev.Id,
|
||||
ev.Name,
|
||||
(ev.CurrentMode.Width, ev.CurrentMode.Height),
|
||||
ev.CurrentMode.RefreshRate,
|
||||
ev.AllModes);
|
||||
|
||||
_clyde._monitorHandles.Add(ev.Id, impl);
|
||||
_monitors[ev.Id] = new Sdl3MonitorReg
|
||||
{
|
||||
DisplayId = ev.DisplayId,
|
||||
Handle = impl
|
||||
};
|
||||
}
|
||||
|
||||
private void WinThreadDestroyMonitor(uint displayId)
|
||||
{
|
||||
var monitorId = GetMonitorIdFromDisplayId(displayId);
|
||||
if (monitorId == 0)
|
||||
return;
|
||||
|
||||
_winThreadMonitors.Remove(monitorId);
|
||||
SendEvent(new EventMonitorDestroy { Id = monitorId });
|
||||
}
|
||||
|
||||
private void ProcessEventDestroyMonitor(EventMonitorDestroy ev)
|
||||
{
|
||||
_monitors.Remove(ev.Id);
|
||||
_clyde._monitorHandles.Remove(ev.Id);
|
||||
}
|
||||
|
||||
private int GetMonitorIdFromDisplayId(uint displayId)
|
||||
{
|
||||
foreach (var (id, monitorReg) in _winThreadMonitors)
|
||||
{
|
||||
if (monitorReg.DisplayId == displayId)
|
||||
{
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private sealed class Sdl3MonitorReg : MonitorReg
|
||||
{
|
||||
public uint DisplayId;
|
||||
}
|
||||
|
||||
private sealed class WinThreadMonitorReg
|
||||
{
|
||||
public uint DisplayId;
|
||||
}
|
||||
}
|
||||
}
|
||||
282
Robust.Client/Graphics/Clyde/Windowing/Sdl3.RawEvent.cs
Normal file
282
Robust.Client/Graphics/Clyde/Windowing/Sdl3.RawEvent.cs
Normal file
@@ -0,0 +1,282 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using SDL3;
|
||||
using ET = SDL3.SDL.SDL_EventType;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl3WindowingImpl
|
||||
{
|
||||
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
|
||||
private static unsafe byte EventWatch(void* userdata, SDL.SDL_Event* sdlevent)
|
||||
{
|
||||
var obj = (Sdl3WindowingImpl)GCHandle.FromIntPtr((IntPtr)userdata).Target!;
|
||||
|
||||
obj.ProcessSdl3Event(in *sdlevent);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void ProcessSdl3Event(in SDL.SDL_Event ev)
|
||||
{
|
||||
switch ((ET)ev.type)
|
||||
{
|
||||
case ET.SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
|
||||
ProcessSdl3EventWindowPixelSizeChanged(in ev.window);
|
||||
break;
|
||||
case ET.SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED:
|
||||
ProcessSdl3EventWindowDisplayScaleChanged(in ev.window);
|
||||
break;
|
||||
case ET.SDL_EVENT_WINDOW_CLOSE_REQUESTED:
|
||||
case ET.SDL_EVENT_WINDOW_MOUSE_ENTER:
|
||||
case ET.SDL_EVENT_WINDOW_MOUSE_LEAVE:
|
||||
case ET.SDL_EVENT_WINDOW_MINIMIZED:
|
||||
case ET.SDL_EVENT_WINDOW_RESTORED:
|
||||
case ET.SDL_EVENT_WINDOW_FOCUS_GAINED:
|
||||
case ET.SDL_EVENT_WINDOW_FOCUS_LOST:
|
||||
case ET.SDL_EVENT_WINDOW_MOVED:
|
||||
ProcessSdl3EventWindowMisc(in ev.window);
|
||||
break;
|
||||
case ET.SDL_EVENT_KEY_DOWN:
|
||||
case ET.SDL_EVENT_KEY_UP:
|
||||
ProcessSdl3KeyEvent(in ev.key);
|
||||
break;
|
||||
case ET.SDL_EVENT_TEXT_INPUT:
|
||||
ProcessSdl3EventTextInput(in ev.text);
|
||||
break;
|
||||
case ET.SDL_EVENT_TEXT_EDITING:
|
||||
ProcessSdl3EventTextEditing(in ev.edit);
|
||||
break;
|
||||
case ET.SDL_EVENT_KEYMAP_CHANGED:
|
||||
ProcessSdl3EventKeyMapChanged();
|
||||
break;
|
||||
case ET.SDL_EVENT_MOUSE_MOTION:
|
||||
ProcessSdl3EventMouseMotion(in ev.motion);
|
||||
break;
|
||||
case ET.SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||
case ET.SDL_EVENT_MOUSE_BUTTON_UP:
|
||||
ProcessSdl3EventMouseButton(in ev.button);
|
||||
break;
|
||||
case ET.SDL_EVENT_MOUSE_WHEEL:
|
||||
ProcessSdl3EventMouseWheel(in ev.wheel);
|
||||
break;
|
||||
case ET.SDL_EVENT_DISPLAY_ADDED:
|
||||
WinThreadSetupMonitor(ev.display.displayID);
|
||||
break;
|
||||
case ET.SDL_EVENT_DISPLAY_REMOVED:
|
||||
WinThreadDestroyMonitor(ev.display.displayID);
|
||||
break;
|
||||
case ET.SDL_EVENT_QUIT:
|
||||
ProcessSdl3EventQuit();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessSdl3EventQuit()
|
||||
{
|
||||
SendEvent(new EventQuit());
|
||||
}
|
||||
|
||||
private void ProcessSdl3EventMouseWheel(in SDL.SDL_MouseWheelEvent ev)
|
||||
{
|
||||
SendEvent(new EventWheel { WindowId = ev.windowID, XOffset = ev.x, YOffset = ev.y });
|
||||
}
|
||||
|
||||
private void ProcessSdl3EventMouseButton(in SDL.SDL_MouseButtonEvent ev)
|
||||
{
|
||||
var mods = SDL.SDL_GetModState();
|
||||
SendEvent(new EventMouseButton
|
||||
{
|
||||
WindowId = ev.windowID,
|
||||
Type = ev.type,
|
||||
Button = ev.button,
|
||||
Mods = mods
|
||||
});
|
||||
}
|
||||
|
||||
private void ProcessSdl3EventMouseMotion(in SDL.SDL_MouseMotionEvent ev)
|
||||
{
|
||||
SendEvent(new EventMouseMotion
|
||||
{
|
||||
WindowId = ev.windowID,
|
||||
X = ev.x,
|
||||
Y = ev.y,
|
||||
XRel = ev.xrel,
|
||||
YRel = ev.yrel
|
||||
});
|
||||
}
|
||||
|
||||
private unsafe void ProcessSdl3EventTextInput(in SDL.SDL_TextInputEvent ev)
|
||||
{
|
||||
var str = Marshal.PtrToStringUTF8((IntPtr)ev.text) ?? "";
|
||||
SendEvent(new EventText { WindowId = ev.windowID, Text = str });
|
||||
}
|
||||
|
||||
private unsafe void ProcessSdl3EventTextEditing(in SDL.SDL_TextEditingEvent ev)
|
||||
{
|
||||
var str = Marshal.PtrToStringUTF8((IntPtr)ev.text) ?? "";
|
||||
SendEvent(new EventTextEditing
|
||||
{
|
||||
WindowId = ev.windowID,
|
||||
Text = str,
|
||||
Start = ev.start,
|
||||
Length = ev.length
|
||||
});
|
||||
}
|
||||
|
||||
private void ProcessSdl3EventKeyMapChanged()
|
||||
{
|
||||
ReloadKeyMap();
|
||||
SendEvent(new EventKeyMapChanged());
|
||||
}
|
||||
|
||||
private void ProcessSdl3KeyEvent(in SDL.SDL_KeyboardEvent ev)
|
||||
{
|
||||
SendEvent(new EventKey
|
||||
{
|
||||
WindowId = ev.windowID,
|
||||
Scancode = ev.scancode,
|
||||
Type = ev.type,
|
||||
Repeat = ev.repeat,
|
||||
Mods = ev.mod,
|
||||
});
|
||||
}
|
||||
|
||||
private void ProcessSdl3EventWindowPixelSizeChanged(in SDL.SDL_WindowEvent ev)
|
||||
{
|
||||
var window = SDL.SDL_GetWindowFromID(ev.windowID);
|
||||
SDL.SDL_GetWindowSize(window, out var width, out var height);
|
||||
var fbW = ev.data1;
|
||||
var fbH = ev.data2;
|
||||
|
||||
SendEvent(new EventWindowPixelSize
|
||||
{
|
||||
WindowId = ev.windowID,
|
||||
Width = width,
|
||||
Height = height,
|
||||
FramebufferWidth = fbW,
|
||||
FramebufferHeight = fbH,
|
||||
});
|
||||
}
|
||||
|
||||
private void ProcessSdl3EventWindowDisplayScaleChanged(in SDL.SDL_WindowEvent ev)
|
||||
{
|
||||
var window = SDL.SDL_GetWindowFromID(ev.windowID);
|
||||
var scale = SDL.SDL_GetWindowDisplayScale(window);
|
||||
|
||||
SendEvent(new EventWindowContentScale { WindowId = ev.windowID, Scale = scale });
|
||||
}
|
||||
|
||||
private void ProcessSdl3EventWindowMisc(in SDL.SDL_WindowEvent ev)
|
||||
{
|
||||
SendEvent(new EventWindowMisc
|
||||
{
|
||||
WindowId = ev.windowID,
|
||||
EventId = ev.type,
|
||||
Data1 = ev.data1,
|
||||
Data2 = ev.data2
|
||||
});
|
||||
}
|
||||
|
||||
private abstract class EventBase;
|
||||
|
||||
private sealed class EventWindowCreate : EventBase
|
||||
{
|
||||
public required Sdl3WindowCreateResult Result;
|
||||
public required TaskCompletionSource<Sdl3WindowCreateResult> Tcs;
|
||||
}
|
||||
|
||||
private sealed class EventKey : EventBase
|
||||
{
|
||||
public uint WindowId;
|
||||
public SDL.SDL_Scancode Scancode;
|
||||
public ET Type;
|
||||
public bool Repeat;
|
||||
public SDL.SDL_Keymod Mods;
|
||||
}
|
||||
|
||||
private sealed class EventMouseMotion : EventBase
|
||||
{
|
||||
public uint WindowId;
|
||||
public float X;
|
||||
public float Y;
|
||||
public float XRel;
|
||||
public float YRel;
|
||||
}
|
||||
|
||||
private sealed class EventMouseButton : EventBase
|
||||
{
|
||||
public uint WindowId;
|
||||
public ET Type;
|
||||
public byte Button;
|
||||
public SDL.SDL_Keymod Mods;
|
||||
}
|
||||
|
||||
private sealed class EventText : EventBase
|
||||
{
|
||||
public uint WindowId;
|
||||
public required string Text;
|
||||
}
|
||||
|
||||
private sealed class EventTextEditing : EventBase
|
||||
{
|
||||
public uint WindowId;
|
||||
public required string Text;
|
||||
public int Start;
|
||||
public int Length;
|
||||
}
|
||||
|
||||
private sealed class EventWindowPixelSize : EventBase
|
||||
{
|
||||
public uint WindowId;
|
||||
public int Width;
|
||||
public int Height;
|
||||
public int FramebufferWidth;
|
||||
public int FramebufferHeight;
|
||||
}
|
||||
|
||||
private sealed class EventWindowContentScale : EventBase
|
||||
{
|
||||
public uint WindowId;
|
||||
public float Scale;
|
||||
}
|
||||
|
||||
private sealed class EventWheel : EventBase
|
||||
{
|
||||
public uint WindowId;
|
||||
public float XOffset;
|
||||
public float YOffset;
|
||||
}
|
||||
|
||||
// SDL_WindowEvents that don't need any handling on the window thread itself.
|
||||
private sealed class EventWindowMisc : EventBase
|
||||
{
|
||||
public uint WindowId;
|
||||
public ET EventId;
|
||||
public int Data1;
|
||||
public int Data2;
|
||||
}
|
||||
|
||||
private sealed class EventMonitorSetup : EventBase
|
||||
{
|
||||
public int Id;
|
||||
public uint DisplayId;
|
||||
public required string Name;
|
||||
public VideoMode CurrentMode;
|
||||
public required VideoMode[] AllModes;
|
||||
}
|
||||
|
||||
private sealed class EventMonitorDestroy : EventBase
|
||||
{
|
||||
public int Id;
|
||||
}
|
||||
|
||||
private sealed class EventKeyMapChanged : EventBase;
|
||||
|
||||
private sealed class EventQuit : EventBase;
|
||||
}
|
||||
}
|
||||
654
Robust.Client/Graphics/Clyde/Windowing/Sdl3.Window.cs
Normal file
654
Robust.Client/Graphics/Clyde/Windowing/Sdl3.Window.cs
Normal file
@@ -0,0 +1,654 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using SDL3;
|
||||
using TerraFX.Interop.Windows;
|
||||
using TerraFX.Interop.Xlib;
|
||||
using BOOL = TerraFX.Interop.Windows.BOOL;
|
||||
using Windows = TerraFX.Interop.Windows.Windows;
|
||||
using GLAttr = SDL3.SDL.SDL_GLAttr;
|
||||
using X11Window = TerraFX.Interop.Xlib.Window;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl3WindowingImpl
|
||||
{
|
||||
private int _nextWindowId = 1;
|
||||
|
||||
public (WindowReg?, string? error) WindowCreate(
|
||||
GLContextSpec? spec,
|
||||
WindowCreateParameters parameters,
|
||||
WindowReg? share,
|
||||
WindowReg? owner)
|
||||
{
|
||||
nint shareWindow = 0;
|
||||
nint shareContext = 0;
|
||||
if (share is Sdl3WindowReg shareReg)
|
||||
{
|
||||
shareWindow = shareReg.Sdl3Window;
|
||||
shareContext = shareReg.GlContext;
|
||||
}
|
||||
|
||||
nint ownerPtr = 0;
|
||||
if (owner is Sdl3WindowReg ownerReg)
|
||||
ownerPtr = ownerReg.Sdl3Window;
|
||||
|
||||
var task = SharedWindowCreate(spec, parameters, shareWindow, shareContext, ownerPtr);
|
||||
|
||||
// Block the main thread (to avoid stuff like texture uploads being problematic).
|
||||
WaitWindowCreate(task);
|
||||
|
||||
#pragma warning disable RA0004
|
||||
// Block above ensured task is done, this is safe.
|
||||
var result = task.Result;
|
||||
#pragma warning restore RA0004
|
||||
if (result.Reg != null)
|
||||
{
|
||||
result.Reg.Owner = result.Reg.Handle;
|
||||
}
|
||||
|
||||
return (result.Reg, result.Error);
|
||||
}
|
||||
|
||||
private void WaitWindowCreate(Task<Sdl3WindowCreateResult> windowTask)
|
||||
{
|
||||
while (!windowTask.IsCompleted)
|
||||
{
|
||||
// Keep processing events until the window task gives either an error or success.
|
||||
WaitEvents();
|
||||
ProcessEvents(single: true);
|
||||
}
|
||||
}
|
||||
|
||||
private Task<Sdl3WindowCreateResult> SharedWindowCreate(
|
||||
GLContextSpec? glSpec,
|
||||
WindowCreateParameters parameters,
|
||||
nint shareWindow,
|
||||
nint shareContext,
|
||||
nint owner)
|
||||
{
|
||||
//
|
||||
// IF YOU'RE WONDERING WHY THIS IS TASK-BASED:
|
||||
// I originally wanted this to be async so we could avoid blocking the main thread
|
||||
// while the OS takes its stupid 100~ms just to initialize a fucking GL context.
|
||||
// This doesn't *work* because
|
||||
// we have to release the GL context while the shared context is being created.
|
||||
// (at least on WGL, I didn't test other platforms and I don't care to.)
|
||||
// Not worth it to avoid a main thread blockage by allowing Clyde to temporarily release the GL context,
|
||||
// because rendering would be locked up *anyways*.
|
||||
//
|
||||
// Basically what I'm saying is that everything about OpenGL is a fucking mistake
|
||||
// and I should get on either Veldrid or Vulkan some time.
|
||||
// Probably Veldrid tbh.
|
||||
//
|
||||
|
||||
// Yes we ping-pong this TCS through the window thread and back, deal with it.
|
||||
var tcs = new TaskCompletionSource<Sdl3WindowCreateResult>();
|
||||
SendCmd(new CmdWinCreate
|
||||
{
|
||||
GLSpec = glSpec,
|
||||
Parameters = parameters,
|
||||
ShareWindow = shareWindow,
|
||||
ShareContext = shareContext,
|
||||
OwnerWindow = owner,
|
||||
Tcs = tcs
|
||||
});
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private static void FinishWindowCreate(EventWindowCreate ev)
|
||||
{
|
||||
ev.Tcs.TrySetResult(ev.Result);
|
||||
}
|
||||
|
||||
private void WinThreadWinCreate(CmdWinCreate cmd)
|
||||
{
|
||||
var (window, context) = CreateSdl3WindowForRenderer(
|
||||
cmd.GLSpec,
|
||||
cmd.Parameters,
|
||||
cmd.ShareWindow,
|
||||
cmd.ShareContext,
|
||||
cmd.OwnerWindow);
|
||||
|
||||
if (window == 0)
|
||||
{
|
||||
var err = SDL.SDL_GetError();
|
||||
|
||||
SendEvent(new EventWindowCreate
|
||||
{
|
||||
Result = new Sdl3WindowCreateResult { Error = err },
|
||||
Tcs = cmd.Tcs
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// We can't invoke the TCS directly from the windowing thread because:
|
||||
// * it'd hit the synchronization context,
|
||||
// which would make (blocking) main window init more annoying.
|
||||
// * it'd not be synchronized to other incoming window events correctly which might be icky.
|
||||
// So we send the TCS back to the game thread
|
||||
// which processes events in the correct order and has better control of stuff during init.
|
||||
var reg = WinThreadSetupWindow(window, context);
|
||||
|
||||
SendEvent(new EventWindowCreate
|
||||
{
|
||||
Result = new Sdl3WindowCreateResult { Reg = reg },
|
||||
Tcs = cmd.Tcs
|
||||
});
|
||||
}
|
||||
|
||||
private static void WinThreadWinDestroy(CmdWinDestroy cmd)
|
||||
{
|
||||
SDL.SDL_DestroyWindow(cmd.Window);
|
||||
}
|
||||
|
||||
private (nint window, nint context) CreateSdl3WindowForRenderer(
|
||||
GLContextSpec? spec,
|
||||
WindowCreateParameters parameters,
|
||||
nint shareWindow,
|
||||
nint shareContext,
|
||||
nint ownerWindow)
|
||||
{
|
||||
var createProps = SDL.SDL_CreateProperties();
|
||||
SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_HIDDEN_BOOLEAN, true);
|
||||
SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, true);
|
||||
SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN, true);
|
||||
|
||||
if (spec is { } s)
|
||||
{
|
||||
SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_OPENGL_BOOLEAN, true);
|
||||
|
||||
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_RED_SIZE, 8);
|
||||
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_GREEN_SIZE, 8);
|
||||
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_BLUE_SIZE, 8);
|
||||
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_ALPHA_SIZE, 8);
|
||||
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_STENCIL_SIZE, 8);
|
||||
SDL.SDL_GL_SetAttribute(
|
||||
GLAttr.SDL_GL_FRAMEBUFFER_SRGB_CAPABLE,
|
||||
s.Profile == GLContextProfile.Es ? 0 : 1);
|
||||
int ctxFlags = 0;
|
||||
#if DEBUG
|
||||
ctxFlags |= SDL.SDL_GL_CONTEXT_DEBUG_FLAG;
|
||||
#endif
|
||||
if (s.Profile == GLContextProfile.Core)
|
||||
ctxFlags |= SDL.SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG;
|
||||
|
||||
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_CONTEXT_FLAGS, (int)ctxFlags);
|
||||
|
||||
if (shareContext != 0)
|
||||
{
|
||||
SDL.SDL_GL_MakeCurrent(shareWindow, shareContext);
|
||||
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 0);
|
||||
}
|
||||
|
||||
var profile = s.Profile switch
|
||||
{
|
||||
GLContextProfile.Compatibility => SDL.SDL_GL_CONTEXT_PROFILE_COMPATIBILITY,
|
||||
GLContextProfile.Core => SDL.SDL_GL_CONTEXT_PROFILE_CORE,
|
||||
GLContextProfile.Es => SDL.SDL_GL_CONTEXT_PROFILE_ES,
|
||||
_ => SDL.SDL_GL_CONTEXT_PROFILE_COMPATIBILITY,
|
||||
};
|
||||
|
||||
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_CONTEXT_PROFILE_MASK, profile);
|
||||
SDL.SDL_SetHint(SDL.SDL_HINT_OPENGL_ES_DRIVER, s.CreationApi == GLContextCreationApi.Egl ? "1" : "0");
|
||||
|
||||
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_CONTEXT_MAJOR_VERSION, s.Major);
|
||||
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_CONTEXT_MINOR_VERSION, s.Minor);
|
||||
|
||||
if (s.CreationApi == GLContextCreationApi.Egl)
|
||||
WsiShared.EnsureEglAvailable();
|
||||
}
|
||||
|
||||
if (parameters.Fullscreen)
|
||||
SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_FULLSCREEN_BOOLEAN, true);
|
||||
|
||||
if ((parameters.Styles & OSWindowStyles.NoTitleBar) != 0)
|
||||
SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_BORDERLESS_BOOLEAN, true);
|
||||
|
||||
if (ownerWindow != 0)
|
||||
{
|
||||
SDL.SDL_SetPointerProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_PARENT_POINTER, ownerWindow);
|
||||
|
||||
if (parameters.StartupLocation == WindowStartupLocation.CenterOwner)
|
||||
{
|
||||
SDL.SDL_GetWindowSize(ownerWindow, out var parentW, out var parentH);
|
||||
SDL.SDL_GetWindowPosition(ownerWindow, out var parentX, out var parentY);
|
||||
|
||||
SDL.SDL_SetNumberProperty(
|
||||
createProps,
|
||||
SDL.SDL_PROP_WINDOW_CREATE_X_NUMBER,
|
||||
parentX + (parentW - parameters.Width) / 2);
|
||||
SDL.SDL_SetNumberProperty(
|
||||
createProps,
|
||||
SDL.SDL_PROP_WINDOW_CREATE_Y_NUMBER,
|
||||
parentY + (parentH - parameters.Height) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
SDL.SDL_SetNumberProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, parameters.Width);
|
||||
SDL.SDL_SetNumberProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, parameters.Height);
|
||||
SDL.SDL_SetStringProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_TITLE_STRING, parameters.Title);
|
||||
|
||||
// ---> CREATE <---
|
||||
var window = SDL.SDL_CreateWindowWithProperties(createProps);
|
||||
|
||||
SDL.SDL_DestroyProperties(createProps);
|
||||
|
||||
if (window == 0)
|
||||
return default;
|
||||
|
||||
nint glContext = SDL.SDL_GL_CreateContext(window);
|
||||
if (glContext == 0)
|
||||
{
|
||||
SDL.SDL_DestroyWindow(window);
|
||||
return default;
|
||||
}
|
||||
|
||||
if ((parameters.Styles & OSWindowStyles.NoTitleOptions) != 0)
|
||||
{
|
||||
var props = SDL.SDL_GetWindowProperties(window);
|
||||
switch (_videoDriver)
|
||||
{
|
||||
case SdlVideoDriver.Windows:
|
||||
{
|
||||
var hWnd = SDL.SDL_GetPointerProperty(
|
||||
props,
|
||||
SDL.SDL_PROP_WINDOW_WIN32_HWND_POINTER,
|
||||
0);
|
||||
WsiShared.SetWindowStyleNoTitleOptionsWindows((HWND)hWnd);
|
||||
break;
|
||||
}
|
||||
case SdlVideoDriver.X11:
|
||||
unsafe
|
||||
{
|
||||
var x11Display = (Display*)SDL.SDL_GetPointerProperty(
|
||||
props,
|
||||
SDL.SDL_PROP_WINDOW_X11_DISPLAY_POINTER,
|
||||
0);
|
||||
var x11Window = (X11Window)SDL.SDL_GetNumberProperty(
|
||||
props,
|
||||
SDL.SDL_PROP_WINDOW_X11_WINDOW_NUMBER,
|
||||
0);
|
||||
WsiShared.SetWindowStyleNoTitleOptionsX11(x11Display, x11Window);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
_sawmill.Warning("OSWindowStyles.NoTitleOptions not implemented on this video driver");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Monitors, window maximize.
|
||||
|
||||
// Make sure window thread doesn't keep hold of the GL context.
|
||||
SDL.SDL_GL_MakeCurrent(IntPtr.Zero, IntPtr.Zero);
|
||||
|
||||
if (parameters.Visible)
|
||||
SDL.SDL_ShowWindow(window);
|
||||
|
||||
return (window, glContext);
|
||||
}
|
||||
|
||||
private Sdl3WindowReg WinThreadSetupWindow(nint window, nint context)
|
||||
{
|
||||
var reg = new Sdl3WindowReg
|
||||
{
|
||||
Sdl3Window = window,
|
||||
GlContext = context,
|
||||
WindowId = SDL.SDL_GetWindowID(window),
|
||||
Id = new WindowId(_nextWindowId++)
|
||||
};
|
||||
var handle = new WindowHandle(_clyde, reg);
|
||||
reg.Handle = handle;
|
||||
|
||||
var windowProps = SDL.SDL_GetWindowProperties(window);
|
||||
switch (_videoDriver)
|
||||
{
|
||||
case SdlVideoDriver.Windows:
|
||||
reg.WindowsHwnd = SDL.SDL_GetPointerProperty(
|
||||
windowProps,
|
||||
SDL.SDL_PROP_WINDOW_WIN32_HWND_POINTER,
|
||||
0);
|
||||
break;
|
||||
case SdlVideoDriver.X11:
|
||||
reg.X11Display = SDL.SDL_GetPointerProperty(
|
||||
windowProps,
|
||||
SDL.SDL_PROP_WINDOW_X11_DISPLAY_POINTER,
|
||||
0);
|
||||
reg.X11Id = (uint)SDL.SDL_GetNumberProperty(windowProps, SDL.SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0);
|
||||
break;
|
||||
}
|
||||
|
||||
AssignWindowIconToWindow(window);
|
||||
|
||||
SDL.SDL_GetWindowSizeInPixels(window, out var fbW, out var fbH);
|
||||
reg.FramebufferSize = (fbW, fbH);
|
||||
|
||||
var scale = SDL.SDL_GetWindowDisplayScale(window);
|
||||
reg.WindowScale = new Vector2(scale, scale);
|
||||
|
||||
SDL.SDL_GetWindowSize(window, out var w, out var h);
|
||||
reg.PrevWindowSize = reg.WindowSize = (w, h);
|
||||
|
||||
SDL.SDL_GetWindowPosition(window, out var x, out var y);
|
||||
reg.PrevWindowPos = reg.WindowPos = (x, y);
|
||||
|
||||
reg.PixelRatio = reg.FramebufferSize / (Vector2)reg.WindowSize;
|
||||
|
||||
return reg;
|
||||
}
|
||||
|
||||
public void WindowDestroy(WindowReg window)
|
||||
{
|
||||
var reg = (Sdl3WindowReg)window;
|
||||
SendCmd(new CmdWinDestroy
|
||||
{
|
||||
Window = reg.Sdl3Window,
|
||||
HadOwner = window.Owner != null
|
||||
});
|
||||
}
|
||||
|
||||
public void UpdateMainWindowMode()
|
||||
{
|
||||
if (_clyde._mainWindow == null)
|
||||
return;
|
||||
|
||||
var win = (Sdl3WindowReg)_clyde._mainWindow;
|
||||
|
||||
if (_clyde._windowMode == WindowMode.Fullscreen)
|
||||
{
|
||||
win.PrevWindowSize = win.WindowSize;
|
||||
win.PrevWindowPos = win.WindowPos;
|
||||
|
||||
SendCmd(new CmdWinWinSetFullscreen
|
||||
{
|
||||
Window = win.Sdl3Window,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
SendCmd(new CmdWinSetWindowed
|
||||
{
|
||||
Window = win.Sdl3Window,
|
||||
Width = win.PrevWindowSize.X,
|
||||
Height = win.PrevWindowSize.Y,
|
||||
PosX = win.PrevWindowPos.X,
|
||||
PosY = win.PrevWindowPos.Y
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void WinThreadWinSetFullscreen(CmdWinWinSetFullscreen cmd)
|
||||
{
|
||||
SDL.SDL_SetWindowFullscreen(cmd.Window, true);
|
||||
}
|
||||
|
||||
private static void WinThreadWinSetWindowed(CmdWinSetWindowed cmd)
|
||||
{
|
||||
SDL.SDL_SetWindowFullscreen(cmd.Window, false);
|
||||
SDL.SDL_SetWindowSize(cmd.Window, cmd.Width, cmd.Height);
|
||||
SDL.SDL_SetWindowPosition(cmd.Window, cmd.PosX, cmd.PosY);
|
||||
}
|
||||
|
||||
public void WindowSetTitle(WindowReg window, string title)
|
||||
{
|
||||
SendCmd(new CmdWinSetTitle
|
||||
{
|
||||
Window = WinPtr(window),
|
||||
Title = title,
|
||||
});
|
||||
}
|
||||
|
||||
private static void WinThreadWinSetTitle(CmdWinSetTitle cmd)
|
||||
{
|
||||
SDL.SDL_SetWindowTitle(cmd.Window, cmd.Title);
|
||||
}
|
||||
|
||||
public void WindowSetMonitor(WindowReg window, IClydeMonitor monitor)
|
||||
{
|
||||
// API isn't really used and kinda wack, don't feel like figuring it out for SDL3 yet.
|
||||
_sawmill.Warning("WindowSetMonitor not implemented on SDL3");
|
||||
}
|
||||
|
||||
public void WindowSetSize(WindowReg window, Vector2i size)
|
||||
{
|
||||
SendCmd(new CmdWinSetSize { Window = WinPtr(window), W = size.X, H = size.Y });
|
||||
}
|
||||
|
||||
public void WindowSetVisible(WindowReg window, bool visible)
|
||||
{
|
||||
window.IsVisible = visible;
|
||||
SendCmd(new CmdWinSetVisible { Window = WinPtr(window), Visible = visible });
|
||||
}
|
||||
|
||||
private static void WinThreadWinSetSize(CmdWinSetSize cmd)
|
||||
{
|
||||
SDL.SDL_SetWindowSize(cmd.Window, cmd.W, cmd.H);
|
||||
}
|
||||
|
||||
private static void WinThreadWinSetVisible(CmdWinSetVisible cmd)
|
||||
{
|
||||
if (cmd.Visible)
|
||||
SDL.SDL_ShowWindow(cmd.Window);
|
||||
else
|
||||
SDL.SDL_HideWindow(cmd.Window);
|
||||
}
|
||||
|
||||
public void WindowRequestAttention(WindowReg window)
|
||||
{
|
||||
SendCmd(new CmdWinRequestAttention { Window = WinPtr(window) });
|
||||
}
|
||||
|
||||
private void WinThreadWinRequestAttention(CmdWinRequestAttention cmd)
|
||||
{
|
||||
var res = SDL.SDL_FlashWindow(cmd.Window, SDL.SDL_FlashOperation.SDL_FLASH_UNTIL_FOCUSED);
|
||||
if (!res)
|
||||
_sawmill.Error("Failed to flash window: {error}", SDL.SDL_GetError());
|
||||
}
|
||||
|
||||
public unsafe void WindowSwapBuffers(WindowReg window)
|
||||
{
|
||||
var reg = (Sdl3WindowReg)window;
|
||||
var windowPtr = WinPtr(reg);
|
||||
|
||||
// On Windows, SwapBuffers does not correctly sync to the DWM compositor.
|
||||
// This means OpenGL vsync is effectively broken by default on Windows.
|
||||
// We manually sync via DwmFlush(). GLFW does this automatically, SDL3 does not.
|
||||
//
|
||||
// Windows DwmFlush logic partly taken from:
|
||||
// https://github.com/love2d/love/blob/5175b0d1b599ea4c7b929f6b4282dd379fa116b8/src/modules/window/sdl/Window.cpp#L1018
|
||||
// https://github.com/glfw/glfw/blob/d3ede7b6847b66cf30b067214b2b4b126d4c729b/src/wgl_context.c#L321-L340
|
||||
// See also: https://github.com/libsdl-org/SDL/issues/5797
|
||||
|
||||
var dwmFlush = false;
|
||||
var swapInterval = 0;
|
||||
|
||||
if (OperatingSystem.IsWindows() && !reg.Fullscreen && reg.SwapInterval > 0)
|
||||
{
|
||||
BOOL compositing;
|
||||
// 6.2 is Windows 8
|
||||
// https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_osversioninfoexw
|
||||
if (OperatingSystem.IsWindowsVersionAtLeast(6, 2)
|
||||
|| Windows.SUCCEEDED(Windows.DwmIsCompositionEnabled(&compositing)) && compositing)
|
||||
{
|
||||
var curCtx = SDL.SDL_GL_GetCurrentContext();
|
||||
var curWin = SDL.SDL_GL_GetCurrentWindow();
|
||||
|
||||
if (curCtx != reg.GlContext || curWin != reg.Sdl3Window)
|
||||
throw new InvalidOperationException("Window context must be current!");
|
||||
|
||||
SDL.SDL_GL_SetSwapInterval(0);
|
||||
dwmFlush = true;
|
||||
swapInterval = reg.SwapInterval;
|
||||
}
|
||||
}
|
||||
|
||||
SDL.SDL_GL_SwapWindow(windowPtr);
|
||||
|
||||
if (dwmFlush)
|
||||
{
|
||||
var i = swapInterval;
|
||||
while (i-- > 0)
|
||||
{
|
||||
Windows.DwmFlush();
|
||||
}
|
||||
|
||||
SDL.SDL_GL_SetSwapInterval(swapInterval);
|
||||
}
|
||||
}
|
||||
|
||||
public uint? WindowGetX11Id(WindowReg window)
|
||||
{
|
||||
CheckWindowDisposed(window);
|
||||
|
||||
if (_videoDriver != SdlVideoDriver.X11)
|
||||
return null;
|
||||
|
||||
var reg = (Sdl3WindowReg)window;
|
||||
return reg.X11Id;
|
||||
}
|
||||
|
||||
public nint? WindowGetX11Display(WindowReg window)
|
||||
{
|
||||
CheckWindowDisposed(window);
|
||||
|
||||
if (_videoDriver != SdlVideoDriver.X11)
|
||||
return null;
|
||||
|
||||
var reg = (Sdl3WindowReg)window;
|
||||
return reg.X11Display;
|
||||
}
|
||||
|
||||
public nint? WindowGetWin32Window(WindowReg window)
|
||||
{
|
||||
CheckWindowDisposed(window);
|
||||
|
||||
if (_videoDriver != SdlVideoDriver.Windows)
|
||||
return null;
|
||||
|
||||
var reg = (Sdl3WindowReg)window;
|
||||
return reg.WindowsHwnd;
|
||||
}
|
||||
|
||||
public void RunOnWindowThread(Action a)
|
||||
{
|
||||
SendCmd(new CmdRunAction { Action = a });
|
||||
}
|
||||
|
||||
public void TextInputSetRect(WindowReg reg, UIBox2i rect, int cursor)
|
||||
{
|
||||
SendCmd(new CmdTextInputSetRect
|
||||
{
|
||||
Window = WinPtr(reg),
|
||||
Rect = new SDL.SDL_Rect
|
||||
{
|
||||
x = rect.Left,
|
||||
y = rect.Top,
|
||||
w = rect.Width,
|
||||
h = rect.Height
|
||||
},
|
||||
Cursor = cursor
|
||||
});
|
||||
}
|
||||
|
||||
private static void WinThreadSetTextInputRect(CmdTextInputSetRect cmdTextInput)
|
||||
{
|
||||
var rect = cmdTextInput.Rect;
|
||||
SDL.SDL_SetTextInputArea(cmdTextInput.Window, ref rect, cmdTextInput.Cursor);
|
||||
}
|
||||
|
||||
public void TextInputStart(WindowReg reg)
|
||||
{
|
||||
SendCmd(new CmdTextInputStart { Window = WinPtr(reg) });
|
||||
}
|
||||
|
||||
private static void WinThreadStartTextInput(CmdTextInputStart cmd)
|
||||
{
|
||||
SDL.SDL_StartTextInput(cmd.Window);
|
||||
}
|
||||
|
||||
public void TextInputStop(WindowReg reg)
|
||||
{
|
||||
SendCmd(new CmdTextInputStop { Window = WinPtr(reg) });
|
||||
}
|
||||
|
||||
private static void WinThreadStopTextInput(CmdTextInputStop cmd)
|
||||
{
|
||||
SDL.SDL_StopTextInput(cmd.Window);
|
||||
}
|
||||
|
||||
public void ClipboardSetText(WindowReg mainWindow, string text)
|
||||
{
|
||||
SendCmd(new CmdSetClipboard { Text = text });
|
||||
}
|
||||
|
||||
private void WinThreadSetClipboard(CmdSetClipboard cmd)
|
||||
{
|
||||
var res = SDL.SDL_SetClipboardText(cmd.Text);
|
||||
if (res)
|
||||
_sawmill.Error("Failed to set clipboard text: {error}", SDL.SDL_GetError());
|
||||
}
|
||||
|
||||
public Task<string> ClipboardGetText(WindowReg mainWindow)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<string>();
|
||||
SendCmd(new CmdGetClipboard { Tcs = tcs });
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private static void WinThreadGetClipboard(CmdGetClipboard cmd)
|
||||
{
|
||||
cmd.Tcs.TrySetResult(SDL.SDL_GetClipboardText());
|
||||
}
|
||||
|
||||
private static void CheckWindowDisposed(WindowReg reg)
|
||||
{
|
||||
if (reg.IsDisposed)
|
||||
throw new ObjectDisposedException("Window disposed");
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static nint WinPtr(WindowReg reg) => ((Sdl3WindowReg)reg).Sdl3Window;
|
||||
|
||||
private WindowReg? FindWindow(uint windowId)
|
||||
{
|
||||
foreach (var windowReg in _clyde._windows)
|
||||
{
|
||||
var glfwReg = (Sdl3WindowReg)windowReg;
|
||||
if (glfwReg.WindowId == windowId)
|
||||
return windowReg;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private sealed class Sdl3WindowReg : WindowReg
|
||||
{
|
||||
public nint Sdl3Window;
|
||||
public uint WindowId;
|
||||
public nint GlContext;
|
||||
#pragma warning disable CS0649
|
||||
public bool Fullscreen;
|
||||
#pragma warning restore CS0649
|
||||
public int SwapInterval;
|
||||
|
||||
// Kept around to avoid it being GCd.
|
||||
public CursorImpl? Cursor;
|
||||
|
||||
public nint WindowsHwnd;
|
||||
public nint X11Display;
|
||||
public uint X11Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
105
Robust.Client/Graphics/Clyde/Windowing/Sdl3.WindowIcons.cs
Normal file
105
Robust.Client/Graphics/Clyde/Windowing/Sdl3.WindowIcons.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Client.Utility;
|
||||
using SDL3;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using TerraFX.Interop.Windows;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed unsafe partial class Sdl3WindowingImpl
|
||||
{
|
||||
// Experimentally on my system, SM_CXICON is 32.
|
||||
// I doubt MS is ever changing that, so...
|
||||
// I wish SDL would take care of this instead of us having to figure out what the "main" icon is. Ugh.
|
||||
private const int MainWindowIconSize = 32;
|
||||
|
||||
// Writing this out like this makes me realize we're spending multiple hundred KBs on storing the window icon.
|
||||
// You know, come to think about it, what if we used LZ4 or Zstd to compress the window icon stored here?
|
||||
// This is absolutely not worth the optimization but hilarious for me to think about.
|
||||
|
||||
// The surface used for the window icon.
|
||||
// This may store additional surfaces as alternate forms.
|
||||
private nint _windowIconSurface;
|
||||
// The data for all the window icons surfaces.
|
||||
// Must be kept around! Pinned!
|
||||
// ReSharper disable once CollectionNeverQueried.Local
|
||||
private byte[][]? _windowIconData;
|
||||
|
||||
private void LoadWindowIcons()
|
||||
{
|
||||
// Sort such that closest to 64 is first.
|
||||
// SDL3 doesn't "figure it out itself" as much as GLFW does, which sucks.
|
||||
var icons = _clyde.LoadWindowIcons().OrderBy(i => Math.Abs(i.Width - MainWindowIconSize)).ToArray();
|
||||
if (icons.Length == 0)
|
||||
{
|
||||
// No window icons at all!
|
||||
return;
|
||||
}
|
||||
|
||||
_windowIconData = new byte[icons.Length][];
|
||||
|
||||
var mainImg = icons[0];
|
||||
|
||||
_sawmill.Verbose(
|
||||
"Have {iconCount} window icons available, choosing {mainIconWidth}x{mainIconHeight} as main",
|
||||
icons.Length,
|
||||
mainImg.Width,
|
||||
mainImg.Height);
|
||||
|
||||
(_windowIconSurface, var mainData) = CreateSurfaceFromImage(mainImg);
|
||||
_windowIconData[0] = mainData;
|
||||
|
||||
for (var i = 1; i < icons.Length; i++)
|
||||
{
|
||||
var (surface, data) = CreateSurfaceFromImage(icons[i]);
|
||||
_windowIconData[i] = data;
|
||||
SDL.SDL_AddSurfaceAlternateImage(_windowIconSurface, surface);
|
||||
// Kept alive by the main surface.
|
||||
SDL.SDL_DestroySurface(surface);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
static (nint, byte[]) CreateSurfaceFromImage(Image<Rgba32> img)
|
||||
{
|
||||
var span = MemoryMarshal.AsBytes(img.GetPixelSpan());
|
||||
var copied = GC.AllocateUninitializedArray<byte>(span.Length, pinned: true);
|
||||
|
||||
span.CopyTo(copied);
|
||||
|
||||
IntPtr surface;
|
||||
fixed (byte* ptr = copied)
|
||||
{
|
||||
surface = SDL.SDL_CreateSurfaceFrom(
|
||||
img.Width,
|
||||
img.Height,
|
||||
SDL.SDL_PixelFormat.SDL_PIXELFORMAT_ABGR8888,
|
||||
(IntPtr)ptr,
|
||||
sizeof(Rgba32) * img.Width);
|
||||
}
|
||||
|
||||
return (surface, copied);
|
||||
}
|
||||
}
|
||||
|
||||
private void DestroyWindowIcons()
|
||||
{
|
||||
SDL.SDL_DestroySurface(_windowIconSurface);
|
||||
_windowIconSurface = 0;
|
||||
_windowIconData = null;
|
||||
}
|
||||
|
||||
private void AssignWindowIconToWindow(nint window)
|
||||
{
|
||||
if (_windowIconSurface == 0)
|
||||
return;
|
||||
|
||||
SDL.SDL_SetWindowIcon(window, (nint) _windowIconSurface);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,15 @@ using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Maths;
|
||||
using SDL3;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using static SDL2.SDL;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl2WindowingImpl
|
||||
private sealed partial class Sdl3WindowingImpl
|
||||
{
|
||||
private bool _windowingRunning;
|
||||
private ChannelWriter<CmdBase> _cmdWriter = default!;
|
||||
@@ -29,34 +29,34 @@ internal partial class Clyde
|
||||
|
||||
while (_windowingRunning)
|
||||
{
|
||||
var res = SDL_WaitEvent(out Unsafe.NullRef<SDL_Event>());
|
||||
if (res == 0)
|
||||
var res = SDL.SDL_WaitEvent(out Unsafe.NullRef<SDL.SDL_Event>());
|
||||
if (!res)
|
||||
{
|
||||
_sawmill.Error("Error while waiting on SDL2 events: {error}", SDL_GetError());
|
||||
_sawmill.Error("Error while waiting on SDL3 events: {error}", SDL.SDL_GetError());
|
||||
continue; // Assume it's a transient failure?
|
||||
}
|
||||
|
||||
while (SDL_PollEvent(out _) == 1)
|
||||
while (SDL.SDL_PollEvent(out _))
|
||||
{
|
||||
// We let callbacks process all events because of stuff like resizing.
|
||||
}
|
||||
|
||||
while (_cmdReader.TryRead(out var cmd) && _windowingRunning)
|
||||
{
|
||||
ProcessSdl2Cmd(cmd);
|
||||
ProcessSdl3Cmd(cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void PollEvents()
|
||||
{
|
||||
while (SDL_PollEvent(out _) == 1)
|
||||
while (SDL.SDL_PollEvent(out _))
|
||||
{
|
||||
// We let callbacks process all events because of stuff like resizing.
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessSdl2Cmd(CmdBase cmdb)
|
||||
private void ProcessSdl3Cmd(CmdBase cmdb)
|
||||
{
|
||||
switch (cmdb)
|
||||
{
|
||||
@@ -113,20 +113,24 @@ internal partial class Clyde
|
||||
WinThreadWinCursorSet(cmd);
|
||||
break;
|
||||
|
||||
case CmdWinWinSetMode cmd:
|
||||
WinThreadWinSetMode(cmd);
|
||||
case CmdWinWinSetFullscreen cmd:
|
||||
WinThreadWinSetFullscreen(cmd);
|
||||
break;
|
||||
|
||||
case CmdWinSetWindowed cmd:
|
||||
WinThreadWinSetWindowed(cmd);
|
||||
break;
|
||||
|
||||
case CmdTextInputSetRect cmd:
|
||||
WinThreadSetTextInputRect(cmd);
|
||||
break;
|
||||
|
||||
case CmdTextInputStart:
|
||||
WinThreadStartTextInput();
|
||||
case CmdTextInputStart cmd:
|
||||
WinThreadStartTextInput(cmd);
|
||||
break;
|
||||
|
||||
case CmdTextInputStop:
|
||||
WinThreadStopTextInput();
|
||||
case CmdTextInputStop cmd:
|
||||
WinThreadStopTextInput(cmd);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -171,25 +175,25 @@ internal partial class Clyde
|
||||
_eventWriter = eventChannel.Writer;
|
||||
}
|
||||
|
||||
private unsafe void SendCmd(CmdBase cmd)
|
||||
private void SendCmd(CmdBase cmd)
|
||||
{
|
||||
if (_clyde._threadWindowApi)
|
||||
{
|
||||
_cmdWriter.TryWrite(cmd);
|
||||
|
||||
SDL_Event ev = default;
|
||||
ev.type = (SDL_EventType)_sdlEventWakeup;
|
||||
SDL.SDL_Event ev = default;
|
||||
ev.type = _sdlEventWakeup;
|
||||
// Post empty event to unstuck WaitEvents if necessary.
|
||||
// This self-registered event type is ignored by the winthread, but it'll still wake it up.
|
||||
|
||||
// This can fail if the event queue is full.
|
||||
// That's not really a problem since in that case something else will be sure to wake the thread up anyways.
|
||||
// NOTE: have to avoid using PushEvents since that invokes callbacks which causes a deadlock.
|
||||
SDL_PeepEvents(&ev, 1, SDL_eventaction.SDL_ADDEVENT, ev.type, ev.type);
|
||||
SDL.SDL_PeepEvents(new Span<SDL.SDL_Event>(ref ev), 1, SDL.SDL_EventAction.SDL_ADDEVENT, ev.type, ev.type);
|
||||
}
|
||||
else
|
||||
{
|
||||
ProcessSdl2Cmd(cmd);
|
||||
ProcessSdl3Cmd(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,93 +215,119 @@ internal partial class Clyde
|
||||
}
|
||||
|
||||
|
||||
private abstract record CmdBase;
|
||||
private abstract class CmdBase;
|
||||
|
||||
private sealed record CmdTerminate : CmdBase;
|
||||
private sealed class CmdTerminate : CmdBase;
|
||||
|
||||
private sealed record CmdWinCreate(
|
||||
GLContextSpec? GLSpec,
|
||||
WindowCreateParameters Parameters,
|
||||
nint ShareWindow,
|
||||
nint ShareContext,
|
||||
nint OwnerWindow,
|
||||
TaskCompletionSource<Sdl2WindowCreateResult> Tcs
|
||||
) : CmdBase;
|
||||
private sealed class CmdWinCreate : CmdBase
|
||||
{
|
||||
public required GLContextSpec? GLSpec;
|
||||
public required WindowCreateParameters Parameters;
|
||||
public required nint ShareWindow;
|
||||
public required nint ShareContext;
|
||||
public required nint OwnerWindow;
|
||||
public required TaskCompletionSource<Sdl3WindowCreateResult> Tcs;
|
||||
}
|
||||
|
||||
private sealed record CmdWinDestroy(
|
||||
nint Window,
|
||||
bool HadOwner
|
||||
) : CmdBase;
|
||||
private sealed class CmdWinDestroy : CmdBase
|
||||
{
|
||||
public nint Window;
|
||||
public bool HadOwner;
|
||||
}
|
||||
|
||||
private sealed record Sdl2WindowCreateResult(
|
||||
Sdl2WindowReg? Reg,
|
||||
string? Error
|
||||
);
|
||||
private sealed class Sdl3WindowCreateResult
|
||||
{
|
||||
public Sdl3WindowReg? Reg;
|
||||
public string? Error;
|
||||
}
|
||||
|
||||
private sealed record CmdRunAction(
|
||||
Action Action
|
||||
) : CmdBase;
|
||||
private sealed class CmdRunAction : CmdBase
|
||||
{
|
||||
public required Action Action;
|
||||
}
|
||||
|
||||
private sealed record CmdSetClipboard(
|
||||
string Text
|
||||
) : CmdBase;
|
||||
private sealed class CmdSetClipboard : CmdBase
|
||||
{
|
||||
public required string Text;
|
||||
}
|
||||
|
||||
private sealed record CmdGetClipboard(
|
||||
TaskCompletionSource<string> Tcs
|
||||
) : CmdBase;
|
||||
private sealed class CmdGetClipboard : CmdBase
|
||||
{
|
||||
public required TaskCompletionSource<string> Tcs;
|
||||
}
|
||||
|
||||
private sealed record CmdWinRequestAttention(
|
||||
nint Window
|
||||
) : CmdBase;
|
||||
private sealed class CmdWinRequestAttention : CmdBase
|
||||
{
|
||||
public nint Window;
|
||||
}
|
||||
|
||||
private sealed record CmdWinSetSize(
|
||||
nint Window,
|
||||
int W, int H
|
||||
) : CmdBase;
|
||||
private sealed class CmdWinSetSize : CmdBase
|
||||
{
|
||||
public nint Window;
|
||||
public int W;
|
||||
public int H;
|
||||
}
|
||||
|
||||
private sealed record CmdWinSetVisible(
|
||||
nint Window,
|
||||
bool Visible
|
||||
) : CmdBase;
|
||||
private sealed class CmdWinSetVisible : CmdBase
|
||||
{
|
||||
public nint Window;
|
||||
public bool Visible;
|
||||
}
|
||||
|
||||
private sealed record CmdWinSetTitle(
|
||||
nint Window,
|
||||
string Title
|
||||
) : CmdBase;
|
||||
private sealed class CmdWinSetTitle : CmdBase
|
||||
{
|
||||
public nint Window;
|
||||
public required string Title;
|
||||
}
|
||||
|
||||
private sealed record CmdCursorCreate(
|
||||
Image<Rgba32> Bytes,
|
||||
Vector2i Hotspot,
|
||||
ClydeHandle Cursor
|
||||
) : CmdBase;
|
||||
private sealed class CmdCursorCreate : CmdBase
|
||||
{
|
||||
public required Image<Rgba32> Bytes;
|
||||
public Vector2i Hotspot;
|
||||
public ClydeHandle Cursor;
|
||||
}
|
||||
|
||||
private sealed record CmdCursorDestroy(
|
||||
ClydeHandle Cursor
|
||||
) : CmdBase;
|
||||
private sealed class CmdCursorDestroy : CmdBase
|
||||
{
|
||||
public ClydeHandle Cursor;
|
||||
}
|
||||
|
||||
private sealed record CmdWinCursorSet(
|
||||
nint Window,
|
||||
ClydeHandle Cursor
|
||||
) : CmdBase;
|
||||
private sealed class CmdWinCursorSet : CmdBase
|
||||
{
|
||||
public nint Window;
|
||||
public ClydeHandle Cursor;
|
||||
}
|
||||
|
||||
private sealed record CmdWinWinSetMode(
|
||||
nint Window,
|
||||
WindowMode Mode
|
||||
) : CmdBase;
|
||||
private sealed class CmdWinWinSetFullscreen : CmdBase
|
||||
{
|
||||
public nint Window;
|
||||
}
|
||||
|
||||
private sealed class CmdWinSetWindowed : CmdBase
|
||||
{
|
||||
public nint Window;
|
||||
public int Width;
|
||||
public int Height;
|
||||
public int PosX;
|
||||
public int PosY;
|
||||
}
|
||||
|
||||
// IME
|
||||
private sealed record CmdTextInputStart : CmdBase
|
||||
private sealed class CmdTextInputStart : CmdBase
|
||||
{
|
||||
public static readonly CmdTextInputStart Instance = new();
|
||||
public nint Window;
|
||||
}
|
||||
|
||||
private sealed record CmdTextInputStop : CmdBase
|
||||
private sealed class CmdTextInputStop : CmdBase
|
||||
{
|
||||
public static readonly CmdTextInputStop Instance = new();
|
||||
public nint Window;
|
||||
}
|
||||
|
||||
private sealed record CmdTextInputSetRect(
|
||||
SDL_Rect Rect
|
||||
) : CmdBase;
|
||||
private sealed class CmdTextInputSetRect : CmdBase
|
||||
{
|
||||
public nint Window;
|
||||
public SDL.SDL_Rect Rect;
|
||||
public int Cursor;
|
||||
}
|
||||
}
|
||||
}
|
||||
226
Robust.Client/Graphics/Clyde/Windowing/Sdl3.cs
Normal file
226
Robust.Client/Graphics/Clyde/Windowing/Sdl3.cs
Normal file
@@ -0,0 +1,226 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using SDL3;
|
||||
using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl3WindowingImpl : IWindowingImpl
|
||||
{
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
private readonly Clyde _clyde;
|
||||
private GCHandle _selfGCHandle;
|
||||
|
||||
private readonly ISawmill _sawmill;
|
||||
private readonly ISawmill _sawmillSdl3;
|
||||
|
||||
private SdlVideoDriver _videoDriver;
|
||||
|
||||
public Sdl3WindowingImpl(Clyde clyde, IDependencyCollection deps)
|
||||
{
|
||||
_clyde = clyde;
|
||||
deps.InjectDependencies(this, true);
|
||||
|
||||
_sawmill = _logManager.GetSawmill("clyde.win");
|
||||
_sawmillSdl3 = _logManager.GetSawmill("clyde.win.sdl3");
|
||||
}
|
||||
|
||||
public bool Init()
|
||||
{
|
||||
InitChannels();
|
||||
|
||||
if (!InitSdl3())
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe bool InitSdl3()
|
||||
{
|
||||
CheckThreadApartment();
|
||||
|
||||
_selfGCHandle = GCHandle.Alloc(this, GCHandleType.Normal);
|
||||
|
||||
SDL.SDL_SetLogPriorities(SDL.SDL_LogPriority.SDL_LOG_PRIORITY_VERBOSE);
|
||||
SDL.SDL_SetLogOutputFunction(&LogOutputFunction, (void*) GCHandle.ToIntPtr(_selfGCHandle));
|
||||
|
||||
SDL.SDL_SetHint(SDL.SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
|
||||
SDL.SDL_SetHint(SDL.SDL_HINT_IME_IMPLEMENTED_UI, "composition");
|
||||
|
||||
// SDL3's GameInput support is currently broken and leaving it on
|
||||
// causes a "that operation is not supported" error to be logged on startup.
|
||||
// https://github.com/libsdl-org/SDL/issues/11813
|
||||
SDL.SDL_SetHint(SDL.SDL_HINT_WINDOWS_GAMEINPUT, "0");
|
||||
|
||||
var res = SDL.SDL_Init(SDL.SDL_InitFlags.SDL_INIT_VIDEO | SDL.SDL_InitFlags.SDL_INIT_EVENTS);
|
||||
if (!res)
|
||||
{
|
||||
_sawmill.Fatal("Failed to initialize SDL3: {error}", SDL.SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
var version = SDL.SDL_GetVersion();
|
||||
var videoDriver = SDL.SDL_GetCurrentVideoDriver();
|
||||
_sawmill.Debug(
|
||||
"SDL3 initialized, version: {major}.{minor}.{patch}, video driver: {videoDriver}",
|
||||
SDL.SDL_VERSIONNUM_MAJOR(version),
|
||||
SDL.SDL_VERSIONNUM_MINOR(version),
|
||||
SDL.SDL_VERSIONNUM_MICRO(version),
|
||||
videoDriver);
|
||||
|
||||
LoadSdl3VideoDriver();
|
||||
|
||||
_sdlEventWakeup = SDL.SDL_RegisterEvents(1);
|
||||
if (_sdlEventWakeup == 0)
|
||||
throw new InvalidOperationException("SDL_RegisterEvents failed");
|
||||
|
||||
LoadWindowIcons();
|
||||
InitCursors();
|
||||
InitMonitors();
|
||||
ReloadKeyMap();
|
||||
|
||||
SDL.SDL_AddEventWatch(&EventWatch, (void*) GCHandle.ToIntPtr(_selfGCHandle));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CheckThreadApartment()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
return;
|
||||
|
||||
if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA)
|
||||
_sawmill.Error("Thread apartment state isn't STA. This will likely break things!!!");
|
||||
}
|
||||
|
||||
private void LoadSdl3VideoDriver()
|
||||
{
|
||||
_videoDriver = SDL.SDL_GetCurrentVideoDriver() switch
|
||||
{
|
||||
"windows" => SdlVideoDriver.Windows,
|
||||
"x11" => SdlVideoDriver.X11,
|
||||
_ => SdlVideoDriver.Other,
|
||||
};
|
||||
}
|
||||
|
||||
public unsafe void Shutdown()
|
||||
{
|
||||
if (_selfGCHandle != default)
|
||||
{
|
||||
SDL.SDL_RemoveEventWatch(&EventWatch, (void*) GCHandle.ToIntPtr(_selfGCHandle));
|
||||
_selfGCHandle.Free();
|
||||
_selfGCHandle = default;
|
||||
}
|
||||
|
||||
SDL.SDL_SetLogOutputFunction(null, null);
|
||||
|
||||
if (SDL.SDL_WasInit(0) != 0)
|
||||
{
|
||||
_sawmill.Debug("Terminating SDL3");
|
||||
SDL.SDL_Quit();
|
||||
}
|
||||
}
|
||||
|
||||
public void FlushDispose()
|
||||
{
|
||||
// Not currently used
|
||||
}
|
||||
|
||||
public void GLMakeContextCurrent(WindowReg? reg)
|
||||
{
|
||||
SDL.SDLBool res;
|
||||
if (reg is Sdl3WindowReg sdlReg)
|
||||
res = SDL.SDL_GL_MakeCurrent(sdlReg.Sdl3Window, sdlReg.GlContext);
|
||||
else
|
||||
res = SDL.SDL_GL_MakeCurrent(IntPtr.Zero, IntPtr.Zero);
|
||||
|
||||
if (!res)
|
||||
_sawmill.Error("SDL_GL_MakeCurrent failed: {error}", SDL.SDL_GetError());
|
||||
}
|
||||
|
||||
public void GLSwapInterval(WindowReg reg, int interval)
|
||||
{
|
||||
((Sdl3WindowReg)reg).SwapInterval = interval;
|
||||
SDL.SDL_GL_SetSwapInterval(interval);
|
||||
}
|
||||
|
||||
public unsafe void* GLGetProcAddress(string procName)
|
||||
{
|
||||
return (void*) SDL.SDL_GL_GetProcAddress(procName);
|
||||
}
|
||||
|
||||
public string GetDescription()
|
||||
{
|
||||
var version = SDL.SDL_GetVersion();
|
||||
|
||||
var major = SDL.SDL_VERSIONNUM_MAJOR(version);
|
||||
var minor = SDL.SDL_VERSIONNUM_MINOR(version);
|
||||
var micro = SDL.SDL_VERSIONNUM_MICRO(version);
|
||||
|
||||
var videoDriver = SDL.SDL_GetCurrentVideoDriver();
|
||||
var revision = SDL.SDL_GetRevision();
|
||||
|
||||
return $"SDL {major}.{minor}.{micro} (rev: {revision}, video driver: {videoDriver})";
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
|
||||
private static unsafe void LogOutputFunction(
|
||||
void* userdata,
|
||||
int category,
|
||||
SDL.SDL_LogPriority priority,
|
||||
byte* message)
|
||||
{
|
||||
var obj = (Sdl3WindowingImpl) GCHandle.FromIntPtr((IntPtr)userdata).Target!;
|
||||
|
||||
var level = priority switch
|
||||
{
|
||||
SDL.SDL_LogPriority.SDL_LOG_PRIORITY_VERBOSE => LogLevel.Verbose,
|
||||
SDL.SDL_LogPriority.SDL_LOG_PRIORITY_DEBUG => LogLevel.Debug,
|
||||
SDL.SDL_LogPriority.SDL_LOG_PRIORITY_INFO => LogLevel.Info,
|
||||
SDL.SDL_LogPriority.SDL_LOG_PRIORITY_WARN => LogLevel.Warning,
|
||||
SDL.SDL_LogPriority.SDL_LOG_PRIORITY_ERROR => LogLevel.Error,
|
||||
SDL.SDL_LogPriority.SDL_LOG_PRIORITY_CRITICAL => LogLevel.Fatal,
|
||||
_ => LogLevel.Error
|
||||
};
|
||||
|
||||
var msg = Marshal.PtrToStringUTF8((IntPtr) message) ?? "";
|
||||
var categoryName = SdlLogCategoryName(category);
|
||||
obj._sawmillSdl3.Log(level, $"[{categoryName}] {msg}");
|
||||
}
|
||||
|
||||
private static string SdlLogCategoryName(int category)
|
||||
{
|
||||
return (SDL.SDL_LogCategory) category switch {
|
||||
// @formatter:off
|
||||
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_APPLICATION => "application",
|
||||
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_ERROR => "error",
|
||||
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_ASSERT => "assert",
|
||||
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_SYSTEM => "system",
|
||||
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_AUDIO => "audio",
|
||||
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_VIDEO => "video",
|
||||
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_RENDER => "render",
|
||||
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_INPUT => "input",
|
||||
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_TEST => "test",
|
||||
_ => "unknown"
|
||||
// @formatter:on
|
||||
};
|
||||
}
|
||||
|
||||
private enum SdlVideoDriver
|
||||
{
|
||||
// These are the ones we need to be able to check against.
|
||||
Other,
|
||||
Windows,
|
||||
X11
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,10 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Utility;
|
||||
using TerraFX.Interop.Windows;
|
||||
using TerraFX.Interop.Xlib;
|
||||
using X11Window = TerraFX.Interop.Xlib.Window;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
@@ -33,6 +36,8 @@ internal partial class Clyde
|
||||
|
||||
public static unsafe void WindowsSharedWindowCreate(HWND hWnd, IConfigurationManager cfg)
|
||||
{
|
||||
// TODO: REMOVE, only used by GLFW, SDL3 does DWMWA_USE_IMMERSIVE_DARK_MODE automatically.
|
||||
|
||||
// >= Windows 11 22000 check
|
||||
if (cfg.GetCVar(CVars.DisplayWin11ImmersiveDarkMode) && Environment.OSVersion.Version.Build >= 22000)
|
||||
{
|
||||
@@ -40,5 +45,37 @@ internal partial class Clyde
|
||||
Windows.DwmSetWindowAttribute(hWnd, 20, &b, (uint) sizeof(BOOL));
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetWindowStyleNoTitleOptionsWindows(HWND hWnd)
|
||||
{
|
||||
DebugTools.Assert(hWnd != HWND.NULL);
|
||||
|
||||
Windows.SetWindowLongPtrW(
|
||||
hWnd,
|
||||
GWL.GWL_STYLE,
|
||||
// Cast to long here to work around a bug in rider with nint bitwise operators.
|
||||
(nint)((long)Windows.GetWindowLongPtrW(hWnd, GWL.GWL_STYLE) & ~WS.WS_SYSMENU));
|
||||
}
|
||||
|
||||
public static unsafe void SetWindowStyleNoTitleOptionsX11(Display* x11Display, X11Window x11Window)
|
||||
{
|
||||
DebugTools.Assert(x11Window != X11Window.NULL);
|
||||
// https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html#idm46181547486832
|
||||
var newPropValString = Marshal.StringToCoTaskMemUTF8("_NET_WM_WINDOW_TYPE_DIALOG");
|
||||
var newPropVal = Xlib.XInternAtom(x11Display, (sbyte*)newPropValString, Xlib.False);
|
||||
DebugTools.Assert(newPropVal != Atom.NULL);
|
||||
|
||||
var propNameString = Marshal.StringToCoTaskMemUTF8("_NET_WM_WINDOW_TYPE");
|
||||
#pragma warning disable CA1806
|
||||
// [display] [window] [property] [type] [format (8, 16,32)] [mode] [data] [element count]
|
||||
Xlib.XChangeProperty(x11Display, x11Window,
|
||||
Xlib.XInternAtom(x11Display, (sbyte*)propNameString, Xlib.False), // should never be null; part of spec
|
||||
Xlib.XA_ATOM, 32, Xlib.PropModeReplace,
|
||||
(byte*)&newPropVal, 1);
|
||||
#pragma warning restore CA1806
|
||||
|
||||
Marshal.FreeCoTaskMem(newPropValString);
|
||||
Marshal.FreeCoTaskMem(propNameString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,29 +133,5 @@ namespace Robust.Client.Graphics
|
||||
IEnumerable<IClydeMonitor> EnumerateMonitors();
|
||||
|
||||
IClydeWindow CreateWindow(WindowCreateParameters parameters);
|
||||
|
||||
/// <summary>
|
||||
/// Set the active text input area in window pixel coordinates.
|
||||
/// </summary>
|
||||
/// <param name="rect">
|
||||
/// This information is used by the OS to position overlays like IMEs or emoji pickers etc.
|
||||
/// </param>
|
||||
void TextInputSetRect(UIBox2i rect);
|
||||
|
||||
/// <summary>
|
||||
/// Indicate that the game should start accepting text input on the currently focused window.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// On some platforms, this will cause an on-screen keyboard to appear.
|
||||
/// The game will also start accepting IME input if configured by the user.
|
||||
/// </remarks>
|
||||
/// <seealso cref="TextInputStop"/>
|
||||
void TextInputStart();
|
||||
|
||||
/// <summary>
|
||||
/// Stop text input, opposite of <see cref="TextInputStart"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="TextInputStart"/>
|
||||
void TextInputStop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,5 +69,7 @@ namespace Robust.Client.Graphics
|
||||
void ShutdownGridEcsEvents();
|
||||
|
||||
void RunOnWindowThread(Action action);
|
||||
|
||||
IFileDialogManager? FileDialogImpl { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,30 @@ namespace Robust.Client.Graphics
|
||||
/// Raised when the window has been resized.
|
||||
/// </summary>
|
||||
event Action<WindowResizedEventArgs> Resized;
|
||||
|
||||
/// <summary>
|
||||
/// Set the active text input area in window pixel coordinates.
|
||||
/// </summary>
|
||||
/// <param name="rect">
|
||||
/// This information is used by the OS to position overlays like IMEs or emoji pickers etc.
|
||||
/// </param>
|
||||
void TextInputSetRect(UIBox2i rect, int cursor);
|
||||
|
||||
/// <summary>
|
||||
/// Indicate that the game should start accepting text input on the currently focused window.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// On some platforms, this will cause an on-screen keyboard to appear.
|
||||
/// The game will also start accepting IME input if configured by the user.
|
||||
/// </remarks>
|
||||
/// <seealso cref="TextInputStop"/>
|
||||
void TextInputStart();
|
||||
|
||||
/// <summary>
|
||||
/// Stop text input, opposite of <see cref="TextInputStart"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="TextInputStart"/>
|
||||
void TextInputStop();
|
||||
}
|
||||
|
||||
public interface IClydeWindowInternal : IClydeWindow
|
||||
|
||||
@@ -880,7 +880,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
if (Editable)
|
||||
{
|
||||
_clyde.TextInputStart();
|
||||
Root?.Window?.TextInputStart();
|
||||
}
|
||||
|
||||
_focusedOnFrame = _timing.CurFrame;
|
||||
@@ -897,7 +897,8 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
OnFocusExit?.Invoke(new LineEditEventArgs(this, _text));
|
||||
|
||||
_clyde.TextInputStop();
|
||||
Root?.Window?.TextInputStop();
|
||||
|
||||
AbortIme(delete: false);
|
||||
}
|
||||
|
||||
@@ -1145,15 +1146,16 @@ namespace Robust.Client.UserInterface.Controls
|
||||
contentBox.Bottom),
|
||||
cursorColor);
|
||||
|
||||
if (Root?.Window is { } window)
|
||||
{
|
||||
// Update IME position.
|
||||
var imeBox = new UIBox2(
|
||||
actualCursorPosition,
|
||||
contentBox.Left,
|
||||
contentBox.Top,
|
||||
contentBox.Right,
|
||||
contentBox.Bottom);
|
||||
|
||||
_master._clyde.TextInputSetRect((UIBox2i) imeBox.Translated(GlobalPixelPosition));
|
||||
window.TextInputSetRect((UIBox2i) imeBox.Translated(GlobalPixelPosition), actualCursorPosition);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1366,15 +1366,16 @@ public sealed class TextEdit : Control
|
||||
baseLine.Y + descent),
|
||||
cursorColor);
|
||||
|
||||
if (UserInterfaceManager.KeyboardFocused == _master)
|
||||
if (UserInterfaceManager.KeyboardFocused == _master && Root?.Window is { } window)
|
||||
{
|
||||
var box = (UIBox2i)new UIBox2(
|
||||
baseLine.X,
|
||||
drawBox.Left,
|
||||
baseLine.Y - height + descent,
|
||||
drawBox.Right,
|
||||
baseLine.Y + descent);
|
||||
var cursorOffset = baseLine.X - drawBox.Left;
|
||||
|
||||
_master._clyde.TextInputSetRect(box.Translated(GlobalPixelPosition));
|
||||
window.TextInputSetRect(box.Translated(GlobalPixelPosition), (int) cursorOffset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1446,7 +1447,7 @@ public sealed class TextEdit : Control
|
||||
|
||||
if (Editable)
|
||||
{
|
||||
_clyde.TextInputStart();
|
||||
Root?.Window?.TextInputStart();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1454,7 +1455,7 @@ public sealed class TextEdit : Control
|
||||
{
|
||||
base.KeyboardFocusExited();
|
||||
|
||||
_clyde.TextInputStop();
|
||||
Root?.Window?.TextInputStop();
|
||||
AbortIme(delete: false);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,9 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
public async Task<Stream?> OpenFile(FileDialogFilters? filters = null)
|
||||
{
|
||||
if (_clyde.FileDialogImpl is { } clydeImpl)
|
||||
return await clydeImpl.OpenFile(filters);
|
||||
|
||||
var name = await GetOpenFileName(filters);
|
||||
if (name == null)
|
||||
{
|
||||
@@ -53,6 +56,9 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
public async Task<(Stream, bool)?> SaveFile(FileDialogFilters? filters, bool truncate = true)
|
||||
{
|
||||
if (_clyde.FileDialogImpl is { } clydeImpl)
|
||||
return await clydeImpl.SaveFile(filters);
|
||||
|
||||
var name = await GetSaveFileName(filters);
|
||||
if (name == null)
|
||||
{
|
||||
|
||||
@@ -1197,6 +1197,7 @@ Types:
|
||||
RuntimeFieldHandle: { }
|
||||
RuntimeMethodHandle: { }
|
||||
RuntimeTypeHandle: { }
|
||||
STAThreadAttribute: { All: True }
|
||||
SByte: { All: True }
|
||||
Single: { All: True }
|
||||
Span`1:
|
||||
|
||||
Reference in New Issue
Block a user