diff --git a/example/lib/screens/navigation/navigation_view.dart b/example/lib/screens/navigation/navigation_view.dart index 66f476ba4..447b24a10 100644 --- a/example/lib/screens/navigation/navigation_view.dart +++ b/example/lib/screens/navigation/navigation_view.dart @@ -15,7 +15,7 @@ class NavigationViewPage extends StatefulWidget { class _NavigationViewPageState extends State with PageMixin { - static const double itemHeight = 300.0; + static const double itemHeight = 500.0; int topIndex = 0; @@ -66,6 +66,7 @@ class _NavigationViewPageState extends State enabled: false, onTap: () => debugPrint('Tapped disabled'), ), + PaneItemHeader(header: const Text('Apps')), PaneItemExpander( icon: const Icon(FluentIcons.account_management), title: const Text('Account'), @@ -84,7 +85,6 @@ class _NavigationViewPageState extends State ), onTap: () => debugPrint('Tapped account'), items: [ - PaneItemHeader(header: const Text('Apps')), PaneItem( icon: const Icon(FluentIcons.mail), title: const Text('Mail'), @@ -376,13 +376,13 @@ NavigationView( selected: topIndex, onItemPressed: (index) { // Do anything you want to do, such as: - if (index == topIndex) { - if (displayMode == PaneDisplayMode.open) { - setState(() => this.displayMode = PaneDisplayMode.compact); - } else if (displayMode == PaneDisplayMode.compact) { - setState(() => this.displayMode = PaneDisplayMode.open); - } - } + // if (index == topIndex) { + // if (displayMode == PaneDisplayMode.open) { + // setState(() => this.displayMode = PaneDisplayMode.compact); + // } else if (displayMode == PaneDisplayMode.compact) { + // setState(() => this.displayMode = PaneDisplayMode.open); + // } + // } }, onChanged: (index) => setState(() => topIndex = index), displayMode: displayMode, diff --git a/example/lib/screens/popups/teaching_tip.dart b/example/lib/screens/popups/teaching_tip.dart index 5c34f7996..9a65ffe24 100644 --- a/example/lib/screens/popups/teaching_tip.dart +++ b/example/lib/screens/popups/teaching_tip.dart @@ -28,12 +28,16 @@ class _TeachingTipPageState extends State with PageMixin { 'Bottom left': FlyoutPlacementMode.bottomLeft, 'Bottom center': FlyoutPlacementMode.bottomCenter, 'Bottom right': FlyoutPlacementMode.bottomRight, - 'Center': FlyoutPlacementMode.left, + 'Center': FlyoutPlacementMode.leftCenter, 'Top left': FlyoutPlacementMode.topLeft, 'Top center': FlyoutPlacementMode.topCenter, 'Top right': FlyoutPlacementMode.topRight, - 'Right': FlyoutPlacementMode.right, - 'Left': FlyoutPlacementMode.left, + 'Right Top': FlyoutPlacementMode.rightTop, + 'Right Center': FlyoutPlacementMode.rightCenter, + 'Right Bottom': FlyoutPlacementMode.rightBottom, + 'Left Top': FlyoutPlacementMode.leftTop, + 'Left Center': FlyoutPlacementMode.leftCenter, + 'Left Bottom': FlyoutPlacementMode.leftBottom, }; var alignment = 'Bottom center'; var placement = 'Bottom center'; diff --git a/lib/src/controls/flyouts/flyout.dart b/lib/src/controls/flyouts/flyout.dart index 245238848..7d87b0c1c 100644 --- a/lib/src/controls/flyouts/flyout.dart +++ b/lib/src/controls/flyouts/flyout.dart @@ -29,10 +29,26 @@ enum FlyoutPlacementMode { bottomRight, /// Preferred location is to the left of the target element. - left, + leftTop, + + /// Preferred location is to the left of the target element, with the top edge + /// of flyout aligned with top edge of the target element. + leftCenter, + + /// Preferred location is to the left of the target element, with the bottom + /// edge of flyout aligned with bottom edge of the target element. + leftBottom, /// Preferred location is to the right of the target element. - right, + rightTop, + + /// Preferred location is to the right of the target element, with the top edge + /// of flyout aligned with top edge of the target element. + rightCenter, + + /// Preferred location is to the right of the target element, with the bottom + /// edge of flyout aligned with bottom edge of the target element. + rightBottom, /// Preferred location is above the target element. topCenter, @@ -48,6 +64,149 @@ enum FlyoutPlacementMode { /// Fills the entire screen. The child is allowed to position itself. full; + @Deprecated( + 'Use FlyoutPlacementMode.leftCenter instead. This was deprecated in v4.11.0.', + ) + static FlyoutPlacementMode get left => FlyoutPlacementMode.leftCenter; + + @Deprecated( + 'Use FlyoutPlacementMode.rightCenter instead. This was deprecated in v4.11.0.', + ) + static FlyoutPlacementMode get right => FlyoutPlacementMode.rightCenter; + + /// Whether the placement is horizontal + bool get isHorizontal { + switch (this) { + case FlyoutPlacementMode.leftTop: + case FlyoutPlacementMode.leftCenter: + case FlyoutPlacementMode.leftBottom: + case FlyoutPlacementMode.rightTop: + case FlyoutPlacementMode.rightCenter: + case FlyoutPlacementMode.rightBottom: + return true; + default: + return false; + } + } + + /// All the horizontal modes. + static List get horizontalPlacements { + return [ + FlyoutPlacementMode.leftTop, + FlyoutPlacementMode.leftCenter, + FlyoutPlacementMode.leftBottom, + FlyoutPlacementMode.rightTop, + FlyoutPlacementMode.rightCenter, + FlyoutPlacementMode.rightBottom, + ]; + } + + /// The vertical alignment of this placement. + TextAlignVertical get verticalAlignment { + switch (this) { + case FlyoutPlacementMode.topCenter: + case FlyoutPlacementMode.topLeft: + case FlyoutPlacementMode.topRight: + case FlyoutPlacementMode.leftTop: + case FlyoutPlacementMode.rightTop: + return TextAlignVertical.top; + case FlyoutPlacementMode.bottomCenter: + case FlyoutPlacementMode.bottomLeft: + case FlyoutPlacementMode.bottomRight: + case FlyoutPlacementMode.leftBottom: + case FlyoutPlacementMode.rightBottom: + return TextAlignVertical.bottom; + case FlyoutPlacementMode.leftCenter: + case FlyoutPlacementMode.rightCenter: + case FlyoutPlacementMode.full: + return TextAlignVertical.center; + case FlyoutPlacementMode.auto: + throw UnsupportedError('Auto mode does not have a vertical alignment'); + } + } + + FlyoutPlacementMode withVerticalAlignment( + TextAlignVertical verticalAlignment, + ) { + switch (verticalAlignment) { + case TextAlignVertical.top: + switch (this) { + case FlyoutPlacementMode.bottomCenter: + return FlyoutPlacementMode.topCenter; + case FlyoutPlacementMode.bottomLeft: + return FlyoutPlacementMode.topLeft; + case FlyoutPlacementMode.bottomRight: + return FlyoutPlacementMode.topRight; + case FlyoutPlacementMode.leftBottom: + case FlyoutPlacementMode.leftCenter: + return FlyoutPlacementMode.leftTop; + case FlyoutPlacementMode.rightBottom: + case FlyoutPlacementMode.rightCenter: + return FlyoutPlacementMode.rightTop; + case FlyoutPlacementMode.topCenter: + case FlyoutPlacementMode.topLeft: + case FlyoutPlacementMode.topRight: + return this; + default: + throw UnsupportedError( + 'Unsupported vertical alignment for $this: $verticalAlignment', + ); + } + case TextAlignVertical.center: + switch (this) { + case FlyoutPlacementMode.topCenter: + case FlyoutPlacementMode.bottomCenter: + case FlyoutPlacementMode.leftCenter: + case FlyoutPlacementMode.rightCenter: + case FlyoutPlacementMode.full: + return this; + case FlyoutPlacementMode.topLeft: + case FlyoutPlacementMode.topRight: + return FlyoutPlacementMode.topCenter; + case FlyoutPlacementMode.bottomLeft: + case FlyoutPlacementMode.bottomRight: + return FlyoutPlacementMode.bottomCenter; + case FlyoutPlacementMode.leftTop: + case FlyoutPlacementMode.leftBottom: + return FlyoutPlacementMode.leftCenter; + case FlyoutPlacementMode.rightTop: + case FlyoutPlacementMode.rightBottom: + return FlyoutPlacementMode.rightCenter; + default: + throw UnsupportedError( + 'Unsupported vertical alignment for $this: $verticalAlignment', + ); + } + case TextAlignVertical.bottom: + switch (this) { + case FlyoutPlacementMode.topCenter: + return FlyoutPlacementMode.bottomCenter; + case FlyoutPlacementMode.topLeft: + return FlyoutPlacementMode.bottomLeft; + case FlyoutPlacementMode.topRight: + return FlyoutPlacementMode.bottomRight; + case FlyoutPlacementMode.leftTop: + case FlyoutPlacementMode.leftCenter: + return FlyoutPlacementMode.leftBottom; + case FlyoutPlacementMode.rightTop: + case FlyoutPlacementMode.rightCenter: + return FlyoutPlacementMode.rightBottom; + case FlyoutPlacementMode.bottomCenter: + case FlyoutPlacementMode.bottomLeft: + case FlyoutPlacementMode.bottomRight: + return this; + default: + throw UnsupportedError( + 'Unsupported vertical alignment for $this: $verticalAlignment', + ); + } + default: + throw UnsupportedError( + 'Unsupported vertical alignment $verticalAlignment', + ); + } + } + /// Resolves this placement with the current text [direction] /// /// Basic usage: @@ -76,14 +235,22 @@ enum FlyoutPlacementMode { return isRtl ? FlyoutPlacementMode.bottomRight : this; case FlyoutPlacementMode.topLeft: return isRtl ? FlyoutPlacementMode.topRight : this; - case FlyoutPlacementMode.left: - return isRtl ? FlyoutPlacementMode.right : this; case FlyoutPlacementMode.bottomRight: return isRtl ? FlyoutPlacementMode.bottomLeft : this; case FlyoutPlacementMode.topRight: return isRtl ? FlyoutPlacementMode.topLeft : this; - case FlyoutPlacementMode.right: - return isRtl ? FlyoutPlacementMode.left : this; + case FlyoutPlacementMode.leftTop: + return isRtl ? FlyoutPlacementMode.rightTop : this; + case FlyoutPlacementMode.leftCenter: + return isRtl ? FlyoutPlacementMode.rightCenter : this; + case FlyoutPlacementMode.leftBottom: + return isRtl ? FlyoutPlacementMode.rightBottom : this; + case FlyoutPlacementMode.rightTop: + return isRtl ? FlyoutPlacementMode.leftTop : this; + case FlyoutPlacementMode.rightCenter: + return isRtl ? FlyoutPlacementMode.leftCenter : this; + case FlyoutPlacementMode.rightBottom: + return isRtl ? FlyoutPlacementMode.leftBottom : this; case FlyoutPlacementMode.auto: return this; } @@ -99,9 +266,13 @@ enum FlyoutPlacementMode { case FlyoutPlacementMode.topLeft: case FlyoutPlacementMode.topRight: return EdgeInsets.only(bottom: additionalOffset); - case FlyoutPlacementMode.left: + case FlyoutPlacementMode.leftTop: + case FlyoutPlacementMode.leftCenter: + case FlyoutPlacementMode.leftBottom: return EdgeInsets.only(right: additionalOffset); - case FlyoutPlacementMode.right: + case FlyoutPlacementMode.rightTop: + case FlyoutPlacementMode.rightCenter: + case FlyoutPlacementMode.rightBottom: return EdgeInsets.only(left: additionalOffset); case FlyoutPlacementMode.auto: case FlyoutPlacementMode.full: @@ -136,12 +307,16 @@ enum FlyoutPlacementMode { maxWidth: rootSize.width._ensurePositive(), maxHeight: targetOffset.dy._ensurePositive(), ); - case FlyoutPlacementMode.left: + case FlyoutPlacementMode.leftTop: + case FlyoutPlacementMode.leftCenter: + case FlyoutPlacementMode.leftBottom: return BoxConstraints( maxWidth: targetOffset.dx._ensurePositive(), maxHeight: rootSize.height._ensurePositive(), ); - case FlyoutPlacementMode.right: + case FlyoutPlacementMode.rightTop: + case FlyoutPlacementMode.rightCenter: + case FlyoutPlacementMode.rightBottom: return BoxConstraints( maxWidth: (rootSize.width - targetOffset.dx)._ensurePositive(), maxHeight: rootSize.height._ensurePositive(), @@ -167,19 +342,23 @@ enum FlyoutPlacementMode { final availableSpace = configuration.autoAvailableSpace!; if (configuration.horizontal) { - final las = FlyoutPlacementMode.left + final las = FlyoutPlacementMode.leftCenter ._getAvailableSpace(targetOffset, rootSize, margin) .biggest; - final ras = FlyoutPlacementMode.right + final ras = FlyoutPlacementMode.rightCenter ._getAvailableSpace(targetOffset, rootSize, margin) .biggest; if (las.width >= availableSpace && ras.width >= availableSpace) { return configuration.preferredMode; } else if (las.width >= availableSpace) { - return FlyoutPlacementMode.left; + return FlyoutPlacementMode.leftCenter.withVerticalAlignment( + configuration.preferredMode.verticalAlignment, + ); } else if (ras.width >= availableSpace) { - return FlyoutPlacementMode.right; + return FlyoutPlacementMode.rightCenter.withVerticalAlignment( + configuration.preferredMode.verticalAlignment, + ); } else { return configuration.preferredMode; } @@ -195,16 +374,20 @@ enum FlyoutPlacementMode { } final isLeftPreferred = [ - FlyoutPlacementMode.left, + FlyoutPlacementMode.leftTop, + FlyoutPlacementMode.leftCenter, + FlyoutPlacementMode.leftBottom, FlyoutPlacementMode.topLeft, FlyoutPlacementMode.bottomLeft, ].contains(configuration.preferredMode); final isCenterPreferred = [ FlyoutPlacementMode.topCenter, - FlyoutPlacementMode.bottomCenter + FlyoutPlacementMode.bottomCenter, ].contains(configuration.preferredMode); final isRightPreferred = [ - FlyoutPlacementMode.right, + FlyoutPlacementMode.rightTop, + FlyoutPlacementMode.rightCenter, + FlyoutPlacementMode.rightBottom, FlyoutPlacementMode.topRight, FlyoutPlacementMode.bottomRight ].contains(configuration.preferredMode); @@ -267,16 +450,12 @@ class FlyoutAutoConfiguration { required this.preferredMode, }) : assert(preferredMode != FlyoutPlacementMode.auto), assert( - horizontal != null && horizontal - ? preferredMode == FlyoutPlacementMode.left || - preferredMode == FlyoutPlacementMode.right - : true, + horizontal != null && horizontal ? preferredMode.isHorizontal : true, 'If the mode horizontal, preferredMode must either be left or right', ), assert(autoAvailableSpace == null || !autoAvailableSpace.isNegative), horizontal = horizontal ?? - [FlyoutPlacementMode.left, FlyoutPlacementMode.right] - .contains(preferredMode); + FlyoutPlacementMode.horizontalPlacements.contains(preferredMode); } /// A delegate for computing the layout of a flyout to be displayed according to @@ -401,9 +580,11 @@ class _FlyoutPositionDelegate extends SingleChildLayoutDelegate { final bottomY = clampVertical(targetOffset.dy); + final horizontalTopY = clampVertical(topY + flyoutSize.height); final horizontalY = clampVertical( - targetOffset.dy - targetSize.height - flyoutSize.height / 4, + topY + flyoutSize.height + flyoutSize.height / 4, ); + final horizontalBottomY = clampVertical(bottomY - flyoutSize.height); final centerX = clampHorizontal( (targetOffset.dx + targetSize.width / 2) - (flyoutSize.width / 2.0), @@ -432,20 +613,36 @@ class _FlyoutPositionDelegate extends SingleChildLayoutDelegate { return Offset(centerX, bottomY); case FlyoutPlacementMode.topCenter: return Offset(centerX, topY); - case FlyoutPlacementMode.left: + case FlyoutPlacementMode.leftTop: return Offset( - clampHorizontal( - targetOffset.dx - flyoutSize.width, - ), + clampHorizontal(targetOffset.dx - flyoutSize.width), + horizontalTopY, + ); + case FlyoutPlacementMode.leftCenter: + return Offset( + clampHorizontal(targetOffset.dx - flyoutSize.width), horizontalY, ); - case FlyoutPlacementMode.right: + case FlyoutPlacementMode.leftBottom: return Offset( - clampHorizontal( - targetOffset.dx + targetSize.width, - ), + clampHorizontal(targetOffset.dx - flyoutSize.width), + horizontalBottomY, + ); + case FlyoutPlacementMode.rightTop: + return Offset( + clampHorizontal(targetOffset.dx + targetSize.width), + horizontalTopY, + ); + case FlyoutPlacementMode.rightCenter: + return Offset( + clampHorizontal(targetOffset.dx + targetSize.width), horizontalY, ); + case FlyoutPlacementMode.rightBottom: + return Offset( + clampHorizontal(targetOffset.dx + targetSize.width), + horizontalBottomY, + ); case FlyoutPlacementMode.full: return Offset(margin, margin); case FlyoutPlacementMode.auto: diff --git a/lib/src/controls/flyouts/menu_flyout.dart b/lib/src/controls/flyouts/menu_flyout.dart index d86f75a92..50905199f 100644 --- a/lib/src/controls/flyouts/menu_flyout.dart +++ b/lib/src/controls/flyouts/menu_flyout.dart @@ -320,7 +320,6 @@ class MenuFlyoutItem extends MenuFlyoutItemBase { return FlyoutListTile( margin: EdgeInsets.zero, selected: selected, - showSelectedIndicator: false, icon: leading ?? () { if (_useIconPlaceholder) return const Icon(null); diff --git a/lib/src/controls/flyouts/teaching_tip.dart b/lib/src/controls/flyouts/teaching_tip.dart index b28b3b6a2..771b2dea1 100644 --- a/lib/src/controls/flyouts/teaching_tip.dart +++ b/lib/src/controls/flyouts/teaching_tip.dart @@ -174,9 +174,15 @@ class TeachingTip extends StatelessWidget { case FlyoutPlacementMode.topRight: alignment = const Alignment(0.75, -0.75); break; - case FlyoutPlacementMode.left: - case FlyoutPlacementMode.right: - alignment = Alignment.center; + case FlyoutPlacementMode.leftTop: + case FlyoutPlacementMode.leftCenter: + case FlyoutPlacementMode.leftBottom: + alignment = const Alignment(0.75, 0.0); + break; + case FlyoutPlacementMode.rightTop: + case FlyoutPlacementMode.rightCenter: + case FlyoutPlacementMode.rightBottom: + alignment = const Alignment(-0.75, 0.0); break; default: return flyout; diff --git a/lib/src/controls/navigation/navigation_view/pane_items.dart b/lib/src/controls/navigation/navigation_view/pane_items.dart index 4a190bb98..b020572bd 100644 --- a/lib/src/controls/navigation/navigation_view/pane_items.dart +++ b/lib/src/controls/navigation/navigation_view/pane_items.dart @@ -732,9 +732,10 @@ class __PaneItemExpanderState extends State<_PaneItemExpander> flyoutController.showFlyout( placementMode: displayMode == PaneDisplayMode.compact - ? FlyoutPlacementMode.right + ? FlyoutPlacementMode.rightTop : FlyoutPlacementMode.bottomCenter, forceAvailableSpace: true, + barrierColor: Colors.transparent, builder: (context) { return MenuFlyout( items: widget.items.map((item) { @@ -925,8 +926,6 @@ class _PaneItemExpanderMenuItem extends MenuFlyoutItemBase { @override Widget build(BuildContext context) { - assert(debugCheckHasFluentTheme(context)); - final theme = FluentTheme.of(context); final navigationTheme = NavigationPaneTheme.of(context); return HoverButton( onPressed: item.enabled ? onPressed : null, @@ -943,41 +942,26 @@ class _PaneItemExpanderMenuItem extends MenuFlyoutItemBase { : navigationTheme.unselectedIconColor?.resolve(states)), size: textStyle.fontSize ?? 16.0, ); - return Container( - padding: const EdgeInsets.symmetric( - horizontal: 10.0, - vertical: 8.0, + return MenuFlyoutItem( + selected: isSelected, + onPressed: onPressed, + leading: IconTheme.merge( + data: iconTheme, + child: item.icon, ), - margin: const EdgeInsetsDirectional.only(bottom: 4.0), - decoration: BoxDecoration( - color: ButtonThemeData.uncheckedInputColor( - theme, - states, - transparentWhenNone: true, - ), - borderRadius: BorderRadius.circular(6.0), + text: DefaultTextStyle( + style: textStyle, + child: item.title ?? const SizedBox.shrink(), ), - child: Row(mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: const EdgeInsetsDirectional.only(end: 12.0), - child: IconTheme.merge( - data: iconTheme, - child: item.icon, - ), - ), - Expanded( - child: DefaultTextStyle( - style: textStyle, - child: item.title ?? const SizedBox.shrink(), - ), - ), - if (item.infoBadge != null) - Padding( + trailing: () { + if (item.infoBadge != null) { + return Padding( padding: const EdgeInsetsDirectional.only(start: 8.0), child: item.infoBadge!, - ), - ]), - ); + ); + } + }(), + ).build(context); }, ); } diff --git a/lib/src/controls/surfaces/commandbar.dart b/lib/src/controls/surfaces/commandbar.dart index ceb628c11..fcef58d8f 100644 --- a/lib/src/controls/surfaces/commandbar.dart +++ b/lib/src/controls/surfaces/commandbar.dart @@ -209,7 +209,7 @@ class CommandBarState extends State { autoModeConfiguration: FlyoutAutoConfiguration( preferredMode: (widget.direction == Axis.horizontal ? FlyoutPlacementMode.bottomRight - : FlyoutPlacementMode.right) + : FlyoutPlacementMode.rightTop) .resolve(Directionality.of(context)), ), additionalOffset: 0.0,