Browse Source

[X11] Use Xft.dpi and QT_*** environment variable to get screen scaling (#12880)

Nikita Tsukanov 2 years ago
parent
commit
09c5f20984

+ 172 - 0
src/Avalonia.X11/Screens/X11Screen.Providers.cs

@@ -0,0 +1,172 @@
+
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Runtime.InteropServices;
+using Avalonia.Platform;
+using static Avalonia.X11.XLib;
+namespace Avalonia.X11.Screens;
+
+internal partial class X11Screens
+{
+    internal class X11Screen
+    {
+        public bool IsPrimary { get; }
+        public string Name { get; set; }
+        public PixelRect Bounds { get; set; }
+        public Size? PhysicalSize { get; set; }
+        public PixelRect WorkingArea { get; set; }
+
+        public X11Screen(
+            PixelRect bounds,
+            bool isPrimary,
+            string name,
+            Size? physicalSize)
+        {
+            IsPrimary = isPrimary;
+            Name = name;
+            Bounds = bounds;
+            PhysicalSize = physicalSize;
+        }
+    }
+
+    internal interface IX11RawScreenInfoProvider
+    {
+        X11Screen[] Screens { get; }
+        event Action Changed;
+    }
+
+    
+    private class Randr15ScreensImpl : IX11RawScreenInfoProvider
+    {
+        private X11Screen[] _cache;
+        private readonly X11Info _x11;
+        private readonly IntPtr _window;
+
+        // Length of a EDID-Block-Length(128 bytes), XRRGetOutputProperty multiplies offset and length by 4
+        private const int EDIDStructureLength = 32; 
+
+        public event Action Changed;
+        
+        public Randr15ScreensImpl(AvaloniaX11Platform platform)
+        {
+            _x11 = platform.Info;
+            _window = CreateEventWindow(platform, OnEvent);
+            XRRSelectInput(_x11.Display, _window, RandrEventMask.RRScreenChangeNotify);
+        }
+
+        private void OnEvent(ref XEvent ev)
+        {
+            if ((int)ev.type == _x11.RandrEventBase + (int)RandrEvent.RRScreenChangeNotify)
+            {
+                _cache = null;
+                Changed?.Invoke();
+            }
+        }
+
+        private unsafe Size? GetPhysicalMonitorSizeFromEDID(IntPtr rrOutput)
+        {
+            if (rrOutput == IntPtr.Zero)
+                return null;
+            var properties = XRRListOutputProperties(_x11.Display, rrOutput, out int propertyCount);
+            var hasEDID = false;
+            for (var pc = 0; pc < propertyCount; pc++)
+            {
+                if (properties[pc] == _x11.Atoms.EDID)
+                    hasEDID = true;
+            }
+
+            if (!hasEDID)
+                return null;
+            XRRGetOutputProperty(_x11.Display, rrOutput, _x11.Atoms.EDID, 0, EDIDStructureLength, false, false,
+                _x11.Atoms.AnyPropertyType, out IntPtr actualType, out int actualFormat, out int bytesAfter, out _,
+                out IntPtr prop);
+            if (actualType != _x11.Atoms.XA_INTEGER)
+                return null;
+            if (actualFormat != 8) // Expecting an byte array
+                return null;
+
+            var edid = new byte[bytesAfter];
+            Marshal.Copy(prop, edid, 0, bytesAfter);
+            XFree(prop);
+            XFree(new IntPtr(properties));
+            if (edid.Length < 22)
+                return null;
+            var width = edid[21]; // 0x15 1 Max. Horizontal Image Size cm. 
+            var height = edid[22]; // 0x16 1 Max. Vertical Image Size cm. 
+            if (width == 0 && height == 0)
+                return null;
+            return new Size(width * 10, height * 10);
+        }
+
+        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 bounds = new PixelRect(mon.X, mon.Y, mon.Width, mon.Height);
+                    Size? pSize = null;
+
+                    for (int o = 0; o < mon.NOutput; o++)
+                    {
+                        var outputSize = GetPhysicalMonitorSizeFromEDID(mon.Outputs[o]);
+                        if (outputSize != null)
+                        {
+                            pSize = outputSize;
+                            break;
+                        }
+                    }
+
+                    screens[c] = new X11Screen(bounds, mon.Primary != 0, name, pSize);
+                }
+
+                XFree(new IntPtr(monitors));
+                _cache = UpdateWorkArea(_x11, screens);
+                return screens;
+            }
+        }
+    }
+
+    private class FallbackScreensImpl : IX11RawScreenInfoProvider
+    {
+        private readonly X11Info _info;
+        public event Action? Changed;
+        
+        public FallbackScreensImpl(AvaloniaX11Platform platform)
+        {
+            _info = platform.Info;
+            if (UpdateRootWindowGeometry())
+                platform.Globals.RootGeometryChangedChanged += () => UpdateRootWindowGeometry();
+        }
+
+        bool UpdateRootWindowGeometry()
+        {
+            var res = XGetGeometry(_info.Display, _info.RootWindow, out var geo);
+            if(res)
+            {
+                Screens = UpdateWorkArea(_info,
+                    new[]
+                    {
+                        new X11Screen(new PixelRect(0, 0, geo.width, geo.height), true, "Default", null)
+                    });
+            }
+
+            return res;
+        }
+
+        public X11Screen[] Screens { get; private set; } = new[]
+            { new X11Screen(new PixelRect(0, 0, 1920, 1280), true, "Default", null) };
+    }
+}

