detached 55d6e7c

feat(webkit): add GtkImage bindings and click handlers for title bars

commit
55d6e7cf61cbfb7c0e67384e7a5b474d13630845
author
bamen · · 2025-10-06 18:12 UTC
committer
bamen · · 2025-10-06 18:56 UTC
parent
1534d2b
signature
unsigned
1 file · +110 -0
M pkg/webkit/workspace_widgets_cgo.go +110 -0
@@ -33,6 +33,7 @@ extern gboolean goIdleCallback(uintptr_t handle);
3333 extern void goHoverCallback(uintptr_t handle);
3434 extern void goFocusEnterCallback(uintptr_t handle);
3535 extern void goFocusLeaveCallback(uintptr_t handle);
36+extern void goTitleBarClickCallback(uintptr_t handle);
3637
3738 // Main thread helpers implemented in thread_helpers.c
3839 void store_main_thread_id();
@@ -51,6 +52,26 @@ static gboolean remove_idle_callback(uintptr_t handle) {
5152 return g_idle_remove_by_data((gpointer)handle);
5253 }
5354
55+// Title bar click callback
56+static 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)
63+static 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+
5475 static void motion_enter_cb(GtkEventControllerMotion* controller, gdouble x, gdouble y, gpointer user_data) {
5576 (void)controller; (void)x; (void)y;
5677 uintptr_t handle = (uintptr_t)user_data;
@@ -367,6 +388,26 @@ static void widget_get_allocation_modern(GtkWidget* widget, int* x, int* y, int*
367388 *height = gtk_widget_get_height(widget);
368389 }
369390 }
391+
392+// GtkImage helpers for favicon display
393+static GtkWidget* image_new(void) {
394+ return gtk_image_new();
395+}
396+
397+static GtkWidget* image_new_from_file(const char* filename) {
398+ if (!filename) return NULL;
399+ return gtk_image_new_from_file(filename);
400+}
401+
402+static 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+
407+static 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+}
370411 */
371412 import "C"
372413
@@ -411,6 +452,11 @@ var (
411452 focusControllers = make(map[uintptr]uintptr)
412453 nextFocusID uintptr = 1
413454
455+ // Title bar click management
456+ titleBarMu sync.Mutex
457+ titleBarCallbacks = make(map[uintptr]func())
458+ nextTitleBarID uintptr = 1
459+
414460 mainThreadInitialized bool
415461 )
416462
@@ -862,6 +908,22 @@ func WidgetRemoveHoverHandler(widget uintptr, token uintptr) {
862908 C.widget_remove_controller((*C.GtkWidget)(unsafe.Pointer(widget)), (*C.GtkEventController)(unsafe.Pointer(controller)))
863909 }
864910
911+// WidgetAttachClickHandler attaches a click handler to any widget (for title bars)
912+func 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+
865927 // WidgetGetBounds returns the absolute bounds of the widget relative to the root window.
866928 func WidgetGetBounds(widget uintptr) (WidgetBounds, bool) {
867929 if widget == 0 || !widgetIsValid(widget) {
@@ -938,6 +1000,17 @@ func goHoverCallback(handle C.uintptr_t) {
9381000 }
9391001 }
9401002
1003+//export goTitleBarClickCallback
1004+func 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+
9411014 // GtkBox functions for stacked panes
9421015
9431016 // NewBox creates a new GtkBox widget with the specified orientation and spacing.
@@ -1238,3 +1311,40 @@ func WidgetGetAllocation(widget uintptr) WidgetAllocation {
12381311 Height: int(height),
12391312 }
12401313 }
1314+
1315+// GtkImage functions for favicon display
1316+
1317+// NewImage creates a new empty GtkImage widget.
1318+func 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.
1324+func 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.
1335+func 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).
1345+func 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+}
@@ -33,6 +33,7 @@ extern gboolean goIdleCallback(uintptr_t handle);
33 extern void goHoverCallback(uintptr_t handle);33 extern void goHoverCallback(uintptr_t handle);
34 extern void goFocusEnterCallback(uintptr_t handle);34 extern void goFocusEnterCallback(uintptr_t handle);
35 extern void goFocusLeaveCallback(uintptr_t handle);35 extern void goFocusLeaveCallback(uintptr_t handle);
36+extern void goTitleBarClickCallback(uintptr_t handle);
36 37
37 // Main thread helpers implemented in thread_helpers.c38 // Main thread helpers implemented in thread_helpers.c
38 void store_main_thread_id();39 void store_main_thread_id();
@@ -51,6 +52,26 @@ static gboolean remove_idle_callback(uintptr_t handle) {
51 return g_idle_remove_by_data((gpointer)handle);52 return g_idle_remove_by_data((gpointer)handle);
52 }53 }
53 54
55+// Title bar click callback
56+static 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)
63+static 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+
54 static void motion_enter_cb(GtkEventControllerMotion* controller, gdouble x, gdouble y, gpointer user_data) {75 static void motion_enter_cb(GtkEventControllerMotion* controller, gdouble x, gdouble y, gpointer user_data) {
55 (void)controller; (void)x; (void)y;76 (void)controller; (void)x; (void)y;
56 uintptr_t handle = (uintptr_t)user_data;77 uintptr_t handle = (uintptr_t)user_data;
@@ -367,6 +388,26 @@ static void widget_get_allocation_modern(GtkWidget* widget, int* x, int* y, int*
367 *height = gtk_widget_get_height(widget);388 *height = gtk_widget_get_height(widget);
368 }389 }
369 }390 }
391+
392+// GtkImage helpers for favicon display
393+static GtkWidget* image_new(void) {
394+ return gtk_image_new();
395+}
396+
397+static GtkWidget* image_new_from_file(const char* filename) {
398+ if (!filename) return NULL;
399+ return gtk_image_new_from_file(filename);
400+}
401+
402+static 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+
407+static 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+}
370 */411 */
371 import "C"412 import "C"
372 413
@@ -411,6 +452,11 @@ var (
411 focusControllers = make(map[uintptr]uintptr)452 focusControllers = make(map[uintptr]uintptr)
412 nextFocusID uintptr = 1453 nextFocusID uintptr = 1
413 454
455+ // Title bar click management
456+ titleBarMu sync.Mutex
457+ titleBarCallbacks = make(map[uintptr]func())
458+ nextTitleBarID uintptr = 1
459+
414 mainThreadInitialized bool460 mainThreadInitialized bool
415 )461 )
416 462
@@ -862,6 +908,22 @@ func WidgetRemoveHoverHandler(widget uintptr, token uintptr) {
862 C.widget_remove_controller((*C.GtkWidget)(unsafe.Pointer(widget)), (*C.GtkEventController)(unsafe.Pointer(controller)))908 C.widget_remove_controller((*C.GtkWidget)(unsafe.Pointer(widget)), (*C.GtkEventController)(unsafe.Pointer(controller)))
863 }909 }
864 910
911+// WidgetAttachClickHandler attaches a click handler to any widget (for title bars)
912+func 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+
865 // WidgetGetBounds returns the absolute bounds of the widget relative to the root window.927 // WidgetGetBounds returns the absolute bounds of the widget relative to the root window.
866 func WidgetGetBounds(widget uintptr) (WidgetBounds, bool) {928 func WidgetGetBounds(widget uintptr) (WidgetBounds, bool) {
867 if widget == 0 || !widgetIsValid(widget) {929 if widget == 0 || !widgetIsValid(widget) {
@@ -938,6 +1000,17 @@ func goHoverCallback(handle C.uintptr_t) {
938 }1000 }
939 }1001 }
940 1002
1003+//export goTitleBarClickCallback
1004+func 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+
941 // GtkBox functions for stacked panes1014 // GtkBox functions for stacked panes
942 1015
943 // NewBox creates a new GtkBox widget with the specified orientation and spacing.1016 // NewBox creates a new GtkBox widget with the specified orientation and spacing.
@@ -1238,3 +1311,40 @@ func WidgetGetAllocation(widget uintptr) WidgetAllocation {
1238 Height: int(height),1311 Height: int(height),
1239 }1312 }
1240 }1313 }
1314+
1315+// GtkImage functions for favicon display
1316+
1317+// NewImage creates a new empty GtkImage widget.
1318+func 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.
1324+func 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.
1335+func 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).
1345+func 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