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.
425 lines
14 KiB
425 lines
14 KiB
using Avalonia.Controls.ApplicationLifetimes;
|
|
using Avalonia.Controls.Embedding;
|
|
using Avalonia.Controls.Platform;
|
|
using Avalonia.Input;
|
|
using Avalonia.Input.Raw;
|
|
using Avalonia.Input.TextInput;
|
|
using Avalonia.Platform.Storage;
|
|
using Avalonia.Rendering;
|
|
using Avalonia.Web.Blazor.Interop;
|
|
using Avalonia.Web.Blazor.Interop.Storage;
|
|
|
|
using Microsoft.AspNetCore.Components;
|
|
using Microsoft.AspNetCore.Components.Web;
|
|
using Microsoft.JSInterop;
|
|
|
|
using SkiaSharp;
|
|
using HTMLPointerEventArgs = Microsoft.AspNetCore.Components.Web.PointerEventArgs;
|
|
|
|
namespace Avalonia.Web.Blazor
|
|
{
|
|
public partial class AvaloniaView : ITextInputMethodImpl
|
|
{
|
|
private readonly RazorViewTopLevelImpl _topLevelImpl;
|
|
private EmbeddableControlRoot _topLevel;
|
|
|
|
// Interop
|
|
private SKHtmlCanvasInterop? _interop = null;
|
|
private SizeWatcherInterop? _sizeWatcher = null;
|
|
private DpiWatcherInterop? _dpiWatcher = null;
|
|
private SKHtmlCanvasInterop.GLInfo? _jsGlInfo = null;
|
|
private AvaloniaModule? _avaloniaModule = null;
|
|
private InputHelperInterop? _inputHelper = null;
|
|
private InputHelperInterop? _canvasHelper = null;
|
|
private NativeControlHostInterop? _nativeControlHost = null;
|
|
private StorageProviderInterop? _storageProvider = null;
|
|
private ElementReference _htmlCanvas;
|
|
private ElementReference _inputElement;
|
|
private ElementReference _nativeControlsContainer;
|
|
private double _dpi = 1;
|
|
private SKSize _canvasSize = new (100, 100);
|
|
|
|
private GRContext? _context;
|
|
private GRGlInterface? _glInterface;
|
|
private const SKColorType ColorType = SKColorType.Rgba8888;
|
|
|
|
private bool _initialised;
|
|
private bool _useGL;
|
|
private bool _inputElementFocused;
|
|
|
|
[Inject] private IJSRuntime Js { get; set; } = null!;
|
|
|
|
public AvaloniaView()
|
|
{
|
|
_topLevelImpl = new RazorViewTopLevelImpl(this);
|
|
|
|
_topLevel = new EmbeddableControlRoot(_topLevelImpl);
|
|
|
|
if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime lifetime)
|
|
{
|
|
_topLevel.Content = lifetime.MainView;
|
|
}
|
|
}
|
|
|
|
internal INativeControlHostImpl GetNativeControlHostImpl()
|
|
{
|
|
return _nativeControlHost ?? throw new InvalidOperationException("Blazor View wasn't initialized yet");
|
|
}
|
|
|
|
internal IStorageProvider GetStorageProvider()
|
|
{
|
|
return _storageProvider ?? throw new InvalidOperationException("Blazor View wasn't initialized yet");
|
|
}
|
|
|
|
private void OnPointerCancel(HTMLPointerEventArgs e)
|
|
{
|
|
if (e.PointerType == "touch")
|
|
{
|
|
_topLevelImpl.RawPointerEvent(RawPointerEventType.TouchCancel, e.PointerType, GetPointFromEventArgs(e),
|
|
GetModifiers(e), e.PointerId);
|
|
}
|
|
}
|
|
|
|
private void OnPointerMove(HTMLPointerEventArgs e)
|
|
{
|
|
var type = e.PointerType switch
|
|
{
|
|
"touch" => RawPointerEventType.TouchUpdate,
|
|
_ => RawPointerEventType.Move
|
|
};
|
|
|
|
_topLevelImpl.RawPointerEvent(type, e.PointerType, GetPointFromEventArgs(e), GetModifiers(e), e.PointerId);
|
|
}
|
|
|
|
private void OnPointerUp(HTMLPointerEventArgs e)
|
|
{
|
|
var type = e.PointerType switch
|
|
{
|
|
"touch" => RawPointerEventType.TouchEnd,
|
|
_ => e.Button switch
|
|
{
|
|
0 => RawPointerEventType.LeftButtonUp,
|
|
1 => RawPointerEventType.MiddleButtonUp,
|
|
2 => RawPointerEventType.RightButtonUp,
|
|
3 => RawPointerEventType.XButton1Up,
|
|
4 => RawPointerEventType.XButton2Up,
|
|
// 5 => Pen eraser button,
|
|
_ => RawPointerEventType.Move
|
|
}
|
|
};
|
|
|
|
_topLevelImpl.RawPointerEvent(type, e.PointerType, GetPointFromEventArgs(e), GetModifiers(e), e.PointerId);
|
|
}
|
|
|
|
private void OnPointerDown(HTMLPointerEventArgs e)
|
|
{
|
|
var type = e.PointerType switch
|
|
{
|
|
"touch" => RawPointerEventType.TouchBegin,
|
|
_ => e.Button switch
|
|
{
|
|
0 => RawPointerEventType.LeftButtonDown,
|
|
1 => RawPointerEventType.MiddleButtonDown,
|
|
2 => RawPointerEventType.RightButtonDown,
|
|
3 => RawPointerEventType.XButton1Down,
|
|
4 => RawPointerEventType.XButton2Down,
|
|
// 5 => Pen eraser button,
|
|
_ => RawPointerEventType.Move
|
|
}
|
|
};
|
|
|
|
_topLevelImpl.RawPointerEvent(type, e.PointerType, GetPointFromEventArgs(e), GetModifiers(e), e.PointerId);
|
|
}
|
|
|
|
private static RawPointerPoint GetPointFromEventArgs(HTMLPointerEventArgs args)
|
|
{
|
|
return new RawPointerPoint
|
|
{
|
|
Position = new Point(args.ClientX, args.ClientY),
|
|
Pressure = args.Pressure,
|
|
XTilt = args.TiltX,
|
|
YTilt = args.TiltY
|
|
// Twist = args.Twist - read from JS code directly when
|
|
};
|
|
}
|
|
|
|
private void OnWheel(WheelEventArgs e)
|
|
{
|
|
_topLevelImpl.RawMouseWheelEvent( new Point(e.ClientX, e.ClientY),
|
|
new Vector(-(e.DeltaX / 50), -(e.DeltaY / 50)), GetModifiers(e));
|
|
}
|
|
|
|
private static RawInputModifiers GetModifiers(MouseEventArgs e)
|
|
{
|
|
var modifiers = RawInputModifiers.None;
|
|
|
|
if (e.CtrlKey)
|
|
modifiers |= RawInputModifiers.Control;
|
|
if (e.AltKey)
|
|
modifiers |= RawInputModifiers.Alt;
|
|
if (e.ShiftKey)
|
|
modifiers |= RawInputModifiers.Shift;
|
|
if (e.MetaKey)
|
|
modifiers |= RawInputModifiers.Meta;
|
|
|
|
if ((e.Buttons & 1L) == 1)
|
|
modifiers |= RawInputModifiers.LeftMouseButton;
|
|
|
|
if ((e.Buttons & 2L) == 2)
|
|
modifiers |= e.Type == "pen" ? RawInputModifiers.PenBarrelButton : RawInputModifiers.RightMouseButton;
|
|
|
|
if ((e.Buttons & 4L) == 4)
|
|
modifiers |= RawInputModifiers.MiddleMouseButton;
|
|
|
|
if ((e.Buttons & 8L) == 8)
|
|
modifiers |= RawInputModifiers.XButton1MouseButton;
|
|
|
|
if ((e.Buttons & 16L) == 16)
|
|
modifiers |= RawInputModifiers.XButton2MouseButton;
|
|
|
|
if ((e.Buttons & 32L) == 32)
|
|
modifiers |= RawInputModifiers.PenEraser;
|
|
|
|
return modifiers;
|
|
}
|
|
|
|
private static RawInputModifiers GetModifiers(KeyboardEventArgs e)
|
|
{
|
|
var modifiers = RawInputModifiers.None;
|
|
|
|
if (e.CtrlKey)
|
|
modifiers |= RawInputModifiers.Control;
|
|
if (e.AltKey)
|
|
modifiers |= RawInputModifiers.Alt;
|
|
if (e.ShiftKey)
|
|
modifiers |= RawInputModifiers.Shift;
|
|
if (e.MetaKey)
|
|
modifiers |= RawInputModifiers.Meta;
|
|
|
|
return modifiers;
|
|
}
|
|
|
|
private void OnKeyDown(KeyboardEventArgs e)
|
|
{
|
|
_topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, e.Code, e.Key, GetModifiers(e));
|
|
}
|
|
|
|
private void OnKeyUp(KeyboardEventArgs e)
|
|
{
|
|
_topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, e.Code, e.Key, GetModifiers(e));
|
|
}
|
|
|
|
private void OnFocus(FocusEventArgs e)
|
|
{
|
|
// if focus has unexpectedly moved from the input element to the container element,
|
|
// shift it back to the input element
|
|
if (_inputElementFocused && _inputHelper is not null)
|
|
{
|
|
_inputHelper.Focus();
|
|
}
|
|
}
|
|
|
|
private void OnInput(ChangeEventArgs e)
|
|
{
|
|
if (e.Value != null)
|
|
{
|
|
var inputData = e.Value.ToString();
|
|
if (inputData != null)
|
|
{
|
|
_topLevelImpl.RawTextEvent(inputData);
|
|
}
|
|
}
|
|
|
|
_inputHelper?.Clear();
|
|
}
|
|
|
|
[Parameter(CaptureUnmatchedValues = true)]
|
|
public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (firstRender)
|
|
{
|
|
AvaloniaLocator.CurrentMutable.Bind<IJSInProcessRuntime>().ToConstant((IJSInProcessRuntime)Js);
|
|
|
|
_avaloniaModule = await AvaloniaModule.ImportAsync(Js);
|
|
|
|
_inputHelper = new InputHelperInterop(_avaloniaModule, _inputElement);
|
|
_canvasHelper = new InputHelperInterop(_avaloniaModule, _htmlCanvas);
|
|
|
|
_inputHelper.Hide();
|
|
_canvasHelper.SetCursor("default");
|
|
_topLevelImpl.SetCssCursor = x =>
|
|
{
|
|
_inputHelper.SetCursor(x); //macOS
|
|
_canvasHelper.SetCursor(x); //windows
|
|
};
|
|
|
|
_nativeControlHost = new NativeControlHostInterop(_avaloniaModule, _nativeControlsContainer);
|
|
_storageProvider = await StorageProviderInterop.ImportAsync(Js);
|
|
|
|
Console.WriteLine("starting html canvas setup");
|
|
_interop = new SKHtmlCanvasInterop(_avaloniaModule, _htmlCanvas, OnRenderFrame);
|
|
|
|
Console.WriteLine("Interop created");
|
|
|
|
var skiaOptions = AvaloniaLocator.Current.GetService<SkiaOptions>();
|
|
_useGL = skiaOptions?.CustomGpuFactory != null;
|
|
|
|
if (_useGL)
|
|
{
|
|
_jsGlInfo = _interop.InitGL();
|
|
Console.WriteLine("jsglinfo created - init gl");
|
|
}
|
|
else
|
|
{
|
|
var rasterInitialized = _interop.InitRaster();
|
|
Console.WriteLine("raster initialized: {0}", rasterInitialized);
|
|
}
|
|
|
|
if (_useGL)
|
|
{
|
|
// create the SkiaSharp context
|
|
if (_context == null)
|
|
{
|
|
Console.WriteLine("create glcontext");
|
|
_glInterface = GRGlInterface.Create();
|
|
_context = GRContext.CreateGl(_glInterface);
|
|
|
|
|
|
// bump the default resource cache limit
|
|
_context.SetResourceCacheLimit(skiaOptions?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024);
|
|
Console.WriteLine("glcontext created and resource limit set");
|
|
}
|
|
|
|
_topLevelImpl.SetSurface(_context, _jsGlInfo!, ColorType,
|
|
new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi);
|
|
}
|
|
else
|
|
{
|
|
_topLevelImpl.SetSurface(ColorType,
|
|
new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, _interop.PutImageData);
|
|
}
|
|
|
|
_interop.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
|
|
|
|
_initialised = true;
|
|
|
|
Threading.Dispatcher.UIThread.Post(async () =>
|
|
{
|
|
_interop.RequestAnimationFrame(true);
|
|
|
|
_sizeWatcher = new SizeWatcherInterop(_avaloniaModule, _htmlCanvas, OnSizeChanged);
|
|
_dpiWatcher = new DpiWatcherInterop(_avaloniaModule, OnDpiChanged);
|
|
|
|
_topLevel.Prepare();
|
|
|
|
_topLevel.Renderer.Start();
|
|
});
|
|
}
|
|
}
|
|
|
|
private void OnRenderFrame()
|
|
{
|
|
if (_useGL && (_jsGlInfo == null))
|
|
{
|
|
Console.WriteLine("nothing to render");
|
|
return;
|
|
}
|
|
if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0)
|
|
{
|
|
Console.WriteLine("nothing to render");
|
|
return;
|
|
}
|
|
|
|
ManualTriggerRenderTimer.Instance.RaiseTick();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_dpiWatcher?.Unsubscribe(OnDpiChanged);
|
|
_sizeWatcher?.Dispose();
|
|
_interop?.Dispose();
|
|
}
|
|
|
|
private void ForceBlit()
|
|
{
|
|
// Note: this is technically a hack, but it's a kinda unique use case when
|
|
// we want to blit the previous frame
|
|
// renderer doesn't have much control over the render target
|
|
// we render on the UI thread
|
|
// We also don't want to have it as a meaningful public API.
|
|
// Therefore we have InternalsVisibleTo hack here.
|
|
|
|
if (_topLevel.Renderer is DeferredRenderer dr)
|
|
{
|
|
dr.Render(true);
|
|
}
|
|
}
|
|
|
|
private void OnDpiChanged(double newDpi)
|
|
{
|
|
if (Math.Abs(_dpi - newDpi) > 0.0001)
|
|
{
|
|
_dpi = newDpi;
|
|
|
|
_interop!.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
|
|
|
|
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
|
|
|
|
ForceBlit();
|
|
}
|
|
}
|
|
|
|
private void OnSizeChanged(SKSize newSize)
|
|
{
|
|
if (_canvasSize != newSize)
|
|
{
|
|
_canvasSize = newSize;
|
|
|
|
_interop!.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
|
|
|
|
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
|
|
|
|
ForceBlit();
|
|
}
|
|
}
|
|
|
|
public void SetClient(ITextInputMethodClient? client)
|
|
{
|
|
if (_inputHelper is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_inputHelper.Clear();
|
|
|
|
var active = client is { };
|
|
|
|
if (active)
|
|
{
|
|
_inputHelper.Show();
|
|
_inputElementFocused = true;
|
|
_inputHelper.Focus();
|
|
}
|
|
else
|
|
{
|
|
_inputElementFocused = false;
|
|
_inputHelper.Hide();
|
|
}
|
|
}
|
|
|
|
public void SetCursorRect(Rect rect)
|
|
{
|
|
}
|
|
|
|
public void SetOptions(TextInputOptions options)
|
|
{
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
_inputHelper?.Clear();
|
|
}
|
|
}
|
|
}
|
|
|