diff --git a/src/Uno.UI.Runtime.Skia.X11/X11ApplicationHost.cs b/src/Uno.UI.Runtime.Skia.X11/X11ApplicationHost.cs index 2dc922e2fa74..ae1e2492c5cc 100644 --- a/src/Uno.UI.Runtime.Skia.X11/X11ApplicationHost.cs +++ b/src/Uno.UI.Runtime.Skia.X11/X11ApplicationHost.cs @@ -16,6 +16,7 @@ using Uno.UI.Runtime.Skia; using System.Threading.Tasks; using System.Runtime.InteropServices.Marshalling; +using Microsoft.UI.Xaml.Controls; namespace Uno.WinUI.Runtime.Skia.X11; @@ -61,6 +62,8 @@ static X11ApplicationHost() ApiExtensibility.Register(typeof(IFileOpenPickerExtension), o => new LinuxFilePickerExtension(o)); ApiExtensibility.Register(typeof(IFolderPickerExtension), o => new LinuxFilePickerExtension(o)); ApiExtensibility.Register(typeof(IFileSavePickerExtension), o => new LinuxFileSaverExtension(o)); + + ApiExtensibility.Register(typeof(ContentPresenter.INativeElementHostingExtension), o => new X11NativeElementHostingExtension()); } public X11ApplicationHost(Func appBuilder) diff --git a/src/Uno.UI.Runtime.Skia.X11/X11NativeElementHostingExtension.cs b/src/Uno.UI.Runtime.Skia.X11/X11NativeElementHostingExtension.cs new file mode 100644 index 000000000000..3d2e3fbf014f --- /dev/null +++ b/src/Uno.UI.Runtime.Skia.X11/X11NativeElementHostingExtension.cs @@ -0,0 +1,339 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using Windows.Foundation; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +namespace Uno.WinUI.Runtime.Skia.X11; + +public class X11NativeElementHostingExtension : ContentPresenter.INativeElementHostingExtension +{ +#pragma warning disable CS0414 // Field is assigned but its value is never used + private static string SampleVideoLink = "https://uno-assets.platform.uno/tests/uno/big_buck_bunny_720p_5mb.mp4"; +#pragma warning restore CS0414 // Field is assigned but its value is never used + + private Rect? _lastArrangeRect; + + public bool IsNativeElement(object content) + { + if (content is not X11Window x11Window) + { + return false; + } + + using var _1 = X11Helper.XLock(x11Window.Display); + + var _3 = XLib.XQueryTree(x11Window.Display, XLib.XDefaultRootWindow(x11Window.Display), out IntPtr root, out _, out var children, out _); + XLib.XFree(children); + + // _NET_CLIENT_LIST only identifies top-level windows, not subwindows. + var status = XLib.XGetWindowProperty( + x11Window.Display, + root, + X11Helper.GetAtom(x11Window.Display, X11Helper._NET_CLIENT_LIST), + 0, + new IntPtr(0x7fffffff), + false, + X11Helper.AnyPropertyType, + out _, + out _, + out var length, + out IntPtr _, + out IntPtr windowArray); + + if (status == X11Helper.Success) + { + unsafe + { + var span = new Span(windowArray.ToPointer(), (int)length); + foreach (var window in span) + { + if (window == x11Window.Window) + { + return true; + } + } + } + } + + return FindWindowById(x11Window.Display, x11Window.Window, root) != IntPtr.Zero; + } + public void AttachNativeElement(XamlRoot owner, object content) + { + if (!IsNativeElementAttached(owner, content) + && content is X11Window x11Window + && owner is XamlRoot xamlRoot + && X11Manager.XamlRootMap.GetHostForRoot(xamlRoot) is X11XamlRootHost host) + { + + using var _1 = X11Helper.XLock(x11Window.Display); + + // this seems to be necessary or else the WM will keep detaching the subwindow + XWindowAttributes attributes = default; + var _2 = XLib.XGetWindowAttributes(x11Window.Display, x11Window.Window, ref attributes); + attributes.override_direct = /* True */ 1; + + unsafe + { + IntPtr attr = Marshal.AllocHGlobal(Marshal.SizeOf(attributes)); + Marshal.StructureToPtr(attributes, attr, false); + X11Helper.XChangeWindowAttributes(x11Window.Display, x11Window.Window, (IntPtr)XCreateWindowFlags.CWOverrideRedirect, (XSetWindowAttributes*)attr.ToPointer()); + Marshal.FreeHGlobal(attr); + } + + var _3 = X11Helper.XReparentWindow(x11Window.Display, x11Window.Window, host.X11Window.Window, 0, 0); + XLib.XSync(x11Window.Display, false); + } + } + public void DetachNativeElement(XamlRoot owner, object content) + { + if (IsNativeElementAttached(owner, content) + && content is X11Window x11Window) + { + using var _1 = X11Helper.XLock(x11Window.Display); + var _2 = XLib.XQueryTree(x11Window.Display, x11Window.Window, out IntPtr root, out _, out var children, out _); + XLib.XFree(children); + var _3 = X11Helper.XReparentWindow(x11Window.Display, x11Window.Window, root, 0, 0); + XLib.XSync(x11Window.Display, false); + } + } + public void ArrangeNativeElement(XamlRoot owner, object content, Rect arrangeRect, Rect? clipRect) + { + if (IsNativeElementAttached(owner, content) + && content is X11Window x11Window + && owner is XamlRoot xamlRoot + && X11Manager.XamlRootMap.GetHostForRoot(xamlRoot) is X11XamlRootHost host + && (int)arrangeRect.Width > 0 && (int)arrangeRect.Height > 0) + { + _lastArrangeRect = arrangeRect; + using var _1 = X11Helper.XLock(x11Window.Display); + var _2 = XLib.XResizeWindow(x11Window.Display, x11Window.Window, (int)arrangeRect.Width, (int)arrangeRect.Height); + var _3 = X11Helper.XMoveWindow(x11Window.Display, x11Window.Window, (int)arrangeRect.X, (int)arrangeRect.Y); + XLib.XSync(x11Window.Display, false); + } + } + public Size MeasureNativeElement(XamlRoot owner, object content, Size childMeasuredSize, Size availableSize) + { + return new Size(200, 200); + } + + /// + /// replace the executable and the args with whatever you have locally. This is only used + /// for internal debugging. However, make sure that you can set a unique title to the window, + /// so that you can then look it up. + /// + public object? CreateSampleComponent(XamlRoot owner, string text) { + if (owner is XamlRoot xamlRoot + && X11Manager.XamlRootMap.GetHostForRoot(xamlRoot) is X11XamlRootHost host) + { + var display = host.X11Window.Display; + if (!Exists("mpv")) + { + return null; + } + + var process = new Process + { + StartInfo = new ProcessStartInfo + { + // FileName = "mpv", + FileName = "xterm", + UseShellExecute = false + } + }; + + // var title = $"Sample Video {Random.Shared.Next()} {text}"; // used to maintain unique titles + // process.StartInfo.ArgumentList.Add("--keep-open=always"); + // process.StartInfo.ArgumentList.Add($"--title={title}"); + // process.StartInfo.ArgumentList.Add(SampleVideoLink); + + var title = $"Sample terminal {Random.Shared.Next()} {text}"; // used to maintain unique titles + process.StartInfo.ArgumentList.Add("-xrm"); + process.StartInfo.ArgumentList.Add("XTerm.vt100.allowTitleOps: false"); + process.StartInfo.ArgumentList.Add("-T"); + process.StartInfo.ArgumentList.Add(title); + + process.Start(); + + using var _1 = X11Helper.XLock(display); + + var _2 = XLib.XQueryTree(display, host.X11Window.Window, out IntPtr root, out _, out var children, out _); + XLib.XFree(children); + + IntPtr window = IntPtr.Zero; + SpinWait.SpinUntil(() => + { + window = FindWindowByTitle(display, root, title); + if (window == IntPtr.Zero) + { + return false; + } + + XWindowAttributes attributes = default; + var _ = XLib.XGetWindowAttributes(display, window, ref attributes); + return attributes.map_state == MapState.IsViewable; + }, 2000); + + if (window == IntPtr.Zero) + { + process.Kill(); + return null; + } + + return new X11Window(display, window); + } + + // For debugging: replace the above with a hardcoded window id, obtainable using e.g. wmctrl + // if (owner is XamlRoot xamlRoot + // && X11Manager.XamlRootMap.GetHostForRoot(xamlRoot) is X11XamlRootHost host) + // { + // return new X11Window(host.X11Window.Display, 0x04a00002); + // } + + return null; + } + + public bool IsNativeElementAttached(XamlRoot owner, object nativeElement) + { + if (nativeElement is X11Window x11Window + && owner is XamlRoot xamlRoot + && X11Manager.XamlRootMap.GetHostForRoot(xamlRoot) is X11XamlRootHost host) + { + using var _1 = X11Helper.XLock(x11Window.Display); + var _2 = XLib.XQueryTree(x11Window.Display, x11Window.Window, out _, out IntPtr parent, out var children, out _); + XLib.XFree(children); + return parent == host.X11Window.Window; + } + + return false; + } + public void ChangeNativeElementVisibility(XamlRoot owner, object content, bool visible) + { + if (content is X11Window x11Window) + { + if (visible) + { + var _3 = XLib.XMapWindow(x11Window.Display, x11Window.Window); + } + else + { + var _3 = X11Helper.XUnmapWindow(x11Window.Display, x11Window.Window); + } + } + } + + // This doesn't seem to work as most (all?) WMs won't change the opacity for subwindows, only top-level windows + public void ChangeNativeElementOpacity(XamlRoot owner, object content, double opacity) + { + // if (IsNativeElementAttached(owner, content) && content is X11Window x11Window) + // { + // // The spec requires a value between 0 and max int, not 0 and 1 + // var actualOpacity = (IntPtr)(opacity * uint.MaxValue); + // + // // if (opacity == 1) + // // { + // // XLib.XDeleteProperty( + // // x11Window.Display, + // // x11Window.Window, + // // X11Helper.GetAtom(x11Window.Display, X11Helper._NET_WM_WINDOW_OPACITY)); + // // } + // // else + // { + // var tmp = new IntPtr[] + // { + // actualOpacity + // }; + // XLib.XChangeProperty( + // x11Window.Display, + // x11Window.Window, + // X11Helper.GetAtom(x11Window.Display, X11Helper._NET_WM_WINDOW_OPACITY), + // X11Helper.GetAtom(x11Window.Display, X11Helper.XA_CARDINAL), + // 32, + // PropertyMode.Replace, + // actualOpacity, + // 1); + // } + // } + } + + private static bool Exists(string fileName) + { + if (File.Exists(fileName)) + { + return true; + } + + var values = Environment.GetEnvironmentVariable("PATH"); + if (values is null) + { + return false; + } + + return values + .Split(Path.PathSeparator) + .Select(path => Path.Combine(path, fileName)) + .Any(File.Exists); + } + + private unsafe static IntPtr FindWindowByTitle(IntPtr display, IntPtr current, string title) + { + var _1 = X11Helper.XFetchName(display, current, out var name); + if (name == title) + { + return current; + } + + var _2 = XLib.XQueryTree(display, + current, + out _, + out _, + out IntPtr children, + out int nChildren); + + var span = new Span(children.ToPointer(), nChildren); + + for (var i = 0; i < nChildren; ++i) + { + IntPtr window = FindWindowByTitle(display, span[i], title); + + if (window != IntPtr.Zero) + { + return window; + } + } + + return IntPtr.Zero; + } + + private unsafe static IntPtr FindWindowById(IntPtr display, IntPtr current, IntPtr id) + { + if (current == id) + { + return current; + } + + var _2 = XLib.XQueryTree(display, + current, + out _, + out _, + out IntPtr children, + out int nChildren); + + var span = new Span(children.ToPointer(), nChildren); + + for (var i = 0; i < nChildren; ++i) + { + IntPtr window = FindWindowById(display, span[i], id); + + if (window != IntPtr.Zero) + { + return window; + } + } + + return IntPtr.Zero; + } +} 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 258c4e1191bb..613e81f1afb8 100644 --- a/src/Uno.UI.Runtime.Skia.X11/X11_Bindings/X11Helper.cs +++ b/src/Uno.UI.Runtime.Skia.X11/X11_Bindings/X11Helper.cs @@ -49,6 +49,8 @@ internal static partial class X11Helper 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_WINDOW_OPACITY = "_NET_WM_WINDOW_OPACITY"; + public const string _NET_CLIENT_LIST = "_NET_CLIENT_LIST"; 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"; @@ -72,6 +74,8 @@ internal static partial class X11Helper public const string ATOM_PAIR = "ATOM_PAIR"; public const string INCR = "INCR"; + public const int Success = 0; + public const int POLLIN = 0x001; /* There is data to read. */ public const int POLLPRI = 0x002; /* There is urgent data to read. */ public const int POLLOUT = 0x004; /* Writing now will not block. */ @@ -313,6 +317,22 @@ public static partial int XPutImage(IntPtr display, IntPtr drawable, IntPtr gc, public static partial IntPtr XCreateImage(IntPtr display, IntPtr visual, uint depth, int format, int offset, IntPtr data, uint width, uint height, int bitmap_pad, int bytes_per_line); + [LibraryImport(libX11)] + public static partial int XFetchName(IntPtr display, IntPtr window, out string name_return); + + [LibraryImport(libX11)] + public unsafe static partial int XChangeWindowAttributes( + IntPtr display, IntPtr window, IntPtr valuemask, XSetWindowAttributes* attributes); + + [LibraryImport(libX11)] + public static partial int XReparentWindow(IntPtr display, IntPtr window, IntPtr parent, int x, int y); + + [LibraryImport(libX11)] + public static partial int XMoveWindow(IntPtr display, IntPtr window, int x, int y); + + [LibraryImport(libX11)] + public static partial int XUnmapWindow(IntPtr display, IntPtr window); + [LibraryImport(libX11)] public static partial int XPending(IntPtr display);