DEV Community

Cover image for gogpu/ui v0.1.54: We Built Flutter's Gesture System in Pure Go — Arena, Recognizers, and All
Andrey Kolkov
Andrey Kolkov

Posted on

gogpu/ui v0.1.54: We Built Flutter's Gesture System in Pure Go — Arena, Recognizers, and All

Series: Building Go's GPU Ecosystem

Recent articles:

The Problem: Every Widget Reinvented Click Detection

Our GUI toolkit has 27 widgets — buttons, checkboxes, sliders, text fields, dropdowns, dialogs, tab views, tree views, data tables, docking panels, and more — and each one implemented its own click/drag/selection logic from scratch. Button tracked pressed state. Slider tracked dragging. TextField had its own MouseDrag handler. No code was shared. No disambiguation existed.

When we wanted to add text selection to TextField (drag to select, double-click to select word, triple-click to select all), we realized the ad-hoc approach couldn't scale. What happens when a TextField is inside a ScrollView? Both want drag events. Who wins?

Flutter solved this problem in 2017 with the GestureArena. We decided to port the same architecture to Go.

What We Built

The Gesture Arena

The arena is the core disambiguation mechanism. When a pointer goes down, all widgets under the pointer register their gesture recognizers in the arena. The recognizers compete:

  • First to accept wins. If a recognizer is confident it detected its gesture (e.g., drag exceeded the slop threshold), it accepts and all others are rejected.
  • Last to not reject wins. If everyone else rejects (e.g., no movement for drag), the remaining recognizer wins by default.
  • Sweep after pointer up. If no one accepted, the first member wins as a last resort.
// Widget implements gesture.GestureAware
func (w *Widget) GestureHitTest(pos geometry.Point) []gesture.Recognizer {
    return []gesture.Recognizer{w.clickRec}
}
Enter fullscreen mode Exit fullscreen mode

Four Recognizers + VelocityTracker

Recognizer What it detects Used by
ClickRecognizer Single/double/triple click with timing Button, Checkbox, Radio
DragRecognizer Pan, horizontal, vertical drag with velocity Slider, SplitView
LongPressRecognizer Press-and-hold (500ms, frame-based) Future: context menu
TapAndDragRecognizer Click counting + drag in one recognizer TextField
VelocityTracker Least-squares velocity for fling detection ScrollView momentum

Per-Device Thresholds

Mouse and touch have different precision. A 1px movement on a mouse is intentional; on a touchscreen it's noise. We use the same constants Flutter validated against millions of users:

Parameter Mouse Touch
Drag threshold (slop) 1px 18px
Double-click timeout 300ms 300ms
Double-click distance 4px (Chromium) 100px
Anti-bounce minimum 40ms 40ms

Unified Pointer Pipeline

We replaced the legacy mouse callback path with a single PointerEvent-based pipeline:

Platform → PointerEvent → HandlePointerEvent
    ├── Gesture Arena (recognizer dispatch for GestureAware widgets)
    └── Derived MouseEvent (existing widgets continue working unchanged)
Enter fullscreen mode Exit fullscreen mode

Every pointer event flows through this single path. No dual dispatch. Existing widgets receive MouseEvent as before — derived from the same PointerEvent that feeds the arena.

Container Widget Hit-Testing

The hardest bug we hit: a Collapsible widget (parent) and a Checkbox (child) both had ClickRecognizers. The parent won the arena and silently dropped clicks meant for the child.

The fix: GestureHitTest(pos geometry.Point) receives widget-local coordinates. Container widgets only return recognizers when the pointer is in their interactive region:

// Collapsible returns recognizer ONLY for header clicks
func (w *Widget) GestureHitTest(pos geometry.Point) []gesture.Recognizer {
    headerRect := geometry.NewRect(0, 0, w.Bounds().Width(), w.headerHeight)
    if !headerRect.Contains(pos) {
        return nil // Content-area click → child widgets handle it
    }
    return []gesture.Recognizer{w.clickRec}
}
Enter fullscreen mode Exit fullscreen mode

TextField: The Full Story

