Browse Source

Merge branch 'ios-tvos-updates' into xy-focus-and-tvos

xy-focus-and-tvos
Max Katz 2 years ago
parent
commit
5dc6bb2f68
  1. 2
      azure-pipelines.yml
  2. 6
      build/SkiaSharp.props
  3. 2
      src/Avalonia.Base/Compatibility/OperatingSystem.cs
  4. 4
      src/Avalonia.Controls/ApplicationLifetimes/IActivatableApplicationLifetime.cs
  5. 2
      src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs
  6. 12
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  7. 9
      src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalApi.cs
  8. 2
      src/Tizen/Avalonia.Tizen/Avalonia.Tizen.csproj
  9. 7
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  10. 20
      src/Windows/Avalonia.Win32/WindowImpl.cs
  11. 11
      src/iOS/Avalonia.iOS/Avalonia.iOS.csproj
  12. 6
      src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs
  13. 1
      src/iOS/Avalonia.iOS/AvaloniaView.Text.cs
  14. 187
      src/iOS/Avalonia.iOS/AvaloniaView.cs
  15. 41
      src/iOS/Avalonia.iOS/ClipboardImpl.cs
  16. 2
      src/iOS/Avalonia.iOS/DispatcherImpl.cs
  17. 4
      src/iOS/Avalonia.iOS/DisplayLinkTimer.cs
  18. 41
      src/iOS/Avalonia.iOS/Eagl/EaglDisplay.cs
  19. 14
      src/iOS/Avalonia.iOS/Eagl/EaglLayerSurface.cs
  20. 20
      src/iOS/Avalonia.iOS/Eagl/LayerFbo.cs
  21. 357
      src/iOS/Avalonia.iOS/InputHandler.cs
  22. 8
      src/iOS/Avalonia.iOS/InsetsManager.cs
  23. 34
      src/iOS/Avalonia.iOS/Metal/MetalDevice.cs
  24. 34
      src/iOS/Avalonia.iOS/Metal/MetalDrawingSession.cs
  25. 49
      src/iOS/Avalonia.iOS/Metal/MetalPlatformGraphics.cs
  26. 25
      src/iOS/Avalonia.iOS/Metal/MetalPlatformSurface.cs
  27. 40
      src/iOS/Avalonia.iOS/Metal/MetalRenderTarget.cs
  28. 4
      src/iOS/Avalonia.iOS/NativeControlHostImpl.cs
  29. 77
      src/iOS/Avalonia.iOS/Platform.cs
  30. 1
      src/iOS/Avalonia.iOS/PlatformSettings.cs
  31. 12
      src/iOS/Avalonia.iOS/SingleViewLifetime.cs
  32. 4
      src/iOS/Avalonia.iOS/Storage/IOSSecurityScopedStream.cs
  33. 19
      src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs
  34. 10
      src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs
  35. 3
      src/iOS/Avalonia.iOS/Stubs.cs
  36. 1
      src/iOS/Avalonia.iOS/TextInputResponder.Properties.cs
  37. 9
      src/iOS/Avalonia.iOS/TextInputResponder.cs
  38. 52
      src/iOS/Avalonia.iOS/TouchHandler.cs
  39. 14
      src/iOS/Avalonia.iOS/UIKitInputPane.cs
  40. 12
      src/iOS/Avalonia.iOS/ViewController.cs
  41. 39
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_SelectedValue.cs

2
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'

6
build/SkiaSharp.props

@ -1,7 +1,7 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="2.88.6" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.88.6" />
<PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="SkiaSharp.NativeAssets.WebAssembly" Version="2.88.6" />
<PackageReference Include="SkiaSharp" Version="2.88.7" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.88.7" />
<PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="SkiaSharp.NativeAssets.WebAssembly" Version="2.88.7" />
</ItemGroup>
</Project>

2
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

4
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 <see cref="ActivationKind"/> enumeration.
/// </summary>
event EventHandler<ActivatedEventArgs> Activated;
event EventHandler<ActivatedEventArgs>? Activated;
/// <summary>
/// An event that is raised when the application is Deactivated for various reasons
/// as described by the <see cref="ActivationKind"/> enumeration.
/// </summary>
event EventHandler<ActivatedEventArgs> Deactivated;
event EventHandler<ActivatedEventArgs>? Deactivated;
/// <summary>
/// Tells the application that it should attempt to leave its background state.

2
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
/// <value>The <see cref="T:Avalonia.Data.IBinding" /> object used
/// when binding to a collection property.</value>
[AssignBinding]
[InheritDataTypeFromItems(nameof(ItemsSource))]
public IBinding? ValueMemberBinding
{
get => _valueBindingEvaluator?.ValueBinding;

12
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<object> ValueProperty =
AvaloniaProperty.Register<BindingHelper, object>("Value");
public object Evaluate(object? dataContext)
public object? Evaluate(object? dataContext)
{
// Only update the DataContext if necessary
if (!Equals(dataContext, DataContext))

9
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 });
}

2
src/Tizen/Avalonia.Tizen/Avalonia.Tizen.csproj

@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.88.6" />
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.88.7" />
<PackageReference Include="HarfBuzzSharp.NativeAssets.Tizen" Version="2.8.2.3" />
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />

7
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);

