Skip to content

Commit

Permalink
[WEP] Customise Window Titlebars (#3508)
Browse files Browse the repository at this point in the history
* Add proposal.
Reference Mac implementation

* Add windows support. Update proposal.

* Update example

* Rename Active->Enable,Inactive->Disabled. Ensure window can get controls back after hiding close on windows. Added guide. Updated example.

* Add ExStyle option for setting titlebar style.

* Fix linux builds

* Tidy up
  • Loading branch information
leaanthony authored Jun 8, 2024
1 parent 2baca5d commit bf9e17a
Show file tree
Hide file tree
Showing 12 changed files with 518 additions and 90 deletions.
111 changes: 111 additions & 0 deletions mkdocs-website/docs/en/learn/guides/customising-windows.md
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions mkdocs-website/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
128 changes: 100 additions & 28 deletions v3/examples/window/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
_ "embed"
"fmt"
"github.com/wailsapp/wails/v3/pkg/w32"
"log"
"math/rand"
"runtime"
Expand Down Expand Up @@ -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)).
Expand All @@ -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)).
Expand All @@ -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)).
Expand All @@ -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").
Expand Down Expand Up @@ -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()
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
8 changes: 0 additions & 8 deletions v3/pkg/application/messageprocessor_window.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
24 changes: 12 additions & 12 deletions v3/pkg/application/webview_window.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,6 @@ type (
isNormal() bool
isVisible() bool
isFocused() bool
setFullscreenButtonEnabled(enabled bool)
setMinimiseButtonEnabled(enabled bool)
setMaximiseButtonEnabled(enabled bool)
focus()
show()
hide()
Expand All @@ -90,6 +87,9 @@ type (
flash(enabled bool)
handleKeyEvent(acceleratorString string)
getBorderSizes() *LRTB
setMinimiseButtonState(state ButtonState)
setMaximiseButtonState(state ButtonState)
setCloseButtonState(state ButtonState)
}
)

Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit bf9e17a

Please sign in to comment.