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
Max Katz 1 year ago
parent
commit
05ac6d2f1d
54 changed files with 1279 additions and 462 deletions
  1. 12 0
      api/Avalonia.nupkg.xml
  2. 75 18
      native/Avalonia.Native/src/OSX/Screens.mm
  3. 2 1
      native/Avalonia.Native/src/OSX/TopLevelImpl.h
  4. 9 0
      native/Avalonia.Native/src/OSX/TopLevelImpl.mm
  5. 8 1
      native/Avalonia.Native/src/OSX/common.h
  6. 2 2
      native/Avalonia.Native/src/OSX/main.mm
  7. 1 0
      native/Avalonia.Native/src/OSX/menu.mm
  8. 3 0
      samples/ControlCatalog/MainView.xaml
  9. 0 10
      samples/ControlCatalog/MainView.xaml.cs
  10. 62 16
      samples/ControlCatalog/Pages/ScreenPage.cs
  11. 13 0
      samples/IntegrationTestApp/MainWindow.axaml
  12. 17 0
      samples/IntegrationTestApp/MainWindow.axaml.cs
  13. 40 1
      src/Avalonia.Base/Platform/PlatformHandle.cs
  14. 0 1
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
  15. 159 11
      src/Avalonia.Controls/Platform/IScreenImpl.cs
  16. 0 5
      src/Avalonia.Controls/Platform/ITopLevelImpl.cs
  17. 120 13
      src/Avalonia.Controls/Platform/Screen.cs
  18. 2 2
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs
  19. 96 8
      src/Avalonia.Controls/Screens.cs
  20. 8 1
      src/Avalonia.Controls/TopLevel.cs
  21. 3 2
      src/Avalonia.Controls/WindowBase.cs
  22. 6 2
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  23. 11 19
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  24. 1 0
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  25. 1 1
      src/Avalonia.Native/EmbeddableTopLevelImpl.cs
  26. 3 3
      src/Avalonia.Native/PopupImpl.cs
  27. 52 37
      src/Avalonia.Native/ScreenImpl.cs
  28. 6 6
      src/Avalonia.Native/TopLevelImpl.cs
  29. 3 3
      src/Avalonia.Native/WindowImpl.cs
  30. 7 5
      src/Avalonia.Native/WindowImplBase.cs
  31. 21 3
      src/Avalonia.Native/avn.idl
  32. 16 2
      src/Avalonia.X11/Screens/X11Screens.cs
  33. 9 5
      src/Avalonia.X11/X11Window.cs
  34. 0 23
      src/Browser/Avalonia.Browser/WinStubs.cs
  35. 11 20
      src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs
  36. 8 1
      src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
  37. 0 37
      src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs
  38. 0 6
      src/Windows/Avalonia.Win32/DirectX/DirectXUnmanagedMethods.cs
  39. 4 13
      src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs
  40. 9 23
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  41. 10 0
      src/Windows/Avalonia.Win32/NativeMethods.txt
  42. 3 7
      src/Windows/Avalonia.Win32/PopupImpl.cs
  43. 70 96
      src/Windows/Avalonia.Win32/ScreenImpl.cs
  44. 3 1
      src/Windows/Avalonia.Win32/Win32Platform.cs
  45. 6 0
      src/Windows/Avalonia.Win32/Win32TypeExtensions.cs
  46. 110 14
      src/Windows/Avalonia.Win32/WinScreen.cs
  47. 1 1
      src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
  48. 33 35
      src/Windows/Avalonia.Win32/WindowImpl.cs
  49. 1 1
      tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
  50. 1 1
      tests/Avalonia.Controls.UnitTests/MenuItemTests.cs
  51. 173 0
      tests/Avalonia.Controls.UnitTests/Platform/ScreensTests.cs
  52. 3 3
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  53. 64 0
      tests/Avalonia.IntegrationTests.Appium/ScreenTests.cs
  54. 1 2
      tests/Avalonia.UnitTests/MockWindowingPlatform.cs

+ 12 - 0
api/Avalonia.nupkg.xml

@@ -1093,6 +1093,12 @@
     <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
     <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
   </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>
     <DiagnosticId>CP0006</DiagnosticId>
     <Target>M:Avalonia.Platform.IAssetLoader.InvalidateAssemblyCache</Target>
@@ -1141,4 +1147,10 @@
     <Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
     <Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
   </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>

+ 75 - 18
native/Avalonia.Native/src/OSX/Screens.mm

@@ -1,36 +1,62 @@
 #include "common.h"
+#include "AvnString.h"
 
 class Screens : public ComSingleObject<IAvnScreens, &IID_IAvnScreens>
 {
-    public:
-    FORWARD_IUNKNOWN()
-    
+private:
+    ComPtr<IAvnScreenEvents> _events;
 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;
         
         @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;
         }
     }
-    
-    virtual HRESULT GetScreen (int index, AvnScreen* ret) override
-    {
+
+    virtual HRESULT GetScreen (
+       CGDirectDisplayID displayId,
+       void** localizedName,
+       AvnScreen* ret
+    ) override {
         START_COM_CALL;
         
         @autoreleasepool
         {
-            if(index < 0 || index >= [NSScreen screens].count)
-            {
-                return E_INVALIDARG;
+            NSScreen* screen;
+            for (NSScreen *s in NSScreen.screens) {
+                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.Width = [screen frame].size.width;
             ret->Bounds.X = [screen frame].origin.x;
@@ -43,14 +69,45 @@ public:
             
             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;
         }
     }
+
+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);
 }

+ 2 - 1
native/Avalonia.Native/src/OSX/TopLevelImpl.h

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

+ 9 - 0
native/Avalonia.Native/src/OSX/TopLevelImpl.mm

@@ -258,6 +258,15 @@ HRESULT TopLevelImpl::SetTransparencyMode(AvnWindowTransparencyMode mode) {
     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() {
     
 }

+ 8 - 1
native/Avalonia.Native/src/OSX/common.h

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

+ 2 - 2
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;
         
         @autoreleasepool
         {
-            *ppv = ::CreateScreens ();
+            *ppv = ::CreateScreens (cb);
             return S_OK;
         }
     }

+ 1 - 0
native/Avalonia.Native/src/OSX/menu.mm

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

+ 3 - 0
samples/ControlCatalog/MainView.xaml

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

+ 0 - 10
samples/ControlCatalog/MainView.xaml.cs

@@ -25,16 +25,6 @@ namespace ControlCatalog
             
             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");
             themes.SelectedItem = App.CurrentTheme;
             themes.SelectionChanged += (sender, e) =>

+ 62 - 16
samples/ControlCatalog/Pages/ScreenPage.cs

@@ -20,33 +20,62 @@ namespace ControlCatalog.Pages
         private IPen _activePen = new Pen(Brushes.Black);
         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 void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
         {
             base.OnAttachedToVisualTree(e);
-            if(VisualRoot is Window w)
+
+            var topLevel = TopLevel.GetTopLevel(this);
+            if (topLevel is Window w)
             {
                 w.PositionChanged += (_, _) => InvalidateVisual();
             }
+
+            if (topLevel?.Screens is { } screens)
+            {
+                screens.Changed += (_, _) =>
+                {
+                    Console.WriteLine("Screens Changed");
+                    InvalidateVisual();
+                };
+            }
         }
 
         public override void Render(DrawingContext 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;
             }
-            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;
 
-            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)
                 {
@@ -63,16 +92,16 @@ namespace ControlCatalog.Pages
                 bool primary = screen.IsPrimary;
                 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);
