mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
Wrap channel and DM titles in native glass and remove the temporary-channel clock. Apply a soft status-area fade across iPhone navigation, and fade the large title with scroll progress and fade in the centered title, with Reduce Motion support. Validation: mobile formatting/analysis and the full mobile suite passed. Native iOS 26/27 simulator checks covered actual text opacity, header resizing, quick reversal, and restored heading position; signed iPhone build, installation, and launch succeeded. `just ci` was blocked by disk exhaustion during Rust compilation. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com>
649 lines
22 KiB
Dart
649 lines
22 KiB
Dart
import 'dart:ui';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
|
|
|
import '../theme/theme.dart';
|
|
import 'buzz_navigation_metrics.dart';
|
|
import 'directional_transition_scope.dart';
|
|
import 'frosted_scroll_under_scope.dart';
|
|
import 'ios_glass_navigation_button.dart';
|
|
import 'ios_glass_navigation_action.dart';
|
|
import 'ios_navigation_bar.dart';
|
|
export 'ios_navigation_bar.dart' show IosNavigationAction;
|
|
|
|
/// Minimum height of the frosted app bar content area below the safe area.
|
|
const _kBarContentMinHeight = buzzNavigationRowHeight;
|
|
const _kBottomBorderWidth = 1.0;
|
|
|
|
TextStyle _effectiveTitleStyle(BuildContext context, TextStyle? titleStyle) {
|
|
final baseStyle =
|
|
context.textTheme.titleSmall ??
|
|
const TextStyle(fontSize: 16, height: 1.4);
|
|
return baseStyle.copyWith(fontWeight: FontWeight.w600).merge(titleStyle);
|
|
}
|
|
|
|
double _barContentHeight(
|
|
BuildContext context,
|
|
TextStyle? titleStyle,
|
|
double titleContentHeight,
|
|
) {
|
|
final style = _effectiveTitleStyle(context, titleStyle);
|
|
final scaledFontSize = MediaQuery.textScalerOf(
|
|
context,
|
|
).scale(style.fontSize ?? 16);
|
|
final scaledTitleHeight = scaledFontSize * (style.height ?? 1);
|
|
final effectiveTitleHeight = titleContentHeight > scaledTitleHeight
|
|
? titleContentHeight
|
|
: scaledTitleHeight;
|
|
final accessibleHeight = Grid.xxs + effectiveTitleHeight + Grid.xxs;
|
|
return accessibleHeight > _kBarContentMinHeight
|
|
? accessibleHeight
|
|
: _kBarContentMinHeight;
|
|
}
|
|
|
|
/// Height for a compact title rail below the app bar's action row.
|
|
///
|
|
/// The rail normally stays at 40dp, but grows with an accessible title rather
|
|
/// than clipping text at larger system text sizes.
|
|
double frostedAppBarLowerTitleHeight(
|
|
BuildContext context, {
|
|
TextStyle? titleStyle,
|
|
}) {
|
|
final style = _effectiveTitleStyle(context, titleStyle);
|
|
final scaledFontSize = MediaQuery.textScalerOf(
|
|
context,
|
|
).scale(style.fontSize ?? 16);
|
|
final titleHeight = scaledFontSize * (style.height ?? 1);
|
|
return titleHeight > 40 ? titleHeight : 40;
|
|
}
|
|
|
|
/// Returns the total height of the [FrostedAppBar] including safe area padding.
|
|
///
|
|
/// Use this to add top spacing to body content so it starts below the bar.
|
|
/// Pass the same [titleStyle] and [titleContentHeight] to the bar and this
|
|
/// helper when customizing them.
|
|
double frostedAppBarHeight(
|
|
BuildContext context, {
|
|
double bottomHeight = 0,
|
|
TextStyle? titleStyle,
|
|
double titleContentHeight = 0,
|
|
bool nativeLargeTitle = false,
|
|
}) {
|
|
if (defaultTargetPlatform == TargetPlatform.iOS) {
|
|
return MediaQuery.paddingOf(context).top +
|
|
IosNavigationMetrics.of(context).compactHeight +
|
|
(nativeLargeTitle
|
|
? IosNavigationMetrics.of(context).largeTitleHeight
|
|
: 0) +
|
|
bottomHeight;
|
|
}
|
|
return MediaQuery.paddingOf(context).top +
|
|
_barContentHeight(context, titleStyle, titleContentHeight) +
|
|
bottomHeight +
|
|
_kBottomBorderWidth;
|
|
}
|
|
|
|
/// A frosted-glass floating app bar designed to sit inside a [Stack].
|
|
///
|
|
/// Renders as a [Positioned] widget pinned to the top of its parent Stack.
|
|
/// Content scrolls underneath with a translucent backdrop blur effect.
|
|
class FrostedAppBar extends StatelessWidget {
|
|
/// Widget displayed on the leading (left) side. If null and the navigator
|
|
/// can pop, a back button is shown automatically.
|
|
final Widget? leading;
|
|
|
|
/// Whether to infer a back button from the current navigator.
|
|
final bool automaticallyImplyLeading;
|
|
|
|
/// Widget displayed in the center/title area.
|
|
final Widget? title;
|
|
|
|
/// Whether [title] is centered in the full bar rather than flowing after the
|
|
/// leading control. Page titles should keep the default; identity-style
|
|
/// headers can opt out.
|
|
final bool centerTitle;
|
|
|
|
/// Optional style merged over the default title style.
|
|
final TextStyle? titleStyle;
|
|
|
|
/// Scaled height needed by a custom title with multiple text lines.
|
|
///
|
|
/// Pass the same value to [frostedAppBarHeight] when spacing body content.
|
|
final double titleContentHeight;
|
|
|
|
/// Optional content displayed below the title row in the same surface.
|
|
final Widget? bottom;
|
|
|
|
/// Height reserved for [bottom].
|
|
final double bottomHeight;
|
|
|
|
/// Extends [bottom] upward into the title row without moving the app bar's
|
|
/// outer bounds. This keeps overlapping controls inside the app bar's hit
|
|
/// test region as well as its paint region.
|
|
final double bottomOverlap;
|
|
|
|
/// Widgets displayed on the trailing (right) side.
|
|
final List<Widget> actions;
|
|
|
|
/// Horizontal inset for the app bar's leading, title, and actions.
|
|
final double horizontalInset;
|
|
|
|
/// Color applied to icons in the app bar.
|
|
final Color? iconColor;
|
|
|
|
/// Paints over the frosted fill instead of the default translucent surface.
|
|
/// Used by the Buzz themes to carry their branded gradient across the app's
|
|
/// top section — see [buzzTopSectionGradient].
|
|
final Gradient? gradient;
|
|
|
|
/// Whether to apply the translucent blur treatment behind the app bar.
|
|
///
|
|
/// The backdrop stays clear at rest. This enables the treatment when the
|
|
/// surrounding scroll scope reports content beneath the controls.
|
|
final bool frosted;
|
|
|
|
/// Keeps the backdrop stable regardless of scroll state on both platforms.
|
|
/// Conversation headers use this while their timeline and composer resize.
|
|
final bool alwaysFrosted;
|
|
|
|
/// Opacity of the frosted surface above the blurred backdrop.
|
|
final double frostedSurfaceOpacity;
|
|
|
|
/// Blur strength of the frosted backdrop.
|
|
final double frostedBlurSigma;
|
|
|
|
/// Whether to draw a divider below the app bar.
|
|
final bool showBottomDivider;
|
|
|
|
/// Opacity of the divider below the app bar.
|
|
final double bottomDividerOpacity;
|
|
|
|
/// Explicit UIKit title for headers whose Flutter title is a custom widget.
|
|
final String? nativeTitle;
|
|
|
|
/// Optional second line and action for a native channel title.
|
|
final String? nativeSubtitle;
|
|
|
|
/// Existing retention disclosure displayed by the native conversation title.
|
|
final String? nativeEphemeralLabel;
|
|
|
|
/// Reactive counterpart avatar and presence badge for a native DM title.
|
|
final IosNavigationAction? nativeTitleAvatar;
|
|
final Color? nativeTitlePresenceColor;
|
|
final VoidCallback? onNativeTitlePressed;
|
|
|
|
/// Whether UIKit should expand the title at the top of the page.
|
|
final bool nativeLargeTitle;
|
|
|
|
/// Duration for coordinated native header and overlapping control movement.
|
|
final Duration nativeLayoutDuration;
|
|
|
|
/// Native replacement for a custom leading widget.
|
|
final IosNavigationAction? nativeLeading;
|
|
|
|
/// Native replacements for composite Flutter actions, including menus.
|
|
final List<IosNavigationAction>? nativeActions;
|
|
|
|
/// Reports when native header images and layout are ready to reveal.
|
|
final ValueChanged<bool>? onNativeReadyChanged;
|
|
|
|
/// Uses a composable Flutter header while a backdrop covers the native view.
|
|
final ValueListenable<bool>? nativeViewSuppressed;
|
|
|
|
const FrostedAppBar({
|
|
super.key,
|
|
this.nativeTitle,
|
|
this.nativeSubtitle,
|
|
this.nativeEphemeralLabel,
|
|
this.nativeTitleAvatar,
|
|
this.nativeTitlePresenceColor,
|
|
this.onNativeTitlePressed,
|
|
this.nativeLargeTitle = false,
|
|
this.nativeLayoutDuration = Duration.zero,
|
|
this.nativeLeading,
|
|
this.nativeActions,
|
|
this.onNativeReadyChanged,
|
|
this.nativeViewSuppressed,
|
|
this.leading,
|
|
this.automaticallyImplyLeading = true,
|
|
this.title,
|
|
this.centerTitle = false,
|
|
this.titleStyle,
|
|
this.titleContentHeight = 0,
|
|
this.bottom,
|
|
this.bottomHeight = 0,
|
|
this.bottomOverlap = 0,
|
|
this.actions = const [],
|
|
this.horizontalInset = Grid.xxs,
|
|
this.iconColor,
|
|
this.gradient,
|
|
this.frosted = true,
|
|
this.alwaysFrosted = false,
|
|
this.frostedSurfaceOpacity = 0.5,
|
|
this.frostedBlurSigma = 20,
|
|
this.showBottomDivider = true,
|
|
this.bottomDividerOpacity = 0.05,
|
|
}) : assert(bottom == null || bottomHeight > 0),
|
|
assert(bottomOverlap >= 0),
|
|
assert(bottom != null || bottomOverlap == 0),
|
|
assert(frostedBlurSigma >= 0),
|
|
assert(bottomDividerOpacity >= 0 && bottomDividerOpacity <= 1);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final suppression = nativeViewSuppressed;
|
|
if (defaultTargetPlatform == TargetPlatform.iOS && suppression != null) {
|
|
return ValueListenableBuilder<bool>(
|
|
valueListenable: suppression,
|
|
builder: (context, suppressed, _) => _build(context, suppressed),
|
|
);
|
|
}
|
|
return _build(context, false);
|
|
}
|
|
|
|
Widget _build(BuildContext context, bool nativeSuppressed) {
|
|
if (defaultTargetPlatform == TargetPlatform.iOS && !nativeSuppressed) {
|
|
final offset = IosNavigationScrollScope.maybeOf(context);
|
|
Widget buildNative(double scrollOffset) {
|
|
// Scrolling tracks the finger directly; only the search focus change
|
|
// uses the coordinated layout transition.
|
|
final layoutDuration = nativeLargeTitle && scrollOffset > 0
|
|
? Duration.zero
|
|
: nativeLayoutDuration;
|
|
final extra = nativeLargeTitle
|
|
? (IosNavigationMetrics.of(context).largeTitleHeight - scrollOffset)
|
|
.clamp(0.0, IosNavigationMetrics.of(context).largeTitleHeight)
|
|
: 0.0;
|
|
final barHeight =
|
|
MediaQuery.paddingOf(context).top +
|
|
IosNavigationMetrics.of(context).compactHeight +
|
|
extra;
|
|
return AnimatedPositioned(
|
|
duration: layoutDuration,
|
|
curve: Curves.easeInOutCubic,
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
height: barHeight + bottomHeight,
|
|
child: Stack(
|
|
children: [
|
|
Positioned(
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
height: barHeight,
|
|
child: IosNavigationBar(
|
|
onReadyChanged: onNativeReadyChanged,
|
|
title:
|
|
nativeTitle ??
|
|
(title is Text ? (title as Text).data ?? '' : ''),
|
|
subtitle: nativeSubtitle,
|
|
ephemeralLabel: nativeEphemeralLabel,
|
|
titleAvatar: nativeTitleAvatar,
|
|
titlePresenceColor: nativeTitlePresenceColor,
|
|
onTitlePressed: onNativeTitlePressed,
|
|
largeTitle: nativeLargeTitle,
|
|
alwaysFrosted: alwaysFrosted,
|
|
foregroundColor: iconColor,
|
|
leading: nativeLeading ?? _nativeAction(leading),
|
|
onBack:
|
|
automaticallyImplyLeading &&
|
|
leading == null &&
|
|
Navigator.canPop(context)
|
|
? () => Navigator.of(context).maybePop()
|
|
: null,
|
|
actions:
|
|
nativeActions ??
|
|
actions
|
|
.map(_nativeAction)
|
|
.whereType<IosNavigationAction>()
|
|
.toList(),
|
|
),
|
|
),
|
|
if (bottom != null)
|
|
AnimatedPositioned(
|
|
duration: layoutDuration,
|
|
curve: Curves.easeInOutCubic,
|
|
top: barHeight - bottomOverlap,
|
|
left: 0,
|
|
right: 0,
|
|
height: bottomHeight + bottomOverlap,
|
|
child: bottomOverlap > 0 || bottom is SizedBox
|
|
? bottom!
|
|
: ColoredBox(
|
|
color: context.colors.surface,
|
|
child: bottom!,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return offset == null
|
|
? buildNative(0)
|
|
: ValueListenableBuilder<double>(
|
|
valueListenable: offset,
|
|
builder: (context, value, child) => buildNative(value),
|
|
);
|
|
}
|
|
final topPadding = MediaQuery.paddingOf(context).top;
|
|
final scrollUnder = FrostedScrollUnderScope.maybeOf(context);
|
|
final isScrolledUnder =
|
|
alwaysFrosted || (scrollUnder?.isScrolledUnder ?? false);
|
|
final paintsFrost = frosted && isScrolledUnder;
|
|
final paintsBottomDivider = showBottomDivider && isScrolledUnder;
|
|
final canPop = Navigator.canPop(context);
|
|
final effectiveTitleStyle = _effectiveTitleStyle(context, titleStyle);
|
|
final barContentHeight = _barContentHeight(
|
|
context,
|
|
titleStyle,
|
|
titleContentHeight,
|
|
);
|
|
final usesAutomaticIosGlassBackButton =
|
|
!nativeSuppressed &&
|
|
leading == null &&
|
|
automaticallyImplyLeading &&
|
|
canPop &&
|
|
Theme.of(context).platform == TargetPlatform.iOS;
|
|
final effectiveIconColor = iconColor ?? context.colors.primary;
|
|
|
|
final effectiveLeading =
|
|
leading ??
|
|
(automaticallyImplyLeading && canPop
|
|
? usesAutomaticIosGlassBackButton
|
|
? IosGlassNavigationButton(
|
|
icon: IosGlassNavigationIcon.back,
|
|
semanticLabel: 'Back',
|
|
onPressed: () => Navigator.of(context).maybePop(),
|
|
width: iosGlassChannelHeaderLeadingWidth,
|
|
buttonCenterX: iosGlassChannelHeaderButtonCenterX,
|
|
foregroundColor: effectiveIconColor,
|
|
)
|
|
: SizedBox(
|
|
width: 48,
|
|
height: 48,
|
|
child: IconButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
color: effectiveIconColor,
|
|
icon: const Icon(LucideIcons.chevronLeft),
|
|
tooltip: 'Back',
|
|
),
|
|
)
|
|
: null);
|
|
|
|
final titleRow = SizedBox(
|
|
height: barContentHeight,
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: horizontalInset),
|
|
child: IconTheme.merge(
|
|
data: IconThemeData(color: effectiveIconColor),
|
|
child: _CenteredNavigationLayout(
|
|
leading: effectiveLeading,
|
|
title: title == null
|
|
? null
|
|
: DefaultTextStyle.merge(
|
|
style: effectiveTitleStyle,
|
|
overflow: TextOverflow.ellipsis,
|
|
maxLines: 1,
|
|
textAlign: centerTitle ? TextAlign.center : TextAlign.start,
|
|
child: centerTitle
|
|
? title!
|
|
: Padding(
|
|
padding: EdgeInsets.only(
|
|
left: effectiveLeading != null
|
|
? usesAutomaticIosGlassBackButton
|
|
? iosGlassChannelHeaderTitleSpacing
|
|
: 0
|
|
: horizontalInset < Grid.gutter
|
|
? Grid.gutter - horizontalInset
|
|
: 0,
|
|
right:
|
|
actions.isEmpty &&
|
|
horizontalInset < Grid.gutter
|
|
? Grid.gutter - horizontalInset
|
|
: 0,
|
|
),
|
|
child: title!,
|
|
),
|
|
),
|
|
actions: actions.isEmpty
|
|
? null
|
|
: Row(mainAxisSize: MainAxisSize.min, children: actions),
|
|
centered: centerTitle,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final contentBody = bottom != null && bottomOverlap > 0
|
|
? SizedBox(
|
|
height: barContentHeight + bottomHeight,
|
|
child: Stack(
|
|
children: [
|
|
Positioned(top: 0, left: 0, right: 0, child: titleRow),
|
|
Positioned(
|
|
top: barContentHeight - bottomOverlap,
|
|
left: 0,
|
|
right: 0,
|
|
height: bottomHeight + bottomOverlap,
|
|
child: bottom!,
|
|
),
|
|
],
|
|
),
|
|
)
|
|
: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
titleRow,
|
|
if (bottom != null) SizedBox(height: bottomHeight, child: bottom),
|
|
],
|
|
);
|
|
|
|
final content = DirectionalTransitionMotion(
|
|
transformKey: const ValueKey(
|
|
'frosted-app-bar-content-transition-transform',
|
|
),
|
|
opacityKey: const ValueKey('frosted-app-bar-content-transition-opacity'),
|
|
child: contentBody,
|
|
);
|
|
|
|
final background = Container(
|
|
key: const ValueKey('frosted-app-bar-background'),
|
|
padding: EdgeInsets.only(top: topPadding),
|
|
decoration: BoxDecoration(
|
|
color: gradient != null
|
|
? null
|
|
: paintsFrost
|
|
? context.colors.surface.withValues(alpha: frostedSurfaceOpacity)
|
|
: Colors.transparent,
|
|
gradient: gradient,
|
|
border: showBottomDivider
|
|
? Border(
|
|
bottom: BorderSide(
|
|
color: paintsBottomDivider
|
|
? navigationDivider(context, bottomDividerOpacity)
|
|
: Colors.transparent,
|
|
width: _kBottomBorderWidth,
|
|
),
|
|
)
|
|
: null,
|
|
),
|
|
child: content,
|
|
);
|
|
|
|
final child = ClipRect(
|
|
child: paintsFrost
|
|
? BackdropFilter(
|
|
filter: ImageFilter.blur(
|
|
sigmaX: frostedBlurSigma,
|
|
sigmaY: frostedBlurSigma,
|
|
),
|
|
child: background,
|
|
)
|
|
: background,
|
|
);
|
|
|
|
return Positioned(top: 0, left: 0, right: 0, child: child);
|
|
}
|
|
}
|
|
|
|
enum _NavigationSlot { leading, title, actions }
|
|
|
|
class _CenteredNavigationLayout extends StatelessWidget {
|
|
const _CenteredNavigationLayout({
|
|
this.leading,
|
|
this.title,
|
|
this.actions,
|
|
required this.centered,
|
|
});
|
|
|
|
final Widget? leading;
|
|
final Widget? title;
|
|
final Widget? actions;
|
|
final bool centered;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (!centered) {
|
|
return Row(
|
|
children: [
|
|
?leading,
|
|
if (title != null) Expanded(child: title!) else const Spacer(),
|
|
?actions,
|
|
],
|
|
);
|
|
}
|
|
return CustomMultiChildLayout(
|
|
delegate: _CenteredNavigationLayoutDelegate(),
|
|
children: [
|
|
if (leading != null)
|
|
LayoutId(id: _NavigationSlot.leading, child: leading!),
|
|
if (title != null) LayoutId(id: _NavigationSlot.title, child: title!),
|
|
if (actions != null)
|
|
LayoutId(id: _NavigationSlot.actions, child: actions!),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _CenteredNavigationLayoutDelegate extends MultiChildLayoutDelegate {
|
|
@override
|
|
void performLayout(Size size) {
|
|
Size leadingSize = Size.zero;
|
|
if (hasChild(_NavigationSlot.leading)) {
|
|
leadingSize = layoutChild(
|
|
_NavigationSlot.leading,
|
|
BoxConstraints.loose(size),
|
|
);
|
|
positionChild(
|
|
_NavigationSlot.leading,
|
|
Offset(0, (size.height - leadingSize.height) / 2),
|
|
);
|
|
}
|
|
|
|
Size actionsSize = Size.zero;
|
|
if (hasChild(_NavigationSlot.actions)) {
|
|
actionsSize = layoutChild(
|
|
_NavigationSlot.actions,
|
|
BoxConstraints.loose(size),
|
|
);
|
|
positionChild(
|
|
_NavigationSlot.actions,
|
|
Offset(
|
|
size.width - actionsSize.width,
|
|
(size.height - actionsSize.height) / 2,
|
|
),
|
|
);
|
|
}
|
|
|
|
if (hasChild(_NavigationSlot.title)) {
|
|
final occupiedSideWidth = leadingSize.width > actionsSize.width
|
|
? leadingSize.width
|
|
: actionsSize.width;
|
|
final sideWidth = occupiedSideWidth == 0
|
|
? 0.0
|
|
: occupiedSideWidth + Grid.xs;
|
|
final titleWidth = (size.width - sideWidth * 2).clamp(0.0, size.width);
|
|
final titleSize = layoutChild(
|
|
_NavigationSlot.title,
|
|
BoxConstraints(maxWidth: titleWidth, maxHeight: size.height),
|
|
);
|
|
positionChild(
|
|
_NavigationSlot.title,
|
|
Offset(
|
|
(size.width - titleSize.width) / 2,
|
|
(size.height - titleSize.height) / 2,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool shouldRelayout(
|
|
covariant _CenteredNavigationLayoutDelegate oldDelegate,
|
|
) => false;
|
|
}
|
|
|
|
IosNavigationAction? _nativeAction(Widget? widget) {
|
|
if (widget is Padding) return _nativeAction(widget.child);
|
|
if (widget is SizedBox) return _nativeAction(widget.child);
|
|
if (widget is IosGlassNavigationAction) {
|
|
return IosNavigationAction(
|
|
label: widget.label,
|
|
onPressed: widget.isBusy ? null : widget.onPressed,
|
|
);
|
|
}
|
|
if (widget is IosGlassNavigationButton) {
|
|
final symbol = switch (widget.icon) {
|
|
IosGlassNavigationIcon.more => 'ellipsis',
|
|
IosGlassNavigationIcon.reply => 'arrowshape.turn.up.left',
|
|
IosGlassNavigationIcon.back => 'chevron.backward',
|
|
IosGlassNavigationIcon.close => 'xmark',
|
|
IosGlassNavigationIcon.camera => 'camera',
|
|
IosGlassNavigationIcon.photoLibrary => 'photo.on.rectangle',
|
|
IosGlassNavigationIcon.palette => 'paintpalette',
|
|
IosGlassNavigationIcon.droplet => 'drop',
|
|
IosGlassNavigationIcon.emoji => 'face.smiling',
|
|
IosGlassNavigationIcon.person => 'person',
|
|
IosGlassNavigationIcon.frame => 'crop',
|
|
IosGlassNavigationIcon.rotateCamera =>
|
|
'arrow.triangle.2.circlepath.camera',
|
|
IosGlassNavigationIcon.shutter => 'circle',
|
|
IosGlassNavigationIcon.colorSwatch => 'circle.fill',
|
|
IosGlassNavigationIcon.sun => 'sun.max',
|
|
IosGlassNavigationIcon.moon => 'moon',
|
|
IosGlassNavigationIcon.systemAppearance => 'circle.lefthalf.filled',
|
|
};
|
|
return IosNavigationAction(
|
|
label: widget.label ?? widget.semanticLabel,
|
|
symbol: widget.label == null ? symbol : null,
|
|
selected: widget.isSelected,
|
|
onPressed: widget.isBusy ? null : widget.onPressed,
|
|
);
|
|
}
|
|
if (widget is IconButton) {
|
|
final icon = widget.icon is Icon ? (widget.icon as Icon).icon : null;
|
|
final symbol = icon == LucideIcons.x
|
|
? 'xmark'
|
|
: icon == LucideIcons.chevronLeft || icon == LucideIcons.arrowLeft
|
|
? 'chevron.backward'
|
|
: icon == LucideIcons.users
|
|
? 'person.2'
|
|
: 'ellipsis';
|
|
return IosNavigationAction(
|
|
label: widget.tooltip ?? 'Action',
|
|
symbol: symbol,
|
|
onPressed: widget.onPressed,
|
|
);
|
|
}
|
|
if (widget is ButtonStyleButton && widget.child is Text) {
|
|
return IosNavigationAction(
|
|
label: (widget.child as Text).data ?? '',
|
|
onPressed: widget.onPressed,
|
|
);
|
|
}
|
|
return null;
|
|
}
|