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.Threading; |
|||
#pragma warning disable CS1591 // Private API doesn't require XML documentation.
|
|||
|
|||
namespace Avalonia.Platform |
|||
{ |
|||
[Unstable] |
|||
public interface IScreenImpl |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the total number of screens available on the device.
|
|||
/// </summary>
|
|||
int ScreenCount { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the list of all screens available on the device.
|
|||
/// </summary>
|
|||
IReadOnlyList<Screen> AllScreens { get; } |
|||
|
|||
Action? Changed { get; set; } |
|||
Screen? ScreenFromWindow(IWindowBaseImpl window); |
|||
|
|||
Screen? ScreenFromTopLevel(ITopLevelImpl topLevel); |
|||
Screen? ScreenFromPoint(PixelPoint point); |
|||
|
|||
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.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Metadata; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Win32.Interop; |
|||
using Windows.Win32; |
|||
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 |
|||
{ |
|||
private Screen[]? _allScreens; |
|||
protected override int GetScreenCount() => GetSystemMetrics(SystemMetric.SM_CMONITORS); |
|||
|
|||
/// <inheritdoc />
|
|||
public int ScreenCount |
|||
protected override IReadOnlyList<nint> GetAllScreenKeys() |
|||
{ |
|||
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 />
|
|||
public IReadOnlyList<Screen> AllScreens |
|||
return screens; |
|||
|
|||
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) |
|||
{ |
|||
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; |
|||
screens.Add(monitor); |
|||
return true; |
|||
} |
|||
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 />
|
|||
public Screen? ScreenFromWindow(IWindowBaseImpl window) |
|||
{ |
|||
var handle = window.Handle?.Handle; |
|||
return null; |
|||
} |
|||
|
|||
if (handle is null) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var monitor = MonitorFromWindow(handle.Value, MONITOR.MONITOR_DEFAULTTONULL); |
|||
protected override Screen? ScreenFromPointCore(PixelPoint point) |
|||
{ |
|||
var monitor = MonitorFromPoint(new POINT |
|||
{ |
|||
X = point.X, |
|||
Y = point.Y |
|||
}, UnmanagedMethods.MONITOR.MONITOR_DEFAULTTONULL); |
|||
|
|||
return FindScreenByHandle(monitor); |
|||
} |
|||
return ScreenFromHMonitor(monitor); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public Screen? ScreenFromPoint(PixelPoint point) |
|||
protected override Screen? ScreenFromRectCore(PixelRect rect) |
|||
{ |
|||
var monitor = MonitorFromRect(new RECT |
|||
{ |
|||
var monitor = MonitorFromPoint(new POINT |
|||
{ |
|||
X = point.X, |
|||
Y = point.Y |
|||
}, MONITOR.MONITOR_DEFAULTTONULL); |
|||
left = rect.TopLeft.X, |
|||
top = rect.TopLeft.Y, |
|||
right = rect.TopRight.X, |
|||
bottom = rect.BottomRight.Y |
|||
}, UnmanagedMethods.MONITOR.MONITOR_DEFAULTTONULL); |
|||
|
|||
return FindScreenByHandle(monitor); |
|||
} |
|||
return ScreenFromHMonitor(monitor); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public Screen? ScreenFromRect(PixelRect rect) |
|||
{ |
|||
var monitor = MonitorFromRect(new RECT |
|||
{ |
|||
left = rect.TopLeft.X, |
|||
top = rect.TopLeft.Y, |
|||
right = rect.TopRight.X, |
|||
bottom = rect.BottomRight.Y |
|||
}, MONITOR.MONITOR_DEFAULTTONULL); |
|||
public WinScreen? ScreenFromHMonitor(IntPtr hmonitor) |
|||
{ |
|||
if (TryGetScreen(hmonitor, out var screen)) |
|||
return screen; |
|||
|
|||
return FindScreenByHandle(monitor); |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
private Screen? FindScreenByHandle(IntPtr handle) |
|||
{ |
|||
return AllScreens.Cast<WinScreen>().FirstOrDefault(m => m.Handle == handle); |
|||
} |
|||
public WinScreen? ScreenFromHwnd(IntPtr hwnd, MONITOR flags = MONITOR.MONITOR_DEFAULTTONULL) |
|||
{ |
|||
var monitor = MonitorFromWindow(hwnd, flags); |
|||
|
|||
return ScreenFromHMonitor(monitor); |
|||
} |
|||
} |
|||
|
|||
@ -1,30 +1,126 @@ |
|||
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 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) |
|||
: base(scaling, bounds, workingArea, isPrimary) |
|||
IsPrimary = info.Base.dwFlags == 1; |
|||
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 />
|
|||
public override int GetHashCode() |
|||
private string? GetDisplayName(ref MONITORINFOEX monitorinfo) |
|||
{ |
|||
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 />
|
|||
public override bool Equals(object? obj) |
|||
// Fallback to MONITORINFOEX - \\DISPLAY1.
|
|||
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