-                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);
 
                 context.DrawRectangle(primary ? _primaryBrush : _defaultBrush, active ? _activePen : _defaultPen, boundsRect);
                 context.DrawRectangle(primary ? _primaryBrush : _defaultBrush, active ? _activePen : _defaultPen, workingAreaRect);
-
+ 
                 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);
                 maxBottom = Math.Max(maxBottom, boundsRect.Bottom);
@@ -80,14 +109,22 @@ namespace ControlCatalog.Pages
 
             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);
                 context.DrawText(formattedText, new Point(0, currentHeight));
                 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}");
                 context.DrawText(formattedText, new Point(15, currentHeight));
                 currentHeight += 20;
@@ -101,17 +138,26 @@ namespace ControlCatalog.Pages
                 currentHeight += 20;
 
                 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));
                 currentHeight += 20;
 
                 formattedText = CreateFormattedText( $"Current: {screen.Equals(activeScreen)}");
                 context.DrawText(formattedText, new Point(15, currentHeight));
                 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)

+ 13 - 0
samples/IntegrationTestApp/MainWindow.axaml

@@ -230,6 +230,19 @@
       <TabItem Header="ScrollBar">
         <ScrollBar Name="MyScrollBar" Orientation="Horizontal" AllowAutoHide="False" Width="200" Height="30" Value="20"/>
       </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>
   </DockPanel>
 </Window>

+ 17 - 0
samples/IntegrationTestApp/MainWindow.axaml.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Globalization;
 using System.Linq;
 using Avalonia;
 using Avalonia.Automation;
@@ -303,6 +304,8 @@ namespace IntegrationTestApp
                 OnShowNewWindowDecorations();
             if (source?.Name == nameof(ToggleTrayIconVisible))
                 OnToggleTrayIconVisible();
+            if (source?.Name == nameof(ScreenRefresh))
+                OnScreenRefresh();
         }
 
         private void OnApplyWindowDecorations(Window window)
@@ -376,5 +379,19 @@ namespace IntegrationTestApp
 
             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();
+        }
     }
 }

+ 40 - 1
src/Avalonia.Base/Platform/PlatformHandle.cs

@@ -5,7 +5,7 @@ namespace Avalonia.Platform
     /// <summary>
     /// Represents a platform-specific handle.
     /// </summary>
-    public class PlatformHandle : IPlatformHandle
+    public class PlatformHandle : IPlatformHandle, IEquatable<PlatformHandle>
     {
         /// <summary>
         /// 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.
         /// </summary>
         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);
+        }
     }
 }

+ 0 - 1
src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs

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

