Browse Source

Add Browser implementation

pull/8110/head
Max Katz 4 years ago
parent
commit
855dade9d5
  1. 4
      samples/ControlCatalog.Web/App.razor.cs
  2. 34
      samples/ControlCatalog.Web/EmbedSample.Browser.cs
  3. 70
      samples/ControlCatalog.Web/Shared/MainLayout.razor.css
  4. 44
      samples/ControlCatalog.Web/wwwroot/css/app.css
  5. 11
      samples/ControlCatalog.Web/wwwroot/js/app.js
  6. 44
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor
  7. 43
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
  8. 152
      src/Web/Avalonia.Web.Blazor/Interop/NativeControlHostImpl.cs
  9. 56
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/NativeControlHost.ts
  10. 35
      src/Web/Avalonia.Web.Blazor/JSObjectControlHandle.cs
  11. 12
      src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs

4
samples/ControlCatalog.Web/App.razor.cs

@ -7,6 +7,10 @@ public partial class App
protected override void OnParametersSet()
{
WebAppBuilder.Configure<ControlCatalog.App>()
.AfterSetup(_ =>
{
ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb();
})
.SetupWithSingleViewLifetime();
base.OnParametersSet();

34
samples/ControlCatalog.Web/EmbedSample.Browser.cs

@ -0,0 +1,34 @@
using System;
using Avalonia;
using Avalonia.Platform;
using Avalonia.Web.Blazor;
using ControlCatalog.Pages;
using Microsoft.JSInterop;
namespace ControlCatalog.Web;
public class EmbedSampleWeb : INativeDemoControl
{
public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func<IPlatformHandle> createDefault)
{
var runtime = AvaloniaLocator.Current.GetRequiredService<IJSInProcessRuntime>();
if (isSecond)
{
var iframe = runtime.Invoke<IJSInProcessObjectReference>("document.createElement", "iframe");
iframe.InvokeVoid("setAttribute", "src", "https://www.youtube.com/embed/kZCIporjJ70");
return new JSObjectControlHandle(iframe);
}
else
{
// window.createAppButton source is defined in "app.js" file.
var button = runtime.Invoke<IJSInProcessObjectReference>("window.createAppButton");
return new JSObjectControlHandle(button);
}
}
}

70
samples/ControlCatalog.Web/Shared/MainLayout.razor.css

@ -1,70 +0,0 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
.main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
}
.top-row a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row:not(.auth) {
display: none;
}
.top-row.auth {
justify-content: space-between;
}
.top-row a, .top-row .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.main > div {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}

44
samples/ControlCatalog.Web/wwwroot/css/app.css

@ -44,47 +44,13 @@ a, .btn-link {
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
.canvas-container {
opacity:1;
background-color:#ccc;
position:fixed;
width:100%;
height:100%;
top:0px;
left:0px;
z-index:500;
}
canvas
{
opacity:1;
background-color:#ccc;
position:fixed;
width:100%;
height:100%;
top:0px;
left:0px;
z-index:500;
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
#app, .page {
height: 100%;
}
.overlay{
opacity:0.0;
background-color:#ccc;
position:fixed;
width:100vw;
height:100vh;
top:0px;
left:0px;
z-index:1000;
}

11
samples/ControlCatalog.Web/wwwroot/js/app.js

@ -1 +1,10 @@

window.createAppButton = function () {
var button = document.createElement('button');
button.innerText = 'Hello world';
var clickCount = 0;
button.onclick = () => {
clickCount++;
button.innerText = 'Click count ' + clickCount;
};
return button;
}

44
src/Web/Avalonia.Web.Blazor/AvaloniaView.razor

@ -10,10 +10,42 @@
onkeydown="@OnKeyDown"
onkeyup="@OnKeyUp">
<canvas @ref="_htmlCanvas" @attributes="AdditionalAttributes"/>
<input @ref="_inputElement"
class="overlay"
type="text"
oninput="@OnInput"/>
<canvas id="htmlCanvas" @ref="_htmlCanvas" @attributes="AdditionalAttributes"/>
<div id="nativeControlsContainer" @ref="_nativeControlsContainer" />
<input id="inputElement" @ref="_inputElement" type="text" oninput="@OnInput"/>
</div>
<style>
#htmlCanvas {
opacity: 1;
background-color: #ccc;
position: fixed;
width: 100vw;
height: 100vh;
top: 0px;
left: 0px;
z-index: 500;
}
#nativeControlsContainer {
position: fixed;
width: 100vw;
height: 100vh;
top: 0px;
left: 0px;
z-index: 700;
}
#inputElement {
opacity: 0.0;
position: fixed;
width: 100vw;
height: 100vh;
top: 0px;
left: 0px;
z-index: 1000;
}
</style>

