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
15 changes: 15 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,23 @@ type EventSource interface {
OnMouseMove(func(x, y float64))
// ... 12 methods total
}

// IME (ADR-045): positions are UTF-8 byte offsets (IMETextRange).
type IMEController interface { // implemented by the host window system
SetIMEEnabled(bool)
SetIMECursorArea(IMECursorArea) // logical DIP
SetIMEContentType(ContentPurpose, ContentHint)
SetIMESurroundingText(IMESurroundingText)
CancelIME()
IMECapabilities() IMECapabilities // which optional operations the platform performs
}
// Optional events: if ime, ok := source.(IMEEventSource); ok { ime.OnIMECanceled(...) }
```

IME composition arrives through EventSource's OnIMECompositionStart/Update/End; `IMEState.Cursor` is
`{-1, -1}` when the IME hides the caret (`HasCursor`). Every IMEController method and `RequestRedraw` are safe from
any goroutine.

### Opaque Handles (8 bytes, zero alloc, GC-safe)

```go
Expand Down
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,38 @@ 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.32.0] - 2026-11-08

### Changed (breaking)

- **IME contract (ADR-045)**, based on gpucontext#30 by @besmpl. One extended `IMEController` instead of versioned
interfaces; all text positions are UTF-8 byte offsets.
- `IMEController`: `SetIMEPosition(x, y int)` is replaced by `SetIMECursorArea(IMECursorArea)` (logical DIP).
New methods: `SetIMEContentType`, `SetIMESurroundingText`, `CancelIME`, `IMECapabilities`. Every method must
be safe from any goroutine.
- `IMEState`: `CursorPos`, `SelectionStart` and `SelectionEnd` are replaced by `Cursor` and `Selection`
(`IMETextRange`). A `{-1, -1}` cursor means the IME hides it (`HasCursor`).
- Migration: hosts implement the new methods. Methods for unsupported operations are no-ops, and
`IMECapabilities` reports what the platform does. Consumers read `state.Cursor.Start` where they read
`state.CursorPos`.

### Added

- `IMETextRange`, `IMESurroundingText`, `IMECursorArea`, `IMEDeleteSurroundingEvent`, with UTF-8 validation that
never clamps.
- `ContentPurpose` (incl. `Chat`) and the `ContentHint` bitset, with names from tables keyed by the constants.
- `IMECapabilities` in three groups, as in winit: cursor area, content type, surrounding text.
- Optional `IMEEventSource` (`OnIMECanceled`, `OnIMEDisabled`, `OnIMEDeleteSurrounding`), found by type assertion.

### Documentation

- `WindowProvider.RequestRedraw` must be safe to call from any goroutine, and requests coalesce.
- Package doc, AGENTS.md and ROADMAP list the IME interfaces.

### Changed

- deps: gputypes v0.8.0 → v0.8.2

## [0.31.3] - 2026-08-31