20
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;
@ -667,8 +668,23 @@ 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);
Dispatcher.UIThread.Post(() =>
{
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");
}
}, DispatcherPriority.Send);
}
public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e)

11
src/iOS/Avalonia.iOS/Avalonia.iOS.csproj

@ -1,9 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-ios16.0</TargetFramework>
<SupportedOSPlatformVersion>13.0</SupportedOSPlatformVersion>
<TargetFrameworks>net7.0-ios16.0;net7.0-tvos</TargetFrameworks>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">13.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tvos'">13.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">13.1</SupportedOSPlatformVersion>
<MSBuildEnableWorkloadResolver>true</MSBuildEnableWorkloadResolver>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<!-- Compatibility attributes are pretty much broken for iOS-like platforms. Verify by hand. -->
<!-- Workaround: https://github.com/dotnet/roslyn-analyzers/issues/6158 -->
<NoWarn>$(NoWarn);CA1416</NoWarn>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" />
@ -12,4 +18,5 @@
<Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\TrimmingEnable.props" />
<Import Project="..\..\..\build\NullableEnable.props" />
</Project>

6
src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs

@ -15,7 +15,7 @@ namespace Avalonia.iOS
public class AvaloniaAppDelegate<TApp> : UIResponder, IUIApplicationDelegate, IAvaloniaAppDelegate
where TApp : Application, new()
{
private EventHandler<ActivatedEventArgs> _onActivated, _onDeactivated;
private EventHandler<ActivatedEventArgs>? _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;
}

1
src/iOS/Avalonia.iOS/AvaloniaView.Text.cs

@ -1,4 +1,3 @@
#nullable enable
using Avalonia.Input.TextInput;
using UIKit;

187
src/iOS/Avalonia.iOS/AvaloniaView.cs

