diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e1072a31..35435a18c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [next] + +- **BREAKING** Removed `SplitButtonBar` and its related widget. Use `SplitButton` or `SplitButton.toggle` instead ([#882](https://github.com/bdlukaa/fluent_ui/pull/882), [#411](https://github.com/bdlukaa/fluent_ui/issues/411)) + ## 4.7.0 - Add Slovak localization ([#850](https://github.com/bdlukaa/fluent_ui/issues/850)) diff --git a/example/lib/screens/inputs/button.dart b/example/lib/screens/inputs/button.dart index ddc9f364e..ae9156c2c 100644 --- a/example/lib/screens/inputs/button.dart +++ b/example/lib/screens/inputs/button.dart @@ -6,8 +6,8 @@ import 'package:url_launcher/link.dart'; import '../../widgets/card_highlight.dart'; -const kSplitButtonHeight = 32.0; -const kSplitButtonWidth = 36.0; +const _kSplitButtonHeight = 32.0; +const _kSplitButtonWidth = 36.0; class ButtonPage extends StatefulWidget { const ButtonPage({super.key}); @@ -24,20 +24,43 @@ class _ButtonPageState extends State with PageMixin { bool toggleDisabled = false; bool toggleState = false; bool splitButtonDisabled = false; + bool splitButtonState = false; bool radioButtonDisabled = false; int radioButtonSelected = -1; AccentColor splitButtonColor = Colors.red; - final splitButtonFlyout = FlyoutController(); - - @override - void dispose() { - splitButtonFlyout.dispose(); - super.dispose(); - } @override Widget build(BuildContext context) { + final theme = FluentTheme.of(context); + + final splitButtonFlyout = FlyoutContent( + constraints: BoxConstraints(maxWidth: 200.0), + child: Wrap( + runSpacing: 10.0, + spacing: 8.0, + children: Colors.accentColors.map((color) { + return IconButton( + autofocus: splitButtonColor == color, + style: ButtonStyle( + padding: ButtonState.all( + EdgeInsets.all(4.0), + ), + ), + onPressed: () { + setState(() => splitButtonColor = color); + Navigator.of(context).pop(color); + }, + icon: Container( + height: _kSplitButtonHeight, + width: _kSplitButtonHeight, + color: color, + ), + ); + }).toList(), + ), + ); + return ScaffoldPage.scrollable( header: const PageHeader(title: Text('Button')), children: [ @@ -276,80 +299,51 @@ ToggleButton( ), CardHighlight( child: Row(children: [ - SizedBox( - height: 40.0, - child: SplitButtonBar(buttons: [ - Button( - style: ButtonStyle(padding: ButtonState.all(EdgeInsets.zero)), - child: Container( - decoration: BoxDecoration( - color: splitButtonDisabled - ? splitButtonColor.secondaryBrushFor( - FluentTheme.of(context).brightness, - ) - : splitButtonColor, - borderRadius: const BorderRadiusDirectional.horizontal( - start: Radius.circular(4.0), - ), - ), - height: kSplitButtonHeight, - width: kSplitButtonWidth, - ), - onPressed: splitButtonDisabled ? null : () {}, + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Text( + 'SplitButton with custom content', + style: theme.typography.caption, ), - FlyoutTarget( - controller: splitButtonFlyout, - child: IconButton( - icon: const SizedBox( - height: kSplitButtonHeight - 13.0, - width: kSplitButtonWidth - 13.0, - child: Icon(FluentIcons.chevron_down, size: 8.0), + ), + SplitButton( + enabled: !splitButtonDisabled, + child: Container( + decoration: BoxDecoration( + color: splitButtonDisabled + ? splitButtonColor.secondaryBrushFor(theme.brightness) + : splitButtonColor, + borderRadius: const BorderRadiusDirectional.horizontal( + start: Radius.circular(4.0), ), - onPressed: splitButtonDisabled - ? null - : () async { - final color = - await splitButtonFlyout.showFlyout( - autoModeConfiguration: FlyoutAutoConfiguration( - preferredMode: FlyoutPlacementMode.bottomCenter, - ), - builder: (context) { - return FlyoutContent( - constraints: BoxConstraints(maxWidth: 200.0), - child: Wrap( - runSpacing: 10.0, - spacing: 8.0, - children: Colors.accentColors.map((color) { - return Button( - autofocus: splitButtonColor == color, - style: ButtonStyle( - padding: ButtonState.all( - EdgeInsets.all(4.0), - ), - ), - onPressed: () { - Navigator.of(context).pop(color); - }, - child: Container( - height: 40.0, - width: 40.0, - color: color, - ), - ); - }).toList(), - ), - ); - }, - ); - - if (color != null) { - setState(() => splitButtonColor = color); - } - }, ), + height: _kSplitButtonHeight, + width: _kSplitButtonWidth, ), - ]), - ), + flyout: splitButtonFlyout, + ), + Padding( + padding: const EdgeInsets.only(bottom: 4.0, top: 8.0), + child: Text( + 'A toggleable SplitButton with text content', + style: theme.typography.caption, + ), + ), + SplitButton.toggle( + enabled: !splitButtonDisabled, + checked: splitButtonState, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text('Choose color'), + ), + onInvoked: () { + debugPrint('Invoked split button primary action'); + setState(() => splitButtonState = !splitButtonState); + }, + flyout: splitButtonFlyout, + ), + ]), const Spacer(), ToggleSwitch( checked: splitButtonDisabled, @@ -361,79 +355,55 @@ ToggleButton( content: const Text('Disabled'), ), ]), - codeSnippet: '''SizedBox( - height: 40.0, - child: SplitButtonBar(buttons: [ - Button( - style: ButtonStyle(padding: ButtonState.all(EdgeInsets.zero)), - child: Container( - decoration: BoxDecoration( - color: splitButtonDisabled - ? splitButtonColor.secondaryBrushFor( - FluentTheme.of(context).brightness, - ) - : splitButtonColor, - borderRadius: const BorderRadiusDirectional.horizontal( - start: Radius.circular(4.0), - ), - ), - height: kSplitButtonHeight, - width: kSplitButtonWidth, - ), - onPressed: splitButtonDisabled ? null : () {}, - ), - FlyoutTarget( - controller: splitButtonFlyout, - child: IconButton( - icon: const SizedBox( - height: kSplitButtonHeight - 13.0, - width: kSplitButtonWidth - 13.0, - child: Icon(FluentIcons.chevron_down, size: 8.0), - ), - onPressed: splitButtonDisabled - ? null - : () async { - final color = await splitButtonFlyout.showFlyout( - autoModeConfiguration: FlyoutAutoConfiguration( - preferredMode: FlyoutPlacementMode.bottomCenter, - ), - builder: (context) { - return FlyoutContent( - constraints: BoxConstraints(maxWidth: 200.0), - child: Wrap( - runSpacing: 10.0, - spacing: 8.0, - children: Colors.accentColors.map((color) { - return Button( - autofocus: splitButtonColor == color, - style: ButtonStyle( - padding: ButtonState.all( - EdgeInsets.all(4.0), - ), - ), - onPressed: () { - Navigator.of(context).pop(color); - }, - child: Container( - height: 40.0, - width: 40.0, - color: color, - ), - ); - }).toList(), - ), - ); - }, - ); + codeSnippet: '''final splitButtonKey = GlobalKey(); - if (color != null) { - setState(() => splitButtonColor = color); - } - }, +// To create a toggle button, use the [SplitButton.toggle] constructor +SplitButton( + key: splitButtonKey, + enabled: !disabled, + child: Container( + decoration: BoxDecoration( + color: disabled + ? color.secondaryBrushFor(theme.brightness) + : color, + borderRadius: const BorderRadiusDirectional.horizontal( + start: Radius.circular(4.0), ), ), - ]), -)''', + height: $_kSplitButtonHeight, + width: $_kSplitButtonWidth, + ), + flyout: FlyoutContent( + constraints: BoxConstraints(maxWidth: 200.0), + child: Wrap( + runSpacing: 10.0, + spacing: 8.0, + children: Colors.accentColors.map((color) { + return Button( + autofocus: splitButtonColor == color, + style: ButtonStyle( + padding: ButtonState.all( + EdgeInsets.all(4.0), + ), + ), + onPressed: () { + setState(() => splitButtonColor = color); + Navigator.of(context).pop(color); + }, + child: Container( + height: $_kSplitButtonHeight, + width: $_kSplitButtonHeight, + color: color, + ), + ); + }).toList(), + ), + ), +) + +// Show the flyout programmatically +splitButtonKey.currentState?.showFlyout(); +''', ), subtitle(content: const Text('RadioButton')), description( diff --git a/lib/fluent_ui.dart b/lib/fluent_ui.dart index 7c9741c24..22c4094a5 100644 --- a/lib/fluent_ui.dart +++ b/lib/fluent_ui.dart @@ -83,6 +83,7 @@ export 'src/controls/surfaces/list_tile.dart'; export 'src/controls/surfaces/progress_indicators.dart'; export 'src/controls/surfaces/snackbar.dart'; export 'src/controls/surfaces/tooltip.dart'; +export 'src/controls/utils/chevron_down.dart'; export 'src/controls/utils/divider.dart'; export 'src/controls/utils/hover_button.dart'; export 'src/controls/utils/info_badge.dart'; diff --git a/lib/src/controls/inputs/buttons/theme.dart b/lib/src/controls/inputs/buttons/theme.dart index dd060b022..6bb410690 100644 --- a/lib/src/controls/inputs/buttons/theme.dart +++ b/lib/src/controls/inputs/buttons/theme.dart @@ -232,7 +232,11 @@ class ButtonThemeData with Diagnosticable { /// Defines the default color used by [Button]s using the current brightness /// and state. - static Color buttonColor(BuildContext context, Set states) { + static Color buttonColor( + BuildContext context, + Set states, { + bool transparentWhenNone = false, + }) { final res = FluentTheme.of(context).resources; if (states.isPressing) { return res.controlFillColorTertiary; @@ -241,7 +245,9 @@ class ButtonThemeData with Diagnosticable { } else if (states.isDisabled) { return res.controlFillColorDisabled; } - return res.controlFillColorDefault; + return transparentWhenNone + ? res.subtleFillColorTransparent + : res.controlFillColorDefault; } /// Defines the default foregournd color used by [Button]s using the current brightness diff --git a/lib/src/controls/inputs/dropdown_button.dart b/lib/src/controls/inputs/dropdown_button.dart index fb1879066..4624c51e4 100644 --- a/lib/src/controls/inputs/dropdown_button.dart +++ b/lib/src/controls/inputs/dropdown_button.dart @@ -2,10 +2,7 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/foundation.dart'; const double _kVerticalOffset = 6.0; -const Widget _kDefaultDropdownButtonTrailing = Icon( - FluentIcons.chevron_down, - size: 8.0, -); +const Widget _kDefaultDropdownButtonTrailing = ChevronDown(); typedef DropDownButtonBuilder = Widget Function( BuildContext context, @@ -325,6 +322,7 @@ class DropDownButtonState extends State { widget.onOpen?.call(); await _flyoutController.showFlyout( + barrierColor: Colors.transparent, placementMode: FlyoutPlacementMode.auto, autoModeConfiguration: FlyoutAutoConfiguration( preferredMode: widget.placement, diff --git a/lib/src/controls/inputs/split_button.dart b/lib/src/controls/inputs/split_button.dart index 70b53d206..33b774231 100644 --- a/lib/src/controls/inputs/split_button.dart +++ b/lib/src/controls/inputs/split_button.dart @@ -1,218 +1,259 @@ -import 'dart:ui' show lerpDouble; - import 'package:fluent_ui/fluent_ui.dart'; -import 'package:flutter/foundation.dart'; -/// A Split Button has two parts that can be invoked separately. One part -/// behaves like a standard button and invokes an immediate action. The other -/// part invokes a flyout that contains additional options that the user can -/// choose from. Use a split button control when you want the user to be able to -/// initiate an immediate action or choose from additional options independently. +typedef SplitButtonSecondaryBuilder = Widget Function( + BuildContext context, + VoidCallback showFlyout, + FlyoutController flyoutController, +); + +enum _SplitButtonType { + normal, + + toggle +} + +/// Represents a button with two parts that can be invoked separately. One part +/// behaves like a standard button and the other part invokes a flyout. +/// +/// ![SplitButton showcase](https://learn.microsoft.com/en-us/windows/apps/design/controls/images/split-button-rtb.png) +/// +/// To show the flyout programmatically, use a [GlobalKey] to +/// invoke [SplitButtonState.showFlyout]: +/// +/// ```dart +/// final splitButtonKey = GlobalKey(); /// -/// ![SplitButton Preview](https://learn.microsoft.com/en-us/windows/apps/design/controls/images/split-button-rtb.png) +/// SplitButton( +/// key: splitButtonKey, +/// ..., +/// ), +/// +/// splitButtonKey.currentState?.showFlyout(); +/// ``` /// /// See also: /// -/// * [Button], a button gives the user a way to trigger an immediate action. -/// * [IconButton], a button with an icon -class SplitButtonBar extends StatelessWidget { - /// Creates a button bar with space in between the buttons. +/// * +/// * [DropDownButton], a button that displays a dropdown menu +class SplitButton extends StatefulWidget { + /// The type of the button + final _SplitButtonType _type; + + /// The primary widget to be displayed + final Widget child; + + /// The secondary widget to be displayed. If not provided, the default chevron + /// down icon is displayed. /// - /// It provides a [ButtonThemeData] above each button to make them - /// fell natural within the bar. - const SplitButtonBar({ - super.key, - required this.buttons, - this.style, - }) : assert(buttons.length == 2, 'There must 2 buttons'); + /// Example: + /// ```dart + /// SplitButton( + /// child: Text('Split Button'), + /// // invoke [showFlyout] to show the flyout, or use the flyoutController + /// // to display a flyout with custom options + /// secondaryBuilder: (context, showFlyout, flyoutController) { + /// return IconButton( + /// icon: const ChevronDown(), + /// onPressed: showFlyout, + /// ); + /// }, + /// flyout: Container( + /// width: 200, + /// height: 200, + /// color: Colors.white, + /// ), + /// ), + /// ``` + /// + /// See also: + /// + /// * [ChevronDown], the default icon used + /// * [flyout], the widget to be displayed when the flyout is requested + final SplitButtonSecondaryBuilder? secondaryBuilder; - /// The buttons in this button bar. Must be only two buttons + /// The widget to be displayed when the flyout is requested /// - /// Usually a List of [Button]s - final List buttons; + /// Usually a [FlyoutContent] or a [MenuFlyout] + final Widget flyout; - /// The style applied to this button bar. If non-null, it's - /// merged with [FluentThemeData.splitButtonThemeData] - final SplitButtonThemeData? style; + /// When the primary part of the button is invoked + final VoidCallback? onInvoked; - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(IntProperty('buttonsAmount', buttons.length)) - ..add(DiagnosticsProperty('style', style)); - } + /// Whether the button is enabled + final bool enabled; - @override - Widget build(BuildContext context) { - assert(debugCheckHasFluentTheme(context)); - final theme = FluentTheme.of(context); - final style = SplitButtonTheme.of(context).merge(this.style); - return Row( - mainAxisSize: MainAxisSize.min, - children: List.generate(buttons.length, (index) { - final buttonStyle = index == buttons.length - 1 - ? style.actionButtonStyle - : style.primaryButtonStyle; - final button = ButtonTheme.merge( - data: ButtonThemeData.all( - ButtonStyle( - shape: ButtonState.all( - RoundedRectangleBorder( - side: BorderSide( - color: theme.resources.controlFillColorDisabled - .withOpacity(0.75), - width: 0.1, - ), - borderRadius: BorderRadiusDirectional.horizontal( - start: index == 0 - ? style.borderRadius?.topLeft ?? Radius.zero - : Radius.zero, - end: index == buttons.length - 1 - ? style.borderRadius?.topRight ?? Radius.zero - : Radius.zero, - ), - ), - ), - ).merge(buttonStyle), - ), - child: FocusTheme( - data: const FocusThemeData(renderOutside: false), - child: buttons[index], - ), - ); - if (index == 0) return button; - return Padding( - padding: EdgeInsetsDirectional.only(start: style.interval ?? 0), - child: button, - ); - }), - ); - } -} + /// Whether the split button is checked + final bool checked; -class SplitButtonTheme extends InheritedTheme { - /// Creates a button theme that controls how descendant [SplitButtonBar]s should - /// look like. - const SplitButtonTheme({ + /// Creates a split button + const SplitButton({ super.key, - required super.child, - required this.data, - }); - - final SplitButtonThemeData data; - - /// Creates a button theme that controls how descendant [SplitButtonBar]s should - /// look like, and merges in the current button theme, if any. - static Widget merge({ - Key? key, - required SplitButtonThemeData data, - required Widget child, - }) { - return Builder(builder: (BuildContext context) { - return SplitButtonTheme( - key: key, - data: _getInheritedSplitButtonThemeData(context).merge(data), - child: child, - ); - }); - } + required this.child, + this.secondaryBuilder, + required this.flyout, + this.onInvoked, + this.enabled = true, + }) : _type = _SplitButtonType.normal, + checked = false; - /// The data from the closest instance of this class that encloses the given - /// context. - /// - /// Defaults to [FluentThemeData.splitButtonTheme] - /// - /// Typical usage is as follows: - /// - /// ```dart - /// SplitButtonThemeData theme = SplitButtonTheme.of(context); - /// ``` - static SplitButtonThemeData of(BuildContext context) { - assert(debugCheckHasFluentTheme(context)); - return SplitButtonThemeData.standard(FluentTheme.of(context)).merge( - _getInheritedSplitButtonThemeData(context), - ); - } - - static SplitButtonThemeData _getInheritedSplitButtonThemeData( - BuildContext context) { - final checkboxTheme = - context.dependOnInheritedWidgetOfExactType(); - return checkboxTheme?.data ?? FluentTheme.of(context).splitButtonTheme; - } + /// Creates a split toggle button + const SplitButton.toggle({ + super.key, + required this.child, + required this.checked, + this.secondaryBuilder, + required this.flyout, + this.onInvoked, + this.enabled = true, + }) : _type = _SplitButtonType.toggle; @override - Widget wrap(BuildContext context, Widget child) { - return SplitButtonTheme(data: data, child: child); - } + State createState() => SplitButtonState(); +} + +class SplitButtonState extends State { + late final FlyoutController flyoutController = FlyoutController(); + + bool _showFocusHighlight = false; @override - bool updateShouldNotify(SplitButtonTheme oldWidget) { - return oldWidget.data != data; + void dispose() { + flyoutController.dispose(); + super.dispose(); } -} -@immutable -class SplitButtonThemeData with Diagnosticable { - final BorderRadius? borderRadius; - final double? interval; - - final ButtonStyle? primaryButtonStyle; - final ButtonStyle? actionButtonStyle; - - const SplitButtonThemeData({ - this.borderRadius, - this.interval, - this.primaryButtonStyle, - this.actionButtonStyle, - }); - - factory SplitButtonThemeData.standard(FluentThemeData theme) { - return SplitButtonThemeData( - borderRadius: BorderRadius.circular(4), - interval: 1, - primaryButtonStyle: ButtonStyle( - padding: ButtonState.all(EdgeInsets.zero), - ), - actionButtonStyle: ButtonStyle( - padding: ButtonState.all(const EdgeInsets.all(6)), + /// Shows the flyout attached to the dropdown button + void showFlyout() async { + setState(() {}); + await flyoutController.showFlyout( + barrierColor: Colors.transparent, + autoModeConfiguration: FlyoutAutoConfiguration( + preferredMode: FlyoutPlacementMode.bottomCenter, ), + builder: (context) { + return widget.flyout; + }, ); + if (mounted) setState(() {}); } - static SplitButtonThemeData lerp( - SplitButtonThemeData? a, - SplitButtonThemeData? b, - double t, - ) { - return SplitButtonThemeData( - borderRadius: BorderRadius.lerp(a?.borderRadius, b?.borderRadius, t), - interval: lerpDouble(a?.interval, b?.interval, t), - primaryButtonStyle: - ButtonStyle.lerp(a?.primaryButtonStyle, b?.primaryButtonStyle, t), - actionButtonStyle: - ButtonStyle.lerp(a?.actionButtonStyle, b?.actionButtonStyle, t), - ); - } - - SplitButtonThemeData merge(SplitButtonThemeData? style) { - return SplitButtonThemeData( - borderRadius: style?.borderRadius ?? borderRadius, - interval: style?.interval ?? interval, - primaryButtonStyle: style?.primaryButtonStyle ?? primaryButtonStyle, - actionButtonStyle: style?.actionButtonStyle ?? actionButtonStyle, - ); + void _updateFocusHighlight(bool focused) { + setState(() => _showFocusHighlight = focused); } @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty( - 'borderRadius', borderRadius)) - ..add(DoubleProperty('interval', interval)) - ..add(DiagnosticsProperty('primaryButtonStyle', primaryButtonStyle)) - ..add(DiagnosticsProperty('actionButtonStyle', actionButtonStyle)); + Widget build(BuildContext context) { + assert(debugCheckHasFluentTheme(context)); + final theme = FluentTheme.of(context); + final radius = BorderRadius.circular(6.0); + + return FocusBorder( + focused: _showFocusHighlight, + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: radius, + color: theme.resources.controlFillColorDefault, + border: Border.all( + color: widget.checked + ? theme.resources.subtleFillColorTransparent + : theme.resources.controlStrokeColorDefault, + ), + ), + child: ClipRRect( + borderRadius: radius, + child: IntrinsicHeight( + child: Row(mainAxisSize: MainAxisSize.min, children: [ + HoverButton( + onPressed: widget.enabled ? widget.onInvoked : null, + onFocusChange: _updateFocusHighlight, + focusEnabled: widget._type == _SplitButtonType.toggle || + widget.secondaryBuilder != null, + builder: (context, states) { + return DecoratedBox( + decoration: BoxDecoration( + color: widget.checked + ? ButtonThemeData.checkedInputColor(theme, states) + : ButtonThemeData.buttonColor( + context, + widget.enabled && + widget.onInvoked == null && + states.isDisabled + ? {} + : states, + transparentWhenNone: true, + ), + ), + child: DefaultTextStyle.merge( + style: widget.checked + ? TextStyle( + color: + FilledButton.foregroundColor(theme, states), + ) + : null, + child: IconTheme.merge( + data: IconThemeData( + color: widget.checked + ? FilledButton.foregroundColor(theme, states) + : null, + ), + child: widget.child, + ), + ), + ); + }, + ), + const Divider( + direction: Axis.vertical, + style: DividerThemeData( + horizontalMargin: EdgeInsets.zero, + verticalMargin: EdgeInsets.zero, + ), + ), + if (widget.secondaryBuilder == null) + HoverButton( + onPressed: widget.enabled ? showFlyout : null, + onFocusChange: _updateFocusHighlight, + focusEnabled: widget._type == _SplitButtonType.normal, + builder: (context, states) { + return FlyoutTarget( + controller: flyoutController, + child: Container( + color: widget.checked + ? ButtonThemeData.checkedInputColor(theme, states) + : ButtonThemeData.buttonColor( + context, + flyoutController.isOpen + ? {ButtonStates.pressing} + : states, + transparentWhenNone: true, + ), + padding: const EdgeInsetsDirectional.symmetric( + horizontal: 12.0, + ), + alignment: Alignment.center, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 100), + opacity: flyoutController.isOpen ? 0.5 : 1, + child: ChevronDown( + iconColor: widget.checked + ? FilledButton.foregroundColor(theme, states) + : null, + ), + ), + ), + ); + }, + ) + else + widget.secondaryBuilder!( + context, + showFlyout, + flyoutController, + ), + ]), + ), + ), + ), + ); } } diff --git a/lib/src/controls/utils/chevron_down.dart b/lib/src/controls/utils/chevron_down.dart new file mode 100644 index 000000000..819ebd10b --- /dev/null +++ b/lib/src/controls/utils/chevron_down.dart @@ -0,0 +1,51 @@ +import 'package:fluent_ui/fluent_ui.dart'; + +/// The chevron down icon. +/// +/// It reacts to change in the current button state and reflect them accordingly +class ChevronDown extends StatelessWidget { + /// The icon size + final double iconSize; + + /// The icon to be displayed + final IconData icon; + + /// The color of the icon + final Color? iconColor; + + /// Creates a chevron down icon. + const ChevronDown({ + super.key, + this.iconSize = 8.0, + this.icon = FluentIcons.chevron_down, + this.iconColor, + }); + + @override + Widget build(BuildContext context) { + final states = HoverButton.maybeOf(context)?.states; + + return TweenAnimationBuilder( + duration: const Duration(milliseconds: 50), + curve: Curves.ease, + tween: Tween( + begin: 1, + end: states == null || !states.isPressing ? 1 : 0.9, + ), + child: Icon(icon, size: iconSize, color: iconColor), + builder: (context, value, child) { + return Opacity( + opacity: value.clamp(0.0, 1.0), + child: Transform.translate( + filterQuality: FilterQuality.high, + offset: Offset(0, value == 1 ? 0 : value * 1), + child: Transform.scale( + scale: value, + child: child, + ), + ), + ); + }, + ); + } +} diff --git a/lib/src/controls/utils/hover_button.dart b/lib/src/controls/utils/hover_button.dart index ebd53ec8c..52c2bd307 100644 --- a/lib/src/controls/utils/hover_button.dart +++ b/lib/src/controls/utils/hover_button.dart @@ -7,6 +7,28 @@ typedef ButtonStateWidgetBuilder = Widget Function( Set state, ); +class _HoverButtonInherited extends InheritedWidget { + const _HoverButtonInherited({ + required super.child, + required this.states, + }); + + final Set states; + + static _HoverButtonInherited of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType<_HoverButtonInherited>()!; + } + + static _HoverButtonInherited? maybeOf(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType<_HoverButtonInherited>(); + } + + @override + bool updateShouldNotify(_HoverButtonInherited oldWidget) { + return states != oldWidget.states; + } +} + /// Base widget for any widget that requires input. class HoverButton extends StatefulWidget { /// Creates a hover button. @@ -143,8 +165,12 @@ class HoverButton extends StatefulWidget { @override State createState() => _HoverButtonState(); - static _HoverButtonState of(BuildContext context) { - return context.findAncestorStateOfType<_HoverButtonState>()!; + static _HoverButtonInherited of(BuildContext context) { + return _HoverButtonInherited.of(context); + } + + static _HoverButtonInherited? maybeOf(BuildContext context) { + return _HoverButtonInherited.maybeOf(context); } } @@ -315,6 +341,11 @@ class _HoverButtonState extends State { ), ); if (widget.margin != null) w = Padding(padding: widget.margin!, child: w); + + w = _HoverButtonInherited( + states: states, + child: w, + ); return w; } } diff --git a/lib/src/styles/theme.dart b/lib/src/styles/theme.dart index 2087816b0..51a0fbad3 100644 --- a/lib/src/styles/theme.dart +++ b/lib/src/styles/theme.dart @@ -215,7 +215,6 @@ class FluentThemeData with Diagnosticable { final RadioButtonThemeData radioButtonTheme; final ScrollbarThemeData scrollbarTheme; final SliderThemeData sliderTheme; - final SplitButtonThemeData splitButtonTheme; final SnackbarThemeData snackbarTheme; final ToggleButtonThemeData toggleButtonTheme; final ToggleSwitchThemeData toggleSwitchTheme; @@ -253,7 +252,6 @@ class FluentThemeData with Diagnosticable { ChipThemeData? chipTheme, ToggleSwitchThemeData? toggleSwitchTheme, IconThemeData? iconTheme, - SplitButtonThemeData? splitButtonTheme, ContentDialogThemeData? dialogTheme, TooltipThemeData? tooltipTheme, DividerThemeData? dividerTheme, @@ -307,7 +305,6 @@ class FluentThemeData with Diagnosticable { iconTheme ??= isLight ? const IconThemeData(color: Colors.black, size: 18.0) : const IconThemeData(color: Colors.white, size: 18.0); - splitButtonTheme ??= const SplitButtonThemeData(); dialogTheme ??= const ContentDialogThemeData(); tooltipTheme ??= const TooltipThemeData(); dividerTheme ??= const DividerThemeData(); @@ -359,7 +356,6 @@ class FluentThemeData with Diagnosticable { radioButtonTheme: radioButtonTheme, scrollbarTheme: scrollbarTheme, sliderTheme: sliderTheme, - splitButtonTheme: splitButtonTheme, toggleButtonTheme: toggleButtonTheme, toggleSwitchTheme: toggleSwitchTheme, tooltipTheme: tooltipTheme, @@ -397,7 +393,6 @@ class FluentThemeData with Diagnosticable { required this.toggleSwitchTheme, required this.bottomNavigationTheme, required this.iconTheme, - required this.splitButtonTheme, required this.dialogTheme, required this.tooltipTheme, required this.dividerTheme, @@ -460,8 +455,6 @@ class FluentThemeData with Diagnosticable { toggleSwitchTheme: ToggleSwitchThemeData.lerp( a.toggleSwitchTheme, b.toggleSwitchTheme, t), iconTheme: IconThemeData.lerp(a.iconTheme, b.iconTheme, t), - splitButtonTheme: - SplitButtonThemeData.lerp(a.splitButtonTheme, b.splitButtonTheme, t), dialogTheme: ContentDialogThemeData.lerp(a.dialogTheme, b.dialogTheme, t), tooltipTheme: TooltipThemeData.lerp(a.tooltipTheme, b.tooltipTheme, t), dividerTheme: DividerThemeData.lerp(a.dividerTheme, b.dividerTheme, t), @@ -534,7 +527,6 @@ class FluentThemeData with Diagnosticable { ChipThemeData? chipTheme, ToggleSwitchThemeData? toggleSwitchTheme, IconThemeData? iconTheme, - SplitButtonThemeData? splitButtonTheme, ContentDialogThemeData? dialogTheme, TooltipThemeData? tooltipTheme, DividerThemeData? dividerTheme, @@ -594,7 +586,6 @@ class FluentThemeData with Diagnosticable { radioButtonTheme: this.radioButtonTheme.merge(radioButtonTheme), scrollbarTheme: this.scrollbarTheme.merge(scrollbarTheme), sliderTheme: this.sliderTheme.merge(sliderTheme), - splitButtonTheme: this.splitButtonTheme.merge(splitButtonTheme), toggleButtonTheme: this.toggleButtonTheme.merge(toggleButtonTheme), toggleSwitchTheme: this.toggleSwitchTheme.merge(toggleSwitchTheme), tooltipTheme: this.tooltipTheme.merge(tooltipTheme),