+ 249 - 0
src/Avalonia.X11/Screens/X11Screens.Scaling.cs

@@ -0,0 +1,249 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+
+namespace Avalonia.X11.Screens;
+
+internal partial class X11Screens
+{
+    interface IScalingProvider
+    {
+        double GetScaling(X11Screen screen, int index);
+    }
+
+    interface IScalingProviderWithChanges : IScalingProvider
+    {
+        event Action SettingsChanged;
+    }
+
+    class PostMultiplyScalingProvider : IScalingProvider
+    {
+        private readonly IScalingProvider _inner;
+        private readonly double _factor;
+
+        public PostMultiplyScalingProvider(IScalingProvider inner, double factor)
+        {
+            _inner = inner;
+            _factor = factor;
+        }
+
+        public double GetScaling(X11Screen screen, int index) => _inner.GetScaling(screen, index) * _factor;
+    }
+
+    class NullScalingProvider : IScalingProvider
+    {
+        public double GetScaling(X11Screen screen, int index) => 1;
+    }
+
+
+    class XrdbScalingProvider : IScalingProviderWithChanges
+    {
+        private readonly XResources _resources;
+        private double _factor = 1;
+
+        public XrdbScalingProvider(AvaloniaX11Platform platform)
+        {
+            _resources = platform.Resources;
+            _resources.ResourceChanged += name =>
+            {
+                if (name == "Xft.dpi")
+                    Update();
+            };
+            Update();
+        }
+
+        void Update()
+        {
+            var factor = 1d;
+            var stringValue = _resources.GetResource("Xft.dpi")?.Trim();
+            if (!string.IsNullOrWhiteSpace(stringValue) && double.TryParse(stringValue, NumberStyles.Any,
+                    CultureInfo.InvariantCulture, out var parsed))
+            {
+                factor = parsed / 96;
+            }
+
+            // ReSharper disable once CompareOfFloatsByEqualityOperator
+            if (_factor != factor)
+            {
+                _factor = factor;
+                SettingsChanged?.Invoke();
+            }
+        }
+        
+        public event Action? SettingsChanged;
+        
+        public double GetScaling(X11Screen screen, int index) => _factor;
+    }
+    
+    class PhysicalDpiScalingProvider : IScalingProvider
+    {
+        private const int FullHDWidth = 1920;
+        private const int FullHDHeight = 1080;
+        
+        public double GetScaling(X11Screen screen, int index)
+        {
+            if (screen.PhysicalSize == null)
+                return 1;
+            return GuessPixelDensity(screen.Bounds, screen.PhysicalSize.Value);
+        }
+
+        double GuessPixelDensity(PixelRect pixel, Size physical)
+        {
+            var calculatedDensity = 1d;
+            if (physical.Width > 0)
+                calculatedDensity = pixel.Width <= FullHDWidth
+                    ? 1
+                    : Math.Max(1, pixel.Width / physical.Width * 25.4 / 96);
+            else if (physical.Height > 0)
+                calculatedDensity = pixel.Height <= FullHDHeight
+                    ? 1
+                    : Math.Max(1, pixel.Height / physical.Height * 25.4 / 96);
+
+            if (calculatedDensity > 3)
+                return 1;
+            else
+            {
+                var sanePixelDensities = new double[] { 1, 1.25, 1.50, 1.75, 2 };
+                foreach (var saneDensity in sanePixelDensities)
+                {
+                    if (calculatedDensity <= saneDensity + 0.20)
+                        return saneDensity;
+                }
+
+                return sanePixelDensities.Last();
+            }
+        }
+    }
+
+    class UserConfiguredScalingProvider : IScalingProvider
+    {
+        private readonly Dictionary<string, double>? _namedConfig;
+        private readonly List<double>? _indexedConfig;
+        
+
+        public UserConfiguredScalingProvider(Dictionary<string, double>? namedConfig, List<double>? indexedConfig)
+        {
+            _namedConfig = namedConfig;
+            _indexedConfig = indexedConfig;
+        }
+        
+        public double GetScaling(X11Screen screen, int index)
+        {
+            if (_indexedConfig != null)
+            {
+                if (index > 0 && index < _indexedConfig.Count)
+                    return _indexedConfig[index];
+                return 1;
+            }
+            if (_namedConfig?.TryGetValue(screen.Name, out var scaling) == true)
+                return scaling;
+
+            return 1;
+        }
+    }
+    
+    class UserScalingConfiguration
+    {
+        public Dictionary<string, double>? NamedConfig { get; set; }
+        public List<double>? IndexedConfig { get; set; }
+    }
+    
+    static (UserScalingConfiguration? config, double global, bool forceAuto)? TryGetEnvConfiguration(
+        string globalFactorName, string userConfigName, string[] autoNames)
+    {
+        var globalFactorString = Environment.GetEnvironmentVariable(globalFactorName);
+        var screenFactorsString = Environment.GetEnvironmentVariable(userConfigName);
+        bool usePhysicalDpi = false;
+        foreach (var autoName in autoNames)
+        {
+            var envValue = Environment.GetEnvironmentVariable(autoName);
+            if (envValue == "1")
+                usePhysicalDpi = true;
+        }
+
+        double? globalFactor = null;
+        if (!string.IsNullOrWhiteSpace(globalFactorString) 
+            && double.TryParse(globalFactorString, NumberStyles.Any, CultureInfo.InvariantCulture, out var parsed))
+            globalFactor = parsed;
+
+        UserScalingConfiguration? userConfig = null;
+        if (!string.IsNullOrWhiteSpace(screenFactorsString))
+        {
+            try
+            {
+                var split = screenFactorsString.Split(';').Where(x => !string.IsNullOrWhiteSpace(x)).ToArray();
+                if (split[0].Contains("="))
+                {
+                    userConfig = new UserScalingConfiguration
+                    {
+                        NamedConfig = split.Select(x => x.Split(new[] { '=' }, 2))
+                            .ToDictionary(x => x[0], x => double.Parse(x[1], CultureInfo.InvariantCulture))
+                    };
+                }
+                else
+                {
+                    userConfig = new UserScalingConfiguration
+                    {
+                        IndexedConfig = split.Select(x => double.Parse(x, CultureInfo.InvariantCulture)).ToList()
+                    };
+                }
+            }
+            catch
+            {
+                Console.Error.WriteLine($"Unable to parse {userConfigName}={screenFactorsString}");
+            }
+        }
+        
+        
+        if (globalFactorString == null && screenFactorsString == null && usePhysicalDpi == null)
+            return null;
+
+        return (userConfig, globalFactor ?? 1, usePhysicalDpi);
+    }
+    
+    
+    static IScalingProvider GetScalingProvider(AvaloniaX11Platform platform)
+    {
+        var envSets = new[]
+        {
+            ("AVALONIA_GLOBAL_SCALE_FACTOR", "AVALONIA_SCREEN_SCALE_FACTORS", new[] { "AVALONIA_USE_PHYSICAL_DPI" })
+        }.ToList();
+
+        if (Environment.GetEnvironmentVariable("AVALONIA_SCREEN_SCALE_IGNORE_QT") != "1")
+        {
+            envSets.Add(("QT_SCALE_FACTOR", "QT_SCREEN_SCALE_FACTORS",
+                new[] { "QT_AUTO_SCREEN_SCALE_FACTOR", "QT_USE_PHYSICAL_DPI" }));
+        }
+
+        UserScalingConfiguration? config = null;
+        double global = 1;
+        bool forceAuto = false;
+
+        
+        foreach (var envSet in envSets)
+        {
+            var envConfig = TryGetEnvConfiguration(envSet.Item1, envSet.Item2, envSet.Item3);
+            if (envConfig != null)
+            {
+                (config, global, forceAuto) = envConfig.Value;
+                break;
+            }
+        }
+
+        IScalingProvider provider;
+        if (config != null)
+            provider = new UserConfiguredScalingProvider(config.NamedConfig, config.IndexedConfig);
+        else if (forceAuto)
+            provider = new PhysicalDpiScalingProvider();
+        else 
+            provider = new XrdbScalingProvider(platform);
+
+        // ReSharper disable once CompareOfFloatsByEqualityOperator
+        if (global != 1)
+            provider = new PostMultiplyScalingProvider(provider, global);
+
+        return provider;
+    }
+}

