viewer: chrome reveal system — LeafViewerChrome state, LeafChrome layers, tap-to-toggle on zoomable pages, opt-in showPageCounter
This commit is contained in:
1 parent
9bb0ff9187
commit
3cc02c9bbe
6 files changed
+342
-22
No files matched your search
@@ -7,6 +7,7 @@ export 'src/misc/animated_favorite_button.dart';
|
|||||||
export 'src/misc/animated_star.dart';
|
export 'src/misc/animated_star.dart';
|
||||||
export 'src/misc/leaf_async_chip.dart';
|
export 'src/misc/leaf_async_chip.dart';
|
||||||
export 'src/popover/leaf_popover.dart';
|
export 'src/popover/leaf_popover.dart';
|
||||||
|
export 'src/viewer/leaf_chrome.dart';
|
||||||
export 'src/viewer/leaf_viewer.dart';
|
export 'src/viewer/leaf_viewer.dart';
|
||||||
export 'src/viewer/leaf_zoom_controller.dart';
|
export 'src/viewer/leaf_zoom_controller.dart';
|
||||||
export 'src/viewer/leaf_zoomable.dart';
|
export 'src/viewer/leaf_zoomable.dart';
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'leaf_viewer_internal.dart';
|
||||||
|
|
||||||
|
/// Reveal state for everything that hides/shows on tap inside a [LeafViewer]:
|
||||||
|
/// video controls, post action rails, the viewer's own paging chrome.
|
||||||
|
///
|
||||||
|
/// One state, one timer. The auto-hide timer runs whenever chrome is visible
|
||||||
|
/// and not held — [hold] only *suppresses* hiding (e.g. a paused video keeps
|
||||||
|
/// controls up); it never shows anything and never cancels hiding while
|
||||||
|
/// playing. Media pages drive [hold]; passive layers just listen.
|
||||||
|
class LeafViewerChrome {
|
||||||
|
LeafViewerChrome({this.hideDelay = const Duration(seconds: 3), this.autoHide = true}) {
|
||||||
|
// Chrome starts visible — arm the hide timer right away, or entry state
|
||||||
|
// would never auto-hide until the first tap.
|
||||||
|
_restartTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How long chrome stays up before auto-hiding while not held.
|
||||||
|
final Duration hideDelay;
|
||||||
|
|
||||||
|
/// Whether chrome auto-hides at all (off on desktop — hiding there is
|
||||||
|
/// tap-driven only).
|
||||||
|
final bool autoHide;
|
||||||
|
|
||||||
|
final ValueNotifier<bool> _visible = ValueNotifier<bool>(true);
|
||||||
|
bool _hold = false;
|
||||||
|
Timer? _timer;
|
||||||
|
|
||||||
|
ValueListenable<bool> get visibleListenable => _visible;
|
||||||
|
bool get visible => _visible.value;
|
||||||
|
|
||||||
|
void toggle() => _visible.value ? hide() : show();
|
||||||
|
|
||||||
|
void show() {
|
||||||
|
_visible.value = true;
|
||||||
|
_restartTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void hide() {
|
||||||
|
_visible.value = false;
|
||||||
|
_cancelTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// While held, the auto-hide timer is suppressed. Releasing restarts it.
|
||||||
|
set hold(bool value) {
|
||||||
|
if (_hold == value) return;
|
||||||
|
_hold = value;
|
||||||
|
value ? _cancelTimer() : _restartTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _restartTimer() {
|
||||||
|
_cancelTimer();
|
||||||
|
if (!autoHide || _hold || !_visible.value) return;
|
||||||
|
_timer = Timer(hideDelay, () => _visible.value = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _cancelTimer() {
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_cancelTimer();
|
||||||
|
_visible.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A passive chrome layer: fades with the viewer's reveal state and stops
|
||||||
|
/// receiving pointers while hidden. Taps outside of actual controls pass
|
||||||
|
/// through to the media underneath.
|
||||||
|
///
|
||||||
|
/// Outside a chrome-enabled [LeafViewer] the layer renders permanently
|
||||||
|
/// visible.
|
||||||
|
class LeafChrome extends StatelessWidget {
|
||||||
|
const LeafChrome({super.key, required this.builder, this.duration = const Duration(milliseconds: 200)});
|
||||||
|
|
||||||
|
final WidgetBuilder builder;
|
||||||
|
final Duration duration;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final chrome = LeafViewerScope.maybeOf(context)?.chrome;
|
||||||
|
if (chrome == null) return builder(context);
|
||||||
|
return ValueListenableBuilder<bool>(
|
||||||
|
valueListenable: chrome.visibleListenable,
|
||||||
|
builder: (context, visible, _) => IgnorePointer(
|
||||||
|
ignoring: !visible,
|
||||||
|
child: AnimatedOpacity(duration: duration, opacity: visible ? 1.0 : 0.0, child: builder(context)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/physics.dart';
|
import 'package:flutter/physics.dart';
|
||||||
@@ -5,6 +6,7 @@ import 'package:flutter/rendering.dart' show RenderViewportBase;
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'leaf_viewer_internal.dart';
|
import 'leaf_viewer_internal.dart';
|
||||||
|
import 'leaf_chrome.dart';
|
||||||
import 'leaf_zoom_controller.dart';
|
import 'leaf_zoom_controller.dart';
|
||||||
|
|
||||||
/// Fullscreen pager with per-page zoom, boundary-aware edge release and a
|
/// Fullscreen pager with per-page zoom, boundary-aware edge release and a
|
||||||
@@ -46,6 +48,8 @@ class LeafViewer extends StatefulWidget {
|
|||||||
this.controlsBuilder,
|
this.controlsBuilder,
|
||||||
this.enableSwipeGestures,
|
this.enableSwipeGestures,
|
||||||
this.enableKeyboard = true,
|
this.enableKeyboard = true,
|
||||||
|
this.enableChrome = false,
|
||||||
|
this.showPageCounter = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
final int pageCount;
|
final int pageCount;
|
||||||
@@ -75,11 +79,31 @@ class LeafViewer extends StatefulWidget {
|
|||||||
/// paging and Esc for dismissal (default true).
|
/// paging and Esc for dismissal (default true).
|
||||||
final bool enableKeyboard;
|
final bool enableKeyboard;
|
||||||
|
|
||||||
|
/// Opt-in chrome reveal: when true the viewer owns a [LeafViewerChrome]
|
||||||
|
/// (reachable via [chromeOf]) whose visibility tap-toggles on the media and
|
||||||
|
/// drives every [LeafChrome] layer plus the paging chrome below. Auto-hide
|
||||||
|
/// applies on touch platforms only; on desktop chrome hides on tap only.
|
||||||
|
final bool enableChrome;
|
||||||
|
|
||||||
|
/// Whether the default control set shows the "current / total" counter.
|
||||||
|
/// Meaningless with huge page counts — turn off there.
|
||||||
|
final bool showPageCounter;
|
||||||
|
|
||||||
/// The zoom controller of the currently active page, if any.
|
/// The zoom controller of the currently active page, if any.
|
||||||
static LeafZoomController? controllerOf(BuildContext context) {
|
static LeafZoomController? controllerOf(BuildContext context) {
|
||||||
return LeafViewerScope.maybeOf(context)?.registry.activeController;
|
return LeafViewerScope.maybeOf(context)?.registry.activeController;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reactive index of the currently settled page, if inside a viewer.
|
||||||
|
static ValueListenable<int>? currentIndexListOf(BuildContext context) {
|
||||||
|
return LeafViewerScope.maybeOf(context)?.currentIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The viewer's chrome reveal controller, if inside a chrome-enabled viewer.
|
||||||
|
static LeafViewerChrome? chromeOf(BuildContext context) {
|
||||||
|
return LeafViewerScope.maybeOf(context)?.chrome;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<LeafViewer> createState() => _LeafViewerState();
|
State<LeafViewer> createState() => _LeafViewerState();
|
||||||
}
|
}
|
||||||
@@ -108,6 +132,7 @@ class _LeafViewerState extends State<LeafViewer> with TickerProviderStateMixin i
|
|||||||
int _currentIndex = 0;
|
int _currentIndex = 0;
|
||||||
bool _dismissing = false;
|
bool _dismissing = false;
|
||||||
Size _viewerSize = Size.zero;
|
Size _viewerSize = Size.zero;
|
||||||
|
LeafViewerChrome? _chrome;
|
||||||
|
|
||||||
LeafZoomController? get _activeController => _controllers[_currentIndex];
|
LeafZoomController? get _activeController => _controllers[_currentIndex];
|
||||||
|
|
||||||
@@ -151,6 +176,9 @@ class _LeafViewerState extends State<LeafViewer> with TickerProviderStateMixin i
|
|||||||
..addListener(() {
|
..addListener(() {
|
||||||
if (mounted) setState(() {});
|
if (mounted) setState(() {});
|
||||||
});
|
});
|
||||||
|
if (widget.enableChrome) {
|
||||||
|
_chrome = LeafViewerChrome(autoHide: !leafIsDesktopPlatform());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -158,6 +186,7 @@ class _LeafViewerState extends State<LeafViewer> with TickerProviderStateMixin i
|
|||||||
_focusNode.dispose();
|
_focusNode.dispose();
|
||||||
_dismissCtrl.dispose();
|
_dismissCtrl.dispose();
|
||||||
_indexNotifier.dispose();
|
_indexNotifier.dispose();
|
||||||
|
_chrome?.dispose();
|
||||||
if (widget.pageController == null) _pageController.dispose();
|
if (widget.pageController == null) _pageController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@@ -256,6 +285,21 @@ class _LeafViewerState extends State<LeafViewer> with TickerProviderStateMixin i
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Chrome reveal ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Wraps the viewer's paging chrome in the chrome reveal, when chrome is on.
|
||||||
|
Widget _chromeRevealed(Widget child) {
|
||||||
|
final chrome = _chrome;
|
||||||
|
if (chrome == null) return child;
|
||||||
|
return ValueListenableBuilder<bool>(
|
||||||
|
valueListenable: chrome.visibleListenable,
|
||||||
|
builder: (context, visible, _) => IgnorePointer(
|
||||||
|
ignoring: !visible,
|
||||||
|
child: AnimatedOpacity(duration: const Duration(milliseconds: 200), opacity: visible ? 1.0 : 0.0, child: child),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Build ───────────────────────────────────────────────────────────────
|
// ── Build ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -271,6 +315,8 @@ class _LeafViewerState extends State<LeafViewer> with TickerProviderStateMixin i
|
|||||||
onKeyEvent: widget.enableKeyboard ? _onKeyEvent : null,
|
onKeyEvent: widget.enableKeyboard ? _onKeyEvent : null,
|
||||||
child: LeafViewerScope(
|
child: LeafViewerScope(
|
||||||
registry: this,
|
registry: this,
|
||||||
|
currentIndex: _indexNotifier,
|
||||||
|
chrome: _chrome,
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
_viewerSize = constraints.biggest;
|
_viewerSize = constraints.biggest;
|
||||||
@@ -319,18 +365,24 @@ class _LeafViewerState extends State<LeafViewer> with TickerProviderStateMixin i
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (widget.controlsBuilder != null)
|
if (widget.controlsBuilder != null)
|
||||||
ValueListenableBuilder<int>(
|
_chromeRevealed(
|
||||||
valueListenable: _indexNotifier,
|
ValueListenableBuilder<int>(
|
||||||
builder: (context, index, _) => widget.controlsBuilder!(
|
valueListenable: _indexNotifier,
|
||||||
context,
|
builder: (context, index, _) => widget.controlsBuilder!(
|
||||||
LeafViewerControls(pageCount: widget.pageCount, currentIndex: index, goToPage: _goToPage),
|
context,
|
||||||
|
LeafViewerControls(pageCount: widget.pageCount, currentIndex: index, goToPage: _goToPage),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else if (desktop)
|
else if (desktop)
|
||||||
ValueListenableBuilder<int>(
|
_chromeRevealed(
|
||||||
valueListenable: _indexNotifier,
|
ValueListenableBuilder<int>(
|
||||||
builder: (context, index, _) =>
|
valueListenable: _indexNotifier,
|
||||||
_LeafViewerDefaultControls(controls: LeafViewerControls(pageCount: widget.pageCount, currentIndex: index, goToPage: _goToPage)),
|
builder: (context, index, _) => _LeafViewerDefaultControls(
|
||||||
|
controls: LeafViewerControls(pageCount: widget.pageCount, currentIndex: index, goToPage: _goToPage),
|
||||||
|
showPageCounter: widget.showPageCounter,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -342,9 +394,10 @@ class _LeafViewerState extends State<LeafViewer> with TickerProviderStateMixin i
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LeafViewerDefaultControls extends StatelessWidget {
|
class _LeafViewerDefaultControls extends StatelessWidget {
|
||||||
const _LeafViewerDefaultControls({required this.controls});
|
const _LeafViewerDefaultControls({required this.controls, this.showPageCounter = true});
|
||||||
|
|
||||||
final LeafViewerControls controls;
|
final LeafViewerControls controls;
|
||||||
|
final bool showPageCounter;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -377,18 +430,19 @@ class _LeafViewerDefaultControls extends StatelessWidget {
|
|||||||
right: 8,
|
right: 8,
|
||||||
child: _ControlButton(icon: Icons.close, onTap: () => Navigator.maybePop(context)),
|
child: _ControlButton(icon: Icons.close, onTap: () => Navigator.maybePop(context)),
|
||||||
),
|
),
|
||||||
Positioned(
|
if (showPageCounter)
|
||||||
left: 0,
|
Positioned(
|
||||||
right: 0,
|
left: 0,
|
||||||
bottom: 16,
|
right: 0,
|
||||||
child: IgnorePointer(
|
bottom: 16,
|
||||||
child: Text(
|
child: IgnorePointer(
|
||||||
'${controls.currentIndex + 1} / ${controls.pageCount}',
|
child: Text(
|
||||||
textAlign: TextAlign.center,
|
'${controls.currentIndex + 1} / ${controls.pageCount}',
|
||||||
style: const TextStyle(color: Colors.white70, fontSize: 14, fontFeatures: [FontFeature.tabularFigures()]),
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(color: Colors.white70, fontSize: 14, fontFeatures: [FontFeature.tabularFigures()]),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'leaf_chrome.dart';
|
||||||
import 'leaf_zoom_controller.dart';
|
import 'leaf_zoom_controller.dart';
|
||||||
|
|
||||||
/// Whether the current platform is a desktop OS. Desktop viewers swap swipe
|
/// Whether the current platform is a desktop OS. Desktop viewers swap swipe
|
||||||
@@ -23,12 +24,20 @@ abstract class LeafViewerRegistry {
|
|||||||
bool get swipeGesturesEnabled;
|
bool get swipeGesturesEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Internal: registry scope shared between [LeafViewer] and [LeafZoomable].
|
/// Internal: registry scope shared between [LeafViewer], [LeafZoomable] and
|
||||||
|
/// the video pages. Carries the viewer's outward state: the active page index
|
||||||
|
/// and, when enabled, the chrome reveal controller.
|
||||||
class LeafViewerScope extends InheritedWidget {
|
class LeafViewerScope extends InheritedWidget {
|
||||||
const LeafViewerScope({required this.registry, super.key, required super.child});
|
const LeafViewerScope({required this.registry, required this.currentIndex, this.chrome, super.key, required super.child});
|
||||||
|
|
||||||
final LeafViewerRegistry registry;
|
final LeafViewerRegistry registry;
|
||||||
|
|
||||||
|
/// The currently settled page index (reactive; also drives the controls).
|
||||||
|
final ValueListenable<int> currentIndex;
|
||||||
|
|
||||||
|
/// Reveal state for chrome layers; null unless the viewer enabled chrome.
|
||||||
|
final LeafViewerChrome? chrome;
|
||||||
|
|
||||||
static LeafViewerScope? maybeOf(BuildContext context) {
|
static LeafViewerScope? maybeOf(BuildContext context) {
|
||||||
return context.dependOnInheritedWidgetOfExactType<LeafViewerScope>();
|
return context.dependOnInheritedWidgetOfExactType<LeafViewerScope>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,6 +227,17 @@ class _LeafZoomableState extends State<LeafZoomable> with TickerProviderStateMix
|
|||||||
(instance) => instance.onDoubleTapDown = (details) => _controller.doubleTap(details.localPosition),
|
(instance) => instance.onDoubleTapDown = (details) => _controller.doubleTap(details.localPosition),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Chrome-enabled viewers: single tap toggles chrome. Registered on
|
||||||
|
// THIS detector so the tap/double-tap arena coordination holds —
|
||||||
|
// a cross-detector tap would fire on the first tap of every
|
||||||
|
// double-tap-zoom.
|
||||||
|
final chrome = LeafViewerScope.maybeOf(context)?.chrome;
|
||||||
|
if (chrome != null) {
|
||||||
|
gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
|
||||||
|
() => TapGestureRecognizer(debugOwner: this),
|
||||||
|
(instance) => instance.onTap = chrome.toggle,
|
||||||
|
);
|
||||||
|
}
|
||||||
return Listener(
|
return Listener(
|
||||||
onPointerSignal: _onPointerSignal,
|
onPointerSignal: _onPointerSignal,
|
||||||
child: RawGestureDetector(
|
child: RawGestureDetector(
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'package:leaf_widgets/leaf_widgets.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('LeafViewerChrome', () {
|
||||||
|
testWidgets('auto-hide fires after hideDelay', (tester) async {
|
||||||
|
final chrome = LeafViewerChrome();
|
||||||
|
addTearDown(chrome.dispose);
|
||||||
|
expect(chrome.visible, isTrue);
|
||||||
|
await tester.pump(const Duration(seconds: 4));
|
||||||
|
expect(chrome.visible, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('hold suppresses the timer until released', (tester) async {
|
||||||
|
final chrome = LeafViewerChrome();
|
||||||
|
addTearDown(chrome.dispose);
|
||||||
|
chrome.hold = true;
|
||||||
|
await tester.pump(const Duration(seconds: 4));
|
||||||
|
expect(chrome.visible, isTrue);
|
||||||
|
chrome.hold = false;
|
||||||
|
await tester.pump(const Duration(seconds: 4));
|
||||||
|
expect(chrome.visible, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('hide cancels a pending timer', (tester) async {
|
||||||
|
final chrome = LeafViewerChrome();
|
||||||
|
await tester.pump(const Duration(seconds: 2));
|
||||||
|
chrome.hide();
|
||||||
|
await tester.pump(const Duration(seconds: 4));
|
||||||
|
expect(chrome.visible, isFalse);
|
||||||
|
chrome.show();
|
||||||
|
expect(chrome.visible, isTrue);
|
||||||
|
chrome.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('autoHide: false never hides', (tester) async {
|
||||||
|
final chrome = LeafViewerChrome(autoHide: false);
|
||||||
|
addTearDown(chrome.dispose);
|
||||||
|
await tester.pump(const Duration(minutes: 1));
|
||||||
|
expect(chrome.visible, isTrue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('viewer chrome integration', () {
|
||||||
|
testWidgets('tap on a zoomable page toggles chrome', (tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: LeafViewer(
|
||||||
|
pageCount: 1,
|
||||||
|
enableChrome: true,
|
||||||
|
pageBuilder: (context, i) => Stack(
|
||||||
|
children: [
|
||||||
|
LeafZoomable(childSize: const Size(400, 300), child: Container(color: const Color(0xFF112233))),
|
||||||
|
LeafChrome(builder: (context) => Text(chromeState(context))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('chrome: visible'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.byType(LeafZoomable));
|
||||||
|
await tester.pump(const Duration(milliseconds: 350));
|
||||||
|
expect(find.text('chrome: hidden'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.byType(LeafZoomable));
|
||||||
|
await tester.pump(const Duration(milliseconds: 350));
|
||||||
|
expect(find.text('chrome: visible'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('LeafChrome stops hit-testing while hidden', (tester) async {
|
||||||
|
var pressed = false;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: LeafViewer(
|
||||||
|
pageCount: 1,
|
||||||
|
enableChrome: true,
|
||||||
|
pageBuilder: (context, i) => Stack(
|
||||||
|
children: [
|
||||||
|
LeafZoomable(childSize: const Size(400, 300), child: Container(color: const Color(0xFF112233))),
|
||||||
|
LeafChrome(
|
||||||
|
builder: (context) => Align(
|
||||||
|
alignment: Alignment.topRight,
|
||||||
|
child: IconButton(onPressed: () => pressed = true, icon: const Icon(Icons.ac_unit)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.byType(LeafZoomable));
|
||||||
|
await tester.pump(const Duration(milliseconds: 350));
|
||||||
|
await tester.tap(find.byIcon(Icons.ac_unit), warnIfMissed: false);
|
||||||
|
expect(pressed, isFalse);
|
||||||
|
|
||||||
|
await tester.tap(find.byType(LeafZoomable));
|
||||||
|
await tester.pump(const Duration(milliseconds: 350));
|
||||||
|
await tester.tap(find.byIcon(Icons.ac_unit));
|
||||||
|
expect(pressed, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('default desktop controls join the reveal and honour showPageCounter', (tester) async {
|
||||||
|
Widget build({required bool showPageCounter}) => MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: LeafViewer(
|
||||||
|
pageCount: 3,
|
||||||
|
enableChrome: true,
|
||||||
|
showPageCounter: showPageCounter,
|
||||||
|
pageBuilder: (context, i) =>
|
||||||
|
LeafZoomable(childSize: const Size(400, 300), child: Container(color: const Color(0xFF112233))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
debugDefaultTargetPlatformOverride = TargetPlatform.linux;
|
||||||
|
await tester.pumpWidget(build(showPageCounter: true));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('1 / 3'), findsOneWidget);
|
||||||
|
|
||||||
|
final chrome = LeafViewer.chromeOf(tester.element(find.byType(LeafZoomable)))!;
|
||||||
|
await tester.tap(find.byType(LeafZoomable));
|
||||||
|
await tester.pump(const Duration(milliseconds: 350));
|
||||||
|
await tester.pump(const Duration(milliseconds: 200));
|
||||||
|
expect(chrome.visible, isFalse);
|
||||||
|
|
||||||
|
await tester.pumpWidget(build(showPageCounter: false));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('1 / 3'), findsNothing);
|
||||||
|
expect(find.byIcon(Icons.chevron_left), findsOneWidget);
|
||||||
|
debugDefaultTargetPlatformOverride = null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
String chromeState(BuildContext context) {
|
||||||
|
final chrome = LeafViewer.chromeOf(context)!;
|
||||||
|
return chrome.visible ? 'chrome: visible' : 'chrome: hidden';
|
||||||
|
}
|
||||||
Reference in new issue
Block a user