+ 159 - 11
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.Threading;
+#pragma warning disable CS1591 // Private API doesn't require XML documentation. 
 
 namespace Avalonia.Platform
 {
     [Unstable]
     public interface IScreenImpl
     {
-        /// <summary>
-        /// Gets the total number of screens available on the device.
-        /// </summary>
         int ScreenCount { get; }
-
-        /// <summary>
-        /// Gets the list of all screens available on the device.
-        /// </summary>
         IReadOnlyList<Screen> AllScreens { get; }
-
+        Action? Changed { get; set; }
         Screen? ScreenFromWindow(IWindowBaseImpl window);
-
+        Screen? ScreenFromTopLevel(ITopLevelImpl topLevel);
         Screen? ScreenFromPoint(PixelPoint point);
-
         Screen? ScreenFromRect(PixelRect rect);
+        Task<bool> RequestScreenDetails();
+    }
+
+    [PrivateApi]
+    public class PlatformScreen(IPlatformHandle platformHandle) : Screen
+    {
+        public override IPlatformHandle? TryGetPlatformHandle() => platformHandle;
+
+        public override int GetHashCode() => platformHandle.GetHashCode();
+        public override bool Equals(object? obj)
+        {
+            return obj is PlatformScreen other && platformHandle.Equals(other.TryGetPlatformHandle()!);
+        }
+    }
+
+    [PrivateApi]
+    public abstract class ScreensBase<TKey, TScreen>(IEqualityComparer<TKey>? screenKeyComparer) : IScreenImpl
+        where TKey : notnull
+        where TScreen : PlatformScreen
+    {
+        private readonly Dictionary<TKey, TScreen> _allScreensByKey = screenKeyComparer is not null ?
+            new Dictionary<TKey, TScreen>(screenKeyComparer) :
+            new Dictionary<TKey, TScreen>();
+        private TScreen[]? _allScreens;
+        private int? _screenCount;
+        private bool? _screenDetailsRequestGranted;
+        private DispatcherOperation? _onChangeOperation;
+
+        protected ScreensBase() : this(null)
+        {
+            
+        }
+
+        public int ScreenCount => _screenCount ??= GetScreenCount();
+
+        public IReadOnlyList<Screen> AllScreens
+        {
+            get
+            {
+                EnsureScreens();
+                return _allScreens;
+            }
+        }
+
+        public Action? Changed { get; set; }
+
+        public Screen? ScreenFromWindow(IWindowBaseImpl window) => ScreenFromTopLevel(window);
+
+        public Screen? ScreenFromTopLevel(ITopLevelImpl topLevel) => ScreenFromTopLevelCore(topLevel);
+
+        public Screen? ScreenFromPoint(PixelPoint point) => ScreenFromPointCore(point);
+
+        public Screen? ScreenFromRect(PixelRect rect) => ScreenFromRectCore(rect);
+
+        public void OnChanged()
+        {
+            // Mark cached fields invalid.
+            _screenCount = null;
+            _allScreens = null;
+            // Schedule a delayed job, so we can accumulate multiple continuous events into one.
+            // Also, if OnChanged was raises on non-UI thread - dispatch it.
+            _onChangeOperation?.Abort();
+            _onChangeOperation = Dispatcher.UIThread.InvokeAsync(() =>
+            {
+                // Ensure screens if there is at least one subscriber already,
+                // Or at least one screen was previously materialized, which we need to update now.
+                if (Changed is not null || _allScreensByKey.Count > 0)
+                {
+                    EnsureScreens();
+                    Changed?.Invoke();
+                }
+            }, DispatcherPriority.Input);
+        }
+
+        public async Task<bool> RequestScreenDetails()
+        {
+            _screenDetailsRequestGranted ??= await RequestScreenDetailsCore();
+
+            return _screenDetailsRequestGranted.Value;
+        }
+
+        protected bool TryGetScreen(TKey key,  [MaybeNullWhen(false)] out TScreen screen)
+        {
+            EnsureScreens();
+            return _allScreensByKey.TryGetValue(key, out screen);
+        }
+
+        protected virtual void ScreenAdded(TScreen screen) => ScreenChanged(screen);
+        protected virtual void ScreenChanged(TScreen screen) {}
+        protected virtual void ScreenRemoved(TScreen screen) => screen.OnRemoved();
+        protected virtual int GetScreenCount() => AllScreens.Count;
+        protected abstract IReadOnlyList<TKey> GetAllScreenKeys();
+        protected abstract TScreen CreateScreenFromKey(TKey key);
+        protected virtual Task<bool> RequestScreenDetailsCore() => Task.FromResult(true);
+
+        protected virtual Screen? ScreenFromTopLevelCore(ITopLevelImpl topLevel)
+        {
+            if (topLevel is IWindowImpl window)
+            {
+                return ScreenHelper.ScreenFromWindow(window, AllScreens);
+            }
+
+            return null;
+        }
+
+        protected virtual Screen? ScreenFromPointCore(PixelPoint point) => ScreenHelper.ScreenFromPoint(point, AllScreens);
+
+        protected virtual Screen? ScreenFromRectCore(PixelRect rect) => ScreenHelper.ScreenFromRect(rect, AllScreens);
+
+        [MemberNotNull(nameof(_allScreens))]
+        private void EnsureScreens()
+        {
+            if (_allScreens is not null)
+                return;
+
+            var screens = GetAllScreenKeys();
+            var screensSet = new HashSet<TKey>(screens, screenKeyComparer);
+
+            _allScreens = new TScreen[screens.Count];
+
+            foreach (var oldScreenKey in _allScreensByKey.Keys)
+            {
+                if (!screensSet.Contains(oldScreenKey))
+                {
+                    if (_allScreensByKey.TryGetValue(oldScreenKey, out var screen)
+                        && _allScreensByKey.Remove(oldScreenKey))
+                    {
+                        ScreenRemoved(screen);
+                    }
+                }
+            }
+
+            int i = 0;
+            foreach (var newScreenKey in screens)
+            {
+                if (_allScreensByKey.TryGetValue(newScreenKey, out var oldScreen))
+                {
+                    ScreenChanged(oldScreen);
+                    _allScreens[i] = oldScreen;
+                }
+                else
+                {
+                    var newScreen = CreateScreenFromKey(newScreenKey);
+                    ScreenAdded(newScreen);
+                    _allScreensByKey[newScreenKey] = newScreen;
+                    _allScreens[i] = newScreen;
+                }
+
+                i++;
+            }
+        }
     }
 }

+ 0 - 5
src/Avalonia.Controls/Platform/ITopLevelImpl.cs

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

+ 120 - 13
src/Avalonia.Controls/Platform/Screen.cs

@@ -1,13 +1,61 @@
 using System;
 using System.ComponentModel;
+using Avalonia.Diagnostics;
+using Avalonia.Metadata;
+using Avalonia.Utilities;
 
 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>
     /// Represents a single display screen.
     /// </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>
         /// Gets the scaling factor applied to the screen by the operating system.
         /// </summary>
@@ -15,19 +63,19 @@ namespace Avalonia.Platform
         /// Multiply this value by 100 to get a percentage.
         /// Both X and Y scaling factors are assumed uniform.
         /// </remarks>
-        public double Scaling { get; }
+        public double Scaling { get; protected set; } = 1;
 
         /// <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;
 
         /// <summary>
-        /// Gets the overall pixel-size of the screen.
+        /// Gets the overall pixel-size and position of the screen.
         /// </summary>
         /// <remarks>
         /// This generally is the raw pixel counts in both the X and Y direction.
         /// </remarks>
-        public PixelRect Bounds { get; }
+        public PixelRect Bounds { get; protected set; }
 
         /// <summary>
         /// 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
         /// other block-out areas such as taskbars etc.
         /// </remarks>
-        public PixelRect WorkingArea { get; }
+        public PixelRect WorkingArea { get; protected set; }
 
         /// <summary>
         /// Gets a value indicating whether the screen is the primary one.
         /// </summary>
-        public bool IsPrimary { get; }
+        public bool IsPrimary { get; protected set; }
 
         /// <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;
 
         /// <summary>
@@ -54,12 +102,71 @@ namespace Avalonia.Platform
         /// <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="isPrimary">Whether the screen is the primary one.</param>
+        [Unstable(ObsoletionMessages.MayBeRemovedInAvalonia12)]
         public Screen(double scaling, PixelRect bounds, PixelRect workingArea, bool isPrimary)
         {
-            this.Scaling = scaling;
-            this.Bounds = bounds;
-            this.WorkingArea = workingArea;
-            this.IsPrimary = isPrimary;
-        } 
+            Scaling = scaling;
+            Bounds = bounds;
+            WorkingArea = workingArea;
+            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;
+        }
     }
 }

+ 2 - 2
src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs

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

+ 96 - 8
src/Avalonia.Controls/Screens.cs

@@ -2,10 +2,10 @@ using System;
 using System.Collections.Generic;
 using System.ComponentModel;
 using System.Linq;
+using System.Threading.Tasks;
+using Avalonia.Metadata;
 using Avalonia.Platform;
 
