From ad3c63f093437299f77d6c31d1c4b28ff0778ef2 Mon Sep 17 00:00:00 2001 From: Snider Date: Fri, 13 Mar 2026 12:05:37 +0000 Subject: [PATCH] feat(window): add Window struct, options, and Manager with CRUD Co-Authored-By: Claude Opus 4.6 --- pkg/window/layout.go | 8 +++ pkg/window/options.go | 67 ++++++++++++++++++ pkg/window/state.go | 11 +++ pkg/window/window.go | 138 ++++++++++++++++++++++++++++++++++++ pkg/window/window_test.go | 143 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 367 insertions(+) create mode 100644 pkg/window/layout.go create mode 100644 pkg/window/options.go create mode 100644 pkg/window/state.go create mode 100644 pkg/window/window.go create mode 100644 pkg/window/window_test.go diff --git a/pkg/window/layout.go b/pkg/window/layout.go new file mode 100644 index 0000000..db411b0 --- /dev/null +++ b/pkg/window/layout.go @@ -0,0 +1,8 @@ +// pkg/window/layout.go +package window + +// LayoutManager persists named window arrangements. +// Full implementation in Task 4. +type LayoutManager struct{} + +func NewLayoutManager() *LayoutManager { return &LayoutManager{} } diff --git a/pkg/window/options.go b/pkg/window/options.go new file mode 100644 index 0000000..d7d5c7e --- /dev/null +++ b/pkg/window/options.go @@ -0,0 +1,67 @@ +// pkg/window/options.go +package window + +// WindowOption is a functional option applied to a Window descriptor. +type WindowOption func(*Window) error + +// ApplyOptions creates a Window and applies all options in order. +func ApplyOptions(opts ...WindowOption) (*Window, error) { + w := &Window{} + for _, opt := range opts { + if opt == nil { + continue + } + if err := opt(w); err != nil { + return nil, err + } + } + return w, nil +} + +func WithName(name string) WindowOption { + return func(w *Window) error { w.Name = name; return nil } +} + +func WithTitle(title string) WindowOption { + return func(w *Window) error { w.Title = title; return nil } +} + +func WithURL(url string) WindowOption { + return func(w *Window) error { w.URL = url; return nil } +} + +func WithSize(width, height int) WindowOption { + return func(w *Window) error { w.Width = width; w.Height = height; return nil } +} + +func WithPosition(x, y int) WindowOption { + return func(w *Window) error { w.X = x; w.Y = y; return nil } +} + +func WithMinSize(width, height int) WindowOption { + return func(w *Window) error { w.MinWidth = width; w.MinHeight = height; return nil } +} + +func WithMaxSize(width, height int) WindowOption { + return func(w *Window) error { w.MaxWidth = width; w.MaxHeight = height; return nil } +} + +func WithFrameless(frameless bool) WindowOption { + return func(w *Window) error { w.Frameless = frameless; return nil } +} + +func WithHidden(hidden bool) WindowOption { + return func(w *Window) error { w.Hidden = hidden; return nil } +} + +func WithAlwaysOnTop(alwaysOnTop bool) WindowOption { + return func(w *Window) error { w.AlwaysOnTop = alwaysOnTop; return nil } +} + +func WithBackgroundColour(r, g, b, a uint8) WindowOption { + return func(w *Window) error { w.BackgroundColour = [4]uint8{r, g, b, a}; return nil } +} + +func WithCentered(centered bool) WindowOption { + return func(w *Window) error { w.Centered = centered; return nil } +} diff --git a/pkg/window/state.go b/pkg/window/state.go new file mode 100644 index 0000000..ecb87eb --- /dev/null +++ b/pkg/window/state.go @@ -0,0 +1,11 @@ +// pkg/window/state.go +package window + +// StateManager persists window positions to disk. +// Full implementation in Task 3. +type StateManager struct{} + +func NewStateManager() *StateManager { return &StateManager{} } + +// ApplyState restores saved position/size to a Window descriptor. +func (sm *StateManager) ApplyState(w *Window) {} diff --git a/pkg/window/window.go b/pkg/window/window.go new file mode 100644 index 0000000..ebd7cbb --- /dev/null +++ b/pkg/window/window.go @@ -0,0 +1,138 @@ +// pkg/window/window.go +package window + +import ( + "fmt" + "sync" +) + +// Window is CoreGUI's own window descriptor — NOT a Wails type alias. +type Window struct { + Name string + Title string + URL string + Width, Height int + X, Y int + MinWidth, MinHeight int + MaxWidth, MaxHeight int + Frameless bool + Hidden bool + AlwaysOnTop bool + BackgroundColour [4]uint8 + DisableResize bool + EnableDragAndDrop bool + Centered bool +} + +// ToPlatformOptions converts a Window to PlatformWindowOptions for the backend. +func (w *Window) ToPlatformOptions() PlatformWindowOptions { + return PlatformWindowOptions{ + Name: w.Name, Title: w.Title, URL: w.URL, + Width: w.Width, Height: w.Height, X: w.X, Y: w.Y, + MinWidth: w.MinWidth, MinHeight: w.MinHeight, + MaxWidth: w.MaxWidth, MaxHeight: w.MaxHeight, + Frameless: w.Frameless, Hidden: w.Hidden, + AlwaysOnTop: w.AlwaysOnTop, BackgroundColour: w.BackgroundColour, + DisableResize: w.DisableResize, EnableDragAndDrop: w.EnableDragAndDrop, + Centered: w.Centered, + } +} + +// Manager manages window lifecycle through a Platform backend. +type Manager struct { + platform Platform + state *StateManager + layout *LayoutManager + windows map[string]PlatformWindow + mu sync.RWMutex +} + +// NewManager creates a window Manager with the given platform backend. +func NewManager(platform Platform) *Manager { + return &Manager{ + platform: platform, + state: NewStateManager(), + layout: NewLayoutManager(), + windows: make(map[string]PlatformWindow), + } +} + +// Open creates a window using functional options, applies saved state, and tracks it. +func (m *Manager) Open(opts ...WindowOption) (PlatformWindow, error) { + w, err := ApplyOptions(opts...) + if err != nil { + return nil, fmt.Errorf("window.Manager.Open: %w", err) + } + return m.Create(w) +} + +// Create creates a window from a Window descriptor. +func (m *Manager) Create(w *Window) (PlatformWindow, error) { + if w.Name == "" { + w.Name = "main" + } + if w.Title == "" { + w.Title = "Core" + } + if w.Width == 0 { + w.Width = 1280 + } + if w.Height == 0 { + w.Height = 800 + } + if w.URL == "" { + w.URL = "/" + } + + // Apply saved state if available + m.state.ApplyState(w) + + pw := m.platform.CreateWindow(w.ToPlatformOptions()) + + m.mu.Lock() + m.windows[w.Name] = pw + m.mu.Unlock() + + return pw, nil +} + +// Get returns a tracked window by name. +func (m *Manager) Get(name string) (PlatformWindow, bool) { + m.mu.RLock() + defer m.mu.RUnlock() + pw, ok := m.windows[name] + return pw, ok +} + +// List returns all tracked window names. +func (m *Manager) List() []string { + m.mu.RLock() + defer m.mu.RUnlock() + names := make([]string, 0, len(m.windows)) + for name := range m.windows { + names = append(names, name) + } + return names +} + +// Remove stops tracking a window by name. +func (m *Manager) Remove(name string) { + m.mu.Lock() + delete(m.windows, name) + m.mu.Unlock() +} + +// Platform returns the underlying platform for direct access. +func (m *Manager) Platform() Platform { + return m.platform +} + +// State returns the state manager for window persistence. +func (m *Manager) State() *StateManager { + return m.state +} + +// Layout returns the layout manager. +func (m *Manager) Layout() *LayoutManager { + return m.layout +} diff --git a/pkg/window/window_test.go b/pkg/window/window_test.go new file mode 100644 index 0000000..0a2e543 --- /dev/null +++ b/pkg/window/window_test.go @@ -0,0 +1,143 @@ +// pkg/window/window_test.go +package window + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWindowDefaults(t *testing.T) { + w := &Window{} + assert.Equal(t, "", w.Name) + assert.Equal(t, 0, w.Width) +} + +func TestWindowOption_Name_Good(t *testing.T) { + w := &Window{} + err := WithName("main")(w) + require.NoError(t, err) + assert.Equal(t, "main", w.Name) +} + +func TestWindowOption_Title_Good(t *testing.T) { + w := &Window{} + err := WithTitle("My App")(w) + require.NoError(t, err) + assert.Equal(t, "My App", w.Title) +} + +func TestWindowOption_URL_Good(t *testing.T) { + w := &Window{} + err := WithURL("/dashboard")(w) + require.NoError(t, err) + assert.Equal(t, "/dashboard", w.URL) +} + +func TestWindowOption_Size_Good(t *testing.T) { + w := &Window{} + err := WithSize(1280, 720)(w) + require.NoError(t, err) + assert.Equal(t, 1280, w.Width) + assert.Equal(t, 720, w.Height) +} + +func TestWindowOption_Position_Good(t *testing.T) { + w := &Window{} + err := WithPosition(100, 200)(w) + require.NoError(t, err) + assert.Equal(t, 100, w.X) + assert.Equal(t, 200, w.Y) +} + +func TestApplyOptions_Good(t *testing.T) { + w, err := ApplyOptions( + WithName("test"), + WithTitle("Test Window"), + WithURL("/test"), + WithSize(800, 600), + ) + require.NoError(t, err) + assert.Equal(t, "test", w.Name) + assert.Equal(t, "Test Window", w.Title) + assert.Equal(t, "/test", w.URL) + assert.Equal(t, 800, w.Width) + assert.Equal(t, 600, w.Height) +} + +func TestApplyOptions_Bad(t *testing.T) { + _, err := ApplyOptions(func(w *Window) error { + return assert.AnError + }) + assert.Error(t, err) +} + +func TestApplyOptions_Empty_Good(t *testing.T) { + w, err := ApplyOptions() + require.NoError(t, err) + assert.NotNil(t, w) +} + +// newTestManager creates a Manager with a mock platform for testing. +func newTestManager() (*Manager, *mockPlatform) { + p := newMockPlatform() + return NewManager(p), p +} + +func TestManager_Open_Good(t *testing.T) { + m, p := newTestManager() + pw, err := m.Open(WithName("test"), WithTitle("Test"), WithURL("/test"), WithSize(800, 600)) + require.NoError(t, err) + assert.NotNil(t, pw) + assert.Equal(t, "test", pw.Name()) + assert.Len(t, p.windows, 1) +} + +func TestManager_Open_Defaults_Good(t *testing.T) { + m, _ := newTestManager() + pw, err := m.Open() + require.NoError(t, err) + assert.Equal(t, "main", pw.Name()) + w, h := pw.Size() + assert.Equal(t, 1280, w) + assert.Equal(t, 800, h) +} + +func TestManager_Open_Bad(t *testing.T) { + m, _ := newTestManager() + _, err := m.Open(func(w *Window) error { return assert.AnError }) + assert.Error(t, err) +} + +func TestManager_Get_Good(t *testing.T) { + m, _ := newTestManager() + _, _ = m.Open(WithName("findme")) + pw, ok := m.Get("findme") + assert.True(t, ok) + assert.Equal(t, "findme", pw.Name()) +} + +func TestManager_Get_Bad(t *testing.T) { + m, _ := newTestManager() + _, ok := m.Get("nonexistent") + assert.False(t, ok) +} + +func TestManager_List_Good(t *testing.T) { + m, _ := newTestManager() + _, _ = m.Open(WithName("a")) + _, _ = m.Open(WithName("b")) + names := m.List() + assert.Len(t, names, 2) + assert.Contains(t, names, "a") + assert.Contains(t, names, "b") +} + +func TestManager_Remove_Good(t *testing.T) { + m, _ := newTestManager() + _, _ = m.Open(WithName("temp")) + m.Remove("temp") + _, ok := m.Get("temp") + assert.False(t, ok) +}