Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: gesture controls #119

Merged
merged 10 commits into from
Dec 30, 2024
4 changes: 2 additions & 2 deletions .github/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@
}
],
"useGitignore": true,
"words": ["flutter_deck", "Mangirdas", "Kazlauskas"],
"ignorePaths": ["doc/website/source/showcase.md"]
"words": ["flutter_deck", "Mangirdas", "Kazlauskas", "subclassing"],
"ignorePaths": ["**/doc/website/source/showcase.md"]
}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ FlutterDeckApp(
),
controls: const FlutterDeckControlsConfiguration(
presenterToolbarVisible: true,
gestures: FlutterDeckGesturesConfiguration.mobileOnly(),
shortcuts: FlutterDeckShortcutsConfiguration(
enabled: true,
nextSlide: SingleActivator(LogicalKeyboardKey.arrowRight),
Expand Down
1 change: 1 addition & 0 deletions doc/website/source/get-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ FlutterDeckApp(
),
controls: const FlutterDeckControlsConfiguration(
presenterToolbarVisible: true,
gestures: FlutterDeckGesturesConfiguration.mobileOnly(),
shortcuts: FlutterDeckShortcutsConfiguration(
enabled: true,
nextSlide: SingleActivator(LogicalKeyboardKey.arrowRight),
Expand Down
18 changes: 15 additions & 3 deletions doc/website/source/playback/controls.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
title: Controls
navOrder: 1
---
By default, every slide deck comes with a presenter toolbar that can be used to control the slide deck. Also, some of the controls can be accessed by using keyboard shortcuts.

By default, every slide deck comes with a presenter toolbar that can be used to control the slide deck. Also, some of the controls can be accessed by using keyboard shortcuts or touch gestures.

To disable all the controls (e.g. you use your own UI to control the slide deck), set the `controls` property for the slide deck configuration to `FlutterDeckControlsConfiguration.disabled()`:

Expand All @@ -24,12 +25,23 @@ FlutterDeckConfiguration(
)
```

To disable the keyboard shortcuts, set the `shortcuts` property to `FlutterDeckShortcutsConfiguration(enabled: false)`:
To disable the keyboard shortcuts, set the `shortcuts` property to `FlutterDeckShortcutsConfiguration.disabled()`:

```dart
FlutterDeckConfiguration(
controls: const FlutterDeckControlsConfiguration(
shortcuts: FlutterDeckShortcutsConfiguration.disabled(),
),
<...>
)
```

To disable the touch gestures, set the `gestures` property to `FlutterDeckGesturesConfiguration.disabled()`:

```dart
FlutterDeckConfiguration(
controls: const FlutterDeckControlsConfiguration(
shortcuts: FlutterDeckShortcutsConfiguration(enabled: false),
gestures: FlutterDeckGesturesConfiguration.disabled(),
),
<...>
)
Expand Down
8 changes: 8 additions & 0 deletions packages/flutter_deck/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# NEXT

- fix: show controls on tap on mobile devices (iOS and Android)
- fix: controls are auto-hidden when the cursor is over them
- feat: add swipe left/right gestures to navigate between slides
- feat: add control gestures configuration
- docs: update documentation website

# 0.17.0

- feat: allow using any widget as a slide
Expand Down
1 change: 1 addition & 0 deletions packages/flutter_deck/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ FlutterDeckApp(
),
controls: const FlutterDeckControlsConfiguration(
presenterToolbarVisible: true,
gestures: FlutterDeckGesturesConfiguration.mobileOnly(),
shortcuts: FlutterDeckShortcutsConfiguration(
enabled: true,
nextSlide: SingleActivator(LogicalKeyboardKey.arrowRight),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ class FlutterDeckConfiguration {
/// for the [FlutterDeckSlide].
final FlutterDeckBackgroundConfiguration background;

/// Configures the controls for the slide deck. By default, the presenter
/// toolbar is visible and the default keyboard controls are enabled.
/// Configures the controls for the slide deck. By default, the presenter
/// toolbar is visible, the default keyboard controls are enabled, and
/// gestures are enabled on mobile platforms only.
///
/// The default keyboard shortcuts are:
/// - Next slide: \[ArrowRight\]
Expand Down
40 changes: 23 additions & 17 deletions packages/flutter_deck/lib/src/controls/flutter_deck_controls.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,25 +69,31 @@ class _Controls extends StatelessWidget {

@override
Widget build(BuildContext context) {
final controlsNotifier = context.flutterDeck.controlsNotifier;

return Theme(
data: ThemeData.light(),
child: Builder(
builder: (context) => Container(
margin: FlutterDeckLayout.slidePadding,
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(32),
color: Theme.of(context).colorScheme.surface,
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
_PreviousButton(),
_SlideNumberButton(),
_NextButton(),
_MarkerControls(),
_OptionsMenuButton(),
],
child: MouseRegion(
onEnter: (_) => controlsNotifier.toggleControlsVisibleDuration(),
onExit: (_) => controlsNotifier.toggleControlsVisibleDuration(),
child: Builder(
builder: (context) => Container(
margin: FlutterDeckLayout.slidePadding,
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(32),
color: Theme.of(context).colorScheme.surface,
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
_PreviousButton(),
_SlideNumberButton(),
_NextButton(),
_MarkerControls(),
_OptionsMenuButton(),
],
),
),
),
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';

/// The configuration for the slide deck controls.
class FlutterDeckControlsConfiguration {
/// Creates a configuration for the slide deck controls. By default, the
/// presenter toolbar is visible and the default keyboard controls are
/// enabled.
/// presenter toolbar is visible, the default keyboard controls are
/// enabled, and gestures are enabled on mobile platforms only.
///
/// The default keyboard shortcuts are:
/// - Next slide: \[ArrowRight\]
Expand All @@ -19,22 +20,61 @@ class FlutterDeckControlsConfiguration {
/// - [LogicalKeyboardKey] for a list of all available keys.
const FlutterDeckControlsConfiguration({
this.presenterToolbarVisible = true,
this.gestures = const FlutterDeckGesturesConfiguration.mobileOnly(),
this.shortcuts = const FlutterDeckShortcutsConfiguration(),
});

/// Creates a configuration for the slide deck controls where they are
/// disabled.
const FlutterDeckControlsConfiguration.disabled()
: presenterToolbarVisible = false,
shortcuts = const FlutterDeckShortcutsConfiguration(enabled: false);
: this(
presenterToolbarVisible: false,
gestures: const FlutterDeckGesturesConfiguration.disabled(),
shortcuts: const FlutterDeckShortcutsConfiguration.disabled(),
);

/// Whether the presenter toolbar is visible or not.
final bool presenterToolbarVisible;

/// The configuration for the slide deck controls gestures.
final FlutterDeckGesturesConfiguration gestures;

/// The configuration for the slide deck keyboard shortcuts.
final FlutterDeckShortcutsConfiguration shortcuts;
}

/// The configuration for the slide deck control gestures.
///
/// The gesture controls are only available on [supportedPlatforms]. By default,
/// gestures are enabled on all platforms.
class FlutterDeckGesturesConfiguration {
/// Creates a configuration for the slide deck control gestures.
const FlutterDeckGesturesConfiguration({
this.supportedPlatforms = const {...TargetPlatform.values},
});

/// Creates a configuration for the slide deck control gestures where they are
/// disabled.
const FlutterDeckGesturesConfiguration.disabled()
: this(supportedPlatforms: const {});

/// Creates a configuration for the slide deck control gestures where they are
/// enabled on mobile platforms only.
const FlutterDeckGesturesConfiguration.mobileOnly()
: this(
supportedPlatforms: const {
TargetPlatform.android,
TargetPlatform.iOS,
},
);

/// The platforms where gestures are enabled.
final Set<TargetPlatform> supportedPlatforms;

/// Whether gestures are enabled on the current platform or not.
bool get enabled => supportedPlatforms.contains(defaultTargetPlatform);
}

/// The configuration for the slide deck keyboard shortcuts.
class FlutterDeckShortcutsConfiguration {
/// Creates a configuration for the slide deck keyboard shortcuts. By default,
Expand All @@ -59,6 +99,10 @@ class FlutterDeckShortcutsConfiguration {
const SingleActivator(LogicalKeyboardKey.period),
});

/// Creates a configuration for the slide deck keyboard shortcuts where they
/// are disabled.
const FlutterDeckShortcutsConfiguration.disabled() : this(enabled: false);

/// Whether keyboard shortcuts are enabled or not.
final bool enabled;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_deck/src/controls/actions/actions.dart';
import 'package:flutter_deck/src/controls/flutter_deck_controls_notifier.dart';
import 'package:flutter_deck/src/flutter_deck.dart';
import 'package:flutter_deck/src/widgets/internal/internal.dart';

/// A widget that handles controls (actions and shortcuts) for the slide deck.
/// A widget that handles controls (actions, gestures and shortcuts) for the
/// slide deck.
///
/// Key bindings are defined in global deck configuration. The following
/// shortcuts are supported:
Expand All @@ -24,19 +26,48 @@ class FlutterDeckControlsListener extends StatelessWidget {
/// [child] is the widget that will be wrapped by this widget. It should be
/// the root of the slide deck.
///
/// [notifier] is the [FlutterDeckControlsNotifier] that will be used to
/// control the slide deck.
/// [controlsNotifier] is the [FlutterDeckControlsNotifier] that will be used
/// to control the slide deck.
///
/// [markerNotifier] is the [FlutterDeckMarkerNotifier] that will be used to
/// control the slide deck's marker.
const FlutterDeckControlsListener({
required this.child,
required this.notifier,
required this.controlsNotifier,
required this.markerNotifier,
super.key,
});

/// The widget below this widget in the tree.
final Widget child;

/// The notifier used to control the slide deck.
final FlutterDeckControlsNotifier notifier;
final FlutterDeckControlsNotifier controlsNotifier;

/// The notifier used to control the slide deck's marker.
final FlutterDeckMarkerNotifier markerNotifier;

void _onHorizontalSwipe(DragEndDetails? details) {
final velocity = details?.primaryVelocity;

if (velocity == null) return;

velocity > 0 ? controlsNotifier.previous() : controlsNotifier.next();
}

void _onMouseHover(PointerEvent event) {
controlsNotifier.showControls();
}

void _onTap() {
final controlsVisible = controlsNotifier.controlsVisible;

controlsNotifier.showControls();

if (!controlsVisible || markerNotifier.enabled) return;

controlsNotifier.next();
}

@override
Widget build(BuildContext context) {
Expand All @@ -45,13 +76,29 @@ class FlutterDeckControlsListener extends StatelessWidget {
Widget widget = Focus(
autofocus: true,
child: ListenableBuilder(
listenable: notifier,
listenable: controlsNotifier,
builder: (context, child) => MouseRegion(
cursor: notifier.controlsVisible
cursor: controlsNotifier.controlsVisible
? MouseCursor.defer
: SystemMouseCursors.none,
onHover: (_) => notifier.showControls(),
child: child,
onHover: _onMouseHover,
child: ListenableBuilder(
listenable: markerNotifier,
builder: (context, child) {
if (!controls.gestures.enabled) return child!;

if (markerNotifier.enabled) {
return GestureDetector(onTap: _onTap, child: child);
}

return GestureDetector(
onHorizontalDragEnd: _onHorizontalSwipe,
onTap: _onTap,
child: child,
);
},
child: child,
),
),
child: child,
),
Expand All @@ -62,10 +109,10 @@ class FlutterDeckControlsListener extends StatelessWidget {
if (controls.presenterToolbarVisible || shortcuts.enabled) {
widget = Actions(
actions: <Type, Action<Intent>>{
GoNextIntent: GoNextAction(notifier),
GoPreviousIntent: GoPreviousAction(notifier),
ToggleDrawerIntent: ToggleDrawerAction(notifier),
ToggleMarkerIntent: ToggleMarkerAction(notifier),
GoNextIntent: GoNextAction(controlsNotifier),
GoPreviousIntent: GoPreviousAction(controlsNotifier),
ToggleDrawerIntent: ToggleDrawerAction(controlsNotifier),
ToggleMarkerIntent: ToggleMarkerAction(controlsNotifier),
},
child: widget,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import 'package:flutter_deck/src/flutter_deck_router.dart';
import 'package:flutter_deck/src/widgets/internal/drawer/drawer.dart';
import 'package:flutter_deck/src/widgets/internal/marker/marker.dart';

const _defaultControlsVisibleDuration = Duration(seconds: 3);

/// The [ChangeNotifier] used to control the slide deck and handle cursor and
/// deck controls visibility.
class FlutterDeckControlsNotifier
Expand All @@ -33,6 +35,7 @@ class FlutterDeckControlsNotifier
final FlutterDeckRouter _router;

var _controlsVisible = false;
var _controlsVisibleDuration = _defaultControlsVisibleDuration;
Timer? _controlsVisibleTimer;

Set<Intent> _disabledIntents = {};
Expand Down Expand Up @@ -116,7 +119,7 @@ class FlutterDeckControlsNotifier
_setControlsVisible(true);

_controlsVisibleTimer = Timer(
const Duration(seconds: 3),
_controlsVisibleDuration,
() => _setControlsVisible(false),
);
}
Expand All @@ -130,6 +133,16 @@ class FlutterDeckControlsNotifier
notifyListeners();
}

/// Toggle the cursor and controls visibility duration.
///
/// The value toggles between 3 seconds and infinite (no auto-hide).
void toggleControlsVisibleDuration() {
_controlsVisibleDuration =
_controlsVisibleDuration > _defaultControlsVisibleDuration
? _defaultControlsVisibleDuration
: const Duration(days: 1); // Infinite enough for this use case...
}

/// Whether the given [intent] is disabled.
bool intentDisabled(Intent intent) => _disabledIntents.contains(intent);
}
3 changes: 2 additions & 1 deletion packages/flutter_deck/lib/src/flutter_deck_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,8 @@ class _FlutterDeckAppState extends State<FlutterDeckApp> {
presenterController: _presenterController,
themeNotifier: _themeNotifier,
child: FlutterDeckControlsListener(
notifier: _controlsNotifier,
controlsNotifier: _controlsNotifier,
markerNotifier: _markerNotifier,
child: FlutterDeckTheme(
data: theme,
child: child!,
Expand Down
Loading
Loading