-#nullable enable
-
 namespace Avalonia.Controls
 {
     /// <summary>
@@ -14,25 +14,50 @@ namespace Avalonia.Controls
     public class Screens
     {
         private readonly IScreenImpl _iScreenImpl;
+        private EventHandler? _changedHandlers;
 
         /// <summary>
         /// Gets the total number of screens available on the device.
         /// </summary>
-        public int ScreenCount => _iScreenImpl?.ScreenCount ?? 0;
+        public int ScreenCount => _iScreenImpl.ScreenCount;
 
         /// <summary>
         /// Gets the list of all screens available on the device.
         /// </summary>
-        public IReadOnlyList<Screen> All => _iScreenImpl?.AllScreens ?? Array.Empty<Screen>();
+        public IReadOnlyList<Screen> All => _iScreenImpl.AllScreens;
 
         /// <summary>
         /// Gets the primary screen on the device.
         /// </summary>
         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>
         /// Initializes a new instance of the <see cref="Screens"/> class.
         /// </summary>
+        [PrivateApi]
         public Screens(IScreenImpl iScreenImpl)
         {
             _iScreenImpl = iScreenImpl;
@@ -41,6 +66,9 @@ namespace Avalonia.Controls
         /// <summary>
         /// Retrieves a Screen for the display that contains the rectangle.
         /// </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>
         /// <returns>The <see cref="Screen"/>.</returns>
         public Screen? ScreenFromBounds(PixelRect bounds)
@@ -56,6 +84,10 @@ namespace Avalonia.Controls
         /// <returns>The <see cref="Screen"/>.</returns>
         public Screen? ScreenFromWindow(WindowBase window)
         {
+            if (window is null)
+            {
+                throw new ArgumentNullException(nameof(window));
+            }
             if (window.PlatformImpl is null)
             {
                 throw new ObjectDisposedException("Window platform implementation was already disposed.");
@@ -64,12 +96,32 @@ namespace Avalonia.Controls
             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>
         /// Retrieves a Screen for the display that contains the specified <see cref="IWindowBaseImpl"/>.
         /// </summary>
         /// <param name="window">The window impl for which to retrieve the Screen.</param>
         /// <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)
         {
             return _iScreenImpl.ScreenFromWindow(window);
@@ -78,6 +130,9 @@ namespace Avalonia.Controls
         /// <summary>
         /// Retrieves a Screen for the display that contains the specified point.
         /// </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>
         /// <returns>The <see cref="Screen"/>.</returns>
         public Screen? ScreenFromPoint(PixelPoint point)
@@ -92,10 +147,43 @@ namespace Avalonia.Controls
         /// <returns>The <see cref="Screen"/>.</returns>
         public Screen? ScreenFromVisual(Visual visual)
         {
-            var tl = visual.PointToScreen(visual.Bounds.TopLeft);
-            var br = visual.PointToScreen(visual.Bounds.BottomRight);
+            if (visual is null)
+            {
+                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);
         }
     }
 }

+ 8 - 1
src/Avalonia.Controls/TopLevel.cs

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

+ 3 - 2
src/Avalonia.Controls/WindowBase.cs

@@ -54,7 +54,6 @@ namespace Avalonia.Controls
 
         public WindowBase(IWindowBaseImpl impl, IAvaloniaDependencyResolver? dependencyResolver) : base(impl, dependencyResolver)
         {
-            Screens = new Screens(impl.Screen!);
             impl.Activated = HandleActivated;
             impl.Deactivated = HandleDeactivated;
             impl.PositionChanged = HandlePositionChanged;
@@ -109,7 +108,9 @@ namespace Avalonia.Controls
             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>
         /// Gets or sets the owner of the window.

+ 6 - 2
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<bool> ExtendClientAreaToDecorationsChanged { get; set; }
@@ -102,7 +101,12 @@ namespace Avalonia.DesignerSupport.Remote
             {
                 return new NoopStorageProvider();
             }
-            
+
+            if (featureType == typeof(IScreenImpl))
+            {
+                return new ScreenStub();
+            }
+
             return base.TryGetFeature(featureType);
         }
         

+ 11 - 19
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)
         {
         }
@@ -243,26 +241,20 @@ namespace Avalonia.DesignerSupport.Remote
         public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) => new IconStub();
     }
 
-    class ScreenStub : IScreenImpl
+    class ScreenStub : ScreensBase<int, PlatformScreen>
     {
-        public int ScreenCount => 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);
-        }
+        protected override IReadOnlyList<int> GetAllScreenKeys() => new[] { 1 };
 
-        public Screen ScreenFromRect(PixelRect rect)
-        {
-            return ScreenHelper.ScreenFromRect(rect, AllScreens);
-        }
+        protected override PlatformScreen CreateScreenFromKey(int key) => new PlatformScreenStub(key);
 
-        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 - 0
src/Avalonia.Native/AvaloniaNativePlatform.cs

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

+ 1 - 1
src/Avalonia.Native/EmbeddableTopLevelImpl.cs

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

+ 3 - 3
src/Avalonia.Native/PopupImpl.cs

@@ -18,7 +18,7 @@ namespace Avalonia.Native
             
             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));
@@ -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)

+ 52 - 37
src/Avalonia.Native/ScreenImpl.cs

@@ -2,66 +2,81 @@ using System;
 using System.Collections.Generic;
 using Avalonia.Native.Interop;
 using Avalonia.Platform;
+using MicroCom.Runtime;
+
+#nullable enable
 
 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;
 
-        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)
-                {
-                    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>();
+                _native.GetScreenIds(displayIdsPtr);
             }
-        }
 
-        public void Dispose ()
-        {
-            _native?.Dispose();
-            _native = null;
+            return displayIds;
         }
 
-        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();
         }
     }
 }

+ 6 - 6
src/Avalonia.Native/TopLevelImpl.cs

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

+ 3 - 3
src/Avalonia.Native/WindowImpl.cs

@@ -25,15 +25,15 @@ namespace Avalonia.Native
             
             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);
         }
 
-        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

+ 7 - 5
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;
 
-            base.Init(handle, screens);
+            base.Init(handle);
 
-            var monitor = Screen!.AllScreens.OrderBy(x => x.Scaling)
-                .FirstOrDefault(m => m.Bounds.Contains(Position));
+            var monitor = this.TryGetFeature<IScreenImpl>()!.AllScreens
+                .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);
         }
