Browse Source

Screens API refactor (#16295)

* 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 Screen
pull/16348/head
Max Katz 2 years ago
committed by GitHub
parent
commit
05ac6d2f1d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 12
      api/Avalonia.nupkg.xml
  2. 93
      native/Avalonia.Native/src/OSX/Screens.mm
  3. 3
      native/Avalonia.Native/src/OSX/TopLevelImpl.h
  4. 9
      native/Avalonia.Native/src/OSX/TopLevelImpl.mm
  5. 9
      native/Avalonia.Native/src/OSX/common.h
  6. 4
      native/Avalonia.Native/src/OSX/main.mm
  7. 1
      native/Avalonia.Native/src/OSX/menu.mm
  8. 3
      samples/ControlCatalog/MainView.xaml
  9. 10
      samples/ControlCatalog/MainView.xaml.cs
  10. 78
      samples/ControlCatalog/Pages/ScreenPage.cs
  11. 13
      samples/IntegrationTestApp/MainWindow.axaml
  12. 17
      samples/IntegrationTestApp/MainWindow.axaml.cs
  13. 41
      src/Avalonia.Base/Platform/PlatformHandle.cs
  14. 1
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
  15. 170
      src/Avalonia.Controls/Platform/IScreenImpl.cs
  16. 5
      src/Avalonia.Controls/Platform/ITopLevelImpl.cs
  17. 133
      src/Avalonia.Controls/Platform/Screen.cs
  18. 4
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs
  19. 104
      src/Avalonia.Controls/Screens.cs
  20. 9
      src/Avalonia.Controls/TopLevel.cs
  21. 5
      src/Avalonia.Controls/WindowBase.cs
  22. 8
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  23. 30
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  24. 1
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  25. 2
      src/Avalonia.Native/EmbeddableTopLevelImpl.cs
  26. 6
      src/Avalonia.Native/PopupImpl.cs
  27. 89
      src/Avalonia.Native/ScreenImpl.cs
  28. 12
      src/Avalonia.Native/TopLevelImpl.cs
  29. 6
      src/Avalonia.Native/WindowImpl.cs
  30. 12
      src/Avalonia.Native/WindowImplBase.cs
  31. 24
      src/Avalonia.Native/avn.idl
  32. 18
      src/Avalonia.X11/Screens/X11Screens.cs
  33. 14
      src/Avalonia.X11/X11Window.cs
  34. 23
      src/Browser/Avalonia.Browser/WinStubs.cs
  35. 31
      src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs
  36. 9
      src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
  37. 37
      src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs
  38. 6
      src/Windows/Avalonia.Win32/DirectX/DirectXUnmanagedMethods.cs
  39. 17
      src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs
  40. 32
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  41. 10
      src/Windows/Avalonia.Win32/NativeMethods.txt
  42. 10
      src/Windows/Avalonia.Win32/PopupImpl.cs
  43. 166
      src/Windows/Avalonia.Win32/ScreenImpl.cs
  44. 4
      src/Windows/Avalonia.Win32/Win32Platform.cs
  45. 6
      src/Windows/Avalonia.Win32/Win32TypeExtensions.cs
  46. 124
      src/Windows/Avalonia.Win32/WinScreen.cs
  47. 2
      src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
  48. 68
      src/Windows/Avalonia.Win32/WindowImpl.cs
  49. 2
      tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
  50. 2
      tests/Avalonia.Controls.UnitTests/MenuItemTests.cs
  51. 173
      tests/Avalonia.Controls.UnitTests/Platform/ScreensTests.cs
  52. 6
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  53. 64
      tests/Avalonia.IntegrationTests.Appium/ScreenTests.cs
  54. 3
      tests/Avalonia.UnitTests/MockWindowingPlatform.cs

12
api/Avalonia.nupkg.xml

@ -1093,6 +1093,12 @@
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left> <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right> <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression> </Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.Screens.#ctor(Avalonia.Platform.IScreenImpl)</Target>
<Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
<Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression> <Suppression>
<DiagnosticId>CP0006</DiagnosticId> <DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IAssetLoader.InvalidateAssemblyCache</Target> <Target>M:Avalonia.Platform.IAssetLoader.InvalidateAssemblyCache</Target>
@ -1141,4 +1147,10 @@
<Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left> <Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
<Right>target/netstandard2.0/Avalonia.Controls.dll</Right> <Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
</Suppression> </Suppression>
<Suppression>
<DiagnosticId>CP0009</DiagnosticId>
<Target>T:Avalonia.Controls.Screens</Target>
<Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
<Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
</Suppression>
</Suppressions> </Suppressions>

93
native/Avalonia.Native/src/OSX/Screens.mm

@ -1,36 +1,62 @@
#include "common.h" #include "common.h"
#include "AvnString.h"
class Screens : public ComSingleObject<IAvnScreens, &IID_IAvnScreens> class Screens : public ComSingleObject<IAvnScreens, &IID_IAvnScreens>
{ {
public: private:
FORWARD_IUNKNOWN() ComPtr<IAvnScreenEvents> _events;
public: public:
virtual HRESULT GetScreenCount (int* ret) override FORWARD_IUNKNOWN()
Screens(IAvnScreenEvents* events) {
_events = events;
CGDisplayRegisterReconfigurationCallback(CGDisplayReconfigurationCallBack, this);
}
virtual HRESULT GetScreenIds (
unsigned int* ptrFirstResult,
int* screenCound) override
{ {
START_COM_CALL; START_COM_CALL;
@autoreleasepool @autoreleasepool
{ {
*ret = (int)[NSScreen screens].count; auto screens = [NSScreen screens];
*screenCound = (int)screens.count;
if (ptrFirstResult == nil)
return S_OK;
for (int i = 0; i < screens.count; i++) {
ptrFirstResult[i] = [[screens objectAtIndex:i] av_displayId];
}
return S_OK; return S_OK;
} }
} }
virtual HRESULT GetScreen (int index, AvnScreen* ret) override virtual HRESULT GetScreen (
{ CGDirectDisplayID displayId,
void** localizedName,
AvnScreen* ret
) override {
START_COM_CALL; START_COM_CALL;
@autoreleasepool @autoreleasepool
{ {
if(index < 0 || index >= [NSScreen screens].count) NSScreen* screen;
{ for (NSScreen *s in NSScreen.screens) {
return E_INVALIDARG; if (s.av_displayId == displayId)
{
screen = s;
break;
}
} }
auto screen = [[NSScreen screens] objectAtIndex:index]; if (screen == nil) {
return E_INVALIDARG;
}
ret->Bounds.Height = [screen frame].size.height; ret->Bounds.Height = [screen frame].size.height;
ret->Bounds.Width = [screen frame].size.width; ret->Bounds.Width = [screen frame].size.width;
ret->Bounds.X = [screen frame].origin.x; ret->Bounds.X = [screen frame].origin.x;
@ -43,14 +69,45 @@ public:
ret->Scaling = 1; ret->Scaling = 1;
ret->IsPrimary = index == 0; ret->IsPrimary = CGDisplayIsMain(displayId);
// Compute natural orientation:
auto naturalScreenSize = CGDisplayScreenSize(displayId);
auto isNaturalLandscape = naturalScreenSize.width > naturalScreenSize.height;
// Normalize rotation:
auto rotation = (int)CGDisplayRotation(displayId) % 360;
if (rotation < 0) rotation = 360 - rotation;
// Get current orientation relative to the natural
if (rotation >= 0 && rotation < 90) {
ret->Orientation = isNaturalLandscape ? AvnScreenOrientation::Landscape : AvnScreenOrientation::Portrait;
} else if (rotation >= 90 && rotation < 180) {
ret->Orientation = isNaturalLandscape ? AvnScreenOrientation::Portrait : AvnScreenOrientation::Landscape;
} else if (rotation >= 180 && rotation < 270) {
ret->Orientation = isNaturalLandscape ? AvnScreenOrientation::LandscapeFlipped : AvnScreenOrientation::PortraitFlipped;
} else {
ret->Orientation = isNaturalLandscape ? AvnScreenOrientation::PortraitFlipped : AvnScreenOrientation::LandscapeFlipped;
}
if (@available(macOS 10.15, *)) {
*localizedName = CreateAvnString([screen localizedName]);
}
return S_OK; return S_OK;
} }
} }
private:
static void CGDisplayReconfigurationCallBack(CGDirectDisplayID display, CGDisplayChangeSummaryFlags flags, void *screens)
{
auto object = (Screens *)screens;
auto events = object->_events;
if (events != nil) {
events->OnChanged();
}
}
}; };
extern IAvnScreens* CreateScreens() extern IAvnScreens* CreateScreens(IAvnScreenEvents* events)
{ {
return new Screens(); return new Screens(events);
} }

3
native/Avalonia.Native/src/OSX/TopLevelImpl.h

@ -58,7 +58,8 @@ public:
virtual HRESULT PointToScreen(AvnPoint point, AvnPoint *ret) override; virtual HRESULT PointToScreen(AvnPoint point, AvnPoint *ret) override;
virtual HRESULT SetTransparencyMode(AvnWindowTransparencyMode mode) override; virtual HRESULT SetTransparencyMode(AvnWindowTransparencyMode mode) override;
virtual HRESULT GetCurrentDisplayId (CGDirectDisplayID* ret) override;
protected: protected:
NSCursor *cursor; NSCursor *cursor;
virtual void UpdateAppearance(); virtual void UpdateAppearance();

9
native/Avalonia.Native/src/OSX/TopLevelImpl.mm

@ -258,6 +258,15 @@ HRESULT TopLevelImpl::SetTransparencyMode(AvnWindowTransparencyMode mode) {
return S_OK; return S_OK;
} }
HRESULT TopLevelImpl::GetCurrentDisplayId (CGDirectDisplayID* ret) {
START_COM_CALL;
auto window = [View window];
*ret = [window.screen av_displayId];
return S_OK;
}
void TopLevelImpl::UpdateAppearance() { void TopLevelImpl::UpdateAppearance() {
} }

9
native/Avalonia.Native/src/OSX/common.h

@ -15,7 +15,7 @@ extern IAvnTopLevel* CreateAvnTopLevel(IAvnTopLevelEvents* events);
extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events); extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events);
extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events); extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events);
extern IAvnSystemDialogs* CreateSystemDialogs(); extern IAvnSystemDialogs* CreateSystemDialogs();
extern IAvnScreens* CreateScreens(); extern IAvnScreens* CreateScreens(IAvnScreenEvents* cb);
extern IAvnClipboard* CreateClipboard(NSPasteboard*, NSPasteboardItem*); extern IAvnClipboard* CreateClipboard(NSPasteboard*, NSPasteboardItem*);
extern NSPasteboardItem* TryGetPasteboardItem(IAvnClipboard*); extern NSPasteboardItem* TryGetPasteboardItem(IAvnClipboard*);
extern NSObject<NSDraggingSource>* CreateDraggingSource(NSDragOperation op, IAvnDndResultCallback* cb, void* handle); extern NSObject<NSDraggingSource>* CreateDraggingSource(NSDragOperation op, IAvnDndResultCallback* cb, void* handle);
@ -89,6 +89,13 @@ public:
- (void) action; - (void) action;
@end @end
@implementation NSScreen (AvNSScreen)
- (CGDirectDisplayID)av_displayId
{
return [self.deviceDescription[@"NSScreenNumber"] unsignedIntValue];
}
@end
class AvnInsidePotentialDeadlock class AvnInsidePotentialDeadlock
{ {
public: public:

4
native/Avalonia.Native/src/OSX/main.mm

@ -287,13 +287,13 @@ public:
} }
} }
virtual HRESULT CreateScreens (IAvnScreens** ppv) override virtual HRESULT CreateScreens (IAvnScreenEvents* cb, IAvnScreens** ppv) override
{ {
START_COM_CALL; START_COM_CALL;
@autoreleasepool @autoreleasepool
{ {
*ppv = ::CreateScreens (); *ppv = ::CreateScreens (cb);
return S_OK; return S_OK;
} }
} }