+ 93 - 0
src/Avalonia.X11/Screens/X11Screens.cs

@@ -0,0 +1,93 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Runtime.InteropServices;
+using Avalonia.Platform;
+using static Avalonia.X11.XLib;
+
+namespace Avalonia.X11.Screens
+{
+    internal partial class X11Screens : IScreenImpl
+    {
+        private IX11RawScreenInfoProvider _impl;
+        private IScalingProvider _scaling;
+        internal event Action Changed;
+
+        public X11Screens(AvaloniaX11Platform platform)
+        {
+            var info = platform.Info;
+            _impl = (info.RandrVersion != null && info.RandrVersion >= new Version(1, 5))
+                ? new Randr15ScreensImpl(platform)
+                : (IX11RawScreenInfoProvider)new FallbackScreensImpl(platform);
+            _impl.Changed += () => Changed?.Invoke();
+            _scaling = GetScalingProvider(platform);
+            if (_scaling is IScalingProviderWithChanges scalingWithChanges)
+                scalingWithChanges.SettingsChanged += () => Changed?.Invoke();
+        }
+
+        private static unsafe X11Screen[] UpdateWorkArea(X11Info info, X11Screen[] screens)
+        {
+            var rect = default(PixelRect);
+            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 PixelRect(pwa[0].ToInt32(), pwa[1].ToInt32(), pwa[2].ToInt32(), pwa[3].ToInt32());
+
+
+            foreach (var s in screens)
+            {
+                s.WorkingArea = s.Bounds.Intersect(wa);
+                if (s.WorkingArea.Width <= 0 || s.WorkingArea.Height <= 0)
+                    s.WorkingArea = s.Bounds;
+            }
+
+            XFree(prop);
+            return screens;
+        }
+        
+
+        public Screen ScreenFromPoint(PixelPoint point)
+        {
+            return ScreenHelper.ScreenFromPoint(point, AllScreens);
+        }
+
+        public Screen ScreenFromRect(PixelRect rect)
+        {
+            return ScreenHelper.ScreenFromRect(rect, AllScreens);
+        }
+
+        public Screen ScreenFromWindow(IWindowBaseImpl window)
+        {
+            return ScreenHelper.ScreenFromWindow(window, AllScreens);
+        }
+
+        public int ScreenCount => _impl.Screens.Length;
+
+        public IReadOnlyList<Screen> AllScreens =>
+            _impl.Screens.Select((s, i) => new Screen(_scaling.GetScaling(s, i), s.Bounds, s.WorkingArea, s.IsPrimary))
+                .ToArray();
+    }
+}

