Skip to content

Commit

Permalink
implement a press event
Browse files Browse the repository at this point in the history
  • Loading branch information
tilucasoli committed Nov 28, 2024
1 parent ec7e98d commit 4b3744b
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 64 deletions.
1 change: 1 addition & 0 deletions packages/mix/lib/mix.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export 'src/core/spec.dart';
export 'src/core/styled_widget.dart';
export 'src/core/utility.dart';
export 'src/core/variant.dart';
export 'src/core/widget_state/press_event_mix_state.dart';
export 'src/core/widget_state/widget_state_controller.dart';
/// MODIFIERS
export 'src/modifiers/align_widget_modifier.dart';
Expand Down
168 changes: 106 additions & 62 deletions packages/mix/lib/src/core/widget_state/internal/gesture_mix_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,45 @@ import 'dart:async';
import 'package:flutter/material.dart';

import '../widget_state_controller.dart';
import '../press_event_mix_state.dart';

abstract interface class WidgetStateHandler {
@visibleForTesting
late final MixWidgetStateController controller;
}

mixin HandlePress on WidgetStateHandler {
int _pressCount = 0;
Timer? _timer;

void pressCallback();

void handlePress({required bool value, required Duration delay}) {
controller.pressed = value;
if (value) {
_pressCount++;
final initialPressCount = _pressCount;
_unpressAfterDelay(initialPressCount, delay: delay);
}
}

void _unpressAfterDelay(int initialPressCount, {required Duration delay}) {
void unpressCallback() {
if (controller.pressed && _pressCount == initialPressCount) {
controller.pressed = false;
pressCallback();
}
}

_timer?.cancel();

if (delay != Duration.zero) {
_timer = Timer(delay, unpressCallback);
} else {
unpressCallback();
}
}
}