@@ -96,7 +97,8 @@ namespace Avalonia.Native
             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();
 
         public void SetTopmost(bool value)

+ 21 - 3
src/Avalonia.Native/avn.idl

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

+ 16 - 2
src/Avalonia.X11/Screens/X11Screens.cs

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

+ 9 - 5
src/Avalonia.X11/X11Window.cs

@@ -129,9 +129,9 @@ namespace Avalonia.X11
 
             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));
 
                 if (monitor != null)
@@ -932,7 +932,14 @@ namespace Avalonia.X11
             }
 
             if (featureType == typeof(IX11OptionsToplevelImplFeature))
+            {
                 return this;
+            }
+
+            if (featureType == typeof(IScreenImpl))
+            {
+                return _platform.Screens;
+            }
 
             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))
             .OrderByDescending(x => x.Width + x.Height).FirstOrDefault();
 

+ 0 - 23
src/Browser/Avalonia.Browser/WinStubs.cs

@@ -22,27 +22,4 @@ namespace Avalonia.Browser
 
         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);
-        }
-    }
 }

+ 11 - 20
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[]
-        {
-            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);
-        }
+        protected override PlatformScreen CreateScreenFromKey(int key) => new PlatformScreenStub(key);
 
-        public Screen? ScreenFromRect(PixelRect rect)
+        private class PlatformScreenStub : PlatformScreen
         {
-            return ScreenHelper.ScreenFromRect(rect, AllScreens);
-        }
-
-        public Screen? ScreenFromWindow(IWindowBaseImpl window)
-        {
-            return ScreenHelper.ScreenFromWindow(window, AllScreens);
+            public PlatformScreenStub(int key) : base(new PlatformHandle((nint)key, nameof(HeadlessScreensStub)))
+            {
+                Scaling = 1;
+                Bounds = WorkingArea = new PixelRect(0, 0, 1920, 1280);
+                IsPrimary = true;
+            }
         }
     }
-    
+
     internal static class TextTestHelper
     {
         public static int GetStartCharIndex(ReadOnlyMemory<char> text)

+ 8 - 1
src/Windows/Avalonia.Win32/Avalonia.Win32.csproj

@@ -5,8 +5,15 @@
     <!-- We still keep BinaryFormatter for WinForms compatibility. -->
     <EnableUnsafeBinaryFormatterSerialization>true</EnableUnsafeBinaryFormatterSerialization>
   </PropertyGroup>
-  <ItemGroup>
+  <ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.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>
     <ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />

+ 0 - 37
src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs

@@ -31,43 +31,6 @@ namespace Avalonia.Win32.DirectX
         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
     {
         public fixed ushort Description[128];

+ 0 - 6
src/Windows/Avalonia.Win32/DirectX/DirectXUnmanagedMethods.cs

@@ -17,12 +17,6 @@ namespace Avalonia.Win32.DirectX
         [DllImport("dxgi", ExactSpelling = true, PreserveSig = false)]
         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)]
         public static extern void D3D11CreateDevice(
             IntPtr adapter, D3D_DRIVER_TYPE DriverType,

+ 4 - 13
src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs

@@ -122,19 +122,10 @@ namespace Avalonia.Win32.DirectX
                     using var output = MicroComRuntime.CreateProxyFor<IDXGIOutput>(outputPointer, true);
                     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. 
-                    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)
+                    if (highestRefreshRate < frequency)
                     {
                         // ooh I like this output! 
                         if (_output is not null)
@@ -143,7 +134,7 @@ namespace Avalonia.Win32.DirectX
                             _output = null;
                         }
                         _output = MicroComRuntime.CloneReference(output);
-                        highestRefreshRate = devMode.dmDisplayFrequency;
+                        highestRefreshRate = frequency;
                     }
                     // and then increment index to move onto the next monitor 
                     outputIndex++;

+ 9 - 23
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@@ -6,6 +6,8 @@ using System.Diagnostics.CodeAnalysis;
 using System.Runtime.InteropServices;
 using System.Runtime.InteropServices.ComTypes;
 using System.Text;
+using Windows.Win32;
+using Windows.Win32.Graphics.Gdi;
 using MicroCom.Runtime;
 
 // ReSharper disable InconsistentNaming
@@ -1174,12 +1176,6 @@ namespace Avalonia.Win32.Interop
         [DllImport("user32.dll", SetLastError = true)]
         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)]
         public static extern IntPtr GetDC(IntPtr hWnd);
 
@@ -1637,10 +1633,6 @@ namespace Avalonia.Win32.Interop
         [DllImport("user32.dll")]
         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")]
         public static extern bool GetTouchInputInfo(
             IntPtr hTouchInput,
@@ -2128,23 +2120,17 @@ namespace Avalonia.Win32.Interop
         }
 
         [StructLayout(LayoutKind.Sequential)]
-        internal struct MONITORINFO
+        internal struct MONITORINFOEX
         {
-            public int cbSize;
-            public RECT rcMonitor;
-            public RECT rcWork;
-            public int dwFlags;
+            internal MONITORINFO Base;
 
-            public static MONITORINFO Create()
-            {
-                return new MONITORINFO() { cbSize = Marshal.SizeOf<MONITORINFO>() };
-            }
+            internal __char_32 szDevice;
 
-            public enum MonitorOptions : uint
+            public static MONITORINFOEX Create()
             {
-                MONITOR_DEFAULTTONULL = 0x00000000,
-                MONITOR_DEFAULTTOPRIMARY = 0x00000001,
-                MONITOR_DEFAULTTONEAREST = 0x00000002
+                var info = new MONITORINFO();
+                info.cbSize = (uint)Marshal.SizeOf<MONITORINFOEX>();
+                return new MONITORINFOEX() { Base = info };
             }
         }
 

+ 10 - 0
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

+ 3 - 7
src/Windows/Avalonia.Win32/PopupImpl.cs

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

+ 70 - 96
src/Windows/Avalonia.Win32/ScreenImpl.cs

@@ -1,124 +1,98 @@
 using System;
 using System.Collections.Generic;
-using System.Linq;
-using Avalonia.Metadata;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
 using Avalonia.Platform;
+using Avalonia.Win32.Interop;
+using Windows.Win32;
 using static Avalonia.Win32.Interop.UnmanagedMethods;
+using winmdroot = global::Windows.Win32;
 
