From 080861ed4a7d7e0169884d23eb45bb27cd07c4c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20L=C3=BCbe=C3=9F?= Date: Sun, 17 Nov 2024 19:09:52 +0100 Subject: [PATCH] Use Attributes for JS<->Beef binding --- BeefSpace.toml | 2 +- .../src/Ultralight/RegisterJsCallAttribute.bf | 53 ++++++++ .../src/Ultralight/UltralightMainWindow.bf | 115 +++++++----------- .../src/Ultralight/UltralightWindow.bf | 72 ++++++++++- 4 files changed, 169 insertions(+), 73 deletions(-) create mode 100644 GlitchyEditor/src/Ultralight/RegisterJsCallAttribute.bf diff --git a/BeefSpace.toml b/BeefSpace.toml index 716f74f..77b79f2 100644 --- a/BeefSpace.toml +++ b/BeefSpace.toml @@ -1,6 +1,6 @@ FileVersion = 1 Projects = {GlitchyEngine = {Path = "GlitchyEngine"}, GlitchLog = {Path = "GlitchLog"}, DirectX = {Path = "GlitchyEngine/vendor/directx/DirectX"}, LodePng = {Path = "vendor/lodepng-beef/lodepng-beef"}, FreeType = {Path = "GlitchyEngine/vendor/freetype"}, cgltf-beef = {Path = "GlitchyEngine/vendor/gltf/cgltf-beef"}, GlitchyEditor = {Path = "GlitchyEditor"}, msdfgen-beef = {Path = "GlitchyEngine/vendor/msdfgen/msdfgen-beef"}, ImGui = {Path = "GlitchyEngine/vendor/imgui/ImGui"}, ImGuiImplDX11 = {Path = "GlitchyEngine/vendor/imgui/ImGuiImplDX11"}, ImGuiImplWin32 = {Path = "GlitchyEngine/vendor/imgui/ImGuiImplWin32"}, ImGuizmo = {Path = "GlitchyEngine/vendor/imgui/ImGuizmo"}, GlitchyEngineHelper = {Path = "GlitchyEngineHelper"}, bon = {Path = "GlitchyEngine/vendor/bon"}, box2d-beef = {Path = "GlitchyEngine/vendor/box2D"}, "Beef.Linq" = {Path = "GlitchyEngine/vendor/Beef.Linq/src"}, ScriptCore = {Path = "ScriptCore"}, Ultralight = {Path = "GlitchyEngine/vendor/Ultralight-beef"}, EditorUI = {Path = "GlitchyEditor/EditorUI"}} -WorkspaceFolders = {GlitchyEngine = ["GlitchyEngine", "GlitchLog", "GlitchyEngineHelper", "ScriptCore"], "GlitchyEngine/Dependencies" = ["cgltf-beef", "DirectX", "FreeType", "ImGui", "ImGuiImplDX11", "ImGuiImplWin32", "ImGuizmo", "LodePng", "msdfgen-beef", "bon", "box2d-beef", "Beef.Linq"]} +WorkspaceFolders = {GlitchyEngine = ["GlitchyEngine", "GlitchLog", "GlitchyEngineHelper", "ScriptCore"], "GlitchyEngine/Dependencies" = ["cgltf-beef", "DirectX", "FreeType", "ImGui", "ImGuiImplDX11", "ImGuiImplWin32", "ImGuizmo", "LodePng", "msdfgen-beef", "bon", "box2d-beef", "Beef.Linq"], EditorDependencies = ["Ultralight"]} [Workspace] StartupProject = "GlitchyEditor" diff --git a/GlitchyEditor/src/Ultralight/RegisterJsCallAttribute.bf b/GlitchyEditor/src/Ultralight/RegisterJsCallAttribute.bf new file mode 100644 index 0000000..dcc103a --- /dev/null +++ b/GlitchyEditor/src/Ultralight/RegisterJsCallAttribute.bf @@ -0,0 +1,53 @@ +using System; +using System.Reflection; +using System.Collections; + +namespace GlitchyEditor.Ultralight; + +[AttributeUsage(.Method |.Field)] +struct BindToJsFunctionAttribute : Attribute +{ + public String FunctionName; + + public this(String functionName) + { + FunctionName = functionName; + } +} + +[AttributeUsage(.Method)] +struct BeefMethodBinderAttribute : Attribute, IOnMethodInit +{ + [Comptime] + public void OnMethodInit(MethodInfo targetMethod, Self* prev) + { + HashSet boundJsFunctions = new HashSet(); + + for (MethodInfo methodInfo in targetMethod.DeclaringType.GetMethods(.Instance | .Public | .NonPublic)) + { + if (methodInfo.GetCustomAttribute() case .Ok(let attribute)) + { + Runtime.Assert(!boundJsFunctions.Contains(attribute.FunctionName), new $"The JS-Method {attribute.FunctionName} is already bound to another method."); + + boundJsFunctions.Add(attribute.FunctionName); + + String str = new $"BindMethodToJsFunction(context, scriptGlue, \"{attribute.FunctionName}\", new:stdAlloc => {methodInfo.Name});\n"; + Compiler.EmitMethodExit(targetMethod, str); + } + } + + for (FieldInfo fieldInfo in targetMethod.DeclaringType.GetFields(.Instance | .Public | .NonPublic)) + { + if (fieldInfo.GetCustomAttribute() case .Ok(let attribute)) + { + // Runtime.Assert(t.IsDelegate, new $"The field must be of delegate type."); + Runtime.Assert(!boundJsFunctions.Contains(attribute.FunctionName), new $"The JS-Method {attribute.FunctionName} is already bound to another method."); + + boundJsFunctions.Add(attribute.FunctionName); + + String str = new $"{fieldInfo.Name} = GetJsCallbackDelegate(context, scriptGlue, \"{attribute.FunctionName}\");\n"; + Compiler.EmitMethodExit(targetMethod, str); + } + } + } +} diff --git a/GlitchyEditor/src/Ultralight/UltralightMainWindow.bf b/GlitchyEditor/src/Ultralight/UltralightMainWindow.bf index c682305..11dfc1a 100644 --- a/GlitchyEditor/src/Ultralight/UltralightMainWindow.bf +++ b/GlitchyEditor/src/Ultralight/UltralightMainWindow.bf @@ -59,9 +59,9 @@ class UltralightMainWindow : UltralightWindow private void UpdateEntityHierarchy() { - if (DoStuffFunction == null) + if (uiCallUpdateEntities == null) { - Log.EngineLogger.Error($"{nameof(DoStuffFunction)} is null, skipping entity hierarchy update."); + Log.EngineLogger.Error($"{nameof(uiCallUpdateEntities)} is null, skipping entity hierarchy update."); return; } @@ -248,7 +248,7 @@ class UltralightMainWindow : UltralightWindow } JSValueRef exception = null; - DoStuffFunction(context, Span(&jsArray, 1), &exception); + uiCallUpdateEntities(context, Span(&jsArray, 1), &exception); if (exception != null) { @@ -262,9 +262,16 @@ class UltralightMainWindow : UltralightWindow //} } } + + [BindToJsFunction("requestEntityHierarchyUpdate")] + void HandleRequestEntityHierarchyUpdate(JSContextRef context, JSObjectRef thisObject, Span arguments, JSValueRef* exception = null) + { + _forceEntityHierarchyRebuild = true; + } private bool _hoveringNonClientArea; + [BindToJsFunction("handleHoverNonClientArea")] void HandleHoverNonClientArea(JSContextRef context, JSObjectRef thisObject, Span arguments, JSValueRef* exception = null) { Log.EngineLogger.Info("HandleHoverNonClientArea"); @@ -290,6 +297,24 @@ class UltralightMainWindow : UltralightWindow Log.ClientLogger.Info($"_hoveringNonClientArea: {_hoveringNonClientArea}"); } + [BindToJsFunction("handleHoverMaximizeWindow")] + void HandleHoverMaximizeButton(JSContextRef context, JSObjectRef thisObject, Span arguments, JSValueRef* exception = null) + { + HandleHoverTitlebarButton(.Maximize, context, thisObject, arguments, exception); + } + + [BindToJsFunction("handleHoverMinimizeWindow")] + void HandleHoverMinimizeButton(JSContextRef context, JSObjectRef thisObject, Span arguments, JSValueRef* exception = null) + { + HandleHoverTitlebarButton(.Minimize, context, thisObject, arguments, exception); + } + + [BindToJsFunction("handleHoverCloseWindow")] + void HandleHoverCloseButton(JSContextRef context, JSObjectRef thisObject, Span arguments, JSValueRef* exception = null) + { + HandleHoverTitlebarButton(.Close, context, thisObject, arguments, exception); + } + void HandleHoverTitlebarButton(TitleBarButton button, JSContextRef context, JSObjectRef thisObject, Span arguments, JSValueRef* exception = null) { if (arguments.Length != 1) @@ -308,83 +333,31 @@ class UltralightMainWindow : UltralightWindow EditorLayer.HoveredTitleBarButton = _window.[Friend]_hoveredTitleBarButton; } - - private void RegisterBeefFunction(JSContextRef context, JSObjectRef object, StringView functionName, JSCallback callback) + + [BindToJsFunction("handleClickMaximizeWindow")] + void HandleClickMaximizeButton(JSContextRef context, JSObjectRef thisObject, Span arguments, JSValueRef* exception = null) { - JSObjectRef func = UltralightHelper.CreateJsFunctionFromDelegate(context, callback); - - JSStringRef name = JSStringCreateWithUTF8CString(functionName.ToScopeCStr!()); - - JSObjectRef exception; - JSObjectSetProperty(context, object, name, func, 0, &exception); - - JSStringRelease(name); + _window.ToggleMaximize(); } - private JsFunctionCall DoStuffFunction; - - delegate JSValueRef JsFunctionCall(JSContextRef context, Span arguments, JSValueRef* exception); - - private JsFunctionCall GetJsCallbackDelegate(JSContextRef context, JSObjectRef object, StringView functionName) + [BindToJsFunction("handleClickMinimizeWindow")] + void HandleClickMinimizeButton(JSContextRef context, JSObjectRef thisObject, Span arguments, JSValueRef* exception = null) { - JSStringRef functionNameString = JSStringCreateWithUTF8CString(functionName.ToScopeCStr!()); - defer JSStringRelease(functionNameString); - - JSValueRef func = JSObjectGetProperty(context, object, functionNameString, null); - - if (!JSValueIsObject(context, func)) - { - return null; - } - - JSObjectRef functionObject = JSValueToObject(context, func, null); - - if (functionObject == null || !JSObjectIsFunction(context, functionObject)) - { - return null; - } - - return new (c, args, ex) => { - return JSObjectCallAsFunction(c, functionObject, object, (uint32)args.Length, args.Ptr, ex); - }; + _window.Minimize(); } - private void RegisterEngineGlueFunctions() + [BindToJsFunction("handleClickCloseWindow")] + void HandleClickCloseButton(JSContextRef context, JSObjectRef thisObject, Span arguments, JSValueRef* exception = null) { - JSContextRef context = ulViewLockJSContext(_view); - defer ulViewUnlockJSContext(_view); - - JSObjectRef globalObject = JSContextGetGlobalObject(context); - - JSValueRef exception = null; - JSStringRef scriptGlueName = JSStringCreateWithUTF8CString("EngineGlue"); - JSValueRef scriptGlue = JSObjectGetProperty(context, globalObject, scriptGlueName, &exception); - JSStringRelease(scriptGlueName); - - if (JSValueGetType(context, scriptGlue) == .kJSTypeUndefined) - { - Log.EngineLogger.Error("Failed to get EngineGlue object from JS context."); - return; - } - - StdAllocator stdAlloc = StdAllocator(); - - RegisterBeefFunction(context, scriptGlue, "handleHoverNonClientArea", new:stdAlloc => HandleHoverNonClientArea); - RegisterBeefFunction(context, scriptGlue, "handleHoverMaximizeWindow", new:stdAlloc (c, t, a, e) => HandleHoverTitlebarButton(.Maximize, c, t, a, e)); - RegisterBeefFunction(context, scriptGlue, "handleHoverMinimizeWindow", new:stdAlloc (c, t, a, e) => HandleHoverTitlebarButton(.Minimize, c, t, a, e)); - RegisterBeefFunction(context, scriptGlue, "handleHoverCloseWindow", new:stdAlloc (c, t, a, e) => HandleHoverTitlebarButton(.Close, c, t, a, e)); - RegisterBeefFunction(context, scriptGlue, "handleClickMaximizeWindow", new:stdAlloc (c, t, a, e) => { _window.ToggleMaximize(); }); - RegisterBeefFunction(context, scriptGlue, "handleClickMinimizeWindow", new:stdAlloc (c, t, a, e) => { _window.Minimize(); }); - RegisterBeefFunction(context, scriptGlue, "handleClickCloseWindow", new:stdAlloc (c, t, a, e) => { _window.Close(); }); - - RegisterBeefFunction(context, scriptGlue, "requestEntityHierarchyUpdate", new:stdAlloc (c, t, a, e) => { _forceEntityHierarchyRebuild = true; }); - - DoStuffFunction = GetJsCallbackDelegate(context, scriptGlue, "callFromEngine_updateEntities"); + _window.Close(); } + + [BindToJsFunction("callFromEngine_updateEntities")] + private JsFunctionCall uiCallUpdateEntities ~ delete _; - protected override void OnDOMReady(C_View* caller, uint64 frame_id, bool is_main_frame, C_String* url) + [BeefMethodBinder] + protected override void BindBeefMethodsToJsFunctions(OpaqueJSContext* context, OpaqueJSValue* scriptGlue, StdAllocator stdAlloc) { - RegisterEngineGlueFunctions(); - + base.BindBeefMethodsToJsFunctions(context, scriptGlue, stdAlloc); } } diff --git a/GlitchyEditor/src/Ultralight/UltralightWindow.bf b/GlitchyEditor/src/Ultralight/UltralightWindow.bf index 64722f3..ef2b897 100644 --- a/GlitchyEditor/src/Ultralight/UltralightWindow.bf +++ b/GlitchyEditor/src/Ultralight/UltralightWindow.bf @@ -150,6 +150,73 @@ abstract class UltralightWindow _texture = newTexture; } + protected delegate JSValueRef JsFunctionCall(JSContextRef context, Span arguments, JSValueRef* exception); + + protected JsFunctionCall GetJsCallbackDelegate(JSContextRef context, JSObjectRef object, StringView functionName) + { + JSStringRef functionNameString = JSStringCreateWithUTF8CString(functionName.ToScopeCStr!()); + defer JSStringRelease(functionNameString); + + JSValueRef func = JSObjectGetProperty(context, object, functionNameString, null); + + if (!JSValueIsObject(context, func)) + { + return null; + } + + JSObjectRef functionObject = JSValueToObject(context, func, null); + + if (functionObject == null || !JSObjectIsFunction(context, functionObject)) + { + return null; + } + + return new (c, args, ex) => { + return JSObjectCallAsFunction(c, functionObject, object, (uint32)args.Length, args.Ptr, ex); + }; + } + + protected void BindMethodToJsFunction(JSContextRef context, JSObjectRef object, StringView functionName, JSCallback callback) + { + JSObjectRef func = UltralightHelper.CreateJsFunctionFromDelegate(context, callback); + + JSStringRef name = JSStringCreateWithUTF8CString(functionName.ToScopeCStr!()); + + JSObjectRef exception; + JSObjectSetProperty(context, object, name, func, 0, &exception); + + JSStringRelease(name); + } + + //[BeefMethodBinder] + protected virtual void BindBeefMethodsToJsFunctions(JSContextRef context, JSValueRef scriptGlue, StdAllocator stdAlloc) + { + } + + private void RegisterEngineGlueFunctions() + { + JSContextRef context = ulViewLockJSContext(_view); + defer ulViewUnlockJSContext(_view); + + JSObjectRef globalObject = JSContextGetGlobalObject(context); + + JSValueRef exception = null; + JSStringRef scriptGlueName = JSStringCreateWithUTF8CString("EngineGlue"); + JSValueRef scriptGlue = JSObjectGetProperty(context, globalObject, scriptGlueName, &exception); + JSStringRelease(scriptGlueName); + + if (JSValueGetType(context, scriptGlue) == .kJSTypeUndefined) + { + Log.EngineLogger.Error("Failed to get EngineGlue object from JS context."); + return; + } + + StdAllocator stdAlloc = StdAllocator(); + BindBeefMethodsToJsFunctions(context, scriptGlue, stdAlloc); + + //uiCallUpdateEntities = GetJsCallbackDelegate(context, scriptGlue, "callFromEngine_updateEntities"); + } + #region Events private void EventHandler(Event e) @@ -287,7 +354,10 @@ abstract class UltralightWindow window.OnDOMReady(caller, frame_id, is_main_frame, url); } - protected virtual void OnDOMReady(ULView caller, uint64 frame_id, bool is_main_frame, ULString url) { } + protected virtual void OnDOMReady(ULView caller, uint64 frame_id, bool is_main_frame, ULString url) + { + RegisterEngineGlueFunctions(); + } private static void OnFailedLoading(void* user_data, C_View* caller, uint64 frame_id, bool is_main_frame, C_String* url, C_String* description, C_String* error_domain, int32 error_code) {