github.com/secoba/wails/v2@v2.6.4/internal/frontend/desktop/linux/window.go (about) 1 //go:build linux 2 // +build linux 3 4 package linux 5 6 /* 7 #cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 8 9 #include <JavaScriptCore/JavaScript.h> 10 #include <gtk/gtk.h> 11 #include <webkit2/webkit2.h> 12 #include <stdio.h> 13 #include <limits.h> 14 #include <stdint.h> 15 #include "window.h" 16 17 */ 18 import "C" 19 import ( 20 "log" 21 "strings" 22 "sync" 23 "unsafe" 24 25 "github.com/secoba/wails/v2/internal/frontend" 26 "github.com/secoba/wails/v2/pkg/menu" 27 "github.com/secoba/wails/v2/pkg/options" 28 "github.com/secoba/wails/v2/pkg/options/linux" 29 ) 30 31 func gtkBool(input bool) C.gboolean { 32 if input { 33 return C.gboolean(1) 34 } 35 return C.gboolean(0) 36 } 37 38 type Window struct { 39 appoptions *options.App 40 debug bool 41 devtoolsEnabled bool 42 gtkWindow unsafe.Pointer 43 contentManager unsafe.Pointer 44 webview unsafe.Pointer 45 applicationMenu *menu.Menu 46 menubar *C.GtkWidget 47 webviewBox *C.GtkWidget 48 vbox *C.GtkWidget 49 accels *C.GtkAccelGroup 50 minWidth, minHeight, maxWidth, maxHeight int 51 } 52 53 func bool2Cint(value bool) C.int { 54 if value { 55 return C.int(1) 56 } 57 return C.int(0) 58 } 59 60 func NewWindow(appoptions *options.App, debug bool, devtoolsEnabled bool) *Window { 61 validateWebKit2Version(appoptions) 62 63 result := &Window{ 64 appoptions: appoptions, 65 debug: debug, 66 devtoolsEnabled: devtoolsEnabled, 67 minHeight: appoptions.MinHeight, 68 minWidth: appoptions.MinWidth, 69 maxHeight: appoptions.MaxHeight, 70 maxWidth: appoptions.MaxWidth, 71 } 72 73 gtkWindow := C.gtk_window_new(C.GTK_WINDOW_TOPLEVEL) 74 C.g_object_ref_sink(C.gpointer(gtkWindow)) 75 result.gtkWindow = unsafe.Pointer(gtkWindow) 76 77 webviewName := C.CString("webview-box") 78 defer C.free(unsafe.Pointer(webviewName)) 79 result.webviewBox = C.gtk_box_new(C.GTK_ORIENTATION_VERTICAL, 0) 80 C.gtk_widget_set_name(result.webviewBox, webviewName) 81 82 result.vbox = C.gtk_box_new(C.GTK_ORIENTATION_VERTICAL, 0) 83 C.gtk_container_add(result.asGTKContainer(), result.vbox) 84 85 result.contentManager = unsafe.Pointer(C.webkit_user_content_manager_new()) 86 external := C.CString("external") 87 defer C.free(unsafe.Pointer(external)) 88 C.webkit_user_content_manager_register_script_message_handler(result.cWebKitUserContentManager(), external) 89 C.SetupInvokeSignal(result.contentManager) 90 91 var webviewGpuPolicy int 92 if appoptions.Linux != nil { 93 webviewGpuPolicy = int(appoptions.Linux.WebviewGpuPolicy) 94 } else { 95 // workaround for https://github.com/wailsapp/wails/issues/2977 96 webviewGpuPolicy = int(linux.WebviewGpuPolicyNever) 97 } 98 99 webview := C.SetupWebview( 100 result.contentManager, 101 result.asGTKWindow(), 102 bool2Cint(appoptions.HideWindowOnClose), 103 C.int(webviewGpuPolicy), 104 ) 105 result.webview = unsafe.Pointer(webview) 106 buttonPressedName := C.CString("button-press-event") 107 defer C.free(unsafe.Pointer(buttonPressedName)) 108 C.ConnectButtons(unsafe.Pointer(webview)) 109 110 if devtoolsEnabled { 111 C.DevtoolsEnabled(unsafe.Pointer(webview), C.int(1), C.bool(debug && appoptions.Debug.OpenInspectorOnStartup)) 112 // Install Ctrl-Shift-F12 hotkey to call ShowInspector 113 C.InstallF12Hotkey(unsafe.Pointer(gtkWindow)) 114 } 115 116 if !(debug || appoptions.EnableDefaultContextMenu) { 117 C.DisableContextMenu(unsafe.Pointer(webview)) 118 } 119 120 // Set background colour 121 RGBA := appoptions.BackgroundColour 122 result.SetBackgroundColour(RGBA.R, RGBA.G, RGBA.B, RGBA.A) 123 124 // Setup window 125 result.SetKeepAbove(appoptions.AlwaysOnTop) 126 result.SetResizable(!appoptions.DisableResize) 127 result.SetDefaultSize(appoptions.Width, appoptions.Height) 128 result.SetDecorated(!appoptions.Frameless) 129 result.SetTitle(appoptions.Title) 130 result.SetMinSize(appoptions.MinWidth, appoptions.MinHeight) 131 result.SetMaxSize(appoptions.MaxWidth, appoptions.MaxHeight) 132 if appoptions.Linux != nil { 133 if appoptions.Linux.Icon != nil { 134 result.SetWindowIcon(appoptions.Linux.Icon) 135 } 136 if appoptions.Linux.WindowIsTranslucent { 137 C.SetWindowTransparency(gtkWindow) 138 } 139 } 140 141 // Menu 142 result.SetApplicationMenu(appoptions.Menu) 143 144 return result 145 } 146 147 func (w *Window) asGTKWidget() *C.GtkWidget { 148 return C.GTKWIDGET(w.gtkWindow) 149 } 150 151 func (w *Window) asGTKWindow() *C.GtkWindow { 152 return C.GTKWINDOW(w.gtkWindow) 153 } 154 155 func (w *Window) asGTKContainer() *C.GtkContainer { 156 return C.GTKCONTAINER(w.gtkWindow) 157 } 158 159 func (w *Window) cWebKitUserContentManager() *C.WebKitUserContentManager { 160 return (*C.WebKitUserContentManager)(w.contentManager) 161 } 162 163 func (w *Window) Fullscreen() { 164 C.ExecuteOnMainThread(C.Fullscreen, C.gpointer(w.asGTKWindow())) 165 } 166 167 func (w *Window) UnFullscreen() { 168 if !w.IsFullScreen() { 169 return 170 } 171 C.ExecuteOnMainThread(C.UnFullscreen, C.gpointer(w.asGTKWindow())) 172 w.SetMinSize(w.minWidth, w.minHeight) 173 w.SetMaxSize(w.maxWidth, w.maxHeight) 174 } 175 176 func (w *Window) Destroy() { 177 C.gtk_widget_destroy(w.asGTKWidget()) 178 C.g_object_unref(C.gpointer(w.gtkWindow)) 179 } 180 181 func (w *Window) Close() { 182 C.gtk_window_close(w.asGTKWindow()) 183 } 184 185 func (w *Window) Center() { 186 C.ExecuteOnMainThread(C.Center, C.gpointer(w.asGTKWindow())) 187 } 188 189 func (w *Window) SetPosition(x int, y int) { 190 invokeOnMainThread(func() { 191 C.SetPosition(unsafe.Pointer(w.asGTKWindow()), C.int(x), C.int(y)) 192 }) 193 } 194 195 func (w *Window) Size() (int, int) { 196 var width, height C.int 197 var wg sync.WaitGroup 198 wg.Add(1) 199 invokeOnMainThread(func() { 200 C.gtk_window_get_size(w.asGTKWindow(), &width, &height) 201 wg.Done() 202 }) 203 wg.Wait() 204 return int(width), int(height) 205 } 206 207 func (w *Window) GetPosition() (int, int) { 208 var width, height C.int 209 var wg sync.WaitGroup 210 wg.Add(1) 211 invokeOnMainThread(func() { 212 C.gtk_window_get_position(w.asGTKWindow(), &width, &height) 213 wg.Done() 214 }) 215 wg.Wait() 216 return int(width), int(height) 217 } 218 219 func (w *Window) SetMaxSize(maxWidth int, maxHeight int) { 220 w.maxHeight = maxHeight 221 w.maxWidth = maxWidth 222 invokeOnMainThread(func() { 223 C.SetMinMaxSize(w.asGTKWindow(), C.int(w.minWidth), C.int(w.minHeight), C.int(w.maxWidth), C.int(w.maxHeight)) 224 }) 225 } 226 227 func (w *Window) SetMinSize(minWidth int, minHeight int) { 228 w.minHeight = minHeight 229 w.minWidth = minWidth 230 invokeOnMainThread(func() { 231 C.SetMinMaxSize(w.asGTKWindow(), C.int(w.minWidth), C.int(w.minHeight), C.int(w.maxWidth), C.int(w.maxHeight)) 232 }) 233 } 234 235 func (w *Window) Show() { 236 C.ExecuteOnMainThread(C.Show, C.gpointer(w.asGTKWindow())) 237 } 238 239 func (w *Window) Hide() { 240 C.ExecuteOnMainThread(C.Hide, C.gpointer(w.asGTKWindow())) 241 } 242 243 func (w *Window) Maximise() { 244 C.ExecuteOnMainThread(C.Maximise, C.gpointer(w.asGTKWindow())) 245 } 246 247 func (w *Window) UnMaximise() { 248 C.ExecuteOnMainThread(C.UnMaximise, C.gpointer(w.asGTKWindow())) 249 } 250 251 func (w *Window) Minimise() { 252 C.ExecuteOnMainThread(C.Minimise, C.gpointer(w.asGTKWindow())) 253 } 254 255 func (w *Window) UnMinimise() { 256 C.ExecuteOnMainThread(C.UnMinimise, C.gpointer(w.asGTKWindow())) 257 } 258 259 func (w *Window) IsFullScreen() bool { 260 result := C.IsFullscreen(w.asGTKWidget()) 261 if result != 0 { 262 return true 263 } 264 return false 265 } 266 267 func (w *Window) IsMaximised() bool { 268 result := C.IsMaximised(w.asGTKWidget()) 269 return result > 0 270 } 271 272 func (w *Window) IsMinimised() bool { 273 result := C.IsMinimised(w.asGTKWidget()) 274 return result > 0 275 } 276 277 func (w *Window) IsNormal() bool { 278 return !w.IsMaximised() && !w.IsMinimised() && !w.IsFullScreen() 279 } 280 281 func (w *Window) SetBackgroundColour(r uint8, g uint8, b uint8, a uint8) { 282 windowIsTranslucent := false 283 if w.appoptions.Linux != nil && w.appoptions.Linux.WindowIsTranslucent { 284 windowIsTranslucent = true 285 } 286 data := C.RGBAOptions{ 287 r: C.uchar(r), 288 g: C.uchar(g), 289 b: C.uchar(b), 290 a: C.uchar(a), 291 webview: w.webview, 292 webviewBox: unsafe.Pointer(w.webviewBox), 293 windowIsTranslucent: gtkBool(windowIsTranslucent), 294 } 295 invokeOnMainThread(func() { C.SetBackgroundColour(unsafe.Pointer(&data)) }) 296 297 } 298 299 func (w *Window) SetWindowIcon(icon []byte) { 300 if len(icon) == 0 { 301 return 302 } 303 C.SetWindowIcon(w.asGTKWindow(), (*C.guchar)(&icon[0]), (C.gsize)(len(icon))) 304 } 305 306 func (w *Window) Run(url string) { 307 if w.menubar != nil { 308 C.gtk_box_pack_start(C.GTKBOX(unsafe.Pointer(w.vbox)), w.menubar, 0, 0, 0) 309 } 310 311 C.gtk_box_pack_start(C.GTKBOX(unsafe.Pointer(w.webviewBox)), C.GTKWIDGET(w.webview), 1, 1, 0) 312 C.gtk_box_pack_start(C.GTKBOX(unsafe.Pointer(w.vbox)), w.webviewBox, 1, 1, 0) 313 _url := C.CString(url) 314 C.LoadIndex(w.webview, _url) 315 defer C.free(unsafe.Pointer(_url)) 316 if w.appoptions.StartHidden { 317 w.Hide() 318 } 319 C.gtk_widget_show_all(w.asGTKWidget()) 320 w.Center() 321 switch w.appoptions.WindowStartState { 322 case options.Fullscreen: 323 w.Fullscreen() 324 case options.Minimised: 325 w.Minimise() 326 case options.Maximised: 327 w.Maximise() 328 } 329 } 330 331 func (w *Window) SetKeepAbove(top bool) { 332 C.gtk_window_set_keep_above(w.asGTKWindow(), gtkBool(top)) 333 } 334 335 func (w *Window) SetResizable(resizable bool) { 336 C.gtk_window_set_resizable(w.asGTKWindow(), gtkBool(resizable)) 337 } 338 339 func (w *Window) SetDefaultSize(width int, height int) { 340 C.gtk_window_set_default_size(w.asGTKWindow(), C.int(width), C.int(height)) 341 } 342 343 func (w *Window) SetSize(width int, height int) { 344 C.gtk_window_resize(w.asGTKWindow(), C.gint(width), C.gint(height)) 345 } 346 347 func (w *Window) SetDecorated(frameless bool) { 348 C.gtk_window_set_decorated(w.asGTKWindow(), gtkBool(frameless)) 349 } 350 351 func (w *Window) SetTitle(title string) { 352 C.SetTitle(w.asGTKWindow(), C.CString(title)) 353 } 354 355 func (w *Window) ExecJS(js string) { 356 jscallback := C.JSCallback{ 357 webview: w.webview, 358 script: C.CString(js), 359 } 360 invokeOnMainThread(func() { C.ExecuteJS(unsafe.Pointer(&jscallback)) }) 361 } 362 363 func (w *Window) StartDrag() { 364 C.StartDrag(w.webview, w.asGTKWindow()) 365 } 366 367 func (w *Window) StartResize(edge uintptr) { 368 C.StartResize(w.webview, w.asGTKWindow(), C.GdkWindowEdge(edge)) 369 } 370 371 func (w *Window) Quit() { 372 C.gtk_main_quit() 373 } 374 375 func (w *Window) OpenFileDialog(dialogOptions frontend.OpenDialogOptions, multipleFiles int, action C.GtkFileChooserAction) { 376 377 data := C.OpenFileDialogOptions{ 378 window: w.asGTKWindow(), 379 title: C.CString(dialogOptions.Title), 380 multipleFiles: C.int(multipleFiles), 381 action: action, 382 } 383 384 if len(dialogOptions.Filters) > 0 { 385 // Create filter array 386 mem := NewCalloc() 387 arraySize := len(dialogOptions.Filters) + 1 388 data.filters = C.AllocFileFilterArray((C.size_t)(arraySize)) 389 filters := unsafe.Slice((**C.struct__GtkFileFilter)(unsafe.Pointer(data.filters)), arraySize) 390 for index, filter := range dialogOptions.Filters { 391 thisFilter := C.gtk_file_filter_new() 392 C.g_object_ref(C.gpointer(thisFilter)) 393 if filter.DisplayName != "" { 394 cName := mem.String(filter.DisplayName) 395 C.gtk_file_filter_set_name(thisFilter, cName) 396 } 397 if filter.Pattern != "" { 398 for _, thisPattern := range strings.Split(filter.Pattern, ";") { 399 cThisPattern := mem.String(thisPattern) 400 C.gtk_file_filter_add_pattern(thisFilter, cThisPattern) 401 } 402 } 403 // Add filter to array 404 filters[index] = thisFilter 405 } 406 mem.Free() 407 filters[arraySize-1] = nil 408 } 409 410 if dialogOptions.CanCreateDirectories { 411 data.createDirectories = C.int(1) 412 } 413 414 if dialogOptions.ShowHiddenFiles { 415 data.showHiddenFiles = C.int(1) 416 } 417 418 if dialogOptions.DefaultFilename != "" { 419 data.defaultFilename = C.CString(dialogOptions.DefaultFilename) 420 } 421 422 if dialogOptions.DefaultDirectory != "" { 423 data.defaultDirectory = C.CString(dialogOptions.DefaultDirectory) 424 } 425 426 invokeOnMainThread(func() { C.Opendialog(unsafe.Pointer(&data)) }) 427 } 428 429 func (w *Window) MessageDialog(dialogOptions frontend.MessageDialogOptions) { 430 431 data := C.MessageDialogOptions{ 432 window: w.gtkWindow, 433 title: C.CString(dialogOptions.Title), 434 message: C.CString(dialogOptions.Message), 435 } 436 switch dialogOptions.Type { 437 case frontend.InfoDialog: 438 data.messageType = C.int(0) 439 case frontend.ErrorDialog: 440 data.messageType = C.int(1) 441 case frontend.QuestionDialog: 442 data.messageType = C.int(2) 443 case frontend.WarningDialog: 444 data.messageType = C.int(3) 445 } 446 invokeOnMainThread(func() { C.MessageDialog(unsafe.Pointer(&data)) }) 447 } 448 449 func (w *Window) ToggleMaximise() { 450 if w.IsMaximised() { 451 w.UnMaximise() 452 } else { 453 w.Maximise() 454 } 455 } 456 457 func (w *Window) ShowInspector() { 458 invokeOnMainThread(func() { C.ShowInspector(w.webview) }) 459 } 460 461 // showModalDialogAndExit shows a modal dialog and exits the app. 462 func showModalDialogAndExit(title, message string) { 463 go func() { 464 data := C.MessageDialogOptions{ 465 title: C.CString(title), 466 message: C.CString(message), 467 messageType: C.int(1), 468 } 469 470 C.MessageDialog(unsafe.Pointer(&data)) 471 }() 472 473 <-messageDialogResult 474 log.Fatal(message) 475 }