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:
parent
2383bcd743
commit
ad3c63f093
5 changed files with 367 additions and 0 deletions
8
pkg/window/layout.go
Normal file
8
pkg/window/layout.go
Normal 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
67
pkg/window/options.go
Normal 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
11
pkg/window/state.go
Normal 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
138
pkg/window/window.go
Normal 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
143
pkg/window/window_test.go
Normal 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)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue