diff --git a/mkdocs-website/docs/en/learn/guides/customising-windows.md b/mkdocs-website/docs/en/learn/guides/customising-windows.md new file mode 100644 index 00000000000..d8217fb06e0 --- /dev/null +++ b/mkdocs-website/docs/en/learn/guides/customising-windows.md @@ -0,0 +1,111 @@ +# Customising Window Controls in Wails + +Wails provides an API to control the appearance and functionality of the controls of a window. +This functionality is available on Windows and macOS, but not on Linux. + +## Setting the Window Button States + +The button states are defined by the `ButtonState` enum: + +```go +type ButtonState int + +const ( + ButtonEnabled ButtonState = 0 + ButtonDisabled ButtonState = 1 + ButtonHidden ButtonState = 2 +) +``` + +- `ButtonEnabled`: The button is enabled and visible. +- `ButtonDisabled`: The button is visible but disabled (grayed out). +- `ButtonHidden`: The button is hidden from the titlebar. + +The button states can be set during window creation or at runtime. + +### Setting Button States During Window Creation + +When creating a new window, you can set the initial state of the buttons using the `WebviewWindowOptions` struct: + +```go +package main + +import ( + "github.com/wailsapp/wails/v3/pkg/application" +) + +func main() { + app := application.New(application.Options{ + Name: "My Application", + }) + + app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + MinimiseButtonState: application.ButtonHidden, + MaximiseButtonState: application.ButtonDisabled, + CloseButtonState: application.ButtonEnabled, + }) + + app.Run() +} +``` + +In the example above, the minimise button is hidden, the maximise button is inactive (grayed out), and the close button is active. + +### Setting Button States at Runtime + +You can also change the button states at runtime using the following methods on the `Window` interface: + +```go +window.SetMinimiseButtonState(wails.ButtonHidden) +window.SetMaximiseButtonState(wails.ButtonEnabled) +window.SetCloseButtonState(wails.ButtonDisabled) +``` + +### Platform Differences + +The button state functionality behaves slightly differently on Windows and macOS: + +| | Windows | Mac | +|-----------------------|------------------------|------------------------| +| Disable Min/Max/Close | Disables Min/Max/Close | Disables Min/Max/Close | +| Hide Min | Disables Min | Hides Min button | +| Hide Max | Disables Max | Hides Max button | +| Hide Close | Hides all controls | Hides Close | + +Note: On Windows, it is not possible to hide the Min/Max buttons individually. +However, disabling both will hide both of the controls and only show the +close button. + +### Controlling Window Style (Windows) + +To control the style of the titlebar on Windows, you can use the `ExStyle` field in the `WebviewWindowOptions` struct: + +Example: +```go +package main + +import ( + "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wailsapp/wails/v3/pkg/w32" +) + +func main() { + app := application.New(application.Options{ + Name: "My Application", + }) + + app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + Windows: application.WindowsWindow{ + ExStyle: w32.WS_EX_TOOLWINDOW | w32.WS_EX_NOREDIRECTIONBITMAP | w32.WS_EX_TOPMOST, + }, + }) + + app.Run() +} +``` + +Other options that affect the Extended Style of a window will be overridden by this setting: +- HiddenOnTaskbar +- AlwaysOnTop +- IgnoreMouseEvents +- BackgroundType \ No newline at end of file diff --git a/mkdocs-website/mkdocs.yml b/mkdocs-website/mkdocs.yml index f85498cc3fe..7f9e36d5cf5 100644 --- a/mkdocs-website/mkdocs.yml +++ b/mkdocs-website/mkdocs.yml @@ -142,6 +142,8 @@ nav: - Learn More: - Runtime: learn/runtime.md - Plugins: learn/plugins.md + - Guides: + - Customising Windows: learn/guides/customising-windows.md - Feedback: getting-started/feedback.md - Feedback: getting-started/feedback.md - What's New in v3?: whats-new.md diff --git a/v3/examples/window/main.go b/v3/examples/window/main.go index f427d44946a..4b705ac5b9b 100644 --- a/v3/examples/window/main.go +++ b/v3/examples/window/main.go @@ -3,6 +3,7 @@ package main import ( _ "embed" "fmt" + "github.com/wailsapp/wails/v3/pkg/w32" "log" "math/rand" "runtime" @@ -64,12 +65,7 @@ func main() { myMenu.Add("New WebviewWindow (Disable Minimise)"). OnClick(func(ctx *application.Context) { app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ - Windows: application.WindowsWindow{ - DisableMinimiseButton: true, - }, - Mac: application.MacWindow{ - DisableMinimiseButton: true, - }, + MinimiseButtonState: application.ButtonDisabled, }). SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)). SetRelativePosition(rand.Intn(1000), rand.Intn(800)). @@ -80,12 +76,29 @@ func main() { myMenu.Add("New WebviewWindow (Disable Maximise)"). OnClick(func(ctx *application.Context) { app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ - Windows: application.WindowsWindow{ - DisableMaximiseButton: true, - }, - Mac: application.MacWindow{ - DisableMaximiseButton: true, - }, + MaximiseButtonState: application.ButtonDisabled, + }). + SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)). + SetRelativePosition(rand.Intn(1000), rand.Intn(800)). + SetURL("https://wails.io"). + Show() + windowCounter++ + }) + myMenu.Add("New WebviewWindow (Hide Minimise)"). + OnClick(func(ctx *application.Context) { + app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + MinimiseButtonState: application.ButtonHidden, + }). + SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)). + SetRelativePosition(rand.Intn(1000), rand.Intn(800)). + SetURL("https://wails.io"). + Show() + windowCounter++ + }) + myMenu.Add("New WebviewWindow (Hide Maximise)"). + OnClick(func(ctx *application.Context) { + app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + MaximiseButtonState: application.ButtonHidden, }). SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)). SetRelativePosition(rand.Intn(1000), rand.Intn(800)). @@ -94,13 +107,22 @@ func main() { windowCounter++ }) } - if runtime.GOOS == "darwin" { + if runtime.GOOS == "darwin" || runtime.GOOS == "windows" { myMenu.Add("New WebviewWindow (Disable Close)"). OnClick(func(ctx *application.Context) { app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ - Mac: application.MacWindow{ - DisableCloseButton: true, - }, + CloseButtonState: application.ButtonDisabled, + }). + SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)). + SetRelativePosition(rand.Intn(1000), rand.Intn(800)). + SetURL("https://wails.io"). + Show() + windowCounter++ + }) + myMenu.Add("New WebviewWindow (Hide Close)"). + OnClick(func(ctx *application.Context) { + app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + CloseButtonState: application.ButtonHidden, }). SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)). SetRelativePosition(rand.Intn(1000), rand.Intn(800)). @@ -110,6 +132,22 @@ func main() { }) } + if runtime.GOOS == "windows" { + myMenu.Add("New WebviewWindow (Custom ExStyle)"). + OnClick(func(ctx *application.Context) { + app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + Windows: application.WindowsWindow{ + DisableMenu: true, + ExStyle: w32.WS_EX_TOOLWINDOW | w32.WS_EX_NOREDIRECTIONBITMAP | w32.WS_EX_TOPMOST, + }, + }). + SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)). + SetRelativePosition(rand.Intn(1000), rand.Intn(800)). + SetURL("https://wails.io"). + Show() + windowCounter++ + }) + } myMenu.Add("New WebviewWindow (Hides on Close one time)"). SetAccelerator("CmdOrCtrl+H"). @@ -278,12 +316,6 @@ func main() { w.SetMinSize(200, 200) }) }) - sizeMenu.Add("Set Max Size (600,600)").OnClick(func(ctx *application.Context) { - currentWindow(func(w *application.WebviewWindow) { - w.SetFullscreenButtonEnabled(false) - w.SetMaxSize(600, 600) - }) - }) sizeMenu.Add("Get Current WebviewWindow Size").OnClick(func(ctx *application.Context) { currentWindow(func(w *application.WebviewWindow) { width, height := w.Size() @@ -297,12 +329,6 @@ func main() { }) }) - sizeMenu.Add("Reset Max Size").OnClick(func(ctx *application.Context) { - currentWindow(func(w *application.WebviewWindow) { - w.SetMaxSize(0, 0) - w.SetFullscreenButtonEnabled(true) - }) - }) positionMenu := menu.AddSubmenu("Position") positionMenu.Add("Set Relative Position (0,0)").OnClick(func(ctx *application.Context) { currentWindow(func(w *application.WebviewWindow) { @@ -346,6 +372,52 @@ func main() { w.Center() }) }) + titleBarMenu := menu.AddSubmenu("Controls") + titleBarMenu.Add("Disable Minimise").OnClick(func(ctx *application.Context) { + currentWindow(func(w *application.WebviewWindow) { + w.SetMinimiseButtonState(application.ButtonDisabled) + }) + }) + titleBarMenu.Add("Enable Minimise").OnClick(func(ctx *application.Context) { + currentWindow(func(w *application.WebviewWindow) { + w.SetMinimiseButtonState(application.ButtonEnabled) + }) + }) + titleBarMenu.Add("Hide Minimise").OnClick(func(ctx *application.Context) { + currentWindow(func(w *application.WebviewWindow) { + w.SetMinimiseButtonState(application.ButtonHidden) + }) + }) + titleBarMenu.Add("Disable Maximise").OnClick(func(ctx *application.Context) { + currentWindow(func(w *application.WebviewWindow) { + w.SetMaximiseButtonState(application.ButtonDisabled) + }) + }) + titleBarMenu.Add("Enable Maximise").OnClick(func(ctx *application.Context) { + currentWindow(func(w *application.WebviewWindow) { + w.SetMaximiseButtonState(application.ButtonEnabled) + }) + }) + titleBarMenu.Add("Hide Maximise").OnClick(func(ctx *application.Context) { + currentWindow(func(w *application.WebviewWindow) { + w.SetMaximiseButtonState(application.ButtonHidden) + }) + }) + titleBarMenu.Add("Disable Close").OnClick(func(ctx *application.Context) { + currentWindow(func(w *application.WebviewWindow) { + w.SetCloseButtonState(application.ButtonDisabled) + }) + }) + titleBarMenu.Add("Enable Close").OnClick(func(ctx *application.Context) { + currentWindow(func(w *application.WebviewWindow) { + w.SetCloseButtonState(application.ButtonEnabled) + }) + }) + titleBarMenu.Add("Hide Close").OnClick(func(ctx *application.Context) { + currentWindow(func(w *application.WebviewWindow) { + w.SetCloseButtonState(application.ButtonHidden) + }) + }) stateMenu := menu.AddSubmenu("State") stateMenu.Add("Minimise (for 2 secs)").OnClick(func(ctx *application.Context) { currentWindow(func(w *application.WebviewWindow) { diff --git a/v3/pkg/application/messageprocessor_window.go b/v3/pkg/application/messageprocessor_window.go index ce69d541f08..1ea5db44153 100644 --- a/v3/pkg/application/messageprocessor_window.go +++ b/v3/pkg/application/messageprocessor_window.go @@ -249,14 +249,6 @@ func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWrite } window.SetFrameless(*frameless) m.ok(rw) - case WindowSetFullscreenButtonEnabled: - enabled := args.Bool("enabled") - if enabled == nil { - m.Error("Invalid SetFullscreenButtonEnabled Message: 'enabled' value required") - return - } - window.SetFullscreenButtonEnabled(*enabled) - m.ok(rw) case WindowSetMaxSize: width := args.Int("width") if width == nil { diff --git a/v3/pkg/application/webview_window.go b/v3/pkg/application/webview_window.go index 0e4f4e4367d..783a12ca971 100644 --- a/v3/pkg/application/webview_window.go +++ b/v3/pkg/application/webview_window.go @@ -71,9 +71,6 @@ type ( isNormal() bool isVisible() bool isFocused() bool - setFullscreenButtonEnabled(enabled bool) - setMinimiseButtonEnabled(enabled bool) - setMaximiseButtonEnabled(enabled bool) focus() show() hide() @@ -90,6 +87,9 @@ type ( flash(enabled bool) handleKeyEvent(acceleratorString string) getBorderSizes() *LRTB + setMinimiseButtonState(state ButtonState) + setMaximiseButtonState(state ButtonState) + setCloseButtonState(state ButtonState) } ) @@ -540,31 +540,31 @@ func (w *WebviewWindow) Fullscreen() Window { return w } -func (w *WebviewWindow) SetFullscreenButtonEnabled(enabled bool) Window { - w.options.FullscreenButtonEnabled = enabled +func (w *WebviewWindow) SetMinimiseButtonState(state ButtonState) Window { + w.options.MinimiseButtonState = state if w.impl != nil { InvokeSync(func() { - w.impl.setFullscreenButtonEnabled(enabled) + w.impl.setMinimiseButtonState(state) }) } return w } -func (w *WebviewWindow) SetMinimiseButtonEnabled(enabled bool) Window { - w.options.FullscreenButtonEnabled = enabled +func (w *WebviewWindow) SetMaximiseButtonState(state ButtonState) Window { + w.options.MaximiseButtonState = state if w.impl != nil { InvokeSync(func() { - w.impl.setMinimiseButtonEnabled(enabled) + w.impl.setMaximiseButtonState(state) }) } return w } -func (w *WebviewWindow) SetMaximiseButtonEnabled(enabled bool) Window { - w.options.FullscreenButtonEnabled = enabled +func (w *WebviewWindow) SetCloseButtonState(state ButtonState) Window { + w.options.CloseButtonState = state if w.impl != nil { InvokeSync(func() { - w.impl.setMaximiseButtonEnabled(enabled) + w.impl.setCloseButtonState(state) }) } return w diff --git a/v3/pkg/application/webview_window_darwin.go b/v3/pkg/application/webview_window_darwin.go index 32ba6765576..40d493d728c 100644 --- a/v3/pkg/application/webview_window_darwin.go +++ b/v3/pkg/application/webview_window_darwin.go @@ -641,22 +641,38 @@ static void windowHide(void *window) { [(WebviewWindow*)window orderOut:nil]; } -static void enableMinimiseButton(void *window, bool enabled) { +// setButtonState sets the state of the given button +// 0 = enabled +// 1 = disabled +// 2 = hidden +static void setButtonState(void *button, int state) { + if (button == nil) { + return; + } + NSButton *nsbutton = (NSButton*)button; + nsbutton.hidden = state == 2; + nsbutton.enabled = state != 1; +} + +// setMinimiseButtonState sets the minimise button state +static void setMinimiseButtonState(void *window, int state) { WebviewWindow* nsWindow = (WebviewWindow*)window; NSButton *minimiseButton = [nsWindow standardWindowButton:NSWindowMiniaturizeButton]; - minimiseButton.enabled = enabled; + setButtonState(minimiseButton, state); } -static void enableMaximiseButton(void *window, bool enabled) { +// setMaximiseButtonState sets the maximise button state +static void setMaximiseButtonState(void *window, int state) { WebviewWindow* nsWindow = (WebviewWindow*)window; NSButton *maximiseButton = [nsWindow standardWindowButton:NSWindowZoomButton]; - maximiseButton.enabled = enabled; + setButtonState(maximiseButton, state); } -static void enableCloseButton(void *window, bool enabled) { +// setCloseButtonState sets the close button state +static void setCloseButtonState(void *window, int state) { WebviewWindow* nsWindow = (WebviewWindow*)window; NSButton *closeButton = [nsWindow standardWindowButton:NSWindowCloseButton]; - closeButton.enabled = enabled; + setButtonState(closeButton, state); } // windowShowMenu opens an NSMenu at the given coordinates @@ -1131,15 +1147,10 @@ func (w *macosWebviewWindow) run() { case MacBackdropNormal: } - if macOptions.DisableMinimiseButton { - w.setMinimiseButtonEnabled(false) - } - if macOptions.DisableMaximiseButton { - w.setMaximiseButtonEnabled(false) - } - if macOptions.DisableCloseButton { - w.setCloseButtonEnabled(false) - } + // Initialise the window buttons + w.setMinimiseButtonState(options.MinimiseButtonState) + w.setMaximiseButtonState(options.MaximiseButtonState) + w.setCloseButtonState(options.CloseButtonState) if options.IgnoreMouseEvents { C.windowIgnoreMouseEvents(w.nsWindow, C.bool(true)) @@ -1270,14 +1281,14 @@ func (w *macosWebviewWindow) startDrag() error { return nil } -func (w *macosWebviewWindow) setMinimiseButtonEnabled(enabled bool) { - C.enableMinimiseButton(w.nsWindow, C.bool(enabled)) +func (w *macosWebviewWindow) setMinimiseButtonState(state ButtonState) { + C.setMinimiseButtonState(w.nsWindow, C.int(state)) } -func (w *macosWebviewWindow) setMaximiseButtonEnabled(enabled bool) { - C.enableMaximiseButton(w.nsWindow, C.bool(enabled)) +func (w *macosWebviewWindow) setMaximiseButtonState(state ButtonState) { + C.setMaximiseButtonState(w.nsWindow, C.int(state)) } -func (w *macosWebviewWindow) setCloseButtonEnabled(enabled bool) { - C.enableCloseButton(w.nsWindow, C.bool(enabled)) +func (w *macosWebviewWindow) setCloseButtonState(state ButtonState) { + C.setCloseButtonState(w.nsWindow, C.int(state)) } diff --git a/v3/pkg/application/webview_window_linux.go b/v3/pkg/application/webview_window_linux.go index cbf8edee643..726b6f1e171 100644 --- a/v3/pkg/application/webview_window_linux.go +++ b/v3/pkg/application/webview_window_linux.go @@ -337,3 +337,18 @@ func (w *linuxWebviewWindow) handleKeyEvent(acceleratorString string) { // } w.parent.processKeyBinding(acceleratorString) } + +// SetMinimiseButtonState is unsupported on Linux +func (w *linuxWebviewWindow) SetMinimiseButtonState(state ButtonState) Window { + return w +} + +// SetMaximiseButtonState is unsupported on Linux +func (w *linuxWebviewWindow) SetMaximiseButtonState(state ButtonState) Window { + return w +} + +// SetFullscreenButtonState is unsupported on Linux +func (w *linuxWebviewWindow) SetCloseButtonState(state ButtonState) Window { + return w +} diff --git a/v3/pkg/application/webview_window_options.go b/v3/pkg/application/webview_window_options.go index 2f220e3f1d1..cad27dba8bb 100644 --- a/v3/pkg/application/webview_window_options.go +++ b/v3/pkg/application/webview_window_options.go @@ -14,6 +14,14 @@ const ( WindowStateFullscreen ) +type ButtonState int + +const ( + ButtonEnabled ButtonState = 0 + ButtonDisabled ButtonState = 1 + ButtonHidden ButtonState = 2 +) + type WebviewWindowOptions struct { // Name is a unique identifier that can be given to a window. Name string @@ -108,6 +116,11 @@ type WebviewWindowOptions struct { // Linux options Linux LinuxWindow + // Toolbar button states + MinimiseButtonState ButtonState + MaximiseButtonState ButtonState + CloseButtonState ButtonState + // ShouldClose is called when the window is about to close. // Return true to allow the window to close, or false to prevent it from closing. ShouldClose func(window *WebviewWindow) bool @@ -284,11 +297,8 @@ type WindowsWindow struct { // Permissions map for WebView2. If empty, default permissions will be granted. Permissions map[CoreWebView2PermissionKind]CoreWebView2PermissionState - // Disables the minimise button - DisableMinimiseButton bool - - // Disables the maximise button - DisableMaximiseButton bool + // ExStyle is the extended window style + ExStyle int } type Theme int @@ -370,15 +380,6 @@ type MacWindow struct { // WebviewPreferences contains preferences for the webview WebviewPreferences MacWebviewPreferences - - // Disables the minimise button - DisableMinimiseButton bool - - // Disables the maximise button - DisableMaximiseButton bool - - // Disables the close button - DisableCloseButton bool } // MacWebviewPreferences contains preferences for the Mac webview diff --git a/v3/pkg/application/webview_window_windows.go b/v3/pkg/application/webview_window_windows.go index 0a313e236c4..9a28b91183a 100644 --- a/v3/pkg/application/webview_window_windows.go +++ b/v3/pkg/application/webview_window_windows.go @@ -192,8 +192,7 @@ func (w *windowsWebviewWindow) run() { w.chromium = edge.NewChromium() - var exStyle uint - exStyle = w32.WS_EX_CONTROLPARENT + exStyle := w32.WS_EX_CONTROLPARENT if options.BackgroundType != BackgroundTypeSolid { exStyle |= w32.WS_EX_NOREDIRECTIONBITMAP if w.parent.options.IgnoreMouseEvents { @@ -210,6 +209,10 @@ func (w *windowsWebviewWindow) run() { exStyle |= w32.WS_EX_APPWINDOW } + if options.Windows.ExStyle != 0 { + exStyle = options.Windows.ExStyle + } + // ToDo: X, Y should also be scaled, should it be always relative to the main monitor? var startX, _ = lo.Coalesce(options.X, w32.CW_USEDEFAULT) var startY, _ = lo.Coalesce(options.Y, w32.CW_USEDEFAULT) @@ -234,7 +237,7 @@ func (w *windowsWebviewWindow) run() { var style uint = w32.WS_OVERLAPPEDWINDOW w.hwnd = w32.CreateWindowEx( - exStyle, + uint(exStyle), windowClassName, w32.MustStringToUTF16Ptr(options.Title), style, @@ -255,9 +258,10 @@ func (w *windowsWebviewWindow) run() { w.setupChromium() - // Min/max buttons - w.setMinimiseButtonEnabled(!options.Windows.DisableMinimiseButton) - w.setMaximiseButtonEnabled(!options.Windows.DisableMaximiseButton) + // Initialise the window buttons + w.setMinimiseButtonState(options.MinimiseButtonState) + w.setMaximiseButtonState(options.MaximiseButtonState) + w.setCloseButtonState(options.CloseButtonState) // Register the window with the application getNativeApplication().registerWindow(w) @@ -1680,3 +1684,41 @@ func NewIconFromResource(instance w32.HINSTANCE, resId uint16) (w32.HICON, error } return result, err } + +func (w *windowsWebviewWindow) setMinimiseButtonState(state ButtonState) { + switch state { + case ButtonDisabled, ButtonHidden: + w.setStyle(false, w32.WS_MINIMIZEBOX) + case ButtonEnabled: + w.setStyle(true, w32.WS_SYSMENU) + w.setStyle(true, w32.WS_MINIMIZEBOX) + + } +} + +func (w *windowsWebviewWindow) setMaximiseButtonState(state ButtonState) { + switch state { + case ButtonDisabled, ButtonHidden: + w.setStyle(false, w32.WS_MAXIMIZEBOX) + case ButtonEnabled: + w.setStyle(true, w32.WS_SYSMENU) + w.setStyle(true, w32.WS_MAXIMIZEBOX) + } +} + +func (w *windowsWebviewWindow) setCloseButtonState(state ButtonState) { + switch state { + case ButtonEnabled: + w.setStyle(true, w32.WS_SYSMENU) + _ = w32.EnableCloseButton(w.hwnd) + case ButtonDisabled: + w.setStyle(true, w32.WS_SYSMENU) + _ = w32.DisableCloseButton(w.hwnd) + case ButtonHidden: + w.setStyle(false, w32.WS_SYSMENU) + } +} + +func (w *windowsWebviewWindow) setGWLStyle(style int) { + w32.SetWindowLong(w.hwnd, w32.GWL_STYLE, uint32(style)) +} diff --git a/v3/pkg/application/window.go b/v3/pkg/application/window.go index 57b85dbfac9..e8d7f214cde 100644 --- a/v3/pkg/application/window.go +++ b/v3/pkg/application/window.go @@ -55,8 +55,10 @@ type Window interface { SetAlwaysOnTop(b bool) Window SetBackgroundColour(colour RGBA) Window SetFrameless(frameless bool) Window - SetFullscreenButtonEnabled(enabled bool) Window SetHTML(html string) Window + SetMinimiseButtonState(state ButtonState) Window + SetMaximiseButtonState(state ButtonState) Window + SetCloseButtonState(state ButtonState) Window SetMaxSize(maxWidth, maxHeight int) Window SetMinSize(minWidth, minHeight int) Window SetRelativePosition(x, y int) Window diff --git a/v3/pkg/w32/window.go b/v3/pkg/w32/window.go index 545b4213542..5a862965904 100644 --- a/v3/pkg/w32/window.go +++ b/v3/pkg/w32/window.go @@ -13,6 +13,21 @@ import ( "unsafe" ) +const ( + SC_CLOSE = 0xF060 + SC_MOVE = 0xF010 + SC_MAXIMIZE = 0xF030 + SC_MINIMIZE = 0xF020 + SC_SIZE = 0xF000 + SC_RESTORE = 0xF120 +) + +var ( + user32 = syscall.NewLazyDLL("user32.dll") + getSystemMenu = user32.NewProc("GetSystemMenu") + enableMenuItem = user32.NewProc("EnableMenuItem") +) + const ( GCLP_HBRBACKGROUND int32 = -10 GCLP_HICON int32 = -14 @@ -251,3 +266,31 @@ func EnumChildWindows(hwnd HWND, callback func(hwnd HWND, lparam LPARAM) LRESULT r, _, _ := procEnumChildWindows.Call(hwnd, syscall.NewCallback(callback), 0) return r } + +func DisableCloseButton(hwnd HWND) error { + hSysMenu, _, err := getSystemMenu.Call(hwnd, 0) + if hSysMenu == 0 { + return err + } + + r1, _, err := enableMenuItem.Call(hSysMenu, SC_CLOSE, MF_BYCOMMAND|MF_DISABLED|MF_GRAYED) + if r1 == 0 { + return err + } + + return nil +} + +func EnableCloseButton(hwnd HWND) error { + hSysMenu, _, err := getSystemMenu.Call(hwnd, 0) + if hSysMenu == 0 { + return err + } + + r1, _, err := enableMenuItem.Call(hSysMenu, SC_CLOSE, MF_BYCOMMAND|MF_ENABLED) + if r1 == 0 { + return err + } + + return nil +} diff --git a/v3/wep/proposals/titlebar-buttons/proposal.md b/v3/wep/proposals/titlebar-buttons/proposal.md new file mode 100644 index 00000000000..8bd6b8c4fdc --- /dev/null +++ b/v3/wep/proposals/titlebar-buttons/proposal.md @@ -0,0 +1,137 @@ +# Wails Enhancement Proposal (WEP) + +## Customising Window Controls in Wails + +**Author**: Lea Anthony +**Created**: 2024-05-20 + +## Summary + +This is a proposal for an API to control the appearance and functionality of window controls in Wails. +This will only be available on Windows and macOS. + +## Motivation + +We currently do not fully support the ability to customise window controls. + +## Detailed Design + +### Controlling Button State + +1. A new enum will be added: + +```go + type ButtonState int + + const ( + ButtonEnabled ButtonState = 0 + ButtonDisabled ButtonState = 1 + ButtonHidden ButtonState = 2 + ) +``` + +2. These options will be added to the `WebviewWindowOptions` option struct: + +```go + MinimiseButtonState ButtonState + MaximiseButtonState ButtonState + CloseButtonState ButtonState +``` + +3. These options will be removed from the current Windows/Mac options: + +- DisableMinimiseButton +- DisableMaximiseButton +- DisableCloseButton + +4. These methods will be added to the `Window` interface: + +```go + SetMinimizeButtonState(state ButtonState) + SetMaximizeButtonState(state ButtonState) + SetCloseButtonState(state ButtonState) +``` + +The settings translate to the following functionality on each platform: + +| | Windows | Mac | +|-----------------------|------------------------|------------------------| +| Disable Min/Max/Close | Disables Min/Max/Close | Disables Min/Max/Close | +| Hide Min | Disables Min | Hides Min button | +| Hide Max | Disables Max | Hides Max button | +| Hide Close | Hides all controls | Hides Close | + +Note: On Windows, it is not possible to hide the Min/Max buttons individually. +However, disabling both will hide both of the controls and only show the +close button. + +### Controlling Window Style (Windows) + +As Windows currently does not have much in the way of controlling the style of the +titlebar, a new option will be added to the `WebviewWindowOptions` option struct: + +```go + ExStyle int +``` + +If this is set, then the new Window will use the style specified in the `ExStyle` field. + +Example: +```go +package main + +import ( + "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wailsapp/wails/v3/pkg/w32" +) + +func main() { + app := application.New(application.Options{ + Name: "My Application", + }) + + app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + Windows: application.WindowsWindow{ + ExStyle: w32.WS_EX_TOOLWINDOW | w32.WS_EX_NOREDIRECTIONBITMAP | w32.WS_EX_TOPMOST, + }, + }) + + app.Run() +} +``` + +## Pros/Cons + +### Pros + +- We bring much needed customisation capabilities to both macOS and Windows + +### Cons + +- Windows works slightly different to macOS +- No Linux support (doesn't look like it's possible regardless of the solution) + +## Alternatives Considered + +The alternative is to draw your own titlebar, but this is a lot of work and often doesn't look good. + +## Backwards Compatibility + +This is not backwards compatible as we remove the old "disable button" options. + +## Test Plan + +As part of the implementation, the window example will be updated to test the functionality. + +## Reference Implementation + +There is a reference implementation as part of this proposal. + +## Maintenance Plan + +This feature will be maintained and supported by the Wails developers. + +## Conclusion + +This API would be a leap forward in giving developers greater control over their application window appearances. +