From a23e265cc6320a00d5835628456e59c77a06736b Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 20:04:12 +0000 Subject: [PATCH] fix(window): normalize layout state before applying geometry --- pkg/window/service.go | 5 +++++ pkg/window/service_test.go | 38 ++++++++++++++++++++++++++++++++++++++ pkg/window/tiling.go | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/pkg/window/service.go b/pkg/window/service.go index 422ae96..0e17015 100644 --- a/pkg/window/service.go +++ b/pkg/window/service.go @@ -436,10 +436,15 @@ func (s *Service) taskRestoreLayout(name string) error { if !found { continue } + if pw.IsMaximised() || pw.IsMinimised() { + pw.Restore() + } pw.SetPosition(state.X, state.Y) pw.SetSize(state.Width, state.Height) if state.Maximized { pw.Maximise() + } else { + pw.Restore() } } return nil diff --git a/pkg/window/service_test.go b/pkg/window/service_test.go index c4987a0..73de842 100644 --- a/pkg/window/service_test.go +++ b/pkg/window/service_test.go @@ -274,6 +274,22 @@ func TestTaskTileWindows_UsesPrimaryScreenSize(t *testing.T) { assert.Equal(t, 1280, rightInfo.X) } +func TestTaskTileWindows_ResetsMaximizedState(t *testing.T) { + _, c := newTestWindowServiceWithScreen(t) + _, _, _ = c.PERFORM(TaskOpenWindow{Opts: []WindowOption{WithName("left")}}) + + _, _, _ = c.PERFORM(TaskMaximise{Name: "left"}) + _, handled, err := c.PERFORM(TaskTileWindows{Mode: "left-half", Windows: []string{"left"}}) + require.NoError(t, err) + assert.True(t, handled) + + result, _, _ := c.QUERY(QueryWindowByName{Name: "left"}) + info := result.(*WindowInfo) + assert.False(t, info.Maximized) + assert.Equal(t, 0, info.X) + assert.Equal(t, 1280, info.Width) +} + func TestTaskSetOpacity_Good(t *testing.T) { _, c := newTestWindowService(t) _, _, _ = c.PERFORM(TaskOpenWindow{Opts: []WindowOption{WithName("test")}}) @@ -336,6 +352,28 @@ func TestTaskApplyWorkflow_Good(t *testing.T) { assert.Equal(t, editor.Width, assistant.X) } +func TestTaskRestoreLayout_ClearsMaximizedState(t *testing.T) { + _, c := newTestWindowServiceWithScreen(t) + _, _, _ = c.PERFORM(TaskOpenWindow{Opts: []WindowOption{WithName("editor")}}) + _, _, _ = c.PERFORM(TaskMaximise{Name: "editor"}) + + svc := core.MustServiceFor[*Service](c, "window") + err := svc.Manager().Layout().SaveLayout("restore", map[string]WindowState{ + "editor": {X: 12, Y: 34, Width: 640, Height: 480, Maximized: false}, + }) + require.NoError(t, err) + + _, handled, err := c.PERFORM(TaskRestoreLayout{Name: "restore"}) + require.NoError(t, err) + assert.True(t, handled) + + result, _, _ := c.QUERY(QueryWindowByName{Name: "editor"}) + info := result.(*WindowInfo) + assert.False(t, info.Maximized) + assert.Equal(t, 12, info.X) + assert.Equal(t, 640, info.Width) +} + func TestTaskMaximise_Good(t *testing.T) { _, c := newTestWindowService(t) _, _, _ = c.PERFORM(TaskOpenWindow{Opts: []WindowOption{WithName("test")}}) diff --git a/pkg/window/tiling.go b/pkg/window/tiling.go index c4cc007..caa845c 100644 --- a/pkg/window/tiling.go +++ b/pkg/window/tiling.go @@ -3,6 +3,18 @@ package window import "fmt" +// normalizeWindowForLayout clears transient maximise/minimise state before +// applying a new geometry. This keeps layout helpers effective even when a +// window was previously maximised. +func normalizeWindowForLayout(pw PlatformWindow) { + if pw == nil { + return + } + if pw.IsMaximised() || pw.IsMinimised() { + pw.Restore() + } +} + // TileMode defines how windows are arranged. // Use: mode := window.TileModeLeftRight type TileMode int @@ -99,6 +111,7 @@ func (m *Manager) TileWindows(mode TileMode, names []string, screenW, screenH in case TileModeLeftRight: w := screenW / len(windows) for i, pw := range windows { + normalizeWindowForLayout(pw) pw.SetPosition(i*w, 0) pw.SetSize(w, screenH) } @@ -109,6 +122,7 @@ func (m *Manager) TileWindows(mode TileMode, names []string, screenW, screenH in } cellW := screenW / cols for i, pw := range windows { + normalizeWindowForLayout(pw) row := i / cols col := i % cols rows := (len(windows) + cols - 1) / cols @@ -118,41 +132,49 @@ func (m *Manager) TileWindows(mode TileMode, names []string, screenW, screenH in } case TileModeLeftHalf: for _, pw := range windows { + normalizeWindowForLayout(pw) pw.SetPosition(0, 0) pw.SetSize(halfW, screenH) } case TileModeRightHalf: for _, pw := range windows { + normalizeWindowForLayout(pw) pw.SetPosition(halfW, 0) pw.SetSize(halfW, screenH) } case TileModeTopHalf: for _, pw := range windows { + normalizeWindowForLayout(pw) pw.SetPosition(0, 0) pw.SetSize(screenW, halfH) } case TileModeBottomHalf: for _, pw := range windows { + normalizeWindowForLayout(pw) pw.SetPosition(0, halfH) pw.SetSize(screenW, halfH) } case TileModeTopLeft: for _, pw := range windows { + normalizeWindowForLayout(pw) pw.SetPosition(0, 0) pw.SetSize(halfW, halfH) } case TileModeTopRight: for _, pw := range windows { + normalizeWindowForLayout(pw) pw.SetPosition(halfW, 0) pw.SetSize(halfW, halfH) } case TileModeBottomLeft: for _, pw := range windows { + normalizeWindowForLayout(pw) pw.SetPosition(0, halfH) pw.SetSize(halfW, halfH) } case TileModeBottomRight: for _, pw := range windows { + normalizeWindowForLayout(pw) pw.SetPosition(halfW, halfH) pw.SetSize(halfW, halfH) } @@ -171,30 +193,39 @@ func (m *Manager) SnapWindow(name string, pos SnapPosition, screenW, screenH int switch pos { case SnapLeft: + normalizeWindowForLayout(pw) pw.SetPosition(0, 0) pw.SetSize(halfW, screenH) case SnapRight: + normalizeWindowForLayout(pw) pw.SetPosition(halfW, 0) pw.SetSize(halfW, screenH) case SnapTop: + normalizeWindowForLayout(pw) pw.SetPosition(0, 0) pw.SetSize(screenW, halfH) case SnapBottom: + normalizeWindowForLayout(pw) pw.SetPosition(0, halfH) pw.SetSize(screenW, halfH) case SnapTopLeft: + normalizeWindowForLayout(pw) pw.SetPosition(0, 0) pw.SetSize(halfW, halfH) case SnapTopRight: + normalizeWindowForLayout(pw) pw.SetPosition(halfW, 0) pw.SetSize(halfW, halfH) case SnapBottomLeft: + normalizeWindowForLayout(pw) pw.SetPosition(0, halfH) pw.SetSize(halfW, halfH) case SnapBottomRight: + normalizeWindowForLayout(pw) pw.SetPosition(halfW, halfH) pw.SetSize(halfW, halfH) case SnapCenter: + normalizeWindowForLayout(pw) cw, ch := pw.Size() pw.SetPosition((screenW-cw)/2, (screenH-ch)/2) } @@ -208,6 +239,7 @@ func (m *Manager) StackWindows(names []string, offsetX, offsetY int) error { if !ok { return fmt.Errorf("window %q not found", name) } + normalizeWindowForLayout(pw) pw.SetPosition(i*offsetX, i*offsetY) } return nil @@ -224,11 +256,13 @@ 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 { + normalizeWindowForLayout(pw) pw.SetPosition(0, 0) pw.SetSize(mainW, screenH) } if len(names) > 1 { if pw, ok := m.Get(names[1]); ok { + normalizeWindowForLayout(pw) pw.SetPosition(mainW, 0) pw.SetSize(screenW-mainW, screenH) } @@ -237,11 +271,13 @@ func (m *Manager) ApplyWorkflow(workflow WorkflowLayout, names []string, screenW // 60/40 split mainW := screenW * 60 / 100 if pw, ok := m.Get(names[0]); ok { + normalizeWindowForLayout(pw) pw.SetPosition(0, 0) pw.SetSize(mainW, screenH) } if len(names) > 1 { if pw, ok := m.Get(names[1]); ok { + normalizeWindowForLayout(pw) pw.SetPosition(mainW, 0) pw.SetSize(screenW-mainW, screenH) } @@ -249,6 +285,7 @@ func (m *Manager) ApplyWorkflow(workflow WorkflowLayout, names []string, screenW case WorkflowPresenting: // Maximise first window if pw, ok := m.Get(names[0]); ok { + normalizeWindowForLayout(pw) pw.SetPosition(0, 0) pw.SetSize(screenW, screenH) }