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" /> |
|||
<PropertyGroup> |
|||
<TargetFramework>xamarin.ios10</TargetFramework> |
|||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> |
|||
<LangVersion>latest</LangVersion> |
|||
</PropertyGroup> |
|||
<ItemGroup> |
|||
<Compile Update="Boilerplate\Shared.cs"> |
|||
<SubType>Code</SubType> |
|||
</Compile> |
|||
<Compile Update="Boilerplate\RuntimePlatform.cs"> |
|||
<SubType>Code</SubType> |
|||
</Compile> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<Reference Include="OpenTK-1.0" /> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" /> |
|||
<ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" /> |
|||
</ItemGroup> |
|||
<Import Project="..\..\Shared\PlatformSupport\PlatformSupport.projitems" Label="Shared" /> |
|||
<Import Project="..\..\..\build\Rx.props" /> |
|||
<Import Project="Sdk.targets" Sdk="MSBuild.Sdk.Extras" /> |
|||
</Project> |
|||
|
|||
@ -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 Foundation; |
|||
using ObjCRuntime; |
|||
using OpenGLES; |
|||
using UIKit; |
|||
|
|||
namespace Avalonia.iOS |
|||
{ |
|||
public class AvaloniaView : UIView |
|||
public partial class AvaloniaView : UIView |
|||
{ |
|||
private EmbeddableImpl _impl; |
|||
private EmbeddableControlRoot _root; |
|||
private Thickness _padding; |
|||
internal IInputRoot InputRoot { get; private set; } |
|||
private TopLevelImpl _topLevelImpl; |
|||
private EmbeddableControlRoot _topLevel; |
|||
private TouchHandler _touches; |
|||
|
|||
public Thickness Padding |
|||
public AvaloniaView() |
|||
{ |
|||
_topLevelImpl = new TopLevelImpl(this); |
|||
_touches = new TouchHandler(this, _topLevelImpl); |
|||
_topLevel = new EmbeddableControlRoot(_topLevelImpl); |
|||
_topLevel.Prepare(); |
|||
|
|||
_topLevel.Renderer.Start(); |
|||
|
|||
var l = (CAEAGLLayer) Layer; |
|||
l.ContentsScale = UIScreen.MainScreen.Scale; |
|||
l.Opaque = true; |
|||
l.DrawableProperties = new NSDictionary( |
|||
EAGLDrawableProperty.RetainedBacking, false, |
|||
EAGLDrawableProperty.ColorFormat, EAGLColorFormat.RGBA8 |
|||
); |
|||
_topLevelImpl.Surfaces = new[] {new EaglLayerSurface(l)}; |
|||
MultipleTouchEnabled = true; |
|||
} |
|||
|
|||
internal class TopLevelImpl : ITopLevelImpl |
|||
{ |
|||
get { return _padding; } |
|||
set |
|||
private readonly AvaloniaView _view; |
|||
public AvaloniaView View => _view; |
|||
|
|||
public TopLevelImpl(AvaloniaView view) |
|||
{ |
|||
_view = view; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_padding = value; |
|||
SetNeedsLayout(); |
|||
// No-op
|
|||
} |
|||
|
|||
public IRenderer CreateRenderer(IRenderRoot root) => new DeferredRenderer(root, |
|||
AvaloniaLocator.Current.GetService<IRenderLoop>()); |
|||
|
|||
public void Invalidate(Rect rect) |
|||
{ |
|||
// No-op
|
|||
} |
|||
|
|||
public void SetInputRoot(IInputRoot inputRoot) |
|||
{ |
|||
_view.InputRoot = inputRoot; |
|||
} |
|||
|
|||
public Point PointToClient(PixelPoint point) => new Point(point.X, point.Y); |
|||
|
|||
public PixelPoint PointToScreen(Point point) => new PixelPoint((int) point.X, (int) point.Y); |
|||
|
|||
public void SetCursor(IPlatformHandle cursor) |
|||
{ |
|||
// no-op
|
|||
} |
|||
|
|||
public IPopupImpl CreatePopup() |
|||
{ |
|||
// In-window popups
|
|||
return null; |
|||
} |
|||
|
|||
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) |
|||
{ |
|||
// No-op
|
|||
} |
|||
|
|||
public Size ClientSize => new Size(_view.Bounds.Width, _view.Bounds.Height); |
|||
public double RenderScaling => _view.ContentScaleFactor; |
|||
public IEnumerable<object> Surfaces { get; set; } |
|||
public Action<RawInputEventArgs> Input { get; set; } |
|||
public Action<Rect> Paint { get; set; } |
|||
public Action<Size> Resized { get; set; } |
|||
public Action<double> ScalingChanged { get; set; } |
|||
public Action<WindowTransparencyLevel> TransparencyLevelChanged { get; set; } |
|||
public Action Closed { get; set; } |
|||
|
|||
public Action LostFocus { get; set; } |
|||
|
|||
// legacy no-op
|
|||
public IMouseDevice MouseDevice { get; } = new MouseDevice(); |
|||
public WindowTransparencyLevel TransparencyLevel { get; } |
|||
|
|||
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = |
|||
new AcrylicPlatformCompensationLevels(); |
|||
} |
|||
|
|||
public AvaloniaView() |
|||
[Export("layerClass")] |
|||
public static Class LayerClass() |
|||
{ |
|||
|
|||
_impl = new EmbeddableImpl(); |
|||
AddSubview(_impl); |
|||
BackgroundColor = UIColor.White; |
|||
AutoresizingMask = UIViewAutoresizing.All; |
|||
_root = new EmbeddableControlRoot(_impl); |
|||
_root.Prepare(); |
|||
return new Class(typeof(CAEAGLLayer)); |
|||
} |
|||
|
|||
public override void TouchesBegan(NSSet touches, UIEvent evt) => _touches.Handle(touches, evt); |
|||
|
|||
public override void TouchesMoved(NSSet touches, UIEvent evt) => _touches.Handle(touches, evt); |
|||
|
|||
public override void TouchesEnded(NSSet touches, UIEvent evt) => _touches.Handle(touches, evt); |
|||
|
|||
public override void TouchesCancelled(NSSet touches, UIEvent evt) => _touches.Handle(touches, evt); |
|||
|
|||
public override void LayoutSubviews() |
|||
{ |
|||
_impl.Frame = new CGRect(Padding.Left, Padding.Top, |
|||
Frame.Width - Padding.Left - Padding.Right, |
|||
Frame.Height - Padding.Top - Padding.Bottom); |
|||
_topLevelImpl.Resized?.Invoke(_topLevelImpl.ClientSize); |
|||
base.LayoutSubviews(); |
|||
} |
|||
|
|||
|
|||
public object Content |
|||
public Control Content |
|||
{ |
|||
get { return _root.Content; } |
|||
set { _root.Content = value; } |
|||
get => (Control)_topLevel.Content; |
|||
set => _topLevel.Content = value; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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