Skip to content
Merged
Show file tree
Hide file tree
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
76 changes: 76 additions & 0 deletions ImGui.App/ForceDpiAware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ public static double GetActualScaleFactor()
return GdiPlusHelper.GetDpiX(IntPtr.Zero);
}

// Modern macOS has no X11; interrogate the display's backing scale via CoreGraphics.
if (OperatingSystem.IsMacOS())
{
return GetMacOSDpiScale();
}

string? xdgSessionType = Environment.GetEnvironmentVariable("XDG_SESSION_TYPE")?.ToLower();

// X11 (and unspecified session type) uses X11 detection; everything else falls back to Wayland.
Expand Down Expand Up @@ -129,6 +135,76 @@ private static double GetX11DpiScale()
return userDpiScale;
}

/// <summary>
/// Gets the DPI scale factor on macOS from the main display's backing scale factor.
/// </summary>
/// <returns>
/// The actual scale factor: <see cref="StandardDpiScale"/> on a standard display,
/// twice that on a Retina display. Falls back to <see cref="StandardDpiScale"/> when the
/// display mode cannot be read or CoreGraphics is unavailable (for example, a headless host).
/// </returns>
// This method is pure native orchestration over the CoreGraphics P/Invokes; every line only
// runs on a macOS host with a display, so it cannot be exercised by the coverage runner (which
// runs on Windows). The testable scale arithmetic lives in MacOSBackingScaleToDpi, which is
// covered by unit tests. Excluded from coverage for that reason, matching the repository's
// treatment of other native C ABI boundary code.
[ExcludeFromCodeCoverage(Justification = "Native CoreGraphics orchestration; only reachable on a macOS host with a display. Testable arithmetic is factored into MacOSBackingScaleToDpi.")]
private static double GetMacOSDpiScale()
{
try
{
uint displayId = NativeMethods.CGMainDisplayID();
nint mode = NativeMethods.CGDisplayCopyDisplayMode(displayId);
if (mode == IntPtr.Zero)
{
return StandardDpiScale;
}

try
{
// The pixel-width to point-width ratio is the display's backing scale factor
// (1.0 on a standard display, 2.0 on Retina).
nuint pixelWidth = NativeMethods.CGDisplayModeGetPixelWidth(mode);
nuint pointWidth = NativeMethods.CGDisplayModeGetWidth(mode);
return MacOSBackingScaleToDpi(pixelWidth, pointWidth);
}
finally
{
// CGDisplayCopyDisplayMode follows the Core Foundation "Copy" ownership rule.
NativeMethods.CGDisplayModeRelease(mode);
}
}
catch (DllNotFoundException)
{
return StandardDpiScale;
}
catch (EntryPointNotFoundException)
{
return StandardDpiScale;
}
}

/// <summary>
/// Converts a macOS display mode's pixel and point widths into an actual DPI scale factor.
/// </summary>
/// <param name="pixelWidth">The display mode width in physical pixels.</param>
/// <param name="pointWidth">The display mode width in points.</param>
/// <returns>
/// <see cref="StandardDpiScale"/> scaled by the pixel-to-point ratio (the display's backing
/// scale factor: <c>1.0</c> on a standard display, <c>2.0</c> on Retina), or
/// <see cref="StandardDpiScale"/> when <paramref name="pointWidth"/> is zero.
/// </returns>
internal static double MacOSBackingScaleToDpi(nuint pixelWidth, nuint pointWidth)
{
if (pointWidth == 0)
{
return StandardDpiScale;
}

double scale = (double)pixelWidth / pointWidth;
return scale * StandardDpiScale;
}

/// <summary>
/// Gets the window scale factor based on the actual scale factor and standard DPI scale.
/// </summary>
Expand Down
43 changes: 43 additions & 0 deletions ImGui.App/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -274,4 +274,47 @@ internal struct StartupOutput
[LibraryImport(GDILibraryName)]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
internal static partial int GdipGetDpiX(IntPtr graphics, out float dpi);