+ 5 - 7
src/Avalonia.X11/TransparencyHelper.cs

@@ -6,7 +6,7 @@ using Avalonia.Controls;
 
 namespace Avalonia.X11
 {
-    internal class TransparencyHelper :  IDisposable, X11Globals.IGlobalsSubscriber
+    internal class TransparencyHelper :  IDisposable
     {
         private readonly X11Info _x11;
         private readonly IntPtr _window;
@@ -35,7 +35,8 @@ namespace Avalonia.X11
             _x11 = x11;
             _window = window;
             _globals = globals;
-            _globals.AddSubscriber(this);
+            _globals.CompositionChanged += UpdateTransparency;
+            _globals.WindowManagerChanged += UpdateTransparency;
         }
 
         public void SetTransparencyRequest(IReadOnlyList<WindowTransparencyLevel> levels)
@@ -106,11 +107,8 @@ namespace Avalonia.X11
         
         public void Dispose()
         {
-            _globals.RemoveSubscriber(this);
+            _globals.WindowManagerChanged -= UpdateTransparency;
+            _globals.CompositionChanged -= UpdateTransparency;
         }
-
-        void X11Globals.IGlobalsSubscriber.WmChanged(string wmName) => UpdateTransparency();
-
-        void X11Globals.IGlobalsSubscriber.CompositionChanged(bool compositing) => UpdateTransparency();
     }
 }

