From a359d96a4d2b9f13d6758140d450df912426df7c Mon Sep 17 00:00:00 2001 From: Jerome Laban Date: Fri, 11 Apr 2025 23:15:02 -0400 Subject: [PATCH 1/5] fix: Adjust native embedding support --- .../BrowserMediaPlayerExtension.cs | 1 + .../BrowserMediaPlayerPresenterExtension.cs | 1 + .../BrowserNativeElementHostingExtension.cs | 15 +- .../WebView/BrowserWebViewProvider.cs | 1 + .../WebAssemblyBrowserHost.cs | 3 + .../BrowserNativeElementHostingExtension.ts | 125 +++++++++- .../Given_BrowserHtmlElement.cs | 231 ++++++++++++++++++ .../BrowserHtmlElement.cs | 171 +++++++++++-- .../BrowserHtmlElement.skia.cs | 222 +++++++++++++++++ .../BrowserHtmlElement.wasm.cs | 191 +++++++++++++++ .../ContentPresenter/ContentPresenter.cs | 12 +- .../ContentPresenter/ContentPresenter.wasm.cs | 41 ++++ src/Uno.UI/ts/WindowManager.ts | 82 +++++++ 13 files changed, 1056 insertions(+), 40 deletions(-) create mode 100644 src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_BrowserHtmlElement.cs create mode 100644 src/Uno.UI/NativeElementHosting/BrowserHtmlElement.skia.cs create mode 100644 src/Uno.UI/NativeElementHosting/BrowserHtmlElement.wasm.cs create mode 100644 src/Uno.UI/UI/Xaml/Controls/ContentPresenter/ContentPresenter.wasm.cs diff --git a/src/Uno.UI.Runtime.Skia.WebAssembly.Browser/BrowserMediaPlayerExtension.cs b/src/Uno.UI.Runtime.Skia.WebAssembly.Browser/BrowserMediaPlayerExtension.cs index ff1c19ad842b..c92dd64aaf49 100644 --- a/src/Uno.UI.Runtime.Skia.WebAssembly.Browser/BrowserMediaPlayerExtension.cs +++ b/src/Uno.UI.Runtime.Skia.WebAssembly.Browser/BrowserMediaPlayerExtension.cs @@ -16,6 +16,7 @@ using Uno.Foundation.Logging; using Uno.Helpers; using Uno.Media.Playback; +using Uno.UI.NativeElementHosting; namespace Uno.UI.Runtime.Skia; diff --git a/src/Uno.UI.Runtime.Skia.WebAssembly.Browser/BrowserMediaPlayerPresenterExtension.cs b/src/Uno.UI.Runtime.Skia.WebAssembly.Browser/BrowserMediaPlayerPresenterExtension.cs index 66bed9883ac7..548a2c84374f 100644 --- a/src/Uno.UI.Runtime.Skia.WebAssembly.Browser/BrowserMediaPlayerPresenterExtension.cs +++ b/src/Uno.UI.Runtime.Skia.WebAssembly.Browser/BrowserMediaPlayerPresenterExtension.cs @@ -6,6 +6,7 @@ using Windows.Foundation; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using Uno.UI.NativeElementHosting; namespace Uno.UI.Runtime.Skia; diff --git a/src/Uno.UI.Runtime.Skia.WebAssembly.Browser/BrowserNativeElementHostingExtension.cs b/src/Uno.UI.Runtime.Skia.WebAssembly.Browser/BrowserNativeElementHostingExtension.cs index c9aa53e44b29..ee6d15c2198b 100644 --- a/src/Uno.UI.Runtime.Skia.WebAssembly.Browser/BrowserNativeElementHostingExtension.cs +++ b/src/Uno.UI.Runtime.Skia.WebAssembly.Browser/BrowserNativeElementHostingExtension.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; using System.Runtime.InteropServices.JavaScript; +using Uno.UI.NativeElementHosting; using ContentPresenter = Microsoft.UI.Xaml.Controls.ContentPresenter; namespace Uno.UI.Runtime.Skia; @@ -61,25 +62,25 @@ public object CreateSampleComponent(string text) private static partial class NativeMethods { - [JSImport($"globalThis.Uno.UI.Runtime.Skia.{nameof(BrowserNativeElementHostingExtension)}.isNativeElement")] + [JSImport($"globalThis.Uno.UI.NativeElementHosting.{nameof(BrowserHtmlElement)}.isNativeElement")] internal static partial bool IsNativeElement(string content); - [JSImport($"globalThis.Uno.UI.Runtime.Skia.{nameof(BrowserNativeElementHostingExtension)}.attachNativeElement")] + [JSImport($"globalThis.Uno.UI.NativeElementHosting.{nameof(BrowserHtmlElement)}.attachNativeElement")] internal static partial bool AttachNativeElement(string content); - [JSImport($"globalThis.Uno.UI.Runtime.Skia.{nameof(BrowserNativeElementHostingExtension)}.detachNativeElement")] + [JSImport($"globalThis.Uno.UI.NativeElementHosting.{nameof(BrowserHtmlElement)}.detachNativeElement")] internal static partial bool DetachNativeElement(string content); - [JSImport($"globalThis.Uno.UI.Runtime.Skia.{nameof(BrowserNativeElementHostingExtension)}.arrangeNativeElement")] + [JSImport($"globalThis.Uno.UI.NativeElementHosting.{nameof(BrowserHtmlElement)}.arrangeNativeElement")] internal static partial bool ArrangeNativeElement(string content, double x, double y, double width, double height); - [JSImport($"globalThis.Uno.UI.Runtime.Skia.{nameof(BrowserNativeElementHostingExtension)}.createSampleComponent")] + [JSImport($"globalThis.Uno.UI.NativeElementHosting.{nameof(BrowserHtmlElement)}.createSampleComponent")] internal static partial void CreateSampleComponent(string parentId, string text); - [JSImport($"globalThis.Uno.UI.Runtime.Skia.{nameof(BrowserNativeElementHostingExtension)}.changeNativeElementOpacity")] + [JSImport($"globalThis.Uno.UI.NativeElementHosting.{nameof(BrowserHtmlElement)}.changeNativeElementOpacity")] internal static partial string ChangeNativeElementOpacity(string content, double opacity); - [JSImport($"globalThis.Uno.UI.Runtime.Skia.{nameof(BrowserNativeElementHostingExtension)}.setSvgClipPathForNativeElementHost")] + [JSImport($"globalThis.Uno.UI.NativeElementHosting.{nameof(BrowserHtmlElement)}.setSvgClipPathForNativeElementHost")] internal static partial string SetSvgClipPathForNativeElementHost(string path); } } diff --git a/src/Uno.UI.Runtime.Skia.WebAssembly.Browser/UI/Xaml/Controls/WebView/BrowserWebViewProvider.cs b/src/Uno.UI.Runtime.Skia.WebAssembly.Browser/UI/Xaml/Controls/WebView/BrowserWebViewProvider.cs index 83553ba9cd9e..e12041eabe0b 100644 --- a/src/Uno.UI.Runtime.Skia.WebAssembly.Browser/UI/Xaml/Controls/WebView/BrowserWebViewProvider.cs +++ b/src/Uno.UI.Runtime.Skia.WebAssembly.Browser/UI/Xaml/Controls/WebView/BrowserWebViewProvider.cs @@ -1,5 +1,6 @@ using Microsoft.UI.Xaml.Controls; using Microsoft.Web.WebView2.Core; +using Uno.UI.NativeElementHosting; using Uno.UI.Runtime.Skia; using Uno.UI.Xaml.Controls; diff --git a/src/Uno.UI.Runtime.Skia.WebAssembly.Browser/WebAssemblyBrowserHost.cs b/src/Uno.UI.Runtime.Skia.WebAssembly.Browser/WebAssemblyBrowserHost.cs index 4112c98c1e30..0e28c263c030 100644 --- a/src/Uno.UI.Runtime.Skia.WebAssembly.Browser/WebAssemblyBrowserHost.cs +++ b/src/Uno.UI.Runtime.Skia.WebAssembly.Browser/WebAssemblyBrowserHost.cs @@ -14,6 +14,7 @@ using Uno.UI.Xaml.Controls; using Uno.UI.Xaml.Controls.Extensions; using Microsoft.Web.WebView2.Core; +using Uno.UI.NativeElementHosting; namespace Uno.UI.Runtime.Skia.WebAssembly.Browser; @@ -67,6 +68,8 @@ private void Initialize() void CreateApp(ApplicationInitializationCallbackParams _) { + BrowserHtmlElement.Initialize(); + var app = _appBuilder(); app.Host = this; diff --git a/src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/BrowserNativeElementHostingExtension.ts b/src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/BrowserNativeElementHostingExtension.ts index bed15a440f90..bc7d162df882 100644 --- a/src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/BrowserNativeElementHostingExtension.ts +++ b/src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/BrowserNativeElementHostingExtension.ts @@ -1,7 +1,24 @@ -namespace Uno.UI.Runtime.Skia { - export class BrowserNativeElementHostingExtension { +namespace Uno.UI.NativeElementHosting { + export class BrowserHtmlElement { private static clipPath: SVGPathElement; + /** Native elements created with the BrowserHtmlElement class */ + private static nativeHandlersMap: { [id: string]: any } = {}; + + private static dispatchEventNativeElementMethod: any; + + public static async initialize() { + let anyModule = window.Module; + + if (anyModule.getAssemblyExports !== undefined) { + const browserExports = await anyModule.getAssemblyExports("Uno.UI"); + + BrowserHtmlElement.dispatchEventNativeElementMethod = browserExports.Uno.UI.NativeElementHosting.BrowserHtmlElement.DispatchEventNativeElementMethod; + } else { + throw `BrowserHtmlElement: Unable to find dotnet exports`; + } + } + public static setSvgClipPathForNativeElementHost(path: string) { if (!document.getElementById("unoNativeElementHostClipPath")) { const svgContainer = document.createElementNS("http://www.w3.org/2000/svg", "svg"); @@ -118,5 +135,109 @@ namespace Uno.UI.Runtime.Skia { element.appendChild(btn); element.addEventListener("pointerdown", _ => alert(`button ${text} clicked`)); } + + public static setStyleString(elementId: string, name: string, value: string) { + const element = document.getElementById(elementId); + + element.style.setProperty(name, value); + } + + public static resetStyle(elementId: string, names: string[]) { + const element = document.getElementById(elementId); + + for (const name of names) { + element.style.setProperty(name, ""); + } + } + + public static setClasses(elementId: string, cssClassesList: string[], classIndex: number) { + const element = document.getElementById(elementId); + + for (let i = 0; i < cssClassesList.length; i++) { + if (i === classIndex) { + element.classList.add(cssClassesList[i]); + } else { + element.classList.remove(cssClassesList[i]); + } + } + } + + public static setUnsetCssClasses(elementId: string, classesToUnset: string[]) { + const element = document.getElementById(elementId); + + classesToUnset.forEach(c => { + element.classList.remove(c); + }); + } + + public static setAttribute(elementId: string, name: string, value: string) { + const element = document.getElementById(elementId); + + element.setAttribute(name, value); + } + + public static getAttribute(elementId: string, name: string) { + const element = document.getElementById(elementId); + + return element.getAttribute(name); + } + + public static removeAttribute(elementId: string, name: string) { + const element = document.getElementById(elementId); + + element.removeAttribute(name); + } + + public static setContentHtml(elementId: string, html: string) { + const element = document.getElementById(elementId); + + element.innerHTML = html; + } + + public static registerNativeHtmlEvent(owner: any, elementId: string, eventName: string, managedHandler: string) { + const element = document.getElementById(elementId); + + if (!BrowserHtmlElement.dispatchEventNativeElementMethod) { + throw `BrowserHtmlElement: The initialize method has not been called`; + } + + const eventHandler = (event: Event) => { + BrowserHtmlElement.dispatchEventNativeElementMethod(owner, eventName, managedHandler, event); + }; + + // Register the handler using a string representation of the managed handler + // the managed representation assumes that the string contains a unique id. + BrowserHtmlElement.nativeHandlersMap["" + managedHandler] = eventHandler; + + element.addEventListener(eventName, eventHandler); + } + + public static unregisterNativeHtmlEvent(elementId: string, eventName: string, managedHandler: any) { + const element = document.getElementById(elementId); + + if (!BrowserHtmlElement.dispatchEventNativeElementMethod) { + throw `BrowserHtmlElement: The initialize method has not been called`; + } + + const key = "" + managedHandler; + const eventHandler = BrowserHtmlElement.nativeHandlersMap[key]; + if (eventHandler) { + element.removeEventListener(eventName, eventHandler); + delete BrowserHtmlElement.nativeHandlersMap[key]; + } + } + + public static invokeJS(command: string): string { + return String(eval(command) || ""); + } + + public static async invokeAsync(command: string): Promise { + // Preseve the original emscripten marshalling semantics + // to always return a valid string. + var result = await eval(command); + + return String(result || ""); + + } } } diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_BrowserHtmlElement.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_BrowserHtmlElement.cs new file mode 100644 index 000000000000..c36f176ea32e --- /dev/null +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_BrowserHtmlElement.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices.JavaScript; +using System.Text; +using System.Threading.Tasks; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Uno.UI.NativeElementHosting; +using Uno.UI.RemoteControl.Messaging.IdeChannel; +using Windows.UI; +using static Private.Infrastructure.TestServices; + +namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml; + +#if (__SKIA__ || __WASM__) && HAS_UNO +[TestClass] +public class Given_BrowserHtmlElement +{ +#if __WASM__ + [TestMethod] + public async Task Given_HasParent() + { + var owner = new ContentControl() { Width = 100, Height = 100 }; + var root = new Border() + { + Child = owner, + Width = 100, + Height = 100, + Background = new SolidColorBrush(Colors.Red), + Padding = new Microsoft.UI.Xaml.Thickness(10) + }; + var SUT = BrowserHtmlElement.CreateHtmlElement("div"); + owner.Content = SUT; + + WindowHelper.WindowContent = root; + + await WindowHelper.WaitForLoaded(owner); + + Assert.AreEqual(owner.TemplatedRoot.GetHtmlId(), SUT.ExecuteJavascript($"return element.parentElement.id")); + } +#endif + + [TestMethod] + public async Task Given_SetAttribute() + { + if (!OperatingSystem.IsBrowser()) + { + Assert.Inconclusive("This test is only supported on the browser."); + } + + var owner = new ContentControl() { Width = 100, Height = 100 }; + var root = new Border() + { + Child = owner, + Width = 100, + Height = 100, + Background = new SolidColorBrush(Colors.Red), + Padding = new Microsoft.UI.Xaml.Thickness(10) + }; + var SUT = BrowserHtmlElement.CreateHtmlElement("div"); + owner.Content = SUT; + + WindowHelper.WindowContent = root; + + SUT.SetHtmlAttribute("myProperty", "myValue"); + Assert.AreEqual("myValue", SUT.GetHtmlAttribute("myProperty")); + + await WindowHelper.WaitForLoaded(owner); + + Assert.AreEqual("myValue", SUT.ExecuteJavascript($"return element.getAttribute(\"myproperty\")")); + + SUT.ClearHtmlAttribute("myproperty"); + + await Task.Delay(100); + + Assert.AreEqual("", SUT.ExecuteJavascript($"return element.getAttribute(\"myproperty\")")); + } + + [TestMethod] + public async Task Given_SetHtmlContent() + { + if (!OperatingSystem.IsBrowser()) + { + Assert.Inconclusive("This test is only supported on the browser."); + } + + var owner = new ContentControl() { Width = 100, Height = 100 }; + var root = new Border() + { + Child = owner, + Width = 100, + Height = 100, + Background = new SolidColorBrush(Colors.Red), + Padding = new Microsoft.UI.Xaml.Thickness(10) + }; + var SUT = BrowserHtmlElement.CreateHtmlElement("div"); + owner.Content = SUT; + + WindowHelper.WindowContent = root; + + SUT.SetHtmlContent("My Inner Content"); + + await WindowHelper.WaitForLoaded(owner); + + Assert.AreEqual("My Inner Content", SUT.ExecuteJavascript($"return element.innerHTML")); + } + + [TestMethod] + public async Task Given_SetStyle() + { + if (!OperatingSystem.IsBrowser()) + { + Assert.Inconclusive("This test is only supported on the browser."); + } + + var owner = new ContentControl() { Width = 100, Height = 100 }; + var root = new Border() + { + Child = owner, + Width = 100, + Height = 100, + Background = new SolidColorBrush(Colors.Red), + Padding = new Microsoft.UI.Xaml.Thickness(10) + }; + var SUT = BrowserHtmlElement.CreateHtmlElement("div"); + owner.Content = SUT; + + WindowHelper.WindowContent = root; + + SUT.SetCssStyle("pointer-events", "all"); + + await WindowHelper.WaitForLoaded(owner); + + Assert.AreEqual("all", SUT.ExecuteJavascript($"return element.style.pointerEvents")); + + SUT.ClearCssStyle("pointer-events"); + + Assert.AreEqual("", SUT.ExecuteJavascript($"return element.style.pointerEvents")); + } + + [TestMethod] + public async Task Given_SetStyles() + { + if (!OperatingSystem.IsBrowser()) + { + Assert.Inconclusive("This test is only supported on the browser."); + } + + var owner = new ContentControl() { Width = 100, Height = 100 }; + var root = new Border() + { + Child = owner, + Width = 100, + Height = 100, + Background = new SolidColorBrush(Colors.Red), + Padding = new Microsoft.UI.Xaml.Thickness(10) + }; + var SUT = BrowserHtmlElement.CreateHtmlElement("div"); + owner.Content = SUT; + + WindowHelper.WindowContent = root; + + SUT.SetCssStyle( + ("pointer-events", "all"), + ("border-style", "none") + ); + + await WindowHelper.WaitForLoaded(owner); + + Assert.AreEqual("all", SUT.ExecuteJavascript($"return element.style.pointerEvents")); + Assert.AreEqual("none", SUT.ExecuteJavascript($"return element.style.borderStyle")); + } + + [TestMethod] + public async Task Given_HtmlEvent() + { + if (!OperatingSystem.IsBrowser()) + { + Assert.Inconclusive("This test is only supported on the browser."); + } + + var owner = new ContentControl() { Width = 100, Height = 100 }; + var root = new Border() + { + Child = owner, + Width = 100, + Height = 100, + Background = new SolidColorBrush(Colors.Red), + Padding = new Microsoft.UI.Xaml.Thickness(10) + }; + + var SUT = BrowserHtmlElement.CreateHtmlElement("div"); + owner.Content = SUT; + + WindowHelper.WindowContent = root; + + JSObject result = null; + + void MyHandler(object s, JSObject e) + { + result = e; + } + + SUT.RegisterHtmlEventHandler("simpleEvent", MyHandler); + + await WindowHelper.WaitForLoaded(owner); + + SUT.ExecuteJavascript($"element.dispatchEvent(new CustomEvent(\"simpleEvent\", {{ detail: 42 }}));"); + + await Task.Delay(100); + + Assert.IsNotNull(result); + Assert.AreEqual(42, result.GetPropertyAsInt32("detail")); + + result = null; + + // Validate unregistration to the event + SUT.UnregisterHtmlEventHandler("simpleEvent", MyHandler); + + SUT.ExecuteJavascript($"element.dispatchEvent(new CustomEvent(\"simpleEvent\", {{ detail: 42 }}));"); + + await Task.Delay(100); + + Assert.IsNull(result); + } +} + +#endif diff --git a/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.cs b/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.cs index cbf2119a917f..539a116b43df 100644 --- a/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.cs +++ b/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.cs @@ -2,52 +2,58 @@ #nullable enable using System; +using System.Globalization; +using System.Reflection; using System.Runtime.InteropServices; using System.Runtime.InteropServices.JavaScript; -namespace Uno.UI.Runtime.Skia; +using System.Threading.Tasks; +using System.Xml.Linq; +using Uno.Extensions; +using Uno.Foundation; +using Uno.UI.Xaml; + +namespace Uno.UI.NativeElementHosting; /// /// A managed handle to a DOM HTMLElement. /// public sealed partial class BrowserHtmlElement : IDisposable { -#if __SKIA__ private GCHandle? _gcHandle; -#endif + + internal nint UnoElementId { get; } /// /// The native HTMLElement id. /// public string ElementId { get; } - private BrowserHtmlElement() + internal bool IsOwner { get; } + + private BrowserHtmlElement(bool isOwner) { -#if __SKIA__ if (!OperatingSystem.IsBrowser()) -#endif { - throw new NotSupportedException($"{nameof(BrowserHtmlElement)} is only supported on the Wasm target with the skia backend."); + throw new NotSupportedException($"{nameof(BrowserHtmlElement)} is only supported on WebAssembly."); } -#if __SKIA__ _gcHandle = GCHandle.Alloc(this, GCHandleType.Weak); var handle = GCHandle.ToIntPtr(_gcHandle.Value); + UnoElementId = handle; ElementId = "uno-" + handle; -#endif + IsOwner = isOwner; } private BrowserHtmlElement(string elementId) + : this(false) { -#if __SKIA__ if (!OperatingSystem.IsBrowser()) -#endif { throw new NotSupportedException($"{nameof(BrowserHtmlElement)} is only supported on the Wasm target with the skia backend."); } -#if __SKIA__ ElementId = elementId; -#endif + RegisterExistingElementNative(elementId); } /// @@ -60,8 +66,9 @@ private BrowserHtmlElement(string elementId) /// The HTML tag name of the created element. public static BrowserHtmlElement CreateHtmlElement(string elementId, string tagName) { - CreateHtmlElementAndAddToStore(elementId, tagName); - return new BrowserHtmlElement(elementId); + var element = new BrowserHtmlElement(elementId); + CreateHtmlElementNative(element.ElementId, element.UnoElementId, tagName); + return element; } /// @@ -71,8 +78,8 @@ public static BrowserHtmlElement CreateHtmlElement(string elementId, string tagN /// Element instance. public static BrowserHtmlElement CreateHtmlElement(string tagName) { - var element = new BrowserHtmlElement(); - CreateHtmlElementAndAddToStore(element.ElementId, tagName); + var element = new BrowserHtmlElement(isOwner: true); + CreateHtmlElementNative(element.ElementId, element.UnoElementId, tagName); return element; } @@ -85,19 +92,133 @@ public static BrowserHtmlElement CreateHtmlElement(string tagName) /// The id of the element. The DOM must contain an element with this id. public static BrowserHtmlElement OwnHtmlElement(string elementId) { - AddToStore(elementId); return new BrowserHtmlElement(elementId); } - [JSImport($"globalThis.Uno.UI.Runtime.Skia.BrowserNativeElementHostingExtension.createHtmlElementAndAddToStore")] - private static partial void CreateHtmlElementAndAddToStore(string id, string tagName); + /// + /// Get the Id of the corresponding element in the HTML DOM + /// + /// Compatibility method with previous version od .NET + public string GetHtmlId() + => ElementId.ToString(CultureInfo.InvariantCulture); + + /// + /// Set one CSS style on a HTML element. + /// + /// + /// The style is using the CSS syntax format, not the DOM syntax. + /// Ex: for font size, use "font-size", not "fontSize". + /// + public void SetCssStyle(string name, string value) + => SetCssStyleNative(name, value); + + /// + /// Set one or many CSS styles on a HTML element. + /// + /// + /// The style is using the CSS syntax format, not the DOM syntax. + /// Ex: for font size, use "font-size", not "fontSize". + /// + public void SetCssStyle(params (string name, string value)[] styles) + => SetCssStyleNative(styles); + + /// + /// Clear one or many CSS styles from a HTML element. + /// + public void ClearCssStyle(params string[] names) + => ClearCssStyleNative(names); + + /// + /// Set one of the predefined class + /// + /// + /// Useful to switch a control from different modes when each mode is bound + /// to a specific class. + /// + public void SetCssClass(string[] classes, int index) + => SetCssClassNative(classes, index); + + /// + /// Add one or many CSS classes to a HTML element, if not present. + /// + public void SetCssClass(params string[] classesToSet) + => SetCssClassNative(classesToSet); + + /// + /// Remove one or many CSS classes from a HTML element, if defined. + /// + public void UnsetCssClass(params string[] classesToUnset) + => UnsetCssClassNative(classesToUnset); + + /// + /// Set a HTML attribute to an element. + /// + public void SetHtmlAttribute(string name, string value) + => SetHtmlAttributeNative(name, value); - [JSImport($"globalThis.Uno.UI.Runtime.Skia.BrowserNativeElementHostingExtension.addToStore")] - private static partial void AddToStore(string id); + /// + /// Set multiple HTML attributes to an element at the same time. + /// + public void SetHtmlAttribute(params (string name, string value)[] attributes) + => SetHtmlAttributeNative(attributes); - [JSImport($"globalThis.Uno.UI.Runtime.Skia.BrowserNativeElementHostingExtension.disposeHtmlElement")] - private static partial bool DisposeHtmlElement(string id); + /// + /// Get the HTML attribute value of an element + /// + public string GetHtmlAttribute(string name) + => GetHtmlAttributeNative(name); + + /// + /// Clear/remove a HTML attribute from an element. + /// + public void ClearHtmlAttribute(string name) + => ClearHtmlAttributeNative(name); + + /// + /// Clear/remove a HTML attribute from an element. + /// + public void RemoveAttribute(string name) + => RemoveAttributeNative(name); - public void Dispose() => DisposeHtmlElement(ElementId); + /// + /// Run javascript in the context of a DOM element. + /// This one is available in the scope as "element". + /// + /// + /// Will work even if the element is not yet loaded into the DOM. + /// + public string ExecuteJavascript(string jsCode) + => ExecuteJavascriptNative(jsCode); + + /// + /// Asynchronously run javascript on a DOM element. + /// This one is available in the scope as "element". + /// The called code is expected to return something awaitable (a Promise). + /// + /// + /// Will work even if the element is not yet loaded into the DOM. + /// + public Task ExecuteJavascriptAsync(string asyncJsCode) + => ExecuteJavascriptNativeAsync(asyncJsCode); + + /// + /// Set raw HTML Content for this element. + /// Don't use this when there's child elements managed by Uno or you'll + /// get expected results. + /// + public void SetHtmlContent(string html) + => SetHtmlContentNative(html); + + /// + /// Will invoke `addEventListener()` on the corresponding HTML element. + /// + public void RegisterHtmlEventHandler(string eventName, EventHandler handler) + => RegisterHtmlEventHandlerNative(eventName, handler); + + /// + /// Unregister previously registered event with RegisterHtmlEventHandler. + /// + public void UnregisterHtmlEventHandler(string eventName, EventHandler handler) + => UnregisterHtmlEventHandlerNative(eventName, handler); } #endif diff --git a/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.skia.cs b/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.skia.cs new file mode 100644 index 000000000000..da7b27c407f8 --- /dev/null +++ b/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.skia.cs @@ -0,0 +1,222 @@ +#if UNO_REFERENCE_API +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; +using System.Xml.Linq; +using Uno.Extensions; + +namespace Uno.UI.NativeElementHosting; + +/// +/// A managed handle to a DOM HTMLElement. +/// +public sealed partial class BrowserHtmlElement : IDisposable +{ + // Handlers need to be wrapped in a class to be passed to JSInterop + private record class EventWrapper(EventHandler handler); + + private readonly Dictionary, EventWrapper> _eventMap = new(); + + private static void CreateHtmlElementNative(string id, nint unoElementId, string tagName) + { + NativeMethods.CreateHtmlElementAndAddToStore(id, tagName); + } + + private static void RegisterExistingElementNative(string elementId) + { + } + + private void SetCssStyleNative(string name, string value) + { + NativeMethods.SetStyleString(ElementId, name, value); + } + + private void SetCssStyleNative(params (string name, string value)[] styles) + { + foreach (var pair in styles) + { + NativeMethods.SetStyleString(ElementId, pair.name, pair.value); + } + } + + private void ClearCssStyleNative(params string[] names) + { + NativeMethods.ResetStyle(ElementId, names); + } + + private void SetCssClassNative(string[] classes, int index) + { + NativeMethods.SetClasses(ElementId, classes, index); + } + + private void SetCssClassNative(params string[] classesToSet) + { + NativeMethods.SetUnsetCssClasses(ElementId, classesToSet); + } + + private void UnsetCssClassNative(params string[] classesToUnset) + { + NativeMethods.SetUnsetCssClasses(ElementId, classesToUnset); + } + + private void SetHtmlAttributeNative(string name, string value) + { + NativeMethods.SetAttribute(ElementId, name, value); + } + + private void SetHtmlAttributeNative(params (string name, string value)[] attributes) + { + foreach (var pair in attributes) + { + NativeMethods.SetAttribute(ElementId, pair.name, pair.value); + } + } + + private string GetHtmlAttributeNative(string name) + { + return NativeMethods.GetAttribute(ElementId, name); + } + + private void ClearHtmlAttributeNative(string name) + { + NativeMethods.RemoveAttribute(ElementId, name); + } + + private void RemoveAttributeNative(string name) + { + NativeMethods.RemoveAttribute(ElementId, name); + } + + private string ExecuteJavascriptNative(string jsCode) + { + var js = $$""" + (function(element) { + {{jsCode}} + })(document.getElementById("{{ElementId}}")); + """; + return NativeMethods.InvokeJS(js); + } + + private Task ExecuteJavascriptNativeAsync(string asyncJsCode) + { + var js = $$""" + (function(element) { + const __f = () => {{asyncJsCode}}; + return __f(element); + })(document.getElementById("{{ElementId}}")); + """; + return NativeMethods.InvokeAsync(js); + } + + private void SetHtmlContentNative(string html) + { + NativeMethods.SetContentHtml(ElementId, html); + } + + [JSExport] + private static bool DispatchEventNativeElementMethod( + [JSMarshalAs] object owner, + string eventName, + [JSMarshalAs] object eventHandler, + JSObject payload) + { + if (eventHandler is EventWrapper handler) + { + handler.handler(owner, payload); + + return true; + } + else + { + return false; + } + } + + private void RegisterHtmlEventHandlerNative(string eventName, EventHandler handler) + { + var wrapper = new EventWrapper(handler); + _eventMap[handler] = wrapper; + + NativeMethods.RegisterNativeHtmlEvent(this, ElementId, eventName, wrapper); + } + + private void UnregisterHtmlEventHandlerNative(string eventName, EventHandler handler) + { + if (_eventMap.TryGetValue(handler, out var wrapper)) + { + _eventMap.Remove(handler); + NativeMethods.UnregisterNativeHtmlEvent(ElementId, eventName, wrapper); + } + } + + public void Dispose() + { + NativeMethods.DisposeHtmlElement(ElementId); + } + + internal static void Initialize() + => NativeMethods.Initialize(); + + private static partial class NativeMethods + { + [JSImport($"globalThis.Uno.UI.NativeElementHosting.BrowserHtmlElement.initialize")] + internal static partial void Initialize(); + + [JSImport($"globalThis.Uno.UI.NativeElementHosting.BrowserHtmlElement.createHtmlElementAndAddToStore")] + internal static partial void CreateHtmlElementAndAddToStore(string id, string tagName); + + [JSImport($"globalThis.Uno.UI.NativeElementHosting.BrowserHtmlElement.addToStore")] + internal static partial void AddToStore(string id); + + [JSImport($"globalThis.Uno.UI.NativeElementHosting.BrowserHtmlElement.disposeHtmlElement")] + internal static partial bool DisposeHtmlElement(string id); + + [JSImport($"globalThis.Uno.UI.NativeElementHosting.BrowserHtmlElement.setStyleString")] + internal static partial void SetStyleString(string elementId, string name, string value); + + [JSImport($"globalThis.Uno.UI.NativeElementHosting.BrowserHtmlElement.resetStyle")] + internal static partial void ResetStyle(string elementId, string[] names); + + [JSImport($"globalThis.Uno.UI.NativeElementHosting.BrowserHtmlElement.setClasses")] + internal static partial void SetClasses(string elementId, string[] classes, int index); + + [JSImport($"globalThis.Uno.UI.NativeElementHosting.BrowserHtmlElement.setUnsetCssClasses")] + internal static partial void SetUnsetCssClasses(string elementId, string[] classesToUnset); + + [JSImport($"globalThis.Uno.UI.NativeElementHosting.BrowserHtmlElement.setAttribute")] + internal static partial void SetAttribute(string elementId, string name, string value); + + [JSImport($"globalThis.Uno.UI.NativeElementHosting.BrowserHtmlElement.getAttribute")] + internal static partial string GetAttribute(string elementId, string name); + + [JSImport($"globalThis.Uno.UI.NativeElementHosting.BrowserHtmlElement.removeAttribute")] + internal static partial void RemoveAttribute(string elementId, string name); + + [JSImport($"globalThis.Uno.UI.NativeElementHosting.BrowserHtmlElement.setContentHtml")] + internal static partial void SetContentHtml(string elementId, string html); + + [JSImport($"globalThis.Uno.UI.NativeElementHosting.BrowserHtmlElement.registerNativeHtmlEvent")] + internal static partial void RegisterNativeHtmlEvent( + [JSMarshalAs] object browserHtmlElement, + string elementId, + string eventName, + [JSMarshalAs] object handler); + + [JSImport($"globalThis.Uno.UI.NativeElementHosting.BrowserHtmlElement.unregisterNativeHtmlEvent")] + internal static partial void UnregisterNativeHtmlEvent( + string elementId, + string eventName, + [JSMarshalAs] object handler); + + [JSImport($"globalThis.Uno.UI.NativeElementHosting.BrowserHtmlElement.invokeJS")] + internal static partial string InvokeJS(string js); + + [JSImport($"globalThis.Uno.UI.NativeElementHosting.BrowserHtmlElement.invokeAsync")] + internal static partial Task InvokeAsync(string js); + } +} +#endif diff --git a/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.wasm.cs b/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.wasm.cs new file mode 100644 index 000000000000..39bd6a8078df --- /dev/null +++ b/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.wasm.cs @@ -0,0 +1,191 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; +using System.Transactions; +using Uno.Extensions; +using Uno.Foundation; +using Uno.UI.Xaml; +using static Microsoft.UI.Xaml.Controls.CollectionChangedOperation; + +namespace Uno.UI.NativeElementHosting; + +partial class BrowserHtmlElement +{ + // Handlers need to be wrapped in a class to be passed to JSInterop + private record class EventWrapper(EventHandler handler); + + private readonly Dictionary, EventWrapper> _eventMap = new(); + + private static void CreateHtmlElementNative(string id, nint unoElementId, string tagName) + { + NativeMethods.CreateNativeElement(id, unoElementId, tagName); + } + + private static void RegisterExistingElementNative(string elementId) + { + } + + internal void AttachToElement(nint ownerId) + { + NativeMethods.AttachNativeElement(ownerId, UnoElementId); + } + + internal void DetachFromElement(nint ownerId) + { + NativeMethods.DetachNativeElement(UnoElementId); + } + + private void SetCssStyleNative(string name, string value) + { + WindowManagerInterop.SetStyleString(UnoElementId, name, value); + } + + private void SetCssStyleNative(params (string name, string value)[] styles) + { + WindowManagerInterop.SetStyles(UnoElementId, styles); + } + + private void ClearCssStyleNative(params string[] names) + { + WindowManagerInterop.ResetStyle(UnoElementId, names); + } + + private void SetCssClassNative(string[] classes, int index) + { + WindowManagerInterop.SetClasses(UnoElementId, classes, index); + } + + private void SetCssClassNative(params string[] classesToSet) + { + WindowManagerInterop.SetUnsetCssClasses(UnoElementId, classesToSet, null); + } + + private void UnsetCssClassNative(params string[] classesToUnset) + { + WindowManagerInterop.SetUnsetCssClasses(UnoElementId, null, classesToUnset); + } + + private void SetHtmlAttributeNative(string name, string value) + { + WindowManagerInterop.SetAttribute(UnoElementId, name, value); + } + + private void SetHtmlAttributeNative(params (string name, string value)[] attributes) + { + WindowManagerInterop.SetAttributes(UnoElementId, attributes); + } + + private string GetHtmlAttributeNative(string name) + { + return WindowManagerInterop.GetAttribute(UnoElementId, name); + } + + private void ClearHtmlAttributeNative(string name) + { + WindowManagerInterop.RemoveAttribute(UnoElementId, name); + } + + private void RemoveAttributeNative(string name) + { + WindowManagerInterop.RemoveAttribute(UnoElementId, name); + } + + private string ExecuteJavascriptNative(string jsCode) + { + var js = $$""" + (function(element) { + {{jsCode}} + })(Uno.UI.WindowManager.current.getView({{UnoElementId}})); + """; + return WebAssemblyRuntime.InvokeJS(js); + } + + private Task ExecuteJavascriptNativeAsync(string asyncJsCode) + { + var js = $$""" + (function(element) { + const __f = () => {{asyncJsCode}}; + return __f(element); + })(Uno.UI.WindowManager.current.getView({{UnoElementId}})); + """; + return WebAssemblyRuntime.InvokeAsync(js); + } + + private void SetHtmlContentNative(string html) + { + WindowManagerInterop.SetContentHtml(UnoElementId, html); + } + + [JSExport] + private static bool DispatchEventNativeElementMethod( + [JSMarshalAs] object owner, + string eventName, + [JSMarshalAs] object eventHandler, + JSObject payload) + { + if (eventHandler is EventWrapper handler) + { + handler.handler(owner, payload); + + return true; + } + else + { + return false; + } + } + + private void RegisterHtmlEventHandlerNative(string eventName, EventHandler handler) + { + var wrapper = new EventWrapper(handler); + _eventMap[handler] = wrapper; + + NativeMethods.RegisterNativeHtmlEvent(this, UnoElementId, eventName, wrapper); + } + + private void UnregisterHtmlEventHandlerNative(string eventName, EventHandler handler) + { + if (_eventMap.TryGetValue(handler, out var wrapper)) + { + _eventMap.Remove(handler); + NativeMethods.UnregisterNativeHtmlEvent(UnoElementId, eventName, wrapper); + } + } + + public void Dispose() + { + NativeMethods.DisposeHtmlElement(UnoElementId); + } + + private static unsafe partial class NativeMethods + { + [JSImport($"globalThis.Uno.UI.WindowManager.current.createNativeElement")] + internal static partial void CreateNativeElement(string elementId, nint unoElementId, string tagName); + + [JSImport($"globalThis.Uno.UI.WindowManager.current.attachNativeElement")] + internal static partial void AttachNativeElement(nint ownerId, nint unoElementId); + + [JSImport($"globalThis.Uno.UI.WindowManager.current.detachNativeElement")] + internal static partial void DetachNativeElement(nint unoElementId); + + [JSImport($"globalThis.Uno.UI.WindowManager.current.disposeNativeElement")] + internal static partial bool DisposeHtmlElement(nint unoElementId); + + [JSImport($"globalThis.Uno.UI.WindowManager.current.registerNativeHtmlEvent")] + internal static partial void RegisterNativeHtmlEvent( + [JSMarshalAs] object browserHtmlElement, + nint unoElementId, + string eventName, + [JSMarshalAs] object handler); + + [JSImport($"globalThis.Uno.UI.WindowManager.current.unregisterNativeHtmlEvent")] + internal static partial void UnregisterNativeHtmlEvent( + nint unoElementId, + string eventName, + [JSMarshalAs] object handler); + } +} diff --git a/src/Uno.UI/UI/Xaml/Controls/ContentPresenter/ContentPresenter.cs b/src/Uno.UI/UI/Xaml/Controls/ContentPresenter/ContentPresenter.cs index 9aa66da07c19..d0caab470204 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ContentPresenter/ContentPresenter.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ContentPresenter/ContentPresenter.cs @@ -1070,11 +1070,11 @@ private void SetContentTemplateRootToPlaceholder() this.Log().DebugFormat("No ContentTemplate was specified for {0} and content is not a UIView, defaulting to TextBlock.", GetType().Name); } - var textBlock = new ImplicitTextBlock(this); - textBlock.SetTemplatedParent(this); - if (!IsNativeHost) { + var textBlock = new ImplicitTextBlock(this); + textBlock.SetTemplatedParent(this); + TemplateBind(TextBlock.TextProperty, nameof(Content)); TemplateBind(TextBlock.HorizontalAlignmentProperty, nameof(HorizontalContentAlignment)); TemplateBind(TextBlock.VerticalAlignmentProperty, nameof(VerticalContentAlignment)); @@ -1087,10 +1087,10 @@ void TemplateBind(DependencyProperty property, string path) => { RelativeSource = RelativeSource.TemplatedParent }); - } - ContentTemplateRoot = textBlock; - IsUsingDefaultTemplate = true; + ContentTemplateRoot = textBlock; + IsUsingDefaultTemplate = true; + } } partial void RegisterContentTemplateRoot(); diff --git a/src/Uno.UI/UI/Xaml/Controls/ContentPresenter/ContentPresenter.wasm.cs b/src/Uno.UI/UI/Xaml/Controls/ContentPresenter/ContentPresenter.wasm.cs new file mode 100644 index 000000000000..39786e047f36 --- /dev/null +++ b/src/Uno.UI/UI/Xaml/Controls/ContentPresenter/ContentPresenter.wasm.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Uno.Disposables; +using Uno.Foundation.Extensibility; +using Uno.Foundation.Logging; +using Uno.UI; +using Uno.UI.NativeElementHosting; +using Windows.Foundation; + +namespace Microsoft.UI.Xaml.Controls; + +partial class ContentPresenter +{ + partial void TryRegisterNativeElement(object oldValue, object newValue) + { + IsNativeHost = newValue is BrowserHtmlElement; + + if (IsNativeHost) + { + ContentTemplateRoot = null; + } + } + + partial void AttachNativeElement() + { + if (Content is BrowserHtmlElement element) + { + element.AttachToElement(HtmlId); + } + } + + partial void DetachNativeElement(object content) + { + if (content is BrowserHtmlElement element) + { + element.DetachFromElement(HtmlId); + } + } +} diff --git a/src/Uno.UI/ts/WindowManager.ts b/src/Uno.UI/ts/WindowManager.ts index 5892fb8eb624..417c78238452 100644 --- a/src/Uno.UI/ts/WindowManager.ts +++ b/src/Uno.UI/ts/WindowManager.ts @@ -119,6 +119,10 @@ namespace Uno.UI { private cursorStyleRule: CSSStyleRule; private allActiveElementsById: { [id: string]: HTMLElement | SVGElement } = {}; + + /** Native elements created with the BrowserHtmlElement class */ + private nativeHandlersMap: { [id: string]: any } = {}; + private uiElementRegistrations: { [id: string]: { typeName: string; @@ -129,6 +133,7 @@ namespace Uno.UI { private static resizeMethod: any; private static dispatchEventMethod: any; + private static dispatchEventNativeElementMethod: any; private static focusInMethod: any; private static dispatchSuspendingMethod: any; private static getDependencyPropertyValueMethod: any; @@ -1346,6 +1351,82 @@ namespace Uno.UI { this.removeLoading(); } + /** + * Creates a native element from BrowserHtlpElement. + */ + public createNativeElement(elementId: string, unoElementId: number, tagname: string): void { + const element = document.createElement(tagname); + element.id = elementId; + (element).unoId = unoElementId; + + // Add the html element to list of elements + this.allActiveElementsById[unoElementId] = element; + } + + /** + * Dispose a native element + */ + public disposeNativeElement(unoElementId: number): void { + this.destroyViewInternal(unoElementId); + } + + /** + * Attaches a native element to a known UIElement-backed element. + */ + public attachNativeElement(ownerId: number, unoElementId: number): void { + var ownerView = this.getView(ownerId); + var elementView = this.getView(unoElementId); + + ownerView.appendChild(elementView); + } + + /** + * Detaches a native element to a known UIElement-backed element. + */ + public detachNativeElement(unoElementId: number): void { + var view = this.getView(unoElementId); + + view.parentElement.removeChild(view); + } + + /** + * Registers a managed event handler + */ + public registerNativeHtmlEvent( + owner: any, + unoElementId: any, + eventName: string, + managedHandler: any + ) { + const element = this.getView(unoElementId); + + const eventHandler = (event: Event) => { + WindowManager.dispatchEventNativeElementMethod(owner, eventName, managedHandler, event); + }; + + // Register the handler using a string representation of the managed handler + // the managed representation assumes that the string contains a unique id. + this.nativeHandlersMap["" + managedHandler] = eventHandler; + + element.addEventListener(eventName, eventHandler); + } + + /** + * Unregisters a managed handler from its element + */ + public unregisterNativeHtmlEvent( + unoElementId: any, + eventName: string, + managedHandler: any) { + const element = this.getView(unoElementId); + const key = "" + managedHandler; + const eventHandler = this.nativeHandlersMap[key]; + if (eventHandler) { + element.removeEventListener(eventName, eventHandler); + delete this.nativeHandlersMap[key]; + } + } + private init() { if (UnoAppManifest.displayName) { @@ -1367,6 +1448,7 @@ namespace Uno.UI { WindowManager.resizeMethod = exports.Microsoft.UI.Xaml.Window.Resize; WindowManager.dispatchEventMethod = exports.Microsoft.UI.Xaml.UIElement.DispatchEvent; + WindowManager.dispatchEventNativeElementMethod = exports.Uno.UI.NativeElementHosting.BrowserHtmlElement.DispatchEventNativeElementMethod; WindowManager.focusInMethod = exports.Microsoft.UI.Xaml.Input.FocusManager.ReceiveFocusNative; WindowManager.dispatchSuspendingMethod = exports.Microsoft.UI.Xaml.Application.DispatchSuspending; WindowManager.keyTrackingMethod = (globalThis).DotnetExports.Uno.Uno.UI.Core.KeyboardStateTracker.UpdateKeyStateNative; From 49b634ebec25328a8951aea81d03c1e76d733c36 Mon Sep 17 00:00:00 2001 From: Jerome Laban Date: Fri, 11 Apr 2025 23:33:21 -0400 Subject: [PATCH 2/5] chore: Adjust runtime test --- .../Tests/Windows_UI_Xaml/Given_BrowserHtmlElement.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_BrowserHtmlElement.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_BrowserHtmlElement.cs index c36f176ea32e..298f35f67863 100644 --- a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_BrowserHtmlElement.cs +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_BrowserHtmlElement.cs @@ -1,4 +1,5 @@ -using System; +#if (__SKIA__ || __WASM__) && HAS_UNO +using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices.JavaScript; @@ -8,14 +9,13 @@ using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Uno.UI.NativeElementHosting; using Uno.UI.RemoteControl.Messaging.IdeChannel; using Windows.UI; using static Private.Infrastructure.TestServices; +using Uno.UI.NativeElementHosting; namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml; -#if (__SKIA__ || __WASM__) && HAS_UNO [TestClass] public class Given_BrowserHtmlElement { From fb04ec5f81560ccdb871366919590593694b9a01 Mon Sep 17 00:00:00 2001 From: Jerome Laban Date: Fri, 11 Apr 2025 23:48:58 -0400 Subject: [PATCH 3/5] chore: Adjust reference target --- .../BrowserHtmlElement.cs | 6 ++ .../BrowserHtmlElement.reference.cs | 85 +++++++++++++++++++ .../BrowserHtmlElement.skia.cs | 2 +- .../BrowserHtmlElement.wasm.cs | 2 +- 4 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 src/Uno.UI/NativeElementHosting/BrowserHtmlElement.reference.cs diff --git a/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.cs b/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.cs index 539a116b43df..d334e21cac20 100644 --- a/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.cs +++ b/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.cs @@ -2,6 +2,7 @@ #nullable enable using System; +using System.Diagnostics.Contracts; using System.Globalization; using System.Reflection; using System.Runtime.InteropServices; @@ -220,5 +221,10 @@ public void RegisterHtmlEventHandler(string eventName, EventHandler ha /// public void UnregisterHtmlEventHandler(string eventName, EventHandler handler) => UnregisterHtmlEventHandlerNative(eventName, handler); + + public void Dispose() + => DisposeNative(); + + partial void DisposeNative(); } #endif diff --git a/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.reference.cs b/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.reference.cs new file mode 100644 index 000000000000..5d5469bd291d --- /dev/null +++ b/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.reference.cs @@ -0,0 +1,85 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; +using System.Xml.Linq; +using Uno.Extensions; + +namespace Uno.UI.NativeElementHosting; + +/// +/// A managed handle to a DOM HTMLElement. +/// +public sealed partial class BrowserHtmlElement : IDisposable +{ + private static void CreateHtmlElementNative(string id, nint unoElementId, string tagName) + => throw new PlatformNotSupportedException(); + + private static void RegisterExistingElementNative(string elementId) + => throw new PlatformNotSupportedException(); + + private void SetCssStyleNative(string name, string value) + => throw new PlatformNotSupportedException(); + + private void SetCssStyleNative(params (string name, string value)[] styles) + => throw new PlatformNotSupportedException(); + + private void ClearCssStyleNative(params string[] names) + => throw new PlatformNotSupportedException(); + + private void SetCssClassNative(string[] classes, int index) + => throw new PlatformNotSupportedException(); + + private void SetCssClassNative(params string[] classesToSet) + => throw new PlatformNotSupportedException(); + + private void UnsetCssClassNative(params string[] classesToUnset) + => throw new PlatformNotSupportedException(); + + private void SetHtmlAttributeNative(string name, string value) + => throw new PlatformNotSupportedException(); + + private void SetHtmlAttributeNative(params (string name, string value)[] attributes) + => throw new PlatformNotSupportedException(); + + private string GetHtmlAttributeNative(string name) + => throw new PlatformNotSupportedException(); + + private void ClearHtmlAttributeNative(string name) + => throw new PlatformNotSupportedException(); + + private void RemoveAttributeNative(string name) + => throw new PlatformNotSupportedException(); + + private string ExecuteJavascriptNative(string jsCode) + => throw new PlatformNotSupportedException(); + + private Task ExecuteJavascriptNativeAsync(string asyncJsCode) + => throw new PlatformNotSupportedException(); + + private void SetHtmlContentNative(string html) + => throw new PlatformNotSupportedException(); + + [JSExport] + private static bool DispatchEventNativeElementMethod( + [JSMarshalAs] object owner, + string eventName, + [JSMarshalAs] object eventHandler, + JSObject payload) + => throw new PlatformNotSupportedException(); + + private void RegisterHtmlEventHandlerNative(string eventName, EventHandler handler) + => throw new PlatformNotSupportedException(); + + private void UnregisterHtmlEventHandlerNative(string eventName, EventHandler handler) + => throw new PlatformNotSupportedException(); + + partial void DisposeNative() + => throw new PlatformNotSupportedException(); + + internal static void Initialize() + => throw new PlatformNotSupportedException(); +} diff --git a/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.skia.cs b/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.skia.cs index da7b27c407f8..1bc7dd5e4a85 100644 --- a/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.skia.cs +++ b/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.skia.cs @@ -153,7 +153,7 @@ private void UnregisterHtmlEventHandlerNative(string eventName, EventHandler Date: Sat, 12 Apr 2025 15:22:05 -0400 Subject: [PATCH 4/5] chore: Adjust wording --- src/Uno.UI/NativeElementHosting/BrowserHtmlElement.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.cs b/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.cs index d334e21cac20..24d5f859788d 100644 --- a/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.cs +++ b/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.cs @@ -50,7 +50,7 @@ private BrowserHtmlElement(string elementId) { if (!OperatingSystem.IsBrowser()) { - throw new NotSupportedException($"{nameof(BrowserHtmlElement)} is only supported on the Wasm target with the skia backend."); + throw new NotSupportedException($"{nameof(BrowserHtmlElement)} is only supported on WebAssembly."); } ElementId = elementId; From 4fc561a6a5901dc5c29e5f97612f2bfe11540ee0 Mon Sep 17 00:00:00 2001 From: Jerome Laban Date: Sun, 13 Apr 2025 21:14:28 -0400 Subject: [PATCH 5/5] chore: Adjust types, renames --- src/Uno.UI/NativeElementHosting/BrowserHtmlElement.skia.cs | 6 +++--- src/Uno.UI/NativeElementHosting/BrowserHtmlElement.wasm.cs | 6 +++--- src/Uno.UI/ts/WindowManager.ts | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.skia.cs b/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.skia.cs index 1bc7dd5e4a85..8638ed0be08c 100644 --- a/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.skia.cs +++ b/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.skia.cs @@ -121,12 +121,12 @@ private void SetHtmlContentNative(string html) private static bool DispatchEventNativeElementMethod( [JSMarshalAs] object owner, string eventName, - [JSMarshalAs] object eventHandler, + [JSMarshalAs] object eventWrapper, JSObject payload) { - if (eventHandler is EventWrapper handler) + if (eventWrapper is EventWrapper wrapper) { - handler.handler(owner, payload); + wrapper.handler(owner, payload); return true; } diff --git a/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.wasm.cs b/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.wasm.cs index edc3b9b71ce6..af466ba904c4 100644 --- a/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.wasm.cs +++ b/src/Uno.UI/NativeElementHosting/BrowserHtmlElement.wasm.cs @@ -124,12 +124,12 @@ private void SetHtmlContentNative(string html) private static bool DispatchEventNativeElementMethod( [JSMarshalAs] object owner, string eventName, - [JSMarshalAs] object eventHandler, + [JSMarshalAs] object eventWrapper, JSObject payload) { - if (eventHandler is EventWrapper handler) + if (eventWrapper is EventWrapper wrapper) { - handler.handler(owner, payload); + wrapper.handler(owner, payload); return true; } diff --git a/src/Uno.UI/ts/WindowManager.ts b/src/Uno.UI/ts/WindowManager.ts index 417c78238452..3ea47eca102b 100644 --- a/src/Uno.UI/ts/WindowManager.ts +++ b/src/Uno.UI/ts/WindowManager.ts @@ -1352,7 +1352,7 @@ namespace Uno.UI { } /** - * Creates a native element from BrowserHtlpElement. + * Creates a native element from BrowserHttpElement. */ public createNativeElement(elementId: string, unoElementId: number, tagname: string): void { const element = document.createElement(tagname); @@ -1360,7 +1360,7 @@ namespace Uno.UI { (element).unoId = unoElementId; // Add the html element to list of elements - this.allActiveElementsById[unoElementId] = element; + this.allActiveElementsById[this.handleToString(unoElementId)] = element; } /**