diff --git a/pkg/display/FEATURES.md b/pkg/display/FEATURES.md index 7da9e0c..1683875 100644 --- a/pkg/display/FEATURES.md +++ b/pkg/display/FEATURES.md @@ -31,6 +31,7 @@ This document tracks the implementation of display server features that enable A - [x] `window_title_get` - Get current window title (returns window name) - [x] `window_always_on_top` - Pin window above others - [x] `window_background_colour` - Set window background color with alpha (transparency) +- [x] `window_opacity` - Set window opacity - [x] `window_fullscreen` - Enter/exit fullscreen mode --- @@ -235,7 +236,6 @@ This document tracks the implementation of display server features that enable A - [x] `tray_info` - Get tray status ### Phase 8 - Remaining Features (Future) -- [ ] window_opacity (true opacity if Wails adds support) - [ ] layout_beside_editor, layout_suggest - [ ] webview_devtools_open, webview_devtools_close - [ ] clipboard_read_image, clipboard_write_image diff --git a/pkg/display/display.go b/pkg/display/display.go index 2fd1a98..bfbf847 100644 --- a/pkg/display/display.go +++ b/pkg/display/display.go @@ -1168,6 +1168,15 @@ func (s *Service) SetWindowBackgroundColour(name string, r, g, b, a uint8) error return err } +// SetWindowOpacity updates a window's opacity via IPC. +func (s *Service) SetWindowOpacity(name string, opacity float32) error { + _, _, err := s.Core().PERFORM(window.TaskSetOpacity{ + Name: name, + Opacity: opacity, + }) + return err +} + // GetFocusedWindow returns the name of the currently focused window. func (s *Service) GetFocusedWindow() string { infos := s.ListWindowInfos() diff --git a/pkg/mcp/tools_window.go b/pkg/mcp/tools_window.go index b735ed0..7604ba2 100644 --- a/pkg/mcp/tools_window.go +++ b/pkg/mcp/tools_window.go @@ -356,6 +356,24 @@ func (s *Subsystem) windowBackgroundColour(_ context.Context, _ *mcp.CallToolReq return nil, WindowBackgroundColourOutput{Success: true}, nil } +// --- window_opacity --- + +type WindowOpacityInput struct { + Name string `json:"name"` + Opacity float32 `json:"opacity"` +} +type WindowOpacityOutput struct { + Success bool `json:"success"` +} + +func (s *Subsystem) windowOpacity(_ context.Context, _ *mcp.CallToolRequest, input WindowOpacityInput) (*mcp.CallToolResult, WindowOpacityOutput, error) { + _, _, err := s.core.PERFORM(window.TaskSetOpacity{Name: input.Name, Opacity: input.Opacity}) + if err != nil { + return nil, WindowOpacityOutput{}, err + } + return nil, WindowOpacityOutput{Success: true}, nil +} + // --- window_fullscreen --- type WindowFullscreenInput struct { @@ -394,5 +412,6 @@ func (s *Subsystem) registerWindowTools(server *mcp.Server) { mcp.AddTool(server, &mcp.Tool{Name: "window_visibility", Description: "Show or hide a window"}, s.windowVisibility) mcp.AddTool(server, &mcp.Tool{Name: "window_always_on_top", Description: "Pin a window above others"}, s.windowAlwaysOnTop) mcp.AddTool(server, &mcp.Tool{Name: "window_background_colour", Description: "Set a window background colour"}, s.windowBackgroundColour) + mcp.AddTool(server, &mcp.Tool{Name: "window_opacity", Description: "Set a window opacity"}, s.windowOpacity) mcp.AddTool(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 e2ed36c..755f4ee 100644 --- a/pkg/window/messages.go +++ b/pkg/window/messages.go @@ -107,6 +107,12 @@ type TaskSetBackgroundColour struct { Alpha uint8 } +// TaskSetOpacity updates the window opacity as a value between 0 and 1. +type TaskSetOpacity struct { + Name string + Opacity float32 +} + // TaskSetVisibility shows or hides a window. type TaskSetVisibility struct { Name string diff --git a/pkg/window/mock_platform.go b/pkg/window/mock_platform.go index d031f9e..293faf6 100644 --- a/pkg/window/mock_platform.go +++ b/pkg/window/mock_platform.go @@ -38,6 +38,7 @@ type MockWindow struct { focused bool visible, alwaysOnTop bool backgroundColor [4]uint8 + opacity float32 closed bool eventHandlers []func(WindowEvent) fileDropHandlers []func(paths []string, targetID string) @@ -55,6 +56,7 @@ 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.backgroundColor = [4]uint8{r, g, b, a} } +func (w *MockWindow) SetOpacity(opacity float32) { w.opacity = opacity } func (w *MockWindow) SetVisibility(visible bool) { w.visible = visible } func (w *MockWindow) SetAlwaysOnTop(alwaysOnTop bool) { w.alwaysOnTop = alwaysOnTop } func (w *MockWindow) Maximise() { w.maximised = true; w.minimised = false; w.visible = true } diff --git a/pkg/window/mock_test.go b/pkg/window/mock_test.go index 442578d..452a1b9 100644 --- a/pkg/window/mock_test.go +++ b/pkg/window/mock_test.go @@ -37,6 +37,7 @@ type mockWindow struct { focused bool visible, alwaysOnTop bool backgroundColor [4]uint8 + opacity float32 devtoolsOpen bool closed bool eventHandlers []func(WindowEvent) @@ -55,6 +56,7 @@ 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.backgroundColor = [4]uint8{r, g, b, a} } +func (w *mockWindow) SetOpacity(opacity float32) { w.opacity = opacity } func (w *mockWindow) SetVisibility(visible bool) { w.visible = visible } func (w *mockWindow) SetAlwaysOnTop(alwaysOnTop bool) { w.alwaysOnTop = alwaysOnTop } func (w *mockWindow) Maximise() { w.maximised = true; w.minimised = false; w.visible = true } diff --git a/pkg/window/platform.go b/pkg/window/platform.go index ecc1533..d277231 100644 --- a/pkg/window/platform.go +++ b/pkg/window/platform.go @@ -43,6 +43,7 @@ type PlatformWindow interface { SetPosition(x, y int) SetSize(width, height int) SetBackgroundColour(r, g, b, a uint8) + SetOpacity(opacity float32) SetVisibility(visible bool) SetAlwaysOnTop(alwaysOnTop bool) diff --git a/pkg/window/service.go b/pkg/window/service.go index f52c710..c2e53f3 100644 --- a/pkg/window/service.go +++ b/pkg/window/service.go @@ -165,6 +165,8 @@ func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) { return nil, true, s.taskSetAlwaysOnTop(t.Name, t.AlwaysOnTop) case TaskSetBackgroundColour: return nil, true, s.taskSetBackgroundColour(t.Name, t.Red, t.Green, t.Blue, t.Alpha) + case TaskSetOpacity: + return nil, true, s.taskSetOpacity(t.Name, t.Opacity) case TaskSetVisibility: return nil, true, s.taskSetVisibility(t.Name, t.Visible) case TaskFullscreen: @@ -371,6 +373,18 @@ func (s *Service) taskSetBackgroundColour(name string, red, green, blue, alpha u return nil } +func (s *Service) taskSetOpacity(name string, opacity float32) error { + if opacity < 0 || opacity > 1 { + return fmt.Errorf("opacity must be between 0 and 1") + } + pw, ok := s.manager.Get(name) + if !ok { + return fmt.Errorf("window not found: %s", name) + } + pw.SetOpacity(opacity) + return nil +} + func (s *Service) taskSetVisibility(name string, visible bool) error { pw, ok := s.manager.Get(name) if !ok { diff --git a/pkg/window/service_test.go b/pkg/window/service_test.go index 08d3f4d..f14ce9a 100644 --- a/pkg/window/service_test.go +++ b/pkg/window/service_test.go @@ -218,6 +218,29 @@ func TestTaskSetBackgroundColour_Good(t *testing.T) { assert.Equal(t, [4]uint8{10, 20, 30, 40}, pw.(*mockWindow).backgroundColor) } +func TestTaskSetOpacity_Good(t *testing.T) { + _, c := newTestWindowService(t) + _, _, _ = c.PERFORM(TaskOpenWindow{Opts: []WindowOption{WithName("test")}}) + + _, handled, err := c.PERFORM(TaskSetOpacity{Name: "test", Opacity: 0.65}) + require.NoError(t, err) + assert.True(t, handled) + + svc := core.MustServiceFor[*Service](c, "window") + pw, ok := svc.Manager().Get("test") + require.True(t, ok) + assert.InDelta(t, 0.65, pw.(*mockWindow).opacity, 0.0001) +} + +func TestTaskSetOpacity_BadRange(t *testing.T) { + _, c := newTestWindowService(t) + _, _, _ = c.PERFORM(TaskOpenWindow{Opts: []WindowOption{WithName("test")}}) + + _, handled, err := c.PERFORM(TaskSetOpacity{Name: "test", Opacity: 1.5}) + require.Error(t, err) + assert.True(t, handled) +} + func TestTaskStackWindows_Good(t *testing.T) { _, c := newTestWindowService(t) _, _, _ = c.PERFORM(TaskOpenWindow{Opts: []WindowOption{WithName("one")}}) diff --git a/pkg/window/wails.go b/pkg/window/wails.go index 4a10646..b899d7b 100644 --- a/pkg/window/wails.go +++ b/pkg/window/wails.go @@ -72,6 +72,7 @@ func (ww *wailsWindow) SetSize(width, height int) { ww.w.SetSize(width, height) func (ww *wailsWindow) SetBackgroundColour(r, g, b, a uint8) { ww.w.SetBackgroundColour(application.NewRGBA(r, g, b, a)) } +func (ww *wailsWindow) SetOpacity(opacity float32) { ww.w.SetOpacity(opacity) } func (ww *wailsWindow) SetVisibility(visible bool) { if visible { ww.w.Show() diff --git a/stubs/wails/v3/pkg/application/application.go b/stubs/wails/v3/pkg/application/application.go index c1969ae..54c7c14 100644 --- a/stubs/wails/v3/pkg/application/application.go +++ b/stubs/wails/v3/pkg/application/application.go @@ -127,6 +127,7 @@ func (w *WebviewWindow) SetSize(width, height int) { w.width, w.height = width, height } func (w *WebviewWindow) SetBackgroundColour(colour RGBA) {} +func (w *WebviewWindow) SetOpacity(opacity float32) {} func (w *WebviewWindow) SetVisibility(visible bool) { w.visible = visible } func (w *WebviewWindow) SetAlwaysOnTop(alwaysOnTop bool) { w.alwaysOnTop = alwaysOnTop } func (w *WebviewWindow) Maximise() { w.maximised = true; w.minimised = false; w.visible = true }