Frosted bottom bar: adjustable blur and opacity with Less/Default/More presets

Settings > Appearance shows a preset control plus Blur and Opacity sliders
(new NooSlider) while frosted glass is on; values persist and feed
NooBottomBar.frostedBlur/frostedOpacity. Default matches the old fixed look.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Ayushya Amitabh
2026-10-06 19:46:54 -04:00
co-authored by Claude Sonnet 5.5
parent 0c063c1fb4
commit 0cda4a1241
10 changed files with 431 additions and 16 deletions
+85
View File
@@ -0,0 +1,85 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:noo/providers/settings_controller.dart';
/// The frosted-glass bottom bar's blur/opacity settings and their presets.
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() => SharedPreferences.setMockInitialValues({}));
Future<SettingsController> build() async {
final settings = SettingsController();
// `_load` is async and not exposed; one event-loop turn lets it finish.
await Future<void>.delayed(Duration.zero);
return settings;
}
test('starts on the Default preset', () async {
final settings = await build();
expect(settings.frostedPreset, FrostedGlassPreset.standard);
expect(settings.bottomBarFrostedBlur, 20);
expect(settings.bottomBarFrostedOpacity, 0.72);
});
test('presets set both values and are recognised again', () async {
final settings = await build();
for (final preset in FrostedGlassPreset.values) {
settings.setFrostedPreset(preset);
expect(settings.bottomBarFrostedBlur, preset.blur);
expect(settings.bottomBarFrostedOpacity, preset.opacity);
expect(settings.frostedPreset, preset);
}
});
test('presets run from lighter to stronger frosting', () {
expect(
FrostedGlassPreset.less.blur,
lessThan(FrostedGlassPreset.standard.blur),
);
expect(
FrostedGlassPreset.standard.blur,
lessThan(FrostedGlassPreset.more.blur),
);
expect(
FrostedGlassPreset.less.opacity,
greaterThan(FrostedGlassPreset.standard.opacity),
);
expect(
FrostedGlassPreset.standard.opacity,
greaterThan(FrostedGlassPreset.more.opacity),
);
});
test('moving a slider off every preset leaves none selected', () async {
final settings = await build();
settings.setFrostedBlur(27);
expect(settings.frostedPreset, isNull);
settings.setFrostedBlur(FrostedGlassPreset.standard.blur);
expect(settings.frostedPreset, FrostedGlassPreset.standard);
});
test('values are clamped to their ranges', () async {
final settings = await build();
settings
..setFrostedBlur(500)
..setFrostedOpacity(0);
expect(settings.bottomBarFrostedBlur, maxFrostedBlur);
expect(settings.bottomBarFrostedOpacity, minFrostedOpacity);
settings
..setFrostedBlur(-3)
..setFrostedOpacity(9);
expect(settings.bottomBarFrostedBlur, minFrostedBlur);
expect(settings.bottomBarFrostedOpacity, maxFrostedOpacity);
});
test('saved values are restored (and clamped) on the next launch', () async {
SharedPreferences.setMockInitialValues({
'ui_bottom_bar_frosted_blur': 32.0,
'ui_bottom_bar_frosted_opacity': 7.0,
});
final settings = await build();
expect(settings.bottomBarFrostedBlur, 32);
expect(settings.bottomBarFrostedOpacity, maxFrostedOpacity);
});
}
+30
View File
@@ -10,6 +10,7 @@ import 'package:noo/widgets/noo/core/noo_chip.dart';
import 'package:noo/widgets/noo/core/noo_fab.dart';
import 'package:noo/widgets/noo/core/noo_progress_bar.dart';
import 'package:noo/widgets/noo/core/noo_search_field.dart';
import 'package:noo/widgets/noo/core/noo_slider.dart';
import 'package:noo/widgets/noo/core/noo_segmented_control.dart';
import 'package:noo/widgets/noo/core/noo_toggle.dart';
import 'package:noo/widgets/noo/nav/noo_nav_style.dart';
@@ -98,6 +99,35 @@ void main() {
}
});
group('NooSlider', () {
testNooWidgets('dragging reports a value within min..max', (
tester,
theme,
c,
) async {
var value = 10.0;
await pumpNoo(
tester,
StatefulBuilder(
builder: (context, setState) => SizedBox(
width: 300,
child: NooSlider(
value: value,
min: 0,
max: 40,
onChanged: (v) => setState(() => value = v),
),
),
),
theme: theme,
);
await tester.drag(find.byType(Slider), const Offset(150, 0));
await tester.pump();
expect(value, greaterThan(10));
expect(value, lessThanOrEqualTo(40));
});
});
group('NooAvatar', () {
testNooWidgets('current user uses accent-soft/accent-text', (
tester,
+39
View File
@@ -1,3 +1,4 @@
import 'dart:ui' show ImageFilter;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
@@ -62,6 +63,44 @@ void main() {
}
}
testNooWidgets('frosted bar uses the given blur sigma and fill opacity', (
tester,
theme,
c,
) async {
await pumpNoo(
tester,
SizedBox(
width: 360,
height: 120,
child: NooBottomBar(
style: NooNavStyle.android,
frosted: true,
frostedBlur: 31,
frostedOpacity: 0.5,
destinations: _destinations,
selectedIndex: 0,
onSelected: (_) {},
),
),
theme: theme,
);
expect(
tester.widget<BackdropFilter>(find.byType(BackdropFilter)).filter,
ImageFilter.blur(sigmaX: 31, sigmaY: 31),
);
expect(
find.byWidgetPredicate(
(w) =>
w is Container &&
w.decoration is BoxDecoration &&
(w.decoration as BoxDecoration).color ==
c.surface.withValues(alpha: 0.5),
),
findsOneWidget,
);
});
test('drawsBehindBody: floating or frosted', () {
expect(
NooBottomBar.drawsBehindBody(NooBottomBarStyle.attached, false),