Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.23.0] - 2026-07-30

### Added

- **`InputEvent` sealed interface** (ADR-058) — unified event type for SDL-style event queue. Sealed via unexported `inputEventTag()` marker — only gpucontext types implement it. Enables exhaustive type switch handling.
- **`KeyEvent`** struct — keyboard key state change (Key, Modifiers, Pressed)
- **`CharEvent`** struct — committed text input (Char rune)
- **`FocusEvent`** struct — window focus state change (Focused bool)
- **`ResizeEvent`** struct — window content area size change (Width, Height in logical DIP)
- `PointerEvent` and `ScrollEvent` now implement `InputEvent` interface
- 8 test cases covering interface satisfaction, type switch, field access

## [0.22.0] - 2026-07-29

### Added
Expand Down
88 changes: 88 additions & 0 deletions input_event.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright 2026 The gogpu Authors
// SPDX-License-Identifier: MIT

package gpucontext

// InputEvent represents a discrete input event from the platform.
//
// InputEvent is a sealed interface — only types defined in this package
// implement it. This enables exhaustive handling via linters and guarantees
// the set of event types is known at compile time.
//
// Use a Go type switch to handle specific event types:
//
// for ev, ok := app.PollInputEvent(); ok; ev, ok = app.PollInputEvent() {
// switch e := ev.(type) {
// case gpucontext.KeyEvent:
// if e.Pressed && e.Key == gpucontext.KeyEscape {
// app.Quit()
// }
// case gpucontext.PointerEvent:
// if e.Type == gpucontext.PointerDown {
// handleClick(e.X, e.Y)
// }
// case gpucontext.ScrollEvent:
// handleScroll(e.DeltaX, e.DeltaY)
// }
// }
//
// Three input models coexist in gogpu — use whichever fits your use case:
// - Callbacks (EventSource): best for GUI frameworks
// - State polling (app.Input()): best for simple games (Ebiten-style)
// - Event queue (app.PollInputEvent()): best for complex games (SDL-style)
//
// Design references:
// - Qt6: QEvent in QtCore (shared core layer)
// - Bevy: bevy_input crate (separate from bevy_app)
// - Gio: io/event.Event with ImplementsEvent() marker
// - SDL3: SDL_PollEvent() + SDL_AppEvent() dual model
type InputEvent interface {
inputEventTag()
}

// KeyEvent represents a keyboard key state change.
//
// A KeyEvent is emitted for each physical key press and release. For text
// input (after keyboard layout and IME processing), use CharEvent instead.
//
// All coordinates and dimensions in the event system use logical DIP
// (device-independent pixels), consistent with WindowProvider.Size().
type KeyEvent struct {
Key Key
Mods Modifiers
Pressed bool // true = key down, false = key up
}

func (KeyEvent) inputEventTag() {}

// CharEvent represents committed text input.
//
// CharEvent is emitted after keyboard layout and input method processing.
// For CJK input, the composition preview is delivered via IME callbacks
// on EventSource; CharEvent contains only the final committed character.
//
// Use CharEvent for text fields. Use KeyEvent for keyboard shortcuts.
type CharEvent struct {
Char rune
}

func (CharEvent) inputEventTag() {}

// FocusEvent represents a window focus state change.
type FocusEvent struct {
Focused bool // true = window gained focus, false = lost focus
}

func (FocusEvent) inputEventTag() {}

// ResizeEvent represents a window content area size change.
//
// Width and Height are in logical DIP (device-independent pixels),
// consistent with WindowProvider.Size(). For physical pixel dimensions,
// multiply by ScaleFactor.
type ResizeEvent struct {
Width int
Height int
}

func (ResizeEvent) inputEventTag() {}
136 changes: 136 additions & 0 deletions input_event_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Copyright 2026 The gogpu Authors
// SPDX-License-Identifier: MIT

package gpucontext

import "testing"

func TestInputEventInterface(t *testing.T) {
events := []InputEvent{
KeyEvent{Key: KeyA, Mods: ModShift, Pressed: true},
KeyEvent{Key: KeyEscape, Pressed: false},
CharEvent{Char: 'A'},
FocusEvent{Focused: true},
FocusEvent{Focused: false},
ResizeEvent{Width: 800, Height: 600},
PointerEvent{Type: PointerDown, X: 100, Y: 200},
PointerEvent{Type: PointerMove, X: 150, Y: 250},
ScrollEvent{DeltaY: -3.0, DeltaMode: ScrollDeltaLine},
}

if len(events) != 9 {
t.Fatalf("expected 9 events, got %d", len(events))
}

for i, ev := range events {
if ev == nil {
t.Errorf("event %d is nil", i)
}
}
}