@ -1,7 +1,6 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.Versioning;
using Avalonia.Controls;
using Avalonia.Controls.Embedding;
@ -12,19 +11,20 @@ using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.iOS.Storage;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Rendering.Composition;
using CoreAnimation;
using Foundation;
using ObjCRuntime;
using OpenGLES;
using UIKit;
using IInsetsManager = Avalonia.Controls.Platform.IInsetsManager;
namespace Avalonia.iOS
{
/// <summary>
/// Root view container for Avalonia content, that can be embedded into iOS visual tree.
/// </summary>
public partial class AvaloniaView : UIView, ITextInputMethodImpl
{
internal IInputRoot InputRoot
@ -32,38 +32,73 @@ namespace Avalonia.iOS
private readonly TopLevelImpl _topLevelImpl;
private readonly EmbeddableControlRoot _topLevel;
private readonly TouchHandler _touches;
private readonly InputHandler _input;
private TextInputMethodClient? _client;
private IAvaloniaViewController? _controller;
private IInputRoot? _inputRoot;
private Metal.MetalRenderTarget? _currentRenderTarget;
public AvaloniaView()
{
_topLevelImpl = new TopLevelImpl(this);
_touches = new TouchHandler(this, _topLevelImpl);
_input = new InputHandler(this, _topLevelImpl);
_topLevel = new EmbeddableControlRoot(_topLevelImpl);
_topLevel.Prepare();
_topLevel.StartRendering();
InitEagl();
MultipleTouchEnabled = true;
InitLayerSurface();
// Remote touch handling
if (OperatingSystem.IsTvOS())
{
AddGestureRecognizer(new UISwipeGestureRecognizer(_input.Handle)
{
Direction = UISwipeGestureRecognizerDirection.Up
});
AddGestureRecognizer(new UISwipeGestureRecognizer(_input.Handle)
{
Direction = UISwipeGestureRecognizerDirection.Right
});
AddGestureRecognizer(new UISwipeGestureRecognizer(_input.Handle)
{
Direction = UISwipeGestureRecognizerDirection.Down
});
AddGestureRecognizer(new UISwipeGestureRecognizer(_input.Handle)
{
Direction = UISwipeGestureRecognizerDirection.Left
});
}
else if (OperatingSystem.IsIOS() || OperatingSystem.IsMacCatalyst())
{
#if !TVOS
MultipleTouchEnabled = true;
#endif
}
}
[ObsoletedOSPlatform("ios12.0", "Use 'Metal' instead.")]
[SupportedOSPlatform("ios")]
[UnsupportedOSPlatform("maccatalyst")]
private void InitEagl()
[SuppressMessage("Interoperability", "CA1422:Validate platform compatibility")]
private void InitLayerSurface()
{
var l = (CAEAGLLayer)Layer;
var l = Layer;
l.ContentsScale = UIScreen.MainScreen.Scale;
l.Opaque = true;
l.DrawableProperties = new NSDictionary(
EAGLDrawableProperty.RetainedBacking, false,
EAGLDrawableProperty.ColorFormat, EAGLColorFormat.RGBA8
);
_topLevelImpl.Surfaces = new[] { new EaglLayerSurface(l) };
#if !MACCATALYST
if (l is CAEAGLLayer eaglLayer)
{
eaglLayer.DrawableProperties = new NSDictionary(
OpenGLES.EAGLDrawableProperty.RetainedBacking, false,
OpenGLES.EAGLDrawableProperty.ColorFormat, OpenGLES.EAGLColorFormat.RGBA8
);
_topLevelImpl.Surfaces = new[] { new Eagl.EaglLayerSurface(eaglLayer) };
}
else
#endif
if (l is CAMetalLayer metalLayer)
{
_topLevelImpl.Surfaces = new[] { new Metal.MetalPlatformSurface(metalLayer, this) };
}
}
/// <inheritdoc />
@ -73,6 +108,12 @@ namespace Avalonia.iOS
public override bool CanResignFirstResponder => true;
/// <inheritdoc />
[ObsoletedOSPlatform("ios17.0", "Use the 'UITraitChangeObservable' protocol instead.")]
[ObsoletedOSPlatform("maccatalyst17.0", "Use the 'UITraitChangeObservable' protocol instead.")]
[ObsoletedOSPlatform("tvos17.0", "Use the 'UITraitChangeObservable' protocol instead.")]
[SupportedOSPlatform("ios")]
[SupportedOSPlatform("tvos")]
[SupportedOSPlatform("maccatalyst")]
public override void TraitCollectionDidChange(UITraitCollection? previousTraitCollection)
{
base.TraitCollectionDidChange(previousTraitCollection);
@ -101,9 +142,10 @@ namespace Avalonia.iOS
{
private readonly AvaloniaView _view;
private readonly INativeControlHostImpl _nativeControlHost;
private readonly IStorageProvider _storageProvider;
internal readonly InsetsManager _insetsManager;
private readonly ClipboardImpl _clipboard;
private readonly IStorageProvider? _storageProvider;
private readonly IClipboard? _clipboard;
private readonly IInputPane? _inputPane;
private IDisposable? _paddingInsets;
public AvaloniaView View => _view;
@ -112,8 +154,16 @@ namespace Avalonia.iOS
{
_view = view;
_nativeControlHost = new NativeControlHostImpl(view);
_storageProvider = new IOSStorageProvider(view);
_insetsManager = new InsetsManager(view);
#if TVOS
_storageProvider = null;
_clipboard = null;
_inputPane = null;
#else
_storageProvider = new Storage.IOSStorageProvider(view);
_clipboard = new ClipboardImpl();
_inputPane = UIKitInputPane.Instance;
#endif
_insetsManager = new InsetsManager();
_insetsManager.DisplayEdgeToEdgeChanged += (_, edgeToEdge) =>
{
// iOS doesn't add any paddings/margins to the application by itself.
@ -128,7 +178,6 @@ namespace Avalonia.iOS
BindingPriority.Style); // lower priority, so it can be redefined by user
}
};
_clipboard = new ClipboardImpl();
}
public void Dispose()
@ -136,7 +185,8 @@ namespace Avalonia.iOS
// No-op
}
public Compositor Compositor => Platform.Compositor;
public Compositor Compositor => Platform.Compositor
?? throw new InvalidOperationException("iOS backend wasn't initialized. Make sure UseiOS was called.");
public void Invalidate(Rect rect)
{
@ -185,8 +235,11 @@ namespace Avalonia.iOS
public void SetFrameThemeVariant(PlatformThemeVariant themeVariant)
{
#if !TVOS
// TODO adjust status bar depending on full screen mode.
if (OperatingSystem.IsIOSVersionAtLeast(13) && _view._controller is not null)
if ((OperatingSystem.IsIOSVersionAtLeast(13)
|| OperatingSystem.IsMacCatalyst())
&& _view._controller is not null)
{
_view._controller.PreferredStatusBarStyle = themeVariant switch
{
@ -195,6 +248,7 @@ namespace Avalonia.iOS
_ => UIStatusBarStyle.Default
};
}
#endif
}
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } =
@ -202,11 +256,6 @@ namespace Avalonia.iOS
public object? TryGetFeature(Type featureType)
{
if (featureType == typeof(IStorageProvider))
{
return _storageProvider;
}
if (featureType == typeof(ITextInputMethodImpl))
{
return _view;
@ -227,9 +276,14 @@ namespace Avalonia.iOS
return _clipboard;
}
if (featureType == typeof(IStorageProvider))
{
return _storageProvider;
}
if (featureType == typeof(IInputPane))
{
return UIKitInputPane.Instance;
return _inputPane;
}
return null;
@ -239,20 +293,76 @@ namespace Avalonia.iOS
[Export("layerClass")]
public static Class LayerClass()
{
return new Class(typeof(CAEAGLLayer));
#if !MACCATALYST
if (Platform.Graphics is Eagl.EaglPlatformGraphics)
{
return new Class(typeof(CAEAGLLayer));
}
else
#endif
{
return new Class(typeof(CAMetalLayer));
}
}
public override void TouchesBegan(NSSet touches, UIEvent? evt) => _touches.Handle(touches, evt);
/// <inheritdoc/>
public override void TouchesBegan(NSSet touches, UIEvent? evt) => _input.Handle(touches, evt);
/// <inheritdoc/>
public override void TouchesMoved(NSSet touches, UIEvent? evt) => _input.Handle(touches, evt);
public override void TouchesMoved(NSSet touches, UIEvent? evt) => _touches.Handle(touches, evt);
/// <inheritdoc/>
public override void TouchesEnded(NSSet touches, UIEvent? evt) => _input.Handle(touches, evt);
public override void TouchesEnded(NSSet touches, UIEvent? evt) => _touches.Handle(touches, evt);
/// <inheritdoc/>
public override void TouchesCancelled(NSSet touches, UIEvent? evt) => _input.Handle(touches, evt);
public override void TouchesCancelled(NSSet touches, UIEvent? evt) => _touches.Handle(touches, evt);
/// <inheritdoc/>
public override void PressesBegan(NSSet<UIPress> presses, UIPressesEvent evt)
{
if (!_input.Handle(presses, evt))
{
base.PressesBegan(presses, evt);
}
}
/// <inheritdoc/>
public override void PressesChanged(NSSet<UIPress> presses, UIPressesEvent evt)
{
if (!_input.Handle(presses, evt))
{
base.PressesBegan(presses, evt);
}
}
/// <inheritdoc/>
public override void PressesEnded(NSSet<UIPress> presses, UIPressesEvent evt)
{
if (!_input.Handle(presses, evt))
{
base.PressesEnded(presses, evt);
}
}
/// <inheritdoc/>
public override void PressesCancelled(NSSet<UIPress> presses, UIPressesEvent evt)
{
if (!_input.Handle(presses, evt))
{
base.PressesCancelled(presses, evt);
}
}
/// <inheritdoc/>
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 +371,10 @@ namespace Avalonia.iOS
get => (Control?)_topLevel.Content;
set => _topLevel.Content = value;
}
internal void SetRenderTarget(Metal.MetalRenderTarget target)
{
_currentRenderTarget = target;
}
}
}

