diff --git a/addons/gdcef/demos/HelloCEF/Control.gd b/addons/gdcef/demos/HelloCEF/Control.gd index 3db9081..a36efa9 100644 --- a/addons/gdcef/demos/HelloCEF/Control.gd +++ b/addons/gdcef/demos/HelloCEF/Control.gd @@ -181,16 +181,16 @@ func _ready(): # Left browser is displaying the first webpage with a 3D scene, we are # enabling webgl. Other default configuration are: - # {"frame_rate", 30} - # {"javascript", true} - # {"javascript_close_windows", false} - # {"javascript_access_clipboard", false} - # {"javascript_dom_paste", false} - # {"image_loading", true} - # {"databases", true} - # {"webgl", true} - var left = $CEF.create_browser(pages[4], $TextRectLeft, {}) - var right = $CEF.create_browser(pages[0], $TextRectRight, {}) + # {"frame_rate": 30} + # {"javascript": true} + # {"javascript_close_windows": false} + # {"javascript_access_clipboard": false} + # {"javascript_dom_paste": false} + # {"image_loading": true} + # {"databases": true} + # {"webgl": true} + var left = $CEF.create_browser(pages[4], $TextRectLeft, {"javascript": true, "webgl": true}) + var right = $CEF.create_browser(pages[0], $TextRectRight, {"javascript": true, "webgl": true}) left.name = "left" right.name = "right" diff --git a/addons/gdcef/demos/JS/Control.gd b/addons/gdcef/demos/JS/Control.gd new file mode 100644 index 0000000..96455d3 --- /dev/null +++ b/addons/gdcef/demos/JS/Control.gd @@ -0,0 +1,212 @@ +# ============================================================================== +# 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() + 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. +# TODO on page_unload ? +# ============================================================================== +func _on_page_loaded(browser): + print("The browser " + browser.name + " has loaded " + browser.get_url()) + browser.register_method(Callable(self, "change_weapon")) + browser.register_method(Callable(self, "set_character_name")) + browser.register_method(Callable(self, "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..a6b2618 --- /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 0000000..1acbade Binary files /dev/null and b/addons/gdcef/demos/JS/icon.png differ 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/JS/qq.html b/addons/gdcef/demos/JS/qq.html new file mode 100644 index 0000000..97f951b --- /dev/null +++ b/addons/gdcef/demos/JS/qq.html @@ -0,0 +1,19 @@ + + + + + + Character Management UI + + + + + + + + + \ No newline at end of file diff --git a/addons/gdcef/demos/README.md b/addons/gdcef/demos/README.md index 5e54933..878ca2a 100644 --- a/addons/gdcef/demos/README.md +++ b/addons/gdcef/demos/README.md @@ -33,3 +33,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..76687ac 100644 --- a/addons/gdcef/gdcef/src/gdbrowser.cpp +++ b/addons/gdcef/gdcef/src/gdbrowser.cpp @@ -23,8 +23,8 @@ // SOFTWARE. //***************************************************************************** -//------------------------------------------------------------------------------ #include "gdbrowser.hpp" +#include "godot_js_binder.hpp" #include "helper_config.hpp" #include "helper_files.hpp" @@ -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 @@ -149,6 +153,8 @@ void GDBrowserView::_bind_methods() &GDBrowserView::getAudioStreamer); ClassDB::bind_method(D_METHOD("get_pixel_color", "x", "y"), &GDBrowserView::getPixelColor); + ClassDB::bind_method(D_METHOD("register_method"), + &GDBrowserView::registerGodotMethod); // Signals ADD_SIGNAL(MethodInfo("on_download_updated", @@ -886,4 +892,84 @@ void GDBrowserView::onDownloadUpdated( // Emit signal for Godot script emit_signal( "on_download_updated", godot::String(file.c_str()), percentage, this); +} + +//------------------------------------------------------------------------------ +void GDBrowserView::registerGodotMethod(const godot::Callable& callable) +{ + godot::String method_name = callable.get_method(); + + BROWSER_DEBUG("Registering gdscript method " + << method_name.utf8().get_data()); + if (!callable.is_valid()) + { + BROWSER_ERROR("Invalid callable provided"); + return; + } + + std::string key = method_name.utf8().get_data(); + m_js_bindings[key] = callable; +} + +//------------------------------------------------------------------------------ +bool GDBrowserView::onProcessMessageReceived( + CefRefPtr browser, + CefRefPtr frame, + CefProcessId source_process, + CefRefPtr message) +{ + BROWSER_DEBUG("Received message " << message->GetName().ToString()); + if (message->GetName() != CALL_GODOT_METHOD) + { + BROWSER_DEBUG("Not method " << CALL_GODOT_METHOD); + return false; + } + + if (message->GetArgumentList()->GetSize() < 1) + { + BROWSER_ERROR("Expected method name as first argument"); + return false; + } + + // Create the callable key + std::string key = message->GetArgumentList()->GetString(0).ToString(); + + // Does not exist ? + auto callable = m_js_bindings[key]; + if (!callable.is_valid()) + { + BROWSER_ERROR("Callable not found for method " << key); + return false; + } + + // Convert the message arguments to a Godot Array + godot::Array args; + auto message_args = message->GetArgumentList(); + for (size_t i = 1; i < message_args->GetSize(); ++i) + { + switch (message_args->GetType(i)) + { + case VTYPE_BOOL: + args.push_back(message_args->GetBool(i)); + break; + case VTYPE_INT: + args.push_back(message_args->GetInt(i)); + break; + case VTYPE_DOUBLE: + args.push_back(message_args->GetDouble(i)); + break; + case VTYPE_STRING: + args.push_back(godot::String( + message_args->GetString(i).ToString().c_str())); + break; + default: + // For unsupported types, pass as string + args.push_back(godot::String( + message_args->GetString(i).ToString().c_str())); + } + } + + // Call the function + callable.callv(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 4cb427e..8f66ec2 100644 --- a/addons/gdcef/gdcef/src/gdbrowser.hpp +++ b/addons/gdcef/gdcef/src/gdbrowser.hpp @@ -189,6 +189,19 @@ class GDBrowserView: public godot::Node return this; } + // --------------------------------------------------------------------- + //! \brief Called when a message is received from a different process. + // --------------------------------------------------------------------- + virtual bool + OnProcessMessageReceived(CefRefPtr browser, + CefRefPtr frame, + CefProcessId source_process, + CefRefPtr message) override + { + return m_owner.onProcessMessageReceived( + browser, frame, source_process, message); + } + private: // CefRenderHandler interfaces // --------------------------------------------------------------------- @@ -285,7 +298,7 @@ class GDBrowserView: public godot::Node { } - private: // CefBrowserProcessHandler interfaces + private: // CefLifeSpanHandler interfaces virtual bool OnBeforePopup(CefRefPtr browser, CefRefPtr frame, @@ -668,6 +681,11 @@ class GDBrowserView: public godot::Node // ------------------------------------------------------------------------- godot::Color getPixelColor(int x, int y) const; + // ------------------------------------------------------------------------- + //! \brief Register a Godot method in the JavaScript context + // ------------------------------------------------------------------------- + void registerGodotMethod(const godot::Callable& callable); + private: void resize_(int width, int height); @@ -769,6 +787,14 @@ class GDBrowserView: public godot::Node CefRefPtr download_item, CefRefPtr callback); + // ------------------------------------------------------------------------- + //! \brief Called when a message is received from a different process. + // ------------------------------------------------------------------------- + bool onProcessMessageReceived(CefRefPtr browser, + CefRefPtr frame, + CefProcessId source_process, + CefRefPtr message); + private: //! \brief CEF interface implementation @@ -818,6 +844,9 @@ class GDBrowserView: public godot::Node //! \brief Download folder (configured from Browser config) fs::path m_download_folder; + + //! \brief + std::unordered_map m_js_bindings; }; #if !defined(_WIN32) diff --git a/addons/gdcef/gdcef/src/gdcef.cpp b/addons/gdcef/gdcef/src/gdcef.cpp index 788693b..572cad3 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,7 +123,7 @@ 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); diff --git a/addons/gdcef/gdcef/src/gdcef.hpp b/addons/gdcef/gdcef/src/gdcef.hpp index 30337d6..a6ec768 100644 --- a/addons/gdcef/gdcef/src/gdcef.hpp +++ b/addons/gdcef/gdcef/src/gdcef.hpp @@ -273,14 +273,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..ed8f9ce --- /dev/null +++ b/addons/gdcef/gdcef/src/godot_js_binder.cpp @@ -0,0 +1,171 @@ +//***************************************************************************** +// 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" + +//------------------------------------------------------------------------------ +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; +} + +//------------------------------------------------------------------------------ +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(); + } +} + +//------------------------------------------------------------------------------ +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..419753b --- /dev/null +++ b/addons/gdcef/gdcef/src/godot_js_binder.hpp @@ -0,0 +1,74 @@ +//***************************************************************************** +// 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" + +// ----------------------------------------------------------------------------- +//! \brief Convert a Godot variant to a V8 value +// ----------------------------------------------------------------------------- +CefRefPtr GodotToV8(const godot::Variant& godot_value); + +// ----------------------------------------------------------------------------- +//! \brief Convert a V8 value to a Godot variant +// ----------------------------------------------------------------------------- +godot::Variant V8ToGodot(CefRefPtr v8_value); + +// **************************************************************************** +//! \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..ff3f0bf 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,127 @@ std::cout << ss.str() << std::endl; \ } -//------------------------------------------------------------------------------ -RenderProcess::~RenderProcess() -{ - DEBUG_RENDER_PROCESS(""); -} - -#if 0 -//------------------------------------------------------------------------------ -void RenderProcess::OnContextInitialized() -{ - CEF_REQUIRE_UI_THREAD(); - DEBUG_RENDER_PROCESS(""); - - // Information used when creating the native window. - CefWindowInfo window_info; - -# if defined(OS_WIN) - // On Windows we need to specify certain flags that will be passed to - // CreateWindowEx(). - window_info.SetAsPopup(NULL, "CEF"); -# endif - - // GDCefBrowser implements browser-level callbacks. - DEBUG_RENDER_PROCESS("Create client handler"); - CefRefPtr handler(new GDCefBrowser()); - - // Specify CEF browser settings here. - CefBrowserSettings browser_settings; - - // Create the first browser window. - DEBUG_RENDER_PROCESS("Create the browser"); - CefBrowserHost::CreateBrowser( - window_info, handler.get(), "", browser_settings, nullptr, nullptr); -} +#ifndef CALL_GODOT_METHOD +# define CALL_GODOT_METHOD "callGodotMethod" #endif //------------------------------------------------------------------------------ -void RenderProcess::OnContextCreated(CefRefPtr browser, - CefRefPtr frame, - CefRefPtr context) +bool GodotMethodHandler::Execute(const CefString& name, + CefRefPtr object, + const CefV8ValueList& arguments, + CefRefPtr& retval, + CefString& exception) { - DEBUG_RENDER_PROCESS(""); -} + DEBUG_RENDER_PROCESS(name.ToString()); -#if 0 -//------------------------------------------------------------------------------ -GDCefBrowser::~GDCefBrowser() -{ - DEBUG_BROWSER_PROCESS(""); -} + // Function does not exist. + if (name != CALL_GODOT_METHOD) + { + exception = "Function does not exist"; + DEBUG_RENDER_PROCESS(exception.ToString()); + return false; + } -//------------------------------------------------------------------------------ -void GDCefBrowser::OnAfterCreated(CefRefPtr browser) -{ - CEF_REQUIRE_UI_THREAD(); - DEBUG_BROWSER_PROCESS(""); + // No browser created, we cannot call the method. + if (m_browser == nullptr) + { + exception = "Browser pointer at NULL"; + DEBUG_RENDER_PROCESS(exception.ToString()); + return true; + } - // Add to the list of existing browsers. - m_browser_list.push_back(browser); -} + // 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"; + DEBUG_RENDER_PROCESS(exception.ToString()); + return false; + } -//------------------------------------------------------------------------------ -void GDCefBrowser::OnBeforeClose(CefRefPtr browser) -{ - CEF_REQUIRE_UI_THREAD(); - DEBUG_BROWSER_PROCESS(""); + // Create and configure the IPC message to the main process. + CefRefPtr msg = + CefProcessMessage::Create(CALL_GODOT_METHOD); + CefRefPtr args = msg->GetArgumentList(); - // Remove from the list of existing browsers. - BrowserList::iterator bit = m_browser_list.begin(); - for (; bit != m_browser_list.end(); ++bit) + // Add the method name as first argument. + args->SetString(0, arguments[0]->GetStringValue()); + + // Add the arguments directly from V8 types. + for (size_t i = 1; i < arguments.size(); ++i) { - if ((*bit)->IsSame(browser)) + auto arg = arguments[i]; + if (arg->IsBool()) + { + args->SetBool(i, arg->GetBoolValue()); + } + else if (arg->IsInt()) { - m_browser_list.erase(bit); - break; + 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()); } } - if (m_browser_list.empty()) - { - DEBUG_BROWSER_PROCESS("CefQuitMessageLoop"); - // All browser windows have closed. - // Quit the application message loop. - CefQuitMessageLoop(); - } + // Send the message to the main process + m_browser->GetMainFrame()->SendProcessMessage(PID_BROWSER, msg); + retval = CefV8Value::CreateBool(true); + + return true; } -#endif \ No newline at end of file + +//------------------------------------------------------------------------------ +// TODO Faire OnContextReleased ? +void RenderProcess::OnContextCreated(CefRefPtr browser, + CefRefPtr frame, + CefRefPtr context) +{ + DEBUG_RENDER_PROCESS(browser->GetIdentifier()); + + // No handler yet, we need to create it first + m_handler = new GodotMethodHandler(browser); + + // Create global JavaScript objects and bind methods + CefRefPtr global = context->GetGlobal(); + + // Create a global Godot bridge object + CefRefPtr godotBridge = + CefV8Value::CreateObject(nullptr, nullptr); + + // Bind only the base callGodotMethod + godotBridge->SetValue( + CALL_GODOT_METHOD, + CefV8Value::CreateFunction(CALL_GODOT_METHOD, m_handler), + V8_PROPERTY_ATTRIBUTE_NONE); + + // Define the godot object + global->SetValue("godot", godotBridge, V8_PROPERTY_ATTRIBUTE_NONE); + + // Create the JavaScript Proxy that intercepts method calls + const char* proxySetup = R"( + const rawGodot = godot; + window.godot = new Proxy({}, { + get: function(target, prop) { + if (prop === 'callGodotMethod') { + return rawGodot.callGodotMethod; + } + return function(...args) { + return rawGodot.callGodotMethod(prop, ...args); + }; + } + }); + )"; + + // Install the Proxy + frame->ExecuteJavaScript(proxySetup, frame->GetURL(), 0); +} \ 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__)