diff --git a/pkg/window/tiling.go b/pkg/window/tiling.go new file mode 100644 index 0000000..40669fe --- /dev/null +++ b/pkg/window/tiling.go @@ -0,0 +1,241 @@ +// pkg/window/tiling.go +package window + +import "fmt" + +// TileMode defines how windows are arranged. +type TileMode int + +const ( + TileModeLeftHalf TileMode = iota + TileModeRightHalf + TileModeTopHalf + TileModeBottomHalf + TileModeTopLeft + TileModeTopRight + TileModeBottomLeft + TileModeBottomRight + TileModeLeftRight + TileModeGrid +) + +var tileModeNames = map[TileMode]string{ + TileModeLeftHalf: "left-half", TileModeRightHalf: "right-half", + TileModeTopHalf: "top-half", TileModeBottomHalf: "bottom-half", + TileModeTopLeft: "top-left", TileModeTopRight: "top-right", + TileModeBottomLeft: "bottom-left", TileModeBottomRight: "bottom-right", + TileModeLeftRight: "left-right", TileModeGrid: "grid", +} + +func (m TileMode) String() string { return tileModeNames[m] } + +// SnapPosition defines where a window snaps to. +type SnapPosition int + +const ( + SnapLeft SnapPosition = iota + SnapRight + SnapTop + SnapBottom + SnapTopLeft + SnapTopRight + SnapBottomLeft + SnapBottomRight + SnapCenter +) + +// WorkflowLayout is a predefined arrangement for common tasks. +type WorkflowLayout int + +const ( + WorkflowCoding WorkflowLayout = iota // 70/30 split + WorkflowDebugging // 60/40 split + WorkflowPresenting // maximised + WorkflowSideBySide // 50/50 split +) + +var workflowNames = map[WorkflowLayout]string{ + WorkflowCoding: "coding", WorkflowDebugging: "debugging", + WorkflowPresenting: "presenting", WorkflowSideBySide: "side-by-side", +} + +func (w WorkflowLayout) String() string { return workflowNames[w] } + +// TileWindows arranges the named windows in the given mode across the screen area. +func (m *Manager) TileWindows(mode TileMode, names []string, screenW, screenH int) error { + windows := make([]PlatformWindow, 0, len(names)) + for _, name := range names { + pw, ok := m.Get(name) + if !ok { + return fmt.Errorf("window %q not found", name) + } + windows = append(windows, pw) + } + if len(windows) == 0 { + return fmt.Errorf("no windows to tile") + } + + halfW, halfH := screenW/2, screenH/2 + + switch mode { + case TileModeLeftRight: + w := screenW / len(windows) + for i, pw := range windows { + pw.SetPosition(i*w, 0) + pw.SetSize(w, screenH) + } + case TileModeGrid: + cols := 2 + if len(windows) > 4 { + cols = 3 + } + cellW := screenW / cols + for i, pw := range windows { + row := i / cols + col := i % cols + rows := (len(windows) + cols - 1) / cols + cellH := screenH / rows + pw.SetPosition(col*cellW, row*cellH) + pw.SetSize(cellW, cellH) + } + case TileModeLeftHalf: + for _, pw := range windows { + pw.SetPosition(0, 0) + pw.SetSize(halfW, screenH) + } + case TileModeRightHalf: + for _, pw := range windows { + pw.SetPosition(halfW, 0) + pw.SetSize(halfW, screenH) + } + case TileModeTopHalf: + for _, pw := range windows { + pw.SetPosition(0, 0) + pw.SetSize(screenW, halfH) + } + case TileModeBottomHalf: + for _, pw := range windows { + pw.SetPosition(0, halfH) + pw.SetSize(screenW, halfH) + } + case TileModeTopLeft: + for _, pw := range windows { + pw.SetPosition(0, 0) + pw.SetSize(halfW, halfH) + } + case TileModeTopRight: + for _, pw := range windows { + pw.SetPosition(halfW, 0) + pw.SetSize(halfW, halfH) + } + case TileModeBottomLeft: + for _, pw := range windows { + pw.SetPosition(0, halfH) + pw.SetSize(halfW, halfH) + } + case TileModeBottomRight: + for _, pw := range windows { + pw.SetPosition(halfW, halfH) + pw.SetSize(halfW, halfH) + } + } + return nil +} + +// SnapWindow snaps a window to a screen edge/corner/centre. +func (m *Manager) SnapWindow(name string, pos SnapPosition, screenW, screenH int) error { + pw, ok := m.Get(name) + if !ok { + return fmt.Errorf("window %q not found", name) + } + + halfW, halfH := screenW/2, screenH/2 + + switch pos { + case SnapLeft: + pw.SetPosition(0, 0) + pw.SetSize(halfW, screenH) + case SnapRight: + pw.SetPosition(halfW, 0) + pw.SetSize(halfW, screenH) + case SnapTop: + pw.SetPosition(0, 0) + pw.SetSize(screenW, halfH) + case SnapBottom: + pw.SetPosition(0, halfH) + pw.SetSize(screenW, halfH) + case SnapTopLeft: + pw.SetPosition(0, 0) + pw.SetSize(halfW, halfH) + case SnapTopRight: + pw.SetPosition(halfW, 0) + pw.SetSize(halfW, halfH) + case SnapBottomLeft: + pw.SetPosition(0, halfH) + pw.SetSize(halfW, halfH) + case SnapBottomRight: + pw.SetPosition(halfW, halfH) + pw.SetSize(halfW, halfH) + case SnapCenter: + cw, ch := pw.Size() + pw.SetPosition((screenW-cw)/2, (screenH-ch)/2) + } + return nil +} + +// StackWindows cascades windows with an offset. +func (m *Manager) StackWindows(names []string, offsetX, offsetY int) error { + for i, name := range names { + pw, ok := m.Get(name) + if !ok { + return fmt.Errorf("window %q not found", name) + } + pw.SetPosition(i*offsetX, i*offsetY) + } + return nil +} + +// ApplyWorkflow arranges windows in a predefined workflow layout. +func (m *Manager) ApplyWorkflow(workflow WorkflowLayout, names []string, screenW, screenH int) error { + if len(names) == 0 { + return fmt.Errorf("no windows for workflow") + } + + switch workflow { + case WorkflowCoding: + // 70/30 split — main editor + terminal + mainW := screenW * 70 / 100 + if pw, ok := m.Get(names[0]); ok { + pw.SetPosition(0, 0) + pw.SetSize(mainW, screenH) + } + if len(names) > 1 { + if pw, ok := m.Get(names[1]); ok { + pw.SetPosition(mainW, 0) + pw.SetSize(screenW-mainW, screenH) + } + } + case WorkflowDebugging: + // 60/40 split + mainW := screenW * 60 / 100 + if pw, ok := m.Get(names[0]); ok { + pw.SetPosition(0, 0) + pw.SetSize(mainW, screenH) + } + if len(names) > 1 { + if pw, ok := m.Get(names[1]); ok { + pw.SetPosition(mainW, 0) + pw.SetSize(screenW-mainW, screenH) + } + } + case WorkflowPresenting: + // Maximise first window + if pw, ok := m.Get(names[0]); ok { + pw.SetPosition(0, 0) + pw.SetSize(screenW, screenH) + } + case WorkflowSideBySide: + return m.TileWindows(TileModeLeftRight, names, screenW, screenH) + } + return nil +} diff --git a/pkg/window/window_test.go b/pkg/window/window_test.go index 7e0bc50..44d1f09 100644 --- a/pkg/window/window_test.go +++ b/pkg/window/window_test.go @@ -79,10 +79,16 @@ func TestApplyOptions_Empty_Good(t *testing.T) { assert.NotNil(t, w) } -// newTestManager creates a Manager with a mock platform for testing. +// newTestManager creates a Manager with a mock platform and clean state for testing. func newTestManager() (*Manager, *mockPlatform) { p := newMockPlatform() - return NewManager(p), p + m := &Manager{ + platform: p, + state: &StateManager{states: make(map[string]WindowState)}, + layout: &LayoutManager{layouts: make(map[string]Layout)}, + windows: make(map[string]PlatformWindow), + } + return m, p } func TestManager_Open_Good(t *testing.T) { @@ -266,3 +272,59 @@ func TestLayoutManager_DeleteLayout_Good(t *testing.T) { _, ok := lm.GetLayout("temp") assert.False(t, ok) } + +// --- Tiling Tests --- + +func TestTileMode_String_Good(t *testing.T) { + assert.Equal(t, "left-half", TileModeLeftHalf.String()) + assert.Equal(t, "grid", TileModeGrid.String()) +} + +func TestManager_TileWindows_Good(t *testing.T) { + m, _ := newTestManager() + _, _ = m.Open(WithName("a"), WithSize(800, 600)) + _, _ = m.Open(WithName("b"), WithSize(800, 600)) + err := m.TileWindows(TileModeLeftRight, []string{"a", "b"}, 1920, 1080) + require.NoError(t, err) + a, _ := m.Get("a") + b, _ := m.Get("b") + aw, _ := a.Size() + bw, _ := b.Size() + assert.Equal(t, 960, aw) + assert.Equal(t, 960, bw) +} + +func TestManager_TileWindows_Bad(t *testing.T) { + m, _ := newTestManager() + err := m.TileWindows(TileModeLeftRight, []string{"nonexistent"}, 1920, 1080) + assert.Error(t, err) +} + +func TestManager_SnapWindow_Good(t *testing.T) { + m, _ := newTestManager() + _, _ = m.Open(WithName("snap"), WithSize(800, 600)) + err := m.SnapWindow("snap", SnapLeft, 1920, 1080) + require.NoError(t, err) + w, _ := m.Get("snap") + x, _ := w.Position() + assert.Equal(t, 0, x) + sw, _ := w.Size() + assert.Equal(t, 960, sw) +} + +func TestManager_StackWindows_Good(t *testing.T) { + m, _ := newTestManager() + _, _ = m.Open(WithName("s1"), WithSize(800, 600)) + _, _ = m.Open(WithName("s2"), WithSize(800, 600)) + err := m.StackWindows([]string{"s1", "s2"}, 30, 30) + require.NoError(t, err) + s2, _ := m.Get("s2") + x, y := s2.Position() + assert.Equal(t, 30, x) + assert.Equal(t, 30, y) +} + +func TestWorkflowLayout_Good(t *testing.T) { + assert.Equal(t, "coding", WorkflowCoding.String()) + assert.Equal(t, "debugging", WorkflowDebugging.String()) +}