committed by
GitHub
35 changed files with 1381 additions and 756 deletions
@ -1,13 +1,24 @@ |
|||||
<Project> |
<Project> |
||||
<Import Project="Sdk.props" Sdk="MSBuild.Sdk.Extras" /> |
<Import Project="Sdk.props" Sdk="MSBuild.Sdk.Extras" /> |
||||
<PropertyGroup> |
<PropertyGroup> |
||||
<TargetFramework>xamarin.ios10</TargetFramework> |
<TargetFramework>xamarin.ios10</TargetFramework> |
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> |
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> |
||||
|
<LangVersion>latest</LangVersion> |
||||
</PropertyGroup> |
</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> |
<ItemGroup> |
||||
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" /> |
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" /> |
||||
|
<ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" /> |
||||
</ItemGroup> |
</ItemGroup> |
||||
<Import Project="..\..\Shared\PlatformSupport\PlatformSupport.projitems" Label="Shared" /> |
|
||||
<Import Project="..\..\..\build\Rx.props" /> |
|
||||
<Import Project="Sdk.targets" Sdk="MSBuild.Sdk.Extras" /> |
<Import Project="Sdk.targets" Sdk="MSBuild.Sdk.Extras" /> |
||||
</Project> |
</Project> |
||||
|
|||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 CoreGraphics; |
||||
|
using Foundation; |
||||
|
using ObjCRuntime; |
||||
|
using OpenGLES; |
||||
using UIKit; |
using UIKit; |
||||
|
|
||||
namespace Avalonia.iOS |
namespace Avalonia.iOS |
||||
{ |
{ |
||||
public class AvaloniaView : UIView |
public partial class AvaloniaView : UIView |
||||
{ |
{ |
||||
private EmbeddableImpl _impl; |
internal IInputRoot InputRoot { get; private set; } |
||||
private EmbeddableControlRoot _root; |
private TopLevelImpl _topLevelImpl; |
||||
private Thickness _padding; |
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; } |
private readonly AvaloniaView _view; |
||||
set |
public AvaloniaView View => _view; |
||||
|
|
||||
|
public TopLevelImpl(AvaloniaView view) |
||||
|
{ |
||||
|
_view = view; |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
{ |
{ |
||||
_padding = value; |
// No-op
|
||||
SetNeedsLayout(); |
|
||||
} |
} |
||||
|
|
||||
|
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() |
||||
{ |
{ |
||||
|
return new Class(typeof(CAEAGLLayer)); |
||||
_impl = new EmbeddableImpl(); |
|
||||
AddSubview(_impl); |
|
||||
BackgroundColor = UIColor.White; |
|
||||
AutoresizingMask = UIViewAutoresizing.All; |
|
||||
_root = new EmbeddableControlRoot(_impl); |
|
||||
_root.Prepare(); |
|
||||
} |
} |
||||
|
|
||||
|
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() |
public override void LayoutSubviews() |
||||
{ |
{ |
||||
_impl.Frame = new CGRect(Padding.Left, Padding.Top, |
_topLevelImpl.Resized?.Invoke(_topLevelImpl.ClientSize); |
||||
Frame.Width - Padding.Left - Padding.Right, |
base.LayoutSubviews(); |
||||
Frame.Height - Padding.Top - Padding.Bottom); |
|
||||
} |
} |
||||
|
|
||||
|
public Control Content |
||||
public object Content |
|
||||
{ |
{ |
||||
get { return _root.Content; } |
get => (Control)_topLevel.Content; |
||||
set { _root.Content = value; } |
set => _topLevel.Content = value; |
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
@ -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; } |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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 |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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"); |
|
||||
} |
|
||||
} |
|
||||
@ -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
|
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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; } |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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; |
|
||||
} |
|
||||
} |
|
||||
@ -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 |
|
||||
}; |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,7 @@ |
|||||
|
namespace Avalonia.iOS |
||||
|
{ |
||||
|
public class SingleViewLifetime |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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(); |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -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(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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>(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue