Skip to content

Commit

Permalink
feat: INativeOverlappedPresenter X11 implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
ramezgerges committed Mar 7, 2024
1 parent 232355f commit acd6433
Show file tree
Hide file tree
Showing 6 changed files with 315 additions and 52 deletions.
39 changes: 0 additions & 39 deletions src/Uno.UI.Runtime.Skia.X11/X11ApplicationViewExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
152 changes: 152 additions & 0 deletions src/Uno.UI.Runtime.Skia.X11/X11NativeOverlappedPresenter.cs
Original file line number Diff line number Diff line change
@@ -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<IntPtr>();
}

Debug.Assert(actual_format == 32);
var span = new Span<IntPtr>(prop.ToPointer(), (int)nItems);

return span.ToArray();
}
}
27 changes: 26 additions & 1 deletion src/Uno.UI.Runtime.Skia.X11/X11WindowWrapper.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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));
}
}
13 changes: 1 addition & 12 deletions src/Uno.UI.Runtime.Skia.X11/X11XamlRootHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading

0 comments on commit acd6433

Please sign in to comment.