1
native/Avalonia.Native/src/OSX/menu.mm

@ -1,4 +1,5 @@
#include "common.h" #include "common.h"
#include "menu.h" #include "menu.h"
#include "KeyTransform.h" #include "KeyTransform.h"

3
samples/ControlCatalog/MainView.xaml

@ -196,6 +196,9 @@
<TabItem Header="HeaderedContentControl"> <TabItem Header="HeaderedContentControl">
<pages:HeaderedContentPage /> <pages:HeaderedContentPage />
</TabItem> </TabItem>
<TabItem Header="Screens">
<pages:ScreenPage />
</TabItem>
<FlyoutBase.AttachedFlyout> <FlyoutBase.AttachedFlyout>
<Flyout> <Flyout>
<StackPanel Width="152" Spacing="8"> <StackPanel Width="152" Spacing="8">

10
samples/ControlCatalog/MainView.xaml.cs

@ -25,16 +25,6 @@ namespace ControlCatalog
var sideBar = this.Get<TabControl>("Sidebar"); var sideBar = this.Get<TabControl>("Sidebar");
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime)
{
var tabItems = (sideBar.Items as IList);
tabItems?.Add(new TabItem()
{
Header = "Screens",
Content = new ScreenPage()
});
}
var themes = this.Get<ComboBox>("Themes"); var themes = this.Get<ComboBox>("Themes");
themes.SelectedItem = App.CurrentTheme; themes.SelectedItem = App.CurrentTheme;
themes.SelectionChanged += (sender, e) => themes.SelectionChanged += (sender, e) =>

78
samples/ControlCatalog/Pages/ScreenPage.cs

@ -20,33 +20,62 @@ namespace ControlCatalog.Pages
private IPen _activePen = new Pen(Brushes.Black); private IPen _activePen = new Pen(Brushes.Black);
private IPen _defaultPen = new Pen(Brushes.DarkGray); private IPen _defaultPen = new Pen(Brushes.DarkGray);
public ScreenPage()
{
var button = new Button();
button.Content = "Request ScreenDetails";
button.VerticalAlignment = Avalonia.Layout.VerticalAlignment.Top;
button.Click += async (sender, args) =>
{
var success = TopLevel.GetTopLevel(this)!.Screens is { } screens ?
await screens.RequestScreenDetails() :
false;
button.Content = "Request ScreenDetails: " + (success ? "Granted" : "Denied");
};
Content = button;
}
protected override bool BypassFlowDirectionPolicies => true; protected override bool BypassFlowDirectionPolicies => true;
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{ {
base.OnAttachedToVisualTree(e); base.OnAttachedToVisualTree(e);
if(VisualRoot is Window w)
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel is Window w)
{ {
w.PositionChanged += (_, _) => InvalidateVisual(); w.PositionChanged += (_, _) => InvalidateVisual();
} }
if (topLevel?.Screens is { } screens)
{
screens.Changed += (_, _) =>
{
Console.WriteLine("Screens Changed");
InvalidateVisual();
};
}
} }
public override void Render(DrawingContext context) public override void Render(DrawingContext context)
{ {
base.Render(context); base.Render(context);
if (!(VisualRoot is Window w)) double beginOffset = (Content as Visual)?.Bounds.Height + 10 ?? 0;
var topLevel = TopLevel.GetTopLevel(this)!;
if (topLevel.Screens is not { } screens)
{ {
var formattedText = CreateFormattedText("Current platform doesn't support Screens API.");
context.DrawText(formattedText, new Point(15, 15 + beginOffset));
return; return;
} }
var screens = w.Screens.All;
var scaling = ((IRenderRoot)w).RenderScaling;
var activeScreen = w.Screens.ScreenFromBounds(new PixelRect(w.Position, PixelSize.FromSize(w.Bounds.Size, scaling))); var activeScreen = screens.ScreenFromTopLevel(topLevel);
double maxBottom = 0; double maxBottom = 0;
for (int i = 0; i<screens.Count; i++ ) for (int i = 0; i<screens.ScreenCount; i++ )
{ {
var screen = screens[i]; var screen = screens.All[i];
if (screen.Bounds.X / 10f < _leftMost) if (screen.Bounds.X / 10f < _leftMost)
{ {
@ -63,16 +92,16 @@ namespace ControlCatalog.Pages
bool primary = screen.IsPrimary; bool primary = screen.IsPrimary;
bool active = screen.Equals(activeScreen); bool active = screen.Equals(activeScreen);
Rect boundsRect = new Rect(screen.Bounds.X / 10f + Math.Abs(_leftMost), screen.Bounds.Y / 10f+Math.Abs(_topMost), screen.Bounds.Width / 10f, Rect boundsRect = new Rect(screen.Bounds.X / 10f + Math.Abs(_leftMost), screen.Bounds.Y / 10f+Math.Abs(_topMost) + beginOffset, screen.Bounds.Width / 10f,
screen.Bounds.Height / 10f); screen.Bounds.Height / 10f);
Rect workingAreaRect = new Rect(screen.WorkingArea.X / 10f + Math.Abs(_leftMost), screen.WorkingArea.Y / 10f+Math.Abs(_topMost), screen.WorkingArea.Width / 10f, Rect workingAreaRect = new Rect(screen.WorkingArea.X / 10f + Math.Abs(_leftMost), screen.WorkingArea.Y / 10f+Math.Abs(_topMost) + beginOffset, screen.WorkingArea.Width / 10f,
screen.WorkingArea.Height / 10f); screen.WorkingArea.Height / 10f);
context.DrawRectangle(primary ? _primaryBrush : _defaultBrush, active ? _activePen : _defaultPen, boundsRect); context.DrawRectangle(primary ? _primaryBrush : _defaultBrush, active ? _activePen : _defaultPen, boundsRect);
context.DrawRectangle(primary ? _primaryBrush : _defaultBrush, active ? _activePen : _defaultPen, workingAreaRect); context.DrawRectangle(primary ? _primaryBrush : _defaultBrush, active ? _activePen : _defaultPen, workingAreaRect);
var identifier = CreateScreenIdentifier((i+1).ToString(), primary); var identifier = CreateScreenIdentifier((i+1).ToString(), primary);
var center = boundsRect.Center - new Point(identifier.Width / 2.0f, identifier.Height / 2.0f); var center = boundsRect.Center - new Point(identifier.Width / 2.0f, identifier.Height / 2.0f + beginOffset);
context.DrawText(identifier, center); context.DrawText(identifier, center);
maxBottom = Math.Max(maxBottom, boundsRect.Bottom); maxBottom = Math.Max(maxBottom, boundsRect.Bottom);
@ -80,14 +109,22 @@ namespace ControlCatalog.Pages
double currentHeight = maxBottom; double currentHeight = maxBottom;
for(int i = 0; i< screens.Count; i++) for(int i = 0; i< screens.ScreenCount; i++)
{ {
var screen = screens[i]; var screen = screens.All[i];
var formattedText = CreateFormattedText($"Screen {i+1}", 18); var formattedText = CreateFormattedText($"Screen {i+1}", 18);
context.DrawText(formattedText, new Point(0, currentHeight)); context.DrawText(formattedText, new Point(0, currentHeight));
currentHeight += 25; currentHeight += 25;
formattedText = CreateFormattedText($"DisplayName: {screen.DisplayName}");
context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 20;
formattedText = CreateFormattedText($"Handle: {screen.TryGetPlatformHandle()}");
context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 20;
formattedText = CreateFormattedText($"Bounds: {screen.Bounds.Width}:{screen.Bounds.Height}"); formattedText = CreateFormattedText($"Bounds: {screen.Bounds.Width}:{screen.Bounds.Height}");
context.DrawText(formattedText, new Point(15, currentHeight)); context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 20; currentHeight += 20;
@ -101,17 +138,26 @@ namespace ControlCatalog.Pages
currentHeight += 20; currentHeight += 20;
formattedText = CreateFormattedText($"IsPrimary: {screen.IsPrimary}"); formattedText = CreateFormattedText($"IsPrimary: {screen.IsPrimary}");
context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 20;
formattedText = CreateFormattedText($"CurrentOrientation: {screen.CurrentOrientation}");
context.DrawText(formattedText, new Point(15, currentHeight)); context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 20; currentHeight += 20;
formattedText = CreateFormattedText( $"Current: {screen.Equals(activeScreen)}"); formattedText = CreateFormattedText( $"Current: {screen.Equals(activeScreen)}");
context.DrawText(formattedText, new Point(15, currentHeight)); context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 30; currentHeight += 30;
} }
context.DrawRectangle(_activePen, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10f+Math.Abs(_topMost), w.Bounds.Width / 10, w.Bounds.Height / 10)); if (topLevel is Window w)
{
var wPos = w.Position;
var wSize = PixelSize.FromSize(w.FrameSize ?? w.ClientSize, w.DesktopScaling);
context.DrawRectangle(_activePen,
new Rect(wPos.X / 10f + Math.Abs(_leftMost), wPos.Y / 10f + Math.Abs(_topMost) + beginOffset,
wSize.Width / 10d, wSize.Height / 10d));
}
} }
private static FormattedText CreateFormattedText(string textToFormat, double size = 12) private static FormattedText CreateFormattedText(string textToFormat, double size = 12)

13
samples/IntegrationTestApp/MainWindow.axaml

@ -230,6 +230,19 @@
<TabItem Header="ScrollBar"> <TabItem Header="ScrollBar">
<ScrollBar Name="MyScrollBar" Orientation="Horizontal" AllowAutoHide="False" Width="200" Height="30" Value="20"/> <ScrollBar Name="MyScrollBar" Orientation="Horizontal" AllowAutoHide="False" Width="200" Height="30" Value="20"/>
</TabItem> </TabItem>
<TabItem Header="Screens">
<StackPanel Spacing="4">
<Button Name="ScreenRefresh" Content="Refresh" />
<TextBox Name="ScreenName" Watermark="DisplayName" UseFloatingWatermark="true" />
<TextBox Name="ScreenHandle" Watermark="Handle" UseFloatingWatermark="true" />
<TextBox Name="ScreenScaling" Watermark="Scaling" UseFloatingWatermark="true" />
<TextBox Name="ScreenBounds" Watermark="Bounds" UseFloatingWatermark="true" />
<TextBox Name="ScreenWorkArea" Watermark="WorkArea" UseFloatingWatermark="true" />
<TextBox Name="ScreenOrientation" Watermark="Orientation" UseFloatingWatermark="true" />
<TextBox Name="ScreenSameReference" Watermark="Is same reference" UseFloatingWatermark="true" />
</StackPanel>
</TabItem>
</TabControl> </TabControl>
</DockPanel> </DockPanel>
</Window> </Window>

17
samples/IntegrationTestApp/MainWindow.axaml.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using Avalonia; using Avalonia;
using Avalonia.Automation; using Avalonia.Automation;
@ -303,6 +304,8 @@ namespace IntegrationTestApp
OnShowNewWindowDecorations(); OnShowNewWindowDecorations();
if (source?.Name == nameof(ToggleTrayIconVisible)) if (source?.Name == nameof(ToggleTrayIconVisible))
OnToggleTrayIconVisible(); OnToggleTrayIconVisible();
if (source?.Name == nameof(ScreenRefresh))
OnScreenRefresh();
} }
private void OnApplyWindowDecorations(Window window) private void OnApplyWindowDecorations(Window window)
@ -376,5 +379,19 @@ namespace IntegrationTestApp
dialog.ShowDialog(this); dialog.ShowDialog(this);
} }
private Screen? _lastScreen;
private void OnScreenRefresh()
{
var lastScreen = _lastScreen;
var screen = _lastScreen = Screens.ScreenFromWindow(this);
ScreenName.Text = screen?.DisplayName;
ScreenHandle.Text = screen?.TryGetPlatformHandle()?.ToString();
ScreenBounds.Text = screen?.Bounds.ToString();
ScreenWorkArea.Text = screen?.WorkingArea.ToString();
ScreenScaling.Text = screen?.Scaling.ToString(CultureInfo.InvariantCulture);
ScreenOrientation.Text = screen?.CurrentOrientation.ToString();
ScreenSameReference.Text = ReferenceEquals(lastScreen, screen).ToString();
}
} }
} }