+ 13 - 16
src/Avalonia.X11/X11Globals.cs

@@ -12,12 +12,16 @@ namespace Avalonia.X11
         private readonly X11Info _x11;
         private readonly IntPtr _rootWindow;
         private readonly IntPtr _compositingAtom;
-        private readonly List<IGlobalsSubscriber> _subscribers = new List<IGlobalsSubscriber>();
         
         private string _wmName;
         private IntPtr _compositionAtomOwner;
         private bool _isCompositionEnabled;
 
+        public event Action WindowManagerChanged;
+        public event Action CompositionChanged;
+        public event Action<IntPtr> RootPropertyChanged;
+        public event Action RootGeometryChangedChanged;
+
         public X11Globals(AvaloniaX11Platform plat)
         {
             _plat = plat;
@@ -40,9 +44,7 @@ namespace Avalonia.X11
                 if (_wmName != value)
                 {
                     _wmName = value;
-                    // The collection might change during enumeration
-                    foreach (var s in _subscribers.ToArray()) 
-                        s.WmChanged(value);
+                    WindowManagerChanged?.Invoke();
                 }
             }
         }
@@ -68,9 +70,7 @@ namespace Avalonia.X11
                 if (_isCompositionEnabled != value)
                 {
                     _isCompositionEnabled = value;
-                    // The collection might change during enumeration
-                    foreach (var s in _subscribers.ToArray()) 
-                        s.CompositionChanged(value);
+                    CompositionChanged?.Invoke();
                 }
             }
         }
@@ -160,6 +160,12 @@ namespace Avalonia.X11
             {
                 if(ev.PropertyEvent.atom == _x11.Atoms._NET_SUPPORTING_WM_CHECK)
                     UpdateWmName();
+                RootPropertyChanged?.Invoke(ev.PropertyEvent.atom);
+            }
+
+            if (ev.type == XEventName.ConfigureNotify)
+            {
+                RootGeometryChangedChanged?.Invoke();
             }
 
             if (ev.type == XEventName.ClientMessage)
@@ -169,14 +175,5 @@ namespace Avalonia.X11
                     UpdateCompositingAtomOwner();
             }
         }
-        
-        public interface IGlobalsSubscriber
-        {
-            void WmChanged(string wmName);
-            void CompositionChanged(bool compositing);
-        }
-
-        public void AddSubscriber(IGlobalsSubscriber subscriber) => _subscribers.Add(subscriber);
-        public void RemoveSubscriber(IGlobalsSubscriber subscriber) => _subscribers.Remove(subscriber);
     }
 }

+ 5 - 3
src/Avalonia.X11/X11Platform.cs

@@ -17,6 +17,7 @@ using Avalonia.Rendering.Composition;
 using Avalonia.Threading;
 using Avalonia.X11;
 using Avalonia.X11.Glx;
+using Avalonia.X11.Screens;
 using static Avalonia.X11.XLib;
 
 namespace Avalonia.X11
@@ -29,12 +30,13 @@ namespace Avalonia.X11
             new Dictionary<IntPtr, X11PlatformThreading.EventHandler>();
         public XI2Manager XI2;
         public X11Info Info { get; private set; }
-        public IX11Screens X11Screens { get; private set; }
+        public X11Screens X11Screens { get; private set; }
         public Compositor Compositor { get; private set; }
         public IScreenImpl Screens { get; private set; }
         public X11PlatformOptions Options { get; private set; }
         public IntPtr OrphanedWindow { get; private set; }
         public X11Globals Globals { get; private set; }
+        public XResources Resources { get; private set; }
         public ManualRawEventGrouperDispatchQueue EventGrouperDispatchQueue { get; } = new();
 
         public void Initialize(X11PlatformOptions options)
@@ -63,6 +65,7 @@ namespace Avalonia.X11
 
             Info = new X11Info(Display, DeferredDisplay, useXim);
             Globals = new X11Globals(this);
+            Resources = new XResources(this);
             //TODO: log
             if (options.UseDBusMenu)
                 DBusHelper.TryInitialize();
@@ -80,8 +83,7 @@ namespace Avalonia.X11
                 .Bind<IMountedVolumeInfoProvider>().ToConstant(new LinuxMountedVolumeInfoProvider())
                 .Bind<IPlatformLifetimeEventsImpl>().ToConstant(new X11PlatformLifetimeEvents(this));
             
