Browse Source

Add native control host support

pull/9028/head
Max Katz 3 years ago
parent
commit
1c58c4dba1
  1. 1
      src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj
  2. 44
      src/Web/Avalonia.Web.Sample/EmbedSample.Browser.cs
  3. 7
      src/Web/Avalonia.Web.Sample/Program.cs
  4. 11
      src/Web/Avalonia.Web.Sample/embed.js
  5. 3
      src/Web/Avalonia.Web/AvaloniaView.cs
  6. 136
      src/Web/Avalonia.Web/BrowserNativeControlHost.cs
  7. 6
      src/Web/Avalonia.Web/BrowserTopLevelImpl.cs
  8. 28
      src/Web/Avalonia.Web/Interop/NativeControlHostHelper.cs
  9. 30
      src/Web/Avalonia.Web/JSObjectControlHandle.cs
  10. 7
      src/Web/Avalonia.Web/webapp/modules/avalonia.ts
  11. 55
      src/Web/Avalonia.Web/webapp/modules/avalonia/nativeControlHost.ts

1
src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj

@ -30,6 +30,7 @@
<ItemGroup>
<WasmExtraFilesToDeploy Include="index.html" />
<WasmExtraFilesToDeploy Include="main.js" />
<WasmExtraFilesToDeploy Include="embed.js" />
<WasmExtraFilesToDeploy Include="favicon.ico" />
<WasmExtraFilesToDeploy Include="Logo.svg" />
<WasmExtraFilesToDeploy Include="app.css" />

44
src/Web/Avalonia.Web.Sample/EmbedSample.Browser.cs

@ -0,0 +1,44 @@
using System;
using System.Runtime.InteropServices.JavaScript;
using Avalonia;
using Avalonia.Platform;
using Avalonia.Web;
using ControlCatalog.Pages;
namespace ControlCatalog.Web;
public class EmbedSampleWeb : INativeDemoControl
{
public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func<IPlatformHandle> createDefault)
{
if (isSecond)
{
var iframe = EmbedInterop.CreateElement("iframe");
iframe.SetProperty("src", "https://www.youtube.com/embed/kZCIporjJ70");
return new JSObjectControlHandle(iframe);
}
else
{
var defaultHandle = (JSObjectControlHandle)createDefault();
_ = JSHost.ImportAsync("embed.js", "./embed.js").ContinueWith(_ =>
{
EmbedInterop.AddAppButton(defaultHandle.Object);
});
return defaultHandle;
}
}
}
internal static partial class EmbedInterop
{
[JSImport("globalThis.document.createElement")]
public static partial JSObject CreateElement(string tagName);
[JSImport("addAppButton", "embed.js")]
public static partial void AddAppButton(JSObject parentObject);
}

7
src/Web/Avalonia.Web.Sample/Program.cs