41
src/Avalonia.Base/Platform/PlatformHandle.cs

@ -5,7 +5,7 @@ namespace Avalonia.Platform
/// <summary> /// <summary>
/// Represents a platform-specific handle. /// Represents a platform-specific handle.
/// </summary> /// </summary>
public class PlatformHandle : IPlatformHandle public class PlatformHandle : IPlatformHandle, IEquatable<PlatformHandle>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PlatformHandle"/> class. /// Initializes a new instance of the <see cref="PlatformHandle"/> class.
@ -29,5 +29,44 @@ namespace Avalonia.Platform
/// Gets an optional string that describes what <see cref="Handle"/> represents. /// Gets an optional string that describes what <see cref="Handle"/> represents.
/// </summary> /// </summary>
public string? HandleDescriptor { get; } public string? HandleDescriptor { get; }
/// <inheritdoc/>
public override string ToString()
{
return $"PlatformHandle {{ {HandleDescriptor} = {Handle} }}";
}
/// <inheritdoc/>
public bool Equals(PlatformHandle? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
return Handle == other.Handle && HandleDescriptor == other.HandleDescriptor;
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{
if (obj is null) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((PlatformHandle)obj);
}
/// <inheritdoc/>
public override int GetHashCode()
{
return (Handle, HandleDescriptor).GetHashCode();
}
public static bool operator ==(PlatformHandle? left, PlatformHandle? right)
{
return Equals(left, right);
}
public static bool operator !=(PlatformHandle? left, PlatformHandle? right)
{
return !Equals(left, right);
}
} }
} }

1
src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs

@ -30,7 +30,6 @@ namespace Avalonia.Controls.Embedding.Offscreen
public abstract IEnumerable<object> Surfaces { get; } public abstract IEnumerable<object> Surfaces { get; }
public double DesktopScaling => _scaling; public double DesktopScaling => _scaling;
public IScreenImpl? Screen { get; }
public IPlatformHandle? Handle { get; } public IPlatformHandle? Handle { get; }
public Size ClientSize public Size ClientSize

170
src/Avalonia.Controls/Platform/IScreenImpl.cs

@ -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++;
}
}
} }
} }

5
src/Avalonia.Controls/Platform/ITopLevelImpl.cs

@ -23,11 +23,6 @@ namespace Avalonia.Platform
/// </summary> /// </summary>
double DesktopScaling { get; } double DesktopScaling { get; }
/// <summary>
/// Gets platform specific display information
/// </summary>
IScreenImpl? Screen { get; }
/// <summary> /// <summary>
/// Get the platform handle. /// Get the platform handle.
/// </summary> /// </summary>

133
src/Avalonia.Controls/Platform/Screen.cs

@ -1,13 +1,61 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using Avalonia.Diagnostics;
using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Platform namespace Avalonia.Platform
{ {
/// <summary>
/// Describes the orientation of a screen.
/// </summary>
public enum ScreenOrientation
{
/// <summary>
/// No screen orientation is specified.
/// </summary>
None,
/// <summary>
/// Specifies that the monitor is oriented in landscape mode where the width of the screen viewing area is greater than the height.
/// </summary>
Landscape = 1,
/// <summary>
/// Specifies that the monitor rotated 90 degrees in the clockwise direction to orient the screen in portrait mode
/// where the height of the screen viewing area is greater than the width.
/// </summary>
Portrait = 2,
/// <summary>
/// Specifies that the monitor rotated another 90 degrees in the clockwise direction (to equal 180 degrees) to orient the screen in landscape mode
/// where the width of the screen viewing area is greater than the height.
/// This landscape mode is flipped 180 degrees from the Landscape mode.
/// </summary>
LandscapeFlipped = 4,
/// <summary>
/// Specifies that the monitor rotated another 90 degrees in the clockwise direction (to equal 270 degrees) to orient the screen in portrait mode
/// where the height of the screen viewing area is greater than the width. This portrait mode is flipped 180 degrees from the Portrait mode.
/// </summary>
PortraitFlipped = 8
}
/// <summary> /// <summary>
/// Represents a single display screen. /// Represents a single display screen.
/// </summary> /// </summary>
public class Screen public class Screen : IEquatable<Screen>
{ {
/// <summary>
/// Gets the device name associated with a display.
/// </summary>
public string? DisplayName { get; protected set; }
/// <summary>
/// Gets the current orientation of a screen.
/// </summary>
public ScreenOrientation CurrentOrientation { get; protected set; }
/// <summary> /// <summary>
/// Gets the scaling factor applied to the screen by the operating system. /// Gets the scaling factor applied to the screen by the operating system.
/// </summary> /// </summary>
@ -15,19 +63,19 @@ namespace Avalonia.Platform
/// Multiply this value by 100 to get a percentage. /// Multiply this value by 100 to get a percentage.
/// Both X and Y scaling factors are assumed uniform. /// Both X and Y scaling factors are assumed uniform.
/// </remarks> /// </remarks>
public double Scaling { get; } public double Scaling { get; protected set; } = 1;
/// <inheritdoc cref="Scaling"/> /// <inheritdoc cref="Scaling"/>
[Obsolete("Use the Scaling property instead."), EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("Use the Scaling property instead.", true), EditorBrowsable(EditorBrowsableState.Never)]
public double PixelDensity => Scaling; public double PixelDensity => Scaling;
/// <summary> /// <summary>
/// Gets the overall pixel-size of the screen. /// Gets the overall pixel-size and position of the screen.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This generally is the raw pixel counts in both the X and Y direction. /// This generally is the raw pixel counts in both the X and Y direction.
/// </remarks> /// </remarks>
public PixelRect Bounds { get; } public PixelRect Bounds { get; protected set; }
/// <summary> /// <summary>
/// Gets the actual working-area pixel-size of the screen. /// Gets the actual working-area pixel-size of the screen.
@ -36,15 +84,15 @@ namespace Avalonia.Platform
/// This area may be smaller than <see href="Bounds"/> to account for notches and /// This area may be smaller than <see href="Bounds"/> to account for notches and
/// other block-out areas such as taskbars etc. /// other block-out areas such as taskbars etc.
/// </remarks> /// </remarks>
public PixelRect WorkingArea { get; } public PixelRect WorkingArea { get; protected set; }
/// <summary> /// <summary>
/// Gets a value indicating whether the screen is the primary one. /// Gets a value indicating whether the screen is the primary one.
/// </summary> /// </summary>
public bool IsPrimary { get; } public bool IsPrimary { get; protected set; }
/// <inheritdoc cref="IsPrimary"/> /// <inheritdoc cref="IsPrimary"/>
[Obsolete("Use the IsPrimary property instead."), EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("Use the IsPrimary property instead.", true), EditorBrowsable(EditorBrowsableState.Never)]
public bool Primary => IsPrimary; public bool Primary => IsPrimary;
/// <summary> /// <summary>
@ -54,12 +102,71 @@ namespace Avalonia.Platform
/// <param name="bounds">The overall pixel-size of the screen.</param> /// <param name="bounds">The overall pixel-size of the screen.</param>
/// <param name="workingArea">The actual working-area pixel-size of the screen.</param> /// <param name="workingArea">The actual working-area pixel-size of the screen.</param>
/// <param name="isPrimary">Whether the screen is the primary one.</param> /// <param name="isPrimary">Whether the screen is the primary one.</param>
[Unstable(ObsoletionMessages.MayBeRemovedInAvalonia12)]
public Screen(double scaling, PixelRect bounds, PixelRect workingArea, bool isPrimary) public Screen(double scaling, PixelRect bounds, PixelRect workingArea, bool isPrimary)
{ {
this.Scaling = scaling; Scaling = scaling;
this.Bounds = bounds; Bounds = bounds;
this.WorkingArea = workingArea; WorkingArea = workingArea;
this.IsPrimary = isPrimary; IsPrimary = isPrimary;
} }
private protected Screen() { }
/// <summary>
/// Tries to get the platform handle for the Screen.
/// </summary>
/// <returns>
/// An <see cref="IPlatformHandle"/> describing the screen handle, or null if the handle
/// could not be retrieved.
/// </returns>
public virtual IPlatformHandle? TryGetPlatformHandle() => null;
/// <inheritdoc/>
public bool Equals(Screen? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
return base.Equals(other);
}
public static bool operator ==(Screen? left, Screen? right)
{
return Equals(left, right);
}
public static bool operator !=(Screen? left, Screen? right)
{
return !Equals(left, right);
}
/// <inheritdoc/>
public override string ToString()
{
var sb = StringBuilderCache.Acquire();
sb.Append("Screen");
sb.Append(" { ");
// Only printing properties that are supposed to be immutable:
sb.AppendFormat("{0} = {1}", nameof(DisplayName), DisplayName);
if (TryGetPlatformHandle() is { } platformHandle)
{
sb.AppendFormat(", {0}: {1}", platformHandle.HandleDescriptor, platformHandle.Handle);
}
sb.Append(" } ");
return StringBuilderCache.GetStringAndRelease(sb);
}
/// <summary>
/// When screen is removed, we should at least empty all the properties.
/// </summary>
internal void OnRemoved()
{
DisplayName = null;
Bounds = WorkingArea = default;
Scaling = default;
CurrentOrientation = default;
}
} }
} }

4
src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs

