Browse Source

Merge pull request #4585 from AvaloniaUI/feature/ios-rework-2020

New iOS backend
Nikita Tsukanov 5 years ago
parent
commit
8f6437361b
35 changed files with 1381 additions and 756 deletions
  1. 2 3
      Avalonia.sln
  2. 3 20
      samples/ControlCatalog.iOS/AppDelegate.cs
  3. 1 0
      samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj
  4. 14 3
      src/iOS/Avalonia.iOS/Avalonia.iOS.csproj
  5. 49 0
      src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs
  6. 0 58
      src/iOS/Avalonia.iOS/AvaloniaRootViewController.cs
  7. 32 0
      src/iOS/Avalonia.iOS/AvaloniaView.Text.cs
  8. 117 26
      src/iOS/Avalonia.iOS/AvaloniaView.cs
  9. 0 26
      src/iOS/Avalonia.iOS/AvaloniaWindow.cs
  10. 4 3
      src/iOS/Avalonia.iOS/Boilerplate/AppBuilder.cs
  11. 19 0
      src/iOS/Avalonia.iOS/Boilerplate/RuntimePlatform.cs
  12. 595 0
      src/iOS/Avalonia.iOS/Boilerplate/Shared.cs
  13. 2 2
      src/iOS/Avalonia.iOS/ClipboardImpl.cs
  14. 0 11
      src/iOS/Avalonia.iOS/CursorFactory.cs
  15. 0 32
      src/iOS/Avalonia.iOS/DisplayLinkRenderTimer.cs
  16. 37 0
      src/iOS/Avalonia.iOS/DisplayLinkTimer.cs
  17. 69 0
      src/iOS/Avalonia.iOS/EaglDisplay.cs
  18. 94 0
      src/iOS/Avalonia.iOS/EaglLayerSurface.cs
  19. 0 23
      src/iOS/Avalonia.iOS/EmbeddableImpl.cs
  20. 0 60
      src/iOS/Avalonia.iOS/EmulatedFramebuffer.cs
  21. 2 2
      src/iOS/Avalonia.iOS/Extensions.cs
  22. 143 0
      src/iOS/Avalonia.iOS/LayerFbo.cs
  23. 50 0
      src/iOS/Avalonia.iOS/Platform.cs
  24. 0 46
      src/iOS/Avalonia.iOS/PlatformIconLoader.cs
  25. 0 14
      src/iOS/Avalonia.iOS/PlatformSettings.cs
  26. 5 28
      src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs
  27. 0 17
      src/iOS/Avalonia.iOS/RuntimeInfo.cs
  28. 7 0
      src/iOS/Avalonia.iOS/SingleViewLifetime.cs
  29. 24 0
      src/iOS/Avalonia.iOS/SoftKeyboardHelper.cs
  30. 0 152
      src/iOS/Avalonia.iOS/Specific/KeyboardEventsHelper.cs
  31. 60 0
      src/iOS/Avalonia.iOS/Stubs.cs
  32. 0 158
      src/iOS/Avalonia.iOS/TopLevelImpl.cs
  33. 52 0
      src/iOS/Avalonia.iOS/TouchHandler.cs
  34. 0 23
      src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs
  35. 0 49
      src/iOS/Avalonia.iOS/iOSPlatform.cs

+ 2 - 3
Avalonia.sln

@@ -122,6 +122,7 @@ EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1-27F5-4255-9AFC-04ABFD11683A}"
 	ProjectSection(SolutionItems) = preProject
 		build\AndroidWorkarounds.props = build\AndroidWorkarounds.props
+		build\ApiDiff.props = build\ApiDiff.props
 		build\Base.props = build\Base.props
 		build\Binding.props = build\Binding.props
 		build\CoreLibraries.props = build\CoreLibraries.props
@@ -148,14 +149,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
 		build\System.Memory.props = build\System.Memory.props
 		build\UnitTests.NetFX.props = build\UnitTests.NetFX.props
 		build\XUnit.props = build\XUnit.props
-		build\ApiDiff.props = build\ApiDiff.props
 	EndProjectSection
 EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Targets", "Targets", "{4D6FAF79-58B4-482F-9122-0668C346364C}"
 	ProjectSection(SolutionItems) = preProject
-		build\UnitTests.NetCore.targets = build\UnitTests.NetCore.targets
 		build\BuildTargets.targets = build\BuildTargets.targets
 		build\LegacyProject.targets = build\LegacyProject.targets
+		build\UnitTests.NetCore.targets = build\UnitTests.NetCore.targets
 	EndProjectSection
 EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Linux", "Linux", "{86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}"
@@ -226,7 +226,6 @@ Global
 	GlobalSection(SharedMSBuildProjectFiles) = preSolution
 		src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13
 		src\Shared\RenderHelpers\RenderHelpers.projitems*{3e908f67-5543-4879-a1dc-08eace79b3cd}*SharedItemsImports = 5
-		src\Shared\PlatformSupport\PlatformSupport.projitems*{4488ad85-1495-4809-9aa4-ddfe0a48527e}*SharedItemsImports = 5
 		src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 5
 		src\Shared\RenderHelpers\RenderHelpers.projitems*{7d2d3083-71dd-4cc9-8907-39a0d86fb322}*SharedItemsImports = 5
 		src\Shared\PlatformSupport\PlatformSupport.projitems*{88060192-33d5-4932-b0f9-8bd2763e857d}*SharedItemsImports = 5

+ 3 - 20
samples/ControlCatalog.iOS/AppDelegate.cs

@@ -11,25 +11,8 @@ namespace ControlCatalog
     // User Interface of the application, as well as listening (and optionally responding) to 
     // application events from iOS.
     [Register("AppDelegate")]
-    public partial class AppDelegate : UIApplicationDelegate
+    public partial class AppDelegate : AvaloniaAppDelegate<App>
     {
-        public override UIWindow Window { get; set; }
-
-        //
-        // This method is invoked when the application has loaded and is ready to run. In this 
-        // method you should instantiate the window, load the UI into it and then make the window
-        // visible.
-        //
-        // You have 17 seconds to return from this method, or iOS will terminate your application.
-        //
-        public override bool FinishedLaunching(UIApplication uiapp, NSDictionary options)
-        {
-            AppBuilder.Configure<App>()
-                .UseiOS()
-                .UseSkia().SetupWithoutStarting();
-            Window = new AvaloniaWindow() {Content = new MainView(), StatusBarColor = Colors.LightSteelBlue};
-            Window.MakeKeyAndVisible();
-            return true;
-        }
+        
     }
-}
+}

+ 1 - 0
samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj

@@ -176,4 +176,5 @@
   </ItemGroup>
   <Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
   <Import Project="..\..\build\LegacyProject.targets" />
+  <Import Project="..\..\build\SkiaSharp.props" />
 </Project>

+ 14 - 3
src/iOS/Avalonia.iOS/Avalonia.iOS.csproj

@@ -1,13 +1,24 @@
-<Project>
+<Project>
   <Import Project="Sdk.props" Sdk="MSBuild.Sdk.Extras" />
   <PropertyGroup>
     <TargetFramework>xamarin.ios10</TargetFramework>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <LangVersion>latest</LangVersion>
   </PropertyGroup>
+  <ItemGroup>
+    <Compile Update="Boilerplate\Shared.cs">
+      <SubType>Code</SubType>
+    </Compile>
+    <Compile Update="Boilerplate\RuntimePlatform.cs">
+      <SubType>Code</SubType>
+    </Compile>
+  </ItemGroup>
+  <ItemGroup>
+    <Reference Include="OpenTK-1.0" />
+  </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
+    <ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
   </ItemGroup>
-  <Import Project="..\..\Shared\PlatformSupport\PlatformSupport.projitems" Label="Shared" />
-  <Import Project="..\..\..\build\Rx.props" />
   <Import Project="Sdk.targets" Sdk="MSBuild.Sdk.Extras" />
 </Project>

+ 49 - 0
src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs

@@ -0,0 +1,49 @@
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Foundation;
+using UIKit;
+
+namespace Avalonia.iOS
+{
+    public class AvaloniaAppDelegate<TApp> : UIResponder, IUIApplicationDelegate
+        where TApp : Application, new()
+    {
+        protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder;
+        
+        [Export("window")]
+        public UIWindow Window { get; set; }
+
+        [Export("application:didFinishLaunchingWithOptions:")]
+        public bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
+        {
+            var builder = AppBuilder.Configure<TApp>();
+            CustomizeAppBuilder(builder);
+            var lifetime = new Lifetime();
+            builder.AfterSetup(_ =>
+            {
+                Window = new UIWindow();
+                var view = new AvaloniaView();
+                lifetime.View = view;
+                Window.RootViewController = new UIViewController
+                {
+                    View = view
+                };
+            });
+            
+            builder.SetupWithLifetime(lifetime);
+            
+            Window.Hidden = false;
+            return true;
+        }
+
+        class Lifetime : ISingleViewApplicationLifetime
+        {
+            public AvaloniaView View;
+            public Control MainView
+            {
+                get => View.Content;
+                set => View.Content = value;
+            }
+        }
+    }
+}

+ 0 - 58
src/iOS/Avalonia.iOS/AvaloniaRootViewController.cs