### Changed
Expand Down
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,9 @@ func (app *App) OnKeyPress(fn func(gpucontext.Key, gpucontext.Modifiers)) {

### IME Support (CJK Input)

`IMEState` and related interfaces enable Input Method Editor support for Chinese, Japanese, and Korean input:
`IMEState`, `IMEController` and the optional `IMEEventSource` enable Input Method Editor support for Chinese,
Japanese, Korean and other complex input. All text positions are UTF-8 byte offsets (`IMETextRange`), as in
Wayland text-input, GTK4 and winit:

```go
// In gogpu/ui - handle IME composition
Expand All @@ -181,21 +183,30 @@ func (input *TextInput) AttachIME(source gpucontext.EventSource) {
})

source.OnIMECompositionUpdate(func(state gpucontext.IMEState) {
// Show composition text with cursor
input.setCompositionText(state.CompositionText, state.CursorPos)
// Composition text with its cursor (UTF-8 byte range; hidden when the IME asks)
input.setCompositionText(state.CompositionText, state.Cursor, state.HasCursor())
})

source.OnIMECompositionEnd(func(committed string) {
// Insert final text
input.insertText(committed)
input.hideCompositionWindow()
})

// Optional events, found by type assertion
if ime, ok := source.(gpucontext.IMEEventSource); ok {
ime.OnIMECanceled(input.clearComposition)
ime.OnIMEDeleteSurrounding(input.deleteAroundCursor)
}
}

// Control IME position (for composition window placement)
// Enable IME and tell it where the caret is and what the field expects
func (input *TextInput) Focus(controller gpucontext.IMEController) {
controller.SetIMEEnabled(true)
controller.SetIMEPosition(input.cursorX, input.cursorY)
controller.SetIMECursorArea(gpucontext.IMECursorArea{X: input.caretX, Y: input.caretY, Height: input.lineHeight})
if controller.IMECapabilities().Has(gpucontext.IMECapabilityContentType) {
controller.SetIMEContentType(gpucontext.ContentPurposeEmail, gpucontext.ContentHintCompletion)
}
}
```

Expand Down
12 changes: 11 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

`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.31.2
## Current: v0.32.0

- IME contract (ADR-045): one `IMEController` (cursor area, content type, surrounding text, cancel,
capabilities), `IMEState` with UTF-8 byte ranges, optional `IMEEventSource`
- `TextureRegionUpdater.UpdateRegion` struct API (`image.Rectangle` + `ImageDataLayout`, #484)
- `DeviceProvider.DownlevelCapabilities()` (ADR-071)
- `DeviceProvider.Features()` — query device capabilities (FeatureRayQuery, etc.)
Expand All @@ -21,6 +23,14 @@

## Released

### v0.32.0 (2026-11-08)
- IME contract per ADR-045 (breaking: `SetIMEPosition` → `SetIMECursorArea`, `IMEState` cursor fields → ranges),
based on gpucontext#30 by @besmpl
- docs: `WindowProvider.RequestRedraw` is safe from any goroutine

### v0.31.3 (2026-08-31)
- deps: gputypes v0.8.0 (ADR-072 struct params)

### v0.31.2 (2026-08-30)
- docs: fix README and ROADMAP for struct-based UpdateRegion API

Expand Down
13 changes: 8 additions & 5 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
// projects to enable GPU resource sharing without circular dependencies:
//
// - DeviceProvider: Interface for providing GPU device and queue
// - EventSource: Interface for window/input events (keyboard, mouse)
// - EventSource: Interface for window/input events (keyboard, mouse, IME composition)
// - IMEController, IMEEventSource: Input Method Editor control and optional events
// (ADR-045; UTF-8 byte offsets, see IMETextRange)
// - 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
Expand Down Expand Up @@ -53,8 +55,9 @@
package gpucontext

const (
stringNone = "None"
stringLeft = "Left"
stringRight = "Right"
stringMiddle = "Middle"
stringNone = "None"
stringUnknown = "Unknown"
stringLeft = "Left"
stringRight = "Right"
stringMiddle = "Middle"
)
38 changes: 0 additions & 38 deletions events.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,44 +90,6 @@ type EventSource interface {
OnIMECompositionEnd(fn func(committed string))
}

// IMEState represents the current state of the Input Method Editor.
// This is used for CJK (Chinese, Japanese, Korean) and other complex text input.
//
// During IME composition, the user types phonetic characters that are converted
// to ideographic characters. The IMEState contains the current preview text
// and cursor information for rendering the composition inline.
type IMEState struct {
// Composing indicates whether IME is currently in composition mode.
Composing bool

// CompositionText is the text currently being composed (e.g., pinyin for Chinese).
// This should be displayed inline at the cursor position with special styling.
CompositionText string

// CursorPos is the cursor position within the composition text.
CursorPos int

// SelectionStart is the start of the selection within the composition text.
// This is used for candidate selection in some IME systems.
SelectionStart int

// SelectionEnd is the end of the selection within the composition text.
SelectionEnd int
}

// IMEController allows widgets to control IME behavior.
// This interface is typically implemented by the host window system.
type IMEController interface {
// SetIMEPosition tells the platform where to show the IME candidate window.
// The coordinates are in screen pixels relative to the window.
SetIMEPosition(x, y int)

// SetIMEEnabled enables or disables IME for the current input context.
// When disabled, key presses are delivered directly without IME processing.
// This is useful for password fields or non-text inputs.
SetIMEEnabled(enabled bool)
}

// Key represents a keyboard key.
//
// Values use grouped ranges with explicit base offsets (the net/http pattern).
Expand Down
67 changes: 0 additions & 67 deletions events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,70 +215,3 @@ func TestMouseButtonConstants(t *testing.T) {
t.Error("MouseButtonMiddle should be 2")
}
}

func TestIMEState(t *testing.T) {
// Test IMEState struct fields
state := IMEState{
Composing: true,
CompositionText: "nihao",
CursorPos: 5,
SelectionStart: 2,
SelectionEnd: 4,
}

if !state.Composing {
t.Error("Composing should be true")
}
if state.CompositionText != "nihao" {
t.Errorf("CompositionText = %q, want \"nihao\"", state.CompositionText)
}
if state.CursorPos != 5 {
t.Errorf("CursorPos = %d, want 5", state.CursorPos)
}
if state.SelectionStart != 2 {
t.Errorf("SelectionStart = %d, want 2", state.SelectionStart)
}
if state.SelectionEnd != 4 {
t.Errorf("SelectionEnd = %d, want 4", state.SelectionEnd)
}
}

func TestIMEStateZeroValue(t *testing.T) {
// Test IMEState zero value
var state IMEState

if state.Composing {
t.Error("Zero value Composing should be false")
}
if state.CompositionText != "" {
t.Errorf("Zero value CompositionText = %q, want empty", state.CompositionText)
}
if state.CursorPos != 0 {
t.Errorf("Zero value CursorPos = %d, want 0", state.CursorPos)
}
if state.SelectionStart != 0 {
t.Errorf("Zero value SelectionStart = %d, want 0", state.SelectionStart)
}
if state.SelectionEnd != 0 {
t.Errorf("Zero value SelectionEnd = %d, want 0", state.SelectionEnd)
}
}

// mockIMEController is used to verify IMEController interface at compile time.
type mockIMEController struct{}

func (mockIMEController) SetIMEPosition(_, _ int) {}
func (mockIMEController) SetIMEEnabled(_ bool) {}

// Ensure mockIMEController implements IMEController.
var _ IMEController = mockIMEController{}

func TestIMEControllerInterface(t *testing.T) {
// Verify IMEController can be used through the interface
var controller IMEController = mockIMEController{}

// These should not panic
controller.SetIMEPosition(100, 200)
controller.SetIMEEnabled(true)
controller.SetIMEEnabled(false)
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ module github.com/gogpu/gpucontext

go 1.25

require github.com/gogpu/gputypes v0.8.0
require github.com/gogpu/gputypes v0.8.2
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github.com/gogpu/gputypes v0.8.0 h1:Iw4g9lqydTcAFKKQWfgbUpdY6WxObGtjf9SuRkzYMJw=
github.com/gogpu/gputypes v0.8.0/go.mod h1:cnXrDMwTpWTvJLW1Vreop3PcT6a2YP/i3s91rPaOavw=
github.com/gogpu/gputypes v0.8.2 h1:XGn8Kib6fXOREwKLzYyfEqaXTBCi9Zs2oAWj1+yW+ic=
github.com/gogpu/gputypes v0.8.2/go.mod h1:cnXrDMwTpWTvJLW1Vreop3PcT6a2YP/i3s91rPaOavw=
Loading
Loading