-namespace Avalonia.Win32
+namespace Avalonia.Win32;
+
+internal unsafe class ScreenImpl : ScreensBase<nint, WinScreen>
 {
-    internal class ScreenImpl : IScreenImpl
-    {
-        private Screen[]? _allScreens;
+    protected override int GetScreenCount() => GetSystemMetrics(SystemMetric.SM_CMONITORS);
 
-        /// <inheritdoc />
-        public int ScreenCount
+    protected override IReadOnlyList<nint> GetAllScreenKeys()
+    {
+        var screens = new List<nint>();
+        var gcHandle = GCHandle.Alloc(screens);
+        try
+        {
+            PInvoke.EnumDisplayMonitors(default, default(winmdroot.Foundation.RECT*), EnumDisplayMonitorsCallback, (IntPtr)gcHandle);
+        }
+        finally
         {
-            get => GetSystemMetrics(SystemMetric.SM_CMONITORS);
+            gcHandle.Free();
         }
 
-        /// <inheritdoc />
-        public IReadOnlyList<Screen> AllScreens
+        return screens;
+
+        static winmdroot.Foundation.BOOL EnumDisplayMonitorsCallback(
+            winmdroot.Graphics.Gdi.HMONITOR monitor,
+            winmdroot.Graphics.Gdi.HDC hdcMonitor,
+            winmdroot.Foundation.RECT* lprcMonitor,
+            winmdroot.Foundation.LPARAM dwData)
         {
-            get
+            if (GCHandle.FromIntPtr(dwData).Target is List<nint> screens)
             {
-                if (_allScreens == null)
-                {
-                    int index = 0;
-                    Screen[] screens = new Screen[ScreenCount];
-                    EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero,
-                        (IntPtr monitor, IntPtr hdcMonitor, ref Rect lprcMonitor, IntPtr data) =>
-                        {
-                            MONITORINFO monitorInfo = MONITORINFO.Create();
-                            if (GetMonitorInfo(monitor, ref monitorInfo))
-                            {
-                                var dpi = 1.0;
-
-                                var shcore = LoadLibrary("shcore.dll");
-                                var method = GetProcAddress(shcore, nameof(GetDpiForMonitor));
-                                if (method != IntPtr.Zero)
-                                {
-                                    GetDpiForMonitor(monitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var x, out _);
-                                    dpi = x;
-                                }
-                                else
-                                {
-                                    var hdc = GetDC(IntPtr.Zero);
-
-                                    double virtW = GetDeviceCaps(hdc, DEVICECAP.HORZRES);
-                                    double physW = GetDeviceCaps(hdc, DEVICECAP.DESKTOPHORZRES);
-
-                                    dpi = (96d * physW / virtW);
-
-                                    ReleaseDC(IntPtr.Zero, hdc);
-                                }
-
-                                RECT bounds = monitorInfo.rcMonitor;
-                                RECT workingArea = monitorInfo.rcWork;
-                                PixelRect avaloniaBounds = bounds.ToPixelRect();
-                                PixelRect avaloniaWorkArea = workingArea.ToPixelRect();
-                                screens[index] =
-                                    new WinScreen(dpi / 96.0d, avaloniaBounds, avaloniaWorkArea, monitorInfo.dwFlags == 1,
-                                        monitor);
-                                index++;
-                            }
-                            return true;
-                        }, IntPtr.Zero);
-                    _allScreens = screens;
-                }
-                return _allScreens;
+                screens.Add(monitor);
+                return true;
             }
+            return false;
         }
+    }
+
+    protected override WinScreen CreateScreenFromKey(nint key) => new(key);
+    protected override void ScreenChanged(WinScreen screen) => screen.Refresh();
 
-        public void InvalidateScreensCache()
+    protected override Screen? ScreenFromTopLevelCore(ITopLevelImpl topLevel)
+    {
+        if (topLevel.Handle?.Handle is { } handle)
         {
-            _allScreens = null;
+            return ScreenFromHwnd(handle);
         }
 
-        /// <inheritdoc />
-        public Screen? ScreenFromWindow(IWindowBaseImpl window)
-        {
-            var handle = window.Handle?.Handle;
+        return null;
+    }
 
-            if (handle is null)
-            {
-                return null;
-            }
-            
-            var monitor = MonitorFromWindow(handle.Value, MONITOR.MONITOR_DEFAULTTONULL);
+    protected override Screen? ScreenFromPointCore(PixelPoint point)
+    {
+        var monitor = MonitorFromPoint(new POINT
+        {
+            X = point.X,
+            Y = point.Y
+        }, UnmanagedMethods.MONITOR.MONITOR_DEFAULTTONULL);
 
-            return FindScreenByHandle(monitor);
-        }
+        return ScreenFromHMonitor(monitor);
+    }
 
-        /// <inheritdoc />
-        public Screen? ScreenFromPoint(PixelPoint point)
+    protected override Screen? ScreenFromRectCore(PixelRect rect)
+    {
+        var monitor = MonitorFromRect(new RECT
         {
-            var monitor = MonitorFromPoint(new POINT
-            {
-                X = point.X,
-                Y = point.Y
-            }, MONITOR.MONITOR_DEFAULTTONULL);
+            left = rect.TopLeft.X,
+            top = rect.TopLeft.Y,
+            right = rect.TopRight.X,
+            bottom = rect.BottomRight.Y
+        }, UnmanagedMethods.MONITOR.MONITOR_DEFAULTTONULL);
 
-            return FindScreenByHandle(monitor);
-        }
+        return ScreenFromHMonitor(monitor);
+    }
 
-        /// <inheritdoc />
-        public Screen? ScreenFromRect(PixelRect rect)
-        {
-            var monitor = MonitorFromRect(new RECT
-            {
-                left = rect.TopLeft.X,
-                top = rect.TopLeft.Y,
-                right = rect.TopRight.X,
-                bottom = rect.BottomRight.Y
-            }, MONITOR.MONITOR_DEFAULTTONULL);
+    public WinScreen? ScreenFromHMonitor(IntPtr hmonitor)
+    {
+        if (TryGetScreen(hmonitor, out var screen))
+            return screen;
 
-            return FindScreenByHandle(monitor);
-        }
+        return null;
+    }
 