TextField was our proof-of-concept. Before v0.1.54, it could place a cursor on click. That's it. No selection, no word selection, no clipboard.

Now:

  • Click to place cursor (with Shift+Click for range extension)
  • Drag to select text character-by-character
  • Double-click to select word (uses wordBoundsAt() with Unicode word boundaries)
  • Triple-click to select all text
  • Double-click + drag for word-by-word selection
  • Ctrl+C/V/X for OS clipboard (Win32, macOS, Linux)

All of this through one TapAndDragRecognizer that provides consecutiveTapCount on every callback — exactly how Flutter's TextSelectionGestureDetector works.

OS Clipboard

We added widget.ClipboardProvider — a DI interface (same pattern as our SoundPlayer) that bridges widget clipboard requests to the platform:

// In widget package — no platform imports
widget.ClipboardWrite("copied text")
text := widget.ClipboardRead()

// Desktop layer registers the platform implementation
widget.RegisterClipboardProvider(gogpuApp) // Win32/macOS/Linux
Enter fullscreen mode Exit fullscreen mode

Ctrl+C copies to system clipboard. Ctrl+V pastes from it. Works across applications.

Signals Integration

Gesture state is exposed as opt-in reactive signals:

dragRecognizer := gesture.NewDragRecognizer(gesture.DragConfig{
    OnDragStart: handleStart,
}, gesture.WithDraggingSignal(isDragging))

// Widget binds to signal for reactive redraw
state.BindToScheduler(isDragging, widget, scheduler)
Enter fullscreen mode Exit fullscreen mode

Bool signals suppress no-op notifications. Point signals fire on every change. Frame-based timers (no goroutines) for long press.

Numbers

  • ~8,300 LOC added (gesture/ package + widget migrations + clipboard + tests)
  • 124 gesture tests, 96% coverage
  • 20 widgets implement GestureAware with GestureHitTest
  • 0 goroutines in the entire gesture system — single-threaded by design
  • 5 enterprise references studied: Flutter (source code verified), Chromium, GTK4, Qt6, Android

What's Next

The gesture system is designed for multi-touch from day one. PointerEvent carries PointerID and PointerType (mouse/touch/pen). When Android support lands (@besmpl is working on it), pinch-to-zoom and multi-finger gestures will plug into the same arena.

Context menu (right-click) is the next visible feature. The gesture infrastructure supports any button — ClickRecognizer just needs a ButtonRight config.


gogpu/ui is an enterprise GUI toolkit for Go with GPU-accelerated rendering. Zero CGO, 220K+ LOC, 70 packages, 7,800+ tests.

Help Us Build Go's GUI Future

The gesture system is the kind of infrastructure that needs real-world validation. We tested with our gallery and examples, but the edge cases live in your applications.

Try it. Clone the repo, run the gallery, drag-select some text, double-click a word. If something feels wrong — file an issue. If it feels right — even better. Tell us.

git clone https://github.com/gogpu/ui.git && cd ui
GOGPU_GRAPHICS_API=vulkan go run ./examples/gallery/
Enter fullscreen mode Exit fullscreen mode

Star the repo. It sounds small, but GitHub stars are how Go developers discover projects. We're listed in awesome-go — every star helps us stay there.

Spread the word. A mention in your team's Slack, a retweet, a discussion with a colleague who's frustrated with GUI options in Go. Most Go developers don't know a pure Go GUI toolkit with GPU rendering exists.

Contribute. The gesture system is designed for extensibility. Build a custom recognizer, add a new widget, improve accessibility, test on your hardware. Check CONTRIBUTING.md. @besmpl contributed 20+ PRs across the ecosystem including Android arm64 support — contributors make this project better.

Support the project. If gogpu saves you from CGO hell or fills a gap in Go's ecosystem, consider supporting on Open Collective. Open source is free to use but not free to build.

The gogpu ecosystem is 1.25M+ lines of pure Go with zero CGO. gogpu/ui alone: 220K+ LOC, 70 packages, 7,800+ tests. Five GPU backends. Three platforms. One goal: make Go the best language for building desktop applications.


GitHub · Discussions · Release v0.1.54 · awesome-go

Top comments (0)