diff --git a/lib/src/widgets/accordion/accordion.dart b/lib/src/widgets/accordion/accordion.dart index 163d92bf..3eda4a0f 100644 --- a/lib/src/widgets/accordion/accordion.dart +++ b/lib/src/widgets/accordion/accordion.dart @@ -11,7 +11,7 @@ import 'package:moon_design/src/theme/tokens/transitions.dart'; import 'package:moon_design/src/utils/color_tween_premul.dart'; import 'package:moon_design/src/utils/extensions.dart'; import 'package:moon_design/src/utils/squircle/squircle_border.dart'; -import 'package:moon_design/src/widgets/common/effects/focus_effect.dart'; +import 'package:moon_design/src/widgets/common/base_control.dart'; import 'package:moon_design/src/widgets/common/icons/icons.dart'; import 'package:moon_design/src/widgets/common/icons/moon_icon.dart'; @@ -233,10 +233,6 @@ class MoonAccordion extends StatefulWidget { class _MoonAccordionState extends State> with TickerProviderStateMixin { static final Animatable _halfTween = Tween(begin: 0.0, end: 0.5); - late final Map> _actions = { - ActivateIntent: CallbackAction(onInvoke: (_) => _handleTap()) - }; - late MoonAccordionSizeProperties _effectiveMoonAccordionSize; late BorderRadiusGeometry _effectiveBorderRadius; late EdgeInsetsGeometry _effectiveHeaderPadding; @@ -265,37 +261,11 @@ class _MoonAccordionState extends State> with TickerProvider Color? _effectiveHoverEffectColor; bool _isExpanded = false; - bool _isFocused = false; - bool _isHovered = false; FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode()); - void _handleHover(bool hover) { - if (hover != _isHovered) { - setState(() => _isHovered = hover); - - if (hover) { - _hoverAnimationController!.forward(); - } else { - _hoverAnimationController!.reverse(); - } - } - } - - void _handleFocus(bool focus) { - if (focus != _isFocused) { - setState(() => _isFocused = focus); - - if (focus) { - _hoverAnimationController!.forward(); - } else { - _hoverAnimationController!.reverse(); - } - } - } - - void _handleFocusChange(bool hasFocus) { - setState(() => _isFocused = hasFocus); + void _handleActiveState(bool isActive) { + isActive ? _hoverAnimationController!.forward() : _hoverAnimationController!.reverse(); } void _handleTap() { @@ -417,42 +387,46 @@ class _MoonAccordionState extends State> with TickerProvider ); } - Widget _buildHeader({bool isContentOutsideHeader = false, required Widget child}) { + Widget _buildDecorationContainer({required Widget child}) { final Color effectiveBorderColor = widget.borderColor ?? context.moonTheme?.accordionTheme.colors.borderColor ?? MoonColors.light.beerus; final List effectiveShadows = widget.shadows ?? context.moonTheme?.accordionTheme.shadows.shadows ?? MoonShadows.light.sm; - return RepaintBoundary( - child: AnimatedBuilder( - animation: _hoverAnimationController!, - builder: (context, child) { - return Container( - height: isContentOutsideHeader ? _effectiveHeaderHeight : null, - padding: isContentOutsideHeader ? _resolvedDirectionalHeaderPadding : null, - clipBehavior: widget.clipBehavior ?? Clip.none, - decoration: widget.decoration ?? - ((widget.hasContentOutside && isContentOutsideHeader) || - (!widget.hasContentOutside && !isContentOutsideHeader) - ? ShapeDecoration( - color: _hoverColor!.value, - shadows: effectiveShadows, - shape: MoonSquircleBorder( - side: widget.showBorder ? BorderSide(color: effectiveBorderColor) : BorderSide.none, - borderRadius: _effectiveBorderRadius.squircleBorderRadius(context), - ), - ) - : null), - child: child, - ); - }, - child: child, - ), + return MoonBaseControl( + borderRadius: _effectiveBorderRadius.squircleBorderRadius(context), + autofocus: widget.autofocus, + focusNode: _effectiveFocusNode, + onTap: _handleTap, + builder: (context, isEnabled, isHovered, isFocused, isPressed) { + final bool isActive = isHovered || isFocused; + _handleActiveState(isActive); + + return AnimatedBuilder( + animation: _hoverAnimationController!, + builder: (context, child) { + return Container( + clipBehavior: widget.clipBehavior ?? Clip.none, + decoration: widget.decoration ?? + ShapeDecoration( + color: _hoverColor!.value, + shadows: effectiveShadows, + shape: MoonSquircleBorder( + side: widget.showBorder ? BorderSide(color: effectiveBorderColor) : BorderSide.none, + borderRadius: _effectiveBorderRadius.squircleBorderRadius(context), + ), + ), + child: child, + ); + }, + child: child, + ); + }, ); } - Widget _buildChildren(BuildContext context, Widget? rootChild) { + Widget _buildContent(BuildContext context, Widget? rootChild) { _effectiveHoverEffectColor ??= context.moonEffects?.controlHoverEffect.primaryHoverColor ?? MoonEffectsTheme(tokens: MoonTokens.light).controlHoverEffect.primaryHoverColor; @@ -466,21 +440,6 @@ class _MoonAccordionState extends State> with TickerProvider _resolvedDirectionalHeaderPadding = _effectiveHeaderPadding.resolve(Directionality.of(context)); - final double effectiveFocusEffectExtent = context.moonEffects?.controlFocusEffect.effectExtent ?? - MoonEffectsTheme(tokens: MoonTokens.light).controlFocusEffect.effectExtent; - - final Color effectiveFocusEffectColor = context.moonEffects?.controlFocusEffect.effectColor ?? - MoonEffectsTheme(tokens: MoonTokens.light).controlFocusEffect.effectColor; - - final Duration effectiveFocusEffectDuration = context.moonEffects?.controlFocusEffect.effectDuration ?? - MoonEffectsTheme(tokens: MoonTokens.light).controlFocusEffect.effectDuration; - - final Curve effectiveFocusEffectCurve = context.moonEffects?.controlFocusEffect.effectCurve ?? - MoonEffectsTheme(tokens: MoonTokens.light).controlFocusEffect.effectCurve; - - final Color effectiveHoverEffectColor = context.moonEffects?.controlHoverEffect.primaryHoverColor ?? - MoonEffectsTheme(tokens: MoonTokens.light).controlHoverEffect.primaryHoverColor; - final Color effectiveBackgroundColor = widget.backgroundColor ?? context.moonTheme?.accordionTheme.colors.backgroundColor ?? MoonColors.light.gohan; @@ -509,13 +468,15 @@ class _MoonAccordionState extends State> with TickerProvider context.moonTheme?.accordionTheme.colors.expandedTrailingColor ?? MoonColors.light.textPrimary; - final TextStyle effectiveHeaderTextStyle = _effectiveMoonAccordionSize.headerTextStyle; - final Color effectiveContentTextColor = context.moonTheme?.accordionTheme.colors.contentColor ?? MoonColors.light.textPrimary; + final TextStyle effectiveHeaderTextStyle = _effectiveMoonAccordionSize.headerTextStyle; final TextStyle effectiveContentTextStyle = _effectiveMoonAccordionSize.contentTextStyle; + final Color effectiveHoverEffectColor = context.moonEffects?.controlHoverEffect.primaryHoverColor ?? + MoonEffectsTheme(tokens: MoonTokens.light).controlHoverEffect.primaryHoverColor; + final Duration effectiveHoverEffectDuration = context.moonEffects?.controlHoverEffect.hoverDuration ?? MoonEffectsTheme(tokens: MoonTokens.light).controlHoverEffect.hoverDuration; @@ -563,90 +524,82 @@ class _MoonAccordionState extends State> with TickerProvider ..begin = effectiveTrailingColor ..end = effectiveExpandedTrailingColor; - return Semantics( - label: widget.semanticLabel, - enabled: _isExpanded, - focused: _isFocused, - child: FocusableActionDetector( - actions: _actions, - focusNode: _effectiveFocusNode, - autofocus: widget.autofocus, - onFocusChange: _handleFocusChange, - onShowFocusHighlight: _handleFocus, - onShowHoverHighlight: _handleHover, - child: MoonFocusEffect( - show: _isFocused, - childBorderRadius: _effectiveBorderRadius, - effectColor: effectiveFocusEffectColor, - effectDuration: effectiveFocusEffectDuration, - effectCurve: effectiveFocusEffectCurve, - effectExtent: effectiveFocusEffectExtent, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: _handleTap, - child: _buildHeader( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _buildHeader( - isContentOutsideHeader: true, - child: Row( - children: [ - if (widget.leading != null) - IconTheme( - data: IconThemeData(color: _leadingColor!.value), - child: DefaultTextStyle( - style: effectiveHeaderTextStyle.copyWith(color: _leadingColor!.value), - child: Padding( - padding: EdgeInsetsDirectional.only(end: _resolvedDirectionalHeaderPadding.left), - child: widget.leading, - ), - ), - ), - IconTheme( - data: IconThemeData(color: _titleColor!.value), - child: DefaultTextStyle( - style: effectiveHeaderTextStyle.copyWith(color: _titleColor!.value), - child: Expanded(child: widget.title), - ), - ), - IconTheme( - data: IconThemeData(color: _trailingColor!.value), - child: DefaultTextStyle( - style: effectiveHeaderTextStyle.copyWith(color: _trailingColor!.value), - child: widget.trailing ?? _buildIcon(context)!, - ), - ), - ], - ), - ), - ClipRect( - child: Column( - children: [ - IconTheme( - data: IconThemeData(color: effectiveContentTextColor), - child: DefaultTextStyle( - style: effectiveContentTextStyle.copyWith(color: effectiveContentTextColor), - child: Align( - alignment: widget.expandedAlignment ?? Alignment.topCenter, - heightFactor: _expansionCurvedAnimation!.value, - child: rootChild, - ), - ), - ), - ], - ), - ), - ], + final Widget header = SizedBox( + height: _effectiveHeaderHeight, + child: Padding( + padding: _resolvedDirectionalHeaderPadding, + child: Row( + children: [ + if (widget.leading != null) + IconTheme( + data: IconThemeData(color: _leadingColor!.value), + child: DefaultTextStyle( + style: effectiveHeaderTextStyle.copyWith(color: _leadingColor!.value), + child: Padding( + padding: EdgeInsetsDirectional.only(end: _resolvedDirectionalHeaderPadding.left), + child: widget.leading, + ), ), ), + IconTheme( + data: IconThemeData(color: _titleColor!.value), + child: DefaultTextStyle( + style: effectiveHeaderTextStyle.copyWith(color: _titleColor!.value), + child: Expanded(child: widget.title), + ), + ), + IconTheme( + data: IconThemeData(color: _trailingColor!.value), + child: DefaultTextStyle( + style: effectiveHeaderTextStyle.copyWith(color: _trailingColor!.value), + child: widget.trailing ?? _buildIcon(context)!, + ), ), + ], + ), + ), + ); + + final Widget childWrapper = ClipRect( + child: IconTheme( + data: IconThemeData(color: effectiveContentTextColor), + child: DefaultTextStyle( + style: effectiveContentTextStyle.copyWith(color: effectiveContentTextColor), + child: Align( + alignment: widget.expandedAlignment ?? Alignment.topCenter, + heightFactor: _expansionCurvedAnimation!.value, + child: rootChild, ), ), ), ); + + return switch (widget.hasContentOutside) { + true => Semantics( + label: widget.semanticLabel, + enabled: _isExpanded, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildDecorationContainer(child: header), + childWrapper, + ], + ), + ), + false => Semantics( + label: widget.semanticLabel, + enabled: _isExpanded, + child: _buildDecorationContainer( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + header, + childWrapper, + ], + ), + ), + ), + }; } @override @@ -682,10 +635,12 @@ class _MoonAccordionState extends State> with TickerProvider ), ); - return AnimatedBuilder( - animation: _expansionAnimationController!.view, - builder: _buildChildren, - child: shouldRemoveChildren ? null : result, + return RepaintBoundary( + child: AnimatedBuilder( + animation: _expansionAnimationController!.view, + builder: _buildContent, + child: shouldRemoveChildren ? null : result, + ), ); } } diff --git a/lib/src/widgets/common/base_control.dart b/lib/src/widgets/common/base_control.dart index c765c150..b9e57056 100644 --- a/lib/src/widgets/common/base_control.dart +++ b/lib/src/widgets/common/base_control.dart @@ -96,7 +96,9 @@ class MoonBaseControl extends StatefulWidget { final FocusNode? focusNode; /// The builder that builds the child of this control. - final MoonBaseControlBuilder builder; + /// + /// This is exclusive with [child]. You cannot use both at the same time. + final MoonBaseControlBuilder? builder; /// The mouse cursor of the control. final MouseCursor cursor; @@ -107,12 +109,23 @@ class MoonBaseControl extends StatefulWidget { /// The tooltip message for this control. final String tooltipMessage; + /// The callback that is called when the control is focused or unfocused. + final void Function(bool)? onFocus; + + /// The callback that is called when the control is hovered or unhovered. + final void Function(bool)? onHover; + /// The callback that is called when the control is tapped or pressed. final VoidCallback? onTap; /// The callback that is called when the control is long-pressed. final VoidCallback? onLongPress; + /// The child of this control. + /// + /// This is exclusive with [builder]. You cannot use both at the same time. + final Widget? child; + /// MDS base control widget. const MoonBaseControl({ super.key, @@ -141,13 +154,19 @@ class MoonBaseControl extends StatefulWidget { this.pulseEffectCurve, this.scaleEffectCurve, this.focusNode, - required this.builder, + this.builder, this.cursor = SystemMouseCursors.click, this.semanticLabel, this.tooltipMessage = "", + this.onFocus, + this.onHover, this.onTap, this.onLongPress, - }); + this.child, + }) : assert( + (child == null) != (builder == null), + "You must provide either a child or a builder, not both.", + ); @override State createState() => _MoonBaseControlState(); @@ -180,12 +199,16 @@ class _MoonBaseControlState extends State { void _handleHover(bool hover) { if (hover != _isHovered) { setState(() => _isHovered = hover); + + widget.onHover?.call(hover); } } void _handleFocus(bool focus) { if (focus != _isFocused) { setState(() => _isFocused = focus); + + widget.onFocus?.call(focus); } } @@ -363,13 +386,14 @@ class _MoonBaseControlState extends State { context.moonEffects?.controlScaleEffect.effectCurve ?? MoonEffectsTheme(tokens: MoonTokens.light).controlScaleEffect.effectCurve; - final Widget child = widget.builder( - context, - _isEnabled, - _isHovered, - _isFocused, - _isPressed, - ); + final Widget child = widget.child ?? + widget.builder!( + context, + _isEnabled, + _isHovered, + _isFocused, + _isPressed, + ); return MoonTooltip( show: _canShowTooltip, @@ -381,60 +405,63 @@ class _MoonBaseControlState extends State { enabled: _isEnabled, focusable: _isEnabled, focused: _isFocused, - child: FocusableActionDetector( - enabled: _isEnabled && widget.isFocusable, - actions: _actions, - mouseCursor: _cursor, - focusNode: _effectiveFocusNode, - autofocus: _isEnabled && widget.autofocus, - onFocusChange: _handleFocusChange, - onShowFocusHighlight: _handleFocus, - onShowHoverHighlight: _handleHover, - child: GestureDetector( - excludeFromSemantics: true, - behavior: HitTestBehavior.opaque, - onTap: _handleTap, - onTapDown: _handleTapDown, - onTapUp: _handleTapUp, - onLongPress: _handleLongPress, - onLongPressStart: _handleLongPressStart, - onLongPressUp: _handleLongPressUp, - onTapCancel: _handleTapCancel, - onHorizontalDragStart: _handleHorizontalDragStart, - onHorizontalDragEnd: _handleHorizontalDragEnd, - onVerticalDragStart: _handleVerticalDragStart, - onVerticalDragEnd: _handleVerticalDragEnd, - child: TouchTargetPadding( - minSize: widget.ensureMinimalTouchTargetSize - ? Size(widget.minTouchTargetSize, widget.minTouchTargetSize) - : Size.zero, - child: RepaintBoundary( - child: AnimatedScale( - scale: _canAnimateScale ? effectiveScaleEffectScalar : 1, - duration: effectiveScaleEffectDuration, - curve: effectiveScaleEffectCurve, - child: MoonPulseEffect( - show: _canAnimatePulse, - showJiggle: widget.showPulseEffectJiggle, - childBorderRadius: widget.borderRadius, - effectColor: effectivePulseEffectColor, - effectExtent: effectivePulseEffectExtent, - effectCurve: effectivePulseEffectCurve, - effectDuration: effectivePulseEffectDuration, - child: AnimatedOpacity( - opacity: _isEnabled ? 1 : effectiveDisabledOpacityValue, - duration: context.moonTransitions?.defaultTransitionDuration ?? - MoonTransitions.transitions.defaultTransitionDuration, - curve: context.moonTransitions?.defaultTransitionCurve ?? - MoonTransitions.transitions.defaultTransitionCurve, - child: MoonFocusEffect( - show: _canAnimateFocus, - effectColor: focusColor, - effectExtent: effectiveFocusEffectExtent, - effectCurve: effectiveFocusEffectCurve, - effectDuration: effectiveFocusEffectDuration, - childBorderRadius: widget.borderRadius, - child: child, + child: AbsorbPointer( + absorbing: !_isEnabled, + child: FocusableActionDetector( + enabled: _isEnabled && widget.isFocusable, + actions: _actions, + mouseCursor: _cursor, + focusNode: _effectiveFocusNode, + autofocus: _isEnabled && widget.autofocus, + onFocusChange: _handleFocusChange, + onShowFocusHighlight: _handleFocus, + onShowHoverHighlight: _handleHover, + child: GestureDetector( + excludeFromSemantics: true, + behavior: HitTestBehavior.opaque, + onTap: _handleTap, + onTapDown: _handleTapDown, + onTapUp: _handleTapUp, + onLongPress: _handleLongPress, + onLongPressStart: _handleLongPressStart, + onLongPressUp: _handleLongPressUp, + onTapCancel: _handleTapCancel, + onHorizontalDragStart: _handleHorizontalDragStart, + onHorizontalDragEnd: _handleHorizontalDragEnd, + onVerticalDragStart: _handleVerticalDragStart, + onVerticalDragEnd: _handleVerticalDragEnd, + child: TouchTargetPadding( + minSize: widget.ensureMinimalTouchTargetSize + ? Size(widget.minTouchTargetSize, widget.minTouchTargetSize) + : Size.zero, + child: RepaintBoundary( + child: AnimatedScale( + scale: _canAnimateScale ? effectiveScaleEffectScalar : 1, + duration: effectiveScaleEffectDuration, + curve: effectiveScaleEffectCurve, + child: MoonPulseEffect( + show: _canAnimatePulse, + showJiggle: widget.showPulseEffectJiggle, + childBorderRadius: widget.borderRadius, + effectColor: effectivePulseEffectColor, + effectExtent: effectivePulseEffectExtent, + effectCurve: effectivePulseEffectCurve, + effectDuration: effectivePulseEffectDuration, + child: AnimatedOpacity( + opacity: _isEnabled ? 1 : effectiveDisabledOpacityValue, + duration: context.moonTransitions?.defaultTransitionDuration ?? + MoonTransitions.transitions.defaultTransitionDuration, + curve: context.moonTransitions?.defaultTransitionCurve ?? + MoonTransitions.transitions.defaultTransitionCurve, + child: MoonFocusEffect( + show: _canAnimateFocus, + effectColor: focusColor, + effectExtent: effectiveFocusEffectExtent, + effectCurve: effectiveFocusEffectCurve, + effectDuration: effectiveFocusEffectDuration, + childBorderRadius: widget.borderRadius, + child: child, + ), ), ), ), diff --git a/lib/src/widgets/common/effects/painters/focus_effect_painter.dart b/lib/src/widgets/common/effects/painters/focus_effect_painter.dart index d700b0d8..c60e3732 100644 --- a/lib/src/widgets/common/effects/painters/focus_effect_painter.dart +++ b/lib/src/widgets/common/effects/painters/focus_effect_painter.dart @@ -4,16 +4,18 @@ import 'package:moon_design/src/utils/color_premul_lerp.dart'; import 'package:moon_design/src/utils/squircle/squircle_radius.dart'; class FocusEffectPainter extends CustomPainter { - final Color color; final Animation animation; - final double effectExtent; + final bool isFilled; final BorderRadius borderRadius; + final Color color; + final double effectExtent; FocusEffectPainter({ - required this.color, required this.animation, - required this.effectExtent, required this.borderRadius, + this.isFilled = false, + required this.color, + required this.effectExtent, }) : super(repaint: animation); @override @@ -29,10 +31,14 @@ class FocusEffectPainter extends CustomPainter { final double heightOffset = (heightIncrease - 1) / 2; final double resolvedExtent = borderRadius != BorderRadius.zero ? (effectExtent / 2) : 0; - final Paint paint = Paint() - ..color = transformedColor - ..style = PaintingStyle.stroke - ..strokeWidth = effectExtent + 1; // +1 for squircle hairline border correction + final Paint paint = isFilled + ? (Paint() + ..color = transformedColor + ..style = PaintingStyle.fill) + : (Paint() + ..color = transformedColor + ..style = PaintingStyle.stroke + ..strokeWidth = effectExtent + 1); // +1 for squircle hairline border correction canvas.drawRRect( RRect.fromRectAndCorners(