-            X11Screens = X11.X11Screens.Init(this);
-            Screens = new X11Screens(X11Screens);
+            Screens = X11Screens = new X11Screens(this);
             if (Info.XInputVersion != null)
             {
                 var xi2 = new XI2Manager();

+ 0 - 337
src/Avalonia.X11/X11Screens.cs

@@ -1,337 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Runtime.InteropServices;
-using Avalonia.Platform;
-using static Avalonia.X11.XLib;
-
-namespace Avalonia.X11
-{
-    internal class X11Screens : IScreenImpl
-    {
-        private IX11Screens _impl;
-
-        public X11Screens(IX11Screens impl)
-        {
-            _impl = impl;
-        }
-
-        private static unsafe X11Screen[] UpdateWorkArea(X11Info info, X11Screen[] screens)
-        {
-            var rect = default(PixelRect);
-            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 PixelRect(pwa[0].ToInt32(), pwa[1].ToInt32(), pwa[2].ToInt32(), pwa[3].ToInt32());
-
-
-            foreach (var s in screens)
-            {
-                s.WorkingArea = s.Bounds.Intersect(wa);
-                if (s.WorkingArea.Width <= 0 || s.WorkingArea.Height <= 0)
-                    s.WorkingArea = s.Bounds;
-            }
-
-            XFree(prop);
-            return screens;
-        }
-
-        private class Randr15ScreensImpl : IX11Screens
-        {
-            private readonly X11ScreensUserSettings _settings;
-            private X11Screen[] _cache;
-            private X11Info _x11;
-            private IntPtr _window;
-            private const int EDIDStructureLength = 32; // Length of a EDID-Block-Length(128 bytes), XRRGetOutputProperty multiplies offset and length by 4
-            
-            public Randr15ScreensImpl(AvaloniaX11Platform platform, X11ScreensUserSettings settings)
-            {
-                _settings = settings;
-                _x11 = platform.Info;
-                _window = CreateEventWindow(platform, OnEvent);
-                XRRSelectInput(_x11.Display, _window, RandrEventMask.RRScreenChangeNotify);
-            }
-
-            private void OnEvent(ref XEvent ev)
-            {
-                // Invalidate cache on RRScreenChangeNotify
-                if ((int)ev.type == _x11.RandrEventBase + (int)RandrEvent.RRScreenChangeNotify)
-                    _cache = null;
-            }
-
-            private unsafe Size? GetPhysicalMonitorSizeFromEDID(IntPtr rrOutput)
-            {
-                if(rrOutput == IntPtr.Zero)
-                    return null;
-                var properties = XRRListOutputProperties(_x11.Display,rrOutput, out int propertyCount);
-                var hasEDID = false;
-                for(var pc = 0; pc < propertyCount; pc++)
-                {
-                    if(properties[pc] == _x11.Atoms.EDID)
-                        hasEDID = true;
-                }
-                if(!hasEDID)
-                    return null;
-                XRRGetOutputProperty(_x11.Display, rrOutput, _x11.Atoms.EDID, 0, EDIDStructureLength, false, false, _x11.Atoms.AnyPropertyType, out IntPtr actualType, out int actualFormat, out int bytesAfter, out _, out IntPtr prop);
-                if(actualType != _x11.Atoms.XA_INTEGER)
-                    return null;
-                if(actualFormat != 8) // Expecting an byte array
-                    return null;
-
-                var edid = new byte[bytesAfter];
-                Marshal.Copy(prop,edid,0,bytesAfter);
-                XFree(prop);
-                XFree(new IntPtr(properties));
-                if(edid.Length < 22)
-                    return null;
-                var width = edid[21]; // 0x15 1 Max. Horizontal Image Size cm. 
-                var height = edid[22]; // 0x16 1 Max. Vertical Image Size cm. 
-                if(width == 0 && height == 0)
-                    return null;
-                return new Size(width * 10, height * 10);
-            }
-
-            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 bounds = new PixelRect(mon.X, mon.Y, mon.Width, mon.Height);
-                        Size? pSize = null;
-                        double density = 0;
-                        if (_settings.NamedScaleFactors?.TryGetValue(name, out density) != true)
-                        {
-                            for(int o = 0; o < mon.NOutput; o++)
-                            {
-                                var outputSize = GetPhysicalMonitorSizeFromEDID(mon.Outputs[o]);
-                                var outputDensity = 1d;
-                                if(outputSize != null)
-                                    outputDensity = X11Screen.GuessPixelDensity(bounds, outputSize.Value);
-                                if(density == 0 || density > outputDensity)
-                                {
-                                    density = outputDensity;
-                                    pSize = outputSize;
-                                }
-                            }
-                        }
-                        if(density == 0)
-                            density = 1;
-                        density *= _settings.GlobalScaleFactor;
-                        screens[c] = new X11Screen(bounds, mon.Primary != 0, name, pSize, density);
-                    }
-                    
-                    XFree(new IntPtr(monitors));
-                    _cache = UpdateWorkArea(_x11, screens);
-                    return screens;
-                }
-            }
-        }
-
-        private 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 PixelRect(0, 0, geo.width, geo.height), true, "Default", null,
-                                settings.GlobalScaleFactor)
-                        });
-                }
-                else
-                {
-                    Screens = new[]
-                    {
-                        new X11Screen(new PixelRect(0, 0, 1920, 1280), true, "Default", null,
-                            settings.GlobalScaleFactor)
-                    };
-                }
-            }
-
-            public X11Screen[] Screens { get; }
-        }
-        
-        public static IX11Screens 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);
-
-            return impl;
-
-        }
-
-        public Screen ScreenFromPoint(PixelPoint point)
-        {
-            return ScreenHelper.ScreenFromPoint(point, AllScreens);
-        }
-
-        public Screen ScreenFromRect(PixelRect rect)
-        {
-            return ScreenHelper.ScreenFromRect(rect, AllScreens);
-        }
-
-        public Screen ScreenFromWindow(IWindowBaseImpl window)
-        {
-            return ScreenHelper.ScreenFromWindow(window, AllScreens);
-        }
-
-        public int ScreenCount => _impl.Screens.Length;
-
-        public IReadOnlyList<Screen> AllScreens =>
-            _impl.Screens.Select(s => new Screen(s.Scaling, s.Bounds, s.WorkingArea, s.IsPrimary)).ToArray();
-    }
-
-    internal interface IX11Screens
-    {
-        X11Screen[] Screens { get; }
-    }
-
-    internal class X11ScreensUserSettings
-    {
-        public double GlobalScaleFactor { get; set; } = 1;
-        public Dictionary<string, double> NamedScaleFactors { get; set; }
-
-        private 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 rv;  
-            }
-
-
-        public static X11ScreensUserSettings Detect()
-        {
-            return DetectEnvironment() ?? new X11ScreensUserSettings();
-        }
-    }
-
-    internal class X11Screen
-    {
-        private const int FullHDWidth = 1920;
-        private const int FullHDHeight = 1080;
-        public bool IsPrimary { get; }
-        public string Name { get; set; }
-        public PixelRect Bounds { get; set; }
-        public Size? PhysicalSize { get; set; }
-        public double Scaling { get; set; }
-        public PixelRect WorkingArea { get; set; }
-
-        public X11Screen(
-            PixelRect bounds,
-            bool isPrimary,
-            string name,
-            Size? physicalSize,
-            double? scaling)
-        {
-            IsPrimary = isPrimary;
-            Name = name;
-            Bounds = bounds;
-            if (physicalSize == null && scaling == null)
-            {
-                Scaling = 1;
-            }
-            else if (scaling == null)
-            {
-                Scaling = GuessPixelDensity(bounds, physicalSize.Value);
-            }
-            else
-            {
-                Scaling = scaling.Value;
-                PhysicalSize = physicalSize;
-            }
-        }
-
-        public static double GuessPixelDensity(PixelRect pixel, Size physical)
-        {
-            var calculatedDensity = 1d;
-            if(physical.Width > 0)
-                calculatedDensity = pixel.Width <= FullHDWidth ? 1 : Math.Max(1, pixel.Width / physical.Width * 25.4 / 96);
-            else if(physical.Height > 0)
-                calculatedDensity = pixel.Height <= FullHDHeight ? 1 : Math.Max(1, pixel.Height / physical.Height * 25.4 / 96);
-            
-            if(calculatedDensity > 3)
-                return 1;
-            else
-            {
-                var sanePixelDensities = new double[] { 1, 1.25, 1.50, 1.75, 2 };
-                foreach(var saneDensity in sanePixelDensities)
-                {
-                    if(calculatedDensity <= saneDensity + 0.20)
-                        return saneDensity;
-                }
-                return sanePixelDensities.Last();
-            }
-        }
-    }
-}

