diff --git a/api/Avalonia.Android.nupkg.xml b/api/Avalonia.Android.nupkg.xml new file mode 100644 index 0000000000..da33e03f2c --- /dev/null +++ b/api/Avalonia.Android.nupkg.xml @@ -0,0 +1,16 @@ + + + + + CP0002 + M:Avalonia.Android.AndroidViewControlHandle.get_HandleDescriptor + baseline/net8.0-android34.0/Avalonia.Android.dll + target/net8.0-android34.0/Avalonia.Android.dll + + + CP0007 + T:Avalonia.Android.AndroidViewControlHandle + baseline/net8.0-android34.0/Avalonia.Android.dll + target/net8.0-android34.0/Avalonia.Android.dll + + \ No newline at end of file diff --git a/api/Avalonia.Browser.nupkg.xml b/api/Avalonia.Browser.nupkg.xml new file mode 100644 index 0000000000..0fb414ed14 --- /dev/null +++ b/api/Avalonia.Browser.nupkg.xml @@ -0,0 +1,22 @@ + + + + + CP0002 + M:Avalonia.Browser.JSObjectControlHandle.get_Handle + baseline/net8.0-browser1.0/Avalonia.Browser.dll + target/net8.0-browser1.0/Avalonia.Browser.dll + + + CP0002 + M:Avalonia.Browser.JSObjectControlHandle.get_HandleDescriptor + baseline/net8.0-browser1.0/Avalonia.Browser.dll + target/net8.0-browser1.0/Avalonia.Browser.dll + + + CP0007 + T:Avalonia.Browser.JSObjectControlHandle + baseline/net8.0-browser1.0/Avalonia.Browser.dll + target/net8.0-browser1.0/Avalonia.Browser.dll + + \ No newline at end of file diff --git a/api/Avalonia.iOS.nupkg.xml b/api/Avalonia.iOS.nupkg.xml new file mode 100644 index 0000000000..5f6e822d81 --- /dev/null +++ b/api/Avalonia.iOS.nupkg.xml @@ -0,0 +1,16 @@ + + + + + CP0002 + M:Avalonia.iOS.UIViewControlHandle.get_HandleDescriptor + baseline/net8.0-tvos17.0/Avalonia.iOS.dll + target/net8.0-tvos17.0/Avalonia.iOS.dll + + + CP0007 + T:Avalonia.iOS.UIViewControlHandle + baseline/net8.0-tvos17.0/Avalonia.iOS.dll + target/net8.0-tvos17.0/Avalonia.iOS.dll + + \ No newline at end of file diff --git a/src/Android/Avalonia.Android/AndroidViewControlHandle.cs b/src/Android/Avalonia.Android/AndroidViewControlHandle.cs index 6d14ea787f..b2ccc6ff6e 100644 --- a/src/Android/Avalonia.Android/AndroidViewControlHandle.cs +++ b/src/Android/Avalonia.Android/AndroidViewControlHandle.cs @@ -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() { diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs index b5a4287fb0..cb03d85fc2 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.cs +++ b/src/Android/Avalonia.Android/AvaloniaView.cs @@ -121,6 +121,7 @@ namespace Avalonia.Android var settings = AvaloniaLocator.Current.GetRequiredService() as AndroidPlatformSettings; settings?.OnViewConfigurationChanged(context); + ((AndroidScreens)_view.TryGetFeature()!).OnChanged(); } } diff --git a/src/Android/Avalonia.Android/Platform/AndroidNativeControlHostImpl.cs b/src/Android/Avalonia.Android/Platform/AndroidNativeControlHostImpl.cs index 7cc1e70cfd..9edd207627 100644 --- a/src/Android/Avalonia.Android/Platform/AndroidNativeControlHostImpl.cs +++ b/src/Android/Avalonia.Android/Platform/AndroidNativeControlHostImpl.cs @@ -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 { diff --git a/src/Android/Avalonia.Android/Platform/AndroidScreens.cs b/src/Android/Avalonia.Android/Platform/AndroidScreens.cs new file mode 100644 index 0000000000..667ec260fa --- /dev/null +++ b/src/Android/Avalonia.Android/Platform/AndroidScreens.cs @@ -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, 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(); + if (_displayService is not null) + { + _listener = new DisplayListener(this); + _displayService.RegisterDisplayListener(_listener, null); + } + } + + protected override IReadOnlyList GetAllScreenKeys() + { + if (_displayService?.GetDisplays() is { } displays) + { + return displays; + } + + if (OperatingSystem.IsAndroidVersionAtLeast(30) && _context.Display is { } defaultDisplay) + { + return [defaultDisplay]; + } + + return Array.Empty(); + } + + 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 + { + public bool Equals(Display? x, Display? y) => x?.DisplayId == y?.DisplayId; + public int GetHashCode(Display obj) => obj.DisplayId; + } +} diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 8b0d80a416..2fb8c5047c 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -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()); + _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; } diff --git a/src/Browser/Avalonia.Browser/BrowserScreens.cs b/src/Browser/Avalonia.Browser/BrowserScreens.cs new file mode 100644 index 0000000000..9f0b22525b --- /dev/null +++ b/src/Browser/Avalonia.Browser/BrowserScreens.cs @@ -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 +{ + private bool _isExtended; + + public BrowserScreens() + { + BrowserScreenHelper.SubscribeOnChanged(BrowserWindowingPlatform.GlobalThis); + BrowserScreenHelper.CheckPermissions(BrowserWindowingPlatform.GlobalThis); + } + + protected override IReadOnlyList 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().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 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; + } + } +} diff --git a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs index 9c15d8d1a5..df010953d9 100644 --- a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs +++ b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs @@ -165,6 +165,11 @@ namespace Avalonia.Browser return AvaloniaLocator.Current.GetService(); } + if (featureType == typeof(IScreenImpl)) + { + return AvaloniaLocator.Current.GetService(); + } + if (featureType == typeof(INativeControlHostImpl)) { return _nativeControlHost; diff --git a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs index 6817e6a6da..4b929660e5 100644 --- a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs @@ -50,4 +50,11 @@ internal static partial class DomHelper (AvaloniaLocator.Current.GetService() as BrowserActivatableLifetime)?.OnVisibilityStateChanged(visibilityState); return Task.CompletedTask; } + + [JSExport] + public static Task ScreensChanged() + { + (AvaloniaLocator.Current.GetService() as BrowserScreens)?.OnChanged(); + return Task.CompletedTask; + } } diff --git a/src/Browser/Avalonia.Browser/Interop/ScreenHelper.cs b/src/Browser/Avalonia.Browser/Interop/ScreenHelper.cs new file mode 100644 index 0000000000..90b1dcfafd --- /dev/null +++ b/src/Browser/Avalonia.Browser/Interop/ScreenHelper.cs @@ -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>] + public static partial Task 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); +} diff --git a/src/Browser/Avalonia.Browser/JSObjectControlHandle.cs b/src/Browser/Avalonia.Browser/JSObjectControlHandle.cs index b0c8cecca6..72b908efe4 100644 --- a/src/Browser/Avalonia.Browser/JSObjectControlHandle.cs +++ b/src/Browser/Avalonia.Browser/JSObjectControlHandle.cs @@ -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(); } diff --git a/src/Browser/Avalonia.Browser/WindowingPlatform.cs b/src/Browser/Avalonia.Browser/WindowingPlatform.cs index bbb1455383..0589b67c0d 100644 --- a/src/Browser/Avalonia.Browser/WindowingPlatform.cs +++ b/src/Browser/Avalonia.Browser/WindowingPlatform.cs @@ -88,6 +88,7 @@ internal class BrowserWindowingPlatform : IWindowingPlatform .Bind().ToConstant(s_keyboard) .Bind().ToSingleton() .Bind().ToSingleton() + .Bind().ToSingleton() .Bind().ToConstant(instance) .Bind().ToSingleton() .Bind().ToSingleton() diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts index c663b4c1b9..1b2393f715 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts @@ -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, diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/screens.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/screens.ts new file mode 100644 index 0000000000..11ee99dc3c --- /dev/null +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/screens.ts @@ -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 { + // 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 { + 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; + } + } +} diff --git a/src/Browser/Avalonia.Browser/webapp/package-lock.json b/src/Browser/Avalonia.Browser/webapp/package-lock.json index 12aa6660d8..51d328418e 100644 --- a/src/Browser/Avalonia.Browser/webapp/package-lock.json +++ b/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", diff --git a/src/Browser/Avalonia.Browser/webapp/package.json b/src/Browser/Avalonia.Browser/webapp/package.json index 065399492e..8df7cb27ed 100644 --- a/src/Browser/Avalonia.Browser/webapp/package.json +++ b/src/Browser/Avalonia.Browser/webapp/package.json @@ -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", diff --git a/src/Tizen/Avalonia.Tizen/NuiScreens.cs b/src/Tizen/Avalonia.Tizen/NuiScreens.cs new file mode 100644 index 0000000000..0c52ffbcf9 --- /dev/null +++ b/src/Tizen/Avalonia.Tizen/NuiScreens.cs @@ -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 +{ + // 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("http://tizen.org/feature/screen.width", out var value) ? value : 0; + + internal static int DisplayHeight => + Information.TryGetValue("http://tizen.org/feature/screen.height", out var value) ? value : 0; + + internal static int DisplayDpi => TizenRuntimePlatform.Info.Value.IsTV ? 72 : + Information.TryGetValue("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 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 + }; + } + } +} diff --git a/src/Tizen/Avalonia.Tizen/TizenRuntimePlatform.cs b/src/Tizen/Avalonia.Tizen/TizenRuntimePlatform.cs index b28db8dff8..f806a03d95 100644 --- a/src/Tizen/Avalonia.Tizen/TizenRuntimePlatform.cs +++ b/src/Tizen/Avalonia.Tizen/TizenRuntimePlatform.cs @@ -22,7 +22,7 @@ internal static class TizenRuntimePlatformServices internal class TizenRuntimePlatform : StandardRuntimePlatform { - private static readonly Lazy Info = new(() => + public static readonly Lazy Info = new(() => { global::Tizen.System.Information.TryGetValue("http://tizen.org/feature/profile", out string profile); diff --git a/src/Tizen/Avalonia.Tizen/TopLevelImpl.cs b/src/Tizen/Avalonia.Tizen/TopLevelImpl.cs index 862d0d7e78..b7c3490622 100644 --- a/src/Tizen/Avalonia.Tizen/TopLevelImpl.cs +++ b/src/Tizen/Avalonia.Tizen/TopLevelImpl.cs @@ -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 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; } diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index ff7098251b..4248987d34 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -296,6 +296,11 @@ namespace Avalonia.iOS return new IOSLauncher(); } + if (featureType == typeof(IScreenImpl)) + { + return (iOSScreens)AvaloniaLocator.Current.GetRequiredService(); + } + return null; } } diff --git a/src/iOS/Avalonia.iOS/NativeControlHostImpl.cs b/src/iOS/Avalonia.iOS/NativeControlHostImpl.cs index 2c03c13592..f5284e4261 100644 --- a/src/iOS/Avalonia.iOS/NativeControlHostImpl.cs +++ b/src/iOS/Avalonia.iOS/NativeControlHostImpl.cs @@ -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() { diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index e2ce25010f..821a322077 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -82,6 +82,7 @@ namespace Avalonia.iOS .Bind().ToConstant(new WindowingPlatformStub()) .Bind().ToSingleton() .Bind().ToConstant(new PlatformIconLoaderStub()) + .Bind().ToSingleton() .Bind().ToSingleton() .Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { diff --git a/src/iOS/Avalonia.iOS/iOSScreens.cs b/src/iOS/Avalonia.iOS/iOSScreens.cs new file mode 100644 index 0000000000..85d298466d --- /dev/null +++ b/src/iOS/Avalonia.iOS/iOSScreens.cs @@ -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 +{ + 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 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; + } +}