diff --git a/CHANGELOG.md b/CHANGELOG.md index 41b92ca7a..0acebb60c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [Unreleased] +* 🖱️ [#954](https://github.com/fluttercommunity/chewie/pull/954): Auto-hide the mouse cursor along with the controls while idle in fullscreen, and show it again on mouse movement (web and desktop). Configurable via `ChewieController.hideCursorInFullScreen` (default `true`). Thanks [Ortes](https://github.com/Ortes). + ## [1.15.0] * 🌐 [#946](https://github.com/fluttercommunity/chewie/pull/946): Web: enter the browser's native (OS-level) fullscreen via the Fullscreen API instead of only expanding the Flutter view inside the browser window. Pressing Escape to leave browser fullscreen also exits Chewie's fullscreen. Controlled by the new `ChewieController.useNativeFullScreenOnWeb` flag (defaults to `true`; no effect on non-web platforms). Thanks [Ortes](https://github.com/Ortes). * 🖱️ [#950](https://github.com/fluttercommunity/chewie/pull/950): Show click cursor on hover over Material controls and progress bar. Thanks [Ortes](https://github.com/Ortes). diff --git a/lib/src/chewie_player.dart b/lib/src/chewie_player.dart index ead0c6bed..b44cc3872 100644 --- a/lib/src/chewie_player.dart +++ b/lib/src/chewie_player.dart @@ -364,6 +364,7 @@ class ChewieController extends ChangeNotifier { this.hideControlsTimer = defaultHideControlsTimer, this.controlsSafeAreaMinimum = EdgeInsets.zero, this.pauseOnBackgroundTap = false, + this.hideCursorInFullScreen = true, }) : assert( playbackSpeeds.every((speed) => speed > 0), 'The playbackSpeeds values must all be greater than 0', @@ -425,6 +426,7 @@ class ChewieController extends ChangeNotifier { )? routePageBuilder, bool? pauseOnBackgroundTap, + bool? hideCursorInFullScreen, }) { return ChewieController( draggableProgressBar: draggableProgressBar ?? this.draggableProgressBar, @@ -492,6 +494,8 @@ class ChewieController extends ChangeNotifier { progressIndicatorDelay: progressIndicatorDelay ?? this.progressIndicatorDelay, pauseOnBackgroundTap: pauseOnBackgroundTap ?? this.pauseOnBackgroundTap, + hideCursorInFullScreen: + hideCursorInFullScreen ?? this.hideCursorInFullScreen, ); } @@ -685,6 +689,12 @@ class ChewieController extends ChangeNotifier { /// Defines if the player should pause when the background is tapped final bool pauseOnBackgroundTap; + /// Whether the mouse cursor auto-hides together with the controls while in + /// fullscreen (and reappears on mouse movement), like most video players. + /// Has no effect outside fullscreen or on devices without a pointer. + /// Defaults to `true`. + final bool hideCursorInFullScreen; + static ChewieController of(BuildContext context) { final chewieControllerProvider = context .dependOnInheritedWidgetOfExactType()!; diff --git a/lib/src/cupertino/cupertino_controls.dart b/lib/src/cupertino/cupertino_controls.dart index 251700799..259c66645 100644 --- a/lib/src/cupertino/cupertino_controls.dart +++ b/lib/src/cupertino/cupertino_controls.dart @@ -57,6 +57,14 @@ class _CupertinoControlsState extends State ChewieController get chewieController => _chewieController!; ChewieController? _chewieController; + // Hides the mouse cursor along with the controls while idle in fullscreen. + MouseCursor get _idleCursor => + chewieController.hideCursorInFullScreen && + chewieController.isFullScreen && + notifier.hideStuff + ? SystemMouseCursors.none + : MouseCursor.defer; + @override void initState() { super.initState(); @@ -87,6 +95,7 @@ class _CupertinoControlsState extends State final buttonPadding = orientation == Orientation.portrait ? 16.0 : 24.0; return MouseRegion( + cursor: _idleCursor, onHover: (_) => _cancelAndRestartTimer(), child: GestureDetector( onTap: () => _cancelAndRestartTimer(), diff --git a/lib/src/material/material_controls.dart b/lib/src/material/material_controls.dart index 3c5de69c8..43700c1a2 100644 --- a/lib/src/material/material_controls.dart +++ b/lib/src/material/material_controls.dart @@ -51,6 +51,14 @@ class _MaterialControlsState extends State // We know that _chewieController is set in didChangeDependencies ChewieController get chewieController => _chewieController!; + // Hides the mouse cursor along with the controls while idle in fullscreen. + MouseCursor get _idleCursor => + chewieController.hideCursorInFullScreen && + chewieController.isFullScreen && + notifier.hideStuff + ? SystemMouseCursors.none + : MouseCursor.defer; + @override void initState() { super.initState(); @@ -68,6 +76,7 @@ class _MaterialControlsState extends State } return MouseRegion( + cursor: _idleCursor, onHover: (_) { _cancelAndRestartTimer(); }, diff --git a/lib/src/material/material_desktop_controls.dart b/lib/src/material/material_desktop_controls.dart index 42a0c8218..5ec178a39 100644 --- a/lib/src/material/material_desktop_controls.dart +++ b/lib/src/material/material_desktop_controls.dart @@ -53,6 +53,14 @@ class _MaterialDesktopControlsState extends State // We know that _chewieController is set in didChangeDependencies ChewieController get chewieController => _chewieController!; + // Hides the mouse cursor along with the controls while idle in fullscreen. + MouseCursor get _idleCursor => + chewieController.hideCursorInFullScreen && + chewieController.isFullScreen && + notifier.hideStuff + ? SystemMouseCursors.none + : MouseCursor.defer; + @override void initState() { super.initState(); @@ -92,6 +100,7 @@ class _MaterialDesktopControlsState extends State focusNode: _focusNode, onKeyEvent: _handleKeyPress, child: MouseRegion( + cursor: _idleCursor, onHover: (_) { _focusNode.requestFocus(); _cancelAndRestartTimer(); diff --git a/test/hide_cursor_test.dart b/test/hide_cursor_test.dart new file mode 100644 index 000000000..3edca0f08 --- /dev/null +++ b/test/hide_cursor_test.dart @@ -0,0 +1,57 @@ +import 'package:chewie/chewie.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:video_player/video_player.dart'; + +const _src = + 'https://assets.mixkit.co/videos/preview/mixkit-spinning-around-the-earth-29351-large.mp4'; + +ChewieController _controller() { + return ChewieController( + videoPlayerController: VideoPlayerController.networkUrl(Uri.parse(_src)), + autoPlay: false, + looping: false, + customControls: const MaterialDesktopControls(), + ); +} + +Finder _hiddenCursor() => find.byWidgetPredicate( + (w) => w is MouseRegion && w.cursor == SystemMouseCursors.none, +); + +void main() { + testWidgets( + 'cursor is never hidden while not in fullscreen, even when idle', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold(body: Chewie(controller: _controller())), + ), + ); + await tester.pump(); + + // Drive a mouse hover so the controls arm their auto-hide timer, then let + // the player go idle (controls hidden). + final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: tester.getCenter(find.byType(Chewie))); + addTearDown(gesture.removePointer); + await gesture.moveTo(tester.getCenter(find.byType(Chewie))); + await tester.pump(const Duration(seconds: 4)); + + // Idle outside fullscreen must not hide the cursor. + expect(_hiddenCursor(), findsNothing); + }, + ); + + test('hideCursorInFullScreen defaults to true and survives copyWith', () { + final controller = ChewieController( + videoPlayerController: VideoPlayerController.networkUrl(Uri.parse(_src)), + ); + expect(controller.hideCursorInFullScreen, isTrue); + expect( + controller.copyWith(hideCursorInFullScreen: false).hideCursorInFullScreen, + isFalse, + ); + }); +}