* Android screen implementation, using Display API (with some fallbacks)

* Browser screen implementation, aditionally add JSObjectPlatformHandle

* iOS screens API implementation with UIScreenPlatformHandle

* Tizen screens implementation, pretty limited but should be better than nothing

* Reduce some API changes to make them more portable in the future

* Update api suppression
This commit is contained in:
Max Katz 2024-08-13 16:22:39 -07:00 коммит произвёл GitHub
Родитель afa60f38de
Коммит 37ba4ce58e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
25 изменённых файлов: 697 добавлений и 96 удалений

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

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Android.AndroidViewControlHandle.get_HandleDescriptor</Target>
<Left>baseline/net8.0-android34.0/Avalonia.Android.dll</Left>
<Right>target/net8.0-android34.0/Avalonia.Android.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0007</DiagnosticId>
<Target>T:Avalonia.Android.AndroidViewControlHandle</Target>
<Left>baseline/net8.0-android34.0/Avalonia.Android.dll</Left>
<Right>target/net8.0-android34.0/Avalonia.Android.dll</Right>
</Suppression>
</Suppressions>

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

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Browser.JSObjectControlHandle.get_Handle</Target>
<Left>baseline/net8.0-browser1.0/Avalonia.Browser.dll</Left>
<Right>target/net8.0-browser1.0/Avalonia.Browser.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Browser.JSObjectControlHandle.get_HandleDescriptor</Target>
<Left>baseline/net8.0-browser1.0/Avalonia.Browser.dll</Left>
<Right>target/net8.0-browser1.0/Avalonia.Browser.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0007</DiagnosticId>
<Target>T:Avalonia.Browser.JSObjectControlHandle</Target>
<Left>baseline/net8.0-browser1.0/Avalonia.Browser.dll</Left>
<Right>target/net8.0-browser1.0/Avalonia.Browser.dll</Right>
</Suppression>
</Suppressions>

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

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.iOS.UIViewControlHandle.get_HandleDescriptor</Target>
<Left>baseline/net8.0-tvos17.0/Avalonia.iOS.dll</Left>
<Right>target/net8.0-tvos17.0/Avalonia.iOS.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0007</DiagnosticId>
<Target>T:Avalonia.iOS.UIViewControlHandle</Target>
<Left>baseline/net8.0-tvos17.0/Avalonia.iOS.dll</Left>
<Right>target/net8.0-tvos17.0/Avalonia.iOS.dll</Right>
</Suppression>
</Suppressions>

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