@@ -1,58 +0,0 @@
-using Avalonia.Media;
-using CoreGraphics;
-using UIKit;
-
-namespace Avalonia.iOS
-{
-    class AvaloniaRootViewController : UIViewController
-    {
-        private object _content;
-        private Color _statusBarColor = Colors.White;
-
-        public object Content
-        {
-            get { return _content; }
-            set
-            {
-                _content = value;
-                var view = (View as AvaloniaView);
-                if (view != null)
-                    view.Content = value;
-            }
-        }
-
-        public Color StatusBarColor
-        {
-            get { return _statusBarColor; }
-            set
-            {
-                _statusBarColor = value;
-                var view = (View as AvaloniaView);
-                if (view != null)
-                    view.BackgroundColor = value.ToUiColor();
-            }
-        }
-
-        void AutoFit()
-        {
-            var needFlip = !UIDevice.CurrentDevice.CheckSystemVersion(8, 0) &&
-               (InterfaceOrientation == UIInterfaceOrientation.LandscapeLeft
-                || InterfaceOrientation == UIInterfaceOrientation.LandscapeRight);
-            // Bounds here (if top level) needs to correspond with the rendertarget 
-            var frame = UIScreen.MainScreen.Bounds;
-            if (needFlip)
-                frame = new CGRect(frame.Y, frame.X, frame.Height, frame.Width);
-            ((AvaloniaView) View).Padding =
-                new Thickness(0, UIApplication.SharedApplication.StatusBarFrame.Size.Height, 0, 0);
-            View.Frame = frame;
-        }
-
-        public override void LoadView()
-        {
-            View = new AvaloniaView() {Content = Content, BackgroundColor = _statusBarColor.ToUiColor()};
-            UIApplication.Notifications.ObserveDidChangeStatusBarOrientation(delegate { AutoFit(); });
-            UIApplication.Notifications.ObserveDidChangeStatusBarFrame(delegate { AutoFit(); });
-            AutoFit();
-        }
-    }
-}

+ 32 - 0
src/iOS/Avalonia.iOS/AvaloniaView.Text.cs

@@ -0,0 +1,32 @@
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Foundation;
+using ObjCRuntime;
+using UIKit;
+
+namespace Avalonia.iOS
+{
+    [Adopts("UIKeyInput")]
+    public partial class AvaloniaView
+    {
+        public override bool CanBecomeFirstResponder => true;
+
+        [Export("hasText")] public bool HasText => false;
+
+        [Export("insertText:")]
+        public void InsertText(string text) =>
+            _topLevelImpl.Input?.Invoke(new RawTextInputEventArgs(KeyboardDevice.Instance,
+                0, InputRoot, text));
+
+        [Export("deleteBackward")]
+        public void DeleteBackward()
+        {
+            // TODO: pass this through IME infrastructure instead of emulating a backspace press
+            _topLevelImpl.Input?.Invoke(new RawKeyEventArgs(KeyboardDevice.Instance,
+                0, InputRoot, RawKeyEventType.KeyDown, Key.Back, RawInputModifiers.None));
+            
+            _topLevelImpl.Input?.Invoke(new RawKeyEventArgs(KeyboardDevice.Instance,
+                0, InputRoot, RawKeyEventType.KeyUp, Key.Back, RawInputModifiers.None));
+        }
+    }
+}

+ 117 - 26
src/iOS/Avalonia.iOS/AvaloniaView.cs

@@ -1,48 +1,139 @@
-using Avalonia.Controls.Embedding;
+using System;
+using System.Collections.Generic;
+using Avalonia.Controls;
+using Avalonia.Controls.Embedding;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Platform;
+using Avalonia.Rendering;
+using CoreAnimation;
 using CoreGraphics;
+using Foundation;
+using ObjCRuntime;
+using OpenGLES;
 using UIKit;
 
 namespace Avalonia.iOS
 {
-    public class AvaloniaView : UIView
+    public partial class AvaloniaView : UIView
     {
-        private EmbeddableImpl _impl;
-        private EmbeddableControlRoot _root;
-        private Thickness _padding;
+        internal IInputRoot InputRoot { get; private set; }
+        private TopLevelImpl _topLevelImpl;
+        private EmbeddableControlRoot _topLevel;
+        private TouchHandler _touches;
 
-        public Thickness Padding
+        public AvaloniaView()
+        {
+            _topLevelImpl = new TopLevelImpl(this);
+            _touches = new TouchHandler(this, _topLevelImpl);
+            _topLevel = new EmbeddableControlRoot(_topLevelImpl);
+            _topLevel.Prepare();
+            
+            _topLevel.Renderer.Start();
+            
+            var l = (CAEAGLLayer) 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)};
+            MultipleTouchEnabled = true;
+        }
+
+        internal class TopLevelImpl : ITopLevelImpl
         {
-            get { return _padding; }
-            set
+            private readonly AvaloniaView _view;
+            public AvaloniaView View => _view;
+
+            public TopLevelImpl(AvaloniaView view)
+            {
+                _view = view;
+            }
+
+            public void Dispose()
             {
-                _padding = value;
-                SetNeedsLayout();
+                // No-op
             }
+
+            public IRenderer CreateRenderer(IRenderRoot root) => new DeferredRenderer(root,
+                AvaloniaLocator.Current.GetService<IRenderLoop>());
+
+            public void Invalidate(Rect rect)
+            {
+                // No-op
+            }
+
+            public void SetInputRoot(IInputRoot inputRoot)
+            {
+                _view.InputRoot = inputRoot;
+            }
+
+            public Point PointToClient(PixelPoint point) => new Point(point.X, point.Y);
+
+            public PixelPoint PointToScreen(Point point) => new PixelPoint((int) point.X, (int) point.Y);
+
+            public void SetCursor(IPlatformHandle cursor)
+            {
+                // no-op
+            }
+
+            public IPopupImpl CreatePopup()
+            {
+                // In-window popups
+                return null;
+            }
+
+            public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel)
+            {
+                // No-op
+            }
+
+            public Size ClientSize => new Size(_view.Bounds.Width, _view.Bounds.Height);
+            public double RenderScaling => _view.ContentScaleFactor;
+            public IEnumerable<object> Surfaces { get; set; }
+            public Action<RawInputEventArgs> Input { get; set; }
+            public Action<Rect> Paint { get; set; }
+            public Action<Size> Resized { get; set; }
+            public Action<double> ScalingChanged { get; set; }
+            public Action<WindowTransparencyLevel> TransparencyLevelChanged { get; set; }
+            public Action Closed { get; set; }
+
+            public Action LostFocus { get; set; }
+
+            // legacy no-op
+            public IMouseDevice MouseDevice { get; } = new MouseDevice();
+            public WindowTransparencyLevel TransparencyLevel { get; }
+
+            public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } =
+                new AcrylicPlatformCompensationLevels();
         }
 
-        public AvaloniaView()
+        [Export("layerClass")]
+        public static Class LayerClass()
         {
-            
-            _impl = new EmbeddableImpl();
-            AddSubview(_impl);
-            BackgroundColor = UIColor.White;
-            AutoresizingMask = UIViewAutoresizing.All;
-            _root = new EmbeddableControlRoot(_impl);
-            _root.Prepare();
+            return new Class(typeof(CAEAGLLayer));
         }
 
+        public override void TouchesBegan(NSSet touches, UIEvent evt) => _touches.Handle(touches, evt);
+
+        public override void TouchesMoved(NSSet touches, UIEvent evt) => _touches.Handle(touches, evt);
+
+        public override void TouchesEnded(NSSet touches, UIEvent evt) => _touches.Handle(touches, evt);
+
+        public override void TouchesCancelled(NSSet touches, UIEvent evt) => _touches.Handle(touches, evt);
+
         public override void LayoutSubviews()
         {
-            _impl.Frame = new CGRect(Padding.Left, Padding.Top,
-                Frame.Width - Padding.Left - Padding.Right,
-                Frame.Height - Padding.Top - Padding.Bottom);
+            _topLevelImpl.Resized?.Invoke(_topLevelImpl.ClientSize);
+            base.LayoutSubviews();
         }
 
-
-        public object Content
+        public Control Content
         {
-            get { return _root.Content; }
-            set { _root.Content = value; }
+            get => (Control)_topLevel.Content;
+            set => _topLevel.Content = value;
         }
     }
-}
+}

+ 0 - 26
src/iOS/Avalonia.iOS/AvaloniaWindow.cs

@@ -1,26 +0,0 @@
-using Avalonia.Media;
-using UIKit;
-
-namespace Avalonia.iOS
-{
-    public sealed class AvaloniaWindow : UIWindow
-    {
-        readonly AvaloniaRootViewController _controller = new AvaloniaRootViewController();
-        public object Content
-        {
-            get { return _controller.Content; }
-            set { _controller.Content = value; }
-        }
-
-        public AvaloniaWindow() : base(UIScreen.MainScreen.Bounds)
-        {
-            RootViewController = _controller;
-        }
-
-        public Color StatusBarColor
-        {
-            get { return _controller.StatusBarColor; }
-            set { _controller.StatusBarColor = value; }
-        }
-    }
-}

+ 4 - 3
src/iOS/Avalonia.iOS/AppBuilder.cs → src/iOS/Avalonia.iOS/Boilerplate/AppBuilder.cs

@@ -1,4 +1,5 @@
 using Avalonia.Controls;
+using Avalonia.iOS;
 using Avalonia.Shared.PlatformSupport;
 
 namespace Avalonia
@@ -6,9 +7,9 @@ namespace Avalonia
     public class AppBuilder : AppBuilderBase<AppBuilder>
     {
         public AppBuilder() : base(new StandardRuntimePlatform(),
-            builder => StandardRuntimePlatformServices.Register(builder.ApplicationType.Assembly))
+            b => StandardRuntimePlatformServices.Register(b.ApplicationType.Assembly))
         {
-
+            this.UseSkia().UseWindowingSubsystem(iOS.Platform.Register);
         }
     }
-}
+}

+ 19 - 0
src/iOS/Avalonia.iOS/Boilerplate/RuntimePlatform.cs

@@ -0,0 +1,19 @@
+using Avalonia.Platform;
+
+namespace Avalonia.Shared.PlatformSupport
+{
+    partial class StandardRuntimePlatform
+    {
+        public RuntimePlatformInfo GetRuntimeInfo()
+        {
+            return new RuntimePlatformInfo
+            {
+                IsDesktop = false,
+                IsMobile = true,
+                IsMono = true,
+                IsUnix = true,
+                OperatingSystem = OperatingSystemType.iOS
+            };
+        }
+    }
+}

+ 595 - 0
src/iOS/Avalonia.iOS/Boilerplate/Shared.cs

