feat(window): add Window struct, options, and Manager with CRUD

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-03-13 12:05:37 +00:00
parent 2383bcd743
commit ad3c63f093
5 changed files with 367 additions and 0 deletions

8
pkg/window/layout.go Normal file
View file

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

67
pkg/window/options.go Normal file
View file

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

11
pkg/window/state.go Normal file
View file

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

138
pkg/window/window.go Normal file
View file

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

143
pkg/window/window_test.go Normal file
View file

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