diff --git a/pkg/window/mock_platform.go b/pkg/window/mock_platform.go index 5322996..a12f5e6 100644 --- a/pkg/window/mock_platform.go +++ b/pkg/window/mock_platform.go @@ -39,13 +39,16 @@ type MockWindow struct { fileDropHandlers []func(paths []string, targetID string) } -func (w *MockWindow) Name() string { return w.name } -func (w *MockWindow) Title() string { return w.title } -func (w *MockWindow) Position() (int, int) { return w.x, w.y } -func (w *MockWindow) Size() (int, int) { return w.width, w.height } -func (w *MockWindow) IsMaximised() bool { return w.maximised } -func (w *MockWindow) IsFocused() bool { return w.focused } -func (w *MockWindow) SetTitle(title string) { w.title = title } +func (w *MockWindow) Name() string { return w.name } +func (w *MockWindow) Title() string { return w.title } +func (w *MockWindow) Position() (int, int) { return w.x, w.y } +func (w *MockWindow) Size() (int, int) { return w.width, w.height } +func (w *MockWindow) IsMaximised() bool { return w.maximised } +func (w *MockWindow) IsFocused() bool { return w.focused } +func (w *MockWindow) SetTitle(title string) { w.title = title } +func (w *MockWindow) SetBounds(x, y, width, height int) { + w.x, w.y, w.width, w.height = x, y, width, height +} 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} } @@ -61,10 +64,10 @@ func (w *MockWindow) Close() { handler(WindowEvent{Type: "close", Name: w.name}) } } -func (w *MockWindow) Show() { w.visible = true } -func (w *MockWindow) Hide() { w.visible = false } -func (w *MockWindow) Fullscreen() {} -func (w *MockWindow) UnFullscreen() {} +func (w *MockWindow) Show() { w.visible = true } +func (w *MockWindow) Hide() { w.visible = false } +func (w *MockWindow) Fullscreen() {} +func (w *MockWindow) UnFullscreen() {} func (w *MockWindow) OnWindowEvent(handler func(WindowEvent)) { w.eventHandlers = append(w.eventHandlers, handler) } diff --git a/pkg/window/mock_test.go b/pkg/window/mock_test.go index 4b79fba..6bd3186 100644 --- a/pkg/window/mock_test.go +++ b/pkg/window/mock_test.go @@ -39,13 +39,16 @@ type mockWindow struct { fileDropHandlers []func(paths []string, targetID string) } -func (w *mockWindow) Name() string { return w.name } -func (w *mockWindow) Title() string { return w.title } -func (w *mockWindow) Position() (int, int) { return w.x, w.y } -func (w *mockWindow) Size() (int, int) { return w.width, w.height } -func (w *mockWindow) IsMaximised() bool { return w.maximised } -func (w *mockWindow) IsFocused() bool { return w.focused } -func (w *mockWindow) SetTitle(title string) { w.title = title } +func (w *mockWindow) Name() string { return w.name } +func (w *mockWindow) Title() string { return w.title } +func (w *mockWindow) Position() (int, int) { return w.x, w.y } +func (w *mockWindow) Size() (int, int) { return w.width, w.height } +func (w *mockWindow) IsMaximised() bool { return w.maximised } +func (w *mockWindow) IsFocused() bool { return w.focused } +func (w *mockWindow) SetTitle(title string) { w.title = title } +func (w *mockWindow) SetBounds(x, y, width, height int) { + w.x, w.y, w.width, w.height = x, y, width, height +} 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} } @@ -59,10 +62,10 @@ func (w *mockWindow) Close() { w.closed = true w.emit(WindowEvent{Type: "close", Name: w.name}) } -func (w *mockWindow) Show() { w.visible = true } -func (w *mockWindow) Hide() { w.visible = false } -func (w *mockWindow) Fullscreen() { w.fullscreened = true } -func (w *mockWindow) UnFullscreen() { w.fullscreened = false } +func (w *mockWindow) Show() { w.visible = true } +func (w *mockWindow) Hide() { w.visible = false } +func (w *mockWindow) Fullscreen() { w.fullscreened = true } +func (w *mockWindow) UnFullscreen() { w.fullscreened = false } func (w *mockWindow) OnWindowEvent(handler func(WindowEvent)) { w.eventHandlers = append(w.eventHandlers, handler) } diff --git a/pkg/window/persistence_test.go b/pkg/window/persistence_test.go index abdde65..d03442a 100644 --- a/pkg/window/persistence_test.go +++ b/pkg/window/persistence_test.go @@ -113,6 +113,33 @@ func TestStateManager_CaptureState_Good(t *testing.T) { assert.NotZero(t, got.UpdatedAt) } +func TestStateManager_CaptureState_PreservesMetadata_Good(t *testing.T) { + sm := NewStateManagerWithDir(t.TempDir()) + sm.SetState("captured", WindowState{ + Screen: "primary", + URL: "/app", + Width: 640, + Height: 480, + }) + + pw := &mockWindow{ + name: "captured", x: 75, y: 125, + width: 1440, height: 900, maximised: true, + } + + sm.CaptureState(pw) + + got, ok := sm.GetState("captured") + require.True(t, ok) + assert.Equal(t, "primary", got.Screen) + assert.Equal(t, "/app", got.URL) + assert.Equal(t, 75, got.X) + assert.Equal(t, 125, got.Y) + assert.Equal(t, 1440, got.Width) + assert.Equal(t, 900, got.Height) + assert.True(t, got.Maximized) +} + func TestStateManager_ApplyState_Good(t *testing.T) { sm := NewStateManagerWithDir(t.TempDir()) sm.SetState("target", WindowState{X: 55, Y: 65, Width: 700, Height: 500}) diff --git a/pkg/window/platform.go b/pkg/window/platform.go index c0e56a9..9974a53 100644 --- a/pkg/window/platform.go +++ b/pkg/window/platform.go @@ -38,6 +38,7 @@ type PlatformWindow interface { // Mutations SetTitle(title string) + SetBounds(x, y, width, height int) SetPosition(x, y int) SetSize(width, height int) SetBackgroundColour(r, g, b, a uint8) diff --git a/pkg/window/service.go b/pkg/window/service.go index 6e83f27..c777ddf 100644 --- a/pkg/window/service.go +++ b/pkg/window/service.go @@ -289,8 +289,7 @@ func (s *Service) taskSetBounds(name string, x, y, width, height int) error { if err != nil { return err } - platformWindow.SetPosition(x, y) - platformWindow.SetSize(width, height) + platformWindow.SetBounds(x, y, width, height) s.manager.State().UpdateBounds(name, x, y, width, height) return nil } @@ -414,8 +413,7 @@ func (s *Service) taskRestoreLayout(name string) error { if !found { continue } - platformWindow.SetPosition(state.X, state.Y) - platformWindow.SetSize(state.Width, state.Height) + platformWindow.SetBounds(state.X, state.Y, state.Width, state.Height) if state.Maximized { platformWindow.Maximise() } else { diff --git a/pkg/window/service_test.go b/pkg/window/service_test.go index 3a50781..1bf9007 100644 --- a/pkg/window/service_test.go +++ b/pkg/window/service_test.go @@ -178,8 +178,12 @@ func TestTaskSetSize_Good(t *testing.T) { } func TestTaskSetBounds_Good(t *testing.T) { - _, c := newTestWindowService(t) + svc, c := newTestWindowService(t) _, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "test"}}) + svc.Manager().State().SetState("test", WindowState{ + Screen: "primary", + URL: "/app", + }) _, handled, err := c.PERFORM(TaskSetBounds{ Name: "test", @@ -205,6 +209,8 @@ func TestTaskSetBounds_Good(t *testing.T) { assert.Equal(t, 200, saved["test"].Y) assert.Equal(t, 800, saved["test"].Width) assert.Equal(t, 600, saved["test"].Height) + assert.Equal(t, "primary", saved["test"].Screen) + assert.Equal(t, "/app", saved["test"].URL) } func TestTaskMaximize_Good(t *testing.T) { diff --git a/pkg/window/state.go b/pkg/window/state.go index da72f56..de838d2 100644 --- a/pkg/window/state.go +++ b/pkg/window/state.go @@ -187,11 +187,18 @@ func (sm *StateManager) UpdateMaximized(name string, maximized bool) { // CaptureState snapshots the current state from a PlatformWindow. func (sm *StateManager) CaptureState(pw PlatformWindow) { + if pw == nil { + return + } x, y := pw.Position() w, h := pw.Size() - sm.SetState(pw.Name(), WindowState{ - X: x, Y: y, Width: w, Height: h, - Maximized: pw.IsMaximised(), + name := pw.Name() + sm.updateState(name, func(state *WindowState) { + state.X = x + state.Y = y + state.Width = w + state.Height = h + state.Maximized = pw.IsMaximised() }) } diff --git a/pkg/window/tiling.go b/pkg/window/tiling.go index 28a073b..fa88f56 100644 --- a/pkg/window/tiling.go +++ b/pkg/window/tiling.go @@ -109,8 +109,7 @@ func (m *Manager) TileWindows(mode TileMode, names []string, screenW, screenH in case TileModeLeftRight: w := screenW / len(windows) for i, pw := range windows { - pw.SetPosition(originX+i*w, originY) - pw.SetSize(w, screenH) + pw.SetBounds(originX+i*w, originY, w, screenH) m.captureState(pw) } case TileModeGrid: @@ -124,56 +123,47 @@ func (m *Manager) TileWindows(mode TileMode, names []string, screenW, screenH in col := i % cols rows := (len(windows) + cols - 1) / cols cellH := screenH / rows - pw.SetPosition(originX+col*cellW, originY+row*cellH) - pw.SetSize(cellW, cellH) + pw.SetBounds(originX+col*cellW, originY+row*cellH, cellW, cellH) m.captureState(pw) } case TileModeLeftHalf: for _, pw := range windows { - pw.SetPosition(originX, originY) - pw.SetSize(halfW, screenH) + pw.SetBounds(originX, originY, halfW, screenH) m.captureState(pw) } case TileModeRightHalf: for _, pw := range windows { - pw.SetPosition(originX+halfW, originY) - pw.SetSize(halfW, screenH) + pw.SetBounds(originX+halfW, originY, halfW, screenH) m.captureState(pw) } case TileModeTopHalf: for _, pw := range windows { - pw.SetPosition(originX, originY) - pw.SetSize(screenW, halfH) + pw.SetBounds(originX, originY, screenW, halfH) m.captureState(pw) } case TileModeBottomHalf: for _, pw := range windows { - pw.SetPosition(originX, originY+halfH) - pw.SetSize(screenW, halfH) + pw.SetBounds(originX, originY+halfH, screenW, halfH) m.captureState(pw) } case TileModeTopLeft: for _, pw := range windows { - pw.SetPosition(originX, originY) - pw.SetSize(halfW, halfH) + pw.SetBounds(originX, originY, halfW, halfH) m.captureState(pw) } case TileModeTopRight: for _, pw := range windows { - pw.SetPosition(originX+halfW, originY) - pw.SetSize(halfW, halfH) + pw.SetBounds(originX+halfW, originY, halfW, halfH) m.captureState(pw) } case TileModeBottomLeft: for _, pw := range windows { - pw.SetPosition(originX, originY+halfH) - pw.SetSize(halfW, halfH) + pw.SetBounds(originX, originY+halfH, halfW, halfH) m.captureState(pw) } case TileModeBottomRight: for _, pw := range windows { - pw.SetPosition(originX+halfW, originY+halfH) - pw.SetSize(halfW, halfH) + pw.SetBounds(originX+halfW, originY+halfH, halfW, halfH) m.captureState(pw) } } @@ -192,32 +182,24 @@ func (m *Manager) SnapWindow(name string, pos SnapPosition, screenW, screenH int switch pos { case SnapLeft: - pw.SetPosition(originX, originY) - pw.SetSize(halfW, screenH) + pw.SetBounds(originX, originY, halfW, screenH) case SnapRight: - pw.SetPosition(originX+halfW, originY) - pw.SetSize(halfW, screenH) + pw.SetBounds(originX+halfW, originY, halfW, screenH) case SnapTop: - pw.SetPosition(originX, originY) - pw.SetSize(screenW, halfH) + pw.SetBounds(originX, originY, screenW, halfH) case SnapBottom: - pw.SetPosition(originX, originY+halfH) - pw.SetSize(screenW, halfH) + pw.SetBounds(originX, originY+halfH, screenW, halfH) case SnapTopLeft: - pw.SetPosition(originX, originY) - pw.SetSize(halfW, halfH) + pw.SetBounds(originX, originY, halfW, halfH) case SnapTopRight: - pw.SetPosition(originX+halfW, originY) - pw.SetSize(halfW, halfH) + pw.SetBounds(originX+halfW, originY, halfW, halfH) case SnapBottomLeft: - pw.SetPosition(originX, originY+halfH) - pw.SetSize(halfW, halfH) + pw.SetBounds(originX, originY+halfH, halfW, halfH) case SnapBottomRight: - pw.SetPosition(originX+halfW, originY+halfH) - pw.SetSize(halfW, halfH) + pw.SetBounds(originX+halfW, originY+halfH, halfW, halfH) case SnapCenter: cw, ch := pw.Size() - pw.SetPosition(originX+(screenW-cw)/2, originY+(screenH-ch)/2) + pw.SetBounds(originX+(screenW-cw)/2, originY+(screenH-ch)/2, cw, ch) } m.captureState(pw) return nil @@ -249,14 +231,12 @@ func (m *Manager) ApplyWorkflow(workflow WorkflowLayout, names []string, screenW // 70/30 split — main editor + terminal mainW := screenW * 70 / 100 if pw, ok := m.Get(names[0]); ok { - pw.SetPosition(originX, originY) - pw.SetSize(mainW, screenH) + pw.SetBounds(originX, originY, mainW, screenH) m.captureState(pw) } if len(names) > 1 { if pw, ok := m.Get(names[1]); ok { - pw.SetPosition(originX+mainW, originY) - pw.SetSize(screenW-mainW, screenH) + pw.SetBounds(originX+mainW, originY, screenW-mainW, screenH) m.captureState(pw) } } @@ -264,22 +244,19 @@ func (m *Manager) ApplyWorkflow(workflow WorkflowLayout, names []string, screenW // 60/40 split mainW := screenW * 60 / 100 if pw, ok := m.Get(names[0]); ok { - pw.SetPosition(originX, originY) - pw.SetSize(mainW, screenH) + pw.SetBounds(originX, originY, mainW, screenH) m.captureState(pw) } if len(names) > 1 { if pw, ok := m.Get(names[1]); ok { - pw.SetPosition(originX+mainW, originY) - pw.SetSize(screenW-mainW, screenH) + pw.SetBounds(originX+mainW, originY, screenW-mainW, screenH) m.captureState(pw) } } case WorkflowPresenting: // Maximize first window if pw, ok := m.Get(names[0]); ok { - pw.SetPosition(originX, originY) - pw.SetSize(screenW, screenH) + pw.SetBounds(originX, originY, screenW, screenH) m.captureState(pw) } case WorkflowSideBySide: diff --git a/pkg/window/wails.go b/pkg/window/wails.go index 3beb7e0..01a6aa4 100644 --- a/pkg/window/wails.go +++ b/pkg/window/wails.go @@ -68,6 +68,10 @@ func (windowHandle *wailsWindow) SetTitle(title string) { windowHandle.title = title windowHandle.w.SetTitle(title) } +func (windowHandle *wailsWindow) SetBounds(x, y, width, height int) { + windowHandle.w.SetPosition(x, y) + windowHandle.w.SetSize(width, height) +} func (windowHandle *wailsWindow) SetPosition(x, y int) { windowHandle.w.SetPosition(x, y) } func (windowHandle *wailsWindow) SetSize(width, height int) { windowHandle.w.SetSize(width, height) } func (windowHandle *wailsWindow) SetBackgroundColour(r, g, b, a uint8) {