Skip to content

Commit

Permalink
Restore countdown clock delay
Browse files Browse the repository at this point in the history
  • Loading branch information
veloce committed Nov 14, 2024
1 parent 8edb9ef commit 94906cd
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 6 deletions.
4 changes: 4 additions & 0 deletions lib/src/view/watch/tv_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand All @@ -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,
Expand Down
26 changes: 23 additions & 3 deletions lib/src/widgets/countdown_clock.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -50,20 +56,31 @@ const _showTenthsThreshold = Duration(seconds: 10);

class _CountdownClockState extends State<CountdownClock> {
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() {
Expand All @@ -74,8 +91,9 @@ class _CountdownClockState extends State<CountdownClock> {
}

void _tick() {
final newTimeLeft = timeLeft - _stopwatch.elapsed;
setState(() {
timeLeft = timeLeft - _stopwatch.elapsed;
timeLeft = newTimeLeft;
if (timeLeft <= Duration.zero) {
timeLeft = Duration.zero;
}
Expand All @@ -86,6 +104,7 @@ class _CountdownClockState extends State<CountdownClock> {
}

void stopClock() {
_delayTimer?.cancel();
_timer?.cancel();
_stopwatch.stop();
}
Expand Down Expand Up @@ -119,6 +138,7 @@ class _CountdownClockState extends State<CountdownClock> {
@override
void dispose() {
super.dispose();
_delayTimer?.cancel();
_timer?.cancel();
}

Expand Down
48 changes: 45 additions & 3 deletions test/widgets/countdown_clock_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand All @@ -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);
});
}

0 comments on commit 94906cd

Please sign in to comment.