From 94906cdc6f5318a8393ea2419d71261735be5085 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 14 Nov 2024 19:14:45 +0100 Subject: [PATCH] Restore countdown clock delay --- lib/src/view/watch/tv_screen.dart | 4 +++ lib/src/widgets/countdown_clock.dart | 26 ++++++++++++-- test/widgets/countdown_clock_test.dart | 48 ++++++++++++++++++++++++-- 3 files changed, 72 insertions(+), 6 deletions(-) diff --git a/lib/src/view/watch/tv_screen.dart b/lib/src/view/watch/tv_screen.dart index 3bfb05846e..daaf61ba66 100644 --- a/lib/src/view/watch/tv_screen.dart +++ b/lib/src/view/watch/tv_screen.dart @@ -95,6 +95,8 @@ class _Body extends ConsumerWidget { ? CountdownClock( key: blackClockKey, timeLeft: gameState.game.clock!.black, + delay: gameState.game.clock!.lag ?? + const Duration(milliseconds: 10), clockUpdatedAt: gameState.game.clock!.at, active: gameState.activeClockSide == Side.black, ) @@ -108,6 +110,8 @@ class _Body extends ConsumerWidget { key: whiteClockKey, timeLeft: gameState.game.clock!.white, clockUpdatedAt: gameState.game.clock!.at, + delay: gameState.game.clock!.lag ?? + const Duration(milliseconds: 10), active: gameState.activeClockSide == Side.white, ) : null, diff --git a/lib/src/widgets/countdown_clock.dart b/lib/src/widgets/countdown_clock.dart index 8411ea1a8a..77ff45c2e7 100644 --- a/lib/src/widgets/countdown_clock.dart +++ b/lib/src/widgets/countdown_clock.dart @@ -11,6 +11,7 @@ import 'package:lichess_mobile/src/utils/screen.dart'; class CountdownClock extends StatefulWidget { const CountdownClock({ required this.timeLeft, + this.delay, this.emergencyThreshold, this.clockUpdatedAt, required this.active, @@ -22,6 +23,11 @@ class CountdownClock extends StatefulWidget { /// The duration left on the clock. final Duration timeLeft; + /// The delay before the clock starts counting down. + /// + /// This can be used to implement lag compensation. + final Duration? delay; + /// If [timeLeft] is less than [emergencyThreshold], the clock will set /// its background color to [ClockStyle.emergencyBackgroundColor]. final Duration? emergencyThreshold; @@ -50,20 +56,31 @@ const _showTenthsThreshold = Duration(seconds: 10); class _CountdownClockState extends State { Timer? _timer; + Timer? _delayTimer; Duration timeLeft = Duration.zero; final _stopwatch = clock.stopwatch(); void startClock() { final now = clock.now(); + final delay = widget.delay ?? Duration.zero; final clockUpdatedAt = widget.clockUpdatedAt ?? now; // UI lag diff: the elapsed time between the time the clock should have started // and the time the clock is actually started final uiLag = now.difference(clockUpdatedAt); + final realDelay = delay - uiLag; - timeLeft = timeLeft - uiLag; + // real delay is negative, we need to adjust the timeLeft. + if (realDelay < Duration.zero) { + timeLeft = timeLeft + realDelay; + } - _scheduleTick(); + if (realDelay > Duration.zero) { + _delayTimer?.cancel(); + _delayTimer = Timer(realDelay, _scheduleTick); + } else { + _scheduleTick(); + } } void _scheduleTick() { @@ -74,8 +91,9 @@ class _CountdownClockState extends State { } void _tick() { + final newTimeLeft = timeLeft - _stopwatch.elapsed; setState(() { - timeLeft = timeLeft - _stopwatch.elapsed; + timeLeft = newTimeLeft; if (timeLeft <= Duration.zero) { timeLeft = Duration.zero; } @@ -86,6 +104,7 @@ class _CountdownClockState extends State { } void stopClock() { + _delayTimer?.cancel(); _timer?.cancel(); _stopwatch.stop(); } @@ -119,6 +138,7 @@ class _CountdownClockState extends State { @override void dispose() { super.dispose(); + _delayTimer?.cancel(); _timer?.cancel(); } diff --git a/test/widgets/countdown_clock_test.dart b/test/widgets/countdown_clock_test.dart index 4669f4ae74..e3599aa2d3 100644 --- a/test/widgets/countdown_clock_test.dart +++ b/test/widgets/countdown_clock_test.dart @@ -170,7 +170,48 @@ void main() { expect(find.text('0:09.7', findRichText: true), findsOneWidget); }); - testWidgets('UI lag compensation', (WidgetTester tester) async { + testWidgets('starts with a delay if set', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: CountdownClock( + timeLeft: Duration(seconds: 10), + active: true, + delay: Duration(milliseconds: 250), + ), + ), + ); + expect(find.text('0:10', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 250)); + expect(find.text('0:10', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.9', findRichText: true), findsOneWidget); + }); + + testWidgets('compensates for UI lag', (WidgetTester tester) async { + final now = clock.now(); + await tester.pump(const Duration(milliseconds: 100)); + + await tester.pumpWidget( + MaterialApp( + home: CountdownClock( + timeLeft: const Duration(seconds: 10), + active: true, + delay: const Duration(milliseconds: 200), + clockUpdatedAt: now, + ), + ), + ); + expect(find.text('0:10', findRichText: true), findsOneWidget); + + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:10', findRichText: true), findsOneWidget); + + // delay was 200ms but UI lagged 100ms so with the compensation the clock has started already + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.9', findRichText: true), findsOneWidget); + }); + + testWidgets('UI lag negative start delay', (WidgetTester tester) async { final now = clock.now(); await tester.pump(const Duration(milliseconds: 200)); @@ -179,11 +220,12 @@ void main() { home: CountdownClock( timeLeft: const Duration(seconds: 10), active: true, + delay: const Duration(milliseconds: 100), clockUpdatedAt: now, ), ), ); - // UI lagged 200ms so the clock time is already 200ms ahead - expect(find.text('0:09.8', findRichText: true), findsOneWidget); + // delay was 100ms but UI lagged 200ms so the clock time is already 100ms ahead + expect(find.text('0:09.9', findRichText: true), findsOneWidget); }); }