diff --git a/.github/workflows/build_linux.yml b/.github/workflows/build_linux.yml
index 760041b6a..93b33ebb2 100644
--- a/.github/workflows/build_linux.yml
+++ b/.github/workflows/build_linux.yml
@@ -17,7 +17,7 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get update
- sudo apt-get install -y --fix-missing libx11-dev libxcursor-dev libxinerama-dev build-essential gettext libtool libtool-bin libpulse-dev libasound2-dev libjack-dev portaudio19-dev
+ sudo apt-get install -y --fix-missing libx11-dev libxcursor-dev libxinerama-dev build-essential gettext libtool libtool-bin libpulse-dev libasound2-dev libjack-dev portaudio19-dev libwayland-dev
- name: Setup Vulkan
uses: ./.github/actions/vulkan
- name: Setup .NET
@@ -45,7 +45,7 @@ jobs:
uses: actions/checkout@v3
- name: Install dependencies
run: |
- sudo apt-get install libx11-dev libxcursor-dev libxinerama-dev
+ sudo apt-get install libx11-dev libxcursor-dev libxinerama-dev libwayland-dev
- name: Setup Vulkan
uses: ./.github/actions/vulkan
- name: Setup .NET
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index fdc8e80c9..5f8c55502 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -29,7 +29,7 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get update
- sudo apt-get install -y --fix-missing libx11-dev libxcursor-dev libxinerama-dev build-essential gettext libtool libtool-bin libpulse-dev libasound2-dev libjack-dev portaudio19-dev
+ sudo apt-get install -y --fix-missing libx11-dev libxcursor-dev libxinerama-dev build-essential gettext libtool libtool-bin libpulse-dev libasound2-dev libjack-dev portaudio19-dev libwayland-dev
- name: Build
run: |
./GenerateProjectFiles.sh -vs2022 -log -verbose -printSDKs -dotnet=8
diff --git a/Flax.flaxproj b/Flax.flaxproj
index cb082f268..75dd90447 100644
--- a/Flax.flaxproj
+++ b/Flax.flaxproj
@@ -13,6 +13,7 @@
"Configuration": {
"UseCSharp": true,
"UseLargeWorlds": false,
- "UseDotNet": true
+ "UseDotNet": true,
+ "UseSDL": true
}
}
\ No newline at end of file
diff --git a/Source/Editor/Content/Items/ContentFolder.cs b/Source/Editor/Content/Items/ContentFolder.cs
index 2bc4c03b3..462d0c1c1 100644
--- a/Source/Editor/Content/Items/ContentFolder.cs
+++ b/Source/Editor/Content/Items/ContentFolder.cs
@@ -282,7 +282,7 @@ namespace FlaxEditor.Content
if (data is DragDataFiles)
return DragDropEffect.Copy;
- return _dragOverItems.Effect;
+ return _dragOverItems?.Effect ?? DragDropEffect.None;
}
///
diff --git a/Source/Editor/Content/Items/CppScriptItem.cs b/Source/Editor/Content/Items/CppScriptItem.cs
index b10710e3a..9f1f55dd0 100644
--- a/Source/Editor/Content/Items/CppScriptItem.cs
+++ b/Source/Editor/Content/Items/CppScriptItem.cs
@@ -20,7 +20,7 @@ namespace FlaxEditor.Content
}
///
- public override string TypeDescription => Path.EndsWith(".h") ? "C++ Header File" : "C++ Source Code";
+ public override string TypeDescription => Path.EndsWith(".h") || Path.EndsWith(".hpp") ? "C++ Header File" : "C++ Source Code";
///
public override SpriteHandle DefaultThumbnail => Editor.Instance.Icons.CPPScript128;
diff --git a/Source/Editor/CustomEditors/Dedicated/LocalizationSettingsEditor.cs b/Source/Editor/CustomEditors/Dedicated/LocalizationSettingsEditor.cs
index db7434125..79e512122 100644
--- a/Source/Editor/CustomEditors/Dedicated/LocalizationSettingsEditor.cs
+++ b/Source/Editor/CustomEditors/Dedicated/LocalizationSettingsEditor.cs
@@ -190,12 +190,12 @@ namespace FlaxEditor.CustomEditors.Dedicated
foreach (var file in files)
FindNewKeysCSharp(file, newKeys, allKeys);
- // C++
- files = Directory.GetFiles(Globals.ProjectSourceFolder, "*.cpp", SearchOption.AllDirectories);
+ // C/C++
+ files = Directory.GetFiles(Globals.ProjectSourceFolder, "*.cpp", SearchOption.AllDirectories).Concat(Directory.GetFiles(Globals.ProjectSourceFolder, "*.c", SearchOption.AllDirectories)).ToArray();
filesCount += files.Length;
foreach (var file in files)
FindNewKeysCpp(file, newKeys, allKeys);
- files = Directory.GetFiles(Globals.ProjectSourceFolder, "*.h", SearchOption.AllDirectories);
+ files = Directory.GetFiles(Globals.ProjectSourceFolder, "*.h", SearchOption.AllDirectories).Concat(Directory.GetFiles(Globals.ProjectSourceFolder, "*.hpp", SearchOption.AllDirectories)).ToArray();;
filesCount += files.Length;
foreach (var file in files)
FindNewKeysCpp(file, newKeys, allKeys);
diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs
index 2f517d903..041d4f053 100644
--- a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs
+++ b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs
@@ -1,7 +1,10 @@
-#if PLATFORM_WINDOWS
+#if PLATFORM_WINDOWS || PLATFORM_SDL
#define USE_IS_FOREGROUND
#else
#endif
+#if PLATFORM_SDL
+#define USE_SDL_WORKAROUNDS
+#endif
// Copyright (c) Wojciech Figat. All rights reserved.
using System.Collections.Generic;
@@ -130,7 +133,7 @@ namespace FlaxEditor.GUI.ContextMenu
}
///
- /// Shows the empty menu popup o na screen.
+ /// Shows the empty menu popup on a screen.
///
/// The target control.
/// The target control area to cover.
@@ -265,7 +268,9 @@ namespace FlaxEditor.GUI.ContextMenu
desc.AllowMaximize = false;
desc.AllowDragAndDrop = false;
desc.IsTopmost = true;
- desc.IsRegularWindow = false;
+ desc.Type = WindowType.Popup;
+ desc.Parent = parentWin.Window;
+ desc.Title = "ContextMenu";
desc.HasSizingFrame = false;
OnWindowCreating(ref desc);
_window = Platform.CreateWindow(ref desc);
@@ -274,6 +279,12 @@ namespace FlaxEditor.GUI.ContextMenu
_window.GotFocus += OnWindowGotFocus;
_window.LostFocus += OnWindowLostFocus;
}
+
+#if USE_IS_FOREGROUND && USE_SDL_WORKAROUNDS
+ // The focus between popup and parent windows doesn't change, force hide the popup when clicked on parent
+ parentWin.Window.MouseDown += OnWindowMouseDown;
+ _window.Closed += () => parentWin.Window.MouseDown -= OnWindowMouseDown;
+#endif
// Attach to the window
_parentCM = parent as ContextMenuBase;
@@ -449,6 +460,17 @@ namespace FlaxEditor.GUI.ContextMenu
}
}
+#if USE_SDL_WORKAROUNDS
+ private void OnWindowGotFocus()
+ {
+ }
+
+ private void OnWindowMouseDown(ref Float2 mousePosition, MouseButton button, ref bool handled)
+ {
+ // The user clicked outside the popup window
+ Hide();
+ }
+#else
private void OnWindowGotFocus()
{
var child = _childCM;
@@ -462,6 +484,7 @@ namespace FlaxEditor.GUI.ContextMenu
});
}
}
+#endif
private void OnWindowLostFocus()
{
@@ -560,7 +583,12 @@ namespace FlaxEditor.GUI.ContextMenu
// Let root context menu to check if none of the popup windows
if (_parentCM == null && UseVisibilityControl && !IsForeground)
{
+#if USE_SDL_WORKAROUNDS
+ if (!IsMouseOver)
+ Hide();
+#else
Hide();
+#endif
}
}
#endif
diff --git a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs
index d727c15fc..84feeabd0 100644
--- a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs
+++ b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs
@@ -351,11 +351,15 @@ namespace FlaxEditor.GUI.Dialogs
// Update eye dropper tool
if (_activeEyedropper)
{
+ // Try reading the color under the cursor in realtime if supported by the platform
Float2 mousePosition = Platform.MousePosition;
Color color = ScreenUtilities.GetColorAt(mousePosition);
- if (_linear)
- color = color.ToLinear();
- SelectedColor = color;
+ if (color != Color.Transparent)
+ {
+ if (_linear)
+ color = color.ToLinear();
+ SelectedColor = color;
+ }
}
}
diff --git a/Source/Editor/GUI/Docking/DockHintWindow.cs b/Source/Editor/GUI/Docking/DockHintWindow.cs
deleted file mode 100644
index 30ffd6287..000000000
--- a/Source/Editor/GUI/Docking/DockHintWindow.cs
+++ /dev/null
@@ -1,545 +0,0 @@
-// Copyright (c) Wojciech Figat. All rights reserved.
-
-using System;
-using FlaxEngine;
-using FlaxEngine.GUI;
-
-namespace FlaxEditor.GUI.Docking
-{
- ///
- /// Helper class used to handle docking windows dragging and docking.
- ///
- public class DockHintWindow
- {
- private FloatWindowDockPanel _toMove;
-
- private Float2 _dragOffset;
- private Float2 _defaultWindowSize;
- private Rectangle _rectDock;
- private Rectangle _rectWindow;
- private Float2 _mouse;
- private DockState _toSet;
- private DockPanel _toDock;
- private bool _lateDragOffsetUpdate;
-
- private Rectangle _rLeft, _rRight, _rBottom, _rUpper, _rCenter;
-
- private DockHintWindow(FloatWindowDockPanel toMove)
- {
- _toMove = toMove;
- _toSet = DockState.Float;
- var window = toMove.Window.Window;
-
- // Remove focus from drag target
- _toMove.Focus();
- _toMove.Defocus();
-
- // Focus window
- window.Focus();
-
- // Check if window is maximized and restore window.
- if (window.IsMaximized)
- {
- // Restore window and set position to mouse.
- var mousePos = window.MousePosition;
- var previousSize = window.Size;
- window.Restore();
- window.Position = Platform.MousePosition - mousePos * window.Size / previousSize;
- }
-
- // Calculate dragging offset and move window to the destination position
- var mouseScreenPosition = Platform.MousePosition;
-
- // If the _toMove window was not focused when initializing this window, the result vector only contains zeros
- // and to prevent a failure, we need to perform an update for the drag offset at later time which will be done in the OnMouseMove event handler.
- if (mouseScreenPosition != Float2.Zero)
- CalculateDragOffset(mouseScreenPosition);
- else
- _lateDragOffsetUpdate = true;
-
- // Get initial size
- _defaultWindowSize = window.Size;
-
- // Init proxy window
- Proxy.Init(ref _defaultWindowSize);
-
- // Bind events
- Proxy.Window.MouseUp += OnMouseUp;
- Proxy.Window.MouseMove += OnMouseMove;
- Proxy.Window.LostFocus += OnLostFocus;
-
- // Start tracking mouse
- Proxy.Window.StartTrackingMouse(false);
-
- // Update window GUI
- Proxy.Window.GUI.PerformLayout();
-
- // Update rectangles
- UpdateRects();
-
- // Hide base window
- window.Hide();
-
- // Enable hit window presentation
- Proxy.Window.RenderingEnabled = true;
- Proxy.Window.Show();
- Proxy.Window.Focus();
- }
-
- ///
- /// Releases unmanaged and - optionally - managed resources.
- ///
- public void Dispose()
- {
- // End tracking mouse
- Proxy.Window.EndTrackingMouse();
-
- // Disable rendering
- Proxy.Window.RenderingEnabled = false;
-
- // Unbind events
- Proxy.Window.MouseUp -= OnMouseUp;
- Proxy.Window.MouseMove -= OnMouseMove;
- Proxy.Window.LostFocus -= OnLostFocus;
-
- // Hide the proxy
- Proxy.Hide();
-
- if (_toMove == null)
- return;
-
- // Check if window won't be docked
- if (_toSet == DockState.Float)
- {
- var window = _toMove.Window?.Window;
- if (window == null)
- return;
- var mouse = Platform.MousePosition;
-
- // Move base window
- window.Position = mouse - _dragOffset;
-
- // Show base window
- window.Show();
- }
- else
- {
- bool hasNoChildPanels = _toMove.ChildPanelsCount == 0;
-
- // Check if window has only single tab
- if (hasNoChildPanels && _toMove.TabsCount == 1)
- {
- // Dock window
- _toMove.GetTab(0).Show(_toSet, _toDock);
- }
- // Check if dock as tab and has no child panels
- else if (hasNoChildPanels && _toSet == DockState.DockFill)
- {
- // Dock all tabs
- while (_toMove.TabsCount > 0)
- {
- _toMove.GetTab(0).Show(DockState.DockFill, _toDock);
- }
- }
- else
- {
- var selectedTab = _toMove.SelectedTab;
-
- // Dock the first tab into the target location
- var firstTab = _toMove.GetTab(0);
- firstTab.Show(_toSet, _toDock);
-
- // Dock rest of the tabs
- while (_toMove.TabsCount > 0)
- {
- _toMove.GetTab(0).Show(DockState.DockFill, firstTab);
- }
-
- // Keep selected tab being selected
- selectedTab?.SelectTab();
- }
-
- // Focus target window
- _toDock.Root.Focus();
- }
-
- _toMove = null;
- }
-
- ///
- /// Creates the new dragging hit window.
- ///
- /// Floating dock panel to move.
- /// The dock hint window object.
- public static DockHintWindow Create(FloatWindowDockPanel toMove)
- {
- if (toMove == null)
- throw new ArgumentNullException();
-
- return new DockHintWindow(toMove);
- }
-
- ///
- /// Creates the new dragging hit window.
- ///
- /// Dock window to move.
- /// The dock hint window object.
- public static DockHintWindow Create(DockWindow toMove)
- {
- if (toMove == null)
- throw new ArgumentNullException();
-
- // Show floating
- toMove.ShowFloating();
-
- // Move window to the mouse position (with some offset for caption bar)
- var window = (WindowRootControl)toMove.Root;
- var mouse = Platform.MousePosition;
- window.Window.Position = mouse - new Float2(8, 8);
-
- // Get floating panel
- var floatingPanelToMove = window.GetChild(0) as FloatWindowDockPanel;
-
- return new DockHintWindow(floatingPanelToMove);
- }
-
- ///
- /// Calculates window rectangle in the dock window.
- ///
- /// Window dock state.
- /// Dock panel rectangle.
- /// Calculated window rectangle.
- public static Rectangle CalculateDockRect(DockState state, ref Rectangle rect)
- {
- Rectangle result = rect;
- switch (state)
- {
- case DockState.DockFill:
- result.Location.Y += Editor.Instance.Options.Options.Interface.TabHeight;
- result.Size.Y -= Editor.Instance.Options.Options.Interface.TabHeight;
- break;
- case DockState.DockTop:
- result.Size.Y *= DockPanel.DefaultSplitterValue;
- break;
- case DockState.DockLeft:
- result.Size.X *= DockPanel.DefaultSplitterValue;
- break;
- case DockState.DockBottom:
- result.Location.Y += result.Size.Y * (1 - DockPanel.DefaultSplitterValue);
- result.Size.Y *= DockPanel.DefaultSplitterValue;
- break;
- case DockState.DockRight:
- result.Location.X += result.Size.X * (1 - DockPanel.DefaultSplitterValue);
- result.Size.X *= DockPanel.DefaultSplitterValue;
- break;
- }
- return result;
- }
-
- private void CalculateDragOffset(Float2 mouseScreenPosition)
- {
- var baseWinPos = _toMove.Window.Window.Position;
- _dragOffset = mouseScreenPosition - baseWinPos;
- }
-
- private void UpdateRects()
- {
- // Cache mouse position
- _mouse = Platform.MousePosition;
-
- // Check intersection with any dock panel
- var uiMouse = _mouse;
- _toDock = _toMove.MasterPanel.HitTest(ref uiMouse, _toMove);
-
- // Check dock state to use
- bool showProxyHints = _toDock != null;
- bool showBorderHints = showProxyHints;
- bool showCenterHint = showProxyHints;
- if (showProxyHints)
- {
- // If moved window has not only tabs but also child panels disable docking as tab
- if (_toMove.ChildPanelsCount > 0)
- showCenterHint = false;
-
- // Disable docking windows with one or more dock panels inside
- if (_toMove.ChildPanelsCount > 0)
- showBorderHints = false;
-
- // Get dock area
- _rectDock = _toDock.DockAreaBounds;
-
- // Cache dock rectangles
- var size = _rectDock.Size;
- var offset = _rectDock.Location;
- var borderMargin = 4.0f;
- var hintWindowsSize = Proxy.HintWindowsSize * Platform.DpiScale;
- var hintWindowsSize2 = hintWindowsSize * 0.5f;
- var centerX = size.X * 0.5f;
- var centerY = size.Y * 0.5f;
- _rUpper = new Rectangle(centerX - hintWindowsSize2, borderMargin, hintWindowsSize, hintWindowsSize) + offset;
- _rBottom = new Rectangle(centerX - hintWindowsSize2, size.Y - hintWindowsSize - borderMargin, hintWindowsSize, hintWindowsSize) + offset;
- _rLeft = new Rectangle(borderMargin, centerY - hintWindowsSize2, hintWindowsSize, hintWindowsSize) + offset;
- _rRight = new Rectangle(size.X - hintWindowsSize - borderMargin, centerY - hintWindowsSize2, hintWindowsSize, hintWindowsSize) + offset;
- _rCenter = new Rectangle(centerX - hintWindowsSize2, centerY - hintWindowsSize2, hintWindowsSize, hintWindowsSize) + offset;
-
- // Hit test
- DockState toSet = DockState.Float;
- if (showBorderHints)
- {
- if (_rUpper.Contains(_mouse))
- toSet = DockState.DockTop;
- else if (_rBottom.Contains(_mouse))
- toSet = DockState.DockBottom;
- else if (_rLeft.Contains(_mouse))
- toSet = DockState.DockLeft;
- else if (_rRight.Contains(_mouse))
- toSet = DockState.DockRight;
- }
- if (showCenterHint && _rCenter.Contains(_mouse))
- toSet = DockState.DockFill;
- _toSet = toSet;
-
- // Show proxy hint windows
- Proxy.Down.Position = _rBottom.Location;
- Proxy.Left.Position = _rLeft.Location;
- Proxy.Right.Position = _rRight.Location;
- Proxy.Up.Position = _rUpper.Location;
- Proxy.Center.Position = _rCenter.Location;
- }
- else
- {
- _toSet = DockState.Float;
- }
-
- // Update proxy hint windows visibility
- Proxy.Down.IsVisible = showProxyHints & showBorderHints;
- Proxy.Left.IsVisible = showProxyHints & showBorderHints;
- Proxy.Right.IsVisible = showProxyHints & showBorderHints;
- Proxy.Up.IsVisible = showProxyHints & showBorderHints;
- Proxy.Center.IsVisible = showProxyHints & showCenterHint;
-
- // Calculate proxy/dock/window rectangles
- if (_toDock == null)
- {
- // Floating window over nothing
- _rectWindow = new Rectangle(_mouse - _dragOffset, _defaultWindowSize);
- }
- else
- {
- if (_toSet == DockState.Float)
- {
- // Floating window over dock panel
- _rectWindow = new Rectangle(_mouse - _dragOffset, _defaultWindowSize);
- }
- else
- {
- // Use only part of the dock panel to show hint
- _rectWindow = CalculateDockRect(_toSet, ref _rectDock);
- }
- }
-
- // Update proxy window
- Proxy.Window.ClientBounds = _rectWindow;
- }
-
- private void OnMouseUp(ref Float2 location, MouseButton button, ref bool handled)
- {
- if (button == MouseButton.Left)
- {
- Dispose();
- }
- }
-
- private void OnMouseMove(ref Float2 mousePos)
- {
- // Recalculate the drag offset because the current mouse screen position was invalid when we initialized the window
- if (_lateDragOffsetUpdate)
- {
- // Calculate dragging offset and move window to the destination position
- CalculateDragOffset(mousePos);
-
- // Reset state
- _lateDragOffsetUpdate = false;
- }
-
- UpdateRects();
- }
-
- private void OnLostFocus()
- {
- Dispose();
- }
-
- ///
- /// Contains helper proxy windows shared across docking panels. They are used to visualize docking window locations.
- ///
- public static class Proxy
- {
- ///
- /// The drag proxy window.
- ///
- public static Window Window;
-
- ///
- /// The left hint proxy window.
- ///
- public static Window Left;
-
- ///
- /// The right hint proxy window.
- ///
- public static Window Right;
-
- ///
- /// The up hint proxy window.
- ///
- public static Window Up;
-
- ///
- /// The down hint proxy window.
- ///
- public static Window Down;
-
- ///
- /// The center hint proxy window.
- ///
- public static Window Center;
-
- ///
- /// The hint windows size.
- ///
- public const float HintWindowsSize = 32.0f;
-
- ///
- /// Initializes the hit proxy windows. Those windows are used to indicate drag target areas (left, right, top, bottom, etc.).
- ///
- public static void InitHitProxy()
- {
- CreateProxy(ref Left, "DockHint.Left");
- CreateProxy(ref Right, "DockHint.Right");
- CreateProxy(ref Up, "DockHint.Up");
- CreateProxy(ref Down, "DockHint.Down");
- CreateProxy(ref Center, "DockHint.Center");
- }
-
- ///
- /// Initializes the hint window.
- ///
- /// Initial size of the proxy window.
- public static void Init(ref Float2 initSize)
- {
- if (Window == null)
- {
- var settings = CreateWindowSettings.Default;
- settings.Title = "DockHint.Window";
- settings.Size = initSize;
- settings.AllowInput = true;
- settings.AllowMaximize = false;
- settings.AllowMinimize = false;
- settings.HasBorder = false;
- settings.HasSizingFrame = false;
- settings.IsRegularWindow = false;
- settings.SupportsTransparency = true;
- settings.ShowInTaskbar = false;
- settings.ShowAfterFirstPaint = false;
- settings.IsTopmost = true;
-
- Window = Platform.CreateWindow(ref settings);
- Window.Opacity = 0.6f;
- Window.GUI.BackgroundColor = Style.Current.Selection;
- Window.GUI.AddChild();
- }
- else
- {
- // Resize proxy
- Window.ClientSize = initSize;
- }
-
- InitHitProxy();
- }
-
- private sealed class DragVisuals : Control
- {
- public DragVisuals()
- {
- AnchorPreset = AnchorPresets.StretchAll;
- Offsets = Margin.Zero;
- }
-
- public override void Draw()
- {
- Render2D.DrawRectangle(new Rectangle(Float2.Zero, Size), Style.Current.SelectionBorder);
- }
- }
-
- private static void CreateProxy(ref Window win, string name)
- {
- if (win != null)
- return;
-
- var settings = CreateWindowSettings.Default;
- settings.Title = name;
- settings.Size = new Float2(HintWindowsSize * Platform.DpiScale);
- settings.AllowInput = false;
- settings.AllowMaximize = false;
- settings.AllowMinimize = false;
- settings.HasBorder = false;
- settings.HasSizingFrame = false;
- settings.IsRegularWindow = false;
- settings.SupportsTransparency = true;
- settings.ShowInTaskbar = false;
- settings.ActivateWhenFirstShown = false;
- settings.IsTopmost = true;
- settings.ShowAfterFirstPaint = false;
-
- win = Platform.CreateWindow(ref settings);
- win.Opacity = 0.6f;
- win.GUI.BackgroundColor = Style.Current.Selection;
- win.GUI.AddChild();
- }
-
- ///
- /// Hides proxy windows.
- ///
- public static void Hide()
- {
- HideProxy(ref Window);
- HideProxy(ref Left);
- HideProxy(ref Right);
- HideProxy(ref Up);
- HideProxy(ref Down);
- HideProxy(ref Center);
- }
-
- private static void HideProxy(ref Window win)
- {
- if (win)
- {
- win.Hide();
- }
- }
-
- ///
- /// Releases proxy data and windows.
- ///
- public static void Dispose()
- {
- DisposeProxy(ref Window);
- DisposeProxy(ref Left);
- DisposeProxy(ref Right);
- DisposeProxy(ref Up);
- DisposeProxy(ref Down);
- DisposeProxy(ref Center);
- }
-
- private static void DisposeProxy(ref Window win)
- {
- if (win)
- {
- win.Close(ClosingReason.User);
- win = null;
- }
- }
- }
- }
-}
diff --git a/Source/Editor/GUI/Docking/DockPanelProxy.cs b/Source/Editor/GUI/Docking/DockPanelProxy.cs
index 888b78232..014433b26 100644
--- a/Source/Editor/GUI/Docking/DockPanelProxy.cs
+++ b/Source/Editor/GUI/Docking/DockPanelProxy.cs
@@ -19,11 +19,7 @@ namespace FlaxEditor.GUI.Docking
private float _tabHeight = Editor.Instance.Options.Options.Interface.TabHeight;
private bool _useMinimumTabWidth = Editor.Instance.Options.Options.Interface.UseMinimumTabWidth;
private float _minimumTabWidth = Editor.Instance.Options.Options.Interface.MinimumTabWidth;
-#if PLATFORM_WINDOWS
- private readonly bool _hideTabForSingleTab = Editor.Instance.Options.Options.Interface.HideSingleTabWindowTabBars;
-#else
- private readonly bool _hideTabForSingleTab = false;
-#endif
+ private readonly bool _hideTabForSingleTab = Utilities.Utils.HideSingleTabWindowTabBars();
///
/// The is mouse down flag (left button).
@@ -54,6 +50,11 @@ namespace FlaxEditor.GUI.Docking
/// The mouse position.
///
public Float2 MousePosition = Float2.Minimum;
+
+ ///
+ /// The mouse position.
+ ///
+ public Float2 MouseStartPosition = Float2.Minimum;
///
/// The start drag asynchronous window.
@@ -196,7 +197,7 @@ namespace FlaxEditor.GUI.Docking
if (_panel.ChildPanelsCount == 0 && _panel.TabsCount == 1 && _panel.IsFloating)
{
// Create docking hint window but in an async manner
- DockHintWindow.Create(_panel as FloatWindowDockPanel);
+ WindowDragHelper.StartDragging(_panel as FloatWindowDockPanel);
}
else
{
@@ -207,7 +208,7 @@ namespace FlaxEditor.GUI.Docking
_panel.SelectTab(index - 1);
// Create docking hint window
- DockHintWindow.Create(win);
+ WindowDragHelper.StartDragging(win, _panel.RootWindow.Window);
}
}
}
@@ -393,6 +394,7 @@ namespace FlaxEditor.GUI.Docking
if (IsSingleFloatingWindow)
return base.OnMouseDown(location, button);
MouseDownWindow = GetTabAtPos(location, out IsMouseDownOverCross);
+ MouseStartPosition = location;
// Check buttons
if (button == MouseButton.Left)
@@ -479,6 +481,20 @@ namespace FlaxEditor.GUI.Docking
StartDrag(MouseDownWindow);
MouseDownWindow = null;
}
+ // Check if single tab is tried to be moved
+ else if (MouseDownWindow != null && _panel.TabsCount <= 1)
+ {
+ if ((MousePosition - MouseStartPosition).Length > 3)
+ {
+ // Clear flag
+ IsMouseLeftButtonDown = false;
+
+ // Check tab under the mouse
+ if (!IsMouseDownOverCross && MouseDownWindow != null)
+ StartDrag(MouseDownWindow);
+ MouseDownWindow = null;
+ }
+ }
// Check if has more than one tab to change order
else if (MouseDownWindow != null && _panel.TabsCount > 1)
{
diff --git a/Source/Editor/GUI/Docking/DockWindow.cs b/Source/Editor/GUI/Docking/DockWindow.cs
index 33ab459e4..2770a8d84 100644
--- a/Source/Editor/GUI/Docking/DockWindow.cs
+++ b/Source/Editor/GUI/Docking/DockWindow.cs
@@ -182,6 +182,25 @@ namespace FlaxEditor.GUI.Docking
/// Window size, set to use default.
/// Window location.
public void ShowFloating(Float2 location, Float2 size, WindowStartPosition position = WindowStartPosition.CenterParent)
+ {
+ CreateFloating(location, size, position, true);
+ }
+
+ ///
+ /// Creates the window in a floating state.
+ ///
+ public void CreateFloating()
+ {
+ CreateFloating(Float2.Zero, Float2.Zero);
+ }
+ ///
+ /// Creates the window in a floating state.
+ ///
+ /// Window location.
+ /// Window size, set to use default.
+ /// Window location.
+ /// Window visibility.
+ public void CreateFloating(Float2 location, Float2 size, WindowStartPosition position = WindowStartPosition.CenterParent, bool showWindow = false)
{
Undock();
@@ -199,14 +218,17 @@ namespace FlaxEditor.GUI.Docking
windowGUI.UnlockChildrenRecursive();
windowGUI.PerformLayout();
- // Show
- window.Show();
- window.BringToFront();
- window.Focus();
- OnShow();
+ if (showWindow)
+ {
+ // Show
+ window.Show();
+ window.BringToFront();
+ window.Focus();
+ OnShow();
- // Perform layout again
- windowGUI.PerformLayout();
+ // Perform layout again
+ windowGUI.PerformLayout();
+ }
}
///
diff --git a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs
index 3104b013e..df19245e4 100644
--- a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs
+++ b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs
@@ -11,6 +11,42 @@ namespace FlaxEditor.GUI.Docking
///
public class FloatWindowDockPanel : DockPanel
{
+ private class FloatWindowDecorations : WindowDecorations
+ {
+ private FloatWindowDockPanel _panel;
+
+ public FloatWindowDecorations(FloatWindowDockPanel panel)
+ : base(panel.RootWindow)
+ {
+ _panel = panel;
+ }
+
+ ///
+ public override bool OnMouseDown(Float2 location, MouseButton button)
+ {
+ if (Title.Bounds.Contains(location) && button == MouseButton.Left)
+ {
+ _panel.BeginDrag();
+ return true;
+ }
+ return base.OnMouseDown(location, button);
+ }
+
+#if !PLATFORM_WINDOWS
+ ///
+ protected override WindowHitCodes OnHitTest(ref Float2 mouse)
+ {
+ var hit = base.OnHitTest(ref mouse);
+ if (hit == WindowHitCodes.Caption)
+ {
+ // Override the system behaviour when interacting with the caption area
+ hit = WindowHitCodes.Client;
+ }
+ return hit;
+ }
+#endif
+ }
+
private MasterDockPanel _masterPanel;
private WindowRootControl _window;
@@ -40,6 +76,26 @@ namespace FlaxEditor.GUI.Docking
Parent = window;
_window.Window.Closing += OnClosing;
_window.Window.LeftButtonHit += OnLeftButtonHit;
+
+ if (Utilities.Utils.UseCustomWindowDecorations())
+ {
+ var decorations = Parent.AddChild(new FloatWindowDecorations(this));
+ decorations.SetAnchorPreset(AnchorPresets.HorizontalStretchTop, false);
+ }
+ }
+
+ ///
+ protected override void PerformLayoutBeforeChildren()
+ {
+ base.PerformLayoutBeforeChildren();
+
+ var decorations = Parent.GetChild();
+ if (decorations != null)
+ {
+ // Apply offset for the title bar
+ foreach (var child in Children)
+ child.Bounds = child.Bounds with { Y = decorations.Height, Height = Parent.Height - decorations.Height };
+ }
}
///
@@ -52,7 +108,7 @@ namespace FlaxEditor.GUI.Docking
return;
// Create docking hint window
- DockHintWindow.Create(this);
+ WindowDragHelper.StartDragging(this);
}
///
@@ -71,22 +127,28 @@ namespace FlaxEditor.GUI.Docking
settings.Title = title;
settings.Size = size;
settings.Position = location;
- settings.MinimumSize = new Float2(1);
+ settings.MinimumSize = new Float2(200, 150);
settings.MaximumSize = Float2.Zero; // Unlimited size
settings.Fullscreen = false;
settings.HasBorder = true;
- settings.SupportsTransparency = false;
+ settings.SupportsTransparency = true;
settings.ActivateWhenFirstShown = true;
settings.AllowInput = true;
settings.AllowMinimize = true;
settings.AllowMaximize = true;
settings.AllowDragAndDrop = true;
settings.IsTopmost = false;
- settings.IsRegularWindow = true;
+ settings.Type = WindowType.Regular;
settings.HasSizingFrame = true;
settings.ShowAfterFirstPaint = false;
settings.ShowInTaskbar = true;
settings.StartPosition = startPosition;
+
+ if (Utilities.Utils.UseCustomWindowDecorations())
+ {
+ settings.HasBorder = false;
+ //settings.HasSizingFrame = false;
+ }
// Create window
return Platform.CreateWindow(ref settings);
diff --git a/Source/Editor/GUI/Docking/MasterDockPanel.cs b/Source/Editor/GUI/Docking/MasterDockPanel.cs
index 8d5eede98..7649dc9f8 100644
--- a/Source/Editor/GUI/Docking/MasterDockPanel.cs
+++ b/Source/Editor/GUI/Docking/MasterDockPanel.cs
@@ -81,7 +81,6 @@ namespace FlaxEditor.GUI.Docking
public DockPanel HitTest(ref Float2 position, FloatWindowDockPanel excluded)
{
// Check all floating windows
- // TODO: gather windows order and take it into account when performing test
for (int i = 0; i < FloatingPanels.Count; i++)
{
var win = FloatingPanels[i];
@@ -94,9 +93,44 @@ namespace FlaxEditor.GUI.Docking
}
// Base
+ //if (!Root?.RootWindow.Window.IsFocused ?? false)
+ // return null;
return base.HitTest(ref position);
}
+ ///
+ /// Performs hit test over dock panel.
+ ///
+ /// Window space position to test.
+ /// Floating window to omit during searching (and all docked to that one).
+ /// Results of the hit test
+ /// True if any dock panels were hit, otherwise false.
+ public bool HitTest(ref Float2 position, FloatWindowDockPanel excluded, out DockPanel[] hitResults)
+ {
+ // Check all floating windows
+ List results = new(FloatingPanels.Count);
+ for (int i = 0; i < FloatingPanels.Count; i++)
+ {
+ var win = FloatingPanels[i];
+ if (win.Visible && win != excluded)
+ {
+ var result = win.HitTest(ref position);
+ if (result != null)
+ results.Add(result);
+ }
+ }
+
+ // Base
+ //if (!Root?.RootWindow.Window.IsFocused ?? false)
+ // return null;
+ var baseResult = base.HitTest(ref position);
+ if (baseResult != null)
+ results.Add(baseResult);
+
+ hitResults = results.ToArray();
+ return hitResults.Length > 0;
+ }
+
internal void LinkWindow(DockWindow window)
{
// Add to the windows list
diff --git a/Source/Editor/GUI/Docking/WindowDragHelper.cs b/Source/Editor/GUI/Docking/WindowDragHelper.cs
new file mode 100644
index 000000000..12587b061
--- /dev/null
+++ b/Source/Editor/GUI/Docking/WindowDragHelper.cs
@@ -0,0 +1,458 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+using System;
+using FlaxEngine;
+using FlaxEngine.GUI;
+
+namespace FlaxEditor.GUI.Docking
+{
+ ///
+ /// Helper class used to handle docking windows dragging and docking.
+ ///
+ public class WindowDragHelper
+ {
+ private FloatWindowDockPanel _toMove;
+
+ private Float2 _dragOffset;
+ private Rectangle _rectDock;
+ private Float2 _mouse;
+ private DockState _toSet;
+ private DockPanel _toDock;
+ private Window _dragSourceWindow;
+
+ private Rectangle _rLeft, _rRight, _rBottom, _rUpper, _rCenter;
+ private Control _dockHintDown, _dockHintUp, _dockHintLeft, _dockHintRight, _dockHintCenter;
+
+ ///
+ /// The hint control size.
+ ///
+ public const float HintControlSize = 32.0f;
+
+ ///
+ /// The opacity of the dragged window when hint controls are shown.
+ ///
+ public const float DragWindowOpacity = 0.4f;
+
+ ///
+ /// Returns true if any windows are being dragged.
+ ///
+ public static bool IsDragActive { get; private set; }
+
+ private WindowDragHelper(FloatWindowDockPanel toMove, Window dragSourceWindow)
+ {
+ IsDragActive = true;
+ _toMove = toMove;
+ _toSet = DockState.Float;
+ var window = toMove.Window.Window;
+
+ // Bind events
+ FlaxEngine.Scripting.Update += OnUpdate;
+ window.MouseUp += OnMouseUp;
+
+ // Update rectangles
+ UpdateRects(Platform.MousePosition);
+
+ // Ensure the dragged window stays on top of every other window
+ window.IsAlwaysOnTop = true;
+
+ _dragSourceWindow = dragSourceWindow;
+ if (_dragSourceWindow != null) // Detaching a tab from existing window
+ {
+ _dragOffset = new Float2(window.Size.X / 2, 10.0f);
+
+ _dragSourceWindow.MouseUp += OnMouseUp; // The mouse up event is sent to the source window on Windows
+
+ // TODO: when detaching tab in floating window (not main window), the drag source window is still main window?
+ var dragSourceWindowWayland = toMove.MasterPanel?.RootWindow.Window ?? Editor.Instance.Windows.MainWindow;
+ window.DoDragDrop(window.Title, _dragOffset, dragSourceWindowWayland);
+ }
+ else
+ {
+ _dragOffset = window.MousePosition;
+ window.DoDragDrop(window.Title, _dragOffset, window);
+ }
+ }
+
+ ///
+ /// Releases unmanaged and - optionally - managed resources.
+ ///
+ public void Dispose()
+ {
+ IsDragActive = false;
+ var window = _toMove?.Window?.Window;
+
+ // Unbind events
+ FlaxEngine.Scripting.Update -= OnUpdate;
+ if (window != null)
+ window.MouseUp -= OnMouseUp;
+ if (_dragSourceWindow != null)
+ _dragSourceWindow.MouseUp -= OnMouseUp;
+
+ RemoveDockHints();
+
+ if (_toMove == null)
+ return;
+
+ if (window != null)
+ {
+ window.Opacity = 1.0f;
+ window.IsAlwaysOnTop = false;
+ window.BringToFront();
+ }
+
+ // Check if window won't be docked
+ if (_toSet == DockState.Float)
+ {
+ if (window == null)
+ return;
+
+ // Show base window
+ window.Show();
+ }
+ else
+ {
+ bool hasNoChildPanels = _toMove.ChildPanelsCount == 0;
+
+ // Check if window has only single tab
+ if (hasNoChildPanels && _toMove.TabsCount == 1)
+ {
+ // Dock window
+ _toMove.GetTab(0).Show(_toSet, _toDock);
+ }
+ // Check if dock as tab and has no child panels
+ else if (hasNoChildPanels && _toSet == DockState.DockFill)
+ {
+ // Dock all tabs
+ while (_toMove.TabsCount > 0)
+ {
+ _toMove.GetTab(0).Show(DockState.DockFill, _toDock);
+ }
+ }
+ else
+ {
+ var selectedTab = _toMove.SelectedTab;
+
+ // Dock the first tab into the target location
+ if (_toMove.TabsCount > 0)
+ {
+ var firstTab = _toMove.GetTab(0);
+ firstTab.Show(_toSet, _toDock);
+
+ // Dock rest of the tabs
+ while (_toMove.TabsCount > 0)
+ {
+ _toMove.GetTab(0).Show(DockState.DockFill, firstTab);
+ }
+ }
+
+ // Keep selected tab being selected
+ selectedTab?.SelectTab();
+ }
+
+ // Focus target window
+ _toDock.Root.Focus();
+ }
+
+ _toMove = null;
+ }
+
+ ///
+ /// Start dragging a floating dock panel.
+ ///
+ /// Floating dock panel to move.
+ /// The window drag helper object.
+ public static WindowDragHelper StartDragging(FloatWindowDockPanel toMove)
+ {
+ if (toMove == null)
+ throw new ArgumentNullException();
+
+ return new WindowDragHelper(toMove, null);
+ }
+
+ ///
+ /// Start dragging a docked panel into a floating window.
+ ///
+ /// Dock window to move.
+ /// The window where dragging started from.
+ /// The window drag helper object.
+ public static WindowDragHelper StartDragging(DockWindow toMove, Window dragSourceWindow)
+ {
+ if (toMove == null)
+ throw new ArgumentNullException();
+
+ // Create floating window
+ toMove.CreateFloating();
+
+ // Get floating panel
+ var window = (WindowRootControl)toMove.Root;
+ var floatingPanelToMove = window.GetChild(0) as FloatWindowDockPanel;
+
+ return new WindowDragHelper(floatingPanelToMove, dragSourceWindow);
+ }
+
+ private sealed class DragVisuals : Control
+ {
+ public DragVisuals()
+ {
+ AnchorPreset = AnchorPresets.StretchAll;
+ Offsets = Margin.Zero;
+ }
+
+ public override void Draw()
+ {
+ base.Draw();
+ Render2D.DrawRectangle(new Rectangle(Float2.Zero, Size), Style.Current.SelectionBorder);
+ }
+ }
+
+ private void AddDockHints()
+ {
+ if (_toDock == null)
+ return;
+
+ if (_toDock.RootWindow.Window != _dragSourceWindow)
+ _toDock.RootWindow.Window.MouseUp += OnMouseUp;
+
+ _dockHintDown = AddHintControl(new Float2(0.5f, 1));
+ _dockHintUp = AddHintControl(new Float2(0.5f, 0));
+ _dockHintLeft = AddHintControl(new Float2(0, 0.5f));
+ _dockHintRight = AddHintControl(new Float2(1, 0.5f));
+ _dockHintCenter = AddHintControl(new Float2(0.5f, 0.5f));
+
+ Control AddHintControl(Float2 pivot)
+ {
+ DragVisuals hintControl = _toDock.AddChild();
+ hintControl.Size = new Float2(HintControlSize);
+ hintControl.BackgroundColor = Style.Current.Selection.AlphaMultiplied(0.6f);
+ hintControl.Pivot = pivot;
+ hintControl.PivotRelative = true;
+ return hintControl;
+ }
+ }
+
+ private void RemoveDockHints()
+ {
+ if (_toDock == null)
+ return;
+
+ if (_toDock.RootWindow.Window != _dragSourceWindow)
+ _toDock.RootWindow.Window.MouseUp -= OnMouseUp;
+
+ _dockHintDown?.Parent.RemoveChild(_dockHintDown);
+ _dockHintUp?.Parent.RemoveChild(_dockHintUp);
+ _dockHintLeft?.Parent.RemoveChild(_dockHintLeft);
+ _dockHintRight?.Parent.RemoveChild(_dockHintRight);
+ _dockHintCenter?.Parent.RemoveChild(_dockHintCenter);
+ _dockHintDown = _dockHintUp = _dockHintLeft = _dockHintRight = _dockHintCenter = null;
+ }
+
+ private void UpdateRects(Float2 mousePos)
+ {
+ // Cache mouse position
+ _mouse = mousePos;
+
+ // Check intersection with any dock panel
+ DockPanel dockPanel = null;
+ if (_toMove.MasterPanel.HitTest(ref _mouse, _toMove, out var hitResults))
+ {
+ dockPanel = hitResults[0];
+
+ // Prefer panel which currently has focus
+ foreach (var hit in hitResults)
+ {
+ if (hit.RootWindow.Window.IsFocused)
+ {
+ dockPanel = hit;
+ break;
+ }
+ }
+
+ // Prefer panel in the same window we hit earlier
+ if (dockPanel?.RootWindow != _toDock?.RootWindow)
+ {
+ foreach (var hit in hitResults)
+ {
+ if (hit.RootWindow == _toDock?.RootWindow)
+ {
+ dockPanel = _toDock;
+ break;
+ }
+ }
+ }
+ }
+
+ if (dockPanel != _toDock)
+ {
+ RemoveDockHints();
+ _toDock = dockPanel;
+ AddDockHints();
+
+ // Make sure the all the dock hint areas are not under other windows
+ _toDock?.RootWindow.Window.BringToFront();
+ //_toDock?.RootWindow.Window.Focus();
+
+ // Make the dragged window transparent when dock hints are visible
+ _toMove.Window.Window.Opacity = _toDock == null ? 1.0f : DragWindowOpacity;
+ }
+
+ // Check dock state to use
+ bool showProxyHints = _toDock != null;
+ bool showBorderHints = showProxyHints;
+ bool showCenterHint = showProxyHints;
+ Control hoveredHintControl = null;
+ Float2 hoveredSizeOverride = Float2.Zero;
+ if (showProxyHints)
+ {
+ // If moved window has not only tabs but also child panels disable docking as tab
+ if (_toMove.ChildPanelsCount > 0)
+ showCenterHint = false;
+
+ // Disable docking windows with one or more dock panels inside
+ if (_toMove.ChildPanelsCount > 0)
+ showBorderHints = false;
+
+ // Get dock area
+ _rectDock = _toDock.DockAreaBounds;
+
+ // Cache dock rectangles
+ var size = _rectDock.Size / Platform.DpiScale;
+ var offset = _toDock.PointFromScreen(_rectDock.Location);
+ var borderMargin = 4.0f;
+ var hintWindowsSize = HintControlSize;
+ var hintWindowsSize2 = hintWindowsSize * 0.5f;
+ var hintPreviewSize = new Float2(Math.Max(HintControlSize * 2, size.X * 0.5f), Math.Max(HintControlSize * 2, size.Y * 0.5f));
+ var centerX = size.X * 0.5f;
+ var centerY = size.Y * 0.5f;
+ _rUpper = new Rectangle(centerX - hintWindowsSize2, borderMargin, hintWindowsSize, hintWindowsSize) + offset;
+ _rBottom = new Rectangle(centerX - hintWindowsSize2, size.Y - hintWindowsSize - borderMargin, hintWindowsSize, hintWindowsSize) + offset;
+ _rLeft = new Rectangle(borderMargin, centerY - hintWindowsSize2, hintWindowsSize, hintWindowsSize) + offset;
+ _rRight = new Rectangle(size.X - hintWindowsSize - borderMargin, centerY - hintWindowsSize2, hintWindowsSize, hintWindowsSize) + offset;
+ _rCenter = new Rectangle(centerX - hintWindowsSize2, centerY - hintWindowsSize2, hintWindowsSize, hintWindowsSize) + offset;
+
+ // Hit test, and calculate the approximation for filled area when hovered over the hint
+ DockState toSet = DockState.Float;
+ if (showBorderHints)
+ {
+ if (_rUpper.Contains(_toDock.PointFromScreen(_mouse)))
+ {
+ toSet = DockState.DockTop;
+ hoveredHintControl = _dockHintUp;
+ hoveredSizeOverride = new Float2(size.X, hintPreviewSize.Y);
+ }
+ else if (_rBottom.Contains(_toDock.PointFromScreen(_mouse)))
+ {
+ toSet = DockState.DockBottom;
+ hoveredHintControl = _dockHintDown;
+ hoveredSizeOverride = new Float2(size.X, hintPreviewSize.Y);
+ }
+ else if (_rLeft.Contains(_toDock.PointFromScreen(_mouse)))
+ {
+ toSet = DockState.DockLeft;
+ hoveredHintControl = _dockHintLeft;
+ hoveredSizeOverride = new Float2(hintPreviewSize.X, size.Y);
+ }
+ else if (_rRight.Contains(_toDock.PointFromScreen(_mouse)))
+ {
+ toSet = DockState.DockRight;
+ hoveredHintControl = _dockHintRight;
+ hoveredSizeOverride = new Float2(hintPreviewSize.X, size.Y);
+ }
+ }
+ if (showCenterHint && _rCenter.Contains(_toDock.PointFromScreen(_mouse)))
+ {
+ toSet = DockState.DockFill;
+ hoveredHintControl = _dockHintCenter;
+ hoveredSizeOverride = new Float2(size.X, size.Y);
+ }
+
+ _toSet = toSet;
+ }
+ else
+ {
+ _toSet = DockState.Float;
+ }
+
+ // Update sizes and opacity of hint controls
+ if (_toDock != null)
+ {
+ if (hoveredHintControl != _dockHintDown)
+ {
+ _dockHintDown.Size = new Float2(HintControlSize);
+ _dockHintDown.BackgroundColor = Style.Current.Selection.AlphaMultiplied(0.6f);
+ }
+ if (hoveredHintControl != _dockHintLeft)
+ {
+ _dockHintLeft.Size = new Float2(HintControlSize);
+ _dockHintLeft.BackgroundColor = Style.Current.Selection.AlphaMultiplied(0.6f);
+ }
+ if (hoveredHintControl != _dockHintRight)
+ {
+ _dockHintRight.Size = new Float2(HintControlSize);
+ _dockHintRight.BackgroundColor = Style.Current.Selection.AlphaMultiplied(0.6f);
+ }
+ if (hoveredHintControl != _dockHintUp)
+ {
+ _dockHintUp.Size = new Float2(HintControlSize);
+ _dockHintUp.BackgroundColor = Style.Current.Selection.AlphaMultiplied(0.6f);
+ }
+ if (hoveredHintControl != _dockHintCenter)
+ {
+ _dockHintCenter.Size = new Float2(HintControlSize);
+ _dockHintCenter.BackgroundColor = Style.Current.Selection.AlphaMultiplied(0.6f);
+ }
+
+ if (_toSet != DockState.Float)
+ {
+ if (hoveredHintControl != null)
+ {
+ hoveredHintControl.BackgroundColor = Style.Current.Selection.AlphaMultiplied(1.0f);
+ hoveredHintControl.Size = hoveredSizeOverride;
+ }
+ }
+ }
+
+ // Update hint controls visibility and location
+ if (showProxyHints)
+ {
+ if (hoveredHintControl != _dockHintDown)
+ _dockHintDown.Location = _rBottom.Location;
+ if (hoveredHintControl != _dockHintLeft)
+ _dockHintLeft.Location = _rLeft.Location;
+ if (hoveredHintControl != _dockHintRight)
+ _dockHintRight.Location = _rRight.Location;
+ if (hoveredHintControl != _dockHintUp)
+ _dockHintUp.Location = _rUpper.Location;
+ if (hoveredHintControl != _dockHintCenter)
+ _dockHintCenter.Location = _rCenter.Location;
+
+ _dockHintDown.Visible = showProxyHints & showBorderHints;
+ _dockHintLeft.Visible = showProxyHints & showBorderHints;
+ _dockHintRight.Visible = showProxyHints & showBorderHints;
+ _dockHintUp.Visible = showProxyHints & showBorderHints;
+ _dockHintCenter.Visible = showProxyHints & showCenterHint;
+ }
+ }
+
+ private void OnMouseUp(ref Float2 location, MouseButton button, ref bool handled)
+ {
+ if (button == MouseButton.Left)
+ Dispose();
+ }
+
+ private void OnUpdate()
+ {
+ var mousePos = Platform.MousePosition;
+
+ if (_mouse != mousePos)
+ OnMouseMove(mousePos);
+ }
+
+ private void OnMouseMove(Float2 mousePos)
+ {
+ if (_dragSourceWindow != null)
+ _toMove.Window.Window.Position = mousePos - _dragOffset;
+
+ UpdateRects(mousePos);
+ }
+ }
+}
diff --git a/Source/Editor/GUI/Input/ValueBox.cs b/Source/Editor/GUI/Input/ValueBox.cs
index 88ec9a4ee..5e1e4aaf4 100644
--- a/Source/Editor/GUI/Input/ValueBox.cs
+++ b/Source/Editor/GUI/Input/ValueBox.cs
@@ -292,6 +292,7 @@ namespace FlaxEditor.GUI.Input
return base.OnMouseDown(location, button);
}
+#if !PLATFORM_SDL
///
public override void OnMouseMove(Float2 location)
{
@@ -318,13 +319,45 @@ namespace FlaxEditor.GUI.Input
base.OnMouseMove(location);
}
+#else
+
+ ///
+ public override void OnMouseMoveRelative(Float2 mouseMotion)
+ {
+ var location = Root.TrackingMouseOffset;
+ if (_isSliding)
+ {
+ // Update sliding
+ ApplySliding(Root.TrackingMouseOffset.X * _slideSpeed);
+ return;
+ }
+
+ // Update cursor type so user knows they can slide value
+ if (CanUseSliding && SlideRect.Contains(location) && !_isSliding)
+ {
+ Cursor = CursorType.SizeWE;
+ _cursorChanged = true;
+ }
+ else if (_cursorChanged && !_isSliding)
+ {
+ Cursor = CursorType.Default;
+ _cursorChanged = false;
+ }
+
+ base.OnMouseMoveRelative(mouseMotion);
+ }
+
+#endif
+
///
public override bool OnMouseUp(Float2 location, MouseButton button)
{
if (button == MouseButton.Left && _isSliding)
{
+#if !PLATFORM_SDL
// End sliding and return mouse to original location
RootWindow.MousePosition = _mouseClickedPosition;
+#endif
EndSliding();
return true;
}
diff --git a/Source/Editor/GUI/MainMenu.cs b/Source/Editor/GUI/MainMenu.cs
index bce668451..6479938a1 100644
--- a/Source/Editor/GUI/MainMenu.cs
+++ b/Source/Editor/GUI/MainMenu.cs
@@ -12,16 +12,6 @@ namespace FlaxEditor.GUI
///
public sealed class MainMenu : ContainerControl
{
-#if PLATFORM_WINDOWS
- private bool _useCustomWindowSystem;
- private Image _icon;
- private Label _title;
- private Button _closeButton;
- private Button _minimizeButton;
- private Button _maximizeButton;
- private LocalizedString _charChromeRestore, _charChromeMaximize;
- private Window _window;
-#endif
private MainMenuButton _selected;
///
@@ -60,200 +50,12 @@ namespace FlaxEditor.GUI
///
/// Initializes a new instance of the class.
///
- /// The main window.
- public MainMenu(RootControl mainWindow)
+ public MainMenu()
: base(0, 0, 0, 20)
{
AutoFocus = false;
AnchorPreset = AnchorPresets.HorizontalStretchTop;
-
-#if PLATFORM_WINDOWS
- _useCustomWindowSystem = !Editor.Instance.Options.Options.Interface.UseNativeWindowSystem;
- if (_useCustomWindowSystem)
- {
- BackgroundColor = Style.Current.LightBackground;
- Height = 28;
-
- var windowIcon = FlaxEngine.Content.LoadAsyncInternal(EditorAssets.WindowIcon);
- FontAsset windowIconsFont = FlaxEngine.Content.LoadAsyncInternal(EditorAssets.WindowIconsFont);
- Font iconFont = windowIconsFont?.CreateFont(9);
-
- _window = mainWindow.RootWindow.Window;
- _window.HitTest += OnHitTest;
- _window.Closed += OnWindowClosed;
-
- ScriptsBuilder.GetBinariesConfiguration(out _, out _, out _, out var configuration);
-
- _icon = new Image
- {
- Margin = new Margin(6, 6, 6, 6),
- Brush = new TextureBrush(windowIcon),
- Color = Style.Current.Foreground,
- KeepAspectRatio = false,
- TooltipText = string.Format("{0}\nVersion {1}\nConfiguration {3}\nGraphics {2}", _window.Title, Globals.EngineVersion, GPUDevice.Instance.RendererType, configuration),
- Parent = this,
- };
-
- _title = new Label(0, 0, Width, Height)
- {
- Text = _window.Title,
- HorizontalAlignment = TextAlignment.Center,
- VerticalAlignment = TextAlignment.Center,
- ClipText = true,
- TextColor = Style.Current.ForegroundGrey,
- TextColorHighlighted = Style.Current.ForegroundGrey,
- Parent = this,
- };
-
- _closeButton = new Button
- {
- Text = ((char)EditorAssets.SegMDL2Icons.ChromeClose).ToString(),
- Font = new FontReference(iconFont),
- BackgroundColor = Color.Transparent,
- BorderColor = Color.Transparent,
- BorderColorHighlighted = Color.Transparent,
- BorderColorSelected = Color.Transparent,
- TextColor = Style.Current.Foreground,
- Width = 46,
- BackgroundColorHighlighted = Color.Red,
- BackgroundColorSelected = Color.Red.RGBMultiplied(1.3f),
- Parent = this,
- };
- _closeButton.Clicked += () => _window.Close(ClosingReason.User);
-
- _minimizeButton = new Button
- {
- Text = ((char)EditorAssets.SegMDL2Icons.ChromeMinimize).ToString(),
- Font = new FontReference(iconFont),
- BackgroundColor = Color.Transparent,
- BorderColor = Color.Transparent,
- BorderColorHighlighted = Color.Transparent,
- BorderColorSelected = Color.Transparent,
- TextColor = Style.Current.Foreground,
- Width = 46,
- BackgroundColorHighlighted = Style.Current.LightBackground.RGBMultiplied(1.3f),
- Parent = this,
- };
- _minimizeButton.Clicked += () => _window.Minimize();
-
- _maximizeButton = new Button
- {
- Text = ((char)(_window.IsMaximized ? EditorAssets.SegMDL2Icons.ChromeRestore : EditorAssets.SegMDL2Icons.ChromeMaximize)).ToString(),
- Font = new FontReference(iconFont),
- BackgroundColor = Color.Transparent,
- BorderColor = Color.Transparent,
- BorderColorHighlighted = Color.Transparent,
- BorderColorSelected = Color.Transparent,
- TextColor = Style.Current.Foreground,
- Width = 46,
- BackgroundColorHighlighted = Style.Current.LightBackground.RGBMultiplied(1.3f),
- Parent = this,
- };
- _maximizeButton.Clicked += () =>
- {
- if (_window.IsMaximized)
- _window.Restore();
- else
- _window.Maximize();
- };
- _charChromeRestore = ((char)EditorAssets.SegMDL2Icons.ChromeRestore).ToString();
- _charChromeMaximize = ((char)EditorAssets.SegMDL2Icons.ChromeMaximize).ToString();
- }
- else
-#endif
- {
- BackgroundColor = Style.Current.LightBackground;
- }
- }
-
-#if PLATFORM_WINDOWS
- ///
- public override void Update(float deltaTime)
- {
- base.Update(deltaTime);
-
- if (_maximizeButton != null)
- {
- _maximizeButton.Text = _window.IsMaximized ? _charChromeRestore : _charChromeMaximize;
- }
- }
-
- private void OnWindowClosed()
- {
- if (_window != null)
- {
- _window.HitTest = null;
- _window = null;
- }
- }
-
- private WindowHitCodes OnHitTest(ref Float2 mouse)
- {
- var dpiScale = _window.DpiScale;
-
- if (_window.IsMinimized)
- return WindowHitCodes.NoWhere;
-
- if (!_window.IsMaximized)
- {
- var pos = _window.ScreenToClient(mouse * dpiScale); // pos is not DPI adjusted
- var winSize = _window.Size;
-
- // Distance from which the mouse is considered to be on the border/corner
- float distance = 5.0f * dpiScale;
-
- if (pos.Y > winSize.Y - distance && pos.X < distance)
- return WindowHitCodes.BottomLeft;
-
- if (pos.X > winSize.X - distance && pos.Y > winSize.Y - distance)
- return WindowHitCodes.BottomRight;
-
- if (pos.Y < distance && pos.X < distance)
- return WindowHitCodes.TopLeft;
-
- if (pos.Y < distance && pos.X > winSize.X - distance)
- return WindowHitCodes.TopRight;
-
- if (pos.X > winSize.X - distance)
- return WindowHitCodes.Right;
-
- if (pos.X < distance)
- return WindowHitCodes.Left;
-
- if (pos.Y < distance)
- return WindowHitCodes.Top;
-
- if (pos.Y > winSize.Y - distance)
- return WindowHitCodes.Bottom;
- }
-
- var mousePos = PointFromScreen(mouse * dpiScale);
- var controlUnderMouse = GetChildAt(mousePos);
- var isMouseOverSth = controlUnderMouse != null && controlUnderMouse != _title;
- var rb = GetRightButton();
- if (rb != null && _minimizeButton != null && new Rectangle(rb.UpperRight, _minimizeButton.BottomLeft - rb.UpperRight).Contains(ref mousePos) && !isMouseOverSth)
- return WindowHitCodes.Caption;
-
- return WindowHitCodes.Client;
- }
-#endif
-
- ///
- /// Return the rightmost button.
- ///
- /// Rightmost button, null if there is no
- private MainMenuButton GetRightButton()
- {
- MainMenuButton b = null;
- foreach (var control in Children)
- {
- if (b == null && control is MainMenuButton)
- b = (MainMenuButton)control;
-
- if (control is MainMenuButton && control.Right > b.Right)
- b = (MainMenuButton)control;
- }
- return b;
+ BackgroundColor = Style.Current.LightBackground;
}
///
@@ -298,26 +100,6 @@ namespace FlaxEditor.GUI
return result;
}
- ///
- public override bool OnMouseDoubleClick(Float2 location, MouseButton button)
- {
- if (base.OnMouseDoubleClick(location, button))
- return true;
-
-#if PLATFORM_WINDOWS
- var child = GetChildAtRecursive(location);
- if (_useCustomWindowSystem && child is not Button && child is not MainMenuButton)
- {
- if (_window.IsMaximized)
- _window.Restore();
- else
- _window.Maximize();
- }
-#endif
-
- return true;
- }
-
///
public override bool OnKeyDown(KeyboardKeys key)
{
@@ -333,16 +115,8 @@ namespace FlaxEditor.GUI
protected override void PerformLayoutAfterChildren()
{
float x = 0;
-
-#if PLATFORM_WINDOWS
- if (_useCustomWindowSystem)
- {
- // Icon
- _icon.X = x;
- _icon.Size = new Float2(Height);
- x += _icon.Width;
- }
-#endif
+ WindowDecorations decorations = Parent.GetChild();
+ x += decorations?.Icon?.Width ?? 0;
// Arrange controls
MainMenuButton rightMostButton = null;
@@ -361,37 +135,21 @@ namespace FlaxEditor.GUI
x += b.Width;
}
}
-
-#if PLATFORM_WINDOWS
- if (_useCustomWindowSystem)
- {
- // Buttons
- _closeButton.Height = Height;
- _closeButton.X = Width - _closeButton.Width;
- _maximizeButton.Height = Height;
- _maximizeButton.X = _closeButton.X - _maximizeButton.Width;
- _minimizeButton.Height = Height;
- _minimizeButton.X = _maximizeButton.X - _minimizeButton.Width;
-
- // Title
- _title.Bounds = new Rectangle(x + 2, 0, _minimizeButton.Left - x - 4, Height);
- //_title.Text = _title.Width < 300.0f ? Editor.Instance.ProjectInfo.Name : _window.Title;
- }
-#endif
+
+ // Fill the right side if title and buttons are not present
+ if (decorations?.Title == null)
+ Width = Parent.Width;
+ else
+ Width = x;
}
-#if PLATFORM_WINDOWS
///
public override void OnDestroy()
{
base.OnDestroy();
-
- if (_window != null)
- {
- _window.Closed -= OnWindowClosed;
- OnWindowClosed();
- }
+
+ if (_selected != null)
+ Selected = null;
}
-#endif
}
}
diff --git a/Source/Editor/GUI/MainMenuButton.cs b/Source/Editor/GUI/MainMenuButton.cs
index 291249d89..5007af92e 100644
--- a/Source/Editor/GUI/MainMenuButton.cs
+++ b/Source/Editor/GUI/MainMenuButton.cs
@@ -42,14 +42,12 @@ namespace FlaxEditor.GUI
Text = text;
var style = Style.Current;
-#if PLATFORM_WINDOWS
- if (Editor.Instance.Options.Options.Interface.UseNativeWindowSystem)
+ if (!Utilities.Utils.UseCustomWindowDecorations())
{
BackgroundColorMouseOver = style.BackgroundHighlighted;
BackgroundColorMouseOverOpened = style.Background;
}
else
-#endif
{
BackgroundColorMouseOver = BackgroundColorMouseOverOpened = style.LightBackground * 1.3f;
}
diff --git a/Source/Editor/GUI/WindowDecorations.cs b/Source/Editor/GUI/WindowDecorations.cs
new file mode 100644
index 000000000..72f62e3e5
--- /dev/null
+++ b/Source/Editor/GUI/WindowDecorations.cs
@@ -0,0 +1,342 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+using System;
+using FlaxEditor.GUI.Docking;
+using FlaxEditor.Options;
+using FlaxEngine;
+using FlaxEngine.GUI;
+
+namespace FlaxEditor.GUI;
+
+///
+/// Represents the title bar of the window with buttons.
+///
+///
+public class WindowDecorations : ContainerControl
+{
+ private Image _icon;
+ private Label _title;
+ private Button _closeButton;
+ private Button _minimizeButton;
+ private Button _maximizeButton;
+ private LocalizedString _charChromeRestore, _charChromeMaximize;
+ private Window _window;
+
+ ///
+ /// The title label in the title bar.
+ ///
+ public Label Title => _title;
+
+ ///
+ /// The icon used in the title bar.
+ ///
+ public Image Icon => _icon;
+
+ ///
+ /// The tooltip shown when hovering over the icon.
+ ///
+ public string IconTooltipText
+ {
+ get => _icon?.TooltipText ?? null;
+ set
+ {
+ if (_icon != null)
+ _icon.TooltipText = value;
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The window.
+ /// When set, omit drawing title and buttons.
+ public WindowDecorations(RootControl window, bool iconOnly = false)
+ : base(0, 0, 0, 20)
+ {
+ _window = window.RootWindow.Window;
+
+ AutoFocus = false;
+ AnchorPreset = AnchorPresets.HorizontalStretchTop;
+ BackgroundColor = Color.Transparent;
+
+ var windowIcon = FlaxEngine.Content.LoadAsyncInternal(EditorAssets.WindowIcon);
+
+ _icon = new Image
+ {
+ Margin = new Margin(4, 4, 4, 4),
+ Brush = new TextureBrush(windowIcon),
+ Color = Style.Current.Foreground,
+ BackgroundColor = Style.Current.LightBackground,
+ KeepAspectRatio = false,
+ Parent = this,
+ };
+
+ if (!iconOnly)
+ {
+ _icon.Margin = new Margin(6, 6, 6, 6);
+ Height = 28;
+
+ _window.HitTest += OnHitTest;
+ _window.Closed += OnWindowClosed;
+
+ FontAsset windowIconsFont = FlaxEngine.Content.LoadAsyncInternal(EditorAssets.WindowIconsFont);
+ Font iconFont = windowIconsFont?.CreateFont(9);
+
+ _title = new Label(0, 0, Width, Height)
+ {
+ Text = _window.Title,
+ HorizontalAlignment = TextAlignment.Center,
+ VerticalAlignment = TextAlignment.Center,
+ ClipText = true,
+ TextColor = Style.Current.ForegroundGrey,
+ TextColorHighlighted = Style.Current.ForegroundGrey,
+ BackgroundColor = Style.Current.LightBackground,
+ Parent = this,
+ };
+
+ _closeButton = new Button
+ {
+ Text = ((char)EditorAssets.SegMDL2Icons.ChromeClose).ToString(),
+ Font = new FontReference(iconFont),
+ BackgroundColor = Style.Current.LightBackground,
+ BorderColor = Color.Transparent,
+ BorderColorHighlighted = Color.Transparent,
+ BorderColorSelected = Color.Transparent,
+ TextColor = Style.Current.Foreground,
+ Width = 46,
+ BackgroundColorHighlighted = Color.Red,
+ BackgroundColorSelected = Color.Red.RGBMultiplied(1.3f),
+ Parent = this,
+ };
+ _closeButton.Clicked += () => _window.Close(ClosingReason.User);
+
+ _minimizeButton = new Button
+ {
+ Text = ((char)EditorAssets.SegMDL2Icons.ChromeMinimize).ToString(),
+ Font = new FontReference(iconFont),
+ BackgroundColor = Style.Current.LightBackground,
+ BorderColor = Color.Transparent,
+ BorderColorHighlighted = Color.Transparent,
+ BorderColorSelected = Color.Transparent,
+ TextColor = Style.Current.Foreground,
+ Width = 46,
+ BackgroundColorHighlighted = Style.Current.LightBackground.RGBMultiplied(1.3f),
+ Parent = this,
+ };
+ _minimizeButton.Clicked += () => _window.Minimize();
+
+ _maximizeButton = new Button
+ {
+ Text = ((char)(_window.IsMaximized ? EditorAssets.SegMDL2Icons.ChromeRestore : EditorAssets.SegMDL2Icons.ChromeMaximize)).ToString(),
+ Font = new FontReference(iconFont),
+ BackgroundColor = Style.Current.LightBackground,
+ BorderColor = Color.Transparent,
+ BorderColorHighlighted = Color.Transparent,
+ BorderColorSelected = Color.Transparent,
+ TextColor = Style.Current.Foreground,
+ Width = 46,
+ BackgroundColorHighlighted = Style.Current.LightBackground.RGBMultiplied(1.3f),
+ Parent = this,
+ };
+ _maximizeButton.Clicked += () =>
+ {
+ if (_window.IsMaximized)
+ _window.Restore();
+ else
+ _window.Maximize();
+ };
+
+ _charChromeRestore = ((char)EditorAssets.SegMDL2Icons.ChromeRestore).ToString();
+ _charChromeMaximize = ((char)EditorAssets.SegMDL2Icons.ChromeMaximize).ToString();
+ }
+ }
+
+ ///
+ public override void Update(float deltaTime)
+ {
+ base.Update(deltaTime);
+
+ if (_maximizeButton != null)
+ {
+ var maximizeText = _window.IsMaximized ? _charChromeRestore : _charChromeMaximize;
+ if (_maximizeButton.Text != maximizeText)
+ _maximizeButton.Text = maximizeText;
+ }
+ }
+
+ private void OnWindowClosed()
+ {
+ if (_window != null)
+ {
+ _window.HitTest -= OnHitTest;
+ _window = null;
+ }
+ }
+
+ ///
+ /// Perform hit test on the window.
+ ///
+ /// The mouse position
+ /// The hit code for given position.
+ protected virtual WindowHitCodes OnHitTest(ref Float2 mouse)
+ {
+ if (_window.IsMinimized)
+ return WindowHitCodes.NoWhere;
+
+ var dpiScale = _window.DpiScale;
+ var pos = _window.ScreenToClient(mouse * dpiScale); // pos is not DPI adjusted
+ if (!_window.IsMaximized)
+ {
+ var winSize = _window.Size;
+
+ // Distance from which the mouse is considered to be on the border/corner
+ float distance = 5.0f * dpiScale;
+
+ if (pos.Y > winSize.Y - distance && pos.X < distance)
+ return WindowHitCodes.BottomLeft;
+
+ if (pos.X > winSize.X - distance && pos.Y > winSize.Y - distance)
+ return WindowHitCodes.BottomRight;
+
+ if (pos.Y < distance && pos.X < distance)
+ return WindowHitCodes.TopLeft;
+
+ if (pos.Y < distance && pos.X > winSize.X - distance)
+ return WindowHitCodes.TopRight;
+
+ if (pos.X > winSize.X - distance)
+ return WindowHitCodes.Right;
+
+ if (pos.X < distance)
+ return WindowHitCodes.Left;
+
+ if (pos.Y < distance)
+ return WindowHitCodes.Top;
+
+ if (pos.Y > winSize.Y - distance)
+ return WindowHitCodes.Bottom;
+ }
+
+ var controlUnderMouse = GetChildAt(pos, control => control != _title);
+ if (_title.Bounds.Contains(pos) && controlUnderMouse == null)
+ return WindowHitCodes.Caption;
+
+ return WindowHitCodes.Client;
+ }
+
+ ///
+ public override bool OnMouseDoubleClick(Float2 location, MouseButton button)
+ {
+ // These may not work with main window due to SDL not passing mouse events
+ // when interacting with hit tests on caption area...
+
+ if (Title.Bounds.Contains(location) && button == MouseButton.Left)
+ {
+ if (_window.IsMaximized)
+ _window.Restore();
+ else
+ _window.Maximize();
+ return true;
+ }
+ else if (Icon.Bounds.Contains(location) && button == MouseButton.Left)
+ {
+ _window.Close(ClosingReason.User);
+ return true;
+ }
+ return base.OnMouseDoubleClick(location, button);
+ }
+
+ ///
+ protected override void PerformLayoutAfterChildren()
+ {
+ // Calculate extents for title bounds area excluding the icon and main menu area
+ float x = 0;
+
+ // Icon
+ if (_icon != null)
+ {
+ _icon.X = x;
+ _icon.Size = new Float2(Height);
+ x += _icon.Width;
+ }
+
+ // Main menu if present
+ if (Parent.GetChild() is MainMenu mainMenu)
+ {
+ for (int i = 0; i < mainMenu.Children.Count; i++)
+ {
+ var c = mainMenu.Children[i];
+ if (c is MainMenuButton b && c.Visible)
+ {
+ b.Bounds = new Rectangle(x, 0, b.Width, Height);
+ x += b.Width;
+ }
+ }
+ }
+
+ // Buttons
+ float rightMostButtonX = Width;
+ if (_closeButton != null)
+ {
+ _closeButton.Height = Height;
+ _closeButton.X = rightMostButtonX - _closeButton.Width;
+ rightMostButtonX = _closeButton.X;
+ }
+ if (_maximizeButton != null)
+ {
+ _maximizeButton.Height = Height;
+ _maximizeButton.X = rightMostButtonX - _maximizeButton.Width;
+ rightMostButtonX = _maximizeButton.X;
+ }
+ if (_minimizeButton != null)
+ {
+ _minimizeButton.Height = Height;
+ _minimizeButton.X = rightMostButtonX - _minimizeButton.Width;
+ rightMostButtonX = _minimizeButton.X;
+ }
+
+ // Title
+ if (_title != null)
+ {
+ _title.Text = _window.Title;
+ _title.Bounds = new Rectangle(x, 0, rightMostButtonX - x, Height);
+ }
+ }
+
+ ///
+ public override void Draw()
+ {
+ base.Draw();
+ DrawBorders();
+ }
+
+ ///
+ /// Draw borders around the window.
+ ///
+ public virtual void DrawBorders()
+ {
+ var win = RootWindow.Window;
+ if (win.IsMaximized)
+ return;
+
+ if (Editor.Instance.UI.StatusBar == null)
+ return;
+
+ const float thickness = 1.0f;
+ Color color = Editor.Instance.UI.StatusBar.StatusColor;
+ Rectangle rect = new Rectangle(thickness * 0.5f, thickness * 0.5f, Parent.Width - thickness, Parent.Height - thickness);
+ Render2D.DrawRectangle(rect, color);
+ }
+
+ ///
+ public override void OnDestroy()
+ {
+ base.OnDestroy();
+
+ if (_window != null)
+ {
+ _window.Closed -= OnWindowClosed;
+ OnWindowClosed();
+ }
+ }
+}
diff --git a/Source/Editor/Managed/ManagedEditor.Internal.cpp b/Source/Editor/Managed/ManagedEditor.Internal.cpp
index de7532921..2220fb0f3 100644
--- a/Source/Editor/Managed/ManagedEditor.Internal.cpp
+++ b/Source/Editor/Managed/ManagedEditor.Internal.cpp
@@ -402,10 +402,11 @@ DEFINE_INTERNAL_CALL(void) EditorInternal_RunVisualScriptBreakpointLoopTick(floa
break;
}
}
+ WindowsManager::WindowsLocker.Unlock();
for (const auto& e : inputEvents)
{
auto window = e.Target ? e.Target : defaultWindow;
- if (!window)
+ if (!window || window->IsClosed())
continue;
switch (e.Type)
{
@@ -435,12 +436,14 @@ DEFINE_INTERNAL_CALL(void) EditorInternal_RunVisualScriptBreakpointLoopTick(floa
case InputDevice::EventType::MouseMove:
window->OnMouseMove(window->ScreenToClient(e.MouseData.Position));
break;
+ case InputDevice::EventType::MouseMoveRelative:
+ window->OnMouseMoveRelative(e.MouseMovementData.PositionRelative);
+ break;
case InputDevice::EventType::MouseLeave:
window->OnMouseLeave();
break;
}
}
- WindowsManager::WindowsLocker.Unlock();
}
WindowsManager::WindowsLocker.Lock();
Array> windows;
diff --git a/Source/Editor/Modules/ContentDatabaseModule.cs b/Source/Editor/Modules/ContentDatabaseModule.cs
index 53e45ff25..fc139f312 100644
--- a/Source/Editor/Modules/ContentDatabaseModule.cs
+++ b/Source/Editor/Modules/ContentDatabaseModule.cs
@@ -1013,7 +1013,7 @@ namespace FlaxEditor.Modules
ContentItem item;
if (path.EndsWith(".cs"))
item = new CSharpScriptItem(path);
- else if (path.EndsWith(".cpp") || path.EndsWith(".h"))
+ else if (path.EndsWith(".cpp") || path.EndsWith(".h") || path.EndsWith(".c") || path.EndsWith(".hpp"))
item = new CppScriptItem(path);
else if (path.EndsWith(".shader") || path.EndsWith(".hlsl"))
item = new ShaderSourceItem(path);
diff --git a/Source/Editor/Modules/ContentImportingModule.cs b/Source/Editor/Modules/ContentImportingModule.cs
index 2a6358758..8029c7418 100644
--- a/Source/Editor/Modules/ContentImportingModule.cs
+++ b/Source/Editor/Modules/ContentImportingModule.cs
@@ -222,7 +222,7 @@ namespace FlaxEditor.Modules
outputExtension = extension;
// Check if can place source files here
- if (!targetLocation.CanHaveScripts && (extension == ".cs" || extension == ".cpp" || extension == ".h"))
+ if (!targetLocation.CanHaveScripts && (extension == ".cs" || extension == ".cpp" || extension == ".h" || extension == ".c" || extension == ".hpp"))
{
// Error
Editor.LogWarning(string.Format("Cannot import \'{0}\' to \'{1}\'. The target directory cannot have scripts.", inputPath, targetLocation.Node.Path));
diff --git a/Source/Editor/Modules/SimulationModule.cs b/Source/Editor/Modules/SimulationModule.cs
index fb1fb61d1..663c2ab32 100644
--- a/Source/Editor/Modules/SimulationModule.cs
+++ b/Source/Editor/Modules/SimulationModule.cs
@@ -267,7 +267,7 @@ namespace FlaxEditor.Modules
}
///
- public override void OnPlayBegin()
+ public override void OnPlayBeginning()
{
Editor.Windows.FlashMainWindow();
diff --git a/Source/Editor/Modules/UIModule.cs b/Source/Editor/Modules/UIModule.cs
index 7cd7e7fef..d0604aace 100644
--- a/Source/Editor/Modules/UIModule.cs
+++ b/Source/Editor/Modules/UIModule.cs
@@ -16,7 +16,6 @@ using FlaxEditor.Windows;
using FlaxEngine;
using FlaxEngine.GUI;
using FlaxEngine.Json;
-using DockHintWindow = FlaxEditor.GUI.Docking.DockHintWindow;
using MasterDockPanel = FlaxEditor.GUI.Docking.MasterDockPanel;
using FlaxEditor.Content.Settings;
using FlaxEditor.Options;
@@ -29,6 +28,40 @@ namespace FlaxEditor.Modules
///
public sealed class UIModule : EditorModule
{
+ private class MainWindowDecorations : WindowDecorations
+ {
+ public MainWindowDecorations(RootControl window, bool iconOnly)
+ : base(window, iconOnly)
+ {
+ }
+
+ ///
+ public override bool OnKeyDown(KeyboardKeys key)
+ {
+ if (base.OnKeyDown(key))
+ return true;
+
+ // Fallback to the edit window for shortcuts
+ var editor = Editor.Instance;
+ return editor.Windows.EditWin.InputActions.Process(editor, this, key);
+ }
+
+ ///
+ public override void DrawBorders()
+ {
+ // Draw main window borders if using a custom style
+ var win = RootWindow.Window;
+ if (win.IsMaximized)
+ return;
+
+ var color = Editor.Instance.UI.StatusBar.StatusColor;
+ var rect = new Rectangle(0.5f, 0.5f, Parent.Width - 1.0f, Parent.Height - 1.0f - StatusBar.DefaultHeight);
+ Render2D.DrawLine(rect.UpperLeft, rect.UpperRight, color);
+ Render2D.DrawLine(rect.UpperLeft, rect.BottomLeft, color);
+ Render2D.DrawLine(rect.UpperRight, rect.BottomRight, color);
+ }
+ }
+
private struct Status
{
public int ID;
@@ -153,6 +186,11 @@ namespace FlaxEditor.Modules
///
public MainMenu MainMenu;
+ ///
+ /// The window decorations (title bar with buttons)
+ ///
+ public WindowDecorations WindowDecorations;
+
///
/// The tool strip control.
///
@@ -456,19 +494,11 @@ namespace FlaxEditor.Modules
InitToolstrip(mainWindow);
InitStatusBar(mainWindow);
InitDockPanel(mainWindow);
+ InitWindowDecorations(mainWindow);
Editor.Options.OptionsChanged += OnOptionsChanged;
-
- // Add dummy control for drawing the main window borders if using a custom style
-#if PLATFORM_WINDOWS
- if (!Editor.Options.Options.Interface.UseNativeWindowSystem)
-#endif
- {
- mainWindow.AddChild(new CustomWindowBorderControl
- {
- Size = Float2.Zero,
- });
- }
+
+ mainWindow.PerformLayout(true);
}
private void InitViewportScaleOptions()
@@ -540,23 +570,6 @@ namespace FlaxEditor.Modules
}
}
- private class CustomWindowBorderControl : Control
- {
- ///
- public override void Draw()
- {
- var win = RootWindow.Window;
- if (win.IsMaximized)
- return;
-
- var color = Editor.Instance.UI.StatusBar.StatusColor;
- var rect = new Rectangle(0.5f, 0.5f, Parent.Width - 1.0f, Parent.Height - 1.0f - StatusBar.DefaultHeight);
- Render2D.DrawLine(rect.UpperLeft, rect.UpperRight, color);
- Render2D.DrawLine(rect.UpperLeft, rect.BottomLeft, color);
- Render2D.DrawLine(rect.UpperRight, rect.BottomRight, color);
- }
- }
-
///
public override void OnEndInit()
{
@@ -588,13 +601,6 @@ namespace FlaxEditor.Modules
UpdateToolstrip();
}
- ///
- public override void OnExit()
- {
- // Cleanup dock panel hint proxy windows (Flax will destroy them by var but it's better to clear them earlier)
- DockHintWindow.Proxy.Dispose();
- }
-
private IColorPickerDialog ShowPickColorDialog(Control targetControl, Color initialValue, ColorValueBox.ColorPickerEvent colorChanged, ColorValueBox.ColorPickerClosedEvent pickerClosed, bool useDynamicEditing)
{
var dialog = new ColorPickerDialog(initialValue, colorChanged, pickerClosed, useDynamicEditing);
@@ -648,10 +654,12 @@ namespace FlaxEditor.Modules
private void InitMainMenu(RootControl mainWindow)
{
- MainMenu = new MainMenu(mainWindow)
+ MainMenu = new MainMenu()
{
Parent = mainWindow
};
+ if (Utilities.Utils.UseCustomWindowDecorations(isMainWindow: true))
+ MainMenu.Height = 28;
var inputOptions = Editor.Options.Options.Input;
@@ -803,6 +811,20 @@ namespace FlaxEditor.Modules
cm.AddButton("Information about Flax", () => new AboutDialog().Show());
}
+ private void InitWindowDecorations(RootControl mainWindow)
+ {
+ ScriptsBuilder.GetBinariesConfiguration(out _, out _, out _, out var configuration);
+ var driver = Platform.DisplayServer;
+ if (!string.IsNullOrEmpty(driver))
+ driver = $" ({driver})";
+
+ WindowDecorations = new MainWindowDecorations(mainWindow, !Utilities.Utils.UseCustomWindowDecorations(isMainWindow: true))
+ {
+ Parent = mainWindow,
+ IconTooltipText = $"{mainWindow.RootWindow.Title}\nVersion {Globals.EngineVersion}\nConfiguration {configuration}\nGraphics {GPUDevice.Instance.RendererType}{driver}",
+ };
+ }
+
private void OnOptionsChanged(EditorOptions options)
{
var inputOptions = options.Input;
@@ -1227,6 +1249,7 @@ namespace FlaxEditor.Modules
{
// Clear UI references (GUI cannot be used after window closing)
MainMenu = null;
+ WindowDecorations = null;
ToolStrip = null;
MasterPanel = null;
StatusBar = null;
diff --git a/Source/Editor/Modules/WindowsModule.cs b/Source/Editor/Modules/WindowsModule.cs
index 162fa3a40..0d1f0e2d7 100644
--- a/Source/Editor/Modules/WindowsModule.cs
+++ b/Source/Editor/Modules/WindowsModule.cs
@@ -10,7 +10,6 @@ using System.Text;
using System.Xml;
using FlaxEditor.Content;
using FlaxEditor.GUI.Dialogs;
-using FlaxEditor.GUI.Docking;
using FlaxEditor.Windows;
using FlaxEditor.Windows.Assets;
using FlaxEditor.Windows.Profiler;
@@ -763,17 +762,19 @@ namespace FlaxEditor.Modules
var settings = CreateWindowSettings.Default;
settings.Title = "Flax Editor";
settings.Size = Platform.DesktopSize * 0.75f;
+ settings.MinimumSize = new Float2(200, 150);
settings.StartPosition = WindowStartPosition.CenterScreen;
settings.ShowAfterFirstPaint = true;
-#if PLATFORM_WINDOWS
- if (!Editor.Instance.Options.Options.Interface.UseNativeWindowSystem)
+
+ if (Utilities.Utils.UseCustomWindowDecorations(isMainWindow: true))
{
settings.HasBorder = false;
-
+#if PLATFORM_WINDOWS && !PLATFORM_SDL
// Skip OS sizing frame and implement it using LeftButtonHit
settings.HasSizingFrame = false;
+#endif
}
-#elif PLATFORM_LINUX
+#if PLATFORM_LINUX && !PLATFORM_SDL
settings.HasBorder = false;
#endif
MainWindow = Platform.CreateWindow(ref settings);
diff --git a/Source/Editor/Options/InterfaceOptions.cs b/Source/Editor/Options/InterfaceOptions.cs
index f26506c74..172f63f36 100644
--- a/Source/Editor/Options/InterfaceOptions.cs
+++ b/Source/Editor/Options/InterfaceOptions.cs
@@ -179,6 +179,34 @@ namespace FlaxEditor.Options
GameWindowThenRestore,
}
+ ///
+ /// Options for type of window decorations to use.
+ ///
+ public enum WindowDecorationsType
+ {
+ ///
+ /// Determined automatically based on the system and any known compatibility issues with native decorations.
+ ///
+ Auto,
+
+ ///
+ /// Automatically choose most compatible window decorations for child windows, prefer custom decorations on main window.
+ ///
+ [EditorDisplay(Name = "Auto (Child Only)")]
+ AutoChildOnly,
+
+ ///
+ /// Use native system window decorations on all windows.
+ ///
+ Native,
+
+ ///
+ /// Use custom client-side window decorations on all windows.
+ ///
+ [EditorDisplay(Name = "Client-side")]
+ ClientSide,
+ }
+
///
/// Gets or sets the Editor User Interface scale. Applied to all UI elements, windows and text. Can be used to scale the interface up on a bigger display. Editor restart required.
///
@@ -268,7 +296,14 @@ namespace FlaxEditor.Options
[EditorDisplay("Interface"), EditorOrder(322)]
public bool ScrollToScriptOnAdd { get; set; } = true;
-#if PLATFORM_WINDOWS
+#if PLATFORM_SDL
+ ///
+ /// Gets or sets a value indicating whether use native window title bar decorations in child windows. Editor restart required.
+ ///
+ [DefaultValue(WindowDecorationsType.AutoChildOnly)]
+ [EditorDisplay("Interface"), EditorOrder(70), Tooltip("Determines whether use native window title bar decorations. Editor restart required.")]
+ public WindowDecorationsType WindowDecorations { get; set; } = WindowDecorationsType.AutoChildOnly;
+#elif PLATFORM_WINDOWS
///
/// Gets or sets a value indicating whether use native window title bar. Editor restart required.
///
@@ -277,7 +312,7 @@ namespace FlaxEditor.Options
public bool UseNativeWindowSystem { get; set; } = false;
#endif
-#if PLATFORM_WINDOWS
+#if PLATFORM_SDL || PLATFORM_WINDOWS
///
/// Gets or sets a value indicating whether a window containing a single tabs hides the tab bar. Editor restart recommended.
///
diff --git a/Source/Editor/Scripting/ScriptsBuilder.cpp b/Source/Editor/Scripting/ScriptsBuilder.cpp
index 8f33d04d7..0d77c4cdd 100644
--- a/Source/Editor/Scripting/ScriptsBuilder.cpp
+++ b/Source/Editor/Scripting/ScriptsBuilder.cpp
@@ -121,9 +121,13 @@ void ScriptsBuilderImpl::sourceDirEvent(const String& path, FileSystemAction act
// Discard non-source files or generated files
if ((!path.EndsWith(TEXT(".cs")) &&
!path.EndsWith(TEXT(".cpp")) &&
+ !path.EndsWith(TEXT(".c")) &&
+ !path.EndsWith(TEXT(".hpp")) &&
!path.EndsWith(TEXT(".h"))) ||
path.EndsWith(TEXT(".Gen.cs")))
+ {
return;
+ }
ScopeLock scopeLock(_locker);
_lastSourceCodeEdited = DateTime::Now();
diff --git a/Source/Editor/Utilities/ScreenUtilities.cpp b/Source/Editor/Utilities/ScreenUtilities.cpp
deleted file mode 100644
index b35e2817c..000000000
--- a/Source/Editor/Utilities/ScreenUtilities.cpp
+++ /dev/null
@@ -1,131 +0,0 @@
-// Copyright (c) Wojciech Figat. All rights reserved.
-
-#include "ScreenUtilities.h"
-#include "Engine/Core/Math/Vector2.h"
-#include "Engine/Core/Delegate.h"
-#include "Engine/Core/Log.h"
-#include "Engine/Profiler/ProfilerCPU.h"
-
-Delegate ScreenUtilities::PickColorDone;
-
-#if PLATFORM_WINDOWS
-
-#include
-
-#pragma comment(lib, "Gdi32.lib")
-
-static HHOOK MouseCallbackHook;
-
-LRESULT CALLBACK OnScreenUtilsMouseCallback(_In_ int nCode, _In_ WPARAM wParam, _In_ LPARAM lParam)
-{
- if (nCode >= 0 && wParam == WM_LBUTTONDOWN)
- {
- UnhookWindowsHookEx(MouseCallbackHook);
-
- // Push event with the picked color
- const Float2 cursorPos = Platform::GetMousePosition();
- const Color32 colorPicked = ScreenUtilities::GetColorAt(cursorPos);
- ScreenUtilities::PickColorDone(colorPicked);
- return 1;
- }
- return CallNextHookEx(NULL, nCode, wParam, lParam);
-}
-
-Color32 ScreenUtilities::GetColorAt(const Float2& pos)
-{
- PROFILE_CPU();
- HDC deviceContext = GetDC(NULL);
- COLORREF color = GetPixel(deviceContext, (int)pos.X, (int)pos.Y);
- ReleaseDC(NULL, deviceContext);
- return Color32(GetRValue(color), GetGValue(color), GetBValue(color), 255);
-}
-
-void ScreenUtilities::PickColor()
-{
- MouseCallbackHook = SetWindowsHookEx(WH_MOUSE_LL, OnScreenUtilsMouseCallback, NULL, NULL);
- if (MouseCallbackHook == NULL)
- {
- LOG(Warning, "Failed to set mouse hook.");
- LOG(Warning, "Error: {0}", GetLastError());
- }
-}
-
-#elif PLATFORM_LINUX
-
-#include "Engine/Platform/Linux/LinuxPlatform.h"
-#include "Engine/Platform/Linux/IncludeX11.h"
-
-Color32 ScreenUtilities::GetColorAt(const Float2& pos)
-{
- X11::XColor color;
-
- X11::Display* display = (X11::Display*) LinuxPlatform::GetXDisplay();
- int defaultScreen = X11::XDefaultScreen(display);
-
- X11::XImage* image;
- image = X11::XGetImage(display, X11::XRootWindow(display, defaultScreen), (int)pos.X, (int)pos.Y, 1, 1, AllPlanes, XYPixmap);
- color.pixel = XGetPixel(image, 0, 0);
- X11::XFree(image);
-
- X11::XQueryColor(display, X11::XDefaultColormap(display, defaultScreen), &color);
-
- Color32 outputColor;
- outputColor.R = color.red / 256;
- outputColor.G = color.green / 256;
- outputColor.B = color.blue / 256;
- outputColor.A = 255;
- return outputColor;
-}
-
-void OnScreenUtilsXEventCallback(void* eventPtr)
-{
- X11::XEvent* event = (X11::XEvent*) eventPtr;
- X11::Display* display = (X11::Display*)LinuxPlatform::GetXDisplay();
- if (event->type == ButtonPress)
- {
- const Float2 cursorPos = Platform::GetMousePosition();
- const Color32 colorPicked = ScreenUtilities::GetColorAt(cursorPos);
- X11::XUngrabPointer(display, CurrentTime);
- ScreenUtilities::PickColorDone(colorPicked);
- LinuxPlatform::xEventRecieved.Unbind(OnScreenUtilsXEventCallback);
- }
-}
-
-void ScreenUtilities::PickColor()
-{
- PROFILE_CPU();
- X11::Display* display = (X11::Display*) LinuxPlatform::GetXDisplay();
- X11::Window rootWindow = X11::XRootWindow(display, X11::XDefaultScreen(display));
-
- X11::Cursor cursor = XCreateFontCursor(display, 130);
- int grabbedPointer = X11::XGrabPointer(display, rootWindow, 0, ButtonPressMask, GrabModeAsync, GrabModeAsync, rootWindow, cursor, CurrentTime);
- if (grabbedPointer != GrabSuccess)
- {
- LOG(Error, "Failed to grab cursor for events.");
- X11::XFreeCursor(display, cursor);
- return;
- }
-
- X11::XFreeCursor(display, cursor);
- LinuxPlatform::xEventRecieved.Bind(OnScreenUtilsXEventCallback);
-}
-
-#elif PLATFORM_MAC
-
-#include
-#include
-
-Color32 ScreenUtilities::GetColorAt(const Float2& pos)
-{
- // TODO: implement ScreenUtilities for macOS
- return { 0, 0, 0, 255 };
-}
-
-void ScreenUtilities::PickColor()
-{
- // This is what C# calls to start the color picking sequence
- // This should stop mouse clicks from working for one click, and that click is on the selected color
- // There is a class called NSColorSample that might implement that for you, but maybe not.
-}
-
-#endif
diff --git a/Source/Editor/Utilities/ScreenUtilities.h b/Source/Editor/Utilities/ScreenUtilities.h
index 421ecf22f..be946dcae 100644
--- a/Source/Editor/Utilities/ScreenUtilities.h
+++ b/Source/Editor/Utilities/ScreenUtilities.h
@@ -2,32 +2,4 @@
#pragma once
-#include "Engine/Core/Types/BaseTypes.h"
-#include "Engine/Core/Math/Color32.h"
-#include "Engine/Core/Math/Vector2.h"
-#include "Engine/Core/Delegate.h"
-
-///
-/// Platform-dependent screen utilities.
-///
-API_CLASS(Static) class FLAXENGINE_API ScreenUtilities
-{
- DECLARE_SCRIPTING_TYPE_MINIMAL(ScreenUtilities);
-
- ///
- /// Gets the pixel color at the specified coordinates.
- ///
- /// Screen-space coordinate to read.
- /// Pixel color at the specified coordinates.
- API_FUNCTION() static Color32 GetColorAt(const Float2& pos);
-
- ///
- /// Starts async color picking. Color will be returned through PickColorDone event when the actions ends (user selected the final color with a mouse). When action is active, GetColorAt can be used to read the current value.
- ///
- API_FUNCTION() static void PickColor();
-
- ///
- /// Called when PickColor action is finished.
- ///
- API_EVENT() static Delegate PickColorDone;
-};
+#include "Engine/Platform/ScreenUtilities.h"
diff --git a/Source/Editor/Utilities/Utils.cs b/Source/Editor/Utilities/Utils.cs
index 688cec470..ea55edc0b 100644
--- a/Source/Editor/Utilities/Utils.cs
+++ b/Source/Editor/Utilities/Utils.cs
@@ -1583,5 +1583,31 @@ namespace FlaxEditor.Utilities
c = c.Parent;
return c as ISceneEditingContext;
}
+
+ internal static bool UseCustomWindowDecorations(bool isMainWindow = false)
+ {
+ return Editor.Instance.Options.Options.Interface.WindowDecorations switch
+ {
+ Options.InterfaceOptions.WindowDecorationsType.Auto => !Platform.SupportsNativeDecorations,
+ Options.InterfaceOptions.WindowDecorationsType.AutoChildOnly => !isMainWindow ? !Platform.SupportsNativeDecorations : true,
+ Options.InterfaceOptions.WindowDecorationsType.Native => false,
+ Options.InterfaceOptions.WindowDecorationsType.ClientSide => true,
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ }
+
+ internal static bool HideSingleTabWindowTabBars()
+ {
+#if PLATFORM_SDL
+ // We should not hide the tab bars if tab handle is the only way to dock the window
+ bool clientSideDecorations = UseCustomWindowDecorations(false);
+ bool draggableDecorations = clientSideDecorations || Platform.SupportsNativeDecorationDragging;
+ return draggableDecorations && Editor.Instance.Options.Options.Interface.HideSingleTabWindowTabBars;
+#elif PLATFORM_WINDOWS
+ return Editor.Instance.Options.Options.Interface.HideSingleTabWindowTabBars;
+#else
+ return false;
+#endif
+ }
}
}
diff --git a/Source/Editor/Viewport/EditorGizmoViewport.cs b/Source/Editor/Viewport/EditorGizmoViewport.cs
index a28c9f04a..d6153f167 100644
--- a/Source/Editor/Viewport/EditorGizmoViewport.cs
+++ b/Source/Editor/Viewport/EditorGizmoViewport.cs
@@ -77,7 +77,7 @@ namespace FlaxEditor.Viewport
public bool SnapToVertex => ContainsFocus && Editor.Instance.Options.Options.Input.SnapToVertex.Process(Root);
///
- public Float2 MouseDelta => _mouseDelta;
+ public Float2 MouseDelta => FlaxEngine.Input.MousePositionDelta;
///
public bool UseSnapping => Root?.GetKey(KeyboardKeys.Control) ?? false;
diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs
index 1e7d2295a..82789e7e5 100644
--- a/Source/Editor/Viewport/EditorViewport.cs
+++ b/Source/Editor/Viewport/EditorViewport.cs
@@ -4,6 +4,7 @@ using System;
using System.Linq;
using FlaxEditor.Content.Settings;
using FlaxEditor.GUI.ContextMenu;
+using FlaxEditor.GUI.Docking;
using FlaxEditor.GUI.Input;
using FlaxEditor.Options;
using FlaxEditor.Viewport.Cameras;
@@ -158,18 +159,22 @@ namespace FlaxEditor.Viewport
private float _movementSpeed;
private float _minMovementSpeed;
private float _maxMovementSpeed;
+#if !PLATFORM_SDL
private float _mouseAccelerationScale;
private bool _useMouseFiltering;
private bool _useMouseAcceleration;
+#endif
// Input
internal bool _disableInputUpdate;
private bool _isControllingMouse, _isViewportControllingMouse, _wasVirtualMouseRightDown, _isVirtualMouseRightDown;
- private int _deltaFilteringStep;
private Float2 _startPos;
+#if !PLATFORM_SDL
private Float2 _mouseDeltaLast;
+ private int _deltaFilteringStep;
private Float2[] _deltaFilteringBuffer = new Float2[FpsCameraFilteringFrames];
+#endif
///
/// The previous input (from the previous update).
@@ -522,10 +527,11 @@ namespace FlaxEditor.Viewport
: base(task)
{
_editor = Editor.Instance;
-
+#if !PLATFORM_SDL
_mouseAccelerationScale = 0.1f;
_useMouseFiltering = false;
_useMouseAcceleration = false;
+#endif
_camera = camera;
if (_camera != null)
_camera.Viewport = this;
@@ -1530,7 +1536,9 @@ namespace FlaxEditor.Viewport
// Hide cursor and start tracking mouse movement
win.StartTrackingMouse(false);
win.Cursor = CursorType.Hidden;
+ win.MouseMoveRelative += OnMouseMoveRelative;
+#if !PLATFORM_SDL
// Center mouse position if it's too close to the edge
var size = Size;
var center = Float2.Round(size * 0.5f);
@@ -1539,6 +1547,7 @@ namespace FlaxEditor.Viewport
_viewMousePos = center;
win.MousePosition = PointToWindow(_viewMousePos);
}
+#endif
}
///
@@ -1550,6 +1559,7 @@ namespace FlaxEditor.Viewport
// Restore cursor and stop tracking mouse movement
win.Cursor = CursorType.Default;
win.EndTrackingMouse();
+ win.MouseMoveRelative -= OnMouseMoveRelative;
}
///
@@ -1631,18 +1641,15 @@ namespace FlaxEditor.Viewport
// Get parent window
var win = (WindowRootControl)Root;
-
- // Get current mouse position in the view
+ if (win.IsFocused)
{
- // When the window is not focused, the position in window does not return sane values
- Float2 pos = PointFromWindow(win.MousePosition);
- if (!float.IsInfinity(pos.LengthSquared))
- _viewMousePos = pos;
+ // Get current mouse position in the view
+ _viewMousePos = PointFromWindow(win.MousePosition);
}
// Update input
var window = win.Window;
- var canUseInput = window != null && window.IsFocused && window.IsForegroundWindow;
+ var canUseInput = window != null && window.IsFocused && window.IsForegroundWindow && !WindowDragHelper.IsDragActive;
{
// Get input buttons and keys (skip if viewport has no focus or mouse is over a child control)
var isViewportControllingMouse = canUseInput && IsControllingMouse;
@@ -1654,9 +1661,17 @@ namespace FlaxEditor.Viewport
else
EndMouseCapture();
}
- bool useMouse = IsControllingMouse || (Mathf.IsInRange(_viewMousePos.X, 0, Width) && Mathf.IsInRange(_viewMousePos.Y, 0, Height));
+
_prevInput = _input;
+#if PLATFORM_SDL
+ bool useMouse = IsControllingMouse || ContainsPoint(ref _viewMousePos) || _prevInput.IsControllingMouse;
var hit = GetChildAt(_viewMousePos, c => c.Visible && !(c is CanvasRootControl) && !(c is UIEditorRoot));
+ if (_prevInput.IsControllingMouse)
+ hit = null;
+#else
+ bool useMouse = IsControllingMouse || ContainsPoint(ref _viewMousePos);
+ var hit = GetChildAt(_viewMousePos, c => c.Visible && !(c is CanvasRootControl) && !(c is UIEditorRoot));
+#endif
if (canUseInput && ContainsFocus && hit == null)
_input.Gather(win.Window, useMouse, ref _prevInput);
else
@@ -1769,6 +1784,10 @@ namespace FlaxEditor.Viewport
if (_input.IsControlDown)
moveDelta *= 0.3f;
+#if PLATFORM_SDL
+ var mouseDelta = _mouseDelta;
+ _mouseDelta = Float2.Zero;
+#else
// Calculate smooth mouse delta not dependant on viewport size
var offset = _viewMousePos - _startPos;
if (_input.IsZooming && !_input.IsMouseRightDown && !_input.IsMouseLeftDown && !_input.IsMouseMiddleDown && !_isOrtho && !rmbWheel && !_isVirtualMouseRightDown)
@@ -1810,6 +1829,7 @@ namespace FlaxEditor.Viewport
mouseDelta += _mouseDeltaLast * _mouseAccelerationScale;
_mouseDeltaLast = currentDelta;
}
+#endif
// Update
moveDelta *= dt * (60.0f * 4.0f);
@@ -1818,12 +1838,14 @@ namespace FlaxEditor.Viewport
mouseDelta *= new Float2(1, -1);
UpdateView(dt, ref moveDelta, ref mouseDelta, out var centerMouse);
+#if !PLATFORM_SDL
// Move mouse back to the root position
if (centerMouse && (_input.IsMouseRightDown || _input.IsMouseLeftDown || _input.IsMouseMiddleDown || _isVirtualMouseRightDown))
{
var center = PointToWindow(_startPos);
win.MousePosition = center;
}
+#endif
// Change Ortho size on mouse scroll
if (_isOrtho && !rmbWheel)
@@ -1835,6 +1857,8 @@ namespace FlaxEditor.Viewport
}
else
{
+#if PLATFORM_SDL
+#else
if (_input.IsMouseLeftDown || _input.IsMouseRightDown || _isVirtualMouseRightDown)
{
// Calculate smooth mouse delta not dependant on viewport size
@@ -1849,6 +1873,7 @@ namespace FlaxEditor.Viewport
_mouseDelta = Float2.Zero;
}
_mouseDeltaLast = Float2.Zero;
+#endif
if (ContainsFocus)
{
@@ -1898,6 +1923,12 @@ namespace FlaxEditor.Viewport
_input.MouseWheelDelta = 0;
}
+ ///
+ public void OnMouseMoveRelative(ref Float2 mouseMotion)
+ {
+ _mouseDelta += mouseMotion;
+ }
+
///
public override bool OnMouseDown(Float2 location, MouseButton button)
{
@@ -1963,7 +1994,8 @@ namespace FlaxEditor.Viewport
if (_isControllingMouse)
{
- OnControlMouseEnd(RootWindow.Window);
+ if (RootWindow?.Window != null)
+ OnControlMouseEnd(RootWindow.Window);
_isControllingMouse = false;
_isVirtualMouseRightDown = false;
}
diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs
index 3d366809c..1b3078051 100644
--- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs
+++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs
@@ -662,16 +662,25 @@ namespace FlaxEditor.Viewport
// Don't allow rubber band selection when gizmo is controlling mouse, vertex painting mode, or cloth painting is enabled
bool canStart = !(IsControllingMouse || IsRightMouseButtonDown || IsAltKeyDown) &&
- Gizmos?.Active is TransformGizmo && !Gizmos.Active.IsControllingMouse;
+ Gizmos?.Active is TransformGizmo;
_rubberBandSelector.TryCreateRubberBand(canStart, _viewMousePos);
}
+ ///
+ protected override void OnControlMouseBegin(Window win)
+ {
+ _rubberBandSelector.ReleaseRubberBandSelection();
+
+ base.OnControlMouseBegin(win);
+ }
+
///
protected override void OnLeftMouseButtonDown()
{
base.OnLeftMouseButtonDown();
- _rubberBandSelector.TryStartingRubberBandSelection(_viewMousePos);
+ if (!IsAltKeyDown)
+ _rubberBandSelector.TryStartingRubberBandSelection(_viewMousePos);
}
///
diff --git a/Source/Editor/Windows/AboutDialog.cs b/Source/Editor/Windows/AboutDialog.cs
index 78b06f0bc..ec5db8301 100644
--- a/Source/Editor/Windows/AboutDialog.cs
+++ b/Source/Editor/Windows/AboutDialog.cs
@@ -130,6 +130,9 @@ namespace FlaxEditor.Windows
"Mono Project - www.mono-project.com",
#if USE_NETCORE
".NET - www.dotnet.microsoft.com",
+#endif
+#if PLATFORM_SDL
+ "Simple DirectMedia Layer - www.libsdl.org",
#endif
"FreeType Project - www.freetype.org",
"Assimp - www.assimp.sourceforge.net",
diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs
index 872e78509..5f9ad71fd 100644
--- a/Source/Editor/Windows/GameWindow.cs
+++ b/Source/Editor/Windows/GameWindow.cs
@@ -874,7 +874,7 @@ namespace FlaxEditor.Windows
_cursorVisible = Screen.CursorVisible;
_cursorLockMode = Screen.CursorLock;
Screen.CursorVisible = true;
- if (Screen.CursorLock == CursorLockMode.Clipped)
+ if (Screen.CursorLock == CursorLockMode.Clipped || Screen.CursorLock == CursorLockMode.Locked)
Screen.CursorLock = CursorLockMode.None;
// Defocus
@@ -963,8 +963,11 @@ namespace FlaxEditor.Windows
if (Editor.StateMachine.IsPlayMode && !Editor.StateMachine.PlayingState.IsPaused)
{
+ // Make sure the cursor is always in the viewport when cursor is locked
+ bool forceCenter = _cursorLockMode != CursorLockMode.None && !IsMouseOver;
+
// Center mouse in play mode
- if (CenterMouseOnFocus)
+ if (CenterMouseOnFocus || forceCenter)
{
var center = PointToWindow(Size * 0.5f);
Root.MousePosition = center;
@@ -989,9 +992,10 @@ namespace FlaxEditor.Windows
_cursorVisible = Screen.CursorVisible;
_cursorLockMode = Screen.CursorLock;
- // Restore cursor visibility (could be hidden by the game)
+ // Restore cursor state, could be hidden or locked by the game
if (!_cursorVisible)
Screen.CursorVisible = true;
+ Screen.CursorLock = CursorLockMode.None;
if (Editor.IsPlayMode && IsDocked && IsSelected && RootWindow.FocusedControl == null)
{
diff --git a/Source/Editor/Windows/SceneTreeWindow.cs b/Source/Editor/Windows/SceneTreeWindow.cs
index d9ba67aa3..b88a57fdc 100644
--- a/Source/Editor/Windows/SceneTreeWindow.cs
+++ b/Source/Editor/Windows/SceneTreeWindow.cs
@@ -26,7 +26,6 @@ namespace FlaxEditor.Windows
private Tree _tree;
private Panel _sceneTreePanel;
private bool _isUpdatingSelection;
- private bool _isMouseDown;
private bool _blockSceneTreeScroll = false;
private DragAssets _dragAssets;
@@ -374,10 +373,7 @@ namespace FlaxEditor.Windows
return true;
if (buttons == MouseButton.Right)
- {
- _isMouseDown = true;
return true;
- }
return false;
}
@@ -388,10 +384,8 @@ namespace FlaxEditor.Windows
if (base.OnMouseUp(location, buttons))
return true;
- if (_isMouseDown && buttons == MouseButton.Right)
+ if (buttons == MouseButton.Right)
{
- _isMouseDown = false;
-
if (Editor.StateMachine.CurrentState.CanEditScene)
{
// Show context menu
@@ -416,14 +410,6 @@ namespace FlaxEditor.Windows
return false;
}
- ///
- public override void OnLostFocus()
- {
- _isMouseDown = false;
-
- base.OnLostFocus();
- }
-
///
public override DragDropEffect OnDragEnter(ref Float2 location, DragData data)
{
diff --git a/Source/Editor/Windows/SplashScreen.cpp b/Source/Editor/Windows/SplashScreen.cpp
index 54a1a7418..64202e2d6 100644
--- a/Source/Editor/Windows/SplashScreen.cpp
+++ b/Source/Editor/Windows/SplashScreen.cpp
@@ -29,7 +29,7 @@ const Char* SplashScreenQuotes[] =
#elif PLATFORM_LINUX
TEXT("Try it on a Raspberry"),
TEXT("Trying to exit vim"),
- TEXT("Sudo flax --loadproject"),
+ TEXT("sudo flax --project HelloWorld.flaxproj"),
#elif PLATFORM_MAC
TEXT("don't compare Macbooks to oranges."),
TEXT("Why does macbook heat up?\nBecause it doesn't have windows"),
@@ -105,6 +105,7 @@ const Char* SplashScreenQuotes[] =
TEXT("You have my bow.\nAnd my axe!"),
TEXT("To the bridge of Khazad-dum."),
TEXT("One ring to rule them all.\nOne ring to find them."),
+ TEXT("Where there's a whip, there's a way."),
TEXT("That's what she said"),
TEXT("We could be compiling shaders here"),
TEXT("Hello There"),
@@ -166,7 +167,7 @@ void SplashScreen::Show()
settings.AllowMaximize = false;
settings.AllowDragAndDrop = false;
settings.IsTopmost = false;
- settings.IsRegularWindow = false;
+ settings.Type = WindowType::Utility;
settings.HasSizingFrame = false;
settings.ShowAfterFirstPaint = true;
settings.StartPosition = WindowStartPosition::CenterScreen;
diff --git a/Source/Engine/Engine/CommandLine.cpp b/Source/Engine/Engine/CommandLine.cpp
index e0f9441eb..232fa91e4 100644
--- a/Source/Engine/Engine/CommandLine.cpp
+++ b/Source/Engine/Engine/CommandLine.cpp
@@ -3,6 +3,7 @@
#include "CommandLine.h"
#include "Engine/Core/Collections/Array.h"
#include "Engine/Core/Utilities.h"
+#include "Engine/Core/Types/StringView.h"
#include
CommandLine::OptionsData CommandLine::Options;
@@ -145,6 +146,12 @@ bool CommandLine::Parse(const Char* cmdLine)
PARSE_BOOL_SWITCH("-monolog ", MonoLog);
PARSE_BOOL_SWITCH("-mute ", Mute);
PARSE_BOOL_SWITCH("-lowdpi ", LowDPI);
+
+#if PLATFORM_LINUX && PLATFORM_SDL
+ PARSE_BOOL_SWITCH("-wayland ", Wayland);
+ PARSE_BOOL_SWITCH("-x11 ", X11);
+#endif
+
#if USE_EDITOR
PARSE_BOOL_SWITCH("-clearcache ", ClearCache);
PARSE_BOOL_SWITCH("-clearcooker ", ClearCookerCache);
@@ -163,3 +170,63 @@ bool CommandLine::Parse(const Char* cmdLine)
return false;
}
+
+bool CommandLine::ParseArguments(const StringView& cmdLine, Array& arguments)
+{
+ int32 start = 0;
+ int32 quotesStart = -1;
+ int32 length = cmdLine.Length();
+ for (int32 i = 0; i < length; i++)
+ {
+ if (cmdLine[i] == ' ' && quotesStart == -1)
+ {
+ int32 count = i - start;
+ if (count > 0)
+ arguments.Add(StringAnsi(cmdLine.Substring(start, count)));
+ start = i + 1;
+ }
+ else if (cmdLine[i] == '\"')
+ {
+ if (quotesStart >= 0)
+ {
+ if (i + 1 < length && cmdLine[i + 1] != ' ')
+ {
+ // End quotes are in the middle of the current word,
+ // continue until the end of the current word.
+ }
+ else
+ {
+ int32 offset = 1;
+ if (quotesStart == start && cmdLine[start] == '\"')
+ {
+ // Word starts and ends with quotes, only include the quoted content.
+ quotesStart++;
+ offset--;
+ }
+ else if (quotesStart != start)
+ {
+ // Start quotes in the middle of the word, include the whole word.
+ quotesStart = start;
+ }
+
+ int32 count = i - quotesStart + offset;
+ if (count > 0)
+ arguments.Add(StringAnsi(cmdLine.Substring(quotesStart, count)));
+ start = i + 1;
+ }
+ quotesStart = -1;
+ }
+ else
+ {
+ quotesStart = i;
+ }
+ }
+ }
+ const int32 count = length - start;
+ if (count > 0)
+ arguments.Add(StringAnsi(cmdLine.Substring(start, count)));
+ if (quotesStart >= 0)
+ return true; // Missing last closing quote
+
+ return false;
+}
diff --git a/Source/Engine/Engine/CommandLine.h b/Source/Engine/Engine/CommandLine.h
index ad49bee04..1733afc1a 100644
--- a/Source/Engine/Engine/CommandLine.h
+++ b/Source/Engine/Engine/CommandLine.h
@@ -4,6 +4,7 @@
#include "Engine/Core/Types/String.h"
#include "Engine/Core/Types/Nullable.h"
+#include "Engine/Core/Collections/Array.h"
///
/// Command line options helper.
@@ -127,6 +128,20 @@ public:
///
Nullable LowDPI;
+#if PLATFORM_LINUX && PLATFORM_SDL
+
+ ///
+ /// -wayland (prefer Wayland over X11 as display server)
+ ///
+ Nullable Wayland;
+
+ ///
+ /// -x11 (prefer X11 over Wayland as display server)
+ ///
+ Nullable X11;
+
+#endif
+
#if USE_EDITOR
///
/// -project !path! (Startup project path)
@@ -200,4 +215,12 @@ public:
/// The command line.
/// True if failed, otherwise false.
static bool Parse(const Char* cmdLine);
+
+ ///
+ /// Parses the command line arguments string into string list of arguments.
+ ///
+ /// The command line.
+ /// The parsed arguments
+ /// True if failed, otherwise false.
+ static bool ParseArguments(const StringView& cmdLine, Array& arguments);
};
diff --git a/Source/Engine/Engine/Engine.cpp b/Source/Engine/Engine/Engine.cpp
index c0f1e06aa..3e1080d4f 100644
--- a/Source/Engine/Engine/Engine.cpp
+++ b/Source/Engine/Engine/Engine.cpp
@@ -101,6 +101,8 @@ int32 Engine::Main(const Char* cmdLine)
CommandLine::Options.Std = true;
#endif
+ Platform::SetHighDpiAwarenessEnabled(!CommandLine::Options.LowDPI.IsTrue());
+
if (Platform::Init())
{
Platform::Fatal(TEXT("Cannot init platform."));
@@ -110,7 +112,6 @@ int32 Engine::Main(const Char* cmdLine)
InitProfilerMemory(cmdLine, 1);
#endif
- Platform::SetHighDpiAwarenessEnabled(!CommandLine::Options.LowDPI.IsTrue());
Time::StartupTime = DateTime::Now();
Globals::StartupFolder = Globals::BinariesFolder = Platform::GetMainDirectory();
#if USE_EDITOR
diff --git a/Source/Engine/Engine/Screen.cpp b/Source/Engine/Engine/Screen.cpp
index e0c1273f1..d2fb7db3d 100644
--- a/Source/Engine/Engine/Screen.cpp
+++ b/Source/Engine/Engine/Screen.cpp
@@ -6,6 +6,8 @@
#include "Engine/Core/Types/Nullable.h"
#include "Engine/Platform/Window.h"
#include "Engine/Engine/EngineService.h"
+#include "Engine/Input/Input.h"
+#include "Engine/Input/Mouse.h"
#if USE_EDITOR
#include "Editor/Editor.h"
#include "Editor/Managed/ManagedEditor.h"
@@ -13,10 +15,14 @@
#include "Engine/Engine/Engine.h"
#endif
-Nullable Fullscreen;
-Nullable Size;
-bool CursorVisible = true;
-CursorLockMode CursorLock = CursorLockMode::None;
+namespace
+{
+ Nullable Fullscreen;
+ Nullable Size;
+ bool CursorVisible = true;
+ CursorLockMode CursorLock = CursorLockMode::None;
+ bool LastGameViewportFocus = false;
+}
class ScreenService : public EngineService
{
@@ -101,9 +107,9 @@ void Screen::SetCursorVisible(const bool value)
const auto win = Engine::MainWindow;
#endif
if (win && Engine::HasGameViewportFocus())
- {
win->SetCursor(value ? CursorType::Default : CursorType::Hidden);
- }
+ else if (win)
+ win->SetCursor(CursorType::Default);
CursorVisible = value;
}
@@ -116,21 +122,31 @@ void Screen::SetCursorLock(CursorLockMode mode)
{
#if USE_EDITOR
const auto win = Editor::Managed->GetGameWindow(true);
+ Rectangle bounds(Editor::Managed->GameViewportToScreen(Float2::Zero), Editor::Managed->GetGameWindowSize());
+ if (win)
+ bounds = Rectangle(win->ScreenToClient(bounds.GetTopLeft()), bounds.Size);
#else
const auto win = Engine::MainWindow;
+ Rectangle bounds = win != nullptr ? win->GetClientBounds() : Rectangle();
#endif
- if (win && mode == CursorLockMode::Clipped)
+ if (win)
{
-#if USE_EDITOR
- Rectangle bounds(Editor::Managed->GameViewportToScreen(Float2::Zero), Editor::Managed->GetGameWindowSize());
-#else
- Rectangle bounds = win->GetClientBounds();
-#endif
- win->StartClippingCursor(bounds);
- }
- else if (win && CursorLock == CursorLockMode::Clipped)
- {
- win->EndClippingCursor();
+ bool inRelativeMode = Input::Mouse->IsRelative();
+ if (mode == CursorLockMode::Clipped)
+ win->StartClippingCursor(bounds);
+ else if (mode == CursorLockMode::Locked)
+ {
+ // Use mouse clip region to restrict the cursor in one spot
+ win->StartClippingCursor(Rectangle(bounds.GetCenter(), Float2(1, 1)));
+ }
+ else if (CursorLock == CursorLockMode::Locked || CursorLock == CursorLockMode::Clipped)
+ win->EndClippingCursor();
+
+ // Enable relative mode when cursor is restricted
+ if (mode != CursorLockMode::None)
+ Input::Mouse->SetRelativeMode(true, win);
+ else if (mode == CursorLockMode::None && inRelativeMode)
+ Input::Mouse->SetRelativeMode(false, win);
}
CursorLock = mode;
}
@@ -190,7 +206,11 @@ void ScreenService::Update()
{
#if USE_EDITOR
// Sync current cursor state in Editor (eg. when viewport focus can change)
- Screen::SetCursorVisible(CursorVisible);
+ const auto win = Editor::Managed->GetGameWindow(true);
+ bool gameViewportFocus = win && Engine::HasGameViewportFocus();
+ if (gameViewportFocus != LastGameViewportFocus)
+ Screen::SetCursorVisible(CursorVisible);
+ LastGameViewportFocus = gameViewportFocus;
#endif
}
diff --git a/Source/Engine/Engine/Time.h b/Source/Engine/Engine/Time.h
index 724f19c58..bda7a7537 100644
--- a/Source/Engine/Engine/Time.h
+++ b/Source/Engine/Engine/Time.h
@@ -16,6 +16,7 @@ API_CLASS(Static, Attributes="DebugCommand") class FLAXENGINE_API Time
friend class Engine;
friend class TimeService;
friend class PhysicsSettings;
+ friend Window;
public:
///
diff --git a/Source/Engine/GraphicsDevice/Vulkan/Android/AndroidVulkanPlatform.cpp b/Source/Engine/GraphicsDevice/Vulkan/Android/AndroidVulkanPlatform.cpp
index 81e638ba4..3d4183be0 100644
--- a/Source/Engine/GraphicsDevice/Vulkan/Android/AndroidVulkanPlatform.cpp
+++ b/Source/Engine/GraphicsDevice/Vulkan/Android/AndroidVulkanPlatform.cpp
@@ -4,6 +4,7 @@
#include "AndroidVulkanPlatform.h"
#include "../RenderToolsVulkan.h"
+#include "Engine/Platform/Window.h"
void AndroidVulkanPlatform::GetInstanceExtensions(Array& extensions, Array& layers)
{
@@ -17,8 +18,10 @@ void AndroidVulkanPlatform::GetDeviceExtensions(Array& extensions,
extensions.Add(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME);
}
-void AndroidVulkanPlatform::CreateSurface(void* windowHandle, VkInstance instance, VkSurfaceKHR* surface)
+void AndroidVulkanPlatform::CreateSurface(Window* window, GPUDeviceVulkan* device, VkInstance instance, VkSurfaceKHR* surface)
{
+ ASSERT(window);
+ void* windowHandle = window->GetNativePtr();
ASSERT(windowHandle);
VkAndroidSurfaceCreateInfoKHR surfaceCreateInfo;
RenderToolsVulkan::ZeroStruct(surfaceCreateInfo, VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR);
diff --git a/Source/Engine/GraphicsDevice/Vulkan/Android/AndroidVulkanPlatform.h b/Source/Engine/GraphicsDevice/Vulkan/Android/AndroidVulkanPlatform.h
index d2da83892..9a7dbe5b8 100644
--- a/Source/Engine/GraphicsDevice/Vulkan/Android/AndroidVulkanPlatform.h
+++ b/Source/Engine/GraphicsDevice/Vulkan/Android/AndroidVulkanPlatform.h
@@ -9,6 +9,8 @@
// Support more backbuffers in case driver decides to use more
#define VULKAN_BACK_BUFFERS_COUNT_MAX 8
+class GPUDeviceVulkan;
+
///
/// The implementation for the Vulkan API support for Android platform.
///
@@ -17,7 +19,7 @@ class AndroidVulkanPlatform : public VulkanPlatformBase
public:
static void GetInstanceExtensions(Array& extensions, Array& layers);
static void GetDeviceExtensions(Array& extensions, Array& layers);
- static void CreateSurface(void* windowHandle, VkInstance instance, VkSurfaceKHR* surface);
+ static void CreateSurface(Window* window, GPUDeviceVulkan* device, VkInstance instance, VkSurfaceKHR* surface);
};
typedef AndroidVulkanPlatform VulkanPlatform;
diff --git a/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.cpp b/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.cpp
index a62e9023e..1d8625c10 100644
--- a/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.cpp
+++ b/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.cpp
@@ -217,7 +217,7 @@ bool GPUSwapChainVulkan::CreateSwapChain(int32 width, int32 height)
ASSERT_LOW_LAYER(_backBuffers.Count() == 0);
// Create platform-dependent surface
- VulkanPlatform::CreateSurface(windowHandle, GPUDeviceVulkan::Instance, &_surface);
+ VulkanPlatform::CreateSurface(_window, _device, GPUDeviceVulkan::Instance, &_surface);
if (_surface == VK_NULL_HANDLE)
{
LOG(Warning, "Failed to create Vulkan surface.");
@@ -356,13 +356,22 @@ bool GPUSwapChainVulkan::CreateSwapChain(int32 width, int32 height)
LOG(Error, "Vulkan swapchain dimensions are invalid {}x{} (minImageExtent={}x{}, maxImageExtent={}x{})", width, height, surfProperties.minImageExtent.width, surfProperties.minImageExtent.height, surfProperties.maxImageExtent.width, surfProperties.maxImageExtent.height);
return true;
}
+
+ uint32_t backbuffersCount = VULKAN_BACK_BUFFERS_COUNT;
+#if PLATFORM_SDL && PLATFORM_LINUX && USE_EDITOR
+ // Wayland compositor might block one of the backbuffers while the window is minimized or fully occluded,
+ // make sure we have at least 3 backbuffers available so double-buffering can be used while we are blocked.
+ if (Platform::UsesWayland())
+ backbuffersCount = Math::Max(backbuffersCount, 3);
+#endif
+
ASSERT(surfProperties.minImageCount <= VULKAN_BACK_BUFFERS_COUNT_MAX);
VkSwapchainCreateInfoKHR swapChainInfo;
RenderToolsVulkan::ZeroStruct(swapChainInfo, VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR);
swapChainInfo.surface = _surface;
swapChainInfo.minImageCount = surfProperties.maxImageCount > 0 // A value of 0 means that there is no limit on the number of image
- ? Math::Min(VULKAN_BACK_BUFFERS_COUNT, surfProperties.maxImageCount)
- : VULKAN_BACK_BUFFERS_COUNT;
+ ? Math::Min(backbuffersCount, surfProperties.maxImageCount)
+ : backbuffersCount;
swapChainInfo.minImageCount = Math::Max(swapChainInfo.minImageCount, surfProperties.minImageCount);
swapChainInfo.minImageCount = Math::Min(swapChainInfo.minImageCount, VULKAN_BACK_BUFFERS_COUNT_MAX);
swapChainInfo.imageFormat = result.format;
@@ -381,7 +390,9 @@ bool GPUSwapChainVulkan::CreateSwapChain(int32 width, int32 height)
swapChainInfo.presentMode = presentMode;
swapChainInfo.clipped = VK_TRUE;
swapChainInfo.compositeAlpha = VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR;
- if (surfProperties.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR)
+ if (_window->GetSettings().SupportsTransparency && surfProperties.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR)
+ swapChainInfo.compositeAlpha = VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR;
+ else if (surfProperties.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR)
swapChainInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
// Create swap chain
diff --git a/Source/Engine/GraphicsDevice/Vulkan/Linux/LinuxVulkanPlatform.cpp b/Source/Engine/GraphicsDevice/Vulkan/Linux/LinuxVulkanPlatform.cpp
index 15ea6ca4e..d8912a767 100644
--- a/Source/Engine/GraphicsDevice/Vulkan/Linux/LinuxVulkanPlatform.cpp
+++ b/Source/Engine/GraphicsDevice/Vulkan/Linux/LinuxVulkanPlatform.cpp
@@ -4,62 +4,61 @@
#include "LinuxVulkanPlatform.h"
#include "../RenderToolsVulkan.h"
+#include "Engine/Platform/Window.h"
-// Contents of vulkan\vulkan_xlib.h inlined here to prevent typename collisions with engine types due to X11 types
#include "Engine/Platform/Linux/IncludeX11.h"
-#ifdef __cplusplus
-extern "C" {
-#endif
-#define VK_KHR_xlib_surface 1
-#define VK_KHR_XLIB_SURFACE_SPEC_VERSION 6
-#define VK_KHR_XLIB_SURFACE_EXTENSION_NAME "VK_KHR_xlib_surface"
-typedef VkFlags VkXlibSurfaceCreateFlagsKHR;
+#define Display X11::Display
+#define Window X11::Window
+#define VisualID X11::VisualID
+#include "vulkan/vulkan_xlib.h"
+#undef Display
+#undef Window
+#undef VisualID
-typedef struct VkXlibSurfaceCreateInfoKHR
-{
- VkStructureType sType;
- const void* pNext;
- VkXlibSurfaceCreateFlagsKHR flags;
- X11::Display* dpy;
- X11::Window window;
-} VkXlibSurfaceCreateInfoKHR;
+#include "vulkan/vulkan_wayland.h"
-typedef VkResult (VKAPI_PTR *PFN_vkCreateXlibSurfaceKHR)(VkInstance instance, const VkXlibSurfaceCreateInfoKHR* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkSurfaceKHR* pSurface);
-typedef VkBool32 (VKAPI_PTR *PFN_vkGetPhysicalDeviceXlibPresentationSupportKHR)(VkPhysicalDevice physicalDevice, uint32_t queueFamilyIndex, X11::Display* dpy, X11::VisualID visualID);
-#ifndef VK_NO_PROTOTYPES
-VKAPI_ATTR VkResult VKAPI_CALL vkCreateXlibSurfaceKHR(
- VkInstance instance,
- const VkXlibSurfaceCreateInfoKHR* pCreateInfo,
- const VkAllocationCallbacks* pAllocator,
- VkSurfaceKHR* pSurface);
-VKAPI_ATTR VkBool32 VKAPI_CALL vkGetPhysicalDeviceXlibPresentationSupportKHR(
- VkPhysicalDevice physicalDevice,
- uint32_t queueFamilyIndex,
- Display* dpy,
- VisualID visualID);
-#endif
-#ifdef __cplusplus
-}
-#endif
-//
-
-// Export X11 surface extension from volk
+// Export extension from volk
extern PFN_vkCreateXlibSurfaceKHR vkCreateXlibSurfaceKHR;
extern PFN_vkGetPhysicalDeviceXlibPresentationSupportKHR vkGetPhysicalDeviceXlibPresentationSupportKHR;
+extern PFN_vkCreateWaylandSurfaceKHR vkCreateWaylandSurfaceKHR;
+extern PFN_vkGetPhysicalDeviceWaylandPresentationSupportKHR vkGetPhysicalDeviceWaylandPresentationSupportKHR;
void LinuxVulkanPlatform::GetInstanceExtensions(Array& extensions, Array& layers)
{
extensions.Add(VK_KHR_SURFACE_EXTENSION_NAME);
extensions.Add(VK_KHR_XLIB_SURFACE_EXTENSION_NAME);
+ extensions.Add(VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME);
}
-void LinuxVulkanPlatform::CreateSurface(void* windowHandle, VkInstance instance, VkSurfaceKHR* surface)
+void LinuxVulkanPlatform::CreateSurface(Window* window, GPUDeviceVulkan* device, VkInstance instance, VkSurfaceKHR* surface)
{
+#if !PLATFORM_SDL
+ void* windowHandle = window->GetNativePtr();
VkXlibSurfaceCreateInfoKHR surfaceCreateInfo;
RenderToolsVulkan::ZeroStruct(surfaceCreateInfo, VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR);
- surfaceCreateInfo.dpy = (X11::Display*)LinuxPlatform::GetXDisplay();
+ surfaceCreateInfo.dpy = (X11::Display*)Platform::GetXDisplay();
surfaceCreateInfo.window = (X11::Window)windowHandle;
VALIDATE_VULKAN_RESULT(vkCreateXlibSurfaceKHR(instance, &surfaceCreateInfo, nullptr, surface));
+#else
+ SDLWindow* sdlWindow = static_cast(window);
+ void* windowHandle = window->GetNativePtr();
+ if (SDLPlatform::UsesWayland())
+ {
+ VkWaylandSurfaceCreateInfoKHR surfaceCreateInfo;
+ RenderToolsVulkan::ZeroStruct(surfaceCreateInfo, VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR);
+ surfaceCreateInfo.display = (wl_display*)sdlWindow->GetWaylandDisplay();
+ surfaceCreateInfo.surface = (wl_surface*)windowHandle;
+ VALIDATE_VULKAN_RESULT(vkCreateWaylandSurfaceKHR(instance, &surfaceCreateInfo, nullptr, surface));
+ }
+ else if (SDLPlatform::UsesX11())
+ {
+ VkXlibSurfaceCreateInfoKHR surfaceCreateInfo;
+ RenderToolsVulkan::ZeroStruct(surfaceCreateInfo, VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR);
+ surfaceCreateInfo.dpy = (X11::Display*)sdlWindow->GetX11Display();
+ surfaceCreateInfo.window = (X11::Window)windowHandle;
+ VALIDATE_VULKAN_RESULT(vkCreateXlibSurfaceKHR(instance, &surfaceCreateInfo, nullptr, surface));
+ }
+#endif
}
#endif
diff --git a/Source/Engine/GraphicsDevice/Vulkan/Linux/LinuxVulkanPlatform.h b/Source/Engine/GraphicsDevice/Vulkan/Linux/LinuxVulkanPlatform.h
index 69b8f040f..11d2008cd 100644
--- a/Source/Engine/GraphicsDevice/Vulkan/Linux/LinuxVulkanPlatform.h
+++ b/Source/Engine/GraphicsDevice/Vulkan/Linux/LinuxVulkanPlatform.h
@@ -12,6 +12,8 @@
// Prevent wierd error 'Invalid VkValidationCacheEXT Object'
#define VULKAN_USE_VALIDATION_CACHE 0
+class GPUDeviceVulkan;
+
///
/// The implementation for the Vulkan API support for Linux platform.
///
@@ -19,7 +21,7 @@ class LinuxVulkanPlatform : public VulkanPlatformBase
{
public:
static void GetInstanceExtensions(Array& extensions, Array& layers);
- static void CreateSurface(void* windowHandle, VkInstance instance, VkSurfaceKHR* outSurface);
+ static void CreateSurface(Window* window, GPUDeviceVulkan* device, VkInstance instance, VkSurfaceKHR* outSurface);
};
typedef LinuxVulkanPlatform VulkanPlatform;
diff --git a/Source/Engine/GraphicsDevice/Vulkan/Mac/MacVulkanPlatform.cpp b/Source/Engine/GraphicsDevice/Vulkan/Mac/MacVulkanPlatform.cpp
index eb68f9d71..a2cfc07e4 100644
--- a/Source/Engine/GraphicsDevice/Vulkan/Mac/MacVulkanPlatform.cpp
+++ b/Source/Engine/GraphicsDevice/Vulkan/Mac/MacVulkanPlatform.cpp
@@ -4,21 +4,40 @@
#include "MacVulkanPlatform.h"
#include "../RenderToolsVulkan.h"
+#include "Engine/Platform/Window.h"
+#include "Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.h"
#include
+#include
void MacVulkanPlatform::GetInstanceExtensions(Array& extensions, Array& layers)
{
extensions.Add(VK_KHR_SURFACE_EXTENSION_NAME);
- extensions.Add(VK_MVK_MACOS_SURFACE_EXTENSION_NAME);
+ extensions.Add(VK_MVK_MACOS_SURFACE_EXTENSION_NAME);
+ extensions.Add(VK_EXT_METAL_SURFACE_EXTENSION_NAME);
}
-void MacVulkanPlatform::CreateSurface(void* windowHandle, VkInstance instance, VkSurfaceKHR* surface)
+void MacVulkanPlatform::CreateSurface(Window* window, GPUDeviceVulkan* device, VkInstance instance, VkSurfaceKHR* surface)
{
- NSWindow* window = (NSWindow*)windowHandle;
- VkMacOSSurfaceCreateInfoMVK surfaceCreateInfo;
- RenderToolsVulkan::ZeroStruct(surfaceCreateInfo, VK_STRUCTURE_TYPE_MACOS_SURFACE_CREATE_INFO_MVK);
- surfaceCreateInfo.pView = (void*)window.contentView;
- VALIDATE_VULKAN_RESULT(vkCreateMacOSSurfaceMVK(instance, &surfaceCreateInfo, nullptr, surface));
+ void* windowHandle = window->GetNativePtr();
+ NSWindow* nswindow = (NSWindow*)windowHandle;
+#if PLATFORM_SDL
+ nswindow.contentView.wantsLayer = YES;
+ nswindow.contentView.layer = [CAMetalLayer layer];
+#endif
+ if (device->InstanceExtensions.Contains(VK_EXT_METAL_SURFACE_EXTENSION_NAME))
+ {
+ VkMetalSurfaceCreateInfoEXT surfaceCreateInfo;
+ RenderToolsVulkan::ZeroStruct(surfaceCreateInfo, VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT);
+ surfaceCreateInfo.pLayer = (CAMetalLayer*)nswindow.contentView.layer;
+ VALIDATE_VULKAN_RESULT(vkCreateMetalSurfaceEXT(instance, &surfaceCreateInfo, nullptr, surface));
+ }
+ else
+ {
+ VkMacOSSurfaceCreateInfoMVK surfaceCreateInfo;
+ RenderToolsVulkan::ZeroStruct(surfaceCreateInfo, VK_STRUCTURE_TYPE_MACOS_SURFACE_CREATE_INFO_MVK);
+ surfaceCreateInfo.pView = (void*)nswindow.contentView;
+ VALIDATE_VULKAN_RESULT(vkCreateMacOSSurfaceMVK(instance, &surfaceCreateInfo, nullptr, surface));
+ }
}
#endif
diff --git a/Source/Engine/GraphicsDevice/Vulkan/Mac/MacVulkanPlatform.h b/Source/Engine/GraphicsDevice/Vulkan/Mac/MacVulkanPlatform.h
index 05ed07792..2f4f93d2f 100644
--- a/Source/Engine/GraphicsDevice/Vulkan/Mac/MacVulkanPlatform.h
+++ b/Source/Engine/GraphicsDevice/Vulkan/Mac/MacVulkanPlatform.h
@@ -11,6 +11,8 @@
// General/Validation Error:0 VK_ERROR_INITIALIZATION_FAILED: Could not create MTLCounterSampleBuffer for query pool of type VK_QUERY_TYPE_TIMESTAMP. Reverting to emulated behavior. (Error code 0): Cannot allocate sample buffer
#define VULKAN_USE_TIMER_QUERIES 0
+class GPUDeviceVulkan;
+
///
/// The implementation for the Vulkan API support for Mac platform.
///
@@ -18,7 +20,7 @@ class MacVulkanPlatform : public VulkanPlatformBase
{
public:
static void GetInstanceExtensions(Array& extensions, Array& layers);
- static void CreateSurface(void* windowHandle, VkInstance instance, VkSurfaceKHR* outSurface);
+ static void CreateSurface(Window* window, GPUDeviceVulkan* device, VkInstance instance, VkSurfaceKHR* outSurface);
};
typedef MacVulkanPlatform VulkanPlatform;
diff --git a/Source/Engine/GraphicsDevice/Vulkan/Win32/Win32VulkanPlatform.cpp b/Source/Engine/GraphicsDevice/Vulkan/Win32/Win32VulkanPlatform.cpp
index 2dc1c8455..08bd300ae 100644
--- a/Source/Engine/GraphicsDevice/Vulkan/Win32/Win32VulkanPlatform.cpp
+++ b/Source/Engine/GraphicsDevice/Vulkan/Win32/Win32VulkanPlatform.cpp
@@ -6,6 +6,7 @@
#include "../RenderToolsVulkan.h"
#include "Engine/Graphics/GPUDevice.h"
+#include "Engine/Platform/Window.h"
void Win32VulkanPlatform::GetInstanceExtensions(Array& extensions, Array& layers)
{
@@ -13,8 +14,9 @@ void Win32VulkanPlatform::GetInstanceExtensions(Array& extensions,
extensions.Add(VK_KHR_WIN32_SURFACE_EXTENSION_NAME);
}
-void Win32VulkanPlatform::CreateSurface(void* windowHandle, VkInstance instance, VkSurfaceKHR* surface)
+void Win32VulkanPlatform::CreateSurface(Window* window, GPUDeviceVulkan* device, VkInstance instance, VkSurfaceKHR* surface)
{
+ void* windowHandle = window->GetNativePtr();
VkWin32SurfaceCreateInfoKHR surfaceCreateInfo;
RenderToolsVulkan::ZeroStruct(surfaceCreateInfo, VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR);
surfaceCreateInfo.hinstance = GetModuleHandle(nullptr);
diff --git a/Source/Engine/GraphicsDevice/Vulkan/Win32/Win32VulkanPlatform.h b/Source/Engine/GraphicsDevice/Vulkan/Win32/Win32VulkanPlatform.h
index 08ad578a4..2de527e2f 100644
--- a/Source/Engine/GraphicsDevice/Vulkan/Win32/Win32VulkanPlatform.h
+++ b/Source/Engine/GraphicsDevice/Vulkan/Win32/Win32VulkanPlatform.h
@@ -10,6 +10,8 @@
#define VULKAN_USE_PLATFORM_WIN32_KHX 1
#define VULKAN_USE_CREATE_WIN32_SURFACE 1
+class GPUDeviceVulkan;
+
///
/// The implementation for the Vulkan API support for Win32 platform.
///
@@ -17,7 +19,7 @@ class Win32VulkanPlatform : public VulkanPlatformBase
{
public:
static void GetInstanceExtensions(Array& extensions, Array& layers);
- static void CreateSurface(void* windowHandle, VkInstance instance, VkSurfaceKHR* surface);
+ static void CreateSurface(Window* window, GPUDeviceVulkan* device, VkInstance instance, VkSurfaceKHR* surface);
};
typedef Win32VulkanPlatform VulkanPlatform;
diff --git a/Source/Engine/GraphicsDevice/Vulkan/iOS/iOSVulkanPlatform.cpp b/Source/Engine/GraphicsDevice/Vulkan/iOS/iOSVulkanPlatform.cpp
index f5b732f44..58b4e3183 100644
--- a/Source/Engine/GraphicsDevice/Vulkan/iOS/iOSVulkanPlatform.cpp
+++ b/Source/Engine/GraphicsDevice/Vulkan/iOS/iOSVulkanPlatform.cpp
@@ -5,6 +5,7 @@
#include "iOSVulkanPlatform.h"
#include "../RenderToolsVulkan.h"
#include "Engine/Core/Delegate.h"
+#include "Engine/Platform/Window.h"
#include
void iOSVulkanPlatform::GetInstanceExtensions(Array& extensions, Array& layers)
@@ -13,8 +14,9 @@ void iOSVulkanPlatform::GetInstanceExtensions(Array& extensions, Ar
extensions.Add(VK_MVK_IOS_SURFACE_EXTENSION_NAME);
}
-void iOSVulkanPlatform::CreateSurface(void* windowHandle, VkInstance instance, VkSurfaceKHR* surface)
+void iOSVulkanPlatform::CreateSurface(Window* window, GPUDeviceVulkan* device, VkInstance instance, VkSurfaceKHR* surface)
{
+ void* windowHandle = window->GetNativePtr();
// Create surface on a main UI Thread
Function func = [&windowHandle, &instance, &surface]()
{
diff --git a/Source/Engine/GraphicsDevice/Vulkan/iOS/iOSVulkanPlatform.h b/Source/Engine/GraphicsDevice/Vulkan/iOS/iOSVulkanPlatform.h
index 8db71ec6e..5fc306e36 100644
--- a/Source/Engine/GraphicsDevice/Vulkan/iOS/iOSVulkanPlatform.h
+++ b/Source/Engine/GraphicsDevice/Vulkan/iOS/iOSVulkanPlatform.h
@@ -11,6 +11,8 @@
// General/Validation Error:0 VK_ERROR_INITIALIZATION_FAILED: Could not create MTLCounterSampleBuffer for query pool of type VK_QUERY_TYPE_TIMESTAMP. Reverting to emulated behavior. (Error code 0): Cannot allocate sample buffer
#define VULKAN_USE_TIMER_QUERIES 0
+class GPUDeviceVulkan;
+
///
/// The implementation for the Vulkan API support for iOS platform.
///
@@ -18,7 +20,7 @@ class iOSVulkanPlatform : public VulkanPlatformBase
{
public:
static void GetInstanceExtensions(Array& extensions, Array& layers);
- static void CreateSurface(void* windowHandle, VkInstance instance, VkSurfaceKHR* outSurface);
+ static void CreateSurface(Window* window, GPUDeviceVulkan* device, VkInstance instance, VkSurfaceKHR* outSurface);
};
typedef iOSVulkanPlatform VulkanPlatform;
diff --git a/Source/Engine/Input/Input.cpp b/Source/Engine/Input/Input.cpp
index 7048140ef..6860c3463 100644
--- a/Source/Engine/Input/Input.cpp
+++ b/Source/Engine/Input/Input.cpp
@@ -79,6 +79,7 @@ Delegate Input::MouseUp;
Delegate Input::MouseDoubleClick;
Delegate Input::MouseWheel;
Delegate Input::MouseMove;
+Delegate Input::MouseMoveRelative;
Action Input::MouseLeave;
Delegate Input::GamepadButtonDown;
Delegate Input::GamepadButtonUp;
@@ -217,6 +218,14 @@ void Mouse::OnMouseMove(const Float2& position, Window* target)
e.MouseData.Position = position;
}
+void Mouse::OnMouseMoveRelative(const Float2& positionRelative, Window* target)
+{
+ Event& e = _queue.AddOne();
+ e.Type = EventType::MouseMoveRelative;
+ e.Target = target;
+ e.MouseMovementData.PositionRelative = positionRelative;
+}
+
void Mouse::OnMouseLeave(Window* target)
{
PROFILE_MEM(Input);
@@ -284,6 +293,11 @@ bool Mouse::Update(EventQueue& queue)
_state.MousePosition = e.MouseData.Position;
break;
}
+ case EventType::MouseMoveRelative:
+ {
+ _state.MousePosition += e.MouseMovementData.PositionRelative;
+ break;
+ }
case EventType::MouseLeave:
{
break;
@@ -924,11 +938,10 @@ void InputService::Update()
WindowsManager::WindowsLocker.Unlock();
// Send input events for the focused window
- WindowsManager::WindowsLocker.Lock();
for (const auto& e : InputEvents)
{
auto window = e.Target ? e.Target : defaultWindow;
- if (!window || !WindowsManager::Windows.Contains(window))
+ if (!window || window->IsClosed())
continue;
switch (e.Type)
{
@@ -958,6 +971,9 @@ void InputService::Update()
case InputDevice::EventType::MouseMove:
window->OnMouseMove(window->ScreenToClient(e.MouseData.Position));
break;
+ case InputDevice::EventType::MouseMoveRelative:
+ window->OnMouseMoveRelative(e.MouseMovementData.PositionRelative);
+ break;
case InputDevice::EventType::MouseLeave:
window->OnMouseLeave();
break;
@@ -973,7 +989,6 @@ void InputService::Update()
break;
}
}
- WindowsManager::WindowsLocker.Unlock();
// Skip if game has no focus to handle the input
if (!Engine::HasGameViewportFocus())
@@ -1014,6 +1029,9 @@ void InputService::Update()
case InputDevice::EventType::MouseMove:
Input::MouseMove(e.MouseData.Position);
break;
+ case InputDevice::EventType::MouseMoveRelative:
+ Input::MouseMoveRelative(e.MouseMovementData.PositionRelative);
+ break;
case InputDevice::EventType::MouseLeave:
Input::MouseLeave();
break;
@@ -1240,6 +1258,7 @@ void InputService::Update()
}
}
+#if !PLATFORM_SDL
// Lock mouse if need to
const auto lockMode = Screen::GetCursorLock();
if (lockMode == CursorLockMode::Locked)
@@ -1248,6 +1267,7 @@ void InputService::Update()
Screen::ScreenToGameViewport(Float2::Zero);
Input::SetMousePosition(pos);
}
+#endif
// Send events for the active actions and axes (send events only in play mode)
if (!Time::GetGamePaused())
diff --git a/Source/Engine/Input/Input.h b/Source/Engine/Input/Input.h
index 73e87f5f0..964a247d9 100644
--- a/Source/Engine/Input/Input.h
+++ b/Source/Engine/Input/Input.h
@@ -108,6 +108,11 @@ public:
///
API_EVENT() static Delegate MouseMove;
+ ///
+ /// Event fired when mouse moves while in relative mode.
+ ///
+ API_EVENT() static Delegate MouseMoveRelative;
+
///
/// Event fired when mouse leaves window.
///
diff --git a/Source/Engine/Input/InputDevice.h b/Source/Engine/Input/InputDevice.h
index 5d2a383be..80f98fbdd 100644
--- a/Source/Engine/Input/InputDevice.h
+++ b/Source/Engine/Input/InputDevice.h
@@ -25,6 +25,7 @@ public:
MouseDoubleClick,
MouseWheel,
MouseMove,
+ MouseMoveRelative,
MouseLeave,
TouchDown,
TouchMove,
@@ -54,6 +55,11 @@ public:
Float2 Position;
} MouseData;
+ struct
+ {
+ Float2 PositionRelative;
+ } MouseMovementData;
+
struct
{
float WheelDelta;
diff --git a/Source/Engine/Input/Mouse.h b/Source/Engine/Input/Mouse.h
index e5e1b3639..d9d2221dd 100644
--- a/Source/Engine/Input/Mouse.h
+++ b/Source/Engine/Input/Mouse.h
@@ -46,12 +46,14 @@ public:
protected:
State _state;
State _prevState;
+ bool _relativeMode;
explicit Mouse()
: InputDevice(SpawnParams(Guid::New(), TypeInitializer), TEXT("Mouse"))
{
_state.Clear();
_prevState.Clear();
+ _relativeMode = false;
}
public:
@@ -114,6 +116,16 @@ public:
return !_state.MouseButtons[static_cast(button)] && _prevState.MouseButtons[static_cast(button)];
}
+ ///
+ /// Gets the current state of mouse relative mode.
+ ///
+ /// The window to check against, or null to check for any window.
+ /// True if mouse is in relative mode, otherwise false.
+ API_FUNCTION() virtual bool IsRelative(Window* window = nullptr) const
+ {
+ return _relativeMode;
+ }
+
public:
///
/// Sets the mouse position.
@@ -121,6 +133,17 @@ public:
/// The new position.
virtual void SetMousePosition(const Float2& newPosition) = 0;
+ ///
+ /// Sets the mouse relative mode state. While enabled, the mouse movement tracking becomes more accurate.
+ /// The cursor will be hidden while in relative mode.
+ ///
+ /// The new relative mode state.
+ /// The window.
+ virtual void SetRelativeMode(bool relativeMode, Window* window)
+ {
+ _relativeMode = relativeMode;
+ }
+
///
/// Called when mouse cursor gets moved by the application. Invalidates the previous cached mouse position to prevent mouse jitter when locking the cursor programmatically.
///
@@ -158,6 +181,13 @@ public:
/// The target window to receive this event, otherwise input system will pick the window automatically.
void OnMouseMove(const Float2& position, Window* target = nullptr);
+ ///
+ /// Called when mouse moves in relative mode.
+ ///
+ /// The mouse position change.
+ /// The target window to receive this event, otherwise input system will pick the window automatically.
+ void OnMouseMoveRelative(const Float2& positionRelative, Window* target = nullptr);
+
///
/// Called when mouse leaves the input source area.
///
diff --git a/Source/Engine/Platform/Base/Enums.h b/Source/Engine/Platform/Base/Enums.h
new file mode 100644
index 000000000..4a903ad19
--- /dev/null
+++ b/Source/Engine/Platform/Base/Enums.h
@@ -0,0 +1,251 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+#pragma once
+
+#include "Engine/Core/Config.h"
+
+///
+/// Window closing reasons.
+///
+API_ENUM() enum class ClosingReason
+{
+ ///
+ /// The unknown.
+ ///
+ Unknown = 0,
+
+ ///
+ /// The user.
+ ///
+ User,
+
+ ///
+ /// The engine exit.
+ ///
+ EngineExit,
+
+ ///
+ /// The close event.
+ ///
+ CloseEvent,
+};
+
+///
+/// Types of default cursors.
+///
+API_ENUM() enum class CursorType
+{
+ ///
+ /// The default.
+ ///
+ Default = 0,
+
+ ///
+ /// The cross.
+ ///
+ Cross,
+
+ ///
+ /// The hand.
+ ///
+ Hand,
+
+ ///
+ /// The help icon
+ ///
+ Help,
+
+ ///
+ /// The I beam.
+ ///
+ IBeam,
+
+ ///
+ /// The blocking image.
+ ///
+ No,
+
+ ///
+ /// The wait.
+ ///
+ Wait,
+
+ ///
+ /// The size all sides.
+ ///
+ SizeAll,
+
+ ///
+ /// The size NE-SW.
+ ///
+ SizeNESW,
+
+ ///
+ /// The size NS.
+ ///
+ SizeNS,
+
+ ///
+ /// The size NW-SE.
+ ///
+ SizeNWSE,
+
+ ///
+ /// The size WE.
+ ///
+ SizeWE,
+
+ ///
+ /// The cursor is hidden.
+ ///
+ Hidden,
+
+ MAX
+};
+
+///
+/// Data drag and drop effects.
+///
+API_ENUM() enum class DragDropEffect
+{
+ ///
+ /// The none.
+ ///
+ None = 0,
+
+ ///
+ /// The copy.
+ ///
+ Copy,
+
+ ///
+ /// The move.
+ ///
+ Move,
+
+ ///
+ /// The link.
+ ///
+ Link,
+};
+
+///
+/// Window hit test codes. Note: they are 1:1 mapping for Win32 values.
+///
+API_ENUM() enum class WindowHitCodes
+{
+ ///
+ /// The transparent area.
+ ///
+ Transparent = -1,
+
+ ///
+ /// The no hit.
+ ///
+ NoWhere = 0,
+
+ ///
+ /// The client area.
+ ///
+ Client = 1,
+
+ ///
+ /// The caption area.
+ ///
+ Caption = 2,
+
+ ///
+ /// The system menu.
+ ///
+ SystemMenu = 3,
+
+ ///
+ /// The grow box
+ ///
+ GrowBox = 4,
+
+ ///
+ /// The menu.
+ ///
+ Menu = 5,
+
+ ///
+ /// The horizontal scroll.
+ ///
+ HScroll = 6,
+
+ ///
+ /// The vertical scroll.
+ ///
+ VScroll = 7,
+
+ ///
+ /// The minimize button.
+ ///
+ MinButton = 8,
+
+ ///
+ /// The maximize button.
+ ///
+ MaxButton = 9,
+
+ ///
+ /// The left side;
+ ///
+ Left = 10,
+
+ ///
+ /// The right side.
+ ///
+ Right = 11,
+
+ ///
+ /// The top side.
+ ///
+ Top = 12,
+
+ ///
+ /// The top left corner.
+ ///
+ TopLeft = 13,
+
+ ///
+ /// The top right corner.
+ ///
+ TopRight = 14,
+
+ ///
+ /// The bottom side.
+ ///
+ Bottom = 15,
+
+ ///
+ /// The bottom left corner.
+ ///
+ BottomLeft = 16,
+
+ ///
+ /// The bottom right corner.
+ ///
+ BottomRight = 17,
+
+ ///
+ /// The border.
+ ///
+ Border = 18,
+
+ ///
+ /// The object.
+ ///
+ Object = 19,
+
+ ///
+ /// The close button.
+ ///
+ Close = 20,
+
+ ///
+ /// The help button.
+ ///
+ Help = 21,
+};
diff --git a/Source/Engine/Platform/Base/PlatformBase.cpp b/Source/Engine/Platform/Base/PlatformBase.cpp
index 4512a59b3..664eaa964 100644
--- a/Source/Engine/Platform/Base/PlatformBase.cpp
+++ b/Source/Engine/Platform/Base/PlatformBase.cpp
@@ -46,6 +46,7 @@ static_assert(sizeof(double) == 8, "Invalid double type size.");
static_assert((PLATFORM_THREADS_LIMIT & (PLATFORM_THREADS_LIMIT - 1)) == 0, "Threads limit must be power of two.");
static_assert(PLATFORM_THREADS_LIMIT % 4 == 0, "Threads limit must be multiple of 4.");
+const Char* PlatformBase::ApplicationClassName = TEXT("FlaxWindow");
float PlatformBase::CustomDpiScale = 1.0f;
Array> PlatformBase::Users;
Delegate PlatformBase::UserAdded;
@@ -291,6 +292,29 @@ PlatformType PlatformBase::GetPlatformType()
return PLATFORM_TYPE;
}
+#if !PLATFORM_SDL
+
+String PlatformBase::GetDisplayServer()
+{
+ return String::Empty;
+}
+
+bool PlatformBase::SupportsNativeDecorations()
+{
+ return true;
+}
+
+bool PlatformBase::SupportsNativeDecorationDragging()
+{
+#if PLATFORM_LINUX
+ return false;
+#else
+ return true;
+#endif
+}
+
+#endif
+
bool PlatformBase::Is64BitApp()
{
#if PLATFORM_64BITS
diff --git a/Source/Engine/Platform/Base/PlatformBase.h b/Source/Engine/Platform/Base/PlatformBase.h
index 44a0c8415..67cfa4698 100644
--- a/Source/Engine/Platform/Base/PlatformBase.h
+++ b/Source/Engine/Platform/Base/PlatformBase.h
@@ -190,6 +190,13 @@ DECLARE_SCRIPTING_TYPE_MINIMAL(PlatformBase);
///
static void Exit();
+public:
+
+ ///
+ /// Application windows class name.
+ ///
+ static const Char* ApplicationClassName;
+
public:
///
/// Copy memory region
@@ -362,6 +369,21 @@ public:
///
API_PROPERTY() static PlatformType GetPlatformType();
+ ///
+ /// Returns the display server name on Linux.
+ ///
+ API_PROPERTY() static String GetDisplayServer();
+
+ ///
+ /// Returns true if system provides decorations for windows.
+ ///
+ API_PROPERTY() static bool SupportsNativeDecorations();
+
+ ///
+ /// Returns true if system provides support for native window dragging events.
+ ///
+ API_PROPERTY() static bool SupportsNativeDecorationDragging();
+
///
/// Returns true if is running 64 bit application (otherwise 32 bit). It's compile-time constant.
///
diff --git a/Source/Engine/Platform/Base/ScreenUtilitiesBase.h b/Source/Engine/Platform/Base/ScreenUtilitiesBase.h
new file mode 100644
index 000000000..b6259c40d
--- /dev/null
+++ b/Source/Engine/Platform/Base/ScreenUtilitiesBase.h
@@ -0,0 +1,42 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+#pragma once
+
+#include "Engine/Core/Types/BaseTypes.h"
+#include "Engine/Core/Math/Color32.h"
+#include "Engine/Core/Math/Vector2.h"
+#include "Engine/Core/Delegate.h"
+
+API_INJECT_CODE(cpp, "#include \"Engine/Platform/ScreenUtilities.h\"");
+
+///
+/// Platform-dependent screen utilities.
+///
+API_CLASS(Static, Name="ScreenUtilities", Tag="NativeInvokeUseName")
+class FLAXENGINE_API ScreenUtilitiesBase
+{
+public:
+ static struct FLAXENGINE_API ScriptingTypeInitializer TypeInitializer;
+
+ ///
+ /// Gets the pixel color at the specified coordinates.
+ ///
+ /// Screen-space coordinate to read.
+ /// Pixel color at the specified coordinates, or transparent color when color couldn't be picked up.
+ API_FUNCTION() static Color32 GetColorAt(const Float2& pos)
+ {
+ return Color32::Transparent;
+ }
+
+ ///
+ /// Starts async color picking. Color will be returned through PickColorDone event when the actions ends (user selected the final color with a mouse). When action is active, GetColorAt can be used to read the current value.
+ ///
+ API_FUNCTION() static void PickColor()
+ {
+ }
+
+ ///
+ /// Called when PickColor action is finished.
+ ///
+ API_EVENT() static Delegate PickColorDone;
+};
diff --git a/Source/Engine/Platform/Base/WindowBase.cpp b/Source/Engine/Platform/Base/WindowBase.cpp
index 3bd34a1a0..cd96dca41 100644
--- a/Source/Engine/Platform/Base/WindowBase.cpp
+++ b/Source/Engine/Platform/Base/WindowBase.cpp
@@ -107,7 +107,7 @@ WindowBase::WindowBase(const CreateWindowSettings& settings)
if (settings.StartPosition == WindowStartPosition::CenterParent
|| settings.StartPosition == WindowStartPosition::CenterScreen)
{
- Rectangle parentBounds = Rectangle(Float2::Zero, Platform::GetDesktopSize());
+ Rectangle parentBounds = Platform::GetMonitorBounds(Float2::Minimum);
if (settings.Parent != nullptr && settings.StartPosition == WindowStartPosition::CenterParent)
parentBounds = settings.Parent->GetClientBounds();
@@ -165,6 +165,15 @@ void WindowBase::SetIsVisible(bool isVisible)
}
}
+bool WindowBase::IsAlwaysOnTop() const
+{
+ return false;
+}
+
+void WindowBase::SetIsAlwaysOnTop(bool isAlwaysOnTop)
+{
+}
+
String WindowBase::ToString() const
{
return GetTitle();
@@ -266,6 +275,13 @@ void WindowBase::OnMouseMove(const Float2& mousePosition)
INVOKE_EVENT_PARAMS_1(OnMouseMove, (void*)&mousePosition);
}
+void WindowBase::OnMouseMoveRelative(const Float2& mousePositionRelative)
+{
+ PROFILE_CPU_NAMED("GUI.OnMouseMoveRelative");
+ MouseMoveRelative(mousePositionRelative);
+ INVOKE_EVENT_PARAMS_1(OnMouseMoveRelative, (void*)&mousePositionRelative);
+}
+
void WindowBase::OnMouseLeave()
{
PROFILE_CPU_NAMED("GUI.OnMouseLeave");
diff --git a/Source/Engine/Platform/Base/WindowBase.h b/Source/Engine/Platform/Base/WindowBase.h
index b1e11f620..9cf1cc771 100644
--- a/Source/Engine/Platform/Base/WindowBase.h
+++ b/Source/Engine/Platform/Base/WindowBase.h
@@ -8,6 +8,7 @@
#include "Engine/Scripting/ScriptingObject.h"
#include "Engine/Input/KeyboardKeys.h"
#include "Engine/Input/Enums.h"
+#include "Enums.h"
class Input;
class Engine;
@@ -17,252 +18,6 @@ class GPUSwapChain;
class TextureData;
class IGuiData;
-///
-/// Window closing reasons.
-///
-API_ENUM() enum class ClosingReason
-{
- ///
- /// The unknown.
- ///
- Unknown = 0,
-
- ///
- /// The user.
- ///
- User,
-
- ///
- /// The engine exit.
- ///
- EngineExit,
-
- ///
- /// The close event.
- ///
- CloseEvent,
-};
-
-///
-/// Types of default cursors.
-///
-API_ENUM() enum class CursorType
-{
- ///
- /// The default.
- ///
- Default = 0,
-
- ///
- /// The cross.
- ///
- Cross,
-
- ///
- /// The hand.
- ///
- Hand,
-
- ///
- /// The help icon
- ///
- Help,
-
- ///
- /// The I beam.
- ///
- IBeam,
-
- ///
- /// The blocking image.
- ///
- No,
-
- ///
- /// The wait.
- ///
- Wait,
-
- ///
- /// The size all sides.
- ///
- SizeAll,
-
- ///
- /// The size NE-SW.
- ///
- SizeNESW,
-
- ///
- /// The size NS.
- ///
- SizeNS,
-
- ///
- /// The size NW-SE.
- ///
- SizeNWSE,
-
- ///
- /// The size WE.
- ///
- SizeWE,
-
- ///
- /// The cursor is hidden.
- ///
- Hidden,
-
- MAX
-};
-
-///
-/// Data drag and drop effects.
-///
-API_ENUM() enum class DragDropEffect
-{
- ///
- /// The none.
- ///
- None = 0,
-
- ///
- /// The copy.
- ///
- Copy,
-
- ///
- /// The move.
- ///
- Move,
-
- ///
- /// The link.
- ///
- Link,
-};
-
-///
-/// Window hit test codes. Note: they are 1:1 mapping for Win32 values.
-///
-API_ENUM() enum class WindowHitCodes
-{
- ///
- /// The transparent area.
- ///
- Transparent = -1,
-
- ///
- /// The no hit.
- ///
- NoWhere = 0,
-
- ///
- /// The client area.
- ///
- Client = 1,
-
- ///
- /// The caption area.
- ///
- Caption = 2,
-
- ///
- /// The system menu.
- ///
- SystemMenu = 3,
-
- ///
- /// The grow box
- ///
- GrowBox = 4,
-
- ///
- /// The menu.
- ///
- Menu = 5,
-
- ///
- /// The horizontal scroll.
- ///
- HScroll = 6,
-
- ///
- /// The vertical scroll.
- ///
- VScroll = 7,
-
- ///
- /// The minimize button.
- ///
- MinButton = 8,
-
- ///
- /// The maximize button.
- ///
- MaxButton = 9,
-
- ///
- /// The left side;
- ///
- Left = 10,
-
- ///
- /// The right side.
- ///
- Right = 11,
-
- ///
- /// The top side.
- ///
- Top = 12,
-
- ///
- /// The top left corner.
- ///
- TopLeft = 13,
-
- ///
- /// The top right corner.
- ///
- TopRight = 14,
-
- ///
- /// The bottom side.
- ///
- Bottom = 15,
-
- ///
- /// The bottom left corner.
- ///
- BottomLeft = 16,
-
- ///
- /// The bottom right corner.
- ///
- BottomRight = 17,
-
- ///
- /// The border.
- ///
- Border = 18,
-
- ///
- /// The object.
- ///
- Object = 19,
-
- ///
- /// The close button.
- ///
- Close = 20,
-
- ///
- /// The help button.
- ///
- Help = 21,
-};
-
API_INJECT_CODE(cpp, "#include \"Engine/Platform/Window.h\"");
///
@@ -290,6 +45,7 @@ protected:
bool _isHorizontalFlippingMouse = false;
bool _isVerticalFlippingMouse = false;
bool _isClippingCursor = false;
+ bool _restoreRelativeMode = false;
explicit WindowBase(const CreateWindowSettings& settings);
virtual ~WindowBase();
@@ -402,6 +158,17 @@ public:
return _maximized;
}
+ ///
+ /// Gets a value that indicates whether a window is always on top of other windows.
+ ///
+ API_PROPERTY() virtual bool IsAlwaysOnTop() const;
+
+ ///
+ /// Sets a value that indicates whether a window is always on top of other windows.
+ ///
+ /// True if always on top.
+ API_PROPERTY() virtual void SetIsAlwaysOnTop(bool isAlwaysOnTop);
+
///
/// Gets the native window handle.
///
@@ -663,7 +430,7 @@ public:
public:
///
- /// Starts drag and drop operation
+ /// Starts a drag and drop operation.
///
/// The data.
/// The result.
@@ -672,6 +439,18 @@ public:
return DragDropEffect::None;
}
+ ///
+ /// Starts a window drag and drop operation.
+ ///
+ /// The data.
+ /// The offset for positioning the window from cursor.
+ /// The window where dragging started.
+ /// The result.
+ API_FUNCTION() virtual DragDropEffect DoDragDrop(const StringView& data, const Float2& offset, Window* dragSourceWindow)
+ {
+ return DragDropEffect::None;
+ }
+
///
/// Starts the mouse tracking.
///
@@ -742,6 +521,17 @@ public:
{
}
+ ///
+ /// Gets the mouse position in window coordinates.
+ ///
+ API_PROPERTY() virtual Float2 GetMousePosition() const;
+
+ ///
+ /// Sets the mouse position in window coordinates.
+ ///
+ /// Mouse position to set on
+ API_PROPERTY() virtual void SetMousePosition(const Float2& position) const;
+
///
/// Gets the mouse cursor.
///
@@ -837,6 +627,12 @@ public:
MouseDelegate MouseMove;
void OnMouseMove(const Float2& mousePosition);
+ ///
+ /// Event fired when mouse moves in relative mode.
+ ///
+ MouseDelegate MouseMoveRelative;
+ void OnMouseMoveRelative(const Float2& mousePositionRelative);
+
///
/// Event fired when mouse leaves window.
///
@@ -932,16 +728,6 @@ public:
API_FUNCTION() bool GetKeyUp(KeyboardKeys key) const;
public:
- ///
- /// Gets the mouse position in window coordinates.
- ///
- API_PROPERTY() Float2 GetMousePosition() const;
-
- ///
- /// Sets the mouse position in window coordinates.
- ///
- /// Mouse position to set on
- API_PROPERTY() void SetMousePosition(const Float2& position) const;
///
/// Gets the mouse position change during the last frame.
diff --git a/Source/Engine/Platform/Clipboard.h b/Source/Engine/Platform/Clipboard.h
index 0eed3cb5e..c2fb23df0 100644
--- a/Source/Engine/Platform/Clipboard.h
+++ b/Source/Engine/Platform/Clipboard.h
@@ -2,7 +2,9 @@
#pragma once
-#if PLATFORM_WINDOWS
+#if PLATFORM_SDL && PLATFORM_LINUX
+#include "SDL/SDLClipboard.h"
+#elif PLATFORM_WINDOWS
#include "Windows/WindowsClipboard.h"
#elif PLATFORM_LINUX
#include "Linux/LinuxClipboard.h"
diff --git a/Source/Engine/Platform/CreateWindowSettings.cs b/Source/Engine/Platform/CreateWindowSettings.cs
index a866480ff..197771298 100644
--- a/Source/Engine/Platform/CreateWindowSettings.cs
+++ b/Source/Engine/Platform/CreateWindowSettings.cs
@@ -11,7 +11,7 @@ namespace FlaxEngine
{
Position = new Float2(100, 100),
Size = new Float2(640, 480),
- MinimumSize = Float2.One,
+ MinimumSize = new Float2(8, 8),
MaximumSize = Float2.Zero, // Unlimited size
StartPosition = WindowStartPosition.CenterParent,
HasBorder = true,
@@ -21,7 +21,7 @@ namespace FlaxEngine
AllowMinimize = true,
AllowMaximize = true,
AllowDragAndDrop = true,
- IsRegularWindow = true,
+ Type = WindowType.Regular,
HasSizingFrame = true,
ShowAfterFirstPaint = true,
};
diff --git a/Source/Engine/Platform/CreateWindowSettings.h b/Source/Engine/Platform/CreateWindowSettings.h
index ecbf721ae..2df533b7c 100644
--- a/Source/Engine/Platform/CreateWindowSettings.h
+++ b/Source/Engine/Platform/CreateWindowSettings.h
@@ -26,6 +26,32 @@ API_ENUM() enum class WindowStartPosition
Manual,
};
+///
+/// Specifies the type of the window.
+///
+API_ENUM() enum class WindowType
+{
+ ///
+ /// Regular window.
+ ///
+ Regular,
+
+ ///
+ /// Utility window.
+ ///
+ Utility,
+
+ ///
+ /// Tooltip window.
+ ///
+ Tooltip,
+
+ ///
+ /// Popup window.
+ ///
+ Popup,
+};
+
///
/// Settings for new window.
///
@@ -119,9 +145,15 @@ DECLARE_SCRIPTING_TYPE_MINIMAL(CreateWindowSettings);
API_FIELD() bool IsTopmost = false;
///
- /// True if it's a regular window, false for tooltips, contextmenu and other utility windows.
+ /// True if it's a regular window, false for tooltips, context menu and other utility windows.
///
- API_FIELD() bool IsRegularWindow = true;
+ API_FIELD() DEPRECATED("Use Type instead") bool IsRegularWindow = true;
+
+ ///
+ /// The type of window. The type affects the behaviour of the window in system level.
+ /// Note: Tooltip and Popup windows require Parent to be set.
+ ///
+ API_FIELD() WindowType Type = WindowType::Regular;
///
/// Enable/disable window sizing frame.
diff --git a/Source/Engine/Platform/Defines.h b/Source/Engine/Platform/Defines.h
index 9958545fd..d6e817bc6 100644
--- a/Source/Engine/Platform/Defines.h
+++ b/Source/Engine/Platform/Defines.h
@@ -140,6 +140,9 @@ API_ENUM() enum class ArchitectureType
#if !defined(PLATFORM_SWITCH)
#define PLATFORM_SWITCH 0
#endif
+#if !defined(PLATFORM_SDL)
+#define PLATFORM_SDL 0
+#endif
#if PLATFORM_WINDOWS
#include "Windows/WindowsDefines.h"
diff --git a/Source/Engine/Platform/GDK/GDKPlatform.cpp b/Source/Engine/Platform/GDK/GDKPlatform.cpp
index ad9bf07c0..969f24934 100644
--- a/Source/Engine/Platform/GDK/GDKPlatform.cpp
+++ b/Source/Engine/Platform/GDK/GDKPlatform.cpp
@@ -30,7 +30,6 @@ inline bool operator==(const APP_LOCAL_DEVICE_ID& l, const APP_LOCAL_DEVICE_ID&
return Platform::MemoryCompare(&l, &r, sizeof(APP_LOCAL_DEVICE_ID)) == 0;
}
-const Char* GDKPlatform::ApplicationWindowClass = TEXT("FlaxWindow");
void* GDKPlatform::Instance = nullptr;
Delegate<> GDKPlatform::Suspended;
Delegate<> GDKPlatform::Resumed;
@@ -317,7 +316,7 @@ void GDKPlatform::PreInit(void* hInstance)
windowsClass.style = CS_HREDRAW | CS_VREDRAW;
windowsClass.lpfnWndProc = WndProc;
windowsClass.hInstance = (HINSTANCE)Instance;
- windowsClass.lpszClassName = ApplicationWindowClass;
+ windowsClass.lpszClassName = ApplicationClassName;
if (!RegisterClassW(&windowsClass))
{
Error(TEXT("Window class registration failed!"));
@@ -477,7 +476,7 @@ void GDKPlatform::Exit()
if (PlmSignalResume)
CloseHandle(PlmSignalResume);
- UnregisterClassW(ApplicationWindowClass, nullptr);
+ UnregisterClassW(ApplicationClassName, nullptr);
XGameRuntimeUninitialize();
}
diff --git a/Source/Engine/Platform/GDK/GDKPlatform.h b/Source/Engine/Platform/GDK/GDKPlatform.h
index 24bdd8dc9..de24458a3 100644
--- a/Source/Engine/Platform/GDK/GDKPlatform.h
+++ b/Source/Engine/Platform/GDK/GDKPlatform.h
@@ -13,11 +13,6 @@ class FLAXENGINE_API GDKPlatform : public Win32Platform
{
public:
- ///
- /// Win32 application windows class name.
- ///
- static const Char* ApplicationWindowClass;
-
///
/// Handle to Win32 application instance.
///
diff --git a/Source/Engine/Platform/GDK/GDKWindow.cpp b/Source/Engine/Platform/GDK/GDKWindow.cpp
index a82fb4aba..c8be2df3f 100644
--- a/Source/Engine/Platform/GDK/GDKWindow.cpp
+++ b/Source/Engine/Platform/GDK/GDKWindow.cpp
@@ -71,7 +71,7 @@ GDKWindow::GDKWindow(const CreateWindowSettings& settings)
// Creating the window
_handle = CreateWindowExW(
exStyle,
- Platform::ApplicationWindowClass,
+ Platform::ApplicationClassName,
settings.Title.GetText(),
style,
x,
diff --git a/Source/Engine/Platform/Linux/IncludeX11.h b/Source/Engine/Platform/Linux/IncludeX11.h
index a2b0f6e7c..f84c8039b 100644
--- a/Source/Engine/Platform/Linux/IncludeX11.h
+++ b/Source/Engine/Platform/Linux/IncludeX11.h
@@ -21,6 +21,7 @@ namespace X11
#include
#include
#include
+#include
}
// Helper macros
diff --git a/Source/Engine/Platform/Linux/LinuxPlatform.cpp b/Source/Engine/Platform/Linux/LinuxPlatform.cpp
index 98e0cbbfc..98525d306 100644
--- a/Source/Engine/Platform/Linux/LinuxPlatform.cpp
+++ b/Source/Engine/Platform/Linux/LinuxPlatform.cpp
@@ -97,7 +97,9 @@ X11::Cursor Cursors[(int32)CursorType::MAX];
X11::XcursorImage* CursorsImg[(int32)CursorType::MAX];
Dictionary KeyNameMap;
Array KeyCodeMap;
-Delegate LinuxPlatform::xEventRecieved;
+#if !PLATFORM_SDL
+Delegate LinuxPlatform::xEventReceived;
+#endif
Window* MouseTrackingWindow = nullptr;
// Message boxes configuration
@@ -655,7 +657,11 @@ static int X11_MessageBoxLoop(MessageBoxData* data)
return 0;
}
+#if !PLATFORM_SDL
DialogResult MessageBox::Show(Window* parent, const StringView& text, const StringView& caption, MessageBoxButtons buttons, MessageBoxIcon icon)
+#else
+DialogResult MessageBox::ShowFallback(Window* parent, const StringView& text, const StringView& caption, MessageBoxButtons buttons, MessageBoxIcon icon)
+#endif
{
if (CommandLine::Options.Headless.IsTrue())
return DialogResult::None;
@@ -842,6 +848,8 @@ DialogResult MessageBox::Show(Window* parent, const StringView& text, const Stri
return data.resultButtonIndex == -1 ? DialogResult::None : data.buttons[data.resultButtonIndex].result;
}
+#if !PLATFORM_SDL
+
int X11ErrorHandler(X11::Display* display, X11::XErrorEvent* event)
{
if (event->error_code == 5)
@@ -852,6 +860,8 @@ int X11ErrorHandler(X11::Display* display, X11::XErrorEvent* event)
return 0;
}
+#endif
+
int32 CalculateDpi()
{
int dpi = 96;
@@ -1207,17 +1217,20 @@ public:
}
};
+#if !PLATFORM_SDL
struct Property
{
- unsigned char* data;
- int format, nitems;
- X11::Atom type;
+ unsigned char* data;
+ int format, nitems;
+ X11::Atom type;
};
+#endif
namespace Impl
{
LinuxKeyboard* Keyboard;
LinuxMouse* Mouse;
+#if !PLATFORM_SDL
StringAnsi ClipboardText;
void ClipboardGetText(String& result, X11::Atom source, X11::Atom atom, X11::Window window)
@@ -1332,6 +1345,7 @@ namespace Impl
X11::XQueryPointer(display, w, &wtmp, &child, &tmp, &tmp, &tmp, &tmp, &utmp);
return FindAppWindow(display, child);
}
+#endif
Dictionary LoadConfigFile(StringView path)
{
@@ -1361,6 +1375,7 @@ namespace Impl
}
}
+#if !PLATFORM_SDL
class LinuxDropFilesData : public IGuiData
{
public:
@@ -1398,7 +1413,7 @@ public:
}
};
-DragDropEffect LinuxWindow::DoDragDrop(const StringView& data)
+DragDropEffect Window::DoDragDrop(const StringView& data)
{
if (CommandLine::Options.Headless.IsTrue())
return DragDropEffect::None;
@@ -1412,13 +1427,14 @@ DragDropEffect LinuxWindow::DoDragDrop(const StringView& data)
StringAnsi dataAnsi(data);
LinuxDropTextData dropData;
dropData.Text = data;
+ unsigned long mainWindow = _window;
// Begin dragging
auto screen = X11::XDefaultScreen(xDisplay);
auto rootWindow = X11::XRootWindow(xDisplay, screen);
- if (X11::XGrabPointer(xDisplay, _window, 1, Button1MotionMask | ButtonReleaseMask, GrabModeAsync, GrabModeAsync, rootWindow, cursorWrong, CurrentTime) != GrabSuccess)
+ if (X11::XGrabPointer(xDisplay, mainWindow, 1, Button1MotionMask | ButtonReleaseMask, GrabModeAsync, GrabModeAsync, rootWindow, cursorWrong, CurrentTime) != GrabSuccess)
return DragDropEffect::None;
- X11::XSetSelectionOwner(xDisplay, xAtomXdndSelection, _window, CurrentTime);
+ X11::XSetSelectionOwner(xDisplay, xAtomXdndSelection, mainWindow, CurrentTime);
// Process events
X11::XEvent event;
@@ -1526,7 +1542,7 @@ DragDropEffect LinuxWindow::DoDragDrop(const StringView& data)
m.window = previousWindow;
m.message_type = xAtomXdndLeave;
m.format = 32;
- m.data.l[0] = _window;
+ m.data.l[0] = mainWindow;
m.data.l[1] = 0;
m.data.l[2] = 0;
m.data.l[3] = 0;
@@ -1555,7 +1571,7 @@ DragDropEffect LinuxWindow::DoDragDrop(const StringView& data)
m.window = window;
m.message_type = xAtomXdndEnter;
m.format = 32;
- m.data.l[0] = _window;
+ m.data.l[0] = mainWindow;
m.data.l[1] = Math::Min(5, version) << 24 | (formats.Count() > 3);
m.data.l[2] = formats.Count() > 0 ? formats[0] : 0;
m.data.l[3] = formats.Count() > 1 ? formats[1] : 0;
@@ -1590,7 +1606,7 @@ DragDropEffect LinuxWindow::DoDragDrop(const StringView& data)
m.window = window;
m.message_type = xAtomXdndPosition;
m.format = 32;
- m.data.l[0] = _window;
+ m.data.l[0] = mainWindow;
m.data.l[1] = 0;
m.data.l[2] = (x << 16) | y;
m.data.l[3] = CurrentTime;
@@ -1633,7 +1649,7 @@ DragDropEffect LinuxWindow::DoDragDrop(const StringView& data)
m.window = previousWindow;
m.message_type = xAtomXdndDrop;
m.format = 32;
- m.data.l[0] = _window;
+ m.data.l[0] = mainWindow;
m.data.l[1] = 0;
m.data.l[2] = CurrentTime;
m.data.l[3] = 0;
@@ -1680,7 +1696,7 @@ DragDropEffect LinuxWindow::DoDragDrop(const StringView& data)
m.window = previousWindow;
m.message_type = xAtomXdndLeave;
m.format = 32;
- m.data.l[0] = _window;
+ m.data.l[0] = mainWindow;
m.data.l[1] = 0;
m.data.l[2] = 0;
m.data.l[3] = 0;
@@ -1706,7 +1722,7 @@ void LinuxClipboard::SetText(const StringView& text)
{
if (CommandLine::Options.Headless.IsTrue())
return;
- auto mainWindow = (LinuxWindow*)Engine::MainWindow;
+ auto mainWindow = Engine::MainWindow;
if (!mainWindow)
return;
X11::Window window = (X11::Window)mainWindow->GetNativePtr();
@@ -1729,7 +1745,7 @@ String LinuxClipboard::GetText()
if (CommandLine::Options.Headless.IsTrue())
return String::Empty;
String result;
- auto mainWindow = (LinuxWindow*)Engine::MainWindow;
+ auto mainWindow = Engine::MainWindow;
if (!mainWindow)
return result;
X11::Window window = (X11::Window)mainWindow->GetNativePtr();
@@ -1758,9 +1774,11 @@ Array LinuxClipboard::GetFiles()
return Array();
}
+#endif
+
void* LinuxPlatform::GetXDisplay()
{
- return xDisplay;
+ return xDisplay;
}
bool LinuxPlatform::CreateMutex(const Char* name)
@@ -2095,6 +2113,7 @@ bool LinuxPlatform::Init()
UnixGetMacAddress(MacAddress);
+#if !PLATFORM_SDL
// Get user locale string
setlocale(LC_ALL, "");
const char* locale = setlocale(LC_CTYPE, NULL);
@@ -2104,6 +2123,7 @@ bool LinuxPlatform::Init()
UserLocale.Replace('_', '-');
if (UserLocale == TEXT("C"))
UserLocale = TEXT("en");
+#endif
// Get computer name string
gethostname(buffer, UNIX_APP_BUFF_SIZE);
@@ -2147,7 +2167,11 @@ bool LinuxPlatform::Init()
// Skip setup if running in headless mode (X11 might not be available on servers)
if (CommandLine::Options.Headless.IsTrue())
return false;
-
+#if PLATFORM_SDL
+ xDisplay = X11::XOpenDisplay(nullptr);
+#endif
+
+#if !PLATFORM_SDL
X11::XInitThreads();
xDisplay = X11::XOpenDisplay(nullptr);
@@ -2286,7 +2310,7 @@ bool LinuxPlatform::Init()
Input::Mouse = Impl::Mouse = New();
Input::Keyboard = Impl::Keyboard = New();
LinuxInput::Init();
-
+#endif
return false;
}
@@ -2296,6 +2320,7 @@ void LinuxPlatform::BeforeRun()
void LinuxPlatform::Tick()
{
+#if !PLATFORM_SDL
UnixPlatform::Tick();
LinuxInput::UpdateState();
@@ -2312,9 +2337,9 @@ void LinuxPlatform::Tick()
continue;
// External event handling
- xEventRecieved(&event);
+ xEventReceived(&event);
- LinuxWindow* window;
+ Window* window;
switch (event.type)
{
case ClientMessage:
@@ -2646,6 +2671,7 @@ void LinuxPlatform::Tick()
}
//X11::XFlush(xDisplay);
+#endif
}
void LinuxPlatform::BeforeExit()
@@ -2654,6 +2680,7 @@ void LinuxPlatform::BeforeExit()
void LinuxPlatform::Exit()
{
+#if !PLATFORM_SDL
for (int32 i = 0; i < (int32)CursorType::MAX; i++)
{
if (Cursors[i])
@@ -2679,6 +2706,7 @@ void LinuxPlatform::Exit()
X11::XCloseDisplay(xDisplay);
xDisplay = nullptr;
}
+#endif
}
String LinuxPlatform::GetSystemName()
@@ -2700,6 +2728,7 @@ Version LinuxPlatform::GetSystemVersion()
return Version(0, 0);
}
+#if !PLATFORM_SDL
int32 LinuxPlatform::GetDpi()
{
return SystemDpi;
@@ -2709,6 +2738,7 @@ String LinuxPlatform::GetUserLocaleName()
{
return UserLocale;
}
+#endif
String LinuxPlatform::GetComputerName()
{
@@ -2916,10 +2946,12 @@ bool LinuxPlatform::SetWorkingDirectory(const String& path)
return chdir(StringAsANSI<>(*path).Get()) != 0;
}
+#if !PLATFORM_SDL
Window* LinuxPlatform::CreateWindow(const CreateWindowSettings& settings)
{
return New(settings);
}
+#endif
extern char **environ;
@@ -2961,6 +2993,7 @@ bool LinuxPlatform::SetEnvironmentVariable(const String& name, const String& val
return setenv(StringAsANSI<>(*name).Get(), StringAsANSI<>(*value).Get(), true) != 0;
}
+#if !PLATFORM_SDL
int32 LinuxPlatform::CreateProcess(CreateProcessSettings& settings)
{
LOG(Info, "Command: {0} {1}", settings.FileName, settings.Arguments);
@@ -3074,6 +3107,7 @@ int32 LinuxPlatform::CreateProcess(CreateProcessSettings& settings)
return returnCode;
}
+#endif
void* LinuxPlatform::LoadLibrary(const Char* filename)
{
diff --git a/Source/Engine/Platform/Linux/LinuxPlatform.h b/Source/Engine/Platform/Linux/LinuxPlatform.h
index 071566f41..bb5f85524 100644
--- a/Source/Engine/Platform/Linux/LinuxPlatform.h
+++ b/Source/Engine/Platform/Linux/LinuxPlatform.h
@@ -36,7 +36,7 @@ public:
///
/// An event that is fired when an XEvent is received during platform tick.
///
- static Delegate xEventRecieved;
+ static Delegate xEventReceived;
public:
@@ -124,8 +124,10 @@ public:
static void Tick();
static void BeforeExit();
static void Exit();
+#if !PLATFORM_SDL
static int32 GetDpi();
static String GetUserLocaleName();
+#endif
static String GetComputerName();
static bool GetHasFocus();
static bool CanOpenUrl(const StringView& url);
@@ -140,11 +142,15 @@ public:
static Guid GetUniqueDeviceId();
static String GetWorkingDirectory();
static bool SetWorkingDirectory(const String& path);
+#if !PLATFORM_SDL
static Window* CreateWindow(const CreateWindowSettings& settings);
+#endif
static void GetEnvironmentVariables(Dictionary& result);
static bool GetEnvironmentVariable(const String& name, String& value);
static bool SetEnvironmentVariable(const String& name, const String& value);
+#if !PLATFORM_SDL
static int32 CreateProcess(CreateProcessSettings& settings);
+#endif
static void* LoadLibrary(const Char* filename);
static void FreeLibrary(void* handle);
static void* GetProcAddress(void* handle, const char* symbol);
diff --git a/Source/Engine/Platform/Linux/LinuxScreenUtilities.cpp b/Source/Engine/Platform/Linux/LinuxScreenUtilities.cpp
new file mode 100644
index 000000000..cd75fe8ef
--- /dev/null
+++ b/Source/Engine/Platform/Linux/LinuxScreenUtilities.cpp
@@ -0,0 +1,164 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+#if PLATFORM_LINUX
+
+#include "Engine/Platform/Types.h"
+#include "Engine/Platform/ScreenUtilities.h"
+#include "Engine/Core/Math/Vector2.h"
+#include "Engine/Core/Delegate.h"
+#include "Engine/Core/Log.h"
+#include "Engine/Core/Math/Vector4.h"
+#include "Engine/Profiler/ProfilerCPU.h"
+#include "Engine/Platform/Linux/LinuxPlatform.h"
+#include "Engine/Platform/Linux/IncludeX11.h"
+
+#if PLATFORM_SDL
+#include
+#include
+
+namespace PortalImpl
+{
+ XdpPortal* Portal = nullptr;
+ int64 MainLoopReady = 0;
+
+ gpointer GLibMainLoop(gpointer data);
+ void PickColorCallback(GObject* source, GAsyncResult* result, gpointer data);
+}
+#endif
+
+Delegate ScreenUtilitiesBase::PickColorDone;
+
+Color32 LinuxScreenUtilities::GetColorAt(const Float2& pos)
+{
+ X11::Display* display = (X11::Display*)Platform::GetXDisplay();
+ if (display)
+ {
+ int defaultScreen = X11::XDefaultScreen(display);
+ X11::Window rootWindow = X11::XRootWindow(display, defaultScreen);
+ X11::XImage* image = X11::XGetImage(display, rootWindow, (int)pos.X, (int)pos.Y, 1, 1, AllPlanes, XYPixmap);
+ if (image)
+ {
+ X11::XColor color;
+ color.pixel = XGetPixel(image, 0, 0);
+ X11::XFree(image);
+
+ X11::XQueryColor(display, X11::XDefaultColormap(display, defaultScreen), &color);
+
+ Color32 outputColor;
+ outputColor.R = color.red / 256;
+ outputColor.G = color.green / 256;
+ outputColor.B = color.blue / 256;
+ outputColor.A = 255;
+ return outputColor;
+ }
+ else
+ {
+ // XWayland doesn't support XGetImage
+ }
+ }
+
+ return Color32::Transparent;
+}
+
+void OnScreenUtilsXEventCallback(void* eventPtr)
+{
+ X11::XEvent* event = (X11::XEvent*) eventPtr;
+ X11::Display* display = (X11::Display*)Platform::GetXDisplay();
+ if (event->type == ButtonPress)
+ {
+ const Float2 cursorPos = Platform::GetMousePosition();
+ const Color32 colorPicked = ScreenUtilities::GetColorAt(cursorPos);
+ X11::XUngrabPointer(display, CurrentTime);
+ ScreenUtilities::PickColorDone(colorPicked);
+ LinuxPlatform::xEventReceived.Unbind(OnScreenUtilsXEventCallback);
+ }
+}
+
+void LinuxScreenUtilities::PickColor()
+{
+ PROFILE_CPU();
+ X11::Display* display = (X11::Display*)Platform::GetXDisplay();
+ if (display)
+ {
+ X11::Window rootWindow = X11::XRootWindow(display, X11::XDefaultScreen(display));
+
+ X11::Cursor cursor = XCreateFontCursor(display, 130);
+ int grabbedPointer = X11::XGrabPointer(display, rootWindow, 0, ButtonPressMask, GrabModeAsync, GrabModeAsync, rootWindow, cursor, CurrentTime);
+ if (grabbedPointer != GrabSuccess)
+ {
+ LOG(Error, "Failed to grab cursor for events.");
+ X11::XFreeCursor(display, cursor);
+ return;
+ }
+
+ X11::XFreeCursor(display, cursor);
+ LinuxPlatform::xEventReceived.Bind(OnScreenUtilsXEventCallback);
+ return;
+ }
+#if PLATFORM_SDL
+ if (PortalImpl::MainLoopReady == 0)
+ {
+ // Initialize portal
+ GError* error = nullptr;
+ PortalImpl::Portal = xdp_portal_initable_new(&error);
+ if (error != nullptr)
+ {
+ PortalImpl::MainLoopReady = 2;
+ LOG(Error, "Failed to initialize XDP Portal");
+ return;
+ }
+
+ // Run the GLib main loop in other thread in order to process asynchronous callbacks
+ g_thread_new(nullptr, PortalImpl::GLibMainLoop, nullptr);
+ while (Platform::AtomicRead(&PortalImpl::MainLoopReady) != 1)
+ Platform::Sleep(1);
+ }
+
+ if (PortalImpl::Portal != nullptr)
+ {
+ // Enter color picking mode, the callback receives the final color
+ xdp_portal_pick_color(PortalImpl::Portal, nullptr, nullptr, PortalImpl::PickColorCallback, nullptr);
+ }
+#endif
+}
+
+#if PLATFORM_SDL
+
+gpointer PortalImpl::GLibMainLoop(gpointer data)
+{
+ GMainContext* mainContext = g_main_context_get_thread_default();
+ GMainLoop* mainLoop = g_main_loop_new(mainContext, false);
+
+ Platform::AtomicStore(&PortalImpl::MainLoopReady, 1);
+
+ g_main_loop_run(mainLoop);
+ g_main_loop_unref(mainLoop);
+ return nullptr;
+}
+
+void PortalImpl::PickColorCallback(GObject* source, GAsyncResult* result, gpointer data)
+{
+ GError* error = nullptr;
+ GVariant* variant = xdp_portal_pick_color_finish(PortalImpl::Portal, result, &error);
+ if (error)
+ {
+ if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ LOG(Info, "XDP Portal pick color cancelled");
+ else
+ LOG(Error, "XDP Portal pick color failed: {}", String(error->message));
+ return;
+ }
+
+ // The color is stored in a triple double variant, extract the values
+ Double4 colorDouble;
+ g_variant_get(variant, "(ddd)", &colorDouble.X, &colorDouble.Y, &colorDouble.Z);
+ g_variant_unref(variant);
+ colorDouble.W = 1.0f;
+ Vector4 colorVector = colorDouble;
+ Color32 color = Color32(colorVector);
+
+ ScreenUtilities::PickColorDone(color);
+}
+#endif
+
+#endif
diff --git a/Source/Engine/Platform/Linux/LinuxScreenUtilities.h b/Source/Engine/Platform/Linux/LinuxScreenUtilities.h
new file mode 100644
index 000000000..27fdb9297
--- /dev/null
+++ b/Source/Engine/Platform/Linux/LinuxScreenUtilities.h
@@ -0,0 +1,24 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+#pragma once
+
+#if PLATFORM_LINUX
+
+#include "Engine/Platform/Base/ScreenUtilitiesBase.h"
+#include "Engine/Core/Types/BaseTypes.h"
+#include "Engine/Core/Math/Color32.h"
+#include "Engine/Core/Delegate.h"
+
+///
+/// Platform-dependent screen utilities.
+///
+class FLAXENGINE_API LinuxScreenUtilities : public ScreenUtilitiesBase
+{
+public:
+
+ // [ScreenUtilitiesBase]
+ static Color32 GetColorAt(const Float2& pos);
+ static void PickColor();
+};
+
+#endif
diff --git a/Source/Engine/Platform/Linux/LinuxWindow.cpp b/Source/Engine/Platform/Linux/LinuxWindow.cpp
index e7996229b..4850c1e3b 100644
--- a/Source/Engine/Platform/Linux/LinuxWindow.cpp
+++ b/Source/Engine/Platform/Linux/LinuxWindow.cpp
@@ -1,6 +1,6 @@
// Copyright (c) Wojciech Figat. All rights reserved.
-#if PLATFORM_LINUX
+#if PLATFORM_LINUX && !PLATFORM_SDL
#include "../Window.h"
#include "Engine/Input/Input.h"
@@ -631,7 +631,7 @@ void LinuxWindow::OnButtonPress(void* event)
}
// Handle double-click
- if (buttonEvent->button == Button1)
+ if (buttonEvent->button == Button1 && !Input::Mouse->IsRelative())
{
if (
buttonEvent->time < (MouseLastButtonPressTime + MouseDoubleClickTime) &&
diff --git a/Source/Engine/Platform/Linux/LinuxWindow.h b/Source/Engine/Platform/Linux/LinuxWindow.h
index 680b38e42..67a40859d 100644
--- a/Source/Engine/Platform/Linux/LinuxWindow.h
+++ b/Source/Engine/Platform/Linux/LinuxWindow.h
@@ -89,6 +89,7 @@ public:
void Focus() override;
void SetTitle(const StringView& title) override;
DragDropEffect DoDragDrop(const StringView& data) override;
+ using WindowBase::DoDragDrop;
void StartTrackingMouse(bool useMouseScreenOffset) override;
void EndTrackingMouse() override;
void SetCursor(CursorType type) override;
diff --git a/Source/Engine/Platform/Mac/MacPlatform.cpp b/Source/Engine/Platform/Mac/MacPlatform.cpp
index 6ba001f20..f8a36d24d 100644
--- a/Source/Engine/Platform/Mac/MacPlatform.cpp
+++ b/Source/Engine/Platform/Mac/MacPlatform.cpp
@@ -276,6 +276,7 @@ bool MacPlatform::Init()
CFRelease(computerName);
}
+#if !PLATFORM_SDL
// Find the maximum scale of the display to handle high-dpi displays scaling factor
{
NSArray* screenArray = [NSScreen screens];
@@ -300,6 +301,7 @@ bool MacPlatform::Init()
Input::Mouse = New();
Input::Keyboard = New();
+#endif
return false;
}
@@ -426,11 +428,16 @@ String MacPlatform::GetMainDirectory()
return path;
}
+#if !PLATFORM_SDL
+
Window* MacPlatform::CreateWindow(const CreateWindowSettings& settings)
{
return New(settings);
}
+#endif
+
+#if !PLATFORM_SDL
int32 MacPlatform::CreateProcess(CreateProcessSettings& settings)
{
LOG(Info, "Command: {0} {1}", settings.FileName, settings.Arguments);
@@ -570,5 +577,6 @@ int32 MacPlatform::CreateProcess(CreateProcessSettings& settings)
return returnCode;
}
+#endif
#endif
diff --git a/Source/Engine/Platform/Mac/MacPlatform.h b/Source/Engine/Platform/Mac/MacPlatform.h
index e80635126..e732e48f3 100644
--- a/Source/Engine/Platform/Mac/MacPlatform.h
+++ b/Source/Engine/Platform/Mac/MacPlatform.h
@@ -27,7 +27,9 @@ public:
static Rectangle GetVirtualDesktopBounds();
static String GetMainDirectory();
static Window* CreateWindow(const CreateWindowSettings& settings);
+#if !PLATFORM_SDL
static int32 CreateProcess(CreateProcessSettings& settings);
+#endif
};
#endif
diff --git a/Source/Engine/Platform/Mac/MacScreenUtilities.cpp b/Source/Engine/Platform/Mac/MacScreenUtilities.cpp
new file mode 100644
index 000000000..62c405635
--- /dev/null
+++ b/Source/Engine/Platform/Mac/MacScreenUtilities.cpp
@@ -0,0 +1,29 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+#if USE_EDITOR && PLATFORM_MAC
+
+#include "Engine/Platform/Types.h"
+#include "Engine/Platform/ScreenUtilities.h"
+#include "Engine/Core/Math/Vector2.h"
+#include "Engine/Core/Delegate.h"
+#include "Engine/Core/Log.h"
+
+#include
+#include
+
+Delegate ScreenUtilitiesBase::PickColorDone;
+
+Color32 MacScreenUtilities::GetColorAt(const Float2& pos)
+{
+ // TODO: implement ScreenUtilities for macOS
+ return { 0, 0, 0, 255 };
+}
+
+void MacScreenUtilities::PickColor()
+{
+ // This is what C# calls to start the color picking sequence
+ // This should stop mouse clicks from working for one click, and that click is on the selected color
+ // There is a class called NSColorSample that might implement that for you, but maybe not.
+}
+
+#endif
diff --git a/Source/Engine/Platform/Mac/MacScreenUtilities.h b/Source/Engine/Platform/Mac/MacScreenUtilities.h
new file mode 100644
index 000000000..4f7ef0347
--- /dev/null
+++ b/Source/Engine/Platform/Mac/MacScreenUtilities.h
@@ -0,0 +1,25 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+#pragma once
+
+#if USE_EDITOR && PLATFORM_MAC
+
+#include "Engine/Platform/Base/ScreenUtilitiesBase.h"
+#include "Engine/Core/Types/BaseTypes.h"
+#include "Engine/Core/Math/Color32.h"
+#include "Engine/Core/Math/Vector2.h"
+#include "Engine/Core/Delegate.h"
+
+///
+/// Platform-dependent screen utilities.
+///
+class FLAXENGINE_API MacScreenUtilities : public ScreenUtilitiesBase
+{
+public:
+
+ // [ScreenUtilitiesBase]
+ static Color32 GetColorAt(const Float2& pos);
+ static void PickColor();
+};
+
+#endif
diff --git a/Source/Engine/Platform/Mac/MacWindow.cpp b/Source/Engine/Platform/Mac/MacWindow.cpp
index b1dc0768f..5a0e0e3be 100644
--- a/Source/Engine/Platform/Mac/MacWindow.cpp
+++ b/Source/Engine/Platform/Mac/MacWindow.cpp
@@ -1,6 +1,6 @@
// Copyright (c) Wojciech Figat. All rights reserved.
-#if PLATFORM_MAC
+#if PLATFORM_MAC && !PLATFORM_SDL
#include "../Window.h"
#include "Engine/Platform/Apple/AppleUtils.h"
@@ -507,7 +507,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r)
Float2 mousePos = GetMousePosition(Window, event);
mousePos = Window->ClientToScreen(mousePos);
MouseButton mouseButton = MouseButton::Left;
- if ([event clickCount] == 2)
+ if ([event clickCount] == 2 && !Input::Mouse->IsRelative())
Input::Mouse->OnMouseDoubleClick(mousePos, mouseButton, Window);
else
Input::Mouse->OnMouseDown(mousePos, mouseButton, Window);
@@ -544,7 +544,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r)
if (IsWindowInvalid(Window)) return;
Float2 mousePos = GetMousePosition(Window, event);
MouseButton mouseButton = MouseButton::Right;
- if ([event clickCount] == 2)
+ if ([event clickCount] == 2 && !Input::Mouse->IsRelative())
Input::Mouse->OnMouseDoubleClick(Window->ClientToScreen(mousePos), mouseButton, Window);
else
Input::Mouse->OnMouseDown(Window->ClientToScreen(mousePos), mouseButton, Window);
@@ -582,7 +582,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r)
default:
return;
}
- if ([event clickCount] == 2)
+ if ([event clickCount] == 2 && !Input::Mouse->IsRelative())
Input::Mouse->OnMouseDoubleClick(Window->ClientToScreen(mousePos), mouseButton, Window);
else
Input::Mouse->OnMouseDown(Window->ClientToScreen(mousePos), mouseButton, Window);
diff --git a/Source/Engine/Platform/Mac/MacWindow.h b/Source/Engine/Platform/Mac/MacWindow.h
index 8d9c5e4fd..f4a8cb706 100644
--- a/Source/Engine/Platform/Mac/MacWindow.h
+++ b/Source/Engine/Platform/Mac/MacWindow.h
@@ -55,6 +55,7 @@ public:
void Focus() override;
void SetTitle(const StringView& title) override;
DragDropEffect DoDragDrop(const StringView& data) override;
+ using WindowBase::DoDragDrop;
void StartTrackingMouse(bool useMouseScreenOffset) override;
void EndTrackingMouse() override;
void SetCursor(CursorType type) override;
diff --git a/Source/Engine/Platform/MessageBox.h b/Source/Engine/Platform/MessageBox.h
index 16ae161cb..80c8e7938 100644
--- a/Source/Engine/Platform/MessageBox.h
+++ b/Source/Engine/Platform/MessageBox.h
@@ -237,4 +237,9 @@ DECLARE_SCRIPTING_TYPE_NO_SPAWN(MessageBox);
/// One of the MessageBoxIcon values that specifies which icon to display in the message box.
/// The message box dialog result.
API_FUNCTION() static DialogResult Show(Window* parent, const StringView& text, const StringView& caption, MessageBoxButtons buttons, MessageBoxIcon icon);
+
+private:
+#if PLATFORM_SDL && PLATFORM_LINUX
+ static DialogResult ShowFallback(Window* parent, const StringView& text, const StringView& caption, MessageBoxButtons buttons, MessageBoxIcon icon);
+#endif
};
diff --git a/Source/Engine/Platform/Platform.Build.cs b/Source/Engine/Platform/Platform.Build.cs
index bdc55ef55..7272270c0 100644
--- a/Source/Engine/Platform/Platform.Build.cs
+++ b/Source/Engine/Platform/Platform.Build.cs
@@ -89,6 +89,21 @@ public class Platform : EngineModule
break;
default: throw new InvalidPlatformException(options.Platform.Target);
}
+
+ if (EngineConfiguration.WithSDL(options))
+ {
+ switch (options.Platform.Target)
+ {
+ case TargetPlatform.Windows:
+ case TargetPlatform.Linux:
+ case TargetPlatform.Mac:
+ options.PublicDependencies.Add("SDL");
+ options.SourcePaths.Add(Path.Combine(FolderPath, "SDL"));
+ break;
+ }
+ if (options.Platform.Target == TargetPlatform.Linux)
+ options.PublicDependencies.Add("Wayland");
+ }
if (options.Target.IsEditor)
{
// Include platform settings headers
diff --git a/Source/Engine/Platform/Platform.h b/Source/Engine/Platform/Platform.h
index 8082cd6a2..279e4141f 100644
--- a/Source/Engine/Platform/Platform.h
+++ b/Source/Engine/Platform/Platform.h
@@ -8,7 +8,9 @@
#include "Types.h"
#include "Defines.h"
-#if PLATFORM_WINDOWS
+#if PLATFORM_SDL
+#include "SDL/SDLPlatform.h"
+#elif PLATFORM_WINDOWS
#include "Windows/WindowsPlatform.h"
#elif PLATFORM_UWP
#include "UWP/UWPPlatform.h"
diff --git a/Source/Engine/Platform/SDL/SDLClipboard.h b/Source/Engine/Platform/SDL/SDLClipboard.h
new file mode 100644
index 000000000..f9862b621
--- /dev/null
+++ b/Source/Engine/Platform/SDL/SDLClipboard.h
@@ -0,0 +1,26 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+#pragma once
+
+#if PLATFORM_SDL && PLATFORM_LINUX
+
+#include "Engine/Platform/Base/ClipboardBase.h"
+
+///
+/// SDL implementation of the clipboard service for Linux platform.
+///
+class FLAXENGINE_API SDLClipboard : public ClipboardBase
+{
+public:
+
+ // [ClipboardBase]
+ static void Clear();
+ static void SetText(const StringView& text);
+ static void SetRawData(const Span& data);
+ static void SetFiles(const Array& files);
+ static String GetText();
+ static Array GetRawData();
+ static Array GetFiles();
+};
+
+#endif
diff --git a/Source/Engine/Platform/SDL/SDLInput.cpp b/Source/Engine/Platform/SDL/SDLInput.cpp
new file mode 100644
index 000000000..76476f162
--- /dev/null
+++ b/Source/Engine/Platform/SDL/SDLInput.cpp
@@ -0,0 +1,836 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+#if PLATFORM_SDL
+
+#include "SDLInput.h"
+#include "SDLWindow.h"
+#include "Engine/Core/Collections/Dictionary.h"
+#include "Engine/Core/Log.h"
+#include "Engine/Input/Input.h"
+#include "Engine/Input/Mouse.h"
+#include "Engine/Input/Keyboard.h"
+#include "Engine/Input/Gamepad.h"
+#include "Engine/Engine/Engine.h"
+#include "Engine/Engine/Screen.h"
+#if USE_EDITOR
+#include "Editor/Editor.h"
+#include "Editor/Managed/ManagedEditor.h"
+#endif
+
+#include
+
+class SDLMouse;
+class SDLKeyboard;
+class SDLGamepad;
+
+// TODO: Turn these into customizable values
+#define TRIGGER_THRESHOLD 30
+#define LEFT_STICK_THRESHOLD 7849
+#define RIGHT_STICK_THRESHOLD 8689
+
+namespace SDLInputImpl
+{
+ SDLMouse* Mouse = nullptr;
+ SDLKeyboard* Keyboard = nullptr;
+ Dictionary Gamepads;
+}
+
+static const KeyboardKeys SDL_TO_FLAX_KEYS_MAP[] =
+{
+ KeyboardKeys::None, // SDL_SCANCODE_UNKNOWN
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::A,
+ KeyboardKeys::B,
+ KeyboardKeys::C,
+ KeyboardKeys::D,
+ KeyboardKeys::E,
+ KeyboardKeys::F,
+ KeyboardKeys::G,
+ KeyboardKeys::H,
+ KeyboardKeys::I,
+ KeyboardKeys::J,
+ KeyboardKeys::K,
+ KeyboardKeys::L,
+ KeyboardKeys::M,
+ KeyboardKeys::N,
+ KeyboardKeys::O,
+ KeyboardKeys::P,
+ KeyboardKeys::Q,
+ KeyboardKeys::R,
+ KeyboardKeys::S,
+ KeyboardKeys::T,
+ KeyboardKeys::U,
+ KeyboardKeys::V,
+ KeyboardKeys::W,
+ KeyboardKeys::X,
+ KeyboardKeys::Y,
+ KeyboardKeys::Z, // 29
+ KeyboardKeys::Alpha1,
+ KeyboardKeys::Alpha2,
+ KeyboardKeys::Alpha3,
+ KeyboardKeys::Alpha4,
+ KeyboardKeys::Alpha5,
+ KeyboardKeys::Alpha6,
+ KeyboardKeys::Alpha7,
+ KeyboardKeys::Alpha8,
+ KeyboardKeys::Alpha9,
+ KeyboardKeys::Alpha0, // 39
+ KeyboardKeys::Return,
+ KeyboardKeys::Escape,
+ KeyboardKeys::Backspace,
+ KeyboardKeys::Tab,
+ KeyboardKeys::Spacebar,
+ KeyboardKeys::Minus,
+ KeyboardKeys::None, //KeyboardKeys::Equals, // ?
+ KeyboardKeys::LeftBracket,
+ KeyboardKeys::RightBracket,
+ KeyboardKeys::Backslash, // SDL_SCANCODE_BACKSLASH ?
+ KeyboardKeys::Oem102, // SDL_SCANCODE_NONUSHASH ?
+ KeyboardKeys::Colon, // SDL_SCANCODE_SEMICOLON ?
+ KeyboardKeys::Quote, // SDL_SCANCODE_APOSTROPHE
+ KeyboardKeys::BackQuote, // SDL_SCANCODE_GRAVE
+ KeyboardKeys::Comma,
+ KeyboardKeys::Period,
+ KeyboardKeys::Slash,
+ KeyboardKeys::Capital,
+ KeyboardKeys::F1,
+ KeyboardKeys::F2,
+ KeyboardKeys::F3,
+ KeyboardKeys::F4,
+ KeyboardKeys::F5,
+ KeyboardKeys::F6,
+ KeyboardKeys::F7,
+ KeyboardKeys::F8,
+ KeyboardKeys::F9,
+ KeyboardKeys::F10,
+ KeyboardKeys::F11,
+ KeyboardKeys::F12,
+ KeyboardKeys::PrintScreen,
+ KeyboardKeys::Scroll,
+ KeyboardKeys::Pause,
+ KeyboardKeys::Insert,
+ KeyboardKeys::Home,
+ KeyboardKeys::PageUp,
+ KeyboardKeys::Delete,
+ KeyboardKeys::End,
+ KeyboardKeys::PageDown,
+ KeyboardKeys::ArrowRight,
+ KeyboardKeys::ArrowLeft,
+ KeyboardKeys::ArrowDown,
+ KeyboardKeys::ArrowUp,
+ KeyboardKeys::Numlock,
+ KeyboardKeys::NumpadDivide,
+ KeyboardKeys::NumpadMultiply,
+ KeyboardKeys::NumpadSubtract,
+ KeyboardKeys::NumpadAdd,
+ KeyboardKeys::Return, // SDL_SCANCODE_KP_ENTER ?
+ KeyboardKeys::Numpad1,
+ KeyboardKeys::Numpad2,
+ KeyboardKeys::Numpad3,
+ KeyboardKeys::Numpad4,
+ KeyboardKeys::Numpad5,
+ KeyboardKeys::Numpad6,
+ KeyboardKeys::Numpad7,
+ KeyboardKeys::Numpad8,
+ KeyboardKeys::Numpad9,
+ KeyboardKeys::Numpad0, //98
+ KeyboardKeys::NumpadDecimal, // SDL_SCANCODE_KP_PERIOD
+ KeyboardKeys::Backslash, // SDL_SCANCODE_NONUSBACKSLASH ?
+ KeyboardKeys::Applications,
+ KeyboardKeys::Sleep, // SDL_SCANCODE_POWER ?
+ KeyboardKeys::None, // SDL_SCANCODE_KP_EQUALS ?
+ KeyboardKeys::F13,
+ KeyboardKeys::F14,
+ KeyboardKeys::F15,
+ KeyboardKeys::F16,
+ KeyboardKeys::F17,
+ KeyboardKeys::F18,
+ KeyboardKeys::F19,
+ KeyboardKeys::F20,
+ KeyboardKeys::F21,
+ KeyboardKeys::F22,
+ KeyboardKeys::F23,
+ KeyboardKeys::F24,
+ KeyboardKeys::Execute,
+ KeyboardKeys::Help,
+ KeyboardKeys::LeftMenu, // SDL_SCANCODE_MENU ?
+ KeyboardKeys::Select,
+ KeyboardKeys::None, // SDL_SCANCODE_STOP
+ KeyboardKeys::None, // SDL_SCANCODE_AGAIN
+ KeyboardKeys::None, // SDL_SCANCODE_UNDO
+ KeyboardKeys::None, // SDL_SCANCODE_CUT
+ KeyboardKeys::None, // SDL_SCANCODE_COPY
+ KeyboardKeys::None, // SDL_SCANCODE_PASTE
+ KeyboardKeys::None, // SDL_SCANCODE_FIND
+ KeyboardKeys::None, // SDL_SCANCODE_MUTE
+ KeyboardKeys::None, // SDL_SCANCODE_VOLUMEUP
+ KeyboardKeys::None, // SDL_SCANCODE_VOLUMEDOWN
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::NumpadSeparator, // SDL_SCANCODE_KP_COMMA ?
+ KeyboardKeys::None, // SDL_SCANCODE_KP_EQUALSAS400
+ KeyboardKeys::None, // SDL_SCANCODE_INTERNATIONAL1
+ KeyboardKeys::None, // SDL_SCANCODE_INTERNATIONAL2
+ KeyboardKeys::None, // SDL_SCANCODE_INTERNATIONAL3
+ KeyboardKeys::None, // SDL_SCANCODE_INTERNATIONAL4
+ KeyboardKeys::None, // SDL_SCANCODE_INTERNATIONAL5
+ KeyboardKeys::None, // SDL_SCANCODE_INTERNATIONAL6
+ KeyboardKeys::None, // SDL_SCANCODE_INTERNATIONAL7
+ KeyboardKeys::None, // SDL_SCANCODE_INTERNATIONAL8
+ KeyboardKeys::None, // SDL_SCANCODE_INTERNATIONAL9
+ KeyboardKeys::None, // SDL_SCANCODE_LANG1
+ KeyboardKeys::None, // SDL_SCANCODE_LANG2
+ KeyboardKeys::None, // SDL_SCANCODE_LANG3
+ KeyboardKeys::None, // SDL_SCANCODE_LANG4
+ KeyboardKeys::None, // SDL_SCANCODE_LANG5
+ KeyboardKeys::None, // SDL_SCANCODE_LANG6
+ KeyboardKeys::None, // SDL_SCANCODE_LANG7
+ KeyboardKeys::None, // SDL_SCANCODE_LANG8
+ KeyboardKeys::None, // SDL_SCANCODE_LANG9
+ KeyboardKeys::None, // SDL_SCANCODE_ALTERASE
+ KeyboardKeys::None, // SDL_SCANCODE_SYSREQ
+ KeyboardKeys::None, // SDL_SCANCODE_CANCEL
+ KeyboardKeys::Clear, // SDL_SCANCODE_CLEAR
+ KeyboardKeys::None, // SDL_SCANCODE_PRIOR
+ KeyboardKeys::None, // SDL_SCANCODE_RETURN2
+ KeyboardKeys::None, // SDL_SCANCODE_SEPARATOR
+ KeyboardKeys::None, // SDL_SCANCODE_OUT
+ KeyboardKeys::None, // SDL_SCANCODE_OPER
+ KeyboardKeys::None, // SDL_SCANCODE_CLEARAGAIN
+ KeyboardKeys::None, // SDL_SCANCODE_CRSEL
+ KeyboardKeys::None, // SDL_SCANCODE_EXSEL
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_00
+ KeyboardKeys::None, // SDL_SCANCODE_KP_000
+ KeyboardKeys::None, // SDL_SCANCODE_THOUSANDSSEPARATOR
+ KeyboardKeys::None, // SDL_SCANCODE_DECIMALSEPARATOR
+ KeyboardKeys::None, // SDL_SCANCODE_CURRENCYUNIT
+ KeyboardKeys::None, // SDL_SCANCODE_CURRENCYSUBUNIT
+ KeyboardKeys::None, // SDL_SCANCODE_KP_LEFTPAREN = 182,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_RIGHTPAREN = 183,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_LEFTBRACE = 184,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_RIGHTBRACE = 185,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_TAB = 186,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_BACKSPACE = 187,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_A = 188,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_B = 189,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_C = 190,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_D = 191,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_E = 192,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_F = 193,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_XOR = 194,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_POWER = 195,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_PERCENT = 196,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_LESS = 197,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_GREATER = 198,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_AMPERSAND = 199,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_DBLAMPERSAND = 200,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_VERTICALBAR = 201,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_DBLVERTICALBAR = 202,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_COLON = 203,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_HASH = 204,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_SPACE = 205,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_AT = 206,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_EXCLAM = 207,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_MEMSTORE = 208,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_MEMRECALL = 209,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_MEMCLEAR = 210,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_MEMADD = 211,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_MEMSUBTRACT = 212,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_MEMMULTIPLY = 213,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_MEMDIVIDE = 214,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_PLUSMINUS = 215,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_CLEAR = 216,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_CLEARENTRY = 217,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_BINARY = 218,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_OCTAL = 219,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_DECIMAL = 220,
+ KeyboardKeys::None, // SDL_SCANCODE_KP_HEXADECIMAL = 221,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::Control, // SDL_SCANCODE_LCTRL = 224,
+ KeyboardKeys::Shift, // SDL_SCANCODE_LSHIFT = 225,
+ KeyboardKeys::Alt, // SDL_SCANCODE_LALT = 226,
+ KeyboardKeys::LeftMenu, // SDL_SCANCODE_LGUI = 227,
+ KeyboardKeys::Control, // SDL_SCANCODE_RCTRL = 228,
+ KeyboardKeys::Shift, // SDL_SCANCODE_RSHIFT = 229,
+ KeyboardKeys::Alt, // SDL_SCANCODE_RALT = 230,
+ KeyboardKeys::RightMenu, // SDL_SCANCODE_RGUI = 231,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::None,
+ KeyboardKeys::Modechange, // SDL_SCANCODE_MODE
+ KeyboardKeys::Sleep, // SDL_SCANCODE_SLEEP
+ KeyboardKeys::None, // SDL_SCANCODE_WAKE
+ KeyboardKeys::None, // SDL_SCANCODE_CHANNEL_INCREMENT = 260,
+ KeyboardKeys::None, // SDL_SCANCODE_CHANNEL_DECREMENT = 261,
+ KeyboardKeys::None, // SDL_SCANCODE_MEDIA_PLAY = 262,
+ KeyboardKeys::None, // SDL_SCANCODE_MEDIA_PAUSE = 263,
+ KeyboardKeys::None, // SDL_SCANCODE_MEDIA_RECORD = 264,
+ KeyboardKeys::None, // SDL_SCANCODE_MEDIA_FAST_FORWARD = 265,
+ KeyboardKeys::None, // SDL_SCANCODE_MEDIA_REWIND = 266,
+ KeyboardKeys::MediaNextTrack, // SDL_SCANCODE_MEDIA_NEXT_TRACK = 267,
+ KeyboardKeys::MediaPrevTrack, // SDL_SCANCODE_MEDIA_PREVIOUS_TRACK = 268,
+ KeyboardKeys::MediaStop, // SDL_SCANCODE_MEDIA_STOP = 269,
+ KeyboardKeys::None, // SDL_SCANCODE_MEDIA_EJECT = 270,
+ KeyboardKeys::MediaPlayPause, // SDL_SCANCODE_MEDIA_PLAY_PAUSE = 271,
+ KeyboardKeys::None, // SDL_SCANCODE_MEDIA_SELECT = 272,
+ KeyboardKeys::None, // SDL_SCANCODE_AC_NEW = 273,
+ KeyboardKeys::None, // SDL_SCANCODE_AC_OPEN = 274,
+ KeyboardKeys::None, // SDL_SCANCODE_AC_CLOSE = 275,
+ KeyboardKeys::None, // SDL_SCANCODE_AC_EXIT = 276,
+ KeyboardKeys::None, // SDL_SCANCODE_AC_SAVE = 277,
+ KeyboardKeys::None, // SDL_SCANCODE_AC_PRINT = 278,
+ KeyboardKeys::None, // SDL_SCANCODE_AC_PROPERTIES = 279,
+ KeyboardKeys::None, // SDL_SCANCODE_AC_SEARCH = 280,
+ KeyboardKeys::None, // SDL_SCANCODE_AC_HOME = 281,
+ KeyboardKeys::None, // SDL_SCANCODE_AC_BACK = 282,
+ KeyboardKeys::None, // SDL_SCANCODE_AC_FORWARD = 283,
+ KeyboardKeys::None, // SDL_SCANCODE_AC_STOP = 284,
+ KeyboardKeys::None, // SDL_SCANCODE_AC_REFRESH = 285,
+ KeyboardKeys::None, // SDL_SCANCODE_AC_BOOKMARKS = 286,
+ KeyboardKeys::None, // SDL_SCANCODE_SOFTLEFT = 287,
+ KeyboardKeys::None, // SDL_SCANCODE_SOFTRIGHT = 288,
+ KeyboardKeys::None, // SDL_SCANCODE_CALL = 289,
+ KeyboardKeys::None, // SDL_SCANCODE_ENDCALL = 290
+};
+
+///
+/// Implementation of the keyboard device for Windows platform.
+///
+///
+class SDLKeyboard : public Keyboard
+{
+public:
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ explicit SDLKeyboard()
+ : Keyboard()
+ {
+ }
+
+public:
+
+};
+
+///
+/// Implementation of the mouse device for SDL platform.
+///
+///
+class SDLMouse : public Mouse
+{
+private:
+ Float2 _oldPosition = Float2::Zero;
+ Window* _relativeModeWindow = nullptr;
+ const SDL_Rect* _oldScreenRect = nullptr;
+public:
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ explicit SDLMouse()
+ : Mouse()
+ {
+ }
+
+public:
+
+ ///
+ /// Returns the previous known position of the mouse before entering relative mode.
+ ///
+ Float2 GetOldMousePosition() const
+ {
+ ASSERT(_relativeModeWindow != nullptr);
+ return _relativeModeWindow->ClientToScreen(_oldPosition);
+ }
+
+ // [Mouse]
+ void SetMousePosition(const Float2& screenPosition) final override
+ {
+#if USE_EDITOR
+ auto window = Editor::Managed->GetGameWindow();
+ if (window == nullptr)
+ window = Engine::MainWindow;
+#else
+ auto window = Engine::MainWindow;
+#endif
+ Float2 position = window->ScreenToClient(screenPosition);
+ SDL_WarpMouseInWindow(static_cast(window)->_window, position.X, position.Y);
+
+ OnMouseMoved(screenPosition);
+ }
+
+ void SetRelativeMode(bool relativeMode, Window* window) final override
+ {
+ ASSERT(window != nullptr);
+ if (relativeMode == _relativeMode)
+ return;
+
+ auto windowHandle = static_cast(window)->_window;
+ if (relativeMode)
+ {
+ _relativeModeWindow = window;
+ SDL_GetMouseState(&_oldPosition.X, &_oldPosition.Y);
+ if (!SDL_CursorVisible())
+ {
+ // Trap the cursor in current location
+ SDL_Rect clipRect = { (int)_oldPosition.X, (int)_oldPosition.Y, 1, 1 };
+ _oldScreenRect = SDL_GetWindowMouseRect(windowHandle);
+ SDL_SetWindowMouseRect(windowHandle, &clipRect);
+ }
+ }
+ else
+ {
+ if (_relativeModeWindow != window)
+ {
+ // FIXME: When floating game window is focused and editor viewport activated, the relative mode gets stuck
+ return;
+ }
+ SDL_SetWindowMouseRect(windowHandle, nullptr);//oldScreenRect);
+ SDL_WarpMouseInWindow(windowHandle, _oldPosition.X, _oldPosition.Y);
+ _oldScreenRect = nullptr;
+ _relativeModeWindow = nullptr;
+ }
+
+ Mouse::SetRelativeMode(relativeMode, window);
+ if (!SDL_SetWindowRelativeMouseMode(windowHandle, relativeMode))
+ LOG(Error, "Failed to set mouse relative mode: {0}", String(SDL_GetError()));
+
+#if PLATFORM_MAC
+ // Warping right before entering relative mode seems to generate motion event for relative mode
+ SDL_PumpEvents();
+ SDL_FlushEvent(SDL_EVENT_MOUSE_MOTION);
+#endif
+ }
+
+ bool IsRelative(Window* window) const override
+ {
+ if (window == nullptr)
+ return _relativeMode;
+ return _relativeModeWindow == window && _relativeMode;
+ }
+};
+
+///
+/// Implementation of the gamepad device for SDL platform.
+///
+///
+class SDLGamepad : public Gamepad
+{
+private:
+
+ SDL_Gamepad* _gamepad = nullptr;
+ SDL_JoystickID _instanceId;
+
+public:
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The joystick.
+ explicit SDLGamepad(SDL_JoystickID instanceId);
+
+ ///
+ /// Finalizes an instance of the class.
+ ///
+ ~SDLGamepad();
+
+private:
+
+ SDLGamepad(SDL_Gamepad* gamepad, SDL_JoystickID instanceId);
+
+public:
+
+ static SDLGamepad* GetGamepadById(SDL_JoystickID id)
+ {
+ SDLGamepad* gamepad = nullptr;
+ SDLInputImpl::Gamepads.TryGet(id, gamepad);
+ return gamepad;
+ }
+
+ SDL_JoystickID GetJoystickInstanceId() const
+ {
+ return _instanceId;
+ }
+
+ void OnAxisMotion(SDL_GamepadAxis axis, int16 value);
+
+ void OnButtonState(SDL_GamepadButton axis, bool pressed);
+
+ // [Gamepad]
+ void SetVibration(const GamepadVibrationState& state) override;
+
+protected:
+
+ // [Gamepad]
+ bool UpdateState() override;
+};
+
+void SDLInput::Init()
+{
+ Input::Mouse = SDLInputImpl::Mouse = New();
+ Input::Keyboard = SDLInputImpl::Keyboard = New();
+}
+
+void SDLInput::Update()
+{
+}
+
+float NormalizeAxisValue(const int16 axisVal)
+{
+ // Normalize [-32768..32767] -> [-1..1]
+ const float norm = axisVal <= 0 ? 32768.0f : 32767.0f;
+ return float(axisVal) / norm;
+}
+
+bool SDLInput::HandleEvent(SDLWindow* window, SDL_Event& event)
+{
+ switch (event.type)
+ {
+ case SDL_EVENT_MOUSE_MOTION:
+ {
+ if (Input::Mouse->IsRelative())
+ {
+ const Float2 mouseDelta(event.motion.xrel, event.motion.yrel);
+ Input::Mouse->OnMouseMoveRelative(mouseDelta, window);
+ }
+ else
+ {
+ const Float2 mousePos = window->ClientToScreen({ event.motion.x, event.motion.y });
+ Input::Mouse->OnMouseMove(mousePos, window);
+ }
+ return true;
+ }
+ case SDL_EVENT_WINDOW_MOUSE_LEAVE:
+ {
+ Input::Mouse->OnMouseLeave(window);
+ return true;
+ }
+ case SDL_EVENT_MOUSE_BUTTON_DOWN:
+ case SDL_EVENT_MOUSE_BUTTON_UP:
+ {
+ Float2 mousePos = window->ClientToScreen({ event.button.x, event.button.y });
+
+ // In case of activating the window from non-focused state, we might not have received the motion event yet
+ if (!Input::Mouse->IsRelative() && event.button.down)
+ Input::Mouse->OnMouseMove(mousePos, window);
+
+ MouseButton button = MouseButton::None;
+ if (event.button.button == SDL_BUTTON_LEFT)
+ button = MouseButton::Left;
+ else if (event.button.button == SDL_BUTTON_RIGHT)
+ button = MouseButton::Right;
+ else if (event.button.button == SDL_BUTTON_MIDDLE)
+ button = MouseButton::Middle;
+ else if (event.button.button == SDL_BUTTON_X1)
+ button = MouseButton::Extended1;
+ else if (event.button.button == SDL_BUTTON_X2)
+ button = MouseButton::Extended2;
+
+ if (Input::Mouse->IsRelative())
+ {
+ // Use the previous visible mouse position here, the event or global
+ // mouse position would cause input to trigger in other editor windows.
+ mousePos = SDLInputImpl::Mouse->GetOldMousePosition();
+ }
+
+ if (!event.button.down)
+ Input::Mouse->OnMouseUp(mousePos, button, window);
+ // Prevent sending multiple mouse down event when double-clicking UI elements
+ else if (event.button.clicks % 2 == 1)
+ Input::Mouse->OnMouseDown(mousePos, button, window);
+ else
+ Input::Mouse->OnMouseDoubleClick(mousePos, button, window);
+
+ return true;
+ }
+ case SDL_EVENT_MOUSE_WHEEL:
+ {
+ Float2 mousePos = window->ClientToScreen({ event.wheel.mouse_x, event.wheel.mouse_y });
+ const float delta = event.wheel.y;
+
+ if (Input::Mouse->IsRelative())
+ {
+ // Use the previous visible mouse position here, the event or global
+ // mouse position would cause input to trigger in other editor windows.
+ mousePos = SDLInputImpl::Mouse->GetOldMousePosition();
+ }
+
+ Input::Mouse->OnMouseWheel(mousePos, delta, window);
+ return true;
+ }
+ case SDL_EVENT_KEY_DOWN:
+ case SDL_EVENT_KEY_UP:
+ {
+ // TODO: scancode support
+ KeyboardKeys key = SDL_TO_FLAX_KEYS_MAP[event.key.scancode];
+ //event.key.mod
+ if (!event.key.down)
+ Input::Keyboard->OnKeyUp(key, window);
+ else
+ Input::Keyboard->OnKeyDown(key, window);
+ return true;
+ }
+ case SDL_EVENT_TEXT_EDITING:
+ {
+ auto edit = event.edit;
+ return true;
+ }
+ case SDL_EVENT_TEXT_INPUT:
+ {
+ String text(event.text.text);
+ for (int i = 0; i < text.Length(); i++)
+ {
+ Input::Keyboard->OnCharInput(text[i], window);
+ }
+ return true;
+ }
+ case SDL_EVENT_GAMEPAD_AXIS_MOTION:
+ {
+ SDLGamepad* gamepad = SDLGamepad::GetGamepadById(event.gaxis.which);
+ SDL_GamepadAxis axis = (SDL_GamepadAxis)event.gaxis.axis;
+ gamepad->OnAxisMotion(axis, event.gaxis.value);
+ break;
+ }
+ case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
+ case SDL_EVENT_GAMEPAD_BUTTON_UP:
+ {
+ SDLGamepad* gamepad = SDLGamepad::GetGamepadById(event.gbutton.which);
+ SDL_GamepadButton button = (SDL_GamepadButton)event.gbutton.button;
+ gamepad->OnButtonState(button, event.gbutton.down);
+ break;
+ }
+ case SDL_EVENT_GAMEPAD_ADDED:
+ {
+ Input::Gamepads.Add(New(event.gdevice.which));
+ Input::OnGamepadsChanged();
+ break;
+ }
+ case SDL_EVENT_GAMEPAD_REMOVED:
+ {
+ for (int i = 0; i < Input::Gamepads.Count(); i++)
+ {
+ SDLGamepad* gamepad = static_cast(Input::Gamepads[i]);
+ if (gamepad->GetJoystickInstanceId() == event.gdevice.which)
+ {
+ Input::Gamepads[i]->DeleteObject();
+ Input::Gamepads.RemoveAtKeepOrder(i);
+ Input::OnGamepadsChanged();
+ break;
+ }
+ }
+ break;
+ }
+ case SDL_EVENT_GAMEPAD_REMAPPED:
+ {
+ auto ev = event.gdevice;
+ LOG(Info, "TODO: SDL_EVENT_GAMEPAD_REMAPPED");
+ break;
+ }
+ case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN:
+ {
+ LOG(Info, "TODO: SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN");
+ break;
+ }
+ case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION:
+ {
+ LOG(Info, "TODO: SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION");
+ break;
+ }
+ case SDL_EVENT_GAMEPAD_TOUCHPAD_UP:
+ {
+ LOG(Info, "TODO: SDL_EVENT_GAMEPAD_TOUCHPAD_UP");
+ break;
+ }
+ case SDL_EVENT_GAMEPAD_SENSOR_UPDATE:
+ {
+ LOG(Info, "TODO: SDL_EVENT_GAMEPAD_SENSOR_UPDATE");
+ break;
+ }
+ case SDL_EVENT_GAMEPAD_STEAM_HANDLE_UPDATED:
+ {
+ auto ev = event.gdevice;
+ LOG(Info, "TODO: SDL_EVENT_GAMEPAD_STEAM_HANDLE_UPDATED");
+ break;
+ }
+ }
+ return false;
+}
+
+Guid GetGamepadGuid(SDL_JoystickID instanceId)
+{
+ SDL_GUID joystickGuid = SDL_GetGamepadGUIDForID(instanceId);
+ Guid guid;
+ Platform::MemoryCopy(&guid.Raw, joystickGuid.data, sizeof(uint8) * 16);
+ return guid;
+}
+
+SDLGamepad::SDLGamepad(SDL_JoystickID instanceId)
+ : SDLGamepad(SDL_OpenGamepad(instanceId), instanceId)
+{
+ LOG(Info, "Gamepad connected: {}", String(SDL_GetGamepadName(_gamepad)));
+}
+
+SDLGamepad::SDLGamepad(SDL_Gamepad* gamepad, SDL_JoystickID instanceId)
+ : Gamepad(GetGamepadGuid(instanceId), String(SDL_GetGamepadName(gamepad)))
+ , _gamepad(gamepad)
+ , _instanceId(instanceId)
+{
+ SDLInputImpl::Gamepads.Add(_instanceId, this);
+}
+
+SDLGamepad::~SDLGamepad()
+{
+ SDL_CloseGamepad(_gamepad);
+ SDLInputImpl::Gamepads.Remove(_instanceId);
+}
+
+void SDLGamepad::SetVibration(const GamepadVibrationState& state)
+{
+ Gamepad::SetVibration(state);
+ LOG(Info, "TODO: SDLGamepad::SetVibration");
+}
+
+bool SDLGamepad::UpdateState()
+{
+ return false;
+}
+
+void SDLGamepad::OnAxisMotion(SDL_GamepadAxis sdlAxis, int16 value)
+{
+ GamepadAxis axis;
+ int16 deadzone = 1; // SDL reports -1 for centered axis?
+ float valueNormalized = NormalizeAxisValue(value);
+ switch (sdlAxis)
+ {
+ case SDL_GAMEPAD_AXIS_LEFTX:
+ axis = GamepadAxis::LeftStickX;
+ deadzone = LEFT_STICK_THRESHOLD;
+ _state.Buttons[(int32)GamepadButton::LeftStickLeft] = value > LEFT_STICK_THRESHOLD;
+ _state.Buttons[(int32)GamepadButton::LeftStickRight] = value < -LEFT_STICK_THRESHOLD;
+ break;
+ case SDL_GAMEPAD_AXIS_LEFTY:
+ axis = GamepadAxis::LeftStickY;
+ deadzone = LEFT_STICK_THRESHOLD;
+ _state.Buttons[(int32)GamepadButton::LeftStickUp] = value < -LEFT_STICK_THRESHOLD;
+ _state.Buttons[(int32)GamepadButton::LeftStickDown] = value > LEFT_STICK_THRESHOLD;
+ valueNormalized = -valueNormalized;
+ break;
+ case SDL_GAMEPAD_AXIS_RIGHTX:
+ deadzone = RIGHT_STICK_THRESHOLD;
+ axis = GamepadAxis::RightStickX;
+ _state.Buttons[(int32)GamepadButton::RightStickLeft] = value > RIGHT_STICK_THRESHOLD;
+ _state.Buttons[(int32)GamepadButton::RightStickRight] = value < -RIGHT_STICK_THRESHOLD;
+ break;
+ case SDL_GAMEPAD_AXIS_RIGHTY:
+ deadzone = RIGHT_STICK_THRESHOLD;
+ axis = GamepadAxis::RightStickY;
+ _state.Buttons[(int32)GamepadButton::RightStickUp] = value < -RIGHT_STICK_THRESHOLD;
+ _state.Buttons[(int32)GamepadButton::RightStickDown] = value > RIGHT_STICK_THRESHOLD;
+ valueNormalized = -valueNormalized;
+ break;
+ case SDL_GAMEPAD_AXIS_LEFT_TRIGGER:
+ deadzone = TRIGGER_THRESHOLD;
+ axis = GamepadAxis::LeftTrigger;
+ _state.Buttons[(int32)GamepadButton::LeftTrigger] = value > TRIGGER_THRESHOLD;
+ break;
+ case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER:
+ deadzone = TRIGGER_THRESHOLD;
+ axis = GamepadAxis::RightTrigger;
+ _state.Buttons[(int32)GamepadButton::RightTrigger] = value > TRIGGER_THRESHOLD;
+ break;
+ default:
+ return;
+ }
+ if (value <= deadzone && value >= -deadzone)
+ valueNormalized = 0.0f;
+ _state.Axis[(int32)axis] = valueNormalized;
+}
+
+void SDLGamepad::OnButtonState(SDL_GamepadButton sdlButton, bool pressed)
+{
+ switch (sdlButton)
+ {
+ case SDL_GamepadButton::SDL_GAMEPAD_BUTTON_SOUTH:
+ _state.Buttons[(int32)GamepadButton::A] = pressed;
+ break;
+ case SDL_GamepadButton::SDL_GAMEPAD_BUTTON_EAST:
+ _state.Buttons[(int32)GamepadButton::B] = pressed;
+ break;
+ case SDL_GamepadButton::SDL_GAMEPAD_BUTTON_WEST:
+ _state.Buttons[(int32)GamepadButton::X] = pressed;
+ break;
+ case SDL_GamepadButton::SDL_GAMEPAD_BUTTON_NORTH:
+ _state.Buttons[(int32)GamepadButton::Y] = pressed;
+ break;
+ case SDL_GamepadButton::SDL_GAMEPAD_BUTTON_LEFT_SHOULDER:
+ _state.Buttons[(int32)GamepadButton::LeftShoulder] = pressed;
+ break;
+ case SDL_GamepadButton::SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER:
+ _state.Buttons[(int32)GamepadButton::RightShoulder] = pressed;
+ break;
+ case SDL_GamepadButton::SDL_GAMEPAD_BUTTON_BACK:
+ _state.Buttons[(int32)GamepadButton::Back] = pressed;
+ break;
+ case SDL_GamepadButton::SDL_GAMEPAD_BUTTON_START:
+ _state.Buttons[(int32)GamepadButton::Start] = pressed;
+ break;
+ case SDL_GamepadButton::SDL_GAMEPAD_BUTTON_LEFT_STICK:
+ _state.Buttons[(int32)GamepadButton::LeftThumb] = pressed;
+ break;
+ case SDL_GamepadButton::SDL_GAMEPAD_BUTTON_RIGHT_STICK:
+ _state.Buttons[(int32)GamepadButton::RightThumb] = pressed;
+ break;
+ case SDL_GamepadButton::SDL_GAMEPAD_BUTTON_DPAD_UP:
+ _state.Buttons[(int32)GamepadButton::DPadUp] = pressed;
+ break;
+ case SDL_GamepadButton::SDL_GAMEPAD_BUTTON_DPAD_DOWN:
+ _state.Buttons[(int32)GamepadButton::DPadDown] = pressed;
+ break;
+ case SDL_GamepadButton::SDL_GAMEPAD_BUTTON_DPAD_LEFT:
+ _state.Buttons[(int32)GamepadButton::DPadLeft] = pressed;
+ break;
+ case SDL_GamepadButton::SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
+ _state.Buttons[(int32)GamepadButton::DPadRight] = pressed;
+ break;
+ }
+}
+
+#endif
diff --git a/Source/Engine/Platform/SDL/SDLInput.h b/Source/Engine/Platform/SDL/SDLInput.h
new file mode 100644
index 000000000..cb1e652a0
--- /dev/null
+++ b/Source/Engine/Platform/SDL/SDLInput.h
@@ -0,0 +1,22 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+#pragma once
+
+#if PLATFORM_SDL
+
+class SDLWindow;
+union SDL_Event;
+
+///
+/// SDL specific implementation of the input system parts.
+///
+class SDLInput
+{
+public:
+
+ static void Init();
+ static void Update();
+ static bool HandleEvent(SDLWindow* window, SDL_Event& event);
+};
+
+#endif
diff --git a/Source/Engine/Platform/SDL/SDLPlatform.Linux.cpp b/Source/Engine/Platform/SDL/SDLPlatform.Linux.cpp
new file mode 100644
index 000000000..a30034d7a
--- /dev/null
+++ b/Source/Engine/Platform/SDL/SDLPlatform.Linux.cpp
@@ -0,0 +1,1923 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+#if PLATFORM_SDL && PLATFORM_LINUX
+
+#include "SDLWindow.h"
+#include "Engine/Core/Log.h"
+#include "Engine/Core/Collections/Array.h"
+#include "Engine/Engine/CommandLine.h"
+#include "Engine/Engine/Engine.h"
+#include "Engine/Engine/Time.h"
+#include "Engine/Graphics/RenderTask.h"
+#include "Engine/Input/Input.h"
+#include "Engine/Input/Mouse.h"
+#include "Engine/Platform/IGuiData.h"
+#include "Engine/Platform/MessageBox.h"
+#include "Engine/Platform/Platform.h"
+#include "Engine/Platform/WindowsManager.h"
+#include "Engine/Platform/Base/DragDropHelper.h"
+#include "Engine/Platform/SDL/SDLClipboard.h"
+#include "Engine/Platform/Unix/UnixFile.h"
+#include "Engine/Profiler/ProfilerCPU.h"
+
+#include "Engine/Platform/Linux/IncludeX11.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+namespace SDLImpl
+{
+ extern String WaylandDisplayEnv;
+ extern String XDGCurrentDesktop;
+}
+
+class LinuxDropFilesData : public IGuiData
+{
+public:
+ Array Files;
+ SDLWindow* Window;
+
+ Type GetType() const override
+ {
+ return Type::Files;
+ }
+ String GetAsText() const override
+ {
+ return String::Empty;
+ }
+ void GetAsFiles(Array* files) const override
+ {
+ files->Add(Files);
+ }
+};
+
+class LinuxDropTextData : public IGuiData
+{
+public:
+ StringView Text;
+
+ Type GetType() const override
+ {
+ return Type::Text;
+ }
+ String GetAsText() const override
+ {
+ return String(Text);
+ }
+ void GetAsFiles(Array* files) const override
+ {
+ }
+};
+
+namespace WaylandImpl
+{
+ wl_display* WaylandDisplay = nullptr;
+ int64 DragOverFlag = 0;
+ int64 Serial = 0;
+ wl_pointer_listener PointerListener =
+ {
+ [](void* data, wl_pointer* wl_pointer, uint32_t serial, wl_surface* surface, wl_fixed_t surface_x, wl_fixed_t surface_y) { }, // Enter event
+ [](void* data, wl_pointer* wl_pointer, uint32_t serial, wl_surface* surface) { }, // Leave event
+ [](void* data, wl_pointer* wl_pointer, uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y) { }, // Motion event
+ [](void* data, wl_pointer* wl_pointer, uint32_t serial, uint32_t time, uint32_t button, uint32_t state) // Button event
+ {
+ // Store the serial for upcoming drag-and-drop action
+ if (state == 1)
+ Platform::AtomicStore(&Serial, serial);
+ else
+ Platform::AtomicStore(&Serial, 0);
+ },
+ [](void* data, wl_pointer* wl_pointer, uint32_t time, uint32_t axis, wl_fixed_t value) { }, // Axis event
+ [](void* data, wl_pointer* wl_pointer) { }, // Frame event
+ [](void* data, wl_pointer* wl_pointer, uint32_t axis_source) { }, // Axis source event
+ [](void* data, wl_pointer* wl_pointer, uint32_t time, uint32_t axis) { }, // Axis stop event
+ [](void* data, wl_pointer* wl_pointer, uint32_t axis, int32_t discrete) { }, // axis discrete
+ [](void* data, wl_pointer* wl_pointer, uint32_t axis, int32_t value120) { }, // Scroll wheel event
+ [](void* data, wl_pointer* wl_pointer, uint32_t axis, uint32_t direction) { }, // Relative direction event
+ };
+
+ wl_pointer* WaylandPointer = nullptr;
+ wl_seat_listener SeatListener =
+ {
+ [](void* data, wl_seat* seat, uint32 capabilities) // Seat capabilities changed event
+ {
+ if ((capabilities & wl_seat_capability::WL_SEAT_CAPABILITY_POINTER) != 0)
+ {
+ WaylandPointer = wl_seat_get_pointer(seat);
+ wl_pointer_add_listener(WaylandPointer, &PointerListener, nullptr);
+ }
+ },
+ [](void* data, wl_seat* seat, const char* name) { } // Seat name event
+ };
+
+ xdg_toplevel_drag_manager_v1* DragManager = nullptr;
+ wl_seat* Seat = nullptr;
+ wl_data_device_manager* DataDeviceManager = nullptr;
+ wl_registry_listener RegistryListener =
+ {
+ [](void* data, wl_registry* registry, uint32 id, const char* interface, uint32 version) // Announce global object event
+ {
+ StringAnsi interfaceStr(interface);
+ if (interfaceStr == "xdg_toplevel_drag_manager_v1")
+ DragManager = static_cast(wl_registry_bind(registry, id, &xdg_toplevel_drag_manager_v1_interface, Math::Min(1U, version)));
+ else if (interfaceStr == "wl_seat")
+ {
+ Seat = static_cast(wl_registry_bind(registry, id, &wl_seat_interface, Math::Min(9U, version)));
+ wl_seat_add_listener(Seat, &SeatListener, nullptr);
+ }
+ else if (interfaceStr == "wl_data_device_manager")
+ DataDeviceManager = static_cast(wl_registry_bind(registry, id, &wl_data_device_manager_interface, Math::Min(3U, version)));
+ },
+ [] (void* data, wl_registry* registry, uint32 id) { }, // Announce global remove event
+ };
+
+ wl_data_offer* DataOffer = nullptr; // The last accepted offer
+ wl_data_offer* SelectionOffer = nullptr;
+ wl_data_device_listener DataDeviceListener =
+ {
+ [](void* data, wl_data_device* data_device, wl_data_offer* id) { }, // Data offer event
+ [](void* data, wl_data_device* data_device, uint32 serial, wl_surface* surface, wl_fixed_t x, wl_fixed_t y, wl_data_offer* id) // Enter event
+ {
+ DataOffer = id;
+
+ SDLWindow* sourceWindow = (SDLWindow*)data;
+ if (sourceWindow != nullptr)
+ {
+ // Let them know that we support the following action at this given point
+ wl_data_offer_set_actions(id, WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE, WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE);
+ }
+ else
+ {
+ wl_data_offer_set_actions(id, WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE, WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE);
+ }
+ },
+ [](void* data, wl_data_device* data_device) // Leave event
+ {
+ // The cursor left the surface area
+ if (DataOffer != nullptr)
+ wl_data_offer_destroy(DataOffer);
+ DataOffer = nullptr;
+ },
+ [](void* data, wl_data_device* data_device, uint32_t time, wl_fixed_t x, wl_fixed_t y) { }, // Motion event
+ [](void* data, wl_data_device* data_device) // Drop event
+ {
+ // The drop is accepted
+ if (DataOffer != nullptr)
+ {
+ wl_data_offer_finish(DataOffer);
+ wl_data_offer_destroy(DataOffer);
+ DataOffer = nullptr;
+ }
+ },
+ [](void* data, wl_data_device* data_device, wl_data_offer* id) // Selection event
+ {
+ // Clipboard: We can read the clipboard content
+ if (SelectionOffer != nullptr)
+ wl_data_offer_destroy(SelectionOffer);
+ SelectionOffer = id;
+ },
+ };
+
+ wl_data_source_listener DataSourceListener =
+ {
+ [](void* data, wl_data_source* source, const char* mime_type) { }, // Target event
+ [](void* data, wl_data_source* source, const char* mime_type, int32_t fd) // Send event
+ {
+ // Clipboard: The other end has accepted and is requesting the data
+ IGuiData* inputData = static_cast(data);
+ if (inputData->GetType() == IGuiData::Type::Text)
+ {
+ UnixFile file(fd);
+ StringAnsi text = StringAnsi(inputData->GetAsText());
+ file.Write(text.Get(), text.Length() * sizeof(StringAnsi::CharType));
+ file.Close();
+ }
+ },
+ [](void* data, wl_data_source* source) // Cancelled event
+ {
+ // Clipboard: other application has replaced the content in clipboad
+ wl_data_source_destroy(source);
+
+ IGuiData* inputData = static_cast(data);
+ Platform::AtomicStore(&WaylandImpl::DragOverFlag, 1);
+ },
+ [](void* data, wl_data_source* source) { }, // DnD drop performed event
+ [](void* data, wl_data_source* source) // DnD Finished event
+ {
+ // The destination has finally accepted the last given dnd_action
+ wl_data_source_destroy(source);
+
+ IGuiData* inputData = static_cast(data);
+ Platform::AtomicStore(&WaylandImpl::DragOverFlag, 1);
+ },
+ [](void* data, wl_data_source* source, uint32_t dnd_action) { }, // Action event
+ };
+
+ wl_data_device* DataDevice = nullptr;
+ wl_event_queue* EventQueue = nullptr;
+ wl_data_device_manager* WrappedDataDeviceManager = nullptr;
+ wl_data_device* WrappedDataDevice = nullptr;
+ bool DraggingActive = false;
+ bool DraggingWindow = false;
+ StringView DraggingData = nullptr;
+ class DragDropJob : public ThreadPoolTask
+ {
+ public:
+ int64 StartFlag = 0;
+ int64 WaitFlag = 0;
+ int64 ExitFlag = 0;
+ SDLWindow* Window = nullptr;
+ SDLWindow* DragSourceWindow = nullptr;
+ Float2 DragOffset = Float2::Zero;
+ uint32 DragSerial = 0;
+
+ // [ThreadPoolTask]
+ bool Run() override
+ {
+ bool dragWindow = DraggingWindow;
+
+ if (EventQueue == nullptr)
+ {
+ if (WrappedDataDevice != nullptr)
+ wl_proxy_wrapper_destroy(WrappedDataDevice);
+ if (WrappedDataDeviceManager != nullptr)
+ wl_proxy_wrapper_destroy(WrappedDataDeviceManager);
+ if (DataDevice != nullptr)
+ wl_data_device_destroy(DataDevice);
+
+ // This seems to throw bogus warnings about wl_data_source still being attached to the queue
+ if (EventQueue != nullptr)
+ wl_event_queue_destroy(EventQueue);
+ EventQueue = wl_display_create_queue(WaylandDisplay);
+
+ WrappedDataDeviceManager = static_cast(wl_proxy_create_wrapper(DataDeviceManager));
+ wl_proxy_set_queue(reinterpret_cast(WrappedDataDeviceManager), EventQueue);
+
+ DataDevice = wl_data_device_manager_get_data_device(WrappedDataDeviceManager, Seat);
+ wl_data_device_add_listener(DataDevice, &DataDeviceListener, nullptr);
+ wl_display_roundtrip(WaylandDisplay);
+ wl_data_device_set_user_data(DataDevice, dragWindow ? DragSourceWindow : Window);
+
+ WrappedDataDevice = static_cast(wl_proxy_create_wrapper(DataDevice));
+ wl_proxy_set_queue(reinterpret_cast(WrappedDataDevice), EventQueue);
+ }
+
+ // Offer data for consumption, the data source is destroyed elsewhere
+ wl_data_source* dataSource = wl_data_device_manager_create_data_source(WrappedDataDeviceManager);
+ wl_data_source* wrappedDataSource = (wl_data_source*)wl_proxy_create_wrapper(dataSource);
+ wl_proxy_set_queue(reinterpret_cast(wrappedDataSource), EventQueue);
+ if (dragWindow)
+ {
+ wl_data_source_offer(dataSource, "flaxengine/window");
+ wl_data_source_offer(dataSource, "text/plain;charset=utf-8"); // TODO: needs support for custom mime-types in SDL
+ wl_data_source_set_actions(dataSource, WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE);
+ }
+ else
+ {
+ wl_data_source_offer(dataSource, "text/plain");
+ wl_data_source_offer(dataSource, "text/plain;charset=utf-8");
+ wl_data_source_set_actions(dataSource, WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE | WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY);
+ }
+ LinuxDropTextData textData;
+ textData.Text = *DraggingData;
+ wl_data_source_add_listener(dataSource, &DataSourceListener, &textData);
+
+ // Begin dragging operation
+ auto draggedWindow = Window->GetSDLWindow();
+ auto dragStartWindow = DragSourceWindow != nullptr ? DragSourceWindow->GetSDLWindow() : draggedWindow;
+ wl_surface* originSurface = static_cast(SDL_GetPointerProperty(SDL_GetWindowProperties(dragStartWindow), SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, nullptr));
+ wl_surface* iconSurface = nullptr;
+ wl_data_device_start_drag(WrappedDataDevice, dataSource, originSurface, iconSurface, DragSerial);
+
+ Platform::AtomicStore(&StartFlag, 1);
+
+ xdg_toplevel_drag_v1* toplevelDrag = nullptr;
+ xdg_toplevel* wrappedToplevel = nullptr;
+
+ // Start dispatching events to keep data offers alive
+ while (Platform::AtomicRead(&ExitFlag) == 0 && Platform::AtomicRead(&Serial) == DragSerial && Platform::AtomicRead(&DragOverFlag) == 0)
+ {
+ if (DragManager != nullptr && wrappedToplevel == nullptr && dragWindow && Platform::AtomicRead(&WaitFlag) != 0)
+ {
+ // Wait until the dragged window has showed up
+ auto toplevel = static_cast(SDL_GetPointerProperty(SDL_GetWindowProperties(draggedWindow), SDL_PROP_WINDOW_WAYLAND_XDG_TOPLEVEL_POINTER, nullptr));
+ if (toplevel != nullptr)
+ {
+ if (Platform::AtomicRead(&DragOverFlag) == 1 || Platform::AtomicRead(&Serial) != DragSerial)
+ break;
+
+ // Attach the window to the ongoing drag operation
+ wrappedToplevel = static_cast(wl_proxy_create_wrapper(toplevel));
+ wl_proxy_set_queue(reinterpret_cast(wrappedToplevel), EventQueue);
+ toplevelDrag = xdg_toplevel_drag_manager_v1_get_xdg_toplevel_drag(DragManager, dataSource);
+
+ Float2 scaledOffset = DragOffset / Window->GetDpiScale();
+ xdg_toplevel_drag_v1_attach(toplevelDrag, wrappedToplevel, static_cast(scaledOffset.X), static_cast(scaledOffset.Y));
+ }
+ }
+
+ if (wl_display_roundtrip_queue(WaylandDisplay, EventQueue) == -1)
+ LOG(Warning, "wl_display_roundtrip_queue failed, errno: {}", errno);
+ }
+
+ if (toplevelDrag != nullptr)
+ {
+ // Wait for pending operations to finish
+ while (Platform::AtomicRead(&DragOverFlag) == 0 && Platform::AtomicRead(&Serial) == DragSerial)
+ {
+ if (wl_display_dispatch_queue_pending(WaylandDisplay, EventQueue) == -1)
+ LOG(Warning, "wl_display_dispatch_queue_pending failed, errno: {}", errno);
+ Platform::Sleep(1);
+ }
+
+ wl_proxy_wrapper_destroy(wrappedToplevel);
+ xdg_toplevel_drag_v1_destroy(toplevelDrag);
+ toplevelDrag = nullptr;
+ }
+
+ Platform::AtomicStore(&DragOverFlag, 1);
+
+ if (wrappedDataSource != nullptr)
+ wl_proxy_wrapper_destroy(wrappedDataSource);
+
+ if (SelectionOffer != nullptr)
+ {
+ wl_data_offer_destroy(SelectionOffer);
+ SelectionOffer = nullptr;
+ }
+
+ // We can't release the queue immediately due to some resources being still used for a while
+ /*if (WaylandQueue != nullptr)
+ {
+ wl_event_queue_destroy(WaylandQueue);
+ WaylandQueue = nullptr;
+ }*/
+
+ return false;
+ }
+ };
+}
+
+// X11
+Delegate LinuxPlatform::xEventReceived;
+
+namespace X11Impl
+{
+ Window* DraggedWindow = nullptr;
+
+ struct Property
+ {
+ unsigned char* data;
+ int format, nitems;
+ X11::Atom type;
+ };
+
+ X11::Display* xDisplay = nullptr;
+ X11::XIM IM = nullptr;
+ X11::XIC IC = nullptr;
+ X11::Atom xAtomDeleteWindow;
+ X11::Atom xAtomXdndEnter;
+ X11::Atom xAtomXdndPosition;
+ X11::Atom xAtomXdndLeave;
+ X11::Atom xAtomXdndDrop;
+ X11::Atom xAtomXdndActionCopy;
+ X11::Atom xAtomXdndStatus;
+ X11::Atom xAtomXdndSelection;
+ X11::Atom xAtomXdndFinished;
+ X11::Atom xAtomXdndAware;
+ X11::Atom xAtomWmState;
+ X11::Atom xAtomWmStateHidden;
+ X11::Atom xAtomWmStateMaxVert;
+ X11::Atom xAtomWmStateMaxHorz;
+ X11::Atom xAtomWmWindowOpacity;
+ X11::Atom xAtomWmName;
+ X11::Atom xAtomAtom;
+ X11::Atom xAtomClipboard;
+ X11::Atom xAtomPrimary;
+ X11::Atom xAtomTargets;
+ X11::Atom xAtomText;
+ X11::Atom xAtomString;
+ X11::Atom xAtomUTF8String;
+ X11::Atom xAtomFlaxRaw;
+ X11::Atom xAtomUriList;
+ X11::Atom xAtomXselData;
+
+ X11::Atom xDnDRequested = 0;
+ X11::Window xDndSourceWindow = 0;
+ DragDropEffect xDndResult;
+ Float2 xDndPos;
+ int32 xDnDVersion = 0;
+ int32 XFixesSelectionNotifyEvent = 0;
+
+ StringAnsi ClipboardText;
+ Array ClipboardData;
+ Array ClipboardFiles;
+
+ void ClipboardGetText(String& result, X11::Atom source, X11::Atom atom, X11::Window window)
+ {
+ X11::Window selectionOwner = X11::XGetSelectionOwner(xDisplay, source);
+ if (selectionOwner == 0)
+ {
+ // No copy owner
+ return;
+ }
+ if (selectionOwner == window)
+ {
+ // Copy/paste from self
+ result.Set(ClipboardText.Get(), ClipboardText.Length());
+ return;
+ }
+
+ // Send event to get data from the owner
+ int format;
+ unsigned long N, size;
+ char* data;
+ X11::Atom target;
+ X11::XEvent event;
+ X11::XConvertSelection(xDisplay, xAtomClipboard, atom, xAtomXselData, window, CurrentTime);
+ X11::XSync(xDisplay, 0);
+ if (X11::XCheckTypedEvent(xDisplay, SelectionNotify, &event))
+ {
+ if (event.xselection.selection != xAtomClipboard)
+ return;
+ if (event.xselection.property)
+ {
+ X11::XGetWindowProperty(event.xselection.display, event.xselection.requestor, event.xselection.property, 0L, (~0L), 0, AnyPropertyType, &target, &format, &size, &N, (unsigned char**)&data);
+ if (target == xAtomUTF8String || target == xAtomString)
+ {
+ result.Set(data, size);
+ X11::XFree(data);
+ }
+ X11::XDeleteProperty(event.xselection.display, event.xselection.requestor, event.xselection.property);
+ }
+ }
+ }
+
+ void ClipboardGetData(Array& result, X11::Atom source, X11::Atom atom, X11::Window window)
+ {
+ X11::Window selectionOwner = X11::XGetSelectionOwner(xDisplay, source);
+ if (selectionOwner == 0)
+ {
+ // No copy owner
+ return;
+ }
+ if (selectionOwner == window)
+ {
+ // Copy/paste from self
+ result.Set(ClipboardData.Get(), ClipboardData.Count());
+ return;
+ }
+
+ // Send event to get data from the owner
+ int format;
+ unsigned long N, size;
+ byte* data;
+ X11::Atom target;
+ X11::XEvent event;
+ X11::XConvertSelection(xDisplay, xAtomClipboard, atom, xAtomXselData, window, CurrentTime);
+ X11::XSync(xDisplay, 0);
+ if (X11::XCheckTypedEvent(xDisplay, SelectionNotify, &event))
+ {
+ if (event.xselection.selection != xAtomClipboard)
+ return;
+ if (event.xselection.property)
+ {
+ X11::XGetWindowProperty(event.xselection.display, event.xselection.requestor, event.xselection.property, 0L,(~0L), 0, AnyPropertyType, &target, &format, &size, &N, (byte**)&data);
+ if (target == atom)
+ {
+ result.Set(data, size);
+ X11::XFree(data);
+ }
+ X11::XDeleteProperty(event.xselection.display, event.xselection.requestor, event.xselection.property);
+ }
+ }
+ }
+
+ void ClipboardGetFiles(Array& result, X11::Atom source, X11::Atom atom, X11::Window window)
+ {
+ X11::Window selectionOwner = X11::XGetSelectionOwner(xDisplay, source);
+ if (selectionOwner == 0)
+ {
+ // No copy owner
+ return;
+ }
+ if (selectionOwner == window)
+ {
+ // Copy/paste from self
+ result.Clear();
+ for (auto file : ClipboardFiles)
+ result.Add(String(file));
+ return;
+ }
+
+ // Send event to get data from the owner
+ int format;
+ unsigned long N, size;
+ char* data;
+ X11::Atom target;
+ X11::XEvent event;
+ X11::XConvertSelection(xDisplay, xAtomClipboard, atom, xAtomXselData, window, CurrentTime);
+ X11::XSync(xDisplay, 0);
+ if (X11::XCheckTypedEvent(xDisplay, SelectionNotify, &event))
+ {
+ if (event.xselection.selection != xAtomClipboard)
+ return;
+ if (event.xselection.property)
+ {
+ X11::XGetWindowProperty(event.xselection.display, event.xselection.requestor, event.xselection.property, 0L, (~0L), 0, AnyPropertyType, &target, &format, &size, &N, (unsigned char**)&data);
+ if (target == atom)
+ {
+ String filesString(StringAnsi(data, size));
+ filesString.Split('\n', result);
+ for (auto& file : result)
+ {
+ if (file.StartsWith(TEXT("file://")))
+ file = file.Substring(7);
+ file = file.TrimTrailing(); // Trim '\r'
+ }
+ X11::XFree(data);
+ }
+ X11::XDeleteProperty(event.xselection.display, event.xselection.requestor, event.xselection.property);
+ }
+ }
+ }
+
+ Property ReadProperty(X11::Display* display, X11::Window window, X11::Atom property)
+ {
+ X11::Atom readType = 0;
+ int readFormat = 0;
+ unsigned long nitems = 0;
+ unsigned long readBytes = 0;
+ unsigned char* result = nullptr;
+ int bytesCount = 1024;
+ if (property != 0)
+ {
+ do
+ {
+ if (result != nullptr)
+ X11::XFree(result);
+ XGetWindowProperty(display, window, property, 0, bytesCount, 0, AnyPropertyType, &readType, &readFormat, &nitems, &readBytes, &result);
+ bytesCount *= 2;
+ } while (readBytes != 0);
+ }
+ Property p = { result, readFormat, (int)nitems, readType };
+ return p;
+ }
+
+ static StringAnsi GetAtomName(X11::Display* display, X11::Atom atom)
+ {
+ char* atomNamePtr = X11::XGetAtomName(display, atom);
+ StringAnsi atomName = StringAnsi(atomNamePtr);
+ X11::XFree(atomNamePtr);
+ return atomName;
+ }
+
+ static X11::Atom SelectTargetFromList(X11::Display* display, const char* targetType, X11::Atom* list, int count)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ X11::Atom atom = list[i];
+ if (atom != 0 && GetAtomName(display, atom) == targetType)
+ return atom;
+ }
+ return 0;
+ }
+
+ static X11::Atom SelectTargetFromAtoms(X11::Display* display, const char* targetType, X11::Atom t1, X11::Atom t2, X11::Atom t3)
+ {
+ if (t1 != 0 && GetAtomName(display, t1) == targetType)
+ return t1;
+ if (t2 != 0 && GetAtomName(display, t2) == targetType)
+ return t2;
+ if (t3 != 0 && GetAtomName(display, t3) == targetType)
+ return t3;
+ return 0;
+ }
+
+ static X11::Window FindAppWindow(X11::Display* display, X11::Window w)
+ {
+ int nprops, i = 0;
+ X11::Atom* a;
+ if (w == 0)
+ return 0;
+ a = X11::XListProperties(display, w, &nprops);
+ for (i = 0; i < nprops; i++)
+ {
+ if (a[i] == xAtomXdndAware)
+ break;
+ }
+ if (nprops)
+ X11::XFree(a);
+ if (i != nprops)
+ return w;
+ X11::Window child, wtmp;
+ int tmp;
+ unsigned int utmp;
+ X11::XQueryPointer(display, w, &wtmp, &child, &tmp, &tmp, &tmp, &tmp, &utmp);
+ return FindAppWindow(display, child);
+ }
+
+ static Float2 GetX11MousePosition()
+ {
+ if (!xDisplay)
+ return Float2::Zero;
+ int32 x = 0, y = 0;
+ uint32 screenCount = (uint32)X11::XScreenCount(xDisplay);
+ for (uint32 i = 0; i < screenCount; i++)
+ {
+ X11::Window outRoot, outChild;
+ int32 childX, childY;
+ uint32 mask;
+ if (X11::XQueryPointer(xDisplay, X11::XRootWindow(xDisplay, i), &outRoot, &outChild, &x, &y, &childX, &childY, &mask))
+ break;
+ }
+ return Float2((float)x, (float)y);
+ }
+}
+
+DragDropEffect Window::DoDragDrop(const StringView& data)
+{
+ if (CommandLine::Options.Headless.IsTrue())
+ return DragDropEffect::None;
+
+ if (SDLPlatform::UsesWayland())
+ return DoDragDropWayland(data);
+ else
+ return DoDragDropX11(data);
+}
+
+DragDropEffect Window::DoDragDropWayland(const StringView& data, Window* dragSourceWindow, Float2 dragOffset)
+{
+ // HACK: For drag-and-drop, we need to run another event queue in a separate thread to avoid racing issues
+ // while SDL is dispatching the main Wayland event queue when receiving the data offer from us.
+
+ Engine::OnDraw();
+
+ if (WaylandImpl::DraggingActive)
+ LOG(Fatal, "Previous drag and drop operation was not finished");
+
+ // Read the latest serial code from mouse event, and check if we are still holding the mouse before committing
+ auto dragSerial = Platform::AtomicRead(&WaylandImpl::Serial);
+ SDLPlatform::Tick(); // Handle events to check for serial changes
+
+ if (!Input::Mouse->GetButton(MouseButton::Left) ||
+ dragSerial == 0 ||
+ dragSerial != Platform::AtomicRead(&WaylandImpl::Serial))
+ {
+ // The mouse up event was ignored earlier, release the button now
+ Input::Mouse->OnMouseUp(Platform::GetMousePosition(), MouseButton::Left, this);
+
+ return DragDropEffect::None;
+ }
+
+ WaylandImpl::DraggingActive = true;
+ WaylandImpl::DraggingData = StringView(data.Get(), data.Length());
+ WaylandImpl::DragOverFlag = 0;
+
+ auto task = New();
+ task->Window = this;
+ task->DragSourceWindow = dragSourceWindow; // Needs to be the parent window when dragging a tab to window
+ task->DragOffset = dragOffset;
+ task->DragSerial = dragSerial;
+ Task::StartNew(task);
+
+ while (Platform::AtomicRead(&task->StartFlag) == 0)
+ {
+ SDLPlatform::Tick();
+ Platform::Sleep(1);
+ }
+
+ while (Platform::AtomicRead(&WaylandImpl::DragOverFlag) == 0)
+ {
+ SDLPlatform::Tick();
+ Engine::OnUpdate(); // For docking updates
+ Engine::OnDraw();
+
+ // The window needs to be finished showing up before we can start dragging it
+ if (IsVisible() && Platform::AtomicRead(&task->WaitFlag) == 0)
+ Platform::AtomicStore(&task->WaitFlag, 1);
+
+ if (!WaylandImpl::DraggingWindow && !Input::Mouse->GetButton(MouseButton::Left))
+ {
+ // Abort in case the dragging was interrupted before receiving any data offers
+ Platform::AtomicStore(&task->ExitFlag, 1);
+ break;
+ }
+
+ Platform::Sleep(1);
+ }
+
+ // The mouse up event was ignored earlier, release the button now
+ Input::Mouse->OnMouseUp(Platform::GetMousePosition(), MouseButton::Left, this);
+
+ Platform::AtomicStore(&task->ExitFlag, 1);
+ while (task->GetState() != TaskState::Finished)
+ {
+ SDLPlatform::Tick();
+ Platform::Sleep(1);
+ }
+
+ WaylandImpl::DraggingActive = false;
+ WaylandImpl::DraggingData = nullptr;
+
+ return DragDropEffect::None;
+}
+
+DragDropEffect Window::DoDragDropX11(const StringView& data)
+{
+ using namespace X11Impl;
+ auto cursorWrong = X11::XCreateFontCursor(xDisplay, 54);
+ auto cursorTransient = X11::XCreateFontCursor(xDisplay, 24);
+ auto cursorGood = X11::XCreateFontCursor(xDisplay, 4);
+ Array> formats;
+ formats.Add(X11::XInternAtom(xDisplay, "text/plain", 0));
+ formats.Add(xAtomText);
+ formats.Add(xAtomString);
+ StringAnsi dataAnsi(data);
+ LinuxDropTextData dropData;
+ dropData.Text = data;
+#if !PLATFORM_SDL
+ X11::Window mainWindow = _window;
+#else
+ X11::Window mainWindow = reinterpret_cast(GetNativePtr());
+#endif
+
+ // Make sure SDL hasn't grabbed the pointer, and force ungrab it
+ XUngrabPointer(xDisplay, CurrentTime);
+ auto hintAutoCapture = SDL_GetHint(SDL_HINT_MOUSE_AUTO_CAPTURE);
+ SDL_SetHint(SDL_HINT_MOUSE_AUTO_CAPTURE, "0");
+
+ // Begin dragging
+ auto screen = X11::XDefaultScreen(xDisplay);
+ auto rootWindow = X11::XRootWindow(xDisplay, screen);
+
+ if (X11::XGrabPointer(xDisplay, mainWindow, 1, Button1MotionMask | ButtonReleaseMask, GrabModeAsync, GrabModeAsync, rootWindow, cursorWrong, CurrentTime) != GrabSuccess)
+ {
+ SDL_SetHint(SDL_HINT_MOUSE_AUTO_CAPTURE, hintAutoCapture);
+ return DragDropEffect::None;
+ }
+ X11::XSetSelectionOwner(xDisplay, xAtomXdndSelection, mainWindow, CurrentTime);
+
+ // Process events
+ X11::XEvent event;
+ enum Status
+ {
+ Unaware,
+ Unreceptive,
+ CanDrop,
+ };
+ int status = Unaware, previousVersion = -1;
+ X11::Window previousWindow = 0;
+ DragDropEffect result = DragDropEffect::None;
+ float lastDraw = Platform::GetTimeSeconds();
+ float startTime = lastDraw;
+ while (true)
+ {
+ X11::XNextEvent(xDisplay, &event);
+
+ if (event.type == SelectionClear)
+ break;
+ if (event.type == SelectionRequest)
+ {
+ // Extract the relavent data
+ X11::Window owner = event.xselectionrequest.owner;
+ X11::Atom selection = event.xselectionrequest.selection;
+ X11::Atom target = event.xselectionrequest.target;
+ X11::Atom property = event.xselectionrequest.property;
+ X11::Window requestor = event.xselectionrequest.requestor;
+ X11::Time timestamp = event.xselectionrequest.time;
+ X11::Display* disp = event.xselection.display;
+ X11::XEvent s;
+ s.xselection.type = SelectionNotify;
+ s.xselection.requestor = requestor;
+ s.xselection.selection = selection;
+ s.xselection.target = target;
+ s.xselection.property = 0;
+ s.xselection.time = timestamp;
+ if (target == xAtomTargets)
+ {
+ Array targets;
+ targets.Add(target);
+ targets.Add(X11::XInternAtom(disp, "MULTIPLE", 0));
+ targets.Add(formats.Get(), formats.Count());
+ X11::XChangeProperty(disp, requestor, property, xAtomAtom, 32, PropModeReplace, (unsigned char*)targets.Get(), targets.Count());
+ s.xselection.property = property;
+ }
+ else if (formats.Contains(target))
+ {
+ s.xselection.property = property;
+ X11::XChangeProperty(disp, requestor, property, target, 8, PropModeReplace, reinterpret_cast(dataAnsi.Get()), dataAnsi.Length());
+ }
+ X11::XSendEvent(event.xselection.display, event.xselectionrequest.requestor, 1, 0, &s);
+ }
+ else if (event.type == MotionNotify)
+ {
+ // Find window under mouse
+ auto window = X11Impl::FindAppWindow(xDisplay, rootWindow);
+ int fmt, version = -1;
+ X11::Atom atmp;
+ unsigned long nitems, bytesLeft;
+ unsigned char* data = nullptr;
+ if (window == previousWindow)
+ version = previousVersion;
+ else if(window == 0)
+ ;
+ else if (X11::XGetWindowProperty(xDisplay, window, xAtomXdndAware, 0, 2, 0, AnyPropertyType, &atmp, &fmt, &nitems, &bytesLeft, &data) != Success)
+ continue;
+ else if (data == 0)
+ continue;
+ else if (fmt != 32)
+ continue;
+ else if (nitems != 1)
+ continue;
+ else
+ version = data[0];
+ if (status == Unaware && version != -1)
+ status = Unreceptive;
+ else if(version == -1)
+ status = Unaware;
+ xDndPos = Float2((float)event.xmotion.x_root, (float)event.xmotion.y_root);
+
+ // Update mouse grab
+ if (status == Unaware)
+ X11::XChangeActivePointerGrab(xDisplay, Button1MotionMask | ButtonReleaseMask, cursorWrong, CurrentTime);
+ else if(status == Unreceptive)
+ X11::XChangeActivePointerGrab(xDisplay, Button1MotionMask | ButtonReleaseMask, cursorTransient, CurrentTime);
+ else
+ X11::XChangeActivePointerGrab(xDisplay, Button1MotionMask | ButtonReleaseMask, cursorGood, CurrentTime);
+
+ if (window != previousWindow && previousVersion != -1)
+ {
+ // Send drag left event
+ auto ww = WindowsManager::GetByNativePtr((void*)previousWindow);
+ if (ww)
+ {
+ ww->_dragOver = false;
+ ww->OnDragLeave();
+ }
+ else
+ {
+ X11::XClientMessageEvent m;
+ memset(&m, 0, sizeof(m));
+ m.type = ClientMessage;
+ m.display = event.xclient.display;
+ m.window = previousWindow;
+ m.message_type = xAtomXdndLeave;
+ m.format = 32;
+ m.data.l[0] = mainWindow;
+ m.data.l[1] = 0;
+ m.data.l[2] = 0;
+ m.data.l[3] = 0;
+ m.data.l[4] = 0;
+ X11::XSendEvent(xDisplay, previousWindow, 0, NoEventMask, (X11::XEvent*)&m);
+ X11::XFlush(xDisplay);
+ }
+ }
+
+ if (window != previousWindow && version != -1)
+ {
+ // Send drag enter event
+ auto ww = WindowsManager::GetByNativePtr((void*)window);
+ if (ww)
+ {
+ xDndPos = ww->ScreenToClient(X11Impl::GetX11MousePosition());
+ xDndResult = DragDropEffect::None;
+ ww->OnDragEnter(&dropData, xDndPos, xDndResult);
+ }
+ else
+ {
+ X11::XClientMessageEvent m;
+ memset(&m, 0, sizeof(m));
+ m.type = ClientMessage;
+ m.display = event.xclient.display;
+ m.window = window;
+ m.message_type = xAtomXdndEnter;
+ m.format = 32;
+ m.data.l[0] = mainWindow;
+ m.data.l[1] = Math::Min(5, version) << 24 | (formats.Count() > 3);
+ m.data.l[2] = formats.Count() > 0 ? formats[0] : 0;
+ m.data.l[3] = formats.Count() > 1 ? formats[1] : 0;
+ m.data.l[4] = formats.Count() > 2 ? formats[2] : 0;
+ X11::XSendEvent(xDisplay, window, 0, NoEventMask, (X11::XEvent*)&m);
+ X11::XFlush(xDisplay);
+ }
+ }
+
+ if (version != -1)
+ {
+ // Send position event
+ auto ww = WindowsManager::GetByNativePtr((void*)window);
+ if (ww)
+ {
+ xDndPos = ww->ScreenToClient(X11Impl::GetX11MousePosition());
+ ww->_dragOver = true;
+ xDndResult = DragDropEffect::None;
+ ww->OnDragOver(&dropData, xDndPos, xDndResult);
+ status = CanDrop;
+ }
+ else
+ {
+ int x, y, tmp;
+ unsigned int utmp;
+ X11::Window wtmp;
+ X11::XQueryPointer(xDisplay, window, &wtmp, &wtmp, &tmp, &tmp, &x, &y, &utmp);
+ X11::XClientMessageEvent m;
+ memset(&m, 0, sizeof(m));
+ m.type = ClientMessage;
+ m.display = event.xclient.display;
+ m.window = window;
+ m.message_type = xAtomXdndPosition;
+ m.format = 32;
+ m.data.l[0] = mainWindow;
+ m.data.l[1] = 0;
+ m.data.l[2] = (x << 16) | y;
+ m.data.l[3] = CurrentTime;
+ m.data.l[4] = xAtomXdndActionCopy;
+ X11::XSendEvent(xDisplay, window, 0, NoEventMask, (X11::XEvent*)&m);
+ X11::XFlush(xDisplay);
+ }
+ }
+
+ previousWindow = window;
+ previousVersion = version;
+ }
+ else if (event.type == ClientMessage && event.xclient.message_type == xAtomXdndStatus)
+ {
+ if ((event.xclient.data.l[1]&1) && status != Unaware)
+ status = CanDrop;
+ if (!(event.xclient.data.l[1]&1) && status != Unaware)
+ status = Unreceptive;
+ }
+ else if (event.type == ButtonRelease && event.xbutton.button == Button1)
+ {
+ if (status == CanDrop)
+ {
+ // Send drop event
+ auto ww = WindowsManager::GetByNativePtr((void*)previousWindow);
+ if (ww)
+ {
+ xDndPos = ww->ScreenToClient(X11Impl::GetX11MousePosition());
+ xDndResult = DragDropEffect::None;
+ ww->OnDragDrop(&dropData, xDndPos, xDndResult);
+ ww->Focus();
+ result = xDndResult;
+ }
+ else
+ {
+ X11::XClientMessageEvent m;
+ memset(&m, 0, sizeof(m));
+ m.type = ClientMessage;
+ m.display = event.xclient.display;
+ m.window = previousWindow;
+ m.message_type = xAtomXdndDrop;
+ m.format = 32;
+ m.data.l[0] = mainWindow;
+ m.data.l[1] = 0;
+ m.data.l[2] = CurrentTime;
+ m.data.l[3] = 0;
+ m.data.l[4] = 0;
+ X11::XSendEvent(xDisplay, previousWindow, 0, NoEventMask, (X11::XEvent*)&m);
+ X11::XFlush(xDisplay);
+ result = DragDropEffect::Copy;
+ }
+ }
+ break;
+ }
+
+ // Redraw
+ const float time = Platform::GetTimeSeconds();
+ if (time - lastDraw >= 1.0f / 20.0f)
+ {
+ lastDraw = time;
+
+ Engine::OnDraw();
+ }
+
+ // Prevent dead-loop
+ if (time - startTime >= 10.0f)
+ {
+ LOG(Warning, "DoDragDrop timed out after 10 seconds.");
+ break;
+ }
+ }
+
+ // Drag end
+ if (previousWindow != 0 && previousVersion != -1)
+ {
+ // Send drag left event
+ auto ww = WindowsManager::GetByNativePtr((void*)previousWindow);
+ if (ww)
+ {
+ ww->_dragOver = false;
+ ww->OnDragLeave();
+ }
+ else
+ {
+ X11::XClientMessageEvent m;
+ memset(&m, 0, sizeof(m));
+ m.type = ClientMessage;
+ m.display = event.xclient.display;
+ m.window = previousWindow;
+ m.message_type = xAtomXdndLeave;
+ m.format = 32;
+ m.data.l[0] = mainWindow;
+ m.data.l[1] = 0;
+ m.data.l[2] = 0;
+ m.data.l[3] = 0;
+ m.data.l[4] = 0;
+ X11::XSendEvent(xDisplay, previousWindow, 0, NoEventMask, (X11::XEvent*)&m);
+ X11::XFlush(xDisplay);
+ }
+ }
+
+ // End grabbing
+ X11::XChangeActivePointerGrab(xDisplay, Button1MotionMask | ButtonReleaseMask, 0, CurrentTime);
+ XUngrabPointer(xDisplay, CurrentTime);
+ X11::XFlush(xDisplay);
+
+ SDL_SetHint(SDL_HINT_MOUSE_AUTO_CAPTURE, hintAutoCapture);
+
+ return result;
+}
+
+void SDLPlatform::PreHandleEvents()
+{
+}
+
+void SDLPlatform::PostHandleEvents()
+{
+ // Handle window dragging release here
+ if (X11Impl::DraggedWindow != nullptr)
+ {
+ Float2 mousePosition;
+ auto buttons = SDL_GetGlobalMouseState(&mousePosition.X, &mousePosition.Y);
+ bool buttonReleased = (buttons & SDL_BUTTON_MASK(SDL_BUTTON_LEFT)) == 0;
+ if (buttonReleased)
+ {
+ // Send simulated mouse up event
+ SDL_Event buttonUpEvent { 0 };
+ buttonUpEvent.motion.type = SDL_EVENT_MOUSE_BUTTON_UP;
+ buttonUpEvent.button.down = false;
+ buttonUpEvent.motion.windowID = SDL_GetWindowID(X11Impl::DraggedWindow->GetSDLWindow());
+ buttonUpEvent.motion.timestamp = SDL_GetTicksNS();
+ buttonUpEvent.motion.state = SDL_BUTTON_LEFT;
+ buttonUpEvent.button.clicks = 1;
+ buttonUpEvent.motion.x = mousePosition.X;
+ buttonUpEvent.motion.y = mousePosition.Y;
+ X11Impl::DraggedWindow->HandleEvent(buttonUpEvent);
+ X11Impl::DraggedWindow = nullptr;
+ }
+ }
+}
+
+bool SDLWindow::HandleEventInternal(SDL_Event& event)
+{
+ switch (event.type)
+ {
+ case SDL_EVENT_WINDOW_MOVED:
+ {
+ if (SDLPlatform::UsesX11())
+ {
+ // X11 doesn't report any mouse events when mouse is over the caption area, send a simulated event instead...
+ Float2 mousePosition;
+ auto buttons = SDL_GetGlobalMouseState(&mousePosition.X, &mousePosition.Y);
+ if ((buttons & SDL_BUTTON_MASK(SDL_BUTTON_LEFT)) != 0 && X11Impl::DraggedWindow == nullptr)
+ {
+ // TODO: verify mouse position, window focus
+ bool result = false;
+ OnLeftButtonHit(WindowHitCodes::Caption, result);
+ if (result)
+ X11Impl::DraggedWindow = this;
+ }
+ }
+ break;
+ }
+ case SDL_EVENT_MOUSE_BUTTON_UP:
+ case SDL_EVENT_MOUSE_MOTION:
+ {
+ if (SDLPlatform::UsesWayland() && WaylandImpl::DraggingActive)
+ {
+ // Ignore mouse events in dragged window
+ return true;
+ }
+ break;
+ }
+ case SDL_EVENT_WINDOW_MOUSE_LEAVE:
+ {
+ OnDragLeave(); // Check for release of mouse button too?
+ break;
+ }
+ case SDL_EVENT_DROP_BEGIN:
+ case SDL_EVENT_DROP_POSITION:
+ case SDL_EVENT_DROP_FILE:
+ case SDL_EVENT_DROP_TEXT:
+ case SDL_EVENT_DROP_COMPLETE:
+ {
+ if (SDLPlatform::UsesWayland())
+ {
+ // HACK: We can't use Wayland listeners due to SDL also using them at the same time causes
+ // some of the events to drop and make it impossible to implement dragging on application side.
+ // We can get enough information through SDL_EVENT_DROP_* events to fill in the blanks for the
+ // drag and drop implementation.
+
+ auto dpiScale = GetDpiScale();
+ const Float2 mousePos = Float2(event.drop.x * dpiScale, event.drop.y * dpiScale);
+ DragDropEffect effect = DragDropEffect::None;
+ String text(event.drop.data);
+ LinuxDropTextData textData;
+ LinuxDropFilesData filesData;
+
+ if (WaylandImpl::DraggingActive && (event.type == SDL_EVENT_DROP_BEGIN || event.type == SDL_EVENT_DROP_POSITION))
+ {
+ // We don't have the window dragging data during these events...
+ text = WaylandImpl::DraggingData;
+ }
+ textData.Text = text;
+
+ if (event.type == SDL_EVENT_DROP_BEGIN)
+ {
+ // We don't know the type of dragged data at this point, so call the events for both types
+ if (!WaylandImpl::DraggingActive)
+ OnDragEnter(&filesData, mousePos, effect);
+ if (effect == DragDropEffect::None)
+ OnDragEnter(&textData, mousePos, effect);
+ }
+ else if (event.type == SDL_EVENT_DROP_POSITION)
+ {
+ Input::Mouse->OnMouseMove(ClientToScreen(mousePos), this);
+
+ // We don't know the type of dragged data at this point, so call the events for both types
+ if (!WaylandImpl::DraggingActive)
+ OnDragOver(&filesData, mousePos, effect);
+ if (effect == DragDropEffect::None)
+ OnDragOver(&textData, mousePos, effect);
+ }
+ else if (event.type == SDL_EVENT_DROP_FILE)
+ {
+ text.Split('\n', filesData.Files);
+ OnDragDrop(&filesData, mousePos, effect);
+ }
+ else if (event.type == SDL_EVENT_DROP_TEXT)
+ OnDragDrop(&textData, mousePos, effect);
+ else if (event.type == SDL_EVENT_DROP_COMPLETE)
+ OnDragLeave();
+
+ // TODO: Implement handling for feedback effect result (https://github.com/libsdl-org/SDL/issues/10448)
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ return false;
+}
+
+void SDLClipboard::Clear()
+{
+ SetText(StringView::Empty);
+}
+
+void SDLClipboard::SetText(const StringView& text)
+{
+ if (CommandLine::Options.Headless.IsTrue())
+ return;
+ auto mainWindow = Engine::MainWindow;
+ if (!mainWindow)
+ return;
+
+ if (X11Impl::xDisplay)
+ {
+ X11Impl::ClipboardText.Set(text.Get(), text.Length());
+ X11Impl::ClipboardData.Clear();
+ X11Impl::ClipboardFiles.Clear();
+
+ X11::Window window = (X11::Window)(mainWindow->GetNativePtr());
+ X11::XSetSelectionOwner(X11Impl::xDisplay, X11Impl::xAtomClipboard, window, CurrentTime); // CLIPBOARD
+ //X11::XSetSelectionOwner(xDisplay, xAtomPrimary, window, CurrentTime); // XA_PRIMARY
+ X11::XFlush(X11Impl::xDisplay);
+ }
+ else
+ {
+ SDL_SetClipboardText(StringAnsi(text).GetText());
+ }
+}
+
+void SDLClipboard::SetRawData(const Span& data)
+{
+ if (CommandLine::Options.Headless.IsTrue())
+ return;
+ auto mainWindow = Engine::MainWindow;
+ if (!mainWindow)
+ return;
+
+ if (X11Impl::xDisplay)
+ {
+ X11Impl::ClipboardData.Set(data.Get(), data.Length());
+ X11Impl::ClipboardText.Clear();
+ X11Impl::ClipboardFiles.Clear();
+
+ X11::Window window = (X11::Window)(mainWindow->GetNativePtr());
+ X11::XSetSelectionOwner(X11Impl::xDisplay, X11Impl::xAtomClipboard, window, CurrentTime); // CLIPBOARD
+ //X11::XSetSelectionOwner(xDisplay, xAtomPrimary, window, CurrentTime); // XA_PRIMARY
+ X11::XFlush(X11Impl::xDisplay);
+ }
+ else
+ {
+ if (data.Length() == 0)
+ return;
+
+ const char* uriList = "flaxengine/raw";
+ Array* buffer = New>(data.Get(), data.Length());
+
+ SDL_SetClipboardData([](void* userdata, const char* mime_type, size_t* size) -> const void*
+ {
+ // Clipboard data is requested
+ Array* buffer = static_cast*>(userdata);
+ if (StringUtils::Compare(mime_type, "flaxengine/raw") == 0)
+ {
+ *size = buffer->Count();
+ return buffer->Get();
+ }
+ *size = 0;
+ return nullptr;
+ },
+ [](void* userdata)
+ {
+ // Cleanup previous clipboard data
+ Array* buffer = static_cast*>(userdata);
+ Delete(buffer);
+ }, buffer, &uriList, 1);
+ }
+}
+
+void SDLClipboard::SetFiles(const Array& files)
+{
+ if (CommandLine::Options.Headless.IsTrue())
+ return;
+ auto mainWindow = Engine::MainWindow;
+ if (!mainWindow)
+ return;
+
+ if (X11Impl::xDisplay)
+ {
+ X11Impl::ClipboardFiles.Clear();
+ for (auto file : files)
+ X11Impl::ClipboardFiles.Add(StringAnsi(file));
+ X11Impl::ClipboardText.Clear();
+ X11Impl::ClipboardData.Clear();
+
+ X11::Window window = (X11::Window)(mainWindow->GetNativePtr());
+ X11::XSetSelectionOwner(X11Impl::xDisplay, X11Impl::xAtomClipboard, window, CurrentTime); // CLIPBOARD
+ //X11::XSetSelectionOwner(xDisplay, xAtomPrimary, window, CurrentTime); // XA_PRIMARY
+ X11::XFlush(X11Impl::xDisplay);
+ }
+ else
+ {
+ if (files.Count() == 0)
+ return;
+
+ const char* uriList = "text/uri-list";
+ StringAnsi* buffer = New();
+ for (auto file : files)
+ {
+ buffer->Append("file://");
+ buffer->Append(StringAnsi(file));
+ buffer->Append("\n");
+ }
+
+ SDL_SetClipboardData([](void* userdata, const char* mime_type, size_t* size) -> const void*
+ {
+ // Clipboard data is requested
+ StringAnsi* buffer = (StringAnsi*)userdata;
+ if (StringUtils::Compare(mime_type, "text/uri-list") == 0)
+ {
+ *size = buffer->Length();
+ return buffer->Get();
+ }
+ *size = 0;
+ return nullptr;
+ },
+ [](void* userdata)
+ {
+ // Cleanup previous clipboard data
+ StringAnsi* buffer = (StringAnsi*)userdata;
+ Delete(buffer);
+ }, buffer, &uriList, 1);
+ }
+}
+
+String SDLClipboard::GetText()
+{
+ if (CommandLine::Options.Headless.IsTrue())
+ return String::Empty;
+ String result;
+ auto mainWindow = Engine::MainWindow;
+ if (!mainWindow)
+ return result;
+ if (X11Impl::xDisplay)
+ {
+ X11::Window window = reinterpret_cast(mainWindow->GetNativePtr());
+ X11Impl::ClipboardGetText(result, X11Impl::xAtomClipboard, X11Impl::xAtomUTF8String, window);
+ if (result.HasChars())
+ return result;
+ X11Impl::ClipboardGetText(result, X11Impl::xAtomClipboard, X11Impl::xAtomString, window);
+ if (result.HasChars())
+ return result;
+ X11Impl::ClipboardGetText(result, X11Impl::xAtomPrimary, X11Impl::xAtomUTF8String, window);
+ if (result.HasChars())
+ return result;
+ X11Impl::ClipboardGetText(result, X11Impl::xAtomPrimary, X11Impl::xAtomString, window);
+ return result;
+ }
+ else
+ {
+ return String(SDL_GetClipboardText());
+ }
+}
+
+Array SDLClipboard::GetRawData()
+{
+ auto mainWindow = Engine::MainWindow;
+ if (!mainWindow)
+ return Array();
+
+ if (X11Impl::xDisplay)
+ {
+ X11::Window window = reinterpret_cast(mainWindow->GetNativePtr());
+ Array array;
+ X11Impl::ClipboardGetData(array, X11Impl::xAtomClipboard, X11Impl::xAtomFlaxRaw, window);
+ return array;
+ }
+ else
+ {
+ size_t length = 0;
+ byte* data = static_cast(SDL_GetClipboardData("flaxengine/raw", &length));
+ Array rawData(data, length);
+ return rawData;
+ }
+}
+
+Array SDLClipboard::GetFiles()
+{
+ auto mainWindow = Engine::MainWindow;
+ if (!mainWindow)
+ return Array();
+
+ if (X11Impl::xDisplay)
+ {
+ X11::Window window = reinterpret_cast(mainWindow->GetNativePtr());
+ Array array;
+ X11Impl::ClipboardGetFiles(array, X11Impl::xAtomClipboard, X11Impl::xAtomUriList, window);
+ if (array.Count() > 0)
+ return array;
+ return Array();
+ }
+ else
+ {
+ size_t length = 0;
+ const char* data = static_cast(SDL_GetClipboardData("text/uri-list", &length));
+ String stringBuffer = String(StringAnsi(data, length));
+ Array files;
+ stringBuffer.Split('\n', files);
+ for (auto& file : files)
+ {
+ if (file.StartsWith(TEXT("file://")))
+ file = file.Substring(7);
+ file = file.TrimTrailing(); // Trim '\r'
+ }
+ return files;
+ }
+}
+
+bool SDLCALL SDLPlatform::X11EventHook(void* userdata, _XEvent* xevent)
+{
+ using namespace X11Impl;
+ const X11::XEvent& event = *(X11::XEvent*)xevent;
+ Window* window;
+
+ // External event handling
+ xEventReceived(xevent);
+
+ if (event.type == ClientMessage)
+ {
+ if ((uint32)event.xclient.message_type == (uint32)xAtomXdndEnter)
+ {
+ // Drag&drop enter
+ X11::Window source = event.xclient.data.l[0];
+ xDnDVersion = (int32)(event.xclient.data.l[1] >> 24);
+ const char* targetTypeFiles = "text/uri-list";
+ if (event.xclient.data.l[1] & 1)
+ {
+ Property p = X11Impl::ReadProperty(xDisplay, source, XInternAtom(xDisplay, "XdndTypeList", 0));
+ xDnDRequested = X11Impl::SelectTargetFromList(xDisplay, targetTypeFiles, (X11::Atom*)p.data, p.nitems);
+ X11::XFree(p.data);
+ }
+ else
+ {
+ xDnDRequested = X11Impl::SelectTargetFromAtoms(xDisplay, targetTypeFiles, event.xclient.data.l[2], event.xclient.data.l[3], event.xclient.data.l[4]);
+ }
+ return false;
+ }
+ else if ((uint32)event.xclient.message_type == (uint32)xAtomXdndPosition)
+ {
+ // Drag&drop move
+ X11::XClientMessageEvent m;
+ memset(&m, 0, sizeof(m));
+ m.type = ClientMessage;
+ m.display = event.xclient.display;
+ m.window = event.xclient.data.l[0];
+ m.message_type = xAtomXdndStatus;
+ m.format = 32;
+ m.data.l[0] = event.xany.window;
+ m.data.l[1] = (xDnDRequested != 0);
+ m.data.l[2] = 0;
+ m.data.l[3] = 0;
+ m.data.l[4] = xAtomXdndActionCopy;
+ X11::XSendEvent(xDisplay, event.xclient.data.l[0], 0, NoEventMask, (X11::XEvent*)&m);
+ X11::XFlush(xDisplay);
+ xDndPos = Float2((float)(event.xclient.data.l[2] >> 16), (float)(event.xclient.data.l[2] & 0xffff));
+ window = WindowsManager::GetByNativePtr((void*)event.xany.window);
+ if (window)
+ {
+ xDndPos = window->ScreenToClient(xDndPos);
+ LinuxDropFilesData dropData;
+ xDndResult = DragDropEffect::None;
+ if (window->_dragOver)
+ {
+ window->OnDragOver(&dropData, xDndPos, xDndResult);
+ }
+ else
+ {
+ window->_dragOver = true;
+ window->OnDragEnter(&dropData, xDndPos, xDndResult);
+ }
+ }
+ return false;
+ }
+ else if ((uint32)event.xclient.message_type == (uint32)xAtomXdndLeave)
+ {
+ window = WindowsManager::GetByNativePtr((void*)event.xany.window);
+ if (window && window->_dragOver)
+ {
+ window->_dragOver = false;
+ window->OnDragLeave();
+ }
+ return false;
+ }
+ else if ((uint32)event.xclient.message_type == (uint32)xAtomXdndDrop)
+ {
+ if (xDnDRequested != 0)
+ {
+ xDndSourceWindow = event.xclient.data.l[0];
+ XConvertSelection(xDisplay, xAtomXdndSelection, xDnDRequested, xAtomPrimary,
+ event.xany.window, xDnDVersion >= 1 ? event.xclient.data.l[2] : CurrentTime);
+ X11::XFlush(xDisplay);
+ }
+ else
+ {
+ X11::XClientMessageEvent m;
+ memset(&m, 0, sizeof(m));
+ m.type = ClientMessage;
+ m.display = event.xclient.display;
+ m.window = event.xclient.data.l[0];
+ m.message_type = xAtomXdndFinished;
+ m.format = 32;
+ m.data.l[0] = event.xany.window;
+ m.data.l[1] = 0;
+ m.data.l[2] = 0;
+ X11::XSendEvent(xDisplay, event.xclient.data.l[0], 0, NoEventMask, (X11::XEvent*)&m);
+ }
+ return false;
+ }
+ }
+ else if (event.type == SelectionNotify)
+ {
+ if (event.xselection.target == xDnDRequested)
+ {
+ // Drag&drop
+ window = WindowsManager::GetByNativePtr((void*)event.xany.window);
+ if (window)
+ {
+ Property p = X11Impl::ReadProperty(xDisplay, event.xany.window, xAtomPrimary);
+ if (xDndResult != DragDropEffect::None)
+ {
+ LinuxDropFilesData dropData;
+ const String filesList((const char*)p.data);
+ filesList.Split('\n', dropData.Files);
+ for (auto& e : dropData.Files)
+ {
+ e.Replace(TEXT("file://"), TEXT(""));
+ e.Replace(TEXT("%20"), TEXT(" "));
+ e = e.TrimTrailing();
+ }
+ xDndResult = DragDropEffect::None;
+ window->OnDragDrop(&dropData, xDndPos, xDndResult);
+ }
+ }
+ X11::XClientMessageEvent m;
+ memset(&m, 0, sizeof(m));
+ m.type = ClientMessage;
+ m.display = xDisplay;
+ m.window = xDndSourceWindow;
+ m.message_type = xAtomXdndFinished;
+ m.format = 32;
+ m.data.l[0] = event.xany.window;
+ m.data.l[1] = 1;
+ m.data.l[2] = xAtomXdndActionCopy;
+ XSendEvent(xDisplay, xDndSourceWindow, 0, NoEventMask, (X11::XEvent*)&m);
+ return false;
+ }
+ return false;
+ }
+ else if (event.type == SelectionRequest)
+ {
+ // Clipboard request
+ if (event.xselectionrequest.selection != xAtomClipboard)
+ return false;
+
+ const X11::XSelectionRequestEvent* xsr = &event.xselectionrequest;
+ X11::XSelectionEvent ev = { 0 };
+ ev.type = SelectionNotify;
+ ev.display = xsr->display;
+ ev.requestor = xsr->requestor;
+ ev.selection = xsr->selection;
+ ev.time = xsr->time;
+ ev.target = xsr->target;
+ ev.property = xsr->property;
+
+ int result = 0;
+ if (ev.target == xAtomTargets)
+ {
+ // Request supported targets for clipboard content
+ Array> types(2);
+ types.Add(xAtomTargets);
+ if (X11Impl::ClipboardData.Count() > 0)
+ types.Add(xAtomFlaxRaw);
+ else if (X11Impl::ClipboardFiles.Count() > 0)
+ types.Add(xAtomUriList);
+ else
+ types.Add(xAtomUTF8String);
+ result = X11::XChangeProperty(xDisplay, ev.requestor, ev.property, xAtomAtom, 32, PropModeReplace, (unsigned char*)types.Get(), types.Count());
+ }
+ // Request target type
+ else if (ev.target == xAtomString || ev.target == xAtomText)
+ result = X11::XChangeProperty(xDisplay, ev.requestor, ev.property, xAtomString, 8, PropModeReplace, (unsigned char*)X11Impl::ClipboardText.Get(), X11Impl::ClipboardText.Length());
+ else if (ev.target == xAtomUTF8String)
+ result = X11::XChangeProperty(xDisplay, ev.requestor, ev.property, xAtomUTF8String, 8, PropModeReplace, (unsigned char*)X11Impl::ClipboardText.Get(), X11Impl::ClipboardText.Length());
+ else if (ev.target == xAtomFlaxRaw)
+ result = X11::XChangeProperty(xDisplay, ev.requestor, ev.property, xAtomFlaxRaw, 8, PropModeReplace, X11Impl::ClipboardData.Get(), X11Impl::ClipboardData.Count());
+ else if (ev.target == xAtomUriList)
+ {
+ StringAnsi fileString;
+ for (auto file : X11Impl::ClipboardFiles)
+ {
+ fileString.Append("file://");
+ fileString.Append(file);
+ fileString.Append("\n");
+ }
+ result = X11::XChangeProperty(xDisplay, ev.requestor, ev.property, xAtomUriList, 8, PropModeReplace, (unsigned char*)fileString.Get(), fileString.Length());
+ }
+ else
+ ev.property = 0;
+ if ((result & 2) == 0)
+ X11::XSendEvent(xDisplay, ev.requestor, 0, 0, (X11::XEvent*)&ev);
+ return false;
+ }
+ else if (event.type == SelectionClear)
+ return false;
+ else if (event.type == XFixesSelectionNotifyEvent)
+ return false;
+ return true;
+}
+
+int X11ErrorHandler(X11::Display* display, X11::XErrorEvent* event)
+{
+ if (event->error_code == 5)
+ return 0; // BadAtom (invalid Atom parameter)
+ char buffer[256];
+ XGetErrorText(display, event->error_code, buffer, sizeof(buffer));
+ LOG(Error, "X11 Error: {0}", String(buffer));
+ return 0;
+}
+
+bool SDLPlatform::InitInternal()
+{
+ bool waylandRequested = (!CommandLine::Options.X11.GetValueOr(false) || CommandLine::Options.Wayland.IsTrue()) && StringAnsi(SDL_GetHint(SDL_HINT_VIDEO_DRIVER)) == "wayland";
+ if (!CommandLine::Options.Headless.IsTrue() && waylandRequested)
+ {
+ // Ignore in X11 session
+ if (!SDLImpl::WaylandDisplayEnv.IsEmpty())
+ {
+ WaylandImpl::WaylandDisplay = (wl_display*)SDL_GetPointerProperty(SDL_GetGlobalProperties(), SDL_PROP_GLOBAL_VIDEO_WAYLAND_WL_DISPLAY_POINTER, nullptr);
+ if (WaylandImpl::WaylandDisplay != nullptr)
+ {
+ // Tap into Wayland registry so we can start listening for events
+ wl_registry* registry = wl_display_get_registry(WaylandImpl::WaylandDisplay);
+ wl_registry_add_listener(registry, &WaylandImpl::RegistryListener, nullptr);
+ wl_display_roundtrip(WaylandImpl::WaylandDisplay);
+ }
+ }
+ }
+
+ return false;
+}
+
+bool SDLPlatform::InitX11(void* display)
+{
+ using namespace X11Impl;
+ if (xDisplay || WaylandImpl::WaylandDisplay)
+ return false;
+
+ // The Display instance must be the same one SDL uses internally
+ xDisplay = (X11::Display*)display;
+ SDL_SetX11EventHook((SDL_X11EventHook)&X11EventHook, nullptr);
+ X11::XSetErrorHandler(X11ErrorHandler);
+
+ xAtomDeleteWindow = X11::XInternAtom(xDisplay, "WM_DELETE_WINDOW", 0);
+ xAtomXdndEnter = X11::XInternAtom(xDisplay, "XdndEnter", 0);
+ xAtomXdndPosition = X11::XInternAtom(xDisplay, "XdndPosition", 0);
+ xAtomXdndLeave = X11::XInternAtom(xDisplay, "XdndLeave", 0);
+ xAtomXdndDrop = X11::XInternAtom(xDisplay, "XdndDrop", 0);
+ xAtomXdndActionCopy = X11::XInternAtom(xDisplay, "XdndActionCopy", 0);
+ xAtomXdndStatus = X11::XInternAtom(xDisplay, "XdndStatus", 0);
+ xAtomXdndSelection = X11::XInternAtom(xDisplay, "XdndSelection", 0);
+ xAtomXdndFinished = X11::XInternAtom(xDisplay, "XdndFinished", 0);
+ xAtomXdndAware = X11::XInternAtom(xDisplay, "XdndAware", 0);
+ xAtomWmStateHidden = X11::XInternAtom(xDisplay, "_NET_WM_STATE_HIDDEN", 0);
+ xAtomWmStateMaxHorz = X11::XInternAtom(xDisplay, "_NET_WM_STATE_MAXIMIZED_HORZ", 0);
+ xAtomWmStateMaxVert = X11::XInternAtom(xDisplay, "_NET_WM_STATE_MAXIMIZED_VERT", 0);
+ xAtomWmWindowOpacity = X11::XInternAtom(xDisplay, "_NET_WM_WINDOW_OPACITY", 0);
+ xAtomWmName = X11::XInternAtom(xDisplay, "_NET_WM_NAME", 0);
+ xAtomAtom = static_cast(4); // XA_ATOM
+ xAtomClipboard = X11::XInternAtom(xDisplay, "CLIPBOARD", 0);
+ xAtomPrimary = static_cast(1); // XA_PRIMARY
+ xAtomTargets = X11::XInternAtom(xDisplay, "TARGETS", 0);
+ xAtomText = X11::XInternAtom(xDisplay, "TEXT", 0);
+ xAtomString = static_cast(31); // XA_STRING
+ xAtomUTF8String = X11::XInternAtom(xDisplay, "UTF8_STRING", 1);
+ if (xAtomUTF8String == 0)
+ xAtomUTF8String = xAtomString;
+ xAtomFlaxRaw = X11::XInternAtom(xDisplay, "flaxengine/raw", 0);
+ xAtomUriList = X11::XInternAtom(xDisplay, "text/uri-list", 0);
+ xAtomXselData = X11::XInternAtom(xDisplay, "XSEL_DATA", 0);
+
+ // We need to override handling of the XFixes selection tracking events from SDL
+ auto screen = X11::XDefaultScreen(xDisplay);
+ auto rootWindow = X11::XRootWindow(xDisplay, screen);
+ int eventBase = 0, errorBase = 0;
+ if (X11::XFixesQueryExtension(xDisplay, &eventBase, &errorBase))
+ {
+ XFixesSelectionNotifyEvent = eventBase + XFixesSelectionNotify;
+ X11::XFixesSelectSelectionInput(xDisplay, rootWindow, xAtomClipboard, XFixesSetSelectionOwnerNotifyMask);
+ X11::XFixesSelectSelectionInput(xDisplay, rootWindow, xAtomPrimary, XFixesSetSelectionOwnerNotifyMask);
+ }
+
+ return false;
+}
+
+void* SDLPlatform::GetXDisplay()
+{
+ return X11Impl::xDisplay;
+}
+
+void SDLPlatform::SetHighDpiAwarenessEnabled(bool enable)
+{
+ base::SetHighDpiAwarenessEnabled(enable);
+}
+
+bool SDLPlatform::UsesWindows()
+{
+ return false;
+}
+
+bool SDLPlatform::UsesWayland()
+{
+ if (X11Impl::xDisplay == nullptr && WaylandImpl::WaylandDisplay == nullptr)
+ {
+ // In case the X11 display pointer has not been updated yet
+ return strcmp(SDL_GetCurrentVideoDriver(), "wayland") == 0;
+ }
+ return WaylandImpl::WaylandDisplay != nullptr;
+}
+
+bool SDLPlatform::UsesX11()
+{
+ if (X11Impl::xDisplay == nullptr && WaylandImpl::WaylandDisplay == nullptr)
+ {
+ // In case the X11 display pointer has not been updated yet
+ return strcmp(SDL_GetCurrentVideoDriver(), "x11") == 0;
+ }
+ return X11Impl::xDisplay != nullptr;
+}
+
+DragDropEffect SDLWindow::DoDragDrop(const StringView& data, const Float2& offset, Window* dragSourceWindow)
+{
+ if (SDLPlatform::UsesWayland())
+ {
+ Float2 dragOffset = offset;
+ if (SDLPlatform::SupportsNativeDecorations() && _settings.HasBorder && dragSourceWindow == this)
+ {
+ // Wayland includes the decorations in the client-space coordinates, adjust the offset for it.
+ // Assume the title decoration based on the current desktop environment...
+ float topOffset = 25.0f;
+ if (SDLImpl::XDGCurrentDesktop.Compare(String("KDE"), StringSearchCase::IgnoreCase) == 0)
+ topOffset = 25.0f;
+ else if (SDLImpl::XDGCurrentDesktop.Compare(String("GNOME"), StringSearchCase::IgnoreCase) == 0)
+ topOffset = 48.0f;
+ dragOffset += Float2(0.0f, topOffset);
+ }
+
+ // Show the window without changing focus
+ if (!_visible)
+ {
+ if (_showAfterFirstPaint)
+ {
+ if (RenderTask)
+ RenderTask->Enabled = true;
+ }
+ else
+ SDL_ShowWindow(_window);
+ }
+ // Only show the window if toplevel dragging is supported
+ if (WaylandImpl::DragManager != nullptr)
+ WindowBase::Show();
+ else
+ Hide();
+
+ WaylandImpl::DraggingWindow = true;
+ DoDragDropWayland(String(""), dragSourceWindow, dragOffset);
+ WaylandImpl::DraggingWindow = false;
+ }
+ else
+ Show();
+ return DragDropEffect::None;
+}
+
+DialogResult MessageBox::Show(Window* parent, const StringView& text, const StringView& caption, MessageBoxButtons buttons, MessageBoxIcon icon)
+{
+ StringAnsi textAnsi(text);
+ StringAnsi captionAnsi(caption);
+
+ SDL_MessageBoxData data;
+ SDL_MessageBoxButtonData dataButtons[3];
+ data.window = parent ? static_cast(parent)->_window : nullptr;
+ data.title = captionAnsi.GetText();
+ data.message = textAnsi.GetText();
+ data.colorScheme = nullptr;
+
+ switch (icon)
+ {
+ case MessageBoxIcon::Error:
+ case MessageBoxIcon::Hand:
+ case MessageBoxIcon::Stop:
+ data.flags |= SDL_MESSAGEBOX_ERROR;
+ break;
+ case MessageBoxIcon::Asterisk:
+ case MessageBoxIcon::Information:
+ case MessageBoxIcon::Question:
+ data.flags |= SDL_MESSAGEBOX_INFORMATION;
+ break;
+ case MessageBoxIcon::Exclamation:
+ case MessageBoxIcon::Warning:
+ data.flags |= SDL_MESSAGEBOX_WARNING;
+ break;
+ default:
+ break;
+ }
+
+ switch (buttons)
+ {
+ case MessageBoxButtons::AbortRetryIgnore:
+ dataButtons[0] =
+ {
+ SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT,
+ (int)DialogResult::Abort,
+ "Abort"
+ };
+ dataButtons[1] =
+ {
+ SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT,
+ (int)DialogResult::Retry,
+ "Retry"
+ };
+ dataButtons[2] =
+ {
+ 0,
+ (int)DialogResult::Ignore,
+ "Ignore"
+ };
+ data.numbuttons = 3;
+ break;
+ case MessageBoxButtons::OK:
+ dataButtons[0] =
+ {
+ SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT | SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT,
+ (int)DialogResult::OK,
+ "OK"
+ };
+ data.numbuttons = 1;
+ break;
+ case MessageBoxButtons::OKCancel:
+ dataButtons[0] =
+ {
+ SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT,
+ (int)DialogResult::OK,
+ "OK"
+ };
+ dataButtons[1] =
+ {
+ SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT,
+ (int)DialogResult::Cancel,
+ "Cancel"
+ };
+ data.numbuttons = 2;
+ break;
+ case MessageBoxButtons::RetryCancel:
+ dataButtons[0] =
+ {
+ SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT,
+ (int)DialogResult::Retry,
+ "Retry"
+ };
+ dataButtons[1] =
+ {
+ SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT,
+ (int)DialogResult::Cancel,
+ "Cancel"
+ };
+ data.numbuttons = 2;
+ break;
+ case MessageBoxButtons::YesNo:
+ dataButtons[0] =
+ {
+ SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT,
+ (int)DialogResult::Yes,
+ "Yes"
+ };
+ dataButtons[1] =
+ {
+ SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT,
+ (int)DialogResult::No,
+ "No"
+ };
+ data.numbuttons = 2;
+ break;
+ case MessageBoxButtons::YesNoCancel:
+ {
+ dataButtons[0] =
+ {
+ SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT,
+ (int)DialogResult::Yes,
+ "Yes"
+ };
+ dataButtons[1] =
+ {
+ 0,
+ (int)DialogResult::No,
+ "No"
+ };
+ dataButtons[2] =
+ {
+ SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT,
+ (int)DialogResult::Cancel,
+ "Cancel"
+ };
+ data.numbuttons = 3;
+ break;
+ }
+ default:
+ break;
+ }
+ data.buttons = dataButtons;
+
+ int result = -1;
+ if (!SDL_ShowMessageBox(&data, &result))
+ {
+#if PLATFORM_LINUX
+ // Fallback to native messagebox implementation in case some system fonts are missing
+ if (SDLPlatform::UsesX11())
+ {
+ LOG(Warning, "Failed to show SDL message box: {0}", String(SDL_GetError()));
+ return ShowFallback(parent, text, caption, buttons, icon);
+ }
+#endif
+ LOG(Error, "Failed to show SDL message box: {0}", String(SDL_GetError()));
+ return DialogResult::Abort;
+ }
+ if (result < 0)
+ return DialogResult::None;
+ return (DialogResult)result;
+}
+
+#endif
diff --git a/Source/Engine/Platform/SDL/SDLPlatform.Mac.cpp b/Source/Engine/Platform/SDL/SDLPlatform.Mac.cpp
new file mode 100644
index 000000000..5770b56d5
--- /dev/null
+++ b/Source/Engine/Platform/SDL/SDLPlatform.Mac.cpp
@@ -0,0 +1,82 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+#if PLATFORM_SDL && PLATFORM_MAC
+
+#include "SDLWindow.h"
+#include "Engine/Core/Log.h"
+#include "Engine/Core/Collections/Array.h"
+#include "Engine/Engine/CommandLine.h"
+#include "Engine/Engine/Engine.h"
+#include "Engine/Engine/Time.h"
+#include "Engine/Graphics/RenderTask.h"
+#include "Engine/Input/Input.h"
+#include "Engine/Input/Mouse.h"
+#include "Engine/Platform/IGuiData.h"
+#include "Engine/Platform/MessageBox.h"
+#include "Engine/Platform/Platform.h"
+#include "Engine/Platform/WindowsManager.h"
+#include "Engine/Platform/Base/DragDropHelper.h"
+#include "Engine/Platform/SDL/SDLClipboard.h"
+#include "Engine/Platform/Unix/UnixFile.h"
+#include "Engine/Profiler/ProfilerCPU.h"
+
+#include "Engine/Platform/Linux/IncludeX11.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+bool SDLPlatform::InitInternal()
+{
+ return false;
+}
+
+bool SDLPlatform::UsesWindows()
+{
+ return false;
+}
+
+bool SDLPlatform::UsesWayland()
+{
+ return false;
+}
+
+bool SDLPlatform::UsesX11()
+{
+ return false;
+}
+
+void SDLPlatform::PreHandleEvents()
+{
+}
+
+void SDLPlatform::PostHandleEvents()
+{
+}
+
+bool SDLWindow::HandleEventInternal(SDL_Event& event)
+{
+ return false;
+}
+
+void SDLPlatform::SetHighDpiAwarenessEnabled(bool enable)
+{
+ // TODO: This is now called before Platform::Init, ensure the scaling is changed accordingly during Platform::Init (see ApplePlatform::SetHighDpiAwarenessEnabled)
+}
+
+DragDropEffect SDLWindow::DoDragDrop(const StringView& data)
+{
+ return DragDropEffect::None;
+}
+
+DragDropEffect SDLWindow::DoDragDrop(const StringView& data, const Float2& offset, Window* dragSourceWindow)
+{
+ Show();
+ return DragDropEffect::None;
+}
+
+#endif
diff --git a/Source/Engine/Platform/SDL/SDLPlatform.Windows.cpp b/Source/Engine/Platform/SDL/SDLPlatform.Windows.cpp
new file mode 100644
index 000000000..ae841b1b4
--- /dev/null
+++ b/Source/Engine/Platform/SDL/SDLPlatform.Windows.cpp
@@ -0,0 +1,259 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+#if PLATFORM_SDL && PLATFORM_WINDOWS
+
+#include "SDLPlatform.h"
+#include "SDLInput.h"
+
+#include "Engine/Core/Collections/Array.h"
+#include "Engine/Platform/WindowsManager.h"
+#include "Engine/Platform/Win32/IncludeWindowsHeaders.h"
+#include "Engine/Input/Mouse.h"
+#include "Engine/Core/Log.h"
+#include "Engine/Engine/Engine.h"
+
+#include
+#include
+#include
+#include
+
+#if USE_EDITOR
+#include
+#endif
+
+namespace WinImpl
+{
+ Window* DraggedWindow;
+ Float2 DraggedWindowStartPosition = Float2::Zero;
+ Float2 DraggedWindowMousePosition = Float2::Zero;
+ Float2 DraggedWindowSize = Float2::Zero;
+}
+
+// The events for releasing the mouse during window dragging are missing, handle the mouse release event here
+bool SDLCALL SDLPlatform::EventMessageHook(void* userdata, MSG* msg)
+{
+ if (msg->message == WM_NCLBUTTONDOWN)
+ {
+ Window* window = WindowsManager::GetByNativePtr(msg->hwnd);
+ Float2 mousePosition(static_cast(static_cast(WINDOWS_GET_X_LPARAM(msg->lParam))), static_cast(static_cast(WINDOWS_GET_Y_LPARAM(msg->lParam))));
+
+ WinImpl::DraggedWindow = window;
+ WinImpl::DraggedWindowStartPosition = window->GetClientPosition();
+ WinImpl::DraggedWindowMousePosition = mousePosition - WinImpl::DraggedWindowStartPosition;
+ WinImpl::DraggedWindowSize = window->GetClientSize();
+
+ bool result = false;
+ WindowHitCodes hit = static_cast(msg->wParam);
+ window->OnHitTest(mousePosition, hit, result);
+ //if (result && hit != WindowHitCodes::Caption)
+ // return false;
+
+ if (hit == WindowHitCodes::Caption)
+ {
+ SDL_Event event{ 0 };
+ event.button.type = SDL_EVENT_MOUSE_BUTTON_DOWN;
+ event.button.down = true;
+ event.button.timestamp = SDL_GetTicksNS();
+ event.button.windowID = SDL_GetWindowID(window->GetSDLWindow());
+ event.button.button = SDL_BUTTON_LEFT;
+ event.button.clicks = 1;
+ event.button.x = WinImpl::DraggedWindowMousePosition.X;
+ event.button.y = WinImpl::DraggedWindowMousePosition.Y;
+
+ SDL_PushEvent(&event);
+ }
+ }
+ return true;
+}
+
+bool SDLPlatform::InitInternal()
+{
+ // Workaround required for handling window dragging events properly
+ SDL_SetWindowsMessageHook(&EventMessageHook, nullptr);
+
+ if (WindowsPlatform::Init())
+ return true;
+
+ return false;
+}
+
+bool SDLPlatform::EventFilterCallback(void* userdata, SDL_Event* event)
+{
+ Window* draggedWindow = *(Window**)userdata;
+ if (draggedWindow == nullptr)
+ return true;
+
+ // When the window is being dragged on Windows, the internal message loop is blocking
+ // the SDL event queue. We need to handle all relevant events in this event watch callback
+ // to ensure dragging related functionality doesn't break due to engine not getting updated.
+ // This also happens to fix the engine freezing during the dragging operation.
+
+ SDLWindow* window = SDLWindow::GetWindowFromEvent(*event);
+ if (event->type == SDL_EVENT_WINDOW_EXPOSED)
+ {
+ // The internal timer is sending exposed events every ~16ms
+ Engine::OnUpdate(); // For docking updates
+ Engine::OnDraw();
+ return false;
+ }
+ else if (event->type == SDL_EVENT_MOUSE_BUTTON_DOWN)
+ {
+ if (window)
+ {
+ bool result = false;
+ window->OnLeftButtonHit(WindowHitCodes::Caption, result);
+ //if (result)
+ // return false;
+ window->HandleEvent(*event);
+ }
+ return false;
+ }
+ else if (event->type == SDL_EVENT_WINDOW_MOVED)
+ {
+ if (window)
+ {
+ window->HandleEvent(*event);
+
+ Float2 windowSize = window->GetClientSize();
+ if (WinImpl::DraggedWindowSize != windowSize)
+ {
+ // The window size changed while dragging, most likely due to maximized window restoring back to previous size.
+ WinImpl::DraggedWindowMousePosition = WinImpl::DraggedWindowStartPosition + WinImpl::DraggedWindowMousePosition - window->GetClientPosition();
+ WinImpl::DraggedWindowStartPosition = window->GetClientPosition();
+ WinImpl::DraggedWindowSize = windowSize;
+ }
+ Float2 windowPosition = Float2(static_cast(event->window.data1), static_cast(event->window.data2));
+ Float2 mousePosition = WinImpl::DraggedWindowMousePosition;
+
+ // Generate mouse movement events while dragging the window around
+ SDL_Event mouseMovedEvent{ 0 };
+ mouseMovedEvent.motion.type = SDL_EVENT_MOUSE_MOTION;
+ mouseMovedEvent.motion.windowID = SDL_GetWindowID(WinImpl::DraggedWindow->GetSDLWindow());
+ mouseMovedEvent.motion.timestamp = SDL_GetTicksNS();
+ mouseMovedEvent.motion.state = SDL_BUTTON_LEFT;
+ mouseMovedEvent.motion.x = mousePosition.X;
+ mouseMovedEvent.motion.y = mousePosition.Y;
+ window->HandleEvent(mouseMovedEvent);
+ }
+ return false;
+ }
+ if (window)
+ window->HandleEvent(*event);
+
+ return false;
+}
+
+void SDLPlatform::PreHandleEvents()
+{
+ SDL_AddEventWatch(EventFilterCallback, &WinImpl::DraggedWindow);
+}
+
+void SDLPlatform::PostHandleEvents()
+{
+ SDL_RemoveEventWatch(EventFilterCallback, &WinImpl::DraggedWindow);
+
+ // Handle window dragging release here
+ if (WinImpl::DraggedWindow != nullptr)
+ {
+ Float2 mousePosition;
+ auto buttons = SDL_GetGlobalMouseState(&mousePosition.X, &mousePosition.Y);
+
+ // Send simulated mouse up event
+ SDL_Event buttonUpEvent { 0 };
+ buttonUpEvent.motion.type = SDL_EVENT_MOUSE_BUTTON_UP;
+ buttonUpEvent.button.down = false;
+ buttonUpEvent.motion.windowID = SDL_GetWindowID(WinImpl::DraggedWindow->GetSDLWindow());
+ buttonUpEvent.motion.timestamp = SDL_GetTicksNS();
+ buttonUpEvent.motion.state = SDL_BUTTON_LEFT;
+ buttonUpEvent.button.clicks = 1;
+ buttonUpEvent.motion.x = mousePosition.X;
+ buttonUpEvent.motion.y = mousePosition.Y;
+ WinImpl::DraggedWindow->HandleEvent(buttonUpEvent);
+ WinImpl::DraggedWindow = nullptr;
+ }
+}
+
+bool SDLWindow::HandleEventInternal(SDL_Event& event)
+{
+ switch (event.type)
+ {
+ case SDL_EVENT_WINDOW_DESTROYED:
+ {
+#if USE_EDITOR
+ // Disable file dropping
+ if (_settings.AllowDragAndDrop)
+ {
+ const auto result = RevokeDragDrop((HWND)_handle);
+ if (result != S_OK)
+ LOG(Warning, "Window drag and drop service error: 0x{0:x}:{1}", result, 2);
+ }
+#endif
+ break;
+ }
+ case SDL_EVENT_MOUSE_BUTTON_UP:
+ {
+ if (WinImpl::DraggedWindow != nullptr && WinImpl::DraggedWindow->_windowId != event.button.windowID)
+ {
+ // Send the button event to dragged window as well
+ Float2 mousePos = ClientToScreen({ event.button.x, event.button.y });
+ Float2 clientPos = WinImpl::DraggedWindow->ScreenToClient(mousePos);
+
+ SDL_Event event2 = event;
+ event2.button.windowID = WinImpl::DraggedWindow->_windowId;
+ event2.button.x = clientPos.X;
+ event2.button.y = clientPos.Y;
+
+ SDLInput::HandleEvent(WinImpl::DraggedWindow, event2);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ return false;
+}
+
+bool SDLPlatform::UsesWindows()
+{
+ return true;
+}
+
+bool SDLPlatform::UsesWayland()
+{
+ return false;
+}
+
+bool SDLPlatform::UsesX11()
+{
+ return false;
+}
+
+void SDLWindow::Focus()
+{
+ auto activateWhenRaised = SDL_GetHint(SDL_HINT_WINDOW_ACTIVATE_WHEN_RAISED);
+ SDL_SetHint(SDL_HINT_WINDOW_ACTIVATE_WHEN_RAISED, "1");
+
+ // Forcing the window to focus causes issues with opening context menus while window is maximized
+ //auto forceRaiseWindow = SDL_GetHint(SDL_HINT_FORCE_RAISEWINDOW);
+ //SDL_SetHint(SDL_HINT_FORCE_RAISEWINDOW, "1");
+
+ SDL_RaiseWindow(_window);
+
+ SDL_SetHint(SDL_HINT_WINDOW_ACTIVATE_WHEN_RAISED, activateWhenRaised);
+ //SDL_SetHint(SDL_HINT_FORCE_RAISEWINDOW, forceRaiseWindow);
+}
+
+void SDLPlatform::SetHighDpiAwarenessEnabled(bool enable)
+{
+ // Other supported values: "permonitor", "permonitorv2"
+ SDL_SetHint("SDL_WINDOWS_DPI_AWARENESS", enable ? "system" : "unaware");
+}
+
+DragDropEffect SDLWindow::DoDragDrop(const StringView& data, const Float2& offset, Window* dragSourceWindow)
+{
+ Show();
+ return DragDropEffect::None;
+}
+
+#endif
diff --git a/Source/Engine/Platform/SDL/SDLPlatform.cpp b/Source/Engine/Platform/SDL/SDLPlatform.cpp
new file mode 100644
index 000000000..1c6232bf6
--- /dev/null
+++ b/Source/Engine/Platform/SDL/SDLPlatform.cpp
@@ -0,0 +1,482 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+#if PLATFORM_SDL
+
+#include "SDLPlatform.h"
+#include "SDLWindow.h"
+#include "Engine/Core/Log.h"
+#include "Engine/Input/Input.h"
+#include "Engine/Input/Mouse.h"
+#include "Engine/Platform/BatteryInfo.h"
+#include "Engine/Platform/CreateProcessSettings.h"
+#include "Engine/Platform/WindowsManager.h"
+#include "Engine/Platform/SDL/SDLInput.h"
+#include "Engine/Engine/CommandLine.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#if PLATFORM_LINUX
+#include "Engine/Engine/CommandLine.h"
+#endif
+
+#define DefaultDPI 96
+
+namespace SDLImpl
+{
+ int32 SystemDpi = 96;
+ String UserLocale("en");
+ bool WindowDecorationsSupported = true;
+ bool SupportsDecorationDragging = true;
+ String WaylandDisplayEnv;
+ String XDGCurrentDesktop;
+}
+
+bool SDLPlatform::Init()
+{
+#if PLATFORM_LINUX
+ bool waylandSession = false;
+ if (!GetEnvironmentVariable(String("WAYLAND_DISPLAY"), SDLImpl::WaylandDisplayEnv))
+ waylandSession = true;
+ GetEnvironmentVariable(String("XDG_CURRENT_DESKTOP"), SDLImpl::XDGCurrentDesktop);
+
+ if (CommandLine::Options.X11.IsTrue())
+ {
+ SDL_SetHintWithPriority(SDL_HINT_VIDEO_DRIVER, "x11", SDL_HINT_OVERRIDE);
+ waylandSession = false;
+ }
+ else if (CommandLine::Options.Wayland.IsTrue())
+ SDL_SetHintWithPriority(SDL_HINT_VIDEO_DRIVER, "wayland", SDL_HINT_OVERRIDE);
+ else if (waylandSession)
+ {
+ // Override the X11 preference when running in Wayland session
+ SDL_SetHintWithPriority(SDL_HINT_VIDEO_DRIVER, "wayland", SDL_HINT_OVERRIDE);
+ }
+
+ // Workaround for libdecor in Gnome+Wayland causing freezes when interacting with the native decorations
+ if (waylandSession && SDLImpl::XDGCurrentDesktop.Compare(String("GNOME"), StringSearchCase::IgnoreCase) == 0)
+ {
+ SDL_SetHint(SDL_HINT_VIDEO_WAYLAND_ALLOW_LIBDECOR, "0");
+ SDLImpl::WindowDecorationsSupported = false;
+ }
+ if (waylandSession)
+ SDLImpl::SupportsDecorationDragging = false;
+#endif
+
+#if PLATFORM_LINUX
+ // The name follows the .desktop entry specification, this is used to get a fallback icon on Wayland:
+ // https://specifications.freedesktop.org/desktop-entry-spec/latest/file-naming.html
+#if USE_EDITOR
+ SDL_SetHint(SDL_HINT_APP_ID, StringAnsi("com.FlaxEngine.FlaxEditor").Get());
+#else
+ // TODO: This should be read from the platform configuration (needed for desktop icon handling)
+ SDL_SetHint(SDL_HINT_APP_ID, StringAnsi("com.FlaxEngine.FlaxGame").Get());
+#endif
+#else
+ SDL_SetHint(SDL_HINT_APP_ID, StringAnsi(ApplicationClassName).Get());
+#endif
+
+ SDL_SetHint(SDL_HINT_WINDOW_ACTIVATE_WHEN_SHOWN, "0");
+ SDL_SetHint(SDL_HINT_WINDOW_ACTIVATE_WHEN_RAISED, "0");
+ SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1"); // Fixes context menu focus issues when clicking unfocused menus
+ SDL_SetHint(SDL_HINT_WINDOWS_ERASE_BACKGROUND_MODE, "0");
+ SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0"); // Already handled during platform initialization
+ SDL_SetHint("SDL_BORDERLESS_RESIZABLE_STYLE", "1"); // Allow borderless windows to be resizable on Windows
+ //SDL_SetHint("SDL_BORDERLESS_WINDOWED_STYLE", "1");
+
+ SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_WARP_MOTION, "0");
+ SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_CURSOR_VISIBLE, "1"); // Needed for tracking mode
+ SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_CENTER, "0"); // Relative mode can be active when cursor is shown and clipped
+ SDL_SetHint(SDL_HINT_MOUSE_DOUBLE_CLICK_RADIUS, "8"); // Reduce the default mouse double-click radius
+
+ //SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP, "1"); // Disables raw mouse input
+ SDL_SetHint(SDL_HINT_WINDOWS_RAW_KEYBOARD, "1");
+
+ SDL_SetHint(SDL_HINT_VIDEO_WAYLAND_SCALE_TO_DISPLAY, "1");
+
+ //if (InitInternal())
+ // return true;
+
+ if (!SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD))
+ Platform::Fatal(String::Format(TEXT("Failed to initialize SDL: {0}."), String(SDL_GetError())));
+
+ int localesCount = 0;
+ auto locales = SDL_GetPreferredLocales(&localesCount);
+ for (int i = 0; i < localesCount; i++)
+ {
+ auto language = StringAnsiView(locales[i]->language);
+ auto country = StringAnsiView(locales[i]->country);
+ if (language.StartsWith("en"))
+ {
+ if (country != nullptr)
+ SDLImpl::UserLocale = String::Format(TEXT("{0}-{1}"), String(language), String(locales[i]->country));
+ else
+ SDLImpl::UserLocale = String(language);
+ break;
+ }
+ }
+ SDL_free(locales);
+
+ if (InitInternal())
+ return true;
+
+#if !PLATFORM_MAC
+ if (!UsesWayland())
+ {
+ // Disable SDL clipboard support
+ SDL_SetEventEnabled(SDL_EVENT_CLIPBOARD_UPDATE, false);
+
+ // Disable SDL drag and drop support
+ SDL_SetEventEnabled(SDL_EVENT_DROP_FILE, false);
+ SDL_SetEventEnabled(SDL_EVENT_DROP_TEXT, false);
+ SDL_SetEventEnabled(SDL_EVENT_DROP_BEGIN, false);
+ SDL_SetEventEnabled(SDL_EVENT_DROP_COMPLETE, false);
+ SDL_SetEventEnabled(SDL_EVENT_DROP_POSITION, false);
+ }
+#endif
+
+ SDLInput::Init();
+ SDLWindow::Init();
+
+ SDLImpl::SystemDpi = (int)(SDL_GetDisplayContentScale(SDL_GetPrimaryDisplay()) * DefaultDPI);
+
+ //SDL_StartTextInput(); // TODO: Call this only when text input is expected (shows virtual keyboard in some cases)
+
+ return base::Init();
+}
+
+void SDLPlatform::LogInfo()
+{
+ base::LogInfo();
+
+ const int32 runtimeVersion = SDL_GetVersion();
+ LOG(Info, "Using SDL version {}.{}.{} ({}), runtime: {}.{}.{} ({})",
+ SDL_VERSIONNUM_MAJOR(SDL_VERSION), SDL_VERSIONNUM_MINOR(SDL_VERSION), SDL_VERSIONNUM_MICRO(SDL_VERSION), String(SDL_REVISION),
+ SDL_VERSIONNUM_MAJOR(runtimeVersion), SDL_VERSIONNUM_MINOR(runtimeVersion), SDL_VERSIONNUM_MICRO(runtimeVersion), String(SDL_GetRevision()));
+
+ LOG(Info, "SDL video driver: {}", String(SDL_GetCurrentVideoDriver()));
+}
+
+void SDLPlatform::Tick()
+{
+ SDLInput::Update();
+
+ PreHandleEvents();
+
+ SDL_PumpEvents();
+ SDL_Event events[32];
+ int count = SDL_PeepEvents(events, SDL_arraysize(events), SDL_GETEVENT, SDL_EVENT_FIRST, SDL_EVENT_LAST);
+ for (int i = 0; i < count; ++i)
+ {
+ SDLWindow* window = SDLWindow::GetWindowFromEvent(events[i]);
+ if (window)
+ window->HandleEvent(events[i]);
+ else if (events[i].type >= SDL_EVENT_JOYSTICK_AXIS_MOTION && events[i].type <= SDL_EVENT_GAMEPAD_STEAM_HANDLE_UPDATED)
+ SDLInput::HandleEvent(nullptr, events[i]);
+ else
+ HandleEvent(events[i]);
+ }
+
+ PostHandleEvents();
+}
+
+bool SDLPlatform::HandleEvent(SDL_Event& event)
+{
+ return true;
+}
+
+String SDLPlatform::GetDisplayServer()
+{
+#if PLATFORM_LINUX
+ String driver(SDL_GetCurrentVideoDriver());
+ if (driver.Length() > 0)
+ driver[0] = StringUtils::ToUpper(driver[0]);
+ return driver;
+#else
+ return String::Empty;
+#endif
+}
+
+bool SDLPlatform::SupportsNativeDecorations()
+{
+ return SDLImpl::WindowDecorationsSupported;
+}
+
+bool SDLPlatform::SupportsNativeDecorationDragging()
+{
+ return SDLImpl::SupportsDecorationDragging;
+}
+
+BatteryInfo SDLPlatform::GetBatteryInfo()
+{
+ BatteryInfo info;
+ int percentage;
+ SDL_PowerState powerState = SDL_GetPowerInfo(nullptr, &percentage);
+
+ if (percentage < 0)
+ info.BatteryLifePercent = 1.0f;
+ else
+ info.BatteryLifePercent = (float)percentage / 100.0f;
+
+ switch (powerState)
+ {
+ case SDL_POWERSTATE_CHARGING:
+ info.State = BatteryInfo::States::BatteryCharging;
+ break;
+ case SDL_POWERSTATE_ON_BATTERY:
+ info.State = BatteryInfo::States::BatteryDischarging;
+ break;
+ case SDL_POWERSTATE_CHARGED:
+ info.State = BatteryInfo::States::Connected;
+ break;
+ default:
+ info.State = BatteryInfo::States::Unknown;
+ }
+ return info;
+}
+
+int32 SDLPlatform::GetDpi()
+{
+ return SDLImpl::SystemDpi;
+}
+
+String SDLPlatform::GetUserLocaleName()
+{
+ return SDLImpl::UserLocale;
+}
+
+void SDLPlatform::OpenUrl(const StringView& url)
+{
+ StringAnsi urlStr(url);
+ SDL_OpenURL(urlStr.GetText());
+}
+
+Float2 SDLPlatform::GetMousePosition()
+{
+#if PLATFORM_LINUX
+ if (UsesWayland())
+ {
+ // Wayland doesn't support reporting global mouse position,
+ // use the last known reported position we got from received window events.
+ return Input::GetMouseScreenPosition();
+ }
+#endif
+ Float2 pos;
+ SDL_GetGlobalMouseState(&pos.X, &pos.Y);
+ return pos;
+}
+
+void SDLPlatform::SetMousePosition(const Float2& pos)
+{
+ SDL_WarpMouseGlobal(pos.X, pos.Y);
+}
+
+Float2 SDLPlatform::GetDesktopSize()
+{
+ SDL_Rect rect;
+ SDL_GetDisplayBounds(SDL_GetPrimaryDisplay(), &rect);
+ return Float2(static_cast(rect.w), static_cast(rect.h));
+}
+
+Rectangle SDLPlatform::GetMonitorBounds(const Float2& screenPos)
+{
+ SDL_Point point{ (int32)screenPos.X, (int32)screenPos.Y };
+ SDL_DisplayID display = SDL_GetDisplayForPoint(&point);
+ SDL_Rect rect;
+ SDL_GetDisplayBounds(display, &rect);
+ return Rectangle(static_cast(rect.x), static_cast(rect.y), static_cast(rect.w), static_cast(rect.h));
+}
+
+Rectangle SDLPlatform::GetVirtualDesktopBounds()
+{
+ int count;
+ const SDL_DisplayID* displays = SDL_GetDisplays(&count);
+ if (displays == nullptr)
+ return Rectangle::Empty;
+
+ Rectangle bounds = Rectangle::Empty;
+ for (int i = 0; i < count; i++)
+ {
+ SDL_DisplayID display = displays[i];
+ SDL_Rect rect;
+ SDL_GetDisplayBounds(display, &rect);
+ bounds = Rectangle::Union(bounds, Rectangle(static_cast(rect.x), static_cast(rect.y), static_cast(rect.w), static_cast(rect.h)));
+ }
+ SDL_free((void*)displays);
+ return bounds;
+}
+
+Window* SDLPlatform::CreateWindow(const CreateWindowSettings& settings)
+{
+ return New(settings);
+}
+
+bool ReadStream(SDL_IOStream*& stream, char* buffer, int64 bufferLength, int64& bufferPosition, LogType logType, CreateProcessSettings& settings)
+{
+ bool flushBuffer = false;
+ bool success = true;
+ auto read = SDL_ReadIO(stream, buffer + bufferPosition, bufferLength - bufferPosition - 1);
+ if (read == 0)
+ {
+ SDL_IOStatus status = SDL_GetIOStatus(stream);
+ if (status != SDL_IO_STATUS_NOT_READY && status != SDL_IO_STATUS_EOF)
+ success = false;
+ if (status != SDL_IO_STATUS_NOT_READY)
+ {
+ stream = nullptr;
+ flushBuffer = true;
+ }
+ }
+ else
+ {
+ int64 startPosition = bufferPosition;
+ bufferPosition += (int64)read;
+ if (bufferPosition == bufferLength - 1)
+ {
+ flushBuffer = true;
+ buffer[bufferPosition++] = '\n'; // Make sure to flush fully filled buffer
+ }
+ else
+ {
+ for (int64 i = startPosition; i < bufferPosition; ++i)
+ {
+ if (buffer[i] == '\n')
+ {
+ flushBuffer = true;
+ break;
+ }
+ }
+ }
+ }
+
+ if (flushBuffer)
+ {
+ int64 start = 0;
+ for (int64 i = 0; i < bufferPosition; ++i)
+ {
+ if (buffer[i] != '\n')
+ continue;
+
+ String str(&buffer[start], (int32)(i - start + 1));
+#if LOG_ENABLE
+ if (settings.LogOutput)
+ Log::Logger::Write(logType, StringView(str.Get(), str.Length() - 1));
+#endif
+ if (settings.SaveOutput)
+ settings.Output.Add(str.Get(), str.Length());
+ start = i + 1;
+ }
+ int64 length = bufferPosition - start;
+ if (length > 0)
+ {
+ // TODO: Use memmove here? Overlapped memory regions with memcpy is undefined behaviour
+ char temp[2048];
+ Platform::MemoryCopy(temp, buffer + start, length);
+ Platform::MemoryCopy(buffer, temp, length);
+ bufferPosition = length;
+ }
+ else
+ bufferPosition = 0;
+ }
+ return success;
+}
+
+int32 SDLPlatform::CreateProcess(CreateProcessSettings& settings)
+{
+ LOG(Info, "Command: {0} {1}", settings.FileName, settings.Arguments);
+ if (settings.WorkingDirectory.HasChars())
+ LOG(Info, "Working directory: {0}", settings.WorkingDirectory);
+
+ int32 result = 0;
+ const bool captureStdOut = settings.LogOutput || settings.SaveOutput;
+ const StringAnsi cmdLine = StringAnsi::Format("\"{0}\" {1}", StringAnsi(settings.FileName), StringAnsi(settings.Arguments));
+ StringAnsi workingDirectory(settings.WorkingDirectory);
+
+ // Populate environment with current values from parent environment.
+ // SDL does not populate the environment with the latest values but with a snapshot captured during initialization.
+ Dictionary envDictionary;
+ GetEnvironmentVariables(envDictionary);
+ SDL_Environment* env = SDL_CreateEnvironment(false);
+ for (auto iter = envDictionary.Begin(); iter != envDictionary.End(); ++iter)
+ SDL_SetEnvironmentVariable(env, StringAnsi(iter->Key).Get(), StringAnsi(iter->Value).Get(), true);
+ for (auto iter = settings.Environment.Begin(); iter != settings.Environment.End(); ++iter)
+ SDL_SetEnvironmentVariable(env, StringAnsi(iter->Key).Get(), StringAnsi(iter->Value).Get(), true);
+
+ // Parse argument list with possible quotes included
+ Array arguments;
+ arguments.Add(StringAnsi(settings.FileName));
+ if (CommandLine::ParseArguments(settings.Arguments, arguments))
+ {
+ LOG(Error, "Failed to parse arguments for process {}: '{}'", settings.FileName.Get(), settings.Arguments.Get());
+ return -1;
+ }
+ Array cmd;
+ for (const StringAnsi& str : arguments)
+ cmd.Add(str.Get());
+ cmd.Add((const char*)0);
+
+#if PLATFORM_WINDOWS
+ bool background = !settings.WaitForEnd || settings.HiddenWindow; // This also hides the window on Windows
+#else
+ bool background = !settings.WaitForEnd;
+#endif
+
+ SDL_PropertiesID props = SDL_CreateProperties();
+ SDL_SetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ARGS_POINTER, cmd.Get());
+ SDL_SetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ENVIRONMENT_POINTER, env);
+ SDL_SetBooleanProperty(props, SDL_PROP_PROCESS_CREATE_BACKGROUND_BOOLEAN, background);
+ if (workingDirectory.HasChars())
+ SDL_SetStringProperty(props, SDL_PROP_PROCESS_CREATE_WORKING_DIRECTORY_STRING, workingDirectory.Get());
+ if (captureStdOut)
+ {
+ SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDOUT_NUMBER, SDL_PROCESS_STDIO_APP);
+ SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDERR_NUMBER, SDL_PROCESS_STDIO_APP);
+ }
+ SDL_Process* process = SDL_CreateProcessWithProperties(props);
+ SDL_DestroyProperties(props);
+ SDL_DestroyEnvironment(env);
+ if (process == nullptr)
+ {
+ LOG(Error, "Failed to run process {}: {}", settings.FileName.Get(), String(SDL_GetError()));
+ return -1;
+ }
+
+ props = SDL_GetProcessProperties(process);
+ int64 pid = SDL_GetNumberProperty(props, SDL_PROP_PROCESS_PID_NUMBER, 0);
+ SDL_IOStream* stdoutStream = nullptr;
+ SDL_IOStream* stderrStream = nullptr;
+ if (captureStdOut)
+ {
+ stdoutStream = static_cast(SDL_GetPointerProperty(props, SDL_PROP_PROCESS_STDOUT_POINTER, nullptr));
+ stderrStream = static_cast(SDL_GetPointerProperty(props, SDL_PROP_PROCESS_STDERR_POINTER, nullptr));
+ }
+
+ // Handle process output in realtime
+ char stdoutBuffer[2049];
+ int64 stdoutPosition = 0;
+ char stderrBuffer[2049];
+ int64 stderrPosition = 0;
+ while (stdoutStream != nullptr && stderrStream != nullptr)
+ {
+ if (stdoutStream != nullptr && !ReadStream(stdoutStream, stdoutBuffer, sizeof(stdoutBuffer), stdoutPosition, LogType::Info, settings))
+ LOG(Warning, "Failed to read process {} stdout: {}", pid, String(SDL_GetError()));
+ if (stderrStream != nullptr && !ReadStream(stderrStream, stderrBuffer, sizeof(stderrBuffer), stderrPosition, LogType::Error, settings))
+ LOG(Warning, "Failed to read process {} stderr: {}", pid, String(SDL_GetError()));
+ Sleep(1);
+ }
+
+ if (settings.WaitForEnd)
+ SDL_WaitProcess(process, true, &result);
+
+ SDL_DestroyProcess(process);
+ return result;
+}
+
+#endif
diff --git a/Source/Engine/Platform/SDL/SDLPlatform.h b/Source/Engine/Platform/SDL/SDLPlatform.h
new file mode 100644
index 000000000..0929e225f
--- /dev/null
+++ b/Source/Engine/Platform/SDL/SDLPlatform.h
@@ -0,0 +1,93 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+#pragma once
+
+#if PLATFORM_SDL
+
+#include "Engine/Platform/Base/Enums.h"
+#if PLATFORM_WINDOWS
+#include "Engine/Platform/Windows/WindowsPlatform.h"
+typedef struct tagMSG MSG;
+#elif PLATFORM_LINUX
+#include "Engine/Platform/Linux/LinuxPlatform.h"
+union _XEvent;
+#elif PLATFORM_MAC
+#include "Engine/Platform/Mac/MacPlatform.h"
+#else
+static_assert(false, "Unsupported Platform");
+#endif
+
+class SDLWindow;
+union SDL_Event;
+
+///
+/// The SDL platform implementation and application management utilities.
+///
+class FLAXENGINE_API SDLPlatform
+#if PLATFORM_WINDOWS
+ : public WindowsPlatform
+{
+ using base = WindowsPlatform;
+#elif PLATFORM_LINUX
+ : public LinuxPlatform
+{
+ using base = LinuxPlatform;
+#elif PLATFORM_MAC
+: public MacPlatform
+{
+ using base = MacPlatform;
+#else
+{
+ static_assert(false, "Unsupported Platform");
+#endif
+ friend SDLWindow;
+
+private:
+ static bool InitInternal();
+#if PLATFORM_LINUX
+ static bool InitX11(void* display);
+#endif
+ static bool HandleEvent(SDL_Event& event);
+#if PLATFORM_WINDOWS
+ static bool EventMessageHook(void* userdata, MSG* msg);
+ static bool EventFilterCallback(void* userdata, SDL_Event* event);
+#elif PLATFORM_LINUX
+ static bool X11EventHook(void* userdata, _XEvent* xevent);
+#elif PLATFORM_MAC
+ static bool EventFilterCallback(void* userdata, SDL_Event* event);
+#endif
+ static void PreHandleEvents();
+ static void PostHandleEvents();
+
+public:
+#if PLATFORM_LINUX
+ static void* GetXDisplay();
+#endif
+ static bool UsesWindows();
+ static bool UsesWayland();
+ static bool UsesX11();
+
+public:
+
+ // [PlatformBase]
+ static bool Init();
+ static void LogInfo();
+ static void Tick();
+ static String GetDisplayServer();
+ static bool SupportsNativeDecorations();
+ static bool SupportsNativeDecorationDragging();
+ static void SetHighDpiAwarenessEnabled(bool enable);
+ static BatteryInfo GetBatteryInfo();
+ static int32 GetDpi();
+ static String GetUserLocaleName();
+ static void OpenUrl(const StringView& url);
+ static Float2 GetMousePosition();
+ static void SetMousePosition(const Float2& pos);
+ static Float2 GetDesktopSize();
+ static Rectangle GetMonitorBounds(const Float2& screenPos);
+ static Rectangle GetVirtualDesktopBounds();
+ static Window* CreateWindow(const CreateWindowSettings& settings);
+ static int32 CreateProcess(CreateProcessSettings& settings);
+};
+
+#endif
diff --git a/Source/Engine/Platform/SDL/SDLWindow.cpp b/Source/Engine/Platform/SDL/SDLWindow.cpp
new file mode 100644
index 000000000..6b7914a86
--- /dev/null
+++ b/Source/Engine/Platform/SDL/SDLWindow.cpp
@@ -0,0 +1,1011 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+#if PLATFORM_SDL
+
+#include "SDLWindow.h"
+#include "SDLInput.h"
+#include "Engine/Core/Log.h"
+#include "Engine/Core/Math/Color32.h"
+#include "Engine/Core/Math/Math.h"
+#include "Engine/Core/Math/Rectangle.h"
+#include "Engine/Engine/Globals.h"
+#include "Engine/Graphics/GPUDevice.h"
+#include "Engine/Graphics/GPUSwapChain.h"
+#include "Engine/Graphics/RenderTask.h"
+#include "Engine/Graphics/Textures/TextureData.h"
+#include "Engine/Input/Input.h"
+#include "Engine/Input/Keyboard.h"
+#include "Engine/Input/Mouse.h"
+#include "Engine/Platform/FileSystem.h"
+#include "Engine/Platform/WindowsManager.h"
+#if PLATFORM_LINUX
+#define COMPILE_WITH_TEXTURE_TOOL 1 // FIXME
+#include "Engine/Tools/TextureTool/TextureTool.h"
+#endif
+
+#define NOGDI
+#include
+#include
+#include
+#undef CreateWindow
+
+#if PLATFORM_WINDOWS
+#include "Engine/Platform/Win32/IncludeWindowsHeaders.h"
+#if USE_EDITOR
+#include
+#endif
+#elif PLATFORM_LINUX
+#include "Engine/Platform/Linux/IncludeX11.h"
+#elif PLATFORM_MAC
+#include
+#else
+static_assert(false, "Unsupported Platform");
+#endif
+
+#define DefaultDPI 96
+
+namespace SDLImpl
+{
+ SDLWindow* LastEventWindow = nullptr;
+ SDL_Cursor* Cursors[SDL_SYSTEM_CURSOR_COUNT] = { nullptr };
+ extern String XDGCurrentDesktop;
+}
+
+SDL_HitTestResult OnWindowHitTest(SDL_Window* win, const SDL_Point* area, void* data);
+void GetRelativeWindowOffset(WindowType type, SDLWindow* parentWindow, Int2& positionOffset);
+Int2 GetSDLWindowScreenPosition(const SDLWindow* window);
+void SetSDLWindowScreenPosition(const SDLWindow* window, const Int2 position);
+
+bool IsPopupWindow(WindowType type)
+{
+ return type == WindowType::Popup || type == WindowType::Tooltip;
+}
+
+void* GetNativeWindowPointer(SDL_Window* window)
+{
+ void* windowPtr;
+ auto props = SDL_GetWindowProperties(window);
+#if PLATFORM_WINDOWS
+ windowPtr = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr);
+#elif PLATFORM_LINUX
+ windowPtr = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, nullptr);
+ if (windowPtr == nullptr)
+ windowPtr = (void*)SDL_GetNumberProperty(props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0);
+#elif PLATFORM_MAC
+ windowPtr = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, nullptr);
+#elif PLATFORM_ANDROID
+ windowPtr = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_ANDROID_WINDOW_POINTER, nullptr);
+#elif PLATFORM_IOS
+ windowPtr = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_UIKIT_WINDOW_POINTER, nullptr);
+#else
+ static_assert(false, "unsupported platform");
+#endif
+ return windowPtr;
+}
+
+void SDLWindow::Init()
+{
+}
+
+SDLWindow::SDLWindow(const CreateWindowSettings& settings)
+ : WindowBase(settings)
+ , _handle(nullptr)
+ , _cachedClientRectangle(Rectangle())
+#if PLATFORM_LINUX
+ , _dragOver(false)
+#endif
+{
+ Int2 clientSize(Math::TruncToInt(settings.Size.X), Math::TruncToInt(settings.Size.Y));
+ _clientSize = Float2(clientSize);
+
+ if (SDLPlatform::UsesWayland())
+ {
+ // The compositor seems to crash when something is rendered to the hidden popup window surface
+ _settings.ShowAfterFirstPaint = _showAfterFirstPaint = false;
+ }
+
+ uint32 flags = SDL_WINDOW_HIDDEN;
+ if (_settings.Type == WindowType::Utility)
+ flags |= SDL_WINDOW_UTILITY;
+ else if (_settings.Type == WindowType::Regular && !_settings.ShowInTaskbar)
+ flags |= SDL_WINDOW_UTILITY;
+ else if (_settings.Type == WindowType::Tooltip)
+ flags |= SDL_WINDOW_TOOLTIP;
+ else if (_settings.Type == WindowType::Popup)
+ flags |= SDL_WINDOW_POPUP_MENU;
+
+ if (!_settings.HasBorder)
+ flags |= SDL_WINDOW_BORDERLESS;
+ if (_settings.AllowInput)
+ flags |= SDL_WINDOW_INPUT_FOCUS;
+ else
+ flags |= SDL_WINDOW_NOT_FOCUSABLE;
+ if (_settings.HasSizingFrame)
+ flags |= SDL_WINDOW_RESIZABLE;
+ if (_settings.IsTopmost)
+ flags |= SDL_WINDOW_ALWAYS_ON_TOP;
+ if (_settings.SupportsTransparency)
+ flags |= SDL_WINDOW_TRANSPARENT;
+
+ // Disable parenting of child windows as those are always on top of the parent window and never show up in taskbar
+ if (_settings.Parent != nullptr && (_settings.Type != WindowType::Tooltip && _settings.Type != WindowType::Popup))
+ _settings.Parent = nullptr;
+
+ // The window position needs to be relative to the parent window
+ Int2 relativePosition(Math::TruncToInt(settings.Position.X), Math::TruncToInt(settings.Position.Y));
+ GetRelativeWindowOffset(_settings.Type, _settings.Parent, relativePosition);
+
+ SDL_PropertiesID props = SDL_CreateProperties();
+ SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_FLAGS_NUMBER, flags);
+ SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, settings.Title.ToStringAnsi().Get());
+ SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, relativePosition.X);
+ SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, relativePosition.Y);
+ SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, clientSize.X);
+ SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, clientSize.Y);
+ SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_EXTERNAL_GRAPHICS_CONTEXT_BOOLEAN, true);
+ if ((flags & SDL_WINDOW_TOOLTIP) != 0)
+ SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_TOOLTIP_BOOLEAN, true);
+ else if ((flags & SDL_WINDOW_POPUP_MENU) != 0)
+ SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_MENU_BOOLEAN, true);
+ if (_settings.Parent != nullptr)
+ SDL_SetPointerProperty(props, SDL_PROP_WINDOW_CREATE_PARENT_POINTER, _settings.Parent->_window);
+
+ _window = SDL_CreateWindowWithProperties(props);
+ SDL_DestroyProperties(props);
+ if (_window == nullptr)
+ Platform::Fatal(String::Format(TEXT("Cannot create SDL window: {0}"), String(SDL_GetError())));
+
+ _windowId = SDL_GetWindowID(_window);
+ _handle = GetNativeWindowPointer(_window);
+ ASSERT(_handle != nullptr);
+
+ SDL_DisplayID display = SDL_GetDisplayForWindow(_window);
+ _dpiScale = SDL_GetWindowDisplayScale(_window);
+ _dpi = Math::TruncToInt(_dpiScale * DefaultDPI);
+
+ Int2 minimumSize(Math::TruncToInt(_settings.MinimumSize.X) , Math::TruncToInt(_settings.MinimumSize.Y));
+ Int2 maximumSize(Math::TruncToInt(_settings.MaximumSize.X) , Math::TruncToInt(_settings.MaximumSize.Y));
+
+ SDL_SetWindowMinimumSize(_window, minimumSize.X, minimumSize.Y);
+#if PLATFORM_MAC
+ // BUG: The maximum size is not enforced correctly, set it to real high value instead
+ if (maximumSize.X == 0)
+ maximumSize.X = 999999;
+ if (maximumSize.Y == 0)
+ maximumSize.Y = 999999;
+#endif
+ SDL_SetWindowMaximumSize(_window, maximumSize.X, maximumSize.Y);
+
+ SDL_SetWindowHitTest(_window, &OnWindowHitTest, this);
+ InitSwapChain();
+
+#if USE_EDITOR
+ // Enable file drag & drop support
+ if (_settings.AllowDragAndDrop)
+ {
+#if PLATFORM_WINDOWS
+ const auto result = RegisterDragDrop((HWND)_handle, (LPDROPTARGET)(Windows::IDropTarget*)this);
+ if (result != S_OK)
+ {
+ LOG(Warning, "Window drag and drop service error: 0x{0:x}:{1}", result, 1);
+ }
+#elif PLATFORM_LINUX
+ auto xDisplay = (X11::Display*)GetX11Display();
+ if (xDisplay)
+ {
+ auto xdndVersion = 5;
+ auto xdndAware = X11::XInternAtom(xDisplay, "XdndAware", 0);
+ if (xdndAware != 0)
+ X11::XChangeProperty(xDisplay, (X11::Window)_handle, xdndAware, (X11::Atom)4, 32, PropModeReplace, (unsigned char*)&xdndVersion, 1);
+ }
+#elif PLATFORM_MAC
+ NSWindow* win = ((NSWindow*)_handle);
+ NSView* view = win.contentView;
+ [win unregisterDraggedTypes];
+ [win registerForDraggedTypes:@[NSPasteboardTypeFileURL, NSPasteboardTypeString, (NSString*)kUTTypeFileURL, (NSString*)kUTTypeUTF8PlainText]];
+#endif
+ }
+#endif
+
+ SDLImpl::LastEventWindow = this;
+
+#if PLATFORM_LINUX
+ // Initialize using the shared Display instance from SDL
+ if (SDLPlatform::UsesX11() && SDLPlatform::GetXDisplay() == nullptr)
+ SDLPlatform::InitX11(GetX11Display());
+
+ // Window focus changes breaks the text input for some reason, just keep it enabled for good
+ if (SDLPlatform::UsesX11() && _settings.AllowInput)
+ SDL_StartTextInput(_window);
+#endif
+
+#if PLATFORM_LINUX && COMPILE_WITH_TEXTURE_TOOL
+ // Ensure windows other than the main window have some kind of icon
+ static SDL_Surface* surface = nullptr;
+ static Array colorData;
+ if (surface == nullptr)
+ {
+ const String iconPath = Globals::BinariesFolder / TEXT("Logo.png");
+ if (FileSystem::FileExists(iconPath))
+ {
+ TextureData icon;
+ if (!TextureTool::ImportTexture(iconPath, icon))
+ {
+ icon.GetPixels(colorData);
+ surface = SDL_CreateSurfaceFrom(icon.Width, icon.Height, SDL_PIXELFORMAT_ABGR8888, colorData.Get(), sizeof(Color32) * icon.Width);
+ }
+ }
+ }
+ if (surface != nullptr)
+ SDL_SetWindowIcon(_window, surface);
+#endif
+}
+
+SDL_Window* SDLWindow::GetSDLWindow() const
+{
+ return _window;
+}
+
+#if PLATFORM_LINUX
+
+void* SDLWindow::GetWaylandDisplay() const
+{
+ return SDL_GetPointerProperty(SDL_GetWindowProperties(_window), SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, nullptr);
+}
+
+void* SDLWindow::GetX11Display() const
+{
+ return SDL_GetPointerProperty(SDL_GetWindowProperties(_window), SDL_PROP_WINDOW_X11_DISPLAY_POINTER, nullptr);
+}
+
+#endif
+
+SDLWindow::~SDLWindow()
+{
+ if (SDLImpl::LastEventWindow == this)
+ SDLImpl::LastEventWindow = nullptr;
+
+ if (_window == nullptr)
+ return;
+
+ if (Input::Mouse != nullptr && Input::Mouse->IsRelative(this))
+ Input::Mouse->SetRelativeMode(false, this);
+
+ SDL_StopTextInput(_window);
+ SDL_DestroyWindow(_window);
+
+ _window = nullptr;
+ _handle = nullptr;
+ _windowId = 0;
+ _visible = false;
+}
+
+WindowHitCodes SDLWindow::OnWindowHit(const Float2 point)
+{
+ WindowHitCodes hit = WindowHitCodes::Client;
+ if (!IsFullscreen())
+ {
+ Float2 screenPosition = ClientToScreen(point);
+ bool handled = false;
+ OnHitTest(screenPosition, hit, handled);
+ if (!handled)
+ {
+ int margin = _settings.HasBorder ? 0 : 0;
+ auto size = GetClientSize();
+ //if (point.Y < 0)
+ // hit = WindowHitCodes::Caption;
+ if (point.Y < margin && point.X < margin)
+ hit = WindowHitCodes::TopLeft;
+ else if (point.Y < margin && point.X > size.X - margin)
+ hit = WindowHitCodes::TopRight;
+ else if (point.Y < margin)
+ hit = WindowHitCodes::Top;
+ else if (point.X < margin && point.Y > size.Y - margin)
+ hit = WindowHitCodes::BottomLeft;
+ else if (point.X < margin)
+ hit = WindowHitCodes::Left;
+ else if (point.X > size.X - margin && point.Y > size.Y - margin)
+ hit = WindowHitCodes::BottomRight;
+ else if (point.X > size.X - margin)
+ hit = WindowHitCodes::Right;
+ else if (point.Y > size.Y - margin)
+ hit = WindowHitCodes::Bottom;
+ else
+ hit = WindowHitCodes::Client;
+ }
+ }
+ return hit;
+}
+
+SDL_HitTestResult OnWindowHitTest(SDL_Window* win, const SDL_Point* area, void* data)
+{
+ SDLWindow* window = static_cast(data);
+ const Float2 point(static_cast(area->x), static_cast(area->y));
+ WindowHitCodes hit = window->OnWindowHit(point);
+ switch (hit)
+ {
+ case WindowHitCodes::Caption:
+ return SDL_HITTEST_DRAGGABLE;
+ case WindowHitCodes::TopLeft:
+ return SDL_HITTEST_RESIZE_TOPLEFT;
+ case WindowHitCodes::Top:
+ return SDL_HITTEST_RESIZE_TOP;
+ case WindowHitCodes::TopRight:
+ return SDL_HITTEST_RESIZE_TOPRIGHT;
+ case WindowHitCodes::Right:
+ return SDL_HITTEST_RESIZE_RIGHT;
+ case WindowHitCodes::BottomRight:
+ return SDL_HITTEST_RESIZE_BOTTOMRIGHT;
+ case WindowHitCodes::Bottom:
+ return SDL_HITTEST_RESIZE_BOTTOM;
+ case WindowHitCodes::BottomLeft:
+ return SDL_HITTEST_RESIZE_BOTTOMLEFT;
+ case WindowHitCodes::Left:
+ return SDL_HITTEST_RESIZE_LEFT;
+ default:
+ return SDL_HITTEST_NORMAL;
+ }
+}
+
+SDLWindow* SDLWindow::GetWindowFromEvent(const SDL_Event& event)
+{
+ SDL_Window* window = SDL_GetWindowFromEvent(&event);
+ if (window == nullptr)
+ return nullptr;
+ if (SDLImpl::LastEventWindow == nullptr || window != SDLImpl::LastEventWindow->_window)
+ SDLImpl::LastEventWindow = GetWindowWithSDLWindow(window);
+ return SDLImpl::LastEventWindow;
+}
+
+SDLWindow* SDLWindow::GetWindowWithSDLWindow(SDL_Window* window)
+{
+ SDLWindow* found = nullptr;
+ WindowsManager::WindowsLocker.Lock();
+ for (auto win : WindowsManager::Windows)
+ {
+ if (win->_window == window)
+ {
+ found = win;
+ break;
+ }
+ }
+ WindowsManager::WindowsLocker.Unlock();
+ return found;
+}
+
+void SDLWindow::HandleEvent(SDL_Event& event)
+{
+ if (_isClosing)
+ return;
+
+ // Platform specific event handling
+ if (HandleEventInternal(event))
+ return;
+
+ switch (event.type)
+ {
+ case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
+ {
+ Close(ClosingReason::User);
+ return;
+ }
+ case SDL_EVENT_WINDOW_DESTROYED:
+ {
+ // Quit
+#if PLATFORM_WINDOWS
+ PostQuitMessage(0);
+#endif
+ return;
+ }
+ case SDL_EVENT_MOUSE_MOTION:
+ {
+ if (_isTrackingMouse && _isUsingMouseOffset)
+ {
+ Float2 delta(event.motion.xrel, event.motion.yrel);
+ _trackingMouseOffset += delta;
+ }
+ break;
+ }
+ case SDL_EVENT_KEY_DOWN:
+ {
+ if (event.key.scancode == SDL_SCANCODE_RETURN && Input::Keyboard->GetKey(KeyboardKeys::Alt))
+ {
+ LOG(Info, "Alt+Enter pressed");
+ SetIsFullscreen(!IsFullscreen());
+ return;
+ }
+ break;
+ }
+ case SDL_EVENT_WINDOW_MOVED:
+ {
+ _cachedClientRectangle.Location = Float2(static_cast(event.window.data1), static_cast(event.window.data2));
+ return;
+ }
+ case SDL_EVENT_WINDOW_HIT_TEST:
+ {
+ return;
+ }
+ case SDL_EVENT_WINDOW_MINIMIZED:
+ {
+ _minimized = true;
+ _maximized = false;
+ return;
+ }
+ case SDL_EVENT_WINDOW_MAXIMIZED:
+ {
+ _minimized = false;
+ _maximized = true;
+
+ CheckForWindowResize();
+ return;
+ }
+ case SDL_EVENT_WINDOW_RESTORED:
+ {
+ if (_maximized)
+ {
+ _maximized = false;
+ // We assume SDL_EVENT_WINDOW_RESIZED is called right afterwards, no need to check for resize here
+ //CheckForWindowResize();
+ }
+ else if (_minimized)
+ {
+ _minimized = false;
+ CheckForWindowResize();
+ }
+ return;
+ }
+ case SDL_EVENT_WINDOW_RESIZED:
+ {
+ int32 width = event.window.data1;
+ int32 height = event.window.data2;
+
+ _clientSize = Float2(static_cast(width), static_cast(height));
+ _cachedClientRectangle.Size = _clientSize;
+
+ // Check if window size has been changed
+ if (width > 0 && height > 0 && (_swapChain == nullptr || width != _swapChain->GetWidth() || height != _swapChain->GetHeight()))
+ OnResize(width, height);
+ return;
+ }
+ case SDL_EVENT_WINDOW_FOCUS_GAINED:
+ {
+ OnGotFocus();
+ if (_settings.AllowInput && !SDLPlatform::UsesX11())
+ SDL_StartTextInput(_window);
+ if (_isClippingCursor)
+ {
+ // The relative mode needs to be disabled for clipping to take effect
+ bool inRelativeMode = Input::Mouse->IsRelative(this) || _restoreRelativeMode;
+ if (inRelativeMode)
+ Input::Mouse->SetRelativeMode(false, this);
+
+ // Restore previous clipping region
+ SDL_Rect rect{ (int)_clipCursorRect.GetX(), (int)_clipCursorRect.GetY(), (int)_clipCursorRect.GetWidth(), (int)_clipCursorRect.GetHeight() };
+ SDL_SetWindowMouseRect(_window, &rect);
+
+ if (inRelativeMode)
+ Input::Mouse->SetRelativeMode(true, this);
+ }
+ else if (_restoreRelativeMode)
+ Input::Mouse->SetRelativeMode(true, this);
+ _restoreRelativeMode = false;
+ return;
+ }
+ case SDL_EVENT_WINDOW_FOCUS_LOST:
+ {
+ if (_settings.AllowInput && !SDLPlatform::UsesX11())
+ SDL_StopTextInput(_window);
+ if (_isClippingCursor)
+ SDL_SetWindowMouseRect(_window, nullptr);
+
+ if (Input::Mouse->IsRelative(this))
+ {
+ Input::Mouse->SetRelativeMode(false, this);
+ _restoreRelativeMode = true;
+ }
+
+ OnLostFocus();
+ return;
+ }
+ case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED:
+ {
+ float scale = SDL_GetWindowDisplayScale(_window);
+ if (scale > 0.0f && _dpiScale != scale)
+ {
+ float oldScale = _dpiScale;
+ _dpiScale = scale;
+ _dpi = static_cast(_dpiScale * DefaultDPI);
+ int w = static_cast(_cachedClientRectangle.GetWidth() * (scale / oldScale));
+ int h = static_cast(_cachedClientRectangle.GetHeight() * (scale / oldScale));
+ _cachedClientRectangle.Size = Float2(static_cast(w), static_cast(h));
+ SDL_SetWindowSize(_window, w, h);
+ // TODO: Recalculate fonts
+ }
+ return;
+ }
+ default:
+ break;
+ }
+
+ if (_settings.AllowInput)
+ {
+ if (SDLInput::HandleEvent(this, event))
+ return;
+ }
+}
+
+void* SDLWindow::GetNativePtr() const
+{
+ return _handle;
+}
+
+void SDLWindow::Show()
+{
+ if (_visible)
+ return;
+
+ if (_showAfterFirstPaint)
+ {
+ if (RenderTask)
+ RenderTask->Enabled = true;
+ return;
+ }
+
+ SDL_ShowWindow(_window);
+ if (_settings.AllowInput && _settings.ActivateWhenFirstShown)
+ Focus();
+ else if (_settings.Parent == nullptr)
+ BringToFront();
+
+ // Reused top-most windows doesn't stay on top for some reason
+ if (_settings.IsTopmost && !IsPopupWindow(_settings.Type))
+ SetIsAlwaysOnTop(true);
+
+ if (_isTrackingMouse)
+ {
+ if (!SDL_CaptureMouse(true))
+ {
+ if (!SDLPlatform::UsesWayland()) // Suppress "That operation is not supported" errors
+ LOG(Warning, "SDL_CaptureMouse: {0}", String(SDL_GetError()));
+ }
+ }
+
+ WindowBase::Show();
+}
+
+void SDLWindow::Hide()
+{
+ if (!_visible)
+ return;
+
+ SDL_HideWindow(_window);
+
+ WindowBase::Hide();
+}
+
+void SDLWindow::Minimize()
+{
+ if (!_settings.AllowMinimize)
+ return;
+
+ SDL_MinimizeWindow(_window);
+}
+
+void SDLWindow::Maximize()
+{
+ if (!_settings.AllowMaximize)
+ return;
+
+ SDL_MaximizeWindow(_window);
+}
+
+void SDLWindow::SetBorderless(bool isBorderless, bool maximized)
+{
+ if (IsFullscreen())
+ SetIsFullscreen(false);
+
+ // Fixes issue of borderless window not going full screen
+ if (IsMaximized())
+ Restore();
+
+ _settings.HasBorder = !isBorderless;
+
+ BringToFront();
+
+ SDL_SetWindowBordered(_window, !isBorderless ? true : false);
+ if (maximized)
+ Maximize();
+ else
+ Focus();
+
+ CheckForWindowResize();
+}
+
+void SDLWindow::Restore()
+{
+ SDL_RestoreWindow(_window);
+}
+
+bool SDLWindow::IsClosed() const
+{
+ return WindowBase::IsClosed() || _handle == nullptr;
+}
+
+bool SDLWindow::IsForegroundWindow() const
+{
+ SDL_WindowFlags flags = SDL_GetWindowFlags(_window);
+ return (flags & SDL_WINDOW_INPUT_FOCUS) != 0;
+}
+
+void SDLWindow::BringToFront(bool force)
+{
+ SDL_RaiseWindow(_window);
+}
+
+void SDLWindow::SetClientBounds(const Rectangle& clientArea)
+{
+ Int2 newPos = Int2(clientArea.GetTopLeft());
+ int newW = static_cast(clientArea.GetWidth());
+ int newH = static_cast(clientArea.GetHeight());
+
+ SDL_SetWindowSize(_window, newW, newH);
+ SetSDLWindowScreenPosition(this, newPos);
+}
+
+void GetRelativeWindowOffset(WindowType type, SDLWindow* parentWindow, Int2& positionOffset)
+{
+ if (!IsPopupWindow(type))
+ return;
+
+ SDLWindow* window = parentWindow;
+ while (window != nullptr)
+ {
+ Int2 parentPosition;
+ SDL_GetWindowPosition(window->GetSDLWindow(), &parentPosition.X, &parentPosition.Y);
+ positionOffset -= parentPosition;
+
+ if (!IsPopupWindow(window->GetSettings().Type))
+ break;
+
+ window = window->GetSettings().Parent;
+ }
+}
+
+Int2 GetSDLWindowScreenPosition(const SDLWindow* window)
+{
+ Int2 relativeOffset(0, 0);
+ GetRelativeWindowOffset(window->GetSettings().Type, window->GetSettings().Parent, relativeOffset);
+
+ Int2 position;
+ SDL_GetWindowPosition(window->GetSDLWindow(), &position.X, &position.Y);
+
+ return position - relativeOffset;
+}
+
+void SetSDLWindowScreenPosition(const SDLWindow* window, const Int2 position)
+{
+ Int2 relativePosition = position;
+ GetRelativeWindowOffset(window->GetSettings().Type, window->GetSettings().Parent, relativePosition);
+ SDL_SetWindowPosition(window->GetSDLWindow(), relativePosition.X, relativePosition.Y);
+}
+
+void SDLWindow::SetPosition(const Float2& position)
+{
+ Int2 topLeftBorder;
+ SDL_GetWindowBordersSize(_window, &topLeftBorder.Y, &topLeftBorder.X, nullptr, nullptr);
+
+ Int2 screenPosition(static_cast(position.X), static_cast(position.Y));
+ screenPosition += topLeftBorder;
+
+ if (false && SDLPlatform::UsesX11())
+ {
+ // TODO: is this needed?
+ auto monitorBounds = Platform::GetMonitorBounds(Float2::Minimum);
+ screenPosition += Int2(monitorBounds.GetTopLeft());
+ }
+
+ SetSDLWindowScreenPosition(this, screenPosition);
+}
+
+void SDLWindow::SetClientPosition(const Float2& position)
+{
+ SetSDLWindowScreenPosition(this, Int2(position));
+}
+
+void SDLWindow::SetIsFullscreen(bool isFullscreen)
+{
+ SDL_SetWindowFullscreen(_window, isFullscreen ? true : false);
+ if (!isFullscreen)
+ {
+ // The window is set to always-on-top for some reason when leaving fullscreen
+ SetIsAlwaysOnTop(false);
+ }
+
+ WindowBase::SetIsFullscreen(isFullscreen);
+}
+
+bool SDLWindow::IsAlwaysOnTop() const
+{
+ SDL_WindowFlags flags = SDL_GetWindowFlags(_window);
+ return (flags & SDL_WINDOW_ALWAYS_ON_TOP) != 0;
+}
+
+void SDLWindow::SetIsAlwaysOnTop(bool isAlwaysOnTop)
+{
+ if (!SDL_SetWindowAlwaysOnTop(_window, isAlwaysOnTop))
+ LOG(Warning, "SDL_SetWindowAlwaysOnTop failed: {0}", String(SDL_GetError()));
+ // Not sure if this should change _settings.IsTopmost to reflect the new value?
+}
+
+Float2 SDLWindow::GetPosition() const
+{
+ Int2 topLeftBorder;
+ SDL_GetWindowBordersSize(_window, &topLeftBorder.Y, &topLeftBorder.X, nullptr, nullptr);
+
+ Int2 position = GetSDLWindowScreenPosition(this);
+ position -= topLeftBorder;
+
+ return Float2(static_cast(position.X), static_cast(position.Y));
+}
+
+Float2 SDLWindow::GetSize() const
+{
+ int top, left, bottom, right;
+ SDL_GetWindowBordersSize(_window, &top, &left, &bottom, &right);
+
+ int w, h;
+ SDL_GetWindowSizeInPixels(_window, &w, &h);
+ return Float2(static_cast(w + left + right), static_cast(h + top + bottom));
+}
+
+Float2 SDLWindow::GetClientSize() const
+{
+ int w, h;
+ SDL_GetWindowSizeInPixels(_window, &w, &h);
+
+ return Float2(static_cast(w), static_cast(h));;
+}
+
+Float2 SDLWindow::ScreenToClient(const Float2& screenPos) const
+{
+ Int2 position = GetSDLWindowScreenPosition(this);
+ return screenPos - Float2(static_cast(position.X), static_cast(position.Y));
+}
+
+Float2 SDLWindow::ClientToScreen(const Float2& clientPos) const
+{
+ Int2 position = GetSDLWindowScreenPosition(this);
+ return clientPos + Float2(static_cast(position.X), static_cast(position.Y));
+}
+
+void SDLWindow::FlashWindow()
+{
+#if PLATFORM_LINUX
+ // KDE bug: flashing brings the window on top of other windows, disable it for now...
+ if (SDLPlatform::UsesWayland() && SDLImpl::XDGCurrentDesktop.Compare(String("KDE"), StringSearchCase::IgnoreCase) == 0)
+ return;
+#endif
+ SDL_FlashWindow(_window, SDL_FLASH_UNTIL_FOCUSED);
+}
+
+float SDLWindow::GetOpacity() const
+{
+ float opacity = SDL_GetWindowOpacity(_window);
+ if (opacity < 0.0f)
+ {
+ LOG(Warning, "SDL_GetWindowOpacity failed: {0}", String(SDL_GetError()));
+ opacity = 1.0f;
+ }
+ return opacity;
+}
+
+void SDLWindow::SetOpacity(const float opacity)
+{
+ if (!SDL_SetWindowOpacity(_window, opacity))
+ LOG(Warning, "SDL_SetWindowOpacity failed: {0}", String(SDL_GetError()));
+}
+
+#if !PLATFORM_WINDOWS
+
+void SDLWindow::Focus()
+{
+ SDL_RaiseWindow(_window);
+}
+
+#endif
+
+String SDLWindow::GetTitle() const
+{
+ return String(SDL_GetWindowTitle(_window));
+}
+
+void SDLWindow::SetTitle(const StringView& title)
+{
+ SDL_SetWindowTitle(_window, title.ToStringAnsi().Get());
+}
+
+void SDLWindow::StartTrackingMouse(bool useMouseScreenOffset)
+{
+ if (_isTrackingMouse)
+ return;
+
+ _isTrackingMouse = true;
+ _trackingMouseOffset = Float2::Zero;
+ _isUsingMouseOffset = useMouseScreenOffset;
+
+ if (_visible)
+ {
+ if (!SDL_CaptureMouse(true))
+ {
+ if (!SDLPlatform::UsesWayland()) // Suppress "That operation is not supported" errors
+ LOG(Warning, "SDL_CaptureMouse: {0}", String(SDL_GetError()));
+ }
+
+ // For viewport camera mouse tracking we want to use relative mode for best precision
+ if (_cursor == CursorType::Hidden)
+ Input::Mouse->SetRelativeMode(true, this);
+ }
+}
+
+void SDLWindow::EndTrackingMouse()
+{
+ if (!_isTrackingMouse)
+ return;
+
+ _isTrackingMouse = false;
+ _isHorizontalFlippingMouse = false;
+ _isVerticalFlippingMouse = false;
+
+ if (!SDL_CaptureMouse(false))
+ {
+ if (!SDLPlatform::UsesWayland()) // Suppress "That operation is not supported" errors
+ LOG(Warning, "SDL_CaptureMouse: {0}", String(SDL_GetError()));
+ }
+
+ Input::Mouse->SetRelativeMode(false, this);
+ _restoreRelativeMode = false;
+}
+
+void SDLWindow::StartClippingCursor(const Rectangle& bounds)
+{
+ if (!IsFocused())
+ return;
+
+ // The cursor is not fully constrained when positioned outside the clip region
+ SetMousePosition(bounds.GetCenter());
+
+ _isClippingCursor = true;
+ SDL_Rect rect{ (int)bounds.GetX(), (int)bounds.GetY(), (int)bounds.GetWidth(), (int)bounds.GetHeight() };
+ SDL_SetWindowMouseRect(_window, &rect);
+ _clipCursorRect = bounds;
+}
+
+void SDLWindow::EndClippingCursor()
+{
+ if (!_isClippingCursor)
+ return;
+
+ _isClippingCursor = false;
+ SDL_SetWindowMouseRect(_window, nullptr);
+}
+
+void SDLWindow::SetMousePosition(const Float2& position) const
+{
+ if (!_settings.AllowInput || !_focused)
+ return;
+
+ SDL_WarpMouseInWindow(_window, position.X, position.Y);
+
+ Float2 screenPosition = ClientToScreen(position);
+ Input::Mouse->OnMouseMoved(screenPosition);
+}
+
+void SDLWindow::SetCursor(CursorType type)
+{
+ CursorType oldCursor = _cursor;
+ WindowBase::SetCursor(type);
+ if (oldCursor != type)
+ SDLWindow::UpdateCursor();
+}
+
+void SDLWindow::CheckForWindowResize()
+{
+ return;
+ // Cache client size
+ _clientSize = GetClientSize();
+ int32 width = (int32)(_clientSize.X);
+ int32 height = (int32)(_clientSize.Y);
+
+ // Check for windows maximized size and see if it needs to adjust position if needed
+ if (_maximized)
+ {
+ // Pick the current monitor data for sizing
+ SDL_Rect rect;
+ auto displayId = SDL_GetDisplayForWindow(_window);
+ SDL_GetDisplayUsableBounds(displayId, &rect);
+
+ if (width > rect.w && height > rect.h)
+ {
+ width = rect.w;
+ height = rect.h;
+ SDL_SetWindowSize(_window, width, height);
+ }
+ }
+
+ // Check if window size has been changed
+ if (width > 0 && height > 0 && (_swapChain == nullptr || width != _swapChain->GetWidth() || height != _swapChain->GetHeight()))
+ OnResize(width, height);
+}
+
+void SDLWindow::UpdateCursor()
+{
+ if (_cursor == CursorType::Hidden)
+ {
+ SDL_HideCursor();
+
+ if (_isTrackingMouse)
+ Input::Mouse->SetRelativeMode(true, this);
+ return;
+ }
+ SDL_ShowCursor();
+ //if (_isTrackingMouse)
+ // Input::Mouse->SetRelativeMode(false, this);
+
+ int32 index = SDL_SYSTEM_CURSOR_DEFAULT;
+ switch (_cursor)
+ {
+ case CursorType::Cross:
+ index = SDL_SYSTEM_CURSOR_CROSSHAIR;
+ break;
+ case CursorType::Hand:
+ index = SDL_SYSTEM_CURSOR_POINTER;
+ break;
+ case CursorType::Help:
+ //index = SDL_SYSTEM_CURSOR_DEFAULT;
+ break;
+ case CursorType::IBeam:
+ index = SDL_SYSTEM_CURSOR_TEXT;
+ break;
+ case CursorType::No:
+ index = SDL_SYSTEM_CURSOR_NOT_ALLOWED;
+ break;
+ case CursorType::Wait:
+ index = SDL_SYSTEM_CURSOR_WAIT;
+ break;
+ case CursorType::SizeAll:
+ index = SDL_SYSTEM_CURSOR_MOVE;
+ break;
+ case CursorType::SizeNESW:
+ index = SDL_SYSTEM_CURSOR_NESW_RESIZE;
+ break;
+ case CursorType::SizeNS:
+ index = SDL_SYSTEM_CURSOR_NS_RESIZE;
+ break;
+ case CursorType::SizeNWSE:
+ index = SDL_SYSTEM_CURSOR_NWSE_RESIZE;
+ break;
+ case CursorType::SizeWE:
+ index = SDL_SYSTEM_CURSOR_EW_RESIZE;
+ break;
+ case CursorType::Default:
+ default:
+ break;
+ }
+
+ if (SDLImpl::Cursors[index] == nullptr)
+ SDLImpl::Cursors[index] = SDL_CreateSystemCursor(static_cast(index));
+ SDL_SetCursor(SDLImpl::Cursors[index]);
+}
+
+void SDLWindow::SetIcon(TextureData& icon)
+{
+ Array colorData;
+ icon.GetPixels(colorData);
+ SDL_Surface* surface = SDL_CreateSurfaceFrom(icon.Width, icon.Height, SDL_PIXELFORMAT_ABGR8888, colorData.Get(), sizeof(Color32) * icon.Width);
+
+ SDL_SetWindowIcon(_window, surface);
+ SDL_free(surface);
+}
+
+#endif
+
diff --git a/Source/Engine/Platform/SDL/SDLWindow.h b/Source/Engine/Platform/SDL/SDLWindow.h
new file mode 100644
index 000000000..a84ec24c9
--- /dev/null
+++ b/Source/Engine/Platform/SDL/SDLWindow.h
@@ -0,0 +1,130 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+#pragma once
+
+#if PLATFORM_SDL
+
+#include "Engine/Platform/Base/WindowBase.h"
+
+struct SDL_Window;
+union SDL_Event;
+#if PLATFORM_LINUX
+class MessageBox;
+#endif
+
+///
+/// Implementation of the window class for SDL platform
+///
+class FLAXENGINE_API SDLWindow : public WindowBase
+#if USE_EDITOR && PLATFORM_WINDOWS
+ , public Windows::IDropTarget
+#endif
+{
+ friend SDLPlatform;
+ friend class SDLMouse;
+#if PLATFORM_LINUX
+ friend LinuxPlatform;
+ friend MessageBox;
+#endif
+
+private:
+ void* _handle; // Opaque, platform specific window handle
+#if USE_EDITOR && PLATFORM_WINDOWS
+ Windows::ULONG _refCount;
+#endif
+#if PLATFORM_LINUX
+ bool _dragOver;
+#endif
+ SDL_Window* _window;
+ uint32 _windowId;
+ Rectangle _clipCursorRect;
+ Rectangle _cachedClientRectangle;
+
+public:
+ static void Init();
+
+public:
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The initial window settings.
+ SDLWindow(const CreateWindowSettings& settings);
+
+ ///
+ /// Finalizes an instance of the class.
+ ///
+ ~SDLWindow();
+
+private:
+ static SDLWindow* GetWindowFromEvent(const SDL_Event& event);
+ static SDLWindow* GetWindowWithSDLWindow(SDL_Window* window);
+ void HandleEvent(SDL_Event& event);
+ bool HandleEventInternal(SDL_Event& event);
+ void CheckForWindowResize();
+ void UpdateCursor();
+
+public:
+ SDL_Window* GetSDLWindow() const;
+#if PLATFORM_LINUX
+ void* GetWaylandDisplay() const;
+ void* GetX11Display() const;
+#endif
+ WindowHitCodes OnWindowHit(const Float2 point);
+
+ // [WindowBase]
+ void* GetNativePtr() const override;
+ void Show() override;
+ void Hide() override;
+ void Minimize() override;
+ void Maximize() override;
+ void SetBorderless(bool isBorderless, bool maximized = false) override;
+ void Restore() override;
+ bool IsClosed() const override;
+ bool IsForegroundWindow() const override;
+ void BringToFront(bool force = false) override;
+ void SetClientBounds(const Rectangle& clientArea) override;
+ void SetPosition(const Float2& position) override;
+ void SetClientPosition(const Float2& position) override;
+ void SetIsFullscreen(bool isFullscreen) override;
+ bool IsAlwaysOnTop() const override;
+ void SetIsAlwaysOnTop(bool isAlwaysOnTop) override;
+ Float2 GetPosition() const override;
+ Float2 GetSize() const override;
+ Float2 GetClientSize() const override;
+ Float2 ScreenToClient(const Float2& screenPos) const override;
+ Float2 ClientToScreen(const Float2& clientPos) const override;
+ void FlashWindow() override;
+ float GetOpacity() const override;
+ void SetOpacity(float opacity) override;
+ void Focus() override;
+ String GetTitle() const override;
+ void SetTitle(const StringView& title) override;
+ DragDropEffect DoDragDrop(const StringView& data) override;
+ DragDropEffect DoDragDrop(const StringView& data, const Float2& offset, Window* dragSourceWindow) override;
+ void StartTrackingMouse(bool useMouseScreenOffset) override;
+ void EndTrackingMouse() override;
+ void StartClippingCursor(const Rectangle& bounds) override;
+ void EndClippingCursor() override;
+ void SetMousePosition(const Float2& position) const override;
+ void SetCursor(CursorType type) override;
+ void SetIcon(TextureData& icon) override;
+
+#if USE_EDITOR && PLATFORM_WINDOWS
+ // [IUnknown]
+ Windows::HRESULT __stdcall QueryInterface(const Windows::IID& id, void** ppvObject) override;
+ Windows::ULONG __stdcall AddRef() override;
+ Windows::ULONG __stdcall Release() override;
+
+ // [Windows::IDropTarget]
+ Windows::HRESULT __stdcall DragEnter(Windows::IDataObject* pDataObj, Windows::DWORD grfKeyState, Windows::POINTL pt, Windows::DWORD* pdwEffect) override;
+ Windows::HRESULT __stdcall DragOver(Windows::DWORD grfKeyState, Windows::POINTL pt, Windows::DWORD* pdwEffect) override;
+ Windows::HRESULT __stdcall DragLeave() override;
+ Windows::HRESULT __stdcall Drop(Windows::IDataObject* pDataObj, Windows::DWORD grfKeyState, Windows::POINTL pt, Windows::DWORD* pdwEffect) override;
+#endif
+#if PLATFORM_LINUX
+ DragDropEffect DoDragDropWayland(const StringView& data, Window* dragSourceWindow = nullptr, Float2 dragOffset = Float2::Zero);
+ DragDropEffect DoDragDropX11(const StringView& data);
+#endif
+};
+
+#endif
diff --git a/Source/Engine/Platform/ScreenUtilities.h b/Source/Engine/Platform/ScreenUtilities.h
new file mode 100644
index 000000000..05678f2da
--- /dev/null
+++ b/Source/Engine/Platform/ScreenUtilities.h
@@ -0,0 +1,15 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+#pragma once
+
+#if PLATFORM_WINDOWS
+#include "Windows/WindowsScreenUtilities.h"
+#elif PLATFORM_LINUX
+#include "Linux/LinuxScreenUtilities.h"
+#elif PLATFORM_MAC
+#include "Mac/MacScreenUtilities.h"
+#else
+#include "Base/ScreenUtilitiesBase.h"
+#endif
+
+#include "Types.h"
diff --git a/Source/Engine/Platform/Types.h b/Source/Engine/Platform/Types.h
index 79fe02efc..e9917e16a 100644
--- a/Source/Engine/Platform/Types.h
+++ b/Source/Engine/Platform/Types.h
@@ -4,8 +4,6 @@
#if PLATFORM_WINDOWS
-class WindowsClipboard;
-typedef WindowsClipboard Clipboard;
class Win32CriticalSection;
typedef Win32CriticalSection CriticalSection;
class Win32ReadWriteLock;
@@ -18,21 +16,25 @@ class WindowsFileSystemWatcher;
typedef WindowsFileSystemWatcher FileSystemWatcher;
class Win32File;
typedef Win32File File;
-class WindowsPlatform;
-typedef WindowsPlatform Platform;
class Win32Thread;
typedef Win32Thread Thread;
+class WindowsClipboard;
+typedef WindowsClipboard Clipboard;
+#if !PLATFORM_SDL
+class WindowsPlatform;
+typedef WindowsPlatform Platform;
class WindowsWindow;
typedef WindowsWindow Window;
+#endif
class Win32Network;
typedef Win32Network Network;
class UserBase;
typedef UserBase User;
+class WindowsScreenUtilities;
+typedef WindowsScreenUtilities ScreenUtilities;
#elif PLATFORM_UWP
-class ClipboardBase;
-typedef ClipboardBase Clipboard;
class Win32CriticalSection;
typedef Win32CriticalSection CriticalSection;
class Win32ReadWriteLock;
@@ -45,21 +47,23 @@ class FileSystemWatcherBase;
typedef FileSystemWatcherBase FileSystemWatcher;
class Win32File;
typedef Win32File File;
-class UWPPlatform;
-typedef UWPPlatform Platform;
class Win32Thread;
typedef Win32Thread Thread;
+class ClipboardBase;
+typedef ClipboardBase Clipboard;
+class UWPPlatform;
+typedef UWPPlatform Platform;
class UWPWindow;
typedef UWPWindow Window;
class Win32Network;
typedef Win32Network Network;
class UserBase;
typedef UserBase User;
+class ScreenUtilitiesBase;
+typedef ScreenUtilitiesBase ScreenUtilities;
#elif PLATFORM_LINUX
-class LinuxClipboard;
-typedef LinuxClipboard Clipboard;
class UnixCriticalSection;
typedef UnixCriticalSection CriticalSection;
class UnixReadWriteLock;
@@ -72,21 +76,25 @@ class LinuxFileSystemWatcher;
typedef LinuxFileSystemWatcher FileSystemWatcher;
class UnixFile;
typedef UnixFile File;
-class LinuxPlatform;
-typedef LinuxPlatform Platform;
class LinuxThread;
typedef LinuxThread Thread;
+#if !PLATFORM_SDL
+class LinuxClipboard;
+typedef LinuxClipboard Clipboard;
+class LinuxPlatform;
+typedef LinuxPlatform Platform;
class LinuxWindow;
typedef LinuxWindow Window;
+#endif
class UnixNetwork;
typedef UnixNetwork Network;
class UserBase;
typedef UserBase User;
+class LinuxScreenUtilities;
+typedef LinuxScreenUtilities ScreenUtilities;
#elif PLATFORM_PS4
-class ClipboardBase;
-typedef ClipboardBase Clipboard;
class UnixCriticalSection;
typedef UnixCriticalSection CriticalSection;
class UnixReadWriteLock;
@@ -99,21 +107,23 @@ class FileSystemWatcherBase;
typedef FileSystemWatcherBase FileSystemWatcher;
class UnixFile;
typedef UnixFile File;
-class PS4Platform;
-typedef PS4Platform Platform;
class PS4Thread;
typedef PS4Thread Thread;
+class ClipboardBase;
+typedef ClipboardBase Clipboard;
+class PS4Platform;
+typedef PS4Platform Platform;
class PS4Window;
typedef PS4Window Window;
class PS4Network;
typedef PS4Network Network;
class PS4User;
typedef PS4User User;
+class ScreenUtilitiesBase;
+typedef ScreenUtilitiesBase ScreenUtilities;
#elif PLATFORM_PS5
-class ClipboardBase;
-typedef ClipboardBase Clipboard;
class UnixCriticalSection;
typedef UnixCriticalSection CriticalSection;
class UnixReadWriteLock;
@@ -126,21 +136,23 @@ class FileSystemWatcherBase;
typedef FileSystemWatcherBase FileSystemWatcher;
class UnixFile;
typedef UnixFile File;
-class PS5Platform;
-typedef PS5Platform Platform;
+class ClipboardBase;
+typedef ClipboardBase Clipboard;
class PS5Thread;
typedef PS5Thread Thread;
+class PS5Platform;
+typedef PS5Platform Platform;
class PS5Window;
typedef PS5Window Window;
class PS5Network;
typedef PS5Network Network;
class PS5User;
typedef PS5User User;
+class ScreenUtilitiesBase;
+typedef ScreenUtilitiesBase ScreenUtilities;
#elif PLATFORM_XBOX_ONE
-class ClipboardBase;
-typedef ClipboardBase Clipboard;
class Win32CriticalSection;
typedef Win32CriticalSection CriticalSection;
class Win32ReadWriteLock;
@@ -153,21 +165,23 @@ class FileSystemWatcherBase;
typedef FileSystemWatcherBase FileSystemWatcher;
class Win32File;
typedef Win32File File;
-class XboxOnePlatform;
-typedef XboxOnePlatform Platform;
class Win32Thread;
typedef Win32Thread Thread;
+class ClipboardBase;
+typedef ClipboardBase Clipboard;
+class XboxOnePlatform;
+typedef XboxOnePlatform Platform;
class GDKWindow;
typedef GDKWindow Window;
class Win32Network;
typedef Win32Network Network;
class GDKUser;
typedef GDKUser User;
+class ScreenUtilitiesBase;
+typedef ScreenUtilitiesBase ScreenUtilities;
#elif PLATFORM_XBOX_SCARLETT
-class ClipboardBase;
-typedef ClipboardBase Clipboard;
class Win32CriticalSection;
typedef Win32CriticalSection CriticalSection;
class Win32ReadWriteLock;
@@ -180,21 +194,23 @@ class FileSystemWatcherBase;
typedef FileSystemWatcherBase FileSystemWatcher;
class Win32File;
typedef Win32File File;
-class XboxScarlettPlatform;
-typedef XboxScarlettPlatform Platform;
class Win32Thread;
typedef Win32Thread Thread;
+class ClipboardBase;
+typedef ClipboardBase Clipboard;
+class XboxScarlettPlatform;
+typedef XboxScarlettPlatform Platform;
class GDKWindow;
typedef GDKWindow Window;
class Win32Network;
typedef Win32Network Network;
class GDKUser;
typedef GDKUser User;
+class ScreenUtilitiesBase;
+typedef ScreenUtilitiesBase ScreenUtilities;
#elif PLATFORM_ANDROID
-class ClipboardBase;
-typedef ClipboardBase Clipboard;
class UnixCriticalSection;
typedef UnixCriticalSection CriticalSection;
class UnixReadWriteLock;
@@ -207,21 +223,23 @@ class FileSystemWatcherBase;
typedef FileSystemWatcherBase FileSystemWatcher;
class AndroidFile;
typedef AndroidFile File;
-class AndroidPlatform;
-typedef AndroidPlatform Platform;
class AndroidThread;
typedef AndroidThread Thread;
+class ClipboardBase;
+typedef ClipboardBase Clipboard;
+class AndroidPlatform;
+typedef AndroidPlatform Platform;
class AndroidWindow;
typedef AndroidWindow Window;
class UnixNetwork;
typedef UnixNetwork Network;
class UserBase;
typedef UserBase User;
+class ScreenUtilitiesBase;
+typedef ScreenUtilitiesBase ScreenUtilities;
#elif PLATFORM_SWITCH
-class ClipboardBase;
-typedef ClipboardBase Clipboard;
class SwitchCriticalSection;
typedef SwitchCriticalSection CriticalSection;
class SwitchReadWriteLock;
@@ -234,21 +252,23 @@ class FileSystemWatcherBase;
typedef FileSystemWatcherBase FileSystemWatcher;
class SwitchFile;
typedef SwitchFile File;
-class SwitchPlatform;
-typedef SwitchPlatform Platform;
class SwitchThread;
typedef SwitchThread Thread;
+class ClipboardBase;
+typedef ClipboardBase Clipboard;
+class SwitchPlatform;
+typedef SwitchPlatform Platform;
class SwitchWindow;
typedef SwitchWindow Window;
class SwitchNetwork;
typedef SwitchNetwork Network;
class SwitchUser;
typedef SwitchUser User;
+class ScreenUtilitiesBase;
+typedef ScreenUtilitiesBase ScreenUtilities;
#elif PLATFORM_MAC
-class MacClipboard;
-typedef MacClipboard Clipboard;
class UnixCriticalSection;
typedef UnixCriticalSection CriticalSection;
class UnixReadWriteLock;
@@ -261,21 +281,25 @@ class MacFileSystemWatcher;
typedef MacFileSystemWatcher FileSystemWatcher;
class UnixFile;
typedef UnixFile File;
-class MacPlatform;
-typedef MacPlatform Platform;
class AppleThread;
typedef AppleThread Thread;
+class MacClipboard;
+typedef MacClipboard Clipboard;
+#if !PLATFORM_SDL
+class MacPlatform;
+typedef MacPlatform Platform;
class MacWindow;
typedef MacWindow Window;
+#endif
class UnixNetwork;
typedef UnixNetwork Network;
class UserBase;
typedef UserBase User;
+class MacScreenUtilities;
+typedef MacScreenUtilities ScreenUtilities;
#elif PLATFORM_IOS
-class ClipboardBase;
-typedef ClipboardBase Clipboard;
class UnixCriticalSection;
typedef UnixCriticalSection CriticalSection;
class UnixReadWriteLock;
@@ -288,19 +312,34 @@ class FileSystemWatcherBase;
typedef FileSystemWatcherBase FileSystemWatcher;
class iOSFile;
typedef iOSFile File;
-class iOSPlatform;
-typedef iOSPlatform Platform;
class AppleThread;
typedef AppleThread Thread;
+class ClipboardBase;
+typedef ClipboardBase Clipboard;
+class iOSPlatform;
+typedef iOSPlatform Platform;
class iOSWindow;
typedef iOSWindow Window;
class UnixNetwork;
typedef UnixNetwork Network;
class UserBase;
typedef UserBase User;
+class ScreenUtilitiesBase;
+typedef ScreenUtilitiesBase ScreenUtilities;
#else
#error Missing Types implementation!
#endif
+
+#if PLATFORM_SDL
+#if PLATFORM_LINUX
+class SDLClipboard;
+typedef SDLClipboard Clipboard;
+#endif
+class SDLPlatform;
+typedef SDLPlatform Platform;
+class SDLWindow;
+typedef SDLWindow Window;
+#endif
diff --git a/Source/Engine/Platform/UWP/UWPWindow.h b/Source/Engine/Platform/UWP/UWPWindow.h
index 3da2a906e..b5a1f9c53 100644
--- a/Source/Engine/Platform/UWP/UWPWindow.h
+++ b/Source/Engine/Platform/UWP/UWPWindow.h
@@ -183,6 +183,7 @@ public:
void Focus() override;
void SetTitle(const StringView& title) override;
DragDropEffect DoDragDrop(const StringView& data) override;
+ using WindowBase::DoDragDrop;
void StartTrackingMouse(bool useMouseScreenOffset) override;
void EndTrackingMouse() override;
void SetCursor(CursorType type) override;
diff --git a/Source/Engine/Platform/Window.cs b/Source/Engine/Platform/Window.cs
index 86b872513..06b02457a 100644
--- a/Source/Engine/Platform/Window.cs
+++ b/Source/Engine/Platform/Window.cs
@@ -17,31 +17,37 @@ namespace FlaxEngine
///
/// Perform window hit test delegate.
///
- /// The mouse position. The coordinate is relative to the upper-left corner of the screen. Use to convert position into client space coordinates.
+ /// The mouse position. The coordinate is relative to the upper-left corner of the screen. Use to convert position into client space coordinates.
/// Hit result.
- public delegate WindowHitCodes HitTestDelegate(ref Float2 mouse);
+ public delegate WindowHitCodes HitTestDelegate(ref Float2 mousePosition);
///
/// Perform mouse buttons action.
///
- /// The mouse position.
+ /// The mouse position.
/// The mouse buttons state.
/// The flag that indicated that event has been handled by the custom code and should not be passed further. By default it is set to false.
- public delegate void MouseButtonDelegate(ref Float2 mouse, MouseButton button, ref bool handled);
+ public delegate void MouseButtonDelegate(ref Float2 mousePosition, MouseButton button, ref bool handled);
///
/// Perform mouse move action.
///
- /// The mouse position.
- public delegate void MouseMoveDelegate(ref Float2 mouse);
+ /// The mouse position.
+ public delegate void MouseMoveDelegate(ref Float2 mousePosition);
+
+ ///
+ /// Perform mouse move action in relative mode.
+ ///
+ /// The relative mouse motion.
+ public delegate void MouseMoveRelativeDelegate(ref Float2 mouseMotion);
///
/// Perform mouse wheel action.
///
- /// The mouse position.
+ /// The mouse position.
/// The mouse wheel move delta (can be positive or negative; normalized to [-1;1] range).
/// The flag that indicated that event has been handled by the custom code and should not be passed further. By default it is set to false.
- public delegate void MouseWheelDelegate(ref Float2 mouse, float delta, ref bool handled);
+ public delegate void MouseWheelDelegate(ref Float2 mousePosition, float delta, ref bool handled);
///
/// Perform touch action.
@@ -99,9 +105,14 @@ namespace FlaxEngine
public event MouseWheelDelegate MouseWheel;
///
- /// Event fired when mouse moves
+ /// Event fired when mouse moves.
///
public event MouseMoveDelegate MouseMove;
+
+ ///
+ /// Event fired when mouse moves in relative mode.
+ ///
+ public event MouseMoveRelativeDelegate MouseMoveRelative;
///
/// Event fired when mouse leaves window.
@@ -273,6 +284,12 @@ namespace FlaxEngine
MouseMove?.Invoke(ref pos);
GUI.OnMouseMove(pos);
}
+
+ internal void Internal_OnMouseMoveRelative(ref Float2 mouseMotion)
+ {
+ MouseMoveRelative?.Invoke(ref mouseMotion);
+ GUI.OnMouseMoveRelative(mouseMotion);
+ }
internal void Internal_OnMouseLeave()
{
diff --git a/Source/Engine/Platform/Window.h b/Source/Engine/Platform/Window.h
index 616b76bd2..92eca4d41 100644
--- a/Source/Engine/Platform/Window.h
+++ b/Source/Engine/Platform/Window.h
@@ -2,7 +2,9 @@
#pragma once
-#if PLATFORM_WINDOWS
+#if PLATFORM_SDL
+#include "SDL/SDLWindow.h"
+#elif PLATFORM_WINDOWS
#include "Windows/WindowsWindow.h"
#elif PLATFORM_UWP
#include "UWP/UWPWindow.h"
diff --git a/Source/Engine/Platform/Windows/WindowsFileSystem.cpp b/Source/Engine/Platform/Windows/WindowsFileSystem.cpp
index efce1abdc..3019b49a7 100644
--- a/Source/Engine/Platform/Windows/WindowsFileSystem.cpp
+++ b/Source/Engine/Platform/Windows/WindowsFileSystem.cpp
@@ -2,6 +2,7 @@
#if PLATFORM_WINDOWS
+#include "WindowsWindow.h"
#include "WindowsFileSystem.h"
#include "Engine/Platform/File.h"
#include "Engine/Platform/Window.h"
@@ -317,7 +318,7 @@ bool WindowsFileSystem::ShowBrowseFolderDialog(Window* parentWindow, const Strin
if (SUCCEEDED(SHCreateItemFromParsingName(initialDirectory.Get(), NULL, IID_PPV_ARGS(&defaultFolder))))
fd->SetFolder(defaultFolder);
- HWND hwndOwner = parentWindow ? parentWindow->GetHWND() : NULL;
+ HWND hwndOwner = parentWindow ? (HWND)parentWindow->GetNativePtr() : NULL;
if (SUCCEEDED(fd->Show(hwndOwner)))
{
ComPtr si;
diff --git a/Source/Engine/Platform/Windows/WindowsInput.cpp b/Source/Engine/Platform/Windows/WindowsInput.cpp
index edcc01141..64299b6a3 100644
--- a/Source/Engine/Platform/Windows/WindowsInput.cpp
+++ b/Source/Engine/Platform/Windows/WindowsInput.cpp
@@ -1,6 +1,6 @@
// Copyright (c) Wojciech Figat. All rights reserved.
-#if PLATFORM_WINDOWS
+#if PLATFORM_WINDOWS && !PLATFORM_SDL
#include "WindowsInput.h"
#include "WindowsWindow.h"
@@ -265,19 +265,38 @@ bool WindowsMouse::WndProc(Window* window, const UINT msg, WPARAM wParam, LPARAM
}
case WM_LBUTTONDBLCLK:
{
- OnMouseDoubleClick(mousePos, MouseButton::Left, window);
+ if (!Input::Mouse->IsRelative())
+ OnMouseDoubleClick(mousePos, MouseButton::Left, window);
+ else
+ OnMouseDown(mousePos, MouseButton::Left, window);
result = true;
break;
}
case WM_RBUTTONDBLCLK:
{
- OnMouseDoubleClick(mousePos, MouseButton::Right, window);
+ if (!Input::Mouse->IsRelative())
+ OnMouseDoubleClick(mousePos, MouseButton::Right, window);
+ else
+ OnMouseDown(mousePos, MouseButton::Right, window);
result = true;
break;
}
case WM_MBUTTONDBLCLK:
{
- OnMouseDoubleClick(mousePos, MouseButton::Middle, window);
+ if (!Input::Mouse->IsRelative())
+ OnMouseDoubleClick(mousePos, MouseButton::Middle, window);
+ else
+ OnMouseDown(mousePos, MouseButton::Middle, window);
+ result = true;
+ break;
+ }
+ case WM_XBUTTONDBLCLK:
+ {
+ const auto button = (HIWORD(wParam) & XBUTTON1) ? MouseButton::Extended1 : MouseButton::Extended2;
+ if (!Input::Mouse->IsRelative())
+ OnMouseDoubleClick(mousePos, button, window);
+ else
+ OnMouseDown(mousePos, button, window);
result = true;
break;
}
diff --git a/Source/Engine/Platform/Windows/WindowsPlatform.cpp b/Source/Engine/Platform/Windows/WindowsPlatform.cpp
index 42accf298..5fd6540ee 100644
--- a/Source/Engine/Platform/Windows/WindowsPlatform.cpp
+++ b/Source/Engine/Platform/Windows/WindowsPlatform.cpp
@@ -4,6 +4,8 @@
#include "Engine/Platform/Platform.h"
#include "Engine/Platform/Window.h"
+#include "Engine/Platform/Windows/WindowsInput.h"
+#include "Engine/Platform/Windows/WindowsWindow.h"
#include "Engine/Platform/FileSystem.h"
#include "Engine/Platform/CreateWindowSettings.h"
#include "Engine/Platform/CreateProcessSettings.h"
@@ -34,7 +36,6 @@
#define CLR_EXCEPTION 0xE0434352
#define VCPP_EXCEPTION 0xE06D7363
-const Char* WindowsPlatform::ApplicationWindowClass = TEXT("FlaxWindow");
void* WindowsPlatform::Instance = nullptr;
#if CRASH_LOG_ENABLE || TRACY_ENABLE
@@ -288,6 +289,8 @@ struct CPUBrand
#endif
+#if !PLATFORM_SDL
+
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
// Find window to process that message
@@ -305,6 +308,8 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
return DefWindowProc(hwnd, msg, wParam, lParam);
}
+#endif
+
long __stdcall WindowsPlatform::SehExceptionHandler(EXCEPTION_POINTERS* ep)
{
if (ep->ExceptionRecord->ExceptionCode == CLR_EXCEPTION)
@@ -469,11 +474,12 @@ DialogResult MessageBox::Show(Window* parent, const StringView& text, const Stri
flags |= MB_ICONHAND;
break;
case MessageBoxIcon::Information:
+ case MessageBoxIcon::Question:
flags |= MB_ICONINFORMATION;
break;
- case MessageBoxIcon::Question:
- flags |= MB_ICONQUESTION;
- break;
+ //case MessageBoxIcon::Question:
+ // flags |= MB_ICONQUESTION;
+ // break;
case MessageBoxIcon::Stop:
flags |= MB_ICONSTOP;
break;
@@ -611,6 +617,7 @@ void WindowsPlatform::PreInit(void* hInstance)
// Disable the process from being showing "ghosted" while not responding messages during slow tasks
DisableProcessWindowsGhosting();
+#if !PLATFORM_SDL
// Register window class
WNDCLASS windowsClass;
Platform::MemoryClear(&windowsClass, sizeof(WNDCLASS));
@@ -619,12 +626,13 @@ void WindowsPlatform::PreInit(void* hInstance)
windowsClass.hInstance = (HINSTANCE)Instance;
windowsClass.hIcon = LoadIconW(GetModuleHandle(nullptr), MAKEINTRESOURCE(IDR_MAINFRAME));
windowsClass.hCursor = LoadCursor(nullptr, IDC_ARROW);
- windowsClass.lpszClassName = ApplicationWindowClass;
+ windowsClass.lpszClassName = ApplicationClassName;
if (!RegisterClassW(&windowsClass))
{
Error(TEXT("Window class registration failed!"));
exit(-1);
}
+#endif
// Init OLE
if (OleInitialize(nullptr) != S_OK)
@@ -767,11 +775,13 @@ bool WindowsPlatform::Init()
DWORD tmp;
Char buffer[256];
+#if !PLATFORM_SDL
// Get user locale string
if (GetUserDefaultLocaleName(buffer, LOCALE_NAME_MAX_LENGTH))
{
UserLocale = String(buffer);
}
+#endif
// Get computer name string
if (GetComputerNameW(buffer, &tmp))
@@ -787,7 +797,9 @@ bool WindowsPlatform::Init()
}
OnPlatformUserAdd(New(userName));
+#if !PLATFORM_SDL
WindowsInput::Init();
+#endif
return false;
}
@@ -821,7 +833,9 @@ void WindowsPlatform::LogInfo()
void WindowsPlatform::Tick()
{
+#if !PLATFORM_SDL
WindowsInput::Update();
+#endif
// Check to see if any messages are waiting in the queue
MSG msg;
@@ -852,8 +866,10 @@ void WindowsPlatform::Exit()
FlaxDbgHelpUnlock();
#endif
+#if !PLATFORM_SDL
// Unregister app class
- UnregisterClassW(ApplicationWindowClass, nullptr);
+ UnregisterClassW(ApplicationClassName, nullptr);
+#endif
Win32Platform::Exit();
}
@@ -930,6 +946,7 @@ BatteryInfo WindowsPlatform::GetBatteryInfo()
return info;
}
+#if !PLATFORM_SDL
int32 WindowsPlatform::GetDpi()
{
return SystemDpi;
@@ -939,6 +956,7 @@ String WindowsPlatform::GetUserLocaleName()
{
return UserLocale;
}
+#endif
String WindowsPlatform::GetComputerName()
{
@@ -1102,6 +1120,7 @@ bool IsProcRunning(HANDLE handle)
return WaitForSingleObject(handle, 0) == WAIT_TIMEOUT;
}
+#if !PLATFORM_SDL
void ReadPipe(HANDLE pipe, Array& rawData, Array& logData, LogType logType, CreateProcessSettings& settings)
{
// Check if any data is ready to read
@@ -1314,6 +1333,7 @@ Window* WindowsPlatform::CreateWindow(const CreateWindowSettings& settings)
{
return New(settings);
}
+#endif
void* WindowsPlatform::LoadLibrary(const Char* filename)
{
diff --git a/Source/Engine/Platform/Windows/WindowsPlatform.h b/Source/Engine/Platform/Windows/WindowsPlatform.h
index a8f0e4996..f989701be 100644
--- a/Source/Engine/Platform/Windows/WindowsPlatform.h
+++ b/Source/Engine/Platform/Windows/WindowsPlatform.h
@@ -13,11 +13,6 @@ class FLAXENGINE_API WindowsPlatform : public Win32Platform
{
public:
- ///
- /// Win32 application windows class name.
- ///
- static const Char* ApplicationWindowClass;
-
///
/// Handle to Win32 application instance.
///
@@ -70,8 +65,10 @@ public:
static String GetSystemName();
static Version GetSystemVersion();
static BatteryInfo GetBatteryInfo();
+#if !PLATFORM_SDL
static int32 GetDpi();
static String GetUserLocaleName();
+#endif
static String GetComputerName();
static bool GetHasFocus();
static bool CanOpenUrl(const StringView& url);
@@ -84,8 +81,10 @@ public:
static void GetEnvironmentVariables(Dictionary& result);
static bool GetEnvironmentVariable(const String& name, String& value);
static bool SetEnvironmentVariable(const String& name, const String& value);
+#if !PLATFORM_SDL
static int32 CreateProcess(CreateProcessSettings& settings);
static Window* CreateWindow(const CreateWindowSettings& settings);
+#endif
static void* LoadLibrary(const Char* filename);
#if CRASH_LOG_ENABLE
static Array GetStackFrames(int32 skipCount = 0, int32 maxDepth = 60, void* context = nullptr);
diff --git a/Source/Engine/Platform/Windows/WindowsScreenUtilities.cpp b/Source/Engine/Platform/Windows/WindowsScreenUtilities.cpp
new file mode 100644
index 000000000..440963af6
--- /dev/null
+++ b/Source/Engine/Platform/Windows/WindowsScreenUtilities.cpp
@@ -0,0 +1,54 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+#if PLATFORM_WINDOWS
+
+#include "Engine/Platform/Types.h"
+#include "Engine/Platform/ScreenUtilities.h"
+#include "Engine/Core/Math/Vector2.h"
+#include "Engine/Core/Delegate.h"
+#include "Engine/Core/Log.h"
+#include "Engine/Profiler/ProfilerCPU.h"
+
+#include
+
+#pragma comment(lib, "Gdi32.lib")
+
+Delegate ScreenUtilitiesBase::PickColorDone;
+
+static HHOOK MouseCallbackHook;
+
+LRESULT CALLBACK OnScreenUtilsMouseCallback(_In_ int nCode, _In_ WPARAM wParam, _In_ LPARAM lParam)
+{
+ if (nCode >= 0 && wParam == WM_LBUTTONDOWN)
+ {
+ UnhookWindowsHookEx(MouseCallbackHook);
+
+ // Push event with the picked color
+ const Float2 cursorPos = Platform::GetMousePosition();
+ const Color32 colorPicked = ScreenUtilities::GetColorAt(cursorPos);
+ ScreenUtilities::PickColorDone(colorPicked);
+ return 1;
+ }
+ return CallNextHookEx(NULL, nCode, wParam, lParam);
+}
+
+Color32 WindowsScreenUtilities::GetColorAt(const Float2& pos)
+{
+ PROFILE_CPU();
+ HDC deviceContext = GetDC(NULL);
+ COLORREF color = GetPixel(deviceContext, (int)pos.X, (int)pos.Y);
+ ReleaseDC(NULL, deviceContext);
+ return Color32(GetRValue(color), GetGValue(color), GetBValue(color), 255);
+}
+
+void WindowsScreenUtilities::PickColor()
+{
+ MouseCallbackHook = SetWindowsHookEx(WH_MOUSE_LL, OnScreenUtilsMouseCallback, NULL, NULL);
+ if (MouseCallbackHook == NULL)
+ {
+ LOG(Warning, "Failed to set mouse hook.");
+ LOG(Warning, "Error: {0}", GetLastError());
+ }
+}
+
+#endif
diff --git a/Source/Engine/Platform/Windows/WindowsScreenUtilities.h b/Source/Engine/Platform/Windows/WindowsScreenUtilities.h
new file mode 100644
index 000000000..91abe22fc
--- /dev/null
+++ b/Source/Engine/Platform/Windows/WindowsScreenUtilities.h
@@ -0,0 +1,25 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+#pragma once
+
+#if PLATFORM_WINDOWS
+
+#include "Engine/Platform/Base/ScreenUtilitiesBase.h"
+#include "Engine/Core/Types/BaseTypes.h"
+#include "Engine/Core/Math/Color32.h"
+#include "Engine/Core/Math/Vector2.h"
+#include "Engine/Core/Delegate.h"
+
+///
+/// Platform-dependent screen utilities.
+///
+class FLAXENGINE_API WindowsScreenUtilities : public ScreenUtilitiesBase
+{
+public:
+
+ // [ScreenUtilitiesBase]
+ static Color32 GetColorAt(const Float2& pos);
+ static void PickColor();
+};
+
+#endif
diff --git a/Source/Engine/Platform/Windows/WindowsWindow.DragDrop.cpp b/Source/Engine/Platform/Windows/WindowsWindow.DragDrop.cpp
new file mode 100644
index 000000000..1aa5c903e
--- /dev/null
+++ b/Source/Engine/Platform/Windows/WindowsWindow.DragDrop.cpp
@@ -0,0 +1,665 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+#if PLATFORM_WINDOWS
+
+#if PLATFORM_SDL
+#include "Engine/Platform/SDL/SDLWindow.h"
+#endif
+
+#include "Engine/Platform/Windows/WindowsWindow.h"
+
+#if USE_EDITOR
+#include "Engine/Core/Collections/Array.h"
+#include "Engine/Core/Log.h"
+#include "Engine/Platform/IGuiData.h"
+#include "Engine/Platform/Base/DragDropHelper.h"
+#include "Engine/Input/Input.h"
+#include "Engine/Input/Mouse.h"
+#endif
+
+#include "Engine/Platform/Win32/IncludeWindowsHeaders.h"
+#include
+
+#if USE_EDITOR
+#include
+#include
+#endif
+
+#if USE_EDITOR
+
+Windows::HRESULT Window::QueryInterface(const Windows::IID& id, void** ppvObject)
+{
+ // Check to see what interface has been requested
+ if ((const IID&)id == IID_IUnknown || (const IID&)id == IID_IDropTarget)
+ {
+ AddRef();
+ *ppvObject = this;
+ return S_OK;
+ }
+
+ // No interface
+ *ppvObject = nullptr;
+ return E_NOINTERFACE;
+}
+
+Windows::ULONG Window::AddRef()
+{
+ _InterlockedIncrement(&_refCount);
+ return _refCount;
+}
+
+Windows::ULONG Window::Release()
+{
+ return _InterlockedDecrement(&_refCount);
+}
+
+HGLOBAL duplicateGlobalMem(HGLOBAL hMem)
+{
+ auto len = GlobalSize(hMem);
+ auto source = GlobalLock(hMem);
+ auto dest = GlobalAlloc(GMEM_FIXED, len);
+ Platform::MemoryCopy(dest, source, len);
+ GlobalUnlock(hMem);
+ return dest;
+}
+
+DWORD dropEffect2OleEnum(DragDropEffect effect)
+{
+ DWORD result;
+ switch (effect)
+ {
+ case DragDropEffect::None:
+ result = DROPEFFECT_NONE;
+ break;
+ case DragDropEffect::Copy:
+ result = DROPEFFECT_COPY;
+ break;
+ case DragDropEffect::Move:
+ result = DROPEFFECT_MOVE;
+ break;
+ case DragDropEffect::Link:
+ result = DROPEFFECT_LINK;
+ break;
+ default:
+ result = DROPEFFECT_NONE;
+ break;
+ }
+ return result;
+}
+
+DragDropEffect dropEffectFromOleEnum(DWORD effect)
+{
+ DragDropEffect result;
+ switch (effect)
+ {
+ case DROPEFFECT_NONE:
+ result = DragDropEffect::None;
+ break;
+ case DROPEFFECT_COPY:
+ result = DragDropEffect::Copy;
+ break;
+ case DROPEFFECT_MOVE:
+ result = DragDropEffect::Move;
+ break;
+ case DROPEFFECT_LINK:
+ result = DragDropEffect::Link;
+ break;
+ default:
+ result = DragDropEffect::None;
+ break;
+ }
+ return result;
+}
+
+HANDLE StringToHandle(const StringView& str)
+{
+ // Allocate and lock a global memory buffer.
+ // Make it fixed data so we don't have to use GlobalLock
+ const int32 length = str.Length();
+ char* ptr = static_cast(GlobalAlloc(GMEM_FIXED, length + 1));
+
+ // Copy the string into the buffer as ANSI text
+ StringUtils::ConvertUTF162ANSI(str.Get(), ptr, length);
+ ptr[length] = '\0';
+
+ return ptr;
+}
+
+void DeepCopyFormatEtc(FORMATETC* dest, FORMATETC* source)
+{
+ // Copy the source FORMATETC into dest
+ *dest = *source;
+
+ if (source->ptd)
+ {
+ // Allocate memory for the DVTARGETDEVICE if necessary
+ dest->ptd = static_cast(CoTaskMemAlloc(sizeof(DVTARGETDEVICE)));
+
+ // Copy the contents of the source DVTARGETDEVICE into dest->ptd
+ *(dest->ptd) = *(source->ptd);
+ }
+}
+
+HRESULT CreateEnumFormatEtc(UINT nNumFormats, FORMATETC* pFormatEtc, IEnumFORMATETC** ppEnumFormatEtc);
+
+///
+/// GUI data for Windows platform
+///
+class WindowsGuiData : public IGuiData
+{
+private:
+ Type _type;
+ Array _data;
+
+public:
+ ///
+ /// Init
+ ///
+ WindowsGuiData()
+ : _type(Type::Unknown)
+ , _data(1)
+ {
+ }
+
+public:
+ ///
+ /// Init from Ole IDataObject
+ ///
+ /// Object
+ void Init(IDataObject* pDataObj)
+ {
+ // Temporary data
+ FORMATETC fmtetc;
+ STGMEDIUM stgmed;
+
+ // Clear
+ _type = Type::Unknown;
+ _data.Clear();
+
+ // Check type
+ fmtetc = { CF_TEXT, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
+ if (pDataObj->GetData(&fmtetc, &stgmed) == S_OK)
+ {
+ // Text
+ _type = Type::Text;
+
+ // Get data
+ char* text = static_cast(GlobalLock(stgmed.hGlobal));
+ _data.Add(String(text));
+ GlobalUnlock(stgmed.hGlobal);
+ ReleaseStgMedium(&stgmed);
+ }
+ else
+ {
+ fmtetc = { CF_UNICODETEXT, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
+ if (pDataObj->GetData(&fmtetc, &stgmed) == S_OK)
+ {
+ // Unicode Text
+ _type = Type::Text;
+
+ // Get data
+ Char* text = static_cast(GlobalLock(stgmed.hGlobal));
+ _data.Add(String(text));
+ GlobalUnlock(stgmed.hGlobal);
+ ReleaseStgMedium(&stgmed);
+ }
+ else
+ {
+ fmtetc = { CF_HDROP, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
+ if (pDataObj->GetData(&fmtetc, &stgmed) == S_OK)
+ {
+ // Files
+ _type = Type::Files;
+
+ // Get data
+ Char item[MAX_PATH];
+ HDROP hdrop = static_cast(GlobalLock(stgmed.hGlobal));
+ UINT filesCount = DragQueryFileW(hdrop, 0xFFFFFFFF, nullptr, 0);
+ for (UINT i = 0; i < filesCount; i++)
+ {
+ if (DragQueryFileW(hdrop, i, item, MAX_PATH) != 0)
+ {
+ _data.Add(String(item));
+ }
+ }
+ GlobalUnlock(stgmed.hGlobal);
+ ReleaseStgMedium(&stgmed);
+ }
+ }
+ }
+ }
+
+public:
+ // [IGuiData]
+ Type GetType() const override
+ {
+ return _type;
+ }
+ String GetAsText() const override
+ {
+ String result;
+ if (_type == Type::Text)
+ {
+ result = _data[0];
+ }
+ return result;
+ }
+ void GetAsFiles(Array* files) const override
+ {
+ if (_type == Type::Files)
+ {
+ files->Add(_data);
+ }
+ }
+};
+
+///
+/// Tool class for Windows Ole support
+///
+class WindowsEnumFormatEtc : public IEnumFORMATETC
+{
+private:
+ ULONG _refCount;
+ ULONG _index;
+ ULONG _formatsCount;
+ FORMATETC* _formatEtc;
+
+public:
+ WindowsEnumFormatEtc(FORMATETC* pFormatEtc, int32 nNumFormats)
+ : _refCount(1)
+ , _index(0)
+ , _formatsCount(nNumFormats)
+ , _formatEtc(nullptr)
+ {
+ // Allocate memory
+ _formatEtc = new FORMATETC[nNumFormats];
+
+ // Copy the FORMATETC structures
+ for (int32 i = 0; i < nNumFormats; i++)
+ {
+ DeepCopyFormatEtc(&_formatEtc[i], &pFormatEtc[i]);
+ }
+ }
+
+ ~WindowsEnumFormatEtc()
+ {
+ if (_formatEtc)
+ {
+ for (uint32 i = 0; i < _formatsCount; i++)
+ {
+ if (_formatEtc[i].ptd)
+ {
+ CoTaskMemFree(_formatEtc[i].ptd);
+ }
+ }
+
+ delete[] _formatEtc;
+ }
+ }
+
+public:
+ HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, _COM_Outptr_ void __RPC_FAR* __RPC_FAR* ppvObject) override
+ {
+ // Check to see what interface has been requested
+ if (riid == IID_IEnumFORMATETC || riid == IID_IUnknown)
+ {
+ AddRef();
+ *ppvObject = this;
+ return S_OK;
+ }
+
+ // No interface
+ *ppvObject = nullptr;
+ return E_NOINTERFACE;
+ }
+ ULONG STDMETHODCALLTYPE AddRef() override
+ {
+ _InterlockedIncrement(&_refCount);
+ return _refCount;
+ }
+ ULONG STDMETHODCALLTYPE Release() override
+ {
+ ULONG ulRefCount = _InterlockedDecrement(&_refCount);
+ if (_refCount == 0)
+ {
+ delete this;
+ }
+ return ulRefCount;
+ }
+
+ // [IEnumFormatEtc]
+ HRESULT STDMETHODCALLTYPE Next(ULONG celt, FORMATETC* pFormatEtc, ULONG* pceltFetched) override
+ {
+ ULONG copied = 0;
+
+ // validate arguments
+ if (celt == 0 || pFormatEtc == nullptr)
+ return E_INVALIDARG;
+
+ // copy FORMATETC structures into caller's buffer
+ while (_index < _formatsCount && copied < celt)
+ {
+ DeepCopyFormatEtc(&pFormatEtc[copied], &_formatEtc[_index]);
+ copied++;
+ _index++;
+ }
+
+ // store result
+ if (pceltFetched != nullptr)
+ *pceltFetched = copied;
+
+ // did we copy all that was requested?
+ return (copied == celt) ? S_OK : S_FALSE;
+ }
+ HRESULT STDMETHODCALLTYPE Skip(ULONG celt) override
+ {
+ _index += celt;
+ return (_index <= _formatsCount) ? S_OK : S_FALSE;
+ }
+ HRESULT STDMETHODCALLTYPE Reset() override
+ {
+ _index = 0;
+ return S_OK;
+ }
+ HRESULT STDMETHODCALLTYPE Clone(IEnumFORMATETC** ppEnumFormatEtc) override
+ {
+ HRESULT result;
+
+ // Make a duplicate enumerator
+ result = CreateEnumFormatEtc(_formatsCount, _formatEtc, ppEnumFormatEtc);
+
+ if (result == S_OK)
+ {
+ // Manually set the index state
+ static_cast(*ppEnumFormatEtc)->_index = _index;
+ }
+
+ return result;
+ }
+};
+
+HRESULT CreateEnumFormatEtc(UINT nNumFormats, FORMATETC* pFormatEtc, IEnumFORMATETC** ppEnumFormatEtc)
+{
+ if (nNumFormats == 0 || pFormatEtc == nullptr || ppEnumFormatEtc == nullptr)
+ return E_INVALIDARG;
+ *ppEnumFormatEtc = new WindowsEnumFormatEtc(pFormatEtc, nNumFormats);
+ return *ppEnumFormatEtc ? S_OK : E_OUTOFMEMORY;
+}
+
+///
+/// Drag drop source and data container for Ole
+///
+class WindowsDragSource : public IDataObject, public IDropSource
+{
+private:
+ ULONG _refCount;
+ int32 _formatsCount;
+ FORMATETC* _formatEtc;
+ STGMEDIUM* _stgMedium;
+
+public:
+ WindowsDragSource(FORMATETC* fmtetc, STGMEDIUM* stgmed, int32 count)
+ : _refCount(1)
+ , _formatsCount(count)
+ , _formatEtc(nullptr)
+ , _stgMedium(nullptr)
+ {
+ // Allocate memory
+ _formatEtc = new FORMATETC[count];
+ _stgMedium = new STGMEDIUM[count];
+
+ // Copy descriptors
+ for (int32 i = 0; i < count; i++)
+ {
+ _formatEtc[i] = fmtetc[i];
+ _stgMedium[i] = stgmed[i];
+ }
+ }
+
+ virtual ~WindowsDragSource()
+ {
+ delete[] _formatEtc;
+ delete[] _stgMedium;
+ }
+
+public:
+ // [IUnknown]
+ HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, _COM_Outptr_ void __RPC_FAR* __RPC_FAR* ppvObject) override
+ {
+ // Check to see what interface has been requested
+ if (riid == IID_IDataObject || riid == IID_IUnknown || riid == IID_IDropSource)
+ {
+ AddRef();
+ *ppvObject = this;
+ return S_OK;
+ }
+
+ // No interface
+ *ppvObject = nullptr;
+ return E_NOINTERFACE;
+ }
+
+ ULONG STDMETHODCALLTYPE AddRef() override
+ {
+ _InterlockedIncrement(&_refCount);
+ return _refCount;
+ }
+
+ ULONG STDMETHODCALLTYPE Release() override
+ {
+ ULONG ulRefCount = _InterlockedDecrement(&_refCount);
+ if (_refCount == 0)
+ {
+ delete this;
+ }
+ return ulRefCount;
+ }
+
+ // [IDropSource]
+ HRESULT STDMETHODCALLTYPE QueryContinueDrag(_In_ BOOL fEscapePressed, _In_ DWORD grfKeyState) override
+ {
+ // If the Escape key has been pressed since the last call, cancel the drop
+ if (fEscapePressed == TRUE || grfKeyState & MK_RBUTTON)
+ return DRAGDROP_S_CANCEL;
+
+ // If the LeftMouse button has been released, then do the drop!
+ if ((grfKeyState & MK_LBUTTON) == 0)
+ return DRAGDROP_S_DROP;
+
+ // Continue with the drag-drop
+ return S_OK;
+ }
+ HRESULT STDMETHODCALLTYPE GiveFeedback(_In_ DWORD dwEffect) override
+ {
+ // TODO: allow to use custom mouse cursor during drop and drag operation
+ return DRAGDROP_S_USEDEFAULTCURSORS;
+ }
+
+ // [IDataObject]
+ HRESULT STDMETHODCALLTYPE GetData(_In_ FORMATETC* pformatetcIn, _Out_ STGMEDIUM* pmedium) override
+ {
+ if (pformatetcIn == nullptr || pmedium == nullptr)
+ return E_INVALIDARG;
+
+ // Try to match the specified FORMATETC with one of our supported formats
+ int32 index = lookupFormatEtc(pformatetcIn);
+ if (index == INVALID_INDEX)
+ return DV_E_FORMATETC;
+
+ // Found a match - transfer data into supplied storage medium
+ pmedium->tymed = _formatEtc[index].tymed;
+ pmedium->pUnkForRelease = nullptr;
+
+ // Copy the data into the caller's storage medium
+ switch (_formatEtc[index].tymed)
+ {
+ case TYMED_HGLOBAL:
+ pmedium->hGlobal = duplicateGlobalMem(_stgMedium[index].hGlobal);
+ break;
+
+ default:
+ return DV_E_FORMATETC;
+ }
+
+ return S_OK;
+ }
+ HRESULT STDMETHODCALLTYPE GetDataHere(_In_ FORMATETC* pformatetc, _Inout_ STGMEDIUM* pmedium) override
+ {
+ return DATA_E_FORMATETC;
+ }
+ HRESULT STDMETHODCALLTYPE QueryGetData(__RPC__in_opt FORMATETC* pformatetc) override
+ {
+ return lookupFormatEtc(pformatetc) == INVALID_INDEX ? DV_E_FORMATETC : S_OK;
+ }
+ HRESULT STDMETHODCALLTYPE GetCanonicalFormatEtc(__RPC__in_opt FORMATETC* pformatectIn, __RPC__out FORMATETC* pformatetcOut) override
+ {
+ // Apparently we have to set this field to NULL even though we don't do anything else
+ pformatetcOut->ptd = nullptr;
+ return E_NOTIMPL;
+ }
+ HRESULT STDMETHODCALLTYPE SetData(_In_ FORMATETC* pformatetc, _In_ STGMEDIUM* pmedium, BOOL fRelease) override
+ {
+ return E_NOTIMPL;
+ }
+ HRESULT STDMETHODCALLTYPE EnumFormatEtc(DWORD dwDirection, __RPC__deref_out_opt IEnumFORMATETC** ppenumFormatEtc) override
+ {
+ // Only the get direction is supported for OLE
+ if (dwDirection == DATADIR_GET)
+ {
+ // TODO: use SHCreateStdEnumFmtEtc API call
+ return CreateEnumFormatEtc(_formatsCount, _formatEtc, ppenumFormatEtc);
+ }
+
+ // The direction specified is not supported for drag+drop
+ return E_NOTIMPL;
+ }
+ HRESULT STDMETHODCALLTYPE DAdvise(__RPC__in FORMATETC* pformatetc, DWORD advf, __RPC__in_opt IAdviseSink* pAdvSink, __RPC__out DWORD* pdwConnection) override
+ {
+ return OLE_E_ADVISENOTSUPPORTED;
+ }
+ HRESULT STDMETHODCALLTYPE DUnadvise(DWORD dwConnection) override
+ {
+ return OLE_E_ADVISENOTSUPPORTED;
+ }
+ HRESULT STDMETHODCALLTYPE EnumDAdvise(__RPC__deref_out_opt IEnumSTATDATA** ppenumAdvise) override
+ {
+ return OLE_E_ADVISENOTSUPPORTED;
+ }
+
+private:
+ int32 lookupFormatEtc(FORMATETC* pFormatEtc) const
+ {
+ // Check each of our formats in turn to see if one matches
+ for (int32 i = 0; i < _formatsCount; i++)
+ {
+ if ((_formatEtc[i].tymed & pFormatEtc->tymed) &&
+ _formatEtc[i].cfFormat == pFormatEtc->cfFormat &&
+ _formatEtc[i].dwAspect == pFormatEtc->dwAspect)
+ {
+ // Return index of stored format
+ return i;
+ }
+ }
+
+ // Format not found
+ return INVALID_INDEX;
+ }
+};
+
+WindowsGuiData GuiDragDropData;
+
+DragDropEffect Window::DoDragDrop(const StringView& data)
+{
+ // Create background worker that will keep updating GUI (perform rendering)
+ const auto task = New();
+ Task::StartNew(task);
+ while (task->GetState() == TaskState::Queued)
+ Platform::Sleep(1);
+
+ // Create descriptors
+ FORMATETC fmtetc = { CF_TEXT, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
+ STGMEDIUM stgmed = { TYMED_HGLOBAL, { nullptr }, nullptr };
+
+ // Create a HGLOBAL inside the storage medium
+ stgmed.hGlobal = StringToHandle(data);
+
+ // Create drop source
+ auto dropSource = new WindowsDragSource(&fmtetc, &stgmed, 1);
+
+ // Do the drag drop operation
+ DWORD dwEffect;
+ HRESULT result = ::DoDragDrop(dropSource, dropSource, DROPEFFECT_COPY | DROPEFFECT_MOVE | DROPEFFECT_LINK | DROPEFFECT_SCROLL, &dwEffect);
+
+ // Wait for job end
+ Platform::AtomicStore(&task->ExitFlag, 1);
+ task->Wait();
+
+ // Release allocated data
+ dropSource->Release();
+ ReleaseStgMedium(&stgmed);
+
+ // Fix hanging mouse state (Windows doesn't send WM_LBUTTONUP when we end the drag and drop)
+ if (Input::GetMouseButton(MouseButton::Left))
+ {
+ ::POINT point;
+ ::GetCursorPos(&point);
+#if PLATFORM_SDL
+ // Reset the internal button state in SDL
+ SendMessageA((HWND)_handle, WM_LBUTTONUP, 0, MAKELPARAM(point.x, point.y));
+#else
+ Input::Mouse->OnMouseUp(Float2((float)point.x, (float)point.y), MouseButton::Left, this);
+#endif
+ }
+
+ return SUCCEEDED(result) ? dropEffectFromOleEnum(dwEffect) : DragDropEffect::None;
+}
+
+HRESULT Window::DragEnter(Windows::IDataObject* pDataObj, Windows::DWORD grfKeyState, Windows::POINTL pt, Windows::DWORD* pdwEffect)
+{
+ POINT p = { pt.x, pt.y };
+ ::ScreenToClient((HWND)_handle, &p);
+ GuiDragDropData.Init((IDataObject*)pDataObj);
+ DragDropEffect effect = DragDropEffect::None;
+ OnDragEnter(&GuiDragDropData, Float2(static_cast(p.x), static_cast(p.y)), effect);
+ Focus();
+ *pdwEffect = dropEffect2OleEnum(effect);
+ return S_OK;
+}
+
+HRESULT Window::DragOver(Windows::DWORD grfKeyState, Windows::POINTL pt, Windows::DWORD* pdwEffect)
+{
+ POINT p = { pt.x, pt.y };
+ ::ScreenToClient((HWND)_handle, &p);
+ DragDropEffect effect = DragDropEffect::None;
+ OnDragOver(&GuiDragDropData, Float2(static_cast(p.x), static_cast(p.y)), effect);
+ *pdwEffect = dropEffect2OleEnum(effect);
+ return S_OK;
+}
+
+HRESULT Window::DragLeave()
+{
+ OnDragLeave();
+ return S_OK;
+}
+
+HRESULT Window::Drop(Windows::IDataObject* pDataObj, Windows::DWORD grfKeyState, Windows::POINTL pt, Windows::DWORD* pdwEffect)
+{
+ POINT p = { pt.x, pt.y };
+ ::ScreenToClient((HWND)_handle, &p);
+ GuiDragDropData.Init((IDataObject*)pDataObj);
+ DragDropEffect effect = DragDropEffect::None;
+ OnDragDrop(&GuiDragDropData, Float2(static_cast(p.x), static_cast(p.y)), effect);
+ *pdwEffect = dropEffect2OleEnum(effect);
+ return S_OK;
+}
+
+#else
+
+DragDropEffect Window::DoDragDrop(const StringView& data)
+{
+ return DragDropEffect::None;
+}
+
+#endif
+
+#endif
diff --git a/Source/Engine/Platform/Windows/WindowsWindow.cpp b/Source/Engine/Platform/Windows/WindowsWindow.cpp
index 9b9a8a670..d3d18305a 100644
--- a/Source/Engine/Platform/Windows/WindowsWindow.cpp
+++ b/Source/Engine/Platform/Windows/WindowsWindow.cpp
@@ -1,6 +1,6 @@
// Copyright (c) Wojciech Figat. All rights reserved.
-#if PLATFORM_WINDOWS
+#if PLATFORM_WINDOWS && !PLATFORM_SDL
#include "WindowsWindow.h"
#include "WindowsPlatform.h"
@@ -10,13 +10,6 @@
#include "Engine/Graphics/GPUSwapChain.h"
#include "Engine/Graphics/RenderTask.h"
#include "Engine/Graphics/GPUDevice.h"
-#if USE_EDITOR
-#include "Engine/Core/Collections/Array.h"
-#include "Engine/Platform/IGuiData.h"
-#include "Engine/Platform/Base/DragDropHelper.h"
-#include "Engine/Input/Input.h"
-#include "Engine/Input/Mouse.h"
-#endif
#include "../Win32/IncludeWindowsHeaders.h"
#include
#if USE_EDITOR
@@ -118,7 +111,7 @@ WindowsWindow::WindowsWindow(const CreateWindowSettings& settings)
if (settings.IsRegularWindow)
style |= WS_BORDER | WS_CAPTION | WS_DLGFRAME | WS_SYSMENU | WS_THICKFRAME | WS_GROUP;
#elif WINDOWS_USE_NEWER_BORDER_LESS
- if (settings.IsRegularWindow)
+ if (settings.Type == WindowType::Regular)
style |= WS_THICKFRAME | WS_SYSMENU | WS_CAPTION;
#endif
exStyle |= WS_EX_WINDOWEDGE;
@@ -127,7 +120,7 @@ WindowsWindow::WindowsWindow(const CreateWindowSettings& settings)
// Creating the window
_handle = CreateWindowExW(
exStyle,
- Platform::ApplicationWindowClass,
+ Platform::ApplicationClassName,
settings.Title.GetText(),
style,
x,
@@ -162,7 +155,7 @@ WindowsWindow::WindowsWindow(const CreateWindowSettings& settings)
#if WINDOWS_USE_NEWER_BORDER_LESS
// Enable shadow
- if (_settings.IsRegularWindow && !_settings.HasBorder && IsCompositionEnabled())
+ if (settings.Type == WindowType::Regular && !_settings.HasBorder && IsCompositionEnabled())
{
const int margin[4] = { 1, 1, 1, 1 };
::DwmExtendFrameIntoClientArea(_handle, margin);
@@ -295,7 +288,7 @@ void WindowsWindow::SetBorderless(bool isBorderless, bool maximized)
if (_settings.IsRegularWindow)
style |= WS_BORDER | WS_CAPTION | WS_DLGFRAME | WS_SYSMENU | WS_THICKFRAME | WS_GROUP;
#elif WINDOWS_USE_NEWER_BORDER_LESS
- if (_settings.IsRegularWindow)
+ if (_settings.Type == WindowType::Regular)
lStyle |= WS_THICKFRAME | WS_SYSMENU;
#endif
@@ -371,7 +364,7 @@ void WindowsWindow::BringToFront(bool force)
{
ASSERT(HasHWND());
- if (_settings.IsRegularWindow)
+ if (_settings.Type == WindowType::Regular)
{
if (IsIconic(_handle))
{
@@ -698,36 +691,6 @@ void WindowsWindow::SetCursor(CursorType type)
UpdateCursor();
}
-#if USE_EDITOR
-
-Windows::HRESULT WindowsWindow::QueryInterface(const Windows::IID& id, void** ppvObject)
-{
- // Check to see what interface has been requested
- if ((const IID&)id == IID_IUnknown || (const IID&)id == IID_IDropTarget)
- {
- AddRef();
- *ppvObject = this;
- return S_OK;
- }
-
- // No interface
- *ppvObject = nullptr;
- return E_NOINTERFACE;
-}
-
-Windows::ULONG WindowsWindow::AddRef()
-{
- _InterlockedIncrement(&_refCount);
- return _refCount;
-}
-
-Windows::ULONG WindowsWindow::Release()
-{
- return _InterlockedDecrement(&_refCount);
-}
-
-#endif
-
void WindowsWindow::CheckForWindowResize()
{
// Skip for minimized window (GetClientRect for minimized window returns 0)
@@ -1345,617 +1308,11 @@ LRESULT WindowsWindow::WndProc(UINT msg, WPARAM wParam, LPARAM lParam)
if (_settings.AllowInput)
{
- if (WindowsInput::WndProc(this, msg, wParam, lParam))
+ if (WindowsInput::WndProc((Window*)this, msg, wParam, lParam))
return true;
}
return DefWindowProc(_handle, msg, wParam, lParam);
}
-#if USE_EDITOR
-
-HGLOBAL duplicateGlobalMem(HGLOBAL hMem)
-{
- auto len = GlobalSize(hMem);
- auto source = GlobalLock(hMem);
- auto dest = GlobalAlloc(GMEM_FIXED, len);
- Platform::MemoryCopy(dest, source, len);
- GlobalUnlock(hMem);
- return dest;
-}
-
-DWORD dropEffect2OleEnum(DragDropEffect effect)
-{
- DWORD result;
- switch (effect)
- {
- case DragDropEffect::None:
- result = DROPEFFECT_NONE;
- break;
- case DragDropEffect::Copy:
- result = DROPEFFECT_COPY;
- break;
- case DragDropEffect::Move:
- result = DROPEFFECT_MOVE;
- break;
- case DragDropEffect::Link:
- result = DROPEFFECT_LINK;
- break;
- default:
- result = DROPEFFECT_NONE;
- break;
- }
- return result;
-}
-
-DragDropEffect dropEffectFromOleEnum(DWORD effect)
-{
- DragDropEffect result;
- switch (effect)
- {
- case DROPEFFECT_NONE:
- result = DragDropEffect::None;
- break;
- case DROPEFFECT_COPY:
- result = DragDropEffect::Copy;
- break;
- case DROPEFFECT_MOVE:
- result = DragDropEffect::Move;
- break;
- case DROPEFFECT_LINK:
- result = DragDropEffect::Link;
- break;
- default:
- result = DragDropEffect::None;
- break;
- }
- return result;
-}
-
-HANDLE StringToHandle(const StringView& str)
-{
- // Allocate and lock a global memory buffer.
- // Make it fixed data so we don't have to use GlobalLock
- const int32 length = str.Length();
- char* ptr = static_cast