41
src/iOS/Avalonia.iOS/ClipboardImpl.cs

@ -1,19 +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<string> GetTextAsync()
public Task<string?> GetTextAsync()
{
return Task.FromResult(UIPasteboard.General.String);
}
public Task SetTextAsync(string text)
public Task SetTextAsync(string? text)
{
UIPasteboard.General.String = text;
return Task.CompletedTask;
@ -21,14 +24,40 @@ namespace Avalonia.iOS
public Task ClearAsync()
{
UIPasteboard.General.String = "";
UIPasteboard.General.Items = Array.Empty<NSDictionary>();
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<string[]> GetFormatsAsync()
{
var formats = new List<string>();
if (UIPasteboard.General.HasStrings)
{
formats.Add(DataFormats.Text);
}
public Task<string[]> GetFormatsAsync() => Task.FromResult(Array.Empty<string>());
return Task.FromResult(formats.ToArray());
}
public Task<object> GetDataAsync(string format) => Task.FromResult<object>(null);
public Task<object?> GetDataAsync(string format)
{
if (format == DataFormats.Text)
{
return Task.FromResult<object?>(UIPasteboard.General.String);
}
return Task.FromResult<object?>(null);
}
}
}
#endif

2
src/iOS/Avalonia.iOS/DispatcherImpl.cs

@ -1,5 +1,3 @@
#nullable enable
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

4
src/iOS/Avalonia.iOS/DisplayLinkTimer.cs

@ -11,7 +11,7 @@ namespace Avalonia.iOS
{
class DisplayLinkTimer : IRenderTimer
{
public event Action<TimeSpan> Tick;
public event Action<TimeSpan>? Tick;
private Stopwatch _st = Stopwatch.StartNew();
public DisplayLinkTimer()
@ -36,4 +36,4 @@ namespace Avalonia.iOS
Tick?.Invoke(_st.Elapsed);
}
}
}
}

41
src/iOS/Avalonia.iOS/EaglDisplay.cs → src/iOS/Avalonia.iOS/Eagl/EaglDisplay.cs

@ -1,15 +1,20 @@
#if !MACCATALYST
using System;
using System.Collections.Generic;
using System.Runtime.Versioning;
using Avalonia.Logging;
using Avalonia.OpenGL;
using Avalonia.Platform;
using Avalonia.Reactive;
using OpenGLES;
namespace Avalonia.iOS
namespace Avalonia.iOS.Eagl
{
[ObsoletedOSPlatform("ios12.0", "Use 'Metal' instead.")]
[ObsoletedOSPlatform("tvos12.0", "Use 'Metal' instead.")]
[UnsupportedOSPlatform("maccatalyst")]
[SupportedOSPlatform("ios")]
[SupportedOSPlatform("tvos")]
class EaglPlatformGraphics : IPlatformGraphics
{
public IPlatformGraphicsContext GetSharedContext() => Context;
@ -19,7 +24,7 @@ namespace Avalonia.iOS
public GlContext Context { get; }
public static GlVersion GlVersion { get; } = new(GlProfileType.OpenGLES, 3, 0);
public EaglPlatformGraphics()
private EaglPlatformGraphics()
{
const string path = "/System/Library/Frameworks/OpenGLES.framework/OpenGLES";
@ -29,15 +34,30 @@ namespace Avalonia.iOS
var iface = new GlInterface(GlVersion, proc => ObjCRuntime.Dlfcn.dlsym(libGl, proc));
Context = new(iface, null);
}
public static EaglPlatformGraphics? TryCreate()
{
try
{
return new EaglPlatformGraphics();
}
catch(Exception e)
{
Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log(null, "Unable to initialize EAGL-based rendering: {0}", e);
return null;
}
}
}
[ObsoletedOSPlatform("ios12.0", "Use 'Metal' instead.")]
[ObsoletedOSPlatform("tvos12.0", "Use 'Metal' instead.")]
[SupportedOSPlatform("ios")]
[SupportedOSPlatform("tvos")]
class GlContext : IGlContext
{
public EAGLContext Context { get; private set; }
public EAGLContext? Context { get; private set; }
public GlContext(GlInterface glInterface, EAGLSharegroup sharegroup)
public GlContext(GlInterface glInterface, EAGLSharegroup? sharegroup)
{
GlInterface = glInterface;
Context = sharegroup == null ?
@ -53,10 +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;
}
@ -87,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();
}
@ -95,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<GlVersion> preferredVersions = null)
public IGlContext CreateSharedContext(IEnumerable<GlVersion>? preferredVersions = null)
{
if (Context == null)
throw new PlatformGraphicsContextLostException();
return new GlContext(GlInterface, Context.ShareGroup);
}
@ -119,6 +141,7 @@ namespace Avalonia.iOS
}
}
public object TryGetFeature(Type featureType) => null;
public object? TryGetFeature(Type featureType) => null;
}
}
#endif