43
src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs

@ -1,5 +1,6 @@
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Embedding;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
@ -18,14 +19,16 @@ namespace Avalonia.Web.Blazor
private EmbeddableControlRoot _topLevel;
// Interop
private SKHtmlCanvasInterop _interop = null!;
private SizeWatcherInterop _sizeWatcher = null!;
private DpiWatcherInterop _dpiWatcher = null!;
private SKHtmlCanvasInterop.GLInfo? _jsGlInfo = null!;
private InputHelperInterop _inputHelper = null!;
private InputHelperInterop _canvasHelper = null!;
private SKHtmlCanvasInterop? _interop = null;
private SizeWatcherInterop? _sizeWatcher = null;
private DpiWatcherInterop? _dpiWatcher = null;
private SKHtmlCanvasInterop.GLInfo? _jsGlInfo = null;
private InputHelperInterop? _inputHelper = null;
private InputHelperInterop? _canvasHelper = null;
private NativeControlHostInterop? _nativeControlHost = null;
private ElementReference _htmlCanvas;
private ElementReference _inputElement;
private ElementReference _nativeControlsContainer;
private double _dpi = 1;
private SKSize _canvasSize = new (100, 100);
@ -49,6 +52,11 @@ namespace Avalonia.Web.Blazor
}
}
internal INativeControlHostImpl GetNativeControlHostImpl()
{
return _nativeControlHost ?? throw new InvalidOperationException("Blazor View wasn't initialized yet");
}
private void OnTouchStart(TouchEventArgs e)
{
foreach (var touch in e.ChangedTouches)
@ -243,7 +251,7 @@ namespace Avalonia.Web.Blazor
}
}
_inputHelper.Clear();
_inputHelper?.Clear();
}
[Parameter(CaptureUnmatchedValues = true)]
@ -253,6 +261,8 @@ namespace Avalonia.Web.Blazor
{
if (firstRender)
{
AvaloniaLocator.CurrentMutable.Bind<IJSInProcessRuntime>().ToConstant((IJSInProcessRuntime)Js);
_inputHelper = await InputHelperInterop.ImportAsync(Js, _inputElement);
_canvasHelper = await InputHelperInterop.ImportAsync(Js, _htmlCanvas);
@ -264,6 +274,8 @@ namespace Avalonia.Web.Blazor
_canvasHelper.SetCursor(x); //windows
};
_nativeControlHost = await NativeControlHostInterop.ImportAsync(Js, _nativeControlsContainer);
Console.WriteLine("starting html canvas setup");
_interop = await SKHtmlCanvasInterop.ImportAsync(Js, _htmlCanvas, OnRenderFrame);
@ -319,9 +331,9 @@ namespace Avalonia.Web.Blazor
public void Dispose()
{
_dpiWatcher.Unsubscribe(OnDpiChanged);
_sizeWatcher.Dispose();
_interop.Dispose();
_dpiWatcher?.Unsubscribe(OnDpiChanged);
_sizeWatcher?.Dispose();
_interop?.Dispose();
}
private void ForceBlit()
@ -345,7 +357,7 @@ namespace Avalonia.Web.Blazor
{
_dpi = newDpi;
_interop.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
_interop!.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
@ -359,7 +371,7 @@ namespace Avalonia.Web.Blazor
{
_canvasSize = newSize;
_interop.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
_interop!.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
@ -369,6 +381,11 @@ namespace Avalonia.Web.Blazor
public void SetClient(ITextInputMethodClient? client)
{
if (_inputHelper is null)
{
return;
}
_inputHelper.Clear();
var active = client is { };
@ -394,7 +411,7 @@ namespace Avalonia.Web.Blazor
public void Reset()
{
_inputHelper.Clear();
_inputHelper?.Clear();
}
}
}

152
src/Web/Avalonia.Web.Blazor/Interop/NativeControlHostImpl.cs

@ -0,0 +1,152 @@
#nullable enable
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor.Interop
{
internal class NativeControlHostInterop : JSModuleInterop, INativeControlHostImpl
{
private const string JsFilename = "./_content/Avalonia.Web.Blazor/NativeControlHost.js";
private const string CreateDefaultChildSymbol = "NativeControlHost.CreateDefaultChild";
private const string CreateAttachmentSymbol = "NativeControlHost.CreateAttachment";
private const string GetReferenceSymbol = "NativeControlHost.GetReference";
private readonly ElementReference hostElement;
public static async Task<NativeControlHostInterop> ImportAsync(IJSRuntime js, ElementReference element)
{
var interop = new NativeControlHostInterop(js, element);
await interop.ImportAsync();
return interop;
}
public NativeControlHostInterop(IJSRuntime js, ElementReference element)
: base(js, JsFilename)
{
hostElement = element;
}
public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent)
{
var element = Invoke<IJSInProcessObjectReference>(CreateDefaultChildSymbol);
return new JSObjectControlHandle(element);
}
public INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func<IPlatformHandle, IPlatformHandle> create)
{
Attachment? a = null;
try
{
using var hostElementJsReference = Invoke<IJSInProcessObjectReference>(GetReferenceSymbol, hostElement);
var child = create(new JSObjectControlHandle(hostElementJsReference));
var attachmenetReference = Invoke<IJSInProcessObjectReference>(CreateAttachmentSymbol);
// It has to be assigned to the variable before property setter is called so we dispose it on exception
#pragma warning disable IDE0017 // Simplify object initialization
a = new Attachment(attachmenetReference, child);
#pragma warning restore IDE0017 // Simplify object initialization
a.AttachedTo = this;
return a;
}
catch
{
a?.Dispose();
throw;
}
}
public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle)
{
var attachmenetReference = Invoke<IJSInProcessObjectReference>(CreateAttachmentSymbol);
var a = new Attachment(attachmenetReference, handle);
a.AttachedTo = this;
return a;
}
public bool IsCompatibleWith(IPlatformHandle handle) => handle is JSObjectControlHandle;
class Attachment : INativeControlHostControlTopLevelAttachment
{
private const string InitializeWithChildHandleSymbol = "InitializeWithChildHandle";
private const string AttachToSymbol = "AttachTo";
private const string ShowInBoundsSymbol = "ShowInBounds";
private const string HideWithSizeSymbol = "HideWithSize";
private const string ReleaseChildSymbol = "ReleaseChild";
private IJSInProcessObjectReference? _native;
private NativeControlHostInterop? _attachedTo;
public Attachment(IJSInProcessObjectReference native, IPlatformHandle handle)
{
_native = native;
_native.InvokeVoid(InitializeWithChildHandleSymbol, ((JSObjectControlHandle)handle).Object);
}
public void Dispose()
{
if (_native != null)
{
_native.InvokeVoid(ReleaseChildSymbol);
_native.Dispose();
_native = null;
}
}
public INativeControlHostImpl? AttachedTo
{
get => _attachedTo!;
set
{
CheckDisposed();
var host = (NativeControlHostInterop?)value;
if (host == null)
{
_native.InvokeVoid(AttachToSymbol);
}
else
{
_native.InvokeVoid(AttachToSymbol, host.hostElement);
}
_attachedTo = host;
}
}
public bool IsCompatibleWith(INativeControlHostImpl host) => host is NativeControlHostInterop;
public void HideWithSize(Size size)
{
CheckDisposed();
if (_attachedTo == null)
return;
_native.InvokeVoid(HideWithSizeSymbol, Math.Max(1, (float)size.Width), Math.Max(1, (float)size.Height));
}
public void ShowInBounds(Rect bounds)
{
CheckDisposed();
if (_attachedTo == null)
throw new InvalidOperationException("Native control isn't attached to a toplevel");
bounds = new Rect(bounds.X, bounds.Y, Math.Max(1, bounds.Width),
Math.Max(1, bounds.Height));
_native.InvokeVoid(ShowInBoundsSymbol, (float)bounds.X, (float)bounds.Y, (float)bounds.Width, (float)bounds.Height);
}
[MemberNotNull(nameof(_native))]
private void CheckDisposed()
{
if (_native == null)
throw new ObjectDisposedException(nameof(Attachment));
}
}
}
}

