From acd64338c41bad01141619ab957a844b43e0f049 Mon Sep 17 00:00:00 2001 From: Ramez Ragaa Date: Thu, 7 Mar 2024 15:46:07 +0000 Subject: [PATCH] feat: INativeOverlappedPresenter X11 implementation --- .../X11ApplicationViewExtension.cs | 39 ----- .../X11NativeOverlappedPresenter.cs | 152 ++++++++++++++++++ .../X11WindowWrapper.cs | 27 +++- .../X11XamlRootHost.cs | 13 +- .../X11_Bindings/X11Helper.cs | 133 +++++++++++++++ .../X11_Bindings/x11bindings_XLib.cs | 3 + 6 files changed, 315 insertions(+), 52 deletions(-) create mode 100644 src/Uno.UI.Runtime.Skia.X11/X11NativeOverlappedPresenter.cs diff --git a/src/Uno.UI.Runtime.Skia.X11/X11ApplicationViewExtension.cs b/src/Uno.UI.Runtime.Skia.X11/X11ApplicationViewExtension.cs index 941cb0d5a172..5f41c71e35f9 100644 --- a/src/Uno.UI.Runtime.Skia.X11/X11ApplicationViewExtension.cs +++ b/src/Uno.UI.Runtime.Skia.X11/X11ApplicationViewExtension.cs @@ -8,45 +8,6 @@ internal class X11ApplicationViewExtension(object owner) : IApplicationViewExten { private readonly ApplicationView _owner = (ApplicationView)owner; - public void ExitFullScreenMode() => TrySetFullScreenMode(false); - - public bool TryEnterFullScreenMode() => TrySetFullScreenMode(true); - - private bool TrySetFullScreenMode(bool on) - { - if (X11Helper.XamlRootHostFromApplicationView(_owner, out var host)) - { - using var _1 = X11Helper.XLock(host.X11Window.Display); - - IntPtr wm_state = X11Helper.GetAtom(host.X11Window.Display, X11Helper._NET_WM_STATE); - IntPtr wm_fullscreen = X11Helper.GetAtom(host.X11Window.Display, X11Helper._NET_WM_STATE_FULLSCREEN); - - if (wm_state == X11Helper.None || wm_fullscreen == X11Helper.None) - { - return false; - } - - // https://stackoverflow.com/a/28396773 - XClientMessageEvent xclient = default; - xclient.type = XEventName.ClientMessage; - xclient.window = host.X11Window.Window; - xclient.message_type = wm_state; - xclient.format = 32; - xclient.ptr1 = on ? 1 : 0; - xclient.ptr2 = wm_fullscreen; - xclient.ptr3 = 0; - xclient.ptr4 = 0; - xclient.ptr5 = 0; - - XEvent xev = default; - xev.ClientMessageEvent = xclient; - var _2 = XLib.XSendEvent(host.X11Window.Display, XLib.XDefaultRootWindow(host.X11Window.Display), false, (IntPtr)(XEventMask.SubstructureRedirectMask | XEventMask.SubstructureNotifyMask), ref xev); - var _3 = XLib.XFlush(host.X11Window.Display); - } - - return true; - } - public bool TryResizeView(Size size) { if (X11Helper.XamlRootHostFromApplicationView(_owner, out var host)) diff --git a/src/Uno.UI.Runtime.Skia.X11/X11NativeOverlappedPresenter.cs b/src/Uno.UI.Runtime.Skia.X11/X11NativeOverlappedPresenter.cs new file mode 100644 index 000000000000..de961651d63c --- /dev/null +++ b/src/Uno.UI.Runtime.Skia.X11/X11NativeOverlappedPresenter.cs @@ -0,0 +1,152 @@ +using System; +using System.Diagnostics; +using System.Linq; +using Microsoft.UI.Windowing; +using Microsoft.UI.Windowing.Native; +using Uno.Disposables; +using Uno.Foundation.Logging; +namespace Uno.WinUI.Runtime.Skia.X11; + +internal class X11NativeOverlappedPresenter(X11Window x11Window, X11WindowWrapper wrapper): INativeOverlappedPresenter +{ + // EWMH has _NET_WM_ALLOWED_ACTIONS: https://specifications.freedesktop.org/wm-spec/wm-spec-1.3.html#idm45912237317440 + // but it turns out that these shouldn't be set by the client, but only read to see what actions are available. + // Setting them doesn't really do anything. + // There is also this from ICCCM, but I don't think people use this anymore: + // https://specifications.freedesktop.org/wm-spec/wm-spec-1.3.html#NORESIZE + // What works is using the Motif WM hints, which aren't standardized or documented anywhere + // https://stackoverflow.com/a/13788970 + + // This doesn't prevent resizing using xlib calls (e.g. XResizeWindow), so settings the size ApplicationView for example would still work. + public void SetIsResizable(bool isResizable) => X11Helper.SetMotifWMFunctions(x11Window, isResizable, (IntPtr)MotifFunctions.Resize); + + public void SetIsModal(bool isModal) { } + + // Making the window unminimizable removes the `-` button in the title bar and greys out the `Minimize` option if + // you open the Menu, but the window will still be minimizable if you click on the window icon in the dock/task bar. + // This is at least what happens on XFCE. Since these are just "hints", each WM can choose what it means to be "minimizable" differently. + public void SetIsMinimizable(bool isMinimizable) => X11Helper.SetMotifWMFunctions(x11Window, isMinimizable, (IntPtr)MotifFunctions.Minimize); + + public void SetIsMaximizable(bool isMaximizable) => X11Helper.SetMotifWMFunctions(x11Window, isMaximizable, (IntPtr)MotifFunctions.Maximize); + + public void SetIsAlwaysOnTop(bool isAlwaysOnTop) + { + X11Helper.SetWMHints( + x11Window, + X11Helper.GetAtom(x11Window.Display, X11Helper._NET_WM_STATE), + isAlwaysOnTop ? 1 : 0, + X11Helper.GetAtom(x11Window.Display, X11Helper._NET_WM_STATE_ABOVE)); + } + + public void Maximize() + { + X11Helper.SetWMHints( + x11Window, + X11Helper.GetAtom(x11Window.Display, X11Helper._NET_WM_STATE), + 1, + X11Helper.GetAtom(x11Window.Display, X11Helper._NET_WM_STATE_MAXIMIZED_HORZ), + X11Helper.GetAtom(x11Window.Display, X11Helper._NET_WM_STATE_MAXIMIZED_VERT)); + } + + public void Minimize(bool activateWindow) + { + using var _1 = X11Helper.XLock(x11Window.Display); + + // Minimizing while in full screen could be buggy depending on the implementation + // https://stackoverflow.com/questions/6381098/minimize-fullscreen-xlib-opengl-window + wrapper.SetFullScreenMode(false); + + // XLib.XScreenNumberOfScreen(x11Window.Display, screen) is buggy. We use the default screen instead (which should be fine for 99% of cases) + var _3 = XLib.XIconifyWindow(x11Window.Display, x11Window.Window, XLib.XDefaultScreen(x11Window.Display)); + var _4 = XLib.XFlush(x11Window.Display); + } + + public void SetBorderAndTitleBar(bool hasBorder, bool hasTitleBar) + { + // Border doesn't seem to do anything, which is fine for now, since it doesn't do anything on WinUI either. + X11Helper.SetMotifWMDecorations(x11Window, hasBorder, (IntPtr)MotifDecorations.Border); + X11Helper.SetMotifWMDecorations(x11Window, hasTitleBar, (IntPtr)MotifDecorations.Title); + } + + public void Restore(bool activateWindow) + { + // https://stackoverflow.com/a/30256233 + using var _1 = X11Helper.XLock(x11Window.Display); + + var shouldActivate = activateWindow; + shouldActivate |= GetWMState().Contains(X11Helper.GetAtom(x11Window.Display, X11Helper._NET_WM_STATE_HIDDEN)); + if (!shouldActivate) + { + XWindowAttributes attributes = default; + var _2 = XLib.XGetWindowAttributes(x11Window.Display, x11Window.Window, ref attributes); + shouldActivate = attributes.map_state == MapState.IsUnmapped; + } + + if (shouldActivate) + { + wrapper.Activate(); + } + } + + public OverlappedPresenterState State + { + get + { + using var _1 = X11Helper.XLock(x11Window.Display); + + var minimized = X11Helper.GetAtom(x11Window.Display, X11Helper._NET_WM_STATE_HIDDEN); + var maximizedHorizontal = X11Helper.GetAtom(x11Window.Display, X11Helper._NET_WM_STATE_MAXIMIZED_HORZ); + var maximizedVertical = X11Helper.GetAtom(x11Window.Display, X11Helper._NET_WM_STATE_MAXIMIZED_VERT); + + foreach (var atom in GetWMState()) + { + if (atom == minimized) + { + return OverlappedPresenterState.Minimized; + } + else if (atom == maximizedHorizontal || atom == maximizedVertical) // maybe should we require both to be considered "maximized"? + { + return OverlappedPresenterState.Maximized; + } + } + + return OverlappedPresenterState.Restored; + } + } + + private unsafe IntPtr[] GetWMState() + { + using var _1 = X11Helper.XLock(x11Window.Display); + + var _2 = XLib.XGetWindowProperty( + x11Window.Display, + x11Window.Window, + X11Helper.GetAtom(x11Window.Display, X11Helper._NET_WM_STATE), + 0, + X11Helper.LONG_LENGTH, + false, + X11Helper.AnyPropertyType, + out IntPtr actualType, + out int actual_format, + out IntPtr nItems, + out _, + out IntPtr prop); + + using var _3 = Disposable.Create(() => XLib.XFree(prop)); + + if (actualType == X11Helper.None) + { + if (this.Log().IsEnabled(LogLevel.Error)) + { + this.Log().Error($"Couldn't get {nameof(OverlappedPresenterState)}: {X11Helper._NET_WM_STATE} does not exist on the window. Make sure you use an EWMH-compliant WM."); + } + + return Array.Empty(); + } + + Debug.Assert(actual_format == 32); + var span = new Span(prop.ToPointer(), (int)nItems); + + return span.ToArray(); + } +} diff --git a/src/Uno.UI.Runtime.Skia.X11/X11WindowWrapper.cs b/src/Uno.UI.Runtime.Skia.X11/X11WindowWrapper.cs index afc5d561b50f..d831f2d59f89 100644 --- a/src/Uno.UI.Runtime.Skia.X11/X11WindowWrapper.cs +++ b/src/Uno.UI.Runtime.Skia.X11/X11WindowWrapper.cs @@ -1,10 +1,13 @@ -using System.Collections.Concurrent; +using System; +using System.Collections.Concurrent; using System.Globalization; using Uno.UI.Xaml.Controls; using Windows.Foundation; using Windows.UI.Core; using Windows.UI.Core.Preview; +using Microsoft.UI.Windowing; using Microsoft.UI.Xaml; +using Uno.Disposables; using Uno.Foundation.Logging; namespace Uno.WinUI.Runtime.Skia.X11; @@ -129,4 +132,26 @@ protected override void ShowCore() // XLib.XMapWindow(x11Window.Display, x11Window.Window); // } } + + protected override IDisposable ApplyOverlappedPresenter(OverlappedPresenter presenter) + { + presenter.SetNative(new X11NativeOverlappedPresenter(_host.X11Window, this)); + return Disposable.Create(() => presenter.SetNative(null)); + } + + protected override IDisposable ApplyFullScreenPresenter() + { + SetFullScreenMode(true); + + return Disposable.Create(() => SetFullScreenMode(false)); + } + + internal void SetFullScreenMode(bool on) + { + X11Helper.SetWMHints( + _host.X11Window, + X11Helper.GetAtom(_host.X11Window.Display, X11Helper._NET_WM_STATE), + on ? 1 : 0, + X11Helper.GetAtom(_host.X11Window.Display, X11Helper._NET_WM_STATE_FULLSCREEN)); + } } diff --git a/src/Uno.UI.Runtime.Skia.X11/X11XamlRootHost.cs b/src/Uno.UI.Runtime.Skia.X11/X11XamlRootHost.cs index ae877da57b0c..3288a16e7c95 100644 --- a/src/Uno.UI.Runtime.Skia.X11/X11XamlRootHost.cs +++ b/src/Uno.UI.Runtime.Skia.X11/X11XamlRootHost.cs @@ -97,18 +97,7 @@ internal void UpdateWindowPropertiesFromCoreApplication() { var coreApplicationView = CoreApplication.GetCurrentView(); - // Sadly, there is no de jure standard for this. It's basically a set of hints used - // in the old Motif WM. Other WMs started using it and it became a thing. - var hintsAtom = X11Helper.GetAtom(X11Window.Display, X11Helper._MOTIF_WM_HINTS); - var _ = XLib.XChangeProperty( - X11Window.Display, - X11Window.Window, - hintsAtom, - hintsAtom, - 32, - PropertyMode.Replace, - new[] { (IntPtr)MotifFlags.Decorations, 0, coreApplicationView.TitleBar.ExtendViewIntoTitleBar ? 0 : (IntPtr)MotifDecorations.All, 0, 0 }, - 5); + X11Helper.SetMotifWMDecorations(X11Window, !coreApplicationView.TitleBar.ExtendViewIntoTitleBar, 0xFF); } private void UpdateWindowPropertiesFromPackage() diff --git a/src/Uno.UI.Runtime.Skia.X11/X11_Bindings/X11Helper.cs b/src/Uno.UI.Runtime.Skia.X11/X11_Bindings/X11Helper.cs index cb9cfea96ebb..d3356df72a93 100644 --- a/src/Uno.UI.Runtime.Skia.X11/X11_Bindings/X11Helper.cs +++ b/src/Uno.UI.Runtime.Skia.X11/X11_Bindings/X11Helper.cs @@ -20,12 +20,15 @@ // SOFTWARE. using System; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Runtime.InteropServices; using System.Threading; using Windows.UI.ViewManagement; using Microsoft.UI.Windowing; using Microsoft.UI.Xaml; +using Uno.Disposables; namespace Uno.WinUI.Runtime.Skia.X11; @@ -43,8 +46,19 @@ internal static class X11Helper public static readonly IntPtr PropertyNewValue = IntPtr.Zero; public const string WM_DELETE_WINDOW = "WM_DELETE_WINDOW"; + public const string _NET_ACTIVE_WINDOW = "_NET_ACTIVE_WINDOW"; public const string _NET_WM_STATE = "_NET_WM_STATE"; public const string _NET_WM_STATE_FULLSCREEN = "_NET_WM_STATE_FULLSCREEN"; + public const string _NET_WM_STATE_ABOVE = "_NET_WM_STATE_ABOVE"; + public const string _NET_WM_STATE_HIDDEN = "_NET_WM_STATE_HIDDEN"; + public const string _NET_WM_STATE_MAXIMIZED_HORZ = "_NET_WM_STATE_MAXIMIZED_HORZ"; + public const string _NET_WM_STATE_MAXIMIZED_VERT = "_NET_WM_STATE_MAXIMIZED_VERT"; + public const string _NET_WM_ALLOWED_ACTIONS = "_NET_WM_ALLOWED_ACTIONS"; + public const string _NET_WM_ACTION_MOVE = "_NET_WM_ACTION_MOVE"; + public const string _NET_WM_ACTION_RESIZE = "_NET_WM_ACTION_RESIZE"; + public const string _NET_WM_ACTION_MINIMIZE = "_NET_WM_ACTION_MINIMIZE"; + public const string _NET_WM_ACTION_MAXIMIZE_HORZ = "_NET_WM_ACTION_MAXIMIZE_HORZ"; + public const string _NET_WM_ACTION_MAXIMIZE_VERT = "_NET_WM_ACTION_MAXIMIZE_VERT"; public const string _NET_WM_ICON = "_NET_WM_ICON"; public const string _MOTIF_WM_HINTS = "_MOTIF_WM_HINTS"; public const string TIMESTAMP = "TIMESTAMP"; @@ -102,6 +116,125 @@ public static bool XamlRootHostFromDisplayInformation(Windows.Graphics.Display.D return false; } + public static void SetWMHints(X11Window x11Window, IntPtr message_type, IntPtr ptr1) + => SetWMHints(x11Window, message_type, ptr1, 0, 0, 0, 0); + + public static void SetWMHints(X11Window x11Window, IntPtr message_type, IntPtr ptr1, IntPtr ptr2) + => SetWMHints(x11Window, message_type, ptr1, ptr2, 0, 0, 0); + + public static void SetWMHints(X11Window x11Window, IntPtr message_type, IntPtr ptr1, IntPtr ptr2, IntPtr ptr3) + => SetWMHints(x11Window, message_type, ptr1, ptr2, ptr3, 0, 0); + + public static void SetWMHints(X11Window x11Window, IntPtr message_type, IntPtr ptr1, IntPtr ptr2, IntPtr ptr3, IntPtr ptr4) + => SetWMHints(x11Window, message_type, ptr1, ptr2, ptr3, ptr4, 0); + + public static void SetWMHints(X11Window x11Window, IntPtr message_type, IntPtr ptr1, IntPtr ptr2, IntPtr ptr3, IntPtr ptr4, IntPtr ptr5) + { + using var _1 = XLock(x11Window.Display); + + // https://stackoverflow.com/a/28396773 + XClientMessageEvent xclient = default; + xclient.send_event = 1; + xclient.type = XEventName.ClientMessage; + xclient.window = x11Window.Window; + xclient.message_type = message_type; + xclient.format = 32; + xclient.ptr1 = ptr1; + xclient.ptr2 = ptr2; + xclient.ptr3 = ptr3; + xclient.ptr4 = ptr4; + xclient.ptr5 = ptr5; + + XEvent xev = default; + xev.ClientMessageEvent = xclient; + var _2 = XLib.XSendEvent(x11Window.Display, XLib.XDefaultRootWindow(x11Window.Display), false, (IntPtr)(XEventMask.SubstructureRedirectMask | XEventMask.SubstructureNotifyMask), ref xev); + var _3 = XLib.XFlush(x11Window.Display); + } + + public static void SetMotifWMDecorations(X11Window x11Window, bool on, IntPtr decorations) + => SetMotifWMHints(x11Window, on, decorations, null); + + public static void SetMotifWMFunctions(X11Window x11Window, bool on, IntPtr functions) + => SetMotifWMHints(x11Window, on, null, functions); + + // Sadly, there is no de jure standard for this. It's basically a set of hints used + // in the old Motif WM. Other WMs started using it and it became a thing. + // https://stackoverflow.com/a/13788970 + private unsafe static void SetMotifWMHints(X11Window x11Window, bool on, IntPtr? decorations, IntPtr? functions) + { + using var _1 = XLock(x11Window.Display); + + var hintsAtom = GetAtom(x11Window.Display, _MOTIF_WM_HINTS); + var _2 = XLib.XGetWindowProperty( + x11Window.Display, + x11Window.Window, + hintsAtom, + 0, + LONG_LENGTH, + false, + AnyPropertyType, + out IntPtr actualType, + out int actual_format, + out IntPtr nItems, + out _, + out IntPtr prop); + + using var _3 = Disposable.Create(() => XLib.XFree(prop)); + + var arr = new IntPtr[5]; + if (actualType != None) + { + Debug.Assert(actual_format == 32 && nItems == 5); + new Span(prop.ToPointer(), 5).CopyTo(new Span(arr, 0, 5)); + } + + if (decorations is { } d) + { + arr[0] |= (IntPtr)MotifFlags.Decorations; + + if ((arr[2] & (IntPtr)MotifDecorations.All) != 0) + { + // Remove the All decoration to be able to turn each decoration or or off individually. + var allDecorations = (int[])Enum.GetValuesAsUnderlyingType(); + arr[2] |= allDecorations.Aggregate(0, (i1, i2) => i1 | i2); + arr[2] &= ~(IntPtr)MotifDecorations.All; + } + + if (on) + { + arr[2] |= d; + } + else + { + arr[2] &= ~d; + } + } + + if (functions is { } f) + { + arr[0] |= (IntPtr)MotifFlags.Functions; + if (on) + { + arr[1] |= f; + } + else + { + arr[1] &= ~f; + } + } + + var _4 = XLib.XChangeProperty( + x11Window.Display, + x11Window.Window, + hintsAtom, + hintsAtom, + 32, + PropertyMode.Replace, + arr, + 5); + var _5 = XLib.XFlush(x11Window.Display); + } + private static Func _getAtom = Funcs.CreateMemoized(XLib.XInternAtom); public static IntPtr GetAtom(IntPtr display, string name, bool only_if_exists = false) => _getAtom(display, name, only_if_exists); diff --git a/src/Uno.UI.Runtime.Skia.X11/X11_Bindings/x11bindings_XLib.cs b/src/Uno.UI.Runtime.Skia.X11/X11_Bindings/x11bindings_XLib.cs index 9798c314927a..dd4e382fa47f 100644 --- a/src/Uno.UI.Runtime.Skia.X11/X11_Bindings/x11bindings_XLib.cs +++ b/src/Uno.UI.Runtime.Skia.X11/X11_Bindings/x11bindings_XLib.cs @@ -128,6 +128,9 @@ public static string GetAtomName(IntPtr display, IntPtr atom) [DllImport(libX11)] public static extern int XSetWMProtocols(IntPtr display, IntPtr window, IntPtr[] protocols, int count); + [DllImport(libX11)] + public static extern int XIconifyWindow(IntPtr display, IntPtr window, int screen_number); + [DllImport(libX11)] public static extern bool XTranslateCoordinates(IntPtr display, IntPtr src_w, IntPtr dest_w, int src_x, int src_y, out int intdest_x_return, out int dest_y_return, out IntPtr child_return);