14
src/iOS/Avalonia.iOS/EaglLayerSurface.cs → src/iOS/Avalonia.iOS/Eagl/EaglLayerSurface.cs

@ -1,4 +1,4 @@
#if !MACCATALYST
using System;
using System.Runtime.Versioning;
using System.Threading;
@ -6,10 +6,13 @@ using Avalonia.OpenGL;
using Avalonia.OpenGL.Surfaces;
using CoreAnimation;
namespace Avalonia.iOS
namespace Avalonia.iOS.Eagl
{
[ObsoletedOSPlatform("ios12.0", "Use 'Metal' instead.")]
[ObsoletedOSPlatform("tvos12.0", "Use 'Metal' instead.")]
[SupportedOSPlatform("ios")]
[SupportedOSPlatform("tvos")]
[UnsupportedOSPlatform("maccatalyst")]
class EaglLayerSurface : IGlPlatformSurface
{
private readonly CAEAGLLayer _layer;
@ -77,19 +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 = Platform.GlFeature.Context;
var ctx = ((EaglPlatformGraphics)Platform.Graphics!).Context;
if (ctx != context)
throw new InvalidOperationException("Platform surface is only usable with tha main context");
using (ctx.MakeCurrent())
{
var fbo = new SizeSynchronizedLayerFbo(ctx.Context, ctx.GlInterface, _layer);
var fbo = new SizeSynchronizedLayerFbo(ctx.Context!, ctx.GlInterface, _layer);
if (!fbo.Sync())
throw new InvalidOperationException("Unable to create render target");
return new RenderTarget(ctx, fbo);
@ -97,3 +100,4 @@ namespace Avalonia.iOS
}
}
}
#endif

20
src/iOS/Avalonia.iOS/LayerFbo.cs → src/iOS/Avalonia.iOS/Eagl/LayerFbo.cs

@ -1,13 +1,17 @@
#if !MACCATALYST
using System;
using System.Runtime.Versioning;
using Avalonia.OpenGL;
using CoreAnimation;
using OpenGLES;
namespace Avalonia.iOS
namespace Avalonia.iOS.Eagl
{
[ObsoletedOSPlatform("ios12.0", "Use 'Metal' instead.")]
[ObsoletedOSPlatform("tvos12.0", "Use 'Metal' instead.")]
[SupportedOSPlatform("ios")]
[SupportedOSPlatform("tvos")]
[UnsupportedOSPlatform("maccatalyst")]
internal class LayerFbo
{
private readonly EAGLContext _context;
@ -28,7 +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;
@ -77,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()
@ -94,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)
@ -138,13 +145,14 @@ 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;
public double Scaling => _oldLayerScale;
}
}
#endif

357
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<UITouch, long> _knownTouches = new Dictionary<UITouch, long>();
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<UIPress> 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<UIKeyboardHidUsage, PhysicalKey> 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,
};
}

8
src/iOS/Avalonia.iOS/InsetsManager.cs

