From 0cda4a1241f565c6d9b32fb5c2bc56a74a194349 Mon Sep 17 00:00:00 2001 From: Ayushya Amitabh Date: Tue, 6 Oct 2026 19:46:54 -0400 Subject: [PATCH] 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 --- .claude/context/styling.md | 15 ++- lib/main.dart | 2 + lib/providers/settings_controller.dart | 66 ++++++++++ lib/widgets/bottom_nav_bar.dart | 6 + lib/widgets/noo/core/noo_slider.dart | 51 ++++++++ lib/widgets/noo/nav/noo_bottom_bar.dart | 40 +++++-- lib/widgets/settings/settings_appearance.dart | 113 ++++++++++++++++++ test/providers/settings_frosted_test.dart | 85 +++++++++++++ test/widgets/noo/core_test.dart | 30 +++++ test/widgets/noo/nav_test.dart | 39 ++++++ 10 files changed, 431 insertions(+), 16 deletions(-) create mode 100644 lib/widgets/noo/core/noo_slider.dart create mode 100644 test/providers/settings_frosted_test.dart diff --git a/.claude/context/styling.md b/.claude/context/styling.md index 1f78a3e..b4913bc 100644 --- a/.claude/context/styling.md +++ b/.claude/context/styling.md @@ -86,7 +86,7 @@ Catalog: | Folder | Components | |---|---| -| `core/` | `NooButton`, `NooFab`, `NooChip`, `NooSegmentedControl`, `NooToggle`, `NooSearchField`, `NooAvatar`, `NooBadge`, `NooProgressBar` | +| `core/` | `NooButton`, `NooFab`, `NooChip`, `NooSegmentedControl`, `NooToggle`, `NooSlider`, `NooSearchField`, `NooAvatar`, `NooBadge`, `NooProgressBar` | | `lists/` | `NooGroupedList`, `NooSettingsRow`, `NooTabOrderRow`, `NooBanner`, `NooInfoNote` (tinted info callout; `SettingsSection(notice:)`), `NooSummaryCard`, `NooSelectionBar` | | `files/` | `NooFileKind` (spec ยง1.2 tiles; `NooFileKind.from(name:, mimeType:, isDirectory:)`), `NooFileTile`, `NooStatusIcon`/`NooSyncStatus`, `NooFileRow` (mobile 64px), `NooFileTableHeader`/`NooFileTableRow` (desktop), `NooSwipeAction` | | `media/` | `NooGridCard`, `NooPhotoTile` (video badge, selection), `NooPhotoGroupHeader`/`NooPhotoGrid` (sliver, or `.box`), `NooActivityItem`, `NooStatCard` | @@ -255,9 +255,16 @@ blocks are noted where they matter: only the attached bar still has a distinct iOS row. A separate, orthogonal `SettingsController.bottomBarFrosted` toggle (Settings โ†’ Appearance โ†’ "Frosted glass bottom bar", off by default, both platforms and both bar - styles) swaps the bar's solid `surface` for a 0.72-alpha one over a - 20-sigma `BackdropFilter` (same sigma as `FrostedGlassContainer`) and drops - `nooDialogShadow` (it would show through the glass). A frosted bar only + styles) swaps the bar's solid `surface` for a translucent one over a + `BackdropFilter` blur and drops + `nooDialogShadow` (it would show through the glass). Strength is user-set: + `FrostedGlassPreset` Less (blur 10 / opacity 0.88), Default (20 / 0.72, the + old fixed values) and More (32 / 0.55), plus Blur (0-40) and Opacity + (0.3-1.0) sliders (`NooSlider`, `core/`) in Settings -> Appearance, shown + only while frosted glass is on; both persist + (`ui_bottom_bar_frosted_blur`/`_opacity`) and reach the bar as + `NooBottomBar.frostedBlur`/`frostedOpacity`. No preset is highlighted once + the sliders are moved off all three. A frosted bar only reads as glass with content behind it, so the shell uses `NooBottomBar.drawsBehindBody(barStyle, frosted)` for `extendBody`, and `bottomBarClearance` (and Files' own trailing sliver) reserve the bar's diff --git a/lib/main.dart b/lib/main.dart index 0e253d0..d042399 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -564,6 +564,8 @@ class _MainShellViewState extends State { style: navStyle, barStyle: bottomBarStyle, frosted: settings.bottomBarFrosted, + frostedBlur: settings.bottomBarFrostedBlur, + frostedOpacity: settings.bottomBarFrostedOpacity, tabs: pinnedTabs, selectedIndex: pinnedIndex, onSearchTap: showBottomBarSearch ? () => openSearch(context) : null, diff --git a/lib/providers/settings_controller.dart b/lib/providers/settings_controller.dart index b01098a..46f7f92 100644 --- a/lib/providers/settings_controller.dart +++ b/lib/providers/settings_controller.dart @@ -21,6 +21,30 @@ enum FabStyle { auto, mini, expanded } /// animated wave with a tick-mark thumb ([squiggly]). enum MediaProgressBarStyle { classic, wavy, slim, squiggly } +/// The frosted-glass bottom bar's blur sigma and surface opacity, in three +/// ready-made strengths (Settings, Appearance). Moving either slider off all +/// three leaves no preset selected (a custom mix). +enum FrostedGlassPreset { + less(blur: 10, opacity: 0.88, label: 'Less'), + standard(blur: 20, opacity: 0.72, label: 'Default'), + more(blur: 32, opacity: 0.55, label: 'More'); + + final double blur; + final double opacity; + final String label; + + const FrostedGlassPreset({ + required this.blur, + required this.opacity, + required this.label, + }); +} + +const double minFrostedBlur = 0; +const double maxFrostedBlur = 40; +const double minFrostedOpacity = 0.3; +const double maxFrostedOpacity = 1.0; + /// Global (not per-account) UI settings: theme, bottom-nav appearance, tab /// configuration, swipe actions. Independent of login state - these mean /// the same thing whether any account is signed in or not, unlike @@ -39,6 +63,8 @@ class SettingsController extends ChangeNotifier { static const _prefFabStyle = 'ui_fab_style'; static const _prefBottomBarStyle = 'ui_bottom_bar_style'; static const _prefBottomBarFrosted = 'ui_bottom_bar_frosted'; + static const _prefFrostedBlur = 'ui_bottom_bar_frosted_blur'; + static const _prefFrostedOpacity = 'ui_bottom_bar_frosted_opacity'; static const _prefNavMenuStyle = 'ui_nav_menu_style'; static const _prefSearchInBottomBar = 'ui_search_in_bottom_bar'; static const _prefAmoledDark = 'ui_amoled_dark'; @@ -53,6 +79,8 @@ class SettingsController extends ChangeNotifier { NooBottomBarStyle _bottomBarStyle = NooBottomBarStyle.attached; NooNavMenuStyle _navMenuStyle = NooNavMenuStyle.drawer; bool _bottomBarFrosted = false; + double _frostedBlur = FrostedGlassPreset.standard.blur; + double _frostedOpacity = FrostedGlassPreset.standard.opacity; bool _searchInBottomBar = false; bool _useDynamicColor = true; bool _amoledDark = false; @@ -98,6 +126,14 @@ class SettingsController extends ChangeNotifier { ThemeMode get themeMode => _themeMode; NooBottomBarStyle get bottomBarStyle => _bottomBarStyle; bool get bottomBarFrosted => _bottomBarFrosted; + double get bottomBarFrostedBlur => _frostedBlur; + double get bottomBarFrostedOpacity => _frostedOpacity; + + /// The preset the current blur/opacity match exactly, or null when the + /// sliders have been moved to something custom. + FrostedGlassPreset? get frostedPreset => FrostedGlassPreset.values + .where((p) => p.blur == _frostedBlur && p.opacity == _frostedOpacity) + .firstOrNull; NooNavMenuStyle get navMenuStyle => _navMenuStyle; bool get searchInBottomBar => _searchInBottomBar; bool get useDynamicColor => _useDynamicColor; @@ -160,6 +196,15 @@ class SettingsController extends ChangeNotifier { } _bottomBarFrosted = prefs.getBool(_prefBottomBarFrosted) ?? _bottomBarFrosted; + _frostedBlur = (prefs.getDouble(_prefFrostedBlur) ?? _frostedBlur).clamp( + minFrostedBlur, + maxFrostedBlur, + ); + _frostedOpacity = + (prefs.getDouble(_prefFrostedOpacity) ?? _frostedOpacity).clamp( + minFrostedOpacity, + maxFrostedOpacity, + ); _searchInBottomBar = prefs.getBool(_prefSearchInBottomBar) ?? _searchInBottomBar; _useDynamicColor = @@ -318,6 +363,27 @@ class SettingsController extends ChangeNotifier { _prefsFuture.then((p) => p.setString(_prefBottomBarStyle, style.name)); } + void setFrostedBlur(double value) { + final v = value.clamp(minFrostedBlur, maxFrostedBlur); + if (_frostedBlur == v) return; + _frostedBlur = v; + notifyListeners(); + _prefsFuture.then((p) => p.setDouble(_prefFrostedBlur, v)); + } + + void setFrostedOpacity(double value) { + final v = value.clamp(minFrostedOpacity, maxFrostedOpacity); + if (_frostedOpacity == v) return; + _frostedOpacity = v; + notifyListeners(); + _prefsFuture.then((p) => p.setDouble(_prefFrostedOpacity, v)); + } + + void setFrostedPreset(FrostedGlassPreset preset) { + setFrostedBlur(preset.blur); + setFrostedOpacity(preset.opacity); + } + void setBottomBarFrosted(bool value) { if (_bottomBarFrosted == value) return; _bottomBarFrosted = value; diff --git a/lib/widgets/bottom_nav_bar.dart b/lib/widgets/bottom_nav_bar.dart index c2dce07..15418a9 100644 --- a/lib/widgets/bottom_nav_bar.dart +++ b/lib/widgets/bottom_nav_bar.dart @@ -17,6 +17,8 @@ class BottomNavBar extends StatelessWidget { final NooNavStyle style; final NooBottomBarStyle barStyle; final bool frosted; + final double frostedBlur; + final double frostedOpacity; final List tabs; final int selectedIndex; final ValueChanged onDestinationSelected; @@ -27,6 +29,8 @@ class BottomNavBar extends StatelessWidget { required this.style, this.barStyle = NooBottomBarStyle.attached, this.frosted = false, + this.frostedBlur = NooBottomBar.defaultFrostedBlur, + this.frostedOpacity = NooBottomBar.defaultFrostedOpacity, required this.tabs, required this.selectedIndex, required this.onDestinationSelected, @@ -39,6 +43,8 @@ class BottomNavBar extends StatelessWidget { style: style, barStyle: barStyle, frosted: frosted, + frostedBlur: frostedBlur, + frostedOpacity: frostedOpacity, destinations: [ for (final tab in tabs) NooNavDestination(icon: tab.icon, label: tab.label), diff --git a/lib/widgets/noo/core/noo_slider.dart b/lib/widgets/noo/core/noo_slider.dart new file mode 100644 index 0000000..ae9fc59 --- /dev/null +++ b/lib/widgets/noo/core/noo_slider.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import '../../../theme/design_tokens.dart'; + +/// A continuous (or [divisions]-stepped) value slider in the design system's +/// colors: accent fill on a `surface3` track, accent thumb. Data-agnostic - +/// the caller owns [value] and formats any readout beside it. +class NooSlider extends StatelessWidget { + final double value; + final double min; + final double max; + final int? divisions; + final ValueChanged? onChanged; + final String? semanticLabel; + + const NooSlider({ + super.key, + required this.value, + required this.min, + required this.max, + this.divisions, + this.onChanged, + this.semanticLabel, + }); + + @override + Widget build(BuildContext context) { + final colors = context.nooColors; + return SliderTheme( + data: SliderTheme.of(context).copyWith( + trackHeight: 4, + activeTrackColor: colors.accent, + inactiveTrackColor: colors.surface3, + thumbColor: colors.accent, + overlayColor: colors.accent.withValues(alpha: 0.12), + thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 9), + trackShape: const RoundedRectSliderTrackShape(), + showValueIndicator: ShowValueIndicator.never, + ), + child: Slider( + value: value.clamp(min, max), + min: min, + max: max, + divisions: divisions, + semanticFormatterCallback: semanticLabel == null + ? null + : (v) => '$semanticLabel ${v.toStringAsFixed(2)}', + onChanged: onChanged, + ), + ); + } +} diff --git a/lib/widgets/noo/nav/noo_bottom_bar.dart b/lib/widgets/noo/nav/noo_bottom_bar.dart index 95a3ce0..51a4f9e 100644 --- a/lib/widgets/noo/nav/noo_bottom_bar.dart +++ b/lib/widgets/noo/nav/noo_bottom_bar.dart @@ -14,11 +14,6 @@ const double _kFloatingPillTop = 8; const double _kAndroidPillWidth = 56; const double _kAndroidPillHeight = 32; -/// Frosted glass: the surface fill's opacity and the backdrop blur sigma - -/// the same sigma [FrostedGlassContainer] uses for the media viewer chrome. -const double _kFrostedAlpha = 0.72; -const double _kFrostedSigma = 20; - /// The mobile bottom bar (DESIGN_SYSTEM.md 3, "Mobile"; `iNav`/`aNav` in /// `Mobile Screen.dc.html`). Shows the 5 pinned tabs - which ones, and in /// what order, is the caller's business; this only draws [destinations]. @@ -71,6 +66,12 @@ class NooBottomBar extends StatelessWidget { final NooNavStyle style; final NooBottomBarStyle barStyle; final bool frosted; + + /// Backdrop blur sigma and surface-fill opacity while [frosted] (user- + /// adjustable in Settings; the defaults match [FrostedGlassContainer]'s + /// blur). + final double frostedBlur; + final double frostedOpacity; final List destinations; final int selectedIndex; final ValueChanged onSelected; @@ -82,6 +83,8 @@ class NooBottomBar extends StatelessWidget { required this.style, this.barStyle = NooBottomBarStyle.attached, this.frosted = false, + this.frostedBlur = defaultFrostedBlur, + this.frostedOpacity = defaultFrostedOpacity, required this.destinations, required this.selectedIndex, required this.onSelected, @@ -99,6 +102,9 @@ class NooBottomBar extends StatelessWidget { return barStyle == NooBottomBarStyle.floating ? 64 : 80; } + static const double defaultFrostedBlur = 20; + static const double defaultFrostedOpacity = 0.72; + /// Whether the host `Scaffold` must draw its body behind the bar /// (`extendBody`) - true for floating (its transparent margin) and for any /// frosted bar (there's nothing to blur otherwise). @@ -144,7 +150,9 @@ class NooBottomBar extends StatelessWidget { ), child: row, ); - final pill = frosted ? _blurred(pillBox, pillRadius) : pillBox; + final pill = frosted + ? _blurred(pillBox, pillRadius, frostedBlur) + : pillBox; return SafeArea( top: false, child: Padding( @@ -176,6 +184,8 @@ class NooBottomBar extends StatelessWidget { onTap: onSearchTap!, size: barHeight, frosted: frosted, + frostedBlur: frostedBlur, + frostedOpacity: frostedOpacity, ), ], ), @@ -195,11 +205,11 @@ class NooBottomBar extends StatelessWidget { child: SizedBox(height: barHeight, child: row), ), ); - return frosted ? _blurred(bar, BorderRadius.zero) : bar; + return frosted ? _blurred(bar, BorderRadius.zero, frostedBlur) : bar; } Color _fill(NooColors colors) => frosted - ? colors.surface.withValues(alpha: _kFrostedAlpha) + ? colors.surface.withValues(alpha: frostedOpacity) : colors.surface; Widget _buildIosRow({NooNavDestination? trailingSearch}) { @@ -286,10 +296,10 @@ class NooBottomBar extends StatelessWidget { /// Clips [child] to [radius] and blurs whatever is drawn behind it - the /// frosted bar's backdrop. [child] supplies the translucent fill on top. -Widget _blurred(Widget child, BorderRadius radius) => ClipRRect( +Widget _blurred(Widget child, BorderRadius radius, double sigma) => ClipRRect( borderRadius: radius, child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: _kFrostedSigma, sigmaY: _kFrostedSigma), + filter: ImageFilter.blur(sigmaX: sigma, sigmaY: sigma), child: child, ), ); @@ -302,12 +312,16 @@ class _SearchSatellite extends StatelessWidget { final VoidCallback onTap; final double size; final bool frosted; + final double frostedBlur; + final double frostedOpacity; const _SearchSatellite({ required this.destination, required this.onTap, required this.size, required this.frosted, + required this.frostedBlur, + required this.frostedOpacity, }); @override @@ -331,7 +345,7 @@ class _SearchSatellite extends StatelessWidget { alignment: Alignment.center, decoration: BoxDecoration( color: frosted - ? colors.surface.withValues(alpha: _kFrostedAlpha) + ? colors.surface.withValues(alpha: frostedOpacity) : colors.surface, border: Border.all(color: colors.line), shape: BoxShape.circle, @@ -339,7 +353,9 @@ class _SearchSatellite extends StatelessWidget { ), child: Icon(destination.icon, size: 24, color: colors.fg1), ); - return frosted ? _blurred(disc, BorderRadius.circular(size / 2)) : disc; + return frosted + ? _blurred(disc, BorderRadius.circular(size / 2), frostedBlur) + : disc; } } diff --git a/lib/widgets/settings/settings_appearance.dart b/lib/widgets/settings/settings_appearance.dart index 23d73c5..50267eb 100644 --- a/lib/widgets/settings/settings_appearance.dart +++ b/lib/widgets/settings/settings_appearance.dart @@ -5,6 +5,7 @@ import '../../providers/settings_controller.dart'; import '../../theme/app_theme.dart'; import '../../theme/design_tokens.dart'; import '../noo/core/noo_segmented_control.dart'; +import '../noo/core/noo_slider.dart'; import '../noo/core/noo_toggle.dart'; import '../noo/lists/noo_settings_row.dart'; import '../noo/nav/noo_nav_style.dart'; @@ -64,6 +65,7 @@ class SettingsAppearanceSection extends StatelessWidget { onChanged: settings.setBottomBarFrosted, ), ), + if (settings.bottomBarFrosted) _FrostedGlassRow(settings: settings), _NavMenuStyleRow(settings: settings), NooSettingsRow( icon: LucideIcons.search, @@ -251,6 +253,117 @@ class _BottomBarStyleRow extends StatelessWidget { } } +/// How strong the frosted-glass bottom bar looks: three presets for a quick +/// pick, and the blur / opacity sliders they set, for anything in between. +/// Only shown while frosted glass is on. +class _FrostedGlassRow extends StatelessWidget { + final SettingsController settings; + + const _FrostedGlassRow({required this.settings}); + + @override + Widget build(BuildContext context) { + final colors = context.nooColors; + return Material( + color: colors.surface, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: NooSpace.md, + vertical: 12, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(LucideIcons.sparkles, size: 20, color: colors.fg2), + const SizedBox(width: 14), + Text( + 'Frost strength', + style: NooText.bodyL.copyWith(color: colors.fg1), + ), + ], + ), + const SizedBox(height: 12), + NooSegmentedControl( + fill: true, + // No segment selected once the sliders no longer match a preset. + value: settings.frostedPreset, + onChanged: (preset) { + if (preset != null) settings.setFrostedPreset(preset); + }, + options: [ + for (final preset in FrostedGlassPreset.values) + NooSegmentOption( + value: preset, + label: preset.label, + ), + ], + ), + const SizedBox(height: 8), + _SliderLine( + label: 'Blur', + readout: settings.bottomBarFrostedBlur.round().toString(), + slider: NooSlider( + value: settings.bottomBarFrostedBlur, + min: minFrostedBlur, + max: maxFrostedBlur, + semanticLabel: 'Blur', + onChanged: settings.setFrostedBlur, + ), + ), + _SliderLine( + label: 'Opacity', + readout: '${(settings.bottomBarFrostedOpacity * 100).round()}%', + slider: NooSlider( + value: settings.bottomBarFrostedOpacity, + min: minFrostedOpacity, + max: maxFrostedOpacity, + semanticLabel: 'Opacity', + onChanged: settings.setFrostedOpacity, + ), + ), + ], + ), + ), + ); + } +} + +class _SliderLine extends StatelessWidget { + final String label; + final String readout; + final Widget slider; + + const _SliderLine({ + required this.label, + required this.readout, + required this.slider, + }); + + @override + Widget build(BuildContext context) { + final colors = context.nooColors; + return Row( + children: [ + SizedBox( + width: 64, + child: Text(label, style: NooText.body.copyWith(color: colors.fg2)), + ), + Expanded(child: slider), + SizedBox( + width: 44, + child: Text( + readout, + textAlign: TextAlign.end, + style: NooText.body.copyWith(color: colors.fg3), + ), + ), + ], + ); + } +} + /// "Navigation menu" (Settings โ†’ Appearance): which widget opens hidden /// tabs + Settings - the original `menu` icon that opens a left `Drawer`, /// or the avatar button instead (opens `showAvatarMenu`, a dropdown diff --git a/test/providers/settings_frosted_test.dart b/test/providers/settings_frosted_test.dart new file mode 100644 index 0000000..c41e82a --- /dev/null +++ b/test/providers/settings_frosted_test.dart @@ -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 build() async { + final settings = SettingsController(); + // `_load` is async and not exposed; one event-loop turn lets it finish. + await Future.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); + }); +} diff --git a/test/widgets/noo/core_test.dart b/test/widgets/noo/core_test.dart index 143d373..504686a 100644 --- a/test/widgets/noo/core_test.dart +++ b/test/widgets/noo/core_test.dart @@ -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, diff --git a/test/widgets/noo/nav_test.dart b/test/widgets/noo/nav_test.dart index 39d3fb6..84b1fd0 100644 --- a/test/widgets/noo/nav_test.dart +++ b/test/widgets/noo/nav_test.dart @@ -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(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),