Skip to content
Open
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
1 change: 1 addition & 0 deletions lib/pressable.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
library;

export 'package:pressable/src/builder.dart';
export 'package:pressable/src/custom_gesture_detector.dart';
export 'package:pressable/src/fill.dart';
export 'package:pressable/src/opacity.dart';
export 'package:pressable/src/platform.dart';
Expand Down
3 changes: 2 additions & 1 deletion lib/src/builder.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:pressable/src/base.dart';
import 'package:pressable/src/custom_gesture_detector.dart';

/// Builds [Widget] inside [PressableBuilder].
typedef PressableBuilderCallback =
Expand Down Expand Up @@ -32,7 +33,7 @@ class _PressableBuilderState extends PressableBaseState<PressableBuilder> {
(widget.onPressed != null || widget.onLongPressed != null)
? SystemMouseCursors.click
: SystemMouseCursors.basic,
child: GestureDetector(
child: CustomGestureDetector(
onTap: widget.onPressed,
onTapDown: widget.onPressed != null ? onPressStarted : null,
onTapUp: widget.onPressed != null ? onPressEnded : null,
Expand Down
134 changes: 134 additions & 0 deletions lib/src/custom_gesture_detector.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';

/// A robust gesture detector that combines GestureDetector with fallback pointer handling
/// for better cross-platform compatibility, especially on Linux desktop touch screens.
class CustomGestureDetector extends StatefulWidget {
const CustomGestureDetector({
super.key,
required this.child,
this.onTap,
this.onTapDown,
this.onTapUp,
this.onTapCancel,
this.onLongPress,
this.onLongPressStart,
this.onLongPressEnd,
this.behavior = HitTestBehavior.opaque,
this.excludeFromSemantics = false,
});

final Widget child;
final VoidCallback? onTap;
final GestureTapDownCallback? onTapDown;
final GestureTapUpCallback? onTapUp;
final VoidCallback? onTapCancel;
final VoidCallback? onLongPress;
final GestureLongPressStartCallback? onLongPressStart;
final GestureLongPressEndCallback? onLongPressEnd;
final HitTestBehavior behavior;
final bool excludeFromSemantics;

@override
State<CustomGestureDetector> createState() => _CustomGestureDetectorState();
}

class _CustomGestureDetectorState extends State<CustomGestureDetector> {
bool _isPressed = false;
int? _activePointerId;
bool _gestureHandled = false;

// Track if we're on a platform that might have touch issues
bool get _needsPointerFallback =>
defaultTargetPlatform == TargetPlatform.linux;
Comment on lines +43 to +44
Copy link

Copilot AI Sep 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider making the platform check configurable rather than hardcoded. This would allow easier testing and future platform support without code changes. You could add a constructor parameter like enablePointerFallback with a default value based on the platform check.

Copilot uses AI. Check for mistakes.

void _handlePointerDown(PointerDownEvent event) {
if (_needsPointerFallback && _activePointerId == null) {
_activePointerId = event.pointer;
_isPressed = true;
_gestureHandled = false;
}
}

void _handlePointerUp(PointerUpEvent event) {
if (_needsPointerFallback &&
_activePointerId == event.pointer &&
_isPressed &&
!_gestureHandled) {

_activePointerId = null;
_isPressed = false;

// Only trigger fallback if gesture detector didn't handle it
widget.onTapUp?.call(TapUpDetails(
kind: event.kind,
globalPosition: event.position,
localPosition: event.localPosition,
));
widget.onTap?.call();
}
}

void _handlePointerCancel(PointerCancelEvent event) {
if (_needsPointerFallback &&
_activePointerId == event.pointer &&
_isPressed &&
!_gestureHandled) {

_activePointerId = null;
_isPressed = false;
widget.onTapCancel?.call();
}
}

void _onGestureTapDown(TapDownDetails details) {
_gestureHandled = true;
widget.onTapDown?.call(details);
}

void _onGestureTapUp(TapUpDetails details) {
_gestureHandled = true;
_isPressed = false;
widget.onTapUp?.call(details);
}

void _onGestureTapCancel() {
_gestureHandled = true;
_isPressed = false;
widget.onTapCancel?.call();
}

void _onGestureTap() {
_gestureHandled = true;
widget.onTap?.call();
}

@override
Widget build(BuildContext context) {
Widget child = GestureDetector(
onTap: widget.onTap != null ? _onGestureTap : null,
onTapDown: widget.onTapDown != null ? _onGestureTapDown : null,
onTapUp: widget.onTapUp != null ? _onGestureTapUp : null,
onTapCancel: widget.onTapCancel != null ? _onGestureTapCancel : null,
onLongPress: widget.onLongPress,
onLongPressStart: widget.onLongPressStart,
onLongPressEnd: widget.onLongPressEnd,
behavior: widget.behavior,
excludeFromSemantics: widget.excludeFromSemantics,
child: widget.child,
);

// Only add pointer fallback on platforms that need it
if (_needsPointerFallback) {
child = Listener(
onPointerDown: _handlePointerDown,
onPointerUp: _handlePointerUp,
onPointerCancel: _handlePointerCancel,
child: child,
);
}

return child;
}
}
3 changes: 2 additions & 1 deletion lib/src/opacity.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:pressable/pressable.dart';
import 'package:pressable/src/base.dart';
import 'package:pressable/src/custom_gesture_detector.dart';

/// Makes [child] semi-transparent when pressed.
class PressableOpacity extends StatefulWidget {
Expand Down Expand Up @@ -49,7 +50,7 @@ class _PressableOpacityState extends PressableBaseState<PressableOpacity>
(widget.onPressed != null || widget.onLongPressed != null)
? SystemMouseCursors.click
: SystemMouseCursors.basic,
child: GestureDetector(
child: CustomGestureDetector(
onTap: widget.onPressed,
onTapDown: widget.onPressed != null ? onPressStarted : null,
onTapUp: widget.onPressed != null ? onPressEnded : null,
Expand Down
3 changes: 2 additions & 1 deletion lib/src/scale.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:pressable/pressable.dart';
import 'package:pressable/src/base.dart';
import 'package:pressable/src/custom_gesture_detector.dart';

/// Scales [child] down when pressed.
class PressableScale extends StatefulWidget {
Expand Down Expand Up @@ -48,7 +49,7 @@ class _PressableScaleState extends PressableBaseState<PressableScale>
(widget.onPressed != null || widget.onLongPressed != null)
? SystemMouseCursors.click
: MouseCursor.defer,
child: GestureDetector(
child: CustomGestureDetector(
onTap: widget.onPressed,
onTapDown: widget.onPressed != null ? onPressStarted : null,
onTapUp: widget.onPressed != null ? onPressEnded : null,
Expand Down