@ -1,22 +1,14 @@
using System;
using Avalonia.Controls.Platform;
using Avalonia.Media;
using UIKit;
namespace Avalonia.iOS;
#nullable enable
internal class InsetsManager : IInsetsManager
{
private readonly AvaloniaView _view;
private IAvaloniaViewController? _controller;
private bool _displayEdgeToEdge = true;
public InsetsManager(AvaloniaView view)
{
_view = view;
}
internal void InitWithController(IAvaloniaViewController controller)
{
_controller = controller;

34
src/iOS/Avalonia.iOS/Metal/MetalDevice.cs

@ -0,0 +1,34 @@
using System;
using System.Runtime.Versioning;
using Avalonia.Metal;
using Avalonia.Utilities;
using Metal;
namespace Avalonia.iOS.Metal;
internal class MetalDevice : IMetalDevice
{
private readonly DisposableLock _syncRoot = new();
public MetalDevice(IMTLDevice device)
{
Device = device;
Queue = device.CreateCommandQueue()
?? throw new InvalidOperationException("IMTLCommandQueue is not available");
}
public IMTLDevice Device { get; }
public IMTLCommandQueue Queue { get; }
IntPtr IMetalDevice.Device => Device.Handle;
IntPtr IMetalDevice.CommandQueue => Queue.Handle;
public bool IsLost => false;
public IDisposable EnsureCurrent() => _syncRoot.Lock();
public object? TryGetFeature(Type featureType) => null;
public void Dispose()
{
Queue.Dispose();
}
}

34
src/iOS/Avalonia.iOS/Metal/MetalDrawingSession.cs

@ -0,0 +1,34 @@
using System;
using Avalonia.Metal;
using CoreAnimation;
namespace Avalonia.iOS.Metal;
internal class MetalDrawingSession : IMetalPlatformSurfaceRenderingSession
{
private readonly MetalDevice _device;
private readonly ICAMetalDrawable _drawable;
public MetalDrawingSession(MetalDevice device, ICAMetalDrawable drawable, PixelSize size, double scaling)
{
_device = device;
_drawable = drawable;
Size = size;
Scaling = scaling;
Texture = _drawable.Texture.Handle;
}
public void Dispose()
{
var buffer = _device.Queue.CommandBuffer();
buffer!.PresentDrawable(_drawable);
buffer.Commit();
}
public IntPtr Texture { get; }
public PixelSize Size { get; }
public double Scaling { get; }
public bool IsYFlipped => false;
}

49
src/iOS/Avalonia.iOS/Metal/MetalPlatformGraphics.cs

@ -0,0 +1,49 @@
using System;
using System.Runtime.Versioning;
using Avalonia.Platform;
using Metal;
using SkiaSharp;
namespace Avalonia.iOS.Metal;
[SupportedOSPlatform("ios")]
[SupportedOSPlatform("macos")]
[SupportedOSPlatform("maccatalyst")]
[SupportedOSPlatform("tvos")]
internal class MetalPlatformGraphics : IPlatformGraphics
{
private readonly IMTLDevice _defaultDevice;
private MetalPlatformGraphics(IMTLDevice defaultDevice)
{
_defaultDevice = defaultDevice;
}
public bool UsesSharedContext => false;
public IPlatformGraphicsContext CreateContext() => new MetalDevice(_defaultDevice);
public IPlatformGraphicsContext GetSharedContext() => throw new NotSupportedException();
public static MetalPlatformGraphics? TryCreate()
{
var device = MTLDevice.SystemDefault;
if (device is null)
{
// Can be null on unsupported OS versions.
return null;
}
#if !TVOS
using var queue = device.CreateCommandQueue();
using var context = GRContext.CreateMetal(new GRMtlBackendContext { Device = device, Queue = queue });
if (context is null)
{
// Can be null on macCatalyst because of older Skia bug.
// Fixed in SkiaSharp 3.0
return null;
}
#endif
return new MetalPlatformGraphics(device);
}
}

25
src/iOS/Avalonia.iOS/Metal/MetalPlatformSurface.cs

@ -0,0 +1,25 @@
using Avalonia.Metal;
using CoreAnimation;
namespace Avalonia.iOS.Metal;
internal class MetalPlatformSurface : IMetalPlatformSurface
{
private readonly CAMetalLayer _layer;
private readonly AvaloniaView _avaloniaView;
public MetalPlatformSurface(CAMetalLayer layer, AvaloniaView avaloniaView)
{
_layer = layer;
_avaloniaView = avaloniaView;
}
public IMetalPlatformSurfaceRenderTarget CreateMetalRenderTarget(IMetalDevice device)
{
var dev = (MetalDevice)device;
_layer.Device = dev.Device;
var target = new MetalRenderTarget(_layer, dev);
_avaloniaView.SetRenderTarget(target);
return target;
}
}

40
src/iOS/Avalonia.iOS/Metal/MetalRenderTarget.cs

@ -0,0 +1,40 @@
using Avalonia.Metal;
using Avalonia.Platform;
using CoreAnimation;
using CoreGraphics;
namespace Avalonia.iOS.Metal;
internal class MetalRenderTarget : IMetalPlatformSurfaceRenderTarget
{
private readonly CAMetalLayer _layer;
private readonly MetalDevice _device;
private 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);
}
}

4
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;

77
src/iOS/Avalonia.iOS/Platform.cs

