v0.11.0 dc50343

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 <gtk/gtktestutils.h>
9#include <gtk/gtkbox.h>
10#include <gtk/gtkpaned.h>
11#include <gdk/gdk.h>
12#include <glib.h>
13#include <glib-object.h>
14#include <graphene.h>
15#include <pthread.h>
16#include <stdint.h>
17#include <stdlib.h>
18
19// Logging helper for diagnostics
20#define WORKSPACE_LOG(fmt, ...) g_printerr("[workspace-cgo] " fmt "\n", ##__VA_ARGS__)
21
22// Helper functions for widget type checking
23static gboolean is_paned_widget(GtkWidget *widget) {
24    return GTK_IS_PANED(widget);
25}
26
27static gboolean is_box_widget(GtkWidget *widget) {
28    return GTK_IS_BOX(widget);
29}
30
31// Callback function for idle handling following the existing pattern
32extern gboolean goIdleCallback(uintptr_t handle);
33extern void goHoverCallback(uintptr_t handle);
34extern void goFocusEnterCallback(uintptr_t handle);
35extern void goFocusLeaveCallback(uintptr_t handle);
36extern void goTitleBarClickCallback(uintptr_t handle);
37
38// Main thread helpers implemented in thread_helpers.c
39void store_main_thread_id();
40int is_main_thread();
41int iterate_main_loop();
42
43static gboolean idle_callback_wrapper(gpointer data) {
44    return goIdleCallback((uintptr_t)data);
45}
46
47static guint add_idle_callback(uintptr_t handle) {
48    return g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, idle_callback_wrapper, (gpointer)handle, NULL);
49}
50
51static gboolean remove_idle_callback(uintptr_t handle) {
52    return g_idle_remove_by_data((gpointer)handle);
53}
54
55// Title bar click callback
56static void title_bar_click_cb(GtkGestureClick* gesture, gint n_press, gdouble x, gdouble y, gpointer user_data) {
57    (void)gesture; (void)n_press; (void)x; (void)y;
58    uintptr_t handle = (uintptr_t)user_data;
59    goTitleBarClickCallback(handle);
60}
61
62// Attach click handler to a widget (for title bars)
63static void widget_attach_click_handler(GtkWidget* widget, uintptr_t handle) {
64    if (!widget) {
65        return;
66    }
67    GtkGesture* click = gtk_gesture_click_new();
68    if (!click) {
69        return;
70    }
71    g_signal_connect_data(click, "pressed", G_CALLBACK(title_bar_click_cb), (gpointer)handle, NULL, 0);
72    gtk_widget_add_controller(widget, GTK_EVENT_CONTROLLER(click));
73}
74
75static void motion_enter_cb(GtkEventControllerMotion* controller, gdouble x, gdouble y, gpointer user_data) {
76    (void)controller; (void)x; (void)y;
77    uintptr_t handle = (uintptr_t)user_data;
78    goHoverCallback(handle);
79}
80
81static GtkEventController* widget_add_hover_controller(GtkWidget* widget, uintptr_t handle) {
82    if (!widget) {
83        return NULL;
84    }
85    GtkEventController* motion = gtk_event_controller_motion_new();
86    if (!motion) {
87        return NULL;
88    }
89    gtk_event_controller_set_propagation_phase(motion, GTK_PHASE_CAPTURE);
90    g_signal_connect_data(motion, "enter", G_CALLBACK(motion_enter_cb), (gpointer)handle, NULL, 0);
91    gtk_widget_add_controller(widget, motion);
92    return motion;
93}
94
95static void widget_remove_controller(GtkWidget* widget, GtkEventController* controller) {
96    if (!widget || !controller) {
97        return;
98    }
99    gtk_widget_remove_controller(widget, controller);
100}
101
102static gboolean widget_get_bounds(GtkWidget* widget, double* x, double* y, double* width, double* height) {
103    if (!widget || !x || !y || !width || !height) {
104        return FALSE;
105    }
106    GtkWidget* root = GTK_WIDGET(gtk_widget_get_root(widget));
107    if (!root) {
108        return FALSE;
109    }
110    graphene_rect_t rect;
111    if (!gtk_widget_compute_bounds(widget, root, &rect)) {
112        return FALSE;
113    }
114    *x = rect.origin.x;
115    *y = rect.origin.y;
116    *width = rect.size.width;
117    *height = rect.size.height;
118    return TRUE;
119}
120
121// Widget validation helper to prevent crashes from invalid widget pointers
122static gboolean is_valid_widget(gpointer widget) {
123    return widget != NULL && GTK_IS_WIDGET(widget);
124}
125
126// Forward declarations for focus controller callbacks defined later in this file
127static void focus_enter_cb(GtkEventControllerFocus* controller, gpointer user_data);
128static void focus_leave_cb(GtkEventControllerFocus* controller, gpointer user_data);
129
130typedef struct {
131    GtkEventController* controller;
132    GtkWidget* widget;
133    gboolean is_attached;
134} ControllerTracker;
135
136static GHashTable* controller_registry = NULL;
137
138static void init_controller_registry(void) {
139    if (controller_registry == NULL) {
140        controller_registry = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_free);
141    }
142}
143
144static void controller_on_widget_destroy(GtkWidget* widget, gpointer user_data) {
145    (void)widget;
146    ControllerTracker* tracker = (ControllerTracker*)user_data;
147    if (!tracker) {
148        return;
149    }
150    tracker->is_attached = FALSE;
151}
152
153static GtkEventController* widget_add_focus_controller(GtkWidget* widget, uintptr_t nodePtr) {
154    if (!widget) {
155        return NULL;
156    }
157
158    init_controller_registry();
159
160    GtkEventController* focus = gtk_event_controller_focus_new();
161    if (!focus) {
162        return NULL;
163    }
164
165    gtk_event_controller_set_propagation_phase(focus, GTK_PHASE_CAPTURE);
166    g_signal_connect(focus, "enter", G_CALLBACK(focus_enter_cb), (gpointer)nodePtr);
167    g_signal_connect(focus, "leave", G_CALLBACK(focus_leave_cb), (gpointer)nodePtr);
168
169    ControllerTracker* tracker = g_new0(ControllerTracker, 1);
170    tracker->controller = focus;
171    tracker->widget = widget;
172    tracker->is_attached = TRUE;
173
174    g_hash_table_insert(controller_registry, focus, tracker);
175
176    g_signal_connect(widget, "destroy", G_CALLBACK(controller_on_widget_destroy), tracker);
177
178    gtk_widget_add_controller(widget, focus);
179    return focus;
180}
181
182static void widget_remove_focus_controller(GtkWidget* widget, GtkEventController* controller) {
183    if (!controller_registry || !controller) {
184        return;
185    }
186
187    ControllerTracker* tracker = g_hash_table_lookup(controller_registry, controller);
188    if (!tracker) {
189        return;
190    }
191
192    if (!tracker->is_attached) {
193        return;
194    }
195
196    if (!GTK_IS_WIDGET(widget)) {
197        tracker->is_attached = FALSE;
198        return;
199    }
200
201    GtkWidget* owner = gtk_event_controller_get_widget(controller);
202    if (GTK_IS_WIDGET(owner) && owner == widget) {
203        gtk_widget_remove_controller(widget, controller);
204    }
205
206    tracker->is_attached = FALSE;
207
208    // Remove from registry; GTK drops the final reference when removing controller
209    g_hash_table_remove(controller_registry, controller);
210}
211
212static GtkWidget* paned_get_start_child(GtkWidget* paned) {
213    if (!paned) return NULL;
214    return gtk_paned_get_start_child(GTK_PANED(paned));
215}
216
217static GtkWidget* paned_get_end_child(GtkWidget* paned) {
218    if (!paned) return NULL;
219    return gtk_paned_get_end_child(GTK_PANED(paned));
220}
221
222static void on_widget_destroyed(GtkWidget* widget, gpointer user_data) {
223    (void)user_data;
224    guint ref_count = widget ? G_OBJECT(widget)->ref_count : 0;
225    WORKSPACE_LOG("widget destroyed: %p ref_count=%u", widget, ref_count);
226}
227
228static void hook_destroy_signal(GtkWidget* widget) {
229    if (!widget) return;
230    g_signal_connect(widget, "destroy", G_CALLBACK(on_widget_destroyed), NULL);
231}
232
233static unsigned int widget_get_ref_count(GtkWidget* widget) {
234    if (!widget) {
235        return 0;
236    }
237    return G_OBJECT(widget)->ref_count;
238}
239
240// Widget realization helpers for proper WebView reparenting
241// Force widget realization and ensure it's ready for rendering
242static void ensure_widget_realized(GtkWidget* widget) {
243    if (!widget) return;
244
245    // Make widget visible first
246    gtk_widget_set_visible(widget, TRUE);
247
248    // For WebKitWebView, we need to ensure the widget is properly sized
249    gtk_widget_set_size_request(widget, 100, 100); // Minimum size to force allocation
250
251    // Queue a redraw to force the rendering context to reinitialize
252    gtk_widget_queue_draw(widget);
253    WORKSPACE_LOG("ensure_widget_realized: widget=%p", widget);
254}
255
256static void add_css_provider(const char* css) {
257    if (!css) {
258        return;
259    }
260    GtkCssProvider* provider = gtk_css_provider_new();
261    if (!provider) {
262        return;
263    }
264    gtk_css_provider_load_from_string(provider, css);
265    GdkDisplay* display = gdk_display_get_default();
266    if (display) {
267        gtk_style_context_add_provider_for_display(display, GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
268    }
269    g_object_unref(provider);
270}
271
272static void widget_add_css_class(GtkWidget* widget, const char* name) {
273    if (!widget || !name) return;
274    gtk_widget_add_css_class(widget, name);
275}
276
277static void widget_remove_css_class(GtkWidget* widget, const char* name) {
278    if (!widget || !name) return;
279    gtk_widget_remove_css_class(widget, name);
280}
281
282// GtkBox helpers for stacked panes
283static GtkWidget* box_new(GtkOrientation orientation, int spacing) {
284    return gtk_box_new(orientation, spacing);
285}
286
287static void box_append(GtkWidget* box, GtkWidget* child) {
288    if (!box || !child) return;
289    gtk_box_append(GTK_BOX(box), child);
290}
291
292static void box_prepend(GtkWidget* box, GtkWidget* child) {
293    if (!box || !child) return;
294    gtk_box_prepend(GTK_BOX(box), child);
295}
296
297static void box_remove(GtkWidget* box, GtkWidget* child) {
298    if (!box || !child) return;
299    gtk_box_remove(GTK_BOX(box), child);
300}
301
302static void box_insert_child_after(GtkWidget* box, GtkWidget* child, GtkWidget* sibling) {
303    if (!box || !child) return;
304    gtk_box_insert_child_after(GTK_BOX(box), child, sibling);
305}
306
307// Widget visibility helpers for stacked panes
308static void widget_set_visible(GtkWidget* widget, gboolean visible) {
309    if (!widget) return;
310    gtk_widget_set_visible(widget, visible);
311}
312
313
314static gboolean widget_get_visible(GtkWidget* widget) {
315    if (!widget) return FALSE;
316    return gtk_widget_get_visible(widget);
317}
318
319static void widget_hide(GtkWidget* widget) {
320    if (!widget) return;
321    gtk_widget_set_visible(widget, FALSE);
322}
323
324static void widget_show(GtkWidget* widget) {
325    if (!widget) return;
326    gtk_widget_set_visible(widget, TRUE);
327}
328
329// workspace_gtk_test_init removed - moved to test files only
330
331// Label helpers for title bars
332static GtkWidget* label_new(const char* text) {
333    return gtk_label_new(text);
334}
335
336static void label_set_text(GtkWidget* label, const char* text) {
337    if (!label || !text) return;
338    gtk_label_set_text(GTK_LABEL(label), text);
339}
340
341static const char* label_get_text(GtkWidget* label) {
342    if (!label) return NULL;
343    return gtk_label_get_text(GTK_LABEL(label));
344}
345
346static void label_set_ellipsize(GtkWidget* label, int mode) {
347    if (!label) return;
348    gtk_label_set_ellipsize(GTK_LABEL(label), (PangoEllipsizeMode)mode);
349}
350
351static void label_set_max_width_chars(GtkWidget* label, int n_chars) {
352    if (!label) return;
353    gtk_label_set_max_width_chars(GTK_LABEL(label), n_chars);
354}
355
356// GTK4 Focus Controller helpers for focus state machine
357static void focus_enter_cb(GtkEventControllerFocus* controller, gpointer user_data) {
358    (void)controller;
359    uintptr_t nodePtr = (uintptr_t)user_data;
360    goFocusEnterCallback(nodePtr);
361}
362
363static void focus_leave_cb(GtkEventControllerFocus* controller, gpointer user_data) {
364    (void)controller;
365    uintptr_t nodePtr = (uintptr_t)user_data;
366    goFocusLeaveCallback(nodePtr);
367}
368
369// Widget allocation helper using modern GTK4 API
370static void widget_get_allocation_modern(GtkWidget* widget, int* x, int* y, int* width, int* height) {
371    if (!widget || !x || !y || !width || !height) {
372        *x = *y = *width = *height = 0;
373        return;
374    }
375
376    graphene_rect_t bounds;
377    // Compute bounds relative to the widget itself (pass widget as target)
378    if (gtk_widget_compute_bounds(widget, widget, &bounds)) {
379        // Extract integer coordinates from the graphene rect
380        *x = (int)bounds.origin.x;
381        *y = (int)bounds.origin.y;
382        *width = (int)bounds.size.width;
383        *height = (int)bounds.size.height;
384    } else {
385        // Fallback if compute_bounds fails
386        *x = *y = 0;
387        *width = gtk_widget_get_width(widget);
388        *height = gtk_widget_get_height(widget);
389    }
390}
391
392// GtkImage helpers for favicon display
393static GtkWidget* image_new(void) {
394    return gtk_image_new();
395}
396
397static GtkWidget* image_new_from_file(const char* filename) {
398    if (!filename) return NULL;
399    return gtk_image_new_from_file(filename);
400}
401
402static void image_set_from_file(GtkWidget* image, const char* filename) {
403    if (!image || !filename) return;
404    gtk_image_set_from_file(GTK_IMAGE(image), filename);
405}
406
407static void image_set_pixel_size(GtkWidget* image, int pixel_size) {
408    if (!image) return;
409    gtk_image_set_pixel_size(GTK_IMAGE(image), pixel_size);
410}
411*/
412import "C"
413
414import (
415	"log"
416	"sync"
417	"unsafe"
418)
419
420// WidgetBounds captures absolute widget bounds relative to the toplevel window.
421type WidgetBounds struct {
422	X      float64
423	Y      float64
424	Width  float64
425	Height float64
426}
427
428// FocusCallbacks holds the enter and leave callbacks for focus events
429type FocusCallbacks struct {
430	OnEnter func()
431	OnLeave func()
432}
433
434// WidgetAllocation represents a widget's allocation (position and size)
435type WidgetAllocation struct {
436	X      int
437	Y      int
438	Width  int
439	Height int
440}
441
442var (
443	hoverMu          sync.Mutex
444	hoverCallbacks           = make(map[uintptr]func())
445	hoverControllers         = make(map[uintptr]uintptr)
446	nextHoverID      uintptr = 1
447	// gtkTestInitOnce removed - no longer needed
448
449	// Focus controller management
450	focusMu          sync.Mutex
451	focusCallbacks           = make(map[uintptr]FocusCallbacks)
452	focusControllers         = make(map[uintptr]uintptr)
453	nextFocusID      uintptr = 1
454
455	// Title bar click management
456	titleBarMu       sync.Mutex
457	titleBarCallbacks = make(map[uintptr]func())
458	nextTitleBarID   uintptr = 1
459
460	mainThreadInitialized bool
461)
462
463// InitMainThread should be called once during gtk_init to store the main thread ID.
464func InitMainThread() {
465	if !mainThreadInitialized {
466		C.store_main_thread_id()
467		mainThreadInitialized = true
468	}
469}
470
471// IsMainThread returns true if the current goroutine is running on the GTK main thread.
472func IsMainThread() bool {
473	if !mainThreadInitialized {
474		return false
475	}
476	return C.is_main_thread() == 1
477}
478
479func widgetIsValid(widget uintptr) bool {
480	if widget == 0 {
481		return false
482	}
483	return C.is_valid_widget(C.gpointer(unsafe.Pointer(widget))) != 0
484}
485
486func widgetRefCount(widget uintptr) uint {
487	if widget == 0 || !widgetIsValid(widget) {
488		return 0
489	}
490	return uint(C.widget_get_ref_count((*C.GtkWidget)(unsafe.Pointer(widget))))
491}
492
493// WidgetIsValid exposes GTK_IS_WIDGET checks for diagnostics in Go.
494func WidgetIsValid(widget uintptr) bool {
495	return widgetIsValid(widget)
496}
497
498// IsPaned checks if a widget is a GtkPaned container.
499func IsPaned(widget uintptr) bool {
500	if widget == 0 {
501		return false
502	}
503	return C.is_paned_widget((*C.GtkWidget)(unsafe.Pointer(widget))) != 0
504}
505
506// IsBox checks if a widget is a GtkBox container.
507func IsBox(widget uintptr) bool {
508	if widget == 0 {
509		return false
510	}
511	return C.is_box_widget((*C.GtkWidget)(unsafe.Pointer(widget))) != 0
512}
513
514// WidgetRefCount exposes the widget reference count for debugging.
515func WidgetRefCount(widget uintptr) uint {
516	return widgetRefCount(widget)
517}
518
519// WidgetHookDestroy attaches a destroy signal handler for diagnostics (debug builds).
520func WidgetHookDestroy(widget uintptr) {
521	if widget == 0 || !widgetIsValid(widget) {
522		return
523	}
524	C.hook_destroy_signal((*C.GtkWidget)(unsafe.Pointer(widget)))
525}
526
527// AddCSSProvider installs application-level CSS rules.
528func AddCSSProvider(css string) {
529	if css == "" {
530		return
531	}
532	cstr := C.CString(css)
533	defer C.free(unsafe.Pointer(cstr))
534	C.add_css_provider(cstr)
535}
536
537// WidgetAddCSSClass attaches a CSS class to a widget.
538func WidgetAddCSSClass(widget uintptr, class string) {
539	if widget == 0 || class == "" || !widgetIsValid(widget) {
540		return
541	}
542	cstr := C.CString(class)
543	defer C.free(unsafe.Pointer(cstr))
544	C.widget_add_css_class((*C.GtkWidget)(unsafe.Pointer(widget)), cstr)
545}
546
547// WidgetRemoveCSSClass removes a CSS class from a widget.
548func WidgetRemoveCSSClass(widget uintptr, class string) {
549	if widget == 0 || class == "" || !widgetIsValid(widget) {
550		return
551	}
552	cstr := C.CString(class)
553	defer C.free(unsafe.Pointer(cstr))
554	C.widget_remove_css_class((*C.GtkWidget)(unsafe.Pointer(widget)), cstr)
555}
556
557// Orientation mirrors GtkOrientation for workspace layout helpers.
558type Orientation int
559
560const (
561	OrientationHorizontal Orientation = Orientation(C.GTK_ORIENTATION_HORIZONTAL)
562	OrientationVertical   Orientation = Orientation(C.GTK_ORIENTATION_VERTICAL)
563)
564
565func goBool(b bool) C.gboolean {
566	if b {
567		return C.gboolean(1)
568	}
569	return C.gboolean(0)
570}
571
572// NewPaned constructs a GtkPaned widget for the given orientation.
573func NewPaned(orientation Orientation) uintptr {
574	paned := C.gtk_paned_new(C.GtkOrientation(orientation))
575	return uintptr(unsafe.Pointer(paned))
576}
577
578// PanedSetStartChild assigns the start child of a GtkPaned.
579func PanedSetStartChild(paned uintptr, child uintptr) {
580	if child != 0 {
581		// Check if widget already has a parent and force unparent if needed
582		parent := C.gtk_widget_get_parent((*C.GtkWidget)(unsafe.Pointer(child)))
583		if parent != nil {
584			C.gtk_widget_unparent((*C.GtkWidget)(unsafe.Pointer(child)))
585		}
586	}
587	C.gtk_paned_set_start_child((*C.GtkPaned)(unsafe.Pointer(paned)), (*C.GtkWidget)(unsafe.Pointer(child)))
588}
589
590// PanedSetEndChild assigns the end child of a GtkPaned.
591func PanedSetEndChild(paned uintptr, child uintptr) {
592	if child != 0 {
593		// Check if widget already has a parent and force unparent if needed
594		parent := C.gtk_widget_get_parent((*C.GtkWidget)(unsafe.Pointer(child)))
595		if parent != nil {
596			C.gtk_widget_unparent((*C.GtkWidget)(unsafe.Pointer(child)))
597		}
598	}
599	C.gtk_paned_set_end_child((*C.GtkPaned)(unsafe.Pointer(paned)), (*C.GtkWidget)(unsafe.Pointer(child)))
600}
601
602// PanedGetStartChild returns the start child pointer for diagnostics.
603func PanedGetStartChild(paned uintptr) uintptr {
604	if paned == 0 {
605		return 0
606	}
607	child := C.paned_get_start_child((*C.GtkWidget)(unsafe.Pointer(paned)))
608	return uintptr(unsafe.Pointer(child))
609}
610
611// PanedGetEndChild returns the end child pointer for diagnostics.
612func PanedGetEndChild(paned uintptr) uintptr {
613	if paned == 0 {
614		return 0
615	}
616	child := C.paned_get_end_child((*C.GtkWidget)(unsafe.Pointer(paned)))
617	return uintptr(unsafe.Pointer(child))
618}
619
620// PanedSetResizeStart configures whether the start child should expand when the paned is resized.
621func PanedSetResizeStart(paned uintptr, resize bool) {
622	C.gtk_paned_set_resize_start_child((*C.GtkPaned)(unsafe.Pointer(paned)), goBool(resize))
623}
624
625// PanedSetResizeEnd configures whether the end child should expand when the paned is resized.
626func PanedSetResizeEnd(paned uintptr, resize bool) {
627	C.gtk_paned_set_resize_end_child((*C.GtkPaned)(unsafe.Pointer(paned)), goBool(resize))
628}
629
630// PanedSetPosition sets the pixel position of the divider.
631func PanedSetPosition(paned uintptr, pos int) {
632	if paned == 0 {
633		return
634	}
635	C.gtk_paned_set_position((*C.GtkPaned)(unsafe.Pointer(paned)), C.int(pos))
636}
637
638// PanedGetPosition gets the current position of the divider in a GtkPaned
639func PanedGetPosition(paned uintptr) int {
640	if paned == 0 {
641		return 0
642	}
643	if !widgetIsValid(paned) {
644		return 0
645	}
646	return int(C.gtk_paned_get_position((*C.GtkPaned)(unsafe.Pointer(paned))))
647}
648
649// WidgetUnparent detaches a widget from its current parent.
650func WidgetUnparent(widget uintptr) {
651	if ok := WidgetUnparentChecked(widget); !ok {
652		log.Printf("[workspace] WidgetUnparent skipped for widget=%#x", widget)
653	}
654}
655
656// WidgetUnparentChecked safely detaches a widget from its parent if still valid.
657//
658// Return semantics:
659//   - Returns false if the widget pointer is zero or not a valid GTK widget (invalid input).
660//   - Returns true if the widget has no parent (already detached).
661//   - Returns true if the widget was successfully unparented (parent changed or is now nil).
662//   - Returns false if the widget is still valid and the parent did not change (unparent failed).
663//
664// Callers can use the return value to distinguish between invalid input, already-detached widgets, and successful unparenting.
665func WidgetUnparentChecked(widget uintptr) bool {
666	if widget == 0 {
667		log.Printf("[workspace] WidgetUnparentChecked: widget=0")
668		return false
669	}
670
671	if !widgetIsValid(widget) {
672		log.Printf("[workspace] WidgetUnparentChecked: widget=%#x invalid before unparent", widget)
673		return false
674	}
675
676	parentBefore := WidgetGetParent(widget)
677	if parentBefore == 0 {
678		log.Printf("[workspace] WidgetUnparentChecked: widget=%#x has no parent", widget)
679		return true
680	}
681
682	refBefore := widgetRefCount(widget)
683	log.Printf("[workspace] WidgetUnparentChecked: widget=%#x parent=%#x ref_before=%d", widget, parentBefore, refBefore)
684
685	C.gtk_widget_unparent((*C.GtkWidget)(unsafe.Pointer(widget)))
686
687	if !widgetIsValid(widget) {
688		log.Printf("[workspace] WidgetUnparentChecked: widget=%#x invalid after unparent", widget)
689		return true
690	}
691
692	parentAfter := WidgetGetParent(widget)
693	refAfter := widgetRefCount(widget)
694	log.Printf("[workspace] WidgetUnparentChecked: widget=%#x parent_after=%#x ref_after=%d", widget, parentAfter, refAfter)
695
696	return parentAfter == 0 || parentAfter != parentBefore
697}
698
699// WidgetSetHExpand configures horizontal expand for a widget.
700func WidgetSetHExpand(widget uintptr, expand bool) {
701	if widget == 0 {
702		return
703	}
704	log.Printf("[workspace] WidgetSetHExpand widget=%#x expand=%v", widget, expand)
705	C.gtk_widget_set_hexpand((*C.GtkWidget)(unsafe.Pointer(widget)), goBool(expand))
706}
707
708func WidgetGetParent(widget uintptr) uintptr {
709	if widget == 0 {
710		log.Printf("[workspace] WidgetGetParent: widget=0")
711		return 0
712	}
713	if !widgetIsValid(widget) {
714		log.Printf("[workspace] WidgetGetParent: widget=%#x invalid", widget)
715		return 0
716	}
717	parent := C.gtk_widget_get_parent((*C.GtkWidget)(unsafe.Pointer(widget)))
718	// Reduced logging: only debug mode shows individual widget operations
719	return uintptr(unsafe.Pointer(parent))
720}
721
722// WidgetSetVExpand configures vertical expand for a widget.
723func WidgetSetVExpand(widget uintptr, expand bool) {
724	if widget == 0 {
725		return
726	}
727	log.Printf("[workspace] WidgetSetVExpand widget=%#x expand=%v", widget, expand)
728	C.gtk_widget_set_vexpand((*C.GtkWidget)(unsafe.Pointer(widget)), goBool(expand))
729}
730
731// WidgetSetSizeRequest sets the minimum size request for a widget.
732// Use -1 for width or height to unset that dimension.
733func WidgetSetSizeRequest(widget uintptr, width, height int) {
734	if widget == 0 || !widgetIsValid(widget) {
735		return
736	}
737	log.Printf("[workspace] WidgetSetSizeRequest widget=%#x width=%d height=%d", widget, width, height)
738	C.gtk_widget_set_size_request((*C.GtkWidget)(unsafe.Pointer(widget)), C.int(width), C.int(height))
739}
740
741// WidgetResetSizeRequest clears explicit size constraints so GTK can recalculate allocation.
742func WidgetResetSizeRequest(widget uintptr) {
743	if widget == 0 || !widgetIsValid(widget) {
744		return
745	}
746	C.gtk_widget_set_size_request((*C.GtkWidget)(unsafe.Pointer(widget)), C.int(-1), C.int(-1))
747}
748
749// WidgetShow makes the widget visible.
750func WidgetShow(widget uintptr) {
751	if widget == 0 {
752		return
753	}
754	log.Printf("[workspace] WidgetShow widget=%#x", widget)
755	C.gtk_widget_set_visible((*C.GtkWidget)(unsafe.Pointer(widget)), C.gboolean(1))
756}
757
758// WidgetGrabFocus focuses the widget if possible.
759func WidgetGrabFocus(widget uintptr) {
760	if widget == 0 {
761		return
762	}
763	if !widgetIsValid(widget) {
764		log.Printf("[workspace] WidgetGrabFocus skipped invalid widget=%#x", widget)
765		return
766	}
767	// Reduced logging: focus grab logged only in consolidated focus operation
768	C.gtk_widget_grab_focus((*C.GtkWidget)(unsafe.Pointer(widget)))
769}
770
771// WidgetSetFocusChild sets or clears the focus child of a widget.
772// Pass 0 as child to clear the focus child (prevents GTK focus chain warnings during reparenting).
773func WidgetSetFocusChild(widget uintptr, child uintptr) {
774	if widget == 0 {
775		return
776	}
777	if !widgetIsValid(widget) {
778		log.Printf("[workspace] WidgetSetFocusChild skipped invalid widget=%#x", widget)
779		return
780	}
781	var childPtr *C.GtkWidget
782	if child != 0 {
783		if !widgetIsValid(child) {
784			log.Printf("[workspace] WidgetSetFocusChild skipped invalid child=%#x", child)
785			return
786		}
787		childPtr = (*C.GtkWidget)(unsafe.Pointer(child))
788	}
789	C.gtk_widget_set_focus_child((*C.GtkWidget)(unsafe.Pointer(widget)), childPtr)
790	log.Printf("[workspace] WidgetSetFocusChild widget=%#x child=%#x", widget, child)
791}
792
793// WidgetRef increments the reference count to keep the widget alive during reparenting.
794func WidgetRef(widget uintptr) bool {
795	if widget == 0 {
796		return false
797	}
798	if !widgetIsValid(widget) {
799		log.Printf("[workspace] WidgetRef skipped invalid widget=%#x", widget)
800		return false
801	}
802	before := widgetRefCount(widget)
803	log.Printf("[workspace] WidgetRef widget=%#x ref_before=%d", widget, before)
804	C.g_object_ref(C.gpointer(unsafe.Pointer(widget)))
805	after := widgetRefCount(widget)
806	log.Printf("[workspace] WidgetRef widget=%#x ref_after=%d", widget, after)
807	return true
808}
809
810// WidgetUnref releases a previously held widget reference.
811func WidgetUnref(widget uintptr) {
812	if widget == 0 {
813		return
814	}
815	if !widgetIsValid(widget) {
816		log.Printf("[workspace] WidgetUnref skipped invalid widget=%#x", widget)
817		return
818	}
819	before := widgetRefCount(widget)
820	log.Printf("[workspace] WidgetUnref widget=%#x ref_before=%d", widget, before)
821	C.g_object_unref(C.gpointer(unsafe.Pointer(widget)))
822	if widgetIsValid(widget) {
823		after := widgetRefCount(widget)
824		log.Printf("[workspace] WidgetUnref widget=%#x ref_after=%d", widget, after)
825	} else {
826		log.Printf("[workspace] WidgetUnref widget=%#x now invalid", widget)
827	}
828}
829
830// WidgetQueueAllocate queues an allocation on the widget.
831func WidgetQueueAllocate(widget uintptr) {
832	if widget == 0 {
833		return
834	}
835	log.Printf("[workspace] WidgetQueueAllocate widget=%#x", widget)
836	C.gtk_widget_queue_allocate((*C.GtkWidget)(unsafe.Pointer(widget)))
837}
838
839// WidgetQueueResize forces GTK to recalculate the widget's layout and size allocation.
840// This is useful when a widget's size requirements have changed (e.g., after adding children).
841func WidgetQueueResize(widget uintptr) {
842	if widget == 0 {
843		return
844	}
845	if !widgetIsValid(widget) {
846		log.Printf("[workspace] WidgetQueueResize skipped invalid widget=%#x", widget)
847		return
848	}
849	log.Printf("[workspace] WidgetQueueResize widget=%#x", widget)
850	C.gtk_widget_queue_resize((*C.GtkWidget)(unsafe.Pointer(widget)))
851}
852
853// WidgetRealizeInContainer ensures a widget is properly realized within a container
854func WidgetRealizeInContainer(widget uintptr) {
855	if widget == 0 {
856		return
857	}
858	// Reduced logging: widget realization logged only in consolidated focus operation
859	C.ensure_widget_realized((*C.GtkWidget)(unsafe.Pointer(widget)))
860}
861
862// WidgetAddHoverHandler registers a hover callback for the given widget and
863// returns a token that can be used to detach it later.
864func WidgetAddHoverHandler(widget uintptr, fn func()) uintptr {
865	if widget == 0 || fn == nil || !widgetIsValid(widget) {
866		return 0
867	}
868	hoverMu.Lock()
869	token := nextHoverID
870	nextHoverID++
871	hoverCallbacks[token] = fn
872	hoverMu.Unlock()
873
874	controller := C.widget_add_hover_controller((*C.GtkWidget)(unsafe.Pointer(widget)), C.uintptr_t(token))
875	if controller == nil {
876		hoverMu.Lock()
877		delete(hoverCallbacks, token)
878		hoverMu.Unlock()
879		return 0
880	}
881
882	hoverMu.Lock()
883	hoverControllers[token] = uintptr(unsafe.Pointer(controller))
884	hoverMu.Unlock()
885	return token
886}
887
888// WidgetRemoveHoverHandler removes a previously registered hover callback.
889func WidgetRemoveHoverHandler(widget uintptr, token uintptr) {
890	if token == 0 {
891		return
892	}
893
894	hoverMu.Lock()
895	controller := hoverControllers[token]
896	delete(hoverCallbacks, token)
897	delete(hoverControllers, token)
898	hoverMu.Unlock()
899
900	if controller == 0 {
901		return
902	}
903
904	if widget == 0 || !widgetIsValid(widget) {
905		return
906	}
907
908	C.widget_remove_controller((*C.GtkWidget)(unsafe.Pointer(widget)), (*C.GtkEventController)(unsafe.Pointer(controller)))
909}
910
911// WidgetAttachClickHandler attaches a click handler to any widget (for title bars)
912func WidgetAttachClickHandler(widget uintptr, fn func()) uintptr {
913	if widget == 0 || fn == nil || !widgetIsValid(widget) {
914		return 0
915	}
916	titleBarMu.Lock()
917	token := nextTitleBarID
918	nextTitleBarID++
919	titleBarCallbacks[token] = fn
920	titleBarMu.Unlock()
921
922	C.widget_attach_click_handler((*C.GtkWidget)(unsafe.Pointer(widget)), C.uintptr_t(token))
923
924	return token
925}
926
927// WidgetGetBounds returns the absolute bounds of the widget relative to the root window.
928func WidgetGetBounds(widget uintptr) (WidgetBounds, bool) {
929	if widget == 0 || !widgetIsValid(widget) {
930		return WidgetBounds{}, false
931	}
932
933	var x, y, width, height C.double
934	if C.widget_get_bounds((*C.GtkWidget)(unsafe.Pointer(widget)), (*C.double)(&x), (*C.double)(&y), (*C.double)(&width), (*C.double)(&height)) == 0 {
935		return WidgetBounds{}, false
936	}
937
938	return WidgetBounds{
939		X:      float64(x),
940		Y:      float64(y),
941		Width:  float64(width),
942		Height: float64(height),
943	}, true
944}
945
946// Global storage for idle callbacks
947var (
948	idleCallbacks         = make(map[uintptr]func() bool)
949	nextIdleID    uintptr = 1
950)
951
952// IdleAdd schedules a function to be called when the main loop is idle.
953func IdleAdd(fn func() bool) uintptr {
954	if fn == nil {
955		return 0
956	}
957	id := nextIdleID
958	nextIdleID++
959	idleCallbacks[id] = fn
960	if C.add_idle_callback(C.uintptr_t(id)) == 0 {
961		delete(idleCallbacks, id)
962		return 0
963	}
964	return id
965}
966
967// IdleRemove cancels a previously scheduled idle callback.
968func IdleRemove(handle uintptr) {
969	if handle == 0 {
970		return
971	}
972	C.remove_idle_callback(C.uintptr_t(handle))
973	delete(idleCallbacks, handle)
974}
975
976// IterateMainLoop processes a single GTK main loop iteration and reports if work was handled.
977func IterateMainLoop() bool {
978	return C.iterate_main_loop() == 1
979}
980
981//export goIdleCallback
982func goIdleCallback(handle uintptr) C.gboolean {
983	if fn, ok := idleCallbacks[handle]; ok {
984		delete(idleCallbacks, handle) // Clean up after calling
985		if fn() {
986			return C.gboolean(1) // G_SOURCE_CONTINUE
987		}
988	}
989	return C.gboolean(0) // G_SOURCE_REMOVE
990}
991
992//export goHoverCallback
993func goHoverCallback(handle C.uintptr_t) {
994	token := uintptr(handle)
995	hoverMu.Lock()
996	fn := hoverCallbacks[token]
997	hoverMu.Unlock()
998	if fn != nil {
999		fn()
1000	}
1001}
1002
1003//export goTitleBarClickCallback
1004func goTitleBarClickCallback(handle C.uintptr_t) {
1005	token := uintptr(handle)
1006	titleBarMu.Lock()
1007	fn := titleBarCallbacks[token]
1008	titleBarMu.Unlock()
1009	if fn != nil {
1010		fn()
1011	}
1012}
1013
1014// GtkBox functions for stacked panes
1015
1016// NewBox creates a new GtkBox widget with the specified orientation and spacing.
1017func NewBox(orientation Orientation, spacing int) uintptr {
1018	box := C.box_new(C.GtkOrientation(orientation), C.int(spacing))
1019	return uintptr(unsafe.Pointer(box))
1020}
1021
1022// BoxAppend adds a child widget to the end of a GtkBox.
1023func BoxAppend(box uintptr, child uintptr) {
1024	if box == 0 || child == 0 {
1025		return
1026	}
1027	log.Printf("[workspace] BoxAppend box=%#x child=%#x", box, child)
1028	C.box_append((*C.GtkWidget)(unsafe.Pointer(box)), (*C.GtkWidget)(unsafe.Pointer(child)))
1029}
1030
1031// BoxPrepend adds a child widget to the beginning of a GtkBox.
1032func BoxPrepend(box uintptr, child uintptr) {
1033	if box == 0 || child == 0 {
1034		return
1035	}
1036	log.Printf("[workspace] BoxPrepend box=%#x child=%#x", box, child)
1037	C.box_prepend((*C.GtkWidget)(unsafe.Pointer(box)), (*C.GtkWidget)(unsafe.Pointer(child)))
1038}
1039
1040// BoxRemove removes a child widget from a GtkBox.
1041func BoxRemove(box uintptr, child uintptr) {
1042	if box == 0 || child == 0 {
1043		return
1044	}
1045	log.Printf("[workspace] BoxRemove box=%#x child=%#x", box, child)
1046	C.box_remove((*C.GtkWidget)(unsafe.Pointer(box)), (*C.GtkWidget)(unsafe.Pointer(child)))
1047}
1048
1049// BoxInsertChildAfter inserts a child widget after a sibling in a GtkBox.
1050func BoxInsertChildAfter(box uintptr, child uintptr, sibling uintptr) {
1051	if box == 0 || child == 0 {
1052		return
1053	}
1054	log.Printf("[workspace] BoxInsertChildAfter box=%#x child=%#x sibling=%#x", box, child, sibling)
1055	C.box_insert_child_after((*C.GtkWidget)(unsafe.Pointer(box)), (*C.GtkWidget)(unsafe.Pointer(child)), (*C.GtkWidget)(unsafe.Pointer(sibling)))
1056}
1057
1058// Widget visibility functions for stacked panes
1059
1060// WidgetSetVisible sets the visibility state of a widget.
1061func WidgetSetVisible(widget uintptr, visible bool) {
1062	if widget == 0 {
1063		return
1064	}
1065	log.Printf("[workspace] WidgetSetVisible widget=%#x visible=%v", widget, visible)
1066	C.widget_set_visible((*C.GtkWidget)(unsafe.Pointer(widget)), goBool(visible))
1067}
1068
1069// WidgetGetVisible returns the visibility state of a widget.
1070func WidgetGetVisible(widget uintptr) bool {
1071	if widget == 0 {
1072		return false
1073	}
1074	return C.widget_get_visible((*C.GtkWidget)(unsafe.Pointer(widget))) != 0
1075}
1076
1077// WidgetHide hides a widget.
1078func WidgetHide(widget uintptr) {
1079	if widget == 0 {
1080		return
1081	}
1082	log.Printf("[workspace] WidgetHide widget=%#x", widget)
1083	C.widget_hide((*C.GtkWidget)(unsafe.Pointer(widget)))
1084}
1085
1086// Label functions for title bars
1087
1088// NewLabel creates a new GtkLabel widget with the specified text.
1089func NewLabel(text string) uintptr {
1090	cstr := C.CString(text)
1091	defer C.free(unsafe.Pointer(cstr))
1092	label := C.label_new(cstr)
1093	return uintptr(unsafe.Pointer(label))
1094}
1095
1096// LabelSetText sets the text of a GtkLabel.
1097func LabelSetText(label uintptr, text string) {
1098	if label == 0 {
1099		return
1100	}
1101	cstr := C.CString(text)
1102	defer C.free(unsafe.Pointer(cstr))
1103	C.label_set_text((*C.GtkWidget)(unsafe.Pointer(label)), cstr)
1104}
1105
1106// LabelGetText returns the text of a GtkLabel.
1107func LabelGetText(label uintptr) string {
1108	if label == 0 {
1109		return ""
1110	}
1111	cstr := C.label_get_text((*C.GtkWidget)(unsafe.Pointer(label)))
1112	if cstr == nil {
1113		return ""
1114	}
1115	return C.GoString(cstr)
1116}
1117
1118// EllipsizeMode represents PangoEllipsizeMode values.
1119type EllipsizeMode int
1120
1121const (
1122	EllipsizeNone   EllipsizeMode = 0 // PANGO_ELLIPSIZE_NONE
1123	EllipsizeStart  EllipsizeMode = 1 // PANGO_ELLIPSIZE_START
1124	EllipsizeMiddle EllipsizeMode = 2 // PANGO_ELLIPSIZE_MIDDLE
1125	EllipsizeEnd    EllipsizeMode = 3 // PANGO_ELLIPSIZE_END
1126)
1127
1128// LabelSetEllipsize sets the ellipsization mode for a GtkLabel.
1129func LabelSetEllipsize(label uintptr, mode EllipsizeMode) {
1130	if label == 0 {
1131		return
1132	}
1133	C.label_set_ellipsize((*C.GtkWidget)(unsafe.Pointer(label)), C.int(mode))
1134}
1135
1136// LabelSetMaxWidthChars sets the maximum width in characters for a GtkLabel.
1137func LabelSetMaxWidthChars(label uintptr, nChars int) {
1138	if label == 0 {
1139		return
1140	}
1141	C.label_set_max_width_chars((*C.GtkWidget)(unsafe.Pointer(label)), C.int(nChars))
1142}
1143
1144// WidgetWaitForDraw is deprecated - GTK4 handles widget operations automatically
1145// This function is kept for compatibility but does nothing in production
1146func WidgetWaitForDraw(widget uintptr) {
1147	// GTK4 documentation states: never force a redraw
1148	// Widget operations are handled automatically by the GTK4 main loop
1149	// This function is a no-op in production code
1150}
1151
1152// WidgetQueueDraw safely queues a redraw operation (GTK4 recommended approach)
1153func WidgetQueueDraw(widget uintptr) {
1154	if widget == 0 || !widgetIsValid(widget) {
1155		return
1156	}
1157	C.gtk_widget_queue_draw((*C.GtkWidget)(unsafe.Pointer(widget)))
1158}
1159
1160// WidgetHasCSSClass checks if a widget has a specific CSS class
1161func WidgetHasCSSClass(widget uintptr, class string) bool {
1162	if widget == 0 || class == "" || !widgetIsValid(widget) {
1163		return false
1164	}
1165	cstr := C.CString(class)
1166	defer C.free(unsafe.Pointer(cstr))
1167	result := C.gtk_widget_has_css_class((*C.GtkWidget)(unsafe.Pointer(widget)), cstr)
1168	return result != 0
1169}
1170
1171// Widget margin functions for creating visual borders
1172
1173// WidgetSetMargin sets all margins (top, right, bottom, left) to the same value
1174func WidgetSetMargin(widget uintptr, margin int) {
1175	if widget == 0 || !widgetIsValid(widget) {
1176		return
1177	}
1178	w := (*C.GtkWidget)(unsafe.Pointer(widget))
1179	C.gtk_widget_set_margin_top(w, C.int(margin))
1180	C.gtk_widget_set_margin_bottom(w, C.int(margin))
1181	C.gtk_widget_set_margin_start(w, C.int(margin))
1182	C.gtk_widget_set_margin_end(w, C.int(margin))
1183}
1184
1185// WidgetSetMarginTop sets the top margin
1186func WidgetSetMarginTop(widget uintptr, margin int) {
1187	if widget == 0 || !widgetIsValid(widget) {
1188		return
1189	}
1190	C.gtk_widget_set_margin_top((*C.GtkWidget)(unsafe.Pointer(widget)), C.int(margin))
1191}
1192
1193// WidgetSetMarginBottom sets the bottom margin
1194func WidgetSetMarginBottom(widget uintptr, margin int) {
1195	if widget == 0 || !widgetIsValid(widget) {
1196		return
1197	}
1198	C.gtk_widget_set_margin_bottom((*C.GtkWidget)(unsafe.Pointer(widget)), C.int(margin))
1199}
1200
1201// WidgetSetMarginStart sets the start (left in LTR) margin
1202func WidgetSetMarginStart(widget uintptr, margin int) {
1203	if widget == 0 || !widgetIsValid(widget) {
1204		return
1205	}
1206	C.gtk_widget_set_margin_start((*C.GtkWidget)(unsafe.Pointer(widget)), C.int(margin))
1207}
1208
1209// WidgetSetMarginEnd sets the end (right in LTR) margin
1210func WidgetSetMarginEnd(widget uintptr, margin int) {
1211	if widget == 0 || !widgetIsValid(widget) {
1212		return
1213	}
1214	C.gtk_widget_set_margin_end((*C.GtkWidget)(unsafe.Pointer(widget)), C.int(margin))
1215}
1216
1217// GTK4 Focus Controller functions for focus state machine
1218
1219// WidgetAddFocusController registers focus callbacks for the given widget and
1220// returns a token that can be used to detach it later.
1221func WidgetAddFocusController(widget uintptr, onEnter, onLeave func()) uintptr {
1222	if widget == 0 || !widgetIsValid(widget) {
1223		return 0
1224	}
1225
1226	focusMu.Lock()
1227	token := nextFocusID
1228	nextFocusID++
1229
1230	callbacks := FocusCallbacks{
1231		OnEnter: onEnter,
1232		OnLeave: onLeave,
1233	}
1234	focusCallbacks[token] = callbacks
1235	focusMu.Unlock()
1236
1237	controller := C.widget_add_focus_controller((*C.GtkWidget)(unsafe.Pointer(widget)), C.uintptr_t(token))
1238	if controller == nil {
1239		focusMu.Lock()
1240		delete(focusCallbacks, token)
1241		focusMu.Unlock()
1242		return 0
1243	}
1244
1245	focusMu.Lock()
1246	focusControllers[token] = uintptr(unsafe.Pointer(controller))
1247	focusMu.Unlock()
1248	return token
1249}
1250
1251// WidgetRemoveFocusController removes a previously registered focus controller.
1252func WidgetRemoveFocusController(widget uintptr, token uintptr) {
1253	if token == 0 {
1254		return
1255	}
1256
1257	focusMu.Lock()
1258	controller := focusControllers[token]
1259	delete(focusCallbacks, token)
1260	delete(focusControllers, token)
1261	focusMu.Unlock()
1262
1263	if controller == 0 {
1264		return
1265	}
1266
1267	if widget == 0 || !widgetIsValid(widget) {
1268		return
1269	}
1270
1271	C.widget_remove_focus_controller((*C.GtkWidget)(unsafe.Pointer(widget)), (*C.GtkEventController)(unsafe.Pointer(controller)))
1272}
1273
1274//export goFocusEnterCallback
1275func goFocusEnterCallback(handle C.uintptr_t) {
1276	token := uintptr(handle)
1277	focusMu.Lock()
1278	callbacks := focusCallbacks[token]
1279	focusMu.Unlock()
1280
1281	if callbacks.OnEnter != nil {
1282		callbacks.OnEnter()
1283	}
1284}
1285
1286//export goFocusLeaveCallback
1287func goFocusLeaveCallback(handle C.uintptr_t) {
1288	token := uintptr(handle)
1289	focusMu.Lock()
1290	callbacks := focusCallbacks[token]
1291	focusMu.Unlock()
1292
1293	if callbacks.OnLeave != nil {
1294		callbacks.OnLeave()
1295	}
1296}
1297
1298// WidgetGetAllocation gets the bounds (position and size) of a widget using modern GTK4 API
1299func WidgetGetAllocation(widget uintptr) WidgetAllocation {
1300	var x, y, width, height C.int
1301
1302	C.widget_get_allocation_modern(
1303		(*C.GtkWidget)(unsafe.Pointer(widget)),
1304		&x, &y, &width, &height,
1305	)
1306
1307	return WidgetAllocation{
1308		X:      int(x),
1309		Y:      int(y),
1310		Width:  int(width),
1311		Height: int(height),
1312	}
1313}
1314
1315// GtkImage functions for favicon display
1316
1317// NewImage creates a new empty GtkImage widget.
1318func NewImage() uintptr {
1319	image := C.image_new()
1320	return uintptr(unsafe.Pointer(image))
1321}
1322
1323// ImageNewFromFile creates a new GtkImage widget from a file path.
1324func ImageNewFromFile(filename string) uintptr {
1325	if filename == "" {
1326		return 0
1327	}
1328	cstr := C.CString(filename)
1329	defer C.free(unsafe.Pointer(cstr))
1330	image := C.image_new_from_file(cstr)
1331	return uintptr(unsafe.Pointer(image))
1332}
1333
1334// ImageSetFromFile updates an existing GtkImage to display an image from a file.
1335func ImageSetFromFile(image uintptr, filename string) {
1336	if image == 0 || filename == "" {
1337		return
1338	}
1339	cstr := C.CString(filename)
1340	defer C.free(unsafe.Pointer(cstr))
1341	C.image_set_from_file((*C.GtkWidget)(unsafe.Pointer(image)), cstr)
1342}
1343
1344// ImageSetPixelSize sets the pixel size for the image (used for scaling icons).
1345func ImageSetPixelSize(image uintptr, pixelSize int) {
1346	if image == 0 {
1347		return
1348	}
1349	C.image_set_pixel_size((*C.GtkWidget)(unsafe.Pointer(image)), C.int(pixelSize))
1350}

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