Add window opacity support
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run

This commit is contained in:
Snider 2026-04-15 18:41:31 +01:00
parent d0e8474785
commit c9562e5e59
8 changed files with 104 additions and 13 deletions

View file

@ -442,6 +442,29 @@ func (s *Subsystem) windowAlwaysOnTop(_ context.Context, _ *mcp.CallToolRequest,
return nil, WindowAlwaysOnTopOutput{Success: true}, nil
}
// --- window_opacity ---
type WindowOpacityInput struct {
Name string `json:"name"`
Opacity float64 `json:"opacity"`
}
type WindowOpacityOutput struct {
Success bool `json:"success"`
}
func (s *Subsystem) windowOpacity(_ context.Context, _ *mcp.CallToolRequest, input WindowOpacityInput) (*mcp.CallToolResult, WindowOpacityOutput, error) {
r := s.core.Action("window.setOpacity").Run(context.Background(), core.NewOptions(
core.Option{Key: "task", Value: window.TaskSetOpacity{Name: input.Name, Opacity: input.Opacity}},
))
if !r.OK {
if e, ok := r.Value.(error); ok {
return nil, WindowOpacityOutput{}, e
}
return nil, WindowOpacityOutput{}, nil
}
return nil, WindowOpacityOutput{Success: true}, nil
}
// --- window_background_colour ---
type WindowBackgroundColourInput struct {
@ -523,6 +546,10 @@ func (s *Subsystem) registerWindowTools(server *mcp.Server) {
Description: `Show or hide a window. Example: {"name":"main","visible":false}`,
}, s.windowVisibility)
addTool(s, server, &mcp.Tool{Name: "window_always_on_top", Description: "Pin a window above others"}, s.windowAlwaysOnTop)
addTool(s, server, &mcp.Tool{
Name: "window_opacity",
Description: `Set a window's opacity. Example: {"name":"main","opacity":0.85}`,
}, s.windowOpacity)
addTool(s, server, &mcp.Tool{Name: "window_background_colour", Description: "Set a window background colour"}, s.windowBackgroundColour)
addTool(s, server, &mcp.Tool{Name: "window_fullscreen", Description: "Set a window to fullscreen mode"}, s.windowFullscreen)
}

View file

