Skip to main content

Gestures

Drift provides gesture detection for handling touch input including taps, drags, and long presses.

Tap Gesture

Use the Tap helper for simple tap gestures:

widgets.Tap(func() {
fmt.Println("Tapped!")
}, myButton)

For more control, use GestureDetector directly:

widgets.GestureDetector{
OnTap: func() {
fmt.Println("Tapped!")
},
ChildWidget: myButton,
}

Pan Gesture (Omnidirectional Drag)

Use the Drag helper for simple pan gestures:

widgets.Drag(func(d widgets.DragUpdateDetails) {
s.SetState(func() {
s.x += d.Delta.X
s.y += d.Delta.Y
})
}, draggableBox)

For more control (OnStart, OnEnd, OnCancel), use GestureDetector:

widgets.GestureDetector{
OnPanStart: func(d widgets.DragStartDetails) {
// Drag started at d.Position
s.SetState(func() {
s.isDragging = true
})
},
OnPanUpdate: func(d widgets.DragUpdateDetails) {
// d.Delta contains movement since last update
s.SetState(func() {
s.x += d.Delta.X
s.y += d.Delta.Y
})
},
OnPanEnd: func(d widgets.DragEndDetails) {
// d.Velocity contains fling velocity
s.SetState(func() {
s.isDragging = false
})
if d.Velocity.X > threshold {
flingRight()
}
},
OnPanCancel: func() {
s.SetState(func() {
s.isDragging = false
})
},
ChildWidget: draggableBox,
}

Axis-Locked Drags

For gestures that should only respond to one axis, use axis-specific callbacks.

Horizontal Drag

Use the HorizontalDrag helper:

widgets.HorizontalDrag(func(d widgets.DragUpdateDetails) {
s.SetState(func() {
s.sliderValue += d.PrimaryDelta
})
}, slider)

Or use GestureDetector for full control:

widgets.GestureDetector{
OnHorizontalDragStart: func(d widgets.DragStartDetails) {
// Horizontal drag started
},
OnHorizontalDragUpdate: func(d widgets.DragUpdateDetails) {
// d.PrimaryDelta is the X movement
s.SetState(func() {
s.offset += d.PrimaryDelta
})
},
OnHorizontalDragEnd: func(d widgets.DragEndDetails) {
// d.PrimaryVelocity is the X velocity
if d.PrimaryVelocity > swipeThreshold {
dismissCard()
}
},
OnHorizontalDragCancel: func() {},
ChildWidget: swipeableCard,
}

Vertical Drag

Use the VerticalDrag helper:

widgets.VerticalDrag(func(d widgets.DragUpdateDetails) {
s.SetState(func() {
s.pullOffset += d.PrimaryDelta
})
}, pullToRefresh)

Or use GestureDetector:

widgets.GestureDetector{
OnVerticalDragStart: func(d widgets.DragStartDetails) {
// Vertical drag started
},
OnVerticalDragUpdate: func(d widgets.DragUpdateDetails) {
// d.PrimaryDelta is the Y movement
s.SetState(func() {
s.sheetHeight -= d.PrimaryDelta
})
},
OnVerticalDragEnd: func(d widgets.DragEndDetails) {
// Snap to positions based on velocity
if d.PrimaryVelocity > 0 {
collapseSheet()
} else {
expandSheet()
}
},
OnVerticalDragCancel: func() {},
ChildWidget: bottomSheet,
}

Gesture Competition

When multiple gesture recognizers compete for the same pointer:

  • Axis-locked drags win when the primary axis movement exceeds slop and is greater than or equal to the orthogonal movement
  • Tap loses if movement exceeds the touch slop
  • Pan wins when total movement exceeds the touch slop
  • Long press wins when held long enough without movement

This enables patterns like swipe-to-dismiss cards inside a vertical ScrollView:

// Vertical ScrollView with horizontally-swipeable cards
widgets.ScrollView{
ScrollDirection: widgets.AxisVertical,
ChildWidget: widgets.Column{
ChildrenWidgets: []core.Widget{
// This card responds to horizontal swipes
// while the parent ScrollView responds to vertical swipes
widgets.GestureDetector{
OnHorizontalDragUpdate: func(d widgets.DragUpdateDetails) {
s.SetState(func() {
s.cardOffset += d.PrimaryDelta
})
},
ChildWidget: swipeCard,
},
},
},
}

Drag Details

The drag callbacks receive detail structs:

DragStartDetails

FieldTypeDescription
Positionrendering.OffsetGlobal position where the drag started

