refactor(ax): make bounds updates declarative
Some checks failed
Security Scan / security (push) Failing after 29s
Test / test (push) Successful in 1m23s

Preserve window metadata during state capture and route compound bounds updates through a single window abstraction.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-03-31 08:40:24 +00:00
parent dc53b04d2a
commit 53a4114224
9 changed files with 103 additions and 77 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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