From c3a416aaaa009597709c56743b4059651409608e Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 31 Aug 2020 18:09:25 +0200 Subject: [PATCH] New iOS backend --- Avalonia.sln | 5 +- samples/ControlCatalog.iOS/AppDelegate.cs | 23 +- .../ControlCatalog.iOS.csproj | 1 + src/iOS/Avalonia.iOS/Avalonia.iOS.csproj | 17 +- src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs | 49 ++ .../AvaloniaRootViewController.cs | 58 -- src/iOS/Avalonia.iOS/AvaloniaView.Text.cs | 32 + src/iOS/Avalonia.iOS/AvaloniaView.cs | 143 ++++- src/iOS/Avalonia.iOS/AvaloniaWindow.cs | 26 - .../{ => Boilerplate}/AppBuilder.cs | 7 +- .../Boilerplate/RuntimePlatform.cs | 19 + src/iOS/Avalonia.iOS/Boilerplate/Shared.cs | 595 ++++++++++++++++++ .../{Clipboard.cs => ClipboardImpl.cs} | 4 +- src/iOS/Avalonia.iOS/CursorFactory.cs | 11 - .../Avalonia.iOS/DisplayLinkRenderTimer.cs | 32 - src/iOS/Avalonia.iOS/DisplayLinkTimer.cs | 37 ++ src/iOS/Avalonia.iOS/EaglDisplay.cs | 69 ++ src/iOS/Avalonia.iOS/EaglLayerSurface.cs | 94 +++ src/iOS/Avalonia.iOS/EmbeddableImpl.cs | 23 - src/iOS/Avalonia.iOS/EmulatedFramebuffer.cs | 60 -- src/iOS/Avalonia.iOS/Extensions.cs | 4 +- src/iOS/Avalonia.iOS/LayerFbo.cs | 143 +++++ src/iOS/Avalonia.iOS/Platform.cs | 50 ++ src/iOS/Avalonia.iOS/PlatformIconLoader.cs | 46 -- src/iOS/Avalonia.iOS/PlatformSettings.cs | 14 - .../PlatformThreadingInterface.cs | 33 +- src/iOS/Avalonia.iOS/RuntimeInfo.cs | 17 - src/iOS/Avalonia.iOS/SingleViewLifetime.cs | 7 + src/iOS/Avalonia.iOS/SoftKeyboardHelper.cs | 24 + .../Specific/KeyboardEventsHelper.cs | 152 ----- src/iOS/Avalonia.iOS/Stubs.cs | 60 ++ src/iOS/Avalonia.iOS/TopLevelImpl.cs | 158 ----- src/iOS/Avalonia.iOS/TouchHandler.cs | 52 ++ src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs | 23 - src/iOS/Avalonia.iOS/iOSPlatform.cs | 49 -- 35 files changed, 1381 insertions(+), 756 deletions(-) create mode 100644 src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs delete mode 100644 src/iOS/Avalonia.iOS/AvaloniaRootViewController.cs create mode 100644 src/iOS/Avalonia.iOS/AvaloniaView.Text.cs delete mode 100644 src/iOS/Avalonia.iOS/AvaloniaWindow.cs rename src/iOS/Avalonia.iOS/{ => Boilerplate}/AppBuilder.cs (57%) create mode 100644 src/iOS/Avalonia.iOS/Boilerplate/RuntimePlatform.cs create mode 100644 src/iOS/Avalonia.iOS/Boilerplate/Shared.cs rename src/iOS/Avalonia.iOS/{Clipboard.cs => ClipboardImpl.cs} (95%) delete mode 100644 src/iOS/Avalonia.iOS/CursorFactory.cs delete mode 100644 src/iOS/Avalonia.iOS/DisplayLinkRenderTimer.cs create mode 100644 src/iOS/Avalonia.iOS/DisplayLinkTimer.cs create mode 100644 src/iOS/Avalonia.iOS/EaglDisplay.cs create mode 100644 src/iOS/Avalonia.iOS/EaglLayerSurface.cs delete mode 100644 src/iOS/Avalonia.iOS/EmbeddableImpl.cs delete mode 100644 src/iOS/Avalonia.iOS/EmulatedFramebuffer.cs create mode 100644 src/iOS/Avalonia.iOS/LayerFbo.cs create mode 100644 src/iOS/Avalonia.iOS/Platform.cs delete mode 100644 src/iOS/Avalonia.iOS/PlatformIconLoader.cs delete mode 100644 src/iOS/Avalonia.iOS/PlatformSettings.cs delete mode 100644 src/iOS/Avalonia.iOS/RuntimeInfo.cs create mode 100644 src/iOS/Avalonia.iOS/SingleViewLifetime.cs create mode 100644 src/iOS/Avalonia.iOS/SoftKeyboardHelper.cs delete mode 100644 src/iOS/Avalonia.iOS/Specific/KeyboardEventsHelper.cs create mode 100644 src/iOS/Avalonia.iOS/Stubs.cs delete mode 100644 src/iOS/Avalonia.iOS/TopLevelImpl.cs create mode 100644 src/iOS/Avalonia.iOS/TouchHandler.cs delete mode 100644 src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs delete mode 100644 src/iOS/Avalonia.iOS/iOSPlatform.cs diff --git a/Avalonia.sln b/Avalonia.sln index c2b8243688..922c8f57dd 100644 --- a/Avalonia.sln +++ b/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 diff --git a/samples/ControlCatalog.iOS/AppDelegate.cs b/samples/ControlCatalog.iOS/AppDelegate.cs index a8fc6b30a0..f1c2241003 100644 --- a/samples/ControlCatalog.iOS/AppDelegate.cs +++ b/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 { - 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() - .UseiOS() - .UseSkia().SetupWithoutStarting(); - Window = new AvaloniaWindow() {Content = new MainView(), StatusBarColor = Colors.LightSteelBlue}; - Window.MakeKeyAndVisible(); - return true; - } + } -} \ No newline at end of file +} diff --git a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj index 3354df5597..2dbc095156 100644 --- a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj +++ b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj @@ -176,4 +176,5 @@ + diff --git a/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj index ef0d8068a2..d5b95b3151 100644 --- a/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj +++ b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj @@ -1,13 +1,24 @@ - + xamarin.ios10 true + latest + + + Code + + + Code + + + + + + - - diff --git a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs new file mode 100644 index 0000000000..b75aad17cf --- /dev/null +++ b/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 : 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(); + 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; + } + } + } +} \ No newline at end of file diff --git a/src/iOS/Avalonia.iOS/AvaloniaRootViewController.cs b/src/iOS/Avalonia.iOS/AvaloniaRootViewController.cs deleted file mode 100644 index 52f55d6574..0000000000 --- a/src/iOS/Avalonia.iOS/AvaloniaRootViewController.cs +++ /dev/null @@ -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(); - } - } -} \ No newline at end of file diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs b/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs new file mode 100644 index 0000000000..dc963726b0 --- /dev/null +++ b/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)); + } + } +} \ No newline at end of file diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index b1de4b8d47..7d367c99d1 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/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()); + + 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 Surfaces { get; set; } + public Action Input { get; set; } + public Action Paint { get; set; } + public Action Resized { get; set; } + public Action ScalingChanged { get; set; } + public Action 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; } } -} +} \ No newline at end of file diff --git a/src/iOS/Avalonia.iOS/AvaloniaWindow.cs b/src/iOS/Avalonia.iOS/AvaloniaWindow.cs deleted file mode 100644 index 19e2bb494c..0000000000 --- a/src/iOS/Avalonia.iOS/AvaloniaWindow.cs +++ /dev/null @@ -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; } - } - } -} diff --git a/src/iOS/Avalonia.iOS/AppBuilder.cs b/src/iOS/Avalonia.iOS/Boilerplate/AppBuilder.cs similarity index 57% rename from src/iOS/Avalonia.iOS/AppBuilder.cs rename to src/iOS/Avalonia.iOS/Boilerplate/AppBuilder.cs index cb8e0a7954..65143c939f 100644 --- a/src/iOS/Avalonia.iOS/AppBuilder.cs +++ b/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 { public AppBuilder() : base(new StandardRuntimePlatform(), - builder => StandardRuntimePlatformServices.Register(builder.ApplicationType.Assembly)) + b => StandardRuntimePlatformServices.Register(b.ApplicationType.Assembly)) { - + this.UseSkia().UseWindowingSubsystem(iOS.Platform.Register); } } -} +} \ No newline at end of file diff --git a/src/iOS/Avalonia.iOS/Boilerplate/RuntimePlatform.cs b/src/iOS/Avalonia.iOS/Boilerplate/RuntimePlatform.cs new file mode 100644 index 0000000000..c5c4d66450 --- /dev/null +++ b/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 + }; + } + } +} \ No newline at end of file diff --git a/src/iOS/Avalonia.iOS/Boilerplate/Shared.cs b/src/iOS/Avalonia.iOS/Boilerplate/Shared.cs new file mode 100644 index 0000000000..c6e6e01e64 --- /dev/null +++ b/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().ToConstant(standardPlatform) + .Bind().ToConstant(new AssetLoader(assembly)) + .Bind().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 Backtraces = new List(); + 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 AssemblyNameCache + = new Dictionary(); + + private AssemblyDescriptor _defaultResmAssembly; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The default assembly from which to load resm: assets for which no assembly is specified. + /// + public AssetLoader(Assembly assembly = null) + { + if (assembly == null) + assembly = Assembly.GetEntryAssembly(); + if (assembly != null) + _defaultResmAssembly = new AssemblyDescriptor(assembly); + } + + /// + /// Sets the default assembly from which to load assets for which no assembly is specified. + /// + /// The default assembly. + public void SetDefaultAssembly(Assembly assembly) + { + _defaultResmAssembly = new AssemblyDescriptor(assembly); + } + + /// + /// Checks if an asset with the specified URI exists. + /// + /// The URI. + /// + /// A base URI to use if is relative. + /// + /// True if the asset could be found; otherwise false. + public bool Exists(Uri uri, Uri baseUri = null) + { + return GetAsset(uri, baseUri) != null; + } + + /// + /// Opens the asset with the requested URI. + /// + /// The URI. + /// + /// A base URI to use if is relative. + /// + /// A stream containing the asset contents. + /// + /// The asset could not be found. + /// + public Stream Open(Uri uri, Uri baseUri = null) => OpenAndGetAssembly(uri, baseUri).Item1; + + /// + /// Opens the asset with the requested URI and returns the asset stream and the + /// assembly containing the asset. + /// + /// The URI. + /// + /// A base URI to use if is relative. + /// + /// + /// The stream containing the resource contents together with the assembly. + /// + /// + /// The asset could not be found. + /// + 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; + } + + /// + /// Gets all assets of a folder and subfolders that match specified uri. + /// + /// The URI. + /// Base URI that is used if is relative. + /// All matching assets as a tuple of the absolute path to the asset and the assembly containing the asset + public IEnumerable 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 = 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(); + path = path.TrimEnd('/') + '/'; + return asm.AvaloniaResources.Where(r => r.Key.StartsWith(path)) + .Select(x => new Uri($"avares://{asm.Name}{x.Key}")); + } + + return Enumerable.Empty(); + } + + 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 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 Resources { get; } + public Dictionary 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); + } + } +} \ No newline at end of file diff --git a/src/iOS/Avalonia.iOS/Clipboard.cs b/src/iOS/Avalonia.iOS/ClipboardImpl.cs similarity index 95% rename from src/iOS/Avalonia.iOS/Clipboard.cs rename to src/iOS/Avalonia.iOS/ClipboardImpl.cs index 8103d6ddf4..b9f74d69c6 100644 --- a/src/iOS/Avalonia.iOS/Clipboard.cs +++ b/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 GetTextAsync() { @@ -31,4 +31,4 @@ namespace Avalonia.iOS public Task GetDataAsync(string format) => throw new PlatformNotSupportedException(); } -} +} \ No newline at end of file diff --git a/src/iOS/Avalonia.iOS/CursorFactory.cs b/src/iOS/Avalonia.iOS/CursorFactory.cs deleted file mode 100644 index a3b2e8906d..0000000000 --- a/src/iOS/Avalonia.iOS/CursorFactory.cs +++ /dev/null @@ -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"); - } -} \ No newline at end of file diff --git a/src/iOS/Avalonia.iOS/DisplayLinkRenderTimer.cs b/src/iOS/Avalonia.iOS/DisplayLinkRenderTimer.cs deleted file mode 100644 index 0cefba7f19..0000000000 --- a/src/iOS/Avalonia.iOS/DisplayLinkRenderTimer.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using Avalonia.Rendering; -using CoreAnimation; -using Foundation; - -namespace Avalonia.iOS -{ - class DisplayLinkRenderTimer : IRenderTimer - { - public event Action 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 - } - } - } -} diff --git a/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs b/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs new file mode 100644 index 0000000000..df73479a65 --- /dev/null +++ b/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 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); + } + } +} \ No newline at end of file diff --git a/src/iOS/Avalonia.iOS/EaglDisplay.cs b/src/iOS/Avalonia.iOS/EaglDisplay.cs new file mode 100644 index 0000000000..635df43407 --- /dev/null +++ b/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; + } +} \ No newline at end of file diff --git a/src/iOS/Avalonia.iOS/EaglLayerSurface.cs b/src/iOS/Avalonia.iOS/EaglLayerSurface.cs new file mode 100644 index 0000000000..64912b8ae3 --- /dev/null +++ b/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); + } + } + } +} \ No newline at end of file diff --git a/src/iOS/Avalonia.iOS/EmbeddableImpl.cs b/src/iOS/Avalonia.iOS/EmbeddableImpl.cs deleted file mode 100644 index ce3c45d79b..0000000000 --- a/src/iOS/Avalonia.iOS/EmbeddableImpl.cs +++ /dev/null @@ -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; - } - } -} diff --git a/src/iOS/Avalonia.iOS/EmulatedFramebuffer.cs b/src/iOS/Avalonia.iOS/EmulatedFramebuffer.cs deleted file mode 100644 index 89c7aaf76c..0000000000 --- a/src/iOS/Avalonia.iOS/EmulatedFramebuffer.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Avalonia.Platform; -using CoreGraphics; -using UIKit; - -namespace Avalonia.iOS -{ - - /// - /// 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. - /// - 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; } - } -} - diff --git a/src/iOS/Avalonia.iOS/Extensions.cs b/src/iOS/Avalonia.iOS/Extensions.cs index 28708b20d8..bf6262e5c5 100644 --- a/src/iOS/Avalonia.iOS/Extensions.cs +++ b/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)); } -} +} \ No newline at end of file diff --git a/src/iOS/Avalonia.iOS/LayerFbo.cs b/src/iOS/Avalonia.iOS/LayerFbo.cs new file mode 100644 index 0000000000..907af58c7e --- /dev/null +++ b/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; + } +} \ No newline at end of file diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs new file mode 100644 index 0000000000..b484559ff3 --- /dev/null +++ b/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().ToConstant(GlFeature) + .Bind().ToConstant(new CursorFactoryStub()) + .Bind().ToConstant(new WindowingPlatformStub()) + .Bind().ToConstant(new ClipboardImpl()) + .Bind().ToConstant(new PlatformSettings()) + .Bind().ToConstant(new PlatformIconLoaderStub()) + .Bind().ToSingleton() + .Bind().ToSingleton() + .Bind().ToConstant(Timer) + .Bind().ToConstant(new PlatformThreadingInterface()) + .Bind().ToConstant(keyboard); + keyboard.PropertyChanged += (_, changed) => + { + if (changed.PropertyName == nameof(KeyboardDevice.FocusedElement)) + softKeyboard.UpdateKeyboard(keyboard.FocusedElement); + }; + } + + + } +} + diff --git a/src/iOS/Avalonia.iOS/PlatformIconLoader.cs b/src/iOS/Avalonia.iOS/PlatformIconLoader.cs deleted file mode 100644 index c539aba846..0000000000 --- a/src/iOS/Avalonia.iOS/PlatformIconLoader.cs +++ /dev/null @@ -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); - } - } -} diff --git a/src/iOS/Avalonia.iOS/PlatformSettings.cs b/src/iOS/Avalonia.iOS/PlatformSettings.cs deleted file mode 100644 index 9545795629..0000000000 --- a/src/iOS/Avalonia.iOS/PlatformSettings.cs +++ /dev/null @@ -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; - } -} diff --git a/src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs b/src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs index 7762e0e03b..fa36ab6c79 100644 --- a/src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs +++ b/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 }); } } -} +} \ No newline at end of file diff --git a/src/iOS/Avalonia.iOS/RuntimeInfo.cs b/src/iOS/Avalonia.iOS/RuntimeInfo.cs deleted file mode 100644 index 101561b246..0000000000 --- a/src/iOS/Avalonia.iOS/RuntimeInfo.cs +++ /dev/null @@ -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 - }; - } -} \ No newline at end of file diff --git a/src/iOS/Avalonia.iOS/SingleViewLifetime.cs b/src/iOS/Avalonia.iOS/SingleViewLifetime.cs new file mode 100644 index 0000000000..914f0ba548 --- /dev/null +++ b/src/iOS/Avalonia.iOS/SingleViewLifetime.cs @@ -0,0 +1,7 @@ +namespace Avalonia.iOS +{ + public class SingleViewLifetime + { + + } +} \ No newline at end of file diff --git a/src/iOS/Avalonia.iOS/SoftKeyboardHelper.cs b/src/iOS/Avalonia.iOS/SoftKeyboardHelper.cs new file mode 100644 index 0000000000..b05ab280d2 --- /dev/null +++ b/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(); + } + } + } +} \ No newline at end of file diff --git a/src/iOS/Avalonia.iOS/Specific/KeyboardEventsHelper.cs b/src/iOS/Avalonia.iOS/Specific/KeyboardEventsHelper.cs deleted file mode 100644 index 4d76539560..0000000000 --- a/src/iOS/Avalonia.iOS/Specific/KeyboardEventsHelper.cs +++ /dev/null @@ -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 -{ - /// - /// 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(); - /// - /// View that needs keyboard events and show/hide keyboard - internal class KeyboardEventsHelper 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().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; - } - - /// - /// HandleEvents in order to suspend keyboard notifications or resume it - /// - 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(); - } -} diff --git a/src/iOS/Avalonia.iOS/Stubs.cs b/src/iOS/Avalonia.iOS/Stubs.cs new file mode 100644 index 0000000000..a35b301a7f --- /dev/null +++ b/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); + } + } +} \ No newline at end of file diff --git a/src/iOS/Avalonia.iOS/TopLevelImpl.cs b/src/iOS/Avalonia.iOS/TopLevelImpl.cs deleted file mode 100644 index 662dfbea96..0000000000 --- a/src/iOS/Avalonia.iOS/TopLevelImpl.cs +++ /dev/null @@ -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 _keyboardHelper; - - private IInputRoot _inputRoot; - - public TopLevelImpl() - { - _keyboardHelper = new KeyboardEventsHelper(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 Input { get; set; } - public Action Paint { get; set; } - public Action Resized { get; set; } - public Action 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 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 TransparencyLevelChanged { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - - public WindowTransparencyLevel TransparencyLevel => WindowTransparencyLevel.None; - - public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(0, 0, 0); - } -} diff --git a/src/iOS/Avalonia.iOS/TouchHandler.cs b/src/iOS/Avalonia.iOS/TouchHandler.cs new file mode 100644 index 0000000000..43b19c85af --- /dev/null +++ b/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 _knownTouches = new Dictionary(); + + public void Handle(NSSet touches, UIEvent evt) + { + foreach (UITouch t in touches) + { + var pt = t.LocationInView(_view).ToAvalonia(); + if (!_knownTouches.TryGetValue(t, out var id)) + _knownTouches[t] = id = _nextTouchPointId++; + + var ev = new RawTouchEventArgs(_device, Ts(evt), Root, + t.Phase switch + { + UITouchPhase.Began => RawPointerEventType.TouchBegin, + UITouchPhase.Ended => RawPointerEventType.TouchEnd, + UITouchPhase.Cancelled => RawPointerEventType.TouchCancel, + _ => RawPointerEventType.TouchUpdate + }, pt, RawInputModifiers.None, id); + + _device.ProcessRawEvent(ev); + + if (t.Phase == UITouchPhase.Cancelled || t.Phase == UITouchPhase.Ended) + _knownTouches.Remove(t); + } + } + + } +} \ No newline at end of file diff --git a/src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs b/src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs deleted file mode 100644 index 8e7578e49e..0000000000 --- a/src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs +++ /dev/null @@ -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(); - } - } -} diff --git a/src/iOS/Avalonia.iOS/iOSPlatform.cs b/src/iOS/Avalonia.iOS/iOSPlatform.cs deleted file mode 100644 index b0092bc98a..0000000000 --- a/src/iOS/Avalonia.iOS/iOSPlatform.cs +++ /dev/null @@ -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(this T builder) where T : AppBuilderBase, 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().ToSingleton() - .Bind().ToTransient() - // TODO: what does this look like for iOS?? - //.Bind().ToTransient() - .Bind().ToTransient() - .Bind().ToConstant(KeyboardDevice) - .Bind().ToSingleton() - .Bind().ToConstant(PlatformThreadingInterface.Instance) - .Bind().ToSingleton() - .Bind().ToSingleton() - .Bind().ToSingleton() - .Bind().ToSingleton() - .Bind().ToSingleton(); - } - } -}