11 changed files with 318 additions and 10 deletions
@ -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); |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
@ -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)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,19 +1,20 @@ |
|||||
import { RuntimeAPI } from "../types/dotnet"; |
import { RuntimeAPI } from "../types/dotnet"; |
||||
import { SizeWatcher, DpiWatcher, Canvas } from "./avalonia/canvas"; |
import { SizeWatcher, DpiWatcher, Canvas } from "./avalonia/canvas"; |
||||
|
|
||||
import { InputHelper } from "./avalonia/input"; |
import { InputHelper } from "./avalonia/input"; |
||||
import { AvaloniaDOM } from "./avalonia/dom"; |
import { AvaloniaDOM } from "./avalonia/dom"; |
||||
import { Caniuse } from "./avalonia/caniuse"; |
import { Caniuse } from "./avalonia/caniuse"; |
||||
import { StreamHelper } from "./avalonia/stream"; |
import { StreamHelper } from "./avalonia/stream"; |
||||
|
import { NativeControlHost } from "./avalonia/NativeControlHost"; |
||||
|
|
||||
export async function createAvaloniaRuntime(api: RuntimeAPI): Promise<void> { |
export async function createAvaloniaRuntime(api: RuntimeAPI): Promise<void> { |
||||
api.setModuleImports("avalonia.ts", { |
api.setModuleImports("avalonia.ts", { |
||||
|
Caniuse, |
||||
Canvas, |
Canvas, |
||||
InputHelper, |
InputHelper, |
||||
SizeWatcher, |
SizeWatcher, |
||||
DpiWatcher, |
DpiWatcher, |
||||
AvaloniaDOM, |
AvaloniaDOM, |
||||
Caniuse, |
StreamHelper, |
||||
StreamHelper |
NativeControlHost |
||||
}); |
}); |
||||
} |
} |
||||
|
|||||
@ -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…
Reference in new issue