@ -1,12 +1,17 @@
using Avalonia;
using Avalonia.Web;
using ControlCatalog;
using ControlCatalog.Web;
internal partial class Program
{
private static void Main(string[] args)
{
BuildAvaloniaApp().SetupBrowserApp("out");
BuildAvaloniaApp()
.AfterSetup(_ =>
{
ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb();
}).SetupBrowserApp("out");
}
public static AppBuilder BuildAvaloniaApp()

11
src/Web/Avalonia.Web.Sample/embed.js

@ -0,0 +1,11 @@
export function addAppButton(parent) {
var button = globalThis.document.createElement('button');
button.innerText = 'Hello world';
var clickCount = 0;
button.onclick = () => {
clickCount++;
button.innerText = 'Click count ' + clickCount;
};
parent.appendChild(button);
return button;
}

3
src/Web/Avalonia.Web/AvaloniaView.cs

@ -333,8 +333,7 @@ namespace Avalonia.Web
internal INativeControlHostImpl GetNativeControlHostImpl()
{
throw new NotImplementedException();
//return _nativeControlHost ?? throw new InvalidOperationException("Blazor View wasn't initialized yet");
return new BrowserNativeControlHost(_nativeControlsContainer);
}
private void ForceBlit()

136
src/Web/Avalonia.Web/BrowserNativeControlHost.cs

@ -0,0 +1,136 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices.JavaScript;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
using Avalonia.Web.Interop;
namespace Avalonia.Web
{
internal class BrowserNativeControlHost : INativeControlHostImpl
{
private readonly JSObject _hostElement;
public BrowserNativeControlHost(JSObject element)
{
_hostElement = element;
}
public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent)
{
var element = NativeControlHostHelper.CreateDefaultChild(null);
return new JSObjectControlHandle(element);
}
public INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func<IPlatformHandle, IPlatformHandle> create)
{
Attachment? a = null;
try
{
var child = create(new JSObjectControlHandle(_hostElement));
var attachmenetReference = NativeControlHostHelper.CreateAttachment();
// 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 = NativeControlHostHelper.CreateAttachment();
var a = new Attachment(attachmenetReference, handle);
a.AttachedTo = this;
return a;
}
public bool IsCompatibleWith(IPlatformHandle handle) => handle is JSObjectControlHandle;
private 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 JSObject? _native;
private BrowserNativeControlHost? _attachedTo;
public Attachment(JSObject native, IPlatformHandle handle)
{
_native = native;
NativeControlHostHelper.InitializeWithChildHandle(_native, ((JSObjectControlHandle)handle).Object);
}
public void Dispose()
{
if (_native != null)
{
NativeControlHostHelper.ReleaseChild(_native);
_native.Dispose();
_native = null;
}
}
public INativeControlHostImpl? AttachedTo
{
get => _attachedTo!;
set
{
CheckDisposed();
var host = (BrowserNativeControlHost?)value;
if (host == null)
{
NativeControlHostHelper.AttachTo(_native, null);
}
else
{
NativeControlHostHelper.AttachTo(_native, host._hostElement);
}
_attachedTo = host;
}
}
public bool IsCompatibleWith(INativeControlHostImpl host) => host is BrowserNativeControlHost;
public void HideWithSize(Size size)
{
CheckDisposed();
if (_attachedTo == null)
return;
NativeControlHostHelper.HideWithSize(_native, Math.Max(1, size.Width), Math.Max(1, 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));
NativeControlHostHelper.ShowInBounds(_native, bounds.X, bounds.Y, bounds.Width, bounds.Height);
}
[MemberNotNull(nameof(_native))]
private void CheckDisposed()
{
if (_native == null)
throw new ObjectDisposedException(nameof(Attachment));
}
}
}
}

6
src/Web/Avalonia.Web/BrowserTopLevelImpl.cs

@ -35,13 +35,11 @@ namespace Avalonia.Web
AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1);
_touchDevice = new TouchDevice();
_penDevice = new PenDevice();
NativeControlHost = _avaloniaView.GetNativeControlHostImpl();
}
public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds;
public void SetClientSize(Size newSize, double dpi)
{
if (Math.Abs(RenderScaling - dpi) > 0.0001)
@ -222,7 +220,7 @@ namespace Avalonia.Web
public ITextInputMethodImpl TextInputMethod => _avaloniaView;
public INativeControlHostImpl? NativeControlHost => _avaloniaView.GetNativeControlHostImpl();
public INativeControlHostImpl? NativeControlHost { get; }
public IStorageProvider StorageProvider { get; } = new BrowserStorageProvider();
}
}

28
src/Web/Avalonia.Web/Interop/NativeControlHostHelper.cs

