* 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
This commit is contained in:
Max Katz 2024-07-16 15:48:18 -07:00 коммит произвёл GitHub
Родитель b865cc41c4
Коммит 05ac6d2f1d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
54 изменённых файлов: 1295 добавлений и 478 удалений

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -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() {
}

Просмотреть файл

@ -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:

Просмотреть файл

@ -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,4 +1,5 @@
#include "common.h"
#include "menu.h"
#include "KeyTransform.h"

Просмотреть файл

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

Просмотреть файл

@ -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) =>

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -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));
}
return ScreenFromBounds(new PixelRect(tl, br));
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);
}
}
/// <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);
}
}
}

Просмотреть файл

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

Просмотреть файл

@ -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.

Просмотреть файл

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

Просмотреть файл

@ -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;
protected override IReadOnlyList<int> GetAllScreenKeys() => new[] { 1 };
public IReadOnlyList<Screen> AllScreens { get; } =
new Screen[] { new Screen(1, new PixelRect(0, 0, 4000, 4000), new PixelRect(0, 0, 4000, 4000), true) };
protected override PlatformScreen CreateScreenFromKey(int key) => new PlatformScreenStub(key);
public Screen ScreenFromPoint(PixelPoint point)
private class PlatformScreenStub : PlatformScreen
{
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);
public PlatformScreenStub(int key) : base(new PlatformHandle((nint) key, nameof(ScreenStub)))
{
Scaling = 1;
Bounds = WorkingArea = new PixelRect(0, 0, 4000, 4000);
IsPrimary = true;
}
}
}

Просмотреть файл

@ -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()))

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -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);
}
return displayIds;
}
public void Dispose ()
protected override AvnScreen CreateScreenFromKey(uint key) => new(key);
protected override void ScreenChanged(AvnScreen screen) => screen.Refresh(_native);
protected override Screen? ScreenFromTopLevelCore(ITopLevelImpl topLevel)
{
var displayId = ((TopLevelImpl)topLevel).Native?.CurrentDisplayId;
return displayId is not null && TryGetScreen(displayId.Value, out var screen) ? screen : null;
}
public void Dispose()
{
_native?.Dispose();
_native = null;
_native = null!;
}
public Screen ScreenFromPoint(PixelPoint point)
private class AvnScreenEvents(ScreenImpl screenImpl) : NativeCallbackBase, IAvnScreenEvents
{
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);
public void OnChanged() => screenImpl.OnChanged();
}
}
}

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -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)]

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -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),
};
protected override PlatformScreen CreateScreenFromKey(int key) => new PlatformScreenStub(key);
public Screen? ScreenFromPoint(PixelPoint point)
private class PlatformScreenStub : PlatformScreen
{
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);
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)

Просмотреть файл

@ -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" />

Просмотреть файл

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

Просмотреть файл

@ -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,

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -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
protected override int GetScreenCount() => GetSystemMetrics(SystemMetric.SM_CMONITORS);
protected override IReadOnlyList<nint> GetAllScreenKeys()
{
private Screen[]? _allScreens;
/// <inheritdoc />
public int ScreenCount
var screens = new List<nint>();
var gcHandle = GCHandle.Alloc(screens);
try
{
get => GetSystemMetrics(SystemMetric.SM_CMONITORS);
PInvoke.EnumDisplayMonitors(default, default(winmdroot.Foundation.RECT*), EnumDisplayMonitorsCallback, (IntPtr)gcHandle);
}
finally
{
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;
}
}
public void InvalidateScreensCache()
{
_allScreens = null;
}
/// <inheritdoc />
public Screen? ScreenFromWindow(IWindowBaseImpl window)
{
var handle = window.Handle?.Handle;
if (handle is null)
{
return null;
}
var monitor = MonitorFromWindow(handle.Value, MONITOR.MONITOR_DEFAULTTONULL);
return FindScreenByHandle(monitor);
}
/// <inheritdoc />
public Screen? ScreenFromPoint(PixelPoint point)
{
var monitor = MonitorFromPoint(new POINT
{
X = point.X,
Y = point.Y
}, MONITOR.MONITOR_DEFAULTTONULL);
return FindScreenByHandle(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);
return FindScreenByHandle(monitor);
}
private Screen? FindScreenByHandle(IntPtr handle)
{
return AllScreens.Cast<WinScreen>().FirstOrDefault(m => m.Handle == handle);
return false;
}
}
protected override WinScreen CreateScreenFromKey(nint key) => new(key);
protected override void ScreenChanged(WinScreen screen) => screen.Refresh();
protected override Screen? ScreenFromTopLevelCore(ITopLevelImpl topLevel)
{
if (topLevel.Handle?.Handle is { } handle)
{
return ScreenFromHwnd(handle);
}
return null;
}
protected override Screen? ScreenFromPointCore(PixelPoint point)
{
var monitor = MonitorFromPoint(new POINT
{
X = point.X,
Y = point.Y
}, UnmanagedMethods.MONITOR.MONITOR_DEFAULTTONULL);
return ScreenFromHMonitor(monitor);
}
protected override Screen? ScreenFromRectCore(PixelRect rect)
{
var monitor = MonitorFromRect(new RECT
{
left = rect.TopLeft.X,
top = rect.TopLeft.Y,
right = rect.TopRight.X,
bottom = rect.BottomRight.Y
}, UnmanagedMethods.MONITOR.MONITOR_DEFAULTTONULL);
return ScreenFromHMonitor(monitor);
}
public WinScreen? ScreenFromHMonitor(IntPtr hmonitor)
{
if (TryGetScreen(hmonitor, out var screen))
return screen;
return null;
}
public WinScreen? ScreenFromHwnd(IntPtr hwnd, MONITOR flags = MONITOR.MONITOR_DEFAULTTONULL)
{
var monitor = MonitorFromWindow(hwnd, flags);
return ScreenFromHMonitor(monitor);
}
}

Просмотреть файл

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

Просмотреть файл

@ -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,

Просмотреть файл

@ -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(() =>
{
private readonly IntPtr _hMonitor;
var shcore = LoadLibrary("shcore.dll");
var method = GetProcAddress(shcore, nameof(GetDpiForMonitor));
return method != IntPtr.Zero;
});
public WinScreen(double scaling, PixelRect bounds, PixelRect workingArea, bool isPrimary, IntPtr hMonitor)
: base(scaling, bounds, workingArea, isPrimary)
internal int Frequency { get; private set; }
public void Refresh()
{
var info = MONITORINFOEX.Create();
PInvoke.GetMonitorInfo(new HMONITOR(hMonitor), (MONITORINFO*)&info);
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);
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
};
}
private string? GetDisplayName(ref MONITORINFOEX monitorinfo)
{
var deviceName = monitorinfo.szDevice;
if (Win32Platform.WindowsVersion >= PlatformConstants.Windows7)
{
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();
}
}
public IntPtr Handle => _hMonitor;
// Fallback to MONITORINFOEX - \\DISPLAY1.
return deviceName.ToString();
}
/// <inheritdoc />
public override int GetHashCode()
private double GetScaling()
{
double dpi;
if (s_hasGetDpiForMonitor.Value)
{
return _hMonitor.GetHashCode();
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);
}
/// <inheritdoc />
public override bool Equals(object? obj)
{
return obj is WinScreen screen && _hMonitor == screen._hMonitor;
}
return dpi / 96d;
}
}

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -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(),

Просмотреть файл

@ -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(),

Просмотреть файл

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

Просмотреть файл

@ -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))
{

Просмотреть файл

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

Просмотреть файл

@ -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(() =>
{