csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
415 lines
15 KiB
415 lines
15 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Runtime.Versioning;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.Embedding;
|
|
using Avalonia.Controls.Platform;
|
|
using Avalonia.Controls.Primitives;
|
|
using Avalonia.Data;
|
|
using Avalonia.Input;
|
|
using Avalonia.Input.Platform;
|
|
using Avalonia.Input.Raw;
|
|
using Avalonia.Input.TextInput;
|
|
using Avalonia.Platform;
|
|
using Avalonia.Platform.Storage;
|
|
using Avalonia.Rendering.Composition;
|
|
using CoreAnimation;
|
|
using Foundation;
|
|
using ObjCRuntime;
|
|
using UIKit;
|
|
using IInsetsManager = Avalonia.Controls.Platform.IInsetsManager;
|
|
|
|
namespace Avalonia.iOS
|
|
{
|
|
/// <summary>
|
|
/// Root view container for Avalonia content, that can be embedded into iOS visual tree.
|
|
/// </summary>
|
|
public partial class AvaloniaView : UIView, ITextInputMethodImpl
|
|
{
|
|
internal IInputRoot InputRoot
|
|
=> _inputRoot ?? throw new InvalidOperationException($"{nameof(IWindowImpl.SetInputRoot)} must have been called");
|
|
internal TopLevel TopLevel => _topLevel;
|
|
|
|
private readonly TopLevelImpl _topLevelImpl;
|
|
private readonly EmbeddableControlRoot _topLevel;
|
|
private readonly InputHandler _input;
|
|
private TextInputMethodClient? _client;
|
|
private IAvaloniaViewController? _controller;
|
|
private IInputRoot? _inputRoot;
|
|
private Metal.MetalRenderTarget? _currentRenderTarget;
|
|
private (PixelSize size, double scaling) _latestLayoutProps;
|
|
|
|
public AvaloniaView()
|
|
{
|
|
_topLevelImpl = new TopLevelImpl(this);
|
|
_input = new InputHandler(this, _topLevelImpl);
|
|
_topLevel = new EmbeddableControlRoot(_topLevelImpl);
|
|
|
|
_topLevel.Prepare();
|
|
|
|
_topLevel.StartRendering();
|
|
|
|
InitLayerSurface();
|
|
|
|
// Remote touch handling
|
|
if (OperatingSystem.IsTvOS())
|
|
{
|
|
AddGestureRecognizer(new UISwipeGestureRecognizer(_input.Handle)
|
|
{
|
|
Direction = UISwipeGestureRecognizerDirection.Up
|
|
});
|
|
AddGestureRecognizer(new UISwipeGestureRecognizer(_input.Handle)
|
|
{
|
|
Direction = UISwipeGestureRecognizerDirection.Right
|
|
});
|
|
AddGestureRecognizer(new UISwipeGestureRecognizer(_input.Handle)
|
|
{
|
|
Direction = UISwipeGestureRecognizerDirection.Down
|
|
});
|
|
AddGestureRecognizer(new UISwipeGestureRecognizer(_input.Handle)
|
|
{
|
|
Direction = UISwipeGestureRecognizerDirection.Left
|
|
});
|
|
}
|
|
else if (OperatingSystem.IsIOS() || OperatingSystem.IsMacCatalyst())
|
|
{
|
|
#if !TVOS
|
|
MultipleTouchEnabled = true;
|
|
|
|
if (OperatingSystem.IsIOSVersionAtLeast(13, 4) || OperatingSystem.IsMacCatalyst())
|
|
{
|
|
var scrollGestureRecognizer = new UIPanGestureRecognizer(_input.HandleScrollWheel)
|
|
{
|
|
// Only respond to scroll events, not touches
|
|
MaximumNumberOfTouches = 0,
|
|
AllowedScrollTypesMask = UIScrollTypeMask.Discrete | UIScrollTypeMask.Continuous
|
|
};
|
|
AddGestureRecognizer(scrollGestureRecognizer);
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
[SuppressMessage("Interoperability", "CA1422:Validate platform compatibility")]
|
|
private void InitLayerSurface()
|
|
{
|
|
var l = Layer;
|
|
l.ContentsScale = UIScreen.MainScreen.Scale;
|
|
l.Opaque = true;
|
|
#if !MACCATALYST
|
|
if (l is CAEAGLLayer eaglLayer)
|
|
{
|
|
eaglLayer.DrawableProperties = new NSDictionary(
|
|
OpenGLES.EAGLDrawableProperty.RetainedBacking, false,
|
|
OpenGLES.EAGLDrawableProperty.ColorFormat, OpenGLES.EAGLColorFormat.RGBA8
|
|
);
|
|
_topLevelImpl.Surfaces = new[] { new Eagl.EaglLayerSurface(eaglLayer) };
|
|
}
|
|
else
|
|
#endif
|
|
if (l is CAMetalLayer metalLayer)
|
|
{
|
|
metalLayer.Opaque = false;
|
|
_topLevelImpl.Surfaces = new[] { new Metal.MetalPlatformSurface(metalLayer, this) };
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool CanBecomeFirstResponder => true;
|
|
|
|
/// <inheritdoc />
|
|
public override bool CanResignFirstResponder => true;
|
|
|
|
/// <inheritdoc />
|
|
[ObsoletedOSPlatform("ios17.0", "Use the 'UITraitChangeObservable' protocol instead.")]
|
|
[ObsoletedOSPlatform("maccatalyst17.0", "Use the 'UITraitChangeObservable' protocol instead.")]
|
|
[ObsoletedOSPlatform("tvos17.0", "Use the 'UITraitChangeObservable' protocol instead.")]
|
|
[SupportedOSPlatform("ios")]
|
|
[SupportedOSPlatform("tvos")]
|
|
[SupportedOSPlatform("maccatalyst")]
|
|
public override void TraitCollectionDidChange(UITraitCollection? previousTraitCollection)
|
|
{
|
|
base.TraitCollectionDidChange(previousTraitCollection);
|
|
|
|
var settings = AvaloniaLocator.Current.GetRequiredService<IPlatformSettings>() as PlatformSettings;
|
|
settings?.TraitCollectionDidChange();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void TintColorDidChange()
|
|
{
|
|
base.TintColorDidChange();
|
|
|
|
var settings = AvaloniaLocator.Current.GetRequiredService<IPlatformSettings>() as PlatformSettings;
|
|
settings?.TraitCollectionDidChange();
|
|
}
|
|
|
|
public void InitWithController<TController>(TController controller)
|
|
where TController : UIViewController, IAvaloniaViewController
|
|
{
|
|
_controller = controller;
|
|
_topLevelImpl._insetsManager.InitWithController(controller);
|
|
}
|
|
|
|
internal class TopLevelImpl : ITopLevelImpl
|
|
{
|
|
private readonly AvaloniaView _view;
|
|
private readonly INativeControlHostImpl _nativeControlHost;
|
|
internal readonly InsetsManager _insetsManager;
|
|
private readonly IStorageProvider? _storageProvider;
|
|
private readonly IClipboard? _clipboard;
|
|
private readonly IInputPane? _inputPane;
|
|
private IDisposable? _paddingInsets;
|
|
|
|
public AvaloniaView View => _view;
|
|
|
|
public TopLevelImpl(AvaloniaView view)
|
|
{
|
|
_view = view;
|
|
Handle = new UIViewControlHandle(_view);
|
|
|
|
_nativeControlHost = new NativeControlHostImpl(view);
|
|
#if TVOS
|
|
_storageProvider = null;
|
|
_clipboard = null;
|
|
_inputPane = null;
|
|
#else
|
|
_storageProvider = new Storage.IOSStorageProvider(view);
|
|
_clipboard = new ClipboardImpl();
|
|
_inputPane = UIKitInputPane.Instance;
|
|
#endif
|
|
_insetsManager = new InsetsManager();
|
|
_insetsManager.DisplayEdgeToEdgeChanged += (_, edgeToEdge) =>
|
|
{
|
|
// iOS doesn't add any paddings/margins to the application by itself.
|
|
// Application is fully responsible for safe area paddings.
|
|
// So, unlikely to android, we need to "fake" safe area insets when edge to edge is disabled.
|
|
_paddingInsets?.Dispose();
|
|
if (!edgeToEdge && view._controller is { } controller)
|
|
{
|
|
_paddingInsets = view._topLevel.SetValue(
|
|
TemplatedControl.PaddingProperty,
|
|
controller.SafeAreaPadding,
|
|
BindingPriority.Style); // lower priority, so it can be redefined by user
|
|
}
|
|
};
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
// No-op
|
|
}
|
|
|
|
public Compositor Compositor => Platform.Compositor
|
|
?? throw new InvalidOperationException("iOS backend wasn't initialized. Make sure UseiOS was called.");
|
|
|
|
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(ICursorImpl? cursor)
|
|
{
|
|
// no-op
|
|
}
|
|
|
|
public IPopupImpl? CreatePopup()
|
|
{
|
|
// In-window popups
|
|
return null;
|
|
}
|
|
|
|
public void SetTransparencyLevelHint(IReadOnlyList<WindowTransparencyLevel> transparencyLevel)
|
|
{
|
|
// No-op
|
|
}
|
|
|
|
public double DesktopScaling => RenderScaling;
|
|
public IPlatformHandle? Handle { get; }
|
|
public Size ClientSize => new Size(_view.Bounds.Width, _view.Bounds.Height);
|
|
public Size? FrameSize => null;
|
|
public double RenderScaling => _view.ContentScaleFactor;
|
|
public IEnumerable<object> Surfaces { get; set; } = Array.Empty<object>();
|
|
public Action<RawInputEventArgs>? Input { get; set; }
|
|
public Action<Rect>? Paint { get; set; }
|
|
public Action<Size, WindowResizeReason>? 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; }
|
|
|
|
public WindowTransparencyLevel TransparencyLevel => WindowTransparencyLevel.None;
|
|
|
|
public void SetFrameThemeVariant(PlatformThemeVariant themeVariant)
|
|
{
|
|
#if !TVOS
|
|
// TODO adjust status bar depending on full screen mode.
|
|
if ((OperatingSystem.IsIOSVersionAtLeast(13)
|
|
|| OperatingSystem.IsMacCatalyst())
|
|
&& _view._controller is not null)
|
|
{
|
|
_view._controller.PreferredStatusBarStyle = themeVariant switch
|
|
{
|
|
PlatformThemeVariant.Light => UIStatusBarStyle.DarkContent,
|
|
PlatformThemeVariant.Dark => UIStatusBarStyle.LightContent,
|
|
_ => UIStatusBarStyle.Default
|
|
};
|
|
}
|
|
#endif
|
|
}
|
|
|
|
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } =
|
|
new AcrylicPlatformCompensationLevels();
|
|
|
|
public object? TryGetFeature(Type featureType)
|
|
{
|
|
if (featureType == typeof(ITextInputMethodImpl))
|
|
{
|
|
return _view;
|
|
}
|
|
|
|
if (featureType == typeof(INativeControlHostImpl))
|
|
{
|
|
return _nativeControlHost;
|
|
}
|
|
|
|
if (featureType == typeof(IInsetsManager))
|
|
{
|
|
return _insetsManager;
|
|
}
|
|
|
|
if (featureType == typeof(IClipboard))
|
|
{
|
|
return _clipboard;
|
|
}
|
|
|
|
if (featureType == typeof(IStorageProvider))
|
|
{
|
|
return _storageProvider;
|
|
}
|
|
|
|
if (featureType == typeof(IInputPane))
|
|
{
|
|
return _inputPane;
|
|
}
|
|
|
|
if (featureType == typeof(ILauncher))
|
|
{
|
|
return new IOSLauncher();
|
|
}
|
|
|
|
if (featureType == typeof(IScreenImpl))
|
|
{
|
|
return (iOSScreens)AvaloniaLocator.Current.GetRequiredService<IScreenImpl>();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
[Export("layerClass")]
|
|
public static Class LayerClass()
|
|
{
|
|
#if !MACCATALYST
|
|
if (Platform.Graphics is Eagl.EaglPlatformGraphics)
|
|
{
|
|
return new Class(typeof(CAEAGLLayer));
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
return new Class(typeof(CAMetalLayer));
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override void TouchesBegan(NSSet touches, UIEvent? evt) => _input.Handle(touches, evt);
|
|
|
|
/// <inheritdoc/>
|
|
public override void TouchesMoved(NSSet touches, UIEvent? evt) => _input.Handle(touches, evt);
|
|
|
|
/// <inheritdoc/>
|
|
public override void TouchesEnded(NSSet touches, UIEvent? evt) => _input.Handle(touches, evt);
|
|
|
|
/// <inheritdoc/>
|
|
public override void TouchesCancelled(NSSet touches, UIEvent? evt) => _input.Handle(touches, evt);
|
|
|
|
/// <inheritdoc/>
|
|
public override void PressesBegan(NSSet<UIPress> presses, UIPressesEvent evt)
|
|
{
|
|
if (!_input.Handle(presses, evt))
|
|
{
|
|
base.PressesBegan(presses, evt);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override void PressesChanged(NSSet<UIPress> presses, UIPressesEvent evt)
|
|
{
|
|
if (!_input.Handle(presses, evt))
|
|
{
|
|
base.PressesBegan(presses, evt);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override void PressesEnded(NSSet<UIPress> presses, UIPressesEvent evt)
|
|
{
|
|
if (!_input.Handle(presses, evt))
|
|
{
|
|
base.PressesEnded(presses, evt);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override void PressesCancelled(NSSet<UIPress> presses, UIPressesEvent evt)
|
|
{
|
|
if (!_input.Handle(presses, evt))
|
|
{
|
|
base.PressesCancelled(presses, evt);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override void LayoutSubviews()
|
|
{
|
|
_topLevelImpl.Resized?.Invoke(_topLevelImpl.ClientSize, WindowResizeReason.Layout);
|
|
var scaling = (double)ContentScaleFactor;
|
|
if (_latestLayoutProps.scaling != scaling)
|
|
{
|
|
_topLevelImpl.ScalingChanged?.Invoke(scaling);
|
|
}
|
|
|
|
_latestLayoutProps = (new PixelSize((int)(Bounds.Width * scaling), (int)(Bounds.Height * scaling)), scaling);
|
|
if (_currentRenderTarget is not null)
|
|
{
|
|
_currentRenderTarget.PendingLayout = _latestLayoutProps;
|
|
}
|
|
|
|
base.LayoutSubviews();
|
|
}
|
|
|
|
public Control? Content
|
|
{
|
|
get => (Control?)_topLevel.Content;
|
|
set => _topLevel.Content = value;
|
|
}
|
|
|
|
internal void SetRenderTarget(Metal.MetalRenderTarget target)
|
|
{
|
|
_currentRenderTarget = target;
|
|
_currentRenderTarget.PendingLayout = _latestLayoutProps;
|
|
}
|
|
}
|
|
}
|
|
|