下面是基于golang的ebiten引擎设计的植物大战僵尸游戏 的UI模块的base模块,请全面评估实现是否合理?有什么需要优化的?最后以同样的格式输出调整后的内容,注意不要简化代码的输出。
```markdown
# File: interactive_test.go
```go
package base
import (
"testing"
"github.com/hajimehoshi/ebiten/v2"
)
func TestInteractiveComponent(t *testing.T) {
t.Run("MouseEvents", func(t *testing.T) {
comp := NewInteractiveComponent()
bounds := NewBounds(10, 10, 100, 100)
comp.SetBounds(bounds)
// Test mouse enter
mouseEntered := false
comp.OnMouseEnter(EventHandlerFunc(func(e *Event) bool {
mouseEntered = true
return true
}))
event := NewMouseEvent(EventTypeMouseMove, comp, 20, 20, ebiten.MouseButtonLeft)
comp.HandleMouseEvent(event)
if !mouseEntered {
t.Error("Mouse enter event not triggered")
}
// Test mouse leave
mouseLeft := false
comp.OnMouseLeave(EventHandlerFunc(func(e *Event) bool {
mouseLeft = true
return true
}))
event = NewMouseEvent(EventTypeMouseMove, comp, 200, 200, ebiten.MouseButtonLeft)
comp.HandleMouseEvent(event)
if !mouseLeft {
t.Error("Mouse leave event not triggered")
}
// Test mouse click
clicked := false
comp.OnMouseClick(EventHandlerFunc(func(e *Event) bool {
clicked = true
return true
}))
event = NewMouseEvent(EventTypeMouseMove, comp, 20, 20, ebiten.MouseButtonLeft)
comp.HandleMouseEvent(event)
event = NewMouseEvent(EventTypeMouseButtonDown, comp, 20, 20, ebiten.MouseButtonLeft)
comp.HandleMouseEvent(event)
event = NewMouseEvent(EventTypeMouseButtonUp, comp, 20, 20, ebiten.MouseButtonLeft)
comp.HandleMouseEvent(event)
event = NewMouseEvent(EventTypeClick, comp, 20, 20, ebiten.MouseButtonLeft)
comp.HandleMouseEvent(event)
if !clicked {
t.Error("Mouse click event not triggered")
}
})
t.Run("KeyboardEvents", func(t *testing.T) {
comp := NewInteractiveComponent()
comp.Focus()
// Test key press
keyPressed := false
comp.OnKeyPress(EventHandlerFunc(func(e *Event) bool {
keyPressed = true
return true
}))
comp.HandleKeyPress(ebiten.KeySpace)
if !keyPressed {
t.Error("Key press event not triggered")
}
})
t.Run("Focus", func(t *testing.T) {
comp := NewInteractiveComponent()
// Test focus gain
focusGained := false
comp.OnFocusGain(EventHandlerFunc(func(e *Event) bool {
focusGained = true
return true
}))
comp.Focus()
if !focusGained {
t.Error("Focus gain event not triggered")
}
// Test focus loss
focusLost := false
comp.OnFocusLoss(EventHandlerFunc(func(e *Event) bool {
focusLost = true
return true
}))
comp.Blur()
if !focusLost {
t.Error("Focus loss event not triggered")
}
})
t.Run("EventPropagation", func(t *testing.T) {
parent := NewInteractiveComponent()
child := NewInteractiveComponent()
bounds := NewBounds(10, 10, 100, 100)
parent.SetBounds(bounds)
child.SetBounds(bounds)
parentClicked := false
childClicked := false
parent.OnMouseClick(EventHandlerFunc(func(e *Event) bool {
parentClicked = true
return true
}))
child.OnMouseClick(EventHandlerFunc(func(e *Event) bool {
childClicked = true
return true
}))
// Simulate click on child
event := NewMouseEvent(EventTypeMouseMove, child, 20, 20, ebiten.MouseButtonLeft)
child.HandleMouseEvent(event)
event = NewMouseEvent(EventTypeMouseButtonDown, child, 20, 20, ebiten.MouseButtonLeft)
child.HandleMouseEvent(event)
event = NewMouseEvent(EventTypeMouseButtonUp, child, 20, 20, ebiten.MouseButtonLeft)
child.HandleMouseEvent(event)
event = NewMouseEvent(EventTypeClick, child, 20, 20, ebiten.MouseButtonLeft)
child.HandleMouseEvent(event)
if !childClicked {
t.Error("Child click event not triggered")
}
if parentClicked {
t.Error("Parent click event should not be triggered")
}
})
t.Run("EventHandlerManagement", func(t *testing.T) {
t.Run("添加和删除事件处理器", func(t *testing.T) {
comp := NewInteractiveComponent()
bounds := NewBounds(10, 10, 100, 100)
comp.SetBounds(bounds)
// Test adding event handler
handlerCalled := false
handler := EventHandlerFunc(func(e *Event) bool {
handlerCalled = true
return true
})
comp.AddEventListener(EventTypeClick, EventPhaseTarget, handler, PriorityNormal)
// Trigger event
event := NewMouseEvent(EventTypeMouseMove, comp, 20, 20, ebiten.MouseButtonLeft)
comp.HandleMouseEvent(event)
event = NewMouseEvent(EventTypeMouseButtonDown, comp, 20, 20, ebiten.MouseButtonLeft)
comp.HandleMouseEvent(event)
event = NewMouseEvent(EventTypeMouseButtonUp, comp, 20, 20, ebiten.MouseButtonLeft)
comp.HandleMouseEvent(event)
event = NewMouseEvent(EventTypeClick, comp, 20, 20, ebiten.MouseButtonLeft)
comp.HandleMouseEvent(event)
if !handlerCalled {
t.Error("Event handler was not called")
}
// Test removing event handler
handlerCalled = false
comp.RemoveEventListener(EventTypeClick, EventPhaseTarget, handler)
comp.HandleMouseEvent(event)
if handlerCalled {
t.Error("Event handler was called after removal")
}
})
t.Run("多个事件处理器", func(t *testing.T) {
comp := NewInteractiveComponent()
bounds := NewBounds(10, 10, 100, 100)
comp.SetBounds(bounds)
handler1Called := false
handler1 := EventHandlerFunc(func(e *Event) bool {
handler1Called = true
return true
})
handler2Called := false
handler2 := EventHandlerFunc(func(e *Event) bool {
handler2Called = true
return true
})
comp.AddEventListener(EventTypeClick, EventPhaseTarget, handler1, PriorityNormal)
comp.AddEventListener(EventTypeClick, EventPhaseTarget, handler2, PriorityNormal)
event := NewMouseEvent(EventTypeMouseMove, comp, 20, 20, ebiten.MouseButtonLeft)
comp.HandleMouseEvent(event)
event = NewMouseEvent(EventTypeMouseButtonDown, comp, 20, 20, ebiten.MouseButtonLeft)
comp.HandleMouseEvent(event)
event = NewMouseEvent(EventTypeMouseButtonUp, comp, 20, 20, ebiten.MouseButtonLeft)
comp.HandleMouseEvent(event)
event = NewMouseEvent(EventTypeClick, comp, 20, 20, ebiten.MouseButtonLeft)
comp.HandleMouseEvent(event)
if !handler1Called || !handler2Called {
t.Error("Not all event handlers were called")
}
})
})
t.Run("DisabledComponent", func(t *testing.T) {
comp := NewInteractiveComponent()
bounds := NewBounds(10, 10, 100, 100)
comp.SetBounds(bounds)
handlerCalled := false
comp.OnMouseClick(EventHandlerFunc(func(e *Event) bool {
handlerCalled = true
return true
}))
// Disable component
comp.SetEnabled(false)
// Try to trigger events
event := NewMouseEvent(EventTypeMouseMove, comp, 20, 20, ebiten.MouseButtonLeft)
comp.HandleMouseEvent(event)
event = NewMouseEvent(EventTypeMouseButtonDown, comp, 20, 20, ebiten.MouseButtonLeft)
comp.HandleMouseEvent(event)
event = NewMouseEvent(EventTypeMouseButtonUp, comp, 20, 20, ebiten.MouseButtonLeft)
comp.HandleMouseEvent(event)
event = NewMouseEvent(EventTypeClick, comp, 20, 20, ebiten.MouseButtonLeft)
comp.HandleMouseEvent(event)
if handlerCalled {
t.Error("Disabled component should not trigger events")
}
// Re-enable component
comp.SetEnabled(true)
comp.HandleMouseEvent(event)
if !handlerCalled {
t.Error("Re-enabled component should trigger events")
}
})
t.Run("EventCoordinates", func(t *testing.T) {
comp := NewInteractiveComponent()
bounds := NewBounds(10, 10, 100, 100)
comp.SetBounds(bounds)
var eventX, eventY float64
comp.OnMouseClick(EventHandlerFunc(func(e *Event) bool {
eventX = e.GetX()
eventY = e.GetY()
return true
}))
testX, testY := 25.0, 35.0
event := NewMouseEvent(EventTypeMouseMove, comp, testX, testY, ebiten.MouseButtonLeft)
comp.HandleMouseEvent(event)
event = NewMouseEvent(EventTypeMouseButtonDown, comp, testX, testY, ebiten.MouseButtonLeft)
comp.HandleMouseEvent(event)
event = NewMouseEvent(EventTypeMouseButtonUp, comp, testX, testY, ebiten.MouseButtonLeft)
comp.HandleMouseEvent(event)
event = NewMouseEvent(EventTypeClick, comp, testX, testY, ebiten.MouseButtonLeft)
comp.HandleMouseEvent(event)
if eventX != testX || eventY != testY {
t.Errorf("Event coordinates incorrect. Got (%v,%v), want (%v,%v)",
eventX, eventY, testX, testY)
}
})
}
```
# File: container.go
```go
package base
import (
"github.com/hajimehoshi/ebiten/v2"
"github.com/decker502/plants-vs-zombies/internal/ui/event"
"sync"
)
// BaseContainer provides a basic implementation of the Container interface
type BaseContainer struct {
*BaseComponent
components []Component
padding Margins
margin Margins
flexible bool
layout Layout
mu sync.RWMutex
}
// NewBaseContainer creates a new BaseContainer
func NewBaseContainer() *BaseContainer {
return &BaseContainer{
BaseComponent: NewBaseComponent(),
components: make([]Component, 0),
padding: NewMargins(0, 0, 0, 0),
margin: NewMargins(0, 0, 0, 0),
flexible: false,
}
}
// AddComponent adds a component to the container
func (c *BaseContainer) AddComponent(component Component) {
if component == nil {
return
}
c.mu.Lock()
defer c.mu.Unlock()
// Check if the component is already a child of another container
if parent := component.GetParent(); parent != nil {
return
}
c.components = append(c.components, component)
component.SetParent(c)
c.Layout()
}
// RemoveComponent removes a component from the container
func (c *BaseContainer) RemoveComponent(component Component) {
if component == nil {
return
}
c.mu.Lock()
defer c.mu.Unlock()
for i, comp := range c.components {
if comp == component {
c.components = append(c.components[:i], c.components[i+1:]...)
component.SetParent(nil)
c.Layout()
return
}
}
}
// AddChild adds a child component
func (c *BaseContainer) AddChild(component Component) {
c.AddComponent(component)
}
// RemoveChild removes a child component
func (c *BaseContainer) RemoveChild(component Component) {
c.RemoveComponent(component)
}
// GetComponents returns all components in the container
func (c *BaseContainer) GetComponents() []Component {
c.mu.RLock()
defer c.mu.RUnlock()
// Return a copy to prevent concurrent modification
components := make([]Component, len(c.components))
copy(components, c.components)
return components
}
// GetChildren returns all child components
func (c *BaseContainer) GetChildren() []Component {
return c.GetComponents()
}
// Clear removes all components from the container
func (c *BaseContainer) Clear() {
c.mu.Lock()
defer c.mu.Unlock()
for _, component := range c.components {
component.SetParent(nil)
}
c.components = make([]Component, 0)
c.Layout()
}
// Update updates all components in the container
func (c *BaseContainer) Update() error {
if !c.IsEnabled() {
return nil
}
for _, component := range c.GetComponents() {
if err := component.Update(); err != nil {
return err
}
}
return nil
}
// Draw draws all components in the container
func (c *BaseContainer) Draw(screen *ebiten.Image) {
if !c.IsVisible() {
return
}
// 先执行布局
c.Layout()
// 绘制子组件
for _, child := range c.GetChildren() {
child.Draw(screen)
}
}
// Layout performs layout for all components
func (c *BaseContainer) Layout() {
if c.layout != nil {
c.layout.Layout(c.GetComponents(), c.GetBounds())
}
}
// LayoutHorizontal lays out components horizontally
func (c *BaseContainer) LayoutHorizontal() {
if c.layout != nil {
c.layout.Layout(c.GetComponents(), c.GetBounds())
}
}
// LayoutGrid lays out components in a grid
func (c *BaseContainer) LayoutGrid(columns int) {
if c.layout != nil {
c.layout.Layout(c.GetComponents(), c.GetBounds())
}
}
// LayoutVertical lays out components vertically
func (c *BaseContainer) LayoutVertical() {
if c.layout != nil {
c.layout.Layout(c.GetComponents(), c.GetBounds())
}
}
// GetPadding returns the container's padding
func (c *BaseContainer) GetPadding() Margins {
return c.padding
}
// SetPadding sets the container's padding
func (c *BaseContainer) SetPadding(padding Margins) {
c.padding = padding
c.Layout()
}
// GetMargin returns the container's margin
func (c *BaseContainer) GetMargin() Margins {
return c.margin
}
// SetMargin sets the container's margin
func (c *BaseContainer) SetMargin(margin Margins) {
c.margin = margin
c.Layout()
}
// IsFlexible implements Container.IsFlexible
func (c *BaseContainer) IsFlexible() bool {
return c.flexible
}
// SetFlexible sets whether this container is flexible
func (c *BaseContainer) SetFlexible(flexible bool) {
c.flexible = flexible
}
// HandleMouseEvent handles mouse events for all components
func (c *BaseContainer) HandleMouseEvent(e event.MouseEvent) bool {
if !c.IsEnabled() || !c.IsVisible() {
return false
}
// First check if the event is within our bounds
if !c.Contains(e.X(), e.Y()) {
return false
}
// Then dispatch to children in reverse order (top to bottom)
components := c.GetComponents()
for i := len(components) - 1; i >= 0; i-- {
if components[i].HandleMouseEvent(e) {
return true
}
}
// Finally dispatch to our own handlers
return c.BaseComponent.HandleMouseEvent(e)
}
// HandleKeyEvent handles key events for all components
func (c *BaseContainer) HandleKeyEvent(e event.KeyEvent) bool {
if !c.IsEnabled() || !c.IsVisible() {
return false
}
// First try focused component
for _, component := range c.GetComponents() {
if component.IsFocused() && component.HandleKeyEvent(e) {
return true
}
}
// Then try our own handlers
return c.BaseComponent.HandleKeyEvent(e)
}
// Contains checks if a point is within any component's bounds
func (c *BaseContainer) Contains(x, y float64) bool {
// First check container bounds
if !c.GetBounds().Contains(x, y) {
return false
}
// Then check component bounds
for _, component := range c.GetComponents() {
if component.Contains(x, y) {
return true
}
}
return false
}
// SetLayout sets the container's layout manager
func (c *BaseContainer) SetLayout(layout Layout) {
c.layout = layout
}
// GetLayout gets the container's layout manager
func (c *BaseContainer) GetLayout() Layout {
return c.layout
}
// AddEventHandler adds an event handler with priority
func (c *BaseContainer) AddEventHandler(eventType event.EventType, handler event.Handler, priority int) {
c.AddEventListener(eventType, event.EventPhaseTarget, handler, priority)
}
// RemoveEventHandler removes an event handler
func (c *BaseContainer) RemoveEventHandler(eventType event.EventType, handler event.Handler) {
c.RemoveEventListener(eventType, event.EventPhaseTarget, handler)
}
// Layout interface defines the behavior of a layout manager
type Layout interface {
// Layout lays out the components
Layout(components []Component, bounds *BaseBounds)
}
// FlowLayout implements a flow layout
type FlowLayout struct {
spacing float64
}
// NewFlowLayout creates a new flow layout
func NewFlowLayout(spacing float64) *FlowLayout {
return &FlowLayout{
spacing: spacing,
}
}
// Layout lays out the components in a flow layout
func (l *FlowLayout) Layout(components []Component, bounds *BaseBounds) {
if len(components) == 0 {
return
}
x := bounds.X()
y := bounds.Y()
maxHeight := float64(0)
for _, component := range components {
// Skip invisible components
if !component.IsVisible() {
continue
}
// Get component bounds
compBounds := component.GetBounds()
// If component would overflow width, move to next line
if x > bounds.X() && x+compBounds.Width() > bounds.X()+bounds.Width() {
x = bounds.X()
y += maxHeight + l.spacing
maxHeight = 0
}
// Set component position
compBounds.SetX(x)
compBounds.SetY(y)
component.SetBounds(compBounds)
// Update position and max height
x += compBounds.Width() + l.spacing
if compBounds.Height() > maxHeight {
maxHeight = compBounds.Height()
}
}
}
// GridLayout implements a grid layout
type GridLayout struct {
columns int
spacing float64
}
// NewGridLayout creates a new grid layout
func NewGridLayout(columns int, spacing float64) *GridLayout {
return &GridLayout{
columns: columns,
spacing: spacing,
}
}
// Layout lays out the components in a grid layout
func (l *GridLayout) Layout(components []Component, bounds *BaseBounds) {
if len(components) == 0 || l.columns <= 0 {
return
}
// Calculate cell size
cellWidth := (bounds.Width() - float64(l.columns-1)*l.spacing) / float64(l.columns)
cellHeight := cellWidth // Square cells for now
// Layout components
row := 0
col := 0
for _, component := range components {
// Skip invisible components
if !component.IsVisible() {
continue
}
// Calculate position
x := bounds.X() + float64(col)*(cellWidth+l.spacing)
y := bounds.Y() + float64(row)*(cellHeight+l.spacing)
// Set bounds
compBounds := component.GetBounds()
compBounds.SetX(x)
compBounds.SetY(y)
compBounds.SetWidth(cellWidth)
compBounds.SetHeight(cellHeight)
component.SetBounds(compBounds)
// Move to next cell
col++
if col >= l.columns {
col = 0
row++
}
}
}
// StackLayout implements a stack layout
type StackLayout struct {
spacing float64
}
// NewStackLayout creates a new stack layout
func NewStackLayout(spacing float64) *StackLayout {
return &StackLayout{
spacing: spacing,
}
}
// Layout lays out the components in a stack layout
func (l *StackLayout) Layout(components []Component, bounds *BaseBounds) {
if len(components) == 0 {
return
}
y := bounds.Y()
for _, component := range components {
// Skip invisible components
if !component.IsVisible() {
continue
}
// Set component bounds
compBounds := component.GetBounds()
compBounds.SetX(bounds.X())
compBounds.SetY(y)
compBounds.SetWidth(bounds.Width())
component.SetBounds(compBounds)
// Update position for next component
y += compBounds.Height() + l.spacing
}
}
```
# File: component_test.go
```go
package base
import (
"github.com/hajimehoshi/ebiten/v2"
"github.com/stretchr/testify/assert"
"github.com/decker502/plants-vs-zombies/internal/ui/event"
"sync"
"sync/atomic"
"testing"
)
func TestBaseComponent(t *testing.T) {
comp := NewBaseComponent()
// Test initial state
assert.True(t, comp.IsEnabled())
assert.True(t, comp.IsVisible())
assert.False(t, comp.IsFocused())
assert.Empty(t, comp.GetChildren())
assert.NotNil(t, comp.GetLayoutConstraints())
// Test state changes
comp.SetEnabled(false)
assert.False(t, comp.IsEnabled())
comp.SetVisible(false)
assert.False(t, comp.IsVisible())
comp.SetFocused(true)
assert.True(t, comp.IsFocused())
// Test bounds
bounds := NewBounds(10, 20, 100, 200)
comp.SetBounds(bounds)
assert.Equal(t, bounds, comp.GetBounds())
// Test constraints
constraints := NewBaseLayoutConstraints()
constraints.SetMinWidth(50)
constraints.SetMinHeight(50)
comp.SetLayoutConstraints(constraints)
assert.Equal(t, constraints, comp.GetLayoutConstraints())
// Test children
child := NewBaseComponent()
comp.AddChild(child)
assert.Equal(t, 1, len(comp.GetChildren()))
assert.Equal(t, child, comp.GetChildren()[0])
assert.Equal(t, comp, child.GetParent())
comp.RemoveChild(child)
assert.Empty(t, comp.GetChildren())
assert.Nil(t, child.GetParent())
}
func TestBaseComponentEventHandling(t *testing.T) {
comp := NewBaseComponent()
assert.NotNil(t, comp)
// Test event handling
mouseEvent := event.NewPool().GetMouseEvent(event.TypeMouseMove, comp, 0, 0, ebiten.MouseButtonLeft)
handled := comp.HandleMouseEvent(mouseEvent)
assert.False(t, handled)
keyEvent := event.NewPool().GetKeyEvent(event.TypeKeyPress, comp, ebiten.KeySpace)
handled = comp.HandleKeyEvent(keyEvent)
assert.False(t, handled)
}
func TestBaseComponentLayout(t *testing.T) {
comp := NewBaseComponent()
assert.NotNil(t, comp)
// Test initial layout
bounds := comp.GetBounds()
assert.Equal(t, float64(0), bounds.X())
assert.Equal(t, float64(0), bounds.Y())
assert.Equal(t, float64(0), bounds.Width())
assert.Equal(t, float64(0), bounds.Height())
// Test layout constraints
constraints := NewBaseLayoutConstraints()
constraints.SetMinWidth(100)
constraints.SetMinHeight(100)
constraints.SetMaxWidth(200)
constraints.SetMaxHeight(200)
comp.SetLayoutConstraints(constraints)
assert.Equal(t, float64(100), comp.GetLayoutConstraints().MinWidth())
assert.Equal(t, float64(100), comp.GetLayoutConstraints().MinHeight())
assert.Equal(t, float64(200), comp.GetLayoutConstraints().MaxWidth())
assert.Equal(t, float64(200), comp.GetLayoutConstraints().MaxHeight())
}
func TestBaseComponentHierarchy(t *testing.T) {
root := NewBaseComponent()
child1 := NewBaseComponent()
child2 := NewBaseComponent()
grandchild := NewBaseComponent()
// Build hierarchy
root.AddChild(child1)
root.AddChild(child2)
child1.AddChild(grandchild)
// Test parent-child relationships
assert.Equal(t, root, child1.GetParent())
assert.Equal(t, root, child2.GetParent())
assert.Equal(t, child1, grandchild.GetParent())
// Test depth calculation
assert.Equal(t, 0, root.GetDepth())
assert.Equal(t, 1, child1.GetDepth())
assert.Equal(t, 1, child2.GetDepth())
assert.Equal(t, 2, grandchild.GetDepth())
// Test children access
assert.Equal(t, 2, len(root.GetChildren()))
assert.Equal(t, 1, len(child1.GetChildren()))
assert.Equal(t, 0, len(child2.GetChildren()))
assert.Equal(t, 0, len(grandchild.GetChildren()))
// Test child removal
root.RemoveChild(child1)
assert.Equal(t, 1, len(root.GetChildren()))
assert.Nil(t, child1.GetParent())
assert.Equal(t, child1, grandchild.GetParent())
}
func TestBaseComponentBounds(t *testing.T) {
comp := NewBaseComponent()
// Test setting and getting bounds
bounds := NewBounds(10.0, 20.0, 100.0, 200.0)
comp.SetBounds(bounds)
gotBounds := comp.GetBounds()
assert.Equal(t, bounds, gotBounds)
// Test bounds calculations
assert.Equal(t, float64(10), gotBounds.X())
assert.Equal(t, float64(20), gotBounds.Y())
assert.Equal(t, float64(100), gotBounds.Width())
assert.Equal(t, float64(200), gotBounds.Height())
// Test point containment
assert.True(t, gotBounds.Contains(15, 25))
assert.False(t, gotBounds.Contains(5, 15))
assert.False(t, gotBounds.Contains(115, 225))
}
func TestBaseComponentContains(t *testing.T) {
comp := NewBaseComponent()
bounds := NewBounds(10, 10, 100, 100)
comp.SetBounds(bounds)
// Test points inside
assert.True(t, comp.Contains(10, 10)) // Top-left
assert.True(t, comp.Contains(60, 60)) // Center
assert.True(t, comp.Contains(110, 110)) // Bottom-right
// Test points outside
assert.False(t, comp.Contains(9, 60)) // Left
assert.False(t, comp.Contains(111, 60)) // Right
assert.False(t, comp.Contains(60, 9)) // Top
assert.False(t, comp.Contains(60, 111)) // Bottom
assert.False(t, comp.Contains(9, 9)) // Top-left
assert.False(t, comp.Contains(111, 111)) // Bottom-right
}
func BenchmarkBaseComponent(b *testing.B) {
comp := NewBaseComponent()
bounds := NewBounds(10, 20, 100, 100)
b.Run("SetBounds", func(b *testing.B) {
for i := 0; i < b.N; i++ {
comp.SetBounds(bounds)
}
})
b.Run("Contains", func(b *testing.B) {
comp.SetBounds(bounds)
for i := 0; i < b.N; i++ {
comp.Contains(60, 45)
}
})
}
// 测试组件生命周期
func TestComponentLifecycle(t *testing.T) {
t.Run("Creation", func(t *testing.T) {
comp := NewBaseComponent()
assert.NotNil(t, comp)
assert.True(t, comp.IsEnabled())
assert.True(t, comp.IsVisible())
})
t.Run("Destruction", func(t *testing.T) {
comp := NewBaseComponent()
parent := NewBaseContainer()
parent.AddChild(comp)
// 测试移除前的状态
assert.Equal(t, parent, comp.Parent())
assert.Contains(t, parent.GetChildren(), comp)
// 测试移除后的状态
parent.RemoveChild(comp)
assert.Nil(t, comp.Parent())
assert.NotContains(t, parent.GetChildren(), comp)
})
t.Run("StateTransitions", func(t *testing.T) {
comp := NewBaseComponent()
// 测试启用/禁用状态转换
comp.SetEnabled(false)
assert.False(t, comp.IsEnabled())
comp.SetEnabled(true)
assert.True(t, comp.IsEnabled())
// 测试显示/隐藏状态转换
comp.SetVisible(false)
assert.False(t, comp.IsVisible())
comp.SetVisible(true)
assert.True(t, comp.IsVisible())
// 测试焦点状态转换
comp.SetFocused(true)
assert.True(t, comp.IsFocused())
comp.SetFocused(false)
assert.False(t, comp.IsFocused())
})
}
// 测试事件处理
func TestComponentEventHandling(t *testing.T) {
comp := NewBaseComponent()
parent := NewBaseComponent()
parent.AddChild(comp)
// Test mouse event
mouseEvent := event.NewPool().GetMouseEvent(event.TypeMouseMove, comp, 10, 20, ebiten.MouseButtonLeft)
assert.True(t, comp.HandleMouseEvent(mouseEvent))
assert.Equal(t, float64(10), mouseEvent.GetX())
assert.Equal(t, float64(20), mouseEvent.GetY())
// Test key event
keyEvent := event.NewPool().GetKeyEvent(event.TypeKeyPress, comp, ebiten.KeySpace)
assert.True(t, comp.HandleKeyEvent(keyEvent))
assert.Equal(t, ebiten.KeySpace, keyEvent.GetKey())
// Test parent-child relationship
assert.Equal(t, parent, comp.GetParent())
}
// 测试布局约束
func TestComponentLayout(t *testing.T) {
t.Run("SizeConstraints", func(t *testing.T) {
comp := NewBaseComponent()
// Set initial bounds
bounds := NewBounds(0, 0, 100, 100)
comp.SetBounds(bounds)
// Test size constraints
constraints := NewBaseLayoutConstraints()
constraints.SetMinWidth(50)
constraints.SetMaxWidth(150)
constraints.SetMinHeight(50)
constraints.SetMaxHeight(150)
comp.SetLayoutConstraints(constraints)
// Test bounds within constraints
newBounds := NewBounds(0, 0, 100, 100)
comp.SetBounds(newBounds)
assert.Equal(t, newBounds.Width(), comp.GetBounds().Width())
assert.Equal(t, newBounds.Height(), comp.GetBounds().Height())
// Test bounds below minimum
newBounds = NewBounds(0, 0, 25, 25)
comp.SetBounds(newBounds)
resultBounds := comp.GetBounds()
assert.Equal(t, 50, resultBounds.Width())
assert.Equal(t, 50, resultBounds.Height())
// Test bounds above maximum
newBounds = NewBounds(0, 0, 200, 200)
comp.SetBounds(newBounds)
resultBounds = comp.GetBounds()
assert.Equal(t, 150, resultBounds.Width())
assert.Equal(t, 150, resultBounds.Height())
})
}
// 测试事件系统边缘情况
func TestComponentEventSystem(t *testing.T) {
t.Run("EventPropagation", func(t *testing.T) {
parent := NewBaseComponent()
child := NewBaseComponent()
parent.AddChild(child)
var propagationPath []string
parentHandler := event.EventHandlerFunc(func(event *event.Event) bool {
propagationPath = append(propagationPath, "parent")
return true
})
childHandler := event.EventHandlerFunc(func(event *event.Event) bool {
propagationPath = append(propagationPath, "child")
return true
})
parent.AddEventListener(event.TypeMouseMove, event.EventPhaseBubbling, parentHandler, 0)
child.AddEventListener(event.TypeMouseMove, event.EventPhaseTarget, childHandler, 0)
event := event.New(event.TypeMouseMove, child)
child.DispatchEvent(event)
assert.Equal(t, []string{"child", "parent"}, propagationPath)
})
t.Run("EventStopPropagation", func(t *testing.T) {
parent := NewBaseComponent()
child := NewBaseComponent()
parent.AddChild(child)
var propagationPath []string
parentHandler := event.EventHandlerFunc(func(event *event.Event) bool {
propagationPath = append(propagationPath, "parent")
return true
})
childHandler := event.EventHandlerFunc(func(event *event.Event) bool {
propagationPath = append(propagationPath, "child")
event.Stop()
return true
})
parent.AddEventListener(event.TypeMouseMove, event.EventPhaseBubbling, parentHandler, 0)
child.AddEventListener(event.TypeMouseMove, event.EventPhaseTarget, childHandler, 0)
event := event.New(event.TypeMouseMove, child)
child.DispatchEvent(event)
assert.Equal(t, []string{"child"}, propagationPath, "Event propagation should be stopped")
})
t.Run("EventPriority", func(t *testing.T) {
comp := NewBaseComponent()
var order []int
handler1 := event.EventHandlerFunc(func(event *event.Event) bool {
order = append(order, 1)
return true
})
handler2 := event.EventHandlerFunc(func(event *event.Event) bool {
order = append(order, 2)
return true
})
handler3 := event.EventHandlerFunc(func(event *event.Event) bool {
order = append(order, 3)
return true
})
// Add handlers with different priorities
comp.AddEventListener(event.TypeMouseMove, event.EventPhaseTarget, handler1, 1)
comp.AddEventListener(event.TypeMouseMove, event.EventPhaseTarget, handler2, 2)
comp.AddEventListener(event.TypeMouseMove, event.EventPhaseTarget, handler3, 0)
event := event.New(event.TypeMouseMove, comp)
comp.DispatchEvent(event)
assert.Equal(t, []int{2, 1, 3}, order, "Handlers should be called in priority order")
})
}
// 并发测试
func TestComponentConcurrency(t *testing.T) {
t.Run("ConcurrentEventHandling", func(t *testing.T) {
comp := NewBaseComponent()
var counter int32
handler := event.EventHandlerFunc(func(event *event.Event) bool {
atomic.AddInt32(&counter, 1)
return true
})
comp.AddEventListener(event.TypeMouseMove, event.EventPhaseTarget, handler, 0)
event := event.NewPool().GetMouseEvent(event.TypeMouseMove, comp, 0, 0, ebiten.MouseButtonLeft)
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
comp.DispatchEvent(event)
}()
}
wg.Wait()
assert.Equal(t, int32(100), atomic.LoadInt32(&counter))
})
t.Run("ConcurrentPropertyAccess", func(t *testing.T) {
comp := NewBaseComponent()
var wg sync.WaitGroup
// Concurrent property access
for i := 0; i < 100; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
bounds := NewBounds(float64(i), float64(100+i), float64(200+i), float64(300+i))
comp.SetBounds(bounds)
_ = comp.GetBounds()
comp.SetEnabled(i%2 == 0)
_ = comp.IsEnabled()
}(i)
}
wg.Wait()
})
}
// 性能测试
func BenchmarkComponent(b *testing.B) {
b.Run("EventDispatch", func(b *testing.B) {
comp := NewBaseComponent()
var counter int64
handler := event.EventHandlerFunc(func(event *event.Event) bool {
atomic.AddInt64(&counter, 1)
return true
})
comp.AddEventListener(event.TypeMouseMove, event.EventPhaseTarget, handler, 0)
evt := event.NewPool().GetMouseEvent(event.TypeMouseMove, comp, 0, 0, ebiten.MouseButtonLeft)
b.ResetTimer()
for i := 0; i < b.N; i++ {
comp.DispatchEvent(evt)
}
})
b.Run("BoundsAccess", func(b *testing.B) {
comp := NewBaseComponent()
bounds := NewBounds(0, 0, 100, 100)
b.ResetTimer()
for i := 0; i < b.N; i++ {
if i%2 == 0 {
comp.SetBounds(bounds)
} else {
_ = comp.GetBounds()
}
}
})
}
```
# File: lifecycle.go
```go
package base
// LifecycleState represents the current state of a component's lifecycle
type LifecycleState int
const (
// LifecycleStateUnmounted indicates the component is not mounted
LifecycleStateUnmounted LifecycleState = iota
// LifecycleStateMounting indicates the component is in the process of mounting
LifecycleStateMounting
// LifecycleStateMounted indicates the component is mounted and active
LifecycleStateMounted
// LifecycleStateUnmounting indicates the component is in the process of unmounting
LifecycleStateUnmounting
)
// LifecycleAware defines the interface for components that need lifecycle management
type LifecycleAware interface {
// GetLifecycleState returns the current lifecycle state
GetLifecycleState() LifecycleState
// SetLifecycleState sets the lifecycle state
SetLifecycleState(state LifecycleState)
// Lifecycle hooks
BeforeMount()
AfterMount()
BeforeUnmount()
AfterUnmount()
// Update is called when the component needs to update
Update()
}
// LifecycleManager provides lifecycle management functionality
type LifecycleManager struct {
state LifecycleState
}
// NewLifecycleManager creates a new lifecycle manager
func NewLifecycleManager() *LifecycleManager {
return &LifecycleManager{
state: LifecycleStateUnmounted,
}
}
// GetLifecycleState returns the current lifecycle state
func (l *LifecycleManager) GetLifecycleState() LifecycleState {
return l.state
}
// SetLifecycleState sets the lifecycle state
func (l *LifecycleManager) SetLifecycleState(state LifecycleState) {
l.state = state
}
// Mount performs the mounting process
func (l *LifecycleManager) Mount(component LifecycleAware) {
if l.state != LifecycleStateUnmounted {
return
}
l.state = LifecycleStateMounting
component.BeforeMount()
l.state = LifecycleStateMounted
component.AfterMount()
}
// Unmount performs the unmounting process
func (l *LifecycleManager) Unmount(component LifecycleAware) {
if l.state != LifecycleStateMounted {
return
}
l.state = LifecycleStateUnmounting
component.BeforeUnmount()
l.state = LifecycleStateUnmounted
component.AfterUnmount()
}
// BaseLifecycleComponent provides a base implementation of LifecycleAware
type BaseLifecycleComponent struct {
*LifecycleManager
}
// NewBaseLifecycleComponent creates a new base lifecycle component
func NewBaseLifecycleComponent() *BaseLifecycleComponent {
return &BaseLifecycleComponent{
LifecycleManager: NewLifecycleManager(),
}
}
// BeforeMount is called before the component is mounted
func (b *BaseLifecycleComponent) BeforeMount() {
// Default implementation does nothing
}
// AfterMount is called after the component is mounted
func (b *BaseLifecycleComponent) AfterMount() {
// Default implementation does nothing
}
// BeforeUnmount is called before the component is unmounted
func (b *BaseLifecycleComponent) BeforeUnmount() {
// Default implementation does nothing
}
// AfterUnmount is called after the component is unmounted
func (b *BaseLifecycleComponent) AfterUnmount() {
// Default implementation does nothing
}
// Update is called when the component needs to update
func (b *BaseLifecycleComponent) Update() {
// Default implementation does nothing
}
```
# File: component.go
```go
// Package base provides core UI components and interfaces
package base
import (
"github.com/hajimehoshi/ebiten/v2"
"github.com/decker502/plants-vs-zombies/internal/ui/event"
"sync"
"github.com/google/uuid"
)
// BaseComponent provides basic implementation of the Component interface
type BaseComponent struct {
id string
enabled bool
visible bool
focused bool
bounds *BaseBounds
parent event.Component
children []event.Component
mu sync.RWMutex
eventHandler *event.Dispatcher
eventPool event.Pool
constraints *BaseLayoutConstraints
layoutManager LayoutManager
flexible bool
flexWeight float64
initialized bool
}
// NewBaseComponent creates a new base component
func NewBaseComponent() *BaseComponent {
return &BaseComponent{
id: uuid.New().String(),
enabled: true,
visible: true,
focused: false,
bounds: &BaseBounds{},
children: make([]event.Component, 0),
eventHandler: event.NewDispatcher(),
eventPool: event.NewPool(),
constraints: &BaseLayoutConstraints{},
flexible: false,
flexWeight: 1.0,
initialized: false,
}
}
// Init initializes the component
func (b *BaseComponent) Init() {
if b.initialized {
return
}
// Initialize children
for _, child := range b.children {
child.Init()
}
b.initialized = true
}
// Destroy cleans up the component
func (b *BaseComponent) Destroy() {
// Clean up children
for _, child := range b.children {
child.Destroy()
}
// Remove from parent
if b.parent != nil {
b.parent.RemoveChild(b)
}
// Clear references
b.children = nil
b.parent = nil
b.eventHandler = nil
b.layoutManager = nil
}
// Update updates the component state
func (b *BaseComponent) Update() error {
return nil
}
// Draw draws the component
func (b *BaseComponent) Draw(screen *ebiten.Image) {
// Base component doesn't draw anything
}
// HandleEvent handles an event
func (b *BaseComponent) HandleEvent(evt event.Event) bool {
if !b.enabled || !b.visible {
return false
}
switch e := evt.(type) {
case event.MouseEvent:
return b.HandleMouseEvent(e)
case event.KeyEvent:
return b.HandleKeyEvent(e)
default:
return false
}
}
// HandleMouseEvent handles a mouse event
func (b *BaseComponent) HandleMouseEvent(evt event.MouseEvent) bool {
if !b.enabled || !b.visible {
return false
}
// Check if the event is within our bounds
if !b.Contains(evt.X(), evt.Y()) {
return false
}
// Dispatch the event to event handlers
return b.DispatchEvent(evt)
}
// HandleKeyEvent handles a key event
func (b *BaseComponent) HandleKeyEvent(evt event.KeyEvent) bool {
if !b.enabled || !b.visible {
return false
}
// Key events only go to focused components
if !b.focused {
return false
}
// Dispatch the event to event handlers
return b.DispatchEvent(evt)
}
// DispatchEvent dispatches an event
func (b *BaseComponent) DispatchEvent(event event.Event) bool {
return b.eventHandler.DispatchEvent(event)
}
// GetBounds returns the component's bounds
func (b *BaseComponent) GetBounds() *BaseBounds {
return b.bounds
}
// SetBounds sets the component's bounds
func (b *BaseComponent) SetBounds(bounds *BaseBounds) {
b.bounds = bounds
}
// SetLayoutConstraints sets the layout constraints
func (b *BaseComponent) SetLayoutConstraints(constraints *BaseLayoutConstraints) {
b.constraints = constraints
}
// GetLayoutConstraints returns the layout constraints
func (b *BaseComponent) GetLayoutConstraints() *BaseLayoutConstraints {
return b.constraints
}
// IsFlexible returns whether the component is flexible
func (b *BaseComponent) IsFlexible() bool {
return b.flexible
}
// GetFlexWeight returns the flex weight of the component
func (b *BaseComponent) GetFlexWeight() float64 {
return b.flexWeight
}
// Parent returns the parent component
func (b *BaseComponent) Parent() event.Component {
return b.parent
}
// SetParent sets the parent component
func (b *BaseComponent) SetParent(parent event.Component) {
b.parent = parent
}
// Children returns the child components
func (b *BaseComponent) Children() []event.Component {
b.mu.RLock()
defer b.mu.RUnlock()
return b.children
}
// AddChild adds a child component
func (b *BaseComponent) AddChild(child event.Component) {
b.mu.Lock()
defer b.mu.Unlock()
// Check if the child is already added
for _, existing := range b.children {
if existing == child {
return
}
}
// Add the child
b.children = append(b.children, child)
child.SetParent(b)
}
// RemoveChild removes a child component
func (b *BaseComponent) RemoveChild(child event.Component) {
b.mu.Lock()
defer b.mu.Unlock()
// Find and remove the child
for i, existing := range b.children {
if existing == child {
b.children = append(b.children[:i], b.children[i+1:]...)
child.SetParent(nil)
return
}
}
}
// Position returns the component's position
func (b *BaseComponent) Position() (x, y float64) {
return b.bounds.X, b.bounds.Y
}
// Size returns the component's size
func (b *BaseComponent) Size() (width, height float64) {
return b.bounds.Width, b.bounds.Height
}
// IsEnabled returns whether the component is enabled
func (b *BaseComponent) IsEnabled() bool {
b.mu.RLock()
defer b.mu.RUnlock()
return b.enabled
}
// SetEnabled sets whether the component is enabled
func (b *BaseComponent) SetEnabled(enabled bool) {
b.mu.Lock()
defer b.mu.Unlock()
b.enabled = enabled
}
// IsVisible returns whether the component is visible
func (b *BaseComponent) IsVisible() bool {
b.mu.RLock()
defer b.mu.RUnlock()
return b.visible
}
// SetVisible sets whether the component is visible
func (b *BaseComponent) SetVisible(visible bool) {
b.mu.Lock()
defer b.mu.Unlock()
b.visible = visible
}
// IsFocused returns whether the component is focused
func (b *BaseComponent) IsFocused() bool {
b.mu.RLock()
defer b.mu.RUnlock()
return b.focused
}
// SetFocused sets the focused state of the component
func (b *BaseComponent) SetFocused(focused bool) {
b.mu.Lock()
defer b.mu.Unlock()
if b.focused != focused {
b.focused = focused
if focused {
evt := b.eventPool.GetEvent(event.EventTypeFocusGain)
evt.SetCurrentTarget(b)
b.DispatchEvent(evt)
b.eventPool.PutEvent(evt)
} else {
evt := b.eventPool.GetEvent(event.EventTypeFocusLoss)
evt.SetCurrentTarget(b)
b.DispatchEvent(evt)
b.eventPool.PutEvent(evt)
}
}
}
// GetID returns the component's ID
func (b *BaseComponent) GetID() string {
b.mu.RLock()
defer b.mu.RUnlock()
return b.id
}
// SetID sets the component's ID
func (b *BaseComponent) SetID(id string) {
b.mu.Lock()
defer b.mu.Unlock()
b.id = id
}
// GetDepth returns the depth of the component
func (b *BaseComponent) GetDepth() int {
depth := 0
parent := b.Parent()
for parent != nil {
depth++
parent = parent.Parent()
}
return depth
}
// AddComponent adds a component to the container
func (b *BaseComponent) AddComponent(component event.Component) {
b.AddChild(component)
}
// RemoveComponent removes a component from the container
func (b *BaseComponent) RemoveComponent(component event.Component) {
b.RemoveChild(component)
}
// GetComponents returns all components in the container
func (b *BaseComponent) GetComponents() []event.Component {
return b.Children()
}
// Contains checks if a point is within the component's bounds
func (b *BaseComponent) Contains(x, y float64) bool {
return b.bounds.Contains(x, y)
}
// AddEventListener adds an event listener
func (b *BaseComponent) AddEventListener(eventType event.EventType, phase event.EventPhase, handler event.Handler, priority int) {
if handler == nil {
return
}
b.eventHandler.AddEventListener(eventType, phase, handler, priority)
}
// RemoveEventListener removes an event listener
func (b *BaseComponent) RemoveEventListener(eventType event.EventType, phase event.EventPhase, handler event.Handler) {
if handler == nil {
return
}
b.eventHandler.RemoveEventListener(eventType, phase, handler)
}
// OnMouseMove adds a mouse move event listener
func (b *BaseComponent) OnMouseMove(handler event.Handler) {
b.AddEventListener(event.EventTypeMouseMove, event.EventPhaseTarget, handler, 0)
}
// OnMouseButtonDown adds a mouse button down event listener
func (b *BaseComponent) OnMouseButtonDown(handler event.Handler) {
b.AddEventListener(event.EventTypeMouseButtonDown, event.EventPhaseTarget, handler, 0)
}
// OnMouseButtonUp adds a mouse button up event listener
func (b *BaseComponent) OnMouseButtonUp(handler event.Handler) {
b.AddEventListener(event.EventTypeMouseButtonUp, event.EventPhaseTarget, handler, 0)
}
// OnMouseEnter adds a mouse enter event listener
func (b *BaseComponent) OnMouseEnter(handler event.Handler) {
b.AddEventListener(event.EventTypeMouseEnter, event.EventPhaseTarget, handler, 0)
}
// OnMouseLeave adds a mouse leave event listener
func (b *BaseComponent) OnMouseLeave(handler event.Handler) {
b.AddEventListener(event.EventTypeMouseLeave, event.EventPhaseTarget, handler, 0)
}
// OnKeyPress adds a key press event listener
func (b *BaseComponent) OnKeyPress(handler event.Handler) {
b.AddEventListener(event.EventTypeKeyPress, event.EventPhaseTarget, handler, 0)
}
// OnKeyRelease adds a key release event listener
func (b *BaseComponent) OnKeyRelease(handler event.Handler) {
b.AddEventListener(event.EventTypeKeyRelease, event.EventPhaseTarget, handler, 0)
}
// OnFocusGain adds a focus gain event listener
func (b *BaseComponent) OnFocusGain(handler event.Handler) {
b.AddEventListener(event.EventTypeFocusGain, event.EventPhaseTarget, handler, 0)
}
// OnFocusLoss adds a focus loss event listener
func (b *BaseComponent) OnFocusLoss(handler event.Handler) {
b.AddEventListener(event.EventTypeFocusLoss, event.EventPhaseTarget, handler, 0)
}
// OnClick adds a click event listener
func (b *BaseComponent) OnClick(handler event.Handler) {
b.AddEventListener(event.EventTypeClick, event.EventPhaseTarget, handler, 0)
}
// OnMouseClick adds a click event handler
func (b *BaseComponent) OnMouseClick(handler event.Handler) {
b.AddEventListener(event.EventTypeClick, event.EventPhaseTarget, handler, 0)
}
// SetFlexible sets whether the component is flexible
func (b *BaseComponent) SetFlexible(flexible bool) {
b.mu.Lock()
defer b.mu.Unlock()
b.flexible = flexible
}
// SetFlexWeight sets the flex weight of the component
func (b *BaseComponent) SetFlexWeight(weight float64) {
b.mu.Lock()
defer b.mu.Unlock()
b.flexWeight = weight
}
// LayoutHorizontal lays out components horizontally
func (b *BaseComponent) LayoutHorizontal() {
if b.layoutManager != nil {
b.layoutManager.Layout(b)
}
}
// LayoutVertical lays out components vertically
func (b *BaseComponent) LayoutVertical() {
if b.layoutManager != nil {
b.layoutManager.Layout(b)
}
}
// LayoutGrid lays out components in a grid
func (b *BaseComponent) LayoutGrid(columns int) {
if b.layoutManager != nil {
b.layoutManager.Layout(b)
}
}
// Priority constants for event handlers
const (
PriorityHighest = 100
PriorityHigh = 75
PriorityNormal = 50
PriorityLow = 25
PriorityLowest = 0
)
```
# File: container_test.go
```go
package base
import (
"testing"
"sync"
"sync/atomic"
"github.com/stretchr/testify/assert"
"github.com/decker502/plants-vs-zombies/internal/ui/event"
)
func TestBaseContainer(t *testing.T) {
t.Run("NewBaseContainer", func(t *testing.T) {
container := NewBaseContainer()
assert.NotNil(t, container)
// Test initial state
assert.Empty(t, container.GetComponents())
assert.Equal(t, Margins{}, container.GetPadding())
// Test adding components
comp1 := NewBaseComponent()
comp2 := NewBaseComponent()
container.AddComponent(comp1)
container.AddComponent(comp2)
components := container.GetComponents()
assert.Equal(t, 2, len(components))
assert.Equal(t, comp1, components[0])
assert.Equal(t, comp2, components[1])
// Test removing components
container.RemoveComponent(comp1)
components = container.GetComponents()
assert.Equal(t, 1, len(components))
assert.Equal(t, comp2, components[0])
// Test clearing components
container.Clear()
assert.Empty(t, container.GetComponents())
})
t.Run("AddChild", func(t *testing.T) {
container := NewBaseContainer()
child := NewBaseComponent()
container.AddComponent(child)
children := container.GetComponents()
assert.Len(t, children, 1, "Container should have one child")
assert.Equal(t, child, children[0], "Container's child is not the one that was added")
})
t.Run("RemoveChild", func(t *testing.T) {
container := NewBaseContainer()
child1 := NewBaseComponent()
child2 := NewBaseComponent()
container.AddComponent(child1)
container.AddComponent(child2)
assert.Len(t, container.GetComponents(), 2, "Container should have two children")
container.RemoveComponent(child1)
children := container.GetComponents()
assert.Len(t, children, 1, "Container should have one child after removal")
assert.Equal(t, child2, children[0], "Wrong child was removed")
})
t.Run("Layout", func(t *testing.T) {
container := NewBaseContainer()
child := NewBaseComponent()
container.AddComponent(child)
// Set container bounds
containerBounds := NewBounds(0, 0, 200, 200)
container.SetBounds(containerBounds)
// Set child constraints
constraints := NewLayoutConstraints()
constraints.SetMinWidth(100)
constraints.SetMinHeight(100)
constraints.SetMaxWidth(150)
constraints.SetMaxHeight(150)
child.SetConstraints(constraints)
// Test child bounds after layout
childBounds := child.GetBounds()
assert.GreaterOrEqual(t, childBounds.Width(), 100, "Child width should be at least min width")
assert.LessOrEqual(t, childBounds.Width(), 150, "Child width should be at most max width")
assert.GreaterOrEqual(t, childBounds.Height(), 100, "Child height should be at least min height")
assert.LessOrEqual(t, childBounds.Height(), 150, "Child height should be at most max height")
})
}
func TestContainerHierarchy(t *testing.T) {
root := NewBaseContainer()
child := NewBaseContainer()
grandchild := NewBaseComponent()
root.AddComponent(child)
child.AddComponent(grandchild)
// Test event propagation
var propagationPath []string
rootHandler := event.EventHandlerFunc(func(e event.Event) bool {
propagationPath = append(propagationPath, "root")
return true
})
childHandler := event.EventHandlerFunc(func(e event.Event) bool {
propagationPath = append(propagationPath, "child")
return true
})
grandchildHandler := event.EventHandlerFunc(func(e event.Event) bool {
propagationPath = append(propagationPath, "grandchild")
return false // Stop propagation
})
root.AddEventListener(event.TypeMouseMove, event.EventPhaseBubbling, rootHandler, 0)
child.AddEventListener(event.TypeMouseMove, event.EventPhaseBubbling, childHandler, 0)
grandchild.AddEventListener(event.TypeMouseMove, event.EventPhaseTarget, grandchildHandler, 0)
evt := event.New(event.TypeMouseMove, grandchild)
root.DispatchEvent(evt)
assert.Equal(t, []string{"grandchild"}, propagationPath, "Event should be stopped at grandchild")
}
func TestContainerChildren(t *testing.T) {
container := NewBaseContainer()
child1 := NewBaseComponent()
child2 := NewBaseComponent()
container.AddComponent(child1)
container.AddComponent(child2)
// Test GetComponents
components := container.GetComponents()
assert.Len(t, components, 2, "Container should have two components")
assert.Contains(t, components, child1, "Container should contain child1")
assert.Contains(t, components, child2, "Container should contain child2")
// Test Clear
container.Clear()
assert.Empty(t, container.GetComponents(), "Container should be empty after Clear")
}
func TestContainerLayout(t *testing.T) {
container := NewBaseContainer()
child := NewBaseComponent()
container.AddComponent(child)
// Set container bounds
containerBounds := NewBounds(0, 0, 200, 200)
container.SetBounds(containerBounds)
// Set child constraints
constraints := NewLayoutConstraints()
constraints.SetMinWidth(100)
constraints.SetMinHeight(100)
constraints.SetMaxWidth(150)
constraints.SetMaxHeight(150)
child.SetConstraints(constraints)
// Test child bounds after layout
childBounds := child.GetBounds()
assert.GreaterOrEqual(t, childBounds.Width(), 100, "Child width should be at least min width")
assert.LessOrEqual(t, childBounds.Width(), 150, "Child width should be at most max width")
assert.GreaterOrEqual(t, childBounds.Height(), 100, "Child height should be at least min height")
assert.LessOrEqual(t, childBounds.Height(), 150, "Child height should be at most max height")
}
func TestContainerGridLayout(t *testing.T) {
container := NewBaseContainer()
container.SetBounds(NewBounds(0, 0, 300, 200))
// Add components
for i := 0; i < 4; i++ {
comp := NewBaseComponent()
container.AddComponent(comp)
}
// Test grid layout
container.LayoutGrid(2) // 2 columns
// Verify component positions
components := container.GetComponents()
assert.Equal(t, 4, len(components))
// Expected cell size
cellWidth := 150.0 // 300/2
cellHeight := 100.0 // 200/2
// Check component bounds
for i, comp := range components {
bounds := comp.GetBounds()
row := float64(i / 2)
col := float64(i % 2)
expectedX := col * cellWidth
expectedY := row * cellHeight
assert.Equal(t, expectedX, bounds.X())
assert.Equal(t, expectedY, bounds.Y())
assert.Equal(t, cellWidth, bounds.Width())
assert.Equal(t, cellHeight, bounds.Height())
}
}
func TestContainerVerticalLayout(t *testing.T) {
container := NewBaseContainer()
container.SetBounds(NewBounds(0, 0, 200, 300))
// Add components with fixed and flexible sizes
comp1 := NewBaseComponent()
comp1.SetBounds(NewBounds(0, 0, 200, 50))
comp2 := NewBaseComponent()
comp2.SetBounds(NewBounds(0, 0, 200, 50))
comp2.SetFlexible(true)
comp2.SetFlexWeight(1.0)
comp3 := NewBaseComponent()
comp3.SetBounds(NewBounds(0, 0, 200, 50))
comp3.SetFlexible(true)
comp3.SetFlexWeight(2.0)
container.AddComponent(comp1)
container.AddComponent(comp2)
container.AddComponent(comp3)
// Test vertical layout
container.LayoutVertical()
// Verify component positions and sizes
assert.Equal(t, float64(0), comp1.GetBounds().Y())
assert.Equal(t, float64(50), comp1.GetBounds().Height())
// Remaining height (300 - 50) = 250 should be distributed according to flex weights
// comp2 (weight 1.0) should get 250 * (1/3) ≈ 83.33
// comp3 (weight 2.0) should get 250 * (2/3) ≈ 166.67
assert.Equal(t, float64(50), comp2.GetBounds().Y())
assert.InDelta(t, 83.33, comp2.GetBounds().Height(), 0.1)
assert.InDelta(t, 133.33, comp3.GetBounds().Y(), 0.1)
assert.InDelta(t, 166.67, comp3.GetBounds().Height(), 0.1)
}
func TestContainerConcurrency(t *testing.T) {
container := NewBaseContainer()
var wg sync.WaitGroup
var counter int32
// Test concurrent component addition/removal
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
child := NewBaseComponent()
container.AddComponent(child)
atomic.AddInt32(&counter, 1)
container.RemoveComponent(child)
atomic.AddInt32(&counter, -1)
}()
}
wg.Wait()
assert.Equal(t, int32(0), atomic.LoadInt32(&counter), "All components should be added and removed")
assert.Empty(t, container.GetComponents(), "Container should be empty after concurrent operations")
}
func BenchmarkContainer(b *testing.B) {
b.Run("ComponentOperations", func(b *testing.B) {
container := NewBaseContainer()
child := NewBaseComponent()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if i%2 == 0 {
container.AddComponent(child)
} else {
container.RemoveComponent(child)
}
}
})
b.Run("EventDispatch", func(b *testing.B) {
container := NewBaseContainer()
child := NewBaseComponent()
container.AddComponent(child)
handler := event.EventHandlerFunc(func(event event.Event) bool {
return true
})
container.AddEventListener(event.TypeMouseMove, event.EventPhaseTarget, handler, 0)
evt := event.New(event.TypeMouseMove, container)
b.ResetTimer()
for i := 0; i < b.N; i++ {
container.DispatchEvent(evt)
}
})
}
func contains(s []Component, e Component) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
```
# File: layout_test.go
```go
package base
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestLayoutConstraints(t *testing.T) {
t.Run("DefaultValues", func(t *testing.T) {
constraints := NewLayoutConstraints()
assert.Equal(t, float64(0), constraints.MinWidth())
assert.Equal(t, float64(0), constraints.MinHeight())
assert.Equal(t, float64(0), constraints.MaxWidth())
assert.Equal(t, float64(0), constraints.MaxHeight())
})
t.Run("SetAndGet", func(t *testing.T) {
constraints := NewLayoutConstraints()
// Test min width
constraints.SetMinWidth(100)
assert.Equal(t, float64(100), constraints.MinWidth())
// Test min height
constraints.SetMinHeight(200)
assert.Equal(t, float64(200), constraints.MinHeight())
// Test max width
constraints.SetMaxWidth(300)
assert.Equal(t, float64(300), constraints.MaxWidth())
// Test max height
constraints.SetMaxHeight(400)
assert.Equal(t, float64(400), constraints.MaxHeight())
})
t.Run("Validation", func(t *testing.T) {
constraints := NewLayoutConstraints()
// Valid constraints
constraints.SetMinWidth(100)
constraints.SetMaxWidth(200)
constraints.SetMinHeight(100)
constraints.SetMaxHeight(200)
assert.NoError(t, constraints.Validate())
// Invalid min/max width
constraints.SetMinWidth(300)
assert.Error(t, constraints.Validate())
constraints.SetMinWidth(100)
// Invalid min/max height
constraints.SetMinHeight(300)
assert.Error(t, constraints.Validate())
})
}
func TestLayoutManager(t *testing.T) {
t.Run("HorizontalLayout", func(t *testing.T) {
container := NewBaseContainer()
container.SetBounds(NewBounds(0, 0, 300, 100))
// Add components
comp1 := NewBaseComponent()
comp1.SetBounds(NewBounds(0, 0, 100, 100))
comp2 := NewBaseComponent()
comp2.SetBounds(NewBounds(0, 0, 100, 100))
comp3 := NewBaseComponent()
comp3.SetBounds(NewBounds(0, 0, 100, 100))
container.AddComponent(comp1)
container.AddComponent(comp2)
container.AddComponent(comp3)
// Test layout
container.LayoutHorizontal()
// Check positions
assert.Equal(t, float64(0), comp1.GetBounds().X())
assert.Equal(t, float64(100), comp2.GetBounds().X())
assert.Equal(t, float64(200), comp3.GetBounds().X())
})
t.Run("VerticalLayout", func(t *testing.T) {
container := NewBaseContainer()
container.SetBounds(NewBounds(0, 0, 100, 300))
// Add components
comp1 := NewBaseComponent()
comp1.SetBounds(NewBounds(0, 0, 100, 100))
comp2 := NewBaseComponent()
comp2.SetBounds(NewBounds(0, 0, 100, 100))
comp3 := NewBaseComponent()
comp3.SetBounds(NewBounds(0, 0, 100, 100))
container.AddComponent(comp1)
container.AddComponent(comp2)
container.AddComponent(comp3)
// Test layout
container.LayoutVertical()
// Check positions
assert.Equal(t, float64(0), comp1.GetBounds().Y())
assert.Equal(t, float64(100), comp2.GetBounds().Y())
assert.Equal(t, float64(200), comp3.GetBounds().Y())
})
t.Run("GridLayout", func(t *testing.T) {
container := NewBaseContainer()
container.SetBounds(NewBounds(0, 0, 200, 200))
// Add components
comp1 := NewBaseComponent()
comp2 := NewBaseComponent()
comp3 := NewBaseComponent()
comp4 := NewBaseComponent()
container.AddComponent(comp1)
container.AddComponent(comp2)
container.AddComponent(comp3)
container.AddComponent(comp4)
// Test layout with 2 columns
container.LayoutGrid(2)
// Check positions (2x2 grid)
assert.Equal(t, float64(0), comp1.GetBounds().X())
assert.Equal(t, float64(0), comp1.GetBounds().Y())
assert.Equal(t, float64(100), comp2.GetBounds().X())
assert.Equal(t, float64(0), comp2.GetBounds().Y())
assert.Equal(t, float64(0), comp3.GetBounds().X())
assert.Equal(t, float64(100), comp3.GetBounds().Y())
assert.Equal(t, float64(100), comp4.GetBounds().X())
assert.Equal(t, float64(100), comp4.GetBounds().Y())
})
}
```
# File: layout.go
```go
// Package base provides core UI components and interfaces
package base
import (
"fmt"
)
// Orientation represents component orientation
type Orientation int
const (
// Horizontal orientation arranges components from left to right
Horizontal Orientation = iota
// Vertical orientation arranges components from top to bottom
Vertical
)
// Alignment represents component alignment options
type Alignment int
const (
AlignStart Alignment = iota // Left or top alignment
AlignCenter // Center alignment
AlignEnd // Right or bottom alignment
AlignStretch // Stretch to fill available space
)
// Margins represents margins for a component
type Margins struct {
Top float64
Right float64
Bottom float64
Left float64
}
// NewMargins creates a new Margins instance
func NewMargins(top, right, bottom, left float64) Margins {
return Margins{
Top: top,
Right: right,
Bottom: bottom,
Left: left,
}
}
// BaseLayoutConstraints defines layout constraints for a component
type BaseLayoutConstraints struct {
minWidth float64
maxWidth float64
minHeight float64
maxHeight float64
orientation Orientation
alignment Alignment
margin Margins
padding Margins
}
// NewBaseLayoutConstraints creates a new BaseLayoutConstraints instance
func NewBaseLayoutConstraints() *BaseLayoutConstraints {
return &BaseLayoutConstraints{
minWidth: 0,
maxWidth: -1, // -1 means no limit
minHeight: 0,
maxHeight: -1,
orientation: Horizontal,
alignment: AlignStart,
margin: Margins{},
padding: Margins{},
}
}
// Validate validates the layout constraints
func (c *BaseLayoutConstraints) Validate() error {
if c.minWidth < 0 {
return fmt.Errorf("minimum width cannot be negative")
}
if c.maxWidth >= 0 && c.maxWidth < c.minWidth {
return fmt.Errorf("maximum width cannot be less than minimum width")
}
if c.minHeight < 0 {
return fmt.Errorf("minimum height cannot be negative")
}
if c.maxHeight >= 0 && c.maxHeight < c.minHeight {
return fmt.Errorf("maximum height cannot be less than minimum height")
}
return nil
}
// Getters and setters
func (c *BaseLayoutConstraints) MinWidth() float64 {
return c.minWidth
}
func (c *BaseLayoutConstraints) SetMinWidth(width float64) {
c.minWidth = width
}
func (c *BaseLayoutConstraints) MaxWidth() float64 {
return c.maxWidth
}
func (c *BaseLayoutConstraints) SetMaxWidth(width float64) {
c.maxWidth = width
}
func (c *BaseLayoutConstraints) MinHeight() float64 {
return c.minHeight
}
func (c *BaseLayoutConstraints) SetMinHeight(height float64) {
c.minHeight = height
}
func (c *BaseLayoutConstraints) MaxHeight() float64 {
return c.maxHeight
}
func (c *BaseLayoutConstraints) SetMaxHeight(height float64) {
c.maxHeight = height
}
func (c *BaseLayoutConstraints) Orientation() Orientation {
return c.orientation
}
func (c *BaseLayoutConstraints) SetOrientation(orientation Orientation) {
c.orientation = orientation
}
func (c *BaseLayoutConstraints) Alignment() Alignment {
return c.alignment
}
func (c *BaseLayoutConstraints) SetAlignment(alignment Alignment) {
c.alignment = alignment
}
func (c *BaseLayoutConstraints) Margin() Margins {
return c.margin
}
func (c *BaseLayoutConstraints) SetMargin(margin Margins) {
c.margin = margin
}
func (c *BaseLayoutConstraints) Padding() Margins {
return c.padding
}
func (c *BaseLayoutConstraints) SetPadding(padding Margins) {
c.padding = padding
}
```
# File: interactive.go
```go
package base
import (
"github.com/hajimehoshi/ebiten/v2"
"github.com/decker502/plants-vs-zombies/internal/ui/event"
)
// InteractiveComponent represents an interactive UI component
type InteractiveComponent struct {
*BaseComponent
enabled bool
visible bool
focused bool
isHovered bool
isPressed bool
eventPool event.Pool
}
// NewInteractiveComponent creates a new interactive component
func NewInteractiveComponent() *InteractiveComponent {
c := &InteractiveComponent{
BaseComponent: NewBaseComponent(),
enabled: true,
visible: true,
eventPool: event.NewPool(),
}
return c
}
// OnMouseMove 处理鼠标移动事件
func (c *InteractiveComponent) OnMouseMove(handler event.Handler) {
c.BaseComponent.OnMouseMove(handler)
}
// OnMouseButtonDown 处理鼠标按下事件
func (c *InteractiveComponent) OnMouseButtonDown(handler event.Handler) {
c.BaseComponent.OnMouseButtonDown(handler)
}
// OnMouseButtonUp 处理鼠标释放事件
func (c *InteractiveComponent) OnMouseButtonUp(handler event.Handler) {
c.BaseComponent.OnMouseButtonUp(handler)
}
// OnClick 处理鼠标点击事件
func (c *InteractiveComponent) OnClick(handler event.Handler) {
c.BaseComponent.OnClick(handler)
}
// OnMouseEnter 处理鼠标进入事件
func (c *InteractiveComponent) OnMouseEnter(handler event.Handler) {
c.BaseComponent.OnMouseEnter(handler)
}
// OnMouseLeave 处理鼠标离开事件
func (c *InteractiveComponent) OnMouseLeave(handler event.Handler) {
c.BaseComponent.OnMouseLeave(handler)
}
// OnKeyPress 处理按键按下事件
func (c *InteractiveComponent) OnKeyPress(handler event.Handler) {
c.BaseComponent.OnKeyPress(handler)
}
// OnKeyRelease 处理按键释放事件
func (c *InteractiveComponent) OnKeyRelease(handler event.Handler) {
c.BaseComponent.OnKeyRelease(handler)
}
// OnFocusGain 处理焦点获得事件
func (c *InteractiveComponent) OnFocusGain(handler event.Handler) {
c.BaseComponent.OnFocusGain(handler)
}
// OnFocusLoss 处理焦点失去事件
func (c *InteractiveComponent) OnFocusLoss(handler event.Handler) {
c.BaseComponent.OnFocusLoss(handler)
}
// OnMouseClick adds a click event handler
func (c *InteractiveComponent) OnMouseClick(handler event.Handler) {
c.BaseComponent.OnMouseClick(handler)
}
// HandleMouseEvent handles mouse events
func (c *InteractiveComponent) HandleMouseEvent(e event.MouseEvent) bool {
if !c.enabled || !c.visible {
return false
}
// Check if the event is within our bounds
if !c.Contains(e.X(), e.Y()) {
if c.isHovered {
c.isHovered = false
leaveEvent := c.eventPool.GetMouseEvent(event.EventTypeMouseLeave, c, e.X(), e.Y(), e.Button())
c.DispatchEvent(leaveEvent)
c.eventPool.PutEvent(leaveEvent)
}
return false
}
// Handle hover state
if !c.isHovered {
c.isHovered = true
enterEvent := c.eventPool.GetMouseEvent(event.EventTypeMouseEnter, c, e.X(), e.Y(), e.Button())
c.DispatchEvent(enterEvent)
c.eventPool.PutEvent(enterEvent)
}
// Handle mouse button events
switch e.Type() {
case event.EventTypeMouseButtonDown:
c.isPressed = true
return c.DispatchEvent(e)
case event.EventTypeMouseButtonUp:
wasPressed := c.isPressed
c.isPressed = false
if wasPressed {
clickEvent := c.eventPool.GetMouseEvent(event.EventTypeClick, c, e.X(), e.Y(), e.Button())
c.DispatchEvent(clickEvent)
c.eventPool.PutEvent(clickEvent)
}
return c.DispatchEvent(e)
case event.EventTypeMouseMove:
return c.DispatchEvent(e)
}
return false
}
// HandleKeyEvent handles keyboard events
func (c *InteractiveComponent) HandleKeyEvent(e event.KeyEvent) bool {
if !c.enabled || !c.visible {
return false
}
// Key events only go to focused components
if !c.focused {
return false
}
// Dispatch the event
return c.DispatchEvent(e)
}
// HandleKeyPress 处理按键按下事件
func (c *InteractiveComponent) HandleKeyPress(key ebiten.Key) bool {
if !c.enabled || !c.visible || !c.focused {
return false
}
e := c.eventPool.GetKeyEvent(event.EventTypeKeyPress, c, key)
defer c.eventPool.PutEvent(e)
return c.DispatchEvent(e)
}
// AddEventListener 添加事件监听器
func (c *InteractiveComponent) AddEventListener(eventType event.EventType, phase event.EventPhase, handler event.Handler, priority int) {
c.BaseComponent.AddEventListener(eventType, phase, handler, priority)
}
// RemoveEventListener 移除事件监听器
func (c *InteractiveComponent) RemoveEventListener(eventType event.EventType, phase event.EventPhase, handler event.Handler) {
c.BaseComponent.RemoveEventListener(eventType, phase, handler)
}
// Update 更新组件状态
func (c *InteractiveComponent) Update() error {
if !c.enabled || !c.visible {
return nil
}
x, y := ebiten.CursorPosition()
contains := c.Contains(float64(x), float64(y))
// Handle mouse enter/leave
if contains != c.isHovered {
c.isHovered = contains
if contains {
e := c.eventPool.GetMouseEvent(event.EventTypeMouseEnter, c, float64(x), float64(y), ebiten.MouseButtonLeft)
c.DispatchEvent(e)
c.eventPool.PutEvent(e)
} else {
e := c.eventPool.GetMouseEvent(event.EventTypeMouseLeave, c, float64(x), float64(y), ebiten.MouseButtonLeft)
c.DispatchEvent(e)
c.eventPool.PutEvent(e)
}
}
return nil
}
// Focus 使组件获得焦点
func (c *InteractiveComponent) Focus() {
if !c.focused && c.enabled {
c.focused = true
e := c.eventPool.GetBaseEvent(event.EventTypeFocusGain, c)
c.DispatchEvent(e)
c.eventPool.PutEvent(e)
}
}
// Blur 使组件失去焦点
func (c *InteractiveComponent) Blur() {
if c.focused {
c.focused = false
e := c.eventPool.GetBaseEvent(event.EventTypeFocusLoss, c)
c.DispatchEvent(e)
c.eventPool.PutEvent(e)
}
}
// IsFocused 返回组件是否获得焦点
func (c *InteractiveComponent) IsFocused() bool {
return c.focused
}
// IsHovered 返回组件是否被鼠标悬停
func (c *InteractiveComponent) IsHovered() bool {
return c.isHovered
}
// IsPressed 返回组件是否被鼠标按下
func (c *InteractiveComponent) IsPressed() bool {
return c.isPressed
}
// SetEnabled 重写基础组件的启用/禁用方法
// enabled: 是否启用组件
func (c *InteractiveComponent) SetEnabled(enabled bool) {
if c.enabled == enabled {
return
}
c.enabled = enabled
if !enabled {
if c.isHovered {
c.isHovered = false
c.DispatchEvent(c.eventPool.GetBaseEvent(event.EventTypeMouseLeave, c))
c.eventPool.PutEvent(c.eventPool.GetBaseEvent(event.EventTypeMouseLeave, c))
}
if c.focused {
c.Blur()
}
}
}
// Contains 检查一个点是否在组件的边界内
// x, y: 要检查的点的坐标
// 返回值: 如果点在组件内返回 true,否则返回 false
func (c *InteractiveComponent) Contains(x, y float64) bool {
bounds := c.GetBounds()
return x >= float64(bounds.X()) &&
x <= float64(bounds.X()+bounds.Width()) &&
y >= float64(bounds.Y()) &&
y <= float64(bounds.Y()+bounds.Height())
}
// Parent returns the parent component
func (c *InteractiveComponent) Parent() Component {
return c.BaseComponent.parent
}
// SetParent sets the parent component
func (c *InteractiveComponent) SetParent(parent Container) {
c.BaseComponent.SetParent(parent)
}
// GetLayoutConstraints returns the layout constraints
func (c *InteractiveComponent) GetLayoutConstraints() *BaseLayoutConstraints {
return c.BaseComponent.constraints
}
// SetLayoutConstraints sets the layout constraints
func (c *InteractiveComponent) SetLayoutConstraints(constraints *BaseLayoutConstraints) {
c.BaseComponent.constraints = constraints
}
// IsFlexible returns whether the component is flexible
func (c *InteractiveComponent) IsFlexible() bool {
return c.BaseComponent.flexible
}
// GetFlexWeight returns the flex weight of the component
func (c *InteractiveComponent) GetFlexWeight() float64 {
return c.BaseComponent.flexWeight
}
// Layout performs layout calculations
func (c *InteractiveComponent) Layout() {
// Layout is handled by the parent container
}
```
# File: bounds.go
```go
package base
import (
"fmt"
)
// BaseBounds defines the position and size of a component
type BaseBounds struct {
x float64
y float64
width float64
height float64
}
// NewBounds creates a new BaseBounds instance
func NewBounds(x, y, width, height float64) *BaseBounds {
return &BaseBounds{
x: x,
y: y,
width: width,
height: height,
}
}
// X returns the x coordinate
func (b *BaseBounds) X() float64 { return b.x }
// Y returns the y coordinate
func (b *BaseBounds) Y() float64 { return b.y }
// Width returns the width
func (b *BaseBounds) Width() float64 { return b.width }
// Height returns the height
func (b *BaseBounds) Height() float64 { return b.height }
// SetX sets the x coordinate
func (b *BaseBounds) SetX(x float64) { b.x = x }
// SetY sets the y coordinate
func (b *BaseBounds) SetY(y float64) { b.y = y }
// SetWidth sets the width
func (b *BaseBounds) SetWidth(width float64) { b.width = width }
// SetHeight sets the height
func (b *BaseBounds) SetHeight(height float64) { b.height = height }
// Contains checks if a point is within the bounds
func (b *BaseBounds) Contains(x, y float64) bool {
return x >= b.x && x <= b.x+b.width && y >= b.y && y <= b.y+b.height
}
// Intersects checks if this bounds intersects with another bounds
func (b *BaseBounds) Intersects(other *BaseBounds) bool {
return !(b.x+b.width < other.x || b.x > other.x+other.width ||
b.y+b.height < other.y || b.y > other.y+other.height)
}
// String returns a string representation of the bounds
func (b *BaseBounds) String() string {
return fmt.Sprintf("Bounds(x: %.2f, y: %.2f, width: %.2f, height: %.2f)",
b.x, b.y, b.width, b.height)
}
```
# File: interfaces.go
```go
// Package base provides base UI components and interfaces
package base
import (
"github.com/hajimehoshi/ebiten/v2"
"github.com/decker502/plants-vs-zombies/internal/ui/event"
)
// Component represents a UI element that can be rendered and interacted with
type Component interface {
event.Component
// Core functionality
Init()
Update() error
Draw(screen *ebiten.Image)
Destroy()
// Event handling
HandleEvent(event event.Event) bool
DispatchEvent(event event.Event) bool
HandleMouseEvent(event event.MouseEvent) bool
HandleKeyEvent(event event.KeyEvent) bool
// State management
IsEnabled() bool
SetEnabled(enabled bool)
IsVisible() bool
SetVisible(visible bool)
IsFocused() bool
SetFocused(focused bool)
// Bounds and layout
GetBounds() *BaseBounds
SetBounds(bounds *BaseBounds)
IsFlexible() bool
GetFlexWeight() float64
GetLayoutManager() LayoutManager
SetLayoutManager(manager LayoutManager)
// Layout constraints
GetLayoutConstraints() *BaseLayoutConstraints
SetLayoutConstraints(constraints *BaseLayoutConstraints)
// Identification
GetID() string
SetID(id string)
// Component interface methods
Parent() event.Component
SetParent(parent event.Component)
Children() []event.Component
AddChild(child event.Component)
RemoveChild(child event.Component)
Position() (x, y float64)
Size() (width, height float64)
Contains(x, y float64) bool
}
// Container represents a component that can contain other components
type Container interface {
Component
// Container-specific functionality
GetParent() Container
SetParent(parent Container)
GetChildren() []Component
AddChild(child Component)
RemoveChild(child Component)
FindChild(id string) Component
AddComponent(component Component)
RemoveComponent(component Component)
GetComponents() []Component
}
// LayoutManager interface for managing component layout
type LayoutManager interface {
Layout(container Container)
GetPreferredSize(container Container) (width, height float64)
}
// Horizontal layout manager
type HorizontalLayoutManager interface {
LayoutManager
SetSpacing(spacing float64)
GetSpacing() float64
}
// Vertical layout manager
type VerticalLayoutManager interface {
LayoutManager
SetSpacing(spacing float64)
GetSpacing() float64
}
// Grid layout manager
type GridLayoutManager interface {
LayoutManager
SetColumns(columns int)
GetColumns() int
SetSpacing(horizontalSpacing, verticalSpacing float64)
GetSpacing() (horizontalSpacing, verticalSpacing float64)
}
```
# File: debug_test.go
```go
package base
import (
"image/color"
"testing"
"github.com/hajimehoshi/ebiten/v2"
"github.com/stretchr/testify/assert"
)
func TestLayoutDebugger(t *testing.T) {
t.Run("NewLayoutDebugger", func(t *testing.T) {
debugger := NewLayoutDebugger()
assert.NotNil(t, debugger, "NewLayoutDebugger should not return nil")
assert.False(t, debugger.IsEnabled(), "Debugger should be disabled by default")
assert.Equal(t, color.RGBA{R: 255, G: 0, B: 0, A: 128}, debugger.color, "Default color should be red with 50% alpha")
})
t.Run("Enable/Disable", func(t *testing.T) {
debugger := NewLayoutDebugger()
debugger.SetEnabled(true)
assert.True(t, debugger.IsEnabled(), "Debugger should be enabled")
debugger.SetEnabled(false)
assert.False(t, debugger.IsEnabled(), "Debugger should be disabled")
})
t.Run("SetColor", func(t *testing.T) {
debugger := NewLayoutDebugger()
testColor := color.RGBA{R: 0, G: 255, B: 0, A: 255}
debugger.SetColor(testColor)
assert.Equal(t, testColor, debugger.color, "Color should be updated")
})
t.Run("DrawDebugInfo", func(t *testing.T) {
debugger := NewLayoutDebugger()
screen := ebiten.NewImage(100, 100)
component := NewBaseComponent()
component.SetBounds(NewBounds(10, 10, 80, 80))
// Test when disabled
debugger.SetEnabled(false)
debugger.DrawDebugInfo(screen, component)
// No assertions needed as disabled debugger shouldn't draw anything
// Test when enabled
debugger.SetEnabled(true)
debugger.DrawDebugInfo(screen, component)
// Visual output testing would require image comparison
})
t.Run("DrawLayoutConstraints", func(t *testing.T) {
debugger := NewLayoutDebugger()
screen := ebiten.NewImage(100, 100)
component := NewBaseComponent()
component.SetBounds(NewBounds(10, 10, 80, 80))
constraints := NewLayoutConstraints()
constraints.SetMinWidth(50)
constraints.SetMinHeight(50)
constraints.SetMaxWidth(100)
constraints.SetMaxHeight(100)
component.SetLayoutConstraints(constraints)
// Test when disabled
debugger.SetEnabled(false)
debugger.DrawLayoutConstraints(screen, component)
// No assertions needed as disabled debugger shouldn't draw anything
// Test when enabled
debugger.SetEnabled(true)
debugger.DrawLayoutConstraints(screen, component)
// Visual output testing would require image comparison
})
t.Run("Container Debug Info", func(t *testing.T) {
debugger := NewLayoutDebugger()
screen := ebiten.NewImage(200, 200)
container := NewBaseContainer()
container.SetBounds(NewBounds(0, 0, 200, 200))
child1 := NewBaseComponent()
child1.SetBounds(NewBounds(10, 10, 80, 80))
container.AddComponent(child1)
child2 := NewBaseComponent()
child2.SetBounds(NewBounds(100, 100, 80, 80))
container.AddComponent(child2)
debugger.SetEnabled(true)
debugger.DrawDebugInfo(screen, container)
// Visual output testing would require image comparison
})
}
func TestComponentBoundsOverlap(t *testing.T) {
t.Run("Overlapping Components", func(t *testing.T) {
comp1 := NewBaseComponent()
comp2 := NewBaseComponent()
comp1.SetBounds(NewBounds(0, 0, 100, 100))
comp2.SetBounds(NewBounds(50, 50, 100, 100))
bounds1 := comp1.GetBounds()
bounds2 := comp2.GetBounds()
assert.True(t, bounds1.Intersects(bounds2), "Components should overlap")
})
t.Run("Non-overlapping Components", func(t *testing.T) {
comp1 := NewBaseComponent()
comp2 := NewBaseComponent()
comp1.SetBounds(NewBounds(0, 0, 100, 100))
comp2.SetBounds(NewBounds(150, 150, 100, 100))
bounds1 := comp1.GetBounds()
bounds2 := comp2.GetBounds()
assert.False(t, bounds1.Intersects(bounds2), "Components should not overlap")
})
t.Run("Adjacent Components", func(t *testing.T) {
comp1 := NewBaseComponent()
comp2 := NewBaseComponent()
comp1.SetBounds(NewBounds(0, 0, 100, 100))
comp2.SetBounds(NewBounds(100, 0, 100, 100))
bounds1 := comp1.GetBounds()
bounds2 := comp2.GetBounds()
assert.False(t, bounds1.Intersects(bounds2), "Adjacent components should not overlap")
})
t.Run("Contained Components", func(t *testing.T) {
comp1 := NewBaseComponent()
comp2 := NewBaseComponent()
comp1.SetBounds(NewBounds(0, 0, 100, 100))
comp2.SetBounds(NewBounds(25, 25, 50, 50))
bounds1 := comp1.GetBounds()
bounds2 := comp2.GetBounds()
assert.True(t, bounds1.Intersects(bounds2), "Contained components should overlap")
})
}
```
# File: types.go
```go
package base
import (
"fmt"
)
// Point represents a 2D point with x and y coordinates
type Point struct {
x float64
y float64
}
// NewPoint creates a new Point
func NewPoint(x, y float64) Point {
return Point{x: x, y: y}
}
// X returns the x coordinate
func (p Point) X() float64 {
return p.x
}
// Y returns the y coordinate
func (p Point) Y() float64 {
return p.y
}
// SetX sets the x coordinate
func (p *Point) SetX(x float64) {
p.x = x
}
// SetY sets the y coordinate
func (p *Point) SetY(y float64) {
p.y = y
}
// Equals returns true if the points are equal
func (p Point) Equals(other Point) bool {
return p.x == other.x && p.y == other.y
}
// Direction represents a layout direction
type Direction int
const (
// DirectionHorizontal represents horizontal direction
DirectionHorizontal Direction = iota
// DirectionVertical represents vertical direction
DirectionVertical
)
// Size represents a 2D size with width and height
type Size struct {
width float64
height float64
}
// NewSize creates a new Size
func NewSize(width, height float64) Size {
return Size{width: width, height: height}
}
// Width returns the width
func (s Size) Width() float64 {
return s.width
}
// Height returns the height
func (s Size) Height() float64 {
return s.height
}
// SetWidth sets the width
func (s *Size) SetWidth(width float64) {
s.width = width
}
// SetHeight sets the height
func (s *Size) SetHeight(height float64) {
s.height = height
}
// Equals returns true if the sizes are equal
func (s Size) Equals(other Size) bool {
return s.width == other.width && s.height == other.height
}
// Color represents an RGBA color
type Color struct {
r, g, b, a float64
}
// NewColor creates a new Color instance
func NewColor(r, g, b, a float64) Color {
return Color{r: r, g: g, b: b, a: a}
}
// R returns the red component
func (c Color) R() float64 {
return c.r
}
// G returns the green component
func (c Color) G() float64 {
return c.g
}
// B returns the blue component
func (c Color) B() float64 {
return c.b
}
// A returns the alpha component
func (c Color) A() float64 {
return c.a
}
// SetR sets the red component
func (c *Color) SetR(r float64) {
c.r = r
}
// SetG sets the green component
func (c *Color) SetG(g float64) {
c.g = g
}
// SetB sets the blue component
func (c *Color) SetB(b float64) {
c.b = b
}
// SetA sets the alpha component
func (c *Color) SetA(a float64) {
c.a = a
}
// Equals returns true if the colors are equal
func (c Color) Equals(other Color) bool {
return c.r == other.r && c.g == other.g && c.b == other.b && c.a == other.a
}
```
# File: types_test.go
```go
package base
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestBounds(t *testing.T) {
t.Run("Basic bounds operations", func(t *testing.T) {
bounds := NewBounds(10, 20, 100, 200)
// Test getters
assert.Equal(t, float64(10), bounds.X())
assert.Equal(t, float64(20), bounds.Y())
assert.Equal(t, float64(100), bounds.Width())
assert.Equal(t, float64(200), bounds.Height())
// Test setters
bounds.SetX(30)
bounds.SetY(40)
bounds.SetWidth(150)
bounds.SetHeight(250)
assert.Equal(t, float64(30), bounds.X())
assert.Equal(t, float64(40), bounds.Y())
assert.Equal(t, float64(150), bounds.Width())
assert.Equal(t, float64(250), bounds.Height())
})
t.Run("Contains point", func(t *testing.T) {
bounds := NewBounds(10, 20, 100, 200)
// Test points inside bounds
assert.True(t, bounds.ContainsPoint(NewPoint(15, 25)))
assert.True(t, bounds.ContainsPoint(NewPoint(50, 100)))
assert.True(t, bounds.ContainsPoint(NewPoint(109, 219)))
// Test points outside bounds
assert.False(t, bounds.ContainsPoint(NewPoint(5, 25)))
assert.False(t, bounds.ContainsPoint(NewPoint(15, 15)))
assert.False(t, bounds.ContainsPoint(NewPoint(111, 100)))
assert.False(t, bounds.ContainsPoint(NewPoint(50, 221)))
})
t.Run("Bounds intersection", func(t *testing.T) {
bounds1 := NewBounds(10, 20, 100, 200)
bounds2 := NewBounds(50, 60, 100, 200)
bounds3 := NewBounds(200, 300, 100, 200)
// Test intersecting bounds
assert.True(t, bounds1.Intersects(bounds2))
assert.True(t, bounds2.Intersects(bounds1))
// Test non-intersecting bounds
assert.False(t, bounds1.Intersects(bounds3))
assert.False(t, bounds3.Intersects(bounds1))
})
}
func TestPoint(t *testing.T) {
t.Run("Basic point operations", func(t *testing.T) {
point := NewPoint(10, 20)
// Test getters
assert.Equal(t, float64(10), point.X())
assert.Equal(t, float64(20), point.Y())
// Test setters
point.SetX(30)
point.SetY(40)
assert.Equal(t, float64(30), point.X())
assert.Equal(t, float64(40), point.Y())
})
t.Run("Point comparison", func(t *testing.T) {
point1 := NewPoint(10, 20)
point2 := NewPoint(10, 20)
point3 := NewPoint(30, 40)
// Test equal points
assert.True(t, point1.Equals(point2))
assert.True(t, point2.Equals(point1))
// Test unequal points
assert.False(t, point1.Equals(point3))
assert.False(t, point3.Equals(point1))
})
}
func TestSize(t *testing.T) {
t.Run("Basic size operations", func(t *testing.T) {
size := NewSize(100, 200)
// Test getters
assert.Equal(t, float64(100), size.Width())
assert.Equal(t, float64(200), size.Height())
// Test setters
size.SetWidth(150)
size.SetHeight(250)
assert.Equal(t, float64(150), size.Width())
assert.Equal(t, float64(250), size.Height())
})
t.Run("Size comparison", func(t *testing.T) {
size1 := NewSize(100, 200)
size2 := NewSize(100, 200)
size3 := NewSize(150, 250)
// Test equal sizes
assert.True(t, size1.Equals(size2))
assert.True(t, size2.Equals(size1))
// Test unequal sizes
assert.False(t, size1.Equals(size3))
assert.False(t, size3.Equals(size1))
})
}
func TestMargins(t *testing.T) {
t.Run("Basic margins operations", func(t *testing.T) {
margins := NewMargins(10, 20, 30, 40)
// Test values
assert.Equal(t, float64(10), margins.Top)
assert.Equal(t, float64(20), margins.Right)
assert.Equal(t, float64(30), margins.Bottom)
assert.Equal(t, float64(40), margins.Left)
// Test spacing calculations
assert.Equal(t, float64(60), margins.GetHorizontalSpacing())
assert.Equal(t, float64(40), margins.GetVerticalSpacing())
})
}
func TestColor(t *testing.T) {
t.Run("Basic color operations", func(t *testing.T) {
color1 := NewColor(1, 0, 0, 1)
color2 := NewColor(1, 0, 0, 1)
color3 := NewColor(0, 1, 0, 1)
// Test getters
assert.Equal(t, float64(1), color1.R())
assert.Equal(t, float64(0), color1.G())
assert.Equal(t, float64(0), color1.B())
assert.Equal(t, float64(1), color1.A())
// Test setters
color1.SetR(0.5)
color1.SetG(0.6)
color1.SetB(0.7)
color1.SetA(0.8)
assert.Equal(t, float64(0.5), color1.R())
assert.Equal(t, float64(0.6), color1.G())
assert.Equal(t, float64(0.7), color1.B())
assert.Equal(t, float64(0.8), color1.A())
// Test equality
assert.True(t, color1.Equals(color1)) // Same instance
assert.True(t, color2.Equals(color2)) // Different instances, same values
assert.False(t, color1.Equals(color2)) // Different values
assert.False(t, color2.Equals(color1)) // Different values
assert.False(t, color1.Equals(color3))
assert.False(t, color3.Equals(color1))
})
}
```
# File: debug.go
```go
package base
import (
"fmt"
"image/color"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
)
// LayoutDebugger provides debugging information for UI layout
type LayoutDebugger struct {
enabled bool
color color.Color
}
// NewLayoutDebugger creates a new LayoutDebugger
func NewLayoutDebugger() *LayoutDebugger {
return &LayoutDebugger{
enabled: false,
color: color.RGBA{R: 255, G: 0, B: 0, A: 128},
}
}
// SetEnabled enables or disables the debugger
func (d *LayoutDebugger) SetEnabled(enabled bool) {
d.enabled = enabled
}
// IsEnabled returns whether the debugger is enabled
func (d *LayoutDebugger) IsEnabled() bool {
return d.enabled
}
// SetColor sets the debug color
func (d *LayoutDebugger) SetColor(c color.Color) {
d.color = c
}
// DrawDebugInfo draws debug information for a component
func (d *LayoutDebugger) DrawDebugInfo(screen *ebiten.Image, component Component) {
if !d.enabled {
return
}
bounds := component.GetBounds()
if bounds == nil {
return
}
// Draw component bounds
ebitenutil.DrawRect(screen, bounds.X(), bounds.Y(), bounds.Width(), bounds.Height(), d.color)
// Draw component info
info := fmt.Sprintf("Pos: (%.1f, %.1f)\nSize: %.1fx%.1f",
bounds.X(), bounds.Y(), bounds.Width(), bounds.Height())
ebitenutil.DebugPrintAt(screen, info, int(bounds.X()), int(bounds.Y()))
// If this is a container, draw debug info for children
if container, ok := component.(Container); ok {
for _, child := range container.GetComponents() {
d.DrawDebugInfo(screen, child)
}
}
}
// DrawLayoutConstraints draws debug information for layout constraints
func (d *LayoutDebugger) DrawLayoutConstraints(screen *ebiten.Image, component Component) {
if !d.enabled {
return
}
constraints := component.GetLayoutConstraints()
if constraints == nil {
return
}
bounds := component.GetBounds()
if bounds == nil {
return
}
// Draw min/max size info
info := fmt.Sprintf("Min: %.1fx%.1f\nMax: %.1fx%.1f",
constraints.MinWidth(), constraints.MinHeight(),
constraints.MaxWidth(), constraints.MaxHeight())
ebitenutil.DebugPrintAt(screen, info,
int(bounds.X()), int(bounds.Y()+bounds.Height()))
}
```
```