diff --git a/pkg/mcp/tools_window.go b/pkg/mcp/tools_window.go index a819a07c..7dfc4d27 100644 --- a/pkg/mcp/tools_window.go +++ b/pkg/mcp/tools_window.go @@ -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) } diff --git a/pkg/window/messages.go b/pkg/window/messages.go index 835cf491..2e9c8a47 100644 --- a/pkg/window/messages.go +++ b/pkg/window/messages.go @@ -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 diff --git a/pkg/window/mock_platform.go b/pkg/window/mock_platform.go index ec92211f..f2a16a1c 100644 --- a/pkg/window/mock_platform.go +++ b/pkg/window/mock_platform.go @@ -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 diff --git a/pkg/window/mock_test.go b/pkg/window/mock_test.go index 941709f2..3b894079 100644 --- a/pkg/window/mock_test.go +++ b/pkg/window/mock_test.go @@ -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 diff --git a/pkg/window/platform.go b/pkg/window/platform.go index 8d3b0145..7eb4f8ef 100644 --- a/pkg/window/platform.go +++ b/pkg/window/platform.go @@ -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 diff --git a/pkg/window/service.go b/pkg/window/service.go index cf49c79c..eb906915 100644 --- a/pkg/window/service.go +++ b/pkg/window/service.go @@ -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 { diff --git a/pkg/window/wails.go b/pkg/window/wails.go index 3a953439..a0ca89ab 100644 --- a/pkg/window/wails.go +++ b/pkg/window/wails.go @@ -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}) } diff --git a/stubs/wails/pkg/application/application.go b/stubs/wails/pkg/application/application.go index 410135ce..ac3c4d0c 100644 --- a/stubs/wails/pkg/application/application.go +++ b/stubs/wails/pkg/application/application.go @@ -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)