Browse Source

Add Browser implementation

Max Katz 3 years ago
parent
commit
855dade9d5

+ 4 - 0
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 - 0
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);
+        }
+    }
+}

+ 0 - 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;
-    }
-}

+ 5 - 39
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;
-}

+ 10 - 1
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;
+}

+ 38 - 6
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>

+ 30 - 13
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 - 0
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 - 0
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 - 0
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();
+            }
+        }
+    }
+}

+ 7 - 5
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();
     }
 }