diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c2c26b..7e22f44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,48 @@ 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.8.0] - 2026-02-06 + +### Added + +- **WindowProvider interface** for window geometry and DPI integration + - `Size() (width, height int)` — window client area in physical pixels + - `ScaleFactor() float64` — DPI scale factor (1.0 = standard, 2.0 = Retina/HiDPI) + - `RequestRedraw()` — request a new frame in on-demand rendering mode + - `NullWindowProvider` — configurable defaults for testing and headless operation + +- **PlatformProvider interface** for OS integration features (optional) + - `ClipboardRead() (string, error)` — read text from system clipboard + - `ClipboardWrite(text string) error` — write text to system clipboard + - `SetCursor(cursor CursorShape)` — change mouse cursor shape + - `DarkMode() bool` — system dark mode detection + - `ReduceMotion() bool` — accessibility: reduced animation preference + - `HighContrast() bool` — accessibility: high contrast mode + - `FontScale() float32` — user's font size preference multiplier + - `NullPlatformProvider` — no-op defaults for testing + +- **CursorShape enum** with 12 standard cursor shapes + - Default, Pointer, Text, Crosshair, Move + - ResizeNS, ResizeEW, ResizeNWSE, ResizeNESW + - NotAllowed, Wait, None (hidden) + - `String()` method for debugging + +### Removed + +- **TouchEventSource interface** — replaced by PointerEventSource (W3C Pointer Events Level 3) + - `TouchID`, `TouchPhase`, `TouchPoint`, `TouchEvent` types removed + - `TouchEventSource` interface removed + - `NullTouchEventSource` removed + - Touch input is fully covered by `PointerEventSource` with `PointerType: Touch` + - W3C recommends Pointer Events over Touch Events (Touch Events is legacy) + +### Notes + +- PlatformProvider is **optional** — use type assertion on WindowProvider: + `if pp, ok := provider.(gpucontext.PlatformProvider); ok { ... }` +- These interfaces enable UI frameworks to access host window and platform + capabilities without direct dependency on gogpu + ## [0.7.0] - 2026-02-05 ### Added @@ -51,6 +93,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **TouchCancelled → TouchCanceled** — US English spelling (misspell linter) - Removed unused `DeviceHandle` alias +[0.8.0]: https://github.com/gogpu/gpucontext/releases/tag/v0.8.0 [0.7.0]: https://github.com/gogpu/gpucontext/releases/tag/v0.7.0 [0.6.0]: https://github.com/gogpu/gpucontext/releases/tag/v0.6.0 [0.5.0]: https://github.com/gogpu/gpucontext/releases/tag/v0.5.0 diff --git a/README.md b/README.md index 3293d3a..5ba0594 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,12 @@ go get github.com/gogpu/gpucontext ## Features - **DeviceProvider** — Interface for injecting GPU device and queue +- **WindowProvider** — Window geometry, DPI scale factor, and redraw requests +- **PlatformProvider** — Clipboard, cursor, dark mode, and accessibility preferences +- **CursorShape** — 12 standard cursor shapes (arrow, pointer, text, resize, etc.) - **EventSource** — Interface for input events (keyboard, mouse, window, IME) - **PointerEventSource** — W3C Pointer Events Level 3 (unified mouse/touch/pen) - **ScrollEventSource** — Scroll/wheel events with pixel/line/page modes -- **TouchEventSource** — Interface for multi-touch input (mobile, tablets, touchscreens) - **Texture** — Minimal interface for GPU textures with TextureUpdater/TextureDrawer/TextureCreator - **IME Support** — Input Method Editor for CJK languages (Chinese, Japanese, Korean) - **Registry[T]** — Generic registry with priority-based backend selection @@ -63,6 +65,54 @@ func NewGPUCanvas(provider gpucontext.DeviceProvider) *Canvas { } ``` +### WindowProvider (for UI frameworks) + +The `WindowProvider` interface enables UI frameworks to query window dimensions and DPI: + +```go +// In gogpu/ui - uses WindowProvider for layout +func (ui *UI) Layout(wp gpucontext.WindowProvider) { + w, h := wp.Size() + scale := wp.ScaleFactor() + dpW := float64(w) / scale + dpH := float64(h) / scale + ui.root.Layout(dpW, dpH) +} +``` + +### PlatformProvider (optional OS integration) + +`PlatformProvider` exposes clipboard, cursor, and system preferences. +Not all hosts support it — use type assertion to check: + +```go +// In gogpu/ui - cursor management +func (ui *UI) UpdateCursor(provider gpucontext.WindowProvider) { + if pp, ok := provider.(gpucontext.PlatformProvider); ok { + pp.SetCursor(gpucontext.CursorPointer) // hand cursor + } +} + +// In gogpu/ui - clipboard +func (ui *UI) Paste(provider gpucontext.WindowProvider) { + if pp, ok := provider.(gpucontext.PlatformProvider); ok { + text, err := pp.ClipboardRead() + if err == nil { + ui.focused.InsertText(text) + } + } +} + +// In gogpu/ui - theme detection +func (ui *UI) DetectTheme(provider gpucontext.WindowProvider) { + if pp, ok := provider.(gpucontext.PlatformProvider); ok { + if pp.DarkMode() { + ui.SetTheme(DarkTheme) + } + } +} +``` + ### EventSource (for UI frameworks) `EventSource` enables UI frameworks to receive input events from host applications: @@ -169,72 +219,6 @@ func (ctx *Context) DrawTexture(tex gpucontext.Texture, x, y float32) error { } ``` -### Touch Input (Multi-touch Support) - -`TouchEventSource` enables multi-touch handling for mobile and tablet applications: - -```go -// Touch phases follow platform conventions (iOS, Android, W3C) -const ( - TouchBegan // First contact - TouchMoved // Touch moved - TouchEnded // Touch lifted - TouchCanceled // System interrupted -) - -// TouchPoint represents a single touch contact -type TouchPoint struct { - ID TouchID // Unique within session - X, Y float64 // Position in logical pixels - Pressure *float32 // Optional: 0.0-1.0 - Radius *float32 // Optional: contact radius -} - -// TouchEvent contains all touch information -type TouchEvent struct { - Phase TouchPhase // Lifecycle stage - Changed []TouchPoint // Touches that triggered this event - All []TouchPoint // All active touches - Modifiers Modifiers // Keyboard modifiers (Ctrl+drag, etc.) - Timestamp time.Duration // For velocity calculations -} -``` - -Usage for gesture handling: - -```go -// Implement pinch-to-zoom -func (app *App) AttachTouchEvents(source gpucontext.EventSource) { - // Check if touch is supported - if tes, ok := source.(gpucontext.TouchEventSource); ok { - tes.OnTouch(func(ev gpucontext.TouchEvent) { - switch ev.Phase { - case gpucontext.TouchBegan: - app.startGesture(ev.Changed) - case gpucontext.TouchMoved: - if len(ev.All) == 2 { - // Pinch gesture - app.handlePinch(ev.All[0], ev.All[1]) - } else if len(ev.All) == 1 { - // Pan gesture - app.handlePan(ev.All[0]) - } - case gpucontext.TouchEnded, gpucontext.TouchCanceled: - app.endGesture() - } - }) - } -} - -// Calculate pinch distance -func (app *App) handlePinch(t1, t2 gpucontext.TouchPoint) { - dx := t1.X - t2.X - dy := t1.Y - t2.Y - distance := math.Sqrt(dx*dx + dy*dy) - app.zoom = distance / app.initialPinchDistance -} -``` - ### Backend Registry The `Registry[T]` provides thread-safe registration with priority-based selection: @@ -277,14 +261,15 @@ names := backends.Available() // ["vulkan", "software"] ▼ gpucontext (imports gputypes) - DeviceProvider, EventSource, Texture - TouchEventSource, Registry + DeviceProvider, WindowProvider, + PlatformProvider, EventSource, + Texture, PointerEventSource, Registry │ ┌───────────────┼───────────────┐ │ │ │ ▼ ▼ ▼ - gogpu gg born-ml/born - (implements) (uses) (implements & uses) + gogpu gg ui + (implements) (uses) (uses) │ ▼ wgpu/hal diff --git a/ROADMAP.md b/ROADMAP.md index 7873179..de67e54 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,17 +4,35 @@ `gpucontext` is the shared foundation for the [gogpu](https://github.com/gogpu) ecosystem, providing interfaces and utilities for GPU resource sharing without circular dependencies. -## Current: v0.3.1 +## Current: v0.8.0 -**Status:** Released +**Status:** In Development -- Import gputypes v0.2.0 (webgpu.h spec-compliant enums) -- DeviceProvider interface -- EventSource interface (with IME support) -- Registry[T] generic +- WindowProvider interface (window geometry, DPI, redraw requests) +- PlatformProvider interface (clipboard, cursor, dark mode, accessibility) +- CursorShape enum (12 standard cursor shapes) +- NullWindowProvider / NullPlatformProvider for testing ## Released +### v0.7.0 (2026-02-05) +- TextureUpdater interface for dynamic texture content + +### v0.6.0 (2026-01-31) +- Gesture events (GestureEvent, GestureEventSource) + +### v0.5.0 (2026-01-31) +- W3C Pointer Events Level 3 +- Scroll events with delta modes +- CI/CD infrastructure + +### v0.4.0 (2026-01-30) +- Texture interfaces (Texture, TextureDrawer, TextureCreator) +- Touch input support (multi-touch, pressure, radius) + +### v0.3.1 (2026-01-29) +- Update gputypes to v0.2.0 + ### v0.3.0 (2026-01-29) - Import gputypes for unified WebGPU types @@ -24,21 +42,12 @@ ### v0.1.1 (2026-01-27) - Initial release with DeviceProvider, EventSource, Registry -## Planned: v0.4.0 - -**Focus:** Extended capabilities - -- [ ] `Capabilities` interface for feature queries -- [ ] `ResourceLimits` struct for GPU limits -- [ ] `ShaderFormat` enum (WGSL, SPIR-V, GLSL) -- [ ] `BufferUsage`, `TextureUsage` flags - ## Future Considerations -### v0.4.0 — Compute Support -- ComputeProvider interface -- WorkgroupLimits -- Storage buffer types +### v0.9.0 — Extended Capabilities +- Capabilities interface for feature queries +- ResourceLimits for GPU limits +- ShaderFormat enum (WGSL, SPIR-V, GLSL) ### v1.0.0 — API Freeze - Stable API guarantee @@ -48,7 +57,7 @@ ## Non-Goals - This package will **never** contain implementations -- This package will **never** have external dependencies +- This package will **never** have external dependencies (beyond gputypes) - This package focuses on **interfaces**, not concrete types ## Contributing diff --git a/doc.go b/doc.go index a922e12..baad92e 100644 --- a/doc.go +++ b/doc.go @@ -5,8 +5,9 @@ // // - DeviceProvider: Interface for providing GPU device and queue // - EventSource: Interface for window/input events (keyboard, mouse) -// - TouchEventSource: Interface for touch input (multi-touch, gestures) -// - PointerEventSource: Interface for unified pointer events (W3C Level 3) +// - PointerEventSource: Interface for unified pointer events (W3C Level 3, mouse+touch+pen) +// - WindowProvider: Interface for window geometry, DPI, and redraw requests +// - PlatformProvider: Interface for clipboard, cursor, dark mode, accessibility // - ScrollEventSource: Interface for detailed scroll events // - Texture: Minimal interface for GPU textures // - TextureDrawer: Interface for drawing textures (2D rendering) diff --git a/platform.go b/platform.go new file mode 100644 index 0000000..84b597e --- /dev/null +++ b/platform.go @@ -0,0 +1,173 @@ +// Copyright 2026 The gogpu Authors +// SPDX-License-Identifier: MIT + +package gpucontext + +// PlatformProvider provides OS integration features. +// +// This interface enables UI frameworks (like gogpu/ui) to access platform +// capabilities such as clipboard, cursor management, and system accessibility +// preferences. +// +// Implementations: +// - gogpu.App implements PlatformProvider via platform-specific code +// - NullPlatformProvider provides no-op defaults for testing +// +// PlatformProvider is optional. Not all WindowProviders support platform +// integration (e.g., headless or embedded systems). +// Use type assertion to check availability: +// +// if pp, ok := provider.(gpucontext.PlatformProvider); ok { +// pp.SetCursor(gpucontext.CursorPointer) +// } +// +// Note: This interface is designed for gogpu <-> ui integration. +// The rendering library (gg) does NOT use this interface. +type PlatformProvider interface { + // ClipboardRead reads text content from the system clipboard. + // Returns empty string and nil error if clipboard is empty or not text. + ClipboardRead() (string, error) + + // ClipboardWrite writes text content to the system clipboard. + ClipboardWrite(text string) error + + // SetCursor changes the mouse cursor shape. + // The cursor is typically reset to CursorDefault at the start of each frame. + SetCursor(cursor CursorShape) + + // DarkMode returns true if the system dark mode is active. + // Used for automatic theme switching. + DarkMode() bool + + // ReduceMotion returns true if the user prefers reduced animation. + // Used to disable or simplify animations for accessibility. + ReduceMotion() bool + + // HighContrast returns true if the user needs high contrast mode. + // Used to adjust colors and borders for accessibility. + HighContrast() bool + + // FontScale returns the user's font size preference multiplier. + // 1.0 = default system font size. Used to scale Sp (scale-independent pixels). + FontScale() float32 +} + +// CursorShape represents the mouse cursor shape. +// +// These values cover the most common cursor shapes across platforms +// (Windows, macOS, Linux). They map directly to platform-specific +// cursor constants. +// +// For applications that need cursor changes: +// +// if pp, ok := provider.(gpucontext.PlatformProvider); ok { +// pp.SetCursor(gpucontext.CursorText) // I-beam for text input +// } +type CursorShape int + +const ( + // CursorDefault is the standard arrow cursor. + CursorDefault CursorShape = iota + + // CursorPointer is the hand cursor for clickable elements. + CursorPointer + + // CursorText is the I-beam cursor for text input areas. + CursorText + + // CursorCrosshair is the crosshair cursor for precise selection. + CursorCrosshair + + // CursorMove is the four-arrow cursor for movable elements. + CursorMove + + // CursorResizeNS is the north-south resize cursor. + CursorResizeNS + + // CursorResizeEW is the east-west resize cursor. + CursorResizeEW + + // CursorResizeNWSE is the NW-SE diagonal resize cursor. + CursorResizeNWSE + + // CursorResizeNESW is the NE-SW diagonal resize cursor. + CursorResizeNESW + + // CursorNotAllowed is the circle-with-line cursor for forbidden actions. + CursorNotAllowed + + // CursorWait is the busy/wait cursor. + CursorWait + + // CursorNone hides the cursor. + CursorNone +) + +// String returns the cursor shape name for debugging. +func (c CursorShape) String() string { + switch c { + case CursorDefault: + return "Default" + case CursorPointer: + return "Pointer" + case CursorText: + return "Text" + case CursorCrosshair: + return "Crosshair" + case CursorMove: + return "Move" + case CursorResizeNS: + return "ResizeNS" + case CursorResizeEW: + return "ResizeEW" + case CursorResizeNWSE: + return "ResizeNWSE" + case CursorResizeNESW: + return "ResizeNESW" + case CursorNotAllowed: + return "NotAllowed" + case CursorWait: + return "Wait" + case CursorNone: + return "None" + default: + return "Unknown" + } +} + +// NullPlatformProvider implements PlatformProvider with no-op behavior. +// Used for testing and platforms without OS integration. +// +// Default return values: +// - ClipboardRead: "", nil +// - ClipboardWrite: nil +// - SetCursor: no-op +// - DarkMode: false +// - ReduceMotion: false +// - HighContrast: false +// - FontScale: 1.0 +type NullPlatformProvider struct{} + +// ClipboardRead returns empty string and nil error. +func (NullPlatformProvider) ClipboardRead() (string, error) { return "", nil } + +// ClipboardWrite does nothing and returns nil. +func (NullPlatformProvider) ClipboardWrite(string) error { return nil } + +// SetCursor does nothing. +func (NullPlatformProvider) SetCursor(CursorShape) {} + +// DarkMode returns false. +func (NullPlatformProvider) DarkMode() bool { return false } + +// ReduceMotion returns false. +func (NullPlatformProvider) ReduceMotion() bool { return false } + +// HighContrast returns false. +func (NullPlatformProvider) HighContrast() bool { return false } + +// FontScale returns 1.0. +func (NullPlatformProvider) FontScale() float32 { return 1.0 } + +// Ensure NullPlatformProvider implements PlatformProvider. +var _ PlatformProvider = NullPlatformProvider{} diff --git a/platform_test.go b/platform_test.go new file mode 100644 index 0000000..5701509 --- /dev/null +++ b/platform_test.go @@ -0,0 +1,194 @@ +// Copyright 2026 The gogpu Authors +// SPDX-License-Identifier: MIT + +package gpucontext + +import "testing" + +func TestNullPlatformProvider_ClipboardRead(t *testing.T) { + var pp PlatformProvider = NullPlatformProvider{} + + text, err := pp.ClipboardRead() + if text != "" { + t.Errorf("ClipboardRead() text = %q, want empty", text) + } + if err != nil { + t.Errorf("ClipboardRead() err = %v, want nil", err) + } +} + +func TestNullPlatformProvider_ClipboardWrite(t *testing.T) { + var pp PlatformProvider = NullPlatformProvider{} + + err := pp.ClipboardWrite("hello") + if err != nil { + t.Errorf("ClipboardWrite() err = %v, want nil", err) + } +} + +func TestNullPlatformProvider_SetCursor(t *testing.T) { + var pp PlatformProvider = NullPlatformProvider{} + + // All cursor shapes should be accepted without panic + cursors := []CursorShape{ + CursorDefault, CursorPointer, CursorText, CursorCrosshair, + CursorMove, CursorResizeNS, CursorResizeEW, CursorResizeNWSE, + CursorResizeNESW, CursorNotAllowed, CursorWait, CursorNone, + } + for _, c := range cursors { + pp.SetCursor(c) + } +} + +func TestNullPlatformProvider_Defaults(t *testing.T) { + var pp PlatformProvider = NullPlatformProvider{} + + if pp.DarkMode() { + t.Error("DarkMode() should return false") + } + if pp.ReduceMotion() { + t.Error("ReduceMotion() should return false") + } + if pp.HighContrast() { + t.Error("HighContrast() should return false") + } + if got := pp.FontScale(); got != 1.0 { + t.Errorf("FontScale() = %f, want 1.0", got) + } +} + +func TestCursorShape_String(t *testing.T) { + tests := []struct { + cursor CursorShape + want string + }{ + {CursorDefault, "Default"}, + {CursorPointer, "Pointer"}, + {CursorText, "Text"}, + {CursorCrosshair, "Crosshair"}, + {CursorMove, "Move"}, + {CursorResizeNS, "ResizeNS"}, + {CursorResizeEW, "ResizeEW"}, + {CursorResizeNWSE, "ResizeNWSE"}, + {CursorResizeNESW, "ResizeNESW"}, + {CursorNotAllowed, "NotAllowed"}, + {CursorWait, "Wait"}, + {CursorNone, "None"}, + {CursorShape(99), "Unknown"}, + } + + for _, tt := range tests { + t.Run(tt.want, func(t *testing.T) { + if got := tt.cursor.String(); got != tt.want { + t.Errorf("CursorShape(%d).String() = %q, want %q", tt.cursor, got, tt.want) + } + }) + } +} + +func TestCursorShape_Values(t *testing.T) { + // Verify cursor shape constants are sequential starting from 0 + if CursorDefault != 0 { + t.Errorf("CursorDefault = %d, want 0", CursorDefault) + } + if CursorPointer != 1 { + t.Errorf("CursorPointer = %d, want 1", CursorPointer) + } + if CursorText != 2 { + t.Errorf("CursorText = %d, want 2", CursorText) + } + if CursorCrosshair != 3 { + t.Errorf("CursorCrosshair = %d, want 3", CursorCrosshair) + } + if CursorMove != 4 { + t.Errorf("CursorMove = %d, want 4", CursorMove) + } + if CursorResizeNS != 5 { + t.Errorf("CursorResizeNS = %d, want 5", CursorResizeNS) + } + if CursorResizeEW != 6 { + t.Errorf("CursorResizeEW = %d, want 6", CursorResizeEW) + } + if CursorResizeNWSE != 7 { + t.Errorf("CursorResizeNWSE = %d, want 7", CursorResizeNWSE) + } + if CursorResizeNESW != 8 { + t.Errorf("CursorResizeNESW = %d, want 8", CursorResizeNESW) + } + if CursorNotAllowed != 9 { + t.Errorf("CursorNotAllowed = %d, want 9", CursorNotAllowed) + } + if CursorWait != 10 { + t.Errorf("CursorWait = %d, want 10", CursorWait) + } + if CursorNone != 11 { + t.Errorf("CursorNone = %d, want 11", CursorNone) + } +} + +// mockPlatformProvider verifies the interface can be implemented by custom types. +type mockPlatformProvider struct { + clipboard string + cursor CursorShape + darkMode bool + reduceMotion bool + highContrast bool + fontScale float32 +} + +func (m *mockPlatformProvider) ClipboardRead() (string, error) { return m.clipboard, nil } +func (m *mockPlatformProvider) ClipboardWrite(text string) error { + m.clipboard = text + return nil +} +func (m *mockPlatformProvider) SetCursor(cursor CursorShape) { m.cursor = cursor } +func (m *mockPlatformProvider) DarkMode() bool { return m.darkMode } +func (m *mockPlatformProvider) ReduceMotion() bool { return m.reduceMotion } +func (m *mockPlatformProvider) HighContrast() bool { return m.highContrast } +func (m *mockPlatformProvider) FontScale() float32 { return m.fontScale } + +// Ensure mockPlatformProvider implements PlatformProvider. +var _ PlatformProvider = &mockPlatformProvider{} + +func TestPlatformProvider_CustomImplementation(t *testing.T) { + mock := &mockPlatformProvider{ + darkMode: true, + reduceMotion: true, + highContrast: true, + fontScale: 1.5, + } + var pp PlatformProvider = mock + + // Test clipboard round-trip + err := pp.ClipboardWrite("test clipboard") + if err != nil { + t.Fatalf("ClipboardWrite() err = %v", err) + } + text, err := pp.ClipboardRead() + if err != nil { + t.Fatalf("ClipboardRead() err = %v", err) + } + if text != "test clipboard" { + t.Errorf("ClipboardRead() = %q, want \"test clipboard\"", text) + } + + // Test cursor + pp.SetCursor(CursorPointer) + if mock.cursor != CursorPointer { + t.Errorf("cursor = %v, want CursorPointer", mock.cursor) + } + + // Test system preferences + if !pp.DarkMode() { + t.Error("DarkMode() should return true") + } + if !pp.ReduceMotion() { + t.Error("ReduceMotion() should return true") + } + if !pp.HighContrast() { + t.Error("HighContrast() should return true") + } + if got := pp.FontScale(); got != 1.5 { + t.Errorf("FontScale() = %f, want 1.5", got) + } +} diff --git a/touch.go b/touch.go deleted file mode 100644 index 48c41d8..0000000 --- a/touch.go +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright 2026 The gogpu Authors -// SPDX-License-Identifier: MIT - -package gpucontext - -import "time" - -// TouchID uniquely identifies a touch point within a touch session. -// The ID remains constant from TouchBegan through TouchEnded/TouchCanceled. -// IDs may be reused after a touch ends. -// -// Design note: Using int for compatibility with both int32 (Android) and int64 (iOS). -// Matches Ebitengine pattern for familiarity. -type TouchID int - -// TouchPhase represents the lifecycle stage of a touch point. -// These phases align with platform conventions: -// - Android: ACTION_DOWN/MOVE/UP/CANCEL -// - iOS: touchesBegan/Moved/Ended/Canceled -// - W3C: touchstart/move/end/cancel -type TouchPhase uint8 - -const ( - // TouchBegan indicates first contact with the touch surface. - // Sent once per touch point at the start of interaction. - TouchBegan TouchPhase = iota - - // TouchMoved indicates the touch point has moved. - // Sent multiple times during drag/pan gestures. - TouchMoved - - // TouchEnded indicates the touch point was lifted normally. - // Sent once per touch point at the end of interaction. - TouchEnded - - // TouchCanceled indicates the system interrupted the touch. - // This can happen when: - // - The app loses focus - // - A system gesture is recognized (e.g., swipe to home) - // - The touch hardware reports an error - // Always handle cancellation to reset UI state properly. - TouchCanceled -) - -// String returns the phase name for debugging. -func (p TouchPhase) String() string { - switch p { - case TouchBegan: - return "Began" - case TouchMoved: - return "Moved" - case TouchEnded: - return "Ended" - case TouchCanceled: - return "Canceled" - default: - return "Unknown" - } -} - -// TouchPoint represents a single point of contact on a touch surface. -// -// Position coordinates are in logical pixels relative to the window's -// content area. The coordinate system matches the graphics system: -// - Origin (0, 0) is at top-left -// - X increases rightward -// - Y increases downward -// -// Design decisions: -// - Using float64 for sub-pixel precision (matches mouse events in EventSource) -// - Pressure/Radius are pointers to indicate optional support -// - No coordinate transformation - that's the UI layer's responsibility -type TouchPoint struct { - // ID uniquely identifies this touch point within the session. - // Track touches by ID, not by array index (indices can change). - ID TouchID - - // X is the horizontal position in logical pixels. - X float64 - - // Y is the vertical position in logical pixels. - Y float64 - - // Pressure is the contact pressure, if supported by hardware. - // Range: 0.0 (no pressure) to 1.0 (maximum pressure). - // nil if pressure sensing is not available. - // - // Use case: Drawing apps, pressure-sensitive UI elements. - Pressure *float32 - - // Radius is the approximate contact radius in logical pixels. - // Represents a circular approximation of the contact area. - // nil if radius detection is not available. - // - // Use case: Distinguishing finger vs knuckle touches, - // accessibility features for users with larger contact areas. - Radius *float32 -} - -// TouchEvent represents a touch input event containing one or more touch points. -// -// Multi-touch handling: -// - TouchBegan: Changed contains new touches, All contains all active touches -// - TouchMoved: Changed contains moved touches, All contains all active touches -// - TouchEnded: Changed contains lifted touches, All contains remaining touches -// - TouchCanceled: Changed contains canceled touches, All may be empty -// -// Example multi-touch pinch gesture processing: -// -// func handleTouch(ev gpucontext.TouchEvent) { -// if ev.Phase == gpucontext.TouchMoved && len(ev.All) == 2 { -// // Calculate distance between two fingers for pinch -// dx := ev.All[0].X - ev.All[1].X -// dy := ev.All[0].Y - ev.All[1].Y -// distance := math.Sqrt(dx*dx + dy*dy) -// // Use distance for zoom... -// } -// } -type TouchEvent struct { - // Phase indicates the lifecycle stage of the touches in Changed. - Phase TouchPhase - - // Changed contains the touch points that triggered this event. - // For TouchBegan: newly added touches - // For TouchMoved: touches that moved - // For TouchEnded: touches that were lifted - // For TouchCanceled: touches that were interrupted - Changed []TouchPoint - - // All contains all currently active touch points. - // Useful for multi-touch gestures that need to track all contacts. - // For TouchEnded/TouchCanceled, this excludes the Changed touches. - All []TouchPoint - - // Modifiers contains keyboard modifier state at the time of the event. - // Useful for modifier+touch combinations (e.g., Ctrl+drag for zoom). - Modifiers Modifiers - - // Timestamp is the event time as duration since an arbitrary reference. - // Useful for calculating velocities in fling gestures. - // Zero if timestamps are not available on the platform. - Timestamp time.Duration -} - -// TouchEventSource extends EventSource with touch input capabilities. -// This interface is optional - not all EventSource implementations -// support touch input (e.g., desktop-only applications). -// -// Implementation note: Rather than adding to EventSource directly, -// we use a separate interface to maintain backward compatibility -// and allow type assertion: -// -// if tes, ok := eventSource.(gpucontext.TouchEventSource); ok { -// tes.OnTouch(handleTouchEvent) -// } -type TouchEventSource interface { - // OnTouch registers a callback for touch events. - // The callback receives a TouchEvent containing all touch information. - // - // Callback threading: Called on the main/UI thread. - // Callbacks should be fast and non-blocking. - // - // Touch events are delivered in order: Began -> Moved* -> Ended/Canceled - // Multi-touch events for simultaneous contacts are coalesced into single events. - OnTouch(fn func(TouchEvent)) -} - -// NullTouchEventSource implements TouchEventSource by ignoring all registrations. -// Useful for platforms or configurations where touch input is not available. -type NullTouchEventSource struct{} - -// OnTouch does nothing. -func (NullTouchEventSource) OnTouch(func(TouchEvent)) {} - -// Ensure NullTouchEventSource implements TouchEventSource. -var _ TouchEventSource = NullTouchEventSource{} diff --git a/window.go b/window.go new file mode 100644 index 0000000..cdc15c7 --- /dev/null +++ b/window.go @@ -0,0 +1,79 @@ +// Copyright 2026 The gogpu Authors +// SPDX-License-Identifier: MIT + +package gpucontext + +// WindowProvider provides window geometry and DPI information. +// +// This interface enables UI frameworks (like gogpu/ui) to query window +// dimensions and scale factor for layout calculations in density-independent +// pixels (Dp), and to request redraws for on-demand rendering. +// +// Implementations: +// - gogpu.App implements WindowProvider via platform window +// - NullWindowProvider provides configurable defaults for testing +// +// Example usage in a UI framework: +// +// func (ui *UI) Layout(wp gpucontext.WindowProvider) { +// w, h := wp.Size() +// scale := wp.ScaleFactor() +// dpW := float64(w) / scale +// dpH := float64(h) / scale +// ui.root.Layout(dpW, dpH) +// } +// +// Note: This interface is designed for gogpu <-> ui integration. +// The rendering library (gg) does NOT use this interface. +type WindowProvider interface { + // Size returns the current window client area dimensions in physical pixels. + Size() (width, height int) + + // ScaleFactor returns the DPI scale factor. + // 1.0 = standard (96 DPI on Windows, 72 on macOS), 2.0 = Retina/HiDPI. + // Used to convert between physical pixels and density-independent pixels (Dp). + ScaleFactor() float64 + + // RequestRedraw requests the host to render a new frame. + // In on-demand rendering mode, this triggers a single frame render. + // In continuous mode, this is a no-op. + RequestRedraw() +} + +// NullWindowProvider implements WindowProvider with configurable defaults. +// Used for testing and headless operation. +// +// When SF is zero (the default), ScaleFactor returns 1.0. +// +// Example: +// +// wp := gpucontext.NullWindowProvider{W: 800, H: 600, SF: 2.0} +// w, h := wp.Size() // 800, 600 +// scale := wp.ScaleFactor() // 2.0 +type NullWindowProvider struct { + // W is the window width in physical pixels. + W int + + // H is the window height in physical pixels. + H int + + // SF is the DPI scale factor. When zero, ScaleFactor returns 1.0. + SF float64 +} + +// Size returns the configured window dimensions. +func (n NullWindowProvider) Size() (int, int) { return n.W, n.H } + +// ScaleFactor returns the configured scale factor, defaulting to 1.0 when zero. +func (n NullWindowProvider) ScaleFactor() float64 { + if n.SF == 0 { + return 1.0 + } + return n.SF +} + +// RequestRedraw does nothing. +func (n NullWindowProvider) RequestRedraw() {} + +// Ensure NullWindowProvider implements WindowProvider. +var _ WindowProvider = NullWindowProvider{} diff --git a/window_test.go b/window_test.go new file mode 100644 index 0000000..ec9cf57 --- /dev/null +++ b/window_test.go @@ -0,0 +1,118 @@ +// Copyright 2026 The gogpu Authors +// SPDX-License-Identifier: MIT + +package gpucontext + +import "testing" + +func TestNullWindowProvider_Size(t *testing.T) { + tests := []struct { + name string + provider NullWindowProvider + wantWidth int + wantHeight int + }{ + {"zero value", NullWindowProvider{}, 0, 0}, + {"standard HD", NullWindowProvider{W: 1920, H: 1080}, 1920, 1080}, + {"small window", NullWindowProvider{W: 320, H: 240}, 320, 240}, + {"4K", NullWindowProvider{W: 3840, H: 2160}, 3840, 2160}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w, h := tt.provider.Size() + if w != tt.wantWidth { + t.Errorf("width = %d, want %d", w, tt.wantWidth) + } + if h != tt.wantHeight { + t.Errorf("height = %d, want %d", h, tt.wantHeight) + } + }) + } +} + +func TestNullWindowProvider_ScaleFactor(t *testing.T) { + tests := []struct { + name string + sf float64 + want float64 + }{ + {"zero defaults to 1.0", 0, 1.0}, + {"standard", 1.0, 1.0}, + {"retina", 2.0, 2.0}, + {"high DPI", 1.5, 1.5}, + {"3x scale", 3.0, 3.0}, + {"fractional", 1.25, 1.25}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + provider := NullWindowProvider{SF: tt.sf} + got := provider.ScaleFactor() + if got != tt.want { + t.Errorf("ScaleFactor() = %f, want %f", got, tt.want) + } + }) + } +} + +func TestNullWindowProvider_RequestRedraw(t *testing.T) { + // RequestRedraw should not panic + provider := NullWindowProvider{} + provider.RequestRedraw() +} + +func TestNullWindowProvider_Interface(t *testing.T) { + // Verify NullWindowProvider can be used through the WindowProvider interface + var wp WindowProvider = NullWindowProvider{W: 800, H: 600, SF: 2.0} + + w, h := wp.Size() + if w != 800 { + t.Errorf("width = %d, want 800", w) + } + if h != 600 { + t.Errorf("height = %d, want 600", h) + } + + sf := wp.ScaleFactor() + if sf != 2.0 { + t.Errorf("ScaleFactor() = %f, want 2.0", sf) + } + + // Should not panic + wp.RequestRedraw() +} + +// mockWindowProvider verifies the interface can be implemented by custom types. +type mockWindowProvider struct { + w, h int + sf float64 + redraws int +} + +func (m *mockWindowProvider) Size() (int, int) { return m.w, m.h } +func (m *mockWindowProvider) ScaleFactor() float64 { return m.sf } +func (m *mockWindowProvider) RequestRedraw() { m.redraws++ } + +// Ensure mockWindowProvider implements WindowProvider. +var _ WindowProvider = &mockWindowProvider{} + +func TestWindowProvider_CustomImplementation(t *testing.T) { + mock := &mockWindowProvider{w: 1024, h: 768, sf: 1.5} + var wp WindowProvider = mock + + w, h := wp.Size() + if w != 1024 || h != 768 { + t.Errorf("Size() = (%d, %d), want (1024, 768)", w, h) + } + + if sf := wp.ScaleFactor(); sf != 1.5 { + t.Errorf("ScaleFactor() = %f, want 1.5", sf) + } + + wp.RequestRedraw() + wp.RequestRedraw() + if mock.redraws != 2 { + t.Errorf("redraws = %d, want 2", mock.redraws) + } +}