diff --git a/lib/screens/repo_tab.dart b/lib/screens/repo_tab.dart index 3c70ae0a..6e241e1f 100644 --- a/lib/screens/repo_tab.dart +++ b/lib/screens/repo_tab.dart @@ -128,6 +128,7 @@ class _RepoTabState extends ConsumerState { controller: _searchController, hintText: context.l10n.storeSearch, clearTooltip: context.l10n.dialogClear, + samplesBackdrop: false, onChanged: (value) => ref.read(repoProvider.notifier).setSearchQuery(value), onClear: () => @@ -657,9 +658,11 @@ class _CategoryChip extends ConsumerWidget { label: label, onTap: onTap, excludeSemantics: true, + // Scrolls with the list over the plain page: see ExtensionRepoCard. child: MornyeGlass.navigation( radius: 24, blurEnabled: ref.watch(mornyeBlurEnabledProvider), + samplesBackdrop: false, child: Material( color: Colors.transparent, child: InkWell( diff --git a/lib/widgets/app_search_field.dart b/lib/widgets/app_search_field.dart index 3860ca78..fb61d610 100644 --- a/lib/widgets/app_search_field.dart +++ b/lib/widgets/app_search_field.dart @@ -24,6 +24,7 @@ class AppSearchField extends StatelessWidget { this.suffixIcon, this.focusNode, this.autofocus = false, + this.samplesBackdrop = true, }); final TextEditingController controller; @@ -37,6 +38,10 @@ class AppSearchField extends StatelessWidget { final FocusNode? focusNode; final bool autofocus; + /// See [MornyeGlass.samplesBackdrop]: false where the field scrolls over + /// the plain page background. + final bool samplesBackdrop; + @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; @@ -107,6 +112,7 @@ class AppSearchField extends StatelessWidget { builder: (context, ref, child) => MornyeGlass.navigation( radius: 28, blurEnabled: ref.watch(mornyeBlurEnabledProvider), + samplesBackdrop: samplesBackdrop, child: child!, ), child: field, diff --git a/lib/widgets/extension_repo_card.dart b/lib/widgets/extension_repo_card.dart index aceea6fb..2f1a373d 100644 --- a/lib/widgets/extension_repo_card.dart +++ b/lib/widgets/extension_repo_card.dart @@ -34,9 +34,12 @@ class ExtensionRepoCard extends ConsumerWidget { } return Padding( padding: margin, + // Repo pages scroll these cards over the plain page background, where a + // backdrop blur is invisible but re-filters the whole card every frame. child: MornyeGlass.navigation( radius: 24, blurEnabled: ref.watch(mornyeBlurEnabledProvider), + samplesBackdrop: false, child: Material(color: Colors.transparent, child: child), ), ); diff --git a/lib/widgets/mornye_chrome.dart b/lib/widgets/mornye_chrome.dart index da4daad5..f97e9d4c 100644 --- a/lib/widgets/mornye_chrome.dart +++ b/lib/widgets/mornye_chrome.dart @@ -257,7 +257,8 @@ class MornyeGlass extends StatelessWidget { this.backdropFilter, }) : _useLens = true, firstInGroup = true, - lastInGroup = true; + lastInGroup = true, + samplesBackdrop = true; /// Shares the navigation bar's tint, blur and subtle edge reflections. const MornyeGlass.navigation({ @@ -271,10 +272,17 @@ class MornyeGlass extends StatelessWidget { this.tintOpacity, this.tintColor, this.backdropFilter, + this.samplesBackdrop = true, }) : _useLens = false; final Widget child; final bool blurEnabled; + + /// False for panels that only ever scroll over the plain page background. + /// Blurring a uniform backdrop changes nothing visible but re-filters the + /// panel's whole area on every scroll frame; the translucent tint, rim and + /// clarity preference stay exactly as with sampling. + final bool samplesBackdrop; final double radius; final bool _useLens; final bool firstInGroup; @@ -311,6 +319,7 @@ class MornyeGlass extends StatelessWidget { tintOpacity: tintOpacity, tintColor: tintColor, backdropFilter: backdropFilter, + samplesBackdrop: samplesBackdrop, child: useGlass ? lens(child) : child, ); } @@ -389,6 +398,7 @@ class _MornyeGlassSurface extends StatelessWidget { this.tintOpacity, this.tintColor, this.backdropFilter, + this.samplesBackdrop = true, }); final Widget child; @@ -400,6 +410,7 @@ class _MornyeGlassSurface extends StatelessWidget { final double? tintOpacity; final Color? tintColor; final ImageFilter? backdropFilter; + final bool samplesBackdrop; @override Widget build(BuildContext context) { @@ -452,7 +463,7 @@ class _MornyeGlassSurface extends StatelessWidget { ), child: ClipRRect( borderRadius: shape, - child: useBlur + child: useBlur && samplesBackdrop ? BackdropFilter( filter: backdropFilter ?? (dark ? _darkBackdrop : _backdropBlur), diff --git a/test/mornye_glass_level_test.dart b/test/mornye_glass_level_test.dart index b83056d2..ab2891db 100644 --- a/test/mornye_glass_level_test.dart +++ b/test/mornye_glass_level_test.dart @@ -5,6 +5,7 @@ import 'package:liquid_glass_easy/liquid_glass_easy.dart'; import 'package:spotiflac_android/providers/runtime_profile_provider.dart'; import 'package:spotiflac_android/theme/mornye_theme.dart'; import 'package:spotiflac_android/widgets/app_switch.dart'; +import 'package:spotiflac_android/widgets/extension_repo_card.dart'; import 'package:spotiflac_android/widgets/mornye_chrome.dart'; MornyeGlassLevel _levelFor({ @@ -183,6 +184,70 @@ void main() { }); } + testWidgets('plain-page panels keep the glass tint without a backdrop pass', ( + tester, + ) async { + Color? tintOf(Finder panel) { + final decorated = tester + .widgetList( + find.descendant(of: panel, matching: find.byType(DecoratedBox)), + ) + .map((box) => box.decoration) + .whereType() + .firstWhere((decoration) => decoration.color != null); + return decorated.color; + } + + await tester.pumpWidget( + ProviderScope( + child: MaterialApp( + theme: MornyeTheme.build(Brightness.dark), + home: const Scaffold( + body: Column( + children: [ + MornyeGlass.navigation( + key: ValueKey('sampled'), + blurEnabled: true, + child: SizedBox(width: 200, height: 40), + ), + MornyeGlass.navigation( + key: ValueKey('plain-page'), + blurEnabled: true, + samplesBackdrop: false, + child: SizedBox(width: 200, height: 40), + ), + ExtensionRepoCard(child: SizedBox(width: 200, height: 40)), + ], + ), + ), + ), + ), + ); + await tester.pump(); + + final sampled = find.byKey(const ValueKey('sampled')); + final plain = find.byKey(const ValueKey('plain-page')); + expect( + find.descendant(of: sampled, matching: find.byType(BackdropFilter)), + findsOneWidget, + ); + expect( + find.descendant(of: plain, matching: find.byType(BackdropFilter)), + findsNothing, + ); + expect( + find.descendant( + of: find.byType(ExtensionRepoCard), + matching: find.byType(BackdropFilter), + ), + findsNothing, + ); + // Same translucent material, so the page looks identical behind it. + expect(tintOf(plain), tintOf(sampled)); + expect(tintOf(plain)!.a, lessThan(1)); + expect(tester.takeException(), isNull); + }); + testWidgets('liquid level keeps the shader glass', (tester) async { await tester.pumpWidget( ProviderScope(