class GestureMixStateWidget extends StatefulWidget {
const GestureMixStateWidget({
Expand Down Expand Up @@ -83,15 +122,21 @@ class GestureMixStateWidget extends StatefulWidget {
State createState() => _GestureMixStateWidgetState();
}

class _GestureMixStateWidgetState extends State<GestureMixStateWidget> {
int _pressCount = 0;
Timer? _timer;
late final MixWidgetStateController _controller;
abstract class _GestureMixStateWidgetStateWithController
extends State<GestureMixStateWidget> implements WidgetStateHandler {}

class _GestureMixStateWidgetState
extends _GestureMixStateWidgetStateWithController with HandlePress {
PressEvent _event = PressEvent.idle;

@override
@protected
late final MixWidgetStateController controller;

@override
void initState() {
super.initState();
_controller = widget.controller ?? MixWidgetStateController();
controller = widget.controller ?? MixWidgetStateController();
}

void _onPanUpdate(DragUpdateDetails event) {
Expand All @@ -102,42 +147,27 @@ class _GestureMixStateWidgetState extends State<GestureMixStateWidget> {
widget.onPanDown?.call(details);
}

_handlePress(bool value) {
_controller.pressed = value;
if (value) {
_pressCount++;
final initialPressCount = _pressCount;
_unpressAfterDelay(initialPressCount);
}
}

void _onPanEnd(DragEndDetails details) {
_handlePress(true);
handlePress(value: true, delay: widget.unpressDelay);
widget.onPanEnd?.call(details);
}

void _onTapUp(TapUpDetails details) {
_controller.longPressed = false;
widget.onTapUp?.call(details);
}

void _onTapCancel() {
_controller.longPressed = false;
widget.onTapCancel?.call();
}

void _onLongPressStart(LongPressStartDetails details) {
_controller.longPressed = true;
controller.longPressed = true;
widget.onLongPressStart?.call(details);
}

void _onLongPressEnd(LongPressEndDetails details) {
_controller.longPressed = false;
controller.longPressed = false;
controller.pressed = false;
setState(() {
_event = PressEvent.idle;
});
widget.onLongPressEnd?.call(details);
}

void _onLongPressCancel() {
_controller.longPressed = false;
controller.longPressed = false;
widget.onLongPressCancel?.call();
}

Expand All @@ -149,28 +179,30 @@ class _GestureMixStateWidgetState extends State<GestureMixStateWidget> {
widget.onPanStart?.call(details);
}

void _unpressAfterDelay(int initialPressCount) {
void unpressCallback() {
if (_controller.pressed && _pressCount == initialPressCount) {
_controller.pressed = false;
}
}
void _onTap() {
handlePress(value: true, delay: widget.unpressDelay);

_timer?.cancel();
widget.onTap?.call();
if (widget.enableFeedback) Feedback.forTap(context);
}

final delay = widget.unpressDelay;
void _onTapDown(TapDownDetails details) {
setState(() {
_event = PressEvent.onTapDown;
});
}

if (delay != Duration.zero) {
_timer = Timer(delay, unpressCallback);
} else {
unpressCallback();
}
void _onTapUp(TapUpDetails details) {
setState(() {
_event = PressEvent.onTapUp;
});
controller.longPressed = false;
widget.onTapUp?.call(details);
}

void _onTap() {
_handlePress(true);
widget.onTap?.call();
if (widget.enableFeedback) Feedback.forTap(context);
void _onTapCancel() {
controller.longPressed = false;
widget.onTapCancel?.call();
}

void _onLongPress() {
Expand All @@ -182,28 +214,40 @@ class _GestureMixStateWidgetState extends State<GestureMixStateWidget> {
void dispose() {
_timer?.cancel();
// Dispose if being managed internally
if (widget.controller == null) _controller.dispose();
if (widget.controller == null) controller.dispose();
super.dispose();
}

@override
void pressCallback() {
setState(() {
_event = PressEvent.idle;
});
}

@override
Widget build(BuildContext context) {
return GestureDetector(
onTapUp: widget.onTap != null ? _onTapUp : null,
onTap: widget.onTap != null ? _onTap : null,
onTapCancel: widget.onTap != null ? _onTapCancel : null,
onLongPressCancel: widget.onLongPress != null ? _onLongPressCancel : null,
onLongPress: widget.onLongPress != null ? _onLongPress : null,
onLongPressStart: widget.onLongPress != null ? _onLongPressStart : null,
onLongPressEnd: widget.onLongPress != null ? _onLongPressEnd : null,
onPanDown: widget.onPanDown != null ? _onPanDown : null,
onPanStart: widget.onPanStart != null ? _onPanStart : null,
onPanUpdate: widget.onPanUpdate != null ? _onPanUpdate : null,
onPanEnd: widget.onPanEnd != null ? _onPanEnd : null,
onPanCancel: widget.onPanCancel != null ? _onPanCancel : null,
behavior: widget.hitTestBehavior,
excludeFromSemantics: widget.excludeFromSemantics,
child: widget.child,
return PressEventMixWidgetState(
_event,
child: GestureDetector(
onTapDown: widget.onTap != null ? _onTapDown : null,
onTapUp: widget.onTap != null ? _onTapUp : null,
onTap: widget.onTap != null ? _onTap : null,
onTapCancel: widget.onTap != null ? _onTapCancel : null,
onLongPressCancel:
widget.onLongPress != null ? _onLongPressCancel : null,
onLongPress: widget.onLongPress != null ? _onLongPress : null,
onLongPressStart: widget.onLongPress != null ? _onLongPressStart : null,
onLongPressEnd: widget.onLongPress != null ? _onLongPressEnd : null,
onPanDown: widget.onPanDown != null ? _onPanDown : null,
onPanStart: widget.onPanStart != null ? _onPanStart : null,
onPanUpdate: widget.onPanUpdate != null ? _onPanUpdate : null,
onPanEnd: widget.onPanEnd != null ? _onPanEnd : null,
onPanCancel: widget.onPanCancel != null ? _onPanCancel : null,
behavior: widget.hitTestBehavior,
excludeFromSemantics: widget.excludeFromSemantics,
child: widget.child,
),
);
}
}
22 changes: 22 additions & 0 deletions packages/mix/lib/src/core/widget_state/press_event_mix_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'package:flutter/widgets.dart';

enum PressEvent {
idle,
onTapUp,
onTapDown;
}

class PressEventMixWidgetState extends InheritedWidget {
const PressEventMixWidgetState(this.event, {super.key, required super.child});

static PressEventMixWidgetState? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType();
}

final PressEvent event;

@override
bool updateShouldNotify(PressEventMixWidgetState oldWidget) {
return event != oldWidget.event;
}
}
17 changes: 15 additions & 2 deletions packages/mix/lib/src/variants/widget_state_variant.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import 'package:flutter/widgets.dart';

import '../core/factory/style_mix.dart';
import '../core/variant.dart';
import '../core/widget_state/internal/gesture_mix_state.dart';

Check warning on line 5 in packages/mix/lib/src/variants/widget_state_variant.dart

View workflow job for this annotation

GitHub Actions / Test

Unused import: '../core/widget_state/internal/gesture_mix_state.dart'.

Try removing the import directive. See https://dart.dev/diagnostics/unused_import to learn more about this problem.

Check warning on line 5 in packages/mix/lib/src/variants/widget_state_variant.dart

View workflow job for this annotation

GitHub Actions / Test Min SDK

Unused import: '../core/widget_state/internal/gesture_mix_state.dart'.

Try removing the import directive. See https://dart.dev/diagnostics/unused_import to learn more about this problem.
import '../core/widget_state/internal/mouse_region_mix_state.dart';
import '../core/widget_state/press_event_mix_state.dart';
import '../core/widget_state/widget_state_controller.dart';
import 'context_variant.dart';

Expand Down Expand Up @@ -60,8 +62,19 @@ class OnHoverVariant extends MixWidgetStateVariant<PointerPosition?> {
}

/// Applies styles when the widget is pressed.
class OnPressVariant extends _ToggleMixStateVariant {
const OnPressVariant() : super(MixWidgetState.pressed);
class OnPressVariant extends MixWidgetStateVariant<PressEvent> {
const OnPressVariant();

@override
PressEvent builder(BuildContext context) {
final event = PressEventMixWidgetState.of(context)?.event;

return event ?? PressEvent.idle;
}

@override
bool when(BuildContext context) =>
MixWidgetState.hasStateOf(context, MixWidgetState.pressed);
}

/// Applies styles when the widget is long pressed.
Expand Down
47 changes: 47 additions & 0 deletions packages/mix/test/src/core/mix_state/press_states_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mix/mix.dart';
import 'package:mix/src/core/widget_state/internal/gesture_mix_state.dart';

Check warning on line 4 in packages/mix/test/src/core/mix_state/press_states_test.dart

View workflow job for this annotation

GitHub Actions / Test

Unused import: 'package:mix/src/core/widget_state/internal/gesture_mix_state.dart'.

Try removing the import directive. See https://dart.dev/diagnostics/unused_import to learn more about this problem.

Check warning on line 4 in packages/mix/test/src/core/mix_state/press_states_test.dart

View workflow job for this annotation

GitHub Actions / Test Min SDK

Unused import: 'package:mix/src/core/widget_state/internal/gesture_mix_state.dart'.

Try removing the import directive. See https://dart.dev/diagnostics/unused_import to learn more about this problem.

import '../../../helpers/context_finder.dart';
import '../../../helpers/testing_utils.dart';

extension _WidgetTesterX on WidgetTester {
PressEventMixWidgetState findPressEventWidget() {
return findWidgetOfType<PressEventMixWidgetState>();
}
}

void main() {
testWidgets(
'Pressable should transition through (onTapDown and onTapUp) states correctly',
(tester) async {
int counter = 0;
await tester.pumpMaterialApp(
PressableBox(
onPress: () {
counter++;
},
child: Text('$counter'),
),
);

// Initial state should be idle
expect(tester.findPressEventWidget().event, PressEvent.idle);

// Press down on the PressableBox
final gesture = await tester.press(find.byType(PressableBox));
await tester.pumpAndSettle();
expect(tester.findPressEventWidget().event, PressEvent.onTapDown);

// Release the press
await gesture.up();
await tester.pumpAndSettle();
expect(tester.findPressEventWidget().event, PressEvent.onTapUp);

expect(counter, equals(1));

await tester.pump(const Duration(milliseconds: 300));
expect(tester.findPressEventWidget().event, PressEvent.idle);
});
}

0 comments on commit 4b3744b

Please sign in to comment.