wails/v3/pkg/application/linux_cgo_gtk4.go
Lea Anthony 716507b488
fix(linux/gtk4): window position, max size, and show/hide on X11 (#4965)
* fix(linux/gtk4): window position, max size, and show/hide on X11 (#4957)

- Use gtk_window_present() in windowShow() to properly restore windows
  after hide instead of gtk_widget_set_visible() which could leave
  windows minimized
- Add X11-conditional window position get/set using XTranslateCoordinates
  and XMoveWindow, gated behind GDK_IS_X11_DISPLAY runtime checks and
  GDK_WINDOWING_X11 compile-time guards. Wayland continues to return 0,0
  gracefully since position is not available there.
- Implement max window size enforcement via notify::default-width/height
  signal handlers that clamp dimensions, replacing the removed
  gtk_window_set_geometry_hints from GTK3

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: treat skipped cross-compile job as success in CI

The cross_compile_results job treats "skipped" as a failure, but the
cross_compile job is legitimately skipped when the PR hasn't been
approved yet. Accept "skipped" alongside "success".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 16:27:20 +11:00

2028 lines
51 KiB
Go

//go:build linux && cgo && gtk4 && !android
package application
import (
"fmt"
"strings"
"sync"
"time"
"unsafe"
"github.com/wailsapp/wails/v3/internal/assetserver/webview"
"github.com/wailsapp/wails/v3/pkg/events"
)
/*
#cgo linux pkg-config: gtk4 webkitgtk-6.0
#include "linux_cgo_gtk4.h"
*/
import "C"
// Calloc handles alloc/dealloc of C data
type Calloc struct {
pool []unsafe.Pointer
}
// NewCalloc creates a new allocator
func NewCalloc() Calloc {
return Calloc{}
}
// String creates a new C string and retains a reference to it
func (c Calloc) String(in string) *C.char {
result := C.CString(in)
c.pool = append(c.pool, unsafe.Pointer(result))
return result
}
// Free frees all allocated C memory
func (c Calloc) Free() {
for _, str := range c.pool {
C.free(str)
}
c.pool = []unsafe.Pointer{}
}
type windowPointer *C.GtkWindow
type identifier C.uint
type pointer unsafe.Pointer
type GSList C.GSList
type GSListPointer *GSList
// getLinuxWebviewWindow safely extracts a linuxWebviewWindow from a Window interface
func getLinuxWebviewWindow(window Window) *linuxWebviewWindow {
if window == nil {
return nil
}
webviewWindow, ok := window.(*WebviewWindow)
if !ok {
return nil
}
lw, ok := webviewWindow.impl.(*linuxWebviewWindow)
if !ok {
return nil
}
return lw
}
var (
nilPointer pointer = nil
nilRadioGroup GSListPointer = nil
)
var (
gtkSignalToMenuItem map[uint]*MenuItem
mainThreadId *C.GThread
)
var registerURIScheme sync.Once
func init() {
gtkSignalToMenuItem = map[uint]*MenuItem{}
mainThreadId = C.g_thread_self()
}
// mainthread stuff
func dispatchOnMainThread(id uint) {
C.dispatchOnMainThread(C.uint(id))
}
//export dispatchOnMainThreadCallback
func dispatchOnMainThreadCallback(callbackID C.uint) {
executeOnMainThread(uint(callbackID))
}
//export activateLinux
func activateLinux(data pointer) {
app := getNativeApplication()
app.markActivated()
processApplicationEvent(C.uint(events.Linux.ApplicationStartup), data)
}
//export processApplicationEvent
func processApplicationEvent(eventID C.uint, data pointer) {
event := newApplicationEvent(events.ApplicationEventType(eventID))
switch event.Id {
case uint(events.Linux.SystemThemeChanged):
isDark := globalApplication.Env.IsDarkMode()
event.Context().setIsDarkMode(isDark)
}
applicationEvents <- event
}
func isOnMainThread() bool {
threadId := C.g_thread_self()
return threadId == mainThreadId
}
// implementation below
func appName() string {
name := C.g_get_application_name()
defer C.free(unsafe.Pointer(name))
return C.GoString(name)
}
func appNew(name string) pointer {
C.install_signal_handlers()
appId := fmt.Sprintf("org.wails.%s", name)
nameC := C.CString(appId)
defer C.free(unsafe.Pointer(nameC))
return pointer(C.gtk_application_new(nameC, C.APPLICATION_DEFAULT_FLAGS))
}
func setProgramName(prgName string) {
cPrgName := C.CString(prgName)
defer C.free(unsafe.Pointer(cPrgName))
C.g_set_prgname(cPrgName)
}
func appRun(app pointer) error {
application := (*C.GApplication)(app)
C.g_application_hold(application)
signal := C.CString("activate")
defer C.free(unsafe.Pointer(signal))
C.signal_connect(unsafe.Pointer(application), signal, C.activateLinux, 0)
status := C.g_application_run(application, 0, nil)
C.g_application_release(application)
C.g_object_unref(C.gpointer(app))
var err error
if status != 0 {
err = fmt.Errorf("exit code: %d", status)
}
return err
}
func appDestroy(application pointer) {
C.g_application_quit((*C.GApplication)(application))
}
func (w *linuxWebviewWindow) contextMenuSignals(menu pointer) {
// GTK4: Context menus use GtkPopoverMenu, signals handled differently
// TODO: Implement GTK4 context menu signal handling
}
func (w *linuxWebviewWindow) contextMenuShow(menu pointer, data *ContextMenuData) {
// GTK4: Use GtkPopoverMenu instead of gtk_menu_popup_at_rect
// TODO: Implement GTK4 context menu popup
}
func (a *linuxApp) getCurrentWindowID() uint {
window := (*C.GtkWindow)(C.gtk_application_get_active_window((*C.GtkApplication)(a.application)))
if window == nil {
return uint(1)
}
identifier, ok := a.windowMap[window]
if ok {
return identifier
}
return uint(1)
}
func (a *linuxApp) getWindows() []pointer {
result := []pointer{}
windows := C.gtk_application_get_windows((*C.GtkApplication)(a.application))
for {
result = append(result, pointer(windows.data))
windows = windows.next
if windows == nil {
return result
}
}
}
func (a *linuxApp) hideAllWindows() {
for _, window := range a.getWindows() {
C.gtk_widget_set_visible((*C.GtkWidget)(window), C.gboolean(0))
}
}
func (a *linuxApp) showAllWindows() {
for _, window := range a.getWindows() {
C.gtk_window_present((*C.GtkWindow)(window))
}
}
func (a *linuxApp) setIcon(icon []byte) {
// TODO: Implement GTK4 icon setting using GdkTexture
gbytes := C.g_bytes_new_static(C.gconstpointer(unsafe.Pointer(&icon[0])), C.ulong(len(icon)))
defer C.g_bytes_unref(gbytes)
}
func clipboardGet() string {
cText := C.clipboard_get_text_sync()
if cText != nil {
result := C.GoString(cText)
C.clipboard_free_text(cText)
return result
}
return ""
}
func clipboardSet(text string) {
display := C.gdk_display_get_default()
clip := C.gdk_display_get_clipboard(display)
cText := C.CString(text)
C.gdk_clipboard_set_text(clip, cText)
C.free(unsafe.Pointer(cText))
}
// Menu - GTK4 uses GMenu/GAction instead of GtkMenu
var menuItemActionCounter uint32 = 0
var menuItemActions = make(map[uint]string)
var menuItemIds = make(map[pointer]uint)
var menuItemIdsMutex sync.RWMutex
func generateActionName(itemId uint) string {
menuItemActionCounter++
name := fmt.Sprintf("action_%d", menuItemActionCounter)
menuItemActions[itemId] = name
return name
}
//export menuActionActivated
func menuActionActivated(id C.guint) {
item, ok := gtkSignalToMenuItem[uint(id)]
if !ok {
return
}
switch item.itemType {
case text:
menuItemClicked <- item.id
case checkbox:
impl := item.impl.(*linuxMenuItem)
currentState := impl.isChecked()
impl.setChecked(!currentState)
menuItemClicked <- item.id
case radio:
menuItem := item.impl.(*linuxMenuItem)
if !menuItem.isChecked() {
menuItem.setChecked(true)
menuItemClicked <- item.id
}
}
}
func menuNewSection() pointer {
return pointer(C.g_menu_new())
}
func menuAppendSection(menu *Menu, section pointer) {
if menu.impl == nil {
return
}
impl := menu.impl.(*linuxMenu)
if impl.native == nil {
return
}
gmenu := (*C.GMenu)(impl.native)
C.g_menu_append_section(gmenu, nil, (*C.GMenuModel)(section))
}
func menuAppendItemToSection(section pointer, item *MenuItem) {
if item.impl == nil {
return
}
menuImpl := item.impl.(*linuxMenuItem)
if menuImpl.native == nil {
return
}
gsection := (*C.GMenu)(section)
gitem := (*C.GMenuItem)(menuImpl.native)
menuImpl.parentMenu = section
menuImpl.isHidden = item.hidden
if !item.hidden {
C.g_menu_append_item(gsection, gitem)
}
}
var menuItemCounters = make(map[pointer]int)
var menuItemCountersLock sync.Mutex
func menuAppend(parent *Menu, menu *MenuItem, hidden bool) {
if parent.impl == nil || menu.impl == nil {
return
}
parentImpl := parent.impl.(*linuxMenu)
menuImpl := menu.impl.(*linuxMenuItem)
if parentImpl.native == nil || menuImpl.native == nil {
return
}
gmenu := (*C.GMenu)(parentImpl.native)
gitem := (*C.GMenuItem)(menuImpl.native)
menuImpl.parentMenu = parentImpl.native
menuImpl.isHidden = hidden
menuItemCountersLock.Lock()
menuImpl.menuIndex = menuItemCounters[parentImpl.native]
menuItemCounters[parentImpl.native]++
menuItemCountersLock.Unlock()
if !hidden {
C.g_menu_append_item(gmenu, gitem)
}
}
func menuBarNew() pointer {
gmenu := C.g_menu_new()
C.set_app_menu_model(gmenu)
return pointer(gmenu)
}
func menuNew() pointer {
return pointer(C.g_menu_new())
}
func menuSetSubmenu(item *MenuItem, menu *Menu) {
if item.impl == nil || menu.impl == nil {
return
}
itemImpl := item.impl.(*linuxMenuItem)
menuImpl := menu.impl.(*linuxMenu)
if itemImpl.native == nil || menuImpl.native == nil {
return
}
gitem := (*C.GMenuItem)(itemImpl.native)
gmenu := (*C.GMenu)(menuImpl.native)
C.g_menu_item_set_submenu(gitem, (*C.GMenuModel)(unsafe.Pointer(gmenu)))
}
func menuGetRadioGroup(item *linuxMenuItem) *GSList {
return nil
}
//export handleClick
func handleClick(idPtr unsafe.Pointer) {
}
func attachMenuHandler(item *MenuItem) uint {
gtkSignalToMenuItem[item.id] = item
return item.id
}
func menuItemChecked(widget pointer) bool {
if widget == nil {
return false
}
menuItemIdsMutex.RLock()
itemId, exists := menuItemIds[widget]
menuItemIdsMutex.RUnlock()
if !exists {
return false
}
actionName, ok := menuItemActions[itemId]
if !ok {
return false
}
cName := C.CString(actionName)
defer C.free(unsafe.Pointer(cName))
return C.get_action_state(cName) != 0
}
func menuItemNew(label string, bitmap []byte) pointer {
return nil
}
func menuItemNewWithId(label string, bitmap []byte, itemId uint) pointer {
cLabel := C.CString(label)
defer C.free(unsafe.Pointer(cLabel))
actionName := generateActionName(itemId)
cAction := C.CString(actionName)
defer C.free(unsafe.Pointer(cAction))
gitem := C.create_menu_item(cLabel, cAction, C.guint(itemId))
menuItemIdsMutex.Lock()
menuItemIds[pointer(gitem)] = itemId
menuItemIdsMutex.Unlock()
return pointer(gitem)
}
func menuItemDestroy(widget pointer) {
if widget != nil {
C.g_object_unref(C.gpointer(widget))
}
}
func menuItemSetHidden(item *linuxMenuItem, hidden bool) {
if item.parentMenu == nil {
return
}
gmenu := (*C.GMenu)(item.parentMenu)
gitem := (*C.GMenuItem)(item.native)
if hidden {
C.menu_remove_item(gmenu, C.gint(item.menuIndex))
} else {
C.menu_insert_item(gmenu, C.gint(item.menuIndex), gitem)
}
}
func menuItemAddProperties(menuItem *C.GtkWidget, label string, bitmap []byte) pointer {
return nil
}
func menuCheckItemNew(label string, bitmap []byte) pointer {
return nil
}
func menuCheckItemNewWithId(label string, bitmap []byte, itemId uint, checked bool) pointer {
cLabel := C.CString(label)
defer C.free(unsafe.Pointer(cLabel))
actionName := generateActionName(itemId)
cAction := C.CString(actionName)
defer C.free(unsafe.Pointer(cAction))
initialState := C.gboolean(0)
if checked {
initialState = C.gboolean(1)
}
gitem := C.create_check_menu_item(cLabel, cAction, C.guint(itemId), initialState)
menuItemIdsMutex.Lock()
menuItemIds[pointer(gitem)] = itemId
menuItemIdsMutex.Unlock()
return pointer(gitem)
}
func menuItemSetChecked(widget pointer, checked bool) {
if widget == nil {
return
}
menuItemIdsMutex.RLock()
itemId, exists := menuItemIds[widget]
menuItemIdsMutex.RUnlock()
if !exists {
return
}
actionName, ok := menuItemActions[itemId]
if !ok {
return
}
cName := C.CString(actionName)
defer C.free(unsafe.Pointer(cName))
state := C.gboolean(0)
if checked {
state = C.gboolean(1)
}
C.set_action_state(cName, state)
}
func menuItemSetDisabled(widget pointer, disabled bool) {
if widget == nil {
return
}
menuItemIdsMutex.RLock()
itemId, exists := menuItemIds[widget]
menuItemIdsMutex.RUnlock()
if !exists {
return
}
actionName, ok := menuItemActions[itemId]
if !ok {
return
}
cName := C.CString(actionName)
defer C.free(unsafe.Pointer(cName))
enabled := C.gboolean(1)
if disabled {
enabled = C.gboolean(0)
}
C.set_action_enabled(cName, enabled)
}
func menuItemSetLabel(widget pointer, label string) {
if widget == nil {
return
}
cLabel := C.CString(label)
defer C.free(unsafe.Pointer(cLabel))
C.g_menu_item_set_label((*C.GMenuItem)(widget), cLabel)
}
func menuItemRemoveBitmap(widget pointer) {
}
func menuItemSetBitmap(widget pointer, bitmap []byte) {
}
func menuItemSetToolTip(widget pointer, tooltip string) {
}
func menuItemSignalBlock(widget pointer, handlerId uint, block bool) {
}
func menuRadioItemNew(group *GSList, label string) pointer {
return nil
}
func menuRadioItemNewWithId(label string, itemId uint, checked bool) pointer {
cLabel := C.CString(label)
defer C.free(unsafe.Pointer(cLabel))
actionName := generateActionName(itemId)
cAction := C.CString(actionName)
defer C.free(unsafe.Pointer(cAction))
initialState := C.gboolean(0)
if checked {
initialState = C.gboolean(1)
}
gitem := C.create_check_menu_item(cLabel, cAction, C.guint(itemId), initialState)
menuItemIdsMutex.Lock()
menuItemIds[pointer(gitem)] = itemId
menuItemIdsMutex.Unlock()
return pointer(gitem)
}
func menuRadioItemNewWithGroup(label string, itemId uint, groupId uint, checkedId uint) pointer {
cLabel := C.CString(label)
defer C.free(unsafe.Pointer(cLabel))
actionName := fmt.Sprintf("radio_group_%d", groupId)
cAction := C.CString(actionName)
defer C.free(unsafe.Pointer(cAction))
targetValue := fmt.Sprintf("%d", itemId)
cTarget := C.CString(targetValue)
defer C.free(unsafe.Pointer(cTarget))
initialValue := fmt.Sprintf("%d", checkedId)
cInitial := C.CString(initialValue)
defer C.free(unsafe.Pointer(cInitial))
gitem := C.create_radio_menu_item(cLabel, cAction, cTarget, cInitial, C.guint(itemId))
menuItemIdsMutex.Lock()
menuItemIds[pointer(gitem)] = itemId
menuItemIdsMutex.Unlock()
return pointer(gitem)
}
// Keyboard accelerator support for GTK4 menus
// namedKeysToGTK maps Wails key names to GDK keysym values
// These are X11 keysym values that GDK uses
var namedKeysToGTK = map[string]C.guint{
"backspace": C.guint(0xff08),
"tab": C.guint(0xff09),
"return": C.guint(0xff0d),
"enter": C.guint(0xff0d),
"escape": C.guint(0xff1b),
"left": C.guint(0xff51),
"right": C.guint(0xff53),
"up": C.guint(0xff52),
"down": C.guint(0xff54),
"space": C.guint(0xff80),
"delete": C.guint(0xff9f),
"home": C.guint(0xff95),
"end": C.guint(0xff9c),
"page up": C.guint(0xff9a),
"page down": C.guint(0xff9b),
"f1": C.guint(0xffbe),
"f2": C.guint(0xffbf),
"f3": C.guint(0xffc0),
"f4": C.guint(0xffc1),
"f5": C.guint(0xffc2),
"f6": C.guint(0xffc3),
"f7": C.guint(0xffc4),
"f8": C.guint(0xffc5),
"f9": C.guint(0xffc6),
"f10": C.guint(0xffc7),
"f11": C.guint(0xffc8),
"f12": C.guint(0xffc9),
"f13": C.guint(0xffca),
"f14": C.guint(0xffcb),
"f15": C.guint(0xffcc),
"f16": C.guint(0xffcd),
"f17": C.guint(0xffce),
"f18": C.guint(0xffcf),
"f19": C.guint(0xffd0),
"f20": C.guint(0xffd1),
"f21": C.guint(0xffd2),
"f22": C.guint(0xffd3),
"f23": C.guint(0xffd4),
"f24": C.guint(0xffd5),
"f25": C.guint(0xffd6),
"f26": C.guint(0xffd7),
"f27": C.guint(0xffd8),
"f28": C.guint(0xffd9),
"f29": C.guint(0xffda),
"f30": C.guint(0xffdb),
"f31": C.guint(0xffdc),
"f32": C.guint(0xffdd),
"f33": C.guint(0xffde),
"f34": C.guint(0xffdf),
"f35": C.guint(0xffe0),
"numlock": C.guint(0xff7f),
}
// parseKeyGTK converts a Wails key string to a GDK keysym value
func parseKeyGTK(key string) C.guint {
// Check named keys first
if result, found := namedKeysToGTK[key]; found {
return result
}
// For single character keys, convert using gdk_unicode_to_keyval
if len(key) != 1 {
return C.guint(0)
}
keyval := rune(key[0])
return C.gdk_unicode_to_keyval(C.guint(keyval))
}
// parseModifiersGTK converts Wails modifiers to GDK modifier type
func parseModifiersGTK(modifiers []modifier) C.GdkModifierType {
var result C.GdkModifierType
for _, mod := range modifiers {
switch mod {
case ShiftKey:
result |= C.GDK_SHIFT_MASK
case ControlKey, CmdOrCtrlKey:
result |= C.GDK_CONTROL_MASK
case OptionOrAltKey:
result |= C.GDK_ALT_MASK
case SuperKey:
result |= C.GDK_SUPER_MASK
}
}
return result
}
// acceleratorToGTK converts a Wails accelerator to GTK key/modifiers
func acceleratorToGTK(accel *accelerator) (C.guint, C.GdkModifierType) {
key := parseKeyGTK(accel.Key)
mods := parseModifiersGTK(accel.Modifiers)
return key, mods
}
// setMenuItemAccelerator sets the keyboard accelerator for a menu item
// This uses gtk_application_set_accels_for_action to register the shortcut
func setMenuItemAccelerator(itemId uint, accel *accelerator) {
if accel == nil {
return
}
// Look up the action name for this menu item
actionName, ok := menuItemActions[itemId]
if !ok {
return
}
// Get the GtkApplication pointer
app := getNativeApplication()
if app == nil || app.application == nil {
return
}
// Convert accelerator to GTK format
key, mods := acceleratorToGTK(accel)
if key == 0 {
return
}
// Build accelerator string using GTK's function
accelString := C.build_accelerator_string(key, mods)
if accelString == nil {
return
}
defer C.g_free(C.gpointer(accelString))
// Set the accelerator on the application
cActionName := C.CString(actionName)
defer C.free(unsafe.Pointer(cActionName))
C.set_action_accelerator((*C.GtkApplication)(app.application), cActionName, accelString)
}
// screen related
func getScreenByIndex(display *C.GdkDisplay, index int) *Screen {
monitors := C.gdk_display_get_monitors(display)
monitor := (*C.GdkMonitor)(C.g_list_model_get_item(monitors, C.guint(index)))
if monitor == nil {
return nil
}
defer C.g_object_unref(C.gpointer(monitor))
var geometry C.GdkRectangle
C.gdk_monitor_get_geometry(monitor, &geometry)
// Use gdk_monitor_get_scale (GTK 4.14+) for fractional scaling support
scaleFactor := float64(C.gdk_monitor_get_scale(monitor))
name := C.gdk_monitor_get_model(monitor)
// GTK4's gdk_monitor_get_geometry returns logical (DIP) coordinates.
// PhysicalBounds needs physical pixel dimensions for proper DPI scaling.
x := int(geometry.x)
y := int(geometry.y)
width := int(geometry.width)
height := int(geometry.height)
return &Screen{
ID: fmt.Sprintf("%d", index),
Name: C.GoString(name),
IsPrimary: index == 0,
ScaleFactor: float32(scaleFactor),
X: x,
Y: y,
Size: Size{
Height: height,
Width: width,
},
Bounds: Rect{
X: x,
Y: y,
Height: height,
Width: width,
},
PhysicalBounds: Rect{
X: int(float64(x) * scaleFactor),
Y: int(float64(y) * scaleFactor),
Height: int(float64(height) * scaleFactor),
Width: int(float64(width) * scaleFactor),
},
WorkArea: Rect{
X: x,
Y: y,
Height: height,
Width: width,
},
PhysicalWorkArea: Rect{
X: int(float64(x) * scaleFactor),
Y: int(float64(y) * scaleFactor),
Height: int(float64(height) * scaleFactor),
Width: int(float64(width) * scaleFactor),
},
Rotation: 0.0,
}
}
func getScreens(app pointer) ([]*Screen, error) {
var screens []*Screen
display := C.gdk_display_get_default()
monitors := C.gdk_display_get_monitors(display)
count := C.g_list_model_get_n_items(monitors)
for i := 0; i < int(count); i++ {
screens = append(screens, getScreenByIndex(display, i))
}
return screens, nil
}
// widgets
func (w *linuxWebviewWindow) setEnabled(enabled bool) {
C.gtk_widget_set_sensitive(w.gtkWidget(), C.gboolean(btoi(enabled)))
}
func btoi(b bool) int {
if b {
return 1
}
return 0
}
func widgetSetVisible(widget pointer, hidden bool) {
C.gtk_widget_set_visible((*C.GtkWidget)(widget), C.gboolean(btoi(!hidden)))
}
func (w *linuxWebviewWindow) close() {
C.gtk_window_destroy(w.gtkWindow())
getNativeApplication().unregisterWindow(windowPointer(w.window))
}
func (w *linuxWebviewWindow) enableDND() {
C.enableDND((*C.GtkWidget)(w.webview), C.uintptr_t(w.parent.id))
}
func (w *linuxWebviewWindow) disableDND() {
C.disableDND((*C.GtkWidget)(w.webview), C.uintptr_t(w.parent.id))
}
func (w *linuxWebviewWindow) execJS(js string) {
InvokeAsync(func() {
value := C.CString(js)
defer C.free(unsafe.Pointer(value))
// WebKitGTK 6.0 uses webkit_web_view_evaluate_javascript
C.webkit_web_view_evaluate_javascript(w.webKitWebView(),
value,
C.gssize(len(js)),
nil,
nil,
nil,
nil,
nil)
})
}
// Preallocated buffer for drag-over JS calls
var dragOverJSBuffer = C.CString(strings.Repeat(" ", 64))
var emptyWorldName = C.CString("")
func (w *linuxWebviewWindow) execJSDragOver(x, y int) {
buf := (*[64]byte)(unsafe.Pointer(dragOverJSBuffer))
n := copy(buf[:], "window._wails.handleDragOver(")
n += writeInt(buf[n:], x)
buf[n] = ','
n++
n += writeInt(buf[n:], y)
buf[n] = ')'
n++
buf[n] = 0
C.webkit_web_view_evaluate_javascript(w.webKitWebView(),
dragOverJSBuffer,
C.gssize(n),
nil,
emptyWorldName,
nil,
nil,
nil)
}
func writeInt(buf []byte, n int) int {
if n < 0 {
buf[0] = '-'
return 1 + writeInt(buf[1:], -n)
}
if n == 0 {
buf[0] = '0'
return 1
}
tmp := n
digits := 0
for tmp > 0 {
digits++
tmp /= 10
}
for i := digits - 1; i >= 0; i-- {
buf[i] = byte('0' + n%10)
n /= 10
}
return digits
}
func getMousePosition() (int, int, *Screen) {
display := C.gdk_display_get_default()
if display == nil {
return 0, 0, nil
}
monitors := C.gdk_display_get_monitors(display)
if monitors == nil {
return 0, 0, nil
}
n := C.g_list_model_get_n_items(monitors)
if n == 0 {
return 0, 0, nil
}
var primaryMonitor *C.GdkMonitor
for i := C.guint(0); i < n; i++ {
mon := (*C.GdkMonitor)(C.g_list_model_get_item(monitors, i))
if mon != nil {
primaryMonitor = mon
break
}
}
if primaryMonitor == nil {
return 0, 0, nil
}
var geometry C.GdkRectangle
C.gdk_monitor_get_geometry(primaryMonitor, &geometry)
// Use gdk_monitor_get_scale (GTK 4.14+) for fractional scaling support
scaleFactor := float64(C.gdk_monitor_get_scale(primaryMonitor))
name := C.gdk_monitor_get_model(primaryMonitor)
// GTK4's gdk_monitor_get_geometry returns logical (DIP) coordinates.
// PhysicalBounds needs physical pixel dimensions for proper DPI scaling.
x := int(geometry.x)
y := int(geometry.y)
width := int(geometry.width)
height := int(geometry.height)
screen := &Screen{
ID: "0",
Name: C.GoString(name),
ScaleFactor: float32(scaleFactor),
X: x,
Y: y,
Size: Size{
Height: height,
Width: width,
},
Bounds: Rect{
X: x,
Y: y,
Height: height,
Width: width,
},
WorkArea: Rect{
X: x,
Y: y,
Height: height,
Width: width,
},
PhysicalBounds: Rect{
X: int(float64(x) * scaleFactor),
Y: int(float64(y) * scaleFactor),
Height: int(float64(height) * scaleFactor),
Width: int(float64(width) * scaleFactor),
},
PhysicalWorkArea: Rect{
X: int(float64(x) * scaleFactor),
Y: int(float64(y) * scaleFactor),
Height: int(float64(height) * scaleFactor),
Width: int(float64(width) * scaleFactor),
},
IsPrimary: true,
}
centerX := x + width/2
centerY := y + height/2
return centerX, centerY, screen
}
func (w *linuxWebviewWindow) destroy() {
w.parent.markAsDestroyed()
if w.gtkmenu != nil {
// GTK4: Different menu destruction
w.gtkmenu = nil
}
C.gtk_window_destroy(w.gtkWindow())
}
func (w *linuxWebviewWindow) fullscreen() {
C.gtk_window_fullscreen(w.gtkWindow())
}
func (w *linuxWebviewWindow) getCurrentMonitor() *C.GdkMonitor {
display := C.gtk_widget_get_display(w.gtkWidget())
surface := C.gtk_native_get_surface((*C.GtkNative)(unsafe.Pointer(w.gtkWindow())))
if surface != nil {
monitor := C.gdk_display_get_monitor_at_surface(display, surface)
if monitor != nil {
return monitor
}
}
return nil
}
func (w *linuxWebviewWindow) getScreen() (*Screen, error) {
monitor := w.getCurrentMonitor()
if monitor == nil {
return nil, fmt.Errorf("no monitor found")
}
name := C.gdk_monitor_get_model(monitor)
var geometry C.GdkRectangle
C.gdk_monitor_get_geometry(monitor, &geometry)
// Use gdk_monitor_get_scale (GTK 4.14+) for fractional scaling support
scaleFactor := float64(C.gdk_monitor_get_scale(monitor))
// GTK4's gdk_monitor_get_geometry returns logical (DIP) coordinates.
// PhysicalBounds needs physical pixel dimensions for proper DPI scaling.
x := int(geometry.x)
y := int(geometry.y)
width := int(geometry.width)
height := int(geometry.height)
return &Screen{
ID: fmt.Sprintf("%d", w.id),
Name: C.GoString(name),
ScaleFactor: float32(scaleFactor),
X: x,
Y: y,
Size: Size{
Height: height,
Width: width,
},
Bounds: Rect{
X: x,
Y: y,
Height: height,
Width: width,
},
WorkArea: Rect{
X: x,
Y: y,
Height: height,
Width: width,
},
PhysicalBounds: Rect{
X: int(float64(x) * scaleFactor),
Y: int(float64(y) * scaleFactor),
Height: int(float64(height) * scaleFactor),
Width: int(float64(width) * scaleFactor),
},
PhysicalWorkArea: Rect{
X: int(float64(x) * scaleFactor),
Y: int(float64(y) * scaleFactor),
Height: int(float64(height) * scaleFactor),
Width: int(float64(width) * scaleFactor),
},
IsPrimary: false,
Rotation: 0.0,
}, nil
}
func (w *linuxWebviewWindow) getCurrentMonitorGeometry() (x int, y int, width int, height int, scaleFactor float64) {
monitor := w.getCurrentMonitor()
if monitor == nil {
return -1, -1, -1, -1, 1
}
var result C.GdkRectangle
C.gdk_monitor_get_geometry(monitor, &result)
// Use gdk_monitor_get_scale (GTK 4.14+) for fractional scaling support
scaleFactor = float64(C.gdk_monitor_get_scale(monitor))
return int(result.x), int(result.y), int(result.width), int(result.height), scaleFactor
}
func (w *linuxWebviewWindow) size() (int, int) {
var width, height C.int
C.gtk_window_get_default_size(w.gtkWindow(), &width, &height)
if width <= 0 || height <= 0 {
width = C.int(C.gtk_widget_get_width(w.gtkWidget()))
height = C.int(C.gtk_widget_get_height(w.gtkWidget()))
}
return int(width), int(height)
}
func (w *linuxWebviewWindow) relativePosition() (int, int) {
x, y := w.position()
monitor := w.getCurrentMonitor()
if monitor == nil {
return x, y
}
var geometry C.GdkRectangle
C.gdk_monitor_get_geometry(monitor, &geometry)
return x - int(geometry.x), y - int(geometry.y)
}
func (w *linuxWebviewWindow) gtkWidget() *C.GtkWidget {
return (*C.GtkWidget)(w.window)
}
func (w *linuxWebviewWindow) windowHide() {
C.gtk_widget_set_visible(w.gtkWidget(), C.gboolean(0))
}
func (w *linuxWebviewWindow) isFullscreen() bool {
return C.gtk_window_is_fullscreen(w.gtkWindow()) != 0
}
func (w *linuxWebviewWindow) isFocused() bool {
return C.gtk_window_is_active(w.gtkWindow()) != 0
}
func (w *linuxWebviewWindow) isMaximised() bool {
return C.gtk_window_is_maximized(w.gtkWindow()) != 0 && !w.isFullscreen()
}
func (w *linuxWebviewWindow) isMinimised() bool {
surface := C.gtk_native_get_surface((*C.GtkNative)(unsafe.Pointer(w.gtkWindow())))
if surface == nil {
return false
}
state := C.gdk_toplevel_get_state((*C.GdkToplevel)(unsafe.Pointer(surface)))
return state&C.GDK_TOPLEVEL_STATE_MINIMIZED != 0
}
func (w *linuxWebviewWindow) isVisible() bool {
return C.gtk_widget_is_visible(w.gtkWidget()) != 0
}
func (w *linuxWebviewWindow) maximise() {
C.gtk_window_maximize(w.gtkWindow())
}
func (w *linuxWebviewWindow) minimise() {
C.gtk_window_minimize(w.gtkWindow())
}
func windowNew(application pointer, menu pointer, menuStyle LinuxMenuStyle, windowId uint, gpuPolicy WebviewGpuPolicy) (window, webview, vbox pointer) {
window = pointer(C.gtk_application_window_new((*C.GtkApplication)(application)))
C.g_object_ref_sink(C.gpointer(window))
C.attach_action_group_to_widget((*C.GtkWidget)(window))
webview = windowNewWebview(windowId, gpuPolicy)
vbox = pointer(C.gtk_box_new(C.GTK_ORIENTATION_VERTICAL, 0))
name := C.CString("webview-box")
defer C.free(unsafe.Pointer(name))
C.gtk_widget_set_name((*C.GtkWidget)(vbox), name)
C.gtk_window_set_child((*C.GtkWindow)(window), (*C.GtkWidget)(vbox))
if menu != nil {
switch menuStyle {
case LinuxMenuStylePrimaryMenu:
headerBar := C.create_header_bar_with_menu((*C.GMenu)(menu))
C.gtk_window_set_titlebar((*C.GtkWindow)(window), headerBar)
default:
menuBar := C.create_menu_bar_from_model((*C.GMenu)(menu))
C.gtk_box_prepend((*C.GtkBox)(vbox), menuBar)
}
}
C.gtk_box_append((*C.GtkBox)(vbox), (*C.GtkWidget)(webview))
C.gtk_widget_set_vexpand((*C.GtkWidget)(webview), C.gboolean(1))
C.gtk_widget_set_hexpand((*C.GtkWidget)(webview), C.gboolean(1))
return
}
func windowNewWebview(parentId uint, gpuPolicy WebviewGpuPolicy) pointer {
c := NewCalloc()
defer c.Free()
manager := C.webkit_user_content_manager_new()
// WebKitGTK 6.0: register_script_message_handler signature changed
C.webkit_user_content_manager_register_script_message_handler(manager, c.String("external"), nil)
// WebKitGTK 6.0: Create network session first
networkSession := C.webkit_network_session_get_default()
// Create web view with settings
settings := C.webkit_settings_new()
// WebKitGTK 6.0: webkit_web_view_new_with_user_content_manager() was removed
// Use create_webview_with_user_content_manager() helper instead
webView := C.create_webview_with_user_content_manager(manager)
C.save_webview_to_content_manager(unsafe.Pointer(manager), unsafe.Pointer(webView))
C.save_window_id(unsafe.Pointer(webView), C.uint(parentId))
C.save_window_id(unsafe.Pointer(manager), C.uint(parentId))
// GPU policy
// WebKitGTK 6.0: WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND was removed
// Only ALWAYS and NEVER are available
switch gpuPolicy {
case WebviewGpuPolicyNever:
C.webkit_settings_set_hardware_acceleration_policy(settings, C.WEBKIT_HARDWARE_ACCELERATION_POLICY_NEVER)
case WebviewGpuPolicyAlways:
C.webkit_settings_set_hardware_acceleration_policy(settings, C.WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS)
default:
// Default to ALWAYS (was ON_DEMAND in older WebKitGTK)
C.webkit_settings_set_hardware_acceleration_policy(settings, C.WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS)
}
C.webkit_web_view_set_settings(C.webkit_web_view((*C.GtkWidget)(webView)), settings)
// Register URI scheme handler
registerURIScheme.Do(func() {
webContext := C.webkit_web_view_get_context(C.webkit_web_view((*C.GtkWidget)(webView)))
cScheme := C.CString("wails")
defer C.free(unsafe.Pointer(cScheme))
C.webkit_web_context_register_uri_scheme(webContext, cScheme,
(*[0]byte)(C.onProcessRequest), nil, nil)
})
_ = networkSession
return pointer(webView)
}
func gtkBool(b bool) C.gboolean {
if b {
return C.gboolean(1)
}
return C.gboolean(0)
}
func (w *linuxWebviewWindow) gtkWindow() *C.GtkWindow {
return (*C.GtkWindow)(w.window)
}
func (w *linuxWebviewWindow) webKitWebView() *C.WebKitWebView {
return C.webkit_web_view((*C.GtkWidget)(w.webview))
}
func (w *linuxWebviewWindow) present() {
C.gtk_window_present(w.gtkWindow())
}
func (w *linuxWebviewWindow) setTitle(title string) {
if !w.parent.options.Frameless {
cTitle := C.CString(title)
C.gtk_window_set_title(w.gtkWindow(), cTitle)
C.free(unsafe.Pointer(cTitle))
}
}
func (w *linuxWebviewWindow) setSize(width, height int) {
C.gtk_window_set_default_size(w.gtkWindow(), C.int(width), C.int(height))
}
func (w *linuxWebviewWindow) setDefaultSize(width int, height int) {
C.gtk_window_set_default_size(w.gtkWindow(), C.int(width), C.int(height))
}
func windowSetGeometryHints(window pointer, minWidth, minHeight, maxWidth, maxHeight int) {
w := (*C.GtkWidget)(window)
if minWidth > 0 && minHeight > 0 {
C.gtk_widget_set_size_request(w, C.int(minWidth), C.int(minHeight))
}
if maxWidth > 0 || maxHeight > 0 {
C.window_set_max_size((*C.GtkWindow)(window), C.int(maxWidth), C.int(maxHeight))
}
}
func (w *linuxWebviewWindow) setResizable(resizable bool) {
C.gtk_window_set_resizable(w.gtkWindow(), gtkBool(resizable))
}
func (w *linuxWebviewWindow) move(x, y int) {
// C-side GDK_IS_X11_DISPLAY check handles X11 vs Wayland correctly,
// including XWayland and GDK_BACKEND=x11 scenarios.
C.window_move_x11(w.gtkWindow(), C.int(x), C.int(y))
}
func (w *linuxWebviewWindow) position() (int, int) {
// C-side GDK_IS_X11_DISPLAY check handles X11 vs Wayland correctly,
// returning 0,0 on non-X11 displays.
var x, y C.int
C.window_get_position_x11(w.gtkWindow(), &x, &y)
return int(x), int(y)
}
func (w *linuxWebviewWindow) unfullscreen() {
C.gtk_window_unfullscreen(w.gtkWindow())
w.unmaximise()
}
func (w *linuxWebviewWindow) unmaximise() {
C.gtk_window_unmaximize(w.gtkWindow())
}
func (w *linuxWebviewWindow) windowShow() {
if w.gtkWidget() == nil {
return
}
C.gtk_window_present(w.gtkWindow())
}
func (w *linuxWebviewWindow) setAlwaysOnTop(alwaysOnTop bool) {
// GTK4: No direct equivalent - compositor-dependent
}
func (w *linuxWebviewWindow) setBorderless(borderless bool) {
C.gtk_window_set_decorated(w.gtkWindow(), gtkBool(!borderless))
}
func (w *linuxWebviewWindow) setFrameless(frameless bool) {
C.gtk_window_set_decorated(w.gtkWindow(), gtkBool(!frameless))
}
func (w *linuxWebviewWindow) setTransparent() {
// GTK4: Transparency via CSS - different from GTK3
}
func (w *linuxWebviewWindow) setBackgroundColour(colour RGBA) {
rgba := C.GdkRGBA{C.float(colour.Red) / 255.0, C.float(colour.Green) / 255.0, C.float(colour.Blue) / 255.0, C.float(colour.Alpha) / 255.0}
C.webkit_web_view_set_background_color(w.webKitWebView(), &rgba)
}
func (w *linuxWebviewWindow) setIcon(icon pointer) {
// GTK4: Window icons handled differently - no gtk_window_set_icon
}
func (w *linuxWebviewWindow) startDrag() error {
C.beginWindowDrag(
w.gtkWindow(),
C.int(w.drag.MouseButton),
C.double(w.drag.XRoot),
C.double(w.drag.YRoot),
C.guint32(w.drag.DragTime))
return nil
}
// startResize is handled by webview_window_linux.go
// GTK4-specific resize using beginWindowResize can be added via a helper function
func (w *linuxWebviewWindow) getZoom() float64 {
return float64(C.webkit_web_view_get_zoom_level(w.webKitWebView()))
}
func (w *linuxWebviewWindow) setZoom(zoom float64) {
if zoom < 1 {
zoom = 1
}
C.webkit_web_view_set_zoom_level(w.webKitWebView(), C.gdouble(zoom))
}
func (w *linuxWebviewWindow) zoomIn() {
w.setZoom(w.getZoom() * 1.10)
}
func (w *linuxWebviewWindow) zoomOut() {
w.setZoom(w.getZoom() / 1.10)
}
func (w *linuxWebviewWindow) zoomReset() {
w.setZoom(1.0)
}
func (w *linuxWebviewWindow) reload() {
uri := C.CString("wails://")
C.webkit_web_view_load_uri(w.webKitWebView(), uri)
C.free(unsafe.Pointer(uri))
}
func (w *linuxWebviewWindow) setURL(uri string) {
target := C.CString(uri)
C.webkit_web_view_load_uri(w.webKitWebView(), target)
C.free(unsafe.Pointer(target))
}
func (w *linuxWebviewWindow) setHTML(html string) {
cHTML := C.CString(html)
uri := C.CString("wails://")
empty := C.CString("")
defer C.free(unsafe.Pointer(cHTML))
defer C.free(unsafe.Pointer(uri))
defer C.free(unsafe.Pointer(empty))
C.webkit_web_view_load_alternate_html(w.webKitWebView(), cHTML, uri, empty)
}
func (w *linuxWebviewWindow) flash(_ bool) {}
func (w *linuxWebviewWindow) setOpacity(opacity float64) {
C.gtk_widget_set_opacity(w.gtkWidget(), C.double(opacity))
}
func (w *linuxWebviewWindow) ignoreMouse(ignore bool) {
// GTK4: Input handling is different
}
func (w *linuxWebviewWindow) copy() {
w.execJS("document.execCommand('copy')")
}
func (w *linuxWebviewWindow) cut() {
w.execJS("document.execCommand('cut')")
}
func (w *linuxWebviewWindow) paste() {
w.execJS("document.execCommand('paste')")
}
func (w *linuxWebviewWindow) delete() {
w.execJS("document.execCommand('delete')")
}
func (w *linuxWebviewWindow) selectAll() {
w.execJS("document.execCommand('selectAll')")
}
func (w *linuxWebviewWindow) undo() {
w.execJS("document.execCommand('undo')")
}
func (w *linuxWebviewWindow) redo() {
w.execJS("document.execCommand('redo')")
}
func (w *linuxWebviewWindow) setupSignalHandlers(emit func(e events.WindowEventType)) {
c := NewCalloc()
defer c.Free()
winID := C.uintptr_t(w.parent.ID())
C.setupWindowEventControllers(w.gtkWindow(), (*C.GtkWidget)(w.webview), winID)
wv := unsafe.Pointer(w.webview)
C.signal_connect(wv, c.String("load-changed"), C.handleLoadChanged, winID)
contentManager := C.webkit_web_view_get_user_content_manager(w.webKitWebView())
C.signal_connect(unsafe.Pointer(contentManager), c.String("script-message-received::external"), C.sendMessageToBackend, 0)
}
//export handleCloseRequest
func handleCloseRequest(window *C.GtkWindow, data C.uintptr_t) C.gboolean {
processWindowEvent(C.uint(data), C.uint(events.Linux.WindowDeleteEvent))
return C.gboolean(1)
}
//export handleNotifyState
func handleNotifyState(object *C.GObject, pspec *C.GParamSpec, data C.uintptr_t) {
windowId := uint(data)
window, ok := globalApplication.Window.GetByID(windowId)
if !ok || window == nil {
return
}
lw := getLinuxWebviewWindow(window)
if lw == nil {
return
}
if lw.isMaximised() {
processWindowEvent(C.uint(data), C.uint(events.Linux.WindowDidResize))
}
if lw.isFullscreen() {
processWindowEvent(C.uint(data), C.uint(events.Linux.WindowDidResize))
}
}
//export handleFocusEnter
func handleFocusEnter(controller *C.GtkEventController, data C.uintptr_t) C.gboolean {
processWindowEvent(C.uint(data), C.uint(events.Linux.WindowFocusIn))
return C.gboolean(0)
}
//export handleFocusLeave
func handleFocusLeave(controller *C.GtkEventController, data C.uintptr_t) C.gboolean {
processWindowEvent(C.uint(data), C.uint(events.Linux.WindowFocusOut))
return C.gboolean(0)
}
//export handleLoadChanged
func handleLoadChanged(wv *C.WebKitWebView, event C.WebKitLoadEvent, data C.uintptr_t) {
switch event {
case C.WEBKIT_LOAD_STARTED:
processWindowEvent(C.uint(data), C.uint(events.Linux.WindowLoadStarted))
case C.WEBKIT_LOAD_REDIRECTED:
processWindowEvent(C.uint(data), C.uint(events.Linux.WindowLoadRedirected))
case C.WEBKIT_LOAD_COMMITTED:
processWindowEvent(C.uint(data), C.uint(events.Linux.WindowLoadCommitted))
case C.WEBKIT_LOAD_FINISHED:
processWindowEvent(C.uint(data), C.uint(events.Linux.WindowLoadFinished))
}
}
//export handleButtonPressed
func handleButtonPressed(gesture *C.GtkGestureClick, nPress C.gint, x C.gdouble, y C.gdouble, data C.uintptr_t) {
windowId := uint(data)
window, ok := globalApplication.Window.GetByID(windowId)
if !ok || window == nil {
return
}
lw := getLinuxWebviewWindow(window)
if lw == nil {
return
}
button := C.gtk_gesture_single_get_current_button((*C.GtkGestureSingle)(unsafe.Pointer(gesture)))
lw.drag.MouseButton = uint(button)
lw.drag.XRoot = int(x)
lw.drag.YRoot = int(y)
lw.drag.DragTime = uint32(C.GDK_CURRENT_TIME)
}
//export handleButtonReleased
func handleButtonReleased(gesture *C.GtkGestureClick, nPress C.gint, x C.gdouble, y C.gdouble, data C.uintptr_t) {
windowId := uint(data)
window, ok := globalApplication.Window.GetByID(windowId)
if !ok || window == nil {
return
}
lw := getLinuxWebviewWindow(window)
if lw == nil {
return
}
button := C.gtk_gesture_single_get_current_button((*C.GtkGestureSingle)(unsafe.Pointer(gesture)))
lw.endDrag(uint(button), int(x), int(y))
}
//export handleKeyPressed
func handleKeyPressed(controller *C.GtkEventControllerKey, keyval C.guint, keycode C.guint, state C.GdkModifierType, data C.uintptr_t) C.gboolean {
windowID := uint(data)
modifiers := uint(state)
var acc accelerator
if modifiers&C.GDK_SHIFT_MASK != 0 {
acc.Modifiers = append(acc.Modifiers, ShiftKey)
}
if modifiers&C.GDK_CONTROL_MASK != 0 {
acc.Modifiers = append(acc.Modifiers, ControlKey)
}
if modifiers&C.GDK_ALT_MASK != 0 {
acc.Modifiers = append(acc.Modifiers, OptionOrAltKey)
}
if modifiers&C.GDK_SUPER_MASK != 0 {
acc.Modifiers = append(acc.Modifiers, SuperKey)
}
keyString, ok := VirtualKeyCodes[uint(keyval)]
if !ok {
return C.gboolean(0)
}
acc.Key = keyString
windowKeyEvents <- &windowKeyEvent{
windowId: windowID,
acceleratorString: acc.String(),
}
return C.gboolean(0)
}
//export onDropEnter
func onDropEnter(data C.uintptr_t) {
windowId := uint(data)
targetWindow, ok := globalApplication.Window.GetByID(windowId)
if !ok || targetWindow == nil {
return
}
if w, ok := targetWindow.(*WebviewWindow); ok {
w.HandleDragEnter()
}
}
//export onDropLeave
func onDropLeave(data C.uintptr_t) {
windowId := uint(data)
targetWindow, ok := globalApplication.Window.GetByID(windowId)
if !ok || targetWindow == nil {
return
}
if w, ok := targetWindow.(*WebviewWindow); ok {
w.HandleDragLeave()
}
}
//export onDropMotion
func onDropMotion(x C.gint, y C.gint, data C.uintptr_t) {
windowId := uint(data)
targetWindow, ok := globalApplication.Window.GetByID(windowId)
if !ok || targetWindow == nil {
return
}
if w, ok := targetWindow.(*WebviewWindow); ok {
w.HandleDragOver(int(x), int(y))
}
}
//export onDropFiles
func onDropFiles(paths **C.char, x C.gint, y C.gint, data C.uintptr_t) {
windowId := uint(data)
targetWindow, ok := globalApplication.Window.GetByID(windowId)
if !ok || targetWindow == nil {
return
}
offset := unsafe.Sizeof(uintptr(0))
var filenames []string
for *paths != nil {
filenames = append(filenames, C.GoString(*paths))
paths = (**C.char)(unsafe.Pointer(uintptr(unsafe.Pointer(paths)) + offset))
}
targetWindow.InitiateFrontendDropProcessing(filenames, int(x), int(y))
}
//export processWindowEvent
func processWindowEvent(windowID C.uint, eventID C.uint) {
windowEvents <- &windowEvent{
WindowID: uint(windowID),
EventID: uint(eventID),
}
}
//export onProcessRequest
func onProcessRequest(request *C.WebKitURISchemeRequest, data C.uintptr_t) {
webView := C.webkit_uri_scheme_request_get_web_view(request)
windowId := uint(C.get_window_id(unsafe.Pointer(webView)))
webviewRequests <- &webViewAssetRequest{
Request: webview.NewRequest(unsafe.Pointer(request)),
windowId: windowId,
windowName: func() string {
if window, ok := globalApplication.Window.GetByID(windowId); ok {
return window.Name()
}
return ""
}(),
}
}
// WebKitGTK 6.0: callback now receives JSCValue directly instead of WebKitJavascriptResult
//
//export sendMessageToBackend
func sendMessageToBackend(contentManager *C.WebKitUserContentManager, value *C.JSCValue,
data unsafe.Pointer) {
// Get the windowID from the contentManager
thisWindowID := uint(C.get_window_id(unsafe.Pointer(contentManager)))
webView := C.get_webview_from_content_manager(unsafe.Pointer(contentManager))
var origin string
if webView != nil {
currentUri := C.webkit_web_view_get_uri(webView)
if currentUri != nil {
uri := C.g_strdup(currentUri)
defer C.g_free(C.gpointer(uri))
origin = C.GoString(uri)
}
}
// WebKitGTK 6.0: JSCValue is passed directly, no need for webkit_javascript_result_get_js_value
message := C.jsc_value_to_string(value)
msg := C.GoString(message)
defer C.g_free(C.gpointer(message))
windowMessageBuffer <- &windowMessage{
windowId: thisWindowID,
message: msg,
originInfo: &OriginInfo{
Origin: origin,
},
}
}
// ============================================================================
// GTK4 Dialog System - Go wrapper functions
// ============================================================================
// Dialog request tracking
var (
dialogRequestCounter uint32
dialogRequestMutex sync.Mutex
fileDialogCallbacks = make(map[uint]chan string)
alertDialogCallbacks = make(map[uint]chan int)
)
func nextDialogRequestID() uint {
dialogRequestMutex.Lock()
defer dialogRequestMutex.Unlock()
dialogRequestCounter++
return uint(dialogRequestCounter)
}
//export fileDialogCallback
func fileDialogCallback(requestID C.uint, files **C.char, count C.int, cancelled C.gboolean) {
dialogRequestMutex.Lock()
ch, ok := fileDialogCallbacks[uint(requestID)]
if ok {
delete(fileDialogCallbacks, uint(requestID))
}
dialogRequestMutex.Unlock()
if !ok {
return
}
if cancelled != 0 {
close(ch)
return
}
if count > 0 && files != nil {
slice := unsafe.Slice(files, int(count))
for _, cstr := range slice {
if cstr != nil {
ch <- C.GoString(cstr)
}
}
}
close(ch)
}
//export alertDialogCallback
func alertDialogCallback(requestID C.uint, buttonIndex C.int) {
dialogRequestMutex.Lock()
ch, ok := alertDialogCallbacks[uint(requestID)]
if ok {
delete(alertDialogCallbacks, uint(requestID))
}
dialogRequestMutex.Unlock()
if !ok {
return
}
ch <- int(buttonIndex)
close(ch)
}
func runChooserDialog(window pointer, allowMultiple, createFolders, showHidden bool, currentFolder, title string, action int, acceptLabel string, filters []FileFilter) (chan string, error) {
requestID := nextDialogRequestID()
resultChan := make(chan string, 100)
dialogRequestMutex.Lock()
fileDialogCallbacks[requestID] = resultChan
dialogRequestMutex.Unlock()
InvokeAsync(func() {
cTitle := C.CString(title)
defer C.free(unsafe.Pointer(cTitle))
dialog := C.create_file_dialog(cTitle)
// Create filter list if we have filters
if len(filters) > 0 {
filterStore := C.g_list_store_new(C.gtk_file_filter_get_type())
defer C.g_object_unref(C.gpointer(filterStore))
for _, filter := range filters {
cName := C.CString(filter.DisplayName)
cPattern := C.CString(filter.Pattern)
C.add_file_filter(dialog, filterStore, cName, cPattern)
C.free(unsafe.Pointer(cName))
C.free(unsafe.Pointer(cPattern))
}
C.set_file_dialog_filters(dialog, filterStore)
}
if currentFolder != "" {
cFolder := C.CString(currentFolder)
file := C.g_file_new_for_path(cFolder)
C.gtk_file_dialog_set_initial_folder(dialog, file)
C.g_object_unref(C.gpointer(file))
C.free(unsafe.Pointer(cFolder))
}
if acceptLabel != "" {
cLabel := C.CString(acceptLabel)
C.gtk_file_dialog_set_accept_label(dialog, cLabel)
C.free(unsafe.Pointer(cLabel))
}
var parent *C.GtkWindow
if window != nil {
parent = (*C.GtkWindow)(window)
}
isFolder := action == 2
isSave := action == 1
if isSave {
C.show_save_file_dialog(parent, dialog, C.uint(requestID))
} else {
C.show_open_file_dialog(parent, dialog, C.uint(requestID), gtkBool(allowMultiple), gtkBool(isFolder))
}
})
return resultChan, nil
}
func runOpenFileDialog(dialog *OpenFileDialogStruct) (chan string, error) {
var action int
if dialog.canChooseDirectories {
action = 2 // GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER
} else {
action = 0 // GTK_FILE_CHOOSER_ACTION_OPEN
}
window := nilPointer
if dialog.window != nil {
nativeWindow := dialog.window.NativeWindow()
if nativeWindow != nil {
window = pointer(nativeWindow)
}
}
buttonText := dialog.buttonText
if buttonText == "" {
buttonText = "_Open"
}
return runChooserDialog(
window,
dialog.allowsMultipleSelection,
false, // createFolders not applicable for open
dialog.showHiddenFiles,
dialog.directory,
dialog.title,
action,
buttonText,
dialog.filters,
)
}
func runSaveFileDialog(dialog *SaveFileDialogStruct) (chan string, error) {
window := nilPointer
if dialog.window != nil {
nativeWindow := dialog.window.NativeWindow()
if nativeWindow != nil {
window = pointer(nativeWindow)
}
}
buttonText := dialog.buttonText
if buttonText == "" {
buttonText = "_Save"
}
return runChooserDialog(
window,
false,
dialog.canCreateDirectories,
dialog.showHiddenFiles,
dialog.directory,
dialog.title,
1, // GTK_FILE_CHOOSER_ACTION_SAVE
buttonText,
dialog.filters,
)
}
func dialogTypeToIconName(dialogType DialogType) string {
switch dialogType {
case InfoDialogType:
return "dialog-information-symbolic"
case WarningDialogType:
return "dialog-warning-symbolic"
case ErrorDialogType:
return "dialog-error-symbolic"
case QuestionDialogType:
return "dialog-question-symbolic"
default:
return ""
}
}
func runQuestionDialog(parent pointer, options *MessageDialog) int {
requestID := nextDialogRequestID()
resultChan := make(chan int, 1)
dialogRequestMutex.Lock()
alertDialogCallbacks[requestID] = resultChan
dialogRequestMutex.Unlock()
InvokeAsync(func() {
cHeading := C.CString(options.Title)
defer C.free(unsafe.Pointer(cHeading))
var cBody *C.char
if options.Message != "" {
cBody = C.CString(options.Message)
defer C.free(unsafe.Pointer(cBody))
}
var cIconName *C.char
var iconData *C.uchar
var iconDataLen C.int
if len(options.Icon) > 0 {
iconData = (*C.uchar)(unsafe.Pointer(&options.Icon[0]))
iconDataLen = C.int(len(options.Icon))
} else {
iconName := dialogTypeToIconName(options.DialogType)
if iconName != "" {
cIconName = C.CString(iconName)
defer C.free(unsafe.Pointer(cIconName))
}
}
buttons := options.Buttons
if len(buttons) == 0 {
buttons = []*Button{{Label: "OK", IsDefault: true}}
}
buttonLabels := make([]*C.char, len(buttons)+1)
for i, btn := range buttons {
buttonLabels[i] = C.CString(btn.Label)
}
buttonLabels[len(buttons)] = nil
defer func() {
for _, label := range buttonLabels[:len(buttons)] {
C.free(unsafe.Pointer(label))
}
}()
defaultButton := -1
cancelButton := -1
destructiveButton := -1
for i, btn := range buttons {
if btn.IsDefault {
defaultButton = i
}
if btn.IsCancel {
cancelButton = i
}
}
if options.DialogType == ErrorDialogType || options.DialogType == WarningDialogType {
if defaultButton >= 0 && !buttons[defaultButton].IsCancel {
destructiveButton = defaultButton
defaultButton = -1
}
}
var parentWindow *C.GtkWindow
if parent != nil {
parentWindow = (*C.GtkWindow)(parent)
}
C.show_message_dialog(
parentWindow,
cHeading,
cBody,
cIconName,
iconData,
iconDataLen,
(**C.char)(unsafe.Pointer(&buttonLabels[0])),
C.int(len(buttons)),
C.int(defaultButton),
C.int(cancelButton),
C.int(destructiveButton),
C.uint(requestID),
)
})
// Wait for result
result := <-resultChan
return result
}
func getPrimaryScreen() (*Screen, error) {
display := C.gdk_display_get_default()
monitors := C.gdk_display_get_monitors(display)
if monitors == nil {
return nil, fmt.Errorf("no monitors found")
}
count := C.g_list_model_get_n_items(monitors)
if count == 0 {
return nil, fmt.Errorf("no monitors found")
}
monitor := (*C.GdkMonitor)(C.g_list_model_get_item(monitors, 0))
if monitor == nil {
return nil, fmt.Errorf("failed to get primary monitor")
}
defer C.g_object_unref(C.gpointer(monitor))
var geometry C.GdkRectangle
C.gdk_monitor_get_geometry(monitor, &geometry)
// Use gdk_monitor_get_scale (GTK 4.14+) for fractional scaling support
scaleFactor := float64(C.gdk_monitor_get_scale(monitor))
name := C.gdk_monitor_get_model(monitor)
// GTK4's gdk_monitor_get_geometry returns logical (DIP) coordinates.
// PhysicalBounds needs physical pixel dimensions for proper DPI scaling.
x := int(geometry.x)
y := int(geometry.y)
width := int(geometry.width)
height := int(geometry.height)
return &Screen{
ID: "0",
Name: C.GoString(name),
IsPrimary: true,
X: x,
Y: y,
Size: Size{
Height: height,
Width: width,
},
Bounds: Rect{
X: x,
Y: y,
Height: height,
Width: width,
},
ScaleFactor: float32(scaleFactor),
WorkArea: Rect{
X: x,
Y: y,
Height: height,
Width: width,
},
PhysicalBounds: Rect{
X: int(float64(x) * scaleFactor),
Y: int(float64(y) * scaleFactor),
Height: int(float64(height) * scaleFactor),
Width: int(float64(width) * scaleFactor),
},
PhysicalWorkArea: Rect{
X: int(float64(x) * scaleFactor),
Y: int(float64(y) * scaleFactor),
Height: int(float64(height) * scaleFactor),
Width: int(float64(width) * scaleFactor),
},
Rotation: 0.0,
}, nil
}
func openDevTools(wv pointer) {
inspector := C.webkit_web_view_get_inspector((*C.WebKitWebView)(wv))
C.webkit_web_inspector_show(inspector)
}
func enableDevTools(wv pointer) {
settings := C.webkit_web_view_get_settings((*C.WebKitWebView)(wv))
enabled := C.webkit_settings_get_enable_developer_extras(settings)
if enabled == 0 {
C.webkit_settings_set_enable_developer_extras(settings, C.gboolean(1))
} else {
C.webkit_settings_set_enable_developer_extras(settings, C.gboolean(0))
}
}
var _ = time.Now
var _ = events.Linux
var _ = strings.TrimSpace