package application import ( "encoding/json" "errors" "fmt" "sync" "time" "github.com/samber/lo" "github.com/wailsapp/wails/v3/pkg/logger" "github.com/wailsapp/wails/v3/pkg/events" ) type ( webviewWindowImpl interface { center() close() destroy() disableSizeConstraints() execJS(js string) focus() forceReload() fullscreen() getScreen() (*Screen, error) getZoom() float64 height() int hide() hide() isFullscreen() bool isMaximised() bool isMinimised() bool isNormal() bool isVisible() bool maximise() minimise() nativeWindowHandle() uintptr on(eventID uint) openContextMenu(menu *Menu, data *ContextMenuData) position() (int, int) reload() restore() // FIXME: not used run() setAlwaysOnTop(alwaysOnTop bool) setBackgroundColour(color RGBA) setFrameless(bool) setFullscreenButtonEnabled(enabled bool) setHTML(html string) setMaxSize(width, height int) setMinSize(width, height int) setPosition(x int, y int) setResizable(resizable bool) setSize(width, height int) setTitle(title string) setURL(url string) setZoom(zoom float64) show() size() (int, int) toggleDevTools() unfullscreen() unmaximise() unminimise() width() int zoom() zoomIn() zoomOut() zoomReset() } ) type WindowEventListener struct { callback func(ctx *WindowEventContext) } type WebviewWindow struct { options WebviewWindowOptions impl webviewWindowImpl implLock sync.RWMutex id uint eventListeners map[uint][]*WindowEventListener eventListenersLock sync.RWMutex contextMenus map[string]*Menu contextMenusLock sync.RWMutex // A map of listener cancellation functions cancellersLock sync.RWMutex cancellers []func() } var windowID uint var windowIDLock sync.RWMutex func getWindowID() uint { windowIDLock.Lock() defer windowIDLock.Unlock() windowID++ return windowID } // Use onApplicationEvent to register a callback for an application event from a window. // This will handle tidying up the callback when the window is destroyed func (w *WebviewWindow) onApplicationEvent(eventType events.ApplicationEventType, callback func()) { cancelFn := globalApplication.On(eventType, callback) w.addCancellationFunction(cancelFn) } // NewWindow creates a new window with the given options func NewWindow(options WebviewWindowOptions) *WebviewWindow { if options.Width == 0 { options.Width = 800 } if options.Height == 0 { options.Height = 600 } if options.URL == "" { options.URL = "/" } result := &WebviewWindow{ id: getWindowID(), options: options, eventListeners: make(map[uint][]*WindowEventListener), contextMenus: make(map[string]*Menu), } return result } func (w *WebviewWindow) addCancellationFunction(canceller func()) { w.cancellersLock.Lock() defer w.cancellersLock.Unlock() w.cancellers = append(w.cancellers, canceller) } // formatJS ensures the 'data' provided marshals to valid json or panics func (w *WebviewWindow) formatJS(f string, callID string, data string) string { j, err := json.Marshal(data) if err != nil { panic(err) } return fmt.Sprintf(f, callID, j) } func (w *WebviewWindow) CallError(callID *string, result string) { if w.impl != nil { w.impl.execJS(w.formatJS("_wails.callErrorCallback('%s', %s);", *callID, result)) } } func (w *WebviewWindow) CallResponse(callID *string, result string) { if w.impl != nil { w.impl.execJS(w.formatJS("_wails.callCallback('%s', %s, true);", *callID, result)) } } func (w *WebviewWindow) DialogError(dialogID *string, result string) { if w.impl != nil { w.impl.execJS(w.formatJS("_wails.dialogErrorCallback('%s', %s);", *dialogID, result)) } } func (w *WebviewWindow) DialogResponse(dialogID *string, result string) { if w.impl != nil { w.impl.execJS(w.formatJS("_wails.dialogCallback('%s', %s, true);", *dialogID, result)) } } // SetTitle sets the title of the window func (w *WebviewWindow) SetTitle(title string) *WebviewWindow { w.implLock.RLock() defer w.implLock.RUnlock() w.options.Title = title if w.impl != nil { invokeSync(func() { w.impl.setTitle(title) }) } return w } // Name returns the name of the window func (w *WebviewWindow) Name() string { return w.options.Name } // SetSize sets the size of the window func (w *WebviewWindow) SetSize(width, height int) *WebviewWindow { // Don't set size if fullscreen if w.IsFullscreen() { return w } w.options.Width = width w.options.Height = height var newMaxWidth = w.options.MaxWidth var newMaxHeight = w.options.MaxHeight if width > w.options.MaxWidth && w.options.MaxWidth != 0 { newMaxWidth = width } if height > w.options.MaxHeight && w.options.MaxHeight != 0 { newMaxHeight = height } if newMaxWidth != 0 || newMaxHeight != 0 { w.SetMaxSize(newMaxWidth, newMaxHeight) } var newMinWidth = w.options.MinWidth var newMinHeight = w.options.MinHeight if width < w.options.MinWidth && w.options.MinWidth != 0 { newMinWidth = width } if height < w.options.MinHeight && w.options.MinHeight != 0 { newMinHeight = height } if newMinWidth != 0 || newMinHeight != 0 { w.SetMinSize(newMinWidth, newMinHeight) } if w.impl != nil { invokeSync(func() { w.impl.setSize(width, height) }) } return w } func (w *WebviewWindow) run() { if w.impl != nil { return } w.implLock.Lock() w.impl = newWindowImpl(w) w.implLock.Unlock() invokeSync(w.impl.run) } // SetAlwaysOnTop sets the window to be always on top. func (w *WebviewWindow) SetAlwaysOnTop(b bool) *WebviewWindow { w.options.AlwaysOnTop = b if w.impl != nil { invokeSync(func() { w.impl.setAlwaysOnTop(b) }) } return w } // Show shows the window. func (w *WebviewWindow) Show() *WebviewWindow { if globalApplication.impl == nil { return w } if w.impl == nil { w.run() return w } invokeSync(w.impl.show) return w } // Hide hides the window. func (w *WebviewWindow) Hide() *WebviewWindow { w.options.Hidden = true if w.impl != nil { invokeSync(w.impl.hide) } return w } func (w *WebviewWindow) SetURL(s string) *WebviewWindow { w.options.URL = s if w.impl != nil { invokeSync(func() { w.impl.setURL(s) }) } return w } // SetZoom sets the zoom level of the window. func (w *WebviewWindow) SetZoom(magnification float64) *WebviewWindow { w.options.Zoom = magnification if w.impl != nil { invokeSync(func() { w.impl.setZoom(magnification) }) } return w } // GetZoom returns the current zoom level of the window. func (w *WebviewWindow) GetZoom() float64 { if w.impl != nil { return invokeSyncWithResult(w.impl.getZoom) } return 1 } // SetResizable sets whether the window is resizable. func (w *WebviewWindow) SetResizable(b bool) *WebviewWindow { w.options.DisableResize = !b if w.impl != nil { invokeSync(func() { w.impl.setResizable(b) }) } return w } // Resizable returns true if the window is resizable. func (w *WebviewWindow) Resizable() bool { return !w.options.DisableResize } // SetMinSize sets the minimum size of the window. func (w *WebviewWindow) SetMinSize(minWidth, minHeight int) *WebviewWindow { w.options.MinWidth = minWidth w.options.MinHeight = minHeight currentWidth, currentHeight := w.Size() newWidth, newHeight := currentWidth, currentHeight var newSize bool if minHeight != 0 && currentHeight < minHeight { newHeight = minHeight w.options.Height = newHeight newSize = true } if minWidth != 0 && currentWidth < minWidth { newWidth = minWidth w.options.Width = newWidth newSize = true } if w.impl != nil { if newSize { invokeSync(func() { w.impl.setSize(newWidth, newHeight) }) } invokeSync(func() { w.impl.setMinSize(minWidth, minHeight) }) } return w } // SetMaxSize sets the maximum size of the window. func (w *WebviewWindow) SetMaxSize(maxWidth, maxHeight int) *WebviewWindow { w.options.MaxWidth = maxWidth w.options.MaxHeight = maxHeight currentWidth, currentHeight := w.Size() newWidth, newHeight := currentWidth, currentHeight var newSize bool if maxHeight != 0 && currentHeight > maxHeight { newHeight = maxHeight w.options.Height = maxHeight newSize = true } if maxWidth != 0 && currentWidth > maxWidth { newWidth = maxWidth w.options.Width = maxWidth newSize = true } if w.impl != nil { if newSize { invokeSync(func() { w.impl.setSize(newWidth, newHeight) }) } invokeSync(func() { w.impl.setMaxSize(maxWidth, maxHeight) }) } return w } // ExecJS executes the given javascript in the context of the window. func (w *WebviewWindow) ExecJS(js string) { if w.impl == nil { return } w.impl.execJS(js) } // Fullscreen sets the window to fullscreen mode. Min/Max size constraints are disabled. func (w *WebviewWindow) Fullscreen() *WebviewWindow { if w.impl == nil { w.options.StartState = WindowStateFullscreen return w } if !w.IsFullscreen() { w.disableSizeConstraints() invokeSync(w.impl.fullscreen) } return w } func (w *WebviewWindow) SetFullscreenButtonEnabled(enabled bool) *WebviewWindow { w.options.FullscreenButtonEnabled = enabled if w.impl != nil { invokeSync(func() { w.impl.setFullscreenButtonEnabled(enabled) }) } return w } // IsMinimised returns true if the window is minimised func (w *WebviewWindow) IsMinimised() bool { if w.impl == nil { return false } return invokeSyncWithResult(w.impl.isMinimised) } // IsVisible returns true if the window is visible func (w *WebviewWindow) IsVisible() bool { if w.impl == nil { return false } return invokeSyncWithResult(w.impl.isVisible) } // IsMaximised returns true if the window is maximised func (w *WebviewWindow) IsMaximised() bool { if w.impl == nil { return false } return invokeSyncWithResult(w.impl.isMaximised) } // Size returns the size of the window func (w *WebviewWindow) Size() (int, int) { if w.impl == nil { return 0, 0 } var width, height int invokeSync(func() { width, height = w.impl.size() }) return width, height } // IsFullscreen returns true if the window is fullscreen func (w *WebviewWindow) IsFullscreen() bool { w.implLock.RLock() defer w.implLock.RUnlock() if w.impl == nil { return false } return invokeSyncWithResult(w.impl.isFullscreen) } // SetBackgroundColour sets the background colour of the window func (w *WebviewWindow) SetBackgroundColour(colour RGBA) *WebviewWindow { w.options.BackgroundColour = colour if w.impl != nil { invokeSync(func() { w.impl.setBackgroundColour(colour) }) } return w } func (w *WebviewWindow) handleMessage(message string) { w.info(message) // Check for special messages if message == "test" { invokeSync(func() { w.SetTitle("Hello World") }) } w.info("ProcessMessage from front end:", message) } // Center centers the window on the screen func (w *WebviewWindow) Center() { if w.impl == nil { w.options.Centered = true return } invokeSync(w.impl.center) } // On registers a callback for the given window event func (w *WebviewWindow) On(eventType events.WindowEventType, callback func(ctx *WindowEventContext)) func() { eventID := uint(eventType) w.eventListenersLock.Lock() defer w.eventListenersLock.Unlock() windowEventListener := &WindowEventListener{ callback: callback, } w.eventListeners[eventID] = append(w.eventListeners[eventID], windowEventListener) if w.impl != nil { w.impl.on(eventID) } return func() { w.eventListenersLock.Lock() defer w.eventListenersLock.Unlock() w.eventListeners[eventID] = lo.Without(w.eventListeners[eventID], windowEventListener) } } func (w *WebviewWindow) handleWindowEvent(id uint) { w.eventListenersLock.RLock() for _, listener := range w.eventListeners[id] { go listener.callback(blankWindowEventContext) } w.eventListenersLock.RUnlock() } // Width returns the width of the window func (w *WebviewWindow) Width() int { if w.impl == nil { return 0 } return invokeSyncWithResult(w.impl.width) } // Height returns the height of the window func (w *WebviewWindow) Height() int { if w.impl == nil { return 0 } return invokeSyncWithResult(w.impl.height) } // Position returns the position of the window func (w *WebviewWindow) Position() (int, int) { w.implLock.RLock() defer w.implLock.RUnlock() if w.impl == nil { return 0, 0 } var x, y int invokeSync(func() { x, y = w.impl.position() }) return x, y } func (w *WebviewWindow) Destroy() { if w.impl == nil { return } // Cancel the callbacks for _, cancelFunc := range w.cancellers { cancelFunc() } invokeSync(w.impl.destroy) } // Reload reloads the page assets func (w *WebviewWindow) Reload() { if w.impl == nil { return } invokeSync(w.impl.reload) } // ForceReload forces the window to reload the page assets func (w *WebviewWindow) ForceReload() { if w.impl == nil { return } invokeSync(w.impl.forceReload) } // ToggleFullscreen toggles the window between fullscreen and normal func (w *WebviewWindow) ToggleFullscreen() { if w.impl == nil { return } invokeSync(func() { if w.IsFullscreen() { w.UnFullscreen() } else { w.Fullscreen() } }) } func (w *WebviewWindow) ToggleDevTools() { if w.impl == nil { return } invokeSync(w.impl.toggleDevTools) } // ZoomReset resets the zoom level of the webview content to 100% func (w *WebviewWindow) ZoomReset() *WebviewWindow { if w.impl != nil { invokeSync(w.impl.zoomReset) } return w } // ZoomIn increases the zoom level of the webview content func (w *WebviewWindow) ZoomIn() { if w.impl == nil { return } invokeSync(w.impl.zoomIn) } // ZoomOut decreases the zoom level of the webview content func (w *WebviewWindow) ZoomOut() { if w.impl == nil { return } invokeSync(w.impl.zoomOut) } // Close closes the window func (w *WebviewWindow) Close() { if w.impl == nil { return } invokeSync(w.impl.close) } func (w *WebviewWindow) Zoom() { if w.impl == nil { return } invokeSync(w.impl.zoom) } // SetHTML sets the HTML of the window to the given html string. func (w *WebviewWindow) SetHTML(html string) *WebviewWindow { w.options.HTML = html if w.impl != nil { invokeSync(func() { w.impl.setHTML(html) }) } return w } // SetPosition sets the position of the window. func (w *WebviewWindow) SetPosition(x, y int) *WebviewWindow { w.options.X = x w.options.Y = y if w.impl != nil { invokeSync(func() { w.impl.setPosition(x, y) }) } return w } // Minimise minimises the window. func (w *WebviewWindow) Minimise() *WebviewWindow { if w.impl == nil { w.options.StartState = WindowStateMinimised return w } if !w.IsMinimised() { invokeSync(w.impl.minimise) } return w } // Maximise maximises the window. Min/Max size constraints are disabled. func (w *WebviewWindow) Maximise() *WebviewWindow { if w.impl == nil { w.options.StartState = WindowStateMaximised return w } if !w.IsMaximised() { w.disableSizeConstraints() invokeSync(w.impl.maximise) } return w } // UnMinimise un-minimises the window. Min/Max size constraints are re-enabled. func (w *WebviewWindow) UnMinimise() { if w.impl == nil { return } if w.IsMinimised() { invokeSync(w.impl.unminimise) } } // UnMaximise un-maximises the window. func (w *WebviewWindow) UnMaximise() { if w.impl == nil { return } if w.IsMaximised() { w.enableSizeConstraints() invokeSync(w.impl.unmaximise) } } // UnFullscreen un-fullscreens the window. func (w *WebviewWindow) UnFullscreen() { if w.impl == nil { return } if w.IsFullscreen() { w.enableSizeConstraints() invokeSync(w.impl.unfullscreen) } } // Restore restores the window to its previous state if it was previously minimised, maximised or fullscreen. func (w *WebviewWindow) Restore() { if w.impl == nil { return } invokeSync(func() { if w.IsMinimised() { w.UnMinimise() } else if w.IsMaximised() { w.UnMaximise() } else if w.IsFullscreen() { w.UnFullscreen() } }) } func (w *WebviewWindow) disableSizeConstraints() { if w.impl == nil { return } invokeSync(func() { if w.options.MinWidth > 0 && w.options.MinHeight > 0 { w.impl.setMinSize(0, 0) } if w.options.MaxWidth > 0 && w.options.MaxHeight > 0 { w.impl.setMaxSize(0, 0) } }) } func (w *WebviewWindow) enableSizeConstraints() { if w.impl == nil { return } invokeSync(func() { if w.options.MinWidth > 0 && w.options.MinHeight > 0 { w.SetMinSize(w.options.MinWidth, w.options.MinHeight) } if w.options.MaxWidth > 0 && w.options.MaxHeight > 0 { w.SetMaxSize(w.options.MaxWidth, w.options.MaxHeight) } }) } // GetScreen returns the screen that the window is on func (w *WebviewWindow) GetScreen() (*Screen, error) { if w.impl == nil { return nil, nil } return invokeSyncWithResultAndError(w.impl.getScreen) } // SetFrameless removes the window frame and title bar func (w *WebviewWindow) SetFrameless(frameless bool) *WebviewWindow { w.options.Frameless = frameless if w.impl != nil { invokeSync(func() { w.impl.setFrameless(frameless) }) } return w } func (w *WebviewWindow) dispatchWailsEvent(event *WailsEvent) { if w.impl != nil { w.impl.execJS(fmt.Sprintf("_wails.dispatchWailsEvent(%s);", event.ToJSON())) } } func (w *WebviewWindow) info(message string, args ...any) { globalApplication.Log(&logger.Message{ Level: "INFO", Message: message, Data: args, Sender: w.Name(), Time: time.Now(), }) } func (w *WebviewWindow) error(message string, args ...any) { globalApplication.Log(&logger.Message{ Level: "ERROR", Message: message, Data: args, Sender: w.Name(), Time: time.Now(), }) } func (w *WebviewWindow) handleDragAndDropMessage(event *dragAndDropMessage) { ctx := newWindowEventContext() ctx.setDroppedFiles(event.filenames) for _, listener := range w.eventListeners[uint(events.FilesDropped)] { listener.callback(ctx) } } func (w *WebviewWindow) openContextMenu(data *ContextMenuData) { menu, ok := w.contextMenus[data.Id] if !ok { // try application level context menu menu, ok = globalApplication.getContextMenu(data.Id) if !ok { w.error("No context menu found for id: %s", data.Id) return } } menu.setContextData(data) if w.impl == nil { return } w.impl.openContextMenu(menu, data) } // RegisterContextMenu registers a context menu and assigns it the given name. func (w *WebviewWindow) RegisterContextMenu(name string, menu *Menu) { w.contextMenusLock.Lock() defer w.contextMenusLock.Unlock() w.contextMenus[name] = menu } // NativeWindowHandle returns the platform native window handle for the window. func (w *WebviewWindow) NativeWindowHandle() (uintptr, error) { if w.impl == nil { return 0, errors.New("native handle unavailable as window is not running") } return w.impl.nativeWindowHandle(), nil } func (w *WebviewWindow) Focus() { if w.impl == nil { w.options.Focused = true return } invokeSync(w.impl.focus) }