-        private Screen? FindScreenByHandle(IntPtr handle)
-        {
-            return AllScreens.Cast<WinScreen>().FirstOrDefault(m => m.Handle == handle);
-        }
+    public WinScreen? ScreenFromHwnd(IntPtr hwnd, MONITOR flags = MONITOR.MONITOR_DEFAULTTONULL)
+    {
+        var monitor = MonitorFromWindow(hwnd, flags);
+
+        return ScreenFromHMonitor(monitor);
     }
 }

+ 3 - 1
src/Windows/Avalonia.Win32/Win32Platform.cs

@@ -56,7 +56,8 @@ namespace Avalonia.Win32
         }
 
         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;
 
@@ -91,6 +92,7 @@ namespace Avalonia.Win32
                 .Bind<ICursorFactory>().ToConstant(CursorFactory.Instance)
                 .Bind<IKeyboardDevice>().ToConstant(WindowsKeyboardDevice.Instance)
                 .Bind<IPlatformSettings>().ToSingleton<Win32PlatformSettings>()
+                .Bind<IScreenImpl>().ToSingleton<ScreenImpl>()
                 .Bind<IDispatcherImpl>().ToConstant(s_instance._dispatcher)
                 .Bind<IRenderTimer>().ToConstant(renderTimer)
                 .Bind<IWindowingPlatform>().ToConstant(s_instance)

+ 6 - 0
src/Windows/Avalonia.Win32/Win32TypeExtensions.cs

@@ -4,6 +4,12 @@ namespace Avalonia.Win32
 {
     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)
         {
             return new PixelRect(rect.left, rect.top, rect.right - rect.left,

+ 110 - 14
src/Windows/Avalonia.Win32/WinScreen.cs

@@ -1,30 +1,126 @@
 using System;
+using System.Runtime.InteropServices;
+using Windows.Win32;
+using Windows.Win32.Devices.Display;
+using Windows.Win32.Foundation;
+using Windows.Win32.Graphics.Gdi;
 using Avalonia.Platform;
+using static Avalonia.Win32.Interop.UnmanagedMethods;
 
-namespace Avalonia.Win32
+namespace Avalonia.Win32;
+
+internal sealed unsafe class WinScreen(IntPtr hMonitor) : PlatformScreen(new PlatformHandle(hMonitor, "HMonitor"))
 {
-    internal class WinScreen : Screen
+    private static readonly Lazy<bool> s_hasGetDpiForMonitor = new(() =>
+    {
+        var shcore = LoadLibrary("shcore.dll");
+        var method = GetProcAddress(shcore, nameof(GetDpiForMonitor));
+        return method != IntPtr.Zero;
+    });
+
+    internal int Frequency { get; private set; }
+
+    public void Refresh()
     {
-        private readonly IntPtr _hMonitor;
+        var info = MONITORINFOEX.Create();
+        PInvoke.GetMonitorInfo(new HMONITOR(hMonitor), (MONITORINFO*)&info);
 
-        public WinScreen(double scaling, PixelRect bounds, PixelRect workingArea, bool isPrimary, IntPtr hMonitor)
-            : base(scaling, bounds, workingArea, isPrimary)
+        IsPrimary = info.Base.dwFlags == 1;
+        Bounds = info.Base.rcMonitor.ToPixelRect();
+        WorkingArea = info.Base.rcWork.ToPixelRect();
+        Scaling = GetScaling();
+        DisplayName ??= GetDisplayName(ref info);
+
+        var deviceMode = new DEVMODEW
         {
-            _hMonitor = hMonitor;
-        }
+            dmFields = DEVMODE_FIELD_FLAGS.DM_DISPLAYORIENTATION | DEVMODE_FIELD_FLAGS.DM_DISPLAYFREQUENCY,
+            dmSize = (ushort)Marshal.SizeOf<DEVMODEW>()
+        };
+        PInvoke.EnumDisplaySettings(info.szDevice.ToString(), ENUM_DISPLAY_SETTINGS_MODE.ENUM_CURRENT_SETTINGS,
+            ref deviceMode);
 
-        public IntPtr Handle => _hMonitor;
+        Frequency = (int)deviceMode.dmDisplayFrequency;
+        CurrentOrientation = deviceMode.Anonymous1.Anonymous2.dmDisplayOrientation switch
+        {
+            DEVMODE_DISPLAY_ORIENTATION.DMDO_DEFAULT => ScreenOrientation.Landscape,
+            DEVMODE_DISPLAY_ORIENTATION.DMDO_90 => ScreenOrientation.Portrait,
+            DEVMODE_DISPLAY_ORIENTATION.DMDO_180 => ScreenOrientation.LandscapeFlipped,
+            DEVMODE_DISPLAY_ORIENTATION.DMDO_270 => ScreenOrientation.PortraitFlipped,
+            _ => ScreenOrientation.None
+        };
+    }
 
-        /// <inheritdoc />
-        public override int GetHashCode()
+    private string? GetDisplayName(ref MONITORINFOEX monitorinfo)
+    {
+        var deviceName = monitorinfo.szDevice;
+        if (Win32Platform.WindowsVersion >= PlatformConstants.Windows7)
         {
-            return _hMonitor.GetHashCode();
+            if (PInvoke.GetDisplayConfigBufferSizes(
+                    QUERY_DISPLAY_CONFIG_FLAGS.QDC_ONLY_ACTIVE_PATHS,
+                    out var numPathInfo, out var numModeInfo) != WIN32_ERROR.NO_ERROR)
+                return null;
+
+            var paths = stackalloc DISPLAYCONFIG_PATH_INFO[(int)numPathInfo];
+            var modes = stackalloc DISPLAYCONFIG_MODE_INFO[(int)numModeInfo];
+
+            if (PInvoke.QueryDisplayConfig(
+                    QUERY_DISPLAY_CONFIG_FLAGS.QDC_ONLY_ACTIVE_PATHS, ref numPathInfo, paths, ref numModeInfo, modes,
+                    default) != WIN32_ERROR.NO_ERROR)
+                return null;
+
+            var sourceName = new DISPLAYCONFIG_SOURCE_DEVICE_NAME();
+            sourceName.header.type = DISPLAYCONFIG_DEVICE_INFO_TYPE.DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME;
+            sourceName.header.size = (uint)sizeof(DISPLAYCONFIG_SOURCE_DEVICE_NAME);
+            var targetName = new DISPLAYCONFIG_TARGET_DEVICE_NAME();
+            targetName.header.type = DISPLAYCONFIG_DEVICE_INFO_TYPE.DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME;
+            targetName.header.size = (uint)sizeof(DISPLAYCONFIG_TARGET_DEVICE_NAME);
+
+            for (var i = 0; i < numPathInfo; i++)
+            {
+                sourceName.header.adapterId = paths[i].targetInfo.adapterId;
+                sourceName.header.id = paths[i].sourceInfo.id;
+
+                targetName.header.adapterId = paths[i].targetInfo.adapterId;
+                targetName.header.id = paths[i].targetInfo.id;
+
+                if (PInvoke.DisplayConfigGetDeviceInfo(ref sourceName.header) != 0)
+                    break;
+
+                if (!sourceName.viewGdiDeviceName.Equals(deviceName.ToString()))
+                    continue;
+
+                if (PInvoke.DisplayConfigGetDeviceInfo(ref targetName.header) != 0)
+                    break;
+
+                return targetName.monitorFriendlyDeviceName.ToString();
+            }
         }
 
-        /// <inheritdoc />
-        public override bool Equals(object? obj)
+        // Fallback to MONITORINFOEX - \\DISPLAY1.
+        return deviceName.ToString();
+    }
+
+    private double GetScaling()
+    {
+        double dpi;
+
+        if (s_hasGetDpiForMonitor.Value)
         {
-            return obj is WinScreen screen && _hMonitor == screen._hMonitor;
+            GetDpiForMonitor(hMonitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var x, out _);
+            dpi = x;
         }
+        else
+        {
+            var hdc = GetDC(IntPtr.Zero);
+
+            double virtW = GetDeviceCaps(hdc, DEVICECAP.HORZRES);
+            double physW = GetDeviceCaps(hdc, DEVICECAP.DESKTOPHORZRES);
+
+            dpi = (96d * physW / virtW);
+
+            ReleaseDC(IntPtr.Zero, hdc);
+        }
+
+        return dpi / 96d;
     }
 }

+ 1 - 1
src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs

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

+ 33 - 35
src/Windows/Avalonia.Win32/WindowImpl.cs

@@ -173,7 +173,7 @@ namespace Avalonia.Win32
                 }
             }
 