@ -1,9 +1,8 @@
using System;
using Avalonia.Controls;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.OpenGL;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
@ -11,6 +10,33 @@ using Avalonia.Threading;
namespace Avalonia
{
public enum iOSRenderingMode
{
/// <summary>
/// Enables EaGL rendering for iOS and tvOS. Not supported on macCatalyst.
/// </summary>
OpenGl = 1,
/// <summary>
/// Enables Metal rendering for all apple targets. Not stable and currently only works on iOS.
/// </summary>
Metal
}
public class iOSPlatformOptions
{
/// <summary>
/// Gets or sets Avalonia rendering modes with fallbacks.
/// The first element in the array has the highest priority.
/// The default value is: <see cref="iOSRenderingMode.OpenGl"/>.
/// </summary>
/// <exception cref="System.InvalidOperationException">Thrown if no values were matched.</exception>
public IReadOnlyList<iOSRenderingMode> RenderingMode { get; set; } = new[]
{
iOSRenderingMode.OpenGl
};
}
public static class IOSApplicationExtensions
{
public static AppBuilder UseiOS(this AppBuilder builder)
@ -27,18 +53,21 @@ namespace Avalonia.iOS
{
static class Platform
{
public static EaglPlatformGraphics GlFeature;
public static DisplayLinkTimer Timer;
internal static Compositor Compositor { get; private set; }
public static iOSPlatformOptions? Options;
public static IPlatformGraphics? Graphics;
public static DisplayLinkTimer? Timer;
internal static Compositor? Compositor { get; private set; }
public static void Register()
{
GlFeature ??= new EaglPlatformGraphics();
Options = AvaloniaLocator.Current.GetService<iOSPlatformOptions>() ?? new iOSPlatformOptions();
Graphics = InitializeGraphics(Options);
Timer ??= new DisplayLinkTimer();
var keyboard = new KeyboardDevice();
AvaloniaLocator.CurrentMutable
.Bind<IPlatformGraphics>().ToConstant((IPlatformGraphics) GlFeature)
.Bind<IPlatformGraphics>().ToConstant(Graphics)
.Bind<ICursorFactory>().ToConstant(new CursorFactoryStub())
.Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformStub())
.Bind<IPlatformSettings>().ToSingleton<PlatformSettings>()
@ -48,7 +77,37 @@ namespace Avalonia.iOS
.Bind<IDispatcherImpl>().ToConstant(DispatcherImpl.Instance)
.Bind<IKeyboardDevice>().ToConstant(keyboard);
Compositor = new Compositor(AvaloniaLocator.Current.GetService<IPlatformGraphics>());
Compositor = new Compositor(AvaloniaLocator.Current.GetService<IPlatformGraphics>());
}
private static IPlatformGraphics InitializeGraphics(iOSPlatformOptions opts)
{
if (opts.RenderingMode is null || !opts.RenderingMode.Any())
{
throw new InvalidOperationException($"{nameof(iOSPlatformOptions)}.{nameof(iOSPlatformOptions.RenderingMode)} must not be empty or null");
}
foreach (var renderingMode in opts.RenderingMode)
{
#if !MACCATALYST
if (renderingMode == iOSRenderingMode.OpenGl
&& !OperatingSystem.IsMacCatalyst()
#pragma warning disable CA1422
&& Eagl.EaglPlatformGraphics.TryCreate() is { } eaglGraphics)
#pragma warning restore CA1422
{
return eaglGraphics;
}
#endif
if (renderingMode == iOSRenderingMode.Metal
&& Metal.MetalPlatformGraphics.TryCreate() is { } metalGraphics)
{
return metalGraphics;
}
}
throw new InvalidOperationException($"{nameof(iOSPlatformOptions)}.{nameof(iOSPlatformOptions.RenderingMode)} has a value of \"{string.Join(", ", opts.RenderingMode)}\", but no options were applied.");
}
}
}

1
src/iOS/Avalonia.iOS/PlatformSettings.cs

@ -1,4 +1,3 @@
#nullable enable
using System;
using Avalonia.Media;
using Avalonia.Platform;

12
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<ActivatedEventArgs> Activated;
public event EventHandler<ActivatedEventArgs> Deactivated;
public event EventHandler<ActivatedEventArgs>? Activated;
public event EventHandler<ActivatedEventArgs>? Deactivated;
public bool TryLeaveBackground() => false;
public bool TryEnterBackground() => false;
}

4
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

19
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;
@ -9,8 +10,6 @@ using Foundation;
using UIKit;
#nullable enable
namespace Avalonia.iOS.Storage;
internal abstract class IOSStorageItem : IStorageBookmarkItem
@ -56,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);
}
@ -82,7 +84,7 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem
}
}
public async Task<IStorageItem?> MoveAsync(IStorageFolder destination)
public Task<IStorageItem?> MoveAsync(IStorageFolder destination)
{
if (destination is not IOSStorageFolder folder)
{
@ -99,9 +101,9 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem
if (NSFileManager.DefaultManager.Move(Url, newPath, out var error))
{
return isDir
return Task.FromResult<IStorageItem?>(isDir
? new IOSStorageFolder(newPath)
: new IOSStorageFile(newPath);
: new IOSStorageFile(newPath));
}
if (error is not null)
@ -109,7 +111,7 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem
throw new NSErrorException(error);
}
return null;
return Task.FromResult<IStorageItem?>(null);
}
finally
{
@ -275,3 +277,4 @@ internal sealed class IOSStorageFolder : IOSStorageItem, IStorageBookmarkFolder
}
}
}
#endif

10
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;
@ -12,8 +13,6 @@ using UniformTypeIdentifiers;
using UTTypeLegacy = MobileCoreServices.UTType;
using UTType = UniformTypeIdentifiers.UTType;
#nullable enable
namespace Avalonia.iOS.Storage;
internal class IOSStorageProvider : IStorageProvider
@ -68,8 +67,10 @@ internal class IOSStorageProvider : IStorageProvider
var allowedUtils = options.FileTypeFilter?.SelectMany(f => f.AppleUniformTypeIdentifiers ?? Array.Empty<string>())
.ToArray() ?? new[]
{
#pragma warning disable CA1422
UTTypeLegacy.Content,
UTTypeLegacy.Item,
#pragma warning restore CA1422
"public.data"
};
documentPicker = new UIDocumentPickerViewController(allowedUtils, UIDocumentPickerMode.Open);
@ -148,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))
{
@ -244,3 +247,4 @@ internal class IOSStorageProvider : IStorageProvider
}
}
}
#endif

