// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // ignore_for_file: uri_does_not_exist_in_doc_import /// @docImport 'elevated_button_theme.dart'; /// @docImport 'menu_anchor.dart'; /// @docImport 'text_button_theme.dart'; /// @docImport 'text_theme.dart'; /// @docImport 'theme.dart'; library; import 'dart:math' as math; import 'package:PiliPlus/common/widgets/dyn/ink_well.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart' hide InkWell; import 'package:flutter/rendering.dart'; /// The base [StatefulWidget] class for buttons whose style is defined by a [ButtonStyle] object. /// /// Concrete subclasses must override [defaultStyleOf] and [themeStyleOf]. /// /// See also: /// * [ElevatedButton], a filled button whose material elevates when pressed. /// * [FilledButton], a filled button that doesn't elevate when pressed. /// * [FilledButton.tonal], a filled button variant that uses a secondary fill color. /// * [OutlinedButton], a button with an outlined border and no fill color. /// * [TextButton], a button with no outline or fill color. /// * , an overview of each of /// the Material Design button types and how they should be used in designs. abstract class ButtonStyleButton extends StatefulWidget { /// Abstract const constructor. This constructor enables subclasses to provide /// const constructors so that they can be used in const expressions. const ButtonStyleButton({ super.key, required this.onPressed, required this.onLongPress, required this.onHover, required this.onFocusChange, required this.style, required this.focusNode, required this.autofocus, required this.clipBehavior, this.statesController, this.isSemanticButton = true, @Deprecated( 'Remove this parameter as it is now ignored. ' 'Use ButtonStyle.iconAlignment instead. ' 'This feature was deprecated after v3.28.0-1.0.pre.', ) this.iconAlignment, this.tooltip, required this.child, }); /// Called when the button is tapped or otherwise activated. /// /// If this callback and [onLongPress] are null, then the button will be disabled. /// /// See also: /// /// * [enabled], which is true if the button is enabled. final VoidCallback? onPressed; /// Called when the button is long-pressed. /// /// If this callback and [onPressed] are null, then the button will be disabled. /// /// See also: /// /// * [enabled], which is true if the button is enabled. final VoidCallback? onLongPress; /// Called when a pointer enters or exits the button response area. /// /// The value passed to the callback is true if a pointer has entered this /// part of the material and false if a pointer has exited this part of the /// material. final ValueChanged? onHover; /// Handler called when the focus changes. /// /// Called with true if this widget's node gains focus, and false if it loses /// focus. final ValueChanged? onFocusChange; /// Customizes this button's appearance. /// /// Non-null properties of this style override the corresponding /// properties in [themeStyleOf] and [defaultStyleOf]. [WidgetStateProperty]s /// that resolve to non-null values will similarly override the corresponding /// [WidgetStateProperty]s in [themeStyleOf] and [defaultStyleOf]. /// /// Null by default. final ButtonStyle? style; /// {@macro flutter.material.Material.clipBehavior} /// /// Defaults to [Clip.none] unless [ButtonStyle.backgroundBuilder] or /// [ButtonStyle.foregroundBuilder] is specified. In those /// cases the default is [Clip.antiAlias]. final Clip? clipBehavior; /// {@macro flutter.widgets.Focus.focusNode} final FocusNode? focusNode; /// {@macro flutter.widgets.Focus.autofocus} final bool autofocus; /// {@macro flutter.material.inkwell.statesController} final WidgetStatesController? statesController; /// Determine whether this subtree represents a button. /// /// If this is null, the screen reader will not announce "button" when this /// is focused. This is useful for [MenuItemButton] and [SubmenuButton] when we /// traverse the menu system. /// /// Defaults to true. final bool? isSemanticButton; /// {@macro flutter.material.ButtonStyleButton.iconAlignment} @Deprecated( 'Remove this parameter as it is now ignored. ' 'Use ButtonStyle.iconAlignment instead. ' 'This feature was deprecated after v3.28.0-1.0.pre.', ) final IconAlignment? iconAlignment; /// Text that describes the action that will occur when the button is pressed or /// hovered over. /// /// This text is displayed when the user long-presses or hovers over the button /// in a tooltip. This string is also used for accessibility. /// /// If null, the button will not display a tooltip. final String? tooltip; /// Typically the button's label. /// /// {@macro flutter.widgets.ProxyWidget.child} final Widget? child; /// Returns a [ButtonStyle] that's based primarily on the [Theme]'s /// [ThemeData.textTheme] and [ThemeData.colorScheme], but has most values /// filled out (non-null). /// /// The returned style can be overridden by the [style] parameter and by the /// style returned by [themeStyleOf] that some button-specific themes like /// [TextButtonTheme] or [ElevatedButtonTheme] override. For example the /// default style of the [TextButton] subclass can be overridden with its /// [TextButton.style] constructor parameter, or with a [TextButtonTheme]. /// /// Concrete button subclasses should return a [ButtonStyle] with as many /// non-null properties as possible, where all of the non-null /// [WidgetStateProperty] properties resolve to non-null values. /// /// ## Properties that can be null /// /// Some properties, like [ButtonStyle.fixedSize] would override other values /// in the same [ButtonStyle] if set, so they are allowed to be null. Here is /// a summary of properties that are allowed to be null when returned in the /// [ButtonStyle] returned by this function, an why: /// /// - [ButtonStyle.fixedSize] because it would override other values in the /// same [ButtonStyle], like [ButtonStyle.maximumSize]. /// - [ButtonStyle.side] because null is a valid value for a button that has /// no side. [OutlinedButton] returns a non-null default for this, however. /// - [ButtonStyle.backgroundBuilder] and [ButtonStyle.foregroundBuilder] /// because they would override the [ButtonStyle.foregroundColor] and /// [ButtonStyle.backgroundColor] of the same [ButtonStyle]. /// /// See also: /// /// * [themeStyleOf], returns the ButtonStyle of this button's component /// theme. @protected ButtonStyle defaultStyleOf(BuildContext context); /// Returns the ButtonStyle that belongs to the button's component theme. /// /// The returned style can be overridden by the [style] parameter. /// /// Concrete button subclasses should return the ButtonStyle for the /// nearest subclass-specific inherited theme, and if no such theme /// exists, then the same value from the overall [Theme]. /// /// See also: /// /// * [defaultStyleOf], Returns the default [ButtonStyle] for this button. @protected ButtonStyle? themeStyleOf(BuildContext context); /// Whether the button is enabled or disabled. /// /// Buttons are disabled by default. To enable a button, set its [onPressed] /// or [onLongPress] properties to a non-null value. bool get enabled => onPressed != null || onLongPress != null; @override State createState() => _ButtonStyleState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add( FlagProperty('enabled', value: enabled, ifFalse: 'disabled'), ) ..add( DiagnosticsProperty('style', style, defaultValue: null), ) ..add( DiagnosticsProperty( 'focusNode', focusNode, defaultValue: null, ), ); } /// Returns null if [value] is null, otherwise `WidgetStatePropertyAll(value)`. /// /// A convenience method for subclasses. static WidgetStateProperty? allOrNull(T? value) => value == null ? null : WidgetStatePropertyAll(value); /// Returns null if [enabled] and [disabled] are null. /// Otherwise, returns a [WidgetStateProperty] that resolves to [disabled] /// when [WidgetState.disabled] is active, and [enabled] otherwise. /// /// A convenience method for subclasses. static WidgetStateProperty? defaultColor( Color? enabled, Color? disabled, ) { if ((enabled ?? disabled) == null) { return null; } return WidgetStateProperty.fromMap({ WidgetState.disabled: disabled, WidgetState.any: enabled, }); } /// A convenience method used by subclasses in the framework, that returns an /// interpolated value based on the [fontSizeMultiplier] parameter: /// /// * 0 - 1 [geometry1x] /// * 1 - 2 lerp([geometry1x], [geometry2x], [fontSizeMultiplier] - 1) /// * 2 - 3 lerp([geometry2x], [geometry3x], [fontSizeMultiplier] - 2) /// * otherwise [geometry3x] /// /// This method is used by the framework for estimating the default paddings to /// use on a button with a text label, when the system text scaling setting /// changes. It's usually supplied with empirical [geometry1x], [geometry2x], /// [geometry3x] values adjusted for different system text scaling values, when /// the unscaled font size is set to 14.0 (the default [TextTheme.labelLarge] /// value). /// /// The `fontSizeMultiplier` argument, for historical reasons, is the default /// font size specified in the [ButtonStyle], scaled by the ambient font /// scaler, then divided by 14.0 (the default font size used in buttons). static EdgeInsetsGeometry scaledPadding( EdgeInsetsGeometry geometry1x, EdgeInsetsGeometry geometry2x, EdgeInsetsGeometry geometry3x, double fontSizeMultiplier, ) { return switch (fontSizeMultiplier) { <= 1 => geometry1x, < 2 => EdgeInsetsGeometry.lerp( geometry1x, geometry2x, fontSizeMultiplier - 1, )!, < 3 => EdgeInsetsGeometry.lerp( geometry2x, geometry3x, fontSizeMultiplier - 2, )!, _ => geometry3x, }; } } /// The base [State] class for buttons whose style is defined by a [ButtonStyle] object. /// /// See also: /// /// * [ButtonStyleButton], the [StatefulWidget] subclass for which this class is the [State]. /// * [ElevatedButton], a filled button whose material elevates when pressed. /// * [FilledButton], a filled ButtonStyleButton that doesn't elevate when pressed. /// * [OutlinedButton], similar to [TextButton], but with an outline. /// * [TextButton], a simple button without a shadow. class _ButtonStyleState extends State with TickerProviderStateMixin { AnimationController? controller; double? elevation; Color? backgroundColor; WidgetStatesController? internalStatesController; void handleStatesControllerChange() { // Force a rebuild to resolve WidgetStateProperty properties setState(() {}); } WidgetStatesController get statesController => widget.statesController ?? internalStatesController!; void initStatesController() { if (widget.statesController == null) { internalStatesController = WidgetStatesController(); } statesController ..update(WidgetState.disabled, !widget.enabled) ..addListener(handleStatesControllerChange); } @override void initState() { super.initState(); initStatesController(); } @override void didUpdateWidget(ButtonStyleButton oldWidget) { super.didUpdateWidget(oldWidget); if (widget.statesController != oldWidget.statesController) { oldWidget.statesController?.removeListener(handleStatesControllerChange); if (widget.statesController != null) { internalStatesController?.dispose(); internalStatesController = null; } initStatesController(); } if (widget.enabled != oldWidget.enabled) { statesController.update(WidgetState.disabled, !widget.enabled); if (!widget.enabled) { // The button may have been disabled while a press gesture is currently underway. statesController.update(WidgetState.pressed, false); } } } @override void dispose() { statesController.removeListener(handleStatesControllerChange); internalStatesController?.dispose(); controller?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); final IconThemeData iconTheme = IconTheme.of(context); final ButtonStyle? widgetStyle = widget.style; final ButtonStyle? themeStyle = widget.themeStyleOf(context); final ButtonStyle defaultStyle = widget.defaultStyleOf(context); T? effectiveValue(T? Function(ButtonStyle? style) getProperty) { final T? widgetValue = getProperty(widgetStyle); final T? themeValue = getProperty(themeStyle); final T? defaultValue = getProperty(defaultStyle); return widgetValue ?? themeValue ?? defaultValue; } T? resolve( WidgetStateProperty? Function(ButtonStyle? style) getProperty, ) { return effectiveValue((ButtonStyle? style) { return getProperty(style)?.resolve(statesController.value); }); } Color? effectiveIconColor() { return widgetStyle?.iconColor?.resolve(statesController.value) ?? themeStyle?.iconColor?.resolve(statesController.value) ?? widgetStyle?.foregroundColor?.resolve(statesController.value) ?? themeStyle?.foregroundColor?.resolve(statesController.value) ?? defaultStyle.iconColor?.resolve(statesController.value) ?? // Fallback to foregroundColor if iconColor is null. defaultStyle.foregroundColor?.resolve(statesController.value); } final double? resolvedElevation = resolve( (ButtonStyle? style) => style?.elevation, ); final TextStyle? resolvedTextStyle = resolve( (ButtonStyle? style) => style?.textStyle, ); Color? resolvedBackgroundColor = resolve( (ButtonStyle? style) => style?.backgroundColor, ); final Color? resolvedForegroundColor = resolve( (ButtonStyle? style) => style?.foregroundColor, ); final Color? resolvedShadowColor = resolve( (ButtonStyle? style) => style?.shadowColor, ); final Color? resolvedSurfaceTintColor = resolve( (ButtonStyle? style) => style?.surfaceTintColor, ); final EdgeInsetsGeometry? resolvedPadding = resolve( (ButtonStyle? style) => style?.padding, ); final Size? resolvedMinimumSize = resolve( (ButtonStyle? style) => style?.minimumSize, ); final Size? resolvedFixedSize = resolve( (ButtonStyle? style) => style?.fixedSize, ); final Size? resolvedMaximumSize = resolve( (ButtonStyle? style) => style?.maximumSize, ); final Color? resolvedIconColor = effectiveIconColor(); final double? resolvedIconSize = resolve( (ButtonStyle? style) => style?.iconSize, ); final BorderSide? resolvedSide = resolve( (ButtonStyle? style) => style?.side, ); final OutlinedBorder? resolvedShape = resolve( (ButtonStyle? style) => style?.shape, ); final WidgetStateMouseCursor mouseCursor = _MouseCursor( (Set states) => effectiveValue( (ButtonStyle? style) => style?.mouseCursor?.resolve(states), ), ); final WidgetStateProperty overlayColor = WidgetStateProperty.resolveWith( (Set states) => effectiveValue( (ButtonStyle? style) => style?.overlayColor?.resolve(states), ), ); final VisualDensity? resolvedVisualDensity = effectiveValue( (ButtonStyle? style) => style?.visualDensity, ); final MaterialTapTargetSize? resolvedTapTargetSize = effectiveValue( (ButtonStyle? style) => style?.tapTargetSize, ); final Duration? resolvedAnimationDuration = effectiveValue( (ButtonStyle? style) => style?.animationDuration, ); final bool resolvedEnableFeedback = effectiveValue((ButtonStyle? style) => style?.enableFeedback) ?? true; final AlignmentGeometry? resolvedAlignment = effectiveValue( (ButtonStyle? style) => style?.alignment, ); final Offset densityAdjustment = resolvedVisualDensity!.baseSizeAdjustment; final InteractiveInkFeatureFactory? resolvedSplashFactory = effectiveValue( (ButtonStyle? style) => style?.splashFactory, ); final ButtonLayerBuilder? resolvedBackgroundBuilder = effectiveValue( (ButtonStyle? style) => style?.backgroundBuilder, ); final ButtonLayerBuilder? resolvedForegroundBuilder = effectiveValue( (ButtonStyle? style) => style?.foregroundBuilder, ); final Clip effectiveClipBehavior = widget.clipBehavior ?? ((resolvedBackgroundBuilder ?? resolvedForegroundBuilder) != null ? Clip.antiAlias : Clip.none); BoxConstraints effectiveConstraints = resolvedVisualDensity .effectiveConstraints( BoxConstraints( minWidth: resolvedMinimumSize!.width, minHeight: resolvedMinimumSize.height, maxWidth: resolvedMaximumSize!.width, maxHeight: resolvedMaximumSize.height, ), ); if (resolvedFixedSize != null) { final Size size = effectiveConstraints.constrain(resolvedFixedSize); if (size.width.isFinite) { effectiveConstraints = effectiveConstraints.copyWith( minWidth: size.width, maxWidth: size.width, ); } if (size.height.isFinite) { effectiveConstraints = effectiveConstraints.copyWith( minHeight: size.height, maxHeight: size.height, ); } } // Per the Material Design team: don't allow the VisualDensity // adjustment to reduce the width of the left/right padding. If we // did, VisualDensity.compact, the default for desktop/web, would // reduce the horizontal padding to zero. final double dy = densityAdjustment.dy; final double dx = math.max(0, densityAdjustment.dx); final EdgeInsetsGeometry padding = resolvedPadding! .add(EdgeInsets.fromLTRB(dx, dy, dx, dy)) .clamp(EdgeInsets.zero, EdgeInsetsGeometry.infinity); // If an opaque button's background is becoming translucent while its // elevation is changing, change the elevation first. Material implicitly // animates its elevation but not its color. SKIA renders non-zero // elevations as a shadow colored fill behind the Material's background. if (resolvedAnimationDuration! > Duration.zero && elevation != null && backgroundColor != null && elevation != resolvedElevation && backgroundColor!.value != resolvedBackgroundColor!.value && backgroundColor!.opacity == 1 && resolvedBackgroundColor.opacity < 1 && resolvedElevation == 0) { if (controller?.duration != resolvedAnimationDuration) { controller?.dispose(); controller = AnimationController( duration: resolvedAnimationDuration, vsync: this, )..addStatusListener((AnimationStatus status) { if (status == AnimationStatus.completed) { setState(() {}); // Rebuild with the final background color. } }); } resolvedBackgroundColor = backgroundColor; // Defer changing the background color. controller!.value = 0; controller!.forward(); } elevation = resolvedElevation; backgroundColor = resolvedBackgroundColor; Widget result = Padding( padding: padding, child: Align( alignment: resolvedAlignment!, widthFactor: 1.0, heightFactor: 1.0, child: resolvedForegroundBuilder != null ? resolvedForegroundBuilder( context, statesController.value, widget.child, ) : widget.child, ), ); if (resolvedBackgroundBuilder != null) { result = resolvedBackgroundBuilder( context, statesController.value, result, ); } result = AnimatedTheme( duration: resolvedAnimationDuration, data: theme.copyWith( iconTheme: iconTheme.merge( IconThemeData(color: resolvedIconColor, size: resolvedIconSize), ), ), child: InkWell( onTap: widget.onPressed, onLongPress: widget.onLongPress, onHover: widget.onHover, mouseCursor: mouseCursor, enableFeedback: resolvedEnableFeedback, focusNode: widget.focusNode, canRequestFocus: widget.enabled, onFocusChange: widget.onFocusChange, autofocus: widget.autofocus, splashFactory: resolvedSplashFactory, overlayColor: overlayColor, highlightColor: Colors.transparent, customBorder: resolvedShape!.copyWith(side: resolvedSide), statesController: statesController, child: result, ), ); if (widget.tooltip != null) { result = Tooltip(message: widget.tooltip, child: result); } final Size minSize; switch (resolvedTapTargetSize!) { case MaterialTapTargetSize.padded: minSize = Size( kMinInteractiveDimension + densityAdjustment.dx, kMinInteractiveDimension + densityAdjustment.dy, ); assert(minSize.width >= 0.0); assert(minSize.height >= 0.0); case MaterialTapTargetSize.shrinkWrap: minSize = Size.zero; } return Semantics( container: true, button: widget.isSemanticButton, enabled: widget.enabled, child: _InputPadding( minSize: minSize, child: ConstrainedBox( constraints: effectiveConstraints, child: Material( elevation: resolvedElevation!, textStyle: resolvedTextStyle?.copyWith( color: resolvedForegroundColor, ), shape: resolvedShape.copyWith(side: resolvedSide), color: resolvedBackgroundColor, shadowColor: resolvedShadowColor, surfaceTintColor: resolvedSurfaceTintColor, type: resolvedBackgroundColor == null ? MaterialType.transparency : MaterialType.button, animationDuration: resolvedAnimationDuration, clipBehavior: effectiveClipBehavior, borderOnForeground: false, child: result, ), ), ), ); } } class _MouseCursor extends WidgetStateMouseCursor { const _MouseCursor(this.resolveCallback); final WidgetPropertyResolver resolveCallback; @override MouseCursor resolve(Set states) => resolveCallback(states)!; @override String get debugDescription => 'ButtonStyleButton_MouseCursor'; } /// A widget to pad the area around a [ButtonStyleButton]'s inner [Material]. /// /// Redirect taps that occur in the padded area around the child to the center /// of the child. This increases the size of the button and the button's /// "tap target", but not its material or its ink splashes. class _InputPadding extends SingleChildRenderObjectWidget { const _InputPadding({super.child, required this.minSize}); final Size minSize; @override RenderObject createRenderObject(BuildContext context) { return _RenderInputPadding(minSize); } @override void updateRenderObject( BuildContext context, covariant _RenderInputPadding renderObject, ) { renderObject.minSize = minSize; } } class _RenderInputPadding extends RenderShiftedBox { _RenderInputPadding(this._minSize, [RenderBox? child]) : super(child); Size get minSize => _minSize; Size _minSize; set minSize(Size value) { if (_minSize == value) { return; } _minSize = value; markNeedsLayout(); } @override double computeMinIntrinsicWidth(double height) { if (child != null) { return math.max(child!.getMinIntrinsicWidth(height), minSize.width); } return 0.0; } @override double computeMinIntrinsicHeight(double width) { if (child != null) { return math.max(child!.getMinIntrinsicHeight(width), minSize.height); } return 0.0; } @override double computeMaxIntrinsicWidth(double height) { if (child != null) { return math.max(child!.getMaxIntrinsicWidth(height), minSize.width); } return 0.0; } @override double computeMaxIntrinsicHeight(double width) { if (child != null) { return math.max(child!.getMaxIntrinsicHeight(width), minSize.height); } return 0.0; } Size _computeSize({ required BoxConstraints constraints, required ChildLayouter layoutChild, }) { if (child != null) { final Size childSize = layoutChild(child!, constraints); final double height = math.max(childSize.width, minSize.width); final double width = math.max(childSize.height, minSize.height); return constraints.constrain(Size(height, width)); } return Size.zero; } @override Size computeDryLayout(BoxConstraints constraints) { return _computeSize( constraints: constraints, layoutChild: ChildLayoutHelper.dryLayoutChild, ); } @override double? computeDryBaseline( covariant BoxConstraints constraints, TextBaseline baseline, ) { final RenderBox? child = this.child; if (child == null) { return null; } final double? result = child.getDryBaseline(constraints, baseline); if (result == null) { return null; } final Size childSize = child.getDryLayout(constraints); return result + Alignment.center .alongOffset(getDryLayout(constraints) - childSize as Offset) .dy; } @override void performLayout() { size = _computeSize( constraints: constraints, layoutChild: ChildLayoutHelper.layoutChild, ); if (child != null) { final BoxParentData childParentData = child!.parentData! as BoxParentData; childParentData.offset = Alignment.center.alongOffset( size - child!.size as Offset, ); } } @override bool hitTest(BoxHitTestResult result, {required Offset position}) { if (super.hitTest(result, position: position)) { return true; } final Offset center = child!.size.center(Offset.zero); return result.addWithRawTransform( transform: MatrixUtils.forceToPoint(center), position: center, hitTest: (BoxHitTestResult result, Offset position) { assert(position == center); return child!.hitTest(result, position: center); }, ); } }