From 31ad83481fd3f9f2d40c6df7f67b013cc4001520 Mon Sep 17 00:00:00 2001 From: Quentin Quadrat Date: Fri, 29 Nov 2024 19:38:55 +0100 Subject: [PATCH] WIP JS bindings --- addons/gdcef/demos/JS/Control.gd | 218 +++++++++++++++++ addons/gdcef/demos/JS/Control.tscn | 28 +++ .../demos/JS/character-management-ui.html | 220 ++++++++++++++++++ addons/gdcef/demos/JS/default_env.tres | 7 + addons/gdcef/demos/JS/icon.png | Bin 0 -> 4843 bytes addons/gdcef/demos/JS/project.godot | 25 ++ addons/gdcef/demos/README.md | 6 + addons/gdcef/gdcef/SConstruct | 2 +- addons/gdcef/gdcef/src/gdbrowser.cpp | 56 +++++ addons/gdcef/gdcef/src/gdbrowser.hpp | 8 +- addons/gdcef/gdcef/src/gdcef.cpp | 60 ++++- addons/gdcef/gdcef/src/gdcef.hpp | 23 +- addons/gdcef/gdcef/src/godot_js_binder.cpp | 174 ++++++++++++++ addons/gdcef/gdcef/src/godot_js_binder.hpp | 64 +++++ addons/gdcef/gdcef/src/register_types.cpp | 1 + .../render_process/src/render_process.cpp | 148 ++++++------ .../render_process/src/render_process.hpp | 80 ++----- 17 files changed, 983 insertions(+), 137 deletions(-) create mode 100644 addons/gdcef/demos/JS/Control.gd create mode 100644 addons/gdcef/demos/JS/Control.tscn create mode 100644 addons/gdcef/demos/JS/character-management-ui.html create mode 100644 addons/gdcef/demos/JS/default_env.tres create mode 100644 addons/gdcef/demos/JS/icon.png create mode 100644 addons/gdcef/demos/JS/project.godot create mode 100644 addons/gdcef/gdcef/src/godot_js_binder.cpp create mode 100644 addons/gdcef/gdcef/src/godot_js_binder.hpp diff --git a/addons/gdcef/demos/JS/Control.gd b/addons/gdcef/demos/JS/Control.gd new file mode 100644 index 0000000..35465f8 --- /dev/null +++ b/addons/gdcef/demos/JS/Control.gd @@ -0,0 +1,218 @@ +# ============================================================================== +# Basic application made in HTML/JS/CSS with interaction with Godot. +# ============================================================================== +extends Control + +# ============================================================================== +# CEF variables +# ============================================================================== +const BROWSER_NAME = "player_stats" +@onready var mouse_pressed: bool = false + +# ============================================================================== +# Variables for character stats +# ============================================================================== +@onready var player_name: String = "Anonymous" +@onready var weapon: String = "sword" +@onready var xp: int = 0 +@onready var level: int = 1 + +# ============================================================================== +# Initial character configuration +# ============================================================================== +func _ready(): + initialize_cef() +# expose_methods() + pass + +# ============================================================================== +# Expose methods to JavaScript via CEF +# ============================================================================== +#func expose_methods(): +# pass + +# ============================================================================== +# Change character's weapon +# ============================================================================== +func change_weapon(new_weapon: String): + print("Weapon changed to: ", new_weapon) + weapon = new_weapon + _update_character_stats() + pass + +# ============================================================================== +# Set character's name +# ============================================================================== +func set_character_name(new_name: String): + print("New name: ", new_name) + player_name = new_name + _update_character_stats() + pass + +# ============================================================================== +# Modify XP (can be positive or negative) +# ============================================================================== +func modify_xp(xp_change: int): + xp += xp_change + _level_up_check() + _update_character_stats() + pass + +# ============================================================================== +# Check for level up +# ============================================================================== +func _level_up_check(): + var previous_level = level + level = 1 + floor(xp / 100) # Simple progression example + + if level > previous_level: + print("Level up! New level: ", level) + pass + +# ============================================================================== +# Update character statistics +# ============================================================================== +func _update_character_stats(): + var character_info = { + "name": player_name, + "weapon": weapon, + "xp": xp, + "level": level + } + print("Character update: ", character_info) + pass + +# ============================================================================== +# Optional method to get complete character state +# ============================================================================== +func get_character_state() -> Dictionary: + return { + "name": player_name, + "weapon": weapon, + "xp": xp, + "level": level + } + +# ============================================================================== +# CEF Callback when a page has ended to load with success. +# ============================================================================== +func _on_page_loaded(browser): + print("The browser " + browser.name + " has loaded " + browser.get_url()) + $CEF.register_method(self, browser, "change_weapon") + $CEF.register_method(self, browser, "set_character_name") + $CEF.register_method(self, browser, "modify_xp") + pass + +# ============================================================================== +# Callback when a page has ended to load with failure. +# Display a load error message using a data: URI. +# ============================================================================== +func _on_page_failed_loading(_err_code, _err_msg, browser): + $AcceptDialog.title = "Alert!" + $AcceptDialog.dialog_text = "The browser " + browser.name + " did not load " + browser.get_url() + $AcceptDialog.popup_centered(Vector2(0, 0)) + $AcceptDialog.show() + pass + +# ============================================================================== +# Split the browser vertically to display two browsers (aka tabs) rendered in +# two separate textures. +# ============================================================================== +func initialize_cef(): + + ### CEF + + if !$CEF.initialize({"incognito": true, "locale": "en-US", + "remote_debugging_port": 7777, "remote_allow_origin": "*"}): + push_error("Failed initializing CEF") + get_tree().quit() + else: + push_warning("CEF version: " + $CEF.get_full_version()) + pass + + ### Browser + + var browser = $CEF.create_browser("", $TextureRect, {"javascript": true}) + browser.name = BROWSER_NAME + browser.connect("on_page_loaded", _on_page_loaded) + browser.connect("on_page_failed_loading", _on_page_failed_loading) + browser.resize($TextureRect.get_size()) + browser.load_data_uri(_load_html_file(), "text/html") + pass + +# ============================================================================== +# Load the HTML file containing the JavaScript code +# ============================================================================== +func _load_html_file(): + var file = FileAccess.open("res://character-management-ui.html", FileAccess.READ) + var content = file.get_as_text() + file.close() + return content + +# ============================================================================== +# Get the browser node interacting with the JavaScript code. +# ============================================================================== +func get_browser(): + var browser = $CEF.get_node(BROWSER_NAME) + if browser == null: + push_error("Failed getting Godot node '" + name + "'") + get_tree().quit() + return browser + +# ============================================================================== +# Get mouse events and broadcast them to CEF +# ============================================================================== +func _on_TextureRect_gui_input(event: InputEvent): + var current_browser = get_browser() + if event is InputEventMouseButton: + if event.button_index == MOUSE_BUTTON_WHEEL_UP: + current_browser.set_mouse_wheel_vertical(2) + elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN: + current_browser.set_mouse_wheel_vertical(-2) + elif event.button_index == MOUSE_BUTTON_LEFT: + mouse_pressed = event.pressed + if mouse_pressed: + current_browser.set_mouse_left_down() + else: + current_browser.set_mouse_left_up() + elif event.button_index == MOUSE_BUTTON_RIGHT: + mouse_pressed = event.pressed + if mouse_pressed: + current_browser.set_mouse_right_down() + else: + current_browser.set_mouse_right_up() + else: + mouse_pressed = event.pressed + if mouse_pressed: + current_browser.set_mouse_middle_down() + else: + current_browser.set_mouse_middle_up() + elif event is InputEventMouseMotion: + if mouse_pressed: + current_browser.set_mouse_left_down() + current_browser.set_mouse_moved(event.position.x, event.position.y) + pass + +# ============================================================================== +# Make the CEF browser reacts from keyboard events. +# ============================================================================== +func _input(event): + if event is InputEventKey: + get_browser().set_key_pressed( + event.unicode if event.unicode != 0 else event.keycode, + event.pressed, event.shift_pressed, event.alt_pressed, + event.is_command_or_control_pressed()) + pass + +# ============================================================================== +# Windows has resized +# ============================================================================== +func _on_texture_rect_resized(): + get_browser().resize($Panel/VBox/TextureRect.get_size()) + pass + +# ============================================================================== +# CEF is implicitly updated by this function. +# ============================================================================== +func _process(_delta): + pass diff --git a/addons/gdcef/demos/JS/Control.tscn b/addons/gdcef/demos/JS/Control.tscn new file mode 100644 index 0000000..8e6ca1c --- /dev/null +++ b/addons/gdcef/demos/JS/Control.tscn @@ -0,0 +1,28 @@ +[gd_scene load_steps=2 format=3 uid="uid://ckewd5tjvl4rj"] + +[ext_resource type="Script" path="res://Control.gd" id="2"] + +[node name="Control" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("2") + +[node name="TextureRect" type="TextureRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="CEF" type="GDCef" parent="."] + +[node name="Timer" type="Timer" parent="."] +wait_time = 10.0 +autostart = true + +[connection signal="gui_input" from="TextureRect" to="." method="_on_TextureRect_gui_input"] diff --git a/addons/gdcef/demos/JS/character-management-ui.html b/addons/gdcef/demos/JS/character-management-ui.html new file mode 100644 index 0000000..2d2b18d --- /dev/null +++ b/addons/gdcef/demos/JS/character-management-ui.html @@ -0,0 +1,220 @@ + + + + + + Character Management UI + + + + +
+
+

Character Management UI

+
+ +
+

Name of the character

+
+ + +
+
+ +
+

Weapon Selection

+
+ + + + + + +
+
+ +
+

XP Management

+
+ + + +
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/addons/gdcef/demos/JS/default_env.tres b/addons/gdcef/demos/JS/default_env.tres new file mode 100644 index 0000000..dfc5189 --- /dev/null +++ b/addons/gdcef/demos/JS/default_env.tres @@ -0,0 +1,7 @@ +[gd_resource type="Environment" load_steps=2 format=3 uid="uid://btpjrvhyjvhj2"] + +[sub_resource type="Sky" id="1"] + +[resource] +background_mode = 2 +sky = SubResource("1") diff --git a/addons/gdcef/demos/JS/icon.png b/addons/gdcef/demos/JS/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1acbade95549969d28cb4f82c6982d6235409321 GIT binary patch literal 4843 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 MO4^Dw@-~tG1M5PK^Z)<= literal 0 HcmV?d00001 diff --git a/addons/gdcef/demos/JS/project.godot b/addons/gdcef/demos/JS/project.godot new file mode 100644 index 0000000..322cd70 --- /dev/null +++ b/addons/gdcef/demos/JS/project.godot @@ -0,0 +1,25 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="JS Bindings" +run/main_scene="res://Control.tscn" +config/features=PackedStringArray("4.3") +config/icon="res://icon.png" + +[physics] + +common/enable_pause_aware_picking=true + +[rendering] + +renderer/rendering_method="gl_compatibility" +environment/defaults/default_environment="res://default_env.tres" diff --git a/addons/gdcef/demos/README.md b/addons/gdcef/demos/README.md index ec91676..1867ffd 100644 --- a/addons/gdcef/demos/README.md +++ b/addons/gdcef/demos/README.md @@ -35,3 +35,9 @@ A demo showing a 3D GUI with a single CEF browser tab showing a radio website. T This demo is based on the asset library: https://godotengine.org/asset-library/asset/127 ![Screenshot](3D/icon.png) + +## Demo 03: CEF browser in 2D with JS bindings + +A demo showing a 2D GUI made in HTML/CSS/JavaScript and loaded in a CEF browser. The demo shows how to bind Godot methods to JavaScript functions using JS Binder. + +![Screenshot](JS/icon.png) diff --git a/addons/gdcef/gdcef/SConstruct b/addons/gdcef/gdcef/SConstruct index b327692..73fd195 100644 --- a/addons/gdcef/gdcef/SConstruct +++ b/addons/gdcef/gdcef/SConstruct @@ -232,7 +232,7 @@ env.Append(CPPPATH=['src/']) env.Append(RPATH=[env['cef_artifacts_folder'][2:-2]]) # Compile the library -sources = ['src/helper_files.cpp', 'src/browser_io.cpp', 'src/gdcef.cpp', 'src/gdbrowser.cpp', 'src/register_types.cpp'] +sources = ['src/helper_files.cpp', 'src/browser_io.cpp', 'src/gdcef.cpp', 'src/gdbrowser.cpp', 'src/register_types.cpp', 'src/godot_js_binder.cpp'] # sources = Glob('src/*.cpp') library = env.SharedLibrary(target=target_path + '/' + target_library, source=sources) Default(library) diff --git a/addons/gdcef/gdcef/src/gdbrowser.cpp b/addons/gdcef/gdcef/src/gdbrowser.cpp index 0ec824f..9b999ec 100644 --- a/addons/gdcef/gdcef/src/gdbrowser.cpp +++ b/addons/gdcef/gdcef/src/gdbrowser.cpp @@ -37,6 +37,10 @@ # include #endif +#ifndef CALL_GODOT_METHOD +# define CALL_GODOT_METHOD "callGodotMethod" +#endif + //------------------------------------------------------------------------------ // Visit the html content of the current page. class Visitor: public CefStringVisitor @@ -886,4 +890,56 @@ void GDBrowserView::onDownloadUpdated( // Emit signal for Godot script emit_signal( "on_download_updated", godot::String(file.c_str()), percentage, this); +} + +//------------------------------------------------------------------------------ +bool GDBrowserView::Impl::OnProcessMessageReceived( + CefRefPtr browser, + CefRefPtr frame, + CefProcessId source_process, + CefRefPtr message) +{ + if (message->GetName() != CALL_GODOT_METHOD) + { + return false; + } + + CefRefPtr args = message->GetArgumentList(); + if (args->GetSize() < 1) + { + return false; + } + + // Get method name from first argument + CefString method_name = args->GetString(0); + + // Convert CEF arguments to Godot arguments + godot::Array godot_args; + for (size_t i = 1; i < args->GetSize(); ++i) + { + switch (args->GetType(i)) + { + case VTYPE_BOOL: + godot_args.push_back(args->GetBool(i)); + break; + case VTYPE_INT: + godot_args.push_back(args->GetInt(i)); + break; + case VTYPE_DOUBLE: + godot_args.push_back(args->GetDouble(i)); + break; + case VTYPE_STRING: + godot_args.push_back( + godot::String(args->GetString(i).ToString().c_str())); + break; + default: + // For unsupported types, pass as string + godot_args.push_back( + godot::String(args->GetString(i).ToString().c_str())); + } + } + + // Call the Godot method + m_owner.call_deferred(method_name.ToString().c_str(), godot_args); + return true; } \ No newline at end of file diff --git a/addons/gdcef/gdcef/src/gdbrowser.hpp b/addons/gdcef/gdcef/src/gdbrowser.hpp index 76a3de8..28a9f65 100644 --- a/addons/gdcef/gdcef/src/gdbrowser.hpp +++ b/addons/gdcef/gdcef/src/gdbrowser.hpp @@ -189,6 +189,12 @@ class GDBrowserView: public godot::Node return this; } + virtual bool + OnProcessMessageReceived(CefRefPtr browser, + CefRefPtr frame, + CefProcessId source_process, + CefRefPtr message) override; + private: // CefRenderHandler interfaces // --------------------------------------------------------------------- @@ -285,7 +291,7 @@ class GDBrowserView: public godot::Node { } - private: // CefBrowserProcessHandler interfaces + private: // CefLifeSpanHandler interfaces virtual bool OnBeforePopup( CefRefPtr browser, diff --git a/addons/gdcef/gdcef/src/gdcef.cpp b/addons/gdcef/gdcef/src/gdcef.cpp index 788693b..1ba1e8d 100644 --- a/addons/gdcef/gdcef/src/gdcef.cpp +++ b/addons/gdcef/gdcef/src/gdcef.cpp @@ -26,6 +26,7 @@ //------------------------------------------------------------------------------ #include "gdcef.hpp" #include "gdbrowser.hpp" +#include "godot_js_binder.hpp" #include "helper_config.hpp" #include "helper_files.hpp" @@ -122,13 +123,15 @@ void GDCef::_bind_methods() GDCEF_DEBUG(""); using namespace godot; - ClassDB::bind_method(D_METHOD("initialize"), &GDCef::initialize); + ClassDB::bind_method(D_METHOD("initialize", "config"), &GDCef::initialize); ClassDB::bind_method(D_METHOD("get_full_version"), &GDCef::version); ClassDB::bind_method(D_METHOD("get_version_part"), &GDCef::versionPart); ClassDB::bind_method(D_METHOD("create_browser"), &GDCef::createBrowser); ClassDB::bind_method(D_METHOD("shutdown"), &GDCef::shutdown); ClassDB::bind_method(D_METHOD("is_alive"), &GDCef::isAlive); ClassDB::bind_method(D_METHOD("get_error"), &GDCef::getError); + ClassDB::bind_method(D_METHOD("register_method"), + &GDCef::registerGodotMethod); } //------------------------------------------------------------------------------ @@ -581,6 +584,61 @@ GDBrowserView* GDCef::createBrowser(godot::String const& url, return browser; } +//------------------------------------------------------------------------------ +//! \brief Method to register Godot methods in the JavaScript context +//! \param [in] godot_object The Godot object containing the method to register +//! \param [in] method_name The name of the method to register +//------------------------------------------------------------------------------ +void GDCef::registerGodotMethod(godot::Object* godot_object, + GDBrowserView* browser, + godot::String const& method_name) +{ + if (godot_object == nullptr) + { + GDCEF_ERROR("Invalid Godot object passed to registerGodotMethod"); + return; + } + + if (browser == nullptr) + { + GDCEF_ERROR("Invalid browser passed to registerGodotMethod"); + return; + } + + // Check that the method exists in the Godot object. + if (!godot_object->has_method(method_name)) + { + GDCEF_ERROR("Method " << method_name.utf8().get_data() + << " does not exist in the Godot object"); + return; + } + + // Handle private GDScript methods (starting with "_"). + // Remove the "_" initial for the JavaScript method name + godot::String js_method_name = method_name; + if (method_name.begins_with("_")) + { + js_method_name = method_name.substr(1); + } + + // Inject the JavaScript code to create the function + std::string js_code = + "window.godot." + std::string(js_method_name.utf8().get_data()) + + " = function() { return window.godot.callGodotMethod('" + + std::string(js_method_name.utf8().get_data()) + "', ...arguments); };"; + + // Get the active browser from the caller + godot::Node* caller = godot::Object::cast_to(godot_object); + if (caller == nullptr) + { + GDCEF_ERROR("Caller object is not a Node"); + return; + } + + // Find the browser in the caller's children + browser->executeJavaScript(js_code.c_str()); +} + //------------------------------------------------------------------------------ void GDCef::Impl::OnAfterCreated(CefRefPtr /*browser*/) { diff --git a/addons/gdcef/gdcef/src/gdcef.hpp b/addons/gdcef/gdcef/src/gdcef.hpp index 30337d6..6e3cf6a 100644 --- a/addons/gdcef/gdcef/src/gdcef.hpp +++ b/addons/gdcef/gdcef/src/gdcef.hpp @@ -108,6 +108,13 @@ class GDCef: public godot::Node // ------------------------------------------------------------------------- bool isAlive(); + // ------------------------------------------------------------------------- + //! \brief Register a Godot method in the JavaScript context + // ------------------------------------------------------------------------- + void registerGodotMethod(godot::Object* godot_object, + GDBrowserView* browser, + const godot::String& method_name); + // ------------------------------------------------------------------------- //! \brief Return the latest error. // ------------------------------------------------------------------------- @@ -273,14 +280,14 @@ class GDCef: public godot::Node //! \param[in] texture_rect the texture container in where to paint the CEF //! output. \param[in] config dictionary of Browser config with default //! values: - //! - {"frame_rate", 30} - //! - {"javascript", STATE_ENABLED} - //! - {"javascript_close_windows", STATE_DISABLED} - //! - {"javascript_access_clipboard", STATE_DISABLED} - //! - {"javascript_dom_paste", STATE_DISABLED} - //! - {"image_loading", STATE_ENABLED} - //! - {"databases", STATE_ENABLED} - //! - {"webgl", STATE_ENABLED} + //! - {"frame_rate": 30} + //! - {"javascript": STATE_ENABLED} + //! - {"javascript_close_windows": STATE_DISABLED} + //! - {"javascript_access_clipboard": STATE_DISABLED} + //! - {"javascript_dom_paste": STATE_DISABLED} + //! - {"image_loading": STATE_ENABLED} + //! - {"databases": STATE_ENABLED} + //! - {"webgl": STATE_ENABLED} //! Wherer STATE_DISABLED / STATE_ENABLED == false / true //! \return the address of the newly created browser (or nullptr in case of //! error). diff --git a/addons/gdcef/gdcef/src/godot_js_binder.cpp b/addons/gdcef/gdcef/src/godot_js_binder.cpp new file mode 100644 index 0000000..5f7900e --- /dev/null +++ b/addons/gdcef/gdcef/src/godot_js_binder.cpp @@ -0,0 +1,174 @@ +//***************************************************************************** +// MIT License +// +// Copyright (c) 2022 Alain Duron +// Copyright (c) 2022 Quentin Quadrat +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +//***************************************************************************** + +#include "godot_js_binder.hpp" + +static CefRefPtr GodotToV8(const godot::Variant& godot_value); +static godot::Variant V8ToGodot(CefRefPtr v8_value); + +//------------------------------------------------------------------------------ +bool GodotMethodInvoker::Execute(const CefString& name, + CefRefPtr object, + const CefV8ValueList& arguments, + CefRefPtr& retval, + CefString& exception) +{ + // Convert arguments JavaScript to Godot arguments + godot::Array godot_args; + for (const auto& arg : arguments) + { + godot_args.append(V8ToGodot(arg)); + } + + // Call the Godot method + godot::Variant result = m_godot_object->call(m_method_name, godot_args); + + // Convert Godot result to V8 value + retval = GodotToV8(result); + return true; +} + +//------------------------------------------------------------------------------ +static CefRefPtr GodotToV8(const godot::Variant& godot_value) +{ + switch (godot_value.get_type()) + { + case godot::Variant::BOOL: + return CefV8Value::CreateBool(godot_value.operator bool()); + + case godot::Variant::INT: + return CefV8Value::CreateInt(godot_value.operator int64_t()); + + case godot::Variant::FLOAT: + return CefV8Value::CreateDouble(godot_value.operator double()); + + case godot::Variant::STRING: + return CefV8Value::CreateString(CefString( + godot_value.operator godot::String().utf8().get_data())); + + case godot::Variant::ARRAY: { + godot::Array godot_array = godot_value.operator godot::Array(); + CefRefPtr js_array = + CefV8Value::CreateArray(godot_array.size()); + + for (int i = 0; i < godot_array.size(); ++i) + { + js_array->SetValue(i, GodotToV8(godot_array[i])); + } + return js_array; + } + + case godot::Variant::DICTIONARY: { + godot::Dictionary godot_dict = + godot_value.operator godot::Dictionary(); + CefRefPtr js_object = + CefV8Value::CreateObject(nullptr, nullptr); + + for (int i = 0; i < godot_dict.size(); ++i) + { + godot::Variant key = godot_dict.keys()[i]; + godot::Variant value = godot_dict.values()[i]; + + js_object->SetValue( + key.operator godot::String().utf8().get_data(), + GodotToV8(value), + V8_PROPERTY_ATTRIBUTE_NONE); + } + return js_object; + } + + default: + return CefV8Value::CreateNull(); + } +} + +//------------------------------------------------------------------------------ +static godot::Variant V8ToGodot(CefRefPtr v8_value) +{ + if (!v8_value.get()) + { + return godot::Variant(); + } + + if (v8_value->IsNull() || v8_value->IsUndefined()) + { + return godot::Variant(); + } + + if (v8_value->IsBool()) + { + return godot::Variant(v8_value->GetBoolValue()); + } + + if (v8_value->IsInt()) + { + return godot::Variant(v8_value->GetIntValue()); + } + + if (v8_value->IsDouble()) + { + return godot::Variant(v8_value->GetDoubleValue()); + } + + if (v8_value->IsString()) + { + return godot::Variant( + godot::String(v8_value->GetStringValue().ToString().c_str())); + } + + if (v8_value->IsArray()) + { + godot::Array godot_array; + int length = v8_value->GetArrayLength(); + + for (int i = 0; i < length; ++i) + { + godot_array.append(V8ToGodot(v8_value->GetValue(i))); + } + + return godot_array; + } + + if (v8_value->IsObject()) + { + godot::Dictionary godot_dict; + std::vector keys; + v8_value->GetKeys(keys); + + for (const auto& key : keys) + { + CefRefPtr property = v8_value->GetValue(key); + if (property.get()) + { + godot_dict[godot::String(key.ToString().c_str())] = + V8ToGodot(property); + } + } + + return godot_dict; + } + + return godot::Variant(); +} \ No newline at end of file diff --git a/addons/gdcef/gdcef/src/godot_js_binder.hpp b/addons/gdcef/gdcef/src/godot_js_binder.hpp new file mode 100644 index 0000000..5e9ada4 --- /dev/null +++ b/addons/gdcef/gdcef/src/godot_js_binder.hpp @@ -0,0 +1,64 @@ +//***************************************************************************** +// MIT License +// +// Copyright (c) 2022 Alain Duron +// Copyright (c) 2022 Quentin Quadrat +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +//***************************************************************************** + +#ifndef GDCEF_GODOT_JS_BINDER_HPP +#define GDCEF_GODOT_JS_BINDER_HPP + +// Godot 4 +#include +#include +#include + +// Chromium Embedded Framework +#include "cef_v8.h" + +// **************************************************************************** +//! \class GodotMethodInvoker +//! \brief Class to handle binding between JavaScript and GDScript methods +// **************************************************************************** +class GodotMethodInvoker: public CefV8Handler +{ +public: + + GodotMethodInvoker(godot::Object* obj, const godot::StringName& method) + : m_godot_object(obj), m_method_name(method) + { + } + + bool Execute(const CefString& name, + CefRefPtr object, + const CefV8ValueList& arguments, + CefRefPtr& retval, + CefString& exception) override; + + IMPLEMENT_REFCOUNTING(GodotMethodInvoker); + +private: + + godot::Object* m_godot_object; + godot::StringName m_method_name; +}; + +#endif // GDCEF_GODOT_JS_BINDER_HPP \ No newline at end of file diff --git a/addons/gdcef/gdcef/src/register_types.cpp b/addons/gdcef/gdcef/src/register_types.cpp index 0cebe34..00f4d91 100644 --- a/addons/gdcef/gdcef/src/register_types.cpp +++ b/addons/gdcef/gdcef/src/register_types.cpp @@ -27,6 +27,7 @@ #include "gdbrowser.hpp" #include "gdcef.hpp" +#include "godot_js_binder.hpp" #include "helper_log.hpp" #include #include diff --git a/addons/gdcef/render_process/src/render_process.cpp b/addons/gdcef/render_process/src/render_process.cpp index 4792d31..54a9c3a 100644 --- a/addons/gdcef/render_process/src/render_process.cpp +++ b/addons/gdcef/render_process/src/render_process.cpp @@ -25,6 +25,7 @@ #include "render_process.hpp" +//------------------------------------------------------------------------------ #define DEBUG_RENDER_PROCESS(txt) \ { \ std::stringstream ss; \ @@ -33,6 +34,7 @@ std::cout << ss.str() << std::endl; \ } +//------------------------------------------------------------------------------ #define DEBUG_BROWSER_PROCESS(txt) \ { \ std::stringstream ss; \ @@ -41,90 +43,102 @@ std::cout << ss.str() << std::endl; \ } -//------------------------------------------------------------------------------ -RenderProcess::~RenderProcess() -{ - DEBUG_RENDER_PROCESS(""); -} +#ifndef CALL_GODOT_METHOD +# define CALL_GODOT_METHOD "callGodotMethod" +#endif -#if 0 //------------------------------------------------------------------------------ -void RenderProcess::OnContextInitialized() +bool GodotMethodHandler::Execute(const CefString& name, + CefRefPtr object, + const CefV8ValueList& arguments, + CefRefPtr& retval, + CefString& exception) { - CEF_REQUIRE_UI_THREAD(); - DEBUG_RENDER_PROCESS(""); + // Function does not exist. + if (name != CALL_GODOT_METHOD) + { + return false; + } + + // No browser created, we cannot call the method. + if (m_browser == nullptr) + { + exception = "Browser pointer at NULL"; + return true; + } - // Information used when creating the native window. - CefWindowInfo window_info; + // Check that there is at least the method name as argument. + if (arguments.size() < 1 || !arguments[0]->IsString()) + { + exception = "First argument must be the method name"; + return false; + } + + // Create and configure the IPC message to the main process. + CefRefPtr msg = + CefProcessMessage::Create(CALL_GODOT_METHOD); + CefRefPtr args = msg->GetArgumentList(); -# if defined(OS_WIN) - // On Windows we need to specify certain flags that will be passed to - // CreateWindowEx(). - window_info.SetAsPopup(NULL, "CEF"); -# endif + // Add the method name as first argument. + args->SetString(0, arguments[0]->GetStringValue()); - // GDCefBrowser implements browser-level callbacks. - DEBUG_RENDER_PROCESS("Create client handler"); - CefRefPtr handler(new GDCefBrowser()); + // Add the arguments directly from V8 types. + for (size_t i = 1; i < arguments.size(); ++i) + { + auto arg = arguments[i]; + if (arg->IsBool()) + { + args->SetBool(i, arg->GetBoolValue()); + } + else if (arg->IsInt()) + { + args->SetInt(i, arg->GetIntValue()); + } + else if (arg->IsDouble()) + { + args->SetDouble(i, arg->GetDoubleValue()); + } + else if (arg->IsString()) + { + args->SetString(i, arg->GetStringValue()); + } + else + { + // For other types, convert them to string + args->SetString(i, arg->GetStringValue()); + } + } - // Specify CEF browser settings here. - CefBrowserSettings browser_settings; + // Send the message to the main process + m_browser->GetMainFrame()->SendProcessMessage(PID_BROWSER, msg); + retval = CefV8Value::CreateBool(true); - // Create the first browser window. - DEBUG_RENDER_PROCESS("Create the browser"); - CefBrowserHost::CreateBrowser( - window_info, handler.get(), "", browser_settings, nullptr, nullptr); + return true; } -#endif //------------------------------------------------------------------------------ +// TODO Faire OnContextReleased ? void RenderProcess::OnContextCreated(CefRefPtr browser, CefRefPtr frame, CefRefPtr context) { - DEBUG_RENDER_PROCESS(""); -} + DEBUG_RENDER_PROCESS(browser->GetIdentifier()); -#if 0 -//------------------------------------------------------------------------------ -GDCefBrowser::~GDCefBrowser() -{ - DEBUG_BROWSER_PROCESS(""); -} - -//------------------------------------------------------------------------------ -void GDCefBrowser::OnAfterCreated(CefRefPtr browser) -{ - CEF_REQUIRE_UI_THREAD(); - DEBUG_BROWSER_PROCESS(""); + // No handler yet, we need to create it first + m_handler = new GodotMethodHandler(browser); - // Add to the list of existing browsers. - m_browser_list.push_back(browser); -} + // Create global JavaScript objects and bind methods + CefRefPtr global = context->GetGlobal(); -//------------------------------------------------------------------------------ -void GDCefBrowser::OnBeforeClose(CefRefPtr browser) -{ - CEF_REQUIRE_UI_THREAD(); - DEBUG_BROWSER_PROCESS(""); + // Create a global Godot bridge object + CefRefPtr godotBridge = + CefV8Value::CreateObject(nullptr, nullptr); - // Remove from the list of existing browsers. - BrowserList::iterator bit = m_browser_list.begin(); - for (; bit != m_browser_list.end(); ++bit) - { - if ((*bit)->IsSame(browser)) - { - m_browser_list.erase(bit); - break; - } - } + // Bind methods from Godot to JavaScript + godotBridge->SetValue( + CALL_GODOT_METHOD, + CefV8Value::CreateFunction(CALL_GODOT_METHOD, m_handler), + V8_PROPERTY_ATTRIBUTE_NONE); - if (m_browser_list.empty()) - { - DEBUG_BROWSER_PROCESS("CefQuitMessageLoop"); - // All browser windows have closed. - // Quit the application message loop. - CefQuitMessageLoop(); - } -} -#endif \ No newline at end of file + global->SetValue("godot", godotBridge, V8_PROPERTY_ATTRIBUTE_NONE); +} \ No newline at end of file diff --git a/addons/gdcef/render_process/src/render_process.hpp b/addons/gdcef/render_process/src/render_process.hpp index 423c72b..46c9653 100644 --- a/addons/gdcef/render_process/src/render_process.hpp +++ b/addons/gdcef/render_process/src/render_process.hpp @@ -63,94 +63,56 @@ #endif // ***************************************************************************** -//! \brief Entry point for the render process +//! \brief JavaScript Method Handler // ***************************************************************************** -class RenderProcess: public CefApp, - // public CefBrowserProcessHandler, - public CefRenderProcessHandler +class GodotMethodHandler: public CefV8Handler { public: - ~RenderProcess(); + GodotMethodHandler(CefRefPtr browser) : m_browser(browser) {} -private: // CefApp methods + bool Execute(const CefString& name, + CefRefPtr object, + const CefV8ValueList& arguments, + CefRefPtr& retval, + CefString& exception) override; - // ------------------------------------------------------------------------- - // virtual CefRefPtr - // GetBrowserProcessHandler() override - // { - // return this; - //} - - virtual CefRefPtr - GetRenderProcessHandler() override - { - return this; - } - -private: // CefBrowserProcessHandler methods - - // ------------------------------------------------------------------------- - // virtual void OnContextInitialized() override; - -private: // CefRenderProcessHandler methods - - // ------------------------------------------------------------------------- - virtual void OnContextCreated(CefRefPtr browser, - CefRefPtr frame, - CefRefPtr context) override; + IMPLEMENT_REFCOUNTING(GodotMethodHandler); private: - IMPLEMENT_REFCOUNTING(RenderProcess); + CefRefPtr m_browser; }; -#if 0 // ***************************************************************************** -//! \brief Browser process handler +//! \brief Entry point for the render process // ***************************************************************************** -class GDCefBrowser: public CefClient, - public CefLifeSpanHandler, - public CefDisplayHandler +class RenderProcess: public CefApp, public CefRenderProcessHandler { public: - ~GDCefBrowser(); - -private: // CefDisplayHandler methods - - // ------------------------------------------------------------------------- - virtual CefRefPtr GetDisplayHandler() override - { - return this; - } + IMPLEMENT_REFCOUNTING(RenderProcess); -private: // CefLifeSpanHandler methods +private: // CefApp methods // ------------------------------------------------------------------------- - virtual CefRefPtr GetLifeSpanHandler() override + virtual CefRefPtr + GetRenderProcessHandler() override { return this; } - // ------------------------------------------------------------------------- - virtual void OnAfterCreated(CefRefPtr browser) override; - - // ------------------------------------------------------------------------- - virtual void OnBeforeClose(CefRefPtr browser) override; - -private: +private: // CefRenderProcessHandler methods // ------------------------------------------------------------------------- - // Include the default reference counting implementation. - IMPLEMENT_REFCOUNTING(GDCefBrowser); + virtual void OnContextCreated(CefRefPtr browser, + CefRefPtr frame, + CefRefPtr context) override; private: - using BrowserList = std::list>; - BrowserList m_browser_list; + CefRefPtr m_handler; }; -#endif #if !defined(_WIN32) # if defined(__clang__)