@ -27,12 +27,12 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
{ {
get get
{ {
if (_parent.Screen is null) if (_parent.TryGetFeature<IScreenImpl>() is not { } screenImpl)
{ {
return Array.Empty<ManagedPopupPositionerScreenInfo>(); return Array.Empty<ManagedPopupPositionerScreenInfo>();
} }
return _parent.Screen.AllScreens return screenImpl.AllScreens
.Select(s => new ManagedPopupPositionerScreenInfo(s.Bounds.ToRect(1), s.WorkingArea.ToRect(1))) .Select(s => new ManagedPopupPositionerScreenInfo(s.Bounds.ToRect(1), s.WorkingArea.ToRect(1)))
.ToArray(); .ToArray();
} }

104
src/Avalonia.Controls/Screens.cs

@ -2,10 +2,10 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Avalonia.Metadata;
using Avalonia.Platform; using Avalonia.Platform;
#nullable enable
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
@ -14,25 +14,50 @@ namespace Avalonia.Controls
public class Screens public class Screens
{ {
private readonly IScreenImpl _iScreenImpl; private readonly IScreenImpl _iScreenImpl;
private EventHandler? _changedHandlers;
/// <summary> /// <summary>
/// Gets the total number of screens available on the device. /// Gets the total number of screens available on the device.
/// </summary> /// </summary>
public int ScreenCount => _iScreenImpl?.ScreenCount ?? 0; public int ScreenCount => _iScreenImpl.ScreenCount;
/// <summary> /// <summary>
/// Gets the list of all screens available on the device. /// Gets the list of all screens available on the device.
/// </summary> /// </summary>
public IReadOnlyList<Screen> All => _iScreenImpl?.AllScreens ?? Array.Empty<Screen>(); public IReadOnlyList<Screen> All => _iScreenImpl.AllScreens;
/// <summary> /// <summary>
/// Gets the primary screen on the device. /// Gets the primary screen on the device.
/// </summary> /// </summary>
public Screen? Primary => All.FirstOrDefault(x => x.IsPrimary); public Screen? Primary => All.FirstOrDefault(x => x.IsPrimary);
/// <summary>
/// Event raised when any screen was changed.
/// </summary>
public event EventHandler? Changed
{
add
{
if (_changedHandlers is null)
{
_iScreenImpl.Changed += ImplChanged;
}
_changedHandlers += value;
}
remove
{
_changedHandlers -= value;
if (_changedHandlers is null)
{
_iScreenImpl.Changed -= ImplChanged;
}
}
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Screens"/> class. /// Initializes a new instance of the <see cref="Screens"/> class.
/// </summary> /// </summary>
[PrivateApi]
public Screens(IScreenImpl iScreenImpl) public Screens(IScreenImpl iScreenImpl)
{ {
_iScreenImpl = iScreenImpl; _iScreenImpl = iScreenImpl;
@ -41,6 +66,9 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Retrieves a Screen for the display that contains the rectangle. /// Retrieves a Screen for the display that contains the rectangle.
/// </summary> /// </summary>
/// <remarks>
/// On mobile, this method always returns null.
/// </remarks>
/// <param name="bounds">Bounds that specifies the area for which to retrieve the display.</param> /// <param name="bounds">Bounds that specifies the area for which to retrieve the display.</param>
/// <returns>The <see cref="Screen"/>.</returns> /// <returns>The <see cref="Screen"/>.</returns>
public Screen? ScreenFromBounds(PixelRect bounds) public Screen? ScreenFromBounds(PixelRect bounds)
@ -56,6 +84,10 @@ namespace Avalonia.Controls
/// <returns>The <see cref="Screen"/>.</returns> /// <returns>The <see cref="Screen"/>.</returns>
public Screen? ScreenFromWindow(WindowBase window) public Screen? ScreenFromWindow(WindowBase window)
{ {
if (window is null)
{
throw new ArgumentNullException(nameof(window));
}
if (window.PlatformImpl is null) if (window.PlatformImpl is null)
{ {
throw new ObjectDisposedException("Window platform implementation was already disposed."); throw new ObjectDisposedException("Window platform implementation was already disposed.");
@ -64,12 +96,32 @@ namespace Avalonia.Controls
return _iScreenImpl.ScreenFromWindow(window.PlatformImpl); return _iScreenImpl.ScreenFromWindow(window.PlatformImpl);
} }
/// <summary>
/// Retrieves a Screen for the display that contains the specified <see cref="TopLevel"/>.
/// </summary>
/// <param name="topLevel">The top level for which to retrieve the Screen.</param>
/// <exception cref="ObjectDisposedException">TopLevel platform implementation was already disposed.</exception>
/// <returns>The <see cref="Screen"/>.</returns>
public Screen? ScreenFromTopLevel(TopLevel topLevel)
{
if (topLevel is null)
{
throw new ArgumentNullException(nameof(topLevel));
}
if (topLevel.PlatformImpl is null)
{
throw new ObjectDisposedException("Window platform implementation was already disposed.");
}
return _iScreenImpl.ScreenFromTopLevel(topLevel.PlatformImpl);
}
/// <summary> /// <summary>
/// Retrieves a Screen for the display that contains the specified <see cref="IWindowBaseImpl"/>. /// Retrieves a Screen for the display that contains the specified <see cref="IWindowBaseImpl"/>.
/// </summary> /// </summary>
/// <param name="window">The window impl for which to retrieve the Screen.</param> /// <param name="window">The window impl for which to retrieve the Screen.</param>
/// <returns>The <see cref="Screen"/>.</returns> /// <returns>The <see cref="Screen"/>.</returns>
[Obsolete("Use ScreenFromWindow(WindowBase) overload."), EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("Use ScreenFromWindow(WindowBase) overload.", true), EditorBrowsable(EditorBrowsableState.Never)]
public Screen? ScreenFromWindow(IWindowBaseImpl window) public Screen? ScreenFromWindow(IWindowBaseImpl window)
{ {
return _iScreenImpl.ScreenFromWindow(window); return _iScreenImpl.ScreenFromWindow(window);
@ -78,6 +130,9 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Retrieves a Screen for the display that contains the specified point. /// Retrieves a Screen for the display that contains the specified point.
/// </summary> /// </summary>
/// <remarks>
/// On mobile, this method always returns null.
/// </remarks>
/// <param name="point">A Point that specifies the location for which to retrieve a Screen.</param> /// <param name="point">A Point that specifies the location for which to retrieve a Screen.</param>
/// <returns>The <see cref="Screen"/>.</returns> /// <returns>The <see cref="Screen"/>.</returns>
public Screen? ScreenFromPoint(PixelPoint point) public Screen? ScreenFromPoint(PixelPoint point)
@ -92,10 +147,43 @@ namespace Avalonia.Controls
/// <returns>The <see cref="Screen"/>.</returns> /// <returns>The <see cref="Screen"/>.</returns>
public Screen? ScreenFromVisual(Visual visual) public Screen? ScreenFromVisual(Visual visual)
{ {
var tl = visual.PointToScreen(visual.Bounds.TopLeft); if (visual is null)
var br = visual.PointToScreen(visual.Bounds.BottomRight); {
throw new ArgumentNullException(nameof(visual));
}
var topLevel = TopLevel.GetTopLevel(visual);
if (topLevel is null)
{
throw new ArgumentException("Control does not belong to a visual tree.", nameof(visual));
}
if (topLevel is WindowBase)
{
var tl = visual.PointToScreen(visual.Bounds.TopLeft);
var br = visual.PointToScreen(visual.Bounds.BottomRight);
return ScreenFromBounds(new PixelRect(tl, br));
}
else
{
return ScreenFromTopLevel(topLevel);
}
}
return ScreenFromBounds(new PixelRect(tl, br)); /// <summary>
/// Asks underlying platform to provide detailed screen information.
/// On some platforms it might include non-primary screens, as well as display names.
/// </summary>
/// <remarks>
/// This method is async and might show a dialog to the user asking for a permission.
/// </remarks>
/// <returns>True, if detailed screen information was provided. False, if denied by the platform or user.</returns>
public Task<bool> RequestScreenDetails() => _iScreenImpl.RequestScreenDetails();
private void ImplChanged()
{
_changedHandlers?.Invoke(this, EventArgs.Empty);
} }
} }
} }

9
src/Avalonia.Controls/TopLevel.cs

@ -138,6 +138,7 @@ namespace Avalonia.Controls
private Border? _transparencyFallbackBorder; private Border? _transparencyFallbackBorder;
private TargetWeakEventSubscriber<TopLevel, ResourcesChangedEventArgs>? _resourcesChangesSubscriber; private TargetWeakEventSubscriber<TopLevel, ResourcesChangedEventArgs>? _resourcesChangesSubscriber;
private IStorageProvider? _storageProvider; private IStorageProvider? _storageProvider;
private Screens? _screens;
private LayoutDiagnosticBridge? _layoutDiagnosticBridge; private LayoutDiagnosticBridge? _layoutDiagnosticBridge;
/// <summary> /// <summary>
@ -540,7 +541,7 @@ namespace Avalonia.Controls
public double RenderScaling => PlatformImpl?.RenderScaling ?? 1; public double RenderScaling => PlatformImpl?.RenderScaling ?? 1;
IStyleHost IStyleHost.StylingParent => _globalStyles!; IStyleHost IStyleHost.StylingParent => _globalStyles!;
/// <summary> /// <summary>
/// File System storage service used for file pickers and bookmarks. /// File System storage service used for file pickers and bookmarks.
/// </summary> /// </summary>
@ -553,6 +554,12 @@ namespace Avalonia.Controls
public IInputPane? InputPane => PlatformImpl?.TryGetFeature<IInputPane>(); public IInputPane? InputPane => PlatformImpl?.TryGetFeature<IInputPane>();
public ILauncher Launcher => PlatformImpl?.TryGetFeature<ILauncher>() ?? new NoopLauncher(); public ILauncher Launcher => PlatformImpl?.TryGetFeature<ILauncher>() ?? new NoopLauncher();
/// <summary>
/// Gets platform screens implementation.
/// </summary>
public Screens? Screens => _screens ??=
PlatformImpl?.TryGetFeature<IScreenImpl>() is { } screenImpl ? new Screens(screenImpl) : null;
/// <summary> /// <summary>
/// Gets the platform's clipboard implementation /// Gets the platform's clipboard implementation
/// </summary> /// </summary>

5
src/Avalonia.Controls/WindowBase.cs

@ -54,7 +54,6 @@ namespace Avalonia.Controls
public WindowBase(IWindowBaseImpl impl, IAvaloniaDependencyResolver? dependencyResolver) : base(impl, dependencyResolver) public WindowBase(IWindowBaseImpl impl, IAvaloniaDependencyResolver? dependencyResolver) : base(impl, dependencyResolver)
{ {
Screens = new Screens(impl.Screen!);
impl.Activated = HandleActivated; impl.Activated = HandleActivated;
impl.Deactivated = HandleDeactivated; impl.Deactivated = HandleDeactivated;
impl.PositionChanged = HandlePositionChanged; impl.PositionChanged = HandlePositionChanged;
@ -109,7 +108,9 @@ namespace Avalonia.Controls
private set => SetAndRaise(IsActiveProperty, ref _isActive, value); private set => SetAndRaise(IsActiveProperty, ref _isActive, value);
} }
public Screens Screens { get; } /// <inheritdoc cref="TopLevel.Screens"/>
public new Screens Screens => base.Screens
?? throw new InvalidOperationException("Windowing backend wasn't properly initialized.");
/// <summary> /// <summary>
/// Gets or sets the owner of the window. /// Gets or sets the owner of the window.

8
src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs

@ -83,7 +83,6 @@ namespace Avalonia.DesignerSupport.Remote
{ {
} }
public IScreenImpl Screen { get; } = new ScreenStub();
public Action GotInputWhenDisabled { get; set; } public Action GotInputWhenDisabled { get; set; }
public Action<bool> ExtendClientAreaToDecorationsChanged { get; set; } public Action<bool> ExtendClientAreaToDecorationsChanged { get; set; }
@ -102,7 +101,12 @@ namespace Avalonia.DesignerSupport.Remote
{ {
return new NoopStorageProvider(); return new NoopStorageProvider();
} }
if (featureType == typeof(IScreenImpl))
{
return new ScreenStub();
}
return base.TryGetFeature(featureType); return base.TryGetFeature(featureType);
} }

30
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@ -123,9 +123,7 @@ namespace Avalonia.DesignerSupport.Remote
{ {
} }
public IScreenImpl Screen { get; } = new ScreenStub();
public void SetMinMaxSize(Size minSize, Size maxSize) public void SetMinMaxSize(Size minSize, Size maxSize)
{ {
} }
@ -243,26 +241,20 @@ namespace Avalonia.DesignerSupport.Remote
public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) => new IconStub(); public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) => new IconStub();
} }
class ScreenStub : IScreenImpl class ScreenStub : ScreensBase<int, PlatformScreen>
{ {
public int ScreenCount => 1; protected override IReadOnlyList<int> GetAllScreenKeys() => new[] { 1 };
public IReadOnlyList<Screen> AllScreens { get; } =
new Screen[] { new Screen(1, new PixelRect(0, 0, 4000, 4000), new PixelRect(0, 0, 4000, 4000), true) };
public Screen ScreenFromPoint(PixelPoint point)
{
return ScreenHelper.ScreenFromPoint(point, AllScreens);
}
public Screen ScreenFromRect(PixelRect rect) protected override PlatformScreen CreateScreenFromKey(int key) => new PlatformScreenStub(key);
{
return ScreenHelper.ScreenFromRect(rect, AllScreens);
}
public Screen ScreenFromWindow(IWindowBaseImpl window) private class PlatformScreenStub : PlatformScreen
{ {
return ScreenHelper.ScreenFromWindow(window, AllScreens); public PlatformScreenStub(int key) : base(new PlatformHandle((nint) key, nameof(ScreenStub)))
{
Scaling = 1;
Bounds = WorkingArea = new PixelRect(0, 0, 4000, 4000);
IsPrimary = true;
}
} }
} }

1
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -110,6 +110,7 @@ namespace Avalonia.Native
.Bind<IDispatcherImpl>() .Bind<IDispatcherImpl>()
.ToConstant(new DispatcherImpl(_factory.CreatePlatformThreadingInterface())) .ToConstant(new DispatcherImpl(_factory.CreatePlatformThreadingInterface()))
.Bind<ICursorFactory>().ToConstant(new CursorFactory(_factory.CreateCursorFactory())) .Bind<ICursorFactory>().ToConstant(new CursorFactory(_factory.CreateCursorFactory()))
.Bind<IScreenImpl>().ToConstant(new ScreenImpl(_factory.CreateScreens))
.Bind<IPlatformIconLoader>().ToSingleton<IconLoader>() .Bind<IPlatformIconLoader>().ToSingleton<IconLoader>()
.Bind<IKeyboardDevice>().ToConstant(KeyboardDevice) .Bind<IKeyboardDevice>().ToConstant(KeyboardDevice)
.Bind<IPlatformSettings>().ToConstant(new NativePlatformSettings(_factory.CreatePlatformSettings())) .Bind<IPlatformSettings>().ToConstant(new NativePlatformSettings(_factory.CreatePlatformSettings()))

2
src/Avalonia.Native/EmbeddableTopLevelImpl.cs

@ -8,7 +8,7 @@ namespace Avalonia.Native
{ {
using (var e = new TopLevelEvents(this)) using (var e = new TopLevelEvents(this))
{ {
Init(new MacOSTopLevelHandle(factory.CreateTopLevel(e)), factory.CreateScreens()); Init(new MacOSTopLevelHandle(factory.CreateTopLevel(e)));
} }
} }
} }

