bunite-core 0.6.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/package.json +5 -2
  2. package/src/bun/core/App.ts +35 -34
  3. package/src/bun/core/BrowserView.ts +3 -9
  4. package/src/bun/core/singleInstanceLock.ts +91 -0
  5. package/src/bun/index.ts +5 -6
  6. package/src/bun/preload/inline.ts +1 -2
  7. package/src/bun/proc/native.ts +54 -78
  8. package/src/native/linux/bunite_linux_appres.cpp +173 -0
  9. package/src/native/linux/bunite_linux_ffi.cpp +263 -0
  10. package/src/native/linux/bunite_linux_internal.h +148 -0
  11. package/src/native/linux/bunite_linux_preload.cpp +1 -0
  12. package/src/native/linux/bunite_linux_runtime.cpp +120 -0
  13. package/src/native/linux/bunite_linux_utils.cpp +114 -0
  14. package/src/native/linux/bunite_linux_view.cpp +244 -0
  15. package/src/native/linux/bunite_linux_window.cpp +101 -0
  16. package/src/native/mac/bunite_mac_appres.mm +163 -0
  17. package/src/native/mac/bunite_mac_ffi.mm +470 -0
  18. package/src/native/mac/bunite_mac_internal.h +151 -0
  19. package/src/native/mac/bunite_mac_runtime.mm +15 -0
  20. package/src/native/mac/bunite_mac_utils.mm +121 -0
  21. package/src/native/mac/bunite_mac_view.mm +279 -0
  22. package/src/native/mac/bunite_mac_window.mm +187 -0
  23. package/src/native/shared/ffi_exports.h +13 -13
  24. package/src/native/shared/permissions.h +14 -0
  25. package/src/native/shared/webview_storage.h +6 -3
  26. package/src/native/win/native_host_cef.cpp +4 -8
  27. package/src/native/win/native_host_ffi.cpp +76 -123
  28. package/src/native/win/native_host_internal.h +5 -3
  29. package/src/native/win/native_host_runtime.cpp +2 -6
  30. package/src/native/win/native_host_utils.cpp +23 -52
  31. package/src/native/win/process_helper_win.cpp +1 -3
  32. package/src/preload/runtime.ts +1 -3
  33. package/src/preload/tsconfig.tsbuildinfo +1 -1
  34. package/src/preload/webviewElement.ts +3 -8
  35. package/src/shared/paths.ts +35 -44
  36. package/src/shared/platform.ts +1 -2
  37. package/src/shared/rpcDemux.ts +47 -2
  38. package/src/shared/webviewPolyfill.ts +64 -6
  39. package/src/bun/core/Utils.ts +0 -301
@@ -0,0 +1,120 @@
1
+ #include "bunite_linux_internal.h"
2
+
3
+ #include <cstdlib>
4
+ #include <string>
5
+ #include <vector>
6
+
7
+ namespace bunite_linux {
8
+
9
+ RuntimeState g_runtime;
10
+
11
+ bool isOnMainThread() {
12
+ return g_runtime.ui_thread_set && pthread_equal(pthread_self(), g_runtime.ui_thread);
13
+ }
14
+
15
+ } // namespace bunite_linux
16
+
17
+ namespace {
18
+
19
+ constexpr int32_t kBuniteAbiVersion = 4;
20
+
21
+ } // namespace
22
+
23
+ extern "C" BUNITE_EXPORT int32_t bunite_abi_version(void) { return kBuniteAbiVersion; }
24
+ extern "C" BUNITE_EXPORT void bunite_set_log_level(int32_t level) { (void)level; }
25
+
26
+ extern "C" BUNITE_EXPORT const char* bunite_engine_name(void) { return "webkitgtk"; }
27
+
28
+ extern "C" BUNITE_EXPORT const char* bunite_engine_version(void) {
29
+ static std::string cached =
30
+ std::to_string(webkit_get_major_version()) + "." +
31
+ std::to_string(webkit_get_minor_version()) + "." +
32
+ std::to_string(webkit_get_micro_version());
33
+ return cached.c_str();
34
+ }
35
+
36
+ extern "C" BUNITE_EXPORT bool bunite_init(
37
+ const char* engine_dir, bool hide_console, bool popup_blocking, const char* engine_config_json
38
+ ) {
39
+ (void)engine_dir; (void)hide_console; (void)engine_config_json;
40
+ auto& rt = bunite_linux::g_runtime;
41
+ if (rt.initialized) return true;
42
+ rt.popup_blocking = popup_blocking;
43
+ rt.ui_thread = pthread_self();
44
+ rt.ui_thread_set = true;
45
+ rt.ui_context = g_main_context_default();
46
+
47
+ gtk_init();
48
+
49
+ GtkApplication* app = gtk_application_new("dev.bunite.app", G_APPLICATION_NON_UNIQUE);
50
+ GError* err = nullptr;
51
+ const gboolean registered = g_application_register(G_APPLICATION(app), nullptr, &err);
52
+ if (!registered) {
53
+ BUNITE_ERROR("bunite_init: g_application_register failed: %s", err ? err->message : "(unknown)");
54
+ if (err) g_error_free(err);
55
+ g_object_unref(app);
56
+ rt.ui_context = nullptr;
57
+ rt.ui_thread_set = false;
58
+ return false;
59
+ }
60
+
61
+ rt.app = app;
62
+ rt.initialized = true;
63
+ return true;
64
+ }
65
+
66
+ // JS drives the default GLib context from Bun's loop.
67
+ extern "C" BUNITE_EXPORT void bunite_run_loop(void) {}
68
+ extern "C" BUNITE_EXPORT void bunite_pump_once(void) {
69
+ if (!bunite_linux::isOnMainThread()) {
70
+ BUNITE_WARN("bunite_pump_once called off the GTK thread; ignoring.");
71
+ return;
72
+ }
73
+ GMainContext* ctx = bunite_linux::g_runtime.ui_context;
74
+ if (!ctx) return;
75
+
76
+ const gint64 deadline = g_get_monotonic_time() + 5000; // 5ms
77
+ do {
78
+ if (!g_main_context_pending(ctx)) break;
79
+ g_main_context_iteration(ctx, FALSE);
80
+ } while (g_get_monotonic_time() < deadline);
81
+ }
82
+
83
+ extern "C" BUNITE_EXPORT void bunite_quit(void) {
84
+ auto& rt = bunite_linux::g_runtime;
85
+ if (!rt.initialized) return;
86
+ rt.shutting_down.store(true);
87
+
88
+ std::vector<GtkWindow*> windows;
89
+ {
90
+ std::lock_guard<std::mutex> lock(rt.object_mutex);
91
+ for (auto& [_, state] : rt.windows) {
92
+ if (state.window) windows.push_back(state.window);
93
+ }
94
+ }
95
+ for (GtkWindow* window : windows) {
96
+ gtk_window_destroy(window);
97
+ }
98
+
99
+ if (rt.app) {
100
+ g_application_quit(G_APPLICATION(rt.app));
101
+ g_object_unref(rt.app);
102
+ rt.app = nullptr;
103
+ }
104
+ rt.ui_context = nullptr;
105
+ rt.ui_thread_set = false;
106
+ rt.initialized = false;
107
+ rt.shutting_down.store(false);
108
+ }
109
+
110
+ extern "C" BUNITE_EXPORT void bunite_free_cstring(const char* value) {
111
+ std::free(const_cast<char*>(value));
112
+ }
113
+
114
+ extern "C" BUNITE_EXPORT void bunite_set_webview_event_handler(BuniteWebviewEventHandler handler) {
115
+ bunite_linux::g_runtime.webview_event_handler = handler;
116
+ }
117
+
118
+ extern "C" BUNITE_EXPORT void bunite_set_window_event_handler(BuniteWindowEventHandler handler) {
119
+ bunite_linux::g_runtime.window_event_handler = handler;
120
+ }
@@ -0,0 +1,114 @@
1
+ #include "bunite_linux_internal.h"
2
+
3
+ #include <json-glib/json-glib.h>
4
+
5
+ #include <cctype>
6
+ #include <cstdio>
7
+ #include <cstring>
8
+
9
+ namespace bunite_linux {
10
+
11
+ void emitWindowEvent(uint32_t window_id, const char* event_name, const std::string& payload) {
12
+ if (BuniteWindowEventHandler h = g_runtime.window_event_handler) {
13
+ h(window_id, strdup(event_name ? event_name : ""), strdup(payload.c_str()));
14
+ }
15
+ }
16
+
17
+ void emitWebviewEvent(uint32_t view_id, const char* event_name, const std::string& payload) {
18
+ if (BuniteWebviewEventHandler h = g_runtime.webview_event_handler) {
19
+ h(view_id, strdup(event_name ? event_name : ""), strdup(payload.c_str()));
20
+ }
21
+ }
22
+
23
+ std::string escapeJsonString(const std::string& value) {
24
+ std::string out;
25
+ out.reserve(value.size() + 8);
26
+ for (unsigned char c : value) {
27
+ switch (c) {
28
+ case '"': out += "\\\""; break;
29
+ case '\\': out += "\\\\"; break;
30
+ case '\b': out += "\\b"; break;
31
+ case '\f': out += "\\f"; break;
32
+ case '\n': out += "\\n"; break;
33
+ case '\r': out += "\\r"; break;
34
+ case '\t': out += "\\t"; break;
35
+ default:
36
+ if (c < 0x20) {
37
+ char buf[8];
38
+ std::snprintf(buf, sizeof(buf), "\\u%04x", c);
39
+ out += buf;
40
+ } else {
41
+ out += static_cast<char>(c);
42
+ }
43
+ }
44
+ }
45
+ return out;
46
+ }
47
+
48
+ bool globMatchCaseInsensitive(const std::string& pattern, const std::string& value) {
49
+ size_t pi = 0, vi = 0;
50
+ size_t star_p = std::string::npos, star_v = 0;
51
+ while (vi < value.size()) {
52
+ if (pi < pattern.size() &&
53
+ std::tolower(static_cast<unsigned char>(pattern[pi])) ==
54
+ std::tolower(static_cast<unsigned char>(value[vi]))) {
55
+ ++pi; ++vi;
56
+ } else if (pi < pattern.size() && pattern[pi] == '*') {
57
+ star_p = pi++; star_v = vi;
58
+ } else if (star_p != std::string::npos) {
59
+ pi = star_p + 1; vi = ++star_v;
60
+ } else {
61
+ return false;
62
+ }
63
+ }
64
+ while (pi < pattern.size() && pattern[pi] == '*') ++pi;
65
+ return pi == pattern.size();
66
+ }
67
+
68
+ std::vector<std::string> parseNavigationRulesJson(const std::string& json) {
69
+ std::vector<std::string> rules;
70
+ if (json.empty()) return rules;
71
+
72
+ JsonParser* parser = json_parser_new();
73
+ if (!json_parser_load_from_data(parser, json.c_str(), (gssize)json.size(), nullptr)) {
74
+ g_object_unref(parser);
75
+ return rules;
76
+ }
77
+ JsonNode* root = json_parser_get_root(parser);
78
+ if (!root || JSON_NODE_TYPE(root) != JSON_NODE_ARRAY) {
79
+ g_object_unref(parser);
80
+ return rules;
81
+ }
82
+ JsonArray* arr = json_node_get_array(root);
83
+ const guint n = json_array_get_length(arr);
84
+ rules.reserve(n);
85
+ for (guint i = 0; i < n; ++i) {
86
+ JsonNode* item = json_array_get_element(arr, i);
87
+ if (!item || json_node_get_value_type(item) != G_TYPE_STRING) continue;
88
+ const gchar* s = json_node_get_string(item);
89
+ if (s && *s) rules.emplace_back(s);
90
+ }
91
+ g_object_unref(parser);
92
+ return rules;
93
+ }
94
+
95
+ bool shouldAlwaysAllowNavigationUrl(const std::string& url) {
96
+ return url == "about:blank" ||
97
+ url.rfind("appres://app.internal/internal/", 0) == 0;
98
+ }
99
+
100
+ bool shouldAllowNavigation(const ViewState* view, const std::string& url) {
101
+ if (!view || shouldAlwaysAllowNavigationUrl(url) || view->navigation_rules.empty()) {
102
+ return true;
103
+ }
104
+ bool allowed = true; // default-allow, last-match-wins
105
+ for (const std::string& raw : view->navigation_rules) {
106
+ const bool block = !raw.empty() && raw.front() == '^';
107
+ const std::string pattern = block ? raw.substr(1) : raw;
108
+ if (pattern.empty()) continue;
109
+ if (globMatchCaseInsensitive(pattern, url)) allowed = !block;
110
+ }
111
+ return allowed;
112
+ }
113
+
114
+ } // namespace bunite_linux
@@ -0,0 +1,244 @@
1
+ #include "bunite_linux_internal.h"
2
+ #include "webview_storage.h"
3
+
4
+ #include <string>
5
+ #include <vector>
6
+
7
+ namespace bunite_linux {
8
+
9
+ namespace {
10
+
11
+ constexpr const char* kViewIdKey = "bunite-view-id";
12
+
13
+ void emit_url(uint32_t view_id, const char* name, WebKitWebView* wv) {
14
+ const char* uri = webkit_web_view_get_uri(wv);
15
+ emitWebviewEvent(view_id, name, uri ? std::string(uri) : std::string{});
16
+ }
17
+
18
+ void on_load_changed(WebKitWebView* wv, WebKitLoadEvent event, gpointer user_data) {
19
+ const uint32_t view_id = GPOINTER_TO_UINT(user_data);
20
+ switch (event) {
21
+ case WEBKIT_LOAD_COMMITTED:
22
+ emit_url(view_id, "did-navigate", wv);
23
+ queueViewRedraw(wv);
24
+ break;
25
+ case WEBKIT_LOAD_FINISHED:
26
+ emit_url(view_id, "dom-ready", wv);
27
+ queueViewRedraw(wv);
28
+ break;
29
+ default: break;
30
+ }
31
+ }
32
+
33
+ GtkWidget* on_create(WebKitWebView* wv, WebKitNavigationAction* action, gpointer user_data) {
34
+ const uint32_t view_id = GPOINTER_TO_UINT(user_data);
35
+ WebKitURIRequest* req = webkit_navigation_action_get_request(action);
36
+ const char* uri = webkit_uri_request_get_uri(req);
37
+ std::string payload = "{\"url\":\"" + escapeJsonString(uri ? uri : "") + "\"}";
38
+ emitWebviewEvent(view_id, "new-window-open", payload);
39
+ (void)wv;
40
+ return nullptr; // cancel; JS opens via load_url if desired
41
+ }
42
+
43
+ // WebKitGTK fires nav-action for sub-frames with no main-frame discriminator — iframes hit nav rules.
44
+ gboolean on_decide_policy(WebKitWebView* wv, WebKitPolicyDecision* decision,
45
+ WebKitPolicyDecisionType type, gpointer user_data) {
46
+ (void)wv;
47
+ if (type != WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION &&
48
+ type != WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION) return FALSE;
49
+ const uint32_t view_id = GPOINTER_TO_UINT(user_data);
50
+ auto* nav = WEBKIT_NAVIGATION_POLICY_DECISION(decision);
51
+ WebKitNavigationAction* action = webkit_navigation_policy_decision_get_navigation_action(nav);
52
+ WebKitURIRequest* request = webkit_navigation_action_get_request(action);
53
+ const char* uri = webkit_uri_request_get_uri(request);
54
+ const std::string url_str = uri ? uri : "";
55
+ const bool allow = shouldAllowNavigation(findView(view_id), url_str);
56
+ emitWebviewEvent(view_id, "will-navigate", url_str);
57
+ if (allow) webkit_policy_decision_use(decision);
58
+ else webkit_policy_decision_ignore(decision);
59
+ return TRUE;
60
+ }
61
+
62
+ } // namespace
63
+
64
+ ViewState* findView(uint32_t view_id) {
65
+ std::lock_guard<std::mutex> lock(g_runtime.object_mutex);
66
+ auto it = g_runtime.views.find(view_id);
67
+ return it == g_runtime.views.end() ? nullptr : &it->second;
68
+ }
69
+
70
+ uint32_t viewIdForWebView(WebKitWebView* wv) {
71
+ if (!wv) return 0;
72
+ return GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(wv), kViewIdKey));
73
+ }
74
+
75
+ bool createView(uint32_t view_id, uint32_t window_id,
76
+ const char* url, const char* html, const char* preload, const char* appres_root,
77
+ const char* navigation_rules_json, const char* preload_origins_json,
78
+ double x, double y, double width, double height, bool auto_resize) {
79
+ auto* window_state = findWindow(window_id);
80
+ if (!window_state) {
81
+ BUNITE_ERROR("bunite_view_create: window %u not found", window_id);
82
+ return false;
83
+ }
84
+ if (findView(view_id)) {
85
+ BUNITE_ERROR("bunite_view_create: view %u already exists", view_id);
86
+ return false;
87
+ }
88
+
89
+ WebKitUserContentManager* ucm = webkit_user_content_manager_new();
90
+
91
+ // Origin-gate the preload so http(s) navigations don't inherit the RPC bridge.
92
+ if (preload && *preload) {
93
+ const char* origins = (preload_origins_json && *preload_origins_json)
94
+ ? preload_origins_json : "[]";
95
+ std::string gated;
96
+ gated.reserve(strlen(preload) + 128);
97
+ gated += "(function(){var _o=";
98
+ gated += origins;
99
+ gated += ";_o.push('appres://app.internal');";
100
+ gated += "if(_o.indexOf(location.origin)<0)return;";
101
+ gated += preload;
102
+ gated += "})();";
103
+ WebKitUserScript* script = webkit_user_script_new(
104
+ gated.c_str(),
105
+ WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
106
+ WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START,
107
+ nullptr, nullptr);
108
+ webkit_user_content_manager_add_script(ucm, script);
109
+ webkit_user_script_unref(script);
110
+ }
111
+
112
+ WebKitWebView* wv = WEBKIT_WEB_VIEW(g_object_new(
113
+ WEBKIT_TYPE_WEB_VIEW,
114
+ "user-content-manager", ucm,
115
+ nullptr));
116
+ g_object_unref(ucm);
117
+
118
+ WebKitSettings* settings = webkit_web_view_get_settings(wv);
119
+ webkit_settings_set_javascript_can_open_windows_automatically(settings, !g_runtime.popup_blocking);
120
+
121
+ g_object_set_data(G_OBJECT(wv), kViewIdKey, GUINT_TO_POINTER(view_id));
122
+ registerAppresScheme(webkit_web_view_get_context(wv));
123
+
124
+ g_signal_connect(wv, "load-changed", G_CALLBACK(on_load_changed), GUINT_TO_POINTER(view_id));
125
+ g_signal_connect(wv, "decide-policy", G_CALLBACK(on_decide_policy), GUINT_TO_POINTER(view_id));
126
+ g_signal_connect(wv, "create", G_CALLBACK(on_create), GUINT_TO_POINTER(view_id));
127
+
128
+ GtkWidget* container = GTK_WIDGET(wv);
129
+ if (auto_resize) {
130
+ gtk_overlay_set_child(window_state->host, GTK_WIDGET(wv));
131
+ } else {
132
+ container = gtk_fixed_new();
133
+ gtk_widget_set_halign(container, GTK_ALIGN_START);
134
+ gtk_widget_set_valign(container, GTK_ALIGN_START);
135
+ gtk_widget_set_overflow(container, GTK_OVERFLOW_HIDDEN);
136
+ gtk_fixed_put(GTK_FIXED(container), GTK_WIDGET(wv), 0, 0);
137
+ gtk_overlay_add_overlay(window_state->host, container);
138
+ }
139
+
140
+ {
141
+ std::lock_guard<std::mutex> lock(g_runtime.object_mutex);
142
+ auto& st = g_runtime.views[view_id];
143
+ st.webview = wv;
144
+ st.container = container;
145
+ st.window_id = window_id;
146
+ st.appres_root = appres_root ? appres_root : "";
147
+ st.preload_script = preload ? preload : "";
148
+ st.navigation_rules = parseNavigationRulesJson(navigation_rules_json ? navigation_rules_json : "");
149
+ }
150
+
151
+ if (!auto_resize) applyViewBounds(view_id, x, y, width, height);
152
+ queueViewRedraw(wv);
153
+
154
+ if (url && *url) {
155
+ webkit_web_view_load_uri(wv, url);
156
+ } else if (html && *html) {
157
+ webkit_web_view_load_html(wv, html, "appres://app.internal/");
158
+ }
159
+
160
+ emitWebviewEvent(view_id, "view-ready", "");
161
+ return true;
162
+ }
163
+
164
+ void queueViewRedraw(WebKitWebView* wv) {
165
+ if (!wv) return;
166
+ GtkWidget* widget = GTK_WIDGET(wv);
167
+ gtk_widget_queue_resize(widget);
168
+ gtk_widget_queue_draw(widget);
169
+
170
+ g_object_ref(widget);
171
+ g_idle_add_full(G_PRIORITY_DEFAULT_IDLE,
172
+ +[](gpointer data) -> gboolean {
173
+ GtkWidget* w = GTK_WIDGET(data);
174
+ gtk_widget_queue_resize(w);
175
+ gtk_widget_queue_draw(w);
176
+ GtkWidget* parent = gtk_widget_get_parent(w);
177
+ if (parent) gtk_widget_queue_draw(parent);
178
+ GtkWidget* grandparent = parent ? gtk_widget_get_parent(parent) : nullptr;
179
+ if (grandparent) gtk_widget_queue_draw(grandparent);
180
+ return G_SOURCE_REMOVE;
181
+ },
182
+ widget,
183
+ +[](gpointer data) { g_object_unref(data); });
184
+ }
185
+
186
+ void applyViewBounds(uint32_t view_id, double x, double y, double w, double h) {
187
+ auto* v = findView(view_id);
188
+ if (!v) return;
189
+ const int scale = gtk_widget_get_scale_factor(GTK_WIDGET(v->webview));
190
+ const int s = scale > 0 ? scale : 1;
191
+ // floor origin, ceil far edge — preserve coverage at non-integer scales.
192
+ const int x0 = (int)(x / s);
193
+ const int y0 = (int)(y / s);
194
+ const int x1 = (int)((x + w + s - 1) / s);
195
+ const int y1 = (int)((y + h + s - 1) / s);
196
+ GtkWidget* widget = GTK_WIDGET(v->webview);
197
+ GtkWidget* container = v->container ? v->container : widget;
198
+ const int width = x1 - x0;
199
+ const int height = y1 - y0;
200
+ gtk_widget_set_margin_start(container, x0);
201
+ gtk_widget_set_margin_top(container, y0);
202
+ gtk_widget_set_size_request(container, width, height);
203
+ gtk_widget_set_size_request(widget, width, height);
204
+ gtk_widget_queue_resize(container);
205
+ gtk_widget_queue_draw(container);
206
+ queueViewRedraw(v->webview);
207
+ }
208
+
209
+ void detachViewSideState(uint32_t view_id) {
210
+ bunite::WebviewContentStorage::instance().remove(view_id);
211
+ std::vector<uint32_t> request_ids;
212
+ for (auto& [rid, p] : g_runtime.pending_route_tasks) {
213
+ if (p.view_id == view_id) request_ids.push_back(rid);
214
+ }
215
+ for (uint32_t rid : request_ids) {
216
+ auto it = g_runtime.pending_route_tasks.find(rid);
217
+ WebKitURISchemeRequest* req = it->second.request;
218
+ g_runtime.pending_route_tasks.erase(it);
219
+ GError* err = g_error_new_literal(G_IO_ERROR, G_IO_ERROR_CANCELLED, "view destroyed");
220
+ webkit_uri_scheme_request_finish_error(req, err);
221
+ g_error_free(err);
222
+ g_object_unref(req);
223
+ }
224
+ }
225
+
226
+ void removeView(uint32_t view_id) {
227
+ WebKitWebView* wv = nullptr;
228
+ GtkWidget* container = nullptr;
229
+ {
230
+ std::lock_guard<std::mutex> lock(g_runtime.object_mutex);
231
+ auto it = g_runtime.views.find(view_id);
232
+ if (it == g_runtime.views.end()) return;
233
+ wv = it->second.webview;
234
+ container = it->second.container;
235
+ g_runtime.views.erase(it);
236
+ }
237
+ detachViewSideState(view_id);
238
+ GtkWidget* target = container ? container : (wv ? GTK_WIDGET(wv) : nullptr);
239
+ if (target && gtk_widget_get_parent(target)) {
240
+ gtk_widget_unparent(target);
241
+ }
242
+ }
243
+
244
+ } // namespace bunite_linux
@@ -0,0 +1,101 @@
1
+ #include "bunite_linux_internal.h"
2
+
3
+ namespace bunite_linux {
4
+
5
+ namespace {
6
+
7
+ gboolean on_close_request(GtkWindow* window, gpointer user_data) {
8
+ (void)window;
9
+ const uint32_t window_id = GPOINTER_TO_UINT(user_data);
10
+ auto* state = findWindow(window_id);
11
+ if (!state) return FALSE;
12
+ if (!state->close_pending.exchange(true)) {
13
+ emitWindowEvent(window_id, "close-requested", "");
14
+ }
15
+ return TRUE; // JS must call destroy or reset_close_pending
16
+ }
17
+
18
+ void on_destroy(GtkWidget* widget, gpointer user_data) {
19
+ (void)widget;
20
+ const uint32_t window_id = GPOINTER_TO_UINT(user_data);
21
+ emitWindowEvent(window_id, "close", "");
22
+ std::vector<uint32_t> orphans;
23
+ bool last = false;
24
+ {
25
+ std::lock_guard<std::mutex> lock(g_runtime.object_mutex);
26
+ // GTK destroyed child view widgets with the parent; drop stale entries.
27
+ for (auto it = g_runtime.views.begin(); it != g_runtime.views.end();) {
28
+ if (it->second.window_id == window_id) {
29
+ orphans.push_back(it->first);
30
+ it = g_runtime.views.erase(it);
31
+ } else ++it;
32
+ }
33
+ g_runtime.windows.erase(window_id);
34
+ last = g_runtime.windows.empty();
35
+ }
36
+ for (uint32_t vid : orphans) detachViewSideState(vid);
37
+ if (last && !g_runtime.shutting_down.load()) {
38
+ emitWindowEvent(0, "all-windows-closed", "");
39
+ }
40
+ }
41
+
42
+ } // namespace
43
+
44
+ WindowState* findWindow(uint32_t window_id) {
45
+ std::lock_guard<std::mutex> lock(g_runtime.object_mutex);
46
+ auto it = g_runtime.windows.find(window_id);
47
+ return it == g_runtime.windows.end() ? nullptr : &it->second;
48
+ }
49
+
50
+ bool createWindow(uint32_t window_id, double x, double y, double width, double height,
51
+ const char* title, const char* title_bar_style,
52
+ bool transparent, bool hidden, bool minimized, bool maximized) {
53
+ (void)x; (void)y; // GTK4/Wayland: compositor places windows
54
+ (void)title_bar_style;
55
+ (void)transparent;
56
+
57
+ if (!g_runtime.app) {
58
+ BUNITE_ERROR("bunite_window_create: GtkApplication not initialized");
59
+ return false;
60
+ }
61
+
62
+ GtkWindow* win = GTK_WINDOW(gtk_application_window_new(g_runtime.app));
63
+ if (title && *title) gtk_window_set_title(win, title);
64
+ gtk_window_set_default_size(win, (int)width, (int)height);
65
+
66
+ GtkOverlay* host = GTK_OVERLAY(gtk_overlay_new());
67
+ gtk_window_set_child(win, GTK_WIDGET(host));
68
+
69
+ const gpointer id_ptr = GUINT_TO_POINTER(window_id);
70
+ g_signal_connect(win, "close-request", G_CALLBACK(on_close_request), id_ptr);
71
+ g_signal_connect(win, "destroy", G_CALLBACK(on_destroy), id_ptr);
72
+
73
+ {
74
+ std::lock_guard<std::mutex> lock(g_runtime.object_mutex);
75
+ auto& st = g_runtime.windows[window_id];
76
+ st.window = win;
77
+ st.host = host;
78
+ st.close_pending.store(false);
79
+ st.minimized.store(minimized);
80
+ st.maximized.store(maximized);
81
+ }
82
+
83
+ if (!hidden) gtk_window_present(win);
84
+ if (maximized) gtk_window_maximize(win);
85
+ if (minimized) gtk_window_minimize(win);
86
+
87
+ return true;
88
+ }
89
+
90
+ void destroyWindow(uint32_t window_id) {
91
+ GtkWindow* w = nullptr;
92
+ {
93
+ std::lock_guard<std::mutex> lock(g_runtime.object_mutex);
94
+ auto it = g_runtime.windows.find(window_id);
95
+ if (it == g_runtime.windows.end()) return;
96
+ w = it->second.window;
97
+ }
98
+ if (w) gtk_window_destroy(w);
99
+ }
100
+
101
+ } // namespace bunite_linux