Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions src/Essentials/src/DeviceDisplay/DeviceDisplay.ios.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,42 @@
#nullable enable
using System;
using System.Runtime.InteropServices;
using Foundation;
using UIKit;
using ObjCRuntime;

namespace Microsoft.Maui.Devices
{
partial class DeviceDisplayImplementation : IDeviceDisplay
{
NSObject? observer;

#if MACCATALYST
static readonly NSString ScreenParametersChangedNotification =
new NSString("NSApplicationDidChangeScreenParametersNotification");

// Core Graphics P/Invoke declarations for Mac Catalyst
[DllImport(Constants.CoreGraphicsLibrary)]
static extern uint CGMainDisplayID();

[DllImport(Constants.CoreGraphicsLibrary)]
static extern IntPtr CGDisplayCopyDisplayMode(uint display);

[DllImport(Constants.CoreGraphicsLibrary)]
static extern void CGDisplayModeRelease(IntPtr mode);

[DllImport(Constants.CoreGraphicsLibrary)]
static extern nuint CGDisplayModeGetWidth(IntPtr mode);

[DllImport(Constants.CoreGraphicsLibrary)]
static extern nuint CGDisplayModeGetHeight(IntPtr mode);

[DllImport(Constants.CoreGraphicsLibrary)]
static extern double CGDisplayModeGetRefreshRate(IntPtr mode);

[DllImport(Constants.CoreGraphicsLibrary)]
static extern double CGDisplayRotation(uint display);

readonly object locker = new object();
NSObject? keepScreenOnActivity;

Expand Down Expand Up @@ -50,6 +78,65 @@ protected override void SetKeepScreenOn(bool keepScreenOn)

protected override DisplayInfo GetMainDisplayInfo()
{
#if MACCATALYST
// On Mac Catalyst, bypass UIScreen entirely and use Core Graphics APIs
// This gets fresh, non-cached screen information directly from the system
// Note: CGMainDisplayID returns the primary display (with menu bar).
// In multi-monitor setups, this may not be the display the app window is on.
var displayId = CGMainDisplayID();
var mode = CGDisplayCopyDisplayMode(displayId);

if (mode == IntPtr.Zero)
{
return GetFallbackDisplayInfo();
}

try
{
var width = (double)CGDisplayModeGetWidth(mode);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] Logic and Correctness - CGDisplayModeGetWidth/Height return the display mode logical size on HiDPI Mac displays, but DisplayInfo.Width/Height are documented and previously returned as physical pixels (UIScreen.Bounds * Scale). On Retina/HiDPI modes this reports values that are too small while still reporting a Retina density. Use CGDisplayModeGetPixelWidth/PixelHeight for the pixel dimensions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a valid concern — we took the reference from Xamarin.Essentials' macOS implementation, which also returns logical units paired with BackingScaleFactor as density (not physical pixel dimensions): xamarin

var height = (double)CGDisplayModeGetHeight(mode);
var refreshRate = CGDisplayModeGetRefreshRate(mode);

// Get rotation from Core Graphics
var rotationDegrees = CGDisplayRotation(displayId);
var rotation = ConvertRotationDegreesToDisplayRotation(rotationDegrees);

// Get scale factor from UIScreen as a fallback (this is usually stable)
var scale = UIScreen.MainScreen.Scale;

return new DisplayInfo(
width: width,
height: height,
density: scale,
// Orientation is intentionally hardcoded to Portrait to match Xamarin's Mac Catalyst
// behavior. Deriving orientation from dimensions/rotation breaks existing tests and
// Mac desktop apps don't have a meaningful orientation concept.
orientation: DisplayOrientation.Portrait,
rotation: rotation,
rate: (float)refreshRate);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] Logic and Correctness - This PR reports the new Mac Catalyst refresh rate, but MainDisplayInfoChanged is still suppressed when only the refresh rate changes because DeviceDisplayImplementationBase.OnMainDisplayInfoChanged gates events through DisplayInfo.Equals, and DisplayInfo.Equals intentionally ignores RefreshRate. A user changing only the display refresh rate will still not receive the event this PR is meant to fix; compare refresh rate in the event-change gate without changing public DisplayInfo.Equals semantics.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In one of the previous reviews it was suggested to remove the from as this might be a breaking change, so removed that.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] Logic and Correctness - This now reports the CoreGraphics refresh rate on Mac Catalyst, but DeviceDisplayImplementationBase.OnMainDisplayInfoChanged() only raises MainDisplayInfoChanged when DisplayInfo.Equals() changes, and DisplayInfo.Equals() ignores RefreshRate. A screen-parameter notification caused only by a refresh-rate change will still be suppressed, leaving subscribers unaware even though MainDisplayInfo.RefreshRate changed. Include refresh rate in the change detection path or explicitly raise for this Mac Catalyst notification scenario.

}
finally
{
CGDisplayModeRelease(mode);
}
#else
// iOS implementation
return GetFallbackDisplayInfo();
#endif
}

static DisplayRotation ConvertRotationDegreesToDisplayRotation(double degrees) =>
degrees switch
{
0 => DisplayRotation.Rotation0,
90 => DisplayRotation.Rotation90,
180 => DisplayRotation.Rotation180,
270 => DisplayRotation.Rotation270,
_ => DisplayRotation.Rotation0
};

DisplayInfo GetFallbackDisplayInfo()
{
var bounds = UIScreen.MainScreen.Bounds;
var scale = UIScreen.MainScreen.Scale;

Expand All @@ -70,8 +157,16 @@ protected override DisplayInfo GetMainDisplayInfo()
protected override void StartScreenMetricsListeners()
{
var notificationCenter = NSNotificationCenter.DefaultCenter;

#if MACCATALYST
// On Mac Catalyst, use multiple notifications to cover all display changes
// NSApplicationDidChangeScreenParametersNotification - for resolution/refresh rate changes
observer = notificationCenter.AddObserver(ScreenParametersChangedNotification, OnMainDisplayInfoChanged);
#else
// On iOS, use status bar orientation changes (deprecated but still works)
var notification = UIApplication.DidChangeStatusBarOrientationNotification;
observer = notificationCenter.AddObserver(notification, OnMainDisplayInfoChanged);
#endif
}

protected override void StopScreenMetricsListeners()
Expand Down
Loading