diff --git a/src/Avalonia.X11/Screens/X11Screen.Providers.cs b/src/Avalonia.X11/Screens/X11Screen.Providers.cs new file mode 100644 index 00000000000..7ba3c67fa69 --- /dev/null +++ b/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) }; + } +} \ No newline at end of file diff --git a/src/Avalonia.X11/Screens/X11Screens.Scaling.cs b/src/Avalonia.X11/Screens/X11Screens.Scaling.cs new file mode 100644 index 00000000000..c57789fcbe3 --- /dev/null +++ b/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? _namedConfig; + private readonly List? _indexedConfig; + + + public UserConfiguredScalingProvider(Dictionary? namedConfig, List? 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? NamedConfig { get; set; } + public List? 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; + } +} \ No newline at end of file diff --git a/src/Avalonia.X11/Screens/X11Screens.cs b/src/Avalonia.X11/Screens/X11Screens.cs new file mode 100644 index 00000000000..bced3b669c4 --- /dev/null +++ b/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 AllScreens => + _impl.Screens.Select((s, i) => new Screen(_scaling.GetScaling(s, i), s.Bounds, s.WorkingArea, s.IsPrimary)) + .ToArray(); + } +} diff --git a/src/Avalonia.X11/TransparencyHelper.cs b/src/Avalonia.X11/TransparencyHelper.cs index 3cc901e3326..dd70643cfdb 100644 --- a/src/Avalonia.X11/TransparencyHelper.cs +++ b/src/Avalonia.X11/TransparencyHelper.cs @@ -6,7 +6,7 @@ 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 @@ public TransparencyHelper(X11Info x11, IntPtr window, X11Globals globals) _x11 = x11; _window = window; _globals = globals; - _globals.AddSubscriber(this); + _globals.CompositionChanged += UpdateTransparency; + _globals.WindowManagerChanged += UpdateTransparency; } public void SetTransparencyRequest(IReadOnlyList levels) @@ -106,11 +107,8 @@ private void SetBlur(bool blur) 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(); } } diff --git a/src/Avalonia.X11/X11Globals.cs b/src/Avalonia.X11/X11Globals.cs index 1ae05447244..80e1eebd166 100644 --- a/src/Avalonia.X11/X11Globals.cs +++ b/src/Avalonia.X11/X11Globals.cs @@ -12,12 +12,16 @@ internal unsafe class X11Globals private readonly X11Info _x11; private readonly IntPtr _rootWindow; private readonly IntPtr _compositingAtom; - private readonly List _subscribers = new List(); private string _wmName; private IntPtr _compositionAtomOwner; private bool _isCompositionEnabled; + public event Action WindowManagerChanged; + public event Action CompositionChanged; + public event Action RootPropertyChanged; + public event Action RootGeometryChangedChanged; + public X11Globals(AvaloniaX11Platform plat) { _plat = plat; @@ -40,9 +44,7 @@ private set 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 @@ public bool IsCompositionEnabled 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 @@ private void OnRootWindowEvent(ref XEvent ev) { 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 @@ private void OnRootWindowEvent(ref XEvent ev) 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); } } diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 91c0190e8b9..cd667e6c01a 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -17,6 +17,7 @@ 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 @@ internal class AvaloniaX11Platform : IWindowingPlatform new Dictionary(); 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 @@ public void Initialize(X11PlatformOptions options) 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 @@ public void Initialize(X11PlatformOptions options) .Bind().ToConstant(new LinuxMountedVolumeInfoProvider()) .Bind().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(); diff --git a/src/Avalonia.X11/X11Screens.cs b/src/Avalonia.X11/X11Screens.cs deleted file mode 100644 index 6c7283952db..00000000000 --- a/src/Avalonia.X11/X11Screens.cs +++ /dev/null @@ -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 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 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(); - } - } - } -} diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 1bdeb91f0cc..9a0067c2795 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -230,6 +230,8 @@ public X11Window(AvaloniaX11Platform platform, IWindowImpl? popupParent, bool ov () => _platform.Options.UseDBusFilePicker ? DBusSystemDialog.TryCreateAsync(Handle) : Task.FromResult(null), () => GtkSystemDialog.TryCreate(this) }); + + platform.X11Screens.Changed += OnScreensChanged; } private class SurfaceInfo : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo @@ -585,6 +587,11 @@ private void OnEvent(ref XEvent ev) return extents; } + private void OnScreensChanged() + { + UpdateScaling(); + } + private bool UpdateScaling(bool skipResize = false) { double newScaling; @@ -592,7 +599,7 @@ private bool UpdateScaling(bool skipResize = false) 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 @@ private void Cleanup(bool fromDestroyNotification) if (!fromDestroyNotification) XDestroyWindow(_x11.Display, handle); } + + _platform.X11Screens.Changed -= OnScreensChanged; if (_useRenderWindow && _renderHandle != IntPtr.Zero) { @@ -1095,7 +1104,7 @@ public void Activate() 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(); diff --git a/src/Avalonia.X11/XResources.cs b/src/Avalonia.X11/XResources.cs new file mode 100644 index 00000000000..a4e832cb609 --- /dev/null +++ b/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 _resources = new(); + private readonly X11Info _x11; + public event Action? 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(); + var missingResources = new HashSet(_resources.Keys); + var changedResources = new HashSet(); + 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(); + } +} \ No newline at end of file