diff --git a/azure-pipelines.yml b/azure-pipelines.yml index a306e22355..3f918d8bc2 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -171,7 +171,7 @@ jobs: displayName: 'Install Workloads' inputs: script: | - dotnet workload install android ios wasm-tools wasm-experimental + dotnet workload install android ios tvos wasm-tools wasm-experimental - task: PowerShell@2 displayName: 'Install Tizen Workload' diff --git a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj index 9826824c3c..dc1309150c 100644 --- a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj +++ b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj @@ -3,6 +3,7 @@ Exe manual net7.0-ios + 13.0 diff --git a/samples/ControlCatalog.iOS/EmbedSample.iOS.cs b/samples/ControlCatalog.iOS/EmbedSample.iOS.cs index ad86d2b578..8fe72d1cff 100644 --- a/samples/ControlCatalog.iOS/EmbedSample.iOS.cs +++ b/samples/ControlCatalog.iOS/EmbedSample.iOS.cs @@ -3,7 +3,6 @@ using Avalonia.Platform; using CoreGraphics; using Foundation; using UIKit; -using WebKit; using Avalonia.iOS; using ControlCatalog.Pages; @@ -13,14 +12,16 @@ public class EmbedSampleIOS : INativeDemoControl { public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func createDefault) { +#if !TVOS if (isSecond) { - var webView = new WKWebView(CGRect.Empty, new WKWebViewConfiguration()); + var webView = new WebKit.WKWebView(CGRect.Empty, new WebKit.WKWebViewConfiguration()); webView.LoadRequest(new NSUrlRequest(new NSUrl("https://www.apple.com/"))); return new UIViewControlHandle(webView); } else +#endif { var button = new UIButton(); var clickCount = 0; diff --git a/samples/ControlCatalog.iOS/Info.plist b/samples/ControlCatalog.iOS/Info.plist index 1dd4416c28..d97c088652 100644 --- a/samples/ControlCatalog.iOS/Info.plist +++ b/samples/ControlCatalog.iOS/Info.plist @@ -18,13 +18,14 @@ 1 2 + 3 + UIRequiredDeviceCapabilities + + arm64 + UILaunchStoryboardName LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - UISupportedInterfaceOrientations UIInterfaceOrientationPortrait diff --git a/src/Avalonia.Base/Compatibility/OperatingSystem.cs b/src/Avalonia.Base/Compatibility/OperatingSystem.cs index 838f7da8b2..ad5fe0246a 100644 --- a/src/Avalonia.Base/Compatibility/OperatingSystem.cs +++ b/src/Avalonia.Base/Compatibility/OperatingSystem.cs @@ -8,18 +8,24 @@ namespace Avalonia.Compatibility #if NET6_0_OR_GREATER public static bool IsWindows() => OperatingSystem.IsWindows(); public static bool IsMacOS() => OperatingSystem.IsMacOS(); + public static bool IsMacCatalyst() => OperatingSystem.IsMacCatalyst(); public static bool IsLinux() => OperatingSystem.IsLinux(); + public static bool IsFreeBSD() => OperatingSystem.IsFreeBSD(); public static bool IsAndroid() => OperatingSystem.IsAndroid(); public static bool IsIOS() => OperatingSystem.IsIOS(); + public static bool IsTvOS() => OperatingSystem.IsTvOS(); public static bool IsBrowser() => OperatingSystem.IsBrowser(); public static bool IsOSPlatform(string platform) => OperatingSystem.IsOSPlatform(platform); #else public static bool IsWindows() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); public static bool IsMacOS() => RuntimeInformation.IsOSPlatform(OSPlatform.OSX); public static bool IsLinux() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); - public static bool IsAndroid() => IsOSPlatform("ANDROID"); - public static bool IsIOS() => IsOSPlatform("IOS"); - public static bool IsBrowser() => IsOSPlatform("BROWSER"); + public static bool IsFreeBSD() => false; + public static bool IsAndroid() => false; + public static bool IsIOS() => false; + public static bool IsMacCatalyst() => false; + public static bool IsTvOS() => false; + public static bool IsBrowser() => false; public static bool IsOSPlatform(string platform) => RuntimeInformation.IsOSPlatform(OSPlatform.Create(platform)); #endif } diff --git a/src/Avalonia.Base/Platform/StandardRuntimePlatform.cs b/src/Avalonia.Base/Platform/StandardRuntimePlatform.cs index 7a5c92c774..b72e10c831 100644 --- a/src/Avalonia.Base/Platform/StandardRuntimePlatform.cs +++ b/src/Avalonia.Base/Platform/StandardRuntimePlatform.cs @@ -1,20 +1,18 @@ -using System; -using System.Threading; using Avalonia.Compatibility; using Avalonia.Metadata; -using Avalonia.Platform.Internal; namespace Avalonia.Platform { [PrivateApi] public class StandardRuntimePlatform : IRuntimePlatform { - private static readonly RuntimePlatformInfo s_info = new() + public virtual RuntimePlatformInfo GetRuntimeInfo() => new() { - IsDesktop = OperatingSystemEx.IsWindows() || OperatingSystemEx.IsMacOS() || OperatingSystemEx.IsLinux(), - IsMobile = OperatingSystemEx.IsAndroid() || OperatingSystemEx.IsIOS() + IsDesktop = OperatingSystemEx.IsWindows() + || OperatingSystemEx.IsMacOS() || OperatingSystemEx.IsMacCatalyst() + || OperatingSystemEx.IsLinux() || OperatingSystemEx.IsFreeBSD(), + IsMobile = OperatingSystemEx.IsAndroid() || (OperatingSystemEx.IsIOS() && !OperatingSystemEx.IsMacCatalyst()), + IsTV = OperatingSystemEx.IsTvOS() }; - - public virtual RuntimePlatformInfo GetRuntimeInfo() => s_info; } } diff --git a/src/Avalonia.Controls/ApplicationLifetimes/IActivatableApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IActivatableApplicationLifetime.cs index fbdfe3aa7d..b04397f1c6 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/IActivatableApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/IActivatableApplicationLifetime.cs @@ -11,13 +11,13 @@ public interface IActivatableApplicationLifetime /// An event that is raised when the application is Activated for various reasons /// as described by the enumeration. /// - event EventHandler Activated; + event EventHandler? Activated; /// /// An event that is raised when the application is Deactivated for various reasons /// as described by the enumeration. /// - event EventHandler Deactivated; + event EventHandler? Deactivated; /// /// Tells the application that it should attempt to leave its background state. diff --git a/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalApi.cs b/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalApi.cs index 3022f92ec5..c1c0a74542 100644 --- a/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalApi.cs +++ b/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalApi.cs @@ -26,7 +26,12 @@ internal unsafe class SkiaMetalApi // Make sure that skia is loaded GC.KeepAlive(new SKPaint()); - var dll = NativeLibraryEx.Load("libSkiaSharp", typeof(SKPaint).Assembly); + // https://github.com/mono/SkiaSharp/blob/25e70a390e2128e5a54d28795365bf9fdaa7161c/binding/SkiaSharp/SkiaApi.cs#L9-L13 + // Note, IsIOS also returns true on MacCatalyst. + var libSkiaSharpPath = OperatingSystemEx.IsIOS() || OperatingSystemEx.IsTvOS() ? + "@rpath/libSkiaSharp.framework/libSkiaSharp" : + "libSkiaSharp"; + var dll = NativeLibraryEx.Load(libSkiaSharpPath, typeof(SKPaint).Assembly); IntPtr address; @@ -75,7 +80,7 @@ internal unsafe class SkiaMetalApi var context = _gr_direct_context_make_metal_with_options(device, queue, pOptions); Marshal.FreeHGlobal(pOptions); if (context == IntPtr.Zero) - throw new ArgumentException(); + throw new InvalidOperationException("Unable to create GRContext from Metal device."); return (GRContext)_contextCtor.Invoke(new object[] { context, true }); } diff --git a/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj index eb4938c13f..b9da0377fd 100644 --- a/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj +++ b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj @@ -1,15 +1,33 @@  - net7.0-ios16.0 - 13.0 + net7.0-ios16.0;net7.0-tvos + 13.0 + 13.0 + + 13.1 true true + + + + $(NoWarn);CA1416 + + + + + + + + + + + diff --git a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs index ecb9e56aa9..e4ec20bbbd 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs @@ -15,7 +15,7 @@ namespace Avalonia.iOS public class AvaloniaAppDelegate : UIResponder, IUIApplicationDelegate, IAvaloniaAppDelegate where TApp : Application, new() { - private EventHandler _onActivated, _onDeactivated; + private EventHandler? _onActivated, _onDeactivated; public AvaloniaAppDelegate() { @@ -37,7 +37,7 @@ namespace Avalonia.iOS protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder; [Export("window")] - public UIWindow Window { get; set; } + public UIWindow? Window { get; set; } [Export("application:didFinishLaunchingWithOptions:")] public bool FinishedLaunching(UIApplication application, NSDictionary launchOptions) @@ -64,7 +64,7 @@ namespace Avalonia.iOS builder.SetupWithLifetime(lifetime); - Window.MakeKeyAndVisible(); + Window!.MakeKeyAndVisible(); return true; } diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs b/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs index 0b0e169e8a..dbdddd2cc5 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs @@ -1,4 +1,3 @@ -#nullable enable using Avalonia.Input.TextInput; using UIKit; diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index a51d3f2b28..26ff0f8e07 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -1,7 +1,6 @@ -#nullable enable - using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Runtime.Versioning; using Avalonia.Controls; using Avalonia.Controls.Embedding; @@ -12,19 +11,20 @@ using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; -using Avalonia.iOS.Storage; using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Rendering.Composition; using CoreAnimation; using Foundation; using ObjCRuntime; -using OpenGLES; using UIKit; using IInsetsManager = Avalonia.Controls.Platform.IInsetsManager; namespace Avalonia.iOS { + /// + /// Root view container for Avalonia content, that can be embedded into iOS visual tree. + /// public partial class AvaloniaView : UIView, ITextInputMethodImpl { internal IInputRoot InputRoot @@ -32,38 +32,74 @@ namespace Avalonia.iOS private readonly TopLevelImpl _topLevelImpl; private readonly EmbeddableControlRoot _topLevel; - private readonly TouchHandler _touches; + private readonly InputHandler _input; private TextInputMethodClient? _client; private IAvaloniaViewController? _controller; private IInputRoot? _inputRoot; + private Metal.MetalRenderTarget? _currentRenderTarget; + private (PixelSize size, double scaling) _latestLayoutProps; public AvaloniaView() { _topLevelImpl = new TopLevelImpl(this); - _touches = new TouchHandler(this, _topLevelImpl); + _input = new InputHandler(this, _topLevelImpl); _topLevel = new EmbeddableControlRoot(_topLevelImpl); _topLevel.Prepare(); _topLevel.StartRendering(); - InitEagl(); - MultipleTouchEnabled = true; + InitLayerSurface(); + + // Remote touch handling + if (OperatingSystem.IsTvOS()) + { + AddGestureRecognizer(new UISwipeGestureRecognizer(_input.Handle) + { + Direction = UISwipeGestureRecognizerDirection.Up + }); + AddGestureRecognizer(new UISwipeGestureRecognizer(_input.Handle) + { + Direction = UISwipeGestureRecognizerDirection.Right + }); + AddGestureRecognizer(new UISwipeGestureRecognizer(_input.Handle) + { + Direction = UISwipeGestureRecognizerDirection.Down + }); + AddGestureRecognizer(new UISwipeGestureRecognizer(_input.Handle) + { + Direction = UISwipeGestureRecognizerDirection.Left + }); + } + else if (OperatingSystem.IsIOS() || OperatingSystem.IsMacCatalyst()) + { +#if !TVOS + MultipleTouchEnabled = true; +#endif + } } - [ObsoletedOSPlatform("ios12.0", "Use 'Metal' instead.")] - [SupportedOSPlatform("ios")] - [UnsupportedOSPlatform("maccatalyst")] - private void InitEagl() + [SuppressMessage("Interoperability", "CA1422:Validate platform compatibility")] + private void InitLayerSurface() { - var l = (CAEAGLLayer)Layer; + var l = Layer; l.ContentsScale = UIScreen.MainScreen.Scale; l.Opaque = true; - l.DrawableProperties = new NSDictionary( - EAGLDrawableProperty.RetainedBacking, false, - EAGLDrawableProperty.ColorFormat, EAGLColorFormat.RGBA8 - ); - _topLevelImpl.Surfaces = new[] { new EaglLayerSurface(l) }; +#if !MACCATALYST + if (l is CAEAGLLayer eaglLayer) + { + eaglLayer.DrawableProperties = new NSDictionary( + OpenGLES.EAGLDrawableProperty.RetainedBacking, false, + OpenGLES.EAGLDrawableProperty.ColorFormat, OpenGLES.EAGLColorFormat.RGBA8 + ); + _topLevelImpl.Surfaces = new[] { new Eagl.EaglLayerSurface(eaglLayer) }; + } + else +#endif + if (l is CAMetalLayer metalLayer) + { + _topLevelImpl.Surfaces = new[] { new Metal.MetalPlatformSurface(metalLayer, this) }; + } } /// @@ -73,6 +109,12 @@ namespace Avalonia.iOS public override bool CanResignFirstResponder => true; /// + [ObsoletedOSPlatform("ios17.0", "Use the 'UITraitChangeObservable' protocol instead.")] + [ObsoletedOSPlatform("maccatalyst17.0", "Use the 'UITraitChangeObservable' protocol instead.")] + [ObsoletedOSPlatform("tvos17.0", "Use the 'UITraitChangeObservable' protocol instead.")] + [SupportedOSPlatform("ios")] + [SupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] public override void TraitCollectionDidChange(UITraitCollection? previousTraitCollection) { base.TraitCollectionDidChange(previousTraitCollection); @@ -101,9 +143,10 @@ namespace Avalonia.iOS { private readonly AvaloniaView _view; private readonly INativeControlHostImpl _nativeControlHost; - private readonly IStorageProvider _storageProvider; internal readonly InsetsManager _insetsManager; - private readonly ClipboardImpl _clipboard; + private readonly IStorageProvider? _storageProvider; + private readonly IClipboard? _clipboard; + private readonly IInputPane? _inputPane; private IDisposable? _paddingInsets; public AvaloniaView View => _view; @@ -112,8 +155,16 @@ namespace Avalonia.iOS { _view = view; _nativeControlHost = new NativeControlHostImpl(view); - _storageProvider = new IOSStorageProvider(view); - _insetsManager = new InsetsManager(view); +#if TVOS + _storageProvider = null; + _clipboard = null; + _inputPane = null; +#else + _storageProvider = new Storage.IOSStorageProvider(view); + _clipboard = new ClipboardImpl(); + _inputPane = UIKitInputPane.Instance; +#endif + _insetsManager = new InsetsManager(); _insetsManager.DisplayEdgeToEdgeChanged += (_, edgeToEdge) => { // iOS doesn't add any paddings/margins to the application by itself. @@ -128,7 +179,6 @@ namespace Avalonia.iOS BindingPriority.Style); // lower priority, so it can be redefined by user } }; - _clipboard = new ClipboardImpl(); } public void Dispose() @@ -136,7 +186,8 @@ namespace Avalonia.iOS // No-op } - public Compositor Compositor => Platform.Compositor; + public Compositor Compositor => Platform.Compositor + ?? throw new InvalidOperationException("iOS backend wasn't initialized. Make sure UseiOS was called."); public void Invalidate(Rect rect) { @@ -185,8 +236,11 @@ namespace Avalonia.iOS public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { +#if !TVOS // TODO adjust status bar depending on full screen mode. - if (OperatingSystem.IsIOSVersionAtLeast(13) && _view._controller is not null) + if ((OperatingSystem.IsIOSVersionAtLeast(13) + || OperatingSystem.IsMacCatalyst()) + && _view._controller is not null) { _view._controller.PreferredStatusBarStyle = themeVariant switch { @@ -195,6 +249,7 @@ namespace Avalonia.iOS _ => UIStatusBarStyle.Default }; } +#endif } public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = @@ -202,11 +257,6 @@ namespace Avalonia.iOS public object? TryGetFeature(Type featureType) { - if (featureType == typeof(IStorageProvider)) - { - return _storageProvider; - } - if (featureType == typeof(ITextInputMethodImpl)) { return _view; @@ -227,9 +277,14 @@ namespace Avalonia.iOS return _clipboard; } + if (featureType == typeof(IStorageProvider)) + { + return _storageProvider; + } + if (featureType == typeof(IInputPane)) { - return UIKitInputPane.Instance; + return _inputPane; } return null; @@ -239,20 +294,77 @@ namespace Avalonia.iOS [Export("layerClass")] public static Class LayerClass() { - return new Class(typeof(CAEAGLLayer)); +#if !MACCATALYST + if (Platform.Graphics is Eagl.EaglPlatformGraphics) + { + return new Class(typeof(CAEAGLLayer)); + } + else +#endif + { + return new Class(typeof(CAMetalLayer)); + } } - public override void TouchesBegan(NSSet touches, UIEvent? evt) => _touches.Handle(touches, evt); + /// + public override void TouchesBegan(NSSet touches, UIEvent? evt) => _input.Handle(touches, evt); + + /// + public override void TouchesMoved(NSSet touches, UIEvent? evt) => _input.Handle(touches, evt); - public override void TouchesMoved(NSSet touches, UIEvent? evt) => _touches.Handle(touches, evt); + /// + public override void TouchesEnded(NSSet touches, UIEvent? evt) => _input.Handle(touches, evt); - public override void TouchesEnded(NSSet touches, UIEvent? evt) => _touches.Handle(touches, evt); + /// + public override void TouchesCancelled(NSSet touches, UIEvent? evt) => _input.Handle(touches, evt); - public override void TouchesCancelled(NSSet touches, UIEvent? evt) => _touches.Handle(touches, evt); + /// + public override void PressesBegan(NSSet presses, UIPressesEvent evt) + { + if (!_input.Handle(presses, evt)) + { + base.PressesBegan(presses, evt); + } + } + /// + public override void PressesChanged(NSSet presses, UIPressesEvent evt) + { + if (!_input.Handle(presses, evt)) + { + base.PressesBegan(presses, evt); + } + } + + /// + public override void PressesEnded(NSSet presses, UIPressesEvent evt) + { + if (!_input.Handle(presses, evt)) + { + base.PressesEnded(presses, evt); + } + } + + /// + public override void PressesCancelled(NSSet presses, UIPressesEvent evt) + { + if (!_input.Handle(presses, evt)) + { + base.PressesCancelled(presses, evt); + } + } + + /// public override void LayoutSubviews() { _topLevelImpl.Resized?.Invoke(_topLevelImpl.ClientSize, WindowResizeReason.Layout); + var scaling = (double)ContentScaleFactor; + _latestLayoutProps = (new PixelSize((int)(Bounds.Width * scaling), (int)(Bounds.Height * scaling)), scaling); + if (_currentRenderTarget is not null) + { + _currentRenderTarget.PendingLayout = _latestLayoutProps; + } + base.LayoutSubviews(); } @@ -261,5 +373,11 @@ namespace Avalonia.iOS get => (Control?)_topLevel.Content; set => _topLevel.Content = value; } + + internal void SetRenderTarget(Metal.MetalRenderTarget target) + { + _currentRenderTarget = target; + _currentRenderTarget.PendingLayout = _latestLayoutProps; + } } } diff --git a/src/iOS/Avalonia.iOS/ClipboardImpl.cs b/src/iOS/Avalonia.iOS/ClipboardImpl.cs index 150f3424e3..8c6cadee0e 100644 --- a/src/iOS/Avalonia.iOS/ClipboardImpl.cs +++ b/src/iOS/Avalonia.iOS/ClipboardImpl.cs @@ -1,19 +1,21 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Avalonia.Input; using Avalonia.Input.Platform; +using Foundation; using UIKit; namespace Avalonia.iOS { internal class ClipboardImpl : IClipboard { - public Task GetTextAsync() + public Task GetTextAsync() { return Task.FromResult(UIPasteboard.General.String); } - public Task SetTextAsync(string text) + public Task SetTextAsync(string? text) { UIPasteboard.General.String = text; return Task.CompletedTask; @@ -21,14 +23,39 @@ namespace Avalonia.iOS public Task ClearAsync() { - UIPasteboard.General.String = ""; + UIPasteboard.General.Items = Array.Empty(); return Task.CompletedTask; } - public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask; + public Task SetDataObjectAsync(IDataObject data) + { + if (data.Contains(DataFormats.Text)) + { + UIPasteboard.General.String = data.GetText(); + } + + return Task.CompletedTask; + } + + public Task GetFormatsAsync() + { + var formats = new List(); + if (UIPasteboard.General.HasStrings) + { + formats.Add(DataFormats.Text); + } - public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); + return Task.FromResult(formats.ToArray()); + } - public Task GetDataAsync(string format) => Task.FromResult(null); + public Task GetDataAsync(string format) + { + if (format == DataFormats.Text) + { + return Task.FromResult(UIPasteboard.General.String); + } + + return Task.FromResult(null); + } } } diff --git a/src/iOS/Avalonia.iOS/DispatcherImpl.cs b/src/iOS/Avalonia.iOS/DispatcherImpl.cs index 9933126133..b39ba1a85a 100644 --- a/src/iOS/Avalonia.iOS/DispatcherImpl.cs +++ b/src/iOS/Avalonia.iOS/DispatcherImpl.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Diagnostics; using System.Runtime.InteropServices; diff --git a/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs b/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs index eb124fd450..676554811e 100644 --- a/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs +++ b/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs @@ -11,7 +11,7 @@ namespace Avalonia.iOS { class DisplayLinkTimer : IRenderTimer { - public event Action Tick; + public event Action? Tick; private Stopwatch _st = Stopwatch.StartNew(); public DisplayLinkTimer() @@ -36,4 +36,4 @@ namespace Avalonia.iOS Tick?.Invoke(_st.Elapsed); } } -} \ No newline at end of file +} diff --git a/src/iOS/Avalonia.iOS/EaglDisplay.cs b/src/iOS/Avalonia.iOS/Eagl/EaglDisplay.cs similarity index 76% rename from src/iOS/Avalonia.iOS/EaglDisplay.cs rename to src/iOS/Avalonia.iOS/Eagl/EaglDisplay.cs index f003c4c6d7..00027ccaf0 100644 --- a/src/iOS/Avalonia.iOS/EaglDisplay.cs +++ b/src/iOS/Avalonia.iOS/Eagl/EaglDisplay.cs @@ -1,15 +1,19 @@ using System; using System.Collections.Generic; using System.Runtime.Versioning; +using Avalonia.Logging; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Reactive; using OpenGLES; -namespace Avalonia.iOS +namespace Avalonia.iOS.Eagl { [ObsoletedOSPlatform("ios12.0", "Use 'Metal' instead.")] + [ObsoletedOSPlatform("tvos12.0", "Use 'Metal' instead.")] + [UnsupportedOSPlatform("maccatalyst")] [SupportedOSPlatform("ios")] + [SupportedOSPlatform("tvos")] class EaglPlatformGraphics : IPlatformGraphics { public IPlatformGraphicsContext GetSharedContext() => Context; @@ -19,7 +23,7 @@ namespace Avalonia.iOS public GlContext Context { get; } public static GlVersion GlVersion { get; } = new(GlProfileType.OpenGLES, 3, 0); - public EaglPlatformGraphics() + private EaglPlatformGraphics() { const string path = "/System/Library/Frameworks/OpenGLES.framework/OpenGLES"; @@ -29,15 +33,30 @@ namespace Avalonia.iOS var iface = new GlInterface(GlVersion, proc => ObjCRuntime.Dlfcn.dlsym(libGl, proc)); Context = new(iface, null); } + + public static EaglPlatformGraphics? TryCreate() + { + try + { + return new EaglPlatformGraphics(); + } + catch(Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log(null, "Unable to initialize EAGL-based rendering: {0}", e); + return null; + } + } } [ObsoletedOSPlatform("ios12.0", "Use 'Metal' instead.")] + [ObsoletedOSPlatform("tvos12.0", "Use 'Metal' instead.")] [SupportedOSPlatform("ios")] + [SupportedOSPlatform("tvos")] class GlContext : IGlContext { - public EAGLContext Context { get; private set; } + public EAGLContext? Context { get; private set; } - public GlContext(GlInterface glInterface, EAGLSharegroup sharegroup) + public GlContext(GlInterface glInterface, EAGLSharegroup? sharegroup) { GlInterface = glInterface; Context = sharegroup == null ? @@ -53,10 +72,10 @@ namespace Avalonia.iOS class ResetContext : IDisposable { - private EAGLContext _old; + private EAGLContext? _old; private bool _disposed; - public ResetContext(EAGLContext old) + public ResetContext(EAGLContext? old) { _old = old; } @@ -87,7 +106,7 @@ namespace Avalonia.iOS { if (Context == null) throw new PlatformGraphicsContextLostException(); - if(EAGLContext.CurrentContext == Context) + if (EAGLContext.CurrentContext == Context) return Disposable.Empty; return MakeCurrent(); } @@ -95,8 +114,10 @@ namespace Avalonia.iOS public bool IsSharedWith(IGlContext context) => context is GlContext other && ReferenceEquals(other.Context?.ShareGroup, Context?.ShareGroup); public bool CanCreateSharedContext => true; - public IGlContext CreateSharedContext(IEnumerable preferredVersions = null) + public IGlContext CreateSharedContext(IEnumerable? preferredVersions = null) { + if (Context == null) + throw new PlatformGraphicsContextLostException(); return new GlContext(GlInterface, Context.ShareGroup); } @@ -119,6 +140,6 @@ namespace Avalonia.iOS } } - public object TryGetFeature(Type featureType) => null; + public object? TryGetFeature(Type featureType) => null; } } diff --git a/src/iOS/Avalonia.iOS/EaglLayerSurface.cs b/src/iOS/Avalonia.iOS/Eagl/EaglLayerSurface.cs similarity index 89% rename from src/iOS/Avalonia.iOS/EaglLayerSurface.cs rename to src/iOS/Avalonia.iOS/Eagl/EaglLayerSurface.cs index 6323bb3acb..cc3ae528a2 100644 --- a/src/iOS/Avalonia.iOS/EaglLayerSurface.cs +++ b/src/iOS/Avalonia.iOS/Eagl/EaglLayerSurface.cs @@ -1,4 +1,3 @@ - using System; using System.Runtime.Versioning; using System.Threading; @@ -6,10 +5,13 @@ using Avalonia.OpenGL; using Avalonia.OpenGL.Surfaces; using CoreAnimation; -namespace Avalonia.iOS +namespace Avalonia.iOS.Eagl { [ObsoletedOSPlatform("ios12.0", "Use 'Metal' instead.")] + [ObsoletedOSPlatform("tvos12.0", "Use 'Metal' instead.")] [SupportedOSPlatform("ios")] + [SupportedOSPlatform("tvos")] + [UnsupportedOSPlatform("maccatalyst")] class EaglLayerSurface : IGlPlatformSurface { private readonly CAEAGLLayer _layer; @@ -77,19 +79,19 @@ namespace Avalonia.iOS static void CheckThread() { - if (Platform.Timer.TimerThread != Thread.CurrentThread) + if (Platform.Timer!.TimerThread != Thread.CurrentThread) throw new InvalidOperationException("Invalid thread, go away"); } public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) { CheckThread(); - var ctx = Platform.GlFeature.Context; + var ctx = ((EaglPlatformGraphics)Platform.Graphics!).Context; if (ctx != context) throw new InvalidOperationException("Platform surface is only usable with tha main context"); using (ctx.MakeCurrent()) { - var fbo = new SizeSynchronizedLayerFbo(ctx.Context, ctx.GlInterface, _layer); + var fbo = new SizeSynchronizedLayerFbo(ctx.Context!, ctx.GlInterface, _layer); if (!fbo.Sync()) throw new InvalidOperationException("Unable to create render target"); return new RenderTarget(ctx, fbo); diff --git a/src/iOS/Avalonia.iOS/LayerFbo.cs b/src/iOS/Avalonia.iOS/Eagl/LayerFbo.cs similarity index 89% rename from src/iOS/Avalonia.iOS/LayerFbo.cs rename to src/iOS/Avalonia.iOS/Eagl/LayerFbo.cs index d971858b6d..dd0a66864f 100644 --- a/src/iOS/Avalonia.iOS/LayerFbo.cs +++ b/src/iOS/Avalonia.iOS/Eagl/LayerFbo.cs @@ -4,10 +4,13 @@ using Avalonia.OpenGL; using CoreAnimation; using OpenGLES; -namespace Avalonia.iOS +namespace Avalonia.iOS.Eagl { [ObsoletedOSPlatform("ios12.0", "Use 'Metal' instead.")] + [ObsoletedOSPlatform("tvos12.0", "Use 'Metal' instead.")] [SupportedOSPlatform("ios")] + [SupportedOSPlatform("tvos")] + [UnsupportedOSPlatform("maccatalyst")] internal class LayerFbo { private readonly EAGLContext _context; @@ -28,7 +31,7 @@ namespace Avalonia.iOS _depthBuffer = depthBuffer; } - public static LayerFbo TryCreate(EAGLContext context, GlInterface gl, CAEAGLLayer layer) + public static LayerFbo? TryCreate(EAGLContext context, GlInterface gl, CAEAGLLayer layer) { if (context != EAGLContext.CurrentContext) return null; @@ -77,7 +80,7 @@ namespace Avalonia.iOS public void Present() { Bind(); - var success = _context.PresentRenderBuffer(GlConsts.GL_RENDERBUFFER); + _context.PresentRenderBuffer(GlConsts.GL_RENDERBUFFER); } public void Dispose() @@ -94,13 +97,16 @@ namespace Avalonia.iOS } [ObsoletedOSPlatform("ios12.0", "Use 'Metal' instead.")] + [ObsoletedOSPlatform("tvos12.0", "Use 'Metal' instead.")] [SupportedOSPlatform("ios")] + [SupportedOSPlatform("tvos")] + [UnsupportedOSPlatform("maccatalyst")] class SizeSynchronizedLayerFbo : IDisposable { private readonly EAGLContext _context; private readonly GlInterface _gl; private readonly CAEAGLLayer _layer; - private LayerFbo _fbo; + private LayerFbo? _fbo; private double _oldLayerWidth, _oldLayerHeight, _oldLayerScale; public SizeSynchronizedLayerFbo(EAGLContext context, GlInterface gl, CAEAGLLayer layer) @@ -138,10 +144,10 @@ namespace Avalonia.iOS { if(!Sync()) throw new InvalidOperationException("Unable to create a render target"); - _fbo.Bind(); + _fbo!.Bind(); } - public void Present() => _fbo.Present(); + public void Present() => _fbo!.Present(); public int Width => _fbo?.Width ?? 0; public int Height => _fbo?.Height ?? 0; diff --git a/src/iOS/Avalonia.iOS/InputHandler.cs b/src/iOS/Avalonia.iOS/InputHandler.cs new file mode 100644 index 0000000000..6f58a78e8f --- /dev/null +++ b/src/iOS/Avalonia.iOS/InputHandler.cs @@ -0,0 +1,432 @@ +using System; +using System.Collections.Generic; +using Avalonia.Collections.Pooled; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Platform; +using Foundation; +using UIKit; + +namespace Avalonia.iOS; + +internal sealed class InputHandler +{ + private static readonly PooledList s_intermediatePointsPooledList = new(ClearMode.Never); + + private readonly AvaloniaView _view; + private readonly ITopLevelImpl _tl; + private readonly TouchDevice _touchDevice = new(); + private readonly MouseDevice _mouseDevice = new(); + private readonly PenDevice _penDevice = new(); + private static long _nextTouchPointId = 1; + private readonly Dictionary _knownTouches = new(); + + public InputHandler(AvaloniaView view, ITopLevelImpl tl) + { + _view = view; + _tl = tl; + } + + private static ulong Ts(UIEvent? evt) => evt is null ? 0 : (ulong)(evt.Timestamp * 1000); + private IInputRoot Root => _view.InputRoot; + + public void Handle(NSSet touches, UIEvent? evt) + { + foreach (UITouch t in touches) + { + if (t.Type == UITouchType.Indirect) + { + // Ignore Indirect input, like remote controller trackpad. + // For Avalonia we handle it independently with gestures. + continue; + } + + if (!_knownTouches.TryGetValue(t, out var id)) + _knownTouches[t] = id = _nextTouchPointId++; + + IInputDevice device = t.Type switch + { + UITouchType.Stylus => _penDevice, +#pragma warning disable CA1416 + UITouchType.IndirectPointer => _mouseDevice, +#pragma warning restore CA1416 + _ => _touchDevice + }; + + var modifiers = RawInputModifiers.None; + if (OperatingSystem.IsIOSVersionAtLeast(13, 4) + || OperatingSystem.IsTvOSVersionAtLeast(13, 4)) + { + modifiers = ConvertModifierKeys(evt?.ModifierFlags); + } + + var ev = new RawTouchEventArgs(device, Ts(evt), Root, + (device, t.Phase) switch + { + (TouchDevice, UITouchPhase.Began) => RawPointerEventType.TouchBegin, + (TouchDevice, UITouchPhase.Ended) => RawPointerEventType.TouchEnd, + (TouchDevice, UITouchPhase.Cancelled) => RawPointerEventType.TouchCancel, + (TouchDevice, _) => RawPointerEventType.TouchUpdate, + + (_, UITouchPhase.Began) => IsRightClick() ? RawPointerEventType.RightButtonDown : RawPointerEventType.LeftButtonDown, + (_, UITouchPhase.Ended or UITouchPhase.Cancelled) => IsRightClick() ? RawPointerEventType.RightButtonUp : RawPointerEventType.RightButtonDown, + (_, _) => RawPointerEventType.Move, + }, ToPointerPoint(t), modifiers, id) + { + IntermediatePoints = evt is {} thisEvent ? new Lazy?>(() => + { + var coalesced = thisEvent.GetCoalescedTouches(t) ?? Array.Empty(); + s_intermediatePointsPooledList.Clear(); + s_intermediatePointsPooledList.Capacity = coalesced.Length - 1; + + // Skip the last one, as it is already processed point. + for (var i = 0; i < coalesced.Length - 1; i++) + { + s_intermediatePointsPooledList.Add(ToPointerPoint(coalesced[i])); + } + + return s_intermediatePointsPooledList; + }) : null + }; + + _tl.Input?.Invoke(ev); + + if (t.Phase is UITouchPhase.Cancelled or UITouchPhase.Ended) + _knownTouches.Remove(t); + + RawPointerPoint ToPointerPoint(UITouch touch) => new() + { + Position = touch.LocationInView(_view).ToAvalonia(), + // in iOS "1.0 represents the force of an average touch", when Avalonia expects 0.5 for "average". + // If MaximumPossibleForce is 0, we ignore it completely. + Pressure = t.MaximumPossibleForce == 0 ? 0.5f : (float)t.Force / 2 + }; + + bool IsRightClick() +#if !TVOS + => OperatingSystem.IsIOSVersionAtLeast(13, 4) && (evt?.ButtonMask.HasFlag(UIEventButtonMask.Secondary) ?? false); +#else + => false; +#endif + } + } + + public bool Handle(NSSet presses, UIPressesEvent? evt) + { + var handled = false; + foreach (UIPress p in presses) + { + PhysicalKey physicalKey; + RawInputModifiers modifier = default; + string? characters = null; + KeyDeviceType keyDeviceType; + + if ((OperatingSystem.IsIOSVersionAtLeast(13, 4) + || OperatingSystem.IsTvOSVersionAtLeast(13, 4)) + && p.Key is { } uiKey + && s_keys.TryGetValue(uiKey.KeyCode, out physicalKey)) + { + modifier = ConvertModifierKeys(uiKey.ModifierFlags); + + keyDeviceType = KeyDeviceType.Keyboard; // very likely + + if (!uiKey.Characters.StartsWith("UIKey")) + characters = uiKey.Characters; + } + else + { + physicalKey = p.Type switch + { + UIPressType.UpArrow => PhysicalKey.ArrowUp, + UIPressType.DownArrow => PhysicalKey.ArrowDown, + UIPressType.LeftArrow => PhysicalKey.ArrowLeft, + UIPressType.RightArrow => PhysicalKey.ArrowRight, + UIPressType.Select => PhysicalKey.Space, + UIPressType.Menu => PhysicalKey.ContextMenu, + UIPressType.PlayPause => PhysicalKey.MediaPlayPause, +#pragma warning disable CA1416 + UIPressType.PageUp => PhysicalKey.PageUp, + UIPressType.PageDown => PhysicalKey.PageDown, +#pragma warning restore CA1416 + _ => PhysicalKey.None + }; + keyDeviceType = KeyDeviceType.Remote; // very likely + } + + var key = physicalKey.ToQwertyKey(); + if (key == Key.None) + continue; + + var ev = new RawKeyEventArgs(KeyboardDevice.Instance!, Ts(evt), Root, + p.Phase switch + { + UIPressPhase.Began => RawKeyEventType.KeyDown, + UIPressPhase.Changed => RawKeyEventType.KeyDown, + UIPressPhase.Stationary => RawKeyEventType.KeyDown, + UIPressPhase.Ended => RawKeyEventType.KeyUp, + _ => RawKeyEventType.KeyUp + }, key, modifier, physicalKey, keyDeviceType, characters); + + _tl.Input?.Invoke(ev); + handled |= ev.Handled; + + if (!ev.Handled && p.Phase == UIPressPhase.Began && !string.IsNullOrEmpty(characters)) + { + var rawTextEvent = new RawTextInputEventArgs( + KeyboardDevice.Instance!, + Ts(evt), + _view.InputRoot, + characters + ); + _tl.Input?.Invoke(rawTextEvent); + handled |= rawTextEvent.Handled; + } + } + + return handled; + } + + public void Handle(UISwipeGestureRecognizer recognizer) + { + var handled = false; + var direction = recognizer.Direction; + var timestamp = 0UL; // todo + + if (OperatingSystem.IsTvOS()) + { + if (direction.HasFlag(UISwipeGestureRecognizerDirection.Up)) + handled = handled || HandleNavigationKey(Key.Up); + if (direction.HasFlag(UISwipeGestureRecognizerDirection.Right)) + handled = handled || HandleNavigationKey(Key.Right); + if (direction.HasFlag(UISwipeGestureRecognizerDirection.Down)) + handled = handled || HandleNavigationKey(Key.Down); + if (direction.HasFlag(UISwipeGestureRecognizerDirection.Left)) + handled = handled || HandleNavigationKey(Key.Left); + } + + if (!handled) + { + // TODO raise RawPointerGestureEventArgs + } + + bool HandleNavigationKey(Key key) + { + // Don't pass PhysicalKey, as physically it's just a touch gesture. + var ev = new RawKeyEventArgs(KeyboardDevice.Instance!, timestamp, Root, + RawKeyEventType.KeyDown, key, RawInputModifiers.None, PhysicalKey.None, KeyDeviceType.Remote, null); + _tl.Input?.Invoke(ev); + var handled = ev.Handled; + + ev.Handled = false; + ev.Type = RawKeyEventType.KeyUp; + _tl.Input?.Invoke(ev); + handled |= ev.Handled; + + return handled; + } + } + + private static RawInputModifiers ConvertModifierKeys(UIKeyModifierFlags? uiModifier) + { + RawInputModifiers modifier = default; + if (uiModifier is { } flags) + { + if (flags.HasFlag(UIKeyModifierFlags.Shift)) + modifier |= RawInputModifiers.Shift; + if (flags.HasFlag(UIKeyModifierFlags.Alternate)) + modifier |= RawInputModifiers.Alt; + if (flags.HasFlag(UIKeyModifierFlags.Control)) + modifier |= RawInputModifiers.Control; + if (flags.HasFlag(UIKeyModifierFlags.Command)) + modifier |= RawInputModifiers.Meta; + } + + return modifier; + } + +#pragma warning disable CA1416 + private static Dictionary s_keys = new() + { + //[UIKeyboardHidUsage.KeyboardErrorRollOver] = PhysicalKey.None, + //[UIKeyboardHidUsage.KeyboardPostFail] = PhysicalKey.None, + //[UIKeyboardHidUsage.KeyboardErrorUndefined] = PhysicalKey.None, + [UIKeyboardHidUsage.KeyboardA] = PhysicalKey.A, + [UIKeyboardHidUsage.KeyboardB] = PhysicalKey.B, + [UIKeyboardHidUsage.KeyboardC] = PhysicalKey.C, + [UIKeyboardHidUsage.KeyboardD] = PhysicalKey.D, + [UIKeyboardHidUsage.KeyboardE] = PhysicalKey.E, + [UIKeyboardHidUsage.KeyboardF] = PhysicalKey.F, + [UIKeyboardHidUsage.KeyboardG] = PhysicalKey.G, + [UIKeyboardHidUsage.KeyboardH] = PhysicalKey.H, + [UIKeyboardHidUsage.KeyboardI] = PhysicalKey.I, + [UIKeyboardHidUsage.KeyboardJ] = PhysicalKey.J, + [UIKeyboardHidUsage.KeyboardK] = PhysicalKey.K, + [UIKeyboardHidUsage.KeyboardL] = PhysicalKey.L, + [UIKeyboardHidUsage.KeyboardM] = PhysicalKey.M, + [UIKeyboardHidUsage.KeyboardN] = PhysicalKey.N, + [UIKeyboardHidUsage.KeyboardO] = PhysicalKey.O, + [UIKeyboardHidUsage.KeyboardP] = PhysicalKey.P, + [UIKeyboardHidUsage.KeyboardQ] = PhysicalKey.Q, + [UIKeyboardHidUsage.KeyboardR] = PhysicalKey.R, + [UIKeyboardHidUsage.KeyboardS] = PhysicalKey.S, + [UIKeyboardHidUsage.KeyboardT] = PhysicalKey.T, + [UIKeyboardHidUsage.KeyboardU] = PhysicalKey.U, + [UIKeyboardHidUsage.KeyboardV] = PhysicalKey.V, + [UIKeyboardHidUsage.KeyboardW] = PhysicalKey.W, + [UIKeyboardHidUsage.KeyboardX] = PhysicalKey.X, + [UIKeyboardHidUsage.KeyboardY] = PhysicalKey.Y, + [UIKeyboardHidUsage.KeyboardZ] = PhysicalKey.Z, + [UIKeyboardHidUsage.Keyboard1] = PhysicalKey.Digit1, + [UIKeyboardHidUsage.Keyboard2] = PhysicalKey.Digit2, + [UIKeyboardHidUsage.Keyboard3] = PhysicalKey.Digit3, + [UIKeyboardHidUsage.Keyboard4] = PhysicalKey.Digit4, + [UIKeyboardHidUsage.Keyboard5] = PhysicalKey.Digit5, + [UIKeyboardHidUsage.Keyboard6] = PhysicalKey.Digit6, + [UIKeyboardHidUsage.Keyboard7] = PhysicalKey.Digit7, + [UIKeyboardHidUsage.Keyboard8] = PhysicalKey.Digit8, + [UIKeyboardHidUsage.Keyboard9] = PhysicalKey.Digit9, + [UIKeyboardHidUsage.Keyboard0] = PhysicalKey.Digit0, + [UIKeyboardHidUsage.KeyboardReturnOrEnter] = PhysicalKey.Enter, + [UIKeyboardHidUsage.KeyboardEscape] = PhysicalKey.Escape, + [UIKeyboardHidUsage.KeyboardDeleteOrBackspace] = PhysicalKey.Delete, + [UIKeyboardHidUsage.KeyboardTab] = PhysicalKey.Tab, + [UIKeyboardHidUsage.KeyboardSpacebar] = PhysicalKey.Space, + [UIKeyboardHidUsage.KeyboardHyphen] = PhysicalKey.NumPadSubtract, + [UIKeyboardHidUsage.KeyboardEqualSign] = PhysicalKey.NumPadEqual, + [UIKeyboardHidUsage.KeyboardOpenBracket] = PhysicalKey.BracketLeft, + [UIKeyboardHidUsage.KeyboardCloseBracket] = PhysicalKey.BracketRight, + [UIKeyboardHidUsage.KeyboardBackslash] = PhysicalKey.Backslash, + // [UIKeyboardHidUsage.KeyboardNonUSPound] = 50, + [UIKeyboardHidUsage.KeyboardSemicolon] = PhysicalKey.Semicolon, + [UIKeyboardHidUsage.KeyboardQuote] = PhysicalKey.Quote, + // [UIKeyboardHidUsage.KeyboardGraveAccentAndTilde] = 53, + [UIKeyboardHidUsage.KeyboardComma] = PhysicalKey.Comma, + [UIKeyboardHidUsage.KeyboardPeriod] = PhysicalKey.Period, + [UIKeyboardHidUsage.KeyboardSlash] = PhysicalKey.Slash, + [UIKeyboardHidUsage.KeyboardCapsLock] = PhysicalKey.CapsLock, + [UIKeyboardHidUsage.KeyboardF1] = PhysicalKey.F1, + [UIKeyboardHidUsage.KeyboardF2] = PhysicalKey.F2, + [UIKeyboardHidUsage.KeyboardF3] = PhysicalKey.F3, + [UIKeyboardHidUsage.KeyboardF4] = PhysicalKey.F4, + [UIKeyboardHidUsage.KeyboardF5] = PhysicalKey.F5, + [UIKeyboardHidUsage.KeyboardF6] = PhysicalKey.F6, + [UIKeyboardHidUsage.KeyboardF7] = PhysicalKey.F7, + [UIKeyboardHidUsage.KeyboardF8] = PhysicalKey.F8, + [UIKeyboardHidUsage.KeyboardF9] = PhysicalKey.F9, + [UIKeyboardHidUsage.KeyboardF10] = PhysicalKey.F10, + [UIKeyboardHidUsage.KeyboardF11] = PhysicalKey.F11, + [UIKeyboardHidUsage.KeyboardF12] = PhysicalKey.F12, + [UIKeyboardHidUsage.KeyboardPrintScreen] = PhysicalKey.PrintScreen, + [UIKeyboardHidUsage.KeyboardScrollLock] = PhysicalKey.ScrollLock, + [UIKeyboardHidUsage.KeyboardPause] = PhysicalKey.Pause, + [UIKeyboardHidUsage.KeyboardInsert] = PhysicalKey.Insert, + [UIKeyboardHidUsage.KeyboardHome] = PhysicalKey.Home, + [UIKeyboardHidUsage.KeyboardPageUp] = PhysicalKey.PageUp, + [UIKeyboardHidUsage.KeyboardDeleteForward] = PhysicalKey.Delete, + [UIKeyboardHidUsage.KeyboardEnd] = PhysicalKey.End, + [UIKeyboardHidUsage.KeyboardPageDown] = PhysicalKey.PageDown, + [UIKeyboardHidUsage.KeyboardRightArrow] = PhysicalKey.ArrowRight, + [UIKeyboardHidUsage.KeyboardLeftArrow] = PhysicalKey.ArrowLeft, + [UIKeyboardHidUsage.KeyboardDownArrow] = PhysicalKey.ArrowDown, + [UIKeyboardHidUsage.KeyboardUpArrow] = PhysicalKey.ArrowUp, + [UIKeyboardHidUsage.KeypadNumLock] = PhysicalKey.NumLock, + [UIKeyboardHidUsage.KeypadSlash] = PhysicalKey.Slash, + [UIKeyboardHidUsage.KeypadAsterisk] = PhysicalKey.NumPadMultiply, + [UIKeyboardHidUsage.KeypadHyphen] = PhysicalKey.NumPadSubtract, + [UIKeyboardHidUsage.KeypadPlus] = PhysicalKey.NumPadAdd, + [UIKeyboardHidUsage.KeypadEnter] = PhysicalKey.Enter, + [UIKeyboardHidUsage.Keypad1] = PhysicalKey.NumPad1, + [UIKeyboardHidUsage.Keypad2] = PhysicalKey.NumPad2, + [UIKeyboardHidUsage.Keypad3] = PhysicalKey.NumPad3, + [UIKeyboardHidUsage.Keypad4] = PhysicalKey.NumPad4, + [UIKeyboardHidUsage.Keypad5] = PhysicalKey.NumPad5, + [UIKeyboardHidUsage.Keypad6] = PhysicalKey.NumPad6, + [UIKeyboardHidUsage.Keypad7] = PhysicalKey.NumPad7, + [UIKeyboardHidUsage.Keypad8] = PhysicalKey.NumPad8, + [UIKeyboardHidUsage.Keypad9] = PhysicalKey.NumPad9, + [UIKeyboardHidUsage.Keypad0] = PhysicalKey.NumPad0, + [UIKeyboardHidUsage.KeypadPeriod] = PhysicalKey.Period, + [UIKeyboardHidUsage.KeyboardNonUSBackslash] = PhysicalKey.IntlBackslash, + //[UIKeyboardHidUsage.KeyboardApplication] = 101, + //[UIKeyboardHidUsage.KeyboardPower] = 102, + //[UIKeyboardHidUsage.KeypadEqualSign] = 103, + [UIKeyboardHidUsage.KeyboardF13] = PhysicalKey.F13, + [UIKeyboardHidUsage.KeyboardF14] = PhysicalKey.F14, + [UIKeyboardHidUsage.KeyboardF15] = PhysicalKey.F15, + [UIKeyboardHidUsage.KeyboardF16] = PhysicalKey.F16, + [UIKeyboardHidUsage.KeyboardF17] = PhysicalKey.F17, + [UIKeyboardHidUsage.KeyboardF18] = PhysicalKey.F18, + [UIKeyboardHidUsage.KeyboardF19] = PhysicalKey.F19, + [UIKeyboardHidUsage.KeyboardF20] = PhysicalKey.F20, + [UIKeyboardHidUsage.KeyboardF21] = PhysicalKey.F21, + [UIKeyboardHidUsage.KeyboardF22] = PhysicalKey.F22, + [UIKeyboardHidUsage.KeyboardF23] = PhysicalKey.F23, + [UIKeyboardHidUsage.KeyboardF24] = PhysicalKey.F24, + //[UIKeyboardHidUsage.KeyboardExecute] = 116, + //[UIKeyboardHidUsage.KeyboardHelp] = 117, + //[UIKeyboardHidUsage.KeyboardMenu] = 118, + [UIKeyboardHidUsage.KeyboardSelect] = PhysicalKey.Space, + //[UIKeyboardHidUsage.KeyboardStop] = 120, + //[UIKeyboardHidUsage.KeyboardAgain] = 121, + //[UIKeyboardHidUsage.KeyboardUndo] = 122, + //[UIKeyboardHidUsage.KeyboardCut] = 123, + //[UIKeyboardHidUsage.KeyboardCopy] = 124, + //[UIKeyboardHidUsage.KeyboardPaste] = 125, + //[UIKeyboardHidUsage.KeyboardFind] = 126, + [UIKeyboardHidUsage.KeyboardMute] = PhysicalKey.AudioVolumeMute, + [UIKeyboardHidUsage.KeyboardVolumeUp] = PhysicalKey.AudioVolumeUp, + [UIKeyboardHidUsage.KeyboardVolumeDown] = PhysicalKey.AudioVolumeDown, + //[UIKeyboardHidUsage.KeyboardLockingCapsLock] = PhysicalKey.CapsLock, + //[UIKeyboardHidUsage.KeyboardLockingNumLock] = PhysicalKey.Space, + //[UIKeyboardHidUsage.KeyboardLockingScrollLock] = 132, + [UIKeyboardHidUsage.KeypadComma] = PhysicalKey.NumPadComma, + //[UIKeyboardHidUsage.KeypadEqualSignAS400] = 134, + //[UIKeyboardHidUsage.KeyboardInternational1] = 135, + //[UIKeyboardHidUsage.KeyboardInternational2] = 136, + //[UIKeyboardHidUsage.KeyboardInternational3] = 137, + //[UIKeyboardHidUsage.KeyboardInternational4] = 138, + //[UIKeyboardHidUsage.KeyboardInternational5] = 139, + //[UIKeyboardHidUsage.KeyboardInternational6] = 140, + //[UIKeyboardHidUsage.KeyboardInternational7] = 141, + //[UIKeyboardHidUsage.KeyboardInternational8] = 142, + //[UIKeyboardHidUsage.KeyboardInternational9] = 143, + //[UIKeyboardHidUsage.KeyboardHangul] = 144, + //[UIKeyboardHidUsage.KeyboardKanaSwitch] = 144, + //[UIKeyboardHidUsage.KeyboardLang1] = 144, + //[UIKeyboardHidUsage.KeyboardAlphanumericSwitch] = 145, + //[UIKeyboardHidUsage.KeyboardHanja] = 145, + //[UIKeyboardHidUsage.KeyboardLang2] = 145, + //[UIKeyboardHidUsage.KeyboardKatakana] = 146, + //[UIKeyboardHidUsage.KeyboardLang3] = 146, + //[UIKeyboardHidUsage.KeyboardHiragana] = 147, + //[UIKeyboardHidUsage.KeyboardLang4] = 147, + //[UIKeyboardHidUsage.KeyboardLang5] = 148, + //[UIKeyboardHidUsage.KeyboardZenkakuHankakuKanji] = 148, + //[UIKeyboardHidUsage.KeyboardLang6] = 149, + //[UIKeyboardHidUsage.KeyboardLang7] = 150, + //[UIKeyboardHidUsage.KeyboardLang8] = 151, + //[UIKeyboardHidUsage.KeyboardLang9] = 152, + //[UIKeyboardHidUsage.KeyboardAlternateErase] = 153, + //[UIKeyboardHidUsage.KeyboardSysReqOrAttention] = 154, + //[UIKeyboardHidUsage.KeyboardCancel] = PhysicalKey.Cancel, + //[UIKeyboardHidUsage.KeyboardClear] = PhysicalKey.NumPadClear, + //[UIKeyboardHidUsage.KeyboardPrior] = PhysicalKey.Prior, + //[UIKeyboardHidUsage.KeyboardReturn] = PhysicalKey.Return, + //[UIKeyboardHidUsage.KeyboardSeparator] = PhysicalKey.Separator, + //[UIKeyboardHidUsage.KeyboardOut] = 160, + //[UIKeyboardHidUsage.KeyboardOper] = 161, + //[UIKeyboardHidUsage.KeyboardClearOrAgain] = 162, + //[UIKeyboardHidUsage.KeyboardCrSelOrProps] = 163, + //[UIKeyboardHidUsage.KeyboardExSel] = 164, + [UIKeyboardHidUsage.KeyboardLeftControl] = PhysicalKey.ControlLeft, + [UIKeyboardHidUsage.KeyboardLeftShift] = PhysicalKey.ShiftLeft, + [UIKeyboardHidUsage.KeyboardLeftAlt] = PhysicalKey.AltLeft, + [UIKeyboardHidUsage.KeyboardLeftGui] = PhysicalKey.MetaLeft, + [UIKeyboardHidUsage.KeyboardRightControl] = PhysicalKey.ControlRight, + [UIKeyboardHidUsage.KeyboardRightShift] = PhysicalKey.ShiftRight, + [UIKeyboardHidUsage.KeyboardRightAlt] = PhysicalKey.AltRight, + [UIKeyboardHidUsage.KeyboardRightGui] = PhysicalKey.MetaRight, + //[UIKeyboardHidUsage.KeyboardReserved] = 65535, + }; +#pragma warning restore CA1416 +} diff --git a/src/iOS/Avalonia.iOS/InsetsManager.cs b/src/iOS/Avalonia.iOS/InsetsManager.cs index 54b769c567..d1439fcd3f 100644 --- a/src/iOS/Avalonia.iOS/InsetsManager.cs +++ b/src/iOS/Avalonia.iOS/InsetsManager.cs @@ -1,22 +1,14 @@ using System; using Avalonia.Controls.Platform; using Avalonia.Media; -using UIKit; namespace Avalonia.iOS; -#nullable enable internal class InsetsManager : IInsetsManager { - private readonly AvaloniaView _view; private IAvaloniaViewController? _controller; private bool _displayEdgeToEdge = true; - public InsetsManager(AvaloniaView view) - { - _view = view; - } - internal void InitWithController(IAvaloniaViewController controller) { _controller = controller; diff --git a/src/iOS/Avalonia.iOS/Metal/MetalDevice.cs b/src/iOS/Avalonia.iOS/Metal/MetalDevice.cs new file mode 100644 index 0000000000..03123a8801 --- /dev/null +++ b/src/iOS/Avalonia.iOS/Metal/MetalDevice.cs @@ -0,0 +1,34 @@ +using System; +using System.Runtime.Versioning; +using Avalonia.Metal; +using Avalonia.Utilities; +using Metal; + +namespace Avalonia.iOS.Metal; + +internal class MetalDevice : IMetalDevice +{ + private readonly DisposableLock _syncRoot = new(); + + public MetalDevice(IMTLDevice device) + { + Device = device; + Queue = device.CreateCommandQueue() + ?? throw new InvalidOperationException("IMTLCommandQueue is not available"); + } + + public IMTLDevice Device { get; } + public IMTLCommandQueue Queue { get; } + IntPtr IMetalDevice.Device => Device.Handle; + IntPtr IMetalDevice.CommandQueue => Queue.Handle; + + public bool IsLost => false; + + public IDisposable EnsureCurrent() => _syncRoot.Lock(); + public object? TryGetFeature(Type featureType) => null; + + public void Dispose() + { + Queue.Dispose(); + } +} diff --git a/src/iOS/Avalonia.iOS/Metal/MetalDrawingSession.cs b/src/iOS/Avalonia.iOS/Metal/MetalDrawingSession.cs new file mode 100644 index 0000000000..0233922025 --- /dev/null +++ b/src/iOS/Avalonia.iOS/Metal/MetalDrawingSession.cs @@ -0,0 +1,34 @@ +using System; +using Avalonia.Metal; +using CoreAnimation; + +namespace Avalonia.iOS.Metal; + +internal class MetalDrawingSession : IMetalPlatformSurfaceRenderingSession +{ + private readonly MetalDevice _device; + private readonly ICAMetalDrawable _drawable; + + public MetalDrawingSession(MetalDevice device, ICAMetalDrawable drawable, PixelSize size, double scaling) + { + _device = device; + _drawable = drawable; + Size = size; + Scaling = scaling; + Texture = _drawable.Texture.Handle; + } + + public void Dispose() + { + var buffer = _device.Queue.CommandBuffer(); + buffer!.PresentDrawable(_drawable); + buffer.Commit(); + } + + public IntPtr Texture { get; } + public PixelSize Size { get; } + + public double Scaling { get; } + + public bool IsYFlipped => false; +} diff --git a/src/iOS/Avalonia.iOS/Metal/MetalPlatformGraphics.cs b/src/iOS/Avalonia.iOS/Metal/MetalPlatformGraphics.cs new file mode 100644 index 0000000000..fb5ffc862c --- /dev/null +++ b/src/iOS/Avalonia.iOS/Metal/MetalPlatformGraphics.cs @@ -0,0 +1,49 @@ +using System; +using System.Runtime.Versioning; +using Avalonia.Platform; +using Metal; +using SkiaSharp; + +namespace Avalonia.iOS.Metal; + +[SupportedOSPlatform("ios")] +[SupportedOSPlatform("macos")] +[SupportedOSPlatform("maccatalyst")] +[SupportedOSPlatform("tvos")] +internal class MetalPlatformGraphics : IPlatformGraphics +{ + private readonly IMTLDevice _defaultDevice; + + private MetalPlatformGraphics(IMTLDevice defaultDevice) + { + _defaultDevice = defaultDevice; + } + + public bool UsesSharedContext => false; + public IPlatformGraphicsContext CreateContext() => new MetalDevice(_defaultDevice); + + public IPlatformGraphicsContext GetSharedContext() => throw new NotSupportedException(); + + public static MetalPlatformGraphics? TryCreate() + { + var device = MTLDevice.SystemDefault; + if (device is null) + { + // Can be null on unsupported OS versions. + return null; + } + +#if !TVOS + using var queue = device.CreateCommandQueue(); + using var context = GRContext.CreateMetal(new GRMtlBackendContext { Device = device, Queue = queue }); + if (context is null) + { + // Can be null on macCatalyst because of older Skia bug. + // Fixed in SkiaSharp 3.0 + return null; + } +#endif + + return new MetalPlatformGraphics(device); + } +} diff --git a/src/iOS/Avalonia.iOS/Metal/MetalPlatformSurface.cs b/src/iOS/Avalonia.iOS/Metal/MetalPlatformSurface.cs new file mode 100644 index 0000000000..285ba95e48 --- /dev/null +++ b/src/iOS/Avalonia.iOS/Metal/MetalPlatformSurface.cs @@ -0,0 +1,25 @@ +using Avalonia.Metal; +using CoreAnimation; + +namespace Avalonia.iOS.Metal; + +internal class MetalPlatformSurface : IMetalPlatformSurface +{ + private readonly CAMetalLayer _layer; + private readonly AvaloniaView _avaloniaView; + + public MetalPlatformSurface(CAMetalLayer layer, AvaloniaView avaloniaView) + { + _layer = layer; + _avaloniaView = avaloniaView; + } + public IMetalPlatformSurfaceRenderTarget CreateMetalRenderTarget(IMetalDevice device) + { + var dev = (MetalDevice)device; + _layer.Device = dev.Device; + + var target = new MetalRenderTarget(_layer, dev); + _avaloniaView.SetRenderTarget(target); + return target; + } +} diff --git a/src/iOS/Avalonia.iOS/Metal/MetalRenderTarget.cs b/src/iOS/Avalonia.iOS/Metal/MetalRenderTarget.cs new file mode 100644 index 0000000000..c8a09527a1 --- /dev/null +++ b/src/iOS/Avalonia.iOS/Metal/MetalRenderTarget.cs @@ -0,0 +1,37 @@ +using Avalonia.Metal; +using Avalonia.Platform; +using CoreAnimation; +using CoreGraphics; + +namespace Avalonia.iOS.Metal; + +internal class MetalRenderTarget : IMetalPlatformSurfaceRenderTarget +{ + private readonly CAMetalLayer _layer; + private readonly MetalDevice _device; + private (PixelSize size, double scaling) _lastLayout; + + public MetalRenderTarget(CAMetalLayer layer, MetalDevice device) + { + _layer = layer; + _device = device; + } + + public (PixelSize size, double scaling) PendingLayout { get; set; } = (new PixelSize(1, 1), 1); + public void Dispose() + { + } + + public IMetalPlatformSurfaceRenderingSession BeginRendering() + { + var (size, scaling) = PendingLayout; + if (_lastLayout != (size, scaling)) + { + _lastLayout = (size, scaling); + _layer.DrawableSize = new CGSize(size.Width, size.Height); + } + + var drawable = _layer.NextDrawable() ?? throw new PlatformGraphicsContextLostException(); + return new MetalDrawingSession(_device, drawable, size, scaling); + } +} diff --git a/src/iOS/Avalonia.iOS/NativeControlHostImpl.cs b/src/iOS/Avalonia.iOS/NativeControlHostImpl.cs index f752936dc8..2c03c13592 100644 --- a/src/iOS/Avalonia.iOS/NativeControlHostImpl.cs +++ b/src/iOS/Avalonia.iOS/NativeControlHostImpl.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System; +using System; using System.Diagnostics.CodeAnalysis; using Avalonia.Controls.Platform; using Avalonia.Platform; diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index bb61861596..1192bb962c 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -1,9 +1,8 @@ using System; - -using Avalonia.Controls; +using System.Collections.Generic; +using System.Linq; using Avalonia.Input; using Avalonia.Input.Platform; -using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Composition; @@ -11,6 +10,33 @@ using Avalonia.Threading; namespace Avalonia { + public enum iOSRenderingMode + { + /// + /// Enables EaGL rendering for iOS and tvOS. Not supported on macCatalyst. + /// + OpenGl = 1, + + /// + /// Enables Metal rendering for all apple targets. Not stable and currently only works on iOS. + /// + Metal + } + + public class iOSPlatformOptions + { + /// + /// Gets or sets Avalonia rendering modes with fallbacks. + /// The first element in the array has the highest priority. + /// The default value is: . + /// + /// Thrown if no values were matched. + public IReadOnlyList RenderingMode { get; set; } = new[] + { + iOSRenderingMode.OpenGl, iOSRenderingMode.Metal + }; + } + public static class IOSApplicationExtensions { public static AppBuilder UseiOS(this AppBuilder builder) @@ -27,18 +53,21 @@ namespace Avalonia.iOS { static class Platform { - public static EaglPlatformGraphics GlFeature; - public static DisplayLinkTimer Timer; - internal static Compositor Compositor { get; private set; } + public static iOSPlatformOptions? Options; + public static IPlatformGraphics? Graphics; + public static DisplayLinkTimer? Timer; + internal static Compositor? Compositor { get; private set; } public static void Register() { - GlFeature ??= new EaglPlatformGraphics(); + Options = AvaloniaLocator.Current.GetService() ?? new iOSPlatformOptions(); + + Graphics = InitializeGraphics(Options); Timer ??= new DisplayLinkTimer(); var keyboard = new KeyboardDevice(); AvaloniaLocator.CurrentMutable - .Bind().ToConstant((IPlatformGraphics) GlFeature) + .Bind().ToConstant(Graphics) .Bind().ToConstant(new CursorFactoryStub()) .Bind().ToConstant(new WindowingPlatformStub()) .Bind().ToSingleton() @@ -48,7 +77,37 @@ namespace Avalonia.iOS .Bind().ToConstant(DispatcherImpl.Instance) .Bind().ToConstant(keyboard); - Compositor = new Compositor(AvaloniaLocator.Current.GetService()); + Compositor = new Compositor(AvaloniaLocator.Current.GetService()); + } + + private static IPlatformGraphics InitializeGraphics(iOSPlatformOptions opts) + { + if (opts.RenderingMode is null || !opts.RenderingMode.Any()) + { + throw new InvalidOperationException($"{nameof(iOSPlatformOptions)}.{nameof(iOSPlatformOptions.RenderingMode)} must not be empty or null"); + } + + foreach (var renderingMode in opts.RenderingMode) + { +#if !MACCATALYST + if (renderingMode == iOSRenderingMode.OpenGl + && !OperatingSystem.IsMacCatalyst() +#pragma warning disable CA1422 + && Eagl.EaglPlatformGraphics.TryCreate() is { } eaglGraphics) +#pragma warning restore CA1422 + { + return eaglGraphics; + } +#endif + + if (renderingMode == iOSRenderingMode.Metal + && Metal.MetalPlatformGraphics.TryCreate() is { } metalGraphics) + { + return metalGraphics; + } + } + + throw new InvalidOperationException($"{nameof(iOSPlatformOptions)}.{nameof(iOSPlatformOptions.RenderingMode)} has a value of \"{string.Join(", ", opts.RenderingMode)}\", but no options were applied."); } } } diff --git a/src/iOS/Avalonia.iOS/PlatformSettings.cs b/src/iOS/Avalonia.iOS/PlatformSettings.cs index 07e366e79e..43cd731701 100644 --- a/src/iOS/Avalonia.iOS/PlatformSettings.cs +++ b/src/iOS/Avalonia.iOS/PlatformSettings.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using Avalonia.Media; using Avalonia.Platform; diff --git a/src/iOS/Avalonia.iOS/SingleViewLifetime.cs b/src/iOS/Avalonia.iOS/SingleViewLifetime.cs index d3924482e4..9627700c7f 100644 --- a/src/iOS/Avalonia.iOS/SingleViewLifetime.cs +++ b/src/iOS/Avalonia.iOS/SingleViewLifetime.cs @@ -12,16 +12,16 @@ internal class SingleViewLifetime : ISingleViewApplicationLifetime, IActivatable avaloniaAppDelegate.Deactivated += (_, args) => Deactivated?.Invoke(this, args); } - public AvaloniaView View; + public AvaloniaView? View; - public Control MainView + public Control? MainView { - get => View.Content; - set => View.Content = value; + get => View!.Content; + set => View!.Content = value; } - public event EventHandler Activated; - public event EventHandler Deactivated; + public event EventHandler? Activated; + public event EventHandler? Deactivated; public bool TryLeaveBackground() => false; public bool TryEnterBackground() => false; } diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs index f6697777be..0dc117b7d6 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs @@ -9,8 +9,6 @@ using Foundation; using UIKit; -#nullable enable - namespace Avalonia.iOS.Storage; internal abstract class IOSStorageItem : IStorageBookmarkItem @@ -56,7 +54,10 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem var properties = attributes is null ? new StorageItemProperties() : - new StorageItemProperties(attributes.Size, (DateTime)attributes.CreationDate, (DateTime)attributes.ModificationDate); + new StorageItemProperties( + attributes.Size, + attributes.CreationDate is { } creationDate ? (DateTime)creationDate : null, + attributes.ModificationDate is { } modificationDate ? (DateTime)modificationDate : null); return Task.FromResult(properties); } @@ -82,7 +83,7 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem } } - public async Task MoveAsync(IStorageFolder destination) + public Task MoveAsync(IStorageFolder destination) { if (destination is not IOSStorageFolder folder) { @@ -99,9 +100,9 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem if (NSFileManager.DefaultManager.Move(Url, newPath, out var error)) { - return isDir + return Task.FromResult(isDir ? new IOSStorageFolder(newPath) - : new IOSStorageFile(newPath); + : new IOSStorageFile(newPath)); } if (error is not null) @@ -109,7 +110,7 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem throw new NSErrorException(error); } - return null; + return Task.FromResult(null); } finally { diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs index 5c53cd2d2d..915cc32980 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs @@ -12,8 +12,6 @@ using UniformTypeIdentifiers; using UTTypeLegacy = MobileCoreServices.UTType; using UTType = UniformTypeIdentifiers.UTType; -#nullable enable - namespace Avalonia.iOS.Storage; internal class IOSStorageProvider : IStorageProvider @@ -68,8 +66,10 @@ internal class IOSStorageProvider : IStorageProvider var allowedUtils = options.FileTypeFilter?.SelectMany(f => f.AppleUniformTypeIdentifiers ?? Array.Empty()) .ToArray() ?? new[] { +#pragma warning disable CA1422 UTTypeLegacy.Content, UTTypeLegacy.Item, +#pragma warning restore CA1422 "public.data" }; documentPicker = new UIDocumentPickerViewController(allowedUtils, UIDocumentPickerMode.Open); @@ -148,7 +148,9 @@ internal class IOSStorageProvider : IStorageProvider { using var documentPicker = OperatingSystem.IsIOSVersionAtLeast(14) ? new UIDocumentPickerViewController(new[] { UTTypes.Folder }, false) : +#pragma warning disable CA1422 new UIDocumentPickerViewController(new string[] { UTTypeLegacy.Folder }, UIDocumentPickerMode.Open); +#pragma warning restore CA1422 if (OperatingSystem.IsIOSVersionAtLeast(13)) { diff --git a/src/iOS/Avalonia.iOS/Stubs.cs b/src/iOS/Avalonia.iOS/Stubs.cs index 6ac89fcab2..a5dbff7797 100644 --- a/src/iOS/Avalonia.iOS/Stubs.cs +++ b/src/iOS/Avalonia.iOS/Stubs.cs @@ -12,6 +12,7 @@ namespace Avalonia.iOS private class CursorImplStub : ICursorImpl { + public CursorImplStub(){} public void Dispose() { } } } @@ -22,7 +23,7 @@ namespace Avalonia.iOS public IWindowImpl CreateEmbeddableWindow() => throw new NotSupportedException(); - public ITrayIconImpl CreateTrayIcon() => null; + public ITrayIconImpl? CreateTrayIcon() => null; } internal class PlatformIconLoaderStub : IPlatformIconLoader diff --git a/src/iOS/Avalonia.iOS/TextInputResponder.Properties.cs b/src/iOS/Avalonia.iOS/TextInputResponder.Properties.cs index 5298be107c..ab0d92e5fa 100644 --- a/src/iOS/Avalonia.iOS/TextInputResponder.Properties.cs +++ b/src/iOS/Avalonia.iOS/TextInputResponder.Properties.cs @@ -1,4 +1,3 @@ -#nullable enable using Avalonia.Input.TextInput; using Foundation; using UIKit; diff --git a/src/iOS/Avalonia.iOS/TextInputResponder.cs b/src/iOS/Avalonia.iOS/TextInputResponder.cs index a5a0c0759f..97b104b918 100644 --- a/src/iOS/Avalonia.iOS/TextInputResponder.cs +++ b/src/iOS/Avalonia.iOS/TextInputResponder.cs @@ -14,8 +14,6 @@ using UIKit; namespace Avalonia.iOS; -#nullable enable - partial class AvaloniaView { @@ -108,7 +106,12 @@ partial class AvaloniaView { get { - var mode = UITextInputMode.CurrentInputMode; + UITextInputMode? mode = null; +#if !TVOS +#pragma warning disable CA1422 + mode = UITextInputMode.CurrentInputMode; +#pragma warning restore CA1422 +#endif // Can be empty see https://developer.apple.com/documentation/uikit/uitextinputmode/1614522-activeinputmodes if (mode is null && UITextInputMode.ActiveInputModes.Length > 0) { diff --git a/src/iOS/Avalonia.iOS/TouchHandler.cs b/src/iOS/Avalonia.iOS/TouchHandler.cs deleted file mode 100644 index 44bf08365f..0000000000 --- a/src/iOS/Avalonia.iOS/TouchHandler.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Collections.Generic; -using Avalonia.Input; -using Avalonia.Input.Raw; -using Avalonia.Platform; -using Foundation; -using UIKit; - -namespace Avalonia.iOS -{ - class TouchHandler - { - private readonly AvaloniaView _view; - private readonly ITopLevelImpl _tl; - public TouchDevice _device = new TouchDevice(); - - public TouchHandler(AvaloniaView view, ITopLevelImpl tl) - { - _view = view; - _tl = tl; - } - - static ulong Ts(UIEvent evt) => (ulong) (evt.Timestamp * 1000); - private IInputRoot Root => _view.InputRoot; - private static long _nextTouchPointId = 1; - private Dictionary _knownTouches = new Dictionary(); - - public void Handle(NSSet touches, UIEvent evt) - { - foreach (UITouch t in touches) - { - var pt = t.LocationInView(_view).ToAvalonia(); - if (!_knownTouches.TryGetValue(t, out var id)) - _knownTouches[t] = id = _nextTouchPointId++; - - var ev = new RawTouchEventArgs(_device, Ts(evt), Root, - t.Phase switch - { - UITouchPhase.Began => RawPointerEventType.TouchBegin, - UITouchPhase.Ended => RawPointerEventType.TouchEnd, - UITouchPhase.Cancelled => RawPointerEventType.TouchCancel, - _ => RawPointerEventType.TouchUpdate - }, pt, RawInputModifiers.None, id); - - _tl.Input?.Invoke(ev); - - if (t.Phase == UITouchPhase.Cancelled || t.Phase == UITouchPhase.Ended) - _knownTouches.Remove(t); - } - } - - } -} diff --git a/src/iOS/Avalonia.iOS/UIKitInputPane.cs b/src/iOS/Avalonia.iOS/UIKitInputPane.cs index 86cae0bf53..8706398104 100644 --- a/src/iOS/Avalonia.iOS/UIKitInputPane.cs +++ b/src/iOS/Avalonia.iOS/UIKitInputPane.cs @@ -1,13 +1,16 @@ using System; using System.Diagnostics; +using System.Runtime.Versioning; using Avalonia.Animation.Easings; using Avalonia.Controls.Platform; using Foundation; using UIKit; -#nullable enable namespace Avalonia.iOS; +[UnsupportedOSPlatform("tvos")] +[SupportedOSPlatform("maccatalyst")] +[SupportedOSPlatform("ios")] internal sealed class UIKitInputPane : IInputPane { public static UIKitInputPane Instance { get; } = new(); @@ -33,7 +36,11 @@ internal sealed class UIKitInputPane : IInputPane private void RaiseEventFromNotification(bool isUp, NSNotification notification) { State = isUp ? InputPaneState.Open : InputPaneState.Closed; - +#if MACCATALYST + OccludedRect = default; + StateChanged?.Invoke(this, new InputPaneStateEventArgs( + State, null, OccludedRect)); +#else var startFrame = UIKeyboard.FrameBeginFromNotification(notification); var endFrame = UIKeyboard.FrameEndFromNotification(notification); var duration = UIKeyboard.AnimationDurationFromNotification(notification); @@ -50,5 +57,6 @@ internal sealed class UIKitInputPane : IInputPane StateChanged?.Invoke(this, new InputPaneStateEventArgs( State, startRect, OccludedRect, TimeSpan.FromSeconds(duration), easing)); +#endif } } diff --git a/src/iOS/Avalonia.iOS/ViewController.cs b/src/iOS/Avalonia.iOS/ViewController.cs index 42a0949a9c..b083cd6c90 100644 --- a/src/iOS/Avalonia.iOS/ViewController.cs +++ b/src/iOS/Avalonia.iOS/ViewController.cs @@ -7,16 +7,20 @@ namespace Avalonia.iOS; [Unstable] public interface IAvaloniaViewController { +#if !TVOS UIStatusBarStyle PreferredStatusBarStyle { get; set; } +#endif bool PrefersStatusBarHidden { get; set; } Thickness SafeAreaPadding { get; } - event EventHandler SafeAreaPaddingChanged; + event EventHandler? SafeAreaPaddingChanged; } /// public class DefaultAvaloniaViewController : UIViewController, IAvaloniaViewController { +#if !TVOS private UIStatusBarStyle? _preferredStatusBarStyle; +#endif private bool? _prefersStatusBarHidden; /// @@ -33,6 +37,7 @@ public class DefaultAvaloniaViewController : UIViewController, IAvaloniaViewCont } } +#if !TVOS /// public override bool PrefersStatusBarHidden() { @@ -55,6 +60,7 @@ public class DefaultAvaloniaViewController : UIViewController, IAvaloniaViewCont SetNeedsStatusBarAppearanceUpdate(); } } +#endif bool IAvaloniaViewController.PrefersStatusBarHidden { @@ -62,7 +68,9 @@ public class DefaultAvaloniaViewController : UIViewController, IAvaloniaViewCont set { _prefersStatusBarHidden = value; +#if !TVOS SetNeedsStatusBarAppearanceUpdate(); +#endif } } @@ -70,5 +78,5 @@ public class DefaultAvaloniaViewController : UIViewController, IAvaloniaViewCont public Thickness SafeAreaPadding { get; private set; } /// - public event EventHandler SafeAreaPaddingChanged; + public event EventHandler? SafeAreaPaddingChanged; }