56
src/Web/Avalonia.Web.Blazor/Interop/Typescript/NativeControlHost.ts

@ -0,0 +1,56 @@
export class NativeControlHost {
public static CreateDefaultChild(parent: HTMLElement): HTMLElement {
return document.createElement("div");
}
// Used to convert ElementReference to JSObjectReference.
// Is there a better way?
public static GetReference(element: Element): Element {
return element;
}
public static CreateAttachment(): NativeControlHostTopLevelAttachment {
return new NativeControlHostTopLevelAttachment();
}
}
class NativeControlHostTopLevelAttachment
{
_child: HTMLElement;
_host: HTMLElement;
InitializeWithChildHandle(child: HTMLElement) {
this._child = child;
this._child.style.position = "absolute";
}
AttachTo(host: HTMLElement): void {
if (this._host) {
this._host.removeChild(this._child);
}
this._host = host;
if (this._host) {
this._host.appendChild(this._child);
}
}
ShowInBounds(x: number, y: number, width: number, height: number): void {
this._child.style.top = y + "px";
this._child.style.left = x + "px";
this._child.style.width = width + "px";
this._child.style.height = height + "px";
this._child.style.display = "block";
}
HideWithSize(width: number, height: number): void {
this._child.style.width = width + "px";
this._child.style.height = height + "px";
this._child.style.display = "none";
}
ReleaseChild(): void {
this._child = null;
}
}

