diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 57c8b700df..2a7d0b30a9 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -43,7 +43,7 @@ namespace ControlCatalog.NetCore /// This method is needed for IDE previewer infrastructure /// public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure().UsePlatformDetect().UseSkia().UseReactiveUI(); + => AppBuilder.Configure().UsePlatformDetect().UseSkia().UseReactiveUI().UseX11(); static void ConsoleSilencer() { diff --git a/src/Avalonia.X11/Avalonia.X11.csproj b/src/Avalonia.X11/Avalonia.X11.csproj index 681eb0a2be..3e75180468 100644 --- a/src/Avalonia.X11/Avalonia.X11.csproj +++ b/src/Avalonia.X11/Avalonia.X11.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 diff --git a/src/Avalonia.X11/Stubs.cs b/src/Avalonia.X11/Stubs.cs index f70c7fee25..91d788c576 100644 --- a/src/Avalonia.X11/Stubs.cs +++ b/src/Avalonia.X11/Stubs.cs @@ -63,12 +63,4 @@ namespace Avalonia.X11 return new FakeIcon(ms.ToArray()); } } - - class ScreenStub : IScreenImpl - { - public int ScreenCount { get; } = 1; - - public Screen[] AllScreens { get; } = - {new Screen(new Rect(0, 0, 1920, 1280), new Rect(0, 0, 1920, 1280), true)}; - } } diff --git a/src/Avalonia.X11/X11Info.cs b/src/Avalonia.X11/X11Info.cs index fece7c2ba2..e102283f87 100644 --- a/src/Avalonia.X11/X11Info.cs +++ b/src/Avalonia.X11/X11Info.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; +using JetBrains.Annotations; using static Avalonia.X11.XLib; // ReSharper disable UnusedAutoPropertyAccessor.Local namespace Avalonia.X11 @@ -17,6 +18,12 @@ namespace Avalonia.X11 public IntPtr DefaultCursor { get; } public X11Atoms Atoms { get; } public IntPtr Xim { get; } + + public int RandrEventBase { get; } + public int RandrErrorBase { get; } + + public Version RandrVersion { get; } + public IntPtr LastActivityTimestamp { get; set; } @@ -33,6 +40,22 @@ namespace Avalonia.X11 //TODO: Open an actual XIM once we get support for preedit in our textbox XSetLocaleModifiers("@im=none"); Xim = XOpenIM(display, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); + + try + { + if (XRRQueryExtension(display, out int randrEventBase, out var randrErrorBase) != 0) + { + RandrEventBase = randrEventBase; + RandrErrorBase = randrErrorBase; + if (XRRQueryVersion(display, out var major, out var minor) != 0) + RandrVersion = new Version(major, minor); + } + } + catch + { + //Ignore, randr is not supported + } + } } } diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index cce5299843..d315ba5ffb 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -41,6 +41,7 @@ namespace Avalonia.X11 .Bind().ToConstant(new PlatformSettingsStub()) .Bind().ToConstant(new SystemDialogsStub()) .Bind().ToConstant(new IconLoaderStub()); + X11Screens.Init(this); EglGlPlatformFeature.TryInitialize(); } diff --git a/src/Avalonia.X11/X11Screens.cs b/src/Avalonia.X11/X11Screens.cs new file mode 100644 index 0000000000..4a9cbe98a4 --- /dev/null +++ b/src/Avalonia.X11/X11Screens.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Runtime.InteropServices; +using Avalonia.Platform; +using static Avalonia.X11.XLib; +using JetBrains.Annotations; + +namespace Avalonia.X11 +{ + class X11Screens : IScreenImpl + { + private IX11Screens _impl; + + private X11Screens(IX11Screens impl) + { + _impl = impl; + } + + static unsafe X11Screen[] UpdateWorkArea(X11Info info, X11Screen[] screens) + { + var rect = default(Rect); + foreach (var s in screens) + { + rect = rect.Union(s.Bounds); + //Fallback value + s.WorkingArea = s.Bounds; + } + + var res = XGetWindowProperty(info.Display, + info.RootWindow, + info.Atoms._NET_WORKAREA, + IntPtr.Zero, + new IntPtr(128), + false, + info.Atoms.AnyPropertyType, + out var type, + out var format, + out var count, + out var bytesAfter, + out var prop); + + if (res != (int)Status.Success || type == IntPtr.Zero || + format == 0 || bytesAfter.ToInt64() != 0 || count.ToInt64() % 4 != 0) + return screens; + + var pwa = (IntPtr*)prop; + var wa = new Rect(pwa[0].ToInt32(), pwa[1].ToInt32(), pwa[2].ToInt32(), pwa[3].ToInt32()); + + + foreach (var s in screens) + s.WorkingArea = s.Bounds.Intersect(wa); + + XFree(prop); + return screens; + } + + class Randr15ScreensImpl : IX11Screens + { + private readonly X11ScreensUserSettings _settings; + private X11Screen[] _cache; + private X11Info _x11; + private IntPtr _window; + + public Randr15ScreensImpl(AvaloniaX11Platform platform, X11ScreensUserSettings settings) + { + _settings = settings; + _x11 = platform.Info; + _window = XCreateSimpleWindow(_x11.Display, _x11.DefaultRootWindow, 0, 0, 1, 1, 0, IntPtr.Zero, IntPtr.Zero); + platform.Windows[_window] = OnEvent; + XRRSelectInput(_x11.Display, _window, RandrEventMask.RRScreenChangeNotify); + } + + private void OnEvent(XEvent ev) + { + // Invalidate cache on RRScreenChangeNotify + if ((int)ev.type == _x11.RandrEventBase + (int)RandrEvent.RRScreenChangeNotify) + _cache = null; + } + + public unsafe X11Screen[] Screens + { + get + { + if (_cache != null) + return _cache; + var monitors = XRRGetMonitors(_x11.Display, _window, true, out var count); + + var screens = new X11Screen[count]; + for (var c = 0; c < count; c++) + { + var mon = monitors[c]; + var namePtr = XGetAtomName(_x11.Display, mon.Name); + var name = Marshal.PtrToStringAnsi(namePtr); + XFree(namePtr); + + var density = 1d; + if (_settings.NamedScaleFactors?.TryGetValue(name, out density) != true) + { + if (mon.MWidth == 0) + density = 1; + else + density = X11Screen.GuessPixelDensity(mon.Width, mon.MWidth); + } + + density *= _settings.GlobalScaleFactor; + + var bounds = new Rect(mon.X, mon.Y, mon.Width, mon.Height); + screens[0] = new X11Screen(bounds, + mon.Primary != 0, + name, + (mon.MWidth == 0 || mon.MHeight == 0) ? (Size?)null : new Size(mon.MWidth, mon.MHeight), + density); + } + + XFree(new IntPtr(monitors)); + _cache = UpdateWorkArea(_x11, screens); + return screens; + } + } + } + + class FallbackScreensImpl : IX11Screens + { + public FallbackScreensImpl(X11Info info, X11ScreensUserSettings settings) + { + if (XGetGeometry(info.Display, info.RootWindow, out var geo)) + { + + Screens = UpdateWorkArea(info, + new[] + { + new X11Screen(new Rect(0, 0, geo.width, geo.height), true, "Default", null, + settings.GlobalScaleFactor) + }); + } + + Screens = new[] {new X11Screen(new Rect(0, 0, 1920, 1280), true, "Default", null, settings.GlobalScaleFactor)}; + } + + public X11Screen[] Screens { get; } + } + + public static void Init(AvaloniaX11Platform platform) + { + var info = platform.Info; + var settings = X11ScreensUserSettings.Detect(); + var impl = (info.RandrVersion != null && info.RandrVersion >= new Version(1, 5)) + ? new Randr15ScreensImpl(platform, settings) + : (IX11Screens)new FallbackScreensImpl(info, settings); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(impl); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new X11Screens(impl)); + + } + + + public int ScreenCount => _impl.Screens.Length; + + public Screen[] AllScreens => + _impl.Screens.Select(s => new Screen(s.Bounds, s.WorkingArea, s.Primary)).ToArray(); + } + + interface IX11Screens + { + X11Screen[] Screens { get; } + } + + class X11ScreensUserSettings + { + public double GlobalScaleFactor { get; set; } = 1; + public Dictionary NamedScaleFactors { get; set; } + + static double? TryParse(string s) + { + if (s == null) + return null; + if (double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var rv)) + return rv; + return null; + } + + + public static X11ScreensUserSettings DetectEnvironment() + { + var globalFactor = Environment.GetEnvironmentVariable("AVALONIA_GLOBAL_SCALE_FACTOR"); + var screenFactors = Environment.GetEnvironmentVariable("AVALONIA_SCREEN_SCALE_FACTORS"); + if (globalFactor == null && screenFactors == null) + return null; + + var rv = new X11ScreensUserSettings + { + GlobalScaleFactor = TryParse(globalFactor) ?? 1 + }; + + try + { + if (!string.IsNullOrWhiteSpace(screenFactors)) + { + rv.NamedScaleFactors = screenFactors.Split(';').Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => x.Split('=')).ToDictionary(x => x[0], + x => double.Parse(x[1], CultureInfo.InvariantCulture)); + } + } + catch + { + //Ignore + } + + return null; + } + + + public static X11ScreensUserSettings Detect() + { + return DetectEnvironment() ?? new X11ScreensUserSettings(); + } + } + + class X11Screen + { + public bool Primary { get; } + public string Name { get; set; } + public Rect Bounds { get; set; } + public Size? PhysicalSize { get; set; } + public double PixelDensity { get; set; } + public Rect WorkingArea { get; set; } + + public X11Screen(Rect bounds, bool primary, + string name, Size? physicalSize, double? pixelDensity) + { + Primary = primary; + Name = name; + Bounds = bounds; + if (physicalSize == null && pixelDensity == null) + { + PixelDensity = 1; + } + else if (pixelDensity == null) + { + PixelDensity = GuessPixelDensity(bounds.Width, physicalSize.Value.Width); + } + else + { + PixelDensity = pixelDensity.Value; + PhysicalSize = physicalSize; + } + } + + public static double GuessPixelDensity(double pixelWidth, double mmWidth) + => Math.Max(1, Math.Round(pixelWidth / mmWidth * 25.4 / 96)); + } +} diff --git a/src/Avalonia.X11/X11Structs.cs b/src/Avalonia.X11/X11Structs.cs index 6d73975b39..fc1bb01b6e 100644 --- a/src/Avalonia.X11/X11Structs.cs +++ b/src/Avalonia.X11/X11Structs.cs @@ -849,7 +849,43 @@ namespace Avalonia.X11 { OwnerGrabButtonMask = 1<<24 } - internal enum GrabMode { + [Flags] + internal enum RandrEventMask + { + RRScreenChangeNotify = 1 << 0, + +/* V1.2 additions */ + RRCrtcChangeNotifyMask = 1 << 1, + RROutputChangeNotifyMask = 1 << 2, + RROutputPropertyNotifyMask = 1 << 3, + +/* V1.4 additions */ + RRProviderChangeNotifyMask = 1 << 4, + RRProviderPropertyNotifyMask = 1 << 5, + RRResourceChangeNotifyMask = 1 << 6, + +/* V1.6 additions */ + RRLeaseNotifyMask = 1 << 7 + } + + internal enum RandrEvent + { + RRScreenChangeNotify = 0, + + /* V1.2 additions */ + RRNotify = 1 + } + + internal enum RandrRotate + { + /* used in the rotation field; rotation and reflection in 0.1 proto. */ + RR_Rotate_0 = 1, + RR_Rotate_90 = 2, + RR_Rotate_180 = 4, + RR_Rotate_270 = 8 + } + + internal enum GrabMode { GrabModeSync = 0, GrabModeAsync = 1 } @@ -1812,4 +1848,18 @@ namespace Avalonia.X11 { public const string XNSpotLocation = "spotLocation"; public const string XNFontSet = "fontSet"; } + + struct XRRMonitorInfo { + public IntPtr Name; + public int Primary; + public int Automatic; + public int NOutput; + public int X; + public int Y; + public int Width; + public int Height; + public int MWidth; + public int MHeight; + public IntPtr Outputs; + } } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 7a05bc896d..3212123238 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -636,8 +636,8 @@ namespace Avalonia.X11 } } - - public IScreenImpl Screen { get; } = new ScreenStub(); + + public IScreenImpl Screen { get; } = AvaloniaLocator.CurrentMutable.GetService(); public Size MaxClientSize { get; } = new Size(1920, 1280); diff --git a/src/Avalonia.X11/XLib.cs b/src/Avalonia.X11/XLib.cs index 89eab88966..1688d69f4f 100644 --- a/src/Avalonia.X11/XLib.cs +++ b/src/Avalonia.X11/XLib.cs @@ -14,7 +14,8 @@ namespace Avalonia.X11 { internal unsafe static class XLib { - const string libX11 = "X11"; + const string libX11 = "libX11.so.6"; + const string libX11Randr = "libXrandr.so.2"; [DllImport(libX11)] public static extern IntPtr XOpenDisplay(IntPtr display); @@ -451,6 +452,22 @@ namespace Avalonia.X11 [DllImport (libX11)] public static extern void XDestroyIC (IntPtr xic); + [DllImport(libX11Randr)] + public static extern int XRRQueryExtension (IntPtr dpy, + out int event_base_return, + out int error_base_return); + + [DllImport(libX11Randr)] + public static extern int XRRQueryVersion(IntPtr dpy, + out int major_version_return, + out int minor_version_return); + + [DllImport(libX11Randr)] + public static extern XRRMonitorInfo* + XRRGetMonitors(IntPtr dpy, IntPtr window, bool get_active, out int nmonitors); + [DllImport(libX11Randr)] + public static extern void XRRSelectInput(IntPtr dpy, IntPtr window, RandrEventMask mask); + public struct XGeometry { public IntPtr root;