@@ -0,0 +1,595 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Threading;
+using Avalonia.Platform;
+using Avalonia.Platform.Interop;
+using Avalonia.Utilities;
+
+namespace Avalonia.Shared.PlatformSupport
+{
+    static class StandardRuntimePlatformServices
+    {
+        public static void Register(Assembly assembly = null)
+        {
+            var standardPlatform = new StandardRuntimePlatform();
+            AssetLoader.RegisterResUriParsers();
+            AvaloniaLocator.CurrentMutable
+                .Bind<IRuntimePlatform>().ToConstant(standardPlatform)
+                .Bind<IAssetLoader>().ToConstant(new AssetLoader(assembly))
+                .Bind<IDynamicLibraryLoader>().ToConstant(
+#if __IOS__
+                    new IOSLoader()
+#else
+                RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
+                    ? (IDynamicLibraryLoader)new Win32Loader()
+                    : new UnixLoader()
+#endif
+                );
+        }
+    }
+    
+    
+    internal partial class StandardRuntimePlatform : IRuntimePlatform
+    {
+        public IDisposable StartSystemTimer(TimeSpan interval, Action tick)
+        {
+            return new Timer(_ => tick(), null, interval, interval);
+        }
+
+        public IUnmanagedBlob AllocBlob(int size) => new UnmanagedBlob(this, size);
+        
+        class UnmanagedBlob : IUnmanagedBlob
+        {
+            private readonly StandardRuntimePlatform _plat;
+            private IntPtr _address;
+            private readonly object _lock = new object();
+#if DEBUG
+            private static readonly List<string> Backtraces = new List<string>();
+            private static Thread GCThread;
+            private readonly string _backtrace;
+            private static readonly object _btlock = new object();
+
+            class GCThreadDetector
+            {
+                ~GCThreadDetector()
+                {
+                    GCThread = Thread.CurrentThread;
+                }
+            }
+
+            [MethodImpl(MethodImplOptions.NoInlining)]
+            static void Spawn() => new GCThreadDetector();
+            
+            static UnmanagedBlob()
+            {
+                Spawn();
+                GC.WaitForPendingFinalizers();
+            }
+            
+#endif
+            
+            public UnmanagedBlob(StandardRuntimePlatform plat, int size)
+            {
+                if (size <= 0)
+                    throw new ArgumentException("Positive number required", nameof(size));
+                _plat = plat;
+                _address = plat.Alloc(size);
+                GC.AddMemoryPressure(size);
+                Size = size;
+#if DEBUG
+                _backtrace = Environment.StackTrace;
+                lock (_btlock)
+                    Backtraces.Add(_backtrace);
+#endif
+            }
+
+            void DoDispose()
+            {
+                lock (_lock)
+                {
+                    if (!IsDisposed)
+                    {
+#if DEBUG
+                        lock (_btlock)
+                            Backtraces.Remove(_backtrace);
+#endif
+                        _plat?.Free(_address, Size);
+                        GC.RemoveMemoryPressure(Size);
+                        IsDisposed = true;
+                        _address = IntPtr.Zero;
+                        Size = 0;
+                    }
+                }
+            }
+
+            public void Dispose()
+            {
+#if DEBUG
+                if (Thread.CurrentThread.ManagedThreadId == GCThread?.ManagedThreadId)
+                {
+                    lock (_lock)
+                    {
+                        if (!IsDisposed)
+                        {
+                            Console.Error.WriteLine("Native blob disposal from finalizer thread\nBacktrace: "
+                                                 + Environment.StackTrace
+                                                 + "\n\nBlob created by " + _backtrace);
+                        }
+                    }
+                }
+#endif
+                DoDispose();
+                GC.SuppressFinalize(this);
+            }
+
+            ~UnmanagedBlob()
+            {
+#if DEBUG
+                Console.Error.WriteLine("Undisposed native blob created by " + _backtrace);
+#endif
+                DoDispose();
+            }
+
+            public IntPtr Address => IsDisposed ? throw new ObjectDisposedException("UnmanagedBlob") : _address; 
+            public int Size { get; private set; }
+            public bool IsDisposed { get; private set; }
+        }
+        
+        
+        
+#if NET461 || NETCOREAPP2_0
+        [DllImport("libc", SetLastError = true)]
+        private static extern IntPtr mmap(IntPtr addr, IntPtr length, int prot, int flags, int fd, IntPtr offset);
+        [DllImport("libc", SetLastError = true)]
+        private static extern int munmap(IntPtr addr, IntPtr length);
+        [DllImport("libc", SetLastError = true)]
+        private static extern long sysconf(int name);
+
+        private bool? _useMmap;
+        private bool UseMmap 
+            => _useMmap ?? ((_useMmap = GetRuntimeInfo().OperatingSystem == OperatingSystemType.Linux)).Value;
+        
+        IntPtr Alloc(int size)
+        {
+            if (UseMmap)
+            {
+                var rv = mmap(IntPtr.Zero, new IntPtr(size), 3, 0x22, -1, IntPtr.Zero);
+                if (rv.ToInt64() == -1 || (ulong) rv.ToInt64() == 0xffffffff)
+                {
+                    var errno = Marshal.GetLastWin32Error();
+                    throw new Exception("Unable to allocate memory: " + errno);
+                }
+                return rv;
+            }
+            else
+                return Marshal.AllocHGlobal(size);
+        }
+
+        void Free(IntPtr ptr, int len)
+        {
+            if (UseMmap)
+            {
+                if (munmap(ptr, new IntPtr(len)) == -1)
+                {
+                    var errno = Marshal.GetLastWin32Error();
+                    throw new Exception("Unable to free memory: " + errno);
+                }
+            }
+            else
+                Marshal.FreeHGlobal(ptr);
+        }
+#else
+        IntPtr Alloc(int size) => Marshal.AllocHGlobal(size);
+        void Free(IntPtr ptr, int len) => Marshal.FreeHGlobal(ptr);
+#endif
+        
+
+    }
+    
+    internal class IOSLoader : IDynamicLibraryLoader
+    {
+        IntPtr IDynamicLibraryLoader.LoadLibrary(string dll)
+        {
+            throw new PlatformNotSupportedException();
+        }
+
+        IntPtr IDynamicLibraryLoader.GetProcAddress(IntPtr dll, string proc, bool optional)
+        {
+            throw new PlatformNotSupportedException();
+        }
+    }
+    
+    public class AssetLoader : IAssetLoader
+    {
+        private const string AvaloniaResourceName = "!AvaloniaResources";
+        private static readonly Dictionary<string, AssemblyDescriptor> AssemblyNameCache
+            = new Dictionary<string, AssemblyDescriptor>();
+
+        private AssemblyDescriptor _defaultResmAssembly;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AssetLoader"/> class.
+        /// </summary>
+        /// <param name="assembly">
+        /// The default assembly from which to load resm: assets for which no assembly is specified.
+        /// </param>
+        public AssetLoader(Assembly assembly = null)
+        {
+            if (assembly == null)
+                assembly = Assembly.GetEntryAssembly();
+            if (assembly != null)
+                _defaultResmAssembly = new AssemblyDescriptor(assembly);
+        }
+
+        /// <summary>
+        /// Sets the default assembly from which to load assets for which no assembly is specified.
+        /// </summary>
+        /// <param name="assembly">The default assembly.</param>
+        public void SetDefaultAssembly(Assembly assembly)
+        {
+            _defaultResmAssembly = new AssemblyDescriptor(assembly);
+        }
+
+        /// <summary>
+        /// Checks if an asset with the specified URI exists.
+        /// </summary>
+        /// <param name="uri">The URI.</param>
+        /// <param name="baseUri">
+        /// A base URI to use if <paramref name="uri"/> is relative.
+        /// </param>
+        /// <returns>True if the asset could be found; otherwise false.</returns>
+        public bool Exists(Uri uri, Uri baseUri = null)
+        {
+            return GetAsset(uri, baseUri) != null;
+        }
+
+        /// <summary>
+        /// Opens the asset with the requested URI.
+        /// </summary>
+        /// <param name="uri">The URI.</param>
+        /// <param name="baseUri">
+        /// A base URI to use if <paramref name="uri"/> is relative.
+        /// </param>
+        /// <returns>A stream containing the asset contents.</returns>
+        /// <exception cref="FileNotFoundException">
+        /// The asset could not be found.
+        /// </exception>
+        public Stream Open(Uri uri, Uri baseUri = null) => OpenAndGetAssembly(uri, baseUri).Item1;
+
+        /// <summary>
+        /// Opens the asset with the requested URI and returns the asset stream and the
+        /// assembly containing the asset.
+        /// </summary>
+        /// <param name="uri">The URI.</param>
+        /// <param name="baseUri">
+        /// A base URI to use if <paramref name="uri"/> is relative.
+        /// </param>
+        /// <returns>
+        /// The stream containing the resource contents together with the assembly.
+        /// </returns>
+        /// <exception cref="FileNotFoundException">
+        /// The asset could not be found.
+        /// </exception>
+        public (Stream stream, Assembly assembly) OpenAndGetAssembly(Uri uri, Uri baseUri = null)
+        {
+            var asset = GetAsset(uri, baseUri);
+
+            if (asset == null)
+            {
+                throw new FileNotFoundException($"The resource {uri} could not be found.");
+            }
+
+            return (asset.GetStream(), asset.Assembly);
+        }
+
+        public Assembly GetAssembly(Uri uri, Uri baseUri)
+        {
+            if (!uri.IsAbsoluteUri && baseUri != null)
+                uri = new Uri(baseUri, uri);
+            return GetAssembly(uri).Assembly;
+        }
+
+        /// <summary>
+        /// Gets all assets of a folder and subfolders that match specified uri.
+        /// </summary>
+        /// <param name="uri">The URI.</param>
+        /// <param name="baseUri">Base URI that is used if <paramref name="uri"/> is relative.</param>
+        /// <returns>All matching assets as a tuple of the absolute path to the asset and the assembly containing the asset</returns>
+        public IEnumerable<Uri> GetAssets(Uri uri, Uri baseUri)
+        {
+            if (uri.IsAbsoluteUri && uri.Scheme == "resm")
+            {
+                var assembly = GetAssembly(uri);
+
+                return assembly?.Resources.Where(x => x.Key.Contains(uri.AbsolutePath))
+                           .Select(x =>new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ??
+                       Enumerable.Empty<Uri>();
+            }
+
+            uri = EnsureAbsolute(uri, baseUri);
+            if (uri.Scheme == "avares")
+            {
+                var (asm, path) = GetResAsmAndPath(uri);
+                if (asm == null)
+                {
+                    throw new ArgumentException(
+                        "No default assembly, entry assembly or explicit assembly specified; " +
+                        "don't know where to look up for the resource, try specifying assembly explicitly.");
+                }
+
+                if (asm?.AvaloniaResources == null)
+                    return Enumerable.Empty<Uri>();
+                path = path.TrimEnd('/') + '/';
+                return asm.AvaloniaResources.Where(r => r.Key.StartsWith(path))
+                    .Select(x => new Uri($"avares://{asm.Name}{x.Key}"));
+            }
+
+            return Enumerable.Empty<Uri>();
+        }
+
+        private Uri EnsureAbsolute(Uri uri, Uri baseUri)
+        {
+            if (uri.IsAbsoluteUri)
+                return uri;
+            if(baseUri == null)
+                throw new ArgumentException($"Relative uri {uri} without base url");
+            if (!baseUri.IsAbsoluteUri)
+                throw new ArgumentException($"Base uri {baseUri} is relative");
+            if (baseUri.Scheme == "resm")
+                throw new ArgumentException(
+                    $"Relative uris for 'resm' scheme aren't supported; {baseUri} uses resm");
+            return new Uri(baseUri, uri);
+        }
+        
+        private IAssetDescriptor GetAsset(Uri uri, Uri baseUri)
+        {           
+            if (uri.IsAbsoluteUri && uri.Scheme == "resm")
+            {
+                var asm = GetAssembly(uri) ?? GetAssembly(baseUri) ?? _defaultResmAssembly;
+
+                if (asm == null)
+                {
+                    throw new ArgumentException(
+                        "No default assembly, entry assembly or explicit assembly specified; " +
+                        "don't know where to look up for the resource, try specifying assembly explicitly.");
+                }
+
+                IAssetDescriptor rv;
+
+                var resourceKey = uri.AbsolutePath;
+                asm.Resources.TryGetValue(resourceKey, out rv);
+                return rv;
+            }
+
+            uri = EnsureAbsolute(uri, baseUri);
+
+            if (uri.Scheme == "avares")
+            {
+                var (asm, path) = GetResAsmAndPath(uri);
+                if (asm.AvaloniaResources == null)
+                    return null;
+                asm.AvaloniaResources.TryGetValue(path, out var desc);
+                return desc;
+            }
+
+            throw new ArgumentException($"Unsupported url type: " + uri.Scheme, nameof(uri));
+        }
+
+        private (AssemblyDescriptor asm, string path) GetResAsmAndPath(Uri uri)
+        {
+            var asm = GetAssembly(uri.Authority);
+            return (asm, uri.AbsolutePath);
+        }
+        
+        private AssemblyDescriptor GetAssembly(Uri uri)
+        {
+            if (uri != null)
+            {
+                if (!uri.IsAbsoluteUri)
+                    return null;
+                if (uri.Scheme == "avares")
+                    return GetResAsmAndPath(uri).asm;
+
+                if (uri.Scheme == "resm")
+                {
+                    var qs = ParseQueryString(uri);
+                    string assemblyName;
+
+                    if (qs.TryGetValue("assembly", out assemblyName))
+                    {
+                        return GetAssembly(assemblyName);
+                    }
+                }
+            }
+
+            return null;
+        }
+
+        private AssemblyDescriptor GetAssembly(string name)
+        {
+            if (name == null)
+                throw new ArgumentNullException(nameof(name));
+
+            AssemblyDescriptor rv;
+            if (!AssemblyNameCache.TryGetValue(name, out rv))
+            {
+                var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
+                var match = loadedAssemblies.FirstOrDefault(a => a.GetName().Name == name);
+                if (match != null)
+                {
+                    AssemblyNameCache[name] = rv = new AssemblyDescriptor(match);
+                }
+                else
+                {
+                    // iOS does not support loading assemblies dynamically!
+                    //
+#if __IOS__
+                    throw new InvalidOperationException(
+                        $"Assembly {name} needs to be referenced and explicitly loaded before loading resources");
+#else
+                    name = Uri.UnescapeDataString(name);
+                    AssemblyNameCache[name] = rv = new AssemblyDescriptor(Assembly.Load(name));
+#endif
+                }
+            }
+
+            return rv;
+        }
+
+        private Dictionary<string, string> ParseQueryString(Uri uri)
+        {
+            return uri.Query.TrimStart('?')
+                .Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries)
+                .Select(p => p.Split('='))
+                .ToDictionary(p => p[0], p => p[1]);
+        }
+
+        private interface IAssetDescriptor
+        {
+            Stream GetStream();
+            Assembly Assembly { get; }
+        }
+
+        private class AssemblyResourceDescriptor : IAssetDescriptor
+        {
+            private readonly Assembly _asm;
+            private readonly string _name;
+
+            public AssemblyResourceDescriptor(Assembly asm, string name)
+            {
+                _asm = asm;
+                _name = name;
+            }
+
+            public Stream GetStream()
+            {
+                return _asm.GetManifestResourceStream(_name);
+            }
+
+            public Assembly Assembly => _asm;
+        }
+        
+        private class AvaloniaResourceDescriptor : IAssetDescriptor
+        {
+            private readonly int _offset;
+            private readonly int _length;
+            public Assembly Assembly { get; }
+
+            public AvaloniaResourceDescriptor(Assembly asm, int offset, int length)
+            {
+                _offset = offset;
+                _length = length;
+                Assembly = asm;
+            }
+            
+            public Stream GetStream()
+            {
+                return new SlicedStream(Assembly.GetManifestResourceStream(AvaloniaResourceName), _offset, _length);
+            }
+        }
+        
+        class SlicedStream : Stream
+        {
+            private readonly Stream _baseStream;
+            private readonly int _from;
+
+            public SlicedStream(Stream baseStream, int from, int length)
+            {
+                Length = length;
+                _baseStream = baseStream;
+                _from = from;
+                _baseStream.Position = from;
+            }
+            public override void Flush()
+            {
+            }
+
+            public override int Read(byte[] buffer, int offset, int count)
+            {
+                return _baseStream.Read(buffer, offset, (int)Math.Min(count, Length - Position));
+            }
+
+            public override long Seek(long offset, SeekOrigin origin)
+            {
+                if (origin == SeekOrigin.Begin)
+                    Position = offset;
+                if (origin == SeekOrigin.End)
+                    Position = _from + Length + offset;
+                if (origin == SeekOrigin.Current)
+                    Position = Position + offset;
+                return Position;
+            }
+
+            public override void SetLength(long value) => throw new NotSupportedException();
+
+            public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
+
+            public override bool CanRead => true;
+            public override bool CanSeek => _baseStream.CanRead;
+            public override bool CanWrite => false;
+            public override long Length { get; }
+            public override long Position
+            {
+                get => _baseStream.Position - _from;
+                set => _baseStream.Position = value + _from;
+            }
+
+            protected override void Dispose(bool disposing)
+            {
+                if (disposing)
+                    _baseStream.Dispose();
+            }
+
+            public override void Close() => _baseStream.Close();
+        }
+
+        private class AssemblyDescriptor
+        {
+            public AssemblyDescriptor(Assembly assembly)
+            {
+                Assembly = assembly;
+
+                if (assembly != null)
+                {
+                    Resources = assembly.GetManifestResourceNames()
+                        .ToDictionary(n => n, n => (IAssetDescriptor)new AssemblyResourceDescriptor(assembly, n));
+                    Name = assembly.GetName().Name;
+                    using (var resources = assembly.GetManifestResourceStream(AvaloniaResourceName))
+                    {
+                        if (resources != null)
+                        {
+                            Resources.Remove(AvaloniaResourceName);
+
+                            var indexLength = new BinaryReader(resources).ReadInt32();
+                            var index = AvaloniaResourcesIndexReaderWriter.Read(new SlicedStream(resources, 4, indexLength));
+                            var baseOffset = indexLength + 4;
+                            AvaloniaResources = index.ToDictionary(r => "/" + r.Path.TrimStart('/'), r => (IAssetDescriptor)
+                                new AvaloniaResourceDescriptor(assembly, baseOffset + r.Offset, r.Size));
+                        }
+                    }
+                }
+            }
+
+            public Assembly Assembly { get; }
+            public Dictionary<string, IAssetDescriptor> Resources { get; }
+            public Dictionary<string, IAssetDescriptor> AvaloniaResources { get; }
+            public string Name { get; }
+        }
+        
+        public static void RegisterResUriParsers()
+        {
+            if (!UriParser.IsKnownScheme("avares"))
+                UriParser.Register(new GenericUriParser(
+                    GenericUriParserOptions.GenericAuthority |
+                    GenericUriParserOptions.NoUserInfo |
+                    GenericUriParserOptions.NoPort |
+                    GenericUriParserOptions.NoQuery |
+                    GenericUriParserOptions.NoFragment), "avares", -1);
+        }
+    }
+}

