5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-05 02:09:38 +08:00
wails/v3/pkg/application/linux_cgo.go
Lea Anthony 4c7b643295 Add webview GPU policy and update Linux options
The commit introduces a set of webview GPU policies to control hardware acceleration. These policies define when hardware acceleration is enabled on the webview. An option for this has been added to the LinuxWindow struct for Linux specific windows. Additional code modification was carried out to use this new GPU policy option when calling `windowNew` function. Finally, the sequence of the GPU Policies in the const declaration has been updated for better readability.
2024-03-06 11:42:19 -06:00

1412 lines
39 KiB
Go

//go:build linux && cgo
package application
import (
"fmt"
"regexp"
"strings"
"unsafe"
"github.com/wailsapp/wails/v3/internal/assetserver/webview"
"github.com/wailsapp/wails/v3/pkg/events"
)
/*
#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 javascriptcoregtk-4.1
#include <JavaScriptCore/JavaScript.h>
#include <gtk/gtk.h>
#include <gdk/gdk.h>
#include <webkit2/webkit2.h>
#include <stdio.h>
#include <limits.h>
#include <stdint.h>
#ifdef G_APPLICATION_DEFAULT_FLAGS
#define APPLICATION_DEFAULT_FLAGS G_APPLICATION_DEFAULT_FLAGS
#else
#define APPLICATION_DEFAULT_FLAGS G_APPLICATION_FLAGS_NONE
#endif
typedef struct CallbackID
{
unsigned int value;
} CallbackID;
extern void dispatchOnMainThreadCallback(unsigned int);
static gboolean dispatchCallback(gpointer data) {
struct CallbackID *args = data;
unsigned int cid = args->value;
dispatchOnMainThreadCallback(cid);
free(args);
return G_SOURCE_REMOVE;
};
static void dispatchOnMainThread(unsigned int id) {
CallbackID *args = malloc(sizeof(CallbackID));
args->value = id;
g_idle_add((GSourceFunc)dispatchCallback, (gpointer)args);
}
typedef struct WindowEvent {
uint id;
uint event;
} WindowEvent;
// exported below
void activateLinux(gpointer data);
extern void emit(WindowEvent* data);
extern gboolean handleDeleteEvent(GtkWidget*, GdkEvent*, uintptr_t);
extern void handleLoadChanged(WebKitWebView*, WebKitLoadEvent, uintptr_t);
void handleClick(void*);
extern gboolean onButtonEvent(GtkWidget *widget, GdkEventButton *event, uintptr_t user_data);
extern void onDragNDrop(
void *target,
GdkDragContext* context,
gint x,
gint y,
gpointer seldata,
guint info,
guint time,
gpointer data);
extern gboolean onKeyPressEvent (GtkWidget *widget, GdkEventKey *event, uintptr_t user_data);
extern void onProcessRequest(void *request, gpointer user_data);
extern void sendMessageToBackend(WebKitUserContentManager *contentManager, WebKitJavascriptResult *result, void *data);
// exported below (end)
static void signal_connect(void *widget, char *event, void *cb, void* data) {
// g_signal_connect is a macro and can't be called directly
g_signal_connect(widget, event, cb, data);
}
static void* new_message_dialog(GtkWindow *parent, const gchar *msg, int dialogType, bool hasButtons) {
// gtk_message_dialog_new is variadic! Can't call from cgo directly
GtkWidget *dialog;
int buttonMask;
// buttons will be added after creation
buttonMask = GTK_BUTTONS_OK;
if (hasButtons) {
buttonMask = GTK_BUTTONS_NONE;
}
dialog = gtk_message_dialog_new(
parent,
GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
dialogType,
buttonMask,
"%s",
msg);
// g_signal_connect_swapped (dialog,
// "response",
// G_CALLBACK (callback),
// dialog);
return dialog;
};
extern void messageDialogCB(gint button);
static void* gtkFileChooserDialogNew(char* title, GtkWindow* window, GtkFileChooserAction action, char* cancelLabel, char* acceptLabel) {
// gtk_file_chooser_dialog_new is variadic! Can't call from cgo directly
return (GtkFileChooser*)gtk_file_chooser_dialog_new(
title,
window,
action,
cancelLabel,
GTK_RESPONSE_CANCEL,
acceptLabel,
GTK_RESPONSE_ACCEPT,
NULL);
}
typedef struct Screen {
const char* id;
const char* name;
int p_width;
int p_height;
int width;
int height;
int x;
int y;
int w_width;
int w_height;
int w_x;
int w_y;
float scale;
double rotation;
bool isPrimary;
} Screen;
static int GetNumScreens(){
return 0;
}
*/
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
var (
nilPointer pointer = nil
nilRadioGroup GSListPointer = nil
)
var (
gtkSignalToMenuItem map[uint]*MenuItem
mainThreadId *C.GThread
)
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) {
processApplicationEvent(C.uint(events.Linux.ApplicationStartup), data)
}
//export processApplicationEvent
func processApplicationEvent(eventID C.uint, data pointer) {
event := newApplicationEvent(events.ApplicationEventType(eventID))
//if data != nil {
// dataCStrJSON := C.serializationNSDictionary(data)
// if dataCStrJSON != nil {
// defer C.free(unsafe.Pointer(dataCStrJSON))
//
// dataJSON := C.GoString(dataCStrJSON)
// var result map[string]any
// err := json.Unmarshal([]byte(dataJSON), &result)
//
// if err != nil {
// panic(err)
// }
//
// event.Context().setData(result)
// }
//}
switch event.Id {
case uint(events.Linux.SystemThemeChanged):
isDark := globalApplication.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 {
// prevent leading number
if matched, _ := regexp.MatchString(`^\d+`, name); matched {
name = fmt.Sprintf("_%s", name)
}
name = strings.Replace(name, "(", "_", -1)
name = strings.Replace(name, ")", "_", -1)
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 appRun(app pointer) error {
application := (*C.GApplication)(app)
//TODO: Only set this if we configure it to do so
C.g_application_hold(application) // allows it to run without a window
signal := C.CString("activate")
defer C.free(unsafe.Pointer(signal))
C.signal_connect(unsafe.Pointer(application), signal, C.activateLinux, nil)
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 contextMenuShow(window pointer, menu pointer, data *ContextMenuData) {
geometry := C.GdkRectangle{
x: C.int(data.X),
y: C.int(data.Y),
}
event := C.GdkEvent{}
gdkWindow := C.gtk_widget_get_window((*C.GtkWidget)(window))
C.gtk_menu_popup_at_rect(
(*C.GtkMenu)(menu),
gdkWindow,
(*C.GdkRectangle)(&geometry),
C.GDK_GRAVITY_NORTH_WEST,
C.GDK_GRAVITY_NORTH_WEST,
(*C.GdkEvent)(&event),
)
}
func getCurrentWindowID(application pointer, windows map[windowPointer]uint) uint {
// TODO: Add extra metadata to window and use it!
window := (*C.GtkWindow)(C.gtk_application_get_active_window((*C.GtkApplication)(application)))
if window == nil {
return uint(1)
}
identifier, ok := windows[window]
if ok {
return identifier
}
// FIXME: Should we panic here if not found?
return uint(1)
}
func getWindows(application pointer) []pointer {
result := []pointer{}
windows := C.gtk_application_get_windows((*C.GtkApplication)(application))
for {
result = append(result, pointer(windows.data))
windows = windows.next
if windows == nil {
return result
}
}
}
func hideAllWindows(application pointer) {
for _, window := range getWindows(application) {
C.gtk_widget_hide((*C.GtkWidget)(window))
}
}
func showAllWindows(application pointer) {
for _, window := range getWindows(application) {
C.gtk_window_present((*C.GtkWindow)(window))
}
}
// Clipboard
func clipboardGet() string {
clip := C.gtk_clipboard_get(C.GDK_SELECTION_CLIPBOARD)
text := C.gtk_clipboard_wait_for_text(clip)
return C.GoString(text)
}
func clipboardSet(text string) {
cText := C.CString(text)
clip := C.gtk_clipboard_get(C.GDK_SELECTION_CLIPBOARD)
C.gtk_clipboard_set_text(clip, cText, -1)
clip = C.gtk_clipboard_get(C.GDK_SELECTION_PRIMARY)
C.gtk_clipboard_set_text(clip, cText, -1)
C.free(unsafe.Pointer(cText))
}
// Menu
func menuAddSeparator(menu *Menu) {
C.gtk_menu_shell_append(
(*C.GtkMenuShell)((menu.impl).(*linuxMenu).native),
C.gtk_separator_menu_item_new())
}
func menuAppend(parent *Menu, menu *MenuItem) {
C.gtk_menu_shell_append(
(*C.GtkMenuShell)((parent.impl).(*linuxMenu).native),
(*C.GtkWidget)((menu.impl).(*linuxMenuItem).native),
)
/* gtk4
C.gtk_menu_item_set_submenu(
(*C.struct__GtkMenuItem)((menu.impl).(*linuxMenuItem).native),
(*C.struct__GtkWidget)((parent.impl).(*linuxMenu).native),
)
*/
}
func menuBarNew() pointer {
return pointer(C.gtk_menu_bar_new())
}
func menuNew() pointer {
return pointer(C.gtk_menu_new())
}
func menuSetSubmenu(item *MenuItem, menu *Menu) {
C.gtk_menu_item_set_submenu(
(*C.GtkMenuItem)((item.impl).(*linuxMenuItem).native),
(*C.GtkWidget)((menu.impl).(*linuxMenu).native))
}
func menuGetRadioGroup(item *linuxMenuItem) *GSList {
return (*GSList)(C.gtk_radio_menu_item_get_group((*C.GtkRadioMenuItem)(item.native)))
}
//export handleClick
func handleClick(idPtr unsafe.Pointer) {
ident := C.CString("id")
defer C.free(unsafe.Pointer(ident))
value := C.g_object_get_data((*C.GObject)(idPtr), ident)
id := uint(*(*C.uint)(value))
item, ok := gtkSignalToMenuItem[id]
if !ok {
return
}
switch item.itemType {
case text, checkbox:
menuItemClicked <- item.id
case radio:
menuItem := (item.impl).(*linuxMenuItem)
if menuItem.isChecked() {
menuItemClicked <- item.id
}
}
}
func attachMenuHandler(item *MenuItem) uint {
signal := C.CString("activate")
defer C.free(unsafe.Pointer(signal))
impl := (item.impl).(*linuxMenuItem)
widget := impl.native
flags := C.GConnectFlags(0)
handlerId := C.g_signal_connect_object(
C.gpointer(widget),
signal,
C.GCallback(C.handleClick),
C.gpointer(widget),
flags)
id := C.uint(item.id)
ident := C.CString("id")
defer C.free(unsafe.Pointer(ident))
C.g_object_set_data(
(*C.GObject)(widget),
ident,
C.gpointer(&id),
)
gtkSignalToMenuItem[item.id] = item
return uint(handlerId)
}
// menuItem
func menuItemChecked(widget pointer) bool {
if C.gtk_check_menu_item_get_active((*C.GtkCheckMenuItem)(widget)) == C.int(1) {
return true
}
return false
}
func menuItemNew(label string, bitmap []byte) pointer {
return menuItemAddProperties(C.gtk_menu_item_new(), label, bitmap)
}
func menuItemAddProperties(menuItem *C.GtkWidget, label string, bitmap []byte) pointer {
/*
// FIXME: Support accelerator configuration
activate := C.CString("activate")
defer C.free(unsafe.Pointer(activate))
accelGroup := C.gtk_accel_group_new()
C.gtk_widget_add_accelerator(menuItem, activate, accelGroup,
C.GDK_KEY_m, C.GDK_CONTROL_MASK, C.GTK_ACCEL_VISIBLE)
*/
cLabel := C.CString(label)
defer C.free(unsafe.Pointer(cLabel))
lbl := unsafe.Pointer(C.gtk_accel_label_new(cLabel))
C.gtk_label_set_use_underline((*C.GtkLabel)(lbl), 1)
C.gtk_label_set_xalign((*C.GtkLabel)(lbl), 0.0)
C.gtk_accel_label_set_accel_widget(
(*C.GtkAccelLabel)(lbl),
(*C.GtkWidget)(unsafe.Pointer(menuItem)))
box := C.gtk_box_new(C.GTK_ORIENTATION_HORIZONTAL, 6)
if img, err := pngToImage(bitmap); err == nil {
gbytes := C.g_bytes_new_static(C.gconstpointer(unsafe.Pointer(&img.Pix[0])),
C.ulong(len(img.Pix)))
defer C.g_bytes_unref(gbytes)
pixBuf := C.gdk_pixbuf_new_from_bytes(
gbytes,
C.GDK_COLORSPACE_RGB,
1, // has_alpha
8,
C.int(img.Bounds().Dx()),
C.int(img.Bounds().Dy()),
C.int(img.Stride),
)
image := C.gtk_image_new_from_pixbuf(pixBuf)
C.gtk_widget_set_visible((*C.GtkWidget)(image), C.gboolean(1))
C.gtk_container_add(
(*C.GtkContainer)(unsafe.Pointer(box)),
(*C.GtkWidget)(unsafe.Pointer(image)))
}
C.gtk_box_pack_end(
(*C.GtkBox)(unsafe.Pointer(box)),
(*C.GtkWidget)(lbl), 1, 1, 0)
C.gtk_container_add(
(*C.GtkContainer)(unsafe.Pointer(menuItem)),
(*C.GtkWidget)(unsafe.Pointer(box)))
C.gtk_widget_show_all(menuItem)
return pointer(menuItem)
}
func menuCheckItemNew(label string, bitmap []byte) pointer {
return menuItemAddProperties(C.gtk_check_menu_item_new(), label, bitmap)
}
func menuItemSetChecked(widget pointer, checked bool) {
value := C.int(0)
if checked {
value = C.int(1)
}
C.gtk_check_menu_item_set_active(
(*C.GtkCheckMenuItem)(widget),
value)
}
func menuItemSetDisabled(widget pointer, disabled bool) {
value := C.int(1)
if disabled {
value = C.int(0)
}
C.gtk_widget_set_sensitive(
(*C.GtkWidget)(widget),
value)
}
func menuItemSetLabel(widget pointer, label string) {
value := C.CString(label)
C.gtk_menu_item_set_label(
(*C.GtkMenuItem)(widget),
value)
C.free(unsafe.Pointer(value))
}
func menuItemRemoveBitmap(widget pointer) {
box := C.gtk_bin_get_child((*C.GtkBin)(widget))
if box == nil {
return
}
children := C.gtk_container_get_children((*C.GtkContainer)(unsafe.Pointer(box)))
defer C.g_list_free(children)
count := int(C.g_list_length(children))
if count == 2 {
C.gtk_container_remove((*C.GtkContainer)(unsafe.Pointer(box)),
(*C.GtkWidget)(children.data))
}
}
func menuItemSetBitmap(widget pointer, bitmap []byte) {
menuItemRemoveBitmap(widget)
box := C.gtk_bin_get_child((*C.GtkBin)(widget))
if img, err := pngToImage(bitmap); err == nil {
gbytes := C.g_bytes_new_static(C.gconstpointer(unsafe.Pointer(&img.Pix[0])),
C.ulong(len(img.Pix)))
defer C.g_bytes_unref(gbytes)
pixBuf := C.gdk_pixbuf_new_from_bytes(
gbytes,
C.GDK_COLORSPACE_RGB,
1, // has_alpha
8,
C.int(img.Bounds().Dx()),
C.int(img.Bounds().Dy()),
C.int(img.Stride),
)
image := C.gtk_image_new_from_pixbuf(pixBuf)
C.gtk_widget_set_visible((*C.GtkWidget)(image), C.gboolean(1))
C.gtk_container_add(
(*C.GtkContainer)(unsafe.Pointer(box)),
(*C.GtkWidget)(unsafe.Pointer(image)))
}
}
func menuItemSetToolTip(widget pointer, tooltip string) {
value := C.CString(tooltip)
C.gtk_widget_set_tooltip_text(
(*C.GtkWidget)(widget),
value)
C.free(unsafe.Pointer(value))
}
func menuItemSignalBlock(widget pointer, handlerId uint, block bool) {
if block {
C.g_signal_handler_block(C.gpointer(widget), C.ulong(handlerId))
} else {
C.g_signal_handler_unblock(C.gpointer(widget), C.ulong(handlerId))
}
}
func menuRadioItemNew(group *GSList, label string) pointer {
cLabel := C.CString(label)
defer C.free(unsafe.Pointer(cLabel))
return pointer(C.gtk_radio_menu_item_new_with_label((*C.GSList)(group), cLabel))
}
// screen related
func getScreenByIndex(display *C.struct__GdkDisplay, index int) *Screen {
monitor := C.gdk_display_get_monitor(display, C.int(index))
// TODO: Do we need to update Screen to contain current info?
// currentMonitor := C.gdk_display_get_monitor_at_window(display, window)
var geometry C.GdkRectangle
C.gdk_monitor_get_geometry(monitor, &geometry)
primary := false
if C.gdk_monitor_is_primary(monitor) == 1 {
primary = true
}
return &Screen{
ID: fmt.Sprintf("%d", index),
Name: fmt.Sprintf("Screen %d", index),
IsPrimary: primary,
Scale: float32(C.gdk_monitor_get_scale_factor(monitor)),
X: int(geometry.x),
Y: int(geometry.y),
Size: Size{
Height: int(geometry.height),
Width: int(geometry.width),
},
Bounds: Rect{
X: int(geometry.x),
Y: int(geometry.y),
Height: int(geometry.height),
Width: int(geometry.width),
},
}
}
func getScreens(app pointer) ([]*Screen, error) {
var screens []*Screen
window := C.gtk_application_get_active_window((*C.GtkApplication)(app))
gdkWindow := C.gtk_widget_get_window((*C.GtkWidget)(unsafe.Pointer(window)))
display := C.gdk_window_get_display(gdkWindow)
count := C.gdk_display_get_n_monitors(display)
for i := 0; i < int(count); i++ {
screens = append(screens, getScreenByIndex(display, i))
}
return screens, nil
}
// widgets
func widgetSetSensitive(widget pointer, enabled bool) {
value := C.int(0)
if enabled {
value = C.int(1)
}
C.gtk_widget_set_sensitive((*C.GtkWidget)(widget), value)
}
func widgetSetVisible(widget pointer, hidden bool) {
if hidden {
C.gtk_widget_hide((*C.GtkWidget)(widget))
} else {
C.gtk_widget_show((*C.GtkWidget)(widget))
}
}
// window related functions
func windowClose(window pointer) {
C.gtk_window_close((*C.GtkWindow)(window))
}
func windowEnableDND(id uint, webview pointer) {
dnd := C.CString("text/uri-list")
defer C.free(unsafe.Pointer(dnd))
targetentry := C.gtk_target_entry_new(dnd, 0, C.guint(id))
defer C.gtk_target_entry_free(targetentry)
C.gtk_drag_dest_set((*C.GtkWidget)(webview), C.GTK_DEST_DEFAULT_DROP, targetentry, 1, C.GDK_ACTION_COPY)
event := C.CString("drag-data-received")
defer C.free(unsafe.Pointer(event))
windowId := C.uint(id)
C.signal_connect(unsafe.Pointer(webview), event, C.onDragNDrop, unsafe.Pointer(C.gpointer(&windowId)))
}
func windowExecJS(webview pointer, js string) {
value := C.CString(js)
C.webkit_web_view_evaluate_javascript((*C.WebKitWebView)(webview),
value,
C.long(len(js)),
nil,
C.CString(""),
nil,
nil,
nil)
C.free(unsafe.Pointer(value))
}
func windowDestroy(window pointer) {
// Should this truly 'destroy' ?
C.gtk_window_close((*C.GtkWindow)(window))
//C.gtk_widget_destroy((*C.GtkWidget)(window))
}
func windowFullscreen(window pointer) {
C.gtk_window_fullscreen((*C.GtkWindow)(window))
}
func windowGetCurrentMonitor(window pointer) *C.GdkMonitor {
// Get the monitor that the window is currently on
display := C.gtk_widget_get_display((*C.GtkWidget)(window))
gdk_window := C.gtk_widget_get_window((*C.GtkWidget)(window))
if gdk_window == nil {
return nil
}
return C.gdk_display_get_monitor_at_window(display, gdk_window)
}
func windowGetCurrentMonitorGeometry(window pointer) (x int, y int, width int, height int, scale int) {
monitor := windowGetCurrentMonitor(window)
if monitor == nil {
return -1, -1, -1, -1, 1
}
var result C.GdkRectangle
C.gdk_monitor_get_geometry(monitor, &result)
scale = int(C.gdk_monitor_get_scale_factor(monitor))
return int(result.x), int(result.y), int(result.width), int(result.height), scale
}
func windowGetAbsolutePosition(window pointer) (int, int) {
var x C.int
var y C.int
C.gtk_window_get_position((*C.GtkWindow)(window), &x, &y)
return int(x), int(y)
}
func windowGetSize(window pointer) (int, int) {
var windowWidth C.int
var windowHeight C.int
C.gtk_window_get_size((*C.GtkWindow)(window), &windowWidth, &windowHeight)
return int(windowWidth), int(windowHeight)
}
func windowGetRelativePosition(window pointer) (int, int) {
x, y := windowGetAbsolutePosition(window)
// The position must be relative to the screen it is on
// We need to get the screen it is on
monitor := windowGetCurrentMonitor(window)
geometry := C.GdkRectangle{}
C.gdk_monitor_get_geometry(monitor, &geometry)
x = x - int(geometry.x)
y = y - int(geometry.y)
// TODO: Scale based on DPI
return x, y
}
func windowHide(window pointer) {
C.gtk_widget_hide((*C.GtkWidget)(window))
}
func windowIsFullscreen(window pointer) bool {
gdkwindow := C.gtk_widget_get_window((*C.GtkWidget)(window))
state := C.gdk_window_get_state(gdkwindow)
return state&C.GDK_WINDOW_STATE_FULLSCREEN > 0
}
func windowIsFocused(window pointer) bool {
// returns true if window is focused
return C.gtk_window_has_toplevel_focus((*C.GtkWindow)(window)) == 1
}
func windowIsMaximized(window pointer) bool {
gdkwindow := C.gtk_widget_get_window((*C.GtkWidget)(window))
state := C.gdk_window_get_state(gdkwindow)
return state&C.GDK_WINDOW_STATE_MAXIMIZED > 0 && state&C.GDK_WINDOW_STATE_FULLSCREEN == 0
}
func windowIsMinimized(window pointer) bool {
gdkwindow := C.gtk_widget_get_window((*C.GtkWidget)(window))
state := C.gdk_window_get_state(gdkwindow)
return state&C.GDK_WINDOW_STATE_ICONIFIED > 0
}
func windowIsVisible(window pointer) bool {
if C.gtk_widget_is_visible((*C.GtkWidget)(window)) == 1 {
return true
}
return false
}
func windowMaximize(window pointer) {
C.gtk_window_maximize((*C.GtkWindow)(window))
}
func windowMinimize(window pointer) {
C.gtk_window_iconify((*C.GtkWindow)(window))
}
func windowNew(application pointer, menu pointer, 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))
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_container_add((*C.GtkContainer)(window), (*C.GtkWidget)(vbox))
if menu != nil {
C.gtk_box_pack_start((*C.GtkBox)(vbox), (*C.GtkWidget)(menu), 0, 0, 0)
}
C.gtk_box_pack_start((*C.GtkBox)(unsafe.Pointer(vbox)), (*C.GtkWidget)(webview), 1, 1, 0)
return
}
func windowNewWebview(parentId uint, gpuPolicy WebviewGpuPolicy) pointer {
c := NewCalloc()
defer c.Free()
manager := C.webkit_user_content_manager_new()
C.webkit_user_content_manager_register_script_message_handler(manager, c.String("external"))
webView := C.webkit_web_view_new_with_user_content_manager(manager)
id := C.uint(parentId)
if !registered {
C.webkit_web_context_register_uri_scheme(
C.webkit_web_context_get_default(),
c.String("wails"),
C.WebKitURISchemeRequestCallback(C.onProcessRequest),
C.gpointer(&id),
nil)
registered = true
}
settings := C.webkit_web_view_get_settings((*C.WebKitWebView)(unsafe.Pointer(webView)))
C.webkit_settings_set_user_agent_with_application_details(settings, c.String("wails.io"), c.String(""))
switch gpuPolicy {
case WebviewGpuPolicyAlways:
C.webkit_settings_set_hardware_acceleration_policy(settings, C.WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS)
break
case WebviewGpuPolicyOnDemand:
C.webkit_settings_set_hardware_acceleration_policy(settings, C.WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND)
break
case WebviewGpuPolicyNever:
C.webkit_settings_set_hardware_acceleration_policy(settings, C.WEBKIT_HARDWARE_ACCELERATION_POLICY_NEVER)
break
default:
C.webkit_settings_set_hardware_acceleration_policy(settings, C.WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND)
}
return pointer(webView)
}
func windowPresent(window pointer) {
C.gtk_window_present((*C.GtkWindow)(window))
// gtk_window_unminimize ((*C.GtkWindow)(w.window)) /// gtk4
}
func windowReload(webview pointer, address string) {
uri := C.CString(address)
C.webkit_web_view_load_uri((*C.WebKitWebView)(webview), uri)
C.free(unsafe.Pointer(uri))
}
func windowResize(window pointer, width, height int) {
C.gtk_window_resize(
(*C.GtkWindow)(window),
C.gint(width),
C.gint(height))
}
func windowShow(window pointer) {
C.gtk_widget_show_all((*C.GtkWidget)(window))
}
func windowSetBackgroundColour(vbox, webview pointer, colour RGBA) {
rgba := C.GdkRGBA{C.double(colour.Red) / 255.0, C.double(colour.Green) / 255.0, C.double(colour.Blue) / 255.0, C.double(colour.Alpha) / 255.0}
C.webkit_web_view_set_background_color((*C.WebKitWebView)(webview), &rgba)
colour.Alpha = 255
cssStr := C.CString(fmt.Sprintf("#webview-box {background-color: rgba(%d, %d, %d, %1.1f);}", colour.Red, colour.Green, colour.Blue, float32(colour.Alpha)/255.0))
provider := C.gtk_css_provider_new()
C.gtk_style_context_add_provider(
C.gtk_widget_get_style_context((*C.GtkWidget)(vbox)),
(*C.GtkStyleProvider)(unsafe.Pointer(provider)),
C.GTK_STYLE_PROVIDER_PRIORITY_USER)
C.g_object_unref(C.gpointer(provider))
C.gtk_css_provider_load_from_data(provider, cssStr, -1, nil)
C.free(unsafe.Pointer(cssStr))
}
func windowSetGeometryHints(window pointer, minWidth, minHeight, maxWidth, maxHeight int) {
size := C.GdkGeometry{
min_width: C.int(minWidth),
min_height: C.int(minHeight),
max_width: C.int(maxWidth),
max_height: C.int(maxHeight),
}
C.gtk_window_set_geometry_hints((*C.GtkWindow)(window), nil, &size, C.GDK_HINT_MAX_SIZE|C.GDK_HINT_MIN_SIZE)
}
func windowSetFrameless(window pointer, frameless bool) {
C.gtk_window_set_decorated((*C.GtkWindow)(window), gtkBool(!frameless))
// TODO: Deal with transparency for the titlebar if possible when !frameless
// Perhaps we just make it undecorated and add a menu bar inside?
}
// TODO: confirm this is working properly
func windowSetHTML(webview pointer, 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(
(*C.WebKitWebView)(webview),
cHTML,
uri,
empty)
}
func windowSetKeepAbove(window pointer, alwaysOnTop bool) {
C.gtk_window_set_keep_above((*C.GtkWindow)(window), gtkBool(alwaysOnTop))
}
func windowSetResizable(window pointer, resizable bool) {
C.gtk_window_set_resizable((*C.GtkWindow)(window), gtkBool(resizable))
}
func windowSetTitle(window pointer, title string) {
cTitle := C.CString(title)
C.gtk_window_set_title((*C.GtkWindow)(window), cTitle)
C.free(unsafe.Pointer(cTitle))
}
func windowSetTransparent(window pointer) {
screen := C.gtk_widget_get_screen((*C.GtkWidget)(window))
visual := C.gdk_screen_get_rgba_visual(screen)
if visual != nil && C.gdk_screen_is_composited(screen) == C.int(1) {
C.gtk_widget_set_app_paintable((*C.GtkWidget)(window), C.gboolean(1))
C.gtk_widget_set_visual((*C.GtkWidget)(window), visual)
}
}
func windowSetURL(webview pointer, uri string) {
target := C.CString(uri)
C.webkit_web_view_load_uri((*C.WebKitWebView)(webview), target)
C.free(unsafe.Pointer(target))
}
//export emit
func emit(we *C.WindowEvent) {
window := globalApplication.getWindowForID(uint(we.id))
if window != nil {
windowEvents <- &windowEvent{
WindowID: window.ID(),
EventID: uint(events.WindowEventType(we.event)),
}
}
}
//export handleDeleteEvent
func handleDeleteEvent(widget *C.GtkWidget, event *C.GdkEvent, data C.uintptr_t) C.gboolean {
processWindowEvent(C.uint(data), C.uint(events.Linux.WindowDeleteEvent))
return C.gboolean(0)
}
//export handleLoadChanged
func handleLoadChanged(webview *C.WebKitWebView, event C.WebKitLoadEvent, data C.uintptr_t) {
switch event {
case C.WEBKIT_LOAD_FINISHED:
processWindowEvent(C.uint(data), C.uint(events.Linux.WindowLoadChanged))
}
}
func windowSetupSignalHandlers(windowId uint, window, webview pointer, emit func(e events.WindowEventType)) {
c := NewCalloc()
defer c.Free()
winID := unsafe.Pointer(uintptr(C.uint(windowId)))
// Set up the window close event
C.signal_connect(unsafe.Pointer(window), c.String("delete-event"), C.handleDeleteEvent, winID)
C.signal_connect(unsafe.Pointer(webview), c.String("load-changed"), C.handleLoadChanged, winID)
contentManager := C.webkit_web_view_get_user_content_manager((*C.WebKitWebView)(webview))
C.signal_connect(unsafe.Pointer(contentManager), c.String("script-message-received::external"), C.sendMessageToBackend, nil)
C.signal_connect(unsafe.Pointer(webview), c.String("button-press-event"), C.onButtonEvent, winID)
C.signal_connect(unsafe.Pointer(webview), c.String("button-release-event"), C.onButtonEvent, winID)
C.signal_connect(unsafe.Pointer(webview), c.String("key-press-event"), C.onKeyPressEvent, winID)
}
func windowShowDevTools(webview pointer) {
inspector := C.webkit_web_view_get_inspector((*C.WebKitWebView)(webview))
C.webkit_web_inspector_show(inspector)
}
func windowStartDrag(window pointer, button uint, xroot int, yroot int, dragTime uint32) {
C.gtk_window_begin_move_drag(
(*C.GtkWindow)(window),
C.int(button),
C.int(xroot),
C.int(yroot),
C.uint32_t(dragTime))
}
func windowToggleDevTools(webview pointer) {
settings := C.webkit_web_view_get_settings((*C.WebKitWebView)(webview))
enabled := C.webkit_settings_get_enable_developer_extras(settings)
switch enabled {
case C.int(0):
enabled = C.int(1)
case C.int(1):
enabled = C.int(0)
}
C.webkit_settings_set_enable_developer_extras(settings, enabled)
}
func windowUnfullscreen(window pointer) {
C.gtk_window_unfullscreen((*C.GtkWindow)(window))
}
func windowUnmaximize(window pointer) {
C.gtk_window_unmaximize((*C.GtkWindow)(window))
}
func windowZoom(webview pointer) float64 {
return float64(C.webkit_web_view_get_zoom_level((*C.WebKitWebView)(webview)))
}
// FIXME: ZoomIn/Out is assumed to be incorrect!
func windowZoomIn(webview pointer) {
ZoomInFactor := 1.10
windowZoomSet(webview, windowZoom(webview)*ZoomInFactor)
}
func windowZoomOut(webview pointer) {
ZoomOutFactor := -1.10
windowZoomSet(webview, windowZoom(webview)*ZoomOutFactor)
}
func windowZoomSet(webview pointer, zoom float64) {
if zoom < 1 { // 1.0 is the smallest allowable
zoom = 1
}
C.webkit_web_view_set_zoom_level((*C.WebKitWebView)(webview), C.double(zoom))
}
func windowMove(window pointer, x, y int) {
C.gtk_window_move((*C.GtkWindow)(window), C.int(x), C.int(y))
}
// FIXME Change this to reflect mouse button!
//
//export onButtonEvent
func onButtonEvent(_ *C.GtkWidget, event *C.GdkEventButton, data C.uintptr_t) C.gboolean {
// Constants (defined here to be easier to use with purego)
GdkButtonPress := C.GDK_BUTTON_PRESS // 4
Gdk2ButtonPress := C.GDK_2BUTTON_PRESS // 5 for double-click
GdkButtonRelease := C.GDK_BUTTON_RELEASE // 7
windowId := uint(C.uint(data))
window := globalApplication.getWindowForID(windowId)
if window == nil {
return C.gboolean(0)
}
lw, ok := (window.(*WebviewWindow).impl).(*linuxWebviewWindow)
if !ok {
return C.gboolean(0)
}
if event == nil {
return C.gboolean(0)
}
if event.button == 3 {
return C.gboolean(0)
}
switch int(event._type) {
case GdkButtonPress:
lw.drag.MouseButton = uint(event.button)
lw.drag.XRoot = int(event.x_root)
lw.drag.YRoot = int(event.y_root)
lw.drag.DragTime = uint32(event.time)
case Gdk2ButtonPress:
// do we need something here?
case GdkButtonRelease:
lw.endDrag(uint(event.button), int(event.x_root), int(event.y_root))
}
return C.gboolean(0)
}
//export onDragNDrop
func onDragNDrop(target unsafe.Pointer, context *C.GdkDragContext, x C.gint, y C.gint, seldata unsafe.Pointer, info C.guint, time C.guint, data unsafe.Pointer) {
var length C.gint
selection := unsafe.Pointer(C.gtk_selection_data_get_data_with_length((*C.GtkSelectionData)(seldata), &length))
extracted := C.g_uri_list_extract_uris((*C.char)(selection))
defer C.g_strfreev(extracted)
uris := unsafe.Slice(
(**C.char)(unsafe.Pointer(extracted)),
int(length))
var filenames []string
for _, uri := range uris {
if uri == nil {
break
}
filenames = append(filenames, strings.TrimPrefix(C.GoString(uri), "file://"))
}
windowDragAndDropBuffer <- &dragAndDropMessage{
windowId: uint(*((*C.uint)(data))),
filenames: filenames,
}
C.gtk_drag_finish(context, C.true, C.false, time)
}
//export onKeyPressEvent
func onKeyPressEvent(widget *C.GtkWidget, event *C.GdkEventKey, userData C.uintptr_t) C.gboolean {
windowID := uint(C.uint(userData))
accelerator, ok := getKeyboardState(event)
if !ok {
return C.gboolean(1)
}
windowKeyEvents <- &windowKeyEvent{
windowId: windowID,
acceleratorString: accelerator,
}
return C.gboolean(1)
}
func getKeyboardState(event *C.GdkEventKey) (string, bool) {
modifiers := uint(event.state) & C.GDK_MODIFIER_MASK
keyCode := uint(event.keyval)
var acc accelerator
// Check Accelerators
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_MOD1_MASK) != 0 {
acc.Modifiers = append(acc.Modifiers, OptionOrAltKey)
}
if modifiers&(C.GDK_SUPER_MASK) != 0 {
acc.Modifiers = append(acc.Modifiers, SuperKey)
}
keyString, ok := VirtualKeyCodes[keyCode]
if !ok {
fmt.Println("Error Could not find key code: ", keyCode)
return "", false
}
acc.Key = keyString
return acc.String(), true
}
//export onProcessRequest
func onProcessRequest(request unsafe.Pointer, data unsafe.Pointer) {
windowId := uint(*((*C.uint)(data)))
webviewRequests <- &webViewAssetRequest{
Request: webview.NewRequest(request),
windowId: windowId,
windowName: globalApplication.getWindowForID(windowId).Name(),
}
}
//export sendMessageToBackend
func sendMessageToBackend(contentManager *C.WebKitUserContentManager, result *C.WebKitJavascriptResult,
data unsafe.Pointer) {
var msg string
value := C.webkit_javascript_result_get_js_value(result)
message := C.jsc_value_to_string(value)
msg = C.GoString(message)
defer C.g_free(C.gpointer(message))
windowMessageBuffer <- &windowMessage{
windowId: uint(windowID),
message: msg,
}
}
func gtkBool(input bool) C.gboolean {
if input {
return C.gboolean(1)
}
return C.gboolean(0)
}
// dialog related
func setWindowIcon(window pointer, icon []byte) {
loader := C.gdk_pixbuf_loader_new()
if loader == nil {
return
}
written := C.gdk_pixbuf_loader_write(
loader,
(*C.uchar)(&icon[0]),
C.ulong(len(icon)),
nil)
if written == 0 {
return
}
C.gdk_pixbuf_loader_close(loader, nil)
pixbuf := C.gdk_pixbuf_loader_get_pixbuf(loader)
if pixbuf != nil {
C.gtk_window_set_icon((*C.GtkWindow)(window), pixbuf)
}
C.g_object_unref(C.gpointer(loader))
}
//export messageDialogCB
func messageDialogCB(button C.int) {
fmt.Println("messageDialogCB", button)
}
func runChooserDialog(window pointer, allowMultiple, createFolders, showHidden bool, currentFolder, title string, action int, acceptLabel string, filters []FileFilter) (chan string, error) {
titleStr := C.CString(title)
defer C.free(unsafe.Pointer(titleStr))
cancelStr := C.CString("_Cancel")
defer C.free(unsafe.Pointer(cancelStr))
acceptLabelStr := C.CString(acceptLabel)
defer C.free(unsafe.Pointer(acceptLabelStr))
fc := C.gtkFileChooserDialogNew(
titleStr,
(*C.GtkWindow)(window),
C.GtkFileChooserAction(action),
cancelStr,
acceptLabelStr)
C.gtk_file_chooser_set_action((*C.GtkFileChooser)(fc), C.GtkFileChooserAction(action))
gtkFilters := []*C.GtkFileFilter{}
for _, filter := range filters {
f := (C.gtk_file_filter_new())
displayStr := C.CString(filter.DisplayName)
C.gtk_file_filter_set_name(f, displayStr)
C.free(unsafe.Pointer(displayStr))
patternStr := C.CString(filter.Pattern)
C.gtk_file_filter_add_pattern(f, patternStr)
C.free(unsafe.Pointer(patternStr))
C.gtk_file_chooser_add_filter((*C.GtkFileChooser)(fc), f)
gtkFilters = append(gtkFilters, f)
}
C.gtk_file_chooser_set_select_multiple(
(*C.GtkFileChooser)(fc),
gtkBool(allowMultiple))
C.gtk_file_chooser_set_create_folders(
(*C.GtkFileChooser)(fc),
gtkBool(createFolders))
C.gtk_file_chooser_set_show_hidden(
(*C.GtkFileChooser)(fc),
gtkBool(showHidden))
if currentFolder != "" {
path := C.CString(currentFolder)
C.gtk_file_chooser_set_current_folder(
(*C.GtkFileChooser)(fc),
path)
C.free(unsafe.Pointer(path))
}
// FIXME: This should be consolidated - duplicate exists in linux_purego.go
buildStringAndFree := func(s C.gpointer) string {
bytes := []byte{}
p := unsafe.Pointer(s)
for {
val := *(*byte)(p)
if val == 0 { // this is the null terminator
break
}
bytes = append(bytes, val)
p = unsafe.Add(p, 1)
}
C.g_free(s) // so we don't have to iterate a second time
return string(bytes)
}
selections := make(chan string)
// run this on the gtk thread
InvokeAsync(func() {
go func() {
response := C.gtk_dialog_run((*C.GtkDialog)(fc))
if response == C.GTK_RESPONSE_ACCEPT {
filenames := C.gtk_file_chooser_get_filenames((*C.GtkFileChooser)(fc))
iter := filenames
count := 0
for {
selections <- buildStringAndFree(C.gpointer(iter.data))
iter = iter.next
if iter == nil || count == 1024 {
break
}
count++
}
close(selections)
C.gtk_widget_destroy((*C.GtkWidget)(unsafe.Pointer(fc)))
}
}()
})
return selections, nil
}
func runOpenFileDialog(dialog *OpenFileDialogStruct) (chan string, error) {
const GtkFileChooserActionOpen = C.GTK_FILE_CHOOSER_ACTION_OPEN
window := nilPointer
if dialog.window != nil {
window = (dialog.window.impl).(*linuxWebviewWindow).window
}
buttonText := dialog.buttonText
if buttonText == "" {
buttonText = "_Open"
}
return runChooserDialog(
window,
dialog.allowsMultipleSelection,
dialog.canCreateDirectories,
dialog.showHiddenFiles,
dialog.directory,
dialog.title,
GtkFileChooserActionOpen,
buttonText,
dialog.filters)
}
func runQuestionDialog(parent pointer, options *MessageDialog) int {
cMsg := C.CString(options.Message)
cTitle := C.CString(options.Title)
defer C.free(unsafe.Pointer(cMsg))
defer C.free(unsafe.Pointer(cTitle))
hasButtons := false
if len(options.Buttons) > 0 {
hasButtons = true
}
dType, ok := map[DialogType]C.int{
InfoDialogType: C.GTK_MESSAGE_INFO,
// ErrorDialogType:
QuestionDialogType: C.GTK_MESSAGE_QUESTION,
WarningDialogType: C.GTK_MESSAGE_WARNING,
}[options.DialogType]
if !ok {
// FIXME: Add logging here!
dType = C.GTK_MESSAGE_INFO
}
dialog := C.new_message_dialog((*C.GtkWindow)(parent), cMsg, dType, C.bool(hasButtons))
if options.Title != "" {
C.gtk_window_set_title(
(*C.GtkWindow)(unsafe.Pointer(dialog)),
cTitle)
}
if img, err := pngToImage(options.Icon); err == nil {
gbytes := C.g_bytes_new_static(
C.gconstpointer(unsafe.Pointer(&img.Pix[0])),
C.ulong(len(img.Pix)))
defer C.g_bytes_unref(gbytes)
pixBuf := C.gdk_pixbuf_new_from_bytes(
gbytes,
C.GDK_COLORSPACE_RGB,
1, // has_alpha
8,
C.int(img.Bounds().Dx()),
C.int(img.Bounds().Dy()),
C.int(img.Stride),
)
image := C.gtk_image_new_from_pixbuf(pixBuf)
C.gtk_widget_set_visible((*C.GtkWidget)(image), C.gboolean(1))
contentArea := C.gtk_dialog_get_content_area((*C.GtkDialog)(dialog))
C.gtk_container_add(
(*C.GtkContainer)(unsafe.Pointer(contentArea)),
(*C.GtkWidget)(image))
}
for i, button := range options.Buttons {
cLabel := C.CString(button.Label)
defer C.free(unsafe.Pointer(cLabel))
index := C.int(i)
C.gtk_dialog_add_button(
(*C.GtkDialog)(dialog), cLabel, index)
if button.IsDefault {
C.gtk_dialog_set_default_response((*C.GtkDialog)(dialog), index)
}
}
defer C.gtk_widget_destroy((*C.GtkWidget)(dialog))
return int(C.gtk_dialog_run((*C.GtkDialog)(unsafe.Pointer(dialog))))
}
func runSaveFileDialog(dialog *SaveFileDialogStruct) (chan string, error) {
window := nilPointer
buttonText := dialog.buttonText
if buttonText == "" {
buttonText = "_Save"
}
results, err := runChooserDialog(
window,
false, // multiple selection
dialog.canCreateDirectories,
dialog.showHiddenFiles,
dialog.directory,
dialog.title,
C.GTK_FILE_CHOOSER_ACTION_SAVE,
buttonText,
dialog.filters)
return results, err
}