@ -1,5 +1,5 @@
using System;
using Android.Runtime;
using Android.Views;
using Avalonia.Controls.Platform;
@ -7,20 +7,16 @@ using Avalonia.Platform;
namespace Avalonia.Android
{
public class AndroidViewControlHandle : INativeControlHostDestroyableControlHandle
public class AndroidViewControlHandle : PlatformHandle, INativeControlHostDestroyableControlHandle
{
internal const string AndroidDescriptor = "JavaObjectHandle";
internal static string AndroidViewDescriptor = "android.view.View";
public AndroidViewControlHandle(View view)
public AndroidViewControlHandle(View view) : base(view.Handle, AndroidViewDescriptor)
{
View = view;
}
public View View { get; }
public string HandleDescriptor => AndroidDescriptor;
IntPtr IPlatformHandle.Handle => View.Handle;
public View View { get; private set; }
public void Destroy()
{

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

@ -121,6 +121,7 @@ namespace Avalonia.Android
var settings =
AvaloniaLocator.Current.GetRequiredService<IPlatformSettings>() as AndroidPlatformSettings;
settings?.OnViewConfigurationChanged(context);
((AndroidScreens)_view.TryGetFeature<IScreenImpl>()!).OnChanged();
}
}

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

@ -52,7 +52,7 @@ namespace Avalonia.Android.Platform
};
}
public bool IsCompatibleWith(IPlatformHandle handle) => handle.HandleDescriptor == AndroidViewControlHandle.AndroidDescriptor;
public bool IsCompatibleWith(IPlatformHandle handle) => handle.HandleDescriptor == AndroidViewControlHandle.AndroidViewDescriptor;
private class AndroidNativeControlAttachment : INativeControlHostControlTopLevelAttachment
{

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

@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using Android.Content;
using Android.Hardware.Display;
using Android.Runtime;
using Android.Util;
using Android.Views;
using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Platform;
using AndroidOrientation = global::Android.Content.Res.Orientation;
namespace Avalonia.Android.Platform;
internal class AndroidScreen(Display display) : PlatformScreen(new PlatformHandle(new IntPtr(display.DisplayId), "DisplayId"))
{
public void Refresh(Context context)
{
DisplayName = display.Name;
var naturalOrientation = ScreenOrientation.Portrait;
var rotation = display.Rotation;
if (OperatingSystem.IsAndroidVersionAtLeast(30)
&& display.DisplayId == context.Display?.DisplayId
&& context.Resources?.DisplayMetrics is { } primaryMetrics)
{
IsPrimary = true;
Scaling = primaryMetrics.Density;
Bounds = WorkingArea = new(0, 0, primaryMetrics.WidthPixels, primaryMetrics.HeightPixels);
var orientation = context.Resources.Configuration?.Orientation;
if (orientation == AndroidOrientation.Square)
naturalOrientation = ScreenOrientation.None;
else if (rotation is SurfaceOrientation.Rotation0 or SurfaceOrientation.Rotation180)
naturalOrientation = orientation == AndroidOrientation.Landscape ?
ScreenOrientation.Landscape :
ScreenOrientation.Portrait;
else
naturalOrientation = orientation == AndroidOrientation.Portrait ?
ScreenOrientation.Landscape :
ScreenOrientation.Portrait;
}
else
{
IsPrimary = false;
// These Display methods are deprecated since 31 SDK,
// But Android doesn't have any replacement, except for the primary screen.
#pragma warning disable CS0618 // Type or member is obsolete
#pragma warning disable CA1422 // Validate platform compatibility
#pragma warning disable CA1416 // Validate platform compatibility
var displayMetrics = new DisplayMetrics();
display.GetRealMetrics(displayMetrics);
#pragma warning restore CA1416 // Validate platform compatibility
#pragma warning restore CA1422 // Validate platform compatibility
#pragma warning restore CS0618 // Type or member is obsolete
Scaling = displayMetrics.Density;
Bounds = WorkingArea = new(0, 0, displayMetrics.WidthPixels, displayMetrics.HeightPixels);
}
CurrentOrientation = (display.Rotation, naturalOrientation) switch
{
(_, ScreenOrientation.None) => ScreenOrientation.None,
(SurfaceOrientation.Rotation0, ScreenOrientation.Landscape) => ScreenOrientation.Landscape,
(SurfaceOrientation.Rotation90, ScreenOrientation.Landscape) => ScreenOrientation.Portrait,
(SurfaceOrientation.Rotation180, ScreenOrientation.Landscape) => ScreenOrientation.LandscapeFlipped,
(SurfaceOrientation.Rotation270, ScreenOrientation.Landscape) => ScreenOrientation.PortraitFlipped,
(SurfaceOrientation.Rotation0, _) => ScreenOrientation.Portrait,
(SurfaceOrientation.Rotation90, _) => ScreenOrientation.Landscape,
(SurfaceOrientation.Rotation180, _) => ScreenOrientation.PortraitFlipped,
(SurfaceOrientation.Rotation270, _) => ScreenOrientation.LandscapeFlipped,
_ => ScreenOrientation.Portrait
};
}
}
internal sealed class AndroidScreens : ScreensBase<Display, AndroidScreen>, IDisposable
{
private readonly Context _context;
private readonly DisplayManager? _displayService;
private readonly DisplayListener? _listener;
public AndroidScreens(Context context) : base(new DisplayComparer())
{
_context = context;
_displayService = context.GetSystemService(Context.DisplayService).JavaCast<DisplayManager>();
if (_displayService is not null)
{
_listener = new DisplayListener(this);
_displayService.RegisterDisplayListener(_listener, null);
}
}
protected override IReadOnlyList<Display> GetAllScreenKeys()
{
if (_displayService?.GetDisplays() is { } displays)
{
return displays;
}
if (OperatingSystem.IsAndroidVersionAtLeast(30) && _context.Display is { } defaultDisplay)
{
return [defaultDisplay];
}
return Array.Empty<Display>();
}
protected override AndroidScreen CreateScreenFromKey(Display display) => new(display);
protected override void ScreenChanged(AndroidScreen screen) => screen.Refresh(_context);
protected override Screen? ScreenFromTopLevelCore(ITopLevelImpl topLevel)
{
var display = ((TopLevelImpl)topLevel).View.Display;
return display is not null && TryGetScreen(display, out var screen) ? screen : null;
}
protected override Screen? ScreenFromPointCore(PixelPoint point) => null;
protected override Screen? ScreenFromRectCore(PixelRect rect) => null;
public void Dispose()
{
_displayService?.UnregisterDisplayListener(_listener);
_displayService?.Dispose();
_listener?.Dispose();
}
private class DisplayListener(AndroidScreens screens) : Java.Lang.Object, DisplayManager.IDisplayListener
{
public void OnDisplayAdded(int displayId) => screens.OnChanged();
public void OnDisplayChanged(int displayId) => screens.OnChanged();
public void OnDisplayRemoved(int displayId) => screens.OnChanged();
}
private class DisplayComparer : IEqualityComparer<Display>
{
public bool Equals(Display? x, Display? y) => x?.DisplayId == y?.DisplayId;
public int GetHashCode(Display obj) => obj.DisplayId;
}
}

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

@ -44,6 +44,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
private readonly AndroidInsetsManager? _insetsManager;
private readonly ClipboardImpl _clipboard;
private readonly AndroidLauncher? _launcher;
private readonly AndroidScreens? _screens;
private ViewImpl _view;
private WindowTransparencyLevel _transparencyLevel;
@ -61,6 +62,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_gl = new EglGlPlatformSurface(this);
_framebuffer = new FramebufferManager(this);
_clipboard = new ClipboardImpl(avaloniaView.Context.GetSystemService(Context.ClipboardService).JavaCast<ClipboardManager>());
_screens = new AndroidScreens(avaloniaView.Context);
RenderScaling = _view.Scaling;
@ -399,6 +401,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform
return _launcher;
}
if (featureType == typeof(IScreenImpl))
{
return _screens;
}
return null;
}

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

