From 7520e2dcb1afcb04699a6223f39e1624637fc92a Mon Sep 17 00:00:00 2001 From: Tom Edwards <109803929+TomEdwardsEnscape@users.noreply.github.com> Date: Thu, 11 Jan 2024 23:52:34 +0100 Subject: [PATCH 01/13] Fix exception when `SelectedValueBinding` evaluates to null (#14171) * Add an item with null values to the SelectingItemsControl test data * Fixed NullReferenceException when SelectedValueBinding evulates to null Fixed SelectedValue bindings being overwritten when a new item is selected --- .../Primitives/SelectingItemsControl.cs | 12 +++--- ...electingItemsControlTests_SelectedValue.cs | 39 ++++++++++--------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index e20e928f8d..6c58044c79 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -690,7 +690,7 @@ namespace Avalonia.Controls.Primitives if (value is null) { // Clearing SelectedValueBinding makes the SelectedValue the item itself - SelectedValue = SelectedItem; + SetCurrentValue(SelectedValueProperty, SelectedItem); return; } @@ -710,7 +710,7 @@ namespace Avalonia.Controls.Primitives } // Re-evaluate SelectedValue with the new binding - SelectedValue = _bindingHelper.Evaluate(selectedItem); + SetCurrentValue(SelectedValueProperty, _bindingHelper.Evaluate(selectedItem)); } finally { @@ -1080,7 +1080,7 @@ namespace Avalonia.Controls.Primitives { var itemValue = _bindingHelper.Evaluate(item); - if (itemValue.Equals(value)) + if (Equals(itemValue, value)) { return item; } @@ -1103,7 +1103,7 @@ namespace Avalonia.Controls.Primitives try { _isSelectionChangeActive = true; - SelectedValue = item; + SetCurrentValue(SelectedValueProperty, item); } finally { @@ -1117,7 +1117,7 @@ namespace Avalonia.Controls.Primitives try { _isSelectionChangeActive = true; - SelectedValue = _bindingHelper.Evaluate(item); + SetCurrentValue(SelectedValueProperty, _bindingHelper.Evaluate(item)); } finally { @@ -1381,7 +1381,7 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty ValueProperty = AvaloniaProperty.Register("Value"); - public object Evaluate(object? dataContext) + public object? Evaluate(object? dataContext) { // Only update the DataContext if necessary if (!Equals(dataContext, DataContext)) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_SelectedValue.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_SelectedValue.cs index 9ea12c62fb..b0e430cbe0 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_SelectedValue.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_SelectedValue.cs @@ -26,9 +26,9 @@ namespace Avalonia.Controls.UnitTests.Primitives Template = Template() }; - sic.SelectedItem = items[0]; + sic.SelectedItem = items[1]; - Assert.Equal(items[0].Name, sic.SelectedValue); + Assert.Equal(items[1].Name, sic.SelectedValue); } [Fact] @@ -42,9 +42,9 @@ namespace Avalonia.Controls.UnitTests.Primitives Template = Template() }; - sic.SelectedIndex = 0; + sic.SelectedIndex = 1; - Assert.Equal(items[0].Name, sic.SelectedValue); + Assert.Equal(items[1].Name, sic.SelectedValue); } [Fact] @@ -60,14 +60,14 @@ namespace Avalonia.Controls.UnitTests.Primitives sic.SelectedItems = new List { - items[1], - items[3], - items[4] + items[2], + items[4], + items[5] }; // When interacting, SelectedItem is the first item in the SelectedItems collection // But when set here, it's the last - Assert.Equal(items[4].Name, sic.SelectedValue); + Assert.Equal(items[5].Name, sic.SelectedValue); } [Fact] @@ -85,9 +85,9 @@ namespace Avalonia.Controls.UnitTests.Primitives Prepare(sic); - sic.SelectedValue = items[1].Name; + sic.SelectedValue = items[2].Name; - Assert.Equal(1, sic.SelectedIndex); + Assert.Equal(2, sic.SelectedIndex); } } @@ -108,7 +108,7 @@ namespace Avalonia.Controls.UnitTests.Primitives sic.SelectedValue = "Item2"; - Assert.Equal(items[1], sic.SelectedItem); + Assert.Equal(items[2], sic.SelectedItem); } } @@ -130,7 +130,7 @@ namespace Avalonia.Controls.UnitTests.Primitives sic.SelectedValueBinding = new Binding("AltProperty"); // Ensure SelectedItem didn't change - Assert.Equal(items[1], sic.SelectedItem); + Assert.Equal(items[2], sic.SelectedItem); Assert.Equal("Alt2", sic.SelectedValue); @@ -147,9 +147,9 @@ namespace Avalonia.Controls.UnitTests.Primitives Template = Template() }; - sic.SelectedIndex = 0; + sic.SelectedIndex = 1; - Assert.Equal(items[0], sic.SelectedValue); + Assert.Equal(items[1], sic.SelectedValue); } [Fact] @@ -167,7 +167,7 @@ namespace Avalonia.Controls.UnitTests.Primitives sic.BeginInit(); sic.EndInit(); - Assert.Equal(items[1].Name, sic.SelectedValue); + Assert.Equal(items[2].Name, sic.SelectedValue); } [Fact] @@ -186,7 +186,7 @@ namespace Avalonia.Controls.UnitTests.Primitives sic.SelectedValue = "Item1"; sic.EndInit(); - Assert.Equal(items[0].Name, sic.SelectedValue); + Assert.Equal(items[1].Name, sic.SelectedValue); } [Fact] @@ -234,7 +234,7 @@ namespace Avalonia.Controls.UnitTests.Primitives var called = false; sic.SelectionChanged += (s, e) => { - Assert.Same(items[1], e.AddedItems.Cast().Single()); + Assert.Same(items[2], e.AddedItems.Cast().Single()); Assert.Empty(e.RemovedItems); called = true; }; @@ -259,7 +259,7 @@ namespace Avalonia.Controls.UnitTests.Primitives var called = false; sic.SelectionChanged += (s, e) => { - Assert.Same(items[1], e.RemovedItems.Cast().Single()); + Assert.Same(items[2], e.RemovedItems.Cast().Single()); Assert.Empty(e.AddedItems); called = true; }; @@ -276,7 +276,7 @@ namespace Avalonia.Controls.UnitTests.Primitives var sic = new SelectingItemsControl { ItemsSource = items, - SelectedIndex = 0, + SelectedIndex = 1, SelectedValueBinding = new Binding("Name"), Template = Template() }; @@ -333,6 +333,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { return new List { + new TestClass(null, null), new TestClass("Item1", "Alt1"), new TestClass("Item2", "Alt2"), new TestClass("Item3", "Alt3"), From d63f0574e755a19ff5cf41436461e569ab99e1ad Mon Sep 17 00:00:00 2001 From: Montague Moran Date: Fri, 12 Jan 2024 04:23:56 +0000 Subject: [PATCH 02/13] Update SkiaSharp to 2.88.7 to fix a memory exception (#14179) --- build/SkiaSharp.props | 6 +++--- src/Tizen/Avalonia.Tizen/Avalonia.Tizen.csproj | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index 5534793662..adca6c4462 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,7 +1,7 @@  - - - + + + diff --git a/src/Tizen/Avalonia.Tizen/Avalonia.Tizen.csproj b/src/Tizen/Avalonia.Tizen/Avalonia.Tizen.csproj index ec843a2875..3e941a4500 100644 --- a/src/Tizen/Avalonia.Tizen/Avalonia.Tizen.csproj +++ b/src/Tizen/Avalonia.Tizen/Avalonia.Tizen.csproj @@ -8,7 +8,7 @@ - + From 8ea685eae40a9dd581bfd7ecf19f2f5604d21c17 Mon Sep 17 00:00:00 2001 From: Ge Date: Fri, 12 Jan 2024 13:55:39 +0800 Subject: [PATCH 03/13] Add InheritDataTypeFromItems for AutoCompleteBox.ValueMemberBinding (#14162) --- .../AutoCompleteBox/AutoCompleteBox.Properties.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs index 8a1d38f88a..bcee7f36e3 100644 --- a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs +++ b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Metadata; namespace Avalonia.Controls { @@ -286,6 +287,7 @@ namespace Avalonia.Controls /// The object used /// when binding to a collection property. [AssignBinding] + [InheritDataTypeFromItems(nameof(ItemsSource))] public IBinding? ValueMemberBinding { get => _valueBindingEvaluator?.ValueBinding; From 97084260d3c86e434a01d6e4f78cf0d2a0489de3 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sat, 13 Jan 2024 10:16:19 +0800 Subject: [PATCH 04/13] Port DragMove logic from WPF (#14186) * Port DragMove logic from WPF to fix missing PointerReleased event on drag out. * remove WindowState.Normal check --- .../Avalonia.Win32/Interop/UnmanagedMethods.cs | 7 ++++++- src/Windows/Avalonia.Win32/WindowImpl.cs | 18 ++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index f31d5b2fc3..872e5e92d8 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1183,7 +1183,12 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll", EntryPoint = "DefWindowProcW")] public static extern IntPtr DefWindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); - + + public const int SC_MOUSEMOVE = 0xf012; + + [DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "SendMessageW")] + public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam); + [DllImport("user32.dll", EntryPoint = "DispatchMessageW")] public static extern IntPtr DispatchMessage(ref MSG lpmsg); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 5651e3b509..ba4a828f1c 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -667,8 +667,22 @@ namespace Avalonia.Win32 public void BeginMoveDrag(PointerPressedEventArgs e) { e.Pointer.Capture(null); - DefWindowProc(_hwnd, (int)WindowsMessage.WM_NCLBUTTONDOWN, - new IntPtr((int)HitTestValues.HTCAPTION), IntPtr.Zero); + + // Mouse.LeftButton actually reflects the primary button user is using. + // So we don't need to check whether the button has been swapped here. + if (e.Pointer.IsPrimary) + { + // SendMessage's return value is dependent on the message send. WM_SYSCOMMAND + // and WM_LBUTTONUP return value just signify whether the WndProc handled the + // message or not, so they are not interesting + + SendMessage(_hwnd, (int)WindowsMessage.WM_SYSCOMMAND, (IntPtr)SC_MOUSEMOVE, IntPtr.Zero); + SendMessage(_hwnd, (int)WindowsMessage.WM_LBUTTONUP, IntPtr.Zero, IntPtr.Zero); + } + else + { + throw new InvalidOperationException("BeginMoveDrag Failed"); + } } public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) From 36e7ab50043a871d725f72be5366950d93737801 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sat, 13 Jan 2024 22:12:07 +0800 Subject: [PATCH 05/13] Post native win32 dragmove on next dispatcher call (#14190) --- src/Windows/Avalonia.Win32/WindowImpl.cs | 28 +++++++++++++----------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index ba4a828f1c..92d009b530 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -26,6 +26,7 @@ using Avalonia.Win32.WinRT; using static Avalonia.Win32.Interop.UnmanagedMethods; using Avalonia.Input.Platform; using System.Diagnostics; +using Avalonia.Threading; using static Avalonia.Controls.Platform.IWin32OptionsTopLevelImpl; using static Avalonia.Controls.Platform.Win32SpecificOptions; @@ -668,21 +669,22 @@ namespace Avalonia.Win32 { e.Pointer.Capture(null); - // Mouse.LeftButton actually reflects the primary button user is using. - // So we don't need to check whether the button has been swapped here. - if (e.Pointer.IsPrimary) + Dispatcher.UIThread.Post(() => { - // SendMessage's return value is dependent on the message send. WM_SYSCOMMAND - // and WM_LBUTTONUP return value just signify whether the WndProc handled the - // message or not, so they are not interesting + if (e.Pointer.IsPrimary) + { + // SendMessage's return value is dependent on the message send. WM_SYSCOMMAND + // and WM_LBUTTONUP return value just signify whether the WndProc handled the + // message or not, so they are not interesting - SendMessage(_hwnd, (int)WindowsMessage.WM_SYSCOMMAND, (IntPtr)SC_MOUSEMOVE, IntPtr.Zero); - SendMessage(_hwnd, (int)WindowsMessage.WM_LBUTTONUP, IntPtr.Zero, IntPtr.Zero); - } - else - { - throw new InvalidOperationException("BeginMoveDrag Failed"); - } + SendMessage(_hwnd, (int)WindowsMessage.WM_SYSCOMMAND, (IntPtr)SC_MOUSEMOVE, IntPtr.Zero); + SendMessage(_hwnd, (int)WindowsMessage.WM_LBUTTONUP, IntPtr.Zero, IntPtr.Zero); + } + else + { + throw new InvalidOperationException("BeginMoveDrag Failed"); + } + }, DispatcherPriority.Send); } public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) From ff978daba7865c0593f5498c25d0ad0a1ccef160 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 8 Jan 2024 21:40:51 -0800 Subject: [PATCH 06/13] Add initial metal support for iOS --- .../Avalonia.Skia/Gpu/Metal/SkiaMetalApi.cs | 9 ++- src/iOS/Avalonia.iOS/AvaloniaView.cs | 50 ++++++++++++--- .../Avalonia.iOS/{ => Eagl}/EaglDisplay.cs | 18 +++++- .../{ => Eagl}/EaglLayerSurface.cs | 8 ++- src/iOS/Avalonia.iOS/{ => Eagl}/LayerFbo.cs | 2 + src/iOS/Avalonia.iOS/Metal/MetalDevice.cs | 32 ++++++++++ .../Avalonia.iOS/Metal/MetalDrawingSession.cs | 34 ++++++++++ .../Metal/MetalPlatformGraphics.cs | 43 +++++++++++++ .../Metal/MetalPlatformSurface.cs | 25 ++++++++ .../Avalonia.iOS/Metal/MetalRenderTarget.cs | 41 ++++++++++++ src/iOS/Avalonia.iOS/Platform.cs | 63 ++++++++++++++++--- 11 files changed, 303 insertions(+), 22 deletions(-) rename src/iOS/Avalonia.iOS/{ => Eagl}/EaglDisplay.cs (89%) rename src/iOS/Avalonia.iOS/{ => Eagl}/EaglLayerSurface.cs (95%) rename src/iOS/Avalonia.iOS/{ => Eagl}/LayerFbo.cs (99%) create mode 100644 src/iOS/Avalonia.iOS/Metal/MetalDevice.cs create mode 100644 src/iOS/Avalonia.iOS/Metal/MetalDrawingSession.cs create mode 100644 src/iOS/Avalonia.iOS/Metal/MetalPlatformGraphics.cs create mode 100644 src/iOS/Avalonia.iOS/Metal/MetalPlatformSurface.cs create mode 100644 src/iOS/Avalonia.iOS/Metal/MetalRenderTarget.cs 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/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index a51d3f2b28..b9b8b78c1e 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -19,7 +19,6 @@ using Avalonia.Rendering.Composition; using CoreAnimation; using Foundation; using ObjCRuntime; -using OpenGLES; using UIKit; using IInsetsManager = Avalonia.Controls.Platform.IInsetsManager; @@ -36,6 +35,7 @@ namespace Avalonia.iOS private TextInputMethodClient? _client; private IAvaloniaViewController? _controller; private IInputRoot? _inputRoot; + private MetalRenderTarget? _currentRenderTarget; public AvaloniaView() { @@ -47,23 +47,33 @@ namespace Avalonia.iOS _topLevel.StartRendering(); - InitEagl(); + InitLayerSurface(); MultipleTouchEnabled = true; } [ObsoletedOSPlatform("ios12.0", "Use 'Metal' instead.")] [SupportedOSPlatform("ios")] [UnsupportedOSPlatform("maccatalyst")] - private void InitEagl() + 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 EaglLayerSurface(eaglLayer) }; + } + else +#endif + if (l is CAMetalLayer metalLayer) + { + _topLevelImpl.Surfaces = new[] { new MetalPlatformSurface(metalLayer, this) }; + } } /// @@ -239,7 +249,16 @@ namespace Avalonia.iOS [Export("layerClass")] public static Class LayerClass() { - return new Class(typeof(CAEAGLLayer)); +#if !MACCATALYST + if (Platform.Graphics is 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); @@ -253,6 +272,12 @@ namespace Avalonia.iOS public override void LayoutSubviews() { _topLevelImpl.Resized?.Invoke(_topLevelImpl.ClientSize, WindowResizeReason.Layout); + if (_currentRenderTarget is not null) + { + _currentRenderTarget.PendingSize = new PixelSize((int)Bounds.Width, (int)Bounds.Height); + _currentRenderTarget.PendingScaling = Window.ContentScaleFactor; + } + base.LayoutSubviews(); } @@ -261,5 +286,10 @@ namespace Avalonia.iOS get => (Control?)_topLevel.Content; set => _topLevel.Content = value; } + + internal void SetRenderTarget(MetalRenderTarget target) + { + _currentRenderTarget = target; + } } } diff --git a/src/iOS/Avalonia.iOS/EaglDisplay.cs b/src/iOS/Avalonia.iOS/Eagl/EaglDisplay.cs similarity index 89% rename from src/iOS/Avalonia.iOS/EaglDisplay.cs rename to src/iOS/Avalonia.iOS/Eagl/EaglDisplay.cs index f003c4c6d7..cbd7089f4c 100644 --- a/src/iOS/Avalonia.iOS/EaglDisplay.cs +++ b/src/iOS/Avalonia.iOS/Eagl/EaglDisplay.cs @@ -1,6 +1,8 @@ +#if !MACCATALYST using System; using System.Collections.Generic; using System.Runtime.Versioning; +using Avalonia.Logging; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Reactive; @@ -19,7 +21,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,6 +31,19 @@ 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.")] @@ -122,3 +137,4 @@ namespace Avalonia.iOS public object TryGetFeature(Type featureType) => null; } } +#endif diff --git a/src/iOS/Avalonia.iOS/EaglLayerSurface.cs b/src/iOS/Avalonia.iOS/Eagl/EaglLayerSurface.cs similarity index 95% rename from src/iOS/Avalonia.iOS/EaglLayerSurface.cs rename to src/iOS/Avalonia.iOS/Eagl/EaglLayerSurface.cs index 6323bb3acb..e62395c985 100644 --- a/src/iOS/Avalonia.iOS/EaglLayerSurface.cs +++ b/src/iOS/Avalonia.iOS/Eagl/EaglLayerSurface.cs @@ -1,10 +1,13 @@ - +#if !MACCATALYST using System; using System.Runtime.Versioning; using System.Threading; using Avalonia.OpenGL; using Avalonia.OpenGL.Surfaces; using CoreAnimation; +using Foundation; +using OpenGLES; +using UIKit; namespace Avalonia.iOS { @@ -84,7 +87,7 @@ namespace Avalonia.iOS 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()) @@ -97,3 +100,4 @@ namespace Avalonia.iOS } } } +#endif diff --git a/src/iOS/Avalonia.iOS/LayerFbo.cs b/src/iOS/Avalonia.iOS/Eagl/LayerFbo.cs similarity index 99% rename from src/iOS/Avalonia.iOS/LayerFbo.cs rename to src/iOS/Avalonia.iOS/Eagl/LayerFbo.cs index d971858b6d..97a9692d1c 100644 --- a/src/iOS/Avalonia.iOS/LayerFbo.cs +++ b/src/iOS/Avalonia.iOS/Eagl/LayerFbo.cs @@ -1,3 +1,4 @@ +#if !MACCATALYST using System; using System.Runtime.Versioning; using Avalonia.OpenGL; @@ -148,3 +149,4 @@ namespace Avalonia.iOS public double Scaling => _oldLayerScale; } } +#endif diff --git a/src/iOS/Avalonia.iOS/Metal/MetalDevice.cs b/src/iOS/Avalonia.iOS/Metal/MetalDevice.cs new file mode 100644 index 0000000000..796cc2a613 --- /dev/null +++ b/src/iOS/Avalonia.iOS/Metal/MetalDevice.cs @@ -0,0 +1,32 @@ +using System; +using Avalonia.Metal; +using Avalonia.Utilities; +using Metal; + +namespace Avalonia.iOS; + +internal class MetalDevice : IMetalDevice +{ + private readonly DisposableLock _syncRoot = new(); + + public MetalDevice(IMTLDevice device) + { + Device = device; + Queue = device.CreateCommandQueue(); + } + + 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..1587377fbd --- /dev/null +++ b/src/iOS/Avalonia.iOS/Metal/MetalDrawingSession.cs @@ -0,0 +1,34 @@ +using System; +using Avalonia.Metal; +using CoreAnimation; + +namespace Avalonia.iOS; + +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..5947a13b0a --- /dev/null +++ b/src/iOS/Avalonia.iOS/Metal/MetalPlatformGraphics.cs @@ -0,0 +1,43 @@ +using System; +using Avalonia.Platform; +using Metal; +using SkiaSharp; + +namespace Avalonia.iOS; +#nullable enable + +internal class MetalPlatformGraphics : IPlatformGraphics +{ + private MetalPlatformGraphics() + { + + } + + public bool UsesSharedContext => false; + public IPlatformGraphicsContext CreateContext() => new MetalDevice(MTLDevice.SystemDefault); + + 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(); + } +} diff --git a/src/iOS/Avalonia.iOS/Metal/MetalPlatformSurface.cs b/src/iOS/Avalonia.iOS/Metal/MetalPlatformSurface.cs new file mode 100644 index 0000000000..453c9992c9 --- /dev/null +++ b/src/iOS/Avalonia.iOS/Metal/MetalPlatformSurface.cs @@ -0,0 +1,25 @@ +using Avalonia.Metal; +using CoreAnimation; + +namespace Avalonia.iOS; + +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..f8a28154df --- /dev/null +++ b/src/iOS/Avalonia.iOS/Metal/MetalRenderTarget.cs @@ -0,0 +1,41 @@ +using Avalonia.Metal; +using Avalonia.Platform; +using CoreAnimation; +using CoreGraphics; +using Foundation; + +namespace Avalonia.iOS; + +internal class MetalRenderTarget : IMetalPlatformSurfaceRenderTarget +{ + private readonly CAMetalLayer _layer; + private readonly MetalDevice _device; + private double _scaling = 1; + private PixelSize _size = new(1, 1); + + public MetalRenderTarget(CAMetalLayer layer, MetalDevice device) + { + _layer = layer; + _device = device; + } + + public double PendingScaling { get; set; } = 1; + public PixelSize PendingSize { get; set; } = new(1, 1); + public void Dispose() + { + } + + public IMetalPlatformSurfaceRenderingSession BeginRendering() + { + // Flush all existing rendering + var buffer = _device.Queue.CommandBuffer(); + buffer.Commit(); + buffer.WaitUntilCompleted(); + _size = PendingSize; + _scaling= PendingScaling; + _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/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index bb61861596..e586052087 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,26 @@ using Avalonia.Threading; namespace Avalonia { + public enum iOSRenderingMode + { + OpenGl = 1, + 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 +46,21 @@ namespace Avalonia.iOS { static class Platform { - public static EaglPlatformGraphics GlFeature; + 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 +70,34 @@ 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 + && EaglPlatformGraphics.TryCreate() is { } eaglGraphics) + { + return eaglGraphics; + } +#endif + + if (renderingMode == iOSRenderingMode.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."); } } } From da16fecbc04b36b40e48e3b453aad372780246b7 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 8 Jan 2024 21:42:18 -0800 Subject: [PATCH 07/13] Make iOS backend compilable with TVOS and MacCatalyst --- .../Compatibility/OperatingSystem.cs | 2 ++ src/iOS/Avalonia.iOS/AvaloniaView.cs | 34 +++++++++++++------ src/iOS/Avalonia.iOS/ClipboardImpl.cs | 2 ++ src/iOS/Avalonia.iOS/InsetsManager.cs | 7 ---- .../Storage/IOSSecurityScopedStream.cs | 4 ++- .../Avalonia.iOS/Storage/IOSStorageItem.cs | 4 ++- .../Storage/IOSStorageProvider.cs | 4 ++- src/iOS/Avalonia.iOS/TextInputResponder.cs | 5 ++- src/iOS/Avalonia.iOS/UIKitInputPane.cs | 13 ++++++- src/iOS/Avalonia.iOS/ViewController.cs | 8 +++++ 10 files changed, 60 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Base/Compatibility/OperatingSystem.cs b/src/Avalonia.Base/Compatibility/OperatingSystem.cs index 838f7da8b2..eac199b32f 100644 --- a/src/Avalonia.Base/Compatibility/OperatingSystem.cs +++ b/src/Avalonia.Base/Compatibility/OperatingSystem.cs @@ -11,6 +11,7 @@ namespace Avalonia.Compatibility public static bool IsLinux() => OperatingSystem.IsLinux(); 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 @@ -19,6 +20,7 @@ namespace Avalonia.Compatibility public static bool IsLinux() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); public static bool IsAndroid() => IsOSPlatform("ANDROID"); public static bool IsIOS() => IsOSPlatform("IOS"); + public static bool IsTvOS() => IsOSPlatform("TVOS"); // untested public static bool IsBrowser() => IsOSPlatform("BROWSER"); public static bool IsOSPlatform(string platform) => RuntimeInformation.IsOSPlatform(OSPlatform.Create(platform)); #endif diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index b9b8b78c1e..f44202c240 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -48,7 +48,9 @@ namespace Avalonia.iOS _topLevel.StartRendering(); InitLayerSurface(); +#if !TVOS MultipleTouchEnabled = true; +#endif } [ObsoletedOSPlatform("ios12.0", "Use 'Metal' instead.")] @@ -111,9 +113,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; @@ -122,8 +125,12 @@ namespace Avalonia.iOS { _view = view; _nativeControlHost = new NativeControlHostImpl(view); +#if !TVOS _storageProvider = new IOSStorageProvider(view); - _insetsManager = new InsetsManager(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. @@ -138,7 +145,6 @@ namespace Avalonia.iOS BindingPriority.Style); // lower priority, so it can be redefined by user } }; - _clipboard = new ClipboardImpl(); } public void Dispose() @@ -195,8 +201,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 { @@ -205,6 +214,7 @@ namespace Avalonia.iOS _ => UIStatusBarStyle.Default }; } +#endif } public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = @@ -212,11 +222,6 @@ namespace Avalonia.iOS public object? TryGetFeature(Type featureType) { - if (featureType == typeof(IStorageProvider)) - { - return _storageProvider; - } - if (featureType == typeof(ITextInputMethodImpl)) { return _view; @@ -232,15 +237,22 @@ namespace Avalonia.iOS return _insetsManager; } +#if !TVOS if (featureType == typeof(IClipboard)) { return _clipboard; } + if (featureType == typeof(IStorageProvider)) + { + return _storageProvider; + } + if (featureType == typeof(IInputPane)) { - return UIKitInputPane.Instance; + return _inputPane; } +#endif return null; } diff --git a/src/iOS/Avalonia.iOS/ClipboardImpl.cs b/src/iOS/Avalonia.iOS/ClipboardImpl.cs index 150f3424e3..0bc03e9160 100644 --- a/src/iOS/Avalonia.iOS/ClipboardImpl.cs +++ b/src/iOS/Avalonia.iOS/ClipboardImpl.cs @@ -1,3 +1,4 @@ +#if !TVOS using System; using System.Threading.Tasks; using Avalonia.Input; @@ -32,3 +33,4 @@ namespace Avalonia.iOS public Task GetDataAsync(string format) => Task.FromResult(null); } } +#endif diff --git a/src/iOS/Avalonia.iOS/InsetsManager.cs b/src/iOS/Avalonia.iOS/InsetsManager.cs index 54b769c567..c746644532 100644 --- a/src/iOS/Avalonia.iOS/InsetsManager.cs +++ b/src/iOS/Avalonia.iOS/InsetsManager.cs @@ -1,22 +1,15 @@ 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/Storage/IOSSecurityScopedStream.cs b/src/iOS/Avalonia.iOS/Storage/IOSSecurityScopedStream.cs index 424ec7589a..c78e74dc84 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSSecurityScopedStream.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSSecurityScopedStream.cs @@ -1,4 +1,5 @@ -using System.IO; +#if !TVOS +using System.IO; using Foundation; @@ -66,3 +67,4 @@ internal sealed class IOSSecurityScopedStream : Stream } } } +#endif diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs index f6697777be..74fd9b7273 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs @@ -1,4 +1,5 @@ -using System; +#if !TVOS +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -275,3 +276,4 @@ internal sealed class IOSStorageFolder : IOSStorageItem, IStorageBookmarkFolder } } } +#endif diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs index 5c53cd2d2d..2848bfaba0 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs @@ -1,4 +1,5 @@ -using System; +#if !TVOS +using System; using System.Linq; using System.Collections.Generic; using System.Diagnostics; @@ -244,3 +245,4 @@ internal class IOSStorageProvider : IStorageProvider } } } +#endif diff --git a/src/iOS/Avalonia.iOS/TextInputResponder.cs b/src/iOS/Avalonia.iOS/TextInputResponder.cs index a5a0c0759f..d3b518ae65 100644 --- a/src/iOS/Avalonia.iOS/TextInputResponder.cs +++ b/src/iOS/Avalonia.iOS/TextInputResponder.cs @@ -108,7 +108,10 @@ partial class AvaloniaView { get { - var mode = UITextInputMode.CurrentInputMode; + UITextInputMode? mode = null; +#if !TVOS + mode = UITextInputMode.CurrentInputMode; +#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/UIKitInputPane.cs b/src/iOS/Avalonia.iOS/UIKitInputPane.cs index 86cae0bf53..7ba6f8eb77 100644 --- a/src/iOS/Avalonia.iOS/UIKitInputPane.cs +++ b/src/iOS/Avalonia.iOS/UIKitInputPane.cs @@ -1,5 +1,7 @@ +#if !TVOS using System; using System.Diagnostics; +using System.Runtime.Versioning; using Avalonia.Animation.Easings; using Avalonia.Controls.Platform; using Foundation; @@ -8,6 +10,9 @@ 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 +38,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 +59,7 @@ internal sealed class UIKitInputPane : IInputPane StateChanged?.Invoke(this, new InputPaneStateEventArgs( State, startRect, OccludedRect, TimeSpan.FromSeconds(duration), easing)); +#endif } } +#endif diff --git a/src/iOS/Avalonia.iOS/ViewController.cs b/src/iOS/Avalonia.iOS/ViewController.cs index 42a0949a9c..5f901fcf01 100644 --- a/src/iOS/Avalonia.iOS/ViewController.cs +++ b/src/iOS/Avalonia.iOS/ViewController.cs @@ -7,7 +7,9 @@ namespace Avalonia.iOS; [Unstable] public interface IAvaloniaViewController { +#if !TVOS UIStatusBarStyle PreferredStatusBarStyle { get; set; } +#endif bool PrefersStatusBarHidden { get; set; } Thickness SafeAreaPadding { get; } event EventHandler SafeAreaPaddingChanged; @@ -16,7 +18,9 @@ public interface IAvaloniaViewController /// 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 } } From 203a17ec63d84ab29ffc9ad7248d22abd76eea4f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 13 Jan 2024 20:24:19 -0800 Subject: [PATCH 08/13] Enable nullable and fix almost all warnings in iOS project --- .../IActivatableApplicationLifetime.cs | 4 +- src/iOS/Avalonia.iOS/Avalonia.iOS.csproj | 9 +++- src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs | 6 +-- src/iOS/Avalonia.iOS/AvaloniaView.Text.cs | 1 - src/iOS/Avalonia.iOS/AvaloniaView.cs | 45 ++++++++++++------- src/iOS/Avalonia.iOS/ClipboardImpl.cs | 39 +++++++++++++--- src/iOS/Avalonia.iOS/DispatcherImpl.cs | 2 - src/iOS/Avalonia.iOS/DisplayLinkTimer.cs | 4 +- src/iOS/Avalonia.iOS/Eagl/EaglDisplay.cs | 25 +++++++---- src/iOS/Avalonia.iOS/Eagl/EaglLayerSurface.cs | 14 +++--- src/iOS/Avalonia.iOS/Eagl/LayerFbo.cs | 18 +++++--- src/iOS/Avalonia.iOS/InsetsManager.cs | 1 - src/iOS/Avalonia.iOS/Metal/MetalDevice.cs | 8 ++-- .../Avalonia.iOS/Metal/MetalDrawingSession.cs | 4 +- .../Metal/MetalPlatformGraphics.cs | 18 +++++--- .../Metal/MetalPlatformSurface.cs | 2 +- .../Avalonia.iOS/Metal/MetalRenderTarget.cs | 5 +-- src/iOS/Avalonia.iOS/NativeControlHostImpl.cs | 4 +- src/iOS/Avalonia.iOS/Platform.cs | 15 ++++--- src/iOS/Avalonia.iOS/PlatformSettings.cs | 1 - src/iOS/Avalonia.iOS/SingleViewLifetime.cs | 12 ++--- .../Avalonia.iOS/Storage/IOSStorageItem.cs | 7 +-- .../Storage/IOSStorageProvider.cs | 6 ++- src/iOS/Avalonia.iOS/Stubs.cs | 3 +- .../TextInputResponder.Properties.cs | 1 - src/iOS/Avalonia.iOS/TextInputResponder.cs | 4 +- src/iOS/Avalonia.iOS/TouchHandler.cs | 6 +-- src/iOS/Avalonia.iOS/UIKitInputPane.cs | 1 - src/iOS/Avalonia.iOS/ViewController.cs | 4 +- 29 files changed, 166 insertions(+), 103 deletions(-) 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/iOS/Avalonia.iOS/Avalonia.iOS.csproj b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj index eb4938c13f..4201ae0ad3 100644 --- a/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj +++ b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj @@ -1,9 +1,15 @@  net7.0-ios16.0 - 13.0 + 13.0 + 13.0 + 13.1 true true + + + + $(NoWarn);CA1416 @@ -12,4 +18,5 @@ + 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 f44202c240..b6cde47c7a 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,7 +11,6 @@ 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; @@ -24,6 +22,9 @@ 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 @@ -35,7 +36,7 @@ namespace Avalonia.iOS private TextInputMethodClient? _client; private IAvaloniaViewController? _controller; private IInputRoot? _inputRoot; - private MetalRenderTarget? _currentRenderTarget; + private Metal.MetalRenderTarget? _currentRenderTarget; public AvaloniaView() { @@ -49,13 +50,14 @@ namespace Avalonia.iOS InitLayerSurface(); #if !TVOS - MultipleTouchEnabled = true; + if (OperatingSystem.IsIOS() || OperatingSystem.IsMacCatalyst()) + { + MultipleTouchEnabled = true; + } #endif } - [ObsoletedOSPlatform("ios12.0", "Use 'Metal' instead.")] - [SupportedOSPlatform("ios")] - [UnsupportedOSPlatform("maccatalyst")] + [SuppressMessage("Interoperability", "CA1422:Validate platform compatibility")] private void InitLayerSurface() { var l = Layer; @@ -68,13 +70,13 @@ namespace Avalonia.iOS OpenGLES.EAGLDrawableProperty.RetainedBacking, false, OpenGLES.EAGLDrawableProperty.ColorFormat, OpenGLES.EAGLColorFormat.RGBA8 ); - _topLevelImpl.Surfaces = new[] { new EaglLayerSurface(eaglLayer) }; + _topLevelImpl.Surfaces = new[] { new Eagl.EaglLayerSurface(eaglLayer) }; } else #endif if (l is CAMetalLayer metalLayer) { - _topLevelImpl.Surfaces = new[] { new MetalPlatformSurface(metalLayer, this) }; + _topLevelImpl.Surfaces = new[] { new Metal.MetalPlatformSurface(metalLayer, this) }; } } @@ -85,6 +87,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); @@ -125,8 +133,12 @@ namespace Avalonia.iOS { _view = view; _nativeControlHost = new NativeControlHostImpl(view); -#if !TVOS - _storageProvider = new IOSStorageProvider(view); +#if TVOS + _storageProvider = null; + _clipboard = null; + _inputPane = null; +#else + _storageProvider = new Storage.IOSStorageProvider(view); _clipboard = new ClipboardImpl(); _inputPane = UIKitInputPane.Instance; #endif @@ -152,7 +164,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) { @@ -237,7 +250,6 @@ namespace Avalonia.iOS return _insetsManager; } -#if !TVOS if (featureType == typeof(IClipboard)) { return _clipboard; @@ -252,7 +264,6 @@ namespace Avalonia.iOS { return _inputPane; } -#endif return null; } @@ -262,7 +273,7 @@ namespace Avalonia.iOS public static Class LayerClass() { #if !MACCATALYST - if (Platform.Graphics is EaglPlatformGraphics) + if (Platform.Graphics is Eagl.EaglPlatformGraphics) { return new Class(typeof(CAEAGLLayer)); } @@ -299,7 +310,7 @@ namespace Avalonia.iOS set => _topLevel.Content = value; } - internal void SetRenderTarget(MetalRenderTarget target) + internal void SetRenderTarget(Metal.MetalRenderTarget target) { _currentRenderTarget = target; } diff --git a/src/iOS/Avalonia.iOS/ClipboardImpl.cs b/src/iOS/Avalonia.iOS/ClipboardImpl.cs index 0bc03e9160..27cbc0246c 100644 --- a/src/iOS/Avalonia.iOS/ClipboardImpl.cs +++ b/src/iOS/Avalonia.iOS/ClipboardImpl.cs @@ -1,20 +1,22 @@ #if !TVOS 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; @@ -22,15 +24,40 @@ 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); + } } } #endif 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/Eagl/EaglDisplay.cs b/src/iOS/Avalonia.iOS/Eagl/EaglDisplay.cs index cbd7089f4c..696a1a1561 100644 --- a/src/iOS/Avalonia.iOS/Eagl/EaglDisplay.cs +++ b/src/iOS/Avalonia.iOS/Eagl/EaglDisplay.cs @@ -8,10 +8,13 @@ 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; @@ -32,7 +35,7 @@ namespace Avalonia.iOS Context = new(iface, null); } - public static EaglPlatformGraphics TryCreate() + public static EaglPlatformGraphics? TryCreate() { try { @@ -47,12 +50,14 @@ namespace Avalonia.iOS } [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 ? @@ -68,10 +73,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; } @@ -102,7 +107,7 @@ namespace Avalonia.iOS { if (Context == null) throw new PlatformGraphicsContextLostException(); - if(EAGLContext.CurrentContext == Context) + if (EAGLContext.CurrentContext == Context) return Disposable.Empty; return MakeCurrent(); } @@ -110,8 +115,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); } @@ -134,7 +141,7 @@ namespace Avalonia.iOS } } - public object TryGetFeature(Type featureType) => null; + public object? TryGetFeature(Type featureType) => null; } } #endif diff --git a/src/iOS/Avalonia.iOS/Eagl/EaglLayerSurface.cs b/src/iOS/Avalonia.iOS/Eagl/EaglLayerSurface.cs index e62395c985..f010dd8f13 100644 --- a/src/iOS/Avalonia.iOS/Eagl/EaglLayerSurface.cs +++ b/src/iOS/Avalonia.iOS/Eagl/EaglLayerSurface.cs @@ -5,14 +5,14 @@ using System.Threading; using Avalonia.OpenGL; using Avalonia.OpenGL.Surfaces; using CoreAnimation; -using Foundation; -using OpenGLES; -using UIKit; -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; @@ -80,19 +80,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 = ((EaglPlatformGraphics)Platform.Graphics).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/Eagl/LayerFbo.cs b/src/iOS/Avalonia.iOS/Eagl/LayerFbo.cs index 97a9692d1c..d986e380d8 100644 --- a/src/iOS/Avalonia.iOS/Eagl/LayerFbo.cs +++ b/src/iOS/Avalonia.iOS/Eagl/LayerFbo.cs @@ -5,10 +5,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; @@ -29,7 +32,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; @@ -78,7 +81,7 @@ namespace Avalonia.iOS public void Present() { Bind(); - var success = _context.PresentRenderBuffer(GlConsts.GL_RENDERBUFFER); + _context.PresentRenderBuffer(GlConsts.GL_RENDERBUFFER); } public void Dispose() @@ -95,13 +98,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) @@ -139,10 +145,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/InsetsManager.cs b/src/iOS/Avalonia.iOS/InsetsManager.cs index c746644532..d1439fcd3f 100644 --- a/src/iOS/Avalonia.iOS/InsetsManager.cs +++ b/src/iOS/Avalonia.iOS/InsetsManager.cs @@ -3,7 +3,6 @@ using Avalonia.Controls.Platform; using Avalonia.Media; namespace Avalonia.iOS; -#nullable enable internal class InsetsManager : IInsetsManager { diff --git a/src/iOS/Avalonia.iOS/Metal/MetalDevice.cs b/src/iOS/Avalonia.iOS/Metal/MetalDevice.cs index 796cc2a613..03123a8801 100644 --- a/src/iOS/Avalonia.iOS/Metal/MetalDevice.cs +++ b/src/iOS/Avalonia.iOS/Metal/MetalDevice.cs @@ -1,9 +1,10 @@ using System; +using System.Runtime.Versioning; using Avalonia.Metal; using Avalonia.Utilities; using Metal; -namespace Avalonia.iOS; +namespace Avalonia.iOS.Metal; internal class MetalDevice : IMetalDevice { @@ -12,7 +13,8 @@ internal class MetalDevice : IMetalDevice public MetalDevice(IMTLDevice device) { Device = device; - Queue = device.CreateCommandQueue(); + Queue = device.CreateCommandQueue() + ?? throw new InvalidOperationException("IMTLCommandQueue is not available"); } public IMTLDevice Device { get; } @@ -23,7 +25,7 @@ internal class MetalDevice : IMetalDevice public bool IsLost => false; public IDisposable EnsureCurrent() => _syncRoot.Lock(); - public object TryGetFeature(Type featureType) => null; + public object? TryGetFeature(Type featureType) => null; public void Dispose() { diff --git a/src/iOS/Avalonia.iOS/Metal/MetalDrawingSession.cs b/src/iOS/Avalonia.iOS/Metal/MetalDrawingSession.cs index 1587377fbd..0233922025 100644 --- a/src/iOS/Avalonia.iOS/Metal/MetalDrawingSession.cs +++ b/src/iOS/Avalonia.iOS/Metal/MetalDrawingSession.cs @@ -2,7 +2,7 @@ using System; using Avalonia.Metal; using CoreAnimation; -namespace Avalonia.iOS; +namespace Avalonia.iOS.Metal; internal class MetalDrawingSession : IMetalPlatformSurfaceRenderingSession { @@ -21,7 +21,7 @@ internal class MetalDrawingSession : IMetalPlatformSurfaceRenderingSession public void Dispose() { var buffer = _device.Queue.CommandBuffer(); - buffer.PresentDrawable(_drawable); + buffer!.PresentDrawable(_drawable); buffer.Commit(); } diff --git a/src/iOS/Avalonia.iOS/Metal/MetalPlatformGraphics.cs b/src/iOS/Avalonia.iOS/Metal/MetalPlatformGraphics.cs index 5947a13b0a..fb5ffc862c 100644 --- a/src/iOS/Avalonia.iOS/Metal/MetalPlatformGraphics.cs +++ b/src/iOS/Avalonia.iOS/Metal/MetalPlatformGraphics.cs @@ -1,20 +1,26 @@ using System; +using System.Runtime.Versioning; using Avalonia.Platform; using Metal; using SkiaSharp; -namespace Avalonia.iOS; -#nullable enable +namespace Avalonia.iOS.Metal; +[SupportedOSPlatform("ios")] +[SupportedOSPlatform("macos")] +[SupportedOSPlatform("maccatalyst")] +[SupportedOSPlatform("tvos")] internal class MetalPlatformGraphics : IPlatformGraphics { - private MetalPlatformGraphics() + private readonly IMTLDevice _defaultDevice; + + private MetalPlatformGraphics(IMTLDevice defaultDevice) { - + _defaultDevice = defaultDevice; } public bool UsesSharedContext => false; - public IPlatformGraphicsContext CreateContext() => new MetalDevice(MTLDevice.SystemDefault); + public IPlatformGraphicsContext CreateContext() => new MetalDevice(_defaultDevice); public IPlatformGraphicsContext GetSharedContext() => throw new NotSupportedException(); @@ -38,6 +44,6 @@ internal class MetalPlatformGraphics : IPlatformGraphics } #endif - return new MetalPlatformGraphics(); + return new MetalPlatformGraphics(device); } } diff --git a/src/iOS/Avalonia.iOS/Metal/MetalPlatformSurface.cs b/src/iOS/Avalonia.iOS/Metal/MetalPlatformSurface.cs index 453c9992c9..285ba95e48 100644 --- a/src/iOS/Avalonia.iOS/Metal/MetalPlatformSurface.cs +++ b/src/iOS/Avalonia.iOS/Metal/MetalPlatformSurface.cs @@ -1,7 +1,7 @@ using Avalonia.Metal; using CoreAnimation; -namespace Avalonia.iOS; +namespace Avalonia.iOS.Metal; internal class MetalPlatformSurface : IMetalPlatformSurface { diff --git a/src/iOS/Avalonia.iOS/Metal/MetalRenderTarget.cs b/src/iOS/Avalonia.iOS/Metal/MetalRenderTarget.cs index f8a28154df..bb2d74235e 100644 --- a/src/iOS/Avalonia.iOS/Metal/MetalRenderTarget.cs +++ b/src/iOS/Avalonia.iOS/Metal/MetalRenderTarget.cs @@ -2,9 +2,8 @@ using Avalonia.Metal; using Avalonia.Platform; using CoreAnimation; using CoreGraphics; -using Foundation; -namespace Avalonia.iOS; +namespace Avalonia.iOS.Metal; internal class MetalRenderTarget : IMetalPlatformSurfaceRenderTarget { @@ -28,7 +27,7 @@ internal class MetalRenderTarget : IMetalPlatformSurfaceRenderTarget public IMetalPlatformSurfaceRenderingSession BeginRendering() { // Flush all existing rendering - var buffer = _device.Queue.CommandBuffer(); + var buffer = _device.Queue.CommandBuffer()!; buffer.Commit(); buffer.WaitUntilCompleted(); _size = PendingSize; 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 e586052087..8cda5a02e2 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -46,10 +46,10 @@ namespace Avalonia.iOS { static class Platform { - public static iOSPlatformOptions Options; - public static IPlatformGraphics Graphics; - 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() { @@ -84,14 +84,17 @@ namespace Avalonia.iOS { #if !MACCATALYST if (renderingMode == iOSRenderingMode.OpenGl - && EaglPlatformGraphics.TryCreate() is { } eaglGraphics) + && !OperatingSystem.IsMacCatalyst() +#pragma warning disable CA1422 + && Eagl.EaglPlatformGraphics.TryCreate() is { } eaglGraphics) +#pragma warning restore CA1422 { return eaglGraphics; } #endif if (renderingMode == iOSRenderingMode.Metal - && MetalPlatformGraphics.TryCreate() is { } metalGraphics) + && Metal.MetalPlatformGraphics.TryCreate() is { } metalGraphics) { return metalGraphics; } 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 74fd9b7273..9819eea382 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs @@ -10,8 +10,6 @@ using Foundation; using UIKit; -#nullable enable - namespace Avalonia.iOS.Storage; internal abstract class IOSStorageItem : IStorageBookmarkItem @@ -57,7 +55,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); } diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs index 2848bfaba0..9b4f2611b1 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs @@ -13,8 +13,6 @@ using UniformTypeIdentifiers; using UTTypeLegacy = MobileCoreServices.UTType; using UTType = UniformTypeIdentifiers.UTType; -#nullable enable - namespace Avalonia.iOS.Storage; internal class IOSStorageProvider : IStorageProvider @@ -69,8 +67,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); @@ -149,7 +149,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 d3b518ae65..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 { @@ -110,7 +108,9 @@ partial class AvaloniaView { 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 index 44bf08365f..b85affbce5 100644 --- a/src/iOS/Avalonia.iOS/TouchHandler.cs +++ b/src/iOS/Avalonia.iOS/TouchHandler.cs @@ -11,7 +11,7 @@ namespace Avalonia.iOS { private readonly AvaloniaView _view; private readonly ITopLevelImpl _tl; - public TouchDevice _device = new TouchDevice(); + public TouchDevice _device = new(); public TouchHandler(AvaloniaView view, ITopLevelImpl tl) { @@ -19,12 +19,12 @@ namespace Avalonia.iOS _tl = tl; } - static ulong Ts(UIEvent evt) => (ulong) (evt.Timestamp * 1000); + static ulong Ts(UIEvent? evt) => evt is null ? 0 : (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) + public void Handle(NSSet touches, UIEvent? evt) { foreach (UITouch t in touches) { diff --git a/src/iOS/Avalonia.iOS/UIKitInputPane.cs b/src/iOS/Avalonia.iOS/UIKitInputPane.cs index 7ba6f8eb77..8db7f1b364 100644 --- a/src/iOS/Avalonia.iOS/UIKitInputPane.cs +++ b/src/iOS/Avalonia.iOS/UIKitInputPane.cs @@ -7,7 +7,6 @@ using Avalonia.Controls.Platform; using Foundation; using UIKit; -#nullable enable namespace Avalonia.iOS; [UnsupportedOSPlatform("tvos")] diff --git a/src/iOS/Avalonia.iOS/ViewController.cs b/src/iOS/Avalonia.iOS/ViewController.cs index 5f901fcf01..b083cd6c90 100644 --- a/src/iOS/Avalonia.iOS/ViewController.cs +++ b/src/iOS/Avalonia.iOS/ViewController.cs @@ -12,7 +12,7 @@ public interface IAvaloniaViewController #endif bool PrefersStatusBarHidden { get; set; } Thickness SafeAreaPadding { get; } - event EventHandler SafeAreaPaddingChanged; + event EventHandler? SafeAreaPaddingChanged; } /// @@ -78,5 +78,5 @@ public class DefaultAvaloniaViewController : UIViewController, IAvaloniaViewCont public Thickness SafeAreaPadding { get; private set; } /// - public event EventHandler SafeAreaPaddingChanged; + public event EventHandler? SafeAreaPaddingChanged; } From 370c6910ed888c3d7df0b335d92e91ca030ee7ef Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 14 Jan 2024 00:18:43 -0800 Subject: [PATCH 09/13] Implement keyboard and remote (+touch) input on iOS and tvOS --- src/iOS/Avalonia.iOS/AvaloniaView.cs | 80 +++++- src/iOS/Avalonia.iOS/InputHandler.cs | 357 +++++++++++++++++++++++++++ src/iOS/Avalonia.iOS/TouchHandler.cs | 52 ---- 3 files changed, 428 insertions(+), 61 deletions(-) create mode 100644 src/iOS/Avalonia.iOS/InputHandler.cs delete mode 100644 src/iOS/Avalonia.iOS/TouchHandler.cs diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index b6cde47c7a..108caa0925 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -32,7 +32,7 @@ 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; @@ -41,7 +41,7 @@ namespace Avalonia.iOS public AvaloniaView() { _topLevelImpl = new TopLevelImpl(this); - _touches = new TouchHandler(this, _topLevelImpl); + _input = new InputHandler(this, _topLevelImpl); _topLevel = new EmbeddableControlRoot(_topLevelImpl); _topLevel.Prepare(); @@ -49,12 +49,33 @@ namespace Avalonia.iOS _topLevel.StartRendering(); InitLayerSurface(); -#if !TVOS - if (OperatingSystem.IsIOS() || OperatingSystem.IsMacCatalyst()) + + // Remote touch handling + if (OperatingSystem.IsTvOS()) { - MultipleTouchEnabled = true; + 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 + } } [SuppressMessage("Interoperability", "CA1422:Validate platform compatibility")] @@ -284,14 +305,55 @@ namespace Avalonia.iOS } } - 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); diff --git a/src/iOS/Avalonia.iOS/InputHandler.cs b/src/iOS/Avalonia.iOS/InputHandler.cs new file mode 100644 index 0000000000..7057f5a84d --- /dev/null +++ b/src/iOS/Avalonia.iOS/InputHandler.cs @@ -0,0 +1,357 @@ +using System; +using System.Collections.Generic; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Platform; +using Foundation; +using UIKit; + +namespace Avalonia.iOS; + +internal sealed class InputHandler +{ + private readonly bool _supportsKey = OperatingSystem.IsIOSVersionAtLeast(13, 4) + || OperatingSystem.IsTvOSVersionAtLeast(13, 4); + + private readonly AvaloniaView _view; + private readonly ITopLevelImpl _tl; + public TouchDevice _device = new(); + private static long _nextTouchPointId = 1; + private readonly Dictionary _knownTouches = new Dictionary(); + + 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) + { + 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); + } + } + + 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 (_supportsKey && p.Key is { } uiKey + && s_keys.TryGetValue(uiKey.KeyCode, out physicalKey)) + { + var uiModifier = uiKey.ModifierFlags; + if (uiModifier.HasFlag(UIKeyModifierFlags.Shift)) + modifier |= RawInputModifiers.Shift; + if (uiModifier.HasFlag(UIKeyModifierFlags.Alternate)) + modifier |= RawInputModifiers.Alt; + if (uiModifier.HasFlag(UIKeyModifierFlags.Control)) + modifier |= RawInputModifiers.Control; + if (uiModifier.HasFlag(UIKeyModifierFlags.Command)) + modifier |= RawInputModifiers.Meta; + + 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, + UIPressType.PageUp => PhysicalKey.PageUp, + UIPressType.PageDown => PhysicalKey.PageDown, + _ => 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 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, + }; +} diff --git a/src/iOS/Avalonia.iOS/TouchHandler.cs b/src/iOS/Avalonia.iOS/TouchHandler.cs deleted file mode 100644 index b85affbce5..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(); - - public TouchHandler(AvaloniaView view, ITopLevelImpl tl) - { - _view = view; - _tl = tl; - } - - static ulong Ts(UIEvent? evt) => evt is null ? 0 : (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); - } - } - - } -} From cefdca3de6ccd6c7bb82846f57f6f033b38a1185 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 14 Jan 2024 00:21:47 -0800 Subject: [PATCH 10/13] Disable Metal for now --- src/iOS/Avalonia.iOS/Platform.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index 8cda5a02e2..6515cc24ba 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -12,21 +12,28 @@ 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: , . + /// The default value is: . /// /// Thrown if no values were matched. public IReadOnlyList RenderingMode { get; set; } = new[] { - iOSRenderingMode.OpenGl, iOSRenderingMode.Metal + iOSRenderingMode.OpenGl }; } From 335d6fb45a5448d7e6131309793e45b49860f867 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 14 Jan 2024 00:21:57 -0800 Subject: [PATCH 11/13] Fix warning --- src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs index 9819eea382..879b8a1a70 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs @@ -84,7 +84,7 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem } } - public async Task MoveAsync(IStorageFolder destination) + public Task MoveAsync(IStorageFolder destination) { if (destination is not IOSStorageFolder folder) { @@ -101,9 +101,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) @@ -111,7 +111,7 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem throw new NSErrorException(error); } - return null; + return Task.FromResult(null); } finally { From 810713f26b3769138375965d45ccdc01c3e63d24 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 14 Jan 2024 00:22:20 -0800 Subject: [PATCH 12/13] Add tvOS target for Avalonia.iOS --- src/iOS/Avalonia.iOS/Avalonia.iOS.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj index 4201ae0ad3..fe9bdbfed9 100644 --- a/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj +++ b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj @@ -1,6 +1,6 @@  - net7.0-ios16.0 + net7.0-ios16.0;net7.0-tvos 13.0 13.0 13.1 From c612077d6bcf0d3fbfda58c64496642749207af1 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 14 Jan 2024 00:54:39 -0800 Subject: [PATCH 13/13] Include tvos workloads --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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'