зеркало из https://github.com/AvaloniaUI/Avalonia.git
Screens API refactor (#16295)
* Draft new API * Push reusable ScreensBaseImpl implementation * Fix tests and stubs * Update ScreensPage sample to work on mobile + show new APIs * Reimplement Windows ScreensImpl, reuse existing screens in other places of backend, use Microsoft.Windows.CsWin32 for interop * Make X11 project buildable, don't utilize new APIs yet * Reimplement macOS Screens API, differenciate screens by CGDirectDisplayID * Fix build * Adjust breaking changes file (none affect users) * Fix missing macOS Screen.DisplayName * Add more tests + fix screen removal * Add screens integration tests * Use hash set with comparer when removing screens * Make screenimpl safer on macOS as per review * Replace UnmanagedCallersOnly usage with source generated EnumDisplayMonitors * Remove unused dllimport * Only implement GetHashCode and Equals on PlatformScreen subclass, without changing base Screen
This commit is contained in:
Родитель
b865cc41c4
Коммит
05ac6d2f1d
|
@ -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(() =>
|
||||
{
|
||||
|
|
Загрузка…
Ссылка в новой задаче