v0.10.0 0f4e9eb

workspace_widgets_cgo.go RawHistoryFolder

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}

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