@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
using Avalonia.Browser.Interop;
using Avalonia.Logging;
using Avalonia.Platform;
using BrowserScreenHelper = Avalonia.Browser.Interop.ScreenHelper;
namespace Avalonia.Browser;
internal sealed class BrowserScreen(JSObject screen) : PlatformScreen(new JSObjectPlatformHandle(screen))
{
internal bool IsCurrent { get; set; }
public void Refresh()
{
IsCurrent = BrowserScreenHelper.IsCurrent(screen);
DisplayName = BrowserScreenHelper.GetDisplayName(screen);
Scaling = BrowserScreenHelper.GetScaling(screen);
IsPrimary = BrowserScreenHelper.IsPrimary(screen);
CurrentOrientation = (ScreenOrientation)BrowserScreenHelper.GetCurrentOrientation(screen);
Bounds = BrowserScreenHelper.GetBounds(screen) is { } boundsArr ?
new PixelRect((int)boundsArr[0], (int)boundsArr[1], (int)boundsArr[2], (int)boundsArr[3]) :
new PixelRect();
WorkingArea = BrowserScreenHelper.GetWorkingArea(screen) is { } workingAreaArr ?
new PixelRect((int)workingAreaArr[0], (int)workingAreaArr[1], (int)workingAreaArr[2],
(int)workingAreaArr[3]) :
new PixelRect();
}
}
internal sealed class BrowserScreens : ScreensBase<JSObject, BrowserScreen>
{
private bool _isExtended;
public BrowserScreens()
{
BrowserScreenHelper.SubscribeOnChanged(BrowserWindowingPlatform.GlobalThis);
BrowserScreenHelper.CheckPermissions(BrowserWindowingPlatform.GlobalThis);
}
protected override IReadOnlyList<JSObject> GetAllScreenKeys() =>
BrowserScreenHelper.GetAllScreens(BrowserWindowingPlatform.GlobalThis);
protected override BrowserScreen CreateScreenFromKey(JSObject key) => new(key);
protected override void ScreenChanged(BrowserScreen screen) => screen.Refresh();
protected override Screen? ScreenFromTopLevelCore(ITopLevelImpl topLevel) =>
AllScreens.OfType<BrowserScreen>().FirstOrDefault(s => s.IsCurrent);
protected override Screen? ScreenFromPointCore(PixelPoint point) =>
_isExtended ? base.ScreenFromPointCore(point) : null;
protected override Screen? ScreenFromRectCore(PixelRect rect) => _isExtended ? base.ScreenFromRectCore(rect) : null;
protected override async Task<bool> RequestScreenDetailsCore()
{
try
{
return _isExtended = await BrowserScreenHelper.RequestDetailedScreens(BrowserWindowingPlatform.GlobalThis);
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Warning, LogArea.BrowserPlatform)?
.Log(this, "Failed to get extended screen details: {Exception}", e);
return false;
}
}
}

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

