fix(window): normalize layout state before applying geometry
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run

This commit is contained in:
Virgil 2026-04-02 20:04:12 +00:00
parent 3cf69533bf
commit a23e265cc6
3 changed files with 80 additions and 0 deletions

View file

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

View file

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

View file

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