Browse Source
* Draft new API * Push reusable ScreensBaseImpl implementation * Fix tests and stubs * Update ScreensPage sample to work on mobile + show new APIs * Reimplement Windows ScreensImpl, reuse existing screens in other places of backend, use Microsoft.Windows.CsWin32 for interop * Make X11 project buildable, don't utilize new APIs yet * Reimplement macOS Screens API, differenciate screens by CGDirectDisplayID * Fix build * Adjust breaking changes file (none affect users) * Fix missing macOS Screen.DisplayName * Add more tests + fix screen removal * Add screens integration tests * Use hash set with comparer when removing screens * Make screenimpl safer on macOS as per review * Replace UnmanagedCallersOnly usage with source generated EnumDisplayMonitors * Remove unused dllimport * Only implement GetHashCode and Equals on PlatformScreen subclass, without changing base Screenpull/16348/head
committed by
GitHub
54 changed files with 1279 additions and 462 deletions
@ -1,25 +1,173 @@ |
|||||
using System.Collections.Generic; |
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using System.Threading.Tasks; |
||||
using Avalonia.Metadata; |
using Avalonia.Metadata; |
||||
|
using Avalonia.Threading; |
||||
|
#pragma warning disable CS1591 // Private API doesn't require XML documentation.
|
||||
|
|
||||
namespace Avalonia.Platform |
namespace Avalonia.Platform |
||||
{ |
{ |
||||
[Unstable] |
[Unstable] |
||||
public interface IScreenImpl |
public interface IScreenImpl |
||||
{ |
{ |
||||
/// <summary>
|
|
||||
/// Gets the total number of screens available on the device.
|
|
||||
/// </summary>
|
|
||||
int ScreenCount { get; } |
int ScreenCount { get; } |
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the list of all screens available on the device.
|
|
||||
/// </summary>
|
|
||||
IReadOnlyList<Screen> AllScreens { get; } |
IReadOnlyList<Screen> AllScreens { get; } |
||||
|
Action? Changed { get; set; } |
||||
Screen? ScreenFromWindow(IWindowBaseImpl window); |
Screen? ScreenFromWindow(IWindowBaseImpl window); |
||||
|
Screen? ScreenFromTopLevel(ITopLevelImpl topLevel); |
||||
Screen? ScreenFromPoint(PixelPoint point); |
Screen? ScreenFromPoint(PixelPoint point); |
||||
|
|
||||
Screen? ScreenFromRect(PixelRect rect); |
Screen? ScreenFromRect(PixelRect rect); |
||||
|
Task<bool> RequestScreenDetails(); |
||||
|
} |
||||
|
|
||||
|
[PrivateApi] |
||||
|
public class PlatformScreen(IPlatformHandle platformHandle) : Screen |
||||
|
{ |
||||
|
public override IPlatformHandle? TryGetPlatformHandle() => platformHandle; |
||||
|
|
||||
|
public override int GetHashCode() => platformHandle.GetHashCode(); |
||||
|
public override bool Equals(object? obj) |
||||
|
{ |
||||
|
return obj is PlatformScreen other && platformHandle.Equals(other.TryGetPlatformHandle()!); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[PrivateApi] |
||||
|
public abstract class ScreensBase<TKey, TScreen>(IEqualityComparer<TKey>? screenKeyComparer) : IScreenImpl |
||||
|
where TKey : notnull |
||||
|
where TScreen : PlatformScreen |
||||
|
{ |
||||
|
private readonly Dictionary<TKey, TScreen> _allScreensByKey = screenKeyComparer is not null ? |
||||
|
new Dictionary<TKey, TScreen>(screenKeyComparer) : |
||||
|
new Dictionary<TKey, TScreen>(); |
||||
|
private TScreen[]? _allScreens; |
||||
|
private int? _screenCount; |
||||
|
private bool? _screenDetailsRequestGranted; |
||||
|
private DispatcherOperation? _onChangeOperation; |
||||
|
|
||||
|
protected ScreensBase() : this(null) |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public int ScreenCount => _screenCount ??= GetScreenCount(); |
||||
|
|
||||
|
public IReadOnlyList<Screen> AllScreens |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
EnsureScreens(); |
||||
|
return _allScreens; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public Action? Changed { get; set; } |
||||
|
|
||||
|
public Screen? ScreenFromWindow(IWindowBaseImpl window) => ScreenFromTopLevel(window); |
||||
|
|
||||
|
public Screen? ScreenFromTopLevel(ITopLevelImpl topLevel) => ScreenFromTopLevelCore(topLevel); |
||||
|
|
||||
|
public Screen? ScreenFromPoint(PixelPoint point) => ScreenFromPointCore(point); |
||||
|
|
||||
|
public Screen? ScreenFromRect(PixelRect rect) => ScreenFromRectCore(rect); |
||||
|
|
||||
|
public void OnChanged() |
||||
|
{ |
||||
|
// Mark cached fields invalid.
|
||||
|
_screenCount = null; |
||||
|
_allScreens = null; |
||||
|
// Schedule a delayed job, so we can accumulate multiple continuous events into one.
|
||||
|
// Also, if OnChanged was raises on non-UI thread - dispatch it.
|
||||
|
_onChangeOperation?.Abort(); |
||||
|
_onChangeOperation = Dispatcher.UIThread.InvokeAsync(() => |
||||
|
{ |
||||
|
// Ensure screens if there is at least one subscriber already,
|
||||
|
// Or at least one screen was previously materialized, which we need to update now.
|
||||
|
if (Changed is not null || _allScreensByKey.Count > 0) |
||||
|
{ |
||||
|
EnsureScreens(); |
||||
|
Changed?.Invoke(); |
||||
|
} |
||||
|
}, DispatcherPriority.Input); |
||||
|
} |
||||
|
|
||||
|
public async Task<bool> RequestScreenDetails() |
||||
|
{ |
||||
|
_screenDetailsRequestGranted ??= await RequestScreenDetailsCore(); |
||||
|
|
||||
|
return _screenDetailsRequestGranted.Value; |
||||
|
} |
||||
|
|
||||
|
protected bool TryGetScreen(TKey key, [MaybeNullWhen(false)] out TScreen screen) |
||||
|
{ |
||||
|
EnsureScreens(); |
||||
|
return _allScreensByKey.TryGetValue(key, out screen); |
||||
|
} |
||||
|
|
||||
|
protected virtual void ScreenAdded(TScreen screen) => ScreenChanged(screen); |
||||
|
protected virtual void ScreenChanged(TScreen screen) {} |
||||
|
protected virtual void ScreenRemoved(TScreen screen) => screen.OnRemoved(); |
||||
|
protected virtual int GetScreenCount() => AllScreens.Count; |
||||
|
protected abstract IReadOnlyList<TKey> GetAllScreenKeys(); |
||||
|
protected abstract TScreen CreateScreenFromKey(TKey key); |
||||
|
protected virtual Task<bool> RequestScreenDetailsCore() => Task.FromResult(true); |
||||
|
|
||||
|
protected virtual Screen? ScreenFromTopLevelCore(ITopLevelImpl topLevel) |
||||
|
{ |
||||
|
if (topLevel is IWindowImpl window) |
||||
|
{ |
||||
|
return ScreenHelper.ScreenFromWindow(window, AllScreens); |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
protected virtual Screen? ScreenFromPointCore(PixelPoint point) => ScreenHelper.ScreenFromPoint(point, AllScreens); |
||||
|
|
||||
|
protected virtual Screen? ScreenFromRectCore(PixelRect rect) => ScreenHelper.ScreenFromRect(rect, AllScreens); |
||||
|
|
||||
|
[MemberNotNull(nameof(_allScreens))] |
||||
|
private void EnsureScreens() |
||||
|
{ |
||||
|
if (_allScreens is not null) |
||||
|
return; |
||||
|
|
||||
|
var screens = GetAllScreenKeys(); |
||||
|
var screensSet = new HashSet<TKey>(screens, screenKeyComparer); |
||||
|
|
||||
|
_allScreens = new TScreen[screens.Count]; |
||||
|
|
||||
|
foreach (var oldScreenKey in _allScreensByKey.Keys) |
||||
|
{ |
||||
|
if (!screensSet.Contains(oldScreenKey)) |
||||
|
{ |
||||
|
if (_allScreensByKey.TryGetValue(oldScreenKey, out var screen) |
||||
|
&& _allScreensByKey.Remove(oldScreenKey)) |
||||
|
{ |
||||
|
ScreenRemoved(screen); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
int i = 0; |
||||
|
foreach (var newScreenKey in screens) |
||||
|
{ |
||||
|
if (_allScreensByKey.TryGetValue(newScreenKey, out var oldScreen)) |
||||
|
{ |
||||
|
ScreenChanged(oldScreen); |
||||
|
_allScreens[i] = oldScreen; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var newScreen = CreateScreenFromKey(newScreenKey); |
||||
|
ScreenAdded(newScreen); |
||||
|
_allScreensByKey[newScreenKey] = newScreen; |
||||
|
_allScreens[i] = newScreen; |
||||
|
} |
||||
|
|
||||
|
i++; |
||||
|
} |
||||
|
} |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,10 @@ |
|||||
|
EnumDisplayMonitors |
||||
|
EnumDisplayMonitors |
||||
|
GetMonitorInfo |
||||
|
MONITORINFOEX |
||||
|
EnumDisplaySettings |
||||
|
GetDisplayConfigBufferSizes |
||||
|
QueryDisplayConfig |
||||
|
DisplayConfigGetDeviceInfo |
||||
|
DISPLAYCONFIG_SOURCE_DEVICE_NAME |
||||
|
DISPLAYCONFIG_TARGET_DEVICE_NAME |
||||
@ -1,124 +1,98 @@ |
|||||
using System; |
using System; |
||||
using System.Collections.Generic; |
using System.Collections.Generic; |
||||
using System.Linq; |
using System.Runtime.CompilerServices; |
||||
using Avalonia.Metadata; |
using System.Runtime.InteropServices; |
||||
using Avalonia.Platform; |
using Avalonia.Platform; |
||||
|
using Avalonia.Win32.Interop; |
||||
|
using Windows.Win32; |
||||
using static Avalonia.Win32.Interop.UnmanagedMethods; |
using static Avalonia.Win32.Interop.UnmanagedMethods; |
||||
|
using winmdroot = global::Windows.Win32; |
||||
|
|
||||
namespace Avalonia.Win32 |
namespace Avalonia.Win32; |
||||
|
|
||||
|
internal unsafe class ScreenImpl : ScreensBase<nint, WinScreen> |
||||
{ |
{ |
||||
internal class ScreenImpl : IScreenImpl |
protected override int GetScreenCount() => GetSystemMetrics(SystemMetric.SM_CMONITORS); |
||||
{ |
|
||||
private Screen[]? _allScreens; |
|
||||
|
|
||||
/// <inheritdoc />
|
protected override IReadOnlyList<nint> GetAllScreenKeys() |
||||
public int ScreenCount |
{ |
||||
|
var screens = new List<nint>(); |
||||
|
var gcHandle = GCHandle.Alloc(screens); |
||||
|
try |
||||
|
{ |
||||
|
PInvoke.EnumDisplayMonitors(default, default(winmdroot.Foundation.RECT*), EnumDisplayMonitorsCallback, (IntPtr)gcHandle); |
||||
|
} |
||||
|
finally |
||||
{ |
{ |
||||
get => GetSystemMetrics(SystemMetric.SM_CMONITORS); |
gcHandle.Free(); |
||||
} |
} |
||||
|
|
||||
/// <inheritdoc />
|
return screens; |
||||
public IReadOnlyList<Screen> AllScreens |
|
||||
|
static winmdroot.Foundation.BOOL EnumDisplayMonitorsCallback( |
||||
|
winmdroot.Graphics.Gdi.HMONITOR monitor, |
||||
|
winmdroot.Graphics.Gdi.HDC hdcMonitor, |
||||
|
winmdroot.Foundation.RECT* lprcMonitor, |
||||
|
winmdroot.Foundation.LPARAM dwData) |
||||
{ |
{ |
||||
get |
if (GCHandle.FromIntPtr(dwData).Target is List<nint> screens) |
||||
{ |
{ |
||||
if (_allScreens == null) |
screens.Add(monitor); |
||||
{ |
return true; |
||||
int index = 0; |
|
||||
Screen[] screens = new Screen[ScreenCount]; |
|
||||
EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, |
|
||||
(IntPtr monitor, IntPtr hdcMonitor, ref Rect lprcMonitor, IntPtr data) => |
|
||||
{ |
|
||||
MONITORINFO monitorInfo = MONITORINFO.Create(); |
|
||||
if (GetMonitorInfo(monitor, ref monitorInfo)) |
|
||||
{ |
|
||||
var dpi = 1.0; |
|
||||
|
|
||||
var shcore = LoadLibrary("shcore.dll"); |
|
||||
var method = GetProcAddress(shcore, nameof(GetDpiForMonitor)); |
|
||||
if (method != IntPtr.Zero) |
|
||||
{ |
|
||||
GetDpiForMonitor(monitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var x, out _); |
|
||||
dpi = x; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
var hdc = GetDC(IntPtr.Zero); |
|
||||
|
|
||||
double virtW = GetDeviceCaps(hdc, DEVICECAP.HORZRES); |
|
||||
double physW = GetDeviceCaps(hdc, DEVICECAP.DESKTOPHORZRES); |
|
||||
|
|
||||
dpi = (96d * physW / virtW); |
|
||||
|
|
||||
ReleaseDC(IntPtr.Zero, hdc); |
|
||||
} |
|
||||
|
|
||||
RECT bounds = monitorInfo.rcMonitor; |
|
||||
RECT workingArea = monitorInfo.rcWork; |
|
||||
PixelRect avaloniaBounds = bounds.ToPixelRect(); |
|
||||
PixelRect avaloniaWorkArea = workingArea.ToPixelRect(); |
|
||||
screens[index] = |
|
||||
new WinScreen(dpi / 96.0d, avaloniaBounds, avaloniaWorkArea, monitorInfo.dwFlags == 1, |
|
||||
monitor); |
|
||||
index++; |
|
||||
} |
|
||||
return true; |
|
||||
}, IntPtr.Zero); |
|
||||
_allScreens = screens; |
|
||||
} |
|
||||
return _allScreens; |
|
||||
} |
} |
||||
|
return false; |
||||
} |
} |
||||
|
} |
||||
|
|
||||
|
protected override WinScreen CreateScreenFromKey(nint key) => new(key); |
||||
|
protected override void ScreenChanged(WinScreen screen) => screen.Refresh(); |
||||
|
|
||||
public void InvalidateScreensCache() |
protected override Screen? ScreenFromTopLevelCore(ITopLevelImpl topLevel) |
||||
|
{ |
||||
|
if (topLevel.Handle?.Handle is { } handle) |
||||
{ |
{ |
||||
_allScreens = null; |
return ScreenFromHwnd(handle); |
||||
} |
} |
||||
|
|
||||
/// <inheritdoc />
|
return null; |
||||
public Screen? ScreenFromWindow(IWindowBaseImpl window) |
} |
||||
{ |
|
||||
var handle = window.Handle?.Handle; |
|
||||
|
|
||||
if (handle is null) |
protected override Screen? ScreenFromPointCore(PixelPoint point) |
||||
{ |
{ |
||||
return null; |
var monitor = MonitorFromPoint(new POINT |
||||
} |
{ |
||||
|
X = point.X, |
||||
var monitor = MonitorFromWindow(handle.Value, MONITOR.MONITOR_DEFAULTTONULL); |
Y = point.Y |
||||
|
}, UnmanagedMethods.MONITOR.MONITOR_DEFAULTTONULL); |
||||
|
|
||||
return FindScreenByHandle(monitor); |
return ScreenFromHMonitor(monitor); |
||||
} |
} |
||||
|
|
||||
/// <inheritdoc />
|
protected override Screen? ScreenFromRectCore(PixelRect rect) |
||||
public Screen? ScreenFromPoint(PixelPoint point) |
{ |
||||
|
var monitor = MonitorFromRect(new RECT |
||||
{ |
{ |
||||
var monitor = MonitorFromPoint(new POINT |
left = rect.TopLeft.X, |
||||
{ |
top = rect.TopLeft.Y, |
||||
X = point.X, |
right = rect.TopRight.X, |
||||
Y = point.Y |
bottom = rect.BottomRight.Y |
||||
}, MONITOR.MONITOR_DEFAULTTONULL); |
}, UnmanagedMethods.MONITOR.MONITOR_DEFAULTTONULL); |
||||
|
|
||||
return FindScreenByHandle(monitor); |
return ScreenFromHMonitor(monitor); |
||||
} |
} |
||||
|
|
||||
/// <inheritdoc />
|
public WinScreen? ScreenFromHMonitor(IntPtr hmonitor) |
||||
public Screen? ScreenFromRect(PixelRect rect) |
{ |
||||
{ |
if (TryGetScreen(hmonitor, out var screen)) |
||||
var monitor = MonitorFromRect(new RECT |
return screen; |
||||
{ |
|
||||
left = rect.TopLeft.X, |
|
||||
top = rect.TopLeft.Y, |
|
||||
right = rect.TopRight.X, |
|
||||
bottom = rect.BottomRight.Y |
|
||||
}, MONITOR.MONITOR_DEFAULTTONULL); |
|
||||
|
|
||||
return FindScreenByHandle(monitor); |
return null; |
||||
} |
} |
||||
|
|
||||
private Screen? FindScreenByHandle(IntPtr handle) |
public WinScreen? ScreenFromHwnd(IntPtr hwnd, MONITOR flags = MONITOR.MONITOR_DEFAULTTONULL) |
||||
{ |
{ |
||||
return AllScreens.Cast<WinScreen>().FirstOrDefault(m => m.Handle == handle); |
var monitor = MonitorFromWindow(hwnd, flags); |
||||
} |
|
||||
|
return ScreenFromHMonitor(monitor); |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -1,30 +1,126 @@ |
|||||
using System; |
using System; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using Windows.Win32; |
||||
|
using Windows.Win32.Devices.Display; |
||||
|
using Windows.Win32.Foundation; |
||||
|
using Windows.Win32.Graphics.Gdi; |
||||
using Avalonia.Platform; |
using Avalonia.Platform; |
||||
|
using static Avalonia.Win32.Interop.UnmanagedMethods; |
||||
|
|
||||
namespace Avalonia.Win32 |
namespace Avalonia.Win32; |
||||
|
|
||||
|
internal sealed unsafe class WinScreen(IntPtr hMonitor) : PlatformScreen(new PlatformHandle(hMonitor, "HMonitor")) |
||||
{ |
{ |
||||
internal class WinScreen : Screen |
private static readonly Lazy<bool> s_hasGetDpiForMonitor = new(() => |
||||
|
{ |
||||
|
var shcore = LoadLibrary("shcore.dll"); |
||||
|
var method = GetProcAddress(shcore, nameof(GetDpiForMonitor)); |
||||
|
return method != IntPtr.Zero; |
||||
|
}); |
||||
|
|
||||
|
internal int Frequency { get; private set; } |
||||
|
|
||||
|
public void Refresh() |
||||
{ |
{ |
||||
private readonly IntPtr _hMonitor; |
var info = MONITORINFOEX.Create(); |
||||
|
PInvoke.GetMonitorInfo(new HMONITOR(hMonitor), (MONITORINFO*)&info); |
||||
|
|
||||
public WinScreen(double scaling, PixelRect bounds, PixelRect workingArea, bool isPrimary, IntPtr hMonitor) |
IsPrimary = info.Base.dwFlags == 1; |
||||
: base(scaling, bounds, workingArea, isPrimary) |
Bounds = info.Base.rcMonitor.ToPixelRect(); |
||||
|
WorkingArea = info.Base.rcWork.ToPixelRect(); |
||||
|
Scaling = GetScaling(); |
||||
|
DisplayName ??= GetDisplayName(ref info); |
||||
|
|
||||
|
var deviceMode = new DEVMODEW |
||||
{ |
{ |
||||
_hMonitor = hMonitor; |
dmFields = DEVMODE_FIELD_FLAGS.DM_DISPLAYORIENTATION | DEVMODE_FIELD_FLAGS.DM_DISPLAYFREQUENCY, |
||||
} |
dmSize = (ushort)Marshal.SizeOf<DEVMODEW>() |
||||
|
}; |
||||
|
PInvoke.EnumDisplaySettings(info.szDevice.ToString(), ENUM_DISPLAY_SETTINGS_MODE.ENUM_CURRENT_SETTINGS, |
||||
|
ref deviceMode); |
||||
|
|
||||
public IntPtr Handle => _hMonitor; |
Frequency = (int)deviceMode.dmDisplayFrequency; |
||||
|
CurrentOrientation = deviceMode.Anonymous1.Anonymous2.dmDisplayOrientation switch |
||||
|
{ |
||||
|
DEVMODE_DISPLAY_ORIENTATION.DMDO_DEFAULT => ScreenOrientation.Landscape, |
||||
|
DEVMODE_DISPLAY_ORIENTATION.DMDO_90 => ScreenOrientation.Portrait, |
||||
|
DEVMODE_DISPLAY_ORIENTATION.DMDO_180 => ScreenOrientation.LandscapeFlipped, |
||||
|
DEVMODE_DISPLAY_ORIENTATION.DMDO_270 => ScreenOrientation.PortraitFlipped, |
||||
|
_ => ScreenOrientation.None |
||||
|
}; |
||||
|
} |
||||
|
|
||||
/// <inheritdoc />
|
private string? GetDisplayName(ref MONITORINFOEX monitorinfo) |
||||
public override int GetHashCode() |
{ |
||||
|
var deviceName = monitorinfo.szDevice; |
||||
|
if (Win32Platform.WindowsVersion >= PlatformConstants.Windows7) |
||||
{ |
{ |
||||
return _hMonitor.GetHashCode(); |
if (PInvoke.GetDisplayConfigBufferSizes( |
||||
|
QUERY_DISPLAY_CONFIG_FLAGS.QDC_ONLY_ACTIVE_PATHS, |
||||
|
out var numPathInfo, out var numModeInfo) != WIN32_ERROR.NO_ERROR) |
||||
|
return null; |
||||
|
|
||||
|
var paths = stackalloc DISPLAYCONFIG_PATH_INFO[(int)numPathInfo]; |
||||
|
var modes = stackalloc DISPLAYCONFIG_MODE_INFO[(int)numModeInfo]; |
||||
|
|
||||
|
if (PInvoke.QueryDisplayConfig( |
||||
|
QUERY_DISPLAY_CONFIG_FLAGS.QDC_ONLY_ACTIVE_PATHS, ref numPathInfo, paths, ref numModeInfo, modes, |
||||
|
default) != WIN32_ERROR.NO_ERROR) |
||||
|
return null; |
||||
|
|
||||
|
var sourceName = new DISPLAYCONFIG_SOURCE_DEVICE_NAME(); |
||||
|
sourceName.header.type = DISPLAYCONFIG_DEVICE_INFO_TYPE.DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME; |
||||
|
sourceName.header.size = (uint)sizeof(DISPLAYCONFIG_SOURCE_DEVICE_NAME); |
||||
|
var targetName = new DISPLAYCONFIG_TARGET_DEVICE_NAME(); |
||||
|
targetName.header.type = DISPLAYCONFIG_DEVICE_INFO_TYPE.DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME; |
||||
|
targetName.header.size = (uint)sizeof(DISPLAYCONFIG_TARGET_DEVICE_NAME); |
||||
|
|
||||
|
for (var i = 0; i < numPathInfo; i++) |
||||
|
{ |
||||
|
sourceName.header.adapterId = paths[i].targetInfo.adapterId; |
||||
|
sourceName.header.id = paths[i].sourceInfo.id; |
||||
|
|
||||
|
targetName.header.adapterId = paths[i].targetInfo.adapterId; |
||||
|
targetName.header.id = paths[i].targetInfo.id; |
||||
|
|
||||
|
if (PInvoke.DisplayConfigGetDeviceInfo(ref sourceName.header) != 0) |
||||
|
break; |
||||
|
|
||||
|
if (!sourceName.viewGdiDeviceName.Equals(deviceName.ToString())) |
||||
|
continue; |
||||
|
|
||||
|
if (PInvoke.DisplayConfigGetDeviceInfo(ref targetName.header) != 0) |
||||
|
break; |
||||
|
|
||||
|
return targetName.monitorFriendlyDeviceName.ToString(); |
||||
|
} |
||||
} |
} |
||||
|
|
||||
/// <inheritdoc />
|
// Fallback to MONITORINFOEX - \\DISPLAY1.
|
||||
public override bool Equals(object? obj) |
return deviceName.ToString(); |
||||
|
} |
||||
|
|
||||
|
private double GetScaling() |
||||
|
{ |
||||
|
double dpi; |
||||
|
|
||||
|
if (s_hasGetDpiForMonitor.Value) |
||||
{ |
{ |
||||
return obj is WinScreen screen && _hMonitor == screen._hMonitor; |
GetDpiForMonitor(hMonitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var x, out _); |
||||
|
dpi = x; |
||||
} |
} |
||||
|
else |
||||
|
{ |
||||
|
var hdc = GetDC(IntPtr.Zero); |
||||
|
|
||||
|
double virtW = GetDeviceCaps(hdc, DEVICECAP.HORZRES); |
||||
|
double physW = GetDeviceCaps(hdc, DEVICECAP.DESKTOPHORZRES); |
||||
|
|
||||
|
dpi = (96d * physW / virtW); |
||||
|
|
||||
|
ReleaseDC(IntPtr.Zero, hdc); |
||||
|
} |
||||
|
|
||||
|
return dpi / 96d; |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,173 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Threading; |
||||
|
using Avalonia.UnitTests; |
||||
|
using Xunit; |
||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Controls.UnitTests.Platform; |
||||
|
|
||||
|
public class ScreensTests : ScopedTestBase |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Should_Preserve_Old_Screens_On_Changes() |
||||
|
{ |
||||
|
using var _ = UnitTestApplication.Start(TestServices.MockThreadingInterface); |
||||
|
|
||||
|
var screens = new TestScreens(); |
||||
|
var totalScreens = new HashSet<TestScreen>(); |
||||
|
|
||||
|
Assert.Equal(0, screens.ScreenCount); |
||||
|
Assert.Empty(screens.AllScreens); |
||||
|
|
||||
|
// Push 2 screens.
|
||||
|
screens.PushNewScreens([1, 2]); |
||||
|
Dispatcher.UIThread.RunJobs(); |
||||
|
|
||||
|
Assert.Equal(2, screens.ScreenCount); |
||||
|
totalScreens.Add(Assert.IsType<TestScreen>(screens.GetScreen(1))); |
||||
|
totalScreens.Add(Assert.IsType<TestScreen>(screens.GetScreen(2))); |
||||
|
|
||||
|
// Push 3 screens, while removing one old.
|
||||
|
screens.PushNewScreens([2, 3, 4]); |
||||
|
Dispatcher.UIThread.RunJobs(); |
||||
|
|
||||
|
Assert.Equal(3, screens.ScreenCount); |
||||
|
Assert.Null(screens.GetScreen(1)); |
||||
|
totalScreens.Add(Assert.IsType<TestScreen>(screens.GetScreen(2))); |
||||
|
totalScreens.Add(Assert.IsType<TestScreen>(screens.GetScreen(3))); |
||||
|
totalScreens.Add(Assert.IsType<TestScreen>(screens.GetScreen(4))); |
||||
|
|
||||
|
Assert.Equal(3, screens.AllScreens.Count); |
||||
|
Assert.Equal(3, screens.ScreenCount); |
||||
|
Assert.Equal(4, totalScreens.Count); |
||||
|
|
||||
|
Assert.Collection( |
||||
|
totalScreens, |
||||
|
s1 => Assert.True(s1.Generation < 0), // this screen was removed.
|
||||
|
s2 => Assert.Equal(2, s2.Generation), // this screen survived first OnChange event, instance should be preserved.
|
||||
|
s3 => Assert.Equal(1, s3.Generation), |
||||
|
s4 => Assert.Equal(1, s4.Generation)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Preserve_Old_Screens_On_Changes_Same_Instance() |
||||
|
{ |
||||
|
using var _ = UnitTestApplication.Start(TestServices.MockThreadingInterface); |
||||
|
|
||||
|
var screens = new TestScreens(); |
||||
|
|
||||
|
Assert.Equal(0, screens.ScreenCount); |
||||
|
Assert.Empty(screens.AllScreens); |
||||
|
|
||||
|
screens.PushNewScreens([1]); |
||||
|
Dispatcher.UIThread.RunJobs(); |
||||
|
|
||||
|
var screen = screens.GetScreen(1); |
||||
|
|
||||
|
Assert.Equal(1, screen.Generation); |
||||
|
Assert.Equal(new IntPtr(1), screen.TryGetPlatformHandle()!.Handle); |
||||
|
|
||||
|
screens.PushNewScreens([1]); |
||||
|
Dispatcher.UIThread.RunJobs(); |
||||
|
|
||||
|
Assert.Equal(2, screen.Generation); |
||||
|
Assert.Equal(new IntPtr(1), screen.TryGetPlatformHandle()!.Handle); |
||||
|
Assert.Same(screens.GetScreen(1), screen); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Raise_Event_And_Update_Screens_On_Changed() |
||||
|
{ |
||||
|
using var _ = UnitTestApplication.Start(TestServices.MockThreadingInterface); |
||||
|
|
||||
|
var hasChangedTimes = 0; |
||||
|
var screens = new TestScreens(); |
||||
|
screens.Changed = () => hasChangedTimes += 1; |
||||
|
|
||||
|
Assert.Equal(0, screens.ScreenCount); |
||||
|
Assert.Empty(screens.AllScreens); |
||||
|
|
||||
|
screens.PushNewScreens([1, 2]); |
||||
|
screens.PushNewScreens([1, 2]); // OnChanged can be triggered multiple times by different events
|
||||
|
Dispatcher.UIThread.RunJobs(); |
||||
|
|
||||
|
Assert.Equal(2, screens.ScreenCount); |
||||
|
Assert.NotEmpty(screens.AllScreens); |
||||
|
|
||||
|
Assert.Equal(1, hasChangedTimes); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Raise_Event_When_Screen_Changed_From_Another_Thread() |
||||
|
{ |
||||
|
using var _ = UnitTestApplication.Start(TestServices.MockThreadingInterface); |
||||
|
|
||||
|
var hasChangedTimes = 0; |
||||
|
var screens = new TestScreens(); |
||||
|
screens.Changed = () => |
||||
|
{ |
||||
|
Dispatcher.UIThread.VerifyAccess(); |
||||
|
hasChangedTimes += 1; |
||||
|
}; |
||||
|
|
||||
|
Task.Run(() => screens.PushNewScreens([1, 2])).Wait(); |
||||
|
Dispatcher.UIThread.RunJobs(); |
||||
|
|
||||
|
Assert.Equal(1, hasChangedTimes); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Trigger_Changed_When_Screen_Removed() |
||||
|
{ |
||||
|
using var _ = UnitTestApplication.Start(TestServices.MockThreadingInterface); |
||||
|
|
||||
|
var screens = new TestScreens(); |
||||
|
screens.PushNewScreens([1, 2]); |
||||
|
Dispatcher.UIThread.RunJobs(); |
||||
|
|
||||
|
var hasChangedTimes = 0; |
||||
|
var screen = screens.GetScreen(2); |
||||
|
screens.Changed = () => |
||||
|
{ |
||||
|
Assert.True(screen.Generation < 0); |
||||
|
hasChangedTimes += 1; |
||||
|
}; |
||||
|
|
||||
|
screens.PushNewScreens([1]); |
||||
|
Dispatcher.UIThread.RunJobs(); |
||||
|
|
||||
|
Assert.Equal(1, hasChangedTimes); |
||||
|
} |
||||
|
|
||||
|
private class TestScreens : ScreensBase<int, TestScreen> |
||||
|
{ |
||||
|
private IReadOnlyList<int> _keys = []; |
||||
|
private int _count; |
||||
|
|
||||
|
public void PushNewScreens(IReadOnlyList<int> keys) |
||||
|
{ |
||||
|
_count = keys.Count; |
||||
|
_keys = keys; |
||||
|
OnChanged(); |
||||
|
} |
||||
|
|
||||
|
public TestScreen GetScreen(int key) => TryGetScreen(key, out var screen) ? screen : null; |
||||
|
|
||||
|
protected override int GetScreenCount() => _count; |
||||
|
|
||||
|
protected override IReadOnlyList<int> GetAllScreenKeys() => _keys; |
||||
|
|
||||
|
protected override TestScreen CreateScreenFromKey(int key) => new(key); |
||||
|
protected override void ScreenChanged(TestScreen screen) => screen.Generation++; |
||||
|
protected override void ScreenRemoved(TestScreen screen) => screen.Generation = -1000; |
||||
|
} |
||||
|
|
||||
|
public class TestScreen(int key) : PlatformScreen(new PlatformHandle(new IntPtr(key), "TestHandle")) |
||||
|
{ |
||||
|
public int Generation { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,64 @@ |
|||||
|
using System; |
||||
|
using System.Globalization; |
||||
|
using Avalonia.Platform; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.IntegrationTests.Appium; |
||||
|
|
||||
|
[Collection("Default")] |
||||
|
public class ScreenTests |
||||
|
{ |
||||
|
private readonly AppiumDriver _session; |
||||
|
|
||||
|
public ScreenTests(DefaultAppFixture fixture) |
||||
|
{ |
||||
|
_session = fixture.Session; |
||||
|
|
||||
|
var tabs = _session.FindElementByAccessibilityId("MainTabs"); |
||||
|
var tab = tabs.FindElementByName("Screens"); |
||||
|
tab.Click(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Can_Read_Current_Screen_Info() |
||||
|
{ |
||||
|
var refreshButton = _session.FindElementByAccessibilityId("ScreenRefresh"); |
||||
|
refreshButton.SendClick(); |
||||
|
|
||||
|
var screenName = _session.FindElementByAccessibilityId("ScreenName").Text; |
||||
|
var screenHandle = _session.FindElementByAccessibilityId("ScreenHandle").Text; |
||||
|
var screenBounds = Rect.Parse(_session.FindElementByAccessibilityId("ScreenBounds").Text); |
||||
|
var screenWorkArea = Rect.Parse(_session.FindElementByAccessibilityId("ScreenWorkArea").Text); |
||||
|
var screenScaling = double.Parse(_session.FindElementByAccessibilityId("ScreenScaling").Text, NumberStyles.Float, CultureInfo.InvariantCulture); |
||||
|
var screenOrientation = Enum.Parse<ScreenOrientation>(_session.FindElementByAccessibilityId("ScreenOrientation").Text); |
||||
|
|
||||
|
Assert.NotNull(screenName); |
||||
|
Assert.NotNull(screenHandle); |
||||
|
Assert.True(screenBounds.Size is { Width: > 0, Height: > 0 }); |
||||
|
Assert.True(screenWorkArea.Size is { Width: > 0, Height: > 0 }); |
||||
|
Assert.True(screenBounds.Size.Width >= screenWorkArea.Size.Width); |
||||
|
Assert.True(screenBounds.Size.Height >= screenWorkArea.Size.Height); |
||||
|
Assert.True(screenScaling > 0); |
||||
|
Assert.True(screenOrientation != ScreenOrientation.None); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Returns_The_Same_Screen_Instance() |
||||
|
{ |
||||
|
var refreshButton = _session.FindElementByAccessibilityId("ScreenRefresh"); |
||||
|
refreshButton.SendClick(); |
||||
|
|
||||
|
var screenName1 = _session.FindElementByAccessibilityId("ScreenName").Text; |
||||
|
var screenHandle1 = _session.FindElementByAccessibilityId("ScreenHandle").Text; |
||||
|
|
||||
|
refreshButton.SendClick(); |
||||
|
|
||||
|
var screenName2 = _session.FindElementByAccessibilityId("ScreenName").Text; |
||||
|
var screenHandle2 = _session.FindElementByAccessibilityId("ScreenHandle").Text; |
||||
|
var screenSameReference = bool.Parse(_session.FindElementByAccessibilityId("ScreenSameReference").Text); |
||||
|
|
||||
|
Assert.Equal(screenName1, screenName2); |
||||
|
Assert.Equal(screenHandle1, screenHandle2); |
||||
|
Assert.True(screenSameReference); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue