1//go:build webkit_cgo
2
3package webkit
4
5/*
6#cgo pkg-config: gtk4
7#include <gtk/gtk.h>
8#include <gdk/gdk.h>
9#include <glib.h>
10#include <glib-object.h>
11#include <graphene.h>
12#include <stdint.h>
13#include <stdlib.h>
14
15// Logging helper for diagnostics
16#define WORKSPACE_LOG(fmt, ...) g_printerr("[workspace-cgo] " fmt "\n", ##__VA_ARGS__)
17
18// Callback function for idle handling following the existing pattern
19extern gboolean goIdleCallback(uintptr_t handle);
20extern void goHoverCallback(uintptr_t handle);
21
22static gboolean idle_callback_wrapper(gpointer data) {
23 return goIdleCallback((uintptr_t)data);
24}
25
26static void add_idle_callback(uintptr_t handle) {
27 g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, idle_callback_wrapper, (gpointer)handle, NULL);
28}
29
30static void motion_enter_cb(GtkEventControllerMotion* controller, gdouble x, gdouble y, gpointer user_data) {
31 (void)controller; (void)x; (void)y;
32 uintptr_t handle = (uintptr_t)user_data;
33 goHoverCallback(handle);
34}
35
36static GtkEventController* widget_add_hover_controller(GtkWidget* widget, uintptr_t handle) {
37 if (!widget) {
38 return NULL;
39 }
40 GtkEventController* motion = gtk_event_controller_motion_new();
41 if (!motion) {
42 return NULL;
43 }
44 gtk_event_controller_set_propagation_phase(motion, GTK_PHASE_CAPTURE);
45 g_signal_connect_data(motion, "enter", G_CALLBACK(motion_enter_cb), (gpointer)handle, NULL, 0);
46 gtk_widget_add_controller(widget, motion);
47 return motion;
48}
49
50static void widget_remove_controller(GtkWidget* widget, GtkEventController* controller) {
51 if (!widget || !controller) {
52 return;
53 }
54 gtk_widget_remove_controller(widget, controller);
55}
56
57static gboolean widget_get_bounds(GtkWidget* widget, double* x, double* y, double* width, double* height) {
58 if (!widget || !x || !y || !width || !height) {
59 return FALSE;
60 }
61 GtkWidget* root = GTK_WIDGET(gtk_widget_get_root(widget));
62 if (!root) {
63 return FALSE;
64 }
65 graphene_rect_t rect;
66 if (!gtk_widget_compute_bounds(widget, root, &rect)) {
67 return FALSE;
68 }
69 *x = rect.origin.x;
70 *y = rect.origin.y;
71 *width = rect.size.width;
72 *height = rect.size.height;
73 return TRUE;
74}
75
76// Widget validation helper to prevent crashes from invalid widget pointers
77static gboolean is_valid_widget(gpointer widget) {
78 return widget != NULL && GTK_IS_WIDGET(widget);
79}
80
81static GtkWidget* paned_get_start_child(GtkWidget* paned) {
82 if (!paned) return NULL;
83 return gtk_paned_get_start_child(GTK_PANED(paned));
84}
85
86static GtkWidget* paned_get_end_child(GtkWidget* paned) {
87 if (!paned) return NULL;
88 return gtk_paned_get_end_child(GTK_PANED(paned));
89}
90
91static void on_widget_destroyed(GtkWidget* widget, gpointer user_data) {
92 (void)user_data;
93 guint ref_count = widget ? G_OBJECT(widget)->ref_count : 0;
94 WORKSPACE_LOG("widget destroyed: %p ref_count=%u", widget, ref_count);
95}
96
97static void hook_destroy_signal(GtkWidget* widget) {
98 if (!widget) return;
99 g_signal_connect(widget, "destroy", G_CALLBACK(on_widget_destroyed), NULL);
100}
101
102static unsigned int widget_get_ref_count(GtkWidget* widget) {
103 if (!widget) {
104 return 0;
105 }
106 return G_OBJECT(widget)->ref_count;
107}
108
109// Widget realization helpers for proper WebView reparenting
110// Force widget realization and ensure it's ready for rendering
111static void ensure_widget_realized(GtkWidget* widget) {
112 if (!widget) return;
113
114 // Make widget visible first
115 gtk_widget_set_visible(widget, TRUE);
116
117 // For WebKitWebView, we need to ensure the widget is properly sized
118 gtk_widget_set_size_request(widget, 100, 100); // Minimum size to force allocation
119
120 // Queue a redraw to force the rendering context to reinitialize
121 gtk_widget_queue_draw(widget);
122 WORKSPACE_LOG("ensure_widget_realized: widget=%p", widget);
123}
124
125static void add_css_provider(const char* css) {
126 if (!css) {
127 return;
128 }
129 GtkCssProvider* provider = gtk_css_provider_new();
130 if (!provider) {
131 return;
132 }
133 gtk_css_provider_load_from_string(provider, css);
134 GdkDisplay* display = gdk_display_get_default();
135 if (display) {
136 gtk_style_context_add_provider_for_display(display, GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
137 }
138 g_object_unref(provider);
139}
140
141static void widget_add_css_class(GtkWidget* widget, const char* name) {
142 if (!widget || !name) return;
143 gtk_widget_add_css_class(widget, name);
144}
145
146static void widget_remove_css_class(GtkWidget* widget, const char* name) {
147 if (!widget || !name) return;
148 gtk_widget_remove_css_class(widget, name);
149}
150*/
151import "C"
152
153import (
154 "log"
155 "sync"
156 "unsafe"
157)
158
159// WidgetBounds captures absolute widget bounds relative to the toplevel window.
160type WidgetBounds struct {
161 X float64
162 Y float64
163 Width float64
164 Height float64
165}
166
167var (
168 hoverMu sync.Mutex
169 hoverCallbacks = make(map[uintptr]func())
170 hoverControllers = make(map[uintptr]uintptr)
171 nextHoverID uintptr = 1
172)
173
174func widgetIsValid(widget uintptr) bool {
175 if widget == 0 {
176 return false
177 }
178 return C.is_valid_widget(C.gpointer(unsafe.Pointer(widget))) != 0
179}
180
181func widgetRefCount(widget uintptr) uint {
182 if widget == 0 || !widgetIsValid(widget) {
183 return 0
184 }
185 return uint(C.widget_get_ref_count((*C.GtkWidget)(unsafe.Pointer(widget))))
186}
187
188// WidgetIsValid exposes GTK_IS_WIDGET checks for diagnostics in Go.
189func WidgetIsValid(widget uintptr) bool {
190 return widgetIsValid(widget)
191}
192
193// WidgetRefCount exposes the widget reference count for debugging.
194func WidgetRefCount(widget uintptr) uint {
195 return widgetRefCount(widget)
196}
197
198// WidgetHookDestroy attaches a destroy signal handler for diagnostics (debug builds).
199func WidgetHookDestroy(widget uintptr) {
200 if widget == 0 || !widgetIsValid(widget) {
201 return
202 }
203 C.hook_destroy_signal((*C.GtkWidget)(unsafe.Pointer(widget)))
204}
205
206// AddCSSProvider installs application-level CSS rules.
207func AddCSSProvider(css string) {
208 if css == "" {
209 return
210 }
211 cstr := C.CString(css)
212 defer C.free(unsafe.Pointer(cstr))
213 C.add_css_provider(cstr)
214}
215
216// WidgetAddCSSClass attaches a CSS class to a widget.
217func WidgetAddCSSClass(widget uintptr, class string) {
218 if widget == 0 || class == "" || !widgetIsValid(widget) {
219 return
220 }
221 cstr := C.CString(class)
222 defer C.free(unsafe.Pointer(cstr))
223 C.widget_add_css_class((*C.GtkWidget)(unsafe.Pointer(widget)), cstr)
224}
225
226// WidgetRemoveCSSClass removes a CSS class from a widget.
227func WidgetRemoveCSSClass(widget uintptr, class string) {
228 if widget == 0 || class == "" || !widgetIsValid(widget) {
229 return
230 }
231 cstr := C.CString(class)
232 defer C.free(unsafe.Pointer(cstr))
233 C.widget_remove_css_class((*C.GtkWidget)(unsafe.Pointer(widget)), cstr)
234}
235
236// Orientation mirrors GtkOrientation for workspace layout helpers.
237type Orientation int
238
239const (
240 OrientationHorizontal Orientation = Orientation(C.GTK_ORIENTATION_HORIZONTAL)
241 OrientationVertical Orientation = Orientation(C.GTK_ORIENTATION_VERTICAL)
242)
243
244func goBool(b bool) C.gboolean {
245 if b {
246 return C.gboolean(1)
247 }
248 return C.gboolean(0)
249}
250
251// NewPaned constructs a GtkPaned widget for the given orientation.
252func NewPaned(orientation Orientation) uintptr {
253 paned := C.gtk_paned_new(C.GtkOrientation(orientation))
254 return uintptr(unsafe.Pointer(paned))
255}
256
257// PanedSetStartChild assigns the start child of a GtkPaned.
258func PanedSetStartChild(paned uintptr, child uintptr) {
259 if child != 0 {
260 // Check if widget already has a parent and force unparent if needed
261 parent := C.gtk_widget_get_parent((*C.GtkWidget)(unsafe.Pointer(child)))
262 if parent != nil {
263 C.gtk_widget_unparent((*C.GtkWidget)(unsafe.Pointer(child)))
264 }
265 }
266 C.gtk_paned_set_start_child((*C.GtkPaned)(unsafe.Pointer(paned)), (*C.GtkWidget)(unsafe.Pointer(child)))
267}
268
269// PanedSetEndChild assigns the end child of a GtkPaned.
270func PanedSetEndChild(paned uintptr, child uintptr) {
271 if child != 0 {
272 // Check if widget already has a parent and force unparent if needed
273 parent := C.gtk_widget_get_parent((*C.GtkWidget)(unsafe.Pointer(child)))
274 if parent != nil {
275 C.gtk_widget_unparent((*C.GtkWidget)(unsafe.Pointer(child)))
276 }
277 }
278 C.gtk_paned_set_end_child((*C.GtkPaned)(unsafe.Pointer(paned)), (*C.GtkWidget)(unsafe.Pointer(child)))
279}
280
281// PanedGetStartChild returns the start child pointer for diagnostics.
282func PanedGetStartChild(paned uintptr) uintptr {
283 if paned == 0 {
284 return 0
285 }
286 child := C.paned_get_start_child((*C.GtkWidget)(unsafe.Pointer(paned)))
287 return uintptr(unsafe.Pointer(child))
288}
289
290// PanedGetEndChild returns the end child pointer for diagnostics.
291func PanedGetEndChild(paned uintptr) uintptr {
292 if paned == 0 {
293 return 0
294 }
295 child := C.paned_get_end_child((*C.GtkWidget)(unsafe.Pointer(paned)))
296 return uintptr(unsafe.Pointer(child))
297}
298
299// PanedSetResizeStart configures whether the start child should expand when the paned is resized.
300func PanedSetResizeStart(paned uintptr, resize bool) {
301 C.gtk_paned_set_resize_start_child((*C.GtkPaned)(unsafe.Pointer(paned)), goBool(resize))
302}
303
304// PanedSetResizeEnd configures whether the end child should expand when the paned is resized.
305func PanedSetResizeEnd(paned uintptr, resize bool) {
306 C.gtk_paned_set_resize_end_child((*C.GtkPaned)(unsafe.Pointer(paned)), goBool(resize))
307}
308
309// PanedSetPosition sets the pixel position of the divider.
310func PanedSetPosition(paned uintptr, pos int) {
311 if paned == 0 {
312 return
313 }
314 C.gtk_paned_set_position((*C.GtkPaned)(unsafe.Pointer(paned)), C.int(pos))
315}
316
317// WidgetUnparent detaches a widget from its current parent.
318func WidgetUnparent(widget uintptr) {
319 if widget == 0 {
320 return
321 }
322 if !widgetIsValid(widget) {
323 log.Printf("[workspace] WidgetUnparent skipped invalid widget=%#x", widget)
324 return
325 }
326 before := widgetRefCount(widget)
327 log.Printf("[workspace] WidgetUnparent widget=%#x ref_before=%d", widget, before)
328 C.gtk_widget_unparent((*C.GtkWidget)(unsafe.Pointer(widget)))
329 if widgetIsValid(widget) {
330 after := widgetRefCount(widget)
331 log.Printf("[workspace] WidgetUnparent widget=%#x ref_after=%d", widget, after)
332 } else {
333 log.Printf("[workspace] WidgetUnparent widget=%#x now invalid", widget)
334 }
335}
336
337// WidgetSetHExpand configures horizontal expand for a widget.
338func WidgetSetHExpand(widget uintptr, expand bool) {
339 if widget == 0 {
340 return
341 }
342 log.Printf("[workspace] WidgetSetHExpand widget=%#x expand=%v", widget, expand)
343 C.gtk_widget_set_hexpand((*C.GtkWidget)(unsafe.Pointer(widget)), goBool(expand))
344}
345
346func WidgetGetParent(widget uintptr) uintptr {
347 if widget == 0 {
348 log.Printf("[workspace] WidgetGetParent: widget=0")
349 return 0
350 }
351 if !widgetIsValid(widget) {
352 log.Printf("[workspace] WidgetGetParent: widget=%#x invalid", widget)
353 return 0
354 }
355 parent := C.gtk_widget_get_parent((*C.GtkWidget)(unsafe.Pointer(widget)))
356 // Reduced logging: only debug mode shows individual widget operations
357 return uintptr(unsafe.Pointer(parent))
358}
359
360// WidgetSetVExpand configures vertical expand for a widget.
361func WidgetSetVExpand(widget uintptr, expand bool) {
362 if widget == 0 {
363 return
364 }
365 log.Printf("[workspace] WidgetSetVExpand widget=%#x expand=%v", widget, expand)
366 C.gtk_widget_set_vexpand((*C.GtkWidget)(unsafe.Pointer(widget)), goBool(expand))
367}
368
369// WidgetShow makes the widget visible.
370func WidgetShow(widget uintptr) {
371 if widget == 0 {
372 return
373 }
374 log.Printf("[workspace] WidgetShow widget=%#x", widget)
375 C.gtk_widget_set_visible((*C.GtkWidget)(unsafe.Pointer(widget)), C.gboolean(1))
376}
377
378// WidgetGrabFocus focuses the widget if possible.
379func WidgetGrabFocus(widget uintptr) {
380 if widget == 0 {
381 return
382 }
383 if !widgetIsValid(widget) {
384 log.Printf("[workspace] WidgetGrabFocus skipped invalid widget=%#x", widget)
385 return
386 }
387 // Reduced logging: focus grab logged only in consolidated focus operation
388 C.gtk_widget_grab_focus((*C.GtkWidget)(unsafe.Pointer(widget)))
389}
390
391// WidgetRef increments the reference count to keep the widget alive during reparenting.
392func WidgetRef(widget uintptr) bool {
393 if widget == 0 {
394 return false
395 }
396 if !widgetIsValid(widget) {
397 log.Printf("[workspace] WidgetRef skipped invalid widget=%#x", widget)
398 return false
399 }
400 before := widgetRefCount(widget)
401 log.Printf("[workspace] WidgetRef widget=%#x ref_before=%d", widget, before)
402 C.g_object_ref(C.gpointer(unsafe.Pointer(widget)))
403 after := widgetRefCount(widget)
404 log.Printf("[workspace] WidgetRef widget=%#x ref_after=%d", widget, after)
405 return true
406}
407
408// WidgetUnref releases a previously held widget reference.
409func WidgetUnref(widget uintptr) {
410 if widget == 0 {
411 return
412 }
413 if !widgetIsValid(widget) {
414 log.Printf("[workspace] WidgetUnref skipped invalid widget=%#x", widget)
415 return
416 }
417 before := widgetRefCount(widget)
418 log.Printf("[workspace] WidgetUnref widget=%#x ref_before=%d", widget, before)
419 C.g_object_unref(C.gpointer(unsafe.Pointer(widget)))
420 if widgetIsValid(widget) {
421 after := widgetRefCount(widget)
422 log.Printf("[workspace] WidgetUnref widget=%#x ref_after=%d", widget, after)
423 } else {
424 log.Printf("[workspace] WidgetUnref widget=%#x now invalid", widget)
425 }
426}
427
428// WidgetQueueAllocate queues an allocation on the widget.
429func WidgetQueueAllocate(widget uintptr) {
430 if widget == 0 {
431 return
432 }
433 log.Printf("[workspace] WidgetQueueAllocate widget=%#x", widget)
434 C.gtk_widget_queue_allocate((*C.GtkWidget)(unsafe.Pointer(widget)))
435}
436
437// WidgetRealizeInContainer ensures a widget is properly realized within a container
438func WidgetRealizeInContainer(widget uintptr) {
439 if widget == 0 {
440 return
441 }
442 // Reduced logging: widget realization logged only in consolidated focus operation
443 C.ensure_widget_realized((*C.GtkWidget)(unsafe.Pointer(widget)))
444}
445
446// WidgetAddHoverHandler registers a hover callback for the given widget and
447// returns a token that can be used to detach it later.
448func WidgetAddHoverHandler(widget uintptr, fn func()) uintptr {
449 if widget == 0 || fn == nil || !widgetIsValid(widget) {
450 return 0
451 }
452 hoverMu.Lock()
453 token := nextHoverID
454 nextHoverID++
455 hoverCallbacks[token] = fn
456 hoverMu.Unlock()
457
458 controller := C.widget_add_hover_controller((*C.GtkWidget)(unsafe.Pointer(widget)), C.uintptr_t(token))
459 if controller == nil {
460 hoverMu.Lock()
461 delete(hoverCallbacks, token)
462 hoverMu.Unlock()
463 return 0
464 }
465
466 hoverMu.Lock()
467 hoverControllers[token] = uintptr(unsafe.Pointer(controller))
468 hoverMu.Unlock()
469 return token
470}
471
472// WidgetRemoveHoverHandler removes a previously registered hover callback.
473func WidgetRemoveHoverHandler(widget uintptr, token uintptr) {
474 if token == 0 {
475 return
476 }
477
478 hoverMu.Lock()
479 controller := hoverControllers[token]
480 delete(hoverCallbacks, token)
481 delete(hoverControllers, token)
482 hoverMu.Unlock()
483
484 if controller == 0 {
485 return
486 }
487
488 if widget == 0 || !widgetIsValid(widget) {
489 return
490 }
491
492 C.widget_remove_controller((*C.GtkWidget)(unsafe.Pointer(widget)), (*C.GtkEventController)(unsafe.Pointer(controller)))
493}
494
495// WidgetGetBounds returns the absolute bounds of the widget relative to the root window.
496func WidgetGetBounds(widget uintptr) (WidgetBounds, bool) {
497 if widget == 0 || !widgetIsValid(widget) {
498 return WidgetBounds{}, false
499 }
500
501 var x, y, width, height C.double
502 if C.widget_get_bounds((*C.GtkWidget)(unsafe.Pointer(widget)), (*C.double)(&x), (*C.double)(&y), (*C.double)(&width), (*C.double)(&height)) == 0 {
503 return WidgetBounds{}, false
504 }
505
506 return WidgetBounds{
507 X: float64(x),
508 Y: float64(y),
509 Width: float64(width),
510 Height: float64(height),
511 }, true
512}
513
514// Global storage for idle callbacks
515var (
516 idleCallbacks = make(map[uintptr]func() bool)
517 nextIdleID uintptr = 1
518)
519
520// IdleAdd schedules a function to be called when the main loop is idle.
521func IdleAdd(fn func() bool) {
522 id := nextIdleID
523 nextIdleID++
524 idleCallbacks[id] = fn
525 C.add_idle_callback(C.uintptr_t(id))
526}
527
528//export goIdleCallback
529func goIdleCallback(handle uintptr) C.gboolean {
530 if fn, ok := idleCallbacks[handle]; ok {
531 delete(idleCallbacks, handle) // Clean up after calling
532 if fn() {
533 return C.gboolean(1) // G_SOURCE_CONTINUE
534 }
535 }
536 return C.gboolean(0) // G_SOURCE_REMOVE
537}
538
539//export goHoverCallback
540func goHoverCallback(handle C.uintptr_t) {
541 token := uintptr(handle)
542 hoverMu.Lock()
543 fn := hoverCallbacks[token]
544 hoverMu.Unlock()
545 if fn != nil {
546 fn()
547 }
548}