+ 2 - 2
src/iOS/Avalonia.iOS/Clipboard.cs → src/iOS/Avalonia.iOS/ClipboardImpl.cs

@@ -6,7 +6,7 @@ using UIKit;
 
 namespace Avalonia.iOS
 {
-    public class Clipboard : IClipboard
+    public class ClipboardImpl : IClipboard
     {
         public Task<string> GetTextAsync()
         {
@@ -31,4 +31,4 @@ namespace Avalonia.iOS
 
         public Task<object> GetDataAsync(string format) => throw new PlatformNotSupportedException();
     }
-}
+}

+ 0 - 11
src/iOS/Avalonia.iOS/CursorFactory.cs

@@ -1,11 +0,0 @@
-using System;
-using Avalonia.Input;
-using Avalonia.Platform;
-
-namespace Avalonia.iOS
-{
-    class CursorFactory : IStandardCursorFactory
-    {
-        public IPlatformHandle GetCursor(StandardCursorType cursorType) => new PlatformHandle(IntPtr.Zero, "NULL");
-    }
-}

+ 0 - 32
src/iOS/Avalonia.iOS/DisplayLinkRenderTimer.cs

@@ -1,32 +0,0 @@
-using System;
-using Avalonia.Rendering;
-using CoreAnimation;
-using Foundation;
-
-namespace Avalonia.iOS
-{
-    class DisplayLinkRenderTimer : IRenderTimer
-    {
-        public event Action<TimeSpan> Tick;
-        private CADisplayLink _link;
-
-        public DisplayLinkRenderTimer()
-        {
-
-            _link = CADisplayLink.Create(OnFrame);
-            _link.AddToRunLoop(NSRunLoop.Main, NSRunLoop.NSDefaultRunLoopMode);
-        }
-
-        private void OnFrame()
-        {
-            try
-            {
-                Tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount));
-            }
-            catch (Exception)
-            {
-                //TODO: log
-            }
-        }
-    }
-}

+ 37 - 0
src/iOS/Avalonia.iOS/DisplayLinkTimer.cs

@@ -0,0 +1,37 @@
+using System;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+using Avalonia.Rendering;
+using CoreAnimation;
+using Foundation;
+using UIKit;
+
+namespace Avalonia.iOS
+{
+    class DisplayLinkTimer : IRenderTimer
+    {
+        public event Action<TimeSpan> Tick;
+        private Stopwatch _st = Stopwatch.StartNew();
+
+        public DisplayLinkTimer()
+        {
+            var link = CADisplayLink.Create(OnLinkTick);
+            TimerThread = new Thread(() =>
+            {
+                link.AddToRunLoop(NSRunLoop.Current, NSRunLoopMode.Common);
+                NSRunLoop.Current.Run();
+            });
+            TimerThread.Start();
+            UIApplication.Notifications.ObserveDidEnterBackground((_,__) => link.Paused = true);
+            UIApplication.Notifications.ObserveWillEnterForeground((_, __) => link.Paused = false);
+        }
+
+        public Thread TimerThread { get;  }
+
+        private void OnLinkTick()
+        {
+            Tick?.Invoke(_st.Elapsed);
+        }
+    }
+}

+ 69 - 0
src/iOS/Avalonia.iOS/EaglDisplay.cs

@@ -0,0 +1,69 @@
+using System;
+using Avalonia.OpenGL;
+using OpenGLES;
+using OpenTK.Graphics.ES30;
+
+namespace Avalonia.iOS
+{
+    class EaglFeature : IWindowingPlatformGlFeature
+    {
+        public IGlContext CreateContext() => throw new System.NotSupportedException();
+
+        public IGlContext MainContext => Context;
+        public GlContext Context { get; } = new GlContext();
+    }
+
+    class GlContext : IGlContext
+    {
+        public EAGLContext Context { get; private set; }
+        
+        public GlContext()
+        {
+            const string path = "/System/Library/Frameworks/OpenGLES.framework/OpenGLES";
+            var libGl = ObjCRuntime.Dlfcn.dlopen(path, 1);
+            if (libGl == IntPtr.Zero)
+                throw new OpenGlException("Unable to load " + path);
+            GlInterface = new GlInterface(Version, proc => ObjCRuntime.Dlfcn.dlsym(libGl, proc));
+            Context = new EAGLContext(EAGLRenderingAPI.OpenGLES3);
+        }
+        
+        public void Dispose()
+        {
+            Context?.Dispose();
+            Context = null;
+        }
+
+        class ResetContext : IDisposable
+        {
+            private EAGLContext _old;
+            private bool _disposed;
+
+            public ResetContext(EAGLContext old)
+            {
+                _old = old;
+            }
+            
+            public void Dispose()
+            {
+                if(_disposed)
+                    return;
+                _disposed = true;
+                EAGLContext.SetCurrentContext(_old);
+                _old = null;
+            }
+        }
+        
+        public IDisposable MakeCurrent()
+        {
+            var old = EAGLContext.CurrentContext;
+            if (!EAGLContext.SetCurrentContext(Context))
+                throw new OpenGlException("Unable to make context current");
+            return new ResetContext(old);
+        }
+
+        public GlVersion Version { get; } = new GlVersion(GlProfileType.OpenGLES, 3, 0);
+        public GlInterface GlInterface { get; }
+        public int SampleCount { get; } = 0;
+        public int StencilSize { get; } = 9;
+    }
+}

+ 94 - 0
src/iOS/Avalonia.iOS/EaglLayerSurface.cs

@@ -0,0 +1,94 @@
+
+using System;
+using System.Threading;
+using Avalonia.OpenGL;
+using CoreAnimation;
+using OpenTK.Graphics.ES30;
+
+namespace Avalonia.iOS
+{
+    class EaglLayerSurface : IGlPlatformSurface
+    {
+        private readonly CAEAGLLayer _layer;
+
+        public EaglLayerSurface(CAEAGLLayer layer)
+        {
+            _layer = layer;
+        }
+
+        class RenderSession : IGlPlatformSurfaceRenderingSession
+        {
+            private readonly GlContext _ctx;
+            private readonly IDisposable _restoreContext;
+            private readonly SizeSynchronizedLayerFbo _fbo;
+
+            public RenderSession(GlContext ctx, IDisposable restoreContext, SizeSynchronizedLayerFbo fbo)
+            {
+                _ctx = ctx;
+                _restoreContext = restoreContext;
+                _fbo = fbo;
+                Size = new PixelSize(_fbo.Width, _fbo.Height);
+                Scaling = _fbo.Scaling;
+                Context = ctx;
+            }
+
+            public void Dispose()
+            {
+                GL.Finish();
+                _fbo.Present();
+                _restoreContext.Dispose();
+            }
+
+            public IGlContext Context { get; }
+            public PixelSize Size { get; }
+            public double Scaling { get; }
+            public bool IsYFlipped { get; }
+        }
+
+        class RenderTarget : IGlPlatformSurfaceRenderTarget
+        {
+            private readonly GlContext _ctx;
+            private readonly SizeSynchronizedLayerFbo _fbo;
+
+            public RenderTarget(GlContext ctx, SizeSynchronizedLayerFbo fbo)
+            {
+                _ctx = ctx;
+                _fbo = fbo;
+            }
+
+            public void Dispose()
+            {
+                CheckThread();
+                using (_ctx.MakeCurrent())
+                    _fbo.Dispose();
+            }
+
+            public IGlPlatformSurfaceRenderingSession BeginDraw()
+            {
+                CheckThread();
+                var restoreContext = _ctx.MakeCurrent();
+                _fbo.Bind();
+                return new RenderSession(_ctx, restoreContext, _fbo);
+            }
+        }
+
+        static void CheckThread()
+        {
+            if (Platform.Timer.TimerThread != Thread.CurrentThread)
+                throw new InvalidOperationException("Invalid thread, go away");
+        }
+        
+        public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget()
+        {
+            CheckThread();
+            var ctx = Platform.GlFeature.Context;
+            using (ctx.MakeCurrent())
+            {
+                var fbo = new SizeSynchronizedLayerFbo(ctx.Context, _layer);
+                if (!fbo.Sync())
+                    throw new InvalidOperationException("Unable to create render target");
+                return new RenderTarget(ctx, fbo);
+            }
+        }
+    }
+}

+ 0 - 23
src/iOS/Avalonia.iOS/EmbeddableImpl.cs

@@ -1,23 +0,0 @@
-using System;
-using System.Reactive.Disposables;
-using Avalonia.Platform;
-
-namespace Avalonia.iOS
-{
-    class EmbeddableImpl : TopLevelImpl
-    {
-        public void SetTitle(string title)
-        {
-            
-        }
-
-        public void SetMinMaxSize(Size minSize, Size maxSize)
-        {
-        }
-
-        public IDisposable ShowDialog()
-        {
-            return Disposable.Empty;
-        }
-    }
-}

+ 0 - 60
src/iOS/Avalonia.iOS/EmulatedFramebuffer.cs

@@ -1,60 +0,0 @@
-using System;
-using System.Runtime.InteropServices;
-using Avalonia.Platform;
-using CoreGraphics;
-using UIKit;
-
-namespace Avalonia.iOS
-{
-
-    /// <summary>
-    /// This is a bit weird, but CG doesn't provide proper bitmap
-    /// with lockable bits, but can create one from data pointer
-    /// So we are using our own buffer here.
-    /// </summary>
-    class EmulatedFramebuffer : ILockedFramebuffer
-    {
-        private nfloat _viewWidth;
-        private nfloat _viewHeight;
-
-        public EmulatedFramebuffer(UIView view)
-        {
-            var factor = (int) UIScreen.MainScreen.Scale;
-            var frame = view.Frame;
-            _viewWidth = frame.Width;
-            _viewHeight = frame.Height;
-            Size = new PixelSize((int)frame.Width * factor, (int)frame.Height * factor);
-            RowBytes = Size.Width * 4;
-            Dpi = new Vector(96, 96) * factor;
-            Format = PixelFormat.Rgba8888;
-            Address = Marshal.AllocHGlobal(Size.Height * RowBytes);
-        }
-
-        public void Dispose()
-        {
-            if (Address == IntPtr.Zero)
-                return;
-            var nfo = (int) CGBitmapFlags.ByteOrder32Big | (int) CGImageAlphaInfo.PremultipliedLast;
-            using (var colorSpace = CGColorSpace.CreateDeviceRGB())
-            using (var bContext = new CGBitmapContext(Address, Size.Width, Size.Height, 8, Size.Width * 4,
-                colorSpace, (CGImageAlphaInfo) nfo))
-            using (var image = bContext.ToImage())
-            using (var context = UIGraphics.GetCurrentContext())
-            {
-                // flip the image for CGContext.DrawImage
-                context.TranslateCTM(0, _viewHeight);
-                context.ScaleCTM(1, -1);
-                context.DrawImage(new CGRect(0, 0, _viewWidth, _viewHeight), image);
-            }
-            Marshal.FreeHGlobal(Address);
-            Address = IntPtr.Zero;
-        }
-
-        public IntPtr Address { get; private set; }
-        public PixelSize Size { get; }
-        public int RowBytes { get; }
-        public Vector Dpi { get; }
-        public PixelFormat Format { get; }
-    }
-}
-

