feat(window): add window opacity support
Some checks failed
Test / test (push) Waiting to run
Security Scan / security (push) Failing after 35s

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 14:42:03 +00:00
parent 45fa6942f7
commit 4f4a4eb8e4
11 changed files with 79 additions and 1 deletions

View file

@ -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

View file

@ -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()

View file

@ -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)
}

View file

@ -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

View file

@ -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 }

View file

@ -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 }

View file

@ -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)

View file

@ -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 {

View file

@ -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")}})

View file

@ -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()

View file

@ -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 }