// CoreGraphics is loaded by its full framework path; macOS frameworks are not on the
// default dylib search path, so a bare "CoreGraphics" name would not resolve. No
// DefaultDllImportSearchPaths here: that attribute controls the Windows loader only.
private const string CoreGraphicsLibraryName = "/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics";

/// <summary>
/// Returns the display ID of the main display.
/// </summary>
/// <returns>The <c>CGDirectDisplayID</c> of the main display.</returns>
[LibraryImport(CoreGraphicsLibraryName)]
internal static partial uint CGMainDisplayID();

/// <summary>
/// Returns a copy of the current display mode for the given display.
/// </summary>
/// <param name="display">The display to query.</param>
/// <returns>A <c>CGDisplayModeRef</c> the caller owns and must release with <see cref="CGDisplayModeRelease"/>, or <see cref="IntPtr.Zero"/> on failure.</returns>
[LibraryImport(CoreGraphicsLibraryName)]
internal static partial nint CGDisplayCopyDisplayMode(uint display);

/// <summary>
/// Returns the width, in physical pixels, of the specified display mode.
/// </summary>
/// <param name="mode">The display mode to query.</param>
/// <returns>The pixel width of the display mode.</returns>
[LibraryImport(CoreGraphicsLibraryName)]
internal static partial nuint CGDisplayModeGetPixelWidth(nint mode);

/// <summary>
/// Returns the width, in points, of the specified display mode.
/// </summary>
/// <param name="mode">The display mode to query.</param>
/// <returns>The point width of the display mode.</returns>
[LibraryImport(CoreGraphicsLibraryName)]
internal static partial nuint CGDisplayModeGetWidth(nint mode);

/// <summary>
/// Releases a display mode obtained from <see cref="CGDisplayCopyDisplayMode"/>.
/// </summary>
/// <param name="mode">The display mode to release.</param>
[LibraryImport(CoreGraphicsLibraryName)]
internal static partial void CGDisplayModeRelease(nint mode);
}
40 changes: 40 additions & 0 deletions tests/ImGui.App.Tests/ForceDpiAwareTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,44 @@ public void GetActualScaleFactor_ReturnsValidValue()
// Assert
Assert.IsGreaterThan(0, actualScale, "Actual scale factor should be greater than 0");
}

[TestMethod]
public void MacOSBackingScaleToDpi_StandardDisplay_ReturnsStandardDpi()
{
// Arrange: pixel width equals point width on a non-Retina display (1x backing scale).
double dpi = ForceDpiAware.MacOSBackingScaleToDpi(1920, 1920);

// Assert
Assert.AreEqual(ForceDpiAware.StandardDpiScale, dpi, "A 1x display should map to the standard DPI scale");
}

[TestMethod]
public void MacOSBackingScaleToDpi_RetinaDisplay_ReturnsDoubleStandardDpi()
{
// Arrange: pixels are twice the points on a Retina display (2x backing scale).
double dpi = ForceDpiAware.MacOSBackingScaleToDpi(3840, 1920);

// Assert
Assert.AreEqual(ForceDpiAware.StandardDpiScale * 2.0, dpi, "A 2x Retina display should map to twice the standard DPI scale");
}

[TestMethod]
public void MacOSBackingScaleToDpi_FractionalScale_ScalesProportionally()
{
// Arrange: a 1.5x backing scale (e.g. a scaled Retina mode).
double dpi = ForceDpiAware.MacOSBackingScaleToDpi(2880, 1920);

// Assert
Assert.AreEqual(ForceDpiAware.StandardDpiScale * 1.5, dpi, "A 1.5x display should scale the standard DPI proportionally");
}

[TestMethod]
public void MacOSBackingScaleToDpi_ZeroPointWidth_FallsBackToStandardDpi()
{
// Arrange: a zero point width (unreadable mode) must not divide by zero.
double dpi = ForceDpiAware.MacOSBackingScaleToDpi(1920, 0);

// Assert
Assert.AreEqual(ForceDpiAware.StandardDpiScale, dpi, "A zero point width should fall back to the standard DPI scale");
}
}
Loading