+ 2 - 2
src/iOS/Avalonia.iOS/Extensions.cs

@@ -14,10 +14,10 @@ namespace Avalonia.iOS
 
         static nfloat ColorComponent(byte c) => ((float) c) / 255;
 
-        public static UIColor ToUiColor(this Color color)=>new UIColor(
+        public static UIColor ToUiColor(this Color color) => new UIColor(
             ColorComponent(color.R),
             ColorComponent(color.G),
             ColorComponent(color.B),
             ColorComponent(color.A));
     }
-}
+}

+ 143 - 0
src/iOS/Avalonia.iOS/LayerFbo.cs

@@ -0,0 +1,143 @@
+using System;
+using CoreAnimation;
+using OpenGLES;
+using OpenTK.Graphics.ES20;
+
+namespace Avalonia.iOS
+{
+    public class LayerFbo
+    {
+        private readonly EAGLContext _context;
+        private readonly CAEAGLLayer _layer;
+        private int _framebuffer;
+        private int _renderbuffer;
+        private int _depthBuffer;
+        private bool _disposed;
+
+        private LayerFbo(EAGLContext context, CAEAGLLayer layer, in int framebuffer, in int renderbuffer, in int depthBuffer)
+        {
+            _context = context;
+            _layer = layer;
+            _framebuffer = framebuffer;
+            _renderbuffer = renderbuffer;
+            _depthBuffer = depthBuffer;
+        }
+
+        public static LayerFbo TryCreate(EAGLContext context, CAEAGLLayer layer)
+        {
+            if (context != EAGLContext.CurrentContext)
+                return null;
+            GL.GenFramebuffers(1, out int fb);
+            GL.GenRenderbuffers(1, out int rb);
+            GL.BindFramebuffer(FramebufferTarget.Framebuffer, fb);
+            GL.BindRenderbuffer(RenderbufferTarget.Renderbuffer,  rb);
+            context.RenderBufferStorage((uint) All.Renderbuffer, layer);
+            
+            GL.FramebufferRenderbuffer(FramebufferTarget.Framebuffer, FramebufferSlot.ColorAttachment0, RenderbufferTarget.Renderbuffer, rb);
+
+            int w;
+            int h;
+            GL.GetRenderbufferParameter(RenderbufferTarget.Renderbuffer, RenderbufferParameterName.RenderbufferWidth, out w);
+            GL.GetRenderbufferParameter(RenderbufferTarget.Renderbuffer, RenderbufferParameterName.RenderbufferHeight, out h);
+            
+            GL.GenRenderbuffers(1, out int depthBuffer);
+            
+            //GL.BindRenderbuffer(RenderbufferTarget.Renderbuffer, depthBuffer);
+            //GL.RenderbufferStorage(RenderbufferTarget.Renderbuffer, RenderbufferInternalFormat.DepthComponent16, w, h);
+            GL.FramebufferRenderbuffer(FramebufferTarget.Framebuffer, FramebufferSlot.DepthAttachment, RenderbufferTarget.Renderbuffer, depthBuffer);
+
+            var frameBufferError = GL.CheckFramebufferStatus(FramebufferTarget.Framebuffer);
+            if(frameBufferError != FramebufferErrorCode.FramebufferComplete)
+            {
+                GL.DeleteFramebuffers(1, ref fb);
+                GL.DeleteRenderbuffers(1, ref depthBuffer);
+                GL.DeleteRenderbuffers(1, ref rb);
+                return null;
+            }
+
+            return new LayerFbo(context, layer, fb, rb, depthBuffer)
+            {
+                Width = w,
+                Height = h
+            };
+        }
+        
+        public int Width { get; private set; }
+        public int Height { get; private set; }
+
+        public void Bind()
+        {
+            GL.BindFramebuffer(FramebufferTarget.Framebuffer, _framebuffer);
+        }
+
+        public void Present()
+        {
+            Bind();
+            var success = _context.PresentRenderBuffer((uint) All.Renderbuffer);
+        }
+
+        public void Dispose()
+        {
+            if(_disposed)
+                return;
+            _disposed = true;
+            GL.DeleteFramebuffers(1, ref _framebuffer);
+            GL.DeleteRenderbuffers(1, ref _depthBuffer);
+            GL.DeleteRenderbuffers(1, ref _renderbuffer);
+            if (_context != EAGLContext.CurrentContext)
+                throw new InvalidOperationException("Associated EAGLContext is not current");
+        }
+    }
+
+    class SizeSynchronizedLayerFbo : IDisposable
+    {
+        private readonly EAGLContext _context;
+        private readonly CAEAGLLayer _layer;
+        private LayerFbo _fbo;
+        private nfloat _oldLayerWidth, _oldLayerHeight, _oldLayerScale;
+        
+        public SizeSynchronizedLayerFbo(EAGLContext context, CAEAGLLayer layer)
+        {
+            _context = context;
+            _layer = layer;
+            
+        }
+
+        public bool Sync()
+        {
+            if (_fbo != null 
+                && _oldLayerWidth == _layer.Bounds.Width
+                && _oldLayerHeight == _layer.Bounds.Height
+                && _oldLayerScale == _layer.ContentsScale)
+                return true;
+            _fbo?.Dispose();
+            _fbo = null;
+            _fbo = LayerFbo.TryCreate(_context, _layer);
+            _oldLayerWidth = _layer.Bounds.Width;
+            _oldLayerHeight = _layer.Bounds.Height;
+            _oldLayerScale = _layer.ContentsScale;
+            return _fbo != null;
+        }
+
+        public void Dispose()
+        {
+            if (_context != EAGLContext.CurrentContext)
+                throw new InvalidOperationException("Associated EAGLContext is not current");
+            _fbo?.Dispose();
+            _fbo = null;
+        }
+
+        public void Bind()
+        {
+            if(!Sync())
+                throw new InvalidOperationException("Unable to create a render target");
+            _fbo.Bind();
+        }
+
+        public void Present() => _fbo.Present();
+
+        public int Width => _fbo?.Width ?? 0;
+        public int Height => _fbo?.Height ?? 0;
+        public double Scaling => _oldLayerScale;
+    }
+}

+ 50 - 0
src/iOS/Avalonia.iOS/Platform.cs

@@ -0,0 +1,50 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Input.Platform;
+using Avalonia.OpenGL;
+using Avalonia.Platform;
+using Avalonia.Rendering;
+using Avalonia.Shared.PlatformSupport;
+
+namespace Avalonia.iOS
+{
+    static class Platform
+    {
+        public static EaglFeature GlFeature;
+        public static DisplayLinkTimer Timer;
+        class PlatformSettings : IPlatformSettings
+        {
+            public Size DoubleClickSize { get; } = new Size(10, 10);
+            public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500);
+        }
+        
+        public static void Register()
+        {
+            GlFeature ??= new EaglFeature();
+            Timer ??= new DisplayLinkTimer();
+            var keyboard = new KeyboardDevice();
+            var softKeyboard = new SoftKeyboardHelper();
+            AvaloniaLocator.CurrentMutable
+                .Bind<IWindowingPlatformGlFeature>().ToConstant(GlFeature)
+                .Bind<IStandardCursorFactory>().ToConstant(new CursorFactoryStub())
+                .Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformStub())
+                .Bind<IClipboard>().ToConstant(new ClipboardImpl())
+                .Bind<IPlatformSettings>().ToConstant(new PlatformSettings())
+                .Bind<IPlatformIconLoader>().ToConstant(new PlatformIconLoaderStub())
+                .Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
+                .Bind<IRenderLoop>().ToSingleton<RenderLoop>()
+                .Bind<IRenderTimer>().ToConstant(Timer)
+                .Bind<IPlatformThreadingInterface>().ToConstant(new PlatformThreadingInterface())
+                .Bind<IKeyboardDevice>().ToConstant(keyboard);
+            keyboard.PropertyChanged += (_, changed) =>
+            {
+                if (changed.PropertyName == nameof(KeyboardDevice.FocusedElement))
+                    softKeyboard.UpdateKeyboard(keyboard.FocusedElement);
+            };
+        }
+
+
+    }
+}
+

+ 0 - 46
src/iOS/Avalonia.iOS/PlatformIconLoader.cs

@@ -1,46 +0,0 @@
-using System.IO;
-using Avalonia.Platform;
-
-namespace Avalonia.iOS
-{
-    class PlatformIconLoader : IPlatformIconLoader
-    {
-        public IWindowIconImpl LoadIcon(IBitmapImpl bitmap)
-        {
-            using (var stream = new MemoryStream())
-            {
-                bitmap.Save(stream);
-                return LoadIcon(stream);
-            }
-        }
-
-        public IWindowIconImpl LoadIcon(Stream stream)
-        {
-            return new FakeIcon(stream);
-        }
-
-        public IWindowIconImpl LoadIcon(string fileName)
-        {
-            using (var file = File.Open(fileName, FileMode.Open))
-            {
-                return new FakeIcon(file);
-            }
-        }
-    }
-
-    // Stores the icon created as a stream to support saving even though an icon is never shown
-    public class FakeIcon : IWindowIconImpl
-    {
-        private readonly Stream stream = new MemoryStream();
-
-        public FakeIcon(Stream stream)
-        {
-            stream.CopyTo(this.stream);
-        }
-
-        public void Save(Stream outputStream)
-        {
-            stream.CopyTo(outputStream);
-        }
-    }
-}