-            Screen = new ScreenImpl();
+            Screen = Win32Platform.Instance.Screen;
             _storageProvider = new Win32StorageProvider(this);
             _inputPane = WindowsInputPane.TryCreate(this);
             _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; }
 
@@ -337,6 +337,11 @@ namespace Avalonia.Win32
 
         public object? TryGetFeature(Type featureType)
         {
+            if (featureType == typeof(IScreenImpl))
+            {
+                return Screen;
+            }
+
             if (featureType == typeof(ITextInputMethodImpl))
             {
                 return Imm32InputMethod.Current;
@@ -1041,15 +1046,14 @@ namespace Avalonia.Win32
 
                 // On expand, if we're given a window_rect, grow to it, otherwise do
                 // not resize.
-                MONITORINFO monitor_info = MONITORINFO.Create();
-                GetMonitorInfo(MonitorFromWindow(_hwnd, MONITOR.MONITOR_DEFAULTTONEAREST), ref monitor_info);
-
-                var window_rect = monitor_info.rcMonitor.ToPixelRect();
-
-                _isFullScreenActive = true;
-                SetWindowPos(_hwnd, IntPtr.Zero, window_rect.X, window_rect.Y,
-                             window_rect.Width, window_rect.Height,
-                             SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED);
+                var screen = Screen.ScreenFromHwnd(_hwnd, MONITOR.MONITOR_DEFAULTTONEAREST);
+                if (screen?.Bounds is { } window_rect)
+                {
+                    _isFullScreenActive = true;
+                    SetWindowPos(_hwnd, IntPtr.Zero, window_rect.X, window_rect.Y,
+                                 window_rect.Width, window_rect.Height,
+                                 SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED);
+                }
             }
             else
             {
@@ -1283,34 +1287,28 @@ namespace Avalonia.Win32
 
         private void MaximizeWithoutCoveringTaskbar()
         {
-            IntPtr monitor = MonitorFromWindow(_hwnd, MONITOR.MONITOR_DEFAULTTONEAREST);
-
-            if (monitor != IntPtr.Zero)
+            var screen = Screen.ScreenFromHwnd(Hwnd, MONITOR.MONITOR_DEFAULTTONEAREST);
+            if (screen?.WorkingArea is { } workingArea)
             {
-                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;
-                    var y = monitorInfo.rcWork.top;
-                    var cx = Math.Abs(monitorInfo.rcWork.right - x);
-                    var cy = Math.Abs(monitorInfo.rcWork.bottom - y);
-                    var style = (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE);
-
-                    if (!style.HasFlag(WindowStyles.WS_THICKFRAME))
-                    {
-                        // 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);
+                    // 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);
             }
         }
 

+ 1 - 1
tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs

@@ -669,7 +669,7 @@ namespace Avalonia.Controls.UnitTests
             popupImpl.SetupGet(x => x.RenderScaling).Returns(1);
             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(
                                         focusManager: new FocusManager(),

+ 1 - 1
tests/Avalonia.Controls.UnitTests/MenuItemTests.cs

@@ -816,7 +816,7 @@ namespace Avalonia.Controls.UnitTests
             popupImpl.SetupGet(x => x.RenderScaling).Returns(1);
             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(
                 inputManager: new InputManager(),

+ 173 - 0
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; }
+    }
+}

+ 3 - 3
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.DesktopScaling).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))
             {
@@ -563,7 +563,7 @@ namespace Avalonia.Controls.UnitTests
             windowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480));
             windowImpl.Setup(x => x.DesktopScaling).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))
             {
@@ -592,7 +592,7 @@ namespace Avalonia.Controls.UnitTests
             var windowImpl = MockWindowingPlatform.CreateWindowMock(400, 300);
             windowImpl.Setup(x => x.DesktopScaling).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))
             {

+ 64 - 0
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);
+    }
+}

+ 1 - 2
tests/Avalonia.UnitTests/MockWindowingPlatform.cs

@@ -34,9 +34,8 @@ namespace Avalonia.UnitTests
             windowImpl.Setup(x => x.MaxAutoSizeHint).Returns(s_screenSize);
             windowImpl.Setup(x => x.DesktopScaling).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(x => x.TryGetFeature(It.Is<Type>(t => t == typeof(IScreenImpl)))).Returns(CreateScreenMock().Object);
 
             windowImpl.Setup(x => x.CreatePopup()).Returns(() =>
             {