@ -1,14 +1,15 @@
package window
type WindowInfo struct {
Name string `json:"name"`
Title string `json:"title"`
X int `json:"x"`
Y int `json:"y"`
Width int `json:"width"`
Height int `json:"height"`
Maximized bool `json:"maximized"`
Focused bool `json:"focused"`
Name string `json:"name"`
Title string `json:"title"`
X int `json:"x"`
Y int `json:"y"`
Width int `json:"width"`
Height int `json:"height"`
Opacity float64 `json:"opacity"`
Maximized bool `json:"maximized"`
Focused bool `json:"focused"`
}
type QueryWindowList struct{}
@ -52,6 +53,11 @@ type TaskSetAlwaysOnTop struct {
AlwaysOnTop bool
}
type TaskSetOpacity struct {
Name string
Opacity float64
}
type TaskSetBackgroundColour struct {
Name string
Red uint8

View file

@ -15,6 +15,7 @@ func (m *MockPlatform) CreateWindow(options PlatformWindowOptions) PlatformWindo
name: options.Name, title: options.Title, url: options.URL, html: options.HTML,
width: options.Width, height: options.Height,
x: options.X, y: options.Y,
opacity: 1.0,
execJSCalls: nil,
}
if options.JS != "" {
@ -42,6 +43,7 @@ type MockWindow struct {
minimised bool
fullscreened bool
zoom float64
opacity float64
contentProtection bool
flashed bool
devToolsOpen bool
@ -66,12 +68,14 @@ func (w *MockWindow) GetZoom() float64 {
}
return w.zoom
}
func (w *MockWindow) GetOpacity() float64 { return w.opacity }
func (w *MockWindow) SetTitle(title string) { w.title = title }
func (w *MockWindow) SetPosition(x, y int) { w.x = x; w.y = y }
func (w *MockWindow) SetSize(width, height int) { w.width = width; w.height = height }
func (w *MockWindow) SetBackgroundColour(r, g, b, a uint8) { w.backgroundColour = [4]uint8{r, g, b, a} }
func (w *MockWindow) SetVisibility(visible bool) { w.visible = visible }
func (w *MockWindow) SetAlwaysOnTop(alwaysOnTop bool) { w.alwaysOnTop = alwaysOnTop }
func (w *MockWindow) SetOpacity(opacity float64) { w.opacity = opacity }
func (w *MockWindow) SetBounds(x, y, width, height int) {
w.x = x
w.y = y

View file

@ -13,6 +13,7 @@ func (m *mockPlatform) CreateWindow(options PlatformWindowOptions) PlatformWindo
name: options.Name, title: options.Title, url: options.URL, html: options.HTML,
width: options.Width, height: options.Height,
x: options.X, y: options.Y,
opacity: 1.0,
}
if options.JS != "" {
w.execJSCalls = append(w.execJSCalls, options.JS)
@ -39,6 +40,7 @@ type mockWindow struct {
minimised bool
fullscreened bool
zoom float64
opacity float64
contentProtection bool
flashed bool
devToolsOpen bool
@ -63,12 +65,14 @@ func (w *mockWindow) GetZoom() float64 {
}
return w.zoom
}
func (w *mockWindow) GetOpacity() float64 { return w.opacity }
func (w *mockWindow) SetTitle(title string) { w.title = title }
func (w *mockWindow) SetPosition(x, y int) { w.x = x; w.y = y }
func (w *mockWindow) SetSize(width, height int) { w.width = width; w.height = height }
func (w *mockWindow) SetBackgroundColour(r, g, b, a uint8) { w.backgroundColour = [4]uint8{r, g, b, a} }
func (w *mockWindow) SetVisibility(visible bool) { w.visible = visible }
func (w *mockWindow) SetAlwaysOnTop(alwaysOnTop bool) { w.alwaysOnTop = alwaysOnTop }
func (w *mockWindow) SetOpacity(opacity float64) { w.opacity = opacity }
func (w *mockWindow) SetBounds(x, y, width, height int) {
w.x = x
w.y = y

View file

@ -42,6 +42,7 @@ type PlatformWindow interface {
IsMinimised() bool
GetBounds() (x, y, width, height int)
GetZoom() float64
GetOpacity() float64
// Mutations
SetTitle(title string)
@ -54,6 +55,7 @@ type PlatformWindow interface {
SetURL(url string)
SetHTML(html string)
SetZoom(magnification float64)
SetOpacity(opacity float64)
SetContentProtection(protection bool)
// Window state

View file

@ -86,6 +86,7 @@ func (s *Service) queryWindowList() []WindowInfo {
w, h := pw.Size()
result = append(result, WindowInfo{
Name: name, Title: pw.Title(), X: x, Y: y, Width: w, Height: h,
Opacity: pw.GetOpacity(),
Maximized: pw.IsMaximised(),
Focused: pw.IsFocused(),
})
@ -103,6 +104,7 @@ func (s *Service) queryWindowByName(name string) *WindowInfo {
w, h := pw.Size()
return &WindowInfo{
Name: name, Title: pw.Title(), X: x, Y: y, Width: w, Height: h,
Opacity: pw.GetOpacity(),
Maximized: pw.IsMaximised(),
Focused: pw.IsFocused(),
}
@ -153,6 +155,10 @@ func (s *Service) registerTaskActions() {
t, _ := opts.Get("task").Value.(TaskSetAlwaysOnTop)
return core.Result{Value: nil, OK: true}.New(s.taskSetAlwaysOnTop(t.Name, t.AlwaysOnTop))
})
c.Action("window.setOpacity", func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskSetOpacity)
return core.Result{Value: nil, OK: true}.New(s.taskSetOpacity(t.Name, t.Opacity))
})
c.Action("window.setBackgroundColour", func(_ context.Context, opts core.Options) core.Result {
t, _ := opts.Get("task").Value.(TaskSetBackgroundColour)
return core.Result{Value: nil, OK: true}.New(s.taskSetBackgroundColour(t.Name, t.Red, t.Green, t.Blue, t.Alpha))
@ -314,7 +320,7 @@ func (s *Service) taskOpenWindow(t TaskOpenWindow) core.Result {
}
x, y := pw.Position()
w, h := pw.Size()
info := WindowInfo{Name: pw.Name(), Title: pw.Title(), X: x, Y: y, Width: w, Height: h}
info := WindowInfo{Name: pw.Name(), Title: pw.Title(), X: x, Y: y, Width: w, Height: h, Opacity: pw.GetOpacity()}
// Attach platform event listeners that convert to IPC actions
s.trackWindow(pw)
@ -446,6 +452,21 @@ func (s *Service) taskSetAlwaysOnTop(name string, alwaysOnTop bool) error {
return nil
}
func (s *Service) taskSetOpacity(name string, opacity float64) error {
pw, ok := s.manager.Get(name)
if !ok {
return coreerr.E("window.taskSetOpacity", "window not found: "+name, nil)
}
if opacity < 0 {
opacity = 0
}
if opacity > 1 {
opacity = 1
}
pw.SetOpacity(opacity)
return nil
}
func (s *Service) taskSetBackgroundColour(name string, red, green, blue, alpha uint8) error {
pw, ok := s.manager.Get(name)
if !ok {

View file

@ -39,7 +39,7 @@ func (wp *WailsPlatform) CreateWindow(options PlatformWindowOptions) PlatformWin
BackgroundColour: application.NewRGBA(options.BackgroundColour[0], options.BackgroundColour[1], options.BackgroundColour[2], options.BackgroundColour[3]),
}
w := wp.app.Window.NewWithOptions(wOpts)
return &wailsWindow{w: w, title: options.Title}
return &wailsWindow{w: w, title: options.Title, opacity: 1.0}
}
func (wp *WailsPlatform) GetWindows() []PlatformWindow {
@ -54,10 +54,11 @@ func (wp *WailsPlatform) GetWindows() []PlatformWindow {
}
// wailsWindow wraps *application.WebviewWindow to implement PlatformWindow.
// It stores the title locally because Wails v3 does not expose a title getter.
// It stores the title and opacity locally because Wails v3 does not expose getters for both.
type wailsWindow struct {
w *application.WebviewWindow
title string
w *application.WebviewWindow
title string
opacity float64
}
func (ww *wailsWindow) Name() string { return ww.w.Name() }
@ -74,6 +75,7 @@ func (ww *wailsWindow) GetBounds() (int, int, int, int) {
return r.X, r.Y, r.Width, r.Height
}
func (ww *wailsWindow) GetZoom() float64 { return ww.w.GetZoom() }
func (ww *wailsWindow) GetOpacity() float64 { return ww.opacity }
func (ww *wailsWindow) SetTitle(title string) { ww.title = title; ww.w.SetTitle(title) }
func (ww *wailsWindow) SetPosition(x, y int) { ww.w.SetPosition(x, y) }
func (ww *wailsWindow) SetSize(width, height int) { ww.w.SetSize(width, height) }
@ -88,6 +90,10 @@ func (ww *wailsWindow) SetVisibility(visible bool) {
}
}
func (ww *wailsWindow) SetAlwaysOnTop(alwaysOnTop bool) { ww.w.SetAlwaysOnTop(alwaysOnTop) }
func (ww *wailsWindow) SetOpacity(opacity float64) {
ww.opacity = opacity
ww.w.SetOpacity(opacity)
}
func (ww *wailsWindow) SetBounds(x, y, width, height int) {
ww.w.SetBounds(application.Rect{X: x, Y: y, Width: width, Height: height})
}

View file

@ -197,6 +197,7 @@ type WebviewWindow struct {
focused bool
visible bool
alwaysOnTop bool
opacity float64
fullscreen bool
closed bool
execJSCalls []string
@ -215,6 +216,7 @@ func newWebviewWindow(options WebviewWindowOptions) *WebviewWindow {
height: options.Height,
visible: !options.Hidden,
alwaysOnTop: options.AlwaysOnTop,
opacity: 1.0,
eventHandlers: make(map[events.WindowEventType][]func(*WindowEvent)),
}
if options.JS != "" {
@ -281,6 +283,16 @@ func (w *WebviewWindow) SetAlwaysOnTop(alwaysOnTop bool) Window {
return w
}
// SetOpacity sets the whole-window opacity.
//
// w.SetOpacity(0.85)
func (w *WebviewWindow) SetOpacity(opacity float64) Window {
w.mu.Lock()
w.opacity = opacity
w.mu.Unlock()
return w
}
func (w *WebviewWindow) Maximise() Window {
w.mu.Lock()
w.maximised = true
@ -596,6 +608,15 @@ func (w *WebviewWindow) ZoomReset() Window { return w }
// z := w.GetZoom()
func (w *WebviewWindow) GetZoom() float64 { return 1.0 }
// GetOpacity returns the current window opacity.
//
// alpha := w.GetOpacity()
func (w *WebviewWindow) GetOpacity() float64 {
w.mu.RLock()
defer w.mu.RUnlock()
return w.opacity
}
// SetZoom sets the zoom magnification factor.
//
// w.SetZoom(1.5)