DragUpdateDetails

FieldTypeDescription
Positionrendering.OffsetCurrent global position
Deltarendering.OffsetMovement since last update
PrimaryDeltafloat64Axis-specific delta (only for axis-locked drags)

DragEndDetails

FieldTypeDescription
Positionrendering.OffsetFinal global position
Velocityrendering.OffsetFling velocity in pixels/second
PrimaryVelocityfloat64Axis-specific velocity (only for axis-locked drags)

Note: PrimaryDelta and PrimaryVelocity are only meaningful for axis-locked recognizers.

Clamp Helper

The Clamp helper constrains a value between min and max bounds:

widgets.Drag(func(d widgets.DragUpdateDetails) {
s.SetState(func() {
s.x = widgets.Clamp(s.x+d.Delta.X, 0, s.maxX)
s.y = widgets.Clamp(s.y+d.Delta.Y, 0, s.maxY)
})
}, draggableBox)

Common Patterns

Swipe to Dismiss

type swipeState struct {
core.StateBase
offset float64
}

func (s *swipeState) Build(ctx core.BuildContext) core.Widget {
return widgets.GestureDetector{
OnHorizontalDragUpdate: func(d widgets.DragUpdateDetails) {
s.SetState(func() {
s.offset += d.PrimaryDelta
})
},
OnHorizontalDragEnd: func(d widgets.DragEndDetails) {
if math.Abs(s.offset) > dismissThreshold || math.Abs(d.PrimaryVelocity) > velocityThreshold {
onDismiss()
} else {
// Snap back
s.SetState(func() {
s.offset = 0
})
}
},
ChildWidget: widgets.Stack{
ChildrenWidgets: []core.Widget{
widgets.Positioned{
Left: widgets.Ptr(s.offset),
Top: widgets.Ptr(0),
ChildWidget: card,
},
},
},
}
}

Pull to Refresh

type refreshState struct {
core.StateBase
pullDistance float64
isRefreshing bool
}

func (s *refreshState) Build(ctx core.BuildContext) core.Widget {
return widgets.GestureDetector{
OnVerticalDragUpdate: func(d widgets.DragUpdateDetails) {
if d.PrimaryDelta > 0 && !s.isRefreshing {
s.SetState(func() {
s.pullDistance += d.PrimaryDelta
})
}
},
OnVerticalDragEnd: func(d widgets.DragEndDetails) {
if s.pullDistance > refreshThreshold {
s.SetState(func() {
s.isRefreshing = true
})
go s.doRefresh()
} else {
s.SetState(func() {
s.pullDistance = 0
})
}
},
ChildWidget: content,
}
}

Draggable Position

type draggableState struct {
core.StateBase
x, y float64
}

func (s *draggableState) Build(ctx core.BuildContext) core.Widget {
return widgets.Stack{
ChildrenWidgets: []core.Widget{
widgets.Positioned{
Left: &s.x,
Top: &s.y,
ChildWidget: widgets.GestureDetector{
OnPanUpdate: func(d widgets.DragUpdateDetails) {
s.SetState(func() {
s.x += d.Delta.X
s.y += d.Delta.Y
})
},
ChildWidget: draggableHandle,
},
},
},
}
}

Text Input

For text input, use a controller pattern:

type formState struct {
core.StateBase
controller *platform.TextEditingController
}

func (s *formState) InitState() {
s.controller = platform.NewTextEditingController("")
}

func (s *formState) Build(ctx core.BuildContext) core.Widget {
return widgets.NativeTextField{
Controller: s.controller,
Placeholder: "Enter text",
KeyboardType: platform.KeyboardTypeText,
OnSubmitted: s.handleSubmit,
}
}

func (s *formState) handleSubmit(text string) {
value := s.controller.Text() // Read current value
s.controller.Clear() // Clear programmatically
}

Keyboard Types

TypeUse
KeyboardTypeTextGeneral text input
KeyboardTypeNumberNumeric input
KeyboardTypeEmailEmail address
KeyboardTypePhonePhone number
KeyboardTypeURLURL input

Haptic Feedback

Add tactile feedback to gestures:

widgets.GestureDetector{
OnTap: func() {
platform.Haptics.LightImpact()
handleTap()
},
ChildWidget: button,
}
MethodUse
LightImpact()Subtle feedback (selections)
MediumImpact()Standard feedback (toggles)
HeavyImpact()Strong feedback (errors)
SelectionClick()Selection change

Next Steps