Skip to content

Commit 5bec45c

Browse files
committed
Ignore pointer scrolls on viewports that have no content dimensions yet
Fixes #1338
1 parent 3178374 commit 5bec45c

3 files changed

Lines changed: 124 additions & 0 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* This file is part of wger Workout Manager <https://github.com/wger-project>.
3+
* Copyright (c) 2026 - 2026 wger Team
4+
*
5+
* wger Workout Manager is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
import 'package:flutter/material.dart';
20+
21+
/// Ignores mouse wheel and trackpad scrolls until the viewport has content
22+
/// dimensions. The framework reads them unguarded, so a scroll that lands on a
23+
/// not-yet-laid-out list would otherwise crash with a null check.
24+
class LaidOutScrollPhysics extends ScrollPhysics {
25+
const LaidOutScrollPhysics({super.parent});
26+
27+
@override
28+
LaidOutScrollPhysics applyTo(ScrollPhysics? ancestor) {
29+
return LaidOutScrollPhysics(parent: buildParent(ancestor));
30+
}
31+
32+
@override
33+
bool shouldAcceptUserOffset(ScrollMetrics position) {
34+
return position.hasContentDimensions && super.shouldAcceptUserOffset(position);
35+
}
36+
}
37+
38+
/// App-wide scroll behavior: the platform default physics wrapped in
39+
/// [LaidOutScrollPhysics].
40+
class WgerScrollBehavior extends MaterialScrollBehavior {
41+
const WgerScrollBehavior();
42+
43+
@override
44+
ScrollPhysics getScrollPhysics(BuildContext context) {
45+
return const LaidOutScrollPhysics().applyTo(super.getScrollPhysics(context));
46+
}
47+
}

lib/main.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import 'package:wger/core/update_app_screen.dart';
4848
import 'package:wger/core/update_server_screen.dart';
4949
import 'package:wger/core/widgets/about.dart';
5050
import 'package:wger/core/widgets/log_overview.dart';
51+
import 'package:wger/core/widgets/scroll_behavior.dart';
5152
import 'package:wger/features/account/widgets/settings.dart';
5253
import 'package:wger/features/auth/screens/auth_screen.dart';
5354
import 'package:wger/features/auth/screens/auto_login_error_screen.dart';
@@ -240,12 +241,14 @@ class MainApp extends ConsumerWidget {
240241

241242
return authAsync.when(
242243
loading: () => MaterialApp(
244+
scrollBehavior: const WgerScrollBehavior(),
243245
theme: light,
244246
darkTheme: dark,
245247
themeMode: themeMode,
246248
home: const SplashScreen(),
247249
),
248250
error: (error, stack) => MaterialApp(
251+
scrollBehavior: const WgerScrollBehavior(),
249252
theme: light,
250253
darkTheme: dark,
251254
themeMode: themeMode,
@@ -258,6 +261,7 @@ class MainApp extends ConsumerWidget {
258261
title: 'wger',
259262
navigatorKey: navigatorKey,
260263
scaffoldMessengerKey: scaffoldMessengerKey,
264+
scrollBehavior: const WgerScrollBehavior(),
261265
theme: light,
262266
darkTheme: dark,
263267
highContrastTheme: lightHc,
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* This file is part of wger Workout Manager <https://github.com/wger-project>.
3+
* Copyright (c) 2026 - 2026 wger Team
4+
*
5+
* wger Workout Manager is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
import 'package:flutter/material.dart';
20+
import 'package:flutter_test/flutter_test.dart';
21+
import 'package:wger/core/widgets/scroll_behavior.dart';
22+
23+
void main() {
24+
ScrollMetrics metrics({double? minExtent, double? maxExtent, double? pixels}) {
25+
return FixedScrollMetrics(
26+
minScrollExtent: minExtent,
27+
maxScrollExtent: maxExtent,
28+
pixels: pixels,
29+
viewportDimension: 100,
30+
axisDirection: AxisDirection.down,
31+
devicePixelRatio: 1,
32+
);
33+
}
34+
35+
group('LaidOutScrollPhysics', () {
36+
const physics = LaidOutScrollPhysics();
37+
38+
test('rejects user offsets before the viewport has content dimensions', () {
39+
expect(physics.shouldAcceptUserOffset(metrics()), isFalse);
40+
});
41+
42+
test('delegates to the parent once laid out', () {
43+
final chained = physics.applyTo(const ClampingScrollPhysics());
44+
45+
expect(
46+
chained.shouldAcceptUserOffset(metrics(minExtent: 0, maxExtent: 50, pixels: 0)),
47+
isTrue,
48+
);
49+
expect(
50+
chained.shouldAcceptUserOffset(metrics(minExtent: 0, maxExtent: 0, pixels: 0)),
51+
isFalse,
52+
);
53+
});
54+
});
55+
56+
testWidgets('WgerScrollBehavior wraps the platform physics', (tester) async {
57+
late ScrollPhysics physics;
58+
await tester.pumpWidget(
59+
MaterialApp(
60+
scrollBehavior: const WgerScrollBehavior(),
61+
home: Builder(
62+
builder: (context) {
63+
physics = ScrollConfiguration.of(context).getScrollPhysics(context);
64+
return const SizedBox();
65+
},
66+
),
67+
),
68+
);
69+
70+
expect(physics, isA<LaidOutScrollPhysics>());
71+
expect(physics.parent, isNotNull);
72+
});
73+
}

0 commit comments

Comments
 (0)