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); | |||
| 33 | 33 | extern void goHoverCallback(uintptr_t handle); | |
| 34 | 34 | extern void goFocusEnterCallback(uintptr_t handle); | |
| 35 | 35 | extern void goFocusLeaveCallback(uintptr_t handle); | |
| 36 | + | extern void goTitleBarClickCallback(uintptr_t handle); | |
| 36 | 37 | ||
| 37 | 38 | // Main thread helpers implemented in thread_helpers.c | |
| 38 | 39 | void store_main_thread_id(); | |
| @@ -51,6 +52,26 @@ static gboolean remove_idle_callback(uintptr_t handle) { | |||
| 51 | 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 | 75 | static void motion_enter_cb(GtkEventControllerMotion* controller, gdouble x, gdouble y, gpointer user_data) { | |
| 55 | 76 | (void)controller; (void)x; (void)y; | |
| 56 | 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 | 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 | 412 | import "C" | |
| 372 | 413 | ||
| @@ -411,6 +452,11 @@ var ( | |||
| 411 | 452 | focusControllers = make(map[uintptr]uintptr) | |
| 412 | 453 | 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 | 460 | mainThreadInitialized bool | |
| 415 | 461 | ) | |
| 416 | 462 | ||
| @@ -862,6 +908,22 @@ func WidgetRemoveHoverHandler(widget uintptr, token uintptr) { | |||
| 862 | 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 | 927 | // WidgetGetBounds returns the absolute bounds of the widget relative to the root window. | |
| 866 | 928 | func WidgetGetBounds(widget uintptr) (WidgetBounds, bool) { | |
| 867 | 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 | 1014 | // GtkBox functions for stacked panes | |
| 942 | 1015 | ||
| 943 | 1016 | // NewBox creates a new GtkBox widget with the specified orientation and spacing. | |
| @@ -1238,3 +1311,40 @@ func WidgetGetAllocation(widget uintptr) WidgetAllocation { | |||
| 1238 | 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 | + | } | |
| @@ -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.c | 38 | // 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 = 1 | 453 | 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 bool | 460 | 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 panes | 1014 | // 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 | + | } | |||