6
src/Avalonia.Native/PopupImpl.cs

@ -18,7 +18,7 @@ namespace Avalonia.Native
using (var e = new PopupEvents(this)) using (var e = new PopupEvents(this))
{ {
Init(new MacOSTopLevelHandle(_native = factory.CreatePopup(e)), factory.CreateScreens()); Init(new MacOSTopLevelHandle(_native = factory.CreatePopup(e)));
} }
PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize)); PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize));
@ -35,9 +35,9 @@ namespace Avalonia.Native
} }
} }
internal sealed override void Init(MacOSTopLevelHandle handle, IAvnScreens screens) internal sealed override void Init(MacOSTopLevelHandle handle)
{ {
base.Init(handle, screens); base.Init(handle);
} }
private void MoveResize(PixelPoint position, Size size, double scaling) private void MoveResize(PixelPoint position, Size size, double scaling)

89
src/Avalonia.Native/ScreenImpl.cs

@ -2,66 +2,81 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Native.Interop; using Avalonia.Native.Interop;
using Avalonia.Platform; using Avalonia.Platform;
using MicroCom.Runtime;
#nullable enable
namespace Avalonia.Native namespace Avalonia.Native
{ {
internal class ScreenImpl : IScreenImpl, IDisposable internal sealed class AvnScreen(uint displayId)
: PlatformScreen(new PlatformHandle(new IntPtr(displayId), "CGDirectDisplayID"))
{
public unsafe void Refresh(IAvnScreens native)
{
void* localizedName = null;
var screen = native.GetScreen(displayId, &localizedName);
IsPrimary = screen.IsPrimary.FromComBool();
Scaling = screen.Scaling;
Bounds = screen.Bounds.ToAvaloniaPixelRect();
WorkingArea = screen.WorkingArea.ToAvaloniaPixelRect();
CurrentOrientation = screen.Orientation switch
{
AvnScreenOrientation.UnknownOrientation => ScreenOrientation.None,
AvnScreenOrientation.Landscape => ScreenOrientation.Landscape,
AvnScreenOrientation.Portrait => ScreenOrientation.Portrait,
AvnScreenOrientation.LandscapeFlipped => ScreenOrientation.LandscapeFlipped,
AvnScreenOrientation.PortraitFlipped => ScreenOrientation.PortraitFlipped,
_ => throw new ArgumentOutOfRangeException()
};
using var avnString = MicroComRuntime.CreateProxyOrNullFor<IAvnString>(localizedName, true);
DisplayName = avnString?.String;
}
}
internal class ScreenImpl : ScreensBase<uint, AvnScreen>, IDisposable
{ {
private IAvnScreens _native; private IAvnScreens _native;
public ScreenImpl(IAvnScreens native) public ScreenImpl(Func<IAvnScreenEvents, IAvnScreens> factory)
{ {
_native = native; using var events = new AvnScreenEvents(this);
_native = factory(events);
} }
public int ScreenCount => _native.ScreenCount; protected override unsafe int GetScreenCount() => _native.GetScreenIds(null);
public IReadOnlyList<Screen> AllScreens protected override unsafe IReadOnlyList<uint> GetAllScreenKeys()
{ {
get var screenCount = _native.GetScreenIds(null);
var displayIds = new uint[screenCount];
fixed (uint* displayIdsPtr = displayIds)
{ {
if (_native != null) _native.GetScreenIds(displayIdsPtr);
{
var count = ScreenCount;
var result = new Screen[count];
for (int i = 0; i < count; i++)
{
var screen = _native.GetScreen(i);
result[i] = new Screen(
screen.Scaling,
screen.Bounds.ToAvaloniaPixelRect(),
screen.WorkingArea.ToAvaloniaPixelRect(),
screen.IsPrimary.FromComBool());
}
return result;
}
return Array.Empty<Screen>();
} }
}
public void Dispose () return displayIds;
{
_native?.Dispose();
_native = null;
} }
public Screen ScreenFromPoint(PixelPoint point) protected override AvnScreen CreateScreenFromKey(uint key) => new(key);
protected override void ScreenChanged(AvnScreen screen) => screen.Refresh(_native);
protected override Screen? ScreenFromTopLevelCore(ITopLevelImpl topLevel)
{ {
return ScreenHelper.ScreenFromPoint(point, AllScreens); var displayId = ((TopLevelImpl)topLevel).Native?.CurrentDisplayId;
return displayId is not null && TryGetScreen(displayId.Value, out var screen) ? screen : null;
} }
public Screen ScreenFromRect(PixelRect rect) public void Dispose()
{ {
return ScreenHelper.ScreenFromRect(rect, AllScreens); _native?.Dispose();
_native = null!;
} }
public Screen ScreenFromWindow(IWindowBaseImpl window) private class AvnScreenEvents(ScreenImpl screenImpl) : NativeCallbackBase, IAvnScreenEvents
{ {
return ScreenHelper.ScreenFromWindow(window, AllScreens); public void OnChanged() => screenImpl.OnChanged();
} }
} }
} }

12
src/Avalonia.Native/TopLevelImpl.cs

@ -92,7 +92,7 @@ internal class TopLevelImpl : ITopLevelImpl, IFramebufferPlatformSurface
_cursorFactory = AvaloniaLocator.Current.GetService<ICursorFactory>(); _cursorFactory = AvaloniaLocator.Current.GetService<ICursorFactory>();
} }
internal virtual void Init(MacOSTopLevelHandle handle, IAvnScreens screens) internal virtual void Init(MacOSTopLevelHandle handle)
{ {
_handle = handle; _handle = handle;
_savedLogicalSize = ClientSize; _savedLogicalSize = ClientSize;
@ -101,8 +101,6 @@ internal class TopLevelImpl : ITopLevelImpl, IFramebufferPlatformSurface
_storageProvider = new SystemDialogs(this, Factory.CreateSystemDialogs()); _storageProvider = new SystemDialogs(this, Factory.CreateSystemDialogs());
_platformBehaviorInhibition = new PlatformBehaviorInhibition(Factory.CreatePlatformBehaviorInhibition()); _platformBehaviorInhibition = new PlatformBehaviorInhibition(Factory.CreatePlatformBehaviorInhibition());
_surfaces = new object[] { new GlPlatformSurface(Native), new MetalPlatformSurface(Native), this }; _surfaces = new object[] { new GlPlatformSurface(Native), new MetalPlatformSurface(Native), this };
Screen = new ScreenImpl(screens);
InputMethod = new AvaloniaNativeTextInputMethod(Native); InputMethod = new AvaloniaNativeTextInputMethod(Native);
} }
@ -159,8 +157,6 @@ internal class TopLevelImpl : ITopLevelImpl, IFramebufferPlatformSurface
public INativeControlHostImpl? NativeControlHost => _nativeControlHost; public INativeControlHostImpl? NativeControlHost => _nativeControlHost;
public IScreenImpl? Screen { get; private set; }
public AutomationPeer? GetAutomationPeer() public AutomationPeer? GetAutomationPeer()
{ {
return _inputRoot is Control c ? ControlAutomationPeer.CreatePeerForElement(c) : null; return _inputRoot is Control c ? ControlAutomationPeer.CreatePeerForElement(c) : null;
@ -357,6 +353,11 @@ internal class TopLevelImpl : ITopLevelImpl, IFramebufferPlatformSurface
return AvaloniaLocator.Current.GetRequiredService<IClipboard>(); return AvaloniaLocator.Current.GetRequiredService<IClipboard>();
} }
if (featureType == typeof(IScreenImpl))
{
return AvaloniaLocator.Current.GetRequiredService<IScreenImpl>();
}
if (featureType == typeof(ILauncher)) if (featureType == typeof(ILauncher))
{ {
return new BclLauncher(); return new BclLauncher();
@ -373,7 +374,6 @@ internal class TopLevelImpl : ITopLevelImpl, IFramebufferPlatformSurface
_nativeControlHost?.Dispose(); _nativeControlHost?.Dispose();
_nativeControlHost = null; _nativeControlHost = null;
(Screen as ScreenImpl)?.Dispose();
_mouse?.Dispose(); _mouse?.Dispose();
} }