3
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

1
src/iOS/Avalonia.iOS/TextInputResponder.Properties.cs

@ -1,4 +1,3 @@
#nullable enable
using Avalonia.Input.TextInput;
using Foundation;
using UIKit;

9
src/iOS/Avalonia.iOS/TextInputResponder.cs

@ -14,8 +14,6 @@ using UIKit;
namespace Avalonia.iOS;
#nullable enable
partial class AvaloniaView
{
@ -108,7 +106,12 @@ partial class AvaloniaView
{
get
{
var mode = UITextInputMode.CurrentInputMode;
UITextInputMode? mode = null;
#if !TVOS
#pragma warning disable CA1422
mode = UITextInputMode.CurrentInputMode;
#pragma warning restore CA1422
#endif
// Can be empty see https://developer.apple.com/documentation/uikit/uitextinputmode/1614522-activeinputmodes
if (mode is null && UITextInputMode.ActiveInputModes.Length > 0)
{

52
src/iOS/Avalonia.iOS/TouchHandler.cs

@ -1,52 +0,0 @@
using System.Collections.Generic;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Platform;
using Foundation;
using UIKit;
namespace Avalonia.iOS
{
class TouchHandler
{
private readonly AvaloniaView _view;
private readonly ITopLevelImpl _tl;
public TouchDevice _device = new TouchDevice();
public TouchHandler(AvaloniaView view, ITopLevelImpl tl)
{
_view = view;
_tl = tl;
}
static ulong Ts(UIEvent evt) => (ulong) (evt.Timestamp * 1000);
private IInputRoot Root => _view.InputRoot;
private static long _nextTouchPointId = 1;
private Dictionary<UITouch, long> _knownTouches = new Dictionary<UITouch, long>();
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);
}
}
}
}

14
src/iOS/Avalonia.iOS/UIKitInputPane.cs

@ -1,13 +1,17 @@
#if !TVOS
using System;
using System.Diagnostics;
using System.Runtime.Versioning;
using Avalonia.Animation.Easings;
using Avalonia.Controls.Platform;
using Foundation;
using UIKit;
#nullable enable
namespace Avalonia.iOS;
[UnsupportedOSPlatform("tvos")]
[SupportedOSPlatform("maccatalyst")]
[SupportedOSPlatform("ios")]
internal sealed class UIKitInputPane : IInputPane
{
public static UIKitInputPane Instance { get; } = new();
@ -33,7 +37,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 +58,7 @@ internal sealed class UIKitInputPane : IInputPane
StateChanged?.Invoke(this, new InputPaneStateEventArgs(
State, startRect, OccludedRect, TimeSpan.FromSeconds(duration), easing));
#endif
}
}
#endif

12
src/iOS/Avalonia.iOS/ViewController.cs

@ -7,16 +7,20 @@ namespace Avalonia.iOS;
[Unstable]
public interface IAvaloniaViewController
{
#if !TVOS
UIStatusBarStyle PreferredStatusBarStyle { get; set; }
#endif
bool PrefersStatusBarHidden { get; set; }
Thickness SafeAreaPadding { get; }
event EventHandler SafeAreaPaddingChanged;
event EventHandler? SafeAreaPaddingChanged;
}
/// <inheritdoc cref="IAvaloniaViewController" />
public class DefaultAvaloniaViewController : UIViewController, IAvaloniaViewController
{
#if !TVOS
private UIStatusBarStyle? _preferredStatusBarStyle;
#endif
private bool? _prefersStatusBarHidden;
/// <inheritdoc/>
@ -33,6 +37,7 @@ public class DefaultAvaloniaViewController : UIViewController, IAvaloniaViewCont
}
}
#if !TVOS
/// <inheritdoc/>
public override bool PrefersStatusBarHidden()
{
@ -55,6 +60,7 @@ public class DefaultAvaloniaViewController : UIViewController, IAvaloniaViewCont
SetNeedsStatusBarAppearanceUpdate();
}
}
#endif
bool IAvaloniaViewController.PrefersStatusBarHidden
{
@ -62,7 +68,9 @@ public class DefaultAvaloniaViewController : UIViewController, IAvaloniaViewCont
set
{
_prefersStatusBarHidden = value;
#if !TVOS
SetNeedsStatusBarAppearanceUpdate();
#endif
}
}
@ -70,5 +78,5 @@ public class DefaultAvaloniaViewController : UIViewController, IAvaloniaViewCont
public Thickness SafeAreaPadding { get; private set; }
/// <inheritdoc/>
public event EventHandler SafeAreaPaddingChanged;
public event EventHandler? SafeAreaPaddingChanged;
}

39
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<TestClass>
{
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<object>().Single());
Assert.Same(items[2], e.AddedItems.Cast<object>().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<object>().Single());
Assert.Same(items[2], e.RemovedItems.Cast<object>().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<TestClass>
{
new TestClass(null, null),
new TestClass("Item1", "Alt1"),
new TestClass("Item2", "Alt2"),
new TestClass("Item3", "Alt3"),

Loading…
Cancel
Save