detached f223216

feat(webkit): add workspace container widgets and dom event handlers

commit
f2232169474fada258ed06bc603b3380aa1f995f
author
bamen · · 2025-09-19 03:41 UTC
parent
eac9484
signature
unsigned
3 files · +736 -0
A pkg/webkit/dom_events.go +25 -0
@@ -0,0 +1,25 @@
1+package webkit
2+
3+import (
4+ "encoding/json"
5+ "fmt"
6+)
7+
8+// DispatchCustomEvent sends a CustomEvent into the page world with the provided detail payload.
9+func (w *WebView) DispatchCustomEvent(eventName string, detail map[string]any) error {
10+ if w == nil {
11+ return ErrNotImplemented
12+ }
13+
14+ payload := "{}"
15+ if detail != nil {
16+ data, err := json.Marshal(detail)
17+ if err != nil {
18+ return fmt.Errorf("failed to marshal event detail: %w", err)
19+ }
20+ payload = string(data)
21+ }
22+
23+ script := fmt.Sprintf(`(function(){try{var detail=%s;document.dispatchEvent(new CustomEvent(%q,{detail:detail}));}catch(e){console.error('[dumber] Failed to dispatch %s', e);}})();`, payload, eventName, eventName)
24+ return w.InjectScript(script)
25+}
@@ -0,0 +1,25 @@
1+package webkit
2+
3+import (
4+ "encoding/json"
5+ "fmt"
6+)
7+
8+// DispatchCustomEvent sends a CustomEvent into the page world with the provided detail payload.
9+func (w *WebView) DispatchCustomEvent(eventName string, detail map[string]any) error {
10+ if w == nil {
11+ return ErrNotImplemented
12+ }
13+
14+ payload := "{}"
15+ if detail != nil {
16+ data, err := json.Marshal(detail)
17+ if err != nil {
18+ return fmt.Errorf("failed to marshal event detail: %w", err)
19+ }
20+ payload = string(data)
21+ }
22+
23+ script := fmt.Sprintf(`(function(){try{var detail=%s;document.dispatchEvent(new CustomEvent(%q,{detail:detail}));}catch(e){console.error('[dumber] Failed to dispatch %s', e);}})();`, payload, eventName, eventName)
24+ return w.InjectScript(script)
25+}
A pkg/webkit/workspace_widgets_cgo.go +526 -0
@@ -0,0 +1,526 @@
1+//go:build webkit_cgo
2+
3+package 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
19+extern gboolean goIdleCallback(uintptr_t handle);
20+extern void goHoverCallback(uintptr_t handle);
21+
22+static gboolean idle_callback_wrapper(gpointer data) {
23+ return goIdleCallback((uintptr_t)data);
24+}
25+
26+static void add_idle_callback(uintptr_t handle) {
27+ g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, idle_callback_wrapper, (gpointer)handle, NULL);
28+}
29+
30+static 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+
36+static 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+
50+static void widget_remove_controller(GtkWidget* widget, GtkEventController* controller) {
51+ if (!widget || !controller) {
52+ return;
53+ }
54+ gtk_widget_remove_controller(widget, controller);
55+}
56+
57+static 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
77+static gboolean is_valid_widget(gpointer widget) {
78+ return widget != NULL && GTK_IS_WIDGET(widget);
79+}
80+
81+static GtkWidget* paned_get_start_child(GtkWidget* paned) {
82+ if (!paned) return NULL;
83+ return gtk_paned_get_start_child(GTK_PANED(paned));
84+}
85+
86+static GtkWidget* paned_get_end_child(GtkWidget* paned) {
87+ if (!paned) return NULL;
88+ return gtk_paned_get_end_child(GTK_PANED(paned));
89+}
90+
91+static 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+
97+static void hook_destroy_signal(GtkWidget* widget) {
98+ if (!widget) return;
99+ g_signal_connect(widget, "destroy", G_CALLBACK(on_widget_destroyed), NULL);
100+}
101+
102+static 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
111+static 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+
125+static 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+
141+static 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+
146+static 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+*/
151+import "C"
152+
153+import (
154+ "log"
155+ "sync"
156+ "unsafe"
157+)
158+
159+// WidgetBounds captures absolute widget bounds relative to the toplevel window.
160+type WidgetBounds struct {
161+ X float64
162+ Y float64
163+ Width float64
164+ Height float64
165+}
166+
167+var (
168+ hoverMu sync.Mutex
169+ hoverCallbacks = make(map[uintptr]func())
170+ hoverControllers = make(map[uintptr]uintptr)
171+ nextHoverID uintptr = 1
172+)
173+
174+func 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+
181+func 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.
189+func WidgetIsValid(widget uintptr) bool {
190+ return widgetIsValid(widget)
191+}
192+
193+// WidgetRefCount exposes the widget reference count for debugging.
194+func WidgetRefCount(widget uintptr) uint {
195+ return widgetRefCount(widget)
196+}
197+
198+// WidgetHookDestroy attaches a destroy signal handler for diagnostics (debug builds).
199+func 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.
207+func 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.
217+func 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.
227+func 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.
237+type Orientation int
238+
239+const (
240+ OrientationHorizontal Orientation = Orientation(C.GTK_ORIENTATION_HORIZONTAL)
241+ OrientationVertical Orientation = Orientation(C.GTK_ORIENTATION_VERTICAL)
242+)
243+
244+func 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.
252+func 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.
258+func PanedSetStartChild(paned uintptr, child uintptr) {
259+ C.gtk_paned_set_start_child((*C.GtkPaned)(unsafe.Pointer(paned)), (*C.GtkWidget)(unsafe.Pointer(child)))
260+}
261+
262+// PanedSetEndChild assigns the end child of a GtkPaned.
263+func PanedSetEndChild(paned uintptr, child uintptr) {
264+ C.gtk_paned_set_end_child((*C.GtkPaned)(unsafe.Pointer(paned)), (*C.GtkWidget)(unsafe.Pointer(child)))
265+}
266+
267+// PanedGetStartChild returns the start child pointer for diagnostics.
268+func PanedGetStartChild(paned uintptr) uintptr {
269+ if paned == 0 {
270+ return 0
271+ }
272+ child := C.paned_get_start_child((*C.GtkWidget)(unsafe.Pointer(paned)))
273+ return uintptr(unsafe.Pointer(child))
274+}
275+
276+// PanedGetEndChild returns the end child pointer for diagnostics.
277+func PanedGetEndChild(paned uintptr) uintptr {
278+ if paned == 0 {
279+ return 0
280+ }
281+ child := C.paned_get_end_child((*C.GtkWidget)(unsafe.Pointer(paned)))
282+ return uintptr(unsafe.Pointer(child))
283+}
284+
285+// PanedSetResizeStart configures whether the start child should expand when the paned is resized.
286+func PanedSetResizeStart(paned uintptr, resize bool) {
287+ C.gtk_paned_set_resize_start_child((*C.GtkPaned)(unsafe.Pointer(paned)), goBool(resize))
288+}
289+
290+// PanedSetResizeEnd configures whether the end child should expand when the paned is resized.
291+func PanedSetResizeEnd(paned uintptr, resize bool) {
292+ C.gtk_paned_set_resize_end_child((*C.GtkPaned)(unsafe.Pointer(paned)), goBool(resize))
293+}
294+
295+// WidgetUnparent detaches a widget from its current parent.
296+func WidgetUnparent(widget uintptr) {
297+ if widget == 0 {
298+ return
299+ }
300+ if !widgetIsValid(widget) {
301+ log.Printf("[workspace] WidgetUnparent skipped invalid widget=%#x", widget)
302+ return
303+ }
304+ before := widgetRefCount(widget)
305+ log.Printf("[workspace] WidgetUnparent widget=%#x ref_before=%d", widget, before)
306+ C.gtk_widget_unparent((*C.GtkWidget)(unsafe.Pointer(widget)))
307+ if widgetIsValid(widget) {
308+ after := widgetRefCount(widget)
309+ log.Printf("[workspace] WidgetUnparent widget=%#x ref_after=%d", widget, after)
310+ } else {
311+ log.Printf("[workspace] WidgetUnparent widget=%#x now invalid", widget)
312+ }
313+}
314+
315+// WidgetSetHExpand configures horizontal expand for a widget.
316+func WidgetSetHExpand(widget uintptr, expand bool) {
317+ if widget == 0 {
318+ return
319+ }
320+ log.Printf("[workspace] WidgetSetHExpand widget=%#x expand=%v", widget, expand)
321+ C.gtk_widget_set_hexpand((*C.GtkWidget)(unsafe.Pointer(widget)), goBool(expand))
322+}
323+
324+func WidgetGetParent(widget uintptr) uintptr {
325+ if widget == 0 {
326+ log.Printf("[workspace] WidgetGetParent: widget=0")
327+ return 0
328+ }
329+ if !widgetIsValid(widget) {
330+ log.Printf("[workspace] WidgetGetParent: widget=%#x invalid", widget)
331+ return 0
332+ }
333+ parent := C.gtk_widget_get_parent((*C.GtkWidget)(unsafe.Pointer(widget)))
334+ // Reduced logging: only debug mode shows individual widget operations
335+ return uintptr(unsafe.Pointer(parent))
336+}
337+
338+// WidgetSetVExpand configures vertical expand for a widget.
339+func WidgetSetVExpand(widget uintptr, expand bool) {
340+ if widget == 0 {
341+ return
342+ }
343+ log.Printf("[workspace] WidgetSetVExpand widget=%#x expand=%v", widget, expand)
344+ C.gtk_widget_set_vexpand((*C.GtkWidget)(unsafe.Pointer(widget)), goBool(expand))
345+}
346+
347+// WidgetShow makes the widget visible.
348+func WidgetShow(widget uintptr) {
349+ if widget == 0 {
350+ return
351+ }
352+ log.Printf("[workspace] WidgetShow widget=%#x", widget)
353+ C.gtk_widget_set_visible((*C.GtkWidget)(unsafe.Pointer(widget)), C.gboolean(1))
354+}
355+
356+// WidgetGrabFocus focuses the widget if possible.
357+func WidgetGrabFocus(widget uintptr) {
358+ if widget == 0 {
359+ return
360+ }
361+ if !widgetIsValid(widget) {
362+ log.Printf("[workspace] WidgetGrabFocus skipped invalid widget=%#x", widget)
363+ return
364+ }
365+ // Reduced logging: focus grab logged only in consolidated focus operation
366+ C.gtk_widget_grab_focus((*C.GtkWidget)(unsafe.Pointer(widget)))
367+}
368+
369+// WidgetRef increments the reference count to keep the widget alive during reparenting.
370+func WidgetRef(widget uintptr) bool {
371+ if widget == 0 {
372+ return false
373+ }
374+ if !widgetIsValid(widget) {
375+ log.Printf("[workspace] WidgetRef skipped invalid widget=%#x", widget)
376+ return false
377+ }
378+ before := widgetRefCount(widget)
379+ log.Printf("[workspace] WidgetRef widget=%#x ref_before=%d", widget, before)
380+ C.g_object_ref(C.gpointer(unsafe.Pointer(widget)))
381+ after := widgetRefCount(widget)
382+ log.Printf("[workspace] WidgetRef widget=%#x ref_after=%d", widget, after)
383+ return true
384+}
385+
386+// WidgetUnref releases a previously held widget reference.
387+func WidgetUnref(widget uintptr) {
388+ if widget == 0 {
389+ return
390+ }
391+ if !widgetIsValid(widget) {
392+ log.Printf("[workspace] WidgetUnref skipped invalid widget=%#x", widget)
393+ return
394+ }
395+ before := widgetRefCount(widget)
396+ log.Printf("[workspace] WidgetUnref widget=%#x ref_before=%d", widget, before)
397+ C.g_object_unref(C.gpointer(unsafe.Pointer(widget)))
398+ if widgetIsValid(widget) {
399+ after := widgetRefCount(widget)
400+ log.Printf("[workspace] WidgetUnref widget=%#x ref_after=%d", widget, after)
401+ } else {
402+ log.Printf("[workspace] WidgetUnref widget=%#x now invalid", widget)
403+ }
404+}
405+
406+// WidgetQueueAllocate queues an allocation on the widget.
407+func WidgetQueueAllocate(widget uintptr) {
408+ if widget == 0 {
409+ return
410+ }
411+ log.Printf("[workspace] WidgetQueueAllocate widget=%#x", widget)
412+ C.gtk_widget_queue_allocate((*C.GtkWidget)(unsafe.Pointer(widget)))
413+}
414+
415+// WidgetRealizeInContainer ensures a widget is properly realized within a container
416+func WidgetRealizeInContainer(widget uintptr) {
417+ if widget == 0 {
418+ return
419+ }
420+ // Reduced logging: widget realization logged only in consolidated focus operation
421+ C.ensure_widget_realized((*C.GtkWidget)(unsafe.Pointer(widget)))
422+}
423+
424+// WidgetAddHoverHandler registers a hover callback for the given widget and
425+// returns a token that can be used to detach it later.
426+func WidgetAddHoverHandler(widget uintptr, fn func()) uintptr {
427+ if widget == 0 || fn == nil || !widgetIsValid(widget) {
428+ return 0
429+ }
430+ hoverMu.Lock()
431+ token := nextHoverID
432+ nextHoverID++
433+ hoverCallbacks[token] = fn
434+ hoverMu.Unlock()
435+
436+ controller := C.widget_add_hover_controller((*C.GtkWidget)(unsafe.Pointer(widget)), C.uintptr_t(token))
437+ if controller == nil {
438+ hoverMu.Lock()
439+ delete(hoverCallbacks, token)
440+ hoverMu.Unlock()
441+ return 0
442+ }
443+
444+ hoverMu.Lock()
445+ hoverControllers[token] = uintptr(unsafe.Pointer(controller))
446+ hoverMu.Unlock()
447+ return token
448+}
449+
450+// WidgetRemoveHoverHandler removes a previously registered hover callback.
451+func WidgetRemoveHoverHandler(widget uintptr, token uintptr) {
452+ if token == 0 {
453+ return
454+ }
455+
456+ hoverMu.Lock()
457+ controller := hoverControllers[token]
458+ delete(hoverCallbacks, token)
459+ delete(hoverControllers, token)
460+ hoverMu.Unlock()
461+
462+ if controller == 0 {
463+ return
464+ }
465+
466+ if widget == 0 || !widgetIsValid(widget) {
467+ return
468+ }
469+
470+ C.widget_remove_controller((*C.GtkWidget)(unsafe.Pointer(widget)), (*C.GtkEventController)(unsafe.Pointer(controller)))
471+}
472+
473+// WidgetGetBounds returns the absolute bounds of the widget relative to the root window.
474+func WidgetGetBounds(widget uintptr) (WidgetBounds, bool) {
475+ if widget == 0 || !widgetIsValid(widget) {
476+ return WidgetBounds{}, false
477+ }
478+
479+ var x, y, width, height C.double
480+ if C.widget_get_bounds((*C.GtkWidget)(unsafe.Pointer(widget)), (*C.double)(&x), (*C.double)(&y), (*C.double)(&width), (*C.double)(&height)) == 0 {
481+ return WidgetBounds{}, false
482+ }
483+
484+ return WidgetBounds{
485+ X: float64(x),
486+ Y: float64(y),
487+ Width: float64(width),
488+ Height: float64(height),
489+ }, true
490+}
491+
492+// Global storage for idle callbacks
493+var (
494+ idleCallbacks = make(map[uintptr]func() bool)
495+ nextIdleID uintptr = 1
496+)
497+
498+// IdleAdd schedules a function to be called when the main loop is idle.
499+func IdleAdd(fn func() bool) {
500+ id := nextIdleID
501+ nextIdleID++
502+ idleCallbacks[id] = fn
503+ C.add_idle_callback(C.uintptr_t(id))
504+}
505+
506+//export goIdleCallback
507+func goIdleCallback(handle uintptr) C.gboolean {
508+ if fn, ok := idleCallbacks[handle]; ok {
509+ delete(idleCallbacks, handle) // Clean up after calling
510+ if fn() {
511+ return C.gboolean(1) // G_SOURCE_CONTINUE
512+ }
513+ }
514+ return C.gboolean(0) // G_SOURCE_REMOVE
515+}
516+
517+//export goHoverCallback
518+func goHoverCallback(handle C.uintptr_t) {
519+ token := uintptr(handle)
520+ hoverMu.Lock()
521+ fn := hoverCallbacks[token]
522+ hoverMu.Unlock()
523+ if fn != nil {
524+ fn()
525+ }
526+}
@@ -0,0 +1,526 @@
1+//go:build webkit_cgo
2+
3+package 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
19+extern gboolean goIdleCallback(uintptr_t handle);
20+extern void goHoverCallback(uintptr_t handle);
21+
22+static gboolean idle_callback_wrapper(gpointer data) {
23+ return goIdleCallback((uintptr_t)data);
24+}
25+
26+static void add_idle_callback(uintptr_t handle) {
27+ g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, idle_callback_wrapper, (gpointer)handle, NULL);
28+}
29+
30+static 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+
36+static 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+
50+static void widget_remove_controller(GtkWidget* widget, GtkEventController* controller) {
51+ if (!widget || !controller) {
52+ return;
53+ }
54+ gtk_widget_remove_controller(widget, controller);
55+}
56+
57+static 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
77+static gboolean is_valid_widget(gpointer widget) {
78+ return widget != NULL && GTK_IS_WIDGET(widget);
79+}
80+
81+static GtkWidget* paned_get_start_child(GtkWidget* paned) {
82+ if (!paned) return NULL;
83+ return gtk_paned_get_start_child(GTK_PANED(paned));
84+}
85+
86+static GtkWidget* paned_get_end_child(GtkWidget* paned) {
87+ if (!paned) return NULL;
88+ return gtk_paned_get_end_child(GTK_PANED(paned));
89+}
90+
91+static 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+
97+static void hook_destroy_signal(GtkWidget* widget) {
98+ if (!widget) return;
99+ g_signal_connect(widget, "destroy", G_CALLBACK(on_widget_destroyed), NULL);
100+}
101+
102+static 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
111+static 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+
125+static 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+
141+static 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+
146+static 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+*/
151+import "C"
152+
153+import (
154+ "log"
155+ "sync"
156+ "unsafe"
157+)
158+
159+// WidgetBounds captures absolute widget bounds relative to the toplevel window.
160+type WidgetBounds struct {
161+ X float64
162+ Y float64
163+ Width float64
164+ Height float64
165+}
166+
167+var (
168+ hoverMu sync.Mutex
169+ hoverCallbacks = make(map[uintptr]func())
170+ hoverControllers = make(map[uintptr]uintptr)
171+ nextHoverID uintptr = 1
172+)
173+
174+func 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+
181+func 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.
189+func WidgetIsValid(widget uintptr) bool {
190+ return widgetIsValid(widget)
191+}
192+
193+// WidgetRefCount exposes the widget reference count for debugging.
194+func WidgetRefCount(widget uintptr) uint {
195+ return widgetRefCount(widget)
196+}
197+
198+// WidgetHookDestroy attaches a destroy signal handler for diagnostics (debug builds).
199+func 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.
207+func 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.
217+func 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.
227+func 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.
237+type Orientation int
238+
239+const (
240+ OrientationHorizontal Orientation = Orientation(C.GTK_ORIENTATION_HORIZONTAL)
241+ OrientationVertical Orientation = Orientation(C.GTK_ORIENTATION_VERTICAL)
242+)
243+
244+func 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.
252+func 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.
258+func PanedSetStartChild(paned uintptr, child uintptr) {
259+ C.gtk_paned_set_start_child((*C.GtkPaned)(unsafe.Pointer(paned)), (*C.GtkWidget)(unsafe.Pointer(child)))
260+}
261+
262+// PanedSetEndChild assigns the end child of a GtkPaned.
263+func PanedSetEndChild(paned uintptr, child uintptr) {
264+ C.gtk_paned_set_end_child((*C.GtkPaned)(unsafe.Pointer(paned)), (*C.GtkWidget)(unsafe.Pointer(child)))
265+}
266+
267+// PanedGetStartChild returns the start child pointer for diagnostics.
268+func PanedGetStartChild(paned uintptr) uintptr {
269+ if paned == 0 {
270+ return 0
271+ }
272+ child := C.paned_get_start_child((*C.GtkWidget)(unsafe.Pointer(paned)))
273+ return uintptr(unsafe.Pointer(child))
274+}
275+
276+// PanedGetEndChild returns the end child pointer for diagnostics.
277+func PanedGetEndChild(paned uintptr) uintptr {
278+ if paned == 0 {
279+ return 0
280+ }
281+ child := C.paned_get_end_child((*C.GtkWidget)(unsafe.Pointer(paned)))
282+ return uintptr(unsafe.Pointer(child))
283+}
284+
285+// PanedSetResizeStart configures whether the start child should expand when the paned is resized.
286+func PanedSetResizeStart(paned uintptr, resize bool) {
287+ C.gtk_paned_set_resize_start_child((*C.GtkPaned)(unsafe.Pointer(paned)), goBool(resize))
288+}
289+
290+// PanedSetResizeEnd configures whether the end child should expand when the paned is resized.
291+func PanedSetResizeEnd(paned uintptr, resize bool) {
292+ C.gtk_paned_set_resize_end_child((*C.GtkPaned)(unsafe.Pointer(paned)), goBool(resize))
293+}
294+
295+// WidgetUnparent detaches a widget from its current parent.
296+func WidgetUnparent(widget uintptr) {
297+ if widget == 0 {
298+ return
299+ }
300+ if !widgetIsValid(widget) {
301+ log.Printf("[workspace] WidgetUnparent skipped invalid widget=%#x", widget)
302+ return
303+ }
304+ before := widgetRefCount(widget)
305+ log.Printf("[workspace] WidgetUnparent widget=%#x ref_before=%d", widget, before)
306+ C.gtk_widget_unparent((*C.GtkWidget)(unsafe.Pointer(widget)))
307+ if widgetIsValid(widget) {
308+ after := widgetRefCount(widget)
309+ log.Printf("[workspace] WidgetUnparent widget=%#x ref_after=%d", widget, after)
310+ } else {
311+ log.Printf("[workspace] WidgetUnparent widget=%#x now invalid", widget)
312+ }
313+}
314+
315+// WidgetSetHExpand configures horizontal expand for a widget.
316+func WidgetSetHExpand(widget uintptr, expand bool) {
317+ if widget == 0 {
318+ return
319+ }
320+ log.Printf("[workspace] WidgetSetHExpand widget=%#x expand=%v", widget, expand)
321+ C.gtk_widget_set_hexpand((*C.GtkWidget)(unsafe.Pointer(widget)), goBool(expand))
322+}
323+
324+func WidgetGetParent(widget uintptr) uintptr {
325+ if widget == 0 {
326+ log.Printf("[workspace] WidgetGetParent: widget=0")
327+ return 0
328+ }
329+ if !widgetIsValid(widget) {
330+ log.Printf("[workspace] WidgetGetParent: widget=%#x invalid", widget)
331+ return 0
332+ }
333+ parent := C.gtk_widget_get_parent((*C.GtkWidget)(unsafe.Pointer(widget)))
334+ // Reduced logging: only debug mode shows individual widget operations
335+ return uintptr(unsafe.Pointer(parent))
336+}
337+
338+// WidgetSetVExpand configures vertical expand for a widget.
339+func WidgetSetVExpand(widget uintptr, expand bool) {
340+ if widget == 0 {
341+ return
342+ }
343+ log.Printf("[workspace] WidgetSetVExpand widget=%#x expand=%v", widget, expand)
344+ C.gtk_widget_set_vexpand((*C.GtkWidget)(unsafe.Pointer(widget)), goBool(expand))
345+}
346+
347+// WidgetShow makes the widget visible.
348+func WidgetShow(widget uintptr) {
349+ if widget == 0 {
350+ return
351+ }
352+ log.Printf("[workspace] WidgetShow widget=%#x", widget)
353+ C.gtk_widget_set_visible((*C.GtkWidget)(unsafe.Pointer(widget)), C.gboolean(1))
354+}
355+
356+// WidgetGrabFocus focuses the widget if possible.
357+func WidgetGrabFocus(widget uintptr) {
358+ if widget == 0 {
359+ return
360+ }
361+ if !widgetIsValid(widget) {
362+ log.Printf("[workspace] WidgetGrabFocus skipped invalid widget=%#x", widget)
363+ return
364+ }
365+ // Reduced logging: focus grab logged only in consolidated focus operation
366+ C.gtk_widget_grab_focus((*C.GtkWidget)(unsafe.Pointer(widget)))
367+}
368+
369+// WidgetRef increments the reference count to keep the widget alive during reparenting.
370+func WidgetRef(widget uintptr) bool {
371+ if widget == 0 {
372+ return false
373+ }
374+ if !widgetIsValid(widget) {
375+ log.Printf("[workspace] WidgetRef skipped invalid widget=%#x", widget)
376+ return false
377+ }
378+ before := widgetRefCount(widget)
379+ log.Printf("[workspace] WidgetRef widget=%#x ref_before=%d", widget, before)
380+ C.g_object_ref(C.gpointer(unsafe.Pointer(widget)))
381+ after := widgetRefCount(widget)
382+ log.Printf("[workspace] WidgetRef widget=%#x ref_after=%d", widget, after)
383+ return true
384+}
385+
386+// WidgetUnref releases a previously held widget reference.
387+func WidgetUnref(widget uintptr) {
388+ if widget == 0 {
389+ return
390+ }
391+ if !widgetIsValid(widget) {
392+ log.Printf("[workspace] WidgetUnref skipped invalid widget=%#x", widget)
393+ return
394+ }
395+ before := widgetRefCount(widget)
396+ log.Printf("[workspace] WidgetUnref widget=%#x ref_before=%d", widget, before)
397+ C.g_object_unref(C.gpointer(unsafe.Pointer(widget)))
398+ if widgetIsValid(widget) {
399+ after := widgetRefCount(widget)
400+ log.Printf("[workspace] WidgetUnref widget=%#x ref_after=%d", widget, after)
401+ } else {
402+ log.Printf("[workspace] WidgetUnref widget=%#x now invalid", widget)
403+ }
404+}
405+
406+// WidgetQueueAllocate queues an allocation on the widget.
407+func WidgetQueueAllocate(widget uintptr) {
408+ if widget == 0 {
409+ return
410+ }
411+ log.Printf("[workspace] WidgetQueueAllocate widget=%#x", widget)
412+ C.gtk_widget_queue_allocate((*C.GtkWidget)(unsafe.Pointer(widget)))
413+}
414+
415+// WidgetRealizeInContainer ensures a widget is properly realized within a container
416+func WidgetRealizeInContainer(widget uintptr) {
417+ if widget == 0 {
418+ return
419+ }
420+ // Reduced logging: widget realization logged only in consolidated focus operation
421+ C.ensure_widget_realized((*C.GtkWidget)(unsafe.Pointer(widget)))
422+}
423+
424+// WidgetAddHoverHandler registers a hover callback for the given widget and
425+// returns a token that can be used to detach it later.
426+func WidgetAddHoverHandler(widget uintptr, fn func()) uintptr {
427+ if widget == 0 || fn == nil || !widgetIsValid(widget) {
428+ return 0
429+ }
430+ hoverMu.Lock()
431+ token := nextHoverID
432+ nextHoverID++
433+ hoverCallbacks[token] = fn
434+ hoverMu.Unlock()
435+
436+ controller := C.widget_add_hover_controller((*C.GtkWidget)(unsafe.Pointer(widget)), C.uintptr_t(token))
437+ if controller == nil {
438+ hoverMu.Lock()
439+ delete(hoverCallbacks, token)
440+ hoverMu.Unlock()
441+ return 0
442+ }
443+
444+ hoverMu.Lock()
445+ hoverControllers[token] = uintptr(unsafe.Pointer(controller))
446+ hoverMu.Unlock()
447+ return token
448+}
449+
450+// WidgetRemoveHoverHandler removes a previously registered hover callback.
451+func WidgetRemoveHoverHandler(widget uintptr, token uintptr) {
452+ if token == 0 {
453+ return
454+ }
455+
456+ hoverMu.Lock()
457+ controller := hoverControllers[token]
458+ delete(hoverCallbacks, token)
459+ delete(hoverControllers, token)
460+ hoverMu.Unlock()
461+
462+ if controller == 0 {
463+ return
464+ }
465+
466+ if widget == 0 || !widgetIsValid(widget) {
467+ return
468+ }
469+
470+ C.widget_remove_controller((*C.GtkWidget)(unsafe.Pointer(widget)), (*C.GtkEventController)(unsafe.Pointer(controller)))
471+}
472+
473+// WidgetGetBounds returns the absolute bounds of the widget relative to the root window.
474+func WidgetGetBounds(widget uintptr) (WidgetBounds, bool) {
475+ if widget == 0 || !widgetIsValid(widget) {
476+ return WidgetBounds{}, false
477+ }
478+
479+ var x, y, width, height C.double
480+ if C.widget_get_bounds((*C.GtkWidget)(unsafe.Pointer(widget)), (*C.double)(&x), (*C.double)(&y), (*C.double)(&width), (*C.double)(&height)) == 0 {
481+ return WidgetBounds{}, false
482+ }
483+
484+ return WidgetBounds{
485+ X: float64(x),
486+ Y: float64(y),
487+ Width: float64(width),
488+ Height: float64(height),
489+ }, true
490+}
491+
492+// Global storage for idle callbacks
493+var (
494+ idleCallbacks = make(map[uintptr]func() bool)
495+ nextIdleID uintptr = 1
496+)
497+
498+// IdleAdd schedules a function to be called when the main loop is idle.
499+func IdleAdd(fn func() bool) {
500+ id := nextIdleID
501+ nextIdleID++
502+ idleCallbacks[id] = fn
503+ C.add_idle_callback(C.uintptr_t(id))
504+}
505+
506+//export goIdleCallback
507+func goIdleCallback(handle uintptr) C.gboolean {
508+ if fn, ok := idleCallbacks[handle]; ok {
509+ delete(idleCallbacks, handle) // Clean up after calling
510+ if fn() {
511+ return C.gboolean(1) // G_SOURCE_CONTINUE
512+ }
513+ }
514+ return C.gboolean(0) // G_SOURCE_REMOVE
515+}
516+
517+//export goHoverCallback
518+func goHoverCallback(handle C.uintptr_t) {
519+ token := uintptr(handle)
520+ hoverMu.Lock()
521+ fn := hoverCallbacks[token]
522+ hoverMu.Unlock()
523+ if fn != nil {
524+ fn()
525+ }
526+}
A pkg/webkit/workspace_widgets_stub.go +185 -0
@@ -0,0 +1,185 @@
1+//go:build !webkit_cgo
2+
3+package webkit
4+
5+import "sync"
6+
7+// Orientation mirrors GtkOrientation in stub builds.
8+type Orientation int
9+
10+const (
11+ OrientationHorizontal Orientation = 0
12+ OrientationVertical Orientation = 1
13+)
14+
15+// WidgetBounds mirrors the CGO struct for stub builds.
16+type WidgetBounds struct {
17+ X float64
18+ Y float64
19+ Width float64
20+ Height float64
21+}
22+
23+type widgetStub struct {
24+ startChild uintptr
25+ endChild uintptr
26+ bounds WidgetBounds
27+ hasBounds bool
28+ hover map[uintptr]func()
29+}
30+
31+var (
32+ widgetMu sync.Mutex
33+ widgetState = map[uintptr]*widgetStub{}
34+ nextWidgetID uintptr = 1
35+ nextHoverToken uintptr = 1
36+)
37+
38+func newWidgetHandle() uintptr {
39+ widgetMu.Lock()
40+ defer widgetMu.Unlock()
41+ id := nextWidgetID
42+ nextWidgetID++
43+ widgetState[id] = &widgetStub{}
44+ return id
45+}
46+
47+// NewTestWidget returns a unique widget handle for tests.
48+func NewTestWidget() uintptr { return newWidgetHandle() }
49+
50+// SetWidgetBoundsForTesting assigns bounds for the widget in stub builds.
51+func SetWidgetBoundsForTesting(widget uintptr, bounds WidgetBounds) {
52+ widgetMu.Lock()
53+ defer widgetMu.Unlock()
54+ stub, ok := widgetState[widget]
55+ if !ok {
56+ stub = &widgetStub{}
57+ widgetState[widget] = stub
58+ }
59+ stub.bounds = bounds
60+ stub.hasBounds = true
61+}
62+
63+// ResetWidgetStubsForTesting clears widget stub state for deterministic tests.
64+func ResetWidgetStubsForTesting() {
65+ widgetMu.Lock()
66+ defer widgetMu.Unlock()
67+ widgetState = map[uintptr]*widgetStub{}
68+ nextWidgetID = 1
69+ nextHoverToken = 1
70+}
71+
72+func NewPaned(orientation Orientation) uintptr {
73+ return newWidgetHandle()
74+}
75+
76+func PanedSetStartChild(paned uintptr, child uintptr) {
77+ widgetMu.Lock()
78+ defer widgetMu.Unlock()
79+ if stub, ok := widgetState[paned]; ok {
80+ stub.startChild = child
81+ }
82+}
83+
84+func PanedSetEndChild(paned uintptr, child uintptr) {
85+ widgetMu.Lock()
86+ defer widgetMu.Unlock()
87+ if stub, ok := widgetState[paned]; ok {
88+ stub.endChild = child
89+ }
90+}
91+
92+func PanedSetResizeStart(paned uintptr, resize bool) {}
93+
94+func PanedSetResizeEnd(paned uintptr, resize bool) {}
95+
96+func WidgetUnparent(widget uintptr) {}
97+
98+func WidgetSetHExpand(widget uintptr, expand bool) {}
99+
100+func WidgetSetVExpand(widget uintptr, expand bool) {}
101+
102+func WidgetGetParent(widget uintptr) uintptr { return 0 }
103+
104+func WidgetShow(widget uintptr) {}
105+
106+func WidgetGrabFocus(widget uintptr) {}
107+
108+func WidgetRef(widget uintptr) bool {
109+ widgetMu.Lock()
110+ defer widgetMu.Unlock()
111+ _, ok := widgetState[widget]
112+ return ok
113+}
114+
115+func WidgetUnref(widget uintptr) {}
116+
117+func WidgetQueueAllocate(widget uintptr) {}
118+
119+func WidgetRealizeInContainer(widget uintptr) {}
120+
121+func WidgetHookDestroy(widget uintptr) {}
122+
123+func WidgetIsValid(widget uintptr) bool {
124+ widgetMu.Lock()
125+ defer widgetMu.Unlock()
126+ _, ok := widgetState[widget]
127+ return ok && widget != 0
128+}
129+
130+func WidgetRefCount(widget uintptr) uint {
131+ if WidgetIsValid(widget) {
132+ return 1
133+ }
134+ return 0
135+}
136+
137+func WidgetAddHoverHandler(widget uintptr, fn func()) uintptr {
138+ if widget == 0 || fn == nil {
139+ return 0
140+ }
141+ widgetMu.Lock()
142+ defer widgetMu.Unlock()
143+ stub, ok := widgetState[widget]
144+ if !ok {
145+ stub = &widgetStub{}
146+ widgetState[widget] = stub
147+ }
148+ if stub.hover == nil {
149+ stub.hover = make(map[uintptr]func())
150+ }
151+ token := nextHoverToken
152+ nextHoverToken++
153+ stub.hover[token] = fn
154+ return token
155+}
156+
157+func WidgetRemoveHoverHandler(widget uintptr, token uintptr) {
158+ widgetMu.Lock()
159+ defer widgetMu.Unlock()
160+ if stub, ok := widgetState[widget]; ok && stub.hover != nil {
161+ delete(stub.hover, token)
162+ }
163+}
164+
165+func WidgetGetBounds(widget uintptr) (WidgetBounds, bool) {
166+ widgetMu.Lock()
167+ defer widgetMu.Unlock()
168+ stub, ok := widgetState[widget]
169+ if !ok || !stub.hasBounds {
170+ return WidgetBounds{}, false
171+ }
172+ return stub.bounds, true
173+}
174+
175+func IdleAdd(fn func() bool) {
176+ if fn != nil {
177+ fn()
178+ }
179+}
180+
181+func AddCSSProvider(css string) {}
182+
183+func WidgetAddCSSClass(widget uintptr, class string) {}
184+
185+func WidgetRemoveCSSClass(widget uintptr, class string) {}
@@ -0,0 +1,185 @@
1+//go:build !webkit_cgo
2+
3+package webkit
4+
5+import "sync"
6+
7+// Orientation mirrors GtkOrientation in stub builds.
8+type Orientation int
9+
10+const (
11+ OrientationHorizontal Orientation = 0
12+ OrientationVertical Orientation = 1
13+)
14+
15+// WidgetBounds mirrors the CGO struct for stub builds.
16+type WidgetBounds struct {
17+ X float64
18+ Y float64
19+ Width float64
20+ Height float64
21+}
22+
23+type widgetStub struct {
24+ startChild uintptr
25+ endChild uintptr
26+ bounds WidgetBounds
27+ hasBounds bool
28+ hover map[uintptr]func()
29+}
30+
31+var (
32+ widgetMu sync.Mutex
33+ widgetState = map[uintptr]*widgetStub{}
34+ nextWidgetID uintptr = 1
35+ nextHoverToken uintptr = 1
36+)
37+
38+func newWidgetHandle() uintptr {
39+ widgetMu.Lock()
40+ defer widgetMu.Unlock()
41+ id := nextWidgetID
42+ nextWidgetID++
43+ widgetState[id] = &widgetStub{}
44+ return id
45+}
46+
47+// NewTestWidget returns a unique widget handle for tests.
48+func NewTestWidget() uintptr { return newWidgetHandle() }
49+
50+// SetWidgetBoundsForTesting assigns bounds for the widget in stub builds.
51+func SetWidgetBoundsForTesting(widget uintptr, bounds WidgetBounds) {
52+ widgetMu.Lock()
53+ defer widgetMu.Unlock()
54+ stub, ok := widgetState[widget]
55+ if !ok {
56+ stub = &widgetStub{}
57+ widgetState[widget] = stub
58+ }
59+ stub.bounds = bounds
60+ stub.hasBounds = true
61+}
62+
63+// ResetWidgetStubsForTesting clears widget stub state for deterministic tests.
64+func ResetWidgetStubsForTesting() {
65+ widgetMu.Lock()
66+ defer widgetMu.Unlock()
67+ widgetState = map[uintptr]*widgetStub{}
68+ nextWidgetID = 1
69+ nextHoverToken = 1
70+}
71+
72+func NewPaned(orientation Orientation) uintptr {
73+ return newWidgetHandle()
74+}
75+
76+func PanedSetStartChild(paned uintptr, child uintptr) {
77+ widgetMu.Lock()
78+ defer widgetMu.Unlock()
79+ if stub, ok := widgetState[paned]; ok {
80+ stub.startChild = child
81+ }
82+}
83+
84+func PanedSetEndChild(paned uintptr, child uintptr) {
85+ widgetMu.Lock()
86+ defer widgetMu.Unlock()
87+ if stub, ok := widgetState[paned]; ok {
88+ stub.endChild = child
89+ }
90+}
91+
92+func PanedSetResizeStart(paned uintptr, resize bool) {}
93+
94+func PanedSetResizeEnd(paned uintptr, resize bool) {}
95+
96+func WidgetUnparent(widget uintptr) {}
97+
98+func WidgetSetHExpand(widget uintptr, expand bool) {}
99+
100+func WidgetSetVExpand(widget uintptr, expand bool) {}
101+
102+func WidgetGetParent(widget uintptr) uintptr { return 0 }
103+
104+func WidgetShow(widget uintptr) {}
105+
106+func WidgetGrabFocus(widget uintptr) {}
107+
108+func WidgetRef(widget uintptr) bool {
109+ widgetMu.Lock()
110+ defer widgetMu.Unlock()
111+ _, ok := widgetState[widget]
112+ return ok
113+}
114+
115+func WidgetUnref(widget uintptr) {}
116+
117+func WidgetQueueAllocate(widget uintptr) {}
118+
119+func WidgetRealizeInContainer(widget uintptr) {}
120+
121+func WidgetHookDestroy(widget uintptr) {}
122+
123+func WidgetIsValid(widget uintptr) bool {
124+ widgetMu.Lock()
125+ defer widgetMu.Unlock()
126+ _, ok := widgetState[widget]
127+ return ok && widget != 0
128+}
129+
130+func WidgetRefCount(widget uintptr) uint {
131+ if WidgetIsValid(widget) {
132+ return 1
133+ }
134+ return 0
135+}
136+
137+func WidgetAddHoverHandler(widget uintptr, fn func()) uintptr {
138+ if widget == 0 || fn == nil {
139+ return 0
140+ }
141+ widgetMu.Lock()
142+ defer widgetMu.Unlock()
143+ stub, ok := widgetState[widget]
144+ if !ok {
145+ stub = &widgetStub{}
146+ widgetState[widget] = stub
147+ }
148+ if stub.hover == nil {
149+ stub.hover = make(map[uintptr]func())
150+ }
151+ token := nextHoverToken
152+ nextHoverToken++
153+ stub.hover[token] = fn
154+ return token
155+}
156+
157+func WidgetRemoveHoverHandler(widget uintptr, token uintptr) {
158+ widgetMu.Lock()
159+ defer widgetMu.Unlock()
160+ if stub, ok := widgetState[widget]; ok && stub.hover != nil {
161+ delete(stub.hover, token)
162+ }
163+}
164+
165+func WidgetGetBounds(widget uintptr) (WidgetBounds, bool) {
166+ widgetMu.Lock()
167+ defer widgetMu.Unlock()
168+ stub, ok := widgetState[widget]
169+ if !ok || !stub.hasBounds {
170+ return WidgetBounds{}, false
171+ }
172+ return stub.bounds, true
173+}
174+
175+func IdleAdd(fn func() bool) {
176+ if fn != nil {
177+ fn()
178+ }
179+}
180+
181+func AddCSSProvider(css string) {}
182+
183+func WidgetAddCSSClass(widget uintptr, class string) {}
184+
185+func WidgetRemoveCSSClass(widget uintptr, class string) {}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close