+ 0 - 14
src/iOS/Avalonia.iOS/PlatformSettings.cs

@@ -1,14 +0,0 @@
-using System;
-using Avalonia.Platform;
-using UIKit;
-
-namespace Avalonia.iOS
-{
-    class PlatformSettings : IPlatformSettings
-    {
-        public Size DoubleClickSize =>new Size(4, 4);
-        public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(200);
-        public double RenderScalingFactor => UIScreen.MainScreen.Scale;
-        public double LayoutScalingFactor => 1;
-    }
-}

+ 5 - 28
src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs

@@ -2,6 +2,7 @@ using System;
 using System.Threading;
 using Avalonia.Platform;
 using Avalonia.Threading;
+using CoreFoundation;
 using Foundation;
 
 namespace Avalonia.iOS
@@ -18,32 +19,7 @@ namespace Avalonia.iOS
             //Mobile platforms are using external main loop
             throw new NotSupportedException(); 
         }
-        /*
-        class Timer : NSObject
-        {
-            private readonly Action _tick;
-            private NSTimer _timer;
-
-            public Timer(TimeSpan interval, Action tick)
-            {
-                _tick = tick;
-                _timer = new NSTimer(NSDate.Now, interval.TotalSeconds, true, OnTick);
-            }
-
-            [Export("onTick")]
-            private void OnTick(NSTimer nsTimer)
-            {
-                _tick();
-            }
-
-            protected override void Dispose(bool disposing)
-            {
-                if(disposing)
-                    _timer.Dispose();
-                base.Dispose(disposing);
-            }
-        }*/
-
+        
         public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
             => NSTimer.CreateRepeatingScheduledTimer(interval, _ => tick());
 
@@ -55,7 +31,8 @@ namespace Avalonia.iOS
                     return;
                 _signaled = true;
             }
-            NSRunLoop.Main.BeginInvokeOnMainThread(() =>
+
+            DispatchQueue.MainQueue.DispatchAsync(() =>
             {
                 lock (this)
                     _signaled = false;
@@ -63,4 +40,4 @@ namespace Avalonia.iOS
             });
         }
     }
-}
+}

+ 0 - 17
src/iOS/Avalonia.iOS/RuntimeInfo.cs

@@ -1,17 +0,0 @@
-using Avalonia.Platform;
-namespace Avalonia.Shared.PlatformSupport
-{
-    internal partial class StandardRuntimePlatform
-    {
-        public RuntimePlatformInfo GetRuntimeInfo() => new RuntimePlatformInfo
-        {
-            IsCoreClr = false,
-            IsDesktop = false,
-            IsMobile = true,
-            IsDotNetFramework = false,
-            IsMono = true,
-            IsUnix = true,
-            OperatingSystem = OperatingSystemType.Android
-        };
-    }
-}

+ 7 - 0
src/iOS/Avalonia.iOS/SingleViewLifetime.cs

@@ -0,0 +1,7 @@
+namespace Avalonia.iOS
+{
+    public class SingleViewLifetime
+    {
+        
+    }
+}

+ 24 - 0
src/iOS/Avalonia.iOS/SoftKeyboardHelper.cs

@@ -0,0 +1,24 @@
+using Avalonia.Controls;
+using Avalonia.Input;
+
+namespace Avalonia.iOS
+{
+    public class SoftKeyboardHelper
+    {
+        private AvaloniaView _oldView;
+        
+        public void UpdateKeyboard(IInputElement focusedElement)
+        {
+            if (_oldView?.IsFirstResponder == true)
+                _oldView?.ResignFirstResponder();
+            _oldView = null;
+            
+            //TODO: Raise a routed event to determine if any control wants to become the text input handler 
+            if (focusedElement is TextBox)
+            {
+                var view = ((focusedElement.VisualRoot as TopLevel)?.PlatformImpl as AvaloniaView.TopLevelImpl)?.View;
+                view?.BecomeFirstResponder();
+            }
+        }
+    }
+}

+ 0 - 152
src/iOS/Avalonia.iOS/Specific/KeyboardEventsHelper.cs

@@ -1,152 +0,0 @@
-using System;
-using System.ComponentModel;
-using System.Linq;
-using Avalonia.Controls;
-using Avalonia.Input;
-using Avalonia.Input.Raw;
-using Avalonia.Platform;
-using ObjCRuntime;
-using UIKit;
-
-namespace Avalonia.iOS.Specific
-{
-    /// <summary>
-    /// In order to have properly handle of keyboard event in iOS View should already made some things in the View:
-    /// 1. Adopt the UIKeyInput protocol - add [Adopts("UIKeyInput")] to your view class
-    /// 2. Implement all the methods required by UIKeyInput:
-    ///     2.1  Implement HasText
-    ///             example:
-    ///                 [Export("hasText")]
-    ///                 bool HasText => _keyboardHelper.HasText()
-    ///     2.2   Implement InsertText
-    ///            example:
-    ///                [Export("insertText:")]
-    ///                void InsertText(string text) => _keyboardHelper.InsertText(text);
-    ///     2.3   Implement InsertText
-    ///            example:
-    ///               [Export("deleteBackward")]
-    ///               void DeleteBackward() => _keyboardHelper.DeleteBackward();
-    /// 3.Let iOS know that this can become a first responder:
-    ///            public override bool CanBecomeFirstResponder => _keyboardHelper.CanBecomeFirstResponder();
-    ///            or
-    ///            public override bool CanBecomeFirstResponder { get { return true; } }
-    ///
-    /// 4. To show keyboard:
-    ///             view.BecomeFirstResponder();
-    /// 5. To hide keyboard
-    ///             view.ResignFirstResponder();
-    /// </summary>
-    /// <typeparam name="TView">View that needs keyboard events and show/hide keyboard</typeparam>
-    internal class KeyboardEventsHelper<TView> where TView : UIView, ITopLevelImpl, IGetInputRoot
-    {
-        private TView _view;
-        private IInputElement _lastFocusedElement;
-
-        public KeyboardEventsHelper(TView view)
-        {
-            _view = view;
-
-            var uiKeyInputAttribute = view.GetType().GetCustomAttributes(typeof(AdoptsAttribute), true).OfType<AdoptsAttribute>().Where(a => a.ProtocolType == "UIKeyInput").FirstOrDefault();
-
-            if (uiKeyInputAttribute == null) throw new NotSupportedException($"View class {typeof(TView).Name} should have class attribute - [Adopts(\"UIKeyInput\")] in order to access keyboard events!");
-
-            HandleEvents = true;
-        }
-
-        /// <summary>
-        /// HandleEvents in order to suspend keyboard notifications or resume it
-        /// </summary>
-        public bool HandleEvents { get; set; }
-
-        public bool HasText() => false;
-
-        public bool CanBecomeFirstResponder() => true;
-
-        public void DeleteBackward()
-        {
-            HandleKey(Key.Back, RawKeyEventType.KeyDown);
-            HandleKey(Key.Back, RawKeyEventType.KeyUp);
-        }
-
-        public void InsertText(string text)
-        {
-            var rawTextEvent = new RawTextInputEventArgs(KeyboardDevice.Instance, (uint)DateTime.Now.Ticks, _view.GetInputRoot(), text);
-            _view.Input(rawTextEvent);
-        }
-
-        private void HandleKey(Key key, RawKeyEventType type)
-        {
-            var rawKeyEvent = new RawKeyEventArgs(KeyboardDevice.Instance, (uint)DateTime.Now.Ticks, _view.GetInputRoot(), type, key, RawInputModifiers.None);
-            _view.Input(rawKeyEvent);
-        }
-
-        //currently not found a way to get InputModifiers state
-        //private static InputModifiers GetModifierKeys(object e)
-        //{
-        //    var im = InputModifiers.None;
-        //    //if (IsCtrlPressed) rv |= InputModifiers.Control;
-        //    //if (IsShiftPressed) rv |= InputModifiers.Shift;
-
-        //    return im;
-        //}
-
-        private bool NeedsKeyboard(IInputElement element)
-        {
-            //may be some other elements
-            return element is TextBox;
-        }
-
-        private void TryShowHideKeyboard(IInputElement element, bool value)
-        {
-            if (value)
-            {
-                _view.BecomeFirstResponder();
-            }
-            else
-            {
-                _view.ResignFirstResponder();
-            }
-        }
-
-        public void UpdateKeyboardState(IInputElement element)
-        {
-            var focusedElement = element;
-            bool oldValue = NeedsKeyboard(_lastFocusedElement);
-            bool newValue = NeedsKeyboard(focusedElement);
-
-            if (newValue != oldValue || newValue)
-            {
-                TryShowHideKeyboard(focusedElement, newValue);
-            }
-
-            _lastFocusedElement = element;
-        }
-
-        public void ActivateAutoShowKeyboard()
-        {
-            var kbDevice = (KeyboardDevice.Instance as INotifyPropertyChanged);
-
-            //just in case we've called more than once the method
-            kbDevice.PropertyChanged -= KeyboardDevice_PropertyChanged;
-            kbDevice.PropertyChanged += KeyboardDevice_PropertyChanged;
-        }
-
-        private void KeyboardDevice_PropertyChanged(object sender, PropertyChangedEventArgs e)
-        {
-            if (e.PropertyName == nameof(KeyboardDevice.FocusedElement))
-            {
-                UpdateKeyboardState(KeyboardDevice.Instance.FocusedElement);
-            }
-        }
-
-        public void Dispose()
-        {
-            HandleEvents = false;
-        }
-    }
-
-    internal interface IGetInputRoot
-    {
-        IInputRoot GetInputRoot();
-    }
-}

+ 60 - 0
src/iOS/Avalonia.iOS/Stubs.cs