+ 11 - 2
src/Avalonia.X11/X11Window.cs

@@ -230,6 +230,8 @@ namespace Avalonia.X11
                 () => _platform.Options.UseDBusFilePicker ? DBusSystemDialog.TryCreateAsync(Handle) : Task.FromResult<IStorageProvider?>(null),
                 () => GtkSystemDialog.TryCreate(this)
             });
+
+            platform.X11Screens.Changed += OnScreensChanged;
         }
 
         private class SurfaceInfo  : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo
@@ -585,6 +587,11 @@ namespace Avalonia.X11
             return extents;
         }
 
+        private void OnScreensChanged()
+        {
+            UpdateScaling();
+        }
+
         private bool UpdateScaling(bool skipResize = false)
         {
             double newScaling;
@@ -592,7 +599,7 @@ namespace Avalonia.X11
                 newScaling = _scalingOverride.Value;
             else
             {
-                var monitor = _platform.X11Screens.Screens.OrderBy(x => x.Scaling)
+                var monitor = _platform.X11Screens.AllScreens.OrderBy(x => x.Scaling)
                     .FirstOrDefault(m => m.Bounds.Contains(_position ?? default));
                 newScaling = monitor?.Scaling ?? RenderScaling;
             }
@@ -926,6 +933,8 @@ namespace Avalonia.X11
                 if (!fromDestroyNotification)
                     XDestroyWindow(_x11.Display, handle);
             }
