From cbef2159b183972aa04c5eadc65c9aea4cce2994 Mon Sep 17 00:00:00 2001 From: Andy Date: Sat, 7 Feb 2026 15:58:18 +0300 Subject: [PATCH] feat: add WindowProvider, PlatformProvider; remove legacy TouchEventSource MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WindowProvider provides window geometry, DPI scale factor, and redraw requests for UI frameworks. PlatformProvider adds optional OS integration: clipboard, cursor management, dark mode detection, and accessibility preferences (reduce motion, high contrast, font scale). CursorShape enum defines 12 standard cursor shapes with String() method. NullWindowProvider and NullPlatformProvider included for testing. Remove TouchEventSource — fully replaced by W3C Pointer Events Level 3 (PointerEventSource with PointerType: Touch). Zero implementations, zero consumers existed. --- CHANGELOG.md | 43 +++++++++++ README.md | 127 ++++++++++++++----------------- ROADMAP.md | 49 +++++++----- doc.go | 5 +- platform.go | 173 ++++++++++++++++++++++++++++++++++++++++++ platform_test.go | 194 +++++++++++++++++++++++++++++++++++++++++++++++ touch.go | 176 ------------------------------------------ window.go | 79 +++++++++++++++++++ window_test.go | 118 ++++++++++++++++++++++++++++ 9 files changed, 695 insertions(+), 269 deletions(-) create mode 100644 platform.go create mode 100644 platform_test.go delete mode 100644 touch.go create mode 100644 window.go create mode 100644 window_test.go 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) + } +}