@ -0,0 +1,28 @@
using System;
using System.Runtime.InteropServices.JavaScript;
namespace Avalonia.Web.Interop;
internal static partial class NativeControlHostHelper
{
[JSImport("NativeControlHost.createDefaultChild", "avalonia.ts")]
internal static partial JSObject CreateDefaultChild(JSObject? parent);
[JSImport("NativeControlHost.createAttachment", "avalonia.ts")]
internal static partial JSObject CreateAttachment();
[JSImport("NativeControlHost.initializeWithChildHandle", "avalonia.ts")]
internal static partial void InitializeWithChildHandle(JSObject element, JSObject child);
[JSImport("NativeControlHost.attachTo", "avalonia.ts")]
internal static partial void AttachTo(JSObject element, JSObject? host);
[JSImport("NativeControlHost.showInBounds", "avalonia.ts")]
internal static partial void ShowInBounds(JSObject element, double x, double y, double width, double height);
[JSImport("NativeControlHost.hideWithSize", "avalonia.ts")]
internal static partial void HideWithSize(JSObject element, double width, double height);
[JSImport("NativeControlHost.releaseChild", "avalonia.ts")]
internal static partial void ReleaseChild(JSObject element);
}

30
src/Web/Avalonia.Web/JSObjectControlHandle.cs

@ -0,0 +1,30 @@
using System;
using System.Runtime.InteropServices.JavaScript;
using Avalonia.Controls.Platform;
namespace Avalonia.Web;
public class JSObjectControlHandle : INativeControlHostDestroyableControlHandle
{
internal const string ElementReferenceDescriptor = "JSObject";
public JSObjectControlHandle(JSObject reference)
{
Object = reference;
}
public JSObject Object { get; }
public IntPtr Handle => throw new NotSupportedException();
public string? HandleDescriptor => ElementReferenceDescriptor;
public void Destroy()
{
if (Object is JSObject inProcess && !inProcess.IsDisposed)
{
inProcess.Dispose();
}
}
}

7
src/Web/Avalonia.Web/webapp/modules/avalonia.ts

@ -1,19 +1,20 @@
import { RuntimeAPI } from "../types/dotnet";
import { SizeWatcher, DpiWatcher, Canvas } from "./avalonia/canvas";
import { InputHelper } from "./avalonia/input";
import { AvaloniaDOM } from "./avalonia/dom";
import { Caniuse } from "./avalonia/caniuse";
import { StreamHelper } from "./avalonia/stream";
import { NativeControlHost } from "./avalonia/NativeControlHost";
export async function createAvaloniaRuntime(api: RuntimeAPI): Promise<void> {
api.setModuleImports("avalonia.ts", {
Caniuse,
Canvas,
InputHelper,
SizeWatcher,
DpiWatcher,
AvaloniaDOM,
Caniuse,
StreamHelper
StreamHelper,
NativeControlHost
});
}

55
src/Web/Avalonia.Web/webapp/modules/avalonia/nativeControlHost.ts

@ -0,0 +1,55 @@
class NativeControlHostTopLevelAttachment {
_child?: HTMLElement;
_host?: HTMLElement;
}
export class NativeControlHost {
public static createDefaultChild(parent?: HTMLElement): HTMLElement {
return document.createElement("div");
}
public static createAttachment(): NativeControlHostTopLevelAttachment {
return new NativeControlHostTopLevelAttachment();
}
public static initializeWithChildHandle(element: NativeControlHostTopLevelAttachment, child: HTMLElement): void {
element._child = child;
element._child.style.position = "absolute";
}
public static attachTo(element: NativeControlHostTopLevelAttachment, host?: HTMLElement): void {
if (element._host && element._child) {
element._host.removeChild(element._child);
}
element._host = host;
if (element._host && element._child) {
element._host.appendChild(element._child);
}
}
public static showInBounds(element: NativeControlHostTopLevelAttachment, x: number, y: number, width: number, height: number): void {
if (element._child) {
element._child.style.top = `${y}px`;
element._child.style.left = `${x}px`;
element._child.style.width = `${width}px`;
element._child.style.height = `${height}px`;
element._child.style.display = "block";
}
}
public static hideWithSize(element: NativeControlHostTopLevelAttachment, width: number, height: number): void {
if (element._child) {
element._child.style.width = `${width}px`;
element._child.style.height = `${height}px`;
element._child.style.display = "none";
}
}
public static releaseChild(element: NativeControlHostTopLevelAttachment): void {
if (element._child) {
element._child = undefined;
}
}
}
Loading…
Cancel
Save