+
+            _platform.X11Screens.Changed -= OnScreensChanged;
             
             if (_useRenderWindow && _renderHandle != IntPtr.Zero)
             {                
@@ -1095,7 +1104,7 @@ namespace Avalonia.X11
 
         public IScreenImpl Screen => _platform.Screens;
 
-        public Size MaxAutoSizeHint => _platform.X11Screens.Screens.Select(s => s.Bounds.Size.ToSize(s.Scaling))
+        public Size MaxAutoSizeHint => _platform.X11Screens.AllScreens.Select(s => s.Bounds.Size.ToSize(s.Scaling))
             .OrderByDescending(x => x.Width + x.Height).FirstOrDefault();
 
 

+ 75 - 0
src/Avalonia.X11/XResources.cs

@@ -0,0 +1,75 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using static Avalonia.X11.XLib;
+namespace Avalonia.X11;
+
+internal class XResources
+{
+    private Dictionary<string, string> _resources = new();
+    private readonly X11Info _x11;
+    public event Action<string>? ResourceChanged;
+
+    public XResources(AvaloniaX11Platform plat)
+    {
+        _x11 = plat.Info;
+        plat.Globals.RootPropertyChanged += OnRootPropertyChanged;
+        UpdateResources();
+    }
+
+    void UpdateResources()
+    {
+        var res = ReadResourcesString() ?? "";
+        var items = res.Split('\n');
+        var newResources = new Dictionary<string, string>();
+        var missingResources = new HashSet<string>(_resources.Keys);
+        var changedResources = new HashSet<string>();
+        foreach (var item in items)
+        {
+            var sp = item.Split(new[] { ':' }, 2);
+            if (sp.Length < 2)
+                continue;
+            var key = sp[0];
+            var value = sp[1].TrimStart();
+            newResources[key] = value;
+            if (!missingResources.Remove(sp[0]) || _resources[key] != value)
+                changedResources.Add(key);
+        }
+        _resources = newResources;
+        foreach (var missing in missingResources)
+            ResourceChanged?.Invoke(missing);
+        foreach (var changed in changedResources)
+            ResourceChanged?.Invoke(changed);
+    }
+
+    public string? GetResource(string key)
+    {
+        _resources.TryGetValue(key, out var value);
+        return value;
+    }
+    
+    string ReadResourcesString()
+    {
+        XGetWindowProperty(_x11.Display, _x11.RootWindow, _x11.Atoms.XA_RESOURCE_MANAGER,
+            IntPtr.Zero, new IntPtr(0x7fffffff),
+            false, _x11.Atoms.XA_STRING, out var actualType, out var actualFormat,
+            out var nitems, out _, out var prop);
+        try
+        {
+            if (actualFormat != 8)
+                return null;
+            return Marshal.PtrToStringAnsi(prop, nitems.ToInt32());
+        }
+        finally
+        {
+            XFree(prop);
+        }
+    }
+    
+    private void OnRootPropertyChanged(IntPtr atom)
+    {
+        if (atom == _x11.Atoms.XA_RESOURCE_MANAGER)
+            UpdateResources();
+    }
+}