diff --git a/v3/pkg/application/application_linux_purego.go b/v3/pkg/application/application_linux_purego.go new file mode 100644 index 000000000..6dae3da20 --- /dev/null +++ b/v3/pkg/application/application_linux_purego.go @@ -0,0 +1,226 @@ +//go:build linux && purego + +package application + +import ( + "fmt" + "log" + "os" + "strings" + "time" + + "github.com/ebitengine/purego" + "github.com/wailsapp/wails/v2/pkg/assetserver/webview" +) + +const ( + gtk3 = "libgtk-3.so" + gtk4 = "libgtk-4.so" +) + +var ( + gtk uintptr + version int + webkit uintptr +) + +func init() { + // needed for GTK4 to function + _ = os.Setenv("GDK_BACKEND", "x11") + var err error + /* + gtk, err = purego.Dlopen(gtk4, purego.RTLD_NOW|purego.RTLD_GLOBAL) + if err == nil { + version = 4 + return + } + + log.Println("Failed to open GTK4: Falling back to GTK3") + */ + gtk, err = purego.Dlopen(gtk3, purego.RTLD_NOW|purego.RTLD_GLOBAL) + if err != nil { + panic(err) + } + version = 3 + + var webkit4 string = "libwebkit2gtk-4.1.so" + webkit, err = purego.Dlopen(webkit4, purego.RTLD_NOW|purego.RTLD_GLOBAL) + if err != nil { + panic(err) + } +} + +type linuxApp struct { + appName string + application uintptr + applicationMenu uintptr + parent *App +} + +func (m *linuxApp) hide() { + // C.hide() +} + +func (m *linuxApp) show() { + // C.show() +} + +func (m *linuxApp) on(eventID uint) { + log.Println("linuxApp.on()", eventID) + // TODO: Setup signal handling as appropriate + // Note: GTK signals seem to be strings! +} + +func (m *linuxApp) setIcon(icon []byte) { + // C.setApplicationIcon(unsafe.Pointer(&icon[0]), C.int(len(icon))) +} + +func (m *linuxApp) name() string { + return m.appName +} + +func (m *linuxApp) getCurrentWindowID() uint { + fmt.Println("getCurrentWindowID") + var getCurrentWindow func(uintptr) uintptr + purego.RegisterLibFunc(&getCurrentWindow, gtk, "gtk_application_get_active_window") + // window := getCurrentWindow(m.application) + // if window != 0 { + // webview := (*WebviewWindow)(window) + // return webview.id + // } + return uint(1) +} + +func (m *linuxApp) setApplicationMenu(menu *Menu) { + if menu == nil { + // Create a default menu + menu = defaultApplicationMenu() + } + + menu.Update() + m.applicationMenu = (menu.impl).(*linuxMenu).native +} + +func (m *linuxApp) activate() { + fmt.Println("linuxApp.activated!", m.application) + var hold func(uintptr) + purego.RegisterLibFunc(&hold, gtk, "g_application_hold") + hold(m.application) + + time.Sleep(50 * time.Millisecond) + m.parent.activate() +} + +func (m *linuxApp) run() error { + // Add a hook to the ApplicationDidFinishLaunching event + // FIXME: add Wails specific events - i.e. Shouldn't platform specific ones be translated to Wails events? + /* m.parent.On(events.Mac.ApplicationDidFinishLaunching, func() { + // Do we need to do anything now? + fmt.Println("ApplicationDidFinishLaunching!") + }) + */ + var g_signal_connect func(uintptr, string, uintptr, uintptr, bool, int) int + purego.RegisterLibFunc(&g_signal_connect, gtk, "g_signal_connect_data") + g_signal_connect(m.application, "activate", purego.NewCallback(m.activate), m.application, false, 0) + + var run func(uintptr, int, []string) int + purego.RegisterLibFunc(&run, gtk, "g_application_run") + + // FIXME: Convert status to 'error' if needed + status := run(m.application, 0, []string{}) + fmt.Println("status", status) + + var release func(uintptr) + purego.RegisterLibFunc(&release, gtk, "g_application_release") + release(m.application) + + purego.RegisterLibFunc(&release, gtk, "g_object_unref") + release(m.application) + + return nil +} + +func (m *linuxApp) destroy() { + var quit func(uintptr) + purego.RegisterLibFunc(&quit, gtk, "g_application_quit") + quit(m.application) +} + +func newPlatformApp(parent *App) *linuxApp { + name := strings.ToLower(parent.options.Name) + if name == "" { + name = "undefined" + } + identifier := fmt.Sprintf("org.wails.%s", strings.Replace(name, " ", "-", -1)) + + var gtkNew func(string, uint) uintptr + purego.RegisterLibFunc(>kNew, gtk, "gtk_application_new") + app := &linuxApp{ + appName: identifier, + parent: parent, + application: gtkNew(identifier, 0), + } + return app +} + +func processApplicationEvent(eventID uint) { + // TODO: add translation to Wails events + // currently reusing Mac specific values + applicationEvents <- eventID +} + +func processWindowEvent(windowID uint, eventID uint) { + windowEvents <- &WindowEvent{ + WindowID: windowID, + EventID: eventID, + } +} + +func processMessage(windowID uint, message string) { + windowMessageBuffer <- &windowMessage{ + windowId: windowID, + message: message, + } +} + +func processURLRequest(windowID uint, wkUrlSchemeTask uintptr) { + fmt.Println("processURLRequest", windowID, wkUrlSchemeTask) + webviewRequests <- &webViewAssetRequest{ + Request: webview.NewRequest(wkUrlSchemeTask), + windowId: windowID, + windowName: globalApplication.getWindowForID(windowID).Name(), + } +} + +func processDragItems(windowID uint, arr []string, length int) { + windowDragAndDropBuffer <- &dragAndDropMessage{ + windowId: windowID, + filenames: arr, + } +} + +func processMenuItemClick(menuID uint) { + menuItemClicked <- menuID +} + +func setIcon(icon []byte) { + if icon == nil { + return + } + fmt.Println("setIcon") + /* + GdkPixbufLoader *loader = gdk_pixbuf_loader_new(); + if (!loader) + { + return; + } + if (gdk_pixbuf_loader_write(loader, buf, len, NULL) && gdk_pixbuf_loader_close(loader, NULL)) + { + GdkPixbuf *pixbuf = gdk_pixbuf_loader_get_pixbuf(loader); + if (pixbuf) + { + gtk_window_set_icon(window, pixbuf); + } + } + g_object_unref(loader);*/ +} diff --git a/v3/pkg/application/dialogs_linux_purego.go b/v3/pkg/application/dialogs_linux_purego.go new file mode 100644 index 000000000..757b4f3d3 --- /dev/null +++ b/v3/pkg/application/dialogs_linux_purego.go @@ -0,0 +1,220 @@ +//go:build linux && purego + +package application + +const AlertStyleWarning = 0 +const AlertStyleInformational = 1 +const AlertStyleCritical = 2 + +var alertTypeMap = map[DialogType]int{ + WarningDialog: AlertStyleWarning, + InfoDialog: AlertStyleInformational, + ErrorDialog: AlertStyleCritical, + QuestionDialog: AlertStyleInformational, +} + +func (m *linuxApp) showAboutDialog(title string, message string, icon []byte) { + // var iconData unsafe.Pointer + // if icon != nil { + // iconData = unsafe.Pointer(&icon[0]) + // } + //C.showAboutBox(C.CString(title), C.CString(message), iconData, C.int(len(icon))) +} + +type linuxDialog struct { + dialog *MessageDialog + + //nsDialog unsafe.Pointer +} + +func (m *linuxDialog) show() { + globalApplication.dispatchOnMainThread(func() { + + // Mac can only have 4 Buttons on a dialog + if len(m.dialog.Buttons) > 4 { + m.dialog.Buttons = m.dialog.Buttons[:4] + } + + // if m.nsDialog != nil { + // //C.releaseDialog(m.nsDialog) + // } + // var title *C.char + // if m.dialog.Title != "" { + // title = C.CString(m.dialog.Title) + // } + // var message *C.char + // if m.dialog.Message != "" { + // message = C.CString(m.dialog.Message) + // } + // var iconData unsafe.Pointer + // var iconLength C.int + // if m.dialog.Icon != nil { + // iconData = unsafe.Pointer(&m.dialog.Icon[0]) + // iconLength = C.int(len(m.dialog.Icon)) + // } else { + // // if it's an error, use the application Icon + // if m.dialog.DialogType == ErrorDialog { + // iconData = unsafe.Pointer(&globalApplication.options.Icon[0]) + // iconLength = C.int(len(globalApplication.options.Icon)) + // } + // } + + // alertType, ok := alertTypeMap[m.dialog.DialogType] + // if !ok { + // alertType = AlertStyleInformational + // } + + // m.nsDialog = C.createAlert(alertType, title, message, iconData, iconLength) + + // Reverse the Buttons so that the default is on the right + reversedButtons := make([]*Button, len(m.dialog.Buttons)) + var count = 0 + for i := len(m.dialog.Buttons) - 1; i >= 0; i-- { + //button := m.dialog.Buttons[i] + //C.alertAddButton(m.nsDialog, C.CString(button.Label), C.bool(button.IsDefault), C.bool(button.IsCancel)) + reversedButtons[count] = m.dialog.Buttons[i] + count++ + } + + buttonPressed := int(0) //C.dialogRunModal(m.nsDialog)) + if len(m.dialog.Buttons) > buttonPressed { + button := reversedButtons[buttonPressed] + if button.callback != nil { + button.callback() + } + } + }) + +} + +func newDialogImpl(d *MessageDialog) *linuxDialog { + return &linuxDialog{ + dialog: d, + } +} + +type linuxOpenFileDialog struct { + dialog *OpenFileDialog +} + +func newOpenFileDialogImpl(d *OpenFileDialog) *linuxOpenFileDialog { + return &linuxOpenFileDialog{ + dialog: d, + } +} + +func (m *linuxOpenFileDialog) show() ([]string, error) { + openFileResponses[m.dialog.id] = make(chan string) + // nsWindow := unsafe.Pointer(nil) + if m.dialog.window != nil { + // get NSWindow from window + //nsWindow = m.dialog.window.impl.(*macosWebviewWindow).nsWindow + } + + // Massage filter patterns into macOS format + // We iterate all filter patterns, tidy them up and then join them with a semicolon + // This should produce a single string of extensions like "png;jpg;gif" + // var filterPatterns string + // if len(m.dialog.filters) > 0 { + // var allPatterns []string + // for _, filter := range m.dialog.filters { + // patternComponents := strings.Split(filter.Pattern, ";") + // for i, component := range patternComponents { + // filterPattern := strings.TrimSpace(component) + // filterPattern = strings.TrimPrefix(filterPattern, "*.") + // patternComponents[i] = filterPattern + // } + // allPatterns = append(allPatterns, strings.Join(patternComponents, ";")) + // } + // filterPatterns = strings.Join(allPatterns, ";") + // } + + // C.showOpenFileDialog(C.uint(m.dialog.id), + // C.bool(m.dialog.canChooseFiles), + // C.bool(m.dialog.canChooseDirectories), + // C.bool(m.dialog.canCreateDirectories), + // C.bool(m.dialog.showHiddenFiles), + // C.bool(m.dialog.allowsMultipleSelection), + // C.bool(m.dialog.resolvesAliases), + // C.bool(m.dialog.hideExtension), + // C.bool(m.dialog.treatsFilePackagesAsDirectories), + // C.bool(m.dialog.allowsOtherFileTypes), + // toCString(filterPatterns), + // C.uint(len(filterPatterns)), + // toCString(m.dialog.message), + // toCString(m.dialog.directory), + // toCString(m.dialog.buttonText), + // nsWindow) + var result []string + for filename := range openFileResponses[m.dialog.id] { + result = append(result, filename) + } + return result, nil +} + +func openFileDialogCallback(id uint, path string) { + channel, ok := openFileResponses[id] + if ok { + channel <- path + } else { + panic("No channel found for open file dialog") + } +} + +func openFileDialogCallbackEnd(id uint) { + channel, ok := openFileResponses[id] + if ok { + close(channel) + delete(openFileResponses, id) + freeDialogID(id) + } else { + panic("No channel found for open file dialog") + } +} + +type linuxSaveFileDialog struct { + dialog *SaveFileDialog +} + +func newSaveFileDialogImpl(d *SaveFileDialog) *linuxSaveFileDialog { + return &linuxSaveFileDialog{ + dialog: d, + } +} + +func (m *linuxSaveFileDialog) show() (string, error) { + saveFileResponses[m.dialog.id] = make(chan string) + // nsWindow := unsafe.Pointer(nil) + if m.dialog.window != nil { + // get NSWindow from window + // nsWindow = m.dialog.window.impl.(*linuxWebviewWindow).nsWindow + } + + // C.showSaveFileDialog(C.uint(m.dialog.id), + // C.bool(m.dialog.canCreateDirectories), + // C.bool(m.dialog.showHiddenFiles), + // C.bool(m.dialog.canSelectHiddenExtension), + // C.bool(m.dialog.hideExtension), + // C.bool(m.dialog.treatsFilePackagesAsDirectories), + // C.bool(m.dialog.allowOtherFileTypes), + // toCString(m.dialog.message), + // toCString(m.dialog.directory), + // toCString(m.dialog.buttonText), + // toCString(m.dialog.filename), + // nsWindow) + return <-saveFileResponses[m.dialog.id], nil +} + +func saveFileDialogCallback(cid uint, path string) { + // put response on channel + channel, ok := saveFileResponses[cid] + if ok { + channel <- path + close(channel) + delete(saveFileResponses, cid) + freeDialogID(cid) + + } else { + panic("No channel found for save file dialog") + } +} diff --git a/v3/pkg/application/mainthread_linux_purego.go b/v3/pkg/application/mainthread_linux_purego.go new file mode 100644 index 000000000..fb69c96e3 --- /dev/null +++ b/v3/pkg/application/mainthread_linux_purego.go @@ -0,0 +1,30 @@ +//go:build linux && purego + +package application + +import "github.com/ebitengine/purego" + +const ( + G_SOURCE_REMOVE = 0 +) + +func (m *linuxApp) dispatchOnMainThread(id uint) { + var dispatch func(uintptr) + purego.RegisterLibFunc(&dispatch, gtk, "g_idle_add") + dispatch(purego.NewCallback(func(uintptr) int { + dispatchOnMainThreadCallback(id) + return G_SOURCE_REMOVE + })) +} + +func dispatchOnMainThreadCallback(callbackID uint) { + mainThreadFunctionStoreLock.RLock() + id := uint(callbackID) + fn := mainThreadFunctionStore[id] + if fn == nil { + Fatal("dispatchCallback called with invalid id: %v", id) + } + delete(mainThreadFunctionStore, id) + mainThreadFunctionStoreLock.RUnlock() + fn() +} diff --git a/v3/pkg/application/menu_linux_purego.go b/v3/pkg/application/menu_linux_purego.go new file mode 100644 index 000000000..36c72ca47 --- /dev/null +++ b/v3/pkg/application/menu_linux_purego.go @@ -0,0 +1,168 @@ +//go:build linux && purego + +package application + +import ( + "fmt" + + "github.com/ebitengine/purego" +) + +type linuxMenu struct { + menu *Menu + native uintptr +} + +func newMenuImpl(menu *Menu) *linuxMenu { + var newMenuBar func() uintptr + purego.RegisterLibFunc(&newMenuBar, gtk, "gtk_menu_bar_new") + result := &linuxMenu{ + menu: menu, + native: newMenuBar(), + } + return result +} + +func (m *linuxMenu) update() { + m.processMenu(m.menu) +} + +func (m *linuxMenu) processMenu(menu *Menu) { + var newMenu func() uintptr + purego.RegisterLibFunc(&newMenu, gtk, "gtk_menu_new") + if menu.impl == nil { + menu.impl = &linuxMenu{ + menu: menu, + native: newMenu(), + } + } + var currentRadioGroup uintptr + + for _, item := range menu.items { + // drop the group if we have run out of radio items + if item.itemType != radio { + currentRadioGroup = 0 + } + + switch item.itemType { + case submenu: + menuItem := newMenuItemImpl(item) + item.impl = menuItem + m.processMenu(item.submenu) + m.addSubMenuToItem(item.submenu, item) + m.addMenuItem(menu, item) + case text, checkbox: + menuItem := newMenuItemImpl(item) + item.impl = menuItem + m.addMenuItem(menu, item) + case radio: + menuItem := newRadioItemImpl(item, currentRadioGroup) + item.impl = menuItem + m.addMenuItem(menu, item) + + var radioGetGroup func(uintptr) uintptr + purego.RegisterLibFunc(&radioGetGroup, gtk, "gtk_radio_menu_item_get_group") + + currentRadioGroup = radioGetGroup(menuItem.native) + case separator: + m.addMenuSeparator(menu) + } + + } + + for _, item := range menu.items { + if item.callback != nil { + m.attachHandler(item) + } + } + +} + +func (m *linuxMenu) attachHandler(item *MenuItem) { + impl := (item.impl).(*linuxMenuItem) + widget := impl.native + flags := 0 + + var handleClick = func() { + item := item + switch item.itemType { + case text, checkbox: + processMenuItemClick(item.id) + case radio: + menuItem := (item.impl).(*linuxMenuItem) + if menuItem.isChecked() { + processMenuItemClick(item.id) + } + default: + fmt.Println("handleClick", item.itemType, item.id) + } + } + + var signalConnectObject func(uintptr, string, uintptr, uintptr, int) uint + purego.RegisterLibFunc(&signalConnectObject, gtk, "g_signal_connect_object") + handlerId := signalConnectObject( + widget, + "activate", + purego.NewCallback(handleClick), + widget, + flags) + + impl.handlerId = handlerId +} + +func (m *linuxMenu) addSubMenuToItem(menu *Menu, item *MenuItem) { + var newMenu func() uintptr + purego.RegisterLibFunc(&newMenu, gtk, "gtk_menu_new") + if menu.impl == nil { + menu.impl = &linuxMenu{ + menu: menu, + native: newMenu(), + } + } + var itemSetSubmenu func(uintptr, uintptr) + purego.RegisterLibFunc(&itemSetSubmenu, gtk, "gtk_menu_item_set_submenu") + + itemSetSubmenu( + (item.impl).(*linuxMenuItem).native, + (menu.impl).(*linuxMenu).native) + + if item.role == ServicesMenu { + // FIXME: what does this mean? + } +} + +func (m *linuxMenu) addMenuItem(parent *Menu, menu *MenuItem) { + var shellAppend func(uintptr, uintptr) + purego.RegisterLibFunc(&shellAppend, gtk, "gtk_menu_shell_append") + shellAppend( + (parent.impl).(*linuxMenu).native, + (menu.impl).(*linuxMenuItem).native, + ) +} + +func (m *linuxMenu) addMenuSeparator(menu *Menu) { + var newSeparator func() uintptr + purego.RegisterLibFunc(&newSeparator, gtk, "gtk_separator_menu_item_new") + var shellAppend func(uintptr, uintptr) + purego.RegisterLibFunc(&shellAppend, gtk, "gtk_menu_shell_append") + + sep := newSeparator() + native := (menu.impl).(*linuxMenu).native + shellAppend(native, sep) +} + +func (m *linuxMenu) addServicesMenu(menu *Menu) { + fmt.Println("addServicesMenu - not implemented") + //C.addServicesMenu(unsafe.Pointer(menu.impl.(*linuxMenu).nsMenu)) +} + +func (l *linuxMenu) createMenu(name string, items []*MenuItem) *Menu { + impl := newMenuImpl(&Menu{label: name}) + menu := &Menu{ + label: name, + items: items, + impl: impl, + } + impl.menu = menu + return menu +} diff --git a/v3/pkg/application/menuitem_linux_purego.go b/v3/pkg/application/menuitem_linux_purego.go new file mode 100644 index 000000000..f5dca543d --- /dev/null +++ b/v3/pkg/application/menuitem_linux_purego.go @@ -0,0 +1,396 @@ +//go:build linux && purego + +package application + +import ( + "fmt" + "runtime" + + "github.com/ebitengine/purego" +) + +type linuxMenuItem struct { + menuItem *MenuItem + native uintptr + handlerId uint +} + +func (l linuxMenuItem) setTooltip(tooltip string) { + globalApplication.dispatchOnMainThread(func() { + l.blockSignal() + defer l.unBlockSignal() + var setToolTip func(uintptr, string) + purego.RegisterLibFunc(&setToolTip, gtk, "gtk_widget_set_tooltip_text") + + setToolTip(l.native, tooltip) + }) +} + +func (l linuxMenuItem) blockSignal() { + var block func(uintptr, uint) + purego.RegisterLibFunc(&block, gtk, "g_signal_handler_block") + + if l.handlerId != 0 { + block(l.native, l.handlerId) + } +} + +func (l linuxMenuItem) unBlockSignal() { + var unblock func(uintptr, uint) + purego.RegisterLibFunc(&unblock, gtk, "g_signal_handler_unblock") + + if l.handlerId != 0 { + unblock(l.native, l.handlerId) + } +} + +func (l linuxMenuItem) setLabel(s string) { + globalApplication.dispatchOnMainThread(func() { + l.blockSignal() + defer l.unBlockSignal() + var setLabel func(uintptr, string) + purego.RegisterLibFunc(&setLabel, gtk, "gtk_menu_item_set_label") + setLabel(l.native, s) + }) +} + +func (l linuxMenuItem) isChecked() bool { + var getActive func(uintptr) int + purego.RegisterLibFunc(&getActive, gtk, "gtk_check_menu_item_get_active") + + if getActive(l.native) == 1 { + return true + } + return false +} + +func (l linuxMenuItem) setDisabled(disabled bool) { + + globalApplication.dispatchOnMainThread(func() { + l.blockSignal() + defer l.unBlockSignal() + + var setSensitive func(uintptr, int) + purego.RegisterLibFunc(&setSensitive, gtk, "gtk_widget_set_sensitive") + + value := 1 + if disabled { + value = 0 + } + setSensitive(l.native, value) + }) +} + +func (l linuxMenuItem) setChecked(checked bool) { + globalApplication.dispatchOnMainThread(func() { + l.blockSignal() + defer l.unBlockSignal() + + var setActive func(uintptr, int) + purego.RegisterLibFunc(&setActive, gtk, "gtk_check_menu_item_set_active") + + value := 0 + if checked { + value = 1 + } + setActive(l.native, value) + }) +} + +func (l linuxMenuItem) setAccelerator(accelerator *accelerator) { + fmt.Println("setAccelerator", accelerator) + // Set the keyboard shortcut of the menu item + // var modifier C.int + // var key *C.char + if accelerator != nil { + // modifier = C.int(toMacModifier(accelerator.Modifiers)) + // key = C.CString(accelerator.Key) + } + + // Convert the key to a string + // C.setMenuItemKeyEquivalent(m.nsMenuItem, key, modifier) +} + +func newMenuItemImpl(item *MenuItem) *linuxMenuItem { + result := &linuxMenuItem{ + menuItem: item, + } + var newWithLabel func(string) uintptr + purego.RegisterLibFunc(&newWithLabel, gtk, "gtk_menu_item_new_with_label") + var newCBWithLabel func(string) uintptr + purego.RegisterLibFunc(&newCBWithLabel, gtk, "gtk_check_menu_item_new_with_label") + + switch item.itemType { + case text: + fmt.Println("text", item.label) + result.native = newWithLabel(item.label) + + case checkbox: + fmt.Println("cb", item.label) + result.native = newCBWithLabel(item.label) + result.setChecked(item.checked) + if item.accelerator != nil { + result.setAccelerator(item.accelerator) + } + case radio: + panic("Shouldn't get here with a radio item") + + case submenu: + fmt.Println("submenu", item.label) + result.native = newWithLabel(item.label) + + default: + panic("WTF") + } + result.setDisabled(result.menuItem.disabled) + + return result +} + +func newRadioItemImpl(item *MenuItem, group uintptr) *linuxMenuItem { + var newWithLabel func(uintptr, string) uintptr + purego.RegisterLibFunc(&newWithLabel, gtk, "gtk_radio_menu_item_new_with_label") + + result := &linuxMenuItem{ + menuItem: item, + native: newWithLabel(group, item.label), + } + result.setChecked(item.checked) + result.setDisabled(result.menuItem.disabled) + return result +} + +func newSpeechMenu() *MenuItem { + speechMenu := NewMenu() + speechMenu.Add("Start Speaking"). + SetAccelerator("CmdOrCtrl+OptionOrAlt+Shift+."). + OnClick(func(ctx *Context) { + // C.startSpeaking() + }) + speechMenu.Add("Stop Speaking"). + SetAccelerator("CmdOrCtrl+OptionOrAlt+Shift+,"). + OnClick(func(ctx *Context) { + // C.stopSpeaking() + }) + subMenu := newSubMenuItem("Speech") + subMenu.submenu = speechMenu + return subMenu +} + +func newHideMenuItem() *MenuItem { + return newMenuItem("Hide " + globalApplication.options.Name). + SetAccelerator("CmdOrCtrl+h"). + OnClick(func(ctx *Context) { + // C.hideApplication() + }) +} + +func newHideOthersMenuItem() *MenuItem { + return newMenuItem("Hide Others"). + SetAccelerator("CmdOrCtrl+OptionOrAlt+h"). + OnClick(func(ctx *Context) { + // C.hideOthers() + }) +} + +func newUnhideMenuItem() *MenuItem { + return newMenuItem("Show All"). + OnClick(func(ctx *Context) { + // C.showAll() + }) +} + +func newUndoMenuItem() *MenuItem { + return newMenuItem("Undo"). + SetAccelerator("CmdOrCtrl+z"). + OnClick(func(ctx *Context) { + // C.undo() + }) +} + +// newRedoMenuItem creates a new menu item for redoing the last action +func newRedoMenuItem() *MenuItem { + return newMenuItem("Redo"). + SetAccelerator("CmdOrCtrl+Shift+z"). + OnClick(func(ctx *Context) { + // C.redo() + }) +} + +func newCutMenuItem() *MenuItem { + return newMenuItem("Cut"). + SetAccelerator("CmdOrCtrl+x"). + OnClick(func(ctx *Context) { + // C.cut() + }) +} + +func newCopyMenuItem() *MenuItem { + return newMenuItem("Copy"). + SetAccelerator("CmdOrCtrl+c"). + OnClick(func(ctx *Context) { + // C.copy() + }) +} + +func newPasteMenuItem() *MenuItem { + return newMenuItem("Paste"). + SetAccelerator("CmdOrCtrl+v"). + OnClick(func(ctx *Context) { + // C.paste() + }) +} + +func newPasteAndMatchStyleMenuItem() *MenuItem { + return newMenuItem("Paste and Match Style"). + SetAccelerator("CmdOrCtrl+OptionOrAlt+Shift+v"). + OnClick(func(ctx *Context) { + // C.pasteAndMatchStyle() + }) +} + +func newDeleteMenuItem() *MenuItem { + return newMenuItem("Delete"). + SetAccelerator("backspace"). + OnClick(func(ctx *Context) { + // C.delete() + }) +} + +func newQuitMenuItem() *MenuItem { + return newMenuItem("Quit " + globalApplication.options.Name). + SetAccelerator("CmdOrCtrl+q"). + OnClick(func(ctx *Context) { + globalApplication.Quit() + }) +} + +func newSelectAllMenuItem() *MenuItem { + return newMenuItem("Select All"). + SetAccelerator("CmdOrCtrl+a"). + OnClick(func(ctx *Context) { + // C.selectAll() + }) +} + +func newAboutMenuItem() *MenuItem { + return newMenuItem("About " + globalApplication.options.Name). + OnClick(func(ctx *Context) { + globalApplication.ShowAboutDialog() + }) +} + +func newCloseMenuItem() *MenuItem { + return newMenuItem("Close"). + SetAccelerator("CmdOrCtrl+w"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.Close() + } + }) +} + +func newReloadMenuItem() *MenuItem { + return newMenuItem("Reload"). + SetAccelerator("CmdOrCtrl+r"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.Reload() + } + }) +} + +func newForceReloadMenuItem() *MenuItem { + return newMenuItem("Force Reload"). + SetAccelerator("CmdOrCtrl+Shift+r"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.ForceReload() + } + }) +} + +func newToggleFullscreenMenuItem() *MenuItem { + result := newMenuItem("Toggle Full Screen"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.ToggleFullscreen() + } + }) + if runtime.GOOS == "darwin" { + result.SetAccelerator("Ctrl+Command+F") + } else { + result.SetAccelerator("F11") + } + return result +} + +func newToggleDevToolsMenuItem() *MenuItem { + return newMenuItem("Toggle Developer Tools"). + SetAccelerator("Alt+Command+I"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.ToggleDevTools() + } + }) +} + +func newZoomResetMenuItem() *MenuItem { + // reset zoom menu item + return newMenuItem("Actual Size"). + SetAccelerator("CmdOrCtrl+0"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.ZoomReset() + } + }) +} + +func newZoomInMenuItem() *MenuItem { + return newMenuItem("Zoom In"). + SetAccelerator("CmdOrCtrl+plus"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.ZoomIn() + } + }) +} + +func newZoomOutMenuItem() *MenuItem { + return newMenuItem("Zoom Out"). + SetAccelerator("CmdOrCtrl+-"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.ZoomOut() + } + }) +} + +func newMinimizeMenuItem() *MenuItem { + return newMenuItem("Minimize"). + SetAccelerator("CmdOrCtrl+M"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.Minimize() + } + }) +} + +func newZoomMenuItem() *MenuItem { + return newMenuItem("Zoom"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.Zoom() + } + }) +} diff --git a/v3/pkg/application/screen_linux_purego.go b/v3/pkg/application/screen_linux_purego.go new file mode 100644 index 000000000..6f7add617 --- /dev/null +++ b/v3/pkg/application/screen_linux_purego.go @@ -0,0 +1,92 @@ +//go:build linux && purego + +package application + +import ( + "fmt" + "sync" + "unsafe" + + "github.com/ebitengine/purego" +) + +func (m *linuxApp) getPrimaryScreen() (*Screen, error) { + return nil, fmt.Errorf("not implemented") +} + +func (m *linuxApp) getScreenByIndex(display uintptr, index int) *Screen { + fmt.Println("getScreenByIndex") + var getMonitor func(uintptr, int) uintptr + purego.RegisterLibFunc(&getMonitor, gtk, "gdk_display_get_monitor") + + monitor := getMonitor(display, index) + + // TODO: Do we need to update Screen to contain current info? + // currentMonitor := C.gdk_display_get_monitor_at_window(display, window) + + var getGeometry func(uintptr, uintptr) + purego.RegisterLibFunc(&getGeometry, gtk, "gdk_monitor_get_geometry") + + //var geometry C.GdkRectangle + /* + struct GdkRectangle { + int x; + int y; + int width; + int height; + } + */ + geometry := make([]byte, 16) + getGeometry(monitor, uintptr(unsafe.Pointer(&geometry[0]))) + fmt.Println("geometry: %v\n", geometry) + + var isPrimary func(uintptr) int + purego.RegisterLibFunc(&isPrimary, gtk, "gdk_monitor_is_primary") + + primary := false + if isPrimary(monitor) == 1 { + primary = true + } + + return &Screen{ + IsPrimary: primary, + Scale: 1.0, + X: 0, //int(geometry.x), + Y: 0, //int(geometry.y), + Size: Size{ + Height: 1024, //int(geometry.height), + Width: 1024, //int(geometry.width), + }, + } +} + +func (m *linuxApp) getScreens() ([]*Screen, error) { + fmt.Println("getScreens") + var wg sync.WaitGroup + var screens []*Screen + wg.Add(1) + + var getWindow func(uintptr) uintptr + purego.RegisterLibFunc(&getWindow, gtk, "gtk_application_get_active_window") + var getDisplay func(uintptr) uintptr + purego.RegisterLibFunc(&getDisplay, gtk, "gdk_window_get_display") + var getMonitorCount func(uintptr) int + purego.RegisterLibFunc(&getMonitorCount, gtk, "getNMonitors") + globalApplication.dispatchOnMainThread(func() { + window := getWindow(m.application) + display := getDisplay(window) + count := getMonitorCount(display) + for i := 0; i < int(count); i++ { + screens = append(screens, + m.getScreenByIndex(display, i), + ) + } + wg.Done() + }) + wg.Wait() + return screens, nil +} + +func getScreenForWindow(window *linuxWebviewWindow) (*Screen, error) { + return window.getScreen() +} diff --git a/v3/pkg/application/webview_window_linux_purego.go b/v3/pkg/application/webview_window_linux_purego.go new file mode 100644 index 000000000..c8e099355 --- /dev/null +++ b/v3/pkg/application/webview_window_linux_purego.go @@ -0,0 +1,725 @@ +//go:build linux && purego + +package application + +import ( + "fmt" + "net/url" + "sync" + "unsafe" + + "github.com/ebitengine/purego" + "github.com/wailsapp/wails/v2/pkg/menu" + "github.com/wailsapp/wails/v3/pkg/events" +) + +var ( + registered bool = false // avoid 'already registered message' +) + +const ( + // https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gdk/gdkwindow.h#L121 + GDK_HINT_MIN_SIZE = 1 << 1 + GDK_HINT_MAX_SIZE = 1 << 2 + // https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gdk/gdkevents.h#L512 + GDK_WINDOW_STATE_ICONIFIED = 1 << 1 + GDK_WINDOW_STATE_MAXIMIZED = 1 << 2 + GDK_WINDOW_STATE_FULLSCREEN = 1 << 4 +) + +type GdkGeometry struct { + minWidth int32 + minHeight int32 + maxWidth int32 + maxHeight int32 + baseWidth int32 + baseHeight int32 + widthInc int32 + heightInc int32 + padding int32 + minAspect float64 + maxAspect float64 + GdkGravity int32 +} + +type linuxWebviewWindow struct { + application uintptr + window uintptr + webview uintptr + parent *WebviewWindow + menubar uintptr + vbox uintptr + menu *menu.Menu + accels uintptr + minWidth, minHeight, maxWidth, maxHeight int +} + +func (w *linuxWebviewWindow) newWebview(gpuPolicy int) uintptr { + var newContentMgr func() uintptr + purego.RegisterLibFunc( + &newContentMgr, + webkit, + "webkit_user_content_manager_new") + var registerScriptMessageHandler func(uintptr, string) + purego.RegisterLibFunc(®isterScriptMessageHandler, webkit, "webkit_user_content_manager_register_script_message_handler") + var newWebview func(uintptr) uintptr + purego.RegisterLibFunc(&newWebview, webkit, "webkit_web_view_new_with_user_content_manager") + + manager := newContentMgr() + registerScriptMessageHandler(manager, "external") + webview := newWebview(manager) + if !registered { + var registerUriScheme func(uintptr, string, uintptr, uintptr, uintptr) + purego.RegisterLibFunc(®isterUriScheme, webkit, "webkit_web_context_register_uri_scheme") + cb := purego.NewCallback(func(request uintptr) { + processURLRequest(w.parent.id, request) + }) + var defaultContext func() uintptr + purego.RegisterLibFunc(&defaultContext, webkit, "webkit_web_context_get_default") + registerUriScheme(defaultContext(), "wails", cb, 0, 0) + } + + var g_signal_connect func(uintptr, string, uintptr, uintptr, bool, int) int + purego.RegisterLibFunc(&g_signal_connect, gtk, "g_signal_connect_data") + + loadChanged := purego.NewCallback(func(window uintptr) { + fmt.Println("loadChanged", window) + }) + g_signal_connect(webview, "load-changed", loadChanged, 0, false, 0) + + if g_signal_connect(webview, "button-press-event", purego.NewCallback(w.buttonPress), 0, false, 0) == 0 { + fmt.Println("failed to connect 'button-press-event") + } + if g_signal_connect(webview, "button-release-event", purego.NewCallback(w.buttonRelease), 0, false, 0) == 0 { + fmt.Println("failed to connect 'button-release-event") + } + + handleDelete := purego.NewCallback(func(uintptr) { + w.close() + if !w.parent.options.HideOnClose { + fmt.Println("Need to do more!") + } + }) + g_signal_connect(w.window, "delete-event", handleDelete, 0, false, 0) + + var getSettings func(uintptr) uintptr + purego.RegisterLibFunc(&getSettings, webkit, "webkit_web_view_get_settings") + var setSettings func(uintptr, uintptr) + purego.RegisterLibFunc(&setSettings, webkit, "webkit_web_view_set_settings") + var setUserAgent func(uintptr, string, string) + purego.RegisterLibFunc(&setUserAgent, webkit, "webkit_settings_set_user_agent_with_application_details") + settings := getSettings(webview) + setUserAgent(settings, "wails.io", "") + + var setHWAccel func(uintptr, int) + purego.RegisterLibFunc(&setHWAccel, webkit, "webkit_settings_set_hardware_acceleration_policy") + + setHWAccel(settings, gpuPolicy) + setSettings(webview, settings) + + return webview +} + +func (w *linuxWebviewWindow) openContextMenu(menu *Menu, data *ContextMenuData) { + // Create the menu + thisMenu := newMenuImpl(menu) + thisMenu.update() + fmt.Println("linux.openContextMenu()") + //C.windowShowMenu(w.nsWindow, thisMenu.nsMenu, C.int(data.X), C.int(data.Y)) +} + +func (w *linuxWebviewWindow) getZoom() float64 { + var getZoom func(uintptr) float32 + purego.RegisterLibFunc(&getZoom, webkit, "webkit_web_view_get_zoom_level") + return float64(getZoom(w.webview)) +} + +func (w *linuxWebviewWindow) setZoom(zoom float64) { + var setZoom func(uintptr, float64) + purego.RegisterLibFunc(&setZoom, webkit, "webkit_web_view_set_zoom_level") + setZoom(w.webview, zoom) +} + +func (w *linuxWebviewWindow) setFrameless(frameless bool) { + var setDecorated func(uintptr, int) + purego.RegisterLibFunc(&setDecorated, gtk, "gtk_window_set_decorated") + decorated := 1 + if frameless { + decorated = 0 + } + setDecorated(w.window, decorated) + if !frameless { + // TODO: Deal with transparency for the titlebar if possible + // Perhaps we just make it undecorated and add a menu bar inside? + } +} + +func (w *linuxWebviewWindow) getScreen() (*Screen, error) { + return getScreenForWindow(w) +} + +func (w *linuxWebviewWindow) show() { + var widgetShow func(uintptr) + purego.RegisterLibFunc(&widgetShow, gtk, "gtk_widget_show_all") + globalApplication.dispatchOnMainThread(func() { + widgetShow(w.window) + }) +} + +func (w *linuxWebviewWindow) hide() { + var widgetHide func(uintptr) + purego.RegisterLibFunc(&widgetHide, gtk, "gtk_widget_hide") + widgetHide(w.window) +} + +func (w *linuxWebviewWindow) setFullscreenButtonEnabled(enabled bool) { + // C.setFullscreenButtonEnabled(w.nsWindow, C.bool(enabled)) + fmt.Println("setFullscreenButtonEnabled - not implemented") + +} + +func (w *linuxWebviewWindow) disableSizeConstraints() { + x, y, width, height, scale := w.getCurrentMonitorGeometry() + w.setMinMaxSize(x, y, width*scale, height*scale) +} + +func (w *linuxWebviewWindow) unfullscreen() { + var unfullScreen func(uintptr) + purego.RegisterLibFunc(&unfullScreen, gtk, "gtk_window_unfullscreen") + + globalApplication.dispatchOnMainThread(func() { + unfullScreen(w.window) + w.unmaximise() + }) +} + +func (w *linuxWebviewWindow) fullscreen() { + var fullScreen func(uintptr) + purego.RegisterLibFunc(&fullScreen, gtk, "gtk_window_fullscreen") + + globalApplication.dispatchOnMainThread(func() { + w.maximise() + // w.lastWidth, w.lastHeight = w.size() // do we need this? + + x, y, width, height, scale := w.getCurrentMonitorGeometry() + if x == -1 && y == -1 && width == -1 && height == -1 { + return + } + w.setMinMaxSize(0, 0, width*scale, height*scale) + w.setSize(width*scale, height*scale) + w.setPosition(0, 0) + fullScreen(w.window) + }) +} + +func (w *linuxWebviewWindow) unminimise() { + var present func(uintptr) + purego.RegisterLibFunc(&present, gtk, "gtk_window_present") + present(w.window) +} + +func (w *linuxWebviewWindow) unmaximise() { + var unmaximize func(uintptr) + purego.RegisterLibFunc(&unmaximize, gtk, "gtk_window_unmaximize") + unmaximize(w.window) +} + +func (w *linuxWebviewWindow) maximise() { + var maximize func(uintptr) + purego.RegisterLibFunc(&maximize, gtk, "gtk_window_maximize") + maximize(w.window) +} + +func (w *linuxWebviewWindow) minimise() { + var iconify func(uintptr) + purego.RegisterLibFunc(&iconify, gtk, "gtk_window_iconify") + iconify(w.window) +} + +func (w *linuxWebviewWindow) on(eventID uint) { + // Don't think this is correct! + // GTK Events are strings + fmt.Println("on()", eventID) + //C.registerListener(C.uint(eventID)) +} + +func (w *linuxWebviewWindow) zoom() { + w.zoomIn() +} + +func (w *linuxWebviewWindow) windowZoom() { + w.zoom() +} + +func (w *linuxWebviewWindow) close() { + var close func(uintptr) + purego.RegisterLibFunc(&close, gtk, "gtk_window_close") + close(w.window) +} + +func (w *linuxWebviewWindow) zoomIn() { + var getZoom func(uintptr) float32 + purego.RegisterLibFunc(&getZoom, webkit, "webkit_web_view_get_zoom_level") + var setZoom func(uintptr, float32) + purego.RegisterLibFunc(&setZoom, webkit, "webkit_web_view_set_zoom_level") + lvl := getZoom(w.webview) + setZoom(w.webview, lvl+0.5) +} + +func (w *linuxWebviewWindow) zoomOut() { + var getZoom func(uintptr) float32 + purego.RegisterLibFunc(&getZoom, webkit, "webkit_web_view_get_zoom_level") + var setZoom func(uintptr, float32) + purego.RegisterLibFunc(&setZoom, webkit, "webkit_web_view_set_zoom_level") + lvl := getZoom(w.webview) + setZoom(w.webview, lvl-0.5) +} + +func (w *linuxWebviewWindow) zoomReset() { + var setZoom func(uintptr, float32) + purego.RegisterLibFunc(&setZoom, webkit, "webkit_web_view_set_zoom_level") + setZoom(w.webview, 0.0) +} + +func (w *linuxWebviewWindow) toggleDevTools() { + var getSettings func(uintptr) uintptr + purego.RegisterLibFunc(&getSettings, webkit, "webkit_web_view_get_settings") + var isEnabled func(uintptr) bool + purego.RegisterLibFunc(&isEnabled, webkit, "webkit_settings_get_enable_developer_extras") + var enableDev func(uintptr, bool) + purego.RegisterLibFunc(&enableDev, webkit, "webkit_settings_set_enable_developer_extras") + settings := getSettings(w.webview) + enabled := isEnabled(settings) + enableDev(settings, !enabled) +} + +func (w *linuxWebviewWindow) reload() { + var reload func(uintptr) + purego.RegisterLibFunc(&reload, webkit, "webkit_web_view_reload") + reload(w.webview) +} + +func (w *linuxWebviewWindow) forceReload() { + var reload func(uintptr) + purego.RegisterLibFunc(&reload, webkit, "webkit_web_view_reload_bypass_cache") + reload(w.webview) +} + +func (w linuxWebviewWindow) getCurrentMonitor() uintptr { + var getDisplay func(uintptr) uintptr + purego.RegisterLibFunc(&getDisplay, gtk, "gtk_widget_get_display") + var getWindow func(uintptr) uintptr + purego.RegisterLibFunc(&getWindow, gtk, "gtk_widget_get_window") + var getMonitor func(uintptr, uintptr) uintptr + purego.RegisterLibFunc(&getMonitor, gtk, "gdk_display_get_monitor_at_window") + + display := getDisplay(w.window) + window := getWindow(w.window) + if window == 0 { + return 0 + } + return getMonitor(display, window) +} + +func (w linuxWebviewWindow) getCurrentMonitorGeometry() (x int, y int, width int, height int, scale int) { + var getGeometry func(uintptr, uintptr) + purego.RegisterLibFunc(&getGeometry, gtk, "gdk_monitor_get_geometry") + var getScaleFactor func(uintptr) int + purego.RegisterLibFunc(&getScaleFactor, gtk, "gdk_monitor_get_scale_factor") + + monitor := w.getCurrentMonitor() + if monitor == 0 { + return -1, -1, -1, -1, 1 + } + result := struct { + x int32 + y int32 + width int32 + height int32 + }{} + getGeometry(monitor, uintptr(unsafe.Pointer(&result))) + scale = getScaleFactor(monitor) + return int(result.x), int(result.y), int(result.width), int(result.height), scale +} + +func (w *linuxWebviewWindow) center() { + x, y, width, height, _ := w.getCurrentMonitorGeometry() + if x == -1 && y == -1 && width == -1 && height == -1 { + return + } + + windowWidth, windowHeight := w.size() + + newX := ((width - int(windowWidth)) / 2) + x + newY := ((height - int(windowHeight)) / 2) + y + + w.setPosition(newX, newY) +} + +func (w *linuxWebviewWindow) isMinimised() bool { + var getWindow func(uintptr) uintptr + purego.RegisterLibFunc(&getWindow, gtk, "gtk_widget_get_window") + var getWindowState func(uintptr) int + purego.RegisterLibFunc(&getWindowState, gtk, "gdk_window_get_state") + + return w.syncMainThreadReturningBool(func() bool { + state := getWindowState(getWindow(w.window)) + return state&GDK_WINDOW_STATE_ICONIFIED > 0 + }) +} + +func (w *linuxWebviewWindow) isMaximised() bool { + var getWindow func(uintptr) uintptr + purego.RegisterLibFunc(&getWindow, gtk, "gtk_widget_get_window") + var getWindowState func(uintptr) int + purego.RegisterLibFunc(&getWindowState, gtk, "gdk_window_get_state") + + return w.syncMainThreadReturningBool(func() bool { + state := getWindowState(getWindow(w.window)) + return state&GDK_WINDOW_STATE_MAXIMIZED > 0 && state&GDK_WINDOW_STATE_FULLSCREEN == 0 + }) +} + +func (w *linuxWebviewWindow) isFullscreen() bool { + var getWindow func(uintptr) uintptr + purego.RegisterLibFunc(&getWindow, gtk, "gtk_widget_get_window") + var getWindowState func(uintptr) int + purego.RegisterLibFunc(&getWindowState, gtk, "gdk_window_get_state") + + return w.syncMainThreadReturningBool(func() bool { + state := getWindowState(getWindow(w.window)) + return state&GDK_WINDOW_STATE_FULLSCREEN > 0 + }) +} + +func (w *linuxWebviewWindow) syncMainThreadReturningBool(fn func() bool) bool { + var wg sync.WaitGroup + wg.Add(1) + var result bool + globalApplication.dispatchOnMainThread(func() { + result = fn() + wg.Done() + }) + wg.Wait() + return result +} + +func (w *linuxWebviewWindow) restore() { + // restore window to normal size + fmt.Println("restore") +} + +func (w *linuxWebviewWindow) execJS(js string) { + var evalJS func(uintptr, string, int, uintptr, string, uintptr, uintptr, uintptr) + purego.RegisterLibFunc(&evalJS, webkit, "webkit_web_view_evaluate_javascript") + evalJS(w.webview, js, len(js), 0, "", 0, 0, 0) +} + +func (w *linuxWebviewWindow) setURL(uri string) { + fmt.Println("setURL", uri) + var loadUri func(uintptr, string) + purego.RegisterLibFunc(&loadUri, webkit, "webkit_web_view_load_uri") + + url, err := url.Parse(uri) + if url != nil && err == nil && url.Scheme == "" && url.Host == "" { + // TODO handle this in a central location, the scheme and host might be platform dependant + url.Scheme = "wails" + url.Host = "wails" + uri = url.String() + loadUri(w.webview, uri) + } +} + +func (w *linuxWebviewWindow) setAlwaysOnTop(alwaysOnTop bool) { + var keepAbove func(uintptr, bool) + purego.RegisterLibFunc(&keepAbove, gtk, "gtk_window_set_keep_above") + keepAbove(w.window, alwaysOnTop) +} + +func newWindowImpl(parent *WebviewWindow) *linuxWebviewWindow { + return &linuxWebviewWindow{ + application: (globalApplication.impl).(*linuxApp).application, + parent: parent, + } +} + +func (w *linuxWebviewWindow) setTitle(title string) { + if !w.parent.options.Frameless { + var setTitle func(uintptr, string) + purego.RegisterLibFunc(&setTitle, gtk, "gtk_window_set_title") + setTitle(w.window, title) + } +} + +func (w *linuxWebviewWindow) setSize(width, height int) { + var setSize func(uintptr, int, int) + purego.RegisterLibFunc(&setSize, gtk, "gtk_window_set_default_size") + setSize(w.window, width, height) +} + +func (w *linuxWebviewWindow) setMinMaxSize(minWidth, minHeight, maxWidth, maxHeight int) { + fmt.Println("setMinMaxSize", minWidth, minHeight, maxWidth, maxHeight) + if minWidth == 0 { + minWidth = -1 + } + if minHeight == 0 { + minHeight = -1 + } + if maxWidth == 0 { + maxWidth = -1 + } + if maxHeight == 0 { + maxHeight = -1 + } + size := GdkGeometry{ + minWidth: int32(minWidth), + minHeight: int32(minHeight), + maxWidth: int32(maxWidth), + maxHeight: int32(maxHeight), + } + + var setHints func(uintptr, uintptr, uintptr, int) + purego.RegisterLibFunc(&setHints, gtk, "gtk_window_set_geometry_hints") + setHints(w.window, 0, uintptr(unsafe.Pointer(&size)), GDK_HINT_MIN_SIZE|GDK_HINT_MAX_SIZE) +} + +func (w *linuxWebviewWindow) setMinSize(width, height int) { + w.setMinMaxSize(width, height, w.parent.options.MaxWidth, w.parent.options.MaxHeight) +} + +func (w *linuxWebviewWindow) setMaxSize(width, height int) { + w.setMinMaxSize(w.parent.options.MinWidth, w.parent.options.MinHeight, width, height) +} + +func (w *linuxWebviewWindow) setResizable(resizable bool) { + var setResizable func(uintptr, int) + purego.RegisterLibFunc(&setResizable, gtk, "gtk_window_set_resizable") + globalApplication.dispatchOnMainThread(func() { + if resizable { + setResizable(w.window, 1) + } else { + setResizable(w.window, 0) + } + }) +} + +func (w *linuxWebviewWindow) size() (int, int) { + var width, height int + var windowGetSize func(uintptr, *int, *int) + purego.RegisterLibFunc(&windowGetSize, gtk, "gtk_window_get_size") + + var wg sync.WaitGroup + wg.Add(1) + globalApplication.dispatchOnMainThread(func() { + windowGetSize(w.window, &width, &height) + wg.Done() + }) + wg.Wait() + return width, height +} + +func (w *linuxWebviewWindow) setPosition(x, y int) { + var windowMove func(uintptr, int, int) + purego.RegisterLibFunc(&windowMove, gtk, "gtk_window_move") + mx, my, _, _, _ := w.getCurrentMonitorGeometry() + fmt.Println("setPosition", mx, my) + globalApplication.dispatchOnMainThread(func() { + windowMove(w.window, x+mx, y+my) + }) +} + +func (w *linuxWebviewWindow) width() int { + width, _ := w.size() + return width +} + +func (w *linuxWebviewWindow) height() int { + _, height := w.size() + return height +} + +func (w *linuxWebviewWindow) buttonPress(widget uintptr, event uintptr, user_data uintptr) { + GdkEventButton := (*byte)(unsafe.Pointer(event)) + fmt.Println("buttonpress", w.parent.id, widget, GdkEventButton, user_data) +} + +func (w *linuxWebviewWindow) buttonRelease(widget uintptr, event uintptr, user_data uintptr) { + GdkEventButton := (*byte)(unsafe.Pointer(event)) + fmt.Println("buttonrelease", w.parent.id, widget, GdkEventButton, user_data) +} + +func (w *linuxWebviewWindow) run() { + for eventId := range w.parent.eventListeners { + w.on(eventId) + } + + globalApplication.dispatchOnMainThread(func() { + app := (globalApplication.impl).(*linuxApp) + menu := app.applicationMenu + var newWindow func(uintptr) uintptr + purego.RegisterLibFunc(&newWindow, gtk, "gtk_application_window_new") + var refSink func(uintptr) + purego.RegisterLibFunc(&refSink, gtk, "g_object_ref_sink") + var boxNew func(int, int) uintptr + purego.RegisterLibFunc(&boxNew, gtk, "gtk_box_new") + var containerAdd func(uintptr, uintptr) + purego.RegisterLibFunc(&containerAdd, gtk, "gtk_container_add") + var boxPackStart func(uintptr, uintptr, int, int, int) + purego.RegisterLibFunc(&boxPackStart, gtk, "gtk_box_pack_start") + + var g_signal_connect func(uintptr, string, uintptr, uintptr, bool, int) int + purego.RegisterLibFunc(&g_signal_connect, gtk, "g_signal_connect_data") + + w.window = newWindow(w.application) + + refSink(w.window) + w.webview = w.newWebview(1) + w.vbox = boxNew(1, 0) + containerAdd(w.window, w.vbox) + if menu != 0 { + w.menubar = menu + boxPackStart(w.vbox, menu, 0, 0, 0) + } + boxPackStart(w.vbox, w.webview, 1, 1, 0) + + w.setSize(w.parent.options.Width, w.parent.options.Height) + w.setTitle(w.parent.options.Title) + w.setAlwaysOnTop(w.parent.options.AlwaysOnTop) + w.setResizable(!w.parent.options.DisableResize) + if w.parent.options.MinWidth != 0 && + w.parent.options.MinHeight != 0 && + w.parent.options.MaxWidth != 0 && + w.parent.options.MaxHeight != 0 { + w.setMinMaxSize( + w.parent.options.MinWidth, + w.parent.options.MinHeight, + w.parent.options.MaxWidth, + w.parent.options.MaxHeight, + ) + } + w.setZoom(w.parent.options.Zoom) + w.setBackgroundColour(w.parent.options.BackgroundColour) + w.setFrameless(w.parent.options.Frameless) + + switch w.parent.options.StartState { + case WindowStateMaximised: + w.maximise() + case WindowStateMinimised: + w.minimise() + case WindowStateFullscreen: + w.fullscreen() + + } + w.center() + + if w.parent.options.URL != "" { + w.setURL(w.parent.options.URL) + } + // We need to wait for the HTML to load before we can execute the javascript + w.parent.On(events.Mac.WebViewDidFinishNavigation, func(_ *WindowEventContext) { + if w.parent.options.JS != "" { + w.execJS(w.parent.options.JS) + } + if w.parent.options.CSS != "" { + js := fmt.Sprintf("(function() { var style = document.createElement('style'); style.appendChild(document.createTextNode('%s')); document.head.appendChild(style); })();", w.parent.options.CSS) + w.execJS(js) + } + }) + if w.parent.options.HTML != "" { + w.setHTML(w.parent.options.HTML) + } + if w.parent.options.Hidden == false { + w.show() + if w.parent.options.X != 0 || w.parent.options.Y != 0 { + w.setPosition(w.parent.options.X, w.parent.options.Y) + } else { + fmt.Println("attempting to set in the center") + w.center() + } + } + }) +} + +func (w *linuxWebviewWindow) setTransparent() { + var getScreen func(uintptr) uintptr + purego.RegisterLibFunc(&getScreen, gtk, "gtk_widget_get_screen") + var getVisual func(uintptr) uintptr + purego.RegisterLibFunc(&getVisual, gtk, "gdk_screen_get_rgba_visual") + var isComposited func(uintptr) int + purego.RegisterLibFunc(&isComposited, gtk, "gdk_screen_is_composited") + var setPaintable func(uintptr, int) + purego.RegisterLibFunc(&setPaintable, gtk, "gtk_widget_set_app_paintable") + var setVisual func(uintptr, uintptr) + purego.RegisterLibFunc(&setVisual, gtk, "gtk_widget_set_visual") + + screen := getScreen(w.window) + visual := getVisual(screen) + if visual == 0 { + return + } + if isComposited(screen) == 1 { + setPaintable(w.window, 1) + setVisual(w.window, visual) + } +} + +func (w *linuxWebviewWindow) setBackgroundColour(colour *RGBA) { + if colour == nil { + return + } + + if colour.Alpha != 0 { + w.setTransparent() + } + + var rgbaParse func(uintptr, string) bool + purego.RegisterLibFunc(&rgbaParse, gtk, "gdk_rgba_parse") + var setBackgroundColor func(uintptr, uintptr) + purego.RegisterLibFunc(&setBackgroundColor, webkit, "webkit_web_view_set_background_color") + + rgba := make([]byte, 4*8) // C.sizeof_GdkRGBA == 32 + pointer := uintptr(unsafe.Pointer(&rgba[0])) + if !rgbaParse( + pointer, + fmt.Sprintf("rgba(%v,%v,%v,%v)", + colour.Red, + colour.Green, + colour.Blue, + float32(colour.Alpha)/255.0, + )) { + return + } + setBackgroundColor(w.webview, pointer) +} + +func (w *linuxWebviewWindow) position() (int, int) { + var getPosition func(uintptr, *int, *int) bool + purego.RegisterLibFunc(&getPosition, gtk, "gtk_window_get_position") + + var x, y int + var wg sync.WaitGroup + wg.Add(1) + go globalApplication.dispatchOnMainThread(func() { + getPosition(w.window, &x, &y) + wg.Done() + }) + wg.Wait() + return x, y +} + +func (w *linuxWebviewWindow) destroy() { + var close func(uintptr) + purego.RegisterLibFunc(&close, gtk, "gtk_window_close") + go globalApplication.dispatchOnMainThread(func() { + close(w.window) + }) +} + +func (w *linuxWebviewWindow) setHTML(html string) { + fmt.Println("setHTML") + var loadHTML func(uintptr, string, string, *string) + purego.RegisterLibFunc(&loadHTML, webkit, "webkit_web_view_load_alternate_html") + go globalApplication.dispatchOnMainThread(func() { + loadHTML(w.webview, html, "wails://", nil) + }) +}