6
src/Avalonia.Native/WindowImpl.cs

@ -25,15 +25,15 @@ namespace Avalonia.Native
using (var e = new WindowEvents(this)) using (var e = new WindowEvents(this))
{ {
Init(new MacOSTopLevelHandle(_native = factory.CreateWindow(e)), factory.CreateScreens()); Init(new MacOSTopLevelHandle(_native = factory.CreateWindow(e)));
} }
_nativeMenuExporter = new AvaloniaNativeMenuExporter(_native, factory); _nativeMenuExporter = new AvaloniaNativeMenuExporter(_native, factory);
} }
internal sealed override void Init(MacOSTopLevelHandle handle, IAvnScreens screens) internal sealed override void Init(MacOSTopLevelHandle handle)
{ {
base.Init(handle, screens); base.Init(handle);
} }
class WindowEvents : WindowBaseEvents, IAvnWindowEvents class WindowEvents : WindowBaseEvents, IAvnWindowEvents

12
src/Avalonia.Native/WindowImplBase.cs

@ -47,14 +47,15 @@ namespace Avalonia.Native
} }
} }
internal override void Init(MacOSTopLevelHandle handle, IAvnScreens screens) internal override void Init(MacOSTopLevelHandle handle)
{ {
_handle = handle; _handle = handle;
base.Init(handle, screens); base.Init(handle);
var monitor = Screen!.AllScreens.OrderBy(x => x.Scaling) var monitor = this.TryGetFeature<IScreenImpl>()!.AllScreens
.FirstOrDefault(m => m.Bounds.Contains(Position)); .OrderBy(x => x.Scaling)
.First(m => m.Bounds.Contains(Position));
Resize(new Size(monitor!.WorkingArea.Width * 0.75d, monitor.WorkingArea.Height * 0.7d), WindowResizeReason.Layout); Resize(new Size(monitor!.WorkingArea.Width * 0.75d, monitor.WorkingArea.Height * 0.7d), WindowResizeReason.Layout);
} }
@ -96,7 +97,8 @@ namespace Avalonia.Native
Native?.BeginMoveDrag(); Native?.BeginMoveDrag();
} }
public Size MaxAutoSizeHint => Screen!.AllScreens.Select(s => s.Bounds.Size.ToSize(1)) public Size MaxAutoSizeHint => this.TryGetFeature<IScreenImpl>()!.AllScreens
.Select(s => s.Bounds.Size.ToSize(1))
.OrderByDescending(x => x.Width + x.Height).FirstOrDefault(); .OrderByDescending(x => x.Width + x.Height).FirstOrDefault();
public void SetTopmost(bool value) public void SetTopmost(bool value)

24
src/Avalonia.Native/avn.idl

@ -428,12 +428,22 @@ struct AvnPoint
double X, Y; double X, Y;
} }
enum AvnScreenOrientation
{
UnknownOrientation,
Landscape,
Portrait,
LandscapeFlipped,
PortraitFlipped
}
struct AvnScreen struct AvnScreen
{ {
AvnRect Bounds; AvnRect Bounds;
AvnRect WorkingArea; AvnRect WorkingArea;
float Scaling; float Scaling;
bool IsPrimary; bool IsPrimary;
AvnScreenOrientation Orientation;
} }
enum AvnPixelFormat enum AvnPixelFormat
@ -668,7 +678,7 @@ interface IAvaloniaNativeFactory : IUnknown
HRESULT CreatePopup(IAvnWindowEvents* cb, IAvnPopup** ppv); HRESULT CreatePopup(IAvnWindowEvents* cb, IAvnPopup** ppv);
HRESULT CreatePlatformThreadingInterface(IAvnPlatformThreadingInterface** ppv); HRESULT CreatePlatformThreadingInterface(IAvnPlatformThreadingInterface** ppv);
HRESULT CreateSystemDialogs(IAvnSystemDialogs** ppv); HRESULT CreateSystemDialogs(IAvnSystemDialogs** ppv);
HRESULT CreateScreens(IAvnScreens** ppv); HRESULT CreateScreens(IAvnScreenEvents* cb, IAvnScreens** ppv);
HRESULT CreateClipboard(IAvnClipboard** ppv); HRESULT CreateClipboard(IAvnClipboard** ppv);
HRESULT CreateDndClipboard(IAvnClipboard** ppv); HRESULT CreateDndClipboard(IAvnClipboard** ppv);
HRESULT CreateCursorFactory(IAvnCursorFactory** ppv); HRESULT CreateCursorFactory(IAvnCursorFactory** ppv);
@ -716,6 +726,8 @@ interface IAvnTopLevel : IUnknown
HRESULT GetInputMethod(IAvnTextInputMethod **ppv); HRESULT GetInputMethod(IAvnTextInputMethod **ppv);
HRESULT SetTransparencyMode(AvnWindowTransparencyMode mode); HRESULT SetTransparencyMode(AvnWindowTransparencyMode mode);
HRESULT GetCurrentDisplayId(uint* ret);
} }
[uuid(e5aca675-02b7-4129-aa79-d6e417210bda), cpp-virtual-inherits] [uuid(e5aca675-02b7-4129-aa79-d6e417210bda), cpp-virtual-inherits]
@ -917,11 +929,17 @@ interface IAvnFilePickerFileTypes : IUnknown
HRESULT GetAppleUniformTypeIdentifiers(int index, IAvnStringArray**ppv); HRESULT GetAppleUniformTypeIdentifiers(int index, IAvnStringArray**ppv);
} }
[uuid(424b1bd4-a111-4987-bfd0-9d642154b1b3)]
interface IAvnScreenEvents : IUnknown
{
HRESULT OnChanged();
}
[uuid(9a52bc7a-d8c7-4230-8d34-704a0b70a933)] [uuid(9a52bc7a-d8c7-4230-8d34-704a0b70a933)]
interface IAvnScreens : IUnknown interface IAvnScreens : IUnknown
{ {
HRESULT GetScreenCount(int* ret); HRESULT GetScreenIds(uint* ptrFirstResult, int* ret);
HRESULT GetScreen(int index, AvnScreen* ret); HRESULT GetScreen(uint screenId, void** localizedName, AvnScreen* ret);
} }
[uuid(792b1bd4-76cc-46ea-bfd0-9d642154b1b3)] [uuid(792b1bd4-76cc-46ea-bfd0-9d642154b1b3)]

18
src/Avalonia.X11/Screens/X11Screens.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia.Platform; using Avalonia.Platform;
using static Avalonia.X11.XLib; using static Avalonia.X11.XLib;
@ -12,7 +13,6 @@ namespace Avalonia.X11.Screens
{ {
private IX11RawScreenInfoProvider _impl; private IX11RawScreenInfoProvider _impl;
private IScalingProvider _scaling; private IScalingProvider _scaling;
internal event Action Changed;
public X11Screens(AvaloniaX11Platform platform) public X11Screens(AvaloniaX11Platform platform)
{ {
@ -67,7 +67,17 @@ namespace Avalonia.X11.Screens
XFree(prop); XFree(prop);
return screens; return screens;
} }
public Screen ScreenFromTopLevel(ITopLevelImpl topLevel)
{
if (topLevel is IWindowImpl window)
{
return ScreenFromWindow(window);
}
return null;
}
public Screen ScreenFromPoint(PixelPoint point) public Screen ScreenFromPoint(PixelPoint point)
{ {
@ -79,6 +89,8 @@ namespace Avalonia.X11.Screens
return ScreenHelper.ScreenFromRect(rect, AllScreens); return ScreenHelper.ScreenFromRect(rect, AllScreens);
} }
public Task<bool> RequestScreenDetails() => Task.FromResult(true);
public Screen ScreenFromWindow(IWindowBaseImpl window) public Screen ScreenFromWindow(IWindowBaseImpl window)
{ {
return ScreenHelper.ScreenFromWindow(window, AllScreens); return ScreenHelper.ScreenFromWindow(window, AllScreens);
@ -98,5 +110,7 @@ namespace Avalonia.X11.Screens
.ToArray(); .ToArray();
} }
} }
public Action Changed { get; set; }
} }
} }

14
src/Avalonia.X11/X11Window.cs

@ -129,9 +129,9 @@ namespace Avalonia.X11
int defaultWidth = 0, defaultHeight = 0; int defaultWidth = 0, defaultHeight = 0;
if (!_popup && Screen != null) if (!_popup && _platform.Screens != null)
{ {
var monitor = Screen.AllScreens.OrderBy(x => x.Scaling) var monitor = _platform.Screens.AllScreens.OrderBy(x => x.Scaling)
.FirstOrDefault(m => m.Bounds.Contains(_position ?? default)); .FirstOrDefault(m => m.Bounds.Contains(_position ?? default));
if (monitor != null) if (monitor != null)
@ -932,7 +932,14 @@ namespace Avalonia.X11
} }
if (featureType == typeof(IX11OptionsToplevelImplFeature)) if (featureType == typeof(IX11OptionsToplevelImplFeature))
{
return this; return this;
}
if (featureType == typeof(IScreenImpl))
{
return _platform.Screens;
}
return null; return null;
} }
@ -1167,9 +1174,6 @@ namespace Avalonia.X11
} }
} }
public IScreenImpl Screen => _platform.Screens;
public Size MaxAutoSizeHint => _platform.X11Screens.AllScreens.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(); .OrderByDescending(x => x.Width + x.Height).FirstOrDefault();

23
src/Browser/Avalonia.Browser/WinStubs.cs

@ -22,27 +22,4 @@ namespace Avalonia.Browser
public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) => new IconStub(); public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) => new IconStub();
} }
internal class ScreenStub : IScreenImpl
{
public int ScreenCount => 1;
public IReadOnlyList<Screen> AllScreens { get; } =
new[] { new Screen(96, new PixelRect(0, 0, 4000, 4000), new PixelRect(0, 0, 4000, 4000), true) };
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);
}
}
} }

31
src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs

@ -344,32 +344,23 @@ namespace Avalonia.Headless
} }
} }
internal class HeadlessScreensStub : IScreenImpl internal class HeadlessScreensStub : ScreensBase<int, PlatformScreen>
{ {
public int ScreenCount { get; } = 1; protected override IReadOnlyList<int> GetAllScreenKeys() => new[] { 1 };
public IReadOnlyList<Screen> AllScreens { get; } = new[] protected override PlatformScreen CreateScreenFromKey(int key) => new PlatformScreenStub(key);
{
new Screen(1, new PixelRect(0, 0, 1920, 1280),
new PixelRect(0, 0, 1920, 1280), true),
};
public Screen? ScreenFromPoint(PixelPoint point)
{
return ScreenHelper.ScreenFromPoint(point, AllScreens);
}
public Screen? ScreenFromRect(PixelRect rect) private class PlatformScreenStub : PlatformScreen
{ {
return ScreenHelper.ScreenFromRect(rect, AllScreens); public PlatformScreenStub(int key) : base(new PlatformHandle((nint)key, nameof(HeadlessScreensStub)))
} {
Scaling = 1;
public Screen? ScreenFromWindow(IWindowBaseImpl window) Bounds = WorkingArea = new PixelRect(0, 0, 1920, 1280);
{ IsPrimary = true;
return ScreenHelper.ScreenFromWindow(window, AllScreens); }
} }
} }
internal static class TextTestHelper internal static class TextTestHelper
{ {
public static int GetStartCharIndex(ReadOnlyMemory<char> text) public static int GetStartCharIndex(ReadOnlyMemory<char> text)

9
src/Windows/Avalonia.Win32/Avalonia.Win32.csproj

@ -5,8 +5,15 @@
<!-- We still keep BinaryFormatter for WinForms compatibility. --> <!-- We still keep BinaryFormatter for WinForms compatibility. -->
<EnableUnsafeBinaryFormatterSerialization>true</EnableUnsafeBinaryFormatterSerialization> <EnableUnsafeBinaryFormatterSerialization>true</EnableUnsafeBinaryFormatterSerialization>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))">
<PackageReference Include="System.Numerics.Vectors" Version="4.5.0" /> <PackageReference Include="System.Numerics.Vectors" Version="4.5.0" />
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" /> <ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />

37
src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs

@ -31,43 +31,6 @@ namespace Avalonia.Win32.DirectX
public override string ToString() => ((IntPtr)Value).ToString(); public override string ToString() => ((IntPtr)Value).ToString();
} }
internal unsafe struct MONITORINFOEXW
{
internal MONITORINFO Base;
internal fixed ushort szDevice[32];
}
internal unsafe struct DEVMODEW
{
public fixed ushort dmDeviceName[32];
public short dmSpecVersion;
public short dmDriverVersion;
public short dmSize;
public short dmDriverExtra;
public int dmFields;
public short dmOrientation;
public short dmPaperSize;
public short dmPaperLength;
public short dmPaperWidth;
public short dmScale;
public short dmCopies;
public short dmDefaultSource;
public short dmPrintQuality;
public short dmColor;
public short dmDuplex;
public short dmYResolution;
public short dmTTOption;
public short dmCollate;
public fixed ushort dmFormName[32];
public short dmUnusedPadding;
public short dmBitsPerPel;
public int dmPelsWidth;
public int dmPelsHeight;
public int dmDisplayFlags;
public int dmDisplayFrequency;
}
internal unsafe struct DXGI_ADAPTER_DESC internal unsafe struct DXGI_ADAPTER_DESC
{ {
public fixed ushort Description[128]; public fixed ushort Description[128];

6
src/Windows/Avalonia.Win32/DirectX/DirectXUnmanagedMethods.cs

@ -17,12 +17,6 @@ namespace Avalonia.Win32.DirectX
[DllImport("dxgi", ExactSpelling = true, PreserveSig = false)] [DllImport("dxgi", ExactSpelling = true, PreserveSig = false)]
internal static extern void CreateDXGIFactory1(ref Guid riid, out void* ppFactory); internal static extern void CreateDXGIFactory1(ref Guid riid, out void* ppFactory);
[DllImport("user32", ExactSpelling = true)]
internal static extern bool GetMonitorInfoW(HANDLE hMonitor, IntPtr lpmi);
[DllImport("user32", ExactSpelling = true)]
internal static extern bool EnumDisplaySettingsW(ushort* lpszDeviceName, uint iModeNum, DEVMODEW* lpDevMode);
[DllImport("d3d11", ExactSpelling = true, PreserveSig = false)] [DllImport("d3d11", ExactSpelling = true, PreserveSig = false)]
public static extern void D3D11CreateDevice( public static extern void D3D11CreateDevice(
IntPtr adapter, D3D_DRIVER_TYPE DriverType, IntPtr adapter, D3D_DRIVER_TYPE DriverType,

17
src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs

@ -122,19 +122,10 @@ namespace Avalonia.Win32.DirectX
using var output = MicroComRuntime.CreateProxyFor<IDXGIOutput>(outputPointer, true); using var output = MicroComRuntime.CreateProxyFor<IDXGIOutput>(outputPointer, true);
DXGI_OUTPUT_DESC outputDesc = output.Desc; DXGI_OUTPUT_DESC outputDesc = output.Desc;
var screen = Win32Platform.Instance.Screen.ScreenFromHMonitor((IntPtr)outputDesc.Monitor.Value);
var frequency = screen?.Frequency ?? highestRefreshRate;
// this handle need not closing, by the way. if (highestRefreshRate < frequency)
HANDLE monitorH = outputDesc.Monitor;
MONITORINFOEXW monInfo = default;
// by setting cbSize we tell Windows to fully populate the extended info
monInfo.Base.cbSize = sizeof(MONITORINFOEXW);
GetMonitorInfoW(monitorH, (IntPtr)(&monInfo));
DEVMODEW devMode = default;
EnumDisplaySettingsW(outputDesc.DeviceName, ENUM_CURRENT_SETTINGS, &devMode);
if (highestRefreshRate < devMode.dmDisplayFrequency)
{ {
// ooh I like this output! // ooh I like this output!
if (_output is not null) if (_output is not null)
@ -143,7 +134,7 @@ namespace Avalonia.Win32.DirectX
_output = null; _output = null;
} }
_output = MicroComRuntime.CloneReference(output); _output = MicroComRuntime.CloneReference(output);
highestRefreshRate = devMode.dmDisplayFrequency; highestRefreshRate = frequency;
} }
// and then increment index to move onto the next monitor // and then increment index to move onto the next monitor
outputIndex++; outputIndex++;

32
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -6,6 +6,8 @@ using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes; using System.Runtime.InteropServices.ComTypes;
using System.Text; using System.Text;
using Windows.Win32;
using Windows.Win32.Graphics.Gdi;
using MicroCom.Runtime; using MicroCom.Runtime;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
@ -1174,12 +1176,6 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll", SetLastError = true)] [DllImport("user32.dll", SetLastError = true)]
public static extern bool GetPointerTouchInfoHistory(uint pointerId, ref int entriesCount, [MarshalAs(UnmanagedType.LPArray), In, Out] POINTER_TOUCH_INFO[] touchInfos); public static extern bool GetPointerTouchInfoHistory(uint pointerId, ref int entriesCount, [MarshalAs(UnmanagedType.LPArray), In, Out] POINTER_TOUCH_INFO[] touchInfos);
[DllImport("user32.dll")]
public static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip,
MonitorEnumDelegate lpfnEnum, IntPtr dwData);
public delegate bool MonitorEnumDelegate(IntPtr hMonitor, IntPtr hdcMonitor, ref Rect lprcMonitor, IntPtr dwData);
[DllImport("user32.dll", SetLastError = true)] [DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr GetDC(IntPtr hWnd); public static extern IntPtr GetDC(IntPtr hWnd);
@ -1637,10 +1633,6 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll")] [DllImport("user32.dll")]
public static extern IntPtr MonitorFromWindow(IntPtr hwnd, MONITOR dwFlags); public static extern IntPtr MonitorFromWindow(IntPtr hwnd, MONITOR dwFlags);
[DllImport("user32", EntryPoint = "GetMonitorInfoW", ExactSpelling = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetMonitorInfo([In] IntPtr hMonitor, ref MONITORINFO lpmi);
[DllImport("user32")] [DllImport("user32")]
public static extern bool GetTouchInputInfo( public static extern bool GetTouchInputInfo(
IntPtr hTouchInput, IntPtr hTouchInput,
@ -2128,23 +2120,17 @@ namespace Avalonia.Win32.Interop
} }
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
internal struct MONITORINFO internal struct MONITORINFOEX
{ {
public int cbSize; internal MONITORINFO Base;
public RECT rcMonitor;
public RECT rcWork;
public int dwFlags;
public static MONITORINFO Create() internal __char_32 szDevice;
{
return new MONITORINFO() { cbSize = Marshal.SizeOf<MONITORINFO>() };
}
public enum MonitorOptions : uint public static MONITORINFOEX Create()
{ {
MONITOR_DEFAULTTONULL = 0x00000000, var info = new MONITORINFO();
MONITOR_DEFAULTTOPRIMARY = 0x00000001, info.cbSize = (uint)Marshal.SizeOf<MONITORINFOEX>();
MONITOR_DEFAULTTONEAREST = 0x00000002 return new MONITORINFOEX() { Base = info };
} }
} }

10
src/Windows/Avalonia.Win32/NativeMethods.txt

@ -0,0 +1,10 @@
EnumDisplayMonitors
EnumDisplayMonitors
GetMonitorInfo
MONITORINFOEX
EnumDisplaySettings
GetDisplayConfigBufferSizes
QueryDisplayConfig
DisplayConfigGetDeviceInfo
DISPLAYCONFIG_SOURCE_DEVICE_NAME
DISPLAYCONFIG_TARGET_DEVICE_NAME

10
src/Windows/Avalonia.Win32/PopupImpl.cs

@ -51,15 +51,11 @@ namespace Avalonia.Win32
{ {
if (_maxAutoSize is null) if (_maxAutoSize is null)
{ {
var monitor = UnmanagedMethods.MonitorFromWindow( var screen = base.Screen.ScreenFromHwnd(Hwnd, UnmanagedMethods.MONITOR.MONITOR_DEFAULTTONEAREST);
Hwnd,
UnmanagedMethods.MONITOR.MONITOR_DEFAULTTONEAREST);
if (monitor != IntPtr.Zero) if (screen is not null)
{ {
var info = UnmanagedMethods.MONITORINFO.Create(); _maxAutoSize = screen.WorkingArea.ToRect(RenderScaling).Size;
UnmanagedMethods.GetMonitorInfo(monitor, ref info);
_maxAutoSize = info.rcWork.ToPixelRect().ToRect(RenderScaling).Size;
} }
} }

166
src/Windows/Avalonia.Win32/ScreenImpl.cs

@ -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);
} }
} }

4
src/Windows/Avalonia.Win32/Win32Platform.cs

@ -56,7 +56,8 @@ namespace Avalonia.Win32
} }
internal static Win32Platform Instance => s_instance; internal static Win32Platform Instance => s_instance;
internal static IPlatformSettings PlatformSettings => AvaloniaLocator.Current.GetRequiredService<IPlatformSettings>(); internal IPlatformSettings PlatformSettings => AvaloniaLocator.Current.GetRequiredService<IPlatformSettings>();
internal ScreenImpl Screen => (ScreenImpl)AvaloniaLocator.Current.GetRequiredService<IScreenImpl>();
internal IntPtr Handle => _hwnd; internal IntPtr Handle => _hwnd;
@ -91,6 +92,7 @@ namespace Avalonia.Win32
.Bind<ICursorFactory>().ToConstant(CursorFactory.Instance) .Bind<ICursorFactory>().ToConstant(CursorFactory.Instance)
.Bind<IKeyboardDevice>().ToConstant(WindowsKeyboardDevice.Instance) .Bind<IKeyboardDevice>().ToConstant(WindowsKeyboardDevice.Instance)
.Bind<IPlatformSettings>().ToSingleton<Win32PlatformSettings>() .Bind<IPlatformSettings>().ToSingleton<Win32PlatformSettings>()
.Bind<IScreenImpl>().ToSingleton<ScreenImpl>()
.Bind<IDispatcherImpl>().ToConstant(s_instance._dispatcher) .Bind<IDispatcherImpl>().ToConstant(s_instance._dispatcher)
.Bind<IRenderTimer>().ToConstant(renderTimer) .Bind<IRenderTimer>().ToConstant(renderTimer)
.Bind<IWindowingPlatform>().ToConstant(s_instance) .Bind<IWindowingPlatform>().ToConstant(s_instance)