@@ -0,0 +1,60 @@
+using System;
+using System.IO;
+using Avalonia.Input;
+using Avalonia.Platform;
+
+namespace Avalonia.iOS
+{
+    class CursorFactoryStub : IStandardCursorFactory
+    {
+        public IPlatformHandle GetCursor(StandardCursorType cursorType) => new PlatformHandle(IntPtr.Zero, "NULL");
+    }
+
+    class WindowingPlatformStub : IWindowingPlatform
+    {
+        public IWindowImpl CreateWindow() => throw new NotSupportedException();
+
+        public IWindowImpl CreateEmbeddableWindow() => throw new NotSupportedException();
+    }
+    
+    class PlatformIconLoaderStub : IPlatformIconLoader
+    {
+        public IWindowIconImpl LoadIcon(IBitmapImpl bitmap)
+        {
+            using (var stream = new MemoryStream())
+            {
+                bitmap.Save(stream);
+                return LoadIcon(stream);
+            }
+        }
+
+        public IWindowIconImpl LoadIcon(Stream stream)
+        {
+            var ms = new MemoryStream();
+            stream.CopyTo(ms);
+            return new IconStub(ms);
+        }
+
+        public IWindowIconImpl LoadIcon(string fileName)
+        {
+            using (var file = File.Open(fileName, FileMode.Open))
+                return LoadIcon(file);
+        }
+    }
+
+    public class IconStub : IWindowIconImpl
+    {
+        private readonly MemoryStream _ms;
+
+        public IconStub(MemoryStream stream)
+        {
+            _ms = stream;
+        }
+
+        public void Save(Stream outputStream)
+        {
+            _ms.Position = 0;
+            _ms.CopyTo(outputStream);
+        }
+    }
+}

+ 0 - 158
src/iOS/Avalonia.iOS/TopLevelImpl.cs

@@ -1,158 +0,0 @@
-using System;
-using System.Collections.Generic;
-using Avalonia.Controls;
-using Avalonia.Controls.Platform.Surfaces;
-using Avalonia.Input;
-using Avalonia.Input.Raw;
-using Avalonia.iOS.Specific;
-using Avalonia.Platform;
-using Avalonia.Rendering;
-using CoreGraphics;
-using Foundation;
-using ObjCRuntime;
-using UIKit;
-
-namespace Avalonia.iOS
-{
-    [Adopts("UIKeyInput")]
-    class TopLevelImpl : UIView, ITopLevelImpl, IFramebufferPlatformSurface, IGetInputRoot
-    {
-        private readonly KeyboardEventsHelper<TopLevelImpl> _keyboardHelper;
-
-        private IInputRoot _inputRoot;
-
-        public TopLevelImpl()
-        {
-            _keyboardHelper = new KeyboardEventsHelper<TopLevelImpl>(this);
-            AutoresizingMask = UIViewAutoresizing.All;
-            _keyboardHelper.ActivateAutoShowKeyboard();
-
-            Surfaces = new object[] { this };
-        }
-
-        [Export("hasText")]
-        public bool HasText => _keyboardHelper.HasText();
-
-        [Export("insertText:")]
-        public void InsertText(string text) => _keyboardHelper.InsertText(text);
-
-        [Export("deleteBackward")]
-        public void DeleteBackward() => _keyboardHelper.DeleteBackward();
-
-        public override bool CanBecomeFirstResponder => _keyboardHelper.CanBecomeFirstResponder();
-        
-        public Action Closed { get; set; }
-        public Action<RawInputEventArgs> Input { get; set; }
-        public Action<Rect> Paint { get; set; }
-        public Action<Size> Resized { get; set; }
-        public Action<double> ScalingChanged { get; set; }
-
-        public new IPlatformHandle Handle => null;
-
-        public double RenderScaling => UIScreen.MainScreen.Scale;
-
-       
-        public override void LayoutSubviews() => Resized?.Invoke(ClientSize);
-
-        public Size ClientSize => Bounds.Size.ToAvalonia();
-
-        public IMouseDevice MouseDevice => iOSPlatform.MouseDevice;
-
-        public IRenderer CreateRenderer(IRenderRoot root)
-        {
-            return new ImmediateRenderer(root);
-        }
-
-        public override void Draw(CGRect rect)
-        {
-            Paint?.Invoke(new Rect(rect.X, rect.Y, rect.Width, rect.Height));
-        }
-
-        public void Invalidate(Rect rect) => SetNeedsDisplay();
-
-        public void SetInputRoot(IInputRoot inputRoot) => _inputRoot = inputRoot;
-
-        public Point PointToClient(PixelPoint point) => point.ToPoint(1);
-
-        public PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, 1);
-
-        public void SetCursor(IPlatformHandle cursor)
-        {
-            //Not supported
-        }
-
-        public IEnumerable<object> Surfaces { get; }
-        
-        public override void TouchesEnded(NSSet touches, UIEvent evt)
-        {
-            var touch = touches.AnyObject as UITouch;
-            if (touch != null)
-            {
-                var location = touch.LocationInView(this).ToAvalonia();
-
-                Input?.Invoke(new RawPointerEventArgs(
-                    iOSPlatform.MouseDevice,
-                    (uint)touch.Timestamp,
-                    _inputRoot,
-                    RawPointerEventType.LeftButtonUp,
-                    location,
-                    RawInputModifiers.None));
-            }
-        }
-
-        Point _touchLastPoint;
-        public override void TouchesBegan(NSSet touches, UIEvent evt)
-        {
-            var touch = touches.AnyObject as UITouch;
-            if (touch != null)
-            {
-                var location = touch.LocationInView(this).ToAvalonia();
-                _touchLastPoint = location;
-                Input?.Invoke(new RawPointerEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot,
-                    RawPointerEventType.Move, location, RawInputModifiers.None));
-
-                Input?.Invoke(new RawPointerEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot,
-                    RawPointerEventType.LeftButtonDown, location, RawInputModifiers.None));
-            }
-        }
-
-        public override void TouchesMoved(NSSet touches, UIEvent evt)
-        {
-            var touch = touches.AnyObject as UITouch;
-            if (touch != null)
-            {
-                var location = touch.LocationInView(this).ToAvalonia();
-                if (iOSPlatform.MouseDevice.Captured != null)
-                    Input?.Invoke(new RawPointerEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot,
-                        RawPointerEventType.Move, location, RawInputModifiers.LeftMouseButton));
-                else
-                {
-                    //magic number based on test - correction of 0.02 is working perfect
-                    double correction = 0.02;
-
-                    Input?.Invoke(new RawMouseWheelEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp,
-                        _inputRoot, location, (location - _touchLastPoint) * correction, RawInputModifiers.LeftMouseButton));
-                }
-                _touchLastPoint = location;
-            }
-        }
-        
-        public ILockedFramebuffer Lock() => new EmulatedFramebuffer(this);
-
-        public IPopupImpl CreatePopup() => null;
-
-        public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel)
-        {
-            //No-op
-        }
-
-        public IInputRoot GetInputRoot() => _inputRoot;
-
-        public Action LostFocus { get; set; }
-        public Action<WindowTransparencyLevel> TransparencyLevelChanged { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
-
-        public WindowTransparencyLevel TransparencyLevel => WindowTransparencyLevel.None;
-
-        public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(0, 0, 0);
-    }
-}

+ 52 - 0
src/iOS/Avalonia.iOS/TouchHandler.cs

@@ -0,0 +1,52 @@
+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;
+        }
+
+        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);
+
+                _device.ProcessRawEvent(ev);
+                
+                if (t.Phase == UITouchPhase.Cancelled || t.Phase == UITouchPhase.Ended)
+                    _knownTouches.Remove(t);
+            }
+        }
+        
+    }
+}

+ 0 - 23
src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs

@@ -1,23 +0,0 @@
-using System;
-using Avalonia.Platform;
-
-namespace Avalonia.iOS
-{
-    class WindowingPlatformImpl : IWindowingPlatform
-    {
-        public IWindowImpl CreateWindow()
-        {
-            throw new NotSupportedException();
-        }
-
-        public IWindowImpl CreateEmbeddableWindow()
-        {
-            throw new NotSupportedException();
-        }
-
-        public IPopupImpl CreatePopup()
-        {
-            throw new NotImplementedException();
-        }
-    }
-}

+ 0 - 49
src/iOS/Avalonia.iOS/iOSPlatform.cs

@@ -1,49 +0,0 @@
-using Avalonia.Controls;
-using Avalonia.Input;
-using Avalonia.Input.Platform;
-using Avalonia.iOS;
-using Avalonia.Platform;
-using Avalonia.Rendering;
-using Avalonia.Shared.PlatformSupport;
-
-namespace Avalonia
-{
-    public static class iOSApplicationExtensions
-    {
-        public static T UseiOS<T>(this T builder) where T : AppBuilderBase<T>, new()
-        {
-            builder.UseWindowingSubsystem(iOSPlatform.Initialize, "iOS");
-            return builder;
-        }
-    }
-}
-
-namespace Avalonia.iOS
-{
-    public class iOSPlatform
-    {
-        internal static MouseDevice MouseDevice;
-        internal static KeyboardDevice KeyboardDevice;
-
-        public static void Initialize()
-        {
-            MouseDevice = new MouseDevice();
-            KeyboardDevice = new KeyboardDevice();
-
-            AvaloniaLocator.CurrentMutable
-                .Bind<IRuntimePlatform>().ToSingleton<StandardRuntimePlatform>()
-                .Bind<IClipboard>().ToTransient<Clipboard>()
-                // TODO: what does this look like for iOS??
-                //.Bind<ISystemDialogImpl>().ToTransient<SystemDialogImpl>()
-                .Bind<IStandardCursorFactory>().ToTransient<CursorFactory>()
-                .Bind<IKeyboardDevice>().ToConstant(KeyboardDevice)
-                .Bind<IPlatformSettings>().ToSingleton<PlatformSettings>()
-                .Bind<IPlatformThreadingInterface>().ToConstant(PlatformThreadingInterface.Instance)
-                .Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoader>()
-                .Bind<IWindowingPlatform>().ToSingleton<WindowingPlatformImpl>()
-                .Bind<IRenderTimer>().ToSingleton<DisplayLinkRenderTimer>()
-                .Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
-                .Bind<IRenderLoop>().ToSingleton<RenderLoop>();
-        }
-    }
-}