@ -165,6 +165,11 @@ namespace Avalonia.Browser
return AvaloniaLocator.Current.GetService<ISystemNavigationManagerImpl>();
}
if (featureType == typeof(IScreenImpl))
{
return AvaloniaLocator.Current.GetService<IScreenImpl>();
}
if (featureType == typeof(INativeControlHostImpl))
{
return _nativeControlHost;

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

@ -50,4 +50,11 @@ internal static partial class DomHelper
(AvaloniaLocator.Current.GetService<IActivatableLifetime>() as BrowserActivatableLifetime)?.OnVisibilityStateChanged(visibilityState);
return Task.CompletedTask;
}
[JSExport]
public static Task ScreensChanged()
{
(AvaloniaLocator.Current.GetService<IScreenImpl>() as BrowserScreens)?.OnChanged();
return Task.CompletedTask;
}
}

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

@ -0,0 +1,41 @@
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
namespace Avalonia.Browser.Interop;
internal static partial class ScreenHelper
{
[JSImport("ScreenHelper.subscribeOnChanged", AvaloniaModule.MainModuleName)]
public static partial void SubscribeOnChanged(JSObject globalThis);
[JSImport("ScreenHelper.checkPermissions", AvaloniaModule.MainModuleName)]
public static partial void CheckPermissions(JSObject globalThis);
[JSImport("ScreenHelper.getAllScreens", AvaloniaModule.MainModuleName)]
public static partial JSObject[] GetAllScreens(JSObject globalThis);
[JSImport("ScreenHelper.requestDetailedScreens", AvaloniaModule.MainModuleName)]
[return: JSMarshalAs<JSType.Promise<JSType.Boolean>>]
public static partial Task<bool> RequestDetailedScreens(JSObject globalThis);
[JSImport("ScreenHelper.getDisplayName", AvaloniaModule.MainModuleName)]
public static partial string GetDisplayName(JSObject screen);
[JSImport("ScreenHelper.getScaling", AvaloniaModule.MainModuleName)]
public static partial double GetScaling(JSObject screen);
[JSImport("ScreenHelper.getBounds", AvaloniaModule.MainModuleName)]
public static partial double[] GetBounds(JSObject screen);
[JSImport("ScreenHelper.getWorkingArea", AvaloniaModule.MainModuleName)]
public static partial double[] GetWorkingArea(JSObject screen);
[JSImport("ScreenHelper.isCurrent", AvaloniaModule.MainModuleName)]
public static partial bool IsCurrent(JSObject screen);
[JSImport("ScreenHelper.isPrimary", AvaloniaModule.MainModuleName)]
public static partial bool IsPrimary(JSObject screen);
[JSImport("ScreenHelper.getCurrentOrientation", AvaloniaModule.MainModuleName)]
public static partial int GetCurrentOrientation(JSObject screen);
}

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

