Bottom bar: add frosted glass option (Settings > Appearance)

Translucent fill over a backdrop blur, both platforms and both bar styles.
Frosted bars draw over an extended body, so the shell's extendBody and the
tab clearance now key off NooBottomBar.drawsBehindBody.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Ayushya Amitabh
2026-10-05 22:16:27 -04:00
co-authored by Claude Sonnet 5.5
parent d6448984d9
commit 944b87b054
9 changed files with 191 additions and 30 deletions
+10 -1
View File
@@ -233,7 +233,16 @@ blocks are noted where they matter:
that constant's doc comment) instead of edge to edge (no opacity/blur knob that constant's doc comment) instead of edge to edge (no opacity/blur knob
either). `floating` ignores `NooNavStyle` and always uses the Android row either). `floating` ignores `NooNavStyle` and always uses the Android row
(icon-only idle tabs, sliding pill, 64px), so iOS matches Android there; (icon-only idle tabs, sliding pill, 64px), so iOS matches Android there;
only the attached bar still has a distinct iOS row. The host `Scaffold` needs `extendBody: true` while it's active 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
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
footprint for attached+frosted too, not just floating. The host `Scaffold` needs `extendBody: true` while it's active
(`main.dart` already wires this off `SettingsController.bottomBarStyle`), (`main.dart` already wires this off `SettingsController.bottomBarStyle`),
which also means every tab's own scrollable list has to reserve enough which also means every tab's own scrollable list has to reserve enough
bottom padding to clear the bar - nothing does that automatically once the bottom padding to clear the bar - nothing does that automatically once the
+8 -3
View File
@@ -36,7 +36,7 @@ import 'widgets/create_menu.dart';
import 'widgets/noo/core/noo_avatar.dart'; import 'widgets/noo/core/noo_avatar.dart';
import 'widgets/noo/core/noo_button.dart'; import 'widgets/noo/core/noo_button.dart';
import 'widgets/noo/core/noo_fab.dart'; import 'widgets/noo/core/noo_fab.dart';
import 'widgets/noo/nav/noo_nav_style.dart'; import 'widgets/noo/nav/noo_bottom_bar.dart';
import 'widgets/noo/nav/noo_sidebar.dart'; import 'widgets/noo/nav/noo_sidebar.dart';
import 'widgets/noo/nav/noo_toolbar.dart'; import 'widgets/noo/nav/noo_toolbar.dart';
import 'widgets/noo/noo_layout.dart'; import 'widgets/noo/noo_layout.dart';
@@ -526,8 +526,12 @@ class _MainShellViewState extends State<MainShellView> {
settings.navMenuStyle == NooNavMenuStyle.drawer, settings.navMenuStyle == NooNavMenuStyle.drawer,
// Floating needs the body to draw behind the bar's own transparent // Floating needs the body to draw behind the bar's own transparent
// margin (see NooBottomBarStyle's doc comment) instead of stopping // margin (see NooBottomBarStyle's doc comment) instead of stopping
// short of it like attached does. // short of it like attached does; a frosted bar needs it too, or
extendBody: bottomBarStyle == NooBottomBarStyle.floating, // there'd be nothing behind it to blur.
extendBody: NooBottomBar.drawsBehindBody(
bottomBarStyle,
settings.bottomBarFrosted,
),
// Android-only extended Upload FAB - iOS uses the top bar's `plus` // Android-only extended Upload FAB - iOS uses the top bar's `plus`
// instead (see AppTopBar). Stays mounted across every tab (picking // instead (see AppTopBar). Stays mounted across every tab (picking
// aside) and collapses to an icon-only circle off Files/Photos, // aside) and collapses to an icon-only circle off Files/Photos,
@@ -549,6 +553,7 @@ class _MainShellViewState extends State<MainShellView> {
bottomNavigationBar: BottomNavBar( bottomNavigationBar: BottomNavBar(
style: navStyle, style: navStyle,
barStyle: bottomBarStyle, barStyle: bottomBarStyle,
frosted: settings.bottomBarFrosted,
tabs: pinnedTabs, tabs: pinnedTabs,
selectedIndex: pinnedIndex, selectedIndex: pinnedIndex,
onSearchTap: showBottomBarSearch ? () => openSearch(context) : null, onSearchTap: showBottomBarSearch ? () => openSearch(context) : null,
+12
View File
@@ -38,6 +38,7 @@ class SettingsController extends ChangeNotifier {
static const _prefSwipeRightAction = 'ui_swipe_right_action'; static const _prefSwipeRightAction = 'ui_swipe_right_action';
static const _prefFabStyle = 'ui_fab_style'; static const _prefFabStyle = 'ui_fab_style';
static const _prefBottomBarStyle = 'ui_bottom_bar_style'; static const _prefBottomBarStyle = 'ui_bottom_bar_style';
static const _prefBottomBarFrosted = 'ui_bottom_bar_frosted';
static const _prefNavMenuStyle = 'ui_nav_menu_style'; static const _prefNavMenuStyle = 'ui_nav_menu_style';
static const _prefSearchInBottomBar = 'ui_search_in_bottom_bar'; static const _prefSearchInBottomBar = 'ui_search_in_bottom_bar';
static const _prefAmoledDark = 'ui_amoled_dark'; static const _prefAmoledDark = 'ui_amoled_dark';
@@ -51,6 +52,7 @@ class SettingsController extends ChangeNotifier {
ThemeMode _themeMode = ThemeMode.system; ThemeMode _themeMode = ThemeMode.system;
NooBottomBarStyle _bottomBarStyle = NooBottomBarStyle.attached; NooBottomBarStyle _bottomBarStyle = NooBottomBarStyle.attached;
NooNavMenuStyle _navMenuStyle = NooNavMenuStyle.drawer; NooNavMenuStyle _navMenuStyle = NooNavMenuStyle.drawer;
bool _bottomBarFrosted = false;
bool _searchInBottomBar = false; bool _searchInBottomBar = false;
bool _useDynamicColor = true; bool _useDynamicColor = true;
bool _amoledDark = false; bool _amoledDark = false;
@@ -95,6 +97,7 @@ class SettingsController extends ChangeNotifier {
Color get seedColor => _seedColor; Color get seedColor => _seedColor;
ThemeMode get themeMode => _themeMode; ThemeMode get themeMode => _themeMode;
NooBottomBarStyle get bottomBarStyle => _bottomBarStyle; NooBottomBarStyle get bottomBarStyle => _bottomBarStyle;
bool get bottomBarFrosted => _bottomBarFrosted;
NooNavMenuStyle get navMenuStyle => _navMenuStyle; NooNavMenuStyle get navMenuStyle => _navMenuStyle;
bool get searchInBottomBar => _searchInBottomBar; bool get searchInBottomBar => _searchInBottomBar;
bool get useDynamicColor => _useDynamicColor; bool get useDynamicColor => _useDynamicColor;
@@ -155,6 +158,8 @@ class SettingsController extends ChangeNotifier {
orElse: () => _navMenuStyle, orElse: () => _navMenuStyle,
); );
} }
_bottomBarFrosted =
prefs.getBool(_prefBottomBarFrosted) ?? _bottomBarFrosted;
_searchInBottomBar = _searchInBottomBar =
prefs.getBool(_prefSearchInBottomBar) ?? _searchInBottomBar; prefs.getBool(_prefSearchInBottomBar) ?? _searchInBottomBar;
_useDynamicColor = _useDynamicColor =
@@ -313,6 +318,13 @@ class SettingsController extends ChangeNotifier {
_prefsFuture.then((p) => p.setString(_prefBottomBarStyle, style.name)); _prefsFuture.then((p) => p.setString(_prefBottomBarStyle, style.name));
} }
void setBottomBarFrosted(bool value) {
if (_bottomBarFrosted == value) return;
_bottomBarFrosted = value;
notifyListeners();
_prefsFuture.then((p) => p.setBool(_prefBottomBarFrosted, value));
}
void setNavMenuStyle(NooNavMenuStyle style) { void setNavMenuStyle(NooNavMenuStyle style) {
if (_navMenuStyle == style) return; if (_navMenuStyle == style) return;
_navMenuStyle = style; _navMenuStyle = style;
+7 -4
View File
@@ -837,10 +837,13 @@ class _FilesViewState extends State<FilesView> {
), ),
// List/grid/table above only pad NooSpace.lg at the bottom - plenty // List/grid/table above only pad NooSpace.lg at the bottom - plenty
// once Scaffold shrinks the body above an attached bar, but floating // once Scaffold shrinks the body above an attached bar, but floating
// draws the body behind the bar instead, so it needs the bar's own // draws the body behind the bar instead (as does a frosted bar), so it
// footprint added on top or the last row ends up under it. // needs the bar's own footprint added on top or the last row ends up
if (context.watch<SettingsController>().bottomBarStyle == // under it.
NooBottomBarStyle.floating) if (NooBottomBar.drawsBehindBody(
context.watch<SettingsController>().bottomBarStyle,
context.watch<SettingsController>().bottomBarFrosted,
))
SliverToBoxAdapter( SliverToBoxAdapter(
child: SizedBox(height: bottomBarClearance(context)), child: SizedBox(height: bottomBarClearance(context)),
), ),
+3
View File
@@ -16,6 +16,7 @@ import 'noo/nav/noo_bottom_bar.dart';
class BottomNavBar extends StatelessWidget { class BottomNavBar extends StatelessWidget {
final NooNavStyle style; final NooNavStyle style;
final NooBottomBarStyle barStyle; final NooBottomBarStyle barStyle;
final bool frosted;
final List<AppTab> tabs; final List<AppTab> tabs;
final int selectedIndex; final int selectedIndex;
final ValueChanged<int> onDestinationSelected; final ValueChanged<int> onDestinationSelected;
@@ -25,6 +26,7 @@ class BottomNavBar extends StatelessWidget {
super.key, super.key,
required this.style, required this.style,
this.barStyle = NooBottomBarStyle.attached, this.barStyle = NooBottomBarStyle.attached,
this.frosted = false,
required this.tabs, required this.tabs,
required this.selectedIndex, required this.selectedIndex,
required this.onDestinationSelected, required this.onDestinationSelected,
@@ -36,6 +38,7 @@ class BottomNavBar extends StatelessWidget {
return NooBottomBar( return NooBottomBar(
style: style, style: style,
barStyle: barStyle, barStyle: barStyle,
frosted: frosted,
destinations: [ destinations: [
for (final tab in tabs) for (final tab in tabs)
NooNavDestination(icon: tab.icon, label: tab.label), NooNavDestination(icon: tab.icon, label: tab.label),
+66 -18
View File
@@ -1,3 +1,4 @@
import 'dart:ui' show ImageFilter;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../theme/design_tokens.dart'; import '../../../theme/design_tokens.dart';
import 'noo_nav_style.dart'; import 'noo_nav_style.dart';
@@ -13,6 +14,11 @@ const double _kFloatingPillTop = 8;
const double _kAndroidPillWidth = 56; const double _kAndroidPillWidth = 56;
const double _kAndroidPillHeight = 32; 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 /// 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 /// `Mobile Screen.dc.html`). Shows the 5 pinned tabs - which ones, and in
/// what order, is the caller's business; this only draws [destinations]. /// what order, is the caller's business; this only draws [destinations].
@@ -48,6 +54,13 @@ const double _kAndroidPillHeight = 32;
/// body draws behind it - see `tab_state_slivers.dart`'s /// body draws behind it - see `tab_state_slivers.dart`'s
/// `bottomBarClearance`. /// `bottomBarClearance`.
/// ///
/// [frosted] (Settings, Appearance) swaps the solid `surface` fill for a
/// translucent one over a backdrop blur, on both bar styles and both nav
/// styles, and drops [nooDialogShadow] (a shadow would show through the
/// glass). It only reads as glass if content draws behind the bar, so the
/// host `Scaffold` needs `extendBody: true` and each scrollable body needs
/// the same trailing clearance floating does - see [drawsBehindBody].
///
/// [searchDestination]/[onSearchTap] (set together, from Settings' /// [searchDestination]/[onSearchTap] (set together, from Settings'
/// "Search in bottom bar" - see `DESIGN_SYSTEM.md`'s floating bottom bar /// "Search in bottom bar" - see `DESIGN_SYSTEM.md`'s floating bottom bar
/// entry) add a Search entry that's never highlighted (tapping it pushes /// entry) add a Search entry that's never highlighted (tapping it pushes
@@ -57,6 +70,7 @@ const double _kAndroidPillHeight = 32;
class NooBottomBar extends StatelessWidget { class NooBottomBar extends StatelessWidget {
final NooNavStyle style; final NooNavStyle style;
final NooBottomBarStyle barStyle; final NooBottomBarStyle barStyle;
final bool frosted;
final List<NooNavDestination> destinations; final List<NooNavDestination> destinations;
final int selectedIndex; final int selectedIndex;
final ValueChanged<int> onSelected; final ValueChanged<int> onSelected;
@@ -67,6 +81,7 @@ class NooBottomBar extends StatelessWidget {
super.key, super.key,
required this.style, required this.style,
this.barStyle = NooBottomBarStyle.attached, this.barStyle = NooBottomBarStyle.attached,
this.frosted = false,
required this.destinations, required this.destinations,
required this.selectedIndex, required this.selectedIndex,
required this.onSelected, required this.onSelected,
@@ -84,6 +99,12 @@ class NooBottomBar extends StatelessWidget {
return barStyle == NooBottomBarStyle.floating ? 64 : 80; return barStyle == NooBottomBarStyle.floating ? 64 : 80;
} }
/// 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).
static bool drawsBehindBody(NooBottomBarStyle barStyle, bool frosted) =>
frosted || barStyle == NooBottomBarStyle.floating;
/// Gap between the floating bar's bottom edge and the safe area below it /// Gap between the floating bar's bottom edge and the safe area below it
/// (itself inside the [SafeArea] that consumes the actual device inset). /// (itself inside the [SafeArea] that consumes the actual device inset).
static const double floatingBottomMargin = 12; static const double floatingBottomMargin = 12;
@@ -111,17 +132,19 @@ class NooBottomBar extends StatelessWidget {
final barHeight = NooBottomBar.rowHeight(style, barStyle); final barHeight = NooBottomBar.rowHeight(style, barStyle);
if (floating) { if (floating) {
final pill = Container( final pillRadius = BorderRadius.circular(28);
final pillBox = Container(
height: barHeight, height: barHeight,
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
decoration: BoxDecoration( decoration: BoxDecoration(
color: colors.surface, color: _fill(colors),
border: Border.all(color: colors.line), border: Border.all(color: colors.line),
borderRadius: BorderRadius.circular(28), borderRadius: pillRadius,
boxShadow: const [nooDialogShadow], boxShadow: frosted ? null : const [nooDialogShadow],
), ),
child: row, child: row,
); );
final pill = frosted ? _blurred(pillBox, pillRadius) : pillBox;
return SafeArea( return SafeArea(
top: false, top: false,
child: Padding( child: Padding(
@@ -152,6 +175,7 @@ class NooBottomBar extends StatelessWidget {
destination: searchDestination!, destination: searchDestination!,
onTap: onSearchTap!, onTap: onSearchTap!,
size: barHeight, size: barHeight,
frosted: frosted,
), ),
], ],
), ),
@@ -161,9 +185,9 @@ class NooBottomBar extends StatelessWidget {
); );
} }
return Container( final bar = Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: colors.surface, color: _fill(colors),
border: ios ? Border(top: BorderSide(color: colors.line)) : null, border: ios ? Border(top: BorderSide(color: colors.line)) : null,
), ),
child: SafeArea( child: SafeArea(
@@ -171,8 +195,13 @@ class NooBottomBar extends StatelessWidget {
child: SizedBox(height: barHeight, child: row), child: SizedBox(height: barHeight, child: row),
), ),
); );
return frosted ? _blurred(bar, BorderRadius.zero) : bar;
} }
Color _fill(NooColors colors) => frosted
? colors.surface.withValues(alpha: _kFrostedAlpha)
: colors.surface;
Widget _buildIosRow({NooNavDestination? trailingSearch}) { Widget _buildIosRow({NooNavDestination? trailingSearch}) {
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -255,6 +284,16 @@ 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(
borderRadius: radius,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: _kFrostedSigma, sigmaY: _kFrostedSigma),
child: child,
),
);
/// The floating bar's own Search entry point, next to the pill rather than /// The floating bar's own Search entry point, next to the pill rather than
/// inside it - always fully round (see [NooBottomBar]'s doc comment), /// inside it - always fully round (see [NooBottomBar]'s doc comment),
/// same fill/border as the pill so the two still read as one family. /// same fill/border as the pill so the two still read as one family.
@@ -262,11 +301,13 @@ class _SearchSatellite extends StatelessWidget {
final NooNavDestination destination; final NooNavDestination destination;
final VoidCallback onTap; final VoidCallback onTap;
final double size; final double size;
final bool frosted;
const _SearchSatellite({ const _SearchSatellite({
required this.destination, required this.destination,
required this.onTap, required this.onTap,
required this.size, required this.size,
required this.frosted,
}); });
@override @override
@@ -278,21 +319,28 @@ class _SearchSatellite extends StatelessWidget {
child: GestureDetector( child: GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: onTap, onTap: onTap,
child: Container( child: _satelliteBody(colors),
width: size,
height: size,
alignment: Alignment.center,
decoration: BoxDecoration(
color: colors.surface,
border: Border.all(color: colors.line),
shape: BoxShape.circle,
boxShadow: const [nooDialogShadow],
),
child: Icon(destination.icon, size: 24, color: colors.fg1),
),
), ),
); );
} }
Widget _satelliteBody(NooColors colors) {
final disc = Container(
width: size,
height: size,
alignment: Alignment.center,
decoration: BoxDecoration(
color: frosted
? colors.surface.withValues(alpha: _kFrostedAlpha)
: colors.surface,
border: Border.all(color: colors.line),
shape: BoxShape.circle,
boxShadow: frosted ? null : const [nooDialogShadow],
),
child: Icon(destination.icon, size: 24, color: colors.fg1),
);
return frosted ? _blurred(disc, BorderRadius.circular(size / 2)) : disc;
}
} }
class _IosItem extends StatelessWidget { class _IosItem extends StatelessWidget {
@@ -53,6 +53,17 @@ class SettingsAppearanceSection extends StatelessWidget {
children: [ children: [
_ThemeRow(settings: settings), _ThemeRow(settings: settings),
_BottomBarStyleRow(settings: settings), _BottomBarStyleRow(settings: settings),
NooSettingsRow(
icon: LucideIcons.glassWater,
label: const Text('Frosted glass bottom bar'),
subtitle: const Text(
'A translucent, blurred bottom bar - content scrolls behind it',
),
trailing: NooToggle(
checked: settings.bottomBarFrosted,
onChanged: settings.setBottomBarFrosted,
),
),
_NavMenuStyleRow(settings: settings), _NavMenuStyleRow(settings: settings),
NooSettingsRow( NooSettingsRow(
icon: LucideIcons.search, icon: LucideIcons.search,
+11 -4
View File
@@ -97,19 +97,26 @@ Widget tabEmptySliver(
/// attached] bars aren't drawn behind (no `Scaffold.extendBody`), so /// attached] bars aren't drawn behind (no `Scaffold.extendBody`), so
/// Scaffold already shrinks the body above them - this is then pure /// Scaffold already shrinks the body above them - this is then pure
/// breathing room, not overlap prevention. [NooBottomBarStyle.floating] /// breathing room, not overlap prevention. [NooBottomBarStyle.floating]
/// bars draw over an extended body instead, so nothing reserves space for /// and frosted bars (see [NooBottomBar.drawsBehindBody]) draw over an
/// extended body instead, so nothing reserves space for
/// them automatically: the clearance has to cover the bar's own footprint /// them automatically: the clearance has to cover the bar's own footprint
/// (see [NooBottomBar.rowHeight]/[NooBottomBar.floatingBottomMargin]) plus /// (see [NooBottomBar.rowHeight]/[NooBottomBar.floatingBottomMargin]) plus
/// the safe-area inset below it, or the last row ends up hidden under it. /// the safe-area inset below it, or the last row ends up hidden under it.
double bottomBarClearance(BuildContext context) { double bottomBarClearance(BuildContext context) {
final settings = context.watch<SettingsController>(); final settings = context.watch<SettingsController>();
if (settings.bottomBarStyle == NooBottomBarStyle.attached) return 100; final barStyle = settings.bottomBarStyle;
if (!NooBottomBar.drawsBehindBody(barStyle, settings.bottomBarFrosted)) {
return 100;
}
final barHeight = NooBottomBar.rowHeight( final barHeight = NooBottomBar.rowHeight(
NooLayout.navStyle(context), NooLayout.navStyle(context),
NooBottomBarStyle.floating, barStyle,
); );
final margin = barStyle == NooBottomBarStyle.floating
? NooBottomBar.floatingBottomMargin
: 0;
final safeBottom = MediaQuery.paddingOf(context).bottom; final safeBottom = MediaQuery.paddingOf(context).bottom;
return barHeight + NooBottomBar.floatingBottomMargin + safeBottom + 24; return barHeight + margin + safeBottom + 24;
} }
/// Bottom padding sliver so the last row/card clears the bottom nav/FAB - /// Bottom padding sliver so the last row/card clears the bottom nav/FAB -
+63
View File
@@ -25,6 +25,69 @@ void main() {
setUpNooTests(); setUpNooTests();
group('NooBottomBar', () { group('NooBottomBar', () {
for (final style in NooNavStyle.values) {
for (final barStyle in NooBottomBarStyle.values) {
testNooWidgets(
'$style/$barStyle: frosted draws a backdrop blur, solid does not',
(tester, theme, c) async {
for (final frosted in [false, true]) {
await pumpNoo(
tester,
SizedBox(
width: 360,
height: 120,
child: NooBottomBar(
style: style,
barStyle: barStyle,
frosted: frosted,
destinations: _destinations,
selectedIndex: 0,
onSelected: (_) {},
searchDestination: const NooNavDestination(
icon: LucideIcons.search,
label: 'Search',
),
onSearchTap: () {},
),
),
theme: theme,
);
expect(
find.byType(BackdropFilter),
frosted ? findsWidgets : findsNothing,
);
}
},
);
}
}
test('drawsBehindBody: floating or frosted', () {
expect(
NooBottomBar.drawsBehindBody(NooBottomBarStyle.attached, false),
isFalse,
);
expect(
NooBottomBar.drawsBehindBody(NooBottomBarStyle.attached, true),
isTrue,
);
expect(
NooBottomBar.drawsBehindBody(NooBottomBarStyle.floating, false),
isTrue,
);
});
test('floating row height is the Android one on both nav styles', () {
expect(
NooBottomBar.rowHeight(NooNavStyle.ios, NooBottomBarStyle.floating),
64,
);
expect(
NooBottomBar.rowHeight(NooNavStyle.ios, NooBottomBarStyle.attached),
50,
);
});
testNooWidgets('android: label sits below the icon, not beside it', ( testNooWidgets('android: label sits below the icon, not beside it', (
tester, tester,
theme, theme,