6
src/Windows/Avalonia.Win32/Win32TypeExtensions.cs

@ -4,6 +4,12 @@ namespace Avalonia.Win32
{ {
internal static class Win32TypeExtensions internal static class Win32TypeExtensions
{ {
public static PixelRect ToPixelRect(this Windows.Win32.Foundation.RECT rect)
{
return new PixelRect(rect.left, rect.top, rect.right - rect.left,
rect.bottom - rect.top);
}
public static PixelRect ToPixelRect(this RECT rect) public static PixelRect ToPixelRect(this RECT rect)
{ {
return new PixelRect(rect.left, rect.top, rect.right - rect.left, return new PixelRect(rect.left, rect.top, rect.right - rect.left,

124
src/Windows/Avalonia.Win32/WinScreen.cs

@ -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;
} }
} }

2
src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs

@ -717,7 +717,7 @@ namespace Avalonia.Win32
case WindowsMessage.WM_DISPLAYCHANGE: case WindowsMessage.WM_DISPLAYCHANGE:
{ {
(Screen as ScreenImpl)?.InvalidateScreensCache(); Screen?.OnChanged();
return IntPtr.Zero; return IntPtr.Zero;
} }

68
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -173,7 +173,7 @@ namespace Avalonia.Win32
} }
} }
Screen = new ScreenImpl(); Screen = Win32Platform.Instance.Screen;
_storageProvider = new Win32StorageProvider(this); _storageProvider = new Win32StorageProvider(this);
_inputPane = WindowsInputPane.TryCreate(this); _inputPane = WindowsInputPane.TryCreate(this);
_nativeControlHost = new Win32NativeControlHost(this, !UseRedirectionBitmap); _nativeControlHost = new Win32NativeControlHost(this, !UseRedirectionBitmap);
@ -274,7 +274,7 @@ namespace Avalonia.Win32
} }
} }
public IScreenImpl Screen { get; } public ScreenImpl Screen { get; }
public IPlatformHandle Handle { get; private set; } public IPlatformHandle Handle { get; private set; }
@ -337,6 +337,11 @@ namespace Avalonia.Win32
public object? TryGetFeature(Type featureType) public object? TryGetFeature(Type featureType)
{ {
if (featureType == typeof(IScreenImpl))
{
return Screen;
}
if (featureType == typeof(ITextInputMethodImpl)) if (featureType == typeof(ITextInputMethodImpl))
{ {
return Imm32InputMethod.Current; return Imm32InputMethod.Current;
@ -1041,15 +1046,14 @@ namespace Avalonia.Win32
// On expand, if we're given a window_rect, grow to it, otherwise do // On expand, if we're given a window_rect, grow to it, otherwise do
// not resize. // not resize.
MONITORINFO monitor_info = MONITORINFO.Create(); var screen = Screen.ScreenFromHwnd(_hwnd, MONITOR.MONITOR_DEFAULTTONEAREST);
GetMonitorInfo(MonitorFromWindow(_hwnd, MONITOR.MONITOR_DEFAULTTONEAREST), ref monitor_info); if (screen?.Bounds is { } window_rect)
{
var window_rect = monitor_info.rcMonitor.ToPixelRect(); _isFullScreenActive = true;
SetWindowPos(_hwnd, IntPtr.Zero, window_rect.X, window_rect.Y,
_isFullScreenActive = true; window_rect.Width, window_rect.Height,
SetWindowPos(_hwnd, IntPtr.Zero, window_rect.X, window_rect.Y, SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED);
window_rect.Width, window_rect.Height, }
SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED);
} }
else else
{ {
@ -1283,34 +1287,28 @@ namespace Avalonia.Win32
private void MaximizeWithoutCoveringTaskbar() private void MaximizeWithoutCoveringTaskbar()
{ {
IntPtr monitor = MonitorFromWindow(_hwnd, MONITOR.MONITOR_DEFAULTTONEAREST); var screen = Screen.ScreenFromHwnd(Hwnd, MONITOR.MONITOR_DEFAULTTONEAREST);
if (screen?.WorkingArea is { } workingArea)
if (monitor != IntPtr.Zero)
{ {
var monitorInfo = MONITORINFO.Create(); var x = workingArea.X;
var y = workingArea.Y;
var cx = workingArea.Width;
var cy = workingArea.Height;
var style = (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE);
if (GetMonitorInfo(monitor, ref monitorInfo)) if (!style.HasFlag(WindowStyles.WS_THICKFRAME))
{ {
var x = monitorInfo.rcWork.left; // When calling SetWindowPos on a maximized window it automatically adjusts
var y = monitorInfo.rcWork.top; // for "hidden" borders which are placed offscreen, EVEN IF THE WINDOW HAS
var cx = Math.Abs(monitorInfo.rcWork.right - x); // NO BORDERS, meaning that the window is placed wrong when we have CanResize
var cy = Math.Abs(monitorInfo.rcWork.bottom - y); // == false. Account for this here.
var style = (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE); var borderThickness = BorderThickness;
x -= (int)borderThickness.Left;
if (!style.HasFlag(WindowStyles.WS_THICKFRAME)) cx += (int)borderThickness.Left + (int)borderThickness.Right;
{ cy += (int)borderThickness.Bottom;
// When calling SetWindowPos on a maximized window it automatically adjusts
// for "hidden" borders which are placed offscreen, EVEN IF THE WINDOW HAS
// NO BORDERS, meaning that the window is placed wrong when we have CanResize
// == false. Account for this here.
var borderThickness = BorderThickness;
x -= (int)borderThickness.Left;
cx += (int)borderThickness.Left + (int)borderThickness.Right;
cy += (int)borderThickness.Bottom;
}
SetWindowPos(_hwnd, WindowPosZOrder.HWND_NOTOPMOST, x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW | SetWindowPosFlags.SWP_FRAMECHANGED);
} }
SetWindowPos(_hwnd, WindowPosZOrder.HWND_NOTOPMOST, x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW | SetWindowPosFlags.SWP_FRAMECHANGED);
} }
} }

2
tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs

@ -669,7 +669,7 @@ namespace Avalonia.Controls.UnitTests
popupImpl.SetupGet(x => x.RenderScaling).Returns(1); popupImpl.SetupGet(x => x.RenderScaling).Returns(1);
windowImpl.Setup(x => x.CreatePopup()).Returns(popupImpl.Object); windowImpl.Setup(x => x.CreatePopup()).Returns(popupImpl.Object);
windowImpl.Setup(x => x.Screen).Returns(screenImpl.Object); windowImpl.Setup(x => x.TryGetFeature(It.Is<Type>(t => t == typeof(IScreenImpl)))).Returns(screenImpl.Object);
var services = TestServices.StyledWindow.With( var services = TestServices.StyledWindow.With(
focusManager: new FocusManager(), focusManager: new FocusManager(),

2
tests/Avalonia.Controls.UnitTests/MenuItemTests.cs

@ -816,7 +816,7 @@ namespace Avalonia.Controls.UnitTests
popupImpl.SetupGet(x => x.RenderScaling).Returns(1); popupImpl.SetupGet(x => x.RenderScaling).Returns(1);
windowImpl.Setup(x => x.CreatePopup()).Returns(popupImpl.Object); windowImpl.Setup(x => x.CreatePopup()).Returns(popupImpl.Object);
windowImpl.Setup(x => x.Screen).Returns(screenImpl.Object); windowImpl.Setup(x => x.TryGetFeature(It.Is<Type>(t => t == typeof(IScreenImpl)))).Returns(screenImpl.Object);
var services = TestServices.StyledWindow.With( var services = TestServices.StyledWindow.With(
inputManager: new InputManager(), inputManager: new InputManager(),

173
tests/Avalonia.Controls.UnitTests/Platform/ScreensTests.cs

@ -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; }
}
}

6
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@ -527,7 +527,7 @@ namespace Avalonia.Controls.UnitTests
windowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480)); windowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480));
windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.DesktopScaling).Returns(1);
windowImpl.Setup(x => x.RenderScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1);
windowImpl.Setup(x => x.Screen).Returns(screens.Object); windowImpl.Setup(x => x.TryGetFeature(It.Is<Type>(t => t == typeof(IScreenImpl)))).Returns(screens.Object);
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
@ -563,7 +563,7 @@ namespace Avalonia.Controls.UnitTests
windowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480)); windowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480));
windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.DesktopScaling).Returns(1);
windowImpl.Setup(x => x.RenderScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1);
windowImpl.Setup(x => x.Screen).Returns(screens.Object); windowImpl.Setup(x => x.TryGetFeature(It.Is<Type>(t => t == typeof(IScreenImpl)))).Returns(screens.Object);
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
@ -592,7 +592,7 @@ namespace Avalonia.Controls.UnitTests
var windowImpl = MockWindowingPlatform.CreateWindowMock(400, 300); var windowImpl = MockWindowingPlatform.CreateWindowMock(400, 300);
windowImpl.Setup(x => x.DesktopScaling).Returns(1.75); windowImpl.Setup(x => x.DesktopScaling).Returns(1.75);
windowImpl.Setup(x => x.RenderScaling).Returns(1.75); windowImpl.Setup(x => x.RenderScaling).Returns(1.75);
windowImpl.Setup(x => x.Screen).Returns(screens.Object); windowImpl.Setup(x => x.TryGetFeature(It.Is<Type>(t => t == typeof(IScreenImpl)))).Returns(screens.Object);
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {

64
tests/Avalonia.IntegrationTests.Appium/ScreenTests.cs

@ -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);
}
}

3
tests/Avalonia.UnitTests/MockWindowingPlatform.cs

@ -34,9 +34,8 @@ namespace Avalonia.UnitTests
windowImpl.Setup(x => x.MaxAutoSizeHint).Returns(s_screenSize); windowImpl.Setup(x => x.MaxAutoSizeHint).Returns(s_screenSize);
windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.DesktopScaling).Returns(1);
windowImpl.Setup(x => x.RenderScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1);
windowImpl.Setup(x => x.Screen).Returns(CreateScreenMock().Object);
windowImpl.Setup(r => r.TryGetFeature(It.IsAny<Type>())).Returns(null); windowImpl.Setup(r => r.TryGetFeature(It.IsAny<Type>())).Returns(null);
windowImpl.Setup(x => x.TryGetFeature(It.Is<Type>(t => t == typeof(IScreenImpl)))).Returns(CreateScreenMock().Object);
windowImpl.Setup(x => x.CreatePopup()).Returns(() => windowImpl.Setup(x => x.CreatePopup()).Returns(() =>
{ {

Loading…
Cancel
Save