@ -1,28 +1,34 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices.JavaScript;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
namespace Avalonia.Browser;
public class JSObjectControlHandle : INativeControlHostDestroyableControlHandle
public class JSObjectPlatformHandle : PlatformHandle
{
internal const string ElementReferenceDescriptor = "JSObject";
public JSObjectControlHandle(JSObject reference)
// GetHashCode returns internal JSHandle.
internal JSObjectPlatformHandle(JSObject reference) : base(reference.GetHashCode(), ElementReferenceDescriptor)
{
Object = reference;
}
public JSObject Object { get; }
}
public IntPtr Handle => throw new NotSupportedException();
public string? HandleDescriptor => ElementReferenceDescriptor;
public class JSObjectControlHandle : JSObjectPlatformHandle, INativeControlHostDestroyableControlHandle
{
public JSObjectControlHandle(JSObject reference) : base(reference)
{
}
public void Destroy()
{
if (Object is JSObject inProcess && !inProcess.IsDisposed)
if (Object is { } inProcess && !inProcess.IsDisposed)
{
inProcess.Dispose();
}

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

@ -88,6 +88,7 @@ internal class BrowserWindowingPlatform : IWindowingPlatform
.Bind<IKeyboardDevice>().ToConstant(s_keyboard)
.Bind<IPlatformSettings>().ToSingleton<BrowserPlatformSettings>()
.Bind<ISystemNavigationManagerImpl>().ToSingleton<BrowserSystemNavigationManagerImpl>()
.Bind<IScreenImpl>().ToSingleton<BrowserScreens>()
.Bind<IWindowingPlatform>().ToConstant(instance)
.Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>()
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()

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

@ -11,6 +11,7 @@ import { WebRenderTargetRegistry } from "./avalonia/rendering/webRenderTargetReg
import { WebRenderTarget } from "./avalonia/rendering/webRenderTarget";
import { SoftwareRenderTarget } from "./avalonia/rendering/softwareRenderTarget";
import { WebGlRenderTarget } from "./avalonia/rendering/webGlRenderTarget";
import { ScreenHelper } from "./avalonia/screens";
async function registerServiceWorker(path: string, scope: string | undefined) {
if ("serviceWorker" in navigator) {
@ -26,6 +27,7 @@ export {
NativeControlHost,
NavigationHelper,
GeneralHelpers,
ScreenHelper,
TimerHelper,
WebRenderTarget,
CanvasSurface,

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

@ -0,0 +1,138 @@
import { JsExports } from "./jsExports";
type SingleScreen = Screen & { window: Window; availLeft: number; availTop: number };
type ScreenDetailedEx = ScreenDetailed & { availLeft: number; availTop: number };
type BrowserScreen = ScreenDetailedEx | SingleScreen;
enum ScreenOrientation {
None,
Landscape = 1,
Portrait = 2,
LandscapeFlipped = 4,
PortraitFlipped = 8
}
export class ScreenHelper {
static detailedScreens?: ScreenDetails;
private static raiseOnChanged() {
JsExports.DomHelper.ScreensChanged();
}
public static async checkPermissions(globalThis: Window): Promise<void> {
// If previous session already granted "window-management" permissions, just re-request details, before they are used.
const { state } = await globalThis.navigator.permissions.query({ name: "window-management" } as any);
if (state === "granted") {
await this.requestDetailedScreens(globalThis);
}
}
public static subscribeOnChanged(globalThis: Window) {
if (this.detailedScreens) {
globalThis.screen.removeEventListener("change", this.raiseOnChanged);
this.detailedScreens.addEventListener("screenschange", this.raiseOnChanged);
// When any screen was added, we re-subscribe on all of them to keep it simpler.
// Just like in C#, it's safer to re-subscribe if handler is the same function - it will trigger it once.
for (const screen of this.detailedScreens.screens) {
screen.addEventListener("change", this.raiseOnChanged);
}
} else {
globalThis.screen.addEventListener("change", this.raiseOnChanged);
}
}
public static getAllScreens(globalThis: Window): BrowserScreen[] {
if (this.detailedScreens) {
return this.detailedScreens.screens as ScreenDetailedEx[];
} else {
const singleScreen = Object.assign(globalThis.screen, { window: globalThis }) as SingleScreen;
return [singleScreen];
}
}
public static async requestDetailedScreens(globalThis: Window): Promise<boolean> {
if (this.detailedScreens) {
return true;
}
if ("getScreenDetails" in globalThis) {
this.detailedScreens = await globalThis.getScreenDetails();
if (this.detailedScreens) {
this.subscribeOnChanged(globalThis);
globalThis.setTimeout(this.raiseOnChanged, 1);
return true;
}
}
return false;
}
static getDisplayName(screen: BrowserScreen) {
return (screen as ScreenDetailed)?.label;
}
static getScaling(screen: BrowserScreen) {
if ("devicePixelRatio" in screen) {
return screen.devicePixelRatio;
}
if ("window" in screen) {
return screen.window.devicePixelRatio;
}
return 1;
}
static getBounds(screen: BrowserScreen): number[] {
const width = screen.width;
const height = screen.height;
if ("left" in screen && "top" in screen) {
return [screen.left, screen.top, width, height];
} else if ("availLeft" in screen && "availTop" in screen) {
// If webapp doesn't have "window-management" perms, "left" and "top" will be undefined.
// To keep getBounds consistent getWorkingArea, while still usable, fallback to availLeft and availTop values.
return [screen.availLeft, screen.availTop, width, height];
}
return [0, 0, width, height];
}
static getWorkingArea(screen: BrowserScreen): number[] {
const width = screen.availWidth;
const height = screen.availHeight;
if ("availLeft" in screen && "availTop" in screen) {
return [screen.availLeft, screen.availTop, width, height];
}
return [0, 0, width, height];
}
static isCurrent(screen: BrowserScreen): boolean {
if (this.detailedScreens) {
return this.detailedScreens.currentScreen === screen;
}
// If detailedScreens were not requested - we have a single screen which always is a current one.
return true;
}
static isPrimary(screen: BrowserScreen): boolean {
if ("isPrimary" in screen) {
return screen.isPrimary;
}
// If detailedScreens were not requested - we have a single screen which always is a current one, and we assume it's a primary one as well.
return true;
}
/* eslint-disable indent */
static getCurrentOrientation(screen: BrowserScreen): ScreenOrientation {
switch (screen.orientation.type) {
case "landscape-primary":
return ScreenOrientation.Landscape;
case "landscape-secondary":
return ScreenOrientation.LandscapeFlipped;
case "portrait-primary":
return ScreenOrientation.Portrait;
case "portrait-secondary":
return ScreenOrientation.PortraitFlipped;
}
}
}

118
src/Browser/Avalonia.Browser/webapp/package-lock.json сгенерированный
Просмотреть файл

@ -11,6 +11,7 @@
"devDependencies": {
"@types/emscripten": "^1.39.6",
"@types/offscreencanvas": "2019.7.1",
"@types/webscreens-window-placement": "^0.1.3",
"@typescript-eslint/eslint-plugin": "^5.38.1",
"esbuild": "^0.15.7",
"eslint": "^8.24.0",
@ -179,6 +180,12 @@
"integrity": "sha512-+HSrJgjBW77ALieQdMJvXhRZUIRN1597L+BKvsyeiIlHHERnqjcuOLyodK3auJ3Y3zRezNKtKAhuQWYJfEgFHQ==",
"dev": true
},
"node_modules/@types/webscreens-window-placement": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@types/webscreens-window-placement/-/webscreens-window-placement-0.1.3.tgz",
"integrity": "sha512-OunHLGJkmAuNlvd7PrRbQy/VleLyxxP3NKwuUo9OS412vO/tzKGwW2K3FqvnM1yebTkCM0W+gszr08m9oDz0lg==",
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.38.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.1.tgz",
@ -493,12 +500,12 @@
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"dependencies": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@ -1612,9 +1619,9 @@
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
@ -2248,18 +2255,6 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/memorystream": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
@ -2380,9 +2375,9 @@
}
},
"node_modules/normalize-package-data/node_modules/semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true,
"bin": {
"semver": "bin/semver"
@ -2498,9 +2493,9 @@
}
},
"node_modules/npm-run-all/node_modules/semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true,
"bin": {
"semver": "bin/semver"
@ -2942,13 +2937,10 @@
}
},
"node_modules/semver": {
"version": "7.3.7",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
@ -3317,12 +3309,6 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
@ -3447,6 +3433,12 @@
"integrity": "sha512-+HSrJgjBW77ALieQdMJvXhRZUIRN1597L+BKvsyeiIlHHERnqjcuOLyodK3auJ3Y3zRezNKtKAhuQWYJfEgFHQ==",
"dev": true
},
"@types/webscreens-window-placement": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@types/webscreens-window-placement/-/webscreens-window-placement-0.1.3.tgz",
"integrity": "sha512-OunHLGJkmAuNlvd7PrRbQy/VleLyxxP3NKwuUo9OS412vO/tzKGwW2K3FqvnM1yebTkCM0W+gszr08m9oDz0lg==",
"dev": true
},
"@typescript-eslint/eslint-plugin": {
"version": "5.38.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.1.tgz",
@ -3636,12 +3628,12 @@
}
},
"braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"requires": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
}
},
"builtins": {
@ -4370,9 +4362,9 @@
}
},
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"requires": {
"to-regex-range": "^5.0.1"
@ -4829,15 +4821,6 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"requires": {
"yallist": "^4.0.0"
}
},
"memorystream": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
@ -4919,9 +4902,9 @@
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true
}
}
@ -5010,9 +4993,9 @@
"dev": true
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true
},
"shebang-command": {
@ -5303,13 +5286,10 @@
}
},
"semver": {
"version": "7.3.7",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
}
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
"dev": true
},
"shebang-command": {
"version": "2.0.0",
@ -5584,12 +5564,6 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

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

@ -10,6 +10,7 @@
"@types/emscripten": "^1.39.6",
"@types/offscreencanvas": "2019.7.1",
"@typescript-eslint/eslint-plugin": "^5.38.1",
"@types/webscreens-window-placement": "^0.1.3",
"esbuild": "^0.15.7",
"eslint": "^8.24.0",
"eslint-config-standard-with-typescript": "^23.0.0",

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

@ -0,0 +1,73 @@
using Avalonia.Platform;
using Tizen.Applications;
using Tizen.Multimedia;
using Tizen.NUI;
using Tizen.System;
namespace Avalonia.Tizen;
internal class NuiScreens : ScreensBase<int, SingleTizenScreen>
{
// See https://github.com/dotnet/maui/blob/8.0.70/src/Essentials/src/DeviceDisplay/DeviceDisplay.tizen.cs
internal const float BaseLogicalDpi = 160.0f;
internal static DeviceOrientation LastDeviceOrientation { get; private set; }
internal static int DisplayWidth =>
Information.TryGetValue<int>("http://tizen.org/feature/screen.width", out var value) ? value : 0;
internal static int DisplayHeight =>
Information.TryGetValue<int>("http://tizen.org/feature/screen.height", out var value) ? value : 0;
internal static int DisplayDpi => TizenRuntimePlatform.Info.Value.IsTV ? 72 :
Information.TryGetValue<int>("http://tizen.org/feature/screen.dpi", out var value) ? value : 72;
public NuiScreens()
{
((CoreApplication)global::Tizen.Applications.Application.Current).DeviceOrientationChanged += (sender, args) =>
{
LastDeviceOrientation = args.DeviceOrientation;
OnChanged();
};
}
protected override int GetScreenCount() => 1;
protected override IReadOnlyList<int> GetAllScreenKeys() => [1];
protected override SingleTizenScreen CreateScreenFromKey(int key)
{
var screen = new SingleTizenScreen(key);
screen.Refresh();
return screen;
}
protected override void ScreenChanged(SingleTizenScreen screen) => screen.Refresh();
}
internal class SingleTizenScreen(int index) : PlatformScreen(new PlatformHandle(new IntPtr(index), nameof(SingleTizenScreen)))
{
public void Refresh()
{
IsPrimary = index == 1;
if (IsPrimary)
{
Bounds = WorkingArea = new PixelRect(0, 0, NuiScreens.DisplayWidth, NuiScreens.DisplayHeight);
Scaling = NuiScreens.DisplayDpi / NuiScreens.BaseLogicalDpi;
var isNaturalLandscape = Bounds.Width > Bounds.Height;
CurrentOrientation = (isNaturalLandscape, NuiScreens.LastDeviceOrientation) switch
{
(true, DeviceOrientation.Orientation_0) => ScreenOrientation.Landscape,
(true, DeviceOrientation.Orientation_90) => ScreenOrientation.Portrait,
(true, DeviceOrientation.Orientation_180) => ScreenOrientation.LandscapeFlipped,
(true, DeviceOrientation.Orientation_270) => ScreenOrientation.PortraitFlipped,
(false, DeviceOrientation.Orientation_0) => ScreenOrientation.Portrait,
(false, DeviceOrientation.Orientation_90) => ScreenOrientation.Landscape,
(false, DeviceOrientation.Orientation_180) => ScreenOrientation.PortraitFlipped,
(false, DeviceOrientation.Orientation_270) => ScreenOrientation.LandscapeFlipped,
_ => ScreenOrientation.None
};
}
}
}

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

@ -22,7 +22,7 @@ internal static class TizenRuntimePlatformServices
internal class TizenRuntimePlatform : StandardRuntimePlatform
{
private static readonly Lazy<RuntimePlatformInfo> Info = new(() =>
public static readonly Lazy<RuntimePlatformInfo> Info = new(() =>
{
global::Tizen.System.Information.TryGetValue("http://tizen.org/feature/profile", out string profile);

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

@ -16,6 +16,7 @@ internal class TopLevelImpl : ITopLevelImpl
private readonly ITizenView _view;
private readonly NuiClipboardImpl _clipboard;
private readonly IStorageProvider _storageProvider;
private readonly NuiScreens _screen;
public TopLevelImpl(ITizenView view, IEnumerable<object> surfaces)
{
@ -24,6 +25,7 @@ internal class TopLevelImpl : ITopLevelImpl
_storageProvider = new TizenStorageProvider();
_clipboard = new NuiClipboardImpl();
_screen = new NuiScreens();
}
public double DesktopScaling => RenderScaling;
@ -112,6 +114,11 @@ internal class TopLevelImpl : ITopLevelImpl
return new TizenLauncher();
}
if (featureType == typeof(IScreenImpl))
{
return _screen;
}
return null;
}

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

@ -296,6 +296,11 @@ namespace Avalonia.iOS
return new IOSLauncher();
}
if (featureType == typeof(IScreenImpl))
{
return (iOSScreens)AvaloniaLocator.Current.GetRequiredService<IScreenImpl>();
}
return null;
}
}

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

@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
using CoreGraphics;
using Foundation;
using ObjCRuntime;
using UIKit;
@ -134,21 +135,16 @@ namespace Avalonia.iOS
}
}
public class UIViewControlHandle : INativeControlHostDestroyableControlHandle
public class UIViewControlHandle : PlatformHandle, INativeControlHostDestroyableControlHandle
{
internal const string UIViewDescriptor = "UIView";
public UIViewControlHandle(UIView view)
public UIViewControlHandle(UIView view) : base(view.Handle.Handle, UIViewDescriptor)
{
View = view;
}
public UIView View { get; }
public string HandleDescriptor => UIViewDescriptor;
IntPtr IPlatformHandle.Handle => View.Handle.Handle;
public void Destroy()
{

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

@ -82,6 +82,7 @@ namespace Avalonia.iOS
.Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformStub())
.Bind<IPlatformSettings>().ToSingleton<PlatformSettings>()
.Bind<IPlatformIconLoader>().ToConstant(new PlatformIconLoaderStub())
.Bind<IScreenImpl>().ToSingleton<iOSScreens>()
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
.Bind<KeyGestureFormatInfo>().ToConstant(new KeyGestureFormatInfo(new Dictionary<Key, string>()
{

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

@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using Avalonia.Platform;
using Foundation;
using ObjCRuntime;
using UIKit;
namespace Avalonia.iOS;
internal class iOSScreen(UIScreen screen) : PlatformScreen(new PlatformHandle(screen.Handle.Handle, nameof(UIScreen)))
{
public void Refresh()
{
IsPrimary = screen.Equals(UIScreen.MainScreen);
Scaling = screen.NativeScale;
DisplayName = IsPrimary ? nameof(UIScreen.MainScreen) : null;
var nativeBounds = screen.NativeBounds;
var scaledBounds = screen.Bounds;
#if !TVOS && !MACCATALYST
var uiOrientation = IsPrimary ?
UIDevice.CurrentDevice.Orientation :
UIDeviceOrientation.LandscapeLeft;
CurrentOrientation = uiOrientation switch
{
UIDeviceOrientation.Portrait => ScreenOrientation.Portrait,
UIDeviceOrientation.PortraitUpsideDown => ScreenOrientation.PortraitFlipped,
UIDeviceOrientation.LandscapeLeft => ScreenOrientation.Landscape,
UIDeviceOrientation.LandscapeRight => ScreenOrientation.LandscapeFlipped,
UIDeviceOrientation.FaceUp or UIDeviceOrientation.FaceDown =>
nativeBounds.Width > nativeBounds.Height ? ScreenOrientation.Landscape : ScreenOrientation.Portrait,
_ => ScreenOrientation.None
};
#endif
// "The bounding rectangle of the physical screen, measured in pixels" - so just cast it to int.
// "This value does not change as the device rotates." - we need to rotate it to match other platforms.
// As a reference, scaled bounds are always rotated.
WorkingArea = Bounds = scaledBounds.Width > scaledBounds.Height && nativeBounds.Width < nativeBounds.Height ?
new PixelRect((int)nativeBounds.X, (int)nativeBounds.Y, (int)nativeBounds.Height, (int)nativeBounds.Width) :
new PixelRect((int)nativeBounds.X, (int)nativeBounds.Y, (int)nativeBounds.Width, (int)nativeBounds.Height);
}
}
internal class iOSScreens : ScreensBase<UIScreen, iOSScreen>
{
public iOSScreens()
{
UIScreen.Notifications.ObserveDidConnect(OnScreenChanged);
UIScreen.Notifications.ObserveDidDisconnect(OnScreenChanged);
UIScreen.Notifications.ObserveModeDidChange(OnScreenChanged);
#if !TVOS
UIDevice.Notifications.ObserveOrientationDidChange(OnScreenChanged);
#endif
void OnScreenChanged(object? sender, NSNotificationEventArgs e) => OnChanged();
}
protected override IReadOnlyList<UIScreen> GetAllScreenKeys() => UIScreen.Screens;
protected override iOSScreen CreateScreenFromKey(UIScreen key) => new(key);
protected override void ScreenChanged(iOSScreen screen) => screen.Refresh();
protected override Screen? ScreenFromPointCore(PixelPoint point) => null;
protected override Screen? ScreenFromRectCore(PixelRect rect) => null;
protected override Screen? ScreenFromTopLevelCore(ITopLevelImpl topLevel)
{
var uiScreen = (topLevel as AvaloniaView.TopLevelImpl)?.View.Window.Screen;
return uiScreen is not null && TryGetScreen(uiScreen, out var screen) ? screen : null;
}
}