func TestInputEventTypeSwitch(t *testing.T) {
events := []InputEvent{
KeyEvent{Key: KeyA, Pressed: true},
PointerEvent{Type: PointerDown, X: 10, Y: 20},
ScrollEvent{DeltaY: 1.0},
CharEvent{Char: 'x'},
FocusEvent{Focused: true},
ResizeEvent{Width: 1024, Height: 768},
}

counts := map[string]int{}
for _, ev := range events {
switch ev.(type) {
case KeyEvent:
counts["key"]++
case PointerEvent:
counts["pointer"]++
case ScrollEvent:
counts["scroll"]++
case CharEvent:
counts["char"]++
case FocusEvent:
counts["focus"]++
case ResizeEvent:
counts["resize"]++
default:
t.Errorf("unexpected event type: %T", ev)
}
}

for _, name := range []string{"key", "pointer", "scroll", "char", "focus", "resize"} {
if counts[name] != 1 {
t.Errorf("expected 1 %s event, got %d", name, counts[name])
}
}
}

func TestKeyEventFields(t *testing.T) {
ev := KeyEvent{Key: KeyA, Mods: ModShift | ModControl, Pressed: true}
if ev.Key != KeyA {
t.Errorf("Key = %v, want KeyA", ev.Key)
}
if !ev.Mods.HasShift() {
t.Error("expected HasShift")
}
if !ev.Mods.HasControl() {
t.Error("expected HasControl")
}
if !ev.Pressed {
t.Error("expected Pressed = true")
}
}

func TestCharEventFields(t *testing.T) {
ev := CharEvent{Char: '日'}
if ev.Char != '日' {
t.Errorf("Char = %q, want '日'", ev.Char)
}
}

func TestFocusEventFields(t *testing.T) {
gained := FocusEvent{Focused: true}
lost := FocusEvent{Focused: false}
if !gained.Focused {
t.Error("expected Focused = true")
}
if lost.Focused {
t.Error("expected Focused = false")
}
}

func TestResizeEventFields(t *testing.T) {
ev := ResizeEvent{Width: 1920, Height: 1080}
if ev.Width != 1920 || ev.Height != 1080 {
t.Errorf("got %dx%d, want 1920x1080", ev.Width, ev.Height)
}
}

func TestPointerEventImplementsInputEvent(t *testing.T) {
var ev InputEvent = PointerEvent{Type: PointerDown, X: 50, Y: 100}
pe, ok := ev.(PointerEvent)
if !ok {
t.Fatal("PointerEvent does not implement InputEvent")
}
if pe.Type != PointerDown {
t.Errorf("Type = %v, want PointerDown", pe.Type)
}
if pe.X != 50 || pe.Y != 100 {
t.Errorf("coordinates = (%v, %v), want (50, 100)", pe.X, pe.Y)
}
}

func TestScrollEventImplementsInputEvent(t *testing.T) {
var ev InputEvent = ScrollEvent{DeltaY: -1.0, Phase: ScrollPhaseBegan}
se, ok := ev.(ScrollEvent)
if !ok {
t.Fatal("ScrollEvent does not implement InputEvent")
}
if se.DeltaY != -1.0 {
t.Errorf("DeltaY = %v, want -1.0", se.DeltaY)
}
if se.Phase != ScrollPhaseBegan {
t.Errorf("Phase = %v, want ScrollPhaseBegan", se.Phase)
}
}
2 changes: 2 additions & 0 deletions pointer.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ type PointerEvent struct {
Timestamp time.Duration
}

func (PointerEvent) inputEventTag() {}

// PointerEventType indicates the type of pointer event.
type PointerEventType uint8

Expand Down
2 changes: 2 additions & 0 deletions scroll.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ type ScrollEvent struct {
IsMomentum bool
}

func (ScrollEvent) inputEventTag() {}

// ScrollDeltaMode indicates the unit of scroll delta values.
type ScrollDeltaMode uint8

Expand Down
Loading