35
src/Web/Avalonia.Web.Blazor/JSObjectControlHandle.cs

@ -0,0 +1,35 @@
#nullable enable
using Avalonia.Controls.Platform;
using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor
{
public class JSObjectControlHandle : INativeControlHostDestroyableControlHandle
{
internal const string ElementReferenceDescriptor = "JSObjectReference";
public JSObjectControlHandle(IJSObjectReference reference)
{
Object = reference;
}
public IJSObjectReference Object { get; }
public IntPtr Handle => throw new NotSupportedException();
public string? HandleDescriptor => ElementReferenceDescriptor;
public void Destroy()
{
if (Object is IJSInProcessObjectReference inProcess)
{
inProcess.Dispose();
}
else
{
_ = Object.DisposeAsync();
}
}
}
}

12
src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs

@ -13,19 +13,19 @@ using SkiaSharp;
namespace Avalonia.Web.Blazor
{
internal class RazorViewTopLevelImpl : ITopLevelImplWithTextInputMethod
internal class RazorViewTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost
{
private Size _clientSize;
private BlazorSkiaSurface? _currentSurface;
private IInputRoot? _inputRoot;
private readonly Stopwatch _sw = Stopwatch.StartNew();
private readonly ITextInputMethodImpl _textInputMethod;
private readonly AvaloniaView _avaloniaView;
private readonly TouchDevice _touchDevice;
private string _currentCursor = CssCursor.Default;
public RazorViewTopLevelImpl(ITextInputMethodImpl textInputMethod)
public RazorViewTopLevelImpl(AvaloniaView avaloniaView)
{
_textInputMethod = textInputMethod;
_avaloniaView = avaloniaView;
TransparencyLevel = WindowTransparencyLevel.None;
AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1);
_touchDevice = new TouchDevice();
@ -175,6 +175,8 @@ namespace Avalonia.Web.Blazor
public WindowTransparencyLevel TransparencyLevel { get; }
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; }
public ITextInputMethodImpl TextInputMethod => _textInputMethod;
public ITextInputMethodImpl TextInputMethod => _avaloniaView;
public INativeControlHostImpl? NativeControlHost => _avaloniaView.GetNativeControlHostImpl();
}
}

Loading…
Cancel
Save