refactor(ax): make bounds updates declarative
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:
parent
dc53b04d2a
commit
53a4114224
9 changed files with 103 additions and 77 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue